简介
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 8192typedef 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 handleFD_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!!!