目次
1. クライアントコードの実装
1.1. 需要分析
RabbitMQ クライアント設定: クライアントは複数のモジュールを持つことができ、各モジュールはブローカー サーバーとの「論理接続」 (チャネル) を確立できます。これらのモジュールのチャネルは相互に影響しません。同時に、これらのチャネルは同じものを再利用します。 TCP 接続。頻繁に TCP 接続を確立/切断するオーバーヘッド (3 回のハンドシェイク、4 回のウェーブなど) を排除します。
ここでは、このロジックに従ってメッセージ キュー クライアントも実装します。これには主に次の 3 つのコア クラスが含まれます。
- ConnectionFactory:接続ファクトリ。このクラスはサーバーのアドレスを保持します。その主な機能は、Connection オブジェクトを作成することです。
- 接続: TCP 接続を表し、Socket オブジェクトを保持し、要求の書き込み/応答の読み取りに使用され、複数の 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) 実装接続
プロパティは次のとおりです
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;
}
Ps: 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();
}
}
追伸: 他のリクエスト操作は 0x1 とほぼ同じです。ここでは 1 つずつ説明することはせず、主に 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. 補足:コールバック実行処理について
プロセスは次のとおりです。
- クライアントは、basicConsume メソッドを呼び出し、キュー メッセージをサブスクライブするコンシューマを作成し、コンシューマ (コンシューマがメッセージを取得した後に実行する特定のアクション) を呼び出します。
- チャネルは、クライアントによって送信されたコンシューマを保存し(実際にコールバックを実行するために 0xc 応答の受信を待機します)、サブスクリプション キュー メッセージ リクエスト(0xa)を送信し、応答を待ちます。
- リクエストを受信した後、BrokerServer は 0xa を解析し、Consumer を作成しました (目的は、サーバーが消費されるメッセージを取得するのを待ってから、メッセージ データを 0xc 応答にパッケージ化することです。クライアントが応答を受信した後、それを実行します) 「コンシューマ」メッセージを取得した後に実行する具体的なアクション「これは RabbitMQ の設定です)」そして、BrokerServer は VirtualHost を呼び出します。
- VirtualHost が新しいコンシューマ サブスクリプション キューを作成した後、キュー内に別のメッセージが見つかった場合は、それをすぐに消費します。
- 具体的には、新しく作成したコールバックを呼び出し、クライアントに 0xc 応答を返し、クライアントは応答を受信した後、「メッセージを取得した後にコンシューマが実行する特定のアクション」を実行します。
3. デモを書く
3.1. 例
この時点で、ホスト/サーバー間のプロデューサー/コンシューマー モデルは基本的に実装されています (日常の開発におけるメッセージ キューの使用に機能的に対応できます) が、強力な拡張性も備えています。引き続き RabbitMQ を参照してください。何かアイデアがある場合、またはわからない問題が発生した場合は、私にプライベートメッセージを送ってください〜
次に、ホスト/サーバー間でプロデューサー/コンシューマー モデルをシミュレートするデモを作成します (便宜上、このマシンでデモします)。
まず 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 を開始します)。
コンシューマを実行します (コンシューマまたはプロデューサのいずれかを連続して実行できます)
プロデューサーを実行する