ClickHouse源码阅读(0000 1011) —— ClickHouse Client端如何接收Server端发送回来的数据

这篇文章中提到:client端对于非INSERT语句,processOrdinaryQuery()方法的代码如下:

        /// Process the query that doesn't require transferring data blocks to the server.
        void processOrdinaryQuery()
        {
            //客户端sendQuery()后, SQL及其他信息封装成数据包发送到服务端, //数据包被发送到对应的端口, 由服务端*Handler进行具体的处理, run() -> runImpl() -> receivePacket()
            connection->sendQuery(query, query_id, QueryProcessingStage::Complete, &context.getSettingsRef(), nullptr,
                                  true);
            sendExternalTables();
            receiveResult();//客户端接收服务端的处理结果, 也是以数据包的形式发送过来的
        }

主要包含三个步骤:1)将SQL从Client端发送到Server端;2)sendExternalTables();3)Client端等待Server端发送回来的结果。

receiveResult()方法如下:

        /// Receives and processes packets coming from server. Also checks if query execution should be cancelled.
        //接收处理服务器Server端发送的数据包, 同时检查SQL执行是否被取消
        void receiveResult()
        {
            InterruptListener interrupt_listener;
            bool cancelled = false;

            // TODO: get the poll_interval from commandline.
            const auto receive_timeout = connection->getTimeouts().receive_timeout;
            constexpr size_t default_poll_interval = 1000000; /// in microseconds
            constexpr size_t min_poll_interval = 5000; /// in microseconds
            const size_t poll_interval
                    = std::max(min_poll_interval,
                               std::min<size_t>(receive_timeout.totalMicroseconds(), default_poll_interval));

            while (true)
            {
                Stopwatch receive_watch(CLOCK_MONOTONIC_COARSE);

                while (true)
                {
                    /// Has the Ctrl+C been pressed and thus the query should be cancelled?
                    /// If this is the case, inform the server about it
                    /// and receive the remaining packets to avoid losing sync.
                    // SQL执行期间要一直检查SQL是否被客户端终止
                    // 如果SQL执行被终止, 需要通知服务器 并 接收剩余的数据包以避免丢失同步
                    if (!cancelled)
                    {
                        auto cancelQuery = [&]
                        {
                            connection->sendCancel();
                            cancelled = true;
                            if (is_interactive)
                                std::cout << "Cancelling query." << std::endl;

                            /// Pressing Ctrl+C twice results in shut down.
                            interrupt_listener.unblock();
                        };

                        if (interrupt_listener.check())
                        {//客户端Ctrl+C手动终止查询
                            cancelQuery();
                        } else
                        {
                            double elapsed = receive_watch.elapsedSeconds();
                            if (elapsed > receive_timeout.totalSeconds())
                            {//连接超时, 终止查询
                                std::cout << "Timeout exceeded while receiving data from server."
                                          << " Waited for " << static_cast<size_t>(elapsed) << " seconds,"
                                          << " timeout is " << receive_timeout.totalSeconds() << " seconds."
                                          << std::endl;

                                cancelQuery();
                            }
                        }
                    }

                    /// Poll for changes after a cancellation check,
                    /// otherwise it never reached because of progress updates from server.
                    // 进行cancellation检查后, 检查working_buffer是否被写满或是否经过了指定的时间间隔
                    // 跳出当前while循环, 接收剩余的数据包
                    if (connection->poll(poll_interval))
                        break;
                }

                //receiveAndProcessPacket()是客户端接收服务端的处理结果
                //receiveAndProcessPacket()=true, 继续执行while循环
                if (!receiveAndProcessPacket())
                    break;
            }

            if (cancelled && is_interactive)
                std::cout << "Query was cancelled." << std::endl;
        }

这部分主要是包含两个while(true){}循环,Client端"阻塞地"等待Server端发送回来的数据。简化如下:

            while (true)
            {
                Stopwatch receive_watch(CLOCK_MONOTONIC_COARSE);

                while (true)
                {
                    if (!cancelled)
                    {
                        //进行cancellation检查
                    }

                    if (connection->poll(poll_interval))
                        break;
                }

                //receiveAndProcessPacket()是客户端接收服务端的处理结果
                //receiveAndProcessPacket()=true, 继续执行while循环
                if (!receiveAndProcessPacket())
                    break;
            }

在内层循环中,一是进行了cancellation检查,二是connection->poll(poll_interval)这个方法,这个方法是在检查working_buffer是否被写满或是否经过了指定的时间间隔。

poll()方法的具体调用如下:

    bool Connection::poll(size_t timeout_microseconds) {
        return static_cast<ReadBufferFromPocoSocket &>(*in).poll(timeout_microseconds);
    }
    // offset() != buffer().size() 表示 working_buffer还没有写满;
    // socket.poll() 方法 是指是否经过了指定的时间间隔
    // || 表示 逻辑或 ; | 表示按位或
    bool ReadBufferFromPocoSocket::poll(size_t timeout_microseconds)
    {
        return offset() != buffer().size() ||
               socket.poll(timeout_microseconds, Poco::Net::Socket::SELECT_READ | Poco::Net::Socket::SELECT_ERROR); //Poco::Net::Socket::SELECT_READ | Poco::Net::Socket::SELECT_ERROR = 0001 | 0100 = 01001
    }

这里是使用的POCO::Net库中的方法,poll完成系统调用和操作:(参考
1)Poco::Socket::poll()并不是linux中系统调用::poll(),而是对::select()、::poll()、::epoll()三种的封装,具体选择哪种模式在编译库时根据定义宏来决定。
2)判断socket描述符是否是非法的,不合法抛出InvalidSocketException异常。
3)poll()只对调用者的socket描述符进行读、写、错误进行监听。

在CK源码中,是根据宏定义使用的::poll()方法。mode传值也只是Poco::Net::Socket::SELECT_READ | Poco::Net::Socket::SELECT_ERROR(此处是按位或,后面有按位与的操作)

 

如果poll()方法返回true,则跳出内层循环,执行receiveAndProcessPacket()方法:

/// Receive a part of the result, or progress info or an exception and process it.
        /// Returns true if one should continue receiving packets.
        // 接收部分结果、进度信息或异常信息并进行处理。
        // ***如果应继续接收数据包, 则返回true***
        bool receiveAndProcessPacket()
        {
            Connection::Packet packet = connection->receivePacket();

            switch (packet.type)
            {
                case Protocol::Server::Data:
                    onData(packet.block);
                    return true;

                case Protocol::Server::Progress:
                    onProgress(packet.progress);
                    return true;

                case Protocol::Server::ProfileInfo:
                    onProfileInfo(packet.profile_info);
                    return true;

                case Protocol::Server::Totals:
                    onTotals(packet.block);
                    return true;

                case Protocol::Server::Extremes:
                    onExtremes(packet.block);
                    return true;

                case Protocol::Server::Exception:
                    onException(*packet.exception);
                    last_exception = std::move(packet.exception);
                    return false;

                case Protocol::Server::Log:
                    onLogData(packet.block);
                    return true;

                case Protocol::Server::EndOfStream:
                    onEndOfStream();
                    return false;

                default:
                    throw Exception("Unknown packet from server", ErrorCodes::UNKNOWN_PACKET_FROM_SERVER);
            }
        }

这个方法是在Client端接收Server端发送过来的数据包,根据数据包的类型选择不同的处理方式。如果Client端接收Server端发送过来的Exception或EndOfStream类型的数据包,则返回false,则!receiveAndProcessPacket()=true,则跳出外层循环,到此SQL执行也就基本结束了;如果如果Client端接收Server端发送过来的Data/Progress/ProfileInfo等类型的数据包,则该方法返回true,则!receiveAndProcessPacket()=false,则继续进行循环等待Server端发送的数据包。

 

以Client端接收Server端发送过来的Data类型的数据包,则调用onData()方法,简单来讲就是将接收到的数据flush到终端。[复杂点需要看下这几个方法:initBlockOutputStream(block); block_out_stream->write(block); block_out_stream->flush();]

        void onData(Block &block)
        {
            if (written_progress_chars)
                clearProgress();

            if (!block)
                return;

            processed_rows += block.rows();
            initBlockOutputStream(block);

            /// The header block containing zero rows was used to initialize block_out_stream, do not output it.
            //头部数据块使用与初始化block_out_stream的, 不包含数据,不要将这个块输出
            if (block.rows() != 0)
            {
                block_out_stream->write(block);
                written_first_block = true;
            }

            /// Received data block is immediately displayed to the user.
            // 接收到的数据块立即显示给用户
            block_out_stream->flush();

            /// Restore progress bar after data block.
            writeProgress();
        }

到这里基本上就结束了。

 

感触:看代码的过程中很容易陷进去深究一个方法,殊不知有的方法只是调用了外部类库中的方法,只需要理解一下就行了,让我苦恼了老半天🤦‍♂️。

对一些基础知识总是没理解透,现在想想,Client端和Server端应该是通过Socket进行通信的,互相发送数据到Socket,再将Socket中的数据读/写到对应的ReadBuffer/WriterBuffe中。

Buffer除了从Socket中读写数据,还会从stream、memory、File、FileDescriptor文件描述符等中读写数据到Buffer。

还需要在看看Socket编程文件描述符、I/O模型等等基础知识,总是理解的不够透测🤦‍♂️

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值