知识巩固源码落实之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等技术内容,立即学习

目录
相关文章
|
6天前
|
网络协议 开发者
TCP连接的四次挥手过程及其必要性
在网络通信中,TCP(传输控制协议)以其可靠性和有序性著称。TCP连接的建立和终止都需要特定的握手过程。本文将详细描述TCP连接的四次挥手(四次挥手)过程,并探讨为什么需要四次挥手来终止一个TCP连接。
21 8
|
6天前
|
网络协议
深入解析:TCP四次挥手断开连接的全过程及必要性
在网络通信中,TCP(传输控制协议)以其可靠性和顺序保证而闻名。然而,TCP连接的建立和终止同样重要,它们确保了网络资源的有效管理和数据传输的完整性。本文将详细描述TCP连接的四次挥手过程,并探讨为何需要四次挥手来正确终止一个TCP连接。
22 2
|
3月前
|
监控 网络协议 UED
TCP协议中的两种保活机制详述
TCP的保活机制通过保活探针和用户配置的保活时间两种方式,为网络通讯提供了重要的保障。它帮助识别并处理那些因为网络不稳定或对端突然下线而变得无响应的连接,对于确保长时间运行的网络应用的稳定性和可靠性非常关键。合理配置和使用TCP保活机制,可以显著提升网络应用的鲁棒性和用户体验。
97 1
|
6月前
|
网络协议 应用服务中间件 网络性能优化
解析TCP /UDP协议的 socket 调用的过程
【6月更文挑战第2天】该文介绍了传输层的两种主要协议TCP和UDP的区别。TCP是面向连接、可靠的,提供顺序无错的数据传输,而UDP则是无连接、不可靠的,不保证数据顺序或不丢失。
|
6月前
|
网络协议
TCP四次挥手全过程详解
TCP四次挥手全过程详解
75 0
|
7月前
|
网络协议 Linux 存储
深入理解Linux网络——TCP连接建立过程(三次握手源码详解)
一、相关实际问题 1. 为什么服务端程序都需要先listen一下 2. 半连接队列和全连接队列长度如何确定 3. “Cannot assign requested address”这个报错是怎么回事 4. 一个客户端端口可以同时用在两条连接上吗 5. 服务端半/全连接队列满了会怎么样 6. 新连接的soket内核对象是什么时候建立的 7. 建立一条TCP连接需要消耗多长时间 8. 服务器负载很正常,但是CPU被打到底了时怎么回事
|
网络协议
TCP编程中的客户端与服务端的通信实现
TCP编程中的客户端与服务端的通信实现
134 0
|
网络协议
TCP协议三次握手的执行流程,tcp的交互模式
TCP协议三次握手的执行流程,tcp的交互模式
166 0
|
网络协议 Java API
java网络编程(2)socket通信案例(TCP和UDP)
java生下来一开始就是为了计算机之间的通信,因此这篇文章也将开始介绍一下java使用socket进行计算机之间的通信,在上一篇文章中已经对网络通信方面的基础知识进行了总结,这篇文章将通过代码案例来解释说明。
264 0
java网络编程(2)socket通信案例(TCP和UDP)
|
网络协议
TCP通信简单梳理
一、什么是TCP协议 TCP协议是一种面向连接的可靠的通信协议,最重要的两个特点:连接、可靠。 二、TCP是如何进行通信的 TCP通过三次握手建立连接后客户端服务端的内核都分别开辟资源,这时候开始进行数据传输,最终通过四次挥手断开连接,释放资源,整体流程如下: 1、三次握手 三次握手流程如下: 1)
248 0