引入

在进行socket编程的第一步,一定是调用socket来创建一个句柄

  int socket(int domain, int type, int protocol);

那么,这个socket是怎么实现的呢?

ps:下文中的family与domain是同一个意思

应用层API详解

API

在一个典型的客户端/服务器常见中,应用程序使用socket进行通信的方式如下:

  • 各个应用程序创建一个socket。socket是一个允许通信的"设备",两个应用程序都需要用到它
  • 服务器将自己的socket绑定到一个众所周知的地址上使得客户端能够定位到它的位置

使用socket()系统调用能够创建一个socket,它返回一个用来在后继系统调用中应用该socket的文件描述符

NAME
       socket -创建一个新 socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int socket(int domain, int type, int protocol);

DESCRIPTION
	  domain: 通信域
	  
	  type:类型
	  
	  protocal:通信范围

RETURN VALUE
	  如果正确,返回一一个非负int类型的值,表示该socket实例唯一标识符的文件描述符。这个值可以调用其他系统调用来进行各种操作,比如绑定和监听端口、发送数据等。套接字将处于close状态

	  如果失败,返回-1,并设置error

(1)socket存在于一个通信domain中,它用于确定:

  • 识别出一个socket的方法(即 socket“地址”的格式)
  • 通信范围(即是在位于同一主机上的应用程序之间还是在位于使用一个网络连接起来的不同主机上的应用程序之间)

其取值如下

domain说明应用程序间的通信地址格式
AF_INETIPv4协议通过 IPv4 网络连接起来的主机128 位 IPv6 地址+16 位端口号
AF_INET6IPv6协议通过 IPv6 网络连接起来的主机32 位 IPv4 地址+16 位端口号
AF_UNIX, AF_LOCALUnix域协议同一主机(内核中)路径名
AF_ROUTE路由套接字
AF_KEY密钥套接字

关于AF和PF:AF_前缀表示地址族,PF_前缀表示协议族 ,因为历史上曾想让一个协议族(PF)支持多个地址族(AF),用PF来创建套接字,用AF来创建套接字地址结构,然而就只是想想,没有实现。现在AF和PF的值是相等的。

现代操作系统至少应该支持如下domain:

  • UNIX (AF_UNIX) domain运行在同一主机上的应用程序之间进行通信
  • IPv4 (AF_INET) domain 允许在使用因特网协议第 4 版(IPv4)网络连接起来的主机上的应用程序之间进行通信
  • IPv6 (AF_INET6) domain 允许在使用因特网协议第 6 版(IPv6)网络连接起来的主机上的应用程序之间进行通信

(2)type取值

type说明
SOCK_STREAM字节流套接字
SOCK_DGRAM数据报套接字
SOCK_SEQPACKET有序分组套接字
SOCK_RAM原始套接字

注意:自Linux内核2.6.17起,type参数可以接受SOCK_STREAM和SOCK_DGRAM类型与SOCK_NONBLOCK、SOCK_CLOEXEC相与。它们分别表示将新创建的socket设为非阻塞的从而无需通过调用 fcntl()来取得同样的结果),以及用fork调用创建子进程时在子进程中关闭该socket。

每个 socket 实现都至少提供了两种 socket:流和数据报,其属性总结如下:

属性流数据报
可靠地递送?是否
消息边界保留?否是
面向连接?是否

流socket(SOCK_STREAM)提供了一个可靠的双向字节流通信信道:

  • 可靠的:表示可以保证发送者传输的数据会完整无缺的到达接收应用程序(假设网络链接和接收者都不会崩溃)或者收到一个传输失败的通知
  • 双向的:表示数据可以在两个socket之间的任意方向上传输
  • 字节流:表示与管道一样一寸照消息边界的概念

(3)protocol取值

protocol说明
IPPROTO_CP(ipproto)TCP传输协议
IPPROTE_UDPUDP传输协议
IPPROTO_SCTPSCTP传输协议

一般都设置为0,因为前两个参数已经完全决定了它的值

探讨:domain之判断套接字类型-AF_INET、AF_INET6 或 AF_UNIX

/**
 * 取得套接字的类型:是网络套接字还是域套接字
 * @param fd {ACL_SOCKET} 网络套接字
 * @return {int} -1: 表示出错或输入非法或非套接字; >= 0 表示成功获得套接字
 *  类型,返回值有 AF_INET、AF_INET6 或 AF_UNIX(仅限 UNIX 平台)
 */
int acl_getsocktype(ACL_SOCKET fd)
{
	ACL_SOCKADDR addr;
	struct sockaddr *sa = (struct sockaddr*) &addr;
	socklen_t len = sizeof(addr);

	if (fd == ACL_SOCKET_INVALID) {
		return -1;
	}

	if (getsockname(fd, sa, &len) == -1) {
		return -1;
	}

#ifdef	ACL_UNIX
	if (sa->sa_family == AF_UNIX) {
		return AF_UNIX;
	}
#endif

#ifdef AF_INET6
	if (sa->sa_family == AF_INET || sa->sa_family == AF_INET6) {
#else
	if (sa->sa_family == AF_INET) {
#endif
		return sa->sa_family;
	}
	return -1;
}

探讨:type之socket分为【流socket和数据报socket】

套接字分为两种:

  • 流socket:
    • 流socket的正常通信需要一对相互连接的socket
    • 因此流socket被称为面向连接的。
  • 报文socket:
    • 数据报socket(SOCK_DGRAM)允许数据以被称为数据报的消息的形式进行交换。
    • 在数据报socket中,消息边界得到了保留,但数据传输是不可靠的,消息的到达可能是无序的、重复或者根本就无法到达的
    • 数据报socket是无连接的socket。与流socket不同,一个数据报socket在使用时无需与另一个socket连接。

在 Internet domain 中,数据报 socket 使用了用户数据报协议(UDP),而流 socket 则(通常)使用了传输控制协议(TCP)。一般来讲,在称呼这两种 socket 时不会使用术语“Internet domain数据报 socket”和“Internet domain 流 socket”,而是分别使用术语“UDP socket”和“TCP socket”

探讨:流socket分为【主动套接字/被动套接字】

流socket通常可以分为主动和被动两种:

  • 默认情况下,使用socket()是主动的。一个主动socket可以用在connect()调用中来建立一个到一个被动 socket 的连接。这种行为被称为执行一个主动的打开。
  • 一个被动 socket(也被称为监听 socket)是一个通过调用 listen()以被标记成允许接入连接的 socket。接受一个接入连接通常被称为执行一个被动的打开。

在大多数使用流 socket 的应用程序中,服务器会执行被动式打开,而客户端会执行主动式打开。

在这里插入图片描述

下面 可以判断 :是监听套接字(listen)还是非监听套接字(connect)

/**
 * 检查套接字:是监听套接字还是网络套接字
 * @param fd {ACL_SOCKET} 套接字句柄
 * @return {int} 返回 -1 表示该句柄非套接字,1 为监听套接字,0 为非监听套接字
 */
int acl_check_socket(ACL_SOCKET fd);

/**
 * 判断套接字是否为监听套接字
 * @param fd {ACL_SOCKET} 套接字句柄
 * @return {int} 返回值 0 表示非监听套接字,非 0 表示为监听套接字
 */
int acl_is_listening_socket(ACL_SOCKET fd);


int acl_check_socket(ACL_SOCKET fd)
{
	int val, ret;
	socklen_t len = sizeof(val);

	ret = getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, (void*) &val, &len);
	if (ret == -1) {
		return -1;
	} else if (val) {
		return 1;
	} else {
		return 0;
	}
}

int acl_is_listening_socket(ACL_SOCKET fd)
{
	return acl_check_socket(fd) == 1;
}

一对连接的流socket在两个端点之间提供了一个双向通信通道。如下:
在这里插入图片描述
连接流socket上IO的语义与管道上IO的语义类似:

  • 要执行IO需要使用read()和write()系统调用(或者socket特有的send()和recv()调用)。由于socket是双向的,因此在连接的两端都可以使用这两个调用。
  • 一个socket可以使用close()系统调用来关闭或者在应用程序终止之后关闭。
    • 之后当对等的应用程序试图从连接的另一端读取数据时将会收到文件结束(当所有缓冲数据都被读取之后)。
    • 如果对等应用程序试图向其socket写入数据,那么它就会收到一个SIGPIPE信号,并且返回EPIPE错误。
    • 处理这种错误的常见方法是忽略 SIGPIPE 信号并通过 EPIPE 错误找出被关闭的连接。

探讨:如何探测套接字中系统缓存区的数据长度

  • 在学习ioctl 时常常跟 read, write 混淆。其实 ioctl 是用来设置硬件控制寄存器,或者读取硬件状态寄存器的数值之类的。
  • 而read,write 是把数据丢入缓冲区,硬件的驱动从缓冲区读取数据一个个发送或者把接收的数据送入缓冲区。
 ioctl(keyFd, FIONREAD, &b)
  • 得到缓冲区里有多少字节要被读取,然后将字节数放入b里面。接下来就可以用read了。
read(keyFd, &b, sizeof(b))

可以,可以通过ioctl探测套接字中系统缓存区的数据长度

/**
 * 探测套接字中系统缓存区的数据长度
 * @param fd {ACL_SOCKET} 描述符
 * @return {int} 系统缓存区数据长度
 */
int acl_peekfd(ACL_SOCKET fd)
{
	int     count;

	/*
	 * Anticipate a series of system-dependent code fragments.
	 */
#ifdef ACL_UNIX
	return (ioctl(fd, FIONREAD, (char *) &count) < 0 ? -1 : count);
#elif defined(ACL_WINDOWS)
	return (ioctlsocket(fd, FIONREAD, (unsigned long *) &count) < 0
		? -1 : count);
#endif
}

内核层实现详解

SYSCALL_DEFINE3

socket系统调用的定义如下:

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
	int retval;
	struct socket *sock;
	int flags;
......
	if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
		flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
 
	sock = sock_create(family, type, protocol, &sock);
......
	retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
......
	return retval;
}

可以看到,除去一些参数合法性校验,socket函数主要由sock_create和sock_map_fd这两个函数完成,因此我们主要分析这两个函数,看它们是怎么在内核一步步创建和管理我们使用的socket。

接下来,我们打开 sock_create 函数看一下。

(1)sock_create()

sock_create() 实际调用的是 __sock_create()。

int __sock_create(struct net *net, int family, int type, int protocol,
			 struct socket **res, int kern)
{
	int err;
	struct socket *sock;
	const struct net_proto_family *pf;
......
	sock = sock_alloc();
......
	sock->type = type;
......
	pf = rcu_dereference(net_families[family]);
......
	err = pf->create(net, sock, protocol, kern);
......    
	*res = sock;
 
	return 0;
}

这里比较重要的是sock_alloc()和pf->create()这两个函数。其中sock_alloc()里体现了linux一切皆文件(Everything is a file)理念,即使用文件系统来管理socket,这也是VFS所要达到的效果。

第一步:先是分配了一个struct socket

 sock = sock_alloc();//创建struct socket结构体

第二步:然后设置这个sock的类型是字节流套接字还是数据报套接字

  sock->type = type;//设置套接字的类型

第三步:获取对应协议族的协议实例对象

这里需要用到family参数。

分析
  • 上面我们知道,family的取值可以是AF_INET等,比如:
/* Supported address families. */
#define AF_UNSPEC	0
#define AF_UNIX		1	/* Unix domain sockets 		*/
#define AF_LOCAL	1	/* POSIX name for AF_UNIX	*/
#define AF_INET		2	/* Internet IP Protocol 	*/
......
#define AF_INET6	10	/* IP version 6			*/
......
#define AF_MPLS		28	/* MPLS */
......
#define AF_MAX		44	/* For now.. */
#define NPROTO		AF_MAX
  • 而family参数是用来作为net_families数组的下标的
	pf = rcu_dereference(net_families[family]);
  • net_families数组定义如下:
static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
  • 在net_families数组中,每一个地址族在这个数组里面都有一项,里面的内容是net_proto_family。也就是说,每一种地址族都有自己的net_proto_family。
struct net_proto_family {
	int		family;
	int		(*create)(struct net *net, struct socket *sock,
				  int protocol, int kern);
	struct module	*owner;
};
  • 也就是说,net_families[family])会返回返回其对应的const struct net_proto_family
  • 一般来将,参数都是AF_INET/AF_INET6,我们来看下IP地址族的net_proto_family 是怎么定义的。如下,里面最重要的是,create函数指向的inet_create:
// net/ipv4/af_inet.c
static const struct net_proto_family inet_family_ops = {
	.family = PF_INET,
	.create = inet_create,// 这个用于 socket 系统调用创建
......
}

  • 这个inet_family_ops就是返回的pf
小结

net_families[]数组的初始化在inet_init()函数,

static const struct net_proto_family inet_family_ops = {
    .family = PF_INET,
    .create = inet_create,
    .owner  = THIS_MODULE,
};

static int __init inet_init(void)
{
...
    (void)sock_register(&inet_family_ops);
...
}

int sock_register(const struct net_proto_family *ops)
{
...
    rcu_assign_pointer(net_families[ops->family], ops);
...
}

因此,net_families[]数组里存放的是各个协议族的信息,以family字段作为下标。此处我们针对TCP协议分析,因此我们family字段是AF_INET,另外一般会有

#define PF_INET     AF_INET

所以pf->create调用的就是inet_create()函数。

第四步:inet_create

现在我们回到函数__sock_create。得到pf之后,上面提到的inet_create就会被调用:

static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
	struct sock *sk;
	struct inet_protosw *answer;
	struct inet_sock *inet;
	struct proto *answer_prot;
	unsigned char answer_flags;
	int try_loading_module = 0;
	int err;
 
	/* Look for the requested type/protocol pair. */
lookup_protocol:
	list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
		err = 0;
		/* Check the non-wild match. */
		if (protocol == answer->protocol) {
			if (protocol != IPPROTO_IP)
				break;
		} else {
			/* Check for the two wild cases. */
			if (IPPROTO_IP == protocol) {
				protocol = answer->protocol;
				break;
			}
			if (IPPROTO_IP == answer->protocol)
				break;
		}
		err = -EPROTONOSUPPORT;
	}
......
	sock->ops = answer->ops;
	answer_prot = answer->prot;
	answer_flags = answer->flags;
......
	sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
......
	inet = inet_sk(sk);
	inet->nodefrag = 0;
	if (SOCK_RAW == sock->type) {
		inet->inet_num = protocol;
		if (IPPROTO_RAW == protocol)
			inet->hdrincl = 1;
	}
	inet->inet_id = 0;
	sock_init_data(sock, sk);
 
	sk->sk_destruct	   = inet_sock_destruct;
	sk->sk_protocol	   = protocol;
	sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
 
	inet->uc_ttl	= -1;
	inet->mc_loop	= 1;
	inet->mc_ttl	= 1;
	inet->mc_all	= 1;
	inet->mc_index	= 0;
	inet->mc_list	= NULL;
	inet->rcv_tos	= 0;
 
	if (inet->inet_num) {
		inet->inet_sport = htons(inet->inet_num);
		/* Add to protocol hash chains. */
		err = sk->sk_prot->hash(sk);
	}
 
	if (sk->sk_prot->init) {
		err = sk->sk_prot->init(sk);
	}
......
}

通过inetsw[]数组获取对应协议类型的接口操作集信息
  • 在inet_create中,我们会先看到一个循环list_for_each_entry_rcu。在这里,socket系统调用的第二个参数type开始起作用。因为循环查看的是inetsw[sock->type]
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {

inetsw又是什么呢?

我们看下它的定义,如下。可以看到它也是一个数组,但是并不能看出它里面存放了什么,只能知道它是一个链表

static struct list_head inetsw[SOCK_MAX];
  • 那我们看下它是怎么初始化的
static int __init inet_init(void)
{
......
	/* Register the socket-side information for inet_create. */
	for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
		INIT_LIST_HEAD(r);
	for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
		inet_register_protosw(q);
......
}
  • inetsw数组是在系统初始化的时候初始化的:
    • 首先,一个循环会将inetsw数组中的每一项,都初始化为一个链表(一个type类型会包含多个 protocol,因而我们需要一个链表。)
    • 接下来的循环,是将inetsw_array注册到inetsw数组里面去。
  • inetsw_array的定义如下:
static struct inet_protosw inetsw_array[] =
{
	{
		.type =       SOCK_STREAM,
		.protocol =   IPPROTO_TCP,
		.prot =       &tcp_prot,
		.ops =        &inet_stream_ops,
		.flags =      INET_PROTOSW_PERMANENT |
			      INET_PROTOSW_ICSK,
	},
	{
		.type =       SOCK_DGRAM,
		.protocol =   IPPROTO_UDP,
		.prot =       &udp_prot,
		.ops =        &inet_dgram_ops,
		.flags =      INET_PROTOSW_PERMANENT,
     },
     {
		.type =       SOCK_DGRAM,
		.protocol =   IPPROTO_ICMP,
		.prot =       &ping_prot,
		.ops =        &inet_sockraw_ops,
		.flags =      INET_PROTOSW_REUSE,
     },
     {
        .type =       SOCK_RAW,
	    .protocol =   IPPROTO_IP,	/* wild card */
	    .prot =       &raw_prot,
	    .ops =        &inet_sockraw_ops,
	    .flags =      INET_PROTOSW_REUSE,
     }
}

  • 可以知道,inetsw里面的内容是struct inet_protows,是协议,也就是inetsw数组对于每个类型有一项,这一项里面是属于这个类型的协议

我们回到 inet_create 的 list_for_each_entry_rcu 循环中。如下。

	struct inet_protosw *answer;
	list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {

		err = 0;
		/* Check the non-wild match. */
		if (protocol == answer->protocol) {
			if (protocol != IPPROTO_IP)
				break;
		} else {
			/* Check for the two wild cases. */
			if (IPPROTO_IP == protocol) {
				protocol = answer->protocol;
				break;
			}
			if (IPPROTO_IP == answer->protocol)
				break;
		}
		err = -EPROTONOSUPPORT;
	}
  • 现在就好理解了:这里是在
    • 先从inetsw数组中,根据type找到属于这个类型的列表
    • 然后依次比较列表中的struct inet_protosw的protocol是不是参数指定的protocol
    • 如果是,就返回符合参数指定的family->type->protocol 的 answer 对象
  • ps:list_for_each_entry_rcu的作用:遍历带有双向链表结构的结构体变量。
    • 它实际上是一个 for 循环,利用传入的 pos 作为循环变量,从表头 head 开始,逐项向后(next 方向)移动 pos,直至又回head
list_for_each_entry_rcu(pos, head, member)
{
	 addr = pos;  //对返回值pos的操作,这样更容易去理解list_for_each_entry,可以把它看作for()循环
	/*获取pos,代码处理*/
}
1、 pos对应的结构体成员地址
2、 head对应的是结构体头指针
3、 member对应的是结构体的成员


用法用例:
typedef struct tagTest_TableList
{
	UINT i;
	UCHAR a;
	struct list_head TableList;
}TEST_LIST_S;
其中struct list_head 是:
struct list_head{
	struct list_head *next,*prev;
};

TEST_LIST_S *g_testlist = NULL;
TEST_LIST_S *temp;
list_for_each_entry_rcu(temp, &(g_testlist->TableList), TableList);
{
	if(NULL == temp)
		continue;
	temp->i = 1;//对获取的temp结构体变量进行操作
	/*...*/
}



设置sock->ops

接下来,struct socket *sock的ops成员变量,被赋值为answer的ops。

	sock->ops = answer->ops;
  • 这个sock是在__sock_create创建的。
  • 而ops,根据上面提到的static struct inet_protosw inetsw_array[]可以看出来,对于TCP来讲,就是inet_stream_ops。后面的任何用户对于这个socket的操作,都是通过inet_stream_ops进行的。
	{
		.type =       SOCK_STREAM,
		.protocol =   IPPROTO_TCP,
		.prot =       &tcp_prot,
		.ops =        &inet_stream_ops,
		.flags =      INET_PROTOSW_PERMANENT |
			      INET_PROTOSW_ICSK,
	},
创建一个struct sock *sk对象

接下来,我们创建一个struct sock *sk对象。

answer_prot = answer->prot;
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);

这里比较令人困惑。socket和sock看起来几乎一样,但是作用不同:socket是用于负责对上提供接口,并且和文件系统关联。而sock,负责向对接内核网络协议栈

在 sk_alloc 函数中,struct inet_protosw *answer 结构的 tcp_prot 赋值给了 struct sock *sk 的 sk_prot 成员。

#define sk_prot			__sk_common.skc_prot

struct sock *sk_alloc(struct net *net, int family, gfp_t priority,
		      struct proto *prot)
{
	...
	sk->sk_prot = sk->sk_prot_creator = prot;

sk->sk_prot 是一个协议回调函数,会根据协议不同而不同
在这里插入图片描述
tcp_prot 的定义如下,里面定义了很多的函数,都是 sock 之下内核协议栈的动作。

//net/ipv4/tcp_ipv4.c

struct proto tcp_prot = {
	.name			= "TCP",
	.owner			= THIS_MODULE,
	.close			= tcp_close,
	.connect		= tcp_v4_connect,
	.disconnect		= tcp_disconnect,
	.accept			= inet_csk_accept,
	.ioctl			= tcp_ioctl,
	.init			= tcp_v4_init_sock,
	.destroy		= tcp_v4_destroy_sock,
	.shutdown		= tcp_shutdown,
	.setsockopt		= tcp_setsockopt,
	.getsockopt		= tcp_getsockopt,
	.keepalive		= tcp_set_keepalive,
	.recvmsg		= tcp_recvmsg,
	.sendmsg		= tcp_sendmsg,
	.sendpage		= tcp_sendpage,
	.backlog_rcv		= tcp_v4_do_rcv,
	.release_cb		= tcp_release_cb,
	.hash			= inet_hash,
    .get_port		= inet_csk_get_port,
......
}

没错,这就是我们的地图了

创建一个struct inet_sock

在inet_create函数中,接下来是创建一个struct inet_sock结构,这个结构一开始就是struct sock,然后扩展了一些信息,剩下的代码就是填充这些信息。这一幕我们会经常看到,将一个结构放在另一个结构的开始位置,然后扩展一些成员,通过对于指针的强制类型转换,来访问这些成员

socket 的创建至此结束。

(2) sock_alloc_file()函数

sock_map_fd调用目的:将sock_create创建的struct socket 结构和文件描述符对应起来。

file结构体的创建及初始化,以及将socket和file结构体关联的操作主要在sock_alloc_file()函数中完成

其他

小结

在这里插入图片描述

参考

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐