项目开发中,实时消息推送已成为提升用户体验的关键技术。无论是聊天应用、通知系统、实时数据展示,还是协同办公场景,都需要服务器能够主动向客户端推送消息。本文将详细介绍springboot中实现网页消息推送的几种主流方案,帮助开发者根据实际需求选择最合适的技术。
一、为什么需要消息推送
传统的http请求是客户端主动请求,服务端被动响应的模式。但在很多场景下,我们需要服务器能够主动将消息推送给浏览器,例如:
- web版即时通讯
- 股票、基金等金融数据实时更新
- 系统通知和提醒
- 协同编辑文档时的实时更新
- ......
二、消息推送实现方案
1. 短轮询 (short polling)
原理:客户端以固定的时间间隔频繁发送请求,询问服务器是否有新消息。
实现方式:
@restcontroller
@requestmapping("/api/messages")
public class messagecontroller {
private final map<string, list<string>> usermessages = new concurrenthashmap<>();
@getmapping("/{userid}")
public list<string> getmessages(@pathvariable string userid) {
list<string> messages = usermessages.getordefault(userid, new arraylist<>());
list<string> result = new arraylist<>(messages);
messages.clear(); // 清空已读消息
return result;
}
@postmapping("/{userid}")
public void sendmessage(@pathvariable string userid, @requestbody string message) {
usermessages.computeifabsent(userid, k -> new arraylist<>()).add(message);
}
}
前端实现:
function startpolling() {
setinterval(() => {
fetch('/api/messages/user123')
.then(response => response.json())
.then(messages => {
if (messages.length > 0) {
messages.foreach(msg => console.log(msg));
}
});
}, 3000); // 每3秒查询一次
}
优点:
- 实现简单,不需要特殊的服务器配置
- 兼容性好,支持几乎所有浏览器和服务器
缺点:
- 资源消耗大,大量无效请求
- 实时性较差,受轮询间隔影响
- 服务器负载高,尤其是在用户量大的情况下
2. 长轮询 (long polling)
原理:客户端发送请求后,如果服务器没有新消息,则保持连接打开直到有新消息或超时,然后客户端立即发起新的请求。
实现方式:
@restcontroller
@requestmapping("/api/long-polling")
public class longpollingcontroller {
private final map<string, deferredresult<list<string>>> waitingrequests = new concurrenthashmap<>();
private final map<string, list<string>> pendingmessages = new concurrenthashmap<>();
@getmapping("/{userid}")
public deferredresult<list<string>> waitformessages(@pathvariable string userid) {
deferredresult<list<string>> result = new deferredresult<>(60000l, new arraylist<>());
// 检查是否有待处理的消息
list<string> messages = pendingmessages.get(userid);
if (messages != null && !messages.isempty()) {
list<string> messagestosend = new arraylist<>(messages);
messages.clear();
result.setresult(messagestosend);
} else {
// 没有消息,等待
waitingrequests.put(userid, result);
result.oncompletion(() -> waitingrequests.remove(userid));
result.ontimeout(() -> waitingrequests.remove(userid));
}
return result;
}
@postmapping("/{userid}")
public void sendmessage(@pathvariable string userid, @requestbody string message) {
// 查看是否有等待的请求
deferredresult<list<string>> deferredresult = waitingrequests.get(userid);
if (deferredresult != null) {
list<string> messages = new arraylist<>();
messages.add(message);
deferredresult.setresult(messages);
waitingrequests.remove(userid);
} else {
// 存储消息,等待下一次轮询
pendingmessages.computeifabsent(userid, k -> new arraylist<>()).add(message);
}
}
}
前端实现:
function longpolling() {
fetch('/api/long-polling/user123')
.then(response => response.json())
.then(messages => {
if (messages.length > 0) {
messages.foreach(msg => console.log(msg));
}
// 立即发起下一次长轮询
longpolling();
})
.catch(() => {
// 出错后延迟一下再重试
settimeout(longpolling, 5000);
});
}
优点:
- 减少无效请求,相比短轮询更高效
- 近实时体验,有消息时立即推送
- 兼容性好,几乎所有浏览器都支持
缺点:
- 服务器资源消耗,大量连接会占用服务器资源
- 可能受超时限制
- 难以处理服务器主动推送的场景
3. server-sent events (sse)
原理:服务器与客户端建立单向连接,服务器可以持续向客户端推送数据,而不需要客户端重复请求。
springboot实现:
@restcontroller
@requestmapping("/api/sse")
public class ssecontroller {
private final map<string, sseemitter> emitters = new concurrenthashmap<>();
@getmapping("/subscribe/{userid}")
public sseemitter subscribe(@pathvariable string userid) {
sseemitter emitter = new sseemitter(long.max_value);
emitter.oncompletion(() -> emitters.remove(userid));
emitter.ontimeout(() -> emitters.remove(userid));
emitter.onerror(e -> emitters.remove(userid));
// 发送一个初始事件保持连接
try {
emitter.send(sseemitter.event().name("init").data("连接已建立"));
} catch (ioexception e) {
emitter.completewitherror(e);
}
emitters.put(userid, emitter);
return emitter;
}
@postmapping("/publish/{userid}")
public responseentity<string> publish(@pathvariable string userid, @requestbody string message) {
sseemitter emitter = emitters.get(userid);
if (emitter != null) {
try {
emitter.send(sseemitter.event()
.name("message")
.data(message));
return responseentity.ok("消息已发送");
} catch (ioexception e) {
emitters.remove(userid);
return responseentity.internalservererror().body("发送失败");
}
} else {
return responseentity.notfound().build();
}
}
@postmapping("/broadcast")
public responseentity<string> broadcast(@requestbody string message) {
list<string> deademitters = new arraylist<>();
emitters.foreach((userid, emitter) -> {
try {
emitter.send(sseemitter.event()
.name("broadcast")
.data(message));
} catch (ioexception e) {
deademitters.add(userid);
}
});
deademitters.foreach(emitters::remove);
return responseentity.ok("广播消息已发送");
}
}
前端实现:
function connectsse() {
const eventsource = new eventsource('/api/sse/subscribe/user123');
eventsource.addeventlistener('init', function(event) {
console.log(event.data);
});
eventsource.addeventlistener('message', function(event) {
console.log('收到消息: ' + event.data);
});
eventsource.addeventlistener('broadcast', function(event) {
console.log('收到广播: ' + event.data);
});
eventsource.onerror = function() {
eventsource.close();
// 可以在这里实现重连逻辑
settimeout(connectsse, 5000);
};
}
优点:
- 真正的服务器推送,节省资源
- 自动重连机制
- 支持事件类型区分
- 相比websocket更轻量
缺点:
- 单向通信,客户端无法通过sse向服务器发送数据
- 连接数限制,浏览器对同一域名的sse连接数有限制
- ie浏览器不支持
4. websocket
原理:websocket是一种双向通信协议,在单个tcp连接上提供全双工通信通道。
springboot配置:
@configuration
@enablewebsocket
public class websocketconfig implements websocketconfigurer {
@override
public void registerwebsockethandlers(websockethandlerregistry registry) {
registry.addhandler(new messagewebsockethandler(), "/ws/messages")
.setallowedorigins("*");
}
}
public class messagewebsockethandler extends textwebsockethandler {
private static final map<string, websocketsession> sessions = new concurrenthashmap<>();
@override
public void afterconnectionestablished(websocketsession session) throws exception {
string userid = extractuserid(session);
sessions.put(userid, session);
}
@override
protected void handletextmessage(websocketsession session, textmessage message) throws exception {
// 处理从客户端接收的消息
string payload = message.getpayload();
// 处理逻辑...
}
@override
public void afterconnectionclosed(websocketsession session, closestatus status) throws exception {
string userid = extractuserid(session);
sessions.remove(userid);
}
private string extractuserid(websocketsession session) {
// 从session中提取用户id
return session.geturi().getquery().replace("userid=", "");
}
// 发送消息给指定用户
public static void sendtouser(string userid, string message) {
websocketsession session = sessions.get(userid);
if (session != null && session.isopen()) {
try {
session.sendmessage(new textmessage(message));
} catch (ioexception e) {
sessions.remove(userid);
}
}
}
// 广播消息
public static void broadcast(string message) {
sessions.foreach((userid, session) -> {
if (session.isopen()) {
try {
session.sendmessage(new textmessage(message));
} catch (ioexception e) {
sessions.remove(userid);
}
}
});
}
}
前端实现:
function connectwebsocket() {
const socket = new websocket('ws://localhost:8080/ws/messages?userid=user123');
socket.onopen = function() {
console.log('websocket连接已建立');
// 可以发送一条消息
socket.send(json.stringify({type: 'join', content: '用户已连接'}));
};
socket.onmessage = function(event) {
const message = json.parse(event.data);
console.log('收到消息:', message);
};
socket.onclose = function() {
console.log('websocket连接已关闭');
// 可以在这里实现重连逻辑
settimeout(connectwebsocket, 5000);
};
socket.onerror = function(error) {
console.error('websocket错误:', error);
socket.close();
};
}
优点:
- 全双工通信,服务器和客户端可以随时相互发送数据
- 实时性最好,延迟最低
- 效率高,建立连接后无需http头,数据传输量小
- 支持二进制数据
缺点:
- 实现相对复杂
- 对服务器要求高,需要处理大量并发连接
- 可能受到防火墙限制
5. stomp (基于websocket)
原理:stomp (simple text oriented messaging protocol) 是一个基于websocket的简单消息传递协议,提供了更高级的消息传递模式。
springboot配置:
@configuration
@enablewebsocketmessagebroker
public class websocketconfig implements websocketmessagebrokerconfigurer {
@override
public void configuremessagebroker(messagebrokerregistry registry) {
// 启用简单的基于内存的消息代理
registry.enablesimplebroker("/topic", "/queue");
// 设置应用的前缀
registry.setapplicationdestinationprefixes("/app");
// 设置用户目的地前缀
registry.setuserdestinationprefix("/user");
}
@override
public void registerstompendpoints(stompendpointregistry registry) {
registry.addendpoint("/ws")
.setallowedorigins("*")
.withsockjs(); // 添加sockjs支持
}
}
@controller
public class messagecontroller {
private final simpmessagingtemplate messagingtemplate;
public messagecontroller(simpmessagingtemplate messagingtemplate) {
this.messagingtemplate = messagingtemplate;
}
// 处理客户端发送到/app/sendmessage的消息
@messagemapping("/sendmessage")
public void processmessage(string message) {
// 处理消息...
}
// 处理客户端发送到/app/chat/{roomid}的消息,并广播到相应的聊天室
@messagemapping("/chat/{roomid}")
@sendto("/topic/chat/{roomid}")
public chatmessage chat(@destinationvariable string roomid, chatmessage message) {
// 处理聊天消息...
return message;
}
// 发送私人消息
@messagemapping("/private-message")
public void privatemessage(privatemessage message) {
messagingtemplate.convertandsendtouser(
message.getrecipient(), // 接收者的用户名
"/queue/messages", // 目的地
message // 消息内容
);
}
// rest api发送广播消息
@postmapping("/api/broadcast")
public responseentity<string> broadcast(@requestbody string message) {
messagingtemplate.convertandsend("/topic/broadcast", message);
return responseentity.ok("消息已广播");
}
// rest api发送私人消息
@postmapping("/api/private-message/{userid}")
public responseentity<string> sendprivatemessage(
@pathvariable string userid,
@requestbody string message) {
messagingtemplate.convertandsendtouser(userid, "/queue/messages", message);
return responseentity.ok("私人消息已发送");
}
}
前端实现:
const stompclient = new stompjs.client({
brokerurl: 'ws://localhost:8080/ws',
connectheaders: {
login: 'user',
passcode: 'password'
},
debug: function (str) {
console.log(str);
},
reconnectdelay: 5000,
heartbeatincoming: 4000,
heartbeatoutgoing: 4000
});
stompclient.onconnect = function (frame) {
console.log('connected: ' + frame);
// 订阅广播消息
stompclient.subscribe('/topic/broadcast', function (message) {
console.log('收到广播: ' + message.body);
});
// 订阅特定聊天室
stompclient.subscribe('/topic/chat/room1', function (message) {
const chatmessage = json.parse(message.body);
console.log('聊天消息: ' + chatmessage.content);
});
// 订阅私人消息
stompclient.subscribe('/user/queue/messages', function (message) {
console.log('收到私人消息: ' + message.body);
});
// 发送消息到聊天室
stompclient.publish({
destination: '/app/chat/room1',
body: json.stringify({
sender: 'user123',
content: '大家好!',
timestamp: new date()
})
});
// 发送私人消息
stompclient.publish({
destination: '/app/private-message',
body: json.stringify({
sender: 'user123',
recipient: 'user456',
content: '你好,这是一条私信',
timestamp: new date()
})
});
};
stompclient.onstomperror = function (frame) {
console.error('stomp错误: ' + frame.headers['message']);
console.error('additional details: ' + frame.body);
};
stompclient.activate();
优点:
- 高级消息模式:主题订阅、点对点消息传递
- 内置消息代理,简化消息路由
- 支持消息确认和事务
- 框架支持完善,springboot集成度高
- 支持认证和授权
缺点:
- 学习曲线较陡
- 资源消耗较高
- 配置相对复杂
三、方案对比与选择建议
| 方案 | 实时性 | 双向通信 | 资源消耗 | 实现复杂度 | 浏览器兼容性 |
|---|---|---|---|---|---|
| 短轮询 | 低 | 否 | 高 | 低 | 极好 |
| 长轮询 | 中 | 否 | 中 | 中 | 好 |
| sse | 高 | 否(单向) | 低 | 低 | ie不支持 |
| websocket | 极高 | 是 | 低 | 高 | 良好(需考虑兼容) |
| stomp | 极高 | 是 | 中 | 高 | 良好(需考虑兼容) |
选择建议:
- 简单通知场景:对实时性要求不高,可以选择短轮询或长轮询
- 服务器单向推送数据:如实时数据展示、通知提醒等,推荐使用sse
- 实时性要求高且需双向通信:如聊天应用、在线游戏等,应选择websocket
- 复杂消息传递需求:如需要主题订阅、点对点消息、消息确认等功能,推荐使用stomp
- 需要考虑老旧浏览器:应避免使用sse和websocket,或提供降级方案
四、总结
在springboot中实现网页消息推送,有多种技术方案可选,每种方案都有其适用场景:
- 短轮询:最简单但效率最低,适合非实时性要求的场景
- 长轮询:改进版的轮询,降低了服务器负载,提高了实时性
- sse:轻量级的服务器推送技术,适合单向通信场景
- websocket:功能最强大的双向通信方案,适合高实时性要求场景
- stomp:基于websocket的消息协议,提供了更高级的消息传递功能
选择合适的推送技术需要根据业务需求、性能要求和浏览器兼容性等因素综合考虑。在实际应用中,也可以结合多种技术,提供优雅降级方案,确保在各种环境下都能提供良好的用户体验。
以上就是springboot实现网页消息推送的5种方法小结的详细内容,更多关于springboot网页消息推送的资料请关注代码网其它相关文章!
发表评论