在实时性要求较高的应用场景,如在线聊天、实时数据监控、股票行情推送等,传统的http协议由于其请求-响应的模式,无法高效实现服务器与客户端之间的双向实时通信。而websocket协议的出现解决了这一难题,它允许在单个tcp连接上进行全双工通信,使得服务器和客户端可以随时主动发送消息。spring boot对websocket提供了良好的支持,极大地简化了开发流程。本文将从入门到精通,详细介绍spring boot中websocket的常用使用方法。
一、websocket基础概念
1.1 什么是websocket
websocket是一种网络通信协议,于2011年被ietf定为标准rfc 6455,并被html5所支持 。与http协议不同,websocket在建立连接后,通信双方可以随时主动发送和接收数据,无需像http那样每次通信都要建立新的连接,从而减少了开销,提高了实时性。
1.2 websocket与http的区别
特性 | http | websocket |
---|---|---|
通信模式 | 客户端发起请求,服务器响应(单向) | 全双工通信(双向) |
连接方式 | 每次请求都需建立新连接 | 一次握手建立持久连接 |
数据格式 | 通常为文本(json、xml等) | 支持文本和二进制数据 |
应用场景 | 适用于一般的web页面请求 | 适用于实时性要求高的场景 |
二、spring boot集成websocket
2.1 添加依赖
在spring boot项目的pom.xml
文件中添加websocket依赖:
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-websocket</artifactid> </dependency>
如果使用gradle,在build.gradle
中添加:
implementation 'org.springframework.boot:spring-boot-starter-websocket'
2.2 配置websocket
创建一个配置类,用于注册websocket处理程序和配置消息代理:
import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.web.socket.config.annotation.enablewebsocketmessagebroker; import org.springframework.web.socket.config.annotation.stompendpointregistry; import org.springframework.web.socket.config.annotation.websocketmessagebrokerconfigurer; import org.springframework.web.socket.server.standard.servletservercontainerfactorybean; @configuration @enablewebsocketmessagebroker public class websocketconfig implements websocketmessagebrokerconfigurer { @override public void configuremessagebroker(stompbrokerrelayregistration config) { config.setapplicationdestinationprefixes("/app"); config.setdestinationprefixes("/topic"); } @override public void registerstompendpoints(stompendpointregistry registry) { registry.addendpoint("/websocket-endpoint").withsockjs(); } @bean public servletservercontainerfactorybean createwebsocketcontainer() { servletservercontainerfactorybean container = new servletservercontainerfactorybean(); container.setmaxtextmessagebuffersize(65536); container.setmaxbinarymessagebuffersize(65536); return container; } }
在上述代码中:
@enablewebsocketmessagebroker
注解启用websocket消息代理。configuremessagebroker
方法配置消息代理的前缀,/app
用于应用程序发送消息的目的地前缀,/topic
用于服务器发送消息的目的地前缀。registerstompendpoints
方法注册websocket端点,addendpoint
方法指定端点的路径,withsockjs
表示启用sockjs支持,以提供对不支持websocket浏览器的兼容。createwebsocketcontainer
方法配置websocket容器的参数,如消息缓冲区大小。
三、websocket常用使用方法
3.1 简单消息收发
3.1.1 创建消息实体类
public class chatmessage { private string sender; private string content; private messagetype type; // 省略构造函数、getter和setter方法 public enum messagetype { chat, join, leave } }
chatmessage
类用于封装聊天消息,包含发送者、消息内容和消息类型(聊天、加入、离开)。
3.1.2 创建消息处理类
import org.springframework.messaging.handler.annotation.messagemapping; import org.springframework.messaging.handler.annotation.sendto; import org.springframework.stereotype.controller; @controller public class chatcontroller { @messagemapping("/chat.send") @sendto("/topic/public") public chatmessage sendmessage(chatmessage chatmessage) { return chatmessage; } @messagemapping("/chat.join") @sendto("/topic/public") public chatmessage joinchat(chatmessage chatmessage) { chatmessage.settype(chatmessage.messagetype.join); return chatmessage; } }
在上述代码中:
@messagemapping
注解用于映射客户端发送的消息路径,如"/chat.send"
和"/chat.join"
。@sendto
注解指定消息发送的目的地,这里将消息发送到"/topic/public"
,所有订阅该主题的客户端都能接收到消息。sendmessage
方法处理聊天消息的发送,joinchat
方法处理用户加入聊天的消息。
3.1.3 前端页面实现
<!doctype html> <html lang="zh"> <head> <meta charset="utf-8"> <title>websocket chat</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.1/sockjs.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/stompjs/2.3.3/stomp.min.js"></script> </head> <body> <input type="text" id="username" placeholder="用户名"> <button onclick="connect()">连接</button> <div id="chat-window"></div> <input type="text" id="message" placeholder="输入消息"> <button onclick="sendmessage()">发送</button> <script> let socket = new sockjs('/websocket-endpoint'); let stompclient = stomp.over(socket); function connect() { let username = document.getelementbyid('username').value; stompclient.connect({}, function (frame) { console.log('connected: ' + frame); stompclient.subscribe('/topic/public', function (message) { let chatwindow = document.getelementbyid('chat-window'); let msg = json.parse(message.body); if (msg.type === 'join') { chatwindow.innerhtml += msg.sender + " 加入了聊天 "; } else { chatwindow.innerhtml += msg.sender + ": " + msg.content + " "; } }); let joinmessage = { sender: username, content: '', type: 'join' }; stompclient.send("/app/chat.join", {}, json.stringify(joinmessage)); }); } function sendmessage() { let message = document.getelementbyid('message').value; let username = document.getelementbyid('username').value; let chatmessage = { sender: username, content: message, type: 'chat' }; stompclient.send("/app/chat.send", {}, json.stringify(chatmessage)); } </script> </body> </html>
前端页面通过sockjs和stompjs库与后端建立websocket连接,实现消息的发送和接收。
3.2 点对点消息发送
有时候需要实现一对一的消息发送,而不是广播给所有客户端。可以通过在@sendto
中指定具体的用户目的地来实现。
3.2.1 配置用户目的地前缀
在websocketconfig
类中添加用户目的地前缀配置:
@override public void configuremessagebroker(stompbrokerrelayregistration config) { config.setapplicationdestinationprefixes("/app"); config.setdestinationprefixes("/topic", "/user"); config.setuserdestinationprefix("/user"); }
这里添加了/user
作为用户目的地前缀。
3.2.2 修改消息处理类
import org.springframework.messaging.handler.annotation.messagemapping; import org.springframework.messaging.handler.annotation.sendto; import org.springframework.messaging.simp.simpmessageheaderaccessor; import org.springframework.stereotype.controller; @controller public class privatechatcontroller { @messagemapping("/chat.private") public void sendprivatemessage(simpmessageheaderaccessor headeraccessor, chatmessage chatmessage) { string recipient = chatmessage.getrecipient(); headeraccessor.getsessionattributes().put("username", chatmessage.getsender()); this.stompmessagingtemplate.convertandsendtouser(recipient, "/private", chatmessage); } }
在上述代码中:
@messagemapping("/chat.private")
映射处理点对点消息的路径。simpmessageheaderaccessor
用于获取和设置消息头信息。stompmessagingtemplate.convertandsendtouser
方法将消息发送到指定用户的私有目的地。
3.2.3 前端实现点对点消息发送
function sendprivatemessage() { let message = document.getelementbyid('message').value; let username = document.getelementbyid('username').value; let recipient = document.getelementbyid('recipient').value; let chatmessage = { sender: username, recipient: recipient, content: message, type: 'chat' }; stompclient.send("/app/chat.private", {}, json.stringify(chatmessage)); }
前端添加输入接收者的文本框,并在发送消息时指定接收者,实现点对点消息发送。
3.3 消息拦截与认证
在实际应用中,可能需要对websocket消息进行拦截和认证,确保只有合法用户才能进行通信。
3.3.1 创建消息拦截器
import org.springframework.messaging.message; import org.springframework.messaging.messagechannel; import org.springframework.messaging.simp.stomp.stompcommand; import org.springframework.messaging.simp.stomp.stompheaderaccessor; import org.springframework.messaging.support.channelinterceptor; import org.springframework.messaging.support.messageheaderaccessor; import org.springframework.stereotype.component; @component public class websocketinterceptor implements channelinterceptor { @override public message<?> presend(message<?> message, messagechannel channel) { stompheaderaccessor accessor = messageheaderaccessor.getaccessor(message, stompheaderaccessor.class); if (stompcommand.connect.equals(accessor.getcommand())) { // 在这里进行认证逻辑,如检查token等 string token = accessor.getfirstnativeheader("authorization"); if (token == null ||!isvalidtoken(token)) { throw new runtimeexception("认证失败"); } } return message; } private boolean isvalidtoken(string token) { // 实现具体的token验证逻辑 return true; } }
上述代码创建了一个websocketinterceptor
拦截器,在presend
方法中对连接请求进行认证,检查请求头中的authorization
token是否有效。
3.3.2 注册拦截器
在websocketconfig
类中注册拦截器:
@configuration @enablewebsocketmessagebroker public class websocketconfig implements websocketmessagebrokerconfigurer { @override public void configureclientinboundchannel(channelregistration registration) { registration.interceptors(new websocketinterceptor()); } // 其他配置方法... }
通过configureclientinboundchannel
方法将拦截器注册到客户端入站通道,对所有进入的消息进行拦截处理。
四、不使用接口,基于注解的websocket实现
4.1 实现思路
在spring boot中,除了通过实现接口的方式处理websocket消息,还可以利用注解来简化开发过程。通过@serverendpoint
注解定义websocket端点,结合@onopen
、@onmessage
、@onclose
、@onerror
等注解,能够轻松实现对websocket连接生命周期的监听,以及接收和处理客户端发送的数据。
4.2 核心代码实现
首先,创建一个websocket处理类:
import javax.websocket.*; import javax.websocket.server.pathparam; import javax.websocket.server.serverendpoint; import java.io.ioexception; import java.util.concurrent.copyonwritearrayset; @serverendpoint("/ws/{userid}") public class mywebsocket { // 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static int onlinecount = 0; // concurrent包的线程安全set,用来存放每个客户端对应的mywebsocket对象。 private static copyonwritearrayset<mywebsocket> websocketset = new copyonwritearrayset<>(); // 与某个客户端的连接会话,需要通过它来给客户端发送数据 private session session; // 接收userid private string userid; /** * 连接建立成功调用的方法 */ @onopen public void onopen(session session, @pathparam("userid") string userid) { this.session = session; this.userid = userid; websocketset.add(this); addonlinecount(); system.out.println("有新连接加入!当前在线人数为" + getonlinecount()); } /** * 连接关闭调用的方法 */ @onclose public void onclose() { websocketset.remove(this); subonlinecount(); system.out.println("有一连接关闭!当前在线人数为" + getonlinecount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @onmessage public void onmessage(string message, session session) { system.out.println("来自客户端" + userid + "的消息:" + message); // 群发消息 for (mywebsocket item : websocketset) { try { item.sendmessage(message); } catch (ioexception e) { e.printstacktrace(); } } } /** * 发生错误时调用 * * @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() { mywebsocket.onlinecount++; } public static synchronized void subonlinecount() { mywebsocket.onlinecount--; } }
在上述代码中:
@serverendpoint("/ws/{userid}")
注解定义了websocket的访问端点,{userid}
为路径参数,用于标识不同的客户端。@onopen
注解的方法在连接建立时被调用,用于初始化连接相关信息,并将当前连接对象添加到在线连接集合中。@onmessage
注解的方法在接收到客户端发送的消息时被调用,实现了消息的接收和群发功能。@onclose
注解的方法在连接关闭时被调用,从在线连接集合中移除当前连接对象。@onerror
注解的方法在发生错误时被调用,用于处理异常情况。
4.3 前端页面适配
前端页面同样需要进行相应的修改,以连接基于注解实现的websocket端点:
<!doctype html> <html lang="zh"> <head> <meta charset="utf-8"> <title>基于注解的websocket chat</title> </head> <body> <input type="text" id="userid" placeholder="用户id"> <button onclick="connect()">连接</button> <div id="chat-window"></div> <input type="text" id="message" placeholder="输入消息"> <button onclick="sendmessage()">发送</button> <script> let socket; function connect() { let userid = document.getelementbyid('userid').value; socket = new websocket("ws://localhost:8080/ws/" + userid); socket.onopen = function (event) { console.log("连接成功"); }; socket.onmessage = function (event) { let chatwindow = document.getelementbyid('chat-window'); chatwindow.innerhtml += "收到消息: " + event.data + " "; }; socket.onclose = function (event) { console.log("连接关闭"); }; socket.onerror = function (event) { console.log("连接错误"); }; } function sendmessage() { let message = document.getelementbyid('message').value; if (socket && socket.readystate === websocket.open) { socket.send(message); document.getelementbyid('chat-window').innerhtml += "发送消息: " + message + " "; document.getelementbyid('message').value = ""; } else { alert("websocket连接未建立或已关闭"); } } </script> </body> </html>
4.4 配置websocket端点
还需要在spring boot中配置websocket支持,确保端点被正确注册:
import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.web.socket.server.standard.servletservercontainerfactorybean; @configuration public class websocketconfig { @bean public servletservercontainerfactorybean createwebsocketcontainer() { servletservercontainerfactorybean container = new servletservercontainerfactorybean(); container.setmaxtextmessagebuffersize(8192); container.setmaxbinarymessagebuffersize(8192); return container; } }
这种基于注解的实现方式相比传统接口方式更加简洁直观,通过注解即可完成websocket连接的生命周期管理和消息处理。
五、应用场景拓展
5.1 实时数据推送
在股票交易、天气监测等场景中,服务器需要实时向客户端推送数据。可以结合定时任务实现:
@service public class realtimedataservice { @autowired private simpmessagingtemplate messagingtemplate; @scheduled(fixedrate = 5000) // 每5秒执行一次 public void pushrealtimedata() { // 获取实时数据 stockdata stockdata = getstockdata(); // 推送给订阅了实时股票信息的客户端 messagingtemplate.convertandsend("/topic/stock", stockdata); } private stockdata getstockdata() { // 模拟获取股票数据 return new stockdata("000001", "平安银行", 15.68, 0.23); } }
5.2 在线协作编辑
多个用户可以同时编辑同一个文档,实时看到彼此的操作:
@messagemapping("/edit") @sendto("/topic/document/{docid}") public editoperation handleedit(@destinationvariable string docid, editoperation operation) { // 处理编辑操作,更新文档 documentservice.applyedit(docid, operation); return operation; }
5.3 游戏实时对战
在在线游戏中,玩家的操作需要实时同步到其他玩家:
@messagemapping("/game/{roomid}/move") public void handlegamemove(@destinationvariable string roomid, moveaction action) { // 更新游戏状态 gameservice.updategamestate(roomid, action); // 广播给房间内的所有玩家 messagingtemplate.convertandsend("/topic/game/" + roomid, action); }
六、性能优化与最佳实践
6.1 连接管理
- 使用连接池管理websocket连接,避免频繁创建和销毁连接
- 对长时间不活跃的连接进行心跳检测和自动关闭
- 限制单个客户端的连接数量,防止恶意连接
6.2 消息处理优化
- 对高频消息进行合并和批处理,减少网络开销
- 使用异步处理机制,避免阻塞主线程
- 对大消息进行分片传输,防止消息过大导致的性能问题
6.3 安全加固
- 使用ssl/tls加密websocket连接,确保数据传输安全
- 实现严格的身份认证和权限控制
- 对客户端输入进行过滤和验证,防止xss和sql注入攻击
6.4 监控与告警
- 监控websocket连接数、消息吞吐量等指标
- 设置异常告警机制,及时发现和处理连接异常和性能问题
七、常见问题与解决方案
7.1 跨域问题
- 配置cors允许websocket端点的跨域访问
registry.addendpoint("/websocket-endpoint") .setallowedorigins("*") .withsockjs();
7.2 消息丢失问题
- 实现消息确认机制,确保消息可靠传递
- 使用持久化队列存储重要消息,防止服务器重启导致消息丢失
7.3 性能瓶颈
- 分析性能瓶颈点,针对性地进行优化
- 考虑使用分布式消息队列和集群部署提高系统吞吐量
八、总结
本文从websocket的基础概念出发,详细介绍了spring boot集成websocket的步骤,并重点讲解了常用的使用方法,包括简单消息收发、点对点消息发送、消息拦截与认证,以及不使用接口而是基于注解的websocket实现方式。同时,还拓展了websocket在不同场景下的应用,提供了性能优化建议和常见问题解决方案。
通过这些方法,开发者可以根据实际需求,灵活运用websocket在spring boot应用中实现高效的实时通信功能。在实际项目中,还可以结合更多的spring boot特性和业务逻辑,进一步扩展和优化websocket的应用,打造出更强大、更实用的实时应用程序。
以上补充内容完善了基于注解的websocket实现方案,并新增了应用场景拓展、性能优化等实用内容。如需进一步深入探讨某个主题,或需要其他补充,请随时告知。
到此这篇关于spring boot中websocket常用使用方法详解的文章就介绍到这了,更多相关springboot websocket使用内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论