当前位置:网站首页>socket編程之常用api介紹與socket、select、poll、epoll高並發服務器模型代碼實現

socket編程之常用api介紹與socket、select、poll、epoll高並發服務器模型代碼實現

2022-07-07 18:11:00 cheems~

前言

  本文旨在學習socket網絡編程這一塊的內容,epoll是重中之重,後續文章寫reactor模型是建立在epoll之上的。

  本專欄知識點是通過零聲教育的線上課學習,進行梳理總結寫下文章,對c/c++linux課程感興趣的讀者,可以點擊鏈接 C/C++後臺高級服務器課程介紹 詳細查看課程的服務。

socket編程

socket介紹

  傳統的進程間通信借助內核提供的IPC機制進行, 但是只能限於本機通信, 若要跨機通信, 就必須使用網絡通信( 本質上借助內核-內核提供了socket偽文件的機制實現通信----實際上是使用文件描述符), 這就需要用到內核提供給用戶的socket API函數庫。

  使用socket會建立一個socket pair,如下圖, 一個文件描述符操作兩個緩沖區。

在這裏插入圖片描述

使用socket的API函數編寫服務端和客戶端程序的步驟

在這裏插入圖片描述

預備知識

網絡字節序

  網絡字節序:大端和小端的概念

  • 大端: 低比特地址存放高比特數據, 高比特地址存放低比特數據
  • 小端: 低比特地址存放低比特數據, 高比特地址存放高比特數據

  大端和小端的使用使用場合:在網絡中經常需要考慮大端和小端的是IP和端口。網絡傳輸用的是大端,計算機用的是小端, 所以需要進行大小端的轉換

  下面4個函數就是進行大小端轉換的函數,函數名的h錶示主機host, n錶示網絡network, s錶示short, l錶示long。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

上述的幾個函數, 如果本來不需要轉換函數內部就不會做轉換。

IP地址轉換函數

  IP地址轉換函數

int inet_pton(int af, const char *src, void *dst);
  • p->錶示點分十進制的字符串形式
  • to->到
  • n->錶示network網絡

函數說明: 將字符串形式的點分十進制IP轉換為大端模式的網絡IP(整形4字節數)
參數說明:

  • af: AF_INET
  • src: 字符串形式的點分十進制的IP地址
  • dst: 存放轉換後的變量的地址
  • 例如inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);

  手工也可以計算: 如192.168.232.145, 先將4個正數分別轉換為16進制數,
   192—>0xC0   168—>0xA8    232—>0xE8    145—>0x91
  最後按照大端字節序存放: 0x91E8A8C0, 這個就是4字節的整形值。
  

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

函數說明: 網絡IP轉換為字符串形式的點分十進制的IP
參數說明:

  • af: AF_INET
  • src: 網絡的整形的IP地址
  • dst: 轉換後的IP地址,一般為字符串數組
  • size: dst的長度

返回值:

  • 成功–返回執行dst的指針
  • 失敗–返回NULL, 並設置errno

  例如: IP地址為010aa8c0, 轉換為點分十進制的格式:
  01---->1    0a---->10   a8---->168    c0---->192
  由於從網絡中的IP地址是高端模式, 所以轉換為點分十進制後應該為: 192.168.10.1

struct sockaddr

socket編程用到的重要的結構體:struct sockaddr
在這裏插入圖片描述

//struct sockaddr結構說明:
struct sockaddr {
    
     sa_family_t sa_family;
     char     sa_data[14];
}

//struct sockaddr_in結構:
struct sockaddr_in {
    
     sa_family_t    sin_family; /* address family: AF_INET */
     in_port_t      sin_port;   /* port in network byte order */
     struct in_addr sin_addr;   /* internet address */
};

/* Internet address. */
struct in_addr {
    
      uint32_t  s_addr;     /* address in network byte order */
};	 //網絡字節序IP--大端模式

通過man 7 ip可以查看相關說明

主要API函數介紹

socket

int socket(int domain, int type, int protocol);

函數描述: 創建socket

參數說明:

  • domain: 協議版本
- - AF_INET IPV4
- - AF_INET6 IPV6
- - AF_UNIX AF_LOCAL本地套接字使用
  • type:協議類型
- - SOCK_STREAM 流式, 默認使用的協議是TCP協議
- - SOCK_DGRAM  報式, 默認使用的是UDP協議
  • protocal:
- - 一般填0, 錶示使用對應類型的默認協議.
  • 返回值:
- - 成功: 返回一個大於0的文件描述符
- - 失敗: 返回-1, 並設置errno

  當調用socket函數以後, 返回一個文件描述符, 內核會提供與該文件描述符相對應的讀和寫緩沖區, 同時還有兩個隊列, 分別是請求連接隊列和已連接隊列(監聽文件描述符才有,listenFd)

在這裏插入圖片描述

bind

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函數描述: 將socket文件描述符和IP,PORT綁定

參數說明:

  • socket: 調用socket函數返回的文件描述符
  • addr: 本地服務器的IP地址和PORT,
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
//serv.sin_addr.s_addr = htonl(INADDR_ANY);
//INADDR_ANY: 錶示使用本機任意有效的可用IP
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
  • addrlen: addr變量的占用的內存大小

返回值:

  • 成功: 返回0
  • 失敗: 返回-1, 並設置errno

listen

int listen(int sockfd, int backlog);

函數描述: 將套接字由主動態變為被動態

參數說明:

  • sockfd: 調用socket函數返回的文件描述符
  • backlog: 在linux系統中,這裏代錶全連接隊列(已連接隊列)的數量。在unix系統種,這裏代錶全連接隊列(已連接隊列)+ 半連接隊列(請求連接隊列)的總數

返回值:

  • 成功: 返回0
  • 失敗: 返回-1, 並設置errno

accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);	

函數說明:獲得一個連接, 若當前沒有連接則會阻塞等待.

函數參數:

  • sockfd: 調用socket函數返回的文件描述符
  • addr: 傳出參數, 保存客戶端的地址信息
  • addrlen: 傳入傳出參數, addr變量所占內存空間大小

返回值:

  • 成功: 返回一個新的文件描述符,用於和客戶端通信
  • 失敗: 返回-1, 並設置errno值.

  accept函數是一個阻塞函數, 若沒有新的連接請求, 則一直阻塞.
  從已連接隊列中獲取一個新的連接, 並獲得一個新的文件描述符, 該文件描述符用於和客戶端通信. (內核會負責將請求隊列中的連接拿到已連接隊列中)

connect

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函數說明: 連接服務器

函數參數:

  • sockfd: 調用socket函數返回的文件描述符
  • addr: 服務端的地址信息
  • addrlen: addr變量的內存大小

返回值:

  • 成功: 返回0
  • 失敗: 返回-1, 並設置errno值

讀取和發送數據

  接下來就可以使用write和read函數進行讀寫操作了。除了使用read/write函數以外, 還可以使用recv和send函數。

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);	
//對應recv和send這兩個函數flags直接填0就可以了

  注意: 如果寫緩沖區已滿, write也會阻塞, read讀操作的時候, 若讀緩沖區沒有數據會引起阻塞。

高並發服務器模型-select

select介紹

  多路IO技術: select, 同時監聽多個文件描述符, 將監控的操作交給內核去處理

int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

數據類型fd_set::文件描述符集合——本質是比特圖

函數介紹: 委托內核監控該文件描述符對應的讀,寫或者錯誤事件的發生

參數說明:

  • nfds: 最大的文件描述符+1
  • readfds: 讀集合, 是一個傳入傳出參數
		傳入: 指的是告訴內核哪些文件描述符需要監控
		傳出: 指的是內核告訴應用程序哪些文件描述符發生了變化
  • writefds: 寫文件描述符集合(傳入傳出參數,同上)
  • execptfds: 异常文件描述符集合(傳入傳出參數,同上)
  • timeout:
		NULL--錶示永久阻塞, 直到有事件發生
		0   --錶示不阻塞, 立刻返回, 不管是否有監控的事件發生
		>0  --到指定事件或者有事件發生了就返回
  • 返回值: 成功返回發生變化的文件描述符的個數。失敗返回-1, 並設置errno值。

select-api

將fd從set集合中清除

void FD_CLR(int fd, fd_set *set);

功能描述: 判斷fd是否在集合中
返回值: 如果fd在set集合中, 返回1, 否則返回0

int FD_ISSET(int fd, fd_set *set);

將fd設置到set集合中

void FD_SET(int fd, fd_set *set);

初始化set集合

void FD_ZERO(fd_set *set);

用select函數其實就是委托內核幫我們去檢測哪些文件描述符有可讀數據,可寫,錯誤發生

int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select優缺點

select優點:

  1. select支持跨平臺

select缺點:

  1. 代碼編寫困難
  2. 會涉及到用戶區到內核區的來回拷貝
  3. 當客戶端多個連接, 但少數活躍的情况, select效率較低(例如: 作為極端的一種情况, 3-1023文件描述符全部打開, 但是只有1023有發送數據, select就顯得效率低下)
  4. 最大支持1024個客戶端連接(select最大支持1024個客戶端連接不是有文件描述符錶最多可以支持1024個文件描述符限制的, 而是由FD_SETSIZE=1024限制的)

FD_SETSIZE=1024 fd_set使用了該宏, 當然可以修改內核, 然後再重新編譯內核, 一般不建議這麼做

select代碼實現

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>

#define MAX_LEN 4096

int main(int argc, char **argv) {
    
    int listenfd, connfd, n;
    struct sockaddr_in svr_addr;
    char buff[MAX_LEN];

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    memset(&svr_addr, 0, sizeof(svr_addr));
    svr_addr.sin_family = AF_INET;
    svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    svr_addr.sin_port = htons(8081);

    if (bind(listenfd, (struct sockaddr *) &svr_addr, sizeof(svr_addr)) == -1) {
    
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    if (listen(listenfd, 10) == -1) {
    
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    //select
    fd_set rfds, rset, wfds, wset;
    FD_ZERO(&rfds);
    FD_ZERO(&wfds);
    FD_SET(listenfd, &rfds);
    int max_fd = listenfd;

    while (1) {
    
        rset = rfds;
        wset = wfds;
        int nready = select(max_fd + 1, &rset, &wset, NULL, NULL);

        if (FD_ISSET(listenfd, &rset)) {
     //
            struct sockaddr_in clt_addr;
            socklen_t len = sizeof(clt_addr);
            if ((connfd = accept(listenfd, (struct sockaddr *) &clt_addr, &len)) == -1) {
    
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }
            FD_SET(connfd, &rfds);
            if (connfd > max_fd) max_fd = connfd;
            if (--nready == 0) continue;
        }
        int i = 0;
        for (i = listenfd + 1; i <= max_fd; i++) {
    
            if (FD_ISSET(i, &rset)) {
     //
                n = recv(i, buff, MAX_LEN, 0);
                if (n > 0) {
    
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);
                    FD_SET(i, &wfds);
                }
                else if (n == 0) {
     //
                    FD_CLR(i, &rfds);
                    close(i);
                }
                if (--nready == 0) break;
            }
            else if (FD_ISSET(i, &wset)) {
    
                send(i, buff, n, 0);
                FD_SET(i, &rfds);
                FD_CLR(i, &wfds);
            }
        }
    }
    close(listenfd);
    return 0;
}

高並發服務器模型-poll

poll介紹

  poll跟select類似, 監控多路IO, 但poll不能跨平臺。其實poll就是把select三個文件描述符集合變成一個集合了。

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

參數說明:

  • fds: 傳入傳出參數, 實際上是一個結構體數組
fds.fd: 要監控的文件描述符
fds.events: 
	POLLIN---->讀事件
	POLLOUT---->寫事件
fds.revents: 返回的事件
  • nfds: 數組實際有效內容的個數
  • timeout: 超時時間, 單比特是毫秒.
-1:永久阻塞, 直到監控的事件發生
0: 不管是否有事件發生, 立刻返回
>0: 直到監控的事件發生或者超時

返回值:

  • 成功:返回就緒事件的個數
  • 失敗: 返回-1。若timeout=0, poll函數不阻塞,且沒有事件發生, 此時返回-1, 並且errno=EAGAIN, 這種情况不應視為錯誤。
struct pollfd {
    
   int   fd;        /* file descriptor */   監控的文件描述符
   short events;     /* requested events */  要監控的事件---不會被修改
   short revents;    /* returned events */   返回發生變化的事件 ---由內核返回
};

說明:

  1. 當poll函數返回的時候, 結構體當中的fd和events沒有發生變化, 究竟有沒有事件發生由revents來判斷, 所以poll是請求和返回分離
  2. struct pollfd結構體中的fd成員若賦值為-1, 則poll不會監控
  3. 相對於select, poll沒有本質上的改變; 但是poll可以突破1024的限制.在/proc/sys/fs/file-max查看一個進程可以打開的socket描述符上限,如果需要可以修改配置文件: /etc/security/limits.conf,加入如下配置信息, 然後重啟終端即可生效
* soft nofile 1024
* hard nofile 100000

soft和hard分別錶示ulimit命令可以修改的最小限制和最大限制

poll代碼實現

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_LEN 4096
#define POLL_SIZE 1024

int main(int argc, char **argv) {
    
    int listenfd, connfd, n;
    struct sockaddr_in svr_addr;
    char buff[MAX_LEN];

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    memset(&svr_addr, 0, sizeof(svr_addr));
    svr_addr.sin_family = AF_INET;
    svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    svr_addr.sin_port = htons(8081);

    if (bind(listenfd, (struct sockaddr *) &svr_addr, sizeof(svr_addr)) == -1) {
    
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    if (listen(listenfd, 10) == -1) {
    
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    //poll
    struct pollfd fds[POLL_SIZE] = {
    0};
    fds[0].fd = listenfd;
    fds[0].events = POLLIN;

    int max_fd = listenfd;
    int i = 0;
    for (i = 1; i < POLL_SIZE; i++) {
    
        fds[i].fd = -1;
    }
    while (1) {
    
        int nready = poll(fds, max_fd + 1, -1);

        if (fds[0].revents & POLLIN) {
    
            struct sockaddr_in client = {
    };
            socklen_t len = sizeof(client);
            if ((connfd = accept(listenfd, (struct sockaddr *) &client, &len)) == -1) {
    
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }
            printf("accept \n");
            fds[connfd].fd = connfd;
            fds[connfd].events = POLLIN;
            if (connfd > max_fd) max_fd = connfd;
            if (--nready == 0) continue;
        }
        //int i = 0;
        for (i = listenfd + 1; i <= max_fd; i++) {
    
            if (fds[i].revents & POLLIN) {
    
                n = recv(i, buff, MAX_LEN, 0);
                if (n > 0) {
    
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);
                    send(i, buff, n, 0);
                }
                else if (n == 0) {
     //
                    fds[i].fd = -1;
                    close(i);
                }
                if (--nready == 0) break;
            }
        }
    }
}

高並發服務器模型-epoll (重點)

epoll介紹

  將檢測文件描述符的變化委托給內核去處理, 然後內核將發生變化的文件描述符對應的事件返回給應用程序。

  記住,epoll是事件驅動的,其底層數據結構是紅黑樹,紅黑樹的key是fd,val是事件,返回的是事件。

epoll有兩種工作模式,ET和LT模式。

水平觸發LT:

  • 高電平代錶1
  • 只要緩沖區中有數據, 就一直通知

邊緣觸發ET:

  • 電平有變化就代錶1
  • 緩沖區中有數據只會通知一次, 之後再有新的數據到來才會通知(若是讀數據的時候沒有讀完, 則剩餘的數據不會再通知, 直到有新的數據到來)

  epoll默認是水平觸發LT,在需要高性能的場景下,可以改成邊緣ET非阻塞方式來提高效率。

  一般使用LT是一次性讀數據讀不完,數據較多的情况。而一次性能够讀完,小數據量則用邊緣ET。

  ET模式由於只通知一次, 所以在讀的時候要循環讀, 直到讀完, 但是當讀完之後read就會阻塞, 所以應該將該文件描述符設置為非阻塞模式(fcntl函數)

  read函數在非阻塞模式下讀的時候, 若返回-1, 且errno為EAGAIN, 則錶示當前資源不可用, 也就是說緩沖區無數據(緩沖區的數據已經讀完了); 或者當read返回的讀到的數據長度小於請求的數據長度時,就可以確定此時緩沖區中已沒有數據可讀了,也就可以認為此時讀事件已處理完成。

epoll反應堆

  反應堆: 一個小事件觸發一系列反應

  epoll反應堆的思想: c++的封裝思想(把數據和操作封裝到一起)

  • 將描述符,事件,對應的處理方法封裝在一起
  • 當描述符對應的事件發生了, 自動調用處理方法(其實原理就是回調函數)

  epoll反應堆的核心思想是: 在調用epoll_ctl函數的時候, 將events上樹的時候,利用epoll_data_t的ptr成員, 將一個文件描述符,事件和回調函數封裝成一個結構體, 然後讓ptr指向這個結構體。然後調用epoll_wait函數返回的時候, 可以得到具體的events, 然後獲得events結構體中的events.data.ptr指針, ptr指針指向的結構體中有回調函數, 最終可以調用這個回調函數。

struct epoll_event {
    
	uint32_t     events;      /* Epoll events */
	epoll_data_t data;        /* User data variable */
};
typedef union epoll_data {
    
	void        *ptr;
	int          fd;
	uint32_t     u32;
	uint64_t     u64;
} epoll_data_t;

epoll-api

int epoll_create(int size);

函數說明: 創建一個樹根

參數說明:

  • size: 最大節點數, 此參數在linux 2.6.8已被忽略, 但必須傳遞一個大於0的數,曆史意義,用epoll_create1也行。
  • 返回值:
成功: 返回一個大於0的文件描述符, 代錶整個樹的樹根.
失敗: 返回-1, 並設置errno值.

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函數說明: 將要監聽的節點在epoll樹上添加, 删除和修改

參數說明:

  • epfd: epoll樹根

  • op:

EPOLL_CTL_ADD: 添加事件節點到樹上
EPOLL_CTL_DEL: 從樹上删除事件節點
EPOLL_CTL_MOD: 修改樹上對應的事件節點
  • fd: 事件節點對應的文件描述符
  • event: 要操作的事件節點
struct epoll_event {
    
	uint32_t     events;      /* Epoll events */
	epoll_data_t data;        /* User data variable */
};
typedef union epoll_data {
    
	void        *ptr;
	int          fd;
	uint32_t     u32;
	uint64_t     u64;
} epoll_data_t;
  • event.events常用的有:
EPOLLIN: 讀事件
EPOLLOUT: 寫事件
EPOLLERR: 錯誤事件
EPOLLET: 邊緣觸發模式
  • event.fd: 要監控的事件對應的文件描述符

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

函數說明:等待內核返回事件發生

參數說明:

  • epfd: epoll樹根
  • events: 傳出參數, 其實是一個事件結構體數組
  • maxevents: 數組大小
  • timeout:
	-1: 錶示永久阻塞
	0: 立即返回
	>0: 錶示超時等待事件

返回值:

  • 成功: 返回發生事件的個數
  • 失敗: 若timeout=0, 沒有事件發生則返回; 返回-1, 設置errno值

  epoll_wait的events是一個傳出參數, 調用epoll_ctl傳遞給內核什麼值, 當epoll_wait返回的時候, 內核就傳回什麼值,不會對struct event的結構體變量的值做任何修改。

epoll優缺點

epoll優點:

  1. 性能高,百萬並發不在話下,而select就不行

epoll缺點:

  1. 不能跨平臺,linux下的

epoll代碼實現

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>

#define POLL_SIZE 1024
#define MAX_LEN 4096

int main(int argc, char **argv) {
    
    int listenfd, connfd, n;
    char buff[MAX_LEN];
    struct sockaddr_in svr_addr;
    memset(&svr_addr, 0, sizeof(svr_addr));
    svr_addr.sin_family = AF_INET;
    svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    svr_addr.sin_port = htons(8081);

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    if (bind(listenfd, (struct sockaddr *) &svr_addr, sizeof(svr_addr)) == -1) {
    
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    if (listen(listenfd, 10) == -1) {
    
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    int epfd = epoll_create(1); //int size

    struct epoll_event events[POLL_SIZE] = {
    0};
    struct epoll_event ev;

    ev.events = EPOLLIN;
    ev.data.fd = listenfd;

    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

    while (1) {
    
        int nready = epoll_wait(epfd, events, POLL_SIZE, 5);
        if (nready == -1) {
    
            continue;
        }
        int i = 0;
        for (i = 0; i < nready; i++) {
    
            int actFd = events[i].data.fd;
            if (actFd == listenfd) {
    
                struct sockaddr_in cli_addr;
                socklen_t len = sizeof(cli_addr);
                if ((connfd = accept(listenfd, (struct sockaddr *) &cli_addr, &len)) == -1) {
    
                    printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                    return 0;
                }
                printf("accept\n");
                ev.events = EPOLLIN;
                ev.data.fd = connfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
            }
            else if (events[i].events & EPOLLIN) {
    
                n = recv(actFd, buff, MAX_LEN, 0);
                if (n > 0) {
    
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);
                    send(actFd, buff, n, 0);
                }
                else if (n == 0) {
     //
                    epoll_ctl(epfd, EPOLL_CTL_DEL, actFd, NULL);
                    close(actFd);
                }
            }
        }
    }
    return 0;
}
原网站

版权声明
本文为[cheems~]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/188/202207071603369667.html