常见IO模型:
-
同步阻塞IO(Blocking IO):使用recv一直等数据直到拷贝到用户空间,并同步返回状态,这段时间内进程始终阻塞
-
同步非阻塞IO(Non-blocking IO): socket设置为non-block,recv不管有没有获取到数据都返回,如果没有数据则等一段时间后在调用recv,如此循环. 只在检查无数据时是非阻塞的,有数据时仍然同步等待数据从内核到用户空间。
-
IO多路复用: 用Reactor模式较多,具体参考第2部分介绍
-
select
//创建fds数组
sockfd = socket(...);
bind(sockfd, addr);
listen(sockfd,port);
for...{
//通过listen_socket,创建connect_socket. listen_socket负责继续监听,connect_socket负责处理数据读写
fds[i] = accept(sockfd);
}
//创建bitmap/rset,并关联fds,最多1024bit
while(1) {
FD_ZERO(&bitmap); //bitmap由用户创建,并拷贝到内核更改,无法复用
for ...{
FD_SET(fds[i], &bitmap)
}
//此处多路复用器阻塞,等待有事件唤醒
select(max+1,&rset);
//traversal fds,时间复杂度O(n)
}
复制代码
限制:
- 一个进程最多能标记1024个fd
- 用户态和内核态之间切换拷贝bitmap
- bitmap无法重用
- 需要完整遍历bitmap每一位
- poll: 相比select无1024数量限制,并能复用pollfd_arr
stuct pollfd{
int fd;
//POLLIN 有数据可读, POLLOUT写事件允许, POLLERR 错误,POLLRDHUP流socket对端关闭连接;...
short events;
short revents;
}
...
for...{
pollfd_arr[i].fd = accept(listen_socket_fd);
pollfd_arr[i].events = POLLIN;
}
while(1){
//此处多路复用器阻塞,
poll(pollfd_arr, count,...);
for i in count{
if (pollfd_arr[i].revents & POLLIN){
read(pollfd_arr[i].fd, buffer);
pollfds[i].revents = 0; //重置状态 复用
}
}
}
复制代码
- epoll: bsd上是kqueue. 不仅告诉调用方有数据,还能提供具体哪个sock有数据,避免了遍历查找
struct epoll_event events[1024]
int epfd = epoll_create
...
for...{
static struct epoll_event ev;
ev.data.fd = accept(listen_socket_fd,....);
ev.events = EPOLLIN;
epoll_ctl(epfd, ev.data.fd,&ev ...);
whil(1){
//阻塞, 返回产生事件的个数,并且 events是内核已排序好的
int count = epoll_wait(epfd, events, ....);
for(i in count){
read(events[i].data.fd, buffer..);
}
}
}
复制代码
epoll在poll基础上进一步优化, events由用户态空间和内核空间共享避免了切换, 并且返回产生事件的句柄数量,以及内核排序好的句柄数组,调用方只需遍历有事件发生的fd数组.
- 异步非阻塞IO: 由操作系统内核负责读取socket数据,并写入用户指定的缓冲区,用户线程不会被阻塞,目前操作系统支持不太完善。
扩展
1. IO多路复用中常用的Reactor反应堆模式
主要角色:
-
handle : linux中称为文件描述符,windows中称为句柄,是对资源在os上的抽象,如socet\打开的文件\timer等,
-
synchronous event demultiplexer 同步多路事件分离器,本质是系统调用,如select poll epoll等,可以等待多个handle,在等待过程中所在线程牌挂起状态不消耗CPU时间。当某个句柄有事件产生时才会返回
-
Event Handler/Concrete Event Handler 定义一些钩子函数或称为回调方法,handle有事件产生就会调用钩子方法,在socket中一般有decode-process-encode这些过程,Concrete处理具体的业务逻辑
-
Initiation Dispatcher 初始事件分发器,用于管理event handler, 并调用多路分离器等待其返回时,将事件分发到event handle事件处理器进行处理
按线程划分:
-
单线程Reactor模型: 与NIO流程相似,一个线程处理所有的IO和process()过程,要求IO和CPU速度匹配,无处理较慢的请求,否则引起后续的请求积压
-
多线程Reactor模型: 一个或多个IO线程,多个工作处理线程,由于reactor既处理IO请求也响应连接请求容易出现瓶颈。
-
主从Reactor模型: 一个main reactor负责监听selector连接池,多个sub reactor 处理accept连接请求,性能较好。
线程池缺点是大并发下耗满线程,服务阻塞。对于大文件传输占用长时间数据读写操作的,可以考虑使用每个连接建立新的线程或异步非阻塞IO模式。并发数少用哪个都没有区别。异步非阻塞IO由于操作系统支持限制用的较少,大多高性能并发服务采用IO多路复用+多线程(线程池)的架构。
2. TCP粘包分包问题
每个UDP消息头部都有来源和端口号的消息头,一次write就是一条消息,接收端也以消息为单位提取,不完整的包会被丢弃,因此不会出现粘包。而TCP是面向流的协议接收端无法得知接收的数据是否已传完,因此要处理粘包、半包问题,解决方案有:
- 基于固定长度消息
- 基于消息边界符
- 消息头指定消息长度。