Bootstrap

SpringBoot集成ZMQ(ZeroMQ)(REQ-REP模式)

Maven依赖

<properties>
    <zeromq.version>0.5.2</zeromq.version>
</properties>

<!-- zeromq-->
<dependency>
    <groupId>org.zeromq</groupId>
    <artifactId>jeromq</artifactId>
    <version>${zeromq.version}</version>
</dependency>

Yml配置

# zeromq配置
zeromq:
  # 过滤开关
  server:
    enabled: true
  client:
    enabled: true
  serverHost: localhost
  serverPort: 5555

单客户端(Socket)非并发请求配置

package com.xxx.web.core.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.zeromq.SocketType;
import org.zeromq.ZMQ;

@Slf4j
@Configuration
public class ZeromqConfig {

    @Value("${zeromq.client.enabled:true}")
    private Boolean enabledClient;
    @Value("${zeromq.server.enabled:true}")
    private Boolean enabledServer;
    @Value("${zeromq.serverHost:localhost}")
    private String serverHost;
    @Value("${zeromq.serverPort:5555}")
    private Integer serverPort;

    @Bean(name = "zmqReqSocket")
    public ZMQ.Socket zmqReqSocket() {
        ZMQ.Socket socket = null;
        if (enabledClient) {
            ZMQ.Context context = ZMQ.context(1);
            socket = context.socket(SocketType.REQ);
            String uri = "tcp://" + serverHost + ":"+ serverPort;
            socket.connect(uri);

            log.info("zeromq connect to " + uri + " success");
        }
        return socket;
    }

    @Bean(name = "zmqRepSocket")
    public ZMQ.Socket zmqRepSocket() {
        ZMQ.Socket socket = null;
        if (enabledServer) {
            ZMQ.Context context = ZMQ.context(1);
            socket = context.socket(SocketType.REP);
            String uri = "tcp://" + serverHost + ":" + serverPort;
            socket.bind(uri);

            log.info("zeromq bind to " + uri + " success");
        }
        return socket;
    }
}

客户端

package com.xxx.web.service;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.zeromq.ZMQ;

import javax.annotation.Resource;

@Slf4j
@Service
public class ZmqService {

    @Resource
    private ZMQ.Socket zmqReqSocket;

    public String sendRequest(String request){
        try {
            // 发送请求
            zmqReqSocket.send(request.getBytes(), 0);
            // 接收响应
            byte[] response = zmqReqSocket.recv(0);
            return new String(response);
        }catch (Exception e){
            log.warn("zmq sendRequest error, e:{}", e);
        }
        return StringUtils.EMPTY;
    }
}

服务端

package com.xxx.runner;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.zeromq.ZMQ;

import javax.annotation.Resource;

/**
 * @author xxx.xxx
 * @description ZmqRepServerRunner 测试ZMQ使用,后续需要删除或者注释掉Component注解
 * @date 2024/10/17 15:42
 */
@Component
@Order(1)
@Slf4j
public class ZmqRepServerRunner implements ApplicationRunner {

    @Resource
    private ZMQ.Socket zmqRepSocket;

    @Override
    public void run(ApplicationArguments args) throws Exception {

        while (true) {
            // 等待接收消息
            byte[] request = zmqRepSocket.recv(0);
            String text = new String(request);
            log.info("接收到消息: " + text);
            // 返回消息
            Thread.sleep(20*1000); //延迟20s
            byte[] reply = ("你好,我是服务端。 原输入:"+text).getBytes();
            zmqRepSocket.send(reply, 0);
        }

    }
}

特性 / 问题

此种注入方式,只允许客户端非并发形式请求Zmq服务。

比如:

服务端设置了20s的延迟,模拟服务请求需要20s才能处理完成并回复响应。当发起第一个请求之后的20s内,如果有业务逻辑再调用ZmqService的sendRequest方法请求Zmq服务,则会报错,因为注入的是同一个Socket,而此Socket上一个请求还未完成。

多客户端并发请求配置

package com.xxx.web.core.config;


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.zeromq.SocketType;
import org.zeromq.ZMQ;

import javax.annotation.PostConstruct;

/**
 * @author xxx.xxx
 * @description Zeromq多实例配置
 * @date 2024/10/18 11:02
 */
@Slf4j
@Configuration
public class ZeromqMultConfig {

    @Value("${zeromq.client.enabled:true}")
    private Boolean enabledClient;

    @Value("${zeromq.server.enabled:true}")
    private Boolean enabledServer;

    @Value("${zeromq.serverHost:localhost}")
    private String serverHost;

    @Value("${zeromq.serverPort:5555}")
    private Integer serverPort;

    private ZMQ.Context zmqContext;

    @PostConstruct
    public void init() {
        // 初始化ZeroMQ上下文,这通常在整个应用程序生命周期内是唯一的
        zmqContext = ZMQ.context(1);
    }

    @Bean(name = "zmqReqSocketFactory")
    public ZMQSocketFactory zmqReqSocketFactory() {
        return new ZMQSocketFactory(zmqContext, enabledClient, serverHost, serverPort, SocketType.REQ);
    }

    @Bean(name = "zmqRepSocketFactory")
    public ZMQSocketFactory zmqRepSocketFactory() {
        return new ZMQSocketFactory(zmqContext, enabledServer, serverHost, serverPort, SocketType.REP);
    }

    // 工厂类,用于根据需要创建ZMQ.Socket实例
    public class ZMQSocketFactory {
        private final ZMQ.Context context;
        private final boolean enabled;
        private final String serverHost;
        private final int serverPort;
        private final SocketType socketType;

        public ZMQSocketFactory(ZMQ.Context context, boolean enabled, String serverHost, int serverPort, SocketType socketType) {
            this.context = context;
            this.enabled = enabled;
            this.serverHost = serverHost;
            this.serverPort = serverPort;
            this.socketType = socketType;
        }

        public ZMQ.Socket createSocket() {
            if (!enabled) {
                return null;
            }

            ZMQ.Socket socket = context.socket(socketType);
            String uri = "tcp://" + serverHost + ":" + serverPort;

            if (socketType == SocketType.REQ) {
                socket.connect(uri);
                log.info("ZeroMQ client connected to {}", uri);
            } else if (socketType == SocketType.REP) {
                socket.bind(uri);
                log.info("ZeroMQ server bound to {}", uri);
            }

            return socket;
        }
    }
}

客户端

package com.xxx.web.service;

import com.fdbatt.web.core.config.ZeromqMultConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.zeromq.ZMQ;

import javax.annotation.Resource;

@Slf4j
@Service
public class ZmqService {

    @Resource
    private ZeromqMultConfig.ZMQSocketFactory zmqReqSocketFactory;

    public String sendRequest(String request){
        ZMQ.Socket zmqReqSocket = null;
        try {
            zmqReqSocket = zmqReqSocketFactory.createSocket();
            // 发送请求
            zmqReqSocket.send(request.getBytes(), 0);
            // 接收响应
            byte[] response = zmqReqSocket.recv(0);
            return new String(response);
        }catch (Exception e){
            log.warn("zmq sendRequest error, e:{}", e);
        }finally {
            if(null != zmqReqSocket){
                zmqReqSocket.close();
            }
        }
        return StringUtils.EMPTY;
    }
}

服务端

跟前面第一种情况一样

package com.xxx.runner;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.zeromq.ZMQ;

import javax.annotation.Resource;

/**
 * @author xxx.xxx
 * @description ZmqRepServerRunner 测试ZMQ使用,后续需要删除或者注释掉Component注解
 * @date 2024/10/17 15:42
 */
@Component
@Order(1)
@Slf4j
public class ZmqRepServerRunner implements ApplicationRunner {

    @Resource
    private ZMQ.Socket zmqRepSocket;


    @Override
    public void run(ApplicationArguments args) throws Exception {

        while (true) {
            // 等待接收消息
            byte[] request = zmqRepSocket.recv(0);
            String text = new String(request);
            log.info("接收到消息: " + text);
            // 返回消息
            Thread.sleep(20*1000); //延迟20s
            byte[] reply = ("你好,我是服务端。 原输入:"+text).getBytes();
            zmqRepSocket.send(reply, 0);
        }

    }
}

特性 / 问题

此种注入方式,允许多客户端并发形式请求Zmq服务,并不会出现单客户端的报错问题。但由于多个客户端连接的是同一个服务,且服务端类型为REP,即同步阻塞的。因此虽然在前一个请求逻辑未完成时,再请求,不会再出现报错问题;但依然需要等待前一个请求完成后,方会执行当前的请求。

比如:

服务端设置了20s的延迟,模拟服务请求需要20s才能处理完成并回复响应。当发起第一个请求之后的第10s,业务逻辑再调用ZmqService的sendRequest方法请求Zmq服务发起第二个请求。之后再过10s,第一个请求已经经过了20s,就会返回响应结果。而第二个请求,此时才会开始处理,也就是在第一个20s后,需再经过20s。

也就是如下经过:

第一个请求触发    第0s

第二个请求触发    第10s

第一个请求响应    第20s

第二个请求响应    第40s

第二个请求,从触发到响应,经过了30s,而服务端只是20s延迟,是因为有10s是在等待服务器处理上一个请求。

;