- 套接字 Socket 模型
下图展示了客户端-服务端的 Socket 程序的调用过程:

服务端首先调用 socket 函数,创建网络协议为 IPv4,以及传输协议为 TCP 的 Socket,接着调用 bind 函数,给这个 Socket 绑定一个 IP 地址和端口。
绑定完 IP 地址和端口后,就可以调用 listen 函数进行监听。如果要判定服务器中一个程序有没有启动,可以通过 netstat 命令查看对应的端口号是否有被监听。
服务端进入监听状态后,调用 accept 函数,来从内核获取客户端的连接,如果没有客户端连接,则会阻塞等待客户端连接的到来。
客户端也会调用 socket 函数创建 Socket,然后调用 connect 函数发起连接,该函数的参数要指明服务端的 IP 地址和端口号。
建立连接的过程就是 TCP 三次握手,这里不再赘述。
连接建立成功后,客户端和服务端就可以相互传输数据了,双方都可以用 read 函数和 write 函数来读写数据。
- I/O 多路复用
I/O 多路复用的核心原理是:通过操作系统内核提供的系统调用( select/poll/epoll)实现一个进程可以从内核中获取多个事件,即一个进程可以监听多个 I/O 事件。一个进程里可以处理多个文件的 I/O ,Linux 下有三种提供 I/O 多路复用的 API,分别是 select、poll、epoll。
- select/poll
select/poll 首先在用户态下把待检测的 Socket 保存在位数组中,接着把位数组从用户态拷贝到内核态。
然后由内核检测事件,通过事件驱动机制(select/poll 只支持水平触发)检测有网络事件产生时,内核需要遍历整个位数组,找到有事件发生的 Socket,并设置它的状态为可读/可写。
最后,把这个位数组从内核态拷贝到用户态,用户态还要继续遍历整个位数组找到可读/可写的 Socket,然后对其处理。
poll 在 select 的基础上将位数组改成了链表,突破了只能保存 1024 个Socket 的限制。
很明显,select/poll 的缺陷在于底层采用的数组结构存储 Socket,select 是静态数组,poll 是动态数组。客户端越多,位数组越大,需要进行的两次遍历和两次拷贝需要的开销越大。
- epoll
epoll 相较于 select/poll 做出了改善:
epoll 首先在用户态下把待检测的 Socket 直接拷贝到内核态的红黑树中做保存,红黑树增删改时间复杂度为 O(logn),优于 select/poll 的位数组拷贝和遍历的时间复杂度 O(n),减少了内核态与用户态之间大量的数据拷贝和内存分配。
然后由内核检测事件,通过事件驱动机制(默认为水平触发,可以设置成边缘触发)检测有网络事件产生时,内核通过红黑树的数据结构,只需 O(logn) 的时间复杂度就可以找到有事件发生的 Socket ,然后把它存入到一个链表中(内核除了红黑树外还维护了一个链表)。
最后,内核把这个链表拷贝到用户态,用户态虽然也是遍历链表来处理事件,但是整个链表保存的都是可读/可写的 Socket,不再像 select/poll 中的位数组那样既保存了有事件的 Socket,又保存了无事件的 Socket,大大提高了检测效率。

No responses yet