代码编织梦想

简介

WSAEventSelect 模型也是 WinSock 中最常见的异步 I/O 模型。

这篇文章我们就来看看如何使用 WSAEventSelect api 来实现一个简单的 TCP 服务器.

API 基础

WSAEventSelect

WSAEventSelect 用来把一个 SOCKET 对象和一个 WSAEVENT 对象关联起来。 lNetworkEvents 表示我们关心的 FD_XXX 网络事件. 如果关心多个 SOCKET 事件,可以使用 OR 的方式指定多个 FD_XXX 标志。

int WSAAPI WSAEventSelect(
  SOCKET   s,
  WSAEVENT hEventObject,
  long     lNetworkEvents
);

当特定的事件发生在相应的 SOCKET 上,该 SOCKET 上接下来的事件将会被阻塞,直到当前的事件被应用处理. 该事件被处理之后,接下来的事件将可以被进一步触发.

事件处理函数
FD_READrecv, recvFrom, WSARecv, WSARecvEx, WSARecvFrom
FD_WRITEsend, sendTo, WSASend, WSASentTo
FD_OOBrecv, recvFrom, WSARecv, WSARecvEx, WSARecvFrom
FD_ACCEPTaccept, AcceptEx, WSAAccept
FD_CONNECTNone
FD_CLOSENone
FD_QOSWSAIoctl (with SIO_GET_QOS)
FD_GROUP_QOSReserved
FD_ROUTINE_INTERFACE_CHANGEWSAIoctl (with SIO_ROUTINE_INTERFACE_CHANGE)
FD_ADDRESS_LIST_CHANGEWSAIoctl (with SIO_ADDRESS_LIST_CHANGE)
WSAEvent

WSACreateEvent 方法用来创建一个 WSAEvent 对象

WSAEVENT WSAAPI WSACreateEvent();

WSAWaitForMultipleEvents 用于等待一组事件中的一个或全部被触发。

DWORD WSAAPI WSAWaitForMultipleEvents(
  DWORD          cEvents,
  const WSAEVENT *lphEvents,
  BOOL           fWaitAll,
  DWORD          dwTimeout,
  BOOL           fAlertable
);
  • cEvents:指定 lphEvents 数组中事件对象的数量。 该参数的最大值是 WSA_MAXIMUM_WAIT_EVENTS (64)
  • lphEvents:事件对象的集合
  • fWaitAll: 指定等待 lphEvents 中所有事件被触发或者其中之一被触发。 如果指定为 TRUE, 那么该函数只有在所有事件对象都被触发之后才会返回。 如果指定为 FALSE, 当事件集合中任何一个事件被触发之后,该方法就会返回。如果在这种情况下有多个事件对象被触发,那个返回值将会返回该事件集合中索引值最小的索引值. 索引值减去 WSA_WAIT_EVENT_0 便是指向 lphEvents 中被触发的事件的索引值.
  • dwTimeout: 如果在 timeout 事件间隔内,没有事件被触发,函数不会一直阻塞,而是在等待 timeout 毫秒后返回。 指定该参数为 WSA_INFINITE, 该函数会一直等待,直到有事件被触发. 指定该参数为 0, 该函数会立即返回.
  • fAlertable: 略

WSAEnumNetworkEvents 用于查询当前 SOCKET 上触发事件对象 hEventObject 的对应 socket 事件(FD_READ, FD_WRITE 等).

int WSAAPI WSAEnumNetworkEvents(
  SOCKET             s,
  WSAEVENT           hEventObject,
  LPWSANETWORKEVENTS lpNetworkEvents
);

实现思路

  1. 创建一个 socket 作为监听 socket
  2. 使用 WSAEventSelect 监听该 SOCKET 上的网络事件
  3. 使用 WSAWaitForMultipleEvents 等待 SOCKET 事件
  4. 当 SOCKET 上有事件被触发,使用 WSAEnumNetworkEvents 查询具体的 SOCKET 事件,并使用相应的 API 处理事件.
  5. 当有新的 SOCKET 连接到来,接收该连接,重复 2-4 步骤.

实例

接下来我们通过一个实例来看看如何实现.

#include <winsock2.h>
#include <windows.h>
#include <stdio.h>

#pragma comment(lib,"ws2_32.lib")

#define PORT 8080
#define DATA_BUFSIZE 8192
 
typedef struct _SOCKET_CONTEXT {
   CHAR   Buffer[DATA_BUFSIZE];
   WSABUF DataBuf;
   SOCKET Socket;
   DWORD  BytesSEND;
   DWORD  BytesRECV;
} SOCKET_CONTEXT, * LPSOCKET_CONTEXT;

BOOL CreateSocketInformation(SOCKET s);
void FreeSocketInformation(DWORD Event);

// 这里我们维护了如下数据结构:
//     EeventArray: 我们为每个 SOCKET 对象创建一个对应的事件对象,以便我们能监听该 SOCKET 上的网络事件
//     SocketArray: 毫无疑问,我们也需要维护所有SOCKET连接的的数组。其中包含 Listen Socket

DWORD            EventTotal = 0;
WSAEVENT         EventArray[WSA_MAXIMUM_WAIT_EVENTS];
LPSOCKET_CONTEXT SocketArray[WSA_MAXIMUM_WAIT_EVENTS];

int main() {

  SOCKET           ListenSocket;
  SOCKET           AcceptSocket;
  SOCKADDR_IN      Addr;
  LPSOCKET_CONTEXT SocketContext;
  WSANETWORKEVENTS NetworkEvents;
  DWORD            Event;
  WSADATA          wsaData;
  DWORD            Flags;
  DWORD            RecvBytes;
  DWORD            SendBytes;

  // 初始化 Listen Socket 对象
  if (WSAStartup(0x0202, &wsaData) != 0) {
    printf("WSAStartup() failed with error %d\n", WSAGetLastError());
    return 1;
  }

  if ((ListenSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
    printf("socket() failed with error %d\n", WSAGetLastError());
    return 1;
  }

  if (CreateSocketInformation(ListenSocket) == FALSE) {
    printf("CreateSocketInformation() failed!\n");
    return 1;
  }

  // 在调用 listen api 之前,我们需要使用 WSAEventSelect 需要将 ListenSocket 与一个 WSAEvent 对象关联起来,这里我们仅仅关系 FD_ACCEPT 和 FS_CLOSE 事件.
  // 当这两个事件之一被触发,我们编译可以从 EventArray[0] 上查询到这些实际,以便进行处理
  if (WSAEventSelect(ListenSocket, EventArray[EventTotal - 1], FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR) {
    printf("WSAEventSelect() 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(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 error %d\n", WSAGetLastError());
    return 1;
  }

  while(TRUE) {

    // 等待当前所有 socket 上的网络事件被触发
    //
    // 这里我们 fWait = FALSE, 也就是说任何一个 SOCKET 上有网络之间,
    // 我们便停止等待,开始处理该事件
    //
    // dwTimeout = WSA_INFINITE, 如果没有网络事件发生,我们就一直等待,
    // 直到有网络事件发生
    //
    // 这里 EventTotal 会随着客户端连接的到来增加,同时我们会创建对应的 Event对象,
    // 并放入 EventArray
    if ((Event = WSAWaitForMultipleWSAWaitForEvents(EventTotal, EventArray, FALSE, WSA_INFINITE, FALSE)) == WSA_WAIT_FAILED) {
      printf("WSAWaitForMultipleEvents() failed with error %d\n", WSAGetLastError());
      return 1;
    }

    // 程序运行到这里,已经有网络事件发生了,我们使用WSAEnumNetworkEvents 查询到底是什么网络事件, 
    // 查询结果保存在 NetworkEvents 对象上
    // 注意,在 API 章节我们已经说明,WSAWaitForMultipleWSAWaitForEvents 的返回值减去 WSA_WAIT_EVENT_0 才是对应的 EventArray 中被触发的事件的索引值
    if (WSAEnumNetworkEvents(
        SocketArray[Event - WSA_WAIT_EVENT_0]->Socket,
        EventArray[Event - WSA_WAIT_EVENT_0], 
        &NetworkEvents) == SOCKET_ERROR
    ) {
      printf("WSAEnumNetworkEvents() failed with error %d\n", WSAGetLastError());
      return 1;
    }
    
    // 检查当前事件是否是 FD_ACCEPT
    // 如果是 FD_ACCEPT事件,使用 accept 接收新的连接。
    if (NetworkEvents.lNetworkEvents & FD_ACCEPT) {
      if (NetworkEvents.iErrorCode[FD_ACCEPT_BIT] != 0) {
        printf("FD_ACCEPT failed with error %d\n", NetworkEvents.iErrorCode[FD_ACCEPT_BIT]);
        break;
      }

      if ((AcceptSocket = accept(SocketArray[Event - WSA_WAIT_EVENT_0]->Socket, NULL, NULL)) == INVALID_SOCKET) {
        printf("accept() failed with error %d\n", WSAGetLastError());
        break;
      }

      if (EventTotal > WSA_MAXIMUM_WAIT_EVENTS) {
        printf("Too many connections - closing socket...\n");
        closesocket(AcceptSocket);
        break;
      }

      // 接收新的连接之后,为该 SOCKET 创建 WSAEvent对象(在CreateSocketInformation 实现)
      // 然后监听该 SOCKET 的 FD_READ, FD_WRITE, FD_CLOSE 事件
      CreateSocketInformation(AcceptSocket);
      if (WSAEventSelect(AcceptSocket, EventArray[EventTotal - 1], FD_READ|FD_WRITE|FD_CLOSE) == SOCKET_ERROR) {
        printf("WSAEventSelect() failed with error %d\n", WSAGetLastError());
        return 1;
      }

      printf("Socket %d got connected...\n", AcceptSocket);
    }

    // 检查当前事件是否是 FD_READ 或者 FD_WRITE
    if (NetworkEvents.lNetworkEvents & FD_READ || NetworkEvents.lNetworkEvents & FD_WRITE) {

      // 检查是不是发生了读错误
      if (NetworkEvents.lNetworkEvents & FD_READ && NetworkEvents.iErrorCode[FD_READ_BIT] != 0) {
        printf("FD_READ failed with error %d\n", NetworkEvents.iErrorCode[FD_READ_BIT]);
        break;
      }

      // 检查是不是发生了写错误
      if (NetworkEvents.lNetworkEvents & FD_WRITE && NetworkEvents.iErrorCode[FD_WRITE_BIT] != 0) {
        printf("FD_WRITE failed with error %d\n", NetworkEvents.iErrorCode[FD_WRITE_BIT]);
        break;
      }

      SocketContext = SocketArray[Event - WSA_WAIT_EVENT_0];

      // Read data only if the receive buffer is empty
      if (SocketContext->BytesRECV == 0) {
        SocketContext->DataBuf.buf = SocketContext->Buffer;
        SocketContext->DataBuf.len = DATA_BUFSIZE;

        Flags = 0;
        if (WSARecv(SocketContext->Socket, &(SocketContext->DataBuf), 1, &RecvBytes, &Flags, NULL, NULL) == SOCKET_ERROR) {
          if (WSAGetLastError() != WSAEWOULDBLOCK) {
            printf("WSARecv() failed with error %d\n", WSAGetLastError());
            FreeSocketInformation(Event - WSA_WAIT_EVENT_0);
            return 1;
          }
        } else {
          printf("WSARecv() is working!\n");
          SocketContext->BytesRECV = RecvBytes;
        }
      }

      if (SocketContext->BytesRECV > SocketContext->BytesSEND) {

        SocketContext->DataBuf.buf = SocketContext->Buffer + SocketContext->BytesSEND;
        SocketContext->DataBuf.len = SocketContext->BytesRECV - SocketContext->BytesSEND;

        if (WSASend(SocketContext->Socket, &(SocketContext->DataBuf), 1, &SendBytes, 0, NULL, NULL) == SOCKET_ERROR) {

          if (WSAGetLastError() != WSAEWOULDBLOCK) {
            printf("WSASend() failed with error %d\n", WSAGetLastError());
            FreeSocketInformation(Event - WSA_WAIT_EVENT_0);
            return 1;
          }
          // A WSAEWOULDBLOCK error has occurred. An FD_WRITE event will be posted
          // when more buffer space becomes available
        } else {

          printf("WSASend() is fine! Thank you...\n");
          SocketContext->BytesSEND += SendBytes;

          if (SocketContext->BytesSEND == SocketContext->BytesRECV) {
            SocketContext->BytesSEND = 0;
            SocketContext->BytesRECV = 0;
          }
        }
      }
    }

    // 检查当前事件是否是 FD_CLOSE
    if (NetworkEvents.lNetworkEvents & FD_CLOSE) {
      // 检查是否发生了错误
      if (NetworkEvents.iErrorCode[FD_CLOSE_BIT] != 0) {
        printf("FD_CLOSE failed with error %d\n", NetworkEvents.iErrorCode[FD_CLOSE_BIT]);
        break;
      } else {
        // socket 正常关闭
        printf("FD_CLOSE is OK!\n");
      }

      printf("Closing socket information %d\n", SocketArray[Event - WSA_WAIT_EVENT_0]->Socket);
      FreeSocketInformation(Event - WSA_WAIT_EVENT_0);
    }
  }
  return 0;
}

 
BOOL CreateSocketInformation(SOCKET s) {
  LPSOCKET_CONTEXT SocketContext;

  if ((EventArray[EventTotal] = WSACreateEvent()) == WSA_INVALID_EVENT) {
    printf("WSACreateEvent() failed with error %d\n", WSAGetLastError());
    return FALSE;
  }

  if ((SocketContext = (LPSOCKET_CONTEXT) GlobalAlloc(GPTR, sizeof(SOCKET_CONTEXT))) == NULL) {
    printf("GlobalAlloc() failed with error %d\n", GetLastError());
    return FALSE;
  }

  // Prepare SocketInfo structure for use
  SocketContext->Socket = s;
  SocketContext->BytesSEND = 0;
  SocketContext->BytesRECV = 0;

  SocketArray[EventTotal] = SocketContext;
  EventTotal++;
  return TRUE;
}

void FreeSocketInformation(DWORD Event) {

  LPSOCKET_CONTEXT SocketContext = SocketArray[Event];
  DWORD i;

  closesocket(SocketContext->Socket);
  GlobalFree(SocketContext);
 
  if(WSACloseEvent(EventArray[Event]) == TRUE) {
    printf("WSACloseEvent() is OK!\n\n");
  }

  // Squash the socket and event arrays
  for (i = Event; i < EventTotal; i++) {
    EventArray[i] = EventArray[i + 1];
    SocketArray[i] = SocketArray[i + 1];
  }
  EventTotal--;
}

END!!!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/zhaoruixiang1111/article/details/109553150

netTerrain自动化网络拓朴-爱代码爱编程

使用Excel、PPT、Visio来管理我们的网络,最终都陷入了一个十字路口,我们必须找到替代方案。为什么这是不可避免的? 网络始终在变化和发展管理人员的变更造成网络文档的缺失我们的网络管理人员也需要假期传统的网络监控管理工具在监视报警和网络性能分析方面做得很好,但是在网络文档管理方面做得很差。例如:此服务器是否处于保修期内?是否正在维护中?我可以与谁

OSPF 与 RIP路由重发布-爱代码爱编程

1、网络拓扑 链接:https://pan.baidu.com/s/1xxeC5Dk-v2trU1wv61QRCQ 提取码:8888 eNSP 链接:https://pan.baidu.com/s/1wP0vHim4yqVV0bc0wmzhFw 提取码:8888 设备接口编号及IP编址如图所示 2、网络需求 a. R1-R2之间运行RIPv2;R2-R3

基于RTL8211E的千兆以太网收发verilog程序(已经硬件验证,初学=语法注释较多)-爱代码爱编程

软件平台quartus 13.1 硬件芯片 Atera,Cyclone IV EP4CE30F23C8 千兆PHY芯片:RTL8211E 通讯协议:udp/ip协议 程序自己按照相关资料,一行一行的写出来的,主要使用了状态机,fifo,RAM和ROM,ROM用于存储以太网的mac,ip等信息,具体的测试方法和注意事项在每一个模块的头部有说明,程序完全按照时

锐捷校园网环境下设置统信UOS(Linux)自动连接网络-爱代码爱编程

1. 写在前面   记录在锐捷校园网环境下使用UOS(Linux)自动连接网络的方法,如若仅需找到快速自动连接网络的方法,请直接跳转到 4. 步骤总结 即可。vincent是我的用户名,根据实际情况做替换即可。 2. 目标   让主机按下开机键后,无需人工干预,连接互联网,启动向日葵客户端,等待远程桌面连接。 3. 分析 自动联网 开机后自

简单科普下5G知识-爱代码爱编程

目录 1、5G概念2、 5G实现的技术指标和三大应用场景是什么?3、5G应用场景和关键技术3.1 5G的关键技术4、VR,AR,MR的概念5、5G的智能应用 1、5G概念 第五代移动通信技术 ■ 移动通信发展和实现的业务 2、 5G实现的技术指标和三大应用场景是什么? 流量密度 10Tbps/KM2连接数密度 100万/km2时延性

Linux网络编程记录-爱代码爱编程

一、字节序 1.字节序:指多字节数据在计算机内存中存储或者网络传输时个字节的存储顺序 2.Little endian 小端字节序 3.Big endian 大端字节序 4.网络字节序=大端字节序 5.例子:在内存中双字0x01020304(DWORD)的存储方式(来源百度百科) 内存地址 4000&4001&4002&

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

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

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

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

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

简介 Select 模型是 WinSock 中最常见的 I/O 模型,这篇文章我们就来看看如何使用 Select api 来实现一个简单的 TCP 服务器. API 基础 Select 模型依赖 WinSock API Select 来检查当前 Socket 是否可写或者可读。 使用这个 API 的优点是我们不需要使用阻塞的 Socket API

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

简介 WSAAsyncSelect 模型也是 WinSock 中常见的异步 I/O 模型。 使用这个模型,网络应用程序通过接收以 Windows 消息为基础的网络事件通知来处理网络请求。 这篇文章我们就来看看如何使用 WSAAsyncSelect api 来实现一个简单的 TCP 服务器. API 基础 要使用 WSAAsyncSelect 模型

GO:HTTP客户端和服务端demo-爱代码爱编程

GO:HTTP客户端和服务端demo 客户端: package main import ( "fmt" "io/ioutil" "net/http" "strings" ) func main() { /* 示例1:GET */ { fmt.Println("++++++++++++++++++++++++++++++++++++

总线/通信笔记3 —— Modbus TCP 的Server使用-爱代码爱编程

文章目录 1. libmodbus库的使用2. Modbus TCP开发实践 1. libmodbus库的使用 Step 1:初始化RTU/TCP指针 modbus_t *ctx; ctx = modbus_new_tcp(NULL,502); Step 2:初始化变量 在这里插入代码片 Step 3:申请内存 mb_mappi