文章目录
1 摘要
Websocket 是一种基于 TCP 连接的可进行双工通讯的协议,能够更好地方便客户端与服务端数据传输,允许服务端向客户端主动发送消息。相较于 HTTP 连接, websocket 不需要3次握手,1次握手之后即可实现服务端与客户端的数据通讯。Websocket 通常用于实时聊天通讯场景,本文将介绍在 SpringBoot 中使用原生注解简易集成 websocket。
2 核心 Maven 依赖
./demo-websocket/pom.xml
<!-- Websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
其中 springboot
的版本为 2.2.5.RELEASE
3 配置信息
./demo-websocket/src/main/resources/application.yml
## config
## server
server:
port: 8200
websocket
的端口即为服务端口,若不自定义,则默认为 8080
端口
4 核心代码类
4.1 websocket 配置类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/common/config/WebsocketConfig.java
package com.ljq.demo.springboot.websocket.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @Description: websocket 配置信息
* @Author: junqiang.lu
* @Date: 2020/3/14
*/
@Configuration
@EnableWebSocket
public class WebsocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
4.2 websocket 消息体封装类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/model/entity/SocketMessage.java
package com.ljq.demo.springboot.websocket.model.entity;
import lombok.Data;
/**
* @Description: 聊天信息封装类
* @Author: junqiang.lu
* @Date: 2020/3/14
*/
@Data
public class SocketMessage {
/**
* 消息发送者
*/
private String from;
/**
* 消息接收者
*/
private String to;
/**
* 消息内容
*/
private String content;
}
4.3 socket 消息编码类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/common/util/SocketEncoder.java
package com.ljq.demo.springboot.websocket.common.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ljq.demo.springboot.websocket.model.entity.SocketMessage;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
/**
* @Description: socket 消息编码器
* @Author: junqiang.lu
* @Date: 2020/3/14
*/
public class SocketEncoder implements Encoder.Text<SocketMessage> {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public String encode(SocketMessage socketMessage) throws EncodeException {
try {
return objectMapper.writeValueAsString(socketMessage);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
@Override
public void init(EndpointConfig endpointConfig) {
}
@Override
public void destroy() {
}
}
4.4 socket 消息解码类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/common/util/SocketDecoder.java
package com.ljq.demo.springboot.websocket.common.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ljq.demo.springboot.websocket.model.entity.SocketMessage;
import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;
/**
* @Description: websocket 消息解码器
* @Author: junqiang.lu
* @Date: 2020/3/14
*/
public class SocketDecoder implements Decoder.Text<SocketMessage> {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public SocketMessage decode(String s) throws DecodeException {
try {
return objectMapper.readValue(s, SocketMessage.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
@Override
public boolean willDecode(String s) {
return (s != null);
}
@Override
public void init(EndpointConfig endpointConfig) {
}
@Override
public void destroy() {
}
}
4.5 websocket 服务端站点类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/web/ChatEndPoint.java
package com.ljq.demo.springboot.websocket.web;
import com.ljq.demo.springboot.websocket.common.util.SocketDecoder;
import com.ljq.demo.springboot.websocket.common.util.SocketEncoder;
import com.ljq.demo.springboot.websocket.model.entity.SocketMessage;
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.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @Description: websocket 服务端站点
* @Author: junqiang.lu
* @Date: 2020/3/14
*/
@Slf4j
@Component
@ServerEndpoint(value = "/chat/{username}", encoders = {SocketEncoder.class}, decoders = {SocketDecoder.class})
public class ChatEndPoint {
private Session session;
private static Set<ChatEndPoint> chatEndPoints = new CopyOnWriteArraySet<>();
private static Map<String, String> users = new HashMap<>();
/**
* 打开 socket 连接
*
* @param session
* @param username
*/
@OnOpen
public void onOpen(Session session, @PathParam("username") String username) {
log.debug("Websocket is opening ");
this.session = session;
chatEndPoints.add(this);
users.put(session.getId(), username);
log.debug("username: {}", username);
SocketMessage message = new SocketMessage();
message.setFrom(username);
message.setContent("Connected !!!, online user: " + users.size());
broadcast(message);
log.debug("online user: {}", users.size());
}
/**
* 发送信息
*
* @param session
* @param message
*/
@OnMessage
public void onMessage(Session session, SocketMessage message) {
message.setFrom(users.getOrDefault(session.getId(), ""));
log.debug("message: {}", message.toString());
broadcast(message);
log.debug("online user: {}", users.size());
}
/**
* 关闭 socket 连接
*
* @param session
*/
@OnClose
public void onClose(Session session) {
log.debug("Socket is closing .");
log.debug("username: {}", users.getOrDefault(session.getId(), ""));
chatEndPoints.remove(this);
users.remove(session.getId());
SocketMessage message = new SocketMessage();
message.setFrom(users.getOrDefault(session.getId(), ""));
message.setContent("Socket disconnected .");
broadcast(message);
log.debug("online user: {}", users.size());
}
/**
* 异常
*
* @param session
*/
@OnError
public void onError(Session session, Throwable throwable) {
log.error("Socket exception", throwable);
log.debug("online user: {}", users.size());
}
/**
* 信息发送
*
* @param message
*/
private static void broadcast(SocketMessage message) {
chatEndPoints.forEach(chatEndPoint -> {
synchronized (chatEndPoint) {
try {
chatEndPoint.session.getBasicRemote().sendObject(message);
} catch (IOException e) {
e.printStackTrace();
} catch (EncodeException e) {
e.printStackTrace();
}
}
});
}
}
websocket
注解说明:
@ServerEndpoint
: websocket 服务端站点注解,用于Java类,类似于 @Controller
, 使用该注解的类,即可接收客户端发来的 socket 信息
@OnOpen
: 该注解用于方法上,打开 socket 连接时触发
@OnMessage
: 该注解用于方法上,在客户端发送消息时触发
@OnClose
: 该注解用于方法上,当关闭 socket 连接时触发
@OnError
: 该注解用于方法上,当 socket 连接出现异常时触发,触发异常,会导致 socket 连接断开。通常发送的消息不合法、格式错误会导致异常抛出。
4.6 SpringBoot 启动类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/DemoWebsocketApplication.java
package com.ljq.demo.springboot.websocket;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @Description: websocket SpringBoot 启动类
* @Author: junqiang.lu
* @Date: 2020/3/14
*/
@SpringBootApplication
public class DemoWebsocketApplication {
public static void main(String[] args) {
SpringApplication.run(DemoWebsocketApplication.class, args);
}
}
5 测试
在线 websocket 测试地址:
启动项目,在测试网站输入 websocket 地址:
ws://127.0.0.1:8200/chat/demo1
127.0.0.1
为本机 ip 地址,可根据需要自行更改 ip 地址
demo1
为用户名,测试时可自行更换
连接成功,返回的结果为:
{
"from": "demo1",
"to": null,
"content": "Connected !!!, online user: 1"
}
控制台日志:
2020-03-18 16:49:29 | DEBUG | http-nio-8200-exec-1 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 42| Websocket is opening
2020-03-18 16:49:29 | DEBUG | http-nio-8200-exec-1 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 47| username: demo1
2020-03-18 16:49:29 | DEBUG | http-nio-8200-exec-1 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 53| online user: 1
发送消息,消息内容:
{
"to": "demo2",
"content": "Hello"
}
返回结果:
{
"from": "demo1",
"to": "demo2",
"content": "Hello"
}
控制台日志:
2020-03-18 16:52:18 | DEBUG | http-nio-8200-exec-2 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 65| message: SocketMessage(from=demo1, to=demo2, content=Hello)
2020-03-18 16:52:18 | DEBUG | http-nio-8200-exec-2 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 68| online user: 1
断开连接,控制台日志:
2020-03-18 16:55:59 | DEBUG | http-nio-8200-exec-3 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 78| Socket is closing .
2020-03-18 16:55:59 | DEBUG | http-nio-8200-exec-3 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 79| username: demo1
2020-03-18 16:55:59 | DEBUG | http-nio-8200-exec-3 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 88| online user: 0
以上为单个用户的测试,可以使用多个设备登录测试网站进行 websocket 连接测试
6 参考资料推荐
A Guide to the Java API for WebSocket
Spring Boot系列十六 WebSocket简介和spring boot集成简单消息代理
【websocket】spring boot 集成 websocket 的四种方式
7 Github 源码
Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.