WebSocket介绍
什么是WebSocket
HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。在WebSocket中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,浏览器和服务器之间的数据交换变得更加简单。
WebSocket VS HTTP
它们的不同点:
1)HTTP的协议标识符是http,WebSocket的是ws;
2)HTTP请求只能由客户端发起,服务器无法主动向客户端推送消息,而WebSocket可以;
3)HTTP请求有同源限制,不同源之间通信需要跨域,而WebSocket没有同源限制。
它们的相同点:
1)都是应用层的通信协议;
2)默认端口一样,都是80或443;
3)都可以用于浏览器和服务器间的通信;
4)都基于TCP协议。
(都是基于TCP协议,http只能从客户端发送请求,websocket可以进行双向通信,任意一端(客户端,服务端)可以通过 建立的 连接将信息推到另一端)
WebSocket优点
- 1)支持双向通信,实时性更强;
- 2)更好的二进制支持;
- 3)较少的控制开销:
连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部; - 4)支持扩展:
ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议(比如支持自定义压缩算法等)。
使用场景
业务场景 | 场景概述 |
---|---|
弹幕 | 终端用户A在自己的手机端发送了一条弹幕信息,但是您也需要在客户A的手机端上将其他N个客户端发送的弹幕信息一并展示。需要通过WebSocket协议将其他客户端发送的弹幕信息从服务端全部推送至客户A的手机端,从而使客户A可以同时看到自己发送的弹幕和其他用户发送的弹幕。 |
在线教育 | 老师进行一对多的在线授课,在客户端内编写的笔记、大纲等信息,需要实时推送至多个学生的客户端,需要通过WebSocket协议来完成。 |
股票等金融产品实时报价 | 股票黄金等价格变化迅速,变化后,可以通过WebSocket协议将变化后的价格实时推送至世界各地的客户端,方便交易员迅速做出交易判断。 |
体育实况更新 | 由于全世界体育爱好者数量众多,因此比赛实况成为其最为关心的热点。这类新闻中最好的体验就是利用WebSocket达到实时的更新。 |
视频会议和聊天 | 尽管视频会议并不能代替和真人相见,但是应用场景众多。WebSocket可以帮助两端或多端接入会议的用户实时传递信息。 |
基于位置的应用 | 越来越多的开发者借用移动设备的GPS功能来实现基于位置的网络应用。如果您一直记录终端用户的位置(例如:您的 App记录用户的运动轨迹),就可以收集到更加细致化的数据。 |
WebSocket快速开始
一个简单的WebSocket聊天Demo
模拟两个web客户端实现聊天功能
客户端代码
client.html:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<style>
*{
margin: 0;
padding: 0;
}
.message{
width: 60%;
margin: 0 10px;
display: inline-block;
text-align: center;
height: 40px;
line-height: 40px;
font-size: 20px;
border-radius: 5px;
border: 1px solid #B3D33F;
}
.form{
width:100%;
position: fixed;
bottom: 300px;
left: 0;
}
.connect{
height: 40px;
vertical-align: top;
/* padding: 0; */
width: 80px;
font-size: 20px;
border-radius: 5px;
border: none;
background: #B3D33F;
color: #fff;
}
</style>
</head>
<body>
<ul id="content"></ul>
<form class="form">
<input type="text" placeholder="请输入发送的消息" class="message" id="message"/>
<input type="button" value="发送" id="send" class="connect"/>
<input type="button" value="连接" id="connect" class="connect"/>
</form>
<script></script>
</body>
</html>
客户端js代码:
var oUl=document.getElementById('content');
var oConnect=document.getElementById('connect');
var oSend=document.getElementById('send');
var oInput=document.getElementById('message');
var ws=null;
oConnect.onclick=function(){
ws=new WebSocket('ws://localhost:5000');
ws.onopen=function(){
oUl.innerHTML+="<li>客户端已连接</li>";
}
ws.onmessage=function(evt){
oUl.innerHTML+="<li>"+evt.data+"</li>";
}
ws.onclose=function(){
oUl.innerHTML+="<li>客户端已断开连接</li>";
};
ws.onerror=function(evt){
oUl.innerHTML+="<li>"+evt.data+"</li>";
};
};
oSend.onclick=function(){
if(ws){
ws.send(oInput.value);
}
}
这里使用的是w3c规范中关于HTML5 websocket API的原生API,这些api很简单,就是利用new WebSocket创建一个指定连接服务端地址的ws实例,然后为该实例注册onopen(连接服务端),onmessage(接受服务端数据),onclose(关闭连接)以及ws.send(建立连接后)发送请求。
WebSocket客户端的 API
WebSocket 构造函数
WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例:
var ws = new WebSocket('ws://localhost:8080');
执行上面语句之后,客户端就会与服务器进行连接。
webSocket.readyState
readyState属性返回实例对象的当前状态,共有四种:
switch (ws.readyState) {
case WebSocket.CONNECTING:
// do something
break;
case WebSocket.OPEN:
// do something
break;
case WebSocket.CLOSING:
// do something
break;
case WebSocket.CLOSED:
// do something
break;
default:
// this never happens
break;
}
webSocket.onopen
实例对象的onopen属性,用于指定连接成功后的回调函数:
ws.onopen = function () {
ws.send('Hello Server!');
}
如果要指定多个回调函数,可以使用addEventListener方法:
ws.addEventListener('open', function (event) {
ws.send('Hello Server!');
});
webSocket.onclose
实例对象的onclose属性,用于指定连接关闭后的回调函数:
ws.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
};
ws.addEventListener("close", function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
});
webSocket.onmessage
实例对象的onmessage属性,用于指定收到服务器数据后的回调函数:
ws.onmessage = function(event) {
var data = event.data;
// 处理数据
};
ws.addEventListener("message", function(event) {
var data = event.data;
// 处理数据
});
注意,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象):
ws.onmessage = function(event){
if(typeof event.data === String){
console.log("Received data string");
}
if(event.data instanceof ArrayBuffer){
var buffer = event.data;
console.log("Received arraybuffer");
}
}
除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型:
// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
console.log(e.data.size);
};
// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
console.log(e.data.byteLength);
};
webSocket.send()
实例对象的send()方法用于向服务器发送数据。
发送文本的例子:
ws.send('your message');
发送 Blob 对象的例子:
var file = document
.querySelector('input[type="file"]')
.files[0];
ws.send(file);
发送 ArrayBuffer 对象的例子:
// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var ii = 0; ii < img.data.length; ii++) {
binary[ii] = img.data[ii];
}
ws.send(binary.buffer);
webSocket.bufferedAmount
实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。
var data = new ArrayBuffer(10000000);
socket.send(data);
if (socket.bufferedAmount === 0) {
// 发送完毕
} else {
// 发送还没结束
}
webSocket.onerror
实例对象的onerror属性,用于指定报错时的回调函数:
socket.onerror = function(event) {
// handle error event
};
socket.addEventListener("error", function(event) {
// handle error event
});
服务端代码
服务端采用Node.js,这里需要基于一个nodejs-websocket的Node.js服务端的库,它是一个轻量级的Node.js websocket server端的实现,实际上也是使用Node.js提供的net模块写成的。
npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm i nodejs-websocket
server.js:
var app=require('http').createServer(handler);
var ws=require('nodejs-websocket');
var fs=require('fs');
app.listen(80);
function handler(req,res){
fs.readFile(__dirname+'/client.html',function(err,data){
if(err){
res.writeHead(500);
return res.end('error ');
}
res.writeHead(200);
res.end(data);
});
}
var server=ws.createServer(function(conn){
console.log('new conneciton');
conn.on("text",function(str){
broadcast(server,str);
});
conn.on("close",function(code,reason){
console.log('connection closed');
})
}).listen(5000);
function broadcast(server, msg) {
server.connections.forEach(function (conn) {
conn.sendText(msg);
})
}
首先利用http模块监听用户的http请求并显示client.html界面,然后创建一个websocket服务端等待用户连接,在接收到用户发送来的数据之后将它广播到所有连接到的客户端。
运行
node server.js
客户端发起连接
请求响应报文
结语
从上面的即时通讯聊天例子我们可以看到,要想做一个点对点的im应用,websocket采取的方式是让所有客户端连接服务端,服务器将不同客户端发送给自己的消息进行转发或者广播。
为了说明html5规范中的websocket在客户端采用了websocket原生的API,实际开发中,有比较著名的两个库socket.io和sockjs,它们都对原始的websocket API做了进一步封装,提供了更多功能,都分为客户端和服务端的实现,实际应用中,可以选择使用。
Socket.IO使用
Socket.IO是一个完全由JavaScript实现、基于Node.js、支持WebSocket的协议用于实时通信、跨平台的开源框架,它包括了客户端的JavaScript和服务器端的Node.js。
官方文档:https://socket.io/docs/v4/
环境准备
搭建Socket.IO环境需要先创建一个作为工作空间的目录,然后安装Node.js,并在工作空间下安装Socket.IO
cnpm install socket.io
服务端示例代码
var app = require('http').createServer(handler)
var io = require('socket.io')(app);
var fs = require('fs');
app.listen(80);
function handler (req, res) {
fs.readFile(__dirname + '/index.html',
function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading index.html');
}
res.writeHead(200);
res.end(data);
});
}
io.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
// socket.io 使用 emit(eventname,data) 发送消息,使用on(eventname,callback)监听消息
//监听客户端发送的 sendMsg 事件
socket.on("sendMsg", function (data) {
// data 为客户端发送的消息,可以是 字符串,json对象或buffer
// 使用 emit 发送消息,broadcast 表示 除自己以外的所有已连接的socket客户端。
socket.broadcast.emit("receiveMsg", data);
})
});
客户端示例代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<label>聊天内容:</label><br />
<textarea id="content" style="height: 200px; width:300px;"></textarea>
<br />
<input id="sendMsg" type="text" />
<button id="btn_send">发送</button>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
// 监听 receiveMsg 事件,用来接收其他客户端推送的消息
socket.on("receiveMsg", function (data) {
content.value += data.client + ":" + data.msg + "\r\n";
});
var content = document.getElementById("content");
var sendMsg = document.getElementById("sendMsg");
var btn_send = document.getElementById("btn_send");
btn_send.addEventListener("click", function () {
var data = {
client: "客户端",
msg: sendMsg.value
};
//给服务端发送 sendMsg事件名的消息
socket.emit("sendMsg", data);
content.value += data.client + ":" + data.msg + "\r\n";
sendMsg.value = "";
});
</script>
</body>
</html>
springboot整合websocket
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocket配置类
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocket操作类
@ ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端, 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
WebSocketServer
@Component
@ServerEndpoint(value = "/websocket/{userId}")
public class WebSocket {
private final static Logger logger = LogManager.getLogger(WebSocket.class);
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的
*/
private static AtomicInteger onlineCount = new AtomicInteger();
/**
* concurrent包的线程安全Map,用来存放每个客户端对应的MyWebSocket对象
*/
private static ConcurrentHashMap<String, WebSocket> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
private String userId;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
//加入map
webSocketMap.put(userId, this);
addOnlineCount(); //在线数加1
logger.info("用户{}连接成功,当前在线人数为{}", userId, getOnlineCount());
try {
sendMessage(String.valueOf(this.session.getQueryString()));
} catch (IOException e) {
logger.error("IO异常");
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
//从map中删除
webSocketMap.remove(userId);
subOnlineCount(); //在线数减1
logger.info("用户{}关闭连接!当前在线人数为{}", userId, getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
logger.info("来自客户端用户:{} 消息:{}",userId, message);
//群发消息
for (String item : webSocketMap.keySet()) {
try {
webSocketMap.get(item).sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 发生错误时调用
*
* @OnError
*/
@OnError
public void onError(Session session, Throwable error) {
logger.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 向客户端发送消息
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
/**
* 通过userId向客户端发送消息
*/
public void sendMessageByUserId(String userId, String message) throws IOException {
logger.info("服务端发送消息到{},消息:{}",userId,message);
if(StringUtils.hasLength(userId) && webSocketMap.containsKey(userId)){
webSocketMap.get(userId).sendMessage(message);
}else{
logger.error("用户{}不在线",userId);
}
}
/**
* 群发自定义消息
*/
public static void sendInfo(String message) throws IOException {
for (String item : webSocketMap.keySet()) {
try {
webSocketMap.get(item).sendMessage(message);
} catch (IOException e) {
continue;
}
}
}
public static int getOnlineCount() {
return onlineCount.get();
}
public static void addOnlineCount() {
onlineCount.incrementAndGet();
}
public static void subOnlineCount() {
onlineCount.decrementAndGet();
}
}
@RestController
@RequestMapping("/webSocket")
public class WebSocketController {
@Autowired
private WebSocket webSocket;
@RequestMapping("/sentMessage")
public void sentMessage(String userId,String message){
try {
webSocket.sendMessageByUserId(userId,message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@ServerEndpoint("/webSocket/{username}")
@Component
public class WebSocketServer {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static AtomicInteger onlineNum = new AtomicInteger();
//concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();
//发送消息
public void sendMessage(Session session, String message) throws IOException {
if(session != null){
synchronized (session) {
System.out.println("发送数据:" + message);
session.getBasicRemote().sendText(message);
}
}
}
//给指定用户发送信息
public void sendInfo(String userName, String message){
Session session = sessionPools.get(userName);
try {
sendMessage(session, message);
}catch (Exception e){
e.printStackTrace();
}
}
// 群发消息
public void broadcast(String message){
for (Session session: sessionPools.values()) {
try {
sendMessage(session, message);
} catch(Exception e){
e.printStackTrace();
continue;
}
}
}
//建立连接成功调用
@OnOpen
public void onOpen(Session session, @PathParam(value = "username") String userName){
sessionPools.put(userName, session);
addOnlineCount();
System.out.println(userName + "加入webSocket!当前人数为" + onlineNum);
// 广播上线消息
Message msg = new Message();
msg.setDate(new Date());
msg.setTo("0");
msg.setText(userName);
broadcast(JSON.toJSONString(msg,true));
}
//关闭连接时调用
@OnClose
public void onClose(@PathParam(value = "username") String userName){
sessionPools.remove(userName);
subOnlineCount();
System.out.println(userName + "断开webSocket连接!当前人数为" + onlineNum);
// 广播下线消息
Message msg = new Message();
msg.setDate(new Date());
msg.setTo("-2");
msg.setText(userName);
broadcast(JSON.toJSONString(msg,true));
}
//收到客户端信息后,根据接收人的username把消息推下去或者群发
// to=-1群发消息
@OnMessage
public void onMessage(String message) throws IOException{
System.out.println("server get" + message);
Message msg=JSON.parseObject(message, Message.class);
msg.setDate(new Date());
if (msg.getTo().equals("-1")) {
broadcast(JSON.toJSONString(msg,true));
} else {
sendInfo(msg.getTo(), JSON.toJSONString(msg,true));
}
}
//错误时调用
@OnError
public void onError(Session session, Throwable throwable){
System.out.println("发生错误");
throwable.printStackTrace();
}
public static void addOnlineCount(){
onlineNum.incrementAndGet();
}
public static void subOnlineCount() {
onlineNum.decrementAndGet();
}
public static AtomicInteger getOnlineNumber() {
return onlineNum;
}
public static ConcurrentHashMap<String, Session> getSessionPools() {
return sessionPools;
}
}
LoginController
@Controller
public class LoginController {
@Autowired
LoginService loginservice;
@RequestMapping("/loginvalidate")
public String loginvalidate(@RequestParam("username") String username,@RequestParam("password") String pwd,HttpSession httpSession){
if(username==null) {
return "login";
}
String realpwd=loginservice.getpwdbyname(username);
if(realpwd != null&& pwd.equals(realpwd)) {
long uid=loginservice.getUidbyname(username);
httpSession.setAttribute("uid", uid);
return "chatroom";
}
return "fail";
}
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/logout")
public String logout(HttpSession httpSession){
return "login";
}
@RequestMapping(value="/currentuser",method = RequestMethod.GET)
@ResponseBody
public User currentuser(HttpSession httpSession){
Long uid = (Long)httpSession.getAttribute("uid");
String name = loginservice.getnamebyid(uid);
return new User(uid, name);
}
}
ChatController
@Controller
public class ChatController {
@Autowired
LoginService loginservice;
@RequestMapping("/onlineusers")
@ResponseBody
public Set<String> onlineusers(@RequestParam("currentuser") String currentuser) {
ConcurrentHashMap<String, Session> map = WebSocketServer.getSessionPools();
Set<String> set = map.keySet();
Iterator<String> it = set.iterator();
Set<String> nameset = new HashSet<String>();
while (it.hasNext()) {
String entry = it.next();
if (!entry.equals(currentuser))
nameset.add(entry);
}
return nameset;
}
@RequestMapping("getuid")
@ResponseBody
public User getuid(@RequestParam("username") String username) {
Long a = loginservice.getUidbyname(username);
User u = new User();
u.setUid(a);
return u;
}
}
LoginServiceImpl
@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,timeout=5)
@Service("loginservice")
public class LoginServiceImpl implements LoginService{
@Autowired
private LoginMapper loginmapper;
@Override
public String getpwdbyname(String name) {
Staff s=loginmapper.getpwdbyname(name);
if(s!=null) {
return s.getPassword();
}
return null;
}
@Override
public Long getUidbyname(String name) {
Staff s = loginmapper.getpwdbyname(name);
if (s != null){
return (long) s.getStaff_id();
}
return null;
}
@Override
public String getnamebyid(long id) {
Staff s=loginmapper.getnamebyid(id);
if(s!=null) {
return s.getUsername();
}
return null;
}
}
实体类
Message
public class Message {
//发送者name
public String from;
//接收者name
public String to;
//发送的文本
public String text;
//发送时间
@JSONField(format="yyyy-MM-dd HH:mm:ss")
public Date date;
}
MSG类
public class MSG {
String msg;
public MSG(String msg) {
super();
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
Staff类
public class Staff {
private byte staff_id;
private String first_name;
private String last_name;
private short address_id;
private String email;
private String username;
private String password;
private String last_update;
}
User类
public class User {
Long uid;
String name;
}
WebSocket在线测试工具
https://websocket.jsonin.com/
测试
WebSocket原理
如何建立连接
客户端:申请协议升级
首先,客户端发起协议升级请求。可以看到,采用的是标准的HTTP报文格式,且只支持GET方法:
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000/url
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
服务端:响应协议升级
服务端返回内容如下,状态代码101表示协议切换:
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
到此完成协议升级,后续的数据交互都按照新的协议来。
Sec-WebSocket-Key/Accept的作用
前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在于提供基础的防护,减少恶意
连接、意外连接。
- @
- 强调:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端/服务端是否合法的 ws客户端、ws服务端,其实并没有实际性的保证
数据帧格式
客户端、服务端数据的交换,离不开数据帧格式的定义。WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。
- 发送端:将消息切割成多个帧,并发送给服务端;
- 接收端:接收消息帧,并将关联的帧重新组装成完整的消息。
具体的帧格式如下所示:
数据传递
一旦WebSocket客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。WebSocket根据opcode来区分操作的类型。比如0x8表示断开连接,0x0-0x2表示数据交互
数据分片
WebSocket的每条消息可能被切分成多个数据帧。当WebSocket的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧。
FIN=1表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。FIN=0,则接收方还需要继续监听接收其余的数据帧。
此外,opcode在数据交换的场景下,表示的是数据的类型。0x01表示文本,0x02表示二进制。而0x00比较特殊,表示延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完。
数据分片例子
下面例子可以很好地演示数据的分片。客户端向服务端两次发送消息,服务端收到消息后回应客户端,这里主要看客户端往服务端发送的消息。
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
第一条消息:
FIN=1, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。
第二条消息:
- 1)FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成,还有后续的数据帧;
- 2)FIN=0,opcode=0x0,表示消息还没发送完成,还有后续的数据帧,当前的数据帧需要接在上一条数据帧之后;
- 3)FIN=1,opcode=0x0,表示消息已经发送完成,没有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。服务端可以将关联的数据帧组装成完整的消息。
连接保持+心跳
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。
但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。
这个时候,可以采用心跳来实现:
- 发送方->接收方:ping
- 接收方->发送方:pong
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。
举例:WebSocket服务端向客户端发送ping,只需要如下代码(采用ws模块)
ws.ping('', false, true);