RabbitMQ详解

概述

RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然.RabbitMQ本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了一个经纪人(Broker)构架,这意味着消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)或者数据持久化都有很好的支持。

RabbitMQ的优点

基于erlang语言开发具有高可用高并发的优点,适合集群服务器。
健壮、稳定、易用、跨平台、支持多种语言、文档齐全。
有消息确认机制和持久化机制,可靠性高。
RabbitMQ 是一个功能强大的开源消息队列软件,具有许多优点

  1. 可靠性:RabbitMQ 支持多种消息传递模式(如点对点、发布/订阅),并提供持久化、消息确认、消息重试等机制,确保消息传递的可靠性。
  2. 灵活性:RabbitMQ 提供了丰富的特性,如消息路由、消息过滤、消息优先级等,可以根据不同场景灵活配置和定制,满足各种消息传递需求。
  3. 可扩展性:RabbitMQ 支持集群部署和横向扩展,可以通过增加节点来扩展系统的吞吐量和容量,以满足高并发和大规模应用的需求。
  4. 性能优越:RabbitMQ 在消息传递性能方面表现优异,支持高吞吐量和低延迟的消息处理,适用于需要快速响应的应用场景。
  5. 多语言支持:RabbitMQ 提供了多种客户端库,支持多种编程语言(如Java、Python、Ruby、C#等),方便开发人员在不同平台上使用。
  6. 管理界面友好:RabbitMQ 提供了一个易于使用的 Web 管理界面,可以方便地监控队列、交换机、连接等信息,进行配置和管理操作。
  7. 社区活跃:作为一个开源项目,RabbitMQ 拥有活跃的社区支持,有大量的文档、教程和社区资源可供参考,方便开发者学习和解决问题。
  8. 与其他系统集成性强:RabbitMQ 支持与各种系统和框架的集成,如Spring、Celery、Logstash等,可以轻松与现有系统进行整合。
    总的来说,RabbitMQ 是一款功能强大、可靠性高、性能优越的消息队列软件,适用于各种分布式系统和微服务架构中的消息通信和异步任务处理

Message 内容对象序列化与反序列化

使用 Java 序列化与反序列化
默认的 SimpleMessageConverter 在发送消息时会将对象序列化成字节数组,若要反序列化对象,需要自定义 MessageConverter
SimpleMessageConverter 对于要发送的消息体 body 为 byte[] 时不进行处理,如果是 String 则转成字节数组,如果是 Java 对象,则使用 jdk 序列化将消息转成字节数组,转出来的结果较大,含class类名,类相应方法等信息。因此性能较差。当使用 RabbitMQ 作为中间件时,数据量比较大,此时就要考虑使用类似 Jackson2JsonMessageConverter 等序列化形式以此提高性能
使用 JSON 序列化与反序列化
RabbitMQ 提供了 Jackson2JsonMessageConverter 来支持消息内容 JSON 序列化与反序列化
消息发送者在发送消息时应设置 MessageConverter 为Jackson2JsonMessageConverter

@Test
public void demo_06_Producer() {
    String routingKey = "hello";
    TestA a = new TestA();
    a.setFieldA("FBI WARNING");
    // 设置 MessageConverter 
    rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
    rabbitTemplate.convertAndSend(routingKey, a);
    System.out.println("发送成功");
}
 消费者也应该配置 MessageConverterJackson2JsonMessageConverter,这样消费者反序列化就能匹配成功  
@Configuration
public class RabbitMQConfig {
    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        // 临时设置 MessageConverter 为 Jackson2JsonMessageConverter
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        return factory;
    }
}
@Service
@Slf4j
public class RabbitmqService {
    
    @NacosValue(value = "${rabbitmq.exchange.fanout.recognize}",autoRefreshed = true)
    private String recognizeFanoutExchangeName;
    
    @NacosValue(value = "${rabbitmq.exchange.fanout.words}",autoRefreshed = true)
    private String wordsFanoutExchangeName;
    
    @Resource
    private RabbitTemplate rabbitTemplate;
    
    /**
    * 发布识别消息
    * @param message
    */
    public void sendRecognizeMessage(String message) {
        log.debug("rabbitMQ中的消息为:message = [{}]", GsonUtils.bean2json(message));
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        rabbitTemplate.convertAndSend(recognizeFanoutExchangeName, "", message);
    }
    
    /**
    * 发布话术库话术采纳消息
    * @param message
    */
    public void sendWordsAcceptMessage(String message) {
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        rabbitTemplate.convertAndSend(wordsFanoutExchangeName, "", message);
    }
    
}

使用 Java 序列化与反序列化
默认的 SimpleMessageConverter 在发送消息时会将对象序列化成字节数组,若要反序列化对象,需要自定义 MessageConverter
@Configuration
public class RabbitMQConfig {

    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new MessageConverter() {
            @Override
            public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
                return null;
            }

            @Override
            public Object fromMessage(Message message) throws MessageConversionException {
                try(ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(message.getBody()))){
                    return (User)ois.readObject();
                }catch (Exception e){
                    e.printStackTrace();
                    return null;
                }
            }
        });

        return factory;
    }

}
@Component
@RabbitListener(queues = "consumer_queue")
public class Receiver {

    @RabbitHandler
    public void processMessage1(User user) {
        System.out.println(user.getName());
    }

}

使用 JSON 序列化与反序列化

RabbitMQ 提供了 Jackson2JsonMessageConverter 来支持消息内容 JSON 序列化与反序列化
消息发送者在发送消息时应设置 MessageConverter 为 Jackson2JsonMessageConverter
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
发送消息

User user = new User(“linyuan”);
rabbitTemplate.convertAndSend(“topic.exchange”,“key.1,user);
消息消费者也应配置 MessageConverterJackson2JsonMessageConverter
@Configuration
public class RabbitMQConfig {
    
    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        return factory;
    }

}
消费消息
@Component
@RabbitListener(queues = "consumer_queue")
public class Receiver {

    @RabbitHandler
    public void processMessage1(@Payload User user) {
        System.out.println(user.getName());
    }

}

注意:被序列化对象应提供一个无参的构造函数,否则会抛出异常
MessageConvert
加 @RabbitListener 注解来指定某方法作为消息消费的方法,例如监听某 Queue 里面的消息,涉及网络传输的应用序列化不可避免,发送端以某种规则将消息转成 byte 数组进行发送,接收端则以约定的规则进行 byte[] 数组的解析
RabbitMQ 的序列化是指 Message 的 body 属性,即我们真正需要传输的内容,RabbitMQ 抽象出一个 MessageConvert 接口处理消息的序列化,其实现有 SimpleMessageConverter(默认)、Jackson2JsonMessageConverter 等
当调用了 convertAndSend 方法时会使用 MessageConvert 进行消息的序列化
SimpleMessageConverter 对于要发送的消息体 body 为 byte[] 时不进行处理,如果是 String 则转成字节数组,如果是 Java 对象,则使用 jdk 序列化将消息转成字节数组,转出来的结果较大,含class类名,类相应方法等信息。因此性能较差
当使用 RabbitMQ 作为中间件时,数据量比较大,此时就要考虑使用类似 Jackson2JsonMessageConverter 等序列化形式以此提高性能

@RabbitListener和@RabbitHandler的使用

使用 @RabbitListener 注解标记方法,当监听到队列 debug 中有消息时则会进行接收并处理

@RabbitListener(queues = "debug") 
public void processMessage1(Message bytes) {    
    System.out.println(new String(bytes)); } 

注意
消息处理方法参数是由 MessageConverter 转化,若使用自定义 MessageConverter 则需要在 RabbitListenerContainerFactory 实例中去设置(默认 Spring 使用的实现是 SimpleRabbitListenerContainerFactory)
消息的 content_type 属性表示消息 body 数据以什么数据格式存储,接收消息除了使用 Message 对象接收消息(包含消息属性等信息)之外,还可直接使用对应类型接收消息 body 内容,但若方法参数类型不正确会抛异常:
application/octet-stream:二进制字节数组存储,使用 byte[]
application/x-java-serialized-object:java 对象序列化格式存储,使用 Object、相应类型(反序列化时类型应该同包同名,否者会抛出找不到类异常)
text/plain:文本数据类型存储,使用 String
application/json:JSON 格式,使用 Object、相应类型

1.@RabbitListener 注解是指定某方法作为消息消费的方法,例如监听某 Queue 里面的消息。
2.@RabbitListener标注在方法上,直接监听指定的队列,此时接收的参数需要与发送市类型一致

@Component
    public class PointConsumer {
    	//监听的队列名
        @RabbitListener(queues = "point.to.point")
        public void processOne(String name) {
            System.out.println("point.to.point:" + name);
        }
     
    }

@RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用
@RabbitListener 标注在类上面表示当有收到消息的时候,就交给 @RabbitHandler 的方法处理,根据接受的参数类型进入具体的方法中。

@Component
@RabbitListener(queues = "consumer_queue")
public class Receiver {
 
    @RabbitHandler
    public void processMessage1(String message) {
        System.out.println(message);
    }
 
    @RabbitHandler
    public void processMessage2(byte[] message) {
        System.out.println(new String(message));
    }
    
}

@Payload 与 @Headers
使用 @Payload 和 @Headers 注解可以消息中的 body 与 headers 信息

@RabbitListener(queues = "debug") public void processMessage1(@Payload String body, @Headers Map<String,Object> headers) {     System.out.println("body:"+body);     System.out.println("Headers:"+headers); } 

也可以获取单个 Header 属性
@RabbitListener(queues = “debug”) public void processMessage1(@Payload String body, @Header String token) { System.out.println(“body:”+body); System.out.println(“token:”+token); }
通过 @RabbitListener 注解声明 Binding
通过 @RabbitListener 的 bindings 属性声明 Binding(若 RabbitMQ 中不存在该绑定所需要的 Queue、Exchange、RouteKey 则自动创建,若存在则抛出异常)

@RabbitListener(bindings = @QueueBinding(
        exchange = @Exchange(value = "topic.exchange",durable = "true",type = "topic"),
        value = @Queue(value = "consumer_queue",durable = "true"),
        key = "key.#"
))
public void processMessage1(Message message) {
    System.out.println(message);
}

@RabbitListener 和 @RabbitHandler 搭配使用
@RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用
@RabbitListener 标注在类上面表示当有收到消息的时候,就交给 @RabbitHandler 的方法处理,具体使用哪个方法处理,根据 MessageConverter 转换后的参数类型

过期时间TTL

RabbitMQ中的TTL(Time-To-Live)是指消息的存活时间,它决定了消息在队列中存活的时长。如果消息在队列中的存活时间超过了TTL设定的数值,那么这条消息就会被自动从队列中移除或者标记为过期消息,具体处理方式取决于队列的配置。
在RabbitMQ中,可以通过以下方式设置消息的TTL:

  1. 队列级别的TTL: 可以在声明队列时指定队列的TTL,这样队列中的所有消息都将遵循该TTL。例如,在声明队列时,可以通过x-message-ttl参数设置消息的TTL。
  2. 消息级别的TTL: 也可以在发布消息时,针对每条消息单独设置TTL。当消息的TTL与队列的TTL同时设置时,以较小的那个数值为准。
    TTL的设置可以用于处理一些临时性的消息,确保消息在一定时间内能够被及时处理,同时也可以用于清理过期的消息,防止队列中积累过多的过期消息而影响系统性能。
    需要注意的是,TTL只是告诉RabbitMQ在一定时间后将消息标记为过期或移除,而不是强制删除消息。实际删除过期消息仍取决于RabbitMQ的内部机制和队列的特性。在RabbitMQ中,使用Java代码来设置消息的过期时间(TTL)可以通过设置队列的参数或者设置消息的属性来实现。下面分别展示这两种方式的Java代码实现。

在RabbitMQ中,可以通过设置消息的过期时间(TTL,Time-To-Live)来控制消息在队列中存活的时间。当消息的存活时间超过设定的过期时间时,RabbitMQ会自动将消息从队列中移除。
要设置消息的过期时间,可以通过以下两种方式来实现:

  1. 设置队列的过期时间:对整个队列设置统一的过期时间,超过该时间的消息将被自动清除。
  2. 设置消息的单独过期时间:对每条消息单独设置过期时间,超过该时间的消息将被自动清除。
    Java代码实现ttl
    设置队列的过期时间
import com.rabbitmq.client.*;

public class QueueTTLExample {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection(); 
             Channel channel = connection.createChannel()) {
            // 声明队列并设置过期时间
            channel.queueDeclare("my_queue", true, false, false, 
                // 设置队列的过期时间为60秒
                java.util.Collections.singletonMap("x-message-ttl", 60000));
        }
    }
}

设置消息的单独过期时间

import com.rabbitmq.client.*;

public class MessageTTLExample {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection(); 
             Channel channel = connection.createChannel()) {
            // 发送消息时设置单独的过期时间
            AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .expiration("60000")  // 设置消息的过期时间为60秒
                .build();
            channel.basicPublish("", "my_queue", properties, "Hello, RabbitMQ!".getBytes());
        }
    }
}

Spring Boot项目中使用RabbitMQ设置消息的过期时间(TTL)可以通过配置RabbitTemplate
以上示例代码使用了RabbitMQ的Java客户端库来设置队列和消息的过期时间。在实际应用中,你可以根据具体的业务需求和场景来调整过期时间的设置,以满足你的实际需求。
在Spring Boot项目中使用RabbitMQ设置消息的过期时间(TTL)可以通过配置RabbitTemplate或AmqpTemplate来实现。下面是一个示例代码,演示了如何在Spring Boot项目中设置消息的过期时间:

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Bean
    public Queue myQueue() {
        return QueueBuilder.durable("my_queue")
                .withArgument("x-message-ttl", 60000) // 设置队列的过期时间为60秒
                .build();
    }

    // 设置消息的过期时间
    public void sendMessageWithTTL() {
        MessageProperties properties = new MessageProperties();
        properties.setExpiration("60000"); // 设置消息的过期时间为60秒
        Message message = new Message("Hello, RabbitMQ!".getBytes(), properties);
        rabbitTemplate.send("my_queue", message);
    }
}

在上面的示例代码中,首先通过@Bean方法创建了一个名为“my_queue”的队列,并设置了该队列的过期时间为60秒。然后通过sendMessageWithTTL()方法,设置了发送到该队列的消息的过期时间为60秒。

这样,在Spring Boot应用程序中就能够实现对队列和消息的过期时间进行设置。你可以根据具体需求调整过期时间的设置值,以满足业务需求。
在Spring Boot项目中成功设置RabbitMQ消息的过期时间。

ack

使用了手动 ack(确认消息)机制,而消费者没有对消息进行 ack
在 RabbitMQ 中,如果使用了手动 ack(确认消息)机制,而消费者没有对消息进行 ack,会导致以下情况发生:

  1. 消息不会被移除: 如果消费者在处理消息后没有发送 ack 给 RabbitMQ,那么 RabbitMQ 将认为这条消息还没有被完全处理,并且会将其重新放回队列中等待下一次投递给消费者。这会导致消息被重复消费,可能引发重复处理的问题。
  2. 队列堆积: 如果大量消息因为没有收到 ack 而被重新投递给消费者,但是消费者依然无法及时处理这些消息并发送 ack,那么这些未被 ack 的消息会继续堆积在队列中,导致队列淤积。
  3. 消费者阻塞: 消费者如果长时间没有 ack,可能会导致消费者阻塞在处理某一条消息的过程中,从而无法继续处理队列中的其他消息。
    为了避免上述问题,确保消费者能够及时对消息进行 ack 是非常重要的。在实际应用中,可以通过合理的代码编写和异常处理机制来确保消息在被正确处理后能够及时地发送 ack。

rabbitmq 手动和自动ack的区别

RabbitMQ中的消息确认(acknowledgement)有两种方式:手动确认和自动确认。

  1. 自动 Acknowledgement:
    ○ 当消费者收到消息时,RabbitMQ会立即将消息标记为已经接收,然后立即将消息从队列中删除。
    ○ 这种方式下,消费者不需要显式地确认消息的处理,RabbitMQ假定消息已经被成功处理。
    ○ 自动 Acknowledgement 适用于那些不要求严格的消息传递保障的应用场景。
  2. 手动 Acknowledgement:
    ○ 在手动确认模式下,消费者需要在处理完消息之后,显式地向 RabbitMQ 发送确认消息,告知 RabbitMQ 消息已经被处理。
    ○ 只有在收到消费者的显式确认后,RabbitMQ 才会将消息标记为已处理,并将其从队列中删除。
    ○ 手动 Acknowledgement 适用于对消息传递的可靠性和一致性有较高要求的应用场景,因为它可以确保消息只有在被成功处理后才被删除。
    选择使用自动还是手动确认取决于应用的需求,如果应用对消息传递的可靠性要求不高,可以选择自动确认;如果需要确保消息被可靠地处理,可以选择手动确认。

推模式和拉模式来消费消息队列

RabbitMQ客户端提供了两种方式来消费消息队列:基于推送(Push)和基于拉取(Pull)。
使用注解方式监听RabbitMQ消息时,底层仍然是通过推模式来实现的。当有消息到达队列时,RabbitMQ会推送消息给消费者,消费者则通过注解方式指定的方法来处理接收到的消息。
与传统的拉模式相比,注解方式可以使代码更加简洁和易于理解,不需要手动编写循环去不断地拉取消息。相反,你只需定义一个带有@RabbitListener注解的方法,当有新消息到达时,框架会自动触发该方法并传递消息作为参数。这样可以减少了繁琐的消息拉取和确认的处理逻辑。
因此,使用注解方式监听RabbitMQ消息是一种更为方便和简洁的推模式实现方式。
在 RabbitMQ 中,可以使用 Java 编写消费者应用程序来接收和处理消息。有两种常见的消费形式:基于推送模型的消费和基于拉取模型的消费。

  1. 基于推送模型的消费:
    ○ 创建 Connection 和 Channel 对象:使用 RabbitMQ 的 Java 客户端库,创建 Connection 和 Channel 对象。
    ○ 声明队列:使用 Channel 对象声明要消费的队列。
    ○ 创建消费者对象:创建一个 Consumer 对象,实现 Consumer 接口的 handleDelivery 方法来处理接收到的消息。
    ○ 注册消费者:使用 Channel 对象的 basicConsume 方法注册消费者,并指定消费的队列和消费者对象。
    ○ 开始消费消息:当消息到达时,RabbitMQ 会自动将消息推送给消费者的 handleDelivery 方法进行处理。
    下面是一个简单的示例代码,演示如何使用基于推送模型的方式消费 RabbitMQ 中的消息:
import com.rabbitmq.client.*;

public class PushConsumer {
    private final static String QUEUE_NAME = "my_queue";

    public static void main(String[] args) throws Exception {
        // 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        // 创建连接
        Connection connection = factory.newConnection();

        // 创建通道
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 创建消费者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("Received message: " + message);
            }
        };

        // 注册消费者
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
2. 基于拉取模型的消费:
  ○ 创建 ConnectionChannel 对象同上。
  ○ 声明队列同上。
  ○ 消费消息:使用 Channel 对象的 basicGet 方法主动从队列中获取一条消息。
  ○ 处理消息:对获取到的消息进行处理。
  ○ 确认消息:使用 Channel 对象的 basicAck 方法确认已经成功处理了消息。
下面是一个简单的示例代码,演示如何使用基于拉取模型的方式消费 RabbitMQ 中的消息:
import com.rabbitmq.client.*;

public class PullConsumer {
    private final static String QUEUE_NAME = "my_queue";

    public static void main(String[] args) throws Exception {
        // 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        // 创建连接
        Connection connection = factory.newConnection();

        // 创建通道
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        while (true) {
            // 拉取消息
            GetResponse response = channel.basicGet(QUEUE_NAME, false);

            if (response != null) {
                // 处理消息
                String message = new String(response.getBody(), "UTF-8");
                System.out.println("Received message: " + message);

                // 确认消息
                channel.basicAck(response.getEnvelope().getDeliveryTag(), false);
            } else {
                // 没有消息,休眠一段时间后重试
                Thread.sleep(1000);
            }
        }
    }
}

这两种消费方式各有优缺点,基于推送模型的消费简单方便,但需要应对大量消息的情况时可能会造成性能和资源浪费;而基于拉取模型的消费可以精细地控制消费速率,但需要手动处理消息的确认和失败重试。

相关推荐

  1. RabbitMQ详解

    2024-03-14 20:32:01       29 阅读
  2. RabbitMQ Streams 详解

    2024-03-14 20:32:01       47 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-14 20:32:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-14 20:32:01       100 阅读
  3. 在Django里面运行非项目文件

    2024-03-14 20:32:01       82 阅读
  4. Python语言-面向对象

    2024-03-14 20:32:01       91 阅读

热门阅读

  1. Python 面试问题:递归

    2024-03-14 20:32:01       46 阅读
  2. LeetCode每日一题[C++]-找出数组的第K大和

    2024-03-14 20:32:01       42 阅读
  3. ChatGPT模型api的python调用

    2024-03-14 20:32:01       36 阅读
  4. vue父子组件生命周期

    2024-03-14 20:32:01       40 阅读
  5. C语言(循环)单元练习

    2024-03-14 20:32:01       31 阅读
  6. TCP网络通信-在C#/Unity中的知识点

    2024-03-14 20:32:01       44 阅读
  7. Nmap常用的一些参数

    2024-03-14 20:32:01       41 阅读
  8. linux Shell 命令行-09-redirect 重定向

    2024-03-14 20:32:01       34 阅读
  9. webpack5基础--10_处理 js 资源

    2024-03-14 20:32:01       34 阅读
  10. 如何计算视频流需要的服务器带宽

    2024-03-14 20:32:01       37 阅读