C/C++教程

知识巩固源码落实之1:tcp服务端epoll实现

本文主要是介绍知识巩固源码落实之1:tcp服务端epoll实现,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1:背景描述

tcp网络通信是日常业务常常会重复实现的业务功能

===》相关的socket接口:socket,bind,listen,accept,send,recv都是我们很熟悉的

===》相关的io多路处理方案:select,poll,epoll可以根据业务场景自己抉择使用

===》但其实,简单tcp服务器实现过程中,总有一些细节需要关注,

===》以及考虑到每次重新实现,多次重写,开始思考备份一些代码。。。。

2:tcp的服务器源码demo(epoll监听客户端连接及业务处理)

作为tcp的服务器,使用epoll对可读事件进行监听(监听accept连接,以及监听接收),进行业务处理。

这里的epoll采用的ET模式。

可以使用网络串口工具进行测试,或者自己实现一个tcp的客户端。

我的代码是在linux环境下使用gcc进行编译并测试的,测试通过。

可以关注的代码细节:

===》设置socket fd为非阻塞

===》设置端口可重用

===》以及epoll事件的管理

/************************************************
info: 实现tcp服务端的代码 监听端口,获取到客户端的连接,并对数据进行解析
data: 2022/02/10
author: hlp
************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/socket.h>
#include <arpa/inet.h>

#include <sys/epoll.h>

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

//实现tcp的服务器功能
//1:创建socket
//2:bind listen accept
//3:recv send
//4:设置fd可重用,非阻塞。
//5:如何用io多路复用呢? 如果用事件机制呢?

#define VPS_PORT 9999

//创建socket
int vps_init_socket();
//创建epoll 并且进行事件监听处理
void vsp_socket_exec(int listenfd);

int main()
{
	int fd = vps_init_socket();
	if(fd < 0)
	{
		printf("create vps socket fd error. \n");
		return -1;
	}else
	{
		printf("create vps socket fd success. \n");
	}

	//epoll进行监听 回调进行处理
	vsp_socket_exec(fd);

	printf("vps socket end. \n");
	return 0;
}

//设置fd非阻塞  默认情况下  fd是阻塞的
int SetNonblock(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;
}

//创建 服务端socket,这里的ip和port写死了
int vps_init_socket()
{
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0)
	{
		printf("create socket error. \n");
		return -1;
	}
	//设置fd非阻塞  设置端口可重用
	SetNonblock(fd);
	int optval = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int));

	//定义fd相关的参数进行绑定
	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(struct sockaddr_in));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(VPS_PORT);
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	if(bind(fd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)) < 0)
	{
		printf("vps socket bind error \n");
		return -1;
	}

	//设置fd为被动套接字 供accept用  设置listen队列的大小 
	if(listen(fd , 20) < 0)
	{
		printf("vps socket listen error \n");
		return -1;
	}

	printf("create and set up socket success. start accept.. \n");
	return fd;
}

//可以梳理socket相关的接口  非阻塞  以及参数  网络字节序相关
/* #include <netinet/in.h>
typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};
struct sockaddr_in
{
	__SOCKADDR_COMMON (sin_);
	in_port_t sin_port;                 
	struct in_addr sin_addr;           
	unsigned char sin_zero[sizeof (struct sockaddr) -
	                       __SOCKADDR_COMMON_SIZE -
	                       sizeof (in_port_t) -
	                       sizeof (struct in_addr)];
};*/

// 创建epoll 返回加入事件的epollfd 失败返回-1
int create_epoll_and_add_listenfd(int listenfd);
// 作为服务器 一直对epoll进行监听 业务处理
int vps_epoll_wait_do_cycle(int epfd, int listenfd);
// 事件触发  处理连接请求
int vps_accept_exec(int epfd, int listenfd);
// 事件触发  处理可读请求 读数据 这里没监听可写,自己理解是业务不复杂频繁,我直接写入发送
int vps_recv_exec(int epfd, int connfd);

//创建epoll 监听acceptfd, 监听接收与发送的逻辑
void  vsp_socket_exec(int listenfd)
{
	//创建epollfd,并加入监听节点
	int epollfd = -1;
	if((epollfd = create_epoll_and_add_listenfd(listenfd)) <0)
	{
		printf("create epollfd error. \n");
		close(listenfd);
		return ;
	}

	printf("create epollfd [%d] success, start epoll wait... \n", epollfd);

	//使用epoll_wait对epoll进行监听
	vps_epoll_wait_do_cycle(epollfd, listenfd);
	return;
}

//创建epoll 并且给epoll增加一个监听节点 EPOLL_ADD  listenfd
int create_epoll_and_add_listenfd(int listenfd)
{
	//创建epoll
	int epfd = -1;
	epfd = epoll_create(1); //参数已经忽略必须大于0
	if(epfd == -1)
	{
		printf("create vsp epoll error. \n");
		return -1;
	}
	//epoll_ctl加入一个节点
	struct epoll_event event;
	event.data.fd = listenfd;
	event.events = EPOLLIN | EPOLLET;  //监听接入 采用ET

	if(epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event) == -1)
	{
		printf("vps epoll add listenfd error. \n");
		close(epfd);
		return -1;
	}
	printf("vps epoll create success and add listenfd success.[%d] \n", epfd);
	return epfd;
}

//使用epoll_wait对epfd进行监听  然后业务处理 
int vps_epoll_wait_do_cycle(int epfd, int listenfd)
{
	struct epoll_event event_wait[1024];
	int nready = 0;

	while(1) //如果多线程 这里应该设置终止标志
	{
		//int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
		nready = epoll_wait(epfd, event_wait, 1024, 1000);
		if(nready < 0)
		{
			if (errno == EINTR)// 信号被中断
    		{
	    		printf("vps epoll_wait return and errno is EINTR \n");
                continue;
    		}
            printf("vps epoll_wait error.[%s]\n", strerror(errno));
            break;
		}

		if(nready == 0)
		{
			continue;
		}

		//这里已经有相关的事件触发了 进行业务处理
		for(int i = 0; i<nready; i++)
		{
			//处理可读,区分listenfd
			if(event_wait[i].events & EPOLLIN)
			{
				if(event_wait[i].data.fd == listenfd)
				{
					//处理accept 这里应该监听可读 不监听可写
					vps_accept_exec(epfd, event_wait[i].data.fd);
				}else
				{
					//处理recv, 可能对端主动关闭,
					vps_recv_exec(epfd, event_wait[i].data.fd); 
				}
			}

			//这种情况下应该从epoll中移除,并关闭fd
    		//这里如果不是客户端发完就终止的业务,我们是不是不del,只有异常时del
    		if (event_wait[i].events & (EPOLLERR | EPOLLHUP)) //EPOLLHUP 已经读完
    		{
    			printf("epoll error [EPOLLERR | EPOLLHUP].\n");
    			epoll_ctl(epfd, EPOLL_CTL_DEL, event_wait[i].data.fd, NULL);
    			close(event_wait[i].data.fd);
    		}
		}
	} 
	return 0;
}

//一般设计是  接收完之后 删除event监听可读事件,塞入回复字符串,监听可写事件进行发送。
//要么用reactor模式处理这里的接收与发送  要么,暂时不关注对发送的监听,这里业务发送不频繁,所以接收到后直接返回必要的数据

// 事件触发  处理连接请求
int vps_accept_exec(int epfd, int listenfd)
{
	//有链接来了   需要epoll接收  epoll_ctl加入监听可读事件
	struct  sockaddr_in cliaddr;
	socklen_t clilen = sizeof(struct sockaddr_in);

	//et模式  把连接都拿出来
	int clifd = -1;
	int ret = 0;
	while(clifd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen))
	{
		//accept 正常返回非负整数  出错时返回-1 这个debug调试一下吧
		if(clifd == -1)
		{
			//资源暂时不可用  应该重试  但是不应该无限重试
			if (((errno == EAGAIN) || (errno == EWOULDBLOCK) )&& ret <3) 
			{
				ret++;
				continue;
			}
			printf(" accept error: [%s]\n", strerror(errno));
			return -1;
		}
		//对已经连接的fd进行处理  应该加入epoll
		SetNonblock(clifd);
		//加入epoll
		struct epoll_event clifd_event;
		clifd_event.data.fd = clifd;
		clifd_event.events = EPOLLIN | EPOLLET; //ET模式要循环读
		if(epoll_ctl(epfd, EPOLL_CTL_ADD, clifd, &clifd_event) == -1)
		{
			printf("vps accetp epoll ctl error . \n");
			close(clifd);
			return -1;
		}
		printf("accept success. [%d:%s:%d] connected \n",clifd, inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
	}
	return 0;
}

// 事件触发  处理可读请求 读数据 这里没监听可写,
int vps_recv_exec(int epfd, int connfd)
{
	//这里是真正的业务处理,接收数据并且主动发送一个返回数据。
	//如果有数据  进行接收  直到接收完了,关闭连接
	printf("start recv data from client [%d].",connfd);
	//这里业务场景不频繁  客户端每发送一次就终止?
	//尽量是让客户端主动断开,
	//可以自己实现一个定时器,检测主动断开处理
	char recv_data[1024] = {0};
	int datalen = -1;
	//可能有信号中断   接收长度是-1的场景
	while(1){
		//不能把 ==0加在这里  否则会在客户端断开的时候死循环
		while((datalen = read(connfd,  recv_data,  1024)) > 0 )
		{
			printf("recv from [%d] data len[%d], data[%s] \n", connfd, datalen, recv_data);
			memset(recv_data, 0, 1024);
		}

		//在客户端关闭 断开连接的时候   接收长度才为0
		printf("recv from [fd:%d] end \n", connfd);

		//给接收到的报文一个回复报文 这里可以保存一些fd和客户端的ip和port相关关系,进行回复消息构造
		const char * send_data = "hi i have recv your msg \n";
		if(strlen(send_data) ==  write(connfd, send_data, strlen(send_data)))
		{
			printf("send buff succes [len:%lu]%s", strlen(send_data), send_data);
		}

		//服务器接收空包是因为客户端关闭导致的,着了应该关闭对应的fd并从epoll中移除
		if(datalen == 0)
		{
			if(epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, 0) == -1)
			{
				printf("vps [fd:%d] close ,remove from epoll event error\n", connfd);
			}else
			{
				printf("vps [fd:%d] close ,remove from epoll event success\n", connfd);
				close(connfd);
			}
			
			break;
		}


		//等于0 可能是读到结束
		if(datalen == -1)
		{
			printf("recv end error: [%s]\n", strerror(errno));//必然触发 已经接收完了
			if (errno == EWOULDBLOCK && errno == EINTR) //不做处理
			{
				continue;
			}
			//这里要不要移除这个fd呢? 按照移除进行处理 tcp就是短连接了
			// if(epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, 0) == -1)
			// {
			// 		printf("vps client [%d] remove from epoll error\n", connectfd);
			// }else
			// {
			// 		printf("vps client [%d] remove from epoll success\n", connectfd);
			// }
			
			// close(connfd);
			break;
		}
	}
	
	return 0;
}

3:代码测试

我使用网络工具进行测试:

在这里插入图片描述

我开始试着积累一些常用代码:自己代码库中备用

我的知识储备更多来自这里,推荐你了解:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

这篇关于知识巩固源码落实之1:tcp服务端epoll实现的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!