レビュー
「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 が実装されます。