网络编程关注的问题
连接的建立
分为两种:服务端处理接收客户端的连接;服务端作为客户端 连接第三方服务;
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)); // ret == -1 && errno == EINPROGRESS 正在建立连接 // ret == -1 && errno = EISCONN 连接建立成功
连接的断开
分为两种:主动断开和被动断开;
// 主动关闭 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 if (errno == EINTR || errno == EWOULDBLOCK) break; close(fd); } 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
阻塞在网络线程;
连接的 fd 阻塞属性决定了 io 函数是否阻塞;
具体差异在:io 函数在数据未到达时是否立刻返回;
// 默认情况下,fd 是阻塞的,设置非阻塞的方法如下; int flag = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flag | O_NONBLOCK);
常见的io函数检测与操作io的方式
connect:
检测功能:检测是否与服务端建立连接 操作功能:与服务端建立连接
accept:
检测功能:检测全连接队列是否为空 操作功能:从全连接队列中取出一条,建立连接(生成fd),并能够获取到客户端的ip地址和端口号
read:
检测功能:检测内核读缓冲区中是否有数据 操作功能:将内核读缓冲区的数据拷贝到用户态来
write:
检测功能:检测内核写缓冲区是否已满 操作功能:将用户态数据拷贝到内核写缓冲区中
注意:
io函数只能检测一条连接以及操作一条连接的数据
操作io方式:
如下描述了io处理函数中非阻塞io方式操作io
说明:
连接断开中,当服务器read0的时候,其实还是可以把数据发送给客户端的,因为read0只能说明读端关闭了,有些服务器的框架需要支持半关闭的情况,shutdown函数是关闭读端或者写端的意思,当关闭读端,那么对端的写端就关闭,close函数是关闭读写端
注意:
read/write函数经常要处理的错误码:
read==-1 && errno == EWOULDBLOCK //表示内核读缓冲区中没数据 read==-1 && errno == EINTR //表示读操作被打断,需要重新读 write==-1 && errno == EWOULDBLOCK //表示内核写缓冲区已满 write==-1 && errno == EINTR //表示写操作被打断,需要重新写 write==-1 && errno == EPIPE //表示写端关闭,也即对端的读端关闭了
io多路复用
io 多路复用只负责检测 io,不负责操作 io;
int n = epoll_wait(epfd, evs, sz, timeout);
timeout = -1 一直阻塞直到网络事件到达;
timeout = 0 不管是否有事件就绪立刻返回;
timeout = 1000 最多等待 1 s,如果1 s内没有事件触发则返 回
如何检测io
连接建立:
1、接收连接的时候,io多路复用一般去监听listenfd的读事件,当三次握手完成之后(这是在协议栈完成的),会在全连接队列中新增一条连接,此时协议栈会发送一个信号给到epoll,然后就触发读信号,此时就说明接收连接已经就绪了,接下来我们再调用accept函数,此时的accept函数只会执行操作io(即只是建立连接)的功能,因为检测io是由io多路复用去做的
2、主动连接的时候,即服务器作为客户端的时候,io多路复用一般去监听socketfd的写事件,当三次握手完成了前两次,并且在第三次握手建立的同时,协议栈会发送一个信号给到epoll(因为第三次握手是我给对方发的,所以这是一个写事件),此时会触发这个写事件,也说明连接已经建立成功了
连接断开:
1、被动断开,当epoll检测到socketfd的EPOLLRDHUP,说明客户端的写端关闭,也即服务端的读端关闭;当检测到socketfd的EPOLLHUP,说明客户端的读写端都关闭了
消息到达:
1、消息到达时,io多路复用一般去监听socketfd的读事件,当协议栈的socketfd对应的读缓冲区有数据的时候,协议栈会发送一个信号给到epoll,然后就会触发epoll的读事件,此时我们调用recv函数去操作io即可,因为检测io的工作epoll已经帮我们做好了
消息发送:
1、消息到达时,io多路复用一般去监听socketfd的写事件,当协议栈的socketfd对应的写缓冲区没满的时候(也即可写),协议栈就会发送一个信号给到epoll,从而触发epoll中所管理的对应的fd的可写事件,此时我们调用send函数去操作io即可,因为检测io的工作epoll已经帮我们做好了
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
reactor编程
连接的建立
// 一、处理客户端的连接 // 1. 注册监听 listenfd 的读事件 struct epoll_event ev; ev.events |= EPOLLIN; epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &ev); // 2. 当触发 listenfd 的读事件,调用 accept 接收新的连 接 int clientfd = accept(listenfd, addr, sz); struct epoll_event ev; ev.events |= EPOLLIN; epoll_ctl(efd, EPOLL_CTL_ADD, clientfd, &ev); // 二、处理连接第三方服务 // 1. 创建 socket 建立连接 int connectfd = socket(AF_INET, SOCK_STREAM, 0); connect(connectfd, (struct sockaddr *)&addr, sizeof(addr)); // 2. 注册监听 connectfd 的写事件 struct epoll_event ev; ev.events |= EPOLLOUT; epoll_ctl(efd, EPOLL_CTL_ADD, connectfd, &ev); // 3. 当 connectfd 写事件被触发,连接建立成功 if (status == e_connecting && e->events & EPOLLOUT) { status == e_connected; // 这里需要把写事件关闭 epoll_ctl(epfd, EPOLL_CTL_DEL, connectfd, NULL); }
连接的断开
if (e->events & EPOLLRDHUP) { // 读端关闭 close_read(fd); close(fd); } if (e->events & EPOLLHUP) { // 读写端都关闭 close(fd); }
消息的到达
// reactor 要用非阻塞io // select if (e->events & EPOLLIN) { while (1) { int n = read(fd, buf, sz); if (n < 0) { if (errno == EINTR) continue; if (errno == EWOULDBLOCK) break; close(fd); } else if (n == 0) { close_read(fd); // close(fd); } // 业务逻辑了 } }
消息的发送
int n = write(fd, buf, dz); if (n == -1) { if (errno == EINTR) continue; if (errno == EWOULDBLOCK) { struct epoll_event ev; ev.events = EPOLLOUT; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); return; } close(fd); } // ... if (e->events & EPOLLOUT) { int n = write(fd, buf, sz); //... if (n == sz) { epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); } }
reactor应用
redis
memcached
redis为什么不使用多线程,而memcached却使用多线程呢?
因为redis中有很多数据结构,对这些数据结构的加锁比较复杂,不好控制粒度
memcached是那种比较简单的数据结构,所以用多线程效率会非常的高
有空去了解一下主线程和子线程之间通过管道通信
nginx
问题总结
1、多线程epoll情况下,epoll_wait或者accept的惊群问题应该怎么解决?
惊群问题其实在内核中已经处理了(通过增加互斥锁的方式),不过内核中只能保证最终只有一个线程能接收到这条连接,其他线程的accept可能返回-1并且errno被置为ewouldblock,所以我们用户态不需要特殊的处理;但在nginx中,它是一种多进程方式使用的epoll,nginx通过在多个进程之间加锁的方式在用户态解决惊群,目的是为了在用户层处理连接的负载均衡。