WebSocket的简单认识&SpringBoot整合websocket
1. 什么是WebSocket?菜鸟对websocket的解释如下
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询(我目前接触的线上聊天也确实是基于这种方式实现的)。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
以下 API 用于创建 WebSocket 对象。
var Socket = new WebSocket(url, [protocol] );
WebSocket 属性
属性 | 描述 |
---|---|
Socket.readyState |
只读属性 readyState 表示连接状态,可以是以下值:
|
Socket.bufferedAmount |
只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。 |
WebSocket 事件:
事件 | 事件处理程序 | 描述 |
---|---|---|
open | Socket.onopen | 连接建立时触发 |
message | Socket.onmessage | 客户端接收服务端数据时触发 |
error | Socket.onerror | 通信发生错误时触发 |
close | Socket.onclose | 连接关闭时触发 |
WebSocket 方法:
方法 | 描述 |
---|---|
Socket.send() |
使用连接发送数据 |
Socket.close() |
关闭连接 |
2. Tomcat中开发简单的WebSocket实例
在这里实现一个简易的聊天室。
开发环境:JDK7+Tomcat7.0.91(如果连接URL报404建议切换tomcat为高版本,我在7.0.88测试就连接不到后端)
最终效果如下:
代码如下:
后端:
package chat; import java.io.IOException; import java.util.HashMap; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; @ServerEndpoint("/chat") public class ChatServer { private boolean first = true; private String name; private Session session; private static final HashMap<String, ChatServer> clientSet = new HashMap<String, ChatServer>(); public ChatServer() { System.out.println("========ChatServer创建============"); } /* * 客户端连接时触发该方法 */ @OnOpen public void onOpen(Session session) throws IOException { this.session = session; } /* * 客户端断开时触发该方法 */ @OnClose public void onClose() { clientSet.remove(name); String message = String.format("【%s %s】", name, "离开了聊天室!"); broadcast(message); } /* * 客户端收到消息时触发该方法 */ @OnMessage public void onMessage(String msg) { if (first) { name = msg; clientSet.put(name, this); String message = String.format("【%s %s】", name, "加入了聊天室!"); // 发送消息 broadcast(message); first = false; } else { broadcast("【" + name + "】" + ":" + msg); } } // 当客户端通信出现错误时,激发该方法 @OnError public void onError(Throwable t) throws Throwable { System.out.println("WebSocket服务端错误 " + t); } public void broadcast(String msg) { // 遍历服务器关联的所有客户端 ChatServer client = null; for (String nickname : clientSet.keySet()) { try { client = (ChatServer) clientSet.get(nickname); synchronized (client) { // 发送消息 client.session.getBasicRemote().sendText(msg); } } catch (IOException e) { System.out.println("聊天错误,向客户端 " + client + " 发送消息出现错误。"); clientSet.remove(name); try { client.session.close(); } catch (IOException e1) { } String message = String.format("【%s %s】", client.name, "已经被断开了连接。"); broadcast(message); } } } }
前端:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>简单聊天室</title> <script type="text/javascript"> var webSocket = null; if ("WebSocket" in window) { // 创建WebSocket对象 webSocket = new WebSocket("ws://localhost:86/WebSocket/chat"); } else { alert("您的浏览器不支持 WebSocket!"); } var sendMsg = function() { var inputElement = document.getElementById('msg'); if (inputElement.value == '') alert("输入内容不为空"); else { if (inputElement.value == "quit" || inputElement.value == "exit") { webSocket.close(); return; } // 发送消息 webSocket.send(inputElement.value); // 清空单行文本框 inputElement.value = ""; } } var send = function(event) { if (event.keyCode == 13) { sendMsg(); } }; webSocket.onopen = function() { n = prompt('请给自己取一个昵称:'); if (n != '' && n != null) webSocket.send(n); else //设置游客登录 webSocket.send("游客" + Math.random() * 100000000000000000); // 此处可以加一个异步请求name是否使用的请求来判断name是否可用 document.getElementById('msg').onkeydown = send; document.getElementById('sendBn').onclick = sendMsg; }; // 为onmessage事件绑定监听器,接收消息 webSocket.onmessage = function(event) { var show = document.getElementById('show') // 接收、并显示消息 show.innerHTML += new Date() + "<br/>" + event.data + "<br/>"; //让聊天框滚动条始终显示新消息 show.scrollTop = show.scrollHeight; }; webSocket.onclose = function() { document.getElementById('msg').onkeydown = null; document.getElementById('sendBn').onclick = null; }; </script> </head> <body> <div style="width: 600px; height: 240px; overflow-y: auto; border: 1px solid #333;" id="show"></div> <input type="text" size="80" id="msg" name="msg" placeholder="输入聊天内容" /> <input type="button" value="发送" id="sendBn" name="sendBn" /> </body> </html>
注意:
ws://localhost:86/WebSocket/chat 这个地址 是 协议://IP:Port/项目名称/websocket地址
第一次建立Socket连接的时候请求头和响应头如下:
- Connection 必须设置 Upgrade,表示客户端希望连接升级。
- Upgrade 字段必须设置 Websocket,表示希望升级到 Websocket 协议。
- Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
- Sec-WebSocket-Version 表示支持的 Websocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。
- Origin 字段是可选的,通常用来表示在浏览器中发起此 Websocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。
- 其他一些定义在 HTTP 协议中的字段,如 Cookie 等,也可以在 Websocket 中使用。
补充:上面是每个连接都会创建1个ChatServer对象,多例模式的对象。Session与HttpSession不一样。
3. SpringBoot中开启WebSocket
springboor整合websocket也非常简单。如下:
1. pom.xml增加如下依赖
<!-- websocket的包 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
2.代码如下
配置类:
package cn.qlq.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
ChatServer.java
package cn.qlq.websocket; import java.io.IOException; import java.util.Hashtable; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; import org.springframework.stereotype.Component; @ServerEndpoint("/chat") @Component public class ChatServer { private boolean first = true; private String name; private Session session; private static final Hashtable<String, ChatServer> clientSet = new Hashtable<String, ChatServer>(); public ChatServer() { System.out.println("========ChatServer创建============"); } /* * 客户端连接时触发该方法 */ @OnOpen public void onOpen(Session session) throws IOException { this.session = session; } /* * 客户端断开时触发该方法 */ @OnClose public void onClose() { clientSet.remove(name); String message = String.format("【%s %s】", name, "离开了聊天室!"); broadcast(message); } /* * 客户端收到消息时触发该方法 */ @OnMessage public void onMessage(String msg) { if (first) { name = msg; clientSet.put(name, this); String message = String.format("【%s %s】", name, "加入了聊天室!"); // 发送消息 broadcast(message); first = false; } else { broadcast("【" + name + "】" + ":" + msg); } } // 当客户端通信出现错误时,激发该方法 @OnError public void onError(Throwable t) throws Throwable { System.out.println("WebSocket服务端错误 " + t); } /** * 将此方法设计为静态方法用于服务器主动调用该方法广播消息 * * @param msg */ public static void broadcast(String msg) { // 遍历服务器关联的所有客户端 ChatServer client = null; for (String nickname : clientSet.keySet()) { try { client = (ChatServer) clientSet.get(nickname); synchronized (client) { // 发送消息 client.session.getBasicRemote().sendText(msg); } } catch (IOException e) { System.out.println("聊天错误,向客户端 " + client + " 发送消息出现错误。"); clientSet.remove(nickname); try { client.session.close(); } catch (IOException e1) { } String message = String.format("【%s %s】", client.name, "已经被断开了连接。"); broadcast(message); } } } }
Controller层代码:
package cn.qlq.controller.websocket; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import cn.qlq.websocket.ChatServer; @RequestMapping("/chat") @Controller public class ChatController { /** * 跳转到聊天页面 * * @return */ @RequestMapping("/chatRoom") public String chatRoom() { return "websocket/chatRoom"; } /** * http请求发送消息 * * @param msg * @return */ @RequestMapping("/sendMsg") public @ResponseBody String sendMsg(String msg) { ChatServer.broadcast(msg); return "success"; } }
前端页面:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>简单聊天室</title> <script type="text/javascript"> var webSocket = null; if ("WebSocket" in window) { // 创建WebSocket对象 webSocket = new WebSocket("ws://192.168.43.137:8088/chat"); } else { alert("您的浏览器不支持 WebSocket!"); } var sendMsg = function() { var inputElement = document.getElementById('msg'); if (inputElement.value == '') alert("输入内容不为空"); else { if (inputElement.value == "quit" || inputElement.value == "exit") { webSocket.close(); return; } // 发送消息 webSocket.send(inputElement.value); // 清空单行文本框 inputElement.value = ""; } } var send = function(event) { if (event.keyCode == 13) { sendMsg(); } }; webSocket.onopen = function() { n = prompt('请给自己取一个昵称:'); if (n != '') webSocket.send(n); else //设置游客登录 webSocket.send("游客" + Math.random() * 100000000000000000); // 此处可以加一个异步请求name是否使用的请求来判断name是否可用 document.getElementById('msg').onkeydown = send; document.getElementById('sendBn').onclick = sendMsg; }; // 为onmessage事件绑定监听器,接收消息 webSocket.onmessage = function(event) { var show = document.getElementById('show') // 接收、并显示消息 show.innerHTML += new Date() + "<br/>" + event.data + "<br/>"; //让聊天框滚动条始终显示新消息 show.scrollTop = show.scrollHeight; }; webSocket.onclose = function() { document.getElementById('msg').onkeydown = null; document.getElementById('sendBn').onclick = null; }; </script> </head> <body> <div style="width: 600px; height: 240px; overflow-y: auto; border: 1px solid #333;" id="show"></div> <input type="text" size="80" id="msg" name="msg" placeholder="输入聊天内容" /> <input type="button" value="发送" id="sendBn" name="sendBn" /> </body> </html>
2.测试
http发送消息:
经测试,上面ChatServer也是多例模式,每次请求都会创建ChatServer实例对象。
Git地址:https://github.com/qiao-zhi/springboot-ssm