HelloWood

Spring Boot 中使用 WebSocket

2019-09-08

Spring Boot 中使用 WebSocket

WebSocket 是一种长连接技术,可以实现服务端和客户端的双向通信,服务端可以主动推送信息给客户端

构建应用

添加依赖

  • build.gradle
1
2
3
4
5
6
7
8
9
10
dependencies {
compile("org.springframework.boot:spring-boot-starter-websocket")
compile("org.webjars:webjars-locator-core")
compile("org.webjars:sockjs-client:1.0.2")
compile("org.webjars:stomp-websocket:2.3.3")
compile("org.webjars:bootstrap:3.3.7")
compile("org.webjars:jquery:3.1.0")

testCompile("org.springframework.boot:spring-boot-starter-test")
}

配置

  • WebSocketConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket").withSockJS();
}
}

其中/topic 是用于推送给客户端的消息路径前缀;/app是用于请求服务端的消息路径前缀, /socket用于客户端建立连接

SockJS用于提供浏览器兼容性,当浏览器不支持 WebSocket 时,就会尝试降级为HTTP流或者长轮询的方式以实现和 WebSocket 相同的效果,参考 4.3. SockJS Fallback

群发消息

群发消息可以将消息发送给所有订阅了该消息的客户端,可以通过 @SendToorg.springframework.messaging.simp.SimpMessagingTemplate#convertAndSend发送

服务端

  • 通过注解实现
1
2
3
4
5
6
7
8
9
10
11
@MessageMapping("/message/broadcast")
@SendTo("/response/message")
public Message broadcastMessage(String title) {
log.info("Receive new broadcast message from socket, title is :" + title);

return Message.builder()
.title(title)
.content("Socket Broadcast:" + title + " content!")
.createTime(LocalDateTime.now())
.build();
}
  • 通过 REST接口调用方法实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;

@GetMapping("/message/broadcast")
@ResponseBody
public void sendBroadcastMessage(String title) {
log.info("Receive new broadcast message from REST interface, title is :" + title);

Message message = Message.builder()
.title(title)
.content("REST Broadcast:" + title + " content!")
.createTime(LocalDateTime.now())
.build();

simpMessagingTemplate.convertAndSend("/response/message", message);
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
function connect() {
var socket = new SockJS('/socket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
stompClient.subscribe('/response/message', function (message) {
console.log("Receive message from server:" + message);
});
});
}

function sendMessage() {
stompClient.send('/request/message/broadcast', {}, "Message");
}

测试

  • 启动应用,用两个不同的浏览器访问 localhost:8080
  • 广播消息建立连接,并发送消息,此时可以看到两个浏览器都收到了刚才发送的消息
  • 通过 REST 接口:
1
curl 'localhost:8080/message/broadcast?title=hello'

发送给指定客户端

群发消息可以将消息发送给所有订阅了该消息的客户端,可以通过 @SendToUserorg.springframework.messaging.simp.SimpMessagingTemplate#convertAndSendToUser发送

服务端

发送给指定客户端,要求客户端有指定的用户名,通过继承org.springframework.web.socket.server.support.DefaultHandshakeHandler实现

  • CustomPrinciple.java
1
2
3
4
5
6
7
8
9
10
@AllArgsConstructor
public class CustomPrinciple implements Principal {

private String name;

@Override
public String getName() {
return this.name;
}
}
  • CustomHandshakeHandler.java
1
2
3
4
5
6
7
8
9
10
@Slf4j
public class CustomHandshakeHandler extends DefaultHandshakeHandler {

@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
String userId = UUID.randomUUID().toString();
log.info("Current username is: {}", userId);
return new CustomPrinciple(userId);
}
}
  • WebSocketConfig.java
1
2
3
4
5
6
7
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket")
.setHandshakeHandler(new CustomHandshakeHandler())
.withSockJS();

}
  • 通过注解实现
1
2
3
4
5
6
7
8
9
10
11
@MessageMapping("/message/specify")
@SendToUser("/response/message")
public Message speicifyMessage(String title) {
log.info("Receive new specify message from socket, title is :" + title);

return Message.builder()
.title(title)
.content("Socket Specify:" + title + " content!")
.createTime(LocalDateTime.now())
.build();
}
  • 通过调用方法实现
1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/message/specify")
@ResponseBody
public void sendSpecifyUserMessage(String title, String username) {
log.info("Receive new specify message from REST interface, title is :" + title);

Message message = Message.builder()
.title(title)
.content("REST Specify:" + title + " content!")
.createTime(LocalDateTime.now())
.build();

simpMessagingTemplate.convertAndSendToUser(username, "/response/message", message);
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
function connect() {
var socket = new SockJS('/socket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
stompClient.subscribe('/user/response/message', function (message) {
console.log("Receive message from server:" + message);
});
});
}

function sendMessage() {
stompClient.send('/request/message/specify', {}, "Message");
}

需要注意的是,发送给指定用户的消息订阅时需要添加额外的 /user前缀

测试

  • 启动应用,用两个不同的浏览器访问 localhost:8080
  • 指定消息建立连接,并发送消息,此时只有发送消息的那个浏览器才能收到消息,另一个没有收到
  • 通过 REST 接口:
    建立连接后可以在控制台看到相应的 username
1
curl 'localhost:8080/message/specify?title=hello&username=ff3cb2ca-9579-46fb-973b-b1bd6420f610'

此时相应的客户端会收到发送的消息,而另一个没有


参考文档

项目地址