分为两种:
1.服务端处理接收客户端的连接;
2.服务端作为客户端连接第三方服务;
int clientfd = accept(listenfd, addr, sz);
// 举例为非阻塞io,阻塞io成功直接返回0;
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
int ret = connect(connectfd, (struct sockaddr*)&addr, sizeof(addr));
connect检测功能:检测建立连接的状态
connect操作功能:具体连接成功
connect第一次调用返回 ENIPROGRESS(正在建立)
accept主要对三次握手时的全连接队列操作
accept检测功能:检测全连接队列里是否有未处理的连接信息
accept操作功能:把该结点取出,生成客户端fd,客户端ip地址和端口(五元组)
listen监听:服务器接收客户端的连接
listen(fd,backlog):在没有调用accept把数据取出前,三次握手结束时最多能堆积backlog大小的数据
分为两种:主动断开和被动断开;
主动断开
close:关闭两个端
shutdown:关闭一个端,例如调用shutdown关闭客户端的读端,那么服务端对应的写端就关闭了,对应write = -1 && errno = EPIPE状态
被动断开:
read = 0:服务端的读端关闭
write = -1 && errno = EPIPE :服务端的写端关闭
有的服务器需要支持半关闭状态,需要知道具体是读端还是写端关闭
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);
}
从读缓冲区中读取数据
read:
read = -1 && EWOULDBLOCK :说明读缓冲区没有数据
read = -1 && EINTR:说明 read 操作被取消
int n = read(fd, buf, sz);
if (n < 0) { // n == -1
if (errno == EINTR || errno == EWOULDBLOCK)
break;
close(fd);
} else if (n == 0) {
close(fd);
} else {
// 处理 buf
}
往写缓冲区中写数据
write:
write= -1 && EWOULDBLOCK :说明读缓冲区没有数据
write= -1 && EINTR:说明 read 操作被取消
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
A.连接建立
1.接收连接
socket -> bind & listen -> listenfd 监听读事件
读事件发生后才会知道客户端与服务器产生连接了,
产生一个结点告诉epoll,触发相应的epoll后续流程
2.主动连接(connect)
在第三次握手时,客户端向服务器发送ACK包的同时给Epoll 发送信号;
触发写事件,说明连接成功了
B.连接断开
EPOLLRDHUP:说明此时服务端的读端关闭了
EPOLLHUP:说明服务端的读写端全都关闭了
C.消息到达
客户端给服务器发送消息,如何监听(读缓冲区是否有数据)
监听客户端的fd ,客户端发送数据(向读缓冲区插入数据时)会给epoll发送信号,
触发读事件,说明读缓冲区有数据了,接下来就可以调用read,操作IO
D.消息发送
一般监听客户端fd的写事件
write buffer 如果满,Epoll 监听写事件,如果write buffer 存在剩余空间,可写状态时,
就会给epoll发送一个信号,触发写事件,用户就可以调用write写数据了
注:只有主动连接和消息发送时检测的是写事件,其他都是读事件。
只能使用 io 函数来进行操作;分为两种操作方式:阻塞 io 和非阻塞 io;
1.阻塞在网络线程;
2.连接的 fd 阻塞属性决定了 io 函数是否阻塞;
3.具体差异在:io 函数在数据未到达时是否立刻返回;
// 默认情况下,fd 是阻塞的,设置非阻塞的方法如下;
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
int n = read(fd,buf,sz);
int n = write(fd,buf,sz);
在数据准备阶段
阻塞时,对于读缓冲区,如果为空,调用read阻塞就会等待填充数据;
阻塞时,对于写缓冲区,如果不能写入数据,调用write阻塞就会等待直到可以写入数据;
非阻塞时,不管缓冲区里有没有数据,会立即返回。
int epoll_wait(int epfd, struct epoll_event*
events, int maxevents, int timeout);
OP操作:通过一个系统调用操作 epoll创建的rb_tree
op:
EPOLL_CTL_ADD //添加
EPOLL_CTL_MOD //修改
EPOLL_CTL_DEL //删除
1.调用epoll_ctl 添加到 epoll 中的事件与网卡驱动程序建立回调关系;
相应事件触发时会调用回调函数(ep_poll_callback);
将触发的事件拷贝到 rdlist 双向链表(就绪队列)中;
2.调用 epoll_wait 将会把 rdlist 中就绪事件拷贝到用户态中(epoll_event 填充一个用户态数组用来接收就绪队列中的数据)
3. maxevents 为预期取出的事件
int epoll_wait 函数返回实际取出的事件
4. timeout 调节阻塞或非阻塞
timeout = -1 为阻塞特性
timeout = 0 为非阻塞的特性
timeout = 1000,阻塞最多1s
1.io多路复用 :检测
2.非阻塞io :检测事件触发后,操作io
3.异步事件:read -> fd > callback
4.reactor为什么搭配非阻塞io?
a.多线程环境 可以将一个 listenfd 放到多个 epoll 去处理 epoll惊群,但只要一个accept调用成功
b.边缘触发下 读事件触发后,read在一次事件循环中把read buffer 读空
c.select bug:当某个socket接收缓冲区有新数据分节到达,然后select报告这个socket描述符可读,
但随后,协议栈检查到这个新分解检验和错误,然后丢弃这个分节,这时候调用read则无数据可读,
如果socket没有被设置nonblocking,此read将阻塞当前线程
1.环境
kv、数据结构、内存数据库
命令处理是单线程的
2.redis为什么要使用单reactor?
命令处理是单线程的 -> 单reactor
操作具体命令事件复杂度比较低
3.redis怎么处理reactor?
1.创建listenfd
2. bind listen
3. listenfd注册读事件
4. 读事件触发的回调 accept
5. clientfd 注册读事件
6. 读事件触发,将会回调readQueryFromClient
4.redis针对reactor做了哪些优化
1.read,decode 记录日志
2.encode,write 获取排行榜记录
…未完待续
…未完待续