高效通信的新范式:探索异步请求在网络设计中的创新思路

2024-09-13 21:12:18 浏览数 (3)

一、前言

Linux网络设计是指在Linux操作系统环境下,构建和优化计算机网络的架构和方案。它对于现代社会中的各个领域都至关重要,包括互联网、云计算、物联网等。

重要性:

  1. 可靠性和稳定性:作为一种开源的操作系统,Linux以其强大的稳定性和可靠性而闻名。因此,在网络设计中采用Linux能够提供更高的可靠性和稳定性,确保数据传输和网络连接的稳固性。
  2. 可扩展性:Linux网络设计提供了灵活的模块化结构,使得网络可以根据需求进行扩展和定制。这使得在变化多端的网络环境中能够自由地添加新的功能和服务,适应不断增长的业务需求。
  3. 安全性:Linux以其出色的安全性而闻名,具备强大的防护机制和安全特性。在网络设计中采用Linux可以提供高级的安全措施,保护数据和资源免受威胁和攻击。
  4. 开放性和自由度:作为开源平台,Linux鼓励用户参与和贡献代码,享有更大的自由度。这意味着在网络设计中,可以根据实际需求进行源代码定制和修改,满足特定需求并与其他系统和设备无缝集成。

挑战:

  1. 复杂性:Linux网络设计涉及多个组件和协议,需要深入理解各种技术和架构原理。此复杂性对于初学者或非专业人士来说可能是一种挑战,需要耐心学习和实践。
  2. 网络规模和负载:随着网络规模的持续增长和复杂性的提高,网络设计可能面临更大的挑战。

异步请求作为提升通信效率的创新思路在Linux网络设计中具有重要意义。传统的阻塞模型在进行网络通信时,通常需要等待请求的返回结果才能继续执行后续操作,这会导致资源的闲置和效率的降低。而异步请求则通过将请求发送后立即返回,并通过回调函数或事件通知的方式处理响应结果,实现了非阻塞式的通信机制。

使用异步请求的优势包括:

  1. 提高系统的并发性:通过异步请求,可以在等待响应的同时处理其他任务,充分利用系统资源,提高系统的并发性能。
  2. 减少等待时间:由于异步请求不需要等待响应结果就能继续执行后续操作,能够减少等待时间,提高通信效率。
  3. 可扩展性:异步请求使得系统能够同时处理多个请求,有助于应对大量并发请求的情况,提高系统的可扩展性。

异步请求也面临一些挑战:

  1. 处理复杂性:异步请求需要设计合适的回调函数或事件处理机制,以确保正确处理返回结果并保持代码的清晰可读性。
  2. 线程安全性:多个异步请求可能会同时访问共享资源,因此需要采取合适的线程同步机制,确保数据的一致性和安全性。
  3. 调试和排错:异步请求的复杂性会增加调试和排错的难度,因为请求和响应的处理不再是线性的。

二、同步和异步的概念

用于形容两者的关系。 同步: 所谓同步,就是发起一个请求时,在返回结果前,该调用不会返回。类似串行的概念。 异步: 异步的概念和同步相对,当发起一个请求时,该调用立刻返回,不等待结果,实际返回的结果由另外的线程 / 进程处理。类似并行的概念。 使用异步的典型例子:NTP时间同步服务器。

同步和异步是用来描述不同的操作方式或通信模式的概念。

  1. 同步(Synchronous):在同步操作中,调用者发起一个请求后,需要等待该操作完成并返回结果,然后才能继续执行后续操作。这意味着调用者会阻塞等待操作的完成。同步操作通常按照顺序依次执行,每个操作必须等待前一个操作完成才能开始。
  2. 异步(Asynchronous):在异步操作中,调用者发起一个请求后,不需要等待该操作的完成,可以继续执行后续操作。异步操作会在后台进行处理,并通过回调函数、事件通知或轮询等方式,在适当的时机将结果返回给调用者。异步操作不会阻塞调用者的执行流程,而是充分利用等待时间去执行其他任务。

对比:

  • 同步操作强调顺序执行,调用者必须等待结果返回后才能继续执行。
  • 异步操作强调非阻塞执行,调用者不需要等待结果返回,可以先执行后续操作。

选择使用同步还是异步操作取决于具体的应用场景和需求。同步操作简单直观,适用于简单任务或依赖前后顺序的操作。异步操作适合处理复杂、耗时较长或需要同时处理多个任务的情况,可以提高系统的并发性和响应能力。

三、异步的优缺点

优点: 异步比同步的性能要好。因为异步的发起请求和处理结果是分开的,不像同步需要等待有结果才返回调用,所以性能比同步高。 缺点: 异步不易理解,流程不清晰。

优点:

  1. 提高系统并发性能:异步操作可以在等待某个任务的同时执行其他任务,从而充分利用系统资源,提高系统的并发性能和吞吐量。
  2. 改善响应性能:由于异步操作不需要等待结果返回,调用者可以立即执行后续操作,使得系统能够更快地响应用户请求,提高用户体验。
  3. 资源利用率高:在等待IO操作完成时,异步操作可以释放CPU等资源,避免资源的闲置浪费,提高系统的资源利用率。
  4. 高度可扩展性:通过异步操作,系统能够同时处理大量的并发请求,适应高负载场景,并具有更好的扩展性。

缺点:

  1. 处理复杂性增加:异步操作通常需要设计回调函数或事件处理机制,代码逻辑较同步操作更加复杂,需要额外的开发和维护工作。
  2. 调试和排错困难:异步操作的执行流程相对复杂,当出现错误时,调试和排错会更加困难,需要细致地跟踪异步操作的过程。
  3. 可能引发并发问题:由于异步操作通常涉及共享资源的访问,如果对共享资源的并发访问控制不当,可能引发竞态条件、死锁等并发问题。
  4. 不适用于简单任务:对于一些简单的任务或者依赖前后顺序的操作,使用异步操作可能会引入额外的复杂性,反而降低代码的可读性和易用性。

四、异步实现逻辑

异步的实现主要分为四个部分。

4.1、初始化环境

实现一个初始化上下文环境的函数。主要是创建一个处理返回结果的线程。 示例:

代码语言:javascript复制
int async_client_init(struct async_context *ctx)
{
	if (ctx == NULL)
		return -EINVAL;
	
	ctx->epfd = epoll_create(1);// IO多路复用器

	// 开启处理结果的线程
	int ret = pthread_create(&ctx->thid, NULL, dns_async_client_callback, ctx);

	if (ret)
	{
		perror("pthread_create error");
		return -1;
	}

	return 0;
}

4.2、处理返回结果

实现一个线程回调函数,主要用于使用IO多路复用器监测是否有数据返回、接收数据和处理数据。 伪代码:

代码语言:javascript复制
void * async_client_callback(void *arg)
{
	struct async_context *ctx = (struct async_context *)arg;
	while (1)
	{
		// 监测可读事件
		//...
		
		//  获取与socketfd关联的数据
		//...
		
		// 从读缓冲区中读取数据
		//...
		
		// 解析数据
		//...
		
		// 删除事件监听
		//...
		
		//释放内存
		//...
	}
}

4.3、发起请求

实现一个请求函数。主要是创建socket、建立连接、准备协议、发送协议、添加fd到epoll等IO多路复用器中。 示例:

代码语言:javascript复制
int async_client_commit(struct async_context *ctx,const char *domain, async_result_cb callback)
{
	// 创建 socket
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sockfd < 0) {
		perror("create socket failedn");
		exit(-1);
	}
	// 配置socket相关信息
	struct sockaddr_in dest;
	bzero(&dest, sizeof(dest));
	dest.sin_family = AF_INET;
	dest.sin_port = htons(53);
	dest.sin_addr.s_addr = inet_addr(DNS_SVR);
	// connect目标,探路
	int ret = connect(sockfd, (struct sockaddr*)&dest, sizeof(dest));

	// 准备协议
	char request[1024] = { 0 };
	//...
	
	// 发送数据
	int slen = sendto(sockfd, request, request_length, 0, (struct sockaddr*)&dest, sizeof(struct sockaddr));

	// 传递数据
	struct ep_arg *eparg = (struct ep_arg *)calloc(1, sizeof(struct ep_arg));
	if (eparg == NULL)
		return -1;
	eparg->sockfd = sockfd;
	eparg->cb = callback;

	// 加入epoll中,监测结果返回
	struct epoll_event ev;
	ev.events = EPOLLIN;
	ev.data.ptr = eparg;

	return epoll_ctl(ctx->epfd, EPOLL_CTL_ADD, sockfd, &ev);

}

4.4、销毁

关闭相关的文件描述符,释放申请的内存块。 示例:

代码语言:javascript复制
int async_client_destory(struct async_context *ctx)
{
	if (ctx == NULL)
		return -EINVAL;

	pthread_cancel(ctx->thid);

	close(ctx->epfd);

	return 0;
}

五、异步请求实现示例

以DNS请求为例,遵循实现异步的四个过程。 示例代码:

代码语言:javascript复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <errno.h>
#include <fcntl.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include <sys/epoll.h>
#include <netdb.h>
#include <arpa/inet.h>

#include <pthread.h>

struct dns_item {
	char *domain;
	char *ip;
};

// 异步相关的结构体
#define ASYNC_EVENT_LENGTH	1024

typedef void(*async_result_cb)(struct dns_item *list, int count);

struct ep_arg {
	int sockfd;
	async_result_cb cb;
};

struct async_context {
	int epfd;
	pthread_t thid;
};

/*
*
* DNS 协议相关实现
*
*/

// ..............................

char *domain[] = {
	"www.baidu.com",
	"tieba.baidu.com",
	"news.baidu.com",
	"zhidao.baidu.com",
	"music.baidu.com",
	"image.baidu.com",
	"v.baidu.com",
	"map.baidu.com",
	"baijiahao.baidu.com",
	"xueshu.baidu.com",
	"cloud.baidu.com",
	"www.163.com",
	"open.163.com",
	"auto.163.com",
	"gov.163.com",
	"money.163.com",
	"sports.163.com",
	"tech.163.com",
	"edu.163.com",
	"www.taobao.com",
	"q.taobao.com",
	"sf.taobao.com",
	"yun.taobao.com",
	"baoxian.taobao.com",
	"www.tmall.com",
	"suning.tmall.com",
	"www.tencent.com",
	"www.qq.com",
	"www.aliyun.com",
	"www.ctrip.com",
	"hotels.ctrip.com",
	"hotels.ctrip.com",
	"vacations.ctrip.com",
	"flights.ctrip.com",
	"trains.ctrip.com",
	"bus.ctrip.com",
	"car.ctrip.com",
	"piao.ctrip.com",
	"tuan.ctrip.com",
	"you.ctrip.com",
	"g.ctrip.com",
	"lipin.ctrip.com",
	"ct.ctrip.com"
};
/*
*
* 异步请求池的实现
*
*/

void dns_async_client_free_domains(struct dns_item *list, int count) {
	int i = 0;

	for (i = 0; i < count; i  ) {
		free(list[i].domain);
		free(list[i].ip);
	}

	free(list);
}

void * dns_async_client_callback(void *arg)
{
	struct async_context *ctx = (struct async_context *)arg;
	while (1)
	{
		struct epoll_event events[ASYNC_EVENT_LENGTH] = { 0 };

		// 监测可读事件
		int nready = epoll_wait(ctx->epfd, events, ASYNC_EVENT_LENGTH, -1);
		
		if (nready < 0)
		{
			if (errno == EINTR || errno == EAGAIN)
				continue;
			else
				break;
		}
		else if (nready == 0)
			continue;

		printf("nready:%dn", nready);

		int i = 0;
		for (i = 0; i < nready;i  )
		{
			//  获取与socketfd关联的数据
			struct ep_arg *data = (struct ep_arg *)events[i].data.ptr;

			int clientfd = data->sockfd;

			if (events[i].events &EPOLLIN)
			{
				char buffer[1024] = { 0 };
				struct sockaddr_in addr;
				size_t addr_len = sizeof(struct sockaddr_in);

				// 从读缓冲区中读取数据
				int n = recvfrom(clientfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&addr, (socklen_t*)&addr_len);

				printf("recvfrom n : %dn", n);
				struct dns_item *domains = NULL;

				// 解析数据
				int count = dns_parse_response(buffer, &domains);

				data->cb(domains, count);

				// 删除事件监听
				epoll_ctl(ctx->epfd, EPOLL_CTL_DEL, clientfd, NULL);

				// 关闭fd
				close(clientfd);
				//释放内存
				dns_async_client_free_domains(domains, count);
				free(data);
			}
		}
	}
}

int dns_async_client_init(struct async_context *ctx)
{
	if (ctx == NULL)
		return -EINVAL;

	// 创建一个epoll对象
	ctx->epfd = epoll_create(1);

	// 开启线程,检测结果返回
	int ret = pthread_create(&ctx->thid, NULL, dns_async_client_callback, ctx);

	if (ret)
	{
		perror("pthread_create error");
		close(ctx->epfd);
		return -1;
	}

	usleep(1); //child go first

	return 0;
}

int dns_async_client_destory(struct async_context *ctx)
{
	if (ctx == NULL)
		return -EINVAL;

	// 关闭线程
	pthread_cancel(ctx->thid);

	// 关闭epoll对象
	close(ctx->epfd);

	return 0;
}

int dns_async_client_commit(struct async_context *ctx,const char *domain, async_result_cb callback)
{
	// 创建 socket
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sockfd < 0) {
		perror("create socket failedn");
		exit(-1);
	}

	printf("url:%sn", domain);

	// 配置socket相关信息
	struct sockaddr_in dest;
	bzero(&dest, sizeof(dest));
	dest.sin_family = AF_INET;
	dest.sin_port = htons(53);
	dest.sin_addr.s_addr = inet_addr(DNS_SVR);

	// connect目标,探路
	int ret = connect(sockfd, (struct sockaddr*)&dest, sizeof(dest));
	printf("connect :%dn", ret);

	// 准备协议
	struct dns_header header = { 0 };
	dns_create_header(&header);

	struct dns_question question = { 0 };
	dns_create_question(&question, domain);

	char request[1024] = { 0 };
	int req_len = dns_build_request(&header, &question, request);
	
	// 发送数据
	int slen = sendto(sockfd, request, req_len, 0, (struct sockaddr*)&dest, sizeof(struct sockaddr));

	// 传递数据
	struct ep_arg *eparg = (struct ep_arg *)calloc(1, sizeof(struct ep_arg));
	if (eparg == NULL)
		return -1;
	eparg->sockfd = sockfd;
	eparg->cb = callback;

	// 加入epoll中,监测结果返回
	struct epoll_event ev;
	ev.events = EPOLLIN;
	ev.data.ptr = eparg;

	return epoll_ctl(ctx->epfd, EPOLL_CTL_ADD, sockfd, &ev);

}

static void dns_async_client_result_callback(struct dns_item *list,int count)
{
	int i = 0;
	for (i = 0; i < count; i  )
	{
		printf("name: %s, ip: %sn", list[i].domain, list[i].ip);
	}
}

int main(int argc, char *argv[]) {

	int count = sizeof(domain) / sizeof(domain[0]);
	int i = 0;
	// 申请一个保存上下文的内存块
	struct async_context *ctx=calloc(1,sizeof(struct async_context));
	if (ctx == NULL)
		return -1;
	
	// 初始化
	int ret = dns_async_client_init(ctx);
	if (ret < 0)
	{
		free(ctx);
		return -2;
	}

	for (i = 0; i < count; i  ) {
		// 发起请求
		dns_async_client_commit(ctx, domain[i],dns_async_client_result_callback);
	}
	getchar();
	
	printf("exitn");
	// 销毁
	dns_async_client_destory(ctx);
	free(ctx);

}

DNS协议部分的实现,协议的准备,协议的处理等。

代码语言:javascript复制
#define DNS_SVR				"114.114.114.114"
#define DNS_HOST			0x01
#define DNS_CNAME			0x05

struct dns_header {
	unsigned short id;
	unsigned short flags;
	unsigned short qdcount;
	unsigned short ancount;
	unsigned short nscount;
	unsigned short arcount;
};

struct dns_question {
	int length;
	unsigned short qtype;
	unsigned short qclass;
	char *qname;
};
int dns_create_header(struct dns_header *header) {

	if (header == NULL) return -1;
	memset(header, 0, sizeof(struct dns_header));

	srandom(time(NULL));

	header->id = random();
	header->flags |= htons(0x0100);
	header->qdcount = htons(1);

	return 0;
}

int dns_create_question(struct dns_question *question, const char *hostname) {

	if (question == NULL) return -1;
	memset(question, 0, sizeof(struct dns_question));

	question->qname = (char*)malloc(strlen(hostname)   2);
	if (question->qname == NULL) return -2;

	question->length = strlen(hostname)   2;

	question->qtype = htons(1);
	question->qclass = htons(1);

	const char delim[2] = ".";

	char *hostname_dup = strdup(hostname);
	char *token = strtok(hostname_dup, delim);

	char *qname_p = question->qname;

	while (token != NULL) {

		size_t len = strlen(token);

		*qname_p = len;
		qname_p   ;

		strncpy(qname_p, token, len 1);
		qname_p  = len;

		token = strtok(NULL, delim);
	}

	free(hostname_dup);

	return 0;
	
}

int dns_build_request(struct dns_header *header, struct dns_question *question, char *request) {

	int header_s = sizeof(struct dns_header);
	int question_s = question->length   sizeof(question->qtype)   sizeof(question->qclass);

	int length = question_s   header_s;

	int offset = 0;
	memcpy(request offset, header, sizeof(struct dns_header));
	offset  = sizeof(struct dns_header);

	memcpy(request offset, question->qname, question->length);
	offset  = question->length;

	memcpy(request offset, &question->qtype, sizeof(question->qtype));
	offset  = sizeof(question->qtype);

	memcpy(request offset, &question->qclass, sizeof(question->qclass));

	return length;
	
}

static int is_pointer(int in) {
	return ((in & 0xC0) == 0xC0);
}
static void dns_parse_name(unsigned char *chunk, unsigned char *ptr, char *out, int *len) {

	int flag = 0, n = 0, alen = 0;
	char *pos = out   (*len);

	while (1) {

		flag = (int)ptr[0];
		if (flag == 0) break;

		if (is_pointer(flag)) {
			
			n = (int)ptr[1];
			ptr = chunk   n;
			dns_parse_name(chunk, ptr, out, len);
			break;
			
		} else {

			ptr   ;
			memcpy(pos, ptr, flag);
			pos  = flag;
			ptr  = flag;

			*len  = flag;
			if ((int)ptr[0] != 0) {
				memcpy(pos, ".", 1);
				pos  = 1;
				(*len)  = 1;
			}
		}
	
	}
	
}

static int dns_parse_response(char *buffer, struct dns_item **domains) {

	int i = 0;
	unsigned char *ptr = buffer;

	ptr  = 4;
	int querys = ntohs(*(unsigned short*)ptr);

	ptr  = 2;
	int answers = ntohs(*(unsigned short*)ptr);

	ptr  = 6;
	for (i = 0;i < querys;i   ) {
		while (1) {
			int flag = (int)ptr[0];
			ptr  = (flag   1);

			if (flag == 0) break;
		}
		ptr  = 4;
	}

	char cname[128], aname[128], ip[20], netip[4];
	int len, type, ttl, datalen;

	int cnt = 0;
	struct dns_item *list = (struct dns_item*)calloc(answers, sizeof(struct dns_item));
	if (list == NULL) {
		return -1;
	}

	for (i = 0;i < answers;i   ) {
		
		bzero(aname, sizeof(aname));
		len = 0;

		dns_parse_name(buffer, ptr, aname, &len);
		ptr  = 2;

		type = htons(*(unsigned short*)ptr);
		ptr  = 4;

		ttl = htons(*(unsigned short*)ptr);
		ptr  = 4;

		datalen = ntohs(*(unsigned short*)ptr);
		ptr  = 2;

		if (type == DNS_CNAME) {

			bzero(cname, sizeof(cname));
			len = 0;
			dns_parse_name(buffer, ptr, cname, &len);
			ptr  = datalen;
			
		} else if (type == DNS_HOST) {

			bzero(ip, sizeof(ip));

			if (datalen == 4) {
				memcpy(netip, ptr, datalen);
				inet_ntop(AF_INET , netip , ip , sizeof(struct sockaddr));

				printf("%s has address %sn" , aname, ip);
				printf("tTime to live: %d minutes , %d secondsn", ttl / 60, ttl % 60);

				list[cnt].domain = (char *)calloc(strlen(aname)   1, 1);
				memcpy(list[cnt].domain, aname, strlen(aname));
				
				list[cnt].ip = (char *)calloc(strlen(ip)   1, 1);
				memcpy(list[cnt].ip, ip, strlen(ip));
				
				cnt   ;
			}
			
			ptr  = datalen;
		}
	}

	*domains = list;
	ptr  = 2;

	return cnt;
	
}

六、思考

1、UDP为什么需要调用connect()函数? connect()函数会发送一个数据包,UDP主要用它作探测 / 探路。

2、函数的返回值有哪些形式? (1)将数据结构作为参数,传给函数(推荐)。这种方式,变量的内存在函数外申请,由函数调用的地方进行管理。例如:

代码语言:javascript复制
int func(struct value *arg)
{
	//......
	return 0;
};

(2)内部创建数据结构变量,返回值返回。这种方式由函数申请内存,调用方管理释放;这种方式不注意的话容易造成内存泄漏。例如:

代码语言:javascript复制
struct value * func(void)
{
	struct value *arg=alloc(1,sizeof(struct value));
	//......
	return arg;
};

(3)将数据结构作为参数,传给函数,同时内部创建数据结构变量,返回值返回(不推荐)。这种方式会添加函数的复杂性。例如:

代码语言:javascript复制
struct value * func(struct value *arg)
{
	struct value *arg1=alloc(1,sizeof(struct value));
	//......
	return arg1;
};

七、总结

异步将请求和接收分离,一个线程负责发起请求,一个线程负责接收返回,减少了等待结果的过程,性能比较高;但不符合人类的理解思维,因此理解起来不那么容易。 实现示例代码中可以作为一个异步请求池的模板,只要将DNS协议部分换成其他的协议实现即可(如HTTP)。

0 人点赞