使用TCP Server+WebSocket实现数据实时展现
项目背景
由于业务需求,需要把RFID读写器读取到的标签信息解析后展示到页面中,考虑到RFID读写器可以充当TCP Client给TCP Server发送消息,于是使用了自定义TCP Server的方式来接收读写器发送的标签信息,使用了WebSocket来给客户端发送解析后的数据
自定义TCP Server
自定义TCP Server我们可以通过java.net包中的ServerSocket类创建,由于在主线程中使用accept()方法会出现阻塞的情况,所以使用一个新线程来处理客户端连接,关键代码如下
// 创建一个端口为9900的TCP Server对象
ServerSocket serverSocket = new ServerSocket(9900);
log.info("the server is start,waiting client connect");
// 创建新线程处理客户端请求,把父线程的bean传递到子
Thread acceptThread = new Thread(() -> acceptConnections(serverSocket));
/**
* 接收客户端连接
*
* @param serverSocket 客户端连接
*/
private void acceptConnections(ServerSocket serverSocket) {
try {
while (true) {
// 等待客户端连接,等待时处于阻塞
Socket clientSocket = serverSocket.accept();
log.info("Client connection is successful");
// 创建新线程处理客户端请求,把父线程的 bean 传递到子线程
Thread clientThread = new Thread(ClientHandler.builder()
.socket(clientSocket)
.machineService(machineService)
.redisUtil(redisUtil)
.build());
clientThread.start();
}
} catch (IOException e) {
log.error("Error accepting connections", e);
}
}
接收客户端数据
当我们把客户端连接给到新线程中后,我们可以通过socket对象来获取输入流,从而达到读取客户端传输过来的数据,关键代码如下
try {
// 通过 Socket 对象获取输入流,读取客户端发来的数据
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
String text = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8);
//处理客户端传输的数据
handleData(text);
}
} catch (Exception e) {
log.error("client monitor error:{0}", e);
}
自定义WebSocket连接
WebSocket的概念我相信大家都有所了解,WebSocket与HTTP不同,WebSocket可以实现服务端与客户端双向通信,WebSocket协议适用于需要实时或频繁通信的应用程序,一般用在股票、游戏、直播上。它提供了一种更高效、更实时的通信机制,与传统的轮询或长轮询相比,WebSocket可以减少网络延迟和服务器负载。
使用WebSocket之前,我们需要先引入WebSocket的依赖
<!-- SpringBoot Websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
引入WebSocket的依赖后,我们可以还需要注入WebSocketConfig
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
代码中ServerEndpointExporter的作用是用于扫描项目中所有包含@ServerEndpoint注解的节点,并把节点注册到Spring中,接下来我们实现一个WebSocket节点
@Slf4j
@Component
@ServerEndpoint(value = "/webSocketServer/{userId}")
public class WebSocketServer {
private static final ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocketServer>> connectionMap = new ConcurrentHashMap<>();
private Session session;
/**
* 建立客户端连接
*/
@OnOpen
public void start(@PathParam("userId") String userId, Session session) {
this.session = session;
CopyOnWriteArraySet<WebSocketServer> connections = new CopyOnWriteArraySet<>();
if (connectionMap.containsKey(userId)) {
connections = connectionMap.get(userId);
}
connections.add(this);
connectionMap.put(userId, connections);
log.info("{}create connection", userId);
}
/**
* 关闭客户端连接
*/
@OnClose
public void end(@PathParam("userId") String userId) {
CopyOnWriteArraySet<WebSocketServer> connections = connectionMap.get(userId);
connections.remove(this);
if (CollectionUtils.isEmpty(connections)) {
connectionMap.remove(userId);
}
log.info("{}exit connection", userId);
}
/**
* 收到客户端消息后调用的方法
*/
@OnMessage
public void pushMsg(String message) {
log.info("收到客户端消息{}", message);
}
@OnError
public void onError(Throwable t) {
log.error("连接异常!{0}", t);
}
/**
* 发送消息
*
* @param msg 消息内容
*/
public static void broadcast(String userId, String msg) {
CopyOnWriteArraySet<WebSocketServer> connections = connectionMap.get(userId);
ReentrantLock lock = new ReentrantLock();
if(ObjectUtil.isNotEmpty(connections)){
connections.parallelStream().forEach(client -> {
if (client != null) {
try {
lock.lock();
client.session.getBasicRemote().sendText(msg);
} catch (IOException e) {
connections.remove(client);
try {
client.session.close();
} catch (IOException e1) {
e.printStackTrace();
}
String message = String.format("* %s", "disconnect");
broadcast(userId, message);
}finally {
lock.unlock();
}
}
});
}
}
实现Websocket节点之后,只需要调用broadcast方法即可向客户端发送信息