MMO移动同步(1)

news2024/9/20 7:51:04

多个客户端同时连入游戏

这篇会从以下五个部分讲解:

同步的基本概念

完善角色进入及离开处理

CharacterManager(C/S)

EntityManager(C/S)

打包运行Win客户端

同步基本概念

同步:角色信息,位置,状态同步;客户端和服务端的数据是一致的;//只有网络游戏有同步,单机游戏没有

两种同步对比

详情请看:两种同步模式

状态同步帧同步
消息传输量
回放较难还原容易
安全性较多逻辑在服务器,安全主要逻辑在客户端,难以避免外挂
战斗校验较难精确校验服务器可进行完整战斗模拟
服务器压力重逻辑,大转发为主,小
网络卡顿表现瞬移,闪回,血量不一致,各种异常战斗卡顿
断线重连无负担游戏时长越长恢复压力越大
实现难点客户端需要做一些插值,或者行为预测等方式来优化卡顿体验。较多的逻辑要在服务器实现,调测压力较大需要规避辉昂宿问题,逻辑要与表现进行分离,对设计有一定要求

消息传输量:帧同步要定时的发送消息,需要高帧率(15b/s)

//做帧同步为回合制下的,一个回合发送一帧,也是合理的

战斗回放:MMO很难做到,RTS容易做到//RTS:是实时游戏而不采回合制

战斗校验:状态同步:每个状态的发送其他人收到是有延迟的

网络卡顿表现:帧同步:一个人掉线所有人都在等(必须保证每个人的战斗状态是一致的,不能模拟某个人)//有的游戏会在玩家掉线时委托AI承担游戏行为,并断线重连

以下的讲解的同步都是状态同步

同步什么?

 //扩展数据:只有打开某些界面,才会请求这些详细信息时,才把信息拉过来;正常情况下信息不会同步

逻辑图

打包发布准备

点击右下角Build ;创建一个Bin文件夹

等待把Window包打包出来

打包完这样:

双击启动,服务端也启动

发现卡住不动了,为了看到错误信息,要在运行目录下配置日志;

把日志复制到运行目录下

 我们可以在这里查看日志文件

显示找不到地图配置表

因为我们的Bin下面根本没有Data目录 ;从客户端拷进来

 //这个时候可以成功进入,并运行到我们恰好写到的小地图系统

角色离开逻辑

在UI界面有个离开按钮

给它添加逻辑

打开UIMainCity.cs//即前面这个框对应的逻辑所在的脚本

BackToCharSelect()

加载角色选择场景;

告诉服务器角色离开;

//离开服务器可以换个角色重新进入

public void BackToCharSelect()
{
    SceneManager.Instance.LoadScene("CharSelect");
    UserService.Instance.SendGameLeave();
}

把事件绑在按钮上

Client:UserService:SendGameLeave
public void SendGameLeave()
{
    Debug.Log("UserGameLeaveRequest");
    NetMessage message = new NetMessage();
    message.Request = new NetMessageRequest();
    message.Request.gameLeave = new UserGameLeaveRequest();
    NetClient.Instance.SendMessage(message);
}

Client:OnGameLeave

 void OnGameLeave(object sender, UserGameLeaveResponse response)
        {
            MapService.Instance.CurrentMapId = 0;
            User.Instance.CurrentCharacter = null;
            Debug.LogFormat("OnGameLeave:{0} [{1}]", response.Result, response.Errormsg);
        }
//Add:鼠标右键调摄像机视角(未做

GameServer:UserService:OnGameLeave

订阅UserGameLeaveRequest协议下的OnGameLeave

在UserService()中加上

            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<UserGameLeaveRequest>(this.OnGameLeave);

OnGameLeave:

//离开时:RemoveCharacter;MapManagerLeave

//进入地图时:AddCharacter;MapMangaer.CharacterEnter

准备信息发给客户端

 void OnGameLeave(NetConnection<NetSession> sender, UserGameLeaveRequest request)
 {
//由Sender传入一个character
     Character character = sender.Session.Character;
     Log.InfoFormat("UserGameLeaveRequest:characterID:{0}:{1} Map:{2}",character.Id,character.Info.Name,character.Info.mapId);
     //收到了游戏的请求,从游戏管理器中把角色移掉
     CharacterManager.Instance.RemoveCharacter(character.Id);
     MapManager.Instance[character.Info.mapId].CharacterLeave(character.Info);
     NetMessage message=new NetMessage();
     message.Response = new NetMessageResponse();
     message.Response.gameLeave=new UserGameLeaveResponse();
     message.Response.gameLeave.Result = Result.Success;
     message.Response.gameLeave.Errormsg = "None";

     byte[] data=PackageHandler.PackMessage(message);
     sender.SendData(data,0,data.Length);

 }

转到定义MapManager:Map

Map:CharacterLeave

输出哪个角色离开了哪张地图

把角色从MapCharacters字典中移除

给所有还在地图中的角色广播此角色离开的信息SendCharacterLeaveMap

internal void CharacterLeave(NCharacterInfo cha)
{
    //离开时输出日志,哪个角色离开了那张地图
    Log.InfoFormat("CharacterLeave: Map:{0} characterId:{1}", this.Define.ID, cha.Id);
    this.MapCharacters.Remove(cha.Id);

    foreach (var kv in this.MapCharacters)
    {
        this.SendCharacterLeaveMap(kv.Value.connection, cha);
    }
}
SendCharacterLeaveMap:

把角色Id传入,此角色离开地图

//此信息发到服务器窗口上

private void SendCharacterLeaveMap(NetConnection<NetSession> conn,NCharacterInfo character)
{
    NetMessage message = new NetMessage();
    message.Response = new NetMessageResponse();

    message.Response.mapCharacterLeave = new MapCharacterLeaveResponse();
    message.Response.mapCharacterLeave.characterId = character.Id;

    byte[] data=PackageHandler.PackMessage(message);
    conn.SendData(data,0,data.Length);

}
演示

测试依赖windows端

每次发布之前删掉ExtremeWorld_Data,ExtrememWorld.exe,这两个

会发现刚刚删掉的那两个,又生成了新的

 //Windows运行时;可以用Unity运行查看报错

//两边可以登录不同的账号

启动服务器:

运行Windows端

 直接选择现有的角色

 

点击进入游戏

打开Unity登录另一个账号

 点击创建角色

点击开始冒险

 跳转到选择角色的面板:

选择新建的角色,点击进入游戏

这里不知道为啥被销毁了//

可以移动新疆炒米粉

回到Windows窗口

现在只能操控新疆炒米粉了

在unity的界面点击返回角色选择的按钮

选择懒羊羊大王

再进入游戏

这里是因为退出去有重新选择角色进入,但是原先的角色没有被销毁

Windows上操作,发现移动的依然是新疆炒米粉

出现多个角色,离开逻辑未完善

Client:MapService:OnMapCharacterLeave()

如果有玩家离开了地图,判断当前的玩家是否是自己

是:所有的角色都要销毁掉(因为我已经不再地图中了)

不是:把哪个角色 移除(保证角色管理器的角色都是再地图中的

private void OnMapCharacterLeave(object sender, MapCharacterLeaveResponse response)
{
    Debug.LogFormat("OnMapCharacterLeave: CharID:{0}", response.characterId);
    if (response.characterId != User.Instance.CurrentCharacter.Id)
        CharacterManager.Instance.RemoveCharacter(response.characterId);
    else
        CharacterManager.Instance.Clear();

}

在Character Manager中需要监听角色离开;

CharacterManager:RemoveCharacter

在角色离开的位置RemoveCharacter做通知;

 在现有的角色列表中找到离开的角色

角色离开告知OnCharacterLeave

public void RemoveCharacter(int characterId)
{
    Debug.LogFormat("RemoveCharacter:{0}", characterId);
   //this.Characters.Remove(characterId);
   if(this.Characters.ContainsKey(characterId))
    {
        //EntityManager.Instance.RemoveEntity(this.Characters[characterId].Info.Entity);
        if(OnCharacterLeave!=null)
            OnCharacterLeave(this.Characters[characterId]);
        this.Characters.Remove(characterId);
    }
}
 GameObjectManager:OnCharacterLeave
游戏对象管理器GameObjectManager负责游戏对象的进入;和离开

销毁掉角色;

//判断是否在角色字典中;因为游戏对有时候是在其他地方被删掉的;所以要判空

void OnCharacterLeave(Character character)
{
    if (!Characters.ContainsKey(character.entityId))
        return;
    if (Characters[character.entityId] != null)
    {
        Destroy(Characters[character.entityId]);
        this.Characters.Remove(character.entityId);
    }
}
把GameObjectManager改成单例类
关于切换场景时游戏对象被销毁;

用单例,单例是全局的;

单例类里面有逻辑保证单例是不被销毁的

这个,改成这个

!!注意使用给单例类必须把Start函数给重载掉

//变成单例类,Start时:+

Destroy时:-

protected override void OnStart()
{
    //管理器在主城加载完之后启动
    StartCoroutine(InitGameObjects());
    //订阅了角色进入的事件
    CharacterManager.Instance.OnCharacterEnter += OnCharacterEnter;
    CharacterManager.Instance.OnCharacterLeave += OnCharacterLeave;
}

private void OnDestroy()
{
    //CharacterManager.Instance.OnCharacterEnter = null;
    CharacterManager.Instance.OnCharacterEnter -= OnCharacterEnter; 
    CharacterManager.Instance.OnCharacterLeave -= OnCharacterLeave; 
}
 所有单例类创建的角色要在它的子节点下

修改这个里的实例化的位置:

GameObject go = (GameObject)Instantiate(obj);

改成:

GameObject go = (GameObject)Instantiate(obj,this.transform);

//关于entityId

前面使用DBid是因为数据库里面只有角色没有怪物;到后面有怪物时就不能用DBId;因此再这里用EntityId

entityId是一种在内存中出现的每次进入游戏不同的Id;用来标识唯一性;

//GameObjectManager:

 改成这样:

 //对于某些会反复使用的代码;我们把CreateCharacterObject抽出一些代码做成InitGameObject

原来的CreateCharacterObject

private void CreateCharacterObject(Character character)
{
    if (!Characters.ContainsKey(character.entityId) || Characters[character.entityId] == null)
    {
        Object obj = Resloader.Load<Object>(character.Define.Resource);
        if(obj == null)
        {
            Debug.LogErrorFormat("Character[{0}] Resource[{1}] not existed.",character.Define.TID, character.Define.Resource);
            return;
        }
        GameObject go = (GameObject)Instantiate(obj,this.transform);
        go.name = "Character_" + character.entityId+ "_" + character.Info.Name;

        //角色实体坐标(服务器返回过来的坐标)要转换成世界坐标
        go.transform.position = GameObjectTool.LogicToWorld(character.position);
        go.transform.forward = GameObjectTool.LogicToWorld(character.direction);
        Characters[character.entityId] = go;

        //以下是绑定了两个对象的脚本,取出来并对它们赋值
        //当然也可以在Start里面get
        EntityController ec = go.GetComponent<EntityController>();
        if (ec != null)
        {
            ec.entity = character;
            ec.isPlayer = character.IsPlayer;
        }
        
        PlayerInputController pc = go.GetComponent<PlayerInputController>();
        if (pc != null)
        {
            if (character.entityId == Models.User.Instance.CurrentCharacter.Id)
            {//如果是当前角色
                User.Instance.CurrentCharacterObject = go;
                MainPlayerCamera.Instance.player = go;
                pc.enabled = true;
                pc.character = character;
                pc.entityController = ec;
            }
            else
            {
                //不是当前玩家禁用角色控制器
                pc.enabled = false;
            }
        }
        UIWorldElementManager.Instance.AddCharacterNameBar(go.transform, character);
    }
}

把要创建角色,游戏体的处理部分做成init;Create在引用它

private void CreateCharacterObject(Character character)
{
    if (!Characters.ContainsKey(character.entityId) || Characters[character.entityId] == null)
    {
        Object obj = Resloader.Load<Object>(character.Define.Resource);
        if(obj == null)
        {
            Debug.LogErrorFormat("Character[{0}] Resource[{1}] not existed.",character.Define.TID, character.Define.Resource);
            return;
        }
        GameObject go = (GameObject)Instantiate(obj,this.transform);
        go.name = "Character_" + character.entityId+ "_" + character.Info.Name;
		Characters[character.entityId] = go;
		UIWorldElementManager.Instance.AddCharacterNameBar(go.transform, character);
	}
	this.InitGameObject(Characters[character.entityId],character);
}

private void InitGameObject(GameObject go,Character character)
{
    //角色实体坐标(服务器返回过来的坐标)要转换成世界坐标
    go.transform.position = GameObjectTool.LogicToWorld(character.position);
    go.transform.forward = GameObjectTool.LogicToWorld(character.direction);
        

        //以下是绑定了两个对象的脚本,取出来并对它们赋值
        //当然也可以在Start里面get
        EntityController ec = go.GetComponent<EntityController>();
        if (ec != null)
        {
            ec.entity = character;
            ec.isPlayer = character.IsPlayer;
        }
        
        PlayerInputController pc = go.GetComponent<PlayerInputController>();
        if (pc != null)
        {
            if (character.entityId == Models.User.Instance.CurrentCharacter.Id)
            {//如果是当前角色
                User.Instance.CurrentCharacterObject = go;
                MainPlayerCamera.Instance.player = go;
                pc.enabled = true;
                pc.character = character;
                pc.entityController = ec;
            }
            else
            {
                //不是当前玩家禁用角色控制器
                pc.enabled = false;
            
            }
        
    }
}

//原先是角色存在时做初始化逻辑;若角色进入又离开(会被删除掉)但是entityId有可能会在多次创建中删除中出现重复的;那么原先的if条件下的角色的属性就不能创建

//可能会导致角色切换回来的时候找不到原来的角色

EntityManager
GameServer:EntityManager
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Common;
using SkillBridge.Message;
using GameServer.Entities;
namespace GameServer.Managers
{
    class EntityManager:Singleton<EntityManager>
    {
        private int idx = 0;
        //用一个类似的类型维护所有entity 的列表
        public List<Entity> AllEntities=new List<Entity>();
        //某个地图的entity有哪些//一个地图id对应一张存实体的列表
        public Dictionary<int,List<Entity>> MapEntities=new Dictionary<int, List<Entity>>();

        public void AddEntity(int mapId,Entity entity)
        {
            AllEntities.Add(entity);
            //每个entity都有唯一的idx//类似与数组模拟链表的索引
            entity.EntityData.Id=++this.idx;

            List<Entity> entities = null;
            if(!MapEntities.TryGetValue(mapId,out entities))
            {//判断mapid是那张地图
                //没有地图,加进去
                entities = new List<Entity>();
                MapEntities[mapId] = entities;
            }
            entities.Add(entity);
        }
        public void RemoveEntity(int mapId,Entity entity)
        {
            //从总实体列表和地图实体列表移除
            this.AllEntities.Remove(entity);
            this.MapEntities[mapId].Remove(entity); 
        }
    }
}
Client:EntityManager
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Entities;
using JetBrains.Annotations;
using SkillBridge.Message;
namespace Managers
{
    interface IEntityNofity
    {
        void OnEntityRemoved();
    }
    class EntityManager:Singleton<EntityManager>
    {
        Dictionary<int,Entity> entities = new Dictionary<int,Entity>();

        //用接口来实现一个事件:例如:要通知entity removed;
        //好处:一个接收者可以接收多种事件
        Dictionary<int,IEntityNofity>notifiers=new Dictionary<int, IEntityNofity>();   

        //在这里注册,它接收通知(删除entity的通知)
        public void RegisterEntityChangeNotify(int entityId,IEntityNofity nofity)
        {
            this.notifiers[entityId] = nofity;
        }
        public void AddEntity(Entity entity)
        {
            entities[entity.entityId] = entity;
        }

        public void RemoveEntity(NEntity entity)
        {
            this.entities.Remove(entity.Id);    
            if(notifiers.ContainsKey(entity.Id))
            {
                notifiers[entity.Id].OnEntityRemoved();
                notifiers.Remove(entity.Id);
            }    
        }
    }
}

//这里面 的通知主要是用来通知Entity Controller ;在EntityController派生接口

并实现接口:

public void OnEntityRemoved()
{
    //把血条删掉;把自己删掉
    //服务器告知entity要删除;在这可以接收移动通知
    if (UIWorldElementManager.Instance != null)
        UIWorldElementManager.Instance.RemoveCharacterNameBar(this.transform);
    Destroy(this.gameObject);
}

在Start时注册:

若entity 不为空,则把方法放在这,并根据id传入RegisterEntityChangeNotify

总结:移动同步

派生接口,实现接口, 把通知注册到管理器中

那么管理器就能通过这个方式调用

//不要关心谁注册,只要知道有人注册,就可以通知

这就是通知机制;保证有人离开后对象可以删除掉

图示:

在Client:CharacterMangaer中的Add Remove Character

添加Entity Manager管理entity

public void AddCharacter(SkillBridge.Message.NCharacterInfo cha)
{
    Debug.LogFormat("AddCharacter:{0}:{1} Map:{2} Entity:{3}", cha.Id, cha.Name, cha.mapId, cha.Entity.String());
    Character character = new Character(cha);
    this.Characters[cha.Id] = character;
    //添加角色时,把角色放进管理器;因为Character是Entity的子类;Character就是Entity
    EntityManager.Instance.AddEntity(character);
    if (OnCharacterEnter != null)
    {
        OnCharacterEnter(character);
    }
}


public void RemoveCharacter(int characterId)
{
    Debug.LogFormat("RemoveCharacter:{0}", characterId);
    //this.Characters.Remove(characterId);
    if (this.Characters.ContainsKey(characterId))
    {
        EntityManager.Instance.RemoveEntity(this.Characters[characterId].Info.Entity);
        if (OnCharacterLeave != null)
            OnCharacterLeave(this.Characters[characterId]);
        //?
        this.Characters.Remove(characterId);
    }
}
Clear()//

删除时要通知给entity;注册事件的和管理器之类

在Clear时,查看当前角色列表Characters都有谁;把它们都remove

public void Clear()
{
    int[] keys=this.Characters.Keys.ToArray();
    foreach (int key in keys)
    {//清除掉;通知事件接收者,角色离开
        this.RemoveCharacter(key);
    }
    this.Characters.Clear();
}
//关于minimap为空

当minmapBoundingBox或playerTransform先被删除了;当前的小地图还没被删除;小地图会为空

add:

if(minmapBoundingBox==null||playerTransform==null) return;

完整的UIMinmap:Update()

void Update()
{
    //if (this.playerTransform == null) playerTransform = MinimapManager.Instance.PlayerTransform;
    
    //组件与组件之间互相引用时,必须检查为空
    if(minmapBoundingBox==null||playerTransform==null) return;

    //Scale.x
    float realWidth = minmapBoundingBox.bounds.size.x;
    float realHeight=minmapBoundingBox.bounds.size.z;

    //这里用玩家相对与世界地图左下角的距离
    float realX = playerTransform.position.x - minmapBoundingBox.bounds.min.x;
    float realY = playerTransform.position.z - minmapBoundingBox.bounds.min.z;

    float pivotX=realX/realWidth;
    float pivotY=realY/realHeight;

    this.minimap.rectTransform.pivot=new Vector2(pivotX,pivotY);
    //minimap相对于父物体mask的位置
    this.minimap.rectTransform.localPosition = Vector2.zero;
    //顺着世界空间角色的y轴旋转;而箭头是xy平面轴的,因此是绕Z轴
    this.arrow.transform.eulerAngles = new Vector3(0, 0, -playerTransform.eulerAngles.y);
}
//关于PlayerInputController的LateUpdate

中的

//在MainPlayerCamera中

在这里;有的玩家初始化时机不同,找不到摄像机(摄像机没有在角色身上:可以修正

修正:把User.Instance.CurrentCharacterObject给player

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

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

相关文章

神仙公司名单(北京篇)

欢迎来到小落科技每日分享频道 大家好&#xff0c;秋招已经火热进行中了&#xff0c;不知道大家准备得怎么样了&#xff1f;特别是咱们25届的小伙伴们&#xff0c;有没有找到心仪的目标&#xff1f; 想必大家最近和我一样&#xff0c;忙着在各种招聘平台上搜罗信息&#xff0c…

如何在 Cursor 中使用驭码CodeRider?

驭码CodeRider 是极狐GitLab 公司自研发布的 AIGC 产品&#xff0c;可以用来进行 AI 编程和 DevOps 流程处理。本文分享如何在 Cursor 中使用驭码CodeRider。 Cursor 是近期比较火爆的一款 AI 代码编辑器&#xff0c;通过将 AI 能力引入软件研发来提升软件研发效率。而驭码Cod…

水凝胶透镜是什么?能用来干啥?

大家好&#xff0c;今天我们来了解一项关于蛋白质驱动的水凝胶透镜的研究——《Toward Tunable Protein‐Driven Hydrogel Lens》发表于《Advanced Science》。我们的眼睛晶状体主要由蛋白质构成&#xff0c;在视觉中起重要作用。但人造光学系统要实现类似功能却不容易。近年来…

【设计文档】数据库设计说明书(Word实际项目案例参考)

一、 总述 &#xff08;一&#xff09; 编写目的 二、 外部设计 &#xff08;一&#xff09; 环境说明 &#xff08;二&#xff09; 指导 三、 物理实现 &#xff08;一&#xff09; 物理结构 &#xff08;二&#xff09; 安全设计 四、 表设计结构 &#xff08;一&am…

【软件文档】软件系统试运行方案、试运行报告(Word项目实际原件)

一、 试运行目的 &#xff08;一&#xff09; 系统功能、性能与稳定性考核 &#xff08;二&#xff09; 系统在各种环境和工况条件下的工作稳定性和可靠性 &#xff08;三&#xff09; 检验系统实际应用效果和应用功能的完善 &#xff08;四&#xff09; 健全系统运行管理体制&…

【数字人】Facevid2vid:用于视频会议的一次性自由视图说话头合成

论文&#xff1a;https://arxiv.org/pdf/2011.15126 github:GitHub - zhanglonghao1992/One-Shot_Free-View_Neural_Talking_Head_Synthesis: Pytorch implementation of paper "One-Shot Free-View Neural Talking-Head Synthesis for Video Conferencing" 一种新颖…

ip地址的管理方法有哪些?是什么

IP地址的管理方法有哪些&#xff1f;随着互联网的快速发展&#xff0c;‌IP地址作为网络设备的唯一标识&#xff0c;‌其管理显得尤为重要。‌有效的IP地址管理不仅可以确保网络的稳定运行&#xff0c;‌还能提高网络资源的利用率。‌本文将深入探讨IP地址的管理方法&#xff0…

网银U盾:财务眼中钉,会计肉中刺!

随着网银U盾的广泛应用&#xff0c;虽然使得财务安全有了大幅提升&#xff0c;但企业财务管理效率却越来越低了。 近期&#xff0c;我们发现&#xff0c;高达85%的企业在采购我们的USB Server时&#xff0c;都是出于网银U盾反复插拔的繁琐、效率低下、管理困难等原因。 想象一…

sqli-labs靶场通关攻略(五十一到六十关)

sqli-labs-master靶场第五十一关 步骤一&#xff0c;尝试输入?sort1 我们发现这关可以报错注入 步骤二&#xff0c;爆库名 ?sort1 and updatexml(1,concat(0x7e,database(),0x7e),1)-- 步骤三&#xff0c;爆表名 ?sort1 and updatexml(1,concat(0x7e,(select group_conc…

CentOS 7 docker 部署遇到内网通,外网不通 问题

CentOS 7 docker 部署遇到内网通&#xff0c;外网不通 问题 [rootlocalhost ~]# systemctl status network ● network.service - LSB: Bring up/down networkingLoaded: loaded (/etc/rc.d/init.d/network; bad; vendor preset: disabled)Active: failed (Result: exit-code) …

多角度解读WMS:探寻仓库管理系统的核心功能

多角度解读 WMS 仓库管理系统 1. 概述 WMS 在数字化工厂中具有举足轻重的地位&#xff0c;它不仅提高了仓储管理的效率与准确性&#xff0c;还能优化整个供应链的管理&#xff0c;支持灵活生产模式&#xff0c;并提供决策支持的关键数据。通过现代前后端技术的架构设计&#xf…

几十块的麦克风能用吗?一文看懂哪个牌子的麦克风好

无论是拍摄短视频、直播还是采访&#xff0c;说一款好的音频设备是非常重要的&#xff0c;它决定了音频质量的高低&#xff0c;如今市面上的麦克风种类也是各式各样的都有&#xff0c;价格上也是参差不齐&#xff0c;有些小伙伴问“几十块的麦克风能用吗”&#xff1f; 我觉得最…

Docker部署项目时的服务端口设置——给容器添加新端口映射

Docker给容器添加新端口映射 1 Docker安装Ubuntu22.042 创建新容器3 给容器添加端口映射3.1 查看运行的容器3.2 查看容器挂载目录3.3 停止容器3.4 停止docker服务3.5 进入容器挂载目录3.6 修改config.v2.json文件3.7 修改hostconfig.json文件3.8 启动docker3.9 启动容器 4 端口…

C语言中volatile与const关键字的深入解析

在C语言编程中&#xff0c;volatile和const是两个非常重要的关键字&#xff0c;它们各自有着独特的用途。本文将深入探讨这两个关键字的工作原理、底层实现机制以及在实际开发中的应用。 volatile关键字 1. 原理与作用 volatile关键字用于告诉编译器&#xff0c;所修饰的变量…

若楠带你初识OpenCV(2)--图片修改、运算,边界填充以及阈值调整

文章目录 OpenCV图片修改1. 图片缩放2. 图片打码3. 图片组合 图像运算1. 直接相加2. add()方法相加3. 权重相加 边界填充阈值调整总结 OpenCV OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是一个开源的计算机视觉和机器学习库&#xff0c;它主要用于实…

Nginx负载均衡数据流分析

1、各机器ip信息 客户端IP&#xff1a;192.168.3.239 Nginx代理服务器IP&#xff1a;192.168.3.241 服务端IP&#xff1a;192.168.3.238 2、架构图&#xff08;略&#xff09; 3、 下图是在服务端上面的抓包分析。 下图是在客户端上面的抓包分析&#xff1a; 下图是在代理服务…

32 配置多路由的静态路由

配置多路由的静态路由 一、多路由器的静态路由配置 ​ 配置网络拓扑配置接口IP地址并通过静态路由的配置实现全网的互通 R0&#xff1a; # 进入特权 Router>enable# 进入全局 Router#configure terminal # 进入接口 Router(config)#interface fastEthernet 0/0# 配置IP R…

如何使用 Mistral 和 Llama2 构建 AI 聊天机器人

开始使用 Mistral 让我们从 Mistral 7B Instruct 的 GGUF 量化版本开始&#xff0c;并使用 AutoClasses ‘AutoModelForCausalLM’ 之一来加载模型。AutoClasses 可以帮助我们自动检索给定模型路径的模型。AudoModelForCausalLM 是具有因果语言建模的模型类之一&#xff0c;这…

SpringBoot如何对接口防刷限流处理

一、API防刷限流&#xff1a; API接口限流&#xff0c;旨在预防用户过度频繁地访问特定接口&#xff0c;以及抵御潜在的恶意攻击行为&#xff0c;这些行为可能导致后端服务器承受过高的负载&#xff0c;进而引发内存资源紧张。为了有效缓解服务器面临的压力&#xff0c;确保服…

数据结构代码集训day15(适合考研、自学、期末和专升本)

本份题目来自B站up&#xff1a;白话拆解数据结构 今日题目如下; &#xff08;1&#xff09;编写算法&#xff0c;实现十进制转十六进制&#xff1b; &#xff08;2&#xff09;汉诺塔&#xff08;Hanoi Tower&#xff09;&#xff0c;又称河内塔&#xff0c;源于印度一个古老…