十一、套接字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 问题的利器。