unix网络编程-select函数

本文详细介绍了select函数在Unix网络编程中的作用,它允许程序等待多个描述符就绪,避免了阻塞在单一I/O操作上。select函数的工作原理、参数解释以及编程注意事项被清晰阐述,包括描述符集的管理、超时参数和不同条件下的套接字状态。文章还给出了一个服务端程序示例,展示了如何在实际应用中使用select处理多个连接。

前言

系统的补充网络编程基础。

基本来源自:《UNIX网络编程》6.3 select函数


select介绍

chapter04_基本TCP套接字编程中使用的是最基本的阻塞式I/O模型。默认情形下, 所有套接字都是阻塞的。如下图所示:

在这里插入图片描述
有了I/O复用(I/O multiplexing), 我们就可以调用select或poll, 阻塞在这两个系统调用中的某一个之上, 而不是阻塞在真正的I/O系统调用上。 (当有套接字需要读取的时候,才调用read,而不是read等待对方发送数据)。如下图所示:
在这里插入图片描述
chapter05_TCP客户_服务器示例 中,当客户端与服务器建立连接后,服务端会创建一个子线程来来处理与客户端之后的事情。

I/O复用并不显得有什么优势,事实上由于使用select需要两个而不是单个系统调用,I/O复用还稍有劣势。不过,使用select的优势在于我们可以等待多个描述符就绪

select允许进程指示内核等待多个事件中的任何一个发生, 并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它

它的接口如下所示:

#include <sys/select.h>  
#include <sys/time.h>  
int select(int nfds, fd_set *readset, fd_set *writeset, fd_set *exceptset,  
			const struct timeval *timeout);

返回: 若有就绪描述符则为其数目,若超时则为0,若出错则为-1。

我们从该函数的最后一个参数timeout开始介绍,它告知内核等待所指 定描述符中的任何一个就绪可花多长时间。 其timeval结构用于指定这段时间的秒数和微秒数。

这个参数有以下三种可能。

  • 永远等待下去: 仅在有一个描述符准备好I/O时才返回。 为此,我们把该参数设置为空指针。
  • 等待一段固定时间: 在有一个描述符准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
  • 根本不等待: 检查描述符后立即返回,这称为轮询(polling) 。为此,该参数必须指向一个timeval结构, 而且其中的定时器值(由该结构指定的秒数和微秒数) 必须为0。

前两种情形的等待通常会被进程在等待期间捕获的信号中断, 并从信号处理函数返回。(小心内核不重启select系统调用。可以参考chapter05_TCP客户_服务器示例

中间的三个参数readset、 writeset和exceptset指定我们要让内核测试读、 写和异常条件的描述符。

如何给这3个参数中的每一个参数指定一个或多个描述符值是一个设计上的问题。 select使用描述符集, 通常是一个整数数组, 其中每个整数中的每一位对应一个描述符。 举例来说,假设使用32位整数, 那么该数组的第一个元素对应于描述符031,第二个元素对应于描述符3263, 依此类 推。 所有这些实现细节都与应用程序无关, 它们隐藏在名为fd_set的数据类 型和以下四个宏中:

void FD_ZERO(fd_set *fdset);  /* clear all bits in fdset */  
void FD_SET(int fd, fd_set *fdset);   /* turn on the bit for fd in fdset */ 
void FD_CLR(int fd, fd_set *fdset); /* trun off the bit for fd in fdset */
int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset */

我们分配一个fd_set数据类型的描述符集, 并用这些宏设置或测试该集合中的每一位, 也可以用C语言中的赋值语句把它赋值成另外一个描述符集。(这个赋值,应该是浅拷贝,应为C中赋值数组,应该只是赋值指针,毕竟没有运算符重载)

nfds参数指定待测试的描述符个数, 它的值是待测试的最大描述符加1。 描述符0, 1, 2, …, 一直到nfds-1均将被测试。(因为描述符是顺序增大分配。测试的不仅仅是参数中的描述符集中的设置的描述符。得遍历fd_set数组,才知道哪些描述符被设置,可读或者可写)

满足下列四个条件中的任何一个时, 一个套接字准备好读

  • 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。 对这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据) 。 我们可以使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。 对于TCP和UDP套接字而言, 其默认值为1。
  • 该连接的读半部关闭(也就是接收了FIN的TCP连接) 。 对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF) 。
  • 该套接字是一个监听套接字且已完成的连接数不为0。 对这样的套接字的accept通常不会阻塞, 不过我们将在15.6节讲解accept可能阻塞的一种时序条件。
  • 其上有一个套接字错误待处理。 对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误) , 同时把errno设置成确切的错误条件。这些待处理错误(pending error) 也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。

下列四个条件中的任何一个满足时, 一个套接字准备好写

  • 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小, 并且或者该套接字已连接, 或者该套接字不需要连接(如UDP套接字) 。 这意味着如果我们把这样的套接字设置成非阻塞(第16章) , 写操作将不阻塞并返回一个正值(如由传输层接受的字节数) 。 我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。 对于TCP和UDP套接字而言, 其默认值通常为2048。
  • 该连接的写半部关闭。 对这样的套接字的写操作将产生SIGPIPE信号(5.12节)
  • 使用非阻塞式connect的套接字已建立连接, 或者connect已经以失败告终。
  • 其上有一个套接字错误待处理。 对这样的套接字的写操作将不阻塞并返回-1(也就是返回一个错误) , 同时把errno设置成确切的错误条件。 这些待处理的错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。

select编程 - 服务端程序

参考:《unix网络编程》6.8 TCP回射服务器程序; Linux编程之select

代码也可见仓库:UNP/chapter06

这个代码是有问题的:FD_ISSET将描述符集中的对应位清零,没有重新置1。需要维护一个数组来记录有哪些套接字。

2022/9/5 修:select的示例代码可以参考:tiny-server-select。代码里面使用了数组保存描述符集。

select函数返回后, 我们使用FD_ISSET宏来测试fd_set数据类型中的描述符。 描述符集内任何与未就绪描述符对应的位返回时均清成0。 为此, 每次重新调用select函数时, 我们都得再次把所有描述符集内所关心的位均置为1。

使用select时最常见的两个编程错误是: 忘了对最大描述符加1; 忘了描述符集是值-结果参数。 第二个错误导致调用select时, 描述符集内我们认为是1的位却被置为0。

#include "log.hpp"

#include <sys/socket.h>
#include <bits/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>

#include <iostream>
#include <string>

#define LISTENQ 1024
#define BUFF_SIZE 1024

using namespace std;

int main(void)
{
    BOOST_LOG_TRIVIAL(info) << "start server.";

    string server_port = "10000";

    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd < 0) {
        BOOST_LOG_TRIVIAL(error) << "Failed to create a listening socket.";
        return -1;
    }
  
    struct sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(stoi(server_port));

    bind(listenfd, (sockaddr *)&servaddr, sizeof(servaddr));

    if(listen(listenfd, LISTENQ) < 0) {
        BOOST_LOG_TRIVIAL(error) << "Failed to set up the listening socket.";
        return -1;
    }

    fd_set readfds;
    FD_ZERO(&readfds);
    int nfds = -1; // 记录最大的套接字+1

    while(1) {
        FD_SET(listenfd, &readfds); // 将监听套接字加入读描述符集

        // 永远等待下去,直到存在套接字可读
        nfds = listenfd > nfds-1 ? listenfd+1 : nfds;
        int n = select(nfds, &readfds, nullptr, nullptr, nullptr);

        if(n < 0) {
            BOOST_LOG_TRIVIAL(error) << "Select error...";
            return -1;
        }

        for(int fd=0; fd<nfds; fd++) {
            if(FD_ISSET(fd, &readfds)) {
                if(fd == listenfd) { // 监听套接字已完成的连接数不为0 --> 监听套接字准备好读
                    int client_sockfd = accept(listenfd, nullptr, nullptr);
                    FD_SET(client_sockfd, &readfds); // 将客户端套接字加入读描述符集
                    nfds = client_sockfd > nfds-1 ? client_sockfd+1 : nfds;
                } else {
                    char buffer[BUFF_SIZE] = {0};
                    int readbytes = read(fd, buffer, BUFF_SIZE);
                    if(readbytes > 0) {
                        cout << "receive data: " << buffer;
                    } else {
                        close(fd); // 这里需要注意,对于Tcp,能否根据返回read的返回值为零,来作为关闭socket的依据?
                        FD_CLR(fd, &readfds);
                        BOOST_LOG_TRIVIAL(info) << "Close socket fd: " << fd;
                    }
                }
            }
        }
    }

    close(listenfd);
}

使用nc作为客户端,连接上面程序,进行测试。

在这里插入图片描述


其他

  • 可以搜下“select epoll 区别”。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

da1234cao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值