Socket和WebSocket实战实现三端实时通信

315 阅读6分钟

需求背景

对于不同的文件类型,需要不同的解析器来解析文件,但解析文件的过程是在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);
    }

具体可以参考文章:

blog.csdn.net/qq_62015542…

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