这一路:从HTTP到WebRTC音视频通话

2,880 阅读10分钟

HTTP简述:

  • http 1.x

    1,http/1.0 支持POST HEAD等请求方法,浏览器每次请求都需要与服务器建立一个TCP连接,请求处理完后立即断开TCP连接。

    2,http/1.1支持PUT、DELETE等请求方法,采用持久连接,多个请求可以共用同一个TCP连接。

  • http2.0

    1,在应用层与传输层之间,增加了一个二进制分帧层。

    2,多路复用,可以实现并行发送多个请求。http 1.x版本是长连接,但多个请求发送是串行。

    3,可以对浏览器等待的关键的数据流设置优先级,对数据流可以采用不同的优先级策略。

    4,更多细节...

  • https http的安全版,运行在安全套接字协议(SSL)或传输层安全协议(TLS)之上,建立了一个信息安全通道,所有的TCP中传输的内容都需要经过加密,保证了数据安全。连接端口是443。TLS后文有简述。

  • tcp传输控制协议:是全双工通信。

    1,首部至少20个字节;UDP首部只有8个字节(源端口号+目的端口号+UDP长度+UDP校验和 均是16位)。

    2,可靠传输(ARQ:超时自动重传请求;滑动窗口协议,是点对点的通信控制)

    3,流量控制(根据接收方窗口缓冲区控制发送方的发送速率)

    4,拥塞控制(举例:慢开始:指数增加;拥塞避免:加法线性增加;快恢复+快重传:当发现带宽网络拥塞时,将ssthresh阈值减为拥塞峰值的一半,然后进行快恢复且使用加法增大传输数据包。 ps:通过wireshark抓包一个TCP连接的数据展示如图:

data.png

  • tcp三次握手之后TLS握手:(三次握手四次挥手略,老生常谈)

    1,客户端会发送一个ClientHello请求,内容包括支持的TLS版本、加密套件以及一个随机数。

    2,服务器收到后会响应ServerHello(会包含一个随机数)、下发服务器自己的证书(客户端可根据证书列表check服务器是否可信)和ServerKeyExchange(包含服务器随机生成的一对密钥中的公钥pubkey),然后响应ServerHelloDone。

    3,客户端进行ClientKeyChange操作,向服务器发送的内容中,包含公钥pubkey加密后的第三个随机数(预主密钥),以及Change Cipher Spec告诉服务器用商定好的算法和密钥进行加密,然后告诉服务器Encrypted Handshake message。

    4,服务器收到后用私钥解密获取到预主密钥并响应Encrypted Handshake message 。

    5,基于上述通信,客户端和服务器都具备了相同的两个随机数和预主密钥,然后通过约定好的算法生成最终的会话密钥,此后通过对称密码进行数据传输。

  • 数字证书(数字签名):私钥加密公钥解密,文章内容会经过哈希算法参与其中,因此可验证文件完整性。

    数字签名的过程:

    1,用户将主体信息(包含一串随机数后面作为私钥privatekey)发给CA,CA会利用主体信息生成主体文件的公钥并且进行签名,签完名的文件即是数字证书,返回给用户。 2,用户将文件通过单向散列函数生成128位的摘要,然后用privatekey私钥对此摘要进行加密。

    3,当用户将文件,加密的摘要,公钥publickey发送其他用户时。他人可以对文件进行同样的单向散列函数产生的摘要h1和用publicekey对加密后的摘要解密生成的摘要h2进行比对。如果相等,则文件合法未被篡。

    客户端端拿到服务器下发的证书时,会对文件信息进行hash算法得到一个数字指纹h1,以及通过用ca公钥(本地保存的)解密签名会得到一个数字指纹h2,如果h1和h2一致,则证书合法。如果是客户端拿到服务器发送的签名证书,则会向CA验证证书的合法性。一般浏览器出厂时会内置了诸多CA机构的数字证书校验方法。 拓展:在区块链的实际使用上,币交易时,input买方需要拿到output卖方的公钥。

客户端与服务器的交互

  • JWT:json web token

    1,普通的token:服务端验证token信息要进行数据的查询操作;而JWT验证token信息并不用,在服务端使用密钥校验就可以,无需查询数据库。

    2,组成:Header头部(base64后)+Playload负载(base64后)+signature签名

    3,工作流程

)6O6MY8JP{I2AFLXE4BSPS1.png

  • 跨域: 服务器响应头设置Cors,在header中配置"Access-Control-Allow-Origin",表示允许访问者能够跨域访问,并且指定允许的域。 客户端请求头header中"Origin"字段,表示发起一个针对跨域资源共享的请求。
  • cookie与session :Cookie是基于客户端存储数据到本地,服务端可以返回Cookie交给客户端存储。Session是基于服务器存储数据。 客户端首次向服务端发起建立网络请求后,服务端会建立一个Session,在响应头中配置"SetCookie:JSESSIONID=xxx"以及domain等。客户端收到后会设置Cookie保存JSESSIONID。在Cookie有效期后,在以后的request请求中,都会带上此JSESSIONID,服务端接收到此请求后,会找到之前已经建立的SESSION。

socket与WebSocket

  • socket:并非协议,是基于TCP/IP网络封装的API,为了方便TCP或UDP而抽象出来的一层,是位于应用和传输控制层之间的一组接口,socket利用TCP/IP协议建立TCP连接,进行依赖于底层的IP协议,更深层次依赖于数据链路层。 维持长链接。

socket的建立过程如图:

{943C62CC-0B6D-8E6B-8A66-D3A27F88F367}.png

代码演示:

    //客户端:
    public static final int PORT = 8080;
    public static final String HOST = "127.0.0.1";

    Socket socket = new Socket(HOST, PORT);
    OutputStream os = socket.getOutputStream();
    InputStream is = socket.getInputStream();

    ByteArrayOutputStream outStream = new ByteArrayOutputStream();

    os.write("向服务端写数据");
    socket.shutdownOutput();

// 读取服务器发送的数据
    byte[] buffer = new byte[8192];
    int len;
    while ((len = is.read(buffer)) != -1) {
        outStream.write(buffer, 0, len);
    }

    //服务端:
    //创建ServerSocket的接口
    public ServerSocket(int port) throws IOException {
        this(port, 50, null);
    }

    ServerSocket(int port, int backlog, InetAddress bindAddr);

    public InetSocketAddress(InetAddress addr, int port) {
        holder = new InetSocketAddressHolder(
                        null,
                        addr == null ? InetAddress.anyLocalAddress() : addr,
                        checkPort(port));
    }

    ServerSocket server = ServerSocket(PORT);
    Socket sk = server.accept();
    //读写数据,代码 类上

  • webSocket:HTTP、WebSocket等应用层协议,都是基于TCP协议来传输数据的。必须依赖HTTP协议进行一次握手,握手成功后,数据就直接从TCP通信传输,与HTTP无关了。WebSocket API是HTML5标准的一部分,实现了客户端与服务端全双工通信,而HTTP是单向通信。 代码演示:
//代码临摹于某大神,有逻辑改动

//server.js
//终端安装websocket库:npm install websocket

var websocket = require('websocket').server
var http = require('http')
const HOST = 'http://127.0.0.1:8080';
const PORT = 8080;
var httpServer = http.createServer().listen(PORT, function(){
    console.log(HOST)
})

var wsServer = new websocket({
    httpServer: httpServer,
    autoAcceptConnections: false,
})
 
wsServer.on('request', function (request) {
    var connect = request.accept()
    connect.on('message', function (message) {
        console.log(Date.now());
        message.fromServer = 1;
        console.log(message)
        connect.send(message.utf8Data)
   })
   
    //监听关闭连接操作
    connection.on("close", function (code, reason) {    
        var message = {}
        message.type = "leave"
        message.data =  "离开了"
        connect.send(JSON.stringify(message))
    })

    //错误处理
    connection.on("error", function (err) {
        console.log(err)
    })
})
//浏览器端:通过<script>的方式使用WebSocket功能。
<script>
    const hostPort = 'ws://127.0.0.1:8080'
    var webSocket = new WebSocket(hostPort) 
    /*readyState一共四种状态
    0: 链接还没有建立,正在建立链接
    1:链接建立
    2:链接正在关闭
    3:链接已经关闭
    */
    webSocket.onopen = function(){
        console.log(webSocket.readyState)
    }

    function send(){
        var text = document.getElementById('text').value
        webSocket.send(text)
    } 
    
    webSocket.onclose = function () {
        console.log("websocket close")
    }
    //实时监听服务器推送到客户端的事件
    webSocket.onmessage = function (msg) {
        console.log(Date.now());
        showMessage(msg.data, msg.type)
    }
    //在页面中现实聊天内容
    function showMessage(str, type) {
        var div = document.createElement('div')
        div.innerHTML = str;
        if (type == "enter") {
            div.style.color = "blue";
        } else if(type == "leave") {
            div.style.color = "red"
        } else if(type == "message") {
            div.style.color = "black"
        }
        document.body.appendChild(div)
    }
    </script>

WebRTC技术是H5标准之一,它通过简单的API为浏览器和移动端应用程序提供了实时通信的功能

  • WebRTC框架

{8FE66DB7-37FE-6978-214B-1A0948FF769E}.png

  • WebRTC通话原理:

1,媒体协商:简而言之,就是对通信两端能够支持的编解码格式取交集,例如A端支持VP8、H264,B端支持H264,H265,那么为保证AB两端通信正常,则选H264...。SDP(Session Description Protocol)用于描述上述这类信息。视频通讯双方必须先交换SDP信息,这个过程叫媒体协商。

2,网络协商:通信两端要了解对方的网络情况,以确保找到一条相互通讯的链路。理想的场景是两端都是私有公网IP,可以直接peer2Peer连接。但真实的情况,终端都是在局域网中,需要NAT(Network Address Translation:网络地址转换)。为解决WebRTC这些问题,引出了STUN和TURN。

(1)STUN(Session Traversal Utilities for NAT, NAT会话穿越应用程序)是一种网络协议,它允许位于NAT后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后,以及NAT为某一个本地端口所绑定的Internet端端口。这些信息被用来在两个同时处于NAT路由器之后的主机之间创建UDP通信。这样就能给无法在公网环境下的视频通信设备分配公网IP以建立通话连接。简而言之:STUN是告知我对方的公网IP地址+端口。

媒体流传输按照P2P的方式,STUN并不是每次都能成功地为需要NAT的通信端分配IP地址,P2P地方式在多人视频通话中,更是严重依赖本地的带宽。TURN能够很好的解决这个问题。

(2)TURN(Traversal Using Relays around NAT)是STUN/RFC5389的一个拓展。主要添加了Relay功能。如果终端在NAT之后,那么在特定的情况下,有可能使得终端无法和其对等端进行直接的通信,这时就需要公网的服务器作为一个中继,对来往的数据进行转发,这个转发协议就被定义为TURN。这种方式的带块由服务器端解决。

(3)在WebRTC中用来描述网络信息的术语叫candidate,也叫网络协商。

3,信令系统(信令服务端)

(1)信令服务端包含交互媒体信息sdp和网络信息candidate,以及房间管理和人员进出等;

(2)信令发送过程:

{BF7C3EC7-C928-7751-460B-D6EE3225D4B8}.png

    //信令集合
    //主动加入房间
    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"
    //发送offer给对端peer
    const SIGNAL_TYPE_OFFER  = "offer"
    //发送offer给对端peer
    const SIGNAL_TYPE_ANSWER = "answer"
    //发送candidate给对端peer
    const SIGNAL_TYPE_CANDIDATE = "candidate"


     // 初始化本地媒体流:  
    navigator.mediaDevices.getUserMedia({  
        audiotrue,  
        videotrue  
    })
     // 打开本地媒体流  
     var localVideo = document.querySelector('#localVideo');  
     localVideo.srcObject = stream;      // 显示画面  
     localStream = stream;   // 保存本地流的句柄
    //DoOffer:  
    1,创建RTCPeerConnecttion对象并设置onicecandidate监听事件。监听事件中会触发 candidate信令。见下一代码块
    2,写入本地session描述 
    3,发送offer信令  
    //handleRemoteOffer:  
    1,写入远端session描述
    //DoAnswer:
    1,写入本地session描述,并发送answer信令。
    //HandleRemoteAnswer:  
    写入本地session描述

RTCPeerConnection音视频通话的核心类

//临摹代码拿来主义(建议整个信令系统临摹一遍,细节印象更深刻)
function createPeerConnection() {  
    var defaultConfiguration = {    
        bundlePolicy"max-bundle",  
        rtcpMuxPolicy"require",  
        iceTransportPolicy:"all",//relay 或者   
        // 修改ice数组测试效果,需要进行封装  
        iceServers: [  
            {  
                "urls": [  
                    "turn:![]()192.168.0.1:61313?transport=udp",  
                    "turn:![]()192.168.0.1:61313?transport=tcp"       // 可以插入多个进行备选  
                ],  
                "username""KinnoWang_CN",  
                "credential""There will be more surprises here, Beyond foresight"  
            },  
            {  
                "urls": [  
                    "stun:![]()192.168.0.1:61313"  
                ]  
                
            }  
        ]  
    };  
  
    pc = new RTCPeerConnection(defaultConfiguration);  
    pc.onicecandidate = handleIceCandidate;  
    pc.ontrack = handleRemoteStreamAdd;  
    pc.onconnectionstatechange = handleConnectionStateChange;  
    pc.oniceconnectionstatechange = handleIceConnectionStateChange  
  
    localStream.getTracks().forEach((track) => pc.addTrack(track, localStream)); // 把本地流设置给RTCPeerConnection  
}

function handleRemoteStreamAdd(event) {
    remoteStream = event.streams[0]
    remoteVideo.srcObject = remoteStream;
}

ps:在使用模拟服务器软件XShell(最新版)时,会提示安装错误,需要修改注册表,修改后重启会ok。