学习笔记_网络编程中关注的问题
概念
全连接队列(sync queue)和半连接队列(accept queue)
当服务端接收到客户端第一次SYN握手请求时,将创建的request_sock结构,存储在半连接队列中(向客户端发送SYN+ACK,并期待客户端响应ACK),此时的连接在服务器端出于SYN_RECV状态。当服务端收到客户端最后的ACK确认时,将半连接中的相应条目删除,然后将相应的连接放入 全连接队列中, 此时服务端连接状态为ESTABLISHED。 进入全连接队列中的连接等待accept()调用取用
accept如果全连接满了, 半连接会报错refuse
Reactor中网络关注的问题
连接的建立
客户端连接服务器, 服务器连接第三方服务(redis, memcahched, )
客户端–>服务器–>第三方服务
int clientfd = accept(listenfd, addr, sz);
- clientfd = -1,且errno == EWOULDBLOCK,说明全连接队列为空
listen(listenfd, backllog);
- backllog, 全连接队列的长度
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
connect(connectfd, (struct sockaddr *)&addr, sizeof(addr));
- errno == ,正在建立连接
- errno == EISCONN, 连接建立成功
连接的断开
TCP是全双工, 有读端和写段两条
// 主动关闭 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 = readd(fd, buf, sz);
- 将数据从内核态的读缓冲区中读出
- 正向的错误,不需要关闭fd
- n = -1, errno = EWOULDBLOCK, 读缓冲区为空
- n = -1, errno = EINRT, 被信号打断
- 其他的n = -1时需要关闭fd
消息发送完毕
用户态
----------------------------------
|读缓冲区| |写缓冲区| 内核态
int n = write(fd, buf, dz);
if (n == -1) {
if (errno == EINTR || errno == EWOULDBLOCK) {
return;
}
close(fd);
}
- 将数据从用户态写到内核态的写缓冲区中
- n = -1, errno = EWOULDBLOCK, 写缓冲区已经满了, 只能等空了再去写
- n = -1, errno = EINRT, 被信号打断
- n = sz, 写入成功
网络IO职责
检测IO
io 函数本身可以检测 io 的状态;但是只能检测一个 fd 对应的状态;io 多路复用可以同时检测多个io的状态;
区别是:io函数可以检测具体状态;io 多路复用只能检测出可读、可写、错误、断开等笼统的事件;
io多路复用没告诉是具体的错误, 可以通过getsockopt查看具体的错误
操作IO, 阻塞IO和非阻塞IO
- 阻塞在网络线程;
- 连接的 fd 阻塞属性决定了 io 函数是否阻塞;
- 具体差异在:io 函数在数据未到达时是否立刻返回;
accept阻塞则说明全链接队列中没有数据
// 默认情况下,fd 是阻塞的,设置非阻塞的方法如下;
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
IO多路复用
只负责检测IO,不负责操作IO
数据准备,就绪队列是否准备好
epoll_wait(epfd, evs, sz, timeout);
- timeout = -1,永久阻塞
- timeout = 0, 非阻塞, 立刻返回
- timeout > 0,设置超时时间
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 epollet
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);
- 通常设置512
- 需要主要的是客户端对应的连接事件是写事件,服务端对应的是读事件
- 需要注意的epoll事件
- EPOLLIN,可读事件
当一个连接需要建立的时候,客户端会发送syn+seq number,此时也代表收到了数据 - EPOLLOUT,可写事件
- EPOLLRDHUP,读端关闭事件
- EPOLLRDHUP,读写端都关闭事件
- EPOLLIN,可读事件
- 需要注意的errno错误码
- EWOULDBLOCK,操作将被阻塞
全连接队列满了,写缓存区满了,读缓存区为空 - EINTR
表示收到了终端信号,对于程序来说应该这个是非致命的
- EWOULDBLOCK,操作将被阻塞
epoll连接的建立
//一、处理客户端的连接
//1.注册监听listenfd的读事件
struct epoll_eventev;
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连接的断开
// 表示读关闭的标志
f(e->events&EPOLLRDHUP){
//读端关闭
(fd);
//close(fd);
}
// EPOLLHUP 表示读写都关闭
if(e->events&EPOLLHUP){
//读写端都关闭
close(fd);
}
epoll数据到达
if(e->events&EPOLLIN){
intn=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);
}
}
epoll_数据发送完毕
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);
}
close(fd);
}// ...
if (e->events & EPOLLOUT) {
int n = write(fd, buf, sz); //...
if (n > 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
}
编程
对于客户端, 单ack包发送出去后说明连接建立成功,所以connect要注册写事件
对于reactor中为什么要用非阻塞IO
触发了读事件,为什么缓冲区中没数据
可能数据到达后读事件已经被取消掉了
为什么新的连接不注册写事件
因为新连接的写缓冲区一定是空, 注册了写事件之后会立即触发写事件
一般write函数直接调用就可以了, 当write失败之后, ret = -1, errno = EWOULDBLOCK才去注册写事件, 写事件触发后再尝试写, 写完之后再将写事件删除
Reactor应用
组成:IO多路复用+非阻塞IO
事件方式通知,事件循环
将对IO的处理转换成对事件的处理
单reactor-redis
水平触发
mask用来映射红黑树中节点的状态
aeFiredEvent-event的封装
aeEventLoop-reactor的封装
acCreateEventLoop-创建reactor
为什么要把read放在网络线程中,写操作放在多线程中?
数据错乱的问题? 写入顺序没有问题,子需要加一个cas锁
为什么不在多线程读?会有数据错乱的问题, 一个线程读了一半,另外一个线程读一半
多reactor-memcached
水平触发
同过epoll_creat创建多个epoll
利用一个reactor专门处理accept,来保证连接的时效
nginx
边缘触发
做反向代理的,将一条数据转发到不同的后端服务器中
listen之后fork
…