socket通信實例

閒來無事,研究了下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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章