WebSocket是什么?
- WebSocket是基于HTTP的全双工通讯协议。在WebSocket协议中,浏览器和服务器只需要完成一次握手,就可以创建持久性连接,并进行双向数据传输。
- WebSocket使用了HTTP/1.1的协议升级特性,一个WebSocket请求首先使用非正常的HTTP请求以特定的模式访问一个URL,这个URL有2种模式:ws、wss,对应HTTP、HTTPS;在请求头中Connection:Upgrade字段表示客户端想要对协议进行升级、Upgrade:WebSocket字段表示客户端想要将请求协议升级为WebSocket协议。
- 特点
- WebSocket使用时需要先创建连接,使得WebSocket成为一种有状态的协议。
- WebSocket连接在端口80(ws)或443(wss)上创建,与HTTP使用的端口相同,基本上所有的***都不会阻止WebSocket连接。
- WebSocket使用HTTP协议进行握手,因此它可以自然地集成到浏览器和HTTP服务器中,而不需要额外的成本。
- 心跳消息(ping、pong)将被反复发送,以保持WebSocket连接一直处于活跃状态。
- 使用该协议,当消息启动或到达的时候,服务端和客户端都可以知道。
- WebSocket连接关闭时将发送一个特殊的关闭消息。
- WebSocket支持跨域。
- HTTP规范要求浏览器将并发连接数限制为每个主机名两个连接,但使用WebSocket,当握手完成后,该限制就不存在了,因为此时的连接不再是HTTP连接了。
- WebSocket协议支持扩展。
更好的二进制支持以及更好的压缩效果。
- 其他类似技术
- 服务器发送事件:SSE(Server-Sent Event)提供的EventSource API可以实现服务器向客户端广播或推送信息,而客户端无法向服务器发送数据,且只支持文本数据。
- SPDY:扩充了HTTP,通过压缩HTTP首标和多路复用等手段改进HTTP请求性能。
- Web实时通信:WebRTC(Web Real-Time Communication)是Web的点对点技术。浏览器可以直接通信,而不需要通过服务器传输所有的数据。WebRTC包含可以让浏览器相互之间实时通信的API。
实现方式
服务器端
使用Java提供的@ServerEndpoint注解实现
创建一个服务端WebSocket处理类,并注解为@ServerEndpoint(value = "/myWebSocket")、@Component。
@ServerEndpoint(value = "/myWebSocket")让该类作为一个服务端Socket,监听/myWebSocket路径的访问。
@Component把该类交给Spring管理。
@ServerEndpoint(value = "/chat") @Component public class MyWebSocket { //用来存放每个客户端对应的MyWebSocket对象 private static CopyOnWriteArraySet<MyWebSocket> user = new CopyOnWriteArraySet<MyWebSocket>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; @OnMessage public void onMessage(String message, Session session) throws Exception { Message m = JSON.parseObject(message, Message.class); boolean flag = false;// true:设置昵称;false:聊天内容 if (m.getType().equals("nickName")) { UserContainer.addUserNickName(session.getId(), m.getMessage()); flag = true; } Message msg = new Message(); if (flag) { msg.setType("welcome"); msg.setMessage(UserContainer.getUserNickName(session.getId()) + " 进入聊天室"); Message count = new Message(); count.setType("count"); count.setMessage(user.size() + ""); for (MyWebSocket myWebSocket : user) { myWebSocket.session.getBasicRemote().sendText(JSON.toJSONString(count)); } } else { msg.setType("news"); msg.setNickName(UserContainer.getUserNickName(session.getId())); msg.setMessage(m.getMessage()); } //群发消息 for (MyWebSocket myWebSocket : user) { myWebSocket.session.getBasicRemote().sendText(JSON.toJSONString(msg)); } } @OnOpen public void onOpen(Session session) { this.session = session; user.add(this); } @OnClose public void onClose(Session session) throws Exception{ user.remove(this); Message msg = new Message(); msg.setType("welcome"); msg.setMessage(UserContainer.getUserNickName(session.getId()) + " 退出聊天室"); UserContainer.removeUserNickName(session.getId()); Message count = new Message(); count.setType("count"); count.setMessage(user.size() + ""); for (MyWebSocket myWebSocket : user) { myWebSocket.session.getBasicRemote().sendText(JSON.toJSONString(count)); myWebSocket.session.getBasicRemote().sendText(JSON.toJSONString(msg)); } } @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } }
使用SpringBoot内置Tomcat,需要添加如下类
@Configuration public class WebConfig { /** * 支持websocket * 如果不使用内置tomcat,则无需配置 * @return */ @Bean public ServerEndpointExporter createServerEndExporter(){ return new ServerEndpointExporter(); } }
使用STOMP消息实现(Java EE开发的颠覆者:Spring Boot实战)
创建Spring项目,引入依赖(Spring Boot版本:2.1.3.RELEASE)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
广播式:服务端有消息时,会将消息发送给所有连接了当前endpoint的浏览器。
配置WebSocket
@Configuration @EnableWebSocketMessageBroker // 开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping public class MyWebSocketConfig implements WebSocketMessageBrokerConfigurer{ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/chat").withSockJS();// 注册一个STOMP的endpoint,并指定使用SockJS协议。 } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic");// 配置消息代理。 } }
封装消息
/** * 浏览器发送给服务端的消息 */ public class ClientMessage { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } /** * 服务端发送给浏览器的消息 */ public class ServerMessage { private String response; public ServerMessage(String response){ this.response=response; } public String getResponse() { return response; } }
Controller
@Controller public class WsController { @MessageMapping("/welcome") // 当浏览器向服务端发送请求时,将"/welcome"映射到这个方法,类似于@RequestMapping @SendTo("/topic/getResponse") // 当服务端有消息时,会对订阅了该路径的浏览器发送消息 public ServerMessage say(ClientMessage message) throws Exception{ Thread.sleep(1000); return new ServerMessage("Welcome,"+message.getName()+"!"); } }
点对点式(Spring Boot实战 205页)
客户端
- 创建WebSocket对象
```java
<!--
1.实例化一个WebSocket对象,指定服务器URL。
2.ws:非加密方式、wss:加密方式
3.可选参数protocols表示支持的子协议,可选值为:XMPP(可扩展消息处理现场协议)、SOAP(简单对象访问协议)、STOMP(简单可互操作的协议)或自定义协议。可以以数组的形式告诉服务器客户端所支持的自协议,服务器将从中选择一个。
-->
var websocket = new WebSocket("ws://localhost:8080/chat"[,["myProtocol1","myProtocol2"]]);
- WebSocket对象的事件 ```java <!-- WebSocket API是纯事件驱动的。WebSocket编程遵循异步编程模式。WebSocket对象的事件如下: open:一旦服务器响应了WebSocket连接请求,open事件触发并建立一个连接。 message:在接收到消息时触发。可以是文本、二进制数据;如果是二进制数据,在接收前需要设置类型。 error:响应故障时触发。 close:WebSocket连接关闭时触发。 --> websocket.onopen = function (event) { console.log("连接成功"); } var binaryType='blob'; <!-- var binaryType='arraybuffer'; --> websocket.onmessage = function (event) { console.log("收到消息"); var blob=new Blob(event.data); <!-- var arr=new Unit8Array(event.data); --> }
WebSocket对象的方法
<!-- 在连接建立后向服务器发送消息 --> var blob=new Blob("hello"); websocket.send(blob); var arr=new Unit8Array([1,2,3]); websocket.send(a.buffer); <!-- 客户端主动关闭连接 --> websocket.close();
WebSocket对象的属性
readyState:连接状态;0:连接中、1:连接建立、2:关闭中、3:连接关闭。 bufferedAmount:发往服务器的缓冲数据量。 protocol:当前使用的子协议。
检查WebSocket支持
if ('WebSocket' in window) { console.log('支持'); }else{ console.log('当前浏览器不支持'); }
完整访问页面
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.css"> <link rel="stylesheet" href="/css/me.css"> <title>Spring Boot+WebSocket+广播式</title> </head> <body onload="disconnect();"> <noscript><h2 style="color:#ff0000">貌似当前浏览器不支持WebSocket</h2></noscript> <div class="ui container"> <div class="ui basic segment center aligned"> <h3>在线人数:<span id="count"></span></h3> </div> <div class="ui segment center aligned" style="height: 500px;overflow-x: hidden;overflow-y: scroll;" id="response"> </div> <div id="conversationDiv" class="ui segment"> <label class="ui big label">输入你的昵称</label> <div class="ui input"> <input type="text" id="name"> </div> <button class="ui orange button" id="sendName" onclick="sendName();">设定</button> </div> <div class="ui basic segment" id="MyMessage"> <div class="ui segment"> <textarea name="" id="message" cols="30" rows="10" style="height:200px;width:100%;"></textarea><br> </div> <button class="ui orange button" onclick="sendMessage();">发送</button> </div> <div class="ui basic segment right aligned"> <button class="ui teal button" id="connect" onclick="connect();">上线</button> <button class="ui red button" id="disconnect" disabled="disabled" onclick="disconnect();">下线</button> </div> </div> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <script src="https://cdn.bootcss.com/sockjs-client/1.4.0/sockjs.min.js"></script> <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script> <script src="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.js"></script> <script th:inline="javascript" type="text/javascript"> var websocket = null; //关闭WebSocket连接 function disconnect() { if (websocket != null) { websocket.close(); } document.getElementById('connect').disabled = false; document.getElementById('disconnect').disabled = true; document.getElementById('conversationDiv').style.display = 'none'; document.getElementById('MyMessage').style.display = 'none'; } function connect() { //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { websocket = new WebSocket("ws://localhost:8080/chat"); //接收到消息的回调方法 websocket.onmessage = function (event) { var data = event.data; var message = JSON.parse(data); if (message.type == "welcome") { document.getElementById('response').innerHTML += '<div style="opacity: 0.5;clear:both;">' + message.message + '</div>' + '<br/>'; } if (message.type == "news") { nickName = $('#name').val(); if (nickName == message.nickName) { document.getElementById('response').innerHTML += '<span style="opacity: 0.5;float:right;clear:both;">' + message.nickName + ' ' + '</span>' + '<div class="ui teal segment p-message" style="float:right;">' + message.message + '</div><br>'; } else { document.getElementById('response').innerHTML += '<span style="opacity: 0.5;float:left;clear:both;">' + message.nickName + ' ' + '</span>' + '<div class="ui teal segment p-message" style="float:left;">' + message.message + '</div><br>'; } } if (message.type == "count") { $('#count').text(message.message); } $('#response').scrollTop($('#response').prop('scrollHeight')); } //连接成功建立的回调方法 websocket.onopen = function () { console.log("连接成功"); document.getElementById('connect').disabled = true; document.getElementById('disconnect').disabled = false; document.getElementById('conversationDiv').style.display = ''; } //连接关闭的回调方法 websocket.onclose = function () { console.log("连接关闭"); document.getElementById('disconnect').disabled = true; } //连接发生错误的回调方法 websocket.onerror = function () { console.log("连接错误"); } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function () { disconnect(); } } else { alert('当前浏览器不支持websocket'); } } //设置昵称 function sendName() { var message = document.getElementById('name').value; websocket.send(JSON.stringify({'type': 'nickName', 'message': message})); document.getElementById('MyMessage').style.display = ''; document.getElementById('conversationDiv').style.display = 'none'; } //发送聊天内容 function sendMessage() { var message = document.getElementById('message').value; websocket.send(JSON.stringify({'type': 'news', 'message': message})); document.getElementById('message').value = ''; } </script> </body> </html>
SockJS
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Spring Boot+WebSocket+广播式</title> </head> <body onload="disconnect()"> <noscript><h2 style="color:#ff0000">貌似当前浏览器不支持WebSocket</h2></noscript> <div> <div> <button id="connect" onclick="connect();">连接</button> <button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button> </div> <div id="conversationDiv"> <label>输入你的昵称</label><input type="text" id="name"> <button id="sendName" onclick="sendName();">发送</button> <p id="response"></p> </div> </div> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <script src="https://cdn.bootcss.com/sockjs-client/1.4.0/sockjs.min.js"></script> <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script> <script th:inline="javascript" type="text/javascript"> var stompClient=null; function setConnected(connected){ document.getElementById('connect').disabled=connected; document.getElementById('disconnect').disabled=!connected; document.getElementById('conversationDiv').style.visibility=connected?'visible':'hidden'; $('#response').html(); } function connect(){ var socket=new SockJS("/chat");// 连接SockJS的endpoint名为"/chat" stompClient=Stomp.over(socket);// 使用STOMP子协议 stompClient.connect({},function(frame){ // 连接WebSocket服务端 setConnected(true); stompClient.subscribe('/topic/getResponse',function(response){// 订阅/topic/getResponse发送的消息。 showResponse(JSON.parse(response.body).response); }); }); } function disconnect(){ if(stompClient!=null){ stompClient.disconnect(); } setConnected(false); } function sendName(){ var name=$('#name').val(); stompClient.send('/welcome',{},JSON.stringify({'name':name}));// 向/welcome发送消息 } function showResponse(message){ var response=$('#response'); response.html(message); } </script> </body> </html>