解決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 }