tcp网络通信是日常业务常常会重复实现的业务功能
===》相关的socket接口:socket,bind,listen,accept,send,recv都是我们很熟悉的
===》相关的io多路处理方案:select,poll,epoll可以根据业务场景自己抉择使用
===》但其实,简单tcp服务器实现过程中,总有一些细节需要关注,
===》以及考虑到每次重新实现,多次重写,开始思考备份一些代码。。。。
作为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; }
我使用网络工具进行测试:
我开始试着积累一些常用代码:自己代码库中备用
我的知识储备更多来自这里,推荐你了解:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习