文章目录
网络编程关注的问题
连接的建立
分为两种:服务端处理接收客户端的连接;服务端作为客户端 连接第三方服务;
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通过在多个进程之间加锁的方式在用户态解决惊群,目的是为了在用户层处理连接的负载均衡。