云计算百科
云计算领域专业知识百科平台

【网络】高级IO——poll版本TCP服务器

目录

前言

一,poll函数

 1.1.参数一:fds

1.2.参数二,nfds

1.3.参数三,timeout

1.4.返回值

1.5.poll函数简单使用示例

二,poll版TCP服务器编写

2.1.编写

2.2.poll的优缺点

2.3.源代码


前言

由于select函数有下面几个特别明显的缺点,就推演出了改进版本——poll函数

  • 比如select监视的fd是有上限的,我的云服务器内核版本下最大上限是1024个fd,主要还是因为fd_set他是一个固定大小的位图结构,位图中的数组开辟之后不会在变化了,这是内核的数据结构,除非你修改内核参数,否则不会在变化了,所以一旦select监视的fd数量超过1024,则select会报错。
  • 除此之外,select大部分的参数都是输入输出型参数,用户和内核都会不断的修改这些参数的值,导致每次调用select前,都需要重新设置fd_set位图中的内容,这在用户层面上会带来很多不必要的遍历+拷贝的成本。

        poll接口主要解决了select接口的两个问题,一个是select监视的fd有上限,另一个是select每次调用前都需要借助第三方数组,向fd_set里面重新设置关心的fd。 

        select() 和 poll() 系统调用的本质一样,poll() 的机制与 select() 类似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。

只要我们理解了我们的select,poll也就不在话下

        poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

一,poll函数

这个函数的功能和select一模一样。都是监视并等待多个文件描述符的属性变化

poll函数处理的还是IO过程里面的等待过程!!!

 1.1.参数一:fds

这个参数就需要我们知道struct pollfd结构体是什么东西!!!

struct pollfd{
int fd;//文件描述符
short events;//等待的事件
short revents;//实际发生的事件
};

         poll 函数是 Unix/Linux 系统中用于监视多个文件描述符(file descriptors)状态变化的一种机制。poll 函数能够同时监视多个文件描述符,以检查其上是否发生了感兴趣的事件(如可读、可写、错误等)。为了实现这一功能,poll 函数使用了一个 pollfd 结构体数组作为输入参数,每个 pollfd 结构体代表了一个被监视的文件描述符及其相关的事件。

下面是对 pollfd 结构体各成员的详细解释:

  • int fd;这个成员变量是一个整数,表示被监视的文件描述符。文件描述符是一个非负整数,它是一个索引值,指向内核中打开文件的表项。通过文件描述符,我们可以对打开的文件进行读写操作。在 poll 函数的上下文中,这个文件描述符可以是任何类型的文件、套接字(socket)或者管道(pipe)等。
  • short events;:这个成员变量是一个位掩码(bitmask),用于指定我们想要监视的、在该文件描述符上可能发生的事件。这些事件可以是以下几种之一(或它们的组合,通过位或操作符 | 实现):
  • POLLIN:有数据可读。
  • POLLOUT:写数据不会阻塞。
  • POLLERR:发生错误。
  • POLLHUP:挂起(hang up)。
  • POLLNVAL:无效的文件描述符。
  • 以及其他一些可能依赖于特定实现的标志。
  • 相比于select,poll将输入和输出事件进行了分离 

    比如我们希望内核帮我们关注 0号文件描述符上的读事件

    struct pollfd rfds;
    rfds.fd = 0; //希望内核帮我们关注0号文件描述符上的事件
    rfds.events |= POLLIN; //希望内核关注0号文件描述符上的读事件
    //rfds.events = POLLIN | POLLOUT; //希望既监听读事件又监听写事件
    rfds.revents = 0; //这个参数用于内核通知我们有事件就绪了,让我们赶紧来取

    如果我们要判断读事件是否就绪

    if(rfds.revents & POLLIN){
    std::cout << "读事件就绪了…" << std::endl;
    }

    • short revents;这个成员变量在 poll 函数调用返回时被填充,它也是一个位掩码,表示在调用 poll 期间,实际发生在文件描述符上的事件。与 events 不同,revents 是由 poll 函数自动填写的,用户不需要(也不应该)在调用 poll 之前设置它。revents 的值可能包含 events 中指定的任何事件,或者由于某种原因(如错误或挂起)而包含其他事件。

    events和revents的取值都是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。 

    • 在调用poll函数之前,可以通过“或”运算符将要检测的事件添加到events成员当中。
    • 在poll函数返回后,可以通过“与”运算符检测revents成员中是否包含特定事件,以得知对应文件描述符的特定事件是否就绪。

    注意:每个结构体的 events 域是由用户来设置,告诉内核我们关注的是什么,而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件 

    • 使用 poll 函数时,你通常会创建一个 pollfd 结构体数组,每个元素代表一个你想要监视的文件描述符及其事件。
    • 然后,你调用 poll 函数,并将这个数组和数组的大小作为参数传递给它。
    • poll 函数会阻塞(或根据需要立即返回),直到一个或多个文件描述符上发生了请求的事件,或者超过了指定的超时时间。
    • 最后,你可以通过检查每个 pollfd 结构体的 revents 成员来确定哪些文件描述符上发生了哪些事件。

    1.2.参数二,nfds

    nfds_t类型是什么?

            nfds_t 类型是在 Unix/Linux 系统编程中,特别是在使用 poll、ppoll、select 等系统调用时,用来表示文件描述符数量的数据类型。这个类型的确切定义可能会根据不同的系统和库实现而有所不同,但通常它是一个足够大的整数类型,以容纳系统可能支持的最大文件描述符数量。

           在大多数现代 Unix-like 系统中,nfds_t 通常是 unsigned int 或 unsigned long 的别名,但这不是一个固定的规则,因此最好查看你的系统或库的文档以获取确切的定义。

    例如,在 glibc(GNU C Library)中,nfds_t 的定义可能看起来像这样(尽管这取决于 glibc 的版本和配置):

     typedef unsigned long nfds_t; 

    或者在某些情况下,它可能是:

     typedef unsigned int nfds_t; 

    当你使用 poll 函数时,你需要将一个 nfds_t 类型的值作为第一个参数传递给函数,这个值表示 pollfd 结构体数组中的元素数量。这个值应该大于或等于数组中实际元素的数量,因为 poll 会检查这个范围内的所有 pollfd 结构体。

     #include <poll.h>  
       int main() {  
     struct pollfd fds[2];  
     // 初始化 fds 数组…  
       nfds_t nfds = sizeof(fds) / sizeof(fds[0]);  
     int timeout = -1; // 无限等待  
       int ret = poll(fds, nfds, timeout);  
     // 处理 poll 的返回值…  
       return 0;  
     } 

    在这个例子中,nfds 被设置为 fds 数组的大小,这是调用 poll 时应该传递的正确值。注意,虽然在这个例子中 nfds 被显式地计算为数组的大小,但在许多情况下,你可能已经知道要监视的文件描述符数量,因此可以直接使用那个值。

    1.3.参数三,timeout

    这个参数是一个输入型参数,单位是毫秒,代表阻塞等待的时间,超过该时间就会变为非阻塞等待。

  • timeout = -1,代表永久阻塞等待
  • timeout = 0,代表永久非阻塞等待
  • timeout > 0,代表先阻塞等待 timeout 毫秒,超过这个时间变为非阻塞等待,poll函数返回
  • 1.4.返回值

    成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;

    失败时,poll() 返回 -1,并设置 errno 为下列值之一:

  • EBADF:一个或多个结构体中指定的文件描述符无效。
  • EFAULT:fds 指针指向的地址超出进程的地址空间。
  • EINTR:请求的事件之前产生一个信号,调用可以重新发起。
  • EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
  • ENOMEM:可用内存不足,无法完成请求。
  • 1.5.poll函数简单使用示例

    #include <stdio.h> // 引入标准输入输出库,用于printf等函数
    #include <stdlib.h> // 引入标准库,用于EXIT_FAILURE等宏定义
    #include <unistd.h> // 引入POSIX操作系统API,用于open、close等函数
    #include <fcntl.h> // 引入文件控制选项,如O_RDONLY
    #include <poll.h> // 引入poll函数及其相关结构体pollfd

    int main() {
    // 尝试以只读模式打开名为"test.txt"的文件
    int fd = open("test.txt", O_RDONLY);
    if (fd < 0) {
    // 如果文件打开失败,打印错误信息并返回失败状态
    perror("Failed to open file");
    return EXIT_FAILURE;
    }

    // 定义一个pollfd结构体数组,用于存储要监视的文件描述符及其事件
    struct pollfd fds[1];
    // 设置数组的第一个元素,指定要监视的文件描述符及其感兴趣的事件(POLLIN,表示可读)
    fds[0].fd = fd;
    fds[0].events = POLLIN;

    // 设置poll函数的超时时间为5000毫秒(5秒)
    int timeout = 5000; // 5 seconds
    // 调用poll函数,监视fds数组中的文件描述符,超时时间为timeout
    int ret = poll(fds, 1, timeout);

    // 检查poll函数的返回值
    if (ret == -1) {
    // 如果poll调用失败,打印错误信息,关闭文件描述符,并返回失败状态
    perror("poll failed");
    close(fd);
    return EXIT_FAILURE;
    }

    // 如果poll超时,没有任何事件发生
    if (ret == 0) {
    printf("No data within five seconds\\n");
    } else if (fds[0].revents & POLLIN) {
    // 如果在文件描述符上发生了POLLIN事件(即可读)
    char buf[1024]; // 定义一个缓冲区,用于存储读取的数据
    ssize_t bytes_read = read(fd, buf, sizeof(buf) – 1); // 从文件描述符中读取数据到缓冲区
    if (bytes_read > 0) { // 如果成功读取到数据
    buf[bytes_read] = '\\0'; // 在读取到的数据末尾添加字符串结束符'\\0'
    printf("Read %zd bytes: %s\\n", bytes_read, buf); // 打印读取到的数据及其长度
    }
    }

    // 关闭文件描述符,释放资源
    close(fd);
    // 程序正常结束
    return EXIT_SUCCESS;
    }

    这段代码展示了如何使用poll函数来监视一个文件描述符(在这个例子中是一个打开的文件)的状态,特别是检查文件是否有数据可读。如果文件在指定的超时时间内变得可读,程序将读取并打印文件内容;如果超时,则打印一条消息表示没有数据可读。

    二,poll版TCP服务器编写

    2.1.编写

    我们这个poll版TCP服务器和那个select版本的差不多!!!所以我们基本就是在select版本上面的基础上进行修改

    PollServer.hpp初始版本

    #pragma once
    #include <iostream>
    #include "Socket.hpp"
    #include <sys/select.h>
    #include <sys/time.h>
    #include <poll.h>

    const uint16_t default_port = 8877; // 默认端口号
    const std::string default_ip = "0.0.0.0"; // 默认IP
    const int default_fd = -1;
    const int fd_num_max=64;
    const int non_event=0;

    class PollServer
    {
    public:
    PollServer(const uint16_t port = default_port, const std::string ip = default_ip)
    : ip_(ip), port_(port)
    {
    for(int i=0;i<fd_num_max;i++)
    {
    event_fds[i].fd=default_fd;
    event_fds[i].events=non_event;//暂时不关心
    event_fds[i].revents=non_event;//暂时不关心
    }
    }
    ~PollServer()
    {
    listensock_.Close();
    }

    bool Init()
    {
    listensock_.Socket();
    listensock_.Bind(port_);
    listensock_.Listen();

    return true;
    }

    void Start()
    {

    }

    private:
    uint16_t port_; // 绑定的端口号
    Sock listensock_; // 专门用来listen的
    std::string ip_; // ip地址
    struct pollfd event_fds[fd_num_max];
    };

    上面这些就是最最基本的东西,接下来我们就很容易写出下面这些东西!!

    void HandlerEvent()
    {
    for (int n = 0; n < fd_num_max; n++)
    {
    int fd = event_fds[n].fd;
    if (fd == default_fd) // 无效的
    continue;

    if (event_fds[n].revents&POLLIN) // fd套接字就绪了
    {
    // 1.是listen套接字就绪了
    if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
    {
    Accept();
    }
    // 2.是通信的套接字就绪了,fd不是listen套接字
    else // 读事件
    {
    Receiver(fd,n);
    }
    }
    }
    }

    void Start()
    {
    int listensock = listensock_.Fd();
    event_fds[0].fd=listensock;//把listen放到首个数组下标里面
    event_fds[0].events=POLLIN;//只关心读事件
    //revent可以不设置
    int timeout=3000;//3s
    for (;;)
    {
    int n = poll(event_fds,fd_num_max,timeout);

    switch (n)
    {
    case 0:
    std::cout << "time out….." << std::endl;
    break;
    case -1:
    std::cout << "poll error" << std::endl;
    break;
    default:
    // 有事件就绪
    std::cout << "get a new link" << std::endl;
    HandlerEvent(); // 处理事件
    break;
    }
    }
    }

    接下来就是修改 Accept()和Receive()了

    void Accept()
    {
    // 我们的连接事件就绪了
    std::string clientip;
    uint16_t clientport;

    int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
    // 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
    if (sockfd < 0)
    return;
    else // 把新fd加入位图
    {
    int i = 1;
    for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
    {
    if (event_fds[i].fd != default_fd) // 没找到空位
    {
    continue;
    }
    else
    { // 找到空位,但不能直接添加
    break;
    }
    }
    if (i != fd_num_max) // 没有满
    {
    event_fds[i].fd = sockfd; // 把新连接加入数组
    event_fds[i].events=POLLIN ;//关心读事件
    event_fds[i].revents = non_event;//重置一下

    Printfd();
    }
    else // 满了
    {
    close(sockfd); // 处理不了了,可以直接选择关闭连接
    //当然,如果想要连接更多链接,我们可以在这里进行扩容操作,不过我们这里就不做了
    }
    }
    }
    void Printfd()
    {
    std::cout << "online fd list: ";
    for (int i = 0; i < fd_num_max; i++)
    {
    if (event_fds[i].fd == default_fd)
    continue;
    else
    {
    std::cout << event_fds[i].fd << " ";
    }
    }
    std::cout << std::endl;
    }

    void Receiver(int fd, int i)
    {
    char in_buff[1024];
    int n = read(fd, in_buff, sizeof(in_buff) – 1);
    if (n > 0)
    {
    in_buff[n] = 0;
    std::cout << "get message: " << in_buff << std::endl;
    }
    else if (n == 0) // 客户端关闭连接
    {
    close(fd); // 我服务器也要关闭
    event_fds[i].fd = default_fd; // 重置数组内的值
    }
    else
    {
    close(fd); // 我服务器也要关闭
    event_fds[i].fd = default_fd; // 重置数组内的值
    }
    }

     PollServer.hpp

    #pragma once
    #include <iostream>
    #include "Socket.hpp"
    #include <sys/select.h>
    #include <sys/time.h>
    #include <poll.h>

    const uint16_t default_port = 8877; // 默认端口号
    const std::string default_ip = "0.0.0.0"; // 默认IP
    const int default_fd = -1;
    const int fd_num_max=64;
    const int non_event=0;

    class PollServer
    {
    public:
    PollServer(const uint16_t port = default_port, const std::string ip = default_ip)
    : ip_(ip), port_(port)
    {
    for(int i=0;i<fd_num_max;i++)
    {
    event_fds[i].fd=default_fd;
    event_fds[i].events=non_event;//暂时不关心
    event_fds[i].revents=non_event;//暂时不关心
    }
    }
    ~PollServer()
    {
    listensock_.Close();
    }

    bool Init()
    {
    listensock_.Socket();
    listensock_.Bind(port_);
    listensock_.Listen();

    return true;
    }

    void Accept()
    {
    // 我们的连接事件就绪了
    std::string clientip;
    uint16_t clientport;

    int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
    // 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
    if (sockfd < 0)
    return;
    else // 把新fd加入位图
    {
    int i = 1;
    for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
    {
    if (event_fds[i].fd != default_fd) // 没找到空位
    {
    continue;
    }
    else
    { // 找到空位,但不能直接添加
    break;
    }
    }
    if (i != fd_num_max) // 没有满
    {
    event_fds[i].fd = sockfd; // 把新连接加入数组
    event_fds[i].events=POLLIN ;//关心读事件
    event_fds[i].revents = non_event;//重置一下

    Printfd();
    }
    else // 满了
    {
    close(sockfd); // 处理不了了,可以直接选择关闭连接
    //当然,如果想要连接更多链接,我们可以在这里进行扩容操作,不过我们这里就不做了
    }
    }
    }
    void Printfd()
    {
    std::cout << "online fd list: ";
    for (int i = 0; i < fd_num_max; i++)
    {
    if (event_fds[i].fd == default_fd)
    continue;
    else
    {
    std::cout << event_fds[i].fd << " ";
    }
    }
    std::cout << std::endl;
    }

    void Receiver(int fd, int i)
    {
    char in_buff[1024];
    int n = read(fd, in_buff, sizeof(in_buff) – 1);
    if (n > 0)
    {
    in_buff[n] = 0;
    std::cout << "get message: " << in_buff << std::endl;
    }
    else if (n == 0) // 客户端关闭连接
    {
    close(fd); // 我服务器也要关闭
    event_fds[i].fd = default_fd; // 重置数组内的值
    }
    else
    {
    close(fd); // 我服务器也要关闭
    event_fds[i].fd = default_fd; // 重置数组内的值
    }
    }

    void HandlerEvent()
    {
    for (int n = 0; n < fd_num_max; n++)
    {
    int fd = event_fds[n].fd;
    if (fd == default_fd) // 无效的
    continue;

    if (event_fds[n].revents&POLLIN) // fd套接字就绪了
    {
    // 1.是listen套接字就绪了
    if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
    {
    Accept();
    }
    // 2.是通信的套接字就绪了,fd不是listen套接字
    else // 读事件
    {
    Receiver(fd,n);
    }
    }
    }
    }

    void Start()
    {
    int listensock = listensock_.Fd();
    event_fds[0].fd=listensock;//把listen放到首个数组下标里面
    event_fds[0].events=POLLIN;//只关心读事件
    //revent可以不设置
    int timeout=3000;//3s
    for (;;)
    {
    int n = poll(event_fds,fd_num_max,timeout);

    switch (n)
    {
    case 0:
    std::cout << "time out….." << std::endl;
    break;
    case -1:
    std::cout << "poll error" << std::endl;
    break;
    default:
    // 有事件就绪
    std::cout << "get a new link" << std::endl;
    HandlerEvent(); // 处理事件
    break;
    }
    }
    }

    private:
    uint16_t port_; // 绑定的端口号
    Sock listensock_; // 专门用来listen的
    std::string ip_; // ip地址
    struct pollfd event_fds[fd_num_max];
    };

    我们再链接一个看看

    非常完美了啊!!!

    2.2.poll的优缺点

    • poll不是也用了一个数组吗?他是怎么解决了fd有上限的问题?

    凭的是这个数组的大小是我们自己设置的,而select的那个数组是已经固定了的。

            其实poll的优点就是解决了select支持的fd有上限,以及用户输入信息和内核输出信息耦合的两个问题。

            但poll的缺点其实在上面的代码已经体现出来了一部分,内核在检测fd是否就绪时,需要遍历整个结构体数组检测events的值,同样用户在处理就绪的fd事件时,也需要遍历整个结构体数组检测revents的值,当rfds结构体数组越来越大时,每次遍历数组其实就会降低服务器的效率,为此,内核提供了epoll接口来解决这样的问题。

            与select相同的是,poll也需要用户自己维护一个第三方数组来存储用户需要关心的fd及事件,只不过poll不需要在每次调用前都重新设置关心的fd,因为用户的输入和内核的输出是分离的,分别在结构体的events和revents的两个字段,做到了输入和输出分离。

    2.3.源代码

    PollServer.hpp

    #pragma once
    #include <iostream>
    #include "Socket.hpp"
    #include <sys/select.h>
    #include <sys/time.h>
    #include <poll.h>

    const uint16_t default_port = 8877; // 默认端口号
    const std::string default_ip = "0.0.0.0"; // 默认IP
    const int default_fd = -1;
    const int fd_num_max=64;
    const int non_event=0;

    class PollServer
    {
    public:
    PollServer(const uint16_t port = default_port, const std::string ip = default_ip)
    : ip_(ip), port_(port)
    {
    for(int i=0;i<fd_num_max;i++)
    {
    event_fds[i].fd=default_fd;
    event_fds[i].events=non_event;//暂时不关心
    event_fds[i].revents=non_event;//暂时不关心
    }
    }
    ~PollServer()
    {
    listensock_.Close();
    }

    bool Init()
    {
    listensock_.Socket();
    listensock_.Bind(port_);
    listensock_.Listen();

    return true;
    }

    void Accept()
    {
    // 我们的连接事件就绪了
    std::string clientip;
    uint16_t clientport;

    int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
    // 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
    if (sockfd < 0)
    return;
    else // 把新fd加入位图
    {
    int i = 1;
    for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
    {
    if (event_fds[i].fd != default_fd) // 没找到空位
    {
    continue;
    }
    else
    { // 找到空位,但不能直接添加
    break;
    }
    }
    if (i != fd_num_max) // 没有满
    {
    event_fds[i].fd = sockfd; // 把新连接加入数组
    event_fds[i].events=POLLIN ;//关心读事件
    event_fds[i].revents = non_event;//重置一下

    Printfd();
    }
    else // 满了
    {
    close(sockfd); // 处理不了了,可以直接选择关闭连接
    //当然,如果想要连接更多链接,我们可以在这里进行扩容操作,不过我们这里就不做了
    }
    }
    }
    void Printfd()
    {
    std::cout << "online fd list: ";
    for (int i = 0; i < fd_num_max; i++)
    {
    if (event_fds[i].fd == default_fd)
    continue;
    else
    {
    std::cout << event_fds[i].fd << " ";
    }
    }
    std::cout << std::endl;
    }

    void Receiver(int fd, int i)
    {
    char in_buff[1024];
    int n = read(fd, in_buff, sizeof(in_buff) – 1);
    if (n > 0)
    {
    in_buff[n] = 0;
    std::cout << "get message: " << in_buff << std::endl;
    }
    else if (n == 0) // 客户端关闭连接
    {
    close(fd); // 我服务器也要关闭
    event_fds[i].fd = default_fd; // 重置数组内的值
    }
    else
    {
    close(fd); // 我服务器也要关闭
    event_fds[i].fd = default_fd; // 重置数组内的值
    }
    }

    void HandlerEvent()
    {
    for (int n = 0; n < fd_num_max; n++)
    {
    int fd = event_fds[n].fd;
    if (fd == default_fd) // 无效的
    continue;

    if (event_fds[n].revents&POLLIN) // fd套接字就绪了
    {
    // 1.是listen套接字就绪了
    if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
    {
    Accept();
    }
    // 2.是通信的套接字就绪了,fd不是listen套接字
    else // 读事件
    {
    Receiver(fd,n);
    }
    }
    }
    }

    void Start()
    {
    int listensock = listensock_.Fd();
    event_fds[0].fd=listensock;//把listen放到首个数组下标里面
    event_fds[0].events=POLLIN;//只关心读事件
    //revent可以不设置
    int timeout=3000;//3s
    for (;;)
    {
    int n = poll(event_fds,fd_num_max,timeout);

    switch (n)
    {
    case 0:
    std::cout << "time out….." << std::endl;
    break;
    case -1:
    std::cout << "poll error" << std::endl;
    break;
    default:
    // 有事件就绪
    std::cout << "get a new link" << std::endl;
    HandlerEvent(); // 处理事件
    break;
    }
    }
    }

    private:
    uint16_t port_; // 绑定的端口号
    Sock listensock_; // 专门用来listen的
    std::string ip_; // ip地址
    struct pollfd event_fds[fd_num_max];
    };

    Socket.hpp

    #pragma once

    #include <iostream>
    #include <string>
    #include <unistd.h>
    #include <cstring>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>

    // 定义一些错误代码
    enum
    {
    SocketErr = 2, // 套接字创建错误
    BindErr, // 绑定错误
    ListenErr, // 监听错误
    };

    // 监听队列的长度
    const int backlog = 10;

    class Sock //服务器专门使用
    {
    public:
    Sock() : sockfd_(-1) // 初始化时,将sockfd_设为-1,表示未初始化的套接字
    {
    }
    ~Sock()
    {
    // 析构函数中可以关闭套接字,但这里选择不在析构函数中关闭,因为有时需要手动管理资源
    }

    // 创建套接字
    void Socket()
    {
    sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd_ < 0)
    {
    printf("socket error, %s: %d", strerror(errno), errno); //错误
    exit(SocketErr); // 发生错误时退出程序
    }
    int opt=1;
    setsockopt(sockfd_,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //服务器主动关闭后快速重启
    }

    // 将套接字绑定到指定的端口上
    void Bind(uint16_t port)
    {
    //让服务器绑定IP地址与端口号
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));//清零
    local.sin_family = AF_INET; // 网络
    local.sin_port = htons(port); // 我设置为默认绑定任意可用IP地址
    local.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口

    if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0) //让自己绑定别人
    {
    printf("bind error, %s: %d", strerror(errno), errno);
    exit(BindErr);
    }
    }

    // 监听端口上的连接请求
    void Listen()
    {
    if (listen(sockfd_, backlog) < 0)
    {
    printf("listen error, %s: %d", strerror(errno), errno);
    exit(ListenErr);
    }
    }

    // 接受一个连接请求
    int Accept(std::string *clientip, uint16_t *clientport)
    {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);

    if(newfd < 0)
    {
    printf("accept error, %s: %d", strerror(errno), errno);
    return -1;
    }

    char ipstr[64];
    inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
    *clientip = ipstr;
    *clientport = ntohs(peer.sin_port);

    return newfd; // 返回新的套接字文件描述符
    }

    // 连接到指定的IP和端口——客户端才会用的
    bool Connect(const std::string &ip, const uint16_t &port)
    {
    struct sockaddr_in peer;//服务器的信息
    memset(&peer, 0, sizeof(peer));
    peer.sin_family = AF_INET;
    peer.sin_port = htons(port);

    inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));

    int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
    if(n == -1)
    {
    std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
    return false;
    }
    return true;
    }

    // 关闭套接字
    void Close()
    {
    close(sockfd_);
    }

    // 获取套接字的文件描述符
    int Fd()
    {
    return sockfd_;
    }

    private:
    int sockfd_; // 套接字文件描述符
    };

    main.cc

    #include"PollServer.hpp"
    #include<memory>

    int main()
    {
    std::unique_ptr<PollServer> svr(new PollServer());
    svr->Init();
    svr->Start();
    }

    makefile

    poll_server:main.cc
    g++ -o $@ $^ -std=c++11
    .PHONY:clean
    clean:
    rm -rf poll_server

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【网络】高级IO——poll版本TCP服务器
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!