简介

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

实现思路

  1. 创建一个 socket 作为监听 socket,并将该 socket 设置为非阻塞模式.
  2. 使用 select api 来非阻塞的简单该监听socket 是否有新连接进来。如果有,则调用 accept 来接收该 client socket
  3. 对于已经与客户段建立的连接,同样的设置为非阻塞模式,使用 select api 来检查该 socket 上是否有数据可读,或者该 socket 是否可写,以便往客户端发送数据。还需要检查socket 是否出错,本文的例子里忽略这点,思路是一样的。
  4. 注意,这里所有的操作都是非阻塞的。

解析来我们通过一个例子看看如何使用 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!!!

WinSock I/O 模型 -- Select 模型相关推荐

  1. 网络基础 select模型

    网络基础 select 模型 (一)select简介 select模型为五种IO模型中的一种(I/O多路复用模型).该模型的函数包括select.poll.epoll等函数.这个函数能够允许指示内核等 ...

  2. SOCKET学习第三阶段(SELECT模型)

    /* 2009-11-18 20:57:05 AUTHOR:BY.Feng */ SELECT套接字模型的学习 Winsock提供了五种类型的套接字I/O 模型,可让Winsock应用程序 对I/O ...

  3. WinSock学习笔记3:Select模型

    WinSock学习笔记3:Select模型 unit Unit1; interface uses   Windows, Messages, SysUtils, Variants, Classes, G ...

  4. winsock select模型实现

    select模型的原理是在指定的socket 的数组中轮询的采集是否可写.可读.有异常的信息,然后将可以操做的套接字 保存在指定的数组中. // selectsocket.cpp : 定义控制台应用程 ...

  5. java socket编程 select_windows socket编程select模型使用

    int select( int nfds,            //忽略 fd_ser* readfds,    //指向一个套接字集合,用来检测其可读性 fd_set* writefds,   / ...

  6. socket select模型

    由于socket recv()方法是堵塞式的,当多个客户端连接服务器时,其中一个socket的recv调用时,会产生堵塞,使其他连接不能继续. 如果想改变这种一直等下去的焦急状态,可以多线程来实现(不 ...

  7. 转:socket select模型示例

    // selectSocketMode.cpp : Defines the entry point for the console application. // #include "std ...

  8. Windows环境下IOCP和SELECT模型性能比较

    在大量客户端连接的情况下,IOCP模型应该是具有先天优势的,首先是每次调用时不需要传入socket列表,其次是他在通知时就已经完成了IO操作,节省了系统调用. 道理是这么个道理,然而在实际应用过程当中 ...

  9. 详细解析SELECT模型

    先看一下下面的这句代码: int iResult = recv(s, buffer,1024); 这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把 ...

最新文章

  1. Python使用sklearn和statsmodels构建多元线性回归模型(Multiple Linear Regression)并解读
  2. c语言Winmain 错误,C语言编译错误 undefined reference to `WinMain@16'
  3. Python学习笔记__8章错误、调试和测试__8.1章错误处理
  4. 算法描述怎么写_管件材料描述怎么写
  5. LeetCode 907. 子数组的最小值之和(单调栈)
  6. vue循环出来列表里面的列表点击click事件只对当前列表有效;
  7. bzoj3612 平衡 (dp)
  8. jw player 5去掉share,info,embed页面
  9. B. Suffix Structures 模拟吧,情况比较多要想周全
  10. MySQL存储过程实例
  11. 转载:我的外语学习历程(如何学会十门外语)
  12. 第6周作业2-IF语句大显身手之成绩判断(网络131黄宇倩)
  13. Smart3D(ContextCapture)跑三维到底要啥配置?!40000元来组建建模集群
  14. 强化学习之Q学习与SARSA
  15. 网站更换服务器对于SEO有哪些影响?
  16. AMESim2020.1仿真编译失败解决方法之一
  17. cesesesese
  18. 日志20140704~1226
  19. How to Flash a ROM to Your Android Phone
  20. STM32使用硬件定时器6制作us级别延时函数

热门文章

  1. STM32F105 PA9/OTG_FS_VBUS Issues
  2. 基于原版Hadoop的YDB部署(转)
  3. 基于哈夫曼编码完成的文件压缩及解压
  4. SQL Server安全(8/11):数据加密(Data Encryption)
  5. jQuery的radio,checkbox,select操作
  6. 2021-07-27-jeesite学习笔记
  7. linux的常用操作——makefile
  8. 【计算机网络复习 数据链路层】3.3.1 差错控制(检错编码)
  9. 【计算机网络复习】1.2.1 分层结构、协议、接口、服务
  10. Leetcode--347. 前k个高频元素