您的位置:首页 > 财经 > 金融 > websocket

websocket

2024/12/23 10:56:58 来源:https://blog.csdn.net/Forbidden_City/article/details/140577840  浏览:    关键词:websocket

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属性返回实例对象的当前状态,共有四种:

CONNECTING:值为0,表示正在连接。
OPEN:值为1,表示连接成功,可以通信了。
CLOSING:值为2,表示连接正在关闭。
CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

switch (ws.readyState) {case WebSocket.CONNECTING:// do somethingbreak;case WebSocket.OPEN:// do somethingbreak;case WebSocket.CLOSING:// do somethingbreak;case WebSocket.CLOSED:// do somethingbreak;default:// this never happensbreak;
}

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*/@Beanpublic 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;/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("userId") String userId) {this.session = session;this.userId = userId;//加入mapwebSocketMap.put(userId, this);addOnlineCount(); //在线数加1logger.info("用户{}连接成功,当前在线人数为{}", userId, getOnlineCount());try {sendMessage(String.valueOf(this.session.getQueryString()));} catch (IOException e) {logger.error("IO异常");}}/*** 连接关闭调用的方法*/@OnClosepublic void onClose() {//从map中删除webSocketMap.remove(userId);subOnlineCount(); //在线数减1logger.info("用户{}关闭连接!当前在线人数为{}", userId, getOnlineCount());}/*** 收到客户端消息后调用的方法** @param message 客户端发送过来的消息*/@OnMessagepublic 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*/@OnErrorpublic 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 {@Autowiredprivate 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;}}}//建立连接成功调用@OnOpenpublic 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));}//关闭连接时调用@OnClosepublic 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群发消息@OnMessagepublic 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));}}//错误时调用@OnErrorpublic 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 {@AutowiredLoginService 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)@ResponseBodypublic 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 {@AutowiredLoginService loginservice;@RequestMapping("/onlineusers")@ResponseBodypublic 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")@ResponseBodypublic 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{@Autowiredprivate LoginMapper loginmapper;@Overridepublic String getpwdbyname(String name) {Staff s=loginmapper.getpwdbyname(name);if(s!=null) {return s.getPassword();}return null;}@Overridepublic Long getUidbyname(String name) {Staff s = loginmapper.getpwdbyname(name);if (s != null){return (long) s.getStaff_id();}return null;}@Overridepublic String getnamebyid(long id) {Staff s=loginmapper.getnamebyid(id);if(s!=null) {return s.getUsername();}return null;}
}

实体类
Message

public class Message {//发送者namepublic String from;//接收者namepublic 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/

测试

建立连接
ws://127.0.0.1:8080/websocket/1
ws://127.0.0.1:8080/websocket/2
发起请求
127.0.0.1:8092/webSocket/sentMessage?userId=1&message=请进入视频会议
127.0.0.1:8092/webSocket/sentMessage?userId=2&message=请进入视频会议

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==

Connection: Upgrade:表示要升级协议
Upgrade: websocket:表示要升级到websocket协议。
Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个
Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
Sec-WebSocket-Key:与后面服务端响应头的Sec-WebSocket-Accept是配套的,提供基本的防
护,比如恶意的连接,或者无意的连接。

服务端:响应协议升级

服务端返回内容如下,状态代码101表示协议切换:

HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=

到此完成协议升级,后续的数据交互都按照新的协议来。

Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。
计算公式为:
1)将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接;
2)通过SHA1计算出摘要,并转成base64字符串。

Sec-WebSocket-Key/Accept的作用

前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在于提供基础的防护,减少恶意
连接、意外连接。

  • @
  • 强调:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端/服务端是否合法的 ws客户端、ws服务端,其实并没有实际性的保证

数据帧格式

客户端、服务端数据的交换,离不开数据帧格式的定义。WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。

  • 发送端:将消息切割成多个帧,并发送给服务端;
  • 接收端:接收消息帧,并将关联的帧重新组装成完整的消息。

具体的帧格式如下所示:
在这里插入图片描述

FIN:1个比特
如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是是消息
(message)的最后一个分片(fragment)。
RSV1, RSV2, RSV3:各占1个比特
一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值
的含义由扩展进行定义。如果出现非零的值,且并没有采用WebSocket扩展,连接出错。
Opcode: 4个比特
操作代码,Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是
不认识的,那么接收端应该断开连接(fail the connection)。可选的操作代码如下:
%x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的
数据帧为其中一个数据分片;

  • %x1:表示这是一个文本帧(frame);
  • %x2:表示这是一个二进制帧(frame);
  • %x3-7:保留的操作代码,用于后续定义的非控制帧;
  • %x8:表示连接断开;
  • %x8:表示这是一个ping操作;
  • %xA:表示这是一个pong操作;
  • %xB-F:保留的操作代码,用于后续定义的控制帧。

Mask: 1个比特
表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;
从服务端向客户端发送数据时,不需要对数据进行掩码操作。
如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。
如果Mask是1,那么在Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1
Payload length:数据载荷的长度,单位是字节。为7位,或7+16位,或1+64位
假设数Payload length === x,如果:

  • x为0~126:数据的长度为x字节;
  • x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度;
  • x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。

此外,如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(bigendian,重要的位在前)。
Masking-key:0或4字节(32位)
所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key
备注:载荷数据的长度,不包括mask key的长度。
Payload data:(x+y) 字节

  • 载荷数据:
    包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节;
  • 扩展数据:
    如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长
    度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。
    如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内;
  • 应用数据:
    任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷
    数据长度 减去 扩展数据长度,就得到应用数据的长度。

数据传递

一旦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);

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com