目录
零、码仙励志
一、WebSocket概述
1.为什么要用WebSocket
2.什么是WebSocket
3.最低支持要求
二、Tomcat实现WebSocket
1.简单实现
2.连接的时候传入参数
3.群聊演示
4.单聊演示
三、SpringBoot整合WebSocket
1.基于注解的方式
1.群聊演示
2.单聊演示
2.基于实现类的方式
1.简单实现
2.连接的时候传入参数
3.群聊演示
4.单聊演示
5.SockJs
3.基于STOMP协议的方式
1.后端代码
2.前端代码
四、参考文献
失败的是事,绝不应是人。
传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。
Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。
这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,已被 W3C 定为标准。使用WebSocket 可以使得客户端和服务器之间的数据交换变得更加简单,它允许服务端主动向客户端推送数据。在 WebSocket 协议中,浏览器和服务器只需要完成一次握手,两者之间就可以直接创建持久性的连接,并进行双向数据传输。
WebSocket 使用了 HTTP/1.1 的协议升级特性,一个 WebSocket 请求首先使用非正常的 HTTP 请求以特定的模式访问一个 URL ,这个 URL 有两种模式,分别是 ws 和 wss ,对应 HTTP 协议中的 HTTP和HTTPS ,在请求头中有一个 Connection:Upgrade 字段,表示客户端想要对协议进行升级,另外还有一个 Upgrade:websocket 字段,表示客户端想要将请求协议升级为 WebSocket 协议。 这两个字段共同告诉服务器要将连接升级为 WebSocket 这样 一种全双工协议,如果服务端同意协议升级,那么在握手完成之后,文本消息或者其他二进制消息就可以同时在两个方向上进行发送,而不需要关闭和重建连接。 此时的客户端和服务端关系是对等的,它们可以互相向对方主动发送消 息。和传统的解决方案相比, WebSocket 主要有如下特点:
WebSocket 使用时需要先创建连接,这使得 WebSocket 成为一种有状态的协议,在之后的通信过程中可以省略部分状态信息(例如身份认证等)。
WebSocket 连接在端口 80 ( WS )或者 443 ( wss )上创建,与 HTTP 使用的端口相同,这样,基本上所有的防火墙都不会阻止 WebSocket 连接。
WebSocket 使用 HTTP 协议进行握手,因此它可以自然而然地集成到网络浏览器和 HTTP 服务器中,而不需要额外的成本。
心跳消息(ping 和 pong)将被反复的发送,进而保持 WebSocket 连接一直处于活跃状态。
使用该协议,当消息启动或者到达的时候,服务端和客户端都可以知道。
WebSocket 连接关闭时将发送一个特殊的关闭消息。
WebSocket 支持跨域,可以避免 Ajax的限制。
HTTP 规范要求浏览器将并发连接数限制为每个主机名两个连接,但是当我们使用 WebSocket 的时候,当握手完成之后,该限制就不存在了,因为此时的连接已经不再是 HTTP 连接了。
WebSocket 协议支持扩展,用户可以扩展协议,实现部分自定义的子协议。
更好的二进制支持以及更好的压缩效果。
WebSocket 既然具有这么多优势,使用场景当然也是非常广泛的,例如:
在线股票网站。
即时聊天。
多人在线游戏。
应用集群通信。
系统性能实时监控。
浏览器支持:所有的最新浏览器支持最新WebSocket规范(RFC 6455) ,从维基百科上介绍浏览器对WebSocket的支持如下表所示:
浏览器 | Chrome | Edge | Firfox | IE | Opera | Safari |
---|---|---|---|---|---|---|
最低版本 | 16 | 支持 | 11.0 | 10 | 12.10 | 6.0 |
移动端支持:移动端基本都支持websocket了,其实和浏览器版支持的版本一样,具体支持如下所示:
最低 | android浏览器 | Chrome 移动版 | Firfox 移动版 | Opera 移动版 | Safari IOS版 |
---|---|---|---|---|---|
最低版本 | 4.4 | 16 | 11.0 | 12.10 | 6.0 |
服务器支持:目前主流的web服务器都已经支持,具体版本如下表所示:
厂商 | 应用服务器 | 备注 |
---|---|---|
IBM | WebSphere | WebSphere 8.0 以上版本支持,7.X 之前版本结合 MQTT 支持类似的 HTTP 长连接 |
甲骨文 | WebLogic | WebLogic 12c 支持,11g 及 10g 版本通过 HTTP Publish 支持类似的 HTTP 长连接 |
微软 | IIS | IIS 7.0+支持 |
Apache | Tomcat | Tomcat 7.0.5+支持,7.0.2X 及 7.0.3X 通过自定义 API 支持 |
Apache | Jetty | Jetty 7.0+支持 |
引入websocket依赖
javax.websocket
javax.websocket-api
1.1
provided
建立一个用来跳转连接的页面
index.html
Title
Easy
EasyPar
GroupChat
SingleChat
服务端代码(Easy.java)
package com.mahaiwuji.socket;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
@ServerEndpoint("/easy")
public class Easy {
/**
* 连接时执行
*
* @param session
*/
@OnOpen
public void onOPen(Session session) {
System.out.println("连接成功");
}
/**
* 收到消息时执行
*
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
System.out.println(message);
session.getBasicRemote().sendText("收到消息");
}
/**
* 关闭时执行
*/
@OnClose
public void onClose(Session session) {
System.out.println("连接关闭");
}
/**
* 连接错误时执行
*
* @param session
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
}
}
客户端代码(easy.html)
Title
Easy
消息:
服务端代码(EasyPar.java)
package com.mahaiwuji.socket;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
@ServerEndpoint("/easyPar/{username}/{sex}")
public class EasyPar {
/**
* 连接时执行
*
* @param session
*/
@OnOpen
public void onOPen(@PathParam("username") String username, @PathParam("sex") String sex, Session session) {
System.out.println("连接成功");
System.out.println(username);
System.out.println(sex);
}
/**
* 收到消息时执行
*
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
System.out.println(message);
session.getBasicRemote().sendText("收到消息");
}
/**
* 关闭时执行
*/
@OnClose
public void onClose(Session session) {
System.out.println("连接关闭");
}
/**
* 连接错误时执行
*
* @param session
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
}
}
客户端代码(easyPar.html)
Title
EasyPar
消息:
服务端代码(GroupChat.java)
package com.mahaiwuji.socket;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@ServerEndpoint("/groupChat/{username}")
public class GroupChat {
// 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
// concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static final Map users = new HashMap();
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private String username;
/**
* 连接时执行
*
* @param username
* @param session
*/
@OnOpen
public void onOPen(@PathParam("username") String username, Session session) {
this.username = username;
this.session = session;
users.put(username, session); // 加入Map中
System.out.println("用户登录:" + username);
addOnlineCount(); // 在线数加1
System.out.println("当前在线人数:" + onlineCount);
}
/**
* 收到消息时执行
*
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
boolean flag = sendMessageToAllUsers(username + ":" + message);
if (flag) {
System.out.println("群发成功");
}
}
/**
* 关闭时执行
*
* @param session
*/
@OnClose
public void onClose(Session session) {
users.remove(username); // 从Map中删除
System.out.println("用户退出:" + username);
subOnlineCount(); // 在线数减1
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 连接错误时执行
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
GroupChat.onlineCount++;
}
public static synchronized void subOnlineCount() {
GroupChat.onlineCount--;
}
/**
* 群发
*
* @param message
* @return
*/
public boolean sendMessageToAllUsers(String message) {
boolean allSendSuccess = true;
Set usernames = users.keySet();
Session session = null;
for (String username : usernames) {
try {
session = users.get(username);
if (session.isOpen()) {
session.getBasicRemote().sendText(message);
} else {
System.out.println("客户端:" + username + ",已断开连接,发送消息失败");
}
} catch (IOException e) {
System.out.println("群发失败");
allSendSuccess = false;
}
}
return allSendSuccess;
}
}
客户端代码(groupchat.html)
Title
GroupChat
消息:
引入json依赖
com.alibaba
fastjson
1.2.47
服务端代码(SingleChat.java)
package com.mahaiwuji.socket;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@ServerEndpoint("/singleChat/{username}")
public class SingleChat {
// 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
// concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static final Map users = new HashMap();
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private String username;
/**
* 连接时执行
*
* @param username
* @param session
*/
@OnOpen
public void onOPen(@PathParam("username") String username, Session session) {
this.username = username;
this.session = session;
users.put(username, session); // 加入Map中
System.out.println("用户登录:" + username);
addOnlineCount(); // 在线数加1
System.out.println("当前在线人数:" + onlineCount);
}
/**
* 收到消息时执行
*
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println(message);
JSONObject parseObject = JSONObject.parseObject(message);
Object nameObject = parseObject.get("name");
Object msgObject = parseObject.get("msg");
String name = JSON.toJSONString(nameObject);
String msg = JSON.toJSONString(msgObject);
name = name.substring(1, name.length() - 1);
msg = msg.substring(1, msg.length() - 1);
System.out.println(name);
System.out.println(msg);
boolean flag = sendMessageToUser(name, username + ":" + msg);
if (flag) {
//发送成功以后让自己也能看到消息
sendMessageToUser(username, "我:" + msg);
} else {
//发送失败给发送者提示
sendMessageToUser(username, "给" + name + "发送消息失败");
}
}
/**
* 关闭时执行
*
* @param session
*/
@OnClose
public void onClose(Session session) {
users.remove(username); // 从Map中删除
System.out.println("用户退出:" + username);
subOnlineCount(); // 在线数减1
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 连接错误时执行
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
SingleChat.onlineCount++;
}
public static synchronized void subOnlineCount() {
SingleChat.onlineCount--;
}
/**
* 发送信息给指定用户
*
* @param username
* @param message
* @return
*/
public boolean sendMessageToUser(String username, String message) {
if (users.get(username) == null)
return false;
Session session = users.get(username);
if (!session.isOpen()) {
System.out.println("客户端:" + username + ",已断开连接,发送消息失败");
return false;
}
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
System.out.println("发送失败");
return false;
}
return true;
}
}
客户端代码(singlechat.html)
Title
SingleChat
对方用户名:
消息:
启动类
AnnotationSocketApplication.java
package com.mahaiwuji;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AnnotationSocketApplication {
public static void main(String[] args) {
SpringApplication.run(AnnotationSocketApplication.class, args);
}
}
引入websocket依赖
org.springframework.boot
spring-boot-starter-websocket
创建节点配置类
SocketConfig.java
package com.mahaiwuji.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class SocketConfig {
//这个bean的注册,用于扫描带有@ServerEndpoint的注解成为websocket,如果你使用外置的tomcat就不需要该配置文件
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
建立一个用来跳转连接的页面
index.html
Title
GroupChat
SingleChat
服务端代码(GroupChat.java)
package com.mahaiwuji.socket;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Component
@ServerEndpoint("/groupChat/{username}")
public class GroupChat {
// 用来存放每个客户端对应的Session对象。
private static final Map users = new HashMap();
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private String username;
/**
* 连接时执行
*
* @param username
* @param session
*/
@OnOpen
public void onOPen(@PathParam("username") String username, Session session) {
this.username = username;
this.session = session;
users.put(username, session); // 加入Map中
System.out.println("用户登录:" + username);
System.out.println("当前在线人数:" + users.size());
}
/**
* 收到消息时执行
*
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
boolean flag = sendMessageToAllUsers(username + ":" + message);
if (flag) {
System.out.println("群发成功");
}
}
/**
* 关闭时执行
*
* @param session
*/
@OnClose
public void onClose(Session session) {
users.remove(username); // 从Map中删除
System.out.println("用户退出:" + username);
System.out.println("有一连接关闭!当前在线人数为" + users.size());
}
/**
* 连接错误时执行
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
/**
* 群发
*
* @param message
* @return
*/
public boolean sendMessageToAllUsers(String message) {
boolean allSendSuccess = true;
Set usernames = users.keySet();
Session session = null;
for (String username : usernames) {
try {
session = users.get(username);
if (session.isOpen()) {
session.getBasicRemote().sendText(message);
} else {
System.out.println("客户端:" + username + ",已断开连接,发送消息失败");
}
} catch (IOException e) {
System.out.println("群发失败");
allSendSuccess = false;
}
}
return allSendSuccess;
}
}
客户端代码(groupchat.html)
Title
GroupChat
消息:
引入json依赖
com.alibaba
fastjson
1.2.47
服务端代码(SingleChat.java)
package com.mahaiwuji.socket;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
@ServerEndpoint("/singleChat/{username}")
public class SingleChat {
// 用来存放每个客户端对应的Session对象。
private static final Map users = new HashMap();
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private String username;
/**
* 连接时执行
*
* @param username
* @param session
*/
@OnOpen
public void onOPen(@PathParam("username") String username, Session session) {
this.username = username;
this.session = session;
users.put(username, session); // 加入Map中
System.out.println("用户登录:" + username);
System.out.println("当前在线人数:" + users.size());
}
/**
* 收到消息时执行
*
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println(message);
JSONObject parseObject = JSONObject.parseObject(message);
Object nameObject = parseObject.get("name");
Object msgObject = parseObject.get("msg");
String name = JSON.toJSONString(nameObject);
String msg = JSON.toJSONString(msgObject);
name = name.substring(1, name.length() - 1);
msg = msg.substring(1, msg.length() - 1);
System.out.println(name);
System.out.println(msg);
boolean flag = sendMessageToUser(name, username + ":" + msg);
if (flag) {
//发送成功以后让自己也能看到消息
sendMessageToUser(username, "我:" + msg);
} else {
//发送失败给发送者提示
sendMessageToUser(username, "给" + name + "发送消息失败");
}
}
/**
* 关闭时执行
*
* @param session
*/
@OnClose
public void onClose(Session session) {
users.remove(username); // 从Map中删除
System.out.println("用户退出:" + username);
System.out.println("有一连接关闭!当前在线人数为" + users.size());
}
/**
* 连接错误时执行
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
/**
* 发送信息给指定用户
*
* @param username
* @param message
* @return
*/
public boolean sendMessageToUser(String username, String message) {
if (users.get(username) == null)
return false;
Session session = users.get(username);
if (!session.isOpen()) {
System.out.println("客户端:" + username + ",已断开连接,发送消息失败");
return false;
}
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
System.out.println("发送失败");
return false;
}
return true;
}
}
客户端代码(singlechat.html)
Title
SingleChat
对方用户名:
消息:
启动类
ImplementsSocketApplication.java
package com.mahaiwuji;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ImplementsSocketApplication {
public static void main(String[] args) {
SpringApplication.run(ImplementsSocketApplication.class, args);
}
}
引入websocket依赖
org.springframework.boot
spring-boot-starter-websocket
创建节点配置类
SocketConfig.java
package com.mahaiwuji.config;
import com.mahaiwuji.interceptor.*;
import com.mahaiwuji.socket.*;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* 通过实现WebSocketConfigurer接口,可以注册相应的WebSocket处理器、路径、允许域、SockJs支持。
*/
@Configuration
@EnableWebSocket
public class SocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
}
}
建立一个用来跳转连接的页面
index.html
Title
Easy
EasyPar
GroupChat
SingleChat
SockJs
服务端代码(Easy.java)
package com.mahaiwuji.socket;
import org.springframework.web.socket.*;
public class Easy implements WebSocketHandler {
/**
* 连接时执行
*
* @param webSocketSession
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
System.out.println("连接成功");
}
/**
* 收到消息时执行
*
* @param webSocketSession
* @param webSocketMessage
* @throws Exception
*/
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage> webSocketMessage) throws Exception {
// 获取beforeHandshake中map放入的值
String name = (String) webSocketSession.getAttributes().get("name");
System.out.println(name);
// 获取客户端发送的内容
System.out.println(webSocketMessage);
String text = (String) webSocketMessage.getPayload();
System.out.println(text);
// 给客户端发送消息
webSocketSession.sendMessage(new TextMessage("收到消息"));
}
/**
* 关闭时执行
*
* @param webSocketSession
* @param closeStatus
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
System.out.println("连接关闭");
}
/**
* 连接错误时执行
*
* @param webSocketSession
* @param throwable
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
System.out.println("发生错误");
}
/**
* 是否处理部分消息,在连接前执行
*
* @return
*/
@Override
public boolean supportsPartialMessages() {
System.out.println("supportsPartialMessages");
return false;
}
}
服务端拦截器代码(EasyInterceptor.java)
package com.mahaiwuji.interceptor;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
/**
* HandshakeInterceptor接口是WebSocket连接握手过程的拦截器,通过实现该接口可以对握手过程进行管理。
* 值得注意的是,beforeHandshake中的map与WebSocketSession中通过getAttributes();
* 返回的Map是同一个Map,我们可以在其中放入一些用户的特定信息。
*/
public class EasyInterceptor implements HandshakeInterceptor {
/**
* 握手前,在连接前执行,并且只在每次连接前执行
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param map
* @return
* @throws Exception
*/
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map map) throws Exception {
//这里给map中放入一个值,在WebSocketSession中获取
map.put("name", "码海无际");
System.out.println("握手前");
return true;
}
/**
* 握手后,也在连接前执行,并且只在每次连接前执行
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param e
*/
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
System.out.println("握手后");
}
}
服务端节点配置类SocketConfig.java
package com.mahaiwuji.config;
import com.mahaiwuji.interceptor.*;
import com.mahaiwuji.socket.*;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* 通过实现WebSocketConfigurer接口,可以注册相应的WebSocket处理器、路径、允许域、SockJs支持。
*/
@Configuration
@EnableWebSocket
public class SocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new Easy(), "/easy").addInterceptors(new EasyInterceptor()).setAllowedOrigins("*");
}
}
客户端代码(easy.html)
Title
Easy
消息:
服务端代码(EasyPar.java)
package com.mahaiwuji.socket;
import org.springframework.web.socket.*;
public class EasyPar implements WebSocketHandler {
/**
* 连接时执行
*
* @param webSocketSession
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
System.out.println("连接成功");
}
/**
* 收到消息时执行
*
* @param webSocketSession
* @param webSocketMessage
* @throws Exception
*/
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage> webSocketMessage) throws Exception {
// 获取beforeHandshake中map放入的值
String username = (String) webSocketSession.getAttributes().get("username");
System.out.println(username);
// 获取客户端发送的内容
System.out.println(webSocketMessage);
String text = (String) webSocketMessage.getPayload();
System.out.println(text);
// 给客户端发送消息
webSocketSession.sendMessage(new TextMessage("收到消息"));
}
/**
* 关闭时执行
*
* @param webSocketSession
* @param closeStatus
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
System.out.println("连接关闭");
}
/**
* 连接错误时执行
*
* @param webSocketSession
* @param throwable
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
System.out.println("发生错误");
}
/**
* 是否处理部分消息,在连接前执行
*
* @return
*/
@Override
public boolean supportsPartialMessages() {
System.out.println("supportsPartialMessages");
return false;
}
}
服务端拦截器代码(EasyParInterceptor.java)
package com.mahaiwuji.interceptor;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;
/**
* HandshakeInterceptor接口是WebSocket连接握手过程的拦截器,通过实现该接口可以对握手过程进行管理。
* 值得注意的是,beforeHandshake中的map与WebSocketSession中通过getAttributes();
* 返回的Map是同一个Map,我们可以在其中放入一些用户的特定信息。
*/
public class EasyParInterceptor implements HandshakeInterceptor {
/**
* 握手前,在连接前执行,并且只在每次连接前执行
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param map
* @return
* @throws Exception
*/
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map map) throws Exception {
System.out.println("握手前");
HttpServletRequest httpServletRequest = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
String username = httpServletRequest.getParameter("username");
System.out.println(username);
String sex = httpServletRequest.getParameter("sex");
System.out.println(sex);
//这里给map中放入一个值,在WebSocketSession中获取
map.put("username", username);
return true;
}
/**
* 握手后,也在连接前执行,并且只在每次连接前执行
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param e
*/
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
System.out.println("握手后");
}
}
服务端节点配置类SocketConfig.java
在registerWebSocketHandlers方法中添加以下代码:
registry.addHandler(new EasyPar(), "/easyPar").addInterceptors(new EasyParInterceptor()).setAllowedOrigins("*");
客户端代码(easyPar.html)
Title
EasyPar
消息:
服务端代码(GroupChat.java)
package com.mahaiwuji.socket;
import org.springframework.web.socket.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class GroupChat implements WebSocketHandler {
// 用来存放每个客户端对应的Session对象。
private static final Map users = new HashMap();
/**
* 连接时执行
*
* @param webSocketSession
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
System.out.println("连接成功");
String username = (String) webSocketSession.getAttributes().get("username");
users.put(username, webSocketSession);
System.out.println("登录用户:" + username);
System.out.println("当前在线人数:" + users.size());
}
/**
* 收到消息时执行
*
* @param webSocketSession
* @param webSocketMessage
* @throws Exception
*/
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage> webSocketMessage) throws Exception {
// 获取beforeHandshake中map放入的值
String username = (String) webSocketSession.getAttributes().get("username");
System.out.println("发送消息用户:" + username);
// 获取客户端发送的内容
System.out.println(webSocketMessage);
String message = (String) webSocketMessage.getPayload();
System.out.println(message);
boolean flag = sendMessageToAllUsers(username + ":" + message);
if (flag) {
System.out.println("群发成功");
}
}
/**
* 关闭时执行
*
* @param webSocketSession
* @param closeStatus
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
System.out.println("连接关闭");
String username = (String) webSocketSession.getAttributes().get("username");
users.remove(username);
System.out.println("退出用户:" + username);
System.out.println("当前在线人数:" + users.size());
}
/**
* 连接错误时执行
*
* @param webSocketSession
* @param throwable
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
System.out.println("发生错误");
}
/**
* 是否处理部分消息,在连接前执行
*
* @return
*/
@Override
public boolean supportsPartialMessages() {
System.out.println("supportsPartialMessages");
return false;
}
/**
* 群发
*
* @param message
* @return
*/
public boolean sendMessageToAllUsers(String message) {
boolean allSendSuccess = true;
Set usernames = users.keySet();
WebSocketSession session = null;
for (String username : usernames) {
try {
session = users.get(username);
if (session.isOpen()) {
session.sendMessage(new TextMessage(message));
} else {
System.out.println("客户端:" + username + ",已断开连接,发送消息失败");
}
} catch (IOException e) {
System.out.println("群发失败");
allSendSuccess = false;
}
}
return allSendSuccess;
}
}
服务端拦截器代码(GroupChatInterceptor.java)
package com.mahaiwuji.interceptor;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* HandshakeInterceptor接口是WebSocket连接握手过程的拦截器,通过实现该接口可以对握手过程进行管理。
* 值得注意的是,beforeHandshake中的map与WebSocketSession中通过getAttributes();
* 返回的Map是同一个Map,我们可以在其中放入一些用户的特定信息。
*/
public class GroupChatInterceptor implements HandshakeInterceptor {
/**
* 握手前,在连接前执行,并且只在每次连接前执行
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param map
* @return
* @throws Exception
*/
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map map) throws Exception {
System.out.println("握手前");
HttpServletRequest httpServletRequest = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
String username = httpServletRequest.getParameter("username");
System.out.println(username);
//这里给map中放入一个值,在WebSocketSession中获取
map.put("username", username);
return true;
}
/**
* 握手后,也在连接前执行,并且只在每次连接前执行
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param e
*/
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
System.out.println("握手后");
}
}
服务端节点配置类SocketConfig.java
在registerWebSocketHandlers方法中添加以下代码:
registry.addHandler(new GroupChat(), "/groupChat").addInterceptors(new GroupChatInterceptor()).setAllowedOrigins("*");
客户端代码(groupchat.html)
Title
GroupChat
消息:
引入json依赖
com.alibaba
fastjson
1.2.47
服务端代码(SingleChat.java)
package com.mahaiwuji.socket;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.socket.*;
import javax.websocket.Session;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class SingleChat implements WebSocketHandler {
// 用来存放每个客户端对应的Session对象。
private static final Map users = new HashMap();
/**
* 连接时执行
*
* @param webSocketSession
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
System.out.println("连接成功");
String username = (String) webSocketSession.getAttributes().get("username");
users.put(username, webSocketSession);
System.out.println("登录用户:" + username);
System.out.println("当前在线人数:" + users.size());
}
/**
* 收到消息时执行
*
* @param webSocketSession
* @param webSocketMessage
* @throws Exception
*/
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage> webSocketMessage) throws Exception {
// 获取beforeHandshake中map放入的值
String username = (String) webSocketSession.getAttributes().get("username");
System.out.println("发送消息用户:" + username);
// 获取客户端发送的内容
System.out.println(webSocketMessage);
String message = (String) webSocketMessage.getPayload();
System.out.println(message);
// 解析客户端发送的内容
JSONObject parseObject = JSONObject.parseObject(message);
Object nameObject = parseObject.get("name");
Object msgObject = parseObject.get("msg");
String name = JSON.toJSONString(nameObject);
String msg = JSON.toJSONString(msgObject);
name = name.substring(1, name.length() - 1);
msg = msg.substring(1, msg.length() - 1);
System.out.println(name);
System.out.println(msg);
boolean flag = sendMessageToUser(name, username + ":" + msg);
if (flag) {
//发送成功以后让自己也能看到消息
sendMessageToUser(username, "我:" + msg);
} else {
//发送失败给发送者提示
sendMessageToUser(username, "给" + name + "发送消息失败");
}
}
/**
* 关闭时执行
*
* @param webSocketSession
* @param closeStatus
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
System.out.println("连接关闭");
String username = (String) webSocketSession.getAttributes().get("username");
users.remove(username);
System.out.println("退出用户:" + username);
System.out.println("当前在线人数:" + users.size());
}
/**
* 连接错误时执行
*
* @param webSocketSession
* @param throwable
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
System.out.println("发生错误");
}
/**
* 是否处理部分消息,在连接前执行
*
* @return
*/
@Override
public boolean supportsPartialMessages() {
System.out.println("supportsPartialMessages");
return false;
}
/**
* 发送信息给指定用户
*
* @param username
* @param message
* @return
*/
public boolean sendMessageToUser(String username, String message) {
if (users.get(username) == null)
return false;
WebSocketSession session = users.get(username);
if (!session.isOpen()) {
System.out.println("客户端:" + username + ",已断开连接,发送消息失败");
return false;
}
try {
session.sendMessage(new TextMessage(message));
} catch (IOException e) {
System.out.println("发送失败");
return false;
}
return true;
}
}
服务端拦截器代码(SingleChatInterceptor.java)
package com.mahaiwuji.interceptor;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* HandshakeInterceptor接口是WebSocket连接握手过程的拦截器,通过实现该接口可以对握手过程进行管理。
* 值得注意的是,beforeHandshake中的map与WebSocketSession中通过getAttributes();
* 返回的Map是同一个Map,我们可以在其中放入一些用户的特定信息。
*/
public class SingleChatInterceptor implements HandshakeInterceptor {
/**
* 握手前,在连接前执行,并且只在每次连接前执行
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param map
* @return
* @throws Exception
*/
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map map) throws Exception {
System.out.println("握手前");
HttpServletRequest httpServletRequest = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
String username = httpServletRequest.getParameter("username");
System.out.println(username);
//这里给map中放入一个值,在WebSocketSession中获取
map.put("username", username);
return true;
}
/**
* 握手后,也在连接前执行,并且只在每次连接前执行
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param e
*/
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
System.out.println("握手后");
}
}
服务端节点配置类SocketConfig.java
在registerWebSocketHandlers方法中添加以下代码:
registry.addHandler(new SingleChat(), "/singleChat").addInterceptors(new SingleChatInterceptor()).setAllowedOrigins("*");
客户端代码(singlechat.html)
Title
SingleChat
对方用户名:
消息:
什么是SockJs:有一些浏览器中缺少对WebSocket的支持,而SockJS是一个浏览器的JavaScript库,它提供了一个类似于网络的对象,SockJS提供了一个连贯的,跨浏览器的JavaScriptAPI,它在浏览器和Web服务器之间创建了一个低延迟、全双工、跨域通信通道。SockJS的一大好处在于提供了浏览器兼容性。即优先使用原生WebSocket,如果浏览器不支持WebSocket,会自动降为轮询的方式。
服务端代码(SockJs.java)
package com.mahaiwuji.socket;
import org.springframework.web.socket.*;
public class SockJs implements WebSocketHandler {
/**
* 连接时执行
*
* @param webSocketSession
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
System.out.println("连接成功");
}
/**
* 收到消息时执行
*
* @param webSocketSession
* @param webSocketMessage
* @throws Exception
*/
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage> webSocketMessage) throws Exception {
// 获取beforeHandshake中map放入的值
String name = (String) webSocketSession.getAttributes().get("name");
System.out.println(name);
// 获取客户端发送的内容
System.out.println(webSocketMessage);
String text = (String) webSocketMessage.getPayload();
System.out.println(text);
// 给客户端发送消息
webSocketSession.sendMessage(new TextMessage("收到消息la"));
}
/**
* 关闭时执行
*
* @param webSocketSession
* @param closeStatus
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
System.out.println("连接关闭");
}
/**
* 连接错误时执行
*
* @param webSocketSession
* @param throwable
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
System.out.println("发生错误");
}
/**
* 是否处理部分消息,在连接前执行
*
* @return
*/
@Override
public boolean supportsPartialMessages() {
System.out.println("supportsPartialMessages");
return false;
}
}
服务端拦截器代码(SockJsInterceptor.java)
package com.mahaiwuji.interceptor;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
/**
* HandshakeInterceptor接口是WebSocket连接握手过程的拦截器,通过实现该接口可以对握手过程进行管理。
* 值得注意的是,beforeHandshake中的map与WebSocketSession中通过getAttributes();
* 返回的Map是同一个Map,我们可以在其中放入一些用户的特定信息。
*/
public class SockJsInterceptor implements HandshakeInterceptor {
/**
* 握手前,在连接前执行,并且只在每次连接前执行
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param map
* @return
* @throws Exception
*/
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map map) throws Exception {
//这里给map中放入一个值,在WebSocketSession中获取
map.put("name", "码海无际");
System.out.println("握手前");
return true;
}
/**
* 握手后,也在连接前执行,并且只在每次连接前执行
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param
*/
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
System.out.println("握手后");
}
}
服务端节点配置类SocketConfig.java
在registerWebSocketHandlers方法中添加以下代码:
registry.addHandler(new SockJs(), "/sockJs").addInterceptors(new SockJsInterceptor()).setAllowedOrigins("*");
registry.addHandler(new SockJs(), "/sockJs").addInterceptors(new SockJsInterceptor()).setAllowedOrigins("*").withSockJS();
客户端代码(sockjs.html)
Title
SockJs
消息:
启动类
StompSocketApplication.java
package com.mahaiwuji;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StompSocketApplication {
public static void main(String[] args) {
SpringApplication.run(StompSocketApplication.class, args);
}
}
引入websocket依赖
org.springframework.boot
spring-boot-starter-websocket
创建节点配置类
WebSocketConfig.java
package com.mahaiwuji.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker //注解开启STOMP协议来传输基于代理的消息,此时控制器支持使用@MessageMapping
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
//groupChatContent用来群聊,user用来单聊,单聊必须用user
config.enableSimpleBroker("/groupChatContent", "/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//用于群聊连接
registry.addEndpoint("/groupChat").withSockJS();
//用于群聊连接
registry.addEndpoint("/singleChat").withSockJS();
}
}
创建实体类,用来传输数据
Chat.java
package com.mahaiwuji.pojo;
public class Chat {
// 发送消息的用户名
private String toUsername;
// 接收消息的用户名
private String fromUsername;
// 发送的内容
private String content;
public String getToUsername() {
return toUsername;
}
public void setToUsername(String toUsername) {
this.toUsername = toUsername;
}
public String getFromUsername() {
return fromUsername;
}
public void setFromUsername(String fromUsername) {
this.fromUsername = fromUsername;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "Chat{" +
"toUsername='" + toUsername + '\'' +
", fromUsername='" + fromUsername + '\'' +
", content='" + content + '\'' +
'}';
}
}
创建与前端页面交互的Controller
ChatController.java
package com.mahaiwuji.controller;
import com.mahaiwuji.pojo.Chat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
@Autowired
public SimpMessagingTemplate template;
/**
* 群聊
*
* @param chat
*/
@MessageMapping("/sendGroupChat")
public void sendGroupChat(Chat chat) {
System.out.println(chat);
//群聊使用convertAndSend方法,第一个参数为目的地,和js中订阅的目的地要一致
template.convertAndSend("/groupChatContent", chat.getToUsername() + ":" + chat.getContent());
}
@MessageMapping("/sendSingleChat")
public void sendSingleChat(Chat chat) {
System.out.println(chat);
// 单聊使用convertAndSendToUser方法
// 第一个参数为用户id
// 此时js中的订阅地址为"/user/" + 接收消息用户 + "/singleChatContent",其中"/user"是固定的
// convertAndSendToUser底层调用了convertAndSend
template.convertAndSendToUser(chat.getFromUsername(), "/singleChatContent", chat.getToUsername() + ":" + chat.getContent());
//让自己也能看到消息
template.convertAndSendToUser(chat.getToUsername(), "/singleChatContent", "我:" + chat.getContent());
}
}
建立一个用来跳转连接的页面
index.html
Title
GroupChat
SingleChat
群聊页面
groupchat.html
Title
GroupChat
消息:
单聊页面
singlechat.html
Title
SingleChat
对方用户名:
消息:
https://www.cnblogs.com/onlymate/p/9521327.html
https://www.jianshu.com/p/d79bf8174196
https://blog.csdn.net/huiyunfei/article/details/90719351
https://www.cnblogs.com/hhhshct/p/8849449.html
https://www.cnblogs.com/jmcui/p/8999998.html
https://blog.csdn.net/qq_35387940/article/details/93483678
https://www.jianshu.com/p/4ce6cb1310e6
《Spring Boot Vue全栈开发实战》- 王松