Unity网络通信笔记

发布于:2025-06-10 ⋅ 阅读:(36) ⋅ 点赞:(0)

需求

首先要意识到网络通信面对的是一个怎么样的情景:

  1. 服务器会连任意个客户端,任意时刻可能有客户端连入连出;
  2. 服务端和客户端可能任意时刻给对方发消息,所以双方都要一直准备好接收。但是两端还有别的事要做,通信不能阻塞主线程;
  3. 发的只能是字节数组,发时要把数据类序列化,接收时反序列化,序列化反序列化程序是每个数据类专门写一个还是写一个通用的?通用的怎么写?
  4. 发的只能是字节数组,但是发的数据类有多种,需要一个数据类型头标记这是哪个数据类。(如果是二进制序列化用于保存文件,就可以通过文件路径知道对应的数据类,无需这个标记ID,但网络通信是一条信道传递多种数据类);
  5. 定义通信的需求,什么情况下需要发数据?发什么数据类?对方回复什么?相当于自定义一套协议。不过没设计好也可以先写纯收发字节数组的模块;

可以把通信部分分成两个模块:序列化反序列化模块、通信模块。前者负责:

  1. 在数据类和字节数组之间转换;
  2. 发送时在数据类的字节数组前加上标记数据类类型的ID;
  3. 接收时根据头的ID判断数据类类型,然后反序列化成数据类;

通信模块只管接收字节数组,发送给另一端,和接收另一端发来的字节数组。 

通信模块

  1. 服务端会连很多客户端,为了存连接的所有客户端可以用一个字典;
  2. 服务端要知道客户端断开了连接,可以通过判断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_EDITOR
                Debug.Log("服务器拒绝连接");
#endif
            }
            else
            {
#if UNITY_EDITOR
                Debug.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()、循环序列化,如果要传输的数据类种类很多、数据类字段很多,才有优势。

分包粘包处理

发送端在消息头再加上消息体的字节数。接收端有一个byte[]缓存区,有一个缓存标志位int cacheMark,

 


网站公告

今日签到

点亮在社区的每一天
去签到