c linux网络编程原理讲解(三)

网络编程原理讲解三

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操作影响测试的速度。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值