この記事では主に 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;
}
これにより、複数のクライアントセッションによって引き起こされる受信の混乱が解決されます。この時点ですべての作業は完了です。上記のコードは、本当に面倒な作業をしたくない場合の参照と学習用です。 HP-SOCKET 通信フレームワークの PACK モデル を直接使用でき、スティッキー パケットの問題を自動的に解決します。
以上がC# での TCP スティッキー パケットの問題の解決例の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。