目录
一、客户端代码实现
1.1、需求分析
RabbitMQ 的客户端设定:一个客户端可以有多个模块(不同的业务,主要是为了解耦),每个模块都可以和 broker server 之间建立 “逻辑上的连接” (channel),这几个模块的channel 彼此之间是互相不影响的,同时这几个 channel 又复用的同一个 TCP 连接,省去了频繁 建立/销毁 TCP 连接的开销(三次握手、四次挥手......).
这里,我们也按照这样的逻辑实现 消息队列 的客户端,主要涉及到以下三个核心类:
- ConnectionFactory:连接工厂,这个类持有服务器的地址,主要功能就是创建 Connection 对象.
- Connection:表示一个 TCP连接,持有 Socket 对象,用来 写入请求/读取响应,管理多个Channel 对象.
- 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:关于回调执行流程
流程如下:
- 客户端调用 basicConsume 方法,创建一个消费者订阅队列消息,并带上 Consumer(消费者拿到消息之后具体要做的动作);
- Channel 保存了客户端发来的 Consumer(等待接收 0xc 响应,真正执行回调),发送订阅队列消息请求(0xa),并等待响应
- BrokerServer 接收到请求后,解析出 0xa ,又创建了一个 Consumer(目的是为了等服务器拿到要消费的消息后,将消息的数据包装成 0xc 的响应,客户端接收到响应之后,执行 “消费者拿到消息后具体要做的动作” 这是 RabbitMQ 的设定),接着 BrokerServer 调用 VirtualHost.
- VirtualHost 创建一个新的消费者订阅队列后,如果发现队列中又消息,立即进行消费
- 具体的,就是调用刚刚新创建的回调,然后向客户端返回 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)
运行消费者(消费者和生产者谁先后运行都可以)
运行生产者