[笔记]windows网络编程之常见模型(下)

简介: [笔记]windows网络编程之常见模型(下)

完成端口模型

是什么?

利用线程池处理异步I/O请求,利用完成端口模型可以管理成百上千Socket。

可以把完成端口看成系统维护的一个队列,操作系统把重叠I/O操作完成的事件通知放到该队列中,因此称其为“完成”端口,当Socket被创建后,可以将其与一个完成端口联系起来。

一个应用程序可以创建多个工作线程用于处理完成端口上的通知事件,通常应该为每个CPU创建一个线程。

一个完成端口实际就是一个通知队列,操作系统把已经完成的重叠I/O请求的通知放到队列中,当某项IO操作完成后,系统会向服务端完成端口发送一个i/o完成数据包,此操作在系统内部完成,应用程序在收到I/o完成数据包后,完成端口队列的一个线程被唤醒,为客户端提供服务,服务完成后,该线程会继续在完成端口上等待.

为什么?

(1) 首先,如果使用“同步”的方式来通信的话,这里说的同步的方式就是说所有的操作都在一个线程内顺序执行完成,这么做缺点是很明显的:因为同步的通信操作会阻塞住来自同一个线程的任何其他操作,只有这个操作完成了之后,后续的操作才可以完成;一个最明显的例子就是咱们在MFC的界面代码中,直接使用阻塞Socket调用的代码,整个界面都会因此而阻塞住没有响应!所以我们不得不为每一个通信的Socket都要建立一个线程,多麻烦?这不坑爹呢么?所以要写高性能的服务器程序,要求通信一定要是异步的。

(2) 各位读者肯定知道,可以使用使用“同步通信(阻塞通信)+多线程”的方式来改善(1)的情况,那么好,想一下,我们好不容易实现了让服务器端在每一个客户端连入之后,都要启动一个新的Thread和客户端进行通信,有多少个客户端,就需要启动多少个线程,对吧;但是由于这些线程都是处于运行状态,所以系统不得不在所有可运行的线程之间进行上下文的切换,我们自己是没啥感觉,但是CPU却痛苦不堪了,因为线程切换是相当浪费CPU时间的,如果客户端的连入线程过多,这就会弄得CPU都忙着去切换线程了,根本没有多少时间去执行线程体了,所以效率是非常低下的,承认坑爹了不?
    (3) 而微软提出完成端口模型的初衷,就是为了解决这种"one-thread-per-client"的缺点的,它充分利用内核对象的调度,只使用少量的几个线程来处理和客户端的所有通信,消除了无谓的线程上下文切换,最大限度的提高了网络通信的性能,

怎么用?

服务端:

1.初始化windows socket 环境

2.创建完成端口对象completionPort

3.根据当前计算机CPU的数量创建工作线程,并将新建的完成端口对象CompletionPort作为线程的参数

4.创建监听SocketListen 并将其绑定到本地地址 端口

5.在while循环中处理来自客服端的请求连接,接收连接,并将得到的与客户端进行通信的socketAccept保存到PER_HANDLE_DATA结构体对象PerHandleData中.将SocketAccept与前面的端口completionPort关联

6.在socket accept上调用 WSARecv函数,异步接收socket上来自客户端的数据,WSARecv函数立即返回,此时socketAccept上不一定有客户端发送来的消息,在工作线程中会检测完成端口对象的状态,并接收来自客户端的数据,再将这些数据发送回客户端程序.

// CompletionPortServer.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>
#define PORT 9990             // 监听的端口
#define DATA_BUFSIZE 8192     // 发送和接收消息的最大长度
#pragma comment(lib, "Ws2_32")
// 定义I/O操作的结构体
typedef struct                       
{
   OVERLAPPED Overlapped;         // 重叠结构
   WSABUF DataBuf;                  // 缓冲区对象
   CHAR Buffer[DATA_BUFSIZE];       // 缓冲区数组
   DWORD BytesSEND;                             // 发送字节数
   DWORD BytesRECV;                             // 接收的字节数    
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;
// 套接字句柄结构体
typedef struct                      
{
   SOCKET Socket;
} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;
// 服务器端工作线程
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID);
int _tmain(int argc, _TCHAR* argv[])
{
  SOCKADDR_IN InternetAddr;             // 服务器地址
  SOCKET Listen;                        // 监听套接字
  SOCKET Accept;                        // 与客户端进行通信的套接字
  HANDLE CompletionPort;                  // 完成端口句柄
  SYSTEM_INFO SystemInfo;               // 获取系统信息(这里主要用于获取CPU数量)
  LPPER_HANDLE_DATA PerHandleData;      // 套接字句柄结构体
  LPPER_IO_OPERATION_DATA PerIoData;    // 定义I/O操作的结构体
  DWORD RecvBytes;                      // 接收到的字节数
  DWORD Flags;                          // WSARecv()函数中指定的标识位
  DWORD ThreadID;                     // 工作线程编号
  WSADATA wsaData;                      // Windows Socket初始化信息
  DWORD Ret;                            // 函数返回值
  // 初始化Windows Sockets环境
  if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)
  {
    printf("WSAStartup failed with error %d\n", Ret);
    return -1;
  }
  // 创建新的完成端口
  if ((CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)
  {
    printf( "CreateIoCompletionPort failed with error: %d\n", GetLastError());
    return -1;
  }
  // 获取系统信息
  GetSystemInfo(&SystemInfo);   
   // 根据CPU数量启动线程
  for(int i = 0; i<SystemInfo.dwNumberOfProcessors * 2; i++)
  {
    HANDLE ThreadHandle;
    // 创建线程,运行ServerWorkerThread()函数            
    if ((ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,
     0, &ThreadID)) == NULL)
    {
       printf("CreateThread() failed with error %d\n", GetLastError());
       return -1;
    }      
    CloseHandle(ThreadHandle);
  }
  // 创建监听套接字
  if ((Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,
    WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
  {
    printf("WSASocket() failed with error %d\n", WSAGetLastError());
    return -1;
  }
  // 绑定到本地地址的9990端口
  InternetAddr.sin_family = AF_INET;
  InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  InternetAddr.sin_port = htons(PORT);
  if (bind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR)
  {
    printf("bind() failed with error %d\n", WSAGetLastError());
    return -1;
  }   
  // 开始监听
  if (listen(Listen, 5) == SOCKET_ERROR)
  {
    printf("listen() failed with error %d\n", WSAGetLastError());
    return -1;
  }
   // 监听端口打开,就开始在这里循环,一有socket连上,WSAAccept就创建一个socket,
   // 这个socket 和完成端口联上
  while(TRUE)
  {
    // 等待客户连接
    if ((Accept = WSAAccept(Listen, NULL, NULL, NULL, 0)) == SOCKET_ERROR)
    {
      printf("WSAAccept() failed with error %d\n", WSAGetLastError());
      return -1;
    }
    // 分配并设置套接字句柄结构体
    if ((PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA))) == NULL)
    {
      printf("GlobalAlloc() failed with error %d\n", GetLastError());
      return -1;
    }   
    PerHandleData->Socket = Accept;
    // 将与客户端进行通信的套接字Accept与完成端口CompletionPort相关联
    if (CreateIoCompletionPort((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData,
        0) == NULL)
    {
      printf("CreateIoCompletionPort failed with error %d\n", GetLastError());
      return -1;
    }
    // 为I/O操作结构体分配内存空间
    if ((PerIoData = (LPPER_IO_OPERATION_DATA) GlobalAlloc(GPTR,sizeof(PER_IO_OPERATION_DATA))) == NULL)
    {
      printf("GlobalAlloc() failed with error %d\n", GetLastError());
      return -1;
    }
    // 初始化I/O操作结构体
    ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
    PerIoData->BytesSEND = 0;
    PerIoData->BytesRECV = 0;
    PerIoData->DataBuf.len = DATA_BUFSIZE;
    PerIoData->DataBuf.buf = PerIoData->Buffer;
    Flags = 0;
    // 接收数据,放到PerIoData中,而perIoData又通过工作线程中的ServerWorkerThread函数取出,
    if (WSARecv(Accept, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
      &(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
    {
      if (WSAGetLastError() != ERROR_IO_PENDING)
      {
        printf("WSARecv() failed with error %d\n", WSAGetLastError());
        return -1;
      }
    }
  }
  return 0;
}
//  工作线程,循环检测完成端口状态,获取PerIoData中的数据
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)
{
  HANDLE CompletionPort = (HANDLE) CompletionPortID;  // 完成端口句柄   
  DWORD BytesTransferred;                   // 数据传输的字节数
  LPOVERLAPPED Overlapped;                  // 重叠结构体
  LPPER_HANDLE_DATA PerHandleData;          // 套接字句柄结构体
  LPPER_IO_OPERATION_DATA PerIoData;        // I/O操作结构体
  DWORD SendBytes, RecvBytes;               // 发送和接收的数量
  DWORD Flags;                              // WSARecv()函数中的标识位
  while(TRUE)
  {
    // 检查完成端口的状态
    if (GetQueuedCompletionStatus(CompletionPort, &BytesTransferred,
      (LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE) == 0)
    {
      printf("GetQueuedCompletionStatus failed with error %d\n", GetLastError());
      return 0;
    }
    // 如果数据传送完了,则退出
    if (BytesTransferred == 0)
    {
      printf("Closing socket %d\n", PerHandleData->Socket);
      // 关闭套接字
      if (closesocket(PerHandleData->Socket) == SOCKET_ERROR)
      {
        printf("closesocket() failed with error %d\n", WSAGetLastError());
        return 0;
      }
      // 释放结构体资源
      GlobalFree(PerHandleData);
      GlobalFree(PerIoData);
      continue;
    }     
    // 如果还没有记录接收的数据数量,则将收到的字节数保存在PerIoData->BytesRECV中
    if (PerIoData->BytesRECV == 0)
    {
      PerIoData->BytesRECV = BytesTransferred;
      PerIoData->BytesSEND = 0;
    }
    else   // 如果已经记录了接收的数据数量,则记录发送数据量
    {
      PerIoData->BytesSEND += BytesTransferred;
    }
    // 将收到的数据原样发送回客户端
    if (PerIoData->BytesRECV > PerIoData->BytesSEND)
    {
      ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED)); //清0为发送准备
      PerIoData->DataBuf.buf = PerIoData->Buffer + PerIoData->BytesSEND;
      PerIoData->DataBuf.len = PerIoData->BytesRECV - PerIoData->BytesSEND;
      // 一个字节一个字节发送发送数据出去
      if (WSASend(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &SendBytes, 0,
          &(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
      {
        if (WSAGetLastError() != ERROR_IO_PENDING)
        {
          printf("WSASend() failed with error %d\n", WSAGetLastError());
          return 0;
        }
      }
    }
    else
    {
      PerIoData->BytesRECV = 0;
      Flags = 0;
      ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
      PerIoData->DataBuf.len = DATA_BUFSIZE;
      PerIoData->DataBuf.buf = PerIoData->Buffer;
      if (WSARecv(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
        &(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
      {
        if (WSAGetLastError() != ERROR_IO_PENDING)
        {
          printf("WSARecv() failed with error %d\n", WSAGetLastError());
          return 0;
        }
      }
    }
   }
}

具体参照
《windows网络编程》第八章 8.7 基于完成端口模型的服务器编程

完成端口(CompletionPort)详解 - 手把手教你玩转网络编程系列之三

单播,组播,多播

是什么?

单播,1 对 1

组播,1 对 多

广播,1 对 局域网所有

为什么?

单播,基于tcp,建立可靠连接,需要c/s ip;

组播,基于udp,监听同一ip(224.0.0.0至239.255.255.255)的一组都会接 收到消息;

广播,基于udp,监听 地址:xx.xx.xx.255 都会接收到消息

用途

单播用于连接建立后的私密通讯,组播和广播用于通知,建立连接前的通讯,获取ip。

安全套接字协议SSL

是什么?

SSL 可以用来保障在internet上数据传输的安全,利用数据加密技术,可确保数据在网络上的传输过程不会被截取及其监听.

SSL用于在web服务器和浏览器之间建立加密连接的标准安全技术.

SSL协议提供的安全信道有以下三个特性:

私密性。因为在握手协议定义了会话密钥后,所有的消息都被加密。

确认性。因为尽管会话的客户端认证是可选的,但是服务器端始终是被认证的。

可靠性。因为传送的消息包括消息完整性检查(使用MAC)。

主要工作在应用层(http,ftp,telnet)和 传输层(tcp/udp)之间的

参考

图解安全套接字SSL协议的工作原理

安全套接层(SSL)协议

简单了解:Openssl开源安全套接字协议

<<windows网络编程代码>>以及广播代码

百度网盘代码 提取码:72y7

相关文章
|
9月前
|
安全 Linux 网络安全
Nipper 3.9.0 for Windows & Linux - 网络设备漏洞评估
Nipper 3.9.0 for Windows & Linux - 网络设备漏洞评估
255 0
Nipper 3.9.0 for Windows & Linux - 网络设备漏洞评估
|
Linux 开发工具 Android开发
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
ijkplayer是由Bilibili基于FFmpeg3.4研发并开源的播放器,适用于Android和iOS,支持本地视频及网络流媒体播放。本文详细介绍如何在新版Android Studio中导入并使用ijkplayer库,包括Gradle版本及配置更新、导入编译好的so文件以及添加直播链接播放代码等步骤,帮助开发者顺利进行App调试与开发。更多FFmpeg开发知识可参考《FFmpeg开发实战:从零基础到短视频上线》。
2052 2
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
|
机器学习/深度学习 数据可视化 计算机视觉
目标检测笔记(五):详细介绍并实现可视化深度学习中每层特征层的网络训练情况
这篇文章详细介绍了如何通过可视化深度学习中每层特征层来理解网络的内部运作,并使用ResNet系列网络作为例子,展示了如何在训练过程中加入代码来绘制和保存特征图。
624 1
目标检测笔记(五):详细介绍并实现可视化深度学习中每层特征层的网络训练情况
|
监控 安全 网络协议
恶意软件无处逃!国内版“Manus”AiPy开发Windows沙箱工具,进程行为+网络传输层级监控! 头像 豪气的
NImplant.exe 是一款后渗透测试工具,可实现远程管理与持久化控制。其优点包括无文件技术、加密通信和插件扩展,但也存在被检测风险及配置复杂等问题。为深入分析其行为,我们基于 aipy 开发了 Windows 沙箱工具,针对桌面上的 NImplant.exe 进行多维度分析,涵盖进程行为、网络连接(如 TCP 请求、目标 IP/域名)、文件控制等,并生成传输层监控报告与沙箱截图。结果显示,aipy 工具响应迅速,报告清晰易读,满足分析需求。
|
12月前
|
网络协议 安全 测试技术
Windows为何在高速网络环境频繁“失速”?
本文深入剖析了企业在高速网络环境中,因Windows系统限制导致传输速率下降的问题,包括接收缓冲区、安全软件及老旧设备等因素,并提供四步定位法及优化方案,助力企业突破传输瓶颈,提升效率。
|
Web App开发 人工智能 JSON
Windows版来啦!Qwen3+MCPs,用AI自动发布小红书图文/视频笔记!
上一篇用 Qwen3+MCPs实现AI自动发小红书的最佳实践 有超多小伙伴关注,同时也排队在蹲Windows版本的教程。
2274 1
|
安全 Ubuntu Linux
Nipper 3.8.0 for Windows & Linux - 网络设备漏洞评估
Nipper 3.8.0 for Windows & Linux - 网络设备漏洞评估
585 0
Nipper 3.8.0 for Windows & Linux - 网络设备漏洞评估
|
XML 安全 网络安全
Nipper 3.7.0 Windows x64 - 网络设备漏洞评估
Nipper 3.7.0 Windows x64 - 网络设备漏洞评估
300 0
Nipper 3.7.0 Windows x64 - 网络设备漏洞评估
|
安全 Windows
【Azure Cloud Service】在Windows系统中抓取网络包 ( 不需要另外安全抓包工具)
通常,在生产环境中,为了保证系统环境的安全和纯粹,是不建议安装其它软件或排查工具(如果可以安装,也是需要走审批流程)。 本文将介绍一种,不用安装Wireshark / tcpdump 等工具,使用Windows系统自带的 netsh trace 命令来获取网络包的步骤
496 32