精华内容
下载资源
问答
  • PC客户端的互联网化设计

    千次阅读 2015-06-21 00:22:50
    作为Windows客户端程序员,自己也在时刻地警醒着:要抛弃传统软件开发思维,努力学习互联网思维。互联网+,这一个充满着机遇的经济新形态,让所有传统企业都感觉到挑战和希望。而最后能否突围成功,则在于是否能用...

            伴随着手机,平板电脑为代表的移动互联网的不断发展,云计算和云服务的兴起,更是推动着传统企业不断地往互联网方向转型。作为Windows客户端程序员,自己也在时刻地警醒着:要抛弃传统软件开发思维,努力学习互联网思维。互联网+,这一个充满着机遇的经济新形态,让所有传统企业都感觉到挑战和希望。而最后能否突围成功,则在于是否能用互联网思维去解决问题。

            好的,大层面讲完,该聚焦到具体的细节上。传统PC客户端开发都采用C/S架构,即服务器/客户端。例如在用户电脑上安装客户端和本地数据库,客户端与服务器端进行通讯;又或者是直接把数据库安装在数据库服务器上。其开发采用的数据库无外乎是MS sql2000Oracle等等,而数据库安装和维护是一个难题。

            一般的PC客户端软件几乎集成了所有的功能,界面人机交互和业务处理逻辑都包含其中,这样由于代码质量,现场环境和用户使用习惯等等,往往会容易出现不同的错误。这些都需要技术支持去帮助客户,甚至要到客户现场去。一旦产品用户数量到了一个大数量级,呵呵,公司的运维成本会显著提高。不仅如此,传统软件开发周期冗长,流程繁琐,已经不适应现今的互联网节奏了。

            因此,为了更加清晰地说明客户端的互联网化设计,现举例公司新产品采用的新型架构设计模式:互联网模式。

             双进程模式:

            1、主进程主要是用于界面展示,用户业务数据输入和人机交互等等,采用C++界面库来实现;

            2、副进程是一个http服务器,它负责数据处理、数据存储和云端交互,是一个数据引擎(跨平台编译,支持WindowsAndroidIOS平台);

            3、它们之间的请求交互通过http协议来进行通讯,并采用Json数据格式进行传输。

            本地数据库:

            采用了sqlite3,它是一款轻型的数据库,占用资源小,并且经常用在嵌入式设备中。不用安装,十分方便。

            客户端架构图:


             系统架构图:


            优点:

            1、业务逻辑不写死在客户端,放到服务器端操作,统一版本,减少碎片化,增强灵活性。

             如果把业务逻辑写在客户端上,一旦它变化,客户端就必须要修改代码,版本就要升级。而我们是不强迫用户升级的,这样就不能全网覆盖,造成版本碎片(这里也是浏览器端和客户端的一种区别);

            2、分工清晰,高效开发,聚焦细节,更能实现敏捷开发的快速迭代;

            客户端只负责界面实现,网络核心通讯和基本业务数据处理;服务器端负责数据库创建,处理,存储和通讯交互,每个人聚焦的点不大,但是却能更让开发者有更多精力和时间设计开发。

            3、有了云端的交互,云同步功能可以支持多终端,多地点了,并且支持本地脱机数据,真正做到了随时随地看数据了。

            除了架构模式,开发流程管理也要做到互联网化:快速迭代,小步快跑,快速试错,大目标拆分为小目标。其中最有名的是Scrum敏捷开发,现介绍其开发模型,如下图所示。


             什么是Sprint

             Sprint是短距离赛跑的意思,这指的是一次迭代,而一次迭代的周期是4个星期

             流程讲解:

            1PM确定Product Backlog(按优先顺序排列的一个产品需求列表),然后做工作量的预估和安排;

            2、通过 Sprint计划会议中挑选出一个Story作为本次迭代完成的目标,这个目标的时间周期是1~4个星期,然后再进行细化,形成一个Sprint Backlog

            3、每个Scrum Team成员根据Sprint Backlog再细化成更小的任务(工作量能细化到2天内完成);

            4、每次Daily Scrum Meeting(每日站立会议)须控制在15分钟左右,每人都要发言。要汇报你昨天完成了什么,并承诺你今天要完成什么,同时可以提出一些难以解决的问题,然后在白板上更新自己的 Sprint burn downSprint燃尽图);

            5、每天都要有一个可以成功编译、并且可以演示的版本;

            6、当一个Sprint Backlog被完成,我们要进行 Srpint Review Meeting(演示评审会议)。产品负责人和客户都要参加,每一个Team成员都要向他们演示自己完成的软件产品;

            7、最后是 Sprint Retrospective Meeting(回顾会议),以轮流发言方式进行,总结改进的地方,然后放入下一轮Sprint的产品需求中

            互联网是一个充满神奇和希望的地方,无论你是开发移动APP端或者windows客户端,都不应妄自菲薄。我们应该用心去做好产品,时刻站在用户的角度,跟随时代潮流,把用户体验和产品体验做到极致,那么我们就是成功的。

            加油吧,蛋炒饭!



    展开全文
  • 网易云音乐PC客户端加密API逆向解析

    千次阅读 2018-03-22 14:59:36
    1、前言网上已经有大量的web端接口解析的方法了,但是对客户端的接口解析基本上找不到什么资料,本文主要分析网易云音乐PC客户端的API接口交互方式。通过内部的代理设置,使用fiddler作为代理工具,即可查看交互流程...

    1、前言

    网上已经有大量的web端接口解析的方法了,但是对客户端的接口解析基本上找不到什么资料,本文主要分析网易云音乐PC客户端的API接口交互方式。

    通过内部的代理设置,使用fiddler作为代理工具,即可查看交互流程:

    网易云音乐PC客户端加密API逆向解析

    可以大致看一下交互方式,通过HTTPS POST交互,POST了一串params的内容,内容加密,返回JSON内容,我要做的重点就在于解析params的生成方式,用于模拟这次交互。 

    (Tan1993:这是后续编写的内容,截图很多都是后补的,所以可能会出现使用不同的调试工具,不同的环境,不同的时间等,不影响阅读。另外本人工作主要是linux网络方向的,像是这次只是我的一点业余爱好,也很少会去逆向东西,如果出现一些比较业余的操作或想法时,还望指出)

    2、初步了解

    下载最新版PC版网易云安装(目前是2.3.0.196231版本),分析在程序所在目录下的文件。 

    网易云音乐PC客户端加密API逆向解析

    动态链接库与可执行文件:

    第一个最让我注意的时libcurl,这个网络库可以用于HTTP协议交互,如果通过该库与服务器交互, od断点到curl_easy_perform再往回推就可以判断转换算法位置了,然而事实比我想象的复杂多了,这个库仅在程序刚运行时用于一些无关的网络交互(Tan1993:记不清了,好像是版本还是客户端信息相关的请求)。

    第二个是libcef,这个是个基于C/C++的Web browser控件,可以简单理解为就是个浏览器的壳子(Tan:为什么说关键API没用到libcurl库,因为除了开始时cef框架还没初始化前网络交互用到那个库而已,一点cef环境起来了,都是通过JS ajax交互了)。

    其他的除了cef依赖的dll外,两个主程序和cloudmusic.dll都比较值得关注。

    资源文件:

    除了在package下的其他都是cef库依赖的资源文件。

    网易云音乐PC客户端加密API逆向解析

    都是未知的格式,一般看到未知格式的文件,我都会用7z尝试打开看看,是不是某种归档格式文件,这个一下就蒙中了,是zip格式的。

    网易云音乐PC客户端加密API逆向解析

    除了几个通过后缀就能看出来的皮肤文件,还有两个比较可疑的文件,翻一翻比较大的orpheus.ntpk文件,里面可以看到都是网页相关的资源文件,看到那个core.js,就让我联想到网页版API提取时用到的那个core.js文件了,脑海里就想着替换然后对转换流程动态分析了,事实有点不尽人意,该zip文件加密了。 

    网易云音乐PC客户端加密API逆向解析

    OK,调研阶段结束,在不进行逆向解析前,能了解到的也就止步于此了。 

    3、第一轮尝试

    其实一开始我是把目光放在libcurl上面的,在断点到curl库的函数上时发现只有程序刚运行时触发过几次,后面所有网络交互都不用这个库了,就转战到cef上。而cef的重点在于内部的JS文件,能提取到该文件才是关键的

    网易云音乐PC客户端加密API逆向解析

    0×2712即CURLOPT_URL宏,eax中存放着url的字符串指针,基本上都是无关的url。

    第一个任务来了,逆向寻找特征串,也就是密码,这里断点到系统文件操作API上,断到CreateFileW,一顿的F9后可以看到加载到default.skin文件了(图中是native.ntpk,同类型的加密ZIP文件),后续就单步调试下去。 

    网易云音乐PC客户端加密API逆向解析

    然后看到一个比较特别的内存块,一看就是PNG格式的文件头,就可以判断这一步资源已经解压缩到内存了。 

    网易云音乐PC客户端加密API逆向解析

    往上推几步,断点,缩小范围,再跟下来,看看哪里做了解压操作,再一步步跟函数。(Tan1993:可能比较业余,但我也只能一点点缩小范围在一点点看流程,凭经验判断可能会做什么操作,缩短到比较短的范围,不然一堆汇编码真的会受不了,感谢世界上程序员的思想都是接近的吧)。 

    网易云音乐PC客户端加密API逆向解析

    得知密码后,就可以解压出core.js文件了(Tan1993:这里仅提供思路,不提供便民服务哈)

    网易云音乐PC客户端加密API逆向解析

    又是这一堆让人窒息的混淆,卡得怀疑人生,先解压缩再看吧。

    解压后,搜几个关键字,比如params,eapi,batch等最上面HTTP交互时的一些特征

    网易云音乐PC客户端加密API逆向解析

    关键代码,像这样混淆的JS代码,如果不通过调试器跟踪,很难看懂,目前能可以看出也只有channel.serialData应该时比较关键的转换函数,但是搜索了整个JS文件都找不到函数定义,不知道是不是混淆到哪个奇怪的地方了。

    虽然cef自带DevTools,但是已经被屏蔽掉了也无法在程序里调出来,所以我想在JS文件中加上alert调试关键参数。然后我修改了core.js文件,按原来的密码压缩回去。但程序根本就起不来,为什么呢,看看原版的.ntpk文件,很明显还有一些奇怪的东西和zip文件一起合成了这个ntpk文件格式。根据经验判断很可能时类似于数字签名的东西(Tan1993:之前我也会对一些可能被篡改的档案末尾对整个文件加盐生成一个hash值用于校验,但是后续跟完网易云的数字签名方式让我又学习了不少)。

    网易云音乐PC客户端加密API逆向解析

    4、第二轮尝试

    为了方便调试,我需要替换掉资源文件中的core.js文件,但是该资源文件不仅仅加密压缩了,还有一些其他内容存在,所以这次跟代码就是为了了解除了zip文件本身以外其他部分内容的作用。

    还是断到CreateFileW函数上,其实第一轮跟代码的时候我就已经发现了部分调用系统加密服务提供程序 (CSP)库的函数。

    网易云音乐PC客户端加密API逆向解析

    一步步跟过来,发现用的是SHA1数字签名算法(Tan1993:不是很了解CSP库,但这个是为Windows系列操作系统制订的底层加密接口,和我理解的SHA不太一样,我姑且将程序内部的那部分称为公钥,与文件头部的校验数据进行校验)。

    网易云音乐PC客户端加密API逆向解析

    文件头NTPK,文件长度0x0D5C5B,校验串长度0×100

    网易云音乐PC客户端加密API逆向解析

    刚好差了0×110长度,除了0×100用于校验的数据,还有0×10的头部。

    由于我是无法在不知道私钥的情况下,再次对该文件进行签名的,所以我只能把程序内部的用于校验的公钥一并替换,再生成一个对应的检验数据,从而通过系统验证,或者直接把验证部分的代码跳转逻辑修改掉(Tan1993:其实可能改分支流程修改会更简单也说不定,但我一开始选择的是替换公钥重新生成校验数据)。

    int GenKey(HCRYPTPROV hProv)
    {
    	HCRYPTKEY hKey;
    	HANDLE hFile = NULL, hOutFile = NULL;
    	DWORD dwSize = 0, dwRead = 0, dwWrite = 0, dwBlobLen = sizeof(bRsaKey);
    	BYTE *pbFileData = NULL;
    	int ret = -1;
    
    	// 先读取原版的dll,加载到内存中
    	hFile = CreateFileW(L"cloudmusic_src.dll",
    		GENERIC_READ, FILE_SHARE_READ, NULL,
    		OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    
    	dwSize = GetFileSize(hFile, NULL);
    	pbFileData = new BYTE[dwSize];
    	ReadFile(hFile, pbFileData, dwSize, &dwRead, NULL);
    	CloseHandle(hFile);
    
    	if (!memcmp(pbFileData + 0x7C3438, bRsaKey, sizeof(bRsaKey)))
    	{
    		// 重新生成密钥对
    		CryptGenKey(hProv, AT_SIGNATURE, CRYPT_EXPORTABLE, &hKey);
    		memset(bRsaKey, 0, sizeof(bRsaKey));
    		CryptExportKey(hKey, NULL, PUBLICKEYBLOB, 0, bRsaKey, &dwBlobLen);
    		// 将新生成的公钥覆盖原本dll中的公钥
    		memcpy(pbFileData + 0x7C3438, bRsaKey, sizeof(bRsaKey));
    		// 随带把debug端口开了(后续再解释)
    		SetDebugPort(pbFileData);
    		hOutFile = CreateFileW(L"cloudmusic.dll",
    			GENERIC_WRITE, FILE_SHARE_READ, NULL,
    			CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    		// 写回到dll中
    		WriteFile(hOutFile, pbFileData, dwSize, &dwWrite, NULL);
    		CloseHandle(hOutFile);
    		ret = 0;
    	}
    	delete[] pbFileData;
    	CryptDestroyKey(hKey);
    	return ret;
    }
    
    int EncFile(HCRYPTPROV hProv, LPCWCHAR wstrInFile, LPCWCHAR wstrOutFile)
    {
    	HCRYPTHASH hHash;
    	DWORD dwSize = 0, dwRead = 0, dwWrite = 0, dwOutSignSize = 0;
    	HANDLE hFile = NULL, hOutFile = NULL;
    	BYTE *pbFileData = NULL, *pbSignData = NULL;
    
    	// 打开带密码的压缩文件
    	hFile = CreateFileW(wstrInFile,
    		GENERIC_READ, FILE_SHARE_READ, NULL,
    		OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    
    	dwSize = GetFileSize(hFile, NULL);
    	pbFileData = new BYTE[dwSize];
    	ReadFile(hFile, pbFileData, dwSize, &dwRead, NULL);
    	CloseHandle(hFile);
    
    	// 打开输出文件
    	hOutFile = CreateFileW(wstrOutFile,
    		GENERIC_WRITE, FILE_SHARE_READ, NULL,
    		CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    	// 写入文件头
    	WriteFile(hOutFile, bHead, sizeof(bHead), &dwWrite, NULL);
    	// 写入原压缩文件长度
    	WriteFile(hOutFile, &dwSize, sizeof(int), &dwWrite, NULL);
    
    	// 创建并计算Hash值
    	CryptCreateHash(hProv, CALG_SHA, 0, 0, &hHash);
    	CryptHashData(hHash, pbFileData, dwSize, 0);
    	CryptSignHash(hHash, AT_SIGNATURE, NULL, 0, NULL, &dwOutSignSize);
    	pbSignData = new BYTE[dwOutSignSize];
    	CryptSignHash(hHash, AT_SIGNATURE, NULL, 0, pbSignData, &dwOutSignSize);
    
    	// 写入Hash值大小
    	WriteFile(hOutFile, &dwOutSignSize, sizeof(int), &dwWrite, NULL);
    	// 写入Hash值(校验数据)
    	WriteFile(hOutFile, pbSignData, dwOutSignSize, &dwWrite, NULL);
    	// 写入原压缩文件
    	WriteFile(hOutFile, pbFileData, dwSize, &dwWrite, NULL);
    
    	CloseHandle(hOutFile);
    
    	delete[] pbSignData;
    	delete[] pbFileData;
    
    	CryptDestroyHash(hHash);
    	return 0;
    }
    

    截了一部分代码,用于修改cloudmusic.dll中的二进制数据,偏移是根据内存加载地址与基址算的,直接固定偏移修改即可。

    到这一步其实我已经可以替换掉core.js文件并且可以alert弹出对话框,显示一些JS运行时数据了,虽然alert弹框并不是那么好用。

    网易云音乐PC客户端加密API逆向解析

    通过alert我可以看到加密前的内容,也就是具体发了哪些数据,以及加密后是什么样子的,很可惜的是当我尝试alert(channel.serialData)时发现是[native code],按我个人理解应该是系统二进制函数才会显示这个的吧(对JS并不是非常了解),怀疑是库函数,但查询无果,后来想了想会不会是JS调用了C++代码(凭我对cef粗糙的理解),我尝试去查了一下,果然是可以的,那么很有可能这部分加密转换的代码还是在主程序中,这就很头疼了,刚从主程序逆向脱离出来到JS这个自由的世界,又要回到看汇编码的环境了。

    5、第三轮尝试

    这一轮主要目的是找到channel.serialData在主程序的位置,根据我对cef的理解,应该是在程序启动时,注册了一部分回调函数,可以从注册的时候找到回调函数入口,然后等触发channel.serialData动作时,从回调函数跟代码跟下来。

    网易云音乐PC客户端加密API逆向解析

    根据DLL版本,我找到了对应的cef源码版本,cef注册回调时是整个结构体的,必须找到对应的版本避免新版本结构体不一样导致偏移位置有差异。

    网易云音乐PC客户端加密API逆向解析

    在看源码的过程中发现结构体里有个很有意思的字段,一个debug端口,调研了一下,这个端口很有用了,可以远程DevTools,这样还用什么alert。

    网易云音乐PC客户端加密API逆向解析

    如果要在调用初始化前把结构体改掉,要么API Hook修改,要么静态文件修改,文件修改的话只能舍弃一些无用代码来改这个结构体了,我选了一个不影响的赋值语句,改成给这个地址赋9222。

    网易云音乐PC客户端加密API逆向解析

    对照源码中结构体计算偏移值

    网易云音乐PC客户端加密API逆向解析

    原本修改cloudmusic.dll的代码中增加个代码段修改的方法

    // 修改Debug Port为9222
    void SetDebugPort(BYTE *pbFileData)
    {
    	if (!memcmp(pbFileData + 0x14EED, bSettingAsm, sizeof(bSettingAsm)))
    	{
    		bSettingAsm[2] = 0x94; // 结构体偏移
    		bSettingAsm[6] = 0x06; // 0x2406也就是9222端口
    		bSettingAsm[7] = 0x24;
    		memcpy(pbFileData + 0x14EED, bSettingAsm, sizeof(bSettingAsm));
    	}
    }
    

    现在我就可以通过http://127.0.0.1:9222远程访问DevTools了。可当我打开网页时一片空白,这时候又凭借我对cef粗略的了解,在程序目录下,并没有devtools相关的资源,其实只要把资源文件补上就可以了(官网已经没有这么老的资源文件档案了,这个还是我网上找的3.1916版本的devtools资源文件)

    网易云音乐PC客户端加密API逆向解析

    这时候所有JS调试命令都可以改成console.log来进行了,方便了好多。

    网易云音乐PC客户端加密API逆向解析

    回到正题,从注册来跟代码实在是太痛苦了。一个是注册的内容比较多,一层叠一层的,而且程序用的是C++ warp的C语言版本的cef库,和源码对照跟的时候还是有点差别的。这时候我想到一个非常好的方法,那就是制造一个死循环。

    6、第四轮尝试

    上面就提到了,我放弃了从注册一步步跟踪回调函数的麻烦方案,而是在JS中知道一个死循环,不停的调用channel.serialData函数,等程序单核满载时,只需要将调试器附加程序,点一点暂停,基本上就是这个函数相关业务流程的代码了(JS到机器码代码按我理解应该在堆上,而加密的代码应该在程序代码段上,所以我定位的时候可以忽略掉很多JS的代码,找到真正相关的代码位置)

    网易云音乐PC客户端加密API逆向解析

    实际上,channel.serialData的汇编码也非常多,流程也分了好多部分,这部分工作量实在是降不下来,但是很多可能是为了防止静态分析的代码,部分特征串是运行时生成的,但是因为这部分特征串都是固定的,所以是可以不用去仔细琢磨的(然而我花了一两天来看那一堆汇编码来算出特征串,非常郁闷,早知道就逆推就好,但说实话,光逆推也会很难,主要是要有一定理解)

    简单说明一下转换流程

    1、 输入url(请求部分)和data(提交的json数据)

    2、 拼成”nobody” + url + “use” + data + “md5forencrypt”字符串

    3、 对字符串计算MD5

    4、 二次拼接url + “-36cd479b6b5-” + data + “-36cd479b6b5-” + md5

    5、 0×10对齐,缺少的部分会以缺少的位数来填充

    6、 私有转换方法(也许是我不知道的一种加密方式?)

    附上一部分分析的图

    网易云音乐PC客户端加密API逆向解析

    网易云音乐PC客户端加密API逆向解析

    待加密数据,0×10字节对齐,每次处理0×10字节的数据

    网易云音乐PC客户端加密API逆向解析

    辅助加密数据(动态生成,但是是固定的,我还傻傻去复现了一遍生成流程)

    网易云音乐PC客户端加密API逆向解析

    开始对0×10进行转换

    网易云音乐PC客户端加密API逆向解析

    一堆异或和位移计算,这个还是很好复现到C的代码中的,这个比较长就不全粘贴了。

    循环转换完后再按照”%02X”格式snprintf到字符串即可。我没有过多去理解这个加密算法究竟是什么原理,只是直译汇编码。

    后来尝试反过来解析,看了一早上没看出来,简单描述一下为什么难以逆转的问题。

    内存块mem

    a1b1b1c1a1b1b1c1 a2b2b2c2a2b2b2c2 ………

     

    eax = a1a2a3a4

    ebx = b1b2b3b4

    ecx = c1c2c3c4

    edx = d1d2d3d4

     

    eax = mem[a4 * 8] ^ mem[b3 * 8 + 3] ^ mem[c2 * 8 + 2] ^ mem[d1* 8 + 1]

    ebx = mem[a3 * 8] ^ mem[b2 * 8 + 3] ^ mem[c1 * 8 + 2] ^mem[d4 * 8 + 1]

    ecx = mem[a2 * 8] ^ mem[b1 * 8 + 3] ^ mem [c4 * 8 + 2] ^ mem[d3* 8 + 1]

    edx = mem[a1 * 8] ^ mem[b4 * 8 + 3] ^ mem[c3 * 8 + 2] ^mem[d2 * 8 + 1]

    然后在得知后面的eax,ebx,ecx,edx逆推原来的,感觉不太可能,但是mem并不是没有规律的一个内存块,而且数组索引时也做了些巧妙的偏移,事实上内存块确实有不少规律(比如a1是偶数时b1是a1的一半,c1是a1 ^ b1),而且和索引时的偏移可能会相得益彰,如果能看出窍门说不定还是能解的,有兴趣的小伙伴也可以研究一下(Tan1993:个人没学过加密学,只略懂一部分概念)

    7、汇总

    其实到这一步,我可以通过远程devtools来看发送前未加密的内容以及结构,同时我也可以通过已经复现的加密方法,对不同业务数据加密发送出去。我发现有一部分请求数据返回内容也是加密的,但这个是可以在客户端控制e_r的值来控制是否需要返回加密内容的。

    网易云音乐PC客户端加密API逆向解析

    写个模拟客户端下载歌曲的小Demo,本来发送和接收都是加密的数据的下载接口,就可以通过服务器验证实现下载了,解析到此告一段落,虽然过程中还有很多内容值得研究,如果有机会以后会继续挖掘。

    网易云音乐PC客户端加密API逆向解析

    8、总结

    由于并没有找到任何的参考资料,断断续续也研究了一周时间。除了实现了目标以外,还是有不少收获的,比如比较有趣的加密算法,数字签名方法,cef库,还有一些逆向的思路。

    比较遗憾的是没有把解密的算法也解析出来,同时在客户端控制e_r的值来控制返回数据是否加密显然不是好方法,官方只需要忽略这个参数强制对部分API返回加密数据,正常的客户端也没有任何影响(难道有平台相关性所以才把这个参数放到客户端的吗?)。

    Tan1993:视情况考虑是否在github提供源码)

    9、彩蛋

    将一件有趣的事,当时我尝试在一台国外IP的服务器上调用web的api接口时发现不能适用,获取不到数据,然后我又跟了一便JS代码发现逻辑不一样,其中发现了一个很有意思的特征串(在你们看不到的地方,总有调皮的程序员):

    网易云音乐PC客户端加密API逆向解析

    *本文作者:Tan993,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。

    展开全文
  • PC客户端软件升级方式简史

    千次阅读 2018-11-06 11:32:36
    对于PC客户端软件每次升级时主程序以及一些重要的动态库都有可能更新,所以下载的压缩包也会比较大。于是就产生了比较文件二进制差异的算法BsDiff,以及Google基于其进一步改进的Courgette(小胡瓜)。这些算法的...
    • 在windows8之前,微软的Windows平台一直没有提供一个想苹果的AppStore或者Linux的包管理这样的统一软件管理工具。所以Windows下的软件安装、升级、卸载的事情一般都是软件自己去负责。这样导致Windows下的软件安装、升级、卸载的方式五花八门,但总体上来说方法都大同小异。安装程序主要分两种,下载器的安装包和离线安装包,这个不赘述。
    • 今天重点聊一下升级,升级功能看似简单,但对于一个想持续经营的客户端软件来说却是一个重要的生命线。开发团队辛苦修改的bug、做的新功能都希望用户能马上通过升级新版本体验到。
    • 在互联网还没普及的蛮荒年代,很多软件公司升级都是发布离线升级包,一般这种包就是一个安装程序,它只负责安装程序需要更新的部分,然后做一些修改注册表之类的系统配置以适应新版本的功能。
    • 现在互联网普及后,所有的PC客户端软件基本上都是使用的在线升级。
      • 最简单的在线升级方式是首先客户端发送检测更新的消息到服务器,服务器给返回是否有新版本,最新版本号以及下载地址等信息,客户端就根据这些信息处理。如果有更新就去刚刚获取到的地址下载最新的安装程序,然后执行安装程序更新。
      • 后来大家觉得每次都重新安装太麻烦,而且安装包也特别大,下载也非常耗时。于是这个下载的程序被替换成了一个压缩包,里面装的是程序需要更新的文件。升级程序下载好压缩包后再解压到安装目录中就完成了软件的升级。
    • 随着敏捷开发方式的普及,软件的升级就变得越来越频繁了。对于PC客户端软件每次升级时主程序以及一些重要的动态库都有可能更新,所以下载的压缩包也会比较大。于是就产生了比较文件二进制差异的算法BsDiff,以及Google基于其进一步改进的Courgette(小胡瓜)。这些算法的加入可以让补丁包缩小了n个数量级。这样需要客户端去下载的压缩包就会很小了,下载耗时也会大大缩短。
    • 另外值得一提的是,随着软件升级包大小越来越大,用户下载更新文件的等待时间也越来越长,于是有些软件就采用了后台静默下载的方式。这种方式虽然流氓,但可能对于用户来说体验要好一些。那么这种情况下主程序一般都还在运行,而升级程序下载完成后想要更新文件立即升级就必须关闭主程序然后进行文件替换,以免文件被占用,导致升级失败。于是Google的chorme搞出一个双目录更新的方法来应对这种情况。所谓双目录更新就是把原来的文件先复制到另一个目录下,更新程序的时候就更新这个文件目录,升级完成后就直接从新的目录中启动新版本。
    • chrome的目录结构是这样的:
    Chorme
        +Application
            +57.0.2987.110
            +57.0.2987.88
                chrome.exe
    
    • 可以看到,他是以版本号做目录名。以后启动chrome.exe时去加载最新版本就可以了。当然它能这样做主要因为chrome.exe本身是个很小的程序,基本它自身是不需要升级的,它主要负责的就是检测版本号然后加载新版本的dll。
    • 当然现在的客户端升级程序还涉及一些入灰度,md5完整性检测断点续传等技术这里也不在赘述。下面我简单介绍一下BsDiff和Courgette。

    BsDiff: Linux中的一个开源工具,致力于快速和轻量的更新Linux的操作系统漏洞(跟微软的安全补丁类似),其算法的核心思想是基于统计学规律进行近似匹配,然后通过一系列的变化(比如BWT变换)提高“近似段”的压缩率。
    Courgette: Google Chrome升级系统的核心模块,基于BsDiff,但对其进行了一系列的改进,将平台相关的信息(即x86汇编指令)融入其中,以期望更精确的定位指针,从而避免统计算法在差异明显时候的错误率。

    • Google官方给了一个10M的升级包例子使用bsdiff可以看到包小了不少,用Courgette更是少了几个数量级。

    • 使用bsdiff算法我们的升级过程是这样的:
    server:
        diff = bsdiff(original, update)
        transmit diff
    
    client:
        receive diff
        update = bspatch(original, diff)
    
    • 大致流程就是这使用bsdiff算法比较不同版本的二进制文件制作补丁包,客户端下载补丁包后调用bspatch生成新的二进制文件。

    • 使用Courgette的升级过程是这样的:

    server:
        asm_old = disassemble(original)
        asm_new = disassemble(update)
        asm_new_adjusted = adjust(asm_new, asm_old)
        asm_diff = bsdiff(asm_old, asm_new_adjusted)
        transmit asm_diff
    
    client:
        receive asm_diff
        asm_old = disassemble(original)
        asm_new_adjusted = bspatch(asm_old, asm_diff)
        update = assemble(asm_new_adjusted)
    
    • Courgette对于bsdiff的优化主要就是在adjust这一步上,具体可以参考Courgette官方说明

    • 最后,Google还开源了一套Windows下的升级协议,大家有兴趣也可以研究下omaha



    作者:吴尼玛cs
    链接:https://www.jianshu.com/p/ca40bfc4a81f
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

    展开全文
  • TeamTalk源码分析(十一) —— pc客户端源码分析

    万次阅读 热门讨论 2017-07-05 16:03:45
    4. libogg是一个语音库,用来解析声音文件的,因为pc客户端可能会收到移动端的语音聊天,相比较传统的*.wav、*.mp3、*.wma,*.ogg格式的不仅音质高,而且音频文件的体积小,腾讯的QQ游戏英雄杀中的语音也是使用这个...

           ——写在前面的话 

           在要不要写这篇文章的纠结中挣扎了好久,就我个人而已,我接触windows编程,已经六七个年头了,尤其是在我读研的三年内,基本心思都是花在学习和研究windows程序上了。我很庆幸我当初学习windows程序走了一条正确的路线:先是学习常用的windows程序原理和基本API,再学习的mfc、wtl等一些常用的框架和类库,同时看了大量windows项目的源码,如金山卫士的开源代码、filezilla、电驴源码等等。个人觉得,基础真的很重要,拿windows开发来说,当你掌握了windows的程序的基本原理,我列一下大致范围:

    1. windows消息机制(消息如何产生、如何发送、如何处理,常见的消息有哪些、消息的优先级、如何自定义消息、窗体消息、常用控件消息)

    2. gdi原理(要熟悉gdi的各种对象,如画笔、画刷、字体、区域、裁剪、位图等,熟悉它们的API,熟悉各种gdi绘图API、当然最好也要熟悉一整套的gdi+的类,gdi与gdi+的区别)

    3. windows进程与线程的概念(进程的概念、如何创建、如何结束、跨进程如何通信;线程的创建与销毁、线程间的同步与资源保护,熟悉windows常用的线程同步对象:临界区、事件、互斥体、信号量等)

    4. windows内存管理(清晰地掌握一个进程地址空间的内存分布、windows堆的创建与管理等)

    5. dll技术(dll的生成、变量的导出、函数的导出、类的导出、如何查看dll导出哪些函数、隐式dll的加载、显示dll的加载、远程dll注入技术等)

    6. PE文件(一个PE文件的结构、有哪些节、如何修改、分别映射到进程地址空间的什么位置等)

    7. windows SEH(结构化异常处理)

    8. windows socket编程

    9. windows读写文件技术(像CreateFile、WriteFile、GetFileSize等这些API应该熟练掌握、内存映射技术)

           当然很多必备的技术也不好归类到windows技术下面,比如socket编程,这涉及到很多网络的知识,比如tcp的三次握手,数据的收发等,还有就是各种字符编码的知识、以及之间的相互转换,又比如一系列的CRT函数及其对应的宽字符版本。当然如果你搞windows开发,一定要熟悉开发工具Visual Studio,熟悉其工程项目的大多数属性配置,而且要做到知其然也知其所以然。如果不是不能跨平台,我敢说VS是史上最好最强大的开发工具,没有之一!我已经有好几年年不做windows开发了,目前主要从事linux开发,但windows的很多设计思想真的很好,非常值得借鉴,而且从编码风格来说,虽然看起来有点怪异,但是非常规范和易懂。

         有了基础知识,你可以轻松地对工作中的一些问题给出解决方案,也能轻松阅读和使用市面上的那些库,比如,如果你深刻理解windows GDI,你不会在一个群里大喊,duilib某个属性为什么不起作用,你可以直接去阅读它的画法代码,如果是bug你可以改bug,如果只是你使用错误,你可以了解到正确的使用方法。所以基础这个东西,在短时间内,可能让你看不出与其他人的差别,但是从长远来看,它决定着你在技术上走的高度与深度。套用侯捷先生的一句话:勿在浮沙筑高台。

          

          —— 正题

          上面简单地介绍了下,我个人学习windows程序设计的一些心得吧。扯的有点远了,让我们回到正题上来,来分析TeamTalk的源码吧。当然这篇文章与前面介绍的不一样,我们不仅介绍程序的正题设计思路,还会介绍一些有意义的细节,比如一些windows开发中常用的一些细节。

      

    一、程序功能    

           我们来先看下TeamTalk pc客户端包括哪些功能:TeamTalk因为开发的初衷是用于企业内部的即时通讯软件,所以,不提供对外注册的功能,一个员工的加入一般是人事部门在后台管理系统来新增该员工信息。其功能包括登录、聊天、群聊和建讨论组,当然聊天过程中可以发文字、表情、图片和文件,还包括查看聊天记录和简单地查看某个员工的个人信息,业务功能其实不多的。下面是一些功能截图:

     

    二、编译方法与项目工程文件介绍

    1. TeamTalk的pc客户端的下载地址是:https://github.com/baloonwj/TeamTalk

     代码包括服务器端代码、pc端、mac端、安卓和IOS端,还有web端所有代码。

     pc客户端代码的编译方法很简单:用VS2013打开win-client\solution目录下的teamtalk.sln,编译即可。你的VS版本至少要是VS2013,因为代码中大量使用了C++11的东西,VS2013以下版本是不支持C++11的语法的。当然,如果你是VS2015的话,可以参考这篇文章来进行修改和编译:http://www.07net01.com/linux/2017/01/1795569.html

     

    打开teamtalk.sln之后,总共有10个解决方法,如下图所示:

     

     

    其中teamtalk是主工程,你应该将它设置成启动工程,编译完成之后就可以调试了。你可以自己配置服务器来连接进行调试,我也可以连接我的测试服务器,具体参见《TeamTalk源码分析(十) —— 开放一个TeamTalk测试服务器地址和几个测试账号》。下面先大致介绍一个各个工程的作用:

    1. Duilib是teamtalk使用的一款开源界面库,该界面库模仿web开发中的布局技术,使用xml文件来布局windows界面,并且在主窗口上绘制所有子控件,也就是所谓的directUI技术;

    2. GifSmiley是程序中用来解析和显示gif格式的图片的库,以支持gif图片的动画效果;

    3. httpclient功能是程序中使用的http请求库,登录前程序会先连接服务器的login_server以获得后续需要登录的msg_server的ip地址和端口号 等信息,这里就是使用的http协议,同时聊天过程中收发的聊天图片与图片服务器msfs也使用http协议来收发这些图片;

    4. libogg是一个语音库,用来解析声音文件的,因为pc客户端可能会收到移动端的语音聊天,相比较传统的*.wav、*.mp3、*.wma,*.ogg格式的不仅音质高,而且音频文件的体积小,腾讯的QQ游戏英雄杀中的语音也是使用这个格式的。

    5. libspeex是一个音频压缩库;

    6. Modules就是TeamTalk中使用的各种库了,展开来看下你就明白了:

     

    7. network是teamtalk使用的网络通信的代码,其实teamtalk pc端和服务器端使用的是同一套网络通信库,只不过如果服务器运行在linux下,其核心的IO复用模型是epoll,而pc客户端使用的IO复用模型是select;

    8. speexdec 也是和ogg格式相关的编码和解码器;

    9. teamtalk是主程序入口工程;

    10. utility包含了teamtalk中用到的一些工具类工程,比如sqlite的包装接口、md5工具类等。

     

    除了上面介绍的一些库以外,程序还使用了sqlite库、谷歌protobuf库、日志库yaolog等。关于yaolog可参见http://blog.csdn.net/gemo/article/details/8499692,这个日志库比较有意思的地方是可以单独打印出网络通信中的字节流的二进制形式,推荐一下,效果如下图所示(位于win-client\bin\teamtalk\Debug\log\socket.log文件中):

    三、程序总体框架介绍

    整个程序使用了mfc框架来做一个架子,而所有的窗口和对话框都使用的是duilib,关于duilib网上有很多资料,这里不介绍duilib细节的东西了。一个mfc程序框架,使用起来也很简单,就是定义一个类集成mfc的CWinApp类,并改写其InitInstance()方法,mfc内部会替我们做好消息循环的步骤。TeamTalk相关的代码如下:

     

    //位于teamtalk.h中
    class CteamtalkApp : public CWinApp
    {
    public:
    	CteamtalkApp();
    
    public:
    	virtual BOOL InitInstance();
    	virtual BOOL ExitInstance();
    
    private:
    	/**
    	 *  创建用户目录
    	 *
    	 * @return  BOOL
    	 * @exception there is no any exception to throw.
    	 */	
    	BOOL _CreateUsersFolder();
    	/**
    	 * 创建主窗口
    	 *
    	 * @return  BOOL
    	 * @exception there is no any exception to throw.
    	 */	
    	BOOL _CreateMainDialog();
    	/**
    	* 销毁主窗口
    	*
    	* @return  BOOL
    	* @exception there is no any exception to throw.
    	*/
    	BOOL _DestroyMainDialog();
    	/**
    	* 判断是否是单实例
    	*
    	* @return  BOOL
    	* @exception there is no any exception to throw.
    	*/
    	BOOL _IsHaveInstance();
    
    	void _InitLog();
    
    private:
    	MainDialog*						m_pMainDialog;
    };
    

     

     

    在teamtalk.cpp中定义了唯一的全局对象CteamtalkApp对象:


     

    接着,所有的初始化工作就是写在CteamtalkApp::InitInstance()方法中了:

     

    BOOL CteamtalkApp::InitInstance()
    {
    	INITCOMMONCONTROLSEX InitCtrls;
    	InitCtrls.dwSize = sizeof(InitCtrls);
    	InitCtrls.dwICC = ICC_WIN95_CLASSES;
    	InitCommonControlsEx(&InitCtrls);
    
    	//log init
    	_InitLog();
    
    	// Verify that the version of the library that we linked against is
    	// compatible with the version of the headers we compiled against.
    	GOOGLE_PROTOBUF_VERIFY_VERSION;
    
    	LOG__(APP, _T("===================================VersionNO:%d======BulidTime:%s--%s==========================")
    		, TEAMTALK_VERSION, util::utf8ToCString(__DATE__), util::utf8ToCString(__TIME__));
    	if (!__super::InitInstance())
    	{
    		LOG__(ERR, _T("__super::InitInstance failed."));
    		return FALSE;
    	}
    	AfxEnableControlContainer();
    
        //为了调试方便,暂且注释掉
    	//if (_IsHaveInstance())
    	//{
    	//	LOG__(ERR, _T("Had one instance,this will exit"));
    	//	HWND hwndMain = FindWindow(_T("TeamTalkMainDialog"), NULL);
    	//	if (hwndMain)
    	//	{
    	//		::SendMessage(hwndMain, WM_START_MOGUTALKINSTANCE, NULL, NULL);
    	//	}
    	//	return FALSE;
    	//}
    
    	//start imcore lib
        //在这里启动任务队列和网络IO线程
    	if (!imcore::IMLibCoreRunEvent())
    	{
    		LOG__(ERR, _T("start imcore lib failed!"));
    	}
    	LOG__(APP, _T("start imcore lib done"));
    
    	//start ui event
        //在这里创建代理窗口并启动定时器定时处理任务
    	if (module::getEventManager()->startup() != imcore::IMCORE_OK)
    	{
    		LOG__(ERR, _T("start ui event failed"));
    	}
    	LOG__(APP, _T("start ui event done"));
    
    	//create user folders
    	_CreateUsersFolder();
    	
    	//duilib初始化
    	CPaintManagerUI::SetInstance(AfxGetInstanceHandle());
    	CPaintManagerUI::SetResourcePath(CPaintManagerUI::GetInstancePath() + _T("..\\gui\\"));//track这个设置了路径,会导致base里设置的无效。
    	::CoInitialize(NULL);
    	::OleInitialize(NULL);
    
    	//无需配置server
    	module::TTConfig* pCfg = module::getSysConfigModule()->getSystemConfig();
    	if (pCfg && pCfg->loginServIP.IsEmpty())
    	{
    		if (!module::getSysConfigModule()->showServerConfigDialog(NULL))
    		{
    			LOG__(APP, _T("server config canceled"));
    			return FALSE;
    		}
    	}
    
    	if (!module::getLoginModule()->showLoginDialog())
    	{
    		LOG__(ERR, _T("login canceled"));
    		return FALSE;
    	}
    	LOG__(APP,_T("login success"));
    
    	//创建主窗口
    	if (!_CreateMainDialog())
    	{
    		LOG__(ERR, _T("Create MianDialog failed"));
    		return FALSE;
    	}
    	LOG__(APP, _T("Create MianDialog done"));
    
    	CPaintManagerUI::MessageLoop();
    	CPaintManagerUI::Term();
    
    	return TRUE;
    }


    上述代码大致做了以下工作:

     

     

    // 1. 初始化yaolog日志库
    
    // 2. google protobuf的版本号检测
    
    // 3. 启动网络通信线程检测网络数据读写,再启动一个线程创建一个队列,如果队列中有任务,则取出该任务执行
    
    // 4. 创建支线程与UI线程的桥梁——代理窗口
    
    // 5. 创建用户文件夹
    
    // 6. 配置duilib的资源文件路径、初始化com库、初始化ole库 
    
    // 7. 如果没有配置登录服务器的地址,则显示配置对话框
    
    // 8. 显示登录对话框
    
    // 9. 登录成功后,登录对话框销毁,显示主对话框
    
    // 10. 启动duilib的消息循环(也就是说不使用mfc的消息循环)


    其它的没什么好介绍的,我们来重点介绍下第3点和第4点。先说第3点,在第3点中又会牵扯出第4点,网络通信线程的启动:

     

     

    //start imcore lib
    //在这里启动任务队列和网络IO线程
    if (!imcore::IMLibCoreRunEvent())
    {
    	LOG__(ERR, _T("start imcore lib failed!"));
    }
    LOG__(APP, _T("start imcore lib done"));	LOG__(ERR, _T("start imcore lib failed!"));
    }
    LOG__(APP, _T("start imcore lib done"));

     

     

    bool IMLibCoreRunEvent()
    {	
    	LOG__(NET, _T("==============================================================================="));
    
    	//在这里启动任务队列处理线程
    	getOperationManager()->startup();
    
    	CAutoLock lock(&g_lock);
    	if (!netlib_is_running())
    	{
    #ifdef _MSC_VER
    		unsigned int m_dwThreadID;
    		//在这里启动网络IO线程
    		g_hThreadHandle = (HANDLE)_beginthreadex(0, 0, event_run, 0, 0, (unsigned*)&m_dwThreadID);
    		if (g_hThreadHandle < (HANDLE)2)
    		{
    			m_dwThreadID = 0;
    			g_hThreadHandle = 0;
    		}
    		return g_hThreadHandle >(HANDLE)1;
    #else
    		pthread_t pt;
    		pthread_create(&pt, NULL, event_run, NULL);
    #endif
    	}
    
    	return true;
    }


    先看getOperationManager()->startup();:

     

     

    IMCoreErrorCode OperationManager::startup()
    {
    	m_operationThread = std::thread([&]
    	{
    		std::unique_lock <std::mutex> lck(m_cvMutex);
    		Operation* pOperation = nullptr;
    		while (m_bContinue)
    		{
    			if (!m_bContinue)
    				break;
    			if (m_vecRealtimeOperations.empty())
    				m_CV.wait(lck);
    			if (!m_bContinue)
    				break;
    			{
    				std::lock_guard<std::mutex> lock(m_mutexOperation);
    				if (m_vecRealtimeOperations.empty())
    					continue;
    				pOperation = m_vecRealtimeOperations.front();
    				m_vecRealtimeOperations.pop_front();
    			}
    
    			if (!m_bContinue)
    				break;
    
    			if (pOperation)
    			{
    				pOperation->process();
    				pOperation->release();
    			}
    		}
    	});
    
    	return IMCORE_OK;
    }

     

     

    这里利用一个C++11的新语法lamda表达式来创建一个线程,线程函数就是lamda表达式的具体内容:先从队列中取出任务,然后执行。所有的任务都继承其基类Operation,而Operation又继承接口类IOperatio,任务类根据自己具体需要做什么来改写process()方法:

     

    class NETWORK_DLL Operation : public IOperation
    {
    	enum OperationState
    	{
    		OPERATION_IDLE = 0,
    		OPERATION_STARTING,
    		OPERATION_RUNNING,
    		OPERATION_CANCELLING,
    		OPERATION_FINISHED
    	};
    
    public:
        /** @name Constructors and Destructor*/
    
        //@{
        /**
         * Constructor 
         */
        Operation();
    	Operation(const std::string& name);
        /**
         * Destructor
         */
        virtual ~Operation();
        //@}
    
    public:
    	virtual void processOpertion() = 0;
    
    public:
    	virtual void process();
    	virtual void release();
    
        inline std::string name() const { return m_name; }
        inline void set_name(__in std::string name){ m_name = name; }
    
    private:
    	OperationState			m_state;
        std::string				m_name;
    };
    

     

    struct NETWORK_DLL IOperation
    {
    public:
    	virtual void process() = 0;
    //private:
    	/**
    	* 必须让容器来释放自己
    	*
    	* @return  void
    	* @exception there is no any exception to throw.
    	*/
    	virtual void release() = 0;
    };
    


    这里我们介绍的任务队列我们称为队列A,下文中还有一个专门做http请求的队列,我们称为队列B。

     

    后半部分代码其实就是启动网络检测线程,检测网络数据读写:

     

    g_hThreadHandle = (HANDLE)_beginthreadex(0, 0, event_run, 0, 0, (unsigned*)&m_dwThreadID);

     

    unsigned int __stdcall event_run(void* threadArgu)
    {
    	LOG__(NET,  _T("event_run"));
    	netlib_init();
    	netlib_set_running();
    	netlib_eventloop();
    	return NULL;
    }

     

    void netlib_eventloop(uint32_t wait_timeout)
    {
    	CEventDispatch::Instance()->StartDispatch(wait_timeout);
    }

     

     

    void CEventDispatch::StartDispatch(uint32_t wait_timeout)
    {
    	fd_set read_set, write_set, excep_set;
    	timeval timeout;
    	timeout.tv_sec = 1;	//wait_timeout 1 second
    	timeout.tv_usec = 0;
    
        while (running)
    	{
    		//_CheckTimer();
    		//_CheckLoop();
    
    		if (!m_read_set.fd_count && !m_write_set.fd_count && !m_excep_set.fd_count)
    		{
    			Sleep(MIN_TIMER_DURATION);
    			continue;
    		}
    
    		m_lock.lock();
    		FD_ZERO(&read_set);
    		FD_ZERO(&write_set);
    		FD_ZERO(&excep_set);
    		memcpy(&read_set, &m_read_set, sizeof(fd_set));
    		memcpy(&write_set, &m_write_set, sizeof(fd_set));
    		memcpy(&excep_set, &m_excep_set, sizeof(fd_set));
    		m_lock.unlock();
    
    		if (!running)
    			break;
    
    		//for (int i = 0; i < read_set.fd_count; i++) {
    		//	LOG__(NET,  "read fd: %d\n", read_set.fd_array[i]);
    		//}
    		int nfds = select(0, &read_set, &write_set, &excep_set, &timeout);
    		if (nfds == SOCKET_ERROR)
    		{
    			//LOG__(NET,  "select failed, error code: %d\n", GetLastError());
    			Sleep(MIN_TIMER_DURATION);
    			continue;			// select again
    		}
    		if (nfds == 0)
    		{
    			continue;
    		}
    		for (u_int i = 0; i < read_set.fd_count; i++)
    		{
    			//LOG__(NET,  "select return read count=%d\n", read_set.fd_count);
    			SOCKET fd = read_set.fd_array[i];
    			CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
    			if (pSocket)
    			{
    				pSocket->OnRead();
    				pSocket->ReleaseRef();
    			}
    		}
    		for (u_int i = 0; i < write_set.fd_count; i++)
    		{
    			//LOG__(NET,  "select return write count=%d\n", write_set.fd_count);
    			SOCKET fd = write_set.fd_array[i];
    			CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
    			if (pSocket)
    			{
    				pSocket->OnWrite();
    				pSocket->ReleaseRef();
    			}
    		}
    		for (u_int i = 0; i < excep_set.fd_count; i++)
    		{
    			LOG__(NET,  _T("select return exception count=%d"), excep_set.fd_count);
    			SOCKET fd = excep_set.fd_array[i];
    			CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
    			if (pSocket)
    			{
    				pSocket->OnClose();
    				pSocket->ReleaseRef();
    			}
    		}
    	}
    }

     

     

     

    我们举个具体的例子来说明这个三个线程的逻辑(任务队列A、网络线程和下文要介绍的专门处理http请求的任务队列B)和代理窗口的消息队列,以在登录对话框输入用户名和密码后接下来的步骤:

     

    //位于LoginDialog.cpp中
    void LoginDialog::_DoLogin()
    {
    	LOG__(APP,_T("User Clicked LoginBtn"));
    
    	m_ptxtTip->SetText(_T(""));
    	CDuiString userName = m_pedtUserName->GetText();
    	CDuiString password = m_pedtPassword->GetText();
    	if (userName.IsEmpty())
    	{
    		CString csTip = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_USERNAME_EMPTY"));
    		m_ptxtTip->SetText(csTip);
    		return;
    	}
    	if (password.IsEmpty())
    	{
    		CString csTip = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_PASSWORD_EMPTY"));
    		m_ptxtTip->SetText(csTip);
    		return;
    	}
    	module::TTConfig* pCfg = module::getSysConfigModule()->getSystemConfig();
    	pCfg->userName = userName;
    	if (m_bPassChanged)
    	{
    		std::string sPass = util::cStringToString(CString(password));
    		char* pOutData = 0;
    		uint32_t nOutLen = 0;
    		int retCode = EncryptPass(sPass.c_str(), sPass.length(), &pOutData, nOutLen);
    		if (retCode == 0 && nOutLen > 0 && pOutData != 0)
    		{
    			pCfg->password = std::string(pOutData, nOutLen);
    			Free(pOutData);
    		}
    		else
    		{
    			LOG__(ERR, _T("EncryptPass Failed!"));
    			CString csTip = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_LOGIN_ENCRYPT_PASE_FAIL"));
    			m_ptxtTip->SetText(csTip);			
    			return;
    		}
    	}
    
    	pCfg->isRememberPWD = m_pChkRememberPWD->GetCheck();
    	module::getSysConfigModule()->saveData();
    
    	CString csTxt = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_BTN_DOLOGIN"));
    	m_pBtnLogin->SetText(csTxt);
    	m_pBtnLogin->SetEnabled(false);
    
    	//连接登陆服务器
    	DoLoginServerParam param;
    	DoLoginServerHttpOperation* pOper = new DoLoginServerHttpOperation(
    		BIND_CALLBACK_1(LoginDialog::OnHttpCallbackOperation), param);
    	module::getHttpPoolModule()->pushHttpOperation(pOper);
    }


    点击登录按钮之后,程序先对用户名和密码进行一些有效性校验,接着产生一个DoLoginServerHttpOperation对象,该类继承IHttpOperation,IHttpOperation再继承ICallbackOpertaion,ICallbackOpertaion再继承Operation类。这个任务会绑定一个任务完成之后的回调函数,即宏BIND_CALLBACK_1,这个宏实际上就是std::bind:

     

     

    #define BIND_CALLBACK_1(func)   std::bind(&func, this, placeholders::_1)
    #define BIND_CALLBACK_2(func)	std::bind(&func, this, placeholders::_1, placeholders::_2)


    往任务队列中放入任务的动作如下:

     

     

    void HttpPoolModule_Impl::pushHttpOperation(module::IHttpOperation* pOperaion, BOOL bHighPriority /*= FALSE*/)
    {
    	if (NULL == pOperaion)
    	{
    		return;
    	}
    
    	CAutoLock lock(&m_mtxLock);
    	if (bHighPriority)
    		m_lstHttpOpers.push_front(pOperaion);
    	else
    		m_lstHttpOpers.push_back(pOperaion);
    	_launchThread();
    	::ReleaseSemaphore(m_hSemaphore, 1, NULL);
    
    	return;
    }


    其中_launchThread()会启动一个线程,该线程函数是另外一个任务队列,专门处理http任务:

     

     

    BOOL HttpPoolModule_Impl::_launchThread()
    {
    	if ((int)m_vecHttpThread.size() >= MAX_THEAD_COUNT)
    	{
    		return TRUE;
    	}
    
    	TTHttpThread* pThread = new TTHttpThread();
    	PTR_FALSE(pThread);
    	if (!pThread->create())
    	{
    		return FALSE;
    	}
    	Sleep(300);
    
    	m_vecHttpThread.push_back(pThread);
    
    	return TRUE;
    }


    线程函数最终实际执行代码如下:

     

     

    UInt32 TTHttpThread::process()
    {
    	module::IHttpOperation * pHttpOper = NULL;
    	HttpPoolModule_Impl *pPool = m_pInstance;
    	while (m_bContinue)
    	{
    		if (WAIT_OBJECT_0 != ::WaitForSingleObject(pPool->m_hSemaphore, INFINITE))
    		{
    			break;
    		}
    
    		if (!m_bContinue)
    		{
    			break;
    		}
    
    		{
    			CAutoLock lock(&(pPool->m_mtxLock));
    			if (pPool->m_lstHttpOpers.empty())
    				pHttpOper = NULL;
    			else
    			{
    				pHttpOper = pPool->m_lstHttpOpers.front();
    				pPool->m_lstHttpOpers.pop_front();
    			}
    		}
    
    		try
    		{
    			if (m_bContinue && pHttpOper)
    			{
    				pHttpOper->process();
    				pHttpOper->release();
    			}
    		}
    		catch (...)
    		{
    			LOG__(ERR, _T("TTHttpThread: Failed to execute opertaion(0x%p)"), pHttpOper);
    		}
    	}
    
    	return 0;
    }


    当这个http任务被任务队列执行时,实际执行DoLoginServerHttpOperation::processOpertion(),代码如下:

     

     

    void DoLoginServerHttpOperation::processOpertion()
    {
    	module::TTConfig* pCfg = module::getSysConfigModule()->getSystemConfig();
    	LOG__(APP, _T("loginAddr = %s"), pCfg->loginServIP);
    	std::string& loginAddr = util::cStringToString(pCfg->loginServIP);
    	std::string url = loginAddr;
    	
    	DoLoginServerParam* pPamram = new DoLoginServerParam();
    	pPamram->resMsg = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_LOGIN_HTTP_DEFERROR"));
    	Http::HttpResponse	response;
    	Http::HttpClient	client;
        //对于登录:url=http://192.168.226.128:8080/msg_server
    	Http::HttpRequest	request("get", url);
    	if (!client.execute(&request, &response))
    	{
    		CString csTemp = util::stringToCString(url);
    		pPamram->result = DOLOGIN_FAIL;
    		LOG__(ERR,_T("failed %s"), csTemp);
    		asyncCallback(std::shared_ptr<void>(pPamram));
    		client.killSelf();
    		return;
    	}
        /**
            {
               "backupIP" : "localhost",
               "code" : 0,
               "discovery" : "http://127.0.0.1/api/discovery",
               "msfsBackup" : "http://127.0.0.1:8700/",
               "msfsPrior" : "http://127.0.0.1:8700/",
               "msg" : "",
               "port" : "8000",
               "priorIP" : "localhost"
            }
         */
    	std::string body = response.getBody();
    	client.killSelf();
    	//json解析
    	try
    	{
    		Json::Reader reader;
    		Json::Value root;
    		if (!reader.parse(body, root))
    		{
    			CString csTemp = util::stringToCString(body);
    			LOG__(ERR, _T("parse data failed,%s"), csTemp);
    			pPamram->result = DOLOGIN_FAIL;
    			pPamram->resMsg = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_LOGIN_HTTP_JSONERROR"));
    			goto End;
    		}
    		int nCode = root.get("code", "").asInt();
    		if (0 == nCode)//登陆成功
    		{
    			LOG__(APP, _T("get msgSvr IP succeed!"));
    			pCfg->msgSevPriorIP = root.get("priorIP", "").asString();
    			pCfg->msgSevBackupIP = root.get("backupIP", "").asString();
    			std::string strPort = root.get("port", "").asString();
    			pCfg->msgServPort = util::stringToInt32(strPort);
    
    			pCfg->fileSysAddr = util::stringToCString(root.get("msfsPrior", "").asString());
    			pCfg->fileSysBackUpAddr = util::stringToCString(root.get("msfsBackup", "").asString());
    			pPamram->result = DOLOGIN_SUCC;
    		}
    		else
    		{
    			LOG__(ERR, _T("get msgSvr IP failed! Code = %d"),nCode);
    			pPamram->result = DOLOGIN_FAIL;
    			CString csRetMsgTemp = util::stringToCString(root.get("msg", "").asString());
    			if (!csRetMsgTemp.IsEmpty())
    				pPamram->resMsg = csRetMsgTemp;
    		}
    	}
    	catch (...)
    	{
    		CString csTemp = util::stringToCString(body);
    		LOG__(ERR,_T("parse json execption,%s"), csTemp);
    		pPamram->result = DOLOGIN_FAIL;
    		pPamram->resMsg = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_LOGIN_HTTP_JSONERROR"));
    	}
    
    End:
    	asyncCallback(std::shared_ptr<void>(pPamram));
    }


    实际上是向login_server发送一个http请求,这是一个同步请求。得到的结果是一个json字符串,代码注释中已经给出。然后调用asyncCallback(std::shared_ptr<void>(pPamram));参数pPamram携带了当前任务的回调函数指针:

     

     

    /**
    * 异步回调,借助UIEvent
    *
    * @param   std::shared_ptr<void> param
    * @return  void
    * @exception there is no any exception to throw.
    */
    
    void asyncCallback(std::shared_ptr<void> param)
    {
    	CallbackOperationEvent* pEvent = new CallbackOperationEvent(m_callback, param);
    	module::getEventManager()->asynFireUIEvent(pEvent);
    }


    这实际上产生了一个回调事件。也就是说队列B做http请求,操作完成后往代理窗口的消息队列中放入一个回调事件,这个事件通过代理窗口过程函数来处理的(这就是上文中第4点介绍的代理窗口过程的作用,实际上是利用windows消息队列来做任务处理(系统有现成的任务队列系统,为何不利用呢?)):

     

     

    module::IMCoreErrorCode UIEventManager::asynFireUIEvent(IN const IEvent* const pEvent)
    {
    	assert(m_hWnd);
    	assert(pEvent);
    	if (0 == m_hWnd || 0 == pEvent)
    		return IMCORE_ARGUMENT_ERROR;
    
    	if (FALSE == ::PostMessage(m_hWnd, UI_EVENT_MSG, reinterpret_cast<WPARAM>(this), reinterpret_cast<WPARAM>(pEvent)))
    		return IMCORE_WORK_POSTMESSAGE_ERROR;
    
    	return IMCORE_OK;
    }


    看到没有?向代理窗口的消息队列中投递一个UI_EVENT_MSG事件,并在消息参数LPARAM中传递了回调事件的对象指针。这样代理窗口过程函数就可以处理这个消息了:

     

     

    LRESULT _stdcall UIEventManager::_WindowProc(HWND hWnd
    											, UINT message
    											, WPARAM wparam
    											, LPARAM lparam)
    {
    	switch (message)
    	{
    	case UI_EVENT_MSG:
    		reinterpret_cast<UIEventManager*>(wparam)->_processEvent(reinterpret_cast<IEvent*>(lparam), TRUE);
    		break;
    	case WM_TIMER:
    		reinterpret_cast<UIEventManager*>(wparam)->_processTimer();
    		break;
    	default:
    		break;
    	}
    	return ::DefWindowProc(hWnd, message, wparam, lparam);
    }

     

     

    void UIEventManager::_processEvent(IEvent* pEvent, BOOL bRelease)
    {
    	assert(pEvent);
    	if (0 == pEvent)
    		return;
    
    	try
    	{
    		pEvent->process();
    		if (bRelease)
    			pEvent->release();
    	}
    	catch (imcore::Exception *e)
    	{
    		LOG__(ERR, _T("event run exception"));
    		pEvent->onException(e);
    		if (bRelease)
    			pEvent->release();
    		if (e)
    		{
    			LOG__(ERR, _T("event run exception:%s"), util::stringToCString(e->m_msg));
    			assert(FALSE);
    		}
    	}
    	catch (...)
    	{
    		LOG__(ERR, _T("operation run exception,unknown reason"));
    		if (bRelease)
    			pEvent->release();
    		assert(FALSE);
    	}
    }


    根据C++的多态特性,pEvent->process()实际上调用的是CallbackOperationEvent.process()。代码如下:

     

    	virtual void process()
    	{
    		m_callback(m_param);
    	}


    m_callback(m_param);调用的就是上文中介绍DoLoginServerHttpOperation操作的回调函数LoginDialog::OnHttpCallbackOperation():

     

     

    void LoginDialog::OnHttpCallbackOperation(std::shared_ptr<void> param)
    {
    	DoLoginServerParam* pParam = (DoLoginServerParam*)param.get();
    	if (DOLOGIN_SUCC == pParam->result)
    	{
    		module::TTConfig* pCfg = module::getSysConfigModule()->getSystemConfig();
    		PTR_VOID(pCfg);
    		LoginParam loginparam;
    		loginparam.csUserName = pCfg->userName;
    		loginparam.password = pCfg->password;
    		loginparam.csUserName.Trim();
    		LoginOperation* pOperation = new LoginOperation(
    			BIND_CALLBACK_1(LoginDialog::OnOperationCallback), loginparam);
    		imcore::IMLibCoreStartOperation(pOperation);
    	}
    	else
    	{
    		m_ptxtTip->SetText(pParam->resMsg);
    		module::TTConfig* pCfg = module::getSysConfigModule()->getSystemConfig();
    		LOG__(ERR, _T("get MsgServer config faild,login server addres:%s:%d"), pCfg->loginServIP,pCfg->loginServPort);
    
    		CString csTxt = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_BTN_LOGIN"));
    		m_pBtnLogin->SetText(csTxt);
    		m_pBtnLogin->SetEnabled(true);
    	}
    }

     

     

    ok,终于到家了。但是这并没结束,我们只介绍了队列B和代理窗口消息队列,还有队列A呢?LoginDialog::OnHttpCallbackOperation()会根据获取的msg_server的情况来再次产生一个新的任务LoginOperation来放入队列A中,这次才是真正的用户登录,根据上面的介绍,LoginOperation任务从队列A中取出来之后,实际执行的是LoginOperation::processOpertion():

     

    void LoginOperation::processOpertion()
    {
    	LOG__(APP,_T("login start,uname:%s,status:%d"), m_loginParam.csUserName
    		, m_loginParam.mySelectedStatus);
    
    	LoginParam* pParam = new LoginParam;
    	pParam->csUserName = m_loginParam.csUserName;
    	pParam->mySelectedStatus = m_loginParam.mySelectedStatus;
    
    	//连接消息服务器
    	module::TTConfig* pCfg = module::getSysConfigModule()->getSystemConfig();
    	CString server = util::stringToCString(pCfg->msgSevPriorIP);
    	LOG__(APP, _T("MsgServeIp:%s,Port:%d"), server, pCfg->msgServPort);
        //8000端口
    	IM::Login::IMLoginRes* pImLoginResp = (IM::Login::IMLoginRes*)module::getTcpClientModule()
    		->doLogin(server, pCfg->msgServPort,m_loginParam.csUserName,m_loginParam.password);
    	if (0 == pImLoginResp || pImLoginResp->result_code() != IM::BaseDefine::REFUSE_REASON_NONE 
    		|| !pImLoginResp->has_user_info())
    	{
    		//TODO,若失败,尝试备用IP
    		LOG__(ERR,_T("add:%s:%d,uname:%s,login for msg server failed"),server,pCfg->msgServPort, m_loginParam.csUserName);
    		if (pImLoginResp)
    		{
    			CString errInfo = util::stringToCString(pImLoginResp->result_string());
    			pParam->errInfo = errInfo;
    			pParam->result = LOGIN_FAIL;
    			pParam->server_result = pImLoginResp->result_code();
    			LOG__(ERR, _T("error code :%d,error info:%s"), pImLoginResp->result_code(), errInfo);
    		}
    		else
    		{
    			pParam->result = IM::BaseDefine::REFUSE_REASON_NO_MSG_SERVER;
    			LOG__(ERR, _T("login msg server faild!"));
    		}
    		asyncCallback(std::shared_ptr<void>(pParam));
    		return;
    	}
    	pParam->result = LOGIN_OK;
    	pParam->serverTime = pImLoginResp->server_time();
    	pParam->mySelectedStatus = pImLoginResp->online_status();
    
    	//存储服务器端返回的userId
    	IM::BaseDefine::UserInfo userInfo = pImLoginResp->user_info();
    	pCfg->userId = util::uint32ToString(userInfo.user_id());
    	pCfg->csUserId = util::stringToCString(pCfg->userId);
    
    	//登陆成功,创建自己的信息
    	module::UserInfoEntity myInfo;
    	myInfo.sId = pCfg->userId;
    	myInfo.csName = m_loginParam.csUserName;
    	myInfo.onlineState = IM::BaseDefine::USER_STATUS_ONLINE;
    	myInfo.csNickName = util::stringToCString(userInfo.user_nick_name());
    	myInfo.avatarUrl = userInfo.avatar_url();
    	myInfo.dId = util::uint32ToString(userInfo.department_id());
    	myInfo.department = myInfo.dId;
    	myInfo.email = userInfo.email();
    	myInfo.gender = userInfo.user_gender();
    	myInfo.user_domain = userInfo.user_domain();
    	myInfo.telephone = userInfo.user_tel();
    	myInfo.status = userInfo.status();
        myInfo.signature = userInfo.sign_info();
    
    	module::getUserListModule()->createUserInfo(myInfo);
    
    	asyncCallback(std::shared_ptr<void>(pParam));
    
    	LOG__(APP, _T("login succeed! Name = %s Nickname = %s sId = %s status = %d")
    		, m_loginParam.csUserName
    		, util::stringToCString(userInfo.user_nick_name())
    		, module::getSysConfigModule()->UserID()
    		, m_loginParam.mySelectedStatus);
    
    	//开始发送心跳包
    	module::getTcpClientModule()->startHeartbeat();
    }


    同理,数据包发生成功以后,会再往代理窗口的消息队列中产生一个回调事件,最终调用刚才说的LoginOperation绑定的回调函数:

     

     

    void asyncCallback(std::shared_ptr<void> param)
    {
    	CallbackOperationEvent* pEvent = new CallbackOperationEvent(m_callback, param);
    	module::getEventManager()->asynFireUIEvent(pEvent);
    }

     

    void LoginDialog::OnOperationCallback(std::shared_ptr<void> param)
    {
    	LoginParam* pLoginParam = (LoginParam*)param.get();
    
    
        if (LOGIN_OK == pLoginParam->result)	//登陆成功
    	{
    		Close(IDOK);
    
    		//创建用户目录
    		_CreateUsersFolder();
    
    		//开启同步消息时间timer
    		module::getSessionModule()->startSyncTimeTimer();
    		module::getSessionModule()->setTime(pLoginParam->serverTime);
    
    		//通知服务器客户端初始化完毕,获取组织架构信息和群列表
    		module::getLoginModule()->notifyLoginDone();
    	}
    	else	//登陆失败处理
    	{
    		module::getTcpClientModule()->shutdown();
    		if (IM::BaseDefine::REFUSE_REASON_NO_MSG_SERVER == pLoginParam->server_result)
    		{
    			CString csTip = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_LOGIN_MSGSVR_FAIL"));
    			m_ptxtTip->SetText(csTip);
    		}
    		else if (!pLoginParam->errInfo.IsEmpty())
    		{
    			m_ptxtTip->SetText(pLoginParam->errInfo);
    		}
    		else
    		{
    			CString errorCode = util::int32ToCString(pLoginParam->server_result);
    			CString csTip = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_LOGIN_UNKNOWN_ERROR"));
    			m_ptxtTip->SetText(csTip + CString(":") + errorCode);
    		}
    	}
    
    	CString csTxt = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_BTN_LOGIN"));
    	m_pBtnLogin->SetText(csTxt);
    	m_pBtnLogin->SetEnabled(true);
    }


    至此,登录才成功。等等,那数据包是怎么发到服务器的呢?这也是一个重点,我们来详细地介绍一下,LoginOperation::processOpertion()中有这一行代码:

     


    doLogin函数代码如下:

     

    IM::Login::IMLoginRes* TcpClientModule_Impl::doLogin(CString &linkaddr, UInt16 port
    	,CString& uName,std::string& pass)
    {
    	m_socketHandle = imcore::IMLibCoreConnect(util::cStringToString(linkaddr), port);
    	imcore::IMLibCoreRegisterCallback(m_socketHandle, this);
    	if(util::waitSingleObject(m_eventConnected, 5000))
    	{
    		IM::Login::IMLoginReq imLoginReq;
    		string& name = util::cStringToString(uName);
    		imLoginReq.set_user_name(name);
    		imLoginReq.set_password(pass);
    		imLoginReq.set_online_status(IM::BaseDefine::USER_STATUS_ONLINE);
    		imLoginReq.set_client_type(IM::BaseDefine::CLIENT_TYPE_WINDOWS);
    		imLoginReq.set_client_version("win_10086");
    
    		if (TCPCLIENT_STATE_OK != m_tcpClientState)
    			return 0;
    
    		sendPacket(IM::BaseDefine::SID_LOGIN, IM::BaseDefine::CID_LOGIN_REQ_USERLOGIN, ++g_seqNum
    			, &imLoginReq);
    		m_pImLoginResp->Clear();
    		util::waitSingleObject(m_eventReceived, 10000);
    	}
    
    	return m_pImLoginResp;
    }

     


    这段代码先连接服务器,然后调用sendPacket()发送登录数据包。如何连接服务器使用了一些“奇技淫巧”,我们后面单独介绍。我们这里先来看sendPacket()发包代码:

     

     

     

    void TcpClientModule_Impl::sendPacket(UInt16 moduleId, UInt16 cmdId, UInt16 seq, google::protobuf::MessageLite* pbBody)
    {
    	m_TTPBHeader.clear();
    	m_TTPBHeader.setModuleId(moduleId);
    	m_TTPBHeader.setCommandId(cmdId);
    	m_TTPBHeader.setSeqNumber(seq);
    
    	_sendPacket(pbBody);
    }

     

    void TcpClientModule_Impl::_sendPacket(google::protobuf::MessageLite* pbBody)
    {
    	UInt32 length = imcore::HEADER_LENGTH + pbBody->ByteSize();
    	m_TTPBHeader.setLength(length);
    	std::unique_ptr<byte> data(new byte[length]);
    	memset(data.get(), 0, length);
    	memcpy(data.get(), m_TTPBHeader.getSerializeBuffer(), imcore::HEADER_LENGTH);
    	if (!pbBody->SerializeToArray(data.get() + imcore::HEADER_LENGTH, pbBody->ByteSize()))
    	{
    		LOG__(ERR, _T("pbBody SerializeToArray failed"));
    		return;
    	}
    	imcore::IMLibCoreWrite(m_socketHandle, data.get(), length);
    }


    其实就是序列化成protobuf要求的格式,然后调用imcore::IMLibCoreWrite(m_socketHandle, data.get(), length);发出去:

     

     

    int IMLibCoreWrite(int key, uchar_t* data, uint32_t size)
    {
    	int nRet = -1;
    	int nHandle = key;
    	CImConn* pConn = TcpSocketsManager::getInstance()->get_client_conn(nHandle);
    	if (pConn) {
    		pConn->Send((void*)data, size);
    	}
    	else {
    		LOG__(NET,  _T("connection is invalied:%d"), key);
    	}
    
    	return nRet;
    }

     

     

    先尝试着直接发送,如果目前tcp窗口太小发不出去,则暂且将数据放在发送缓冲区里面,并检测socket可写事件。这里就是和服务器一样的网络库的代码了,前面一系列的文章,我们已经介绍过了。

    int CImConn::Send(void* data, int len)
    {
    	if (m_busy)
    	{
    		m_out_buf.Write(data, len);
    		return len;
    	}
    
    	int offset = 0;
    	int remain = len;
    	while (remain > 0) {
    		int send_size = remain;
    		if (send_size > NETLIB_MAX_SOCKET_BUF_SIZE) {
    			send_size = NETLIB_MAX_SOCKET_BUF_SIZE;
    		}
    
    		int ret = netlib_send(m_handle, (char*)data + offset, send_size);
    		if (ret <= 0) {
    			ret = 0;
    			break;
    		}
    
    		offset += ret;
    		remain -= ret;
    	}
    
    	if (remain > 0)
    	{
    		m_out_buf.Write((char*)data + offset, remain);
    		m_busy = true;
    		LOG__(NET,  _T("send busy, remain=%d"), m_out_buf.GetWriteOffset());
    	}
    
    	return len;
    }

     


    数据发出去以后,服务器应答登录包,网络线程会检测到socket可读事件:

     

     

    void CBaseSocket::OnRead()
    {
    	if (m_state == SOCKET_STATE_LISTENING)
    	{
    		_AcceptNewSocket();
    	}
    	else
    	{
    		u_long avail = 0;
    		if ( (ioctlsocket(m_socket, FIONREAD, &avail) == SOCKET_ERROR) || (avail == 0) )
    		{
    			m_callback(m_callback_data, NETLIB_MSG_CLOSE, (net_handle_t)m_socket, NULL);
    		}
    		else
    		{
    			m_callback(m_callback_data, NETLIB_MSG_READ, (net_handle_t)m_socket, NULL);
    		}
    	}
    }

     

     

     

     

    void imconn_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam)
    {
    	NOTUSED_ARG(handle);
    	NOTUSED_ARG(pParam);
    
    	CImConn* pConn = TcpSocketsManager::getInstance()->get_client_conn(handle);
    	if (!pConn)
    	{
    		//LOG__(NET, _T("connection is invalied:%d"), handle);
    		return;
    	}
    	pConn->AddRef();
    
    	//	LOG__(NET,  "msg=%d, handle=%d\n", msg, handle);
    
    	switch (msg)
    	{
    	case NETLIB_MSG_CONFIRM:
    		pConn->onConnect();
    		break;
    	case NETLIB_MSG_READ:
    		pConn->OnRead();
    		break;
    	case NETLIB_MSG_WRITE:
    		pConn->OnWrite();
    		break;
    	case NETLIB_MSG_CLOSE:
    		pConn->OnClose();
    		break;
    	default:
    		LOG__(NET,  _T("!!!imconn_callback error msg: %d"), msg);
    		break;
    	}
    
    	pConn->ReleaseRef();
    }

     

     

    void CImConn::OnRead()
    {
    	for (;;)
    	{
    		uint32_t free_buf_len = m_in_buf.GetAllocSize() - m_in_buf.GetWriteOffset();
    		if (free_buf_len < READ_BUF_SIZE)
    			m_in_buf.Extend(READ_BUF_SIZE);
    
    		int ret = netlib_recv(m_handle, m_in_buf.GetBuffer() + m_in_buf.GetWriteOffset(), READ_BUF_SIZE);
    		if (ret <= 0)
    			break;
    
    		m_in_buf.IncWriteOffset(ret);
    		while (m_in_buf.GetWriteOffset() >= imcore::HEADER_LENGTH)
    		{
    			uint32_t len = m_in_buf.GetWriteOffset();
    			uint32_t length = CByteStream::ReadUint32(m_in_buf.GetBuffer());
    			if (length > len)
    				break;
    
    			try
    			{
    				imcore::TTPBHeader pbHeader;
    				pbHeader.unSerialize((byte*)m_in_buf.GetBuffer(), imcore::HEADER_LENGTH);
    				LOG__(NET, _T("OnRead moduleId:0x%x,commandId:0x%x"), pbHeader.getModuleId(), pbHeader.getCommandId());
    				if (m_pTcpSocketCB)
    					m_pTcpSocketCB->onReceiveData((const char*)m_in_buf.GetBuffer(), length);
    				LOGBIN_F__(SOCK, "OnRead", m_in_buf.GetBuffer(), length);
    			}
    			catch (std::exception& ex)
    			{
    				assert(FALSE);
    				LOGA__(NET, "std::exception,info:%s", ex.what());
    				if (m_pTcpSocketCB)
    					m_pTcpSocketCB->onReceiveError();
    			}
    			catch (...)
    			{
    				assert(FALSE);
    				LOG__(NET, _T("unknown exception"));
    				if (m_pTcpSocketCB)
    					m_pTcpSocketCB->onReceiveError();
    			}
    			m_in_buf.Read(NULL, length);
    		}
    	}
    }


    收取数据,并解包:

     

    void TcpClientModule_Impl::onReceiveData(const char* data, int32_t size)
    {
    	if (m_pServerPingTimer)
    		m_pServerPingTimer->m_bHasReceivedPing = TRUE;
    
    	imcore::TTPBHeader header;
    	header.unSerialize((byte*)data, imcore::HEADER_LENGTH);	
    	if (IM::BaseDefine::CID_OTHER_HEARTBEAT == header.getCommandId() && IM::BaseDefine::SID_OTHER == header.getModuleId())
    	{
    		//模块器端过来的心跳包,不跳到业务层派发
    		return;
    	}
    
    	LOG__(NET, _T("receiveData message moduleId:0x%x,commandId:0x%x")
    		, header.getModuleId(), header.getCommandId());
    
    	if (g_seqNum == header.getSeqNumber())
    	{
    		m_pImLoginResp->ParseFromArray(data + imcore::HEADER_LENGTH, size - imcore::HEADER_LENGTH);
    		::SetEvent(m_eventReceived);
    		return;
    	}
    
    	//将网络包包装成任务放到逻辑任务队列里面去
    	_handlePacketOperation(data, size);
    }

     

    void TcpClientModule_Impl::_handlePacketOperation(const char* data, UInt32 size)
    {
    	std::string copyInBuffer(data, size);
    	imcore::IMLibCoreStartOperationWithLambda(
    		[=]()
    	{
    		imcore::TTPBHeader header;
    		header.unSerialize((byte*)copyInBuffer.data(),imcore::HEADER_LENGTH);
    
    		module::IPduPacketParse* pModule
    			= (module::IPduPacketParse*)__getModule(header.getModuleId());
    		if (!pModule)
    		{
    			assert(FALSE);
    			LOG__(ERR, _T("module is null, moduleId:%d,commandId:%d")
    				, header.getModuleId(), header.getCommandId());
    			return;
    		}
    		std::string pbBody(copyInBuffer.data() + imcore::HEADER_LENGTH, size - imcore::HEADER_LENGTH);
    		pModule->onPacket(header, pbBody);
    	});
    }


    根据不同的命令号来做相应的处理:

     

     

    void UserListModule_Impl::onPacket(imcore::TTPBHeader& header, std::string& pbBody)
    {
    	switch (header.getCommandId())
    	{
    	case IM::BaseDefine::BuddyListCmdID::CID_BUDDY_LIST_RECENT_CONTACT_SESSION_RESPONSE:
    		_recentlistResponse(pbBody);
    		break;
    	case IM::BaseDefine::BuddyListCmdID::CID_BUDDY_LIST_STATUS_NOTIFY:
    		_userStatusNotify(pbBody);
    		break;
    	case IM::BaseDefine::BuddyListCmdID::CID_BUDDY_LIST_USER_INFO_RESPONSE:
    		_usersInfoResponse(pbBody);
    		break;
    	case IM::BaseDefine::BuddyListCmdID::CID_BUDDY_LIST_REMOVE_SESSION_RES:
    		_removeSessionResponse(pbBody);
    		break;
    	case IM::BaseDefine::BuddyListCmdID::CID_BUDDY_LIST_ALL_USER_RESPONSE:
    		_allUserlistResponse(pbBody);
    		break;
    	case IM::BaseDefine::BuddyListCmdID::CID_BUDDY_LIST_USERS_STATUS_RESPONSE:
    		_usersLineStatusResponse(pbBody);
    		break;
    	case IM::BaseDefine::BuddyListCmdID::CID_BUDDY_LIST_CHANGE_AVATAR_RESPONSE:
    		_changeAvatarResponse(pbBody);
    		break;
    	case  IM::BaseDefine::CID_BUDDY_LIST_REMOVE_SESSION_NOTIFY:
    		_removeSessionNotify(pbBody);
    		break;
    	case IM::BaseDefine::CID_BUDDY_LIST_DEPARTMENT_RESPONSE:
    		_departmentResponse(pbBody);
    		break;
        case IM::BaseDefine::BuddyListCmdID::CID_BUDDY_LIST_AVATAR_CHANGED_NOTIFY:
            _avatarChangeNotify(pbBody);
            break;
        case IM::BaseDefine::BuddyListCmdID::CID_BUDDY_LIST_CHANGE_SIGN_INFO_RESPONSE:
            _changeSignInfoResponse(pbBody);
            break;
        case IM::BaseDefine::BuddyListCmdID::CID_BUDDY_LIST_SIGN_INFO_CHANGED_NOTIFY:
            _signInfoChangedNotify(pbBody);
            break;
    	default:
    		LOG__(ERR, _T("Unknow commandID:%d"), header.getCommandId());
    		return;
    	}
    }


    每一个处理分支,都最终会产生一个事件放入代理窗口的消息队列中。这前面已经介绍过了。这里我不得不说一点,teamtalk对于其它数据包的应答都是走的上面的介绍的流程,但是对于登录的应答却是使用了一些特殊处理。听我慢慢道来:

     


    上文中发送了登录数据包之后,在那里等一个事件10秒钟,如果10秒内这个事件有信号,则认为登录成功。那么什么情况该事件会有信号呢?

    该事件在构造函数里面创建,默认无信号:

     

    当网络线程收到数据以后(上文逻辑流中介绍过了):

     

    除了心跳包直接过滤以外,通过一个序列号(Seq,变量g_seqNum)唯一标识了登录数据包的应答,如果收到这个序列号的数据,则置信m_eventReceived。这样等待在那里的登录流程就可以返回了,同时也得到了登录应答,登录应答数据记录在成员变量m_pImLoginResp中。如果是其它的数据包,则走的流程是_handlePacketOperation(data, size);,处理逻辑上文也介绍了。

     

    至此,整个客户端程序结构就介绍完了,我们总结一下,实际上程序有如下几类线程:

    1. 网络事件检测线程,用于接收和发送网络数据;

    2. http任务处理线程用于处理http操作;

    3. 普通的任务处理线程,用于处理一般性的任务,比如登录;

    4. UI线程,界面逻辑处理,同时在UI线程里面有一个代理窗口的窗口过程函数,用于非UI线程与UI线程之间的数据流和逻辑中转,核心是利用PostMessage往代理线程投递事件,事件消息参数携带任务信息。

     

    至于,像聊天、查看用户信息这些业务性的内容,留给有兴趣的读者自己去研究吧。

     

    四、程序中使用的一些比较有意思的技巧摘录

    1. 唯一实例判断

    很多程序只能启动一个实例,当你再次启动某个程序的实例时,会激活前一个实例,其实实现起来很简单,就是新建一个命名的Mutex,因为Mutex可以跨进程,当再次启动程序实例时,创建同名的Mutex,会无法创建,错误信息是已经存在。这是windows上非常常用的技巧,如果你从事windows开发,请你务必掌握它。看teamtalk的实现:

    #ifdef _DEBUG
    	#define  AppSingletonMutex _T("{7A666640-EDB3-44CC-954B-0C43F35A2E17}")
    #else
    	#define  AppSingletonMutex _T("{5676532A-6F70-460D-A1F0-81D6E68F046A}")
    #endif
    BOOL CteamtalkApp::_IsHaveInstance()
    {
    	// 单实例运行
    	HANDLE hMutex = ::CreateMutex(NULL, TRUE, AppSingletonMutex);
    	if (hMutex != NULL && GetLastError() == ERROR_ALREADY_EXISTS)
    	{
    		MessageBox(0, _T("上次程序运行还没完全退出,请稍后再启动!"), _T("TeamTalk"), MB_OK);
    		return TRUE;
    	}
    
    	return FALSE;
    }

     

    2. socket函数connect()连接等待时长设定

      传统的做法是将socket设置为非阻塞的,调用完connect函数之后,调用select函数检测socket是否可写,在select函数里面设置超时时间。代码如下:

    为了调试方便,暂且注释掉
    int ret = ::connect(m_hSocket, (struct sockaddr*)&addrSrv, sizeof(addrSrv));
    if (ret == 0)
    {
    	m_bConnected = TRUE;
    	return TRUE;
    }
    
    if (ret == SOCKET_ERROR && WSAGetLastError() != WSAEWOULDBLOCK)
    {
    	return FALSE;
    }
    
    fd_set writeset;
    FD_ZERO(&writeset);
    FD_SET(m_hSocket, &writeset);
    struct timeval tv = { timeout, 0 };
    if (::select(m_hSocket + 1, NULL, &writeset, NULL, &tv) != 1)
    {
    	return FALSE;
    }
    return TRUE;

    我们看看teamtalk里面怎么做的:



     

    红色箭头的地方调用connect函数连接服务器,然后绿色的箭头等待一个事件有信号(内部使用WaitForSingleObject函数),那事件什么时候有信号呢?

     

    网络线程检测第一次到socket可写时,调用onConnectDone函数:

    实际做的事情还是和上面介绍的差不多。其实对于登录流程做成同步的,也是和这个类似,上文中我们介绍过。我早些年刚做windows网络通信方面的项目时,开始总是找不到好的处理等待登录请求应答的方法。这里是一种很不错的设置超时等待的方法。

     

    3. teamtalk的截图功能

         不知道,你在使用qq这样的截图工具时,QQ截图工具能自动检测出某个窗口的范围。这个功能在teamtalk中也有实现,实现代码如下:

     

    BOOL ScreenCapture::initCapture(__in HWND hWnd)
    {
    	//register hot key
    	const std::wstring screenCaptureHotkeyName = L"_SCREEN_CAPTURE_HOTKEY";
        int iHotkeyId = (int)GlobalAddAtom(screenCaptureHotkeyName.c_str());
    
        if (!RegisterHotKey(hWnd, iHotkeyId, MOD_CONTROL | MOD_SHIFT, 0x51)) //ctrl + shift + Q
        {
            GlobalDeleteAtom(iHotkeyId);
        }
    
    	m_iHotkeyId = iHotkeyId;
    	m_hRegisterHotkeyWnd = hWnd;
    
        return createMsgWindow();
    }


    程序初始化时,注册截屏快捷键,这里是ctrl+shift+Q(QQ默认是ctrl+alt+A)。当点击截屏按钮之后,开始启动截图:

     

     

    HWND hDesktopWnd = GetDesktopWindow();
    HDC hScreenDC = GetDC(hDesktopWnd);
    
    RECT rc = { 0 };
    GetWindowRect(hDesktopWnd, &rc);
    int cx = rc.right - rc.left;
    int cy = rc.bottom - rc.top;
    
    HBITMAP hBitmap = CreateCompatibleBitmap(hScreenDC, cx, cy);
    m_hMemDC = CreateCompatibleDC(hScreenDC);
    HGDIOBJ hOldBitmap = SelectObject(m_hMemDC, (HGDIOBJ)hBitmap);
    BitBlt(m_hMemDC, 0, 0, cx, cy, hScreenDC, 0, 0, SRCCOPY);
    
    m_hBkgMemDC = CreateCompatibleDC(hScreenDC);
    HBITMAP hBkgBitmap = CreateCompatibleBitmap(hScreenDC, cx, cy);
    SelectObject(m_hBkgMemDC, (HGDIOBJ)hBkgBitmap);
    BitBlt(m_hBkgMemDC, 0, 0, cx, cy, hScreenDC, 0, 0, SRCCOPY);
    
    HDC hMaskDC = CreateCompatibleDC(hScreenDC);
    HBITMAP hMaskBitmap = CreateCompatibleBitmap(hScreenDC, cx, cy);
    SelectObject(hMaskDC, (HGDIOBJ)hMaskBitmap);
    
    BLENDFUNCTION ftn = { AC_SRC_OVER, 0, 100, 0};
    AlphaBlend(m_hBkgMemDC, 0, 0, cx, cy, hMaskDC, 0, 0, cx, cy, ftn);
    DeleteObject(hMaskBitmap);
    DeleteDC(hMaskDC);
    
    m_hDrawMemDC = CreateCompatibleDC(hScreenDC);
    HBITMAP hDrawBitmap = CreateCompatibleBitmap(hScreenDC, cx, cy);
    SelectObject(m_hDrawMemDC, hDrawBitmap);
    
    ReleaseDC(hDesktopWnd, hScreenDC);
    


    实际上就是在桌面窗口上画图。再遍历当前所有有显示区域的窗口,并记录这些窗口的窗口句柄和矩形区域:

     

     

    for (HWND hWnd = GetTopWindow(NULL); NULL != hWnd; hWnd = GetWindow(hWnd, GW_HWNDNEXT))
    {
    	if (!IsWindow(hWnd)
    		|| !IsWindowVisible(hWnd)
    		|| IsIconic(hWnd))
    	{
    		continue;
    	}
    
    	RECT rcWnd = { 0 };
    	GetWindowRect(hWnd, &rcWnd);
    	adjustRectInScreen(rcWnd);
    	if (ScreenCommon::isRectEmpty(rcWnd))
    	{
    		continue;
    	}
    
    	wchar_t szTxt[MAX_PATH] = { 0 };
    	GetWindowText(hWnd, szTxt, MAX_PATH);
    	if (wcslen(szTxt) <= 0)
    	{
    		continue;
    	}
    
    	//combine the rect with the screen rect
    	m_lsWndList.push_back(ScreenCaptureWndInfo(hWnd, rcWnd));
    }
    
    return m_lsWndList.size() > 0;


    然后显示一个截图工具:

     

     

    BOOL UIScreenCaptureMgr::createWindows()
    {
    	m_hBkgUI = BkgroundUI::Instance()->createWindow();
    
    	wchar_t szImg[MAX_PATH] = {0};
    	GetModuleFileName(NULL, szImg, MAX_PATH);
    	PathRemoveFileSpec(szImg);
    	PathRemoveFileSpec(szImg);
    
    	std::wstring strBkgPic = std::wstring(szImg) + L"\\gui\\ScreenCapture\\sc_toolbar_normal.png";
    	std::wstring strHoverPic = std::wstring(szImg) + L"\\gui\\ScreenCapture\\sc_toolbar_hover.png";
    	std::wstring strSelPic = std::wstring(szImg) + L"\\gui\\ScreenCapture\\sc_toolbar_select.png";
    	
    
    	EditToolbarInfo toolBarInfo = {
    		0, 0, 193, 37,
    		strBkgPic,
    		strHoverPic,
    		strSelPic,
    		{
    			{ 9, 5, 35, 31 },
    			{ 43, 5, 69, 31 },
    			{ 85, 5, 112, 31 },
    			{ 119, 5, 185, 31 }
    		}
    	};
    
    	m_hEditToolBarUI = EditToolbarUI::Instance()->createWindow(toolBarInfo, m_hBkgUI);
        SetWindowPos(m_hBkgUI, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
        forceForgroundWindow(m_hBkgUI);
    	ShowWindow(m_hBkgUI, SW_SHOW);
    
    	return TRUE;
    }


    然后安装一个消息钩子(hook):

     

     

    BOOL ScreenCapture::installMsgHook(BOOL bInstall)
    {
    	BOOL result = FALSE;
    	if (bInstall)
    	{
    		if (!m_hMouseHook)
    		{
    			m_hMouseHook = SetWindowsHookEx(WH_MOUSE, MouseProc, NULL, GetCurrentThreadId());
    			result = (NULL != m_hMouseHook);
    		}
    	}
    	else
    	{
    		UnhookWindowsHookEx(m_hMouseHook);
    		m_hMouseHook = NULL;
    		result = TRUE;
    	}
    
    	return result;
    }

     

    LRESULT ScreenCapture::MouseProc(_In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam)
    {
    	PMOUSEHOOKSTRUCT pHookInfo = (PMOUSEHOOKSTRUCT)lParam;
    	int xPos = pHookInfo->pt.x;
    	int yPos = pHookInfo->pt.y;
    
    	LRESULT lResHandled = CallNextHookEx(ScreenCapture::getInstance()->getMouseHook(), nCode, wParam, lParam);
    	if (WM_LBUTTONDBLCLK == wParam )
    	{
            ScreenCommon::postNotifyMessage(WM_SNAPSHOT_FINISH_CAPTURE, 0, 0);
    	}
        else if (WM_RBUTTONDBLCLK == wParam)
        {
            ScreenCommon::postNotifyMessage(WM_SNAPSHOT_CANCEL_CPATURE, 0, 0);
        }
    	else if (WM_LBUTTONDOWN == wParam)
    	{
            if (CM_AUTO_SELECT == CaptureModeMgr::Instance()->getMode())
            {
                CaptureModeMgr::Instance()->changeMode(CM_MANAL_SELECT);
            }
    	}
    
    	CaptureModeMgr::Instance()->handleMouseMsg(wParam, xPos, yPos);
    	return lResHandled;
    }

     

     

     

     

     

    在钩子函数中,如果出现鼠标双击事件,则表示取消截图;如果出现双击事件,则表示完成截图。如果鼠标按下则表示开始绘制截图区域,然后处理鼠标移动事件:

     

    void CaptureModeMgr::handleMouseMsg(__in UINT uMsg, __in int xPos, __in int yPos)
    {
    	IModeMsgHandler *msgHandler = getModeHandler();
    	if (!msgHandler) return;
    
    	if (WM_MOUSEMOVE == uMsg)
    	{
    		msgHandler->onMouseMove(xPos, yPos);
    	}
    	else if (WM_LBUTTONDOWN == uMsg)
    	{
    		msgHandler->onLButtonDown(xPos, yPos);
    	}
    	else if (WM_LBUTTONUP == uMsg)
    	{
    		msgHandler->onLButtonUp(xPos, yPos);
    	}
    	else if (WM_LBUTTONDBLCLK == uMsg)
    	{
    		msgHandler->onLButtonDBClick(xPos, yPos);
    	}
    }


    选取区域结束时,将选择的区域保存为位图并存至某个路径下:

     

     

    void ScreenCapture::finishCapture()
    {
    	RECT rcSelect = {0};
    	UIScreenCaptureMgr::Instance()->sendBkgMessage(WM_SNAPSHOT_TEST_SELECT_RECT, (WPARAM)&rcSelect, 0);
    
        rcSelect.left += 2;
        rcSelect.top += 2;
        rcSelect.right -= 2;
        rcSelect.bottom -= 2;
    	if (!ScreenCommon::isRectEmpty(rcSelect))
    	{
    		ScreenSnapshot::Instance()->saveRect(rcSelect, m_strSavePath);
    	}
    	
    	cancelCapture();
    	if (m_callBack) m_callBack->onScreenCaptureFinish(m_strSavePath);
    }	RECT rcSelect = {0};
    	UIScreenCaptureMgr::Instance()->sendBkgMessage(WM_SNAPSHOT_TEST_SELECT_RECT, (WPARAM)&rcSelect, 0);
    
        rcSelect.left += 2;
        rcSelect.top += 2;
        rcSelect.right -= 2;
        rcSelect.bottom -= 2;
    	if (!ScreenCommon::isRectEmpty(rcSelect))
    	{
    		ScreenSnapshot::Instance()->saveRect(rcSelect, m_strSavePath);
    	}
    	
    	cancelCapture();
    	if (m_callBack) m_callBack->onScreenCaptureFinish(m_strSavePath);
    }

     

     

     

     

     

     

    BOOL ScreenSnapshot::saveRect(__in RECT &rc, __in std::wstring &savePath)
    {
    	snapshotScreen();
    
    	CxImage img;
    	int cx = rc.right - rc.left;
    	int cy = rc.bottom - rc.top;
    
    	HDC hSaveDC = CreateCompatibleDC(m_hMemDC);
    	HBITMAP hBitmap = CreateCompatibleBitmap(m_hMemDC, cx, cy);
    	HBITMAP hSaveBitmap = (HBITMAP)SelectObject(hSaveDC, (HGDIOBJ)hBitmap);
    	BitBlt(hSaveDC, 0, 0, cx, cy, m_hMemDC, rc.left, rc.top, SRCCOPY);
    	hBitmap = (HBITMAP)SelectObject(hSaveDC, (HBITMAP)hSaveBitmap);
    	
    	BOOL result = FALSE;
    	do 
    	{
    		if (!img.CreateFromHBITMAP(hBitmap))
    		{
    			break;
    		}
    		if (!img.Save(savePath.c_str(), CXIMAGE_FORMAT_BMP))
    		{
    			break;
    		}
    		result = TRUE;
    	} while (FALSE);
    
    	DeleteObject((HGDIOBJ)hBitmap);
    	DeleteDC(hSaveDC);
    
    	return result;
    }


    注意整个过程使用了一个神奇的windows API,你没看错,它叫mouse_event,很少有windows API长成这个样子。利用这个api可以用程序模拟鼠标很多事件,后面有时间我会专门介绍一下这个有用的API函数。当然,关于截图的描述,你可能有点迷糊。没关系,后面我会专门写一篇文章细致地探究下teamtalk的屏幕截图效果实现,因为这里面有价值的东西很多。

     

     

    4. 线程的创建

     

    IMCoreErrorCode OperationManager::startup()
    {
    	m_operationThread = std::thread([&]
    	{
    		std::unique_lock <std::mutex> lck(m_cvMutex);
    		Operation* pOperation = nullptr;
    		while (m_bContinue)
    		{
    			if (!m_bContinue)
    				break;
    			if (m_vecRealtimeOperations.empty())
    				m_CV.wait(lck);
    			if (!m_bContinue)
    				break;
    			{
    				std::lock_guard<std::mutex> lock(m_mutexOperation);
    				if (m_vecRealtimeOperations.empty())
    					continue;
    				pOperation = m_vecRealtimeOperations.front();
    				m_vecRealtimeOperations.pop_front();
    			}
    
    			if (!m_bContinue)
    				break;
    
    			if (pOperation)
    			{
    				pOperation->process();
    				pOperation->release();
    			}
    		}
    	});
    
    	return IMCORE_OK;
    }
    


    这是利用lamda表达式创建一个线程典型的语法,其中m_operationThread是一个成员变量,类型是std::thread,std::thread([&]中括号中的&符号表示该lamda表达式以引用的方式捕获了所有外部的自动变量,这是在一个成员函数里面,也就是说在线程函数里面可以以引用的方式使用该类的所有成员变量。这个语法值得大家学习。

     

     

    5. teamtalk的httpclient工程可以直接拿来使用,作者主页:http://xiangwangfeng.com,github链接:https://github.com/xiangwangfeng/httpclient

     

           另外teamtalk pc端大量使用C++11的语法和一些替代原来平常的写法,这个就不专门列出来了,后面我将会专门写一篇文章来介绍C++11中那些好用的工程级技巧。

     

            好了,这篇文章就到此为止了。限于作者水平有限,文中难免有错漏和不足,欢迎批评指正。也欢迎加入我们的QQ交流群:49114021。

     

           如果您对服务器开发技术感兴趣,可以关注我的微信公众号『高性能服务器开发』,这个微信公众号致力于将服务器开发技术通俗化、平民化,让服务器开发技术不再神秘,其中整理了将服务器开发需要掌握的一些基础技术归纳整理,既有基础理论部分,也有实战部分。

    展开全文
  • html5时插入一个视频播放标签video后,测试时android、PC客户端播放正常,唯独ios无法播放。 找了很多办法,把视频转换mp4各种格式;更换了好几个播放器,发现还是行不通。 之前的做法是,请求一个视频链接...
  • Socket Android手机客户端PC服务端局域网内联测试,笔者采用的是 PC服务器,Android平板客户端PC模拟器客户端, 前段时间为了加深对Socket通信的印象和知识的深度掌握,我模仿了QQ的一些元素,也借鉴了其他牛人...
  • 现在,我们已经得到了应用程序的绿色版本(无需安装,拷贝整个文件目录之后即可使用),但是作为客户端应用程序,我们更希望能直接得到一个安装包,安装之后通过桌面快捷方式的形式去访问,这时候就需要Inno Setup...
  • vuejs用APP客户端扫描PC端二维码登录

    千次阅读 2017-08-31 06:16:41
    最近在APP客户端扫描PC端二维码登录,于是记录一下实现过程,前端是vuejs,APP是IOS和安卓客户端:1.安装QRCode,npm i QRCode --save-dev就可以了。2.安装成功后在对应的单页面中引用import QRCode from 'qrcode'...
  • 很多前端项目需要PC端,vue项目打包结合electron非常方便 vue项目及打包就不再多说,一般大家都会将打包好的文件指向dist文件目录,下面就以此条件进行解说 项目安装依赖 npm i -D electron@latest npm i -D ...
  • java服务器,android做客户端,实现数据传输

    万次阅读 热门讨论 2014-03-09 08:10:42
    需要用一台windows电脑服务器,在android端与其进行数据交换,实现一些业务。 简单起见,用java写这个服务器,以前没过,试试水。很简单的代码,纯粹找思路。 服务器端代码: package com.test; import ...
  • 网页自适应pc端和移动端

    万次阅读 2018-03-01 13:17:54
    PC的屏幕宽度,一般都在1000像素以上(目前主流宽度是1366×768),有的还达到了2000像素。同样的内容,要在大小迥异的屏幕上,都呈现出满意的效果,并不是一件容易的事。  于是,网页设计师不得不面对一个难题:...
  • web浏览器程序打包成客户端可安装程序第一步第二步 第一步 参考:node-webkit打包,我完全按照这个来,亲测,可行 另附node-webkit下载地址:https://nwjs.io/downloads/ 第二步 利用inno setup(免费的安装制作软件)...
  • 公司正在一个教育类网站,后端是用java写的。公司下阶段计划制作一个桌面客户端,学生通过客户端就直接进入到我们的网站,希望借此屏蔽掉浏览器的兼容性问题,使用起来也更简单。 另一个功能点是,学生可以提前从...
  • 客户端开发设计总结

    万次阅读 多人点赞 2016-09-19 16:01:04
    1 基础设施程序最基本的处理就是数据IO以及为了并行计算所的操作,它们通常会作为程序的底层框架,供上层使用。2 交互从PC到现在的移动设备还有VR,人机交互中的输入设备在不断自然化,可是展示界面的元素仍然还是...
  • 客户端是怎样炼

    千次阅读 2015-05-06 17:15:14
    本文主要从开发的角度讲一下一个客户端的开发主要涉及的方方面面,让想了解App开发的同学个快速入门。 一、开发前准备 这个主要是产品的设计阶段。 1、确定产品的功能List,主要操作逻辑,产品满足的...
  • 客户端、瘦客户端与智能客户端

    千次阅读 2011-05-10 15:34:00
    一个典型的胖客户端包含一个或多个在用户的PC上运行的应用程序,用户可以查看并操作数据、处理一些或所有的业务规则——同时提供一个丰富的用户界面做出响应。服务器负责管理对数据的访问并负责执行一些或所有的业务...
  • 客户端升级系统升级策略

    千次阅读 2015-07-28 19:32:46
    去年年中重新了一个灰度升级系统,专门为客户端升级服务。现在分享下这个系统的升级策略。  发布版  所有版本号比发布版低的客户端都要升级到发布版。升级的形式有两种,登录升级和使用中升级。 1.登录升级 ...
  • pc网页的设计尺寸

    千次阅读 2016-10-18 16:20:00
    pc端页面设计图到底选择多大比较合适? 文字大小设置什么样比较合适? 搞前端的都会遇到这些问题。 直接说说我在项目中的实现。 如果全屏页面,我的设计图尺寸是1920*960px(960是chrome浏览器可视区域的...
  • 客户端软件的结构思考(一)

    千次阅读 2017-10-30 17:13:22
    但是直到今天,我仍然没找到所谓的“完美”的答案,但是在这个成长过程中,因为借鉴、融合和吸纳了许多其他的pc软件的设计思想和技巧,我在做pc软件整体结构设计时越来越得心应手。下面是我成长的心路历程,故事很长...
  •   C++ 客户端开发在2010年之前应该还是挺流行的,自从移动端,web兴起之后,PC客户端开发就逐渐走下坡路了,甚至很多语言、框架都消失了,退出了企业的招聘舞台,像VB就是鲜明的例子。大部分公司都走向了移动,web...
  • redis入门——客户端

    千次阅读 2016-12-01 00:42:29
    redis入门——客户端篇@(Redis)[redis, 入门, 客户端命令, jdeis]redis入门客户端篇 redis的客户端 redis-cli redis-desktop-manager jedis redis客户端的使用以redis-cli为例 redis的数据类型 redis的各个数据类型...
  • 智能客户端技术

    千次阅读 2011-09-09 15:05:14
    智能客户端(Smart Client),结合了瘦客户端(B/S模式)和胖客户端(C/S模式)的长处,是下一代的客户端软件技术。  要了解智能客户端,首先要认识瘦客户端技术和胖客户端技术各自的优缺点。  对于前者,典型的应用...
  • vue自动识别设备为移动端或pc端跳转链接 代码: data() { return { la_id: "", nowurl: "", }; }, created() { // other ... this.linktab(); }, methods: { linktab() { let goUrl = this.isMobile();...
  • 客户端的思想玩转前端

    千次阅读 2017-05-10 09:14:20
    以前所的大多是客户端方面的工作,很少参与前端的开发,一直以为web前端使用html、js和css写代码是一件挺痛苦的事情,最近在项目中参与了前端界面的开发,再次深入的对于前端的各种功能进行了学习和尝试,突然发现...
  • 它是个http服务器,它专门接收从客户端通过基于http的REST协议发送过来的命令他是bootstrap客户端:它接收到客户端的命令后,需要想办法把这些命令发送给目标安卓机器的bootstrap来驱动uiatuomator来事情 ...
  • [NodeJS]创建HTTP、HTTPS服务器与客户端

    万次阅读 2017-06-01 08:32:18
    将二者结合一个路径,from、to既可以是相对路径也可以是绝对路径。 // http://ligangblog.com/javascript/a?a=1 url.resolve( 'http://ligangblog.com/javascript/' , 'a?a=1' ); // ...
  • [HTML5点滴]客户端存储那些事

    千次阅读 2017-01-18 09:05:30
    客户端存储 译者:文蔺 原文:http://www.html5rocks.com/en/tutorials/offline/storage/ 客户端存储介绍本文是关于客户端存储(client-side storage)的。这是一个通用术语,包含几个独立但相关的 API: Web ...
  • Android Studio TCP客户端实现

    千次阅读 多人点赞 2020-07-18 16:24:52
    需要在手机一个客户端,然后上去网上查了巨久巨多代码,为了避免让有需要的人少走弯路,就一篇博文来推一下自己的做法,如果各位大大们有什么好的建议,也希望各位可以在评论区写下高见抑或是发送到邮箱...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 60,662
精华内容 24,264
关键字:

网页做成pc客户端