websocket
websocket协议
Websocket协议设计的目标是使用一个连接实现客户端与服务器之间的通讯以此替代http长轮训。
WebSocket 的出现使得浏览器提供对 Socket 的支持成为可能,从而在浏览器和服务器之间提供了一个基于 TCP 连接的双向通道。
Websocket位于TCP之上(位于应用层),主要包括握手过程、数据传输两个主要部分
默认情况下,普通websocket连接使用80端口,安全的websocket连接使用443端口,基于TLS安全层。
- ws默认是80端口
- wss默认端口是443
upgrade头部字段
浏览器需要通过 HTTP 协议的 GET 请求,将 HTTP 协议升级为 WebSocket 协议。
请求头
请求头说明
Connection:Upgrade 协议升级为WebSocket协议
Upgrade:WebSocket 标识该HTTP请求是一个协议升级请求
HTTP1.1 协议规定,Upgrade表示将通信协议从HTTP/1.1转向该字段指定的协议
Sec-Websocket-Key: 则是用于握手协议的密钥。
客户端采用base64编码的24位随机字符序列,服务器接受客户端HTTP协议升级的证明。
要求服务端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答
Sec-WebSocket-Extensions: 协议扩展类型
响应头
Sec-WebSocket-Accept:浏览器对这个值进行验证,以证明确实是目标服务器回应了 WebSocket 请求。
nginx配置
平时很多项目都是使用nginx部署上线的,那么如果项目里使用了websocket,相对应的也需要去配置nginx。
如下配置,表明websocket
连接进入的时候,进行一个连接升级将http
连接变成websocket
的连接。
location /myWs/ {
proxy_http_version 1.1;
proxy_read_timeout 3600s; // 超时设置
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
默认情况下,如果代理服务器在60秒内未传输任何数据,则连接将关闭。可以使用proxy_read_timeout指令来增加此超时时间 。
或者,可以将代理服务器配置为定期发送WebSocket ping帧以重置超时并检查连接是否仍然有效。
网页在线测试
启动springboot应用,websocket在线测试
在线websocket测试-在线工具-postjson (coolaf.com)
服务端开发(第一套)
使用 @ServerEndpoint 进行的传统 WebSocket 开发,
原生注解
javax.websocket.*的方式这套代码中定义了一套适用于 WS 开发的注解和相关支持,我们可以利用它和 Tomcat 进行WS 开发,
依赖和配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置类中注入ServerEndpointExporter
先写个能扫描 @ServerEndpoint 的配置, 不然在客户端连接的时候会一直连不上。不是在 SpringBoot 下开发的可以跳过这一环节。
@Configuration public class WebSocketConfig { /** * 注入ServerEndpointExporter, * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
javax.websocket
使用的这几个注解需要注意一下,首先是他们的包都在 **javax.websocket **下。并不是 spring 提供的,而 jdk 自带的,下面是他们的具体作用。
@ServerEndpoint
javax.websocket.server.ServerEndpoint
通过这个 springboot 就可以知道你暴露出去的 ws 应用的路径,有点类似我们经常用的@RequestMapping。
比如你的启动端口是 8080,而这个注解的值是 ws,那我们就可以通过 ws://127.0.0.1:8080/ws 来连接你的应用
//客户端连接的地址 @ServerEndpoint("/myWs")
@OnOpen
javax.websocket.OnOpen
当 websocket 建立连接成功后会触发这个注解修饰的方法,注意它有一个 Session 参数
@OnClose
javax.websocket.OnClose
当 websocket 建立的连接断开后会触发这个注解修饰的方法,注意它有一个 Session 参数
@OnMessage
javax.websocket.OnMessage
当客户端发送消息到服务端时,会触发这个注解修改的方法,它有一个 String 入参表明客户端传入的值
@OnError
javax.websocket.OnError
当 websocket 建立连接时出现异常会触发这个注解修饰的方法,注意它有一个 Session 参数
session.getBasicRemote().sendText(*)
服务端发送消息给客户端,必须通过上面说的 Session 类,通常是在@OnOpen 方法中,当连接成功后把 session 存入 Map 的 value,key 是与 session 对应的用户标识,当要发送的时候通过 key 获得 session 再发送,这里可以通过 session.getBasicRemote().sendText(*)* 来对客户端发送消息。
服务器端
@ServerEndpoint主要是将目前的类定义成一个websocket服务器端, 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
注意:这个方式开发的 WebSocket 服务器,每个客户端连接都会创建一个@ServerEndpoint 标注的类的实例。
/**
* @ServerEndpoint 该注解可以将类定义成一个WebSocket服务器端
*/
@ServerEndpoint("/myWs")
@Component
public class WebSocketServerOne {
//concurrent包下线程安全的Set
private static final CopyOnWriteArraySet<WebSocketServerOne> SESSIONS = new CopyOnWriteArraySet<>();
//声明session对象,通过该对象可以发送消息给指定的用户
private Session session;
/**
* @param session
* @OnOpen 当开启一个新的会话时调用,客户端与服务端握手成功后调用的方法
*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
SESSIONS.add(this);
System.out.println(String.format("成功建立连接~ 当前总连接数为:%s", SESSIONS.size()));
}
/**
* @OnClose 当会话关闭时调用
*/
@OnClose
public void onClose() {
SESSIONS.remove(this);
System.out.println(String.format("成功关闭连接~ 当前总连接数为:%s", SESSIONS.size()));
}
/**
* 服务端接收客户端消息
* 通过@OnMessage注解指定接收消息的方法
*
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println(message);
}
/**
* @param session
* @param error
* @OnError 当连接过程中异常时调用
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
/**
* 服务端发送数据给客户端
*
* @param message
*/
public void sendMessage(String message) {
try {
//发送消息则由RemoteEndpoint完成,其实例由session维护
this.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 群发消息
*
* @param message
*/
public static void fanoutMessage(String message) {
SESSIONS.forEach(ws -> ws.sendMessage(message));
}
}
我们可以通过
1、session.getBasicRemote获取同步消息发送的实例,然后调用其 sendXxx()方法就可以发送消息
2、可以通过session.getAsyncRemote获取异步消息发送实例
session.getBasicRemote().sendText(msg);
编写前端代码测试
WebSocket的所有操作都是采用事件的方式触发的,这样不会阻塞UI,是的UI有更快的响应时间,有更好的用户体验。
WebSocket.send()
方法将需要通过 WebSocket 链接传输至服务器的数据排入队列,并根据所需要传输的 data bytes 的大小来增加 bufferedAmount
的值。若数据无法传输(例如数据需要缓存而缓冲区已满)时,套接字会自行关闭。
// WebSocket构造函数,创建WebSocket对象
let ws = null;
export function ws_create(url) {
ws = new WebSocket(url);
ws.onopen = onopen;
ws.onmessage = onmessage;
ws.onclose = onclose;
ws.onerror = onerror;
}
// 连接成功后的回调函数
function onopen() {
console.log("客户端连接成功");
// 向服务器发送消息
ws.send("hello");
}
//当客户端收到服务端发来的消息时,触发onmessage事件,参数e.data包含server传递过来的数据
function onmessage(e) {
console.log("收到服务器响应", e.data);
}
//当客户端收到服务端发送的关闭连接请求时,触发onclose事件
function onclose(e) {
var code = e.code; // 表示服务端发送的关闭码
var reason = e.reason; // 表示服务器关闭连接的原因
var wasClean = e.wasClean; // 表示连接是否完全关闭
console.log("关闭客户端连接");
}
//如果出现连接、处理、接收、发送数据失败的时候触发onerror事件
function onerror(e) {
console.log("连接失败了");
}
export function ws_close() {
ws.close();
//ws.close(code, reason)
}
close()
方法关闭WebSocket
连接或连接尝试(如果有的话)。如果连接已经关闭,则此方法不执行任何操作。code一个数字状态码,它解释了连接关闭的原因。如果没有传这个参数,默认使用 1005
后端控制台打印如下:
服务端开发(第二套)
使用Spring 封装的形式,更适合业务要求比较灵活多变。
依赖和配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置类:
@Configuration @EnableWebSocket//用于开启WebSocket相关功能, public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) { webSocketHandlerRegistry .addHandler(myWebsocketHandler(), "myWS")//监听某个 URL .setAllowedOrigins("*");//设置跨域 } public WebSocketHandler myWebsocketHandler() { return new MyHandler(); } }
当客户端通过
/mySW
l和服务端连接通信时,使用MyHandler
处理会话。
WebSocketHandler
可以实现org.springframework.web.socket.WebSocketHandler,
也可以继承org.springframework.web.socket.handler.TextWebSocketHandler
或org.springframework.web.socket.handler.BinaryWebSocketHandler
它们的继承关系如下图所示:
根据业务的不同,
----------------------------------
若是只涉及到文本信息,那么我们可以继承
TextWebSocketHandler
;若是只需要传递二进制信息,那么可以继承
BinaryWebSocketHandler
;如果两种信息都有的话,可以继承
AbstractWebSocketHandler
或实现WebSocketHandler
接口
处理类
MyHandler.java
@Component
public class MyHandler extends TextWebSocketHandler {
/**
* afterConnectionEstablished,和客户端链接成功的时候触发该方法
*
* @param session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("和客户端建立连接");
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
session.close(CloseStatus.SERVER_ERROR);
System.out.println("连接异常" + exception.getMessage());
}
/**
* handleTextMessage,和客户端建立连接后,处理客户端发送的请求。
*
* @param session
* @param message
* @throws Exception
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 获得客户端传来的消息
String receiveMessage = message.getPayload();
System.out.println("server 接收到消息 " + receiveMessage);
session.sendMessage(new TextMessage(receiveMessage));
}
/**
* afterConnectionClosed,和客户端断开连接的时候触发该方法
*
* @param session
* @param status
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
super.afterConnectionClosed(session, status);
System.out.println("和客户端断开连接");
}
}
启动springboot应用,websocket在线测试
在线websocket测试-在线工具-postjson (coolaf.com)
服务端开发(第三套)
STOMP实现websocket,
STOMP:Simple (or Streaming) Text Orientated Messaging Protocol,即简单(流)文本定向消息协议
对于STOMP协议来说, client分为消费者client与生产者client两种. server是指broker, 也就是消息队列的管理者。STOMP协议并不是为websocket设计的, 它是属于消息队列的一种协议, 和amqp, jms平级。只不过由于它的简单性恰巧可以用于定义websocket的消息体格式。
依赖和配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置:WebSocketConfig.java
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 配置客户端尝试连接地址
registry.addEndpoint("/ws").setAllowedOrigins("*");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 设置广播节点
registry.enableSimpleBroker("/topic", "/queue");
// 客户端向服务端发送消息需有/app 前缀
registry.setApplicationDestinationPrefixes("/app");
// 指定用户发送(一对一)的前缀 /user/
registry.setUserDestinationPrefix("/user/");
}
}
请求处理的Controller
@Controller
public class WsController {
@Autowired
SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/ws/chat")
public void handleMsg(Authentication authentication, ChatMsg chatMsg) {
Hr hr = (Hr) authentication.getPrincipal();
chatMsg.setFrom(hr.getUsername());
chatMsg.setFromNickname(hr.getName());
chatMsg.setDate(new Date());
simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/chat", chatMsg);
}
}
拦截器【扩展】
我们使用 registration.interceptors(interceptor) 方法来添加拦截器。拦截器可以用于处理客户端发送的消息,例如在消息到达服务器之前进行身份验证、日志记录等操作。
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private MyInterceptor interceptor;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
//参考基础配置...
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//参考基础配置...
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
//注入一个名为 MyInterceptor 的拦截器
registration.interceptors(interceptor);
}
}
MyInterceptor
import java.util.Map;
import org.apache.logging.log4j.util.Strings;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
@Component
public class MyInterceptor implements HandshakeInterceptor {
/**
* 握手前
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
System.out.println("握手开始");
String hostName = request.getRemoteAddress().getHostName();
String sessionId = hostName+String.valueOf((int)(Math.random()*1000));
if (Strings.isNotBlank(sessionId)) {
// 放入属性域
attributes.put("session_id", sessionId);
System.out.println("用户 session_id " + sessionId + " 握手成功!");
return true;
}
System.out.println("用户登录已失效");
return false;
}
/**
* 握手后
*/
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
System.out.println("握手完成");
}
}
消息转换器【扩展】
在 configureMessageConverters() 方法中,我们使用MyMessageConverter messageConverters.add(messageConverter) 方法来添加消息转换器。消息转换器可以用于将消息从一种格式转换为另一种格式,例如将 JSON 格式的消息转换为 Java 对象。
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private MyMessageConverter messageConverter;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
//参考基础配置...
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//参考基础配置...
}
@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
//注入一个名为 MyMessageConverter 的消息转换器
messageConverters.add(messageConverter);
return true;
}
}
MyMessageConverter
websocket客户端
创建websocket
以下 API 用于创建 WebSocket 对象。
//第一个参数 url, 指定连接的 URL
//第二个参数 protocol 是可选的,指定了可接受的子协议。
var Socket = new WebSocket(url, [protocol] );
浏览器端创建一个WebSocket
//参数url格式: ws://ip地址:端口号/资源名称 var ws = new WebSocket(url);
websocket回调
//onopen onopen
ws.addEventListener('open', function (event) {
ws.send('Hello Server!');
});
ws.addEventListener("close", function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
});
ws.addEventListener("message", function(event) {
//文本数据
if(typeOf event.data === String) {
console.log("Received data string");
}
var data = event.data;
// 处理数据
});
ws.addEventListener("error", function(event) {
// handle error event
});
websocket属性
Socket.readyState
只读属性 readyState 表示连接状态,共有四种。
- CONNECTING:值为0,表示连接尚未建立。
- OPEN:值为1,表示连接成功,可以通信了。
- CLOSING:值为2,表示连接正在关闭。
- CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
Socket.bufferedAmount
只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。
send()和close()
send()方法:使用连接发送数据
close()方法:关闭连接
ws.send('your message');
心跳检测和断线重连
在前端监听到WebSocket的onclose()事件时,重新创建WebSocket连接。
// 主窗口socket
import { Message } from "element-ui";
let Socket = ''
let myPopup = null;
let currRecordType = '1';
let getMsgFun = null;
let urlSocket = '';
var lockReconnect = false; //避免ws重复连接
/**
* 建立websocket连接
* @param {string} url ws地址
* @param recordType 录制类型
* @param fun 接收信息的回调函数
*/
export const createSocket = (url, recordType, fun) => {
urlSocket = url;
getMsgFun = fun;
currRecordType = recordType;
Socket && Socket.close(1000)
if (!Socket) {
console.log('建立websocket连接:' + url)
Socket = new WebSocket(url)
console.log(Socket);
Socket.onopen = onopenWS
Socket.onmessage = onmessageWS
Socket.onerror = onerrorWS
Socket.onclose = oncloseWS
} else {
console.log('websocket已连接')
}
}
/**
* 关闭websocket连接
*/
export const closeSocket = () => {
Socket && Socket.close(1000)
}
/**
* 打开ws之后初始化
*/
const onopenWS = () => {
console.log('onopenWS');
//心跳检测重置
heartCheck.reset().start();
}
/**WS数据接收统一处理 */
const onmessageWS = e => {
//如果获取到消息,心跳检测重置
//拿到任何消息都说明当前连接是正常的
heartCheck.reset().start();
if (e.data != 'main-pong') {
getMsgFun({
data: {
msg: e.data,
type: currRecordType,
}
});
}
}
/**
* 连接失败
*/
const onerrorWS = () => {
// Socket.close() 只有在触发close事件之前,才会触发error事件
console.log(Socket.readyState);
}
/**
* 连接断开
*/
const oncloseWS = (e) => {
console.log('websocket已断开....')
Socket = null;
if (e.code == 1000) {
// alert("正常关闭");
//1000表示正常关闭
} else {
// alert("异常断开,重连");
//1005表示连接的关闭是由于未提供错误信息而引起的
reconnect();
}
}
function reconnect() {
if (lockReconnect) return;
lockReconnect = true;
//设置延迟避免请求过多
setTimeout(function () {
// alert('执行断线重连...' + urlSocket)
createSocket(urlSocket, currRecordType, getMsgFun)
lockReconnect = false;
}, 2000);
}
//心跳检测 5s
var heartCheck = {
timeout: 5000,
timeoutObj: null,
serverTimeoutObj: null,
reset: function () {
//停止发送心跳
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function () {
var self = this;
this.timeoutObj = setTimeout(function () {
//这里发送一个心跳,后端收到后,返回一个心跳消息,
Socket.send("main-ping");
//如果超过一定时间还没重置,说明后端主动断开了
self.serverTimeoutObj = setTimeout(function () {
Socket.close(1000); //执行Socket.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
}, self.timeout)
}, this.timeout)
}
}