WinSock I/O 模型 -- Select 模型-爱代码爱编程
简介
Select 模型是 WinSock 中最常见的 I/O 模型,这篇文章我们就来看看如何使用 Select api 来实现一个简单的 TCP 服务器.
API 基础
Select 模型依赖 WinSock API Select 来检查当前 Socket 是否可写或者可读。
使用这个 API 的优点是我们不需要使用阻塞的 Socket API (recv, send) 来等待 Socket 状态准备就绪,我们可以异步的检查 Socket 的状态来进行读数据或者写数据.
Select 方法的声明如下:
int WSAAPI select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
const timeval *timeout
);
其中:
nfds: 直接忽略即可,该参数的设计是为了兼容 Berkeley Socket 的实现
redfds: 返回值,当前可读的 socket 的集合
writefds: 返回值,当前可写的 socket 的集合
exceptfds:返回值,当前发生错误的 socket 的集合
返回值: 表示当前准备就绪的 socket 的数量。 这里的准备就绪包含 可读,可写,或者储出错的socket。如果返回 SOCKET_ERROR,表示发生错误,可以使用 WSAGetLastError 来获取具体的错误码。
fd_set
fd_set 是一个 socket 的集合,作为 select 方法的输入输出参数.
这里使用到的操作包括:
- FD_ZERO : 重置 fd_set
- FD_SET: 将 socket handle 添加到当前 fd_set
- FD_ISSET: 检查某个 socket handle 是否处于当前 fd_set
实现思路
- 创建一个 socket 作为监听 socket,并将该 socket 设置为非阻塞模式.
- 使用 select api 来非阻塞的简单该监听socket 是否有新连接进来。如果有,则调用 accept 来接收该 client socket
- 对于已经与客户段建立的连接,同样的设置为非阻塞模式,使用 select api 来检查该 socket 上是否有数据可读,或者该 socket 是否可写,以便往客户端发送数据。还需要检查socket 是否出错,本文的例子里忽略这点,思路是一样的。
- 注意,这里所有的操作都是非阻塞的。
解析来我们通过一个例子看看如何使用 Select.
实例
本文的例子可以直接拷贝运行。 读者如果不需要运行,直接注意加注释的代码段即可.
服务器实现
#include <WinSock2.h>
#include <Windows.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32")
#define DEFALT_PORT 8080
#define DATA_BUFFER 8192
typedef struct _SOCKET_CONTEXT {
SOCKET Socket;
WSABUF DataBuf;
OVERLAPPED Overlapped;
CHAR Buffer[DATA_BUFFER];
DWORD BytesSEND;
DWORD BytesRECV;
} SOCKET_CONTEXT, * LPSOCKET_CONTEXT;
BOOL CreateSocketContext(SOCKET s);
void FreeSocketContext(DWORD Index);
DWORD TotalSockets = 0;
LPSOCKET_CONTEXT SocketArray[FD_SETSIZE];
int main() {
INT Ret;
WSADATA wsaData;
SOCKET ListenSocket;
SOCKET AcceptSocket;
SOCKADDR_IN Addr;
ULONG NonBlock = 1;
FD_SET ReadSet;
FD_SET WriteSet;
DWORD Total;
DWORD Flags;
DWORD RecvBytes;
DWORD SentBytes;
DWORD i;
if ((Ret = WSAStartup(0x0202, &wsaData)) != 0) {
printf("WSAStartup failed with error %d\n", Ret);
WSACleanup();
return 1;
}
if ((ListenSocket = WSASocketW(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET) {
printf("WSASocket failed with error %d\n", WSAGetLastError());
return 1;
}
Addr.sin_family = AF_INET;
Addr.sin_addr.s_addr= htonl(INADDR_ANY);
Addr.sin_port = htons(DEFALT_PORT);
if (bind(ListenSocket, (PSOCKADDR) &Addr, sizeof(Addr)) == SOCKET_ERROR) {
printf("bind failed with error %d\n", WSAGetLastError());
return 1;
}
if (listen(ListenSocket, 10)) {
printf("listen failed with eror %d\n", WSAGetLastError());
return 1;
}
// 设置监听socket为异步模式
if (ioctlsocket(ListenSocket, FIONBIO, &NonBlock) == SOCKET_ERROR) {
printf("ioctlsocket failed with error %d\n", WSAGetLastError());
return 1;
}
while (TRUE) {
// 清空 ReadSet 和 WriteSet,我们将该集合中放入我们关心的 socket handle
FD_ZERO(&ReadSet);
FD_ZERO(&WriteSet);
// 将监听socket 放入 ReadSet, 以便当有新连接到来的时候,我们可以检查到该事件
FD_SET(ListenSocket, &ReadSet);
// 我们同时也关心已经建立的客户段连接的可读可写状态,以便我们从客户端接收数据或者写数据
// 这里一些小逻辑,直接忽略
for (i = 0; i < TotalSockets; i++) {
if (SocketArray[i]->BytesRECV > SocketArray[i]->BytesSEND) {
FD_SET(SocketArray[i]->Socket, &WriteSet);
} else {
FD_SET(SocketArray[i]->Socket, &ReadSet);
}
}
// 使用 select 检查当前 ReadSet 和 WriteSet 中的socket 是否有新的事件到来
if ((Total = select(0, &ReadSet, &WriteSet, NULL, NULL)) == SOCKET_ERROR) {
printf("select failed with error %d\n", WSAGetLastError());
return 1;
}
// 使用 FD_ISSET 判断监听 socket 是否可以读,也就是说有新的连接到来
// 如果有,调用 accept 来接收该新连接
if (FD_ISSET(ListenSocket, &ReadSet)) {
Total--;
if ((AcceptSocket = accept(ListenSocket, NULL, NULL)) != INVALID_SOCKET) {
NonBlock = 1;
if (ioctlsocket(AcceptSocket, FIONBIO, &NonBlock) == SOCKET_ERROR) {
printf("ioctlsocket failed with error %d\n", WSAGetLastError());
return 1;
}
if (CreateSocketContext(AcceptSocket) == FALSE) {
printf("CreateSocketContext failed");
return 1;
}
} else {
if (WSAGetLastError() != WSAEWOULDBLOCK) {
printf("accept failed with error %d\n", WSAGetLastError());
return 1;
} else {
printf("accept returns WSAEWOULDBLOCK\n");
}
}
}
// 接下来检查可读的客户段连接
for (i = 0; Total > 0 && i < TotalSockets; i++) {
LPSOCKET_CONTEXT Ctx = SocketArray[i];
if (FD_ISSET(Ctx->Socket, &ReadSet)) {
Total--;
Ctx->DataBuf.buf = Ctx->Buffer;
Ctx->DataBuf.len = DATA_BUFFER;
//当前 socket 可读,那么调用 WSARecv 从该 socket 读取数据
// 如果 WSARecv 返回 0, 是说该连接已经断开
Flags = 0;
if (WSARecv(Ctx->Socket, &(Ctx->DataBuf), 1, &RecvBytes, &Flags, NULL, NULL) == SOCKET_ERROR) {
if (WSAGetLastError() != WSAEWOULDBLOCK) {
printf("WSARecv failed with error %d\n", WSAGetLastError());
FreeSocketContext(i);
} else {
printf("WSARecv returns WSAEWOULDBLOCK");
}
continue;
} else {
Ctx->BytesRECV = RecvBytes;
// If zero bytes are received, this indicates the peer closed the connection.
if (RecvBytes == 0) {
FreeSocketContext(i);
continue;
} else {
printf("Recv %d bytes data from the socket %d\n", RecvBytes, Ctx->Socket);
}
}
}
// 接下来检查可写的客户段连接
if (FD_ISSET(Ctx->Socket, &WriteSet)) {
Total--;
Ctx->DataBuf.buf = Ctx->Buffer + Ctx->BytesSEND;
Ctx->DataBuf.len = Ctx->BytesRECV - Ctx->BytesSEND;
if (WSASend(Ctx->Socket, &(Ctx->DataBuf), 1, &SentBytes, 0, NULL, NULL) == SOCKET_ERROR) {
if (WSAGetLastError() != WSAEWOULDBLOCK) {
printf("WSASend failed with error %d\n", WSAGetLastError());
FreeSocketContext(i);
} else {
printf("WSASend returns WSAEWOULDBLOCK");
}
continue;
} else {
Ctx->BytesSEND += SentBytes;
if (Ctx->BytesSEND == Ctx->BytesRECV) {
Ctx->BytesSEND = 0;
Ctx->BytesRECV = 0;
}
}
}
}
}
}
BOOL CreateSocketContext(SOCKET s) {
LPSOCKET_CONTEXT Ctx;
printf("Accepted a new socket %d\n", s);
if ((Ctx = (LPSOCKET_CONTEXT) GlobalAlloc(GPTR, sizeof(SOCKET_CONTEXT))) == NULL) {
printf("GlobalAlloc() failed with error %d\n", GetLastError());
return FALSE;
}
Ctx->Socket = s;
Ctx->BytesSEND = 0;
Ctx->BytesRECV = 0;
SocketArray[TotalSockets] = Ctx;
TotalSockets++;
return TRUE;
}
void FreeSocketContext(DWORD Index) {
DWORD i;
LPSOCKET_CONTEXT Ctx = SocketArray[Index];
printf("Closing socket %d\n", Ctx->Socket);
closesocket(Ctx->Socket);
GlobalFree(Ctx);
for (i = Index; i < TotalSockets; i++) {
SocketArray[i] = SocketArray[i + 1];
}
TotalSockets--;
}
客户端实现
搭配该服务器,使用下面 client 实现进行测试。 这里仅仅做测试用,忽略了大部分的错误检查.
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#define DEFAULT_COUNT 20
#define DEFAULT_PORT 8080
#define DEFAULT_BUFFER 2048
#define DEFAULT_MESSAGE "\'A test message from client\'"
#pragma warning(disable:4996)
#pragma comment(lib, "ws2_32")
char szMessage[1024];
char szServer[128];
int main(int argc, char **argv) {
WSADATA wsaData;
SOCKET ClientSocket;
char szBuffer[DEFAULT_BUFFER];
int ret, i;
SOCKADDR_IN ServerAddr;
struct hostent *host = NULL;
WSAStartup(0x0202, &wsaData);
strcpy_s(szMessage, sizeof(szMessage), DEFAULT_MESSAGE);
strcpy_s(szServer, sizeof(szServer), "127.0.0.1");
ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = htons(DEFAULT_PORT);
ServerAddr.sin_addr.s_addr = inet_addr(szServer);
if (connect(ClientSocket, (struct sockaddr *) &ServerAddr, sizeof(ServerAddr)) == SOCKET_ERROR) {
printf("connect failed with error %d\n", WSAGetLastError());
return 1;
}
printf("Sending and receiving data if any...\n");
for(i = 0; i < DEFAULT_COUNT; i++) {
if ((ret = send(ClientSocket, szMessage, strlen(szMessage), 0)) == SOCKET_ERROR) {
printf("send() failed with error %d\n", WSAGetLastError());
break;
}
printf("send() is OK. Send %d bytes: %s\n", ret, szBuffer);
if ((ret = recv(ClientSocket, szBuffer, DEFAULT_BUFFER, 0)) == SOCKET_ERROR) {
printf("recv() failed with error %d\n", WSAGetLastError());
break;
}
if (ret == 0) {
printf("It is a graceful close!\n");
break;
}
szBuffer[ret] = '\0';
printf("recv() is OK. Received %d bytes: %s\n", ret, szBuffer);
}
if(closesocket(ClientSocket) == 0) {
printf("closesocket() is OK!\n");
} else {
printf("closesocket() failed with error %d\n", WSAGetLastError());
}
WSACleanup();
return 0;
}
END!!!
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/zhaoruixiang1111/article/details/109347452