【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 枚举:

AppleTalk16

AppleTalk 地址。

Atm22

本机 ATM 服务地址。

Banyan21

Banyan 地址。

Ccitt10

CCITT 协议(如 X.25)的地址。

Chaos5

MIT CHAOS 协议的地址。

Cluster24

Microsoft 群集产品的地址。

DataKit9

Datakit 协议的地址。

13

直接数据链接接口地址。

DecNet12

DECnet 地址。

Ecma8

欧洲计算机制造商协会 (ECMA) 地址。

FireFox19

FireFox 地址。

HyperChannel15

NSC Hyperchannel 地址。

Ieee1284425

IEEE 1284.4 工作组地址。

3

ARPANET IMP 地址。

InterNetwork2

IP 版本 4 的地址。

InterNetworkV623

IP 版本 6 的地址。

Ipx6

IPX 或 SPX 地址。

Irda26

IrDA 地址。

Iso7

ISO 协议的地址。

Lat14

LAT 地址。

Max29

MAX 地址。

NetBios17

NetBios 地址。

NetworkDesigners28

支持网络设计器 OSI 网关的协议的地址。

NS6

Xerox NS 协议的地址。

Osi7

OSI 协议的地址。

Pup4

PUP 协议的地址。

Sna11

IBM SNA 地址。

Unix1

Unix 本地到主机地址。

Unknown-1

未知的地址族。

Unspecified0

未指定的地址族。

VoiceView18

VoiceView 地址。

2.SocketType 枚举: 

Dgram2

支持数据报,即最大长度固定(通常很小)的无连接、不可靠消息。 消息可能会丢失或重复并可能在到达时不按顺序排列。 Socket 类型的 Dgram 在发送和接收数据之前不需要任何连接,并且可以与多个对方主机进行通信。 Dgram 使用数据报协议 (ProtocolType.Udp) 和 AddressFamily.InterNetwork 地址族。

Raw3

支持对基础传输协议的访问。 通过使用 Raw,可以使用 Internet 控制消息协议 (ProtocolType.Icmp) 和 Internet 组管理协议 (ProtocolType.Igmp) 这样的协议来进行通信。 在发送时,您的应用程序必须提供完整的 IP 标头。 所接收的数据报在返回时会保持其 IP 标头和选项不变。

Rdm4

支持无连接、面向消息、以可靠方式发送的消息,并保留数据中的消息边界。 RDM(以可靠方式发送的消息)消息会依次到达,不会重复。 此外,如果消息丢失,将会通知发送方。 如果使用 Rdm 初始化 Socket,则在发送和接收数据之前无需建立远程主机连接。 利用 Rdm,您可以与多个对方主机进行通信。

Seqpacket5

在网络上提供排序字节流的面向连接且可靠的双向传输。 Seqpacket 不重复数据,它在数据流中保留边界。 Seqpacket 类型的 Socket 与单个对方主机通信,并且在通信开始之前需要建立远程主机连接。

Stream1

支持可靠、双向、基于连接的字节流,而不重复数据,也不保留边界。 此类型的 Socket 与单个对方主机通信,并且在通信开始之前需要建立远程主机连接。 Stream 使用传输控制协议 (ProtocolType.Tcp) 和 AddressFamilyInterNetwork 地址族。

Unknown-1

指定未知的 Socket 类型。

3.ProtocolType 枚举:

Ggp3

网关到网关协议。

Icmp1

网际消息控制协议。

IcmpV658

用于 IPv6 的 Internet 控制消息协议。

Idp22

Internet 数据报协议。

Igmp2

网际组管理协议。

IP0

网际协议。

IPSecAuthenticationHeader51

IPv6 身份验证头。 有关详细信息,请参阅 https://www.ietf.org 上的 RFC 2292,第 2.2.1 节。

IPSecEncapsulatingSecurityPayload50

IPv6 封装式安全措施负载头。

IPv44

Internet 协议版本 4。

IPv641

Internet 协议版本 6 (IPv6)。

IPv6DestinationOptions60

IPv6 目标选项头。

IPv6FragmentHeader44

IPv6 片段头。

IPv6HopByHopOptions0

IPv6 逐跳选项头。

IPv6NoNextHeader59

IPv6 No Next 头。

IPv6RoutingHeader43

IPv6 路由头。

Ipx1000

Internet 数据包交换协议。

ND77

网络磁盘协议(非正式)。

Pup12

PARC 通用数据包协议。

Raw255

原始 IP 数据包协议。

Spx1256

顺序包交换协议。

SpxII1257

顺序包交换协议第 2 版。

Tcp6

传输控制协议。

Udp17

用户数据报协议。

Unknown-1

未知协议。

Unspecified0

未指定的协议。

上面分别列举了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,把这篇内容真正的跑起来

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

  • 26
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Unity实现Socket通讯时,常常会遇到TCP粘包和拆包的问题。下面我将介绍在Unity如何解决这些问题。 TCP粘包是指在传输过程,由于数据缓冲区的限制,多个小的数据包可能会被合并成一个大的数据包,导致数据的解析和处理出现问题。为了解决这个问题,可以通过以下两种方式来处理。 第一种方式是定长包头+包体的设计。即在数据包前面添加一个固定长度的包头,包头包含了包体的长度信息。接收方在接收数据时,首先读取包头的长度信息,然后再根据长度信息读取相应长度的数据进行解析和处理。 第二种方式是使用特殊的字符序列作为包的分隔符。例如,在每个数据包的末尾添加一个换行符或其他不常用的字符作为分隔符。接收方在接收数据时,通过查找这个分隔符来确定包的结束位置,然后对数据进行解析和处理。 TCP拆包是指在传输过程,一个大的数据包可能会被拆分成多个小的数据包,导致数据的解析和处理出现问题。为了解决这个问题,可以通过以下方式来处理。 可以在接收方使用缓冲区来接收数据,并且设置一个最大接收长度。当接收到的数据长度小于最大接收长度时,将数据放入缓冲区,并在缓冲区进行数据的拼接。当接收到的数据长度大于等于最大接收长度时,对缓冲区的数据进行解析和处理,并清空缓冲区。 以上是Unity实现Socket通讯时解决TCP粘包和拆包问题的方法。希望对你有帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值