如果你曾经在工作中使用过网络软件,脑海中应该会有客户端和服务器端的概念。不管是浏览器和Web服务器,还是应用程序和MySQL服务器,都是其中一方发送请求,而另一方服务这些请求。你可以将其视为快餐车模式。你的应用程序下订单,然后快餐车完成订单。你需要的数据来源于快餐车服务器。该模型就是我们平时如何尝试理解应用程序和服务器之间发生的一切。因此对于这个新的消息通信机制,你可能会问:哪个是顾客,哪个是快餐车,还有我怎样下订单呢?
这确实是个问题。RabbitMQ不是快餐车而是消息投递服务。应用程序从RabbitMQ获得的数据并不是由Rabbit产生的,就如同你收到的快递包裹也不是由FedEx生产的一样。因此,让我们把Rabbit当作一种投递服务。应用程序可以发送和接收包裹。而数据所在的服务器也可以发送和接收。RabbitMQ在应用程序和服务器之间扮演着路由器的角色。所以当应用程序连接到RabbitMQ时,他就必须做个决定:我是在发送还是在接收呢?或者从AMQP的角度思考,我是一个生产者还是一个消费者呢?
生产者(producer)创建消息,然后发布(发送)到代理服务器(RabbitMQ)。什么是消息呢?消息包含两部分内容:有效载荷(payload)和标签(label)。有效载荷就是你想要传输的数据。他可以是任何内容,一个JSON数组或者是你最喜欢的iguana Ziggy的MPEG-4。RabbitMQ不会在意这些。标签就更有趣了。他描述了有效载荷,并且RabbitMQ用他来决定谁将获得消息的拷贝。举例来说,不同于TCP协议的是,当你明确指定发送方和接收方时,AMQP只会用标签表述这条消息(一个交换器的名称和可选的主题标记),然后把消息交由Rabbit。Rabbit会根据标签把消息发送给感兴趣的接收方。这种通信方式是一种“发后即忘”(fire-and-forget)的单向方式。生产者会创建消息并设置标签(见下图所示)。
消费者很容易理解。他们连接到代理服务器上,并订阅到队列(queue)上。把消息队列想象成一个具名邮箱。每当消息到达特定的邮箱时,RabbitMQ会将其发送给其中一个订阅的/监听的消费者。当消费者接收到消息时,他只得到消息的一部分:有效荷载。在消息路由过程中,消息的标签并没有随着有效载荷一同传递。RabbitMQ甚至不会告诉你是谁生产/发送了消息。就好比你拿起信件时,却发现所有的信封都是空白的。想要知道这条消息是否是从Millie姑妈发来的唯一方式是他在信里签了名。同理,如果需要明确知道是谁生产的AMQP消息的话,就要看生产者是否把发送信息放入有效载荷中。
整个过程其实很简单:生产者创建消息,消费者接收这些消息。你的应用程序可以作为生产者,向其他应用程序发送消息。或者作为一个消费者,接收消息。也可以在两者之间进行切换。不过在此之前,他必须先建立一条信道(channel)。等等!什么是信道呢?
你必须首先连接到Rabbit,才能消费或者发布消息。你在应用程序和Rabbit代理服务器之间创建一条TCP连接。一旦TCP连接打开(你通过了认证),应用程序就可以创建一条AMQP信道。信道是建立在“真实的”TCP连接内的虚拟连接。AMQP命令都是通过信道发送的。每条信道都会被指派一个唯一IP(AMQP库会帮你记住ID的)。不论是发布消息、订阅队列或是接收消息,这些动作都是通过信道完成的。你也许会问为什么我们需要信道呢?为什么不直接通过TCP连接发送AMQP命令呢?主要原因在于对操作系统来说建立和销毁TCP会话是非常昂贵的开销。假设应用程序从队列消费消息,并根据服务需求合理调度线程。假设你只进行TCP连接,那么每个线程都需要自行连接到Rabbit。也就是说高峰期有每秒成百上千条连接。这不仅造成TCP连接的巨大浪费,而且操作系统每秒也就只能建立这点数量的连接。因此,你可能很快就碰到性能瓶颈了。如果我们为所有线程只使用一条TCP连接以满足性能方面的要求,但又能确保每个线程的私密性,就像拥有独立连接一样的话,那不就非常完美吗?这就是要引入信道概念的原因。线程启动后,会在现成的连接上创建一条信道,也就获得了连接到Rabbit上的私密通信路径,而不会给操作系统的TCP栈造成额外负担,如下图所示。因此,你可以每秒成百上千次的创建信道而不会影响操作系统。在一条TCP连接上创建多少条信道是没有限制的。把他想象成一束光纤电缆就可以了。
每条电缆中的光纤束都可以传输(就像一条信道)。一条电缆有许多光纤束,允许所有连接的线程通过多条光纤束同时进行传输和接收。TCP连接就像电缆,而AMQP信道就像一条条独立光纤束。
我们来举个例子吧。假设你正在编写一个服务用来跟踪代客泊车。所有人都和RabbitMQ进行交流。服务必须要完成两个任务:
- 存储代客票ID以及对应的车辆停放泊车位。
- 返回指定代客票ID对应的泊车位。
就第一任务而言,你提供的服务将扮演消费者的角色。他订阅Rabbit队列,等待“存放票”消息。该消息包含票ID和泊车位号码。对于第二个任务,你提供的服务既是消费者也是生产者。他需要接收消息来获取特定代客票ID,然后他需要发布一个包含对应泊车位号码的应答消息。
为了实现第二个任务,应用程序要扮演生产者的角色。一旦建立到RabbitMQ代理服务器的连接,应用程序将创建多条信道:chan_recv信道用于服务接收消息的线程,chan_sendX(X就是线程号)信道用于服务每一个应答线程。你使用chan_recv设置队列的订阅,用来接收包含“票查询”请求的消息。当应用程序通过chan_recv信道收到一条票查询消息时,他检查消息中包含的票ID。一旦确认了对应的泊车位号,应用程序将创建线程发送应答(原始线程则继续接收新的请求)。然后新的应答线程创建包含泊车位号的消息。最终,新线程为应答消息设置标签并通过chan_sendX信道将其发送给Rabbit。如果只有一条信道,新应答线程将无法分享TCP连接。你有两个选择。其一,每个线程使用一个连接。这意味着你的应用程序在响应当前请求前无法处理新的票查询请求。其二,为每个发送线程都分配TCP连接,这样会浪费TCP资源。使用多个信道,线程可以同时共享连接。这意味着对请求的应答不会阻塞消费新的请求,而且也不会浪费TCP连接。有时,你可能会选择仅使用一条信道,但是有了AMQP,你可以灵活的使用多个信道来满足应用的需求,而不会有众多TCP连接的开销。
重要的是记住消费者和生产者是消息发送和消息接收概念的体现,而非客户端和服务器端。从总体上来说,消息通信,特别是AMQP,可以被当作加强版的传输层。使用信道。你能够根据应用需要,尽可能多的创建并行的传输层,而不会被TCP连接约束所限制。当你理解了这些概念时,你就能把RabbitMQ看作软件的路由器了。