C++网络编程select模式的同步特性
C++网络编程select模式的同步特性
在C++网络编程中,select
模式是一种常用的I/O多路复用机制,它允许程序同时监控多个文件描述符(如套接字)的状态变化,以确定哪些描述符可以进行读、写或异常处理操作。select
模式具有一些同步特性,这些特性对于编写高效、稳定的网络应用程序至关重要。
1. select
函数概述
select
函数定义在<sys/select.h>
头文件中(在Windows下为<winsock2.h>
),其原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds:被监控的文件描述符集合中最大文件描述符的值加1。
- readfds:指向可读文件描述符集合的指针。
- writefds:指向可写文件描述符集合的指针。
- exceptfds:指向异常文件描述符集合的指针。
- timeout:设置
select
函数的超时时间。如果为NULL
,则select
函数将一直阻塞,直到有文件描述符状态发生变化;如果timeout
的两个成员均为0,则select
函数不阻塞,立即返回。
fd_set
是一个文件描述符集合类型,通过一组宏来操作:
// 清空文件描述符集合
void FD_ZERO(fd_set *set);
// 将指定文件描述符添加到集合中
void FD_SET(int fd, fd_set *set);
// 将指定文件描述符从集合中移除
void FD_CLR(int fd, fd_set *set);
// 检查指定文件描述符是否在集合中
int FD_ISSET(int fd, fd_set *set);
2. 同步特性分析
2.1 阻塞与非阻塞同步
- 阻塞模式:当
select
的timeout
参数为NULL
时,select
函数会一直阻塞,直到有文件描述符状态发生变化(可读、可写或有异常)。在阻塞期间,程序的执行流会停留在select
函数调用处,不会继续执行后续代码。这就实现了一种同步机制,确保只有在有可用I/O操作时,程序才会继续进行相应的读写操作。 例如,在一个简单的服务器程序中,可能会这样使用:
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <cstring>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
int server_fd, new_socket, valread;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[1024] = {0};
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定套接字
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
fd_set read_fds;
fd_set tmp_fds;
FD_ZERO(&read_fds);
FD_ZERO(&tmp_fds);
FD_SET(server_fd, &read_fds);
int activity;
while (true) {
tmp_fds = read_fds;
activity = select(server_fd + 1, &tmp_fds, NULL, NULL, NULL);
if (activity < 0) {
perror("select error");
break;
} else if (activity > 0) {
if (FD_ISSET(server_fd, &tmp_fds)) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
perror("accept");
continue;
}
FD_SET(new_socket, &read_fds);
std::cout << "New connection, socket fd is " << new_socket << " , ip is : " << inet_ntoa(address.sin_addr) << " , port : " << ntohs(address.sin_port) << std::endl;
}
for (int i = 0; i < MAX_CLIENTS; i++) {
if (FD_ISSET(i, &tmp_fds)) {
valread = read(i, buffer, 1024);
if (valread == 0) {
getpeername(i, (struct sockaddr *)&address, (socklen_t *)&addrlen);
std::cout << "Host disconnected , ip " << inet_ntoa(address.sin_addr) << " , port " << ntohs(address.sin_port) << std::endl;
close(i);
FD_CLR(i, &read_fds);
} else {
buffer[valread] = '\0';
std::cout << "Message from connected user: " << buffer << std::endl;
}
}
}
}
}
close(server_fd);
return 0;
}
在上述代码中,select
函数以阻塞模式运行,服务器会一直等待客户端连接或已有客户端发送数据。只有当有新连接或数据可读时,程序才会继续执行后续的accept
和read
操作。
- 非阻塞模式:当
select
的timeout
参数不为NULL
且tv_sec
和tv_usec
都为0时,select
函数不会阻塞,而是立即返回。这种方式允许程序在不阻塞的情况下检查文件描述符的状态,实现了一种异步查询的同步方式。例如:
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <cstring>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
int server_fd, new_socket, valread;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[1024] = {0};
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定套接字
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
fd_set read_fds;
fd_set tmp_fds;
FD_ZERO(&read_fds);
FD_ZERO(&tmp_fds);
FD_SET(server_fd, &read_fds);
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
int activity;
while (true) {
tmp_fds = read_fds;
activity = select(server_fd + 1, &tmp_fds, NULL, NULL, &timeout);
if (activity < 0) {
perror("select error");
break;
} else if (activity > 0) {
if (FD_ISSET(server_fd, &tmp_fds)) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
perror("accept");
continue;
}
FD_SET(new_socket, &read_fds);
std::cout << "New connection, socket fd is " << new_socket << " , ip is : " << inet_ntoa(address.sin_addr) << " , port : " << ntohs(address.sin_port) << std::endl;
}
for (int i = 0; i < MAX_CLIENTS; i++) {
if (FD_ISSET(i, &tmp_fds)) {
valread = read(i, buffer, 1024);
if (valread == 0) {
getpeername(i, (struct sockaddr *)&address, (socklen_t *)&addrlen);
std::cout << "Host disconnected , ip " << inet_ntoa(address.sin_addr) << " , port " << ntohs(address.sin_port) << std::endl;
close(i);
FD_CLR(i, &read_fds);
} else {
buffer[valread] = '\0';
std::cout << "Message from connected user: " << buffer << std::endl;
}
}
}
}
// 这里可以执行其他非I/O相关的任务
}
close(server_fd);
return 0;
}
在这个例子中,select
函数不会阻塞程序,程序可以在每次调用select
后立即执行其他任务,然后再次调用select
检查文件描述符状态。
2.2 多文件描述符同步
select
模式允许同时监控多个文件描述符,这对于处理多个客户端连接或多种类型的I/O操作(如套接字和管道)非常有用。通过将多个文件描述符添加到相应的fd_set
集合中,select
函数可以一次性检查这些描述符的状态。例如,一个服务器程序可能同时需要监听新的客户端连接(监听套接字)和读取已连接客户端发送的数据(客户端套接字):
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <cstring>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
int server_fd, new_socket, valread;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[1024] = {0};
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定套接字
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
fd_set read_fds;
fd_set tmp_fds;
FD_ZERO(&read_fds);
FD_ZERO(&tmp_fds);
FD_SET(server_fd, &read_fds);
int activity;
while (true) {
tmp_fds = read_fds;
activity = select(server_fd + 1, &tmp_fds, NULL, NULL, NULL);
if (activity < 0) {
perror("select error");
break;
} else if (activity > 0) {
if (FD_ISSET(server_fd, &tmp_fds)) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
perror("accept");
continue;
}
FD_SET(new_socket, &read_fds);
std::cout << "New connection, socket fd is " << new_socket << " , ip is : " << inet_ntoa(address.sin_addr) << " , port : " << ntohs(address.sin_port) << std::endl;
}
for (int i = 0; i < MAX_CLIENTS; i++) {
if (FD_ISSET(i, &tmp_fds)) {
valread = read(i, buffer, 1024);
if (valread == 0) {
getpeername(i, (struct sockaddr *)&address, (socklen_t *)&addrlen);
std::cout << "Host disconnected , ip " << inet_ntoa(address.sin_addr) << " , port " << ntohs(address.sin_port) << std::endl;
close(i);
FD_CLR(i, &read_fds);
} else {
buffer[valread] = '\0';
std::cout << "Message from connected user: " << buffer << std::endl;
}
}
}
}
}
close(server_fd);
return 0;
}
在这个服务器代码中,server_fd
用于监听新的客户端连接,而当有新连接建立后,新的客户端套接字new_socket
也被添加到read_fds
集合中。select
函数会同时检查server_fd
是否有新连接到来以及已连接客户端的套接字是否有数据可读,实现了多文件描述符之间的同步监控。
2.3 读写同步
select
函数可以通过readfds
和writefds
参数分别监控文件描述符的可读和可写状态,从而实现读写操作的同步。在网络编程中,这一点尤为重要,因为我们需要确保在发送数据之前,套接字是可写的,并且在接收数据之前,套接字是可读的。例如:
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <cstring>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
int server_fd, new_socket, valread, send_bytes;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[1024] = {0};
char response[1024] = "Hello from server!";
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定套接字
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
fd_set read_fds;
fd_set write_fds;
fd_set tmp_read_fds;
fd_set tmp_write_fds;
FD_ZERO(&read_fds);
FD_ZERO(&write_fds);
FD_ZERO(&tmp_read_fds);
FD_ZERO(&tmp_write_fds);
FD_SET(server_fd, &read_fds);
int activity;
while (true) {
tmp_read_fds = read_fds;
tmp_write_fds = write_fds;
activity = select(server_fd + 1, &tmp_read_fds, &tmp_write_fds, NULL, NULL);
if (activity < 0) {
perror("select error");
break;
} else if (activity > 0) {
if (FD_ISSET(server_fd, &tmp_read_fds)) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
perror("accept");
continue;
}
FD_SET(new_socket, &read_fds);
FD_SET(new_socket, &write_fds);
std::cout << "New connection, socket fd is " << new_socket << " , ip is : " << inet_ntoa(address.sin_addr) << " , port : " << ntohs(address.sin_port) << std::endl;
}
for (int i = 0; i < MAX_CLIENTS; i++) {
if (FD_ISSET(i, &tmp_read_fds)) {
valread = read(i, buffer, 1024);
if (valread == 0) {
getpeername(i, (struct sockaddr *)&address, (socklen_t *)&addrlen);
std::cout << "Host disconnected , ip " << inet_ntoa(address.sin_addr) << " , port " << ntohs(address.sin_port) << std::endl;
close(i);
FD_CLR(i, &read_fds);
FD_CLR(i, &write_fds);
} else {
buffer[valread] = '\0';
std::cout << "Message from connected user: " << buffer << std::endl;
if (FD_ISSET(i, &tmp_write_fds)) {
send_bytes = send(i, response, strlen(response), 0);
if (send_bytes < 0) {
perror("send error");
}
}
}
}
}
}
}
close(server_fd);
return 0;
}
在上述代码中,read_fds
集合用于监控套接字是否可读,当有数据可读时,读取数据并处理。同时,write_fds
集合用于监控套接字是否可写,当读取到客户端数据后,检查该客户端套接字是否可写,如果可写则向客户端发送响应数据,实现了读写操作的同步。
3. select
模式同步特性的优缺点
3.1 优点
- 跨平台支持:
select
模式在UNIX/Linux和Windows平台上都有相应的实现,这使得基于select
的网络应用程序具有较好的跨平台性。 - 简单易用:
select
函数的接口相对简单,对于初学者来说容易理解和掌握。通过操作fd_set
集合和设置timeout
参数,就可以实现基本的I/O多路复用和同步功能。 - 多文件描述符监控:能够同时监控多个文件描述符的状态变化,适用于处理多个客户端连接或多种类型I/O操作的场景,提高了程序的并发处理能力。
3.2 缺点
- 文件描述符数量限制:在一些系统中,
fd_set
集合所能容纳的文件描述符数量有限(例如在Linux系统中,默认情况下FD_SETSIZE
为1024)。当需要处理大量并发连接时,这可能成为瓶颈。 - 线性扫描效率低:
select
函数返回后,需要线性扫描fd_set
集合来确定哪些文件描述符状态发生了变化。随着文件描述符数量的增加,这种线性扫描的效率会显著降低。 - 内核用户空间数据拷贝开销:每次调用
select
函数时,都需要将用户空间的fd_set
集合拷贝到内核空间,返回时又要将内核空间的结果拷贝回用户空间,这增加了系统开销。
4. 应用场景
4.1 小型网络服务器
对于并发连接数较少的小型网络服务器,select
模式的简单性和跨平台性使其成为一个不错的选择。例如,一个简单的日志收集服务器,可能只需要处理少量客户端的连接并接收日志数据,使用select
模式可以轻松实现对这些连接的监控和数据读取。
4.2 嵌入式系统网络应用
在一些资源受限的嵌入式系统中,select
模式的低复杂度和跨平台特性使其适合用于实现网络功能。例如,一个智能家居设备需要与云端服务器进行通信,通过select
模式可以有效地管理设备与服务器之间的网络连接,在有限的资源下实现稳定的通信。
总结
select
模式在C++网络编程中提供了一种基本的I/O多路复用和同步机制。通过合理利用其阻塞与非阻塞特性、多文件描述符监控以及读写同步等功能,可以开发出高效、稳定的网络应用程序。然而,由于其自身的一些局限性,在处理大规模并发连接等场景时,可能需要考虑其他更高效的I/O多路复用机制,如epoll
(在Linux系统中)或IOCP
(在Windows系统中)。但对于许多小型网络应用和嵌入式系统网络开发,select
模式仍然具有重要的应用价值。在实际开发中,应根据具体的需求和场景,选择最合适的网络编程技术来实现高效、可靠的网络通信。