この記事では主に C# における TCP スティッキー パケットの問題の解決策を詳しく紹介します。興味のある方は参考にしてください。
1 TCP スティッキー パケットの原理。送信者が送信した複数のデータ パケットが、受信者が受信バッファから受信したときに 1 つのパケットにまとめられ、次のデータ パケットの先頭が前のデータ パケットの末尾の直後に来ることを意味します。スティッキー パケットの現象には、送信者または受信者が原因である可能性があります。
2. 送信者によって発生するスティッキー パケットは、TCP プロトコル自体によって発生します。TCP の送信効率を向上させるために、送信者はデータ パケットを送信する前に十分なデータを収集する必要があります。連続して数回送信されるデータが非常に小さい場合、通常、TCP は最適化アルゴリズムに従ってデータを 1 つのパケットに結合して一度に送信し、受信側がスティッキー パケット データを受信できるようにします。受信側によってスティッキー パケットが発生するのは、受信側のユーザー プロセスがデータを時間内に受信しないことが原因で、スティッキー パケット現象が発生します。 3. これは、受信側が最初に受信したデータをシステムの受信バッファーに置き、次のパケットがユーザー プロセスによって前のデータ パケットが取り除かれていない場合、ユーザー プロセスがバッファーからデータを取得するためです。データの次のパケットがシステム受信バッファに配置されると、前のデータ パケットの後に受信され、ユーザー プロセスは事前に設定されたバッファ サイズに従ってシステム受信バッファからデータをフェッチし、次のデータ パケットを取得します。一度に複数のデータパケット。 ,2. 解決原理とコード実装
1. パケットヘッダー(送信時に動的に取得される、パケットボディの長さを含む固定長)+パケットボディの送信メカニズムを使用します。図に示すように、
HeaderSize にはパケット本体の長さが格納され、HeaderSize 自体は 4 バイトの固定長です。完全なデータ パケット (L) = HeaderSize+BodySize;アルゴリズム
基本的な考え方は、まず処理対象となる受信データストリーム、つまりシステムバッファデータ(長さをMとする)を所定の構造データ形式に強制変換し、その後構造データ長を取り出すことである。そこからフィールド L を抽出し、パケット ヘッダーに基づいて最初のパケットのデータ長を計算します。
M=システムバッファサイズ; L=ユーザーによって送信されたデータパケット=HeaderSize+BodySize;
1) L 2) L=M の場合、データ ストリームのコンテンツがたまたまデータの完全な構造である (つまり、ユーザー定義のバッファーがシステムの受信バッファー サイズと等しい) ことを意味します。一時バッファに直接保存できます。
3) L>M の場合、データ ストリームのコンテンツが完全な構造化データを形成するには十分ではなく、処理する前に次のデータ パケットとマージする必要があることを意味します。
4) コードの実装は次のとおりです (データの受信には HP-SOCKET フレームワークのサーバー側が使用されます) int headSize = 4;//包头长度 固定4
byte[] surplusBuffer = null;//不完整的数据包,即用户自定义缓冲区
/// <summary>
/// 接收客户端发来的数据
/// </summary>
/// <param name="connId">每个客户的会话ID</param>
/// <param name="bytes">缓冲区数据</param>
/// <returns></returns>
private HandleResult OnReceive(IntPtr connId, byte[] bytes)
{
//bytes 为系统缓冲区数据
//bytesRead为系统缓冲区长度
int bytesRead = bytes.Length;
if (bytesRead > 0)
{
if (surplusBuffer == null)//判断是不是第一次接收,为空说是第一次
surplusBuffer = bytes;//把系统缓冲区数据放在自定义缓冲区里面
else
surplusBuffer = surplusBuffer.Concat(bytes).ToArray();//拼接上一次剩余的包
//已经完成读取每个数据包长度
int haveRead = 0;
//这里totalLen的长度有可能大于缓冲区大小的(因为 这里的surplusBuffer 是系统缓冲区+不完整的数据包)
int totalLen = surplusBuffer.Length;
while (haveRead <= totalLen)
{
//如果在N此拆解后剩余的数据包连一个包头的长度都不够
//说明是上次读取N个完整数据包后,剩下的最后一个非完整的数据包
if (totalLen - haveRead < headSize)
{
byte[] byteSub = new byte[totalLen - haveRead];
//把剩下不够一个完整的数据包存起来
Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
surplusBuffer = byteSub;
totalLen = 0;
break;
}
//如果够了一个完整包,则读取包头的数据
byte[] headByte = new byte[headSize];
Buffer.BlockCopy(surplusBuffer, haveRead, headByte, 0, headSize);//从缓冲区里读取包头的字节
int bodySize = BitConverter.ToInt32(headByte, 0);//从包头里面分析出包体的长度
//这里的 haveRead=等于N个数据包的长度 从0开始;0,1,2,3....N
//如果自定义缓冲区拆解N个包后的长度 大于 总长度,说最后一段数据不够一个完整的包了,拆出来保存
if (haveRead + headSize + bodySize > totalLen)
{
byte[] byteSub = new byte[totalLen - haveRead];
Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
surplusBuffer = byteSub;
break;
}
else
{
//挨个分解每个包,解析成实际文字
String strc = Encoding.UTF8.GetString(surplusBuffer, haveRead + headSize, bodySize);
//AddMsg(string.Format(" > [OnReceive] -> {0}", strc));
//依次累加当前的数据包的长度
haveRead = haveRead + headSize + bodySize;
if (headSize + bodySize == bytesRead)//如果当前接收的数据包长度正好等于缓冲区长度,则待拼接的不规则数据长度归0
{
surplusBuffer = null;//设置空 回到原始状态
totalLen = 0;//清0
}
}
}
}
return HandleResult.Ok;
}
各接続の IntPtr connId セッション ID を注意深く確認してください
private HandleResult OnReceive(IntPtr connId, byte[] bytes) { }
ただし、サーバー側はマルチスレッド、マルチユーザー モードであるため、最初のデータが生成される各データ パケットがどのセッションであるかを区別する必要もあります。 2 番目のパケットは別のセッションからのデータである可能性があるため、上記のコードはシングル セッション モードでのみ機能します。
以下でこの問題を解決していきます。
c#
safeCon
currentDictionaryを使用して、
最新のコード//线程安全的字典
ConcurrentDictionary<IntPtr, byte[]> dic = new ConcurrentDictionary<IntPtr, byte[]>();
int headSize = 4;//包头长度 固定4
/// <summary>
/// 接收客户端发来的数据
/// </summary>
/// <param name="connId">每个客户的会话ID</param>
/// <param name="bytes">缓冲区数据</param>
/// <returns></returns>
private HandleResult OnReceive(IntPtr connId, byte[] bytes)
{
//bytes 为系统缓冲区数据
//bytesRead为系统缓冲区长度
int bytesRead = bytes.Length;
if (bytesRead > 0)
{
byte[] surplusBuffer = null;
if (dic.TryGetValue(connId, out surplusBuffer))
{
byte[] curBuffer = surplusBuffer.Concat(bytes).ToArray();//拼接上一次剩余的包
//更新会话ID 的最新字节
dic.TryUpdate(connId, curBuffer, surplusBuffer);
surplusBuffer = curBuffer;//同步
}
else
{
//添加会话ID的bytes
dic.TryAdd(connId, bytes);
surplusBuffer = bytes;//同步
}
//已经完成读取每个数据包长度
int haveRead = 0;
//这里totalLen的长度有可能大于缓冲区大小的(因为 这里的surplusBuffer 是系统缓冲区+不完整的数据包)
int totalLen = surplusBuffer.Length;
while (haveRead <= totalLen)
{
//如果在N此拆解后剩余的数据包连一个包头的长度都不够
//说明是上次读取N个完整数据包后,剩下的最后一个非完整的数据包
if (totalLen - haveRead < headSize)
{
byte[] byteSub = new byte[totalLen - haveRead];
//把剩下不够一个完整的数据包存起来
Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
dic.TryUpdate(connId, byteSub, surplusBuffer);
surplusBuffer = byteSub;
totalLen = 0;
break;
}
//如果够了一个完整包,则读取包头的数据
byte[] headByte = new byte[headSize];
Buffer.BlockCopy(surplusBuffer, haveRead, headByte, 0, headSize);//从缓冲区里读取包头的字节
int bodySize = BitConverter.ToInt32(headByte, 0);//从包头里面分析出包体的长度
//这里的 haveRead=等于N个数据包的长度 从0开始;0,1,2,3....N
//如果自定义缓冲区拆解N个包后的长度 大于 总长度,说最后一段数据不够一个完整的包了,拆出来保存
if (haveRead + headSize + bodySize > totalLen)
{
byte[] byteSub = new byte[totalLen - haveRead];
Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
dic.TryUpdate(connId, byteSub, surplusBuffer);
surplusBuffer = byteSub;
break;
}
else
{
//挨个分解每个包,解析成实际文字
String strc = Encoding.UTF8.GetString(surplusBuffer, haveRead + headSize, bodySize);
AddMsg(string.Format(" > {0}[OnReceive] -> {1}", connId, strc));
//依次累加当前的数据包的长度
haveRead = haveRead + headSize + bodySize;
if (headSize + bodySize == bytesRead)//如果当前接收的数据包长度正好等于缓冲区长度,则待拼接的不规则数据长度归0
{
byte[] xbtye=null;
dic.TryRemove(connId, out xbtye);
surplusBuffer = null;//设置空 回到原始状态
totalLen = 0;//清0
}
}
}
}
return HandleResult.Ok;
}
以上がC# での TCP スティッキー パケットの問題の解決例の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。