文件描述符:当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
IO多路复用是一种同步IO模型,实现一个进程可以监视多个文件句柄(socket、文件或者管道等等),一旦某个文件句柄就绪,就能够通知程序进行相应的读写操作。
IO多路复用相比于多线程的优势在于系统的开销小,系统不必创建和维护进程或线程,免去了线程或进程的切换带来的开销。而操作系统支持IO多路复用的系统调用有select,poll和epoll。
在默认的情况下,如果一个网络应用程序的一个套接字绑定了一个端口( 8080),这时候,别的套接字就无法使用这个端口( 8080 )。
但是端口复用允许在一个应用程序可以把多个套接字绑在一个端口上而不出错。通过设置socket的SO_REUSEADDR选项,即可实现端口复用:
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
(const void *)&opt, sizeof(opt));
【为什么要有这个端口复用呢】 ?
因为在服务端结束后,也就是第三次挥手的时候会有个等待释放时间(time_wait),这个时间段大概是1-4分钟(2MSL), 在这个时间内,端口不会迅速的被释放,所以可通过端口复用的方法来解决这个问题。
SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。
【为什么一个端口可以建立多个连接】 ?
一个TCP连接需要由四元组来形成,即(src_ip,src_port,dst_ip,dst_port)。
假设有客户端建立了连接(src_ip1,src_port1,dst_ip1,dst_port1),那么,如果我们还有listen在(src_ip1,src_port1),那么当(dst_ip1,dst_port1)发送消息过来,系统应该把消息给谁?所以就说明了客户端占用了某一端口时,该端口就不能被其它进程listen了。
作为一个服务器监控一个端口,比如80端口,它为什么可以建立上百万个连接?首先要明白一点,当accept出来后的新socket,它所占用的本地端口依然是80端口,很多新手都以为是一个新的随机端口。由四元组就很容易分析到了,同一个(src_ip,src_port),它所对应的(dst_ip,dst_port)可以无穷变化,这样就可以建立很多个客户端的请求了。
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
select的底层是一个fd_set的数据结构,本质上是一个long类型的数组,数组中每一个元素都对应于一个文件描述符,通过轮询所有的文件描述符来检查是否有事件发生。
【优点】:
可移植性好;
连接数少并且连接都十分活跃的情况下,效率也不错。
【缺点】:
可以监听的最大文件描述符数量为1024(因为内核写定了)。
检查是否有事件发生是采用轮询遍历的方式,当文件描述符很多时开销很大。
int poll(struct pollfd* fds, unsigned int nfds, int timeout);
poll与select差不多,但poll的文件描述符没有最大数量的限制,但是依然采用轮询遍历的方式检查是否有事件发生。
// 返回epoll文件描述符,size表示要监听的数目 (这个返回的fd要记得close)
int epoll_create(int size);
// epoll事件注册函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 等待事件发生,events是返回的事件链表
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll是一种更加高效的IO多路复用的方式,它可以监视的文件描述符数量突破了1024的限制(十万),同时不需要通过轮询遍历的方式去检查文件描述符上是否有事件发生,因为epoll_wait返回的就是有事件发生的文件描述符。本质上是事件驱动。
具体是通过红黑树和就绪链表实现的,红黑树存储所有的文件描述符,就绪链表存储有事件发生的文件描述符;
epoll_ctl可以对文件描述符结点进行增、删、改、查,并且告知内核注册回调函数(事件)。
一旦文件描述符上有事件发生时,那么内核将该文件描述符节点插入到就绪链表里面
这时候epoll_wait将会接收到消息,并且将数据拷贝到用户空间。
表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
避免在主进程epoll再次监听到同一个可读事件,可以把对应的描述符设置为EPOLL_ONESHOT,效果是监听到一次事件后就将对应的描述符从监听集合中移除,也就不会再被追踪到。读完之后可以再把对应的描述符重新手动加上。