WebSocket是一种在TCP
连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
WebSocket的实现方式有很多所以网上的文章都有些不一样,推荐使用第一种和第二种
1.pom先导入依赖
org.springframework.boot
spring-boot-starter-websocket
2.创建WebSocket配置类,通过这个配置才能去扫描WebSocket的注解
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @Description WebSocket配置类
*/
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.创建WebSocket服务
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
/**
* @Description WebSocket服务
*/
@Component
@ServerEndpoint("/websocket/{userId}") // 接口路径 ws://localhost:8080/websocket/1
public class WebSocket {
private static final Logger log = LoggerFactory.getLogger(WebSocket.class);
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 用户ID
*/
private String userId;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
//虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
// 注:底下WebSocket是当前类名
private static final CopyOnWriteArraySet webSockets = new CopyOnWriteArraySet<>();
// 用来存在线连接用户信息
private static final ConcurrentHashMap sessionPool = new ConcurrentHashMap();
/**
* 链接成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId) throws InterruptedException {
try {
this.session = session;
this.userId = userId;
webSockets.add(this);
sessionPool.put(userId, session);
log.info(session.getId());
log.info("【websocket消息】有新的连接,总数为:" + webSockets.size());
} catch (Exception e) {
e.printStackTrace();
}
//响应消息
session.getAsyncRemote().sendText("连接成功");
}
}
/**
* 链接关闭调用的方法
*/
@OnClose
public void onClose() {
try {
webSockets.remove(this);
sessionPool.remove(this.userId);
log.info("【websocket消息】连接断开,总数为:" + webSockets.size());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message
*/
@OnMessage
public void onMessage(String message) {
log.info("【websocket消息】收到客户端消息:" + message);
}
/**
* 发送错误时的处理
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误,原因:" + error.getMessage());
error.printStackTrace();
}
// 此为广播消息
public void sendAllMessage(String message) {
log.info("【websocket消息】广播消息:" + message);
for (WebSocket webSocket : webSockets) {
try {
if (webSocket.session.isOpen()) {
webSocket.session.getAsyncRemote().sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 此为单点消息
public void sendOneMessage(String userId, String message) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:" + message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 此为单点消息(多人)
public void sendMoreMessage(String[] userIds, String message) {
for (String userId : userIds) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:" + message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// 调用 :
//@Resource
//private WebSocket webSocket;
public void sendTest() {
//创建业务消息信息
JSONObject obj = new JSONObject();
obj.put("cmd", "topic");//业务类型
obj.put("msgId", "messageid");//消息id
obj.put("msgTxt", "内容");//消息内容
//全体发送
sendAllMessage(obj.toJSONString());
//单个用户发送 (userId为用户id)
sendOneMessage(userId, obj.toJSONString());
//多个用户发送 (userIds为多个用户id,逗号‘,’分隔)
sendMoreMessage(new String[]{"1"}, obj.toJSONString());
}
}
前端调用接口路径 ws://localhost:8080/websocket/1
1.在项目的pom.xml文件中,添加以下依赖:
org.springframework.boot
spring-boot-starter-websocket
2.创建一个WebSocket配置类,用于配置WebSocket相关的拦截路径参数和处理器。
@Configuration
@EnableWebSocket //启用WebSocket支持
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//auth用于鉴权处理 openid用户的id
registry.addHandler(webSocketHandler(),"/netgate/{auth}/{openid}") //注册Handler
.addInterceptors(new WebSocketHandshakeInterceptor()) //握手过滤器注册Interceptor
.setAllowedOrigins("*"); //关闭跨域校验
//注册SockJS,提供SockJS支持(主要是兼容ie8)
registry.addHandler(myHandler(), "/my-websocket-url") //注册Handler
.withSockJS(); //支持sockjs协议
}
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new
ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(2*1024*1024);//8192*1024 1024*1024*1024
container.setMaxBinaryMessageBufferSize(2*1024*1024);
container.setAsyncSendTimeout(55000l);
container.setMaxSessionIdleTimeout(55000l);//心跳
return container;
}
@Bean
public TextWebSocketHandler webSocketHandler() {
return new NetgateHandler();
}
@Bean
public WebSocketHandler myHandler() {
return new MyWebSocketHandler();
}
}
3.Websocket握手过滤器
public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {
private final static Logger LOGGER = LoggerFactory.getLogger(WebSocketHandshakeInterceptor.class);
/**
* 握手前
*/
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
String path = request.getURI().getPath();
if(requestIsValid(path)){
String[] params = getParams(path);
attributes.put("WEBSOCKET_AUTH", params[0]);
attributes.put("WEBSOCKET_OPENID", params[1]);
attributes.put("WEBSOCKET_FIRSTONE","yes");
}
}
System.out.println("================Before Handshake================");
return true;
}
/**
* 握手后
*/
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
System.out.println("================After Handshake================");
if(e!=null) e.printStackTrace();
System.out.println("================After Handshake================");
}
private boolean requestIsValid(String url){
//在这里可以写上具体的鉴权逻辑
boolean isvalid = false;
if(StringUtils.isNotEmpty(url)
&& url.startsWith("/netgate/")
&& url.split("/").length==6){
isvalid = true;
}
return isvalid;
}
private String[] getParams(String url){
url = url.replace("/netgate/","");
return url.split("/");
}
}
4.创建一个WebSocket处理器类,用于处理WebSocket的连接、消息和事件,有两种接口实现方式。
TextWebSocketHandler:文本内容
BinaryWebSocketHandler:二进制内容
/**
* Websocket处理器
*/
@Component
public class NetgateHandler extends TextWebSocketHandler {
@Autowired
private MqttGateway mqttGateway;
/*
* 网关连接集合
* 第一级:设备序列号 sn
* 第二级:用户唯一标识 openid
*/
private static ConcurrentHashMap> netgates = new ConcurrentHashMap>();
/**
* 处理前端发送的文本信息
* js调用websocket.send时候,会调用该方法
* WebSocketSession代表每个客户端会话
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
if(!session.isOpen()) {
System.out.println("连接已关闭,不再处理该连接的消息!");
return;
}
String mes = ObjectUtils.toString(message.getPayload(),"");
String pid = session.getAttributes().get("WEBSOCKET_PID").toString();
String sn = session.getAttributes().get("WEBSOCKET_SN").toString();
if(message==null || "".equals(mes)){
System.out.println(getSysDate()+"============接收到空消息,不予处理。");
}else if(mes.length()==1){
//心跳消息过滤掉
return;
}else {
//转发成mqtt消息
String topic = "pay/"+pid+"/server/"+sn;
System.out.println(topic);
System.out.println(getSysDate()+"============消息处理完成:"+mes);
mqttGateway.sendToMqtt(topic,mes);
}
}
/**
* 当新连接建立的时候,被调用
* 连接成功时候,会触发页面上onOpen方法
*
* @param session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println(getSysDate()+"============正在初始化连接:"+session.getId());
try {
//初始化连接,把session存储起来
this.initUsers(session);
} catch (Exception e) {
System.out.println(getSysDate()+"============初始化连接异常-开始:"+e.getMessage());
e.printStackTrace();
System.out.println(getSysDate()+"============初始化连接异常-结束:"+e.getMessage());
}
System.out.println(getSysDate()+"============初始化连接完成:"+session.getId());
}
/**
* 当连接关闭时被调用
*
* @param session
* @param status
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println(getSysDate()+"============正在关闭连接:"+session.getId()+",isOpen:"+session.isOpen()+";code:"+status.getCode());
try {
System.out.println("断开连接状态值"+status.getCode());
this.removeSession(session);
} catch (Exception e) {
System.out.println(getSysDate()+"============关闭连接异常-开始:"+e.getMessage());
e.printStackTrace();
System.out.println(getSysDate()+"============关闭连接异常-结束:"+e.getMessage());
}
System.out.println(getSysDate()+"============正在关闭完成:"+session.getId()+",isOpen:"+session.isOpen()+";code:"+status.getCode());
}
/**
* 传输错误时调用
*
* @param session
* @param exception
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
System.out.println(getSysDate()+"============发生传输错误:"+session.getId()+";session.isOpen():"+session.isOpen()+";exception:"+exception.getMessage());
exception.printStackTrace();
if (session.isOpen()) {
//try { session.close(); } catch (Exception e) {e.printStackTrace();}
}else {
try {
this.removeSession(session);
} catch (Exception e) {
System.out.println(getSysDate()+"============传输错误处理异常-开始:"+e.getMessage());
e.printStackTrace();
System.out.println(getSysDate()+"============传输错误处理异常-结束:"+e.getMessage());
}
}
System.out.println(getSysDate()+"============错误处理结束:"+session.getId()+";session.isOpen():"+session.isOpen()+";exception:"+exception.getMessage());
}
public synchronized void sendMsgToNetgateSn(String sn, String msg) {
if(netgates.size()>0 && netgates.containsKey(sn) && !netgates.get(sn).isEmpty()){
//获取EID对应的后台管理连接 多个
for (WebSocketSession ws: netgates.get(sn).values()){
System.out.println("对网关指令开始发送啦:sn="+sn+"消息内容"+msg);
try {ws.sendMessage(new TextMessage(msg));} catch (IOException e) {System.out.println(getSysDate()+"发生了异常:"+e.getMessage());e.printStackTrace();continue;}
}
}
}
//连接接入的处理方法
private synchronized void initUsers(WebSocketSession session){
String pid = (String) session.getAttributes().get("WEBSOCKET_PID");
String sn = (String) session.getAttributes().get("WEBSOCKET_SN");
String openid = (String) session.getAttributes().get("WEBSOCKET_OPENID");
if(StringUtils.isNotEmpty(pid) && StringUtils.isNotEmpty(sn) && StringUtils.isNotEmpty(openid)){
ConcurrentHashMap netgate = netgates.get(sn);
if(netgate == null){
netgate = new ConcurrentHashMap();
}
WebSocketSession session_exist = netgate.get(sn);
if(session_exist!=null) {
System.out.println("检测到相同SN重复连接,SN:"+sn+",连接ID:"+session_exist.getId()+",准备清理失效的连接。。。");
try {session_exist.close();} catch (IOException e) {e.printStackTrace();}
}
netgate.putIfAbsent(openid, session);
netgates.put(sn,netgate);
}
}
//连接被关闭时处理集合
private synchronized void removeSession(WebSocketSession session){
String sn = (String) session.getAttributes().get("WEBSOCKET_SN");
String openid = (String) session.getAttributes().get("WEBSOCKET_OPENID");
if(netgates.get(sn).containsKey(openid)) {
WebSocketSession exist_session = netgates.get(sn).get(openid);
//确保是同一个session 不是同一个session则不应该进行下一步的处理
if(exist_session.getId()!=null && exist_session.getId().equals(session.getId())) {
netgates.get(sn).remove(openid);
System.out.println("有一网关连接关闭!SN:"+sn+",当前在线数量为"+netgates.get(sn).keySet().size());
}else {
System.out.println("检测到关闭session异常,程序中断处理,关闭sessionId:"+session.getId()+",当前实际sessionId:"+exist_session.getId());
}
}else {
System.out.println("检测到关闭session异常,程序中断处理,系统中未找到对应的session,Sn="+sn+"openid="+openid);
}
}
private String getSysDate() {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
return df.format(new Date());
}
}
public class MyWebSocketHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 在WebSocket连接建立时执行的逻辑
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 处理接收到的文本消息
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 处理传输错误事件
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 在WebSocket连接关闭时执行的逻辑
}
}
5.Controller中可以通过@Autowired注入WebSocketHandler,并在方法中调用处理器的方法来与WebSocket进行交互。
1.引入pom依赖配置application
org.t-io
tio-websocket-spring-boot-starter
3.5.5.v20191010-RELEASE
tio:
websocket:
server:
port: 8989
2.实现handler
@Component
public class MyHandler implements IWsMsgHandler {
/**
* 握手
*
* @param httpRequest
* @param httpResponse
* @param channelContext
* @return
* @throws Exception
*/
@Override
public HttpResponse handshake(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
return httpResponse;
}
/**
* 握手成功
*
* @param httpRequest
* @param httpResponse
* @param channelContext
* @throws Exception
*/
@Override
public void onAfterHandshaked(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
System.out.println("握手成功");
}
/**
* 接收二进制文件
*
* @param wsRequest
* @param bytes
* @param channelContext
* @return
* @throws Exception
*/
@Override
public Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
return null;
}
/**
* 断开连接
*
* @param wsRequest
* @param bytes
* @param channelContext
* @return
* @throws Exception
*/
@Override
public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
System.out.println("关闭连接");
return null;
}
/**
* 接收消息
*
* @param wsRequest
* @param s
* @param channelContext
* @return
* @throws Exception
*/
@Override
public Object onText(WsRequest wsRequest, String s, ChannelContext channelContext) throws Exception {
System.out.println("接收文本消息:" + s);
return "success";
}
}
3.主启动类加上开启注解
@EnableTioWebSocketServer
1.引入maven坐标
org.springframework.boot
spring-boot-starter-websocket
2.加入配置类
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 配置客户端尝试连接地址
registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 设置广播节点
registry.enableSimpleBroker("/topic", "/user");
// 客户端向服务端发送消息需有/app 前缀
registry.setApplicationDestinationPrefixes("/app");
// 指定用户发送(一对一)的前缀 /user/
registry.setUserDestinationPrefix("/user/");
}
}
3.添加处理器
@Controller
public class WSController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/hello")
@SendTo("/topic/hello")
public ResponseMessage hello(RequestMessage requestMessage) {
System.out.println("接收消息:" + requestMessage);
return new ResponseMessage("服务端接收到你发的:" + requestMessage);
}
@GetMapping("/sendMsgByUser")
public @ResponseBody
Object sendMsgByUser(String token, String msg) {
simpMessagingTemplate.convertAndSendToUser(token, "/msg", msg);
return "success";
}
@GetMapping("/sendMsgByAll")
public @ResponseBody
Object sendMsgByAll(String msg) {
simpMessagingTemplate.convertAndSend("/topic", msg);
return "success";
}
@GetMapping("/test")
public String test() {
return "test-stomp.html";
}
}
websocket在线测试 (websocket-test.com)
在线websocket测试-在线工具-postjson (coolaf.com)
下面是两个前端的示例
本地websocket测试
WebSocket Examples: Reverse
SSE 是基于 HTTP 协议的;单向通信,只能由服务端向客户端单向通信;默认支持断线重连;只能传送文本消息;不支持IE;
sse 规范
在 html5 的定义中,服务端 sse,一般需要遵循以下要求
请求头
开启长连接 + 流方式传递
Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache
Connection: keep-alive
数据格式
服务端发送的消息,由 message 组成,其格式: field:value\n\n
其中 field 有五种可能
空: 即以:开头,表示注释,可以理解为服务端向客户端发送的心跳,确保连接不中断
data:数据。订阅后,服务端在消息可用时立即发送给客户端。事件是采用 UTF-8 编码的文本消息。事件之间由两个换行符分隔\n\n。每个事件由一个或多个名称:值字段组成,由单个换行符\n 分隔。
event: 事件,默认值
id: 数据标识符 id 字段表示,相当于每一条数据的编号 。服务器可以发送唯一的事件标识符(id字段)。如果连接中断,客户端会自动重新连接并发送最后接收到的带有header的 Last-Event-ID 的事件 ID。
retry: 重连时间 ,在重试字段中,服务器可以发送超时(以毫秒为单位),之后客户端应在连接中断时自动重新连接。如果未指定此字段,则标准应为 3000 毫秒。
SSE工具类
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@Slf4j
public class SSEServer {
/**
* 当前连接数
*/
private static AtomicInteger count = new AtomicInteger(0);
private static Map sseEmitterMap = new ConcurrentHashMap<>();
public static SseEmitter connect(String userId){
//设置超时时间,0表示不过期,默认是30秒,超过时间未完成会抛出异常
SseEmitter sseEmitter = new SseEmitter(0L);
//注册回调
sseEmitter.onCompletion(completionCallBack(userId));
sseEmitter.onError(errorCallBack(userId));
sseEmitter.onTimeout(timeOutCallBack(userId));
sseEmitterMap.put(userId,sseEmitter);
//数量+1
count.getAndIncrement();
log.info("create new sse connect ,current user:{}",userId);
return sseEmitter;
}
/**
* 给指定用户发消息
*/
public static void sendMessage(String userId, String message){
if(sseEmitterMap.containsKey(userId)){
try{
sseEmitterMap.get(userId).send(message);
}catch (IOException e){
log.error("user id:{}, send message error:{}",userId,e.getMessage());
e.printStackTrace();
}
}
}
/**
* 想多人发送消息,组播
*/
public static void groupSendMessage(String groupId, String message){
if(sseEmitterMap!=null&&!sseEmitterMap.isEmpty()){
sseEmitterMap.forEach((k,v) -> {
try{
if(k.startsWith(groupId)){
v.send(message, MediaType.APPLICATION_JSON);
}
}catch (IOException e){
log.error("user id:{}, send message error:{}",groupId,message);
removeUser(k);
}
});
}
}
public static void batchSendMessage(String message) {
sseEmitterMap.forEach((k,v)->{
try{
v.send(message,MediaType.APPLICATION_JSON);
}catch (IOException e){
log.error("user id:{}, send message error:{}",k,e.getMessage());
removeUser(k);
}
});
}
/**
* 群发消息
*/
public static void batchSendMessage(String message, Set userIds){
userIds.forEach(userId->sendMessage(userId,message));
}
public static void removeUser(String userId){
sseEmitterMap.remove(userId);
//数量-1
count.getAndDecrement();
log.info("remove user id:{}",userId);
}
public static List getIds(){
return new ArrayList<>(sseEmitterMap.keySet());
}
public static int getUserCount(){
return count.intValue();
}
private static Runnable completionCallBack(String userId) {
return () -> {
log.info("结束连接,{}",userId);
removeUser(userId);
};
}
private static Runnable timeOutCallBack(String userId){
return ()->{
log.info("连接超时,{}",userId);
removeUser(userId);
};
}
private static Consumer errorCallBack(String userId){
return throwable -> {
log.error("连接异常,{}",userId);
removeUser(userId);
};
}
}
Controller层
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import vip.huhailong.catchat.sse.SSEServer;
@Slf4j
@RestController
@CrossOrigin
@RequestMapping("/sse")
public class SSEController {
@GetMapping(value = "/connect/{userId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@PathVariable String userId){
return SSEServer.connect(userId);
}
@GetMapping("/process")
public void sendMessage() throws InterruptedException {
for(int i=0; i<=100; i++){
if(i>50&&i<70){
Thread.sleep(500L);
}else{
Thread.sleep(100L);
}
SSEServer.batchSendMessage(String.valueOf(i));
}
}
}
前端
Home
nginx 配置 proxy_buffering off
不配置proxy_buffering off的话,会出现请求发出后,接口收到直接返回,无法保持长连接。
参考网上说明:proxy_buffering这个参数用来控制是否打开后端响应内容的缓冲区,如果这个设置为off,那么proxy_buffers和proxy_busy_buffers_size这两个指令将会失效。