API
在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写、异常事件。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
文件描述符集合fd_set
是一个用于管理文件描述符集合的结构体。select调用返回时,内核将修改fd_set通知应用程序哪些文件描述符已就绪。
typedef struct {
unsigned long fds_bits[FD_SETSIZE / (8 * sizeof(unsigned long))];
} fd_set;
fds_bits
一个数组,用于存储文件描述符的位图,每个文件描述符状态(可读、可写、异常)在位图中用一位表示。这样可以使用少量的存储空间来表示多个文件描述符的状态。
sizeof(unsigned long)返回unsigned long类型在当前系统上的字节大小。通常在 32 位系统上为 4 字节,而在 64 位系统上为 8 字节。
因为1字节有8 位,所以8*sizeof(unsigned long)表示一个unsigned long能表示的位数。
FD_SETSIZE
一个常量,定义了fd_set能容纳的最大文件描述符的数量,这限制了select能同时处理的文件描述符总量。它的值通常是1024,但可以在不同的实现中有所不同。
例子
假设FD_SETSIZE是1024,且unsigned long是 8 字节,则8*sizeof(unsigned long)=64,1024/64=16,这意味着fd_set中需要16个unsigned long来表示 1024 个文件描述符的状态。
常用的宏
位操作比较繁琐,可以用宏访问fd_set结构体中的位。
// FD_ZERO: 初始化 fd_set 结构,将指定的集合 set 清空。
FD_ZERO(fd_set *set);
// FD_SET: 将文件描述符 fd 添加到 fd_set 结构 set 中。
FD_SET(int fd, fd_set *set);
// FD_CLR: 从 fd_set 结构 set 中移除文件描述符 fd。
FD_CLR(int fd, fd_set *set);
// FD_ISSET: 检查文件描述符 fd 是否在 fd_set 结构 set 中。
FD_ISSET(int fd, fd_set *set);
参数
nfds
监视的文件描述符数量,通常是最高文件描述符的值加一。例如,如果你监视的文件描述符是 0, 1, 和 2,则nfds应为 3。
readfds
指向一个fd_set结构的指针,用于指定需要监视可读事件的文件描述符集合。
writefds
指向一个fd_set结构的指针,用于指定需要监视可写事件的文件描述符集合。
exceptfds
指向一个fd_set结构的指针,用于指定需要监视异常条件的文件描述符集合。异常条件通常包括紧急数据等。
timeout
一个timeval结构的指针,用于设置select函数的超时时间。采用指针参数,是因为内核将修改它以告诉程序select等待了多久。如果设置为NULL,则表示无限等待;如果指定了时间,则select将在超时后返回。
struct timeval {
long tv_sec; // 秒数
long v_usec; // 微秒数
};
返回值
成功
返回就绪(可读、可写和异常)的文件描述符数量。
超时
在超时时间内没有任何文件描述符就绪,返回0。
失败
返回-1,并设置errno为EINTR,可以通过perror函数输出错误信息。
文件操作符就绪条件
socket可读条件
接收到数据
对端发送了数据,数据会被放入socket的接收缓冲区。一旦接收缓冲区中有数据可供读取,socket 的状态会变为可读,可以调用读取函数来获取数据。
对端关闭连接
对端调用了关闭操作,本端socket仍然可读,但读取的数据量为 0,表示连接已经正常关闭,而没有任何剩余数据可供读取。这是一个正常的情况,通常可以通过检查返回值来判断连接状态,进而进行相应的清理或处理。
接收新的连接
监听socket上有新的连接请求到达时,监听socket被标记为可读,可以调用accept函数来接收这个连接。
错误条件
socket上可能会发生错误,例如,连接被重置、超时、网络不可达等情况都可能导致socket状态不正常。当socket发生错误时,通常会将错误信息存储在一个内部状态中,使用getsockopt来读取和清除该错误。
socket可写条件
发送缓冲区可用
当发送缓冲区有可用字节,socket 会被标记为可写。
关闭连接
当对一个已关闭写通道的socket执行写操作时,通常会触发一个SIGPIPE信号。这种情况发生在尝试写入数据到一个已经关闭的连接时。
非阻塞模式
socket使用非阻塞connect连接成功或超时后,socket可写。
错误条件
socket上可能会发生错误,使用getsockopt来读取和清除该错误。
socket异常条件
接收带外数据
带外数据是一种特殊的传输方式,用于发送紧急数据。带外数据通常用于需要立即处理的重要信息,例如中断信号或控制信息。带外数据的接收被视为异常条件,需特别处理。
处理带外数据
server.cpp
#include <libgen.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
// 检查参数数量
if (argc < 3) {
printf("usage: %s ip_address, port number\n", basename(argv[0]));
return -1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
// 初始化地址结构
struct sockaddr_in address = {0};
address.sin_family = AF_INET;
if (inet_pton(AF_INET, ip, &address.sin_addr) <= 0) {
perror("Invalid address");
return -1;
}
address.sin_port = htons(port);
// 创建监听套接字
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
// 绑定地址
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(struct sockaddr_in));
assert(ret != -1);
// 开始监听
ret = listen(listenfd, 5);
assert(ret != -1);
struct sockaddr_in client_address = {0};
socklen_t client_addrlength = sizeof(client_address);
// 接受客户端连接
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
if (connfd < 0) {
printf("errno is: %d\n", errno);
close(listenfd);
return -1; // 添加返回以结束程序
}
char buffer[1024];
fd_set read_fds;
fd_set exception_fds;
FD_ZERO(&read_fds);
FD_ZERO(&exception_fds);
// 主循环,处理接收到的数据
while (1) {
memset(buffer, '\0', sizeof(buffer));
FD_SET(connfd, &read_fds);
FD_SET(connfd, &exception_fds);
ret = select(connfd + 1, &read_fds, NULL, &exception_fds, NULL);
if (ret < 0) {
printf("selection failure\n");
break;
}
// 处理正常数据
if (FD_ISSET(connfd, &read_fds)) {
ret = recv(connfd, buffer, sizeof(buffer) - 1, 0);
if (ret <= 0) {
break; // 处理关闭连接
}
printf("get %d bytes of normal data: %s\n", ret, buffer);
}
// 处理紧急数据
else if (FD_ISSET(connfd, &exception_fds)) {
ret = recv(connfd, buffer, sizeof(buffer) - 1, MSG_OOB);
if (ret <= 0) {
break; // 处理关闭连接
}
printf("get %d bytes of oob data: %s\n", ret, buffer);
}
}
close(connfd);
close(listenfd);
return 0;
}
client.cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 2222
#define SERVER_IP "192.168.32.162"
int main() {
int sockfd;
struct sockaddr_in server_addr;
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
return 1;
}
// 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
// 连接到服务器
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Connection failed");
close(sockfd);
return 1;
}
// 发送普通数据
const char *msg = "Hello, server!";
send(sockfd, msg, strlen(msg), 0);
// 发送带外数据
const char *urgent_msg = "Urgent data!";
send(sockfd, urgent_msg, strlen(urgent_msg), MSG_OOB);
// 关闭套接字
close(sockfd);
return 0;
}
运行结果
先运行server.cpp,再运行client.cpp,结果如下。