根据源码,模拟实现 RabbitMQ - 网络通讯设计,实现客户端Connection、Channel(完结)

目录

一、客户端代码实现

1.1、需求分析

1.2、具体实现

1)实现 ConnectionFactory

2)实现 Connection

3)实现 Channel

二、补充1:关于回调执行流程

三、补充2:深刻理解 RPC 远程通信原理

3.1、再谈自定义应用层协议

3.2、再谈 BrokerServer

3.3、再谈 Connection、Channel

四、编写 Demo 

3.1、实例 

3.2、实例演示


一、客户端代码实现


1.1、需求分析

RabbitMQ 的客户端设定:一个客户端可以有多个模块(不同的业务,主要是为了解耦),每个模块都可以和 broker server 之间建立 “逻辑上的连接” (channel),这几个模块的channel 彼此之间是互相不影响的,同时这几个 channel 又复用的同一个 TCP 连接,省去了频繁 建立/销毁 TCP 连接的开销(三次握手、四次挥手......).

这里,我们也按照这样的逻辑实现 消息队列 的客户端,主要涉及到以下三个核心类:

  1. ConnectionFactory:连接工厂,这个类持有服务器的地址,主要功能就是创建 Connection 对象.
  2. Connection:表示一个 TCP连接,持有 Socket 对象,用来 写入请求/读取响应,管理多个Channel 对象.
  3. Channel:表示一个逻辑上的连接,需要提供一系列的方法,去和服务器提供的核心 API 对应(客户端提供的这些方法的内部,就是写入了一个特定的请求,然后等待服务器响应).

1.2、具体实现

1)实现 ConnectionFactory

主要用来创建 Connection 对象.

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)实现 Connection

属性如下

    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;

构造如下

这里不光需要初始化属性,还需要创建一个扫描线程,由这个线程负责不停的从 socket 中读取响应数据,把这个响应数据再交给对应的 channel 负责处理

    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();
    }

释放 Connection 相关资源

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

使用这个方法来区别,当前的响应是一个针对控制请求的响应,还是服务器推送过来的消息.

如果是服务器推送过来的消息,就响应表明是 0xc,也就是一个回调,通过线程池来进行处理;

如果只是一个普通的响应,就把这个结果放到 channel 的 哈希表中(随后 channel 会唤醒所有阻塞等待响应的线程,去 map 中拿数据).

    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 中提供创建 Channel 的方法

    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;
    }

Ps:代码中使用了很多次 UUID ,这里我们和之前一样,使用加前缀的方式来进行区分.

3)实现 Channel

属性和构造如下

    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 创建 channel

主要就是构造构造出 request,然后发送请求到 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();
    }

二、补充1:关于回调执行流程


流程如下:

  1. 客户端调用 basicConsume 方法,创建一个消费者订阅队列消息,并带上 Consumer(消费者拿到消息之后具体要做的动作);
  2. Channel 保存了客户端发来的 Consumer(等待接收 0xc 响应,真正执行回调),发送订阅队列消息请求(0xa),并等待响应
  3. BrokerServer 接收到请求后,解析出 0xa ,又创建了一个 Consumer(目的是为了等服务器拿到要消费的消息后,将消息的数据包装成 0xc 的响应,客户端接收到响应之后,执行 “消费者拿到消息后具体要做的动作”  这是 RabbitMQ 的设定),接着 BrokerServer 调用 VirtualHost.
  4. VirtualHost 创建一个新的消费者订阅队列后,如果发现队列中又消息,立即进行消费
  5. 具体的,就是调用刚刚新创建的回调,然后向客户端返回 0xc 的响应,客户端接收到响应之后,执行 “消费者拿到消息后具体要做的动作”.

三、补充2:深刻理解 RPC 远程通信原理

3.1、再谈自定义应用层协议

a)这个自定义应用层协议实际上就是在描述将来 客户端 和 服务器 之间通讯的消息格式长啥样

b)首先是一个 Int 类型的 type,描述了这个消息到底是用来干什么的(要调用服务器这边的哪一个服务).

c)然后就是 payload 的数据载荷,承载着将来调用 VirtualHost 中的具体的服务所需要的参数(例如创建交换机所需要的参数就有:交换机名字、交换机类型、是否自动删除、是否持久化、扩展参数).

因为 TCP 是面向字节流的(IO 流中主要提供的就是二进制数据的读写),因此这里不太适合使用 JSON 格式数据进行网络传输(可读性不好,效率不高),因此这里 payload 是一个 字节数组,将具体的数据序列化成 byte 数组放进来.

d)这里要注意的一点是,TCP 是面向字节流的,因此会出现粘包问题,那么为了解决这个问题,由两种办法,第一种就是约定分割符(读到指定分隔符就截止),第二种就是描述好 payload 的长度.

这里我采用的就是第二种办法,只需要在协议里面在添加一个 length 字段,用来描述 payload 的长度.

import java.io.Serializable

//Socket 自定义应用层协议(请求)
data class Request(
    val type: Int,
    val length: Int,
    val payload: ByteArray,
): Serializable

//Socket 自定义应用层协议(响应)
data class Response(
    val type: Int,
    val length: Int,
    val payload: ByteArray,
): Serializable

//基本参数(每个请求都会携带的参数,这里进行了一个封住)
open class ReqBaseArguments(
    open val rid: String = "",
    open val channelId: String = "",
): Serializable

//基本响应参数(每个响应都会携带的参数),主要是为了应对 mq 回调响应处理
open class RespBaseArguments(
    open val rid: String,
    open val channelId: String,
    open val ok: Boolean,
): Serializable

//主要的请求: 创建交换机、删除交换机、创建队列
data class ExchangeDeclareReq(
    val name: String,
    val type: ExchangeType,
    val durable: Boolean,
    val autoDelete: Boolean,
    val arguments: MutableMap<String, Any>,
    override val rid: String,
    override val channelId: String,
): ReqBaseArguments(), Serializable

data class ExchangeDeleteReq(
    val name: String,
    override val rid: String,
    override val channelId: String,
): ReqBaseArguments(), Serializable

data class QueueDeclareReq(
    val name: String,
    val durable: Boolean,
    val exclusive: Boolean,
    val autoDelete: Boolean,
    val arguments: MutableMap<String, Any>,
    override val rid: String,
    override val channelId: String,
): ReqBaseArguments(), Serializable

3.2、再谈 BrokerServer

a)BrokerServer 就是一个中间服务,也可以简单理解为 VirtualHost 的代理(BrokerServer 接收客户端请求,调用 VirtualHost 中具体的服务).

b)BrokerServer 启动的时候,就会通过 accept 阻塞等待客户端这边的 TCP 连接,连接成功之后只需要为该客户端其分配一个线程,处理之后的任务.

c)此时这个线程就会处于一个死循环循环,通过 IO 流读取到 客户端 请求中的 type、length、payload ,并按照约定的格式进行解析 payload,得到具体数据(这里不仅包含了 VirtualHost 服务中所需要的具体的参数,还携带了 channelId 和 rid)

d)此时,只需要根据 IO 流中读取出的 type,调用对应 VirtualHost 中的服务即可.

e)最后再将 VirtualHost 处理后得到的响应封装成 我们约定的应用层协议格式,通过 IO 写入到流中,让客户端去读取.

class BrokerServer(
    port: Int
) {

    private val socket = ServerSocket(port)
    private val clientPool = Executors.newFixedThreadPool(5)

    //key: channelId ,value: Socket
    //注意:这里的 Channel 只表示一个 "逻辑" 上的连接(创建,销毁 channel),这个 Map 是为了后台信息统计
    private val channelSession = ConcurrentHashMap<String, Socket>()

    private val virtualHost = VirtualHost()

    fun start() {
        println("[BrokerServer] 启动!")
        while (true) {
            val client = socket.accept()
            clientPool.submit {
                clientProcess(client)
            }
        }
    }

    private fun clientProcess(client: Socket) {
        println("[BrokerServer] 客户端上线!ip: ${client.inetAddress}, port: ${client.port}")
        try {
            client.getInputStream().use { inputStream ->
                client.getOutputStream().use { outputStream ->
                    DataInputStream(inputStream).use { dataInputStream ->
                        DataOutputStream(outputStream).use { dataOutputStream ->
                            while (true) {
                                val request = readRequest(dataInputStream)
                                val response = process(request, client)
                                writeResponse(response, dataOutputStream)
                            }
                        }
                    }
                }
            }
        } catch (e: EOFException) {
            println("[BrokerServer] 客户端正常下线!ip: ${client.inetAddress}, port: ${client.port}")
        } catch (e: Exception) {
            println("[BrokerServer] 客户端连接异常!ip: ${client.inetAddress}, port: ${client.port}")
        } finally {
            client.close()
            removeChannelSession(client)
        }
    }

    private fun process(request: Request, client: Socket) = with(request) {
        //1.解析请求
        val req = BinaryTool.bytesToAny(payload)
        //2.获取请求中的 channelId,记录和 Socket 的关系(让每个 channel 都对应自己的 Socket,类似于 Session)
        val reqBase = req as ReqBaseArguments
        //3.根据 type 类型执行不同的服务(创建 Channel、销毁 Channel、创建交换机、删除交换机...)
        val ok = when(type) {
            1 -> {
                channelSession[reqBase.channelId] = client
                println("[BrokerServer] channel 创建成功!channelId: ${reqBase.channelId}")
                true
            }
            2 -> {
                channelSession.remove(reqBase.channelId)
                println("[BrokerServer] channel 销毁成功!channelId: ${reqBase.channelId}")
                true
            }
            3 -> virtualHost.exchangeDeclare(req as ExchangeDeclareReq)
            4 -> virtualHost.exchangeDelete(req as ExchangeDeleteReq)
            5 -> virtualHost.queueDeclare(req as QueueDeclareReq)
            //...
            else -> throw RuntimeException("[BrokerServer] 客户端请求 type 非法!type: $type")
        }
        //4.返回响应
        val respBase = RespBaseArguments(reqBase.rid, reqBase.channelId, ok)
        val payload = BinaryTool.anyToBytes(respBase)
        Response(type, payload.size, payload)
    }

    /**
     * 读取客户端请求
     * 使用 DataInputStream 的主要原因就是有多种读取方式,例如 readInt()、readLong(),这些都是原生 InputStream 没有的
     */
    private fun readRequest(dataInputStream: DataInputStream) = with(dataInputStream) {
        val type = readInt()
        val length = readInt()
        val payload = ByteArray(length)
        val n = read(payload)
        if (n != length) throw RuntimeException("[BrokerServer] 读取客户端请求异常!")
        Request(type, length, payload)
    }

    /**
     * 将响应写回给客户端
     */
    private fun writeResponse(response: Response, outputStream: DataOutputStream) = with(outputStream) {
        writeInt(response.type)
        writeInt(response.length)
        write(response.payload)
        flush()
    }

    //删除所有和这个 clientSocket 有关的 Channel
    private fun removeChannelSession(client: Socket) {
        val channelIdList = mutableListOf<String>()
        //这里不能直接删除,会破坏迭代器结构
        for (entry in channelSession) {
            if (entry.value == client) channelIdList.add(entry.key)
        }
        for (channelId in channelIdList) {
            channelSession.remove(channelId)
        }
    }

}

 

class VirtualHost {

    fun exchangeDeclare(req: ExchangeDeclareReq): Boolean {
        //执行业务逻辑
        //...
        println("[VirtualHost] 创建交换机成功!")
        return true
    }

    fun exchangeDelete(req: ExchangeDeleteReq): Boolean {
        //执行业务逻辑
        //...
        println("[VirtualHost] 删除交换机成功!")
        return true
    }

    fun queueDeclare(req: QueueDeclareReq): Boolean {
        //执行业务逻辑
        //...
        println("[VirtualHost] 创建队列成功!")
        return true
    }

}

3.3、再谈 Connection、Channel

a)一个 Connection 就是一个 TCP 连接,因此频繁 建立/断开连接(三次握手、四次挥手...)的开销也是相当大的,因此就引入了 Channel. 

b)一个 Connection 下可以有多个 Channel(此处使用 map 来维护).  Channel 只是简单的表示一个逻辑上的连接,可以理解为一个大的项目下被拆分成的多个小的微服务. 实现了 TCP 连接的复用.

c)起初,我们需要先创建出 Connection 与服务端建立连接,初始化构造中只需要写一个死循环,不断的从服务端这边读取响应.

d)接着,通过 Connection 创建出 Channel 来完成具体的业务(Channel 中就提供了一系列方法,就像调用本地的方法一样,调用到远程服务器的接口).

e)例如 Channel 中提供的创建叫交换机方法(channel.exchangeDeclare(...)),这个方法中具体要做的就是将传入的参数,封装到一个对象中,序列化成 二进制 数据,这就是将来协议中要传输的 payload.   进一步的,协议 Request 就构造出来了,通过 IO 写到流中,供服务端读取.

d)为了能够让每次请求和响应都能对的上,Channel 这里我维护了一个 map(key 是 rid、value 是具体的响应),客户端和服务端之间的每个请求和响应都会携带上这个 rid 这个参数,这样将来 Connection 客户端接受到响应的时候,就可以直接把 响应中的 rid 提取出来,交给 Channel 的 map 中(响应来之前,Channel 一直阻塞等待,直到响应来了 -> 能通过 rid  从 map 中得到).

class ConnectionFactory(
    private val host: String,
    private val port: Int,
) {

    fun newConnection() = Connection(host, port)

}
class Connection(
    ip: String,
    port: Int,
) {

    private val socket = Socket(ip, port)
    private val channelMap = ConcurrentHashMap<String, Channel>()
    //下述这样提前创建好,是为了将来 Channel 在读写请求的时候的方便(Channel 就不用获取输入输出流了)
    private val inputStream = socket.getInputStream()
    private val outputStream = socket.getOutputStream()
    private val dataInputStream = DataInputStream(inputStream)
    private val dataOutputStream = DataOutputStream(outputStream)

    init {
        //此线程负责不停的从服务器这边获取响应
         Thread {
             try {
                 while (!socket.isClosed) {
                     //读取服务器响应
                     val resp = readResp()
                     //将响应交给对应的 Channel
                     putRespToChannel(resp)
                 }
             } catch (e: SocketException) {
                 println("[Connection] 客户端正常断开连接")
             } catch (e: Exception) {
                 println("[Connection] 客户端异常断开连接")
                 e.printStackTrace()
             }
         }.start()
    }


    /**
     * 将客户端 Connection 接收到的请求,交给对应的 Channel 处理(此时 Channel 还在阻塞等待服务端响应)
     */
    private fun putRespToChannel(resp: Response) {
        //这里由于不涉及回调,所以每个 type 类型的响应都长一样,就按照一样的方式解析了
        val baseResp = BinaryTool.bytesToAny(resp.payload) as RespBaseArguments
        val channel = channelMap[baseResp.channelId]
            ?: throw RuntimeException("[Connection] 该响应对应的 Channel 不存在!channelId: ${baseResp.channelId}")
        //将响应交给 Channel
        channel.notifyResp(baseResp)
    }

    /**
     * 创建 Channel
     */
    fun createChannel(): Channel { //1.创建 Channel,保存到 map 种
        val channelId = "C-${UUID.randomUUID()}"
        val channel = Channel(channelId, this)
        channelMap[channelId] = channel
        //2.告知服务端 Channel 创建
        val ok = channel.createChannel()
        //3.如果 Channel 创建不成功,客户端这边也应该要删除对应的 Channel 信息
        if (!ok) channelMap.remove(channelId)
        return channel
    }

    private fun readResp() = with(dataInputStream) {
        val type = readInt()
        val length = readInt()
        val payload = ByteArray(length)
        val n = read(payload)
        if (n != length) throw RuntimeException("[Connection] 客户端读取响应异常!")
        Response(type, length, payload)
    }

    fun writeReq(request: Request) = with(dataOutputStream) {
        writeInt(request.type)
        writeInt(request.length)
        write(request.payload)
        flush()
    }

}
class Channel(
    private val channelId: String,
    private val connection: Connection,  //自己当前属于哪个 Channel
) {

    //key: rid(为了能让每个 Channel 对应上自己的响应)
    //value: RespBaseArguments(具体的响应)
    //当 Connection 的扫描线程接收到响应之后,就会将响应传给这个 map
    private val ridRespMap = ConcurrentHashMap<String, RespBaseArguments>()
    //这个锁是用来阻塞等待服务端响应的(避免轮询),当服务端传来响应时,Connection 就会唤醒锁
    private val locker = Object()

    private fun generateRid() = "R-${UUID.randomUUID()}"

    private fun waitResp(rid: String): RespBaseArguments {
        val respBase: RespBaseArguments
        while (ridRespMap[rid] == null) { // 如果为空,说明此时服务端还没有传来响应
            synchronized(locker) { //为了避免轮询,就让其阻塞等待
                locker.wait()
            }
        }
        //出了这个循环,那么 ridRespMap[rid] 一定不为空
        return ridRespMap[rid]!!
    }

    fun notifyResp(respBase: RespBaseArguments) {
        ridRespMap[respBase.rid] = respBase
        synchronized(locker) {
            //当前也不直到有多少线程在等待响应,就全部唤醒
            locker.notifyAll()
        }
    }

    /**
     * 创建 Channel
     */
    fun createChannel(): Boolean {
        //1.创建基本请求
        val reqBase = ReqBaseArguments(
            rid = generateRid(),
            channelId = channelId
        )
        //2.构造 TCP 通信请求
        val payload = BinaryTool.anyToBytes(reqBase)
        val req = Request(
            type = 1,
            length = payload.size,
            payload = payload
        )
        //3.发送请求
        connection.writeReq(req)
        //4.等待客户端响应
        val respBase = waitResp(reqBase.rid)
        return respBase.ok
    }

    fun removeChannel(): Boolean {
        //1.创建基本请求
        val reqBase = ReqBaseArguments(
            rid = generateRid(),
            channelId = channelId
        )
        //2.构造 TCP 通信请求
        val payload = BinaryTool.anyToBytes(reqBase)
        val req = Request(
            type = 2,
            length = payload.size,
            payload = payload
        )
        //3.发送请求
        connection.writeReq(req)
        //4.等待客户端响应
        val respBase = waitResp(reqBase.rid)
        return respBase.ok
    }

    fun exchangeDeclare(
        name: String,
        type: ExchangeType,
        durable: Boolean,
        autoDelete: Boolean,
        arguments: MutableMap<String, Any>,
    ): Boolean {
        val exchangeDeclareReq = ExchangeDeclareReq(
            name = name,
            type = type,
            durable = durable,
            autoDelete = autoDelete,
            arguments = arguments,
            rid = generateRid(),
            channelId = channelId,
        )
        val payload = BinaryTool.anyToBytes(exchangeDeclareReq)
        val req = Request(
            type = 3,
            length = payload.size,
            payload = payload,
        )
        connection.writeReq(req)
        val respBase = waitResp(exchangeDeclareReq.rid)
        return respBase.ok
    }

    fun exchangeDelete(name: String): Boolean {
        val exchangeDeleteReq = ExchangeDeleteReq(
            name = name,
            rid = generateRid(),
            channelId = channelId,
        )
        val payload = BinaryTool.anyToBytes(exchangeDeleteReq)
        val req = Request(
            type = 4,
            length = payload.size,
            payload = payload,
        )
        connection.writeReq(req)
        val respBase = waitResp(exchangeDeleteReq.rid)
        return respBase.ok
    }

    fun queueDeclare(
        name: String,
        durable: Boolean,
        exclusive: Boolean,
        autoDelete: Boolean,
        arguments: MutableMap<String, Any>,
    ): Boolean {
        val queueDeclareReq = QueueDeclareReq(
            name = name,
            durable = durable,
            exclusive = exclusive,
            autoDelete = autoDelete,
            arguments = arguments,
            rid = generateRid(),
            channelId = channelId,
        )
        val payload = BinaryTool.anyToBytes(queueDeclareReq)
        val req = Request(
            type = 5,
            length = payload.size,
            payload = payload,
        )
        connection.writeReq(req)
        val resp = waitResp(queueDeclareReq.rid)
        return resp.ok
    }

}

四、编写 Demo 


3.1、实例 

到了这里基本就实现完成了一个 跨主机/服务器 之间的生产者消费者模型了(功能上可以满足日常开发对消息队列的使用),但是还具有很强的扩展性,可以继续参考 RabbitMQ,如果有想法的,或者是遇到不会的问题,可以私信我~

以下我来我来编写一个 demo,模拟 跨主机/服务器 之间的生产者消费者模型(这里为了方便,就在本机演示).

首先再 spring boot 项目的启动类中 创建 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、实例演示

启动 spring boot 项目(启动 BrokerServer)

运行消费者(消费者和生产者谁先后运行都可以)

运行生产者

<think>好的,我现在需要帮助用户在VC++中使用rabbitmq-c库的示例代码。根据用户提供的引用内容,首先得确认用户的需求是找到或编写在VC++环境下调用rabbitmq-c的示例代码。用户提到的引用[2]和[3]提到需要编译rabbitmq-c和Boost库,并且可能需要封装C++类库。 首先,我应该先检查rabbitmq-c的官方文档或GitHub仓库,看看是否有现成的示例代码。不过用户提到引用[3]有一个发送和接收接口的简单示例,可能用户已经找到了部分代码,但需要具体的步骤指导。另外,用户可能遇到编译依赖的问题,比如如何正确链接rabbitmq-c和Boost库。 接下来,我需要考虑VC++项目的配置,包括包含头文件路径、库文件路径以及链接所需的.lib文件。可能还需要设置运行时库的路径,确保DLL文件在可执行文件旁边。此外,代码示例部分应该包括初始化连接、创建通道、声明队列、发送和接收消息的基本流程,使用rabbitmq-c的API。 同时,需要注意用户提到的引用[2]中提到的现场控制器使用Windows 10 x64和MSVC编译器,所以需要确保示例代码兼容该环境。可能需要指导用户如何下载和编译rabbitmq-c,以及如何处理可能的依赖问题,比如Boost库的编译。 最后,应该验证示例代码的正确性,确保每一步骤都符合rabbitmq-c的使用方法,并提醒用户常见错误,比如连接参数错误或内存管理问题。如果用户遇到编译错误,可能需要检查库的版本兼容性或路径设置是否正确。</think>在VC++中使用rabbitmq-c库需要经过环境配置和API调用两个关键阶段。以下是具体实现步骤: ### 一、环境配置 1. **编译rabbitmq-c** - 下载最新版rabbitmq-c源码(建议v0.13.0+) - 使用CMake生成VS工程文件 - 编译生成`rabbitmq.4.lib`静态库和动态库[^2] 2. **Boost库准备** - 下载Boost 1.82+源码 - 执行`bootstrap.bat`生成编译工具 - 编译System和Regex等必要模块 ### 二、示例代码 ```cpp #include <amqp.h> #include <amqp_tcp_socket.h> int main() { // 建立连接 amqp_connection_state_t conn = amqp_new_connection(); amqp_socket_t* socket = amqp_tcp_socket_new(conn); // 连接参数配置 amqp_status_enum status = amqp_socket_open(socket, "localhost", 5672); if (status != AMQP_STATUS_OK) { // 错误处理 } // 登录RabbitMQ服务器 amqp_rpc_reply_t login_reply = amqp_login( conn, "/", 0, 131072, 0, AMQP_SASL_METHOD_PLAIN, "guest", "guest" ); // 创建通道 amqp_channel_open(conn, 1); // 声明队列 amqp_queue_declare(conn, 1, amqp_cstring_bytes("test_queue"), 0 /*non-durable*/, 1 /*durable*/, 0, 0, amqp_empty_table); // 发送消息 amqp_basic_publish(conn, 1, amqp_cstring_bytes(""), amqp_cstring_bytes("test_queue"), 0, 0, NULL, amqp_cstring_bytes("Hello RabbitMQ!")); // 关闭连接 amqp_connection_close(conn, AMQP_REPLY_SUCCESS); amqp_destroy_connection(conn); return 0; } ``` ### 三、项目配置 | 配置项 | 参数值 | |-----------------|--------------------------------| | 附加包含目录 | rabbitmq-c/include;boost_1_82 | | 附加库目录 | rabbitmq-c/lib;boost/lib | | 附加依赖项 | rabbitmq.4.lib;libboost_system-vc143-mt-x64-1_82.lib |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈亦康

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值