C++网络编程快速入门(二):Linux下使用select演示简单服务端程序

select参数解释

extern int select (int __nfds, 
		   fd_set *__restrict __readfds,
		   fd_set *__restrict __writefds,
		   fd_set *__restrict __exceptfds,
		   struct timeval *__restrict __timeout);

__nfds:一般设置为所有需要使用select函数检测时间的fd中的最大值+1
__readfds:需要监听可读事件的fd集合
__writefds:需要监听可写事件的fd集合
__exceptfds:需要监听可写事件的fd集合
__timeout:超时时间,在这个设定的时间内检测这些fd事件,超过这个超时时间,select将立即返回
fd_set这个结构体信息如下:

/* fd_set for select and pselect.  */
typedef struct
  {
    
    
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
  } fd_set;

可以简化看做:

typedef struct {
    
    
	long int __fds_bits[16];	
} fd_set;

long int 占8字节,也就是8 * 8 = 64个bit,所以__fds_bits数组总共就占64 * 16 = 1024个fd状态。每个bit位0表示无事件,1表示有事件。

select使用规范

1、select在调用前后可能会修改readfdswritefdsexceptfds中的内容,所以如果在下次调用时复用这些fd_set,则要在下次调用前使用FD_ZEROfd_set清空,然后调用FD_SET将要检测的fd添加到fd_set中
2、linux系统下select函数也会修改timeval结构体的值,所以想要复用也必须给其重新设置值
3、select函数的timeval结构体中的tv_sectv_usec如果都被设置为0,代表着检测事件的总时间被设置为0,行为就变成了select检测相关集合中的fd,如果没有需要的事件,则立即返回
4、如果select函数的timeval参数设置为NULL,则select会一直阻塞下去,直到我们需要的事件被触发
5、Linux下,select函数第一个参数必须设置为需要检测事件fd中最大值+1,所以每次产生一个新fd都需要和maxfd作比较

select使用缺点

1、select函数需要将fd集合从用户态拷贝到内核态,在fd较多时开销较大。并且每次检测时也是在内核中遍历这个fd_set。
2、单个进程能够监视的文件描述符数量上存在最大限制,linux上我们算过了,只有1024个
3、select函数每次调用之前都要对传入的参数重新设定,比较麻烦
4、在linux上select函数的实现原理是底层的poll函数,所以select和poll本质上没有区别

基本流程

在这里插入图片描述

实例代码

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <sys/time.h>
#include <vector>
#include <errno.h>
#include <stdio.h>
#include <string.h>
using namespace std;
int main() {
    
    
    // 创建一个监听socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) {
    
    
        cout << "create listen error" << endl;
        return -1;
    }

    // 初始化服务器地址
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_add当断开各个客户端时,服务端的select函数对各个客户端fd进行检测时,仍然会触发可读事件r = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);
    if (bind(listenfd, (struct sockaddr *)& bindaddr, sizeof(bindaddr)) == -1) {
    
    
        cout << "bind listen socket error" << endl;
        close(listenfd);
        return -1;
    }

    // 启动监听
    if (listen(listenfd,SOMAXCONN) == -1) {
    
    
        cout << "listen error" << endl;
        close(listenfd);
        return -1;
    }

    // 存储客户端socket的数组
    vector<int> clientfds;
    int maxfd;

    while (true) {
    
    
        fd_set readset;
        // 对标志位清零
        FD_ZERO(&readset);

        // 将监听的socket加入到待检测的可读事件中
        // 第listenfd位被置为1
        FD_SET(listenfd, &readset);
        maxfd = listenfd;

        // 将客户端socket加入到待检测的可读事件中
        for (int i = 0; i < clientfds.size(); i++) {
    
    
            if (clientfds[i] != -1) {
    
    
                FD_SET(clientfds[i], &readset);
                maxfd = max(maxfd, clientfds[i]);
            }
        }

        // 设置超时时间为1s
        timeval time;
        time.tv_sec = 1;
        time.tv_usec = 0;

        // 暂且只检测可读事件,不检测可写和异常事件
        int ret = select(maxfd + 1, &readset, NULL, NULL, &time);
        if (ret == -1) {
    
    
            // 出错
            if (errno != EINTR)
                break;
        } else if (ret == 0) {
    
    
            // select函数超时
            continue;
        } else {
    
    
            // 检测到某个socket有事件
            // 是否是监听socket的可读事件
            if (FD_ISSET(listenfd, &readset)) {
    
    
                // 如果是监听socket的可读事件,表示现在有新的连接到来
                struct sockaddr_in clientaddr;
                socklen_t clientaddrlen = sizeof(clientaddr);
                // 接受客户端连接
                int clientfd = accept(listenfd, (struct sockaddr *)& clientaddr, &clientaddrlen);
                if (clientfd == -1) {
    
    
                    cout << "client socket error" << endl;
                    break;
                } else {
    
    
                    cout << "accept a client connection , fd:" << clientfd << endl;
                    clientfds.push_back(clientfd);
                }
            } else {
    
    
                // 从client socket上接受数据
                // 假设对端发送过来的数据长度不超过63个字符
                char recvbuf[64];
                for (int i = 0; i < clientfds.size(); i++) {
    
    
                    if (clientfds[i] != -1 && FD_ISSET(clientfds[i], &readset)) {
    
    
                        memset(recvbuf, 0, sizeof(recvbuf));
                        // 接受数据
                        int length = recv(clientfds[i], recvbuf, 64, 0);
                        if (length <= 0) {
    
    
                            cout << "recv data error, clientfd:" << clientfds[i] << endl;
                            close(clientfds[i]);
                            // 不直接删除该元素而是将位置元素标记
                            clientfds[i] = -1;
                            continue;
                        } else {
    
    
                            cout << "clientfd:" << clientfds[i] << "recv data: " << recvbuf << endl;
                        }
                    }
                }
            }
        }
    }
    // 处理之后,关闭所有客户端
    for (int i = 0; i < clientfds.size(); i++) {
    
    
        if (clientfds[i] != -1)
            close(clientfds[i]);
    }
    // 关闭监听
    close(listenfd);
    return 0;
}

通信效果演示

这里不需要写客户端程序,直接用nc命令模拟,指定一下服务端的ip地址和端口号就可以通信了,127.0.0.1就是系统的回环地址,直接是用于本机内的socket的通信。这里我们开了两个客户端,分别发送hello和hello world。
由于nc命令发送的数据是按换行符区分的,所以数据包最后一个都是以\n结束。
当断开各个客户端时,服务端的select函数对各个客户端fd进行检测时,仍然会触发可读事件
不过此时对这些fd进行调用recv函数的话会返回0,表示对端关闭了连接。然后服务端这边将fd进行置-1,然后关闭连接即可。
在这里插入图片描述

往期文章

C++网络编程快速入门(一):TCP网络通信基本流程以及基础函数使用

Supongo que te gusta

Origin blog.csdn.net/qq_42604176/article/details/120998962
Recomendado
Clasificación