TCP服务器可以用下面图示来解释:
迎宾相当于listenfd,一直在监听客户端的请求,当有客户到来,将客户指引给大堂内的服务员,此时即建立了连接,大堂服务员相当于connfd,之后和当前客户端的所有交互都是connfd进行。
建立连接的三次握手是在协议栈中完成的,不发生在服务端的任何api中,不受应用程序控制
IO多路复用
IO多路复用的三种实现方式:select,poll,epoll
select函数
定义:该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒该函数。
例如,调用select,告知内核仅在下列情况发生时才返回:
- 集合{1,4,5}中的任何描述符准备好读
- 集合{2,7}中的任何描述符准备好写
- 集合{1,4}中的任何描述符有异常条件待处理
- 超过了设定的超时时长
调用select告知内核对哪些文件描述符(读/写/异常)感兴趣以及等待多长时间
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,
const struct timeval *timeout);
函数参数(一共5个参数)
- timeout – 告知内核等待所指定描述符中的任何一个就绪可花多长时间。
timeval结构用于指定这段时间的秒数和微秒数
struct timeval {
long tv_sec /秒/
long tv_usev /微秒/
};
该参数有以下三种可能:
(1)永远等待下去(阻塞):仅在有一个描述符准备好I/O(读/写)时才返回,将该参数设置为空指针
(2)等待一段固定时间:在有一个描述符准备好I/O时返回,但是不能超过设定的超时时间
(3)根本不等待:检查描述符后立即返回,这称为轮询。 - 中间的三个参数readset、writeset、excepset
指定我们要让内核测试读、写、异常条件的描述符集合;如果对某个条件不感兴趣,就可以把该变量设置为空指针。
我们分配fd_set数据类型的描述符集,并用下面这些宏设置或测试该集合中的每一位
void FD_ZERO(fd_set *fdset); //清空fdset中所有位
void FD_SET(int fd, fd_set *fdset); // 将fd加入fdset集合
void FD_CLR(int fd, fd_set *fdset); // 将fd从fdset集合中清除
void FD_ISSET(int fd, fd_set *fdset); // 判断fd是否在fdset集合中
- maxfdp1指定待测试的描述符个数
它的值是待测试的最大文件描述符+1
select函数修改由指针readset、writeset、exceptset所指向的描述符集,因而这三个参数都是值-结果参数
。
调用该函数时,我们指定所关心的描述符的值,该函数返回时,结果将指示那些描述符已就绪。
该函数赶回后,使用FD_ISSET宏来测试fd_set数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清成0。因此每次重新调用select函数时,都得再次把所有描述符集内所关心的位均置为1。
返回值
整数:表示所有描述符集的已就绪的总位数。
0:表示在任何描述符就绪之前定时器到时间。
-1:出错了
// todo: 有空要补充一下代码
缺点:
监听的文件描述符是有限的(最多1024个);每次监听都要把文件描述符从用户态拷贝到内核态,效率低;监听到返回的变化文件描述符需要再次遍历查询;代码编写困难
优点:
可移植性好,Windows也支持
poll函数
poll提供的功能与select类似
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
函数参数
- fdarray – 指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定要监控的文件描述符.
struct pollfd {
int fd; // 文件描述符
short events; // 监控的时间
short revents; // 监控事件中满足条件返回的事件
};
- nfds – 监控数组中有多少文件描述符需要被监控
- timeout – 毫秒级等待
-1:阻塞等
0:立即返回
>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值
返回值
若有就绪描述符则为其数目,若超时则为0,若出错则为-1
// todo: 有空要补充一下代码
和select类似,相对于select的优势:没有1024最大文件描述符的限制;传入、传出事件分离,无需每次调用时,重新设定监听事件(select参数是输入输出参数,请求和返回的文件描述符是一个变量)
epoll函数
3个API,使用的数据结构是红黑树
基础API
- 创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关
#include <sys/epoll.h>
int epoll_create(int size)
// size:监听数目
// 返回值:成功,非负文件描述符;失败,-1
- 控制某个epoll监控的文件描述符上的时间:注册、修改、删除
将需要监听的文件描述符添加到红黑树中(红黑树插入删除元素效率很高)
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
// epfd: epoll_create创建的句柄
// op: 表示动作,用三个宏来表示
// EPOLL_CTL_ADD (注册新的fd到epfd)
// EPOLL_CTL_MOD (修改已经注册的fd的监听事件)
// EPOLL_CTL_DEL (从epfd删除一个fd)
// event: 告诉内核需要监听的事件
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
- 等待所监控文件描述符上有事件的产生(类似于select的调用)
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
// events:用来存内核得到事件的集合,可简单看作数组。
// maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
// timeout:超时时间
// -1:阻塞
// 0:立即返回,非阻塞
// >0:指定毫秒
// 返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
优点:
显著提高程序在大量连接中,只有少量活跃情况下的系统CPU利用率(1000个连接,只监听10个文件描述符),它会复用文件描述符集合来传递结果,而不用迫使开发者每次等待事件之前都必须重新准备要被监听的文件描述符集合(epoll_wait传入一个空的结构体数组,有事件发生的文件描述符写入数组);
获取事件的时候,无需遍历整个被监听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入ready队列的描述符集合就行。
边沿触发与水平触发的区别
epoll中有两种触发模式,可以减少epoll_wait的调用次数,提高执行效率
边沿触发(epoll ET):只有数据到来才触发epoll_wait,不管缓冲区中是否还有数据
水平触发(epoll LT):只要缓冲区有数据都会触发epoll_wait(默认此方式)
服务器中1请求1线程