深入理解MQ生产端的底层通信过程-理解channel

引子

在上一篇《接下来一段时间会对大家进行网络通信的魔鬼训练-理解socket》里,标题里用到了“魔鬼训练”。一向注重标题贴合内容本意,为什么我会用这个词,这里跟大家解释一下。

这是我自己的一个学习方法。有些朋友是知道的,我日语学的不错的。1年的时间内从0开始学习通过了最高级(一级)的考试。当时我就是用的这种方法。简单介绍一下当时学习的整个过程:

我加入了当时一个特别大(鼎盛时期有8W人)的对日公司。听小伙伴们说有的人3个月就可以过日语三级(最低级别是四级)了。我就想啊,别人要是可以我也可以。于是我一个0基础,直接跳过了四级班,直接报了三级班。

可以想象,每次上课,我如听天书,就是奔着老师的颜值去的。但是私下里,我自己在赶进度。自己找其他人学习了假名等基础知识,然后抱着四级课本从头到尾的捋内容。捋完之后开始捋三级课本。怎么可能看的懂!但是我在三级课本里用到的四级内容的地方,我知道去哪里找了。所以当我终于赶上了老师的进度,三级的东西基本不会,但是四级的内容就差不多了。

我一边跟着老师学,听不明白的,我就找之前的内容自己补习。三个月后的日语考试,在这个班30多个人中,我考了十几名。

我如法炮制,考二级的时候,我在整个公司一起举行的考试中考了第7名。考一级的时候,我是第1名。第2名的分数被我远远的甩在了后面。

于是我的名字那时候经常出现在公司的周刊上面。还有人专门从其他办公楼跑过来看一眼,说是想知道这个传说中的人物长成什么样子。

这整件事情魔鬼在哪里呢,外面只看到了结果。那时候刚大学毕业,大家一起住公司宿舍。那个宿舍有7层楼,只有两层有人住。一层住男生,一层住女生。我下班之后就找没人的楼层自己在那里读日语。除了别人来叫我玩,我很少主动找别人玩的,有时间就学啊。

910ed554b6b272caab2ebbf14b25c373.png

1年的时间努力一把其实够好几年收益的。我虽然当时努力的方向不对,但是也收益了。头两年我是在沈阳的,来到北京之后找工作去了当时不错的名企,薪资也比同届的其他人高出不少。其实那时候我的技术很菜,但是日语能学的这么好,说明学习能力不错的。年轻可培养嘛,所以人家也很乐意收。

不过这里说明了,努力的方向不对。如果我用来学习技术,那我这几年的整个人生状态都将被改变。但是我花了2年搞日语;花了1年多搞文学;如果不是要生小鲜肉,我大概已经北大文学系研究生毕业了;后来读的是中科院心理学的研究生。爱好,我是认真的。人各有所求,虽然技术上发力的晚,我也不后悔。各阶段自己想做的事情都做了。

简而言之,以上的学习方法推荐给大家,近期的文章安排也是遵循这一思路。另外一点很重要:这不是一个成功的例子,而是一个失败的教训。虽然一直以来平台都不错,还是有希望的。不可否认的是因为我那时候没有好好学习技术,现在步履艰难。换个词大家应该就能理解了:不务正业。

4327d6ed12b8dd3fc3defd87aa4dacbd.png

深入理解Channel

引入Channel

《RabbitMQ设计原理解析》里提到RabbitMQ的工作原理如下:

9c549cf693e0f38b0817bb9504cbf8df.png

简单理解一下:在生产端也就是产生消息的客户端。如果咱们使用MQ发送消息,那就是咱们的应用服务器。它会通过Channel和MQ服务器也就是Broker进行通信。在Broker内部有一个路由层也就是Exchange。Exchange是交换机的意思,顾名思义,它的作用是进行一些规则的匹配,根据不同的规则将不同的消息转发到不同的消息队列Queue上。Queue里的消息到达消费端也要进行Channel。在《架构师三大难-领域划分问题》示例三(异步处理模式)里,我也讲了:消费端和生产端有可能是同一个应用,也可以设计为不同的应用。

好,那在这整个流程中,Channel到底是个什么东西呢?

其实Channel通常翻译为信道或者渠道。不是Linux网络编程里的标准概念,而是一种抽象。什么的抽象呢?文件的读取等操作的抽象。看下面Java NIO中最重要的通道的实现:

  • FileChannel:从文件中读写数据(不可异步读写)

  • DatagramChannel: 能通过UDP读写网络中的数据

  • SocketChannel:能通过TCP读写网络中的数据

  • ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel

再联想一下上篇《接下来一段时间会对大家进行网络通信的魔鬼训练-理解socket》里提到的Socket的本质:

socket的本质就是一种类型的文件,所以一个socket在进行读写操作时会对应一个文件描述符fd(file descriptor)。

6c84c6b99f3e6dde6886954e06508ec6.png

这一段是让大家回忆一下:socket是一种文件。对应于“文件的读取等操作的抽象”中的文件。

Java中怎么使用Channel

Channel的概念语言无关,这里只是因为本人更熟悉Java。使用别的语言不影响对概念的理解。

上篇文章中用到的ServerSocket和Socket类中都有getChannel的实现:

/**
 * Returns the unique {@link java.nio.channels.SocketChannel SocketChannel}
 * object associated with this socket, if any.
 *
 * <p> A socket will have a channel if, and only if, the channel itself was
 * created via the {@link java.nio.channels.SocketChannel#open
 * SocketChannel.open} or {@link
 * java.nio.channels.ServerSocketChannel#accept ServerSocketChannel.accept}
 * methods.
 *
 * @return  the socket channel associated with this socket,
 *          or {@code null} if this socket was not created
 *          for a channel
 *
 * @since 1.4
 * @spec JSR-51
 */
public SocketChannel getChannel() {
    return null;
}

注释直接翻译成中文:

返回一个唯一的SocketChannel对象或者返回null。一个Socket只有在Channel本身是由SocketChannel.open或者ServerSocketChannel.accept方法创建之时才会存在(不为null)。

上面代码已经很清楚了,直接使用Socket时,getChannel就是返回null。没有用的。那正确的获取SocketChannel的方法是什么呢?来看下面的代码:

@Test
public void getChannel() throws Exception {
    InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 520);
    for (int i=1; i <= 2; i++) {
        SocketChannel socketChannel = SocketChannel.open(inetSocketAddress);
        String msg = i == 1 ? "客户端:我知道我是任性太任性,伤透了你的心。我是追梦的人,追一生的缘分。" :
                "客户端:我愿意嫁给你,你却不能答应我。";
        ByteBuffer writeBuffer = ByteBuffer.allocate(20000);

        writeBuffer.put(msg.getBytes());
        writeBuffer.flip();
        socketChannel.write(writeBuffer);
        writeBuffer.clear();

        ByteBuffer readBuffer = ByteBuffer.allocate(20000);
        readBuffer.flip();
        socketChannel.read(readBuffer);

        socketChannel.close();
    }
}

服务端代码还是用《接下来一段时间会对大家进行网络通信的魔鬼训练-理解socket》中服务端的代码。因为客户端没有打印任何东西,直接看服务端运行结果:

客户端:我知道我是任性太任性,伤透了你的心。我是追梦的人,追一生的缘分。    

服务端:我知道你是任性太任性,伤透了我的心。同是追梦的人,难舍难分。

客户端:我愿意嫁给你,你却不能答应我。       

服务端:你愿意嫁给你,我却不能向你承诺。

和上篇文章运行结果一致。

看到这里大家是不是更疑惑了,SocketChannel和Socket做的同样的事情?

public static SocketChannel open(SocketAddress remote) throws IOException {
    SocketChannel sc = open();
    try {
       sc.connect(remote);
    } catch (Throwable x) {
         try {
          sc.close();
        } catch (Throwable suppressed) {
;
         }
        throw x;
    }
    assert sc.isConnected();
    return sc;
}

注意里面标红加粗的三个方法,open方法点进去有实现。这里有只截取open方法中我想说明的地方:

SocketChannelImpl(SelectorProvider var1) throws IOException {
    super(var1);
    this.fd = Net.socket(true);
    this.fdVal = IOUtil.fdVal(this.fd);
    this
    this.state = 0;
}

结合客户端例子的代码,发现了什么?

e550f91e3e92365560d45fc590b2dfe1.png

这张图眼熟不?《接下来一段时间会对大家进行网络通信的魔鬼训练-理解socket》的图又来了一遍。看!TCP客户端这里做的事情,Channel全都做了!

SocketChannel就是Socket 的替代类, 支持阻塞通信与非阻塞通信。Socket原生只支持最简的阻塞通信。Socket是在java.net下,SocketChannel在java.nio包下。

MQ中怎么使用Channel

下面是RabbitMQ的客户端代码,为什么用RabbitMQ呢?《RabbitMQ设计原理解析》里有说明:因为它实现了AMQP的标准协议。先上代码:

public static final String EXCHANGE_NAME = "RABBITMQ_Fanout";
 
    public static void main(String[] args) throws Exception{
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
 
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        String message = "This message is from Fanout mode.特点是Consumer均可获取到消息";
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println("---【Producer发送消息】" + message + "---" );
 
        channel.close();;
        connection.close();
    }

解析一下这段代码:这是一个生产端的代码。首先新建一个连接工厂的实例。一个连接工厂可以创建多个连接。一个连接可以创建多个Channel信道。Channel可以声明为fanout广播,也可以设置其他路由形式。然后就可以通过Channel进行数据发送了。

在AMQP协议中,有channel的概念,在RabbitMq中,channel表示逻辑连接或者叫虚拟连接,是棣属于TCP连接的。一个TCP连接里可以创建多个channel,在Rabbit MQ里,消息的发送和接收都是基于channel的。

44557a8d9c9191b2a5cd4d3f426a450e.png

有了TCP连接后,还需要channel的原因如下:

  • 创建和销毁TCP连接很耗时;

  • 打开太多TCP连接,耗操作系统资源,并发量大到一定程度,系统的吞吐量会降低;

  • 使用一个connection多channel的方式,可以提升连接的利用率。

因此采用多个channel多路复用一个TCP连接的方式才比较合理。

https://www.rabbitmq.com/api-guide.html#connection-and-channel-lifspan

说话要有证据,官网上这样说:

Client connections are meant to be long-lived. The underlying protocol is designed and optimized for long running connections. That means that opening a new connection per operation, e.g. a message published, is unnecessary and strongly discouraged as it will introduce a lot of network roundtrips and overhead.

Channels are also meant to be long-lived but since many recoverable protocol errors will result in channel closure, channel lifespan could be shorter than that of its connection. Closing and opening new channels per operation is usually unnecessary but can be appropriate. When in doubt, consider reusing channels first.

Channel-level exceptions such as attempts to consume from a queue that does not exist will result in channel closure. A closed channel can no longer be used and will not receive any more events from the server (such as message deliveries). 

翻译一下:

客户端连接是要长久存活的,基础协议(AMQP协议)里做了连接要长久存活的优化。就是说为了发一条消息而打开一个新的连接没有必要,而且强烈不推荐。因为会带来额外的通信开销。

Channel也是长久存活的,但是由于许多可以恢复的错误会导致Channel关闭,所以channel的生命周期会比连接短。每次操作都关闭和打开新Channel,没有必要但是却可以是比较合适的选择。如果不知道怎么做更好的时候,优先复用Channel。

Channel级别的异常比如尝试消费一个不存在的队列会导致Channel关闭。一个关闭的Channel不会再被使用,也不会再收到任何如消息传递之类的服务端事件。

简单来说,Channel是为了复用连接产生的,如果Channel异常了,就可以直接关闭Channel不至于整个连接都关闭,减少开销。

Java的Channel和MQ的Channel比较

本质上,不管是Java的Channel还是MQ的Channel都为了连接的复用而生。所以Java的Channel出现在java.nio包里,做的是io多路复用的事情。只是因为Channel是个逻辑概念,究竟是先产生Channel,还是必须依附于连接由设计者自己说了算。

总结

很多公司都有渠道(Channel)一说,但是不同的场景意义各不相同。比如:业务渠道、支付渠道。本质上,这些渠道的对端各有不同,又希望复用,才产生了网关。从这个意义上来说,网关非常合适用多路复用来解释。

Channel这个概念是理解NIO、netty的基础,也是理解MQ底层通信过程的基础。不管是什么类型的MQ,在创建连接时都需要同时配置channel,只不过有时候使用了约定大于配置,不是显式的。其实这一篇我写了万把字。太长了所以拆成了几篇,看完这几篇,我预期上很多之前看不懂的源会清晰很多。

往期推荐

避免线上故障的10条建议

为什么要持续重构

代码整洁之道--边界

LRU缓存实现(Java)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值