WebRTC直播间搭建记录

考虑到后续增加平台直播的可能性,笔记记录一下WebRTC相关.

在这里插入图片描述

让我们分别分析两种情况下的WebRTC连接建立过程:

情况一:AB之间可以直接通信

1.信令交换:
设备A和设备B首先通过信令服务器交换SDP(Session Description Protocol)信息和候选者(candidates)。SDP包含有关会话的信息,包括设备的媒体能力和网络地址。

2.ICE框架协商:
设备A和设备B使用ICE框架收集本地候选者(本地网络地址),然后交换候选者信息,包括通过STUN服务器获取的公共IP地址和端口号。

3.连接建立:
根据ICE框架收集的候选者信息,设备A和设备B尝试直接建立点对点连接。根据候选者优先级排序,选择最佳的候选者进行连接。

4.直接通信:
如果ICE框架成功建立了连接,设备A和设备B之间可以直接进行实时通信,例如音频、视频或数据传输。

情况二:AB之间不能直接通信
1.信令交换:
设备A和设备B通过信令服务器交换SDP信息和候选者。

2.ICE框架协商:
设备A和设备B分别收集本地候选者,并将候选者信息发送给对方。

3.无法直接建立连接:
如果ICE框架无法直接建立连接(例如由于双方都位于NAT环境或防火墙后),则ICE框架会返回无法建立连接的错误或超时。

4.使用TURN服务器:
在无法直接建立连接的情况下,设备A和设备B将使用TURN服务器作为中继器来中转数据流。设备A和设备B分别连接到TURN服务器,并将数据流通过TURN服务器进行中转,从而实现对等通信。

什么情况下AB不能直接建立点对点连接?

双方位于不同的私有网络:如果设备A和设备B都处于不同的私有网络(例如家庭网络),则无法直接访问对方的局域网地址。

AB之间无法直接建立点对点连接的情况通常是由于网络地址转换(NAT)或防火墙的存在,导致设备无法直接接收对方发送的数据包。具体情况包括:

NAT类型限制:如果设备A或设备B位于对称NAT或受限NAT网络中,会导致UDP包的发送和接收出现问题,从而无法建立直接连接。

防火墙限制:防火墙可以阻止对UDP或特定端口的访问,这会影响设备之间的直接通信。

公共IP地址不可用:有些设备可能没有公共IP地址,而是通过NAT路由器共享局域网IP地址。

在这些情况下,WebRTC利用ICE框架和STUN/TURN服务器来实现网络穿透,确保设备之间可以建立可靠的实时通信连接。通过STUN服务器获取公共网络地址,通过TURN服务器进行数据中转,解决了设备间无法直接通信的问题。

附一下个人直播间搭建部分代码

<html>

<head>
    <title>简单直播间</title>
    <style type="text/css">
    body {
        background: #888888 center center no-repeat;
        color: white;
    }

    button {
        cursor: pointer;
        user-select: none;
    }

    .room {
        border: 1px solid black;
        cursor: pointer;
        user-select: none;
        text-align: center;
        background: rgba(0, 0, 0, 0.5);
    }

    video {
        width: 75%;
        border: 2px solid black;
        border-image: linear-gradient(#F80, #2ED) 20 20;
    }

    #chat {
        position: fixed;
        top: 5px;
        right: 5px;
        width: calc(25% - 30px);
        height: calc(75% - 10px);
        background: rgba(0, 0, 0, 0.5);
    }

    .content {
        height: calc(100% - 66px);
        margin-top: 5px;
        margin-bottom: 5px;
        overflow-y: scroll;
    }

    #chatSend {
        white-space: nowrap;
    }

    #chatSend>input {
        width: calc(100% - 90px);
    }

    #chatSend>button {
        width: 80px;
    }

    #chatTag {
        margin-left: 10px;
        line-height: 30px;
    }

    .chatKuang {
        border: 1px solid white;
        margin: 3px;
        border-radius: 5px;
    }

    .hintKuang {
        margin: 3px;
        text-align: center;
    }
    </style>
</head>

<body>
    <div id="create">
        名称:<input id="name" type="text" />
        <button onclick="createRoom()">创建直播间</button>
        <span id="count"></span>
    </div>
    <video id="localVideo" autoplay controls="controls"></video>
    <br />
    <div id="chat">
        <div id="chatTag">
            <button onclick="changeTag(0)">房间列表</button>
            <button onclick="changeTag(1)">房间聊天</button>
        </div>
        <div id="roomContent" class="content"></div>
        <div id="chatContent" class="content" style="display: none"></div>
        <div id="chatSend">
            <input type="text" />
            <button onclick="chatSend()">发送</button>
        </div>
    </div>
    <script type="text/javascript">
    // 背景图片
    (function() {
        var img = new Image();
        img.addEventListener("load", function() {
            document.querySelector("body").style.background =
                "url('" + this.src + "') center center no-repeat";
            let _img = this;
            calculateBackgroundImageScale(_img);
            window.onresize = function(_img) {
                calculateBackgroundImageScale(_img);
            }
        });

        img.src = "https://parva.cool/share/sky043.jpg";
    })();

    //计算背景图片缩放(自适应窗口大小)
    function calculateBackgroundImageScale(img) {
        let w1 = document.body.clientWidth;
        let h1 = document.body.clientHeight;
        let w2 = img.width;
        let h2 = img.height;
        let scale1 = w1 / w2;
        let scale2 = h1 / h2;
        let scale = scale1 > scale2 ? scale1 : scale2;
        document.querySelector("body").style.backgroundSize =
            Math.ceil(w2 * scale) + "px " + Math.ceil(h2 * scale) + "px";
    }

    // 存储本地媒体流
    var localStream;

    // 与服务器的websocket通信
    var socket = new WebSocket("wss://parva.cool/rtc");

    // 判断自己是否正在直播
    var isMe;

    // 当前的标签页(0:房间列表, 1:房间聊天)
    var tag = 0;

    // 发送聊天信息
    function chatSend() {
        let input = document.querySelector("#chatSend input");
        let msg = input.value;
        input.value = "";
        if (Object.keys(pcs).length == 0) return;
        if (isMe) {
            socket.send(JSON.stringify({ event: "chatSend", msg: msg }));
        } else {
            socket.send(JSON.stringify({
                event: "chatSend",
                msg: msg,
                roomName: Object.keys(pcs)[0]
            }));
        }
    }

    // 切换标签
    function changeTag(t) {
        tag = t;
        document.querySelector("#roomContent").style.display = "none";
        document.querySelector("#chatContent").style.display = "none";
        if (tag == 0) {
            document.querySelector("#roomContent").style.display = "block";
        } else if (tag == 1) {
            document.querySelector("#chatContent").style.display = "block";
        }
    }

    // 创建直播间
    function createRoom() {
        let roomName = document.querySelector("#name").value;
        if (!localStream) {
            navigator.mediaDevices.getDisplayMedia({ video: true, audio: true })
                .then((stream) => {
                    localStream = stream;
                    socket.send(JSON.stringify({
                        event: "createRoom",
                        roomName: roomName
                    }));
                });
        } else {
            socket.send(JSON.stringify({
                event: "createRoom",
                roomName: roomName
            }));
        }
    }

    // 关闭直播间
    function closeRoom() {
        socket.send(JSON.stringify({ event: "closeRoom" }));
        document.querySelector("input").disabled = false;
        document.querySelector("button").innerHTML = "创建直播间";
        document.querySelector("button").setAttribute("onclick",
            "createRoom()");
        document.querySelector("video").srcObject = null;
        pcs = {};
        let content = document.querySelector("#chatContent");
        content.innerHTML = "";
        changeTag(0);
        let count = document.querySelector("#count");
        count.innerHTML = "";
        localStream.getTracks()[0].stop();
    }

    // 进入直播间
    var 防止双击;
    var 防止双击setTimeout;

    function joinRoom(roomName) {
        if (防止双击 == roomName) return;
        else 防止双击 = roomName;
        clearTimeout(防止双击setTimeout);
        防止双击setTimeout = setTimeout(function() { 防止双击 = ""; }, 800);
        let name = document.querySelector("#name").value;
        socket.send(JSON.stringify({
            event: "joinRoom",
            name: name,
            roomName: roomName
        }));
    }

    // 接收服务器的消息
    socket.onmessage = function(event) {
        let json = JSON.parse(event.data);

        // 接收聊天信息
        if (json.event === "chat") {
            let chatName = document.createElement("span");
            chatName.innerHTML = json.name + " : ";
            chatName.setAttribute("class", "chatName");
            let chatMessage = document.createElement("span");
            chatMessage.innerHTML = json.msg;
            chatName.setAttribute("class", "chatMessage");
            let chatKuang = document.createElement("div");
            chatKuang.setAttribute("class", "chatKuang");
            chatKuang.appendChild(chatName);
            chatKuang.appendChild(chatMessage);
            let content = document.querySelector("#chatContent");
            content.appendChild(chatKuang);
            content.scrollTop = 9999999;
        }

        // 通知新人加入房间
        if (json.event === "joinHint") {
            let hintKuang = document.createElement("div");
            hintKuang.setAttribute("class", "hintKuang");
            hintKuang.innerHTML = json.name + "加入直播房间!"
            let content = document.querySelector("#chatContent");
            content.appendChild(hintKuang);
            content.scrollTop = 9999999;
        }

        // 有人退出当前房间
        if (json.event === "quitHint") {
            let hintKuang = document.createElement("div");
            hintKuang.setAttribute("class", "hintKuang");
            hintKuang.innerHTML = json.name + "退出房间.."
            let content = document.querySelector("#chatContent");
            content.appendChild(hintKuang);
            content.scrollTop = 9999999;
        }

        // 接收房间人数
        if (json.event === "count") {
            let count = document.querySelector("#count");
            count.innerHTML = "\t在场众神数量 : " + json.count;
        }

        // 所有房间的信息
        if (json.event === "roomsInfo") {
            if (tag != 0) return;
            let content = document.querySelector("#roomContent");
            content.innerHTML = "";
            for (let i = 0; i < json.info.length; i++) {
                let div = document.createElement("div");
                div.innerHTML = json.info[i];
               
                //点击进入房间事件捆绑
                div.setAttribute("onclick", "joinRoom('" + json.info[i] + "')");
                div.setAttribute("class", "room");
                content.appendChild(div);
            }
        }

        // 创建房间失败
        if (json.event === "createRoomFailed") {
            alert("创建房间失败,名称已存在 或 名称格式有误");
            localStream.getTracks()[0].stop();
        }

        // 创建房间成功
        if (json.event === "createRoomOk") {
            document.querySelector("input").disabled = true;
            document.querySelector("button").innerHTML = "关闭直播间";
            document.querySelector("button").setAttribute("onclick",
                "closeRoom()");
           
           //将捕捉的本地媒体流赋值到直播窗口
            document.querySelector("video").srcObject = localStream;
            document.querySelector("video").volume = 0;
            isMe = true;
            changeTag(1);
            let hintKuang = document.createElement("div");
            hintKuang.setAttribute("class", "hintKuang");
            hintKuang.innerHTML = "创建直播房间成功!"
            let content = document.querySelector("#chatContent");
            content.appendChild(hintKuang);
            content.scrollTop = 9999999;
        }

        // 加入房间失败
        if (json.event === "joinRoomFailed") {
            alert("加入房间失败,名称已存在 或 名称格式有误");
        }

        // 主播下播,退出房间
        if (json.event === "roomClosed") {
            document.querySelector("video").srcObject = null;
            pcs = {};
            let content = document.querySelector("#chatContent");
            content.innerHTML = "";
            changeTag(0);
            document.querySelector("input").disabled = false;
            let count = document.querySelector("#count");
            count.innerHTML = "";
        }

        // 有人欲加入我的直播间
        if (json.event === "joinRoom") {
            // 为对方创建一个pc实例,并发送offer给他
            var pc = createRTCPeerConnection(json.name);
            // rtc建立连接:创建一个offer给对方
            pc.createOffer(function(desc) {
                pc.setLocalDescription(desc);
                socket.send(JSON.stringify({
                    event: "_offer",
                    data: {
                        sdp: desc,
                        nameB: json.name
                    }
                }));
            }, function(error) {
                console.log("CreateOffer Failure callback: " + error);
            });
        }

        // rtc建立连接:接收到offer
        if (json.event === "_offer") {
            // 为对方创建一个pc实例,并发送offer给他
            var pc = createRTCPeerConnection(json.data.nameA);
            pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp));
            // rtc建立连接:创建一个answer给对方
            pc.createAnswer(function(desc) {
                pc.setLocalDescription(desc);
                socket.send(JSON.stringify({
                    event: "_answer",
                    data: {
                        sdp: desc,
                        nameA: json.data.nameA
                    }
                }));
            }, function(error) {
                console.log("CreateAnswer Failure callback: " + error);
            });
        }

        // rtc建立连接:接收到answer   --A收到B的answer
        // 将来自对方的 Answer SDP 设置为本地 WebRTC 连接的远程描述,以便双方能够正确地理解和处理对方的媒体数据,从而建立成功的实时通信连接。
        if (json.event === "_answer") {

            pcs[json.data.nameB].setRemoteDescription(
                new RTCSessionDescription(json.data.sdp));
        }

        // rtc建立连接:接收到_ice_candidate
        if (json.event === "_ice_candidate") {
            pcs[json.data.from].addIceCandidate(
                new RTCIceCandidate(json.data.candidate));
        }
    }

    // Q:一对多直播情况下 直播用户A的远程描述需要怎么变化?
    // A:连接都是独立的 分别设置对应的远端描述符 A的描述符不变



    // 存储pc实例
    var pcs = {};

    // stun和turn服务器URL及配置
    var iceServer = {
        iceServers: [
            { urls: "stun:parva.cool:3478" },
            {
                urls: "turn:parva.cool:3478",
                username: "parva",
                credential: "Parva089"
            }
        ]
    };

    // 创建RTCPeerConnection实例
    function createRTCPeerConnection(name) {
        let pc = new RTCPeerConnection(iceServer);
        if (!isMe) {
            for (let n in pcs) pcs[n].close();
            pcs = {};
        }
        // 以{对方的名字:PC实例}键值对形式把PC实例存储起来
        pcs[name] = pc;
        if (localStream) pc.addStream(localStream);
        else pc.addStream(new MediaStream());

        pc.onicecandidate = function(event) {
            if (event.candidate !== null)
                socket.send(JSON.stringify({
                    event: "_ice_candidate",
                    data: {
                        candidate: event.candidate,
                        to: Object.keys(pcs).find(k => pcs[k] == pc)
                    }
                }));
        }

        pc.ontrack = function(event) {
            if (isMe) return;
            changeTag(1);
            document.querySelector("video").srcObject = event.streams[0];
            document.querySelector("input").disabled = true;
            let hintKuang = document.createElement("div");
            hintKuang.setAttribute("class", "hintKuang");
            hintKuang.innerHTML = "成功进入直播间!"
            let content = document.querySelector("#chatContent");
            content.innerHTML = "";
            content.appendChild(hintKuang);
            content.scrollTop = 9999999;
        }

        return pc;
    }
    </script>
</body>

</html>

建立 BC 和 A 之间的 WebRTC 连接涉及以下步骤和流程:

前提条件

A 是直播主播,已经在直播间创建了媒体流并开始直播。
B 和 C 是观众,希望加入直播间并观看 A 的直播。

建立 WebRTC 连接的过程:
1.B 或 C 加入直播间:
B 或 C 在前端页面选择要加入的直播间并点击加入按钮。
前端通过 WebSocket 向服务器发送加入房间的请求,包括用户信息和房间名称。

2.服务器收到加入房间请求:
后端服务器接收到 B 或 C 的加入房间请求,将其添加到对应房间的用户列表中。

3.A 发送 WebRTC offer 给 B 或 C:
当 B 或 C成功加入房间后,后端服务器会通知 A(主播)有新用户加入了直播间。
A 在前端收到加入房间的通知后,使用 WebRTC 创建一个 PeerConnection 对象(pc),并生成一个 SDP offer。
A 将这个 SDP offer 通过 WebSocket 发送给服务器。

4.服务器转发 WebRTC offer 给 B 或 C:
后端服务器收到 A 发送的 SDP offer。
后端服务器将 SDP offer 转发给房间内除 A 之外的其他用户(即 B 或 C)。

5.B 或 C 接收 WebRTC offer:
B 或 C 前端收到 A 发送的 SDP offer。
B 或 C 使用 WebRTC 创建一个 PeerConnection 对象(pc),并设置 A 的 SDP offer 作为远端描述(setRemoteDescription)。

6.B 或 C 创建 WebRTC answer 给 A:
B 或 C 使用自己的本地媒体流(视频和音频)创建一个 SDP answer。
B 或 C 将这个 SDP answer 发送给服务器。

7.服务器转发 WebRTC answer 给 A:
后端服务器收到 B 或 C 发送的 SDP answer。
后端服务器将 SDP answer 转发给 A。

8.A 接收 WebRTC answer:
A 前端收到 B 或 C 发送的 SDP answer。
A 设置 B 或 C 的 SDP answer 作为远端描述(setRemoteDescription)。

9.ICE 候选者交换:
PeerConnection 开始收集和交换 ICE 候选者信息(网络地址、端口等)。

10.B、C 和 A 通过服务器交换 ICE 候选者信息,以便彼此建立直接的通信路径。
建立直播观看连接:

完成以上步骤后,B 或 C 和 A 之间的 WebRTC 连接就建立起来了。
B 或 C 的本地媒体流通过 ICE 候选者的协商直接传输到 A,A 的直播内容会被 B 或 C 观看。

总结:

通过以上步骤,B 或 C 可以加入 A 创建的直播间,并与 A 建立起 WebRTC 连接,实现实时的音视频传输和观看直播内容。整个过程涉及前端的用户交互和媒体流处理,以及后端的 WebRTC 信令传递和 ICE 候选者交换,共同实现了观众与主播之间的实时通信和直播功能。

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

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

相关文章

科技驱动未来,提升AI算力,GPU扩展正当时

要说这两年最火的科技是什么&#xff1f;我想“AI人工智能”肯定是最有资格上榜的&#xff0c;尤其ChatGPT推出后迅速在社交媒体上走红&#xff0c;短短5天&#xff0c;注册用户数就超过100万&#xff0c;2023年一月末&#xff0c;ChatGPT的月活用户更是突破1亿&#xff0c;成为…

嵌入式4-16

tftpd #include <myhead.h> #define SER_IP "192.168.125.243" //服务器IP地址 #define SER_PORT 69 //服务器端口号 #define CLI_IP "192.168.125.244" //客户端IP地址 #define CLI_PORT 8889 //客户端端…

C#创建磁性窗体的方法:创建特殊窗体

目录 一、磁性窗体 二、磁性窗体的实现方法 (1)无标题窗体的移动 (2)Left属性 (3)Top属性 二、设计一个磁性窗体的实例 &#xff08;1&#xff09;资源管理器Resources.Designer.cs设计 &#xff08;2&#xff09;公共类Frm_Play.cs &#xff08;3&#xff09;主窗体 …

Java Spring 框架下利用 MyBatis 实现请求 MySQL 数据库的存储过程

Java Spring 框架下利用 MyBatis 实现请求 MySQL 数据库的存储过程 环境准备与前置知识1. 创建 MySQL 存储过程2. 配置数据源3. 创建实体类4. 创建 Mapper 接口5. 创建 Mapper XML 文件6. 创建 Service 接口及Impl实现类7. 创建 Controller 类8. 测试与总结 在现代的 Web 应用开…

STM32 F103 C8T6开发笔记14:与HLK-LD303-24G测距雷达通信

今日尝试配通STM32 F103 ZET6与HLK-LD303-24G测距雷达的串口通信解码 文章提供测试代码...... 目录 HLK-LD303-24G测距雷达外观&#xff1a; 线路连接准备&#xff1a; 定时器与串口配置准备&#xff1a; 定时器2的初始化&#xff1a; 串口1、2初始化&#xff1a; 串口1、2自定…

ARP代理

10.1.0.1/8 和10.2.0.1/8是在同一个网段 10.1.0.2/16 和10.2.0.2/16 不在同一个网段 10.1.0.1/8 和10.1.0.2/16 是可以ping通的 包发出来了&#xff0c;报文有发出来&#xff0c;目的地址是广播包 广播请求&#xff0c;发到路由器的接口G 0/0/0 target不是本接口&#xff0…

【C++学习】C++IO流

这里写目录标题 &#x1f680;C语言的输入与输出&#x1f680;什么是流&#x1f680;CIO流&#x1f680;C标准IO流&#x1f680;C文件IO流 &#x1f680;C语言的输入与输出 C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键盘)读取…

windows网络驱动开发

基石&#xff1a;WFP 1、简介 Windows过滤平台&#xff08;Windows Filtering Platform, WFP&#xff09;&#xff0c;是从Vista系统后新增的一套系统API和服务。开发者可以在WFP框架已划分的不同分层中进行过滤、重定向、修改网络数据包&#xff0c;以实现防火墙、入侵检测系…

pdf做批注编辑工具 最新pdf reader pro3.3.1.0激活版

PDF Reader Pro是一款功能强大的PDF阅读和编辑工具。它提供了多种工具和功能&#xff0c;帮助用户对PDF文档进行浏览、注释、编辑、转换和签名等操作。以下是PDF Reader Pro的一些主要特色&#xff1a; 最新pdf reader pro3.3.1.0激活版下载 多种查看模式&#xff1a;PDF Reade…

上海计算机学会 2023年10月月赛 乙组T4 树的覆盖(树、最小点覆盖、树形dp)

第四题&#xff1a;T4树的覆盖 标签&#xff1a;树、最小点覆盖、树形 d p dp dp题意&#xff1a;求树的最小点覆盖集的大小和对应的数量&#xff0c;数量对 1 , 000 , 000 , 007 1,000,000,007 1,000,000,007取余数。所谓覆盖集&#xff0c;是该树的点构成的集合&#xff0c;…

vue:如何通过两个点的经纬度进行距离的计算(很简单)

首先假设从api获取到了自己的纬经度和别人的纬经度 首先有一个概念需要说一下 地球半径 由于地球不是一个完美的球体&#xff0c;所以并不能用一个特别准确的值来表示地球的实际半径&#xff0c;不过由于地球的形状很接近球体&#xff0c;用[6357km] 到 [6378km]的范围值可以…

板式热交换器强度

1、不同标准中对于板换压板的规定 (1) NB/T 47004.1-2017《板式热交换器 第1部分&#xff1a;可拆卸板式热交换器》6.3压紧板6.3.3条“压紧板应有足够的刚性&#xff0c;以保证板式热交换器在正常操作状态不发生泄漏”。 (2) NB/T 47004-2009《板式热交换器》5.3紧板5.3.3条“…

Springboot+Vue项目-基于Java+MySQL的蜗牛兼职网系统(附源码+演示视频+LW)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &…

每日一题 — 串联所有单词的子串

30. 串联所有单词的子串 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a;因为words里面的每一个字符串的长度都是固定的&#xff0c;所以可以将题转换成字符在字符串中的所有异位词 设出哈希表定义left和right进窗口维护count判断出窗口维护count 代码&#xff1a; …

[html]一个动态js倒计时小组件

先看效果 代码 <style>.alert-sec-circle {stroke-dasharray: 735;transition: stroke-dashoffset 1s linear;} </style><div style"width: 110px; height: 110px; float: left;"><svg style"width:110px;height:110px;"><cir…

【Qt】:界面优化(一:基本语法)

界面优化 一.基本语法1.设置指定控件样式2.设置全局控件样式3.从文件加载样式表4.使⽤Qt Designer编辑样式&#xff08;最常用&#xff09; 二.选择器1.概述2.子控件选择器3.伪类型选择器 三.盒模型 在网页前端开发领域中,CSS是一个至关重要的部分.描述了一个网页的"样式&…

快速删除node_modules依赖包的命令rimraf

1、安装rimraf npm install -g rimraf 2、使用命令删除node_modules rimraf node_modules *** window系统&#xff0c;使用命令很快就删除node_modules ***

Jmeter 场景测试:登录--上传--下载--登出

为了练习Jmeter的使用&#xff0c;今天我要测试的场景是“登录--上传--下载--登出”这样一个过程. 测试的目标是我曾经练手写的一个文件分享系统&#xff0c;它要求用户只有登录后才可以下载想要的文件。 Jmeter总体结构&#xff1a; 第一步&#xff1a;添加HTTP Cookie管理器…

微信公众号-获取用户位置

目前获取方式为&#xff0c;在用户进入公众号时&#xff0c;提示是否允许获取地理位置&#xff0c;允许后&#xff0c;将地理位置在每次进入公众号时上报给公众号。 则可以根据公众号开发文档&#xff0c;进行上报提示&#xff0c;例如引入邮件系统&#xff0c;进行管理员提示&…

vscode如何方便地添加todo和管理todo

如果想在vscode中更加方便的添加和管理TODO标签&#xff0c;比如添加高亮提醒和查看哪里有TODO标签等&#xff0c;就可以通过安装插件快速实现。 安装插件 VSCode关于TODO使用人数最多的插件是TODO Height和Todo Tree 按住 CtrlShiftX按键进入应用扩展商店&#xff0c;输入to…