Unity-TCP-网络聊天功能(一): API、客户端服务器、数据格式、粘包拆包

news2024/11/20 14:21:01

1.TCP相关API介绍与服务端编写

TCP是面向连接的。因此需要创建监听器,监听客户端的连接。当连接成功后,会返回一个TcpClient对象。通过TcpClient可以接收和发送数据。

VS创建C# .net控制台应用

项目中创建文件夹Net,Net 下添加TCPServer.cs类,用来创建TCPListener和Accept客户端连接,实例化一个TCPServcer放在Main函数执行。Client用来管理每一个客户端的接收发送消息。

namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            TCPServer tcpServer = new TCPServer();
            tcpServer.Start();

            while(true)
            {
                Console.ReadLine();//监听用户输入,不写这句话运行测试会立刻跳出
            }
        }
    }
}
namespace Server.Net
{
    internal class TCPServer
    {
        TcpListener tcpListener;
        //启动服务器,创建监听器
        public void Start()
        {
            try
            {
                //创建监听器,如果不指定IP会以本机的IP为服务器的IP
                tcpListener = TcpListener.Create(7788);//1-65535
                tcpListener.Start(500);//启动监听,传递最大接收多少客户端连接。

                Console.WriteLine("TCP Server Start");

                //启动服务器后,还要调用接收连接
                Accept();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            
        }

        //监听客户端连接
        //由于内部使用的方法是AcceptTcpClientAsync异步,因此方法也要是async
        //这样就可以通过await的方式等待
        public async void Accept()
        {
            try
            {
                //监听TCP客户端连接的返回的对象
                TcpClient tcpClient = await tcpListener.AcceptTcpClientAsync();
                Console.WriteLine("客户端已连接:" + tcpClient.Client.RemoteEndPoint);//打印IP
                                                                               //使用构建的Client类来缓存这些TcpClient
                Client client = new Client(tcpClient);
                Accept();//继续接受来自客户端的连接即可
            }
            catch (Exception e)
            {
                Console.WriteLine($"Accept:{e.Message}");
                tcpListener.Stop();
            }
        }
    }
}
namespace Server.Net
{
    //每一个客户端都是一个独立的Client
    internal class Client
    {
        TcpClient client;
        public Client(TcpClient tcpClient)
        {
            client = tcpClient;
            Receive();
        }

        //接收消息
        public async void Receive()
        {
            //处于连接状态就持续接收信息。
            while(client.Connected)
            {
                try
                {
                    byte[] buffer = new byte[4096];//存储接收到的数据,定义初始容量,一般不超过4096或1024
                    //把消息存到buffer中,从第几个字节开始存,存多长,,,,,,返回length表示接收到多少数据。
                    int length = await client.GetStream().ReadAsync(buffer, 0, buffer.Length);

                    if(length > 0)//表示有效信息
                    {
                        //$作用是将{}内容当做表达式
                        Console.WriteLine($"接收到的数据长度:{length}");
                        Console.WriteLine($"接收到的数据内容:{Encoding.UTF8.GetString(buffer, 0, length)}");//将byte数组转换为字符串
                    }
                    else
                    {
                        //客户端关闭了
                        client.Close();
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine($"Receive Error:{e.Message}");
                    client.Close();//出现错误断开客户端连接。
                }
            }            
        }

        //发送消息
        public async void Send(byte[] data)
        {
            try
            {
                //数据写入网络数据流中。就是发送
                await client.GetStream().WriteAsync(data, 0, data.Length);
                Console.WriteLine("发送成功! " + $"发送的消息内容:{Encoding.UTF8.GetString(data, 0, data.Length)}");
            }
            catch (Exception e)
            {
                client.Close();//关闭客户端
                Console.WriteLine($"send error:{e.Message}");
            }
        }
    }
}

运行测试

2.实现客户端和服务器的消息收发

编写Unity客户端,Client项目,创建Script/Net/Client.cs

注意收发消息都必须是异步的async,因为不能阻塞我们的程序。

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

public class Client
{
    private static Client instance = new Client();
    public static Client Instance => instance;//单例模式便于调用
    
    private TcpClient client;//跟服务器通信需要调用client
    
    public  void Start()
    {
        client = new TcpClient();
        Connect();
    }
    
    //连接服务器接口
    public async void Connect()
    {
        try
        {
            await client.ConnectAsync("127.0.0.1", 7788);
            Debug.Log("TCP 连接成功");
            Receive();
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
        }
    }
    
    //接收接口
    public async void Receive()
    {
        while (client.Connected)
        {
            try
            {
                byte[] buff = new byte[4096];
                int length = await client.GetStream().ReadAsync(buff, 0, buff.Length);
                if (length > 0)
                {
                    Debug.Log($"接收到的数据长度:{length}");
                    Debug.Log($"接收到的数据内容:{Encoding.UTF8.GetString(buff, 0, length)}");
                }
                else
                {
                    client.Close();
                }
            }
            catch (Exception e)
            {
                Debug.Log(e.Message);
                client.Close();
            }
        }
    }
    
    //发送接口
    public async void Send(byte[] data)
    {
        try
        {
            await client.GetStream().WriteAsync(data, 0, data.Length);
            Debug.Log("发送成功! " + $"发送的消息内容:{Encoding.UTF8.GetString(data, 0, data.Length)}");
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
            client.Close();
        }
    }
}

Unity创建Scripts/GameManager.cs来启动客户端连接服务器

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

public class GameManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Client.Instance.Start();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            Client.Instance.Send(Encoding.UTF8.GetBytes("login..."));
        }
    }
}
public async void Receive()
{
    //处于连接状态就持续接收信息。
    while(client.Connected)
    {
        try
        {
            byte[] buffer = new byte[4096];//存储接收到的数据,定义初始容量,一般不超过4096或1024
            //把消息存到buffer中,从第几个字节开始存,存多长,,,,,,返回length表示接收到多少数据。
            int length = await client.GetStream().ReadAsync(buffer, 0, buffer.Length);

            if(length > 0)//表示有效信息
            {
                //$作用是将{}内容当做表达式
                Console.WriteLine($"接收到的数据长度:{length}");
                Console.WriteLine($"接收到的数据内容:{Encoding.UTF8.GetString(buffer, 0, length)}");//将byte数组转换为字符串

                Send(Encoding.UTF8.GetBytes("测试返回..."));
            }
            else
            {
                //客户端关闭了
                client.Close();
            }
        }
        catch (Exception e)
        {
            Console.WriteLine($"Receive Error:{e.Message}");
            client.Close();//出现错误断开客户端连接。
        }
    }            
}

启动服务器和客户端显示连接成功,测试收发消息。

注意接收消息成功之后,如果需要返回给客户端消息,必须要返回给特定的客户端,不能搞错。需要通过ClientManager写一个字典管理所有Client,或者直接在Client里面写一个tempClient

public Client tempClient;//缓存客户端发送消息
public async void Accept()
    {
        try
        {
            //监听TCP客户端连接的返回的对象
            TcpClient tcpClient = await tcpListener.AcceptTcpClientAsync();
            Console.WriteLine("客户端已连接:" + tcpClient.Client.RemoteEndPoint);//打印IP
                                                                           //使用构建的Client类来缓存这些TcpClient
            Client client = new Client(tcpClient);
            tempClient = client;
            Accept();//继续接受来自客户端的连接即可
        }
        catch (Exception e)
        {
            Console.WriteLine($"Accept:{e.Message}");
            tcpListener.Stop();
        }
    }
static void Main(string[] args)
{
    TCPServer tcpServer = new TCPServer();
    tcpServer.Start();

    while(true)
    {
        var str = Console.ReadLine();//监听用户输入,不写这句话运行测试会立刻跳出
        tcpServer.tempClient.Send(Encoding.UTF8.GetBytes($"测试主动发送数据:{str}"));
    }
}

3.使用LitJSON更好的组织网络数据

在Server项目的VS中打开NuGet包管理器安装LitJson

在PM输入Install-Package LitJson -Version 0.18.0

每个包都由其所有者许可给你。NuGet 不负责第三方包,也不授予其许可证。一些包可能包括受其他许可证约束的依赖关系。单击包源(源) URL 可确定任何依赖关系。

程序包管理器控制台主机版本 5.10.0.7240

键入 "get-help NuGet" 可查看所有可用的 NuGet 命令。

PM> Install-Package LitJson -Version 0.18.0


正在尝试收集与目标为“.NETFramework,Version=v4.7.2”的项目“Server”有关的包“LitJson.0.18.0”的依赖项信息
收集依赖项信息花费时间 1.81 sec
正在尝试解析程序包“LitJson.0.18.0”的依赖项,DependencyBehavior 为“Lowest”
解析依赖项信息花费时间 0 ms
正在解析操作以安装程序包“LitJson.0.18.0”
已解析操作以安装程序包“LitJson.0.18.0”
从“nuget.org”检索包“LitJson 0.18.0” 
GET https://api.nuget.org/v3-flatcontainer/litjson/0.18.0/litjson.0.18.0.nupkg
OK https://api.nuget.org/v3-flatcontainer/litjson/0.18.0/litjson.0.18.0.nupkg 137 毫秒
已通过内容哈希 zVK/1iUURxvEZH1eiviFpS8Qbh1fZe926ie18vhHuisVYcNbdVjgG85X8BsGt7WBU5Ka3gZvaQXRX6EsoD2hrw== 从 https://api.nuget.org/v3/index.json 安装 LitJson 0.18.0 。
正在将程序包“LitJson.0.18.0”添加到文件夹“E:\graduate\learn\Unity\NetProject\Server\packages”
已将程序包“LitJson.0.18.0”添加到文件夹“E:\graduate\learn\Unity\NetProject\Server\packages”
已将程序包“LitJson.0.18.0”添加到“packages.config”
已将“LitJson 0.18.0”成功安装到 Server
执行 nuget 操作花费时间 8.44 sec
已用时间: 00:00:11.4081423

创建Helper/JsonHelper.cs

using System.Text;

using LitJson;

namespace Server.Helper
{
internal class JsonHelper
    {
        //Object转换为String
        public static string ToJson(object x)//object C#所有类派生于此,所以C#类及自己创建的类都有ToString()
        {
            string str = JsonMapper.ToJson(x);
            return str;
        }

        //String转换为Object
        public static T ToObject<T>(string x)
        {
            return JsonMapper.ToObject<T>(x);
        }

        public static T ToObject<T>(byte[] b)
        {
            string x = Encoding.UTF8.GetString(b, 0, b.Length);
            return ToObject<T>(x);
        }

        public static string GetTestToString()
        {
            JsonTest jsonTest = new JsonTest();
            jsonTest.id = 1;
            jsonTest.name = "jsonTest";
            return ToJson(jsonTest);
        }
    }

    public class JsonTest
    {
        public int id;
        public string name;
    }
}
while(true)
{
    var str = Console.ReadLine();//监听用户输入,不写这句话运行测试会立刻跳出
    //tcpServer.tempClient.Send(Encoding.UTF8.GetBytes($"测试主动发送数据:{str}"));
    var jsonStr = JsonHelper.GetTestToString();
    tcpServer.tempClient.Send(Encoding.UTF8.GetBytes(jsonStr));
}

运行测试,,目前Unity收到的消息还没有反序列化成原本的Object,因为服务器目前发送的是Json格式的String,Unity收到的也是Json格式的String

Unity Client项目创建Scripts/Helper/JsonHelper.cs,然后找到\NetProject\Server\packages\LitJson.0.18.0\lib\net45\LitJson.dll复制进Unity的Plugins文件夹,,,,重开一下脚本就重新编译好了,不会报错LitJson缺失的问题了

using System.Text;
using LitJson;
using UnityEngine;

public class JsonHelper
{
    //Object转换为String
    public static string ToJson(object x)//object C#所有类派生于此,所以C#类及自己创建的类都有ToString()
    {
        string str = JsonMapper.ToJson(x);
        return str;
    }

    //String转换为Object
    public static T ToObject<T>(string x)
    {
        return JsonMapper.ToObject<T>(x);
    }

    public static T ToObject<T>(byte[] b)
    {
        string x = Encoding.UTF8.GetString(b, 0, b.Length);
        return ToObject<T>(x);
    }

    public static string GetTestToString()
    {
        JsonTest jsonTest = new JsonTest();
        jsonTest.id = 1;
        jsonTest.name = "jsonTest";
        var jsonStr = ToJson(jsonTest);

        var jsonTestObj = ToObject<JsonTest>(ToJson(jsonTest));
        Debug.Log($"{jsonTestObj.id}   /  {jsonTestObj.name}");
        return jsonStr;
    }
    
    public class JsonTest
    {
        public int id;
        public string name;
    }
}
//接收接口
public async void Receive()
{
    while (client.Connected)
    {
        try
        {
            byte[] buff = new byte[4096];
            int length = await client.GetStream().ReadAsync(buff, 0, buff.Length);
            if (length > 0)
            {
                Debug.Log($"接收到的数据长度:{length}");
                Debug.Log($"接收到的数据内容:{Encoding.UTF8.GetString(buff, 0, length)}");
                var jsonTest = JsonHelper.ToObject<JsonHelper.JsonTest>(Encoding.UTF8.GetString(buff, 0, length));
                Debug.Log(jsonTest.id + " " + jsonTest.name);
            }
            else
            {
                client.Close();
            }
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
            client.Close();
        }
    }
}

4.粘包和拆包处理

粘包是因为计算机底层将多条小的数据合并成一条大的发送给另一端,减少网络传输的基本开销,需要拆包将多条数据拆分

用4个字节表示包体大小和消息ID,包体内容可能是账号、密码、业务所需要用到的数据(邮箱、移动(方向轴 目标点))

接收到消息看长度是单条消息没接收完还是一下粘包接受了多条消息。

先看Server端的Client

internal class Client
{
    TcpClient client;
    public Client(TcpClient tcpClient)
    {
        client = tcpClient;
        Receive();
    }

    byte[] data = new byte[4096];//接收消息的缓冲区
    int msgLength = 0;//接收到的消息长度

    //接收消息
    public async void Receive()
    {
        //处于连接状态就持续接收信息。
        while(client.Connected)
        {
            try
            {
                byte[] buffer = new byte[4096];//存储接收到的数据,定义初始容量,一般不超过4096或1024
                //把消息存到buffer中,从第几个字节开始存,存多长,,,,,,返回length表示接收到多少数据。
                int length = await client.GetStream().ReadAsync(buffer, 0, buffer.Length);

                if(length > 0)//表示有效信息
                {
                    //$作用是将{}内容当做表达式
                    Console.WriteLine($"接收到的数据长度:{length}");
                    Console.WriteLine($"接收到的数据内容:{Encoding.UTF8.GetString(buffer, 0, length)}");//将byte数组转换为字符串
                    //Send(Encoding.UTF8.GetBytes("测试返回..."));                    
                    Array.Copy(buffer, 0, data, msgLength, length);//把接收到length长度的消息复制到数据缓冲区中msgLength索引后的位置
                    msgLength += length;//每次收到网络数据都要加上数据的长度
                    Handle();
                }
                else
                {
                    //客户端关闭了
                    client.Close();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine($"Receive Error:{e.Message}");
                client.Close();//出现错误断开客户端连接。
            }
        }            
    }

    private void Handle()
    {
        //包体大小(4) 协议ID(4) 包体(byte[])
        if (msgLength >= 8)
        {
            byte[] _size = new byte[4];
            Array.Copy(data, 0, _size, 0, 4);//把包体大小从第0位缓存4位长度
            int size = BitConverter.ToInt32(_size, 0);//获得包体大小

            //本次要拿的长度
            var _length = 8 + size;//实际完整消息的长度:包体大小(4)+协议ID(4)+包体(byte[])

            if (msgLength>=_length)//判断数据缓冲区的长度是否大于一条完整消息的长度。
            {
                //拿出id
                byte[] _id = new byte[4];
                Array.Copy(data, 4, _id, 0, 4);//把协议ID从第4位缓存4位长度
                int id = BitConverter.ToInt32(_id, 0);//获得协议ID

                //包体
                byte[] body = new byte[size];
                Array.Copy(data, 8, body, 0, size);//把包体从第8位缓存size位长度

                if (msgLength>_length)//如果接收到的数据长度大于这次取出的完整一条数据的长度,说明还有数据
                {
                    for (int i = 0; i < msgLength - _length; i++)
                    {
                        data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                    }
                }
                msgLength -= _length;//减掉已经取完的消息长度
                Console.WriteLine($"收到客户端请求:{id}");
                //根据id进行处理,,实际项目一般使用观察者模式,监听id和Action事件绑定
                switch (id)
                {
                    case 1001://注册请求
                        RegisterMsgHandle(body);
                        break;
                    case 1002://登录业务
                        LoginMsgHandle(body);
                        break;
                    case 1003://聊天业务
                        ChatMsgHandle(body);
                        break;

                }
            }
        }
    }

    //处理注册请求
    private void RegisterMsgHandle(byte[] obj)
    {
        
    }

    //处理登录请求
    private void LoginMsgHandle(byte[] obj)
    {

    }

    //处理聊天请求
    private void ChatMsgHandle(byte[] obj)
    {

    }

    //发送消息
    public async void Send(byte[] data)
    {
        try
        {
            //数据写入网络数据流中。就是发送
            await client.GetStream().WriteAsync(data, 0, data.Length);
            Console.WriteLine("发送成功! " + $"发送的消息内容:{Encoding.UTF8.GetString(data, 0, data.Length)}");
        }
        catch (Exception e)
        {
            client.Close();//关闭客户端
            Console.WriteLine($"send error:{e.Message}");
        }
    }
}

根据id进行处理,,实际项目一般使用观察者模式,监听id和Action事件绑定

Unity客户端添加Scritps/Helper/MessageHelper.cs脚本

public class MessageHelper
{
    private static MessageHelper instance = new MessageHelper();
    public static MessageHelper Instance => instance;//单例
    
    byte[] data = new byte[4096];//接收消息的缓冲区
    int msgLength = 0;//接收到的消息长度

    //client接收到消息时,把buffer的数据复制到data数据缓冲区,数据长度加上接受的新有效数据流长度,handle处理数据
    public void CopyToData(byte[] buffer, int length)
    {
        Array.Copy(buffer, 0, data, msgLength, length);
        msgLength += length;
        Handle();
    }
    
    private void Handle()
    {
        //包体大小(4) 协议ID(4) 包体(byte[])
        if (msgLength >= 8)
        {
            byte[] _size = new byte[4];
            Array.Copy(data, 0, _size, 0, 4);//把包体大小从第0位缓存4位长度
            int size = BitConverter.ToInt32(_size, 0);//获得包体大小

            //本次要拿的长度
            var _length = 8 + size;//实际完整消息的长度:包体大小(4)+协议ID(4)+包体(byte[])

            if (msgLength>=_length)//判断数据缓冲区的长度是否大于一条完整消息的长度。
            {
                //拿出id
                byte[] _id = new byte[4];
                Array.Copy(data, 4, _id, 0, 4);//把协议ID从第4位缓存4位长度
                int id = BitConverter.ToInt32(_id, 0);//获得协议ID

                //包体
                byte[] body = new byte[size];
                Array.Copy(data, 8, body, 0, size);//把包体从第8位缓存size位长度

                if (msgLength>_length)//如果接收到的数据长度大于这次取出的完整一条数据的长度,说明还有数据
                {
                    for (int i = 0; i < msgLength - _length; i++)
                    {
                        data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                    }
                }
                msgLength -= _length;//减掉已经取完的消息长度
                Debug.Log($"收到客户端请求:{id}");
                //根据id进行处理,,实际项目一般使用观察者模式,监听id和Action事件绑定
                switch (id)
                {
                    case 1001://注册请求
                        RigisterMsgHandle(body);
                        break;
                    case 1002://登录业务
                        LoginMsgHandle(body);
                        break;
                    case 1003://聊天业务
                        ChatMsgHandle(body);
                        break;

                }
            }
        }
    }

    //按格式封装消息,发送到服务器
    public void SendToServer(int id, string str)
    {
        Debug.Log("ID:" + id);
        var body = Encoding.UTF8.GetBytes(str);
        byte[] send_buff = new byte[body.Length + 8];

        int size = body.Length;

        var _size = BitConverter.GetBytes(size);
        var _id = BitConverter.GetBytes(id);

        Array.Copy(_size, 0, send_buff, 0, 4);
        Array.Copy(_id, 0, send_buff, 4, 4);
        Array.Copy(body, 0, send_buff, 8, body.Length);

        Client.Instance.Send(send_buff);
    }
    
    //处理注册(结果)请求
    private void RigisterMsgHandle(byte[] obj)
    {
            
    }

    //处理登录(结果)请求
    private void LoginMsgHandle(byte[] obj)
    {

    }

    //处理聊天(转发)请求
    private void ChatMsgHandle(byte[] obj)
    {

    }
}
public async void Receive()
{
    while (client.Connected)
    {
        try
        {
            byte[] buff = new byte[4096];
            int length = await client.GetStream().ReadAsync(buff, 0, buff.Length);
            if (length > 0)
            {
                Debug.Log($"接收到的数据长度:{length}");
                //接收到处理CopyToData给MessageHelper处理信息
                MessageHelper.Instance.CopyToData(buff, length);
            }
            else
            {
                client.Close();
            }
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
            client.Close();
        }
    }
}

发送消息的时候必须按照格式发送

//按格式封装后,发送消息
public void SendToClient(int id, string str)
{
    //包体转换为byte[]
    var body = Encoding.UTF8.GetBytes(str);

    //包体大小(4) 协议ID(4) 包体内容
    byte[] send_buff = new byte[body.Length + 8];

    int size = body.Length;
    var _size = BitConverter.GetBytes(size);
    var _id = BitConverter.GetBytes(id);

    Array.Copy(_size, 0, send_buff, 0, 4);
    Array.Copy(_id, 0, send_buff, 4, 4);
    Array.Copy(body, 0, send_buff, 8, body.Length);

    Send(send_buff);
}

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

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

相关文章

界面组件DevExtreme v22.2亮点——UI模板库升级换代!

DevExtreme拥有高性能的HTML5 / JavaScript小部件集合&#xff0c;使您可以利用现代Web开发堆栈&#xff08;包括React&#xff0c;Angular&#xff0c;ASP.NET Core&#xff0c;jQuery&#xff0c;Knockout等&#xff09;构建交互式的Web应用程序。从Angular和Reac&#xff0c…

高频链表算法

1.从尾到头打印链表值 输入一个链表的头节点&#xff0c;从尾到头反过来返回每个节点的值(用数组返回) 思路 &#xff08;1&#xff09;如果使用数组来保存反转之后的链表数据&#xff0c;这样只需要使用到队列或栈的知识&#xff0c;关键是unshif和push,reverse函数 &…

【vue2】vuex基础与五大配置项

&#x1f973;博 主&#xff1a;初映CY的前说(前端领域) &#x1f31e;个人信条&#xff1a;想要变成得到&#xff0c;中间还有做到&#xff01; &#x1f918;本文核心&#xff1a;vuex基础认识、state、getters、mutations actions、modules使用 目录(文末原素材) 一、…

【JavaEE初阶】第九节.多线程 (基础篇)定时器(案例三)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 前言 一、定时器概述、 二、定时器的实现 2.1 Java标准库 定时器的使用 2.2 自己模拟实现一个定时器 2.3 对自己实现的定时器的进一步优化 2.3.1 为何需要再进行优化 2…

CMOS图像传感器——了解光圈

在之前有提到传感器英寸,也提到了曝光三要素之一的ISO,这里主要说明另外一个曝光三要素——光圈。在本文中,我们将介绍光圈及其工作原理。 一、什么是光圈 光圈可以定义为镜头中的开口,光线通过该开口进入相机。类比眼睛是的工作原理,就容易理解了:当人在明亮和黑暗的环…

【链表之单链表】

前言&#xff1a;链表是什么&#xff1f; 链表的操作 1.单链表的结构 2.头文件的包含 3.动态申请一个节点 4.单链表打印 5.单链表尾插 6.单链表头插 7.单链表尾删 8.单链表头删 9.单链表查找 10.单链表在pos位置之后插入x 11.单链表在pos位置之前插入x 12. 单链表…

【数据挖掘】基于粒子群算法优化支持向量机PSO-SVM对葡萄酒数据集进行分类

1.粒子群算法的概念 PSO是粒子群优化算法&#xff08;Particle Swarm Optimization&#xff09;的英文缩写&#xff0c;是一种基于种群的随机优化技术&#xff0c;由Eberhart和Kennedy于1995年提出。粒子群算法是模仿昆虫、兽群、鸟群和鱼群等的群集行为&#xff0c;这些群体按…

中国电子学会2021年03月份青少年软件编程Scratch图形化等级考试试卷三级真题(含答案)

2021-03Scratch三级真题 分数&#xff1a;100题数&#xff1a;38 一、单选题(共25题&#xff0c;每题2分&#xff0c;共50分) 1.在《采矿》游戏中&#xff0c;当角色捡到黄金时财富值加1分&#xff0c;捡到钻石时财富值加2分&#xff0c;下面哪个程序实现这个功能&#xff1…

【软件测试】资深测试总结的测试必备8点,堪称测试人的好莱坞大片......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 测试8板斧。测试8板…

Task8:Excel的数据可视化

目录一 条形图二 条件单元格格式三 迷你图四 练习题一 条形图 【例子】直观的展示销售额之间的差别 方法&#xff1a;【开始】–>【条件格式】–>【数据条】 【只想显示条形图&#xff0c;不想显示金额】 1.条形图区域—>条件格式—>管理规则 2.选择设置的规则&a…

单应性Homography梳理,概念解释,传统方法,深度学习方法

Homography 这篇博客比较清晰准确的介绍了关于刚性变换&#xff0c;仿射变换&#xff0c;透视投影变换的理解 单应性变换 的 条件和表示 用 [无镜头畸变] 的相机从不同位置拍摄 [同一平面物体] 的图像之间存在单应性&#xff0c;可以用 [透视变换] 表示 。 opencv单应性变换求…

Active Directory计算机备份和恢复

在Active Directory&#xff08;AD&#xff09;环境中&#xff0c;用户通过域中的计算机认证他们自身。从AD中删除这些计算机账户时&#xff0c;系统也会自动从域中删除它们。于是&#xff0c;用户不能再通过些计算机登录网络。为允许用户访问域资源&#xff0c;必须恢复这些已…

聚集千百个企业管理系统的API资产,打造API资产全生命周期一站式集成体验

API——接口&#xff0c;作为软件世界中的连接服务和传输数据的重要管道&#xff0c;已经成为数字时代的新型基础设施&#xff0c;是各领域驱动数字变革的重要力量之一。传统企业集成主要采用点对点或ESB集成方式&#xff0c;基于全新API战略中台的API新型集成方式通过解耦系统…

SpringBoot跨域请求解决方案详细分析

跨域的定义 跨域是指不同域名之间的相互访问&#xff0c;这是由浏览器的同源策略决定的&#xff0c;是浏览器对JavaScript施加的安全措施&#xff0c;防止恶意文件破坏。同源策略&#xff1a;同源策略是一种约定&#xff0c;它是浏览器最核心的也是最基本的安全策略&#xff0…

【数据产品】缓存设计

背景&#xff1a;为什么需要做缓存&#xff1f; 我所做的产品的指标设计越来越复杂&#xff0c;查询性能也随之下降。因此需要增加缓存层&#xff0c; 以提高接口查询效率。 哪些层需要做缓存&#xff1f; 随着指标系统的应用&#xff0c;该产品的查询逻辑也越来越简单&…

二分查找核心思路--单调性--极值

在最初的二分查找中&#xff0c;我们将一组数据按大小排序&#xff0c;然后根据arr[mid]与要查找的k的大小比较&#xff0c;从而每次去掉一半的数字&#xff0c;使时间复杂度简化为O&#xff08;logN&#xff09;。 排序本质上是让数据的单调性统一&#xff0c;变为单增或单减…

spring中的JSR-303统一校验

1.在前后端的传输参数的过程中数据在何处校验? 在前后端都需要进行校验,只是分工不同. 2.各个层的校验内容: 1.Controller层主要负责校验残水的合法性,包括: 必填的参数字段,数据格式的校验 2.Service层的业务校验是审核业务中的规则的相关内容,比如:课程已经审核通过所以提…

vue3 为何比 vue2 快

vue3 为何比 vue2 快 测试环境&#xff1a;https://vue-next-template-explorer.netlify.app/ 1、proxy 响应式 vue3 优缺点&#xff1a; 深度监听性能更好可监听 新增 / 删除 属性可监听数组变化Proxy 能规避 Object.defineProxy 的问题Proxy 无法兼容所有浏览器&#xff…

OAuth2介绍

目录 一、什么是OAuth2 二、OAuth2中的角色 三、认证流程 四、令牌的特点 五、OAuth2授权方式 授权码 隐藏方式 密码方式 凭证方式 一、什么是OAuth2.0 概念&#xff1a;第三方授权解决方案 OAuth2.0是目前使用非常广泛的授权机制&#xff0c;用于授权第三方应用获取…

[NRF52] mesh DFU

mesh DFU升级过程&#xff1a;完整流程&#xff1a;以前nRF SDK DFU的实现是通过nRF51 Dongle配合主机nRF connect工具&#xff0c;且借助Secure DFU的后台式更新速率较快&#xff08;见另一篇笔记&#xff09;&#xff0c;现在的nRF mesh DFU分角色&#xff0c;全都由DK充当&a…