【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,把这篇内容真正的跑起来

也希望我的文笔通过不断的写作能逐渐得到提高。

猜你喜欢

转载自blog.csdn.net/s_GQY/article/details/106187350