精选 SELECT 模型:实现服务器端和客户端代码
作为现代计算机应用开发中最广泛使用的两个平台,服务器和客户端是解决客户和服务交互的重要基础。而实现这两个平台的代码,则必须是高度灵活和功能强大的,以满足不同应用的需要。为此,开发人员需要使用一种可靠,可拓展并且强大的模型。
SELECT模型是一种高效的模型,它是开发人员用于创建高度可扩展,可伸缩和底层 I/O 传输的关键工具。通过数据流聚合输入为套接字的事件,SELECT模型允许开发人员以非阻止或异步方式获取数据,并向客户端提供可用。
在本文中,我们将深入研究SELECT模型的原理,并演示如何使用它来构建服务器和客户端代码。
SELECT模型的原理
SELECT模型是Linux和Windows操作系统中很常见的系统调用。该模型通过一种集中的单线程事件循环机制来管理多个套接字连接。当SELECT模型开始监听套接字时,它会等待客户端发送数据,而不会阻塞服务列表中的事件。
选择模型涉及使用select系统调用来侦听I/O事件,如读取和写入。从而使一个I/O流被处理,而不需要使线程一直阻塞在该操作上,同时它还可进行轮询等操作。
SELECT模型是管理现代计算机应用程序中多个连接的一种有效方式。换句话说,它使一个进程能够同时监听多个套接字,实现跨网络连接的数据传输。
如下所示,SELECT模型的主要组件:
1. 文件描述符列表:用于存储套接字描述符。
2. 输入类型:用于存储套接字的输入类型(读取,写入,错误)。
3. 时间值:指示SELECT模型应该等待事件的时间戳,以避免频繁检查描述符表。
SELECT模型有优秀的伸缩性,因为其设计支持处理许多并发的网络连接。 它的工作原理如下:
SELECT模型将套接字添加到描述符列表中。然后,它轮流等待事件,并选择发生I/O事件的描述符。一旦有一个连接事件发生,接收数据的套接字就准备好处理数据。然后,开发人员可以执行必要的操作来处理数据。SELECT模型更新时间值并检查下一个事件。
使用SELECT模型构建服务器端代码
在这个案例中,我们将使用SELECT模型来开发一个简单的Web服务器。 我们声明必要的库:
“`
#include
#include
#include
#include
#include
#include
#include
#include
“`
声明主要变量
创建描述符表fd_set来保存所有Socket,并建立一个监听套接字serverfd:
“`
fd_set master_fds, fds;
int max_sockfd, clientfd [1024], max_client = -1, sockfd, len;
char buffer[1025];
struct sockaddr_in serveraddr, clientaddr;
“`
初始化描述符表fd_set
使用FD_ZERO初始化描述符表fd_set,并将serverfd添加到主描述符表中:
“`
FD_ZERO(&master_fds);
FD_SET(serverfd, &master_fds);
“`
监听套接字serverfd
使用listen函数将serverfd绑定在给定的IP地址和端口号上。
“`
serverfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(PORT);
serveraddr.sin_addr.s_addr = htons(INADDR_ANY);
bind(serverfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
listen(serverfd, 5);
“`
使用fd_set描述符来接受传入套接字连接
使用FD_ISSET函数检查用于接收连接的描述符和主描述符。然后,使用accept函数接受连接,将连接套接字添加到connfd中。在客户端连接列表中存储该客户端套接字。
“`
while (true) {
fds = master_fds;
if (select(max_sockfd + 1, &fds, NULL, NULL, NULL) == -1) {
perror(“select”);
exit(EXIT_FLURE);
}
for (sockfd = 0; sockfd
if (FD_ISSET(sockfd, &fds)) {
if (sockfd == serverfd) {
len = sizeof(clientaddr);
clientfd[max_client + 1] = accept(serverfd, (struct sockaddr *)&clientaddr, &len);
if(clientfd[max_client + 1] == -1) {
perror(“accept”);
} else {
FD_SET(clientfd[max_client + 1], &master_fds);
if (clientfd[max_client + 1] > max_sockfd) {
max_sockfd = clientfd[max_client + 1];
}
if (max_client
max_client = max_sockfd;
}
printf(“Accept connection from %s on port %d\n”, inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
}
} else {
if ((len = recv(sockfd, buffer, sizeof(buffer), 0))
if (len == 0) {
printf(“Connection closed with Client with server FD: %d\n”, sockfd);
} else {
perror(“recv”);
}
close(sockfd);
FD_CLR(sockfd, &master_fds);
if (sockfd == max_sockfd) {
while (FD_ISSET(max_sockfd, &master_fds) == false) {
max_sockfd -= 1;
}
}
} else {
buffer[len] = ‘\0’;
printf(“Received data from Client with Server FD: %s\n”, buffer);
}
}
}
}
}
“`
使用SELECT模型构建客户端代码
在这个案例中,我们将使用SELECT模型来开发一个简单的客户端,它连接到同一SERVER FD并发送字符串。
声明必要的库:
“`
#include
#include
#include
#include
#include
#include
#include
#include
#include
“`
声明变量
然后,声明需要的变量,包括套接字描述符clientfd和描述符列表fds:
“`
int sockfd, len;
struct sockaddr_in serveraddr;
char buffer[1025];
fd_set fds;
“`
建立套接字
通过socket函数建立TCP套接字。
“`
clientfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serveraddr, 0, sizeof(serveraddr));
“`
设置服务器地址
使用inet_pton函数将IP地址转换为网络字节序,并将其存储在服务器地址结构体中。
“`
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(ipaddr); //IP地址
serveraddr.sin_port = htons(portno); //端口号
“`
与服务器建立连接
使用connect函数与服务器建立连接。
“`
if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr))
perror(“Connect()”);
exit(EXIT_FLURE);
}
“`
发送消息
使用FD_ZERO,FD_SET和FD_ISSET等函数,通过SELECT模型检查描述符列表。在建立连接后,使用write函数将要发送的消息存储在buffer中,并将其发送给服务器。
“`
while(strncmp(buffer, “END”, strlen(buffer)-1) != 0) {
FD_ZERO(&fds);
FD_SET(clientfd, &fds);
FD_SET(STDIN_FILENO, &fds);
select(clientfd + 1, &fds, NULL, NULL, NULL);
if (FD_ISSET(STDIN_FILENO, &fds)) {
fgets(buffer, sizeof(buffer), stdin);
len = strlen(buffer);
if (write(clientfd, buffer, len) != len ) {
perror(“Write() error\n”);
exit(EXIT_FLURE);
}
}
}
“`
关闭连接
使用close函数关闭客户端套接字。
“`
printf(“Exiting client.\n”);
close(clientfd);
return 0;
“`
结论
相关问题拓展阅读:
- 如何将select模型改为异步选择模型
- 服务器端,在WSAEventSelect模型下,怎么判断收到的FD
如何将select模型改为异步选择模型
异步选择(WSAAsyncSelect)模型是一个有用的异步 I/O 模型。利用这个模型,应用程序可在一个套接字上,
接收以 Windows 消息为基础的网络事件通知。具体的做法是在建好一个套接字后,调用WSAAsyncSelect函数。
该模型的核心即是WSAAsyncSelect函数。
█ 要想使用 WSAAsyncSelect 模型,在应用程序中,首先必须用CreateWindow函数创建一个窗口,再为该窗口提供一个窗口例程函数(WinProc)。
█ WSAAsyncSelect 的函数原型如下:
view plain copy
int WSAAsyncSelect(
__inSOCKET s,
__inHWND hWnd,
__inunsigned int wMsg,
__inlong lEvent
);
● s 参数指定的是我们感兴趣的那个套接字。
● hWnd 参数指定一个窗口句柄,它对应于网络事件发生之后,想要收到通知消息的那个窗口。
● wMsg 参数指定在发生网络事件时,打算接收的消息。该消息会投递到由hWnd窗口句柄指定的那个窗口。
(通常,应用程序需要将这个消息设为比Windows的WM_USER大的一个值,避免网络窗口消息与系统预定义的标准窗口消息发生混淆与冲突)
● lEvent 参数指定一个位掩码,对应于一系列网络事件的组合,大多数应用程序通常感兴趣的网络事件类型包括:
FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE。当然,到底使用FD_ACCEPT,还是使用FD_CONNECT类型,
要取决于应用程序的身份是客户端,还是服务器。如应用程序同时对多个网络事件有兴趣,只需对各种类型执行一次简单的按位OR(或)运算,
然后将它们分配给lEvent就可以了,例如:
WSAAsyncSeltct(s, hwnd, WM_SOCKET, FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE);
解释说明:我们的应用程序以后便可在套接字s上,接收到有关连接、发送、接收以及套接字关闭这一系列网络事件的通知。
█ 注意 ①:
多个事件务必在套接字上一次注册!
另外还要注意的是,一旦在某个套接字上允许了事件通知,那么以后除非明确调用closesocket命令,
或者由应用程序针对那个套接字调用了WSAAsyncSelect,从而更改了注册的网络事件类型,否则的话,
事件通知会永远有效!若将lEvent参数设为0,效果相当于停止在套接字上进行的所有网络事件通知。
█ 注意 ②:
若应用程序针对一个套接字调用了WSAAsyncSelect,那么套接字的模式会从“锁定”变成“非锁定”。
这样一来,如果调用了像WSARecv这样的Winsock函数,但当时却并没有数据可用,那么必然会造成调用的失败,并返回WSAEWOULDBLOCK错误。
为防止这一点,歼迟应用程序应依赖于由WSAAsyncSelect的uMsg参数指定的用户自定义窗口消息,来判断网络事件类型何时在套接字上发生;而不应盲目地进行调用。
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接完成的通知
FD_CLOSE 应用程序想接收与套接字关闭的通知
█ 应用程序在一个套接字上成功调用了闭改御WSAAsyncSelect之后,会在与hWnd窗口句柄对应的窗口例程中,以Windows消息的形式,接收网轿岩络事件通知。
窗口例程通常定义如下:
view plain copy
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
● hWnd 参数指定一个窗口的句柄,对窗口例程的调用正是由那个窗口发出的。
● uMsg 参数指定需要对哪些消息进行处理。这里我们感兴趣的是WSAAsyncSelect调用中定义的消息。
● wParam 参数指定在其上面发生了一个网络事件的套接字。假若同时为这个窗口例程分配了多个套接字,这个参数的重要性便显示出来了。
● lParam参数中,包含了两方面重要的信息。其中, lParam的低字(低位字)指定了已经发生的网络事件,而lParam的高字(高位字)包含了可能出现的任何错误代码。
█ 步骤:网络事件消息抵达一个窗口例程后,应用程序首先应检查lParam的高字位,以判断是否在网络错误。
这里有一个特殊的宏: WSAGETSELECTERROR,可用它返回高字位包含的错误信息。
若应用程序发现套接字上没有产生任何错误,接着便应调查到底是哪个网络事件类型,具体的做法便是读取lParam低字位的内容。
此时可使用另一个特殊的宏:WSAGETSELECTEVENT,用它返回lParam的低字部分。
█ 注意 ③:应用程序如何对 FD_WRITE 事件通知进行处理。
只有在三种条件下,才会发出 FD_WRITE 通知:
■ 使用 connect 或 WSAConnect,一个套接字首次建立了连接。
■ 使用 accept 或 WSAAccept,套接字被接受以后。
■ 若 send、WSASend、sendto 或 WSASendTo 操作失败,返回了 WSAEWOULDBLOCK 错误,而且缓冲区的空间变得可用。
因此,作为一个应用程序,自收到首条 FD_WRITE 消息开始,便应认为自己必然能在一个套接字上发出数据,
直至一个send、WSASend、sendto 或 WSASendTo 返回套接字错误 WSAEWOULDBLOCK。
经过了这样的失败以后,要再用另一条 FD_WRITE 通知应用程序再次发送数据。
用例:
view plain copy
WSAAsyncSelect(pThis->m_SockListen, pThis->GetSafeHwnd(), WM_SOCKET, FD_ACCEPT | FD_CLOSE);//WM_SOCKET为自定义消息#define WM_SOCKET WM_USER+100
view plain copy
//override窗口过程函数
LRESULT CServerDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_SYSCOMMAND:
if (wParam == SC_CLOSE) {
closesocket(m_SockClient);
closesocket(m_SockListen);
WSACleanup();
}
break;
case WM_SOCKET:
if (WSAGETSELECTERROR(lParam)) {
–m_ClientNums;
closesocket(wParam);
break;
}
switch(WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT:
{
if (m_ClientNums >= 1) {
break;
}
m_SockClient = accept(wParam, NULL, NULL);
WSAAsyncSelect(m_SockClient, m_hWnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CLOSE);
++m_ClientNums;
break;
}
case FD_READ:
{
TCHAR szBuf = {0};
recv(wParam, (char *)szBuf, MAX_BUF_SIZE, 0);
ShowMsg(szBuf);
break;
}
case FD_WRITE:
wParam = wParam;
break;
case FD_CLOSE:
–m_ClientNums;
closesocket(wParam);
break;
}
default:break;
}
return CDialog::WindowProc(message, wParam, lParam);
服务器端,在WSAEventSelect模型下,怎么判断收到的FD
——解决方案——
WSAWaitForMultipleEvents
——解决方案——
TCP客户端用WSAWaitForMultipleEvents等待所有数据连接的FD_READ、FD_CLOSE
TCP服务器端用WSAWaitForMultipleEvents等待监听的FD_ACCEPT和数据连接的FD_READ、FD_CLOSE
不管是客户端还是服塌弯务器端,每建立一个SOCKET就WSACreateEvent一个EVENT并WSAEventSelect,在WSAWaitForMultipleEvents里面等这些EVENT,EVENT与SOCKET一一对应,更具WSAWaitForMultipleEvents的返回值来确定是哪个SOCKET
——解决方案——
你要支耐衫陵持更昌戚多的客户端就用完成端口。。
——解决方案——
WSAWaitForMultipleEvents对每64个Event分组进行查询的方式达到支持> 64客户端的支持
缺点就是效率比完成端口低
关于select模型服务器端和客户端代码的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。