前言
系统的补充网络编程基础。
基本来源自:《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 区别”。