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等技术内容,立即学习