WebRTC实现双端音视频聊天(Vue3 + SpringBoot)
目录
概述
相关概念
双端连接整体实现步骤概述
文章代码实现注意点
STUN和TURN服务器的搭建
开发过程描述
后端开发流程
前端开发流程
效果演示
Gitee源码地址
概述
-
文章描述使用WebRTC技术实现一对一音视频通话。
-
由于设备摄像头限制(一台电脑作测试无法在开启的双端同时获取摄像头数据流),导致一台电脑无法同时测试双端,因此文章使用mp4音视频文件模拟摄像头音视频数据流输入。
-
使用技术
-
前端:Vue3,WebRTC相关API,axios
-
后端信令服务器实现:SpringBoot,WebSocket
-
相关概念
-
Peer-to-Peer (P2P) 连接:WebRTC主要是基于 P2P 连接的,这意味着通信是直接在两端的浏览器之间进行的,而不需要经过中介服务器(尽管可能会使用服务器来初始化和协调连接)。这种方式降低了延迟并节省了带宽。
-
SDP(Session Description Protocol):描述媒体信息(如音频、视频编码格式、传输协议等)的协议。例如我们在双方构建连接时,我们需要知道对方使用的音视频编解码格式,以确保双方使用相同编解码格式。编解码格式就是定义在SDP信息中的其中之一的信息。
-
ICE Candidate:ICE 候选是 WebRTC 在 P2P 连接过程中为寻找最佳传输路径(如 STUN 或 TURN 服务器)提供的一系列地址和端口。在双方构建连接时需要知道对方的公网IP地址和端口,以实现P2P连接,Candidate信息中就包含自身的公网IP和端口。
-
STUN(Session Traversal Utilities for NAT)服务器:是 NAT 穿透的协议,用来获取客户端的公网 IP 地址和端口。我们身处各种局域网中,对方如果想要和我们构建P2P连接,就必然要知道我们的公网IP和端口才能和我们连接上,我们可以通过STUN服务器获取我们的公网IP和端口。
-
TURN(Traversal Using Relays around NAT)服务器:当 STUN 连接不可用时,TURN 服务器作为中继服务器转发数据。当STUN服务器无法帮助我们获取公网IP和端口时,我们就可以使用TURN服务器作为中转站传递音视频流数据。
-
信令服务器:上面介绍了媒体信息SDP和网络信息Candidate,这些实际上可以称为"信令",我们如果想要与对端连接,那么我们就需要知道对端的媒体信息和网络信息来构建连接,信令服务器就是帮助我们实现两端的信息交换的。本文中信令服务器就是我们自己编写的SpringBoot后端,来帮助两端互传连接信息。
双端连接整体实现步骤概述
在大致知道了上面介绍的WebRTC基本概念之后,我们以双端音视频互联的整体过程。
假设存在A端(发起端)和B端(接收端)。
1. 创建RTC连接对象(new RTCPeerConnection),此对象存在构建连接时所需的API。
2. A端和B端分别连接后端WebSocket(信令服务器),以为接下来信息互传奠定基础。
3. A端创建媒体信息SDP(createOffer)保存到本地(setLocalDescription),将A端SDP信息通过WebSocket发送给B端。
4. B端接收到A端的SDP信息,设置为远端媒体信息(setRemoteDescription),然后B端创建应答媒体信息(实际上就是B端的媒体信息)SDP(createAnswer)保存到本地(setLocalDescription),并将B端创建的应答媒体信息SDP通过WebSocket发送给A端。
5. A端收到B端发送的应答媒体信息SDP后,保存为远端媒体信息(setRemoteDescription)。
6. 至此,A端和B端媒体信息SDP交换完毕。
7. 开始交换网络信息Candidate,我们在创建RTC连接对象时(步骤1)监听网络信息的获取(onicecandidate),当我们调用setRemoteDescription函数设置了远端媒体信息之后,会触发onicecandidate并给予condidate网络信息。
8. 我们将监听到的网络信息candidate通过WebSocket发送给对端,对端收到后将对方的网络信息配置上(addIceCandidate)以实现连接。
9. 当媒体信息SDP和网络信息Candidate互相交换并设置上之后,就可以开始音视频流数据互传显示了。
10. 通过addTrack发送本地流数据,通过ontrack监听对端音视频流数据的发送,监听到就显示对端音视频。
媒体协商和网络协商时序图:
总结:在视频互传之前重要的就是交换媒体SDP信息和网络Candidate信息(媒体和网络协商),当双方都获取到对方的媒体和网络信息之后。就能够成功构建连接并传递音视频数据了。
文章代码实现注意点
在最开始的概述中有提到,本文提供的1对1音视频聊天代码示例中没有真实调用用户摄像头获取音视频流数据,因为作者只有一台电脑,为了可以更方便的在一台电脑上开启两端并测试,因此使用了MP4音视频作为音视频流数据输入作为测试。
这实际上并不会和真实开启摄像头获取音视频数据流有很大的区别。仅仅是获取流数据的方式不同罢了。
在真实的场景下,可以使用API:getUserMedia去获取摄像头音视频流数据即可。
const stream = await navigator.mediaDevices.getUserMedia({video: true,audio: true
});
STUN和TURN服务器的搭建
为了能够获取到我们本地的公网IP和端口去和对端创建连接,我们可以尝试去搭建STUN服务器和TURN中继服务器。
注:此步骤不是一定需要做,因为Google给我们提供了一个免费公用的STUN服务器地址:stun:stun.l.google.com:19302,如果你发现用不了,或需要搭建复杂的音视频通话应用,还是推荐自己搭建一下STUN/TURN服务器。
我们直接搭建开源的Coturn服务器即可,因为Coturn 同时支持 TURN 和 STUN 协议。
下面会介绍在CentOS8中搭建Coturn服务器步骤:
1. 安装所需依赖包
yum install -y make gcc cc gcc-c++ wget openssl-devel libevent libevent-devel openssl
2. yum直接一键下载安装
sudo yum install coturn# (验证安装)安装程序结束后执行如下命令查看是否正确输出turnserver路径
which turnserver
3. 配置Coturn相关属性,找到配置文件路径:
find / -name turnserver.conf
4. 获取服务器内网IP和公网IP
# 输入命令查看Ip
ifconfig
找到自己启用的网络下的内网IP,公网IP就是你连接服务器的IP地址。
5. 使用openSSL生成cert和pkey配置的自签名证书
openssl req -x509 -newkey rsa:2048 -keyout /turn_server_pkey.pem -out /turn_server_cert.pem -days 999 -nodes
输入上面命令后,填写一下证书的一些信息(城市,地区等),随便填一下回车回车!就行。
上面的/turn_server_pkey.pem和 /turn_server_cert.pem 请自己设置好保存证书的路径,上面默认放到了根路径下。
6. 编辑刚才找到的配置文件
将下面的配置部分修改后替换掉原配置文件的所有内容。
# 网卡名
relay-device=eth0
#内网IP
listening-ip=172.24.52.189
listening-port=3478
#内网IP,加密访问配置
relay-ip=172.24.52.189
tls-listening-port=5349
# 外网IP
external-ip=自己的外网IP
relay-threads=500
#打开密码验证
lt-cred-mech
cert=/turn_server_cert.pem
pkey=/turn_server_pkey.pem
min-port=40000
max-port=65535
#设置用户名和密码,创建IceServer时使用
user=user:123456
# 外网IP绑定的域名
realm=你自己IP绑定的域名
# 服务器名称,用于OAuth认证,默认和realm相同,部分浏览器本段不设可能会引发cors错误。
server-name=你自己IP绑定的域名
# 认证密码,和前面设置的密码保持一致
cli-password=123456
7. 开启端口访问
7.1 开启云服务器安全组端口
开启4000-65535端口的原因:外部客户端与 TURN 服务器的通信使用动态端口。通常,操作系统会为每个连接分配一个临时端口(通常是大于 1024 的端口),而 40000 到 65535 端口 作为 高端端口,是常用的临时端口范围。因此,为了确保 TURN 服务器能够处理大量的并发连接,并为每个连接分配一个端口,需要确保 TURN 服务器的端口范围足够大。
7.2 开启本地防火墙端口
#开放端口
firewall-cmd --zone=public --add-port=3478/udp --permanent
firewall-cmd --zone=public --add-port=3478/tcp --permanent
#重启防火墙
firewall-cmd --reload
8. 启动Coturn服务器
turnserver -o -a -f
9. 测试启动状态
访问测试网站:Trickle ICE
开发过程描述
如下仅展示关键性代码解释说明,具体代码请到文章最后获取Gitee源码地址。
后端开发流程
-
websocket连接成功后维护用户连接信息并广播join消息。数据携带用户ID列表。
// 后端维护Session连接的数据结构
private final HashMap<String, WebSocketSession> userMap = new HashMap<>();
-
编写接收信息通用接口,dto对象包含userID,type,data(JSON序列化字符串),接口根据传入userId取出session,给session发送消息对象。
前端开发流程
-
日志系统,监听ice状态及日志打印。
-
创建随机ID,连接ws。
-
协商函数:协商前创建peerConnection对象并监听candidate,当双方都连接成功后调用,判断本地offerFlag状态,如果为true,创建offer设置本地并发送消息给对端。
// STUN 服务器
const iceServers = [{urls: "stun:stun.l.google.com:19302" // Google公开的STUN 服务器},{urls: "stun:自己的STUN服务器IP:3478" // 自己的Stun服务器},{urls: "turn:自己的TRUN服务器IP:3478", // 自己的TURN服务器username: "userName",credential: "Password"}
];// 创建RTC连接对象并监听和获取condidate信息
function createPeerConnection() {wlog("开始创建PC对象...")peerConnection = new RTCPeerConnection(iceServers);wlog("创建PC对象成功")// 创建RTC连接对象后连接websocketinitWebSocket();// 监听网络信息(ICE Candidate)peerConnection.onicecandidate = (event) => {if (event.candidate) {candidateInfo = event.candidate;wlog("candidate信息变化...");// 将candidate信息发送给远端setTimeout(()=>{sendCandidate(event.candidate);}, 150)}};// 监听远端音视频流peerConnection.ontrack = (event) => {nextTick(() => {wlog("====> 收到远端数据流 <=====")if (!remoteVideo.value.srcObject) {remoteVideo.value.srcObject = event.streams[0];remoteVideo.value.play(); // 强制播放}});// remoteVideo.value.srcObject = event.streams[0];};// 监听ice连接状态peerConnection.oniceconnectionstatechange = () => {wlog(`RTC连接状态改变:${peerConnection.iceConnectionState}`);};// 添加本地音视频流到 PeerConnectionlocalStream.getTracks().forEach(track => {peerConnection.addTrack(track, localStream);});
}
-
candidate监听:当监听到candidate后判断双方是否已连接,如果已连接,构造并发送candidate给对端。
-
解析消息处理器
-
解析join:type为join取出userId列表,如果为一个代表仅自己在线,标识为创建offer端,日志打印相关信息,如果有两个者取出对方ID保存,代表双方都上线成功,日志打印,调用协商函数,开始媒体协商和网络协商。
-
解析offer:type为offer,说明收到发起端offer,将offer设置为远端信息,然后创建answer设置到本地,构建answer消息发送给对端。
-
解析answer:type为answer,说明收到接收端应答,取出answer设置为远端消息。
-
解析candidate:type为candidate,说明收到对端的网络信息,取出设置到本地。
-
// 消息处理器 - 解析器
function handleSignalingMessage(message) {wlog("收到ws消息,开始解析...")wlog(message)let parseMsg = JSON.parse(message);wlog(`解析结果:${parseMsg}`);if (parseMsg.type == "join") {joinHandle(parseMsg.data);} else if (parseMsg.type == "offer") {wlog("收到发起端offer,开始解析...");offerHandle(parseMsg.data);} else if (parseMsg.type == "answer") {wlog("收到接收端的answer,开始解析...");answerHandle(parseMsg.data);}else if(parseMsg.type == "candidate"){wlog("收到远端candidate,开始解析...");candidateHandle(parseMsg.data);}}// 远端Candidate处理器
async function candidateHandle(candidate){peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(candidate)));wlog("+++++++ 本端candidate设置完毕 ++++++++");
}// 接收端的answer处理
async function answerHandle(answer) {wlog("将answer设置为远端信息");peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(answer))); // 设置远端SDP
}// 发起端offer处理器
async function offerHandle(offer) {wlog("将发起端的offer设置为远端媒体信息");await peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(offer)));wlog("创建Answer 并设置到本地");let answer = await peerConnection.createAnswer()await peerConnection.setLocalDescription(answer);wlog("发送answer给发起端");// 构造answer消息发送给对端let paramObj = {userId: oppositeUserId,type: "answer",data: JSON.stringify(answer)}// 执行发送const res = await axios.post(`${BaseUrl}/rtcs/sendMessage`, paramObj);
}// 加入处理器
function joinHandle(userIds) {// 判断连接的用户个数if (userIds.length == 1 && userIds[0] == userId) {wlog("标识为发起端,等待对方加入房间...")isRoomEmpty.value = true;// 存在一个连接并且是自身,标识我们是发起端offerFlag = true;} else if (userIds.length > 1) {// 对方加入了wlog("对方已连接...")isRoomEmpty.value = false;// 取出对方IDfor (let id of userIds) {if (id != userId) {oppositeUserId = id;}}wlog(`对端ID: ${oppositeUserId}`)// 开始交换SDP和CandidateswapVideoInfo()}
}
效果演示
初始状态
发起端加入房间
接收端加入房间
Gitee源码地址
源码地址:点击访问Gitee项目源代码。
相关文章:
WebRTC实现双端音视频聊天(Vue3 + SpringBoot)
目录 概述 相关概念 双端连接整体实现步骤概述 文章代码实现注意点 STUN和TURN服务器的搭建 开发过程描述 后端开发流程 前端开发流程 效果演示 Gitee源码地址 概述 文章描述使用WebRTC技术实现一对一音视频通话。 由于设备摄像头限制(一台电脑作测试无法…...
第6章详细设计-6.9 PCB审查
6.9 PCB审查 6.9.1 布局阶段注意事项1.结构设计要求在PCB布局之前弄清楚产品的结构2.布局要求 6.9.2 布线注意事项6.9.3 接地处理(1)射频链路接地。(2)腔壳接地孔。(3)螺钉放置(需要了解结构知识…...
docker与大模型(口语化原理和实操讲解)
文章目录 一、镜像images1)下载安装2)docker images相关命令(保存、删除、上传、别名、搜索镜像) 二、容器container1)展现所有在跑的容器服务ps2)start /restart / kill / stop /rm3)exec /cp4)run/create…...
Linux之vim模式下全选命令
在Linux系统中,使用Vim编辑器进行全选操作可以通过以下几种方式实现: 1.使用键盘快捷键 按下 ”ggVG”(先按下”g”,再按下”g”,再按下”V”,最后按下”G”)可以全选当前文件内容。其中 ”g…...
云原生周刊:Kubernetes v1.32 要来了
开源项目推荐 Woodpecker Woodpecker 是一款轻量级且功能强大的 CI/CD 引擎,以其高度可扩展性和易用性著称。它支持多种版本控制系统与编程语言,能够灵活适配不同开发流程,帮助团队实现高效的持续集成与交付。无论是个人项目还是大型团队&a…...
# JVM学习
JVM JVM是什么? Java虚拟机(JVM) 是一个抽象的计算机,它是一个运行时环境,用于执行Java字节码或编译后的Java程序。JVM屏蔽了底层操作系统的差异,使得Java程序可以在任何支持JVM的操作系统上运行。 JVM能…...
【代码随想录day33】【C++复健】62.不同路径;63. 不同路径 II;343. 整数拆分;96.不同的二叉搜索树
感觉dp的题真的很适合背,当然不是死记硬背,而是当做一种模板题,出来一道新的题就往模板题上面去靠,如果套对模板的话剩下的事情其实就简单了。所以只要看一遍解法知道大致思路其实就够了,毕竟大部分dp的代码也不算难写…...
《勇者斗恶龙3:HD-2D重制版》找幽灵船攻略分享
《勇者斗恶龙3:HD-2D重制版》中的幽灵船是游戏里非常独特的一个区域,而想要找到幽灵船的话还是比较麻烦的,首先是听到关于幽灵船在世界海域上航行的传闻,包括在海盗巢穴中,但幽灵船的出现有一些具体条件。 勇者斗恶龙3…...
基于 MATLAB 的模拟退火算法详解及实现
以下是一篇更详细的关于 模拟退火算法 (Simulated Annealing) 的 MATLAB 实现的教程和代码示例,涵盖基本概念、核心思想和代码实现。 一、模拟退火算法简介 模拟退火算法(Simulated Annealing,简称 SA)是一种随机优化算法&#x…...
MQTT 服务器常用的有哪些?
MQTT(Message Queuing Telemetry Transport)是一种轻量级的消息传输协议,常用于物联网(IoT)设备之间的通信。以下是一些常用的 MQTT 服务器(也称为 MQTT Broker): 1.Eclipse Mosqui…...
【android USB 串口通信助手】stm32 源码demo 单片机与手机通信 Android studio 20241118
android 【OTG线】 接 下位机STM32【USB】 通过百度网盘分享的文件:USBToSerialPort.apk 链接:https://pan.baidu.com/s/122McdmBDUxEtYiEKFunFUg?pwd8888 提取码:8888 android 【OTG线】 接 【USB转TTL】 接 【串口(下位机 SMT32等)】 需…...
汽车资讯新探索:Spring Boot技术引领
摘要 随着信息技术在管理上越来越深入而广泛的应用,管理信息系统的实施在技术上已逐步成熟。本文介绍了汽车资讯网站的开发全过程。通过分析汽车资讯网站管理的不足,创建了一个计算机管理汽车资讯网站的方案。文章介绍了汽车资讯网站的系统分析部分&…...
简单的MCU与FPGA通过APB总线实现通讯(fpga mcu APB):乘法器为例
测试平台: GW1N4器件内置 M1内核;并且可以设置 APB总线与fpga 逻辑进行交互; 框图: +---------------------+ | | | M1 Microprocessor | <-----------------+ | | | | +-----------------…...
css uniapp背景图宽度固定高度自适应可以重复
page {height: 100%;background-image: url(https://onlinekc.a.hlidc.cn/uploads/20241115/350f94aaf493d05625a7ddbc86c7804e.png);background-repeat: repeat;background-size: contain;} 如果不要重复 把background-repeat: repeat;替换background-repeat: no-repeat;...
深度学习--优化器
笔记内容侵权联系删 优化器 在梯度下降算法中,有各种不同的改进版本。在面向对象的语言实现中,往往把不同的梯度下降算法封装成一个对象,称为优化器。 算法改进的目的,包括但不限于: 加快算法收敛速度; 尽量避过或冲过局部极值; …...
【嵌入式】关于push老仓库到新仓库的方法
1. 背景 公司项目经常会有需要从开源项目中镜像代码过来的活,所以常常会在自己的服务器上创建一个对应的仓库,然后使用命令将期push过去。为方便日后抄命令,这里记录一下使用的命令。 2. 操作步骤 2.1. 已下载的代码push 特别提醒: 使用此脚本前请确保你修改的代码已保存…...
从线下到线上,上门洗衣服务如何实现智能化升级?
在现代快节奏生活的推动下,上门洗衣服务作为一种新兴的服务模式正逐渐崭露头角。它以其便捷性和创新性,改变了传统洗衣行业的格局,为消费者提供了全新的选择,同时也为洗衣品牌带来了新的机遇与挑战。 一、上门洗衣服务的市场现状1…...
SQL字段来源表的解析
测试例子: SELECT e.NAME, d.DEPT_NAME,d.DEPT_ID,EMP_ID,100EMP_ID100 FROM EMP e JOIN DEPT d ON e.DEPT_ID d.DEPT_ID WHERE e.EMP_ID IN (SELECT EMP_ID FROM EMP WHERE DEPT_ID 10) 代码示例: package com.test; import org.apache.calcite.jd…...
理解 Python 解释器:CPython 与 IPython 的比较及选择指南
理解 Python 解释器:CPython 与 IPython 的比较及选择指南 在选择适合自己需求的 Python 解释器时,理解 CPython 和 IPython 之间的主要差异至关重要。本文将详细解释 CPython 和 IPython 的特性、优势和适用场景,以帮助用户做出明智的选择。…...
Java NIO 深度解析:构建高效的 I/O 操作
在 Java 编程领域,I/O 操作一直是至关重要的部分,它直接影响着应用程序的性能和响应能力。Java NIO(New I/O)作为传统 I/O 的增强版本,为处理大量并发连接和高效的数据传输提供了更强大的工具和机制。本文将深入探讨 J…...
总结拓展十六:特殊采购业务——VMI采购模式
1、VMI的定义 VMI采购模式(Vendor Managed Inventory)是一种合作性策略,旨在通过供应商管理库存,使供应链中的企业和供应商双方都能获得最低成本。在这种模式下,供应商根据共享的用户企业库存和实际耗用数据&#x…...
vue2 + iview(view-design) 中封装使用 vxe-table 处理表格渲染大量数据卡顿现象
今天遇到需求,iview组件分页每页100页时候页面卡顿现象严重,改造为使用vxe-table cell-mouseenter"handleCellMouseEnter" cell-mouseleave"handleCellMouseLeave" 这两个用来处理vxe-table 内容过多鼠标悬浮上去滚动 tooltip直接…...
初学者指南:知识库问答(KBQA)多跳路径的核心与应用
初学者指南:知识库问答(KBQA)多跳路径的核心与应用 知识库问答(Knowledge Base Question Answering, KBQA)旨在利用结构化知识库(如Wikidata、Freebase)回答自然语言问题。在实际应用中&#x…...
创建springboot+vue项目相关配置问题
安装并配置jdk23 在官网下载jdk Java Downloads | Oracle 中国 下载完成后双击即可安装。 安装完成后配置环境变量 此电脑->右键->属性->高级系统设置 然后一直点击确定即可。 键盘上win r java -version 可以验证是否配置成功 下载并配置maven 在官网下…...
基于AOA算术优化的KNN数据聚类算法matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于AOA算术优化的KNN数据聚类算法matlab仿真。通过AOA优化算法,搜索最优的几个特征数据,进行KNN聚类,同时对比不同个数特征下…...
【机器学习】在泊松分布中,当λ值较大时,其近似正态分布的误差如何评估?
在泊松分布中,当参数 λ 较大时,其近似正态分布的有效性可以通过 中心极限定理 和误差分析来理解和评估。以下内容结合理论推导和实际案例展开说明: 1. 泊松分布的定义 泊松分布是用于建模单位时间或单位空间内随机事件发生次数的概率分布&a…...
ABAP开发-面向对象开发_2
系列文章目录 文章目录 系列文章目录[TOC](文章目录) 前言接口和类1、首先创建一个接口2、在创建的接口的基础上创建一个类PERSON3、创建子类STUDENT4、创建子类TEACHER5、SE38使用创建的类 总结 前言 接口和类 全局类 SE24 创建一个接口-》创建一个实现接口的类-》再创建两个…...
微信小程序-prettier 格式化
一.安装prettier插件 二.配置开发者工具的设置 配置如下代码在setting.json里: "editor.formatOnSave": true,"editor.defaultFormatter": "esbenp.prettier-vscode","prettier.documentSelectors": ["**/*.wxml"…...
241118学习日志——[CSDIY] [ByteDance] 后端训练营 [06]
CSDIY:这是一个非科班学生的努力之路,从今天开始这个系列会长期更新,(最好做到日更),我会慢慢把自己目前对CS的努力逐一上传,帮助那些和我一样有着梦想的玩家取得胜利!!&…...
Android WMS概览
WMS(WindowManagerService)是 Android 系统的核心服务,负责管理应用和系统的窗口,包括窗口的创建、销毁、布局、层级管理、输入事件分发以及动画显示等。它通过协调 InputManager 和 SurfaceFlinger 实现触摸事件处理和窗口渲染&a…...
wordpress 访问/东莞疫情最新消息通知
最近遇到使用zxing生成的一维条码打印出来的条码图形很模糊根本识别不了。其实原因只有一句话: bitmap没有直接使用PrintDocument的Graphics画布进行绘制,而是中间处理了一下外部传过来一个图片,这个图片看起来很成像质量很好,但其…...
邵东网站建设 www.quan-web.com/seo网站优化工具
面试的时候,如果要手写算法题目,判断一个数是不是素数,可以说是非常常见的问题了,这道题目回答并不算难,但是想要以优雅高效的方法回答,却并不轻松,下面我会介绍三种方式,时间复杂度…...
重庆政府服务网/seo顾问培训
今天是 201220122012 年 444 月 121212 日星期四,编写程序,输入今天开始到 121212 月 313131 日之间的任意日期,输出那一天是星期几。 输入格式 共两行,第一行包含一个整数 mmm,表示询问日期的月份。 第二行包含一个…...
自己做网站怎么做/竞价培训
文件上传是很常见的实用功能。本文演示如何用Express实现文件上传功能。例程本例需要两个文件及一个目录test39.js:主程序;index.html:用于上传文件的前端页面;temp_folder:存放被上传的文件。test39.js:va…...
怎么做能让网站收录的快/搜索引擎营销实训报告
1.今年又遇到些事,很纠结,又修练了一次;2.又颓废了段时间,该收收心认真做点事情了;3.最近看书的时间少了,得好好静下心来多看点书;4.性格还是那么地急、快,对于做事有利也有弊&#…...
如何做合格的新闻网站编辑/国外域名购买
很早就想写这样一篇博文了,可是一直没来得及动笔。在学校的时候,时间似乎总是不够用,因为一旦有点时间,你就想是不是该用来多看点论文。所以我很高兴,工作的生活给了我真正自由的时间,让我可以多分享一些自…...