WebSocket介绍

为什么需要 WebSocket? 

       初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

       答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

       举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。

      这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

      轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

 

简介                                      

     WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

    它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

其他特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

 

 WebSocket 的作用                 

        其实上面已经讲了它的优点了,不过最近看知乎看到一段有关WebSocket挺有意义的,所以复制来。

      在讲Websocket之前,我就顺带着讲下 long poll 和 ajax轮询 的原理。
     首先是 ajax轮询 ,ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。
场景再现:
客户端:啦啦啦,有没有新信息(Request)
服务端:没有(Response)
客户端:啦啦啦,有没有新信息(Request)
服务端:没有。。(Response)
客户端:啦啦啦,有没有新信息(Request)
服务端:你好烦啊,没有啊。。(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:好啦好啦,有啦给你。(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:。。。。。没。。。。没。。。没有(Response) ---- loop

long poll 
long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。
场景再现
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)
服务端:额。。 等待到有消息的时候。。来 给你(Response)
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loop

从上面可以看出其实这两种方式,都是在不断地建立HTTP连接,然后等待服务端处理,可以体现HTTP协议的另外一个特点,被动性
何为被动性呢,其实就是,服务端不能主动联系客户端,只能有客户端发起。
简单地说就是,服务器是一个很懒的冰箱(这是个梗)(不会、不能主动发起连接),但是上司有命令,如果有客户来,不管多么累都要好好接待。

说完这个,我们再来说一说上面的缺陷(原谅我废话这么多吧OAQ)
从上面很容易看出来,不管怎么样,上面这两种都是非常消耗资源的。
ajax轮询 需要服务器有很快的处理速度和资源。(速度)
long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)

 

     通过上面这个例子,我们可以看出,这两种方式都不是最好的方式,需要很多资源。
一种需要更快的速度,一种需要更多的'电话'。这两种都会导致'电话'的需求越来越高。
哦对了,忘记说了HTTP还是一个无状态协议。(感谢评论区的各位指出OAQ)
通俗的说就是,服务器因为每天要接待太多客户了,是个健忘鬼,你一挂电话,他就把你的东西全忘光了,把你的东西全丢掉了。你第二次还得再告诉服务器一遍。

所以在这种情况下出现了,Websocket出现了。
他解决了HTTP的这几个难题。
首先,被动性,当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦。
所以上面的情景可以做如下修改。
客户端:啦啦啦,我要建立Websocket协议,需要的服务:chat,Websocket协议版本:17(HTTP Request)
服务端:ok,确认,已升级为Websocket协议(HTTP Protocols Switched)
客户端:麻烦你有信息的时候推送给我噢。。
服务端:ok,有的时候会告诉你的。
服务端:balabalabalabala
服务端:balabalabalabala
服务端:哈哈哈哈哈啊哈哈哈哈
服务端:笑死我了哈哈哈哈哈哈哈

就变成了这样,只需要经过一次HTTP请求,就可以做到源源不断的信息传送了。(在程序设计中,这种设计叫做回调,即:你有信息了再来通知我,而不是我傻乎乎的每次跑来问你)
这样的协议解决了上面同步有延迟,而且还非常消耗资源的这种情况。
那么为什么他会解决服务器上消耗资源的问题呢?
其实我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(PHP等)来处理。
简单地说,我们有一个非常快速的接线员(Nginx),他负责把问题转交给相应的客服(Handler)
本身接线员基本上速度是足够的,但是每次都卡在客服(Handler)了,老有客服处理速度太慢。,导致客服不够。
Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。
这样就可以解决客服处理速度过慢的问题了。

同时,在传统的方式上,要不断的建立,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁。
虽然接线员很快速,但是每次都要听这么一堆,效率也会有所下降的,同时还得不断把这些信息转交给客服,不但浪费客服的处理时间,而且还会在网路传输中消耗过多的流量/时间。
但是Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。
同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(当然客户端还是等主动发送信息过来的。。),没有信息的时候就交给接线员(Nginx),不需要占用本身速度就慢的客服(Handler)
 

服务端代码

import json

from flask import Flask, request
from geventwebsocket.websocket import WebSocket
from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler

ws_serv = Flask(__name__)

user_socket_dict = {}


@ws_serv.route("/toy/<toy_id>")
def toy(toy_id):
    user_socket = request.environ.get("wsgi.websocket")  # type:WebSocket
    if user_socket:
        user_socket_dict[toy_id] = user_socket
        print(len(user_socket_dict), user_socket_dict)

    while True:
        user_msg = user_socket.receive()
        if not user_msg:
            return "断了!友尽!"
        print(user_msg, type(user_msg))  # {to_user:toy001,music:"uuid4().mp3"}
        user_msg_dict = json.loads(user_msg)
        to_user = user_msg_dict.get("to_user")
        to_user_socket = user_socket_dict.get(to_user)
        try:
            to_user_socket.send(user_msg)
        except:
            continue


@ws_serv.route("/app/<app_id>")
def app(app_id):
    user_socket = request.environ.get("wsgi.websocket")  # type:WebSocket
    if user_socket:
        user_socket_dict[app_id] = user_socket
        print(len(user_socket_dict), user_socket_dict)
    while True:
        user_msg = user_socket.receive()
        if not user_msg:
            return "断了!友尽!"
        print(user_msg, type(user_msg))  # {to_user:toy001,music:"uuid4().mp3"}
        user_msg_dict = json.loads(user_msg)
        to_user = user_msg_dict.get("to_user")
        to_user_socket = user_socket_dict.get(to_user)

        try:
            to_user_socket.send(user_msg)
        except:
            continue


if __name__ == '__main__':
    http_serv = WSGIServer(("0.0.0.0", 9528), application=ws_serv, handler_class=WebSocketHandler)
    http_serv.serve_forever()
服务端

前端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title id="title"></title>

</head>
<body>
<audio id="player" autoplay controls></audio>
<p>DeviceKey:<input type="text" id="device_key">
    <button onclick="open_toy()">玩具开机</button>
</p>
<p>消息来自:<span id="from_user"></span></p>
<p>好友类型:<span id="from_user_type"></span></p>
<p>
    <button onclick="start_reco()">开始录音</button>
    <button onclick="stop_reco()">发送语音消息</button>
    <button onclick="recv_msg()">收取消息</button>
</p>
<p>
    <button onclick="ai_reco()" style="background-color: cornflowerblue">发送语音指令</button>
</p>
</body>
<script type="application/javascript" src="/static/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/static/Recorder.js"></script>
<script type="application/javascript">
    var ws = null;
    var toy_id = null;

    function open_toy() {
        var device_key = document.getElementById("device_key").value;
        $.post(
            "http://192.168.11.40:9527/open_toy",
            {device_key: device_key},
            function (data) {
                console.log(data);
                if (data.code == 0) {
                    document.getElementById("title").innerText = data.name;
                    toy_id = data.toy_id;
                    create_ws(toy_id);
                }
                document.getElementById("player").src = "http://192.168.11.40:9527/get_music/" + data.music;
            },
            "json"
        );
    }


    function create_ws(toy_id) {
        ws = new WebSocket("ws://192.168.11.40:9528/toy/" + toy_id); // 456
        ws.onmessage = function (eventMessage) { //456.onmessage
            var recv_msg = JSON.parse(eventMessage.data);
            console.log(recv_msg);
            if (recv_msg.music) {
                document.getElementById("player").src = "http://192.168.11.40:9527/get_music/" + recv_msg.music;
            } else {
                document.getElementById("from_user").innerText = recv_msg.from_user;
                document.getElementById("from_user_type").innerText = recv_msg.friend_type;
                document.getElementById("player").src = "http://192.168.11.40:9527/get_chat/" + recv_msg.chat;
            }
        };
        ws.onclose = function () {
            create_ws(toy_id);
        };
    }


    var serv = "http://192.168.11.40:9527";

    var reco = null;
    var audio_context = new AudioContext();//音频内容对象
    navigator.getUserMedia = (navigator.getUserMedia ||
        navigator.webkitGetUserMedia ||
        navigator.mozGetUserMedia ||
        navigator.msGetUserMedia);

    navigator.getUserMedia({audio: true}, create_stream, function (err) {
        console.log(err)
    });

    function create_stream(user_media) {
        var stream_input = audio_context.createMediaStreamSource(user_media);
        reco = new Recorder(stream_input);
    }


    function start_reco() {
        reco.record();
    }

    function stop_reco() {
        reco.stop();

        reco.exportWAV(function (wav_file) {
            console.log(wav_file);
            var formdata = new FormData(); // form 表单 {key:value}
            formdata.append("reco", wav_file); // form input type="file"
            formdata.append("user_id", toy_id);
            formdata.append("friend_type",document.getElementById("from_user_type").innerText);
            formdata.append("to_user", document.getElementById("from_user").innerText);
            // # <input type="text" name = "key"> value

            $.ajax({
                url: serv + "/toy_uploader",
                type: 'post',
                processData: false,
                contentType: false,
                data: formdata,
                dataType: 'json',
                success: function (data) {
                    console.log(data);
                    if(data.DATA.code == 0){
                        document.getElementById("player").src =
                            "http://192.168.11.40:9527/get_music/SendOK.mp3";
                    }
                    var send_str = {
                        to_user: document.getElementById("from_user").innerText,
                        from_user: toy_id,
                        friend_type:data.DATA.friend_type,
                        chat: data.DATA.filename
                    };
                    ws.send(JSON.stringify(send_str));
                }
            })
        });

        reco.clear();
    }

    function ai_reco() {
        reco.stop();

        reco.exportWAV(function (wav_file) {
            console.log(wav_file);
            var formdata = new FormData(); // form 表单 {key:value}
            formdata.append("reco", wav_file); // form input type="file"
            formdata.append("toy_id", toy_id);
            $.ajax({
                url: serv + "/ai_uploader",
                type: 'post',
                processData: false,
                contentType: false,
                data: formdata,
                dataType: 'json',
                success: function (data) {
                    console.log(data);
                    if (data.chat) {
                        document.getElementById("from_user").innerText = data.from_user;
                        document.getElementById("from_user_type").innerText = data.friend_type;
                        document.getElementById("player").src = "http://192.168.11.40:9527/get_chat/" + data.chat;
                    } else {
                        document.getElementById("from_user").innerText = data.from_user;
                        document.getElementById("player").src = "http://192.168.11.40:9527/get_music/" + data.music;
                    }
                }
            })
        });

        reco.clear();
    }

    function recv_msg() {
        var from_user = document.getElementById("from_user").innerText;
        $.post("http://192.168.11.40:9527/recv_msg", {
            from_user: from_user,
            to_user: toy_id
        }, function (data) {
            console.log(data);
            var chat_info = data.pop();
            document.getElementById("player").src = "http://192.168.11.40:9527/get_chat/" + chat_info.chat;
            document.getElementById("player").onended = function () {
                if(data.length == 0){
                    return
                }
                document.getElementById("player").src = "http://192.168.11.40:9527/get_chat/" + data.pop().chat;
            }
        }, "json")
    }

</script>
</html>
前端

 

 
posted @ 2019-06-16 14:57  7411  阅读(270)  评论(0编辑  收藏  举报