TCP/IP网络编程(6)
1. IO复用
并发服务器的实现方法
在网络程序中,数据通信时间比CPU运算时间占比更大,因此,采用并发的形式向多个客户端提供服务是一种有效利用CPU的方式。并发服务器的主要实现模型及方法如下所示:
- 多进程服务器,通过常见多个进程提供服务
- 多路复用服务器,通过捆绑并统一管理IO对象提供服务
- 多线程服务器,通过生成与客户端等量的线程提供服务
基于IO复用的服务器端:
复用的概念:在一个通信频道中传输多个数据(信号)的技术。典型的例子:通信领域时分复用技术以及频分复用技术,在同一条通信线路上,分时传递不同的信号,提高信号的线路的利用率,或者是叠加不同频率的信号。
select函数实现并发服务器
select函数的功能和调用顺序:使用select函数可以将多个文件描述符集中到一起统一监视:
- 是否存在套接字接收数据
- 无需阻塞传输数据的套接字有哪些
- 哪些套接字发生了异常
1. 设置文件描述符
利用select函数可以监视多个文件描述符(套接字),此时需要将文件描述符集中到一起,集中时需要按照监视项(接收,传输,异常)进行区分,可使用fd_set数组执行此操作。fd_set是存有0和1的位数组。
fd_i表示文件描述符i所在的位置,如果对应的位置的值为1,则表示该文件描述符是监视对象,如上图中的文件描述符fd0和fd2就是监视对象。由于fd_set是位数组,直接将文件描述符的数字值注册到fd_set数组中较为繁琐,因此可使用已有的宏定义进行注册等一系列操作:
FD_ZERO(fd_set* fdset); // 将位数组fdset的所有位初始化为0 FD_SET(int fd, fd_set* fdset); // 注册文件描述符fd FD_CLR(int fd, fd_set* fdset); // 清楚文件描述符fd的信息 FD_ISSET(int fd, fd_set* fdset); // 若位数组fdset中包含文件描述符的信息,则返回真
上述函数可以用于验证select函数的调用结果:
2. 调用select函数
#include <sys/select.h> #include <sys/time.h> /* select函数 input: int maxfd: 监视对象文件描述符数量 fd_set* readset: 用于注册 读取数据的 文件描述符对象 fd_set* writeset:用于注册 传输无阻塞数据的 文件描述符对象 fd_set* exceptset:若需要关注文件描述符是否发生异常,则将文件描述符注册到此数组中 const struct timeval *timeout:用于设置select函数的超时时间 return: -1 : select函数发生错误 0 :select函数超时 >0 : 发生事件的文件描述符数量 (可读,可写,异常) */ int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval *timeout);
2.1 文件描述符的监视范围:
文件描述符的监视范围与select函数的第一个参数有关,select函数要求通过第一个参数传递监视对象文件描述符的数量,因此,需要得到注册在fd_set变量中的文件描述符的数量。每次新建文件描述符的时候,其值都会加1,因此仅仅需要将最大的文件描述符的值加1,传递给select函数的第一个参数即可。(加1是因为文件描述符从0开始)
2.2 select函数的超时时间:
select函数的超时时间与select函数的最后一个参数有关,本来select函数只有在监视的文件描述符发生变化的时候才会返回,否则就会一直处于阻塞状态,如果设置了超时时间,则select函数在超过超时时间之后,如果未发生变化,也会返回,且返回值位0。如果不想设置超时时间,则最后一个参数传递NULL即可。
2.2 调用select函数后fd_set的变化:
select函数调用完成后,向其传递的fd_set数组将可能发生变化,原来所有为1的位置均会变为0,但是如果文件描述符发生了变化,则其对应位置的值仍然为1,因此可以认为值为1的位置上的文件描述符发生了变化。
3.基于windows平台的select函数
windows平台上的select函数与Linux下的基本一致,区别是,select函数的第一个参数是为了与Linux平台保持兼容性二添加的,并没有特殊的含义。
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfsd, const struct timeval* timeout); // timeval定义 typedef struct timeval { long tv_sec; // seconds long tv_usec; // microseconds }
windows中fd_set不像Linux中那样采用位数组,而实定义了一个结构体:
typedef struct fd_set { u_int fd_count; SOCKET fd_array[FD_SIZE]; }
其中,fd_count用于保存套接字句柄数量,fd_array用于保存套接字句柄。Window下这样定义的原因:Linux文件描述符从0开始递增,因此可以找出当前的文件描述符的数量和最后生成的文件描述符的关系。但是Windows套接字句柄并非从0开始,而且句柄的数值之间无规律可循,因此需要保存句柄数量以及句柄数组。但是处理fd_set的四个宏定义的名称,方法,以及功能与Linux完全相同,完全具备与Linux的兼容性。
4. select函数的应用示例(回声服务器)
服务端:
// echoserver.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <stdio.h> #include <WinSock2.h> #pragma comment(lib, "Ws2_32.lib") // 宏定义 #define BUFF_SIZE 1*1024 // 缓冲区大小 #define ADDR_SIZE sizeof(SOCKADDR_IN) #define SOCK_PORT 13000 // error handler void error_handle(char* message) { printf("%s\n", message); system("pause"); exit(1); } int main() { WSADATA wsadata; SOCKET hServerSocket; // 服务端socket监听 SOCKET hClientSocket; // 地址 SOCKADDR_IN serverAddr; SOCKADDR_IN clientAddr; TIMEVAL timeout; // 超时时间 fd_set reads, copyReads; char buffer[BUFF_SIZE]; // 定义缓冲区 int addr_szie = sizeof(SOCKADDR_IN); if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) { error_handle("WsaStratUp() Failed"); } // 初始化服务端socket hServerSocket = socket(PF_INET, SOCK_STREAM, 0); // 初始化服务端地址 memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(SOCK_PORT); if (bind(hServerSocket, (SOCKADDR*)&serverAddr, ADDR_SIZE) == SOCKET_ERROR) { error_handle("Failed to bind the server socket."); } // 开始监听 if (listen(hServerSocket, 5) == SOCKET_ERROR) { error_handle("Failed to start the server socket listen"); } FD_ZERO(&reads); FD_SET(hServerSocket, &reads); printf("Waiting for connect from client...\n"); while (true) { copyReads = reads; // 设置超时时间 每次select之后,timeout中的值将被替换为超时前剩余的时间,因此在每一次select之前都需要重新设置超时时间 timeout.tv_sec = 5; timeout.tv_usec = 5000; int res = select(0, ©Reads, NULL, NULL, &timeout); if (res < 0) { // 发生异常 清除socket,处理异常并退出 closesocket(hServerSocket); WSACleanup(); error_handle("Error occurs when slect."); } else if (res == 0) { // 返回值为0表示超时 printf("The select timeout.\n"); continue; } else { for (int i=0; i<reads.fd_count; i++) { // 在select之后,fd_set中发生变化的套接字句柄所在位置保持为1,未发生变化的全部为0 if (FD_ISSET(reads.fd_array[i], ©Reads)) // 有套接字发生变化 { if (reads.fd_array[i] == hServerSocket) // 如果是服务端监听socket发生变化 { // 接收新的连接 hClientSocket = accept(hServerSocket, (SOCKADDR*)&clientAddr, &addr_szie); // 将对应的客户端socket加入到fd_set中 FD_SET(hClientSocket, &reads); printf("New connect client address:%s , port:%d.\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); } else { // 客户端套接字发生变化,接收数据 hClientSocket = reads.fd_array[i]; memset(buffer, 0, BUFF_SIZE); int strLen = recv(hClientSocket, buffer, BUFF_SIZE, 0); if (strLen <= 0) { // close the request FD_CLR(hClientSocket, &reads); closesocket(hClientSocket); printf("Closed socket %d\n", hClientSocket); } else { // 将数据发送回客户端 send(hClientSocket, buffer, strLen, 0); } } } } } } closesocket(hServerSocket); WSACleanup(); return 0; }
客户端测试程序:
// clentdemon.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <stdio.h> #include <WinSock2.h> #pragma comment(lib, "Ws2_32.lib") // 宏定义 #define BUFF_SIZE 1*1024 #define ADDR_SIZE sizeof(SOCKADDR_IN) #define SERV_ADDR "127.0.0.1" // 服务端地址 #define SOCK_PORT 13000 // 端口 // error handler void error_handle(char* message) { printf("%s\n", message); system("pause"); exit(1); } int main() { char buffer[BUFF_SIZE]; // 缓冲区 WSADATA wsaData; SOCKET hServerSocket; // 服务端socket SOCKADDR_IN serverAddr; // 服务端地址 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { error_handle("Failed when wsaStartup!"); } memset(buffer, 0, BUFF_SIZE); memset(&serverAddr, 0, ADDR_SIZE); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = inet_addr(SERV_ADDR); serverAddr.sin_port = htons(SOCK_PORT); hServerSocket = socket(PF_INET, SOCK_STREAM, 0); // 连接客户端 if (connect(hServerSocket, (SOCKADDR*)&serverAddr, ADDR_SIZE) == SOCKET_ERROR) { error_handle("Failed to connect to server."); } printf("Successfully connect to server!\n"); char* quitFlag1 = "q"; char* quitFlag2 = "Q"; while (true) { memset(buffer, 0, BUFF_SIZE); printf("Please input the message to send: "); scanf("%s", buffer); if (strlen(buffer) > 0) { if (strncmp(buffer, quitFlag1, 1)==0 || strncmp(buffer, quitFlag2, 1)==0) { printf("Quit the client routine.\n"); break; } else { send(hServerSocket, buffer, BUFF_SIZE, 0); // 发送消息 } } memset(buffer, 0, BUFF_SIZE); int strLen = recv(hServerSocket, buffer, BUFF_SIZE, 0); if (strLen > 0) { printf("Receive from server: %s\n", buffer); } else continue; } closesocket(hServerSocket); WSACleanup(); return 0; }
测试效果:
服务端效果:
客户端测试效果:
写一个多线程的客户端测试程序(使用c++11标准库多线程实现)
// clentdemon.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <stdio.h> #include <WinSock2.h> #include <thread> #include <mutex> #include <vector> #include <chrono> using namespace std; #pragma comment(lib, "Ws2_32.lib") // 宏定义 #define BUFF_SIZE 1*1024 #define ADDR_SIZE sizeof(SOCKADDR_IN) #define SERV_ADDR "127.0.0.1" // 服务端地址 #define SOCK_PORT 13000 // 端口 // error handler void error_handle(char* message) { printf("%s\n", message); system("pause"); exit(1); } class client { public: client(int threadNum) { m_threadNum = threadNum; init(); } ~client() { for (int i=0; i<m_threadArray.size(); i++) { thread* pThread = m_threadArray.at(i); if (pThread) { delete pThread; pThread = NULL; } } m_threadArray.clear(); } void exec() { for (int i=0; i<m_threadNum; ++i) { thread* pThread = m_threadArray.at(i); /* joinable()函数是一个布尔类型的函数,他会返回一个布尔值来表示当前的线程是否是可执行线程(能被join或者detach),因为相同的线程不能join两次, 也不能join完再detach,同理也不能detach完了再去join,所以joinable函数就是用来判断当前这个线程是否可以joinable的。通常不能被joinable有 以下几种情况: 1)由thread的缺省构造函数而造成的(thread()没有参数)。 2)该thread被move过(包括move构造和move赋值)。 3)该线程被join或者detach过。 */ if (!pThread || !pThread->joinable()) { printf("Failed to start thread %d.\n", this_thread::get_id()); continue; } pThread->detach(); printf("Thread %d has been started!\n", this_thread::get_id()); /* join() 表示主线程需要等待子线程运行结束回收掉子线程的资源后,再往下运行 detach() 表示子线程不需要等待主线程,detach的作用就是将主线程与子线程分离, 主线程将不再等待子线程的运行,也就是说两个线程同时运行,当主线程结束的时候, 进程结束,此时子线程也会被回收。 */ } } bool isAllThreadFinished() { return (m_threadNum == m_finishedThreadCnt); } protected: void init() { m_finishedThreadCnt = 0; // 初始化所有线程 for (int i=0; i < m_threadNum; ++i) { thread* pThread = new thread(&client::funct, this); // 注意类成员作为thread函数的时候的特殊写法 m_threadArray.push_back(pThread); } } void funct() { char buffer[BUFF_SIZE]; // 缓冲区 WSADATA wsaData; SOCKET hServerSocket; // 服务端socket SOCKADDR_IN serverAddr; // 服务端地址 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("Failed when wsaStartup in thread %d!", this_thread::get_id()); //线程结束标记 m_threadCntMutex.lock(); m_finishedThreadCnt++; m_threadCntMutex.unlock(); return; } memset(buffer, 0, BUFF_SIZE); memset(&serverAddr, 0, ADDR_SIZE); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = inet_addr(SERV_ADDR); serverAddr.sin_port = htons(SOCK_PORT); hServerSocket = socket(PF_INET, SOCK_STREAM, 0); // 连接客户端 if (connect(hServerSocket, (SOCKADDR*)&serverAddr, ADDR_SIZE) == SOCKET_ERROR) { // error_handle("Failed to connect to server."); printf("Thread %d Failed to connect to the server.\n", this_thread::get_id()); closesocket(hServerSocket); WSACleanup(); //线程结束标记 m_threadCntMutex.lock(); m_finishedThreadCnt++; m_threadCntMutex.unlock(); return; } printf("Socket in thread %d successfully connected to server!\n", this_thread::get_id()); char* quitFlag1 = "q"; char* quitFlag2 = "Q"; int count = 0; while (true) { chrono::seconds sec(2); this_thread::sleep_for(sec); count++; sprintf(buffer, "Hello world info from thread %d.", this_thread::get_id()); if (count > 5) { printf("The socket in thread %d will exit, bye bye.\n", this_thread::get_id()); break; } if (strlen(buffer) > 0) { if (strncmp(buffer, quitFlag1, 1) == 0 || strncmp(buffer, quitFlag2, 1) == 0) { printf("Quit the client routine.\n"); break; } else { send(hServerSocket, buffer, BUFF_SIZE, 0); // 发送消息 } } memset(buffer, 0, BUFF_SIZE); int strLen = recv(hServerSocket, buffer, BUFF_SIZE, 0); if (strLen > 0) { printf("Receive from server: %s\n", buffer); } else continue; } closesocket(hServerSocket); WSACleanup(); printf("Thread %d successfully exit.\n", this_thread::get_id()); //线程结束标记 m_threadCntMutex.lock(); m_finishedThreadCnt++; m_threadCntMutex.unlock(); return; } private: int m_threadNum; int m_finishedThreadCnt; mutex m_threadCntMutex; vector<thread*> m_threadArray; }; int main() { client c(5); // 客户端创建5个线程 c.exec(); /* 这里必须要判断所有线程是否都已经结束,如果不判断,会导致socket线程还在运行, 但是主线程已经结束,主线程结束,detach或者join的所有子线程也会被回收,导致 线程运行结果不正确。 */ while (!c.isAllThreadFinished()); // 等待所有线程结束 return 0; }
多线程运行结果:
服务端:
多线程客户端运行结果
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)