解决Socket粘包问题——C#代码
前天晚上,曾经的一个同事问我socket发送消息如果太频繁接收方就会有消息重叠,因为当时在外面,没有多加思考 第一反应还以为是多线程导致的数据不同步导致的,让他加个线程锁搞定。后来回到家慢慢思考感觉这个和加锁没啥关系,如果是多线程导致的,消息只会被覆盖呀。后来就上网搜索socket 消息重叠,后来了解到这属于socket粘包。
简单以自己的理解介绍下Socket粘包。
Socket粘包简单说就是:Socket发送方 发送消息很频繁导致接收方接收到的消息是之前的两个或者多个消息连在一起。至于详细介绍可以百度相关资料了解。
Socket粘包一般分为两种:
1、Socket发送方:Socket发送消息时会将消息存放在一个缓冲区里,等待缓冲区满或者等待时间已到则发送出去。
2、Socket接收方:接收方一般是因为没有及时从Socket缓冲区里取数据导致后来数据累加到一起才获取导致。
在此只介绍发送方导致的
发送方处理方法一般是两种
1、NoDelay=true,NoDelay是否正在使用 Nagle 算法,这是socket提供的一个方法,我试了单独使用感觉效果不大,还有就是在发送后加延时,这样就不会造成粘包。
2、分包方法 即发送消息的头4个字节加上消息长度,接受方接收到数据后先解析长度然后根据消息长度来获取消息。每次接收数据有个总长度 在总长度里循环遍历消息即可。
直接上代码。
分包方法发送端主要代码
1 //测试要发送的消息 2 string[] sendData = new string[] { "00", "11", "2222", "333333", "44444444", "我爱中国 中国爱我 哈哈哈 中国", "大中华你china"}; 3 int userKey = listConnect.SelectedIndex; 4 if (userKey < 0) 5 { 6 MessageBox.Show("请选择要发送者!"); 7 } 8 9 else 10 { 11 //为了测试循环发送消息 12 for (int i = 0; i < 7; i++) 13 { 14 15 //获取消息内容 16 byte[] msg = Encoding.UTF8.GetBytes(sendData[i]); 17 18 byte[] array = new byte[msg.Length + 1 + 4]; 19 20 //第一个字节代表 消息类型 1:文字消息 2:文件 21 array[0] = 1; 22 //将消息长度写入4个字节 从第二个2个字节开始写入 23 ConvertIntToByteArray(msg.Length, ref array); 24 27 //字节拷贝 28 Buffer.BlockCopy(msg, 0, array, 5, msg.Length); 29 //socket发送 dic存储的当前连接的socket列表 30 dic[userKey].Send(array); 31 32 33 } 34 }
接收端消息部分代码
1 //循环监听接收消息 2 while (flag) 3 { 4 byte[] bytes = new byte[1024 * 1024 * 2]; 5 6 //消息总长度 7 int dataLength = 0; 8 dataLength = ServerSocket.receive(out bytes); 9 10 //判断消息类型 11 if (bytes[0] == 1) 12 { 13 14 //解决粘包问题代码 15 16 int msgBeginIndex = -1; 17 int msgEndIndex = -1; 18 int msgLength = 0; 19 int n = 0; 20 21 /**以下是接收数据包在byte[]中的内容 22 * 23 0=1 代表消息类型 24 1=2 消息长度(消息长度占4个字节) 25 2=0 .. 26 3=0 .. 27 4=0 .. 28 5=48 消息内容 29 6=48 .. 30 以下内容和前面结构一样,粘包导致两个消息粘在一起 31 7=1 32 8=2 33 9=0 34 10=0 35 11=0 36 12=49 37 13=49 38 ... 39 ******/ 40 while (n < dataLength) 41 { 42 //将消息长度4个字节单独提取 43 byte[] msgLengthByte = new byte[4]; 44 45 for (int i = 0; i < 4; i++) 46 { 47 //msgEndIndex:消息结束所在索引 48 //第一个次消息长度可能是2; 49 //第二个长度可能是8了索引这里要使用msgEndIndex; 50 //最后+2+i:每个消息记录的长度都是从本次消息的第二个字节开始 直到第四个字节 51 52 //之所以msgEndIndex默认=-1 消息长度所在位置在前一个消息结束后的第二个字节开始 ; 53 //-1+2=1 这样保证获取第一个消息所在长度,也能保证后面的消息长度所在位置 54 msgLengthByte[i] = bytes[msgEndIndex + 2 + i]; 55 } 56 //将消息长度转成int 57 msgLength = BitConverter.ToInt32(msgLengthByte, 0); 58 59 //消息开始索引=消息结束索引+6(前面5个字节不是消息内容消息内容都是从第6个字节开始;+6是因为msgEndIndex索引是从-1开始) 60 msgBeginIndex = msgEndIndex + 6; 61 62 //消息结束索引位置=游标索引开始位置 +消息长度-1 因为消息开始索引本身就是消息开始 索引消息结束必须减除开始索引 63 msgEndIndex = msgBeginIndex + msgLength - 1; 64 65 //获取消息内容 66 string receive = Encoding.UTF8.GetString(bytes, msgBeginIndex, msgLength); 67 if (receive != null) 68 { 69 txtLog.BeginInvoke(new Action(() => 70 { 71 72 txtLog.AppendText(DateTime.Now.ToString("HH:mm:ss") + " " + receive + " \r\n"); 73 })); 74 } 75 76 77 78 n = msgEndIndex + 1; 79 } 80 } 81 82 83 //文件接收 84 else if (bytes[0] == 2) 85 { 86 } 87 }