本文已参与「新人创作礼」活动,一起开启掘金创作之路。
在开始之前希望大家都知道以下几点:
首先,一个端口肯定只能绑定一个socket,当然这个socket可能会产生很多“socket连接”;
其次,只要服务器性能好一个端口就可以绑定无数个“socket连接”;
再次,一个socket句柄代表两个地址对“本地IP:端口”--“远程IP:端口” 。
关于这一点我们多说几句:我们知道在广袤的互联网中唯一标示一个进程(应用程序)需要三元组:IP地址、端口和协议。
此时我问你 描述两个应用进程之间的端到端的通信需要什么?显然需要五元组:本地IP、本地端口、远程IP、远程端口和协议。而接下来要讲的套接字,它的本质就是涵盖着五元组信息的类/对象。其操作也大多围绕这五元组信息展开的。
一、socket()函数——“亚当夏娃”套接字的诞生
使用socket()函数建立套接字的时候,我们实际上是在内核里面建立了一个数据结构(或者说是对象)。这个数据结构包括了上面所说的地址,此外还有协议等条目(只不过刚建立的时候这些条目还未指定具体值而已!)。socket()函数执行成功的话返回一个int型的描述符,它指向前面那个被维护在内核里的socket数据结构。我们的任何操作都是通过这个描述符而作用到那个数据结构上的。
socket结构究竟包括哪些内容呢?实际上 socket 结构体的定义如下:
struct socket
{
socket_state state;
unsigned long flags;
const struct proto_ops *ops;
struct fasync_struct *fasync_list;
struct file *file;
struct sock *sk;
wait_queue_head_t wait;
short type;
};
其中,struct sock 包含有一个 sock_common 结构体,而sock_common结构体又包含有struct inet_sock 结构体,而struct inet_sock 结构体的部分定义如下:
struct inet_sock
{
struct sock sk;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
struct ipv6_pinfo *pinet6;
#endif
__u32 daddr; //IPv4的目的地址。
__u32 rcv_saddr; //IPv4的本地接收地址。
__u16 dport; //目的端口。
__u16 num; //本地端口(主机字节序)。
…………
}
由此,我们知道socket结构体不仅仅记录了本地的IP:端口号,还记录了目的IP:端口,即 “本地地址”和“远程地址” 。
也就是说socket在新建的时候只是一个待填充的 空的结构,后面的connect()和bind()函数实际上可以看做给内核里边的那个socket对象设置具体IP和端口信息的过程。 (注:此处没有包括listen、accept,不过貌似也可以包括。)
注:这个描述符是我们用来监听用的称为——监听socket。
二、bind()和connect()函数——给套接字赋予“本地地址”(对客户端是“远程地址”)
依照建立套接字的目的不同,赋予套接字地址的方式有两种:服务器端使用bind,客户端使用connect。
1、对于服务端而言, bind()函数的作用是将标注有socket地址信息的数据结构(struct sockaddr)和socket()函数创建的那个套接字联系起来,即赋予这个套接字一个“本地地址”(通常称为命名socket)。对于客户端通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址。
2、对于客户端而言,connect()函数的作用是将客户端socket()函数建立socket和其所期望连接的服务器之间建立关系。只不过这个服务器是用标注相应信息的结构对象(struct sockaddr对象)来表征的。注:(1)作为客户端,你想要连接的服务器的地址肯定是已经知道了的。(2)在connect建立socket和socket地址两者关系的同时,它也在尝试着建立远端的连接。
总的来说,就是将客户端socket()建立的socket都同服务端的地址绑定。
三、listen()函数——开始监听的开关
listen函数的作用可以理解成开启监听的开关,listen被执行之后开关被打开,就开始监听了。轮询的时候判断到有连接请求过来的时候调用accept接受即可。详细的将使用listen系统调用后会创建一个监听队列以存放待处理的客户连接。
四、accept函数——建立套接字连接
accept()接受一个客户端的连接请求(该客户端的所有信息均来自“监听socket”),并返回一个新的套接字。所谓“新的”就是说这个套接字与socket()返回的用于监听和接受客户端的连接请求的套接字不是同一个套接字。与本次接受的客户端的通信是通过在这个新的套接字上发送和接收数据来完成的。再次调用accept()可以接受下一个客户端的连接请求,并再次返回一个新的套接字(与socket()返回的套接字、之前accept()返回的套接字都不同的新的套接字)。
假设一共有三个 客户端连接到服务器端。那么在服务器端就一共有4个套接字:第1个是socket()返回的、经过bind或connect填充“本地IP:端口”的用于监听的套接字;其余3个是分别调用3次accept()返回的包含“本地/远程”双重信息的不同的套接字(他们之间远程信息不同)。
如果已经有客户端连接到服务器端,不再需要监听和接受更多的客户端连接的时候,可以关闭由socket()返回的套接字,而不会影响与客户端之间的通信。当某个客户端断开连接、或者是与某个客户端的通信完成之后,服务器端需要关闭用于与该客户端通信的套接字。
由以上可知对服务端而言bind操作的只是那个socket()返回、用于监听的socket。其他几个“socket连接”,他们在内核中应该也是有对应的对象的(猜测)。不过他们并不用于监听,而是用于具体读写。这种“一对多”的关系可以这样理解:一个仅有“本地IP:端口”的父亲(监听socket)将自己监听到的信息用于派生,派生出了一个又一个同时具备“本地IP:端口”和“远程IP:端口”的新的socket(即socket连接)。注:(1)上述的“派生”的过程由accept函数完成的(2)这里监听socket和socket连接的结构应该是一样的,只不过“派生”出的socket把“远程IP:端口号”的信息也分别填充了。(3)“远程IP:端口”的信息是来自监听socket的。
至此为什么只有这个新的套接字才能用于同这次接受的客户端之间的通信也就一目了然了,因为这个套接字才真正包含了“本地信息”和“远程信息”,只有同时包含这两重信息才可以用于数据的收发。
五、accept返回的fd和listen的fd是什么关系?
(1)首先他们肯定是不等的。一个socket是由一个五元组来唯一标识的,即(协议,server_ip, server_port, client_ip,client_port)。只要该五元组中任何一个值不同,则其代表的socket就不同。accept()返回fd和listen fd的端口是一样吗_cws1214的专栏-CSDN博客
(2)那这样算不算 一个监听socket和若干socket连接共享端口呢?
答:准确的说两者的概念有一点区别。“一个端口不能被多个socket共享”,应该理解成“彼此独立的socket不能共享一个端口”。显然“监听socket”和由其产生的“socket连接”并不是彼此独立的socket。也就是说他们确实共享端口了,但是此共享非彼共享。或者你可以理解成“本地IP:端口”是被他们所共有的,即都是用“监听socket”的那份。
参考: