综述
写下此篇主要源自前几天的一个面试,结果很失败,第一轮电话面试就挂了……现在回忆起来,面试官问的也都是常规问题,没回答好只能怪自己。于是,痛定思痛,写下此篇,算是对自己十年编程工作的一个总结,也为后来者和新入行的朋友们分享一些经验。
由于是个人总结,全凭经验,内容顶多算是对知识点复习时候的索引,难免有错误和疏忽之处,很多知识点只能点到为止,做不到面面俱到,详细的内容还是需要读者自行学习和实验,还望大家海涵。希望大家留言指出错误之处和提供改进建议,我也是抛砖引玉了。
首先介绍一下我自己,我在2010年从江西师范大学本科毕业,软件工程专业,学校一般专业也一般,平平无奇。这十年来做过程序员,写过网络小说,也做过一段时间外卖(不是美团外卖骑手,就是卖两荤两素的那种快餐)……虽然经历挺弯弯绕绕的,但做程序员的时间最长,算来至今差不多也快10年了。
我长期从事windows客户端开发,因此本次分享也主要围绕在客户端开发的方方面面,期间也会提到一些服务器开发方面的东西,毕竟这么些年,多多少少也都开发过一些。
记得刚毕业那会儿做基于MFC的客户端,逐渐过渡到以duilib为主的directxui架构,后来大规模使用QT,还用WPF做了几个项目,这几年又流行以浏览器嵌web的开发方式,如electron客户端框架开始被大量使用等等,可以说,客户端开发流行的趋势是存在的,而这个趋势就是让客户端的开发、发布、更新变得更为快速和敏捷。
但无论开发方式变得多么的花里胡哨,依然绕不开windows的基础原理,因此,我从头开始讲起。
Windows
1.PE文件结构
既然从windows开始谈,不得不提到PE文件结构,这个东西在某些项目开发中还是必经之路。所谓PE,就是Windows系统下可执行文件总称,也就是我们看到的以.EXE、.dll结尾的那些可执行文件的二进制结构。这里面可谓博大精深,详细内容没个三天说不完,我只说我用到或觉得比较重要的东西。
一个是各个节表的意义以及节表和节数据之间的对应关系。应该知道如何通过节表而找到节数据内容,因为有时候需要做一些资源替换,比如替换个EXE图标之类的操作。而其中导入导出表又比较重要,程序所使用和导出的函数都在这一块,通过这个表可以完成很多诡异的功能,所以需要特别注意一下。当然,所有涉及到直接修改PE文件结构的操作都会影响到数字签名,如果对数字签名有需求的同学不建议这么干。
第二个是PE文件内的各种地址类型,虚拟内存地址,相对虚拟地址,文件偏移地址。这里面的地址有的是以磁盘为参考系,有的是以内存为参考系,通过这些地址就能计算出节数据处于内存中的具体位置,然后才可以对这些节数据做替换或者修改。
相关参考:
如何直接在内存中运行EXE或者内存加载DLL的操作,有些函数没实现,只要理解过程
https://www.write-bug.com/article/1968.html
手动写壳,能按照过程手动实现知识差不多就梳理了一遍
https://bbs.pediy.com/thread-250960.htm
https://bbs.pediy.com/thread-251267.htm
2.钩子
钩子是Windows系统提供的一套“用以捕获消息和修改消息”的机制,用途非常广。钩子种类有很多,有鼠标钩子、键盘钩子、窗口消息钩子等。
但是我想说的是另外一种钩子:函数钩子。确切来说应该叫函数的替换,往往在不能修改目标源代码的情况下,还要在目标进程里实现一些自定义操作,比如在第三方游戏里显示一张图片之类的,这就要用到函数钩子。
这种钩子实现方式有很多,我常用的是通过修改IAT(PE文件里面的导入表)的函数地址来做到这一点,当然,这里面要扩展下去能讲很多很多,我也只能止步于此。不过好在现在有很多类库做的很好,推荐还是用现成的类库做项目比较稳定,比如MinHook就很好用,地址这里:https://www.codeproject.com/Articles/44326/MinHook-The-Minimalistic-x-x-API-Hooking-Libra。
案例:我做过一个项目,需要在第三方游戏内嵌入定制的充值二维码,其实就类似于Steam平台的Game overlay功能,由于游戏是第三方的,修改他们的源码肯定是不可能,因此只能采用钩子的方式,钩住Directx的一些函数,完成这个功能,这个功能扩展到后期,甚至能在游戏内打开自制的ui和播放视频等,这估计以后要专门开个专题来讲。当然,这里面钩子只是一个环节,要做的工作还有很多,比如ui我们采用CEF嵌网页形式,离屏渲染这一块也是重点。这里插一句,有个叫reshade的项目很不错,可以借鉴一下,地址在这里:https://github.com/crosire/reshade,这个项目就类似于我说的在游戏内显示自定义的ui或者图片。
3.线程通信、进程通信
由于线程是在同一个进程中,所以一般在代码的级别就能实现通信,最简单的如创建一个全局变量就可以,当然围绕这个全局变量需要一些锁来保证读写的互斥性。消息通信,windows基本的通信方式,UI线程拥有自己的消息队列,会自行处理,工作线程需要手动处理消息队列,用sendmessage和postmessage发送消息,需要注意sendmessage会阻塞。还有就是Event事件,分为有信号无信号,Event也有手动和自动之分,一般使用WaitforsingleObject函数来等待事件。
进程通信方式较为丰富一些,首先,WM_COPYDATA消息就能用于进程之间传递消息,但是这个消息只能用sendmessage发送,会有一定的阻塞风险。文件映射和共享内存也是常用方式,它们可以使文件或者是内存映射到系统内存中,以提供给其他进程访问。还有一种方法是管道,分为匿名管道和命名管道,命名管道简单点,不同进程打开一个相同名字的管道即可实现消息收发,双向的。匿名管道的特性则有些不同,匿名管道多用于父子进程通信。当然,剪切板和Sockets也不失为一个进程通信的选择。
4.iocp
完成端口是Windows系统提供的一套异步机制,最常用来做网络开发,但由于这只是一套输入输出异步通知机制,所以你用iocp来做文件读写的通知都没问题。这里我只谈谈基于iocp的网络模型编程。
我做过基于iocp+tcp类型的网络库,也做过iocp+udp+kcp算法的网络库,精通谈不上,但我对IOCP的这套API还是蛮喜欢的,毕竟比较亲民,用起来比较方便。
tcp协议方面的东西太多,光是socket的参数就有一堆,这几只说几个要点:
iocp+tcp的模式,由于tcp是流传输,没头没尾的,所以数据包格式一定要定义好,最主要的问题就是能解析到数据包的长度,有了长度才能把包读取出来,然后才能做解析之类的操作。常见的三种方式定义数据包:1.固定格式的数据包,比如http用回车换行区分头部的命令和body,json以对称的花括号({})为解析方式,我们也可以定义一个特殊的字符串作为数据包尾部。2.固定长度的数据包。3.自定义带头部的数据包,这个头部固定格式,比如第一字节标识位,第二个字节定义数据包长度,第三个字节做个crc校验之类的,这个按照需求定义。解析的时候也从头部开始解析,读到长度后将整个数据包取出。我常用第三种方式,可自定义的空间最大,也最灵活。
iocp+udp模式则没这么多讲究,udp协议直接是基于报的基础,不需要定义数据包头尾,直接发送什么接收什么。但是,由于标准MTU值为576字节,所以一般建议UDP数据长度控制在548字节以内(此段百度),所以,依然需要自己在逻辑上定义好数据包格式,这里包头应该还要多一个序号,因为UDP不是按顺序的,接到报文后还要按照序号拼装起来,同时还要处理重发、滑动窗口、拥塞控制……算了,最好还是加一个kcp算法吧,可以参考这个库:https://github.com/skywind3000/kcp。
iocp则需要知道有个完成端口自定义结构体,通过CONTAINING_RECORD宏命令可以将iocp的回调对象转为自定义的结构体,不过这很细节。此外就是除了经常使用的WSASend、WSARecv、AcceptEx是异步的之外,ConnectEx和DisconnectEx也能实现连接很断开的异步,不过这两个函数需要使用WSAIoctl来获取,其他更多细节看这里:https://github.com/yunfengbasara/dictinoary,我做了一个简单库,可以参考一下。
UI架构
1.MFC
严格来说,MFC应该不仅仅只是UI架构这么简单,它是Windows提供的一套应用程序开发框架,是对Windows API的一套封装,功能可谓是大而全。我在10年前用纯Windows API做UI的时候,那种酸爽真的可谓是一言难尽……
MFC这几年也有更新,也加了很多特性进去,不过总体来说,大架构没变,需要注意的点就是消息路由机制。虽说文档对应视图的机制(类似MVC模型)现在看来也是平平无奇,但是当年对我这个新手来说冲击也是蛮大的。
2.DirectUI
MFC或者Win32 API开发中,每个控件都可以说是一个窗口。DirectUI则跟MFC不同,整个应用就一个窗口,里面所有的控件如按钮,滚动条,列表等,都是虚拟出来的。我常用的库有duilib,地址这里:https://github.com/duilib/duilib,听说SOUI也挺不错,不过我没用过,看过一些例子,稍微提一下。
DirectUI善于制作一些酷炫的界面,实现自定义标题栏或者通过分层窗口实现的异形界面都比较好。如果用MFC来做自定义标题栏的话,那要重写一大堆事件,比较麻烦。
在我整个客户端开发生涯中,始终都绕不开关于异形界面的设计于制作……在那段时间有点空,我采用Directx2D作为绘制引擎,分层窗口技术做了一套DirectUI架构的UI库,仅供参考,地址如下:https://github.com/yunfengbasara/D2DUI。
需要注意的是,在使用WS_EX_LAYERED分层模式的窗口时,窗口的WM_PAINT消息是无法触发的,你要选择合适的时机手动调用UpdateLayeredWindow函数来刷新整个ui,详细的内容可以参考上面那个地址。
3.QT
以前使用QT最主要的原因其实不是跨平台,而是QT提供的这套C++的库其实挺不错,开发很友好。前端时间将以前的项目用QML尝试重写了一番,发现QML其实也挺不错的,就是学习起来有点陡峭,倒腾好久才完成主界面,里面还有很多细节,就没细细研究进去了,如果选择QT的话,QML是个不错的推荐。
但是,我现在用QT做客户端,基本上把QT当作浏览器使用。因为现代客户端按照开发节奏和更新时间来说,都变得非常频繁,最关键的还是,万物皆可JavaScript嘛,一切能用JavaScript实现的,最终都将被JavaScript实现。而且,web开发人员比较容易招聘也是一个方面,我们能很快速的拉起一支web开发团队。所以,QT作为浏览器核心,用web来做UI开发,非常适合。
QT能讲的东西太多,这里只罗列下信号与槽,作为知识的梳理。QT信号槽的连接模式分为:自动连接(同线程)、直接连接(不同线程)、队列连接(不实时)、阻塞连接(不实时,不同线程)、防止重复连接。
4.Electron
由于近几年在客户端开发中,我把QT当成了一个浏览器来使用,整体ui采用嵌web的形式,那么有没有更为纯粹的以web为核心的客户端开发模式呢?答案是有,那就是Electron这个东西,可能还有其他类似的框架,如NW.js,MiniBlink(精简的Chrome版本,开发过程采用Electron,发布出来的库可以采用MiniBlink),WKE等等,不过WKE似乎已经没有更新了,WKE最求最精简的浏览器接口,如果单纯从做客户端UI而言,我觉得如果在对软件体积大小有苛刻要求的情况下,反而还不如使用duilib这类较为原生的一套框架,毕竟浏览器本身就比较大。
我只用过Electron,所以这里只简单谈谈我对Electron在客户端开发方面的评价。
网上都说Electron如何如何好,采用web架构的ui可用的第三方库种类多,各种酷炫特效,开发效率如何如何的肝,就连Visual Studio Code都是用Electron开发的……于是我决定采用Electron开发客户端。不得不说,到目前为止,我体会到Electron上面提到的所有优点,但是,我居然在打包这个环节被坑了一把,人算不如天算……
怎么说打包这个事呢,我用的打包工具是electron-builder,这玩意儿虽说配置方便,一句命令搞定打包,但是却间歇性的连不上服务器。换公司专门的打包服务器也一样,搞不明白。虽说不是天天打包,但我确实是碰到过都准备发版本了,却卡在打包这个环节……心态真的爆炸。
不过除此之外,Electron确实是个不错的框架,一些原生功能实现不了的也可以用ffi-napi调用c++编写的dll来实现,问题也不大。不过这玩意也存在一些莫名其妙的编译问题,打包过程中,我也碰到过ffi-napi打包的错误,反正也是按照网上那一堆流程下来,运气好的话也能解决。
因为受不了这种时不时出现的打包问题,我开始尝试用.NET的平台。
5.WPF
上面说到的Electron客户端框架的优点有很多,比如开发语言方面,可以直接用ES6,由于浏览器版本确定,所以一些新特性在拿不准的情况下都可以先试下,而不像普通web那样要考虑很多浏览器版本的兼容。这里特别说下ES6这个语言,新特性很多,学习网站很多,我不一一列举了。不过我最喜欢的特性还是async/await这组东西,说实话,恕我才疏学浅,到目前为止,async/await算是真正解决异步编程框架中如何处理同步问题的了。
较低版本的JavaScript虽说没有async/await这组关键字,但也能用Babel来转换,问题不大。
.net 4.5以上都支持async/await,这点非常不错,win10默认的.net版本都是4.6。
再一个就是WPF提供的MVVM框架,大白话就是数据绑定,逻辑UI分离,数据层变了绑定的对应UI也会相应改变,不用像MFC那样写一大串控制代码。虽说React也可以有类似的绑定机制,但React只能算是个视图层面的绑定,当然,React也可以通过各种插件实现MVVM这套框架,不过我建议还是把React当作一个纯粹的UI层来使用。
MVVM应该是MVC加强版,灵活性也更高,核心就是要实现视图(view)和数据层(model)分离,视图变化时,数据层用改变,或者数据层变化时,视图不用改变,而使用系统提供的绑定机制,从而实现了视图数据分离。同时,将视图的逻辑写在viewmodel层中,以便提供给不同的view重用。
C++
1.多态原理
我常用的语言有C++、C#、JavaScript和GO,对C++说不上精通,但还算是熟悉一点。几乎是每次面试都会被问到C++多态的原理……
多态的实现需要重写类里面的虚函数,而虚函数就是多态实现的关键。含有虚函数的类会多一个叫“虚函数表”的东西,这个表记录的都是虚函数地址,多态的原理就是覆盖虚函数表上需要实现多态的函数地址。
纯文字讲起来有点绕,网上有很多文章讲的很好,比如这里:https://blog.csdn.net/haoel/article/details/1948051/,另外这篇C++类内存分布也挺有看头:https://www.cnblogs.com/jerry19880126/p/3616999.html。
2.其他
额,C++有太多东西可讲,比如shared_ptr智能指针,lambda等等,这些如果扩展开来估计能再写一篇,暂不展开了。有很多大神把这些东西讲的比较透彻,看他们的帖子就好。:)
总结
这算是总结的第一篇,只是一些基础性的东西,以后我找时间讲下其他东西,比如:网络协议,主要还是TCP/IP、GRPC等,Windows音视频新框架MediaFoundation,一些着色器的算法(在Electron开发中,可以嵌入Three.js作为UI,3D效果的UI确实不得了,用上着色器更是能让客户端的效果爆炸),神经网络的0基础实现(不用任何框架,不用Tensorflow,存粹的代码实现一个深度学习网络,目前我用JS做了一个五子棋的深度学习网络,正在考虑用CUDA重新改造中)。
以上这些,找个时间吧,谢谢。