《Unity3D网络游戏实战》 2021年2月8日22:54:57
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Constant { public const string IP = "127.0.0.1" ; public const int POINT = 8888 ; }
第1章 网络游戏的开端:Echo 1.1 网络基本概念 Socket:网络上的两个程序通过一个双向的通信连接实现数据交换,这个连接的一端称为Socket
包含:
连接使用的协议
本地主机的IP地址
本地的协议端口
远程主机的IP地址
远程协议端口
通信流程:
开启一个连接之前,需要创建一个Socket对象(使用API Socket),然后绑定本地使用的端口(使用API Bind)
服务端开启监听(使用API Listen),等待客户端接入
客户端连接服务器(使用API Connect)
服务器接受连接(使用API Accept)
客户端和服务端通过Send和Receive等API收发数据,操作系统会自动完成数据的确认、重传等步骤,确保传输的数据准确无误
某一方关闭连接(使用API Close),操作系统会执行“四次挥手”的步骤,关闭双方连接
1.2 IP地址:网络上计算机都是通过IP地址识别。通常,每一个IP地址对应于一台计算机
1.3 端口:是设备与外界通信交流的出口。范围0~65535
1.4 TCP和UDP协议:
TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议(本书的Socket通信特指使用TCP协议的通信)
UDP是一种无连接的、不可靠的、但传输效率较高的协议
1.2 开始网络编程:Echo Echo程序是网络编程中最基础的案例。建立网络连接后,客户端向服务端发送一行文本,服务端收到后将文本发送回客户端
客户端Echo代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 using System.Collections;using System.Collections.Generic;using UnityEngine;using System.Net.Sockets;using UnityEngine.UI;public class Ch1_Echo : MonoBehaviour { private Socket socket; public InputField inputField; public Text text; public void Connection () { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect("127.0.0.1" , 8888 ); } public void Send () { string sendStr = inputField.text; byte [] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr); socket.Send(sendBytes); byte [] readBuff = new byte [1024 ]; int count = socket.Receive(readBuff); string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0 , count); text.text = recvStr; socket.Close(); } }
创建Socket对象 1 socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
①地址族 AddressFamily
AddressFamily的值
含义
InterNetwork
使用IPv4
InterNetwork
使用IPv6
②套接字类型 SocketType
套接字类型,游戏开发中最常用的是字节流套接字,即Stream
SocketType的值
含义
Dgram
支持数据报,即最大长度固定(通常很小)的无连接、不可靠消息。消息可能会丢失或重复并可能在达到时不按顺序排列。Dgram类型的Socket在发送和接收数据之前不需要任何连接,并且可以与多个对方主机进行通信。Dgram使用数据报协议(UDP)和InterNetworkAddressFamily
Raw
支持对基础传输协议的访问。通过使用SocketTypeRaw,可以使用Internet控制消息协议(ICMP)和Internet组管理协议(Igmp)这样的协议来进行通信。在发送时,您的应用程序必须提供完整的IP标头。所接收的数据报在返回时会保持其IP标头和选项不变
RDM
支持无连接、面向消息、以可靠方式发送的消息,并保留数据中的消息边界。RDM(以可靠方式发送的消息)消息会依次到达,不会重复。此外,如果消息丢失,将会通知发送方。如果使用RDM初始化Socket,则在发送和接收数据之前无需建立远程主机连接。利用RDM,可以与多个对方主机进行通信
Seqpacket
在网络上提供排序字节流的面向连接且可靠的双向传输。Seqpacket不重复数据,它在数据流中保留边界。Seqpacket类型的Socket与单个对方主机通信,并且在通信开始之前需要建立远程主机连接
Stream
支持可靠、双向、基于连接的字节流,而不重复数据,也不保留边界。此类型的Socket与单个对方主机通信,并且在通信开始之前需要建立远程主机连接。Stream使用传输控制协议(TCP)和InterNetworkAddressFamily
Unknown
指定未知的Socket类型
③协议 ProtocolType
常用的协议
含义
常用的协议
含义
GGP
网关到网关的协议
PARC
通用数据包协议
ICMP
网际消息控制协议
RAW
原始IP数据包协议
ICMPv6
用于IPv6的Internet控制消息协议
TCP
传输控制协议
IDP
Internet数据报协议
UDP
用户数据包协议
IGMP
网际组管理协议
Unknown
未知协议
IP
网际协议
Unspecified
未指定的协议
Internet
数据包交换协议
若使用UDP协议,则改为:
1 socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
服务端Echo代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 using System;using System.Net;using System.Net.Sockets;namespace EchoServer { public class Ch1_Echo { public static void Main (string [] args ) { Socket listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ipAdr = IPAddress.Parse("127.0.0.1" ); IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888 ); listenfd.Bind(ipEp); listenfd.Listen(0 ); Console.WriteLine("[ 服务器 ] 启动成功" ); while (true ) { Socket connfd = listenfd.Accept(); Console.WriteLine("[ 服务器 ] Accept" ); byte [] readBuff = new byte [1024 ]; int count = connfd.Receive(readBuff); string readStr = System.Text.Encoding.Default.GetString(readBuff, 0 , count); Console.WriteLine("[ 服务器 ] 接收:" + readStr); byte [] sendBytes = System.Text.Encoding.Default.GetBytes(readStr); connfd.Send(sendBytes); } } } }
Echo演示
1.3 更多API Socket类的一些常用属性
属性
说明
AddressFamily
获取Socket的地址族
Available
获取已经从网络接收且可供读取的数据量
Blocking
获取或设置一个值,该值指示Socket是否处于阻塞模式
Connected
获取一个值,该值指示Socket是否连接
IsBound
指示Socket是否绑定到特定本地端口
OSSupportsIPv6
指示操作系统和网络适配器是否支持Internet协议第6版(IPv6)
ProtocolType
获取Socket的协议类型
SendBufferSize
指定Socket发送缓冲区的大小
SendTimeout
指定之后同步Send调用将超时的时间长度
ReceiveBufferSize
指定Socket接收缓冲区的大小
ReceiveTimeout
指定之后同步Receive调用将超时的时间长度
Ttl
指定Socket发送的Internet协议(IP)数据包的生存时间(TTL)值
Socket类的一些常用方法
方法
说明
Bind
使Socket与一个本地终结点相关联
Listen
将Socket置于侦听状态
Accept
为新建连接创建新的Socket
Connect
建立与远程主机的连接
Send
将数据发送到连接的Socket
Receive
接收来自绑定的Socket的数据
Close
关闭Socket连接并释放所有关联的资源
Shutdown
禁用某Socket上的发送和接收
Disconnect
关闭套接字连接并允许重用套接字
BeginAccept
开始一个异步操作来接受一个传入的连接尝试
EndAccept
异步接受传入的连接尝试
BeginConnect
开始一个对远程主机连接的异步请求
EndConnect
结束挂起的异步连接请求
BeginDisconnect
开始异步请求从远程终结点断开连接
EndDisconnect
结束挂起的异步断开连接请求
BeginReceive
开始从连接的Socket中异步接收数据
EndReceive
*将数据异步发送到连接的Socket(结束挂起的异步接收)
BeginSend
开始异步发送数据
EndSend
结束挂起的异步发送
GetSocketOption
返回Socket选项的值
SetSocketOption
设置Socket选项
Poll
确定Socket的状态
Select
确定一个或多个套接字的状态
第2章 分身有术:异步和多路复用 2.1 异步Connect 1 2 3 4 5 6 7 8 9 10 public IAsyncResult BeginConnect ( string host, int port, AsyncCallback requestCallback, object state ) public void EndConnect ( IAsyncResult asyncResult )
参数
说明
host
远程主机的名称(IP),如”127.0.0.1“
port
远程主机的端口号,如“8888”
requestCallback
一个AsyncCallback委托,即回调函数,回调函数的参数必须是这样的形式:void ConnectCallback(IAsyncResult ar)
state
一个用户定义对象,可包含连接操作的相关信息。此对象会被传递给回调函数
2.2 异步Receive和Send 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public IAsyncResult BeginReceive ( byte [] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state ) public int EndReceive ( IAsyncResult asyncResult )public IAsyncResult BeginSend ( byte [] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state )public int EndSend ( IAsyncReuslt asyncResult )
参数
说明
buffer
Byte类型的数组,它存储接收到的数据
offset
buffer中存储数据的位置,该位置从0开始计数
size
最多接收的字节数
socketFlags
SocketFlags值的按位组合,这里设置为0
callback
回调函数,一个AsyncCallback委托
state
一个用户定义对象,其中包含接收操作的相关信息。当操作完成时,此对象会被传递给EndReceive/EndSend委托
2.3 异步Accept 1 2 3 4 5 6 7 8 public IAsyncResult BeginAccept ( AsyncCallback callback, object state )public Socket EndAccept ( IAsyncResult asyncResult )
参数
说明
AsyncCallbcak
回调函数
state
表示状态信息,必须保证state中包含socket的句柄
当Receive返回值小于等于0时,表示Socket连接断开,可以关闭Socket。但也有一种特例,后面介绍
实践:做个聊天室 客户端聊天室代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 using System.Collections;using System.Collections.Generic;using UnityEngine;using System.Net.Sockets;using UnityEngine.UI;using System;public class Ch2_Chat : MonoBehaviour { private Socket socket; public InputField inputField; public Text text; private byte [] readBuff = new byte [1024 ]; private string recvStr = "" ; public void Connection () { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect(Constant.IP, Constant.POINT); socket.BeginReceive(readBuff, 0 , 1024 , 0 , ReceiveCallback, socket); } public void Send () { string sendStr = inputField.text; byte [] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr); socket.Send(sendBytes); } public void Close () { socket.Close(); } public void ReceiveCallback (IAsyncResult ar ) { try { Socket socket = (Socket)ar.AsyncState; int count = socket.EndReceive(ar); string s = System.Text.Encoding.Default.GetString(readBuff, 0 , count); recvStr = recvStr + "\n" + s; socket.BeginReceive(readBuff, 0 , 1024 , 0 , ReceiveCallback, socket); } catch (SocketException ex) { Debug.Log("Socket Receive Fail" + ex.ToString()); } } private void Update () { text.text = recvStr; } }
服务端聊天室代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 using System;using System.Net;using System.Net.Sockets;using System.Collections.Generic;namespace ChatServer { public class ClientState { public Socket socket; public byte [] readBuff = new byte [1024 ]; } public class Ch2_Chat { static Socket listenfd; static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>(); public static void Main (string [] args ) { listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ipAdr = IPAddress.Parse(Constant.IP); IPEndPoint ipEP = new IPEndPoint(ipAdr, Constant.POINT); listenfd.Bind(ipEP); listenfd.Listen(0 ); Console.WriteLine("[ 服务器 ] 启动成功" ); listenfd.BeginAccept(AcceptCallback, listenfd); Console.ReadLine(); } private static void AcceptCallback (IAsyncResult ar ) { try { Console.WriteLine("[ 服务器 ] Accept" ); Socket listenfd = (Socket)ar.AsyncState; Socket clientfd = listenfd.EndAccept(ar); ClientState state = new ClientState(); state.socket = clientfd; clients.Add(clientfd, state); clientfd.BeginReceive(state.readBuff, 0 , 1024 , 0 , ReceiveCallback, state); listenfd.BeginAccept(AcceptCallback, listenfd); } catch (SocketException ex) { Console.WriteLine("Socket Accept fail" + ex.ToString()); } } private static void ReceiveCallback (IAsyncResult ar ) { try { ClientState state = (ClientState)ar.AsyncState; Socket clientfd = state.socket; int count = clientfd.EndReceive(ar); if (count == 0 ) { clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Socket Close" ); return ; } string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 0 , count); string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr; byte [] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr); foreach (ClientState s in clients.Values) { s.socket.Send(sendBytes); } clientfd.BeginReceive(state.readBuff, 0 , 1024 , 0 , ReceiveCallback, state); } catch (SocketException ex) { Console.WriteLine("Socket Receive fail" + ex.ToString()); } } } }
聊天室演示
2.4 状态监测Poll 通过不断判断,利用同步的方式替代异步,因为同步程序更简单,而且不会引发线程问题。不足之处在于不断检测可能导致CPU占用过高。
1 2 3 4 public bool poll ( int microSeconds, SelectMode mode )
参数
说明
microSeconds
等待回应的时间,以微秒为单位,如果该参数为-1,表示一直等待,如果为0,表示非阻塞
mode
有3种可选的模式,分别如下: SelectRead:如果Socket可读(可以接受数据),返回true,否则返回false; SelectWrite:如果Socket可写,返回true,否则返回false; SelectError:如果连接失败,返回true,否则返回false
Poll方法将会检查Socket的状态。如果指定mode参数为SelectMode.SelectRead,则可确定Socket是否为可读;指定参数为SelectMode.SelectWrite可确定为是否可写;指定参数为SelectModeError,可以监测错误条件。Poll将在指定的时段(以微秒为单位)内阻止执行,如果希望无限期地等待响应,可将microSeconds设置为一个负整数;如果希望不阻塞,可将microSeconds设置为0(即没有任何等待,如果设置较长时间,服务端无法及时处理多个客户端同时连接的情况,且这样设置会导致程序的CPU占用率很高)
客户端Poll核心代码 1 2 3 4 5 6 7 8 9 10 11 public void Update (){ if (socket == null ) reutrn; if (socket.Poll(0 , SelectMode.SelectRead)) { byte [] readBuff = new byte [1024 ]; int count = socket.Receive(readBuff); string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0 , count); text.text = recvStr; } }
服务端Poll完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 using System;using System.Net;using System.Net.Sockets;using System.Collections.Generic;namespace Ch2 { public class ClientState { public Socket socket; public byte [] readBuff = new byte [1024 ]; } class Ch2_Poll { static Socket listenfd; static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>(); public static void Main (string [] args ) { listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ipAdr = IPAddress.Parse(Constant.IP); IPEndPoint ipEP = new IPEndPoint(ipAdr, Constant.POINT); listenfd.Bind(ipEP); listenfd.Listen(0 ); Console.WriteLine("[ 服务器 ] 启动成功" ); while (true ) { if (listenfd.Poll(0 , SelectMode.SelectRead)) { ReadListenfd(listenfd); } foreach (ClientState s in clients.Values) { Socket clientfd = s.socket; if (clientfd.Poll(0 , SelectMode.SelectRead)) { if (!ReadClientfd(clientfd)) { break ; } } } System.Threading.Thread.Sleep(1 ); } } public static void ReadListenfd (Socket listenfd ) { Console.WriteLine("Accept" ); Socket clientfd = listenfd.Accept(); ClientState state = new ClientState(); state.socket = clientfd; clients.Add(clientfd, state); } public static bool ReadClientfd (Socket clientfd ) { ClientState state = clients[clientfd]; int count = 0 ; try { count = clientfd.Receive(state.readBuff); } catch (SocketException ex) { clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Receive SocketException" + ex.ToString()); return false ; } if (count == 0 ) { clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Socket Close" ); return false ; } string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 0 , count); Console.WriteLine("Receive" + recvStr); string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr; byte [] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr); foreach (ClientState cs in clients.Values) { cs.socket.Send(sendBytes); } return true ; } } }
2.5 多路复用Select 同时检测多个Socket的状态。在设置要监听的Socket列表后,如果有一个(或多个)Socket可读(或可写,或发生错误信息),那就返回这些可读的Socket,如果没有可读的,那就阻塞 。此方法解决了Poll导致CPU占用过高的问题。
1 2 3 4 5 6 public static void Select ( IList checkRead, IList checkWrite, IList checkError, int microSeconds )
参数
说明
checkRead
检测是否有可读的Socket列表
checkWrite
检测是否有可写的Socket列表
checkError
检测是否有出错的Socket列表
microSeconds
等待回应时间,以微秒为单位,如果该参数为-1表示一直等待,如果为0表示非阻塞
客户端Select核心代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void Update (){ if (socket == null ){ return ; } checkRead.Clear(); checkRead.Add(socket); Socket.Select(checkRead, null , null , 0 ); foreach (Socket s in checkRead) { byte [] readBuff = new byte [1024 ]; int count = socket.Receive(readBuff); string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0 , count); text.text = recvStr; } }
由于程序在Update中不断检测数据,性能较差。商业上为了做到性能上的极致,多使用异步(或使用多线程模拟异步程序)。本书将会使用异步客户端、Select服务端演示程序。
服务端Select核心代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 using System;using System.Net;using System.Net.Sockets;using System.Collections.Generic;namespace Ch2 { public class ClientState { public Socket socket; public byte [] readBuff = new byte [1024 ]; } class Ch2_Select { static Socket listenfd; static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>(); public static void Main (string [] args ) { listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ipAdr = IPAddress.Parse(Constant.IP); IPEndPoint ipEP = new IPEndPoint(ipAdr, Constant.POINT); listenfd.Bind(ipEP); listenfd.Listen(0 ); Console.WriteLine("[ 服务器 ] 启动成功" ); List<Socket> checkRead = new List<Socket>(); while (true ) { checkRead.Clear(); checkRead.Add(listenfd); foreach (ClientState s in clients.Values) { checkRead.Add(s.socket); } Socket.Select(checkRead, null , null , 1000 ); foreach (Socket s in checkRead) { if (s == listenfd) { ReadListenfd(s); } else { ReadClientfd(s); } } } } public static void ReadListenfd (Socket listenfd ) { ... } public static bool ReadClientfd (Socket clientfd ) { ... } } }
第3章 实践出真知:大乱斗游戏 游戏说明:
打开客户端即视为进入游戏,在随机出生点刷出角色
使用鼠标左键点击场景,角色会自动走到指定位置
在站立状态下,点击鼠标右键可使角色发起攻击,角色会向鼠标指向的方向进攻
每个角色默认有100滴血(hp),受到攻击会掉血,死亡后从场景消失,提示“game over”
若玩家断线,视为死亡,从场景消失
知识铺垫 1.委托 回调函数的一种实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public delegate void DelegateStr (string str ) ;public static void PrintStr (string str ){ Console.WriteLine("PrintStr:" + str); } public static void Main (string [] args ){ DelegateStr fun = new DelegateStr(PrintStr); fun("Hello CT" ); Console.ReadLine(); }
2.通信协议 通信协议时通信双方对数据传送控制的一种约定,通信双方必须共同遵守。本节使用一种最简单的字符串协议来实现。协议格式如下:
1 2 3 消息名|参数1 , 参数2 , 参数3 ,... Move|127.0 .0 .1 :1234 , 10 , 0 , 8 ,
其他客户端收到服务端转发的字符串后,使用Split(‘|’)和Split(‘,’)解析
1 2 3 4 5 6 7 8 9 10 11 string str = "Move|127.0.0.1:1234, 10, 0, 8," ;string [] args = str.Split('|' );string msgName = args[0 ];string msgBody = args[1 ];string [] bodyArgs = msgBody.Split(',' );string desc = bodyArgs[0 ];float x = float .Parse(bodyArgs[1 ]);float y = float .Parse(bodyArgs[2 ]);float z = float .Parse(bodyArgs[3 ]);
结合委托的知识,客户端程序提供各种消息类型(通过消息名区分)的处理方法,网络模块解析消息,将不同类型的消息派发给不同的方法去处理。例如:如果收到一条”Move“协议,就交给OnMove方法处理;收到”Enter”协议,就交给OnEnter方法去处理
3.消息队列 C#的异步通信由线程池实现,不同的BeginReceive不一定在同一线程中执行。创建一个消息列表,每当收到消息便在列表末端添加数据,这个列表由主线程读取,它可以作为主线程和异步接收线程之间的桥梁。本章例子,消息队列使用List实现。
消息队列示意图:
客户端大乱斗代码 BaseHuman.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Ch3_BaseHuman : MonoBehaviour { protected bool isMoving = false ; protected bool isAttacking = false ; protected float attackTime = float .MinValue; private Vector3 targetPosition; public float speed = 3f ; private Animator animator; public string desc = "" ; public void MoveTo (Vector3 pos ) { targetPosition = pos; isMoving = true ; animator.SetBool("isMoving" , true ); } public void MoveUpdate () { if (isMoving == false ) return ; Vector3 pos = transform.position; transform.position = Vector3.MoveTowards(pos, targetPosition, speed * Time.deltaTime); transform.LookAt(targetPosition); if (Vector3.Distance(pos, targetPosition) < 0.05f ) { isMoving = false ; animator.SetBool("isMoving" , false ); } } public void Attack () { isAttacking = true ; attackTime = Time.time; animator.SetBool("isAttacking" , true ); } public void AttackUpdate () { if (!isAttacking) return ; if (Time.time - attackTime < 1.2f ) return ; isAttacking = false ; animator.SetBool("isAttacking" , false ); } protected void Start () { animator = GetComponent<Animator>(); } protected void Update () { MoveUpdate(); AttackUpdate(); } }
CtrlHuman.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Ch3_CtrlHuman : Ch3_BaseHuman { new void Start () { base .Start(); } new void Update () { base .Update(); if (Input.GetMouseButtonDown(0 )) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; Physics.Raycast(ray, out hit); if (hit.collider.tag == "Terrain" ) { MoveTo(hit.point); string sendStr = "Move|" ; sendStr += Ch3_NetManager.GetDesc() + "," ; sendStr += hit.point.x + "," ; sendStr += hit.point.y + "," ; sendStr += hit.point.z + "," ; Ch3_NetManager.Send(sendStr); } } if (Input.GetMouseButtonDown(1 )) { if (isAttacking) return ; if (isMoving) return ; Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; Physics.Raycast(ray, out hit); transform.LookAt(hit.point); transform.eulerAngles = new Vector3(0 , transform.eulerAngles.y, 0 ); Attack(); string sendStr = "Attack|" ; sendStr += Ch3_NetManager.GetDesc() + "," ; sendStr += transform.eulerAngles.y + "," ; Ch3_NetManager.Send(sendStr); Vector3 lineEnd = transform.position + 0.5f * Vector3.up; Vector3 lineStart = lineEnd + 20 * transform.forward; if (Physics.Linecast(lineStart,lineEnd,out hit)) { GameObject hitObj = hit.collider.gameObject; if (hitObj == gameObject) return ; Ch3_SyncHuman h = hitObj.GetComponent<Ch3_SyncHuman>(); if (h == null ) return ; sendStr = "Hit|" ; sendStr += Ch3_NetManager.GetDesc() + "," ; sendStr += h.desc + "," ; Ch3_NetManager.Send(sendStr); } } } }
SyncHuman.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Ch3_SyncHuman : Ch3_BaseHuman { public void SyncAttack (float eulY ) { transform.eulerAngles = new Vector3(0 , eulY, 0 ); Attack(); } new void Start () { base .Start(); } new void Update () { base .Update(); } }
NetManager.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 using System;using System.Collections;using System.Collections.Generic;using System.Net.Sockets;using UnityEngine;public static class Ch3_NetManager { static Socket socket; static byte [] readBuff = new byte [1024 ]; public delegate void MsgListener (string str ) ; private static Dictionary<string , MsgListener> listeners = new Dictionary<string , MsgListener>(); static List<string > msgList = new List<string >(); public static void AddListener (string msgName, MsgListener listener ) { listeners[msgName] = listener; } public static string GetDesc () { if (socket == null ) return "" ; if (!socket.Connected) return "" ; return socket.LocalEndPoint.ToString(); } public static void Connect (string ip, int port ) { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect(ip, port); socket.BeginReceive(readBuff, 0 , 1024 , 0 , ReceiveCallback, socket); } private static void ReceiveCallback (IAsyncResult ar ) { try { Socket socket = (Socket)ar.AsyncState; int count = socket.EndReceive(ar); string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0 , count); msgList.Add(recvStr); socket.BeginReceive(readBuff, 0 , 1024 , 0 , ReceiveCallback, socket); } catch (SocketException ex) { Debug.Log("Socket Receive fail" + ex.ToString()); } } public static void Send (string sendStr ) { if (socket == null ) return ; if (!socket.Connected) return ; byte [] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr); socket.Send(sendBytes); } public static void Update () { if (msgList.Count <= 0 ) return ; string msgStr = msgList[0 ]; msgList.RemoveAt(0 ); string [] split = msgStr.Split('|' ); string msgName = split[0 ]; string msgArgs = split[1 ]; if (listeners.ContainsKey(msgName)) { listeners[msgName](msgArgs); } } }
Main.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 using System;using System.Collections;using System.Collections.Generic;using UnityEngine;public class Ch3_Main : MonoBehaviour { public GameObject humanPrefab; public Ch3_BaseHuman myHuman; public Dictionary<string , Ch3_BaseHuman> otherHumans = new Dictionary<string , Ch3_BaseHuman>(); private void Awake () { Ch3_NetManager.AddListener("Enter" , OnEnter); Ch3_NetManager.AddListener("List" , OnList); Ch3_NetManager.AddListener("Move" , OnMove); Ch3_NetManager.AddListener("Leave" , OnLeave); Ch3_NetManager.AddListener("Attack" , OnAttack); Ch3_NetManager.AddListener("Die" , OnDie); Ch3_NetManager.Connect(Constant.IP, Constant.POINT); GameObject obj = (GameObject)Instantiate(humanPrefab); float x = UnityEngine.Random.Range(-5 , 5 ); float z = UnityEngine.Random.Range(-5 , 5 ); obj.transform.position = new Vector3(x, 0 , z); myHuman = obj.AddComponent<Ch3_CtrlHuman>(); myHuman.desc = Ch3_NetManager.GetDesc(); Vector3 pos = myHuman.transform.position; Vector3 eul = myHuman.transform.eulerAngles; string sendStr = "Enter|" + Ch3_NetManager.GetDesc() + "," + pos.x + "," + pos.y + "," + pos.z + "," + eul.y; Ch3_NetManager.Send(sendStr); } private void Start () { Ch3_NetManager.Send("List|" ); } private void Update () { Ch3_NetManager.Update(); } void OnEnter (string msgArgs ) { Debug.Log("OnEnter:" + msgArgs); string [] split = msgArgs.Split(',' ); string desc = split[0 ]; float x = float .Parse(split[1 ]); float y = float .Parse(split[2 ]); float z = float .Parse(split[3 ]); float eulY = float .Parse(split[4 ]); if (desc == Ch3_NetManager.GetDesc()) return ; GameObject obj = (GameObject)Instantiate(humanPrefab); obj.transform.position = new Vector3(x, y, z); obj.transform.eulerAngles = new Vector3(0 , eulY, 0 ); Ch3_BaseHuman h = obj.AddComponent<Ch3_SyncHuman>(); h.desc = desc; otherHumans.Add(desc, h); } private void OnList (string msgArgs ) { Debug.Log("OnList" + msgArgs); string [] split = msgArgs.Split(',' ); int count = (split.Length - 1 ) / 6 ; for (int i = 0 ; i < count; i++) { string desc = split[i * 6 + 0 ]; float x = float .Parse(split[i * 6 + 1 ]); float y = float .Parse(split[i * 6 + 2 ]); float z = float .Parse(split[i * 6 + 3 ]); float eulY = float .Parse(split[i * 6 + 4 ]); int hp = int .Parse(split[i * 6 + 5 ]); Debug.Log("test:" + desc); if (desc == Ch3_NetManager.GetDesc()) continue ; GameObject obj = (GameObject)Instantiate(humanPrefab); obj.transform.position = new Vector3(x, y, z); obj.transform.eulerAngles = new Vector3(0 , eulY, 0 ); Ch3_BaseHuman h = obj.AddComponent<Ch3_SyncHuman>(); h.desc = desc; otherHumans.Add(desc, h); } } void OnMove (string msgArgs ) { Debug.Log("OnMove:" + msgArgs); string [] split = msgArgs.Split(',' ); string desc = split[0 ]; float x = float .Parse(split[1 ]); float y = float .Parse(split[2 ]); float z = float .Parse(split[3 ]); if (!otherHumans.ContainsKey(desc)) return ; Ch3_BaseHuman h = otherHumans[desc]; Vector3 targetPos = new Vector3(x, y, z); h.MoveTo(targetPos); } void OnLeave (string msgArgs ) { Debug.Log("OnLeave:" + msgArgs); string [] split = msgArgs.Split(',' ); string desc = split[0 ]; if (!otherHumans.ContainsKey(desc)) return ; Ch3_BaseHuman h = otherHumans[desc]; Destroy(h.gameObject); otherHumans.Remove(desc); } void OnAttack (string msgArgs ) { Debug.Log("OnAttack" + msgArgs); string [] split = msgArgs.Split(',' ); string desc = split[0 ]; float eulY = float .Parse(split[1 ]); if (!otherHumans.ContainsKey(desc)) { return ; } Ch3_SyncHuman h = (Ch3_SyncHuman)otherHumans[desc]; h.SyncAttack(eulY); } void OnDie (string msgArgs ) { Debug.Log("OnDie:" + msgArgs); string [] split = msgArgs.Split(',' ); string hitDesc = split[0 ]; if (hitDesc == myHuman.desc) { Debug.Log("Game Over!" ); return ; } if (!otherHumans.ContainsKey(hitDesc)) return ; Ch3_SyncHuman h = (Ch3_SyncHuman)otherHumans[hitDesc]; h.gameObject.SetActive(false ); } }
服务端大乱斗代码 Battle.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 using System;using System.Net;using System.Net.Sockets;using System.Collections.Generic;using System.Reflection;using System.Linq;namespace Ch3 { public class ClientState { public Socket socket; public byte [] readBuff = new byte [1024 ]; public int hp = -100 ; public float x = 0 ; public float y = 0 ; public float z = 0 ; public float eulY = 0 ; } class Ch3_Battle { static Socket listenfd; public static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>(); public static void Main (string [] args ) { listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ipAdr = IPAddress.Parse(Constant.IP); IPEndPoint ipEP = new IPEndPoint(ipAdr, Constant.POINT); listenfd.Bind(ipEP); listenfd.Listen(0 ); Console.WriteLine("[ 服务器 ] 启动成功" ); List<Socket> checkRead = new List<Socket>(); while (true ) { checkRead.Clear(); checkRead.Add(listenfd); foreach (ClientState s in clients.Values) { checkRead.Add(s.socket); } Socket.Select(checkRead, null , null , 1000 ); foreach (Socket s in checkRead) { if (s == listenfd) { ReadListenfd(s); } else { ReadClientfd(s); } } } } public static void ReadListenfd (Socket listenfd ) { Console.WriteLine("Accept" ); Socket clientfd = listenfd.Accept(); ClientState state = new ClientState(); state.socket = clientfd; clients.Add(clientfd, state); } public static bool ReadClientfd (Socket clientfd ) { ClientState state = clients[clientfd]; int count = 0 ; try { count = clientfd.Receive(state.readBuff); } catch (SocketException ex) { MethodInfo mei = typeof (Ch3_EventHandler).GetMethod("OnDisconnect" ); object [] ob = { state }; mei.Invoke(null , ob); clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Receive SocketException" + ex.ToString()); return false ; } if (count == 0 ) { MethodInfo mei = typeof (Ch3_EventHandler).GetMethod("OnDisconnect" ); object [] ob = { state }; mei.Invoke(null , ob); clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Socket Close" ); return false ; } string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 0 , count); string [] split = recvStr.Split('|' ); Console.WriteLine("Receive" + recvStr); string msgName = split[0 ]; string msgArgs = split[1 ]; string funName = "Msg" + msgName; MethodInfo mi = typeof (Ch3_MsgHandler).GetMethod(funName); object [] o = { state, msgArgs }; mi.Invoke(null , o); return true ; } public static void Send (ClientState cs, string sendStr ) { byte [] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr); cs.socket.Send(sendBytes); } } }
MsgHandler.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace Ch3 { public class Ch3_MsgHandler { public static void MsgEnter (ClientState c, string msgArgs ) { Console.WriteLine("MsgEnter:" + msgArgs); string [] split = msgArgs.Split(',' ); string desc = split[0 ]; float x = float .Parse(split[1 ]); float y = float .Parse(split[2 ]); float z = float .Parse(split[3 ]); float eulY = float .Parse(split[4 ]); c.hp = 100 ; c.x = x; c.y = y; c.z = z; c.eulY = eulY; string sendStr = "Enter|" + msgArgs; foreach (ClientState cs in Ch3_Battle.clients.Values) { Ch3_Battle.Send(cs, sendStr); } } public static void MsgList (ClientState c, string msgArgs ) { string sendStr = "List|" ; foreach (ClientState cs in Ch3_Battle.clients.Values) { sendStr += cs.socket.RemoteEndPoint.ToString() + "," ; sendStr += cs.x.ToString() + "," ; sendStr += cs.y.ToString() + "," ; sendStr += cs.z.ToString() + "," ; sendStr += cs.eulY.ToString() + "," ; sendStr += cs.hp.ToString() + "," ; } Console.WriteLine("MsgList:" + sendStr); Ch3_Battle.Send(c, sendStr); } public static void MsgMove (ClientState c, string msgArgs ) { string [] split = msgArgs.Split(',' ); string desc = split[0 ]; float x = float .Parse(split[1 ]); float y = float .Parse(split[2 ]); float z = float .Parse(split[3 ]); c.x = x; c.y = y; c.z = z; string sendStr = "Move|" + msgArgs; foreach (ClientState cs in Ch3_Battle.clients.Values) { Ch3_Battle.Send(cs, sendStr); } } public static void MsgAttack (ClientState c, string msgArgs ) { string sendStr = "Attack|" + msgArgs; foreach (ClientState cs in Ch3_Battle.clients.Values) { Ch3_Battle.Send(cs, sendStr); } } public static void MsgHit (ClientState c, string msgArgs ) { string [] split = msgArgs.Split(',' ); string attDesc = split[0 ]; string hitDesc = split[1 ]; ClientState hitCS = null ; foreach (ClientState cs in Ch3_Battle.clients.Values) { if (cs.socket.RemoteEndPoint.ToString() == hitDesc) hitCS = cs; } if (hitCS == null ) return ; hitCS.hp -= 25 ; if (hitCS.hp <= 0 ) { string sendStr = "Die|" + hitCS.socket.RemoteEndPoint.ToString(); foreach (ClientState cs in Ch3_Battle.clients.Values) { Ch3_Battle.Send(cs, sendStr); } } } } }
EventHandler.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace Ch3 { public class Ch3_EventHandler { public static void OnDisconnect (ClientState c ) { string desc = c.socket.RemoteEndPoint.ToString(); Console.WriteLine("OnDisconnect:" + desc); string sendStr = "Leave|" + desc + "," ; foreach (ClientState cs in Ch3_Battle.clients.Values) { Ch3_Battle.Send(cs, sendStr); } } } }
大乱斗游戏截图演示
第4章 正确收发数据流 2021年2月11日18:47:30
4.1 粘包问题的三种解决方法 1.长度信息法 在每个数据包前面加上长度信息。每次接收到数据后,先读取表示长度的字节,如果缓冲区的数据长度大于要取的字节数,则取出相应的字节,否则等待下一次数据接收。(本书采用该方法解决粘包问题)
2.固定长度法 每次都以相同的长度发送数据,假设规定每条信息的长度都为10个字符,那么发送“Hello” “Unity”两条信息可以发送成”Hello…” “Unity…”,其中”.”表示填充字符,是为凑数,无实际意义。接收方读取10个字符,作为一条消息去处理。否则存起来,等待下次接收。
3.结束符号法 规定一个结束符号,作为消息间的分隔符。假设规定结束符号位”$”,那么发送“Hello” “Unity”可以发送成”Hello$” “Unity$”。接收方读到”$”,就将它前面的数据提取出来,作为一条消息去处理。后面的存起来,等待下次接收
4.2 解决粘包问题的代码实现 发送数据 1 2 3 4 5 6 7 8 9 10 11 public void Send (string sendStr ){ byte [] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr); Int16 len = (Int16) bodyBytes.Length; byte [] lenBytes = BitConverter.GetBytes(len); byte [] sendBytes = lenBytes.Concat(bodyBytes).ToArray(); socket.Send(sendBytes); }
以HelloWorld为例
变量
数据
sendStr
“HelloWorld”
bodyBytes
H e l l o W o r l d
len
10
lenBytes
0 A
sendBytes
0 A H e l l o W o r l d
接收数据 游戏程序一般会使用”长度信息法”处理粘包问题,核心思想是定义一个缓冲区(readBuff)和一个指示缓冲区有效长度变量(buffCount)
1 2 3 4 byte [] readBuff = new byte [1024 ];int buffCount = 0 ;
因为存在粘包现象,缓冲区里面会保存尚未处理的数据。所以接收数据时不再从缓冲区开头的位置写入,而是把新数据放在有效数据之后。
1 2 3 4 5 6 7 socket.BeginReceive(readBuff, buffCount, 1024 -buffCount, 0 , ReceiveCallback, socket);
在收到数据后,程序需要更新buffCount,以使下一次接收数据时,写入到缓冲区有效数据末尾
1 2 3 4 5 6 7 8 public void ReceiveCallback (IAsyncResult ar ){ Socket socket = (Socket) ar.AsyncState; int count = socket.EndReceive(ar); buffCount += count; ... }
处理数据(三种情况) 对于缓冲区数据长度,会有以下几种情况:
由于消息长度是16位(2B),缓冲区至少要有2个字节数据才能把长度信息解析处理,否则消息不完整,不解析它
1 2 3 4 5 6 7 public void OnReceiveData (){ if (buffCount <= 2 ) return ; ... }
1 2 3 4 5 6 7 8 9 10 public void OnReceiveData (){ if (buffCount <= 2 ) return ; Int16 bodyLength = BitConverter.ToInt16(readBuff, 0 ); if (buffCount < 2 + bodyLength); ... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void OnReceiveData (){ if (buffCount <= 2 ) return ; Int16 bodyLength = BitConverter.ToInt16(readBuff, 0 ); if (buffCount < 2 + bodyLength) return ; string s = System.Text.Encoding.UTF8.GetString(readBuff, 2 , bodyLength); int start = 2 + bodyLength; int count = buffCount - start; Array.Copy(readBuff, start, readBuff, 0 , count); buffCount -= start; if (readBuff.length > 2 ) { OnReceiveData(); } }
Array.Copy解析:
参数
说明
sourceArray
源数组
sourceIndex
源数组的起始位置
destinationArray
目标数组
destinationIndex
目标数组的起始位置
length
要复制的消息长度
4.3 大端小端问题 如果使用BitConverter.GetBytes将数字转换成二进制数据,转换出来的数据有可能基于大端模式,也有可能基于小端模式。我们规定必须使用小端编码(低地址放低位,高地址放高位)
经过简化的BitConverter.ToInt16源码
可以发现,对于不同计算机编码方式会有不同,那么不同计算机读取出老的数据长度也可能不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static short ToInt16 (byte [] value , int startIndex ){ if (startIndex % 2 == 0 ) { return *((short *) pbyte); } else { if (IsLittleEndian) { return (short )((*pbyte) | (*(pbyte + 1 ) << 8 )); } else { return (short )((*pbyte << 8 ) | (*(pbyte + 1 ))); } } }
使用Reverse()兼容大小端编码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void Send (){ string sendStr = InputField.text; byte [] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr); Int16 len = (Int16)bodyBytes.Length; byte [] lenBytes = BitConverter.GetBytes(len); if (!BitConverter.IsLittleEndian) { Debug.Log("[Send] Reverse lenBytes" ); lenBytes.Reverse(); } byte [] sendBytes = lenBytes.Concat(bodyBytes).ToArray(); socket.Send(sendBytes); }
手动还原数值 1 2 3 4 5 6 7 void OnReceiveData (){ ... Int16 bodyLength = (short )((readBuff[1 ] << 8 ) | readBuff[0 ]); ... }
4.5 完整发送数据 不完整发送示例
解决发送不完整问题 要让数据能够发送完整,需要在发送前将数据保存起来;如果发送不完整,在Send回调函数中继续发送数据。分析详见书P113-117
概括:构造一个ByteArray结构和一个发送队列Queue,使用异步发送。判断Queue不为空,就取队列第一个ByteArray发送,如果发送不完整,则继续发送。如果发送完整,则查看Queue是否为空,不为空,则发送下一条(把自己删除,将下一条移到First)
ByteArray 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 using System;public class ByteArray { const int DEFAULT_SIZE = 1024 ; int initSize = 0 ; public byte [] bytes; public int readIdx = 0 ; public int writeIdx = 0 ; private int capacity = 0 ; public int remain { get {return capacity-writeIdx; }} public int length { get { return writeIdx - readIdx; }} public ByteArray (int size = DEFAULT_SIZE ) { bytes = new byte [size]; capacity = size; initSize = size; readIdx = 0 ; writeIdx = 0 ; } public ByteArray (byte [] defaultBytes ) { bytes = defaultBytes; capacity = defaultBytes.Length; initSize = defaultBytes.Length; readIdx = 0 ; writeIdx = defaultBytes.Length; } public void Resize (int size ) { if (size < length) return ; if (size < initSize) return ; int n = 1 ; while (n < size) n*=2 ; capacity = n; byte [] newBytes = new byte [capacity]; Array.Copy(bytes, readIdx, newBytes, 0 , writeIdx - readIdx); bytes = newBytes; writeIdx = length; readIdx = 0 ; } public void CheckAndMoveBytes () { if (length < 8 ){ MoveBytes(); } } public void MoveBytes () { Array.Copy(bytes, readIdx, bytes, 0 , length); writeIdx = length; readIdx = 0 ; } public int Write (byte [] bs, int offset, int count ) { if (remain < count){ Resize(length + count); } Array.Copy(bs, offset, bytes, writeIdx, count); writeIdx += count; return count; } public int Read (byte [] bs, int offset, int count ) { count = Math.Min(count, length); Array.Copy(bytes, readIdx, bs, offset, count); readIdx += count; CheckAndMoveBytes(); return count; } public Int16 ReadInt16 () { if (length < 2 ) return 0 ; Int16 ret = (Int16)((bytes[readIdx+1 ] << 8 ) | bytes[readIdx]); readIdx += 2 ; CheckAndMoveBytes(); return ret; } public override string ToString () { return BitConverter.ToString(bytes, readIdx, length); } public string Debug () { return string .Format("readIdx({0}) writeIdx({1}) bytes({2})" , readIdx, writeIdx, BitConverter.ToString(bytes, 0 , bytes.Length)); } }
用于发送缓冲区(writeIdx=sendBytes.Length)
用于接收缓冲区
个人:ByteArray存在的问题 在重设失败时内存会导致溢出异常,正确的重设方式应该是把传入的数据和readIdx+remain作比较,考虑是否可以通过移动数据的方式解决,如果传入数据的count>readIdx+remain,此时才应该重设尺寸
Queue 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 Queue<ByteArray> writeQueue = new Queue<ByteArray>(); public void Send (){ byte [] sendBytes = 要发送的数据; ByteArray ba = new ByteArray(sendBytes); int count = 0 ; lock (writeQueue) { writeQueue.Enqueue(ba); count = writeQueue.Count; } if (count == 1 ) { socket.BeginSend(sendBytes, 0 , sendBytes.Length, 0 , SendCallback, socket); } Debug.Log("[Send]" + BitConverter.ToString(sendBytes)); } public void SendCallback (IAsyncResult ar ){ Socket sokcet = (Socket) ar.AsyncState; int count = socket.EndSend(ar); ByteArray ba; lock (writeQueue) { ba = writeQueue.First(); } ba.readIdx += count; if (ba.Length == 0 ) { lock (writeQueue) { writeQueue.Dequeue(); ba = writeQueue.First(); } } if (ba != null ) { socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0 , SendCallback, socket); } }
第5章 深入了解TCP,解决暗藏问题 5.1 从TCP到铜线 应用层 应用层功能是应用程序(游戏程序)提供的功能。在给客户端发送“hello”的例子中,程序把“hello”转化成二进制流传给传输层(传送给send方法,如图 5-1 所示)。操作系统会对二进制数据做一系列加工,使它适合于网络传输
传输层 IP协议最大的数据长度65535B = 20B(TCP头部) + 65515(用户数据)
TCP就是在网络层(IP协议)的基础上,增加了数据拆分(把TCP数据拆分成多个IP包)、确认重传、流量控制等机制
网络层 IP协议会给TCP数据添加本地地址、目的地址等信息
网络接口 在多层处理后,数据通过物理介质(如电缆、光纤)传输到接收方,接收方再依照相反的过程解析,得到用户数据。实际上,IP协议还会被封装成更为底层的链路层协议,以完成数据校验等一些功能。
5.2 数据传输流程 三次握手
四次挥手
5.3 常用TCP参数
ReceiveBufferSize:指定了操作系统读缓冲区的大小,默认是8192。当接收缓冲区满了的时候,发送端回暂停发送数据,较大的缓冲区可以减少发送端暂停的概率,提高发送效率
SendBufferSize:指定了操作系统写缓冲区大小,默认也是8192
NoDelay:指定发送数据时是否使用Nagle算法,对于实时性要求较高的游戏,该值需要设置成true。Nagle是一种节省网络流量的机制,默认情况下,TCP会使用Nagle算法去发送数据。Nagle算法的机制在于,如果发送端欲多次发送包含少量字节的数据包时,发送端不会立马发送数据,而是积攒到一定数量后再将其组成一个较大的数据包发送出去。这样可以提升网络传输效率,但降低网络的实时性。大部分网络游戏都会关闭Nagle算法
TTL:发送的IP数据包的生存时间值(Time To Live,TTL)。TTL是IP头部的一个值,该值表示一个IP数据报能够经过的最大的路由器跳数。主要作用是避免IP包在网络中的无限循环和收发。
ReuseAddress:端口复用,让同一个端口可被多个socket使用。最常见的用途是,防止服务器重启时,之前绑定的端口还未释放或者程序突然退出而系统没有释放端口。
1 2 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true );
LingerState:设置套接字保持连接的时间。开启LingerState能够在一定程度上保证发送数据的完整性。
1 2 3 socket.LingerState = new LingerOption(true , 10 );
当客户端调用Close()关闭Socket连接时,会给服务端发送FIN信号,然后进入等待。当服务端收到FIN信号时,数据长度为0,这也说明可以通过Receive的count==0判断Close。
5.4 Close的恰当时机 客户端当有数据需要发送时,就不Close;发送完,再Close
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bool isClosing = false ;public void Close (){ if (writeQueue.Count > 0 ) { isClosing = true ; } else { socket.Close(); } }
在关闭连接过程中,程序只负责将已有的数据发送完,不会发送新的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void Send (){ if (isClosing) return ; ... } public void SendCallback (IAsyncResult ar ){ ... if (ba != null ) { socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0 , SendCallback, socket); } else if (isClosing) { socket.Close(); } }
5.5 异常处理 以EndReceive为例
异常
发生条件
ArgumentNullException
asyncResult为null
ArgumentException
asyncResult通过调用未返回BeginReceive方法
InvalidOperationException
EndReceive之前已调用为异步读取
SocketException
尝试访问套接字时出错
ObjectDisposedException
Socket已关闭
5.6 心跳机制 TCP有一个连接检测机制,如果在指定时间内没有数据发送,会给对端发送一个信号(通过SetSocketOption的KeepAlive选项开启)。对端如果收到这个信号,回送一个TCP信号,确认已经收到,这样就知道此连接通畅。如果一段时间没有收到对方响应,会重试,几次无果后,关闭socket。
1 Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true );
TCP默认的KeepAlive很烂,上述“一段时间”默认为2小时。一般会自行实现心跳机制。客户端定时(如间隔1min)向服务端发送PING消息,服务端收到后回应PONG消息。
补充 一条完整数据的编码格式
蛮牛网课——游戏同步算法 URL:https://edu.manew.com/course/741/task/22633/show
作者:罗培羽