一对一WebRTC视频通话系列(二)——websocket和join信令实现

本系列博客主要记录WebRtc实现过程中的一些重点,代码全部进行了注释,便于理解WebRTC整体实现。


一对一WebRTC视频通话系列往期博客:

一对一WebRTC视频通话系列(一)—— 创建页面并显示摄像头画面


websocket和join信令实现

    • 一、websocket实现
      • 1.1客户端
      • 1.2服务端
    • 二、join信令实现
      • 2.1 客户端
      • 2.2 服务端

一、websocket实现

1.1客户端

main.js文件中,定义了一个名为ZeroRTCEngine的类,该类使用WebSocket进行通信。ZeroRTCEngine类的实例化过程如下:

  1. 首先,声明并定义一个名为zeroRTCEngine的类,接受一个wsUrl参数。
  2. 在ZeroRTCEngine类中,定义一个init方法,用于初始化WebSocket地址。
  3. 定义ZeroRTCEngine类的方法createWebsocket,用于创建WebSocket对象。
  4. createWebsocket方法中,定义多种WebSocket事件处理函数.
  5. 定义onopenonmessageoncloseonerror方法,用于处理WebSocket连接状态和接收到的消息。

在实际使用中,通过调用ZeroRTCEngine类的createWebsocket方法来创建一个WebSocket实例,并监听各种事件(如打开、消息传递、关闭和错误)。

	//定义 ZeroRTCEngine 的 init 方法
	//参数 wsUrl:WebSocket 的地址
	ZeroRTCEngine.prototype.init = function(wsUrl) {
	    //设置 WebSocket url
	    this.wsUrl = wsUrl;
	    this.signaling =null;
	}
	
	//定义 ZeroRTCEngine 的 createWebsocket 方法
	ZeroRTCEngine.prototype.createWebsocket = function() {
	    //创建 WebSocket 对象
	    zeroRTCEngine = this;
	    zeroRTCEngine.signaling = new WebSocket(this.wsUrl);
	
	    //设置打开函数
	    zeroRTCEngine.signaling.onopen = function () {
	        zeroRTCEngine.onopen();
	    }
	
	    //设置关闭函数
	    zeroRTCEngine.signaling.onclose = function (event) {
	        zeroRTCEngine.onclose(event);
	    }
	    //设置错误函数
	    zeroRTCEngine.signaling.onerror = function (event) {
	        zeroRTCEngine.onerror(event);
	    }
	    //设置消息传递函数
	    zeroRTCEngine.signaling.onmessage = function (event) {
	        zeroRTCEngine.onmessage(event);
	    }
	}
	
	
	ZeroRTCEngine.prototype.onopen = function() {
	    console.log("WebSocket connection established.");
	}
	
	ZeroRTCEngine.prototype.onmessage = function(event) {
	    console.log("Received message:" + event.data);
	    //解析收到的消息
	    var message = JSON.parse(event.data);
	}
	
	ZeroRTCEngine.prototype.onclose = function(event) {
	    console.log("WebSocket connection closed." + event.data + ", reason" + EventCounts.reason);
	}
	
	ZeroRTCEngine.prototype.onerror = function(event) {
	    console.log("WebSocket connection error:" + event.data);  
	}
	
	zeroRTCEngine = new ZeroRTCEngine("ws://192.168.3.181:8099");
	zeroRTCEngine.createWebsocket();

1.2服务端

创建一个名为signal_server.jsjs新文件,搭建简单的WebSocket服务器,用于处理客户端连接、消息发送和接收、连接关闭等基本操作。在实际应用中,可能需要根据需求对代码进行修改和扩展。

  1. 使用ws.createServer()方法创建一个WebSocket服务器。这个方法接受一个回调函数作为参数,该回调函数在客户端连接到服务器时被调用。
  2. 在回调函数中,使用console.log()输出一条connection established的消息,表示连接已建立。然后,向客户端发送一条消息,例如"我收到你的连接了"。
  3. 使用conn.on("text", function (str) {...})监听客户端发送的消息。
    当客户端发送消息时,会触发这个回调函数。使用console.info()输出一条消息,表示收到的消息。
  4. 在连接关闭时,使用conn.on("close", function (code, reason) {...})监听连接关闭。
    当客户端关闭连接时,会触发这个回调函数。使用console.log()输出一条消息,表示连接已关闭。
  5. 使用了conn.on("error", function (err) {...})来监听连接错误。当发生错误时,会触发这个回调函数,输出错误信息。
  6. 使用server.listen()方法将服务器绑定到一个指定的端口。在实际应用中,通常需要根据不同的需求来修改这个端口。
	// 引入ws模块
	var ws = require("nodejs-websocket");
	// 定义端口号
	var port =8099;
	
	// 创建一个WebSocket服务器
	var server = ws.createServer(function (conn) {
	    // 连接建立时的输出
	    console.log("New connection");
	    // 向客户端发送消息
	    conn.sendText("我收到你的连接了")
	    // 监听客户端发送的消息
	    conn.on("text", function (str) {
	        console.info("Received msg:"+str);
	    });
	
	    // 连接关闭时的输出
	    conn.on("close", function (code, reason) {
	        console.log("Connection closed,code:" + code + " reason:" + reason);
	    });
	    // 监听连接错误
	    conn.on("error", function (error) {
	        console.error("发生错误:", error);
	    })
	}).listen(port);

在ubuntu中使用下列指令开启服务端:

	node signal_server.js

在这之前切记先用以下两行命令初始化:

	sudo npm init -y
	sudo npm install nodejs-websocket

在这里插入图片描述
在这里插入图片描述

二、join信令实现

2.1 客户端

修改按钮点击事件

document.getElementById('joinBtn').onclick = function () {
    roomId = document.getElementById('roomId').value;
    if(roomId == '' || roomId == "Please enter the room ID"){
        alert('Please enter the room ID');
        return;
    }

    console.log("joinBtn clicked,roomId: " + roomId);
    //初始化本地码流
    initLocalStream();
}

首先创建一个JSON对象,包含命令(cmd)、房间ID(roomId)和用户ID(uid),然后将这个JSON对象转换为字符串。接着,调用zeroRTCEngine.sendMessage方法将这个字符串发送给服务器,表示用户要加入房间。最后,在控制台输出一条消息,表明用户已经成功加入房间。

// join 主动加入房间
// leave 主动离开房间
// new-peer 有人加入房间,通知已经在房间的人
// peer-leave 有人离开房间,通知已经在房间的人
// offer 发送offer给对端peer
// answer发送offer给对端peer
// candidate 发送candidate给对端peer
const SIGNAL_TYPE_JOIN = "join";
const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
const SIGNAL_TYPE_LEAVE = "leave";
const SIGNAL_TYPE_NEW_PEER = "new-peer";
const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
const SIGNAL_TYPE_OFFER = "offer";
const SIGNAL_TYPE_ANSWER = "answer";
const SIGNAL_TYPE_CANDIDATE = "candidate";

var localUserId = Math.random().toString(36).substr(2);//本地ID   
var remoteUserId = -1; //对方用户ID
var roomId = -1; //房间ID

ZeroRTCEngine.prototype.sendMessage = function(message) {
    this.signaling.send(message);
}

function doJoin(roomId) {
    var jsonMsg = {
        'cmd': 'join',
        'roomId': roomId,
        'uid': localUserId,
    }; 
    var message = JSON.stringify(jsonMsg); //将json对象转换为字符串
    zeroRTCEngine.sendMessage(message);   //设计方法:用实现方法而不是直接用变量    
    console.info("doJoin message: " + message);
}

运行效果:
在这里插入图片描述

2.2 服务端

修改signal_server.js
1.添加相关宏定义,以及WebRTC所需要用到的Map类。

// join 主动加入房间
// leave 主动离开房间
// new-peer 有人加入房间,通知已经在房间的人
// peer-leave 有人离开房间,通知已经在房间的人
// offer 发送offer给对端peer
// answer发送offer给对端peer
// candidate 发送candidate给对端peer
const SIGNAL_TYPE_JOIN = "join";
const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
const SIGNAL_TYPE_LEAVE = "leave";
const SIGNAL_TYPE_NEW_PEER = "new-peer";
const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
const SIGNAL_TYPE_OFFER = "offer";
const SIGNAL_TYPE_ANSWER = "answer";
const SIGNAL_TYPE_CANDIDATE = "candidate";


/** ----- ZeroRTCMap ----- */
var ZeroRTCMap = function () {
    this._entrys = new Array();

    this.put = function (key, value) {
        if (key == null || key == undefined) {
            return;
        }
        var index = this._getIndex(key);
        if (index == -1) {
            var entry = new Object();
            entry.key = key;
            entry.value = value;
            this._entrys[this._entrys.length] = entry;
        } else {
            this._entrys[index].value = value;
        }
    };
    this.get = function (key) {
        var index = this._getIndex(key);
        return (index != -1) ? this._entrys[index].value : null;
    };
    this.remove = function (key) {
        var index = this._getIndex(key);
        if (index != -1) {
            this._entrys.splice(index, 1);
        }
    };
    this.clear = function () {
        this._entrys.length = 0;
    };
    this.contains = function (key) {
        var index = this._getIndex(key);
        return (index != -1) ? true : false;
    };
    this.size = function () {
        return this._entrys.length;
    };
    this.getEntrys = function () {
        return this._entrys;
    };
    this._getIndex = function (key) {
        if (key == null || key == undefined) {
            return -1;
        }
        var _length = this._entrys.length;
        for (var i = 0; i < _length; i++) {
            var entry = this._entrys[i];
            if (entry == null || entry == undefined) {
                continue;
            }
            if (entry.key === key) {// equal
                return i;
            }
        }
        return -1;
    };
}

2.定义Client函数,用于初始化用户信息

function Client(uid, conn, roomId) {
    this.uid = uid;     // 用户所属的id
    this.conn = conn;   // uid对应的websocket连接
    this.roomId = roomId;
}

3.处理加入房间请求
监听客户端发送的消息,如果时加入房间的,利用handleJoin()函数进行处理

// 监听客户端发送的消息
    conn.on("text", function (str) {
        console.info("Received msg:"+str);
        var jsonMsg = JSON.parse(str);

        switch(jsonMsg.cmd){
            case SIGNAL_TYPE_JOIN:
                handleJoin(jsonMsg, conn); 
            break;
            
        }
    });

定义handleJoin()函数,处理加入房间请求。它首先获取房间ID用户ID,然后获取房间Map。如果房间不存在,则创建一个新的房间。如果房间已满,给出提示并拒绝加入请求。如果房间未满,创建一个新的客户端并将其添加到房间。如果房间中用户数大于1,则通知房间中的其他用户有新人加入。

实现原理:
1.从传入的messageconn参数中获取房间ID用户ID
2.尝试获取roomId对应的房间Map。如果房间Map不存在,则创建一个新的房间Map。
3.检查房间是否已满。如果已满,给出提示并返回;否则,创建一个新的客户端并将其添加到房间。
4.如果房间中用户数大于1,则遍历房间中的所有用户,通知他们有新人加入。同时,通知自己房间中有新人加入。

function handleJoin(message, conn){
    // 获取房间ID和用户ID
    var roomId = message.roomId;
    var uid = message.uid;
    console.log(" uid:"+uid + "try to join room: "+roomId);

    // 获取房间Map
    var roomMap = roomTableMap.get(roomId);
    // 如果房间不存在,则创建一个新的房间
    if(roomMap == null){
        roomMap = new ZeroRTCMap();
        roomTableMap.put(roomId, roomMap);
    }

    // 如果房间已满,给出提示
    if(roomMap.size() >= 1){
        console.error("roomId:" + roomId + " is full");

        conn.sendText('roomId:' + roomId + ' is full');
        return;
    }

    // 创建一个新的客户端
    var client = new Client(uid, conn, roomId);
    // 将用户添加到房间
    roomMap.put(uid, client);
    // 如果房间中的用户数大于1,则通知房间中的其他用户
    if(roomMap.size() > 1){
        
        // 获取房间中的所有用户
        var clients = roomMap.getEntrys();
        for(var i in clients){
            var remoteUid = clients[i].key;
            if(remoteUid != uid){
                // 通知已经在房间的人,有新人加入
                var jsonMsg = {
                    'cmd':SIGNAL_TYPE_NEW_PEER,
                    'remoteUid':uid
                };
                var msg = JSON.stringify(jsonMsg);

                // 通知自己,房间里有的人
                jsonMsg = {
                    'cmd':SIGNAL_TYPE_RESP_JOIN,
                    'remoteUid':remoteUid
                };
                msg = JSON.stringify(jsonMsg);
                console.info("resp-join"+msg);
                conn.sendText(msg);
            }
        }
    }
}

整体代码:

// 引入ws模块
var ws = require("nodejs-websocket");
// 定义端口号
var port =8099;

// join 主动加入房间
// leave 主动离开房间
// new-peer 有人加入房间,通知已经在房间的人
// peer-leave 有人离开房间,通知已经在房间的人
// offer 发送offer给对端peer
// answer发送offer给对端peer
// candidate 发送candidate给对端peer
const SIGNAL_TYPE_JOIN = "join";
const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
const SIGNAL_TYPE_LEAVE = "leave";
const SIGNAL_TYPE_NEW_PEER = "new-peer";
const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
const SIGNAL_TYPE_OFFER = "offer";
const SIGNAL_TYPE_ANSWER = "answer";
const SIGNAL_TYPE_CANDIDATE = "candidate";


/** ----- ZeroRTCMap ----- */
var ZeroRTCMap = function () {
    this._entrys = new Array();

    this.put = function (key, value) {
        if (key == null || key == undefined) {
            return;
        }
        var index = this._getIndex(key);
        if (index == -1) {
            var entry = new Object();
            entry.key = key;
            entry.value = value;
            this._entrys[this._entrys.length] = entry;
        } else {
            this._entrys[index].value = value;
        }
    };
    this.get = function (key) {
        var index = this._getIndex(key);
        return (index != -1) ? this._entrys[index].value : null;
    };
    this.remove = function (key) {
        var index = this._getIndex(key);
        if (index != -1) {
            this._entrys.splice(index, 1);
        }
    };
    this.clear = function () {
        this._entrys.length = 0;
    };
    this.contains = function (key) {
        var index = this._getIndex(key);
        return (index != -1) ? true : false;
    };
    this.size = function () {
        return this._entrys.length;
    };
    this.getEntrys = function () {
        return this._entrys;
    };
    this._getIndex = function (key) {
        if (key == null || key == undefined) {
            return -1;
        }
        var _length = this._entrys.length;
        for (var i = 0; i < _length; i++) {
            var entry = this._entrys[i];
            if (entry == null || entry == undefined) {
                continue;
            }
            if (entry.key === key) {// equal
                return i;
            }
        }
        return -1;
    };
}

var roomTableMap = new ZeroRTCMap();

function Client(uid, conn, roomId) {
    this.uid = uid;     // 用户所属的id
    this.conn = conn;   // uid对应的websocket连接
    this.roomId = roomId;
}

// 处理加入房间请求
function handleJoin(message, conn){
    // 获取房间ID和用户ID
    var roomId = message.roomId;
    var uid = message.uid;
    console.log(" uid:"+uid + "try to join room: "+roomId);

    // 获取房间Map
    var roomMap = roomTableMap.get(roomId);
    // 如果房间不存在,则创建一个新的房间
    if(roomMap == null){
        roomMap = new ZeroRTCMap();
        roomTableMap.put(roomId, roomMap);
    }

    // 如果房间已满,给出提示
    if(roomMap.size() >= 1){
        console.error("roomId:" + roomId + " is full");

        conn.sendText('roomId:' + roomId + ' is full');
        return;
    }

    // 创建一个新的客户端
    var client = new Client(uid, conn, roomId);
    // 将用户添加到房间
    roomMap.put(uid, client);
    // 如果房间中的用户数大于1,则通知房间中的其他用户
    if(roomMap.size() > 1){
        
        // 获取房间中的所有用户
        var clients = roomMap.getEntrys();
        for(var i in clients){
            var remoteUid = clients[i].key;
            if(remoteUid != uid){
                // 通知已经在房间的人,有新人加入
                var jsonMsg = {
                    'cmd':SIGNAL_TYPE_NEW_PEER,
                    'remoteUid':uid
                };
                var msg = JSON.stringify(jsonMsg);

                // 通知自己,房间里有的人
                jsonMsg = {
                    'cmd':SIGNAL_TYPE_RESP_JOIN,
                    'remoteUid':remoteUid
                };
                msg = JSON.stringify(jsonMsg);
                console.info("resp-join"+msg);
                conn.sendText(msg);
            }
        }
    }
}

// 创建一个WebSocket服务器
var server = ws.createServer(function (conn) {
    // 连接建立时的输出
    console.log("New connection");
    // 向客户端发送消息
    conn.sendText('我收到你的连接了')
    // 监听客户端发送的消息
    conn.on("text", function (str) {
        console.info("Received msg:"+str);
        var jsonMsg = JSON.parse(str);

        switch(jsonMsg.cmd){
            case SIGNAL_TYPE_JOIN:
                handleJoin(jsonMsg, conn); 
            break;
            
        }
    });
    
    // 连接关闭时的输出
    conn.on("close", function (code, reason) {
        console.log("Connection closed,code:" + code + " reason:" + reason); 
    });
    // 监听连接错误
    conn.on("error", function (error) {
        console.error("发生错误:", error);
    })
}).listen(port);

运行效果:

客户1:
第一个创建,刚开始没有红框的内容。在客户2新加入后,收到服务端发来的信令。
在这里插入图片描述

客户2:
创建后,会收到服务端发来的,房间内已有的客户1的信息。
在这里插入图片描述
客户3:
房间内已经有2个客户了,会提示房间已满,无法加入房间。
在这里插入图片描述
服务端:
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深入浅出学习Pytorch—Pytorch简介与2024年最新安装(GPU)

深入浅出学习Pytorch—Pytorch简介 学习原因&#xff1a;Pytorch日益增长的发展速度与深度学习时代的迫切需要 Pytorch模型训练 pytorch实现模型训练包括以下的几个方面&#xff08;学习路线&#xff09; 数据&#xff1a;数据预处理与数据增强模型&#xff1a;如何构建模型模…

全栈开发之路——前端篇(4)watch监视、数据绑定和计算属性

全栈开发一条龙——前端篇 第一篇&#xff1a;框架确定、ide设置与项目创建 第二篇&#xff1a;介绍项目文件意义、组件结构与导入以及setup的引入。 第三篇&#xff1a;setup语法&#xff0c;设置响应式数据。 辅助文档&#xff1a;HTML标签大全&#xff08;实时更新&#xff…

【JVM】从硬件层面和应用层面的有序性和可见性,到Java的volatile和synchronized

Java的关键字volatile保证了有序性和可见性&#xff0c;这里我试着从底层开始讲一下有序性和可见性。 一&#xff0c;一致性 数据如果同时被两个cpu读取了&#xff0c;如何保证数据的一致性&#xff1f;或者换句话说&#xff0c;cpu1改了数据&#xff0c;cpu2的数据就成了无效…

esp32-cam 1. 出厂固件编译与测试

0. 环境 - ubuntu18 - esp32-cam - usb转ttl ch340 硬件连接 esp32-camch340板子U0RTXDU0TRXDGNDGND5V5V 1. 安装依赖 sudo apt-get install vim sudo apt install git sudo apt-get install git wget flex bison gperf python python-pip python-setuptools python-serial p…

【Linux】awk命令学习

最近用的比较多&#xff0c;学习总结一下。 文档地址&#xff1a;https://www.gnu.org/software/gawk/manual/gawk.html 一、awk介绍二、语句结构1.条件控制语句1&#xff09;if2&#xff09;for3&#xff09;while4&#xff09;break&continue&next&exit 2.比较运…

20240503解决Ubuntu20.04和WIN10双系统下WIN10的时间异常的问题

20240503解决Ubuntu20.04和WIN10双系统下WIN10的时间异常的问题 2024/5/3 9:33 缘起&#xff1a;因为工作需要&#xff0c;编译服务器上都会安装Ubuntu20.04。 但是因为WINDOWS强悍的生态系统&#xff0c;偶尔还是有必须要用WINDOWS的时候&#xff0c;于是也安装了WIN10。 双系…

软件应用开发安全设计指南

1.1 应用系统架构安全设计要求 设计时要充分考虑到系统架构的稳固性、可维护性和可扩展性&#xff0c;以确保系统在面对各种安全威胁时能够稳定运行。 在设计系统架构时&#xff0c;要充分考虑各种安全威胁&#xff0c;如DDoS攻击、SQL注入、跨站脚本攻击&#xff08;XSS&…

2022 亚马逊云科技中国峰会,对话开发者论坛

目录 前言 最近整理资料发现还有一些前 2 年的内容没发出来&#xff0c;故补发记录&#xff0c;每年都有新的感悟。 开发者论坛 1. 你认为什么是开发者社区&#xff0c;如何定义一个成功的开发者社区&#xff1f; 我认为可以把开发者社区看成一个 “产品” 来对待&#xff…

SpringBoot @DS注解 和 DynamicDataSource自定义实现多数据源的2种实现方式

前言 在实际的项目中&#xff0c;我们经常会遇到需要操作多个数据源的情况&#xff0c;SpringBoot为我们提供了多种实现多数据源的方式。本文将介绍两种常见的方式&#xff1a;使用DS注解实现多数据源的切换以及使用DynamicDataSource自定义实现多数据源的切换。 我们将分别介…

【Unity】在空物体上实现 IPointerClickHandler 不起作用

感谢Unity接口IPointerClickHandler使用说明_哔哩哔哩_bilibiliUnity接口IPointerClickHandler使用说明, 视频播放量 197、弹幕量 0、点赞数 3、投硬币枚数 2、收藏人数 2、转发人数 0, 视频作者 游戏创作大陆, 作者简介 &#xff0c;相关视频&#xff1a;在Unity多场景同时编辑…

扩散模型(Diffusion Model)概述

扩散模型&#xff08;Diffusion Model&#xff09;是图像生成模型的一种。有别于此前 AI 领域大名鼎鼎的 GAN、VAE 等算法&#xff0c;扩散模型另辟蹊径&#xff0c;其主要思想是一种先对图像增加噪声&#xff0c;再逐步去噪的过程&#xff0c;其中如何去噪还原图像是算法的核心…

移动机器人系统与技术:自动驾驶、移动机器人、旋翼无人机

这本书全面介绍了机器人车辆的技术。它介绍了道路上自动驾驶汽车所需的概念。此外&#xff0c;读者可以在六足机器人的构造、编程和控制方面获得宝贵的知识。 这本书还介绍了几种不同类型旋翼无人机的控制器和空气动力学。它包括各种旋翼推进飞行器在不同空气动力学环境下的模…

备考2024年小学生古诗文大会:吃透10道历年真题和知识点(持续)

根据往年的安排&#xff0c;2024年上海市小学生古诗文大会预计还有一个月就将启动。我们继续来随机看10道往年的上海小学生古诗文大会真题&#xff0c;这些题目来自我去重、合并后的1700在线题库&#xff0c;每道题我都提供了参考答案和独家解析。 根据往期的经验&#xff0c;只…

pg数据库学习知识要点分析-1

知识要点1 对象标识OID 在PostgreSQL内部&#xff0c;所有的数据库对象都通过相应的对象标识符&#xff08;object identifier&#xff0c;oid&#xff09;进行管理&#xff0c;这些标识符是无符号的4字节整型。数据库对象与相应oid 之间的关系存储在对应的系统目录中&#xf…

如何使用 Node.js 开发一个文件上传功能?

在 Node.js 中实现文件上传功能可以通过多种方式完成&#xff0c;但其中最常用的方法之一是使用 Express 框架和 Multer 中间件。Express 是一个流行的 Node.js Web 框架&#xff0c;而 Multer 是一个用于处理文件上传的中间件。 步骤 1: 准备工作 首先&#xff0c;确保你已经…

基于Springboot的旅游管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的旅游管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&…

数字旅游以科技创新为核心竞争力:推动旅游服务的智能化、高效化,满足游客日益增长的旅游需求

一、引言 随着科技的飞速发展&#xff0c;数字旅游作为旅游业与信息技术结合的产物&#xff0c;正以其独特的魅力改变着传统旅游业的格局。科技创新作为数字旅游的核心竞争力&#xff0c;不仅推动了旅游服务的智能化、高效化&#xff0c;更满足了游客日益增长的旅游需求。本文…

HIVE数据导出

HIVE数据导出 1.INSERT OVERWRITE LOCAL DIRECTORY "路径" SELECT 查询语句; INSERT OVERWRITE LOCAL DIRECTORY "/usr/local/soft/hive-3.1.2/data/output" select * from learn2.partition_student6; 导出数据时 通过执行MapReduce任务导出到本地文件系统…

Python安装win32api

&#x1f4da;博客主页&#xff1a;knighthood2001 ✨公众号&#xff1a;认知up吧 &#xff08;目前正在带领大家一起提升认知&#xff0c;感兴趣可以来围观一下&#xff09; &#x1f383;知识星球&#xff1a;【认知up吧|成长|副业】介绍 ❤️感谢大家点赞&#x1f44d;&…

【云原生】Docker 实践(二):什么是 Docker 的镜像

【Docker 实践】系列共包含以下几篇文章&#xff1a; Docker 实践&#xff08;一&#xff09;&#xff1a;在 Docker 中部署第一个应用Docker 实践&#xff08;二&#xff09;&#xff1a;什么是 Docker 的镜像Docker 实践&#xff08;三&#xff09;&#xff1a;使用 Dockerf…