
什么是websocket
Websocket是一种在单个TCP连接上进行全双工通信的协议
为什么需要websocket
HTTP协议有一个缺陷:通信只能由客户端发起
这种单向请求的特点导致如果服务端出现连续状态的变化,客户端要想获知就比较麻烦,我们只能使用”轮询”模式
轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

特点
- 建立在TCP协议之上,服务端的实现比较容易
- 与HTTP协议有良好的兼容。默认端口也是80和443,并且握手阶段采用HTTP协议,因此握手时不容易屏蔽,能通过各种HTTP代理服务器
- 数据格式比较轻,新能开销小,通信高效
- 可以发送文本,也可以发送二进制数据
- 没有同源限制,客户端可以与任意服务器通信
- 协议标识符是
ws
(如果加密,则为wss
),服务器网址就是URL
实战
STOMP协议
STOMP即Simple Text Orientated Messaging Protocol,简单文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互
后端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic", "/user");
config.setApplicationDestinationPrefixes("/app"); config.setUserDestinationPrefix("/user/"); }
@Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/my-websocket").setAllowedOrigins("*").withSockJS(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Controller @EnableScheduling @SpringBootApplication public class App {
public static void main(String[] args) { SpringApplication.run(App.class, args); }
@Autowired private SimpMessagingTemplate messagingTemplate;
@GetMapping("/") public String index() { return "index"; }
@Scheduled(fixedRate = 1000) public Object time() throws Exception { DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); messagingTemplate.convertAndSend("/topic/time", df.format(new Date())); return "time"; }
@Scheduled(fixedRate = 2000) @SendToUser("/greetings") public String greeting2() { messagingTemplate.convertAndSendToUser("2", "/greetings", "欢迎您,用户: 2"); return "OK"; }
@Scheduled(fixedRate = 2000) @SendToUser("/greetings") public String greeting1() { messagingTemplate.convertAndSendToUser("1", "/greetings", "欢迎您,用户: 1"); return "OK"; }
@Scheduled(fixedRate = 9000) public Object notification() { messagingTemplate.convertAndSend("/topic/notification", "hello world!"); return "ok"; } }
|
前端
demo1和demo2都是订阅了两个topic。1个是获得服务器推送的时间,另一个是获得定向推送的消息(demo1向userId=1的用户推送,demo2向userId=2的用户推送)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| <template> <div id="app"> <div> <label>WebSocket连接状态:</label> <button type="button" :disabled="connected" @click="connect()">连接</button> <button type="button" @click="disconnect()" :disabled="!connected">断开</button> </div>
<div v-if="connected"> <label>当前服务器时间:{{ time }}</label> <br />消息列表: <br /> <hr /> <table> <thead> <tr> <th>内容</th> </tr> </thead> <tbody> <tr v-for="row in lala"> <td>{{row}}</td> </tr> </tbody> </table> </div> </div> </template>
<script>
export default { name: "Demo1", data() { return { stompClient: "", connected: false, lala: [], time: "" }; }, methods: { connect() { const socket = new SockJS("http://localhost:8080/my-websocket"); this.stompClient = Stomp.over(socket); const that = this this.stompClient.connect({}, function(frame) { that.stompClient.subscribe("/user/1/greetings", function(msg) { that.lala.push(msg); }); that.stompClient.subscribe("/topic/time", function(response) { that.time = response.body; });
that.connected = true; }); }, disconnect() { if (this.stompClient != null) { this.stompClient.disconnect(); }
this.connected = false; this.lala = []; } } }; </script>
|
demo3是订阅后端topic,每隔一段时间推送消息给浏览器,随后浏览器显示Notification
并且该实例有自动重连的实现,心跳机制使用SockJS内部实现支持
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| <template> <div id="app"> <div>服务器推送,客户端显示Notification</div> <div>WebSocket连接状态:{{ connected }}</div> <div> <button type="button" :disabled="connected" @click="connect()"> 连接 </button> <button type="button" @click="disconnect()" :disabled="!connected"> 断开 </button> </div> </div> </template>
<script>
export default { name: "Demo3", data() { return { stompClient: "", connected: false, socketUrl: "http://localhost:8080/my-websocket", lockReconnect: false, } }, methods: { showNotification(info) { if (window.Notification) { var popNotice = function () { if (Notification.permission == "granted") { var notification = new Notification("Hi,你好", { body: info, icon: "https://pcoss.guan18.com/%E6%A9%98%E8%92%9C.jpeg", })
notification.onclick = function () { notification.close() } } }
if (Notification.permission == "granted") { popNotice() } else if (Notification.permission != "denied") { Notification.requestPermission(function (permission) { popNotice() }) } } else { alert("浏览器不支持Notification") } }, successCallback() { console.log('连接成功!') this.connected = true this.stompClient.subscribe("/topic/notification", (frame) => { this.showNotification(frame.body) }) }, reconnect() { if(this.lockReconnect) { return } this.lockReconnect = true const reconTimeout = setTimeout(() => { console.log('重连中...') this.lockReconnect = false this.socket = new SockJS(this.socketUrl) this.stompClient = Stomp.over(this.socket) this.stompClient.connect({}, (frame) => { clearTimeout(reconTimeout) this.successCallback() }, () => { this.connected = false this.connect() }) }, 5000) }, connect() { var socket = new SockJS(this.socketUrl) this.stompClient = Stomp.over(socket) this.stompClient.connect({}, this.successCallback, this.reconnect) }, disconnect() { if (this.stompClient != null) { this.stompClient.disconnect() }
this.connected = false }, }, } </script>
|
demo3演示

前端源码
后端源码