IO检测与处理
IO函数
连接的建立
需要服务端和客户端分别建立socket套接字,客户端发送connect请求,服务端监听该请求,并accept该请求:
// 客户端
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
int ret = connect(connectfd, (struct sockaddr *)&addr, sizeof(addr));
// 服务器
// accept负责从全连接队列获取fd,并操作IO
int clientfd = accept(listenfd, addr, sz);
连接的断开
TCP是全双工的,一个socket连接,会维护一个读缓冲区和一个写缓冲区
因此,连接的关闭分为主动和被动断开两种:
主动关闭:
close(fd);
shutdown(fd, SHUT_RDWR);
// 主动关闭本地读端,对端写段关闭
shutdown(fd, SHUT_RD);
// 主动关闭本地写端,对端读段关闭
shutdown(fd, SHUT_WR);
被动关闭:客户端关闭读或写端,服务器关闭写或读端(半关闭),也可能是读写端全关闭:
// 被动:读端关闭
// 有的网络编程需要支持半关闭状态
int n = read(fd, buf, sz);
if (n == 0) {
close_read(fd);
// write()
// close(fd);
}
// 被动:写端关闭
int n = write(fd, buf, sz);
if (n == -1 && errno == EPIPE) {
close_write(fd);
// close(fd);
}
消息的处理
从读缓冲区读取数据:
int n = read(fd, buf, sz);
if (n < 0)
{
// n == -1:
// 如果是:被其他信号打断,或buff为空,下次再继续读
if (errno == EINTR || errno == EWOULDBLOCK)
break;
// 其他的错误,关闭连接
close(fd);
}
// 客户端关闭客户端,发送FIN信号到服务器
else if (n == 0)
{
close(fd);
}
else
{
// 处理 buf
}
往写缓冲区中写数据:
int n = write(fd, buf, dz);
if (n == -1)
{
if (errno == EINTR || errno == EWOULDBLOCK)
{
return;
}
close(fd);
}
网络IO职责
检测IO
io 函数本身可以检测 io 的状态;但是只能检测一个 fd 对应的状态;io 多路复用可以同时检测多个io的状态;
区别是:io函数可以检测具体状态;io 多路复用只能检测出可读、可写、错误、断开等笼统的事件;
操作IO
IO函数操作IO,分为两种:阻塞和非阻塞IO,阻塞在网络线程,连接的fd阻塞属性决定了IO函数是否阻塞,其具体差异在:io 函数在数据未到达时是否立刻返回,也就是数据准备阶段是否会阻塞。
默认情况下,fd 是阻塞的,设置非阻塞的方法如下;
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
IO多路复用
io 多路复用只负责检测io,不负责操作io,如epoll:
int n = epoll_wait(epfd, evs, sz, timeout);
timeout = -1 一直阻塞直到网络事件到达;
imeout = 0 不管是否有事件就绪立刻返回;
timeout = 1000 最多等待 1 s,如果1 s内没有事件触发则返回;
epoll在数据准备阶段也是阻塞,也就是epoll_wait会产生阻塞,如果epoll的就绪链表有数据,则说明数据准备好了进行返回,否则就会阻塞,timeout就是阻塞等待的事件。
epoll结构和接口:
struct eventpoll {
// ...
struct rb_root rbr; // 管理 epoll 监听的事件
struct list_head rdllist; // 保存着 epoll_wait 返回满⾜条件的事件
// ...
};
struct epitem {
// ...
struct rb_node rbn; // 红⿊树节点
struct list_head rdllist; // 双向链表节点
struct epoll_filefd ffd; // 事件句柄信息
struct eventpoll *ep; // 指向所属的eventpoll对象
struct epoll_event event; // 注册的事件类型
// ...
};
struct epoll_event {
__uint32_t events; // epollin epollout epollel(边缘触发)
epoll_data_t data; // 保存 关联数据
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
int epoll_create(int size);
/**
op:
EPOLL_CTL_ADD
EPOLL_CTL_MOD
EPOLL_CTL_DEL
event.events:
EPOLLIN 注册读事件
EPOLLOUT 注册写事件
EPOLLET 注册边缘触发模式,默认是水平触发
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
/**
events[i].events:
EPOLLIN 触发读事件
EPOLLOUT 触发写事件
EPOLLERR 连接发生错误
EPOLLRDHUP 连接读端关闭
EPOLLHUP 连接双端关闭
*/
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
调用 epoll_create 会创建一个 epoll 对象;调用 epoll_ctl 添加到 epoll 中的事件都会与网卡驱动程序建立回调关系,相应事件触发时会调用回调函数 ( ep_poll_callback ),将触发的事件拷贝到 rdlist 双向链表中;调用 epoll_wait 将会把 rdlist 中就绪事件拷贝到用户态
中;
Reactor
其将对IO的处理,转化为对事件的处理
组成:IO多路复用+非阻塞IO
单reactor
一个reactor用管理listenfd和connfd
如redis网络封装:
ae:async event anet:异步网络 connection:redis-cli和redis-server的数据处理 networking:协议处理 (redis协议、io多线程)
redis源码没读完,以后补上解读连接
io多线程
网络是单线程,但io是多线程,这也就是redis为什么单线程还这么快的原因。 以读事件举例:
int n=read(fd,buf,sz);
cmd,args=decode(buf,sz); // 数据处理
out=logic(cmd,args); // 业务逻辑处理
reply=encode(out);
write(fd,reply,sizeof(reply)); // 数据处理
redis6.0的单reactor的io多线程优化:
- 将read和decode做为任务插入到读队列中,利用负载均衡(取余),将这个队列的任务分配到不同的IO线程中去处理
- 一个连接read和decode做完后,将其返回结果交给主线程,进行业务逻辑处理
- 因为在reactor循环中,读写很耗CPU,很容易把后面的连接卡死了,所以把耗时的操作进行异步处理
- 主线程处理后,将encode和write插入到写队列,分配到不同的io线程去处理,处理完后进入主线程
优化2: 单线程读,多线程写,由于读缓冲区可能只会读一部分,多线程读一个缓冲区可能造成顺序错乱;但写操作是一次性,要么全部写入,要么写入失败,不会产生顺序错乱。
多线程reactor:one eventloop per thread
如memcache
reactor中可以有多个epoll,可以用一个处理连接(mysql也是用一个select处理连接),然后由这个reactor将连接分配到不同的reactor,reactor的数量和CPU的核心数相关
当每个连接有较多的交互,临界资源较多以至于加锁麻烦时,性能不如单reactor
多进程reactor
如Nigix:
一个master进程fork出多个worker进程,每个进程都有一个reactor,每个worker进程都监听在一个端口上(因为是fork出来的),连接都放在共享内存里,哪个进程拿到了这把锁,谁就accept这个连接,并交由这个进程的reactor处理。
由于nginx内一个进程只有一个线程,所以也是one eventloop per thread
与redis和memcached不同,nginx采用边缘触发,因为nginx主要用来做反向代理,把这条连接的数据转发给不同的客户端服务器,它自己又不用处理数据,所以用边缘触发效率更高;
而redis要做业务开发,全部读完处理不过来,而且没必要一次全读完,反正下次会继续触发。