Unity网络通信笔记

需求

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

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

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

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

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

调试用具

首先写一个能打印字节数组的函数,用于直接查看消息内容。

cs 复制代码
void PrintBytes(byte[] bytes) {
        string byteString = string.Join(", ", bytes);
        Debug.Log(byteString);
    }

通信模块

  1. 服务端会连很多客户端,为了存连接的所有客户端可以用一个字典;
  2. 服务端要知道客户端断开了连接,可以通过判断socket.Receive(buffer)返回的数是否为0.没收到消息时这个函数会阻塞线程,不返回,若返回0则说明客户端断开了连接。需要紧接着执行socket.Shutdown()和socket.Close();
cs 复制代码
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();
    }
}

序列化反序列化模块

用于网络通信的二进制序列化和一般二进制序列化多了消息头,消息头包括:

  1. 标记是哪种数据类的ID;
  2. 用于处理分包黏包而加的消息体长度;

如果暂时没有实战项目,纯为了学习,该怎么写数据类序列化反序列化的部分?

是否要写一个能序列化任意数据类的程序?

序列化之后就是一个字节数组,已经无法知道是什么数据类了,所以加数据类ID必须在序列化的函数里。那么这个《能序列化任意数据类的程序》也就不再能序列化任意数据类了。然后意识到"能序列化任意数据类"对网络传输意义不大,任何数据类都要通过头ID才知道怎么反序列化。

不过《能序列化任意数据类的程序》还是能解决对大型数据类一个个字段序列化太麻烦的问题。

使用GetFields()的"万能"序列化程序还有一个问题,就是不能序列化基本数据类型,只能序列化class。

综上,这个《能序列化任意数据类的程序》对输入的object需要先用is判断具体类型,加ID头,然后用GetFields()、循环序列化,如果要传输的数据类种类很多、数据类字段很多,才有优势。

消息体长度需要把数据类序列化后才能知道,却要放在消息体前面。这么看序列化函数里用byte[]处理太不灵活了,不如用List<byte>。

分包粘包处理

一句话概括分包粘包:接收端接收到的消息不一定是一条,可能小于1,可能是多条,可能有"小数"。

发送端在消息头再加上消息体的字节数。接收端有一个byte[]缓存区有一个缓存标志位int cacheMark。**前面的消息解析完后,最后不完整的消息是搬运到缓冲区头还是原地不动?**搬到缓存区头造成额外劳动。

如果原地不动,那么

  1. 把完整消息解析后如果尾部有残留消息需要把cacheMark移到残留消息头,但是如果没有残留消息则不移动(感觉这样很别扭);
  2. 把残留消息拼好解析后把cacheMark设0。

相关推荐
云云32114 分钟前
亚矩阵云手机针对AdMob广告平台怎么进行多账号的广告风控
大数据·网络·线性代数·游戏·智能手机·矩阵
网安INF1 小时前
CVE-2020-1938源码分析与漏洞复现(Tomcat 文件包含/读取)
java·网络·web安全·网络安全·tomcat·漏洞复现
nice_evil2 小时前
华为Openeuler/Linux/CentOs 网络配置及故障排查/远程连接设置ssh/ibmc等问题及解决方案合集
linux·网络·centos·openeuler·网络配置与激活
风清再凯3 小时前
docker 网络
网络·docker·容器
沐土Arvin3 小时前
三次握手建立连接,四次挥手释放连接——TCP协议的核心机制
java·网络·tcp/ip
Amy.Wang4 小时前
常见的网络协议有哪些
网络·网络协议
Mountain and sea4 小时前
ABB RobotStudio 和 S7-PLCSIM Advanced V5.0 搭建虚拟通信环境,实现 PLC 对机器人布尔量、数字量和模拟量的控制。
网络·机器人
freyazzr5 小时前
TCP/IP 网络编程 | Reactor事件处理模式
开发语言·网络·c++·网络协议·tcp/ip
AI风老师5 小时前
通信网络基础概念
开发语言·网络·php
边啵儿6 小时前
如何确定某个路由器的路由表?(计算机网络)
网络·计算机网络