이 글은 주로 C#의 TCP 끈적한 패킷 문제에 대한 해결책을 자세히 소개하며, 관심 있는 친구들은 이를 참고할 수 있습니다.
1 TCP 끈적한 패킷의 원리
1. 이는 송신자가 보낸 여러 데이터 패킷이 수신자가 수신할 때 하나의 패킷에 결합됨을 의미합니다. 수신 버퍼에서 다음 데이터 패킷의 헤드는 이전 데이터 패킷의 테일 바로 뒤에 옵니다. 패킷이 끈적이는 현상에는 여러 가지 이유가 있습니다. 이는 보낸 사람이나 받는 사람에 의해 발생할 수 있습니다.
2. 송신자에 의해 발생하는 끈적한 패킷은 TCP 프로토콜 자체로 인해 발생합니다. TCP는 전송 효율성을 높이기 위해 종종 데이터 패킷을 보내기 전에 송신자에게 충분한 데이터를 수집하도록 요구합니다. 연속해서 여러 번 전송된 데이터가 매우 작은 경우 일반적으로 TCP는 최적화 알고리즘에 따라 데이터를 하나의 패킷으로 결합하여 한 번에 전송하므로 수신자는 고정 패킷 데이터를 수신합니다. 수신측에서 발생하는 끈적끈적한 패킷은 수신측의 사용자 프로세스가 제때에 데이터를 수신하지 못하기 때문에 발생하며, 이로 인해 끈적끈적한 패킷 현상이 발생하게 됩니다.
3. 수신자가 먼저 수신한 데이터를 시스템 수신 버퍼에 넣고, 다음 패킷 시 사용자 프로세스가 이전 데이터 패킷을 가져가지 않은 경우 사용자 프로세스가 버퍼에서 데이터를 가져오기 때문입니다. 데이터가 도착하면 다음 데이터 패킷이 시스템 수신 버퍼에 배치되면 이전 데이터 패킷 이후에 수신되며 사용자 프로세스는 미리 설정된 버퍼 크기에 따라 시스템 수신 버퍼에서 데이터를 가져옵니다. 한 번에 여러 개의 데이터 패킷을 처리합니다. ,
2. 솔루션 원리 및 코드 구현
1. 패킷 헤더(전송 시 동적으로 획득되는 패킷 본문의 길이를 포함하는 고정 길이) + 패킷 본문의 전송 메커니즘을 사용합니다. 그림과 같이
HeaderSize는 패킷 본문의 길이를 저장하며 HeaderSize 자체는 4바이트의 고정 길이입니다.
전체 데이터 패킷(L) = HeaderSize+BodySize
2. 알고리즘
기본 아이디어는 먼저 수신된 처리 대상 데이터 스트림, 즉 시스템 버퍼 데이터(길이가 M으로 설정됨)를 미리 정해진 구조화된 데이터 형식으로 강제 변환하고 구조화된 데이터 길이 필드 L을 꺼내는 것입니다. 그런 다음 패킷 헤더를 기반으로 첫 번째 패킷 데이터 길이를 계산합니다.
M=시스템 버퍼 크기; L=사용자가 전송한 데이터 패킷=HeaderSize+BodySize;
1) Lfd32ab80cfda8e923ebabf1ff5c74cd3M이면 데이터 스트림의 내용이 완전한 구조화된 데이터를 형성하기에 충분하지 않으며 처리하기 전에 다음 데이터 패킷과 병합되어야 함을 의미합니다.
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) { }
그러나 서버 측도 각 데이터 패킷이 생성되는 세션을 구별해야 합니다. 서버 측이 다중 스레드, 다중 사용자 모드이기 때문에 첫 번째 데이터 두 번째 것은 다른 세션의 데이터일 수 있으므로 위의 코드는 단일 세션 모드에서만 작동합니다.
이 문제는 아래에서 해결하겠습니다.
최신 코드
//线程安全的字典 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!