网络基础——Socket编程
套接字底层原理
TCP套接字
TPC的服务端要先监听一个端口,一般是先调用bind
函数,给这个Soket赋予一个IP地址和端口。
当服务端有了IP和端口号,就可以调用listen
函数进行监听。这时候客户端就可以发起连接了。
在操作系统中,为每个Socket维护两个队列。一个是已经建立了连接的队列,三次握手已完毕,处于established
状态。一个是还没有完全建立连接的,没有完成三次握手,处于syn_rcv
的状态。
在服务端等待的时候,客户端可以通过connect
函数发起连接。先在参数中指明要连接的IP地址和端口号,然后开始三次握手,操作系统会给客户端分配一个临时的端口。一旦握手成功,服务端的accept
就会返回另一个用于传输数据的Socket。
连接建立成功之后,双发开始通过read
和write
函数来读写数据,就像往一个文件流里面写东西一样。
UDP套接字
因为UDP通信无需建立连接看,所以也不需要三次握手,也就不需要调用listen
和connect
函数函数,但是,UPD的交互仍然需要IP地址和端口号,因而需要bind
。UDP是没有维护连接状态的,因而不需要对没对连接建立一组Socket,而是只要有一个Socket,就能够和多个客户端通信。也正是因为没有连接状态,每次通信的时候,调用sendto
和recvfrom
,都可以传入IP地址和端口。
服务器如何提高并发量
Web请求一般都是HTTP请求,而HTTP协议又是基于TCP的,所以,我们主要探索如何让服务器同时处理更多TCP连接请求。
-
多进程
当有新的请求进来,fork出一个子进程,让子进程处理该请求,提高并发量
-
多线程
进程开销太大,线程则轻量级的多,所以我们还可以通过在进程中创建新的线程来处理请求。
上面基于进程或线程的模型还是有问题,因为每新进来一个 TCP 连接请求,就需要分配一个进程或线程,从而引发著名的
C10K
问题(一台机器要维护 1 万个连接,就要创建 1 万个进程或者线程,操作系统是无法承受的。如果维持 1 亿用户在线需要 10 万台服务器,成本也太高了),为此,又诞生了一种新的技术 —— 多路 IO 复用。 -
多路IO复用
所谓多路IO复用可以简单理解为一个线程维护多个Socket(前面多进程多线程说的都是一个进程或线程维护一个Socket),这也有两种实现方式:轮询和事件通知。
因为 Socket 在 Linux 系统中以文件描述符形式存在,所以我们把一个线程维护的所有 Socket 叫做文件描述符集合,所谓轮询就是调用内核的
select
函数监听文件描述符集合是否有变化,一旦有变化,就会依次查看每个文件描述符,对那些发生变化的文件描述符进行读写操作,然后再调用select
函数监听下一轮的变化。显然,轮询的效率有点低,因为每次文件描述符集合有变化,都要将全部 Socket 轮询一遍,这大大影响了系统能够支撑的最大连接数。如果改成事件通知的方式,情况要好很多。所谓事件通知,就是某个文件描述符发生变化,调用
epoll
函数主动通知。这种方式使得监听的 Socket 数据增加的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常多。上限就为系统定义的、进程打开的最大文件描述符个数。因此,
epoll
被称为解决 C10K 问题的利器。