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挺有意义的,所以复制来。
首先是 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就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。
这样就可以解决客服处理速度过慢的问题了。
虽然接线员很快速,但是每次都要听这么一堆,效率也会有所下降的,同时还得不断把这些信息转交给客服,不但浪费客服的处理时间,而且还会在网路传输中消耗过多的流量/时间。
但是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>