需求
首先要意识到网络通信面对的是一个怎么样的情景:
- 服务器会连任意个客户端,任意时刻可能有客户端连入连出;
- 服务端和客户端可能任意时刻给对方发消息,所以双方都要一直准备好接收。但是两端还有别的事要做,通信不能阻塞主线程;
- 发的只能是字节数组,发时要把数据类序列化,接收时反序列化,序列化反序列化程序是每个数据类专门写一个还是写一个通用的?通用的怎么写?
- 发的只能是字节数组,但是发的数据类有多种,需要一个数据类型头标记这是哪个数据类。(如果是二进制序列化用于保存文件,就可以通过文件路径知道对应的数据类,无需这个标记ID,但网络通信是一条信道传递多种数据类);
- 定义通信的需求,什么情况下需要发数据?发什么数据类?对方回复什么?相当于自定义一套协议。不过没设计好也可以先写纯收发字节数组的模块;
可以把通信部分分成两个模块:序列化反序列化模块、通信模块。前者负责:
- 在数据类和字节数组之间转换;
- 发送时在数据类的字节数组前加上标记数据类类型的ID;
- 接收时根据头的ID判断数据类类型,然后反序列化成数据类;
通信模块只管接收字节数组,发送给另一端,和接收另一端发来的字节数组。
通信模块
- 服务端会连很多客户端,为了存连接的所有客户端可以用一个字典;
- 服务端要知道客户端断开了连接,可以通过判断socket.Receive(buffer)返回的数是否为0.没收到消息时这个函数会阻塞线程,不返回,若返回0则说明客户端断开了连接。需要紧接着执行socket.Shutdown()和socket.Close();
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Mercenaria;
using UnityEngine;public class MyNetManager : MonoSingletonDontDestroy<MyNetManager>
{Socket socket;Queue<byte[]> queueSend = new Queue<byte[]>();Queue<byte[]> queueReceive = new Queue<byte[]>();Thread threadSend, threadReceive;byte[] buffer = new byte[1024 * 20];int lenReveive;bool on;void Start(){Connect("127.0.0.1", 8080);}void Update(){if (queueReceive.Count > 0){Debug.Log(queueReceive.Dequeue());}}public void Connect(string ip, int port){if (on){return;}if (socket == null){socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);}IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);try{socket.Connect(iPEndPoint);on = true;ThreadPool.QueueUserWorkItem(SendMessage);ThreadPool.QueueUserWorkItem(ReceiveMessage);}catch (SocketException ex){if (ex.ErrorCode == 10061){
#if UNITY_EDITORDebug.Log("服务器拒绝连接");
#endif}else{
#if UNITY_EDITORDebug.Log("连接失败" + ex.Message);
#endif}}}public void Send(byte[] message){byte[] bufferSend = new byte[message.Length];Array.Copy(message,bufferSend,message.Length);//深拷贝queueSend.Enqueue(bufferSend);}void SendMessage(object obj){while (on){if (queueSend.Count > 0){socket.Send(queueSend.Dequeue());}}}void ReceiveMessage(object obj){while (on){if (socket.Available > 0){lenReveive = socket.Receive(buffer);byte[] data=new byte[buffer.Length];Array.Copy(buffer,data,buffer.Length);queueReceive.Enqueue(data);}}}public void Close(){if (socket != null){socket.Shutdown(SocketShutdown.Both);socket.Close();on = false;threadSend = null;threadReceive = null;}}void OnDestroy(){Close();}
}
序列化反序列化模块
如果暂时没有实战项目,纯为了学习,该怎么写数据类序列化反序列化的部分?
是否要写一个能序列化任意数据类的程序?
考虑到还要加上分辨数据类的ID头,这个ID头和数据类的对应关系是人为规定的。序列化之后就是一个字节数组,已经无法知道是什么数据类了,所以加数据类ID必须在序列化的函数里。那么这个《能序列化任意数据类的程序》也就不再能序列化任意数据类了。然后意识到“能序列化任意数据类”对网络传输意义不大,任何数据类都要通过头ID才知道怎么反序列化。
不过《能序列化任意数据类的程序》还是能解决对大型数据类一个个字段序列化太麻烦的问题。
使用GetFields()的“万能”序列化程序还有一个问题,就是不能序列化基本数据类型,只能序列化class。
综上,这个《能序列化任意数据类的程序》对输入的object需要先用is判断具体类型,加ID头,然后用GetFields()、循环序列化,如果要传输的数据类种类很多、数据类字段很多,才有优势。