[Unity通訊]一個基於socket的3DARPG網路遊戲(一):建立連線和事件分發
阿新 • • 發佈:2019-02-08
一.客戶端
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型),用來記錄訊息的長度,這樣就可以實現分包了。同樣的,伺服器端只接受了一條資訊,也是粘包的體現。