【Unity】 在Unity中實現Tcp通訊(1)——客戶端

提起Tcp,相信不管是老鳥還是萌新多多少少都聽說過一些概念,在網絡編程中,Tcp也是一個必須掌握的內容。

而在Unity3D的開發當中,Tcp通訊更是重中之重,不懂Tcp,日常開發工作就會變得尤爲艱難甚至寸步難行。

本篇文章我就詳細的記錄一下我所瞭解的Unity中的Tcp通訊,並逐步去實現一個比較常用的Tcp通訊框架。

首先了解兩條比較基礎的東西:

 

  1. Tcp的概念:Tcp是網絡通訊協議中的一種,學過計算機網絡就應該知道,網絡協議模型共有5層,Tcp位列運輸層中,是一種面向連接的安全可靠全雙工通信協議。具體概念不多做介紹,如果對此有些迷惑可以看這篇文章,https://blog.csdn.net/Sqdmn/article/details/103581960
  2. 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 協議的地址。

13

直接數據鏈接接口地址。

DecNet 12

DECnet 地址。

Ecma 8

歐洲計算機製造商協會 (ECMA) 地址。

FireFox 19

FireFox 地址。

HyperChannel 15

NSC Hyperchannel 地址。

Ieee12844 25

IEEE 1284.4 工作組地址。

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 使用數據報協議 (ProtocolType.Udp) 和 AddressFamily.InterNetwork 地址族。

Raw 3

支持對基礎傳輸協議的訪問。 通過使用 Raw,可以使用 Internet 控制消息協議 (ProtocolType.Icmp) 和 Internet 組管理協議 (ProtocolType.Igmp) 這樣的協議來進行通信。 在發送時,您的應用程序必須提供完整的 IP 標頭。 所接收的數據報在返回時會保持其 IP 標頭和選項不變。

Rdm 4

支持無連接、面向消息、以可靠方式發送的消息,並保留數據中的消息邊界。 RDM(以可靠方式發送的消息)消息會依次到達,不會重複。 此外,如果消息丟失,將會通知發送方。 如果使用 Rdm 初始化 Socket,則在發送和接收數據之前無需建立遠程主機連接。 利用 Rdm,您可以與多個對方主機進行通信。

Seqpacket 5

在網絡上提供排序字節流的面向連接且可靠的雙向傳輸。 Seqpacket 不重複數據,它在數據流中保留邊界。 Seqpacket 類型的 Socket 與單個對方主機通信,並且在通信開始之前需要建立遠程主機連接。

Stream 1

支持可靠、雙向、基於連接的字節流,而不重複數據,也不保留邊界。 此類型的 Socket 與單個對方主機通信,並且在通信開始之前需要建立遠程主機連接。 Stream 使用傳輸控制協議 (ProtocolType.Tcp) 和 AddressFamilyInterNetwork 地址族。

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();
}

這裏包含了粘包處理的代碼。粘包問題可能比較難理解,這裏進行一下分析:

  1. 什麼是粘包:一次通訊包含了多條數據
  2. 爲什麼會產生粘包:當數據包很小時,Tcp協議會把較小的數據包合併到一起,使一些零散的小包通過一次通訊就可以傳輸完畢。
  3. 如何解決粘包:這裏採用我最熟悉的也是最常用的方式,包體定長。包體定長就是指無論客戶端還是服務端,在發送數據包之前,需要把這個包的長度寫入到包頭,在解包的時候首先讀出包體長度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,把這篇內容真正的跑起來

也希望我的文筆通過不斷的寫作能逐漸得到提高。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章