【网络编程】poll

主旨思想

  • 用一个结构体记录文件描述符集合,并记录用户态状态和内核态状态

函数说明

  • 概览
#include  
struct pollfd { 
    int fd; /* 委托内核检测的文件描述符 */ 
    short events; /* 委托内核检测文件描述符的什么事件 */ 
    short revents; /* 文件描述符实际发生的事件 */ 
};

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

          通过man poll查看帮助
          参数
               fds:是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
               nfds:这个是第一个参数数组中最后一个有效元素的下标 + 1
               timeout:阻塞时长
                         0:不阻塞
                        -1:阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
                       >0:具体的阻塞时长(ms)
         返回值
              -1:失败
              >0(n):检测的集合中有n个文件描述符发生了变化

events及revents取值,如果有多个事件需要检测,用|即可,如同时检测读和写:POLLIN|POLLOUT

【网络编程】poll_第1张图片

代码实现

注意事项

  • nfds表示的监听文件描述符的下标,所以在遍历时,需要使用fds[i].fd取得相应的文件描述符
  • 如何优雅的更新nfds?代码中使用连接的文件描述符作为替代更新

服务器端:
 

#include 
#include 
#include 
#include 
#include 
#include 

#define SERVERIP "127.0.0.1"
#define PORT 6789


int main()
{
    // 1. 创建socket(用于监听的套接字)
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) {
        perror("socket");
        exit(-1);
    }
    // 2. 绑定
    struct sockaddr_in server_addr;
    server_addr.sin_family = PF_INET;
    // 点分十进制转换为网络字节序
    inet_pton(AF_INET, SERVERIP, &server_addr.sin_addr.s_addr);
    // 服务端也可以绑定0.0.0.0即任意地址
    // server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    int ret = bind(listenfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (ret == -1) {
        perror("bind");
        exit(-1);
    }
    // 3. 监听
    ret = listen(listenfd, 8);
        if (ret == -1) {
        perror("listen");
        exit(-1);
    }
    
    struct pollfd fds[1024];
    // 初始化
    for (int i = 0; i < 1024; i++) {
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    // 将监听文件描述符加入
    fds[0].fd = listenfd;
    int nfds = 0;
    // 不断循环等待客户端连接
    while (1) {
        // 使用poll,设置为永久阻塞,有文件描述符变化才返回
        int num = poll(fds, nfds + 1, -1);
        if (num == -1) {
            perror("poll");
            exit(-1);
        } else if (num == 0) {
            // 当前无文件描述符有变化,执行下一次遍历
            // 在本次设置中无效(因为select被设置为永久阻塞)
            continue;
        } else {
            // 首先判断监听文件描述符是否发生改变(即是否有客户端连接)
            if (fds[0].revents & POLLIN) {
                // 4. 接收客户端连接
                struct sockaddr_in client_addr;
                socklen_t client_addr_len = sizeof(client_addr);
                int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &client_addr_len);
                if (connfd == -1) {
                    perror("accept");
                    exit(-1);
                }
                // 输出客户端信息,IP组成至少16个字符(包含结束符)
                char client_ip[16] = {0};
                inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip));
                unsigned short client_port = ntohs(client_addr.sin_port);
                printf("ip:%s, port:%d\n", client_ip, client_port);
                // 遍历集合, 将新的需要监听的文件描述符加入集合
                for (int i = 1; i < 1024; i++) {
                    if (fds[i].fd == -1) {
                        fds[i].fd = connfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }
                // 更新最大的监听文件描述符集合下标
                // 存在问题:使用文件描述符替代最大对应下标
                nfds = nfds > connfd ? nfds : connfd;
            }

            // 遍历集合判断是否有变动,如果有变动,那么通信
            char recv_buf[1024] = {0};
            for (int i = 1; i <= nfds; i++) {
                if (fds[i].fd != -1 && fds[i].revents & POLLIN) {
                    ret = read(fds[i].fd, recv_buf, sizeof(recv_buf));
                    if (ret == -1) {
                        perror("read");
                        exit(-1);
                    } else if (ret > 0) {
                        printf("recv server data : %s\n", recv_buf);
                        write(fds[i].fd, recv_buf, strlen(recv_buf));
                    } else {
                        // 表示客户端断开连接
                        printf("client closed...\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                        break;
                    }
                }
            }
        }
    }

    close(listenfd);
    return 0;
}

客户端:

#include 
#include 
#include 
#include 
#include 

#define SERVERIP "127.0.0.1"
#define PORT 6789

int main()
{
    // 1. 创建socket(用于通信的套接字)
    int connfd = socket(AF_INET, SOCK_STREAM, 0);
    if (connfd == -1) {
        perror("socket");
        exit(-1);
    }
    // 2. 连接服务器端
    struct sockaddr_in server_addr;
    server_addr.sin_family = PF_INET;
    inet_pton(AF_INET, SERVERIP, &server_addr.sin_addr.s_addr);
    server_addr.sin_port = htons(PORT);
    int ret = connect(connfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (ret == -1) {
        perror("connect");
        exit(-1);
    }
    // 3. 通信
    char recv_buf[1024] = {0};
    while (1) {
        // 发送数据
        char *send_buf = "client message";
        write(connfd, send_buf, strlen(send_buf));
        // 接收数据
        ret = read(connfd, recv_buf, sizeof(recv_buf));
        if (ret == -1) {
            perror("read");
            exit(-1);
        } else if (ret > 0) {
            printf("recv server data : %s\n", recv_buf);
        } else {
            // 表示客户端断开连接
            printf("client closed...\n");
        }
        // 休眠的目的是为了更好的观察,放在此处可以解决read: Connection reset by peer问题
        sleep(1);
    }
    // 关闭连接
    close(connfd);
    return 0;
}

存在问题(缺点)

  • 缺点同select第一点和第二点(如下),即解决了第三点和第四点
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

你可能感兴趣的:(网络编程,linux,运维,服务器)