再次之前要說一下TCP和UDP的區別
TCP是可靠傳輸,UDP是不可靠傳輸;
但是TCP有一個缺點就是會粘包,因為TCP是基於數據流的協議,而UDP是基於數據報的協議
一、什么是粘包
發送端可以是一K一K地發送數據,而接收端的應用程序可以兩K兩K地提走數據,當然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個整體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。而UDP是面向消息的協議,每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不同的。怎樣定義消息呢?可以認為對方一次性write/send的數據為一個消息,需要明白的是當對方send一條信息的時候,無論底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成后才呈現在內核緩沖區。
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。
- TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
- UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
- tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭。
兩種情況下會發生粘包:
①發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會合到一起,產生粘包)
②接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
拆包的發生情況
當發送端緩沖區的長度大於網卡的MTU時,tcp會將這次發送的數據拆成幾個數據包發送出去。
補充問題一:為何tcp是可靠傳輸,udp是不可靠傳輸
tcp在數據傳輸時,發送端先把數據發送到自己的緩存中,然后協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則重新發送數據,所以tcp是可靠的(也就是存在三次握手確定消息已經成功發送到對端)
而udp發送數據,對端是不會返回確認信息的,因此不可靠(而udp沒有確認機制)
補充問題二:send(字節流)和recv(1024)及sendall
recv里指定的1024意思是從緩存里一次拿出1024個字節的數據
send的字節流是先放入己端緩存,然后由協議控制將緩存內容發往對端,如果待發送的字節流大小大於緩存剩余空間,那么數據丟失,用sendall就會循環調用send,數據不會丟失
二、解決粘包問題的方法
粘包問題的關鍵在於:
接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然后接收端來一個死循環接收完所有數據
解決方法:
我們可以把報頭做成字典,字典里包含將要發送的真實數據的詳細信息,然后json序列化,然后用struck將序列化后的數據長度打包成4個字節(4個自己足夠用了)
發送時:
先發報頭長度
再編碼報頭內容然后發送
最后發真實內容
接收時:
先手報頭長度,用struct取出來
根據取出的長度收取報頭內容,然后解碼,反序列化
從反序列化的結果中取出待取數據的詳細信息,然后去取真實的數據內容
正是由於TCP的粘包問題,所以在針對我們的項目中,我選擇了使用UDP socket的通信方法
這個方法中主要就是兩個方法,一個是消息的發送,一個是消息的接收
項目是基於Unity的所以語言選擇了C#
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace WindowsFormsApp2
{
/// <summary>
/// 發送接收消息幫助類
/// </summary>
public class UDP_CLIENT_HELPER
{
static Socket client;
private string _ip_server = string.Empty;
private int _port_server = 6001;
private string _ip_client = string.Empty;
private int _port_client = 6000;
public UDP_CLIENT_HELPER(string _Ip_server, int _Port_server, string _Ip_client, int _Port_client)
{
this._ip_server = _Ip_server;
this._port_server = _Port_server;
this._ip_client = _Ip_client;
this._port_client = _Port_client;
if (client == null)
{
client = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
client.Bind(new IPEndPoint(IPAddress.Parse(_ip_client), _port_client));
}
}
/// <summary>
/// 發送消息
/// </summary>
/// <param name="message"></param>
public void sendMsg(string message)
{
//綁定服務器端口和ip並發送
EndPoint point = new IPEndPoint(IPAddress.Parse(_ip_server), _port_server);
client.SendTo(Encoding.UTF8.GetBytes(message), point);
}
/// <summary>
/// 接收消息
/// </summary>
public void ReciveMsg()
{
while (true)
{
//循環接收服務器發來的消息 並返回JObject對象
EndPoint point = new IPEndPoint(IPAddress.Any, 0);
byte[] buffer = new byte[1024];
int length = client.ReceiveFrom(buffer, ref point);//接收數據報
string message = Encoding.UTF8.GetString(buffer, 0, length);
Newtonsoft.Json.Linq.JObject obj = (Newtonsoft.Json.Linq.JObject)JsonConvert.DeserializeObject(message);
string type = obj["message"]["type"].ToString();
}
}
}
}