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
相关推荐
WZF-Sang1 小时前
Linux权限理解【Shell的理解】【linux权限的概念、管理、切换】【粘滞位理解】
linux·运维·服务器·开发语言·学习
小橞1 小时前
Linux搭建简易路由转发
linux·运维·服务器
嵌入式DZC2 小时前
搭建内网文件服务器(FTP),以及实现内网Gitee
运维·服务器
robot_大菜鸟2 小时前
linux-L7-linux 查看json文件
linux·运维
Flying_Fish_roe2 小时前
linux-网络管理-网络抓包
linux·网络·php
小技与小术4 小时前
lvs-nat模式实验详解
linux·运维·服务器·网络·lvs
FreeBuf_4 小时前
SSHamble:一款针对SSH技术安全的研究与分析工具
运维·安全·ssh
繁依Fanyi4 小时前
828华为云征文|华为Flexus云服务器搭建OnlyOffice私有化在线办公套件
服务器·开发语言·前端·python·算法·华为·华为云
喵不拉几4 小时前
WebSocket 协议
网络·websocket·网络协议
张太行_4 小时前
Linux 调用write()函数后,内核一般多久将数据写入磁盘
linux·运维