소스코드를 기반으로 RabbitMQ 시뮬레이션 및 구현 - 클라이언트 연결 및 채널 구현을 위한 네트워크 통신 설계(완료)

목차

1. 클라이언트 코드 구현

1.1 수요분석

1.2 구체적인 구현

1) ConnectionFactory 구현

2) 연결 구현

3) 채널 구현

2. 데모 작성 

2.1 예 

2.1 예시 시연


1. 클라이언트 코드 구현


1.1 수요분석

RabbitMQ 클라이언트 설정: 클라이언트는 여러 모듈을 가질 수 있으며 각 모듈은 브로커 서버와 "논리적 연결"(채널)을 설정할 수 있습니다. 이러한 모듈의 채널은 서로 영향을 미치지 않습니다. 동시에 이러한 채널은 동일하게 재사용됩니다. TCP 연결 - 자주 TCP 연결을 설정/파기하는 오버헤드를 제거합니다(3개의 핸드셰이크, 4개의 웨이브...).

여기서는 주로 다음 세 가지 핵심 클래스를 포함하는 이 논리에 따라 메시지 대기열 클라이언트도 구현합니다.

  1. ConnectionFactory: 연결 팩토리. 이 클래스는 서버의 주소를 보유하며 주요 기능은 연결 개체를 생성하는 것입니다.
  2. 연결: TCP 연결을 나타내고 소켓 개체를 보유하며 요청 쓰기/응답 읽기에 사용되며 여러 채널 개체를 관리합니다.
  3. 채널: 논리적 연결을 나타내며 서버가 제공하는 핵심 API에 대응하는 일련의 메소드를 제공해야 합니다(내부적으로 클라이언트가 제공하는 이러한 메소드는 특정 요청을 작성한 후 서버의 응답을 기다립니다).

1.2 구체적인 구현

1) ConnectionFactory 구현

주로 연결 개체를 만드는 데 사용됩니다.

public class ConnectionFactory {

    //broker server 的 ip 地址
    private String host;
    //broker server 的端口号
    private int port;

//    //访问 broker server 的哪个虚拟主机
//    //这里暂时先不涉及
//    private String virtualHostName;
//    private String username;
//    private String password;

    public Connection newConnection() throws IOException {
        Connection connection = new Connection(host, port);
        return connection;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }
}

2) 연결 구현

속성은 다음과 같습니다

    private Socket socket;
    //一个 socket 连接需要管理多个 channel
    private ConcurrentHashMap<String, Channel> channelMap = new ConcurrentHashMap<>();
    private InputStream inputStream;
    private OutputStream outputStream;
    // DataXXX 主要用来 读取/写入 特定格式数据(例如 readInt())
    private DataInputStream dataInputStream;
    private DataOutputStream dataOutputStream;
    //用来处理 0xc 的回调,这里开销可能会很大,不希望把 Connection 阻塞住,因此使用 线程池 来处理
    private ExecutorService callbackPool;

구조는 다음과 같습니다

속성을 초기화해야 할 뿐만 아니라 스캐닝 스레드도 생성해야 하는데, 이 스레드는 소켓에서 응답 데이터를 지속적으로 읽어 해당 채널에 처리를 위해 응답 데이터를 전달하는 역할을 담당합니다.

    public Connection(String host, int port) throws IOException {
        socket = new Socket(host, port);
        inputStream = socket.getInputStream();
        outputStream = socket.getOutputStream();
        dataInputStream = new DataInputStream(inputStream);
        dataOutputStream = new DataOutputStream(outputStream);

        callbackPool = Executors.newFixedThreadPool(4);

        //创建一个扫描线程,由这个线程负责不停的从 socket 中读取响应数据,把这个响应数据再交给对应的 channel 负责处理
        Thread t = new Thread(() -> {
            try {
                while(!socket.isClosed()) {
                    Response response = readResponse();
                    dispatchResponse(response);
                }
            } catch (SocketException e) {
                //连接正常断开的,此时这个异常可以忽略
                System.out.println("[Connection] 连接正常断开!");
            } catch(IOException | ClassNotFoundException | MqException e) {
                System.out.println("[Connection] 连接异常断开!");
                e.printStackTrace();
            }
        });
        t.start();
    }

연결 해제 관련 리소스

    public void close() {
        try {
            callbackPool.shutdown();
            channelMap.clear();
            inputStream.close();
            outputStream.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

이 방법을 사용하면 현재 응답이 제어 요청에 대한 응답인지 서버에서 푸시한 메시지인지 구분할 수 있습니다.

서버가 푸시한 메시지인 경우 응답은 0xc를 나타내며 이는 콜백이며 스레드 풀을 통해 처리됩니다.

정상적인 응답인 경우 결과를 채널의 해시 테이블에 넣습니다. 그러면 채널은 맵에서 데이터를 가져오기 위해 응답을 기다리고 있는 차단된 모든 스레드를 깨울 것입니다.

    public void dispatchResponse(Response response) throws IOException, ClassNotFoundException, MqException {
        if(response.getType() == 0xc) {
            //服务器推送过来的消息数据
            SubScribeReturns subScribeReturns = (SubScribeReturns) BinaryTool.fromBytes(response.getPayload());
            //根据 channelId 找到对应的 channel 对象
            Channel channel = channelMap.get(subScribeReturns.getChannelId());
            if(channel == null) {
                throw new MqException("[Connection] 该消息对应的 channel 再客户端中不存在!channelId=" + channel.getChannelId());
            }
            //执行该 channel 对象内部的回调(这里的开销未知,有可能很大,同时不希望把这里阻塞住,所以使用线程池来执行)
            callbackPool.submit(() -> {
                try {
                    channel.getConsumer().handlerDelivery(subScribeReturns.getConsumerTag(), subScribeReturns.getBasicProperties(),
                            subScribeReturns.getBody());
                } catch(MqException | IOException e) {
                    e.printStackTrace();
                }
            });
        } else {
            //当前响应是针对刚才的控制请求的响应
            BasicReturns basicReturns = (BasicReturns) BinaryTool.fromBytes(response.getPayload());
            //把这个结果放到 channel 的 哈希表中
            Channel channel = channelMap.get(basicReturns.getChannelId());
            if(channel == null) {
                throw new MqException("[Connection] 该消息对应的 channel 在客户端中不存在!channelId=" + channel.getChannelId());
            }
            channel.putReturns(basicReturns);
        }
    }

요청 보내기 및 응답 읽기

    /**
     * 发送请求
     * @param request
     * @throws IOException
     */
    public void writeRequest(Request request) throws IOException {
        dataOutputStream.writeInt(request.getType());
        dataOutputStream.writeInt(request.getLength());
        dataOutputStream.write(request.getPayload());
        dataOutputStream.flush();
        System.out.println("[Connection] 发送请求!type=" + request.getType() + ", length=" + request.getLength());
    }

    /**
     * 读取响应
     */
    public Response readResponse() throws IOException {
        Response response = new Response();
        response.setType(dataInputStream.readInt());
        response.setLength(dataInputStream.readInt());
        byte[] payload = new byte[response.getLength()];
        int n = dataInputStream.read(payload);
        if(n != response.getLength()) {
            throw new IOException("读取的响应格式不完整! n=" + n + ", responseLen=" + response.getLength());
        }
        response.setPayload(payload);
        System.out.println("[Connection] 收到响应!type=" + response.getType() + ", length=" + response.getLength());
        return response;
    }

Connection에서 채널을 생성하는 방법 제공

    public Channel createChannel() throws IOException {
        String channelId = "C-" + UUID.randomUUID().toString();
        Channel channel = new Channel(channelId, this);
        //放到 Connection 管理的 channel 的 Map 集合中
        channelMap.put(channelId, channel);
        //同时也需要把 “创建channel” 这个消息告诉服务器
        boolean ok = channel.createChannel();
        if(!ok) {
            //如果创建失败,就说明这次创建 channel 操作不顺利
            //把刚才加入 hash 表的键值对再删了
            channelMap.remove(channelId);
            return null;
        }
        return channel;
    }

추신: UUID는 코드에서 여러 번 사용됩니다. 여기서는 이전과 같이 접두사를 사용하여 구별합니다.

3) 채널 구현

속성 및 구성은 다음과 같습니다

    private String channelId;
    // 当前这个 channel 是属于哪一个连接
    private Connection connection;
    //用来存储后续客户端收到的服务器响应,已经辨别是哪个响应(要对的上号) key 是 rid
    private ConcurrentHashMap<String, BasicReturns> basicReturnsMap = new ConcurrentHashMap<>();
    //如果当前 Channel 订阅了某个队列,就需要记录对应的回调是什么,当该队列消息返回回来的时候,调用回调
    //此处约定一个 Channel 只能有一个回调
    private Consumer consumer;

    public Channel(String channelId, Connection connection) {
        this.channelId = channelId;
        this.connection = connection;
    }

    public String getChannelId() {
        return channelId;
    }

    public void setChannelId(String channelId) {
        this.channelId = channelId;
    }

    public Connection getConnection() {
        return connection;
    }

    public void setConnection(Connection connection) {
        this.connection = connection;
    }

    public ConcurrentHashMap<String, BasicReturns> getBasicReturnsMap() {
        return basicReturnsMap;
    }

    public void setBasicReturnsMap(ConcurrentHashMap<String, BasicReturns> basicReturnsMap) {
        this.basicReturnsMap = basicReturnsMap;
    }

    public Consumer getConsumer() {
        return consumer;
    }

    public void setConsumer(Consumer consumer) {
        this.consumer = consumer;

채널을 생성하려면 0x1을 구현하세요.

가장 중요한 것은 요청을 구성한 다음 BrokerServer 서버에 요청을 보내고 서버 응답을 기다리는 것을 차단하는 것입니다.

    /**
     * 0x1
     * 和服务器进行交互,告诉服务器,此处客户端已经创建了新的 channel 了
     * @return
     */
    public boolean createChannel() throws IOException {
        //构造 payload
        BasicArguments arguments = new BasicArguments();
        arguments.setChannelId(channelId);
        arguments.setRid(generateRid());
        byte[] payload = BinaryTool.toBytes(arguments);
        //发送请求
        Request request = new Request();
        request.setType(0x1);
        request.setLength(payload.length);
        request.setPayload(payload);
        connection.writeRequest(request);

        //等待服务器响应
        BasicReturns basicReturns = waitResult(arguments.getRid());
        return basicReturns.isOk();
    }

    /**
     * 生成 rid
     * @return
     */
    public String generateRid() {
        return "R-" + UUID.randomUUID().toString();
    }


    /**
     * 阻塞等待服务器响应
     * @param rid
     * @return
     */
    private BasicReturns waitResult(String rid) {
        BasicReturns basicReturns = null;
        while((basicReturns = basicReturnsMap.get(rid)) == null) {
            //查询结果为空,就说明咱们去菜鸟驿站要取的包裹还没到
            //此时就需要阻塞等待
            synchronized (this) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        basicReturnsMap.remove(rid);
        return basicReturns;
    }


    /**
     * 由 Connection 中的方法调用,区分为普通响应之后触发
     * 将响应放回到 channel 管理的 map 中,并唤醒所有线程
     * @param basicReturns
     */
    public void putReturns(BasicReturns basicReturns) {
        basicReturnsMap.put(basicReturns.getRid(), basicReturns);
        synchronized (this) {
            //当前也不知道有多少线程再等待上述的这个响应
            //因此就把所有等待的线程唤醒
            notifyAll();
        }
    }

Ps: 다른 요청 연산은 0x1과 거의 동일합니다. 여기서는 하나씩 보여주지 않고 주로 0xa에 대해 이야기하겠습니다.

0xa 소비자는 대기열 메시지를 구독합니다. 여기서 콜백은 먼저 속성에 설정되어야 Connection이 이 속성을 통해 콜백을 처리할 수 있습니다.

ChannelId가 ConsumerTag라는 점에 동의한다는 점은 주목할 가치가 있습니다.

    public boolean basicConsume(String queueName, boolean autoAck, Consumer consumer) throws IOException, MqException {
        //先设置回调
        if(this.consumer != null) {
            throw new MqException("该 channel 已经设置过消费消息回调了,不能重复!");
        }
        this.consumer = consumer;
        BasicConsumeArguments basicConsumeArguments = new BasicConsumeArguments();
        basicConsumeArguments.setRid(generateRid());
        basicConsumeArguments.setChannelId(channelId);
        basicConsumeArguments.setConsumerTag(channelId); // 注意:此处的 consumerTag 使用 channelId 来表示
        basicConsumeArguments.setQueueName(queueName);
        basicConsumeArguments.setAutoAck(autoAck);
        byte[] payload = BinaryTool.toBytes(basicConsumeArguments);

        Request request = new Request();
        request.setType(0xa);
        request.setLength(payload.length);
        request.setPayload(payload);

        connection.writeRequest(request);
        BasicReturns basicReturns = waitResult(basicConsumeArguments.getRid());
        return basicReturns.isOk();
    }

2. 보충 : 콜백 실행 과정에 대해


프로세스는 다음과 같습니다.

  1. 클라이언트는 basicConsume 메서드를 호출하고, 대기열 메시지를 구독할 소비자를 생성하고, 소비자를 가져옵니다(소비자가 메시지를 받은 후 수행할 특정 작업).
  2. 채널은 클라이언트가 보낸 Consumer를 저장하고(실제로 콜백을 실행하기 위해 0xc 응답 수신을 대기) 구독 큐 메시지 요청(0xa)을 보내고 응답을 기다립니다.
  3. 요청을 받은 후 BrokerServer는 0xa를 구문 분석하고 소비자를 생성합니다. (목적은 서버가 메시지를 소비할 때까지 기다린 다음 메시지 데이터를 0xc 응답으로 패키징하는 것입니다. 클라이언트가 응답을 받은 후 "Consumer"라는 메시지를 받은 후 수행할 특정 작업을 실행합니다. 이것이 RabbitMQ의 설정입니다. 그런 다음 BrokerServer가 VirtualHost를 호출합니다.
  4. VirtualHost가 새로운 소비자 구독 대기열을 생성한 후 대기열에서 다른 메시지를 찾으면 즉시 이를 소비합니다.
  5. 구체적으로는 새로 생성된 콜백을 호출한 후 클라이언트에 0xc 응답을 반환하고, 클라이언트가 응답을 받은 후 "메시지를 받은 후 소비자가 수행할 특정 작업"을 실행합니다.

3. 데모 작성 


3.1 예 

현 시점에서는 호스트/서버 전반에 걸쳐 생산자-소비자 모델이 기본적으로 구현되었지만(일상적인 개발에서 메시지 큐 사용을 기능적으로 충족할 수 있음) 확장성도 강력합니다. 계속 RabbitMQ를 참조하면 됩니다 . 아이디어가 있거나 모르는 문제가 발생하면 개인 메시지를 보내주세요~

다음으로 호스트/서버 전체에서 생산자-소비자 모델을 시뮬레이션하는 데모를 작성하겠습니다(편의상 이 시스템에서 시연하겠습니다).

먼저 스프링 부트 프로젝트의 시작 클래스에서 BrokerServer를 생성하고 포트 번호를 바인딩한 후 시작합니다.

@SpringBootApplication
public class RabbitmqProjectApplication {
    public static ConfigurableApplicationContext context;
    public static void main(String[] args) throws IOException {
        context = SpringApplication.run(RabbitmqProjectApplication.class, args);
        BrokerServer brokerServer = new BrokerServer(9090);
        brokerServer.start();
    }

}

소비자 쓰기

public class DemoConsumer {

    public static void main(String[] args) throws IOException, MqException, InterruptedException {
        //建立连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setPort(9090);
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        //创建交换机和队列(这里和生产者创建交换机和队列不冲突,谁先启动,就按照谁的创建,即使已经存在交换机和队列,再创建也不会有什么副作用)
        channel.exchangeDeclare("demoExchange", ExchangeType.DIRECT, true, false, null);
        channel.queueDeclare("demoQueue", true, false, false, null);

        //消费者消费消息
        channel.basicConsume("demoQueue", true, new Consumer() {
            @Override
            public void handlerDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException {
                System.out.println("开销消费");
                System.out.println("consumerTag=" + consumerTag);
                System.out.println("body=" + new String(body));
                System.out.println("消费完毕");
            }
        });

        //由于消费者不知道生产者要生产多少,就在这里通过循环模拟一直等待
        while(true) {
            Thread.sleep(500);
        }
    }

}

쓰기 생산자

public class DemoProducer {

    public static void main(String[] args) throws IOException, InterruptedException {
        //建立连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setPort(9090);
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        //创建交换机和队列(这里和消费者创建交换机和队列不冲突,谁先启动,就按照谁的创建,即使已经存在交换机和队列,再创建也不会有什么副作用)
        channel.exchangeDeclare("demoExchange", ExchangeType.DIRECT, true, false, null);
        channel.queueDeclare("demoQueue", true, false, false, null);

        //生产消息
        byte[] body1 = "Im cyk1 !".getBytes();
        channel.basicPublish("demoExchange", "demoQueue", null, body1);

        Thread.sleep(500);

        //关闭连接
        channel.close();
        connection.close();
    }

}

3.2 예시 시연

스프링 부트 프로젝트 시작(BrokerServer 시작)

소비자 실행(소비자 또는 생산자 중 하나를 순차적으로 실행할 수 있음)

생산자 실행

추천

출처blog.csdn.net/CYK_byte/article/details/132466715