需求背景
对于不同的文件类型,需要不同的解析器来解析文件,但解析文件的过程是在Python端实现的,作为Java选手,只能去调用Python的接口来实现文件的上传,同时需要实时获取文件解析的状态,来给用户一个正面的反馈。
技术选型
由于业务原因需要实时的获取文件的解析状态,因此选用实时通信的协议,和前端通信选用的是webSocket、和python端通信使用的是socket;同时在两种协议交互的中间采用事件监听的方式实现,这里有多种方式实现。下面是具体的业务逻辑图。
WebSocket简单使用
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>your-version</version>
</dependency>
基本代码实现
配置类实现:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
/**
* WebSocket服务
*/
@Component
@Slf4j
@ServerEndpoint("/ws/connectWs/{sid}")
public class WebSocketServer {
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
try {
session.getBasicRemote().sendText("Echo: " + message);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
}
这样前端建立websocket连接就可以了,或者新建一个Java的客户端也可以建立连接。
前端简单连接:
if('WebSocket' in window){
//连接WebSocket节点
websocket = new WebSocket("ws://localhost:8080/ws/connectWs/"+clientId);
}
具体可以参考文章:
Socket简单实现
通过以下代码就可以轻松实现Socket通信。
服务端
@Test
public void testSocketServer(){
int port = 12346;
try (ServerSocket serverSocket = new ServerSocket(port)) {
while (true) {
System.out.println("Server started, waiting for connection...");
try (Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream()), true)) {
System.out.println("Connected to client");
String message;
while ((message = in.readLine()) != null) {
System.out.println("PythonReceived message: " + message);
String response = "Python回复的消息: " + message;
out.println(response);
}
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
客户端
@Test
public void testSocketClient() {
String hostname = "localhost";
int port = 12346;
try (Socket socket = new Socket(hostname, port)) {
OutputStream output = socket.getOutputStream();
BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String message = "Hello Server";
output.write((message + "\n").getBytes());
String response = input.readLine();
System.out.println("Server response: " + response);
} catch (Exception e) {
e.printStackTrace();
}
}
事件模板
首先需要定义一个在事件中传输的对象,也就是事件类
public class MessageEvent extends EventObject {
private final String message;
public MessageEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}
事件监听器接口
public interface MessageEventListener extends EventListener {
void onMessageReceived(MessageEvent event);
}
创建事件监听器
@Component
@Slf4j
public class MessageEventListenerImpl implements MessageEventListener {
@Override
public void onMessageReceived(@NotNull MessageEvent event) {
String message = event.getMessage();
log.info("User ID: {}, Message received: ", message);
// 处理接收到的数据
}
}
事件管理器
import org.springframework.stereotype.Component;
import java.util.concurrent.CopyOnWriteArrayList;
@Component
public class EventPublisher {
private final CopyOnWriteArrayList<MessageEventListener> listeners = new CopyOnWriteArrayList<>();
// 注册事件监听器
public void addMessageEventListener(MessageEventListener listener) {
listeners.add(listener);
}
// 注销事件监听器
public void removeMessageEventListener(MessageEventListener listener) {
listeners.remove(listener);
}
// 触发事件
public void fireMessageEvent(MessageEvent event) {
for (MessageEventListener listener : listeners) {
listener.onMessageReceived(event);
}
}
}
项目初始化的操作
@Component
@Slf4j
public class ReceiveEventRunner implements CommandLineRunner {
@Autowired
private MessageEventListener messageEventListener;
@Autowired
private EventPublisher eventPublisher;
@Override
public void run(String... args) throws Exception {
log.info("初始化监听服务器发送事件》》》》》》》》》》》》》");
eventPublisher.addMessageEventListener(messageEventListener);
}
}
项目实战
socket代码
由于项目连接python是作为客户端的因此具体的代码如下:详细解释也如下
import com.whj.event.EventPublisher;
import com.whj.event.MessageEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
@Service
@Slf4j
public class NioClientService {
// 采用selector 保证多个信道不会发生阻塞的情况
private Selector selector;
private final EventPublisher eventPublisher;
private StringBuilder partialMessage = new StringBuilder();
private static final Charset CHARSET = StandardCharsets.UTF_8;
@Autowired
public NioClientService(EventPublisher eventPublisher) {
// 初始化事件管理器
this.eventPublisher = eventPublisher;
}
public void startClient(String message) {
while (true) {
try {
connectAndProcess(message);
} catch (Exception e) {
log.warn("socket连接失败{},正在尝试重连", e.getMessage());
try {
// 为了防止消耗资源,socket断开连接以后休眠5s继续重连
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
}
}
}
private void connectAndProcess(String message) throws IOException {
try {
selector = Selector.open();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
// 开始进行连接,IP地址和端口应该使用配置文件实现
socketChannel.connect(new InetSocketAddress("2.8.18.4", 8879));
socketChannel.register(selector, SelectionKey.OP_CONNECT, message);
log.info("Client connected to server : {}", message);
while (true) {
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
if (key.isConnectable()) {
connect(key);
} else if (key.isReadable()) {
read(key);
} else if (key.isWritable()) {
write((SocketChannel) key.channel(), message);
}
}
}
} catch (IOException e) {
log.warn("Connection lost, retrying...", e);
// 重试连接前等待一段时间
try {
Thread.sleep(5000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
private void connect(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
if (socketChannel.isConnectionPending()) {
// 确定是保持连接的状态
socketChannel.finishConnect();
log.info("Connection python finished");
}
// SocketChannel为非阻塞模式(non-blocking mode).设置之后,就可以在异步模式下调用connect(),read()和write()了。
socketChannel.configureBlocking(false);
// 改变读写状态
socketChannel.register(selector, SelectionKey.OP_WRITE, key.attachment());
}
private void read(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
// 读取的数据放入缓存
ByteBuffer buffer = ByteBuffer.allocate(256);
int numRead = socketChannel.read(buffer);
if (numRead == -1) {
socketChannel.close();
return;
}
buffer.flip();
// 解决中文乱码问题, 同时需要保证服务器发送的字符相同,我这里是UTF-8的字符编码格式
String receivedData = CHARSET.decode(buffer).toString();
buffer.clear();
partialMessage.append(receivedData);
int endOfMessage;
// 根据”\n” 来区分一条消息是否发送完毕,解决TCP粘包问题
while ((endOfMessage = partialMessage.indexOf("\n")) != -1) {
String message = partialMessage.substring(0, endOfMessage).trim();
log.info("Received from server: " + message);
eventPublisher.fireMessageEvent(new MessageEvent(this, message));
partialMessage.delete(0, endOfMessage + 1);
}
// 这里可以选择打开还是关闭,关闭就意味着,通信不会断开.在我的需求中就选择关闭
//socketChannel.close();
}
private void write(SocketChannel socketChannel, String message) throws IOException {
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
// 写数据
socketChannel.write(buffer);
// 改变模式
socketChannel.register(selector, SelectionKey.OP_READ);
}
}
那么什么时候建立连接呢?
由于只有一个服务端和客户端,那么在项目启动的时候建立连接是最合适的,因此需要利用接口CommandLineRunner在项目启动的时候就开启连接。
@Component
@Slf4j
public class ConnectSeverRunner implements CommandLineRunner {
@Autowired
private NioClientService nioClientService;
@Override
public void run(String... args) throws Exception {
// 异步需要新开线程
new Thread(() -> {
log.info("开始连接socket》》》》》》》》》》》》》》");
nioClientService.startClient("开始连接socket》》》》》》》》》》》》》》》");
}).start();
}
}
websocket代码
注意需要加载WebSocketConfiguration的配置信息
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import com.whj.model.User;
/**
* WebSocket服务
*/
@Component
@Slf4j
@ServerEndpoint("/ws/connectWs")
public class WebSocketServer {
/**
* Session 缓存,因为存在同一个用户打开多个窗口的情况,所以一个用户可能存在多个session,KEY:username value:List<Server>
*/
private static Map<String, List<WebSocketServer>> sessionMap = new ConcurrentHashMap<>();
private Session session;
// 这里是用户的登录信息
private static final UserService userService;
static {
userService = new UserService();
}
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
String userName = session.getUserPrincipal().getName();
User user = userService.getUserByName(userName);
log.debug("New webSocket connect add!:{}--{}", userName, user.getId());
if (null != sessionMap.get(user.getUserName())) {
List<WebSocketServer> serverList = sessionMap.get(user.getUserName());
serverList.add(this);
} else {
List<WebSocketServer> serverList = new ArrayList<>();
serverList.add(this);
sessionMap.put(user.getUserName(), serverList);
}
sendMessage(session, "connect success!!");
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.debug("New webSocket message :{}", message);
JSONObject messageJson = JSONObject.parseObject(message, JSONObject.class);
sendMessage(session, messageJson.getString("message"));
}
@OnClose
public void onClose() {
log.debug("New webSocket close :{}", session.getId());
removeSession(this.session);
}
/**
* 关闭连接,删除该session下的server
*
* @param session 当前session
*/
private void removeSession(Session session) {
User user = userService.getUserByName(session.getUserPrincipal().getName());
List<WebSocketServer> serverList = sessionMap.get(user.getUserName());
for (int i = 0; i < serverList.size(); i++) {
if (serverList.get(i).session == session) {
serverList.remove(i);
break;
}
}
if (serverList.isEmpty()) {
sessionMap.remove(user.getUserName());
}
}
/**
* 根据session发送的接口
*
* @param session
* @param msg
*/
private static void sendMessage(Session session, String msg) {
try {
session.getAsyncRemote().sendText(msg);
log.info("send message success to websocket:{}!!", session.getUserPrincipal().getName());
} catch (Exception e) {
log.error("send message error:{}", e.getMessage());
}
}
/**
* 根据用户名给指定的用户发送信息
*
* @param userName
* @param msg
*/
public void sendMessage(String userName, String msg) {
try {
List<WebSocketServer> serverList = sessionMap.get(userName);
for (WebSocketServer server : serverList) {
sendMessage(server.session, msg);
log.info("send message success to websocket:{}!!", userName);
}
} catch (Exception e) {
log.error("send message error:{}", e.getMessage());
}
}
/**
* 广播信息给所有当前用户在线的浏览器
*
* @param msg
*/
public void broadcastMessage(String msg) {
for (Map.Entry<String, List<WebSocketServer>> entry : sessionMap.entrySet()) {
List<WebSocketServer> serverList = entry.getValue();
if (null == serverList || serverList.isEmpty()) continue;
for (WebSocketServer server : serverList) {
sendMessage(server.session, msg);
}
}
}
}
SocketChannel 介绍可以参考这个文章:zhuanlan.zhihu.com/p/489248536