网络模型Reactor的实现和应用
IO模型
常见的IO模型有四种:
(1)同步阻塞IO(BlockingIO):即传统的IO模型,例如:read、write、send、recv等。
(2)同步非阻塞IO(Non-blockingIO):默认创建的socket都是阻塞的,非阻塞IO要求socket设置非阻塞标志NONBLOCK。
(3)IO多路复用(IOMultiplexing):通过io多路复用select,poll,epoll可以实现异步阻塞IO,即Reactor模型。
(4)异步IO(AsynchronousIO):即经典的Proactor设计模式,也称为异步非阻塞IO。
Reactor事件驱动模型的原理与实现
单Reactor + 单进程/单线程
该方案示意图如下(以进程举例):
Reactor 对象通过 select/poll/epoll 监控连接事件,收到事件后通过 dispatch 进行分发。
如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。
如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的Handler)来进行响应。Handler 会完成 read-> 处理 ->send 的完整业务流程。
这种优点很明显,就是简单,不用考虑进程间通信、线程安全、资源竞争等问题。但是也有自身的局限性,就是无法利用多核资源,只适用于业务处理非常快速的场景,Redis就是采用的这种方案。
单Reactor + 多线程
该方案示意图如下:
与第一种方案相比,不同的是:Handler只负责响应事件,并不负责处理事件,Handler读取数据后会发送给Processor进行处理。Processor在子线程中完成业务处理,然后将结果发送给Handler。由Handler将结果返回给client。
你可能主要到没有列出单Reactor + 多进程方案,主要因为如果采用多进程,就要考虑进程间通信的问题,比如子进程处理完成后需要通知父进程将结果返回给对应的client,处理比较复杂。但多线程之间数据是共享的,复杂度相对比较低。
另外,这种方案下,主线程承担了所有的事件监听和响应。瞬间高并发时可能会成为性能瓶颈。这时就需要多Reactor的方案了。
多Reactor + 多进程/多线程
该方案示意图如下(以进程举例):
父进程中 mainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor接收,将新的连接分配给某个子进程。
子进程的 subReactor 将 mainReactor 分配的连接加入连接队列进行监听,并创建一个Handler 用于处理连接的各种事件。
当有新的事件发生时,subReactor 会调用连接对应的 Handler(即第 2 步中创建的Handler)来进行响应。
Handler 完成 read→处理→send 的完整业务流程。
目前著名的开源系统 Nginx 采用的是多 Reactor 多进程,采用多 Reactor 多线程的实现有Memcache 和 Netty。不过需要注意的是 Nginx 中与上图中的方案稍有差异,具体表现在主进程中并没有mainReactor来建立连接,而是由子进程中的subReactor建立。
基于单Reactor+单进程实现TCP百万并发实战
原理图
环境
4c4g,ubuntu(四台,三个用来跑客户端),vscode(开发工具),NetAssist(网络测试工具)
服务端实现步骤
1、创建epoll;
// 创建epoll
int epfd = epoll_create(1024);
2、监听20个端口,分别将对应的socketFd的accept监听事件添加到epoll内;
for (int i = 0; i < MAX_PORTS; ++i) {
int socket_fd = init_socket(port + i); // 创建socketfd
// conn_info_list保存socketfd与事件回调函数之间的映射关系
conn_info_list[socket_fd].fd = socket_fd;
conn_info_list[socket_fd].r_action.recv_callback = accept_cb;
// 设置accept监听事件到epoll
set_event(socket_fd, EPOLLIN, 1);
}
3、epoll_wait循环等待就绪事件,根据事件类型调用对应就绪事件的回调函数;
while(1) {
struct epoll_event events[MAX_EVENTS] = {0};
int nreadys = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nreadys < 0) {
printf("epoll error, epfd: %d.\n", epfd);
continue;
}
for (int i = 0; i < nreadys; ++i) {
int connfd = events[i].data.fd;
if (events[i].events & EPOLLIN) {
conn_info_list[connfd].r_action.recv_callback(connfd);
}
if (events[i].events & EPOLLOUT) {
conn_info_list[connfd].send_callback(connfd);
}
}
}
- accept_cb
int accept_cb(int fd) {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// printf("waiting for accept client, socket fd: %d\n", fd);
int client_fd = accept(fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd < 0) {
printf("accept error, err: %d --> %s\n", errno, strerror(errno));
return client_fd;
}
// printf("success to accept client, client_fd: %d\n", client_fd);
conn_info_list[client_fd].fd = client_fd;
conn_info_list[client_fd].r_action.recv_callback = recv_cb;
conn_info_list[client_fd].send_callback = send_cb;
memset(conn_info_list[client_fd].rbuf, 0, BUF_SIZE);
conn_info_list[client_fd].rlen = 0;
memset(conn_info_list[client_fd].wbuf, 0, BUF_SIZE);
conn_info_list[client_fd].wlen = 0;
set_event(client_fd, EPOLLIN | EPOLLET, 1); // | EPOLLET
if (client_fd % 1000 == 0) {
struct timeval current;
gettimeofday(¤t, NULL);
int time_used = TIME_SUB_MS(current, begin);
memcpy(&begin, ¤t, sizeof(struct timeval));
printf("success to accept client, client_fd: %d, time used: %d\n", client_fd, time_used);
}
return 0;
}
- recv_cb
int recv_cb(int fd) {
memset(conn_info_list[fd].rbuf, 0, BUF_SIZE);
char *buffer = conn_info_list[fd].rbuf;
int count = recv(fd, buffer, BUF_SIZE, 0);
if (count < 0) {
perror("recv");
printf("client error, client_fd: %d.\n", fd);
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
return -1;
}
if (count == 0) {
// printf("client disconnected, client_fd: %d.\n", fd);
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
return -1;
}
// printf("recv %d bytes: %s\n", count, conn_info_list[fd].rbuf);
conn_info_list[fd].rlen = count;
conn_info_list[fd].wlen = count;
memcpy(conn_info_list[fd].wbuf, buffer, conn_info_list[fd].rlen);
// printf("[%d]RECV: %s\n", count, conn_info_list[fd].wbuf);
set_event(fd, EPOLLOUT, 0);
return count;
}
- send_cb
int send_cb(int fd) {
int count = 0;
if (conn_info_list[fd].wlen != 0) {
count = send(fd, conn_info_list[fd].wbuf, conn_info_list[fd].wlen, 0);
if (count < 0) {
perror("send");
printf("client error, fd: %d.\n", fd);
return -1;
}
}
set_event(fd, EPOLLIN, 0);
return count;
}
测试
环境准备,因为测百万并发需要服务器改动一些配置
vi /etc/sysctl.conf
net.ipv4.tcp_mem = 262144 786432 786432
net.ipv4.tcp_wmem = 1024 1024 2048
net.ipv4.tcp_rmem = 1024 1024 2048
fs.file-max = 1048576
net.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 1200
让以上修改生效:sysctl -p
客户端程序
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <errno.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>
#define MAX_BUFFER 128
#define MAX_EPOLLSIZE (384*1024)
#define MAX_PORT 20
#define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)
int isContinue = 0;
static int ntySetNonblock(int fd) {
int flags;
flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) return flags;
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) < 0) return -1;
return 0;
}
static int ntySetReUseAddr(int fd) {
int reuse = 1;
return setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
}
int main(int argc, char **argv) {
if (argc <= 2) {
printf("Usage: %s ip port\n", argv[0]);
exit(0);
}
const char *ip = argv[1];
int port = atoi(argv[2]);
int connections = 0;
char buffer[128] = {0};
int i = 0, index = 0;
struct epoll_event events[MAX_EPOLLSIZE];
int epoll_fd = epoll_create(MAX_EPOLLSIZE);
strcpy(buffer, " Data From MulClient\n");
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
struct timeval tv_begin;
gettimeofday(&tv_begin, NULL);
while (1) {
if (++index >= MAX_PORT) index = 0;
struct epoll_event ev;
int sockfd = 0;
if (connections < 340000 && !isContinue) {
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
goto err;
}
//ntySetReUseAddr(sockfd);
addr.sin_port = htons(port+index);
if (connect(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
perror("connect");
goto err;
}
ntySetNonblock(sockfd);
ntySetReUseAddr(sockfd);
sprintf(buffer, "Hello Server: client --> %d\n", connections);
send(sockfd, buffer, strlen(buffer), 0);
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLOUT;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);
connections ++;
}
//connections ++;
if (connections % 1000 == 999 || connections >= 340000) {
struct timeval tv_cur;
memcpy(&tv_cur, &tv_begin, sizeof(struct timeval));
gettimeofday(&tv_begin, NULL);
int time_used = TIME_SUB_MS(tv_begin, tv_cur);
printf("connections: %d, sockfd:%d, time_used:%d\n", connections, sockfd, time_used);
int nfds = epoll_wait(epoll_fd, events, connections, 100);
for (i = 0;i < nfds;i ++) {
int clientfd = events[i].data.fd;
if (events[i].events & EPOLLOUT) {
// sprintf(buffer, "data from %d\n", clientfd);
send(sockfd, buffer, strlen(buffer), 0);
} else if (events[i].events & EPOLLIN) {
char rBuffer[MAX_BUFFER] = {0};
ssize_t length = recv(sockfd, rBuffer, MAX_BUFFER, 0);
if (length > 0) {
// printf(" RecvBuffer:%s\n", rBuffer);
if (!strcmp(rBuffer, "quit")) {
isContinue = 0;
}
} else if (length == 0) {
printf(" Disconnect clientfd:%d\n", clientfd);
connections --;
close(clientfd);
} else {
if (errno == EINTR || errno == EAGAIN) continue;
printf(" Error clientfd:%d, errno:%d\n", clientfd, errno);
close(clientfd);
}
} else {
printf(" clientfd:%d, errno:%d\n", clientfd, errno);
close(clientfd);
}
}
}
usleep(500);
}
return 0;
err:
printf("error : %s\n", strerror(errno));
return 0;
}
测试结果:(同时有百万连接时,可以使用NetAssist工具发起tcp连接看是否可以正常连接及正常收发数据)
技术参考
文章参考<零声教育>的C/C++linux服务器高级架构系统教程学习: Linux C/C++高级开发