网络编程原则
明确:网络IO的职责
也就是说,封装是可以从两个层面来封装,一个是从操作IO的层面,比如各种接收、发送数据格式,监听数据流等,一个是从检测IO层面,也叫做事件引擎,专门用来检测IO事件。
比如redis的封装层面如下:
ps,粘包问题是在【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; // 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中就绪事件拷贝到用户态中
epoll编程
连接建立
(1)处理客户端的连接
// 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);
(2)去连接第三方服务
// 1. 创建 socket 建立连接
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
connect(connectfd, (struct sockaddr *)&addr, sizeof(addr));
//2. 等待connectfd连接成功(当可写时,也就是可以发送ACK时就表示连接成功了)
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
if (e->events & EPOLLIN) { //数据到达时也就是发生了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);
}
close(fd);
}
// ...
if (e->events & EPOLLOUT) {
int n = write(fd, buf, sz);
//...
if (n > 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
}