提起Tcp,相信不管是老鳥還是萌新多多少少都聽說過一些概念,在網絡編程中,Tcp也是一個必須掌握的內容。
而在Unity3D的開發當中,Tcp通訊更是重中之重,不懂Tcp,日常開發工作就會變得尤爲艱難甚至寸步難行。
本篇文章我就詳細的記錄一下我所瞭解的Unity中的Tcp通訊,並逐步去實現一個比較常用的Tcp通訊框架。
首先了解兩條比較基礎的東西:
- Tcp的概念:Tcp是網絡通訊協議中的一種,學過計算機網絡就應該知道,網絡協議模型共有5層,Tcp位列運輸層中,是一種面向連接的安全可靠全雙工通信協議。具體概念不多做介紹,如果對此有些迷惑可以看這篇文章,https://blog.csdn.net/Sqdmn/article/details/103581960
- Tcp通信過程:這裏主要了解3次握手和4次揮手就足夠了,可以深入瞭解一下3次握手的過程,以及爲什麼要3次握手。依然看https://blog.csdn.net/Sqdmn/article/details/103581960
要實現C#的Tcp通訊,需要使用System.Net.Sockets這個命名空間下的Socket類:
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
可以看到,創建Socket對象需要3個參數,下面介紹這個3個參數的含義。
1.AddressFamily 枚舉:
AppleTalk | 16 |
AppleTalk 地址。 |
Atm | 22 |
本機 ATM 服務地址。 |
Banyan | 21 |
Banyan 地址。 |
Ccitt | 10 |
CCITT 協議(如 X.25)的地址。 |
Chaos | 5 |
MIT CHAOS 協議的地址。 |
Cluster | 24 |
Microsoft 羣集產品的地址。 |
DataKit | 9 |
Datakit 協議的地址。 |
DataLink | 13 |
直接數據鏈接接口地址。 |
DecNet | 12 |
DECnet 地址。 |
Ecma | 8 |
歐洲計算機製造商協會 (ECMA) 地址。 |
FireFox | 19 |
FireFox 地址。 |
HyperChannel | 15 |
NSC Hyperchannel 地址。 |
Ieee12844 | 25 |
IEEE 1284.4 工作組地址。 |
ImpLink | 3 |
ARPANET IMP 地址。 |
InterNetwork | 2 |
IP 版本 4 的地址。 |
InterNetworkV6 | 23 |
IP 版本 6 的地址。 |
Ipx | 6 |
IPX 或 SPX 地址。 |
Irda | 26 |
IrDA 地址。 |
Iso | 7 |
ISO 協議的地址。 |
Lat | 14 |
LAT 地址。 |
Max | 29 |
MAX 地址。 |
NetBios | 17 |
NetBios 地址。 |
NetworkDesigners | 28 |
支持網絡設計器 OSI 網關的協議的地址。 |
NS | 6 |
Xerox NS 協議的地址。 |
Osi | 7 |
OSI 協議的地址。 |
Pup | 4 |
PUP 協議的地址。 |
Sna | 11 |
IBM SNA 地址。 |
Unix | 1 |
Unix 本地到主機地址。 |
Unknown | -1 |
未知的地址族。 |
Unspecified | 0 |
未指定的地址族。 |
VoiceView | 18 |
VoiceView 地址。 |
2.SocketType 枚舉:
Dgram | 2 |
支持數據報,即最大長度固定(通常很小)的無連接、不可靠消息。 消息可能會丟失或重複並可能在到達時不按順序排列。 Socket 類型的 Dgram 在發送和接收數據之前不需要任何連接,並且可以與多個對方主機進行通信。 Dgram 使用數據報協議 ( |
Raw | 3 |
支持對基礎傳輸協議的訪問。 通過使用 Raw,可以使用 Internet 控制消息協議 ( |
Rdm | 4 |
支持無連接、面向消息、以可靠方式發送的消息,並保留數據中的消息邊界。 RDM(以可靠方式發送的消息)消息會依次到達,不會重複。 此外,如果消息丟失,將會通知發送方。 如果使用 Rdm 初始化 Socket,則在發送和接收數據之前無需建立遠程主機連接。 利用 Rdm,您可以與多個對方主機進行通信。 |
Seqpacket | 5 |
在網絡上提供排序字節流的面向連接且可靠的雙向傳輸。 Seqpacket 不重複數據,它在數據流中保留邊界。 Seqpacket 類型的 Socket 與單個對方主機通信,並且在通信開始之前需要建立遠程主機連接。 |
Stream | 1 |
支持可靠、雙向、基於連接的字節流,而不重複數據,也不保留邊界。 此類型的 Socket 與單個對方主機通信,並且在通信開始之前需要建立遠程主機連接。 Stream 使用傳輸控制協議 ( |
Unknown | -1 |
指定未知的 Socket 類型。 |
3.ProtocolType 枚舉:
Ggp | 3 |
網關到網關協議。 |
Icmp | 1 |
網際消息控制協議。 |
IcmpV6 | 58 |
用於 IPv6 的 Internet 控制消息協議。 |
Idp | 22 |
Internet 數據報協議。 |
Igmp | 2 |
網際組管理協議。 |
IP | 0 |
網際協議。 |
IPSecAuthenticationHeader | 51 |
IPv6 身份驗證頭。 有關詳細信息,請參閱 https://www.ietf.org 上的 RFC 2292,第 2.2.1 節。 |
IPSecEncapsulatingSecurityPayload | 50 |
IPv6 封裝式安全措施負載頭。 |
IPv4 | 4 |
Internet 協議版本 4。 |
IPv6 | 41 |
Internet 協議版本 6 (IPv6)。 |
IPv6DestinationOptions | 60 |
IPv6 目標選項頭。 |
IPv6FragmentHeader | 44 |
IPv6 片段頭。 |
IPv6HopByHopOptions | 0 |
IPv6 逐跳選項頭。 |
IPv6NoNextHeader | 59 |
IPv6 No Next 頭。 |
IPv6RoutingHeader | 43 |
IPv6 路由頭。 |
Ipx | 1000 |
Internet 數據包交換協議。 |
ND | 77 |
網絡磁盤協議(非正式)。 |
Pup | 12 |
PARC 通用數據包協議。 |
Raw | 255 |
原始 IP 數據包協議。 |
Spx | 1256 |
順序包交換協議。 |
SpxII | 1257 |
順序包交換協議第 2 版。 |
Tcp | 6 |
傳輸控制協議。 |
Udp | 17 |
用戶數據報協議。 |
Unknown | -1 |
未知協議。 |
Unspecified | 0 |
未指定的協議。 |
上面分別列舉了3個枚舉所有的值及對應的含義,實際上
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
這行代碼的意思就是使用IPV4地址,全雙工安全可靠通訊,Tcp協議來創建 一個Socket對象。
Socket工作流程如下:
1.調用Connect方法連接服務器,連接失敗則跳出
public void Connect(string ip, int port)
{
m_IP = ip;
m_Port = port;
m_Socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
try
{
m_Socket.Connect(new IPEndPoint(IPAddress.Parse(ip), port));
m_ReceiveStream = new MemoryStream();
m_IsConnected = true;
StartReceive();
if (OnConnectSuccess != null)
{
OnConnectSuccess();
}
Debug.Log("連接服務器:" + ip + "成功!");
}
catch (Exception e)
{
if (OnConnectFail != null)
{
OnConnectFail();
}
Debug.Log(e.Message);
}
}
2.使用BeginReceive方法,使當前進入阻塞狀態,等待接收服務端發送的消息,成功接收到消息後對應的數據會寫入到一個字節流中等待處理
private void StartReceive()
{
if (!m_IsConnected) return;
m_Socket.BeginReceive(m_ReceiveBuffer,0,m_ReceiveBuffer.Length,SocketFlags.None,OnReceive, m_Socket);
}
3.當接收到消息時,調用EndReceive方法結束本次數據接收,然後開始解包,解包成功再次調用BeginReceive方法開始新一輪數據接
private void OnReceive(IAsyncResult ir)
{
if (!m_IsConnected) return;
try
{
int length = m_Socket.EndReceive(ir);
if (length < 1)//包長爲0
{
Debug.Log("服務器斷開連接");
Close();
return;
}
//1.設置數據流指針的到尾部
m_ReceiveStream.Position = m_ReceiveStream.Length;
//2.把接收到的數據全部寫入數據流
m_ReceiveStream.Write(m_ReceiveBuffer, 0, length);
//3.一個數據包至少包含包長,包的編碼兩部分信息,這兩部分信息都用ushort表示,而一個
// ushort佔2個byte,所以一個包的長度至少是4
if (m_ReceiveStream.Length < 4)
{
StartReceive();
return;
}
//4.循環解包
while (true)
{
m_ReceiveStream.Position = 0;
byte[] msgLenBuffer = new byte[2];
m_ReceiveStream.Read(msgLenBuffer, 0, 2);
//5.整個數據的包體中是包含了包體編碼這部分數據的,所以需要+2
int msgLen = BitConverter.ToUInt16(msgLenBuffer, 0) + 2;
//6.整個消息的包體長度包含了包長,包的編碼及具體數據,所以這個實際長度需要在msgLen
// 的基礎上再+2
int fullLen = 2 + msgLen;
//7.接收到的包體長度小於實際長度,說明這不是一個完整包,跳出循環繼續下一次接收
if (m_ReceiveStream.Length < fullLen)
{
break;
}
byte[] msgBuffer = new byte[msgLen];
m_ReceiveStream.Position = 2;
m_ReceiveStream.Read(msgBuffer, 0, msgLen);
lock (m_ReceiveQueue)
{
m_ReceiveQueue.Enqueue(msgBuffer);//把真實數據入隊,等待主線程處理
}
int remainLen = (int)m_ReceiveStream.Length - fullLen;
if (remainLen < 1)
{
m_ReceiveStream.Position = 0;
m_ReceiveStream.SetLength(0);
break;
}
m_ReceiveStream.Position = fullLen;
byte[] remainBuffer = new byte[remainLen];
m_ReceiveStream.Read(remainBuffer, 0, remainLen);
m_ReceiveStream.Position = 0;
m_ReceiveStream.SetLength(0);
m_ReceiveStream.Write(remainBuffer, 0, remainLen);
remainBuffer = null;
}
}
catch(Exception e)
{
Debug.Log("++服務器斷開連接," + e.Message);
Close();
return;
}
StartReceive();
}
這裏包含了粘包處理的代碼。粘包問題可能比較難理解,這裏進行一下分析:
- 什麼是粘包:一次通訊包含了多條數據
- 爲什麼會產生粘包:當數據包很小時,Tcp協議會把較小的數據包合併到一起,使一些零散的小包通過一次通訊就可以傳輸完畢。
- 如何解決粘包:這裏採用我最熟悉的也是最常用的方式,包體定長。包體定長就是指無論客戶端還是服務端,在發送數據包之前,需要把這個包的長度寫入到包頭,在解包的時候首先讀出包體長度msgLen,通過計算得出本次通訊實際的包體長度fullLen = msgLen+2,如果接收到的包體長度m_ReceiverBuffer.Length大於實際長度fullLen,則可以認爲發生粘包,此時只處理msgLen這個長度的包即可,剩餘的數據重新寫入m_ReceiverBuffer,下一次接收的包會和這個剩餘包重新組成一個完整包。
4.得到真實的數據,把真實數據入隊,並在Unity主線程的update中去處理
private void Update()
{
if (m_IsConnected)
CheckReceiveBuffer();
}
private void CheckReceiveBuffer()
{
while (true)
{
if (m_CheckCount > 5)//每幀處理5條數據
{
m_CheckCount = 0;
break;
}
m_CheckCount++;
lock (m_ReceiveQueue)
{
if (m_ReceiveQueue.Count < 1)
{
break;
}
byte[] buffer = m_ReceiveQueue.Dequeue();
byte[] msgContent = new byte[buffer.Length - 2];
ushort msgCode = 0;
using (MemoryStream ms = new MemoryStream(buffer))
{
byte[] msgCodeBuffer = new byte[2];
ms.Read(msgCodeBuffer, 0, msgCodeBuffer.Length);//讀包的編碼
msgCode = BitConverter.ToUInt16(msgCodeBuffer, 0);//得到包編碼
ms.Read(msgContent, 0, msgContent.Length);
}
if (onReceive != null)
{
onReceive(msgCode, msgContent);
}
}
}
}
爲什麼需要在Update中去處理呢?因爲BeginReceive是多線程異步接收到數據的,而unity的api不允許在非主線程中去訪問,所以要把在非主線程中得到的數據入隊,並在unity主線程中去處理。
以上是Tcp通訊在Unity中的發起連接,收包,拆包的過程。
下面來了解發包的過程。
上面提到過爲了解決粘包,需要把消息包體進行定長,所以發包第一步就是先把包體長度寫入數據流,然後把消息編碼寫入數據流,最後才寫入真實的要發送的數據內容,調用BeginSend進行異步發送。
public void Send(ushort msgCode, byte[] buffer)
{
if (!m_IsConnected) return;
byte[] sendMsgBuffer = null;
using (MemoryStream ms = new MemoryStream())
{
int msgLen = buffer.Length;
byte[] lenBuffer = BitConverter.GetBytes((ushort)msgLen);
byte[] msgCodeBuffer = BitConverter.GetBytes(msgCode);
ms.Write(lenBuffer, 0, lenBuffer.Length);
ms.Write(msgCodeBuffer, 0, msgCodeBuffer.Length);
ms.Write(buffer, 0, msgLen);
sendMsgBuffer = ms.ToArray();
}
lock (m_SendQueue)
{
m_SendQueue.Enqueue(sendMsgBuffer);
CheckSendBuffer();
}
}
private void CheckSendBuffer()
{
lock (m_SendQueue)
{
if (m_SendQueue.Count > 0)
{
byte[] buffer = m_SendQueue.Dequeue();
m_Socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, SendCallback, m_Socket);
}
}
}
private void SendCallback(IAsyncResult ir)
{
m_Socket.EndSend(ir);
CheckSendBuffer();
}
這裏爲了保證線程安全仍然需要把數據入隊,在確認到消息成功發送後才進行下一次數據的發送。
以上就是Unity中實現Tcp的全部內容。下面貼上整個通訊框架的代碼,直接調用Connect方法進行連接,連接成功後調用Send方法進行發送
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Policy;
using UnityEngine;
using UnityEngine.SocialPlatforms;
public class SocketMgr : MonoBehaviour
{
public static SocketMgr Instance = null;
public Action<ushort, byte[]> onReceive = null;
public Action OnConnectSuccess = null;
public Action OnConnectFail = null;
public Action OnDisConnect = null;
public bool IsConnected
{
get
{
return m_IsConnected;
}
}
private void Awake()
{
Instance = this;
m_ReceiveBuffer = new byte[1024 * 512];
m_SendQueue = new Queue<byte[]>();
m_ReceiveQueue = new Queue<byte[]>();
m_OnEventCallQueue = new Queue<Action>();
}
public void Connect(string ip, int port)
{
m_IP = ip;
m_Port = port;
m_Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
m_Socket.Connect(new IPEndPoint(IPAddress.Parse(ip), port));
m_ReceiveStream = new MemoryStream();
m_IsConnected = true;
StartReceive();
if (OnConnectSuccess != null)
{
OnConnectSuccess();
}
Debug.Log("連接服務器:" + ip + "成功!");
}
catch (Exception e)
{
if (OnConnectFail != null)
{
OnConnectFail();
}
Debug.Log(e.Message);
}
}
public void Close()
{
if (!m_IsConnected) return;
m_IsConnected = false;
try { m_Socket.Shutdown(SocketShutdown.Both); }
catch { }
m_Socket.Close();
m_SendQueue.Clear();
m_ReceiveQueue.Clear();
m_ReceiveStream.SetLength(0);
m_ReceiveStream.Close();
m_Socket = null;
m_ReceiveStream = null;
m_OnEventCallQueue.Enqueue(OnDisConnect);
}
public void Send(ushort msgCode, byte[] buffer)
{
if (!m_IsConnected) return;
byte[] sendMsgBuffer = null;
using (MemoryStream ms = new MemoryStream())
{
int msgLen = buffer.Length;
byte[] lenBuffer = BitConverter.GetBytes((ushort)msgLen);
byte[] msgCodeBuffer = BitConverter.GetBytes(msgCode);
ms.Write(lenBuffer, 0, lenBuffer.Length);
ms.Write(msgCodeBuffer, 0, msgCodeBuffer.Length);
ms.Write(buffer, 0, msgLen);
sendMsgBuffer = ms.ToArray();
}
lock (m_SendQueue)
{
m_SendQueue.Enqueue(sendMsgBuffer);
CheckSendBuffer();
}
}
private void Update()
{
if (m_IsConnected)
CheckReceiveBuffer();
if(m_OnEventCallQueue.Count > 0)
{
Action a = m_OnEventCallQueue.Dequeue();
if (a != null) a();
}
}
private void StartReceive()
{
if (!m_IsConnected) return;
m_Socket.BeginReceive(m_ReceiveBuffer, 0, m_ReceiveBuffer.Length, SocketFlags.None, OnReceive, m_Socket);
}
private void OnReceive(IAsyncResult ir)
{
if (!m_IsConnected) return;
try
{
int length = m_Socket.EndReceive(ir);
if (length < 1)
{
Debug.Log("服務器斷開連接");
Close();
return;
}
m_ReceiveStream.Position = m_ReceiveStream.Length;
m_ReceiveStream.Write(m_ReceiveBuffer, 0, length);
if (m_ReceiveStream.Length < 4)
{
StartReceive();
return;
}
while (true)
{
m_ReceiveStream.Position = 0;
byte[] msgLenBuffer = new byte[2];
m_ReceiveStream.Read(msgLenBuffer, 0, 2);
int msgLen = BitConverter.ToUInt16(msgLenBuffer, 0) + 2;
int fullLen = 2 + msgLen;
if (m_ReceiveStream.Length < fullLen)
{
break;
}
byte[] msgBuffer = new byte[msgLen];
m_ReceiveStream.Position = 2;
m_ReceiveStream.Read(msgBuffer, 0, msgLen);
lock (m_ReceiveQueue)
{
m_ReceiveQueue.Enqueue(msgBuffer);
}
int remainLen = (int)m_ReceiveStream.Length - fullLen;
if (remainLen < 1)
{
m_ReceiveStream.Position = 0;
m_ReceiveStream.SetLength(0);
break;
}
m_ReceiveStream.Position = fullLen;
byte[] remainBuffer = new byte[remainLen];
m_ReceiveStream.Read(remainBuffer, 0, remainLen);
m_ReceiveStream.Position = 0;
m_ReceiveStream.SetLength(0);
m_ReceiveStream.Write(remainBuffer, 0, remainLen);
remainBuffer = null;
}
}
catch(Exception e)
{
Debug.Log("++服務器斷開連接," + e.Message);
Close();
return;
}
StartReceive();
}
private void CheckSendBuffer()
{
lock (m_SendQueue)
{
if (m_SendQueue.Count > 0)
{
byte[] buffer = m_SendQueue.Dequeue();
m_Socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, SendCallback, m_Socket);
}
}
}
private void CheckReceiveBuffer()
{
while (true)
{
if (m_CheckCount > 5)
{
m_CheckCount = 0;
break;
}
m_CheckCount++;
lock (m_ReceiveQueue)
{
if (m_ReceiveQueue.Count < 1)
{
break;
}
byte[] buffer = m_ReceiveQueue.Dequeue();
byte[] msgContent = new byte[buffer.Length - 2];
ushort msgCode = 0;
using (MemoryStream ms = new MemoryStream(buffer))
{
byte[] msgCodeBuffer = new byte[2];
ms.Read(msgCodeBuffer, 0, msgCodeBuffer.Length);
msgCode = BitConverter.ToUInt16(msgCodeBuffer, 0);
ms.Read(msgContent, 0, msgContent.Length);
}
if (onReceive != null)
{
onReceive(msgCode, msgContent);
}
}
}
}
private void SendCallback(IAsyncResult ir)
{
m_Socket.EndSend(ir);
CheckSendBuffer();
}
private void OnDestroy()
{
Close();
m_SendQueue = null;
m_ReceiveQueue = null;
m_ReceiveStream = null;
m_ReceiveBuffer = null;
m_OnEventCallQueue.Clear();
m_OnEventCallQueue = null;
}
private Queue<Action> m_OnEventCallQueue = null;
private Queue<byte[]> m_SendQueue = null;
private Queue<byte[]> m_ReceiveQueue = null;
private MemoryStream m_ReceiveStream = null;
private byte[] m_ReceiveBuffer = null;
private bool m_IsConnected = false;
private string m_IP = string.Empty;
private int m_CheckCount = 0;
private int m_Port = int.MaxValue;
private Socket m_Socket = null;
}
這是我在CSDN的第一篇博客,文筆不是很好,寫的也比較亂
下一篇就去實現服務端的Tcp,把這篇內容真正的跑起來
也希望我的文筆通過不斷的寫作能逐漸得到提高。