网络模型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 + 单进程/单线程

该方案示意图如下(以进程举例):
Alt

Reactor 对象通过 select/poll/epoll 监控连接事件,收到事件后通过 dispatch 进行分发。

如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。

如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的Handler)来进行响应。Handler 会完成 read-> 处理 ->send 的完整业务流程。

这种优点很明显,就是简单,不用考虑进程间通信、线程安全、资源竞争等问题。但是也有自身的局限性,就是无法利用多核资源,只适用于业务处理非常快速的场景,Redis就是采用的这种方案。

单Reactor + 多线程

该方案示意图如下:
Alt

与第一种方案相比,不同的是:Handler只负责响应事件,并不负责处理事件,Handler读取数据后会发送给Processor进行处理。Processor在子线程中完成业务处理,然后将结果发送给Handler。由Handler将结果返回给client。

你可能主要到没有列出单Reactor + 多进程方案,主要因为如果采用多进程,就要考虑进程间通信的问题,比如子进程处理完成后需要通知父进程将结果返回给对应的client,处理比较复杂。但多线程之间数据是共享的,复杂度相对比较低。

另外,这种方案下,主线程承担了所有的事件监听和响应。瞬间高并发时可能会成为性能瓶颈。这时就需要多Reactor的方案了。

多Reactor + 多进程/多线程

该方案示意图如下(以进程举例):
Alt
父进程中 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(&current, NULL);

        int time_used = TIME_SUB_MS(current, begin);
        memcpy(&begin, &current, 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连接看是否可以正常连接及正常收发数据)
Alt

技术参考

文章参考<零声教育>的C/C++linux服务器高级架构系统教程学习: Linux C/C++高级开发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值