reactor模式
服务器端
在poll模式的基础上,我们需要对代码进行一点改良。首先,由于reactor是基于事件的模型,我们不单单只关心IO监听本身,epoll池帮助我们管理了IO事件的分发,那么对于每一个fd来说,我们关系的是对应fd监听事件发生之后我们做什么样的处理,于是我们可以定一个一个自己的fd事件管理块。
我们注意到这个管理块中主要有两个板块,一个是缓冲区,一个是回调函数。
之前我们的代码中缓冲区只有两块,一个写缓冲,一个读缓冲,这会导致我们所有的连接共用一块缓冲区。
这样的好处是,代码比较简单,能够实现简单的需求。
坏处是在后面多线程的基础上会产生读写冲突而且很难管理。
于是我们为每一个fd的事件定义单独的缓冲区将事件进行隔离。
回调函数部分,对于每个监听到的IO我们定义了自己的IO处理事件的回调函数,在用户提出IO需求之前用户将事件注册在回调函数中。IO事件分发之后,在对IO进行处理时处理器分别调用不同的回调函数进行处理。
但是我们的代码中没有演示回调函数的调用,所以我们这里注释掉。
typedef struct sock_item{ //conn_item
int fd;//句柄
char *rbuffer;
int rlength;
char *wbuffer;
int wlength;
int event; //事件
//回调函数
// void (*recv_cb)(int fd,char *buffer,int length );
// void (*send_cb)(int fd,char *buffer,int length );
// void (*accept_cb)(int fd,char *buffer,int length);
}sock_item;
其次我们要考虑的是,有了这么一个控制块,我们如何分配内存。我们知道内存的资源很宝贵,假设我们想要建立100W条连接。如果我们一开始就申请100w个控制块,那么很多控制块刚开始在内存中是不会被用到的,造成内存资源的浪费,于是我们需要动态扩容。
所以我们再建立一个结构体做成类似下图链表的结构。
并且定义一个全局的reactor控制块,这个控制块可以考虑做成单例模式。
//这个结构体用来实现扩容
typedef struct eventblock
{
sock_item* items; //ITEM_LEGHTH 个管理块
struct eventblock* next; //指向下一个EventBlock 串成链表
/* data */
}eventblock ;
typedef struct reactor //可以做成单例模式
{
// Reactor(){
// epfd =epoll_create(1);
// first=NULL;
// blkcnt=0;
// }
int epfd;
eventblock* first; //指向第一个EventBlock 串成链表
int blkcnt;
//
/* data */
}reactor;
然后我们实现扩容的函数,和查找控制块的函数。
扩容函数实现
//实现一个扩容的函数
int reactor_resize(reactor* r){
if(r==NULL) return -1;
// printf("resize\n");
fflush(stdout);
//申请空间
sock_item* items=(sock_item*)malloc(ITEM_LEGHTH*sizeof(sock_item));
if(items==NULL){
printf("items申请失败");
fflush(stdout);
return -2;
}
memset(items,0,ITEM_LEGHTH*sizeof(sock_item));
eventblock* eb=(eventblock*)malloc(sizeof(eventblock));
if(eb==NULL){
printf("eventblock申请失败");
fflush(stdout);
free(items);
return -3;
}
memset(eb,0,sizeof(eventblock));
eb->items=items;
//找到队尾
if(r->first==NULL){
r->first=eb;
}else{
eventblock* tmp=r->first;
while(tmp->next!=NULL){
tmp=tmp->next;
}
tmp->next=eb;
}
r->blkcnt++;
return 0;
}
这个函数每调用一次我们就申请一个新的eventblock插在尾部。
我们首先申请items的空间,和eventblock的空间,然后判断队尾,记住考虑第一次插入的情况,将刚刚申请的eventblock插在尾部,最后我们将全局控制块r中的eventblock的数量加一。
查找控制块函数实现
sock_item* reactor_lookup(reactor *r,int fd){
int col=fd/ITEM_LEGHTH;
int row=fd%ITEM_LEGHTH;
// printf("reactor_lookup col=%d,row=%d\n",col,row);
fflush(stdout);
while(col+1>r->blkcnt){
int error=reactor_resize(r);
if(error!=0) return NULL;
}
int i=0;
eventblock* find=r->first;
for(;i<col&&find!=NULL;i++,find=find->next);
return &(find->items[row]);
}
这里我们首先通过索引fd的值确定第几个控制块和控制块的哪个位置,然后进行是否扩容的判断。
最后我们将找到的控制块指针返回。
如何修改代码
我们在接受客户端连接后,就查找对应客户端fd的控制块,然后申请缓冲区空间,并将信息储存进去。
//将客户端加入poll池
ev.events=EPOLLIN;
ev.data.fd=connfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
//这里找到对应的sock_item并申请缓冲空间
sock_item* sock_client=reactor_lookup(r,connfd);
if(sock_client==NULL){
printf("debug sock_client apply faild\n");
fflush(stdout);
}else{
// printf("debug sock_client=%p\n",(void *)sock_client);
// printf("debug items=%p\n",(void *)sock_client->r);
fflush(stdout);
}
sock_client->fd=connfd;
sock_client->rbuffer=calloc(1, BUFFER_LENGTH);
sock_client->rlength=0;
sock_client->wbuffer=calloc(1, BUFFER_LENGTH);
sock_client->wlength=0;
在读写客户端之前。调用查找函数找到对应控制块进行响应操作
else if(events[i].events&EPOLLIN){
//这里先获取到sock_item
sock_item* sock_client=reactor_lookup(r,clientfd);
char *rbuff=sock_client->rbuffer;//拿到缓冲区指针
...
...
读写
}
客户端
可以考虑用Linux下的wrk进行测试,这里我是自己手写了一个客户端,循环端口建立tcp连接,访问服务器
客户端比较简单,这里不做分析
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define START_PORT 1025
#define END_PORT 65535
#define TARGET_PORT 9995
int sockfds[END_PORT]={0};
int idx=0;
void connectAndSendData(const char *target_ip,int port) {
struct sockaddr_in server_addr,client_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
if (inet_pton(AF_INET, target_ip, &(server_addr.sin_addr)) <= 0) { //转化点分式ip成为long
perror("inet_pton");
return;
}
server_addr.sin_port = htons(TARGET_PORT);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
return;
}
//在这里指定port 并且bind
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(port);
if (bind(sockfd, (struct sockaddr *)&client_addr, sizeof(client_addr)) < 0) {
perror("bind");
close(sockfd);
return;
}
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == 0) {
// printf("Connected to %s:%d\n", target_ip, TARGET_PORT);
sockfds[idx++]=sockfd;
if(idx%100==0){ printf("client %d connect\n",idx);}
// 发送数据
char *message = "Hello, server!";
if (send(sockfd, message, strlen(message), 0) == -1) {
perror("send");
} else {
// printf("Data sent successfully.\n");
}
}
// close(sockfd);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <target_ip>\n", argv[0]);
return 1;
}
char *target_ip = argv[1];
for (int port = START_PORT; port <= END_PORT; port++) {
connectAndSendData(target_ip,port);
}
return 0;
}
测试
最后我们给出连接测试,这里我使用一台虚拟机连接服务器,读者可以试着使用两到三台虚拟机当作客户端进行压力测试。因为一张网卡只能够使用65535个端口。
服务端可以看到接收到60000多条连接
客户端每次建立连接发送并且收到一条数据,这里我没有打印,因为IO操作影响测试的速度。