<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; C++</title>
	<atom:link href="https://blog.gmem.cc/category/work/cpp/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Sun, 12 Apr 2026 02:07:19 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>记录一次KeyDB缓慢的定位过程</title>
		<link>https://blog.gmem.cc/debugging-slow-keydb</link>
		<comments>https://blog.gmem.cc/debugging-slow-keydb#comments</comments>
		<pubDate>Thu, 28 Jan 2021 07:04:58 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[Redis]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35755</guid>
		<description><![CDATA[<p>环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点： [crayon-69db8c96ad40c074921796/] KeyDB配置 KeyDB通过StatefulSet管理，一共有三个实例：  [crayon-69db8c96ad411614545947/] 这三个实例： 由于反亲和设置，会在每个节点上各运行一个实例 启用Active - Active（--active-replica）模式的多主（--multi-master）复制 ：每个实例都是另外两个的Slave，每个实例都支持读写 故障描述 触发条件 出现一个节点宕机的情况，就可能出现此故障。经过一段时间以后，会出现GET/PUT或者任何其它请求处理缓慢的情况。 故障特征 此故障有两个明显的特征： 故障出现前需要等待的时间，随机性很强，有时甚至测试了数小时都没有发现请求缓慢的情况。常常发生的情况是，宕机后剩下的两个实例，一个很快出现缓慢问题，另外一个却还能运行较长时间 请求处理延缓的时长不定，有时候没有明显延缓，有时候长达10+秒。而且一次缓慢请求后，可以跟着10多次正常速度处理的请求。这个特征提示故障和某种周期性的、长时间占用的锁有关。在锁被释放的间隙，请求可以被快速处理 故障分析 <a class="read-more" href="https://blog.gmem.cc/debugging-slow-keydb">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/debugging-slow-keydb">记录一次KeyDB缓慢的定位过程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">环境说明</span></div>
<div class="blog_h2"><span class="graybg">运行环境</span></div>
<p>这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点：</p>
<pre class="crayon-plain-tag"># kubectl get node -o wide
NAME              STATUS   VERSION   INTERNAL-IP      OS-IMAGE                KERNEL-VERSION              CONTAINER-RUNTIME
192.168.104.51    Ready    v1.18.3   192.168.104.51   CentOS Linux 7 (Core)   3.10.0-862.3.2.el7.x86_64   docker://19.3.9
192.168.104.72    Ready    v1.18.3   192.168.104.72   CentOS Linux 7 (Core)   3.10.0-862.3.2.el7.x86_64   docker://19.3.9
192.168.104.108   Ready    v1.18.3   192.168.104.108  CentOS Linux 7 (Core)   3.10.0-862.3.2.el7.x86_64   docker://19.3.9</pre>
<div class="blog_h2"><span class="graybg">KeyDB配置</span></div>
<p><a href="/keydb-study-note">KeyDB</a>通过StatefulSet管理，一共有三个实例： </p>
<pre class="crayon-plain-tag"># kubectl -n default get pod -o wide -l app.kubernetes.io/name=keydb
NAME             READY   STATUS    RESTARTS     IP             NODE            
keydb-0   1/1     Running   0            172.29.2.63     192.168.104.108 
keydb-1   1/1     Running   0            172.29.1.69     192.168.104.72  
keydb-2   1/1     Running   0            172.29.1.121    192.168.104.51</pre>
<p>这三个实例：</p>
<ol>
<li>由于反亲和设置，会在每个节点上各运行一个实例</li>
<li>启用Active - Active（--active-replica）模式的多主（--multi-master）复制 ：每个实例都是另外两个的Slave，每个实例都支持读写</li>
</ol>
<div class="blog_h1"><span class="graybg">故障描述</span></div>
<div class="blog_h2"><span class="graybg">触发条件</span></div>
<p>出现一个节点宕机的情况，就可能出现此故障。经过一段时间以后，会出现<span style="background-color: #c0c0c0;">GET/PUT或者任何其它请求处理缓慢的情况</span>。</p>
<div class="blog_h2"><span class="graybg">故障特征</span></div>
<p>此故障有两个明显的特征：</p>
<ol>
<li>故障出现前需要等待的时间，随机性很强，有时甚至测试了数小时都没有发现请求缓慢的情况。常常发生的情况是，宕机后剩下的两个实例，一个很快出现缓慢问题，另外一个却还能运行较长时间</li>
<li>请求处理延缓的时长不定，有时候没有明显延缓，有时候长达10+秒。而且一次缓慢请求后，可以跟着10多次正常速度处理的请求。这个特征提示故障和某种<span style="background-color: #c0c0c0;">周期性的、长时间占用的锁</span>有关。在锁被释放的间隙，请求可以被快速处理</li>
</ol>
<div class="blog_h1"><span class="graybg">故障分析</span></div>
<div class="blog_h2"><span class="graybg">触发故障</span></div>
<p>我们将节点192.168.104.108强制关闭，这样实例keydb-0无法访问，另外两个节点无法和它进行Replication。</p>
<p>分别登录另外两个节点，监控GET/SET操作的性能：</p>
<pre class="crayon-plain-tag">kubectl -n default exec -it keydb-1 -- bash -c  \
  'while true; do key=keydb-1-$(date +%s); keydb-cli set $key $key-val; keydb-cli get $key; done'

kubectl -n default exec -it keydb-2 -- bash -c \
 'while true; do key=keydb-2-$(date +%s); keydb-cli set $key $key-val; keydb-cli get $key; done'</pre>
<p>监控Replication相关信息：</p>
<pre class="crayon-plain-tag">watch -- kubectl -n default exec -i keydb-1 -- keydb-cli info replication

watch -- kubectl -n default exec -i keydb-2 -- keydb-cli info replication</pre>
<p>监控KeyDB日志： </p>
<pre class="crayon-plain-tag">kubectl -n default logs  keydb-1 -f

kubectl -n default logs  keydb-2 -f</pre>
<p>经过一段时间，keydb-1请求处理随机延缓的情况出现：</p>
<pre class="crayon-plain-tag">127.0.0.1:6379&gt; set hello world
OK
(1.24s)
127.0.0.1:6379&gt; set hello world
OK
(8.96s)
127.0.0.1:6379&gt; get hello
"world"
(5.99s)
127.0.0.1:6379&gt; get hello
"world"
(9.44s) </pre>
<p>此时keydb-2仍然正常运行，请求处理速度正常</p>
<div class="blog_h2"><span class="graybg">缓慢查询</span></div>
<p>获取keydb-1的慢查询，没有发现有价值的信息。而且延缓的时间没有计算在内：</p>
<pre class="crayon-plain-tag">127.0.0.1:6379&gt; slowlog get 10                                         
1) 1) (integer) 7                                                      
   2) (integer) 1611833042                                             
   3) (integer) 14431               # 最慢的查询才耗时14ms                                            
   4) 1) "set"                                                         
      2) "keydb-1-1611833042"                                          
      3) "keydb-1-1611833042-val"                                      
   5) "127.0.0.1:38488"                                                
   6) ""                                                               
2) 1) (integer) 6                                                      
   2) (integer) 1611831322                                             
   3) (integer) 14486                                                  
   4) 1) "get"                                                         
      2) "keydb-1-1611831312"                                          
   5) "127.0.0.1:51680"                                                
   6) ""  </pre>
<div class="blog_h2"><span class="graybg">日志分析</span></div>
<p>部署KeyDB已经设置<pre class="crayon-plain-tag">--loglevel debug</pre>，以获得尽可能详尽的日志。</p>
<p>由于正在运行不间断执行SET/GET操作的脚本，因此日志量很大而刷屏，但是每隔一段时间就会出现卡顿。下面是keydb-1的日志片段：</p>
<pre class="crayon-plain-tag">7:11:S 28 Jan 2021 08:57:51.233 - Client closed connection
7:11:S 28 Jan 2021 08:57:51.251 - Accepted 127.0.0.1:44224
7:11:S 28 Jan 2021 08:57:51.252 - Client closed connection
7:12:S 28 Jan 2021 08:57:51.276 - Accepted 127.0.0.1:44226
7:11:S 28 Jan 2021 08:57:51.277 - Client closed connection
# 这一行日志之后，卡顿了10s。没有任何日志输出
7:11:S 28 Jan 2021 08:57:51.279 * Connecting to MASTER keydb-0.keydb:6379
7:11:S 28 Jan 2021 08:58:01.290 * Unable to connect to MASTER: Resource temporarily unavailable
7:11:S 28 Jan 2021 08:58:01.290 - Accepted 127.0.0.1:44228
7:11:S 28 Jan 2021 08:58:01.290 - Accepted 127.0.0.1:44264</pre>
<p>从日志信息上可以看到，卡顿前keydb-1正在尝试连接到已经宕机的keydb-0，这个连接尝试被阻塞10秒后报<pre class="crayon-plain-tag">EAGAIN</pre>错误。</p>
<p>阻塞期间SET/GET请求得不到处理，猜测原因包括：</p>
<ol>
<li>连接keydb-0的时候，占用了某种全局的锁，SET/GET请求也需要持有该锁</li>
<li>连接keydb-0、处理SET/GET请求，由同一线程负责</li>
</ol>
<p>第2种猜测应该不大可能，因为KeyDB宣称的优势之一就是，支持多线程处理请求。并且我们设置了参数<pre class="crayon-plain-tag">--server-threads 2</pre>，也就是有两个线程用于处理请求。</p>
<p>EAGAIN这个报错也没有参考价值，因为目前不卡顿的实例keydb-2输出的日志是一样的，只是没有任何卡顿：</p>
<pre class="crayon-plain-tag">7:11:S 28 Jan 2021 08:19:22.624 * Connecting to MASTER keydb-0.keydb:6379
# 仅仅耗时5ms即检测到连接失败
7:11:S 28 Jan 2021 08:19:22.629 * Unable to connect to MASTER: Resource temporarily unavailable</pre>
<div class="blog_h2"><span class="graybg">源码分析</span></div>
<div class="blog_h3"><span class="graybg">复制定时任务</span></div>
<p>我们使用的KeyDB版本是5.3.3，尝试用关键字“Connecting to MASTER”搜索，发现只有一个匹配，位于<pre class="crayon-plain-tag">replicationCron</pre>函数中。从函数名称上就可以看到，它是和复制（Replication）有关的定时任务。</p>
<p>KeyDB启动时会调用<pre class="crayon-plain-tag">initServer</pre>进行初始化，后者会在事件循环中每1ms调度一次<pre class="crayon-plain-tag">serverCron</pre>。serverCron负责后台任务的总体调度，它的一个职责就是，每1s调度一次replicationCron函数。</p>
<p>下面看一下replicationCron的源码：</p>
<pre class="crayon-plain-tag">/* Replication cron function, called 1 time per second. */
void replicationCron(void) {
    static long long replication_cron_loops = 0;
    serverAssert(GlobalLocksAcquired());
    listIter liMaster;
    listNode *lnMaster;
    listRewind(g_pserver-&gt;masters, &amp;liMaster);
    // 遍历当前实例的每一个Master
    while ((lnMaster = listNext(&amp;liMaster)))
    {
        redisMaster *mi = (redisMaster*)listNodeValue(lnMaster);
        std::unique_lock&lt;decltype(mi-&gt;master-&gt;lock)&gt; ulock;
        // 获得              Master的 客户端的 锁
        if (mi-&gt;master != nullptr)
            ulock = decltype(ulock)(mi-&gt;master-&gt;lock);

        /* Non blocking connection timeout? */
        // 如果当前复制状态为：正在连接到Master
        // 或者复制状态处于握手阶段（包含多个状态）且超时了
        if (mi-&gt;masterhost &amp;&amp;
            (mi-&gt;repl_state == REPL_STATE_CONNECTING ||
            slaveIsInHandshakeState(mi)) &amp;&amp;
            (time(NULL)-mi-&gt;repl_transfer_lastio) &gt; g_pserver-&gt;repl_timeout)
        {
            // 那么取消握手 —— 取消进行中的非阻塞连接尝试，或者取消进行中的RDB传输
            serverLog(LL_WARNING,"Timeout connecting to the MASTER...");
            cancelReplicationHandshake(mi);
        }

        /* Bulk transfer I/O timeout? */
        // 如果当前正在接收来自Master的RDB文件且超时了
        if (mi-&gt;masterhost &amp;&amp; mi-&gt;repl_state == REPL_STATE_TRANSFER &amp;&amp;
            (time(NULL)-mi-&gt;repl_transfer_lastio) &gt; g_pserver-&gt;repl_timeout)
        {
            serverLog(LL_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the 'repl-timeout' parameter in keydb.conf to a larger value.");
            // 那么取消握手
            cancelReplicationHandshake(mi);
        }

        /* Timed out master when we are an already connected replica? */
        // 如果当前复制状态为：已连接。而且超时之前没有活动（正常情况下有心跳维持）
        if (mi-&gt;masterhost &amp;&amp; mi-&gt;master &amp;&amp; mi-&gt;repl_state == REPL_STATE_CONNECTED &amp;&amp;
            (time(NULL)-mi-&gt;master-&gt;lastinteraction) &gt; g_pserver-&gt;repl_timeout)
        {
            // 那么释放掉客户端资源
            serverLog(LL_WARNING,"MASTER timeout: no data nor PING received...");
            if (FCorrectThread(mi-&gt;master))
                freeClient(mi-&gt;master);
            else
                freeClientAsync(mi-&gt;master);
        }

        /* Check if we should connect to a MASTER */
        // 上面几个分支都不会匹配我们的场景，因为keydb-0已经宕机，因此
        // 状态必然是REPL_STATE_CONNECT
        if (mi-&gt;repl_state == REPL_STATE_CONNECT) {
            // 这一行就是卡顿前的日志
            serverLog(LL_NOTICE,"Connecting to MASTER %s:%d",
                mi-&gt;masterhost, mi-&gt;masterport);
            // 发起连接
            if (connectWithMaster(mi) == C_OK) {
                serverLog(LL_NOTICE,"MASTER &lt;-&gt; REPLICA sync started");
            }
        }

        // 每秒钟发送心跳给Master
        if (mi-&gt;masterhost &amp;&amp; mi-&gt;master &amp;&amp;
            !(mi-&gt;master-&gt;flags &amp; CLIENT_PRE_PSYNC))
            replicationSendAck(mi);
    }

    // 后面处理和本实例的Slave有关的逻辑，例如发送心跳。和我们的场景无关，略...
}</pre>
<p>很明显，卡顿是因为调用<pre class="crayon-plain-tag">connectWithMaster</pre>导致的。从代码注释也可以看到，KeyDB期望这个连接操作是非阻塞的，但是不知道为何，在我们的场景中严重的阻塞了。</p>
<p>进一步查看connectWithMaster的代码：</p>
<pre class="crayon-plain-tag">int connectWithMaster(redisMaster *mi) {
    int fd;

    fd = anetTcpNonBlockBestEffortBindConnect(NULL,
        mi-&gt;masterhost,mi-&gt;masterport,NET_FIRST_BIND_ADDR);
    if (fd == -1) {
        int sev = g_pserver-&gt;enable_multimaster ? LL_NOTICE : LL_WARNING;
        // 这一行是卡顿10s后的日志，因此阻塞发生在anetTcpNonBlockBestEffortBindConnect函数中
        serverLog(sev,"Unable to connect to MASTER: %s", strerror(errno));
        return C_ERR;
    }
    // ...
}

int anetTcpNonBlockBestEffortBindConnect(char *err, char *addr, int port,
                                         char *source_addr)
{
    return anetTcpGenericConnect(err,addr,port,source_addr,
            // 非阻塞 + BestEffort绑定
            ANET_CONNECT_NONBLOCK|ANET_CONNECT_BE_BINDING);
}


static int anetTcpGenericConnect(char *err, char *addr, int port,
                                 char *source_addr, int flags)
{
    int s = ANET_ERR, rv;
    char portstr[6];  /* strlen("65535") + 1; */
    struct addrinfo hints, *servinfo, *bservinfo, *p, *b;

    snprintf(portstr,sizeof(portstr),"%d",port);
    memset(&amp;hints,0,sizeof(hints));
    // 不指定地址族，这会触发getaddrinfo同时进行A/AAAA查询
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    // 根据Master的主机名查找得到IP地址信息（addrinfo）列表
    if ((rv = getaddrinfo(addr,portstr,&amp;hints,&amp;servinfo)) != 0) {
        anetSetError(err, "%s", gai_strerror(rv));
        return ANET_ERR;
    }
    // 遍历Master的IP地址列表
    for (p = servinfo; p != NULL; p = p-&gt;ai_next) {
        // 创建套接字，如果socket/connect调用失败，则尝试下一个
        if ((s = socket(p-&gt;ai_family,p-&gt;ai_socktype,p-&gt;ai_protocol)) == -1)
            continue;
        // 设置套接字选项SO_REUSEADDR
        if (anetSetReuseAddr(err,s) == ANET_ERR) 
            goto error;
        // 设置套接字选项 SO_REUSEPORT
        if (flags &amp; ANET_CONNECT_REUSEPORT &amp;&amp; anetSetReusePort(err, s) != ANET_OK)
            goto error;
        // 调用fcntl设置 O_NONBLOCK
        if (flags &amp; ANET_CONNECT_NONBLOCK &amp;&amp; anetNonBlock(err,s) != ANET_OK)
            goto error;
        if (source_addr) {
            int bound = 0;
            /* Using getaddrinfo saves us from self-determining IPv4 vs IPv6 */
            // 解析源地址
            if ((rv = getaddrinfo(source_addr, NULL, &amp;hints, &amp;bservinfo)) != 0)
            {
                anetSetError(err, "%s", gai_strerror(rv));
                goto error;
            }
            for (b = bservinfo; b != NULL; b = b-&gt;ai_next) {
                // 绑定到第一个源地址
                if (bind(s,b-&gt;ai_addr,b-&gt;ai_addrlen) != -1) {
                    bound = 1;
                    break;
                }
            }
            freeaddrinfo(bservinfo);
            if (!bound) {
                // 绑定源地址失败，跳转到Best Effort绑定
                anetSetError(err, "bind: %s", strerror(errno));
                goto error;
            }
        }
        // 发起连接
        if (connect(s,p-&gt;ai_addr,p-&gt;ai_addrlen) == -1) {
            // 我们的场景下套接字是非阻塞的，因此这里会立即返回EINPROGRESS，属于预期行为
            if (errno == EINPROGRESS &amp;&amp; flags &amp; ANET_CONNECT_NONBLOCK)
                goto end;
            // 其它错误均认为失败，尝试连接下一个Master地址
            close(s);
            s = ANET_ERR;
            continue;
        }

        goto end;
    }
    if (p == NULL)
        anetSetError(err, "creating socket: %s", strerror(errno));

error:
    if (s != ANET_ERR) {
        close(s);
        s = ANET_ERR;
    }

end:
    freeaddrinfo(servinfo);

    // 上面指定源地址，绑定失败时跳转到此处。尝试不指定源地址来连接
    if (s == ANET_ERR &amp;&amp; source_addr &amp;&amp; (flags &amp; ANET_CONNECT_BE_BINDING)) {
        return anetTcpGenericConnect(err,addr,port,NULL,flags);
    } else {
        return s;
    }
}</pre>
<p>尽管可以确定connectWithMaster调用的anetTcpGenericConnect就是发生阻塞的地方，但是从代码上看不出什么问题，就是简单的socket、bind，外加一个非阻塞的connect操作。</p>
<div class="blog_h3"><span class="graybg">请求处理逻辑</span></div>
<p>从现象上我们已经看到了，复制定时器卡顿的时候，请求处理也无法进行。通过代码分析，也明确了卡顿期间，复制定时器持有Master的客户端的锁。</p>
<p>那么，关于请求处理（线程？）会和复制定时器产生锁争用的猜测是否正确呢？</p>
<div class="blog_h2"><span class="graybg">单步跟踪</span></div>
<p>为了精确定位阻塞的代码，我们使用GDB进行单步跟踪：</p>
<pre class="crayon-plain-tag">#              需要特权模式，否则无法加载符号表
docker run -it --rm --name gdb --privileged --net=host --pid=host --entrypoint gdb docker.gmem.cc/debug

(gdb) attach 449
(gdb) break replication.cpp:3084
# 连续执行s，以step into anet.c
(gdb) s
# 连续执行n命令
(gdb) n
# 卡顿后，查看变量
# 解析的地址
(gdb) p addr
$2 = 0x7f2f31411281 "keydb-0.keydb"
# getaddrinfo的返回值
(gdb) p rv
$3 = -3</pre>
<p>进入anetTcpGenericConnect后，逐行执行，多次测试，均在<pre class="crayon-plain-tag">anet.c</pre>的291行出现卡顿：</p>
<pre class="crayon-plain-tag">if ((rv = getaddrinfo(addr,portstr,&amp;hints,&amp;servinfo)) != 0) {
    anetSetError(err, "%s", gai_strerror(rv));
    return ANET_ERR;
}</pre>
<p>也就是说，调用getaddrinfo函数耗时可能长达数秒。这是来自glibc的标准函数，用于将主机名解析为IP地址。</p>
<p>调试过程中发现此函数的返回值是-3，我们的场景中，需要解析的地址是keydb-0.keydb，卡顿时函数的返回值是-3，<pre class="crayon-plain-tag">man getaddrinfo</pre>可以了解到此返回值的意义：</p>
<p style="padding-left: 30px;">EAI_AGAIN  The name server returned a temporary failure indication. Try again later.</p>
<p>乍看起来，好像是<a href="/tcp-ip-study-note#dns">DNS</a>服务器，也就是K8S的<a href="/coredns-study-note">CoreDNS</a>存在问题。但无法解释此时keydb-2.keydb没有受到影响？</p>
<div class="blog_h2"><span class="graybg">检查CoreDNS</span></div>
<p>为了确认CoreDNS是否存在问题，我们分别在宿主机上、两个实例的网络命名空间中进行验证：</p>
<pre class="crayon-plain-tag"># nslookup keydb-0.keydb.default.svc.cluster.local 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb.default.svc.cluster.local: NXDOMAIN

# nslookup keydb-0.keydb.svc.cluster.local 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb.svc.cluster.local: NXDOMAIN

# nslookup keydb-0.keydb.cluster.local 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb.cluster.local: NXDOMAIN

# nslookup keydb-0.keydb 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb: SERVFAIL</pre>
<p>反复测试循环测试，没有任何解析缓慢的现象。此外，查看CoreDNS的日志，我们也发现了来自keydb-1.keydb和keydb-2.keydb的查询请求，请求都是通过UDP协议发送的，处理耗时都是亚毫秒级别。</p>
<p>也就是说，从KeyDB实例所在宿主机/命名空间到CoreDNS的网络链路、CoreDNS服务器自身，都没有问题。</p>
<p>这就让人头疼了……难道问题出在getaddrinfo函数内部？或者在单步跟踪时判断错误，问题和DNS无关？为了确认，我们在CoreDNS上动了点手脚，强制将keydb-0.keydb解析到一个不存在的IP地址：</p>
<pre class="crayon-plain-tag">.:53 {
    # ...
    hosts {
        192.168.144.51  keydb-1.keydb
    }
    # ...
}</pre>
<p>结果很快，卡顿的问题就消失了。所以，我们更加怀疑问题出在getaddrinfo函数上了。</p>
<div class="blog_h2"><span class="graybg">调试getaddrinfo</span></div>
<p>查看文件/etc/lsb-release，可以看到KeyDB镜像是基于Ubuntu 18.04.4 LTS构建的，使用的libc6版本是2.27-3ubuntu1。</p>
<p>在launchpad.net找到了它的<a href="http://launchpadlibrarian.net/365856914/libc6-dbg_2.27-3ubuntu1_amd64.deb">调试文件</a>和<a href="http://launchpadlibrarian.net/365856911/glibc-source_2.27-3ubuntu1_all.deb">源码</a>。下载deb包，解压后复制到GDB容器，然后设置一下调试文件目录，就可以step into到glibc的代码进行跟踪了：</p>
<pre class="crayon-plain-tag">ar x libc6-dbg_2.27-3ubuntu1_amd64.deb
tar -xf data.tar.xz 

# 拷贝到我们正在运行GDB的容器
docker cp usr gdb:/root

# 修改调试文件搜索目录
(gdb) set debug-file-directory /root/usr/lib/debug
# 打断点，下面是缓慢的执行路径
(gdb) b anet.c:291
(gdb) b getaddrinfo.c:342
(gdb) b getaddrinfo.c:786  
# (gdb) print fct4
# $2 = (nss_gethostbyname4_r) 0x7f32f97e9a70 &lt;_nss_dns_gethostbyname4_r&gt;
(gdb) b dns-host.c:317
(gdb) b res_query.c:336
(gdb) b res_query.c:495                       # invoke __res_context_querydomain
(gdb) b res_query.c:601                       # invoke __res_context_query    
(gdb) b res_query.c:216                       # invoke __res_context_send
(gdb) b res_send.c:1066 if buflen==45         # send_dg</pre>
<p>通过调试，我们发现getaddrinfo会依次对4个名字进行DNS查询：</p>
<p style="padding-left: 30px;">keydb-0.keydb.default.svc.cluster.local. <br />keydb-0.keydb.svc.cluster.local. <br />keydb-0.keydb.cluster.local.<br />keydb-0.keydb.</p>
<p>CoreDNS的日志显示，所有请求都快速的处理完毕：</p>
<pre class="crayon-plain-tag">4242 "A IN keydb-0.keydb.default.svc.cluster.local. udp 68 false 512" NXDOMAIN qr,aa,rd 161 0.000215337s
38046 "AAAA IN keydb-0.keydb.default.svc.cluster.local. udp 68 false 512" NXDOMAIN qr,aa,rd 161 0.000203934s

23194 "A IN keydb-0.keydb.svc.cluster.local. udp 63 false 512" NXDOMAIN qr,aa,rd 156 0.000301011s
23722 "AAAA IN keydb-0.keydb.svc.cluster.local. udp 63 false 512" NXDOMAIN qr,aa,rd 156 0.000125386s

36552 "A IN keydb-0.keydb.cluster.local. udp 59 false 512" NXDOMAIN qr,aa,rd 152 0.000281247s
217 "AAAA IN keydb-0.keydb.cluster.local. udp 59 false 512" NXDOMAIN qr,aa,rd 152 0.000150689s

6776 "A IN keydb-0.keydb. udp 45 false 512" NOERROR - 0 0.000196686s
6776 "A IN keydb-0.keydb. udp 45 false 512" NOERROR - 0 0.000157011s </pre>
<p>最后一个名字，也就是传递给getaddrinfo的原始请求keydb-0.keydb.的处理过程有以下值得注意的点：</p>
<ol>
<li>从GDB角度来看，<span style="background-color: #c0c0c0;">卡顿就是在解析该名字时出现</span></li>
<li>从CoreDNS日志上看，没有AAAA请求。由于KeyDB<span style="background-color: #c0c0c0;">指定了AF_UNSPEC，getaddrinfo会同时发送并等待A/AAAA应答</span>。可能<span style="background-color: #c0c0c0;">因为某种原因，该名字的AAAA解析过程没有完成，导致getaddrinfo一直等待到超时</span>。作为对比，没有卡顿的keydb-2的A/AAAA查询处理过程都是正常的</li>
<li>其它名字是一次A请求，一次AAAA请求。该名字却是两次A请求，而且，<span style="background-color: #c0c0c0;">第一次A请求日志出现了数秒后，第二次日志才出现</span>。有可能第二次是getaddrinfo没有收到应答而进行的重试</li>
<li>前三个名字分别的错误码是NXDOMAIN，该名字的错误码却是<a href="/tcp-ip-study-note#dns-rtnmsg">NOERROR</a>。通过nslookup/dig查询，错误码却是SERVFAIL，难道是CoreDNS日志有BUG？尽管如此，是否不同的错误码影响了getaddrinfo的行为</li>
</ol>
<div class="blog_h2"><span class="graybg">抓包分析</span></div>
<p>glibc的代码是优化过（<a href="https://stackoverflow.com/questions/30089652/glibc-optimizations-required">也必须优化</a>）的，GDB跟踪起来相当耗时，因此我们打算换一个角度来定位问题。基于上一节的分析，我们相信实例keydb-1.keydb在发送DNS请求的时候存在超时或丢包的情况，可以抓包来证实：</p>
<pre class="crayon-plain-tag"># 进入keydb-1.keydb的网络命名空间
nsenter -t 449 --net
# 抓包
tcpdump -i any -vv -nn udp port 53</pre>
<p>抓包的结果如下： </p>
<p style="padding-left: 30px;"><span style="background-color: #c0c0c0;">对 keydb-0.keydb.default.svc.cluster.local.  的A请求</span><br /><em> 172.29.1.69.42083 &gt; 10.96.0.10.53: [bad udp cksum 0xb829 -&gt; 0x95aa!] 22719+ A? keydb-0.keydb.default.svc.cluster.local. (68)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.42083: [bad udp cksum 0xb886 -&gt; 0x3f57!] 22719 NXDomain*- q: A? keydb-0.keydb.default.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (161)</em><br /><span style="background-color: #c0c0c0;">对keydb-0.keydb.default.svc.cluster.local.  的AAAA请求，注意，仍然使用之前的UDP套接字</span><br /> 172.29.1.69.42083 &gt; 10.96.0.10.53: [bad udp cksum 0xb829 -&gt; 0x4d76!] 41176+ AAAA? keydb-0.keydb.default.svc.cluster.local. (68)<br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.42083: [bad udp cksum 0xb886 -&gt; 0xf722!] 41176 NXDomain*- q: AAAA? keydb-0.keydb.default.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (161)</em><br /><span style="background-color: #c0c0c0;">对 keydb-0.keydb.svc.cluster.local. 的A请求，注意，这里使用了新的UDP套接字</span><br /> <em>172.29.1.69.45508 &gt; 10.96.0.10.53: [bad udp cksum 0xb824 -&gt; 0x3b5e!] 45156+ A? keydb-0.keydb.svc.cluster.local. (63)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.45508: [bad udp cksum 0xb881 -&gt; 0x21ce!] 45156 NXDomain*- q: A? keydb-0.keydb.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (156)</em><br /><span style="background-color: #c0c0c0;">对keydb-0.keydb.svc.cluster.local. 的AAAA请求</span><br /> 172.29.1.69.45508 &gt; 10.96.0.10.53: [bad udp cksum 0xb824 -&gt; 0x8a4e!] 18036+ AAAA? keydb-0.keydb.svc.cluster.local. (63)<br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.45508: [bad udp cksum 0xb881 -&gt; 0x70be!] 18036 NXDomain*- q: AAAA? keydb-0.keydb.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (156)</em><br /><span style="background-color: #c0c0c0;">对 keydb-0.keydb.cluster.local. 的A请求</span><br /><em>172.29.1.69.48243 &gt; 10.96.0.10.53: [bad udp cksum 0xb820 -&gt; 0x5054!] 2718+ A? keydb-0.keydb.cluster.local. (59)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.48243: [bad udp cksum 0xb87d -&gt; 0x36c4!] 2718 NXDomain*- q: A? keydb-0.keydb.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (152)</em><br /><span style="background-color: #c0c0c0;">对 keydb-0.keydb.cluster.local. 的AAAA请求</span><br /> <em>172.29.1.69.48243 &gt; 10.96.0.10.53: [bad udp cksum 0xb820 -&gt; 0x5147!] 61098+ AAAA? keydb-0.keydb.cluster.local. (59)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain，这里开始，我们保留时间戳那一行日志</span><br /><em>14:42:15.028168 IP (tos 0x0, ttl 63, id 44636, offset 0, flags [DF], proto UDP (17), length 180)</em><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.48243: [bad udp cksum 0xb87d -&gt; 0x37b7!] 61098 NXDomain*- q: AAAA? keydb-0.keydb.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (152)</em><br /><span style="background-color: #c0c0c0;">对keydb-0.keydb的A请求，这里还没有出现卡顿</span><br /><em>14:42:15.028328 IP (tos 0x0, ttl 64, id 30583, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 172.29.1.69.47652 &gt; 10.96.0.10.53: [bad udp cksum 0xb812 -&gt; 0x181e!] 26682+ A? keydb-0.keydb. (45)</em><br /><span style="background-color: #c0c0c0;">很快接收到CoreDNS的ServFail应答，抓包和我们nslookup/dig的错误码一致，CoreDNS日志显示的应该不正常</span><br /><span style="background-color: #c0c0c0;">猜测“有可能第二次是getaddrinfo没有收到应答而进行的重试”被排除，至少说没收到应答不是网络层面的原因</span><br /><em>14:42:15.028651 IP (tos 0x0, ttl 63, id 44637, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.47652: [bad udp cksum 0xb812 -&gt; 0x981b!] 26682 ServFail- q: A? keydb-0.keydb. 0/0/0 (45)</em><br /><span style="background-color: #c0c0c0;">再一次对keydb-0.keydb.的A请求，<strong>注意时间戳，刚好5秒之后</strong>，这是默认DNS请求超时。<strong>还是使用之前的套接字</strong></span><br /><em>14:42:20.029271 IP (tos 0x0, ttl 64, id 33006, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 172.29.1.69.47652 &gt; 10.96.0.10.53: [bad udp cksum 0xb812 -&gt; 0x181e!] 26682+ A? keydb-0.keydb. (45)</em><br /><span style="background-color: #c0c0c0;">很快接收到CoreDNS的ServFail应答</span><br /><em>14:42:20.029812 IP (tos 0x0, ttl 63, id 46397, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.47652: [bad udp cksum 0xb812 -&gt; 0x981b!] 26682 ServFail- q: A? keydb-0.keydb. 0/0/0 (45)</em></p>
<p>通过上述分析我们可以相信，keydb-1.keydb容器到CoreDNS之间的DNS通信是没有问题的。但是，getaddrinfo似乎没有收到keydb-0.keydb的第一次应答，并且在超时（5s）之后进行重试</p>
<div class="blog_h2"><span class="graybg">Conntrack竞态条件</span></div>
<p>tcpdump和应用程序之间，还有个netfilter框架。回想起之前阅读过的文章：<a href="/dns-problems-on-k8s">Kubernetes上和DNS相关的问题</a>，conntrack相关的竞态条件可能导致DNS查询5秒超时。遗憾的是，这里的故障和此竞态条件无关：</p>
<ol>
<li>通过<pre class="crayon-plain-tag">conntrack -S</pre>看到的<pre class="crayon-plain-tag">insert_failed</pre>是0</li>
<li>故障一旦出现，就每次都会超时5s，没有竞态条件的随机性</li>
<li>如果是conntrack竞态条件导致，无法解释为什么前面3个名字解析正常，也无法解释为什么CoreDNS中配置一个静态解析故障就消失</li>
</ol>
<div class="blog_h1"><span class="graybg">深入理解</span></div>
<div class="blog_h2"><span class="graybg">getaddrinfo</span></div>
<p>在IPv4中，我们使用<pre class="crayon-plain-tag">gethostbyname</pre>实现主机名到地址的解析。<pre class="crayon-plain-tag">getaddrinfo</pre>也用于地址解析，而且它是协议无关的，既可用于IPv4也可用于IPv6。它的原型如下：</p>
<pre class="crayon-plain-tag">int getaddrinfo(const char* hostname,  // 主机名，可以使用IP地址或者DNS名称
                const char* service,   // 服务名，可以使用端口号或者/etc/services中的服务名
                const struct addrinfo* hints, // 可以NULL，或者一个addrinfo，提示调用者想得到的信息类型
                struct addrinfo** res);  // 解析得到的addrinfo，地址的链表</pre>
<p>此函数返回的是套接字地址信息的链表，地址信息存储在下面的addrinfo结构中。参数<pre class="crayon-plain-tag">hints</pre>会影响getaddrinfo的行为，提示信息同样存放在addrinfo结构中：</p>
<pre class="crayon-plain-tag">struct addrinfo
{
  // 额外的提示标记
  int ai_flags;	
  // 提示需要查询哪些地址族，默认AF_UNSPEC，这意味着同时查询IPv4和IPv6地址
  // 也就是同时发起A/AAAA查询
  int ai_family;
  // 提示偏好的套接字类型，例如SOCK_STREAM|SOCK_DGRAM，默认可以返回任何套接字类型
  int ai_socktype;
  // 提示返回的套接字地址的协议类型
  int ai_protocol;

  // 套接字地址
  socklen_t ai_addrlen;
  struct sockaddr *ai_addr;
  // ...
  // 指向链表的下一条目
  struct addrinfo *ai_next;
};</pre>
<p>很多软件调用getaddrinfo的时候，都会指定AF_UNSPEC（或者不提供hints，效果一样），例如KeyDB。但是，很多运行环境根本没有IPv6支持，这就凭白的给DNS服务器增加了负担。这也是在K8S中查看CoreDNS日志，总是会发现很多AAAA记录的原因。</p>
<div class="blog_h3"><span class="graybg">解析流程概览</span></div>
<p>KeyDB 5.3.3使用的glibc版本是2.27。函数getaddrinfo过于冗长，这里就不贴出来了，大概梳理一下：</p>
<ol>
<li>如果可能，它会通过/var/run/nscd/socket访问DNS缓存服务，我们没有这个服务</li>
<li>初始化NSS的hosts数据库，如果没有在文件中配置，则默认使用<pre class="crayon-plain-tag">hosts: dns [!UNAVAIL=return] files</pre>，我们的环境下配置是<pre class="crayon-plain-tag">hosts: files dns</pre></li>
<li>通过<a href="/linux-faq#nss">NSS</a>进行名字查询，实际上是调用<pre class="crayon-plain-tag">gethostbyname4_r</pre>函数：
<ol>
<li>查找files源，调用<pre class="crayon-plain-tag">_nss_files_gethostbyname4_r</pre>函数，也就是打开/etc/hosts查找。K8S容器中，/etc/hosts中仅仅存在当前Pod的条目，因此files源不会匹配</li>
<li>查找dns源，调用<pre class="crayon-plain-tag">_nss_dns_gethostbyname4_r</pre>函数：
<ol>
<li>读取/etc/resolv.conf构建<pre class="crayon-plain-tag">resolv_context</pre>。我们的环境下，配置文件内容为：<br />
<pre class="crayon-plain-tag">nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5</pre>
</li>
<li>
<p>调用<pre class="crayon-plain-tag">__res_context_search</pre>， 执行DNS查找逻辑。它会<span style="background-color: #c0c0c0;">将上面的search domain作为域名后缀，产生多个名字，逐个尝试。每个名字查询失败时都会重试，重试时尽可能选择不同的DNS服务器。可能同时发起A/AAA查询</span></p>
</li>
</ol>
</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">DNS搜索逻辑</span></div>
<p>__res_context_search()首先会计算一下，待查找名字中的dot的数量。如果<span style="background-color: #c0c0c0;">名字以dot结尾，或者dot数量大于等于ndots</span>，则直接调用<pre class="crayon-plain-tag">__res_context_querydomain</pre>向DNS服务器发请求，该函数会同时发起A/AAAA查询。</p>
<p>否则，它会根据/etc/resolv.conf中的search domain列表，给待查找的名字加后缀，然后多次向DNS服务器发请求。我们的环境下，待查找名字为keydb-0.keydb，getaddrinfo函数会依次尝试：</p>
<p style="padding-left: 30px;">keydb-0.keydb.default.svc.cluster.local. <br />keydb-0.keydb.svc.cluster.local. <br />keydb-0.keydb.cluster.local.<br />keydb-0.keydb.</p>
<p>需要注意：</p>
<ol>
<li>向DNS服务器发请求，仍然是由__res_context_querydomain()负责</li>
<li>一旦查找成功，就立即返回不再尝试其它search domain</li>
<li><span style="background-color: #c0c0c0;">不加修饰的原始名字，会放在最后尝试</span></li>
</ol>
<p>在K8S中，*.cluster.local一般都由CoreDNS自身负责，处理速度会很快。至于keydb-0.keydb.的处理速度，如果为CoreDNS配置了上游DNS，则处理速度依赖于外部环境。</p>
<p>__res_context_querydomain仅仅是在domain参数不为空的时候，将name和domain连接起来，然后调用<pre class="crayon-plain-tag">__res_context_query</pre>函数。</p>
<div class="blog_h3"><span class="graybg">DNS查询过程</span></div>
<p>__res_context_query负责和DNS服务器的交互，完成<span style="background-color: #c0c0c0;">单个名字的DNS查</span>询。它会调用<pre class="crayon-plain-tag">__res_context_mkquery</pre><span style="background-color: #c0c0c0;">构建一个查询请求（对应DNS报文），然后发送</span>，然后等待应答。这是一个阻塞的过程，KeyDB在期望非阻塞的代码路径下调用getaddrinfo且没有任何缓存机制，同时还加了锁，我觉得是不妥的。这导致DNS缓慢/不可用会极大的影响KeyDB的服务质量。</p>
<p>发送DNS请求的代码在<pre class="crayon-plain-tag">__res_context_send</pre>中，<span style="background-color: #c0c0c0;">重试逻辑</span>发生在该函数中，我们的环境下重试次数为2，这解释了两次keydb-0.keydb. A查询：</p>
<pre class="crayon-plain-tag">//                  重试次数，statp-&gt;retry为2
for (try = 0; try &lt; statp-&gt;retry; try++) {
    // 如果有多个DNS服务器，重试时会轮询它们
    for (unsigned ns_shift = 0; ns_shift &lt; statp-&gt;nscount; ns_shift++)
    {
    unsigned int ns = ns_shift + ns_offset;
    if (ns &gt;= statp-&gt;nscount)
        ns -= statp-&gt;nscount;

    same_ns:
    if (__glibc_unlikely (v_circuit)) {
        // ...
    } else {
        // 使用UDP方式发送请求
        n = send_dg(statp, buf, buflen, buf2, buflen2,
                &amp;ans, &amp;anssiz, &amp;terrno,
                ns, &amp;v_circuit, &amp;gotsomewhere, ansp,
                ansp2, nansp2, resplen2, ansp2_malloced);
        if (n &lt; 0)
            return (-1);
        if (n == 0 &amp;&amp; (buf2 == NULL || *resplen2 == 0))
            // 如果有多个DNS服务器的时候，会尝试下一个
            goto next_ns;
        // ...
    }
    return (resplen);
next_ns: ;
   } /*foreach ns*/
} /*foreach retry*/</pre>
<p>通常情况下，都是通过UDP协议进行DNS查询的，因此会调用<pre class="crayon-plain-tag">send_dg</pre>函数。在我们的场景中，两次尝试均5秒超时（尽管抓包显示应答报文很快就收到），__res_context_send设置错误码ETIMEDOUT，返回-1：</p>
<pre class="crayon-plain-tag">__res_iclose(statp, false);
if (!v_circuit) {
    if (!gotsomewhere)
        __set_errno (ECONNREFUSED);	/* no nameservers found */
    else
        __set_errno (ETIMEDOUT);	/* no answer obtained */
} else
    __set_errno (terrno);
return (-1);</pre>
<p>而它的调用者__res_context_query则在返回值是-1的时候，设置错误码TRY_AGAIN，这就是我们从KeyDB日志上看到报错The name server returned a temporary failure indication的原因：</p>
<pre class="crayon-plain-tag">if (n &lt; 0) {
    RES_SET_H_ERRNO(statp, TRY_AGAIN);
    return (n);
}</pre>
<div class="blog_h3"><span class="graybg">缓慢之源</span></div>
<p>缓慢的根源是send_dg函数，它阻塞了5秒。该函数的原型如下：</p>
<pre class="crayon-plain-tag">// 如果没有错误，返回第一个应答的字节数
// 对于可恢复错误，返回0；对于不可恢复错误，返回负数
static int send_dg(
    // 各种选项、DNS服务器列表、指向DNS服务器的套接字（文件描述符）
    res_state statp,
    // 查询请求1的缓冲区 和 长度
	const u_char *buf, int buflen, 
    // 查询请求2的缓冲区 和 长度
    const u_char *buf2, int buflen2,
    // 收到的第1个应答   和 最大长度
	u_char **ansp,      int *anssizp,
    // 出现错误时，将errno设置到此字段
	int *terrno, 
    // 使用的DNS服务器的序号
    int ns, 
    // 如果由于UDP数据报的限制而导致截断，则v_circuit设置为1，提示调用者使用TCP方式重试
    int *v_circuit, 
    // 提示访问DNS服务器时，是拒绝服务还是超时。如果是超时则设置为1
    int *gotsomewhere,
    // 提示遇到超长应答的时候，是否重新分配缓冲区
    u_char **anscp,
    // 收到的第2个应答 和 最大长度
	u_char **ansp2, int *anssizp2, 
    // 第2个应答的实际长度 是否为第2个应答重新分配了缓冲区
    int *resplen2, int *ansp2_malloced);</pre>
<p>该函数会向指定序号的DNS服务器发送DNS查询。它同时支持IPv4/IPv6查询，你可以传递两个查询请求，分别放在buf和buf2参数中。<span style="background-color: #c0c0c0;">如果提供了两个查询请求，默认使用并行方式发送查询</span>。设置选项<pre class="crayon-plain-tag">RES_SINGLKUP</pre>可以强制串行发送；设置选项<pre class="crayon-plain-tag">RES_SNGLKUPREOP</pre>可以<span style="background-color: #c0c0c0;">强制串行发送，同时总是关闭并重新打开套接字</span>，这样可以和某些行为异常的DNS服务器一起工作。</p>
<p>由于请求可以并行发送，因此应答到达的顺序是不确定的。<span style="background-color: #c0c0c0;">先收到的</span>应答会存放在ansp中，入参最大长度anssizp。入参anscp用于提示，应答过长的时候的处理方式：</p>
<ol>
<li>如果anscp不为空：则自动分配新的缓冲区，并且ansp、anscp都被修改为指向该缓冲区</li>
<li>如果anscp为空：则过长的部分被截断，DNS包头的TC字段被设置为1</li>
</ol>
<p>glibc的2.27-3ubuntu1版本中send_dg的完整实现如下：</p>
<pre class="crayon-plain-tag">static int
send_dg(res_state statp,
	const u_char *buf, int buflen, const u_char *buf2, int buflen2,
	u_char **ansp, int *anssizp,
	int *terrno, int ns, int *v_circuit, int *gotsomewhere, u_char **anscp,
	u_char **ansp2, int *anssizp2, int *resplen2, int *ansp2_malloced)
{
	const HEADER *hp = (HEADER *) buf;
	const HEADER *hp2 = (HEADER *) buf2;
	struct timespec now, timeout, finish;
	struct pollfd pfd[1];
	int ptimeout;
	struct sockaddr_in6 from;
	int resplen = 0;
	int n;

	/*
	 * Compute time for the total operation.
	 */
	int seconds = (statp-&gt;retrans &lt;&lt; ns); // 0. 计算超时
	if (ns &gt; 0)
		seconds /= statp-&gt;nscount;
	if (seconds &lt;= 0)
		seconds = 1;
	bool single_request_reopen = (statp-&gt;options &amp; RES_SNGLKUPREOP) != 0; // 0. 确定是否并行请求
	bool single_request = (((statp-&gt;options &amp; RES_SNGLKUP) != 0)
			       | single_request_reopen);
	int save_gotsomewhere = *gotsomewhere;

	int retval;
 retry_reopen: // tx1. 如果套接字没有创建，则创建， SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC，非阻塞
	retval = reopen (statp, terrno, ns); // tx1. 然后调用一下connect操作，不发数据
	if (retval &lt;= 0)
	  {
	    if (resplen2 != NULL)
	      *resplen2 = 0;
	    return retval;
	  }
 retry:
	evNowTime(&amp;now);
	evConsTime(&amp;timeout, seconds, 0);
	evAddTime(&amp;finish, &amp;now, &amp;timeout);
	int need_recompute = 0;
	int nwritten = 0;
	int recvresp1 = 0;  // 用于标记请求1的应答是否接收到
	/* Skip the second response if there is no second query.
	   To do that we mark the second response as received.  */
	int recvresp2 = buf2 == NULL; // 用于标记请求2的应答是否接收到，如果buf2为空则立即标记为1
	pfd[0].fd = EXT(statp).nssocks[ns];
	pfd[0].events = POLLOUT; // tx2. 准备监听可写事件
 wait:
	if (need_recompute) {
	recompute_resend:
		evNowTime(&amp;now);
		if (evCmpTime(finish, now) &lt;= 0) {
		poll_err_out:
			return close_and_return_error (statp, resplen2);
		}
		evSubTime(&amp;timeout, &amp;finish, &amp;now);
		need_recompute = 0;
	}
	/* Convert struct timespec in milliseconds.  */
	ptimeout = timeout.tv_sec * 1000 + timeout.tv_nsec / 1000000;

	n = 0;
	if (nwritten == 0)
	  n = __poll (pfd, 1, 0); // tx2. 等待套接字可写
	if (__glibc_unlikely (n == 0))       {
		n = __poll (pfd, 1, ptimeout); // rx1. 等待套接字可读，5秒超时
		need_recompute = 1;
	}
	if (n == 0) {
		if (resplen &gt; 1 &amp;&amp; (recvresp1 || (buf2 != NULL &amp;&amp; recvresp2)))
		  { // 处理某些DNS服务器不支持处理并行请求的场景
		    /* There are quite a few broken name servers out
		       there which don't handle two outstanding
		       requests from the same source.  There are also
		       broken firewall settings.  If we time out after
		       having received one answer switch to the mode
		       where we send the second request only once we
		       have received the first answer.  */
		    if (!single_request)
		      {
			statp-&gt;options |= RES_SNGLKUP; // 这里永久改变为串行发送请求。statp是线程本地变量，
			single_request = true;         // KeyDB复制定时任务总是在同一线程中运行
			*gotsomewhere = save_gotsomewhere;
			goto retry;
		      }
		    else if (!single_request_reopen)
		      {
			statp-&gt;options |= RES_SNGLKUPREOP;
			single_request_reopen = true;
			*gotsomewhere = save_gotsomewhere;
			__res_iclose (statp, false);
			goto retry_reopen;
		      }

		    *resplen2 = 1;
		    return resplen;
		  }

		*gotsomewhere = 1;
		if (resplen2 != NULL)
		  *resplen2 = 0;
		return 0;
	}
	if (n &lt; 0) {
		if (errno == EINTR)
			goto recompute_resend;

		goto poll_err_out;
	}
	__set_errno (0);
	if (pfd[0].revents &amp; POLLOUT) { // tx3. 监听到可写事件
#ifndef __ASSUME_SENDMMSG
		static int have_sendmmsg;
#else
# define have_sendmmsg 1
#endif
		if (have_sendmmsg &gt;= 0 &amp;&amp; nwritten == 0 &amp;&amp; buf2 != NULL // 查询请求2不为空
		    &amp;&amp; !single_request) // 且允许并行发送
		  {
		    struct iovec iov[2];
		    struct mmsghdr reqs[2];
		    reqs[0].msg_hdr.msg_name = NULL;
		    reqs[0].msg_hdr.msg_namelen = 0;
		    reqs[0].msg_hdr.msg_iov = &amp;iov[0];
		    reqs[0].msg_hdr.msg_iovlen = 1;
		    iov[0].iov_base = (void *) buf;
		    iov[0].iov_len = buflen;
		    reqs[0].msg_hdr.msg_control = NULL;
		    reqs[0].msg_hdr.msg_controllen = 0;

		    reqs[1].msg_hdr.msg_name = NULL;
		    reqs[1].msg_hdr.msg_namelen = 0;
		    reqs[1].msg_hdr.msg_iov = &amp;iov[1];
		    reqs[1].msg_hdr.msg_iovlen = 1;
		    iov[1].iov_base = (void *) buf2;
		    iov[1].iov_len = buflen2;
		    reqs[1].msg_hdr.msg_control = NULL;
		    reqs[1].msg_hdr.msg_controllen = 0;
            // 发送消息，注意这里同时发送2个查询请求，返回值是实际发送的数量
		    int ndg = __sendmmsg (pfd[0].fd, reqs, 2, MSG_NOSIGNAL);
		    if (__glibc_likely (ndg == 2))
		      {
			if (reqs[0].msg_len != buflen
			    || reqs[1].msg_len != buflen2)
			  goto fail_sendmmsg;

			pfd[0].events = POLLIN;
			nwritten += 2;
		      }
		    else if (ndg == 1 &amp;&amp; reqs[0].msg_len == buflen)
		      goto just_one;
		    else if (ndg &lt; 0 &amp;&amp; (errno == EINTR || errno == EAGAIN))
		      goto recompute_resend;
		    else
		      {
#ifndef __ASSUME_SENDMMSG
			if (__glibc_unlikely (have_sendmmsg == 0))
			  {
			    if (ndg &lt; 0 &amp;&amp; errno == ENOSYS)
			      {
				have_sendmmsg = -1;
				goto try_send;
			      }
			    have_sendmmsg = 1;
			  }
#endif

		      fail_sendmmsg:
			return close_and_return_error (statp, resplen2);
		      }
		  }
		else
		  { // 不支持并行发送
		    ssize_t sr;
#ifndef __ASSUME_SENDMMSG
		  try_send:
#endif
		    if (nwritten != 0)
		      sr = send (pfd[0].fd, buf2, buflen2, MSG_NOSIGNAL);
		    else
		      sr = send (pfd[0].fd, buf, buflen, MSG_NOSIGNAL); // tx4. 发送查询请求1

		    if (sr != (nwritten != 0 ? buflen2 : buflen)) { // 发送长度和缓冲区长度不匹配
		      if (errno == EINTR || errno == EAGAIN) // 如果原因是EINTR或EAGAIN，则尝试重发
			goto recompute_resend;
		      return close_and_return_error (statp, resplen2);
		    }
		  just_one:
		    if (nwritten != 0 || buf2 == NULL || single_request)
		      pfd[0].events = POLLIN;  // 串行模式下，后续只需监听可读时间
		    else
		      pfd[0].events = POLLIN | POLLOUT; // 并行发送，如果实际仅发送1个消息，跳转到这里。后续需要继续写入发送失败的那个消息
		    ++nwritten;
		  }
		goto wait; // tx4. 发送完毕，回到上面的wait分支等待应答
	} else if (pfd[0].revents &amp; POLLIN) { // rx2. 监听到套接字可读
		int *thisanssizp; // 本次读数据到哪个缓冲
		u_char **thisansp;
		int *thisresplenp;

		if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) {
			/* We have not received any responses
			   yet or we only have one response to
			   receive.  */
			thisanssizp = anssizp;
			thisansp = anscp ?: ansp;
			assert (anscp != NULL || ansp2 == NULL);
			thisresplenp = &amp;resplen;
		} else {
			thisanssizp = anssizp2;
			thisansp = ansp2;
			thisresplenp = resplen2;
		}

		if (*thisanssizp &lt; MAXPACKET
		    /* If the current buffer is not the the static
		       user-supplied buffer then we can reallocate
		       it.  */
		    &amp;&amp; (thisansp != NULL &amp;&amp; thisansp != ansp)
#ifdef FIONREAD
		    /* Is the size too small?  */
		    &amp;&amp; (ioctl (pfd[0].fd, FIONREAD, thisresplenp) &lt; 0
			|| *thisanssizp &lt; *thisresplenp)
#endif
                    ) {
			/* Always allocate MAXPACKET, callers expect
			   this specific size.  */
			u_char *newp = malloc (MAXPACKET);
			if (newp != NULL) {
				*thisanssizp = MAXPACKET;
				*thisansp = newp;
				if (thisansp == ansp2)
				  *ansp2_malloced = 1;
			}
		}
		/* We could end up with truncation if anscp was NULL
		   (not allowed to change caller's buffer) and the
		   response buffer size is too small.  This isn't a
		   reliable way to detect truncation because the ioctl
		   may be an inaccurate report of the UDP message size.
		   Therefore we use this only to issue debug output.
		   To do truncation accurately with UDP we need
		   MSG_TRUNC which is only available on Linux.  We
		   can abstract out the Linux-specific feature in the
		   future to detect truncation.  */
		HEADER *anhp = (HEADER *) *thisansp;
		socklen_t fromlen = sizeof(struct sockaddr_in6);
		assert (sizeof(from) &lt;= fromlen);
		*thisresplenp = recvfrom(pfd[0].fd, (char*)*thisansp, // rx3. 读取应答
					 *thisanssizp, 0,
					(struct sockaddr *)&amp;from, &amp;fromlen);
		if (__glibc_unlikely (*thisresplenp &lt;= 0))       {
			if (errno == EINTR || errno == EAGAIN) {
				need_recompute = 1;
				goto wait;  // 如果EINTR|EAGAIN则重新等待
			}
			return close_and_return_error (statp, resplen2);
		}
		*gotsomewhere = 1;
		if (__glibc_unlikely (*thisresplenp &lt; HFIXEDSZ))       { // 消息比报文头长度还小，错误
			/*
			 * Undersized message.
			 */
			*terrno = EMSGSIZE;
			return close_and_return_error (statp, resplen2);
		}
		if ((recvresp1 || hp-&gt;id != anhp-&gt;id)
		    &amp;&amp; (recvresp2 || hp2-&gt;id != anhp-&gt;id)) { // 查询标识符不匹配，可能服务器缓慢，返回之前查询的应答
			/*
			 * response from old query, ignore it.
			 * XXX - potential security hazard could
			 *	 be detected here.
			 */
			goto wait;
		}
		if (!(statp-&gt;options &amp; RES_INSECURE1) &amp;&amp; // 安全性检查type1
		    !res_ourserver_p(statp, &amp;from)) {
			/*
			 * response from wrong server? ignore it.
			 * XXX - potential security hazard could
			 *	 be detected here.
			 */
			goto wait;
		}
		if (!(statp-&gt;options &amp; RES_INSECURE2) // 安全性检查type2
		    &amp;&amp; (recvresp1 || !res_queriesmatch(buf, buf + buflen,
						       *thisansp,
						       *thisansp
						       + *thisanssizp))
		    &amp;&amp; (recvresp2 || !res_queriesmatch(buf2, buf2 + buflen2,
						       *thisansp,
						       *thisansp
						       + *thisanssizp))) {
			/*
			 * response contains wrong query? ignore it.
			 * XXX - potential security hazard could
			 *	 be detected here.
			 */
			goto wait;
		}
		if (anhp-&gt;rcode == SERVFAIL ||
		    anhp-&gt;rcode == NOTIMP ||
		    anhp-&gt;rcode == REFUSED) {  //  rx4. 处理服务器不愿意处理请求的情况
		next_ns:
			if (recvresp1 || (buf2 != NULL &amp;&amp; recvresp2)) {
			  *resplen2 = 0;
			  return resplen;
			}
			if (buf2 != NULL)
			  {
			    /* No data from the first reply.  */
			    resplen = 0;
			    /* We are waiting for a possible second reply.  */
			    if (hp-&gt;id == anhp-&gt;id)
			      recvresp1 = 1;
			    else
			      recvresp2 = 1;

			    goto wait;  // 事件类型仍然是POLLIN，会导致超时
			  }

			/* don't retry if called from dig */
			if (!statp-&gt;pfcode)
			  return close_and_return_error (statp, resplen2);
			__res_iclose(statp, false);
		}
		if (anhp-&gt;rcode == NOERROR &amp;&amp; anhp-&gt;ancount == 0 // rx.4 处理nodata的情况，名字请求，请求的记录类型不存在
		    &amp;&amp; anhp-&gt;aa == 0 &amp;&amp; anhp-&gt;ra == 0 &amp;&amp; anhp-&gt;arcount == 0) {
			goto next_ns;
		}
		if (!(statp-&gt;options &amp; RES_IGNTC) &amp;&amp; anhp-&gt;tc) { // rx.4 处理应答截断的情况
			/*
			 * To get the rest of answer,
			 * use TCP with same server.
			 */
			*v_circuit = 1; // 提示使用TCP重发请求
			__res_iclose(statp, false);
			// XXX if we have received one reply we could
			// XXX use it and not repeat it over TCP...
			if (resplen2 != NULL)
			  *resplen2 = 0;
			return (1);
		}
		/* Mark which reply we received.  */
		if (recvresp1 == 0 &amp;&amp; hp-&gt;id == anhp-&gt;id)
			recvresp1 = 1;
		else
			recvresp2 = 1;
		/* Repeat waiting if we have a second answer to arrive.  */
		if ((recvresp1 &amp; recvresp2) == 0) { // 如果只有一个查询请求，recvresp2一开始就标记为1，因此不会走到这个分支
			if (single_request) { // 如果是串行模式，这里开始处理第2个请求
				pfd[0].events = POLLOUT;
				if (single_request_reopen) {  // 如果需要关闭并重新打开套接字
					__res_iclose (statp, false);
					retval = reopen (statp, terrno, ns);
					if (retval &lt;= 0)
					  {
					    if (resplen2 != NULL)
					      *resplen2 = 0;
					    return retval;
					  }
					pfd[0].fd = EXT(statp).nssocks[ns];
				}
			}
			goto wait;  // 事件类型已经改为POLLOUT，因此不会发生超时
		}
		/* All is well.  We have received both responses (if
		   two responses were requested).  */
		return (resplen); // rx.5 DNS查询完毕
	} else if (pfd[0].revents &amp; (POLLERR | POLLHUP | POLLNVAL)) // poll出现错误
	  /* Something went wrong.  We can stop trying.  */
	  return close_and_return_error (statp, resplen2);
	else {
		/* poll should not have returned &gt; 0 in this case.  */
		abort ();
	}
}</pre>
<p>注释中tx.标注了DNS查询请求发送的基本过程，rx.则标注了DNS查询应答接收的基本过程。调试查询keydb-0.keydb时该函数的行为，发现以下事实：</p>
<ol>
<li>查询时串行发送的，而不是并行。因此正常流程应该是发送A查询，接收A应答，发送AAAA查询，接收AAAA应答</li>
<li>仅执行了1225行，没有执行1223行。也就是说仅仅发送了A查询，没有发送AAA查询</li>
<li>走到了1241行的分支，也就是说，<span style="background-color: #c0c0c0;">A请求的应答报文是接收到的</span>：<br />
<pre class="crayon-plain-tag">// (gdb) i r eax
// eax            0x2d     45   A应答长度45
		*thisresplenp = recvfrom(pfd[0].fd, (char*)*thisansp,
					 *thisanssizp, 0,
					(struct sockaddr *)&amp;from, &amp;fromlen);</pre></p>
<p>由于接收到的应答是servfail，因此走到这个分支：</p>
<pre class="crayon-plain-tag">if (anhp-&gt;rcode == SERVFAIL ||
		    anhp-&gt;rcode == NOTIMP ||
		    anhp-&gt;rcode == REFUSED) {
		next_ns:
			if (recvresp1 || (buf2 != NULL &amp;&amp; recvresp2)) {
			  *resplen2 = 0;
			  return resplen;
			}
			if (buf2 != NULL)
			  {
			    /* No data from the first reply.  */
			    resplen = 0;
			    /* We are waiting for a possible second reply.  */
			    if (hp-&gt;id == anhp-&gt;id)
			      recvresp1 = 1;  // 接收到第一个应答
			    else
			      recvresp2 = 1;
                // 由于同时需要进行A和AAAA查询，这里仅仅接收到A应答（串行发送）
			    goto wait; // 因此需要跳转到这里，等待套接字可写，以发送AAAA请求
			  }</pre>
</li>
<li>
<p>CoreDNS应答A查询SERVFAIL，重新跳转到wait标签：
<pre class="crayon-plain-tag">if (need_recompute) { // 等待A应答的时候，设置了超时 need_recompute，因此再次wait执行这个分支
	recompute_resend:
		evNowTime(&amp;now);
		if (evCmpTime(finish, now) &lt;= 0) {
		poll_err_out: // 如果超时了，直接关闭套接字并返回错误
			return close_and_return_error (statp, resplen2);
		}
		evSubTime(&amp;timeout, &amp;finish, &amp;now);
		need_recompute = 0;
	}
	/* Convert struct timespec in milliseconds.  */
	ptimeout = timeout.tv_sec * 1000 + timeout.tv_nsec / 1000000;

	n = 0;
	if (nwritten == 0)
	  n = __poll (pfd, 1, 0);  // 发送A请求的时候在这里pull，等待套接字可写。timeout 0表示立即返回
	if (__glibc_unlikely (n == 0))       {
		n = __poll (pfd, 1, ptimeout);  // 接收A应答的时候在这里poll，等待套接字可读
		need_recompute = 1; // 发送AAAA请求时，在这里等待套接字可写
	}</pre>
</li>
<li>
<p> 这时，由于nwritten已经被设置为1，因此走带有timeout的poll分支。然后在1110行出现5秒超时，并因为poll返回值是0而导致send_dg函数退出。在一次A请求处理过程中，有两次在1110行poll：
<ol>
<li>第一次是尝试A请求的应答，poll前的pollfd是{fd = 87, events = 1, revents = 4}，之后是{fd = 87, events = 1, revents = 1}</li>
<li>第二次就是因为这个跳转，poll前的pollfd是{fd = 87, events = 1, revents = 1}，超时之后是{fd = 87, events = 1, revents = 0}</li>
</ol>
</li>
</ol>
<p>poll函数原型：<pre class="crayon-plain-tag">int poll(struct pollfd *fds, nfds_t nfds, int timeout);</pre>，它等待文件描述符集合中的某个可用（可执行I/O）。文件描述符集合由参数fds指定，它是pollfd结构的数组：</p>
<pre class="crayon-plain-tag">struct pollfd {
    // 打开文件的描述符
    int   fd;         
    // 输入参数，应用程序感兴趣的事件类型。如果置零则revents中仅能返回POLLHUP,POLLERR,POLLNVAL事件
    short events;
    // 输出参数，内核填充实际发生的事件
    short revents;    
};</pre>
<p>如果文件描述符集中没有任何一个发生了events中指定的事件，则该函数会阻塞，直到超时或者被信号处理器中断。</p>
<p><span style="background-color: #c0c0c0;">事件类型1表示POLLIN，即有数据可读；事件类型4表示POLLOUT</span>，即文件描述符可写。正常情况下该函数返回就绪的（revents非零）文件描述符数量，超时返回0，出现错误则返回-1</p>
<p>第2次在1110行的poll行为难以理解：</p>
<ol>
<li>A的应答已经接收到，而由于进行的是串行发送A/AAAA，此时尚未发送AAAA请求，因此可以<span style="background-color: #c0c0c0;">预期后续不会有可读事件</span></li>
<li>poll时events设置为POLLIN（肯定会导致超时），难道不是应该设置为POLLOUT，尝试发送AAAA请求或重试A请求么？</li>
</ol>
<p>为了进行对照，我们由调试了没有发生缓慢问题的keydb-2.keydb。它在第2次执行1110行的poll时没有超时，pollfd的状态是{fd = 88, events = 1, revents = 1}。连续两次poll到可读事件，这提示进行了并行A/AAAA查询。检查变量single_request_reopen、single_request果然都是false，从CoreDNS日志上也可以看到A/AAAA</p>
<p>可能的情况是，keydb-1.keydb最初是并行发送A/AAAA查询的，后来由于某种原因，改为串行发送，从而导致出现5秒超时相关的缓慢现象。根源应该还是在glibc中，因为KeyDB调用getaddrinfo的方式是固定的。</p>
<p>回顾一下send_dg的代码，可以发现<pre class="crayon-plain-tag">statp-&gt;options</pre>决定了是否进行串行发送，statp是<pre class="crayon-plain-tag">resolv_context</pre>的一个字段，后者则是一个线程本地变量。如果某次并行发送请求后，可以接收到第一个应答，而在继续等待第二个应答时出现超时（1113行），则send_dg函数会修改statp-&gt;options，改为串行发送，<span style="background-color: #c0c0c0;">这个修改具有全局性影响</span>，以后KeyDB的复制定时任务（总是由同一线程执行）调用getaddrinfo，都会使用串行方式发送请求。</p>
<p>改变为串行方式后，由于CoreDNS应答keydb-0.keydb.以SERVFAIL，导致跳转到wait标签（1363行），进而执行了一次必然超时的poll调用。CoreDNS应答其它（加了search domain后缀的）域名以NXDOMAIN，则不会导致超时的poll调用，<span style="background-color: #c0c0c0;">因为会在1396行修改事件类型为POLLOUT</span>。</p>
<div class="blog_h1"><span class="graybg">解决方案</span></div>
<p>触发本文中的glibc缺陷，需要满足以下条件：</p>
<ol>
<li>出现某个KeyDB节点宕机的情况，并且没有修复。这会导致复制定时任务反复执行DNS查询，从而可能触发缺陷</li>
<li>某个DNS查询的应答UDP包丢失，导致当前线程串行发送DNS请求。由于UDP本身的不可靠性，随着程序不断运行，最终会发生</li>
<li>DNS服务器返回SERVFAIL、NOTIMP或者REFUSED应答</li>
</ol>
<p>第1、2个条件都是随机性的，我们没法干预，只有从第3个条件入手。作为最快速的解决方案，只需要配置KeyDB，使用全限定域名来指定replicaof即可。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/debugging-slow-keydb">记录一次KeyDB缓慢的定位过程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/debugging-slow-keydb/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>扩展Envoy</title>
		<link>https://blog.gmem.cc/extend-envoy</link>
		<comments>https://blog.gmem.cc/extend-envoy#comments</comments>
		<pubDate>Thu, 25 Apr 2019 03:53:37 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=26573</guid>
		<description><![CDATA[<p>前言 Enovy进程中运行着一系列的Inbound/Outbound监听器，分别代理外部发往工作负载、工作负载发往外部的网络流量。监听器的核心是过滤器链 ，链中每个过滤器都能够控制流量的处理流程。 扩展Envoy的主要方式就是开发新的过滤器。过滤器分为两个类别： 网络过滤器（L3/L4），是Envoy网络连接处理的核心 HTTP过滤器（L7），由特殊的网络过滤器HttpConnectionManager管理，专门处理HTTP1/HTTP2/gRPC请求 网络过滤器 根据行为的不同，网络过滤器分为： 读过滤器，当Envoy从下游连接接收到流量时调用 写过滤器，当Envoy准备向下游连接发送流量时调用 读/写过滤器，在上述两种情况下均调用  由于网络过滤器操控套接字的原始字节（外加少量事件，例如TLS握手完毕、连接断开），因此它的接口比较简单。 每个过滤器都可以中止迭代流程，并在未来继续后续过滤器的迭代。这种中止/继续迭代的机制，让实现复杂的需求成为可能，例如调用限速服务，异步的根据调用结果决定是否继续迭代。 网络过滤器之间可以在同一个下游连接的上下文内共享一些静态或动态数据。 接口 L4过滤器的接口非常简单，总共只有4个方法。 ReadFilter [crayon-69db8c96ae5da957974431/] WriteFilter [crayon-69db8c96ae5df474446132/] Filter [crayon-69db8c96ae5e1404135360/] 示例 <a class="read-more" href="https://blog.gmem.cc/extend-envoy">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/extend-envoy">扩展Envoy</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">前言</span></div>
<p>Enovy进程中运行着一系列的Inbound/Outbound监听器，分别代理外部发往工作负载、工作负载发往外部的网络流量。<span style="background-color: #c0c0c0;">监听器的核心是过滤器链</span> ，链中每个过滤器都能够控制流量的处理流程。</p>
<p>扩展Envoy的主要方式就是开发新的过滤器。过滤器分为两个类别：</p>
<ol>
<li>网络过滤器（L3/L4），是Envoy网络连接处理的核心</li>
<li>HTTP过滤器（L7），由特殊的网络过滤器HttpConnectionManager管理，专门处理HTTP1/HTTP2/gRPC请求</li>
</ol>
<div class="blog_h1"><span class="graybg">网络过滤器</span></div>
<p>根据行为的不同，网络过滤器分为：</p>
<ol>
<li>读过滤器，当Envoy从下游连接接收到流量时调用</li>
<li>写过滤器，当Envoy准备向下游连接发送流量时调用</li>
<li>读/写过滤器，在上述两种情况下均调用 </li>
</ol>
<p>由于网络过滤器操控套接字的原始字节（外加少量事件，例如TLS握手完毕、连接断开），因此它的接口比较简单。</p>
<p>每个过滤器都<span style="background-color: #c0c0c0;">可以中止迭代流程</span>，并在未来<span style="background-color: #c0c0c0;">继续后续过滤器</span>的迭代。这种中止/继续迭代的机制，让实现复杂的需求成为可能，例如调用限速服务，异步的根据调用结果决定是否继续迭代。</p>
<p>网络过滤器之间可以<span style="background-color: #c0c0c0;">在同一个下游连接的上下文内<a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/data_sharing_between_filters#arch-overview-data-sharing-between-filters">共享</a>一些静态或动态数据</span>。</p>
<div class="blog_h2"><span class="graybg">接口</span></div>
<p>L4过滤器的接口非常简单，总共只有4个方法。</p>
<div class="blog_h3"><span class="graybg">ReadFilter</span></div>
<pre class="crayon-plain-tag">class ReadFilter {
public:
 
  /**
   * 当连接上的数据被读取时调用
   * @param data 读取到的，可能已经被修改过的数据
   * @param end_stream 当连接启用半关闭语义时，用于提示是否到了最后一字节
   * @return status 过滤器管理器使用此状态决定如何进一步迭代其它过滤器
   */
  virtual FilterStatus onData(Buffer::Instance&amp; data, bool end_stream) PURE;
 
  /**
   * 当新连接刚创建时调用，过滤器链的迭代可以被中止
   * @return status 过滤器管理器使用此状态决定如何进一步迭代其它过滤器
   */
  virtual FilterStatus onNewConnection() PURE;
 
  /**
   * 初始化用于和过滤器管理器交互的读过滤器回调，过滤器被注册时，将被过滤器管理器调用一次
   * 任何需要用到底层连接的构造，需要在此函数的回调中执行
   *
   * IMPORTANT: 出站、复杂逻辑不要在此，放在onNewConnection()
   *
   */
  virtual void initializeReadFilterCallbacks(ReadFilterCallbacks&amp; callbacks) PURE;
}</pre>
<div class="blog_h3"><span class="graybg">WriteFilter</span></div>
<pre class="crayon-plain-tag">class WriteFilter {
public:
  /**
   * 当在此连接上发生数据写入时调用
   * @param data 需要写入的，可能已经被修改过的数据
   * @param end_stream 当连接启用半关闭语义时，用于提示是否到了最后一字节
   */
  virtual FilterStatus onWrite(Buffer::Instance&amp; data, bool end_stream) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">Filter</span></div>
<pre class="crayon-plain-tag">class Filter : public WriteFilter, public ReadFilter {}; </pre>
<div class="blog_h2"><span class="graybg">示例</span></div>
<p>Envoy提供了一个<a href="https://github.com/envoyproxy/envoy-filter-example">过滤器的Demon项目</a>。我们基于这个项目来入门过滤器的开发。</p>
<p>构建此项目之前，注意将Bazel升级到0.23以上，否则你可能遇到错误：bazel parameter 'ctx' has no default value ...</p>
<div class="blog_h3"><span class="graybg">构建示例</span></div>
<p>参考如下命令完成示例项目的构建：</p>
<pre class="crayon-plain-tag"># 签出源码
git clone https://github.com/envoyproxy/envoy-filter-example.git
git submodule update --init
cd envoy-filter-example

# 根据你的构建环境选择适当的bazel选项
# bazel需要到Google下载部分软件包源码，可能需要代理
bazel build  -c dbg --copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1"  //:envoy

# 运行Envoy单元测试
bazel test -c dbg --copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1" @envoy//test/...

# 运行集成测试
bazel test -c dbg --copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1" //:echo2_integration_test</pre>
<div class="blog_h3"><span class="graybg">代码解读</span></div>
<p>示例项目实现了名为Echo2的网络读过滤器，此过滤器的实现如下：</p>
<pre class="crayon-plain-tag">#pragma once

#include "envoy/network/filter.h"

#include "common/common/logger.h"

namespace Envoy {
namespace Filter {

//            实现接口
class Echo2 : public Network::ReadFilter, Logger::Loggable&lt;Logger::Id::filter&gt; {
public:
  Network::FilterStatus onData(Buffer::Instance&amp; data, bool end_stream) override;
  // 新连接到达后不做任何处理，继续调用下一个过滤器
  Network::FilterStatus onNewConnection() override { return Network::FilterStatus::Continue; }
  // 初始化回调集
  void initializeReadFilterCallbacks(Network::ReadFilterCallbacks&amp; callbacks) override {
    read_callbacks_ = &amp;callbacks;
  }

private:
  Network::ReadFilterCallbacks* read_callbacks_{};
};

} // namespace Filter
} // namespace Envoy


namespace Envoy {
namespace Filter {

// 接收到下游发来的数据后，简单的记录日志
Network::FilterStatus Echo2::onData(Buffer::Instance&amp; data, bool) {
  ENVOY_CONN_LOG(trace, "echo: got {} bytes", read_callbacks_-&gt;connection(), data.length());
  // 并把收到的数据直接Echo给下游
  read_callbacks_-&gt;connection().write(data, false);
  // 然后停止过滤器迭代，不调用它们
  return Network::FilterStatus::StopIteration;
}

} // namespace Filter
} // namespace Envoy</pre>
<p>每个过滤器都需要以一个独特的名称进行注册，否则Envoy无法知道它的存在，你也不能在配置文件中引用之。</p>
<p>Envoy过滤器的注册，一律通过模板化的静态变量Registry::RegisterFactory进行：</p>
<pre class="crayon-plain-tag">#include &lt;string&gt;

#include "echo2.h"

#include "envoy/registry/registry.h"
#include "envoy/server/filter_config.h"

namespace Envoy {
namespace Server {
namespace Configuration {

class Echo2ConfigFactory : public NamedNetworkFilterConfigFactory {
public:
  //                                                    没有配置
  Network::FilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message&amp;, FactoryContext&amp;) override {
    // 过滤器工厂回调，初始化过滤器链时，Envoy会调用此方法
    // 通常你会在这里实例化过滤器，并添加到过滤器管理器中
    return [](Network::FilterManager&amp; filter_manager) -&gt; void {
      filter_manager.addReadFilter(Network::ReadFilterSharedPtr{new Filter::Echo2()});
    };
  }

  // 创建空白的过滤器配置Proto消息对象
  // 任何过滤器的配置，均以不透明的google.protobuf.Struct类型传递，并被转换为JSON、解析，然后填充到此Proto对象
  ProtobufTypes::MessagePtr createEmptyConfigProto() override {
    return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Empty()};
  }

  // 过滤器的独特名称，很重要
  std::string name() override { return "echo2"; }
};

/**
 * 静态的注册
 */
static Registry::RegisterFactory&lt;Echo2ConfigFactory, NamedNetworkFilterConfigFactory&gt; registered_;

} 
} 
}</pre>
<p>Registry::RegisterFactory是一个模板，在这个例子中，它会创建一个Echo2ConfigFactory，并在调用FactoryRegistry&lt;NamedNetworkFilterConfigFactory&gt;::registerFactory时传入它：</p>
<pre class="crayon-plain-tag">template &lt;class T, class Base&gt; class RegisterFactory {
public:
  RegisterFactory() { FactoryRegistry&lt;Base&gt;::registerFactory(instance_); }

private:
  T instance_{};
};</pre>
<div class="blog_h3"><span class="graybg">运行示例</span></div>
<p>可以使用如下的Envoy配置文件：</p>
<pre class="crayon-plain-tag">static_resources:
  listeners:
    name: listener_0
    address:
      socket_address:
        address: 127.0.0.1
        port_value: 15001
    filter_chains:
    - filters:
      - name: echo2</pre>
<p>启动Envoy的命令行参数：</p>
<pre class="crayon-plain-tag">bazel-bin/envoy -c echo2_server.yaml -l trace</pre>
<p>使用Telnet登陆，然后可以输入文字并回车，Envoy会回响你的输入：</p>
<pre class="crayon-plain-tag">telnet 127.0.0.1 15001</pre>
<div class="blog_h1"><span class="graybg">HTTP过滤器</span></div>
<p>HTTP过滤器类似于网络过滤器，也是形成一个栈。HTTP过滤器栈由HttpConnectionManager管理，HttpConnectionManager是一个L4过滤器。</p>
<p>根据行为的不同，HTTP过滤器分为：</p>
<ol>
<li>解码器（Decoder），当HTTP连接管理器解码请求流的一部分（头、体、尾）时调用</li>
<li>编码器（Encoder），当HTTP连接管理器准备编码响应流的一部分（头、体、尾）时调用</li>
<li>编解码器，在上述两种情况下均调用</li>
</ol>
<p>需要注意，HTTP过滤器操作的对象是流，而不是连接：</p>
<ol>
<li>对于HTTP1.1，在任意时间点每个连接上最多有一个流</li>
<li>对于HTTP2或者gRPC，实现了连接的多路复用，允许多个流同时依托于单个L4连接</li>
</ol>
<div class="blog_h2"><span class="graybg">接口</span></div>
<p>HTTP过滤器接口屏蔽了L4协议的细节。和L4过滤器一样，HTTP过滤器也能够中止、继续过滤器迭代，各HTTP过滤器同样可以在同一个请求流的上下文内共享一些静态或动态数据。</p>
<p>L7过滤器的类图如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/04/streamfilter-diagram.png"><img class="aligncenter size-full wp-image-26603" src="https://blog.gmem.cc/wp-content/uploads/2019/04/streamfilter-diagram.png" alt="streamfilter-diagram" width="394" height="230" /></a></p>
<div class="blog_h3"><span class="graybg">StreamFilterBase</span></div>
<p>HTTP流编解码器公共的父接口：</p>
<pre class="crayon-plain-tag">class StreamFilterBase {
public:

  /**
   * 当过滤器将要被销毁时调用，销毁可能在流正常结束后，或者因为RESET提前发生
   *
   * 任何过滤器都应在此方法中确保，所有异步事件 —— 例如定时器、网络调用 —— 被清理干净
   */
  virtual void onDestroy() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamDecoderFilter</span></div>
<p>HTTP流解码器，负责处理下游发来的请求：</p>
<pre class="crayon-plain-tag">class StreamDecoderFilter {
public:
 
  /**
   * 处理已经被http_parser解析好的请求头
   * @param headers 请求头的映射
   * @param end_stream 提示当前流是否header-only的
   * @return FilterHeadersStatus 用于确定是否继续迭代后续过滤器
   */
  virtual FilterHeadersStatus decodeHeaders(HeaderMap&amp; headers, bool end_stream) PURE;

  /**
   * 处理已经被http_parser解析好的数据帧
   * @param data 存放数据帧的缓冲区
   * @param end_stream 提示当前是否最后一个数据帧
   * @return FilterDataStatus 用于确定是否继续迭代后续过滤器
   */
  virtual FilterDataStatus decodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  /**
   * 处理已经被http_parser解析好的请求尾，隐含end_stream = true
   * @param trailers supplies the decoded trailers.
   */
  virtual FilterTrailersStatus decodeTrailers(HeaderMap&amp; trailers) PURE;

  /**
   * 过滤器管理器调用此方法来初始化解码回调集
   */
  virtual void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks&amp; callbacks) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamEncoderFilter</span></div>
<p>HTTP流编码器，可以处理准备发给下游的响应：</p>
<pre class="crayon-plain-tag">class StreamEncoderFilter : public StreamFilterBase {
public:
  /*
   * 当配置Envoy，让其代理（通常不会配置）Expect:100-continue请求，
   * 并且当前请求指定了Expect:100-continue时，会调用此方法
   */
  virtual FilterHeadersStatus encode100ContinueHeaders(HeaderMap&amp; headers) PURE;

  /**
   * 处理响应头
   */
  virtual FilterHeadersStatus encodeHeaders(HeaderMap&amp; headers, bool end_stream) PURE;

  /**
   * 处理响应体
   */
  virtual FilterDataStatus encodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  /**
   * 处理响应尾，隐含end_stream = true
   */
  virtual FilterTrailersStatus encodeTrailers(HeaderMap&amp; trailers) PURE;

  /**
   * 处理元数据，新的元数据应该直接存入metadata_map
   * 不要通过StreamDecoderFilterCallbacks::encodeMetadata()来添加元数据
   *
   */
  virtual FilterMetadataStatus encodeMetadata(MetadataMap&amp; metadata_map) PURE;

  /**
   * 滤器管理器调用此方法来初始化编码回调集
   */
  virtual void setEncoderFilterCallbacks(StreamEncoderFilterCallbacks&amp; callbacks) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamFilter </span></div>
<pre class="crayon-plain-tag">class StreamFilter : public virtual StreamDecoderFilter, public virtual StreamEncoderFilter {};</pre>
<div class="blog_h2"><span class="graybg">示例</span></div>
<p>Envoy提供的envoy-filter-example示例项目中也提供了一个HTTP过滤器， 其代码存放在http-filter-example目录下。这是一个解码过滤器，它会为下游请求添加一个请求头。</p>
<div class="blog_h3"><span class="graybg">构建示例</span></div>
<p>参考如下命令完成示例项目的构建：</p>
<pre class="crayon-plain-tag">bazel build  -c dbg --copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1"  //http-filter-example:envoy </pre>
<div class="blog_h3"><span class="graybg">代码解读</span></div>
<p>过滤器配置、过滤器类的声明：</p>
<pre class="crayon-plain-tag">#pragma once

#include &lt;string&gt;

#include "envoy/server/filter_config.h"

#include "http-filter-example/http_filter.pb.h"

namespace Envoy {
namespace Http {

// 过滤器的配置对象
class HttpSampleDecoderFilterConfig {
public:
  // 构造函数，配置对象的入参是Proto消息，sample::Decoder依据你写的Proto文件自动生成
  HttpSampleDecoderFilterConfig(const sample::Decoder&amp; proto_config);

  const std::string&amp; key() const { return key_; }
  const std::string&amp; val() const { return val_; }

private:
  const std::string key_;
  const std::string val_;
};

typedef std::shared_ptr&lt;HttpSampleDecoderFilterConfig&gt; HttpSampleDecoderFilterConfigSharedPtr;

// 过滤器声明
class HttpSampleDecoderFilter : public StreamDecoderFilter {
public:
  HttpSampleDecoderFilter(HttpSampleDecoderFilterConfigSharedPtr);
  ~HttpSampleDecoderFilter();

  // 需要实现Http::StreamFilterBase的方法
  void onDestroy() override;

  // 需要实现Http::StreamDecoderFilter的方法
  FilterHeadersStatus decodeHeaders(HeaderMap&amp;, bool) override;
  FilterDataStatus decodeData(Buffer::Instance&amp;, bool) override;
  FilterTrailersStatus decodeTrailers(HeaderMap&amp;) override;
  void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks&amp;) override;

private:
  const HttpSampleDecoderFilterConfigSharedPtr config_;
  StreamDecoderFilterCallbacks* decoder_callbacks_;
  const LowerCaseString headerKey() const;
  const std::string headerValue() const;
};

} // namespace Http
} // namespace Envoy</pre>
<p>Proto文件：</p>
<pre class="crayon-plain-tag">syntax = "proto3";

package sample;

import "validate/validate.proto";

message Decoder {
    string key = 1 [(validate.rules).string.min_bytes = 1];
    string val = 2 [(validate.rules).string.min_bytes = 1];
}</pre>
<p>BUILD文件中的规则http_filter_proto负责从Proto文件生成C++代码。 </p>
<p>过滤器的实现如下：</p>
<pre class="crayon-plain-tag">#include &lt;string&gt;

#include "http_filter.h"

#include "envoy/server/filter_config.h"

namespace Envoy {
namespace Http {

// 配置对象的构造函数，入参是Proto对象
HttpSampleDecoderFilterConfig::HttpSampleDecoderFilterConfig(const sample::Decoder&amp; proto_config)
    : key_(proto_config.key()), val_(proto_config.val()) {}

// 过滤器的构造函数，入参是配置对象
HttpSampleDecoderFilter::HttpSampleDecoderFilter(HttpSampleDecoderFilterConfigSharedPtr config): config_(config) {}

HttpSampleDecoderFilter::~HttpSampleDecoderFilter() {}

void HttpSampleDecoderFilter::onDestroy() {}

const LowerCaseString HttpSampleDecoderFilter::headerKey() const {
  return LowerCaseString(config_-&gt;key());
}

const std::string HttpSampleDecoderFilter::headerValue() const {
  return config_-&gt;val();
}

FilterHeadersStatus HttpSampleDecoderFilter::decodeHeaders(HeaderMap&amp; headers, bool) {
  // 添加一个请求头
  headers.addCopy(headerKey(), headerValue());
  // 设置响应体
  auto body_text = fmt::format("{}:{}", headerKey().get(), headerValue().c_str());
  // 添加一个响应头
  auto modify_headers = [this](HeaderMap&amp; headers) -&gt; void {
    headers.addCopy(headerKey(), headerValue());
  };
  decoder_callbacks_-&gt;sendLocalReply(Http::Code::OK, body_text, modify_headers, absl::nullopt);
  // 本地响应已经发送，必须停止迭代，否则执行到Router会出现断言失败
  return FilterHeadersStatus::StopIteration;
}

// 如果不增加任何逻辑，简单返回Continue即可
FilterDataStatus HttpSampleDecoderFilter::decodeData(Buffer::Instance&amp;, bool) {
  return FilterDataStatus::Continue;
}

FilterTrailersStatus HttpSampleDecoderFilter::decodeTrailers(HeaderMap&amp;) {
  return FilterTrailersStatus::Continue;
}

void HttpSampleDecoderFilter::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks&amp; callbacks) {
  decoder_callbacks_ = &amp;callbacks;
}

} // namespace Http
} // namespace Envoy</pre>
<p>配置工厂，能够创建过滤器工厂，过滤器工厂FilterFactoryCb本质上就是一个函数，Envoy调用它来创建过滤器。</p>
<pre class="crayon-plain-tag">#include &lt;string&gt;

#include "http_filter.h"

#include "common/config/json_utility.h"
#include "envoy/registry/registry.h"

#include "http-filter-example/http_filter.pb.h"
#include "http-filter-example/http_filter.pb.validate.h"

namespace Envoy {
namespace Server {
namespace Configuration {

class HttpSampleDecoderFilterConfigFactory : public NamedHttpFilterConfigFactory {
public:
  // 从JSON配置创建过滤器工厂
  Http::FilterFactoryCb createFilterFactory(const Json::Object&amp; json_config, const std::string&amp;, FactoryContext&amp; context) override {
    sample::Decoder proto_config;
    // 将JSON配置转化为Proto配置
    translateHttpSampleDecoderFilter(json_config, proto_config);
    return createFilter(proto_config, context);
  }
  // V2 API的createFilterFactory变体，过滤器配置以Proto消息的形式传入，目前可以不实现此方法
  // 未来V1 API废弃后，必须实现
  Http::FilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message&amp; proto_config,
                                                     const std::string&amp;,
                                                     FactoryContext&amp; context) override {

    return createFilter(Envoy::MessageUtil::downcastAndValidate&lt;const sample::Decoder&amp;&gt;(proto_config), context);
  }

  // 新的空白配置Proto
  ProtobufTypes::MessagePtr createEmptyConfigProto() override {
    return ProtobufTypes::MessagePtr{new sample::Decoder()};
  }

  // 独特的名称
  std::string name() override { return "sample"; }

private:
  Http::FilterFactoryCb createFilter(const sample::Decoder&amp; proto_config, FactoryContext&amp;) {
    // 将Proto对象转化为配置对象
    Http::HttpSampleDecoderFilterConfigSharedPtr config =
        std::make_shared&lt;Http::HttpSampleDecoderFilterConfig&gt;( Http::HttpSampleDecoderFilterConfig(proto_config));

    return [config](Http::FilterChainFactoryCallbacks&amp; callbacks) -&gt; void {
      auto filter = new Http::HttpSampleDecoderFilter(config);
      // 添加此过滤器，注意L7过滤器由过滤器链工厂管理，每个监听器可以有多个过滤器链
      callbacks.addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr{filter});
    };
  }

  void translateHttpSampleDecoderFilter(const Json::Object&amp; json_config, sample::Decoder&amp; proto_config) {
    JSON_UTIL_SET_STRING(json_config, proto_config, key);
    JSON_UTIL_SET_STRING(json_config, proto_config, val);
  }
};

// 静态注册，类似于L4过滤器
static Registry::RegisterFactory&lt;HttpSampleDecoderFilterConfigFactory, NamedHttpFilterConfigFactory&gt; register_;

} // namespace Configuration
} // namespace Server
} // namespace Envoy</pre>
<div class="blog_h3"><span class="graybg">运行示例</span></div>
<p>可以使用如下的Envoy配置文件： </p>
<pre class="crayon-plain-tag">static_resources:
  listeners:
    name: listener_0
    address:
      socket_address:
        address: 127.0.0.1
        port_value: 15001
    filter_chains:
      - filters:
          - name: envoy.http_connection_manager
            config:
              stat_prefix: sample
              route_config:
                name: gmem
                virtual_hosts:
                  - name: gmem
                    domains: ["*"]
                    routes:
                      - match:
                          prefix: "/"
                        route:
                          cluster: gmem
              http_filters:
                - name: sample
                  config:
                    key: via
                    val: sample-filter
                - name: envoy.router
  clusters:
    - name: gmem
      connect_timeout: 1s
      type: STRICT_DNS
      dns_lookup_family: V4_ONLY
      lb_policy: ROUND_ROBIN
      hosts:
        - socket_address:
            address: gmem.cc
            port_value: 80</pre>
<p>使用curl来测试：</p>
<pre class="crayon-plain-tag">curl -D - http://127.0.0.1:15001

HTTP/1.1 200 OK
content-length: 17
content-type: text/plain
# 添加的响应头
via: sample-filter
date: Tue, 30 Apr 2019 03:44:45 GMT
server: envoy

# 设置的响应体
via:sample-filter</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/extend-envoy">扩展Envoy</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/extend-envoy/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Istio Mixer与Envoy的交互机制解读</title>
		<link>https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy</link>
		<comments>https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy#comments</comments>
		<pubDate>Mon, 18 Mar 2019 07:50:34 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=25903</guid>
		<description><![CDATA[<p>前言 在前些日子的文章Istio Pilot与Envoy的交互机制解读中我们详细研究了Istio Pilot如何基于xDS协议和Envoy代理进行各种配置信息的交换。Istio的另一个核心组件是Mixer，它提供三类功能： 遥测报告（Telemetry Reporting），该功能是服务网格可观察性的基础。为服务启用日志记录、监控、追踪、计费流 前置条件检查（Precondition Checking），响应服务请求之前进行一系列检查，例如身份验证、白名单检查、ACL检查 配额管理（Quota Management），基于特定的维度进行配额，控制对受限资源的争用 本文结合源码分析Mixer的设计、实现细节，同时关注Envoy与它的集成机制。 代码结构 istio Mixer的代码位于mixer目录下： 子目录 说明 adapter 包含各种适配器的实现，适配器封装了Mixer和外部基础设施后端（例如Prometheus）交互的逻辑 cmd 包含以下可执行文件的入口点： mixc  用于和Mixer服务器实例进行交互的命令行客户端 <a class="read-more" href="https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy">Istio Mixer与Envoy的交互机制解读</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">前言</span></div>
<p>在前些日子的文章<a href="/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a>中我们详细研究了Istio Pilot如何基于xDS协议和Envoy代理进行各种配置信息的交换。Istio的另一个核心组件是Mixer，它提供三类功能：</p>
<ol>
<li>遥测报告（Telemetry Reporting），该功能是服务网格可观察性的基础。为服务启用日志记录、监控、追踪、计费流</li>
<li>前置条件检查（Precondition Checking），响应服务请求之前进行一系列检查，例如身份验证、白名单检查、ACL检查</li>
<li>配额管理（Quota Management），基于特定的维度进行配额，控制对受限资源的争用</li>
</ol>
<p>本文结合源码分析Mixer的设计、实现细节，同时关注Envoy与它的集成机制。</p>
<div class="blog_h1"><span class="graybg">代码结构</span></div>
<div class="blog_h2"><span class="graybg">istio</span></div>
<p>Mixer的代码位于<a href="https://github.com/istio/istio/tree/master/mixer">mixer目录</a>下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">子目录</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>adapter</td>
<td>包含各种适配器的实现，适配器封装了Mixer和外部基础设施后端（例如Prometheus）交互的逻辑</td>
</tr>
<tr>
<td>cmd</td>
<td>
<p>包含以下可执行文件的入口点：</p>
<p style="padding-left: 30px;">mixc  用于和Mixer服务器实例进行交互的命令行客户端</p>
<p style="padding-left: 30px;">mixs  在本地启动一个Mixer服务器，或者列出可用的CRD、探测Mixer服务器的状态</p>
</td>
</tr>
<tr>
<td>docker</td>
<td>Docker镜像定义</td>
</tr>
<tr>
<td>template</td>
<td>
<p>模板，Mixer架构的基础构建块，通过自定义模板可以扩展Mixer</p>
<p>模板定义了将请求属性（Attribute）转换为适配器的输入的Schema（类型信息，使用Protubuf语法描述），每个适配器可以支持任意数量的template</p>
<p>模板决定了适配器会收到的数据、也决定了使用适配器必须创建的instance</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">Mixs启动过程</span></div>
<p>如果使用Istio官方默认的Chart来部署，则会创建istio-telemetry、istio-policy两套Deployment。它们的启动参数没有区别，分别负责Mixer的遥测、策略检查。这两个Deployment分别对应同名的Service，监听9091端口。</p>
<p>实际上网络监听是由Mixs的Sidecar，也就是Envoy负责的。Mixs Pod本身监听的是UDS  unix:///sock/mixer.socket，Envoy负责将9091端口的请求转发给此UDS。</p>
<div class="blog_h2"><span class="graybg">如何调试</span></div>
<p>在本地调试Mixer服务端时，参考如下启动参数：</p>
<pre class="crayon-plain-tag">mixs server --port 9091 --monitoringPort 9099  --log_output_level api:debug \
    --configStoreURL=k8s:///home/alex/.kube/config --configDefaultNamespace=istio-system</pre>
<div class="blog_h2"><span class="graybg">入口点</span></div>
<p>mixs server的入口点位于：</p>
<pre class="crayon-plain-tag">func main() {
// supportedTemplates()  map[string]template.Info
// supportedAdapters() []adptr.InfoFn
// 这两个方法都是自动生成的，包含编译的Mixer支持的模板、适配器的列表
// 模板/适配器信息中包含其属性清单
	rootCmd := cmd.GetRootCmd(os.Args[1:], supportedTemplates(), supportedAdapters(), shared.Printf, shared.Fatalf)

	if err := rootCmd.Execute(); err != nil {
		os.Exit(-1)
	}
}

func serverCmd(info map[string]template.Info, adapters []adapter.InfoFn, printf, fatalf shared.FormatFn) *cobra.Command {
// 默认Mixer参数
	sa := server.DefaultArgs()
// 使用自动生成的模板、适配器信息
	sa.Templates = info
	sa.Adapters = adapters

	serverCmd := &amp;cobra.Command{
		Use:   "server",
		Short: "Starts Mixer as a server",
		Run: func(cmd *cobra.Command, args []string) {
// 调用runServer启动服务
			runServer(sa, printf, fatalf)
		},
	}
}

func runServer(sa *server.Args, printf, fatalf shared.FormatFn) {
	// 创建服务器对象
	s, err := server.New(sa)
	// 启动gRPC服务
	s.Run()
	// 等待shutdown信号可读
	err = s.Wait()
        // 执行清理工作
	_ = s.Close()
}</pre>
<div class="blog_h3"><span class="graybg">server.New</span></div>
<p>该函数创建一个全功能的Mixer服务器，并且准备好接收请求：</p>
<pre class="crayon-plain-tag">func New(a *Args) (*Server, error) {
	return newServer(a, newPatchTable())
}</pre>
<div class="blog_h3"><span class="graybg">server.Run</span></div>
<p>该方法启动Mixs服务器：</p>
<pre class="crayon-plain-tag">func (s *Server) Run() {
// 准备好关闭通道
	s.shutdown = make(chan error, 1)
// 设置可用性状态，并通知探针控制器，探针被嵌入到Server
	s.SetAvailable(nil)
	go func() {
		// 启动gRPC服务，传入原始套接字的监听器对象
		err := s.server.Serve(s.listener)

		// 关闭通道
		s.shutdown &lt;- err
	}()
}</pre>
<div class="blog_h3"><span class="graybg">server.Wait</span></div>
<p>该方法很简单，就是在shutdown通道上等待。</p>
<div class="blog_h3"><span class="graybg">server.Close</span></div>
<p>该方法关闭Mixs服务器使用的各种资源。</p>
<div class="blog_h2"><span class="graybg">patchTable</span></div>
<p>newPatchTable创建一个新的patchTable结构：</p>
<pre class="crayon-plain-tag">func newPatchTable() *patchTable {
	return &amp;patchTable{
		newRuntime:    runtime.New,
		configTracing: tracing.Configure,
		startMonitor:  startMonitor,
		listen:        net.Listen,
		configLog:     log.Configure,
		runtimeListen: func(rt *runtime.Runtime) error { return rt.StartListening() },
	}
}</pre>
<p>此结构就是几个函数的集合：</p>
<pre class="crayon-plain-tag">type patchTable struct {
// 此函数创建一个Runtime，Runtime是Mixer运行时环境的主要入口点
// 它监听配置、实例化Handler、创建分发机制（dispatch machinery）、处理请求
	newRuntime func(s store.Store, templates map[string]*template.Info, adapters map[string]*adapter.Info,
		defaultConfigNamespace string, executorPool *pool.GoroutinePool,
		handlerPool *pool.GoroutinePool, enableTracing bool) *runtime.Runtime
// 配置追踪系统，通常在启动时调用一次，此调用返回后，追踪系统可以接受数据
	configTracing func(serviceName string, options *tracing.Options) (io.Closer, error)
// 暴露Mixer自我监控信息的HTTP服务
	startMonitor  func(port uint16, enableProfiling bool, lf listenFunc) (*monitor, error)
// 监听本地端口并返回一个监听器
	listen        listenFunc
// 配置Istio的日志子系统
	configLog     func(options *log.Options) error
// 让Runtime开始监听配置变更，每当配置变更，Runtime处理新配置并创建Dispatcher
	runtimeListen func(runtime *runtime.Runtime) error
}</pre>
<div class="blog_h2"><span class="graybg">newServer</span></div>
<p>此方法创建一个新的Mixs服务器，服务器由下面的结构表示：</p>
<pre class="crayon-plain-tag">type Server struct {
// 关闭通道
	shutdown  chan error
// 服务API请求的gRPC服务器
	server    *grpc.Server
// API线程池
	gp        *pool.GoroutinePool
// 适配器线程池
	adapterGP *pool.GoroutinePool
// API网络监听器
	listener  net.Listener
// 监控服务器，此结构包含两个字段，一个是http.Server，一个是关闭通道
	monitor   *monitor
// 用于关闭追踪子系统
	tracer    io.Closer
// 可伸缩的策略检查缓存
	checkCache *checkcache.Cache
// 将入站API调用分发给配置好的适配器
	dispatcher dispatcher.Dispatcher

	livenessProbe  probe.Controller
	readinessProbe probe.Controller
// 管理探针控制器所需要的可用性状态，内嵌
	*probe.Probe
}</pre>
<p>该方法的逻辑如下：</p>
<pre class="crayon-plain-tag">func newServer(a *Args, p *patchTable) (*Server, error) {
// 校验Mixs启动参数
	if err := a.validate(); err != nil {
		return nil, err
	}
// 配置日志子系统
	if err := p.configLog(a.LoggingOptions); err != nil {
		return nil, err
	}

	apiPoolSize := a.APIWorkerPoolSize
	adapterPoolSize := a.AdapterWorkerPoolSize

	s := &amp;Server{}

// 创建线程池
// API 线程池
	s.gp = pool.NewGoroutinePool(apiPoolSize, a.SingleThreaded)
	s.gp.AddWorkers(apiPoolSize)

// 适配器线程池
	s.adapterGP = pool.NewGoroutinePool(adapterPoolSize, a.SingleThreaded)
	s.adapterGP.AddWorkers(adapterPoolSize)

	tmplRepo := template.NewRepository(a.Templates)
// 从适配器名称到adapter.Info的映射
	adapterMap := config.AdapterInfoMap(a.Adapters, tmplRepo.SupportsTemplate)

// 状态探针
	s.Probe = probe.NewProbe()

// gRPC选项
	var grpcOptions []grpc.ServerOption
	grpcOptions = append(grpcOptions, grpc.MaxConcurrentStreams(uint32(a.MaxConcurrentStreams)))
	grpcOptions = append(grpcOptions, grpc.MaxMsgSize(int(a.MaxMessageSize)))
// 一元（请求/应答模式）gRPC请求的服务器端拦截器
	var interceptors []grpc.UnaryServerInterceptor
	var err error

// 如果启用了追踪（tracing.option提供了ZipkinURL、JaegerURL或LogTraceSpans=true）
	if a.TracingOptions.TracingEnabled() {
		s.tracer, err = p.configTracing("istio-mixer", a.TracingOptions)
		if err != nil {
			_ = s.Close()
			return nil, fmt.Errorf("unable to setup tracing")
		}
// 则添加基于OpenTracing的追踪拦截器
		interceptors = append(interceptors, otgrpc.OpenTracingServerInterceptor(ot.GlobalTracer()))
	}
// OpenTracing、Prometheus监控拦截器，都来自项目https://github.com/grpc-ecosystem
// 将Prometheus拦截器添加到末尾
	interceptors = append(interceptors, grpc_prometheus.UnaryServerInterceptor)
// 启用Prometheus时间直方图记录，RPC调用的耗时会被记录。Prometheus持有、查询Histogram指标的成本比较高
// 生成的指标都是面向gRPC协议的、通用的，不牵涉Istio的逻辑。指标名以grpc_开头
	grpc_prometheus.EnableHandlingTimeHistogram()
// 将所有拦截器串连为单个拦截器，并添加到gRPC选项
	grpcOptions = append(grpcOptions, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(interceptors...)))

	network := "tcp"
	address := fmt.Sprintf(":%d", a.APIPort)
	if a.APIAddress != "" {
		idx := strings.Index(a.APIAddress, "://")
		if idx &lt; 0 {
			address = a.APIAddress
		} else {
			network = a.APIAddress[:idx]
			address = a.APIAddress[idx+3:]
		}
	}

	if network == "unix" {
// 如果监听UDS，则移除先前的文件
		if err = os.Remove(address); err != nil &amp;&amp; !os.IsNotExist(err) {
			// 除了文件未找到以外的错误，都不允许
			return nil, fmt.Errorf("unable to remove unix://%s: %v", address, err)
		}
	}
// 调用net.Listen监听
	if s.listener, err = p.listen(network, address); err != nil {
		_ = s.Close()
		return nil, fmt.Errorf("unable to listen: %v", err)
	}
// ConfigStore用于测试目的，通常都会使用ConfigStoreURL（例如k8s:///home/alex/.kube/config）
	st := a.ConfigStore
	if st != nil &amp;&amp; a.ConfigStoreURL != "" {
		_ = s.Close()
		return nil, fmt.Errorf("invalid arguments: both ConfigStore and ConfigStoreURL are specified")
	}

	if st == nil {
		configStoreURL := a.ConfigStoreURL
		if configStoreURL == "" {
			configStoreURL = "k8s://"
		}
// Registry存储URL scheme与后端实现之间的对应关系

		reg := store.NewRegistry(config.StoreInventory()...)
		groupVersion := &amp;schema.GroupVersion{Group: crd.ConfigAPIGroup, Version: crd.ConfigAPIVersion}
// 创建一个Store实例，它持有Backend，Backend代表一个无类型的Mixer存储后端 —— 例如K8S
// 默认情况下，configStoreURL的Scheme为k8s，Istio会调用config/crd.NewStore
// 传入configStoreURL、GroupVersion、criticalKinds 来创建Backend
		if st, err = reg.NewStore(configStoreURL, groupVersion, rc.CriticalKinds()); err != nil {
			_ = s.Close()
			return nil, fmt.Errorf("unable to connect to the configuration server: %v", err)
		}
	}

	var rt *runtime.Runtime
// 所有模板，目标决定了各分类的适配器（例如所有metric类适配器）在运行时需要处理的数据类型
	templateMap := make(map[string]*template.Info, len(a.Templates))
	for k, v := range a.Templates {
		t := v
		templateMap[k] = &amp;t
	}
// 创建运行时，传入存储、模板、适配器、线程池、是否启用追踪等信息
	rt = p.newRuntime(st, templateMap, adapterMap, a.ConfigDefaultNamespace,
		s.gp, s.adapterGP, a.TracingOptions.TracingEnabled())

// 监听配置存储的变更，初始化配置
	if err = p.runtimeListen(rt); err != nil {
		_ = s.Close()
		return nil, fmt.Errorf("unable to listen: %v", err)
	}

// 等待配置存储同步完成
	log.Info("Awaiting for config store sync...")
	if err := st.WaitForSynced(30 * time.Second); err != nil {
		return nil, err
	}

// 设置分发器，分发器负责将API请求分发给配置好的适配器处理
	s.dispatcher = rt.Dispatcher()

// 如果启用了策略检查缓存，则创建LRU缓存对象
	if a.NumCheckCacheEntries &gt; 0 {
		s.checkCache = checkcache.New(a.NumCheckCacheEntries)
	}

// 此全局变量决定是否利用包golang.org/x/net/trace进行gRPC调用追踪
	grpc.EnableTracing = a.EnableGRPCTracing

// 节流阀，限制调用频度
	throttler := loadshedding.NewThrottler(a.LoadSheddingOptions)
// Evaluator方法根据名称返回配置好的LoadEvaluator
// LoadEvaluator能够评估请求是否超过阈值
	if eval := throttler.Evaluator(loadshedding.GRPCLatencyEvaluatorName); eval != nil {
		grpcOptions = append(grpcOptions, grpc.StatsHandler(eval.(*loadshedding.GRPCLatencyEvaluator)))
	}

// 创建gRPC服务器
	s.server = grpc.NewServer(grpcOptions...)
// 注册服务到gRPC服务器
// 注册时需要提供grpc.ServiceDesc，其中包含服务名、方法集合（方法名到处理函数的映射
// api.NewGRPCServer返回 mixerpb.MixerServer 接口，它仅仅包含Check / Report两个方法
	mixerpb.RegisterMixerServer(s.server, api.NewGRPCServer(s.dispatcher, s.gp, s.checkCache, throttler))

// 探针
	if a.LivenessProbeOptions.IsValid() {
		s.livenessProbe = probe.NewFileController(a.LivenessProbeOptions)
		s.RegisterProbe(s.livenessProbe, "server")
		s.livenessProbe.Start()
	}

	if a.ReadinessProbeOptions.IsValid() {
		s.readinessProbe = probe.NewFileController(a.ReadinessProbeOptions)
		rt.RegisterProbe(s.readinessProbe, "dispatcher")
		st.RegisterProbe(s.readinessProbe, "store")
		s.readinessProbe.Start()
	}

// 启动监控服务
	if s.monitor, err = p.startMonitor(a.MonitoringPort, a.EnableProfiling, p.listen); err != nil {
		_ = s.Close()
		return nil, fmt.Errorf("unable to setup monitoring: %v", err)
	}
// 启动ControlZ监听器
	go ctrlz.Run(a.IntrospectionOptions, nil)

	return s, nil
}</pre>
<div class="blog_h2"><span class="graybg">p.newRuntime</span></div>
<p>patchTable的newRuntime函数会调用runtime.New，创建一个新的Mixer运行时 —— Mixer运行时环境的主要入口点，负责监听配置、实例化Handler、创建分发机制（dispatch machinery）、处理请求：</p>
<pre class="crayon-plain-tag">func New(
	s store.Store,
	templates map[string]*template.Info,
	adapters map[string]*adapter.Info,
	defaultConfigNamespace string,
	executorPool *pool.GoroutinePool,
	handlerPool *pool.GoroutinePool,
	enableTracing bool) *Runtime {

	// Ephemeral表示一个短暂的配置状态，它可以被入站配置变更事件所更新
	// Ephemeral本身包含的数据没有价值，你必须调用它的BuildSnapshot方法来创建稳定的、完全解析的配置的快照
	e := config.NewEphemeral(templates, adapters)
	rt := &amp;Runtime{
// 默认配置命名空间
		defaultConfigNamespace: defaultConfigNamespace,
// 短暂配置状态
		ephemeral:              e,
// 配置快照
		snapshot:               config.Empty(),
// 适配器处理器列表
		handlers:               handler.Empty(),
// API请求分发器，需要协程池
		dispatcher:             dispatcher.New(executorPool, enableTracing),
// 适配器处理器的协程池
		handlerPool:            handlerPool,
		Probe:                  probe.NewProbe(),
		store:                  s,
	}

// 从ephemeral构建出新c.snapshot、新c.handlers、新路由表（用于解析入站请求并将其路由给适当的处理器）
// 然后替换路由表，最后清理上一次配置对应的处理器
	rt.processNewConfig()
// 设置探针结果为：尚未监听存储
	rt.Probe.SetAvailable(errNotListening)

	return rt
}</pre>
<div class="blog_h2"><span class="graybg">p.runtimeListen</span></div>
<p>创建Runtime之后，p.runtimeListen被调用。此函数会调用Runtime.StartListening方法来监听配置的变更，同样会立即触发processNewConfig调用。之后，processNewConfig调用会通过store.WatchChanges的回调反复发生。</p>
<pre class="crayon-plain-tag">func (c *Runtime) StartListening() error {
// Runtime的状态锁
	c.stateLock.Lock()
	defer c.stateLock.Unlock()

	if c.shutdown != nil {
		return errors.New("already listening")
	}

// 生成adapter、template等对象类型到它的proto消息的映射（合并到一个映射中）
// adapter.Info.DefaultConfig、template.Info.CtrCfg，以及
// &amp;configpb.Rule{}、&amp;configpb.AttributeManifest{}、&amp;v1beta1.Info{} ...
// 都实现了proto.Message接口
	kinds := config.KindMap(c.snapshot.Adapters, c.snapshot.Templates)
// 开始监控存储，返回当前资源集（key到spec的映射）、监控用的通道
	data, watchChan, err := store.StartWatch(c.store, kinds)
	if err != nil {
		return err
	}

// 设置并覆盖相同的临时状态，其实就是把ephemeral.entries = data
	c.ephemeral.SetState(data)
// 处理新配置
	c.processNewConfig()
// 初始化运行时的关闭通道
	c.shutdown = make(chan struct{})
// 增加一个计数
	c.waitQuiesceListening.Add(1)
	go func() {
// 只有shutdown通道关闭，此监控配置存储变化的循环才会退出
// 当有新的配置变更被发现后，调用onConfigChange，此方法会导致processNewConfig
		store.WatchChanges(watchChan, c.shutdown, watchFlushDuration, c.onConfigChange)
// shutdown通道关闭后，
		c.waitQuiesceListening.Done()
	}()
// 重置可用性状态，此等待组不再阻塞，StopListening方法可以顺利返回
	c.Probe.SetAvailable(nil)

	return nil
}</pre>
<div class="blog_h2"><span class="graybg">onConfigChange</span></div>
<p>当配置存储有变化后，Runtime的该方法会被调用，它的逻辑很简单：</p>
<pre class="crayon-plain-tag">func (c *Runtime) onConfigChange(events []*store.Event) {
// 更新或者擅长ephemeral.entries中的条目
	c.ephemeral.ApplyEvent(events)
// 对最新的配置进行处理
	c.processNewConfig()default
}</pre>
<div class="blog_h2"><span class="graybg">processNewConfig </span></div>
<p>Runtime的processNewConfig方法负责处理从配置存储（K8S）中<span style="background-color: #c0c0c0;">拉取的最新CR，然后创建配置快照、创建处理器表、路由表，并改变Dispatcher的路由</span>：</p>
<pre class="crayon-plain-tag">func (c *Runtime) processNewConfig() {
// 构建一个稳定的、完全解析的配置的快照
	newSnapshot, _ := c.ephemeral.BuildSnapshot()
// 当前运行时使用的处理器
	oldHandlers := c.handlers
// 创建新的处理器表
	newHandlers := handler.NewTable(oldHandlers, newSnapshot, c.handlerPool)
// 返回ExpressionBuilder，用于创建一系列预编译表达式
	builder := compiled.NewBuilder(newSnapshot.Attributes)
// 构建并返回路由表，路由表决定了什么条件下调用什么适配器
	newRoutes := routing.BuildTable(
		newHandlers, newSnapshot, builder, c.defaultConfigNamespace, log.DebugEnabled())

// 改变分发器的路由，分发器负责基于路由表来调用适配器
	oldContext := c.dispatcher.ChangeRoute(newRoutes)

// 修改实例变量
	c.handlers = newHandlers
	c.snapshot = newSnapshot

	log.Debugf("New routes in effect:\n%s", newRoutes)

// 关闭旧的处理器，注意处理器实现了io.Closer接口，这个接口由Istio自己负责，和适配器开发无关
	cleanupHandlers(oldContext, oldHandlers, newHandlers, maxCleanupDuration)
}</pre>
<div class="blog_h2"><span class="graybg">e.BuildSnapshot</span></div>
<p>该方法生成一个完全解析的（没有任何外部依赖）的配置快照。<span style="background-color: #c0c0c0;">快照主要包含静态、动态模板/适配器信息、以及规则信息</span>：</p>
<pre class="crayon-plain-tag">func (e *Ephemeral) BuildSnapshot() (*Snapshot, error) {
	errs := &amp;multierror.Error{}
// 下一个快照的ID
	id := e.nextID
	e.nextID++

	log.Debugf("Building new config.Snapshot: id='%d'", id)

// 一组和istio本身状态监控有关的Prometheus计数器
	counters := newCounters(id)

	e.lock.RLock()

// 处理属性清单，获得属性列表。清单来源有三个地方：
// 1、配置存储中attributemanifest类型的CR。第一次调用该方法时，尚未加载这些CR
// 2、自动生成的template.Info.AttributeManifests
// 注意清单中每个属性，都具有全网格唯一的名称
	attributes := e.processAttributeManifests(counters, errs)

// 处理静态适配器的处理器配置 —— 各种适配器的CR/实例，获得处理器（HandlerStatic）列表
// 对于从配置存储加载的资源，如果在自动生成的adapter.Info中找到对应条目，则认为是合法的处理器
// 对于每个处理器，会创建HandlerStatic结构，此结构表示基于Compiled-in的适配器的处理器
	shandlers := e.processStaticAdapterHandlerConfigs(counters, errs)

// 返回属性描述符查找器（AttributeDescriptorFinder）
	af := ast.NewFinder(attributes)
// 处理静态模板的实例配置 —— 各种模板的CR，获得实例（InstanceStatic）列表
// 对于从配置存储加载的资源，如果在自动生成的template.Info中找到对应条目，则认为是合法的实例
// 对于每个实例，会创建InstanceStatic结构，此结构表示基于Compiled-in的模板的Instance
	instances := e.processInstanceConfigs(af, counters, errs)

// 开始处理动态资源，所谓动态资源，是指没有特定CRD的模板（也就没有对应CR的实例）
// 以及没有特定CRD的适配器（也就没有对应CR的处理器）
// 动态模板注册为template类型的CR
	dTemplates := e.processDynamicTemplateConfigs(counters, errs)
// 动态适配器注册为adapter类型的CR
	dAdapters := e.processDynamicAdapterConfigs(dTemplates, counters, errs)
// 动态处理器注册为handler类型的CR，它必须引用某个adapter的名称
	dhandlers := e.processDynamicHandlerConfigs(dAdapters, counters, errs)
// 动态处理器注册为instance类型的CR，它必须引用某个template的名称
	dInstances := e.processDynamicInstanceConfigs(dTemplates, af, counters, errs)

// 处理规则，规则可以引用上述的静态和动态资源
	rules := e.processRuleConfigs(shandlers, instances, dhandlers, dInstances, af, counters, errs)

// 构建配置快照
	s := &amp;Snapshot{
		ID:                id,
		Templates:         e.templates,
		Adapters:          e.adapters,
		TemplateMetadatas: dTemplates,
		AdapterMetadatas:  dAdapters,
		Attributes:        ast.NewFinder(attributes),
		HandlersStatic:    shandlers,
		InstancesStatic:   instances,
		Rules:             rules,

		HandlersDynamic:  dhandlers,
		InstancesDynamic: dInstances,

		Counters: counters,
	}
	e.lock.RUnlock()

	return s, errs.ErrorOrNil()
}</pre>
<div class="blog_h1"><span class="graybg">适配器初始化过程</span></div>
<p>适配器的初始化过程，是Mixer服务器初始化的一部分。在Mixer服务器启动过程中有如下逻辑：</p>
<pre class="crayon-plain-tag">adapterMap := config.AdapterInfoMap(a.Adapters, tmplRepo.SupportsTemplate)</pre>
<p> 该方法会生成得到所有适配器的adaptor.Info对象：</p>
<pre class="crayon-plain-tag">type Info struct {
	// 适配器的正式名称，必须是RFC 1035兼容的DNS标签
	// 此名称会用在Istio配置中，因此应当简短而具有描述性
	Name string
	// 实现此适配器的包，例如
	// istio.io/istio/mixer/adapter/denier
	Impl string
	// 人类可读的适配器的描述信息
	Description string
	// 该函数指针能够创建一个新的HandlerBuilder，HandlerBuilder能够创建出此适配器的Handler
	NewBuilder NewBuilderFn
	// 此适配器声明支持的模板
	SupportedTemplates []string
	// 传递给HandlerBuilder.Build的适配器的默认参数
	DefaultConfig proto.Message
}</pre>
<p>入参a.Adapters来自supportedAdapters()，此函数是自动生成的。a.Adapters的每个元素的类型是<span style="background-color: #c0c0c0;">adapter.InfoFn。调用此函数即得到对应的adaptor.Info对象</span>：</p>
<pre class="crayon-plain-tag">type InfoFn func() Info</pre>
<p>config.AdapterInfoMap的主要逻辑就是调用各种适配器的adapter.InfoFn方法，并且对adaptor.Info进行各种校验。例如检查它的NewBuilder、NewBuilder字段是否为非空，检查它是否和声明支持的模板兼容。</p>
<p>适配器如果需要初始化，那么<span style="background-color: #c0c0c0;">初始化逻辑就发生在InfoFn中</span>。</p>
<div class="blog_h2"><span class="graybg">Prometheus</span></div>
<p>本节以Prometheus适配器为例，了解适配器的初始化过程。</p>
<div class="blog_h3"><span class="graybg">初始化Info</span></div>
<pre class="crayon-plain-tag">const (
	metricsPath = "/metrics"
// Istio会暴露三个和Prometheus Exporter端口：
// istio-mixer.istio-system:42422，所有由Mixer的Prometheus适配器生成的网格指标
// istio-mixer.istio-system:9093，用于监控Mixer自身的指标
// istio-mixer.istio-system:9102，Envoy生成的原始统计信息，从Statsd转换为Prometheus格式
	defaultAddr = ":42422"
)

func GetInfo() adapter.Info {
	ii, _ := GetInfoWithAddr(defaultAddr)
	return ii
}</pre>
<p>GetInfoWithAddr方法的实现如下：</p>
<pre class="crayon-plain-tag">func GetInfoWithAddr(addr string) (adapter.Info, Server) {
// HandlerBuilder单例
	singletonBuilder := &amp;builder{
// HTTP服务器，这里不会启动监听
		srv: newServer(addr),
	}
// 创建注册表singletonBuilder.registry = prometheus.NewPedanticRegistry()
// 情况指标信息 singletonBuilder.metrics = make(map[string]*cinfo)
	singletonBuilder.clearState()
// 返回adaptor.Info对象
	return adapter.Info{
		Name:        "prometheus",
		Impl:        "istio.io/istio/mixer/adapter/prometheus",
		Description: "Publishes prometheus metrics",
		SupportedTemplates: []string{
			metric.TemplateName,
		},
		NewBuilder:    func() adapter.HandlerBuilder { return singletonBuilder },
		DefaultConfig: &amp;config.Params{},
	}, singletonBuilder.srv
}</pre>
<div class="blog_h3"><span class="graybg">初始化Handler </span></div>
<p>每当配置变更后，适配器的Handler会被初始化。Runtime.processNewConfig会调用：</p>
<pre class="crayon-plain-tag">newHandlers := handler.NewTable(oldHandlers, newSnapshot, c.handlerPool)</pre>
<p>创建handler.Table，此表包含了所有实例化的、配置好的适配器的处理器的信息：</p>
<pre class="crayon-plain-tag">type Table struct {
// 表格条目
	entries map[string]Entry

	counters tableCounters
}

// 单个处理器
type Entry struct {
	// 处理器的名称
	Name string

	// 处理器对象
	Handler adapter.Handler

	// 适配器名称
	AdapterName string

	// 创建此处理器使用的适配器配置（参数）的签名信息
	Signature signature

	// 传递给处理器的adapter.Env
	env env
}</pre>
<p>每个适配器可以消费多个实例，对于适配器和实例的每个组合，handler.NewTable方法会为其创建Handler：</p>
<pre class="crayon-plain-tag">// 适配器实例 - 模板实例的映射
// map[*HandlerStatic][]*InstanceStatic
instancesByHandler := config.GetInstancesGroupedByHandlers(snapshot)
// map[*HandlerDynamic][]*InstanceDynamic
instancesByHandlerDynamic := config.GetInstancesGroupedByHandlersDynamic(snapshot)

// 表
t := &amp;Table{
	entries:  make(map[string]Entry, len(instancesByHandler)+len(instancesByHandlerDynamic)),
	counters: newTableCounters(snapshot.ID),
}

// 对于每个静态处理器 - 实例组合
for handler, instances := range instancesByHandler {
        // 为其创建条目，并加入到表中
	createEntry(old, t, handler, instances, snapshot.ID,
// 这个回调用于用于创建处理器
		func(handler hndlr, instances interface{}) (h adapter.Handler, e env, err error) {
// 环境信息
			e = NewEnv(snapshot.ID, handler.GetName(), gp).(env)
// 创建出处理器
			h, err = config.BuildHandler(handler.(*config.HandlerStatic), instances.([]*config.InstanceStatic), e, snapshot.Templates)
			return h, e, err
		})
}

// 对于每个动态处理器 - 实例组合
for handler, instances := range instancesByHandlerDynamic {
	createEntry(old, t, handler, instances, snapshot.ID, ...
}</pre>
<p>config.BuildHandler经过几层转发，最终会调用Prometheus适配器的方法：</p>
<pre class="crayon-plain-tag">func (b *builder) Build(ctx context.Context, env adapter.Env) (adapter.Handler, error) {

	cfg := b.cfg
	var metricErr *multierror.Error

// 用于收集指标配置
	newMetrics := make([]*config.Params_MetricInfo, 0, len(cfg.Metrics))

	// 检查指标是否被重新定义，也就是对应的CR是否被修改
	// 如果是，则清空指标注册表、指标映射。重定义会导致Prometheus客户端Panic
	// 添加、移除则没有问题
	var cl *cinfo
// 遍历新配置的指标列表
	for _, m := range cfg.Metrics {
		// 当前指标表中没有匹配项，加入
		if cl = b.metrics[m.InstanceName]; cl == nil {
			newMetrics = append(newMetrics, m)
			continue
		}

		// 散列值没有变，和之前的指标配置一样
		if cl.sha == computeSha(m, env.Logger()) {
			continue
		}

		// 散列值不匹配，发生了重定义。适配器需要重现加载
		env.Logger().Warningf("Metric %s redefined. Reloading adapter.", m.Name)
		// 重建注册表、清空指标信息
		b.clearState()
		// 将所有指标作为新配置看待
		newMetrics = cfg.Metrics
		break
	}

	env.Logger().Debugf("%d new metrics defined", len(newMetrics))

// 遍历处理所有新指标
	var err error
	for _, m := range newMetrics {
		ns := defaultNS
		if len(m.Namespace) &gt; 0 {
			ns = safeName(m.Namespace)
		}
// 指标全名，即CR的名称
		mname := m.InstanceName
		if len(m.Name) != 0 {
// 转换为短名
			mname = m.Name
		}
// 构建出指标信息cinfo
		ci := &amp;cinfo{kind: m.Kind, sha: computeSha(m, env.Logger())}
		ci.sortedLabels = make([]string, len(m.LabelNames))
		copy(ci.sortedLabels, m.LabelNames)
		sort.Strings(ci.sortedLabels)

// 根据指标类型的不同，分别处理。逻辑都是注册指标到注册表
		switch m.Kind {
		case config.GAUGE:
			ci.c, err = registerOrGet(b.registry, newGaugeVec(ns, mname, m.Description, m.LabelNames))
			b.metrics[m.InstanceName] = ci
		case config.COUNTER:
			ci.c, err = registerOrGet(b.registry, newCounterVec(ns, mname, m.Description, m.LabelNames))
			b.metrics[m.InstanceName] = ci
		case config.DISTRIBUTION:
			ci.c, err = registerOrGet(b.registry, newHistogramVec(ns, mname, m.Description, m.LabelNames, m.Buckets))
			b.metrics[m.InstanceName] = ci
		default:
			metricErr = multierror.Append(metricErr, fmt.Errorf("unknown metric kind (%d); could not register metric %v", m.Kind, m))
		}
	}

// 启动Exporter的HTTP服务器，如果已经启动则不管
	if err := b.srv.Start(env, promhttp.HandlerFor(b.registry, promhttp.HandlerOpts{})); err != nil {
		return nil, err
	}

// 如果配置了指标过期功能，则定期删除老旧指标
	var expiryCache cache.ExpiringCache
	if cfg.MetricsExpirationPolicy != nil {
		checkDuration := cfg.MetricsExpirationPolicy.ExpiryCheckIntervalDuration
		if checkDuration == 0 {
			checkDuration = cfg.MetricsExpirationPolicy.MetricsExpiryDuration / 2
		}
		expiryCache = cache.NewTTLWithCallback(
			cfg.MetricsExpirationPolicy.MetricsExpiryDuration,
			checkDuration,
			deleteOldMetrics)
	}

	return &amp;handler{b.srv, b.metrics, expiryCache}, metricErr.ErrorOrNil()
}</pre>
<div class="blog_h3"><span class="graybg">暴露指标</span></div>
<p>b.srv.Start会启动作为Exporter的HTTP服务器： </p>
<pre class="crayon-plain-tag">func (s *serverInst) Start(env adapter.Env, metricsHandler http.Handler) (err error) {
// 加锁保护
	s.lock.Lock()
	defer s.lock.Unlock()

	// 如果服务器已经启动了，则委托
	// just switch the delegate handler.
	if s.srv != nil {
		s.refCnt++
		s.handler.setDelegate(metricsHandler)
		return nil
	}
// 否则，创建监听
	listener, err := net.Listen("tcp", s.addr)
	s.port = listener.Addr().(*net.TCPAddr).Port
// 配置ServerMux
	srvMux := http.NewServeMux()
	s.handler = &amp;metaHandler{delegate: metricsHandler}
	srvMux.Handle(metricsPath, s.handler)
	srv := &amp;http.Server{Addr: s.addr, Handler: srvMux}
// 在后台运行
	env.ScheduleDaemon(func() {
// 开始监听
		env.Logger().Infof("serving prometheus metrics on %d", s.port)
		if err := srv.Serve(listener.(*net.TCPListener)); err != nil {
			if err == http.ErrServerClosed {
				env.Logger().Infof("HTTP server stopped")
			} else {
				_ = env.Logger().Errorf("prometheus HTTP server error: %v", err) 
			}
		}
	})
	s.srv = srv
	s.refCnt++

	return nil
} </pre>
<p>使用Istio官方Chart安装时，其内置的Prometheus服务器会自动采集该HTTP服务器暴露的指标。</p>
<div class="blog_h1"><span class="graybg">Mixs处理请求过程 </span></div>
<p>在运行期间，Envoy代理会向Mixer服务发起CHECK/REPORT/QUOTA等调用。Mixer会将这些请求转发给匹配的适配器进行处理。</p>
<p>本节以Prometheus适配器为例，说明REPORT请求的处理过程。</p>
<div class="blog_h2"><span class="graybg">相关配置</span></div>
<p>以官方Chart部署Istio时，会创建如下Rule：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: promhttp
spec:
  actions:
  - handler: handler.prometheus
    instances:
    - requestcount.metric
    - requestduration.metric
    - requestsize.metric
    - responsesize.metric
  match: context.protocol == "http" || context.protocol == "grpc"</pre>
<p>这里我们测试requestcount这个指标，和它相关的Handler、Instance配置片断如下：</p>
<pre class="crayon-plain-tag"># Handler
apiVersion: config.istio.io/v1alpha2
kind: prometheus
metadata:
  name: handler
  namespace: istio-system
spec:
  metrics:
  - instance_name: requestcount.metric.istio-system
    kind: COUNTER
    label_names:
    - reporter
    - source_app
    - source_principal
    - source_workload
    - source_workload_namespace
    - source_version
    - destination_app
    - destination_principal
    - destination_workload
    - destination_workload_namespace
    - destination_version
    - destination_service
    - destination_service_name
    - destination_service_namespace
    - request_protocol
    - response_code
    - connection_security_policy
    name: requests_total

# Instance
kind: metric
metadata:
  name: requestcount
spec:
  dimensions:
    connection_security_policy: conditional((context.reporter.kind | "inbound") == "outbound", "unknown", conditional(connection.mtls | false, "mutual_tls", "none"))
    destination_app: destination.labels["app"] | "unknown"
    destination_principal: destination.principal | "unknown"
    destination_service: destination.service.host | "unknown"
    destination_service_name: destination.service.name | "unknown"
    destination_service_namespace: destination.service.namespace | "unknown"
    destination_version: destination.labels["version"] | "unknown"
    destination_workload: destination.workload.name | "unknown"
    destination_workload_namespace: destination.workload.namespace | "unknown"
    reporter: conditional((context.reporter.kind | "inbound") == "outbound", "source", "destination")
    request_protocol: api.protocol | context.protocol | "unknown"
    response_code: response.code | 200
    source_app: source.labels["app"] | "unknown"
    source_principal: source.principal | "unknown"
    source_version: source.labels["version"] | "unknown"
    source_workload: source.workload.name | "unknown"
    source_workload_namespace: source.workload.namespace | "unknown"
  monitored_resource_type: '"UNSPECIFIED"'
  value: "1"</pre>
<div class="blog_h2"><span class="graybg">发送请求</span></div>
<p>要触发Mixer服务器端的处理逻辑，不需要运行Envoy代理，调用命令行客户端mixc就可以了。  </p>
<p>为了匹配上面的promhttp规则，我们需要发送一个属性context.protocol的值为http的REPORT请求：</p>
<pre class="crayon-plain-tag">mixc report -m localhost:9091 \
    -t request.time=2019-03-27T11:00:00.000Z,response.time=2019-03-27T11:00:00.900Z  \
    -a context.protocol=http,context.reporter.kind=outbound,source.namespace=default  \
    -a destination.service=kubernetes

# 2019-03-27T03:52:05.237085Z     info    parsed scheme: ""
# 2019-03-27T03:52:05.237179Z     info    scheme "" not registered, fallback to default scheme
# 2019-03-27T03:52:05.237532Z     info    ccResolverWrapper: sending new addresses to cc: [{localhost:9091 0  &lt;nil&gt;}]
# 2019-03-27T03:52:05.237592Z     info    ClientConn switching balancer to "pick_first"
# 2019-03-27T03:52:05.237768Z     info    pickfirstBalancer: HandleSubConnStateChange: 0xc0001940b0, CONNECTING
# 2019-03-27T03:52:05.237788Z     info    blockingPicker: the picked transport is not ready, loop back to repick
# 2019-03-27T03:52:05.241228Z     info    pickfirstBalancer: HandleSubConnStateChange: 0xc0001940b0, READY
# Report RPC returned OK</pre>
<div class="blog_h2"><span class="graybg">拦截请求</span></div>
<div class="blog_h3"><span class="graybg">gRPC接口</span></div>
<p>Mixer处理请求的接口由以下Proto文件定义：</p>
<pre class="crayon-plain-tag">service Mixer {
  // 进行先决条件检查，或者进行配额
  rpc Check(CheckRequest) returns (CheckResponse) {}

  // 遥测报告
  rpc Report(ReportRequest) returns (ReportResponse) {}
}</pre>
<div class="blog_h3"><span class="graybg">Prometheus拦截器</span></div>
<p>通过前面章节的源码分析，我们了解到，在Mixer服务启动时，注册了OpenTracing、Prometheus的gRPC拦截器。因此首先会执行Prometheus拦截器：</p>
<pre class="crayon-plain-tag">// 自动生成的代码
func _Mixer_Report_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(ReportRequest)
	if err := dec(in); err != nil {
		return nil, err
	}
// 没有拦截器，直接调用MixerServer实现
	if interceptor == nil {
		return srv.(MixerServer).Report(ctx, in)
	}
	info := &amp;grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: "/istio.mixer.v1.Mixer/Report",
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(MixerServer).Report(ctx, req.(*ReportRequest))
	}
// 实际上是有拦截器的，调用拦截器，通过拦截器再调用MixerServer实现
	return interceptor(ctx, in, info, handler)
}</pre>
<p>来自<a href="https://github.com/grpc-ecosystem/go-grpc-prometheus">go-grpc-prometheus</a>项目的Prometheus拦截器，逻辑如下：</p>
<pre class="crayon-plain-tag">func UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// grpc_server_started_total指标
	monitor := newServerReporter(Unary, info.FullMethod)
// grpc_server_msg_received_total指标
	monitor.ReceivedMessage()
// 调用MixerServer实现
	resp, err := handler(ctx, req)
// grpc_server_handled_total指标
// grpc_server_handling_seconds指标，直方图
	monitor.Handled(grpc.Code(err))
	if err == nil {
// grpc_server_msg_sent_total
		monitor.SentMessage()
	}
	return resp, err
}</pre>
<div class="blog_h2"><span class="graybg">处理请求</span></div>
<div class="blog_h3"><span class="graybg">Report</span></div>
<p>MixerServer接口的实现定义在api.grpcServer结构中。Report方法会逐个处理每条消息，并进行：</p>
<ol>
<li>预处理：调用匹配的属性生成处理器</li>
<li>处理：调用匹配的主处理器</li>
</ol>
<p>注意，单次Mixer请求可以携带多条消息，每条消息都对应Envoy代理处理的一个实际请求。</p>
<p>Report方法的实现如下：</p>
<pre class="crayon-plain-tag">func (s *grpcServer) Report(ctx context.Context, req *mixerpb.ReportRequest) (*mixerpb.ReportResponse, error) {
// 限流逻辑，默认情况下Mixer的限流是关闭的
// req.Attributes的类型是 []v1.CompressedAttributes，每个元素表示报告的一条信息，客户端可以一次报送多条信息
// 但是对于非REPORT请求，每次只能有一条消息
	if s.throttler.Throttle(loadshedding.RequestInfo{PredictedCost: float64(len(req.Attributes))}) {
		return nil, grpc.Errorf(codes.Unavailable, "Server is currently overloaded. Please try again.")
	}

	if len(req.Attributes) == 0 {
		// 没有报告任何东西
		return reportResp, nil
	}

// Words表示消息级别的字典 —— 属性名的数组、字符串属性值
	for i := 0; i &lt; len(req.Attributes); i++ {
		if len(req.Attributes[i].Words) == 0 {
// req.DefaultWords为所有消息的默认字典。可以让请求中多个消息共享字典，进而减少请求大小
			req.Attributes[i].Words = req.DefaultWords
		}
	}

	// bag around the input proto that keeps track of reference attributes
// 创建一个ProtoBag —— 基于属性Proto消息，实现Bag接口（用于访问属性集）
	protoBag := attribute.NewProtoBag(&amp;req.Attributes[0], s.globalDict, s.globalWordList)

// 从对象池中取得一个MutableBag，对象池避免了反复的内存分配，然后将其parent设置为protoBag
// accumBag（请求包requestBag），跟踪除了第一个以外，所有消息相对于第一个的delta
	accumBag := attribute.GetMutableBag(protoBag)

// reportBag（响应包responseBag），持有预处理之后的输出状态，预处理适配器可能会生成一些新属性，这些新属性以delta的形式存储在此
	reportBag := attribute.GetMutableBag(accumBag)

// 基于GlobalTracer，启动并返回操作名称（operationName）为Report的Span，使用从ctx中找到的Span作为ChildOfRef
// 如果找不到作为parent的Span，则创建一个根Span
	reportSpan, reportCtx := opentracing.StartSpanFromContext(ctx, "Report")
// 从对象池中获得reporter，为其提供路由上下文（report.rc）、报告上下文（r.ctx，其中包含了Trace树的信息）
	reporter := s.dispatcher.GetReporter(reportCtx)

	var errors *multierror.Error
/* 开始逐个处理消息 */
	for i := 0; i &lt; len(req.Attributes); i++ {
// 以Report为父Span，依次创建子Span： attribute bag N
		span, newctx := opentracing.StartSpanFromContext(reportCtx, fmt.Sprintf("attribute bag %d", i))

// 第一个属性块（消息）作为protoBag的基础，计算每个子包的delta
		if i &gt; 0 {
			if err := accumBag.UpdateBagFromProto(&amp;req.Attributes[i], s.globalWordList); err != nil {
				err = fmt.Errorf("request could not be processed due to invalid attributes: %v", err)
// 为子Span记录字段，然后结束Span
				span.LogFields(otlog.String("error", err.Error()))
				span.Finish()
				errors = multierror.Append(errors, err)
				break
			}
		}

		lg.Debug("Dispatching Preprocess")
// 预处理，将请求包分发给那些需要提前执行的适配器，例如属性生成适配器
		if err := s.dispatcher.Preprocess(newctx, accumBag, reportBag); err != nil {
			err = fmt.Errorf("preprocessing attributes failed: %v", err)
			span.LogFields(otlog.String("error", err.Error()))
			span.Finish()
			errors = multierror.Append(errors, err)
			continue
		}

// 主处理，分发给主适配器
		lg.Debug("Dispatching to main adapters after running preprocessors")
		lg.Debuga("Attribute Bag: \n", reportBag)
		lg.Debugf("Dispatching Report %d out of %d", i+1, len(req.Attributes))

		if err := reporter.Report(reportBag); err != nil {
			span.LogFields(otlog.String("error", err.Error()))
			span.Finish()
			errors = multierror.Append(errors, err)
			continue
		}

		span.Finish()

		// 清空包内容，准备处理下一个请求包使用
		reportBag.Reset()
	}
/* 结束逐个处理消息 */

// 重置，并放回对象池
	reportBag.Done()
	accumBag.Done()
	protoBag.Done()

// 刷出，调用reporter.impl.getSession.dispatchBufferedReports()，将之前缓冲的dispatchState全部分发出去
// 然后将会话放回对象池
	if err := reporter.Flush(); err != nil {
		errors = multierror.Append(errors, err)
	}
// 将Reporter对象也放回池中
	reporter.Done()

// 结束Span
	if errors != nil {
		reportSpan.LogFields(otlog.String("error", errors.Error()))
	}
	reportSpan.Finish()

	if errors != nil {
		lg.Errora("Report failed:", errors.Error())
		return nil, grpc.Errorf(codes.Unknown, errors.Error())
	}
// 返回响应
	return reportResp, nil
}</pre>
<div class="blog_h3"><span class="graybg">Preprocess </span></div>
<p>Dispatcher.Preprocess方法负责请求预处理，将请求包分发给那些需要提前执行的适配器，并收集它们产生的属性：</p>
<pre class="crayon-plain-tag">func (d *Impl) Preprocess(ctx context.Context, bag attribute.Bag, responseBag *attribute.MutableBag) error {
// 返回一个session，此结构表示对Dispatcher接口（的实现Impl）的一个调用会话
// 其中包含了处理调用所需的所有可变状态
// getSession从对象池获取一个session对象，然后设置它的
// s.impl，Dispatcher对象
// s.rc，路由上下文对象
// s.ctx 包含Span信息的上下文
// s.variety 需要调用的适配器的种类
// s.bag 请求包
	s := d.getSession(ctx, tpb.TEMPLATE_VARIETY_ATTRIBUTE_GENERATOR, bag)
// s.responseBag 响应包
	s.responseBag = responseBag
// 执行分发
	err := s.dispatch()
	if err == nil {
		err = s.err
	}
// 放回对象池
	d.putSession(s)
	return err
}</pre>
<div class="blog_h3"><span class="graybg">dispatch </span></div>
<p>session.dispatch方法真正负责请求包的分发工作：</p>
<pre class="crayon-plain-tag">func (s *session) dispatch() error {
// 根据报告者类型（从context.reporter.kind获取），默认inbound推断命名空间
// inbound 则命名空间为destination.namespace
// outbound 则命名空间为source.namespace
	namespace, err := getIdentityNamespace(s.bag)
	if err != nil {
// 无法获取命名空间，出错
// 更新直方图（Observe一个值）：
// mixer_dispatcher_destinations_per_request
// mixer_dispatcher_instances_per_request
		updateRequestCounters(0, 0)
		log.Warnf("unable to determine identity namespace: '%v', operation='%d'", err, s.variety)
		return err
	}
// 从路由表获得s.variety类型的、namespace命名空间的目的地列表
// 注意：如果当前命名空间没有匹配的目的地，则使用默认配置存储命名空间（istio-system）中定义的目的地
	destinations := s.rc.Routes.GetDestinations(s.variety, namespace)

// 要访问的目标服务 
	destinationService := ""
	v, ok := s.bag.Get("destination.service")
	if ok {
		destinationService = v.(string)
	}
// 创建一个新的Context，携带键值对，以前面的子Span上下文为父，0=adapter.RequestData为键值对
// RequestData定义了关于请求的信息，例如它的目的服务
	ctx := adapter.NewContextWithRequestData(s.ctx, &amp;adapter.RequestData{
		DestinationService: adapter.Service{
			FullName: destinationService,
		},
	})

// 确保能够将请求并行的分发给所有处理器，将s.completed设置为足够大的chan *dispatchState
// 每个chan *dispatchState收集单个目的地的处理结果
	s.ensureParallelism(destinations.Count())

	foundQuota := false
// 构建出的实例数量
	ninputs := 0
// 匹配的目的地数量
	ndestinations := 0
	for _, destination := range destinations.Entries() {
// dispatchState持有和单个目的地相关的输入/输出状态
		var state *dispatchState

// 对于REPORT处理器
		if s.variety == tpb.TEMPLATE_VARIETY_REPORT {
// 生成并缓存分发状态到s.reportStates
			state = s.reportStates[destination]
			if state == nil {
// 从对象池中获取一个dispatchState并对其赋值，对象池在Mixer中大量使用，减少了内存分配
				state = s.impl.getDispatchState(ctx, destination)
				s.reportStates[destination] = state
			}
		}

		for _, group := range destination.InstanceGroups {
// 判断请求包是否和每个实例组匹配
			groupMatched := group.Matches(s.bag)

			if groupMatched {
				ndestinations++
			}

// 遍历每个组中的每个实例，调用其构建器。构建器的逻辑取决于你配置的各种模板实例，例如metric的CR
			for j, input := range group.Builders {
				if s.variety == tpb.TEMPLATE_VARIETY_QUOTA {
// 对于配额适配器，必须要求实例构建器名称和实例名一致
// CRD名称即模板信息名TemplateInfo.Name，例如        logentries
// 实例名，即CR名，例如 kubectl -n istio-system get logentries.config.istio.io
// 得到的accesslog、tcpaccesslog                  
					if !strings.EqualFold(input.InstanceShortName, s.quotaArgs.Quota) {
						continue
					}
					if !groupMatched {
						// 这是一个条件性的配额，并且当前不匹配条件，直接返回请求的额度
						s.quotaResult.Amount = s.quotaArgs.Amount
						s.quotaResult.ValidDuration = defaultValidDuration
					}
					foundQuota = true
				}

				if !groupMatched {
					continue
				}

				var instance interface{}
// 从请求包构建出实例，Builder方法是自动生成的
				if instance, err = input.Builder(s.bag); err != nil {
					log.Errorf("error creating instance: destination='%v', error='%v'", destination.FriendlyName, err)
					s.err = multierror.Append(s.err, err)
					continue
				}
				ninputs++
// 对于REPORT模板，在执行分发前，尽可能的将实例累积到分发状态的instances列表中
				if s.variety == tpb.TEMPLATE_VARIETY_REPORT {
					state.instances = append(state.instances, instance)
					continue
				}

// 对于其它模板类型，直接分发给处理器
				state = s.impl.getDispatchState(ctx, destination)
				state.instances = append(state.instances, instance)
				if s.variety == tpb.TEMPLATE_VARIETY_ATTRIBUTE_GENERATOR {
// 属性生成处理器需要处理Mapper —— 将处理器输出映射入主属性集的函数
					state.mapper = group.Mappers[j]
					state.inputBag = s.bag
				}

// 配额模板相关参数
				state.quotaArgs.BestEffort = s.quotaArgs.BestEffort
				state.quotaArgs.DeduplicationID = s.quotaArgs.DeduplicationID
				state.quotaArgs.QuotaAmount = s.quotaArgs.Amount
// 直接分发
				s.dispatchToHandler(state)
			}
		}
	}

// Observe mixer_dispatcher_destinations_per_request
// Observe mixer_dispatcher_instances_per_request
	updateRequestCounters(ndestinations, ninputs)

// 等待所有处理器处理完毕
	s.waitForDispatched()

// 如果当前执行的是配额处理器，且没有找到配额，则警告但是允许访问
	if s.variety == tpb.TEMPLATE_VARIETY_QUOTA &amp;&amp; !foundQuota {
		s.quotaResult.Amount = s.quotaArgs.Amount
		s.quotaResult.ValidDuration = defaultValidDuration
		log.Warnf("Requested quota '%s' is not configured", s.quotaArgs.Quota)
	}

	return nil
}</pre>
<p>需要注意： </p>
<ol>
<li>对于REPORT模板，仅仅是将生成的Instance存放到dispatchState.instances数组中，不分发。延迟到所有请求消息处理完毕后，由Reporter.Flush统一分发</li>
<li>对于CHECK模板，直接调用session.dispatchToHandler进行分发</li>
</ol>
<p>分发不是直接在当前线程调用适配器，而是排队，由协程池的调度循环异步处理：</p>
<pre class="crayon-plain-tag">func (s *session) dispatchToHandler(ds *dispatchState) {
	s.activeDispatches++
	ds.session = s
// 调用协程池，调度一个工作
	s.impl.gp.ScheduleWork(ds.invokeHandler, nil)
}</pre>
<p>dispatchState.invokeHandler方法真正直接调用适配器：</p>
<pre class="crayon-plain-tag">func (ds *dispatchState) invokeHandler(interface{}) {
// 顺利处理完毕，没有Panic
	reachedEnd := false

	defer func() {
		if reachedEnd {
			return
		}
// 从适配器代码导致的Panic中恢复，防止Mixer直接崩了
		r := recover()
		ds.err = fmt.Errorf("panic during handler dispatch: %v", r)
		log.Errorf("%v\n%s", ds.err, debug.Stack())

		if log.DebugEnabled() {
			log.Debugf("stack dump for handler dispatch panic:\n%s", debug.Stack())
		}
// 提示此此目的地的分发处理完毕
		ds.session.completed &lt;- ds
	}()

// 跟踪
	span, ctx, start := ds.beginSpan(ds.ctx)

	log.Debugf("begin dispatch: destination='%s'", ds.destination.FriendlyName)

	switch ds.destination.Template.Variety {
// 属性生成器
	case tpb.TEMPLATE_VARIETY_ATTRIBUTE_GENERATOR:
		ds.outputBag, ds.err = ds.destination.Template.DispatchGenAttrs(
			ctx, ds.destination.Handler, ds.instances[0], ds.inputBag, ds.mapper)
// 前置条件检查
	case tpb.TEMPLATE_VARIETY_CHECK:
		ds.checkResult, ds.err = ds.destination.Template.DispatchCheck(
			ctx, ds.destination.Handler, ds.instances[0])
// 遥测/报告
	case tpb.TEMPLATE_VARIETY_REPORT:
		ds.err = ds.destination.Template.DispatchReport(
			ctx, ds.destination.Handler, ds.instances)
// 配额
	case tpb.TEMPLATE_VARIETY_QUOTA:
		ds.quotaResult, ds.err = ds.destination.Template.DispatchQuota(
			ctx, ds.destination.Handler, ds.instances[0], ds.quotaArgs)
// 无法处理的模板类型，Panic
	default:
		panic(fmt.Sprintf("unknown variety type: '%v'", ds.destination.Template.Variety))
	}

	log.Debugf("complete dispatch: destination='%s' {err:%v}", ds.destination.FriendlyName, ds.err)
// 追踪
	ds.completeSpan(span, time.Since(start), ds.err)
// 将当前目的地设置为分发处理完毕
	ds.session.completed &lt;- ds

	reachedEnd = true
}</pre>
<p>可以看到，上述方法都是把调用委托给目的地的TemplateInfo.Dispatch***函数指针处理的。这些函数指针就是适配器的相应方法。对于Metric模板，Prometheus适配器的方法实现如下：</p>
<pre class="crayon-plain-tag">func (h *handler) HandleMetric(_ context.Context, vals []*metric.Instance) error {
	var result *multierror.Error

// 遍历Instance
	for _, val := range vals {
// 获取该Instance对应的handler（例如requestcount.metric.istio-system）的信息（cinfo）
		ci := h.metrics[val.Name]
		if ci == nil {
			result = multierror.Append(result, fmt.Errorf("could not find metric info from adapter config for %s", val.Name))
			continue
		}
		collector := ci.c
		switch ci.kind {
// 按指标类型分别处理
		case config.GAUGE:
			vec := collector.(*prometheus.GaugeVec)
			amt, err := promValue(val.Value)
			if err != nil {
				result = multierror.Append(result, fmt.Errorf("could not get value for metric %s: %v", val.Name, err))
				continue
			}
			pl := promLabels(val.Dimensions)
			if h.labelsCache != nil {
				h.labelsCache.Set(key(val.Name, "gauge", pl, ci.sortedLabels), &amp;cacheEntry{vec, pl})
			}
			vec.With(pl).Set(amt)
		case config.COUNTER:
// 转换为指标向量，指标向量的每个元素是具有不同标签值的同一类（名字相同）指标
			vec := collector.(*prometheus.CounterVec)
			amt, err := promValue(val.Value)
			if err != nil {
				result = multierror.Append(result, fmt.Errorf("could not get value for metric %s: %v", val.Name, err))
				continue
			}
			pl := promLabels(val.Dimensions)
			if h.labelsCache != nil {
				h.labelsCache.Set(key(val.Name, "counter", pl, ci.sortedLabels), &amp;cacheEntry{vec, pl})
			}
// vec.With(pl)返回具有指定标签集的指标对象，这里是Counter，然后加上一个值（在当前时间点）
			vec.With(pl).Add(amt)
		case config.DISTRIBUTION:
// DISTRIBUTION映射为Prometheus类型 Histogram
			vec := collector.(*prometheus.HistogramVec)
			amt, err := promValue(val.Value)
			if err != nil {
				result = multierror.Append(result, fmt.Errorf("could not get value for metric %s: %v", val.Name, err))
				continue
			}
			pl := promLabels(val.Dimensions)
			if h.labelsCache != nil {
				h.labelsCache.Set(key(val.Name, "distribution", pl, ci.sortedLabels), &amp;cacheEntry{vec, pl})
			}
			vec.With(pl).Observe(amt)
		}
	}

	return result.ErrorOrNil()
}


cinfo struct {
// 负责收集指标的接口，gauge counter等都实现了此接口
	c            prometheus.Collector
	sha          [sha1.Size]byte
	kind         config.Params_MetricInfo_Kind
	sortedLabels []string
}</pre>
<div class="blog_h2"><span class="graybg">主要接口</span></div>
<div class="blog_h3"><span class="graybg">Dispatcher</span></div>
<p>将入站的API调用分发给配置的适配器：</p>
<pre class="crayon-plain-tag">type Dispatcher interface {
	// 进行预处理，将请求包分发给那些需要提前执行的适配器，
	// 目前这种适配器主要指属性生成适配器
	Preprocess(ctx context.Context, requestBag attribute.Bag, responseBag *attribute.MutableBag) error

	// 进行CHECK分发，基于CHECK类型模板的Instance，将被转发给感兴趣的适配器
	Check(ctx context.Context, requestBag attribute.Bag) (adapter.CheckResult, error)

	// 获取能够缓冲REPORT请求的报告器
	GetReporter(ctx context.Context) Reporter

	// 进行QUOTA分发
	Quota(ctx context.Context, requestBag attribute.Bag, qma QuotaMethodArgs) (adapter.QuotaResult, error)
}</pre>
<div class="blog_h3"><span class="graybg">Reporter</span></div>
<p>负责产生一系列的报告：</p>
<pre class="crayon-plain-tag">type Reporter interface {
	// 添加一个条目（请求包）到报告状态中
	Report(requestBag attribute.Bag) error

	// 刷出所有缓冲的状态到适当的适配器
	Flush() error

	// 完成Reporter的处理过程
	Done()
}</pre>
<div class="blog_h2"><span class="graybg">主要结构</span></div>
<div class="blog_h3"><span class="graybg">Destination</span></div>
<p>目的地，包含一个目标处理器，以及需要（在满足条件的情况下）发送给它的实例：</p>
<pre class="crayon-plain-tag">type Destination struct {
	// 用于调试的目的地ID
	id uint32

	// 需要调用的处理器
	Handler adapter.Handler

	// 用于监控/日志目的的处理器名称
	HandlerName string

	// 用于监控/日志目的的适配器名称（处理器类型）
	AdapterName string

	// 使用的模板，由于某些适配器支持多种模板，这些适配器可能对应多个Destination
	// 每种模板都有类型，并且定义了支持它的适配器必须实现的接口
	Template *TemplateInfo

	// 实例组，每组实例在满足条件的情况下，会发送给处理器
	InstanceGroups []*InstanceGroup

	// 最大允许的实例数
	maxInstances int

	// 用于监控/日志目的目的地名称
	FriendlyName string

	// 性能计数器
	Counters DestinationCounters
}</pre>
<div class="blog_h3"><span class="graybg">dispatchState</span></div>
<p>此结构用于收集<span style="background-color: #c0c0c0;">单个目的地（适配器+模板组合）</span>的处理状态和结果：</p>
<pre class="crayon-plain-tag">type dispatchState struct {
// 所属的分发调用会话
	session *session
// 上下文，其中包含了OpenTracing的Span信息
	ctx     context.Context
// 目的地
	destination *routing.Destination
// 对于属性生成模板，将模板输出映射入主属性列表的函数
	mapper      template.OutputMapperFn
// 输入包
	inputBag  attribute.Bag
// 配额请求的参数
	quotaArgs adapter.QuotaArgs
// 构建出的，供适配器消费的实例列表
	instances []interface{}

// 处理过程中的错误信息
	err         error
// 输出包
	outputBag   *attribute.MutableBag
// CHECK调用的结果
	checkResult adapter.CheckResult
// QUOTA调用的结果
	quotaResult adapter.QuotaResult
}</pre>
<div class="blog_h3"><span class="graybg">session</span></div>
<p>对一个客户端CHECK/REPORT/QUOTA请求的预处理和主处理的过程，是一个会话。此结构存储相关的信息：</p>
<pre class="crayon-plain-tag">type session struct {
// 拥有此会话的Dispatcher
	impl *Impl

// 本次会话使用的路由上下文
	rc *RoutingContext

// 上下文信息
	ctx          context.Context
// 输入包
	bag          attribute.Bag
// 配额调用的参数
	quotaArgs    QuotaMethodArgs
// 输出包
	responseBag  *attribute.MutableBag
// 报告请求的分发状态
	reportStates map[*routing.Destination]*dispatchState

// CHECK/QUOTA调用的结果
	checkResult adapter.CheckResult
	quotaResult adapter.QuotaResult
	err         error

// 正在执行的分发操作数量
	activeDispatches int

// 收集已完成的分发
	completed chan *dispatchState

// 本次操作的模板类别
	variety tpb.TemplateVariety
}</pre>
<div class="blog_h3"><span class="graybg">TemplateInfo</span></div>
<p>和模板有关的信息：</p>
<pre class="crayon-plain-tag">type TemplateInfo struct {
// 模板名称
	Name             string
// 模板种类
	Variety          tpb.TemplateVariety
// 各种Mixer调用的函数指针
	DispatchReport   template.DispatchReportFn
	DispatchCheck    template.DispatchCheckFn
	DispatchQuota    template.DispatchQuotaFn
	DispatchGenAttrs template.DispatchGenerateAttributesFn
}</pre>
<div class="blog_h3"><span class="graybg">InstanceGroup </span></div>
<p>按照匹配条件分组的、需要发送给适配器的实例的信息：</p>
<pre class="crayon-plain-tag">type InstanceGroup struct {
	// 用于调试的ID
	id uint32

	// 预编译的表达式，何时应用此实例组
	Condition compiled.Expression

	// 用于构建出实例的函数+名称
	Builders []NamedBuilder

	// 映射器函数，用于将属性生成适配器的输出属性，映射入主属性集
	Mappers []template.OutputMapperFn
}

type NamedBuilder struct {
	InstanceShortName string
	Builder           template.InstanceBuilderFn
}

OutputMapperFn func(attrs attribute.Bag) (*attribute.MutableBag, error)</pre>
<div class="blog_h3"><span class="graybg">QuotaMethodArgs</span></div>
<p>进行配额请求时，需要的参数 + 配额（资源）的类型：</p>
<pre class="crayon-plain-tag">type QuotaMethodArgs struct {
	// 在出现RPC调用并重试时，用于额度分配/释放（Quota allocation/allocation）调用的去重复
	DeduplicationID string

	// 分配那种配额
	Quota string

	// 分配的量
	Amount int64

	// 如果设置为true，则允许响应返回比请求少的额度。如果设置为false，那么额度不足时，直接返回0
	BestEffort bool
}</pre>
<div class="blog_h3"><span class="graybg">QuotaArgs</span></div>
<p>进行配额请求时，需要的参数：</p>
<pre class="crayon-plain-tag">QuotaArgs struct {
	DeduplicationID string
	QuotaAmount int64
	BestEffort bool
}</pre>
<div class="blog_h3"><span class="graybg">QuotaResult</span></div>
<p>由处理器提供的，额度分配的结果： </p>
<pre class="crayon-plain-tag">QuotaResult struct {
	// RPC调用的状态（状态码、消息、详情）
	Status rpc.Status
	// 分配的额度何时过期，0表示永不过期
	ValidDuration time.Duration
	// 分配的额度，可能比请求的额度小
	Amount int64
}</pre>
<div class="blog_h1"><span class="graybg">Envoy代理请求过程</span></div>
<p>在探索Envoy如何向Mixer发送请求之前，我们先来分析一下Envoy作为网络代理，是如何工作的。</p>
<div class="blog_h2"><span class="graybg">整体过程</span></div>
<div class="blog_h3"><span class="graybg">启动监听</span></div>
<ol>
<li>通过xDS或者静态配置，获得Envoy代理的监听器信息</li>
<li>如果监听器bind_to_port，则直接调用libevent的接口，绑定监听，回调函数设置为ListenerImpl::listenCallback</li>
</ol>
<div class="blog_h3"><span class="graybg">连接接受</span></div>
<ol>
<li>DispatcherImpl通过libevent，接收到请求，调用ListenerImpl::listenCallback</li>
<li>根据入站时的目的端口，选择适当的监听器处理请求，调用onAccept。存在Iptables重定向的情况下，监听器为15001
<ol>
<li>构建出监听器过滤器链</li>
<li>执行过滤器链，对于15001来说，此链只有OriginalDstFilter一个过滤器</li>
<li>OriginalDstFilter恢复原始目的地址</li>
<li>查找和原始目的地址匹配的监听器，并<span style="background-color: #c0c0c0;">转交请求</span></li>
</ol>
</li>
<li>如果发生请求转交，则接受者监听器也会执行类似于2的逻辑。但是<span style="background-color: #c0c0c0;">不会再次发生转交</span></li>
<li>实际负责连接的那个监听器，会调用ActiveListener.newConnection，并间接的创建ConnectionImpl</li>
<li>ConnectionImpl会利用<span style="background-color: #c0c0c0;">连接套接字（ConnectionSocketPtr）的文件描述符，调用Dispatcher.createFileEvent，注册读写事件的回调</span></li>
<li>到此，连接接受完毕，后续的读写事件由libevent异步触发</li>
</ol>
<div class="blog_h3"><span class="graybg">数据读写</span></div>
<ol>
<li>发生可读、可写、关闭事件时，ConnectionImpl::onFileEvent被调用</li>
<li>可写事件的回调onWriteReady先调用</li>
<li>可读事件的回调onReadReady后调用
<ol>
<li>尝试循环读取，根据读取结果设置Post操作</li>
<li>处理读取到的数据
<ol>
<li>遍历网络过滤器链
<ol>
<li>如果是L7连接，则执行HTTP网络管理器</li>
</ol>
</li>
</ol>
</li>
<li>执行Post IO操作</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">L4核心接口</span></div>
<div class="blog_h3"><span class="graybg">LinkedObject</span></div>
<p>这个混入类为任意的unique_ptr所持有的对象增加行为，允许方便的将这种对象link/unlink到列表中：</p>
<pre class="crayon-plain-tag">template &lt;class T&gt; class LinkedObject {
public:
  // 对象唯一性指针的列表
  typedef std::list&lt;std::unique_ptr&lt;T&gt;&gt; ListType;

  // 返回列表的迭代器
  typename ListType::iterator entry();

  // 对象当前是否被插入到列表，只要调用过moveInto***方法就返回true
  bool inserted();

  // 在两个列表之间移动对象
  void moveBetweenLists(ListType&amp; list1, ListType&amp; list2);

  // 移动对象到列表，放在最前面，注意所有权的转移
  void moveIntoList(std::unique_ptr&lt;T&gt;&amp;&amp; item, ListType&amp; list);

  // 移动对象到列表，放在最后面
  void moveIntoListBack(std::unique_ptr&lt;T&gt;&amp;&amp; item, ListType&amp; list);

  // 从列表中移除条目
  std::unique_ptr&lt;T&gt; removeFromList(ListType&amp; list);
};</pre>
<div class="blog_h3"><span class="graybg">DeferredDeletable</span></div>
<p>标记性接口。任何实现此接口的对象，都可以传递给Dispatcher。Dispatcher确保，未来在事件循环中删除对象。</p>
<pre class="crayon-plain-tag">class DeferredDeletable {
public:
  virtual ~DeferredDeletable() {}
};</pre>
<p>使用此接口，进行事件处理时，不需要担心栈unwind的问题</p>
<div class="blog_h3"><span class="graybg">ConnectionHandler</span></div>
<p>抽象的连接处理器，总体负责网络连接的处理。<span style="background-color: #c0c0c0;">ActiveListener、ActiveSocket的_parent都指向此对象</span>。</p>
<pre class="crayon-plain-tag">class ConnectionHandler {
public:

  // 此处理器持有的活动连接数
  virtual uint64_t numConnections() PURE;

  // 添加一个监听器到此处理器
  virtual void addListener(ListenerConfig&amp; config) PURE;

  // 根据地址查找监听器。返回监听器的指针，所有权不转移
  virtual Network::Listener* findListenerByAddress(const Network::Address::Instance&amp; address) PURE;

  // 移除使用指定tag作为键的监听器。监听器拥有的所有连接也会被移除
  virtual void removeListeners(uint64_t listener_tag) PURE;

  // 停止使用指定tag作为键的监听器。监听器拥有的所有连接不会被关闭，此方法用于draining
  virtual void stopListeners(uint64_t listener_tag) PURE;

  // 停止所有监听器
  virtual void stopListeners() PURE;

  // 禁用所有监听器。不会关闭监听器拥有的连接鹅，用于临时暂停接受连接
  virtual void disableListeners() PURE;

  // 启用所有监听器
  virtual void enableListeners() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">Listener</span></div>
<p>套接字监听器的抽象接口，是否此对象则停止对套接字的监听：</p>
<pre class="crayon-plain-tag">class Listener {
public:
  // 临时禁止接收新连接
  virtual void disable() PURE;

  // 继续接收新连接
  virtual void enable() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ActiveListener</span></div>
<p>表示某个连接处理器ConnectionHandler所拥有的活动的监听器， ActiveListener引用一个Listener。</p>
<div class="blog_h3"><span class="graybg">ListenerFilter</span></div>
<pre class="crayon-plain-tag">class ListenerFilter {
public:
  /**
   * 在新的连接被接受，但是Connection对象尚未创建之前调用
   * @param cb 此回调提供一些重要方法
   * @return 过滤器管理器根据此返回状态，决定是否继续迭代过滤器链
   */
  virtual FilterStatus onAccept(ListenerFilterCallbacks&amp; cb) PURE;
};</pre>
<p>通过参数cb，可以continueFilterChain。</p>
<div class="blog_h3"><span class="graybg">ListenerFilterManager</span></div>
<p>此接口用于管理监听器过滤器链：</p>
<pre class="crayon-plain-tag">class ListenerFilterManager {
public:
  // 为指定的监听器添加过滤器
  virtual void addAcceptFilter(ListenerFilterPtr&amp;&amp; filter) PURE;
};</pre>
<p>ActiveSocket实现了ListenerFilterManager、ListenerFilterCallbacks。</p>
<div class="blog_h3"><span class="graybg">ListenerFilterCallbacks</span></div>
<p>此接口供监听器过滤器使用，后者通过它和监听器管理器通信：</p>
<pre class="crayon-plain-tag">class ListenerFilterCallbacks {
public:
  /**
   * @return ConnectionSocket 过滤器所操作的连接套接字
   */
  virtual ConnectionSocket&amp; socket() PURE;

  /**
   * @return 分发事件的Dispatcher
   */
  virtual Event::Dispatcher&amp; dispatcher() PURE;

  /**
   * 继续执行过滤器链
   */
  virtual void continueFilterChain(bool success) PURE;
};TransportSocket</pre>
<p>ActiveSocket实现了ListenerFilterManager、ListenerFilterCallbacks。 </p>
<div class="blog_h3"><span class="graybg">ListenerCallbacks</span></div>
<p>此接口供监听器使用：</p>
<pre class="crayon-plain-tag">class ListenerCallbacks {
public:
  /**
   * 当新连接被接受后，回调此方法
   * @param socket 移动到被调用者的套接字
   * @param redirected 提示套接字已经经过重定向
   */
  virtual void onAccept(ConnectionSocketPtr&amp;&amp; socket, bool hand_off_restored_destination_connections = true) PURE;

  /**
   * 当新连接被接受后，回调此方法
   * @param new_connection 移动到被调用者的套接字
   */
  virtual void onNewConnection(ConnectionPtr&amp;&amp; new_connection) PURE;
};</pre>
<p>ActiveListener实现了此接口。</p>
<div class="blog_h3"><span class="graybg">FilterChainManager</span></div>
<p>此接口用于管理过滤器链：</p>
<pre class="crayon-plain-tag">class FilterChainManager {
public:
  /**
   * 查找匹配新连接的元数据的过滤器链
   * @param socket 提供元数据
   * @return const FilterChain* 使用的过滤器链，如果没有匹配返回nullptr
   */
  virtual const FilterChain* findFilterChain(const ConnectionSocket&amp; socket) const PURE;
};</pre>
<p>ListenerImpl实现了此接口。</p>
<div class="blog_h3"><span class="graybg">FilterManager</span></div>
<p>负责添加网络过滤器给过滤器管理器，也就是Connection：</p>
<pre class="crayon-plain-tag">class FilterManager {
public:
  virtual ~FilterManager() {}

  // 添加一个写过滤器，过滤器以FIFO顺序调用
  virtual void addWriteFilter(WriteFilterSharedPtr filter) PURE;

  // 添加读写过滤器，相当于同时调用addWriteFilter/addReadFilter
  virtual void addFilter(FilterSharedPtr filter) PURE;

  // 添加一个读过滤器，过滤器以FIFO顺序调用
  virtual void addReadFilter(ReadFilterSharedPtr filter) PURE;

  // 实例化所有安装的读过滤器，相当于针对每个过滤器调用onNewConnection()
  virtual bool initializeReadFilters() PURE;
}</pre>
<div class="blog_h3"><span class="graybg">FilterChain</span></div>
<p>单个过滤器链的接口：</p>
<pre class="crayon-plain-tag">class FilterChain {
public:
  // 基于此过滤器链的新连接，使用的TransportSocketFactory，不同链使用的工厂可能不同（传输协议不同，RAW，TLS...）
  virtual const TransportSocketFactory&amp; transportSocketFactory() const PURE;
  // 基于此过滤器链的新连接，为了创建所有过滤器需要的工厂的集合
  virtual const std::vector&lt;FilterFactoryCb&gt;&amp; networkFilterFactories() const PURE;
}; </pre>
<div class="blog_h3"><span class="graybg">Filter</span></div>
<pre class="crayon-plain-tag">class Filter : public WriteFilter, public ReadFilter {};</pre>
<div class="blog_h3"><span class="graybg">ReadFilter</span></div>
<p>读处理路径（处理下游发来的数据）上的二进制（4层）过滤器：</p>
<pre class="crayon-plain-tag">class ReadFilter {
public:

  /**
   * 当连接上的数据被读取时调用
   * @param data 读取到的，可能已经被修改过的数据
   * @param end_stream 当连接启用半关闭语义时，用于提示是否到了最后一字节
   * @return status 过滤器管理器使用此状态决定如何进一步迭代其它过滤器
   */
  virtual FilterStatus onData(Buffer::Instance&amp; data, bool end_stream) PURE;

  /**
   * 当新连接刚创建时调用，过滤器链的迭代可以被中止
   * @return status 过滤器管理器使用此状态决定如何进一步迭代其它过滤器
   */
  virtual FilterStatus onNewConnection() PURE;

  /**
   * 初始化用于和过滤器管理器交互的读过滤器回调，过滤器被注册时，将被过滤器管理器调用一次
   * 任何需要用到底层连接的构造，需要在此函数的回调中执行
   *
   * IMPORTANT: 出站、复杂逻辑不要在此，放在onNewConnection()
   *
   */
  virtual void initializeReadFilterCallbacks(ReadFilterCallbacks&amp; callbacks) PURE;
}</pre>
<div class="blog_h3"><span class="graybg">WriteFilter</span></div>
<p>写处理路径（向下游发送数据）上的二进制（4层）过滤器： </p>
<pre class="crayon-plain-tag">class WriteFilter {
public:
  /**
   * 当在此连接上发生数据写入时调用
   * @param data 需要写入的，可能已经被修改过的数据
   * @param end_stream 当连接启用半关闭语义时，用于提示是否到了最后一字节
   */
  virtual FilterStatus onWrite(Buffer::Instance&amp; data, bool end_stream) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ConnectionSocket</span></div>
<p>连接套接字，表示传递给一个Connection的套接字：</p>
<ol>
<li>对于服务端，该对象表示已经Accept的套接字</li>
<li>对于客户端，该对象表示正在连接到远程地址的套接字</li>
</ol>
<pre class="crayon-plain-tag">class ConnectionSocket : public virtual Socket {
public:
  // 返回远程地址
  virtual const Address::InstanceConstSharedPtr&amp; remoteAddress() const PURE;
  // 用于服务器端，恢复原始目的地址
  virtual void restoreLocalAddress(const Address::InstanceConstSharedPtr&amp; local_address) PURE;
  // 设置远程地址
  virtual void setRemoteAddress(const Address::InstanceConstSharedPtr&amp; remote_address) PURE;
  // 原始目的地址是否被恢复
  virtual bool localAddressRestored() const PURE;
  // 设置传输协议，例如RAW_BUFFER, TLS
  virtual void setDetectedTransportProtocol(absl::string_view protocol) PURE;
  // 返回传输协议
  virtual absl::string_view detectedTransportProtocol() const PURE;
  // 设置请求的应用协议，例如ALPN in TLS
  virtual void setRequestedApplicationProtocols(const std::vector&lt;absl::string_view&gt;&amp; protocol) PURE;
  // 返回请求的应用协议
  virtual const std::vector&lt;std::string&gt;&amp; requestedApplicationProtocols() const PURE;
  // 设置请求的服务器名称
  virtual void setRequestedServerName(absl::string_view server_name) PURE;
  // 返回请求的服务器名称
  virtual absl::string_view requestedServerName() const PURE;
};</pre>
<div class="blog_h3"><span class="graybg">TransportSocket</span></div>
<p>传输套接字，负责实际的读写，也进行某些数据转换（例如TLS）：</p>
<pre class="crayon-plain-tag">class TransportSocket {
public:
  // 连接对象调用此方法依次，初始化传输套接字的回调
  virtual void setTransportSocketCallbacks(TransportSocketCallbacks&amp; callbacks) PURE;

  // 由网络级协商选择的协议
  virtual std::string protocol() const PURE;

  // 套接字是否已经被flush和close
  virtual bool canFlushClose() PURE;

  // 关闭传输套接字
  virtual void closeSocket(Network::ConnectionEvent event) PURE;

  // 读取到缓冲
  virtual IoResult doRead(Buffer::Instance&amp; buffer) PURE;

  /**
   * 将缓冲写入底层套接字
   * @param buffer 缓冲
   * @param end_stream 提示是否是流的终点，如果true则缓冲中所有数据都被写出去，连接变成半关闭
   */
  virtual IoResult doWrite(Buffer::Instance&amp; buffer, bool end_stream) PURE;

  // 底层传输建立后回调
  virtual void onConnected() PURE;

  // 如果当前是SSL连接，则返回Ssl::Connection，否则返回nullptr
  virtual const Ssl::Connection* ssl() const PURE;
};</pre>
<div class="blog_h3"><span class="graybg">TransportSocketCallbacks</span></div>
<p>传输套接字使用此回调集，和Connection通信：</p>
<pre class="crayon-plain-tag">class TransportSocketCallbacks {
public:

  // 返回关联到连接的IO句柄，从此局部可以得到连接套接字的FD
  virtual IoHandle&amp; ioHandle() PURE;
  virtual const IoHandle&amp; ioHandle() const PURE;

  // 返回关联的连接
  virtual Network::Connection&amp; connection() PURE;

  // 是否读缓冲应该被排干（drain，也就是调用过滤器链进行处理），用于强制配置的读缓冲大小限制
  virtual bool shouldDrainReadBuffer() PURE;

  // 将读缓冲标记为可（被事件循环）读
  virtual void setReadBufferReady() PURE;

  // 发起（Raise）一个连接事件到Connection对象，TLS使用此方法告知握手完成
  virtual void raiseEvent(ConnectionEvent event) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">Connection</span></div>
<p>该接口表示原始的连接，它实现了FilterManager接口：</p>
<pre class="crayon-plain-tag">class Connection : public Event::DeferredDeletable, public FilterManager {
public:
  // 状态枚举
  enum class State { Open, Closing, Closed };

  // 连接发送字节后的回调
  typedef std::function&lt;void(uint64_t bytes_sent)&gt; BytesSentCb;

  // 注册当此连接上发生事件后执行的回调
  virtual void addConnectionCallbacks(ConnectionCallbacks&amp; cb) PURE;

  // 注册每当bytes被写入底层TransportSocket后执行的回调
  virtual void addBytesSentCallback(BytesSentCb cb) PURE;

  // 为此连接启用半关闭语义，从一个已经被对端半关闭的连接上进行读操作，不会关闭连接
  virtual void enableHalfClose(bool enabled) PURE;

  // 关闭连接
  virtual void close(ConnectionCloseType type) PURE;

  // 返回分发器
  virtual Event::Dispatcher&amp; dispatcher() PURE;

  // 返回唯一性的本地连接ID
  virtual uint64_t id() const PURE;

  // 返回网络级协商选择的下一个使用的协议
  virtual std::string nextProtocol() const PURE;

  // 为连接启用/禁用NO_DELAY
  virtual void noDelay(bool enable) PURE;

  // 启禁针对此连接的套接字读。当重新启用读时，如果输入缓冲有内容，会通过过滤器链分发
  virtual void readDisable(bool disable) PURE;

  // 当禁用套接字读后，Envoy是否应当检测TCP连接关闭。默认对新连接来说，检测
  virtual void detectEarlyCloseWhenReadDisabled(bool should_detect) PURE;

  // 读操作是否启用
  virtual bool readEnabled() const PURE;

  // 返回远程地址
  virtual const Network::Address::InstanceConstSharedPtr&amp; remoteAddress() const PURE;

  // 返回本地地址，对于客户端连接来说，即原始地址；对于服务器连接来说
  // 是本地的目的地址
  // 对于服务器连接来说，此地址可能和代理的监听地址不一样，因为下游连接可能被重定向，或者代理在透明模式下运行
  virtual const Network::Address::InstanceConstSharedPtr&amp; localAddress() const PURE;

  // 更新连接状态，出于性能的考虑，最终一致
  virtual void setConnectionStats(const ConnectionStats&amp; stats) PURE;

  // 如果该连接是SSL，则返回SSL连接数据；否则返回nullptr
  virtual const Ssl::Connection* ssl() const PURE;

  // 返回服务器名称，对于TLS来说即SNI
  virtual absl::string_view requestedServerName() const PURE;

  // 返回连接状态
  virtual State state() const PURE;

  /**
   * 写入数据到连接，数据将经过过滤器链
   * @param data 需要写入的数据
   * @param end_stream 如果为true，则提示此为最后一次写操作，导致连接半关闭。必须enableHalfClose(true)才能传入true
   */
  virtual void write(Buffer::Instance&amp; data, bool end_stream) PURE;

  // 设置该连接的缓冲区的软限制
  // 对于读缓冲，限制处理流水线在flush到下一个stage前能缓冲的最大字节数
  // 对于写缓冲，设置水位。如果缓冲了足够的数据，触发onAboveWriteBufferHighWatermark调用
  virtual void setBufferLimits(uint32_t limit) PURE;

  // 获得软限制
  virtual uint32_t bufferLimit() const PURE;

  // 本地地址是否被还原为原始目的地址
  virtual bool localAddressRestored() const PURE;

  // 连接当前是否高于高水位
  virtual bool aboveHighWatermark() const PURE;

  // 获取此连接的套接字选项
  virtual const ConnectionSocket::OptionsSharedPtr&amp; socketOptions() const PURE;

  // 获取关联到此连接的StreamInfo对象。StreamInfo典型用于日志目的
  // 每个过滤器都可以通过StreamInfo.FilterState来添加特定的信息
  // 在此上下文中每个连接对应一个StreamInfo。而对于HTTP连接管理器，每个请求对应一个StreamInfo
  virtual StreamInfo::StreamInfo&amp; streamInfo() PURE;
  virtual const StreamInfo::StreamInfo&amp; streamInfo() const PURE;

  // 设置延迟连接关闭的超时
  virtual void setDelayedCloseTimeout(std::chrono::milliseconds timeout) PURE;
  virtual std::chrono::milliseconds delayedCloseTimeout() const PURE;
}</pre>
<div class="blog_h3"><span class="graybg">ConnectionCallbacks</span></div>
<p>L4连接上发生的事件的回调：</p>
<pre class="crayon-plain-tag">class ConnectionCallbacks {
public:
  virtual ~ConnectionCallbacks() {}

  // ConnectionEvent的回调
  virtual void onEvent(ConnectionEvent event) PURE;

  // 当连接的写缓冲超过高水位后调用
  virtual void onAboveWriteBufferHighWatermark() PURE;

  // 当连接的写缓冲，从超过高水位变为低于低水位后调用
  virtual void onBelowWriteBufferLowWatermark() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ActiveConnection</span></div>
<p>表示某个连接处理器所（通过ActiveListener）拥有的活动的连接。ActiveConnection引用一个Connection、一个ActiveListener。</p>
<div class="blog_h2"><span class="graybg">L7核心接口</span></div>
<div class="blog_h3"><span class="graybg">Connection</span></div>
<p>表示可以拥有多个流（Stream）的HTTP客户端/服务器连接：</p>
<pre class="crayon-plain-tag">class Connection {
public:

  // 分发入站的请求数据
  virtual void dispatch(Buffer::Instance&amp; data) PURE;

  // 给对端提示以go away，从此时开始，不能创建新的流
  virtual void goAway() PURE;

  // 返回连接的协议
  virtual Protocol protocol() PURE;

  // 给对端提示以shutdown notice，对端不应该在发送任何新的流，但是对于已经达到的流u，不会被重置
  virtual void shutdownNotice() PURE;

  // HTTP编解码器是否有数据需要写入，但是由于协议的原因（例如窗口更新），无法完成
  virtual bool wantsToWrite() PURE;

  // 当底层的Network::Connection超过高水位后，调用此方法
  virtual void onUnderlyingConnectionAboveWriteBufferHighWatermark() PURE;

  // 当底层的Network::Connection超过高水位后，然后由低于低水位后调用此方法
  virtual void onUnderlyingConnectionBelowWriteBufferLowWatermark() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ConnectionCallbacks</span></div>
<p>HTTP连接级别的回调：</p>
<pre class="crayon-plain-tag">class ConnectionCallbacks {
public:
  // 对端提示go away时触发此回调，不允许创建新流
  virtual void onGoAway() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ServerConnection</span></div>
<p>服务器端连接：</p>
<pre class="crayon-plain-tag">class ServerConnection : public virtual Connection {};</pre>
<p>HTTP连接管理器ConnectionManagerImpl.codec字段是ServerConnection类型，后者承担读取到的请求数据的分发（Dispatch，给HTTP解析器）职责。</p>
<div class="blog_h3"><span class="graybg">ServerConnectionCallbacks</span></div>
<p>继承ConnectionCallbacks回调，并添加方法：</p>
<pre class="crayon-plain-tag">class ServerConnectionCallbacks : public virtual ConnectionCallbacks {
public:
  /**
   * 当对端初始化一个新的请求流后触发此回调
   * @param response_encoder 提供用于创建响应的编码器，请求、响应由同一流对象管理
   * @param is_internally_created 提示此流是流客户端创建，还是由Envoy自己创建（例如内部重定向）
   */
  virtual StreamDecoder&amp; newStream(StreamEncoder&amp; response_encoder,
                                   bool is_internally_created = false) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamDecoder</span></div>
<p>HTTP流解码器，可以解码下游发来的请求：</p>
<pre class="crayon-plain-tag">class StreamDecoder {
public:
  virtual ~StreamDecoder() {}

  // 处理解码后的100-Continue头的map
  virtual void decode100ContinueHeaders(HeaderMapPtr&amp;&amp; headers) PURE;

  // 处理解码后的头
  virtual void decodeHeaders(HeaderMapPtr&amp;&amp; headers, bool end_stream) PURE;

  // 处理解码后的数据帧
  virtual void decodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  // 处理解码后的尾帧
  virtual void decodeTrailers(HeaderMapPtr&amp;&amp; trailers) PURE;

  // 处理解码后的元数据
  virtual void decodeMetadata(MetadataMapPtr&amp;&amp; metadata_map) PURE;
};</pre>
<p>这里这里的decode有歧义：</p>
<ol>
<li>decoded，表示经由http_parser解析，结构化为C++对象 —— HTTP语境</li>
<li>decode，调用Envoy的流解码器处理那些C++对象 —— Envoy语境</li>
</ol>
<div class="blog_h3"><span class="graybg">StreamEncoder</span></div>
<p>HTTP流编码器，可以编码需要发送给下游的应答，接口类似于StreamDecoder。</p>
<div class="blog_h3"><span class="graybg">StreamCallbacks</span></div>
<p>针对HTTP流的回调：</p>
<pre class="crayon-plain-tag">class StreamCallbacks {
public:
  // 对端重置了流后调用，参数是重置原因
  virtual void onResetStream(StreamResetReason reason) PURE;

  // 当一个流（HTTP2），或者流发向的连接（HTTP1）超过高水位后调用
  virtual void onAboveWriteBufferHighWatermark() PURE;

  // 当一个流（HTTP2），或者流发向的连接（HTTP1）从超过高水位降到低于低水位后调用
  virtual void onBelowWriteBufferLowWatermark() PURE;
} </pre>
<div class="blog_h3"><span class="graybg">ActiveStream</span></div>
<p>表示连接上的单个HTTP流，实现了StreamDecoder、StreamCallbacks、FilterChainFactoryCallbacks接口。</p>
<div class="blog_h3"><span class="graybg">FilterChainFactoryCallbacks</span></div>
<p>HTTP连接管理器提供给过滤器链工厂的回调集，依赖于此回调工厂能够以应用程序特定的方式构建过滤器链：</p>
<pre class="crayon-plain-tag">class FilterChainFactoryCallbacks {
public:

  // 添加读取流数据时使用的解码器
  virtual void addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr filter) PURE;

  // 添加写入流数据时使用的编码器
  virtual void addStreamEncoderFilter(Http::StreamEncoderFilterSharedPtr filter) PURE;

  // 添加读写编解码器
  virtual void addStreamFilter(Http::StreamFilterSharedPtr filter) PURE;

  // 添加访问日志处理器，在流被销毁时调用
  virtual void addAccessLogHandler(AccessLog::InstanceSharedPtr handler) PURE;
}</pre>
<div class="blog_h3"><span class="graybg">StreamFilterBase</span></div>
<p>流编码/解码过滤器的基类：</p>
<pre class="crayon-plain-tag">class StreamFilterBase {
public:
  /**
   * 在过滤器被销毁前调用此方法，这可能发生在
   * 1、正常的流（下游+上游）完成后
   * 2、发生重置后
   * 每个过滤器负责确保在此方法的上下文中，所有异步事件被清理完毕。这些异步事件包括定时器、网络调用等
   *
   * 不在析构函数中进行清理而使用onDestroy钩子的原因和Envoy的延迟删除模型有关。此模型规避了Stack unwind
   * 有关的复杂性。在onDestroy之后，过滤器不得调用编码/解码过滤器回调
   */
  virtual void onDestroy() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamDecoderFilter</span></div>
<p>流解码过滤器，继承StreamFilterBase：</p>
<pre class="crayon-plain-tag">class StreamDecoderFilter : public StreamFilterBase {
public:
  // 解码请求头
  virtual FilterHeadersStatus decodeHeaders(HeaderMap&amp; headers, bool end_stream) PURE;

  // 解码数据帧
  virtual FilterDataStatus decodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  // 解码请求尾
  virtual FilterTrailersStatus decodeTrailers(HeaderMap&amp; trailers) PURE;

  // 设置此解码过滤器的过滤器回调
  virtual void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks&amp; callbacks) PURE;
} </pre>
<div class="blog_h3"><span class="graybg">StreamFilterCallbacks</span></div>
<p>传递给所有（读/写）过滤器的回调函数集，用于写响应数据、和底层流交互：</p>
<pre class="crayon-plain-tag">class StreamFilterCallbacks {
public:

  // 获取L4网络连接
  virtual const Network::Connection* connection() PURE;

  // 返回线程本地的Dispatcher，从此分发器来分配定时器
  virtual Event::Dispatcher&amp; dispatcher() PURE;

  // 将底层的流进行重置
  virtual void resetStream() PURE;

  // 返回当前请求使用的路由
  // 实现应当能够进行路由缓存，避免反复查找。如果过滤器修改了请求头，则路由可能需要改变，此时应当调用clearRouteCache()

  // 未来可能会允许过滤器对路由条目进行覆盖
  virtual Router::RouteConstSharedPtr route() PURE;

  // 返回被缓存的路由条目的上游集群信息（clusterInfo）。该方法用于避免在过滤器链中进行反复的查找，同时
  // 确保当路由被picked/repicked后能提供clusterInfo的一致性视图
  virtual Upstream::ClusterInfoConstSharedPtr clusterInfo() PURE;

  // 为当前请求清除路由缓存，如果过滤器修改了请求头，并且此修改可能影响选路，则必须调用该方法
  virtual void clearRouteCache() PURE;

  // 返回用于日志目的的流唯一标识
  virtual uint64_t streamId() PURE;

  // 返回用于日志目的的StreamInfo
  virtual StreamInfo::StreamInfo&amp; streamInfo() PURE;

  // 返回追踪用的当前追踪上下文
  virtual Tracing::Span&amp; activeSpan() PURE;

  // 返回追踪配置
  virtual const Tracing::Config&amp; tracingConfig() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamDecoderFilterCallbacks</span></div>
<p>继承StreamFilterCallbacks，添加专用于解码（读）过滤器的回调：</p>
<pre class="crayon-plain-tag">class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks {
public:
  /**
   * 使用缓冲的头，以及请求体，继续迭代过滤器链。该方法仅仅在以下情况之一才会调用
   * 1、先前的过滤器在decodeHeaders()后返回StopIteration
   * 2、先前的过滤器在decodeData()后返回StopIterationAndBuffer, StopIterationAndWatermark 或 StopIterationNoBuffer
   *
   * HTTP连接管理器会分发请求头、缓冲的请求体给过滤器链中的下一个过滤器
   * 
   * 如果请求没有完成，当前过滤器仍然会继续接受decodeData()调用，并且必须返回适当的的状态码
   *
   */
  virtual void continueDecoding() PURE;

  // 返回当前过滤器、或者链中先前过滤器缓冲的数据。如果尚未缓冲任何内容，返回nullpt
  virtual const Buffer::Instance* decodingBuffer() PURE;

  /**
   * 添加解码处理后的、缓冲的请求体数据。在某些高级用例中，decodeData()返回StopIterationAndBuffer不能满足
   * 需要，需要调用此方法：
   *
   * 1) 对于header-only请求需要被转换为包含请求体的请求，可以在 decodeHeaders() 回调中调用此方法，添加请求体
   * 后续过滤器会依次接收调用decodeHeaders(..., false)、decodeData(..., true)。在直接迭代、停止后继续迭代
   * 场景下，都可以使用
   * 
   *
   * 2) 如果某个过滤器希望在end_stream=true的情况下，在一个数据回调中查看所有缓冲的数据，可以调用该方法，以立即缓冲数据
   * 避免同时处理已缓冲数据、以及当前回调产生的数据
   *
   * 3) 如果某个过滤器在调用后续过滤器时，需要添加额外的缓冲请求体数据
   *
   * 4) 如果在decodeTrailers()回调中需要添加额外的数据。可以在前述回调的上下文中调用此方法
   * 所有后续过滤器会依次接受decodeData(..., false)、decodeTrailers()调用
   *
   * 在其它场景下调用此方法是错误
   *
   * @param data Buffer::Instance 添加需要被解码的数据
   * @param streaming_filter boolean 提示该过滤器是流式处理还是缓冲了完整请求体
   */
  virtual void addDecodedData(Buffer::Instance&amp; data, bool streaming_filter) PURE;

  /**
   * 添加解码后的请求尾。只能在end_stream=true时在decodeData中调用 
   * 在decodeData中调用时，请求尾映射被初始化为空map并以引用的方式返回
   * 该方法最多调用一次
   *
   * @return 返回新的空请求尾map
   */
  virtual HeaderMap&amp; addDecodedTrailers() PURE;

  /* 基于其它的状态码、请求体，生成一个Envoy本地的响应并发送给下游
   * 如果是gRPC请求，则本地响应编码为gRPC响应，HTTP状态码置为200。从参数生成grpc-status、grpc-message
   *
   * @param response_code HTTP状态码
   * @param body_text HTTP请求体，以text/plain发送或者编码在grpc-message头中
   * @param modify_headers 可选的回调函数，用于修改响应头
   * @param grpc_status gRPC状态码，覆盖通过httpToGrpcStatus推导出的gRPC状态码
   */
  virtual void sendLocalReply(Code response_code, absl::string_view body_text,
                              std::function&lt;void(HeaderMap&amp; headers)&gt; modify_headers,
                              const absl::optional&lt;Grpc::Status::GrpcStatus&gt; grpc_status) PURE;

  /**
   * 编码100-Continue响应头。该头不在encodeHeaders中处理，因为大部分情况下Envoy用户和过滤器
   * 不希望代理100-Continue，而是直接吐出，可以忽略多次编码响应头encodeHeaders()的复杂性
   *
   * @param headers supplies 需要编码的头
   */
  virtual void encode100ContinueHeaders(HeaderMapPtr&amp;&amp; headers) PURE;

  /**
   * 编码响应头。HTTP连接管理器会自动探测一些不发给下游的伪头
   *
   * @param headers 需要编码的头
   * @param end_stream 这是不是一个header-only的request/response
   */
  virtual void encodeHeaders(HeaderMapPtr&amp;&amp; headers, bool end_stream) PURE;

  /**
   * 编码响应数据
   * @param data 需要编码的数据
   * @param end_stream 提示这是不是最后一个数据帧
   */
  virtual void encodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  /**
   * 编码响应尾数据，隐含意味着流的结束
   * @param trailers supplies 需要编码的尾
   */
  virtual void encodeTrailers(HeaderMapPtr&amp;&amp; trailers) PURE;

  /**
   * 编码元数据
   *
   * @param metadata_map supplies 需要编码的元数据的unique_ptr
   */
  virtual void encodeMetadata(MetadataMapPtr&amp;&amp; metadata_map) PURE;

  /**
   * 当解码过滤器的缓冲，或者过滤器需要发送数据到的那些缓冲，超越高水位后调用
   *
   * 对于路由过滤器这样的HTTP过滤器，会使用多个缓冲（codec、connection...），该方法可能被调用多次
   * 这些过滤器应当负责，在对应的缓冲被排干后，以等同次数调用低水位回调connection etc.)
   */
  virtual void onDecoderFilterAboveWriteBufferHighWatermark() PURE;

  /**
   * 当解码过滤器的缓冲，或者过滤器需要发送数据到的那些缓冲，超越高水位后，后降低到低于低水位后调用
   */
  virtual void onDecoderFilterBelowWriteBufferLowWatermark() PURE;

  /**
   * 需要订阅下游流、下游连接上的水位事件的过滤器，调用此方法
   * 订阅后，对于每个outstanding backed up buffer，过滤器的回调都被调用
   */
  virtual void addDownstreamWatermarkCallbacks(DownstreamWatermarkCallbacks&amp; callbacks) PURE;

  /**
   * 需要停止订阅下游流、下游连接上的水位事件的过滤器，调用此方法
   * 在DownstreamWatermarkCallbacks的回调函数的栈下面调用此方法不安全
   */
  virtual void removeDownstreamWatermarkCallbacks(DownstreamWatermarkCallbacks&amp; callbacks) PURE;

  /**
   * 用于改变解码过滤器的缓冲区大小
   *
   * @param limit 新的缓冲大小
   */
  virtual void setDecoderBufferLimit(uint32_t limit) PURE;

  // 返回解码过滤器缓冲大小，0表示无限制u
  virtual uint32_t decoderBufferLimit() PURE;

  // 将当前流看作是新的，就好像它的所有头都是刚到达一样
  // 如果操作成功，会导致创建新的过滤器链，并且上游请求可能和原始的下游流关联
  // 如果操作失败，并且下面列出的前置条件不满足，则调用者负责处理和终止原始流
  //
  // 前置条件
  //   - 流必须已经被完整的读取
  //   - 流必须没有请求体
  //
  // 注意HTTP消毒器不会针对这种重新创建的流进行操作，它假设消毒已经完成
  virtual bool recreateStream() PURE;
} </pre>
<div class="blog_h3"><span class="graybg">ActiveStreamDecoderFilter</span></div>
<p>表示活动的解码过滤器，继承ActiveStreamFilterBase，实现了StreamFilterCallbacks、StreamDecoderFilterCallbacks，也就是说，过滤器和过滤器回调是一体的。</p>
<p>该对象持有一个StreamDecoderFilter对象。ActiveStream通过字段<pre class="crayon-plain-tag">std::list&lt;ActiveStreamDecoderFilterPtr&gt; decoder_filters_</pre>来引用这种对象。</p>
<div class="blog_h2"><span class="graybg">HTTP1核心接口</span></div>
<div class="blog_h3"><span class="graybg">ActiveRequest</span></div>
<p>多个地方存在命名为ActiveRequest的结构，表示一个活动的HTTP1请求。</p>
<p>例如，作为Http::Http1::ServerConnectionImpl的私有成员：</p>
<pre class="crayon-plain-tag">struct ActiveRequest {
  // 构造请求对象时，必须传入响应编码器
  ActiveRequest(ConnectionImpl&amp; connection) : response_encoder_(connection) {}

  HeaderString request_url_;
  // 请求解码器
  StreamDecoder* request_decoder_{};
  // 响应编码器
  ResponseStreamEncoderImpl response_encoder_;
  // 请求的处理是否已经完毕
  bool remote_complete_{};
};</pre>
<p>作为Http::CodecClient的私有成员：</p>
<pre class="crayon-plain-tag">struct ActiveRequest : LinkedObject&lt;ActiveRequest&gt;,
                       public Event::DeferredDeletable,
                       public StreamCallbacks,
                       public StreamDecoderWrapper {
  ActiveRequest(CodecClient&amp; parent, StreamDecoder&amp; inner) : StreamDecoderWrapper(inner), parent_(parent) {}

  // 流回调函数
  void onResetStream(StreamResetReason reason) override { parent_.onReset(*this, reason); }
  void onAboveWriteBufferHighWatermark() override {}
  void onBelowWriteBufferLowWatermark() override {}
  void onPreDecodeComplete() override { parent_.responseDecodeComplete(*this); }
  void onDecodeComplete() override {}

  // 编码器
  StreamEncoder* encoder_{};
  CodecClient&amp; parent_;
} </pre>
<div class="blog_h2"><span class="graybg">启动监听</span></div>
<p>Envoy代理的需要创建哪些监听器，由Bootstrap配置 + xDS响应共同决定，本文不讨论细节，可以参考<a href="/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a>。</p>
<p>如果某个监听器配置了bind_to_port（默认情况下virtual 15001配置了），则会调用libevent的API，注册套接字监听事件：</p>
<pre class="crayon-plain-tag">ListenerImpl::ListenerImpl(Event::DispatcherImpl&amp; dispatcher, Socket&amp; socket, ListenerCallbacks&amp; cb,
                           bool bind_to_port, bool hand_off_restored_destination_connections)
    : BaseListenerImpl(dispatcher, socket), cb_(cb),
      hand_off_restored_destination_connections_(hand_off_restored_destination_connections),
      listener_(nullptr) {
  if (bind_to_port) {
    // 注册监听
    setupServerSocket(dispatcher, socket);
  }
}

void ListenerImpl::setupServerSocket(Event::DispatcherImpl&amp; dispatcher, Socket&amp; socket) {
  // 重置 CSmartPtr&lt;evconnlistener, evconnlistener_free&gt; ListenerPtr引用为新的evconnlistener
  listener_.reset(
      // libevent的base                      当前对象方法                   套接字的文件描述符
      evconnlistener_new(&amp;dispatcher.base(), listenCallback, this, 0, -1, socket.ioHandle().fd()));

  if (!listener_) {
    throw CreateListenerException(
        fmt::format("cannot listen on socket: {}", socket.localAddress()-&gt;asString()));
  }

  if (!Network::Socket::applyOptions(socket.options(), socket,
                                     envoy::api::v2::core::SocketOption::STATE_LISTENING)) {
    throw CreateListenerException(fmt::format("cannot set post-listen socket option on socket: {}",
                                              socket.localAddress()-&gt;asString()));
  }

  evconnlistener_set_error_cb(listener_.get(), errorCallback);
}</pre>
<div class="blog_h2"><span class="graybg">接受连接</span></div>
<p>Envoy默认会在15001端口上监听，当流量到达（通常是通过其它端口重定向到15001）时，Envoy的DispatcherImpl循环得到通知，并通过libevent回调Envoy::Network::ListenerImpl::listenCallback方法，该方法是一切<span style="background-color: #c0c0c0;">新连接处理的起点</span>：</p>
<pre class="crayon-plain-tag">void ListenerImpl::listenCallback(evconnlistener*, evutil_socket_t fd, sockaddr* remote_addr,
                                  int remote_addr_len, void* arg) {
  // 传递的参数是监听器对象
  ListenerImpl* listener = static_cast&lt;ListenerImpl*&gt;(arg);

  // IoSocketHandle为IO操作的抽象接口
  IoHandlePtr io_handle = std::make_unique&lt;IoSocketHandle&gt;(fd);

  // 如果监听器在ANY地址（0.0.0.0）上监听，从新套接字上获取本地地址
  const Address::InstanceConstSharedPtr&amp; local_address =
  // 获取远程地址
  const Address::InstanceConstSharedPtr&amp; remote_address =
      (remote_addr-&gt;sa_family == AF_UNIX)
          ? Address::peerAddressFromFd(io_handle-&gt;fd())
          : Address::addressFromSockAddr(*reinterpret_cast&lt;const sockaddr_storage*&gt;(remote_addr),
                                         remote_addr_len,
                                         local_address-&gt;ip()-&gt;version() == Address::IpVersion::v6);
  // 调用监听器的onAccept回调
  listener-&gt;cb_.onAccept(
      std::make_unique&lt;AcceptedSocketImpl&gt;(std::move(io_handle), local_address, remote_address),
      listener-&gt;hand_off_restored_destination_connections_);
}</pre>
<p>回调ConnectionHandlerImpl::ActiveListener::onAccept的逻辑如下：</p>
<pre class="crayon-plain-tag">// 此回调在新连接创建后调用
// socket 需要移动给被调用者的套接字对象
// redirected 如果套接字是第一次被其它监听器接受，并且随后被重定向给一个新的监听器时，为true
//            接收者监听器不得再次重定向
void ConnectionHandlerImpl::ActiveListener::onAccept(
    Network::ConnectionSocketPtr&amp;&amp; socket, bool hand_off_restored_destination_connections) {
  // 代表当前正在处理的套接字对象
  auto active_socket = std::make_unique&lt;ActiveSocket&gt;(*this, std::move(socket),
                                                      hand_off_restored_destination_connections);

  // 构建出过监听器过滤器链
  config_.filterChainFactory().createListenerFilterChain(*active_socket);
  // 开始迭代过滤器链
  active_socket-&gt;continueFilterChain(true);

  // 如果监听器过滤器链没有迭代完毕，则active_socket暂存到sockets_列表里
  // 防止active_socket因为超出作用域而被自动删除
  if (active_socket-&gt;iter_ != active_socket-&gt;accept_filters_.end()) {
    active_socket-&gt;startTimer();
    active_socket-&gt;moveIntoListBack(std::move(active_socket), sockets_);
  }
}</pre>
<div class="blog_h3"><span class="graybg">监听器过滤器链</span></div>
<p>ActiveSocket实现了Network::ListenerFilterManager：</p>
<pre class="crayon-plain-tag">class ListenerFilterManager {
public:
  virtual ~ListenerFilterManager() {}
  // 为监听器添加一个过滤器，过滤器以FIFO顺序被调用
  virtual void addAcceptFilter(ListenerFilterPtr&amp;&amp; filter) PURE;
};</pre>
<p>ListenerImpl的createListenerFilterChain方法支持为ListenerFilterManager提供过滤器链：</p>
<pre class="crayon-plain-tag">bool ListenerImpl::createListenerFilterChain(Network::ListenerFilterManager&amp; manager) {
  return Configuration::FilterChainUtility::buildFilterChain(manager, listener_filter_factories_);
}</pre>
<p>listener_filter_factories_在ListenerImpl初始化阶段创建，它是Network::ListenerFilterFactoryCb的迭代器，表示<span style="background-color: #c0c0c0;">当前监听器启用的所有监听器过滤器的工厂回调的集合</span>。调用ListenerFilterFactoryCb可以将过滤器安装到ListenerFilterManager，也就是ActiveSocket上：</p>
<pre class="crayon-plain-tag">bool FilterChainUtility::buildFilterChain( Network::ListenerFilterManager&amp; filter_manager,
    const std::vector&lt;Network::ListenerFilterFactoryCb&gt;&amp; factories) {
  for (const Network::ListenerFilterFactoryCb&amp; factory : factories) {
    factory(filter_manager);
  }
  return true;
}</pre>
<p>默认情况下，监听器virtual（15001端口）只配置一个监听器过滤器OriginalDstFilter，它的工厂如下：</p>
<pre class="crayon-plain-tag">class OriginalDstConfigFactory : public Server::Configuration::NamedListenerFilterConfigFactory {
public:
  // 此即工厂回调的工厂
  Network::ListenerFilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message&amp;,
                               Server::Configuration::ListenerFactoryContext&amp;) override {
    return [](Network::ListenerFilterManager&amp; filter_manager) -&gt; void {
      // 上段代码的factory(filter_manager)调用的是此Lambda
      // 简单的创建OriginalDstFilter并添加到管理器
      filter_manager.addAcceptFilter(std::make_unique&lt;OriginalDstFilter&gt;());
    };
  }

  ProtobufTypes::MessagePtr createEmptyConfigProto() override {
    return std::make_unique&lt;Envoy::ProtobufWkt::Empty&gt;();
  }

  std::string name() override { return ListenerFilterNames::get().OriginalDst; }
};</pre>
<div class="blog_h3"><span class="graybg">迭代监听器过滤器链</span></div>
<p>当一个过滤器返回FilterStatus::StopIteration以终止过滤器迭代，那么：</p>
<ol>
<li>如果希望继续遍历后续过滤器链，以true参数调用下面的方法</li>
<li>如果过滤器执行失败，需要关闭连接，以false参数调用下面的方法</li>
</ol>
<pre class="crayon-plain-tag">void ConnectionHandlerImpl::ActiveSocket::continueFilterChain(bool success) {
  // 开始/继续迭代
  if (success) {
    if (iter_ == accept_filters_.end()) {
      iter_ = accept_filters_.begin();
    } else {
      iter_ = std::next(iter_);
    }
    // 从当前过滤器迭代到监听器过滤器集的尾部
    for (; iter_ != accept_filters_.end(); iter_++) {
     // 调用监听器过滤器的onAccept方法，this就是ActievSocket对象
      Network::FilterStatus status = (*iter_)-&gt;onAccept(*this);
      // 上一个过滤器提示，应当停止迭代
      if (status == Network::FilterStatus::StopIteration) {
        // 则本次过滤器迭代终止。上一个过滤器负责在未来重启迭代
        return;
      }
    }
    // 所有监听器过滤器都执行成功

    // 检查套接字是否需要重定向给其它监听器
    ActiveListener* new_listener = nullptr;

    // OriginalDstFilter会导致下面的分支执行
    if (hand_off_restored_destination_connections_ &amp;&amp; socket_-&gt;localAddressRestored()) {
      // 找到匹配原始目的地址的那个监听器
      new_listener = listener_.parent_.findActiveListenerByAddress(*socket_-&gt;localAddress());
    }
    if (new_listener != nullptr) {
      // 将由Iptables重定向到当前监听器的连接转交给匹配原始目的地址的监听器处理
      // 同时传递hand_off_restored_destination_connections=false，防止再次重定向
      new_listener-&gt;onAccept(std::move(socket_), false);
    } else {
      // 非重定向连接，或者重定向连接的接收者监听器
      if (socket_-&gt;detectedTransportProtocol().empty()) {
        // 设置默认传输协议
        socket_-&gt;setDetectedTransportProtocol(
            Extensions::TransportSockets::TransportSocketNames::get().RawBuffer);
      }
      // 在此监听器上创建新的连接对象
      listener_.newConnection(std::move(socket_));
    }
  }

  // 过滤器执行完毕，如果ActiveSocket已经linked，则unlink并删除
  if (inserted()) {
    unlink();
  }
}</pre>
<div class="blog_h3"><span class="graybg">OriginalDstFilter</span></div>
<p>该监听器过滤器的onAccept方法的实现如下：</p>
<pre class="crayon-plain-tag">Network::FilterStatus OriginalDstFilter::onAccept(Network::ListenerFilterCallbacks&amp; cb) {
  ENVOY_LOG(debug, "original_dst: New connection accepted");
  Network::ConnectionSocket&amp; socket = cb.socket();
  const Network::Address::Instance&amp; local_address = *socket.localAddress();

  // 通过系统调用os_syscalls.getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, &amp;orig_addr, &amp;addr_len)获取原始目的IP
  if (local_address.type() == Network::Address::Type::Ip) {
    // 我们的例子中，原始目的IP地址为127.0.0.1:9898
    Network::Address::InstanceConstSharedPtr original_local_address = getOriginalDst(socket.ioHandle().fd());

    // 即使对于use_original_dst设置为true的监听器（也就是当前监听器），仍然能够接收不是由iptables重定向的连接
    // 如果连接不是被重定向的，那么getOriginalDst()的返回值和当前套接字的本地地址（Envoy代理端）相同
    // 这种情况下，当前监听器直接处理连接，而不会转交（hand off）给其它监听器
    if (original_local_address) {
      // 修改local_address_，并设置local_address_restored_为true
      socket.restoreLocalAddress(original_local_address);
    }
  }
  // 总是继续迭代监听器过滤器链
  return Network::FilterStatus::Continue;
}</pre>
<div class="blog_h2"><span class="graybg">处理TCP连接</span></div>
<p>监听器过滤器的处理是以ActiveSocket为中心的，套接字请求接受后，连接的处理则以ActiveListener为中心。 </p>
<p>执行完监听器过滤器后，ActiveSocket调用ActiveListener.newConnection，开始连接的处理：</p>
<pre class="crayon-plain-tag">void ConnectionHandlerImpl::ActiveListener::newConnection(Network::ConnectionSocketPtr&amp;&amp; socket) {
  // 首先，查找匹配此套接字的过滤器链
  const auto filter_chain = config_.filterChainManager().findFilterChain(*socket);
  if (filter_chain == nullptr) {
    // 找不到过滤器，记录统计信息，关闭套接字，结束处理
    ENVOY_LOG_TO_LOGGER(parent_.logger_, debug, "closing connection: no matching filter chain found");
    stats_.no_filter_chain_match_.inc();
    socket-&gt;close();
    return;
  }
  // 创建一个传输套接字，此套接字负责实际的读写操作
  // 具体工厂和协议有关，默认RawBufferSocketFactory，创建RawBufferSocket
  auto transport_socket = filter_chain-&gt;transportSocketFactory().createTransportSocket(nullptr);
  // 创建连接对象，设置为connected
  Network::ConnectionPtr new_connection = parent_.dispatcher_.createServerConnection(std::move(socket), std::move(transport_socket));
  // 设置缓冲区大小
  new_connection-&gt;setBufferLimits(config_.perConnectionBufferLimitBytes());
  // 写过滤器的顺序可能需要倒置
  new_connection-&gt;setWriteFilterOrder(config_.reverseWriteFilterOrder());

  // 为连接初始化过滤器链
  const bool empty_filter_chain = !config_.filterChainFactory().createNetworkFilterChain(
      *new_connection, filter_chain-&gt;networkFilterFactories());
  // 如果初始化过滤器链失败，则关闭连接
  if (empty_filter_chain) {
    ENVOY_CONN_LOG_TO_LOGGER(parent_.logger_, debug, "closing connection: no filters", *new_connection);
    new_connection-&gt;close(Network::ConnectionCloseType::NoFlush);
    return;新连接处理的起点
  }

  // 监听器的新连接回调
  onNewConnection(std::move(new_connection));
}</pre>
<div class="blog_h3"><span class="graybg">查找过滤器链配置</span></div>
<p>在ActiveListener::newConnection期间，调用config_.filterChainManager().findFilterChain来查找匹配连接的过滤器链配置：</p>
<pre class="crayon-plain-tag">const Network::FilterChain* ListenerImpl::findFilterChain(const Network::ConnectionSocket&amp; socket) const {
  // 本地地址（恢复到原始目的地址后的）
  const auto&amp; address = socket.localAddress();

  // 根据目的端口匹配
  if (address-&gt;type() == Network::Address::Type::Ip) {
    const auto port_match = destination_ports_map_.find(address-&gt;ip()-&gt;port());
    if (port_match != destination_ports_map_.end()) {
      return findFilterChainForDestinationIP(*port_match-&gt;second.second, socket);
    }
  }

  // 缺省匹配
  const auto port_match = destination_ports_map_.find(0);
  if (port_match != destination_ports_map_.end()) {
    return findFilterChainForDestinationIP(*port_match-&gt;second.second, socket);
  }

  return nullptr;
}</pre>
<div class="blog_h3"><span class="graybg">实例化过滤器链</span></div>
<p>在ActiveListener::newConnection期间，调用config_.filterChainFactory().createNetworkFilterChain()为连接实例化过滤器链：</p>
<pre class="crayon-plain-tag">bool ListenerImpl::createNetworkFilterChain(
    Network::Connection&amp; connection,
    const std::vector&lt;Network::FilterFactoryCb&gt;&amp; filter_factories) {
  return Configuration::FilterChainUtility::buildFilterChain(connection, filter_factories);
}

// 和构建监听器过滤器时的逻辑一样，遍历过滤器工厂，传入Connection调用之
bool FilterChainUtility::buildFilterChain(Network::FilterManager&amp; filter_manager,
                                          const std::vector&lt;Network::FilterFactoryCb&gt;&amp; factories) {
  for (const Network::FilterFactoryCb&amp; factory : factories) {
    factory(filter_manager);
  }
  // 初始化所有读过滤器，也就是调用每个过滤器的onNewConnection 
  return filter_manager.initializeReadFilters();
}</pre>
<div class="blog_h3"><span class="graybg">监听器新连接回调</span></div>
<p>当ActiveListener为新连接准备好过滤器链后，会调用自身的：</p>
<pre class="crayon-plain-tag">void ConnectionHandlerImpl::ActiveListener::onNewConnection( Network::ConnectionPtr&amp;&amp; new_connection) {
  ENVOY_CONN_LOG_TO_LOGGER(parent_.logger_, debug, "new connection", *new_connection);

  // 如果新连接的状态不是已经关闭
  if (new_connection-&gt;state() != Network::Connection::State::Closed) {
    ActiveConnectionPtr active_connection(
        new ActiveConnection(*this, std::move(new_connection), parent_.dispatcher_.timeSystem()));
    // 存放到当前ActiveListener的字段中，防止析构
    active_connection-&gt;moveIntoList(std::move(active_connection), connections_);
    // 将新连接加入到连接处理器中。注意C++ 11中++是原子操作
    parent_.num_connections_++;
  }
  // 否则，新连接将在此立即析构
}</pre>
<div class="blog_h2"><span class="graybg">处理HTTP连接</span></div>
<p>对于HTTP协议， 处理TCP连接的逻辑仍然使用。</p>
<div class="blog_h3"><span class="graybg">HTTP连接管理器</span></div>
<p>在实例化过滤器链时，HTTP连接会有一个过滤器 —— HTTP连接管理器（ConnectionManagerImpl），它的工厂如下：</p>
<pre class="crayon-plain-tag">Network::FilterFactoryCb HttpConnectionManagerFilterConfigFactory::createFilterFactoryFromProtoTyped(
    const envoy::config::filter::network::http_connection_manager::v2::HttpConnectionManager&amp; roto_config,
    Server::Configuration::FactoryContext&amp; context) {
  // 线程本地的一个缓存提供者，每500ms为当前线程更新日期信息
  std::shared_ptr&lt;Http::TlsCachingDateProviderImpl&gt; date_provider =
      context.singletonManager().getTyped&lt;Http::TlsCachingDateProviderImpl&gt;(
          SINGLETON_MANAGER_REGISTERED_NAME(date_provider), [&amp;context] {
            return std::make_shared&lt;Http::TlsCachingDateProviderImpl&gt;(context.dispatcher(),
                                                                      context.threadLocal());
          });
  // 此管理器维护RouteConfigProvider，后者提供路由信息
  std::shared_ptr&lt;Router::RouteConfigProviderManager&gt; route_config_provider_manager =
      context.singletonManager().getTyped&lt;Router::RouteConfigProviderManager&gt;(
          SINGLETON_MANAGER_REGISTERED_NAME(route_config_provider_manager), [&amp;context] {
            return std::make_shared&lt;Router::RouteConfigProviderManagerImpl&gt;(context.admin());
          });

  // 连接管理器的配置
  std::shared_ptr&lt;HttpConnectionManagerConfig&gt; filter_config(new HttpConnectionManagerConfig(
      proto_config, context, *date_provider, *route_config_provider_manager));

  // 此Lambda捕获了上面的共享指针，因此避免了引用计数清零
  // 此Lambda即HTTP连接管理器的L4过滤器工厂
  return [route_config_provider_manager, filter_config, &amp;context,
          date_provider](Network::FilterManager&amp; filter_manager) -&gt; void {
    // 为Connection添加一个读过滤器ConnectionManagerImpl
    filter_manager.addReadFilter(Network::ReadFilterSharedPtr{new Http::ConnectionManagerImpl(
        *filter_config, context.drainDecision(), context.random(), context.httpContext(),
        context.runtime(), context.localInfo(), context.clusterManager(),
        &amp;context.overloadManager(), context.dispatcher().timeSystem())});
  };
}</pre>
<p>ConnectionManagerImpl的构造函数如下：</p>
<pre class="crayon-plain-tag">ConnectionManagerImpl::ConnectionManagerImpl(ConnectionManagerConfig&amp; config,
                                             const Network::DrainDecision&amp; drain_close,
                                             Runtime::RandomGenerator&amp; random_generator,
                                             Http::Context&amp; http_context, Runtime::Loader&amp; runtime,
                                             const LocalInfo::LocalInfo&amp; local_info,
                                             Upstream::ClusterManager&amp; cluster_manager,
                                             Server::OverloadManager* overload_manager,
                                             Event::TimeSystem&amp; time_system)
    // ConnectionManagerConfig 连接管理器的配置
    //                 ConnectionManagerStats 统计指标
    : config_(config), stats_(config_.stats()),
    // 连接持续的时长
      conn_length_(new Stats::Timespan(stats_.named_.downstream_cx_length_ms_, time_system)),
    // Network::DrainDecision，给出连接是否应该被Drain并关闭
    //                           随机数生成器                           HTTP上下文，每个服务器一个，提供Tracer等信息
      drain_close_(drain_close), random_generator_(random_generator), http_context_(http_context),
   // 能从磁盘读取Envoy运行时快照   本地环境信息        集群管理器
      runtime_(runtime), local_info_(local_info), cluster_manager_(cluster_manager),
   // ConnectionManagerListenerStats 连接管理器监听器统计信息
      listener_stats_(config_.listenerStats()),
   // 过载管理，停止接受请求、禁止Keepalive
      overload_stop_accepting_requests_ref_(
          overload_manager ? overload_manager-&gt;getThreadLocalOverloadState().getState(
                                 Server::OverloadActionNames::get().StopAcceptingRequests)
                           : Server::OverloadManager::getInactiveState()),
      overload_disable_keepalive_ref_(
          overload_manager ? overload_manager-&gt;getThreadLocalOverloadState().getState(
                                 Server::OverloadActionNames::get().DisableHttpKeepAlive)
                           : Server::OverloadManager::getInactiveState()),
    // 授时和定时器管理
      time_system_(time_system) {}</pre>
<div class="blog_h2"><span class="graybg">注册读写回调</span></div>
<p>实际负责连接的ActiveListener，会调用自己的newConnection创建新Connection对象：</p>
<pre class="crayon-plain-tag">Network::ConnectionPtr new_connection =
    parent_.dispatcher_.createServerConnection(std::move(socket), std::move(transport_socket));</pre>
<p>可以看到，创建服务器端连接的职责委托给ConnectionHandle.Dispatcher对象，连接套接字、传输套接字的所有权都被转移：</p>
<pre class="crayon-plain-tag">Network::ConnectionPtr DispatcherImpl::createServerConnection(Network::ConnectionSocketPtr&amp;&amp; socket,
                                       Network::TransportSocketPtr&amp;&amp; transport_socket) {
  ASSERT(isThreadSafe());
  return std::make_unique&lt;Network::ConnectionImpl&gt;(*this, std::move(socket),
                                                   std::move(transport_socket), true);
}</pre>
<p>可以看到连接套接字、传输套接字的所有权继续转移，给ConnectionImpl的构造函数：</p>
<pre class="crayon-plain-tag">ConnectionImpl::ConnectionImpl(Event::Dispatcher&amp; dispatcher, ConnectionSocketPtr&amp;&amp; socket,
                               TransportSocketPtr&amp;&amp; transport_socket, bool connected)
      // 传输套接字                                     连接套接字
    : transport_socket_(std::move(transport_socket)), socket_(std::move(socket)),
      // 过滤器管理器                  流信息（日志用）
      filter_manager_(*this, *this), stream_info_(dispatcher.timeSystem()),
      // 创建写缓冲
      write_buffer_(
                                                                   // 高低水位回调
          dispatcher.getWatermarkFactory().create([this]() -&gt; void { this-&gt;onLowWatermark(); },
                                                  [this]() -&gt; void { this-&gt;onHighWatermark(); })),
      dispatcher_(dispatcher), id_(next_global_id_++) {
  // 如果连接套接字的fd不可用，认为发生OOM，让进程崩掉
  RELEASE_ASSERT(ioHandle().fd() != -1, "");
  // 设置为已连接
  if (!connected) {
    connecting_ = true;
  }

  // 边缘触发，注册读写事件监听器
  file_event_ = dispatcher_.createFileEvent(
      // 传输套接字的描述符
      ioHandle().fd(), [this](uint32_t events) -&gt; void { onFileEvent(events); },
      Event::FileTriggerType::Edge, Event::FileReadyType::Read | Event::FileReadyType::Write);
  // 注册传输套接字回调
  transport_socket_-&gt;setTransportSocketCallbacks(*this);
}</pre>
<p>可以看到，当读写事件到达时，libevent会回调ConnectionImpl::onFileEvent</p>
<div class="blog_h2"><span class="graybg">触发读写回调</span></div>
<p>当发生可读、可写事件时，ConnectionImpl::onFileEvent被调用：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onFileEvent(uint32_t events) {
  // 期望连接状态为Connected，否则认为是错误，需要关闭套接字
  if (immediate_error_event_ != ConnectionEvent::Connected) {
    if (bind_error_) {
      // 绑定失败
      if (connection_stats_ &amp;&amp; connection_stats_-&gt;bind_errors_) {
        connection_stats_-&gt;bind_errors_-&gt;inc();
      }
    } else {
      // 其它错误
      ENVOY_CONN_LOG(debug, "raising immediate error", *this);
    }
    // 关闭套接字并退出
    closeSocket(immediate_error_event_);
    return;
  }

  // 关闭事件
  if (events &amp; Event::FileReadyType::Closed) {
    // 过早关闭（early close）和读操作绝不需要同时发生
    ASSERT(!(events &amp; Event::FileReadyType::Read));
    // 关闭套接字
    ENVOY_CONN_LOG(debug, "remote early close", *this);
    closeSocket(ConnectionEvent::RemoteClose);
    return;
  }

  // 可写事件
  if (events &amp; Event::FileReadyType::Write) {
    onWriteReady();
  }

  // 可读事件，由于写事件回调可能会关闭套接字（导致fd为-1），因此做个判断
  if (ioHandle().fd() != -1 &amp;&amp; (events &amp; Event::FileReadyType::Read)) {
    onReadReady();
  }
}</pre>
<div class="blog_h2"><span class="graybg">处理TCP读</span></div>
<div class="blog_h3"><span class="graybg">整体流程</span></div>
<ol>
<li>尝试循环读取，根据读取结果设置Post操作</li>
<li>处理读取到的数据</li>
<li>执行后操作</li>
</ol>
<pre class="crayon-plain-tag">void ConnectionImpl::onReadReady() {
  ENVOY_CONN_LOG(trace, "read ready", *this);

  // 断言已经连接成功
  ASSERT(!connecting_);

  // 调用传输套接字执行循环的读操作，直到没有更多数据可读，或者出错
  IoResult result = transport_socket_-&gt;doRead(read_buffer_);
  // 实际读取的字节数
  uint64_t new_buffer_size = read_buffer_.length();
  // 更新指标
  updateReadBufferStats(result.bytes_processed_, new_buffer_size);

  // 如果到达流的终点（对端关闭），但是不支持半关闭语义
  // 则后操作设置为关闭
  if ((!enable_half_close_ &amp;&amp; result.end_stream_read_)) {
    result.end_stream_read_ = false;
    result.action_ = PostIoAction::Close;
  }

  // 如果到达流终点了，或者读取的字节数不为零
  read_end_stream_ |= result.end_stream_read_;
  if (result.bytes_processed_ != 0 || result.end_stream_read_) {
    // 处理读取的数据
    onRead(new_buffer_size);
  }

  // 如果后操作应当关闭连接，或者两边都进入半关闭状态（一方关闭发送通道后，仍可接受另一方发送过来的数据）
  if (result.action_ == PostIoAction::Close || bothSidesHalfClosed()) {
    // 则关闭套接字
    closeSocket(ConnectionEvent::RemoteClose);
  }
}</pre>
<div class="blog_h3"><span class="graybg">读取到缓冲区</span></div>
<p>传输套接字的真实类型取决于传输协议（transport_protocol），默认协议是raw_buffer，对应RawBufferSocket：</p>
<pre class="crayon-plain-tag">IoResult RawBufferSocket::doRead(Buffer::Instance&amp; buffer) {
  // IO操作之后应当执行的操作，枚举Close/KeepOpen
  PostIoAction action = PostIoAction::KeepOpen;
  uint64_t bytes_read = 0;
  bool end_stream = false;
  // 循环读取
  do {
    // 尝试读取最多16K，这个16K是随便取的值
    Api::SysCallIntResult result = buffer.read(callbacks_-&gt;ioHandle().fd(), 16384);
    ENVOY_CONN_LOG(trace, "read returns: {}", callbacks_-&gt;connection(), result.rc_);
    
    // 依据系统调用返回码决定进一步操作
    if (result.rc_ == 0) {
      // 对端关闭
      end_stream = true;
      break;
    } else if (result.rc_ == -1) {
      // 远程错误（可能是没有数据可读，读完了）
      ENVOY_CONN_LOG(trace, "read error: {}", callbacks_-&gt;connection(), result.errno_);
      if (result.errno_ != EAGAIN) {
        // 错误号不是11（Try again）后操作设置为关闭
        action = PostIoAction::Close;
      }
      break;
    } else {
      // 否则，返回码是实际读取的字节数
      bytes_read += result.rc_;
      // 如果缓冲区超过限制
      if (callbacks_-&gt;shouldDrainReadBuffer()) {
        callbacks_-&gt;setReadBufferReady();
        break;
      }
    }
  } while (true);

  return {action, bytes_read, end_stream};
}</pre>
<div class="blog_h3"><span class="graybg">处理缓冲区数据</span></div>
<p>循环读取了可用的数据到缓冲区后， ConnectionImpl会调用自己的onRead方法：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onRead(uint64_t read_buffer_size) {
  // 连接不可读则返回
  if (!read_enabled_) {
    return;
  }

  // 缓冲区为空，同时没有读取到流终点
  if (read_buffer_size == 0 &amp;&amp; !read_end_stream_) {
    return;
  }

  if (read_end_stream_) {
    // 针对原始套接字的read()调用，在EOF首次（可能是对端半关闭导致）发生后，总会返回0。这里要进行判断，以免重复处理
    if (read_end_stream_raised_) {
      ASSERT(read_buffer_size == 0);
      return;
    }
    // 防止重复处理
    read_end_stream_raised_ = true;
  }
  // 转交给过滤器管理器，过滤器管理器就是Connection本身
  filter_manager_.onRead();
}</pre>
<div class="blog_h3"><span class="graybg">遍历过滤器链</span></div>
<p>如果有数据需要处理，则调用过滤器管理器的onRead方法：</p>
<pre class="crayon-plain-tag">void FilterManagerImpl::onRead() {
  // 断言上行过滤器（读取下游发来的数据）不为空
  ASSERT(!upstream_filters_.empty());
  // 传入nullptr，则从过滤器链的头部开始遍历
  onContinueReading(nullptr);
}</pre>
<p>这里的过滤器链遍历逻辑，和监听器过滤器链遍历很类似：</p>
<pre class="crayon-plain-tag">void FilterManagerImpl::onContinueReading(ActiveReadFilter* filter) {
  // 如果不设置上一个迭代的过滤器，则从头开始，否则，从下一个开始
  std::list&lt;ActiveReadFilterPtr&gt;::iterator entry;
  if (!filter) {
    entry = upstream_filters_.begin();
  } else {
    entry = std::next(filter-&gt;entry());
  }
 
  // 遍历过滤器
  for (; entry != upstream_filters_.end(); entry++) {
    // 延迟初始化，如果过滤器尚未初始化，则调用其onNewConnection
    if (!(*entry)-&gt;initialized_) {
      (*entry)-&gt;initialized_ = true;
      FilterStatus status = (*entry)-&gt;filter_-&gt;onNewConnection();
      if (status == FilterStatus::StopIteration) {
        return;
      }
    }
    // 获取先前的读缓冲区
    BufferSource::StreamBuffer read_buffer = buffer_source_.getReadBuffer();
    // 不管是可读、还是EOF，都要调用过滤器
    if (read_buffer.buffer.length() &gt; 0 || read_buffer.end_stream) {
      FilterStatus status = (*entry)-&gt;filter_-&gt;onData(read_buffer.buffer, read_buffer.end_stream);
      if (status == FilterStatus::StopIteration) {
        // 任何一个过滤器都可以终止迭代
        return;
      }
    }
  }
}</pre>
<p>过滤器链上的过滤器会被依次的调用。</p>
<div class="blog_h2"><span class="graybg">处理HTTP下游请求读</span></div>
<div class="blog_h3"><span class="graybg">HTTP连接管理器 </span></div>
<p>对于L7连接，需要调用的网络过滤器通常就是ConnectionManagerImpl：</p>
<pre class="crayon-plain-tag">Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance&amp; data, bool) {
  // 如果编解码器没有创建，则创建之
  if (!codec_) {
    // 编解码器的类型是ServerConnection
    codec_ = config_.createCodec(read_callbacks_-&gt;connection(), data, *this);
    // 更新监控指标
    if (codec_-&gt;protocol() == Protocol::Http2) {
      stats_.named_.downstream_cx_http2_total_.inc();
      stats_.named_.downstream_cx_http2_active_.inc();
    } else {
      stats_.named_.downstream_cx_http1_total_.inc();
      stats_.named_.downstream_cx_http1_active_.inc();
    }
  }

  bool redispatch;
  do {
    redispatch = false;

    try {
      // 尝试进行报文分发
      codec_-&gt;dispatch(data);
    } catch (const CodecProtocolException&amp; e) {
      // 分发出现错误
      // 执行到这里，HTTP/1.1编解码器已经发送400状态码，HTTP/2编解码器已经发送GOAWAY
      ENVOY_CONN_LOG(debug, "dispatch error: {}", read_callbacks_-&gt;connection(), e.what());
      stats_.named_.downstream_cx_protocol_error_.inc();

      // 当出现协议错误的情况下，连接上的所有流都需要重置
      resetAllStreams();
      // 刷出写缓冲区、延迟关闭
      read_callbacks_-&gt;connection().close(Network::ConnectionCloseType::FlushWriteAndDelay);
      return Network::FilterStatus::StopIteration;
    }

    // 处理入站数据可能会导致出站数据的释放，这里再次检查
    // 看此连接是否可以在未决编解码数据发送后优雅的关闭
    // 调用Network::ReadFilterCallbacks-&gt;connection().close(Network::ConnectionCloseType::FlushWriteAndDelay)
    checkForDeferredClose();

    // 对于HTTP/1编解码器来说，它会在单个消息完成后，暂停分发
    if (codec_-&gt;protocol() != Protocol::Http2) {
       // 如果没有额外流并且还有更多数据，执行重分发
      if (read_callbacks_-&gt;connection().state() == Network::Connection::State::Open &amp;&amp;
          data.length() &gt; 0 &amp;&amp; streams_.empty()) {
        redispatch = true;
      }
      // 如果仅有单个已经完成请求处理但未应答的非WebSockert流，则暂停套接字读，以apply back pressure
      if (!streams_.empty() &amp;&amp; streams_.front()-&gt;state_.remote_complete_) {
        read_callbacks_-&gt;connection().readDisable(true);
      }
    }
  } while (redispatch);

  return Network::FilterStatus::StopIteration;
}</pre>
<p>注意ConnectionManagerImpl总是会终止网络过滤器的迭代过程。</p>
<div class="blog_h3"><span class="graybg">创建编解码器</span></div>
<pre class="crayon-plain-tag">Http::ServerConnectionPtr
HttpConnectionManagerConfig::createCodec(Network::Connection&amp; connection,
                                         const Buffer::Instance&amp; data,
                                         Http::ServerConnectionCallbacks&amp; callbacks) {
  // 根据协议类型创建适当的HTTP编解码器
  switch (codec_type_) {
  case CodecType::HTTP1:
    return Http::ServerConnectionPtr{
        new Http::Http1::ServerConnectionImpl(connection, callbacks, http1_settings_)};
  case CodecType::HTTP2:
    return Http::ServerConnectionPtr{new Http::Http2::ServerConnectionImpl(
        connection, callbacks, context_.scope(), http2_settings_, maxRequestHeadersKb())};
  case CodecType::AUTO:
    return Http::ConnectionManagerUtility::autoCreateCodec(connection, data, callbacks,
                                                           context_.scope(), http1_settings_,
                                                           http2_settings_, maxRequestHeadersKb());
  }

  NOT_REACHED_GCOVR_EXCL_LINE;
}</pre>
<p>默认情况下，走的是CodecType::AUTO分支：</p>
<pre class="crayon-plain-tag">ServerConnectionPtr ConnectionManagerUtility::autoCreateCodec(
    Network::Connection&amp; connection, const Buffer::Instance&amp; data,
    ServerConnectionCallbacks&amp; callbacks, Stats::Scope&amp; scope, const Http1Settings&amp; http1_settings,
    const Http2Settings&amp; http2_settings, const uint32_t max_request_headers_kb) {
  // 基于协议探测+应用层协议协商（ALPN）来确定下一个L7协议
  // Http2::ALPN_STRING == "h2"，是HTTP/2在ALPN中的代号
  if (determineNextProtocol(connection, data) == Http2::ALPN_STRING) {
    // 使用HTTP/2协议
    return ServerConnectionPtr{new Http2::ServerConnectionImpl(
        connection, callbacks, scope, http2_settings, max_request_headers_kb)};
  } else {
    // 使用HTTP/1协议
    return ServerConnectionPtr{
        new Http1::ServerConnectionImpl(connection, callbacks, http1_settings)};
  }
}</pre>
<p>HTTP协议的版本不同，则ServerConnection的实际类型不同，对于HTTP/1来说，调用Http1::ServerConnectionImpl的构造函数：</p>
<pre class="crayon-plain-tag">ServerConnectionImpl::ServerConnectionImpl(Network::Connection&amp; connection,
                                           ServerConnectionCallbacks&amp; callbacks,
                                           Http1Settings settings)
    : ConnectionImpl(connection, HTTP_REQUEST), callbacks_(callbacks), codec_settings_(settings) {}</pre>
<p>这个函数没什么好说的，它的初始化列表中创建了ConnectionImpl，这是http::Connection的实现：</p>
<pre class="crayon-plain-tag">ConnectionImpl::ConnectionImpl(Network::Connection&amp; connection, http_parser_type type)
    // L2 Connection对象       支持水位的缓冲                    低水位回调
    : connection_(connection), output_buffer_([&amp;]() -&gt; void { this-&gt;onBelowLowWatermark(); },
    //                                                        高水位回调
                                              [&amp;]() -&gt; void { this-&gt;onAboveHighWatermark(); }) {
  // 设置水位，低水位为入参的1/2，高水位为入参
  output_buffer_.setWatermarks(connection.bufferLimit());
  // 初始化HTTP报文解析器
  http_parser_init(&amp;parser_, type);
  parser_.data = this;
}</pre>
<p>http_parser_init来自Node.js项目：</p>
<pre class="crayon-plain-tag">请求头void
http_parser_init (http_parser *parser, enum http_parser_type t)
{
  void *data = parser-&gt;data;
  memset(parser, 0, sizeof(*parser));
  parser-&gt;data = data;
  parser-&gt;type = t;
  parser-&gt;state = (t == HTTP_REQUEST ? s_start_req : (t == HTTP_RESPONSE ? s_start_res : s_start_req_or_res));
  parser-&gt;http_errno = HPE_OK;
}</pre>
<p>此解析器比较严格，如果你的应用程序的HTTP报文头不符合规范可能导致无法解析。 </p>
<div class="blog_h3"><span class="graybg">HTTP1数据分发</span></div>
<p>HTTP连接管理器会调用ServerConnection的dispatch方法进行数据分发，后者从http1::ConnectionImpl继承的dispatch逻辑如下：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::dispatch(Buffer::Instance&amp; data) {
  ENVOY_CONN_LOG(trace, "parsing {} bytes", connection_, data.length());

  // 是否可以直接分发，仅对于Upgrade请求返回true
  if (maybeDirectDispatch(data)) {
    return;
  }

  // 总是尝试将解析器从暂停中恢复
  http_parser_pause(&amp;parser_, 0);

  ssize_t total_parsed = 0;
  if (data.length() &gt; 0) {
    // 获取原始缓冲切片，第一个参数是切片数组，第二个为数组大小，返回值是实际需要的切片数
    // 第一次调用就是为了获得实际需要的切片数
    uint64_t num_slices = data.getRawSlices(nullptr, 0);
    // #define STACK_ARRAY(var, type, num) StackArray&lt;type&gt; var(::alloca(sizeof(type) * num), num)
    // 在栈上创建数组变量slices
    STACK_ARRAY(slices, Buffer::RawSlice, num_slices);
    // 将带解析数据分到切片中
    data.getRawSlices(slices.begin(), num_slices);
    // 逐个处理切片
    for (const Buffer::RawSlice&amp; slice : slices) {
    //                              获取切片的裸数据，传递给HTTP解析器
      total_parsed += dispatchSlice(static_cast&lt;const char*&gt;(slice.mem_), slice.len_);
    }
  } else {
    dispatchSlice(nullptr, 0);
  }
  // 解析完毕，分发完毕，对应的Envoy解码也完毕
  ENVOY_CONN_LOG(trace, "parsed {} bytes", connection_, total_parsed);
  // 排干已经解析的数据
  data.drain(total_parsed);

  // 如果Upgrade请求已经被处理，并且存在：
  // 1、请求体数据
  // 2、或者early upgrade载荷
  // 需要发送，则发送之
  maybeDirectDispatch(data);
}</pre>
<p>从上面的代码我们看到，HTTP请求数据是划分为切片，逐个切片进行解析的：</p>
<pre class="crayon-plain-tag">// 切片内容示例
// GET /healthz HTTP/1.1\r\nUser-Agent: curl/7.35.0\r\nAccept: */*\r\nHost: podinfo-canary.default.svc.k8s.gmem.cc\r\n\r\n
size_t ConnectionImpl::dispatchSlice(const char* slice, size_t len) {
  ssize_t rc = http_parser_execute(&amp;parser_, &amp;settings_, slice, len);
  // 解析失败则抛出异常
  if (HTTP_PARSER_ERRNO(&amp;parser_) != HPE_OK &amp;&amp; HTTP_PARSER_ERRNO(&amp;parser_) != HPE_PAUSED) {
    sendProtocolError();
    throw CodecProtocolException("http/1.1 protocol error: " + std::string(http_errno_name(HTTP_PARSER_ERRNO(&amp;parser_))));
  }

  return rc;
}</pre>
<div class="blog_h3"><span class="graybg">HTTP1数据解析</span></div>
<p>注意：由于HTTP1不支持多路复用，因此请求解析结果信息都是以Http::Http1::ConnectionImpl的实例变量的形式存放的。</p>
<p>http_parser_execute的实现细节我们不去深究，这里主要关注一下settings_，其类型为：</p>
<pre class="crayon-plain-tag">struct http_parser_settings {
  // 在解析了HTTP报文的各个部分之后，执行对应的回调
  http_cb      on_message_begin;
  http_data_cb on_url;
  http_data_cb on_status;
  http_data_cb on_header_field;
  http_data_cb on_header_value;
  http_cb      on_headers_complete;
  http_data_cb on_body;
  http_cb      on_message_complete;
  // 调用on_chunk_header时当前chunk的长度存放在 parser-&gt;content_length
  http_cb      on_chunk_header;
  http_cb      on_chunk_complete;
};</pre>
<p>Envoy提供的settings_变量如下，注意ConnectionImpl对象调用了HTTP解析器，并且把自身传递给parser.data：</p>
<pre class="crayon-plain-tag">http_parser_settings ConnectionImpl::settings_{
    [](http_parser* parser) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onMessageBeginBase();
      return 0;
    },
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onUrl(at, length);
      return 0;
    },
    nullptr, // on_status
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onHeaderField(at, length);
      return 0;
    },
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onHeaderValue(at, length);
      return 0;
    },
    [](http_parser* parser) -&gt; int {
      return static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onHeadersCompleteBase();
    },
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onBody(at, length);
      return 0;
    },
    [](http_parser* parser) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onMessageCompleteBase();
      return 0;
    },
    nullptr, // on_chunk_header
    nullptr  // on_chunk_complete
};</pre>
<p>最初被回调的是onMessageBeginBase方法，表示开始解析HTTP报文了：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onMessageBeginBase() {
  ENVOY_CONN_LOG(trace, "message begin", connection_);
  ASSERT(!current_header_map_);
  // HeaderMapImpl是为性能高度优化的Http::HeaderMap实现，尽量避免拷贝和内存分配
  current_header_map_ = std::make_unique&lt;HeaderMapImpl&gt;();
  // 解析状态，Field / Value / Done
  header_parsing_state_ = HeaderParsingState::Field;
  onMessageBegin();
}

void ServerConnectionImpl::onMessageBegin() {
  if (!resetStreamCalled()) {
    // 如果没有进行流重置，则初始化当前ActiveRequest对象
    ASSERT(!active_request_);
    active_request_ = std::make_unique&lt;ActiveRequest&gt;(*this);
    //               StreamDecoder                                        ResponseStreamEncoderImpl
    active_request_-&gt;request_decoder_ = &amp;callbacks_.newStream(active_request_-&gt;response_encoder_);
  }
}</pre>
<p>解析出URL路径后，回调：</p>
<pre class="crayon-plain-tag">void ServerConnectionImpl::onUrl(const char* data, size_t length) {
  if (active_request_) {
    active_request_-&gt;request_url_.append(data, length);
  }
}</pre>
<p>为请求设置请求URL的路径部分，例如 /healthz。</p>
<p>解析完每个请求头后，依次回调onHeaderField、onHeaderValue方法：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onHeaderField(const char* data, size_t length) {
  if (header_parsing_state_ == HeaderParsingState::Done) {
    // 忽略 trailers
    return;
  }
  // 解析请求值后，下面的判断为true
  if (header_parsing_state_ == HeaderParsingState::Value) {
    // 完成上一个请求头的处理
    completeLastHeader();
  }

  // 暂存到一个缓冲区中
  current_header_field_.append(data, length);
}

void ConnectionImpl::onHeaderValue(const char* data, size_t length) {
  if (header_parsing_state_ == HeaderParsingState::Done) {
    // 忽略 trailers
    return;
  }
  // 设置头解析状态
  header_parsing_state_ = HeaderParsingState::Value;
  // 暂存到一个缓冲区中
  current_header_value_.append(data, length);
} </pre>
<p>在解析完最后一个请求头后会执行：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::completeLastHeader() {
  if (!current_header_field_.empty()) {
    toLowerTable().toLowerCase(current_header_field_.buffer(), current_header_field_.size());
    // 存储到请求头映射中
    current_header_map_-&gt;addViaMove(std::move(current_header_field_),
                                    std::move(current_header_value_));
  }
  // 设置头解析状态
  header_parsing_state_ = HeaderParsingState::Field;
  // 由于std::move的移动语义 HeaderString 变成一个空壳子
  ASSERT(current_header_field_.empty());
  ASSERT(current_header_value_.empty());
}</pre>
<p>完成全部请求头的处理后，回调onHeadersCompleteBase：</p>
<pre class="crayon-plain-tag">int ConnectionImpl::onHeadersCompleteBase() {
  // 将最后一个请求头加入映射
  completeLastHeader();
  if (!(parser_.http_major == 1 &amp;&amp; parser_.http_minor == 1)) {
    // 如果不是HTTP/1.1，则设置协议
    protocol_ = Protocol::Http10;
  }
  if (Utility::isUpgrade(*current_header_map_)) {
    // 根据请求头判定是否客户端在请求升级协议
    handling_upgrade_ = true;
  }

  // 移动请求头映射
  int rc = onHeadersComplete(std::move(current_header_map_));
  current_header_map_.reset();
  // 设置请求头解析状态
  header_parsing_state_ = HeaderParsingState::Done;

  // 返回2，提示http_parser不去期望请求体和更多的信息
  return handling_upgrade_ ? 2 : rc;
}


int ServerConnectionImpl::onHeadersComplete(HeaderMapImplPtr&amp;&amp; headers) {
  // 需要处理响应比请求完成发生更早的情况，这种情况可能由上层代码导致
  if (active_request_) {
    const char* method_string = http_method_str(static_cast&lt;http_method&gt;(parser_.method));

    // 如果请求使用HEAD方法，则给与响应编码器以提示，便于它正确设置内容长度、传输编码等头字段
    active_request_-&gt;response_encoder_.isResponseToHeadRequest(parser_.method == HTTP_HEAD);

    // 当前CONNECT方法是不支持的，但是http_parser_parse_url需要知晓CONNECT
    handlePath(*headers, parser_.method);
    ASSERT(active_request_-&gt;request_url_.empty());
    // 添加Method头
    headers-&gt;insertMethod().value(method_string, strlen(method_string));

    // 判断请求体是否存在，这里使用新的RFC语义来判断 ——  content-length头、hunked transfer-encoding头存在
    // 意味着请求体存在 —— 而不是基于HTTP方法判断
    // 如果没有请求体，延迟对StreamDecoder.decodeHeaders()的调用，直到http解析器flush（回调onMessageComplete）
    if (parser_.flags &amp; F_CHUNKED || (parser_.content_length &gt; 0 &amp;&amp; parser_.content_length != ULLONG_MAX) || handling_upgrade_) {
      // 没有请求体，立即解码请求头
      active_request_-&gt;request_decoder_-&gt;decodeHeaders(std::move(headers), false);

      // If the connection has been closed (or is closing) after decoding headers, pause the parser
      // so we return control to the caller.
      if (connection_.state() != Network::Connection::State::Open) {
        http_parser_pause(&amp;parser_, 1);
      }

    } else {
      // 移动以便延迟解码请求头
      deferred_end_stream_headers_ = std::move(headers);
    }
  }

  return 0;
}</pre>
<p>完成整个请求处理后，回调onMessageCompleteBase： </p>
<pre class="crayon-plain-tag">void ConnectionImpl::onMessageCompleteBase() {
  if (handling_upgrade_) {
    // 如果当前是Upgrade请求则不调用onMessageComplete
    // Upgrade载荷将作为流的体看待
    ASSERT(!deferred_end_stream_headers_);
    // 暂停解析
    http_parser_pause(&amp;parser_, 1);
    return;
  }
  onMessageComplete();
}

void ServerConnectionImpl::onMessageComplete() {
  if (active_request_) {
    Buffer::OwnedImpl buffer;
    // 提示请求端消息处理完毕
    active_request_-&gt;remote_complete_ = true;

    // 如果延迟了请求头解码，这里进行解码
    if (deferred_end_stream_headers_) {
      active_request_-&gt;request_decoder_-&gt;decodeHeaders(std::move(deferred_end_stream_headers_),
                                                       true);
      deferred_end_stream_headers_.reset();
    } else {
    // 否则，解码数据
      active_request_-&gt;request_decoder_-&gt;decodeData(buffer, true);
    }
  }

  // 总是暂停HTTP解析器，这样调用者同时只能处理单个请求，从而施加反向压力（apply back pressure）
  // 调用者需要检测缓冲中有更多的数据，并进行再次分发
  http_parser_pause(&amp;parser_, 1);
}</pre>
<div class="blog_h3"><span class="graybg">HTTP请求头解码 </span></div>
<p>经过上一节的分析，我们了解到，当HTTP解析器处理完请求后，会调用ServerConnectionImpl::onMessageComplete回调，该回调则会调用ActiveStream（实现了StreamDecoder）进行请求解码。</p>
<p>这个请求解码是Envoy上下文的，它会执行Envoy的核心代理逻辑 —— 遍历HTTP过滤器链、进行路由选择：</p>
<pre class="crayon-plain-tag">// 该函数的逻辑顺序很复杂，但也很重要
//
// 我们希望在选路之前做尽量少的工作，并且创建一个过滤器链来最大化需要定制过滤器行为—— 例如注册访问日志器 ——的请求数量
// 要达成目标，需要在以下几个事项之间进行权衡：
// 1、对无效请求进行合法性检查，因为无效请求可能因为没有完整的头信息而无法选路
// 2、检查服务器错误响应（连接关闭、HEAD请求...）所需要的状态
// 3、过滤器对请求本身的、可能影响选路的修改
//
void ConnectionManagerImpl::ActiveStream::decodeHeaders(HeaderMapPtr&amp;&amp; headers, bool end_stream) {
  // 移动请求头为ActiveStream的字段
  request_headers_ = std::move(headers);
  if (Http::Headers::get().MethodValues.Head == request_headers_-&gt;Method()-&gt;value().c_str()) {
    // 判断是否HEAD请求
    is_head_request_ = true;
  }
  ENVOY_STREAM_LOG(debug, "request headers complete (end_stream={}):\n{}", *this, end_stream, *request_headers_);

  // 如果请求仅有请求头（header-only，没有体），则在此可以结束解码流程
  // 如果我们将请求转换为header-only，则一旦后续的decodeData/decodeTrailers被调用则当前流就被标记为完成
  // 下面的方法设置remote_complete_ = end_stream
  maybeEndDecode(end_stream);

  // 如果过载了，只要解码了请求头，就丢弃请求
  // 连接管理器是为当前L4连接服务的，它是一个网络过滤器。当出现过载后，其overload_stop_accepting_requests_ref_ == Active
  if (connection_manager_.overload_stop_accepting_requests_ref_ == Server::OverloadActionState::Active) {
    // 在此特殊分支下，不去创建过滤器链 —— 如果存在内存过载风险更重要的是避免内存分配，而非创建过滤器
    // 标记为过滤器已创建
    state_.created_filter_chain_ = true;
    connection_manager_.stats_.named_.downstream_rq_overload_close_.inc();
    // 由Envoy直接应答下游
    sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_),
                   Http::Code::ServiceUnavailable, "envoy overloaded", nullptr, is_head_request_,
                   absl::nullopt);
    return;
  }
  //                               是否Envoy需要代理Expect: 100-Continue
  if (!connection_manager_.config_.proxy100Continue() &amp;&amp; request_headers_-&gt;Expect() &amp;&amp;
      request_headers_-&gt;Expect()-&gt;value() == Headers::get().ExpectValues._100Continue.c_str()) {
    // 执行到这里意味着Envoy在处理100-Continue，跳过过滤器链，直接发送100-Continue给编码器
    // 100-Continue用于客户端在发送POST数据给服务器前，征询服务器情况，看服务器是否处理POST的数据，
    // 如果不处理，客户端则不上传POST数据，如果处理，则POST上传数据。在现实应用中，通常在POST大数据时，
    // 才会使用100-continue协议
    // 服务器端的行为应该是：返回100-Continue表示自己能够处理POST数据，或者错误码

    // 执行一些统计指标收集
    chargeStats(continueHeader());

    // 执行响应编码
    response_encoder_-&gt;encode100ContinueHeaders(continueHeader());
    // 移除Expect头，防止在上游再次处理
    request_headers_-&gt;removeExpect();
  }

  // 从请求头中读取UserAgent —— 针对特定user agent的统计指标
  connection_manager_.user_agent_.initializeFromHeaders(
      *request_headers_, connection_manager_.stats_.prefix_, connection_manager_.stats_.scope_);

  // 确保codec版本（HTTP协议版本）是支持的
  Protocol protocol = connection_manager_.codec_-&gt;protocol();
  if (protocol == Protocol::Http10) {
    // 这种情况下，HTTP/1.x中除了1.1都可以
    stream_info_.protocol(protocol);
    if (!connection_manager_.config_.http1Settings().accept_http_10_) {
      // 如果配置中没有显式支持HTTP/1.0，发送Envoy本地响应Upgrade Required
      sendLocalReply(false, Code::UpgradeRequired, "", nullptr, is_head_request_, absl::nullopt);
      return;
    } else {
      // HTTP/1.0 默认不支持连接复用，除非请求头指定Keep-Alive，需要保证连接关闭
      state_.saw_connection_close_ = true;
      if (request_headers_-&gt;Connection() &amp;&amp;
          absl::EqualsIgnoreCase(request_headers_-&gt;Connection()-&gt;value().getStringView(),
                                 Http::Headers::get().ConnectionValues.KeepAlive)) {
        state_.saw_connection_close_ = false;
      }
    }
  }
  // 如果缺少Host头
  if (!request_headers_-&gt;Host()) {
    if ((protocol == Protocol::Http10) &amp;&amp; !connection_manager_.config_.http1Settings().default_host_for_http_10_.empty()) {
      // 当前是HTTP10且配置了缺省Host头，则设置此头
      request_headers_-&gt;insertHost().value(connection_manager_.config_.http1Settings().default_host_for_http_10_);
    } else {
      // 非HTTP10，必须有Host头，对于HTTP11来说Host头重命名为:authority
      // Envoy本地应答
      sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_), Code::BadRequest, "", nullptr, is_head_request_, absl::nullopt);
      return;
    }
  }
  
  // 处理请求头部过长的情况
  ASSERT(connection_manager_.config_.maxRequestHeadersKb() &gt; 0);
  if (request_headers_-&gt;byteSize() &gt; (connection_manager_.config_.maxRequestHeadersKb() * 1024)) {
    sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_),
                   Code::RequestHeaderFieldsTooLarge, "", nullptr, is_head_request_, absl::nullopt);
    return;
  }

  // 当前在应用层，我们仅仅支持相对路径。在这里预期codec已经把路径打散成片
  // 注意：目前HTTP11 codec仅在allow_absolute_url标记启用的情况下才进行打散操作
  //  我们也会检查:path头，因为CONNECT请求没有URL路径，而当前不支持CONNECT请求
  if (!request_headers_-&gt;Path() || request_headers_-&gt;Path()-&gt;value().c_str()[0] != '/') {
    connection_manager_.stats_.named_.downstream_rq_non_relative_path_.inc();
    sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_), Code::NotFound, "", nullptr,
                   is_head_request_, absl::nullopt);
    return;
  }

  // 对于HTTP11，如果请求头Connection: Close，表示不启用keep-Alive
  if (protocol == Protocol::Http11 &amp;&amp; request_headers_-&gt;Connection() &amp;&amp;
      absl::EqualsIgnoreCase(request_headers_-&gt;Connection()-&gt;value().getStringView(),
                             Http::Headers::get().ConnectionValues.Close)) {
    // 那么意味着客户端已经关闭连接
    state_.saw_connection_close_ = true;
  }

  // 如果当前请求不是内部重定向
  if (!state_.is_internally_created_) { // Only sanitize headers on first pass.
    // 根据配置、请求头来修改下游连接的远程地址（客户端地址）
    // 日志目的
    stream_info_.setDownstreamRemoteAddress(ConnectionManagerUtility::mutateRequestHeaders(
        *request_headers_, connection_manager_.read_callbacks_-&gt;connection(),
        connection_manager_.config_, *snapped_route_config_, connection_manager_.random_generator_,
        connection_manager_.runtime_, connection_manager_.local_info_));
  }
  ASSERT(stream_info_.downstreamRemoteAddress() != nullptr);

  ASSERT(!cached_route_);
  // 刷新缓存的路由（条目），可能设置cached_cluster_info_ —— 目标上游集群信息，意味着选路可能完成
  refreshCachedRoute();
  const bool upgrade_rejected = createFilterChain() == false;

  // TODO 如果在准备过滤器迭代时，发现链中没有任何过滤器，连接管理器应该返回404，当前实现时不返回响应
  if (protocol == Protocol::Http11 &amp;&amp; cached_route_.value()) {
    if (upgrade_rejected) {
      // 当前路由不支持升级，因此发送Envoy本地响应
      connection_manager_.stats_.named_.downstream_rq_ws_on_non_ws_route_.inc();
      sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_), Code::Forbidden, "",
                     nullptr, is_head_request_, absl::nullopt);
      return;
    }
    // 允许WebSocket请求穿过启用了WebSocket支持的路由
  }

  // 如果有路由，且路由配置了idle超时
  if (cached_route_.value()) {
    const Router::RouteEntry* route_entry = cached_route_.value()-&gt;routeEntry();
    if (route_entry != nullptr &amp;&amp; route_entry-&gt;idleTimeout()) {
      idle_timeout_ms_ = route_entry-&gt;idleTimeout().value();
      if (idle_timeout_ms_.count()) {
        // 如果流超时定时器没创建，则创建之
        if (stream_idle_timer_ == nullptr) {
          stream_idle_timer_ =
              connection_manager_.read_callbacks_-&gt;connection().dispatcher().createTimer(
                  [this]() -&gt; void { onIdleTimeout(); });
        }
      } else if (stream_idle_timer_ != nullptr) {
        // 如果存在流超时定时器，但是路由的idle超时为0，则禁用定时器
        stream_idle_timer_ = nullptr;
      }
    }
  }

  // 进行请求追踪
  if (connection_manager_.config_.tracingConfig()) {
    traceRequest();
  }

  // 解码请求头
  decodeHeaders(nullptr, *request_headers_, end_stream);

  // 重置超时定时器
  resetIdleTimer();
}</pre>
<p>请求头的解码逻辑位于decodeHeaders方法中，上面的方法传入的第一个参数是nullptr，表示从头开始迭代过滤器链：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::decodeHeaders(ActiveStreamDecoderFilter* filter,
                                                        HeaderMap&amp; headers, bool end_stream) {
  // 从头，或者从指定过滤器开始迭代
  std::list&lt;ActiveStreamDecoderFilterPtr&gt;::iterator entry;
  std::list&lt;ActiveStreamDecoderFilterPtr&gt;::iterator continue_data_entry = decoder_filters_.end();
  if (!filter) {
    entry = decoder_filters_.begin();
  } else {
    entry = std::next(filter-&gt;entry());
  }

  // 遍历剩下的过滤器
  for (; entry != decoder_filters_.end(); entry++) {
    ASSERT(!(state_.filter_call_state_ &amp; FilterCallState::DecodeHeaders));
    // 设置状态位
    state_.filter_call_state_ |= FilterCallState::DecodeHeaders;
    (*entry)-&gt;end_stream_ =
    // 仅仅解码请求头，或者传入end_stream=true（表示这是header-only的请求）
        decoding_headers_only_ || (end_stream &amp;&amp; continue_data_entry == decoder_filters_.end());
    // 调用过滤器来解码请求头，返回的状态决定后续流程走向
    FilterHeadersStatus status = (*entry)-&gt;decodeHeaders(headers, (*entry)-&gt;end_stream_);
    // ContinueAndEndStream表示继续迭代后续过滤器，但是忽略后续的请求体、尾 —— 这意味着创建header-only请求/应答
    ASSERT(!(status == FilterHeadersStatus::ContinueAndEndStream &amp;&amp; (*entry)-&gt;end_stream_));
    // 清空状态位
    state_.filter_call_state_ &amp;= ~FilterCallState::DecodeHeaders;
    ENVOY_STREAM_LOG(trace, "decode headers called: filter={} status={}", *this,
                     static_cast&lt;const void*&gt;((*entry).get()), static_cast&lt;uint64_t&gt;(status));

    // 处理请求头的回调被调用后的通用处理逻辑：
    // 根据status设置ActiveStream的一些字段，例如stopped_、headers_only、headers_continued_
    // 只有返回true，才可能继续迭代
    if (!(*entry)-&gt;commonHandleAfterHeadersCallback(status, decoding_headers_only_) &amp;&amp;
        std::next(entry) != decoder_filters_.end()) {
      // 如果当前不是最后一个过滤器，停止迭代。否则，还需要继续处理先前过滤器添加了体的情况
      return;
    }

    // 这里处理特殊的情况：我们使用header-only请求，但是前面的过滤器填充了请求体
    // 这意味着不能在内联迭代（inline iteration）阶段再向后面的过滤器传递end_stream = true了
    if (end_stream &amp;&amp; buffered_request_data_ &amp;&amp; continue_data_entry == decoder_filters_.end()) {
      // 设置下一个执行的过滤器（为当前过滤器）
      continue_data_entry = entry;
    }
  }

  if (continue_data_entry != decoder_filters_.end()) {
    // 从当前过滤器继续迭代，调用continueDecoding()以防再调用decodeHeaders()
    ASSERT(buffered_request_data_);
    // 仿冒stopped_ = true，因为continueDecoding() 期望之
    (*continue_data_entry)-&gt;stopped_ = true;
    // 使用缓冲的请求头、体数据继续迭代
    (*continue_data_entry)-&gt;continueDecoding();
  }

  if (end_stream) {
    // 解除超时过滤器
    disarmRequestTimeout();
  }
}</pre>
<p>单个过滤器解码请求头的逻辑由ActiveStreamDecoderFilter.decodeHeaders提供：</p>
<pre class="crayon-plain-tag">FilterHeadersStatus decodeHeaders(HeaderMap&amp; headers, bool end_stream) {
  is_grpc_request_ = Grpc::Common::hasGrpcContentType(headers);
  return handle_-&gt;decodeHeaders(headers, end_stream);
}</pre>
<p>可以看到，它只是判断一下是否gRPC请求，然后就转交给 StreamDecoderFilter handle_，这个handle是一个个具体的HTTP过滤器。</p>
<p>HTTP过滤器可能对请求头进行任意的操作，例如修改某个头，最终它会返回下面的枚举值之一：</p>
<pre class="crayon-plain-tag">enum class FilterHeadersStatus {
  // 继续迭代下一个过滤器
  Continue,
  // 不再迭代后续过滤器
  StopIteration,
  // 继续迭代下一个过滤器，但是不忽略报文体、尾，也就是创建header-only的请求/响应
  ContinueAndEndStream
};</pre>
<p>返回值会影响如何进行后续的过滤器迭代。</p>
<div class="blog_h3"><span class="graybg">创建过滤器链</span></div>
<p>此方法考虑了协议升级的情况：</p>
<pre class="crayon-plain-tag">bool ConnectionManagerImpl::ActiveStream::createFilterChain() {
  // 过滤器链已经创建则返回，HTTP过滤器链只有一个（相对于单个HTTP连接管理器），而不像网络过滤器，可以有多个
  if (state_.created_filter_chain_) {
    return false;
  }
  bool upgrade_rejected = false;
  // 升级的目标协议
  auto upgrade = request_headers_ ? request_headers_-&gt;Upgrade() : nullptr;
  // 标记为过滤器已创建
  state_.created_filter_chain_ = true;
  if (upgrade != nullptr) {
    // 需要进行协议升级判断
    const Router::RouteEntry::UpgradeMap* upgrade_map = nullptr;
    // 设置UpgradeMap，包含路由条目支持的升级协议信息
    if (cached_route_.has_value() &amp;&amp; cached_route_.value() &amp;&amp; cached_route_.value()-&gt;routeEntry()) {
      upgrade_map = &amp;cached_route_.value()-&gt;routeEntry()-&gt;upgradeMap();
    }
    // 创建升级的过滤器链
    if (connection_manager_.config_.filterFactory().createUpgradeFilterChain( upgrade-&gt;value().c_str(), upgrade_map, *this)) {
      state_.successful_upgrade_ = true;
      connection_manager_.stats_.named_.downstream_cx_upgrades_total_.inc();
      connection_manager_.stats_.named_.downstream_cx_upgrades_active_.inc();
      return true;
    } else {
      upgrade_rejected = true;
      // 失败，退化为默认过滤器链，调用者将会发送Envoy本地响应提示升级失败
    }
  }
  // 创建默认过滤器链
  connection_manager_.config_.filterFactory().createFilterChain(*this);
  return !upgrade_rejected;
}</pre>
<p>默认过滤器链在下面的方法中创建：</p>
<pre class="crayon-plain-tag">void HttpConnectionManagerConfig::createFilterChain(Http::FilterChainFactoryCallbacks&amp; callbacks) {
  for (const Http::FilterFactoryCb&amp; factory : filter_factories_) {
    factory(callbacks);
  }
}</pre>
<p>可以看到，和网络过滤器一样的模式，调用各过滤器提供的工厂，传输FilterChainFactoryCallbacks。 </p>
<div class="blog_h2"><span class="graybg">和HTTP上游集群交互 </span></div>
<div class="blog_h3"><span class="graybg">HTTP路由处理</span></div>
<p>最后一个HTTP过滤器通常都是Envoy::Router::Filter，此过滤器决定如何转发下游请求给上游集群。毕竟Envoy只是个代理，它不负责实质性的请求处理。 </p>
<pre class="crayon-plain-tag">Http::FilterHeadersStatus Filter::decodeHeaders(Http::HeaderMap&amp; headers, bool end_stream) {
  // 确保向上游发送的出站请求需要的HTTP/2头都存在
  ASSERT(headers.Path());
  ASSERT(headers.Method());
  ASSERT(headers.Host());

  // 来自下游的头
  downstream_headers_ = &amp;headers;

  // 是否为gRPC请求
  grpc_request_ = Grpc::Common::hasGrpcContentType(headers);

  // 增加rq_total计数
  config_.stats_.rq_total_.inc();

  // 获取路由
  route_ = callbacks_-&gt;route();
  if (!route_) {
    // 增加no_route计数
    config_.stats_.no_route_.inc();
    ENVOY_STREAM_LOG(debug, "no cluster match for URL '{}'", *callbacks_, headers.Path()-&gt;value().c_str());
    // 记录没有路由这一情况到StreamInfo
    callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoRouteFound);
    // 设置本地响应
    callbacks_-&gt;sendLocalReply(Http::Code::NotFound, "", nullptr, absl::nullopt);
    return Http::FilterHeadersStatus::StopIteration;
  }

  // 如果有请求的直接响应，则返回直接响应，否则返回nullptr
  // 直接响应即Envoy自己生成的响应，而非代理上游集群的
  const auto* direct_response = route_-&gt;directResponseEntry();
  if (direct_response != nullptr) {
    config_.stats_.rq_direct_response_.inc();
    // 重写Path头
    direct_response-&gt;rewritePathHeader(headers, !config_.suppress_envoy_headers_);
    // 发送本地响应
    callbacks_-&gt;sendLocalReply(
        // 使用直接响应的头、体
        direct_response-&gt;responseCode(), direct_response-&gt;responseBody(),
        // 修改响应头的Lambda
        [this, direct_response,
         &amp;request_headers = headers](Http::HeaderMap&amp; response_headers) -&gt; void {
          // 基于请求头得到重定向路径
          const auto new_path = direct_response-&gt;newPath(request_headers);
          if (!new_path.empty()) {
            // 添加头
            response_headers.addReferenceKey(Http::Headers::get().Location, new_path);
          }
          // 在转发之前，进行可能是破坏性的响应头转换，例如添加/删除头
          // 只能在获取所有初始响应头后调用单次
          direct_response-&gt;finalizeResponseHeaders(response_headers, callbacks_-&gt;streamInfo());
        },
        absl::nullopt);
    // Router过滤器总是停止迭代
    return Http::FilterHeadersStatus::StopIteration;
  }

  // 匹配请求的路由条目
  route_entry_ = route_-&gt;routeEntry();
  // 从集群管理器cm_中后去路由条目提供的山有集群名称，例如 outbound|9898||podinfo-canary.default.svc.k8s.gmem.cc
  Upstream::ThreadLocalCluster* cluster = config_.cm_.get(route_entry_-&gt;clusterName());
  if (!cluster) {
    // 找不到集群
    config_.stats_.no_cluster_.inc();
    ENVOY_STREAM_LOG(debug, "unknown cluster '{}'", *callbacks_, route_entry_-&gt;clusterName());
    // 记录错误并进行本地应答
    callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoRouteFound);
    callbacks_-&gt;sendLocalReply(route_entry_-&gt;clusterNotFoundResponseCode(), "", nullptr, absl::nullopt);
    return Http::FilterHeadersStatus::StopIteration;
  }
  // 从线程本地的cluster获得ClusterInfo对象，此对象可以安全的超越ThreadLocalCluster的生命周期存在
  cluster_ = cluster-&gt;info();

  // 虚拟上游集群，根据请求路径确定
  request_vcluster_ = route_entry_-&gt;virtualCluster(headers);
  ENVOY_STREAM_LOG(debug, "cluster '{}' match for URL '{}'", *callbacks_, route_entry_-&gt;clusterName(), headers.Path()-&gt;value().c_str());

  // 上游集群的统计指标的备选前缀
  const Http::HeaderEntry* request_alt_name = headers.EnvoyUpstreamAltStatName();
  if (request_alt_name) {
    alt_stat_prefix_ = std::string(request_alt_name-&gt;value().c_str()) + ".";
    headers.removeEnvoyUpstreamAltStatName();
  }

  // 看看是不是应该立即杀死一定比例的、此集群的流量
  // maintenanceMode()返回集群是否出于维护模式，出于此模式则不应该作为路由的目标，过滤器
  // 可以根据自己的需要来处理此调用的返回值。此方法的实现可能引入某种随机性，不会每次返回一致的值
  if (cluster_-&gt;maintenanceMode()) {
    // 上游服务器资源溢出，流需要被重置
    callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamOverflow);
    chargeUpstreamCode(Http::Code::ServiceUnavailable, nullptr, true);
    // 进行本地应答
    callbacks_-&gt;sendLocalReply(Http::Code::ServiceUnavailable, "maintenance mode",
                               [this](Http::HeaderMap&amp; headers) {
                                 if (!config_.suppress_envoy_headers_) {
                                   // 添加Envoy特殊响应头
                                   headers.insertEnvoyOverloaded().value( Http::Headers::get().EnvoyOverloadedValues.True);
                                 }
                               },
                               absl::nullopt);
    cluster_-&gt;stats().upstream_rq_maintenance_mode_.inc();
    return Http::FilterHeadersStatus::StopIteration;
  }

  // 获取上游集群的连接池
  Http::ConnectionPool::Instance* conn_pool = getConnPool();
  if (!conn_pool) {
    // 如果无法得到/创建线程池，所有该集群没有任何可用（健康的）端点
    // 发送本地响应
    sendNoHealthyUpstreamResponse();
    return Http::FilterHeadersStatus::StopIteration;
  }

  /* 开始向上游集群的主机发送请求 */

  // 根据路由配置和请求头来决定实际使用的请求超时时间。请求头中的超时优先级更高
  timeout_ = FilterUtility::finalTimeout(*route_entry_, headers, !config_.suppress_envoy_headers_, grpc_request_);

  // 如果请求头x-envoy-upstream-rq-timeout-alt-response存在，则在请求上游超时后
  if (headers.EnvoyUpstreamRequestTimeoutAltResponse()) {
    // 设置响应码
    timeout_response_code_ = Http::Code::NoContent;
    // 同时移除x-envoy-upstream-rq-timeout-alt-response头
    headers.removeEnvoyUpstreamRequestTimeoutAltResponse();
  }

  // 如果此RouteEntry所属的虚拟主机的配置要求在上游请求中添加x-envoy-attempt-count头，则添加之
  include_attempt_count_ = route_entry_-&gt;includeAttemptCount();
  if (include_attempt_count_) {
    headers.insertEnvoyAttemptCount().value(attempt_count_);
  }

  // 将当前Span的追踪上下文注入到请求头
  callbacks_-&gt;activeSpan().injectContext(headers);
  // 在转发请求前，进行可能是销毁性的请求头转换 —— 例如URL重写、添加额外的头、删除头
  // 此方法必须仅在转发前调用单次
  route_entry_-&gt;finalizeRequestHeaders(headers, callbacks_-&gt;streamInfo(), !config_.suppress_envoy_headers_);

  // 设置Scheme头，HTTP和HTTPS
  FilterUtility::setUpstreamScheme(headers, *cluster_);

  // 重试状态
  retry_state_ = createRetryState(route_entry_-&gt;retryPolicy(), headers, *cluster_, config_.runtime_,
                       config_.random_, callbacks_-&gt;dispatcher(), route_entry_-&gt;priority());
  // 请求是否应该被shadow（镜像）
  do_shadowing_ = FilterUtility::shouldShadow(route_entry_-&gt;shadowPolicy(), config_.runtime_,  callbacks_-&gt;streamId());

  ENVOY_STREAM_LOG(debug, "router decoding headers:\n{}", *callbacks_, headers);
  // 上游请求对象
  upstream_request_ = std::make_unique&lt;UpstreamRequest&gt;(*this, *conn_pool);
  // 上游请求不会在本地走过滤器链，下面的方法仅仅是
  // 1、调用conn_pool_.newStream()创建新的流
  // 2、将新的流赋值给UpstreamRequest.conn_pool_stream_handle_变量
  upstream_request_-&gt;encodeHeaders(end_stream);
  if (end_stream) {
    // 执行此回调，用于上游请求以异步发送的，这里不代表上游请求处理完毕
    // 在Dispatcher上注册超时定时器，在上游请求执行超时后回调onResponseTimeout
    onRequestComplete();
  }

  return Http::FilterHeadersStatus::StopIteration;
}</pre>
<p>如果选择的路由的上游集群没有健康的端点，则会调用：</p>
<pre class="crayon-plain-tag">void Filter::sendNoHealthyUpstreamResponse() {
  callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoHealthyUpstream);
  chargeUpstreamCode(Http::Code::ServiceUnavailable, nullptr, false);
  callbacks_-&gt;sendLocalReply(Http::Code::ServiceUnavailable, "no healthy upstream", nullptr, absl::nullopt);
}</pre>
<p>给与客户端503响应，响应体设置为 no healthy upstream 。</p>
<div class="blog_h3"><span class="graybg">上游集群连接池</span></div>
<p>Envoy和上游集群主机的交互，是通过连接池进行的。每个上游主机对应一个连接池对象，根据协议和配置的不同，连接池中维持的连接数量也不同。对于HTTP/2协议，由于多路复用的关系，不考虑套接字选项的情况下，池中总是只有单个连接。</p>
<p>路由过滤器会调用getConnPool()来获取连接池：</p>
<pre class="crayon-plain-tag">Http::ConnectionPool::Instance* Filter::getConnPool() {
  // 获取集群支持的特性，位域字段
  auto features = cluster_-&gt;features();
  // 根据上游集群的配置、下游连接的类型来决定使用什么协议
  // 根据运行时配置，集群可能将HTTP2降级为HTTP1
  Http::Protocol protocol;
  if (features &amp; Upstream::ClusterInfo::Features::USE_DOWNSTREAM_PROTOCOL) {
    // 如果使用下游的协议
    protocol = callbacks_-&gt;streamInfo().protocol().value();
  } else {
    // 否则，如果上游支持HTTP2则使用之，不支持则HTTP11
    protocol = (features &amp; Upstream::ClusterInfo::Features::HTTP2) ? Http::Protocol::Http2
                                                                   : Http::Protocol::Http11;
  }
  // cm_是集群管理器
  return config_.cm_.httpConnPoolForCluster(route_entry_-&gt;clusterName(), route_entry_-&gt;priority(),
                                            protocol, this);
}</pre>
<p>连接池的管理，实际上由集群管理器负责：</p>
<pre class="crayon-plain-tag">Http::ConnectionPool::Instance*
ClusterManagerImpl::httpConnPoolForCluster(const std::string&amp; cluster, ResourcePriority priority,
                                           Http::Protocol protocol, LoadBalancerContext* context) {
  // 获取线程本地的集群管理器对象
  ThreadLocalClusterManagerImpl&amp; cluster_manager = tls_-&gt;getTyped&lt;ThreadLocalClusterManagerImpl&gt;();
  // 根据名称查找上游集群
  auto entry = cluster_manager.thread_local_clusters_.find(cluster);
  if (entry == cluster_manager.thread_local_clusters_.end()) {
    return nullptr;
  }

  // 委托给上游集群
  return entry-&gt;second-&gt;connPool(priority, protocol, context);
} </pre>
<p>获取连接池的工作进一步委托给上游集群（ClusterEntry）：</p>
<pre class="crayon-plain-tag">Http::ConnectionPool::Instance*
ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::connPool(
    ResourcePriority priority, Http::Protocol protocol, LoadBalancerContext* context) {
  // 根据负载均衡上下文（就是Router这个过滤器），也就是负载均衡策略，来决定使用哪个主机的连接池
  HostConstSharedPtr host = lb_-&gt;chooseHost(context);
  if (!host) {
    ENVOY_LOG(debug, "no healthy host for HTTP connection pool");
    cluster_info_-&gt;stats().upstream_cx_none_healthy_.inc();
    return nullptr;
  }

  // 从下游连接继承套接字选项
  std::vector&lt;uint8_t&gt; hash_key = {uint8_t(protocol), uint8_t(priority)};

  // 基于下游套接字选项来计算连接池的哈希键。以便基于套接字选项来控制连接池，让不同选项的连接不池化在一起
  bool have_options = false;
  if (context &amp;&amp; context-&gt;downstreamConnection()) {
    const Network::ConnectionSocket::OptionsSharedPtr&amp; options =
        context-&gt;downstreamConnection()-&gt;socketOptions();
    if (options) {超时
      for (const auto&amp; option : *options) {
        have_options = true;
        option-&gt;hashKey(hash_key);
      }
    }
  }

  // 获取单个主机的连接池容器
  ConnPoolsContainer&amp; container = *parent_.getHttpConnPoolsContainer(host, true);

  // 根据套接字选项的哈希，从连接池容器中获得连接池
  Http::ConnectionPool::Instance&amp; pool = container.pools_-&gt;getPool(hash_key, [&amp;]() {
    return parent_.parent_.factory_.allocateConnPool(
        parent_.thread_local_dispatcher_, host, priority, protocol,
        have_options ? context-&gt;downstreamConnection()-&gt;socketOptions() : nullptr);
  });

  return &amp;pool;
}</pre>
<div class="blog_h3"><span class="graybg">准备上游请求</span></div>
<p>获得可用的连接池对象后，Router过滤器会创建UpstreamRequest ，并调用它的encodeHeaders方法：</p>
<pre class="crayon-plain-tag">// encodeHeaders不需要变量过滤器链，因为过滤器链是为下游连接服务的
void Filter::UpstreamRequest::encodeHeaders(bool end_stream) {
  ASSERT(!encode_complete_);
  encode_complete_ = end_stream;

  // 创建一个新的流，并赋值给UpstreamRequest.conn_pool_stream_handle_
  // 注意UpstreamRequest实现了StreamDecoder，能够解码上游响应
  Http::ConnectionPool::Cancellable* handle = conn_pool_.newStream(*this, *this);
  if (handle) {
    // 可能在newStream()调用期间发生reset，这种情况下handle为nullptr
    conn_pool_stream_handle_ = handle;
  }
}</pre>
<p>连接池的newStream方法创建一个连接到某个上游主机的新的流：</p>
<pre class="crayon-plain-tag">/**
   * 在连接池上创建一个新的流
   * @param response_decoder 响应解码器 —— 对于上游请求，Router过滤器需要对其返回的应答进行解码
   * @param cb 当连接准备好和失败时执行的回调，如果有可用的连接/出现立即的失败，这些回调可能在当前方法的上下文中直接调用
   *           这种情况下，此函数返回nullptr
   * @return Cancellable* 如果池中没有可用的连接，上述cb不会被立即调用，该方法会返回一个Cancellable类型的handle
   *                      调用者可以使用该句柄来取消请求
   *                      注意：一旦任何回调函数被调用，则句柄不再有效。要取消请求，必须将流重置
   */
  virtual Cancellable* newStream(Http::StreamDecoder&amp; response_decoder, Callbacks&amp; callbacks) PURE;</pre>
<p>上述方法的实现如下：</p>
<pre class="crayon-plain-tag">ConnectionPool::Cancellable* ConnPoolImpl::newStream(StreamDecoder&amp; response_decoder,
                                                     ConnectionPool::Callbacks&amp; callbacks) {
  // 统计指标收集
  host_-&gt;cluster().stats().upstream_rq_total_.inc();
  host_-&gt;stats().rq_total_.inc();
  if (!ready_clients_.empty()) {
    // 如果有可用的客户端，则取出一个放到不可用列表中
    ready_clients_.front()-&gt;moveBetweenLists(ready_clients_, busy_clients_);
    ENVOY_CONN_LOG(debug, "using existing connection", *busy_clients_.front()-&gt;codec_client_);
    // 然后将请求关联到客户端
    attachRequestToClient(*busy_clients_.front(), response_decoder, callbacks);
    return nullptr;
  }

  //                   ResourceManager非完全一致的同步最大连接数、未决请求等信息
  //                                              是否可以创建新的请求
  if (host_-&gt;cluster().resourceManager(priority_).pendingRequests().canCreate()) {
    //                                            是否可以创建新的连接
    bool can_create_connection = host_-&gt;cluster().resourceManager(priority_).connections().canCreate();
    if (!can_create_connection) {
      // 连接总数超标
      host_-&gt;cluster().stats().upstream_cx_overflow_.inc();
    }

    // 如果池中根本没有连接，则立即创建一个防止饥饿
    if ((ready_clients_.size() == 0 &amp;&amp; busy_clients_.size() == 0) || can_create_connection) {
      // 创建新的客户端ActiveClient
      // 将其放入busy_clients_列表
      createNewConnection();
    }
    // 创建请求并排队
    return newPendingRequest(response_decoder, callbacks);
  } else {
    // 超过允许的未决请求的最大数量
    ENVOY_LOG(debug, "max pending requests overflow");
    callbacks.onPoolFailure(ConnectionPool::PoolFailureReason::Overflow, nullptr);
    host_-&gt;cluster().stats().upstream_rq_pending_overflow_.inc();
    return nullptr;
  }
} </pre>
<p>可以看到，如果连接池有空闲的HTTP客户端，则将UpstreamRequest关联到一个空闲连接：</p>
<pre class="crayon-plain-tag">void ConnPoolImpl::attachRequestToClient(ActiveClient&amp; client, StreamDecoder&amp; response_decoder,
                                         ConnectionPool::Callbacks&amp; callbacks) {
  ASSERT(!client.stream_wrapper_);
  // 将UpstreamRequest+ActiveClient封装为流编解码包装器
  client.stream_wrapper_ = std::make_unique&lt;StreamWrapper&gt;(response_decoder, client);
  // 回调onPoolReady：当连接池中有连接能够处理上游请求时执行
  callbacks.onPoolReady(*client.stream_wrapper_, client.real_host_description_);
}

// StreamWrapper的构造函数：
ConnPoolImpl::StreamWrapper::StreamWrapper(StreamDecoder&amp; response_decoder, ActiveClient&amp; parent)
    // CodecClient支持多种HTTP协议类型下的多路流、底层连接的管理
    : StreamEncoderWrapper(parent.codec_client_-&gt;newStream(*this)),
      StreamDecoderWrapper(response_decoder), parent_(parent) {
  // 添加回调
  StreamEncoderWrapper::inner_.getStream().addCallbacks(*this);
}

// 底层请求流
StreamEncoder&amp; CodecClient::newStream(StreamDecoder&amp; response_decoder) {
  // response_decoder即UpstreamRequest
  ActiveRequestPtr request(new ActiveRequest(*this, response_decoder));
  // 创建出站请求流
  request-&gt;encoder_ = &amp;codec_-&gt;newStream(*request);
  request-&gt;encoder_-&gt;getStream().addCallbacks(*request);
  request-&gt;moveIntoList(std::move(request), active_requests_);
  disableIdleTimer();
  return *active_requests_.front()-&gt;encoder_;
}

StreamEncoder&amp; ClientConnectionImpl::newStream(StreamDecoder&amp; response_decoder) {
  if (resetStreamCalled()) {
    throw CodecClientException("cannot create new streams after calling reset");
  }
  // 为连接启用读
  while (!connection_.readEnabled()) {
    connection_.readDisable(false);
  }
  request_encoder_ = std::make_unique&lt;RequestStreamEncoderImpl&gt;(*this);
  // 将UpstreamRequest纳入未决响应列表
  pending_responses_.emplace_back(&amp;response_decoder);
  return *request_encoder_;
}</pre>
<p>反之，如果连接池没有空闲HTTP客户端，则创建PendingRequest并排队：</p>
<pre class="crayon-plain-tag">ConnectionPool::Cancellable* ConnPoolImplBase::newPendingRequest(StreamDecoder&amp; decoder, ConnectionPool::Callbacks&amp; callbacks) {
  ENVOY_LOG(debug, "queueing request due to no available connections");
  // 创建PendingRequest
  PendingRequestPtr pending_request(new PendingRequest(*this, decoder, callbacks));
  // 加入pending_requests_列表，然后返回
  pending_request-&gt;moveIntoList(std::move(pending_request), pending_requests_);
  return pending_requests_.front().get();
}</pre>
<p>排队的请求会在以后，因为某种事件而关联到可用连接。例如新的针对上游主机的L4连接建立后：</p>
<pre class="crayon-plain-tag">// Envoy::Http::Http1::ConnPoolImpl::attachRequestToClient conn_pool.cc:66
// Envoy::Http::Http1::ConnPoolImpl::processIdleClient conn_pool.cc:238
  client.stream_wrapper_.reset();
  if (pending_requests_.empty() || delay) {
    // 没有未决请求，将客户端加入空闲列表
    client.moveBetweenLists(busy_clients_, ready_clients_);
  } else {
    // 绑定请求到客户端
    attachRequestToClient(client, pending_requests_.back()-&gt;decoder_, pending_requests_.back()-&gt;callbacks_);
    pending_requests_.pop_back();
  }
// Envoy::Http::Http1::ConnPoolImpl::onConnectionEvent conn_pool.cc:183
  if (event == Network::ConnectionEvent::Connected) {
    conn_connect_ms_-&gt;complete();
    // 有空闲客户端了，处理之
    processIdleClient(client, false);
  }
// Envoy::Http::Http1::ConnPoolImpl::ActiveClient::onEvent conn_pool.h:89
    void onEvent(Network::ConnectionEvent event) override {
      parent_.onConnectionEvent(*this, event);
    }
// Envoy::Network::ConnectionImpl::raiseEvent connection_impl.cc:329
void ConnectionImpl::raiseEvent(ConnectionEvent event) {
  for (ConnectionCallbacks* callback : callbacks_) {
    callback-&gt;onEvent(event);
  }
  if (state() == State::Open &amp;&amp; event == ConnectionEvent::Connected &amp;&amp; write_buffer_-&gt;length() &gt; 0) {
    onWriteReady();
  }
}
// Envoy::Network::RawBufferSocket::onConnected raw_buffer_socket.cc:83
void RawBufferSocket::onConnected() { callbacks_-&gt;raiseEvent(ConnectionEvent::Connected); }
// Envoy::Network::ConnectionImpl::onWriteReady connection_impl.cc:519
    if (error == 0) {
      ENVOY_CONN_LOG(debug, "connected", *this);
      connecting_ = false;
      transport_socket_-&gt;onConnected();
      ...
// Envoy::Network::ConnectionImpl::onFileEvent connection_impl.cc:467</pre>
<p>到这里为止，我们还没搞清楚，针对上游主机的请求到底是何时、由谁发出去的。实际上这是在Router过滤器的onPoolReady回调中进行的。</p>
<div class="blog_h3"><span class="graybg">发送上游请求</span></div>
<p>不管请求是异步还是同步的关联到HTTP客户端（attachRequestToClient），都会触发onPoolReady。此回调会真正发出请求：</p>
<pre class="crayon-plain-tag">void Filter::UpstreamRequest::onPoolReady(Http::StreamEncoder&amp; request_encoder,
                                          Upstream::HostDescriptionConstSharedPtr host) {
  ENVOY_STREAM_LOG(debug, "pool ready", *parent_.callbacks_);

  // 设置UpstreamRequest.upstream_host_ = host
  // 调用UpstreamRequest、Router的StreamInfo.onUpstreamHostSelected()
  onUpstreamHostSelected(host);
  request_encoder.getStream().addCallbacks(*this);

  // 创建per-try的定时器。per_try_timeout_字段被设置为已启用的定时器
  setupPerTryTimeout();
  conn_pool_stream_handle_ = nullptr;
  // 将StreamWrapper设置为请求编码器
  setRequestEncoder(request_encoder);
  calling_encode_headers_ = true;
  if (parent_.route_entry_-&gt;autoHostRewrite() &amp;&amp; !host-&gt;hostname().empty()) {
    // 如果当前路由条目设置了自动头重写，则使用目标上游主机的名称来覆盖请求头
    parent_.downstream_headers_-&gt;Host()-&gt;value(host-&gt;hostname());
  }
  
  // 注入传递当前追踪需要的头
  if (span_ != nullptr) {
    span_-&gt;injectContext(*parent_.downstream_headers_);
  }

  // 日志用途信息
  stream_info_.onFirstUpstreamTxByteSent();
  parent_.callbacks_-&gt;streamInfo().onFirstUpstreamTxByteSent();
  // 进行请求头编码，调用StreamEncoderWrapper，后者装饰一个StreamEncoder的实现RequestStreamEncoderImpl
  request_encoder.encodeHeaders(*parent_.downstream_headers_, !buffered_request_body_ &amp;&amp; encode_complete_ &amp;&amp; !encode_trailers_);
  calling_encode_headers_ = false;

  // 在encodeHeaders()调用过程中可能发生RESET，这里需要进行测试，尽管是非常边缘的情况
  // 例如对于HTTP/2 codec，当帧由于某种原因无法编码的情况下就会出现RESET —— 比如头过大，超过64K
  if (deferred_reset_reason_) {
    // 重置回调
    onResetStream(deferred_reset_reason_.value());
  } else {
    // 编码请求体
    if (buffered_request_body_) {
      stream_info_.addBytesSent(buffered_request_body_-&gt;length());
      request_encoder.encodeData(*buffered_request_body_, encode_complete_ &amp;&amp; !encode_trailers_);
    }
    // 编码请求尾
    if (encode_trailers_) {
      request_encoder.encodeTrailers(*parent_.downstream_trailers_);
    }
    // 记录日志用的流信息
    if (encode_complete_) {
      stream_info_.onLastUpstreamTxByteSent();
      parent_.callbacks_-&gt;streamInfo().onLastUpstreamTxByteSent();
    }
  }
} </pre>
<p>上游请求的编码逻辑如下：</p>
<pre class="crayon-plain-tag">void RequestStreamEncoderImpl::encodeHeaders(const HeaderMap&amp; headers, bool end_stream) {
  // Method、Path头必须存在
  const HeaderEntry* method = headers.Method();
  const HeaderEntry* path = headers.Path();
  if (!method || !path) {
    throw CodecClientException(":method and :path must be specified");
  }
  // 如果是HEAD请求
  if (method-&gt;value() == Headers::get().MethodValues.Head.c_str()) {
    head_request_ = true;
  }
  // 如果是HEAD请求，则设置pending_response.head_request_ = true
  connection_.onEncodeHeaders(headers);
  // 写入报文最前面的部分
  connection_.reserveBuffer(std::max(4096U, path-&gt;value().size() + 4096));
  connection_.copyToBuffer(method-&gt;value().c_str(), method-&gt;value().size());
  connection_.addCharToBuffer(' ');
  connection_.copyToBuffer(path-&gt;value().c_str(), path-&gt;value().size());
  connection_.copyToBuffer(REQUEST_POSTFIX, sizeof(REQUEST_POSTFIX) - 1);
  // 写入请求头部分，包括写入一些额外的头
  StreamEncoderImpl::encodeHeaders(headers, end_stream);
}</pre>
<p>可以看到，<span style="background-color: #c0c0c0;">上游请求的编码，是不走HTTP过滤器链</span>的。 </p>
<div class="blog_h3"><span class="graybg">接收上游响应</span></div>
<p>那么，上游的响应又是如何接收到的呢？在newStream方法调用createNewConnection创建新客户端时，对应的L4连接也会被创建 —— libevent事件回调会被注册：</p>
<pre class="crayon-plain-tag">void ConnPoolImpl::createNewConnection() {
  ActiveClientPtr client(new ActiveClient(*this));
  client-&gt;moveIntoList(std::move(client), busy_clients_);
}
// ActiveClient的构造函数会创建L4连接
ConnPoolImpl::ActiveClient::ActiveClient(ConnPoolImpl&amp; parent)
    : parent_(parent),
      // 连接到服务器端的超时回调
      connect_timer_(parent_.dispatcher_.createTimer([this]() -&gt; void { onConnectTimeout(); })),
      remaining_requests_(parent_.host_-&gt;cluster().maxRequestsPerConnection()) {
  // ...
  // 调用HostImpl.createConnection()
  Upstream::Host::CreateConnectionData data =
      parent_.host_-&gt;createConnection(parent_.dispatcher_, parent_.socket_options_, nullptr);
}

Host::CreateConnectionData HostImpl::createConnection(
    Event::Dispatcher&amp; dispatcher, const Network::ConnectionSocket::OptionsSharedPtr&amp; options,
    Network::TransportSocketOptionsSharedPtr transport_socket_options) const {
  // 创建L4客户端连接
  return {createConnection(dispatcher, *cluster_, address_, options, transport_socket_options),
          shared_from_this()};
}</pre>
<p>响应就是通过libevent回调传递，其<span style="background-color: #c0c0c0;">网络层的处理路径和处理下游请求时是完全一样的——不管是读下游请求还是上游响应，L4过滤器的onData都会被调用</span>，在onContinueReading方法中进行报文的读取。</p>
<p>对于HTTP1来说，当报文头读取完毕后，Http::Http1::ClientConnectionImpl::onHeadersComplete被回调，它会转调PendingResponse.decoder.decodeHeaders方法，后者进而调用UpstreamRequest::decodeHeaders：</p>
<pre class="crayon-plain-tag">void Filter::UpstreamRequest::decodeHeaders(Http::HeaderMapPtr&amp;&amp; headers, bool end_stream) {
  stream_info_.onFirstUpstreamRxByteReceived();
  parent_.callbacks_-&gt;streamInfo().onFirstUpstreamRxByteReceived();
  maybeEndDecode(end_stream);
  // 读取头
  upstream_headers_ = headers.get();
  // 获取响应码
  const uint64_t response_code = Http::Utility::getResponseStatus(*headers);
  stream_info_.response_code_ = static_cast&lt;uint32_t&gt;(response_code);
  // 调用Router
  parent_.onUpstreamHeaders(response_code, std::move(headers), end_stream);
}</pre>
<p>UpstreamRequest.parent_就是Router过滤器，其onUpstreamHeaders的实现如下：</p>
<pre class="crayon-plain-tag">void Filter::onUpstreamHeaders(const uint64_t response_code, Http::HeaderMapPtr&amp;&amp; headers,
                               bool end_stream) {
  ENVOY_STREAM_LOG(debug, "upstream headers complete: end_stream={}", *callbacks_, end_stream);

  // 异常检测信息收集，为上游主机添加一个状态码
  upstream_request_-&gt;upstream_host_-&gt;outlierDetector().putHttpResponseCode(response_code);

  // 健康检查快速失败标记 x-envoy-immediate-health-check-fail，可能通过管理端点设置
  if (headers-&gt;EnvoyImmediateHealthCheckFail() != nullptr) {
    // 设置上游主机健康状态
    upstream_request_-&gt;upstream_host_-&gt;healthChecker().setUnhealthy();
  }

  // 重试相关的处理
  if (retry_state_) {
    // onHostAttempted：当针对一个主机的请求尝试失败了，并且可以进行下一个尝试时，调用此回调
    retry_state_-&gt;onHostAttempted(upstream_request_-&gt;upstream_host_);
    // 判断是否应该进行重试，如果是，调用回调，也就是doRetry()
    RetryStatus retry_status = retry_state_-&gt;shouldRetry(
        headers.get(), absl::optional&lt;Http::StreamResetReason&gt;(), [this]() -&gt; void { doRetry(); });
    // 捕获上游主机，因为后面的setupRetry()调用会清除upstream_request_
    const auto upstream_host = upstream_request_-&gt;upstream_host_;
    if (retry_status == RetryStatus::Yes &amp;&amp; setupRetry(end_stream)) {
      // 重试
      Http::CodeStats&amp; code_stats = httpContext().codeStats();
      code_stats.chargeBasicResponseStat(cluster_-&gt;statsScope(), "retry.", static_cast&lt;Http::Code&gt;(response_code));
      upstream_host-&gt;stats().rq_error_.inc();
      return;
    } else if (retry_status == RetryStatus::NoOverflow) {
      // 上游过载
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamOverflow);
    } else if (retry_status == RetryStatus::NoRetryLimitExceeded) {
      // 达到最大重试次数
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamRetryLimitExceeded);
    }

    // 由于end_stream为false时不会调用cleanup()，保证重试定时器被销毁
    retry_state_.reset();
  }
  
  // 处理重定向
  if (static_cast&lt;Http::Code&gt;(response_code) == Http::Code::Found &amp;&amp;
      route_entry_-&gt;internalRedirectAction() == InternalRedirectAction::Handle &amp;&amp; setupRedirect(*headers)) {
    return;
    // If the redirect could not be handled, fail open and let it pass to the
    // next downstream.
  }

  // 处理响应头x-envoy-upstream-service-time
  if (DateUtil::timePointValid(downstream_request_complete_time_)) {
    Event::Dispatcher&amp; dispatcher = callbacks_-&gt;dispatcher();
    MonotonicTime response_received_time = dispatcher.timeSystem().monotonicTime();
    std::chrono::milliseconds ms = std::chrono::duration_cast&lt;std::chrono::milliseconds&gt;(
        response_received_time - downstream_request_complete_time_);
    if (!config_.suppress_envoy_headers_) {
      headers-&gt;insertEnvoyUpstreamServiceTime().value(ms.count());
    }
  }

  // 根据响应头来设置此上游主机是否金丝雀版本
  upstream_request_-&gt;upstream_canary_ =
      (headers-&gt;EnvoyUpstreamCanary() &amp;&amp; headers-&gt;EnvoyUpstreamCanary()-&gt;value() == "true") ||
      upstream_request_-&gt;upstream_host_-&gt;canary();
  chargeUpstreamCode(response_code, *headers, upstream_request_-&gt;upstream_host_, false);

  // 处理非500响应头，主要是进行一些指标的收集
  if (!Http::CodeUtility::is5xx(response_code)) {
    handleNon5xxResponseHeaders(*headers, end_stream);
  }

  // downstream_set_cookies_为需要添加到上游响应头中的Cookies
  for (const auto&amp; header_value : downstream_set_cookies_) {
    headers-&gt;addReferenceKey(Http::Headers::get().SetCookie, header_value);
  }

  // 对响应头进行一系列最后处理：
  // 添加一系列用户定义的响应头，按照顺序： route-action-level、route-level、virtual host level、route-action-level
  route_entry_-&gt;finalizeResponseHeaders(*headers, callbacks_-&gt;streamInfo());

  downstream_response_started_ = true;
  if (end_stream) {
    onUpstreamComplete();
  }

  // 开始向下游发送响应头，这个是要走过滤器链的
  callbacks_-&gt;encodeHeaders(std::move(headers), end_stream);
}</pre>
<p>可以看到，<span style="background-color: #c0c0c0;">上游响应的解码，也是不走HTTP过滤器链</span>的。</p>
<p>另外需要注意，不管是下游请求、上游响应，<span style="background-color: #c0c0c0;">都会经由http_parser回调L7连接的on***Complete方法，不同之处是，对于下游请求来说L7连接的实现是ServerConnectionImpl，而对于上游响应来说L7连接的实现是ClientConnectionImpl</span>。</p>
<p>上游响应头处理完毕后，响应体回调onMessageComplete很快执行：</p>
<pre class="crayon-plain-tag">void ClientConnectionImpl::onMessageComplete() {
  ENVOY_CONN_LOG(trace, "message complete", connection_);
  if (ignore_message_complete_for_100_continue_) {
    ignore_message_complete_for_100_continue_ = false;
    return;
  }
  if (!pending_responses_.empty()) {
    // 取出未决响应，注意这里是HTTP11，每个连接上同时只会有一个未决响应
    PendingResponse response = pending_responses_.front();
    pending_responses_.pop_front();

    if (deferred_end_stream_headers_) {
      // 解码响应头
      response.decoder_-&gt;decodeHeaders(std::move(deferred_end_stream_headers_), true);
      deferred_end_stream_headers_.reset();
    } else {
      // 解码响应体
      Buffer::OwnedImpl buffer;
      response.decoder_-&gt;decodeData(buffer, true);
    }
  }
}</pre>
<p>response.decoder就是UpstreamRequest，其decodeData方法会调用Router过滤器的onUpstreamData，这类似于读取响应头时调用onUpstreamHeaders，类似的、可能被调用的其它回调包括onUpstreamTrailers、onUpstreamMetadata。</p>
<div class="blog_h3"><span class="graybg">处理上游超时 </span></div>
<p>Router过滤器不负责真正的发送上游请求，这是由连接池异步进行的。它调用upstream_request_的encodeHeaders后，<span style="background-color: #c0c0c0;">立即回调onRequestComplete</span>，后者注册了定时器来处理请求超时：</p>
<pre class="crayon-plain-tag">void Filter::onRequestComplete() {
  downstream_end_stream_ = true;
  // 获取事件分发器
  Event::Dispatcher&amp; dispatcher = callbacks_-&gt;dispatcher();
  downstream_request_complete_time_ = dispatcher.timeSystem().monotonicTime();

  // 有可能我们得到一个立即的RESET，因此这里判断上游请求是否为空
  if (upstream_request_) {
    maybeDoShadowing();
    // 如果配置了超时，则注册定时器，回调为onResponseTimeout
    if (timeout_.global_timeout_.count() &gt; 0) {
      response_timeout_ = dispatcher.createTimer([this]() -&gt; void { onResponseTimeout(); });
      response_timeout_-&gt;enableTimer(timeout_.global_timeout_);
    }
  }
}</pre>
<p>如果上游请求超时，下面的函数被调用：</p>
<pre class="crayon-plain-tag">void Filter::onResponseTimeout() {
  ENVOY_STREAM_LOG(debug, "upstream timeout", *callbacks_);
  cluster_-&gt;stats().upstream_rq_timeout_.inc();

  // 可能在执行上游请求重试backoff期间发生超时，那时是没有上游请求的。这种情况下仿冒一个RESET
  if (upstream_request_) {
    if (upstream_request_-&gt;upstream_host_) {
      upstream_request_-&gt;upstream_host_-&gt;stats().rq_timeout_.inc();
    }
    // 请求已经处理，不能取消，必须重置流
    upstream_request_-&gt;resetStream();
  }
  // 触发上游重置，重置的原因有Reset, GlobalTimeout, PerTryTimeout几种，这里是GlobalTimeout
  onUpstreamReset(UpstreamResetType::GlobalTimeout, absl::optional&lt;Http::StreamResetReason&gt;());
}</pre>
<div class="blog_h3"><span class="graybg">处理上游重置</span></div>
<pre class="crayon-plain-tag">void Filter::onUpstreamReset(UpstreamResetType type, const absl::optional&lt;Http::StreamResetReason&gt;&amp; reset_reason) {
  // 全局性超时
  ASSERT(type == UpstreamResetType::GlobalTimeout || upstream_request_);
  // 上游重置
  if (type == UpstreamResetType::Reset) {
    ENVOY_STREAM_LOG(debug, "upstream reset", *callbacks_);
  }

  Upstream::HostDescriptionConstSharedPtr upstream_host;
  if (upstream_request_) {
    upstream_host = upstream_request_-&gt;upstream_host_;
    if (upstream_host) {
      // 为上游主机的断路检测器提供信息，如果是RESET，则记录503，否则记录504（网关超时）
      upstream_host-&gt;outlierDetector().putHttpResponseCode(
          enumToInt(type == UpstreamResetType::Reset ? Http::Code::ServiceUnavailable
                                                     : timeout_response_code_));
    }
  }

  // 全局超时时不会重试，已经开始响应处理后也不会重试
  if (type != UpstreamResetType::GlobalTimeout &amp;&amp; !downstream_response_started_ &amp;&amp; retry_state_) {
    // 回调retry modifiers
    if (upstream_host != nullptr) {
      retry_state_-&gt;onHostAttempted(upstream_host);
    }
    // 判断是否需要重试
    RetryStatus retry_status = retry_state_-&gt;shouldRetry(nullptr, reset_reason, [this]() -&gt; void { doRetry(); });
    if (retry_status == RetryStatus::Yes &amp;&amp; setupRetry(true)) {
      // 需要重试
      if (upstream_host) {
        upstream_host-&gt;stats().rq_error_.inc();
      }
      return;
    // 不应该重试
    } else if (retry_status == RetryStatus::NoOverflow) {
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamOverflow);
    } else if (retry_status == RetryStatus::NoRetryLimitExceeded) {
      callbacks_-&gt;streamInfo().setResponseFlag(
          StreamInfo::ResponseFlag::UpstreamRetryLimitExceeded);
    }
  }

  // 如果尚未向下游发送任何信息，则发送具有适当响应码的响应；否则仅仅是重置响应
  if (downstream_response_started_) {
    if (upstream_request_ != nullptr &amp;&amp; upstream_request_-&gt;grpc_rq_success_deferred_) {
      upstream_request_-&gt;upstream_host_-&gt;stats().rq_error_.inc();
    }
    // 删除重试定时器
    cleanup();
    callbacks_-&gt;resetStream();
  } else {
    cleanup();
    Http::Code code;
    const char* body;
    if (type == UpstreamResetType::GlobalTimeout || type == UpstreamResetType::PerTryTimeout) {
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamRequestTimeout);

      code = timeout_response_code_;
      body = code == Http::Code::GatewayTimeout ? "upstream request timeout" : "";
    } else {
      StreamInfo::ResponseFlag response_flags =
          streamResetReasonToResponseFlag(reset_reason.value());
      callbacks_-&gt;streamInfo().setResponseFlag(response_flags);
      code = Http::Code::ServiceUnavailable;
      body = "upstream connect error or disconnect/reset before headers";
    }

    const bool dropped = reset_reason &amp;&amp; reset_reason.value() == Http::StreamResetReason::Overflow;
    chargeUpstreamCode(code, upstream_host, dropped);
    // 如果有非5xx响应，却仍然被后端重置，或者在响应开始前超时，作为一个错误看待
    if (upstream_host != nullptr &amp;&amp; !Http::CodeUtility::is5xx(enumToInt(code))) {
      upstream_host-&gt;stats().rq_error_.inc();
    }
    // 发送本地响应
    callbacks_-&gt;sendLocalReply(code, body,
                               [dropped, this](Http::HeaderMap&amp; headers) {
                                 if (dropped &amp;&amp; !config_.suppress_envoy_headers_) {
                                   headers.insertEnvoyOverloaded().value(
                                       Http::Headers::get().EnvoyOverloadedValues.True);
                                 }
                               },
                               absl::nullopt);
  }
}</pre>
<div class="blog_h2"><span class="graybg">处理HTTP下游响应写</span></div>
<div class="blog_h3"><span class="graybg">发送本地响应</span></div>
<p>在Envoy的HTTP解码过滤器处理下游请求的过程中，可能由于多种原因（通常是异常情况），立即应答下游客户端，而不向上游转发请求。此时会调用ActiveStreamDecoderFilter，或者直接调用ActiveStream的：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::sendLocalReply(
    bool is_grpc_request, Code code, absl::string_view body,
    const std::function&lt;void(HeaderMap&amp; headers)&gt;&amp; modify_headers, bool is_head_request,
    const absl::optional&lt;Grpc::Status::GrpcStatus&gt; grpc_status) {
  // 断言当前流的响应头尚未设置
  ASSERT(response_headers_ == nullptr);
  // 对于过早错误的处理，尽可能尝试创建出过滤器链，以便记录访问日志
  if (!state_.created_filter_chain_) {
    createFilterChain();
  }
  // 调用此工具函数
  Utility::sendLocalReply(is_grpc_request,
                          // 编码响应头的回调
                          [this, modify_headers](HeaderMapPtr&amp;&amp; headers, bool end_stream) -&gt; void {
                            if (modify_headers != nullptr) {
                              // 转发sendLocalReply的入参
                              modify_headers(*headers);
                            }
                            // 移动响应头
                            response_headers_ = std::move(headers);
                            // 编码响应头
                            encodeHeaders(nullptr, *response_headers_, end_stream);
                          },
                          // 编码响应体的回调
                          [this](Buffer::Instance&amp; data, bool end_stream) -&gt; void {
                            // 编码响应体
                            encodeData(nullptr, data, end_stream);
                          },
                          // 被销毁？重置      响应码 响应体 gRPC状态码    提示是否header-only
                          state_.destroyed_, code, body, grpc_status, is_head_request);
}</pre>
<p>上面代码调用的工具函数实现如下：</p>
<pre class="crayon-plain-tag">void Utility::sendLocalReply(
    bool is_grpc, std::function&lt;void(HeaderMapPtr&amp;&amp; headers, bool end_stream)&gt; encode_headers,
    std::function&lt;void(Buffer::Instance&amp; data, bool end_stream)&gt; encode_data, const bool&amp; is_reset,
    Code response_code, absl::string_view body_text,
    const absl::optional&lt;Grpc::Status::GrpcStatus&gt; grpc_status, bool is_head_request) {
  // encode_headers()调用可能重置流，但是在调用它之前，不能是已重置状态
  ASSERT(!is_reset);
  // 如果请求是gRPC，则返回trailers-only的响应
  if (is_grpc) {
    // 处理gRPC协议的响应头
    HeaderMapPtr response_headers{new HeaderMapImpl{
        {Headers::get().Status, std::to_string(enumToInt(Code::OK))},
        {Headers::get().ContentType, Headers::get().ContentTypeValues.Grpc},
        // gRPC状态码作为响应头
        {Headers::get().GrpcStatus,
         std::to_string(
             enumToInt(grpc_status ? grpc_status.value()
                                   : Grpc::Utility::httpToGrpcStatus(enumToInt(response_code))))}}};
    if (!body_text.empty() &amp;&amp; !is_head_request) {
      // 如果提供了响应体，则编码为gRPC消息
      response_headers-&gt;insertGrpcMessage().value(body_text);
    }
    encode_headers(std::move(response_headers), true); // 编码响应头
    return;
  }
  // 处理非gRPC协议的响应头
  HeaderMapPtr response_headers{ new HeaderMapImpl{{Headers::get().Status, std::to_string(enumToInt(response_code))}}};
  if (!body_text.empty()) {
    response_headers-&gt;insertContentLength().value(body_text.size());
    response_headers-&gt;insertContentType().value(Headers::get().ContentTypeValues.Text);
  }

  // 对于header-only响应，编码完头即返回
  if (is_head_request) {
    encode_headers(std::move(response_headers), true);
    return;
  }
  // 否则，如果响应体不为空，则编码头后，再编码体
  encode_headers(std::move(response_headers), body_text.empty());
  // encode_headers()) 调用可能修改了is_reset，因此再次测试：
  if (!body_text.empty() &amp;&amp; !is_reset) {
    // OwnedImpl封装一个分配的evbuffer，evbuffer用于libevent的缓冲网络I/O的缓冲区的处理
    Buffer::OwnedImpl buffer(body_text);
    encode_data(buffer, true);
  }
}</pre>
<div class="blog_h3"><span class="graybg">HTTP响应头编码</span></div>
<p>响应头编码由HTTP连接管理器的ActiveStream::encodeHeaders方法完成：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::encodeHeaders(ActiveStreamEncoderFilter* filter,
                                                        HeaderMap&amp; headers, bool end_stream) {
  // 重置per-stream的空闲定时器，也就是重新计时
  resetIdleTimer();
  // 解除请求超时报警
  disarmRequestTimeout();
  // 设置 state_.local_complete_ = end_stream，并开始迭代过滤器链
  std::list&lt;ActiveStreamEncoderFilterPtr&gt;::iterator entry = commonEncodePrefix(filter, end_stream);
  // 在何处开始encodeData()调用
  std::list&lt;ActiveStreamEncoderFilterPtr&gt;::iterator continue_data_entry = encoder_filters_.end();

  for (; entry != encoder_filters_.end(); entry++) {
    // 设置过滤器调用状态为正在编码响应头
    ASSERT(!(state_.filter_call_state_ &amp; FilterCallState::EncodeHeaders));
    state_.filter_call_state_ |= FilterCallState::EncodeHeaders;
    // 设置过滤器的end_stream，如果header-only，或者传入end_stream==true
    // end_stream意味着后面没有响应体需要处理
    (*entry)-&gt;end_stream_ =
        encoding_headers_only_ || (end_stream &amp;&amp; continue_data_entry == encoder_filters_.end());
    // 调用过滤器进行编码
    FilterHeadersStatus status = (*entry)-&gt;handle_-&gt;encodeHeaders(headers, (*entry)-&gt;end_stream_);
    // 重置过滤器调用状态
    state_.filter_call_state_ &amp;= ~FilterCallState::EncodeHeaders;
    ENVOY_STREAM_LOG(trace, "encode headers called: filter={} status={}", *this,
                     static_cast&lt;const void*&gt;((*entry).get()), static_cast&lt;uint64_t&gt;(status));

    // 根据上一个过滤器的处理结果决定是否需要继续迭代
    const auto continue_iteration = (*entry)-&gt;commonHandleAfterHeadersCallback(status, encoding_heade刷出rs_only_);

    // 对于header-only应答，标记为local_complete_
    // 这样可以保证不会在doEndStream()中重置下游请求
    if (encoding_headers_only_) {
      state_.local_complete_ = true;
    }

    // 不继续迭代，也不会执行后面的基本响应头
    if (!continue_iteration) {
      return;
    }

    // 这里处理使用header-only响应，但是某个过滤器添加了响应体的情况
    // 不能传递end_stream = true给后续的过滤器
    if (end_stream &amp;&amp; buffered_response_data_ &amp;&amp; continue_data_entry == encoder_filters_.end()) {
      continue_data_entry = entry;
    }
  }

  // 基本响应头
  // 设置Date头
  connection_manager_.config_.dateProvider().setDateHeader(headers);
  // 设置Server头
  // 使用setReference()是安全的，因为serverName()在监听器的生命周期内不会改变
  headers.insertServer().value().setReference(connection_manager_.config_.serverName());
  // 如果是Upgrade请求，且没有响应体，则设置Content-Length头为0
  // 否则，移除Connection头
  // 移除Transfer=Encoding头
  // 如果请求头中设置了Envoy强制跟踪头（x-envoy-force-trace），且存在request-id，则在响应头中设置request-id
  // 移除KeepAlive头
  // 移除ProxyConnection头
  // 根据需要添加内容到Via头
  ConnectionManagerUtility::mutateResponseHeaders(headers, request_headers_.get(), connection_manager_.config_.via());

  // 如果当前应当drain/close连接，在编码响应头块之前发送go away帧
  if (connection_manager_.drain_state_ == DrainState::NotDraining &amp;&amp;
      // drainClose如果连接应当被drain和close返回true
      // 如果监听器正处于draing状态（原因可能是健康检查、热重启）。此方法的返回值由监听器本地、全局DrainManager共同决定
      // local_drain_manager_-&gt;drainClose() || parent_.server_.drainManager().drainClose()
      connection_manager_.drain_close_.drainClose()) {

    // 对于HTTP/1.1请求来说不做什么实质性的事情，仅仅让L4连接有额外的时间和后续请求竞争
    // 此方法在HTTP/1.1和HTTP/2之间保持逻辑一致
    connection_manager_.startDrainSequence();
    connection_manager_.stats_.named_.downstream_cx_drain_close_.inc();
    ENVOY_STREAM_LOG(debug, "drain closing connection", *this);
  }

  // 由于Connection: Close头，的原因，设置DrainState为Closing
  if (connection_manager_.drain_state_ == DrainState::NotDraining &amp;&amp; state_.saw_connection_close_) {
    ENVOY_STREAM_LOG(debug, "closing connection due to connection close header", *this);
    connection_manager_.drain_state_ = DrainState::Closing;
  }
  // 由于过载，且配置了在过载后禁用Keepalive，设置DrainState为Closing
  if (connection_manager_.drain_state_ == DrainState::NotDraining &amp;&amp;
      connection_manager_.overload_disable_keepalive_ref_ == Server::OverloadActionState::Active) {
    ENVOY_STREAM_LOG(debug, "disabling keepalive due to envoy overload", *this);
    connection_manager_.drain_state_ = DrainState::Closing;
    connection_manager_.stats_.named_.downstream_cx_overload_disable_keepalive_.inc();
  }

  // 如果准备在对端尚未完成的情况下销毁流，同时连接不支持多路分发（非HTTP2），设置DrainState为Closing
  if (!state_.remote_complete_) {
    if (connection_manager_.codec_-&gt;protocol() != Protocol::Http2) {
      connection_manager_.drain_state_ = DrainState::Closing;
    }

    connection_manager_.stats_.named_.downstream_rq_response_before_rq_complete_.inc();
  }

  // DrainState被置为Closing，且当前不是HTTP2
  if (connection_manager_.drain_state_ == DrainState::Closing &amp;&amp;
      connection_manager_.codec_-&gt;protocol() != Protocol::Http2) {
    // 如果不是Upgrade请求，则设置Connection:Close响应头
    // 关于Connection: close，如果出现在：
    // 1、请求头，表示它希望服务器在发送应答消息后关闭连接
    // 2、响应头，表示服务器会在发送应答消息后关闭连接，如果请求头是Connection: Keep-Alive则同时意味着服务器不支持连接重用
    if (!Utility::isUpgrade(headers)) {
      headers.insertConnection().value().setReference(Headers::get().ConnectionValues.Close);
    }
  }

  // 分布式追踪相关处理
  // 关于x-envoy-decorator-operation头：
  // 1、如果入站请求提供了此头，则应该覆盖在由追踪系统生成的server span中本地定义的operation(span)名
  // 2、如果出站响应存在此头，则应该覆盖任何本地定义的client span的operation(span)名
  if (connection_manager_.config_.tracingConfig()) {
    if (connection_manager_.config_.tracingConfig()-&gt;operation_name_ == Tracing::OperationName::Ingress) {
      // 对于ingress（inbound）响应
      // 如果请求头没有指定x-envoy-decorator-operation，则使用decorator的operation name作为x-envoy-decorator-operation响应头
      if (decorated_operation_) {
        headers.insertEnvoyDecoratorOperation().value(*decorated_operation_);
      }
    } else if (connection_manager_.config_.tracingConfig()-&gt;operation_name_ == Tracing::OperationName::Egress) {
      // 对于egress（outbound）响应
      const HeaderEntry* resp_operation_override = headers.EnvoyDecoratorOperation();
      // 如果已经提供x-envoy-decorator-operation，则覆盖当前Spance的operation值
      if (resp_operation_override) {
        if (!resp_operation_override-&gt;value().empty() &amp;&amp; active_span_) {
          active_span_-&gt;setOperation(resp_operation_override-&gt;value().c_str());
        }
        // 移除x-envoy-decorator-operation头，防止传播给服务
        headers.removeEnvoyDecoratorOperation();
      }
    }
  }

  // 进行统计指标收集
  chargeStats(headers);
  stream_info_.onFirstDownstreamTxByteSent();

  // 现在实际完成基于codec的响应头编码，生成、刷出响应。如果end_stream则endEncode()
  response_encoder_-&gt;encodeHeaders( headers, encoding_headers_only_ || (end_stream &amp;&amp; continue_data_entry == encoder_filters_.end()));
  if (continue_data_entry != encoder_filters_.end()) {
    // 调用之前中止迭代的过滤器的continueEncoding()方法，此方法不会重复调用encodeHeaders()
    // 仿冒的设置stopped_ since=true，原因是continueEncoding()要求如此
    ASSERT(buffered_response_data_);
    (*continue_data_entry)-&gt;stopped_ = true;
    (*continue_data_entry)-&gt;continueEncoding();
  } else {
    // 对于header-only响应 —— 不管是过滤器将其转换为header-only还是上游仅仅返回headers，结束响应编码的处理
    maybeEndEncode(encoding_headers_only_ || end_stream);
  }
}


void ConnectionManagerImpl::ActiveStream::maybeEndEncode(bool end_stream) {
  if (end_stream) {
    // 应当接受响应编码的处理
    // 增加日志信息
    stream_info_.onLastDownstreamTxByteSent();
    // 结束span
    request_response_timespan_-&gt;complete();
    // 处理由于上游响应或者reset导致应当结束的流
    connection_manager_.doEndStream(*this);
  }
}</pre>
<p>每个过滤器的<pre class="crayon-plain-tag">encodeHeaders(Http::HeaderMap&amp; headers, bool)</pre>方法会被调用，返回的Http::FilterHeadersStatus会影响响应头编码的后续处理流程。</p>
<div class="blog_h3"><span class="graybg">HTTP响应体编码</span></div>
<p>如果响应体缓冲区不为空，则需要在编码响应头后，继续处理响应体。响应体缓冲区的内容可能是由上游服务提供，也可能是由某个过滤器写入和修改。</p>
<p>响应体编码由HTTP连接管理器的ActiveStream::encodeData方法完成：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::encodeData(ActiveStreamEncoderFilter* filter,  Buffer::Instance&amp; data, bool end_stream) {
  // 和编码响应头时一样，重置空闲定时器
  resetIdleTimer();

  // 如果先前已经设置此状态，则直接返回
  if (encoding_headers_only_) {
    return;
  }

  // 产生编码过滤器的列表
  std::list&lt;ActiveStreamEncoderFilterPtr&gt;::iterator entry = commonEncodePrefix(filter, end_stream);
  // 在何处开始encodeTrailers调用
  auto trailers_added_entry = encoder_filters_.end();

  // 是否在响应体编码之前，响应尾已经存在了
  const bool trailers_exists_at_start = response_trailers_ != nullptr;
  for (; entry != encoder_filters_.end(); entry++) {
    // 如果任何一个过滤器的end_stream_被标记，则意味着这个以及后续的过滤器不应该处理数据
    if ((*entry)-&gt;end_stream_) {
      return;
    }
    ASSERT(!(state_.filter_call_state_ &amp; FilterCallState::EncodeData));

    // 设置过滤器调用状态
    state_.filter_call_state_ |= FilterCallState::EncodeData;
    if (end_stream) {
      // 最后一个数据帧      
      state_.filter_call_state_ |= FilterCallState::LastDataFrame;
    }
    // 检查response_trailers_，应对前面的过滤器的encodeData()方法调用addEncodedTrailers()的情况
    // 如果前面的过滤器添加了响应尾，则通知当前、后续过滤器，流处理尚不能结束
    (*entry)-&gt;end_stream_ = end_stream &amp;&amp; !response_trailers_;
    // 调用过滤器进行响应体编码
    FilterDataStatus status = (*entry)-&gt;handle_-&gt;encodeData(data, (*entry)-&gt;end_stream_);
    // 重置过滤器调用状态
    state_.filter_call_state_ &amp;= ~FilterCallState::EncodeData;
    if (end_stream) {
      state_.filter_call_state_ &amp;= ~FilterCallState::LastDataFrame;
    }
    ENVOY_STREAM_LOG(trace, "encode data called: filter={} status={}", *this,
                     static_cast&lt;const void*&gt;((*entry).get()), static_cast&lt;uint64_t&gt;(status));

    // 迭代前没有没有响应尾，但是        现在有响应尾（某过滤器添加）
    if (!trailers_exists_at_start &amp;&amp; response_trailers_ &amp;&amp; trailers_added_entry == encoder_filters_.end()) {
      // 这设置为当前过滤器
      trailers_added_entry = entry;
    }
    // 消息体回调通用处理逻辑
    if (!(*entry)-&gt;commonHandleAfterDataCallback(status, data, state_.encoder_filters_streaming_)) {
      return;
    }
  }

  ENVOY_STREAM_LOG(trace, "encoding data via codec (size={} end_stream={})", *this, data.length(),  end_stream);
  // 日志信息
  stream_info_.addBytesSent(data.length());

  // 如果在encodeData期间添加了响应尾，则需要触发decodeTrailers，让过滤器有机会处理这些尾数据
  if (trailers_added_entry != encoder_filters_.end()) {
    response_encoder_-&gt;encodeData(data, false);
    encodeTrailers(trailers_added_entry-&gt;get(), *response_trailers_);
  } else {
    // 调用StreamEncoder进行实际的响应体写入，并刷出
    response_encoder_-&gt;encodeData(data, end_stream);
    maybeEndEncode(end_stream);
  }
}</pre>
<p>每个过滤器的<pre class="crayon-plain-tag">encodeData(Buffer::Instance&amp;, bool)</pre>方法会被调用，返回的Http::FilterHeadersStatus会影响响应体编码的后续处理流程。 </p>
<div class="blog_h1"><span class="graybg">Envoy发送请求过程</span></div>
<p>Istio使用的不是原版的Envoy，它在项目<a href="https://github.com/istio/proxy">istio/proxy</a>中对Envoy进行了扩展，并在构建时引用Envoy的某个特定Commit Id，构建出完整的、增强的Envoy二进制文件。</p>
<p>Istio对Envoy做的增强主要是引入若干自定义过滤器，Mixer的客户端功能就是依赖于过滤器实现的。 </p>
<div class="blog_h2"><span class="graybg">如何调试</span></div>
<div class="blog_h3"><span class="graybg">启动程序</span></div>
<p>如果要创建完全本地的调试环境，你需要签出两个项目并构建：</p>
<pre class="crayon-plain-tag"># istio
git clone https://github.com/istio/istio.git

# istio/proxy
git clone https://github.com/istio/proxy.git</pre>
<p> 通过上面的项目，启动Pilot Discovery、Pilot Agent、Mixer三个程序。Mixer的启动方式前文已经有说明，Pilot Discovery、Agent的启动方式可以参考<a href="https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a>一文，需要注意的是，必须把binaryPath参数设置为istio/proxy项目构建出的envoy的路径。</p>
<pre class="crayon-plain-tag">pilot proxy sidecar  ... --binaryPath=/home/alex/CPP/projects/clion/istio/proxy/bazel-bin/src/envoy/envoy</pre>
<p>从istio/proxy构建出envoy时，注意保留调试符号。 </p>
<div class="blog_h3"><span class="graybg">调试Envoy</span></div>
<p>在Pilot Agent启动后，它会产生一个Envoy子进程。你可以用GDB连接到该进程，并在GDB控制台设置源码目录：</p>
<pre class="crayon-plain-tag">directory /home/alex/CPP/lib/libevent/2.1.8-stable
directory /home/alex/CPP/projects/clion/istio/proxy 
directory /home/alex/CPP/projects/clion/istio/proxy/bazel-proxy</pre>
<p>然后暂停程序执行，确保源码路径已经匹配上。 </p>
<div class="blog_h2"><span class="graybg">流量拦截</span></div>
<p>执行下面的命令，获取本地运行的Envoy代理的配置：</p>
<pre class="crayon-plain-tag">curl http://127.0.0.1:15000/config_dump</pre>
<p>可以看到，监听器virtual的端口是15001。假设我们想访问podinfo-canary.default.svc.k8s.gmem.cc在9898端口提供的服务，来了解Envoy代理的行为，可以先设置Iptables规则：</p>
<pre class="crayon-plain-tag"># 针对lo接口的请求不走PREROUTING链
iptables -t nat -A OUTPUT -p tcp -o lo --dport 9898 -j REDIRECT --to-port 15001</pre>
<p>然后，发起请求：</p>
<pre class="crayon-plain-tag">curl -H 'Host: podinfo-canary.default.svc.k8s.gmem.cc' http://127.0.0.1:9898/healthz</pre>
<p>此请求会触发Envoy的处理流程，包括对Mixer的L4、L7过滤器的调用。</p>
<div class="blog_h2"><span class="graybg">Check调用</span></div>
<p>Mixer过滤器处理HTTP请求头的逻辑如下：</p>
<pre class="crayon-plain-tag">FilterHeadersStatus Filter::decodeHeaders(HeaderMap&amp; headers, bool) {
  ENVOY_LOG(debug, "Called Mixer::Filter : {}", __func__);
  request_total_size_ += headers.byteSize();
  // 配置，包含目的服务信息
  ::istio::control::http::Controller::PerRouteConfig config;
  auto route = decoder_callbacks_-&gt;route();
  if (route) {
    ReadPerRouteConfig(route-&gt;routeEntry(), &amp;config);
  }
  // control是每个线程对应一个的控制对象
  // controller是Mixer控制器，以MixerFitlerConfig为参数，执行任务来保证对HTTP/TCP请求的控制
  // RequestHandler handler_，请求处理器，支持对Mixer服务器发起CHECK/REPORT调用
  handler_ = control_.controller()-&gt;CreateRequestHandler(config);

  state_ = Calling;
  initiating_call_ = true;
  // CheckData用于抽取额外的HTTP数据，供Mixer Check使用 —— 它持有HeaderMap、Envoy Metadata、网络连接等信息
  CheckData check_data(headers,
                       decoder_callbacks_-&gt;streamInfo().dynamicMetadata(),
                       decoder_callbacks_-&gt;connection());
  // HeaderUpdate用Istio属性来更新HTTP请求头
  Utils::HeaderUpdate header_update(&amp;headers);
  headers_ = &amp;headers;

  // Check调用相关逻辑：
  // 1、从客户端代理中抽取转发的属性
  // 2、从请求中抽取属性
  // 3、从配置中抽取属性
  // 4、如果有必要，将一部分属性转发给下游
  // 5、执行Check调用
  cancel_check_ = handler_-&gt;Check(
      &amp;check_data, &amp;header_update,
      // TransportCheckFunc 用于异步发起Check调用
      control_.GetCheckTransport(decoder_callbacks_-&gt;activeSpan()),
      // CheckDoneFunc 用于异步调用完成后处理CheckResponse
      [this](const CheckResponseInfo&amp; info) { completeCheck(info); });
  initiating_call_ = false;

  if (state_ == Complete) {
    return FilterHeadersStatus::Continue;
  }
  ENVOY_LOG(debug, "Called Mixer::Filter : {} Stop", __func__);
  return FilterHeadersStatus::StopIteration;
}</pre>
<p>从上面的代码我们可以看到，Mixer过滤器在处理下游请求头期间，会<span style="background-color: #c0c0c0;">异步的发起Check调用</span>：</p>
<pre class="crayon-plain-tag">CancelFunc RequestHandlerImpl::Check(CheckData* check_data,
                                     HeaderUpdate* header_update,
                                     TransportCheckFunc transport,
                                     CheckDoneFunc on_done) {
  // 添加转发的属性
  AddForwardAttributes(check_data);
  // 移除Istio属性 x-istio-attributes
  header_update-&gt;RemoveIstioAttributes();
  // 注入一个包含静态转发属性的头
  service_context_-&gt;InjectForwardedAttributes(header_update);

  if (!service_context_-&gt;enable_mixer_check()) {
    // 如果没有启动Check功能，直接以OK响应回调CheckDoneFunc
    CheckResponseInfo check_response_info;
    check_response_info.response_status = Status::OK;
    on_done(check_response_info);
    return nullptr;
  }

  // 添加Check相关属性
  AddCheckAttributes(check_data);

  // 根据Quota配置添加quota需求
  service_context_-&gt;AddQuotas(&amp;request_context_);
  // 异步发送Check调用
  return service_context_-&gt;client_context()-&gt;SendCheck(transport, on_done,
                                                       &amp;request_context_);
}</pre>
<p>此异步调用完成后，回调：</p>
<pre class="crayon-plain-tag">void Filter::completeCheck(const CheckResponseInfo&amp; info) {
  auto status = info.response_status;
  ENVOY_LOG(debug, "Called Mixer::Filter : check complete {}", status.ToString());
  // 流已经被重置，停止回调
  if (state_ == Responded) {
    return;
  }

  route_directive_ = info.route_directive;

  Utils::CheckResponseInfoToStreamInfo(info, decoder_callbacks_-&gt;streamInfo());

  // 处理来自路由指令的直接响应
  if (route_directive_.direct_response_code() != 0) {
    int status_code = route_directive_.direct_response_code();
    ENVOY_LOG(debug, "Mixer::Filter direct response {}", status_code);
    state_ = Responded;
    decoder_callbacks_-&gt;sendLocalReply(
        Code(status_code), route_directive_.direct_response_body(),
        [this](HeaderMap&amp; headers) {
          UpdateHeaders(headers, route_directive_.response_header_operations());
        },
        absl::nullopt);
    return;
  }

  // 如果状态不是OK，即使没有直接响应，也sendLocalReply
  if (!status.ok()) {
    state_ = Responded;

    int status_code = ::istio::utils::StatusHttpCode(status.error_code());
    decoder_callbacks_-&gt;sendLocalReply(Code(status_code), status.ToString(),
                                       nullptr, absl::nullopt);
    return;
  }

  // 将状态置为完成
  state_ = Complete;

  // 更新请求头
  if (nullptr != headers_) {
    UpdateHeaders(*headers_, route_directive_.request_header_operations());
    headers_ = nullptr;
    if (route_directive_.request_header_operations().size() &gt; 0) {
      decoder_callbacks_-&gt;clearRouteCache();
    }
  }

  if (!initiating_call_) {
    decoder_callbacks_-&gt;continueDecoding();
  }
}</pre>
<div class="blog_h2"><span class="graybg">Report调用</span></div>
<p>Report调用是延迟触发的，Mixer过滤器实现了Envoy::AccessLog::Instance（访问记录器），Report调用作为log方法逻辑的一部分。</p>
<p>Envoy在处理请求之后，可能会延迟的删除一些对象：</p>
<pre class="crayon-plain-tag">DispatcherImpl::DispatcherImpl(Buffer::WatermarkFactoryPtr&amp;&amp; factory, Api::Api&amp; api)
    : deferred_delete_timer_(createTimer([this]() -&gt; void { clearDeferredDeleteList(); })), 
      // 延迟删除定时器</pre>
<p>代表当前请求流的ActiveStream对象就是这样延迟删除的，删除时其析构函数被调用：</p>
<pre class="crayon-plain-tag">ConnectionManagerImpl::ActiveStream::~ActiveStream() {
  // ...
  // 遍历所有日志访问记录器
  for (const AccessLog::InstanceSharedPtr&amp; access_log : connection_manager_.config_.accessLogs()) {
    access_log-&gt;log(request_headers_.get(), response_headers_.get(), response_trailers_.get(),
                    stream_info_);
  }
  // ...
}</pre>
<p>可以看到，在ActiveStream析构时会调用所有访问日志记录器，包括Envoy::Http::Mixer::Filter::log：</p>
<pre class="crayon-plain-tag">void Filter::log(const HeaderMap* request_headers,
                 const HeaderMap* response_headers,
                 const HeaderMap* response_trailers,
                 const StreamInfo::StreamInfo&amp; stream_info) {
  ENVOY_LOG(debug, "Called Mixer::Filter : {}", __func__);
  if (!handler_) {
    if (request_headers == nullptr) {
      return;
    }
    // 可能因为请求被其它过滤器拒绝，Mixer过滤器没调用，因此handler尚未初始化
    ::istio::control::http::Controller::PerRouteConfig config;
    ReadPerRouteConfig(stream_info.routeEntry(), &amp;config);
    handler_ = control_.controller()-&gt;CreateRequestHandler(config);
  }

  // 如果没有调用check，则check属性没被抽取
  CheckData check_data(*request_headers, stream_info.dynamicMetadata(), decoder_callbacks_-&gt;connection());
  // ReportData提供接口，抽取HTTP属性，供Mixer Report调用使用
  ReportData report_data(response_headers, response_trailers, stream_info, request_total_size_);
  handler_-&gt;Report(&amp;check_data, &amp;report_data);
}</pre>
<p>Report方法的实现如下：</p>
<pre class="crayon-plain-tag">void RequestHandlerImpl::Report(CheckData* check_data,
                                ReportData* report_data) {
  if (!service_context_-&gt;enable_mixer_report()) {
    return;
  }

  // 添加转发的属性
  AddForwardAttributes(check_data);
  // 添加Check属性
  AddCheckAttributes(check_data);

  AttributesBuilder builder(&amp;request_context_);
  // 抽取Report属性
  builder.ExtractReportAttributes(report_data);

  // 发送Report请求
  service_context_-&gt;client_context()-&gt;SendReport(request_context_);
}</pre>
<p>发送Report请求的工作最终委托给::istio::mixerclient::MixerClient：</p>
<pre class="crayon-plain-tag">void ClientContextBase::SendReport(const RequestContext&amp; request) {
  mixer_client_-&gt;Report(*request.attributes);
}</pre>
<p>MixerClient包含批量处理的逻辑：</p>
<pre class="crayon-plain-tag">void MixerClientImpl::Report(const Attributes &amp;attributes) {
  report_batch_-&gt;Report(attributes);
}

void ReportBatch::Report(const Attributes&amp; request) {
  std::lock_guard&lt;std::mutex&gt; lock(mutex_);
  ++total_report_calls_;
  // 添加请求、压缩
  batch_compressor_-&gt;Add(request);
  // 如果超过批量限制，立即Report
  if (batch_compressor_-&gt;size() &gt;= options_.max_batch_entries) {
    FlushWithLock();
  } else {
    // 否则，延迟发送
    if (batch_compressor_-&gt;size() == 1 &amp;&amp; timer_create_) {
      if (!timer_) {
        timer_ = timer_create_([this]() { Flush(); });
      }
      timer_-&gt;Start(options_.max_batch_time_ms);
    }
  }
} </pre>
<div class="blog_h2"><span class="graybg">属性抽取 </span></div>
<p>MixerClient通过rRPC向Mixer服务器发送的是属性（Attributes），过滤器在调用MixerClient之前，会进行属性的抽取。</p>
<div class="blog_h3"><span class="graybg">Report属性 </span></div>
<pre class="crayon-plain-tag">void AttributesBuilder::ExtractReportAttributes(ReportData *report_data) {
  utils::AttributesBuilder builder(request_-&gt;attributes);

  std::string dest_ip;
  int dest_port;
  // 抽取 destination.ip
  if (report_data-&gt;GetDestinationIpPort(&amp;dest_ip, &amp;dest_port)) {
    if (!builder.HasAttribute(utils::AttributeName::kDestinationIp)) {
      builder.AddBytes(utils::AttributeName::kDestinationIp, dest_ip);
    }
    if (!builder.HasAttribute(utils::AttributeName::kDestinationPort)) {
      builder.AddInt64(utils::AttributeName::kDestinationPort, dest_port);
    }
  }

  std::string uid;
  // 抽取 destination.uid
  if (report_data-&gt;GetDestinationUID(&amp;uid)) {
    builder.AddString(utils::AttributeName::kDestinationUID, uid);
  }

  // 抽取 response.headers  所有响应头作为一个属性
  std::map&lt;std::string, std::string&gt; headers = report_data-&gt;GetResponseHeaders();
  builder.AddStringMap(utils::AttributeName::kResponseHeaders, headers);

  // 抽取 response.time
  builder.AddTimestamp(utils::AttributeName::kResponseTime, std::chrono::system_clock::now());

  ReportData::ReportInfo info;
  report_data-&gt;GetReportInfo(&amp;info);
  // 抽取 request.size
  builder.AddInt64(utils::AttributeName::kRequestBodySize, info.request_body_size);
  // 抽取 response.size
  builder.AddInt64(utils::AttributeName::kResponseBodySize, info.response_body_size);
  // 抽取 request.total_size
  builder.AddInt64(utils::AttributeName::kRequestTotalSize, info.request_total_size);
  // 抽取 response.total_size
  builder.AddInt64(utils::AttributeName::kResponseTotalSize, info.response_total_size);
  // 抽取 response.duration
  builder.AddDuration(utils::AttributeName::kResponseDuration, info.duration);

  // 抽取check属性
  if (!request_-&gt;check_status.ok()) {
    // 抽取 response.code
    builder.AddInt64(utils::AttributeName::kResponseCode, utils::StatusHttpCode(request_-&gt;check_status.error_code()));
    // 抽取 check.error_code
    builder.AddInt64(utils::AttributeName::kCheckErrorCode, request_-&gt;check_status.error_code());
    // 抽取 check.error_message
    builder.AddString(utils::AttributeName::kCheckErrorMessage, request_-&gt;check_status.ToString());
  } else {
    builder.AddInt64(utils::AttributeName::kResponseCode, info.response_code);
  }

  ReportData::GrpcStatus grpc_status;
  if (report_data-&gt;GetGrpcStatus(&amp;grpc_status)) {
    // 抽取 response.grpc_status
    builder.AddString(utils::AttributeName::kResponseGrpcStatus,  grpc_status.status);
    // 抽取 response.grpc_message
    builder.AddString(utils::AttributeName::kResponseGrpcMessage, grpc_status.message);
  }

  builder.AddString(utils::AttributeName::kContextProxyErrorCode, info.response_flags);

  ReportData::RbacReportInfo rbac_info;
  if (report_data-&gt;GetRbacReportInfo(&amp;rbac_info)) {
    if (!rbac_info.permissive_resp_code.empty()) {
      // 抽取 context.proxy_error_code
      builder.AddString(utils::AttributeName::kRbacPermissiveResponseCode, rbac_info.permissive_resp_code);
    }
    if (!rbac_info.permissive_policy_id.empty()) {
      // 抽取 rbac.permissive.effective_policy_id"
      builder.AddString(utils::AttributeName::kRbacPermissivePolicyId, rbac_info.permissive_policy_id);
    }
  }

  builder.FlattenMapOfStringToStruct(report_data-&gt;GetDynamicFilterState());
}</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy">Istio Mixer与Envoy的交互机制解读</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Istio Pilot与Envoy的交互机制解读</title>
		<link>https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy</link>
		<comments>https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy#comments</comments>
		<pubDate>Fri, 25 Jan 2019 03:37:18 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=24743</guid>
		<description><![CDATA[<p>前言 在基于Istio+Envoy实现的服务网格中，Istio的角色是控制平面，它是实现了Envoy的发现协议集xDS的管理服务器端。Envoy本身则作为网格的数据平面，和Istio通信，获得各种资源的配置并更新自身的代理规则。 除了实现xDS协议，Istio还负责： Envoy统计数据的收集，从Statd格式转换为Prometheus格式。（注：目前看来Envoy也直接暴露了Prometheus的Exporter） 限速服务、策略服务 和第三方Tracer的对接 数字证书分发 等功能。这些功能都需要Istio和Envoy的协同才能生效。最基础和关键的协同是Istio组件Pilot和Envoy之间基于xDS协议进行的各种Envoy配置信息的推送。 Istio的文档并没有对Istio Pilot和Envoy如何交互进行描述，本文结合Istio、Envoy的源码来探讨这些细节。 model包分析 Pilot的model包为很多Pilot抽象创建了模型（结构），并定义了它们支持的操作。注意这里建模的是Pilot的抽象，因此名词Service是指Istio的抽象服务，而非K8S的Service或者Envoy的Cluster。 config.go 对Istio的配置信息、配置存储进行建模。 Config 代表一个Istio配置单元： [crayon-69db8c96b3805284219426/] ConfigMeta 配置的元数据： [crayon-69db8c96b3809010948710/] ConfigStore 定义一组平台无关的，但是底层平台（例如K8S）必须支持的API，通过这些API可以存取Istio配置信息 <a class="read-more" href="https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">前言</span></div>
<p>在基于Istio+Envoy实现的服务网格中，Istio的角色是控制平面，它是实现了Envoy的发现协议集xDS的管理服务器端。Envoy本身则作为网格的数据平面，和Istio通信，获得各种资源的配置并更新自身的代理规则。</p>
<p>除了实现xDS协议，Istio还负责：</p>
<ol>
<li>Envoy统计数据的收集，从Statd格式转换为Prometheus格式。（注：目前看来Envoy也直接暴露了Prometheus的Exporter）</li>
<li>限速服务、策略服务</li>
<li>和第三方Tracer的对接</li>
<li>数字证书分发</li>
</ol>
<p>等功能。这些功能都需要Istio和Envoy的协同才能生效。最基础和关键的协同是Istio组件Pilot和Envoy之间基于xDS协议进行的各种Envoy配置信息的推送。</p>
<p>Istio的文档并没有对Istio Pilot和Envoy如何交互进行描述，本文结合Istio、Envoy的源码来探讨这些细节。</p>
<div class="blog_h1"><span class="graybg">model包分析</span></div>
<p>Pilot的model包为很多<span style="background-color: #c0c0c0;">Pilot抽象</span>创建了模型（结构），并定义了它们支持的操作。注意这里建模的是Pilot的抽象，因此名词Service是指Istio的抽象服务，而非K8S的Service或者Envoy的Cluster。</p>
<div class="blog_h2"><span class="graybg">config.go</span></div>
<p>对Istio的配置信息、配置存储进行建模。</p>
<div class="blog_h3"><span class="graybg">Config</span></div>
<p>代表一个Istio配置单元：</p>
<pre class="crayon-plain-tag">type Config struct {
	ConfigMeta
	// 配置内容以Proto消息的形式存储
	Spec proto.Message
}</pre>
<div class="blog_h3"><span class="graybg">ConfigMeta</span></div>
<p>配置的元数据：</p>
<pre class="crayon-plain-tag">type ConfigMeta struct {
	//匹配内容消息类型的短类型名称，例如route-rule
	Type string 
	// API组和版本
	Group string 
	Version string 
	// 命名空间范围内唯一性名称
	Name string 
	// 命名空间
	Namespace string 
	// FQDN后缀
	Domain string 
	// 标签集
	Labels map[string]string 
	// 注解集
	Annotations map[string]string 
	// 资源版本，跟踪对配置注册表的变更
	ResourceVersion string 
	CreationTimestamp meta_v1.Time 
}</pre>
<div class="blog_h3"><span class="graybg">ConfigStore</span></div>
<p>定义一组平台无关的，但是底层平台（例如K8S）必须支持的API，通过这些API可以存取Istio配置信息</p>
<p>每个配置信息的键，由type + name + namespace的组合构成，确保每个配置具有唯一的键</p>
<p>写操作是异步执行的，也就是说Update后立即Get可能无法获得最新结果。有资源版本判断资源是否更新</p>
<p>此接口返回的引用，仅支持只读操作，对其修改存在线程安全问题</p>
<pre class="crayon-plain-tag">type ConfigStore interface {
    // 返回配置描述符，其实就是[]ProtoSchema类型，ProtoSchema描述了资源的Group/Version/Type等信息
    ConfigDescriptor() ConfigDescriptor
    Get(typ, name, namespace string) (config *Config, exists bool)
    List(typ, namespace string) ([]Config, error)
    Create(config Config) (revision string, err error)
    Update(config Config) (newRevision string, err error)
    Delete(typ, name, namespace string) error
}</pre>
<div class="blog_h3"><span class="graybg">ConfigStoreCache</span></div>
<p>表示ConfigStore的本地完整复制的缓存，此缓存主动和远程存储保持同步，并且在获取更新时提供提供通知机制。</p>
<p>为了获得通知，事件处理器必须在Run之前注册，缓存需要在Run之后有一个初始的同步延迟。</p>
<pre class="crayon-plain-tag">type ConfigStoreCache interface {
        // CRUD接口
    ConfigStore
    // 添加某种配置类型的事件处理器
    RegisterEventHandler(typ string, handler func(Config, Event))
    Run(stop &lt;-chan struct{})
    // 初始缓存同步完毕后返回true
    HasSynced() bool
}</pre>
<div class="blog_h3"><span class="graybg">IstioConfigStore</span></div>
<p>此接口扩展ConfigStore，增加一些针对Istio资源的操控接口：</p>
<pre class="crayon-plain-tag">type IstioConfigStore interface {
	ConfigStore
	// 列出ServiceEntry
	ServiceEntries() []Config
	// 列出绑定到指定工作负载标签的Gateway
	Gateways(workloadLabels LabelsCollection) []Config
	// 列出绑定到指定工作负载标签EnvoyFilter
	EnvoyFilter(workloadLabels LabelsCollection) *Config
	// 列出关联到指定目标服务实例的Mixerclient HTTP API Specs
	HTTPAPISpecByDestination(instance *ServiceInstance) []Config
	// 列出关联到指定目标服务实例的Mixerclient quota specifications
	QuotaSpecByDestination(instance *ServiceInstance) []Config
	// 列出关联到指定服务+端口的身份验证策略
	// 如果存在多个不同范围（全局、命名空间、服务）的策略，最精确的那个被返回。如果同一范围有多个策略，返回第一个
	AuthenticationPolicyByDestination(service *Service, port *Port) *Config
	// 列出指定命名空间的ServiceRoles
	ServiceRoles(namespace string) []Config
	// 列出指定命名空间的ServiceRoleBindings
	ServiceRoleBindings(namespace string) []Config
	// 列出名字为DefaultRbacConfigName的RbacConfig
	RbacConfig() *Config
}</pre>
<div class="blog_h2"><span class="graybg">context.go</span></div>
<div class="blog_h3"><span class="graybg">Environment</span></div>
<p>此结构为Pilot提供聚合的环境性的API：</p>
<pre class="crayon-plain-tag">type Environment struct {
	// 内嵌接口：用于列出服务、实例
	ServiceDiscovery

	// 已经废弃，使用 PushContext.ServiceAccounts
	ServiceAccounts

	// 内嵌接口：用于列出路由规则
	IstioConfigStore

	// 网格配置信息
	Mesh *meshconfig.MeshConfig

	// 用于和Mixer通信
	MixerSAN []string

	// 全局的推送上下文，已经废弃
	// 除非出于测试、处理新连接的目的，不要使用此字段
	PushContext *PushContext
}</pre>
<div class="blog_h3"><span class="graybg">Proxy</span></div>
<p>此结构建模代理（Envoy代理）的属性，xDS使用此结构对代理进行识别：</p>
<pre class="crayon-plain-tag">type Proxy struct {
	// 此代理所在的集群
	ClusterID string
	// 节点类型（也就是说运行代理的那个Pod的代理角色
	Type NodeType
	// 用于识别代理以及它的同地协作的服务实例的IP地址
	IPAddress string
	// 平台先关的Sidecar代理ID
	ID string
	// 短主机名的DNS后缀
	Domain string
	// 节点的元数据
	Metadata map[string]string
}</pre>
<div class="blog_h3"><span class="graybg">NodeType</span></div>
<p>用于区分不同代理在网格中的职责。</p>
<pre class="crayon-plain-tag">type NodeType string
const (
	// 应用程序容器的边车代理，普通被网格管理的Pod使用这种代理角色
	Sidecar NodeType = "sidecar"
	// 独立运行的，集群入口代理，istio-ingress中运行的是这种代理
	Ingress NodeType = "ingress"
	// 独立运行的，作为L7/L4路由器的代理，istio-ingressgateway、istio-egressgateway中运行的是这种代理
	Router NodeType = "router"
)</pre>
<div class="blog_h2"><span class="graybg">push_context.go</span></div>
<div class="blog_h3"><span class="graybg">EndpointShard</span></div>
<p>端点分片，存储<span style="background-color: #c0c0c0;">单个服务</span>的<span style="background-color: #c0c0c0;">单个注册表</span>中的<span style="background-color: #c0c0c0;">单个分片</span>的名称及其端点列表：</p>
<pre class="crayon-plain-tag">type EndpointShard struct {
	Shard   string
	Entries []*IstioEndpoint
}</pre>
<div class="blog_h3"><span class="graybg">EndpointShardsByService</span></div>
<p>存储<span style="background-color: #c0c0c0;">单个服务的所有分片</span>信息。使用K8S作为注册表时，Shards通常只有一个元素，其键是"Kubernetes"，其值是Shard名为"Kubernetes"的EndpointShard</p>
<pre class="crayon-plain-tag">type EndpointShardsByService struct {
	// 这种结构下，每个注册表只能有一个分片
	// 映射的键是注册表名称
	Shards map[string]*EndpointShard
	ServiceAccounts map[string]bool
}</pre>
<div class="blog_h3"><span class="graybg">IstioEndpoint</span></div>
<p>此结构用于代替NetworkEndpoint和ServiceInstance，做了以下优化：</p>
<ol>
<li>ServicePortName字段代替ServicePort字段。原因是进行了端点回调（endpoint callbacks are made）时端口号、协议可能不可用</li>
<li>合并两个结构，原因是一对一关系</li>
<li>不再持有Service的指针。原因是接收到端点时，服务对象可能不可用</li>
<li>提供缓存的EnvoyEndpoint对象，避免为每次请求/每个客户端重新分配</li>
</ol>
<pre class="crayon-plain-tag">type IstioEndpoint struct {
	// 工作负载的标签
	Labels map[string]string
	Family AddressFamily
	Address string
	EndpointPort uint32
	// 跟踪端口的名称，避免最终一致性相关的问题。某些情况下Endpoint先于Service可见，这时进行端口查找会失败
	// 端口名到号的映射将在集群计算时进行
	ServicePortName string
	// 用于遥测
	UID string
	// 缓存的LbEndpoint（来自Envoy Go客户端包），通过数据转换得到，避免重复计算
	EnvoyEndpoint *endpoint.LbEndpoint
	ServiceAccount string
}</pre>
<div class="blog_h3"><span class="graybg">PushContext</span></div>
<p><a href="#PushContext">参考下文</a>。</p>
<div class="blog_h2"><span class="graybg">service.go</span></div>
<div class="blog_h3"><span class="graybg">Service</span></div>
<p>此结构对Istio服务进行建模，每个服务具有全限定的名称（FQDN），一个或多个监听的端口，一个可选的和服务关联的负载均衡器/虚拟IP地址（FQDN解析到此地址）。</p>
<p>例如，在K8S中，服务kubernetes关联到FQDN kubernetes.default.svc.cluster.local，具有虚拟IP地址10.96.0.1，监听443端口。</p>
<pre class="crayon-plain-tag">// 主机名，可能是通配符
type Hostname string

type Service struct {
	// 主机名
	Hostname Hostname 

	// 服务的负载均衡器IPv4地址
	Address string 

	// 多集群支持，服务在每个集群中的负载均衡器IPv4地址
	ClusterVIPs map[string]string 

	// 监听的端口列表
	Ports PortList 

	// 运行服务的账号
	ServiceAccounts []string 

	// 指示服务是否位于网格外部，这种服务通过ServiceEntry定义
	MeshExternal bool

	// 在路由之前，如何解析服务的实例
	Resolution Resolution

	// 服务创建时间
	CreationTime time.Time 

	// 额外的属性，Mixer/RBAC 策略会用到
	Attributes ServiceAttributes
}</pre>
<div class="blog_h3"><span class="graybg">Resolution</span></div>
<p>用于指示在路由请求之前，如何解析出服务的实例：</p>
<pre class="crayon-plain-tag">type Resolution int

const (
	// 代理根据自己本地的负载均衡池决定使用哪个端点
	ClientSideLB Resolution = iota
	// 代理进行DNS解析，并把请求发给解析结果
	DNSLB
	// 代理直接根据请求者指定的目的地址
	Passthrough
)</pre>
<div class="blog_h3"><span class="graybg">ServiceInstance</span></div>
<p>服务的特定版本的一个实例，绑定到一个NetworkEndpoint：</p>
<pre class="crayon-plain-tag">type ServiceInstance struct {
	// 关联的端点
	Endpoint         NetworkEndpoint 
	// 所属的服务
	Service          *Service        
	// 标签集
	Labels           Labels          
	AvailabilityZone string          
	ServiceAccount   string          
}</pre>
<div class="blog_h3"><span class="graybg">NetworkEndpoint</span></div>
<p>建模<span style="background-color: #c0c0c0;">关联到服务的实例</span>的网络地址：</p>
<pre class="crayon-plain-tag">type NetworkEndpoint struct {
	// 地址族
	Family AddressFamily
	Address string
	Port int
	ServicePort *Port
	UID string
}</pre>
<div class="blog_h3"><span class="graybg">Port</span></div>
<p>对服务监听的网络端口进行建模：</p>
<pre class="crayon-plain-tag">type Port struct {
	// 易读的端口名，如果服务包含多个端口，则此字段必须
	Name string 

	// 服务的端口号，非必须关联到服务背后的实例的端口
	Port int 

	// 使用的协议
	Protocol Protocol 
}

// 端口集
type PortList []*Port</pre>
<div class="blog_h3"><span class="graybg">Protocol</span></div>
<p>通信协议：</p>
<pre class="crayon-plain-tag">type Protocol string
// 目前支持的协议枚举
const (
	ProtocolGRPC Protocol = "GRPC"
	ProtocolHTTP Protocol = "HTTP"
	ProtocolHTTP2 Protocol = "HTTP2"
	ProtocolHTTPS Protocol = "HTTPS"
	ProtocolTCP Protocol = "TCP"
	ProtocolTLS Protocol = "TLS"
	ProtocolUDP Protocol = "UDP"
	ProtocolMongo Protocol = "Mongo"
	ProtocolRedis Protocol = "Redis"
	ProtocolUnsupported Protocol = "UnsupportedProtocol"
)</pre>
<div class="blog_h3"><span class="graybg">TrafficDirection</span></div>
<p>流量的方向：</p>
<pre class="crayon-plain-tag">type TrafficDirection string
const (
	TrafficDirectionInbound TrafficDirection = "inbound"
	TrafficDirectionOutbound TrafficDirection = "outbound"
)</pre>
<div class="blog_h3"><span class="graybg">ServiceDiscovery</span></div>
<p>此接口用于发现服务的实例：</p>
<pre class="crayon-plain-tag">type ServiceDiscovery interface {
	// 列出所有服务
	Services() ([]*Service, error)

	// 废弃，根据主机名获得服务
	GetService(hostname Hostname) (*Service, error)

	// 取回服务的、匹配指定标签集的实例
	Instances(hostname Hostname, ports []string, labels LabelsCollection) ([]*ServiceInstance, error)
	InstancesByPort(hostname Hostname, servicePort int, labels LabelsCollection) ([]*ServiceInstance, error)

	// 返回和指定代理同地协作（co-located）的实例，所谓co-located是指运行在相同的命名空间和安全上下文
	//
	// 对于以Sidecar方式运行的代理，返回非空的切片；对于独立运行的代理，返回空切片
	GetProxyServiceInstances(*Proxy) ([]*ServiceInstance, error)

	// 返回一个IPv4地址关联的管理端口
	ManagementPorts(addr string) PortList

	// 返回一个IPv4地址关联的健康检查探针
	WorkloadHealthCheckInfo(addr string) ProbeList
}</pre>
<div class="blog_h1"><span class="graybg">发现服务启动过程</span></div>
<p><span style="background-color: #c0c0c0;">Pilot（Pilot Discovery，其对应的客户端组件是Pilot Agent）</span>是Istio最关键的组件，它的职责是将用户提供的、简单的、CRD形式的配置文件，转换为Envoy能理解的格式，并推送给Envoy以更新代理配置。</p>
<p>Pilot的启动逻辑位于bootstrap包中。</p>
<div class="blog_h2"><span class="graybg">如何调试</span></div>
<p>我们需要进行单步跟踪，才能了解Pilot在初始化期间做了哪些事情。为了调试的方便，我们在K8S集群外部启动Pilot服务。参考下面的启动参数：</p>
<pre class="crayon-plain-tag">export POD_NAME=istio-pilot-54f79f8bd7-w8b2g
export POD_NAMESPACE=istio-system

# pilot-discovery的入口点代码位于 pilot/cmd/pilot-discovery/main.go

                # 输出详尽日志
pilot discovery --log_output_level=default:debug --log_caller=default --domain=k8s.gmem.cc \
                # 提供kubeconfig，注意不支持--masterUrl，不提供此参数Istio会假设在集群内部运行
                --kubeconfig=/home/alex/.kube/config \
                # 配置文件
                --meshConfig=pilot/meshConfig</pre>
<p>其中meshConfig可以直接从K8S集群中提取，此配置存放在Configmap中，名称为istio-system/istio。内容如下：</p>
<pre class="crayon-plain-tag">disablePolicyChecks: false
enableTracing: true
accessLogFile: "/dev/stdout"
policyCheckFailOpen: false
sdsUdsPath: ""
sdsRefreshDelay: 15s
defaultConfig:
  connectTimeout: 10s
  configPath: "/etc/istio/proxy"
  binaryPath: "/usr/local/bin/envoy"
  serviceCluster: istio-proxy
  drainDuration: 45s
  parentShutdownDuration: 1m0s
  proxyAdminPort: 15000
  concurrency: 0
  zipkinAddress: zipkin.istio-system:9411
  controlPlaneAuthPolicy: NONE
  discoveryAddress: istio-pilot.istio-system:15007</pre>
<div class="blog_h2"><span class="graybg">启动过程概述</span></div>
<p>Pilot的入口点位于pilot/cmd/pilot-discovery/main.go，它使用包<a href="https://github.com/spf13/cobra">spf13.cobra</a>来管理一组子命令，Pilot核心功能由discovery子命令实现。</p>
<p>入口点的启动过程如下：</p>
<ol>
<li>解析命令行参数</li>
<li>创建主服务，服务建模在结构bootstrap.Server中
<ol>
<li>调用initKubeClient方法，初始化K8S客户端</li>
<li>调用initClusterRegistries方法，初始化clusterStore字段</li>
<li>调用initMesh方法，初始化mesh字段</li>
<li>调用initMixerSan方法，初始化Mixer服务的SAN</li>
<li>调用initConfigController方法，初始化configController字段。通过addStartFunc延后调用配置控制器的Run方法</li>
<li>如果启用了Istio的IngressController功能，则调用configaggregate.MakeCache，包装configController，使其能处理Ingress类型的资源</li>
<li>调用initServiceControllers方法，此控制器能够从底层服务发现机制中获取Istio抽象服务。通过addStartFunc延后调用服务控制器的Run方法</li>
<li>调用initDiscoveryService方法，初始化发现服务，发现服务依赖于前面创建的配置控制器、服务控制器，对xDS的支持有发现服务提供</li>
<li>调用initMonitor方法，初始化Pilot监控服务</li>
<li>调用initMultiClusterController方法，初始化多集群控制器，目前可以用于跨越多个K8S集群的服务网格</li>
<li>启动ControlZ监听器</li>
</ol>
</li>
<li>启动主服务，其实就是执行先前注册的延迟启动函数：
<ol>
<li>启动配置控制器</li>
<li>启动Ingress同步器</li>
<li>启动Service控制器</li>
<li>启动HTTP服务、gRPC服务、安全gRPC服务</li>
</ol>
</li>
<li>等待停止信号</li>
</ol>
<div class="blog_h2"><span class="graybg">bootstrap.Server</span></div>
<p>表示Pilot主服务，它不是一个简单的（侦听单个端口）服务，而是很多服务的集合。包含的字段如下：</p>
<pre class="crayon-plain-tag">type Server struct {
	// xDS服务管理服务器的实现
	EnvoyXdsServer    *envoyv2.DiscoveryServer
	ServiceController *aggregate.Controller
        // 网格配置
	mesh             *meshconfig.MeshConfig
        // 配置控制器，负责监控K8S集群中，Istio的自定义资源的变更
	configController model.ConfigStoreCache
        // Mixer服务的SAN列表
	mixerSAN         []string
        // K8S客户端
	kubeClient       kubernetes.Interface
        // 启动需要执行的函数列表
	startFuncs       []startFunc
        // 集群信息存储
	clusterStore     *clusterregistry.ClusterStore
	httpServer       *http.Server
	grpcServer       *grpc.Server
	secureGRPCServer *grpc.Server
        // 发现服务，支持xDS协议
	discoveryService *envoy.DiscoveryService
        // 包装configController，提供针对Gateways、EnvoyFilter、ServiceEntries等资源的强类型接口
	istioConfigStore model.IstioConfigStore
        // HTTP请求多路分发器，根据URL匹配来决定由哪个handler处理请求
	mux              *http.ServeMux
        // 监控各种K8S原生对象，并将更新推送给EDS等组件
	kubeRegistry     *kube.Controller
}</pre>
<div class="blog_h2"><span class="graybg">initKubeClient</span></div>
<p>仅仅当args.Service.Registries包含Kubernetes，才会创建K8S客户端：</p>
<pre class="crayon-plain-tag">for _, r := range args.Service.Registries {
	if serviceregistry.ServiceRegistry(r) == serviceregistry.KubernetesRegistry {
		needToCreateClient = true
		break
	}
}</pre>
<div class="blog_h2"><span class="graybg">initClusterRegistries</span></div>
<p>仅当创建了K8S客户端，才会调用此方法。</p>
<p>此方法创建的对象很简单，就是创建一个空的clusterregistry.ClusterStore：</p>
<pre class="crayon-plain-tag">type ClusterStore struct {
	rc        map[string]*RemoteCluster
	storeLock sync.RWMutex
}</pre><br />
<pre class="crayon-plain-tag">type RemoteCluster struct {
	Cluster        *k8s_cr.Cluster
	FromSecret     string
	Client         *clientcmdapi.Config
	ClusterStatus  string
	Controller     *kube.Controller
	ControlChannel chan struct{}
}</pre>
<div class="blog_h2"><span class="graybg">initMixerSan</span></div>
<p>Istio组件Mixer和Pilot需要相互通信，如果mesh.DefaultConfig.ControlPlaneAuthPolicy为mTLS，也就是说Mixer - Pilot通信需要双向TLS认证时，才调用此方法：</p>
<pre class="crayon-plain-tag">func (s *Server) initMixerSan(args *PilotArgs) error {
	if s.mesh.DefaultConfig.ControlPlaneAuthPolicy == meshconfig.AuthenticationPolicy_MUTUAL_TLS {
		s.mixerSAN = envoy.GetMixerSAN(args.Config.ControllerOptions.DomainSuffix, args.Namespace)
	}
	return nil
}</pre>
<div class="blog_h2"><span class="graybg">initConfigController</span></div>
<p>此方法转调makeKubeConfigController方法创建配置控制器：</p>
<pre class="crayon-plain-tag">func (s *Server) makeKubeConfigController(args *PilotArgs) (model.ConfigStoreCache, error) {
	kubeCfgFile := s.getKubeCfgFile(args)
        // crd.Client，负责Istio自定义资源的CRUD操作
        // 为每组API的每个版本（apiVersion）创建独立的REST客户端
        // apiVersion包括networking.istio.io/v1alpha3、config.istio.io/v1alpha2、authentication.istio.io/v1alpha1、rbac.istio.io/v1alpha1
	configClient, err := crd.NewClient(kubeCfgFile, "", model.IstioConfigTypes, args.Config.ControllerOptions.DomainSuffix)
        // 注册CRD
	if !args.Config.DisableInstallCRDs {
		if err = configClient.RegisterResources(); err != nil {
			return nil, multierror.Prefix(err, "failed to register custom resources.")
		}
	}
        // 创建配置控制器
	return crd.NewController(configClient, args.Config.ControllerOptions), nil
}</pre>
<div class="blog_h3"><span class="graybg">crd.controller</span></div>
<p>initConfigController方法返回值的真实类型是crd.controller，除了实现ConfigStoreCache 接口的方法以外，它还负责管理Informer。它为每种CR创建一个Informer：</p>
<pre class="crayon-plain-tag">for _, schema := range client.ConfigDescriptor() {
	out.addInformer(schema, options.WatchedNamespace, options.ResyncPeriod)
}</pre>
<p>Pilot中的控制器的工作模式基本都是一样的，<a href="#resources-processing-framework">下文</a>我们详细分析。</p>
<div class="blog_h3"><span class="graybg">ingress.controller</span></div>
<p>如果必要（允许Istio处理Ingress资源的话），initConfigController还会创建IngressController：</p>
<pre class="crayon-plain-tag">// 下面的方法创建了一个监听Ingress资源的控制器
ingress.NewController(s.kubeClient, s.mesh, args.Config.ControllerOptions)</pre>
<div class="blog_h3"><span class="graybg">config/aggregate</span></div>
<p>包config/aggregate可以将处理不同资源类型的model.ConfigStoreCache组合起来，形成一个更大的model.ConfigStoreCache。接口保持不变，根据资源类型委托给适当的被包装的子对象。</p>
<p>默认的配置下，Pilot会把Config控制器、Ingress控制器组合起来：</p>
<pre class="crayon-plain-tag">configController, err := configaggregate.MakeCache([]model.ConfigStoreCache{
	s.configController,
	ingress.NewController(s.kubeClient, s.mesh, args.Config.ControllerOptions),
})</pre>
<div class="blog_h2"><span class="graybg">Istio作为Ingress控制器</span></div>
<p>Istio网格的入口通常是Gateway，但是它也能够处理K8S原生的Ingress资源，也就是说Istio可以扮演K8S通常意义上的Ingress Controller。实际<span style="background-color: #c0c0c0;">上入口流量由istio-ingress这个deployment的Pod处理</span>。</p>
<p>哪些Ingress资源会被管理，由主服务的mesh.IngressControllerMode配置决定：</p>
<ol>
<li>MeshConfig_OFF（0）：禁用Istio的IngressController功能</li>
<li>MeshConfig_DEFAULT（1）：作为整个K8S集群默认的Ingress控制器</li>
<li>MeshConfig_STRICT（2）：仅仅处理包含了注解kubernetes.io/ingress.class，且值等于mesh.IngressClass（默认istio）的Ingress资源</li>
</ol>
<p>创建IngressController的逻辑位于initConfigController方法中。</p>
<div class="blog_h2"><span class="graybg">initServiceControllers</span></div>
<p>此方法首先创建一个聚合控制器：</p>
<pre class="crayon-plain-tag">serviceControllers := aggregate.NewController()</pre>
<p>serviceregistry/aggregate中定义的“聚合控制器”，能够管理多个平台（例如K8S）的服务发现机制（ServiceRegistry）。</p>
<p>然后，它调用createK8sServiceControllers方法创建kube.Controller：</p>
<pre class="crayon-plain-tag">func (s *Server) createK8sServiceControllers(serviceControllers *aggregate.Controller, args *PilotArgs) (err error) { 
	kubectl := kube.NewController(s.kubeClient, args.Config.ControllerOptions) // 创建控制器
	s.kubeRegistry = kubectl // 赋值给主服务的kubeRegistry字段
	serviceControllers.AddRegistry( // 添加到聚合控制器的registries中
		aggregate.Registry{
			Name:             serviceregistry.KubernetesRegistry, // 服务注册表类型
			ClusterID:        string(serviceregistry.KubernetesRegistry), // 如果多个同一类型的注册表存在则有意义
			ServiceDiscovery: kubectl, // 服务发现，可获得Istio的抽象服务（ServiceInstance）
			ServiceAccounts:  kubectl, // 暴露Istio的抽象服务账号（字符串）
			Controller:       kubectl, // 控制器，可通知Service、ServiceInstance的变更
		}
	)
}</pre>
<p>然后，创建ServiceEntry发现服务，并添加到聚合控制器：</p>
<pre class="crayon-plain-tag">serviceEntryStore := external.NewServiceDiscovery(s.configController, s.istioConfigStore)
serviceEntryRegistry := aggregate.Registry{
	Name:             "ServiceEntries",
	Controller:       serviceEntryStore,
	ServiceDiscovery: serviceEntryStore,
	ServiceAccounts:  serviceEntryStore,
}
serviceControllers.AddRegistry(serviceEntryRegistry)</pre>
<p>最后，注册ServiceController的延迟启动。</p>
<div class="blog_h3"><span class="graybg">kube.Controller</span></div>
<p>kube.Controller能够监控K8S中service、endpoint、node、pod等对象。它实现了model.Controller、model.ServiceDiscovery、model.ServiceAccounts等接口。</p>
<p>该结构包含以下字段：</p>
<pre class="crayon-plain-tag">type Controller struct {
	domainSuffix string
	client    kubernetes.Interface
	queue     Queue   // 事件队列
	services  cacheHandler  // 各种K8S对象的处理器
	endpoints cacheHandler
	nodes     cacheHandler
	pods *PodCache 
	// 允许此控制器读取环境信息，并发布状态信息
	Env *model.Environment
	// 多集群环境下识别集群
	ClusterID string
	// XDSUpdater推送EDS变更到ADS模型
	EDSUpdater model.XDSUpdater
	// 用于请求全局配置的变更
	ConfigUpdater model.ConfigUpdater
	stop chan struct{}
}</pre>
<div class="blog_h2"><span class="graybg"><a id="initDiscoveryService"></a>initDiscoveryService</span></div>
<p>此方法首先创建一个新的发现服务：</p>
<pre class="crayon-plain-tag">discovery, err := envoy.NewDiscoveryService(
	s.ServiceController,
	s.configController,
	environment,  // 提供聚合性的上下文信息API
	args.DiscoveryOptions, // 监听地址等信息
)

s.discoveryService = discovery
s.mux = s.discoveryService.RestContainer.ServeMux</pre>
<p>结构envoy.DiscoveryService是真正的发现服务，是Pilot的核心。它负责推送配置给Envoy。</p>
<p>然后，创建一个gRPC服务器、一个HTTP服务器：</p>
<pre class="crayon-plain-tag">s.initGrpcServer()

s.httpServer = &amp;http.Server{
	Addr:    args.DiscoveryOptions.HTTPAddr,
	Handler: discovery.RestContainer}</pre>
<p>创建xDS服务：</p>
<pre class="crayon-plain-tag">// 基于Envoy协议v2版本
s.EnvoyXdsServer = envoyv2.NewDiscoveryServer(environment, istio_networking.NewConfigGenerator(args.Plugins))
// 用于将envoy/v2从envoy/解耦
s.EnvoyXdsServer.ConfigUpdater = s.discoveryService</pre>
<p>注册envoy包的全局函数：</p>
<pre class="crayon-plain-tag">// 当配置变更时，请求一个更新
envoy.Push = s.EnvoyXdsServer.Push
envoy.BeforePush = s.EnvoyXdsServer.BeforePush</pre>
<p>将xDS服务注册到上述gRPC服务器上：</p>
<pre class="crayon-plain-tag">s.EnvoyXdsServer.Register(s.grpcServer)</pre>
<p>在使用K8S作为后端时，还会配置kube.Controller：</p>
<pre class="crayon-plain-tag">s.kubeRegistry.Env = environment
s.kubeRegistry.ConfigUpdater = discovery
s.kubeRegistry.EDSUpdater = s.EnvoyXdsServer</pre>
<p>最后，注册httpServer、grpcServer的延迟启动。</p>
<div class="blog_h3"><span class="graybg">NewDiscoveryService</span></div>
<p>proxy/envoy包中定义的这个函数，负责创建一个新的发现服务。它会创建一个RESTful容器并将发现服务注册到此容器：</p>
<pre class="crayon-plain-tag">container := restful.NewContainer()
out.Register(container)
out.RestContainer = container</pre>
<p>它还会向serviceregistry/kube.Controller注册Service、Instance的处理器，对于K8S来说，当Serivce、Endpoint发生变更时，会获得通知。</p>
<pre class="crayon-plain-tag">serviceHandler := func(*model.Service, model.Event) { out.clearCache() }
ctl.AppendServiceHandler(serviceHandler)
instanceHandler := func(*model.ServiceInstance, model.Event) { out.clearCache() }
err := ctl.AppendInstanceHandler(instanceHandler)</pre>
<p>这些处理器的逻辑很简单，就是清空Envoy缓存，但是kube.Controller附加了一些<a href="#Service">前置逻辑</a>。</p>
<div class="blog_h1"><span class="graybg">发现服务如何处理K8S资源</span></div>
<div class="blog_h2"><span class="graybg"><a id="resources-processing-framework"></a>资源处理框架</span></div>
<p>不论是K8S内置资源，而是Istio的CR，都由client-go负责List&amp;Watch，并把事件会传递给config/kube/crd/controller.go、config/kube/ingress/controller.go、serviceregistry/kube/controller.go等控制器中的回调函数处理：</p>
<pre class="crayon-plain-tag">informer.AddEventHandler(
	cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
// 处理Prometheus指标
			k8sEvents.With(prometheus.Labels{"type": otype, "event": "add"}).Add(1)
// 放入队列
			c.queue.Push(kube.NewTask(handler.Apply, obj, model.EventAdd))
		},</pre>
<p>可以看到，此回调函数的逻辑仅仅是将事件及其处理函数封装为Task并存放到控制器的队列中：</p>
<pre class="crayon-plain-tag">func (q *queueImpl) Push(item Task) {
	q.cond.L.Lock()
	defer q.cond.L.Unlock()
	if !q.closing {
		q.queue = append(q.queue, item)
	}
        // 唤醒等待cond的一个goroutine
	q.cond.Signal()
}
type Task struct {
	handler Handler
	obj     interface{}
	event   model.Event
}</pre>
<p>队列的Run循环则取出事件并调用其处理函数：</p>
<pre class="crayon-plain-tag">if err := item.handler(item.obj, item.event); err != nil {
   ...
}</pre>
<p>处理函数就是ChainHandler结构的Apply方法，此结构允许针对一个事件串行的调用多个实际的处理函数：</p>
<pre class="crayon-plain-tag">type ChainHandler struct {
	funcs []Handler
}

func (ch *ChainHandler) Apply(obj interface{}, event model.Event) error {
	for _, f := range ch.funcs {
		if err := f(obj, event); err != nil {
// 链中任何一个实际处理函数出错，都会导致中止处理
			return err
		}
	}
	return nil
}</pre>
<p>ChainHandler是在对应控制器的初始化阶段创建的，实际处理函数也在那时注册。</p>
<div class="blog_h2"><span class="graybg">Envoy缓存清除</span></div>
<p>Pilot对很多K8S资源变更的响应，都是简单的清除掉Envoy的配置缓存。它们调用的是envoy包的v1版本的DiscoveryService.clearCache方法：</p>
<pre class="crayon-plain-tag">func (ds *DiscoveryService) clearCache() {
	ds.ConfigUpdate(true)
}</pre>
<p>可以看到，此方法仅仅是简单的请求一次Envoy配置的Full Push。 也就是说<span style="background-color: #c0c0c0;">Pilot在配置变更的情况下，通常会完整的推送Envoy配置</span>，幸好推送过程具有防抖动支持，而且配置变更不是频繁操作，否则可能出现性能问题。</p>
<div class="blog_h2"><span class="graybg">Ingress的处理</span></div>
<p>config/kube/ingress/controller.go注册了多个处理函数，第一个处理函数判断是否informer已经完全同步，如果不是，则中止处理。第二个处理函数则是简单的清除Envoy缓存配置。</p>
<div class="blog_h2"><span class="graybg">CRD的处理</span></div>
<p>第一个处理函数是控制器的notify方法，此函数判断是否informer已经完全同步，如果不是，则中止处理：</p>
<pre class="crayon-plain-tag">func (c *controller) notify(obj interface{}, event model.Event) error {
	if !c.HasSynced() {
		return errors.New("waiting till full synchronization")
	}
        // 检查对象是否是DeletedFinalStateUnknown，如果否，对其调用MetaNamespaceKeyFunc，看看能否获得缓存键
	_, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
	return nil
}</pre>
<p>第二个处理函数，首先将IstioObject转换为model.Config，然后<span style="background-color: #c0c0c0;">转调</span>在启动阶段通过s.configController.RegisterEventHandler()为<span style="background-color: #c0c0c0;">不同资源类型注册的实际处理函数</span>：</p>
<pre class="crayon-plain-tag">c.kinds[typ].handler.Append(func(object interface{}, ev model.Event) error {
	item, ok := object.(IstioObject)
	config, err := ConvertObject(schema, item, c.client.domainSuffix)
	f(*config, ev)
})</pre>
<p><span class="graybg">MeshPolicy、VirtualService、DestinationRule、Gateway的实际处理函数，都仅仅是清除掉所有Envoy缓存。</span></p>
<div class="blog_h3"><span class="graybg">ServiceEntry</span></div>
<p>此CRD的实际处理函数的逻辑复杂一些。首先将model.Config.Spec转换为具体的Istio结构：</p>
<pre class="crayon-plain-tag">serviceEntry := config.Spec.(*networking.ServiceEntry)</pre>
<p>然后，将ServiceEntry转换为Istio内部的“服务”（model.Service）并异步处理 。model.Service具有FQDN、若干端口、可选的负载均衡/虚拟IP：</p>
<pre class="crayon-plain-tag">// 一个ServiceEntry可能转换为多个服务
services := convertServices(serviceEntry, config.CreationTimestamp.Time)
for _, handler := range c.serviceHandlers {
	for _, service := range services {
		go handler(service, event)
	}
}</pre>
<p>最后，将ServiceEntry转换为Istio内部的“实例”（model.ServiceInstance）并异步处理。model.ServiceInstance关联到一个网络端点（ip:port），具有一个服务描述和一组描述服务版本的标签：</p>
<pre class="crayon-plain-tag">instances := convertInstances(serviceEntry, config.CreationTimestamp.Time)
for _, handler := range c.instanceHandlers {
	for _, instance := range instances {
		go handler(instance, event)
	}
}</pre>
<p>不管是服务还是实例，go handler ...的逻辑仍然仅仅是清除掉所有Envoy缓存。</p>
<p>在这里可以注意到，ServiceEntry所（通常）表示的外部服务、和一般性的K8S服务，在Istio内部具有相同的表示 —— Service + ServiceInstance。</p>
<div class="blog_h2"><span class="graybg">其它K8S原生资源</span></div>
<p>第一个处理函数是控制器的notify方法，此函数判断是否informer已经完全同步，如果不是，则中止处理。</p>
<div class="blog_h3"><span class="graybg">Node</span></div>
<p>对于节点资源，默认情况下没有其它处理逻辑。</p>
<div class="blog_h3"><span class="graybg">Pod</span></div>
<p>pilot的serviceregistry包维护了一个最终一致性的Pod缓存：</p>
<pre class="crayon-plain-tag">func newPodCache(ch cacheHandler, c *Controller) *PodCache {
	out := &amp;PodCache{
		cacheHandler: ch,
		c:            c,
		keys:         make(map[string]string),
	}
	ch.handler.Append(func(obj interface{}, ev model.Event) error {
		return out.event(obj, ev)
	})
	return out
}</pre>
<p>Pod事件会触发PodCache.event方法的调用，此方法会更新Pod的IP地址和namespace/name形式的Key之间的映射关系，并且<a href="#WorkloadUpdate">为EDS服务更新工作负载</a>：</p>
<pre class="crayon-plain-tag">func (pc *PodCache) event(obj interface{}, ev model.Event) error {
	// 注意，Pod被删除后，可能得到*v1.Pod，也可能得到DeletionFinalStateUnknown这个标记对象
	pod, ok := obj.(*v1.Pod)
	if !ok {
		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
		if !ok {
			return fmt.Errorf("couldn't get object from tombstone %+v", obj)
		}
                // 可以从DeletionFinalStateUnknown.Obj获得原始对象
		pod, ok = tombstone.Obj.(*v1.Pod)
		if !ok {
			return fmt.Errorf("tombstone contained object that is not a pod %#v", obj)
		}
	}

	ip := pod.Status.PodIP
	// 新创建的Pod的IP地址为空，直到通过UpdateStatus分配了IP
	if len(ip) &gt; 0 {
		key := KeyFunc(pod.Name, pod.Namespace)
		switch ev {
		case model.EventAdd:
			switch pod.Status.Phase {
			case v1.PodPending, v1.PodRunning:
				// 锁定并更新Pod缓存
				pc.rwMu.Lock()
				pc.keys[ip] = key
				pc.rwMu.Unlock()
// model.XDSUpdater
// 更新工作负载，在EDS看来工作负载的ID是IP地址
				if pc.c.EDSUpdater != nil {
					pc.c.EDSUpdater.WorkloadUpdate(ip, pod.ObjectMeta.Labels, pod.ObjectMeta.Annotations)
				}
			}
		case model.EventUpdate:
			switch pod.Status.Phase {
			case v1.PodPending, v1.PodRunning:
				// 更新Pod缓存
			default:
				// 其它状态下，删除Pod缓存

			}
		case model.EventDelete:
			// 删除Pod缓存
		}
	}

	return nil
}</pre>
<div class="blog_h3"><span class="graybg"><a id="Service"></a>Service</span></div>
<pre class="crayon-plain-tag">// 当接收到K8S Service更新时触发
c.services.handler.Append(func(obj interface{}, event model.Event) error {
	svc := *obj.(*v1.Service)

	// 不处理kube-system中定义的service
	if svc.Namespace == meta_v1.NamespaceSystem {
		return nil
	}
	if c.EDSUpdater != nil {
		hostname := svc.Name + "." + svc.Namespace
		ports := map[string]uint32{}
		portsByNum := map[uint32]string{}

		for _, port := range svc.Spec.Ports {
			ports[port.Name] = uint32(port.Port)
			portsByNum[uint32(port.Port)] = port.Name
		}
		// 变更服务信息后调用
		c.EDSUpdater.SvcUpdate(c.ClusterID, hostname, ports, portsByNum)
		// Bypass convertService and the cache invalidation.

		// 请求完整的配置更新并返回
		c.ConfigUpdater.ConfigUpdate(true)
		return nil
	}
// f的逻辑是清空缓存，默认不会执行到这个旧逻辑，只有当禁用了Direct EDS（环境变量PILOT_DIRECT_EDS=0）才会执行
	if svcConv := convertService(svc, c.domainSuffix); svcConv != nil {
		f(svcConv, event)
	}
	return nil
})</pre>
<div class="blog_h3"><span class="graybg">Endpoint</span></div>
<pre class="crayon-plain-tag">// 当接收到K8S Endpoint更新时触发
c.endpoints.handler.Append(func(obj interface{}, event model.Event) error {
	ep := obj.(*v1.Endpoints)

	// 不处理kube-system中的端点
	if ep.Namespace == meta_v1.NamespaceSystem {
		return nil
	}

	if c.EDSUpdater != nil {
// 更新端点
		c.updateEDS(ep)
	} else {
// 旧逻辑，如果端点对应的服务存在，则清空缓存
		if item, exists := c.serviceByKey(ep.Name, ep.Namespace); exists {
			if svc := convertService(*item, c.domainSuffix); svc != nil {
				f(&amp;model.ServiceInstance{Service: svc}, event)
			}
		}
	}
	return nil
})</pre>
<div class="blog_h1"><span class="graybg">发现服务如何更新和推送配置</span></div>
<p>&nbsp;</p>
<p>Pilot Discovery和Envoy代理配置的更新（这些配置更新都是由于K8S资源变化而引发）、推送有关的逻辑，主要分布在三个包中：</p>
<ol>
<li>proxy/envoy，主要的逻辑在此，包括xDS的实现</li>
<li>model，推送上下文（PushContext）定义在此包中，此结构在每次推送时创建，持有和本次推送相关的所有上下文信息</li>
<li>networking，负责生成Envoy的配置文件格式</li>
</ol>
<p>推送<span style="background-color: #c0c0c0;">由DiscoveryService.ConfigUpdate()方法触发</span>，<span style="background-color: #c0c0c0;">转调DiscoveryServer.Push</span>()，后者<span style="background-color: #c0c0c0;">调用PushContext</span>初始化一系列相关的数据结构，并调用<span style="background-color: #c0c0c0;">networking包中ConfigGenerator的方法生成Envoy v2的结构</span>，然后向所有连接到Pilot的Envoy代理发起推送。</p>
<div class="blog_h2"><span class="graybg">DiscoveryService</span></div>
<p>此结构负责发布服务、集群、路由给所有的代理。</p>
<pre class="crayon-plain-tag">type DiscoveryService struct {
	*model.Environment
	webhookClient   *http.Client
	webhookEndpoint string
	// 缓存，目前的实现，在任何路由、服务、端点发生变化时，都会Flush缓存。应当实现某种缓存过期策略
	// 避免反复Flush或者过期缓存滞留其中
	sdsCache *discoveryCache
    // 发现服务会注册REST路由到此容器
	RestContainer *restful.Container
	// 是否在去回弹之后，需要进行一个完整推送。如果仅仅要求EDS则为false
	fullPush bool
}</pre>
<p>缓存的结构如下，是一种通用的存储：</p>
<pre class="crayon-plain-tag">type discoveryCacheEntry struct {
	data          []byte // 什么都可以存
	hit           uint64 // atomic
	miss          uint64 // atomic
	resourceCount uint32
}

type discoveryCache struct {
	name     string
	disabled bool
	mu       sync.RWMutex
	cache    map[string]*discoveryCacheEntry
}</pre>
<div class="blog_h3"><span class="graybg">ClearCache</span></div>
<p>此方法清除缓存，目前的实现很简陋，就是发起<pre class="crayon-plain-tag">ds.ConfigUpdate(true)</pre>调用，会请求完整的配置推送。</p>
<div class="blog_h3"><span class="graybg">ConfigUpdate</span></div>
<p><a id="ConfigUpdater-Impl"></a>此方法实现ConfigUpdater接口。逻辑如下：</p>
<pre class="crayon-plain-tag">// 100ms
DebounceAfter = envDuration("PILOT_DEBOUNCE_AFTER", 100*time.Millisecond)
// 10s
DebounceMax = envDuration("PILOT_DEBOUNCE_MAX", 10*time.Second)

func (ds *DiscoveryService) ConfigUpdate(full bool) {
    // 设置完整推送标记
	if full {
		ds.fullPush = true
	}
    // 去回弹逻辑
	if DebounceAfter &gt; 0 {
        // 如果定时器标记已经设置，不做任何操作，这意味着频繁调用ConfigUpdate不会引发不良后果
		if !clearCacheTimerSet {
            // 此标记在实际执行推送时清除
			clearCacheTimerSet = true
            // 记录去回弹操作开始时间戳
			startDebounce := lastClearCacheEvent
            // 100ms后开始第一次去回弹判断
			time.AfterFunc(DebounceAfter, func() {
				ds.debouncePush(startDebounce)
			})
		}
		return
	}

    // 旧逻辑
    // 如果上一次配置变更发生在1秒前，执行推送
	if time.Since(lastClearCacheEvent) &gt; 1*time.Second {
		ds.doPush()
		return
	}
    // 如果上一次变更在1秒内，但是上一次推送大于clearCacheTime，也推送
	if time.Since(lastClearCache) &gt; time.Duration(clearCacheTime)*time.Second {
		ds.doPush()
		return
	}
    // 否则
	if !clearCacheTimerSet {
		clearCacheTimerSet = true
		time.AfterFunc(1*time.Second, func() {
			ds.clearCache() // 一秒后判断重新判断是否需要推送
		})
	}

}</pre>
<p>debouncePush方法中包含一些额外的去回弹逻辑：</p>
<pre class="crayon-plain-tag">func (ds *DiscoveryService) debouncePush(startDebounce time.Time) {
	// 上一次请求清除缓存的时间
	since := time.Since(lastClearCacheEvent)
    // 距今如果大于200ms，或者从启动去回弹到到现在大于10s
    // 如果反复请求推送，则||左侧的表达式一直不会满足。为了防止无限的去回弹，必然的推送在最初请求的10s后发生
	if since &gt; 2*DebounceAfter ||
		time.Since(startDebounce) &gt; DebounceMax {
		ds.doPush()

	} else {
        // 下一轮去回弹
		time.AfterFunc(DebounceAfter, func() {
			ds.debouncePush(startDebounce)
		})
	}
}</pre>
<p>doPush方法真正触发配置推送：</p>
<pre class="crayon-plain-tag">func (ds *DiscoveryService) doPush() {
	// 本次推送正在处理时，可能有别的配置变更发生，因此这里需要撤销标记，防止遗漏事件
	clearCacheTimerSet = false
	lastClearCache = time.Now()
	full := ds.fullPush
    // 获取自上次推送依赖的服务端点更新
	edsUpdates := BeforePush()

	// Update the config values, next ConfigUpdate and eds updates will use this
	clearCacheMutex.Lock()
	ds.fullPush = false
	clearCacheMutex.Unlock()
    // 推送，全部或者增量的EDS更新
	Push(full, edsUpdates)
}</pre>
<p>注意，全局函数BefoerPush实际上是envoyv2.DiscoveryServer.BeforePush方法：</p>
<pre class="crayon-plain-tag">// 以envoy包的全局变量作为媒介，在DiscoveryService和v2.DiscoveryServer之间传递这两个函数，避免它们直接依赖
envoy.Push = s.EnvoyXdsServer.Push
envoy.BeforePush = s.EnvoyXdsServer.BeforePush</pre>
<p>类似的，全局函数Push 也是envoyv2.DiscoveryServer.Push方法。</p>
<p>也就是说，推送的职责在这里转交给DiscoveryServer了。</p>
<div class="blog_h2"><span class="graybg">DiscoveryServer</span></div>
<p>此结构提供了Envoy v2 xds API的gRPC实现，在启动阶段由<a href="#initDiscoveryService">initDiscoveryService</a>创建。</p>
<pre class="crayon-plain-tag">type DiscoveryServer struct {
	// 环境信息
	Env *model.Environment

	// 仅仅用于调试和压力测试
	MemRegistry *MemServiceDiscovery

	// 负责使用 Istio的网络API + Service注册表信息 生成数据平面的配置信息（也就是Envoy代理配置）
	ConfigGenerator core.ConfigGenerator

	// 目前主要用于判断（K8S的）初始缓存同步是否完成
	ConfigController model.ConfigStoreCache

	// 初始连接的限速器
	initThrottle chan time.Time
	// 限速器
	throttle chan time.Time

	// 为/debug/adsz提供配置快照。默认false，可以通过环境变量PILOT_DEBUG_ADSZ_CONFIG=1启用
	DebugConfigs bool

	// 保护被ADS读写的全局数据结构, 包括 EDSUpdates和shards
	mutex sync.RWMutex

	// 服务的端点分片列表，此字段从增量更新构建
	EndpointShardsByService map[string]*model.EndpointShardsByService

	// 工作负载列表，可用于检测变更。直接由registry推送的更新计算得到
	WorkloadsByID map[string]*Workload

	// 负责请求配置更新，实现了放抖动（debouncing，延迟配置推送，防止Regsitry连续的资源更新导致过频推送）且能进行变更检测
	// 用于将envoy/v2从envoy/解耦，在Istio 1.1将进行简化/清理
	ConfigUpdater model.ConfigUpdater

	// 跟踪自上一次完整推送之后的所有服务的（端点的）变更，从1.0.3+仅仅用于跟踪两次推送之间的增量
	// 示例：
	// {
	//   details.default.svc.k8s.gmem.cc: {
	//     Shards: {
	//       "Kubernetes": {
	//         Shard: "Kubernetes",
	//         Entries: [ endpoints... ]
	//       }
	//     }  
	//   }
	// }
	edsUpdates map[string]*model.EndpointShardsByService

	// 保护全局推送上下文，一旦配置变更此上下文就改变，多个地方需要读取此上下文
	pushContextMutex sync.RWMutex
}</pre>
<p>此结构支持的方法繁多，定义在proxy/envoy/v2包中的ads.go、cds.go、eds.go、lds.go、rds.go等文件中，对应xDS API的各子集以及ADS。其中一部分方法是通过接口暴露的，例如：</p>
<ol>
<li>AggregatedDiscoveryServiceServer，Envoy提供的gRPC接口。在ads.go中实现</li>
<li>XDSUpdater，在push_context.go中定义，在eds.go中实现</li>
</ol>
<div class="blog_h3"><span class="graybg">BeforePush</span></div>
<p>此方法的逻辑很简单，将上次变更依赖的服务端点变更增量提取出来返回，并重置此“变更增量”为空：</p>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) BeforePush() map[string]*model.EndpointShardsByService {
	edsUpdates := s.edsUpdates
	// 重置，后续的更新由新的map跟踪
	s.edsUpdates = map[string]*model.EndpointShardsByService{}
	return edsUpdates
}</pre>
<p>在未来，Istio对增量更新提供完整支持后，这里需要重构。在将proxy/envoy/discovery合并到v2 discovery之后，此方法不再允许外部访问。</p>
<div class="blog_h3"><span class="graybg">Push</span></div>
<p>该方法在准备好配置后，使用ADS协议执行推送：</p>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) Push(full bool, edsUpdates map[string]*model.EndpointShardsByService) {
	if !full {
		// 执行增量推送
		go s.AdsPushAll(version, s.globalPushContext(), false, edsUpdates)
		return
	}
	// 获取推送上下文
	pc := s.globalPushContext()
	if pc != nil {
        // 将model.LastPushStatus赋值为当前推送上下文，并更新监控指标
		pc.OnConfigChange()
	}
	// 创建新的全局推送上下文，基于此上下文进行推送
	t0 := time.Now()
	push := model.NewPushContext()
	push.ServiceAccounts = s.ServiceAccounts
    // 初始化此推送上下文
	if err := push.InitContext(s.Env); err != nil {
		adsLog.Errorf("XDS: failed to update services %v", err)
		return
	}
    // 生成所有代理共享的配置，例如出站监听器/集群的配置
	if err := s.ConfigGenerator.BuildSharedPushState(s.Env, push); err != nil {
		adsLog.Errorf("XDS: Failed to rebuild share state in configgen: %v", err)
		totalXDSInternalErrors.Add(1)
		return
	}
    // 列出端点并创建分片
	if err := s.updateServiceShards(push); err != nil {
		return
	}
    // 替换掉全局上下文
	s.Env.PushContext = push

    // 异步推送
	go s.AdsPushAll(versionLocal, push, true, nil)
}</pre>
<div class="blog_h3"><span class="graybg">AdsPushAll</span></div>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) AdsPushAll(version string, push *model.PushContext, full bool, edsUpdates map[string]*model.EndpointShardsByService) {
	if !full {
        // 更新集群信息，进行增量EDS推送。只有那些变化了的集群才被更新、推送
		s.edsIncremental(version, push, edsUpdates)
		return
	}

	// 赋值一个临时映射，避免加锁
	cMap := make(map[string]*EdsCluster, len(edsClusters))
	for k, v := range edsClusters {
		cMap[k] = v
	}

	// 为每个集群更新端点
	for clusterName, edsCluster := range cMap {
		if err := s.updateCluster(push, clusterName, edsCluster); err != nil {
			adsLog.Errorf("updateCluster failed with clusterName %s", clusterName)
		}
	}
    // 向所有客户端连接发送一个信号
	s.startPush(version, push, true, nil)
}</pre>
<div class="blog_h3"><span class="graybg">startPush</span></div>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) startPush(version string, push *model.PushContext, full bool,
	edsUpdates map[string]*model.EndpointShardsByService) {

	// 遍历所有已连接的Envoy，这覆盖ADS / EDS(0.7)，它们共享一样的连接表
	adsClientsMutex.RLock()
	// 复制以避免加锁
	pending := []*XdsConnection{}
	for _, v := range adsClients {
		pending = append(pending, v)
	}
	adsClientsMutex.RUnlock()

	// 触发每个代理的配置的重现计算，包括Envoy监听的所有配置，包括EDS
	pendingPush := int32(len(pending))
	tstart := time.Now()
	for {
		if len(pending) == 0 {
			// 无需推送，或者都推送完了，退出
			return
		}
		currentVersion := versionInfo()
		// 如果具有更新版本的推送正在进行，则当前推送取消
		if !allowConcurrentPush &amp;&amp; version != currentVersion &amp;&amp; full {
			return
		}
		c := pending[0]
		pending = pending[1:]
		edsOnly := edsUpdates
		if full {
			edsOnly = nil
		}
		// 非阻塞性推送，如果下一次推送紧跟着就到来，可能导致问题
		// 这里是逐个处理客户端，可以优化为并行的进行
		client := c
		// time.After返回一个只读通道，在给定的超时到达后，通道可读
		to := time.After(PushTimeout)
		select {
		// 尝试向客户端的推送通道发送事件
		case client.pushChannel &lt;- &amp;XdsEvent{
			push:               push,
			pending:            &amp;pendingPush,
			version:            version,
			edsUpdatedServices: edsOnly,
		}:
 			// 该客户端推送成功
			client.LastPush = time.Now()
			client.LastPushFailure = timeZero
 		// 如果推送通道不可用，检查客户端连接是否关闭
		case &lt;-client.doneChannel:
			adsLog.Infof("Client closed connection %v", client.ConID)
		case &lt;-to:
 		// 推送超时，可能是由于Envoy代理处于异常状态，无法接收数据
			pushTimeouts.Add(1)
			// 放回去等待重试
			pending = append(pending, c)
		}
	}
}</pre>
<p>可以看到，startPush<span style="background-color: #c0c0c0;">仅仅是把事件放入通道</span>，并不直接牵涉到gRPC相关的操作。这<span style="background-color: #c0c0c0;">实现了数据和传输的解耦</span>。</p>
<div class="blog_h2"><span class="graybg"><a id="PushContext"></a>PushContext</span></div>
<p>推送上下文，此结构跟踪推送的状态（指标、错误）。在一次推送完毕后，指标全部置零。</p>
<pre class="crayon-plain-tag">type PushContext struct {
	// 操控下面的map的锁      
	proxyStatusMutex sync.RWMutex
	//             ID         ErrCode 记录推送给代理时的事件
	ProxyStatus map[string]map[string]ProxyPushStatus

	// 最后一次配置变更（导致推送状态重置）的时间
	Start time.Time
	End   time.Time

	// 保护下面的字段
	Mutex sync.Mutex 

	// 推送开始时系统中的所有服务
	Services []*Service 

	// 以主机名索引的所有服务
	ServiceByHostname map[Hostname]*Service 

	// 服务关联的DestinationRule（和目的地相关的规则）
	destinationRuleHosts   []Hostname
	destinationRuleByHosts map[Hostname]*combinedDestinationRule

	// 环境信息
	Env *Environment 

	ServiceAccounts func(string) []string 

	// 用于跟踪服务名称和端口的映射关系ServicePort2Name is used to keep track of service name and port mapping.
	// ADS的名字使用端口号，而端点的名字则使用端口名为准
	//                   服务名  端口号+名称信息
	ServicePort2Name map[string]PortList 

	initDone bool
}</pre>
<p>此结构提供了Add、UpdateMetrics方法，和Prometheus统计指标有关：</p>
<pre class="crayon-plain-tag">// 可以步进和推送有关的Prometheus计数器
func (ps *PushContext) Add(metric *PushMetric, key string, proxy *Proxy, msg string)
// 基于推送的当前状态来更新Prometheus指标
func (ps *PushContext) UpdateMetrics()</pre>
<div class="blog_h3"><span class="graybg">InitContext</span></div>
<p>该方法负责初始化一个新的推送上下文，生成所需的一切数据结构，在执行Envoy配置推送之前你需要（从创建PushContext的协程）调用它。</p>
<div class="blog_h3"><span class="graybg">initServiceRegistry</span></div>
<p>该方法缓存注册表中的所有服务。</p>
<div class="blog_h3"><span class="graybg">initVirtualServices</span></div>
<p>该方法缓存所有VirtualService对象。</p>
<div class="blog_h3"><span class="graybg">initDestinationRules</span></div>
<p>该方法初始化所有DestinationRule对象。</p>
<div class="blog_h3"><span class="graybg">VirtualServices</span></div>
<p>列出绑定到指定网关的序列服务。</p>
<div class="blog_h3"><span class="graybg">DestinationRule</span></div>
<p>返回一个服务的DestinationRule。</p>
<div class="blog_h3"><span class="graybg">SubsetToLabels</span></div>
<p>计算出给定服务的给定子集对应的标签。Istio使用K8S Pod标签区分子集。</p>
<div class="blog_h2"><span class="graybg">ConfigUpdater</span></div>
<p>此接口负责请求配置更新，在配置推送之前，需要进行去回弹（防止连续触发的频繁推送）。</p>
<pre class="crayon-plain-tag">type ConfigUpdater interface {
	ConfigUpdate(full bool)
}</pre>
<p>目前，接口由<a href="#ConfigUpdater-Impl">DiscoveryService实现</a>，而DiscoveryServer也引用前者：</p>
<pre class="crayon-plain-tag">s.EnvoyXdsServer.ConfigUpdater = s.discoveryService</pre>
<div class="blog_h2"><span class="graybg">XDSUpdater</span></div>
<p>XDSUpdater用于<span style="background-color: #c0c0c0;">xDS模型的直接更新，以及增量推送</span>。DiscoveryServer实现了此接口。</p>
<p>Pilot使用多个注册表 —— 例如每个K8S集群就是一个注册表。每个<span style="background-color: #c0c0c0;">注册表负责跟踪关联到网格中服务的端点的变更</span>，并在变更后调用EDSUpdate方法。</p>
<p>注册表可以单个服务的端点分组为更小的子集（例如每个Deployment一个子集），以处理端点数量巨大的服务。由于限制可扩容性，Istio倾向于避免传递大量的对象，例如某个注册表的全部端点，或者某个服务在全部注册表中的端点。</p>
<p>Istio在未来会进行一些优化，例如以标签、网关、或者区域来分组端点。</p>
<pre class="crayon-plain-tag">type XDSUpdater interface {

	// 当端点列表发生变化，或者ServiceEntry的标签发生变化，该方法被调用
	// 对于一个集群/主机名，其所有端点必须一并发送给代理
	// shard是一个键，目前是注册表的名称（例如Kubernetes）
	EDSUpdate(shard, hostname string, entry []*IstioEndpoint) error

	// 当一个服务-端口映射定义发生变化后调用，对服务的标签、注解或者其它属性进行变更，可能触发EDS/CDS重新计算、增量推送
	// 但不会影响LDS/RDS
	SvcUpdate(shard, hostname string, ports map[string]uint32, rports map[uint32]string)

	// 当一个工作负载的标签、注解发生变化时由注册表负责调用
	// 对于K8S来说，id是Pod的IP地址（如果Pod在默认/主网络中）
	WorkloadUpdate(id string, labels map[string]string, annotations map[string]string)
}</pre>
<div class="blog_h3"><span class="graybg">SvcUpdate</span></div>
<p>当服务的信息发生变化后，调用此方法，主要逻辑是更新全局推送上下文中的ServicePort2Name字段。</p>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) SvcUpdate(cluster, hostname string, ports map[string]uint32, rports map[uint32]string) {
	pc := s.globalPushContext()
	s.mutex.Lock()
	defer s.mutex.Unlock()
	// 在1.0中，服务仅仅来自primary K8S集群
	if cluster == string(serviceregistry.KubernetesRegistry) {
		pl := model.PortList{}
		for k, v := range ports {
			pl = append(pl, &amp;model.Port{
				Port: int(v),
				Name: k,
			})
		}
		pc.ServicePort2Name[hostname] = pl
	}
}</pre>
<div class="blog_h3"><span class="graybg"><a id="WorkloadUpdate"></a>WorkloadUpdate</span></div>
<p>当工作负载的标签/注解发生变化后，PodCache调用此方法：</p>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) WorkloadUpdate(id string, labels map[string]string, annotations map[string]string) {
	if labels == nil {
		// 没有标签，不需要推送更新给Envoy，删除缓存
		s.mutex.Lock()
		delete(s.WorkloadsByID, id)
		s.mutex.Unlock()
		return
	}
        // 使用读锁，禁止写
	s.mutex.RLock()
	w, f := s.WorkloadsByID[id]
	s.mutex.RUnlock()
	if !f {
		// 不存在于缓存中，可能从未连接到此工作负载，不需要推送
		s.mutex.Lock()
		s.WorkloadsByID[id] = &amp;Workload{
			Labels:      labels,
			Annotations: annotations,
		}
		s.mutex.Unlock()
		return
	}
        // 深度比较
	if reflect.DeepEqual(w.Labels, labels) {
		// 标签没有变化，也不推送
		return
	}

	w.Labels = labels
	// Pod标签变化，意味着需要重新计算Envoy配置

	// 为了简单和安全，简单的进行全部推送。其实只需要识别出影响到哪些工作负载（那些可能使用到当前工作负载的），并且仅仅为这些负载进行推送
	adsLog.Infof("Label change, full push %s ", id)
	s.ConfigUpdater.ConfigUpdate(true)
}</pre>
<div class="blog_h3"><span class="graybg">updateEDS</span></div>
<p>kube.Controller在接收到K8S的端点更新后，会调用其自身的updateEDS方法：</p>
<pre class="crayon-plain-tag">func (c *Controller) updateEDS(ep *v1.Endpoints) {
// 计算出K8S服务名
	hostname := serviceHostname(ep.Name, ep.Namespace, c.domainSuffix)
// 收集端点
	endpoints := []*model.IstioEndpoint{}
	for _, ss := range ep.Subsets {
		for _, ea := range ss.Addresses {
			// 如果端点对应的Pod不存在
			pod, exists := c.pods.getPodByIP(ea.IP)
			if !exists {
				// 则请求一次配置更新。Pod信息可能后续很快推送过来
				if c.ConfigUpdater != nil {
					c.ConfigUpdater.ConfigUpdate(true)
				}
				continue
			}
// 如果Pod存在，则抓取其标签、UID等信息，构建出IstioEndpoint
			labels := map[string]string(convertLabels(pod.ObjectMeta))
			uid := fmt.Sprintf("kubernetes://%s.%s", pod.Name, pod.Namespace)
			for _, port := range ss.Ports {
				endpoints = append(endpoints, &amp;model.IstioEndpoint{
					Address:         ea.IP,
					EndpointPort:    uint32(port.Port),
					ServicePortName: port.Name,
					Labels:          labels,
					UID:             uid,
					ServiceAccount:  kubeToIstioServiceAccount(pod.Spec.ServiceAccountName, pod.GetNamespace(), c.domainSuffix),
				})
			}
		}
	}
	//                     更新   Kubernetes分片  这个服务          这些端点
	err := c.EDSUpdater.EDSUpdate(c.ClusterID, string(hostname), endpoints)
	if err != nil {
		//如果仅仅进行EDS推送不可以，则全局推送
		c.ConfigUpdater.ConfigUpdate(true)
	} else {
                // 仅仅进行EDS推送
		c.ConfigUpdater.ConfigUpdate(false)
	}
}</pre>
<div class="blog_h2"><span class="graybg">ConfigGenerator</span></div>
<p>此接口位于networking.core包中，<span style="background-color: #c0c0c0;">负责生成xDS响应</span>。其实现位于v1alpha3子包（Istio CRD的API版本号）中。接口规格如下：</p>
<pre class="crayon-plain-tag">type ConfigGenerator interface {
	// 此方法重新计算所有Envoy代理之间的共享状态，这些状态包括：
	// 1、Sidecar的出站集群（outbound clusters）、出站监听器（outbound listener）
	// 2、Sidecar/Gateway的路由表
	// 这些状态保存在ConfigGenerator对象中，在后续调用BuildClusters/BuildListeners/BuildHTTPRoutes时重用
	// 该方法不会调用插件，大部分插件需要的是特定代理的（例如mixer/authn/authz)的信息
	// BuildYYY函数会针对预计算对象调用插件
	BuildSharedPushState(env *model.Environment, push *model.PushContext) error

	// LDS。为指定的代理构建inbound/outbound listeners信息，多次调用此函数，不会反复构建同一监听器
	BuildListeners(env *model.Environment, node *model.Proxy, push *model.PushContext) ([]*v2.Listener, error)

	// CDS。为指定代理构建clusters信息
	BuildClusters(env *model.Environment, node *model.Proxy, push *model.PushContext) ([]*v2.Cluster, error)

	// RDS。 为指定代理构建HTTP routes信息
	BuildHTTPRoutes(env *model.Environment, node *model.Proxy, push *model.PushContext, routeName string) (*v2.RouteConfiguration, error)
}</pre>
<p>注意BuildYYY函数的返回值均为Envoy v2协议的ProtoBuf消息类型。</p>
<div class="blog_h3"><span class="graybg">BuildSharedPushState</span></div>
<p>该方法的实现如下：</p>
<pre class="crayon-plain-tag">func (configgen *ConfigGeneratorImpl) BuildSharedPushState(env *model.Environment, push *model.PushContext) error {
    // 为Sidecar代理、Router代理分别构建出站集群
	configgen.OutboundClustersForSidecars = configgen.buildOutboundClusters(env, model.Sidecar, push)
	configgen.OutboundClustersForGateways = configgen.buildOutboundClusters(env, model.Router, push)
	return nil
}</pre><br />
<pre class="crayon-plain-tag">func (configgen *ConfigGeneratorImpl) buildOutboundClusters(env *model.Environment, proxyType model.NodeType, push *model.PushContext) []*v2.Cluster {
	clusters := make([]*v2.Cluster, 0)
	// 遍历上下文中包含的所有服务
	for _, service := range push.Services {
		// 获取服务的目的地规则（可能为空）
		config := push.DestinationRule(service.Hostname)
		for _, port := range service.Ports {
			if port.Protocol == model.ProtocolUDP {
				continue
			}
			// 得到服务包含的主机列表，如果服务的Resolution设置为DNSLB则返回nil
			hosts := buildClusterHosts(env, service, port.Port)

			// BuildSubsetKey：生成引用特定（限定服务、端口、子集）实例集的键，Envoy使用此键查询Pilot，进而获得子集中包含的实例列表
			// 示例：outbound|443||kubernetes.default.svc.k8s.gmem.cc
			clusterName := model.BuildSubsetKey(model.TrafficDirectionOutbound, "", service.Hostname, port.Port)
            // 获得集群（上游服务）的Istio账号
			upstreamServiceAccounts := env.ServiceAccounts.GetIstioServiceAccounts(service.Hostname, []string{port.Name})
            // convertResolution将Istio解析枚举转换为Envoy的集群发现类型（默认v2.Cluster_EDS）
            // ClientSideLB -&gt; v2.Cluster_EDS
            // DNSLB -&gt; v2.Cluster_STRICT_DNS
            // Passthrough -&gt; v2.Cluster_ORIGINAL_DST
            // 生成默认（相对于子集来说是默认）集群v2.Cluster信息，包括流量策略TrafficPolicy（连接池、异常检测、TLS设置、负载均衡器）
			defaultCluster := buildDefaultCluster(env, clusterName, convertResolution(service.Resolution), hosts)
            // 更新v2.Cluster.EdsClusterConfig，将此集群的EDS的配置源设置为ADS
			updateEds(env, defaultCluster, service.Hostname)
            // 如果是HTTP/2协议，则将最大并发流数量设置为1073741824
			setUpstreamProtocol(defaultCluster, port)
            // 将当前集群添加到列表
			clusters = append(clusters, defaultCluster)
			if config != nil {
                // 如果此服务具有关联的目的地规则
				destinationRule := config.Spec.(*networking.DestinationRule)
                // 如果使用mTLS，将DestinationRule的TrafficPolicy的证书字段填充上
				convertIstioMutual(destinationRule, upstreamServiceAccounts)
                // 将TrafficPolicy转换为v2.Cluster的对应字段
				applyTrafficPolicy(defaultCluster, destinationRule.TrafficPolicy, port)
				for _, subset := range destinationRule.Subsets {
                    // 处理子集，处理方式和默认集群类似
                    // 子集集群的名字示例 outbound|9080|v1|details.default.svc.k8s.gmem.cc
					subsetClusterName := model.BuildSubsetKey(model.TrafficDirectionOutbound, subset.Name, service.Hostname, port.Port)
					subsetCluster := buildDefaultCluster(env, subsetClusterName, convertResolution(service.Resolution), hosts)
					updateEds(env, subsetCluster, service.Hostname)
					setUpstreamProtocol(subsetCluster, port)
					applyTrafficPolicy(subsetCluster, destinationRule.TrafficPolicy, port)
					applyTrafficPolicy(subsetCluster, subset.TrafficPolicy, port)
					// 调用插件，以修改生成的配置
					for _, p := range configgen.Plugins {
						p.OnOutboundCluster(env, push, service, port, subsetCluster)
					}
					clusters = append(clusters, subsetCluster)
				}
			} else {
				// 如果全局启用了mTLS，并且此服务不是外部服务，并且当前代理是边车
				if env.Mesh.AuthPolicy == meshconfig.MeshConfig_MUTUAL_TLS &amp;&amp; !service.MeshExternal &amp;&amp; proxyType == model.Sidecar {
					applyUpstreamTLSSettings(defaultCluster, buildIstioMutualTLS(upstreamServiceAccounts, ""))
				}
			}

			// 为默认集群调用插件
			for _, p := range configgen.Plugins {
				p.OnOutboundCluster(env, push, service, port, defaultCluster)
			}
		}
	}

	return clusters
}</pre>
<div class="blog_h2"><span class="graybg">Plugin</span></div>
<p>networking包提供了一种插件机制，允许在构造xdsapi.Listener期间，对xdsapi.Listener进行任意的修改。例如启用：</p>
<ol>
<li>AuthenticationPlugin插件，可以在入站监听器、出站集群上设置mTLS认证</li>
<li>mixer插件，可以在入站监听器上设置策略检查</li>
</ol>
<p>插件类型包括：</p>
<pre class="crayon-plain-tag">const (
	// 身份验证插件
	Authn = "authn"
	// RBAC插件
	Authz = "authz"
	// Envoyfilter插件
	Envoyfilter = "envoyfilter"
	// 健康检查插件
	Health = "health"
	// Mixer 插件
	Mixer = "mixer"
)</pre>
<p>插件接口规格如下：</p>
<pre class="crayon-plain-tag">type Plugin interface {
	// 每当为一个服务添加一个出站监听器（到LDS输出）后，调用此方法
	// 可用于在出站路径上添加额外的监听器
	OnOutboundListener(in *InputParams, mutable *MutableObjects) error

	// 每当为一个服务添加一个监听器（到LDS输出）后，调用此方法
	// 可用于添加额外的监听器
	OnInboundListener(in *InputParams, mutable *MutableObjects) error

	// 每当一个新集群添加到CDS输出后，调用此方法
	// 为每次推送调用一次，而非为每个边车/代理
	OnOutboundCluster(env *model.Environment, push *model.PushContext, service *model.Service, servicePort *model.Port,
		cluster *xdsapi.Cluster)

	// 每当一个新集群添加到CDS输出后，调用此方法
	// 为每个边车/代理调用
	OnInboundCluster(env *model.Environment, node *model.Proxy, push *model.PushContext, service *model.Service, servicePort *model.Port,
		cluster *xdsapi.Cluster)

	// 每当新的虚拟主机（和路由）添加到出站路径的RDS后调用
	OnOutboundRouteConfiguration(in *InputParams, routeConfiguration *xdsapi.RouteConfiguration)

	// 每当新的虚拟主机（和路由）添加到入站路径的RDS后调用
	OnInboundRouteConfiguration(in *InputParams, routeConfiguration *xdsapi.RouteConfiguration)
}</pre>
<p>具体的插件，只需要按需实现部分方法即可。</p>
<div class="blog_h3"><span class="graybg">InputParams</span></div>
<p>一系列传递给插件回调函数（Plugin接口函数）的值，这些值应该被插件<span style="background-color: #c0c0c0;">只读访问</span>：</p>
<pre class="crayon-plain-tag">type InputParams struct {
	// 必须字段，监听器的协议，例如TCP/HTTP
	ListenerProtocol ListenerProtocol
	// 必须字段，环境信息
	Env *model.Environment
	// xDS响应是给哪个节点的
	Node *model.Proxy
	// 网格中所有代理服务的实例
	ProxyInstances []*model.ServiceInstance
	// 和监听器同地协作的服务实例（应用到Sidecar）
	ServiceInstance *model.ServiceInstance
	// S和监听器同地协作的服务（应用到Sidecar）
	// 对于出站TCP监听器，此字段指向目的地服务
	Service *model.Service
	// 监听器的端口
	// 对于边车的outbound/inbound监听器，指向服务端口（而非端点端口）
	// 对于网关的inbound监听器，指向网关服务器端口
	Port *model.Port

	// 推送上下文
	Push *model.PushContext
}</pre>
<div class="blog_h3"><span class="graybg">MutableObjects</span></div>
<p>传递给On*Listener回调的一系列对象：</p>
<pre class="crayon-plain-tag">type MutableObjects struct {
	// 当前正在生成的监听器配置
	Listener *xdsapi.Listener
	// 关联到此监听器的过滤器链
	FilterChains []FilterChain
}</pre>
<div class="blog_h3"><span class="graybg">xdsapi.Cluster</span></div>
<p>传递给On*Cluster回调的Envoy Cluster对象，直接对应ProtoBuffer消息。</p>
<div class="blog_h3"><span class="graybg">xdsapi.RouteConfiguration</span></div>
<p>传递给On*RouteConfiguration回调的Envoy RouteConfiguration对象，直接对应ProtoBuffer消息。</p>
<div class="blog_h1"><span class="graybg">代理启动和重载</span></div>
<p>本章讨论Pilot Agent的启动过程，以及Envoy的Bootstrap配置是如何生成和重新载入的。Pilot Agnet和Pilot Discovery如何通信在下一章分析。</p>
<div class="blog_h2"><span class="graybg">如何调试</span></div>
<p>为了方便调试，我们在本地启动Pilot Agent，参考下面的命令行参数：</p>
<pre class="crayon-plain-tag">export POD_NAME=productpage-v1-8d69b45c-5qcv7
export POD_NAMESPACE=default
export INSTANCE_IP=10.0.0.1
export ISTIO_BOOTSTRAP=pilot/envoy_bootstrap_tmpl.tpl

# pilot-agent的入口点代码位于 pilot/cmd/pilot-agent/main.go

            # 此代理的角色，可选sidecar、ingress、router
pilot proxy sidecar --log_output_level=default:debug --log_caller=default --domain=default.svc.k8s.gmem.cc \
                # Envoy二进制文件的位置
                --binaryPath=/home/alex/CPP/projects/clion/envoy/bazel-bin/source/exe/envoy-static \
                # 生成的Envoy配置文件存放在何处
                --configPath=pilot/envoyConfig \
                # 当前代理的节点属于哪个服务
                --serviceCluster=productpage \
                # 发现服务的地址
                --discoveryAddress=localhost:15010 \
                # Envoy进程的日志级别
                --proxyLogLevel=debug</pre>
<div class="blog_h2"><span class="graybg">启动过程概述</span></div>
<p>Pilot Agent的入口点位于pilot/cmd/pilot-agent/main.go，它使用包spf13.cobra来管理一组子命令，Pilot核心功能由proxy子命令实现。</p>
<p>入口点的启动过程如下：</p>
<ol>
<li>解析命令行参数</li>
<li>初始化model.Proxy对象，在K8S环境下
<ol>
<li>ID=$POD_NAME.$POD_NAMESPACE</li>
<li>Domain 从命令行--domain参数传入</li>
<li>IPAddress，从环境变量INSTANCE_IP读入</li>
</ol>
</li>
<li>如果控制平面的身份验证策略是mTLS，则初始化Pilot的SAN</li>
<li>调用model.ValidateProxyConfig进行代理配置合法性验证</li>
<li>初始化数字证书位置信息，默认存储在/etc/certs/。如果当前代理的角色不是sidecar而是ingress，还要初始化Ingress数字证书位置信息</li>
<li>如果提供了Envoy配置模板，且CustomConfigFile为空（不使用自定义Envoy配置文件），则根据此模板生成一个Envoy配置文件，并将其路径赋值给CustomConfigFile</li>
<li>如果提供了statusPort，则调用status.NewServer启动一个状态服务。访问状态服务的/healthz/ready可以知晓代理是否准备好</li>
<li>调用envoy.NewProxy，创建一个envoy结构</li>
<li>调用proxy.NewAgent，创建一个agent结构</li>
<li>调用envoy.NewWatcher，创建一个watcher结构</li>
<li>调用<a href="#watcher.Run">watcher.Run</a>启动Watcher
<ol>
<li>启动Agent控制循环</li>
<li>调用Reload立即载入，导致Envoy进程启动</li>
<li>调用watchCerts监听数字证书的变化</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">envoy</span></div>
<p>此结构位于proxy.envoy包中，封装启动envoy所需的信息：</p>
<pre class="crayon-plain-tag">func NewProxy(config meshconfig.ProxyConfig, node string, logLevel string, pilotSAN []string) proxy.Proxy {
	// 设置命令行传入的envoy的日志级别
	var args []string
	if logLevel != "" {
		args = append(args, "-l", logLevel)
	}

	return envoy{
        // ProxyConfig结构
		config:    config,
        // 节点的标识符，例如sidecar~10.0.0.1~productpage-v1-8d69b45c-5qcv7.default~default.svc.k8s.gmem.cc
		node:      node,
        // 额外的Envoy参数
		extraArgs: args,
        // 启用mTLS时所需的Pilot的SAN
		pilotSAN:  pilotSAN,
	}
}</pre>
<p>该结构实现了Proxy接口：</p>
<pre class="crayon-plain-tag">type Proxy interface {
	// 启动代理
	Run(interface{}, int, &lt;-chan error) error
	// 在代理退出后，清除对应的纪元
	Cleanup(int)

	// 如果以期望配置启动代理的最大尝试次数到达仍然失败，Agent在终结之前调用此方法
	Panic(interface{})
}</pre>
<div class="blog_h3"><span class="graybg">Run</span></div>
<p>此方法生成Envoy的Bootstrap配置，并启动Envoy进程：</p>
<pre class="crayon-plain-tag">func (proxy envoy) Run(config interface{}, epoch int, abort &lt;-chan error) error {
	var fname string
	// 使用自定义配置文件，或者通过模板生成配置文件
	if len(proxy.config.CustomConfigFile) &gt; 0 {
		fname = proxy.config.CustomConfigFile
	} else {
		out, err := bootstrap.WriteBootstrap(&amp;proxy.config, proxy.node, epoch, proxy.pilotSAN, proxy.opts, os.Environ())
		fname = out
	}

	// 处理Envoy命令行参数
	args := proxy.args(fname, epoch)
	if len(proxy.config.CustomConfigFile) == 0 {
		args = append(args, "--v2-config-only")
	}
    // e.g. -c pilot/envoyConfig/envoy-rev0.json --restart-epoch 0 --drain-time-s 2 --parent-shutdown-time-s 3 --service-cluster productpage --service-node sidecar~10.0.0.1~productpage-v1-8d69b45c-5qcv7.default~default.svc.k8s.gmem.cc --max-obj-name-len 189 --allow-unknown-fields -l warn --v2-config-only]
	log.Infof("Envoy command: %v", args)

	/* 异步命令行调用 */
	cmd := exec.Command(proxy.config.BinaryPath, args...)
	cmd.Stdout = os.Stdout  // 直接把Envoy子进程的标准输出和错误收集过来
	cmd.Stderr = os.Stderr
	if err := cmd.Start(); err != nil {
		return err
	}

	// 等待Envoy进程退出
	if proxy.errChan != nil {
		// 把Envoy退出错误码存入err通道
		go func() {
			proxy.errChan &lt;- cmd.Wait()
		}()
		return nil
	}
    // 再建一个通道和协程，当Envoy退出时协程写入通道，让当前协程从select case中退出
	done := make(chan error, 1)
	go func() {
		done &lt;- cmd.Wait()
	}()

	select {
   // Agent可以发送Abort信号，作为响应此Envoy实例需要停止
	case err := &lt;-abort:
		log.Warnf("Aborting epoch %d", epoch)
		if errKill := cmd.Process.Kill(); errKill != nil {
			log.Warnf("killing epoch %d caused an error %v", epoch, errKill)
		}
		return err
   // Envoy实例自然死亡
	case err := &lt;-done:
		return err
	}
}</pre>
<div class="blog_h3"><span class="graybg">WriteBootstrap</span></div>
<p>此方法生成Envoy Bootstrap配置。使用的配置模板，按以下逻辑寻找：</p>
<ol>
<li>如果指定了--customConfigFile，使用此文件。这步逻辑仅仅用于测试，实际无用</li>
<li>如果config.ProxyBootstrapTemplatePath不为空，使用此文件。此字段没有通过命令行暴露</li>
<li>否则，使用常量DefaultCfgDir指定的值，也就是默认配置</li>
<li>如果设置了环境变量ISTIO_BOOTSTRAP，使用此变量指定的配置模板路径</li>
</ol>
<p>默认配置的路径是/var/lib/istio/envoy/envoy_bootstrap_tmpl.json，内容为Envoy的JSON格式的配置的Go Template。</p>
<p>生成的Envoy配置文件样例：</p>
<pre class="crayon-plain-tag">{
  // 节点（边车和其代理的应用所在Pod）信息
  "node": {
    "id": "sidecar~10.0.0.1~productpage-v1-8d69b45c-5qcv7.default~default.svc.k8s.gmem.cc",
    "cluster": "productpage",
    "metadata": {
      "istio": "sidecar"
    }
  },
  // 监控指标配置，抽取标签
  "stats_config": {
    "use_all_default_tags": false,
    "stats_tags": [
      {
        "tag_name": "cluster_name",
        "regex": "^cluster\\.((.+(\\..+\\.svc\\.cluster\\.local))\\.)"
      },
      {
        "tag_name": "tcp_prefix",
        "regex": "^tcp\\.((.*)\\.)\\w+$"
      },
      {
        "tag_name": "response_code",
        "regex": "_rq(_(\\d{3}))$"
      },
      {
        "tag_name": "response_code_class",
        "regex": "_rq(_(\\dxx))$"
      },
      {
        "tag_name": "http_conn_manager_listener_prefix",
        "regex": "^listener(=\\.).*\\.http\\.(((:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
      },
      {
        "tag_name": "http_conn_manager_prefix",
        "regex": "^http\\.(((:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
      },
      {
        "tag_name": "listener_address",
        "regex": "^listener\\.(((:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
      }
    ]
  },
  // 管理端口配置，管理端点也暴露了Prometheus Exporter
  "admin": {
    "access_log_path": "/dev/null",
    "address": {
      "socket_address": {
        "address": "127.0.0.1",
        "port_value": 15000
      }
    }
  },
  // 动态发现配置
  "dynamic_resources": {
    "lds_config": {
      "ads": {}
    },
    "cds_config": {
      "ads": {}
    },
    // 使用集群xds-grpc提供的聚合发现服务
    "ads_config": {
      "api_type": "GRPC",
      "refresh_delay": "1s",
      "grpc_services": [
        {
          "envoy_grpc": {
            "cluster_name": "xds-grpc"
          }
        }
      ]
    }
  },
  // 静态配置
  "static_resources": {
    "clusters": [
      // Prometheus Exporter
      {
        "name": "prometheus_stats",
        "type": "STATIC",
        "connect_timeout": "0.250s",
        "lb_policy": "ROUND_ROBIN",
        "hosts": [
          {
            "socket_address": {
              "protocol": "TCP",
              "address": "127.0.0.1",
              "port_value": 15000
            }
          }
        ]
      },
      // xDS服务地址，测试时注意把前文提到的发现服务开启
      {
        "name": "xds-grpc",
        "type": "STRICT_DNS",
        "connect_timeout": "1s",
        "lb_policy": "ROUND_ROBIN",
        "hosts": [
          {
            "socket_address": {
              "address": "localhost",
              "port_value": 15010
            }
          }
        ],
        "circuit_breakers": {
          "thresholds": [
            {
              "priority": "DEFAULT",
              "max_connections": 100000,
              "max_pending_requests": 100000,
              "max_requests": 100000
            },
            {
              "priority": "HIGH",
              "max_connections": 100000,
              "max_pending_requests": 100000,
              "max_requests": 100000
            }
          ]
        },
        "upstream_connection_options": {
          "tcp_keepalive": {
            "keepalive_time": 300
          }
        },
        "http2_protocol_options": {}
      }
    ],
    // 暴露Prometheus指标的监听器
    "listeners": [
      {
        "address": {
          "socket_address": {
            "protocol": "TCP",
            "address": "0.0.0.0",
            "port_value": 15090
          }
        },
        "filter_chains": [
          {
            "filters": [
              {
                "name": "envoy.http_connection_manager",
                "config": {
                  "codec_type": "AUTO",
                  "stat_prefix": "stats",
                  "route_config": {
                    "virtual_hosts": [
                      {
                        "name": "backend",
                        "domains": [
                          "*"
                        ],
                        "routes": [
                          // 访问curl http://localhost:15090/stats/prometheus可以直接看到指标
                          {
                            "match": {
                              "prefix": "/stats/prometheus"
                            },
                            "route": {
                              "cluster": "prometheus_stats"
                            }
                          }
                        ]
                      }
                    ]
                  },
                  "http_filters": {
                    "name": "envoy.router"
                  }
                }
              }
            ]
          }
        ]
      }
    ]
  }
}</pre>
<div class="blog_h2"><span class="graybg">agent</span></div>
<p>此结构位于proxy.envoy包中，它是envoy proxy的代理人（Agent） 。它负责管理envoy进程的生命周期和重启。</p>
<p>Agent跟踪所有运行中的代理epochs及其配置。Envoy<span style="background-color: #c0c0c0;">热重启通过启动具有递增的重启纪元的（strictly incremented restart epoch）代理进程</span>实现。<span style="background-color: #c0c0c0;">优雅的关闭旧的epochs并且将所有必须的状态传递给最新的epoch是envoy的职责</span>，Agent不会去主动关闭旧的epoch对应的envoy进程。</p>
<p>最初的epoch是0。</p>
<p>这里使用的<a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/hot_restart.html">重启协议</a>（restart protocol ）是和Envoy的重启纪元（restart epochs）语义匹配的：为了成功启动用来替代正在运行的Envoy的新进程，新进程的重启纪元必须<span style="background-color: #c0c0c0;">正好比所有正在运行的其它Envoy进程中纪元最大的那个高1</span>。</p>
<p>Agent需要调用Proxy的两个函数：</p>
<ol>
<li>Run函数，用于启动代理，且必须一直阻塞直到代理退出</li>
<li>Cleanup函数，用于在代理退出后立即进行清理，此函数必须是非阻塞的，因为它在Agent的主控制循环中被调用</li>
</ol>
<p>这两个函数都以代理的纪元作为入参。</p>
<p>每当Run函数返回了错误，Agent都假设代理启动失败了，并且会重试若干次。后续的重启可能重用之前失败的启动的纪元号。</p>
<pre class="crayon-plain-tag">func NewAgent(proxy Proxy, retry Retry) Agent {
	return &amp;agent{
        // Proxy接口的实现，也就是上面的envoy
		proxy:    proxy,
        // 重试（为proxy应用新配置）规则
		retry:    retry,
        // 当前纪元以及对应的配置
		epochs:   make(map[int]interface{}),
        // 通过此通道传递期望的配置
		configCh: make(chan interface{}),
        // 通过此通道提示代理退出
		statusCh: make(chan exitStatus),
        // 通过此通道终止运行中的实例
		abortCh:  make(map[int]chan error),
	}
}</pre>
<p>该结构实现了Agent接口：</p>
<pre class="crayon-plain-tag">type Agent interface {
	// 为代理设置期望的配置 —— Agent对比当前活动的配置和期望的配置，如果有必要，触发重启操作
	// 如果重启失败，以指数后退（exponential back-off）重试
	ScheduleConfigUpdate(config interface{})
    // 启动Agent控制循环
	Run(ctx context.Context)
}</pre>
<div class="blog_h3"><span class="graybg">Run</span></div>
<pre class="crayon-plain-tag">func (a *agent) Run(ctx context.Context) {
	// 限速器，平滑1QPS，爆发10QPS。为了处理所有通道的消息需要高QPS
	rateLimiter := rate.NewLimiter(1, 10)
	for {
        // Wait 阻塞，直到有了1个事件许可，等价于WaitN(ctx,1)
		err := rateLimiter.Wait(ctx)
		var delay time.Duration = 1&lt;&lt;63 - 1
        // 如果先前已经安排了下一次调度，设置延迟时间戳
		if a.retry.restart != nil {
			delay = time.Until(*a.retry.restart)
		}

		select {
        // 尝试从配置通道读取新配置，注意，Watcher从此通道传来的仅仅是散列值，此值仅用于识别配置是否变化，而不包含实际的配置信息
		case config := &lt;-a.configCh:
            // 如果和当前配置不同
			if !reflect.DeepEqual(a.desiredConfig, config) {
                // 赋值给期望配置
				a.desiredConfig = config

				// 重置 重试预算（剩余的重试次数）
				a.retry.budget = a.retry.MaxRetries
				a.reconcile()
			}
         // 如果此通道可读，则说明代理应该退出
		case status := &lt;-a.statusCh:
			// 删除status中的epoch
			delete(a.epochs, status.epoch)
             // 删除终止通道，防止在非Abort错误时自我终止
			delete(a.abortCh, status.epoch)
             // 更新配置
			a.currentConfig = a.epochs[a.latestEpoch()]
             // Abort错误
			if status.err == errAbort {
				log.Infof("Epoch %d aborted", status.epoch)
			} else if status.err != nil {
				// 用于Envoy热重启竞态条件的关系，这里需要通过杀死所有Envoy进程，立即的、非优雅的重启
				a.abortAll()
			} else {
                 // 正常退出
				log.Infof("Epoch %d exited normally", status.epoch)
			}

			// 清理此纪元的配置文件
			a.proxy.Cleanup(status.epoch)

			// 如果存在错误，调度一次重试
			if status.err != nil {
				// 延迟已调度的延迟，如果不为空，说明已经调度了
				if a.retry.restart == nil {
                        // 如果还有重试预算
					if a.retry.budget &gt; 0 {
						delayDuration := a.retry.InitialInterval * (1 &lt;&lt; uint(a.retry.MaxRetries-a.retry.budget))
						restart := time.Now().Add(delayDuration)
						a.retry.restart = &amp;restart // 调度下一次重试
						a.retry.budget = a.retry.budget - 1
					} else {
                        // 没有重试预算了，Panic
						a.proxy.Panic(status.epoch)
						return
					}
				}
			}
           // 如果已经到了下次调度的延迟
		case &lt;-time.After(delay):
			a.reconcile()
           // 通道关闭
		case _, more := &lt;-ctx.Done():
			if !more {
				a.terminate()
				return
			}
		}
	}
}</pre>
<div class="blog_h3"><span class="graybg">reconcile</span></div>
<p>此方法重现载入新的代理配置：</p>
<pre class="crayon-plain-tag">func (a *agent) reconcile() {
	// 取消掉任何计划重启
	a.retry.restart = nil

	// 再次检查期望配置是否和当前配置相同
	if reflect.DeepEqual(a.desiredConfig, a.currentConfig) {
		return
	}

	// 新增纪元
	epoch := a.latestEpoch() + 1
	// 重置abort通道
	abortCh := make(chan error, MaxAborts)
	a.epochs[epoch] = a.desiredConfig
	a.abortCh[epoch] = abortCh
	a.currentConfig = a.desiredConfig
    // 在新协程中启动代理
	go a.waitForExit(a.desiredConfig, epoch, abortCh)
}</pre>
<div class="blog_h3"><span class="graybg">waitForExit</span></div>
<p>此方法调用代理的Run()方法，并且在其崩了后，将错误码写入通道：</p>
<pre class="crayon-plain-tag">func (a *agent) waitForExit(config interface{}, epoch int, abortCh &lt;-chan error) {
	err := a.proxy.Run(config, epoch, abortCh)
	a.statusCh &lt;- exitStatus{epoch: epoch, err: err}
}</pre>
<div class="blog_h2"><span class="graybg">watcher</span></div>
<p>此结构位于proxy.envoy包中，<span style="background-color: #c0c0c0;">负责当代理配置发生变化后，触发Agent的reload</span>。它持有agent，还监控数字证书的变更：</p>
<pre class="crayon-plain-tag">func NewWatcher(config meshconfig.ProxyConfig, agent proxy.Agent, role model.Proxy,
	certs []CertSource, pilotSAN []string) Watcher {
	return &amp;watcher{
        // Agent
		agent:    agent,
        // 代理角色
		role:     role,
        // ProxyConfig
		config:   config,
        // CertSource列表
		certs:    certs,
        // Pilot的SAN
		pilotSAN: pilotSAN,
	}
}</pre>
<p>此结构实现了Watcher接口：</p>
<pre class="crayon-plain-tag">type Watcher interface {
	// 阻塞性的运行监控循环
	Run(context.Context)
	// 使用最新的配置Reload Agent
	Reload()
}</pre>
<div class="blog_h3"><span class="graybg"><a id="watcher.Run"></a>Run</span></div>
<pre class="crayon-plain-tag">func (w *watcher) Run(ctx context.Context) {
	// 启动Agent的控制循环
	go w.agent.Run(ctx)

	// 立即以最新配置重载Agent
	w.Reload()

	// 监控数字证书文件的变更
	certDirs := make([]string, 0, len(w.certs))
	for _, cert := range w.certs {
		certDirs = append(certDirs, cert.Directory)
	}
    // 如果数字证书发生变化，则调用Reload方法重载Agent
	go watchCerts(ctx, certDirs, watchFileEvents, defaultMinDelay, w.Reload)

	&lt;-ctx.Done()
}</pre>
<div class="blog_h3"><span class="graybg">Reload</span></div>
<p>促使Agent立即启动Envoy：</p>
<pre class="crayon-plain-tag">func (w *watcher) Reload() {
    // 生成数字证书的哈希
	h := sha256.New()
	for _, cert := range w.certs {
		generateCertHash(h, cert.Directory, cert.Files)
	}
    // 以哈希值作为配置信息，调度下次更新
	w.agent.ScheduleConfigUpdate(h.Sum(nil))
}</pre>
<p>Agnet的ScheduleConfigUpdate很简单，就是写通道：</p>
<pre class="crayon-plain-tag">func (a *agent) ScheduleConfigUpdate(config interface{}) {
	a.configCh &lt;- config
}</pre>
<p>从这里可以看出，<span style="background-color: #c0c0c0;">导致代理需要重新载入的唯一原因，就是数字证书发生了变更</span>。 </p>
<div class="blog_h2"><span class="graybg">ProxyConfig</span></div>
<p>此结构表示当前代理的配置，和命令行选项是对应的：</p>
<pre class="crayon-plain-tag">type ProxyConfig struct {
    // 生成的配置文件存放的路径
    ConfigPath string 
    // Envoy二进制文件的位置
    BinaryPath string 
    // 所属的集群，也就是K8S服务
    ServiceCluster string 
    // 热重启时多久Drain掉连接，必须大于1s
    DrainDuration *duration.Duration 
    // 热重启时关闭父进程的延迟
    ParentShutdownDuration *duration.Duration 
    // 发现服务（xDS）的访问地址，代理连接此服务进行各种资源的发现
    DiscoveryAddress string 
    // 服务发现的轮询间隔，用于EDS, CDS, LDS, 但不用于RDS，必须大于1ms
    DiscoveryRefreshDelay *duration.Duration 
    // Zipkin兼容的Tracer地址
    ZipkinAddress string 
    // Envoy连接到上游集群端点的超时
    ConnectTimeout *duration.Duration 
    // Statsd UDP监听地址
    StatsdUdpAddress string 
    // 管理端口，在此端口监听管理命令
    ProxyAdminPort int32 
    // 此实例所在的可用性区域
    // 在K8S中，节点的可用性区域通过注解failure-domain.beta.kubernetes.io/zone设置
    AvailabilityZone string 
    // 控制平面的身份验证策略
    ControlPlaneAuthPolicy AuthenticationPolicy 
    // 自定义的代理配置文件路径，当前Mixer、Pilot的前置代理使用自定义配置文件
    CustomConfigFile string 
    // Envoy监控指标的name字段的最大长度
    StatNameLength int32 
    // 并发的工作线程数量
    Concurrency int32 
    // Envoy Bootstrap配置模板的路径
    ProxyBootstrapTemplatePath string 
    // 如何重定向入站流量到Envoy
    InterceptionMode     ProxyConfig_InboundInterceptionMode 
    XXX_NoUnkeyedLiteral struct{}                            
    XXX_unrecognized     []byte                              
    XXX_sizecache        int32                               
}</pre>
<div class="blog_h2"><span class="graybg">CertSource</span></div>
<p>此结构表示数字证书的存储位置：</p>
<pre class="crayon-plain-tag">type CertSource struct {
	// 所在目录
	Directory string
	// 证书文件列表
	Files []string
}</pre>
<div class="blog_h1"><span class="graybg">代理和发现服务的通信</span></div>
<p>代理端的通信行为（主要是xDS）完全由Envoy负责，和Istio无关，后者仅仅提供Bootstrap配置并在必要时Reload。</p>
<div class="blog_h2"><span class="graybg">启动过程</span></div>
<p>Envoy的入口点定义在source/exe/main.cc中，仅仅是创建一个Envoy::MainCommon对象并调用其run方法：</p>
<pre class="crayon-plain-tag">int main(int argc, char** argv) {
  std::unique_ptr&lt;Envoy::MainCommon&gt; main_common;
  main_common = std::make_unique&lt;Envoy::MainCommon&gt;(argc, argv);
  return main_common-&gt;run() ? EXIT_SUCCESS : EXIT_FAILURE;
}</pre>
<p>Envoy::MainCommon对象的创建过程如下：</p>
<ol>
<li>创建化负责信号处理、线程池、命令参数解析、定时器的实例变量</li>
<li>创建MainCommonBase base_：
<ol>
<li>创建热重启器</li>
<li>进行日志配置</li>
<li>创建化监控指标的存储</li>
<li>创建Envoy服务<span style="background-color: #c0c0c0;">InstanceImpl —— Envoy服务本体</span>
<ol>
<li>初始化热重启器</li>
<li>通过组件工厂ComponentFactory创建DrainManager</li>
<li>初始化InstanceImpl：
<ol>
<li>加载Bootstrap配置</li>
<li>配置StoreRoot，该对象是负责监控指标的存储，包括Sink的管理</li>
<li>从Bootstrap配置创建一个InitialImpl，表示Envoy服务器的最初配置</li>
<li>利用InitialImpl创建Admin端点，并启动其监听器，开始监听15000端口</li>
<li>创建过载管理器，用于保护Envoy实例所在节点不资源耗尽</li>
<li>创建监听器管理器ListenerManager</li>
<li>通过主线程的Dispatcher，为主线程注册线程本地存储（TLS）</li>
<li>调用StoreRoot的initializeThreading，启用多线程支持</li>
<li>创建Runtime::Loader，此对象能够从磁盘读取Envoy的运行时快照</li>
<li>创建SSL上下文管理器</li>
<li>创建集群管理器工厂ProdClusterManagerFactory</li>
<li>传入集群管理器工厂，初始化Envoy服务器的主配置Configuration::MainImpl
<ol>
<li>使用密钥管理器添加bootstrap_中配置的静态密钥</li>
<li>使用集群管理工厂，传入bootstrap配置，<a href="#init-cluster-mgr">创建集群管理器</a></li>
<li>创建限速客户端工厂RateLimitClientFactory</li>
<li>调用监听器管理器，添加所有静态配置的监听器</li>
<li>调用initializeTracers，为当前Envoy服务器初始化Tracer</li>
<li>调用initializeStatsSinks，初始化监控信息的Sink</li>
</ol>
</li>
<li>为HTTP上下文设置Tracer</li>
<li>如果配置了LDS动态资源，则调用监听器管理器，创建注册gRPC订阅</li>
<li>将Sinks添加到StoreRoot</li>
<li>为StoreRoot的Flush设置定时器</li>
<li>初始化用于死锁检测的GuardDog</li>
</ol>
</li>
</ol>
</li>
</ol>
</li>
</ol>
<p>run方法简单的转发给MainCommonBase.run，进而转发给Server::InstanceImpl的run方法。如果运行模式是Serve，则后者的逻辑如下：</p>
<ol>
<li>初始化RunHelper</li>
<li>调用GuardDog创建针对主线程的WatchDog</li>
<li>启动WatchDog，由主线程的Dispatcher创建定时器，定期touch此WatchDog</li>
<li>阻塞性的运行Dispatcher事件循环
<ol>
<li>调用runPostCallbacks，在运行事件循环之前执行所有后置回调。默认情况下没有需要执行的回调</li>
<li>调用event_base_loop，运行event_base直到1-N个未决/活动事件可用</li>
</ol>
</li>
<li>当Dispatcher.exit被调用（也就是主事件循环退出）后，停止WatchDog</li>
<li>调用Server::InstanceImpl的terminate方法，停止Envoy服务器</li>
<li>重置RunHelper</li>
</ol>
<p>从第5步开始，主线程会阻塞很长时间。后续的逻辑都主要通过libevent事件驱动 —— 当发生网络事件后执行某种回调。</p>
<div class="blog_h3"><span class="graybg">信号处理</span></div>
<p>如果定义了宏ENVOY_HANDLE_SIGNALS，则MainCommon的成员变量负责处理信号：</p>
<pre class="crayon-plain-tag">#ifdef ENVOY_HANDLE_SIGNALS
  // 在备选栈上执行信号处理
  Envoy::SignalAction handle_sigs;
  // 打印Backtrace并退出
  Envoy::TerminateHandler log_on_terminate;
#endif</pre>
<div class="blog_h3"><span class="graybg">线程池</span></div>
<p>MainCommon的成员变量<pre class="crayon-plain-tag">PlatformImpl platform_impl_;</pre>定义平台依赖的操作，目前仅仅包含一个线程工厂实现。</p>
<div class="blog_h3"><span class="graybg">命令参数解析</span></div>
<p>MainCommon的成员变量<pre class="crayon-plain-tag">Envoy::OptionsImpl options_;</pre>负责利用<a href="http://tclap.sourceforge.net/">TCLAP</a>解析命令行参数，它实现<pre class="crayon-plain-tag">Envoy::Server::Option</pre>接口，通过此接口可获得各种Envoy启动选项。</p>
<div class="blog_h3"><span class="graybg">定时器</span></div>
<p>MainCommon的成员变量<pre class="crayon-plain-tag">Event::RealTimeSystem real_time_system_;</pre>用于墙上时间度量，以及设置定时器、执行回调。</p>
<div class="blog_h3"><span class="graybg">MainCommonBase</span></div>
<p>MainCommon持有此类型的一个对象，并且把绝大部分职责委托给此类处理。构造函数逻辑如下：</p>
<pre class="crayon-plain-tag">MainCommonBase::MainCommonBase(OptionsImpl&amp; options, Event::TimeSystem&amp; time_system,
                               TestHooks&amp; test_hooks, Server::ComponentFactory&amp; component_factory,
                               std::unique_ptr&lt;Runtime::RandomGenerator&gt;&amp;&amp; random_generator,
                               Thread::ThreadFactory&amp; thread_factory)
    : options_(options), component_factory_(component_factory), thread_factory_(thread_factory) {
  // c-ares是一个C语言实现的异步请求DNS的实现，在实例初始化时，应该先调用该函数对该库相关内部模块
  ares_library_init(ARES_LIB_INIT_ALL);
  // 忽略SIGPIPE信号
  Event::Libevent::Global::initialize();

  switch (options_.mode()) {
  case Server::Mode::InitOnly:
  case Server::Mode::Serve: {
    if (!options.hotRestartDisabled()) {
      // 热重启器，接口由HotRestart提供，实现代码和配置的“热”重启
      restarter_ = std::make_unique&lt;Server::HotRestartImpl&gt;(options_);init-cluster-mgr
    }
    // ThreadLocal::InstanceImpl实现Instance，负责注册线程，读写线程本地数据
    tls_ = std::make_unique&lt;ThreadLocal::InstanceImpl&gt;();
    // 写、读日志锁，实现类是ProcessSharedMutex，可以跨进程使用的互斥量
    Thread::BasicLockable&amp; log_lock = restarter_-&gt;logLock();
    Thread::BasicLockable&amp; access_log_lock = restarter_-&gt;accessLogLock();
    auto local_address = Network::Utility::getLocalAddress(options_.localAddressIpVersion());
    // 日志上下文，相当于同时调用setLogLevel, setLogFormat, setLock
    logging_context_ =
        std::make_unique&lt;Logger::Context&gt;(options_.logLevel(), options_.logFormat(), log_lock);
    // 配置各Envoy组件的日志
    configureComponentLogLevels();
    // 监控指标存储，ThreadLocalStoreImpl是支持线程本地缓存的StoreRoot实现
    stats_store_ = std::make_unique&lt;Stats::ThreadLocalStoreImpl&gt;(options_.statsOptions(),
    // Stats::StatDataAllocator负责创建Counter、Gauge等Metric的实例
                                                                 restarter_-&gt;statsAllocator());

    // 创建Envoy服务器
    server_ = std::make_unique&lt;Server::InstanceImpl&gt;(
        options_, time_system, local_address, test_hooks, *restarter_, *stats_store_,
        access_log_lock, component_factory, std::move(random_generator), *tls_, thread_factory);

    break;// 1、使用密钥管理器添加bootstrap_中配置的静态密钥
// 2、使用集群管理工厂，传入bootstrap配置，创建集群管理器
// 3、创建限速客户端工厂RateLimitClientFactory
// 4、调用监听器管理器，添加所有静态配置的监听器
// 对于每个监听器，如果存在draining的监听器占用了它绑定的地址，则夺取其SocketSharedPtr
// 如果不存在，则调用创建新的SocketSharedPtr，可能导致创建底层套接字，并绑定到端口
// 5、调用initializeTracers，为当前Envoy服务器初始化Tracer
// 6、调用initializeStatsSinks，初始化监控信息的Sink
  }
  case Server::Mode::Validate:
    break;
  }
}</pre>
<div class="blog_h3"><span class="graybg">InstanceImpl</span></div>
<p>InstanceImpl类实现接口Instance，代表一个运行中的、由若干紧密协作的组件构成的、独立运行的Envoy服务。</p>
<p>该类的构造函数逻辑如下：</p>
<pre class="crayon-plain-tag">// 创建基于文件的日志Sink
if (!options.logPath().empty()) {
    file_logger_ = std::make_unique&lt;Logger::FileSinkDelegate&gt;(
            options.logPath(), access_log_manager_, Logger::Registry::getSink());
}
// 初始化热重启器
restarter_.initialize(*dispatcher_, *this);
// 创建DrainManager
drain_manager_ = component_factory.createDrainManager(*this);
// 初始化Envoy服务
initialize(options, local_address, component_factory);</pre>
<p>初始化逻辑如下：</p>
<pre class="crayon-plain-tag">// 首先打印epoch信息，以及所有以及注册的、静态链接的扩展
ENVOY_LOG(info, "initializing epoch {} (hot restart version={})", options.restartEpoch(),
          restarter_.version());

ENVOY_LOG(info, "statically linked extensions:");
ENVOY_LOG(info, "  access_loggers: {}",
          Registry::FactoryRegistry&lt;Configuration::AccessLogInstanceFactory&gt;::allFactoryNames());
// 编写扩展时，你需要负责注册扩展，只需要声明如下形式的静态变量即可
//                                  扩展类名               扩展所属分类
// static Registry::RegisterFactory&lt;FileAccessLogFactory, Server::Configuration::AccessLogInstanceFactory&gt; register_;


// 将Bootstrap配置（由Pilot Agent生成）读取到envoy::config::bootstrap::v2::Bootstrap中
// Bootstrap是Protobuf消息（::google::protobuf::Message）
// 自动依据扩展名来决定如何解析配置文件
// 如果指定了--config-yaml选项，则YAML中的配置覆盖到--config-path指定的配置中
// 如果以Bootstrap V2配置解析失败，则尝试以V1重新解析
InstanceUtil::loadBootstrapConfig(bootstrap_, options);
// 记录Bootstrap配置修改时间
bootstrap_config_update_time_ = time_system_.systemTime();

// 需要尽早配置StoreRoot，以支持监控指标收集
// TagProducer分析一个指标名，从中抽取一系列标签
stats_store_.setTagProducer(Config::Utility::createTagProducer(bootstrap_));
// StatsMatcher决定哪些指标启用禁用
stats_store_.setStatsMatcher(Config::Utility::createStatsMatcher(bootstrap_));

// 在上述存储中创建Envoy服务状态统计
// struct ServerStats {
//   Stats::Gauge &amp;uptime_;
//   Stats::Gauge &amp;concurrency_;
//   ...
// };
server_stats_ = std::make_unique&lt;ServerStats&gt;(
                // (stats_store_).gauge("server." + std::string("uptime")), ...
    ServerStats{ALL_SERVER_STATS(POOL_GAUGE_PREFIX(stats_store_, "server."))});

// 收集server.***指标
server_stats_-&gt;concurrency_.set(options_.concurrency());
server_stats_-&gt;hot_restart_epoch_.set(options_.restartEpoch());

// 本地信息，例如节点名称、所属集群、IP地址
local_info_ = std::make_unique&lt;LocalInfo::LocalInfoImpl&gt;(
    bootstrap_.node(), local_address, options.serviceZone(), options.serviceClusterName(),
    options.serviceNodeName());

// 创建一个Initial对象，此对象表示初始化配置 —— 在加载主配置之前需要知道的配置信息
Configuration::InitialImpl initial_config(bootstrap_);

// 如果可以，关闭父进程中的admin processing，这让admin processing可以启动一个新进程
HotRestart::ShutdownParentAdminInfo info;
info.original_start_time_ = original_start_time_;
restarter_.shutdownParentAdmin(info);
original_start_time_ = info.original_start_time_;

// 全局的admin HTTP端点
admin_ = std::make_unique&lt;AdminImpl&gt;(initial_config.admin().profilePath(), *this);
admin_-&gt;startHttpListener(initial_config.admin().accessLogPath(), options.adminAddressPath(),
                              initial_config.admin().address(),
                              stats_store_.createScope("listener.admin."));

// ConfigTracker供admin端点/config_dump使用，管理一系列能够提供配置信息的回调函数
config_tracker_entry_ =
// add返回EntryOwner，此类型实现了map条目的RAII语义 —— 仅当EntryOwner或ConfigTracker销毁后条目被移除
      admin_-&gt;getConfigTracker().add("bootstrap", [this] { return dumpBootstrapConfig(); });

// 将admin监听器添加到连接管理器。ConnectionHandler能够添加/删除/启用/禁用/停止网路监听器
// 调用handler-&gt;addListener(*listener_)后，创建ActiveListener，监听15000端口。也就是说ConnectionHandler负责启动端口监听
admin_-&gt;addListenerToHandler(handler_.get());

// 创建过载管理器
overload_manager_ = std::make_unique&lt;OverloadManagerImpl&gt;(dispatcher(), stats(), threadLocal(),
                                                            bootstrap_.overload_manager());

// 创建监听器管理器，负责管理：所有监听器、所有负责连接处理的工作线程（Worker）
// 创建指定数量的WorkerImpl，为每个WorkerImpl向过载管理器注册过载回调（行为是不再接受连接）
// 为每个WorkerImpl创建Dispatcher，并将此Dispatcher注册到ThreadLocal::Instance以支持后续读写线程本地变量
listener_manager_ = std::make_unique&lt;ListenerManagerImpl&gt;(*this, listener_component_factory_,
    worker_factory_, time_system_);

// 主线程也需要注册到TLS
thread_local_.registerThread(*dispatcher_, true);

// 当所有工作线程对象初始化后，调用下面的方法，调用Stats::StoreRoot的如下方法，以支持多线程操作
stats_store_.initializeThreading(*dispatcher_, thread_local_);

// Runtime::Loader能够从磁盘读取运行时快照
runtime_loader_ = component_factory.createRuntime(*this, initial_config);

// SSL上下文管理器，管理进程中所有SSL上下文
// 实现类ContextManagerImpl，线程模型如下：上下文可以从任意线程创建（但是实践上通常从主线程分配）
// 上下文的分配/销毁是少见操作，因此整体上使用一把锁来保护
ssl_context_manager_ = std::make_unique&lt;Ssl::ContextManagerImpl&gt;(time_system_);

// ProdClusterManagerFactory是ClusterManagerFactory的产品环境实现，Envoy很多命名包含Prod的类，用于和测试、Mock用途的类区分
// 集群管理器工厂，负责创建集群管理操作所需要的对象
cluster_manager_factory_ = std::make_unique&lt;Upstream::ProdClusterManagerFactory&gt;(
      runtime(), stats(), threadLocal(), random(), dnsResolver(), sslContextManager(), dispatcher(),
      localInfo(), secretManager(), api(), http_context_);

// Configuration::MainImpl是主服务器配置的实现，其初initialize方法必须在Envoy服务器完全准备好后调用，应用自举配置到服务器：
config_.initialize(bootstrap_, *this, *cluster_manager_factory_);
// 为HTTP上下文设置Tracer
http_context_.setTracer(config_.httpTracer());

// 如果包含LDS动态资源
if (bootstrap_.dynamic_resources().has_lds_config()) {
    // 则调用监听器管理器，创建LDS API Provider
    // 委托给ListenerComponentFactory.createLdsApi，最终会在cm.adsMux()上注册gRPC订阅
    listener_manager_-&gt;createLdsApi(bootstrap_.dynamic_resources().lds_config());
}

// 将Sink添加到StoreRoot
for (Stats::SinkPtr&amp; sink : config_.statsSinks()) {
    stats_store_.addSink(*sink);
}
// 注册Stat刷出定时器
// 某些Sink需要Dispatcher的支持，因此在主循环开始前，不能刷出
stat_flush_timer_ = dispatcher_-&gt;createTimer([this]() -&gt; void { flushStats(); });
stat_flush_timer_-&gt;enableTimer(config_.statsFlushInterval());

// 看门狗，用于死锁检测，在Worker启动之前、主循环run之前初始化
guard_dog_ = std::make_unique&lt;Server::GuardDogImpl&gt;(stats_store_, config_, time_system_, api());</pre>
<div class="blog_h3"><span class="graybg">MainCommonBase::run</span></div>
<p>此方法的代码如下：</p>
<pre class="crayon-plain-tag">bool MainCommonBase::run() {
  // 对应不同的运行模式
  switch (options_.mode()) {
  case Server::Mode::Serve:
    // 调用Server::InstanceImpl::run
    server_-&gt;run();
    return true;
  case Server::Mode::Validate: {
    ...
  }
  case Server::Mode::InitOnly: {
    ...
  }
}</pre>
<div class="blog_h3"><span class="graybg">InstanceImpl::run</span></div>
<p>此方法的代码如下：</p>
<pre class="crayon-plain-tag">void InstanceImpl::run() {
  // 创建运行助手
  //                                        服务器实例
  //                                               服务器选项  事件分发器     集群管理器
  run_helper_ = std::make_unique&lt;RunHelper&gt;(*this, options_, *dispatcher_, clusterManager(),
  //                                        日志管理器             初始化管理器     过载管理器
                                            access_log_manager_, init_manager_, overloadManager(),
  //                                        用于启动工作线程的回调函数
                                            [this]() -&gt; void { startWorkers(); });
  // 看门狗
  auto watchdog = guard_dog_-&gt;createWatchDog(api_-&gt;threadFactory().currentThreadId());
  watchdog-&gt;startWatchdog(*dispatcher_);
  // Event::Dispatcher是对libevent的包装，负责事件分发
  // 启动事件循环，阻塞
  dispatcher_-&gt;run(Event::Dispatcher::RunType::Block);
  // 重置看门狗
  guard_dog_-&gt;stopWatching(watchdog);
  watchdog.reset();
  // 停止
  terminate();
  // 重置运行助手
  run_helper_.reset();
}</pre>
<div class="blog_h3"><span class="graybg">RunHelper</span></div>
<p>此运行助手对象的职责包括：</p>
<ol>
<li>初始化信号处理，主要时关闭Instance</li>
<li>设置集群管理器的<span style="background-color: #c0c0c0;">初始化后</span>回调
<ol>
<li>暂停RDS订阅</li>
<li>执行初始化管理器的初始化，初始化其上注册的所有目标，<span style="background-color: #c0c0c0;">最后启动工作线程</span></li>
<li>恢复RDS订阅</li>
</ol>
</li>
<li>启动过载管理器</li>
</ol>
<p>构造函数代码如下：</p>
<pre class="crayon-plain-tag">RunHelper::RunHelper(Instance&amp; instance, Options&amp; options, Event::Dispatcher&amp; dispatcher,
                     Upstream::ClusterManager&amp; cm, AccessLog::AccessLogManager&amp; access_log_manager,
                     InitManagerImpl&amp; init_manager, OverloadManager&amp; overload_manager,
                     std::function&lt;void()&gt; workers_start_cb) {

  // 创建基于Dispatcher的信号处理事件回调
  if (options.signalHandlingEnabled()) {
    sigterm_ = dispatcher.listenForSignal(SIGTERM, [&amp;instance]() {
      instance.shutdown();
    });

    sigint_ = dispatcher.listenForSignal(SIGINT, [&amp;instance]() {
      instance.shutdown();
    });

    sig_usr_1_ = dispatcher.listenForSignal(SIGUSR1, [&amp;access_log_manager]() {
      access_log_manager.reopen();
    });

    sig_hup_ = dispatcher.listenForSignal(SIGHUP, []() {
      // 不响应挂断信号，要知道如何热重启，查看文档
    });
  }

  // 启动集群管理器初始化后回调，直到上游集群全部初始化完毕，不会启动工作线程并处理流量
  cm.setInitializedCb([&amp;instance, &amp;init_manager, &amp;cm, workers_start_cb]() {
    // 如果实例被关闭，不执行任何操作。随时都可能收到信号
    if (instance.isShutdown()) {
      return;
    }

    // 暂停RDS发现，确保在订阅了所有RDS资源之前，不会发送任何请求
    // 订阅在初始化回调中发生，因此在初始化管理器的初始化回调执行完毕之前暂停RDS发现
    cm.adsMux().pause(Config::TypeUrl::get().RouteConfiguration);

    // 所有集群均已经初始化，现在初始化 初始化管理器

    // 下面的回调不能捕获this，因为它执行的时候RunHelper可能已经销毁
    init_manager.initialize([&amp;instance, workers_start_cb]() {
      // 如果实例被关闭，不执行任何操作。随时都可能收到信号
      if (instance.isShutdown()) {
        return;
      }
      // 否则（在初始化所有注册的target之后）启动工作线程
      workers_start_cb();
    });

    // 初始化回调执行完毕
    cm.adsMux().resume(Config::TypeUrl::get().RouteConfiguration);
  });
  // 启动负载管理器
  overload_manager.start();
}</pre>
<div class="blog_h3"><span class="graybg">DispatcherImpl::run</span></div>
<p>此方法的代码如下：</p>
<pre class="crayon-plain-tag">void DispatcherImpl::run(RunType type) {
  run_tid_ = api_.threadFactory().currentThreadId();
  // 在执行事件循环之间的钩子回调
  runPostCallbacks();
  // 启动libevent事件循环
  event_base_loop(base_.get(), type == RunType::NonBlock ? EVLOOP_NONBLOCK : 0);
}</pre>
<p>到这里可以看到，启动过程的最后，主线程陷入无限循环。</p>
<p>所有后续的逻辑，由Envoy的事件机制来触发。此事件机制的核心是事件分发器接口（Dispatcher），在Envoy启动期间，很多事件回调被注册到Dispatcher，并在以后异步的、可能反复的执行。</p>
<div class="blog_h2"><span class="graybg">事件分发器</span></div>
<p>Envoy使用<a href="/libevent-study-note">libevent2</a>提供的事件机制，但是在其上做了一层封装 —— Dispatcher，事件回调就是通过此Dispatcher注册的。Dispatcher的接口如下：</p>
<pre class="crayon-plain-tag">namespace Envoy {
namespace Event {


struct FileReadyType {
  // 文件可读
  static const uint32_t Read = 0x1;
  // 文件可写
  static const uint32_t Write = 0x2;
  // 对端关闭了文件
  static const uint32_t Closed = 0x4;
};

/**
 * 事件分发器的post()调用之后执行的回调
 */
typedef std::function&lt;void()&gt; PostCb;

/**
 * 抽象的事件分发循环
 */
class Dispatcher {
public:
  virtual ~Dispatcher() {}

  /**
   * 此分发器使用的时间源
   */
  virtual TimeSystem&amp; timeSystem() PURE;

  /**
   * 清空延迟删除队列
   */
  virtual void clearDeferredDeleteList() PURE;

  /**
   * 创建一个服务器连接
   * @param socket 该指针指向ConnectionSocket，此结构持有一个套接字（FD）以及一些元数据（例如本地地址）
   *               对于服务器连接来说，此结构代表一个已经accept的套接字
   *               对于客户端连接来说，此结构代码一个正连接到远程地址的套接字
   * @param transport_socket 提供连接使用的传输套接字。TransportSocket负责实际的读写以及数据转换（例如TLS）
   * @return Network::ConnectionPtr 返回一个归属调用者的服务器连接对象
   */
  virtual Network::ConnectionPtr
  createServerConnection(Network::ConnectionSocketPtr&amp;&amp; socket, Network::TransportSocketPtr&amp;&amp; transport_socket) PURE;

  /**
   * 创建一个客户端连接
   * @param address 需要连接到的服务器
   * @param source_address 绑定到的本地地址，或者nulptr自动绑定
   * @param transport_socket 此连接使用的传输套接字
   * @param options 套接字选项
   * @return Network::ClientConnectionPtr 返回一个归属调用者的客户端连接对象
   */
  virtual Network::ClientConnectionPtr
  createClientConnection(Network::Address::InstanceConstSharedPtr address,
                         Network::Address::InstanceConstSharedPtr source_address,
                         Network::TransportSocketPtr&amp;&amp; transport_socket,
                         const Network::ConnectionSocket::OptionsSharedPtr&amp; options) PURE;

  /**
   * 创建一个供启动事件循环的线程独占使用的异步DNS解析器
   * @param resolvers DNS服务器地址，默认/etc/resolv.conf
   * @return 返回归属调用者的Network::DnsResolverSharedPtr
   */
  virtual Network::DnsResolverSharedPtr
  createDnsResolver(const std::vector&lt;Network::Address::InstanceConstSharedPtr&gt;&amp; resolvers) PURE;

  /**
   * 创建一个文件（对于Linux来说，任何文件式的接口，包括普通文件、套接字都支持），当其可读可写时触发回调
   * @param fd 监控的文件描述符
   * @param cb 文件可读写时执行的回调
   * @param trigger 边缘触发还是水平触发
   * @param events 此事件最初监听的事件类型，FileReadyType按位或
   */
  virtual FileEventPtr createFileEvent(int fd, FileReadyCb cb, FileTriggerType trigger, uint32_t events) PURE;

  /**
   * @return Filesystem::WatcherPtr 返回归属调用者的文件系统监控器
   */
  virtual Filesystem::WatcherPtr createFilesystemWatcher() PURE;

  /**
   * 在指定的端口上创建一个监听器
   * @param socket 需要监听的套接字
   * @param cb 监听器事件的处理回调
   * @param bind_to_port 是否需要绑定到传输端口
   * @param hand_off_restored_destination_connections 当恢复（restoring）了新连接的目的地址后，该监听器是否
   *                                                  应该搜索另外一个（更匹配连接的）监听器
   * @return Network::ListenerPtr 返回归属调用者的监听器
   */
  virtual Network::ListenerPtr createListener(Network::Socket&amp; socket,
                                              Network::ListenerCallbacks&amp; cb, bool bind_to_port,
                                              bool hand_off_restored_destination_connections) PURE;

  /**
   *  分配一个定时器
   */
  virtual Event::TimerPtr createTimer(TimerCb cb) PURE;

  /**
   * 提交一个条目，延迟删除
   */
  virtual void deferredDelete(DeferredDeletablePtr&amp;&amp; to_delete) PURE;

  /**
   * 退出事件循环
   */
  virtual void exit() PURE;

  /**
   * 监听信号事件，进程中只有单个分发器可以处理信号，否则行为未定义
   *
   * @param signal_num 需要监听的信号
   * @param cb 信号发生时执行的回调
   * @return SignalEventPtr返回归属调用者的信号事件
   */
  virtual SignalEventPtr listenForSignal(int signal_num, SignalCb cb) PURE;

  /**
   * 添加一个Post回调，此回调在执行事件循环的那个线程异步的执行。此调用线程安全
   */
  virtual void post(PostCb callback) PURE;

  /**
   * 执行事件循环，知道某个回调或其它线程调用exit()
   * 阻塞模式：除非exit()被调用，不会退出循环
   * 非阻塞模式：仅仅执行活动事件的回调，然后事件循环就退出
   */
  enum class RunType { Block, NonBlock };
  virtual void run(RunType type) PURE;

  /**
   * 获取此分发器的带水位支持的缓冲的工厂
   */
  virtual Buffer::WatermarkFactory&amp; getWatermarkFactory() PURE;
};

typedef std::unique_ptr&lt;Dispatcher&gt; DispatcherPtr;

} // namespace Event
} // namespace Envoy</pre>
<p>DispatcherImpl是基于libevent的Dispatcher实现。可以看到，注册回调主要依靠createFileEvent方法。</p>
<p>在Envoy启动阶段，会注册多个事件回调，包括热重启模块的信号回调、DNS解析模块的回调、ADS客户端的定时器回调。</p>
<div class="blog_h2"><span class="graybg">初始化管理器</span></div>
<div class="blog_h3"><span class="graybg">Init::Manager</span></div>
<p>该接口用于执行<span style="background-color: #c0c0c0;">多个目标的初始化</span>：</p>
<pre class="crayon-plain-tag">class Manager {
public:
  virtual ~Manager() {}

  /**
   * 注册一个在未来需要初始化的目标，初始化管理器会在适当的时候，调用所有目标的initialize方法
   */
  virtual void registerTarget(Target&amp; target) PURE;

  enum class State {
    /**
     * 目标们尚未被初始化
     */
    NotInitialized,
    /**
     * 目标们正在被初始化
     */
    Initializing,
    /**
     * 所有目标已经初始化完毕
     */
    Initialized
  };

  /**
   * 返回状态
   */
  virtual State state() const PURE;
}; </pre>
<p>单个初始化目标由下面的类型表示：</p>
<pre class="crayon-plain-tag">class Target {
public:
  virtual ~Target() {}

  /**
   * 当目标应该进行初始化时调用该方法
   * @param callback 目标的初始化完成后，调用的回调
   */
  virtual void initialize(std::function&lt;void()&gt; callback) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">Server::InitManagerImpl</span></div>
<p>该实现用于Post集群管理器、Pre监听时的初始化管理。集群管理器并不是单个实例，例如每个Cluster都有自己的初始化管理器。</p>
<pre class="crayon-plain-tag">class InitManagerImpl : public Init::Manager {
public:
  // 初始化所有目标，并在完毕后执行总回调
  // 1、如果目标集为空，直接总回调，进入Initialized状态
  // 2、否则，将此总回调赋值给实例变量。进入Initializing状态，依次initializeTarget每个目标
  void initialize(std::function&lt;void()&gt; callback);
  void registerTarget(Init::Target&amp; target) override;
  State state() const override { return state_; }

private:
  // 调用单个目标的initialize，并在完成后回调中移除此目标
  // 如果移除后没有更多目标，将管理器设置为Initialized状态，并且执行总回调
  void initializeTarget(Init::Target&amp; target);
  std::list&lt;Init::Target*&gt; targets_;
  State state_{State::NotInitialized};
  // 总回调
  std::function&lt;void()&gt; callback_;
};</pre>
<div class="blog_h2"><span class="graybg">热重启</span></div>
<p>HotRestartImpl实现热重启功能，在初始化时它为UDS注册一个事件回调：</p>
<pre class="crayon-plain-tag">void HotRestartImpl::initialize(Event::Dispatcher&amp; dispatcher, Server::Instance&amp; server) {
  socket_event_ =
      // 注册事件回调
      dispatcher.createFileEvent(my_domain_socket_,
                                 [this](uint32_t events) -&gt; void {
                                   ASSERT(events == Event::FileReadyType::Read);
                                   onSocketEvent();
                                 },
                                 // 边缘触发，监听可读事件
                                 Event::FileTriggerType::Edge, Event::FileReadyType::Read);
  server_ = &amp;server;
}</pre>
<p>事件到达后调用onSocketEvent()方法，获取RCP消息，根据消息类型做出各种处理，例如：关闭Admin端点、Drain监听器、返回监控统计信息、停止当前进程。</p>
<div class="blog_h2"><span class="graybg">DNS解析器</span></div>
<p>DnsResolver负责异步的DNS解析。多个组件需要进行DNS解析，例如StrictDnsClusterImpl.startPreInit方法会解析集群的DNS域名。</p>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>InstanceImpl在构造函数中创建DnsResolverImpl：</p>
<pre class="crayon-plain-tag">InstanceImpl::InstanceImpl( ... ): ...
   dns_resolver_(dispatcher_-&gt;createDnsResolver({})){
   ...
}
Network::DnsResolverSharedPtr DispatcherImpl::createDnsResolver( const std::vector&lt;Network::Address::InstanceConstSharedPtr&gt;&amp; resolvers) {
  return Network::DnsResolverSharedPtr{new Network::DnsResolverImpl(*this, resolvers)};
}</pre>
<p>DnsResolverImpl在初始化期间，会将onAresSocketStateChange方法作为回调传递给ares：</p>
<pre class="crayon-plain-tag">DnsResolverImpl::DnsResolverImpl(){
  initializeChannel(&amp;options, 0);
}

void DnsResolverImpl::initializeChannel(ares_options* options, int optmask) {
  // 将回调提供给ares
  options-&gt;sock_state_cb = [](void* arg, int fd, int read, int write) {
    static_cast&lt;DnsResolverImpl*&gt;(arg)-&gt;onAresSocketStateChange(fd, read, write);
  };
  options-&gt;sock_state_cb_data = this;
  // 初始化ares
  ares_init_options(&amp;channel_, options, optmask | ARES_OPT_SOCK_STATE_CB);
}</pre>
<p>当某个ares套接字可读可写时，会调用onAresSocketStateChange：</p>
<pre class="crayon-plain-tag">void DnsResolverImpl::onAresSocketStateChange(int fd, int read, int write) {
  auto it = events_.find(fd);
  // 如果不可读不可写，而且之间已经跟踪了此fd，则删除fd不再跟踪
  if (read == 0 &amp;&amp; write == 0) {
    if (it != events_.end()) {
      events_.erase(it);
    }
    return;
  }

  // 如果之前没有跟踪过此fd
  if (it == events_.end()) {
    // 注册事件回调
    events_[fd] = dispatcher_.createFileEvent(
        fd, [this, fd](uint32_t events) { onEventCallback(fd, events); },
        Event::FileTriggerType::Level, Event::FileReadyType::Read | Event::FileReadyType::Write);
  }
}

// 事件回调的逻辑是调用ares：
void DnsResolverImpl::onEventCallback(int fd, uint32_t events) {
  const ares_socket_t read_fd = events &amp; Event::FileReadyType::Read ? fd : ARES_SOCKET_BAD;
  const ares_socket_t write_fd = events &amp; Event::FileReadyType::Write ? fd : ARES_SOCKET_BAD;
  ares_process_fd(channel_, read_fd, write_fd);
  updateAresTimer();
}</pre>
<div class="blog_h3"><span class="graybg">解析过程</span></div>
<p>上面的“ares套接字可读可写”，可以由解析DNS的请求来触发，例如StrictDnsClusterImpl.startPreInit会触发DNS解析：</p>
<pre class="crayon-plain-tag">void StrictDnsClusterImpl::startPreInit() {
  for (const ResolveTargetPtr&amp; target : resolve_targets_) {
    target-&gt;startResolve();
  }
}

void StrictDnsClusterImpl::ResolveTarget::startResolve() {
  active_query_ = parent_.dns_resolver_-&gt;resolve(
  ..., []( address_list )-&gt;void {
         // 解析成功后的回调
       }
  )
}

ActiveDnsQuery* DnsResolverImpl::resolve(const std::string&amp; dns_name, DnsLookupFamily dns_lookup_family, ResolveCb callback) {
  std::unique_ptr&lt;PendingResolution&gt; pending_resolution( new PendingResolution(callback, dispatcher_, channel_, dns_name));  
  pending_resolution-&gt;getHostByName(AF_INET6);
}</pre>
<p>上面代码的最后一个方法，回调被传递给PendingResolution对象，随即调用其getHostByName方法：</p>
<pre class="crayon-plain-tag">void DnsResolverImpl::PendingResolution::getHostByName(int family) {
  ares_gethostbyname(channel_, dns_name_.c_str(), family,
                     [](void* arg, int status, int timeouts, hostent* hostent) {
                       static_cast&lt;PendingResolution*&gt;(arg)-&gt;onAresHostCallback(status, timeouts, hostent);
                     }, this);
}</pre>
<p>此方法调用c-ares库进行主机名解析，回调是PendingResolution的onAresHostCallback方法：</p>
<pre class="crayon-plain-tag">void DnsResolverImpl::PendingResolution::onAresHostCallback(int status, int timeouts,
                                                            hostent* hostent) {
  // 解析处地址列表，并调用上面的回调C0
  callback_(std::move(address_list));
}</pre>
<p>总之，DNS解析是由Dispatcher的回调机制和ares的回调机制协作实现的，达到的效果是异步、高效的DNS解析。</p>
<div class="blog_h2"><span class="graybg"><a id="init-cluster-mgr"></a>集群初始化</span></div>
<p>在Envoy主服务InstanceImpl的主配置的初始化过程中，会创建集群管理器ClusterManagerImpl。</p>
<div class="blog_h3"><span class="graybg">ClusterManagerImpl</span></div>
<p>集群管理器负责Bootstrap配置中定义的<span style="background-color: #c0c0c0;">静态集群的初始化，以及CDS/EDS的订阅的启动</span>。这个处理过程比较复杂，分为多个阶段完成</p>
<ol>
<li>初始化静态/DNS集群</li>
<li>初始化预定义的EDS集群</li>
<li>如果需要，初始化CDS订阅，并等待响应</li>
<li>初始化CDS提供的集群，分为两个阶段
<ol>
<li>主（Primary）集群初始化，所有非EDS集群是主集群</li>
<li>次（Secondary）集群初始化，EDS集群为次集群。每个EDS集群会独自创建一个xDS订阅，这导致EDS集群需要依赖于非EDS集群（xds-grpc，STRICT_DNS集群），因此EDS集群需要在第二阶段初始化</li>
</ol>
</li>
<li>如果集群启用了主动健康检查，此时会触发单轮检查</li>
</ol>
<pre class="crayon-plain-tag">ClusterManagerImpl::ClusterManagerImpl(...)
    : ...,
      // 注册针对每个集群的初始化回调，每个集群本身初始化完毕后调用此回调。集群管理器使用此回调进行后初始化处理
      init_helper_([this](Cluster&amp; cluster) { onClusterInit(cluster); }),
      config_tracker_entry_(
          admin.getConfigTracker().add("clusters", [this] { return dumpClusterConfigs(); })),
      // 使用主线程分发器，及其时间源
      time_source_(main_thread_dispatcher.timeSystem()), dispatcher_(main_thread_dispatcher),
      http_context_(http_context) {
  // gRPC异步客户端管理器（AsyncClientManager）可以为每个gRPC服务（envoy::api::v2::core::GrpcService配置）
  // 创建AsyncClient的工厂
  async_client_manager_ =
      std::make_unique&lt;Grpc::AsyncClientManagerImpl&gt;(*this, tls, time_source_, api);
  // 获得自举配置中的集群管理器配置
  const auto&amp; cm_config = bootstrap.cluster_manager();
  ...
  // 处理v1 API的遗留风格的服务发现配置
  if (bootstrap.dynamic_resources().deprecated_v1().has_sds_config()) {
    eds_config_ = bootstrap.dynamic_resources().deprecated_v1().sds_config();
  }

  // 读取所有静态资源
  for (const auto&amp; cluster : bootstrap.static_resources().clusters()) {
    // 加载所有主集群，默认配置下有prometheus-stat、xds-grpc两个静态配置的主集群
    if (cluster.type() != envoy::api::v2::Cluster::EDS) {
      loadCluster(cluster, "", false, active_clusters_);
    }
  }

  // 如果必要，创建ADS，可能依赖于主集群
  if (bootstrap.dynamic_resources().has_ads_config()) {
    ads_mux_ = std::make_unique&lt;Config::GrpcMuxImpl&gt;(
        local_info, // 代理本地环境信息
        // 创建gRPC异步客户端
        Config::Utility::factoryForGrpcApiConfigSource(
            *async_client_manager_, bootstrap.dynamic_resources().ads_config(), stats)
            -&gt;create(),
        // 使用主线程的事件分发器
        main_thread_dispatcher,
        // 寻找gRPC服务
        *Protobuf::DescriptorPool::generated_pool()-&gt;FindMethodByName(
            "envoy.service.discovery.v2.AggregatedDiscoveryService.StreamAggregatedResources"),
        // 随机数生成器
                 // Stats::Scope，被限定了scope的stats
        random_, stats_,
        // 限速配置
        Envoy::Config::Utility::parseRateLimitSettings(bootstrap.dynamic_resources().ads_config()));
  } else {
    // 没有ADS配置，提供一个占位符
    ads_mux_ = std::make_unique&lt;Config::NullGrpcMuxImpl&gt;();
  }

  // 在ADS初始化后，加载EDS类型的静态集群，这种集群可能依赖ADS来发现端点
  for (const auto&amp; cluster : bootstrap.static_resources().clusters()) {
    // Now load all the secondary clusters.
    if (cluster.type() == envoy::api::v2::Cluster::EDS) {
      loadCluster(cluster, "", false, active_clusters_);
    }
  }
  // 更新监控指标
  cm_stats_.cluster_added_.add(bootstrap.static_resources().clusters().size());
  updateGauges();

  // 到这里，所有静态集群已经加载完毕，现在检查基于v1 API SDS的集群、基于ADS的集群
  const ClusterInfoMap loaded_clusters = clusters();
  if (bootstrap.dynamic_resources().deprecated_v1().has_sds_config()) {
    ...
  }

  // 获取当前本地集群的名称
  absl::optional&lt;std::string&gt; local_cluster_name;
  if (!cm_config.local_cluster_name().empty()) {
    local_cluster_name_ = cm_config.local_cluster_name();
    local_cluster_name = cm_config.local_cluster_name();
    if (active_clusters_.find(local_cluster_name.value()) == active_clusters_.end()) {
      throw EnvoyException(
          fmt::format("local cluster '{}' must be defined", local_cluster_name.value()));
    }
  }

  // 一旦最初的静态Bootstrap集群被创建（包括本地集群），就可以创建线程本地的集群管理器
  tls_-&gt;set([this, local_cluster_name](
                Event::Dispatcher&amp; dispatcher) -&gt; ThreadLocal::ThreadLocalObjectSharedPtr {
    // ThreadLocalClusterManagerImpl在线程本地缓存集群数据，并从parent central dynamic cluster获取更新
    // 此对象维护负载均衡器状态、所有已经创建的线程池
    return std::make_shared&lt;ThreadLocalClusterManagerImpl&gt;(*this, dispatcher, local_cluster_name);
  });

  // 创建CDS客户端，并传递给集群管理器初始化助手ClusterManagerInitHelper对象
  if (bootstrap.dynamic_resources().has_cds_config()) {
    cds_api_ = factory_.createCds(bootstrap.dynamic_resources().cds_config(), eds_config_, *this);
    init_helper_.setCds(cds_api_.get());
  } else {
    init_helper_.setCds(nullptr);
  }

  // 将所有静态自举集群传递给初始化助手处理，这会导致：
  // 1、初始化所有主集群
  // 2、进行post-init处理，来初始化任何线程感知的负载均衡器，并创建per-worker的主机（端点）集更新
  for (auto&amp; cluster : active_clusters_) {
    init_helper_.addCluster(*cluster.second-&gt;cluster_);
  }

  // 将状态设置为WaitingForStaticInitialize
  // 如果所有主集群都初始化完毕了，可能进行静态配置的次集群（EDS）初始化
  // 此方法会调用maybeFinishInitialize
  init_helper_.onStaticLoadComplete();

  // 启动ADS客户端，创建新的gRPC流
  ads_mux_-&gt;start();
} </pre>
<div class="blog_h3"><span class="graybg">ClusterImplBase::initialize</span></div>
<p>由集群管理器调用init_helper_.addCluster完成，该方法实现如下：</p>
<pre class="crayon-plain-tag">void ClusterManagerInitHelper::addCluster(Cluster&amp; cluster) {
  // 集群第一次初始化后执行的回调。例如，对于动态DNS集群，此回调将在最初的DNS解析完成后调用
  const auto initialize_cb = [&amp;cluster, this] { onClusterInit(cluster); };
  if (cluster.initializePhase() == Cluster::InitializePhase::Primary) {
    // 主集群初始化
    primary_init_clusters_.push_back(&amp;cluster);
    cluster.initialize(initialize_cb);
  } else {
    // 从CDS获取到的动态集群，走这个分支
    secondary_init_clusters_.push_back(&amp;cluster);
    // 如果当前已经启动了第二阶段初始化，则立即开始初始化
    if (started_secondary_initialize_) {
      cluster.initialize(initialize_cb);
    }
  }
}</pre>
<p>可以看到，init_helper会调用集群的initialize方法，这是定义在ClusterImplBase中的模板方法：</p>
<pre class="crayon-plain-tag">void ClusterImplBase::initialize(std::function&lt;void()&gt; callback) {
  // 设置初始化回调
  initialization_complete_callback_ = callback;
  // 开始进行预初始化
  startPreInit();
}</pre>
<div class="blog_h3"><span class="graybg">StaticClusterImpl::startPreInit</span></div>
<p>集群初始化的<span style="background-color: #c0c0c0;">第一步是预初始化</span>，静态集群对该方法的实现如下：</p>
<pre class="crayon-plain-tag">void StaticClusterImpl::startPreInit() {
  // 检查是否配置了监控检查，如果是，将所有节点标记为不健康
  const auto&amp; health_checker_flag =
      health_checker_ != nullptr
          ? absl::optional&lt;Upstream::Host::HealthFlag&gt;(Host::HealthFlag::FAILED_ACTIVE_HC)
          : absl::nullopt;
  // PriorityState为每个优先级绑定一组 主机+对应的位置权重映射
  auto&amp; priority_state = priority_state_manager_-&gt;priorityState();
  for (size_t i = 0; i &lt; priority_state.size(); ++i) {
    priority_state_manager_-&gt;updateClusterPrioritySet(
        i, std::move(priority_state[i].first), absl::nullopt, absl::nullopt, health_checker_flag,
        overprovisioning_factor_);
  }
  priority_state_manager_.reset();
  // 预初始化完成后回调
  onPreInitComplete();
}</pre>
<div class="blog_h3"><span class="graybg">StrictDnsClusterImpl::startPreInit</span></div>
<p>DNS集群的startPreInit，从DNS解析开始：</p>
<pre class="crayon-plain-tag">void StrictDnsClusterImpl::startPreInit() {
  for (const ResolveTargetPtr&amp; target : resolve_targets_) {
    target-&gt;startResolve();
  }
}</pre>
<p>DNS解析完成后，异步的执行下面的回调：</p>
<pre class="crayon-plain-tag">void StrictDnsClusterImpl::ResolveTarget::startResolve() {
  ENVOY_LOG(debug, "starting async DNS resolution for {}", dns_address_);
  parent_.info_-&gt;stats().update_attempt_.inc();

  active_query_ = parent_.dns_resolver_-&gt;resolve(
      dns_address_, parent_.dns_lookup_family_,
      [this](const std::list&lt;Network::Address::InstanceConstSharedPtr&gt;&amp;&amp; address_list) -&gt; void {
        active_query_ = nullptr;
        // 异步解析完成，更新指标
        parent_.info_-&gt;stats().update_success_.inc();
         
        // 为每个解析结果创建主机对象HostImpl
        std::unordered_map&lt;std::string, HostSharedPtr&gt; updated_hosts;
        HostVector new_hosts;
        for (const Network::Address::InstanceConstSharedPtr&amp; address : address_list) {
          new_hosts.emplace_back(new HostImpl(
              parent_.info_, dns_address_, Network::Utility::getAddressWithPort(*address, port_),
              lb_endpoint_.metadata(), lb_endpoint_.load_balancing_weight().value(),
              locality_lb_endpoint_.locality(), lb_endpoint_.endpoint().health_check_config(),
              locality_lb_endpoint_.priority()));
        }

        HostVector hosts_added;
        HostVector hosts_removed;
        // 调用BaseDynamicClusterImpl::updateDynamicHostList更新主机列表
        if (parent_.updateDynamicHostList(new_hosts, hosts_, hosts_added, hosts_removed, updated_hosts, all_hosts_)) {
          parent_.updateAllHosts(hosts_added, hosts_removed, locality_lb_endpoint_.priority());
        } else {
          parent_.info_-&gt;stats().update_no_rebuild_.inc();
        }

        all_hosts_ = std::move(updated_hosts);

        // 结束处理，尽管集群可能有多个DNS名称，这里在解析成功一个后就结束初始化过程
        parent_.onPreInitComplete();
        resolve_timer_-&gt;enableTimer(parent_.dns_refresh_rate_ms_);
      });
}</pre>
<div class="blog_h3"><span class="graybg">EdsClusterImpl::startPreInit</span></div>
<p>EDS集群的startPreInit，从启动EDS订阅开始：</p>
<pre class="crayon-plain-tag">void EdsClusterImpl::startPreInit() { subscription_-&gt;start({cluster_name_}, *this); } </pre>
<div class="blog_h3"><span class="graybg">ClusterImplBase::onPreInitComplete</span></div>
<p>预初始化完成后，此时端点列表已经获得。onPreInitComplete执行下面的回调，进行必要的健康检查：</p>
<pre class="crayon-plain-tag">void ClusterImplBase::onPreInitComplete() {
  // 防止重复调用
  if (initialization_started_) {
    return;
  }
  initialization_started_ = true;

  // 执行onInitDone回调
  init_manager_.initialize([this]() { onInitDone(); });
}


void ClusterImplBase::onInitDone() {
  if (health_checker_ &amp;&amp; pending_initialize_health_checks_ == 0) {
    for (auto&amp; host_set : prioritySet().hostSetsPerPriority()) {
      pending_initialize_health_checks_ += host_set-&gt;hosts().size();
    }

    // 在健康检查完毕后调用finishInitialization
    health_checker_-&gt;addHostCheckCompleteCb([this](HostSharedPtr, HealthTransition) -&gt; void {
      if (pending_initialize_health_checks_ &gt; 0 &amp;&amp; --pending_initialize_health_checks_ == 0) {
        finishInitialization();
      }
    });
  }
  // 不需要健康检查，直接调用
  if (pending_initialize_health_checks_ == 0) {
    finishInitialization();
  }
}</pre>
<p>然后调用finishInitialization方法。</p>
<div class="blog_h3"><span class="graybg">ClusterImplBase::finishInitialization</span></div>
<pre class="crayon-plain-tag">void ClusterImplBase::finishInitialization() {
  ASSERT(initialization_complete_callback_ != nullptr);
  ASSERT(initialization_started_);

  auto snapped_callback = initialization_complete_callback_;
  initialization_complete_callback_ = nullptr;
  
  // 重新载入健康（通过健康检查）的主机
  if (health_checker_ != nullptr) {
    reloadHealthyHosts();
  }

  if (snapped_callback != nullptr) {
    // 执行回调，也就是ClusterManagerInitHelper::onClusterInit
    snapped_callback();
  }
}</pre>
<div class="blog_h3"><span class="graybg">ClusterManagerInitHelper.onClusterInit</span></div>
<p>此回调的实现：</p>
<pre class="crayon-plain-tag">void ClusterManagerInitHelper::onClusterInit(Cluster&amp; cluster) {
  // 针对每个集群的初始化回调
  per_cluster_init_callback_(cluster);
  // 将当前集群，移除主集群列表/此集群列表
  removeCluster(cluster);
}

void ClusterManagerInitHelper::removeCluster(Cluster&amp; cluster) {
  if (state_ == State::AllClustersInitialized) {
    return;
  }
  std::list&lt;Cluster*&gt;* cluster_list;
  if (cluster.initializePhase() == Cluster::InitializePhase::Primary) {
    cluster_list = &amp;primary_init_clusters_;
  } else {
    ASSERT(cluster.initializePhase() == Cluster::InitializePhase::Secondary);
    cluster_list = &amp;secondary_init_clusters_;
  }
  cluster_list-&gt;remove(&amp;cluster);
  maybeFinishInitialize();
}</pre>
<p>per_cluster_init_callback_在集群管理器构造时，通过初始化列表传递给ClusterManagerInitHelper，其实现就是集群管理器的onClusterInit方法：</p>
<div class="blog_h3"><span class="graybg">ClusterManagerImpl::onClusterInit</span></div>
<p>集群管理器通过此方法进行集群的后初始化处理：</p>
<pre class="crayon-plain-tag">void ClusterManagerImpl::onClusterInit(Cluster&amp; cluster) {
  // 到这里为止，集群尚未配置以支持跨线程更新
  // ClusterData，持有集群配置、版本信息、Cluster的引用等信息
  auto cluster_data = active_clusters_.find(cluster.info()-&gt;name());
  if (cluster_data-&gt;second-&gt;thread_aware_lb_ != nullptr) {
    // 如果线程感知的负载均衡器已经存在，则初始化之
    cluster_data-&gt;second-&gt;thread_aware_lb_-&gt;initialize();
  }管理器

  // 配置以支持跨线程更新
  // PrioritySet，包含单个集群的所有HostSet，以优先级分组
  // addMemberUpdateCb，如果任何HostSet发生变化，或者新HostSet创建，调用此回调
  cluster.prioritySet().addMemberUpdateCb([&amp;cluster, this](uint32_t priority,
                                                           const HostVector&amp; hosts_added,
                                                           const HostVector&amp; hosts_removed) {
    // 当集群的主机集更新时，此回调触发。需要将此更新发送给所有线程本地配置

    // 某些情况下，合并一个时间窗口内多次主机集更新可以提升性能。目前能实现的、安全的合并，不支持添加、删除
    // 主机的情况。也就是说，仅仅对那些改变主机健康状态/权重/元数据的更新，可以被合并
    bool scheduled = false;
    // 合并时间窗口1000ms
    const auto merge_timeout =
        PROTOBUF_GET_MS_OR_DEFAULT(cluster.info()-&gt;lbConfig(), update_merge_window, 1000);
    // Remember: we only merge updates with no adds/removes — just hc/weight/metadata changes.
    const bool is_mergeable = !hosts_added.size() &amp;&amp; !hosts_removed.size();

    if (merge_timeout &gt; 0) {
      // 尝试调度合并，如果不支持合并，返回false管理器
      scheduled = scheduleUpdate(cluster, priority, is_mergeable, merge_timeout);
    }

    // 无法调度
    if (!scheduled) {
      // 立即向线程本地集群递送更新
      postThreadLocalClusterUpdate(cluster, priority, hosts_added, hosts_removed);
    }
  });

  // 递送第一次集群主机集更新
  // 遍历集群的所有HostSet
  for (auto&amp; host_set : cluster.prioritySet().hostSetsPerPriority()) {
    if (host_set-&gt;hosts().empty()) {
      continue;
    }
    // 对于包含主机的HostSet，向线程本地集群递送更新
    postThreadLocalClusterUpdate(cluster, host_set-&gt;priority(), host_set-&gt;hosts(), HostVector{});
  }
}


void ClusterManagerImpl::postThreadLocalClusterUpdate(const Cluster&amp; cluster, uint32_t priority,
                                                      const HostVector&amp; hosts_added,
                                                      const HostVector&amp; hosts_removed) {
  const auto&amp; host_set = cluster.prioritySet().hostSetsPerPriority()[priority];

  // 拷贝各类主机副本
  HostVectorConstSharedPtr hosts_copy(new HostVector(host_set-&gt;hosts()));
  HostVectorConstSharedPtr healthy_hosts_copy(new HostVector(host_set-&gt;healthyHosts()));
  HostVectorConstSharedPtr degraded_hosts_copy(new HostVector(host_set-&gt;healthyHosts()));
  HostsPerLocalityConstSharedPtr hosts_per_locality_copy = host_set-&gt;hostsPerLocality().clone();
  HostsPerLocalityConstSharedPtr healthy_hosts_per_locality_copy =  host_set-&gt;healthyHostsPerLocality().clone();
  HostsPerLocalityConstSharedPtr degraded_hosts_per_locality_copy = host_set-&gt;degradedHostsPerLocality().clone();

  // 在所有已经注册的线程的Dispatcher上执行回调（异步），并最后在主线程上同步的执行
  // 每个工作线程都有自己的Dispatcher
  // 调用Dispatcher.post()。导致回调在目标线程的Dispatcher事件循环上下文中执行
  tls_-&gt;runOnAllThreads([this, name = cluster.info()-&gt;name(), priority, hosts_copy,
                         healthy_hosts_copy, degraded_hosts_copy, hosts_per_locality_copy,
                         healthy_hosts_per_locality_copy, degraded_hosts_per_locality_copy,
                         locality_weights = host_set-&gt;localityWeights(), hosts_added,
                         hosts_removed]() {
    // 这里应该只对线程本地数据进行操作
    // 调用线程本地集群管理器，更新集群的主机成员
    // 如果使用线程感知LB（TLS集群），则重新创建负载均衡器
    ThreadLocalClusterManagerImpl::updateClusterMembership(
        name, priority,
        HostSetImpl::updateHostsParams(hosts_copy, hosts_per_locality_copy, healthy_hosts_copy,
                                       healthy_hosts_per_locality_copy, degraded_hosts_copy,
                                       degraded_hosts_per_locality_copy),
        locality_weights, hosts_added, hosts_removed, *tls_);
  });
}</pre>
<div class="blog_h3"><span class="graybg">InitHelper::maybeFinishInitialize </span></div>
<p>在每个集群的初始化后都会调用， 判断初始化过程是否可以结束：</p>
<pre class="crayon-plain-tag">void ClusterManagerInitHelper::maybeFinishInitialize() {
  // 如果当前正在进行初始的静态集群的加载，或者正等待CDS初始化完毕，不做任何事情
  if (state_ == State::Loading || state_ == State::WaitingForCdsInitialize) {
    return;
  }

  // 如果主集群没有完全初始化，不做任何事情
  if (!primary_init_clusters_.empty()) {
    return;
  }

  // 如果正在等待次集群初始化
  if (!secondary_init_clusters_.empty()) {
    // 如果次集群初始化阶段尚未开始
    if (!started_secondary_initialize_) {
      // 此触发次集群初始化阶段
      started_secondary_initialize_ = true;
      // Cluster::initialize()方法可能修改列表secondary_init_clusters_，移除当前正在初始化的集群
      for (auto iter = secondary_init_clusters_.begin(); iter != secondary_init_clusters_.end();) {
        Cluster* cluster = *iter;
        ++iter;
        // 初始化次集群
        cluster-&gt;initialize([cluster, this] { onClusterInit(*cluster); });
      }
    }
    return;
  }

  started_secondary_initialize_ = false;
  // 等待静态初始化，且需要CDS
  if (state_ == State::WaitingForStaticInitialize &amp;&amp; cds_) {
    // 进行CDS的初始化
    state_ = State::WaitingForCdsInitialize;
    cds_-&gt;initialize();
  } else {
    // 所有集群已经初始化完毕
    state_ = State::AllClustersInitialized;
    if (initialized_callback_) {
      initialized_callback_();
    }
  }
}</pre>
<div class="blog_h3"><span class="graybg">ClusterManagerImpl::setInitializedCb</span></div>
<p>调用此方法，可以设置在所有集群都初始化之后，调用的回调。</p>
<p>RunHelper会调用此函数，注册的回调会导致InitManager初始化，后者会导致：</p>
<ol>
<li>LdsApiImpl，注册到InitManager的Target，被初始化</li>
<li>LdsApiImpl初始化完毕后，工作进程启动</li>
</ol>
<div class="blog_h2"><span class="graybg">监听器初始化</span></div>
<div class="blog_h3"><span class="graybg">ListenerManagerImpl</span></div>
<p>在InstanceImpl初始化阶段，它会创建全局的监听器管理器：</p>
<pre class="crayon-plain-tag">listener_manager_ = std::make_unique&lt;ListenerManagerImpl&gt;(*this, listener_component_factory_,
                                                            worker_factory_, time_system_);</pre>
<p>构造函数如下：</p>
<pre class="crayon-plain-tag">ListenerManagerImpl::ListenerManagerImpl(Instance&amp; server,
                                         ListenerComponentFactory&amp; listener_factory,
                                         WorkerFactory&amp; worker_factory, TimeSource&amp; time_source)
    : server_(server), time_source_(time_source), factory_(listener_factory),
      stats_(generateStats(server.stats())),
      config_tracker_entry_(server.admin().getConfigTracker().add(
          "listeners", [this] { return dumpListenerConfigs(); })) {
  // 创建工作线程
  for (uint32_t i = 0; i &lt; server.options().concurrency(); i++) {
    workers_.emplace_back(worker_factory.createWorker(server.overloadManager()));
  }
}</pre>
<div class="blog_h3"><span class="graybg">MainImpl::initialize</span></div>
<p>在主配置初始化阶段，静态监听器被创建：</p>
<pre class="crayon-plain-tag">const auto&amp; listeners = bootstrap.static_resources().listeners();
for (ssize_t i = 0; i &lt; listeners.size(); i++) {
  server.listenerManager().addOrUpdateListener(listeners[i], "", false);
}</pre>
<div class="blog_h3"><span class="graybg">ListenerManagerImpl::addOrUpdateListener</span></div>
<p>MainImpl::initialize将监听器创建工作委托给监听器管理器完成：</p>
<pre class="crayon-plain-tag">bool ListenerManagerImpl::addOrUpdateListener(const envoy::api::v2::Listener&amp; config,
                                              const std::string&amp; version_info, bool modifiable) {
  // 如果监听器没有配置名称，随机生成
  std::string name;
  if (!config.name().empty()) {
    name = config.name();
  } else {
    name = server_.random().uuid();
  }
  const uint64_t hash = MessageUtil::hash(config);
  ENVOY_LOG(debug, "begin add/update listener: name={} hash={}", name, hash);

  auto existing_active_listener = getListenerByName(active_listeners_, name);
  auto existing_warming_listener = getListenerByName(warming_listeners_, name);

  if ((existing_warming_listener != warming_listeners_.end() &amp;&amp;
       (*existing_warming_listener)-&gt;blockUpdate(hash)) ||
      (existing_active_listener != active_listeners_.end() &amp;&amp;
       (*existing_active_listener)-&gt;blockUpdate(hash))) {
    // 重复或者被锁定的监听器，不得更新
    return false;
  }

  // 实例化监听器对象
  ListenerImplPtr new_listener(
      new ListenerImpl(config, version_info, *this, name, modifiable, workers_started_, hash));
  ListenerImpl&amp; new_listener_ref = *new_listener;

  // 强制要求相同名字的监听器配置必须具有相同的IP地址，以避免更新期间出现冲突，兵却让我们可以使用相同的绑定地址
  // 需要注意，如果绑定到端口0（让内核自由选择端口），新监听器会使用对应的旧监听器所监听的端口
  if ((existing_warming_listener != warming_listeners_.end() &amp;&amp;
       *(*existing_warming_listener)-&gt;address() != *new_listener-&gt;address()) ||
      (existing_active_listener != active_listeners_.end() &amp;&amp;
       *(*existing_active_listener)-&gt;address() != *new_listener-&gt;address())) {
    throw EnvoyException("监听地址发生变化，不允许");
  }

  bool added = false;
  if (existing_warming_listener != warming_listeners_.end()) {
    // 现有监听器在预热阶段，获取现有监听器的套接字，然后替换原先的监听器
    new_listener-&gt;setSocket((*existing_warming_listener)-&gt;getSocket());
    *existing_warming_listener = std::move(new_listener);
  } else if (existing_active_listener != active_listeners_.end()) {
    // 现有监听器是激活的，工作线程是否已经启动，影响处理方式
    // 但是不管工作线程是否启动，都需要从现有监听器将套接字拿过来
    new_listener-&gt;setSocket((*existing_active_listener)-&gt;getSocket());
    if (workers_started_) {
      // 工作线程已经启动，加入到预热列表
      warming_listeners_.emplace_back(std::move(new_listener));
    } else {
      // 工作线程尚未启动，替换激活列表中现有监听器
      *existing_active_listener = std::move(new_listener);
    }
  } else {
    if (!new_listener-&gt;bindToPort() &amp;&amp;
        (hasListenerWithAddress(warming_listeners_, *new_listener-&gt;address()) ||
         hasListenerWithAddress(active_listeners_, *new_listener-&gt;address()))) {
      const std::string message =
      throw EnvoyException("无法添加，现有监听器的地址和当前监听器的重复");
    }

    // 新添加的监听器，工作线程是否已经启动影响处理方式

    // 查找是否存在draining的监听器绑定到相同地址
    Network::SocketSharedPtr draining_listener_socket;
    auto existing_draining_listener = std::find_if(
        draining_listeners_.cbegin(), draining_listeners_.cend(),
        [&amp;new_listener](const DrainingListener&amp; listener) {
          return *new_listener-&gt;address() == *listener.listener_-&gt;socket().localAddress();
        });
    if (existing_draining_listener != draining_listeners_.cend()) {
      // Draining监听器已经监听了我们的套接字，这是一个边缘情况（Edge case）
      // 发生的原因可能是监听器移除，然后由很快被添加回来（使用相同的地址，相同或不同的名称）
      draining_listener_socket = existing_draining_listener-&gt;listener_-&gt;getSocket();
    }

    // 为新监听器设置套接字，使用draining的
    new_listener-&gt;setSocket(draining_listener_socket
                                ? draining_listener_socket
                                // 或者创建新的
                                : factory_.createListenSocket(new_listener-&gt;address(),
                                                              new_listener-&gt;listenSocketOptions(),
                                                              new_listener-&gt;bindToPort()));
    if (workers_started_) {
      // 如果工作线程已经启动，作为预热监听器添加
      warming_listeners_.emplace_back(std::move(new_listener));
    } else {
      // 否则作为激活监听器添加
      active_listeners_.emplace_back(std::move(new_listener));
    }

    added = true;
  }
  
  // 执行监听器的初始化
  new_listener_ref.initialize();
  return true;
}</pre>
<div class="blog_h3"><span class="graybg">ListenerImpl</span></div>
<p>如果需要创建新的监听器，则监听器管理器会调用下面的构造函数：</p>
<pre class="crayon-plain-tag">ListenerImpl::ListenerImpl(const envoy::api::v2::Listener&amp; config, const std::string&amp; version_info,
                           ListenerManagerImpl&amp; parent, const std::string&amp; name, bool modifiable,
                           bool workers_started, uint64_t hash)
      // 监听器管理器    监听地址
    : parent_(parent), address_(Network::Address::resolveProtoAddress(config.address())),
      // 全局的监控指标scope
      global_scope_(parent_.server_.stats().createScope("")),
      // 监听器的监控指标scope
      listener_scope_(
          parent_.server_.stats().createScope(fmt::format("listener.{}.", address_-&gt;asString()))),
      // 是否需要绑定到端口
      bind_to_port_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(config.deprecated_v1(), bind_to_port, true)),
      // 是否直接转发给原始目的地对应的监听器
      hand_off_restored_destination_connections_(
          PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, use_original_dst, false)),
      // 每个连接的缓冲区限额
      per_connection_buffer_limit_bytes_(
          PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, per_connection_buffer_limit_bytes, 1024 * 1024)),
      // 监听器Tag，用于连接处理器跟踪
      listener_tag_(parent_.factory_.nextListenerTag()), name_(name),
      reverse_write_filter_order_(
          PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, bugfix_reverse_write_filter_order, true)),
      // 监听器是否可修改或删除    
                               // 监听器是在工作线程启动前，还是启动后添加的
                                                                  // 查重哈希
      modifiable_(modifiable), workers_started_(workers_started), hash_(hash),
      // Drain管理器，负责处理连接的draining
      local_drain_manager_(parent.factory_.createDrainManager(config.drain_type())),
      // 配置           配置版本信息
      config_(config), version_info_(version_info),
      // 过滤器处理超时
      listener_filters_timeout_(
          PROTOBUF_GET_MS_OR_DEFAULT(config, listener_filters_timeout, 15000)) {
  // 允许侦听任意地址
  if (config.has_transparent()) {
    addListenSocketOptions(Network::SocketOptionFactory::buildIpTransparentOptions());
  }
  // 允许绑定到非本地的，以及目前不存在的地址
  if (config.has_freebind()) {
    addListenSocketOptions(Network::SocketOptionFactory::buildIpFreebindOptions());
  }
  // TFO，简化握手，提高连接打开速度
  if (config.has_tcp_fast_open_queue_length()) {
    addListenSocketOptions(Network::SocketOptionFactory::buildTcpFastOpenOptions(
        config.tcp_fast_open_queue_length().value()));
  }

  // 其它套接字选项
  if (config.socket_options().size() &gt; 0) {
    addListenSocketOptions(
        Network::SocketOptionFactory::buildLiteralOptions(config.socket_options()));
  }

  // 如果监听器过滤器不为空，则创建监听器过滤器工厂（的列表）
  if (!config.listener_filters().empty()) {
    listener_filter_factories_ =
        parent_.factory_.createListenerFilterFactoryList(config.listener_filters(), *this);
  }
  // 如果设置了use_original_dst标记，强制添加original dst这个监听器过滤器
  if (PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, use_original_dst, false)) {
    auto&amp; factory =
        Config::Utility::getAndCheckFactory&lt;Configuration::NamedListenerFilterConfigFactory&gt;(
            Extensions::ListenerFilters::ListenerFilterNames::get().OriginalDst);
    listener_filter_factories_.push_back(
        factory.createFilterFactoryFromProto(Envoy::ProtobufWkt::Empty(), *this));
  }
  // 如果设置了use_proxy_proto标记，强制添加代理协议监听器
  // 此监听器位于监听器过滤器链的尾部
  if (PROTOBUF_GET_WRAPPED_OR_DEFAULT(config.filter_chains()[0], use_proxy_proto, false)) {
    auto&amp; factory =
        Config::Utility::getAndCheckFactory&lt;Configuration::NamedListenerFilterConfigFactory&gt;(
            Extensions::ListenerFilters::ListenerFilterNames::get().ProxyProtocol);
    listener_filter_factories_.push_back(
        factory.createFilterFactoryFromProto(Envoy::ProtobufWkt::Empty(), *this));
  }

  bool need_tls_inspector = false;
  // 存放所有过滤器链匹配
  std::unordered_set&lt;envoy::api::v2::listener::FilterChainMatch, MessageUtil, MessageUtil&gt; filter_chains;

  // 遍历过滤器链
  for (const auto&amp; filter_chain : config.filter_chains()) {
    const auto&amp; filter_chain_match = filter_chain.filter_chain_match();
    if (filter_chains.find(filter_chain_match) != filter_chains.end()) {
      throw EnvoyException("多个过滤器链使用了相同的匹配规则");
    }
    filter_chains.insert(filter_chain_match);

    // 如果没有配置传输套接字，那么，默认：
    // 1、对于普通TCP流量，使用raw_buffer
    // 2、对于TLS流量，使用基于BoringSSL的tls
    auto transport_socket = filter_chain.transport_socket();
    if (!filter_chain.has_transport_socket()) {
      if (filter_chain.has_tls_context()) {
        transport_socket.set_name(Extensions::TransportSockets::TransportSocketNames::get().Tls);
        MessageUtil::jsonConvert(filter_chain.tls_context(), *transport_socket.mutable_config());
      } else {
        transport_socket.set_name(
            Extensions::TransportSockets::TransportSocketNames::get().RawBuffer);
      }
    }

    // DownstreamTransportSocketConfigFactory，每个用于下游连接的传输套接字（例如RawBufferSocketFactory）实现此接口
    // 其createTransportSocketFactory方法返回传输套接字的工厂
    auto&amp; config_factory = Config::Utility::getAndCheckFactory&lt;
        Server::Configuration::DownstreamTransportSocketConfigFactory&gt;(transport_socket.name());
    // 转换为上述工厂的配置信息
    ProtobufTypes::MessagePtr message =
        Config::Utility::translateToFactoryConfig(transport_socket, config_factory);

    // 可以基于IP地址CIDR匹配
    std::vector&lt;std::string&gt; destination_ips;
    for (const auto&amp; destination_ip : filter_chain_match.prefix_ranges()) {
      const auto&amp; cidr_range = Network::Address::CidrRange::create(destination_ip);
      destination_ips.push_back(cidr_range.asString());
    }

    // 可以基于服务器名称（例如TLS协议的SNI）匹配
    std::vector&lt;std::string&gt; server_names(filter_chain_match.server_names().begin(),
                                          filter_chain_match.server_names().end());

    // 不支持局部通配符名称，仅仅支持*.gmem.cc，而不支持www.*.cc
    for (const auto&amp; server_name : server_names) {
      if (server_name.find('*') != std::string::npos &amp;&amp; !isWildcardServerName(server_name)) {
        throw EnvoyException();
      }
    }

    // 可以基于应用层协议匹配
    std::vector&lt;std::string&gt; application_protocols(
        filter_chain_match.application_protocols().begin(),
        filter_chain_match.application_protocols().end());

    // 传输套接字工厂上下文
    Server::Configuration::TransportSocketFactoryContextImpl factory_context(
        parent_.server_.sslContextManager(), *listener_scope_, parent_.server_.clusterManager(),
        parent_.server_.localInfo(), parent_.server_.dispatcher(), parent_.server_.random(),
        parent_.server_.stats());
    // 关联初始化管理器，此管理器作为动态密钥提供者
    factory_context.setInitManager(initManager());
    // 添加此过滤器链配置
    addFilterChain(
        PROTOBUF_GET_WRAPPED_OR_DEFAULT(filter_chain_match, destination_port, 0), destination_ips,
        server_names, filter_chain_match.transport_protocol(), application_protocols,
        filter_chain_match.source_type(),
        // 创建传输套接字工厂
        config_factory.createTransportSocketFactory(*message, factory_context, server_names),
        parent_.factory_.createNetworkFilterFactoryList(filter_chain.filters(), *this));

    need_tls_inspector |= filter_chain_match.transport_protocol() == "tls" ||
                          (filter_chain_match.transport_protocol().empty() &amp;&amp;
                           (!server_names.empty() || !application_protocols.empty()));
  }

  // 用于更快的查找
  convertDestinationIPsMapToTrie();

  // 如果需要，且没有明确配置，自动注入TLS Inspector这个监听器过滤器
  if (need_tls_inspector) {
    for (const auto&amp; filter : config.listener_filters()) {
      if (filter.name() == Extensions::ListenerFilters::ListenerFilterNames::get().TlsInspector) {
        need_tls_inspector = false;
        break;
      }
    }
    if (need_tls_inspector) {
      // 过滤器链规则依赖TLS Inspector但是没有明确配置，这里进行注入
      // 如果没有编译TLS Inspector到Envoy二进制文件中，注入会失败
      auto&amp; factory =
          Config::Utility::getAndCheckFactory&lt;Configuration::NamedListenerFilterConfigFactory&gt;(
              Extensions::ListenerFilters::ListenerFilterNames::get().TlsInspector);
      listener_filter_factories_.push_back(
          factory.createFilterFactoryFromProto(Envoy::ProtobufWkt::Empty(), *this));
    }
  }
}</pre>
<div class="blog_h3"><span class="graybg">ListenerComponentFactory::createListenSocket</span></div>
<p>监听器管理器创建新的监听器后，可能需要为其创建监听套接字，这是通过调用ListenerComponentFactory实现的：</p>
<pre class="crayon-plain-tag">Network::SocketSharedPtr ProdListenerComponentFactory::createListenSocket(Network::Address::InstanceConstSharedPtr address,
                                                 const Network::Socket::OptionsSharedPtr&amp; options, bool bind_to_port) {

  // 对于每个监听器配置，跨越所有工作线程共享单个网络监听套接字

  // 使用UNIX管道（UDS）
  if (address-&gt;type() == Network::Address::Type::Pipe) {
    const std::string addr = fmt::format("unix://{}", address-&gt;asString());
    const int fd = server_.hotRestart().duplicateParentListenSocket(addr);
    if (fd != -1) {
      return std::make_shared&lt;Network::UdsListenSocket&gt;(fd, address);
    }
    return std::make_shared&lt;Network::UdsListenSocket&gt;(address);
  }
  // 使用TCP
  const std::string addr = fmt::format("tcp://{}", address-&gt;asString());
  // 尝试通过IPC请求，从父进程获取套接字文件描述符
  const int fd = server_.hotRestart().duplicateParentListenSocket(addr);
  if (fd != -1) {
    // 获取到了
    return std::make_shared&lt;Network::TcpListenSocket&gt;(fd, address, options);
  }
  // 自行创建套接字
  return std::make_shared&lt;Network::TcpListenSocket&gt;(address, options, bind_to_port);
}

// 监听套接字构造函数
NetworkListenSocket(const Address::InstanceConstSharedPtr&amp; address,
                    const Network::Socket::OptionsSharedPtr&amp; options, bool bind_to_port)
                      // 进行系统调用，创建socket，返回其句柄
    : ListenSocketImpl(address-&gt;socket(T::type), address) {
  RELEASE_ASSERT(fd_ != -1, "");

  // 绑定前设置套接字选项
  setPrebindSocketOptions();
  // 创建底层套接字
  setupSocket(options, bind_to_port);
}
// 套接字选项：重用地址
void NetworkListenSocket&lt;NetworkSocketTrait&lt;Address::SocketType::Stream&gt;&gt;::setPrebindSocketOptions() {
  int on = 1;
  // 提供系统调用封装
  auto&amp; os_syscalls = Api::OsSysCallsSingleton::get();
  Api::SysCallIntResult status = os_syscalls.setsockopt(fd_, SOL_SOCKET, SO_REUSEADDR, &amp;on, sizeof(on));
}
// 创建底层套接字
void ListenSocketImpl::setupSocket(const Network::Socket::OptionsSharedPtr&amp; options, bool bind_to_port) {
  // 监听套接字选项
  setListenSocketOptions(options);
  if (bind_to_port) {
     // 绑定套接字到地址
     const Api::SysCallIntResult result = local_address_-&gt;bind(fd_);
  }
}</pre>
<div class="blog_h3"><span class="graybg">ListenerImpl::initialize</span></div>
<p>监听器管理创建/更新监听器后，会调用此方法：</p>
<pre class="crayon-plain-tag">void ListenerImpl::initialize() {
  last_updated_ = timeSource().systemTime();
  // 如果工作线程已经启动，则不使用全局的Init管理器，而使用局部的/每个监听器专有的初始化管理器
  if (workers_started_) {
    dynamic_init_manager_.initialize([this]() -&gt; void {
      if (!initialize_canceled_) {
        // 预热后回调
        parent_.onListenerWarmed(*this);
      }
    });
  }
} </pre>
<div class="blog_h2"><span class="graybg">工作线程</span></div>
<div class="blog_h3"><span class="graybg">创建</span></div>
<p>工作线程对象（WorkerImpl）是由监听器管理器在其构造函数中创建的。 ProdWorkerFactory::createWorker的实现如下：</p>
<pre class="crayon-plain-tag">WorkerPtr ProdWorkerFactory::createWorker(OverloadManager&amp; overload_manager) {
  // 为工作线程创建一个事件分发器
  Event::DispatcherPtr dispatcher(api_.allocateDispatcher(time_system_));
  return WorkerPtr{new WorkerImpl(
      tls_, hooks_, std::move(dispatcher),
      // ConnectionHandler用于增删改查启禁监听器
      Network::ConnectionHandlerPtr{new ConnectionHandlerImpl(ENVOY_LOGGER(), *dispatcher)},
      overload_manager, api_)};
}</pre>
<p>工作线程的构造函数如下：</p>
<pre class="crayon-plain-tag">WorkerImpl::WorkerImpl(ThreadLocal::Instance&amp; tls, TestHooks&amp; hooks,
                       Event::DispatcherPtr&amp;&amp; dispatcher, Network::ConnectionHandlerPtr handler,
                       OverloadManager&amp; overload_manager, Api::Api&amp; api)
    : tls_(tls), hooks_(hooks), dispatcher_(std::move(dispatcher)), handler_(std::move(handler)),
      api_(api) {
  // 注册线程到TLS
  tls_.registerThread(*dispatcher_, false);
  // 过载后的行为，不再接受新连接
  overload_manager.registerForAction(
      OverloadActionNames::get().StopAcceptingConnections, *dispatcher_,
      [this](OverloadActionState state) { stopAcceptingConnectionsCb(state); });
}</pre>
<p>需要注意，注册到TLS时，使用的是线程的分发器，而不是线程本身。</p>
<div class="blog_h3"><span class="graybg">启动</span></div>
<p>工作线程的启动，默认配置下，是由RunHelper注册给集群管理器的setInitializedCb回调触发的。此回调会初始化InitManager，默认配置下，注册到InitManager的唯一Target是LdsApiImpl。而InitManager的总回调就是InstanceImpl::startWorkers方法。</p>
<p>也就是说，当LDS客户端初始化完毕后，工作线程才会启动。</p>
<p>Envoy服务实例提供了启动线程的方法：</p>
<pre class="crayon-plain-tag">void InstanceImpl::startWorkers() {
  // 转调监听器管理器
  listener_manager_-&gt;startWorkers(*guard_dog_);
  // 到这里，所有监听端口已经正常运作，可以接管（热重启场景下）所有流量了
  // 通知父进程，drain掉所有它的监听器
  restarter_.drainParentListeners();
  // 下面的方法在新发动的主进程中调用，开始父进程的关闭逻辑，最终导致原来的主（父）进程终结
  drain_manager_-&gt;startParentShutdownSequence();
}</pre>
<p>监听器管理器的同名方法内容如下：</p>
<pre class="crayon-plain-tag">void ListenerManagerImpl::startWorkers(GuardDog&amp; guard_dog) {
  // 所有依赖初始化完毕，启动工作线程
  workers_started_ = true;
  for (const auto&amp; worker : workers_) {
    // 此时应该没有处于预热中的监听器
    for (const auto&amp; listener : active_listeners_) {
      // 将监听器添加到工作线程
      addListenerToWorker(*worker, *listener);
    }
    worker-&gt;start(guard_dog);
  }
}</pre>
<p>WorkerImpl::start方法就是启动一个物理线程：</p>
<pre class="crayon-plain-tag">void WorkerImpl::start(GuardDog&amp; guard_dog) {
  thread_ = api_.threadFactory().createThread([this, &amp;guard_dog]() -&gt; void { threadRoutine(guard_dog); });
}</pre>
<p>在新线程中，会执行下面的函数：</p>
<pre class="crayon-plain-tag">void WorkerImpl::threadRoutine(GuardDog&amp; guard_dog) {
  // 和主线程类似，先创建看门狗
  auto watchdog = guard_dog.createWatchDog(api_.threadFactory().currentThreadId());
  watchdog-&gt;startWatchdog(*dispatcher_);
  // 然后阻塞的运行此线程的事件循环
  dispatcher_-&gt;run(Event::Dispatcher::RunType::Block);
  // 事件循环退出，进程需要退出，关闭看门狗
  guard_dog.stopWatching(watchdog);

  // 在实际退出线程之前，必须关闭所有活动的网络连接
  // 这会阻止主线程运行任何可能引用线程本地变量的析构函数
  // 析构网络连接处理器
  handler_.reset();
  // 准备退出当前线程，所有线程本地变量将被释放
  tls_.shutdownThread();
  watchdog.reset();
}</pre>
<p>只要当线程真正启动后，它的事件循环才会运作，  ListenerManagerImpl::addListenerToWorker注册的关联监听器到工作线程的请求才会被执行。</p>
<div class="blog_h3"><span class="graybg">关联监听器</span></div>
<p>在工作线程启动之前，需要关联到监听器：</p>
<pre class="crayon-plain-tag">void ListenerManagerImpl::addListenerToWorker(Worker&amp; worker, ListenerImpl&amp; listener) {
  // 需要添加的监听器             添加成功或失败后执行的回调
  worker.addListener(listener, [this, &amp;listener](bool success) -&gt; void {
    // 这些逻辑会在工作线程上执行，不会阻塞主线程的的post调用
    server_.dispatcher().post([this, success, &amp;listener]() -&gt; void {
      // 理论上说，可能出现监听器添加到一个工作线程，却无法添加到其它工作线程的情况
      if (!success &amp;&amp; !listener.onListenerCreateFailure()) {
        // 除了记录日志以外，可能需要添加一个服务器选项，在发生这种以外时退出整个服务器
        ENVOY_LOG(critical, "listener '{}' failed to listen on address '{}' on worker", listener.name(), listener.socket().localAddress()-&gt;asString());
        stats_.listener_create_failure_.inc();
        // 移除监听器
        removeListener(listener.name());
      }
    });
  });
}

void WorkerImpl::addListener(Network::ListenerConfig&amp; listener, AddListenerCompletion completion) {
  // 监听器添加动作是在工作线程的Dispatcher.post中，异步的、在Dispatcher的事件循环上下文中执行的
  // 我们必须处理监听器无法成功添加的情况：存在一种竞态条件，两个进程可以成功绑定到一个地址，但是以EADDRINUSE listen()却失败
  dispatcher_-&gt;post([this, &amp;listener, completion]() -&gt; void {
    try {
      // 调用此线程的ConnectionHandler，添加监听器
      handler_-&gt;addListener(listener);
      // 监听器添加完成后的钩子
      hooks_.onWorkerListenerAdded();
      // 执行回调
      completion(true);
    } catch (const Network::CreateListenerException&amp; e) {
      // 执行回调
      completion(false);
    }
  });
}


void ConnectionHandlerImpl::addListener(Network::ListenerConfig&amp; config) {
  // 此连接处理器所管理的监听器对象
  ActiveListenerPtr l(new ActiveListener(*this, config));
  if (disable_listeners_) {
    l-&gt;listener_-&gt;disable();
  }
  listeners_.emplace_back(config.socket().localAddress(), std::move(l));
} </pre>
<div class="blog_h2"><span class="graybg">ADS客户端</span></div>
<p>在集群管理器初始化一节中，我们了解到，如果Bootstrap配置中包含了ADS配置，则集群管理器会创建<span style="background-color: #c0c0c0;">GrpcMuxImpl —— ADS API的客户端实现</span>，用于通过gRPC协议，在连接到管理服务器的单个流中<span style="background-color: #c0c0c0;">管理多个gRPC订阅</span>。GrpcMuxImpl支持ADS订阅的同时，也可用于单种xDS API，例如EDS。</p>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>GrpcMuxImpl的start方法包括延迟创建到管理服务器的流的逻辑：</p>
<pre class="crayon-plain-tag">void GrpcMuxImpl::start() { establishNewStream(); }</pre>
<p>集群管理器在初始化完毕所有静态集群后，会调用上述方法。 </p>
<p>GrpcMuxImpl的构造函数中也包括延迟创建到管理服务器的流的逻辑：</p>
<pre class="crayon-plain-tag">//             创建定时器
retry_timer_ = dispatcher.createTimer([this]() -&gt; void { establishNewStream(); });</pre>
<p>定时器是libevent支持的一种事件触发方式。在定时器到期后，会执行GrpcMuxImpl::establishNewStream方法：</p>
<pre class="crayon-plain-tag">void GrpcMuxImpl::establishNewStream() {
  // 异步客户端，创建gRPC双向流。将GrpcMuxImpl自身作为回调（AsyncStreamCallbacks）传入
  stream_ = async_client_-&gt;start(service_method_, *this);
  if (stream_ == nullptr) {
    handleFailure(); // 通过定时器重试
    return;
  }  
  // 设置统计指标
  control_plane_stats_.connected_state_.set(1);
  // subscriptions_为订阅列表，最初值为：type.googleapis.com/envoy.api.v2.Cluster
  for (const auto type_url : subscriptions_) {
    queueDiscoveryRequest(type_url);
  }
}</pre>
<p>queueDiscoveryRequest会调用sendDiscoveryRequest发送xDS请求：</p>
<pre class="crayon-plain-tag">void GrpcMuxImpl::queueDiscoveryRequest(const std::string&amp; type_url) {
  // 排队，然后触发所有请求处理
  request_queue_.push(type_url);
  drainRequests();
}
void GrpcMuxImpl::drainRequests() {
  while (!request_queue_.empty()) {
    if (!rate_limiting_enabled_ || limit_request_-&gt;consume()) {
      // 不限速
      // 逐个发送请求
      sendDiscoveryRequest(request_queue_.front());
      request_queue_.pop();
    } else {
      // 限速
      drain_request_timer_-&gt;enableTimer(
          std::chrono::milliseconds(limit_request_-&gt;nextTokenAvailableMs()));
      break;
    }
  }
}
void GrpcMuxImpl::sendDiscoveryRequest(const std::string&amp; type_url) {
  ...
  stream_-&gt;sendMessage(request, false);
  ...
}</pre>
<p>除了establishNewStream以外，subscribe方法也会调用queueDiscoveryRequest。<span style="background-color: #c0c0c0;">各种xDS API的客户端实现，例如CdsApiImpl，都会调用subscribe方法</span>，来订阅某种资源：</p>
<pre class="crayon-plain-tag">GrpcMuxWatchPtr GrpcMuxImpl::subscribe(const std::string&amp; type_url,
                                       const std::vector&lt;std::string&gt;&amp; resources,
                                       GrpcMuxCallbacks&amp; callbacks) {
  // GrpcMuxWatch是多路复用gRPC订阅的句柄，此句柄销毁后，订阅被取消
  auto watch = std::unique_ptr&lt;GrpcMuxWatch&gt;(new GrpcMuxWatchImpl(resources, callbacks, type_url, *this));

  // 如果这种类型的API尚未订阅，则订阅之
  if (!api_state_[type_url].subscribed_) {
    api_state_[type_url].request_.set_type_url(type_url);
    api_state_[type_url].request_.mutable_node()-&gt;MergeFrom(local_info_.node());
    api_state_[type_url].subscribed_ = true;
    subscriptions_.emplace_back(type_url);
  }
  // 发送订阅请求
  queueDiscoveryRequest(type_url);
  return watch;
} </pre>
<div class="blog_h3"><span class="graybg">处理推送</span></div>
<p>ADS客户端启动后，Istio Pilot随时可能将动态的集群信息推送过来。</p>
<p>推送信息经过一系列的处理（包括Envoy的过滤器链）后，被onReceiveMessage处理：</p>
<pre class="crayon-plain-tag">void GrpcMuxImpl::onReceiveMessage(std::unique_ptr&lt;envoy::api::v2::DiscoveryResponse&gt;&amp;&amp; message) {
  // type_url，例如 type.googleapis.com/envoy.api.v2.Cluster
  const std::string&amp; type_url = message-&gt;type_url();
  if (api_state_.count(type_url) == 0) {
    // 当前没有订阅者
    return;
  }
  if (api_state_[type_url].watches_.empty()) {
    // 更新nonce值，此值由服务器提供，标识最后一次响应
    api_state_[type_url].request_.set_response_nonce(message-&gt;nonce());
    if (message-&gt;resources().empty()) {
      // 没有订阅句柄也没有资源，可能的原因是，Envoy解除了某个资源的订阅，而且此资源也从管理服务器上删除掉了
      // 例如，一个被删除的集群，触发对ClusterLoadAssignment的unwatch，与此同时XDS服务器发送了
      // ClusterLoadAssignment的空列表过来
      // 接受此资源，但是不再发送请求，原因是没有watch了
      api_state_[type_url].request_.set_version_info(message-&gt;version_info());
    } else {
      // 没有订阅句柄（GrpcMuxWatchImpl），但是获取了资源，不应该发生
      // 发送一个NACK（不更新version_info）
      queueDiscoveryRequest(type_url);
    }
    return;
  }
  try {
    // 使用一个新的映射存储资源
    std::unordered_map&lt;std::string, ProtobufWkt::Any&gt; resources;
    GrpcMuxCallbacks&amp; callbacks = api_state_[type_url].watches_.front()-&gt;callbacks_;
    for (const auto&amp; resource : message-&gt;resources()) {
      if (type_url != resource.type_url()) {
        throw EnvoyException("type_url不匹配");
      }
      const std::string resource_name = callbacks.resourceName(resource);
      resources.emplace(resource_name, resource);
    }
    // 遍历订阅句柄
    for (auto watch : api_state_[type_url].watches_) {
       // 如果当前没有资源
      if (watch-&gt;resources_.empty()) {
        // 执行SubscriptionCallbacks.onConfigUpdate回调
        // 各资源客户端，例如CdsApiImpl，就是Config::SubscriptionCallbacks&lt;envoy::api::v2::Cluster&gt;的实现
        // 这导致转调CdsApiImpl.onConfigUpdate
        watch-&gt;callbacks_.onConfigUpdate(message-&gt;resources(), message-&gt;version_info());
        continue;
      }
      // 如果当前有资源，则针对新资源调用onConfigUpdate
      Protobuf::RepeatedPtrField&lt;ProtobufWkt::Any&gt; found_resources;
      for (auto watched_resource_name : watch-&gt;resources_) {
        auto it = resources.find(watched_resource_name);
        if (it != resources.end()) {
          found_resources.Add()-&gt;MergeFrom(it-&gt;second);
        }
      }
      watch-&gt;callbacks_.onConfigUpdate(found_resources, message-&gt;version_info());
    }
    // 更新资源版本号
    api_state_[type_url].request_.set_version_info(message-&gt;version_info());
  } catch (const EnvoyException&amp; e) {
    for (auto watch : api_state_[type_url].watches_) {
      // 调用更新失败回调
      watch-&gt;callbacks_.onConfigUpdateFailed(&amp;e);
    }
    ::google::rpc::Status* error_detail = api_state_[type_url].request_.mutable_error_detail();
    error_detail-&gt;set_code(Grpc::Status::GrpcStatus::Internal);
    error_detail-&gt;set_message(e.what());
  }
  // 更新nonce
  api_state_[type_url].request_.set_response_nonce(message-&gt;nonce());
  // 发起下一次发现请求
  queueDiscoveryRequest(type_url);
} </pre>
<p>可以看到onReceiveMessage会转调各订阅者提供的回调函数，完毕后则会触发下一次发现请求。</p>
<div class="blog_h2"><span class="graybg">CDS客户端</span></div>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>CDS客户端是在集群管理器初始化阶段创建的：</p>
<pre class="crayon-plain-tag">ClusterManagerImpl::ClusterManagerImpl(){
  if (bootstrap.dynamic_resources().has_cds_config()) {
    // ProdClusterManagerFactory::createCds
    //   CdsApiImpl::create
    cds_api_ = factory_.createCds(bootstrap.dynamic_resources().cds_config(), eds_config_, *this);
    init_helper_.setCds(cds_api_.get());
  } else {
    init_helper_.setCds(nullptr);
  }
}</pre>
<p>构造函数如下：</p>
<pre class="crayon-plain-tag">CdsApiImpl::CdsApiImpl(const envoy::api::v2::core::ConfigSource&amp; cds_config,
                       const absl::optional&lt;envoy::api::v2::core::ConfigSource&gt;&amp; eds_config,
                       ClusterManager&amp; cm, Event::Dispatcher&amp; dispatcher,
                       Runtime::RandomGenerator&amp; random, const LocalInfo::LocalInfo&amp; local_info,
                       Stats::Scope&amp; scope)
               // 创建一个stats的scope（前缀）
    : cm_(cm), scope_(scope.createScope("cluster_manager.cds.")) {
  // 检查本地节点信息
  Config::Utility::checkLocalInfo("cds", local_info);

  subscription_ =
      Config::SubscriptionFactory::subscriptionFromConfigSource&lt;envoy::api::v2::Cluster&gt;(
          cds_config, local_info, dispatcher, cm, random, *scope_,
          // 遗留的REST构造器
          [this, &amp;cds_config, &amp;eds_config, &amp;cm, &amp;dispatcher, &amp;random, &amp;local_info,
           &amp;scope]() -&gt; Config::Subscription&lt;envoy::api::v2::Cluster&gt;* {
            return new CdsSubscription(Config::Utility::generateStats(*scope_), cds_config,
                                       eds_config, cm, dispatcher, random, local_info,
                                       scope.statsOptions());
          },
          // REST方法
          "envoy.api.v2.ClusterDiscoveryService.FetchClusters",
          // GRPC方法
          "envoy.api.v2.ClusterDiscoveryService.StreamClusters");
}</pre>
<p>在每个集群初始化后都会触发的maybeFinishInitialize中，进行CDS客户端初始化：</p>
<pre class="crayon-plain-tag">void ClusterManagerInitHelper::maybeFinishInitialize() {
  ...
  if (state_ == State::WaitingForStaticInitialize &amp;&amp; cds_) {
    state_ = State::WaitingForCdsInitialize;
    cds_-&gt;initialize();
  } else {
  }
}</pre>
<p>初始化逻辑很简单，就是启动订阅：</p>
<pre class="crayon-plain-tag">void initialize() override { subscription_-&gt;start({}, *this); }</pre>
<p>CdsApiImpl.subscription_字段类型为<pre class="crayon-plain-tag">Config::Subscription&lt;envoy::api::v2::Cluster&gt;</pre>，就是上文提到的GrpcMuxImpl，CDS会订阅type.googleapis.com/envoy.api.v2.Cluster这种资源。</p>
<div class="blog_h3"><span class="graybg">处理推送 </span></div>
<p>GrpcMuxImpl::onReceiveMessage对接收到的推送进行处理后，转交给CdsApiImpl::onConfigUpdate处理：</p>
<pre class="crayon-plain-tag">void CdsApiImpl::onConfigUpdate(const ResourceVector&amp; resources, const std::string&amp; version_info) {
  // 暂定对端点资源的发现请求。在处理LDS/CDS更新时，为了避免RDS/EDS泛洪更新，调用此方法
  cm_.adsMux().pause(Config::TypeUrl::get().ClusterLoadAssignment);
  // 此方法结束后恢复端点资源的发现请求
  Cleanup eds_resume([this] { cm_.adsMux().resume(Config::TypeUrl::get().ClusterLoadAssignment); });

  // 遍历集群，获取名称，如果存在重复项，抛出异常
  std::unordered_set&lt;std::string&gt; cluster_names;
  for (const auto&amp; cluster : resources) {
    if (!cluster_names.insert(cluster.name()).second) {
      throw EnvoyException(fmt::format("duplicate cluster {} found", cluster.name()));
    }
  }
  // 进行消息验证
  for (const auto&amp; cluster : resources) {
    MessageUtil::validate(cluster);
  }
  // 跟踪哪些集群需要被移除。每次CDS更新，都把所有集群推送过来，没有在推送列表中的动态集群，需要移除
  ClusterManager::ClusterInfoMap clusters_to_remove = cm_.clusters();
  for (auto&amp; cluster : resources) {
    const std::string cluster_name = cluster.name();
    clusters_to_remove.erase(cluster_name);
    // 添加或更新集群
    if (cm_.addOrUpdateCluster(cluster, version_info)) {
      ENVOY_LOG(debug, "cds: add/update cluster '{}'", cluster_name);
    }
  }

  for (auto cluster : clusters_to_remove) {
    const std::string cluster_name = cluster.first;
    // 删除集群
    if (cm_.removeCluster(cluster_name)) {
      ENVOY_LOG(debug, "cds: remove cluster '{}'", cluster_name);
    }
  }

  version_info_ = version_info;
  runInitializeCallbackIfAny();
}</pre>
<p>addOrUpdateCluster负责添加或更新集群：</p>
<pre class="crayon-plain-tag">bool ClusterManagerImpl::addOrUpdateCluster(const envoy::api::v2::Cluster&amp; cluster,
                                            const std::string&amp; version_info) {
  // 首先判断这个集群是新的，还是对既有动态集群的更新
  // 同时检查预热中的集群列表、活动集群列表，确定是否需要执行更新，或者更新需要被阻止。阻止条件
  //   1、静态配置的集群不允许在主配置（相对Bootstrap配置）中更新
  //   2、或，配置哈希没变化
  const std::string cluster_name = cluster.name();
  const auto existing_active_cluster = active_clusters_.find(cluster_name);
  const auto existing_warming_cluster = warming_clusters_.find(cluster_name);
  const uint64_t new_hash = MessageUtil::hash(cluster);
  if ((existing_active_cluster != active_clusters_.end() &amp;&amp;
       existing_active_cluster-&gt;second-&gt;blockUpdate(new_hash)) ||
      (existing_warming_cluster != warming_clusters_.end() &amp;&amp;
       existing_warming_cluster-&gt;second-&gt;blockUpdate(new_hash))) {
    return false;
  }

  if (existing_active_cluster != active_clusters_.end() ||
      existing_warming_cluster != warming_clusters_.end()) {
    // 已经初始化的情况下，下面的调用没有作用。其本意是从对应待初始化列表中移除集群
    init_helper_.removeCluster(*existing_active_cluster-&gt;second-&gt;cluster_);
    cm_stats_.cluster_modified_.inc();
  } else {
    cm_stats_.cluster_added_.inc();
  }

  // 添加/修改集群的时机不同，则逻辑完全不同：
  // 1、在Envoy服务初始化时期，使用Init管理器来处理和主/次集群、静态/CDS集群、预热所有集群相关的复杂逻辑
  // 2、在初始化之后，为每个集群独立的处理预热
  // 
  // 注意：将所有预热逻辑集中到Init管理器中是可能的，但是为了不让Init管理器更加复杂，这里独立处理了。未来可能进行重构
  // 统一处理
 
  // 所有集群初始化完毕之后use_active_map=true
  const bool use_active_map = init_helper_.state() != ClusterManagerInitHelper::State::AllClustersInitialized;
  // 加入到活动or预热ClusterMap中
  loadCluster(cluster, version_info, true, use_active_map ? active_clusters_ : warming_clusters_);

  if (use_active_map) {
    auto&amp; cluster_entry = active_clusters_.at(cluster_name);
    // 更新各线程本地信息
    createOrUpdateThreadLocalCluster(*cluster_entry);
    // 调用集群初始化助手添加集群
    init_helper_.addCluster(*cluster_entry-&gt;cluster_);
  } else {
    auto&amp; cluster_entry = warming_clusters_.at(cluster_name);
    // 初始化集群，并在回调中预热集群
    cluster_entry-&gt;cluster_-&gt;initialize([this, cluster_name] {
      auto warming_it = warming_clusters_.find(cluster_name);
      auto&amp; cluster_entry = *warming_it-&gt;second;

      updates_map_.erase(cluster_name);
      active_clusters_[cluster_name] = std::move(warming_it-&gt;second);
      warming_clusters_.erase(warming_it);
      
      // 预热完成
      createOrUpdateThreadLocalCluster(cluster_entry);
      // 调用ClusterManagerImpl::onClusterInit
      // 此时集群还没有设置以支持跨线程更新，原因是避免在初始化阶段进行不必要的更新
      // 如果必要，该方法会首先初始化线程感知的负载均衡器
      onClusterInit(*cluster_entry.cluster_);
      updateGauges();
    });
  }

  updateGauges();
  return true;
}</pre>
<p>addOrUpdateCluster可能会调用ClusterManagerInitHelper::addCluster，就是集群管理器初始化，针对静态配置的集群调用的方法。但是这次走针对次集群的分支。</p>
<div class="blog_h2"><span class="graybg">EDS客户端</span></div>
<p>EdsClusterImpl是一种集群实现，它基于EDS协议发现自己的主机（端点，对应Envoy API是ClusterLoadAssignment.endpoints_）。</p>
<p>EdsClusterImpl同时充当了EDS客户端的角色。CDS客户端是单例的，而<span style="background-color: #c0c0c0;">每个EDS集群都是EDS客户端，它们各自独立向ADS服务器发起订阅</span>。</p>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>Eds集群本身可能就是通过CDS订阅获得的，在EdsClusterImpl的构造函数中，它会创建订阅：</p>
<pre class="crayon-plain-tag">EdsClusterImpl::EdsClusterImpl(
    // 集群配置                              运行时加载器
    const envoy::api::v2::Cluster&amp; cluster, Runtime::Loader&amp; runtime,
    Server::Configuration::TransportSocketFactoryContext&amp; factory_context,
    // 监控指标scope                提示是否通过xDS添加了此集群
    Stats::ScopePtr&amp;&amp; stats_scope, bool added_via_api)
    // 父对象
    : BaseDynamicClusterImpl(cluster, runtime, factory_context, std::move(stats_scope), added_via_api),
      //  集群管理器                           本地信息
      cm_(factory_context.clusterManager()), local_info_(factory_context.localInfo()),
      // 获取集群名称
      cluster_name_(cluster.eds_cluster_config().service_name().empty()
                        ? cluster.name()
                        : cluster.eds_cluster_config().service_name()) {
  // 检查本地信息
  Config::Utility::checkLocalInfo("eds", local_info_);
  // 获取EDS配合自
  const auto&amp; eds_config = cluster.eds_cluster_config().eds_config();
  Event::Dispatcher&amp; dispatcher = factory_context.dispatcher();
  Runtime::RandomGenerator&amp; random = factory_context.random();
  Upstream::ClusterManager&amp; cm = factory_context.clusterManager();
  // 创建ClusterLoadAssignment订阅
  subscription_ = Config::SubscriptionFactory::subscriptionFromConfigSource&lt;envoy::api::v2::ClusterLoadAssignment&gt;(
      // 配置源envoy::api::v2::core::ConfigSource
      eds_config, local_info_, dispatcher, cm, random, info_-&gt;statsScope(),
      // 遗留的v1 REST订阅构造器
      [this, &amp;eds_config, &amp;cm, &amp;dispatcher,
       &amp;random]() -&gt; Config::Subscription&lt;envoy::api::v2::ClusterLoadAssignment&gt;* {
        return new SdsSubscription(info_-&gt;stats(), eds_config, cm, dispatcher, random);
      },
      // REST订阅方法
      "envoy.api.v2.EndpointDiscoveryService.FetchEndpoints",
      // GRPC订阅方法
      "envoy.api.v2.EndpointDiscoveryService.StreamEndpoints");
}</pre>
<p>在EDS集群的preInit阶段，会启动订阅，<span style="background-color: #c0c0c0;">只会订阅当前集群的端点</span>：</p>
<pre class="crayon-plain-tag">// subscription_的start方法支持传入一个vector，提供需要抓取的资源的名称
void EdsClusterImpl::startPreInit() { subscription_-&gt;start({cluster_name_}, *this); }</pre>
<div class="blog_h3"><span class="graybg">处理推送</span></div>
<p>GrpcMuxImpl::onReceiveMessage对接收到的推送进行处理后，转交给EdsClusterImpl::onConfigUpdate处理： </p>
<pre class="crayon-plain-tag">void EdsClusterImpl::onConfigUpdate(const ResourceVector&amp; resources, const std::string&amp;) {
  if (resources.empty()) {
    // 当前集群没有ClusterLoadAssignment
    info_-&gt;stats().update_empty_.inc();
    // 仍然需要进行预初始化
    onPreInitComplete();
    return;
  }
  // EDS资源长度必须为1
  if (resources.size() != 1) {
    throw EnvoyException(fmt::format("Unexpected EDS resource length: {}", resources.size()));
  }
  // ClusterLoadAssignme
  const auto&amp; cluster_load_assignment = resources[0];
  // 消息合法性验证
  MessageUtil::validate(cluster_load_assignment);
  // 被更新主机列表
  std::unordered_map&lt;std::string, HostSharedPtr&gt; updated_hosts;
  // 负责管理一个集群的PriorityState，PriorityState持有每个优先级对应的 主机集+对应的位置权重图
  PriorityStateManager priority_state_manager(*this, local_info_);
  // locality_lb_endpoint不是一个端点，而是一个位置（例如可用性区域）中所有端点的集合，
  // 它具有权重（相对于同优先级的其它locality）、优先级等属性
  for (const auto&amp; locality_lb_endpoint : cluster_load_assignment.endpoints()) {
    const uint32_t priority = locality_lb_endpoint.priority();
    if (priority &gt; 0 &amp;&amp; !cluster_name_.empty() &amp;&amp; cluster_name_ == cm_.localClusterName()) {
      // 对于本地集群，端点路由优先级必须是0
      throw EnvoyException(fmt::format("Unexpected non-zero priority for local cluster '{}'.", cluster_name_));
    }
    // 放入 PriorityState 也就是 std::vector&lt;std::pair&lt;HostListPtr, LocalityWeightsMap&gt;&gt; 中
    // vector是以优先级为索引的列表，HostListPtr是Host列表，LocalityWeightsMap是Locality到Weight的映射
    priority_state_manager.initializePriorityFor(locality_lb_endpoint);

    for (const auto&amp; lb_endpoint : locality_lb_endpoint.lb_endpoints()) {
      // 遍历端点列表
      // 先创建HostImpl，然后存放到PriorityState中
      priority_state_manager.registerHostForPriority(
          "", resolveProtoAddress(lb_endpoint.endpoint().address()), locality_lb_endpoint,
          // 设置主机状态为不健康
          lb_endpoint, Host::HealthFlag::FAILED_EDS_HEALTH);
    }
  }

  // 跟踪是否重建负载均衡结构
  bool cluster_rebuilt = false;

  const uint32_t overprovisioning_factor = PROTOBUF_GET_WRAPPED_OR_DEFAULT(
      cluster_load_assignment.policy(), overprovisioning_factor, kDefaultOverProvisioningFactor);

  // 遍历新配置中所有优先级
  auto&amp; priority_state = priority_state_manager.priorityState();
  for (size_t i = 0; i &lt; priority_state.size(); ++i) {
    if (priority_state[i].first != nullptr) {
      if (locality_weights_map_.size() &lt;= i) {
        locality_weights_map_.resize(i + 1);
      }
      // 为某个优先级的所有locality更新主机列表
      cluster_rebuilt |= updateHostsPerLocality(
          i, overprovisioning_factor, *priority_state[i].first, locality_weights_map_[i],
          priority_state[i].second, priority_state_manager, updated_hosts);
    }
  }

  // 遍历所有不在配置中的优先级
  for (size_t i = priority_state.size(); i &lt; priority_set_.hostSetsPerPriority().size(); ++i) {
    const HostVector empty_hosts;
    LocalityWeightsMap empty_locality_map;

    if (locality_weights_map_.size() &lt;= i) {
      locality_weights_map_.resize(i + 1);
    }
    cluster_rebuilt |=
        updateHostsPerLocality(i, overprovisioning_factor, empty_hosts, locality_weights_map_[i],
                               empty_locality_map, priority_state_manager, updated_hosts);
  }

  all_hosts_ = std::move(updated_hosts);

  if (!cluster_rebuilt) {
    info_-&gt;stats().update_no_rebuild_.inc();
  }

  // Preinit完成回调
  onPreInitComplete();
}</pre>
<div class="blog_h2"><span class="graybg">LDS客户端</span></div>
<p>LDS客户端是作为InitManager的target之一，进行初始化的。InitManager会先初始化它的所有目标，最后初始化它自己。</p>
<p>InitManager会在集群管理器初始化完毕——所有集群都添加之后被调用。</p>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>如果配置了动态LDS资源，则在InstanceImpl的初始化过程中，会创建LDS客户端：</p>
<pre class="crayon-plain-tag">if (bootstrap_.dynamic_resources().has_lds_config()) {
  listener_manager_-&gt;createLdsApi(bootstrap_.dynamic_resources().lds_config());
}

void ListenerManagerImpl::createLdsApi(const envoy::api::v2::core::ConfigSource&amp; lds_config) override {
  ASSERT(lds_api_ == nullptr);
  lds_api_ = factory_.createLdsApi(lds_config);
}

LdsApiPtr ProdListenerComponentFactory::createLdsApi(const envoy::api::v2::core::ConfigSource&amp; lds_config) override {
  return std::make_unique&lt;LdsApiImpl&gt;(
      lds_config, server_.clusterManager(), server_.dispatcher(), server_.random(),
      server_.initManager(), server_.localInfo(), server_.stats(), server_.listenerManager());
}

LdsApiImpl::LdsApiImpl(const envoy::api::v2::core::ConfigSource&amp; lds_config,
                       Upstream::ClusterManager&amp; cm, Event::Dispatcher&amp; dispatcher,
                       Runtime::RandomGenerator&amp; random, Init::Manager&amp; init_manager,
                       const LocalInfo::LocalInfo&amp; local_info, Stats::Scope&amp; scope,
                       ListenerManager&amp; lm)
    : listener_manager_(lm), scope_(scope.createScope("listener_manager.lds.")), cm_(cm) {
  // std::unique_ptr&lt;Config::Subscription&lt;envoy::api::v2::Listener&gt;&gt; subscription_;
  // 订阅，获得一个GrpcMuxSubscriptionImpl对象
  subscription_ =
      Envoy::Config::SubscriptionFactory::subscriptionFromConfigSource&lt;envoy::api::v2::Listener&gt;(
          lds_config, local_info, dispatcher, cm, random, *scope_,
          [this, &amp;lds_config, &amp;cm, &amp;dispatcher, &amp;random, &amp;local_info,
           &amp;scope]() -&gt; Config::Subscription&lt;envoy::api::v2::Listener&gt;* {
            return new LdsSubscription(Config::Utility::generateStats(*scope_), lds_config, cm,
                                       dispatcher, random, local_info, scope.statsOptions());
          },
          "envoy.api.v2.ListenerDiscoveryService.FetchListeners",
          "envoy.api.v2.ListenerDiscoveryService.StreamListeners");
  Config::Utility::checkLocalInfo("lds", local_info);
  // 向初始化管理器注册自己
  init_manager.registerTarget(*this);
}</pre>
<p>初始化管理器会调用：</p>
<pre class="crayon-plain-tag">void LdsApiImpl::initialize(std::function&lt;void()&gt; callback) {
  initialize_callback_ = callback;
  subscription_-&gt;start({}, *this);
}</pre>
<p>导致LDS订阅启动。 </p>
<div class="blog_h3"><span class="graybg">处理推送</span></div>
<p>GrpcMuxImpl::onReceiveMessage对接收到的推送进行处理后，转交给LdsApiImpl::onConfigUpdate处理：</p>
<pre class="crayon-plain-tag">void LdsApiImpl::onConfigUpdate(const ResourceVector&amp; resources, const std::string&amp; version_info) {
  // 暂停RDS订阅并在此函数执行完毕后恢复
  cm_.adsMux().pause(Config::TypeUrl::get().RouteConfiguration);
  Cleanup rds_resume([this] { cm_.adsMux().resume(Config::TypeUrl::get().RouteConfiguration); });

  // 返回结果中包含重复的监听器，不允许
  std::unordered_set&lt;std::string&gt; listener_names;
  for (const auto&amp; listener : resources) {
    if (!listener_names.insert(listener.name()).second) {
      throw EnvoyException(fmt::format("duplicate listener {} found", listener.name()));
    }
  }
  // 验证每个消息的合法性
  for (const auto&amp; listener : resources) {
    MessageUtil::validate(listener);
  }
  // 跟踪需要移除的监听器
  std::unordered_map&lt;std::string, std::reference_wrapper&lt;Network::ListenerConfig&gt;&gt; listeners_to_remove;

  // 将所有现存监听器存放到一个映射中
  for (const auto&amp; listener : listener_manager_.listeners()) {
    listeners_to_remove.emplace(listener.get().name(), listener);
  }
  // 对于存在于本次订阅结果中的监听器，不被移除
  for (const auto&amp; listener : resources) {
    listeners_to_remove.erase(listener.name());
  }

  // 不存在的监听器，移除。必须全量推送？
  for (const auto&amp; listener : listeners_to_remove) {
    if (listener_manager_.removeListener(listener.first)) {
    }
  }

  // 添加或更新监听器，逻辑类似于静态监听器
  for (const auto&amp; listener : resources) {
    const std::string listener_name = listener.name();
    try {
      // 如果新旧配置完全一样，添加会失败
      if (listener_manager_.addOrUpdateListener(listener, version_info, true)) {
        ENVOY_LOG(info, "lds: add/update listener '{}'", listener_name);
      } else {
        ENVOY_LOG(debug, "lds: add/update listener '{}' skipped", listener_name);
      }
    } catch (const EnvoyException&amp; e) {
      throw EnvoyException(fmt::format("Error adding/updating listener {}: {}", listener_name, e.what()));
    }
  }

  version_info_ = version_info;
  // LDS的初始化回调尚未调用，调用之
  runInitializeCallbackIfAny();
}</pre>
<div class="blog_h1"><span class="graybg">总结</span></div>
<div class="blog_h2"><span class="graybg">Pilot Discovery</span></div>
<p>该组件一方面实现了完整的xDS协议管理服务器端，一方面对接底层基础设施（K8S），从中获取各种各样的资源。</p>
<p>Discovery关注的K8S资源类型包括：</p>
<ol>
<li>Ingress：需要此资源的原因是，Istio可以作为Ingress Controller</li>
<li>Istio CRD，Istio需要：
<ol>
<li>VirtualService、DestinationRule来构建Envoy的Cluster配置、Listener路由规则</li>
<li>Gateway来为作为网关的Envoy代理（ingressgateway、egressgateway，或者任Pod）提供监听器，并由绑定到网关的VirtualService提供路由表</li>
<li>ServiceEntry来创建一部分Istio服务，进而构建Envoy的Cluster配置、Listener配置</li>
</ol>
</li>
<li>Service：Istio需要这种资源创建Istio服务，进而构建Envoy的Cluster配置、Listener配置</li>
<li>Pod：Istio需要这种资源为EDS集群更新工作负载</li>
</ol>
<p>当Discovery发现上述某种资源变更后，会准备好一个推送上下文，并且依据此上下文生成所需的Envoy配置，推送给Envoy代理。</p>
<p>推送服务由DiscoveryService负责，它转调DiscoveryServer。后者提供了Envoy v2 xDS API的gRPC客户端实现，其逻辑在ads.go、cds.go、eds.go、lds.go、rds.go等多个源文件中实现，对应了xDS API的不同部分。</p>
<p>所有连接到Discovery的、发起了订阅请求的代理，都会接收到推送。</p>
<div class="blog_h2"><span class="graybg">Pilot Proxy</span></div>
<p>该组件包装了Envoy代理，主要实现以下功能：</p>
<ol>
<li>从模板生成Bootstrap配置</li>
<li>守护Envoy代理，如果宕机重新启动</li>
<li>当Bootstrap配置文件更新后，重现配置Envoy代理</li>
<li> 更新数字证书</li>
</ol>
<p>Proxy有多种运行模式：sidecar、ingress、proxy，分别用作普通Pod的边车、Ingress控制器、入口/出口网关。</p>
<div class="blog_h2"><span class="graybg">Envoy</span></div>
<p>该组件实现了完整的xDS协议客户端。和Pilot Discovery的交互主要由GrpcMuxImpl负责，它是基于gRPC的ADS实现。</p>
<p>除了通过Bootstrap配置加载的静态资源，其它资源都需要通过xDS协议，向Pilot Discovery订阅以获取。GrpcMuxImpl获得资源推送后，会调用CDS、EDS、LDS、RDS等API客户端：</p>
<ol>
<li>CDS，动态加载集群，可能创建EDS</li>
<li>EDS，为EDS集群更新端点</li>
<li>LDS，动态加载监听器，可能触发工作线程的启动</li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Bazel学习笔记</title>
		<link>https://blog.gmem.cc/bazel-study-note</link>
		<comments>https://blog.gmem.cc/bazel-study-note#comments</comments>
		<pubDate>Mon, 07 Jan 2019 08:46:20 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Java]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=24313</guid>
		<description><![CDATA[<p>简介 Bazel是Google开源的，类似于Make、Maven或Gradle的构建和测试工具。它使用可读性强的、高层次的构建语言，支持多种编程语言，以及为多种平台进行交叉编译。 Bazel的优势： 高层次的构建语言：更加简单，Bazel抽象出库、二进制、脚本、数据集等概念，不需要编写调用编译器或链接器的脚本 快而可靠：能够缓存所有已经完成的工作步骤，并且跟踪文件内容、构建命令的变动情况，避免重复构建。此外Bazel还支持高度并行构建、增量构建 多平台支持：可以在Linux/macOS/Windows上运行，可以构建在桌面/服务器/移动设备上运行的应用程序 可扩容性：处理10万以上源码文件时仍然能保持速度 可扩展性：支持Android、C/C++、Java、Objective-C、Protocol Buffer、Python…还支持扩展以支持其它语言 如何工作 当运行构建或者测试时，Bazel会： 加载和目标相关的BUILD文件 分析输入及其依赖，应用指定的构建规则，产生一个Action图。这个图表示需要构建的目标、目标之间的关系，以及为了构建目标需要执行的动作。Bazel依据此图来跟踪文件变动，并确定哪些目标需要重新构建 针对输入执行构建动作，直到最终的构建输出产生出来 如何使用 当你需要构建或者测试一个项目时，通常执行以下步骤： 下载并安装Bazel 创建一个工作空间。Bazel从此工作空间寻找构建输入和BUILD文件，同时也将构建输出存放在（指向）工作空间（的符号链接中） 编写BUILD文件，以及可选的WORKSPACE文件，告知Bazel需要构建什么，如何构建。此文件基于Starlark这种DSL 从命令行调用Bazel命令，构建、测试或者运行项目 概念和术语 Workspace <a class="read-more" href="https://blog.gmem.cc/bazel-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/bazel-study-note">Bazel学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Bazel是Google开源的，类似于Make、Maven或Gradle的构建和测试工具。它使用可读性强的、高层次的构建语言，支持多种编程语言，以及为多种平台进行交叉编译。</p>
<p>Bazel的优势：</p>
<ol>
<li>高层次的构建语言：更加简单，Bazel抽象出<span style="background-color: #c0c0c0;">库、二进制、脚本、数据集等</span>概念，不需要编写调用编译器或链接器的脚本</li>
<li>快而可靠：能够缓存所有已经完成的工作步骤，并且跟踪文件内容、构建命令的变动情况，避免重复构建。此外Bazel还支持高度<span style="background-color: #c0c0c0;">并行构建、增量构建</span></li>
<li>多平台支持：可以在Linux/macOS/Windows上运行，可以构建在桌面/服务器/移动设备上运行的应用程序</li>
<li>可扩容性：处理10万以上源码文件时仍然能保持速度</li>
<li>可扩展性：支持Android、C/C++、Java、Objective-C、Protocol Buffer、Python…还支持扩展以支持其它语言</li>
</ol>
<div class="blog_h2"><span class="graybg">如何工作</span></div>
<p>当运行构建或者测试时，Bazel会：</p>
<ol>
<li>加载和目标相关的<span style="background-color: #c0c0c0;">BUILD</span>文件</li>
<li>分析输入及其依赖，应用指定的<span style="background-color: #c0c0c0;">构建规则</span>，产生一个Action图。这个图表示需要构建的目标、目标之间的关系，以及为了构建目标需要执行的动作。Bazel依据此图来跟踪文件变动，并确定哪些目标需要重新构建</li>
<li>针对输入执行构建动作，直到最终的构建输出产生出来</li>
</ol>
<div class="blog_h2"><span class="graybg">如何使用</span></div>
<p>当你需要构建或者测试一个项目时，通常执行以下步骤：</p>
<ol>
<li>下载并安装Bazel</li>
<li>创建一个工作空间。Bazel从此工作空间寻找构建输入和BUILD文件，同时也将构建输出存放在（指向）工作空间（的符号链接中）</li>
<li>编写BUILD文件，以及可选的WORKSPACE文件，告知Bazel需要构建什么，如何构建。此文件基于<span style="background-color: #c0c0c0;">Starlark这种DSL</span></li>
<li>从命令行调用Bazel命令，构建、测试或者运行项目</li>
</ol>
<div class="blog_h2"><span class="graybg">概念和术语</span></div>
<div class="blog_h3"><span class="graybg">Workspace</span></div>
<p>工作空间是一个目录，它包含：</p>
<ol>
<li>构建目标所需要的源码文件，以及相应的BUILD文件</li>
<li>指向构建结果的符号链接</li>
<li>WORKSPACE文件，可以为空，可以包含对外部依赖的引用</li>
</ol>
<div class="blog_h3"><span class="graybg">Package</span></div>
<p>包是工作空间中主要的代码组织单元，其中包含一系列相关的文件（主要是代码）以及描述这些文件之间关系的BUILD文件</p>
<p>包是工作空间的子目录，它的<span style="background-color: #c0c0c0;">根目录必须包含文件BUILD.bazel或BUILD</span>。<span style="background-color: #c0c0c0;">除了那些具有BUILD文件的子目录——子包——</span>以外，其它子目录属于包的一部分</p>
<div class="blog_h3"><span class="graybg">Target</span></div>
<p>包是一个容器，它的元素定义在BUILD文件中，包括：</p>
<ol>
<li>规则（Rule），指定输入集和输出集之间的关系，声明从输入产生输出的必要步骤。<span style="background-color: #c0c0c0;">一个规则的输出可以是另外一个规则的输入</span></li>
<li>文件（File），可以分为两类：
<ol>
<li>源文件</li>
<li>自动生成的文件（Derived files），由构建工具依据规则生成</li>
</ol>
</li>
<li>包组：一组包，<span style="background-color: #c0c0c0;">包组用于限制特定规则的可见性</span>。包组由函数package_group定义，参数是包的列表和包组名称。你可以在规则的visibility属性中引用包组，声明那些包组可以引用当前包中的规则</li>
</ol>
<p>任何包生成的文件都属于当前包，不能为其它包生成文件。但是可以<span style="background-color: #c0c0c0;">从其它包中读取输入</span></p>
<div class="blog_h3"><span class="graybg">Label</span></div>
<p>引用一个目标时需要使用“标签”。标签的规范化表示：<pre class="crayon-plain-tag">@project//my/app/main:app_binary</pre>， 冒号前面是所属的包名，后面是目标名。如果不指定目标名，则默认以包路径最后一段作为目标名，例如：</p>
<pre class="crayon-plain-tag">//my/app
//my/app:app</pre>
<p>这两者是等价的。在BUILD文件中，引用当前包中目标时，包名部分可以省略，因此下面四种写法都可以等价：</p>
<pre class="crayon-plain-tag"># 当前包为my/app
//my/app:app
//my/app
:app
app</pre>
<p>在BUILD文件中，引用当前包中定义的<span style="background-color: #c0c0c0;">规则</span>时，<span style="background-color: #c0c0c0;">冒号不能省略</span>。引用当前包中<span style="background-color: #c0c0c0;">文件</span>时，<span style="background-color: #c0c0c0;">冒号可以省略</span>。 例如：<pre class="crayon-plain-tag">generate.cc</pre>。</p>
<p>但是，<span style="background-color: #c0c0c0;">从其它包引用时、从命令行引用时，都必须使用完整的标签</span>：<pre class="crayon-plain-tag">//my/app:generate.cc</pre></p>
<p>@project这一部分通常不需要使用，引用外部存储库中的目标时，project填写外部存储库的名字。</p>
<div class="blog_h3"><span class="graybg">Rule</span></div>
<p>规则指定<span style="background-color: #c0c0c0;">输入和输出之间的关系</span>，并且说明<span style="background-color: #c0c0c0;">产生输出的步骤</span>。</p>
<p>规则有很多类型。每个规则都具有一个名称属性，<span style="background-color: #c0c0c0;">此名称亦即目标名称</span>。对于某些规则，此名称就是产生的输出的文件名。</p>
<p>在BUILD中声明规则的语法时：</p>
<pre class="crayon-plain-tag">规则类型(
    name = "...",
    其它属性 = ...
)</pre>
<div class="blog_h3"><span class="graybg">BUILD文件</span></div>
<p>BUILD文件定义了包的所有元数据。其中的语句被<span style="background-color: #c0c0c0;">从上而下的逐条解释</span>，某些语句的顺序很重要， 例如<span style="background-color: #c0c0c0;">变量必须先定义</span>后使用，但是<span style="background-color: #c0c0c0;">规则声明的顺序无所谓</span>。</p>
<p>BUILD文件仅能包含ASCII字符，且不得声明函数、使用for/if语句，你可以在Bazel扩展——扩展名为.bzl的文件中声明函数、控制结构。并在BUILD文件中用load语句加载Bazel扩展：</p>
<pre class="crayon-plain-tag">load("//foo/bar:file.bzl", "some_library")</pre>
<p>上面的语句加载foo/bar/file.bzl并添加其中定义的符号some_libraray到当前环境中，load语句可以用来加载规则、函数、常量（字符串、列表等）。</p>
<p><span style="background-color: #c0c0c0;">load语句必须出现在顶级作用域</span>，不能出现在函数中。第一个参数说明扩展的位置，你可以为导入的符号设置别名。</p>
<p>规则的类型，一般以编程语言为前缀，例如cc，java，后缀通常有：</p>
<ol>
<li>*_binary 用于构建目标语言的可执行文件</li>
<li>*_test 用于自动化测试，其目标是可执行文件，如果测试通过应该退出0</li>
<li>*_library 用于构建目标语言的库 </li>
</ol>
<div class="blog_h3"><span class="graybg">Dependency</span></div>
<p>目标A依赖B，就意味着A在<span style="background-color: #c0c0c0;">构建或执行期间需要B</span>。所有目标的依赖关系构成非环有向图（DAG）称为依赖图。</p>
<p>距离为1的依赖称为直接依赖，大于1的依赖则称为传递性依赖。</p>
<p>依赖分为以下几种：</p>
<ol>
<li>srcs依赖：直接被当前规则消费的文件</li>
<li>deps依赖：独立编译的模块，为当前规则提供头文件、符号、库、数据</li>
<li>data依赖：不属于源码，不影响目标如何构建，但是目标在运行时可能依赖之</li>
</ol>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Bazel</span></div>
<div class="blog_h3"><span class="graybg">Ubuntu</span></div>
<p>参考下面的步骤安装Bazel：</p>
<pre class="crayon-plain-tag">echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add -

sudo apt-get update &amp;&amp; sudo apt-get install bazel</pre>
<p>可以用如下命令升级到最新版本的Bazel：</p>
<pre class="crayon-plain-tag">sudo apt-get install --only-upgrade bazel</pre>
<div class="blog_h2"><span class="graybg">Bazelisk</span></div>
<p>这是基于Go语言编写的Bazel启动器，它会为你的工作区下载最适合的Bazel，并且透明的将命令转发给该Bazel。</p>
<p>由于Bazellisk提供了和Bazel一样的接口，因此通常直接将其命名为bazel：</p>
<pre class="crayon-plain-tag">sudo wget -O /usr/local/bin/bazel https://github.com/bazelbuild/bazelisk/releases/download/v0.0.8/bazelisk-linux-amd64
sudo chmod +x /usr/local/bin/bazel </pre>
<div class="blog_h1"><span class="graybg">入门</span></div>
<div class="blog_h2"><span class="graybg">构建C++项目</span></div>
<div class="blog_h3"><span class="graybg">示例项目</span></div>
<p>执行下面的命令下载示例项目：</p>
<pre class="crayon-plain-tag">git clone https://github.com/bazelbuild/examples/</pre>
<p>你可以看到stage1、stage2、stage3这几个WORKSPACE：</p>
<pre class="crayon-plain-tag">examples
└── cpp-tutorial
    ├──stage1
    │  ├── main
    │  │   ├── BUILD
    │  │   └── hello-world.cc
    │  └── WORKSPACE
    ├──stage2
    │  ├── main
    │  │   ├── BUILD
    │  │   ├── hello-world.cc
    │  │   ├── hello-greet.cc
    │  │   └── hello-greet.h
    │  └── WORKSPACE
    └──stage3
       ├── main
       │   ├── BUILD
       │   ├── hello-world.cc
       │   ├── hello-greet.cc
       │   └── hello-greet.h
       ├── lib
       │   ├── BUILD
       │   ├── hello-time.cc
       │   └── hello-time.h
       └── WORKSPACE</pre>
<p>本节后续内容会依次使用到这三个WORKSPACE。</p>
<div class="blog_h3"><span class="graybg">通过Bazel构建</span></div>
<p>第一步是创建工作空间。工作空间中包含以下特殊文件：</p>
<ol>
<li>WORKSPACE，此文件位于根目录中，将当前目录<span style="background-color: #c0c0c0;">定义为Bazel工作空间</span></li>
<li>BUILD，告诉Bazel项目的不同部分如何构建。工作空间中<span style="background-color: #c0c0c0;">包含BUILD文件的目录称为包</span></li>
</ol>
<p>当Bazel构建项目时，<span style="background-color: #c0c0c0;">所有的输入和依赖都必须位于工作空间中</span>。除非被链接，不同工作空间的文件相互独立没有关系。</p>
<p>每个BUILD文件包含若干Bazel指令，其中最重要的指令类型是构建<span style="background-color: #c0c0c0;">规则（Build Rule）</span>，构建规则说明如何产生期望的输出——例如可执行文件或库。<span style="background-color: #c0c0c0;"> BUILD中的每个构建规则也称为目标（Target）</span>，<span style="background-color: #c0c0c0;">目标指向若干源文件和依赖，也可以指向其它目标</span>。</p>
<p>下面是stage1的BUILD文件：</p>
<pre class="crayon-plain-tag">cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
)</pre>
<p>这里定义了一个名为hello-world的目标，它使用了内置的cc_binary规则。该规则告诉Bazel，从源码hello-world.cc构建一个自包含的可执行文件。</p>
<p>执行下面的命令可以触发构建：</p>
<pre class="crayon-plain-tag">#   //main: BUILD文件相对于工作空间的位置
#          hello-world 是BUILD文件中定义的目标
bazel build //main:hello-world</pre>
<p>构建完成后，工作空间根目录会出现bazel-bin等目录，它们都是指向$HOME/.cache/bazel某个后代目录的符号链接。执行：</p>
<pre class="crayon-plain-tag">bazel-bin/main/hello-world</pre>
<p>可以运行构建好的二进制文件。</p>
<div class="blog_h3"><span class="graybg">查看依赖图</span></div>
<p>Bazel会根据BUILD中的声明产生一张依赖图，并根据这个依赖图实现精确的增量构建。</p>
<p>要查看依赖图，先安装：</p>
<pre class="crayon-plain-tag">sudo apt install graphviz xdot</pre>
<p>然后执行：</p>
<pre class="crayon-plain-tag">bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' --output graph | xdot</pre>
<div class="blog_h3"><span class="graybg">指定多个目标 </span></div>
<p>大型项目通常会划分为多个包、多个目标，以实现更快的增量构建、并行构建。工作空间stage2包含单个包、两个目标：</p>
<pre class="crayon-plain-tag"># 首先构建hello-greet库，cc_library是内建规则
cc_library(
    name = "hello-greet",
    srcs = ["hello-greet.cc"],
    # 头文件
    hdrs = ["hello-greet.h"],
)

# 然后构建hello-world二进制文件
cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
    deps = [
        # 提示Bazel，需要hello-greet才能构建当前目标
        # 依赖当前包中的hello-greet目标
        ":hello-greet",
    ],
)</pre>
<div class="blog_h3"><span class="graybg">使用多个包</span></div>
<p>工作空间stage3更进一步的划分出新的包，提供打印时间的功能：</p>
<pre class="crayon-plain-tag">cc_library(
    name = "hello-time",
    srcs = ["hello-time.cc"],
    hdrs = ["hello-time.h"],
    # 让当前目标对于工作空间的main包可见。默认情况下目标仅仅被当前包可见
    visibility = ["//main:__pkg__"],
)</pre><br />
<pre class="crayon-plain-tag">cc_library(
    name = "hello-greet",
    srcs = ["hello-greet.cc"],
    hdrs = ["hello-greet.h"],
)

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
    deps = [
        # 依赖当前包中的hello-greet目标
        ":hello-greet",
        # 依赖工作空间根目录下的lib包中的hello-time目标
        "//lib:hello-time",
    ],
)</pre>
<div class="blog_h3"><span class="graybg">如何引用目标</span></div>
<p>在BUILD文件或者命令行中，你都使用标签（Label）来引用目标，其语法为：</p>
<pre class="crayon-plain-tag">//path/to/package:target-name

# 当引用当前包中的其它目标时，可以：
//:target-name
# 当引用当前BUILD文件中其它目标时，可以：
:target-name</pre>
<div class="blog_h1"><span class="graybg">目录布局</span></div>
<pre class="crayon-plain-tag">workspace-name&gt;/                          # 工作空间根目录
  bazel-my-project =&gt; &lt;...my-project&gt;     # execRoot的符号链接，所有构建动作在此目录下执行
  bazel-out =&gt; &lt;...bin&gt;                   # outputPath的符号链接
  bazel-bin =&gt; &lt;...bin&gt;                   # 最近一次写入的二进制目录的符号链接，即$(BINDIR)
  bazel-genfiles =&gt; &lt;...genfiles&gt;         # 最近一次写入的genfiles目录的符号链接，即$(GENDIR)



/home/user/.cache/bazel/                  # outputRoot，所有工作空间的Bazel输出的根目录
  _bazel_$USER/                           # outputUserRoot，当前用户的Bazel输出的根目录
    install/
      fba9a2c87ee9589d72889caf082f1029/   # installBase，Bazel安装清单的哈希值
        _embedded_binaries/               # 第一次运行时从Bazel可执行文件的数据段解开的可执行文件或脚本
    7ffd56a6e4cb724ea575aba15733d113/     # outputBase，某个工作空间根目录的哈希值
      action_cache/                       # Action cache目录层次
      action_outs/                        # Action output目录
      command.log                         # 最近一次Bazel命令的stdout/stderr输出
      external/                           # 远程存储库被下载、链接到此目录
      server/                             # Bazel服务器将所有服务器有关的文件存放在此
        jvm.out                           # Bazel服务器的调试输出
      execroot/                           # 所有Bazel Action的工作目录
        &lt;workspace-name&gt;/                 # Bazel构建的工作树
          _bin/                           # 助手工具链接或者拷贝到此
          bazel-out/                      # outputPath，构建的实际输出目录
            local_linux-fastbuild/        # 每个独特的BuildConfiguration实例对应一个子目录
              bin/                        # 单个构建配置二进制输出目录，$(BINDIR)
                foo/bar/_objs/baz/        # 命名为//foo/bar:baz的cc_*规则的Object文件所在目录
                  foo/bar/baz1.o          # //foo/bar:baz1.cc对应的Object文件
                  other_package/other.o   # //other_package:other.cc对应的Object文件
                foo/bar/baz               # //foo/bar:baz这一cc_binary生成的构件
                foo/bar/baz.runfiles/     # //foo/bar:baz生成的二进制构件的runfiles目录
                  MANIFEST
                  &lt;workspace-name&gt;/
                    ...
              genfiles/                   # 单个构建配置生成的源文件目录，$(GENDIR)
              testlogs/                   # Bazel的内部测试运行器将日志文件存放在此
              include/                    # 按需生成的include符号链接树，符号链接bazel-include指向这里
            host/                         # 本机的BuildConfiguration
        &lt;packages&gt;/                       # 构建引用的包，对于此包来说，它就像一个正常的WORKSPACE </pre>
<div class="blog_h1"><span class="graybg">Starlark</span></div>
<p>Bazel配置文件使用Starlark（原先叫Skylark）语言，具有短小、简单、线程安全的特点。</p>
<p>这种语言的语法和Python很类似，Starlark是Python2/Python3的子集。不支持的Python特性包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">不支持的特性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>隐含字符串连接</td>
<td>需要明确使用 + 操作符</td>
</tr>
<tr>
<td>链式比较操作符</td>
<td>例如：1 &lt; x &lt; 5</td>
</tr>
<tr>
<td>class</td>
<td>使用struct函数</td>
</tr>
<tr>
<td>import</td>
<td>使用load语句</td>
</tr>
<tr>
<td>is</td>
<td>使用==代替</td>
</tr>
<tr>
<td colspan="2">以下关键字：while、yield、try、raise、except、finally 、global、nonlocal</td>
</tr>
<tr>
<td colspan="2">以下数据类型：float、set</td>
</tr>
<tr>
<td colspan="2">生成器、生成器表达式</td>
</tr>
<tr>
<td colspan="2">lambda以及嵌套函数</td>
</tr>
<tr>
<td colspan="2">绝大多数内置函数、方法</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">数据类型</span></div>
<p>Starlark支持的数据类型包括：None、bool、dict、function、int、list、string，以及两种Bazel特有的类型：depset、struct。</p>
<div class="blog_h2"><span class="graybg">代码示例</span></div>
<pre class="crayon-plain-tag"># 定义一个数字
number = 18

# 定义一个字典
people = {
    "Alice": 22,
    "Bob": 40,
    "Charlie": 55,
    "Dave": 14,
}

names = ", ".join(people.keys())

# 定义一个函数
def greet(name):
  """Return a greeting."""
  return "Hello {}!".format(name)
# 调用函数
greeting = greet(names)


def fizz_buzz(n):
  """Print Fizz Buzz numbers from 1 to n."""
  # 循环结构
  for i in range(1, n + 1):
    s = ""
    # 分支结构
    if i % 3 == 0:
      s += "Fizz"
    if i % 5 == 0:
      s += "Buzz"
    print(s if s else i)</pre>
<div class="blog_h1"><span class="graybg">变量</span></div>
<p>你可以在BUILD文件中声明和使用变量。使用变量可以减少重复的代码：</p>
<pre class="crayon-plain-tag">COPTS = ["-DVERSION=5"]

cc_library(
  name = "foo",
  copts = COPTS,
  srcs = ["foo.cc"],
)

cc_library(
  name = "bar",
  copts = COPTS,
  srcs = ["bar.cc"],
  deps = [":foo"],
)</pre>
<div class="blog_h2"><span class="graybg">跨BUILD变量</span></div>
<p>如果要声明跨越多个BUILD文件共享的变量，<span style="background-color: #c0c0c0;">必须把变量放入.bzl文件中</span>，然后通过load加载bzl文件。</p>
<div class="blog_h2"><span class="graybg">Make变量</span></div>
<p>所谓Make变量，是一类特殊的、可展开的字符串变量，这种变量类似Shell中变量替换那样的展开。</p>
<p>Bazel提供了：</p>
<ol>
<li>预定义变量，可以在任何规则中使用</li>
<li>自定义变量，在规则中定义。仅仅在依赖该规则的那些规则中，可以使用这些变量</li>
</ol>
<div class="blog_h3"><span class="graybg">使用Make变量</span></div>
<p>仅仅那些标记为Subject to 'Make variable' substitution的规则属性，才可以使用Make变量。例如：</p>
<pre class="crayon-plain-tag"># 使用Make变量FOO
my_attr = "prefix $(FOO) suffix"
# 如果变量FOO的值为bar，则实际my_attr的值为prefix bar suffix</pre>
<p>如果要使用$字符，需要用<pre class="crayon-plain-tag">$$</pre>代替。 </p>
<div class="blog_h3"><span class="graybg">一般预定义变量</span></div>
<p>执行命令：<pre class="crayon-plain-tag">bazel info --show_make_env [build options]</pre>可以查看所有预定义变量的列表。</p>
<p>任何规则可以使用以下变量：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 180px; text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>COMPILATION_MODE</td>
<td>编译模式：fastbuild、dbg、opt</td>
</tr>
<tr>
<td>BINDIR</td>
<td>目标体系结构的二进制树的根目录</td>
</tr>
<tr>
<td>GENDIR</td>
<td>目标体系结构的生成代码树的根目录</td>
</tr>
<tr>
<td>TARGET_CPU</td>
<td>目标体系结构的CPU</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">genrule预定义变量</span></div>
<p>下表中的变量可以在genrule规则的cmd属性中使用：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 180px; text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>OUTS</td>
<td>genrule的outs列表，如果只有一个输出文件，可以用<pre class="crayon-plain-tag">$@</pre></td>
</tr>
<tr>
<td>SRCS</td>
<td>genrule的srcs列表，如果只有一个输入文件，可以用<pre class="crayon-plain-tag">$&lt;</pre></td>
</tr>
<tr>
<td>@D</td>
<td>
<p>输出目录，如果：</p>
<ol>
<li>outs仅仅包含一个文件名，则展开为包含该文件的目录</li>
<li>outs包含多个文件，则此变量展开为在genfiles树中，当前包的根目录</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">输入输出路径变量</span></div>
<p>下表中的变量以Bazel的Label为参数，获取包的某类输入/输出路径：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 180px; text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>execpath</td>
<td rowspan="2">
<p>获取指定标签对应的规则（此规则必须仅仅输出单个文件）或文件（必须是单个文件），位于execroot下的对应路径</p>
<p>对于项目myproject，<span style="background-color: #c0c0c0;">所有构建动作</span>在工作空间根目录下的符号链接bazel-myproject对应的目录下执行，此目录即execroot。源码empty.source被链接到bazel-myproject/testapp/empty.source，因此其execpath为testapp/empty.source</p>
<p>对于目标：</p>
<pre class="crayon-plain-tag">cc_binary(
  name = "app",
  srcs = ["app.cc"]
)</pre>
<p>执行构建：<pre class="crayon-plain-tag">bazel build //testapp:app</pre>时： </p>
<pre class="crayon-plain-tag">$(execpath :app)  # bazel-out/host/bin/testapp/app
$(execpath empty.source) # testapp/empty.source </pre>
</td>
</tr>
<tr>
<td>execpaths</td>
</tr>
<tr>
<td>rootpath</td>
<td rowspan="2">
<p>获取runfiles路径，二进制文件通过此路径在运行时寻找其依赖
<p>对于上面的//testapp:app目标：</p>
<pre class="crayon-plain-tag">$(rootpath :app)  # testapp/app
$(rootpath empty.source)  # testapp/empty.source </pre>
</td>
</tr>
<tr>
<td>rootpaths</td>
</tr>
<tr>
<td>location</td>
<td rowspan="2">
<p>根据当前所声明的属性，等价于execpath或rootpath
<p>对于上面的//testapp:app目标：</p>
<pre class="crayon-plain-tag">$(location :app) # bazel-out/host/bin/testapp/app
$(location empty.source) # testapp/empty.source </pre>
</td>
</tr>
<tr>
<td>locations</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">一般规则</span></div>
<div class="blog_h2"><span class="graybg">规则列表</span></div>
<div class="blog_h3"><span class="graybg">filegroup</span></div>
<p>为一组目标指定一个名字，你可以从其它规则中方便的引用这组目标。
<p>Bazel鼓励使用filegroup，而不是直接引用目录。Bazel构建系统不能完全了解目录中文件的变化情况，因而文件发生变化时，可能不会进行重新构建。而使用filegroup，即使联用glob，目录中所有文件仍然能够被构建系统正确的监控。</p>
<p>示例：</p>
<pre class="crayon-plain-tag">filegroup(
    name = "exported_testdata",
    srcs = glob([
        "testdata/*.dat",
        "testdata/logs/**/*.log",
    ]),
)</pre>
<p>要引用filegroup，只需要使用标签：</p>
<pre class="crayon-plain-tag">cc_library(
    name = "my_library",
    srcs = ["foo.cc"],
    data = [
        "//my_package:exported_testdata",
        "//my_package:mygroup",
    ],
)</pre>
<div class="blog_h3"><span class="graybg">test_suite</span></div>
<p>定义一组测试用例，给出一个有意义的名称，便于在特定时机  —— 例如迁入代码、执行压力测试 —— 时执行这些测试用例。</p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 匹配当前包中所有small测试
test_suite(
    name = "small_tests",
    tags = ["small"],
)
# 匹配不包含flaky标记的测试
test_suite(
    name = "non_flaky_test",
    tags = ["-flaky"],
)
# 指定测试列表
test_suite(
    name = "smoke_tests",
    tests = [
        "system_unittest",
        "public_api_unittest",
    ],
)</pre>
<div class="blog_h3"><span class="graybg">alias</span></div>
<p>为规则设置一个别名：</p>
<pre class="crayon-plain-tag">filegroup(
    name = "data",
    srcs = ["data.txt"],
)
# 定义别名
alias(
    name = "other",
    actual = ":data",
)</pre>
<div class="blog_h3"><span class="graybg">config_setting</span></div>
<p>通过匹配<span style="background-color: #c0c0c0;">以Bazel标记或平台约束来表达的“配置状态”</span>，config_setting能够触发可配置的属性。</p>
<p>下面这个例子，匹配针对ARM平台的构建：</p>
<pre class="crayon-plain-tag">config_setting(
    name = "arm_build",
    values = {"cpu": "arm"},
)</pre>
<p>下面的例子，匹配任何定义了宏FOO=bar的针对X86平台的调试（-c dbg）构建：</p>
<pre class="crayon-plain-tag">config_setting(
    name = "x86_debug_build",
    values = {
        "cpu": "x86",
        "compilation_mode": "dbg",
        "define": "FOO=bar"
    },
)</pre>
<p>下面的库，通过select来声明可配置属性：</p>
<pre class="crayon-plain-tag">cc_binary(
    name = "mybinary",
    srcs = ["main.cc"],
    deps = select({
        # 如果config_settings arm_build匹配正在进行的构建，则依赖arm_lib这个目标
        ":arm_build": [":arm_lib"],
        # 如果config_settings x86_debug_build匹配正在进行的构建，则依赖x86_devdbg_lib
        ":x86_debug_build": [":x86_devdbg_lib"],
        # 默认情况下，依赖generic_lib
        "//conditions:default": [":generic_lib"],
    }),
) </pre>
<div class="blog_h3"><span class="graybg">genrule</span></div>
<p>一般性的规则 —— 使用用户指定的Bash命令，生成一个或多个文件。使用genrule理论上可以实现任何构建行为，例如压缩JavaScript代码。但是在执行C++、Java等构建任务时，最好使用相应的专用规则，更加简单。</p>
<p>不要使用genrule来运行测试，如果需要一般性的测试规则，可以考虑使用sh_test。</p>
<p>genrule在一个Bash shell环境下执行，当任意一个命令或管道失败（set -e -o pipefail），整个规则就失败。你不应该在genrule中访问网络。</p>
<p>示例：</p>
<pre class="crayon-plain-tag">genrule(
    name = "foo",
    # 不需要输入
    srcs = [],
    # 生成一个foo.h
    outs = ["foo.h"],
    # 运行当前规则所在包下的一个Perl脚本
    cmd = "./$(location create_foo.pl) &gt; \"$@\"",
    tools = ["create_foo.pl"],
) </pre>
<div class="blog_h1"><span class="graybg">C++规则</span></div>
<div class="blog_h2"><span class="graybg">规则列表</span></div>
<div class="blog_h3"><span class="graybg">cc_binary</span></div>
<p>隐含输出：</p>
<ol>
<li>name.stripped，仅仅当显式要求才会构建此输出，针对生成的二进制文件运行strip -g以驱除debug符号。额外的strip选项可以通过命令行--stripopt=-foo传入</li>
<li>name.dwp，仅仅当显式要求才会构建此输出，如果启用了 Fission ，则此文件包含用于远程调试的调试信息，否则是空文件</li>
</ol>
<p>属性列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>name</td>
<td>目标的名称</td>
</tr>
<tr>
<td>deps</td>
<td>
<p>需要链接到此二进制目标的其它库的列表，以Label引用</p>
<p>这些库可以是cc_library或objc_library定义的目标</p>
</td>
</tr>
<tr>
<td>srcs</td>
<td>
<p>C/C++源文件列表，以Label引用</p>
<p>这些文件是C/C++源码文件或头文件，可以是自动生成的或人工编写的。</p>
<p>所有cc/c/cpp文件都会被编译。<span style="background-color: #c0c0c0;">如果某个声明的文件在其它规则的outs列表中，则当前规则自动依赖于那个规则</span></p>
<p>所有.h文件都不会被编译，仅仅供源码文件包含之。所有.h/.cc等文件都可以包含<span style="background-color: #c0c0c0;">srcs中声明的</span>、<span style="background-color: #c0c0c0;">deps中声明的目标的hdrs中声明</span>的头文件。也就是说，任何#include的文件要么在此属性中声明，要么在依赖的cc_library的hdrs属性中声明</p>
<p>如果某个规则的名称出现在srcs列表中，则当前规则自动依赖于那个规则：</p>
<ol>
<li>如果那个规则的输出是C/C++源文件，则它们被编译进当前目标</li>
<li>如果那个规则的输出是库文件，则被链接到当前目标</li>
</ol>
<p>允许的文件类型：</p>
<ol>
<li>C/C++源码，扩展名.c, .cc, .cpp, .cxx, .c++, .C</li>
<li>C/C++头文件，扩展名.h, .hh, .hpp, .hxx, .inc</li>
<li>汇编代码，扩展名.S</li>
<li>归档文件，扩展名.a, .pic.a</li>
<li>共享库，扩展名.so, .so.version，version为soname版本号</li>
<li>对象文件，扩展名.o, .pic.o</li>
<li>任何能够产生上述文件的规则</li>
</ol>
</td>
</tr>
<tr>
<td>copts</td>
<td>
<p>字符串列表</p>
<p>为C++编译器提供的选项，在编译目标之前，这些选项按顺序添加到COPTS。这些选项仅仅影响当前目标的编译，而<span style="background-color: #c0c0c0;">不影响其依赖</span>。选项中的任何路径都<span style="background-color: #c0c0c0;">相对于当前工作空间而非当前包</span></p>
<p>也可以在bazel build时通过--copts选项传入，例如：</p>
<pre class="crayon-plain-tag">--copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1" </pre>
</td>
</tr>
<tr>
<td>defines</td>
<td>
<p>字符串列表
<p>为C++编译器传递宏定义，实际上会前缀以-D并添加到COPTS。与copts属性不同，这些宏定义会添加到当前目标，以及<span style="background-color: #c0c0c0;">所有依赖它的目标</span></p>
</td>
</tr>
<tr>
<td>includes</td>
<td>
<p>字符串列表</p>
<p>为C++编译器传递的头文件包含目录，实际上会前缀以-isystem并添加到COPTS。与copts属性不同，这些头文件包含会影响当前目标，以及<span style="background-color: #c0c0c0;">所有依赖它的目标</span></p>
<p>如果不清楚有何副作用，可以<span style="background-color: #c0c0c0;">传递-I到copts</span>，而不是使用当前属性</p>
</td>
</tr>
<tr>
<td>linkopts </td>
<td>
<p>字符串列表</p>
<p>为C++链接器传递选项，在链接二进制文件之前，此属性中的每个字符串被添加到LINKOPTS</p>
<p>此属性列表中，任何不以$和-开头的项，都被认为是deps中声明的<span style="background-color: #c0c0c0;">某个目标的Label，目标产生的文件会添加到链接选项</span>中</p>
</td>
</tr>
<tr>
<td>linkshared</td>
<td>
<p>布尔，默认False。用于创建共享库</p>
<p>要创建共享库，指定属性linkshared = True，对于GCC来说，会添加选项-shared。生成的结果适合被Java这类应用程序加载</p>
<p>需要注意，这里创建的共享库<span style="background-color: #c0c0c0;">绝不会被链接到依赖它的二进制文件</span>，而只适用于被其它程序手工的加载。因此，不能代替cc_library</p>
<p>如果同时指定<pre class="crayon-plain-tag">linkopts=['-static']</pre>和linkshared=True，你会得到一个完全自包含的单元。如果同时指定linkstatic=True和linkshared=True会得到一个基本是完全自包含的单元</p>
</td>
</tr>
<tr>
<td>linkstatic</td>
<td>
<p>布尔，默认True</p>
<p>对于cc_binary和cc_test，以静态形式链接二进制文件。对于cc_binary此选项默认True，其它目标默认False</p>
<p>如果当前目标是binary或test，此选项提示构建工具，<span style="background-color: #c0c0c0;">尽可能链接到用户库的.a版本而非.so</span>版本。某些系统库可能仍然需要动态链接，原因是没有静态库，这导致最终的输出仍然使用动态链接，不是完全静态的</p>
<p>链接一个可执行文件时，实际上有三种方式：</p>
<ol>
<li>STATIC，使用完全静态链接特性。所有依赖都被静态链接，GCC命令示例：<br />
<pre class="crayon-plain-tag">gcc -static foo.o libbar.a libbaz.a -lm</pre>
</li>
<li>STATIC，所有用户库静态链接（如果存在静态库版本），但是系统库（除去C/C++运行时库）动态链接，GCC命令示例：<br />
<pre class="crayon-plain-tag"># 此方式可以由linkstatic=True 启用
gcc foo.o libfoo.a libbaz.a -lm </pre>
</li>
<li>DYNAMIC，所有依赖被动态链接（如果存在动态库版本），GCC命令示例：<br />
<pre class="crayon-plain-tag"># 此方式可以由linkstatic=False 启用
gcc foo.o libfoo.so libbaz.so -lm </pre>
</li>
</ol>
<p>对于cc_library来说，linkstatic属性的含义不同。对于C++库来说：</p>
<ol>
<li>linkstatic=True表示仅仅允许静态链接，也就是不产生.so文件</li>
<li>linkstatic=False表示允许动态链接，同时产生.a和.so文件</li>
</ol>
</td>
</tr>
<tr>
<td>malloc </td>
<td>
<p>指向标签，默认@bazel_tools//tools/cpp:malloc</p>
<p>覆盖默认的malloc依赖，默认情况下C++二进制文件链接到//tools/cpp:malloc，这是一个空库，这导致实际上链接到libc的malloc</p>
</td>
</tr>
<tr>
<td>nocopts</td>
<td>
<p>字符串</p>
<p>从C++编译命令中移除匹配的选项，此属性的值是正则式，任何匹配正则式的、已经存在的COPTS被移除 </p>
</td>
</tr>
<tr>
<td>stamp </td>
<td>
<p>整数，默认-1</p>
<p>用于将构建信息嵌入到二进制文件中，可选值：</p>
<ol>
<li>stamp = 1，将构建信息嵌入，目标二进制仅仅在其依赖变化时重新构建</li>
<li>stamp = 0，总是将构建信息替换为常量值，有利于构建结果缓存</li>
<li>stamp = -1 ，由--[no]stamp标记控制是否嵌入</li>
</ol>
</td>
</tr>
<tr>
<td>toolchains </td>
<td>
<p>标签列表</p>
<p>提供构建变量（Make variables，这些变量可以被当前目标使用）的工具链的标签列表 </p>
</td>
</tr>
<tr>
<td>win_def_file</td>
<td>
<p>标签</p>
<p>传递给链接器的Windows DEF文件。在Windows上，此属性可以在链接共享库时导出符号 </p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">cc_import</span></div>
<p>导入预编译好的C/C++库。</p>
<p>属性列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>hdrs</td>
<td>此预编译库对外发布的头文件列表，依赖此库的规则（dependent rule）会直接将这些头文件包含在源码列表中</td>
</tr>
<tr>
<td>alwayslink</td>
<td>
<p>布尔，默认False</p>
<p>如果为True，则依赖此库的二进制文件会将此静态库归档中的对象文件链接进去，就算某些对象文件中的符号并没有被二进制文件使用</p>
</td>
</tr>
<tr>
<td>interface_library</td>
<td>用于链接共享库时使用的接口（导入）库</td>
</tr>
<tr>
<td>shared_library</td>
<td>共享库，Bazel保证在运行时可以访问到共享库</td>
</tr>
<tr>
<td>static_library</td>
<td>静态库</td>
</tr>
<tr>
<td>system_provided</td>
<td>提示运行时所需的共享库由操作系统提供，如果为True则应该指定interface_library，shared_library应该为空</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">cc_library</span></div>
<p>对于所有cc_*规则来说，构建所需的任何头文件都要在hdrs或srcs中声明。</p>
<p>对于cc_library规则，在hdrs声明的头文件构成库的公共接口。这些头文件可以被当前库的hdrs/srcs中的文件直接包含，也可以被依赖（deps）当前库的其它cc_*的hdrs/srcs直接包含。<span style="background-color: #c0c0c0;">位于srcs中的头文件，则仅仅能被当前库的</span>hdrs/srcs包含。</p>
<p>cc_binary和cc_test不会暴露接口，因此它们没有hdrs属性。</p>
<p>属性列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>name</td>
<td>库的名称</td>
</tr>
<tr>
<td>deps</td>
<td>需要链接到（into）当前库的其它库</td>
</tr>
<tr>
<td>srcs</td>
<td>头文件和源码列表</td>
</tr>
<tr>
<td>hdrs</td>
<td>导出的头文件列表</td>
</tr>
<tr>
<td>copts/nocopts</td>
<td>传递给C++编译命令的参数</td>
</tr>
<tr>
<td>defines</td>
<td>宏定义列表</td>
</tr>
<tr>
<td>include_prefix</td>
<td>hdrs中头文件的路径前缀</td>
</tr>
<tr>
<td>includes</td>
<td>
<p>字符串列表</p>
<p>需要添加到编译命令的包含文件列表</p>
</td>
</tr>
<tr>
<td>linkopts</td>
<td>链接选项</td>
</tr>
<tr>
<td>linkstatic</td>
<td>是否生成动态库</td>
</tr>
<tr>
<td>strip_include_prefix</td>
<td>
<p>字符串</p>
<p>需要脱去的头文件路径前缀，也就是说使用hdrs中头文件时，要把这个前缀去除，路径才匹配</p>
</td>
</tr>
<tr>
<td>textual_hdrs</td>
<td>
<p>标签列表</p>
<p>头文件列表，这些头文件是不能独立编译的。依赖此库的目标，直接以文本形式包含这些头文件到它的源码列表中，这样才能正确编译这些头文件</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">常见用例</span></div>
<div class="blog_h3"><span class="graybg">通配符</span></div>
<p>可以使用Glob语法为目标添加多个文件：</p>
<pre class="crayon-plain-tag">cc_library(
    name = "build-all-the-files",
    srcs = glob(["*.cc"]),
    hdrs = glob(["*.h"]),
)</pre>
<div class="blog_h3"><span class="graybg">传递性依赖</span></div>
<p>如果源码依赖于某个头文件，则该源码的规则需要dep头文件的库，仅仅直接依赖才需要声明：</p>
<pre class="crayon-plain-tag"># 三明治依赖面包
cc_library(
    name = "sandwich",
    srcs = ["sandwich.cc"],
    hdrs = ["sandwich.h"],
    # 声明当前包下的目标为依赖
    deps = [":bread"],
)
# 面包依赖于面粉，三明治间接依赖面粉，因此不需要声明
cc_library(
    name = "bread",
    srcs = ["bread.cc"],
    hdrs = ["bread.h"],
    deps = [":flour"],
)

cc_library(
    name = "flour",
    srcs = ["flour.cc"],
    hdrs = ["flour.h"],
)</pre>
<div class="blog_h3"><span class="graybg">添加头文件路径 </span></div>
<p>有些时候你不愿或不能将头文件放到工作空间的include目录下，现有的库的include目录可能不符合</p>
<div class="blog_h3"><span class="graybg">导入已编译库</span></div>
<p>导入一个库，用于静态链接：</p>
<pre class="crayon-plain-tag">cc_import(
  name = "mylib",
  hdrs = ["mylib.h"],
  static_library = "libmylib.a",
  # 如果为1则libmylib.a总会链接到依赖它的二进制文件
  alwayslink = 1,
)</pre>
<p>导入一个库，用于共享链接（UNIX）： </p>
<pre class="crayon-plain-tag">cc_import(
  name = "mylib",
  hdrs = ["mylib.h"],
  shared_library = "libmylib.so",
)</pre>
<p>通过接口库（Interface library）链接到共享库（Windows）：</p>
<pre class="crayon-plain-tag">cc_import(
  name = "mylib",
  hdrs = ["mylib.h"],
  # mylib.lib是mylib.dll的导入库，此导入库会传递给链接器
  interface_library = "mylib.lib",
  # mylib.dll在运行时需要，链接时不需要
  shared_library = "mylib.dll",
)</pre>
<p>在二进制目标中选择链接到共享库还是静态库（UNIX）：</p>
<pre class="crayon-plain-tag">cc_import(
  name = "mylib",
  hdrs = ["mylib.h"],
  # 同时声明共享库和静态库
  static_library = "libmylib.a",
  shared_library = "libmylib.so",
)

# 此二进制目标链接到静态库
cc_binary(
  name = "first",
  srcs = ["first.cc"],
  deps = [":mylib"],
  linkstatic = 1, # default value
)

# 此二进制目标链接到共享库
cc_binary(
  name = "second",
  srcs = ["second.cc"],
  deps = [":mylib"],
  linkstatic = 0,
)</pre>
<div class="blog_h3"><span class="graybg">包含外部库</span></div>
<p>你可以在WORKSPACE中调用new_*存储库函数，来从网络中下载依赖。下面的例子下载Google Test库：</p>
<pre class="crayon-plain-tag"># 下载归档文件，并让其在工作空间的存储库中可用
new_http_archive(
    name = "gtest",
    url = "https://github.com/google/googletest/archive/release-1.7.0.zip",
    sha256 = "b58cb7547a28b2c718d1e38aee18a3659c9e3ff52440297e965f5edffe34b6d0",
    # 外部库的构建规则编写在gtest.BUILD
    # 如果此归档文件已经自带了BUILD文件，则可以调用不带new_前缀的函数
    build_file = "gtest.BUILD",
    # 去除路径前缀
    strip_prefix = "googletest-release-1.7.0",
)</pre>
<p>构建此外部库的规则如下：</p>
<pre class="crayon-plain-tag">cc_library(
    name = "main",
    srcs = glob(
        # 前缀去除，原来是googletest-release-1.7.0/src/*.cc
        ["src/*.cc"],
        # 排除此文件
        exclude = ["src/gtest-all.cc"]
    ),
    hdrs = glob([
        # 前缀去除
        "include/**/*.h",
        "src/*.h"
    ]),
    copts = [
        # 前缀去除，原来是external/gtest/googletest-release-1.7.0/include
        "-Iexternal/gtest/include"
    ],
    # 链接到pthread
    linkopts = ["-pthread"],
    visibility = ["//visibility:public"],
)</pre>
<div class="blog_h3"><span class="graybg">使用外部库</span></div>
<p>沿用上面的例子，下面的目标使用gtest编写测试代码：</p>
<pre class="crayon-plain-tag">cc_test(
    name = "hello-test",
    srcs = ["hello-test.cc"],
    # 前缀去除
    copts = ["-Iexternal/gtest/include"],
    deps = [
        # 依赖gtest存储库的main目标
        "@gtest//:main",
        "//lib:hello-greet",
    ],
)</pre>
<div class="blog_h1"><span class="graybg">外部依赖</span></div>
<p>Bazel允许依赖其它项目中定义的目标，这些<span style="background-color: #c0c0c0;">来自其它项目的依赖叫做“外部依赖“</span>。当前工作空间的<span style="background-color: #c0c0c0;">WORKSPACE文件声明从何处下载外部依赖的源码</span>。</p>
<p>外部依赖可以有自己的1-N个BUILD文件，其中定义自己的目标。当前项目可以使用这些目标。例如下面的两个项目结构：</p>
<pre class="crayon-plain-tag">/
  home/
    user/
      project1/
        WORKSPACE
        BUILD
        srcs/
          ...
      project2/
        WORKSPACE
        BUILD
        my-libs/</pre>
<p>如果project1需要依赖定义在project2/BUILD中的目标:foo，则可以在其WORKSPACE中<span style="background-color: #c0c0c0;">声明一个存储库（repository）</span>，名字为project2，位于/home/user/project2。然后，可以在BUILD中通过标签<span style="background-color: #c0c0c0;">@project2//:foo</span>引用目标foo。</p>
<p>除了依赖来自文件系统其它部分的目标、下载自互联网的目标以外，用户还可以<span style="background-color: #c0c0c0;">编写自己的存储库规则（repository rules ）以实现更复杂的行为</span>。</p>
<p>WORKSPACE的语法格式和BUILD相同，但是允许使用<a href="https://docs.bazel.build/versions/master/be/workspace.html">不同的规则集</a>。</p>
<p>Bazel会把外部依赖下载到<pre class="crayon-plain-tag">$(bazel info output_base)/external</pre>目录中，要删除掉外部依赖，执行：</p>
<pre class="crayon-plain-tag">bazel clean --expunge</pre>
<div class="blog_h2"><span class="graybg">外部依赖类型</span></div>
<div class="blog_h3"><span class="graybg">Bazel项目</span></div>
<p>可以使用local_repository、git_repository或者http_archive这几个规则来引用。</p>
<p>引用本地Bazel项目的例子：</p>
<pre class="crayon-plain-tag">local_repository(
    name = "coworkers_project",
    path = "/path/to/coworkers-project",
)</pre>
<p>在BUILD中，引用coworkers_project中的目标//foo:bar时，使用标签@coworkers_project//foo:bar </p>
<div class="blog_h3"><span class="graybg">非Bazel项目</span></div>
<p>可以使用new_local_repository、new_git_repository或者new_http_archive这几个规则来引用。你需要自己编写BUILD文件来构建这些项目。</p>
<p>引用本地非Bazel项目的例子：</p>
<pre class="crayon-plain-tag">new_local_repository(
    name = "coworkers_project",
    path = "/path/to/coworkers-project",
    build_file = "coworker.BUILD",
)</pre><br />
<pre class="crayon-plain-tag">cc_library(
    name = "some-lib",
    srcs = glob(["**"]),
    visibility = ["//visibility:public"],
)&amp;nbsp;</pre>
<p>在BUILD文件中，使用标签@coworkers_project//:some-lib引用上面的库。 </p>
<div class="blog_h3"><span class="graybg">外部包</span></div>
<p>对于Maven仓库，可以使用规则maven_jar/maven_server来下载JAR包，并将其作为Java依赖。</p>
<div class="blog_h2"><span class="graybg">依赖拉取</span></div>
<p>默认情况下，执行bazel Build时会按需自动拉取依赖，你也可以禁用此特性，并使用bazel fetch预先手工拉取依赖。</p>
<div class="blog_h2"><span class="graybg">使用代理</span></div>
<p>Bazel可以使用HTTPS_PROXY或HTTP_PROXY定义的代理地址。</p>
<div class="blog_h2"><span class="graybg">依赖缓存</span></div>
<p>Bazel会缓存外部依赖，当WORKSPACE改变时，会重新下载或更新这些依赖。</p>
<div class="blog_h1"><span class="graybg">.bazelrc</span></div>
<p>Bazel命令接收大量的参数，其中一部分很少变化，这些不变的配置项可以存放在.bazelrc中。</p>
<div class="blog_h2"><span class="graybg">位置</span></div>
<p>Bazel按以下顺序寻找.bazelrc文件：</p>
<ol>
<li>除非指定--nosystem_rc，否则寻找/etc/bazel.bazelrc</li>
<li>除非指定--noworkspace_rc，否则寻找工作空间根目录的.bazelrc</li>
<li>除非指定--nohome_rc，否则寻找当前用户的$HOME/.bazelrc</li>
</ol>
<div class="blog_h2"><span class="graybg">语法</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">元素</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>import</td>
<td>导入其它bazelrc文件，例如：<pre class="crayon-plain-tag">import %workspace%/tools/bazel.rc</pre></td>
</tr>
<tr>
<td>默认参数</td>
<td>
<p>可以提供以下行：</p>
<p>startup ... 启动参数<br />common... 适用于所有命令的参数<br /><em>command</em>...为某个子命令指定参数，例如buildquery、</p>
<p>以上三类行，都可以出现多次</p>
</td>
</tr>
<tr>
<td>
<p>--config</p>
</td>
<td>
<p>用于定义一组参数的组合，在调用bazel命令时指定--config=memcheck，可以引用名为memcheck的参数组。此参数组的定义示例：</p>
<pre class="crayon-plain-tag">build:memcheck --strip=never --test_timeout=3600</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">扩展</span></div>
<p>所谓Bazel扩展，是扩展名为.bzl的文件。你可以使用load语句加载扩展中定义的符号到BUILD中。
<div class="blog_h2"><span class="graybg">构建阶段</span></div>
<p>一次Bazel构建包含三个阶段：</p>
<ol>
<li>加载阶段：加载、eval本次构建需要的所有扩展、所有BUILD文件。宏在此阶段执行，规则被实例化。BUILD文件中<span style="background-color: #c0c0c0;">调用的宏/函数，在此阶段执行函数体</span>，其结果是<span style="background-color: #c0c0c0;">宏里面实例化的规则被填充到BUILD文件</span>中</li>
<li>分析阶段：<span style="background-color: #c0c0c0;">规则的代码——也就是它的implementation函数被执行</span>，导致规则的Action被实例化，Action描述如何从输入产生输出</li>
<li>执行阶段：执行Action，产生输出，<span style="background-color: #c0c0c0;">测试也在此阶段执行</span></li>
</ol>
<p>Bazel会并行的读取/解析/eval BUILD文件和.bzl文件。每个文件在每次构建最多被读取一次，eval的结果被缓存并重用。每个文件在它<span style="background-color: #c0c0c0;">的全部依赖被解析之后</span>才eval。加载一个.bzl文件没有副作用，仅仅是定义值和函数</p>
<div class="blog_h2"><span class="graybg">宏</span></div>
<p>宏（Macro）是一种函数，用来实例化（instantiates）规则。如果BUILD文件太过重复或复杂，可以考虑使用宏，以便减少代码。<span style="background-color: #c0c0c0;">宏的函数在BUILD文件被读取时就立即执行</span>。BUILD被读取（eval）之后，宏被替换为它生成的规则。bazel query只会列出生成的规则而非宏。</p>
<p>编写宏时需要注意：</p>
<ol>
<li>所有实例化规则的公共函数，都必须具有一个无默认值的name参数</li>
<li>公共函数应当具有docstring</li>
<li>在BUILD文件中，调用宏时name参数必须是关键字参数</li>
<li>宏所生成的规则的name属性，必须以调用宏的name参数作为后缀</li>
<li>大部分情况下，可选参数应该具有默认值None</li>
<li>应当具有可选的visibility参数</li>
</ol>
<div class="blog_h3"><span class="graybg">示例</span></div>
<p>要在宏中实例化原生规则（Native rules，不需要load即可使用的那些规则），可以<span style="background-color: #c0c0c0;">使用native模块</span>：</p>
<pre class="crayon-plain-tag"># 该宏实例化一个genrule规则
def file_generator(name, arg, visibility=None):
  // 生成一个genrule规则
  native.genrule(
    name = name,
    outs = [name + ".txt"],
    cmd = "$(location generator) %s &gt; $@" % arg,
    tools = ["//test:generator"],
    visibility = visibility,
  )</pre>
<p>使用上述宏的BUILD文件：</p>
<pre class="crayon-plain-tag">load("//path:generator.bzl", "file_generator")

file_generator(
    name = "file",
    arg = "some_arg",
)</pre>
<p>执行下面的命令查看宏展开后的情况：</p>
<pre class="crayon-plain-tag"># bazel query --output=build //label

genrule(
  name = "file",
  tools = ["//test:generator"],
  outs = ["//test:file.txt"],
  cmd = "$(location generator) some_arg &gt; $@",
)</pre>
<div class="blog_h2"><span class="graybg">规则</span></div>
<p><a href="https://docs.bazel.build/versions/master/skylark/rules.html">规则（Rule）</a>比宏更强大，能够对Bazel内部特性进行访问，并可以完全控制Bazel。</p>
<p>规则定义了为了产生输出，需要在输入上执行的一系列动作。例如，C++二进制文件规则以一系列.cpp文件为输入，针对输入调用g++，输出一个可执行文件。注意，从Bazel的角度来说，不但cpp文件是输入，g++、C++库也是输入。当编写自定义规则时，你需要注意，将执行Action所需的库、工具作为输入看待。</p>
<p>Bazel内置了一些规则，这些规则叫原生规则，例如cc_library、cc_library，对一些语言提供了基础的支持。通过编写自定义规则，你可以实现对任何语言的支持。</p>
<p>定义在.bzl中的规则，用起来就像原生规则一样 —— 规则的目标具有标签、可以出现在bazel query。</p>
<p>规则在分析阶段的行为，由它的implementation函数决定。此函数不得调用任何外部工具，它只是注册在执行阶段需要的Action。</p>
<div class="blog_h3"><span class="graybg">自定义规则</span></div>
<p>在.bzl文件中，你可以调用rule创建自定义规则，并将其保存到全局变量：</p>
<pre class="crayon-plain-tag">def _empty_impl(ctx):
    # 分析阶段此函数被执行
    print("This rule does nothing")

empty = rule(implementation = _empty_impl)</pre>
<p>然后，规则可以通过load加载到BUILD文件：</p>
<pre class="crayon-plain-tag">load("//empty:empty.bzl", "empty")

# 实例化规则
empty(name = "nothing")</pre>
<div class="blog_h3"><span class="graybg">规则属性 </span></div>
<p>属性即实例化规则时需要提供的参数，例如srcs、deps。在自定义规则的时候，你可以列出所有属性的名字和Schema：</p>
<pre class="crayon-plain-tag">sum = rule(
    implementation = _impl,
    attrs = {
        # 定义一个整数属性，一个列表属性
        "number": attr.int(default = 1),
        "deps": attr.label_list(),
    },
)</pre>
<p>实例化规则的时候，你需要以参数的形式指定属性：</p>
<pre class="crayon-plain-tag">sum(
    name = "my-target",
    deps = [":other-target"],
)

sum(
    name = "other-target",
)</pre>
<p>如果实例化规则的时候，没有指定某个属性的值（且没指定默认值），规则的实现函数会在ctx.attr中看到一个占位符，此占位符的值取决于属性的类型。</p>
<p>使用default为属性指定默认值，使用 mandatory=True 声明属性必须提供。</p>
<div class="blog_h3"><span class="graybg">默认属性</span></div>
<p>任何规则自动具有以下属性：deprecation, features, name, tags, testonly, visibility。</p>
<p>任何测试规则具有以下额外属性：args, flaky, local, shard_count, size, timeout。</p>
<div class="blog_h3"><span class="graybg">特殊属性</span></div>
<p>有两类特殊属性需要注意：</p>
<ol>
<li>依赖属性：例如attr.label、attr.label_list，用于声明拥有此属性的目标所依赖的其它目标</li>
<li>输出属性：例如attr.output、attr.output_list，声明目标的输出文件，较少使用</li>
</ol>
<p>上面两类属性的值都是Label类型。 </p>
<div class="blog_h3"><span class="graybg">隐含依赖</span></div>
<p>具有默认值的依赖属性，称为隐含依赖（implicit dependency）。如果要硬编码规则和工具（例如编译器）之间的关系，可通过隐含依赖。从规则的角度来看，这些工具仍然属于输入，就像源代码一样。</p>
<div class="blog_h3"><span class="graybg">私有属性</span></div>
<p>某些情况下，我们会为规则添加具有默认值的属性，同时还想禁止用户修改属性值，这种情况下可以使用私有属性。</p>
<p>私有属性以下划线 _ 开头，必须具有默认值。</p>
<div class="blog_h3"><span class="graybg">目标</span></div>
<p>实例化规则不会返回值，但是会定义一个新的目标。</p>
<div class="blog_h3"><span class="graybg">规则实现</span></div>
<p>任何规则都需要提供一个实现函数。提供在分析阶段需要严格执行的逻辑。此函数不能有任何读写行为，仅仅用于注册Action。</p>
<p>实现函数具有唯一性入参  —— <a href="https://docs.bazel.build/versions/master/skylark/lib/ctx.html">规则上下文</a>，通常将其命名为ctx。通过规则上下文你可以：</p>
<ol>
<li>访问规则属性</li>
<li>获得输入输出文件的handle</li>
<li>创建Actions</li>
<li>通过providers向依赖于当前规则的其它规则传递信息</li>
</ol>
<div class="blog_h3"><span class="graybg">ctx</span></div>
<p>规则上下文对象的具有以下主要方法或属性：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">方法/属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>action</td>
<td>废弃，使用ctx.actions.run()或ctx.actions.run_shell()代替</td>
</tr>
<tr>
<td>actions.run</td>
<td>
<p>创建一个调用可执行文件的Action，参数：Bazel加载阶段</p>
<p>outputs 此动作的输出文件列表<br />inputs 此动作输入文件的列表/depset<br />executable 执行此动作需要调用的可执行文件<br />tools 执行此动作需要的工具的列表/depset<br />arguments 传递给可执行文件的参数列表<br />mnemonic 动作的描述<br />progress_message 动作执行时，显示给用户的信息<br />use_default_shell_env 是否在内建Shell环境下运行可执行文件<br />env 环境变量字典<br />execution_requirements 调度此动作需要的信息<br />input_manifests 输入runfiles元数据，通常由resolve_command生成</p>
<p>示例：</p>
<pre class="crayon-plain-tag">def _impl(ctx):
    # The list of arguments we pass to the script.
    args = [ctx.outputs.out.path] + [f.path for f in ctx.files.chunks]

    # 调用可执行文件
    ctx.actions.run(
        inputs = ctx.files.chunks,
        outputs = [ctx.outputs.out],
        arguments = args,
        progress_message = "Merging into %s" % ctx.outputs.out.short_path,
        executable = ctx.executable._merge_tool,
    )
// 规则定义
concat = rule(
    implementation = _impl,
    attrs = {
        "chunks": attr.label_list(allow_files = True),
        "out": attr.output(mandatory = True),
        "_merge_tool": attr.label(
            executable = True,
            cfg = "host",
            allow_files = True,
            default = Label("//actions_run:merge"),
        ),
    },
) </pre>
</td>
</tr>
<tr>
<td>actions.run_shell</td>
<td>
<p>创建一个执行Shell脚本的Action
<p>示例：</p>
<p><pre class="crayon-plain-tag">def _impl(ctx):
    output = ctx.outputs.out
    input = ctx.file.file

    # 访问inputs中声明的文件
    ctx.actions.run_shell(
        inputs = [input],
        outputs = [output],
        progress_message = "Getting size of %s" % input.short_path,
        command = "stat -L -c%%s '%s' &gt; '%s'" % (input.path, output.path),
    )

# 规则定义
size = rule(
    implementation = _impl,
    attrs = {"file": attr.label(mandatory = True, allow_single_file = True)},
    outputs = {"out": "%{name}.size"},
)</pre>
</td>
</tr>
<tr>
<td>actions.write</td>
<td>此Action写入内容到文件</td>
</tr>
<tr>
<td>actions.declare_file</td>
<td>此Action创建新的文件</td>
</tr>
<tr>
<td>actions.do_nothing</td>
<td>不做任何事情的Action</td>
</tr>
<tr>
<td>ctx.attr</td>
<td>用于访问属性值的结构 </td>
</tr>
<tr>
<td>bin_dir</td>
<td>二进制目录的根</td>
</tr>
<tr>
<td>genfiles_dir</td>
<td>genfiles目录的根</td>
</tr>
<tr>
<td>build_file_path</td>
<td>相对于源码目录根的，当前BUILD文件的路径</td>
</tr>
<tr>
<td>executable </td>
<td>一个结构，可以引用任何通过<pre class="crayon-plain-tag">attr.label(executable=True)</pre>定义的规则属性</td>
</tr>
<tr>
<td>expand_location </td>
<td>
<p>展开input中定义的所有$(location //x)为目标x的真实路径。仅仅对当前规则的直接依赖、明确列在targets属性中的目标使用</p>
<p><pre class="crayon-plain-tag">string ctx.expand_location(input, targets=[])</pre>
</td>
</tr>
<tr>
<td>features</td>
<td>列出此规则明确启用的特性列表 </td>
</tr>
<tr>
<td>file</td>
<td>
<p>此结构包含任何通过<pre class="crayon-plain-tag">attr.labe(allow_single_file=True)</pre>定义的属性所指向的文件。此结构的字段名即文件属性名，结构字段值是file或Node类型</p>
<p>此结构是表达式<pre class="crayon-plain-tag">list(ctx.attr.&lt;ATTR&gt;.files)[0]</pre>的快捷方式</p>
</td>
</tr>
<tr>
<td>fragments</td>
<td>用于访问目标配置中的配置片断（configuration fragments ） </td>
</tr>
<tr>
<td>host_configuration</td>
<td>返回主机配置的configuration对象。configuration包含构建所在的运行环境信息 </td>
</tr>
<tr>
<td>host_fragments </td>
<td>用于访问host配置中的配置片断（configuration fragments ）  </td>
</tr>
<tr>
<td>label</td>
<td>当前正在分析的目标的标签 </td>
</tr>
<tr>
<td>outputs</td>
<td>一个包含所有预声明的输出文件的伪结构 </td>
</tr>
<tr>
<td>resolve_command</td>
<td>
<p>解析一个命令，返回(inputs, command, input_manifests)元组：</p>
<p>inputs，表示解析后的输入列表<br />command，解析后的命令的argv列表<br />input_manifests，执行命令需要的runfiles元数据</p>
</td>
</tr>
<tr>
<td>resolve_tools </td>
<td>解析工具，返回(inputs, input_manifests)元组 </td>
</tr>
<tr>
<td>runfiles </td>
<td>创建一个Runfiles</td>
</tr>
<tr>
<td>toolchains</td>
<td>此规则需要的工具链 </td>
</tr>
<tr>
<td>var </td>
<td>配置变量的字典</td>
</tr>
<tr>
<td>workspace_name </td>
<td>当前工作空间的名称 </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">存储库规则</span></div>
<p>存储库规则用于定义外部存储库。<span style="background-color: #c0c0c0;">外部存储库是一种规则</span>，这种规则只能用在WORKSPACE文件中，可以在<span style="background-color: #c0c0c0;">Bazel加载阶段</span>启用<span style="background-color: #c0c0c0;">非封闭性</span>（ non-hermetic，所谓封闭是指自包含，不依赖于外部环境）操作。每个外部存储库都创建自己的WORKSPACE，具有自己的BUILD文件和构件。</p>
<p>外部存储库可以用来：</p>
<ol>
<li>加载第三方依赖，例如Maven打包的库</li>
<li>为运行构件的主机生成特化的BUILD文件</li>
</ol>
<p>在bzl文件中，调用repository_rule函数可以创建一个存储库规则，你需要将其存放在全局变量中：</p>
<pre class="crayon-plain-tag">local_repository = repository_rule(
    # 实现函数
    implementation=_impl,
    local=True,
    # 属性列表
    attrs={"path": attr.string(mandatory=True)}) </pre>
<p>每个存储库规则都必须提供实现函数，其中包含<span style="background-color: #c0c0c0;">在Bazel加载阶段需要执行</span>的严格的逻辑。该函数具有唯一的入参repository_ctx：</p>
<pre class="crayon-plain-tag">def _impl(repository_ctx):
  # 你可以通过repository_ctx访问属性值、调用非密封性函数（例如查找、执行二进制文件，创建或下载文件到存储库）
  repository_ctx.symlink(repository_ctx.attr.path, "") </pre>
<p>引用存储库中规则时，可以使用<pre class="crayon-plain-tag">@REPO_NAMAE//package:target</pre>这样的标签。</p>
<div class="blog_h3"><span class="graybg">repository_ctx</span></div>
<p>存储库规则上下文对象的具有以下主要方法或属性：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">方法/属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>attr</td>
<td>用于访问所有属性的结构</td>
</tr>
<tr>
<td>download</td>
<td>
<pre class="crayon-plain-tag">struct repository_ctx.download(url, output='', sha256='', executable=False)</pre></p>
<p> 下载文件到输出路径，返回包含字段sha256的结构</p>
</td>
</tr>
<tr>
<td>download_and_extract</td>
<td>下载并解压</td>
</tr>
<tr>
<td>execute</td>
<td>执行指定的命令</td>
</tr>
<tr>
<td>file</td>
<td>以指定的内容在存储库目录下生成文件</td>
</tr>
<tr>
<td>name</td>
<td>此规则生成的外部存储库的名称</td>
</tr>
<tr>
<td>path</td>
<td>返回字符串/路径/标签对应的实际路径</td>
</tr>
<tr>
<td>symlink</td>
<td>在文件系统中创建符号链接<br />
<pre class="crayon-plain-tag"># from 符号链接的源，string/Label/path类型
# to 相对于存储库目录的符号链接文件的路径
None repository_ctx.symlink(from, to)</pre>
</td>
</tr>
<tr>
<td>template</td>
<td>使用模板创建文件</td>
</tr>
<tr>
<td>which</td>
<td>返回指定程序的路径</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">命令</span></div>
<div class="blog_h2"><span class="graybg">bazel</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">analyze-profile</td>
<td>分析构建配置数据（build profile data）</td>
</tr>
<tr>
<td class="blog_h3">aquery</td>
<td>针对post-analysis操作图执行查询</td>
</tr>
<tr>
<td class="blog_h3">build</td>
<td>构建指定的目标：<br />
<pre class="crayon-plain-tag"># 构建foo/bar包中的wiz目标
bazel build //foo/bar:wiz
# 构建foo/bar包中的bar目标
bazel build //foo/bar
# 构建foo/bar包中的所有规则
bazel build //foo/bar:all
# 构建foo目录下所有子代包的所有规则
bazel build //foo/...
bazel build //foo/...:all
# 构建foo目录下所有子代包的所有目标（规则和文件）
bazel build //foo/...:*
bazel build //foo/...:all-targets</pre></p>
<p>如果目标标签不以<pre class="crayon-plain-tag">//</pre>开头，则相对于当前目录。如果当前目录是foo则bar:wiz等价于//foo/bar:wiz</p>
<p>Bazel支持通过符号链接来寻找子包，除了：</p>
<ol>
<li>那些指向输出目录的子目录的符号链接，例如bazel-bin</li>
<li>包含了名为DONT_FOLLOW_SYMLINKS_WHEN_TRAVERSING_THIS_DIRECTORY_VIA_A_RECURSIVE_TARGET_PATTERN文件的目录</li>
</ol>
<p>指定了<pre class="crayon-plain-tag">tags = ["manual"]</pre>的目标必须手工构建，无法通过...、:*、:all等自动构建</p>
<p>常用选项：</p>
<p style="padding-left: 30px;">--loading_phase_threads   加载阶段使用的线程数量，可以防止并发太多导致下载缓慢，进而超时</p>
</td>
</tr>
<tr>
<td class="blog_h3">canonicalize-flags</td>
<td>规范化Bazel标记</td>
</tr>
<tr>
<td class="blog_h3">clean</td>
<td>清除输出文件，可选的停止服务器</td>
</tr>
<tr>
<td class="blog_h3">cquery</td>
<td>针对post-analysis依赖图查询</td>
</tr>
<tr>
<td class="blog_h3">dump</td>
<td>输出Bazel服务器的内部状态</td>
</tr>
<tr>
<td class="blog_h3">info</td>
<td>输出Bazel服务器的运行时信息</td>
</tr>
<tr>
<td class="blog_h3">fetch</td>
<td>
<p>拉取某个目标的外部依赖</p>
<p>使用<pre class="crayon-plain-tag">--fetch=false</pre>标记可以禁止在构建时进行自动的外部依赖（本地系统依赖除外）抓取，通过local_repository、new_local_repository声明的“本地”外部存储库，总是会抓取</p>
<p>如果禁用了自动抓取，你需要在以下时机手工抓取：</p>
<ol>
<li>第一次构建之前</li>
<li>每当新增了外部依赖之后</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag"># 抓取两个外部依赖
bazel fetch //foo:bar //bar:baz
# 抓取工作空间的全部外部依赖
bazel fetch //...</pre>
<p><strong><span style="background-color: #c0c0c0;">存储库缓存</span></strong></p>
<p>Bazel会避免反复抓取同一个文件，即使：</p>
<ol>
<li>多个工作空间使用同一外部依赖</li>
<li>外部存储库的定义改变了，但是需要下载的还是那个文件</li>
</ol>
<p>Bazel在本地文件系统维护外部存储库的缓存，默认位置在~/.cache/bazel/_bazel_$USER/cache/repos/v1/。可以使用选项--repository_cache指定不同的缓存位置。缓存可以被所有命名空间、所有Bazel版本共享</p>
<p><strong><span style="background-color: #c0c0c0;">避免下载</span></strong></p>
<p>你可以指定--distdir选项，其值是一个只读的目录，bazel会在目录中寻找文件，而非去网络上下载。匹配方式是URL中的Basename + 文件哈希。如果不指定哈希值，则Bazel不会去--distdir寻找文件</p>
</td>
</tr>
<tr>
<td class="blog_h3">mobile-install</td>
<td>在移动设备上安装目标</td>
</tr>
<tr>
<td class="blog_h3">query</td>
<td>执行依赖图查询</td>
</tr>
<tr>
<td class="blog_h3">run</td>
<td>运行指定的目标</td>
</tr>
<tr>
<td class="blog_h3">shutdown</td>
<td>关闭Bazel服务器</td>
</tr>
<tr>
<td class="blog_h3">test</td>
<td>构建并运行指定的测试目标</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/bazel-study-note">Bazel学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/bazel-study-note/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>基于Broadway的HTML5视频监控</title>
		<link>https://blog.gmem.cc/html5-vs-with-broadway</link>
		<comments>https://blog.gmem.cc/html5-vs-with-broadway#comments</comments>
		<pubDate>Mon, 09 Oct 2017 10:22:08 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[Multimedia]]></category>
		<category><![CDATA[视频监控]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16500</guid>
		<description><![CDATA[<p>简介 Broadway是一个基于JavaScript的H.264解码器，支持Baseline Profile，我们在HTML5视频监控技术预研一文中介绍过这个库。如果你的监控摄像头支持Baseline的H.264码流，利用Broadway可以实现不需要重新编码的视频监控，这样服务器的负载可以大大减轻。 本文不进行理论知识的讨论，仅仅给出一个简单的实现。此实现由三个部分组成： 基于live555的C++程序，用来从视频源取RTP流，解析出NALU然后通过WebSocket推送给WebSocket服务器 基于Spring Boot的Java WebSocket服务器，接收C++程序推送来的NALU并广播给客户端 基于Broadway的HTML5视频监控客户端，为了简化开发，我们使用了Broadway的一个封装http-live-player 代码托管于GitHub：https://github.com/gmemcc/h5vs.git C++部分 这部分主要是一个RTSP客户端，功能上面已经介绍过，此客户端依赖于我以前一篇文章中的live555 RTSP客户端封装。 WebSocket客户端 [crayon-69db8c96b7f91043841410/] [crayon-69db8c96b7f97998358898/] 主程序 [crayon-69db8c96b7f9a128076345/] Java部分 这部分实现了NALU转发功能，基于Spring Boot实现。 主程序 <a class="read-more" href="https://blog.gmem.cc/html5-vs-with-broadway">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/html5-vs-with-broadway">基于Broadway的HTML5视频监控</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">简介</span></div>
<p>Broadway是一个基于JavaScript的H.264解码器，支持Baseline Profile，我们在<a href="/research-on-html5-video-surveillance">HTML5视频监控技术预研</a>一文中介绍过这个库。如果你的监控摄像头支持Baseline的H.264码流，利用Broadway可以实现不需要重新编码的视频监控，这样服务器的负载可以大大减轻。</p>
<p>本文不进行理论知识的讨论，仅仅给出一个简单的实现。此实现由三个部分组成：</p>
<ol>
<li>基于live555的C++程序，用来从视频源取RTP流，解析出NALU然后通过WebSocket推送给WebSocket服务器</li>
<li>基于Spring Boot的Java WebSocket服务器，接收C++程序推送来的NALU并广播给客户端</li>
<li>基于Broadway的HTML5视频监控客户端，为了简化开发，我们使用了Broadway的一个封装http-live-player</li>
</ol>
<p>代码托管于GitHub：<a href="https://github.com/gmemcc/h5vs.git">https://github.com/gmemcc/h5vs.git</a></p>
<div class="blog_h2"><span class="graybg">C++部分</span></div>
<p>这部分主要是一个RTSP客户端，功能上面已经介绍过，此客户端依赖于我以前一篇文章中的<a href="/realtime-communication-protocols#rtsp-client-wrapper">live555 RTSP客户端封装</a>。</p>
<div class="blog_h3"><span class="graybg">WebSocket客户端</span></div>
<pre class="crayon-plain-tag">//
// Created by alex on 10/9/17.
//

#ifndef LIVE5555_WEBSOCKETCLIENT_H
#define LIVE5555_WEBSOCKETCLIENT_H

#include &lt;pthread.h&gt;

#include &lt;websocketpp/config/asio_no_tls_client.hpp&gt;
#include &lt;websocketpp/client.hpp&gt;

typedef websocketpp::client&lt;websocketpp::config::asio_client&gt; WebSocketppClient;
typedef websocketpp::connection_hdl WebSocketppConnHdl;

class WebSocketClient {
private:
    char *url;
    pthread_t wsThread;
    WebSocketppClient *wsppClient;
    WebSocketppConnHdl wsppConnHdl;
public:
    WebSocketClient( char *url );

    char *getUrl() const;

    virtual void connect();

    virtual void sendBytes( unsigned char *buf, unsigned size );

    virtual void sendText( char *text );

    virtual ~WebSocketClient();

    pthread_t getWsThread() const;

    WebSocketppClient *getWsppClient();

    void setWsppConnHdl( WebSocketppConnHdl wsppConnHdl );
};


#endif //LIVE5555_WEBSOCKETCLIENT_H</pre><br />
<pre class="crayon-plain-tag">//
// Created by alex on 10/9/17.
//

#include "WebSocketClient.h"

using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;

#include "spdlog/spdlog.h"

static auto LOGGER = spdlog::stdout_color_st( "WebSocketClient" );

WebSocketClient::WebSocketClient( char *url ) : url( url ), wsppClient( new WebSocketppClient()) {
}

WebSocketClient::~WebSocketClient() {
    delete wsppClient;
}

static void *wsRoutine( void *arg ) {
    WebSocketClient *client = (WebSocketClient *) arg;

    WebSocketppClient *wsppClient = client-&gt;getWsppClient();
    wsppClient-&gt;clear_access_channels( websocketpp::log::alevel::frame_header );
    wsppClient-&gt;clear_access_channels( websocketpp::log::alevel::frame_payload );
    wsppClient-&gt;init_asio();

    websocketpp::lib::error_code ec;
    WebSocketppClient::connection_ptr con = wsppClient-&gt;get_connection( std::string( client-&gt;getUrl()), ec );
    wsppClient-&gt;connect( con );
    client-&gt;setWsppConnHdl( con-&gt;get_handle());
    wsppClient-&gt;run();
}

void WebSocketClient::connect() {
    pthread_create( &amp;wsThread, NULL, wsRoutine, (void *) this );
}

void WebSocketClient::sendBytes( unsigned char *buf, unsigned size ) {
    wsppClient-&gt;send( wsppConnHdl, buf, size, websocketpp::frame::opcode::BINARY );
}

void WebSocketClient::sendText( char *text ) {
    wsppClient-&gt;send( wsppConnHdl, text, strlen( text ), websocketpp::frame::opcode::TEXT );
}

char *WebSocketClient::getUrl() const {
    return url;
}

pthread_t WebSocketClient::getWsThread() const {
    return wsThread;
}

WebSocketppClient *WebSocketClient::getWsppClient() {
    return wsppClient;
};

void WebSocketClient::setWsppConnHdl( WebSocketppConnHdl wsppConnHdl ) {
    this-&gt;wsppConnHdl = wsppConnHdl;
}</pre>
<div class="blog_h3"><span class="graybg">主程序</span></div>
<pre class="crayon-plain-tag">#include &lt;iostream&gt;
#include "live5555/client.h"

#include "spdlog/spdlog.h"

#include "WebSocketClient.h"

static auto LOGGER = spdlog::stdout_color_st( "wspush" );

class VideoSink : public SinkBase {
private:
#ifdef _SAVE_H264_SEQ
    FILE *os = fopen( "./rtsp.h264", "w" );
#endif
    WebSocketClient *wsClient;
    bool firstFrameWritten;
    const char *sPropParameterSetsStr;
    unsigned char const start_code[4] = { 0x00, 0x00, 0x00, 0x01 };
public:
    VideoSink( UsageEnvironment &amp;env, unsigned int recvBufSize, WebSocketClient *wsClient ) : SinkBase( env, recvBufSize ), wsClient( wsClient ) {
        // 缓冲区前面留出起始码4字节
        recvBuf += sizeof( start_code );
    }

    virtual ~VideoSink() {
    }

    virtual void onMediaSubsessionOpened( MediaSubsession *subSession ) {
        sPropParameterSetsStr = subSession-&gt;fmtp_spropparametersets();
    }

    void afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ) override {
        size_t scLen = sizeof( start_code );
        if ( !firstFrameWritten ) {
            // 填写起始码
            memcpy( recvBuf - scLen, start_code, scLen );
            // 防止RTSP源不送SPS/PPS
            unsigned numSPropRecords;
            SPropRecord *sPropRecords = parseSPropParameterSets( sPropParameterSetsStr, numSPropRecords );
            for ( unsigned i = 0; i &lt; numSPropRecords; ++i ) {
                unsigned int propLen = sPropRecords[ i ].sPropLength;
                size_t bufLen = propLen + scLen;
                unsigned char buf[bufLen];
                memcpy( buf, start_code, scLen );
                memcpy( buf + scLen, sPropRecords[ i ].sPropBytes, propLen );
                wsClient-&gt;sendBytes( buf, bufLen );
#ifdef _SAVE_H264_SEQ
                fwrite( buf, sizeof( unsigned char ), bufLen, os );
#endif
            }
            firstFrameWritten = true;
        }
#ifdef _SAVE_H264_SEQ
        fwrite( recvBuf - scLen, sizeof( unsigned char ), frameSize + scLen, os );
#endif
        unsigned naluHead = recvBuf[ 0 ];
        unsigned nri = naluHead &gt;&gt; 5;
        unsigned f = nri &gt;&gt; 2;
        unsigned type = naluHead &amp; 0b00011111;
        wsClient-&gt;sendBytes( recvBuf - scLen, frameSize + scLen );
        LOGGER-&gt;trace( "NALU info: nri {} type {}", nri, type );
        SinkBase::afterGettingFrame( frameSize, numTruncatedBytes, presentationTime );
    }
};

class H264RTSPClient : public RTSPClientBase {
private:
    VideoSink *videoSink;
public:
    H264RTSPClient( UsageEnvironment &amp;env, const char *rtspURL, VideoSink *videoSink ) :
        RTSPClientBase( env, rtspURL ), videoSink( videoSink ) {}

protected:
    // 测试用的摄像头（RTSP源）仅仅有一个子会话，因此这里简化了实现：
    bool acceptSubSession( const char *mediumName, const char *codec ) override {
        return true;
    }

    MediaSink *createSink( const char *mediumName, const char *codec, MediaSubsession *subSession ) override {
        videoSink-&gt;onMediaSubsessionOpened( subSession );
        return videoSink;
    }
};

int main() {
    spdlog::set_pattern( "%Y-%m-%d %H:%M:%S.%e [%l] [%n] %v" );
    spdlog::set_level( spdlog::level::trace );

    WebSocketClient *wsClient;
    wsClient = new WebSocketClient( "ws://192.168.0.89:9090/h264src" );
    wsClient-&gt;connect();
    sleep( 3 ); // 等待WebSocket连接建立
    wsClient-&gt;sendText( "ch1" );
    TaskScheduler *scheduler = BasicTaskScheduler::createNew();
    BasicUsageEnvironment *env = BasicUsageEnvironment::createNew( *scheduler );
    VideoSink *sink = new VideoSink( *env, 1024 * 1024, wsClient );
    H264RTSPClient *client = new H264RTSPClient( *env, "rtsp://admin:kingsmart123@192.168.0.196:554/ch1/sub/av_stream", sink );
    client-&gt;start();
    return 0;
}</pre>
<div class="blog_h2"><span class="graybg">Java部分</span></div>
<p>这部分实现了NALU转发功能，基于Spring Boot实现。</p>
<div class="blog_h3"><span class="graybg">主程序</span></div>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import org.kurento.client.KurentoClient;
import org.kurento.client.KurentoClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.*;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
import sun.security.acl.PrincipalImpl;

import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@SpringBootApplication
@EnableWebSocket
@EnableWebSocketMessageBroker
@EnableScheduling
public class VideoSurveillanceApp extends AbstractWebSocketMessageBrokerConfigurer implements WebSocketConfigurer {

    private static final Logger LOGGER = LoggerFactory.getLogger( VideoSurveillanceApp.class );

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        // WebSocket消息缓冲区大小，如果客户端发来的消息较大，需要按需调整
        container.setMaxTextMessageBufferSize( 1024 * 1024 );
        container.setMaxBinaryMessageBufferSize( 1024 * 1024 );
        return container;
    }

    @Override
    public void registerWebSocketHandlers( WebSocketHandlerRegistry registry ) {
        registry.addHandler( h264FrameSinkHandler(), "/h264sink" );
        registry.addHandler( h264FrameSrcHandler(), "/h264src" );
    }

    @Bean
    public WebSocketHandler h264FrameSrcHandler() {
        return new H264FrameSrcHandler();
    }

    @Bean
    public WebSocketHandler h264FrameSinkHandler() {
        return new H264FrameSinkHandler();
    }

    public static void main( String[] args ) {
        new SpringApplication( VideoSurveillanceApp.class ).run( args );
    }

}</pre>
<div class="blog_h3"><span class="graybg">H264FrameSrcHandler</span></div>
<p>此Bean接受C++程序的NALU推送：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

import javax.inject.Inject;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


public class H264FrameSrcHandler extends AbstractWebSocketHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger( H264FrameSrcHandler.class );

    private Map&lt;String, String&gt; sessionIdToChannel = new ConcurrentHashMap&lt;&gt;();

    @Inject
    private H264FrameSinkHandler sinkHandler;

    public void afterConnectionEstablished( WebSocketSession session ) throws Exception {
        LOGGER.debug( "{} connected.", session.getRemoteAddress() );
    }

    @Override
    protected void handleBinaryMessage( WebSocketSession session, BinaryMessage message ) throws Exception {
        ByteBuffer payload = message.getPayload();
        StringBuilder hex = new StringBuilder();
        byte[] pa = payload.array();
        int len = 16;
        if ( pa.length &lt; 16 ) len = pa.length;
        for ( byte i = 0; i &lt; len; i++ ) {
            hex.append( String.format( "%02x ",Byte.toUnsignedInt( pa[i] )  ) );
        }
        LOGGER.debug( "Received binary message {} bytes: {}...", payload.array().length, hex );
        String chnl = sessionIdToChannel.get( session.getId() );
        if ( chnl != null ) sinkHandler.broadcast( chnl, payload );
    }

    @Override
    protected void handleTextMessage( WebSocketSession session, TextMessage message ) throws Exception {
        String payload = message.getPayload();
        sessionIdToChannel.put( session.getId(), payload );
        LOGGER.debug( "Received text message: {}", payload );
    }
}</pre>
<div class="blog_h3"><span class="graybg">H264FrameSinkHandler</span></div>
<p>此Bean向Web客户端广播NALU：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.mutable.MutableInt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import javax.inject.Inject;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class H264FrameSinkHandler extends TextWebSocketHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger( H264FrameSinkHandler.class );

    public static final String ACTION_INIT = "init";

    private static final String ACTION_INIT_RESP = "initresp";

    public static final String ACTION_PLAY = "play";

    public static final String ACTION_STOP = "stop";

    public static final String KEY_ACTION = "action";


    @Inject
    private ObjectMapper om;

    private Map&lt;String, List&lt;WebSocketSession&gt;&gt; chnlToSessions = new ConcurrentHashMap&lt;&gt;();

    @Override
    protected void handleTextMessage( WebSocketSession session, TextMessage message ) throws Exception {
        String client = session.getId() + '@' + session.getRemoteAddress();
        Map req = om.readValue( message.getPayload(), Map.class );
        Map resp = new LinkedHashMap();
        Object action = req.get( KEY_ACTION );
        if ( ACTION_INIT.equals( action ) ) {
            String channel = (String) req.get( "channel" );
            LOGGER.debug( "{} request to subscribe channel {}", client, channel );
            addPushTarget( channel, session );

            resp.put( KEY_ACTION, ACTION_INIT_RESP );
            resp.put( "width", 352 );
            resp.put( "height", 288 );
            session.sendMessage( new TextMessage( om.writeValueAsString( resp ) ) );
        } else if ( ACTION_PLAY.equals( action ) ) {
            LOGGER.debug( "{} request to receive nalu push", session.getRemoteAddress(), client );
            session.getAttributes().put( ACTION_PLAY, true );
        }
    }

    private synchronized void addPushTarget( String channel, WebSocketSession session ) {
        List&lt;WebSocketSession&gt; sessions = chnlToSessions.get( channel );
        if ( sessions == null ) {
            sessions = new ArrayList&lt;&gt;();
            chnlToSessions.put( channel, sessions );
        }
        sessions.add( session );
    }

    public synchronized void broadcast( String chnl, ByteBuffer payload ) {
        List&lt;WebSocketSession&gt; sessions = chnlToSessions.get( chnl );
        if ( sessions == null ) return;
        sessions.forEach( sess -&gt; {
            try {
                if ( sess.isOpen() &amp;&amp; Boolean.TRUE.equals( sess.getAttributes().get( ACTION_PLAY ) ) ) {
                    sess.sendMessage( new BinaryMessage( payload ) );
                }
            } catch ( Exception e ) {
                LOGGER.error( e.getMessage(), e );
            }
        } );
    }

    @Scheduled( fixedRate = 10000 )
    public synchronized void cleanup() {
        final MutableInt counter = new MutableInt( 0 );
        chnlToSessions.values().forEach( sessions -&gt; {
            Iterator&lt;WebSocketSession&gt; it = sessions.listIterator();
            while ( it.hasNext() ) {
                if ( !it.next().isOpen() ) {
                    it.remove();
                    counter.increment();
                }
            }
        } );
        if ( counter.intValue() &gt; 0 ) LOGGER.debug( "Remove {} invalid websocket session.", counter );
    }
}</pre>
<div class="blog_h2"><span class="graybg">Web部分</span></div>
<p>我们对http-live-player进行了简单的修改，主要是修改其通信方式以配合上述WebSocket服务器。核心代码没有变动，因此这里不张贴其代码。</p>
<div class="blog_h3"><span class="graybg">客户端代码</span></div>
<pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Broadway Video Surveillance&lt;/title&gt;
    &lt;script src="js/broadway/http-live-player.js"&gt;&lt;/script&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class="nvbar"&gt;
    &lt;div class="title"&gt;基于Broadway+WebSocket的视频监控示例&lt;/div&gt;
    &lt;div class="subtitle"&gt;http://192.168.0.89:9090/broadway.html&lt;/div&gt;
&lt;/div&gt;
&lt;div class="videos-wrapper"&gt;
    &lt;div id="videos"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;script type="text/javascript"&gt;
    var videos = document.getElementById( 'videos' );
    for ( var i = 0; i &lt; 9; i++ ) {
        var canvas = document.createElement( "canvas" );
        videos.appendChild( canvas );
        var player = new WSAvcPlayer( canvas, "webgl", 'ch1', true );
        player.connect( "ws://" + document.location.host + "/h264sink" );
    }
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<div class="blog_h3"><span class="graybg">效果截图</span></div>
<p>下面的截图是开了九画面的视频监控，使用的是子码流，在测试机器上CPU压力不大。</p>
<p><img class="aligncenter size-full wp-image-16511" src="https://blog.gmem.cc/wp-content/uploads/2017/10/html5-h264.png" alt="html5-h264" width="798" height="697" /></p>
<p>注意：如果Broadway来不及解码，http-live-player会把缓冲区中的所有NALU全部丢弃，这可能导致暂时的花屏。选择适当的帧率、码率、画幅可以尽量避免这种情况的发生。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/html5-vs-with-broadway">基于Broadway的HTML5视频监控</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/html5-vs-with-broadway/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>基于C/C++的WebSocket库</title>
		<link>https://blog.gmem.cc/websocket-library-for-c-or-cpp</link>
		<comments>https://blog.gmem.cc/websocket-library-for-c-or-cpp#comments</comments>
		<pubDate>Tue, 19 Sep 2017 07:46:33 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[WebSocket]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16206</guid>
		<description><![CDATA[<p>libwebsockets 简介 libwebsockets是一个纯C语言的轻量级WebSocket库，它的CPU、内存占用很小，同时支持作为服务器端/客户端。其特性包括： 支持ws://和wss://协议 可以选择和OpenSSL、CyaSSL或者WolfSSL链接 轻量和高速，即使在每个线程处理多达250个连接的情况下 支持事件循环、零拷贝。支持poll()、libev（epoll）、libuv libwebsockets提供的API相当底层，实现简单的功能也需要相当冗长的代码。 构建 [crayon-69db8c96b854e476920470/] Echo示例 CMake项目配置 [crayon-69db8c96b8554745727619/] 客户端 [crayon-69db8c96b8556090920485/] 服务器 [crayon-69db8c96b855a032060634/] 封装 为了简化编程复杂度，应该考虑对libwebsockets进行适当封装。本节给出一个简单封装的例子。 客户端封装 [crayon-69db8c96b855d413059433/] 使用客户端封装 <a class="read-more" href="https://blog.gmem.cc/websocket-library-for-c-or-cpp">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/websocket-library-for-c-or-cpp">基于C/C++的WebSocket库</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">libwebsockets</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>libwebsockets是一个纯C语言的轻量级WebSocket库，它的CPU、内存占用很小，同时支持作为服务器端/客户端。其特性包括：</p>
<ol>
<li>支持ws://和wss://协议</li>
<li>可以选择和OpenSSL、CyaSSL或者WolfSSL链接</li>
<li>轻量和高速，即使在每个线程处理多达250个连接的情况下</li>
<li>支持事件循环、零拷贝。支持poll()、libev（epoll）、libuv</li>
</ol>
<p>libwebsockets提供的API相当底层，实现简单的功能也需要相当冗长的代码。</p>
<div class="blog_h2"><span class="graybg">构建</span></div>
<pre class="crayon-plain-tag">git clone git clone https://github.com/warmcat/libwebsockets.git
cd libwebsockets
mkdir build &amp;&amp; cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/home/alex/CPP/lib/libwebsockets ..
make &amp;&amp; make install </pre>
<div class="blog_h2"><span class="graybg">Echo示例</span></div>
<div class="blog_h3"><span class="graybg">CMake项目配置</span></div>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 2.8.9)
project(libws-study C)

include_directories(/home/alex/CPP/lib/libwebsockets/include)

set(CMAKE_CXX_FLAGS "-w -pthread")

set(SF_CLIENT client.c)
set(SF_SERVER server.c)

add_executable(client ${SF_CLIENT})
target_link_libraries(client /home/alex/CPP/lib/libwebsockets/lib/libwebsockets.so)


add_executable(server ${SF_SERVER})
target_link_libraries(server /home/alex/CPP/lib/libwebsockets/lib/libwebsockets.so)</pre>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<pre class="crayon-plain-tag">#include "libwebsockets.h"
#include &lt;signal.h&gt;

static volatile int exit_sig = 0;
#define MAX_PAYLOAD_SIZE  10 * 1024

void sighdl( int sig ) {
    lwsl_notice( "%d traped", sig );
    exit_sig = 1;
}

/**
 * 会话上下文对象，结构根据需要自定义
 */
struct session_data {
    int msg_count;
    unsigned char buf[LWS_PRE + MAX_PAYLOAD_SIZE];
    int len;
};

/**
 * 某个协议下的连接发生事件时，执行的回调函数
 *
 * wsi：指向WebSocket实例的指针
 * reason：导致回调的事件
 * user 库为每个WebSocket会话分配的内存空间
 * in 某些事件使用此参数，作为传入数据的指针
 * len 某些事件使用此参数，说明传入数据的长度
 */
int callback( struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len ) {
    struct session_data *data = (struct session_data *) user;
    switch ( reason ) {
        case LWS_CALLBACK_CLIENT_ESTABLISHED:   // 连接到服务器后的回调
            lwsl_notice( "Connected to server\n" );
            break;

        case LWS_CALLBACK_CLIENT_RECEIVE:       // 接收到服务器数据后的回调，数据为in，其长度为len
            lwsl_notice( "Rx: %s\n", (char *) in );
            break;
        case LWS_CALLBACK_CLIENT_WRITEABLE:     // 当此客户端可以发送数据时的回调
            if ( data-&gt;msg_count &lt; 3 ) {
                // 前面LWS_PRE个字节必须留给LWS
                memset( data-&gt;buf, 0, sizeof( data-&gt;buf ));
                char *msg = (char *) &amp;data-&gt;buf[ LWS_PRE ];
                data-&gt;len = sprintf( msg, "你好 %d", ++data-&gt;msg_count );
                lwsl_notice( "Tx: %s\n", msg );
                // 通过WebSocket发送文本消息
                lws_write( wsi, &amp;data-&gt;buf[ LWS_PRE ], data-&gt;len, LWS_WRITE_TEXT );
            }
            break;
    }
    return 0;
}

/**
 * 支持的WebSocket子协议数组
 * 子协议即JavaScript客户端WebSocket(url, protocols)第2参数数组的元素
 * 你需要为每种协议提供回调函数
 */
struct lws_protocols protocols[] = {
    {
        //协议名称，协议回调，接收缓冲区大小
        "", callback, sizeof( struct session_data ), MAX_PAYLOAD_SIZE,
    },
    {
        NULL, NULL,   0 // 最后一个元素固定为此格式
    }
};

int main() {
    // 信号处理函数
    signal( SIGTERM, sighdl );

    // 用于创建vhost或者context的参数
    struct lws_context_creation_info ctx_info = { 0 };
    ctx_info.port = CONTEXT_PORT_NO_LISTEN;
    ctx_info.iface = NULL;
    ctx_info.protocols = protocols;
    ctx_info.gid = -1;
    ctx_info.uid = -1;

    // 创建一个WebSocket处理器
    struct lws_context *context = lws_create_context( &amp;ctx_info );

    char *address = "192.168.0.89";
    int port = 9090;
    char addr_port[256] = { 0 };
    sprintf( addr_port, "%s:%u", address, port &amp; 65535 );

    // 客户端连接参数
    struct lws_client_connect_info conn_info = { 0 };
    conn_info.context = context;
    conn_info.address = address;
    conn_info.port = port;
    conn_info.ssl_connection = 0;
    conn_info.path = "/h264src";
    conn_info.host = addr_port;
    conn_info.origin = addr_port;
    conn_info.protocol = protocols[ 0 ].name;

    // 下面的调用触发LWS_CALLBACK_PROTOCOL_INIT事件
    // 创建一个客户端连接
    struct lws *wsi = lws_client_connect_via_info( &amp;conn_info );
    while ( !exit_sig ) {
        // 执行一次事件循环（Poll），最长等待1000毫秒
        lws_service( context, 1000 );
        /**
         * 下面的调用的意义是：当连接可以接受新数据时，触发一次WRITEABLE事件回调
         * 当连接正在后台发送数据时，它不能接受新的数据写入请求，所有WRITEABLE事件回调不会执行
         */
        lws_callback_on_writable( wsi );
    }
    // 销毁上下文对象
    lws_context_destroy( context );

    return 0;
}</pre>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<pre class="crayon-plain-tag">static int protocol0_callback( struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len ) {
    struct session_data *data = (struct session_data *) user;
    switch ( reason ) {
        case LWS_CALLBACK_ESTABLISHED:       // 当服务器和客户端完成握手后
            break;
        case LWS_CALLBACK_RECEIVE:           // 当接收到客户端发来的帧以后
            // 判断是否最后一帧
            data-&gt;fin = lws_is_final_fragment( wsi );
            // 判断是否二进制消息
            data-&gt;bin = lws_frame_is_binary( wsi );
            // 对服务器的接收端进行流量控制，如果来不及处理，可以控制之
            // 下面的调用禁止在此连接上接收数据
            lws_rx_flow_control( wsi, 0 );

            // 业务处理部分，为了实现Echo服务器，把客户端数据保存起来
            memcpy( &amp;data-&gt;buf[ LWS_PRE ], in, len );
            data-&gt;len = len;

            // 需要给客户端应答时，触发一次写回调
            lws_callback_on_writable( wsi );
            break;
        case LWS_CALLBACK_SERVER_WRITEABLE:   // 当此连接可写时
            lws_write( wsi, &amp;data-&gt;buf[ LWS_PRE ], data-&gt;len, LWS_WRITE_TEXT );
            // 下面的调用允许在此连接上接收数据
            lws_rx_flow_control( wsi, 1 );
            break;
    }
    // 回调函数最终要返回0，否则无法创建服务器
    return 0;
}

int main() {
    // 信号处理函数
    signal( SIGTERM, sighdl );

    struct lws_context_creation_info ctx_info = { 0 };
    ctx_info.port = 9090;
    ctx_info.iface = NULL; // 在所有网络接口上监听
    ctx_info.protocols = protocols;
    ctx_info.gid = -1;
    ctx_info.uid = -1;
    ctx_info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
    struct lws_context *context = lws_create_context( &amp;ctx_info );
    while ( !exit_sig ) {
        lws_service( context, 1000 );
    }
    lws_context_destroy( context );
}</pre>
<div class="blog_h2"><span class="graybg">封装</span></div>
<p>为了简化编程复杂度，应该考虑对libwebsockets进行适当封装。本节给出一个简单封装的例子。</p>
<div class="blog_h3"><span class="graybg">客户端封装</span></div>
<pre class="crayon-plain-tag">#ifndef LIVE555_WSCLIENT_H
#define LIVE555_WSCLIENT_H

#include "libwebsockets.h"

#ifndef LWS_MAX_PAYLOAD_SIZE
#define LWS_MAX_PAYLOAD_SIZE  1024 * 1024
#endif

#ifndef SPDLOG_CONST
#define SPDLOG_CONST
const auto LOGGER = spdlog::stdout_color_st( "console" );
#endif

/**
 * 通用回调函数签名
 */
typedef void (*lws_callback)( struct lws *wsi, void *user, void *in, size_t len );

// 用户数据对象
typedef struct lws_user_data {
    // 缓冲区
    unsigned char *buf;
    // 缓冲区有效字节数
    int len;
    // 用户自定义数据
    void *user;
    // 读写缓冲区之前需要加锁
    volatile bool locked;
    // 指示当前缓冲区的数据的重要性，如果为真，发送之前不得被覆盖
    volatile bool critical;
    // 本次数据发送类型
    lws_write_protocol type;
    // 回调函数
    lws_callback esta_callback;
    lws_callback recv_callback;
    lws_callback writ_callback;
};

void writ_callback_send_buf( struct lws *wsi, void *user, void *in, size_t len ) {
    struct lws_user_data *data = (struct lws_user_data *) user;
    if ( __sync_bool_compare_and_swap( &amp;data-&gt;locked, 0, 1 )) {
        unsigned char *buf;
        char hex[128]= { 0 };
        int writ_count;

        int len = data-&gt;len;
        if ( len == 0 ) goto cleanup;

        buf = data-&gt;buf + LWS_PRE;
        writ_count = lws_write( wsi, buf, len, data-&gt;type );
        if ( data-&gt;type == LWS_WRITE_BINARY ) {
            char *phex = hex;
            for ( int i = 0; i &lt; 16; i++ ) {
                unsigned char c = *buf++;
                sprintf( phex, "%02x ", c );
                phex += 3;
            }
        }
        LOGGER-&gt;debug( "lws_write {} bytes: {}...", writ_count, hex );
        cleanup:
        data-&gt;locked = 0;
        data-&gt;critical = 0;
        data-&gt;len = 0;
    }
}

static int lws_protocol_0_callback( struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len ) {
    struct lws_user_data *data = (struct lws_user_data *) user;
    switch ( reason ) {
        case LWS_CALLBACK_CLIENT_ESTABLISHED:
            if ( data-&gt;esta_callback )data-&gt;esta_callback( wsi, user, in, len );
            break;
        case LWS_CALLBACK_CLIENT_RECEIVE:
            if ( data-&gt;recv_callback )data-&gt;recv_callback( wsi, user, in, len );
            break;
        case LWS_CALLBACK_CLIENT_WRITEABLE:
            if ( data-&gt;writ_callback )data-&gt;writ_callback( wsi, user, in, len );
            break;
    }
    return 0;
}

typedef struct lws_client {
    struct lws *wsi;
    struct lws_context *context;
    lws_user_data *data;
    int *cycle;

    // 连接参数
    char *address;
    char *path;
    int port;

    void (*fill_buf)( lws_client *client, void *buf, int len, lws_write_protocol type );

    void (*fire_writable)( lws_client *client );
};

void fill_buf( lws_client *client, void *buf, int len, lws_write_protocol type ) {
    lws_user_data *data = client-&gt;data;
    data-&gt;type = type;
    data-&gt;len = len;
    memcpy( data-&gt;buf + LWS_PRE, buf, len );
}

void fire_writable( lws_client *client ) {
    lws_callback_on_writable( client-&gt;wsi );
    // 停止当前事件循环等待
    lws_cancel_service( client-&gt;context );
}

void *lws_service_thread_func( void *arg ) {
    lws_client *client = (lws_client *) arg;

    struct lws_context_creation_info ctx_info = { 0 };
    ctx_info.port = CONTEXT_PORT_NO_LISTEN;
    ctx_info.iface = NULL;
    const struct lws_protocols protocols[] = {
        {
            "", lws_protocol_0_callback, sizeof( struct lws_user_data ), LWS_MAX_PAYLOAD_SIZE, 0, 0, LWS_MAX_PAYLOAD_SIZE
        },
        {
            NULL, NULL,                  0
        }
    };
    static const struct lws_extension exts[] = {
        {
            "permessage-deflate",
            lws_extension_callback_pm_deflate,
            "permessage-deflate; client_no_context_takeover; client_max_window_bits"
        },
        { NULL, NULL, NULL /* terminator */ }
    };
    ctx_info.protocols = protocols;
    ctx_info.extensions = exts;
    ctx_info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
    ctx_info.gid = -1;
    ctx_info.uid = -1;

    struct lws_context *context = lws_create_context( &amp;ctx_info );
    client-&gt;context = context;

    char addr_port[256] = { 0 };
    sprintf( addr_port, "%s:%u", client-&gt;address, client-&gt;port &amp; 65535 );

    struct lws_client_connect_info conn_info = { 0 };
    conn_info.context = context;
    conn_info.address = client-&gt;address;
    conn_info.port = client-&gt;port;
    conn_info.ssl_connection = 0;
    conn_info.path = client-&gt;path;
    conn_info.host = addr_port;
    conn_info.origin = addr_port;
    conn_info.protocol = protocols[ 0 ].name;
    // 用户数据对象由调用者提供，因为需要提供回调
    conn_info.userdata = client-&gt;data;

    struct lws *wsi = lws_client_connect_via_info( &amp;conn_info );
    client-&gt;wsi = wsi;

    int *loop_cycle = client-&gt;cycle;
    int cycle = *loop_cycle;
    while ( *loop_cycle &gt;= 0 ) {
        lws_service( context, cycle );
    }
    lws_context_destroy( context );
}

/**
 * 连接到WebSocket服务器
 * @param address  IP地址
 * @param path  上下文路径URL
 * @param port 端口
 * @param data 用户数据
 * @param loop_cycle 事件循环周期，如果大于等于0则启动事件循环，后续将其置为-1则导致循环终止
 * @return
 */
lws_client *lws_connect( char *address, char *path, int port, lws_user_data *data, int loop_cycle ) {
    lws_client *client = (lws_client *) malloc( sizeof( lws_client ));
    client-&gt;data = data;
    client-&gt;cycle = (int *) malloc( sizeof( int ));
    *client-&gt;cycle = loop_cycle;
    client-&gt;address = address;
    client-&gt;path = path;
    client-&gt;port = port;
    client-&gt;fill_buf = fill_buf;
    client-&gt;fire_writable = fire_writable;
    pthread_t *lws_service_thread = (pthread_t *) malloc( sizeof( pthread_t ));
    pthread_create( lws_service_thread, NULL, lws_service_thread_func, client );
    return client;

}

#endif</pre>
<div class="blog_h3"><span class="graybg">使用客户端封装</span></div>
<pre class="crayon-plain-tag">// 创建用户数据对象
lws_user_data *data = new lws_user_data();
data-&gt;buf = new unsigned char[LWS_PRE + LWS_MAX_PAYLOAD_SIZE];
data-&gt;writ_callback = writ_callback_send_buf_bin;  // 注册回调

// 创建客户端
lws_client *ws_client = lws_connect( "192.168.0.89", "/h264src", 9090, data, 10 );

// 发送数据，需要同步
lws_user_data *data = client-&gt;data;
// GCC内置CAS语义
if ( __sync_bool_compare_and_swap( &amp;data-&gt;locked, 0, 1 )) {
    client-&gt;fill_buf( client, sink-&gt;recvBuf, frameSize );
    client-&gt;fire_writable( client );
    data-&gt;locked = 0;
}</pre>
<div class="blog_h2"><span class="graybg">常见问题</span></div>
<div class="blog_h3"><span class="graybg">error on reading from skt : 104</span></div>
<p><a href="/network-faq#skt-enos">错误代码104</a>的含义是连接被重置，我遇到这个问题的原因是，Spring的WebSocket消息缓冲区大小不足。</p>
<div class="blog_h1"><span class="graybg">WebSocket++</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>WebSocket++是一个仅仅由头文件构成的C++库，它实现了WebSocket协议（RFC6455），通过它，你可以在C++项目中使用WebSocket客户端或者服务器。</p>
<p>WebSocket++使用两个可以相互替换的网络传输模块，其中一个基于C++ I/O流，另一个基于Asio。</p>
<p>WebSocket++的主要特性包括：</p>
<ol>
<li>事件驱动的接口</li>
<li>支持WSS、IPv6</li>
<li>灵活的依赖管理 —— Boost或者C++ 11标准库</li>
<li>可移植性：Posix/Windows、32/64bit、Intel/ARM/PPC</li>
<li>线程安全</li>
</ol>
<div class="blog_h2"><span class="graybg">构建</span></div>
<pre class="crayon-plain-tag">git clone https://github.com/zaphoyd/websocketpp.git
cd websocketpp
mkdir build &amp;&amp; cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/home/alex/CPP/lib/websocketpp ..
make &amp;&amp; make install </pre>
<div class="blog_h2"><span class="graybg">Echo示例</span></div>
<div class="blog_h3"><span class="graybg">CMake项目配置</span></div>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 3.6)
project(websocket__)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS "-pthread")
add_definitions(-D_WEBSOCKETPP_CPP11_FUNCTIONAL_)
add_definitions(-D_WEBSOCKETPP_CPP11_THREAD_)
add_definitions(-D_WEBSOCKETPP_CPP11_SYSTEM_ERROR_)
add_definitions(-D_WEBSOCKETPP_CPP11_MEMORY_)


include_directories(/home/alex/CPP/lib/websocketpp/include /home/alex/CPP/lib/boost/1.65.1/include/)

set(SF_CLIENT client.cpp)
add_executable(client ${SF_CLIENT})
target_link_libraries(client /home/alex/CPP/lib/boost/1.65.1/lib/libboost_system.so)

set(SF_SERVER server.cpp)
add_executable(server ${SF_SERVER})
target_link_libraries(server /home/alex/CPP/lib/boost/1.65.1/lib/libboost_system.so)</pre>
<div class="blog_h3"><span class="graybg">客户端 </span></div>
<pre class="crayon-plain-tag">#include &lt;websocketpp/config/asio_no_tls_client.hpp&gt;

#include &lt;websocketpp/client.hpp&gt;

#include &lt;iostream&gt;

typedef websocketpp::client&lt;websocketpp::config::asio_client&gt; client;

using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;

// 消息指针
typedef websocketpp::config::asio_client::message_type::ptr message_ptr;

// 打开连接时的回调
void on_open( client *c, websocketpp::connection_hdl hdl ) {
    std::string msg = "Hello 1";
    // 发送文本消息
    c-&gt;send( hdl, msg, websocketpp::frame::opcode::text );
    c-&gt;get_alog().write( websocketpp::log::alevel::app, "Tx: " + msg );

}

// 连接失败时的回调
void on_fail( client *c, websocketpp::connection_hdl hdl ) {
    c-&gt;get_alog().write( websocketpp::log::alevel::app, "Connection Failed" );
}

// 接收到服务器发来的WebSocket消息后的回调
void on_message( client *c, websocketpp::connection_hdl hdl, message_ptr msg ) {
    c-&gt;get_alog().write( websocketpp::log::alevel::app, "Rx: " + msg-&gt;get_payload());
    // 关闭连接，导致事件循环退出
    c-&gt;close( hdl, websocketpp::close::status::normal, "" );
}

// 关闭连接时的回调
void on_close( client *c, websocketpp::connection_hdl hdl ) {
}

int main( int argc, char *argv[] ) {
    client echo_client;

    // 调整日志策略
    echo_client.clear_access_channels( websocketpp::log::alevel::frame_header );
    echo_client.clear_access_channels( websocketpp::log::alevel::frame_payload );

    std::string uri = "ws://192.168.0.89:9090/h264src";

    try {
        // 初始化ASIO ASIO
        echo_client.init_asio();

        // 注册回调函数
        echo_client.set_open_handler( std::bind( &amp;on_open, &amp;echo_client, ::_1 ));
        echo_client.set_fail_handler( std::bind( &amp;on_fail, &amp;echo_client, ::_1 ));
        echo_client.set_message_handler( std::bind( &amp;on_message, &amp;echo_client, ::_1, ::_2 ));
        echo_client.set_close_handler( std::bind( &amp;on_close, &amp;echo_client, ::_1 ));

        // 在事件循环启动前创建一个连接对象
        websocketpp::lib::error_code ec;
        client::connection_ptr con = echo_client.get_connection( uri, ec );
        echo_client.connect( con );
        con-&gt;get_handle(); // 连接句柄，发送消息时必须要传入

        // 启动事件循环（ASIO的io_service），当前线程阻塞
        echo_client.run();
    } catch ( const std::exception &amp;e ) {
        std::cout &lt;&lt; e.what() &lt;&lt; std::endl;
    } catch ( websocketpp::lib::error_code e ) {
        std::cout &lt;&lt; e.message() &lt;&lt; std::endl;
    } catch ( ... ) {
        std::cout &lt;&lt; "other exception" &lt;&lt; std::endl;
    }
}</pre>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<pre class="crayon-plain-tag">#include &lt;iostream&gt;

#include &lt;websocketpp/config/asio_no_tls.hpp&gt;
#include &lt;websocketpp/server.hpp&gt;

typedef websocketpp::server&lt;websocketpp::config::asio&gt; server;
typedef websocketpp::config::asio::message_type::ptr message_ptr;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;

void on_open( server *s, websocketpp::connection_hdl hdl ) {
    // 根据连接句柄获得连接对象
    server::connection_ptr con = s-&gt;get_con_from_hdl( hdl );
    // 获得URL路径
    std::string path = con-&gt;get_resource();
    s-&gt;get_alog().write( websocketpp::log::alevel::app, "Connected to path " + path );
}

void on_message( server *s, websocketpp::connection_hdl hdl, message_ptr msg ) {
    s-&gt;send( hdl, msg-&gt;get_payload(), websocketpp::frame::opcode::text );
}

int main() {
    server echo_server;
    // 调整日志策略
    echo_server.set_access_channels( websocketpp::log::alevel::all );
    echo_server.clear_access_channels( websocketpp::log::alevel::frame_payload );

    try {
        echo_server.init_asio();

        echo_server.set_open_handler( bind( &amp;on_open, &amp;echo_server, ::_1 ));
        echo_server.set_message_handler( bind( &amp;on_message, &amp;echo_server, ::_1, ::_2 ));
        // 在所有网络接口的9090上监听
        echo_server.listen( 9090 );

        // 启动服务器端Accept事件循环
        echo_server.start_accept();

        // 启动事件循环（ASIO的io_service），当前线程阻塞
        echo_server.run();
    } catch ( websocketpp::exception const &amp;e ) {
        std::cout &lt;&lt; e.what() &lt;&lt; std::endl;
    } catch ( ... ) {
        std::cout &lt;&lt; "other exception" &lt;&lt; std::endl;
    }
}</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/websocket-library-for-c-or-cpp">基于C/C++的WebSocket库</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/websocket-library-for-c-or-cpp/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>C++日志组件spdlog</title>
		<link>https://blog.gmem.cc/spdlog</link>
		<comments>https://blog.gmem.cc/spdlog#comments</comments>
		<pubDate>Tue, 12 Sep 2017 06:49:38 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[LOG]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16071</guid>
		<description><![CDATA[<p>基础 spdlog是基于C++ 11的日志组件，它非常轻量，使用时你仅仅需要引入头文件就可以了。 线程安全 名字空间[crayon-69db8c96b8bae979337557-i/]之下的大多数函数都是线程安全的，除了： [crayon-69db8c96b8bb3512940902/] 日志器对象的大部分方法也是线程安全的，除了： [crayon-69db8c96b8bb5560891658/] 所有以_mt结尾的SINK都是线程安全的，以_st结尾的则不是。  代码示例 [crayon-69db8c96b8bb7477827112/] 输出格式 spdlog默认的输出格式为： [crayon-69db8c96b8bba232283847/] 要定制输出格式，可以调用： [crayon-69db8c96b8bbd155525853/] 或者实现自己的格式化器： [crayon-69db8c96b8bbf533545862/] Pattern说明 输出格式的Pattern中可以有若干[crayon-69db8c96b8bc1916584747-i/]开头的标记，含义如下表： 标记 说明 <a class="read-more" href="https://blog.gmem.cc/spdlog">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/spdlog">C++日志组件spdlog</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">基础</span></div>
<p><a href="https://github.com/gabime/spdlog">spdlog</a>是基于C++ 11的日志组件，它非常轻量，使用时你仅仅需要引入头文件就可以了。</p>
<div class="blog_h3"><span class="graybg">线程安全</span></div>
<p>名字空间<pre class="crayon-plain-tag">spdlog::</pre>之下的大多数函数都是线程安全的，除了：</p>
<pre class="crayon-plain-tag">void spdlog::set_pattern(const std::string&amp;);
void spdlog::set_formatter(formatter_ptr);
void spdlog::set_error_handler(log_err_handler);</pre>
<p>日志器对象的大部分方法也是线程安全的，除了：</p>
<pre class="crayon-plain-tag">void spdlog::logger::set_pattern(const std::string&amp;);
void spdlog::logger::set_formatter(formatter_ptr);
void spdlog::set_error_handler(log_err_handler);</pre>
<p>所有以_mt结尾的SINK都是线程安全的，以_st结尾的则不是。 </p>
<div class="blog_h3"><span class="graybg">代码示例</span></div>
<pre class="crayon-plain-tag">#include "spdlog/spdlog.h"
#include &lt;iostream&gt;

// 多线程的基于控制台（stdout）的日志记录器，支持高亮。类似的stdout_color_st是单线程版本
auto console = spdlog::stdout_color_mt( "console" );
// 基于文件的简单日志
auto logger = spdlog::basic_logger_mt("basic_logger", "logs/basic.txt");
// 基于滚动文件的日志，每个文件5MB，三个文件
auto logger = spdlog::rotating_logger_mt("file_logger", "myfilename", 1024 * 1024 * 5, 3);

// 定制输出格式
spdlog::set_pattern("*** [%H:%M:%S %z] [thread %t] %v ***");

// 多个日志器共享SINK
auto daily_sink = std::make_shared&lt;spdlog::sinks::daily_file_sink_mt&gt;("logfile", 23, 59);
// 下面几个同步日志器共享的输出到目标文件
auto net_logger = std::make_shared&lt;spdlog::logger&gt;("net", daily_sink);
auto hw_logger = std::make_shared&lt;spdlog::logger&gt;("hw", daily_sink);
auto db_logger = std::make_shared&lt;spdlog::logger&gt;("db", daily_sink); 

// 一个日志器使用多个SINK
std::vector&lt;spdlog::sink_ptr&gt; sinks;
sinks.push_back( std::make_shared&lt;spdlog::sinks::stdout_sink_st&gt;());
sinks.push_back( std::make_shared&lt;spdlog::sinks::daily_file_sink_st&gt;( "logfile", 23, 59 ));
auto combined_logger = std::make_shared&lt;spdlog::logger&gt;( "name", begin( sinks ), end( sinks ));
spdlog::register_logger( combined_logger );

// 异步
// 每个日志器分配8192长度的队列，队列长度必须2的幂
spdlog::set_async_mode(8192); 
// 程序退出前清理
spdlog::drop_all();

// 注册日志器
spdlog::register_logger(net_logger);
// 注册后，其它代码可以根据名称获得日志器
auto logger = spdlog::get(net_logger);

// 记录日志
// 设置最低级别
console-&gt;set_level(spdlog::level::debug);
console-&gt;debug("Hello World") ;
// 使用占位符
console-&gt;info("Hello {}" ,"World"); 
// 带格式化的占位符：d整数，x十六进制，o八进制，b二进制                
console-&gt;warn("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42);
// 带格式化的占位符：f浮点数
console-&gt;info("Support for floats {:03.2f}", 1.23456);
// 左对齐，保证30字符宽度
console-&gt;error("{:&lt;30}", "left aligned");
// 指定占位符位置序号
console-&gt;info("Positional args are {1} {0}..", "too", "supported");

// 记录自定义类型，需要重载&lt;&lt;操作符
#include &lt;spdlog/fmt/ostr.h&gt; 
class Duck{}
std::ostream&amp; operator&lt;&lt;(std::ostream&amp; os, const Duck&amp; duck){ 
    return os &lt;&lt; duck.getName(); 
}
Duck duck;
console-&gt;info("custom class with operator&lt;&lt;: {}..", duck);</pre>
<div class="blog_h2"><span class="graybg">输出格式</span></div>
<p>spdlog默认的输出格式为：</p>
<pre class="crayon-plain-tag">[2014-31-10 23:46:59.678] [info] [my_loggername] Some message</pre>
<p>要定制输出格式，可以调用：</p>
<pre class="crayon-plain-tag">spdlog::set_pattern(pattern_string);
// 示例
spdlog::set_pattern("*** [%H:%M:%S %z] [thread %t] %v ***");</pre>
<p>或者实现自己的格式化器：</p>
<pre class="crayon-plain-tag">spdlog::set_formatter(std::make_shared&lt;my_custom_formatter&gt;());</pre>
<div class="blog_h3"><span class="graybg">Pattern说明</span></div>
<p>输出格式的Pattern中可以有若干<pre class="crayon-plain-tag">%</pre>开头的标记，含义如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">标记</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>%v</td>
<td>实际需要被日志记录的文本，如果文本中有{占位符}会被替换</td>
</tr>
<tr>
<td>%t</td>
<td>线程标识符</td>
</tr>
<tr>
<td>%P</td>
<td>进程标识符</td>
</tr>
<tr>
<td>%n</td>
<td>日志记录器名称</td>
</tr>
<tr>
<td>%l</td>
<td>日志级别</td>
</tr>
<tr>
<td>%L</td>
<td>日志级别简写</td>
</tr>
<tr>
<td>%a</td>
<td>简写的周几，例如Thu</td>
</tr>
<tr>
<td>%A</td>
<td>周几，例如Thursday</td>
</tr>
<tr>
<td>%b</td>
<td>简写的月份，例如Aug</td>
</tr>
<tr>
<td>%B</td>
<td>月份，例如August</td>
</tr>
<tr>
<td>%c</td>
<td>日期时间，例如Thu Aug 23 15:35:46 2014</td>
</tr>
<tr>
<td>%C</td>
<td>两位年份，例如14</td>
</tr>
<tr>
<td>%Y</td>
<td>四位年份，例如2014</td>
</tr>
<tr>
<td>%D 或 %x</td>
<td>MM/DD/YY格式日期，例如"08/23/14</td>
</tr>
<tr>
<td>%m</td>
<td>月份，1-12之间</td>
</tr>
<tr>
<td>%d</td>
<td>月份中的第几天，1-31之间</td>
</tr>
<tr>
<td>%H</td>
<td>24小时制的小时，0-23之间</td>
</tr>
<tr>
<td>%I</td>
<td>12小时制的小时，1-12之间</td>
</tr>
<tr>
<td>%M</td>
<td>分钟，0-59</td>
</tr>
<tr>
<td>%S</td>
<td>秒，0-59</td>
</tr>
<tr>
<td>%e</td>
<td>当前秒内的毫秒，0-999</td>
</tr>
<tr>
<td>%f</td>
<td>当前秒内的微秒，0-999999</td>
</tr>
<tr>
<td>%F</td>
<td>当前秒内的纳秒， 0-999999999</td>
</tr>
<tr>
<td>%p</td>
<td>AM或者PM</td>
</tr>
<tr>
<td>%r</td>
<td>12小时时间，例如02:55:02 pm</td>
</tr>
<tr>
<td>%R</td>
<td>等价于%H:%M，例如23:55</td>
</tr>
<tr>
<td>%T 或 %X</td>
<td>HH:MM:SS</td>
</tr>
<tr>
<td>%z</td>
<td>时区UTC偏移，例如+02:00</td>
</tr>
<tr>
<td>%+</td>
<td>表示默认格式</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/spdlog">C++日志组件spdlog</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/spdlog/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>实时通信协议族</title>
		<link>https://blog.gmem.cc/realtime-communication-protocols</link>
		<comments>https://blog.gmem.cc/realtime-communication-protocols#comments</comments>
		<pubDate>Sat, 09 Sep 2017 04:16:11 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[Multimedia]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16006</guid>
		<description><![CDATA[<p>网络和多媒体 实时性问题 因特网最初设计用于数据的传输，例如文件、电子邮件。那时的语音传输由专门的电话网络负责。随着节点数量的增加、网络带宽的提高，基于因特网的多媒体需求逐渐出现，包括在线影视、在线视频会议。为了响应这类需求，研究人员开发出了专门的协议，包括： 实时传输协议（Realtime Transmission Protocol，RTP），用于传输媒体 RTP的控制部分：实时传输控制协议（Realtime Transmission Control Protocol，RTCP） 实时流协议（Realtime Streaming Protocol） 注意：RTP这个术语有时候指RTP协议标准，有时候则指RTP协议标准中的RTP部分（不包含RTCP）。 我们都知道，TCP/IP协议族的基础是IP协议，此协议能很好的处理包的路由递送，但是却无法防止丢包、控制延迟。但是此协议让路由器逻辑很简单、网络易于扩容，这是因特网能繁荣的基础。 为了增强端到端的可靠性，TCP协议被引入，这是因特网上最广泛使用的协议。TCP能够自动重发丢失的包，并且保证包的顺序，TCP还提供了拥塞控制机制。 TCP的某些特性，在用来传递多媒体时，反而成为了障碍，原因是： 很多多媒体应用，例如视频监控，对延迟非常敏感 多媒体传输可以容忍某些丢包情况，其质量不会受到影响 因此，很多多媒体传输都是在UDP协议之上进行的。 多媒体应用程序可以分为两个类别： 交互式应用。例如视频会议、VoIP <a class="read-more" href="https://blog.gmem.cc/realtime-communication-protocols">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/realtime-communication-protocols">实时通信协议族</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">网络和多媒体</span></div>
<div class="blog_h2"><span class="graybg">实时性问题</span></div>
<p>因特网最初设计用于数据的传输，例如文件、电子邮件。那时的语音传输由专门的电话网络负责。随着节点数量的增加、网络带宽的提高，基于因特网的多媒体需求逐渐出现，包括在线影视、在线视频会议。为了响应这类需求，研究人员开发出了专门的协议，包括：</p>
<ol>
<li>实时传输协议（Realtime Transmission Protocol，RTP），用于传输媒体</li>
<li>RTP的控制部分：实时传输控制协议（Realtime Transmission Control Protocol，RTCP）</li>
<li>实时流协议（Realtime Streaming Protocol）</li>
</ol>
<p>注意：RTP<span style="background-color: #c0c0c0;">这个术语有时候指RTP协议标准，有时候则指RTP协议标准中的RTP部分</span>（不包含RTCP）。</p>
<p>我们都知道，TCP/IP协议族的基础是IP协议，此协议能很好的处理包的路由递送，但是却无法防止丢包、控制延迟。但是此协议让路由器逻辑很简单、网络易于扩容，这是因特网能繁荣的基础。</p>
<p>为了增强端到端的可靠性，TCP协议被引入，这是因特网上最广泛使用的协议。TCP能够自动重发丢失的包，并且保证包的顺序，TCP还提供了拥塞控制机制。</p>
<p>TCP的某些特性，在用来传递多媒体时，反而成为了障碍，原因是：</p>
<ol>
<li>很多多媒体应用，例如视频监控，对延迟非常敏感</li>
<li>多媒体传输可以容忍某些丢包情况，其质量不会受到影响</li>
</ol>
<p>因此，很多多媒体传输都是在UDP协议之上进行的。</p>
<p>多媒体应用程序可以分为两个类别：</p>
<ol>
<li><span style="background-color: #c0c0c0;">交互式应用</span>。例如视频会议、VoIP</li>
<li>非交互式应用。又可以细分为：
<ol>
<li><span style="background-color: #c0c0c0;">实时流媒体</span>，例如视频监控预览、网络赛事直播</li>
<li><span style="background-color: #c0c0c0;">非实时流媒体</span>，例如视频点播网站、歌曲点播应用、视频监控回放</li>
</ol>
</li>
</ol>
<p>除了非实时流媒体应用之外，多媒体应用不能容忍过长时间的缓冲以避免抖动，也不允许延迟的出现。</p>
<div class="blog_h2"><span class="graybg">互操作性问题</span></div>
<p>大量多媒体应用程序，使用了不同的编码方式，这些编码方式在媒体质量、带宽消耗、计算资源消耗之间做了不同的权衡。</p>
<p>不同的多媒体应用（例如两个独立开发的VoIP应用）要能够相互通信，就必须以某种双方都能理解的编码方式进行媒体的交换。</p>
<div class="blog_h1"><span class="graybg">实时传输协议</span></div>
<p>直接使用UDP传输流媒体数据包不能满足需求，因为UDP没有任何关于延迟、抖动的语义，或者它不理解何为“实时”。</p>
<p>实时传输协议（RTP）是一种专门处理实时需求的传输协议标准，主要用于处理音频、视频数据。 RTP允许接受者知晓接收到的数据包们在时间维上的关系，这样：</p>
<ol>
<li>在缓冲媒体流并播放时不会出现顺序混乱的情况</li>
<li>多个媒体流（例如音频、视频）能够在播放时保持同步关系</li>
</ol>
<p>此外RTP协议：</p>
<ol>
<li>允许通信双方进行协商（Negotiation），选取两者都认可的编码方式。这解决互操作性问题</li>
<li>具有识别包丢失的问题，这样端点应用程序能够<a href="/webrtc-server-basedon-kurento#interoperability">进行适当的处理</a></li>
<li>允许进行拥塞控制，媒体接收者能够向发送者进行网络拥塞状况的反馈（Feedback），这样发送者可以对码流质量进行调整，以改变带宽占用</li>
<li>支持帧指示（Frame Indication），例如媒体接收者需要知道那些数据包是属于相同的视频场景（Video Scene，帧）的，这样才能进行合适的处理</li>
</ol>
<div class="blog_h2"><span class="graybg">RTP的历史</span></div>
<p>通过网络进行音频传输的尝试从1970年代就开始了，在70-80年代多个语音包传输、时间戳、序列号相关的专利被批准。在1991年DARTnet成功完成了一系列语音传输的尝试，DARTnet使用的音频会议工具最终成为RTP版本0。</p>
<p>1992年RTP版本1发布，包含了若干因特网草案，此操作最终在1995年成为RTP版本2，包含：</p>
<ol>
<li>RFC1889，RTP</li>
<li>RFC1890，RTP Profile —— AVP（用于音视频会议的RTP profile，最小化控制）</li>
</ol>
<p>1996年，网景基于RTP和其他协议发布了Netscape LiveMidea。微软的NetMeeting软件也支持RTP。</p>
<p>RTP的设计确定了一个后续被广泛认同的原则 ——<span style="background-color: #c0c0c0;"> 应用层分帧原则（Application Level Framing ALF ）</span>。ALF认为应用程序更了解自己的需要，网络协议应该尽可能保持简单。例如MPEG解码器明白怎么样处理丢帧，如何从I帧、B帧丢失中恢复。</p>
<p>RTP支持大量种类的应用程序，对于每一类应用程序，RTP定义了一种Profile。Profile可以是：</p>
<ol>
<li>一种对RTP协议头结构的约定</li>
<li>定义对RTP协议的扩展或者修改</li>
</ol>
<p>RTP的载荷格式规定，则解释了RTP头之后的数据的结构。</p>
<div class="blog_h2"><span class="graybg">RTP组成</span></div>
<p>RTP作为一个标准，实际上定义了一对协议：</p>
<ol>
<li>RTP，用于交换媒体数据</li>
<li>RTCP，用于传输的控制，例如周期性的获得数据流传输质量的反馈信息、负责多个媒体流的同步。RTCP也负责传输组会话（Group Session）的参与者信息</li>
</ol>
<p>尽管RTP协议相对于传输层是独立的，但是它通常在UDP/IP之上运行。当基于UDP/IP传输时，RTP和RTCP使用连续的两个端口号。</p>
<p>为了发起一个RTP会话，应用程序需要定义一对特定的目的传输地址（Destination Transport Addresses，DTA）—— 一个网络地址加上两个端口（分别用于RTP、RTCP）。在一个多媒体（eg，音频 + 视频 + 文本）会话中，每个媒体都在单独的RTP会话中传输。</p>
<div class="blog_h3"><span class="graybg">视频会议的例子</span></div>
<p>在视频会议中，音频、视频媒体在不同的RTP会话中传输，这些会话使用的DTA是不同的，也就是它们使用两对不同的UDP端口。</p>
<p>视频会议也可能基于组播技术实现，这种情况下，需要使用两对多播地址的DTA。</p>
<p>音频、视频的RTP会话没有直接的关联，这允许接收者仅仅接收音频或者视频流。为了实现一个源的音视频同步，接收者可以使用RTCP包中的时序信息。</p>
<p>每个会议参与者都使用视频、音频应用程序，以块的方式发送数据。这些数据作为RTP包的载荷，RTP头中有专门的字段识别这些数据是如何编码的。</p>
<p>RTP头包含时序信息、序列号，接收者可以用这些信息重新构造音视频流的时序，不同源（音视频）时序信息都是单独构建的。</p>
<div class="blog_h2"><span class="graybg">RTP结构</span></div>
<p>RTP数据包的整体结构、在网络和应用中的传递方式，如下图所示：</p>
<p><img class="aligncenter size-full wp-image-16018" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rpt-package.png" alt="rpt-package" width="100%" /></p>
<p>关于此图的说明如下：</p>
<ol>
<li>RTP协议通常运行在UDP之上，这意味着数据都是无状态、推送的方式传递</li>
<li>RTP数据包是一个瘦协议，对需要持续传递数据（流式）的应用程序提供支持：
<ol>
<li>时序重构（Timing Reconstruction）</li>
<li>丢帧检测（Frame Loss Detection）</li>
<li>数据安全（Data Security）</li>
<li>内容识别（Content Identification）</li>
</ol>
</li>
<li>RTP协议不负责处理带宽保留（Reserve Bandwidth）和保证QoS</li>
<li>RTP数据包的载荷部分是数字化（由编码器负责）的媒体流</li>
</ol>
<div class="blog_h2"><span class="graybg">转换器/混合器</span></div>
<p>除了发送者、接收者角色之外，RTP协议还定义了另外两个参与此协议处理的角色 —— 转换器（Translater）、混合器（Mixer）它们位于发送者、接收者角色<span style="background-color: #c0c0c0;">之间</span>，对<span style="background-color: #c0c0c0;">经过（Passthrough）</span>它们的RTP包做出处理：</p>
<ol>
<li>转换器：对经过的RTP载荷进行转换，例如可以降低视频码流的比特率，降低带宽需求</li>
<li>混合器：用于混合来自多个媒体源的流，例如可以混合多个视频会议参与者的视频流，形成一个单独的流</li>
</ol>
<p>注意，仅仅当若干RTP流经过混合器，混合器才起作用。例如在一个电话会议的应用场景中，多个音频流通常会经过混合器混合为一个流，以节约带宽占用。</p>
<div class="blog_h2"><span class="graybg">RTP协议头</span></div>
<p>RTP协议的头格式如下图所示：</p>
<p><img class="aligncenter size-full wp-image-16022" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-header.png" alt="rtp-header" width="100%" /></p>
<ol>
<li>最前面的12字节（到SSRC为止）总是存在（上图中1、2、3……表示bit）
<ol>
<li>V：2bit的版本号，一般取值2</li>
<li>P：1bit的补白标记，如果此标记被设置，RTP包的尾部会包含1-N个补白字节。这些字节不属于载荷。补白的最后一个字节记录了补白的总数（包含它自己）。之所以需要补白，是为了满足某些加密算法对块（Block）长度的规定</li>
<li>X：1bit的扩展标记，如果此标记被设置，则在标准头后面包含1个扩展头</li>
<li>CC：4bit的CSRC标识符的计数器。如果载荷包含来自多个源的数据，则此计数大于1</li>
<li>M：1bit的Marker标记，此标记的意义由Profile定义，此标记通常用于提示重要事件的发生，例如帧边界</li>
<li>PT：7bit的载荷类型，指示载荷的数据类型。支持的类型包括PCM、MPEG1、MPEG2、JPEG视频、H.261等等。更多载荷类型可以通过Profile规范、载荷格式规范添加</li>
<li>16bit序列号：每当会话发送一个新的RTP包后，此序列号增加1。接收者可以基于此序列号进行丢包检测。此序列号的初始值是随机的，这样RTP包被加密后，尝试破解变得更加困难。当丢包出现后RTP协议层不做任何操作，应用程序负责对丢包事件做出响应，例如：某些视频应用可能在丢包时自动重放前一帧；另一些视频应用可能因为丢包而降低比特率</li>
<li>32bit时间戳：记录载荷中第一个字节的采样发生的时间。此字段的用途包括：让接收者可以按照适当的时间间隔来播放采样；允许多个媒体流保持同步；在计算抖动平滑（Jitter smoothing）时使用。时间戳使用的时钟解析度必须足够高，以满足同步精度、抖动度量精度。时间戳的初始值也是随机的，RTP没有规定时间戳的计量单位 —— 时间戳仅仅是时钟的tick计数，两个tick之间对应真实时间是多少，也是和应用程序相关的，这些仍然由Profile、载荷格式规定。频率表示每秒内有多少tick，因而tick数量 / 频率即得到对应真实世界的时间</li>
<li>32bit的SSRC标识符：用于识别同步源的标识符，此标识符被随机的生成，确保同一媒体会话中，任何两个同步源的标识符都不同。但是即使随机生成也有一定的概率出现重复，因此RTP实现必须有能力识别、解决冲突。当一个信号源改变自己的传递地址后，SSRC标识符必须也更改</li>
</ol>
</li>
<li> 32bit的CSRC标识符，此标识符最多有15个（取决于CC），用于识别此包的载荷部分由哪些（Contributing ）源构成。CSRC标识符由混合器（Mixer）插入到包头中，其值就是Contributing源的SSRC头</li>
<li>后面是可选的扩展头</li>
<li>在RTP后面，是1-N个音视频帧，作为RTP载荷</li>
</ol>
<div class="blog_h1"><span class="graybg">实时传输控制协议</span></div>
<p>RTCP协议专门用于配合RTP协议使用。</p>
<p>在RTP会话中，参与者定期向RTP会话的所有参与者通过组播发送RTCP包。RTCP包中包含媒体发送者/接收者的报告，其内容包括发送数据包的数量、丢失数据包的数量、抖动信息（Jitter）。</p>
<p>应用程序可能使用RTCP中的信息，来自适应的改变媒体流的质量，以适应可用网络带宽。</p>
<p>RTCP为来自同一个发送者的不同媒体流提供了一种协作、同步的机制。例如，当SSRC取值冲突时，需要某个流改变SSRC，这就是通过RTCP完成的。</p>
<p>对于牵涉到多个单独的多媒体流的应用程序，它们之间的同步基于一个通用的系统时钟完成。最初发起会话的那个系统提供此时钟，RTCP消息可以保证会话的所有参与者都使用相同的时钟。</p>
<p>RTCP还用于传输会话中各成员之间的关系。</p>
<p>当会话参与者越来越多时，RTCP数据报的总量会变多。为了防止影响网络，RTCP包占据会话总数据量不会超过5%，这意味着随着参与者的增加RTCP包发送频率会降低</p>
<div class="blog_h2"><span class="graybg">RTCP头</span></div>
<p><img class="aligncenter size-full wp-image-16037" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtcp-header.png" alt="rtcp-header" width="100%" /></p>
<ol>
<li>2bit的版本号，使用的RTP协议的版本</li>
<li>1bit的补白标记，RTP包的最后是否具有补白</li>
<li>5bit的接收报告计数（Reception Report Count ），此包中包含的接收报告块的数量</li>
<li>8bit的消息类型</li>
<li>16bit的长度，指示此包的总长度</li>
<li>32bit的SSRC，同步源标识</li>
</ol>
<div class="blog_h2"><span class="graybg">RTCP消息类型</span></div>
<p>RFC 3550定义了五种类型的RTCP报文：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">报文类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>RR</td>
<td>接收者报告（Receiver Report），由不作为活动发送者的会话参与者生成，包含接收质量反馈信息。具体内容包括：接收到的最高包序列号、丢包数量、抖动情况、用于计算收发者之间延迟的时间戳信息</td>
</tr>
<tr>
<td>SR</td>
<td>发送者报告（Sender Report），由活动的发送者生成，除了RR中的信息外，SR还包含一个发送者信息段。此段提供媒体间（Inter-midea）同步需要的信息、累计发包数量、立即发送字节数</td>
</tr>
<tr>
<td>SDES</td>
<td>源描述条目（Source Description Items），包含对源的描述信息。在RTP包中源由32bit的一个头字段标识，但是这个名字不适合人类阅读，SDES则提供了一个所谓规范化名称（Canonical Names)作为会话参与者的唯一性标识。规范化名称可能包括用户名、电话号码、电子邮件地址或者其他信息</td>
</tr>
<tr>
<td>BYE</td>
<td>提示发送者结束会话的参与</td>
</tr>
<tr>
<td>APP</td>
<td>应用特定功能（Application Specific Functions），主要用于新应用、新特性开发时的实验性功能</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">实时流协议</span></div>
<p>RTSP由RFC2326定义，他是一个应用层多媒体展现协议，支持对实时媒体流进行控制 —— 例如暂停播放、Seek、快进、倒放，这些控制行为类似于DVD播放器。RTSP协议本身通常不进行媒体流的传递。</p>
<p>RTSP服务器为客户端维护一个会话，此会话由一个标识符来识别。RTSP协议支持TCP或者UDP传输，在一个RTSP会话中，客户端可能打开、关闭多个传输连接，以发送RTSP请求。</p>
<p>RTSP需要和低层的RTP或者RSVP之类的协议协同，才能在因特网上提供完整的流媒体服务。RTSP在RTP的基础上提供了选择传输通道（TCP/单播UDP/组播UDP）、传输机制的方法。RTSP报文独立于媒体流发送。</p>
<p>RTSP在客户端和流媒体服务器之间创建、控制音视频媒体流。服务器负责提供回放、录制等服务。</p>
<p>RTSP中的每个展现（Presentation）和媒体流都通过一个RTSP URL来识别，整体的展现信息和媒体属性在一个展现描述文件（Presenttation Description File）中记录，此文件中的信息可能包括编码方式、语言、RTSP URLs、目的地址/端口以及其它参数。客户端可以通过HTTP、电子邮件等方式获得展现描述文件。</p>
<p>RTSP协议有意的模仿HTTP协议的设计，但是两者有以下重要的不同：</p>
<ol>
<li>RTSP是有状态的，它必须维护会话状态，让RTSP请求和某个流关联</li>
<li>RTSP是对称的，媒体服务器和客户端都可以发起请求。例如服务器可以发起请求，来设置流的回放参数</li>
</ol>
<p>RTSP支持以下方法：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>OPTIONS</td>
<td>
<p>返回服务器接收的请求类型。报文示例：</p>
<pre class="crayon-plain-tag"># 客户端请求
C-&gt;S:  OPTIONS rtsp://gmem.cc/media.mp4 RTSP/1.0
       CSeq: 1
       Require: implicit-play
       Proxy-Require: gzipped-messages
# 服务器应答
S-&gt;C:  RTSP/1.0 200 OK
       CSeq: 1
       # 支持的请求类型列表
       Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE</pre>
</td>
</tr>
<tr>
<td>DESCRIBE</td>
<td>
<p>客户端发起此报文，或者RTSP URL所代表的展现/媒体对象的描述信息。报文示例：
<pre class="crayon-plain-tag"># 客户端请求
C-&gt;S: DESCRIBE rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 2
# 服务器应答
S-&gt;C: RTSP/1.0 200 OK
      CSeq: 2
      Content-Base: rtsp://gmem.cc/media.mp4
      Content-Type: application/sdp
      Content-Length: 460
      # 下面是一个媒体行 —— 基于AVP Profile的RTP传输
      m=video 0 RTP/AVP 96
      a=control:streamid=0
      a=range:npt=0-7.741000
      a=length:npt=7.741000
      # 视频编码方式
      a=rtpmap:96 MP4V-ES/5544
      a=mimetype:string;"video/MP4V-ES"
      a=AvgBitRate:integer;304018
      a=StreamName:string;"hinted video track"
      # 下面是一个媒体行，音频部分
      m=audio 0 RTP/AVP 97
      a=control:streamid=1
      a=range:npt=0-7.712000
      a=length:npt=7.712000
      a=rtpmap:97 mpeg4-generic/32000/2
      a=mimetype:string;"audio/mpeg4-generic"
      a=AvgBitRate:integer;65790
      a=StreamName:string;"hinted audio track"</pre>
</td>
</tr>
<tr>
<td>ANNOUNCE</td>
<td>
<p>当由客户端发起时，更新RTSP URL所代表的展现/媒体对象的描述信息
<p>当由服务器发起时，实时的更新会话描述</p>
</td>
</tr>
<tr>
<td>SETUP</td>
<td>
<p>客户端请求服务器为某个流分配资源，并启动一个RTSP会话。报文示例：</p>
<pre class="crayon-plain-tag">C-&gt;S: SETUP rtsp://gmem.cc/media.mp4/streamid=0 RTSP/1.0
      CSeq: 3
      # 基于AVP Profile的RTP，使用UDP单播，RTP/RTCP端口
      Transport: RTP/AVP;unicast;client_port=8000-8001

S-&gt;C: RTSP/1.0 200 OK
      CSeq: 3
      # 附加服务器端口信息，媒体源唯一标识
      Transport: RTP/AVP;unicast;client_port=8000-8001;server_port=9000-9001;ssrc=1234ABCD
      # 分配会话标识符
      Session: 12345678</pre>
</td>
</tr>
<tr>
<td>PLAY</td>
<td>
<p>客户端请求服务器通过SETUP分配的流推送数据。报文示例：
<pre class="crayon-plain-tag">C-&gt;S: PLAY rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 4
      Range: npt=5-20
      Session: 12345678

S-&gt;C: RTSP/1.0 200 OK
      CSeq: 4
      Session: 12345678
      RTP-Info: url=rtsp://gmem.cc/media.mp4/streamid=0;seq=9810092;rtptime=3450012</pre>
</td>
</tr>
<tr>
<td>PAUSE</td>
<td>客户端临时停止流的递送，但是不释放服务器资源</td>
</tr>
<tr>
<td>TEARDOWN</td>
<td>客户端请求服务器停止流的递送，并释放分配的资源</td>
</tr>
<tr>
<td>GET_PARAMETER</td>
<td>
<p>获取RTSP URL所代表的展现/流的某个参数的值。报文示例：
<pre class="crayon-plain-tag">S-&gt;C: GET_PARAMETER rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 9
      Content-Type: text/parameters
      Session: 12345678
      Content-Length: 15
      # 获取两个参数
      packets_received
      jitter

C-&gt;S: RTSP/1.0 200 OK
      CSeq: 9
      Content-Length: 46
      Content-Type: text/parameters

      packets_received: 10
      jitter: 0.3838 </pre>
</td>
</tr>
<tr>
<td>SET_PARAMETER</td>
<td>
<p>设置RTSP URL所代表的展现/流的某个参数的值。报文示例：
<pre class="crayon-plain-tag">C-&gt;S: SET_PARAMETER rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 10
      Content-length: 20
      Content-type: text/parameters
      # 设置的参数名、值
      barparam: barstuff

S-&gt;C: RTSP/1.0 451 Invalid Parameter
      CSeq: 10
      Content-length: 10
      Content-type: text/parameters

      barparam </pre>
</td>
</tr>
<tr>
<td>REDIRECT</td>
<td>
<p>服务器发起，通知客户端，必须重新连接到一个媒体位置，此报文的location头指示媒体的新位置。报文示例：
<pre class="crayon-plain-tag">S-&gt;C: REDIRECT rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 11
      # 新的位置
      Location: rtsp://cast.gmem.cc.com:8001
      Range: clock=19960213T143205Z-</pre>
</td>
</tr>
<tr>
<td>RECORD</td>
<td>客户端基于展现描述，发起媒体数据某个范围的录制请求</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">Live555</span></div>
<p>Live555项目提供了一套C++库，用于RTP/RTCP、RTSP、SIP等标准协议下的多媒体应用开发。Live555库被用来实现Live555媒体服务器、Live555代理服务器。
<p>Live555还可以用来流化、接收、处理MPEG, H.265, H.264, H.263+, DV等视频编码格式以及若干音频编码格式。要支持其它音视频编码格式，你只需要简单的扩展。</p>
<div class="blog_h2"><span class="graybg">组件</span></div>
<p>下载Live555后，你得到以下几个组件，它们被位于不同的子目录：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">组件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>UsageEnvironment</td>
<td>
<p>包含类UsageEnvironment、TaskScheduler用于延迟事件的调度，例如异步读事件、输出错误/警告消息</p>
<p>包含类HashTable，一个哈希表实现</p>
</td>
</tr>
<tr>
<td>groupsock</td>
<td>此组件中的类封装了网络接口和套接字。Groupsock对组播的收发行为进行了封装</td>
</tr>
<tr>
<td>liveMedia</td>
<td>定义了一个类层次，其根是Medium。提供对多种流媒体类型、编码方式的支持</td>
</tr>
<tr>
<td>BasicUsageEnvironment</td>
<td>定义了UsageEnvironment的一个具体化子类，主要在简单的、基于控制台的应用程序中使用。读事件和延迟操作在一个select()循环中处理</td>
</tr>
<tr>
<td>testProgs</td>
<td>基于BasicUsageEnvironment实现了一些样例</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>Linux下安装Live555库的步骤如下：</p>
<pre class="crayon-plain-tag">wget http://www.live555.com/liveMedia/public/live.2017.07.18.tar.gz
tar xzf live.2017.07.18.tar.gz 
pushd live
# 如果需要保留调试信息，可以修改config.linux文件，添加编译参数-O0 -g3 
./genMakefiles linux
make &amp;&amp; make install PREFIX=/home/alex/CPP/lib/live555</pre>
<div class="blog_h2"><span class="graybg">RTSP客户端</span></div>
<div class="blog_h3"><span class="graybg">DESCRIBE示例</span></div>
<pre class="crayon-plain-tag">#include "liveMedia.hh"
#include "BasicUsageEnvironment.hh"
#include &lt;iostream&gt;

using namespace std;

volatile char eventLoopWatchVariable = 0;

int main() {
    TaskScheduler *scheduler = BasicTaskScheduler::createNew();
    UsageEnvironment *env = BasicUsageEnvironment::createNew( *scheduler );
    const char *url = "rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream";
    RTSPClient *client = RTSPClient::createNew( *env, url, 0 );  // 参数3传入1则控制台会打印调试信息
    // 发送RTSP DESCRIBE命令，注意所有RTSP命令都是异步发送的，其应答后续在事件循环中被处理
    client-&gt;sendDescribeCommand( []( RTSPClient *rtspClient, int resultCode, char *resultString ) -&gt; void {
        // 如果resultCode不为0说明失败
        // 打印SDP
        cout &lt;&lt; resultString &lt;&lt; endl;
    } );

    // 直到eventLoopWatchVariable变为非零之前，下面的事件循环不会停止
    env-&gt;taskScheduler().doEventLoop( &amp;eventLoopWatchVariable );
    return 0;
}</pre>
<p>公司的IP摄像头给出如下应答：</p>
<pre class="crayon-plain-tag">v=0
o=- 1505140878681876 1505140878681876 IN IP4 192.168.0.196
s=Media Presentation
e=NONE
b=AS:5050
t=0 0
a=control:rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream/
# 视频基于RTP/VAP传输类型，编码方式H264
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
b=AS:5000
a=recvonly
a=control:rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream/trackID=1
# 90000表示时钟频率，即每秒内，有多少个时间戳tick，或者说每秒RTP时间戳增加多少
a=rtpmap:96 H264/90000
# H.264 Profile: baseline , constraints 0 , level-idc 4.1 
# level-idc，用于提示自己的解码能力 —— 最大多大的分辨率、帧率、码率
a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z00AFJWoWCWm4CAgIEA=,aO48gA==
a=Media_header:MEDIAINFO=494D4B48010100000400000100000000000000000000000000000000000000000000000000000000;
a=appversion:1.0</pre>
<p>上述内容和WebRTC的SDP Offer类似，都属于会话描述协议。SDP消息可以划分为三个主要的段，分别对会话、时间、媒体进行描述。SDP各字段的含义如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">SDP字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><strong><em>会话描述</em></strong></td>
</tr>
<tr>
<td>v </td>
<td>协议版本号，总是0</td>
</tr>
<tr>
<td>o</td>
<td>发起者以及会话标识符</td>
</tr>
<tr>
<td>s</td>
<td>会话的名称 </td>
</tr>
<tr>
<td>i</td>
<td>会话的描述和简短信息 </td>
</tr>
<tr>
<td>u </td>
<td>
<p>描述（Description）的URI </p>
</td>
</tr>
<tr>
<td>e </td>
<td>0-N个电子邮件地址，附加联系人名称 </td>
</tr>
<tr>
<td>p</td>
<td>0-N个电话号码，附加联系人名称  </td>
</tr>
<tr>
<td>c </td>
<td>连接信息</td>
</tr>
<tr>
<td>b</td>
<td>0-N个带宽信息行 </td>
</tr>
<tr>
<td>z</td>
<td>时区调整</td>
</tr>
<tr>
<td>k</td>
<td>加密密钥</td>
</tr>
<tr>
<td>a</td>
<td>0-N个会话属性行</td>
</tr>
<tr>
<td colspan="2"><strong><em>时间描述（1-N）</em></strong></td>
</tr>
<tr>
<td>t</td>
<td>
<p>会话活动时间</p>
<p>其中的绝对时间基于网络时间协议（<span style="color: #222222;">NTP）格式，即1900年到目前的秒数</span></p>
<p>开始时间为0表示会话是永久的；结束时间为零表示会话持续时间不限制</p>
</td>
</tr>
<tr>
<td>r</td>
<td>0-N个repeat times</td>
</tr>
<tr>
<td colspan="2"><strong><em>媒体描述（0-N）</em></strong></td>
</tr>
<tr>
<td>m</td>
<td>
<p>媒体名称、传输地址，以及传输协议。示例：</p>
<pre class="crayon-plain-tag"># 音频，在49170端口，基于Profile AVP、载荷类型0（PCMU）的RTP协议传输
m=audio 49170 RTP/AVP 0
# 视频，在51372端口，基于Profile AVP、载荷类型96的RTP协议传输
m=video 51372 RTP/AVP 96</pre>
<p>载荷类型如果是96-127之间，则表示载荷类型是动态分配的，后面会出现a=rtpmap行来映射此载荷类型：</p>
<pre class="crayon-plain-tag">a=rtpmap:96 H264/90000</pre>
<p>载荷类型可以声明若干个，表示这些类型在会话中都可能使用</p>
</td>
</tr>
<tr>
<td>i</td>
<td>媒体的标题或者信息</td>
</tr>
<tr>
<td>c</td>
<td>连接信息</td>
</tr>
<tr>
<td>b</td>
<td>带宽信息</td>
</tr>
<tr>
<td>k</td>
<td>加密密钥</td>
</tr>
<tr>
<td>a</td>
<td>0-N个媒体属性行，可以覆盖会话属性行同名属性</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg"><a id="rtsp-client-wrapper"></a>客户端封装</span></div>
<p>为简化开发，下面给出一个live555的RTSP客户端封装。</p>
<p>创建如下CMake项目：</p>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 3.6)
project(live5555)

set(CMAKE_CXX_STANDARD 11)

set(LIVE555_HOME /home/alex/CPP/lib/live555)

include_directories(${LIVE555_HOME}/include/UsageEnvironment)
include_directories(${LIVE555_HOME}/include/BasicUsageEnvironment)
include_directories(${LIVE555_HOME}/include/liveMedia)
include_directories(${LIVE555_HOME}/include/groupsock)

include_directories(/home/alex/CPP/lib/spdlog/include)

set(CMAKE_CXX_FLAGS "-w -pthread")

set(LIVE5555_SRC SinkBase.cpp RTSPClientBase.cpp)

add_library(live5555 ${LIVE5555_SRC})
target_link_libraries(
        live5555
        ${LIVE555_HOME}/lib/libliveMedia.a
        ${LIVE555_HOME}/lib/libgroupsock.a
        ${LIVE555_HOME}/lib/libBasicUsageEnvironment.a
        ${LIVE555_HOME}/lib/libUsageEnvironment.a
)

set(WS_PUSH_SRC wspush.cpp)
add_executable(wspush ${WS_PUSH_SRC})
target_link_libraries(
        wspush
        live5555
)</pre>
<p>基础头文件：</p>
<pre class="crayon-plain-tag">#ifndef LIVE5555_COMMON_H
#define LIVE5555_COMMON_H

#include "liveMedia.hh"
#include "BasicUsageEnvironment.hh"

#endif //LIVE5555_COMMON_H</pre>
<p>类RTSPClientBase，对RTSPClient进行扩展，将RTSP命令回调函数指针转换为成员函数，实现基本的取流逻辑：</p>
<pre class="crayon-plain-tag">#ifndef LIVE5555_RTSPCLIENTBASE_H
#define LIVE5555_RTSPCLIENTBASE_H

#include "common.h"

class RTSPClientBase : public RTSPClient {
private:
    MediaSession *session;
    // 事件循环监控变量，不为零时事件循环退出
    volatile char eventLoopWatchVariable;
    char *rtspURL;
    volatile int acceptedSubSessionCount;
    volatile int preparedSubSessionCount;

    static void onDescribeResponse( RTSPClient *client, int resultCode, char *resultString );

    static void onSetupResponse( RTSPClient *client, int resultCode, char *resultString );

    static void onPlayResponse( RTSPClient *client, int resultCode, char *resultString );

    static void onSubSessionClose( void *clientData );

protected:
    RTSPClientBase( UsageEnvironment &amp;env, const char *rtspURL );

    // 处理RTSP命令DESCRIBE的响应
    virtual void onDescribeResponse( int resultCode, const char *sdp );

    // 处理RTSP命令SETUP的响应
    virtual void onSetupResponse( int resultCode, const char *resultString );

    virtual void onPlayResponse( int resultCode, char *resultString );

    // 是否初始化指定的子会话
    virtual bool acceptSubSession( const char *mediumName, const char *codec )=0;

    virtual MediaSink *createSink( const char *mediumName, const char *codec, MediaSubsession *subSession )=0;

    // 处理子会话关闭事件
    virtual void onSubSessionClose( MediaSubsession *subsess );

public:
    virtual void start();

    virtual void stop();
};

#endif //LIVE5555_RTSPCLIENTBASE_H</pre><br />
<pre class="crayon-plain-tag">#include "live5555/RTSPClientBase.h"
#include "spdlog/spdlog.h"

static auto LOGGER = spdlog::stdout_color_st( "RTSPClientBase" );

static const char *getResultString( char *resultString ) {
    return resultString ? resultString : "N/A";
}

// 父构造函数的第三个参数是调试信息冗余级别
RTSPClientBase::RTSPClientBase( UsageEnvironment &amp;env, const char *rtspURL ) :
    RTSPClient( env, rtspURL, 0, NULL, 0, -1 ) {
}

void RTSPClientBase::start() {
    LOGGER-&gt;trace( "Starting RTSP client..." );
    this-&gt;rtspURL = rtspURL;
    LOGGER-&gt;trace( "Send RTSP command: DESCRIBE" );
    sendDescribeCommand( onDescribeResponse );
    LOGGER-&gt;trace( "Startup live555 eventloop" );
    envir().taskScheduler().doEventLoop( &amp;eventLoopWatchVariable );
}


void RTSPClientBase::onDescribeResponse( RTSPClient *client, int resultCode, char *resultString ) {
    LOGGER-&gt;trace( "DESCRIBE response received, resultCode: {}", resultCode );
    RTSPClientBase *clientBase = (RTSPClientBase *) client;
    bool ok = false;
    if ( resultCode == 0 ) {
        clientBase-&gt;onDescribeResponse( resultCode, resultString );
    } else {
        LOGGER-&gt;trace( "Stopping due to DESCRIBE failure" );
        clientBase-&gt;stop();
    };
    delete[] resultString;
}

void RTSPClientBase::onDescribeResponse( int resultCode, const char *sdp ) {
    LOGGER-&gt;debug( "SDP received: \n{}", sdp );
    UsageEnvironment &amp;env = envir();
    LOGGER-&gt;trace( "Create new media session according to SDP" );
    session = MediaSession::createNew( env, sdp );
    if ( session &amp;&amp; session-&gt;hasSubsessions()) {
        MediaSubsessionIterator *it = new MediaSubsessionIterator( *session );
        // 遍历子会话，SDP中的每一个媒体行（m=***）对应一个子会话
        while ( MediaSubsession *subsess = it-&gt;next()) {
            const char *mediumName = subsess-&gt;mediumName();
            // 初始化子会话，导致相应的RTPSource被创建
            LOGGER-&gt;trace( "Initialize sub session {}", mediumName );
            if ( !acceptSubSession( mediumName, subsess-&gt;codecName())) {
                continue;
            }
            acceptedSubSessionCount++;
            bool ok = subsess-&gt;initiate();
            if ( !ok ) {
                LOGGER-&gt;error( "Failed to initialize sub session: {}", mediumName );
                stop();
                break;
            }
            const Boolean muxed = subsess-&gt;rtcpIsMuxed();
            const char *codec = subsess-&gt;codecName();
            const int port = subsess-&gt;clientPortNum();
            LOGGER-&gt;debug( "Initialized sub session... \nRTCP Muxed: {}\nPort: {}\nMedium : {}\nCodec: {}", muxed, port, mediumName, codec );

            LOGGER-&gt;trace( "Send RTSP command: SETUP for subsession {}", mediumName );
            sendSetupCommand( *subsess, onSetupResponse, False, False );
        }
    } else {
        stop();
    }
}

void RTSPClientBase::onSetupResponse( RTSPClient *client, int resultCode, char *resultString ) {
    LOGGER-&gt;trace( "SETUP response received, resultCode: {}, resultString: {}", resultCode, getResultString( resultString ));
    RTSPClientBase *clientBase = (RTSPClientBase *) client;
    if ( resultCode == 0 ) {
        clientBase-&gt;preparedSubSessionCount++;
        clientBase-&gt;onSetupResponse( resultCode, resultString );
    } else {
        LOGGER-&gt;trace( "Stopping due to SETUP failure" );
        clientBase-&gt;stop();
    }
    delete[] resultString;
}

void RTSPClientBase::onSetupResponse( int resultCode, const char *resultString ) {
    if ( preparedSubSessionCount == acceptedSubSessionCount ) {
        MediaSubsessionIterator *it = new MediaSubsessionIterator( *session );
        while ( MediaSubsession *subsess = it-&gt;next()) {
            const char *mediumName = subsess-&gt;mediumName();
            const char *codec = subsess-&gt;codecName();
            if ( acceptSubSession( mediumName, codec )) {
                MediaSink *sink = createSink( mediumName, codec, subsess );
                // 让Sink回调能够感知Client对象
                subsess-&gt;miscPtr = this;
                // 导致Sink的continuePlaying被调用，准备接受数据推送
                sink-&gt;startPlaying( *subsess-&gt;readSource(), NULL, subsess );
                // 此时数据推送不会立即开始，直到调用STSP命令PLAY
                RTCPInstance *rtcp = subsess-&gt;rtcpInstance();
                if ( rtcp ) {
                    // 正确处理针对此子会话的RTCP命令
                    rtcp-&gt;setByeHandler( onSubSessionClose, subsess );
                }
                LOGGER-&gt;trace( "Send RTSP command: PLAY" );
                // PLAY命令可以针对整个会话，也可以针对每个子会话
                sendPlayCommand( *session, onPlayResponse );
            }
        }
    }
}

void RTSPClientBase::onPlayResponse( RTSPClient *client, int resultCode, char *resultString ) {
    LOGGER-&gt;trace( "PLAY response received, resultCode: {}, resultString: {}", resultCode, getResultString( resultString ));
    RTSPClientBase *clientBase = (RTSPClientBase *) client;
    if ( resultCode == 0 ) {
        clientBase-&gt;onPlayResponse( resultCode, resultString );
    } else {
        LOGGER-&gt;trace( "Stopping due to PLAY failure" );
        clientBase-&gt;stop();
    }
    delete[] resultString;
}

void RTSPClientBase::onPlayResponse( int resultCode, char *resultString ) {
    // 此时服务器应该开始推送流过来
    // 如果播放的是定长的录像，这里应该注册回调，在时间到达后关闭客户端
    double &amp;startTime = session-&gt;playStartTime();
    double &amp;endTime = session-&gt;playEndTime();
    LOGGER-&gt;debug_if( startTime == endTime, "Session is infinite" );
}

void RTSPClientBase::onSubSessionClose( void *clientData ) {
    MediaSubsession *subsess = (MediaSubsession *) clientData;
    RTSPClientBase *clientBase = (RTSPClientBase *) subsess-&gt;miscPtr;
    clientBase-&gt;onSubSessionClose( subsess );
}

void RTSPClientBase::onSubSessionClose( MediaSubsession *subsess ) {
    LOGGER-&gt;debug( "Stopping subsession..." );
    // 首先关闭子会话的SINK
    Medium::close( subsess-&gt;sink );
    subsess-&gt;sink = NULL;

    // 检查是否所有兄弟子会话均已经结束
    MediaSession &amp;session = subsess-&gt;parentSession();
    MediaSubsessionIterator iter( session );
    while (( subsess = iter.next()) != NULL ) {
        // 存在未结束的子会话，不能关闭当前客户端
        if ( subsess-&gt;sink != NULL ) return;
    }
    // 关闭客户端
    LOGGER-&gt;debug( "All subsession closed" );
    stop();
}

void RTSPClientBase::stop() {
    LOGGER-&gt;debug( "Stopping RTSP client..." );
    // 修改事件循环监控变量
    eventLoopWatchVariable = 0;
    UsageEnvironment &amp;env = envir();
    if ( session != NULL ) {
        Boolean someSubsessionsWereActive = False;
        MediaSubsessionIterator iter( *session );
        MediaSubsession *subsession;
        // 检查是否存在需要处理的子会话
        while (( subsession = iter.next()) != NULL ) {
            if ( subsession-&gt;sink != NULL ) {
                // 强制关闭子会话的SINK
                Medium::close( subsession-&gt;sink );
                subsession-&gt;sink = NULL;
                if ( subsession-&gt;rtcpInstance() != NULL ) {
                    // 服务器可能在处理TEARDOWN时发来RTCP包BYE
                    subsession-&gt;rtcpInstance()-&gt;setByeHandler( NULL, NULL );
                }
                someSubsessionsWereActive = True;
            }
        }

        if ( someSubsessionsWereActive ) {
            // 向服务器发送TEARDOWN命令，让服务器关闭输入流
            sendTeardownCommand( *session, NULL );
        }
    }
    // 关闭客户端
    Medium::close( this );
}</pre>
<p>类SinkBase，一个基础的Sink实现，从流中获取帧：</p>
<pre class="crayon-plain-tag">#ifndef LIVE5555_SINKBASE_H
#define LIVE5555_SINKBASE_H

#include "common.h"

class SinkBase : public MediaSink {

private:
    static void afterGettingFrame( void *clientData, unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime,
        unsigned /*durationInMicroseconds*/ );

protected:
    unsigned recvBufSize;

    unsigned char *recvBuf;

    SinkBase( UsageEnvironment &amp;env, unsigned recvBufSize );

    // sink-&gt;startPlaying会调用continuePlaying，实现播放逻辑
    virtual Boolean continuePlaying();

    virtual ~SinkBase();

public:
    virtual void afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime );
};

#endif //LIVE5555_SINKBASE_H</pre><br />
<pre class="crayon-plain-tag">#include "live5555/SinkBase.h"
#include "spdlog/spdlog.h"

static auto LOGGER = spdlog::stdout_color_st( "SinkBase" );

SinkBase::SinkBase( UsageEnvironment &amp;env, unsigned recvBufSize ) : MediaSink( env ) {
    this-&gt;recvBufSize = recvBufSize;
    this-&gt;recvBuf = new unsigned char[recvBufSize];
}

SinkBase::~SinkBase() {
    delete[] this-&gt;recvBuf;
}

// 缺省实现：保存已分帧源的下一帧到缓冲区中，然后执行回调
Boolean SinkBase::continuePlaying() {
    if ( fSource == NULL ) return False;
    fSource-&gt;getNextFrame( recvBuf, recvBufSize, afterGettingFrame, this, onSourceClosure, this );
    return True;
};

// 由于getNextFrame需要的是一个函数指针，因此这里用静态函数。此函数简单的转调对应的成员函数
void SinkBase::afterGettingFrame( void *clientData, unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime,
    unsigned /*durationInMicroseconds*/ ) {
    SinkBase *sink = (SinkBase *) clientData;
    sink-&gt;afterGettingFrame( frameSize, numTruncatedBytes, presentationTime );
}

// 缺省实现：递归获取下一帧
void SinkBase::afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ) {
    LOGGER-&gt;trace( "Frame of {} bytes received",frameSize );
    fSource-&gt;getNextFrame( recvBuf, recvBufSize, afterGettingFrame, this, onSourceClosure, this );
} </pre>
<div class="blog_h3"><span class="graybg">封装应用示例</span></div>
<p>下面的客户端基于上节的封装：</p>
<pre class="crayon-plain-tag">#ifndef LIVE5555_LIVE5555_H
#define LIVE5555_LIVE5555_H

#include "RTSPClientBase.h"
#include "SinkBase.h"

#endif //LIVE5555_LIVE5555_H</pre><br />
<pre class="crayon-plain-tag">#include &lt;iostream&gt;
#include "live5555/client.h"
#include "spdlog/spdlog.h"

static auto LOGGER = spdlog::stdout_color_st( "wspush" );

class VideoSink : public SinkBase {
public:
    VideoSink( UsageEnvironment &amp;env, unsigned int recvBufSize ) : SinkBase( env, recvBufSize ) {}

    void afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ) override {
        unsigned naluHead = recvBuf[ 0 ];
        unsigned nri = naluHead &gt;&gt; 5;
        unsigned f = nri &gt;&gt; 2;
        unsigned type = naluHead &amp; 0b00011111;
        LOGGER-&gt;trace( "NALU info: nri {} type {}", nri, type );
        SinkBase::afterGettingFrame( frameSize, numTruncatedBytes, presentationTime );
    }
};

class H264RTSPClient : public RTSPClientBase {
private:
    MediaSink *videoSink;
public:
    H264RTSPClient( UsageEnvironment &amp;env, const char *rtspURL, MediaSink *videoSink ) :
        RTSPClientBase( env, rtspURL ), videoSink( videoSink ) {}

protected:
    // 测试用的摄像头（RTSP源）仅仅有一个子会话，因此这里简化了实现：
    bool acceptSubSession( const char *mediumName, const char *codec ) override {
        return true;
    }

    MediaSink *createSink( const char *mediumName, const char *codec, MediaSubsession *subSession) override {
        return videoSink;
    }
};

int main() {
    spdlog::set_pattern( "%Y-%m-%d %H:%M:%S.%e [%l] [%n] %v" );
    spdlog::set_level( spdlog::level::trace );

    TaskScheduler *scheduler = BasicTaskScheduler::createNew();
    BasicUsageEnvironment *env = BasicUsageEnvironment::createNew( *scheduler );
    VideoSink *sink = new VideoSink( *env, 1024 * 1024 );
    H264RTSPClient *client = new H264RTSPClient( *env, "rtsp://admin:kingsmart123@192.168.0.196:554/ch1/sub/av_stream", sink );
    client-&gt;start();
    return 0;
}</pre>
<p>此客户端很简单，它建立RTSP会话，然后依次执行DESCRIBE、SETUP、PLAY命令，最终建立RTP会话，开始视频流的传输。</p>
<p>live555的RTSP客户端中的所有网络事件 —— 包括RTSP请求响应、RTP推送 —— 都由BasicTaskScheduler的事件循环处理，该事件循环会不断的执行select()系统调用，监听网络事件的到达。事件循环由RTSPClientBase.start()启动。</p>
<p>在SETUP响应回调中，发送PLAY命令之前，我们调用了VideoSink.startPlaying()方法，此方法会转调VideoSink.continuePlaying()方法，后者则调用H264RTPSource.getNextFrame()注册下一帧的处理回调。getNextFrame()会将解析得到的帧存放到你指定的缓冲区中，测试时打印前几帧：</p>
<p style="padding-left: 30px;"><span class="monospace">67 42 00 1F 95 A8 14 01 6E 9B 80 80 80 81 </span><br /><span class="monospace">68 CE 3C 80 </span><br /><span class="monospace">61 E4 A0 4F F3 7A 06 B9 36 39 80 07 4C 9A ...</span></p>
<p>可以看到，这些帧都是标准的H264 NALU格式（不带起始码），第一个是SPS，第二个是PPS，后续是普通切片。</p>
<div class="blog_h3"><span class="graybg">H264RTPSource</span></div>
<p>RTP包到达后，事件循环委托H264RTPSource进行如下处理：</p>
<ol>
<li>检查RTP包头的合法性</li>
<li>剔除RTP包头和Padding</li>
<li>抽取H264帧，然后针对自己调用afterGetting(this)，并导致之前通过getNextFrame()注册的回调函数被调用</li>
</ol>
<div class="blog_h1"><span class="graybg">术语列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Jitter</td>
<td>
<p>抖动（Jitter）是TCP/IP网络和组件天生具有的一种不被期望的“倾向”。它是数据报被接收到的延迟（Delay）时间的变化性（Variation）</p>
<p>发送方以固定的频率发送数据报，但是由于网络拥塞、不适当的数据报排队或者配置错误，接收者接收到数据报的频率可能会在较大范围变动</p>
<p>抖动会影响流媒体的回放体验，在等待延迟到达的数据报时，可能出现gap</p>
</td>
</tr>
<tr>
<td>NPT</td>
<td>
<p>正常播放时间（Normal Play Time），指示相对于展现（presentation）开始点的，流的当前位置（时间偏移）</p>
<p>NPT使用一个浮点数表示，整数部分可能按秒数、或者小时+分钟+秒数来解释，小数部分则进行一秒内度量（例如.500表示当前秒过了一半）</p>
<p>展现的开始点的NPT定义为0.0，负数的意义没有定义</p>
<p>当x倍速播放时，NPT的增长速度增加为x倍</p>
</td>
</tr>
<tr>
<td>Presentation</td>
<td>
<p>展现，一个或者多个被渲染到客户端流，它们共同组成了一个Media feed</p>
</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/realtime-communication-protocols">实时通信协议族</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/realtime-communication-protocols/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>HTML5视频监控技术预研</title>
		<link>https://blog.gmem.cc/research-on-html5-video-surveillance</link>
		<comments>https://blog.gmem.cc/research-on-html5-video-surveillance#comments</comments>
		<pubDate>Mon, 28 Aug 2017 05:49:57 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Graphic]]></category>
		<category><![CDATA[HTML]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[JSMpeg]]></category>
		<category><![CDATA[MSE]]></category>
		<category><![CDATA[Multimedia]]></category>
		<category><![CDATA[WebRTC]]></category>
		<category><![CDATA[视频监控]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15526</guid>
		<description><![CDATA[<p>引言 安防类项目中通常都有视频监控方面的需求。视频监控客户端主要是Native应用的形式，在Web端需要利用NPAPI、ActiveX之类的插件技术实现。 但是，IE式微，Chrome也放弃了NPAPI，另一方面，监控设备硬件厂商的视频输出格式则逐渐标准化。这让基于开放、标准化接口的Web视频监控成为可能。 本文讨论以HTML5及其衍生技术为基础的B/S架构实时视频监控解决方案。主要包括两方面的内容： 视频编码、流媒体基础知识，以及相关的库、框架的介绍 介绍可以用于视频监控的HTML5特性，例如媒体标签、MSE、WebRTC，以及相关的库、框架 本文仅仅简介若干种备选的解决方案，本站其它文章进行了更加深入的探讨： H.264学习笔记 实时通信协议族 基于Kurento搭建WebRTC服务器 基于Broadway的HTML5视频监控 音视频编码 音频、视频的编码（Codec，压缩）算法有很多，不同浏览器对音视频的编码算法的支持有差异。H264这样的监控设备常用的视频编码格式，主流浏览器都有某种程度的支持。 常见的音频编码算法包括： MP3, Vorbis, AAC；常见的视频编码算法包括： H.264, HEVC, VP8, VP9。 编码后的音频、视频通常被封装在一个比特流容器格式（container）中，这些格式中常见的有： <a class="read-more" href="https://blog.gmem.cc/research-on-html5-video-surveillance">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/research-on-html5-video-surveillance">HTML5视频监控技术预研</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">引言</span></div>
<p>安防类项目中通常都有视频监控方面的需求。视频监控客户端主要是Native应用的形式，在Web端需要利用NPAPI、ActiveX之类的插件技术实现。</p>
<p>但是，IE式微，Chrome也放弃了NPAPI，另一方面，监控设备硬件厂商的视频输出格式则逐渐标准化。这让基于开放、标准化接口的Web视频监控成为可能。</p>
<p>本文讨论以HTML5及其衍生技术为基础的B/S架构实时视频监控解决方案。主要包括两方面的内容：</p>
<ol>
<li>视频编码、流媒体基础知识，以及相关的库、框架的介绍</li>
<li>介绍可以用于视频监控的HTML5特性，例如媒体标签、MSE、WebRTC，以及相关的库、框架</li>
</ol>
<p>本文仅仅简介若干种备选的解决方案，本站其它文章进行了更加深入的探讨：</p>
<ol>
<li><a href="/h264-study-note">H.264学习笔记</a></li>
<li><a href="/realtime-communication-protocols">实时通信协议族</a></li>
<li><a href="/webrtc-server-basedon-kurento">基于Kurento搭建WebRTC服务器</a></li>
<li><a href="/html5-vs-with-broadway">基于Broadway的HTML5视频监控</a></li>
</ol>
<div class="blog_h1"><span class="graybg">音视频编码</span></div>
<p>音频、视频的编码（Codec，压缩）算法有很多，不同浏览器对音视频的编码算法的支持有<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility">差异</a>。H264这样的监控设备常用的视频编码格式，主流浏览器都有某种程度的支持。</p>
<p>常见的音频编码算法包括： MP3, Vorbis, AAC；常见的视频编码算法包括： H.264, HEVC, VP8, VP9。</p>
<p>编码后的音频、视频通常被封装在一个比特流容器格式（container）中，这些格式中常见的有： MP4, FLV, WebM,  ASF, ISMA等。</p>
<div class="blog_h2"><span class="graybg">JSMpeg</span></div>
<p>视频解码工作通常由浏览器本身负责，配合video实现视频播放。</p>
<p>现代浏览器的JS引擎性能较好，因此出现了纯粹由JS实现的解码器<a href="https://github.com/phoboslab/jsmpeg">JSMpeg</a>，它能够解码视频格式MPEG1、音频格式MP2。支持通过Ajax加载静态视频文件，支持低延迟（小于50ms）的流式播放（通过WebSocket）。JSMpeg包括以下组件：</p>
<ol>
<li>MPEG-TS分流器（demuxer）。muxer负责把视频、音频、字幕打包成一种容器格式，demuxer则作相反的工作</li>
<li>MPEG1视频解码器</li>
<li>MP2音频解码器</li>
<li>WebGL渲染器、Canvas2D渲染器</li>
<li>WebAudio音频输出组件</li>
</ol>
<p>JSMpeg的优势在于兼容性好，几乎所有现代浏览器都能运行JSMpeg。</p>
<div class="blog_h3"><span class="graybg">性能</span></div>
<p>JSMpeg不能使用硬件加速。在iPhone 5S这样的设备上，JSMpeg能够处理720p@30fps视频。</p>
<p>比起现代解码器，MPEG1压缩率较低，因而需要更大的带宽。720p的视频大概占用250KB/s的带宽。</p>
<div class="blog_h3"><span class="graybg">示例</span></div>
<p>下面我们尝试利用ffmpeg编码本地摄像头视频，并通过JSMpeg播放。</p>
<p>创建一个NPM项目，安装依赖：</p>
<pre class="crayon-plain-tag">npm install jsmpeg --save
npm install ws --save</pre>
<p>JSMpeg提供了一个中继器，能够把基于HTTP的MPEG-TS流转换后通过WebSocket发送给客户端。此脚本需要<a href="https://github.com/phoboslab/jsmpeg/blob/master/websocket-relay.js">到Github下载</a>。 下面的命令启动一个中继器：</p>
<pre class="crayon-plain-tag">node ./app/websocket-relay.js 12345 8800 8801
# Listening for incomming MPEG-TS Stream on http://127.0.0.1:8800/&lt;secret&gt;
# Awaiting WebSocket connections on ws://127.0.0.1:8801/
# 实际上在所有网络接口上监听，并非仅仅loopback</pre>
<p>下面的命令捕获本地摄像头（Linux），并编码为MPEG1格式，然后发送到中继器：</p>
<pre class="crayon-plain-tag"># 从摄像头/dev/video0以480的分辨率捕获原始视频流
ffmpeg -s 640x480 -f video4linux2 -i /dev/video0 \
       # 输出为原始MPEG-1视频（JSMpeg可用），帧率30fps，比特率800kbps
       -f mpegts -codec:v mpeg1video -b 800k -r 30 http://127.0.0.1:8800/12345
# 在我的机器上，上述ffmpeg私有内存占用18MB</pre>
<p>上述命令执行后，中继器控制台上打印：</p>
<pre class="crayon-plain-tag">Stream Connected: ::ffff:127.0.0.1:42399</pre>
<p>客户端代码：</p>
<pre class="crayon-plain-tag">var player = new JSMpeg.Player( 'ws://127.0.0.1:8801/', {
    canvas: document.getElementById( 'canvas' ),
    autoplay: true
} ); </pre>
<div class="blog_h2"><span class="graybg">Broadway</span></div>
<p><a href="https://github.com/mbebenita/Broadway">Broadway</a>是一个基于JavaScript的H.264解码器，其源码来自于Android的H.264解码器，利用Emscripten转译成了JavaScript，之后利用Google的Closure编译器优化，并针对WebGL进一步优化。</p>
<p>注意：Broadway仅仅支持Baseline这个H.264 Profile。</p>
<p><a href="https://github.com/131/h264-live-player">h264-live-player</a>是基于Broadway实现的播放器，允许通过WebSocket来传输NAL单元（原始H.264帧），并在画布上渲染。我们运行一下它的示例应用：</p>
<pre class="crayon-plain-tag">git clone https://github.com/131/h264-live-player.git
cd h264-live-player
npm install</pre>
<p>因为我的机器是Linux，所以修改h264-live-player/lib/ffmpeg.js， 把ffpmeg的参数改为：</p>
<pre class="crayon-plain-tag">var args = [
    "-f", "video4linux2",
    "-i",  "/dev/video0" ,
    "-framerate", this.options.fps,
    "-video_size", this.options.width + 'x' + this.options.height,
    '-pix_fmt',  'yuv420p',
    '-c:v',  'libx264',
    '-b:v', '600k',
    '-bufsize', '600k',
    '-vprofile', 'baseline',
    '-tune', 'zerolatency',
    '-f' ,'rawvideo',
    '-'
];</pre>
<p>然后运行<pre class="crayon-plain-tag">node server-ffmpeg</pre>，打开http://127.0.0.1:8080/，可以看到自己摄像头传来的H.264码流，效果还不错。</p>
<div class="blog_h2"><span class="graybg">服务器端技术</span></div>
<div class="blog_h3"><span class="graybg">ffpmeg</span></div>
<p>老牌的编解码库，支持很多的音频、视频格式的编解码，支持多种容器格式，支持多种流协议。关于ffpmeg的详细介绍参见<a href="/linux-command-faq#ffmpeg">Linux命令知识集锦</a>。</p>
<p>ffpmeg除了提供开发套件之外，还有一个同名的命令行工具，直接使用它就可以完成很多编解码、流转换的工作。</p>
<p>类似的库是libav，ffpmeg和它的功能非常相似，特性更多一些。</p>
<div class="blog_h3"><span class="graybg">x264</span></div>
<p>官网自称是最好的H.264编码器。特性包括：</p>
<ol>
<li>提供一流的性能、压缩比。特别是性能方面，可以在普通PC上并行编码4路或者更多的1080P流</li>
<li>提供最好的视频质量，具有最高级的心理视觉优化</li>
<li>支持多种不同应用程序所需要的特性，例如电视广播、蓝光低延迟视频应用、Web视频</li>
</ol>
<div class="blog_h1"><span class="graybg">流媒体技术</span></div>
<p>有了上面介绍的HTML5标签、合理编码的视频格式，就可以实现简单的监控录像回放了。但是，要进行实时监控画面预览则没有这么简单，必须依赖流媒体技术实现。</p>
<div class="blog_h2"><span class="graybg">流媒体</span></div>
<p>所谓多媒体（Multimedia）是指多种内容形式 —— 文本、音频、视频、图片、动画等的组合。</p>
<p>所谓流媒体，就是指源源不断的由提供者产生，并持续的被终端用户接收、展示的多媒体，就像水流一样。现实世界中的媒体，有些天生就是流式的，例如电视、广播，另外一些则不是，例如书籍、CD。</p>
<p>流媒体技术（从传递媒体角度来看）可以作为文件下载的替代品。</p>
<p><span style="background-color: #c0c0c0;">流媒体技术关注的是如何传递媒体，而不是如何编码媒体</span>，具体的实现就是各种流媒体协议。封装后的媒体比特流（容器格式）由流媒体服务器递送到流媒体客户端。流媒体协议可能对底层容器格式、编码格式有要求，也可能没有任何要求。</p>
<div class="blog_h2"><span class="graybg">直播</span></div>
<p>直播流（Live streaming）和静态文件播放的关键差异：</p>
<ol>
<li>点播的目标文件通常位于服务器上，具有一定的播放时长、文件大小。浏览器可以使用渐进式下载，一边下载一边播放</li>
<li>直播不存在播放起点、终点。它表现为一种流的形式，源源不断的从视频采集源通过服务器，传递到客户端</li>
<li>直播流通常是自适应的（adaptive），其码率随着客户端可用带宽的变化，可能变大、变小，以尽可能消除延迟</li>
</ol>
<p>流媒体技术不但可以用于监控画面预览，也可以改善录像播放的用户体验，比起简单的静态文件回放，流式回放具有以下优势：</p>
<ol>
<li>延迟相对较低，播放能够尽快开始</li>
<li>自适应流可以避免卡顿</li>
</ol>
<div class="blog_h2"><span class="graybg">流协议</span></div>
<p>主流的用于承载视频流的流媒体协议包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 18%; text-align: center;">协议</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>HLS</td>
<td>
<p>HTTP实时流（HTTP Live Streaming），由苹果开发，基于HTTP协议</p>
<p>HLS的工作原理是，把整个流划分成一个个较小的文件，客户端在建立流媒体会话后，基于HTTP协议下载流片段并播放。客户端可以从多个服务器（源）下载流。</p>
<p>在建立会话时，客户端需要下载extended M3U (m3u8) 播放列表文件，其中包含了MPEG-2 TS（Transport Stream）容器格式的视频的列表。在播放完列表中的文件后，需要再次下载<span style="color: #444444;">m3u8，如此循环</span></p>
<p>此协议在移动平台上支持较好，目前的Android、iOS版本都支持</p>
<p>此协议的重要缺点是高延迟（5s以上通常），要做到低延迟会导致频繁的缓冲（下载新片段）并对服务器造成压力，不适合视频监控</p>
<p>播放HLS流的HTML代码片段：</p>
<pre class="crayon-plain-tag">&lt;video src="http://movie.m3u8" height="329" width="480"&gt;&lt;/video&gt;</pre>
</td>
</tr>
<tr>
<td>RTMP</td>
<td>
<p>实时消息协议（Real Time Messaging Protocol），由Macromedia（Adobe）开发。此协议实时性很好，需要Flash插件才能在客户端使用，但是Adobe已经打算在不久的将来放弃对Flash的支持了
<p>有一个开源项目<a href="https://github.com/Bilibili/flv.js">HTML5 FLV Player</a>，它支持在没有Flash插件的情况下，播放Flash的视频格式FLV。此项目依赖于<a href="https://w3c.github.io/media-source/">MSE</a>，支持以下特性：</p>
<ol>
<li>支持H.264 + AAC/MP3编码的FLV容器格式的播放</li>
<li>分段（segmented）视频播放</li>
<li>基于HTTP的FLV低延迟实时流播放</li>
<li>兼容主流浏览器</li>
<li>资源占用低，可以使用客户端的硬件加速</li>
</ol>
</td>
</tr>
<tr>
<td>RTSP</td>
<td>
<p>实时流协议（Real Time Streaming Protocol），由RealNetworks等公司开发。此协议负责控制通信端点（Endpoint）之间的媒体会话（media sessions） —— 例如播放、暂停、录制。通常需要结合：实时传输协议（Real-time Transport Protocol）、实时控制协议（Real-time Control Protocol）来实现视频流本身的传递</p>
<p>大部分浏览器没有对RTSP提供原生的支持</p>
<p>RTSP 2.0版本目前正在开发中，和旧版本不兼容</p>
</td>
</tr>
<tr>
<td>MPEG-DASH</td>
<td>
<p>基于HTTP的动态自适应流（Dynamic Adaptive Streaming over HTTP），它类似于HLS，也是把流切分为很小的片段。DASH为支持为每个片段提供多种码率的版本，以满足不同客户带宽</p>
<p>协议的客户端根据自己的可用带宽，选择尽可能高（避免卡顿、重新缓冲）的码率进行播放，并根据网络状况实时调整码率</p>
<p>DASH不限制编码方式，你可以使用H.265, H.264, VP9等视频编码算法</p>
<p>Chrome 24+、Firefox 32+、Chrome for Android、IE 10+支持此格式</p>
<p>类似于HLS的高延迟问题也存在</p>
</td>
</tr>
<tr>
<td>WebRTC</td>
<td>
<p>WebRTC是一整套API，为浏览器、移动应用提供实时通信（RealTime Communications）能力。它包含了流媒体协议的功能，但是不是以协议的方式暴露给开发者的</p>
<p>WebRTC支持Chrome 23+、Firefox 22+、Chrome for Android，提供Java / Objective-C绑定</p>
<p>WebRTC主要有三个职责：</p>
<ol>
<li>捕获客户端音视频，对应接口MediaStream（也就是getUserMedia）</li>
<li>音视频传输，对应接口RTCPeerConnection</li>
<li>任意数据传输，对应接口RTCDataChannel</li>
</ol>
<p>WebRTC内置了点对点的支持，也就是说流不一定需要经过服务器中转</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">服务器端技术</span></div>
<p>视频监控通常都是CS模式（而非P2P），在服务器端，你需要部署流媒体服务。</p>
<div class="blog_h3"><span class="graybg">GStreamer</span></div>
<p><a href="https://gstreamer.freedesktop.org/">这是</a>一个开源的跨平台多媒体框架。通过它你可以构建各种各样的媒体处理组件，包括流媒体组件。通过插件机制，GStreamer支持上百种编码格式，包括MPEG-1, MPEG-2, MPEG-4, H.261, H.263, H.264, RealVideo, MP3, WMV, FLV</p>
<p><a href="https://www.kurento.org/">Kurento</a>、<a href="http://www.flumotion.net/features/">Flumotion</a>是基于GStreamer构建的流媒体服务器软件。</p>
<div class="blog_h3"><span class="graybg">Live555</span></div>
<p><a href="http://www.live555.com/">Live555</a>是流媒体服务开发的基础库，支持 RTP/RTCP/RTSP/SIP等协议，适合在硬件资源受限的情况下使用（例如嵌入式设备）。</p>
<p>基于Live555的软件包括：</p>
<ol>
<li>Live555媒体服务器，完整的RTSP服务器</li>
<li>openRTSP，一个命令行程序，支持提供RTSP流、接收RTSP流、把RTSP流中的媒体录像到磁盘</li>
<li>playSIP，可以进行VoIP通话</li>
<li>liveCaster，支持组播的MP3流媒体服务</li>
</ol>
<div class="blog_h3"><span class="graybg">其它</span></div>
<p>流媒体服务实现有很多，它们中的一些在最初针对特定的流协议，大部分都走向多元化。例如，Red5是一个RTMP流媒体服务器，Wowza是一个综合的流媒体服务器，支持WebRTC的流媒体服务在后面的章节介绍。</p>
<div class="blog_h1"><span class="graybg">HTML5媒体标签</span></div>
<p>HTML5支持<pre class="crayon-plain-tag">&lt;audio&gt;</pre>和<pre class="crayon-plain-tag">&lt;video&gt;</pre>标签（两者都对应了HTMLMediaElement的子类型）以实现视频、音频的播放。</p>
<div class="blog_h2"><span class="graybg">&lt;audio&gt;</span></div>
<p>此标签用于在浏览器中创建一个纯音频播放器。播放静态文件的示例：</p>
<pre class="crayon-plain-tag">&lt;audio controls preload="auto"&gt;
    &lt;source src="song.mp3" type="audio/mpeg"&gt;
    &lt;!-- 备选格式，如果浏览器不支持mp3 --&gt;
    &lt;source src="song.ogg" type="audio/ogg"&gt;
    &lt;!-- 如果浏览器不支持audio标签，显示下面的连接 --&gt;
    &lt;a href="audiofile.mp3"&gt;download audio&lt;/a&gt;
&lt;/audio&gt;</pre>
<div class="blog_h2"><span class="graybg">&lt;video&gt;</span></div>
<p>此标签用于在浏览器中创建一个视频播放器。播放静态文件的示例：</p>
<pre class="crayon-plain-tag">&lt;!-- poster指定预览图，autoplay自动播放，muted静音 --&gt;
&lt;video controls width="640" height="480" poster="movie.png" autoplay muted&gt;
  &lt;source src="movie.mp4" type="video/mp4"&gt;
  &lt;!-- 备选格式，如果浏览器不支持mp4 --&gt;
  &lt;source src="movie.webm" type="video/webm"&gt;
  &lt;!-- 可以附带字幕 --&gt;
  &lt;track src="subtitles_en.vtt" kind="subtitles" srclang="en" label="English"&gt;
  &lt;!-- 如果浏览器不支持video标签，显示下面的连接 --&gt;
  &lt;a href="videofile.mp4"&gt;download video&lt;/a&gt;
&lt;/video&gt;</pre>
<div class="blog_h2"><span class="graybg">&lt;canvas&gt;</span></div>
<p>在画布中，你可以进行任意的图形绘制，当然可以去逐帧渲染视频内容。</p>
<div class="blog_h2"><span class="graybg">编程方式创建</span></div>
<p>音频、视频播放器标签也可以利用JavaScript编程式的创建，示例代码：</p>
<pre class="crayon-plain-tag">var video = document.createElement( 'video' );
if ( video.canPlayType( 'video/mp4' ) ) {
    video.setAttribute( 'src', 'movie.mp4' );
}
else if ( video.canPlayType( 'video/webm' ) ) {
    video.setAttribute( 'src', 'movie.webm' );
}
video.width = 640;
video.height = 480; </pre>
<div class="blog_h1"><span class="graybg">MSE</span></div>
<p>媒体源扩展（Media Source Extensions，MSE）是一个W3C草案，<a href="http://caniuse.com/#feat=mediasource">桌面浏览器对MSE的支持较好</a>。MSE扩展流video/audio元素的能力，允许你<span style="background-color: #c0c0c0;">通过JavaScript来生成（例如从服务器抓取）媒体流供video/audio元素播放</span>。使用MSE你可以：</p>
<ol>
<li>通过JavaScript来构建媒体流，不管媒体是如何捕获的</li>
<li>处理自适应码流、广告插入、时间平移（time-shifting，回看）、视频编辑等应用场景</li>
<li>最小化JavaScript中处理媒体解析的代码</li>
</ol>
<p>MSE定义支持的（你生成的）<a href="https://www.w3.org/TR/media-source/">媒体格式</a>，只有符合要求的容器格式、编码格式才能被MSE处理。通常容器格式是<span style="color: #24292e;">ISO BMFF（MP4），也就是说你需要生成MP4的片断，然后Feed给MSE进行播放。</span></p>
<p>MediaSource对象作为video/audio元素的媒体来源，它可以具有多个SourceBuffer对象。应用程序把数据片段（segment）附加到SourceBuffer中，并可以根据系统性能对数据片段的质量进行适配。SourceBuffer中包含多个track buffer —— 分别对应音频、视频、文本等可播放数据。这些数据被音频、视频解码器解码，然后在屏幕上显示、在扬声器中播放：</p>
<p> <img class="aligncenter size-large wp-image-15564" src="https://blog.gmem.cc/wp-content/uploads/2017/08/pipeline_model.png" alt="pipeline_model" width="710" height="516" /></p>
<p>要把MediaSource提供给video/audio播放，调用：</p>
<pre class="crayon-plain-tag">video.src = URL.createObjectURL(mediaSource);</pre>
<div class="blog_h2"><span class="graybg">基于MSE的框架</span></div>
<div class="blog_h3"><span class="graybg">wfs</span></div>
<p><a href="https://github.com/ChihChengYang/wfs.js">wfs</a>是一个播放原始H.264帧的HTML5播放器，它的工作方式是把H.264 NAL单元封装为 ISO BMFF（MP4）片，然后Feed给MSE处理。</p>
<div class="blog_h3"><span class="graybg">flv.js</span></div>
<p><a href="https://github.com/Bilibili/flv.js">flv.js</a>是一个HTML5 Flash视频播放器，基于纯JS，不需要Flash插件的支持。此播放器将FLV流转换为ISO BMFF（MP4）片断，然后把MP4片断提供给video元素使用。</p>
<p>flv.js支持Chrome 43+, FireFox 42+, Edge 15.15048+以上版本的直播流 。</p>
<div class="blog_h3"><span class="graybg">Streamedian</span></div>
<p><a href="https://github.com/Streamedian/html5_rtsp_player/wiki/HTML5-RTSP-Player">Streamedian</a>是一个HTML5的RTSP播放器。实现了RTSP客户端功能，你可以利用此框架直接播放RTSP直播流。此播放器把RTP协议下的H264/AAC在转换为ISO BMFF供video元素使用。Streamedian支持Chrome 23+, FireFox 42+, Edge 13+，以及Android 5.0+。不支持iOS和IE。</p>
<p>在服务器端，你需要安装Streamedian提供的代理（此代理收费），此代理将RTSP转换为WebSocket。Streamedian处理视频流的流程如下：<img class="aligncenter size-large wp-image-15609" src="https://blog.gmem.cc/wp-content/uploads/2017/08/streamedian-1024x391.png" alt="streamedian" width="710" height="271" /></p>
<div class="blog_h1"><span class="graybg">WebRTC</span></div>
<p>WebRTC是一整套API，其中一部分供Web开发者使用，另外一部分属于要求浏览器厂商实现的接口规范。WebRTC解决诸如客户端流媒体发送、点对点通信、视频编码等问题。<a href="http://iswebrtcreadyyet.com/legacy.html">桌面浏览器对WebRTC的支持较好</a>，WebRTC也很容易和Native应用集成。</p>
<p>使用MSE时，你需要自己构建视频流。使用WebRTC时则可以直接捕获客户端视频流。</p>
<p>使用WebRTC时，大部分情况下流量不需要依赖于服务器中转，服务器的作用主要是：</p>
<ol>
<li>在信号处理时，转发客户端的数据</li>
<li>配合实现NAT/防火墙穿透</li>
<li>在点对点通信失败时，作为中继器使用</li>
</ol>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p><img class="aligncenter size-full wp-image-15576" src="https://blog.gmem.cc/wp-content/uploads/2017/08/webrtcArchitecture.png" alt="webrtcarchitecture" width="100%" /></p>
<div class="blog_h2"><span class="graybg">流捕获</span></div>
<div class="blog_h3"><span class="graybg">捕获视频</span></div>
<p>主要是捕获客户端摄像头、麦克风。在视频监控领域用处不大，这里大概了解一下。流捕获通过navigator.getUserMedia调用实现： </p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript"&gt;
    navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.getUserMedia;
    var success = function ( stream ) {
        var video = document.getElementById( 'camrea' );
        // 把MediaStream对象转换为Blob URL，提供给video播放
        video.src = URL.createObjectURL( stream );
        video.play();
    }
    var error = function ( err ) {
        console.log( err )
    }
    // 调用成功后，得到MediaStream对象
    navigator.getUserMedia( { video: true, audio: true }, success, error );
&lt;/script&gt;
&lt;video id="camrea" width="640" height="480"/&gt;</pre>
<p>三个调用参数分别是：</p>
<ol>
<li><a href="http://io13webrtc.appspot.com/#22">约束条件</a>，你可以指定媒体类型、分辨率、帧率 </li>
<li>成功后的回调，你可以在回调中解析出URL提供给video元素播放</li>
<li>失败后的回调</li>
</ol>
<div class="blog_h3"><span class="graybg">捕获音频</span></div>
<p>捕获音频类似：</p>
<pre class="crayon-plain-tag">navigator.getUserMedia( { audio: true }, function ( stream ) {
    var audioContext = new AudioContext();

    // 从捕获的音频流创建一个媒体源管理
    var streamSource = audioContext.createMediaStreamSource( stream );

    // 把媒体源连接到目标（默认是扬声器）
    streamSource.connect( audioContext.destination );
}, error );</pre>
<div class="blog_h3"><span class="graybg">MediaStream</span></div>
<p>MediaStream对象提供以下方法：</p>
<ol>
<li>getAudioTracks()，音轨列表</li>
<li>getVideoTracks()，视轨列表</li>
</ol>
<p>每个音轨、视轨都有个label属性，对应其设备名称。</p>
<div class="blog_h3"><span class="graybg">Camera.js</span></div>
<p><a href="https://github.com/idevelop/camera.js">Camera.js</a>是对getUserMedia的简单封装，简化了API并提供了跨浏览器支持：</p>
<pre class="crayon-plain-tag">camera.init( {
    width: 640,
    height: 480,
    fps: 30, // 帧率
    mirror: false,  // 是否显示为镜像
    targetCanvas: document.getElementById( 'webcam' ), // 默认null，如果设置了则在画布中渲染

    onFrame: function ( canvas ) {
        // 每当新的帧被捕获，调用此回调
    },

    onSuccess: function () {
        // 流成功获取后
    },

    onError: function ( error ) {
        // 如果初始化失败
    },

    onNotSupported: function () {
        // 当浏览器不支持camera.js时
    }
} );
// 暂停
camera.pause();
// 恢复
camera.start();</pre>
<p><a href="https://idevelop.ro/predator-vision/">掠食者视觉</a>是基于Camera实现的一个好玩的例子（移动侦测）。</p>
<div class="blog_h2"><span class="graybg">信号处理</span></div>
<p>在端点之间（Peer）发送流之前，需要进行通信协调、发送控制消息，即所谓信号处理（Signaling），信号处理牵涉到三类信息：</p>
<ol>
<li>会话控制信息：初始化、关闭通信，报告错误</li>
<li>网络配置：对于其它端点来说，本机的IP和端口是什么</li>
<li>媒体特性：本机能够处理什么音视频编码、多高的分辨率。本机发送什么样的音视频编码</li>
</ol>
<p>WebRTC没有对信号处理规定太多，我们可以通过Ajax/WebSocket通信，以SIP、Jingle、ISUP等协议完成信号处理。点对点连接设立后，流的传输并不需要服务器介入。信号处理的示意图如下：</p>
<p><img class="aligncenter wp-image-15587 size-full" src="https://blog.gmem.cc/wp-content/uploads/2017/08/jsep.png" alt="jsep" width="100%" /></p>
<div class="blog_h3"><span class="graybg">示例代码</span></div>
<p>下面的代表片段包含了一个视频电话的信号处理过程：</p>
<pre class="crayon-plain-tag">// 信号处理通道，底层传输方式和协议自定义
var signalingChannel = createSignalingChannel();
var conn;

// 信号通过此回调送达本地，可能分多次送达
signalingChannel.onmessage = function ( evt ) {
    if ( !conn ) start( false );

    var signal = JSON.parse( evt.data );
    // 会话描述协议（Session Description Protocol），用于交换媒体配置信息（分辨率、编解码能力）
    if ( signal.sdp )
    // 设置Peer的RTCSessionDescription
        conn.setRemoteDescription( new RTCSessionDescription( signal.sdp ) );
    else
    // 添加Peer的Candidate信息
        conn.addIceCandidate( new RTCIceCandidate( signal.candidate ) );
};

// 调用此方法启动WebRTC，获取本地流并显示，侦听连接上的事件并处理
function start( isCaller ) {
    conn = new RTCPeerConnection( { /**/ } );

    // 把地址/端口信息发送给其它Peer。所谓Candidate就是基于ICE框架获得的本机可用地址/端口
    conn.onicecandidate = function ( evt ) {
        signalingChannel.send( JSON.stringify( { "candidate": evt.candidate } ) );
    };

    // 当远程流到达后，在remoteView元素中显示
    conn.onaddstream = function ( evt ) {
        remoteView.src = URL.createObjectURL( evt.stream );
    };

    // 获得本地流
    navigator.getUserMedia( { "audio": true, "video": true }, function ( stream ) {
        // 在remoteView元素中显示
        localView.src = URL.createObjectURL( stream );
        // 添加本地流，Peer将接收到onaddstream事件
        conn.addStream( stream );


        if ( isCaller )
        // 获得本地的RTCSessionDescription
            conn.createOffer( gotDescription );
        else
        // 针对Peer的RTCSessionDescription生成兼容的本地SDP
            conn.createAnswer( conn.remoteDescription, gotDescription );

        function gotDescription( desc ) {
            // 设置自己的RTCSessionDescription
            conn.setLocalDescription( desc );
            // 把自己的RTCSessionDescription发送给Peer
            signalingChannel.send( JSON.stringify( { "sdp": desc } ) );
        }
    } );
}

// 通信发起方调用：
start( true );</pre>
<div class="blog_h2"><span class="graybg">流转发</span></div>
<p>主要牵涉到的接口是RTCPeerConnection，上面的例子中已经包含了此接口的用法。WebRTC在底层做很多复杂的工作，这些工作对于JavaScript来说是透明的： </p>
<ol>
<li>执行解码</li>
<li>屏蔽丢包的影响</li>
<li>点对点通信：WebRTC引入流交互式连接建立（Interactive Connectivity Establishment，ICE）框架。ICE负责建立点对点链路的建立：
<ol>
<li>首先尝试直接</li>
<li>不行的话尝试STUN（Session Traversal Utilities for NAT）协议。此协议通过一个简单的保活机制确保NAT端口映射在会话期间有效</li>
<li>仍然不行尝试TURN（Traversal Using Relays around NAT）协议。此协议依赖于部署在公网上的中继服务器。只要端点可以访问TURN服务器就可以建立连接</li>
</ol>
</li>
<li>通信安全</li>
<li>带宽适配</li>
<li>噪声抑制</li>
<li>动态抖动缓冲（dynamic jitter buffering），抖动是由于网络状况的变化，缓冲用于收集、存储数据，定期发送</li>
</ol>
<div class="blog_h2"><span class="graybg">任意数据交换</span></div>
<p>通过RTCDataChannel完成，允许点对点之间任意的数据交换。RTCPeerConnection连接创建后，不但可以传输音视频流，还可以打开多个信道（RTCDataChannel）进行任意数据的交换。RTCDataChanel的特点是：</p>
<ol>
<li>类似于WebSocket的API</li>
<li>支持带优先级的多通道</li>
<li>超低延迟，因为不需要通过服务器中转</li>
<li>支持可靠/不可靠传输语义。支持SCTP、DTLS、UDP几种传输协议</li>
<li>内置安全传输（DTLS）</li>
<li>内置拥塞控制</li>
</ol>
<p>使用RTCDataChannel可以很好的支持游戏、远程桌面、实时文本聊天、文件传输、去中心化网络等业务场景。</p>
<div class="blog_h2"><span class="graybg">adapter.js</span></div>
<p><a href="https://github.com/webrtc/adapter">WebRTC adapter</a>是一个垫片库，使用它开发WebRTC应用时，不需要考虑不同浏览器厂商的<a href="https://webrtc.org/web-apis/interop/">API前缀差异</a>。</p>
<div class="blog_h2"><span class="graybg">WebRTC示例</span></div>
<p>本节列出一些WebRTC的代码示例，这些例子都使用adapter.js。</p>
<div class="blog_h3"><span class="graybg">限定分辨率</span></div>
<pre class="crayon-plain-tag">// 指定分辨率
// adapter.js 支持Promise
navigator.mediaDevices.getUserMedia( { video: { width: { exact: 640 }, height: { exact: 480 } } } ).then( stream =&gt; {
    let video = document.createElement( 'video' );
    document.body.appendChild( video );
    video.srcObject = stream;
    video.play();
} ).catch( err =&gt; console.log( err ) );</pre>
<div class="blog_h3"><span class="graybg">在画布中截图</span></div>
<pre class="crayon-plain-tag">// video为video元素
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);</pre>
<div class="blog_h2"><span class="graybg">WebRTC框架</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">框架</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td><a href="http://peerjs.com/">PeerJS </a></td>
<td>
<p>简化WebRTC的点对点通信、视频、音频调用</p>
<p>提供云端的PeerServer，你也可以自己搭建服务器</p>
</td>
</tr>
<tr>
<td><a href="https://github.com/peer5/sharefest">Sharefest</a></td>
<td>基于Web的P2P文件共享</td>
</tr>
<tr>
<td><a href="https://github.com/webRTC-io/webRTC.io">webRTC.io</a></td>
<td>
<p>WebRTC的一个抽象层，同时提供了客户端、服务器端Node.js组件。服务器端组件抽象了STUN</p>
<p>类似的框架还有<a href="https://github.com/andyet/SimpleWebRTC">SimpleWebRTC</a>、<a href="https://github.com/priologic/easyrtc">easyrtc</a></p>
</td>
</tr>
<tr>
<td><a href="https://www.openwebrtc.org/">OpenWebRTC</a></td>
<td>
<p>允许你构建能够和遵循WebRTC标准的浏览器进行通信的Native应用程序，支持Java绑定</p>
</td>
</tr>
<tr>
<td><a href="https://nextrtc.org/">NextRTC</a></td>
<td>
<p>基于Java实现的WebRTC信号处理服务器</p>
</td>
</tr>
<tr>
<td><a href="https://github.com/meetecho/janus-gateway">Janus</a></td>
<td>
<p>这是一个WebRTC网关，纯服务器端组件，目前仅仅支持Linux环境下安装</p>
<p>Janus本身实现了到浏览器的WebRTC连接机制，支持以JSON格式交换数据，支持在服务器端应用逻辑 - 浏览器之间中继RTP/RTCP和消息。特殊化的功能有服务器端插件完成</p>
<p>官网地址：<a href="https://janus.conf.meetecho.com/index.html">https://janus.conf.meetecho.com</a></p>
</td>
</tr>
<tr>
<td><a href="https://www.kurento.org">Kurento</a></td>
<td>
<p>这是一个开源的WebRTC媒体服务器</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">备选方案一：从RTSP开始</span></div>
<p>我们首先尝试的方案是直接使用RTSP源，原因是海康、大华主流厂商的较新的IP摄像头均支持暴露标准化的RTSP流。</p>
<div class="blog_h2"><span class="graybg">尝试播放</span></div>
<p>使用VLC播放器，打开网络串流：rtsp://admin:12345@192.168.0.196:554/ch1/main/av_stream，视频源为公司门口的海康摄像头的主码流（main，子码流为sub）。</p>
<p>发现可以正常播放，说明视频格式应该是标准的。VLC菜单 Tool ⇨ Codec Info查看，编码格式为H264。</p>
<p>浏览器无法直接使用RTSP协议，因此，需要有服务器端来处理视频源的RTSP，将其转换为：</p>
<ol>
<li>通过WebSocket发送的视频片断，由客户端的：
<ol>
<li>JSMpeg/Broadway直接解码，渲染到画布</li>
<li>或者，构造MP4片断Feed给MSE播放</li>
</ol>
</li>
<li>或者，通过WebRTC网关，转换后提供给客户端的WebRTC代码处理</li>
<li>或者，使用浏览器插件机制，例如Chrome的NaCl</li>
</ol>
<div class="blog_h2"><span class="graybg">实现方式一：MSE</span></div>
<p>Streamedian的服务器端需要授权，我们选用了另外一个实现。</p>
<p><span style="color: #24292e;"><a href="https://github.com/veyesys/h5stream">H5S</a>是一个基于live555实现的开源的HTML5 RTSP网关，支持将RTSP/H264流输入转换为HTML5 MSE支持的H264，客户端基于MSE。</span></p>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<p>尝试在容器中运行H5S：</p>
<pre class="crayon-plain-tag">docker create --name ubuntu-16.04 -h ubuntu-16 --network local --dns 172.21.0.1 --ip 172.21.0.6 -it docker.gmem.cc/ubuntu:16.04 bash
docker start ubuntu-16.04
docker exec -it ubuntu-16.04 bash

apt update &amp;&amp; apt install wget
wget https://raw.githubusercontent.com/veyesys/release/master/h5stream/H5S-r1.0.1128.16-Ubuntu-16.04-64bit.tar.gz
tar xzf H5S-r1.0.1128.16-Ubuntu-16.04-64bit.tar.gz &amp;&amp; mv H5S-r1.0.1128.16-Ubuntu-16.04-64bit h5s-1.0

cd h5s-1.0
export LD_LIBRARY_PATH=`pwd`/lib/:$LD_LIBRARY_PATH
# 指定两次密码，可能H5S存在bug，不这样报身份验证失败
./h5ss rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream admin 12345</pre>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<p>使用H5S自带的基于MSE的客户端代码 + Chrome 49，播放后发现画面静止。控制它查看发现解码错误。打开chrome://media-internals/，发现错误Media segment did not begin with key frame. Support for such segments will be available in a future version。看样子是提供给SourceBuffer的数据不是以关键帧开始导致，未来版本的Chrome可能取消此限制。</p>
<p>换成Chrome 50，可以正常播放，但是流畅度较差，播放一段时间后出现卡死的情况。</p>
<div class="blog_h3"><span class="graybg">小结</span></div>
<p>H5S实现不完善，在不修改源码的情况下，服务器端只能接入一路视频输入。客户端也存在不流畅、卡死的问题，不适合生产环境。</p>
<div class="blog_h2"><span class="graybg">实现方式二：JSMpeg</span></div>
<div class="blog_h3"><span class="graybg">转码进程</span></div>
<p>在上文中我们已经成功尝试了利用JSMpege + WebSocket的方式，在网页中显示摄像头捕获的视频。ffmpeg转换RTSP也是非常简单的：</p>
<pre class="crayon-plain-tag">ffmpeg -i rtsp://admin:12345@192.168.0.196:554/ch1/main/av_stream -s 427x240 -f mpegts -vcodec mpeg1video -b 800k -r 30 http://127.0.0.1:8800/12345</pre>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<p>可以使用JSMpeg自带的简单Node.js服务器测试：</p>
<pre class="crayon-plain-tag">node ./app/websocket-relay.js 12345 8800 8801 </pre>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<p>下面是客户端代码，默认JSMpeg会基于WebGL渲染，但是我的机器最多开到8画面，开9画面时出现警告：</p>
<p>Too many active WebGL contexts. Oldest context will be lost，且第一画面丢失，简单的通融方法是，第9画面使用Canvas2D渲染：</p>
<pre class="crayon-plain-tag">new JSMpeg.Player( 'ws://127.0.0.1:8801/', {
    canvas: document.getElementById( 'canvas9' ),
    autoplay: true,
    // 浏览器对WebGL context的数量有限制
    disableGl: true
} ); </pre>
<p>渲染截图：</p>
<p><img class="aligncenter size-large wp-image-15669" src="https://blog.gmem.cc/wp-content/uploads/2017/08/jsmpeg-s9-1024x621.png" alt="jsmpeg-s9" width="100%" /></p>
<div class="blog_h3"><span class="graybg">小结</span></div>
<p>这种方式客户端解码压力较大，同时开9画面的352x288视频，我的机器上CPU占用率大概到40%左右，画面变化较为剧烈的时候会出现卡顿现象。</p>
<div class="blog_h2"><span class="graybg">实现方式三：Broadway</span></div>
<p>与JSMpeg类似，Broadway也是JavaScript解码工具。关键之处是，Broadway支持的视频编码是H.264，意味着可能免去消耗服务器资源的视频重编码。</p>
<p><a id="hk-av-config"></a>最初的尝试并不顺利，根据IP摄像头的RTSP Describe应答（SDP），我们推断其H.264 Profile为Baseline，但是不转码的情况下Broadway根本无法播放。后来查看ffmpeg的日志输出，发现其实际上使用的Profile是Main。进一步尝试，发现摄像头是可以配置为Baseline的：</p>
<p><img class="aligncenter size-full wp-image-16196" src="https://blog.gmem.cc/wp-content/uploads/2017/08/hk-config.png" alt="hk-config" width="707" height="465" /></p>
<p>只需要把编码复杂度设置为低，H.264的Profile就从Main变为Baseline。</p>
<p>设置完毕后，仍然基于h264-live-player的Demo进行测试，使用如下命令行抽取原始H.264帧：</p>
<pre class="crayon-plain-tag">ffmpeg -i rtsp://admin:12345@192.168.0.196:554/ch1/main/av_stream -c:v copy -f rawvideo  -</pre>
<p>即可免转码的进行实时视频预览了。 </p>
<p>此实现方式更多细节信息请参考<a href="/html5-vs-with-broadway">基于Broadway的HTML5视频监控</a>。</p>
<div class="blog_h2"><span class="graybg">实现方式四：NaCl</span></div>
<p>Chrome放弃NPAPI之后，插件开发需要使用PPAPI /NaCl。目前能找到的实现有<a href="https://www.videoexpertsgroup.com/vxg-chrome-plugin/">VXG Chrome Plugin</a>，这是一个商业产品，需要授权。除了RTSP之外，还支持RTMP、HLS等协议。</p>
<p>插件方案的缺点是，需要安装，而且仅仅针对单种浏览器。优势则是灵活性高，理论上性能可以做的很好。</p>
<div class="blog_h2"><span class="graybg">实现方式五：WebRTC</span></div>
<p>WebRTC相关的框架非常多，经过简单的比较，我们决定从Kurento入手。主要原因是：</p>
<ol>
<li>容易扩展的模块化设计</li>
<li>提供Java客户端、JS客户端</li>
<li>可以在服务器端合成多画面，这样可以减轻客户端解码压力，特别是那些低配置的客户端</li>
<li>内置对RTSP协议的支持</li>
</ol>
<p><a href="/webrtc-server-basedon-kurento#vs">基于Kurento搭建WebRTC服务器</a>一文详细讨论了这种实现方式。</p>
<div class="blog_h1"><span class="graybg">备选方案二：从设备SDK开始</span></div>
<p>这里的设备，主要包括：网络硬盘录像机（NVR）、视频服务器、IP摄像头。为了便于二次开发，硬件厂商都为这些设备配置的相应的SDK套件。这些SDK通常都提供了：实时码流预览、录像文件回放、播放控制（如：暂停、单帧前进、单帧后退）、获取码流基本信息、播放截图等功能。</p>
<p>我们的基本目标是，通过SDK得到标准化的码流，例如H264格式。具体如何操作，得看厂商的SDK，但是思路基本是：</p>
<ol>
<li>如果SDK直接支持获取标准格式的流，例如RTSP，那么备选方案一就可以直接用上</li>
<li>如果SDK支持获取标准编码的视频帧，例如H264，那我们只需要将其包装为合适的容器格式，再通过RTSP/HTTP的方式发送出去</li>
<li>如果SDK支持获取解码后的原始图像数据，例如RGB、YV12，我们可以基于H264再次编码，然后按第2步方式处理。这种方式对服务器性能要求比较高，CPU压力较大，PC机处理不了多少个通道</li>
<li>如果都不支持，只提供了封装好的播放控件 —— 这个就比较悲催了，不过通过OS底层API，例如Windows的GDI应该也是可以实现，否则那些屏幕录像软件怎么做的呢？</li>
</ol>
<div class="blog_h2"><span class="graybg">海康SDK</span></div>
<p>根据Linux版本的海康设备网络编程指南的描述，我们应该可以：</p>
<ol>
<li>调用NET_DVR_Init进行SDK初始化</li>
<li>调用NET_DVR_Login登陆到目标设备</li>
<li>调用NET_DVR_RealPlay进行播放，此时返回一个实时播放句柄
<ol>
<li>如果设备支持RTSP协议取流：针对上述句柄调用NET_DVR_SetStandardDataCallBack，可以设置一个标准的数据回调函数，此回调会接受到标准码流，这对应上面的第1种思路</li>
<li>如果设备不支持RTSP协议取流：针对上述句柄调用NET_DVR_SetRealDataCallBack，然后通过PlayM4播放库中的PlayM4_SetDecCallBack回调得到<a href="/image-processing-faq#yv12">yv12</a>格式的原始图像。这对应上面的第3种思路</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">示例代码</span></div>
<p>cmake构建配置：</p>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 3.6)
project(hikvision)

include_directories(/home/alex/CPP/lib/hcnedsdk/include)

set(SOURCE_FILES getstream.cpp)
add_executable(getstream ${SOURCE_FILES})
target_link_libraries(getstream /home/alex/CPP/lib/hcnedsdk/lib/libhcnetsdk.so)</pre>
<p> C++代码：</p>
<pre class="crayon-plain-tag">#include &lt;HCNetSDK.h&gt;
#include &lt;stdio.h&gt;
#include &lt;cstring&gt;
#include &lt;unistd.h&gt;

// RTSP协议取流
void CALLBACK cbStdData( LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, DWORD dwUser ) {
    switch ( dwDataType ) {
        case NET_DVR_SYSHEAD:        // 系统头数据，回调的第一个包是40字节的文件头
            break;
        case NET_DVR_STREAMDATA:     // 基于私有协议时：视频流数据（包括复合流和音视频分开的视频流数据）
            break;
        case NET_DVR_STD_VIDEODATA:  // 基于标准协议时：标准视频流数据（RTP包）
            break;
        case NET_DVR_STD_AUDIODATA:  // 基于标准协议时：标准音频流数据
            break;
        case NET_DVR_SDP:            // SDP信息(RTSP传输时有效)
            break;
        case NET_DVR_PRIVATE_DATA:   // 私有数据,包括智能信息叠加等
            break;
    }
}

int main() {
    // SDK初始化
    BOOL result = NET_DVR_Init();
    if ( !result ) return 1;

    // 同步登陆
    NET_DVR_USER_LOGIN_INFO struLoginInfo = { 0 };
    struLoginInfo.bUseAsynLogin = 0;
    strcpy( struLoginInfo.sDeviceAddress, "192.168.0.196" );
    struLoginInfo.wPort = 8000;
    strcpy( struLoginInfo.sUserName, "admin" );
    strcpy( struLoginInfo.sPassword, "12345" );
    NET_DVR_DEVICEINFO_V40 struDevInfo = { 0 };
    LPNET_DVR_DEVICEINFO_V30 lpDevInfo30;
    long lUserID = NET_DVR_Login_V40( &amp;struLoginInfo, &amp;struDevInfo );
    if ( lUserID &lt; 0 ) {
        printf( "登陆失败，错误码 %d\n", NET_DVR_GetLastError());
        NET_DVR_Cleanup();
        return 1;
    } else {
        lpDevInfo30 = &amp;struDevInfo.struDeviceV30;
        printf( "成功登陆到设备：%s\n", lpDevInfo30-&gt;sSerialNumber );
        printf( "SDK字符串编码方式（1 GB2312，2 GBK，3 BIG5，6 UTF-8）：%d\n", struDevInfo.byCharEncodeType );
        printf( "设备类型（31 高清网络摄像机）：%d\n", lpDevInfo30-&gt;wDevType );
        printf( "模拟通道起始号：%d，模拟通道个数%d，数字通道起始号：%d，数字通道个数%d\n", lpDevInfo30-&gt;byStartChan, lpDevInfo30-&gt;byChanNum,
                lpDevInfo30-&gt;byStartDChan, lpDevInfo30-&gt;byIPChanNum + lpDevInfo30-&gt;byHighDChanNum &lt;&lt; 8 );
        printf( "主码流是否支持RTSP方式：%s，子码流是否支持RTSP方式：%s\n", lpDevInfo30-&gt;byMainProto &gt; 0 ? "是" : "否",
                lpDevInfo30-&gt;bySubProto &gt; 0 ? "是" : "否" );
    }

    // 启动预览
    NET_DVR_PREVIEWINFO struPrevInfo = { 0 };
    struPrevInfo.hPlayWnd = NULL;    // Linux 64 位系统不支持软解码功能
    struPrevInfo.lChannel = 1;       // 预览通道号
    struPrevInfo.dwStreamType = 0;   // 0-主码流， 1-子码流， 2-码流 3， 3-码流 4，以此类推
    struPrevInfo.dwLinkMode = 0;     // 0- TCP 方式， 1- UDP 方式， 2- 组播方式， 3- RTP 方式， 4-RTP/RTSP， 5-RSTP/HTTP
    struPrevInfo.bBlocked = 1;       // 0- 非阻塞取流， 1- 阻塞取流
    struPrevInfo.byProtoType = 1;    // 应用层取流协议使用RTSP
    LONG lRealHandle = NET_DVR_RealPlay_V40( lUserID, &amp;struPrevInfo, NULL, NULL );
    if ( lRealHandle == -1 ) {
        printf( "启动预览失败，错误码 %d\n", NET_DVR_GetLastError());
        NET_DVR_Logout( lUserID );
        NET_DVR_Cleanup();
        return 1;
    }

    if ( lpDevInfo30-&gt;byMainProto ) {
        printf( "设置获取标准码流的回调\n" );
        // 仅支持对 支持RTSP协议取流的设备的 标准码流回调
        NET_DVR_SetStandardDataCallBack( lRealHandle, cbStdData, NULL );
    }

    sleep( 120 );
    // 停止预览
    NET_DVR_StopRealPlay( lRealHandle );
    // 登出
    NET_DVR_Logout( lUserID );
    // SDK清理
    NET_DVR_Cleanup();
    return 0;
}</pre>
<p>运行脚本：</p>
<pre class="crayon-plain-tag">export HKLIB_HOME=/home/alex/CPP/lib/hcnedsdk/lib
export LD_LIBRARY_PATH=$HKLIB_HOME:$HKLIB_HOME/HCNetSDKCom
./getstream </pre>
<p>此程序运行后，会自动获取到基于RTSP协议的媒体流，回调函数会反复被调用：</p>
<ol>
<li>第一次调用为40字节的头，不太清楚有什么用</li>
<li>第二次调用传递了SDP</li>
<li>后续调用传递标准音视频数据，其内容是RTP封包</li>
</ol>
<div class="blog_h1"><span class="graybg">总结</span></div>
<p>基于HTM5的视频监控，媒体流从采集设备到浏览器，主要路径如下图所示：</p>
<p><img class="aligncenter size-full wp-image-15824" src="https://blog.gmem.cc/wp-content/uploads/2017/08/h5vs-dataflow.png" alt="h5vs-dataflow" width="100%" /></p>
<p>对上图的说明如下：</p>
<ol>
<li>在设备层，需要以某种方式获得码流，以流协议的方式发送出去。最常用的方式是RTSP/RTP。流的可能获取路径为：
<ol>
<li>设备直接暴露RTSP协议端点，并且发送标准码流</li>
<li>设备SDK允许获取标准码流，需要自己以RTSP协议发送</li>
<li>设备SDK允许获得解码后的逐帧，需要直接编码为H264，然后以RTSP发送</li>
</ol>
</li>
<li>流媒体层通常需要引入专门的流媒体服务器，这类服务器能够在内部进行各种流协议的转换，可以解除客户端对特定流协议的依赖</li>
<li>客户端和服务器端的传输方式，可以有TCP、HTTP、P2P（WebRTC）、WebSocket等多种。其中
<ol>
<li>直接的TCP协议浏览器是不支持的，这意味着RTSP/RTMP等协议，在浏览器端必须要有插件才可以使用</li>
<li>WebSocket通常配合JSMpeg或者MSE使用，由程序向JSMpeg/MSE不断Feed视频帧</li>
</ol>
</li>
<li>客户端解码展示的技术主要有三类：
<ol>
<li>浏览器内置的解码能力，主要通过video标签，MSE属于此类</li>
<li>JavaScript软解码，主要是JSMpeg、Broadway</li>
<li>插件机制，例如Chrome的NaCl</li>
</ol>
</li>
</ol>
<p>能够免于引入流媒体层的方案，需要：设备能直接暴露标准码流的RTSP端点，并且安装浏览器插件。缺点也很明显，一个是设备的访问密码暴露给了客户端，第二个是目前没有成熟、开源的插件可用。我相信主要原因是合理技术方向不在于此，没人愿意去开发。</p>
<p>直接使用设备层的RTSP端点，可能存在兼容性问题。一个是它发送的码流是否标准化，第二个是市场上有多少设备没有暴露RTSP端点。</p>
<p>客户端方面，JSMpeg是兼容性较好的方案，WebRTC/MSE都有部分平台不支持（但是桌面级的浏览器大部分支持）。JSMpeg的缺点是：</p>
<ol>
<li>如果基于WebGL渲染，受限于浏览器WebGL上下文最大数量，多画面可能无法渲染。某些流媒体服务器支持在服务器端合成多画面Grid，可以规避此缺点</li>
<li>如果基于Canvas2D渲染，画质较差（我的机器上还有莫名其妙的斜线）</li>
<li>对码流格式要求严格，仅仅支持MPEG-TS，此格式压缩比差，网络带宽占用大</li>
<li>性能相对较差，尽管使用了MPEG-TS这种简单的视频格式，基于JavaScript解码渲染仍然使客户端压力较大。我的机器（i7-4940MX / Quadro K5100M / Ubuntu 14.04 LTS）上会出现卡顿情况</li>
</ol>
<p>和JSMpeg类似的库是Broadway，后者能够进行Baseline的H.264解码。如果设备支持Baseline H.264输出，使用Broadway可以很好的解决服务器端转码导致的资源消耗问题。</p>
<div class="blog_h1"><span class="graybg">附录</span></div>
<div class="blog_h2"><span class="graybg">参考资料</span></div>
<ol>
<li><a href="https://developer.mozilla.org/en-US/Apps/Fundamentals/Audio_and_video_delivery">Audio and Video Delivery</a></li>
<li><a href="https://www.w3.org/TR/media-source/">W3C Recommendation - Media Source Extensions™</a></li>
<li><a href="https://webrtc.org">WebRTC Project Home</a></li>
<li><a href="https://imququ.com/post/html5-live-player-3.html">HTML5 视频直播（三）</a></li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/research-on-html5-video-surveillance">HTML5视频监控技术预研</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/research-on-html5-video-surveillance/feed</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
	</channel>
</rss>
