大纲
- 网络模块
- Socket
- 游戏中的网络框架:框架设计、TDR/ProtoBuf
- 解决方案:传输协议、弱网处理、网络同步
- 问题解答
目的
- 游戏项目中网络相关开发知识
网络模块
游戏中的网络模块与其它软件系统基本原理并无差异,基本上是解决如何把网络消息快速安全的发送到其端,然后其他端及时地处理该消息并作出对应的游戏表现。
网络模型
- OSI 开放系统互连模型(Open System Interconnect)
- TCP/IP 协议栈分层模型
游戏开发中最主要会使用到应用层和传输层,在传输层中TCP与UDP之间的区别在于:
- TCP:传输可靠(保证顺序),三次握手建立连接,数据包没有限制,不丢包(重传机制RDT)有拥塞控制,可以依靠网络层去分帧。
- UDP:传输不可靠(不保证顺序可能丢包),强调传输性而非可靠性,实现机制比较简单,无连接随时发送且包小于64K。
关于TCP与UDP的选择,TCP相比UDP数据安全可靠,但需要建立连接,机制复杂带来额外的开销,在网络环境不好的情况下效果非常差。简单来说,TCP适合应用在网络条件较好且对安全可靠性有要求的场景,UDP适合应用在网络环境较差且要求响应速度比较高(安全性是其次的)。
在网络游戏开发中,通信协议无论对于客户端开发还是服务器开发都很重要,因此制定一个合适的通信协议是很有必要的。例如,在有联网需求的弱网游戏如即时互动类游戏中,选择通信协议主要会关注延迟低、易用性、低成本等因素。
下面简单对比下TCP、UDP、HTTP、WebSocket四种协议的利弊和特性:
TCP
- 特性:全双工协议、面向连接且保证可靠性、基于IP层对应OSI参考模型中位于传输层、适用于二进制传输
- 优点:可靠性、全双工协议、开源支持多、应用较广泛、面向连接、研发成本低、报文内容无限制(IP层自动分包、重传、不大于1452字节)
- 缺点:操作系统方面比较消耗内存且支持的连接数量有限,设计上看协议比较复杂且自定义应用层协议,网络方面看在网络差的情况下延迟较高,传输方面来看效率低于UDP协议。
UDP
- 特性:无连接不可靠,基于IP协议层对应OSI参考模型中的传输层,最大努力交付,适用于二进制传输。
- 优点:操作系统层面看并发高且内存消耗较低,传输层面看效率高且网络延迟低,另外传输模型简单且研发成本低。
- 缺点:协议不可靠且是单向协议,开源支持少,报文内容有限不能大于1464字节,设计上协议比较复杂,在网络差的环境中容易丢失数据报文。
HTTP
- 特性:基于TCP/IP的应用层协议、无状态无连接、支持C/S模型、适用于文本传输
- 优点:协议比较成熟,基于TCP/IP且拥有TCP优点,研发成本很低,开源软件较多(Apache、Nginx、Tomcat...)
- 缺点:无状态无连接,只有PULL模式且不支持PUSH,数据报文较大。
WebSocket
- 特性:有状态、面向连接、数据报头小、适用于Web3.0以及即时联网通讯。
- 优点:协议比较成熟,基于TCP/IP且拥有TCP的优点,数据报文较小且包头非常小,面向连接而且是有状态的协议,开源较多开发较快。
- 缺点:暂无
对比特性后如何选择协议呢?
- 对于弱联网类游戏如消除类、卡牌类可以直接使用HTTP,若考虑安全性可使用HTTPS或是对内容提做对称加密。
- 对于实时性且交互性要求较高的可优先选择WebSocket其次是TCP。
- 对于实时性要求极高且可达性要求一般的可选择UDP
- 局域网对战类、赛车类可直接使用UDP,不过对于公网对战、P2P的UDP还需要进行“打洞”处理。
Socket
在同一台机器上两个不同的进程之间有多种通信方式,如管道、消息队列、信号量、信号、共享内存等。网络中要在两个不同机器的进程之间通信,就需要Socket套接字。从概念上来看,Socket是一套基于TCP/IP协议封装的API,它处于网络应用层给开发者提供方便的接口来快速实现网络通信。从编程角度来讲,Socket是一个无符号整形变量,用来标识一个通信进程。两个进程在网络中通信就必须要知道对方的IP和端口以及通信所采用的协议栈,Socket和它们绑定。
在网络中传递一个对象时,客户端上的对象与服务器上的对象在内存地址上肯定是不一样的,如何知道客户端传递过来的对象就是服务器上的呢?GUID,服务器在同步一个对象引用(指针)的时候,会为其分配专门的GUID并通过网络进行发送。客户端上通过识别这个ID就可以找到对应的类对象。
Socekt是应用层与TCP/IP的中间软件抽象层,是一组接口。在设计模式中Socket是一个门面模式,它把复杂的TPC/IP隐藏在Socket接口后面。对用户来说就是一组简单的接口,使用Socket去组织数据以符合指定的协议。
服务器首先初始化Socket,然后与端口绑定,对端口及逆行监听,调用accept阻塞,等待客户端连接。此时如果有客户端初始化一个Socket,然后连接服务器,如果连接成功此时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
Socket中TCP的三次握手建立连接,即交换三个分组。
- 客户端向服务器发送一个SNY J
- 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1。
- 客户端在向服务器发一个确认ACK K+1
当客户端调用connect时触发了连接请求,先服务器发送SYN J包,此时connect进行堵塞状态。服务器监听到连接请求即收到SYN J包,调用accept函数接收请求向客户端发送SYN K、ACK J+1。此时accept进入阻塞状态。客户端收到服务器的SYN K,ACK J+1之后此时connect返回并对SYN K进行确认。服务器收到ACK K+1时accept返回,至此三次握手完毕建立连接。
简单来说,客户端的connect在三次握手的第二次返回,而服务器端的accept在三次握手的第三次返回。
Socket的工作方式
对于Socket最需要关注的是它的工作方式,在客户端Socket主要有两种工作方式:
- 同步方式
无论是TCP还是UDP,在使用Socket进行连接、发送、接收的时候,在未完成工作前代码不再继续往下执行,处于等待状态,知道语句完成对应工作后才继续执行下一条语句。
值得注意的是,TCP和UDP对于一件工作是否完成的定义不同,以发送Send为例:
对于TCP而言,未完成的工作包括:
- 缓冲区满了数据无法写入
- 数据写入了但还没轮到它发送
- 数据发送了但还未收到ACK确认
对于UDP而言,未完成的工作就是缓冲区满了就数据无法写入了。
- 异步方式
异步方式下不论对应工作是否完成都会往下执行,但工作完成后,是通过一个回调函数来通知调用者的。注意这个回调时在一个Socket内部创建的子线程上下文。
在同步方式中,可以理解为有一个Loop不停地轮询是否完成了工作,直到工作完成后才结束Loop。为了避免UI以及主逻辑被卡住,一般需要将以同步方式工作的操作都放到自制的子线程中。而在异步方式中,实质上是Socket内部创建了一个子线程。
游戏中的网络架构
理解了Socket之后所谓游戏中的网络框架也不难以理解,其实就是在Socket基础上进一步封装一套更方便游戏内部的消息传输机制,同时将消息解析细节和逻辑层代码进行分离,使逻辑层代码更加清晰可读。
框架设计要点
- 独立分层设计
- 事件机制
- 单独线程编码解码
游戏中常用的模块之间通信时,使用事件与代理可以极大的降低模块耦合,实现原理是函数指针,比如A模块执行了某个操作后通过广播向所有模块发送消息,这些模块如果事先绑定了对应消息的函数指针,就会收到该消息并处理。这个过程中A并不知道发送给谁,也不知道其他模块又做了什么。
低配版网络框架设计:比如客户端想要发送一个比较复杂的数据结构,如一个包含字符串和数字的结构体。首先这个数据需要在客户端转换成二进制,通过Socket发送到服务器,服务器的网络机制通过其Socket监听该数据包,然后解析二进制数据并还原到逻辑上层。
高配版网络框架设计:游戏中希望直接能将一个对象从客户端发送到服务器,或者直接将某个函数发送到服务器去执行,更甚者想要在逻辑层实现UDP的可靠数据传输,这些较为复杂的机制都包含在网络框架中,可借用第三方开源库实现如Google的Protobuf。
ProtoBuf是一种轻便高效的结构化数据存储格式,与平台和语言无关且可扩展,通常用于通讯协议和数据存储等领域。简单来说或,就是用来按照二进制格式保存和读取的开源库,在进行网络传输的时候需要把数据转换成二进制,通过网络层发送过去,但是如何把复杂数据(类对象)准确地转换成二进制发送并在接收端快速准确解析,这就是一个问题。ProtoBuf就可以做这个工作,它可以把一个对象序列化成二进制,然后在接收端再反序列化陈原来的对象内容,这样就成功的传输了一个类对象。这个过程是在应用层实现,所以本质上游戏网络层的实现对应的就是网络模型中应用层。
游戏开发协议设计
协议是通信双方能够理解的一种数据格式,是为网络中进行数据交换而建立的规则、标准或约定的集合。协议设计主要包含三要素:
- 语法:用户数据与控制信息的结构与格式,以及数据出现的顺序。
- 语义:解释控制信息每个部分的意义,规定了需要发出何种控制信息,以及完成的动作与做出什么样的响应。
- 时序:对事件发生顺序的详细说明
简单来说,语义是表示要做什么,语法表示要怎么做,时序表示做的顺序。
由于游戏的特殊性,对流量消耗要尽量的少,安全性要求更高,以及对平台支持要足够多等。着就要求游戏协议设计需要尽量简单、通用,代码层面上易扩展、解析效率足够高等特点。
基于此,我们需要从三个层次来考虑游戏设计的方案:
- 应用层面:文本协议、二进制协议、数据格式
- 安全层面:常规加密、动态加密
- 传输层面
应用层面常用的协议类型包括文本类型和二进制类型
- 文本协议
文本协议设计的目的是方便人理解,典型的HTTP协议格式就非常贴近文字描述。由于HTTP是客户端浏览器与Web服务器之间的应用层通信协议,所以适用性非常广泛。不过你会发现,当需要基于一个很简单的应答时,HTTP同样需要带上很多头信息,这对于流量有要求的游戏应用来说,是非常浪费的。
文本协议的优点在于通用且适用性广泛、方便理解且可读性好。而缺点在于它是基于行读的因此解析效率一般,另外HTTP携带的附带信息过多而造成传输效率低下,由于HTTP是无状态的服务器也就不知道客户端的状态,因此必须基于客户端的请求来回应,如此一来实时性就很低。另外,文本协议很难嵌入其他数据,且对二进制支持很差。
如果对游戏实时性要求不高且对流量要求也不高的情况,文本协议是一个不错的选择,一般而言短链接游戏多使用这种方式。
- 二进制协议
二进制协议就是一串字节流,是一个典型的IP协议,通常包括消息头和变长的消息体,消息头的长度固定,消息体的长度不固定,消息体包含着主要的内容主体。一般消息头都会包含消息体的长度,这样就能够基于头信息从数据流中解析出完整的二进制消息。
二进制协议head
消息头部分包含
-
cmd
命令字
二进制协议中命令字是双方协议文档中规定好的,比如0x01表示登录、0x02表示注册等,其实就是一个命令号。 -
sign
验证串
验证串是一个验证字符串,对消息体数据进行一定加密验证,保证数据安全。 -
content-leg
消息体长度 -
HeaderCRC
非必须的头验证
二进制协议消息体body
部分包含
message:login{
string username;
int64 password;
}
因为字段的数据类型有定义,顺序也有定义,整个二进制流读取的时候,是基于顺序读取的。
二进制协议的优点在于没有冗余字段,因此传输高效且耗费流量小,其次由于是基于基础数据类型操作所以解析速度快。缺点在于可读性差且不利于调试,其次可扩展性差,对于复杂数据结构支持不够友好。
如果游戏对实时性要求比较高且对流量也有要求,使用二进制协议会比较好,一般大型多人网游都采用二进制协议来设计。
数据交换格式
一般而言消息体的格式决定了语义和时序,格式不同的数据的序列化和反序列化也是不同的,比如message:login
中可以基于JSON格式来定义,也可以基于XML格式来定义,定义不同解析方式也各不相同。一般的消息体数据格式主要包括:JSON、ProtoBuf、XML、自定义等等。
JSON
JSON是一种轻量级的数据交换格式,互联网应用广泛,框架支持很多。其优势在于开源且格式统一,解析速度一般。缺点在于会有一些冗余字节不够简洁。
ProtoBuf
ProtoBuf是Google提供的开源序列化框架,类似XML\JSON这样的数据表示语言,但比它们占用空间都要小,没有冗余字段。优点在于灵活、解析速度快且易于开发(基于配置自动生成代码),也支持多种语言。一条消息数据使用ProtoBuf序列化后的大小是是二进制序列化的十分之一、JSON的十分之一、是XML的二十分之一。
XML
XML强烈不建议使用,因为它除了无效字符(标签)过多外,解析效率还很低。
网络模块设计
相对于服务端,客户端不需要处理大量网络数据,单线程就足以满足性能需求。异步Socket回调函数ReceiveCb把收到的消息按顺序存入消息列表msgList中,Update方法将依次读取和处理,在监听表中插入信息。Update会根据监听表和协议名,调用相应的处理方法。