Tcpネットワーク通信の詳しい解説その3(バッファサイズ定義が大きすぎる・小さすぎる問題の解決)

レビュー

「Tcpネットワーク通信の詳細解説」クイックジャンプ
「Tcpネットワーク通信の詳細解説2(サブパッケージングとスティッキーパケットの解決策)」クイックジャンプ

問題が発見されました

「Tcpネットワーク通信の詳細解説2(サブパッケージングとスティッキーパケットの解決)」では、メッセージ本文の前にメッセージ長識別子を追加し、それを先に読み込むことでサブパッケージングとパケットスティッキーの問題を解決しました。メッセージが読み取られるたびに長さの識別子を取得し、受信したメッセージが完全であるかどうかを判断します。不完全なメッセージの場合は、次のメッセージ信号を待ってから処理するため、サブパッケージ化とパケットの固着の問題が解決されます。ただし、Tcp メッセージには一時キャッシュ スペースreadBuff を
使用します。このキャッシュ スペースにはサイズを定義する必要があります。前の記事BUFFER_SIZEで定義したキャッシュ スペースのサイズは 1024 です。次に、問題のある爆弾が仕掛けられます。単一メッセージの送信サイズが BUFFER_SIZE より大きい場合、爆弾はこの時点で爆発します。このメッセージの長さの識別子を読み取ります。この識別子が 2000 の場合、readBuff がキャッシュされたデータは 2000 未満です。この時点では、メッセージが不完全であることをデフォルトとして設定し、次の処理バッチを待ちます。しかし、バッファ領域が使い果たされていることがわかり、決して処理できなくなります。残りの情報を受信すると、ソケット全体がブロックされ、麻痺します。BUFFER_SIZE を非常に大きなサイズに増やすことはできますか? まず最初に、これは可能ですが、ほとんどの短いメッセージはそれほど大きなバッファ スペースを使用しないため、誰もこれを行おうとはしません。 . 大量のメモリの浪費を引き起こします。概要: 一時キャッシュ領域は最大のメモリを必要とします。実際、ほとんどの時間メモリは無駄に消費されており、後でより大きなメッセージの送信が行われると、キャッシュ領域の拡張が必要になる場合があり、結果としてメモリの浪費が大きくなります。


改善する

問題が発見されたので、それを修正する必要があります。問題は readBuff メモリの爆発にあるため、解決策は readBuff から開始することです。readBuff 領域に受信したキャッシュを格納しないようにし、それを特定のメッセージ処理オブジェクトなので、BUFFER_SIZE を 1 に定義し、毎回 1 バイトのメッセージしか受信しない場合でも、輻輳は発生しません。もちろん、1 は大げさであり、何百ものメッセージを受信すると、時代は私たちが望んでいることではありません。ただし、256 または 512 を定義することは可能です。

成し遂げる

次のように SocketClient を実装します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;

namespace S
{
    /// <summary>
    /// Socket客户端
    /// </summary>
    public class SocketClient : ISocketClient
    {
        protected ReceiveDataHandler receiveDataHandler;
        protected Socket socket;
        protected SocketType socketType = SocketType.Stream;
        protected AddressFamily addressFamily = AddressFamily.InterNetwork;
        protected ProtocolType protocolType = ProtocolType.Tcp;
        
        /// <summary>
        /// 接收到的服务器消息队列
        /// </summary>
        protected Queue<byte[]> msgQueue;

        /// <summary>
        /// 开始进行连接
        /// </summary>
        /// <param name="ip">服务器ip地址</param>
        /// <param name="port">服务器端口</param>
        public void BeginConnect(string ip, int port)
        {
            if (IsConnected())
            {
                Debug.LogWarning("当前客户端已经连接服务器,禁止继续连接!");
                return;
            }
            socket = new Socket(addressFamily, socketType, protocolType);
            IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(ip), port);
            socket.BeginConnect(endPoint, OnConnect, socket);
        }

        /// <summary>
        /// 服务器连接成功
        /// </summary>
        /// <param name="ar"></param>
        public virtual void OnConnect(IAsyncResult ar)
        {
            try
            {
                socket.EndConnect(ar);
                msgQueue = new Queue<byte[]>();
                receiveDataHandler = new ReceiveDataHandler(this);
                socket.BeginReceive(receiveDataHandler.Buffer, receiveDataHandler.CurIndex, receiveDataHandler.EmptySize,
                    SocketFlags.None,
                    OnReceive, socket);
            }
            catch (Exception e)
            {
                Debug.LogError($"Client 连接服务器失败,Error:{e}");
            }
        }

        /// <summary>
        /// 接收到服务器消息
        /// </summary>
        /// <param name="ar"></param>
        public virtual void OnReceive(IAsyncResult ar)
        {
            int count = socket.EndReceive(ar);
            receiveDataHandler.OnReceive(count);
            socket.BeginReceive(receiveDataHandler.Buffer, receiveDataHandler.CurIndex,
                receiveDataHandler.EmptySize, SocketFlags.None,
                OnReceive, socket);
        }

        /// <summary>
        /// 向服务器发送字符串
        /// </summary>
        /// <param name="msg">字符串消息</param>
        public virtual void Send(string msg)
        {
            byte[] datas = Encoding.UTF8.GetBytes(msg);
            Send(datas);
        }

        /// <summary>
        /// 向服务器发送字符串(异步)
        /// </summary>
        /// <param name="msg">字符串消息</param>
        /// <param name="callback">发送结束的回调</param>
        public virtual void SendAsync(string msg,AsyncCallback callback)
        {
            byte[] datas = Encoding.UTF8.GetBytes(msg);
            SendAsync(datas,callback);
        }

        /// <summary>
        /// 向服务器发送字节流
        /// </summary>
        /// <param name="datas">字节流消息</param>
        public virtual void Send(byte[] datas)
        {
            if (!IsConnected()) return;
            int dataLen = datas.Length;
            //消息头
            byte[] headDatas = BitConverter.GetBytes(dataLen);
            //组合消息头和消息体的完整消息
            byte[] fullDatas = headDatas.Concat(datas).ToArray();
            socket.Send(fullDatas);
        }

        /// <summary>
        /// 向服务器发送字节流(异步)
        /// </summary>
        /// <param name="datas">字节流消息</param>
        /// <param name="callback">发送结束的回调</param>
        public virtual void SendAsync(byte[] datas,AsyncCallback callback)
        {
            if (!IsConnected()) return;
            int dataLen = datas.Length;
            //消息头
            byte[] headDatas = BitConverter.GetBytes(dataLen);
            //组合消息头和消息体的完整消息
            byte[] fullDatas = headDatas.Concat(datas).ToArray();
            socket.BeginSend(fullDatas, 0, fullDatas.Length, SocketFlags.None, callback, socket);
        }

        /// <summary>
        /// 是否已经连接
        /// </summary>
        /// <returns></returns>
        public bool IsConnected()
        {
            return socket!=null&&socket.Connected;
        }

        /// <summary>
        /// 关闭连接
        /// </summary>
        public void CloseConnect()
        {
            if (!IsConnected()) return;
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
            socket.Dispose();
            socket = null;
            if (receiveDataHandler!=null)
            {
                receiveDataHandler.Dispose();
                receiveDataHandler = null;
            }
        }

        /// <summary>
        /// 压入一个消息
        /// </summary>
        /// <param name="datas">消息字节流</param>
        public void EnqueueMsg(byte[] datas)
        {
            if (msgQueue == null) return;
            msgQueue.Enqueue(datas);
        }

        /// <summary>
        /// 取出一个消息
        /// </summary>
        /// <returns>消息字节流</returns>
        public byte[] DequeueMsg()
        {
            if (msgQueue == null) return null;
            return msgQueue.Dequeue();
        }

        /// <summary>
        /// 未处理的消息数量
        /// </summary>
        /// <returns></returns>
        public int GetMsgCount()
        {
            return msgQueue==null?0:msgQueue.Count;
        }

        /// <summary>
        /// 是否存在未处理的消息
        /// </summary>
        /// <returns></returns>
        public bool HasMsg()
        {
            return GetMsgCount() > 0;
        }
    }
}

キャッシュされたデータはすべて ReceiveDataHandler オブジェクトを通じて処理されることがわかります。そこで、ReceiveDataHandler を実装しましょう。

using System;

namespace S
{
    /// <summary>
    /// 接收到服务器数据的处理器
    /// </summary>
    public class ReceiveDataHandler
    {
        private SocketClient client;

        /// <summary>
        /// 接收数据的缓存区
        /// </summary>
        public byte[] Buffer { get; private set; }

        /// <summary>
        /// 缓存区总大小
        /// </summary>
        public const int TotalSize = 256;

        /// <summary>
        /// 缓存区中待处理数据的起始位置
        /// </summary>
        public int CurIndex { get; private set; }

        /// <summary>
        /// 缓存区中空置的大小
        /// </summary>
        public int EmptySize => TotalSize - CurIndex;

        /// <summary>
        /// 消息体缓存区
        /// </summary>
        private MsgBuffer m_MsgBuffer;

        /// <summary>
        /// 消息头缓存区
        /// </summary>
        private HeadBuffer m_HeadBuffer;

        /// <summary>
        /// 待处理数据大小
        /// </summary>
        private int WaitingSize = 0;


        public ReceiveDataHandler(SocketClient client)
        {
            this.client = client;
            Buffer = new byte[TotalSize];
            m_HeadBuffer = new HeadBuffer(sizeof(Int32));
            m_MsgBuffer = new MsgBuffer(0);
            CurIndex = 0;
            WaitingSize = 0;
        }


        /// <summary>
        /// 接收到服务器数据
        /// </summary>
        /// <param name="Size">数据大小</param>
        public void OnReceive(int Size)
        {
            WaitingSize += Size;
            while (WaitingSize > 0)
            {
                if (!m_HeadBuffer.IsDone)
                {
                    AddSize(m_HeadBuffer.AddBuffer(Buffer, CurIndex, WaitingSize));
                }

                if (m_HeadBuffer.IsDone)
                {
                    if (m_MsgBuffer.IsEmpty) m_MsgBuffer.Reset(m_HeadBuffer.Value);
                    AddSize(m_MsgBuffer.AddBuffer(Buffer, CurIndex, WaitingSize));
                    if (m_MsgBuffer.IsDone) //接收到一条完整的消息
                    {
                        client.EnqueueMsg(m_MsgBuffer.Datas);
                        m_MsgBuffer.Clear();
                        m_HeadBuffer.Clear();
                    }
                }
            }
        }

        /// <summary>
        /// 缓存区当前索引移位
        /// </summary>
        /// <param name="realSize">移位量级</param>
        void AddSize(int realSize)
        {
            CurIndex += realSize;
            WaitingSize -= realSize;
            if (CurIndex == TotalSize) CurIndex = 0;
        }

        /// <summary>
        /// 释放当前对象
        /// </summary>
        public void Dispose()
        {
            client = null;
            Buffer = null;
            CurIndex = 0;
            WaitingSize = 0;
            if (m_HeadBuffer!=null)
            {
                m_HeadBuffer.Dispose();
                m_HeadBuffer = null;
            }

            if (m_MsgBuffer!=null)
            {
                m_MsgBuffer.Dispose();
                m_MsgBuffer = null;
            }
        }
    }
}

Buffer基本クラスを定義します.このオブジェクトは主に完全なメッセージを受信するために使用され、メッセージが完全であるかどうかを判断することができます.そして、Bufferから継承するメッセージヘッダーバッファ領域とメッセージボディバッファ領域をそれぞれ定義します.実装は次のようになります

using System;

namespace S
{
    /// <summary>
    /// 数据缓存区
    /// </summary>
    public class Buffer
    {
        /// <summary>
        /// 缓存区数据
        /// </summary>
        public byte[] Datas { get; protected set; }

        /// <summary>
        /// 缓存区总大小
        /// </summary>
        public int TotalSize { get; private set; }

        /// <summary>
        /// 缓存区已经写入的大小
        /// </summary>
        public int CurSize { get; private set; }

        /// <summary>
        /// 剩余未使用的缓存区大小
        /// </summary>
        public int EmptySize => TotalSize - CurSize;

        /// <summary>
        /// 缓存区是否使用完毕
        /// </summary>
        public bool IsDone => CurSize == TotalSize;

        /// <summary>
        /// 缓存区是不是还未使用
        /// </summary>
        public bool IsEmpty => CurSize == 0;

        /// <summary>
        /// 构造缓存区
        /// </summary>
        /// <param name="TotalSize">缓存区大小</param>
        public Buffer(int TotalSize)
        {
            this.TotalSize = TotalSize;
            CurSize = 0;
            Datas = new byte[TotalSize];
        }

        /// <summary>
        /// 增加缓存数据
        /// </summary>
        /// <param name="datas">缓存数据</param>
        /// <param name="startIndex">缓存数据的起始位置</param>
        /// <param name="len">缓存数据的长度</param>
        /// <returns>返回实际写入数据的大小</returns>
        public virtual int AddBuffer(byte[] datas, int startIndex, int len)
        {
            if (IsDone) return 0;
            len = EmptySize >= len ? len : EmptySize;
            Array.Copy(datas, startIndex, Datas, CurSize, len);
            CurSize += len;
            return len;
        }

        /// <summary>
        /// 清除缓存区数据
        /// </summary>
        public virtual void Clear()
        {
            Datas = null;
            Datas = new byte[TotalSize];
            CurSize = 0;
        }

        /// <summary>
        /// 重新设置缓存区
        /// </summary>
        /// <param name="TotalSize">缓存区大小</param>
        public void Reset(int TotalSize)
        {
            this.TotalSize = TotalSize;
            Clear();
        }
        
        /// <summary>
        /// 释放当前缓存区
        /// </summary>
        public virtual void Dispose()
        {
            Datas = null;
            CurSize = 0;
            TotalSize = 0;
        }
    }
    
    /// <summary>
    /// 消息头缓存区
    /// </summary>
    public class HeadBuffer : Buffer
    {
        public int Value { get; private set; }

        public HeadBuffer(int TotalSize) : base(TotalSize)
        {
        }

        public override int AddBuffer(byte[] datas, int startIndex, int len)
        {
            int realLen = base.AddBuffer(datas, startIndex, len);
            if (IsDone) Value = BitConverter.ToInt32(Datas, 0);
            return realLen;
        }

        public override void Clear()
        {
            base.Clear();
            Value = 0;
        }
    }

    /// <summary>
    /// 消息体缓存区
    /// </summary>
    public class MsgBuffer : Buffer
    {
        public MsgBuffer(int TotalSize) : base(TotalSize)
        {
        }
    }
}

この時点で、新しく改良されたクライアント Tcp が実装されます。

おすすめ

転載: blog.csdn.net/weixin_42498461/article/details/131472448