十一、套接字 Socket
基于 TCP 和 UDP 协定的 Socket 编程。
Socket 编程进行的是端到端的通信,往往意识不到两头通过多少局域网,多少路由器,因此可能设置的参数,也只能是端到端协定之上网络层和传输层的。
在网络层,Socket 函数须要指定到底是 IPv4 还是 IPv6,别离对应设置为 AF_INET 和 AF_INET6。另外,还要指定到底是 TCP 还是 UDP。还记得咱们后面讲过的,TCP 协定是基于数据流的,所以设置为 SOCK_STREAM,而 UDP 是基于数据报的,因此设置为 SOCK_DGRAM。
基于 TCP 协定的 Socket 程序函数调用过程
TCP 的服务端要先监听一个端口,个别是先调用 bind 函数,给这个 Socket 赋予一个 IP 地址和端口。为什么须要端口呢?要晓得,你写的是一个应用程序,当一个网络包来的时候,内核要通过 TCP 头外面的这个端口,来找到你这个应用程序,把包给你。为什么要 IP 地址呢?有时候,一台机器会有多个网卡,也就会有多个 IP 地址,你能够抉择监听所有的网卡,也能够抉择监听一个网卡,这样,只有发给这个网卡的包,才会给你。
当服务端有了 IP 和端口号,就能够调用 listen 函数进行监听。在 TCP 的状态图外面,有一个 listen 状态,当调用这个函数之后,服务端就进入了这个状态,这个时候客户端就能够发动连贯了。
在内核中,为每个 Socket 保护两个队列。 一个是曾经建设了连贯的队列,这时候连贯三次握手曾经结束,处于 established 状态;一个是还没有齐全建设连贯的队列,这个时候三次握手还没实现,处于 syn_rcvd 的状态。 接下来,服务端调用 accept 函数,拿出一个曾经实现的连贯进行解决。如果还没有实现,就要等着。在服务端期待的时候,客户端能够通过 connect 函数发动连贯。先在参数中指明要连贯的 IP 地址和端口号,而后开始发动三次握手。内核会给客户端调配一个长期的端口。一旦握手胜利,服务端的 accept 就会返回另一个 Socket。
监听的 Socket 和真正用来传数据的 Socket 是两个,一个叫作监听 Socket,一个叫作已连贯 Socket。
连贯建设胜利之后,单方开始通过 read 和 write 函数来读写数据,就像往一个文件流外面写货色一样。这个图就是基于 TCP 协定的 Socket 程序函数调用过程。
说 TCP 的 Socket 就是一个文件流,是十分精确的。因为,Socket 在 Linux 中就是以文件的模式存在的。除此之外,还存在文件描述符。写入和读出,也是通过文件描述符。在内核中,Socket 是一个文件,那对应就有文件描述符。每一个过程都有一个数据结构 task_struct,外面指向一个文件描述符数组,来列出这个过程关上的所有文件的文件描述符。文件描述符是一个整数,是这个数组的下标。
这个数组中的内容是一个指针,指向内核中所有关上的文件的列表。既然是一个文件,就会有一个 inode,只不过 Socket 对应的 inode 不像真正的文件系统一样,保留在硬盘上的,而是在内存中的。在这个 inode 中,指向了 Socket 在内核中的 Socket 构造。在这个构造外面,次要的是两个队列,一个是发送队列,一个是接管队列。在这两个队列外面保留的是一个缓存 sk_buff。这个缓存外面可能看到残缺的包的构造。看到这个,是不是能和后面讲过的收发包的场景分割起来了?整个数据结构如下:
基于 UDP 协定的 Socket 程序函数调用过程
对于 UDP 来讲,过程有些不一样。UDP 是没有连贯的,所以不须要三次握手,也就不须要调用 listen 和 connect,然而,UDP 的的交互依然须要 IP 和端口号,因此也须要 bind。UDP 是没有保护连贯状态的,因此不须要每对连贯建设一组 Socket,而是只有有一个 Socket,就可能和多个客户端通信。也正是因为没有连贯状态,每次通信的时候,都调用 sendto 和 recvfrom,都能够传入 IP 地址和端口。这个图的内容就是基于 UDP 协定的 Socket 程序函数调用过程。
服务器如何接更多的我的项目?
会了这几个根本的 Socket 函数之后,你就能够轻松地写一个网络交互的程序了。就像下面的过程一样,在建设连贯后,进行一个 while 循环。客户端发了收,服务端收了发。
当然这只是万里长征的第一步,因为如果应用这种办法,基本上只能一对一沟通。如果你是一个服务器,同时只能服务一个客户,必定是不行的。这就相当于老板成立一个公司,只有本人一个人,本人亲自上来服务客户。
最大连接数:
零碎会用一个四元组来标识一个 TCP 连贯。{本机 IP, 本机端口, 对端 IP, 对端端口}
服务器通常固定在某个本地端口上监听,期待客户端的连贯申请。因而,服务端端 TCP 连贯四元组中只有对端 IP, 也就是客户端的 IP 和对端的端口,也即客户端的端口是可变的,因而,最大 TCP 连接数 = 客户端 IP 数×客户端端口数。对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方。
当然,服务端最大并发 TCP 连接数远不能达到实践下限。首先次要是文件描述符限度,依照下面的原理,Socket 都是文件,所以首先要通过 ulimit 配置文件描述符的数目;另一个限度是内存,按下面的数据结构,每个 TCP 连贯都要占用肯定内存,操作系统是无限的。
所以,作为老板,在资源无限的状况下,要想接更多的我的项目,就须要升高每个我的项目耗费的资源数目。
多过程形式
这就相当于你是一个代理,在那里监听来的申请。一旦建设了一个连贯,就会有一个已连贯 Socket,这时候你能够创立一个子过程,而后将基于已连贯 Socket 的交互交给这个新的子过程来做。
在 Linux 下,创立子过程应用 fork 函数。通过名字能够看出,这是在父过程的根底上齐全拷贝一个子过程。在 Linux 内核中,会复制文件描述符的列表,也会复制内存空间,还会复制一条记录以后执行到了哪一行程序的过程。显然,复制的时候在调用 fork,复制结束之后,父过程和子过程都会记录以后刚刚执行完 fork。这两个过程刚复制完的时候,简直截然不同,只是依据 fork 的返回值来辨别到底是父过程,还是子过程。如果返回值是 0,则是子过程;如果返回值是其余的整数,就是父过程。过程复制过程如下:
因为复制了文件描述符列表,而文件描述符都是指向整个内核对立的关上文件列表的,因此父过程方才因为 accept 创立的已连贯 Socket 也是一个文件描述符,同样也会被子过程取得。
接下来,子过程就能够通过这个已连贯 Socket 和客户端进行互通了,当通信结束之后,就能够退出过程,那父过程如何晓得子过程干完了我的项目,要退出呢?还记得 fork 返回的时候,如果是整数就是父过程吗?这个整数就是子过程的 ID,父过程能够通过这个 ID 查看子过程是否实现我的项目,是否须要退出。
多线程形式
在 Linux 下,通过 pthread_create 创立一个线程,也是调用 do_fork。不同的是,尽管新的线程在 task 列表会新创建一项,然而很多资源,例如文件描述符列表、过程空间,还是共享的,只不过多了一个援用而已。
新的线程也能够通过已连贯 Socket 解决申请,从而达到并发解决的目标。
下面基于过程或者线程模型的,其实还是有问题的。新到来一个 TCP 连贯,就须要调配一个过程或者线程。一台机器无奈创立很多过程或者线程。有个 C10K,它的意思是一台机器要保护 1 万个连贯,就要创立 1 万个过程或者线程,那么操作系统是无奈接受的。如果维持 1 亿用户在线须要 10 万台服务器,老本也太高了。
IO 多路复用,一个线程保护多个 Socket
因为 Socket 是文件描述符,因此某个线程盯的所有的 Socket,都放在一个文件描述符汇合 fd_set 中,这就是我的项目进度墙,而后调用 select 函数来监听文件描述符汇合是否有变动。一旦有变动,就会顺次查看每个文件描述符。那些发生变化的文件描述符在 fd_set 对应的位都设为 1,示意 Socket 可读或者可写,从而能够进行读写操作,而后再调用 select,接着盯着下一轮的变动。
IO 多路复用,从“派人盯着”到“有事告诉”)
下面 select 函数还是有问题的,因为每次 Socket 所在的文件描述符汇合中有 Socket 发生变化的时候,都须要通过轮询的形式,也就是须要将全副我的项目都过一遍的形式来查看进度,这大大影响了一个项目组可能撑持的最大的我的项目数量。因此应用 select,可能同时盯的我的项目数量由 FD_SETSIZE 限度。
如果改成事件告诉的形式,状况就会好很多,项目组不须要通过轮询挨个盯着这些我的项目,而是当我的项目进度发生变化的时候,被动告诉项目组,而后项目组再依据我的项目停顿状况做相应的操作。
能实现这件事件的函数叫 epoll,它在内核中的实现不是通过轮询的形式,而是通过注册 callback 函数的形式,当某个文件描述符发送变动的时候,就会被动告诉。
如图所示,假如过程关上了 Socket m, n, x 等多个文件描述符,当初须要通过 epoll 来监听是否这些 Socket 都有事件产生。其中 epoll_create 创立一个 epoll 对象,也是一个文件,也对应一个文件描述符,同样也对应着关上文件列表中的一项。在这项外面有一个红黑树,在红黑树里,要保留这个 epoll 要监听的所有 Socket。
当 epoll_ctl 增加一个 Socket 的时候,其实是退出这个红黑树,同时红黑树外面的节点指向一个构造,将这个构造挂在被监听的 Socket 的事件列表中。当一个 Socket 来了一个事件的时候,能够从这个列表中失去 epoll 对象,并调用 call back 告诉它。
这种告诉形式使得监听的 Socket 数据减少的时候,效率不会大幅度降低,可能同时监听的 Socket 的数目也十分的多了。下限就为零碎定义的、过程关上的最大文件描述符个数。因此,epoll 被称为解决 C10K 问题的利器。