项目服务中遇到如下场景:设备A生成带有身份信息"Device0001"的二维码,使用手机扫码付款,付款成功后服务端返回信息给设备A
因为要从服务端发送消息,有两种方式:1.服务端调用设备A的接口;2.socket会话传输。由于设备A无法搭建服务,所以排除方案一。socket实现流程大致如下:
设备A中弹出二维码同时创建与服务端的socket连接;服务端收到付款回调后单独推送消息给"Device0001"或广播通知并携带接收对象的身份信息;设备A收到消息后解析消息内容并作出后续操作。
项目服务中添加socket服务作服务端,设备A作为socket客户端,此处粘出服务端代码及js客户端demo
<!--webSocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
package com.mark.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
package com.mark.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}") // 接口路径 ws://localhost:80/webSocket/userId;
// 类似于@RequestMapping和@Bean的结合,把当前类当做一个实例,提供socket服务,对外暴露/websocket/{userId}的接口访问
public class WebSocket {
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
//虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
// 注:底下WebSocket是当前类名
private static CopyOnWriteArraySet<WebSocket> webSockets =new CopyOnWriteArraySet<>();
// 用来存在线连接数
private static Map<String,Session> sessionPool = new HashMap<String,Session>();
/**
* 链接成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value="userId")String userId) {
try {
this.session = session;
webSockets.add(this);
sessionPool.put(userId, session);
System.out.println("=========================");
sessionPool.entrySet().forEach(System.out::println);
System.out.println("=========================");
log.info("【websocket消息】有新的连接,总数为:"+webSockets.size());
} catch (Exception e) {
}
}
/**
* 链接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam(value="userId")String userId) {
try {
webSockets.remove(this);
sessionPool.remove(userId);
System.out.println("=========================");
sessionPool.entrySet().forEach(System.out::println);
System.out.println("=========================");
log.info("【websocket消息】连接断开,总数为:"+webSockets.size());
} catch (Exception e) {
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message
*/
@OnMessage
public void onMessage(String message, @PathParam(value="userId")String userId) {
String id = session.getId();
log.info("【websocket消息】收到客户端消息:"+message);
sendAllMessage("【websocket消息】 "+id+" 号客户端收到服务端群体推送消息:"+message);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sendOneMessage(userId,"【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();
}
}
}
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=2.0, user-scalable=yes" />
<title>聊天室示例</title>
</head>
<body>
<textarea id="sendText"></textarea>
<button onclick="sendMessage()">发送</button>
<script>
var connect;
window.onload=function unit(){
if ("WebSocket" in window){
// 打开一个 web socket
connect = new WebSocket("ws://ip:port/websocket/myid");
connect.onopen = function(){
// Web Socket 已连接上
alert("已连接");
};
connect.onmessage = function (evt){
var r_msg = evt.data;
document.body.innerHTML+="
接收的信息为:"+r_msg;
};//接收时自己也会接收,可以自己设置过滤的if语句
connect.onclose = function(){
// 关闭 websocket
alert("连接关闭");
};
}
else{
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
};
function sendMessage(){
connect.send(document.getElementById("sendText").value);
document.getElementById("sendText").innerHTML="";
}
</script>
</body>
</html>
依赖导入

配置类添加

服务端启动
集成到原来的SpringBoot项目中后直接启动原项目,/websocket/{userId}这个路径会当成websocket的入口,类似于接口调用

客户端连接


nginx配置详解
# server的同级加入这段
# 如果 $http_upgrade 不为 '' (空),则 $connection_upgrade 为 upgrade 。
# 如果 $http_upgrade 为 '' (空),则 $connection_upgrade 为 close。
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# server内location做长连接处理
location / {
proxy_http_version 1.1; # 表示反向代理发送的HTTP协议的版本是1.1,HTTP1.1支持长连接
proxy_pass http://localhost:8080/;
proxy_set_header Host $host; # 表示传递时请求头不变
proxy_set_header X-Real-IP $remote_addr; # 保留客户端IP
proxy_read_timeout 3600s; # 超时自动断联的时间设置
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 保留X-Forwarded-For头不发生改变
proxy_set_header Upgrade $http_upgrade; # 设置Upgrade不变
proxy_set_header Connection $connection_upgrade; # 如果 $http_upgrade为upgrade,则请求为upgrade(websocket),如果不是,就关闭连接
}
nginx配置整体结构
http{
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# 负载均衡
upstream xxx{
server ip:port;
server ip:port;
}
server{
listen 80;
server_name location;
location / {
.....
}
}
}
wss问题
在线上环境存在https页面中调用websocket的场景,这时候如果没有配置wss会报相关错误,只需要在listen443的server中做上述配置即可解决,当然一般线上环境80转443后直接写在443中也就不存在这个问题了。
有问题和bug欢迎讨论~