C++后台开发之网络编程-3.1网络io与select、poll、epoll

0、运行环境

# linux
# gcc

1、socket简介

Socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求。

服务端步骤:

• socket:创建服务器socket实例
• bind:绑定ip地址和端⼝
• listen:开始监听
• accept:接收客户端请求
• read:读取客户端传来的数据
• write:给客户端传数据
• close:关闭socket,结束通信

客户端步骤:

• socket:创建客户端socket实例
• connect:连接服务器
• read:读取客户端传来的数据
• write:给客户端传数据
• close:关闭socket,结束通信

2、简易的socket通信程序

该程序只能实现多个个客户端连接,但是只能发送一条消息。后面版本逐渐解决问题

仅列出服务端程序,客户端用网络调试助手进行模拟调试。

#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>
 
#define MAXLNE  4096

int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
 
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
 
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -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;
    }
    

    printf("========waiting for client's request========\n");
    while (1) {

        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;
        }

        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }

    return 0;
}

3、多线程socket通信程序

上面的程序每个客户端只能发送一个消息是因为,accpet函数阻塞了。为了解决该问题考虑为每个客户端分配一个线程,来处理每个客户端的请求。

即将下列过程放入线程中完成:

		n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }

改写如下:

pthread_t threadid;
pthread_create(&threadid, NULL, client_routine, &connfd);

并定义客户端的线程函数:

void* client_routine(void* arg){

    int connfd = *(int *)arg;  
    char buff[MAXLNE];

    while(1){
        int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

            send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
            break;
        }
    }

}

该部分的所有代码如下:(编译时加上参数-lpthread)

例如文件为socket.c,则用gcc socket.c -o socket -lpthread编译

#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 <pthread.h>
 
#define MAXLNE  4096

void* client_routine(void* arg){

    int connfd = *(int *)arg;  
    char buff[MAXLNE];

    while(1){
        int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

            send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
            break;
        }
    }

}


int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
 
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
 
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -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;
    }
    printf("========waiting for client's request-muti-clients========\n");
    while (1) {

        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;
        }

        pthread_t threadid;
        pthread_create(&threadid, NULL, client_routine, &connfd);
  
        //close(connfd);
    }

    return 0;
}

缺点:上述采取的一客户端一线程的作法,内存消耗大,不适合大量客户端连接的情况,很难突破C10K的并发量

3、基于select、poll、epoll的socket通信程序

相对于上面的做法,这三种方法是将所有要处理的套接字放在一个类似于容器(不同方法不同)的地方,然后不断的判断这里面的套接字的状态,如果这里面的套接字有读或写的请求,则会依次拿出来处理。关于select、poll、epoll的相关api以及完整的所有代码如下。

select
/*
// sizeof(fd_set) = 128bytes = 1024bit
// 也就是说最多可以检测1024个文件描述符
// fd_set是传入传出参数(指针)
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
	- nfds      : 委托内核检测的最大文件描述符+1
    - readfds   : 要检测的读文件描述符集合(就是委托内核检测读缓冲区中是否有数据进来)
    - writefds  : 要检测的写文件描述符集合(委托内核检测写缓冲区是否能写,没满就可以写)
    - exceptfds : 没啥用
    - timeout   : 这个数据类型我们之前已经接触过了(一个结构体,包含s和ms两部分),用于设置超时时间
    - 返回值     : -1表示失败, >0(n), 检测的集合中有n个描述符发生了变化
    
        
        
// 下面这些函数用于对fd_set进行各种操作
        
// 将参数fd指定的文件描述符置0
void FD_CLR(int fd, fd_set* set);
// 判断fd对应的文件描述符标志位是0还是1,返回对应的0和1
void FD_ISSET(int fd, fd_set* set);
// 将参数fd指定的文件描述符置1
void FD_SET(int fd, fd_set* set);
// 全部初始化为0
void FD_ZERO(fd_set* set);
*/
poll
/*
struct pollfd {
   	int 	fd;			// 委托内核检测的文件描述符
    short   events;		// 委托内核检测文件描述符的什么事件,   POLLIN 数据(包括普通数据和优先数据)可读,POLLRDNORM	普通数据可读
    short 	revents     // 文件描述符实际发生的事件 
}

int poll(struct pollfd* fds, nfds_t nfds, int timeout)
     - fds     : 需要检测的文件描述符集合
     - nfds    : 这是第一个参数数组中最后一个有效元素的下标+1
     - timeout : 阻塞时长,0表示不阻塞,-1表示阻塞,当检测到需要检测的文件描述符发生变化时,接触阻塞,>0(n)阻塞时长n,单位为 
     返回值:
       -1表示失败
       >0(n)表示成功,表示检测到了n个文件描述符发生了变化
*/
epoll
/**
epoll_event的结构
    typedef union epoll_data {  
        void *ptr;  
        int fd;  
        uint32_t u32;  
        uint64_t u64;  
    } epoll_data_t;
    
    struct epoll_event {  
        uint32_t events;      // Epoll events  
        epoll_data_t data;      // User data variable  
    };

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    epoll_ctl参数:
        epfd:指定的epoll模型。
        op:表示具体的动作,用三个宏来表示。
            EPOLL_CTL_ADD:注册新的文件描述符到指定的epoll模型中(往红黑树中新增节点)。
            EPOLL_CTL_MOD:修改已经注册的文件描述符的监听事件(修改红黑树中特定节点的数据)。
            EPOLL_CTL_DEL:从epoll模型中删除指定的文件描述符(删除红黑树中的节点)。
        fd:需要监视的文件描述符。
        event:需要监视该文件描述符上的哪些事件。
    返回值:
        函数调用成功返回0,调用失败返回-1,同时错误码会被设置。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    epoll_wait参数:
        epfd:指定的epoll模型。
        events:内核会将已经就绪的事件拷贝到events数组当中(events不能是空指针,内核只负责将就绪事件拷贝到该数组中,不会帮我们在用户态中分配内存)。
        maxevents:events数组中的元素个数,该值不能大于创建epoll模型时传入的size值。
        timeout:表示epoll_wait函数的超时时间,单位是毫秒(ms)。
    返回值:
        如果函数调用成功,则返回有事件就绪的文件描述符个数。
        如果timeout时间耗尽,则返回0。
        如果函数调用失败,则返回-1,同时错误码会被设置。
*/

#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 <pthread.h>
#include <sys/poll.h>
#include <sys/epoll.h>

 
#define MAXLNE  4096
#define POLL_SIZE 1024


void* client_routine(void* arg){

    int connfd = *(int *)arg;  
    char buff[MAXLNE];

    while(1){
        int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

            send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
            break;
        }
    }

}


int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
 
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
 
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -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;
    }
    

#if 0 // 只能一个客户端连接,并发送接收数据
    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("========waiting for client's request========\n");
    while (1) {

        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }

    return 0;
}

#elif 0 // 能够解决多个客户端连接,但是每个客户端只能发送一条数据,因为accept阻塞了
    printf("========waiting for client's request========\n");
    while (1) {

        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;
        }

        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }

    return 0;
}

#elif 0 // 创建线程,每个线程单独处理每个客户端的请求
/*
优点:逻辑简单
缺点:不适合大量客户端,很难突破一个数值 C10K
*/
    printf("========waiting for client's request-muti-clients========\n");
    while (1) {

        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;
        }

        pthread_t threadid;
        pthread_create(&threadid, NULL, client_routine, &connfd);
  
        //close(connfd);
    }

    return 0;
}
#elif 0 // select
/*
1、1个select,可以管理1024fd,多做几个select,能够突破C10K
2、无法突破C1000K
*/
/*每次调用select,都需要fd_set从用户态拷贝到内核态,然后再从内核态拷贝到用户态而且还需要遍历fd_set而且只支持1024个文件描述符*/

    fd_set rdfs, rset; // 一个bit位数组,只置0-1。用来存放描述符的集合,如果有该描述符,则对应位置1。

    FD_ZERO(&rdfs);
    FD_SET(listenfd, &rdfs); // 将下标为listenfd的地方置1,代表将listenfd放入读描述符集

    int  max_fd = listenfd; // 为了配合select中第一个参数,最大的文件描述符 +1,代表之后select检查的时候只会检查到这。

    while (1){
        rset = rdfs; 

        // select调用返回时,除了那些已经就绪的描述符外,select将清除readfds、writefds和exceptfds中的所有没有就绪的描述符。
        // 下述代码的rset集合中的未就绪的描述符就会被清除,也就是置0
        // 返回值是就绪的描述符个数
        int nready = select(max_fd + 1, &rset, NULL, NULL, NULL);

        if(FD_ISSET(listenfd, &rset)){ // 判断监听的描述符是否在,即是否有新的客户端进行,有的话则接收并分配套接字
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // accept分配fd是从小到大的分配,所有listenfd比后面进来客户端的值都小
            if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }

            FD_SET(connfd, &rdfs); // 将新进来的客户端描述符放入集合

            if(connfd > max_fd) max_fd = connfd;

            if(nready-- == 0) continue; // 处理监听的描述符就绪外,其他的描述符都没有就绪
        }

        // 对就绪的客户端进行操作
        for(int i = listenfd + 1; i <= max_fd; i++){
            if(FD_ISSET(i, &rset)){ 

                n = recv(i, buff, MAXLNE, 0);
                if (n > 0) {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);

                    send(i, buff, n, 0);
                } else if (n == 0) {
                    // 客户端关闭连接,应该从集合中清除出去
                    FD_CLR(i, &rdfs);
                    close(i);
                }
                if(nready-- == 0) break; // 没有就绪的客户端了
            }
        }
    }

    return 0;
}

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

    int max_fd = listenfd;

    while(1){
        int nready = poll(fds, max_fd + 1, -1);

        if(fds[0].revents & POLLIN){ // 同样先判断监听是否就绪
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // accept分配fd是从小到大的分配,所有listenfd比后面进来客户端的值都小
            if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }

            fds[connfd].fd = connfd; // 将新进来的客户端描述符放入fds
            fds[connfd].events =  POLLIN;

            if(connfd > max_fd) max_fd = connfd;
            if(nready-- == 0) continue; // 处理监听的描述符就绪外,其他的描述符都没有就绪
        }

        // 对就绪的客户端进行操作
        for(int i = listenfd + 1; i <= max_fd; i++){
            if(fds[i].revents & POLLIN){
                n = recv(i, buff, MAXLNE, 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中清除出去
                    fds[i].fd = -1;
                    close(i);
                }
                if(nready-- == 0) break; // 没有就绪的客户端了
            }
        }
    }

    return 0;
}
#else // epoll
//poll/select -->  
/*
* 相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。
* 因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
*/
    int epfd = epoll_create(1); // epoll_create函数用于创建一个epoll模型 :

    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); // epoll_ctl函数用于向指定的epoll模型中注册事件,这里将监听描述符放入

    while(1){

        int nready = epoll_wait(epfd, events, POLL_SIZE, 5); // epoll_wait函数用于收集监视的事件中已经就绪的事件

        if(nready == -1) continue;

        for(int i = 0; i < nready; i++){

            int clientfd = events[i].data.fd;

            if(clientfd == listenfd){

                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                // accept分配fd是从小到大的分配,所有listenfd比后面进来客户端的值都小
                if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
                    printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                    return 0;
                }

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

                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); // epoll_ctl函数用于向指定的epoll模型中注册事件:

            }else if(events[i].events & EPOLLIN){
                    // printf("ac \n");
                    n = recv(clientfd, buff, MAXLNE, 0); // n = recv(clientfd, buff, MAXLNE, 0);
                    if (n > 0) { // if (n > 0) {
                        buff[n] = '\0'; // buff[n] = '\0';
                        printf("recv msg from client: %s\n", buff); // printf("recv msg from client: %s\n", buff);

                        send(clientfd, buff, n, 0); // send(clientfd, buff, n, 0);

                    } else if (n == 0) {
                        // 客户端关闭连接,应该清除出去
                        ev.events = EPOLLIN;
                        ev.data.fd = clientfd;
                        epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
                        close(clientfd);
                    }

            }

        }
    }

    return 0;
}
#endif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值