读书笔记:《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 包含了进行网络通信必须的五种信息:
- 连接使用的协议
- 本机主机的 IP 地址
- 本地主机的 协议端口
- 远程主机的 IP 地址
- 远程主机的 协议端口
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 方法的参数说明。
Paramater | Description |
---|---|
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}
- 由 BeginConnect 最后一个传入的 socket,可由 ar.AsyncState 获取到
- 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 方法的参数说明。
Parameter | Description |
---|---|
buffer | byte 类型数组,用于存储接收到的数据 |
offset | buffer 中存储数据的位置,该位置从 0 开始计数 |
size | 最多接收的字节数 |
socketFlags | SocketFlags值的按位组合 |
callback | 回调函数,一个 AsyncCallback 委托 |
state | 一个用户定义对象,其中包含接收操作的相关信息。当操作完成时,此对象会被传递给 EndReceive 委托 |
程序在两个地方调用了 BeginReceive:
- ConnectCallback,在连接成功后,就开始接收数据,接收到数据后,回调函数 ReceiveCallback 被调用
- 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 方法的参数说明。
Parameter | Description |
---|---|
buffer | byte 类型数组,包含要发送的数据 |
offset | 从 buffer 中的 offset 位置开始发送 |
size | 要发送的字节数 |
socketFlags | SocketFlags 的按位组合 |
callback | 回调函数,一个 AsyncCallback 委托 |
state | 一个用户定义对象,其中包含接收操作的相关信息。当操作完成时,此对象会被传递给 EndReceive 委托 |
EndSend 函数原型如下。它的返回值代表发送的字节数,如果发送失败会抛出异常。
1public int EndSend (
2 IAsyncResult asyncResult
3)