epoll机制处理TCP多客户端连接

服务端

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
 #include <sys/epoll.h>
typedef struct sockaddr* (SA);
int add_fd(int epfd,int fd)
{
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev);
    if(-1 == ret)
    {
        perror("add fd");
    }
    return ret;
}
int del_fd(int epfd,int fd)
{
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    int ret = epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&ev);
    if(-1 == ret)
    {
        perror("add fd");
    }
    return ret;
}

int main(int argc, char *argv[])
{
    //监听套接字
    int listfd = socket(AF_INET,SOCK_STREAM,0 );
    if(-1 ==listfd)
    {
        perror("socket");
        exit(1);
    }
    struct sockaddr_in ser,cli;
    bzero(&ser,sizeof(ser));
    bzero(&cli,sizeof(cli));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr =inet_addr("127.0.0.1");

    //man 7 socket 
    int on = 1;
    setsockopt(listfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
    setsockopt(listfd,SOL_SOCKET,SO_REUSEPORT,&on,sizeof(on));
    int ret = bind(listfd,(SA)&ser,sizeof(ser));
    if(-1 ==ret)
    {
        perror("bind");
        exit(1);
    }
    //建立连接的排队数
    listen(listfd,3);
    socklen_t len = sizeof(cli);

    struct epoll_event rev[10]={0};
    //1 create set 
    int epfd = epoll_create(10);
    if(-1 == epfd)
    {
        perror("epoll_create");
        return 1;
    }
    // 2 .add fd  
    
    add_fd(epfd,listfd);
	int num = 0;
	while(1)
    {    
        //3 wait event 
        int ep_ret = epoll_wait(epfd,rev,10,-1);
        int i = 0 ;
        //4 find fd handle
        for(i = 0 ;i<ep_ret;i++)
        {

            if(rev[i].data.fd == listfd)
            {    //通讯套接字
                int conn = accept(listfd,(SA)&cli,&len);
                if(-1 == conn)
                {
                    perror("accept");
                    continue;
                }
                add_fd(epfd,conn);
				num++;
				char clientIP[INET_ADDRSTRLEN];
				inet_ntop(AF_INET, &(cli.sin_addr), clientIP, INET_ADDRSTRLEN);
				int clientPort = ntohs(cli.sin_port);
				printf("cli connected. ip: %s, port: %d. total clis: %d\n", clientIP, clientPort, num);
            }
            else 
            {
                int conn = rev[i].data.fd;
                char buf[512]={0};
                int rd_ret = recv(conn,buf,sizeof(buf),0);
                if(rd_ret<=0)
                {
                    del_fd(epfd,conn);
                    close(conn);
					num--;
					printf("cli off line! total clis:%d\n",num);
                    break;
                }
                time_t tm;
                time(&tm);
                sprintf(buf,"%s %s",buf,ctime(&tm));
                send(conn,buf,strlen(buf),0);
            }
        }
    }
    close(listfd);
    return 0;
}

初始化与套接字创建

listfd = socket(AF_INET,SOCK_STREAM,0);

创建一个TCP套接字
bind(listfd,(SA)&ser,sizeof(ser));

将服务器的IP地址和端口绑定到创建的套接字上
listen(listfd,3);

将套接字设置为监听模式,最多可处理3个连接排队

setsockopt

是一个用于设置套接字选项的系统调用函数,配置套接字的各种行为和特性

复制代码
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  • sockfd:套接字文件描述符,即你要配置的套接字。

  • level:指定要设置的选项所在的协议层。常见的值有:

    • SOL_SOCKET:用于设置套接字层的选项。
    • IPPROTO_TCP:用于设置TCP协议层的选项(如TCP_NODELAY)。
    • IPPROTO_IP:用于设置IP协议层的选项(如IP_TTL)。
  • optname :要设置的选项名,取决于level。例如:

    • 对于SOL_SOCKET层,常见的选项包括:
      • SO_REUSEADDR:允许重用本地地址。
      • SO_KEEPALIVE:启用TCP连接的保活机制。
    • 对于IPPROTO_TCP层,常见的选项包括:
      • TCP_NODELAY:禁用Nagle算法,提高数据传输的实时性。
  • optval :指向要设置的选项值的指针。它的类型和长度取决于optname

  • optlenoptval指向的数据长度。

int on = 1;

setsockopt(listfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

setsockopt(listfd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));
int on = 1; 这一行代码的作用是设置一个整型变量 on 的值为 1,通常用于启用某些套接字选项。初始化一个整型变量 on,并将其值设置为 1。这个值通常用作标志,表示在调用 setsockopt 函数时,某些套接字选项应该被启用或激活。

这里,on 的值 1 被用来启用 SO_REUSEADDRSO_REUSEPORT 这两个选项。具体说明如下:

  1. SO_REUSEADDR :允许重用本地地址。在绑定地址和端口时,如果这个地址和端口处于 TIME_WAIT 状态(例如,前一个连接已经关闭),仍然可以重新绑定。

  2. SO_REUSEPORT:允许多个套接字绑定到相同的端口。这对于提高多进程或多线程服务器的性能很有用,因为它允许多个进程共享同一个端口。

epoll的创建与配置

epoll_create(10);

创建一个epoll实例,用于管理多个文件描述符。
add_fd(epfd,listfd);

将监听套接字添加到epoll实例中,以便在有连接到来时可以被检测到。

事件循环

epoll_wait(epfd,rev,10,-1);

等待有事件发生,rev数组保存触发的事件。

处理连接事件

  • accept(listfd,(SA)&cli,&len);:接受新的客户端连接,并将其套接字添加到epoll实例中。
  • 记录并显示新连接的客户端IP和端口信息。

处理数据事件

  • recv(conn,buf,sizeof(buf),0);:从客户端接收数据。
  • 如果接收到的数据为空或出错,表示客户端断开连接,从epoll实例中移除并关闭套接字。
  • 如果接收到数据,服务器会在数据后附加当前时间并发送回客户端。

客户端

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
#include <sys/time.h>
typedef struct sockaddr* (SA);


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

    int conn= socket(AF_INET,SOCK_STREAM,0);
    if(-1 == conn)
    {
        perror("socket");
        exit(1);
    }

    struct sockaddr_in ser;
    bzero(&ser,sizeof(ser));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr =inet_addr("127.0.0.1");

    int ret = connect(conn,(SA)&ser,sizeof(ser));
    if(-1 == ret)
    {
        perror("connect");
        exit(1);
    }
    int i =5;
    struct timeval tv;
    tv.tv_sec  = 3;
    tv.tv_usec = 0 ;
    setsockopt(conn,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)); //recv == -1 timeout 
    while(1)
    {
        char buf[512]="hello,this tcp test";
        send(conn,buf,strlen(buf),0);
        bzero(buf,sizeof(buf));
        int ret = recv(conn,buf,sizeof(buf),0);
        if(ret==0)
        {
            printf("ser close\n");
            break;
        }
        if(ret<=0)
        {
            printf("time out,contineu\n");
        }
        printf("ser:%s\n",buf);
        sleep(1);
    }
    close(conn);
    return 0;
}

初始化与套接字创建

conn= socket(AF_INET,SOCK_STREAM,0);

创建一个TCP套接字。
connect(conn,(SA)&ser,sizeof(ser));

连接到服务器指定的IP地址和端口。

超时设置

setsockopt(conn,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv));

设置套接字的接收超时时间为3秒。如果在3秒内没有接收到数据,recv将返回-1,并超时。

通信循环

  • 客户端通过send函数向服务器发送字符串"hello,this tcp test"。
  • recv(conn,buf,sizeof(buf),0);:等待服务器的响应。
    • 如果超时,则输出"time out, continue"。
    • 如果接收到数据,则输出服务器的响应内容。

函数

send

send 函数用于在网络编程中通过套接字发送数据。它常用于TCP/IP套接字通信中,将数据从客户端发送到服务器或从服务器发送到客户端。

复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd

    • 要发送数据的套接字文件描述符。这个描述符应是一个有效且已连接的套接字。
  • buf

    • 指向包含要发送数据的缓冲区的指针。数据应存储在内存中,类型为 const void *
  • len

    • 要从缓冲区中发送的字节数。表示需要传输的数据长度。
  • flags

    • 用于修改 send 函数行为的标志。常见的标志包括:
      • 0:默认行为。
      • MSG_OOB:发送带外数据(通常不用于常规TCP数据)。
      • MSG_DONTWAIT:非阻塞模式发送数据,如果发送操作会阻塞,则立即返回。

recv

recv 函数用于从套接字接收数据,是网络编程中一个重要的系统调用。它通常用于接收客户端或服务器发送的数据。

复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd

    • 套接字文件描述符,必须是一个有效的已连接套接字。
  • buf

    • 指向存储接收到数据的缓冲区的指针。接收到的数据会被放入这个缓冲区中。
  • len

    • 缓冲区 buf 的大小(以字节为单位)。这是 recv 函数可以读取的最大字节数。
  • flags

    • 用于修改 recv 函数的行为的标志。常见的标志包括:
      • 0:默认行为。
      • MSG_OOB:接收带外数据(通常不用于常规TCP数据)。
      • MSG_PEEK:查看数据但不移除数据。可以用于检查数据而不从队列中移除它。

epoll_ctl

epoll_ctl 函数是 Linux 下用于管理 epoll 实例的系统调用。它允许在 epoll 实例中添加、修改或删除事件。epoll 是一种高效的 I/O 事件通知机制,适用于处理大量并发连接的网络应用。

复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd

    • epoll 实例的文件描述符。它是通过调用 epoll_createepoll_create1 创建的。
  • op

    • 操作类型,指定要对 epfd 执行的操作。这可以是以下值之一:
      • EPOLL_CTL_ADD:将一个文件描述符添加到 epoll 实例中。
      • EPOLL_CTL_MOD:修改已经添加到 epoll 实例中的文件描述符的事件。
      • EPOLL_CTL_DEL:从 epoll 实例中删除一个文件描述符。
  • fd

    • 要管理的文件描述符。它是需要添加、修改或删除的文件描述符(如套接字描述符)。
  • event

    • 指向 epoll_event 结构体的指针,该结构体描述了感兴趣的事件。

epoll_event 结构体

复制代码
struct epoll_event {
    uint32_t events;  // 事件标志
    epoll_data_t data;  // 用户数据
};
  • events
    • 指定感兴趣的事件,如 EPOLLIN(可读事件)、EPOLLOUT(可写事件)、EPOLLERR(错误事件)等。
  • data
    • 用户定义的数据,可以用于存储与文件描述符相关的附加信息。通常为 epoll_data_t 类型。

epoll_wait

epoll_wait 函数用于等待 epoll 实例中监控的文件描述符发生事件。这是 epoll 机制的核心部分,它会阻塞程序直到一个或多个事件发生,或者直到超时。

复制代码
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epfd

    • epoll 实例的文件描述符,通常通过 epoll_createepoll_create1 创建得到。
  • events

    • 指向 epoll_event 结构体数组的指针,用于存储发生的事件信息。epoll_wait 将填充这个数组以通知哪些文件描述符上有事件发生。
  • maxevents

    • events 数组的大小,表示 epoll_wait 可以返回的最大事件数。即 events 数组能够存储的最大事件数量。
  • timeout

    • 等待事件的超时时间,单位为毫秒。可以是以下值之一:
      • -1:无限期等待,直到有事件发生。
      • 0 :立即返回,不阻塞。如果没有事件立即发生,则返回 0
      • 大于 0 的值 :等待指定的毫秒数。如果在指定时间内有事件发生,则返回;否则返回 0
相关推荐
寻星探路5 小时前
【深度长文】万字攻克网络原理:从 HTTP 报文解构到 HTTPS 终极加密逻辑
java·开发语言·网络·python·http·ai·https
七夜zippoe8 小时前
CANN Runtime任务描述序列化与持久化源码深度解码
大数据·运维·服务器·cann
盟接之桥8 小时前
盟接之桥说制造:引流品 × 利润品,全球电商平台高效产品组合策略(供讨论)
大数据·linux·服务器·网络·人工智能·制造
会员源码网9 小时前
理财源码开发:单语言深耕还是多语言融合?看完这篇不踩坑
网络·个人开发
米羊1219 小时前
已有安全措施确认(上)
大数据·网络
Fcy6489 小时前
Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)
linux·运维·服务器·进程
袁袁袁袁满9 小时前
Linux怎么查看最新下载的文件
linux·运维·服务器
代码游侠10 小时前
学习笔记——设备树基础
linux·运维·开发语言·单片机·算法
主机哥哥10 小时前
阿里云OpenClaw部署全攻略,五种方案助你快速部署!
服务器·阿里云·负载均衡
Harvey90310 小时前
通过 Helm 部署 Nginx 应用的完整标准化步骤
linux·运维·nginx·k8s