整体介绍
WebSocket 是一种网络通信协议,它提供了一种在客户端和服务器之间进行的双向实时通信的技术。WebSocket 协议建立在 HTTP 协议之上,使用 ws://或 wss://作为 URI 方案,其中 wss 表示使用 SSL/TLS 加密的 WebSocket 链接。
本文中将通过以下几方面介绍:
- WebSocket 概述:使用 WebSocket 的原因、通信过程、底层原理。
- WebSocket 使用:Java 语言通过 SpringBoot 框架实现 WebSocket。
- WebSocket 实战:实现简单多人聊天功能,了解开发思路。
- WebSocket 场景:了解 WebSocket 实际开发中的使用场景。
WebSocket 概述
使用 WebSocket 的原因:WebSocket 提供了客户端和服务器之间进行双向实时通信。
不使用 WebSocket 技术,普通 B/S 架构的交互图如下:
通过交互图可以发现,客户端和服务器之间通信方向固定,HTTP 请求只能由客户端发起,服务器无法直接向浏览器发送请求。
如果面对三方应用间结果的及时通知可以采取轮询方式,但是这样的轮询,会导致浪费带宽,实时性差,服务器压力大。
为了避免轮询所带来的系列问题,采取 WebSocket 协议进行通信。
1. 介绍
WebSocket 协议的介绍:
- 在 2008 年提出,2011 年正式称为国际标准(RFC 6455)。
- 是一种单个 TCP 连接上进行全双工通信协议,解决 HTTP 协议的一些局限性。
- 是作为 HTML5 标准的一部分。
2. 特点
WebSocket 协议的特点:
- 双向通信:WebSocket 允许客户端和服务器之间进行双向实时通信,而不需要频繁发送 HTTP 请求。
- 实时性:WebSocket 协议支持实时数据传输,可以在短时间内发送和接收大量数据。
- 较少的资源消耗:与传统的 HTTP 请求相比,WebSocket 协议可以减少客户端和服务器之间的通信次数,从而降低网络延迟和资源消耗。
3. 通信过程
通信过程步骤简述:
步骤 1:浏览器发起 HTTP 请求,请求建立 WebSocket 连接
步骤 2:服务器响应同意协议更改
步骤 3:浏览器和服务器之间相互发送消息
通信过程示意图:
4. 底层原理
WebSocket 底层原理:
- WebSocket 协议在建立 TCP 协议基础上,所以服务器端也是容易实现,不同语言都有支持。
- TCP 协议是全双工协议,HTTP 协议是基于它,设计成了单向的。
- WebSocket 没有同源限制。同源是指 IP 和端口相同。
WebSocket 使用
通过 Spring 实现的 WebSocket 功能,进行实现业务功能。
1. 基于 Java 注解
WebSocket 服务端实现:
- 使用的注解有:@ServerEndpoint(监听连接)、@OnOpen(连接成功)、@OnClose(连接关闭)、@OnMessage(收到消息)。
- 配置类: 需要将 Spring 中的 ServerEndpointExporter 对象注入。
步骤1:搭建一个 SpringBoot 工程,在 pom.xml 文件中引入 websocket 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
步骤2:编写服务器端处理 WebSocket 请求类
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/myWs")
@Component
@Slf4j
public class WsServerEndport {
private static Map<String, Session> sessionMap = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session) {
sessionMap.put(session.getId(), session);
log.info("websocket is open: {}", session.getId());
}
@OnMessage
public String onMessage(String text) {
log.info("websocket is receive message:{}", text);
return "receive message: " + text;
}
@OnClose
public void onClose(Session session) {
sessionMap.remove(session.getId());
log.info("websocket is close");
}
//定时 服务器-> 客户端 发送消息
@Scheduled(fixedRate = 2000)
public void sendMsg() throws IOException {
for (String sessionId : sessionMap.keySet()) {
log.info("websocket is send message:{} ", sessionId);
sessionMap.get(sessionId).getBasicRemote().sendText("心跳");
}
}
}
步骤3: 由于测试中使用到了定时器注解@Scheduled,所以在程序启动类上声明开启定时任务
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);
}
}
步骤4: 注入 ServerEndpointExporter 对象
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 客户端实现:
步骤1: 创建 HTML 页面,编写 js 方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ws client</title>
</head>
<body>
<div id="wslog"></div>
<script>
let wslog = document.getElementById("wslog");
let ws = new WebSocket("ws://localhost:8080/myWs")
ws.onopen = function () {
ws.send("client");
}
ws.onmessage = function (message) {
wslog.append(message.data);
}
ws.onclose = function () {
ws.close();
}
ws.onerror = function (e) {
console.log(e)
}
</script>
</body>
</html>
2. 基于 Spring 框架实现
Spring 针对 WebScoket 定义了一些接口和抽象类:
- HttpSessionHandshakeInterceptor(抽象类):握手拦截器,在握手前后添加操作。
- AbstractWebSocketHandler(抽象类):websocket处理程序,监听连接前,连接中和连接后。
- WebSocketConfigurer(接口):配置程序,配置监听端口信息、握手拦截器、处理程序。
WebSocket 服务端实现:
步骤1: 搭建一个 SpringBoot 工程,在 pom.xml 文件中引入 websocket 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
步骤2: 编写服务器端 WebSocket 建立握手类
import lombok.extern.slf4j.Slf4j;
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.support.HttpSessionHandshakeInterceptor;
import java.util.Map;
@Component
@Slf4j
public class MyWsInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 拦截握手前:
log.info(request.getRemoteAddress().toString() + "开始握手");
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
// 拦截握手后:
log.info(request.getRemoteAddress().toString() + "握手完成");
super.afterHandshake(request, response, wsHandler, ex);
}
}
步骤3: 除WebSocket本身的Sesssion外,实际场景中需要额外信息,所以封装Session
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.web.socket.WebSocketSession;
@AllArgsConstructor
@Data
public class SessionBean {
private WebSocketSession webSocketSession;
private Integer clientId;
}
步骤4: 编写服务器端 WebSocket 处理方法类
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@Slf4j
public class MyWsHandler extends AbstractWebSocketHandler {
private static Map<String, SessionBean> sessionBeanMap;
private static AtomicInteger clientIdMaker;
static {
sessionBeanMap = new ConcurrentHashMap<>();
clientIdMaker = new AtomicInteger(0);
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
super.afterConnectionEstablished(session);
// 建立连接:
SessionBean sessionBean = new SessionBean(session, clientIdMaker.getAndIncrement());
sessionBeanMap.put(session.getId(), sessionBean);
log.info(sessionBeanMap.get(session.getId()).getClientId() + " : 打开连接");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
log.info(sessionBeanMap.get(session.getId()).getClientId() + " : " + message.getPayload());
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
super.handleTransportError(session, exception);
// 连接异常
if (session.isOpen()) {
session.close();
}
sessionBeanMap.remove(session.getId());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
super.afterConnectionClosed(session, status);
// 连接关闭
log.info(sessionBeanMap.get(session.getId()).getClientId() + " : 关闭连接");
if (session.isOpen()) {
session.close();
}
sessionBeanMap.remove(session.getId());
}
//定时发送任务
@Scheduled(fixedDelay = 2000)
public void sendMsg() throws IOException{
for (String key : sessionBeanMap.keySet()) {
sessionBeanMap.get(key).getWebSocketSession().sendMessage(new TextMessage("心跳"));
}
}
}
步骤5: 由于测试中使用到了定时器注解@Scheduled,所以在程序启动类上声明开启定时任务
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);
}
}
步骤6: 编写服务器端 WebSocket 配置类
import org.springframework.stereotype.Component;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import javax.annotation.Resource;
@Component
@EnableWebSocket
public class MyWsConfig implements WebSocketConfigurer {
@Resource
private MyWsHandler myWsHandler;
@Resource
private MyWsInterceptor myWsInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myWsHandler, "/myWs1").addInterceptors(myWsInterceptor).setAllowedOrigins("*");
}
}
WebSocket 客户端实现:
步骤1: 创建 HTML 页面,编写 js 方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ws client</title>
</head>
<body>
<div id="wslog"></div>
<script>
let wslog = document.getElementById("wslog");
let ws = new WebSocket("ws://localhost:8080/myWs1")
ws.onopen = function () {
ws.send("client");
}
ws.onmessage = function (message) {
wslog.append(message.data);
}
ws.onclose = function () {
ws.close();
}
ws.onerror = function (e) {
console.log(e)
}
</script>
</body>
</html>
WebSocket 实战
实现多人群聊功能,基于 Spring 框架实现的代码进行改造
WebSocket 服务端改造:
步骤1: 改造服务器端 WebSocket 处理方法类
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@Slf4j
public class MyWsHandler extends AbstractWebSocketHandler {
private static Map<String, SessionBean> sessionBeanMap;
private static AtomicInteger clientIdMaker;
private static StringBuffer stringBuffer;
static {
sessionBeanMap = new ConcurrentHashMap<>();
clientIdMaker = new AtomicInteger(0);
stringBuffer = new StringBuffer();
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
super.afterConnectionEstablished(session);
// 建立连接:
SessionBean sessionBean = new SessionBean(session, clientIdMaker.getAndIncrement());
sessionBeanMap.put(session.getId(), sessionBean);
log.info(sessionBeanMap.get(session.getId()).getClientId() + " : 打开连接");
stringBuffer.append(sessionBeanMap.get(session.getId()).getClientId()).append("进入群聊<br/>");
sendAllMessage(sessionBeanMap);
}
private static void sendAllMessage(Map<String, SessionBean> sessionBeanMap) {
for (String key : sessionBeanMap.keySet()) {
try {
sessionBeanMap.get(key).getWebSocketSession().sendMessage(new TextMessage(stringBuffer.toString()));
} catch (IOException e) {
e.printStackTrace();
log.error(e.getMessage());
}
}
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
log.info(sessionBeanMap.get(session.getId()).getClientId() + " : " + message.getPayload());
stringBuffer.append(sessionBeanMap.get(session.getId()).getClientId()).append(message.getPayload()).append("<br/>");
sendAllMessage(sessionBeanMap);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
super.handleTransportError(session, exception);
// 连接异常
if (session.isOpen()) {
session.close();
}
sessionBeanMap.remove(session.getId());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
super.afterConnectionClosed(session, status);
int clientId = sessionBeanMap.get(session.getId()).getClientId();
// 连接关闭
log.info(sessionBeanMap.get(session.getId()).getClientId() + " : 关闭连接");
if (session.isOpen()) {
session.close();
}
sessionBeanMap.remove(session.getId());
stringBuffer.append(clientId).append("退出群聊<br/>");
sendAllMessage(sessionBeanMap);
}
// //定时发送任务
// @Scheduled(fixedDelay = 2000)
// public void sendMsg() throws IOException{
// for (String key : sessionBeanMap.keySet()) {
// sessionBeanMap.get(key).getWebSocketSession().sendMessage(new TextMessage("心跳"));
// }
// }
}
WebSocket 客户端改造:
步骤1: 改造 HTML 页面,编写 js 方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ws client</title>
</head>
<body>
<!-- <div id="wslog"></div> -->
<p style="border: 1px solid black; width: 600px; height: 400px;" id="talkMsg"></p>
<input type="text" id="message" /><button id="sendBtn" onclick="sendMsg()">发送</button>
<script>
let wslog = document.getElementById("wslog");
let ws = new WebSocket("ws://localhost:8080/myWs1")
ws.onopen = function () {
//ws.send("client");
}
ws.onmessage = function (message) {
// wslog.append(message.data);
document.getElementById("talkMsg").innerHTML = message.data;
}
function sendMsg() {
ws.send(document.getElementById("message").value);
document.getElementById("message").value = "";
}
ws.onclose = function () {
ws.close();
}
ws.onerror = function (e) {
console.log(e)
}
</script>
</body>
</html>
WebSocket 场景
应用多数是消息订阅
应用场景:
- 商城类的销售客服
- 管理类的人员沟通
- 视频的弹幕
- 股票基金类的数据
- 网页版的在线游戏
- 网站的总数统计
场景特点:
- 实时性要求高。
- 数据会收集到服务器端,从服务器端往客户端去发送。
评论区