..

读书笔记:《Unity3D 网络游戏实战》

网络游戏的开端:Echo

藏在幕后的服务器

客户端和客户端之间通过服务端的消息转发进行通信。例如在下图中,玩家 A 和玩家 B 的便是通过服务端的转发实现了位置同步。

sequenceDiagram
    participant ClientA
    participant Server
    participant ClientB

    ClientA->>Server: 玩家A移动的坐标
    activate Server
    Server->>ClientB: 将新坐标发给ClientB
    deactivate Server

    ClientB->>Server: 玩家B移动的坐标
    activate Server
    Server->>ClientA: 将新坐标发给ClientA
    deactivate Server

服务器与服务器之间通常使用 TCP 网络通信,各个服务器相互连接,形成服务器集群。

flowchart TB

subgraph Server
    subgraph CoreServer
        ServerA
    end
    subgraph SubServer
    direction RL
        ServerB
        ServerC
    end
end

subgraph Clients
    ClientA
    ClientB
    ClientC
    ClientD
end

ServerA <--> ServerB & ServerC

ServerB <--> ClientA & ClientB

ServerC <--> ClientC & ClientD

网络连接的端点:Socket

网络上的两个程序通过一个双向的通信连接实现数据交换,连接的一端称为 Socket。

一个 Socket 包含了进行网络通信必须的五种信息:

  1. 连接使用的协议
  2. 本机主机的 IP 地址
  3. 本地主机的 协议端口
  4. 远程主机的 IP 地址
  5. 远程主机的 协议端口

Socket 通信流程

flowchart TB

subgraph Client
    direction TB
    ClientSocket
    ClientConnect
    ClientSend
    ClientReceive
    ClientClose
end

subgraph Server
    direction TB
    ServerSocket
    ServerBind
    ServerListen
    ServerAccept
    ServerReceive
    ServerSend
    ServerClose
end

ServerSocket -->
ServerBind -->
ServerListen -->
ServerAccept -->
ServerReceive -->
ServerSend -->
ServerClose

ClientSocket -->
ClientConnect -->
ClientSend -->
ClientReceive -->
ClientClose

ClientConnect --> ServerAccept
ClientSend --> ServerReceive
ServerSend --> ClientReceive
ClientClose --> ServerClose

TCP 与 UDP

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议

UDP 是无连接的、不可靠的、但传输效率高的协议

开始网络编程:Echo

sequenceDiagram

Client ->> Server: 发送文本
activate Server
Server ->> Client: 回应文本
deactivate Server

客户端代码

服务端代码

分身有术:异步和多路复用

以下给出一个简单的异步代码实现。为主线程设置一个五秒后被调用的方法。在这五秒期间,主线程可以做其他的事情。

 1public class Async : MonoBehaviour
 2{
 3    private void Start()
 4    {
 5        var timer = new Timer(TimeOut, null, 5000, 0);
 6        // Do other things.
 7    }
 8
 9    private void TimeOut(System.Object state)
10    {
11        Debug.Log("Time out")
12    }
13}
sequenceDiagram
    Participant 主线程
    Participant 另外某条线程

    主线程 ->> 另外某条线程: new Timer(TimeOut, null, 5000, 0)
    activate 主线程
    activate 另外某条线程
    Note right of 另外某条线程: 等待5000毫秒
    Note left of 主线程: 其他程序代码
    另外某条线程 ->> 主线程: 调用TimeOut方法
    deactivate 另外某条线程
    deactivate 主线程

异步客户端

同步模式中,客户端使用 API Connect 连接服务器,并使用 API Send 和 Receive 接收数据。

异步模式中,客户端可以使用 BeginConnect 和 EndConnect 等 API 完成同样的功能。

异步 Connect

每一个同步 API 对应着两个异步 API,分别是在原名称前面加上 Begin 和 End。

1public IAsyncRequest BeginConnect(
2    string host,
3    int port,
4    AsyncCallback requestCallback,
5    object state
6)

以下是 BeginConnect 方法的参数说明。

ParamaterDescription
host远程主机的 IP,如 127.0.0.1
port远程主机的端口,如 8888
requestCallback一个 AsyncCallback 委托,即回调函数
state一个用户定义对象,可包含连接操作的相关信息,此对象会被传递给回调函数

IAsyncResult 是 .Net 提供的一种异步操作,通过名为 BeginXXXX 和 EndXXX 两个方法开实现原同步方法的异步调用。

ASyncCallback 委托用来调用回调方法,状态对象用来向回调方法传递状态信息。

BeginXXX 方法返回一个实现 IAsyncRequest 接口的对象。

EndXXX 方法用于结束异步操作并返回操作结果。它包含有一个 IAsyncResult 参数,用于获取异步操作是否完成的信息,返回值与同步方法相同。

 1public void Connection()
 2{
 3    socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocalType.Tcp);
 4    socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket);
 5}
 6
 7public void ConnectCallback(IAsyncResult ar)
 8{
 9    try
10    {
11        Socket socket = (Socket) ar.AsyncState;
12        socket.EndConnect(ar);
13        Debug.Log("Socket Connect Succ");
14    }
15    catch (SocketException ex)
16    {
17        Debug.Log("Socket Connect Fail" + ex.ToString());
18    }
19}
  1. 由 BeginConnect 最后一个传入的 socket,可由 ar.AsyncState 获取到
  2. try-catch 是 c# 处理异常的结构,它运行将任何可能发生异常情形的程序代码放置在 try 语句中进行监控,异常发生后,catch 中的代码将会被执行

异步 Receive

Receive 是一个阻塞方法,会让客户端一直卡着,直至收到服务端的数据为止。

与 BeginConnect 相似,BeginReceive 用于实现异步数据的接收。

1public IAsyncResult BeginReceive
2{
3    byte[] buffer,
4    int offset,
5    int size,
6    SocketFlags socketFlags,
7    AsyncCallback callback,
8    object state
9}

以下是 BeginReceive 方法的参数说明。

ParameterDescription
bufferbyte 类型数组,用于存储接收到的数据
offsetbuffer 中存储数据的位置,该位置从 0 开始计数
size最多接收的字节数
socketFlagsSocketFlags值的按位组合
callback回调函数,一个 AsyncCallback 委托
state一个用户定义对象,其中包含接收操作的相关信息。当操作完成时,此对象会被传递给 EndReceive 委托

程序在两个地方调用了 BeginReceive:

  1. ConnectCallback,在连接成功后,就开始接收数据,接收到数据后,回调函数 ReceiveCallback 被调用
  2. BeginReceive 内部,接收完一串数据后,等待下一串数据的到来
flowchart LR
subgraph SyncReceive
    BeginReceive
    ReceiveCallback
end

BeginReceive --> ReceiveCallback
ReceiveCallback --> BeginReceive

Socket --> BeginConnect --> ConnectCallback --> BeginReceive

异步 Send

TCP是可靠连接,当接收方没有收到数据时,发送方会重新发送数据,直至确认接收方收到数据为止。

在操作系统内部,每个 Socket 都会有一个发送缓冲区,用于保存那些接收方还没有确认的数据。

Socket 的用户层面与系统层面

  • Socket 使用的协议、IP、端口属于用户层面的属性,可以直接修改
  • 操作系统层面拥有“发送”和“接收”两个缓冲区,当调用 Send 方法时,程序将要发送的字节流写入到发送缓冲区中,再由操作系统完成数据的发送和确认
    • 发送缓冲区的长度是有限的(默认值约为8KB),如果缓冲区满,那么Send就会阻塞,直到缓冲区的数据被确认腾出空间。
    • Send 过程只是把数据写入到发送缓冲区,然后由操作系统负责重传、确认等步骤。Send 方法返回只代表成功将数据放到发送缓存区中,对方可能还没有收到数据。
1public IAsyncResult BeginSend(
2    byte[] buffer,
3    int offset,
4    int size,
5    SocketFlags socketFlags,
6    AsyncCallback callback,
7    object state
8)

以下是 BeginSend 方法的参数说明。

ParameterDescription
bufferbyte 类型数组,包含要发送的数据
offset从 buffer 中的 offset 位置开始发送
size要发送的字节数
socketFlagsSocketFlags 的按位组合
callback回调函数,一个 AsyncCallback 委托
state一个用户定义对象,其中包含接收操作的相关信息。当操作完成时,此对象会被传递给 EndReceive 委托

EndSend 函数原型如下。它的返回值代表发送的字节数,如果发送失败会抛出异常。

1public int EndSend (
2    IAsyncResult asyncResult
3)

实践出真知:大乱斗游戏

正确收发数据流

深入了解 TCP,解决暗藏问题

通用客户端网络模块

通用服务端框架

完整大项目:坦克大战

UI 界面模块

游戏大厅和房间

战斗和胜负判定

同步战斗信息

There is nothing new under the sun.