⽹络编程关注的问题
连接建⽴
分为两种:服务端处理接收客户端的连接;服务端作为客户端 连接第三方服务;
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
io 函数本身可以检测 io 的状态;但是只能检测一个 fd 对应的 状态;io 多路复用可以同时检测多个 io 的状态;区别是:io函 数可以检测具体状态;io 多路复用只能检测出可读、可写、错 误、断开等笼统的事件;
操作 IO
只能使用 io 函数来进行操作;分为两种操作方式:阻塞 io 和非 阻塞 io;
readbuffer里面有数据就read出去 , writebuffer里面没有数据就write进去。
⽹络编程流程
阻塞io模型和⾮阻塞io模型
阻塞在哪⾥? 阻塞在⽹络线程
什么来决定阻塞还是⾮阻塞?
连接的fd阻塞属性决定了 io 函数是否阻塞;
fcntl(c->fd, F_SETFL, O_NONBLOCK);
// 默认情况下,fd 是阻塞的,设置非阻塞的方法如下;
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
io函数 read,wrirte ,recv , send
具体的差异?
io函数在数据未就绪时是否⽴刻返回
非阻塞IO处理方式
连接建立
connect 第一次返回-1 errno= EINPROGRESS(正在建立)
多次调用 EISCONN (已经建立)
listen(fd , backlog)
accept int fd = accept(listenfd , addr , len);
客户端fd 客户端ip和端口
accept两个功能: 1 检测全连接队列里面是否有未处理的连接信息。(检测IO)
2 如果全连接里面有节点就会返回connfd和客户端五元组信息(操作IO)
连接断开
主动 1 close 关闭读端和写端
2 shutdown shutdown(fd, SHUT_RDWR);
// 主动关闭本地读端,对端写端关闭
shutdown(fd, SHUT_RD);
// 主动关闭本地写端,对端读端关闭
shutdown(fd, SHUT_WR);
被动
read=0 连接断开 (服务端的读端关闭了)
write=-1 && errno = EPIPE (写端关闭了)
数据到达
read = -1 && EWOULDBLOCK // 没有数据
EINTR // 被中断打断了
消息发送
write = -1 && EWOULDBLOCK // 写缓冲区满
EINTR // 被中断打断
io函数只能检测一条连接的就绪状态以及操作一条连接的io数据
阻塞io模型 + 多线程
每⼀个线程处理⼀个 fd 连接 bio
优点:处理及时
缺点:线程利⽤率很低,线程的数量是有限的(如果是非阻塞io,cpu利用率低)
io多路复⽤(⽹络线程)
⽤⼀个线程来检测多个io的就绪状态,不会去操作具体io
⽔平触发的时候,io函数既可以是阻塞的也可以是⾮阻塞的。
边缘触发的时候,io函数只能是⾮阻塞的。
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; epoll_data_t data; // 保存 关联数据 }; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; }epoll_data_t;
主要函数
epoll_create系统调⽤
int epoll_create(int size);
size参数告诉内核这个epoll对象会处理的事件⼤致数量,⽽不是能够处理的事件的最⼤数。 在现在linux版本中,这个size参数已经没有意义了; 返回:epoll对象句柄;之后针对该epoll的操作需要通过该句柄来标识该epoll对象;
epoll_ctl系统调⽤
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epoll_ctl向epoll对象添加、修改或删除事件; 返回:0表示成功,-1表示错误,根据errno错误码判断错误类型。
op类型: EPOLL_CTL_ADD 添加新的事件到epoll中 、EPOLL_CTL_MOD 修改epoll中的事件 EPOLL_CTL_DEL 删除epoll中的事件。
event.events 取值:
EPOLLIN 表示该连接上有数据可读(tcp连接远端主动关闭连接,也是可读事件,因为需要处理发送来的FIN包;FIN包就是read 返回 0)
EPOLLOUT 表示该连接上可写发送(主动向上游服务器发起⾮阻塞tcp连接,连接建⽴成功事件相当于可写事件)
EPOLLRDHUP 表示tcp连接的远端关闭或半关闭连接 (服务器的读端已经关闭了)
EPOLLPRI 表示连接上有紧急数据需要读
EPOLLERR 表示连接发⽣错误
EPOLLHUP 表示连接被挂起 (读写端全部关闭)
EPOLLET 将触发⽅式设置为边缘触发,系统默认为⽔平触发
EPOLLONESHOT 表示该事件只处理⼀次,下次需要处理时需重新加⼊epoll
epoll_wait系统调⽤
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
收集 epoll 监控的事件中已经发⽣的事件,如果 epoll 中没有任何⼀个事件发⽣,则最多等待 timeout 毫秒后返回。 返回:表示当前发⽣的事件个数,返回0表示本次没有事件发⽣; 返回-1表示出现错误,需要检查errno错误码判断错误类型。
注意:
events 这个数组必须在⽤户态分配内存,内核负责把就绪事件复制到该数组中; maxevents 表示本次可以返回的最⼤事件数⽬,⼀般设置为 events 数组的⻓度; timeout表示在没有检测到事件发⽣时最多等待的时间;如果设置为0,检测到rdllist为空⽴ 刻返回;如果设置为-1,⼀直等待
原理图
要点
所有添加到epoll中的事件都会与⽹卡驱动程序建⽴回调关系,相应的事件发⽣时会调⽤这⾥的回 调⽅法(ep_poll_callback),它会把这样的事件放在rdllist双向链表中。调用 epoll_wait 将会把 rdlist 中就绪事件拷贝到 用户态中;
reactor模型
组成:⾮阻塞的io + io多路复⽤;
io多路复用负责检测IO
IO函数操作IO
为什么搭配非阻塞IO:
1 多线程环境 : 将一个listenfd交给多个epoll去处理 (如果是阻塞的IO那么其他的线程就会被阻塞住) (惊群)--->(用户层不需要处理枷锁问题,内核协议栈已经处理好了)
2 边缘触发下必须使用非阻塞IO: 读事件触发时read要再一次事件循环中把readbuffer读空
如果此时readbuffer已经为空了,阻塞IO就会被阻塞住
3 select bug : 当某个socket接收缓冲区中有新数据分节到达,然后select报告这个socket描述符可读,但随后,协议栈检查到这个新分节检验和错误,然后丢弃这个分节,这时候调用read则无数据可读,如果socket没有被设置nonblocking ,此时read将阻塞当前线程。
是不是io多路复用一定要搭配非阻塞IO:
MYSQL: select 接收数据,每条连接一个线程。
libevent: 可以加一个系统调用看看缓冲区多少数据,效率低,
int n = EVBUFFER_MAX_READ_DEFAULT;
if( ioctl(fd , FIONREAD , &n) < 0) return -1;
return n;
取出读缓冲区字节数
特征:基于事件循环,以事件驱动或者事件回调的⽅式来实现业务逻辑;
表述:将连接的io处理转化为事件处理;
单reactor模型
原理图:
代表:redis 内存数据库 数据结构太复杂,锁的粒度不好选择,所以使用单线程
redis 6.0 多线程
环境: 命令处理是单线程的 只能使用单reactor
redis为什么要使用单reactor : 1单线程业务逻辑 2 操作具体命令时间复杂度比较低
redis 针对reactor做了哪些优化: read/decode 记录日志
write/encode 获取排行榜记录
因为解协议的时间特别长,占用单线程。
networking.c IO多线程操作
单reactor模型 + 任务队列 + 线程池
原理图
代表:skynet
多reactor多线程
连接reactor处理connect然后将connfd通过pip负载均衡给不同线程的epoll管理
环境: key/value 内存数据库 , 命令处理多线程。
为什么使用: 数据结构简单,多线程处理效率高。 kv数据操作简单,更高程度并发处理业务
多进程
用户层进行处理: master进程创建的时候会创建一个共享内存,fork多个worker进程共享同一个共享内存,把这个锁放到共享内存当中,多个worker进程去争夺这把锁,谁拿到这把锁,谁就可以处理listenfd读事件
负载均很做法: 每一个进程处理 7/8 * n 个连接,当连接数大于 7/8 * n 的时候这个worker就不会再加入连接了,交给其他wroker去处理。
多reactor + 消息队列 + 线程池
业务场景中⽐较多 ⽹络密集型 + 业务密集型
TCP状态转换图
epoll处理细节图
redis连接上游服务器时要设置EPOLLOUT事件。当上游服务器回第一个ACK的时候会触发EPOLLOUT
服务端主动断开的情况:服务端主动调用close()或者shutdown(SHUT_WR)都会主动发送一个fin给客户端。当write()==-1 && errno = EPIPE此时写通道关闭,但此时仍然可能收到数据(如果客户端支持版关闭状态的话)。
系统调用的时候线程不会切换,但是中断的优先级 > 系统调用,线程会被切换出去。当read() == -1 && errno == EWOULDBLOCK 或者 errno == EINTR时我们需要重试。
三次握⼿
四次挥⼿
主动断开方在收到第三次握手数据包的时候会进入time_wait状态并等待2个数据包的时间,为什么需要等待呢,是因为我们不确定我们的ack包是否发送到对端去了,如果这个包发送失败了。那么在2个数据包的时间内又会收到对端发送来的一个fin包。然后再发送ack。
客户端主动发送fin包之后关闭自身写端,服务端收到fin之后关闭读端,但是不关闭写端,此时服务器可能还需要发送一些数据包给客户端。发送完之后服务端再把自己的写端关闭。客户端收到fin包之后把自己的读端关闭并且等待两个数据包的时间。并发送一个ack包。
这个ack包对epoll来说是EPOLLHUP , server端收到之后就可以调用close将fd关闭。close(fd)只是将这个fd的引用-1了。内核中检测到没有数据引用这个fd了,内核才会将它释放掉。
半关闭状态
背景
客户端关闭写通道,此时服务端想要推送完所有数据后再关闭; 推送系统中 close_read() close_write() close() 服务端 close_read send send ... close 客户端 close()、
实现
需要实现半关闭状态;close-wait阶段;必须要收到ack包,才能知道客户端收到了我们推送的所 有数据;
细节
发送端: shutdown(SHUT_WR) 发送⼀个 FIN 包,并且标记该 socket 为 SEND_SHUTDOWN; shutdown(SHUT_RD) 不发送任何包,但是标记该 socket 为 RCV_SHUTDOWN;
接收端: 收到 FIN 包标记该 socket 为RCV_SHUTDOWN; 对于epoll⽽⾔,如果⼀个socket同时标记为 SEND_SHUTDOWN 和 RCV_SHUTDOWN;那么poll 会返回 EPOLLHUP; 如果⼀个socket被标记为 RCV_SHUTDOWN;poll会返回 EPOLLRDHUP;
应⽤:skynet ⽀持半关闭状态
tcp-keepalive
背景
tcp是⾯向连接的,⼀般情况下,两端应⽤可以通过发送接收数据得知对端的存活;当两端都没有 数据的时候,如何判断连接是否正常?系统默认keepalive是关闭的,当keepalive开启时,可以保 持连接检测对⽅主机是否崩溃;
属性
1. tcp_keepalive_time 两端多久没有数据交换,开始发送 keepalive syn 探活包;
2. tcp_keepalive_probes 发送多少次探活包 (如果发送三次都没有收到ack则说明断开了)
3. tcp_keepalive_intvl 探活包发送间隔
这是在传输层,应用层无法感知,只有当read() == -1 && errno == ETIMEOUT的时候,我们就可以close了。
如何开启
linux全局
/etc/sysctl.conf
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
sysctl -p 使其⽣效
没有配置开启
单个连接
可以使⽤三个属性设置:TCP_KEEPCNT、TCP_KEEPIDLE、TCP_KEEPINTVL; 开启:SO_KEEPALIVE
redis源码
// 开启tcp-keepalive int val = 1; if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof(val)) == -1) { anetSetError(err, "setsockopt SO_KEEPALIVE: %s", strerror(errno)); return ANET_ERR; } // 设置 val = interval; if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &val, sizeof(val)) < 0) { anetSetError(err, "setsockopt TCP_KEEPIDLE: %s\n", strerror(errno)); return ANET_ERR; } /* Send next probes after the specified interval. Note that we set the * delay as interval / 3, as we send three probes before detecting * an error (see the next setsockopt call). */ val = interval/3; if (val == 0) val = 1; if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &val, sizeof(val)) < 0) { anetSetError(err, "setsockopt TCP_KEEPINTVL: %s\n", strerror(errno)); return ANET_ERR; } /* Consider the socket in error state after three we send three ACK * probes without getting a reply. */ val = 3; if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &val, sizeof(val)) < 0) { anetSetError(err, "setsockopt TCP_KEEPCNT: %s\n", strerror(errno)); return ANET_ERR; }
为什么应⽤层需要开启⼼跳检测?
因为传输层探活检测,⽆法判断进程阻塞或者死锁的情况;
⼼跳检测: 每隔10秒发送⼀次⼼跳包 3次没有收到 close
应⽤
1. 数据库间,主从复制,使⽤⼼跳检测;
2. 客户端与服务器,使⽤⼼跳检测;
3. 客户端->反向代理->上游服务器;反向代理与上游服务器使⽤探活检测;
4. 服务端->数据库,使⽤探活检测;对于数据库⽽⾔,服务端是否阻塞跟它⽆关;