UDP 套接字编程
上一篇介绍了基于 TCP 的 Socket 编程,今天我们继续介绍基于 UDP 的 Socket 编程,基于 UDP 的 Socket 编程要简单一些,因为 UDP 通信无需建立连接,所以不需要三次握手,也就不需要调用 listen 和 connect 函数,但是,UDP 的的交互仍然需要 IP 和端口号,因而也需要 bind。UDP 是没有维护连接状态的,因而不需要每对连接建立一组 Socket,而是只要有一个 Socket,就能够和多个客户端通信。也正是因为没有连接状态,每次通信的时候,调用 sendto 和 recvfrom,都可以传入 IP 地址和端口。
服务器如何同时提供更多连接
我们以 Web 请求为例,介绍如何让服务器同时处理更多请求,提高并发量。Web 请求一般都是 HTTP 请求,而 HTTP 协议又是基于 TCP 的,所以,我们主要探讨如何让服务器同时处理更多 TCP 连接请求。
服务器通常固定在某个本地端口上监听,等待客户端的连接请求。服务器端 TCP 连接四元组中只有对端 IP 和对端端口(即客户端IP和端口)是可变的,因此,最大 TCP 连接数 = 客户端 IP 数 × 客户端端口数。对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方。
当然,服务端最大并发 TCP 连接数远不能达到理论上限。首先主要是文件描述符限制,按照前面介绍的原理,Socket 都是文件,所以首先要通过 ulimit 配置文件描述符的数目;另一个限制是内存,按上面的数据结构,每个 TCP 连接都要占用一定内存,操作系统是有限的。
为了尽可能多的让服务器提供更多连接,处理更多请求,通常有以下几种解决方案:
1、多进程:当有新的请求进来,fork出一个子进程,让子进程处理该请求,提高并发量。
2、多线程:进程开销太大,线程则轻量级的多,所以我们还可以通过在进程中创建新的线程来处理请求。
上面基于进程或线程的模型还是有问题,因为每新进来一个 TCP 连接请求,就需要分配一个进程或线程,从而引发著名的C10K问题(一台机器要维护 1 万个连接,就要创建 1 万个进程或者线程,操作系统是无法承受的。如果维持 1 亿用户在线需要 10 万台服务器,成本也太高了),为此,又诞生了一种新的技术 —— 多路 IO 复用。
3、多路IO复用
所谓多路 IO 复用可以简单理解为一个线程维护多个 Socket(前面多进程或多线程都是一个进程或线程维护一个 Socket),这也有两种实现方式:轮询和事件通知。
因为 Socket 在 Linux 系统中以文件描述符形式存在,所以我们把一个线程维护的所有 Socket 叫做文件描述符集合,所谓轮询就是调用内核的 select 函数监听文件描述符集合是否有变化,一旦有变化,就会依次查看每个文件描述符,对那些发生变化的文件描述符进行读写操作,然后再调用 select 函数监听下一轮的变化。
显然,轮询的效率有点低,因为每次文件描述符集合有变化,都要将全部 Socket 轮询一遍,这大大影响了系统能够支撑的最大连接数。如果改成事件通知的方式,情况要好很多。所谓事件通知,就是某个文件描述符发生变化,调用 epoll 函数主动通知。这种方式使得监听的 Socket 数据增加的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常多,上限就是操作系统定义的、进程打开的最大文件描述符个数。也因此,epoll 被称为解决 C10K 问题的利器。
关于更底层的实现原理我们将留到后面讲 Nginx 的时候深入展开。