目录
- 1,TCP通信原理
- [2,select 函数](#2,select 函数)
-
- [2.1,select 函数 fd_set集合](#2.1,select 函数 fd_set集合)
- 2.2,使用select来处理并发
1,TCP通信原理

如上图所示,
左侧有两大块内存,一个是负责监听客户端连接的部分,另一个是负责和客户端通信的部分。
右侧的内存是客户端的部分,用于连接服务端,以及和服务端进行通信。
服务端需要用两个文件描述符去操作这两块内存。一个是用于监听的文件描述符,一个是用于通信的文件描述符。
在服务端调用 accept() 函数其实就是监听客户端有没有对服务端进行连接。如果没有连接就会一直阻塞。
2,select 函数
select函数的作用在于去检测一个 线性表中所存储的所有文件描述符的状态。用于处理服务端与多个客户端通信时使用。
#include<sys/select.h>
struct timeval{
time_t tv_sec; //seconds 秒
suseconds_t tv_usec; //microseconds 微秒
};
int select(int nfds , fd_set* readfds , fd_set* writefds , fd_set* exceptfds ,
struct timeval* timeout );
//函数返回值
检测的符合条件的文件描述符的个数。
//函数参数:
nfds: 委托内核检测的这三个集合中最大的文件描述符 + 1。
内核需要线性便利这些集合中的文件描述符,这个值是循环结束的条件
在windows中这个参数是无效的,指定为 -1 即可。
readfds: 文件描述符的读集合,
内核只检测这个集合中文件描述符对应的读缓冲区
传入传出参数,读集合一般情况下都是需要检测的,这样才知道通过哪个文件描述符接收数据
writefds: 文件描述符的写集合,
内核只检测这个集合中文件描述符对应的写缓冲区
传入传出参数,如果不需要这个参数可以指定为NULL。
exceptfds: 异常集合,用于查看用于检测的这些读写文件描述符的集合有无异常。
timeout: 检测时长,超过检测时长,就会返回。操作时长就是秒加上微妙。
第一章我们了解了TCP服务端通信需要两种文件描述符,实际在通信的过程当中一个服务端可能需要与多个客户端通信,所以需要 N 个用于通信的文件描述符和 1 个用于监听的文件描述符。
所以 select 函数需要检测 N + 1 个文件描述符。
但实际上这个被用于检测的线性表能装的文件描述符的最大数量是 1024 个 。
如下图所示为一个文件描述符表,此表为内核中的表。

我们通过select函数,将一系列的文件描述符集合传递拷贝到内核中,然后内核会生成一个线性表用于存储,然后我们去循环遍历这个线性表,当检测到有文件描述符状态发生变化,就会传出对应的三个集合。
2.1,select 函数 fd_set集合
占内存 1024 个标志位。一个标志位对应一个文件描述符。占用128个字节。

系统提供了一些操作函数,让我们把文件描述符放入 fd_set 集合中,除此之外还支持其他操作。
如下:
//1,将文件描述符 fd 从set集合中删除 == 将fd对应的标志位设置为0
void FD_CLR(int fd,fd_set* set);
//2,判断文件描述符 fd 是否在 set 集合中 == 读一下fd对应的标志位到底是 0 还是 1
int FD_ISSET(int fd,fd_set* set);
//3,将文件描述符fd添加到 set 集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd,fd_set* set);
//4,将set集合中,所有文件描述符对应的标志位设置为0,集合中没有添加任何文件描述符
void FD_ZERO(fd_set* set);
2.2,使用select来处理并发


select函数实现TCP并发处理
javascript
#include <iostream>
#include <vector>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/time.h>
#include <cstring>
#define PORT 8080
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
int main()
{
//1,获取监听文件描述符
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket < 0) {
std::cerr << "Socket creation failed" << std::endl;
return -1;
}
//2,绑定IP和端口
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(PORT);
if (bind(serverSocket, reinterpret_cast<sockaddr*>(&serverAddr), sizeof(serverAddr)) < 0) {
std::cerr << "Bind failed" << std::endl;
close(serverSocket);
return -1;
}
if (listen(serverSocket, MAX_CLIENTS) < 0) {
std::cerr << "Listen failed" << std::endl;
close(serverSocket);
return -1;
}
//3,将监听的文件描述符添加到 fd_set 集合中
std::vector<int> clientSockets(MAX_CLIENTS, -1);
fd_set readFds;
fd_set tmpFds;
FD_ZERO(&readFds);
FD_SET(serverSocket, &readFds);
//4,用select进行检测
while (true) {
tmpFds = readFds;
int activity = select(serverSocket + 1, &tmpFds, nullptr, nullptr, nullptr);
if (activity < 0)
{
std::cerr << "Select error" << std::endl;
break;
}
else if (activity > 0)
{
//4.1 通过 accept() 获取客户端文件描述符,并将客户端文件描述符添加到 clientSockets 数组中。
if (FD_ISSET(serverSocket, &tmpFds))
{
sockaddr_in clientAddr;
socklen_t clientAddrLen = sizeof(clientAddr);
int newSocket = accept(serverSocket, reinterpret_cast<sockaddr*>(&clientAddr), &clientAddrLen);
if (newSocket < 0)
{
std::cerr << "Accept failed" << std::endl;
continue;
}
for (size_t i = 0; i < clientSockets.size(); ++i)
{
if (clientSockets[i] == -1)
{
clientSockets[i] = newSocket;
FD_SET(newSocket, &readFds);
break;
}
}
}
//4.2 遍历 clientSockets 数组,查看所有通信的文件描述符的状态,并进行通信处理。
for (size_t i = 0; i < clientSockets.size(); ++i)
{
if (clientSockets[i] != -1 && FD_ISSET(clientSockets[i], &tmpFds))
{
char buffer[BUFFER_SIZE];
int valread = read(clientSockets[i], buffer, BUFFER_SIZE);
if (valread <= 0)
{
close(clientSockets[i]);
FD_CLR(clientSockets[i], &readFds);
clientSockets[i] = -1;
} else
{
buffer[valread] = '\0';
std::cout << "Received from client " << i << ": " << buffer << std::endl;
}
}
}
}
}
close(serverSocket);
for (int sock : clientSockets)
{
if (sock != -1)
{
close(sock);
}
}
return 0;
}