跳到主要内容RabbitMQ 常见工作模式及发布确认实现 | 极客日志Javajava
RabbitMQ 常见工作模式及发布确认实现
综述由AI生成RabbitMQ 支持多种消息传递模式,包括工作队列、发布订阅、路由、通配符、RPC 通信及发布确认机制。内容详细阐述了各模式的原理与 Java 客户端实现代码,涵盖生产者消费者竞争消费、交换机类型绑定规则、RPC 回调队列设计以及防止消息丢失的确认策略。通过对比单独确认、批量确认和异步确认的性能差异,帮助开发者根据业务场景选择合适的可靠性方案。
筑梦师20 浏览 一、Work Queues(工作队列模式)
工作队列模式支持多个消费者接收消息,消费者之间是竞争关系,每个消息只能被一个消费者接收。
每个工作模式的实现,都先需要引入 RabbitMQ 的依赖:
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.3</version>
</dependency>
1.1 生产者
生产者步骤:
- 创建连接
- 创建 Channel
- 声明一个队列 Queue
- 生产消息
- 释放资源
package org.example.rabbitmq.workqueues;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ProducerDemo {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel connection.createChannel();
channel.queueDeclare(, , , , );
( ; i < ; i++) {
+ i;
channel.basicPublish(, , , msg.getBytes());
}
System.out.println();
channel.close();
connection.close();
}
}
channel
=
"workQueues"
true
false
true
null
for
int
i
=
0
10
String
msg
=
"hello workQueues"
""
"workQueues"
null
"消息发送成功"
1.2 消费者
- 创建连接
- 创建 Channel
- 声明一个队列 Queue
- 消费消息
- 释放资源
package org.example.rabbitmq.workqueues;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("workQueues", false, false, true, null);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer1 接收到消息:" + new String(body));
}
};
channel.basicConsume("workQueues", true, consumer);
Thread.sleep(100);
}
}
二、Publish/Subscribe(发布/订阅)
在发布/订阅模型中,多了一个 Exchange 角色。
Exchange 常见有三种类型,分别代表不同的路由规则,也就分别对应不同的工作模式:
- Fanout:广播,将消息交给所有绑定到交换机的队列(Publish/Subscribe 模式)
- Direct:定向,把消息交给符合指定 routing key 的队列(Routing 模式)
- Topic:通配符,把消息交给符合 routing pattern(路由模式)的队列(Topics 模式)
2.1 生产者
- 创建连接
- 创建信道
- 声明交换机
- 声明两个队列
- 交换机与队列进行绑定
- 生产消息
- 释放资源
声明交换机的方法是 Channel 类下的 exchangeDeclare 方法。
- exchange – the name of the exchange,交换机名称
- type – the exchange type,交换机类型
- durable – true if we are declaring a durable exchange,是否可持久化
- autoDelete – true if the server should delete the exchange when it is no longer in use,是否自动删除
- internal – true if the exchange is internal,是否内部使用
- arguments – other properties,参数
package org.example.rabbitmq.fanout;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("fanout.exchange", BuiltinExchangeType.FANOUT, false);
channel.queueDeclare("fanout.queue1", true, false, false, null);
channel.queueDeclare("fanout.queue2", true, false, false, null);
channel.queueBind("fanout.queue1", "fanout.exchange", "");
channel.queueBind("fanout.queue2", "fanout.exchange", "");
for (int i = 0; i < 10; i++) {
String msg = "hello fanout" + i;
channel.basicPublish("fanout.exchange", "", null, msg.getBytes());
}
System.out.println("发送消息成功");
channel.close();
connection.close();
}
}
2.2 消费者
- 创建连接
- 创建 Channel
- 声明一个队列 Queue
- 消费消息
- 释放资源
package org.example.rabbitmq.fanout;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("fanout.queue1", true, false, false, null);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer1 接收到消息:" + new String(body));
}
};
channel.basicConsume("fanout.queue1", consumer);
}
}
三、Routing(路由模式)
Routing(路由模式):队列和交换机的绑定,不能是任意的绑定了,而是要指定一个 BindingKey(RoutingKey 的一种),消息的发送方在向 Exchange 发送消息时,也需要指定消息的 RoutingKey,Exchange 也不再把消息交给每一个绑定的 key,而是根据消息的 RoutingKey 进行判断,只有队列绑定时的 BindingKey 和发送消息的 RoutingKey 完全一致,才会接收到消息。
3.1 生产者
- 创建连接
- 创建信道
- 声明交换机,类型为 direct
- 声明队列
- 队列与交换机绑定,绑定的时候加上 BindingKey 参数
- 生产消息,消息发送的时候指定 routingKey
- 释放资源
package org.example.rabbitmq.direct;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("direct.exchange", BuiltinExchangeType.DIRECT, true);
channel.queueDeclare("direct.queue1", true, false, false, null);
channel.queueDeclare("direct.queue2", true, false, false, null);
channel.queueBind("direct.queue1", "direct.exchange", "a");
channel.queueBind("direct.queue2", "direct.exchange", "a");
channel.queueBind("direct.queue2", "direct.exchange", "b");
channel.queueBind("direct.queue2", "direct.exchange", "c");
String msgA = "hello direct routingKey is a";
channel.basicPublish("direct.exchange", "a", null, msgA.getBytes());
String msgB = "hello direct routingKey is b";
channel.basicPublish("direct.exchange", "b", null, msgB.getBytes());
String msgC = "hello direct routingKey is c";
channel.basicPublish("direct.exchange", "c", null, msgC.getBytes());
System.out.println("发送消息成功");
channel.close();
connection.close();
}
}
3.2 消费者
- 创建连接
- 创建 Channel
- 声明一个队列 Queue
- 消费消息
- 释放资源
package org.example.rabbitmq.direct;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("direct.queue1", true, false, false, null);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer1 接收到消息:" + new String(body));
}
};
channel.basicConsume("direct.queue1", consumer);
}
}
四、Topics(通配符模式)
- topics 模式使用的交换机类型为 topic(Routing 模式使用的交换机类型为 direct)
- topic 类型的交换机在匹配规则上进行了扩展,Binding Key 支持通配符匹配(direct 类型的交换机路由规则是 BindingKey 和 RoutingKey 完全匹配)。
在 topic 类型的交换机在匹配规则上,有些要求:
- RoutingKey 是一系列由点 ( . ) 分隔的单词,比如 "stock.usd.nyse","nyse.vmw","quick.orange.rabbit"
- BindingKey 和 RoutingKey 一样,也是点 ( . ) 分割的字符串。
- Binding Key 中可以存在两种特殊字符串,用于模糊匹配
* 表示一个单词
# 表示多个单词 (0-N 个)
4.1 生产者
- 创建连接
- 创建信道
- 声明交换机,类型为 topic
- 声明队列
- 队列与交换机绑定,绑定的时候加上 BindingKey 参数
- 生产消息,消息发送的时候指定 routingKey
- 释放资源
package org.example.rabbitmq.topics;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("topic.exchange", BuiltinExchangeType.TOPIC, true);
channel.queueDeclare("topic.queue1", true, false, false, null);
channel.queueDeclare("topic.queue2", true, false, false, null);
channel.queueBind("topic.queue1", "topic.exchange", "*.a.*");
channel.queueBind("topic.queue2", "topic.exchange", "*.*.b");
channel.queueBind("topic.queue2", "topic.exchange", "c.#");
String msgA = "hello topic routingKey is word.a.word";
channel.basicPublish("topic.exchange", "word.a.word", null, msgA.getBytes());
String msgB = "hello topic routingKey is word.word.b";
channel.basicPublish("topic.exchange", "word.word.b", null, msgB.getBytes());
String msgC = "hello topic routingKey is c.word.word.word.word.b";
channel.basicPublish("topic.exchange", "c.word.word.word.word.b", null, msgC.getBytes());
String msgD = "hello topic routingKey is c.a.b";
channel.basicPublish("topic.exchange", "c.a.b", null, msgD.getBytes());
System.out.println("发送消息成功");
channel.close();
connection.close();
}
}
4.2 消费者
- 创建连接
- 创建 Channel
- 声明一个队列 Queue
- 消费消息
- 释放资源
package org.example.rabbitmq.topics;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("topic.queue1", true, false, false, null);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer1 接收到消息:" + new String(body));
}
};
channel.basicConsume("topic.queue1", consumer);
}
}
五、RPC 通信
RPC(Remote Procedure Call),即远程过程调用。它是一种通过网络从远程计算机上请求服务,而不需要了解底层网络的技术。类似于 Http 远程调用。
RabbitMQ 实现 RPC 通信的过程,大概是通过两个队列实现一个可回调的过程。
- 客户端发送消息到一个指定的队列,并在消息属性中设置 replyTo 字段,这个字段指定了一个回调队列,服务端处理后,会把响应结果发送到这个队列。
- 服务端接收到请求后,处理请求并发送响应消息到 replyTo 指定的回调队列
- 客户端在回调队列上等待响应消息,一旦收到响应,客户端会检查消息的 correlationId 属性,以确保它是所期望的响应。
5.1 客户端
- 声明两个队列,包含回调队列 replyQueueName,声明本次请求的唯一标志 corrId
- 将 replyQueueName 和 corrId 配置到要发送的消息队列中
- 使用阻塞队列来阻塞当前进程,监听回调队列中的消息,把请求放到阻塞队列中
- 阻塞队列有消息后,主线程被唤醒,打印返回内容
package org.example.rabbitmq.rpc;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;
public class Client {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("rpc.request.queue", true, false, false, null);
channel.queueDeclare("rpc.response.queue", true, false, false, null);
String correlationId = UUID.randomUUID().toString();
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.correlationId(correlationId)
.replyTo("rpc.response.queue")
.build();
String msg = "hello rpc";
channel.basicPublish("", "rpc.request.queue", properties, msg.getBytes());
final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Client 接收到消息:" + new String(body));
if (correlationId.equals(properties.getCorrelationId())) {
response.offer(new String(body));
}
}
};
channel.basicConsume("rpc.response.queue", true, consumer);
String result = response.take();
System.out.println(" [RPC_Client] Result:" + result);
}
}
5.2 服务器
- 接收消息
- 根据消息内容进行响应处理,把应答结果返回到回调队列中
package org.example.rabbitmq.rpc;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Server {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Server 接收到请求:" + new String(body));
String response = "响应";
AMQP.BasicProperties props = new AMQP.BasicProperties().builder()
.correlationId(properties.getCorrelationId())
.replyTo("rpc.request.queue")
.build();
channel.basicPublish("", "rpc.response.queue", props, response.getBytes());
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("rpc.request.queue", false, consumer);
}
}
六、Publisher Confirms(发布确认)
消息中间件都会有消息丢失的问题发生,大概分为以下三种丢失情况:
- 生产者问题:因为应用故障,网络等问题,生产者没有成功向消息中间件发送消息;
- 消息中间件问题:生产者成功发送了消息,消息中间件自己原因导致消息丢失;
- 消费者问题:消费者消费消息时,处理出现问题,导致消费者消费失败的消息从消息队列中删除了。
RabbitMQ 针对上面三种情况给出的解决方案:
- 针对生产者问题:采取 Publisher Confirms(发布确认)机制解决;
- 针对消息中间件问题:通过持久化机制解决;
- 针对消费者问题:通过消息应答机制解决;
生产者将信道设置成 confirm(确认)模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都会被指派一个唯一的 ID(从 1 开始);
一旦消息被投递到所有匹配的队列之后,RabbitMQ 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了;
如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出。
broker 回传给生产者的确认消息中 deliveryTag 包含了确认消息的序号,此外 broker 也可以设置 channel.basicAck() 方法中的 multiple 参数,表示到这个序号之前的所有消息都已经得到了处理。
6.1 Publishing Messages Individually(单独确认)
跟生产者发送消息,只有调用 Channel 类 confirmSelect() 设置信道为 confirm 模式,和 Channel 类 waitForConfirmsOrDie() 方法等待手动确认。
private static void publishingMessagesIndividually() throws IOException, TimeoutException, InterruptedException {
try (Channel channel = createChannel()) {
channel.confirmSelect();
channel.queueDeclare("publish.confirm.queue1", true, false, false, null);
long start = System.currentTimeMillis();
for (int i = 0; i < 200; i++) {
String msg = "Publishing Messages Individually " + i;
channel.basicPublish("", "publish.confirm.queue1", null, msg.getBytes());
channel.waitForConfirmsOrDie(5000);
}
long end = System.currentTimeMillis();
System.out.println("Publishing Messages Individually(单独确认) 发送 200 条消息耗时 " + (end - start));
}
}
观察上面代码,会发现这种策略是每发送一条消息后就调用 channel.waitForConfirmsOrDie() 方法,之后等待服务端的确认,这实际上是一种串行同步等待的方式。
尤其对于持久化的消息来说,需要等待消息确认存储在磁盘之后才会返回(调用 Linux 内核的 fsync 方法)。
但是发布确认机制是支持异步的。可以一边发送消息,一边等待消息确认。
6.2 Publishing Messages in Batches(批量确认)
每发送一批消息后,调用 channel.waitForConfirms 方法,等待服务器的确认返回。
跟单独确认区别就是,发送到一定消息再进行等待确认。
private static void publishingMessagesInBatches() throws IOException, TimeoutException, InterruptedException {
try (Channel channel = createChannel()) {
channel.confirmSelect();
channel.queueDeclare("publish.confirm.queue2", true, false, false, null);
long start = System.currentTimeMillis();
int batchSize = 100;
int flag = 0;
for (int i = 0; i < 200; i++) {
String msg = "Publishing Messages in Batches " + i;
channel.basicPublish("", "publish.confirm.queue2", null, msg.getBytes());
if (flag == batchSize) {
channel.waitForConfirmsOrDie(5000);
flag = 0;
}
flag++;
}
if (flag > 0) {
channel.waitForConfirmsOrDie(5000);
}
long end = System.currentTimeMillis();
System.out.println("Publishing Messages in Batches(批量确认) 发送 200 条消息耗时 " + (end - start));
}
}
相比于单独确认策略,批量确认极大地提升了 confirm 的效率,
缺点是出现 Basic.Nack 或者超时时,我们不清楚具体哪条消息出了问题。客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量。
当消息经常丢失时,批量确认的性能应该是不升反降的。
6.3 Handling Publisher Confirms Asynchronously(异步确认)
提供一个回调方法,服务端确认了一条或者多条消息后客户端会回这个方法进行处理。
异步 confirm 方法的编程实现最为复杂。Channel 接口提供了一个方法 addConfirmListener,这个方法可以添加 ConfirmListener 回调接口。
ConfirmListener 接口中包含两个方法:handleAck(long deliveryTag, boolean multiple) 和 handleNack(long deliveryTag, boolean multiple),分别对应处理 RabbitMQ 发送给生产者的 ack 和 nack。
deliveryTag 表示发送消息的序号,multiple 表示是否批量确认。我们需要为每一个 Channel 维护一个已发送消息的序号集合。
当收到 RabbitMQ 的 confirm 回调时,从集合中删除对应的消息。当 Channel 开启 confirm 模式后,channel 上发送消息都会附带一个从 1 开始递增的 deliveryTag 序号。
我们可以使用 SortedSet 的有序性来维护这个已发消息的集合。
- 当收到 ack 时,从序列中删除该消息的序号。如果为批量确认消息,表示小于等于当前序号 deliveryTag 的消息都收到了,则清除对应集合
- 当收到 nack 时,处理逻辑类似,不过需要结合具体的业务情况,进行消息重发等操作。
private static void handlingPublisherConfirmsAsynchronously() throws IOException, TimeoutException, InterruptedException {
try (Channel channel = createChannel()) {
channel.confirmSelect();
channel.queueDeclare("publish.confirm.queue3", true, false, false, null);
SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<>());
long start = System.currentTimeMillis();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
confirmSet.headSet(deliveryTag + 1).clear();
} else {
confirmSet.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
confirmSet.headSet(deliveryTag + 1).clear();
} else {
confirmSet.remove(deliveryTag);
}
}
});
int flag = 0;
for (int i = 0; i < 200; i++) {
String msg = "Handling Publisher Confirms Asynchronously " + i;
long nextPublishSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish("", "publish.confirm.queue3", null, msg.getBytes());
confirmSet.add(nextPublishSeqNo);
}
while (confirmSet.isEmpty()) {
Thread.sleep(20);
}
long end = System.currentTimeMillis();
System.out.println("Handling Publisher Confirms Asynchronously(异步确认) 发送 200 条消息耗时 " + (end - start));
}
}
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online