1. 程式人生 > >[Unity通訊]一個基於socket的3DARPG網路遊戲(一):建立連線和事件分發

[Unity通訊]一個基於socket的3DARPG網路遊戲(一):建立連線和事件分發

一.客戶端

1.定義一個訊息體,伺服器和客戶端通訊的時候,傳輸的就是這樣的資訊。

using System.Collections;
using System.Text;

public class SocketMessage {

    //大模組,例如登入註冊模組,角色模組(行走、釋放技能),購買模組
    public int ModuleType { get; set; }
    //進一步分類,例如登入註冊模組中含有登入和註冊兩種型別
    public int MessageType { get; set; }
    //SocketMessage的核心,包含各種內容
    public string Message { get; set; }
    //資訊的總位元組數
    public int Length { get; set; }

    public SocketMessage(int moduleType, int messageType, string message)
    {
        ModuleType = moduleType;
        MessageType = messageType;
        Message = message;
        //Length的位元組數,ModuleType的位元組數,MessageType的位元組數,系統自動新增的儲存字串長度的位元組,Message的位元組數
        Length = 4 + 4 + 4 + 1 + Encoding.UTF8.GetBytes(message).Length;
    }

}

2.因為傳輸的是二進位制資訊,所以要有一個類專門來讀取和寫入二進位制
using System.Collections;
using System.IO;
using System;
using System.Text;

//對SocketMessage的讀寫
public class ByteArray {

    //為了節省傳輸的流量,所以傳輸的是二進位制
    //讀與寫操作都是對一個流來進行的,這裡使用MemoryStream
    private MemoryStream memoryStream;
    private BinaryReader binaryReader;
    private BinaryWriter binaryWriter;

    private int readIndex = 0;
    private int writeIndex = 0;

    public ByteArray()
    {
        memoryStream = new MemoryStream();
        binaryReader = new BinaryReader(memoryStream);
        binaryWriter = new BinaryWriter(memoryStream);
    }

    public void Destroy()
    {
        binaryReader.Close();
        binaryWriter.Close();
        memoryStream.Close();
        memoryStream.Dispose();
    }

    public int GetReadIndex()
    {
        return readIndex;
    }

    public int GetLength()
    {
        return (int)memoryStream.Length;
    }

    public int GetPosition()
    {
        //position是從0開始的
        return (int)memoryStream.Position;
    }

    public byte[] GetByteArray()
    {
        return memoryStream.ToArray();
    }

    public void Seek(int offset, SeekOrigin seekOrigin)
    {
        //offset:相對於 SeekOrigin 所指定的位置的偏移量引數
        memoryStream.Seek(offset, seekOrigin);
    }


    #region read
    public bool ReadBoolean()
    {
        Seek(readIndex, SeekOrigin.Begin);
        bool a = binaryReader.ReadBoolean();
        readIndex += 1;
        return a;
    }

    public short ReadInt16()
    {
        Seek(readIndex, SeekOrigin.Begin);
        short a = binaryReader.ReadInt16();
        readIndex += 2;
        return a;
    }

    public int ReadInt32()
    {
        Seek(readIndex, SeekOrigin.Begin);
        int a = binaryReader.ReadInt32();
        readIndex += 4;
        return a;
    }

    public float ReadSingle()
    {
        Seek(readIndex, SeekOrigin.Begin);
        float a = binaryReader.ReadSingle();
        readIndex += 4;
        return a;
    }

    public double ReadDouble()
    {
        Seek(readIndex, SeekOrigin.Begin);
        double a = binaryReader.ReadDouble();
        readIndex += 8;
        return a;
    }

    public string ReadString()
    {
        Seek(readIndex, SeekOrigin.Begin);
        string a = binaryReader.ReadString();
        //因為binaryWriter寫字串時會在字串前面加一位元組,儲存字串的長度
        readIndex += Encoding.UTF8.GetBytes(a).Length + 1;
        return a;
    }
    #endregion

    #region write
    public void Write(bool value)
    {
        Seek(writeIndex, SeekOrigin.Begin);
        binaryWriter.Write(value);
        writeIndex += 1;
    }

    public void Write(short value)
    {
        Seek(writeIndex, SeekOrigin.Begin);
        binaryWriter.Write(value);
        writeIndex += 2;
    }

    public void Write(int value)
    {
        Seek(writeIndex, SeekOrigin.Begin);
        binaryWriter.Write(value);
        writeIndex += 4;
    }

    public void Write(float value)
    {
        Seek(writeIndex, SeekOrigin.Begin);
        binaryWriter.Write(value);
        writeIndex += 4;
    }

    public void Write(double value)
    {
        Seek(writeIndex, SeekOrigin.Begin);
        binaryWriter.Write(value);
        writeIndex += 8;
    }

    public void Write(string value)
    {
        Seek(writeIndex, SeekOrigin.Begin);
        binaryWriter.Write(value);
        //因為binaryWriter寫字串時會在字串前面加一位元組,儲存字串的長度
        writeIndex += Encoding.UTF8.GetBytes(value).Length + 1;
    }

    public void Write(byte[] value)
    {
        Seek(writeIndex, SeekOrigin.Begin);
        binaryWriter.Write(value);
        writeIndex += value.Length;
    }
    #endregion
}

3.定義socket客戶端,用來連線伺服器,並收發資訊
using System.Collections;
using System.Net.Sockets;
using System.Net;
using System;
using System.Text;
using System.Threading;
using UnityEngine;

public class SocketClient {

    private Socket socket;//當前套接字
    private ByteArray byteArray = new ByteArray();//位元組陣列快取
    private Thread handleMessage;//處理訊息的執行緒

    public SocketClient()
    {
        handleMessage = new Thread(HandleMessage);
        handleMessage.Start();
    }

    public SocketClient(Socket socket)
    {
        this.socket = socket;
        handleMessage = new Thread(HandleMessage);
        handleMessage.Start();
    }

    public Socket GetSocket()
    {
        return socket;
    }

    public void Destroy()
    {
        handleMessage.Abort();
        socket.Close();
        byteArray.Destroy();
    }

    /// <summary>
    /// 非同步連線伺服器
    /// </summary>
    public void AsynConnect()
    {
        IPEndPoint serverIp = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        socket.BeginConnect(serverIp, asyncResult =>
        {
            socket.EndConnect(asyncResult);
            Debug.Log("connect success!");
            
            AsynRecive();
            AsynSend(new SocketMessage(19, 89, "你好,伺服器"));
            AsynSend(new SocketMessage(19, 89, "你好,伺服器"));
            AsynSend(new SocketMessage(19, 89, "你好,伺服器"));
        }, null);
    }

    /// <summary>
    /// 非同步接受資訊
    /// </summary>
    public void AsynRecive()
    {
        byte[] data = new byte[1024];
        socket.BeginReceive(data, 0, data.Length, SocketFlags.None,
        asyncResult =>
        {
            int length = socket.EndReceive(asyncResult);
            byte[] temp = new byte[length];
            Debug.Log("接受到的位元組數為" + length);
            Array.Copy(data, 0, temp, 0, length);

            byteArray.Write(temp);

            AsynRecive();
        }, null);
    }

    /// <summary>
    /// 非同步傳送資訊
    /// </summary>
    public void AsynSend(SocketMessage sm)
    {
        ByteArray ba = new ByteArray();
        ba.Write(sm.Length);
        ba.Write(sm.ModuleType);
        ba.Write(sm.MessageType);
        ba.Write(sm.Message);

        byte[] data = ba.GetByteArray();
        ba.Destroy();
        socket.BeginSend(data, 0, data.Length, SocketFlags.None, asyncResult =>
        {
            int length = socket.EndSend(asyncResult);
        }, null);
    }

    /// <summary>
    /// 解析資訊
    /// </summary>
    public void HandleMessage()
    {
        int tempLength = 0;//用來暫存資訊的長度
        bool hasGetMessageLength = false;//是否得到了訊息的長度

        while (true)
        {
            if (!hasGetMessageLength)
            {
                if (byteArray.GetLength() - byteArray.GetReadIndex() > 4)//訊息的長度為int,佔四個位元組
                {
                    tempLength = byteArray.ReadInt32();//讀取訊息的長度
                    hasGetMessageLength = true;
                }
            }
            else
            {
                //根據長度就可以判斷訊息是否完整
                //GetReadIndex()可以得到已讀的位元組
                //注意上面的ReadInt32讀取後,讀的索引會加上4,要把多餘的減去
                if ((tempLength + byteArray.GetReadIndex() - 4) <= byteArray.GetLength())
                {
                    SocketMessage sm = new SocketMessage(byteArray.ReadInt32(), byteArray.ReadInt32(), byteArray.ReadString());
                    //SocketServer.HandleMessage(this, sm);
                    SocketSingletion.Instance.Send(sm);
                    hasGetMessageLength = false;
                }
            }
        }
    }

}

4.定義一個單例基類
using UnityEngine;
using System.Collections;

public class MonoSingletion<T> : MonoBehaviour {

    private static T instance;
    public static T Instance
    {
        get
        {
            return instance;
        }
    }

    void Awake()
    {
        instance = GetComponent<T>();
    }
}

5.定義一個類,管理socke客戶端的生命週期,並提供事件介面供其他類使用
using UnityEngine;
using System.Collections;

public class SocketSingletion : MonoSingletion<SocketSingletion> {

    public SocketClient socketClient;
    public delegate void SendDelegate(SocketMessage sm);
    public event SendDelegate sendEvent = null;

	// Use this for initialization
	void Start () 
    {
        socketClient = new SocketClient();
        socketClient.AsynConnect();
	}
	
	// Update is called once per frame
	void Update () {
	
	}

    public void Send(SocketMessage sm)
    {
        sendEvent(sm);
    }

    void OnDestroy()
    {
        print("Destroy socketClient");
        socketClient.Destroy();
    }
}


二.伺服器端

0.這裡為了方便直接用c#寫的伺服器端,開啟vs,直接把ByteArray、SocketClient、SocketMessage這三個類複製過來

1.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;

public class SocketServer {

    private Socket socket;//當前套接字
    public Dictionary<string, SocketClient> dictionary = new Dictionary<string, SocketClient>();//string為ip地址

    public void Listen()
    {
        IPEndPoint serverIp = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        socket.Bind(serverIp);
        socket.Listen(100);
        Console.WriteLine("server ready.");
        AsynAccept(socket);
    }

    /// <summary>
    /// 非同步連線客戶端
    /// </summary>
    public void AsynAccept(Socket serverSocket)
    {
        serverSocket.BeginAccept(asyncResult =>
        {
            Socket client = serverSocket.EndAccept(asyncResult);
            SocketClient socketClient = new SocketClient(client);
            
            string s = socketClient.GetSocket().RemoteEndPoint.ToString();
            Console.WriteLine("連線的客戶端為: " + s);
            dictionary.Add(s, socketClient);

            socketClient.AsynRecive();
            socketClient.AsynSend(new SocketMessage(20, 15, "你好,客戶端"));
            socketClient.AsynSend(new SocketMessage(20, 15, "你好,客戶端"));
            socketClient.AsynSend(new SocketMessage(20, 15, "你好,客戶端"));
            AsynAccept(serverSocket);
        }, null);
    }

    /// <summary>
    /// 解析資訊
    /// </summary>
    public static void HandleMessage(SocketClient sc, SocketMessage sm)
    {
        Console.WriteLine(sc.GetSocket().RemoteEndPoint.ToString() + "   " + 
            sm.Length + "   " + sm.ModuleType + "   " + sm.MessageType + "   " + sm.Message);
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication6
{
    class Program
    {
        static void Main(string[] args)
        {
            SocketServer socketServer = new SocketServer();
            socketServer.Listen();
            Console.ReadKey();
        }
    }
}


三.測試

1.在unity中新建一個測試類

using UnityEngine;
using System.Collections;

public class ReceiveSocketMessage : MonoBehaviour {

	// Use this for initialization
	void Start () 
    {
        SocketSingletion.Instance.sendEvent += PrintInfo;
	}
	
	// Update is called once per frame
	void Update () 
    {
	
	}

    public void PrintInfo(SocketMessage sm)
    {
        print("   " + sm.Length + "   " + 
            sm.ModuleType + "   " + sm.MessageType + "   " + sm.Message);
    }
}

2.執行程式

分析:伺服器端向客戶端傳送了三條資訊,而本人使用了兩個gameobject來訂閱接收的事件,所以列印了6條資訊。同時,在客戶端中只接受到兩條資訊,一條位元組數為62,另一條位元組數為31,說明出現了粘包問題了,這裡本人使用的是為每條傳遞的訊息體前加了4個位元組(int型),用來記錄訊息的長度,這樣就可以實現分包了。同樣的,伺服器端只接受了一條資訊,也是粘包的體現。