Resurgam的文章

  • 基于TDI实现的网络通信

    1.概述在Windows操作系统下提供了两种网络编程模式,分别为用户模式和内核模式。顾名思义,用户模式是主要是通过调用用户层的API接口函数实现的用户程序,内核模式主要是通过调用内核层的内核API函数接口或是自定义构造通信协议实现的内核驱动程序。用户模式虽然易于开发,但是容易被监控;内核模式实现较为复杂,但是实现更为底层,通信更为隐蔽,较难监控。
    内核模式下的网络编程,主要是通过传输数据接口TDI(Transport Data Interface)和网络驱动接口规范NDIS(Network Driver Interface Specification)实现的。TDI是直接使用现有的TCP/IP协议来通信的,无需自己重新编写新的协议,所以,基于TDI开发的网络通信是能够被网络防火墙检测到的。而NDIS可以直接在网络上读写原始报文,需要自定义通信协议,能够绕过网络防火墙的检测。
    数据回传对于病毒木马来说是极为关键的一步,稍有差池,则会原形毕露。所以,内核下的网络通信,将会使病毒木马的通信方式更为底层隐蔽,难以检测。
    接下来,本文将介绍基于TDI实现的TCP网络通信,实现一个驱动客户端程序,能够与用户层的服务端程序建立TCP连接并使用TCP通信。
    2.实现过程前面用户篇的时候介绍过Socket编程之TCP通信的小程序,Socket编程中调用的API函数接口比较容易理解,然而,内核下的TDI并没有现成封装好的函数接口供程序调用。为了方便读者类比Socket编程来理解TDI编程,所以,接下来将基于TDI实现TCP客户端的实现原理分成5个部分来介绍,分别为:TDI初始化、TDI TCP连接、TDI TCP数据发送、TDI TCP数据接收以及TDI关闭。
    2.1 TDI初始化在调用TDI实现TCP数据通信之前,需要先初始化TDI操作。初始化操作主要包括创建本地地址对象、创建端点对象以及将端点对象与本地地址对象进行关联。那么,具体的TDI初始化实现步骤如下所示。
    首先,在创建本地地址对象之前,先构建本地地址拓展属性结构PFILE_FULL_EA_INFORMATION。主要设置该拓展属性结构的名称为TdiTransportAddress,拓展属性结构的内容是TA_IP_ADDRESS,里面存储着通信协议类型、本地IP地址及端口等信息。TA_IP_ADDRESS中的AddressType表示通信协议类型,TDI_ADDRESS_TYPE_IP则支持UDP和TCP等IP协议。将IP地址以及端口都置为0,表示本机本地IP地址和随机端口。
    在构建本地地址拓展属性结构完成之后,就可以调用ZwCreateFile函数来根据本地地址拓展属性结构创建本地地址对象。该函数中,打开的设备名称为“\\Device\\Tcp”,即打开TCP设备驱动服务。ZwCreateFile函数中的重要参数是拓展属性(Extended Attributes),通过拓展属性可以向其他的驱动程序传递信息。所以,驱动程序会将本地地址拓展属性结构的数据传递TCP设备驱动,创建本地地址对象,并获取对象句柄。在获取本地地址对象句柄后,通过调用ObReferenceObjectByHandle函数来获取本地地址对象的文件对象,并根据得到的文件对象调用IoGetRelatedDeviceObject获取对应的本地地址对象的驱动设备指针,以方便后续的操作。
    然后,便可以开始创建端点对象。同样的,在创建端点对象之前,先构建上下文拓展属性结构PFILE_FULL_EA_INFORMATION。主要设置该拓展属性结构的名称为TdiConnectionContext,拓展属性结构的内容为CONNECTION_CONTEXT。本文并没有用到CONNECTION_CONTEXT结构里的数据,所以都置为0。
    构建上下文拓展属性结构完成后,同样是调用ZwCreateFile函数根据上下文拓展属性结构来创建端点对象。仍是打开TCP设备驱动服务,向TCP设备驱动传递上下文结构数据,创建端点对象,并获取端点对象句柄。在获取端点对象句柄之后,直接调用ObReferenceObjectByHandle函数来获取端点对象的文件对象,以方便后续的操作。
    最后,创建了本地地址对象和端点对象后,需要将两者关联起来,没有关联地址的端点没有任何用处。其中,本地地址对象存储的信息向系统表明驱动程序使用的本地IP地址和端口。直接调用TdiBuildInternalDeviceControlIrp函数创建TDI的I/O请求包IRP,消息类型为TDI_ASSOCIATE_ADDRESS,表示端点对象关联本地地址对象;需要用到上述获取的本地地址驱动设备对象指针以及端点文件对象指针作为参数。TdiBuildInternalDeviceControlIrp实际是一个宏,它内部调用了IoBuildDeviceIoControlRequest,给这个宏的一些参数实际被忽略了。所以,再调用TdiBuildAssociateAddress宏,将上述获取的本地地址驱动设备对象指针以及端点文件对象指针添加到IRP的I/O堆栈空间中。
    完成上述3个操作之后,就可以调用IoCallDriver函数向驱动设备发送TDI的I/O请求包IRP。其中,驱动程序需要等待系统执行IRP,所以,需要调用IoSetCompletionRoutine设置完成回调函数,通知程序IRP执行完成。这样,TDI的初始化操作到此结束了。
    那么,TDI初始化的具体实现代码如下所示。
    // TDI初始化设置NTSTATUS TdiOpen(PDEVICE_OBJECT *ppTdiAddressDevObj, PFILE_OBJECT *ppTdiEndPointFileObject, HANDLE *phTdiAddress, HANDLE *phTdiEndPoint){ DbgPrint("Enter OpenTdi\n"); NTSTATUS status = STATUS_UNSUCCESSFUL; PFILE_FULL_EA_INFORMATION pAddressEaBuffer = NULL; ULONG ulAddressEaBufferLength = 0; PTA_IP_ADDRESS pTaIpAddr = NULL; UNICODE_STRING ustrTDIDevName; OBJECT_ATTRIBUTES ObjectAttributes = { 0 }; IO_STATUS_BLOCK iosb = { 0 }; HANDLE hTdiAddress = NULL; PFILE_OBJECT pTdiAddressFileObject = NULL; PDEVICE_OBJECT pTdiAddressDevObj = NULL; PFILE_FULL_EA_INFORMATION pContextEaBuffer = NULL; ULONG ulContextEaBufferLength = 0; HANDLE hTdiEndPoint = NULL; PFILE_OBJECT pTdiEndPointFileObject = NULL; KEVENT irpCompleteEvent = { 0 }; PIRP pIrp = NULL; do { // 为本地地址拓展属性结构申请内存及初始化 ulAddressEaBufferLength = sizeof(FILE_FULL_EA_INFORMATION) + TDI_TRANSPORT_ADDRESS_LENGTH + sizeof(TA_IP_ADDRESS); pAddressEaBuffer = (PFILE_FULL_EA_INFORMATION)ExAllocatePool(NonPagedPool, ulAddressEaBufferLength); if (NULL == pAddressEaBuffer) { ShowError("ExAllocatePool[Address]", 0); break; } RtlZeroMemory(pAddressEaBuffer, ulAddressEaBufferLength); RtlCopyMemory(pAddressEaBuffer->EaName, TdiTransportAddress, (1 + TDI_TRANSPORT_ADDRESS_LENGTH)); pAddressEaBuffer->EaNameLength = TDI_TRANSPORT_ADDRESS_LENGTH; pAddressEaBuffer->EaValueLength = sizeof(TA_IP_ADDRESS); // 初始化本机IP地址与端口 pTaIpAddr = (PTA_IP_ADDRESS)((PUCHAR)pAddressEaBuffer->EaName + pAddressEaBuffer->EaNameLength + 1); pTaIpAddr->TAAddressCount = 1; pTaIpAddr->Address[0].AddressLength = TDI_ADDRESS_LENGTH_IP; pTaIpAddr->Address[0].AddressType = TDI_ADDRESS_TYPE_IP; pTaIpAddr->Address[0].Address[0].sin_port = 0; // 0表示本机任意随机端口 pTaIpAddr->Address[0].Address[0].in_addr = 0; // 0表示本机本地IP地址 RtlZeroMemory(pTaIpAddr->Address[0].Address[0].sin_zero, sizeof(pTaIpAddr->Address[0].Address[0].sin_zero)); // 创建TDI驱动设备字符串与初始化设备对象 RtlInitUnicodeString(&ustrTDIDevName, COMM_TCP_DEV_NAME); InitializeObjectAttributes(&ObjectAttributes, &ustrTDIDevName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); // 根据本地地址拓展属性结构创建本地地址对象 status = ZwCreateFile(&hTdiAddress, GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, &ObjectAttributes, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN, 0, pAddressEaBuffer, ulAddressEaBufferLength); if (!NT_SUCCESS(status)) { ShowError("ZwCreateFile[Address]", status); break; } // 根据本地地址对象句柄获取对应的本地地址文件对象 status = ObReferenceObjectByHandle(hTdiAddress, FILE_ANY_ACCESS, 0, KernelMode, &pTdiAddressFileObject, NULL); if (!NT_SUCCESS(status)) { ShowError("ObReferenceObjectHandle[EndPoint]", status); break; } // 获取本地地址文件对象对应的驱动设备 pTdiAddressDevObj = IoGetRelatedDeviceObject(pTdiAddressFileObject); if (NULL == pTdiAddressDevObj) { ShowError("IoGetRelatedDeviceObject", 0); } // 为上下文拓展属性申请内存并初始化 ulContextEaBufferLength = FIELD_OFFSET(FILE_FULL_EA_INFORMATION, EaName) + TDI_CONNECTION_CONTEXT_LENGTH + 1 + sizeof(CONNECTION_CONTEXT); pContextEaBuffer = (PFILE_FULL_EA_INFORMATION)ExAllocatePool(NonPagedPool, ulContextEaBufferLength); if (NULL == pContextEaBuffer) { ShowError("ExAllocatePool[EndPoint]", 0); break; } RtlZeroMemory(pContextEaBuffer, ulContextEaBufferLength); RtlCopyMemory(pContextEaBuffer->EaName, TdiConnectionContext, (1 + TDI_CONNECTION_CONTEXT_LENGTH)); pContextEaBuffer->EaNameLength = TDI_CONNECTION_CONTEXT_LENGTH; pContextEaBuffer->EaValueLength = sizeof(CONNECTION_CONTEXT); // 根据上下文创建TDI端点对象 status = ZwCreateFile(&hTdiEndPoint, GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, &ObjectAttributes, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN, 0, pContextEaBuffer, ulContextEaBufferLength); if (!NT_SUCCESS(status)) { ShowError("ZwCreateFile[EndPoint]", status); break; } // 根据TDI端点对象句柄获取对应的端点文件对象 status = ObReferenceObjectByHandle(hTdiEndPoint, FILE_ANY_ACCESS, 0, KernelMode, &pTdiEndPointFileObject, NULL); if (!NT_SUCCESS(status)) { ShowError("ObReferenceObjectHandle[EndPoint]", status); break; } // 设置事件 KeInitializeEvent(&irpCompleteEvent, NotificationEvent, FALSE); // 将TDI端点与本地地址对象关联, 创建TDI的I/O请求包:TDI_ASSOCIATE_ADDRESS pIrp = TdiBuildInternalDeviceControlIrp(TDI_ASSOCIATE_ADDRESS, pTdiAddressDevObj, pTdiEndPointFileObject, &irpCompleteEvent, &iosb); if (NULL == pIrp) { ShowError("TdiBuildInternalDeviceControlIrp", 0); return STATUS_INSUFFICIENT_RESOURCES; } // 拓展I/O请求包 TdiBuildAssociateAddress(pIrp, pTdiAddressDevObj, pTdiEndPointFileObject, NULL, NULL, hTdiAddress); // 设置完成实例的回调函数 IoSetCompletionRoutine(pIrp, TdiCompletionRoutine, &irpCompleteEvent, TRUE, TRUE, TRUE); // 发送I/O请求包并等待执行 status = IoCallDriver(pTdiAddressDevObj, pIrp); if (STATUS_PENDING == status) { KeWaitForSingleObject(&irpCompleteEvent, Executive, KernelMode, FALSE, NULL); } // 返回数据 *ppTdiAddressDevObj = pTdiAddressDevObj; *ppTdiEndPointFileObject = pTdiEndPointFileObject; *phTdiAddress = hTdiAddress; *phTdiEndPoint = hTdiEndPoint; }while (FALSE); // 释放内存 if (pTdiAddressFileObject) { ObDereferenceObject(pTdiAddressFileObject); // 测试 } if (pContextEaBuffer) { ExFreePool(pContextEaBuffer); } if (pAddressEaBuffer) { ExFreePool(pAddressEaBuffer); } DbgPrint("Leave OpenTdi\n"); return status;}
    2.2 TDI TCP连接在TDI初始化完成之后,驱动程序便可以向TCP服务端程序发送连接请求,并建立TCP连接。主要操作就是构造一个包含服务器IP地址以及监听端口的IRP,再发送给驱动程序执行。那么,基于TDI的TCP连接的具体实现流程如下所示。
    首先,直接调用TdiBuildInternalDeviceControlIrp宏创建IRP,设置IRP的消息类型为TDI_CONNECT,表示建立TCP连接。
    然后,构建TDI连接信息结构TDI_CONNECTION_INFORMATION,主要设置IP地址的相关信息TA_IP_ADDRESS,包括通信协议类型、服务器的IP地址以及服务器监听端口号等。并调用TdiBuildConnect宏将TDI连接信息结构的数据添加到IRP的I/O堆栈空间中。
    最后,调用IoCallDriver函数向驱动程序发送上述构建好的IRP,并创建完成回调函数等待系统处理IRP。系统成功处理完毕后,驱动程序便与服务端程序成功建立TCP连接。
    那么,TCP连接的具体实现代码如下所示。
    // TDI TCP连接服务器NTSTATUS TdiConnection(PDEVICE_OBJECT pTdiAddressDevObj, PFILE_OBJECT pTdiEndPointFileObject, LONG *pServerIp, LONG lServerPort){ DbgPrint("Enter TdiConnection\n"); NTSTATUS status = STATUS_SUCCESS; IO_STATUS_BLOCK iosb = { 0 }; PIRP pIrp = NULL; KEVENT connEvent = { 0 }; TA_IP_ADDRESS serverTaIpAddr = { 0 }; ULONG serverIpAddr = 0; USHORT serverPort = 0; TDI_CONNECTION_INFORMATION serverConnection = { 0 }; // 创建连接事件 KeInitializeEvent(&connEvent, NotificationEvent, FALSE); // 创建TDI连接I/O请求包:TDI_CONNECT pIrp = TdiBuildInternalDeviceControlIrp(TDI_CONNECT, pTdiAddressDevObj, pTdiEndPointFileObject, &connEvent, &iosb); if (NULL == pIrp) { ShowError("TdiBuildInternalDeviceControlIrp_TDI_CONNECT", 0); return STATUS_INSUFFICIENT_RESOURCES; } // 初始化服务器IP地址与端口 serverIpAddr = INETADDR(pServerIp[0], pServerIp[1], pServerIp[2], pServerIp[3]); serverPort = HTONS(lServerPort); serverTaIpAddr.TAAddressCount = 1; serverTaIpAddr.Address[0].AddressLength = TDI_ADDRESS_LENGTH_IP; serverTaIpAddr.Address[0].AddressType = TDI_ADDRESS_TYPE_IP; serverTaIpAddr.Address[0].Address[0].sin_port = serverPort; serverTaIpAddr.Address[0].Address[0].in_addr = serverIpAddr; serverConnection.UserDataLength = 0; serverConnection.UserData = 0; serverConnection.OptionsLength = 0; serverConnection.Options = 0; serverConnection.RemoteAddressLength = sizeof(TA_IP_ADDRESS); serverConnection.RemoteAddress = &serverTaIpAddr; // 把上述的地址与端口信息增加到I/O请求包中,增加连接信息 TdiBuildConnect(pIrp, pTdiAddressDevObj, pTdiEndPointFileObject, NULL, NULL, NULL, &serverConnection, 0); // 设置完成实例回调函数 IoSetCompletionRoutine(pIrp, TdiCompletionRoutine, &connEvent, TRUE, TRUE, TRUE); // 发送I/O请求包并等待执行 status = IoCallDriver(pTdiAddressDevObj, pIrp); if (STATUS_PENDING == status) { KeWaitForSingleObject(&connEvent, Executive, KernelMode, FALSE, NULL); } DbgPrint("Leave TdiConnection\n"); return status;}2.3 TDI TCP数据发送在成功建立TCP连接之后,客户端程序与服务端程序便可以相互通信,进行数据的交互了。基于TDI实现的TCP数据发送主要的操作便是构造一个发送数据的IRP,并向IRP添加发送的数据,然后发送给驱动程序处理即可。具体的实现流程如下所示。
    首先,直接调用TdiBuildInternalDeviceControlIrp宏创建IRP,设置IRP的消息类型为TDI_SEND,表示发送数据。
    然后,需要将发送的数据调用IoAllocateMdl函数来将数据创建一份新的映射并获取分配到的MDL结构,因为驱动程序接下来需要调用TdiBuildSend宏将TDI发送数据的MDL结构数据添加到IRP的I/O堆栈空间中,以此传递发送的数据信息。
    最后,调用IoCallDriver函数向驱动程序发送上述构建好的IRP,并创建完成回调函数等待系统处理IRP。处理完毕后,要记得调用IoFreeMdl函数来释放MDL。
    那么,TCP数据发送的具体实现代码如下所示。
    // TDI TCP发送信息NTSTATUS TdiSend(PDEVICE_OBJECT pTdiAddressDevObj, PFILE_OBJECT pTdiEndPointFileObject, PUCHAR pSendData, ULONG ulSendDataLength){ DbgPrint("Enter TdiSend\n"); NTSTATUS status = STATUS_SUCCESS; KEVENT sendEvent; PIRP pIrp = NULL; IO_STATUS_BLOCK iosb = {0}; PMDL pSendMdl = NULL; // 初始化事件 KeInitializeEvent(&sendEvent, NotificationEvent, FALSE); // 创建I/O请求包:TDI_SEND pIrp = TdiBuildInternalDeviceControlIrp(TDI_SEND, pTdiAddressDevObj, pTdiEndPointFileObject, &sendEvent, &iosb); if (NULL == pIrp) { ShowError("TdiBuildInternalDeviceControlIrp", 0); return STATUS_INSUFFICIENT_RESOURCES; } // 创建MDL pSendMdl = IoAllocateMdl(pSendData, ulSendDataLength, FALSE, FALSE, pIrp); if (NULL == pSendMdl) { ShowError("IoAllocateMdl", 0); return STATUS_INSUFFICIENT_RESOURCES; } MmProbeAndLockPages(pSendMdl, KernelMode, IoModifyAccess); // 拓展I/O请求包,添加发送信息 TdiBuildSend(pIrp, pTdiAddressDevObj, pTdiEndPointFileObject, NULL, NULL, pSendMdl, 0, ulSendDataLength); // 设置完成实例回调函数 IoSetCompletionRoutine(pIrp, TdiCompletionRoutine, &sendEvent, TRUE, TRUE, TRUE); // 发送I/O请求包并等待执行 status = IoCallDriver(pTdiAddressDevObj, pIrp); if (STATUS_PENDING == status) { KeWaitForSingleObject(&sendEvent, Executive, KernelMode, FALSE, NULL); } // 释放MDL if (pSendMdl) { IoFreeMdl(pSendMdl); } DbgPrint("Leave TdiSend\n"); return status;}
    2.4 TDI TCP数据接收基于TDI实现的TCP数据接收具体实现流程和数据发送类似,同样是构造数据接收的IRP,设置接收数据缓冲区,将IRP发送给驱动程序处理即可。具体的数据接收实现流程如下所示。
    首先,直接调用TdiBuildInternalDeviceControlIrp宏创建IRP,设置IRP的消息类型为TDI_RECV,表示发送接收。
    然后,需要将数据接收缓冲区调用IoAllocateMdl函数来将缓冲区创建一份新的映射并获取分配到的MDL结构,因为驱动程序接下来需要调用TdiBuildReceive宏将TDI接收数据缓冲区的MDL结构数据添加到IRP的I/O堆栈空间中,以此传递接收数据缓冲区的信息。
    最后,调用IoCallDriver函数向驱动程序发送上述构建好的IRP,并创建完成回调函数等待系统处理IRP。处理完毕后,要记得调用IoFreeMdl函数来释放MDL。
    那么,TCP数据接收的具体实现代码如下所示。
    // TDI TCP接收信息ULONG_PTR TdiRecv(PDEVICE_OBJECT pTdiAddressDevObj, PFILE_OBJECT pTdiEndPointFileObject, PUCHAR pRecvData, ULONG ulRecvDataLength){ DbgPrint("Enter TdiRecv\n"); NTSTATUS status = STATUS_SUCCESS; KEVENT recvEvent; PIRP pIrp = NULL; IO_STATUS_BLOCK iosb = { 0 }; PMDL pRecvMdl = NULL; ULONG_PTR ulRecvSize = 0; // 初始化事件 KeInitializeEvent(&recvEvent, NotificationEvent, FALSE); // 创建I/O请求包:TDI_SEND pIrp = TdiBuildInternalDeviceControlIrp(TDI_RECV, pTdiAddressDevObj, pTdiEndPointFileObject, &recvEvent, &iosb); if (NULL == pIrp) { ShowError("TdiBuildInternalDeviceControlIrp", 0); return STATUS_INSUFFICIENT_RESOURCES; } // 创建MDL pRecvMdl = IoAllocateMdl(pRecvData, ulRecvDataLength, FALSE, FALSE, pIrp); if (NULL == pRecvMdl) { ShowError("IoAllocateMdl", 0); return STATUS_INSUFFICIENT_RESOURCES; } MmProbeAndLockPages(pRecvMdl, KernelMode, IoModifyAccess); // 拓展I/O请求包,添加发送信息 TdiBuildReceive(pIrp, pTdiAddressDevObj, pTdiEndPointFileObject, NULL, NULL, pRecvMdl, TDI_RECEIVE_NORMAL, ulRecvDataLength); // 设置完成实例回调函数 IoSetCompletionRoutine(pIrp, TdiCompletionRoutine, &recvEvent, TRUE, TRUE, TRUE); // 发送I/O请求包并等待执行 status = IoCallDriver(pTdiAddressDevObj, pIrp); if (STATUS_PENDING == status) { KeWaitForSingleObject(&recvEvent, Executive, KernelMode, FALSE, NULL); } // 获取实际接收的数据大小 ulRecvSize = pIrp->IoStatus.Information; // 释放MDL if (pRecvMdl) { IoFreeMdl(pRecvMdl); } DbgPrint("Leave TdiRecv\n"); return status;}
    2.5 TDI关闭所谓的关闭TDI,主要是负责资源数据的释放和清理工作。通过调用ObDereferenceObject函数释放端点文件对象资源,调用ZwClose函数关闭端点对象句柄以及本地地址对象句柄。
    那么,TDI关闭的具体实现代码如下所示。
    // TDI关闭释放VOID TdiClose(PFILE_OBJECT pTdiEndPointFileObject, HANDLE hTdiAddress, HANDLE hTdiEndPoint){ DbgPrint("Enter TdiClose\n"); if (pTdiEndPointFileObject) { ObDereferenceObject(pTdiEndPointFileObject); } if (hTdiEndPoint) { ZwClose(hTdiEndPoint); } if (hTdiAddress) { ZwClose(hTdiAddress); } DbgPrint("Leave TdiClose\n");}
    3.测试在64位Windows 10操作系统上,先运行TCP服务端程序ChatServer.exe,设置服务端程序的IP地址以及监听端口分别为127.0.0.1和12345,并开始进行监听。然后,直接加载并运行上述驱动程序,连接监听状态的服务端程序,连接成功,并成功向服务端程序发送数据“I am Demon`Gan—->From TDI”。服务端程序成功与驱动程序建立TCP连接,并成功接收来自驱动程序发送的数据。处于用户层的服务端程序向驱动程序发送数据“nice to meet you, Demon”,驱动程序也能成功接收。所以,基于TDI通信的驱动程序测试成功,如图7-3所示。

    打开cmd.exe命令行窗口后,输入命令netstat -ano来查看网络连接情况以及对应的进程PID,执行命令的结果如图7-4所示,从中可以知道,与服务端程序建立通信连接的进程PID为4,即system.exe进程,因为是驱动程序与用户程序建立的TCP连接,所以,进程PID显示为4。

    4.小结基于TDI的TCP客户端的实现原理实际上是通过构造不同信息的TDI的I/O请求包IRP,携带不同的参数数据,发送给驱动函数进行处理实现的。实现该程序的关键,则是在于TDI的I/O请求包IRP的构建上。
    其中,在通信的过程中,要注意及时调用IoFreeMdl函数来释放创建的MDL。同时,驱动程序可以通过调用PsCreateSystemThread函数创建一个多线程,循环接收来自服务端程序的数据。
    0  留言 2021-08-30 11:08:21
  • docker容器网络设置详解

    问题描述docker 中有的时候需要从容器内向外网环境进行访问,这个时候我边出现了一个诡异的问题,从容器的宿主机直接通过 curl 命令使用域名可以正常的访问并返回正确的解决,但是从容器中向外调用外网环境的这个域名的时候,curl 命令会被卡住,一直到超时都没有任何返回数据,但是一个很神奇的事情是,如果将该域名换成对应的 IP 的时候,从容器内向外调用外网环境的这个 IP 的时候,这个时候就可以正确的返回结果。
    问题解析出现了上面的问题,通过查看调用命令返回的结果,发现出现该问题的主要原因是由于在域名调用的过程中会出现在容器内域名无法解析的问题.
    解决方案如果是通过 run 命令来启动容器的话,只需要增加 —net=host 这个参数,具体命令如下所示:
    docker run -d --net=host --name nginx-1 nginx:latest
    下面我们来讲解一下上面的这个启动命令中增加 -–net=host 这个配置参数的作用,该配置的作用主要是表明该容器不会虚拟自己的网卡,配置自己的 IP,而是使用宿主机的 IP 和端口,所以使用这个命令的时候,docker run 命令中的 -p 也会失效,因为它本来就使用的是宿主机的 IP 和端口,根本无需进行端口映射,通过上面的配置,我们便可以从容器内通过域名正常的访问外网环境啦~
    相关拓展通过上面碰到的这个问题,我们知道了 docker 容器网络模式相关的一些东西,接下来我们就来拓展一下 docker 网络模式的详细内容。
    docker 自身有四种网络模式,还有一些自定义的网络模式,我们今天主要来了解一下自身的四种网络模式:

    Host:容器使用宿主机的网卡是 IP 端口,不会虚拟自己的网卡,也不会配置自己的 IP
    docker run --net=host
    Container:容器不会虚拟自己的网卡,也不会配置自己的 IP,而是和一个指定容器共享 IP 和端口范围
    docker run --net=container:containerName/containerId
    None:关闭容器网络功能
    docker run --net=none
    Bridge:该模式会为每一个容器虚拟网卡并设置IP,并将容器连接到宿主机中创建好的 docker0 虚拟网桥,通过 docker0 网桥以及 Iptables nat 表配置与宿主机通信
    docker run --net=bridge

    在我们安装 docker 的过程中,docker会自动创建三个网络模式,通过下面的命令即可看到:
    docker network ls
    输出结果如下所示:
    NETWORK ID NAME DRIVER SCOPE1c175577e460 bridge bridge local2b90b47781d5 host host local8469af2f3d91 none null local在启动容器的时候,我们可以通过 –-net 参数来指定使用哪种网络模式,默认 docker 容器会使用 bridge 网络模式。
    通过下面的几个命令,我们可以查看每一种网络往事的详细信息:

    查看 bridge 网络模式详情
    docker network inspect bridge
    查看 host 网络模式详情
    docker network inspect host
    查看 none 网络模式详情
    docker network inspect none
    0  留言 2021-08-05 10:07:39
  • 利用inf2cat根据inf文件生成cat文件

    背景Inf2Cat (Inf2Cat.exe) 是一个命令行工具,该工具确定驱动程序包的 INF 文件是否可以针对指定的 Windows 版本列表进行数字签名。如果可以,那么 Inf2Cat 会生成适用于指定 Windows 版本的未签名的目录文件 CAT。
    Inf2Cat 工具检查驱动程序包的 INF 文件是否存在结构错误,并验证驱动程序包是否可以进行数字签名。只有当 INF 文件中引用的所有文件都存在并且源文件位于正确的位置时,驱动程序包才能被签名。如果 INF 文件无法签名或包含结构错误,驱动程序包可能未正确安装,或者可能在安装期间错误地显示驱动程序签名警告对话框。
    对于 WDM 驱动程序的安装,都需要用到 INF 文件。其中,有些 WDM 驱动安装,需要根据 INF 生成 CAT 文件,这样,驱动程序方可安装,例如键盘、鼠标等设备驱动程序。
    本文主要介绍使用 WDK 自带的 Inf2Cat.exe 工具,根据驱动程序的 INF 文件,生成 CAT 文件。
    实现过程Inf2Cat.exe 的使用命令为:
    Inf2Cat /driver:PackagePath/os:WindowsVersionList [/nocat] [/verbose] [other switches]
    其中,参数的含义为:

    /driver:PackagePath:指定包含驱动程序包的 INF 文件的目录路径。如果指定的目录包含多个驱动程序包的 INF 文件,那么 Inf2Cat 会为每个驱动程序包创建目录文件注意:可以使用 /drv: 开关来代替 /driver: 开关
    /os:WindowsVersionList:将 Inf2Cat 配置为验证驱动程序包的 INF 文件是否符合由 WindowsVersionList 指定的 Windows 版本的签名要求。WindowsVersionList 是一个逗号分隔列表,其中包含以下一个或多个版本标识符




    Windows version
    Version identifier




    Windows 8.1 x86 Edition
    6_3_X86


    Windows 8.1 x64 Edition
    6_3_X64


    Windows 8.1 ARM Edition
    6_3_ARM


    Windows Server 2012 R2
    Server6_3_X64


    Windows 8 x64 Edition
    8_X64


    Windows 8 x86 Edition
    8_X86


    Windows 8 ARM Edition
    8_ARM


    Windows Server 2012
    Server8_X64


    Windows Server 2008 R2 x64 Edition
    Server2008R2_X64


    Windows Server 2008 R2 Itanium Edition
    Server2008R2_IA64


    Windows 7 x64 Edition
    7_X64


    Windows 7 x86 Edition
    7_X86


    Windows Server 2008 x64 Edition
    Server2008_X64


    Windows Server 2008 Itanium Edition
    Server2008_IA64


    Windows Server 2008 x86 Edition
    Server2008_X86


    Windows Vista x64 Edition
    Vista_X64


    Windows Vista x86 Edition
    Vista_X86


    Windows Server 2003 x64 Edition
    Server2003_X64


    Windows Server 2003 Itanium Edition
    Server2003_IA64


    Windows Server 2003 x86 Edition
    Server2003_X86


    Windows XP x64 Edition
    XP_X64


    Windows XP x86 Edition
    XP_X86


    Windows 2000
    2000



    注意:说明从 Windows Server 2008 R2 开始,Windows Server 操作系统将不再支持基于 x86 的平台。
    Inf2Cat 忽略版本标识符字符串的字母字符的大小写。 例如,vista_x64 和 Vista_X64 都是 Windows Vista x64 Edition 的有效标识符。

    /nocat:将 Inf2Cat 配置为验证驱动程序包是否符合指定的 Windows 版本的签名要求,而不生成目录文件
    /verbose:将 Inf2Cat 配置为在命令窗口中显示详细信息
    other switches:将 Inf2Cat 配置为向文件中添加 DRM 级别目录属性或 PE 目录属性或者向文件中添加页面哈希。若要获得详细信息,请使用 /? 开关

    程序测试现在,我们对 C:\Users\用户名\Desktop\DriverTest 目录下的 DriverTest.inf 和 DriverTest.sys 驱动文件生成对应的 .cat 文件。其中,该程序包的 inf 文件中的 inf 版本部分仅包含以下 CatalogFile 指令:
    [Version]. . .CatalogFile = DriverTest.cat. . .
    对于该示例,以下 Inf2Cat 命令将验证是否可以针对 Win7、Win8、Win8.1 的 64 位版本,为驱动程序包进行签名。如果可以针对这些版本对程序包进行签名,那么 Inf2Cat 将创建未签名的目录文件 DriverTest.cat。
    在运行窗口中输入 cmd,打开命令行窗口,依次输入下述命令:
    // 切换到 WDK 中的 inf2cat.exe 程序目录cd C:\Program Files (x86)\Windows Kits\8.1\bin\x86// 运行 inf2cat.exe 生成 cat 文件inf2cat.exe /driver:C:\Users\用户名\Desktop\DriverTest /os:7_x64,8_x64,6_3_X64
    其中,C:\Program Files (x86)\Windows Kits\8.1\bin\x86 是 inf2cat.exe 程序所在的目录路径;C:\Users\用户名\Desktop\DriverTest 为程序包路径。
    运行后,成功生成 drivertest.cat 文件。运行结果如下所示:

    总结在使用 Inf2Cat.exe 程序生成 cat 文件的时候,要求 inf 文件中,Version 版本部分一定指定 CatalogFile 文件。否则,Inf2Cat 会报错。
    同时,也要注意系统的版本的标识符,可以参考上述表格。
    特别要注意的是,当我们对驱动程序签名的时候,除了对 .sys 驱动程序签名之外,还需要对生成的 .cat 文件进行签名。否则,加载驱动会出错。
    1  留言 2021-05-13 08:47:21
  • 基于 WFP 实现的网络监控

    WFP 全称 Windows Filtering Platform,即 Windows 过滤平台。随着网络的高速发展,网络安全问题越来越受到重视,同时随着 WindowsOS 的快速更新换代,以往的网络过滤框架已经不能满足需要,于是导致了 WFP 的出现。WFP 是 VISTA 中引入的 API 集,也是从 VISTA 系统后新增的一套系统 API 和服务,在新版的操作系统中,开发人员可以通过这套 API 集将 Windows 防火墙嵌入到开发软件中,可以恰到好去的处理 Windows 防火墙的一些设置。
    WFP 为网络数据包过滤提供了架构支持,是微软在 VISTA 之后,替代之前的基于包过滤的防火墙设计,如 Transport DriverInterface(TDI)过滤、Network Driver InterfaceSpecification(NDIS)过滤、Winsocklayered Service Providers(LSP)。
    在 VISITA 及以后的系统中,系统防火墙的过滤钩子驱动不再适用,只能使用 WFP。WFP 允许程序员编写代码和操作系统的网络协议栈进行交互,同时在网络数据到达最后的归宿前,将数据进行过滤,拦截,修改等。流程如下图所示。

    Filter Engine 是 WFP 的核心组件,用来过滤 TCP/IP 协议的网络数据。在 TCP/IP 协议栈中存在 Filtering Layer,把网络数据传递到 Filter Engine 中处理。如果 Filtering Layer 中 Filter 的所有过滤条件都满足,Filter Engine 就会执行 Filter 指定的过滤操作。其中,Filter 可以指定 Callout 去完成特定的过滤操作。Callout 是 WFP 的功能拓展,驱动程序需要将 Callout 注册到 Filter Engine 中,这样 Filter Engine 才能调用 Callout 函数去处理网络数据。
    接下来,本文将介绍基于 WFP 实现监控系统上网络连接情况,并阻止指定进程建立通信连接。
    实现过程在调用 WFP 函数开发程序之前,先来介绍下程序所需要包含的头文件以及导入的库函数。
    要使用 WFP 框架,就需要向驱动程序中加入头文件以及导入库文件,头文件有:
    #include <fwpsk.h>#include <fwpmk.h>
    在链接器中添加库文件 fwpkclnt.lib 和 uuid.lib 库文件:
    属性-->链接器-->输入-->附加依赖库,添加fwpkclnt.lib和uuid.lib库文件
    由于程序使用的是 NDIS6,所以,需要在预处理器中添加预处理指令:
    属性-->C/C++ -->预处理器,添加“NDIS_SUPPORT_NDIS6”
    经过上述的设置,接下来,就可以进行 WFP 开发了。
    在驱动程序创建好驱动设备之后,就可以调用 FwpsCalloutRegister 函数向 Filter Engine 注册一个 Callout,即使 Filter Engine 还没有启动。FwpsCalloutRegister 函数的最后一个参数是一个 GUID 的数据类型,该数值表示 Callout 的 Key,代表了一个 Callout,具有唯一性。
    其中,WFP 一次性要注册的 Callout 函数不是 1 个,而是 3 个:

    notifyFn:负责处理 notifications
    classifyFn:负责处理 classifications
    flowDeleteFn:负责处理 flow deletions,是可选的

    为了便于理解,可以认为 Callout 函数相当于回调函数,classifyFn 相当于 pre 事前回调,notifyFn 和 flowDeleteFn 相当于事后回调函数。
    WFP API 是面向会话(Session)的,大多数函数调用是在会话的上下文中进行。驱动程序可以通过调用 FwpmEngineOpen 函数创建新会话,调用 FwpmEngineClose 函数来结束会话。
    WFP API 同时具有事务性,大多数函数调用是在事务的上下文中进行。驱动程序可以调用 FwpmTransactionBegin 函数开始事务,调用 FwpmTransactionCommit 函数提交事务,调用 FwpmTransactionAbort 来终止事务。
    驱动程序中,每个会话只能进行一个事务。如果在第一个事务提交或者中止之前就开始第二个事务,程序则会返回错误。
    那么,一个 WFP 框架驱动程序大体是这样子的:

    首先,调用 FwpsCalloutRegister 函数根据驱动设备对象向 Filter Engine 注册一个 Callout,指明 Callout Key 以及 3 个 Callout 函数 notifyFn、classifyFn 和 flowDeleteFn
    然后,调用 FwpmEngineOpen 函数创建一个 WFP 会话句柄,并调用 FwpmTransactionBegin 函数开始事务
    接着,创建过滤点。先调用 FwpmCalloutAdd 函数将前面注册好的 Callout 添加到会话,注意 Callout Key 要保持一致;再调用 FwpmFilterAdd 函数添加 Filter,注意设置过滤层和 Callout Key。本文要实现的是过滤进程联网的功能,而且联网一般都是用 IPV4 协议,所以过滤条件标志设置为 FWPM_LAYER_ALE_AUTH_CONNECT_V4。同时,必须为这个过滤条件标志指定一个 GUID,该 GUID 值任意,只要在系统范围内不重复
    最后,调用 FwpmTransactionCommit 函数提交事务,让上述操作开始生效

    经过上述 4 个步骤,就可以完成 Callout 的注册以及设置过滤条件。当满足所有过滤条件的数据包出现的时候,系统便会调用 Callout 函数 notifyFn 进行处理。
    当程序成功注册回调函数之后,就可以在 notifyFn 函数中实现对网络连接情况进行监控,还能对连接进行控制。其中,回调函数第 1 个参数 FWPS_INCOMING_VALUES0 中存储着网络连接的 IP、端口、协议等信息;第 2 个参数 FWPS_INCOMING_METADATA_VALUES0 存储着进程 ID、路径等信息;第 3 个参数 FWPS_CLASSIFY_OUT0 还可以控制是允许连接还是拒绝连接。
    当程序不用 WFP 的时候,就要调用 FwpmFilterDeleteById、FwpmCalloutDeleteById 以及 FwpsCalloutUnregisterById 函数把添加的过滤器对象和回调函数删除掉,并调用 FwpmEngineClose 关闭 WFP 会话。
    注册 Callout 的实现代码如下所示。
    // 注册CalloutNTSTATUS RegisterCallout( PDEVICE_OBJECT pDevObj, IN const GUID *calloutKey, IN FWPS_CALLOUT_CLASSIFY_FN classifyFn, IN FWPS_CALLOUT_NOTIFY_FN notifyFn, IN FWPS_CALLOUT_FLOW_DELETE_NOTIFY_FN flowDeleteNotifyFn, OUT ULONG32 *calloutId){ NTSTATUS status = STATUS_SUCCESS; FWPS_CALLOUT sCallout = { 0 }; // 设置Callout sCallout.calloutKey = *calloutKey; sCallout.classifyFn = classifyFn; sCallout.flowDeleteFn = flowDeleteNotifyFn; sCallout.notifyFn = notifyFn; // 注册Callout status = FwpsCalloutRegister(pDevObj, &sCallout, calloutId); if (!NT_SUCCESS(status)) { ShowError("FwpsCalloutRegister", status); return status; } return status;}
    创建过滤点的实现代码如下所示。
    // 设置过滤点NTSTATUS SetFilter( IN const GUID *layerKey, IN const GUID *calloutKey, OUT ULONG64 *filterId, OUT HANDLE *engine){ HANDLE hEngine = NULL; NTSTATUS status = STATUS_SUCCESS; FWPM_SESSION session = { 0 }; FWPM_FILTER mFilter = { 0 }; FWPM_CALLOUT mCallout = { 0 }; FWPM_DISPLAY_DATA mDispData = { 0 }; // 创建Session session.flags = FWPM_SESSION_FLAG_DYNAMIC; status = FwpmEngineOpen(NULL, RPC_C_AUTHN_WINNT, NULL, &session, &hEngine); if (!NT_SUCCESS(status)) { ShowError("FwpmEngineOpen", status); return status; } // 开始事务 status = FwpmTransactionBegin(hEngine, 0); if (!NT_SUCCESS(status)) { ShowError("FwpmTransactionBegin", status); return status; } // 设置Callout参数 mDispData.name = L"MY WFP TEST"; mDispData.description = L"WORLD OF DEMON"; mCallout.applicableLayer = *layerKey; mCallout.calloutKey = *calloutKey; mCallout.displayData = mDispData; // 添加Callout到Session中 status = FwpmCalloutAdd(hEngine, &mCallout, NULL, NULL); if (!NT_SUCCESS(status)) { ShowError("FwpmCalloutAdd", status); return status; } // 设置过滤器参数 mFilter.action.calloutKey = *calloutKey; mFilter.action.type = FWP_ACTION_CALLOUT_TERMINATING; mFilter.displayData.name = L"MY WFP TEST"; mFilter.displayData.description = L"WORLD OF DEMON"; mFilter.layerKey = *layerKey; mFilter.subLayerKey = FWPM_SUBLAYER_UNIVERSAL; mFilter.weight.type = FWP_EMPTY; // 添加过滤器 status = FwpmFilterAdd(hEngine, &mFilter, NULL, filterId); if (!NT_SUCCESS(status)) { ShowError("FwpmFilterAdd", status); return status; } // 提交事务 status = FwpmTransactionCommit(hEngine); if (!NT_SUCCESS(status)) { ShowError("FwpmTransactionCommit", status); return status; } *engine = hEngine; return status;}
    notifyFn 函数的实现代码如下所示。
    // 回调函数#if (NTDDI_VERSION >= NTDDI_WIN8) VOID NTAPI classifyFn( _In_ const FWPS_INCOMING_VALUES0* inFixedValues, _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_opt_ void* layerData, _In_opt_ const void* classifyContext, _In_ const FWPS_FILTER2* filter, _In_ UINT64 flowContext, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut )#elif (NTDDI_VERSION >= NTDDI_WIN7) VOID NTAPI classifyFn( _In_ const FWPS_INCOMING_VALUES0* inFixedValues, _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_opt_ void* layerData, _In_opt_ const void* classifyContext, _In_ const FWPS_FILTER1* filter, _In_ UINT64 flowContext, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut )#else VOID NTAPI classifyFn( _In_ const FWPS_INCOMING_VALUES0* inFixedValues, _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_opt_ void* layerData, _In_ const FWPS_FILTER0* filter, _In_ UINT64 flowContext, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut )#endif{ /* 。WFP 的回调函数里提供了丰富的信息,这是 WFP 最大的优点, 不用我们为获得各种相关信息而绞尽脑汁。 比如在 FWPM_LAYER_ALE_AUTH_CONNECT_V4 的回调函数里,我们能获得进程 ID、进程路径、本地、远 程的 IP 地址/端口号以及协议代码。 但最爽的是此回调函数的最后一个参数,能让我们指定一个值,决定是 放行还是拦截. */ ULONG ulLocalIp = inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_ADDRESS].value.uint32; UINT16 uLocalPort = inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_PORT].value.uint16; ULONG ulRemoteIp = inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_ADDRESS].value.uint32; UINT16 uRemotePort = inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_PORT].value.uint16; KIRQL kCurrentIrql = KeGetCurrentIrql(); ULONG64 processId = inMetaValues->processId; UCHAR szProcessPath[256] = { 0 }; CHAR szProtocalName[256] = { 0 }; RtlZeroMemory(szProcessPath, 256); ULONG i = 0; // 获取进程路径 for (i = 0; i < inMetaValues->processPath->size; i++) { // 里面是宽字符存储的 szProcessPath[i] = inMetaValues->processPath->data[i]; } // 允许连接 classifyOut->actionType = FWP_ACTION_PERMIT; // 禁止指定进程网络连接 if (NULL != wcsstr((PWCHAR)szProcessPath, L"tcpclient.exe")) { KdPrint(("TCPClient.exe[FWP_ACTION_BLOCK]\n")); // 拒绝连接 classifyOut->actionType = FWP_ACTION_BLOCK; classifyOut->rights = classifyOut->rights & (~FWPS_RIGHT_ACTION_WRITE); classifyOut->flags = classifyOut->flags | FWPS_CLASSIFY_OUT_FLAG_ABSORB; } // 协议判断 ProtocalIdToName(inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_PROTOCOL].value.uint16, szProtocalName); // 显示 DbgPrint("Protocal=%s, LocalIp=%u.%u.%u.%u:%d, RemoteIp=%u.%u.%u.%u:%d, IRQL=%d, PID=%I64d, Path=%S\n", szProtocalName, (ulLocalIp >> 24) & 0xFF, (ulLocalIp >> 16) & 0xFF, (ulLocalIp >> 8) & 0xFF, (ulLocalIp)& 0xFF, uLocalPort, (ulRemoteIp >> 24) & 0xFF, (ulRemoteIp >> 16) & 0xFF, (ulRemoteIp >> 8) & 0xFF, (ulRemoteIp)& 0xFF, uRemotePort, kCurrentIrql, processId, (PWCHAR)szProcessPath);}
    测试在 64 位 Windows 10 系统下,直接加载并运行上述驱动程序,成功获取计算机上的网络连接情况,并成功阻止 TCPClient.exe 进程的网络连接,如下图所示。

    小结WFP 实现起来比较复杂,建议大家一边阅读配套的示例代码,一边结合本文的讲解来理解。
    在 WFP 程序开发之前,项目工程要记得包含 fwpsk.h 头文件以及 fwpmk.h 头文件,在链接器中导入 fwpkclnt.lib 以及 uuid.lib 库文件,同时在预处理器中添加 NDIS_SUPPORT_NDIS6 宏,以支持 NDIS6 的使用。
    对于 WFP 框架的实现流程较为固定,主要包括打开 WFP 引擎会话、确认引擎的过滤权限、注册回调函数以及提交事务并启动回调等。在注册回调函数的过程中,要注意指明过滤条件。
    0  留言 2021-05-12 09:22:40
  • 使用ChangeScreenWidthHeight实现更改屏幕分辨率

    背景之前,帮别人写过一个小程序,其中,程序有个功能就是要更改电脑的分辨率。后来了解到,可以使用 ChangeDisplaySettings 函数去实现这个操作。现在,我就把实现的原理和过程,写成文档,分享给大家。
    函数介绍ChangeDisplaySettings 函数
    把缺省显示设备的设置改变为由 LPDEVMODE 设定的图形模式。
    函数声明
    LONG ChangeDisplaySettings ( LPDEVMODE lpDevMode, DWORD dwflags );
    参数

    lpDevMode [in]指向描述新图形模式的DEVMODE结构的指针。 如果lpDevMode为NULL,则当前在注册表中的所有值将用于显示设置。 对于lpDevMode参数传递NULL,对于dwFlags参数传递0是在动态模式更改后返回默认模式的最简单方法。DEVMODE的dmSize成员必须初始化为DEVMODE结构的大小(以字节为单位)。 必须初始化DEVMODE的dmDriverExtra成员,以指示DEVMODE结构之后的专用驱动程序数据的字节数。 此外,您可以使用DEVMODE结构中的任何或所有以下成员。



    VALUE
    MEANING




    dmBitsPerPel
    每像素位数


    dmPelsWidth
    像素宽度


    dmPelsHeight
    像素高度


    dmDisplayFlags
    模式标志


    dmDisplayFrequency
    模式频率


    dmPosition
    设备在多显示器配置中的位置



    除了使用一个或多个前面的DEVMODE成员之外,还必须在dmFields成员中设置以下一个或多个值来更改显示设置。



    VALUE
    MEANING




    DM_BITSPERPEL
    使用dmBitsPerPel值


    DM_PELSWIDTH
    使用 dmPelsWidth 值


    DM_PELSHEIGHT
    使用 dmPelsHeight 值


    DM_DISPLAYFLAGS
    使用 dmDisplayFlags 值


    DM_DISPLAYFREQUENCY
    使用 dmDisplayFrequency 值


    DM_POSITION
    使用 dmPosition 值




    dwflags [in]指示如何更改图形模式。 此参数可以是以下值之一。



    VALUE
    MEANING




    0
    当前屏幕的图形模式将被动态更改


    CDS_FULLSCREEN
    这种模式本质上是暂时的。如果您切换到另一台桌面,则此模式将不会重置


    CDS_GLOBAL
    这些设置将保存在全局设置区域,以便它们将影响机器上的所有用户。 否则,仅修改用户的设置。 此标志仅在使用CDS_UPDATEREGISTRY标志指定时有效


    CDS_NORESET
    设置将保存在注册表中,但不会生效。 此标志仅在使用CDS_UPDATEREGISTRY标志指定时有效


    CDS_RESET
    即使请求的设置与当前设置相同,应该更改设置


    CDS_SET_PRIMARY
    该设备将成为主要设备


    CDS_TEST
    系统测试是否可以设置所请求的图形模式


    CDS_UPDATEREGISTRY
    当前屏幕的图形模式将被动态更改,图形模式将在注册表中更新。 模式信息存储在USER配置文件中



    返回值

    返回DISP_CHANGE_SUCCESSFUL表示成功,其它表示失败。

    实现原理实现修改计算机分辨率功能中,关键的是对 ChangeDisplaySettings 函数的第一个参数赋值。第一个参数是一个结构体 DEVMODE,表示计算机显示的模式。关键是其中的两个参数:dmPelsWidth表示屏幕显示的宽度,dmPelsHeight表示屏幕显示的高度。这样,就指定的屏幕的分辨率。
    其中,分辨率的值并不是随意设置的。如果你不知道分辨率该设什么值,你可以打开自己电脑的分辨率,查看都有哪些分辨率的值。
    编码实现BOOL SetDisplay(int iWidth, int iHeight){ // 设置 DEVMODE 参数 DEVMODE stDevMode = { 0 }; stDevMode.dmSize = sizeof(stDevMode); stDevMode.dmBitsPerPel = 32; stDevMode.dmPelsWidth = iWidth; stDevMode.dmPelsHeight = iHeight; stDevMode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL; // CDS_UPDATEREGISTRY表示修改是持久的,并在相关注册表中写入了数据 LONG lRet = ::ChangeDisplaySettings(&stDevMode, CDS_UPDATEREGISTRY); if (DISP_CHANGE_SUCCESSFUL == lRet) { return TRUE; } return FALSE;}
    程序测试直接运行程序,计算机的分辨率成功更改。
    总结要注意的是,分辨率的值并不是随意设置的。如果你不知道分辨率该设什么值,你可以打开自己电脑的分辨率,查看都有哪些分辨率的值。
    1  留言 2019-01-22 09:34:13
  • Socket通信之UDP文件传输实验小程序

    背景如果你看到这篇文章的标题,会感到奇怪吧。UDP传输是一个无连接传输,数据发送之后,接收端接不接收得到,发送端是不理会的,所以,会发生丢包的情况。但是,我之所以写这个小程序,是因为一位网友本来打算请我帮ta写个小程序,应付作业的,作业要求是:

    基于UDP测试MP3文件(大于10M)的传输,并测试接收到的文件与发送文件是否一致。还可以调整发送端读取数据缓冲区的大小和延迟时间,以及接收端缓冲区的大小来解决。

    最终,因为我出价太高,而没有交易完成。但是,我就在和ta谈价期间,完成了这个小程序。这个小程序要求有界面的,但是,为了方便初学者的学习理解,本文特地重新开发了个新的程序,是基于控制台程序实现的。
    现在,就把实现过程和原理整理成文档,分享给大家。
    实现过程这个程序的实现,是根据我之前写的《Socket通信之UDP通信小程序》这篇文章修改而来的,大家可以参考这篇文章。在此,不仔细讲解UDP Socket如何使用。我只是大概讲下开发这个UDP文件传输小程序的实现原理。
    我们首先开发接收端程序,所谓的接收端程序,就是把接收的数据,不断地写入到文件中。具体实现过程就是:

    首先,从 recvfrom 函数获取到数据
    然后,根据判断生成文件路径是否存在,若不存在,则创建文件;若存在,则打开文件
    将文件指针移到文件的末尾,接着向文件中写入上述接收到的数据
    关闭文件,释放句柄
    重复上面 4 个步骤,直到文件接收完毕。

    // 数据接收void RecvMsg(){ int iSize = 40960; BYTE *lpBuf = new BYTE[iSize]; int iLen = 0; int iRet = 0; while (TRUE) { sockaddr_in addr = { 0 }; // 注意此处, 既是输入参数也是输出参数 int iLen = sizeof(addr); // 接收数据 int iRet = ::recvfrom(g_sock, (char *)lpBuf, iSize, 0, (sockaddr *)(&addr), &iLen); // 存储为文件 if (0 < iRet) { RecvFile(lpBuf, iRet); } } delete[] lpBuf; lpBuf = NULL;}
    接着,我们就可以开发发送端。发送端的开发流程就是:

    首先,我们根据文件路径打开文件,获取文件句柄
    然后,获取文件的大小,并读取文件的所有数据
    接着,根据发送缓冲区的大小,循环读取数据,并调用发送数据的函数,同时根据延迟时间进行发送延迟
    发送完毕,则释放内存,关闭句柄

    // 文件数据传输void SendFileData(char *pszFileName, char *pszDestIp, int iDestPort, int iBufferSize, int iElapseTime){ // 打开文件 HANDLE hFile = ::CreateFile(pszFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hFile) { ::MessageBox(NULL, "CreateFile Error!", NULL, MB_OK); return; } // 申请动态内存 BYTE *lpBuf = new BYTE[iBufferSize]; if (NULL == lpBuf) { ::CloseHandle(hFile); ::MessageBox(NULL, "申请动态内存错误!", NULL, MB_OK); return; } // 获取文件大小 DWORD dwFileSize = ::GetFileSize(hFile, NULL); DWORD dwRet = 0; BOOL bStop = FALSE; // 读取文件并发送 do { // 读取 ::RtlZeroMemory(lpBuf, iBufferSize); ::ReadFile(hFile, lpBuf, iBufferSize, &dwRet, NULL); if (dwRet < iBufferSize) { bStop = TRUE; } // 发送 SendMsg((char *)lpBuf, dwRet, pszDestIp, iDestPort); // 时间间隔 Sleep(iElapseTime); } while (FALSE == bStop); // 释放 delete[] lpBuf; lpBuf = NULL; ::CloseHandle(hFile); ::MessageBox(NULL, "发送完毕!", "DONE", MB_OK);}
    程序测试接收端绑定的地址和端口是:127.0.0.1:4321,发送端绑定的地址和端口是:127.0.0.1:12345,发送一个10.3M大小的MP3文件。设置发送缓冲区大小是10240字节,延时是100毫秒;接收端接收缓冲区大小为40960字节。
    我们先运行接收端,然后在运行发送段。等待一段时间后,文件接收完毕,打开接受到的MP3文件,居然可以正常播放。大家可以自己调整参数试试吧,或许就不能播放了哦。


    总结要特别注意一点就是,接收端使用 recvfrom 函数接收程序的时候,如果返回值为 -1,这么这时就要检查是否发送端发送数据的缓冲区比接收端接收数据的缓冲区还要大,若接收端接收数据的缓冲区较小,则应该调整缓冲区大小,使接收端接缓冲区大小比发送区缓冲区大小大才行。因为 recvfrom 函数返回值为 -1 的原因,可能是上面提到的情况,也可能是 recvfrom 函数的参数有误。
    1  留言 2018-12-20 12:26:26
  • Socket通信之UDP通信小程序

    背景之前自己做过关于Socket通信的视频教程,主要是教大家怎么使用Windows提供的Socket函数接口去实现网络通信的。当时讲了两个小程序的实现,一个是TCP通信,另一个是UDP通信。
    如今,我把视频教程讲解的内容,重新整理成文档的形式,并对程序简化,使用使用控制台重新开发,方便初学者的理解。本文先讲解使用Socket实现UDP通信的小程序,先把程序实现过程和原理整理成文档,分享给大家。
    函数介绍socket 函数
    根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数。
    函数声明
    SOCKET WSAAPI socket( _In_ int af, _In_ int type, _In_ int protocol);
    参数

    af [in]地址族规范。 地址系列的可能值在Winsock2.h头文件中定义。当前支持的值为AF_INET或AF_INET6,它们是IPv4和IPv6的Internet地址族格式。 type[in]指定Socket类型,SOCK_STREAM类型指定产生流式套接字,SOCK_DGRAM类型指定产生数据报式套接字,而SOCK_RAW类型指定产生原始套接字(只有管理员权限用户才能创建原始套接字)。protocol[in]与特定的地址家族相关的协议IPPROTO_TCP、IPPROTO_UDP和IPPROTO_IP,如果指定为0,那么系统就会根据地址格式和套接字类别,自动选择一个合适的协议。
    返回值

    如果没有发生错误,套接字返回引用新套接字的描述符。 否则,返回值为INVALID_SOCKET,并且可以通过调用WSAGetLastError来检索特定的错误代码。

    bind 函数
    将本地地址与套接字相关联。
    函数声明
    int bind( _In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen);
    参数

    s [in]标识未绑定套接字的描述符。名称[in]指向本地地址的sockaddr结构的指针,以分配给绑定的套接字。namelen [in]name参数指向的值的长度(以字节为单位)。
    返回值

    如果没有发生错误,则bind返回零。 否则,它返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。

    htons 函数
    将整型变量从主机字节顺序转变成网络字节顺序, 就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
    函数声明
    u_short WSAAPI htons( _In_ u_short hostshort);
    参数

    hostshort [in]
    主机字节顺序为16位。

    返回值

    返回TCP / IP网络字节顺序。

    inet_addr 函数
    将一个点分十进制的IP转换成一个长整数型数。
    函数声明
    unsigned long inet_addr( _In_ const char *cp);
    参数

    cp [in]
    点分十进制的IP字符串,以NULL结尾。

    返回值

    如果没有发生错误,则inet_addr函数将返回一个无符号长整型值,其中包含给定的Internet地址的适当的二进制表示形式。

    sendto 函数
    将数据发送到特定目的地。
    函数声明
    int sendto( _In_ SOCKET s, _In_ const char *buf, _In_ int len, _In_ int flags, _In_ const struct sockaddr *to, _In_ int tolen);
    参数

    s [in]标识(可能连接)套接字的描述符。buf [in]指向包含要发送的数据的缓冲区的指针。len [in]由buf参数指向的数据的长度(以字节为单位)。flags[in]一组指定呼叫方式的标志。to[in]指向包含目标套接字地址的sockaddr结构的可选指针。tolen[in]由to参数指向的地址的大小(以字节为单位)。
    返回值

    如果没有发生错误,sendto返回发送的总字节数,可以小于len指示的数字。 否则,返回值SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。

    recvfrom 函数
    接收数据报并存储源地址。
    函数声明
    int recvfrom( _In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags, _Out_ struct sockaddr *from, _Inout_opt_ int *fromlen);
    参数

    s [in]标识绑定套接字的描述符。buf [out]用于传入数据的缓冲区。len [in]由buf参数指向的缓冲区的长度(以字节为单位)。flags[in]一组修改函数调用行为的选项,超出了为关联套接字指定的选项。 有关详细信息,请参阅下面的备注。from[out]指向sockaddr结构中缓冲区的可选指针,它将在返回时保存源地址。fromlen [in,out,optional]指向from参数指向的缓冲区的大小(以字节为单位)的可选指针。
    返回值

    如果没有发生错误,recvfrom返回接收到的字节数。 如果连接已正常关闭,返回值为零。 否则,返回值SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。

    实现原理如下图所示:

    我们可以看到,UDP通信时,是无连接的,是不区分 Server 与 Client 的,所以,我们在程序实现的时候,只需要一个程序就可以了。
    和TCP通信程序一样,都首先要初始化Winsock服务环境。
    初始化Winsock环境后,便调用 socket 函数创建数据报套接字;然后对sockaddr_in结构体进行设置,设置绑定的IP地址和端口等信息并调用 bind 函数绑定;绑定成功后,就可以使用 recvfrom 函数和 sendto 函数与另一UDP程序进行数据的收发。通信结束后,变关闭套接字,释放资源。
    编码实现导入库文件#include <Winsock2.h>#pragma comment(lib, "Ws2_32.lib")
    UDP通信程序初始化Winsock库环境,创建数据报套接字,绑定IP地址和端口。
    // 绑定IP地址和端口BOOL Bind(char *lpszIp, int iPort){ // 初始化 Winsock 库 WSADATA wsaData = { 0 }; ::WSAStartup(MAKEWORD(2, 2), &wsaData); // 创建数据报套接字 g_sock = socket(AF_INET, SOCK_DGRAM, 0); if (INVALID_SOCKET == g_sock) { return FALSE; } // 设置绑定IP地址和端口信息 sockaddr_in addr = { 0 }; addr.sin_family = AF_INET; addr.sin_port = ::htons(iPort); addr.sin_addr.S_un.S_addr = ::inet_addr(lpszIp); // 绑定IP地址和端口 if (0 != bind(g_sock, (sockaddr *)(&addr), sizeof(addr))) { return FALSE; } // 创建接收信息多线程 ::CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)RecvThreadProc, NULL, NULL, NULL); return TRUE;}
    数据发送。
    // 数据发送void SendMsg(char *lpszText, char *lpszIp, int iPort){ // 设置目的主机的IP地址和端口等地址信息 sockaddr_in addr = { 0 }; addr.sin_family = AF_INET; addr.sin_port = ::htons(iPort); addr.sin_addr.S_un.S_addr = ::inet_addr(lpszIp); // 发送数据到目的主机 ::sendto(g_sock, lpszText, (1 + ::lstrlen(lpszText)), 0, (sockaddr *)(&addr), sizeof(addr)); printf("[sendto]%s\n", lpszText);}
    数据接收。
    // 数据接收void RecvMsg(){ char szBuf[MAX_PATH] = { 0 }; while (TRUE) { sockaddr_in addr = { 0 }; // 注意此处, 既是输入参数也是输出参数 int iLen = sizeof(addr); // 接收数据 ::recvfrom(g_sock, szBuf, MAX_PATH, 0, (sockaddr *)(&addr), &iLen); printf("[recvfrom]%s\n", szBuf); }}
    程序测试我们进行本机测试,先开启一个程序输入绑定的地址和端口为:127.0.0.1:12345。
    再启动另一个程序,绑定的地址和端口为:127.0.0.1:4321。
    然后,直接相互发送数据,进行数据通信。

    总结有 2 个地方如果稍不注意的话,便很容易出错:
    一是在使用Socket函数之前,一定要对Winsock服务进行初始化,初始化是由WSAStartup函数实现的。如果不进行初始化操作,而直接使用Socket函数,会报错。
    二是对于接收数据的 recvfrom 函数,最后一个参数一定要格外注意,它是既是输入参数也是输出参数,也就是说,一定要给它一个初值,初值大小就是sockaddr_in结构体的大小。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2018-12-20 12:26:15
  • Socket通信之TCP通信小程序

    背景之前自己做过关于Socket通信的视频教程,主要是教大家怎么使用Windows提供的Socket函数接口去实现网络通信的。当时讲了两个小程序的实现,一个是TCP通信,另一个是UDP通信。
    如今,我把视频教程讲解的内容,重新整理成文档的形式,并对程序简化,使用使用控制台重新开发,方便初学者的理解。本文先讲解使用Socket实现TCP通信的小程序,先把程序实现过程和原理整理成文档,分享给大家。
    函数介绍socket 函数
    根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数。
    函数声明
    SOCKET WSAAPI socket( _In_ int af, _In_ int type, _In_ int protocol);
    参数

    af [in]地址族规范。 地址系列的可能值在Winsock2.h头文件中定义。当前支持的值为AF_INET或AF_INET6,它们是IPv4和IPv6的Internet地址族格式。 type[in]指定Socket类型,SOCK_STREAM类型指定产生流式套接字,SOCK_DGRAM类型指定产生数据报式套接字,而SOCK_RAW类型指定产生原始套接字(只有管理员权限用户才能创建原始套接字)。protocol[in]与特定的地址家族相关的协议IPPROTO_TCP、IPPROTO_UDP和IPPROTO_IP,如果指定为0,那么系统就会根据地址格式和套接字类别,自动选择一个合适的协议。
    返回值

    如果没有发生错误,套接字返回引用新套接字的描述符。 否则,返回值为INVALID_SOCKET,并且可以通过调用WSAGetLastError来检索特定的错误代码。

    bind 函数
    将本地地址与套接字相关联。
    函数声明
    int bind( _In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen);
    参数

    s [in]标识未绑定套接字的描述符。名称[in]指向本地地址的sockaddr结构的指针,以分配给绑定的套接字。namelen [in]name参数指向的值的长度(以字节为单位)。
    返回值

    如果没有发生错误,则bind返回零。 否则,它返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。

    htons 函数
    将整型变量从主机字节顺序转变成网络字节顺序, 就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
    函数声明
    u_short WSAAPI htons( _In_ u_short hostshort);
    参数

    hostshort [in]
    主机字节顺序为16位。

    返回值

    返回TCP / IP网络字节顺序。

    inet_addr 函数
    将一个点分十进制的IP转换成一个长整数型数。
    函数声明
    unsigned long inet_addr( _In_ const char *cp);
    参数

    cp [in]
    点分十进制的IP字符串,以NULL结尾。

    返回值

    如果没有发生错误,则inet_addr函数将返回一个无符号长整型值,其中包含给定的Internet地址的适当的二进制表示形式。

    listen函数
    将一个套接字置于正在监听传入连接的状态。
    函数声明
    int listen( _In_ SOCKET s, _In_ int backlog);
    参数

    s [in]标识绑定的未连接套接字的描述符。backlog[in]待连接队列的最大长度。 如果设置为SOMAXCONN,负责套接字的底层服务提供商将积压设置为最大合理值。 如果设置为SOMAXCONN_HINT(N)(其中N是数字),则积压值将为N,调整为范围(200, 65535)。
    返回值

    如果没有发生错误,listen将返回零。否则,返回值SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。

    accept 函数
    允许在套接字上进行连接尝试。
    函数声明
    SOCKET accept( _In_ SOCKET s, _Out_ struct sockaddr *addr, _Inout_ int *addrlen);
    参数

    s [in]一个描述符,用于标识使用listen功能处于侦听状态的套接字。 连接实际上是由accept返回的套接字。addr [out]一个可选的指向缓冲区的指针,它接收通信层已知的连接实体的地址。 addr参数的确切格式由创建sockaddr结构的套接字时建立的地址族确定。addrlen [in,out]指向一个整数的可选指针,其中包含addr参数指向的结构长度。
    返回值

    如果没有发生错误,则accept返回一个SOCKET类型的值,该值是新套接字的描述符。 此返回值是实际连接所在的套接字的句柄。否则,返回值为INVALID_SOCKET,并且可以通过调用WSAGetLastError来检索特定的错误代码。

    send 函数
    在建立连接的套接字上发送数据。
    函数声明
    int send( _In_ SOCKET s, _In_ const char *buf, _In_ int len, _In_ int flags);
    参数

    s [in]标识连接的套接字的描述符。buf [in]指向包含要发送的数据的缓冲区的指针。len [in]由buf参数指向的缓冲区中数据的长度(以字节为单位)。标志[in]一组指定呼叫方式的标志。
    返回值

    如果没有发生错误,发送返回发送的总字节数,可以小于len参数中要发送的数量。 否则,返回值SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。

    recv 函数
    从连接的套接字或绑定的无连接套接字接收数据。
    函数声明
    int recv( _In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags);
    参数

    s [in]标识连接的套接字的描述符。buf [out]指向缓冲区的指针,用于接收传入的数据。len [in]由buf参数指向的缓冲区的长度(以字节为单位)。标志[in]一组影响此功能的行为的标志。
    返回值

    如果没有发生错误,则recv返回接收的字节数,buf参数指向的缓冲区将包含接收到的数据。 如果连接已正常关闭,返回值为0。

    实现原理如下图所示:

    无论对服务器端来说还是客户端来说,都首先要初始化Winsock服务环境。
    服务器端初始化Winsock环境后,便调用 socket 函数创建流式套接字;然后对sockaddr_in结构体进行设置,设置服务器绑定的IP地址和端口等信息并调用 bind 函数绑定;绑定成功后,便可以调用 listen 函数设置连接数量,并进行监听。直到有来自客户端的连接请求,服务器便调用 accept 函数接受连接请求,建立连接。这时,便可以使用 recv 函数和 send 函数与客户端进行数据的收发。通信结束后,变关闭套接字,释放资源。
    客户端初始化环境后,便调用 socket 函数创建流式套接字;然后对sockaddr_in结构体进行设置,设置服务器的IP地址和端口等信息并调用 connect 函数向服务器发送连接请求,并等待服务器的响应。服务器接受连接请求后,便成功与服务器建立连接,这时,便可以使用 recv 函数和 send 函数与客户端进行数据的收发。通信结束后,变关闭套接字,释放资源。
    编码实现导入库文件#include <Winsock2.h>#pragma comment(lib, "Ws2_32.lib")
    服务器端初始化Winsock库环境,创建流式套接字,绑定服务器IP地址和端口,并进行监听。
    // 绑定端口并监听BOOL SocketBindAndListen(char *lpszIp, int iPort){ // 初始化 Winsock 库 WSADATA wsaData = {0}; ::WSAStartup(MAKEWORD(2, 2), &wsaData); // 创建流式套接字 g_ServerSocket = ::socket(AF_INET, SOCK_STREAM, 0); if (INVALID_SOCKET == g_ServerSocket) { return FALSE; } // 设置服务端地址和端口信息 sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = ::htons(iPort); addr.sin_addr.S_un.S_addr = ::inet_addr(lpszIp); // 绑定IP和端口 if (0 != ::bind(g_ServerSocket, (sockaddr *)(&addr), sizeof(addr))) { return FALSE; } // 设置监听 if (0 != ::listen(g_ServerSocket, 1)) { return FALSE; } // 创建接收数据多线程 ::CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)RecvThreadProc, NULL, NULL, NULL); return TRUE;}
    服务器端数据发送。
    // 发送数据void SendMsg(char *pszSend){ // 发送数据 ::send(g_ClientSocket, pszSend, (1 + ::lstrlen(pszSend)), 0); printf("[send]%s\n", pszSend);}
    服务器端接收连接请求并接收数据。
    // 接受连接请求 并 接收数据void AcceptRecvMsg(){ sockaddr_in addr = { 0 }; // 注意:该变量既是输入也是输出 int iLen = sizeof(addr); // 接受来自客户端的连接请求 g_ClientSocket = ::accept(g_ServerSocket, (sockaddr *)(&addr), &iLen); printf("accept a connection from client!\n"); char szBuf[MAX_PATH] = { 0 }; while (TRUE) { // 接收数据 int iRet = ::recv(g_ClientSocket, szBuf, MAX_PATH, 0); if (0 >= iRet) { continue; } printf("[recv]%s\n", szBuf); }}
    客户端初始化Winsock库环境,创建流式套接字,并连接服务器。
    // 连接到服务器BOOL Connection(char *lpszServerIp, int iServerPort){ // 初始化 Winsock 库 WSADATA wsaData = { 0 }; ::WSAStartup(MAKEWORD(2, 2), &wsaData); // 创建流式套接字 g_ClientSocket = ::socket(AF_INET, SOCK_STREAM, 0); if (INVALID_SOCKET == g_ClientSocket) { return FALSE; } // 设置服务端地址和端口信息 sockaddr_in addr = { 0 }; addr.sin_family = AF_INET; addr.sin_port = ::htons(iServerPort); addr.sin_addr.S_un.S_addr = ::inet_addr(lpszServerIp); // 连接到服务器 if (0 != ::connect(g_ClientSocket, (sockaddr *)(&addr), sizeof(addr))) { return FALSE; } // 创建接收数据多线程 ::CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)RecvThreadProc, NULL, NULL, NULL); return TRUE;}
    客户端发送数据。
    // 发送数据void SendMsg(char *pszSend){ // 发送数据 ::send(g_ClientSocket, pszSend, (1 + ::lstrlen(pszSend)), 0); printf("[send]%s\n", pszSend);}
    客户端接收数据。
    // 接收数据void RecvMsg(){ char szBuf[MAX_PATH] = { 0 }; while (TRUE) { // 接收数据 int iRet = ::recv(g_ClientSocket, szBuf, MAX_PATH, 0); if (0 >= iRet) { continue; } printf("[recv]%s\n", szBuf); }}
    程序测试我们进行本机测试,服务器地址端口为:127.0.0.1:12345。
    先运行服务器,进行绑定并监听,然后再运行客户端进行连接,连接成功后,就可以相互进行数据通信。

    总结有 3 个地方如果稍不注意的话,便很容易出错:
    一是在使用Socket函数之前,一定要对Winsock服务进行初始化,初始化是由WSAStartup函数实现的。如果不进行初始化操作,而直接使用Socket函数,会报错。
    二是对于服务端中的接受来自客户端连接请求的accept函数,第三个参数一定要格外注意,它是既是输入参数也是输出参数,也就是说,一定要给它一个初值,初值大小就是sockaddr_in结构体的大小。
    三是测试的时候,如果服务端和客户端通信一直不成功,可以试着使用CMD命令的ping指令ping下两台主机是否能ping通,若不能,则检查是否在同一网段内或者防火墙是否关闭;若ping通,则检查自己的代码是否有误,可以单步进行调试。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2018-12-20 12:26:48
  • 使用ChangeScreenWidthHeight实现更改屏幕分辨率

    背景之前,帮别人写过一个小程序,其中,程序有个功能就是要更改电脑的分辨率。后来了解到,可以使用 ChangeDisplaySettings 函数去实现这个操作。现在,我就把实现的原理和过程,写成文档,分享给大家。
    函数介绍ChangeDisplaySettings 函数
    把缺省显示设备的设置改变为由 LPDEVMODE 设定的图形模式。
    函数声明
    LONG ChangeDisplaySettings ( LPDEVMODE lpDevMode, DWORD dwflags );
    参数

    lpDevMode [in]指向描述新图形模式的DEVMODE结构的指针。 如果lpDevMode为NULL,则当前在注册表中的所有值将用于显示设置。 对于lpDevMode参数传递NULL,对于dwFlags参数传递0是在动态模式更改后返回默认模式的最简单方法。DEVMODE的dmSize成员必须初始化为DEVMODE结构的大小(以字节为单位)。 必须初始化DEVMODE的dmDriverExtra成员,以指示DEVMODE结构之后的专用驱动程序数据的字节数。 此外,您可以使用DEVMODE结构中的任何或所有以下成员。



    VALUE
    MEANING




    dmBitsPerPel
    每像素位数


    dmPelsWidth
    像素宽度


    dmPelsHeight
    像素高度


    dmDisplayFlags
    模式标志


    dmDisplayFrequency
    模式频率


    dmPosition
    设备在多显示器配置中的位置



    除了使用一个或多个前面的DEVMODE成员之外,还必须在dmFields成员中设置以下一个或多个值来更改显示设置。



    VALUE
    MEANING




    DM_BITSPERPEL
    使用dmBitsPerPel值


    DM_PELSWIDTH
    使用 dmPelsWidth 值


    DM_PELSHEIGHT
    使用 dmPelsHeight 值


    DM_DISPLAYFLAGS
    使用 dmDisplayFlags 值


    DM_DISPLAYFREQUENCY
    使用 dmDisplayFrequency 值


    DM_POSITION
    使用 dmPosition 值




    dwflags [in]指示如何更改图形模式。 此参数可以是以下值之一。



    VALUE
    MEANING




    0
    当前屏幕的图形模式将被动态更改


    CDS_FULLSCREEN
    这种模式本质上是暂时的。如果您切换到另一台桌面,则此模式将不会重置


    CDS_GLOBAL
    这些设置将保存在全局设置区域,以便它们将影响机器上的所有用户。 否则,仅修改用户的设置。 此标志仅在使用CDS_UPDATEREGISTRY标志指定时有效


    CDS_NORESET
    设置将保存在注册表中,但不会生效。 此标志仅在使用CDS_UPDATEREGISTRY标志指定时有效


    CDS_RESET
    即使请求的设置与当前设置相同,应该更改设置


    CDS_SET_PRIMARY
    该设备将成为主要设备


    CDS_TEST
    系统测试是否可以设置所请求的图形模式


    CDS_UPDATEREGISTRY
    当前屏幕的图形模式将被动态更改,图形模式将在注册表中更新。 模式信息存储在USER配置文件中



    返回值

    返回DISP_CHANGE_SUCCESSFUL表示成功,其它表示失败。

    实现原理实现修改计算机分辨率功能中,关键的是对 ChangeDisplaySettings 函数的第一个参数赋值。第一个参数是一个结构体 DEVMODE,表示计算机显示的模式。关键是其中的两个参数:dmPelsWidth表示屏幕显示的宽度,dmPelsHeight表示屏幕显示的高度。这样,就指定的屏幕的分辨率。
    其中,分辨率的值并不是随意设置的。如果你不知道分辨率该设什么值,你可以打开自己电脑的分辨率,查看都有哪些分辨率的值。
    编码实现BOOL SetDisplay(int iWidth, int iHeight){ // 设置 DEVMODE 参数 DEVMODE stDevMode = { 0 }; stDevMode.dmSize = sizeof(stDevMode); stDevMode.dmBitsPerPel = 32; stDevMode.dmPelsWidth = iWidth; stDevMode.dmPelsHeight = iHeight; stDevMode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL; // CDS_UPDATEREGISTRY表示修改是持久的,并在相关注册表中写入了数据 LONG lRet = ::ChangeDisplaySettings(&stDevMode, CDS_UPDATEREGISTRY); if (DISP_CHANGE_SUCCESSFUL == lRet) { return TRUE; } return FALSE;}
    程序测试直接运行程序,计算机的分辨率成功更改。
    总结要注意的是,分辨率的值并不是随意设置的。如果你不知道分辨率该设什么值,你可以打开自己电脑的分辨率,查看都有哪些分辨率的值。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2018-11-07 10:31:48

发送私信

每个人最终和自己越长越像

25
文章数
16
评论数
eject