WebSocket知识、轮询、长轮询、长连接

一、WebSocket理论知识

1.什么是websocket

WebSocket是HTML5新增的协议,它的目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,比如说,服务器可以在任意时刻发送消息给浏览器。

为什么传统的HTTP协议不能做到WebSocket实现的功能?这是因为HTTP协议是一个请求-响应协议,请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。换句话说,浏览器不主动请求,服务器是没法主动发数据给浏览器的。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

2.为什么使用Websocket

也有人说,HTTP协议其实也能实现啊,比如用轮询或者Comet。

轮询

轮询是指浏览器通过JavaScript启动一个定时器,然后以固定的间隔给服务器发请求,询问服务器有没有新消息。

缺点:浪费客户端资源

websocket

浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方。

WebSocket知识、轮询、长轮询、长连接

WebSocket知识、轮询、长轮询、长连接浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。

3.websocket协议

WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。我们来看看WebSocket连接是如何创建的。

首先,WebSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求,格式如下:

GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13

该请求和普通的HTTP请求有几点不同:

  1. GET请求的地址不是类似/path/,而是以ws://开头的地址;

  2. 请求头Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket连接;

  3. Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据;

  4. Sec-WebSocket-Version指定了WebSocket的协议版本。

随后,服务器如果接受该请求,就会返回如下响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string

该响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。

版本号和子协议规定了双方能理解的数据格式,以及是否支持压缩等等。如果仅使用WebSocket的API,就不需要关心这些。

现在,一个WebSocket连接就建立成功,浏览器和服务器就可以随时主动发送消息给对方。消息有两种,一种是文本,一种是二进制数据。通常,我们可以发送JSON格式的文本,这样,在浏览器处理起来就十分容易。

浏览器

很显然,要支持WebSocket通信,浏览器得支持这个协议,这样才能发出ws://xxx的请求。目前,支持WebSocket的主流浏览器如下:

  • Chrome

  • Firefox

  • IE >= 10

  • Sarafi >= 6

  • Android >= 4.4

  • iOS >= 8

4.轮询、长轮询、长连接概念

轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。

  • 优点:后端程序编写比较容易。
  • 缺点:请求中有大半是无用,浪费带宽和服务器资源。(而每一次的 HTTP 请求和应答都带有完整的 HTTP 头信息,这就增加了每次传输的数据量)
  • 实例:适于小型应用。

长轮询:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接(或到了设定的超时时间关闭连接),客户端处理完响应信息后再向服务器发送新的请求。

  • 优点:在无消息的情况下不会频繁的请求,节省了网络流量,解决了服务端一直疲于接受请求的窘境
  • 缺点:服务器hold连接会消耗资源,需要同时维护多个线程,服务器所能承载的TCP连接数是有上限的,这种轮询很容易把连接数顶满。
  • 实例:WebQQ、Hi网页版、Facebook IM。

长连接:在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。
连接保持 - Http 发起请求 在请求中写一个协议 - WebSocket - 服务器收到Websocket请求 ,自动保持此连接 - 永久不断开,除非主动断开 - 可以通过此连接主动找到客户端

  • 优点:消息即时到达,不发无用请求。
  • 缺点:服务器维护一个长连接会增加开销。
  • 实例:Gmail聊天

二、WebSocket的相关方法

1.WebSocket 属性

以下是 WebSocket 对象的属性。假定我们使用了以上代码创建了 Socket 对象:

类型 解释
Socket.readyState 只读属性 readyState 表示连接状态,可以是以下值:0 - 表示连接尚未建立。1 - 表示连接已建立,可以进行通信。2 - 表示连接正在进行关闭。3 - 表示连接已经关闭或者连接不能打开。
Socket.bufferedAmount 只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。

2.WebSocket事件

以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:

事件 事件处理程序 描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发

3.WebSocket 方法

以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:

方法 描述
Socket.send() 使用连接发送数据
Socket.close() 关闭连接

三、Websocket实战练习

既然学习了websocket的使用,现在我们就要基于flask来实现即时通信的简易版,也就是群聊和私聊的网页版,对于页面效果,大家就别吐槽了,仅用作练习学习。

环境包准备

由于是基于flask来实现的websocket,我们需要装flask和gevent-websocket

pip3 install flask
pip3 install gevent-websocket

1.websocket实现群聊代码

群聊后端代码groupChat.py

from flask import Flask,render_template,request
from geventwebsocket.handler import WebSocketHandler # 提供ws协议处理
from geventwebsocket.server import WSGIServer # 承载服务
from geventwebsocket.websocket import WebSocket # 提供语法提示

app = Flask(__name__)

user_socket_dict = {}

# 群发消息视图
@app.route("/groupchat/<username>")
def groupChat(username):
# 获取当前客户端与服务器的socket连接
user_socket = request.environ.get("wsgi.websocket") # type: WebSocket
if user_socket:
# 保存链接到字典中,用户名作为键
user_socket_dict[username] = user_socket
print(len(user_socket_dict),user_socket_dict)
while 1:
msg = user_socket.receive() # 接受每个客户端的消息
# 遍历字典,给每一个客户端发送消息
for name,socket in user_socket_dict.items():
try:
socket.send(msg)
except:
pass

# 获取聊天页面视图
@app.route("/chat")
def chat():
return render_template("groupChat.html")


if __name__ == '__main__':
# 通过WSGIServer来启动web服务,并指定用WebSocketHandler来处理websocket的请求
server = WSGIServer(("0.0.0.0",9527),app,handler_class=WebSocketHandler)
server.serve_forever()

前端页面groupChat.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="text-align: center">
<p id="login-tag">
<input type="text" id="username">
<button id="login-btn" onclick="login()">登录</button>
</p>
<p>
<input type="text" id="content">
<button id="send-btn" onclick="sendMsg()">发送</button>
</p>
</div>
<div id="chat_list"></div>

</body>
<script>
var ws = null;

// 登录建立websocket链接函数
function login() {
var username = document.getElementById("username").value;
console.log(username);
var tag = document.getElementById("login-tag");

tag.style.display = "none"; // 登录后隐藏登录框
ws = new WebSocket("ws://192.168.16.13:9527/groupchat/"+username);

// 监听电话
ws.onmessage = function (messageEvent) {
// 获取服务器发送过来的数据
var msg = messageEvent.data;
// 对json字符串进行反序列化
msg_dic = JSON.parse(msg);
var p = document.createElement("p");
p.innerText = msg_dic.from_user + ":" + msg_dic.info;

// 添加聊天记录到页面中
document.getElementById("chat_list").appendChild(p)
};
}

// 发送消息的函数
function sendMsg() {
// 原生js获取数据
var username = document.getElementById("username").value;
var content = document.getElementById("content").value;

// 把数据封装在自定义对象中
var msg = {
from_user:username,
info:content
};
// 通过websocket链接发送数据
ws.send(JSON.stringify(msg));
}

</script>
</html>

启动flask项目,访问页面192.168.16.13:9527/chat进行群聊,可以开多个服务器模拟多用户群聊,查看效果。

2.websocket实现私聊实战

私聊后端代码privateChat.py

from flask import Flask,render_template,request
from geventwebsocket.handler import WebSocketHandler
from geventwebsocket.server import WSGIServer
from geventwebsocket.websocket import WebSocket
import json

app = Flask(__name__)

user_socket_dict = {}

# 用户私聊视图
@app.route("/privateChat/<username>")
def privateChat(username):
# 获取客户端和服务器之间的链接
user_socket = request.environ.get("wsgi.websocket") # type: WebSocket

if user_socket:
# 保存链接到字典中,用户名作为键
user_socket_dict[username] = user_socket
print(len(user_socket_dict), user_socket_dict)

while True:
# 获取客户端发送的信息
msg = user_socket.receive()
msg_dic = json.loads(msg)
to_user = msg_dic.get("to_user") # 获取目标用户名
to_user_socket = user_socket_dict.get(to_user) # 根据用户名获取用户的链接
to_user_socket.send(msg) # 给目标用户发送信息

# 获取聊天页面视图
@app.route("/chat")
def chat():
return render_template("privateChat.html")


if __name__ == '__main__':
# 通过WSGIServer来启动web服务,并指定用WebSocketHandler来处理websocket的请求
server = WSGIServer(("0.0.0.0",8520),app,handler_class=WebSocketHandler)
server.serve_forever()

私聊前端代码privateChat.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="text-align: center">
<p id="login-tag">
<input type="text" id="username">
<button id="login-btn" onclick="login()">登录</button>
</p>
<p>
<input type="text" id="content">
<button id="send-btn" onclick="sendMsg()">发送</button>
</p>
</div>
<div id="chat_list"></div>

</body>
<script>
var ws = null;

// 登录建立websocket链接函数
function login() {
var username = document.getElementById("username").value;
console.log(username);
var tag = document.getElementById("login-tag");

tag.style.display = "none"; // 登录后隐藏登录框
ws = new WebSocket("ws://192.168.16.13:9527/groupchat/"+username);

// 监听电话
ws.onmessage = function (messageEvent) {
// 获取服务器发送过来的数据
var msg = messageEvent.data;
// 对json字符串进行反序列化
msg_dic = JSON.parse(msg);
var p = document.createElement("p");
p.innerText = msg_dic.from_user + ":" + msg_dic.info;

// 添加聊天记录到页面中
document.getElementById("chat_list").appendChild(p)
};
}

// 发送消息的函数
function sendMsg() {
// 原生js获取数据
var username = document.getElementById("username").value;
var content = document.getElementById("content").value;

// 把数据封装在自定义对象中
var msg = {
from_user:username,
info:content
};
// 通过websocket链接发送数据
ws.send(JSON.stringify(msg));
}

</script>
</html>
上一篇:C# 依赖注入


下一篇:HTTP长连接?短连接?长轮询?短轮询?