浅谈多人游戏原理和简单实现。

news2024/11/17 6:38:12
  • 😜           :是江迪呀
  • ✒️本文关键词websocket网络原理多人游戏
  • ☀️每日   一言这世上有两种东西无法直视,一是太阳,二是人心!

在这里插入图片描述

一、我的游戏史

我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿是我挨得最狠的,以至于现在回想起来,屁股还条件反射的隐隐作痛。

后来我骗我妈说我要学习英语、练习打字以后成为祖国的栋梁之才!让她给我买台小霸王学习机(游戏机),在我一哭二闹三上吊胡搅蛮缠的攻势下,我妈妥协了。就此我接触到了FC游戏。现在还能记得我和朋友玩激龟快打,满屋子的小朋友在看的场景。经常有家长在我家门口喊他家小孩吃饭。那时候我们县城里面有商店卖游戏卡,小卡一张5块钱,一张传奇卡25-40块不等(所谓传奇卡就是角色扮演,带有存档的游戏),每天放学都要去商店去看看,有没有新的游戏卡,买不起,就看下封面过过瘾。我记得我省吃俭用一个多月,买了两张卡:《哪吒传奇》和《重装机兵》那是真的上瘾,没日没夜的玩。

再然后我接触到了手机游戏,记得那时候有个软件叫做冒泡游戏(我心目的中的Stream),里面好多游戏,太吸引我了。一个游戏一般都是几百KB,最大也就是几MB,不过那时候流量很贵,1块钱1MB,并且!一个月只有30Mb。我姑父是收手机的,我在他那里搞到了一部半智能手机,牌子我现在还记得:诺基亚N70,那时候我打开游戏就会显示一杯冒着热气的咖啡,我很喜欢这个图标,因为看见它意味着我的游戏快加载完成了,没想到,十几年后我们会再次相遇,哈哈哈哈。我当时玩了一款网游叫做:《幻想三国》,第一回接触网游简直惊呆了,里面好多人都是其他玩的家,这太有趣了。并且我能在我的手机上看到其他玩家,能够看到他们的行为动作,这太神奇了!!!我也一直思考这到底是怎么实现的!

最后是电脑游戏,单机:《侠盗飞车》、《植物大战僵尸》、《虐杀原型》;网游:《DNF》、《CF》、《LOL》、《梦幻西游》我都玩过。

不过那个疑问一直没有解决,也一值留在我心中 —— 在网络游戏中,是如何实时更新其他玩家行为的呢?

二、解惑

在我进入大学后,我选择了软件开发专业,真巧!再次遇到了那个冒着热气的咖啡图标,这时我才知道它叫做——Java。我很认真的去学,希望有一天能够做一款游戏!

参加工作后,我并没有如愿以偿,我成为了一名Java开发程序员,但是我在日常的开发的都是web应用,接触到大多是HTTP请求,它是种请求-响应协议模式。这个问题也还是想不明白,难道每当其他玩家做一个动作都需要发送一次HTTP请求?然后响应给其他玩家。这样未免效率也太低了吧,如果一个服务器中有几千几万人,那么服务器得承受多大压力呀!一定不是这样的!!!

直到我遇到了Websocket,这是一种长连接,而HTTP是一种短连接,顿时这个问题我就想明白了。在此二者的区别我就不过多赘述了,请看我的另一篇文章:WebSocket详解以及应用

知道了这个知识后,我终于能够大致明白了网络游戏的基本原理。原来网络游戏是由客户端服务器端组成的,客户端就是我们下载到电脑或者手机上的应用,而服务器端就是把其他玩家连接起来的中转站,还有一点需要说明的是,网络游戏是分房间的,这个房间就相当于一台服务器。首先,在玩家登陆客户端并选择房间建立长连接后,A玩家做出移动的动作,随即会把这个动作指令上传给服务器,然后服务器再将指令广播到房间中的其他玩家的客户端来操作A的角色,这样就可以实现实时更新其他玩家行为。

在这里插入图片描述

三、简单实现

客户端服务端在处理指令时,方法必须是配套的。比如说,有新的玩家连接到服务器,那么服务器就应当向其它客户端广播创建一个新角色的指令,客户端在接收到该指令后,执行客户端创建角色的方法。
为了方便演示,这里需要定义两个HTML来表示两个不同的客户端不同的玩家,这两套客户端代码除了玩家的信息不一样,其它完全一致!!!

3.1 客户端实现步骤

我在这里客户端使用HTML+JQ实现

客户端——1代码:

(1)创建画布

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas Game</title>
    <style>
        canvas {
            border: 1px solid black;
        }
    </style>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<canvas id="gameCanvas" width="800" height="800"></canvas>
</body>
</html>

(2)设置1s60帧更新页面

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function clearCanvas() {
   ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function gameLoop() {
      clearCanvas();
      players.forEach(player => {
          player.draw();
      });
  }
 setInterval(gameLoop, 1000 / 60);
//清除画布方法
function clearCanvas() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

(3)连接游戏服务器并处理指令

这里使用websocket链接游戏服务器

 //连接服务器
const websocket = new WebSocket("ws://192.168.31.136:7070/websocket?userId=" + userId + "&userName=" + userName);
 //向服务器发送消息
 function sendMessage(userId,keyCode){
      const messageData = {
          playerId: userId,
          keyCode: keyCode
      };
      websocket.send(JSON.stringify(messageData));
  }
  //接收服务器消息,并根据不同的指令,做出不同的动作
  websocket.onmessage = event => {
        const data = JSON.parse(event.data);
        // 处理服务器发送过来的消息
        console.log('Received message:', data);
        //创建游戏对象
        if(data.type == 1){
            console.log("玩家信息:" +  data.players.length)
            for (let i = 0; i < data.players.length; i++) {
                console.log("玩家id:"+playerOfIds);
                createPlayer(data.players[i].playerId,data.players[i].pointX, data.players[i].pointY, data.players[i].color);
            }
        }
        //销毁游戏对象
        if(data.type == 2){
            console.log("玩家信息:" +  data.players.length)
            for (let i = 0; i < data.players.length; i++) {
                destroyPlayer(data.players[i].playerId)
            }
        }
        //移动游戏对象
        if(data.type == 3){
            console.log("移动;玩家信息:" +  data.players.length)
            for (let i = 0; i < data.players.length; i++) {
                players.filter(player => player.id === data.players[i].playerId)[0].move(data.players[i].keyCode)
            }
        }
    };

(4)创建玩家对象

//存放游戏对象
let players = [];
//playerId在此写死,正常情况下应该是用户登录获取的
const userId = "1"; // 用户的 id
const userName = "逆风笑"; // 用户的名称
//玩家对象
class Player {
        constructor(id,x, y, color) {
            this.id = id;
            this.x = x;
            this.y = y;
            this.size = 30;
            this.color = color;
        }
		//绘制游戏角色方法
        draw() {
            ctx.fillStyle = this.color;
            ctx.fillRect(this.x, this.y, this.size, this.size);
        }
		//游戏角色移动方法	
        move(keyCode) {
            switch (keyCode) {
                case 37: // Left
                    this.x = Math.max(0, this.x - 10);
                    break;
                case 38: // Up
                    this.y = Math.max(0, this.y - 10);
                    break;
                case 39: // Right
                    this.x = Math.min(canvas.width - this.size, this.x + 10);
                    break;
                case 40: // Down
                    this.y = Math.min(canvas.height - this.size, this.y + 10);
                    break;
            }
            this.draw();
        }
    }

(5)客户端创建角色方法

//创建游戏对象方法
function createPlayer(id,x, y, color) {
   const player = new Player(id,x, y, color);
    players.push(player);
    playerOfIds.push(id);
    return player;
}

(6)客户端销毁角色方法

在玩家推出客户端后,其它玩家的客户端应当销毁对应的角色。

//角色销毁
function destroyPlayer(playId){
   players = players.filter(player => player.id !== playId);
}

客户端——2代码:

客户端2的代码只有玩家信息不一致:

  const userId = "2"; // 用户的 id
  const userName = "逆风哭"; // 用户的名称

3.2 服务器端

服务器端使用Java+websocket来实现!

(1)引入依赖:

 <dependency>
   <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
     <version>2.1.2.RELEASE</version>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-websocket</artifactId>
     <version>2.3.7.RELEASE</version>
 </dependency>
 <dependency>
     <groupId>org.apache.commons</groupId>
     <artifactId>commons-lang3</artifactId>
     <version>3.11</version>
 </dependency>
 <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>fastjson</artifactId>
     <version>1.2.75</version>
 </dependency>
 <dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
     <version>1.16.16</version>
 </dependency>
 <dependency>
     <groupId>cn.hutool</groupId>
     <artifactId>hutool-all</artifactId>
     <version>5.6.3</version>
 </dependency>

(2)创建服务器

@Component
@ServerEndpoint("/websocket")
@Slf4j
public class Server {
    /**
     * 服务器玩家池
     * 解释:这里使用 ConcurrentHashMap为了保证线程安全,不会出现同一个玩家存在多条记录问题
     * 使用 static fina修饰 是为了保证 playerPool 全局唯一
     */
    private static final ConcurrentHashMap<String, Server> playerPool = new ConcurrentHashMap<>();
    /**
     * 存储玩家信息
     */
    private static final ConcurrentHashMap<String, Player> playerInfo = new ConcurrentHashMap<>();
    /**
     * 已经被创建了的玩家id
     */
    private static ConcurrentHashMap<String, Server> createdPlayer = new ConcurrentHashMap<>();

    private Session session;

    private Player player;

    /**
     * 连接成功后调用的方法
     */
    @OnOpen
    public void webSocketOpen(Session session) throws IOException {
        Map<String, List<String>> requestParameterMap = session.getRequestParameterMap();
        String userId = requestParameterMap.get("userId").get(0);
        String userName = requestParameterMap.get("userName").get(0);
        this.session = session;
        if (!playerPool.containsKey(userId)) {
            int locationX = getLocation(151);
            int locationY = getLocation(151);
            String color = PlayerColorEnum.getValueByCode(getLocation(1) + 1);
            Player newPlayer = new Player(userId, userName, locationX, locationY,color,null);
            playerPool.put(userId, this);
            this.player = newPlayer;
            //存放玩家信息
            playerInfo.put(userId,newPlayer);
        }
        log.info("玩家:{}|{}连接了服务器", userId, userName);
        // 创建游戏对象
        this.createPlayer(userId);
    }

    /**
     * 接收到消息调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) throws IOException, InterruptedException {
        log.info("用户:{},消息{}:",this.player.getPlayerId(),message);
        PlayerDTO playerDTO = new PlayerDTO();
        Player player = JSONObject.parseObject(message, Player.class);
        List<Player> players = new ArrayList<>();
        players.add(player);
        playerDTO.setPlayers(players);
        playerDTO.setType(OperationType.MOVE_OBJECT.getCode());
        String returnMessage = JSONObject.toJSONString(playerDTO);
        //广播所有玩家
        for (String key : playerPool.keySet()) {
            synchronized (session){
                String playerId = playerPool.get(key).player.getPlayerId();
                if(!playerId.equals(this.player.getPlayerId())){
                    playerPool.get(key).session.getBasicRemote().sendText(returnMessage);
                }
            }
        }
    }

    /**
     * 关闭连接调用方法
     */
    @OnClose
    public void onClose() throws IOException {
        String playerId = this.player.getPlayerId();
        log.info("玩家{}退出!", playerId);
        Player playerBaseInfo = playerInfo.get(playerId);
        //移除玩家
        for (String key : playerPool.keySet()) {
            playerPool.remove(playerId);
            playerInfo.remove(playerId);
            createdPlayer.remove(playerId);
        }
        //通知客户端销毁对象
        destroyPlayer(playerBaseInfo);
    }

    /**
     * 出现错误时调用的方法
     */
    @OnError
    public void onError(Throwable error) {
        log.info("服务器错误,玩家id:{},原因:{}",this.player.getPlayerId(),error.getMessage());
    }
    /**
     * 获取随即位置
     * @param seed
     * @return
     */
    private int getLocation(Integer seed){
        Random random = new Random();
        return random.nextInt(seed);
    }
}

websocket配置:

@Configuration
public class ServerConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

(3)创建玩家对象

玩家对象:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
    /**
     * 玩家id
     */
    private String playerId;
    /**
     * 玩家名称
     */
    private String playerName;
    /**
     * 玩家生成的x坐标
     */
    private Integer pointX;
    /**
     * 玩家生成的y坐标
     */
    private Integer pointY;
    /**
     * 玩家生成颜色
     */
    private String color;
    /**
     * 玩家动作指令
     */
    private Integer keyCode;
}

创建玩家对象返回给客户端DTO:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerDTO {
    private Integer type;
    private List<Player> players;
}

玩家移动指令返回给客户端DTO:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerMoveDTO {
    private Integer type;
    private List<Player> players;
}

(4)动作指令

public enum OperationType {
    CREATE_OBJECT(1,"创建游戏对象"),
    DESTROY_OBJECT(2,"销毁游戏对象"),
    MOVE_OBJECT(3,"移动游戏对象"),
    ;
    private Integer code;
    private String value;

    OperationType(Integer code, String value) {
        this.code = code;
        this.value = value;
    }

    public Integer getCode() {
        return code;
    }

    public String getValue() {
        return value;
    }
}

(5)创建对象方法

  /**
     * 创建对象方法
     * @param playerId
     * @throws IOException
     */
    private void createPlayer(String playerId) throws IOException {
        if (!createdPlayer.containsKey(playerId)) {
            List<Player> players = new ArrayList<>();
            for (String key : playerInfo.keySet()) {
                Player playerBaseInfo = playerInfo.get(key);
                players.add(playerBaseInfo);
            }
            PlayerDTO playerDTO = new PlayerDTO();
            playerDTO.setType(OperationType.CREATE_OBJECT.getCode());
            playerDTO.setPlayers(players);
            String syncInfo = JSONObject.toJSONString(playerDTO);
            for (String key :
                    playerPool.keySet()) {
                playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
            }
            // 存放
            createdPlayer.put(playerId, this);
        }
    }

(6)销毁对象方法

   /**
     * 销毁对象方法
     * @param playerBaseInfo
     * @throws IOException
     */
    private void destroyPlayer(Player playerBaseInfo) throws IOException {
        PlayerDTO playerDTO = new PlayerDTO();
        playerDTO.setType(OperationType.DESTROY_OBJECT.getCode());
        List<Player> players = new ArrayList<>();
        players.add(playerBaseInfo);
        playerDTO.setPlayers(players);
        String syncInfo = JSONObject.toJSONString(playerDTO);
        for (String key :
                playerPool.keySet()) {
            playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
        }
    }

四、演示

4.1 客户端1登陆服务器

在这里插入图片描述

4.2 客户端2登陆服务器

在这里插入图片描述

4.3 客户端2移动

在这里插入图片描述

4.4 客户端1移动

在这里插入图片描述

4.5 客户端1退出

在这里插入图片描述
完结撒花

完整代码传送门

五、总结

以上就是我对网络游戏如何实现玩家实时同步的理解与实现,我实现后心里也释然了,哈哈哈,真的好有趣!!!
我希望大家也是,不要失去好奇心,遇到自己感兴趣的事情,一定要多思考呀~

后来随着我经验的不断积累,我又去了解了一下Java作为游戏服务器的相关内容,发现Netty更适合做这个并且更容易入门,比如《我的世界》一些现有的服务器就是使用Netty实现的。有空也实现下,玩玩~

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

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

相关文章

RSA算法与错误敏感攻击

参见《RSA 算法的错误敏感攻击研究与实践》 RSA 算法简介 RSA 算法原理&#xff1a; 1&#xff09; RSA 算法密钥产生过程 &#xff08;1&#xff09;系统随机产生两个大素数 p p p 和 q q q&#xff0c;对这两个数据保密&#xff1b; &#xff08;2&#xff09;计算 n p …

Java类的声明周期、对象的创建过程

一、类的生命周期 使用类时&#xff0c;要先使用类加载器将类的字节码从磁盘加载到内存的方法区中&#xff0c;用Class对象表示加载到内存中的类&#xff0c;Class类是JDK中提供的类创建对象时&#xff0c;是根据内存中的Class对象&#xff0c;在堆中分配内存&#xff0c;完成…

c语言之指针的学习

1.指针是什么 &#xff08;指针是内存中一个最小单元的编号,也就是地址&#xff09; int main() {int a10;//当我们取出地址a的时候,取出的其实是a占4个字节中的第一个字节的地址int *pa&a;//pa是一个指针变量,用于存放地址//pa在口头语上常说为指针//指针本质上就是地址,…

C++学习|CUFFT计算一维傅里叶变换

CUFFT计算一维傅里叶变换 CUFFT库介绍CUFFTW计算一维傅里叶变换CUFFT计算一维傅里叶变换 前言&#xff1a;之前实现了CPU运行一维傅里叶变换&#xff0c;最近要改成GPU加速一维傅里叶变换&#xff0c;于是有了此篇作为记录&#xff0c;方便以后查阅。 CUFFT库介绍 CUFFT&#…

Protein - ECD (ExtraCellular Domain) 膜蛋白胞外区的 UniProt 与 PDB 数据分析

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132597158 ECD 是 Extracellular Domain 的缩写&#xff0c;指的是跨膜蛋白质的细胞外部分 (膜蛋白的胞外区)&#xff0c;通常包含一些功能性的结…

JVM的故事——类文件结构

类文件结构 文章目录 类文件结构一、概述二、无关性基石三、Class类文件的结构 一、概述 计算机是只认由0、1组成的二进制码的&#xff0c;不过随着发展&#xff0c;我们编写的程序可以被编译成与指令集无关、平台中立的一种格式。 二、无关性基石 对于不同平台和不同平台的…

77GHz线性调频连续波雷达

文章目录 前言 一、背景 二、优缺点 三、工作原理 四、电路模块设计 4.1.LFMCW信号源 4.2.发射电路 4.3.接收电路 4.4.信号处理器 五、应用 5.1.汽车测距 5.2.军事方面 5.3.气象方面 总结 前言 这篇文章是博主本科期间整理的关于77GHz线性调频连续波雷达的相关资料&#xff0c;…

【Java】文件操作和IO

文件操作和IO 文件树形结构组织和目录文件路径 Java中操作文件File 文件内容的读写(数据流)Reader和Writer字符输入流 Reader字符输出流 WriterFileReader 和 FileWriterFileReaderFileWriter InputStream和OutputStreamInputStreamFileInputStreamFileOutputStream 小程序扫描…

Vue3实现24小时倒计时

方法一:时间戳(24小时以内,毫秒为单位)转成时间,并且倒计时 效果预览: <script> // 剩余时间的时间戳,24小时的时间戳是86400000 const exTime = ref(86400000) // 支付时间期限 const payTime = ref() const maxtime = ref(0) //倒计时(时间戳,毫秒单位)转换成秒…

java 工程管理系统源码+项目说明+功能描述+前后端分离 + 二次开发

​ ​工程项目管理系统是指从事工程项目管理的企业&#xff08;以下简称工程项目管理企业&#xff09;受业主委托&#xff0c;按照合同约定&#xff0c;代表业主对工程项目的组织实施进行全过程或若干阶段的管理和服务。 如今建筑行业竞争激烈&#xff0c;内卷严重&#xff0c…

OPENCV实现ORB特征检测

# -*- coding:utf-8 -*- """ 作者:794919561 日期:2023/8/31 """ import cv2 import numpy as np# 读图像 img = cv2.imread(F:\\learnOpenCV\\openCVLearning\\pictures\\chess.jpg)

应急日光灯 补光灯 太阳能路灯 升压LED电源驱动恒流IC

产品说明 AP9196 是一系列外围电路简洁的宽调光比升压调光 恒流驱动器&#xff0c;适用于 3-40V 输入电压范围的 LED 照明领域。 AP9196 采用我司专利算法&#xff0c;可以实现高精度的恒流 效果&#xff0c;输出电流恒流精度≤3&#xff05;&#xff0c;电压工作范围为 5-40V&…

重庆市人才系统注册流程

1、IE浏览器打开重庆市科技局官网首页(http://kjj.cq.gov.cn/) 2、选择“重庆市科技管理信息系统”—选择“科技资源共享”—板块—注册—选择“个人注册”—填写注册信息—注册 3、注册—选择“个人注册”—填写注册信息—注册—登录 4、选择“科技管理系统“ 5、选择“科技人…

Linux进程概念及其状态

文章目录 &#x1f347;1. 什么是进程&#x1f348;1.1 概念&#x1f348;1.2 理解进程 &#x1f34b;2. Linux的PCB&#x1f34e;3. 查看进程 & 杀死进程&#x1f352;4. 系统调用获取进程标识符&#x1f353;4.1 进程PID&#x1f353;4.2 父进程PPID &#x1f346;5. 系统…

MetInfo5.0文件包含漏洞

MetInfo历史版本与文件 环境在这里下载&#xff0c;使用phpstudy搭建 我们来看到这个index.php&#xff0c;如下图所示&#xff0c;其中定义了fmodule变量与module变量&#xff0c;其中require_once语句表示将某个文件引入当前文件&#xff0c;在这个代码中&#xff0c;通过r…

【JasperReports笔记06】JasperReport报表开发之常见的组件元素(Table、Subreport、Barcode等)

这篇文章&#xff0c;主要介绍JasperReport报表开发之常见的组件元素&#xff08;Table、Subreport、Barcode等&#xff09;。 目录 一、基础组件元素 1.1、StaticText 1.2、TextField 1.3、Image 1.4、Break分页 1.5、Rectangle矩形区域 1.6、Ellipse椭圆区域 1.7、Li…

基于MQTT协议的物联网关

随着工业领域的不断发展&#xff0c;数字化转型已经成为企业迈向未来的必由之路。在这个数字化浪潮中&#xff0c;HiWoo Box以其强大的功能和创新的设计&#xff0c;在工业物联网领域被越来越多的人所熟知。特别是其基于MQTT协议的物联网关能力&#xff0c;也为企业实现智能化数…

喷泉码浅谈

01、喷泉码简介 喷泉码&#xff08;Fountain Code&#xff09;是一种在无线通信、数据传输和网络编码领域中使用的错误纠正技术。它与传统的纠错码和编码方法有所不同&#xff0c;喷泉码被设计用于在不确定信道条件下的高效数据传输。传统的纠错码&#xff08;如海明码、RS码等…

无涯教程-Android - RadioButton函数

RadioButton有两种状态:选中或未选中,这允许用户从一组中选择一个选项。 Radio Button 示例 本示例将带您完成一些简单的步骤,以展示如何使用Linear Layout和RadioButton创建自己的Android应用程序。 以下是修改后的主要Activity文件 src/MainActivity.java 的内容。 packa…

【算法】函数渐近的界基础知识及定理

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…