代码编织梦想

简介

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 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

TCP/IP-爱代码爱编程

TCP/IP在数据包设计上采用封装和分用的策略,所谓封装就是在应用程序在发送数据的过程中,每一层都增加一些首部信息,这些信息用于和接收端同层次进行沟通,例如当数据从应用程序发送到以太网过程中数据逐层加工的示意图如下所示: 1.应用层 运行在TCP协议上的协议: HTTP(Hypertext Transfer Protocol,超文本传输协议),主要用于普

计算机网络体系结构-爱代码爱编程

计算机网络体系结构 计算机网络是利用通信线路将地理上分散的、具有独立功能的计算机系统和通信设备按不同的形式连接起来,以功能完善的网络软件及协议实现资源共享和信息传递的系统。 网络模型 OSI七层模型,开放系统互连参考模型(Open System Interconnect,简称OSI)是国际标准化组织(ISO)和国际电报电话咨询委员会(CCITT)联合

【IPv4】十进制点分表示法转换-爱代码爱编程

IPv4 十进制点分表示法转换 文章目录 IPv4 十进制点分表示法转换一. IPv4 地址表示二. 二进制表示法转换为十进制点法其他相关文章 一. IPv4 地址表示 IPv4 地址是由 32位(二进制位) 组成。IP 地址是 TCP/IP 协议集网络层的地址标识符。 一个 IP 地址的二进制形式如下: 1100 0000 1010

网络编程复习(TCP)----和服务器说 hello!-爱代码爱编程

网络编程复习 --对TCP服务器说hello! 1.什么是TCP?2.TCP能干嘛?3.TCP的实现过程tcp服务器tcp客户端地址重复使用收发消息recv/send4.TCP的优缺点 1.什么是TCP? TCP是一种传输控bai制协议,是面向连接的、可du靠的、基于字节流zhi之间的传输层通信协dao议,由IETF的RFC 793定义。在简

TCP/IP网络协议——单播多播和广播-爱代码爱编程

文章目录 一、TCP和UDP区别二、IP地址三、广播1 受限的广播2 指向网络的广播3 指向子网的广播4 指向所有子网的广播四、多播总结 一、TCP和UDP区别 协议层协议TCP(传输控制协议)和UDP(用户数据报协议) TCP为两台主机提供高可靠性的数据通信,包括把应用层程序交给他的数据分成合适的小块交给下面的网络层,确认接收到的分组,设置

TCP粘包的解决方案-爱代码爱编程

前言 TCP粘包是个常见的问题,也有很多文章谈到。我根据自己的经验,争取用一种简洁易懂的办法把粘包的问题和解决办法和读者分享,希望留言指正。 基本概念 TCP本质上是数据流,从原理上看,没有包的概念,TCP包对应用程序员可以是透明的。 粘包实际上是把底层包的实现和上层流的概念混在一起。 粘包问题本质上是如何确定数据流的边界。 确定边界

linux-linux系统中部署多个服务器的nginx负载均衡-亲测有效-爱代码爱编程

一、编辑nginx.conf文件  1、修改nginx.conf文件 进入conf文件夹,cd /usr/local/nginx/conf 编辑nginx.conf,vim nginx.conf,         #keepalive_timeout  0;     keepalive_timeout  65;     #gzip

2020-10-20-爱代码爱编程

场景分析: 某IT公司的员工小东,在公司部署了一台zabbix用来监控window系统服务器的一些使用情况。他现在想做到一个功能,就是通过浏览器发现某台window主机出现内存过高,或者cpu利用率过高,或者某台服务器的一些服务挂了的一些告警后,他可以在浏览器哪里通过执行脚本来打开远程桌面,然后他只需要填入远程主机的账号和密码就可以登陆那台出现的wind

Error: Cannot find module ‘webpack-cli/bin/config-yargs‘-爱代码爱编程

今天在使用 webpack 创建项目,启动 webpack-dev-server 时,报了错: 这个是 package.json 中创建的 scripts : "scripts": { "start": "webpack-dev-server --config ./config/webpack.common.js " }, 下面是报错:

利用python搭建socket server服务器-爱代码爱编程

socketserver 利用封装好的socketserver进行服务器监听 import socketserver ip_port=("192.168.20.135",9999) class MyServer(socketserver.BaseRequestHandler): def Handle(self): print

nginx之server从配置到监听-爱代码爱编程

        一般的 nginx 某个虚拟主机配置文件可能如下: http { include mime.types; access_log logs/access.log; gzip on; server { listen 80; server_name w

WinSock I/O 模型 -- WSAEventSelect 模型-爱代码爱编程

简介 WSAEventSelect 模型也是 WinSock 中最常见的异步 I/O 模型。 这篇文章我们就来看看如何使用 WSAEventSelect api 来实现一个简单的 TCP 服务器. API 基础 WSAEventSelect WSAEventSelect 用来把一个 SOCKET 对象和一个 WSAEVENT 对象关联起来。 lN