2025-04-04 Unity 网络基础5——TCP分包与黏包

news2025/4/10 21:37:33

文章目录

  • 1 分包与黏包
  • 2 解决方案
    • 2.1 数据接口
    • 2.2 定义消息
    • 2.3 NetManager
    • 2.4 分包、黏包处理
  • 3 测试
    • 3.1 服务端
    • 3.2 客户端
    • 3.3 直接发送
    • 3.4 黏包发送
    • 3.5 分包发送
    • 3.6 分包、黏包发送
    • 3.7 其他

1 分包与黏包

​ 分包、黏包指在网络通信中由于各种因素(网络环境、API 规则等)造成的消息与消息之间出现的两种状态。

  • 分包:一个消息分成了多个消息进行发送。
  • 黏包:一个消息和另一个消息黏在了一起。
分包黏包示意图

注意:

​ 分包和黏包可能同时发生。

2 解决方案

​ 为消息添加头部,记录消息的长度。

​ 当接收到消息时,通过消息长度来判断是否分包、黏包,从而对消息进行拆分、合并处理。

​ 每次只处理完整的消息。

2.1 数据接口

  1. 消息接口。
public interface INetMessage
{
    int MessageId { get; }

    int BytesLength { get; }

    byte[] ToBytes();

    int FromBytes(byte[] bytes, int index);
}
  1. 接口扩展类,用于封装基本类型的序列化。
public static class ByteLengthExtension
{
    public static int GetBytesLength(this INetMessage message, int value)
    {
        return sizeof(int);
    }

    public static int GetBytesLength(this INetMessage message, string value)
    {
        return sizeof(int) + Encoding.UTF8.GetByteCount(value);
    }

    public static int GetBytesLength(this INetMessage message, bool value)
    {
        return sizeof(bool);
    }

    public static int GetBytesLength(this INetMessage message, float value)
    {
        return sizeof(float);
    }
}

public static class INetMessageExtension
{
    public static int Write(this INetMessage message, byte[] bytes, int index, int value)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        return index + sizeof(int);
    }

    public static int Read(this INetMessage message, byte[] bytes, int index, ref int value)
    {
        value = BitConverter.ToInt32(bytes, index);
        return index + sizeof(int);
    }

    public static int Write(this INetMessage message, byte[] bytes, int index, string value)
    {
        var strBytes = Encoding.UTF8.GetBytes(value);

        BitConverter.GetBytes(strBytes.Length).CopyTo(bytes, index);
        index += sizeof(int);

        strBytes.CopyTo(bytes, index);
        return index + strBytes.Length;
    }

    public static int Read(this INetMessage message, byte[] bytes, int index, ref string value)
    {
        int length = BitConverter.ToInt32(bytes, index);
        index += sizeof(int);

        value = Encoding.UTF8.GetString(bytes, index, length);
        return index + length;
    }

    public static int Write(this INetMessage message, byte[] bytes, int index, bool value)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        return index + sizeof(bool);
    }

    public static int Read(this INetMessage message, byte[] bytes, int index, ref bool value)
    {
        value = BitConverter.ToBoolean(bytes, index);
        return index + sizeof(bool);
    }

    public static int Write(this INetMessage message, byte[] bytes, int index, float value)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        return index + sizeof(float);
    }

    public static int Read(this INetMessage message, byte[] bytes, int index, ref float value)
    {
        value = BitConverter.ToSingle(bytes, index);
        return index + sizeof(float);
    }

    public static int Write(this INetMessage message, byte[] bytes, int index, INetMessage value)
    {
        value.ToBytes().CopyTo(bytes, index);
        return index + value.BytesLength;
    }

    public static int Read(this INetMessage message, byte[] bytes, int index, ref INetMessage value)
    {
        value.FromBytes(bytes, index);
        return index + value.BytesLength;
    }
}

2.2 定义消息

​ 在 ToBytes() 方法中,先写入消息 Id,然后写入该消息的长度,最后写入消息内容。

public class PlayerMessage : INetMessage
{
    public int    PlayerId;
    public string Name;
    public int    Atk;
    public int    Lev;

    public int MessageId { get => 1001; }

    public int BytesLength
    {
        get => this.GetBytesLength(MessageId) + 
               sizeof(int) + // 消息长度
               this.GetBytesLength(PlayerId) +
               this.GetBytesLength(Name) +
               this.GetBytesLength(Atk) +
               this.GetBytesLength(Lev);
    }

    public byte[] ToBytes()
    {
        var length = BytesLength;
        var bytes  = new byte[length];
        var index  = 0;
        index = this.Write(bytes, index, MessageId);

        // 写入消息长度
        index = this.Write(bytes, index, length - sizeof(int) * 2); // 减去消息长度和消息 Id 的长度

        index = this.Write(bytes, index, PlayerId);
        index = this.Write(bytes, index, Name);
        index = this.Write(bytes, index, Atk);
        index = this.Write(bytes, index, Lev);
        return bytes;
    }

    public int FromBytes(byte[] bytes, int index)
    {
        // 反序列化不需要解析 Id,在此之前应解析 Id 从而使用该方法
        index = this.Read(bytes, index, ref PlayerId);
        index = this.Read(bytes, index, ref Name);
        index = this.Read(bytes, index, ref Atk);
        index = this.Read(bytes, index, ref Lev);
        return index;
    }

    public override string ToString()
    {
        return $"PlayerMessage: {PlayerId}, {Name}, {Atk}, {Lev}";
    }
}

2.3 NetManager

​ 使用单例管理器 NetManager 对消息的发送与接受进行管理。

  • _socket:客户端的 Socket。
  • _sendMessages:发送消息的公共队列,主线程塞消息,发送线程拿消息进行发送。
  • _receiveMessages:接收消息的公共队列,主线程拿消息,接收线程获取消息塞进去。
  • _isConnected:是否与服务器连接。
  • _cacheBytes:消息缓冲区。
  • _cacheBytesLength:当前缓冲长度。
public class NetManager : MonoBehaviour
{
    public static NetManager Instance { get; private set; } // 单例模式
    
    private Socket _socket;
    
    private Queue<INetMessage> _sendMessages = new Queue<INetMessage>();

    private Queue<INetMessage> _receiveMessages = new Queue<INetMessage>();

    private bool _isConnected = false;

    private byte[] _cacheBytes = new byte[1024 * 1024]; // 大小为 1MB
    
    private int _cacheBytesLength;
    
    private void Awake() 
    {
        Instance = this;
    }

    private void OnDestroy()
    {
        Close();
        Instance = null;
    }
    
    ...
}
  1. 连接与断开
    • Connect():连接指定 ip 与 port 的服务器。
    • Close():断开连接。
public class NetManager : MonoBehaviour
{
    ...
        
    public void Connect(string ip, int port)
    {
        _socket ??= new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        try
        {
            _socket.Connect(ip, port);
            _isConnected = true;

            // 开启两个线程,一个负责发送消息,一个负责接收消息
            ThreadPool.QueueUserWorkItem(SendMessageThread);
            ThreadPool.QueueUserWorkItem(ReceiveMessageThread);
        }
        catch (SocketException e)
        {
            if (e.ErrorCode == 10061)
            {
                Debug.LogError("服务器拒绝连接");
            }
            else
            {
                Debug.LogError("连接失败");
            }
        }
    }

    public void Close()
    {
        _isConnected = false;
        _socket?.Shutdown(SocketShutdown.Both);
        _socket?.Close();
    }
    
    /// <summary>
    /// 发送线程的任务
    /// </summary>
    private void SendMessageThread(object state)
    {
        while (_isConnected)
        {
            if (_sendMessages.Count > 0) // 有消息就发送
            {
                var message = _sendMessages.Dequeue();
                _socket.Send(message.ToBytes());
            }
        }
    }

    private void ReceiveMessageThread(object state)
    {
        while (_isConnected)
        {
            if (_socket.Available > 0)
            {
                var receiveBytes = new byte[1024];
                var length       = _socket.Receive(receiveBytes);
                HandleReceiveMessage(receiveBytes, length); // 核心方法,依据消息长度进行处理
            }
        }
    }
    
    ...
}
  1. 发送消息
    • Send():发送指定消息。
    • SendTest():发送字节数组(测试分包、黏包使用)。
public class NetManager : MonoBehaviour
{
    ...
        
    public void Send(INetMessage message)
    {
        _sendMessages.Enqueue(message); // 塞入消息队列
    }

    public void SendTest(byte[] bytes)
    {
        _socket.Send(bytes);
    }
    
    ...
}
  1. 接收消息

    Update() 方法中不断判断接收消息队列,有消息则打印出来。

public class NetManager : MonoBehaviour
{
    ...
        
    private void Update()
    {
        if (_receiveMessages.Count > 0)
        {
            var message = _receiveMessages.Dequeue();
            Debug.Log(message); // 主线程处理消息
        }
    }
    
    ...
}

2.4 分包、黏包处理

  1. 收到消息时看之前有没有缓存,如果有,直接拼接到后面。
  2. 循环处理消息体
    • 处理前置信息(Id、解析长度)。
    • if 缓冲区 + 该条消息体 >= 一条完整信息时
      • 判断消息 Id。
      • 塞入消息队列。
      • 更新 index_cacheBytesLength
    • else 还没有接收到一条完整消息
      • 解析了前置信息,但是没有成功解析消息体,则回退到解析 Id 的位置。
      • 缓存剩余数据。
public class NetManager : MonoBehaviour
{
    ...
        
    private void HandleReceiveMessage(byte[] receiveBytes, int receiveNum)
    {
        var messageId = 0;
        var index     = 0; // receiveBytes 的处理进度(下标)

        // 收到消息时看之前有没有缓存
        // 如果有,直接拼接到后面
        receiveBytes.CopyTo(_cacheBytes, _cacheBytesLength);
        _cacheBytesLength += receiveNum;

        while (true)
        {
            var messageLength = -1; // 当前消息长度

            // 处理前置信息
            if (_cacheBytesLength - index >= 8)
            {
                // 解析 Id
                messageId =  BitConverter.ToInt32(_cacheBytes, index);
                index     += sizeof(int);

                // 解析长度
                messageLength =  BitConverter.ToInt32(_cacheBytes, index);
                index         += sizeof(int);
            }

            // 处理消息体
            if (messageLength != -1 && _cacheBytesLength - index >= messageLength)
            {
                // 解析消息体
                INetMessage message = default;
                switch (messageId)
                {
                    case 1001:
                        message = new PlayerMessage();
                        message.FromBytes(_cacheBytes, index);
                        break;
                }

                if (message != default)
                {
                    _receiveMessages.Enqueue(message); // 塞入消息队列
                }
                index += messageLength;

                // 如果消息体长度等于缓存长度,证明缓存已经处理完毕
                if (index == _cacheBytesLength)
                {
                    _cacheBytesLength = 0;
                    break;
                }
            }
            else // 消息体还没有接收完毕
            {
                // 解析了前置信息,但是没有成功解析消息体
                if (messageLength != -1)
                {
                    index -= 8; // 回退到解析 Id 的位置
                }

                // 缓存剩余的数据
                _cacheBytesLength -= index;
                Array.Copy(_cacheBytes, index, _cacheBytes, 0, _cacheBytesLength);

                break;
            }
        }
    }
}

3 测试

​ 服务端、客户端配置见 2025-03-25 Unity 网络基础4——TCP同步通信_unity tcp-CSDN博客。

3.1 服务端

​ 封装 ServerSocket 和 ClientSocket 方便通信。

  1. Program.cs
    • 创建 ServerSocket,使用本地 ip 127.0.0.1 作为服务器地址。
    • 循环监听指令。
      • “exit”:退出。
      • “B:1001”:向所有客户端发送消息。
// See https://aka.ms/new-console-template for more information

using NetLearningTcpServerExercise2;

var serverSocket = new ServerSocket();
serverSocket.Start("127.0.0.1", 8080, 1024);
Console.WriteLine("Server Start!");

while (true)
{
    var input = Console.ReadLine();
    if (input == "exit")
    {
        serverSocket.Close();
        break;
    }
    else if (input?[..2] == "B:") // 输入命令向所有客户端广播消息
    {
        if (input[2..] == "1001")
        {
            var playerMsg = new PlayerMessage()
            {
                PlayerId = 1,
                Name     = "张三",
                Atk      = 100,
                Lev      = 10
            };
            serverSocket.Broadcast(playerMsg);
        }
    }
}
  1. ServerSocket.cs
// ------------------------------------------------------------
// @file       ServerSocket.cs
// @brief
// @author     zheliku
// @Modified   2025-03-24 02:03:06
// @Copyright  Copyright (c) 2025, zheliku
// ------------------------------------------------------------

namespace NetLearningTcpServerExercise2;

using System.Net;
using System.Net.Sockets;

public class ServerSocket
{
    private readonly Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

    private Dictionary<int, ClientSocket> _clientSockets = new Dictionary<int, ClientSocket>();

    private bool _Running;

    public void Start(string ip, int port, int maxClientCount)
    {
        _socket.Bind(new IPEndPoint(IPAddress.Parse(ip), port));
        _socket.Listen(maxClientCount);
        _Running = true;

        ThreadPool.QueueUserWorkItem(AcceptClient);
        ThreadPool.QueueUserWorkItem(ReceiveMessage);
    }

    public void Close()
    {
        _Running = false;
        foreach (var clientSocket in _clientSockets.Values)
        {
            clientSocket.Close();
        }
        _clientSockets.Clear();

        try
        {
            _socket.Shutdown(SocketShutdown.Both);
        }
        catch { }
        finally
        {
            _socket.Close();
        }
    }

    public void Broadcast(INetMessage message)
    {
        foreach (var clientSocket in _clientSockets.Values)
        {
            clientSocket.SendMessage(message);
        }
    }

    private void AcceptClient(object? state)
    {
        while (_Running)
        {
            try
            {
                // 连入客户端
                var clientSocket = new ClientSocket(_socket.Accept());

                // clientSocket.SendMessage("Welcome to the server!");

                _clientSockets.Add(clientSocket.Id, clientSocket);
            }
            catch (Exception e)
            {
                Console.WriteLine("ClientSocket Accept Wrong: " + e);
            }
        }
    }

    private void ReceiveMessage(object? state)
    {
        while (_Running)
        {
            if (_clientSockets.Count > 0)
            {
                foreach (var clientSocket in _clientSockets.Values)
                {
                    clientSocket.ReceiveMessage();
                }
            }
        }
    }
}
  1. ClientSocket.cs
// ------------------------------------------------------------
// @file       ClientSocket.cs
// @brief
// @author     zheliku
// @Modified   2025-03-24 01:03:42
// @Copyright  Copyright (c) 2025, zheliku
// ------------------------------------------------------------

namespace NetLearningTcpServerExercise2;

using System.Net.Sockets;
using System.Text;

public class ClientSocket
{
    private static int _ClientBeginId = 1;

    private readonly Socket _socket;

    private byte[] _cacheBytes = new byte[1024 * 1024]; // 缓冲区,大小为 1MB
    private int    _cacheBytesLength;

    public int Id;

    public bool Connected
    {
        get => _socket.Connected;
    }

    public ClientSocket(Socket socket)
    {
        Id      = _ClientBeginId++;
        _socket = socket;
    }

    public void Close()
    {
        try
        {
            _socket.Shutdown(SocketShutdown.Both);
        }
        catch { }
        finally
        {
            _socket.Close();
        }
    }

    public void SendMessage(INetMessage message)
    {
        try
        {
            _socket.Send(message.ToBytes());
        }
        catch (Exception e)
        {
            Console.WriteLine("SendMessage Wrong: " + e);
            Close();
        }
    }

    public void ReceiveMessage()
    {
        try
        {
            if (_socket.Available > 0)
            {
                var buffer        = new byte[1024 * 5];
                var receiveLength = _socket.Receive(buffer);
                HandleReceiveMessage(buffer, receiveLength);
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("ReceiveMessage Wrong: " + e);
            Close();
        }
    }

    private void MessageHandle(object? state)
    {
        if (state == null) return;

        Console.WriteLine($"Receive message from client {_socket} (ID {Id}): {state}");
    }

    private void HandleReceiveMessage(byte[] receiveBytes, int receiveNum)
    {
        var messageId = 0;
        var index     = 0;

        // 收到消息时看之前有没有缓存
        // 如果有,直接拼接到后面
        receiveBytes.CopyTo(_cacheBytes, _cacheBytesLength);
        _cacheBytesLength += receiveNum;

        while (true)
        {
            var messageLength = -1;

            // 处理前置信息
            if (_cacheBytesLength - index >= 8)
            {
                // 解析 Id
                messageId =  BitConverter.ToInt32(_cacheBytes, index);
                index     += sizeof(int);

                // 解析长度
                messageLength =  BitConverter.ToInt32(_cacheBytes, index);
                index         += sizeof(int);
            }

            // 处理消息体
            if (messageLength != -1 && _cacheBytesLength - index >= messageLength)
            {
                // 解析消息体
                INetMessage message = default;
                switch (messageId)
                {
                    case 1001:
                        message = new PlayerMessage();
                        message.FromBytes(_cacheBytes, index);
                        break;
                }

                if (message != default)
                {
                    ThreadPool.QueueUserWorkItem(MessageHandle, message);
                }
                index += messageLength;

                // 如果消息体长度等于缓存长度,证明缓存已经处理完毕
                if (index == _cacheBytesLength)
                {
                    _cacheBytesLength = 0;
                    break;
                }
            }
            else // 消息体还没有接收完毕
            {
                // 解析了前置信息,但是没有成功解析消息体
                if (messageLength != -1)
                {
                    index -= 8; // 回退到解析 Id 的位置
                }

                // 缓存剩余的数据
                _cacheBytesLength -= index;
                Array.Copy(_cacheBytes, index, _cacheBytes, 0, _cacheBytesLength);

                break;
            }
        }
    }
}

3.2 客户端

​ 在 Unity 中创建场景,4 个按钮分别发送不同消息。

image-20250404190900044

​ 编写 Lesson7.cs 测试脚本,挂载到 Lesson7 场景物体上。

​ “发送”、“黏包”、“分包”、“分包、黏包”按钮分别挂载到 Lesson7 的 BtnSendBtnSend1BtnSend2BtnSend3 成员上。

// ------------------------------------------------------------
// @file       Lesson7.cs
// @brief
// @author     zheliku
// @Modified   2025-03-24 02:03:40
// @Copyright  Copyright (c) 2025, zheliku
// ------------------------------------------------------------

namespace Lesson
{
    using System;
    using System.Threading.Tasks;
    using UnityEngine;
    using UnityEngine.UI;

    public class Lesson7 : MonoBehaviour
    {
        public Button BtnSend;
        public Button BtnSend1;
        public Button BtnSend2;
        public Button BtnSend3;

        private void Start()
        {
            NetManager.Instance.Connect("127.0.0.1", 8080);

            BtnSend.onClick.AddListener(() =>
            {
                ...
            });

            BtnSend1.onClick.AddListener(() =>
            {
                ...
            });
            
            BtnSend2.onClick.AddListener(async () =>
            {
                ...
            });
            
            BtnSend3.onClick.AddListener(async () =>
            {
                ...
            });
        }
    }
}

3.3 直接发送

​ 启动服务端,再启动 Unity。直接发送的逻辑如下:

BtnSend.onClick.AddListener(() =>
{
    var playerMsg = new PlayerMessage()
    {
        PlayerId = 1001,
        Name     = "发送",
        Atk      = 100,
        Lev      = 1
    };
    NetManager.Instance.Send(playerMsg);
});

​ 点击 Unity 中的“发送”按钮,服务器端接收到消息。

image-20250404191338724

3.4 黏包发送

image-20250404192804164

​ 黏包发送定义了两条消息,顺序存放在一个长字节数组中一并发送。具体逻辑如下:

BtnSend1.onClick.AddListener(() =>
{
    var playerMsg1 = new PlayerMessage()
    {
        PlayerId = 1002,
        Name     = "黏包1",
        Atk      = 100,
        Lev      = 1
    };
    var playerMsg2 = new PlayerMessage()
    {
        PlayerId = 1003,
        Name     = "黏包2",
        Atk      = 63,
        Lev      = 5
    };

    var bytes = new byte[playerMsg1.BytesLength + playerMsg2.BytesLength];
    playerMsg1.ToBytes().CopyTo(bytes, 0);
    playerMsg2.ToBytes().CopyTo(bytes, playerMsg1.BytesLength);
    NetManager.Instance.SendTest(bytes);
});

​ 点击 Unity 中的“黏包”按钮,服务器端一次性接收到 2 条消息。

image-20250404191754725

3.5 分包发送

image-20250404192814792

​ 分包发送定义了 1 条消息,将前 10 个字节拷贝到一个数组中,剩余字节拷贝到另一个数组中。两个数组间隔 0.5s 发送。具体逻辑如下:

BtnSend2.onClick.AddListener(async () =>
{
    var playerMsg = new PlayerMessage()
    {
        PlayerId = 1004,
        Name     = "分包",
        Atk      = 100,
        Lev      = 1
    };

    var bytes  = playerMsg.ToBytes();
    var bytes1 = new byte[10];
    var bytes2 = new byte[bytes.Length - 10];
    Array.Copy(bytes, 0, bytes1, 0, 10);
    Array.Copy(bytes, 10, bytes2, 0, bytes.Length - 10);

    NetManager.Instance.SendTest(bytes1);
    await Task.Delay(500); // 注释改行后会自动黏包,服务端立刻接收到消息
    NetManager.Instance.SendTest(bytes2);
});

​ 点击 Unity 中的“分包”按钮,服务器端等待 0.5s 后,才接收到消息。

image-20250404192025667

3.6 分包、黏包发送

image-20250404192824995

​ 分包、黏包发送定义了 2 条消息,将消息 1 和消息 2 的前 10 个字节拷贝到一个数组中,消息 2 剩余字节拷贝到另一个数组中。两个数组间隔 0.5s 发送。具体逻辑如下:

BtnSend3.onClick.AddListener(async () =>
{
    var playerMsg1 = new PlayerMessage()
    {
        PlayerId = 1005,
        Name     = "分包黏包1",
        Atk      = 9,
        Lev      = 1
    };
    var playerMsg2 = new PlayerMessage()
    {
        PlayerId = 1006,
        Name     = "分包黏包2",
        Atk      = 63,
        Lev      = 55
    };
    var bytes1 = playerMsg1.ToBytes();
    var bytes2 = playerMsg2.ToBytes();

    var bytes2_1 = new byte[10];
    var bytes2_2 = new byte[bytes2.Length - 10];
    Array.Copy(bytes2, 0, bytes2_1, 0, 10);
    Array.Copy(bytes2, 10, bytes2_2, 0, bytes2.Length - 10);

    var firstBytes = new byte[bytes1.Length + bytes2_1.Length];
    Array.Copy(bytes1, 0, firstBytes, 0, bytes1.Length);
    Array.Copy(bytes2_1, 0, firstBytes, bytes1.Length, bytes2_1.Length);

    var secondBytes = bytes2_2;

    NetManager.Instance.SendTest(firstBytes);
    await Task.Delay(500); // 注释改行后会自动黏包,服务端立刻接收到消息
    NetManager.Instance.SendTest(secondBytes);
});

​ 点击 Unity 中的“分包、黏包”按钮,服务器端立刻接收到消息 1。

image-20250404192344679

​ 等待 0.5s 后,接收到消息 2。

image-20250404192411217

3.7 其他

​ 在服务端输入 “B:1001” 命令后,回到 Unity,发现接收到消息。

image-20250404192625942 image-20250404192650849

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

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

相关文章

chromium魔改——绕过无限debugger反调试

在进行以下操作之前&#xff0c;请确保已完成之前文章中提到的 源码拉取及编译 部分。 如果已顺利完成相关配置&#xff0c;即可继续执行后续操作。 在浏览器中实现“无限 debugger”的反调试技术是一种常见的手段&#xff0c;用于防止他人通过开发者工具对网页进行调试或逆向…

JS dom修改元素的style样式属性

1通过样式属性修改 第三种 toggle有就删除 没就加上

灭火器离位检测:智能视觉守护安全

利用视觉分析实现明火检测&#xff1a;技术、功能与应用 一、背景 清明节期间&#xff0c;兰州市连续发生多起因祭祖烧纸引发山火的警情&#xff0c;如七里河区魏岭乡赵某某等人上坟烧纸未妥善处理烛火引燃杂草&#xff0c;导致3人烧伤&#xff1b;七里河区彭家坪石板山村村民…

网络:华为数通HCIA学习:IP路由基础

华为HCIA学习 IP路由基础路由协议或路由种类以及对应路由的优先级按工作区域分类&#xff1a;按工作机制及算法分类&#xff1a;路由的优先级路由器选择最优路由的顺序是什么? 前言自治系统LAN和广播域路由选路IP路由表路由度量建立路由表最长匹配原则路由器转发数据包总结 IP…

多线程开发中List的使用

由于ArrayList在多线程高并发情况下是不安全的&#xff0c;因此要慎用&#xff0c;那么此时如果涉及到集合操作&#xff0c;应该怎么选&#xff1a; 方案一&#xff1a;Vector: 特点&#xff1a;通过给所有方法都用 synchronized 修饰从而保证线程安全&#xff0c; 缺点&…

使用 .NET 9 和 Azure 构建云原生应用程序:有什么新功能?

随着 .NET 9 推出一系列以云为中心的增强功能&#xff0c;开发人员拥有比以往更多的工具来在 Azure 上创建可扩展、高性能的云原生应用程序。让我们深入了解 .NET 9 中的一些出色功能&#xff0c;这些功能使构建、部署和优化云应用程序变得更加容易&#xff0c;并附有示例以帮助…

前端页面鼠标移动监控(鼠标运动、鼠标监控)鼠标防抖处理、mousemove、debounce()、事件停止触发、超时触发

文章目录 代码使用lodashjs库debounce函数做防抖处理&#xff08;只有鼠标移动停止并超过一定时间&#xff0c;才会触发&#xff09;手写防抖函数写法1写法2&#xff08;注意addEventListener监听函数的第二个参数接收的是一个函数&#xff0c;需要构造一个匿名返回函数&#x…

开源守护,智护童年——幼儿园未成年行为与安全智能监控系统

在孩子成长的每一步&#xff0c;安全始终是第一位的。幼儿园作为孩子们探索世界的起点&#xff0c;其安全管理的重要性不言而喻。然而&#xff0c;哭闹、打闹、意外跌倒&#xff0c;甚至外部隐患如陌生人逗留、内部管理疏漏等问题&#xff0c;常常让传统人工监控捉襟见肘。家长…

WinForm真入门(5)——控件的基类Control

控件的基类–Control 用于 Windows 窗体应用程序的控件都派生自 Control类并继承了许多通用成员,这些成员都是平时使用控件的过程最常用到的。无论要学习哪个控件的使用&#xff0c;都离不开这些基本成员&#xff0c;尤其是一些公共属性。由于 Conlrol 类规范了控件的基本特征…

《Linux内存管理:实验驱动的深度探索》【附录】【实验环境搭建 4】【Qemu 如何模拟numa架构】

我们在学习 linux 内核时&#xff0c;会涉及到很多 numa 的知识&#xff0c;那我们该如何在 qemu 中模拟这种情况&#xff0c;来配合我们的学习呢&#xff1f; 我们该如何模拟 如下的 numa 架构 Qemu 模拟 NUMA 架构 -M virt,gic-version3,virtualizationon,typevirt \ -cp…

【YOLO系列(V5-V12)通用数据集-工程用车检测数据集】

YOLO格式的工程车检测数据集&#xff0c;适用于YOLOv5-v11所有版本&#xff0c;可以用于本科毕设、发paper、做课设等等&#xff0c;有需要的在这里获取&#xff1a; 【YOLO系列&#xff08;V5-V12&#xff09;通用数据集-工程用车检测数据集】 【工程车类型检测数据集】共2655…

卫星智能化健康管理#卫星工程系列

伴随我国航天业飞速发展&#xff0c;积累了大量的卫星试验数据&#xff0c;如何从海量、多源、多模态的卫星试验数据中挖掘分析出内部规律和潜在价值&#xff0c;构建卫星装备系统的全生命周期试验数据知识体系显得尤为迫切。卫星故障传统的诊断方法局限在门限层面&#xff0c;…

Neo4j操作数据库(Cypher语法)

Neo4j数据库操作语法 使用的数据库版本 (终端查询) >neo4j --version 2025.03.0批量上传数据 UNWIND [{name: Alice, age: 30},{name: Bob, age: 25} ] AS person CREATE (p:Person) SET p.name = person.name, p.age = person.age RETURN p;查询结点总数 MATCH (n) RETU…

[GN] Python3基本数据类型 -- 与C的差异

文章目录 前言Python3的基本数据类型6个标准的数据类型NumbersStringListtupleSetsDictionaries Python运算符逻辑 运算符成员运算符身份运算符 Python3 数字Python3 序列序列切片序列相加序列相乘序列相关内置函数 Python3 列表访问列表的值更新列表删除列表元素拼接列表嵌套列…

MSF上线到CS工具中 实战方案(可执行方案)

目录 实际案例背景 步骤详解 1. 获取低权限 Meterpreter 会话 1.1 使用 Metasploit 获取会话 2. 提权到 SYSTEM 权限 2.1 使用 getsystem 自动提权 2.2 如果 getsystem 失败&#xff1a;使用令牌冒充 (incognito 模块) 3. 上线到 Cobalt Strike 3.1 生成 Cobalt Strik…

IntelliJ IDEA 2020~2024 创建SpringBoot项目编辑报错: 程序包org.springframework.boot不存在

目录 前奏解决结尾 前奏 哈&#xff01;今天在处理我的SpringBoot项目时&#xff0c;突然遇到了一些让人摸不着头脑的错误提示&#xff1a; java: 程序包org.junit不存在 java: 程序包org.junit.runner不存在 java: 程序包org.springframework.boot.test.context不存在 java:…

基于DeepSeek、ChatGPT支持下的地质灾害风险评估、易发性分析、信息化建库及灾后重建

前言&#xff1a; 地质灾害是指全球地壳自然地质演化过程中&#xff0c;由于地球内动力、外动力或者人为地质动力作用下导致的自然地质和人类的自然灾害突发事件。在降水、地震等自然诱因的作用下&#xff0c;地质灾害在全球范围内频繁发生。我国不仅常见滑坡灾害&#xff0c;还…

Websoft9分享:在数字化转型中选择开源软件可能遇到的难题

引言&#xff1a;中小企业数字化转型的必由之路 全球94.57%的企业已采用开源软件&#xff08;数据来源&#xff1a;OpenLogic 2024报告)&#xff0c;开源生态估值达8.8万亿美元。中小企业通过开源软件构建EPR系统、企业官网、数据分析平台等&#xff0c;可节省80%软件采购成本。…

Windows修改hosts文件让向日癸软件联网

Windows修改hosts文件让向日癸软件联网 前言一、查看向日葵软件使用的网址及IP1.清除dns记录2.打开向日葵软件并将dns记录导出txt 二、修改Windows服务器的hosts文件1.winx选择Windows PowerShell(管理员)2.在Windows PowerShell中输入如下内容&#xff1a;3.在hosts文件最后添…

2021 CCF CSP-S2.括号序列

题目 4091. 括号序列 算法标签: 区间 d p dp dp 思路 区间 d p dp dp添加维表示形态 f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k], 对于每种形态考虑状态如何进行转移, 枚举的时候不能重复, 星号也要定义唯一的解析方式, 算法时间复杂度 O ( n 3 ) O(n ^ 3) O(n3) 代码 #…