大话客户端网络编程

大话客户端网络编程

  • Slicol

前言

其实我和网络编程是有缘分的。我来公司做的第一件事情,就是重构一个网络模块。作为初生牛犊,那时我还不知道网络模块和其它模块有什么不同。不就是一个模块嘛。结果后来就掉了网络模块深深的坑里了。

其实我对网络编程是又爱又恨的。自从做了人生中第一个网络模块后,就像一个魔咒一样,后面经历的每一个项目的网络模块几乎都是我做的。而且无论是项目研发阶段、运营阶段、还是非常稳定的运营阶段,网络模块永远都有做不完的事情。

网络编程,是游戏开发逃脱不了的宿命!


1.网络模块

1.1. 重要

网络模块的重要性,无须多言。

1.2. 神秘

一个项目的模块很多,但是网络模块只有一个。再结合网络模块的重要性,所以,大部分新来的同学,都没有机会做网络模块。

两个项目的背包模块几乎无法相同,但是网络模块却几乎通用。继续结合网络模块的重要性,所以一个项目的网络模块大概只会在——曾经做过网络模块的同学——手里不断重构和完善,最后几乎可以与业务无关。于是,大部分已经工作一段时间的同学,也没有机会做网络模块。

我遇到很多新老同学过来打听网络模块的情况。大家觉得它很神秘,跃跃欲试,却不知从何入手。

1.3. 简单

其实网络模块没有什么神秘。它的一般性框架是这样的(火影手游的PVP网络模块是专用的网络模块,详见我之前的文章):

图1 网络模块的一般性框架

看起来好像很简单,大致就分为两大部分:连接管理器协议管理器。而且这两个管理器的实现也相当简单。大致如下:

图2 连接管理器与协议管理器的实现
  • ConnectManager。它维护一个Connection实例列表。这些实例根据底层通讯接口的不同,有多种类型。Connection封装了数据的收发逻辑,它分别为Send和Receive提供Buffer。它主要实现以下功能:
    • 创建连接。这部分功能主要在XXConnection类实现,不同的通讯接口,连接方式不同。比如Apollo的通讯接口,需要提供公司的一揽子参数。而Bluetooth接口而需要提供BluetoothMacAddress。而采用底层的Socket实现的UDP通讯接口,则无须Connect过程,那么我们就给它虚拟一个假的Connect过程,以保持IConnection接口的统一性。等等等等。
    • 收发数据。大部分通讯接口对数据的Recv是通过轮询实现的,在ConnectManager里将轮询操作统一转换为事件方式。
    • 断线重连。Connection分别为Send和Recv提供了Buffer,以便支持静默重连,使上层逻辑在大部分情况下无须关心网络是否断开,也可以发送数据。比如,网络突然断开了,假设A模块不负责维护在线状态,那么在它看来,它依然可以正常发送数据。假设B模块负责维护在线状态,那么它应该监听到网络断开,然后进行重连,最后重连成功。在网络重连成功后,缓存在Connection的数据,就可以发送出去了。整个过程,对A模块是不可感知的。(是不是觉得断线静默重连也没有想像中那么复杂了?)
  • ProtocolManager。相对ConnectManager,它简单得多。它维护一个从协议ID到协议类的映射(对于C#这种具有反射机制的语言,可以直接映射到协议类,但是对于C++则可以用其它方法来实现映射)。并且定义了协议格式。

到此为止,一个几乎通用的网络模块框架基本上搭完了。是不是很简单?

1.4. 模块糖

模块糖,这是我杜撰的一个词。就像语法糖一样。对于不同的项目,可以给ConnectManager和ProtocolManager加一些糖,让它用起来更甜。比如将SendProtocol(pid, PTLObj, connId)包装成SendDirProtocol(pid,PTLObj)和SendZoneProtocol(pid, PTLObj)等;将CreateConnection(connId,type,ip,port)包装成CreateDirConnection(ip,port)和CreateZoneConnection(ip,port)等。

等等等等。


2.连接层

上面聊了一下网络模块的一般性框架。下面从具体实现来聊聊相关技术。掌握了这些技术点,便可以轻松实现一个网络模块的连接层。

2.1. 关于Socket

Socket就是常说的套接字。说实话我对这个翻译是很懵逼的。Socket就是我们正常网络编程中能够接触到的最底层的通讯接口。对于它的原理,在这篇文章中,我们只意会,不言传。

对于Socket,我们最需要关注的是它的工作方式。在客户端Socket主要有2种工作方式:

  • 同步方式。无论是UDP还是TCP,在用Socket进行连接、发送、接收的时候,在未完成工作前代码不再继续往下执行,处于等待状态,直到该语句完成对应个工作后才继续执行下一条语句。值得注意的是,UDP和TCP对于一件工作是否完成的定义不同,以Send为例,如下图。
    • 对于TCP来说,未完成工作就是:(1)缓冲区满了,数据无法写入,(2)或者数据写入了但是还没轮到它发送,(3)或者数据发送了,但是未收到ACK确认。
    • 对于UDP来说,未完成工作就是:缓冲区满了,数据无法写入。
图3 Socket同步方式时序图
  • 异步方式。即不论对应工作是否完成,都会继续往下执行。当工作完成后,是通过一个回调来通知调用者。参照上图,不需要单独用时序图来说明了。

在同步方式中,可以理解为有一个Loop在不停地轮询是否完成工作,直到工作完成才结束Loop。为了避免UI以及主逻辑被卡住,一般需要将以同步方式工作的操作都放在子线程中。

这个时候,你可能会问,在实际应用中,我们应该选择“多线程同步方式”还是“异步方式”呢?我们先看看下表的对比:

对比 多线程同步方式 异步方式
性能
复杂度
灵活性

所以,如果对网络连接没有特别要求的情况下,优先考虑异步方式,省心高效。但是,如果我们想在UDP基础上实现一个自定义的协议栈,那么可以使用多线程同步方式,把对自定义协议栈的实现逻辑放在子线程中,避免对主线程产生性能影响。

2.2. 关于多线程

当我们不得不使用多线程同步方式时,就要面对一个令很多新同学都感到陌生神秘的东西:线程。由于使用多线程的情况并不多,所以主要掌握以下几点大概便可以在网络编程中使用多线程了。

  • 线程函数。如果说主线程是从Main函数开始的(在Unity+C#里,你是看不到Main函数的。),那么子线程也是从一个函数开始。为了防止主线程与子线程的代码逻辑搞混,建议将线程函数定义在一个单独的类里。由这个函数所调用的所有被调用函数都在这个类里。

  • 前台线程和后台线程。切记,系统默认创建的子线程是前台线程,它将带来一个问题,就是当主线程已经结束时,程序还会运行。如果将它设置为后台线程,则当主线程结束时,所有后台线程都会无异常中止。

  • 线程同步。当主线程和子线程存在共用数据时,为了避免多线程同时操作同一数据,需要使用“”。C#有多种锁定方式,比较常用的是lock语句。建议不要直接lock需要操作的数据,而是为这个数据定义一个对应的object,lock这个object。因为有些类型的数据,比如int,是无法直接lock的。

  • 异常处理。使用Try/Catch进行异常处理时,不要在线程的创建处TryCatch。一旦线程创建成功,线程执行过程中的异常,是无法在其它线程中被捕获的。正确的做法是在线程函数里TryCatch。

当然关于多线程的其它知识,有很多专门的文章介绍。

2.3. 关于连接器

Connection是对底层或者基础通讯接口以及可能使用的线程相关逻辑进行封装。一般情况下,按照所使用的通讯接口类型进行封装。

  • 如果使用Apollo的通讯组件,可以封装成ApolloConnection。
  • 如果使用Socket,则封装成TCPConnection/UDPConnection。
  • 如果使用Bluetooth,则封装一个BluetoothConnection。
  • 如果使用RS232串口通讯,则封装一个RS232Connection。

这个世界上有很多种通讯方式,你都可以封装成对应的Connection,以便统一它的通讯接口。除此之外,它主要还将提供Send和Recv的数据缓存。

2.4. 关于数据包/数据流

从图1中,我们看到,一个“协议实例”将转换为一个“协议数据包”,然后“协议数据包”将以“数据流/数据包”的形式发送出去。

在不同的传输协议中,数据的发送形式是不同的。在TCP传输中,数据是以流的形式发送。而在UDP传输中,数据是以包的形式发送。

它们的区别在于,一个数据包里包含一个协议的完整数据。而一段数据流里可能包含的是多个协议的数据,或者一个不完整的协议数据。

2.5. 关于轮询

如果采用Socket的异步方式进行工作,Socket可以直接回调给Connection,然后Connection在回调函数里向ConnectManager发事件。那么,为什么还要使用轮询?因为,如果有些通讯接口不支持异步工作方式(很多自制的硬件其驱动只支持同步操作的),那么就要进行线程同步操作。而这种情况下,一般强烈建议采用主线程向子线程轮询的方式进行数据同步。

  • 可扩展性。既然轮询不可避免,为了扩展性,就统一使用轮询机制来在ConnectManager和Connection之间通讯。
  • 异常隔离。防止业务层模块异常导致整个Connection的异常。因为Connection作为基础功能,还有其它模块在使用。

3.协议层

协议层相对连接层简单得多。它的主要相关技术如下。

3.1. 协议格式

最基本的协议格式如下:

  • 协议头
    • PID: 协议ID
    • Index: 协议发送序列号
    • DataBuffSize: 协议体的数据长度
    • CheckSum: 校验和
  • 协议体
    • DataBuff: 协议数据Buffer。

以上协议格式定义了一个协议数据包。其中DataBuff来自对协议实例的序列化。

为了实现对协议实例的序列化,我们可以自定义一个IProtocolBase接口,让具体协议来实现这个接口。

但是,在实际应用中,我们都是直接使用Google的ProtoBuf作为协议的基类。它已经提供了非常高效的序列化和反序列化功能。

3.2. 协议流

在章节2.4中得知,有些情况下,协议层收到来自连接层的数据,并不一定是一个恰好完整的协议数据包,而有可能一段数据流。于是,为了统一逻辑,不管收到的是数据包,还是数据流,我都将它们统一为协议流

在ProtocolManager中,需要对协议流进行合并或分割处理。其实很简单,它的逻辑流程如下所示。(需要注意的是,如果系统中同时存在多个Connection,需要为每一个Connection定义一个协议流。)

st=>start: OnRecvData
e=>end
e2=>end: 对协议反序列化
op_WriteInPtlStream=>operation: 将数据写入协议流
cond_CanReadPtlHead=>condition: 协议流长度是否够读一个协议头?
op_ReadPtlHead=>operation: 读出协议头数据
op_ReadDataSize=>operation: 得到协议体长度
cond_CanReadPtlBody=>condition: 协议流长度是否够读一个协议体?
op_ReadPtlData=>operation: 取出协议体数据
cond_CheckPtlData=>condition: 协议体数据CheckSum通过?

st->op_WriteInPtlStream->cond_CanReadPtlHead
cond_CanReadPtlHead(yes)->op_ReadPtlHead->op_ReadDataSize->cond_CanReadPtlBody
cond_CanReadPtlHead(no)->e
cond_CanReadPtlBody(yes)->op_ReadPtlData->cond_CheckPtlData
cond_CanReadPtlBody(no)->e
cond_CheckPtlData(yes)->e2->cond_CanReadPtlHead
cond_CheckPtlData(no)->cond_CanReadPtlHead

<center><small>图4 协议流处理逻辑</small></center>

3.3. 协议分类

一般情况下,协议可以分为这几类:

  • 只发送,不需要监听回包。用于向服务器上报数值。
  • 无发送,只需要监听回包。用于服务器Push数值,或者触发逻辑。
  • 一处发送,多处监听回包。用于基础功能协议。
  • 一处发送,一处监听回包。用于具体功能协议。

ProtocolManager应该对上面4种协议都能提供支持。

3.4. 协议ID规则

后台喜欢把协议ID叫CMD,或者CmdID。我一般直译为PID。PID的规则一般有两种:

  • 同一条协议,发包和回包时,PID相同。因为发包和回包时,虽然协议体内容不同,但却是一回一答,是为同一个功能服务的。
  • 同一条协议,发包和回包时,PID不同。因为发包和回包时,协议体的内容不同。目前比较流行这种方式。为了使编程更方便,以及代码容易理解,一般将回包的PID定义为发包的PID+1。

4.调试

无论做什么模块开发,都离不开调试。而网络模块对于调试的要求更高。可以这么说,你编写一个网络模块可能需要2天,但是将来花在调试它的时间可能是直到项目结束。

所以,在你完成网络模块的代码编写之后,一定不要忘记,为了能够高效地调试,做好一切准备。

4.1. 网络日志系统

我相信,你的项目中一定已经有了现成的日志系统。但是那远远不够。建议在其基础上封装一个网络日志系统,并且为它提供一个专用面板。它会比在总日志文本里看网络日志要高效得多,性能也可控得多。它应该提供如下功能:

  • 单独输出网络模块的日志。
  • 以16进制显示每一个Connection的Send和Recv缓冲区数据。这是你能够接触到的最底层接口的数据,后台会经常和你Check这些数据。
  • 列出每一条被注册的协议。
  • 记录发送和接收到的每一条协议的内容。如果该协议是注册的,则可以反序列化为结构性信息,如果未注册,则提示未注册,并且显示16进制数据。
  • 统计断线重连次数,断线时长,网络延时等。
4.2. 网络状况模拟

在研发阶段,这个功能是非常有用的。可以帮助你高效测试网络模块在各种网络情况下,是否正常工作。也可以为业务模块提供网络相关的测试手段。比如,测试在线模块,断线重连逻辑(再也不需要拨网线了)等。

4.3. 抓包工具

一般使用Wireshark和Fiddler。网络编程必备工具。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,590评论 18 139
  • https://nodejs.org/api/documentation.html 工具模块 Assert 测试 ...
    KeKeMars阅读 6,297评论 0 6
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,169评论 11 349
  • 这一期将会讲Snapseed的视角功能,视角顾名思义就是照片的角度,透视。改变视角可以让照片是歪的,朝下的,偏左抑...
    一起手机摄影阅读 7,261评论 6 62
  • 今天读利未记18-20。 我看到神是分别为圣的神。以色列人曾寄居在埃及地,之后神又把以色列人带往迦南美地,埃及和迦...
    尤加利阅读 2,882评论 0 0