閒來無事,研究了下socket~ 文章最後會給出實例鏈接,如果不想看介紹的可以直接下下來運行。 本人小白一枚,如有錯誤請看者不奢賜教。
客戶端爲unity,服務端爲vs,都是用c#語言編寫。
先說下我對socket的理解,建立連接、通信、釋放連接。此爲socket通信的三次握手。
對於socket通信,c#底層已經爲我們封裝好了,我們可以選擇使用UDP還是TCP,這裏我使用的是TCP連接。
先介紹下socket通信的基礎 : **建立連接,發送數據,接收數據,釋放連接**。
建立連接 :
// 該類用於阻塞當前線程,並等待信號,接收到繼續進行的信號後,釋放等待線程
private static ManualResetEvent connectDone = new ManualResetEvent (false);
// 設置ip地址與端口號
IPEndPoint remoteEP = new IPEndPoint (IPAddress.Parse (serverIP), port);
// 創建socket
tcpSockt = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 異步創建socket連接
tcpSockt.BeginConnect (remoteEP, new AsyncCallback (connectCallback), tcpSockt);
if (connectDone.WaitOne (timeOutSec, false)) { // 阻塞當前線程, socket連接成功或超時,流程繼續
if (zSocketManager.IsConnected) {
receive (tcpSockt);
return;
} else {
throw new ArgumentException ("連接失敗,請檢查網絡設置");
}
} else {
tcpSockt.Close ();
throw new ArgumentException ("連接失敗,請檢查網絡設置");
}
} catch (Exception e) {
Debug.Log (e.ToString ());
}
發送數據
發送數據時,我們需要與服務定義好一些包頭,包括服務器版本號、協議數據長度、協議號等。將這些包頭數據與協議數據拼接成真正的數據後通過Socket.Send方法發送給服務器。
public void Send (string data, NetCommand _command)
{
byte[] bytesData = Encoding.UTF8.GetBytes (data); // 字符串轉成字節
bytesData = packMsg (bytesData, _command); // 數據添加包頭
if (tcpSockt == null || !tcpSockt.Connected)
throw new ArgumentException ("參數socket爲null,或者未連接到遠程計算機");
else if (bytesData.Length == 0) {
throw new ArgumentException ("參數data爲null ,或者長度爲 0");
} else {
send (bytesData);
}
}
// 拼接包頭和協議數據
byte[] packMsg (byte[] _data, NetCommand _command)
{
byte[] head = new byte[17];
head [0] = 20;
head [1] = 15;
head [2] = 8;
head [3] = 18;
head [4] = 0; // protoVersion
Array.Copy (System.BitConverter.GetBytes (0), 0, head, 5, 4); // serverVersion
byte[] _dataLength = new byte[4]; // 把數據長度添加到 包頭中
_dataLength = System.BitConverter.GetBytes (_data.Length);
Array.Copy (_dataLength, 0, head, 9, 4);
byte[] _commandByte = new byte[4];
_commandByte = System.BitConverter.GetBytes ((int)_command);
Array.Copy (_commandByte, 0, head, 13, 4);
byte[] _result = new byte[head.Length + _data.Length];
Array.Copy (head, 0, _result, 0, head.Length);
Array.Copy (_data, 0, _result, head.Length, _data.Length);
return _result;
}
void send (byte[] _data)
{
int leftLength = _data.Length;
int hasSend = 0;
int flag = 0;
while (true) { // 通過循環將數據全部發送出去
if (tcpSockt.Poll (timeOutSec, SelectMode.SelectWrite)) {
var _sendLen = tcpSockt.Send (_data, hasSend, leftLength, SocketFlags.None);
leftLength -= _sendLen;
hasSend += _sendLen;
if (leftLength == 0) { // 協議數據已全部發送完
flag = 0;
break;
} else {
if (_sendLen > 0)
continue;
else { // 發送數據時出錯
flag = -2;
}
}
} else { // 發送超時
flag = -1;
break;
}
}
if (flag != 0)
Debug.LogError ("send flag :[" + flag + "]");
}
接收數據
接收數據我用的是異步接收。接收數據時會出現粘包和半包問題(我對於粘包的理解是要接收的數據大於緩存數據大小,需要多次接收。而半包就是要接收的數據小於緩存數據大小),主要的解決思路就是設置一個整形變量a,當接收到數據時,判斷a是否爲0,如果爲0,則該數據爲新的協議數據),對其進行拆包,獲取包頭和協議數據。如果a大於0,則爲粘包,需要接收剩餘數據。
// 通過該方法讓socket監聽接收數據
void receive (Socket _client)
{
try {
StateObject state = new StateObject ();
state.CreateBuffer ();
state.workSocket = _client;
// socket開始異步接收數據,當有接收到數據時,會調用receiveCallback方法
_client.BeginReceive (state.Buffer, 0, StateObject.BufferSize, SocketFlags.None, new AsyncCallback (receiveCallback), state);
} catch (Exception e) {
Debug.LogError (e.ToString ());
}
}
void receiveCallback (IAsyncResult _ar)
{
try {
StateObject state = (StateObject)_ar.AsyncState;
Socket client = state.workSocket;
if (!client.Connected) {
Debug.LogError ("服務器斷開連接");
DisConnect ();
return;
}
// 開始接收數據, 本次最多接收 BufferSize 個字節
int bytesRead = client.EndReceive (_ar);
Debug.Log ("已接收 字節數 " + bytesRead);
if (bytesRead > 0) {
if (leftLength > 0) { // 繼續接收剩餘數據
leftLength -= bytesRead;
bytebuffer.Add (state.Buffer);
if (leftLength <= 0) { // 數據接收完畢
receiveData.bytebuffer = bytebuffer.GetBytes();
receiveData.data = getString(receiveData.bytebuffer);
if (receiveDataCallback != null)
receiveDataCallback (receiveData);
}
} else {
receiveData = null;
receiveData = new ReceiveData ();
unpackMsg (state.Buffer,bytesRead, out receiveData.command, out leftLength);
if (leftLength <= 0) { // 數據接收完畢
receiveData.bytebuffer = bytebuffer.GetBytes();
receiveData.data = getString(receiveData.bytebuffer);
if (receiveDataCallback != null)
receiveDataCallback (receiveData);
}
}
}
receive (client);
} catch (Exception e) {
Debug.LogError (e.ToString ());
}
}
// 拆包
void unpackMsg (byte[] _data, int _hasReceived, out NetCommand _command, out int _leftSize)
{
byte[] head = new byte[17]; // 包頭大小爲17
for (int i = 0; i < head.Length; i++) {
head [i] = _data [i];
}
byte[] _dataLengthByte = new byte[4];
for (int i = 0; i < _dataLengthByte.Length; i++) {
_dataLengthByte [i] = head [i + 9];
}
// 獲取協議數據大小
_leftSize = System.BitConverter.ToInt32 (_dataLengthByte, 0);
byte[] _commandByte = new byte[4];
for (int i = 0; i < _commandByte.Length; i++) {
_commandByte [i] = head [i + 13];
}
// 獲取協議號
_command = (NetCommand)System.BitConverter.ToInt32 (_commandByte, 0);
if (_leftSize + 17 > _data.Length)
_leftSize -= _data.Length - 17; // 還有數據沒發完
else
_leftSize = 0; // 數據發送完畢
bytebuffer.Clear ();
bytebuffer.Add (_data, 17, _hasReceived);
}
釋放鏈接
釋放鏈接就比較簡單了,一句代碼的事情,調用 socket.Close()方法就能斷開連接。但是,這裏有一個問題,比如客戶端要斷開連接,做完了斷開連接的處理後,向服務端發送了斷開連接協議,然後調用socket.Close方法斷開了連接,但是服務端並不會接收到斷開連接的協議,因爲socket已經斷開了,也沒辦法做斷開連接的操作,且服務端照樣會發送數據給該客戶端,這就出現了問題。解決的方法是,當客戶端要斷開連接時,先發送斷開連接協議,服務端收到協議後,處理斷開連接的方法,之後回給客戶端斷開連接的協議,客戶端處理斷開連接的方法,之後發送向服務端發送確認斷開協議,服務端收到後,會給客戶端確認斷開協議,並關閉socket,客戶端收到回過來的確認斷開協議後,調用socket.Close方法斷開鏈接。
至此,socket通信的理論知識就講解到這裏。如有錯誤請看者不奢賜教。
工程地址 : 鏈接:http://pan.baidu.com/s/1miNyrCs 密碼:jxm7