概述
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯,看下图:
首先我们来看个典型的 Websocket 握手
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
熟悉HTTP的童鞋可能发现了,这段类似HTTP协议的握手请求中,多了几个东西。我会顺便讲解下作用
Upgrade: websocket
Connection: Upgrade
这个就是Websocket的核心了,告诉 Apache 、 Nginx 等服务器:注意啦,我发起的是Websocket协议,快点帮我找到对应的助理处理~不是那个老土的HTTP。
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
首先, Sec-WebSocket-Key 是一个 Base64 encode 的值,这个是浏览器随机生成的,告诉服务器:泥煤,不要忽悠窝,我要验证尼是不是真的是Websocket助理。
然后, Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
最后, Sec-WebSocket-Version 是告诉服务器所使用的 Websocket Draft (协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。。不过现在还好,已经定下来啦大家都使用的一个东西 脱水: 服务员,我要的是13岁的噢→_→
然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~
Upgrade: websocket
Connection: Upgrade
依然是固定的,告诉客户端即将升级的是 Websocket 协议。
然后, Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key 。
后面的, Sec-WebSocket-Protocol 则是表示最终使用的协议。
tomcat代码实现
项目结构
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>websocket</artifactId>
<groupId>com.websocket</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>websocket-tomcat</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package com.ax.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();
}
}
websocket连接握手
package com.ax.config;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
public class WsHandshake extends ServerEndpointConfig.Configurator {
/**
* 握手,可以从session中获取属性,前提是先调用 com.ax.controller.LoginController#login(javax.servlet.http.HttpSession)
* @param sec
* @param request
* @param response
*/
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
Object httpSession = request.getHttpSession();
if (httpSession == null) {
return;
}
HttpSession session = (HttpSession) request.getHttpSession();
String username = (String) session.getAttribute("username");
if (StringUtils.isEmpty(username) || !"123456".equals(username)) {
System.out.println("用户已经登录");
}
}
}
简单的登录接口,把登录用户名放入session
package com.ax.controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
@RestController
public class LoginController {
@GetMapping("/login")
public String login(HttpSession httpSession, String username) {
if (!StringUtils.isEmpty(username)) {
httpSession.setAttribute("username", username);
return "success";
}
return "fail";
}
}
定时任务模拟推送消息,分别是广播推送和队列推送
package com.ax.service;
import com.ax.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class OrderTask {
@Autowired
private WebSocketServer webSocketServer;
@Scheduled(cron = "0/1 * * * * ?")
public void send() {
webSocketServer.sendAll("发送全部订单");
webSocketServer.sendToUser("123456", "发送单个订单");
}
}
WebSocketServer,连接路径指定了username和握手,username可以用来鉴权,添加了广播推送和队列推送两种常见的方式。
package com.ax.websocket;
import com.ax.config.WsHandshake;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint(value = "/hello/{username}", configurator = WsHandshake.class)
@Component
@Slf4j
public class WebSocketServer {
private static Map<String, Session> sessions = new ConcurrentHashMap<>();
private static Map<String, String> users = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("username") String username) {
log.info("建立连接, username: " + username);
sessions.put(session.getId(), session);
users.put(username, session.getId());
}
@OnClose
public void onClose(Session session) {
sessions.remove(session.getId());
for (Map.Entry<String, String> entry : users.entrySet()) {
if (entry.getValue().equals(session.getId())) {
users.remove(entry.getKey());
}
}
log.info("关闭连接");
}
@OnMessage
public void onMessage(Session session, String message) {
log.info("接收消息: " + message);
try {
session.getBasicRemote().sendText("hello, client");
} catch (IOException e) {
e.printStackTrace();
}
}
@OnError
public void onError(Session session, Throwable ex) {
log.error("发生异常: " + ex);
}
public void sendAll(String message) {
try {
for (Session session : sessions.values()) {
session.getBasicRemote().sendText(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void sendToUser(String username, String message) {
try {
String sessionId = users.get(username);
if (!StringUtils.isEmpty(sessionId)) {
sessions.get(sessionId).getBasicRemote().sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.ax;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>hello</h1>
<button id="btn">发送</button>
<a href="/login" onclick="return false">登录</a>
<script>
var ed = null;
if('WebSocket' in window){
ed = new WebSocket("ws://localhost:8080/hello/123456");
} else {
alert('Not support ed')
}
ed.onError = function(){
console.log('error')
};
ed.onOpen = function(event){
console.log('onOpen' + event)
}
ed.onMessage = function(event){
console.log('接收消息 ' + event)
}
ed.onClose = function(){
console.log('关闭连接')
}
var btn = document.getElementById('btn')
btn.onclick = function(){
ed.send('hello, server')
}
</script>
</body>
</html>
优缺点
优点
- 代码实现简单
缺点
- 比较原始,如tcp和http的关系,这个websocket就像tcp,很多功能需要自己实现
- 握手拦截缺少需要的方法和参数
- 心跳等功能需要自己实现
- 发布订阅功能需要自定义协议实现