﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>BlogJava-聂永的博客-随笔分类-HTTP</title><link>http://www.blogjava.net/yongboy/category/54836.html</link><description>记录工作/学习的点点滴滴。</description><language>zh-cn</language><lastBuildDate>Wed, 24 Feb 2021 03:32:27 GMT</lastBuildDate><pubDate>Wed, 24 Feb 2021 03:32:27 GMT</pubDate><ttl>60</ttl><item><title>Apisix 1.5 升级到 2.2 踩坑备忘</title><link>http://www.blogjava.net/yongboy/archive/2021/02/23/435806.html</link><dc:creator>nieyong</dc:creator><author>nieyong</author><pubDate>Tue, 23 Feb 2021 06:57:00 GMT</pubDate><guid>http://www.blogjava.net/yongboy/archive/2021/02/23/435806.html</guid><wfw:comment>http://www.blogjava.net/yongboy/comments/435806.html</wfw:comment><comments>http://www.blogjava.net/yongboy/archive/2021/02/23/435806.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/yongboy/comments/commentRss/435806.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/yongboy/services/trackbacks/435806.html</trackback:ping><description><![CDATA[<h2 id="toc_1">零、前言</h2>
<p>线上运行的 APISIX 为 1.5 版本，而社区已经发布了 Apisix 2.2，是时候需要升级到最新版了，能够享受最版本带来的大量的BugFix，性能增强，以及新增特性的支持等~</p>
<p>从Apisix 1.5升级到Apisix 2.2过程中，不是一帆风顺的，中间踩了不少坑，所谓前车之鉴后事之师，这里给大家简单梳理一下我们团队所在具体业务环境下，升级过程中踩的若干坑，以及一些需要避免的若干注意事项等。</p>
<p>下文所说原先版本，皆指Apisix 1.5，新版则是Apisix 2.2版本。</p>
<h2 id="toc_2">一、已有服务发现机制无法正常工作</h2>
<p>针对上游Upstream没有使用服务发现的路由来讲，本次升级没有遇到什么问题。</p>
<p>公司内部线上业务大都基于Consul KV方式实现服务注册和服务发现，因此我们自行实现了一个 <code>consul_kv.lua</code> 模块实现服务发现流程。</p>
<p>这在Apisix 1.5下面一切工作正常。</p>
<p>但在Apisix 2.2下面，就无法直接工作了，原因如下：</p>
<ul>
<li>服务发现配置指令变了</li>
<li>上游对象包含服务发现时需增加字段 <code>discovery_type</code> 进行索引</li>
</ul>
<h3 id="toc_3">2.1 服务发现配置指令变了</h3>
<p>原先运行中仅支持一种服务发现机制，需要配置在 <code>apisix</code>层级下面：</p>
<pre class="line-numbers"><code class="language-yaml">apisix:
    ......
    discover: consul_kv
    ......    
</code></pre>
<p>新版需要直接在<code>config*.yaml</code>文件中顶层层级下进行配置，可支持多种不同的路由发现机制，如下：</p>
<pre class="line-numbers"><code class="language-yaml">discovery:                      # service discovery center
  eureka:
    host:                       # it's possible to define multiple eureka hosts addresses of the same eureka cluster.
      - "http://127.0.0.1:8761"
    prefix: "/eureka/"
    fetch_interval: 30          # default 30s
    weight: 100                 # default weight for node
    timeout:
      connect: 2000             # default 2000ms
      send: 2000                # default 2000ms
      read: 5000
</code></pre>
<p>我们有所变通，直接在配置文件顶层配置consul_kv多个集群相关参数，避免 <code>discovery</code> 层级过深。</p>
<pre class="line-numbers"><code class="language-text"> discovery:
    consul_kv: 1
consul_kv:
  servers:
    -
      host: "172.19.5.30"
      port: 8500
    -
      host: "172.19.5.31"
      port: 8500
  prefix: "upstreams"
  timeout:
    connect: 6000
    read: 6000
    wait: 60
  weight: 1
  delay: 5
  connect_type: "long" # long connect
  ......
</code></pre>
<p>当然，这仅仅保证了服务发现模块能够在启动时被正常加载。</p>
<p>推荐阅读：</p>
<ul>
<li><a href="https://github.com/apache/apisix/blob/master/doc/zh-cn/discovery.md#upstream-%E9%85%8D%E7%BD%AE">https://github.com/apache/apisix/blob/master/doc/zh-cn/discovery.md#upstream-%E9%85%8D%E7%BD%AE</a></li>
</ul>
<h3 id="toc_4">2.2  upstream对象新增字段discovery_type</h3>
<p>Apisix当前同时支持多种服务发现机制，这个很赞。对应的代价，就是需要额外引入 <code>discovery_type</code> 字段，用于索引可能同时存在的多个服务发现机制。</p>
<p>以 Cousul KV方式服务发现为例，那么需要在已有的 <code>upstream</code> 对象中需要添加该字段：</p>
<pre class="line-numbers"><code class="language-javascript">"discovery_type" : "consul_kv"
</code></pre>
<p>原先的一个<code>upstream</code>对象，仅仅需要 <code>service_name</code> 字段属性指定服务发现相关地址即可：</p>
<pre class="line-numbers"><code class="language-javascript">{
    "id": "d6c1d325-9003-4217-808d-249aaf52168e",
    "name": "grpc_upstream_hello",
    ......
    "service_name": "http://172.19.5.30:8500/v1/kv/upstreams/grpc/grpc_hello",
    "create_time": 1610437522,
    "desc": "demo grpc service",
    "type": "roundrobin"
}
</code></pre>
<p>而新版的则需要添加<code>discovery_type</code>字段，表明该<code>service_name</code> 字段对应的具体模块名称，效果如下：</p>
<pre class="line-numbers"><code class="language-javascript">{
    "id": "d6c1d325-9003-4217-808d-249aaf52168e",
    "name": "grpc_upstream_hello",
    ......
    "service_name": "http://172.19.5.30:8500/v1/kv/upstreams/grpc/grpc_hello",
    "create_time": 1610437522,
    "desc": "demo grpc service",
    "type": "roundrobin",
    "discovery_type":"consul_kv"
}
</code></pre>
<p>后面我们若支持Consul Service或ETCD KV方式服务发现机制，则会非常弹性和清晰。</p>
<p>调整了配置指令，添加上述字段之后，后端服务发现其实就已经起作用了。</p>
<p>但gRPC代理路由并不会生效&#8230;&#8230;</p>
<h2 id="toc_5">二、gRPC当前不支持upstream_id</h2>
<p>在我们的系统中，上游和路由是需要单独分开管理的，因此创建的HTTP或GRPC路由需要处理支持<code>upstream_id</code>的索引。</p>
<p>这在1.5版本中，grpc路由是没问题的，但到了apisix 2.2版本中，维护者 <code>@spacewander</code> 暂时没做支持，原因是规划grpc路由和dubbo路由处理逻辑趋于一致，更为紧凑。从维护角度我是认可的，但作为使用者来讲，这就有些不合理了，直接丢弃了针对以往数据的支持。</p>
<p>作为当前Geek一些方式，在 <code>apisix/init.lua</code> 中，最小成本 （优雅和成本成反比）修改如下，找到如下代码：</p>
<pre class="line-numbers"><code class="language-lua">    -- todo: support upstream id
    api_ctx.matched_upstream = (route.dns_value and
                                route.dns_value.upstream)
                               or route.value.upstream 
</code></pre>
<p>直接替换为下面代码即可解决燃眉之急：</p>
<pre class="line-numbers"><code class="language-lua">    local up_id = route.value.upstream_id
    if up_id then
        local upstreams = core.config.fetch_created_obj("/upstreams")
        if upstreams then
            local upstream = upstreams:get(tostring(up_id))
            if not upstream then
                core.log.error("failed to find upstream by id: " .. up_id)
                return core.response.exit(502)
            end
            if upstream.has_domain then
                local err
                upstream, err = lru_resolved_domain(upstream,
                                                    upstream.modifiedIndex,
                                                    parse_domain_in_up,
                                                    upstream)
                if err then
                    core.log.error("failed to get resolved upstream: ", err)
                    return core.response.exit(500)
                end
            end
            if upstream.value.pass_host then
                api_ctx.pass_host = upstream.value.pass_host
                api_ctx.upstream_host = upstream.value.upstream_host
            end
            core.log.info("parsed upstream: ", core.json.delay_encode(upstream))
            api_ctx.matched_upstream = upstream.dns_value or upstream.value
        end
    else
        api_ctx.matched_upstream = (route.dns_value and
                                route.dns_value.upstream)
                               or route.value.upstream  
    end
</code></pre>
<h2 id="toc_6">三、自定义auth插件需要微调</h2>
<p>新版的apisix auth授权插件支持多个授权插件串行执行，这个功能也很赞，但此举导致了先前为具体业务定制的授权插件无法正常工作，这时需要微调一下。</p>
<p>原先调用方式：</p>
<pre class="line-numbers"><code class="language-lua">    local consumers = core.lrucache.plugin(plugin_name, "consumers_key",
            consumer_conf.conf_version,
            create_consume_cache, consumer_conf)
</code></pre>
<p>因为新版的<code>lrucache</code>不再提供 <code>plugin</code> 函数，需要微调一下：</p>
<pre class="line-numbers"><code class="language-lua">local lrucache = core.lrucache.new({
  type = "plugin",
})
......
    local consumers = lrucache("consumers_key", consumer_conf.conf_version,
        create_consume_cache, consumer_conf)
</code></pre>
<p>另一处是，顺利授权之后，需要赋值<code>consumer</code>相关信息：</p>
<pre class="line-numbers"><code class="language-text">    ctx.consumer = consumer
    ctx.consumer_id = consumer.consumer_id
</code></pre>
<p>此时需要替换成如下方式，为（可能存在的）后续的授权插件继续作用。</p>
<pre class="line-numbers"><code class="language-text">consumer_mod.attach_consumer(ctx, consumer, consumer_conf)
</code></pre>
<p>更多请参考：<code>apisix/plugins/key-auth.lua</code> 源码。</p>
<h2 id="toc_7">四、ETCD V2数据迁移到V3</h2>
<p>迁移分为三步：</p>
<ol>
<li>升级线上已有ETCD  3.3.*版本到3.4.*，满足新版Apisix的要求，这时ETCD实例同时支持了V2和V3格式数据</li>
<li>迁移V2数据到V3
<ul>
<li>因为数据量不是非常多，我采取了一个非常简单和原始的方式</li>
<li>使用 etcdctl 完成V2数据到导出</li>
<li>然后使用文本编辑器vim等完成数据的替换，生成etcdctl v3格式的数据导入命令脚本</li>
<li>运行之后V3数据导入脚本，完成V2到V3的数据导入</li>
</ul></li>
<li>修改V3 <code>/apisix/upstreams</code> 中包含服务注册的数据，一一添加 <code>"discovery_type" : "consul_kv"</code>属性</li>
</ol>
<p>基于以上操作之后，从而完成了ETCD V2到V3的数据迁移。</p>
<h2 id="toc_8">五、启动apisix后发现ETCD V3已有数据无法加载</h2>
<p>我们在运维层面，使用 <code>/usr/local/openresty/bin/openresty -p /usr/local/apisix -g daemon off;</code> 方式运行网关程序。</p>
<p>这也就导致，自动忽略了官方提倡的：<code>apisix start</code> 命令自动提前为ETCD V3初始化的一些键值对内容。</p>
<p>因此，需要提前为ETCD V3建立以下键值对内容：</p>
<pre class="line-numbers"><code class="language-text">Key                         Value
/apisix/routes          :   init_dir
/apisix/upstreams       :   init_dir
/apisix/services        :   init_dir
/apisix/plugins         :   init_dir
/apisix/consumers       :   init_dir
/apisix/node_status     :   init_dir
/apisix/ssl             :   init_dir
/apisix/global_rules    :   init_dir
/apisix/stream_routes   :   init_dir
/apisix/proto           :   init_dir
/apisix/plugin_metadata :   init_dir
</code></pre>
<p>不提前建立的话，就会导致apisix重启后，无法正常加载ETCD中已有数据。</p>
<p>其实有一个补救措施，需要修改 <code>apisix/init.lua</code> 内容，找到如下代码：</p>
<pre class="line-numbers"><code class="language-lua">            if not dir_res.nodes then
                dir_res.nodes = {}
            end
</code></pre>
<p>比较geek的行为，使用下面代码替换一下即可完成兼容：</p>
<pre class="line-numbers"><code class="language-lua">                if dir_res.key then
                    dir_res.nodes = { clone_tab(dir_res) }
                else
                    dir_res.nodes = {}
                end
</code></pre>
<h2 id="toc_9">六、apisix-dashboard的支持</h2>
<p>我们基于apisix-dashboard定制开发了大量的针对公司实际业务非常实用的企业级特性，但也导致了无法直接升级到最新版的apisix-dashboard。</p>
<p>因为非常基础的上游和路由没有发生多大改变，因此这部分升级的需求可以忽略。</p>
<p>实际上，只是在提交上游表单时，包含服务注册信息JSON字符串中需要增加 <code>discovery_type</code> 字段和对应值即可完成支持。</p>
<h2 id="toc_10">七、小结</h2>
<p>花费了一些时间完成了从Apisix 1.5升级到Apisix 2.2的行为，虽然有些坑，但整体来讲，还算顺利。目前已经上线并全量部署运行，目前运行良好。</p>
<p>针对还停留在Apisix 1.5的用户，新版增加了Control API以及多种服务发现等新特性支持，还是非常值得升级的。</p>
<p>升级之前，不妨仔细阅读每一个版本的升级日志（地址：<a href="https://github.com/apache/apisix/blob/2.2/CHANGELOG.md">https://github.com/apache/apisix/blob/2.2/CHANGELOG.md</a> ），然后需要根据具体业务做好兼容测试准备和准备升级步骤，这些都是非常有必要的。</p>
<p>针对我们团队来讲，升级到最新版，一方面降低了版本升级的压力，另一方面也能够辅助我们能参与到开源社区中去，挺好~</p><img src ="http://www.blogjava.net/yongboy/aggbug/435806.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/yongboy/" target="_blank">nieyong</a> 2021-02-23 14:57 <a href="http://www.blogjava.net/yongboy/archive/2021/02/23/435806.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>HTTP API设计笔记</title><link>http://www.blogjava.net/yongboy/archive/2018/01/02/433000.html</link><dc:creator>nieyong</dc:creator><author>nieyong</author><pubDate>Tue, 02 Jan 2018 12:53:00 GMT</pubDate><guid>http://www.blogjava.net/yongboy/archive/2018/01/02/433000.html</guid><wfw:comment>http://www.blogjava.net/yongboy/comments/433000.html</wfw:comment><comments>http://www.blogjava.net/yongboy/archive/2018/01/02/433000.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/yongboy/comments/commentRss/433000.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/yongboy/services/trackbacks/433000.html</trackback:ping><description><![CDATA[<h2 id="toc_0">前言</h2>

<p>最近一段时间，要为一个手机终端APP程序从零开始设计一整套HTTP API，因为面向的用户很固定，一个新的移动端APP。目前还是项目初期，自然要求一切快速、从简，实用性为主。</p>

<p>下面将逐一论述我们是如何设计HTTP API，虽然相对大部分人而言，没有什么新意，但对我来说很新鲜的。避免忘却，趁着空闲尽快记录下来。</p>

<h2 id="toc_1">技术堆栈的选择</h2>

<p>PHP嘛？团队内也没几个人熟悉。</p>

<p>Java？好几年没有碰过了，那么复杂的解决方案，再加上团队内也没什么人会 &hellip;&hellip;</p>

<p>团队使用过Lua，基于OpenResty构建过TCP、HTTP网关等，对Lua + Nginx组合非常熟悉，能够快速的应用在线上环境。再说Lua语法小巧、简单，一个新手半天就可以基本熟悉，马上开工。</p>

<p>看来，Nginx + Lua是目前最为适合我们的了。</p>

<p>HTTP API，需要充分利用HTTP具体操作语义，来应对具体的业务操作方法。基于此，没有闭门造车，我们选择了 <a href="http://lor.sumory.com/">http://lor.sumory.com/</a> 这么一个小巧的框架，用于辅助HTTP API的开发开发。</p>

<p>嗯，OpenResty + Lua + Lor，就构成了我们简单技术堆栈。</p>

<h2 id="toc_2">HTTP API简要设计</h2>

<h3 id="toc_3">HTTP API路径和语义</h3>

<p>每一具体业务逻辑，直接在URL Path中体现出来。我们要的是简单快速，数据结构之间的连接关系，尽可能的去淡化。eg：</p>

<pre><code>/resource/video/ID
</code></pre>

<p>比如用户反馈这一模块，将使用下面比较固定的路径：</p>

<pre><code>/user/feedback
</code></pre>

<ul>
<li><code>GET</code>，以用户维度查询反馈的历史列表，可分页

<ul>
<li><code>curl -X GET http://localhost/user/feedback?page=1</code></li>
</ul></li>
<li><code>POST</code>，提交一个反馈

<ul>
<li><code>curl -X POST http://localhost/user/feedback -d &quot;content=hello&quot;</code></li>
</ul></li>
<li><code>DELETE</code>，删除一个或多个反馈，参数附加在URL路径中。

<ul>
<li><code>curl -X DELETE http://localhost/user/feedback?id=1001</code></li>
</ul></li>
<li><code>PUT</code>，更新评论内容

<ul>
<li><code>curl -X PUT http://localhost/user/feedback/1234 -d &quot;content=hello2&quot;</code></li>
</ul></li>
</ul>

<p>用户属性很多，用户昵称只是其中一个部分，因此更新昵称这一行为，HTTP的 <code>PATCH</code> 方法可更精准的描述部分数据更新的业务需求：</p>

<pre><code>/user/nickname
</code></pre>

<ul>
<li><code>PATCH</code>，更新用户昵称，昵称是用户属性之一，可以使用更轻量级的 <code>PATCH</code> 语义

<ul>
<li><code>curl -X PATCH http://localhost/user/nickname -d &quot;nickname=hello2&quot;</code></li>
</ul></li>
</ul>

<p>嗯，同一类的资源URL虽然固定了，但HTTP Method呈现了不同的业务逻辑需求。</p>

<h3 id="toc_4">HTTP API的访问授权</h3>

<p>实际业务HTTP API的访问是需要授权的。</p>

<p>传统的Access Token解决方案，有session回话机制，一般需要结合Web浏览器，需要写入到Cookie中，或生产一个JSessionID用于标识等。这针对单纯面向移动终端的HTTP API后端来讲，并没有义务去做这一的兼容，略显冗余。</p>

<p>另外就是 <code>OAUTH</code> 认证了，有整套的认证方案并已工业化，很是成熟了，但对我们而言还是太重，不太适合轻量级的HTTP API，不太可能花费太多的精力去做它的运维工作。</p>

<p>最终选择了轻量级的 <a href="https://jwt.io/">Json Web Token</a>，非常紧凑，开箱即用。</p>

<p><img src="https://i.ytimg.com/vi/BzZi_kfnaWc/maxresdefault.jpg" alt=""/></p>

<p>最佳做法是把JWT Token放在HTTP请求头部中，不至于和其它参数混淆：</p>

<pre><code class="language-bash">curl -H &quot;Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI2NyIsInV0eXBlIjoxfQ.LjkZYriurTqIpHSMvojNZZ60J0SZHpqN3TNQeEMSPO8&quot; -X GET http://localhost/user/info
</code></pre>

<p>下面是一副浏览器段的一般认证流程，这与HTTP API认证大体一致：</p>

<p><img src="https://upload-images.jianshu.io/upload_images/1821058-2e28fe6c997a60c9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700" alt=""/></p>

<p>JWT的Lua实现，推荐: <code>https://github.com/SkyLothar/lua-resty-jwt.git</code>，简单够用。</p>

<h3 id="toc_5">JWT和Lor的结合</h3>

<p>jwt需要和业务进行绑定，结合 lor 这个API开发框架提供的中间件机制，可在业务处理之前，在合适位置进行权限拦截。</p>

<ul>
<li>用户需要请求进行授权接口，比如登陆等</li>
<li>服务器端会把用户标识符，比如用户id等，存入JWT的payload负荷中，然后生成Token字符串，发给客户端</li>
<li>客户端收到JWT生成的Token字符串，在后续的请求中需要附加在HTTP请求的Header中</li>
<li>完成认证过程</li>
</ul>

<p>不同于OAUTH，JWT协议的<strong>自包含</strong>特性，决定了后端可以将很多属性信息存放在payload负荷中，其token生成之后后端可以不用存储；下次客户端发送请求时会发送给服务器端，后端获取之后，直接验证即可，验证通过，可以直接读取原先保存其中的所有属性。</p>

<p>下面梳理一下Jwt认证和Lor的结合。</p>

<ul>
<li>全局拦截，针对所有PATH，所有HTTP Method，这里处理JWT认证，若认证成功，会直接把用户id注入到当前业务处理上下文中，后面的业务可以直接读取当前用户的id值</li>
</ul>

<pre><code class="language-lua">app:use(function(req, res, next)
    local token = ngx.req.get_headers()[&quot;Authorization&quot;]
    -- 校验失败，err为错误代码，比如 400
    local payload, err = verify_jwt(token)
    if err then
        res:status(err):send(&quot;bad access token reqeust&quot;)
        return
    end

    -- 注入进当前上下文中，避免每次从token中获取
    req.params.uid = payload.uid

    next()
end)
</code></pre>

<ul>
<li>针对具体路径进行设定权限拦截，较粗粒度；比如 /user 只允许已登陆授权用户访问</li>
</ul>

<pre><code class="language-lua">app:use(&quot;/user&quot;, function(req, res, next)
    if not req.params.uid then
        -- 注意，这里没有调用next()方法，请求到这里就截止了，不在匹配后面的路由
        res:status(403):send(&quot;not allowed reqeust&quot;)
    else
        next() -- 满足以上条件，那么继续匹配下一个路由
    end
end)
</code></pre>

<ul>
<li>一种是较细粒度，具体到每一个API接口，因为虽然URL一致，但不同的HTTP Method有时请求权限还是有区别的</li>
</ul>

<pre><code class="language-lua">local function check_token(req, res, next)
    if not req.params.uid then
        res:status(403):send(&quot;not allowed reqeust&quot;)
    else
        next()
    end
end

local function check_master(req, res, next)
    if not req.params.uid ~= master_uid then
        res:status(403):send(&quot;not allowed reqeust&quot;)
    else
        next()
    end
end

local lor = require(&quot;lor.index&quot;)
local app = lor()

-- 声明一个group router
local user_router = lor:Router()

-- 假设查看是不需要用户权限的
user_router:get(&quot;/feedback&quot;, function(req, res, next)
end)

user_router:put(&quot;/feedback&quot;, check_token, function(req, res, next)
end)

user_router:post(&quot;/feedback&quot;, check_token, function(req, res, next)
end)

-- 只有管理员才有权限删除
user_router:delete(&quot;/feedback&quot;, check_master, function(req, res, next)
end)

-- 以middleware的形式将该group router加载进来
app:use(&quot;/user&quot;, user_router())

......

app:run()
</code></pre>

<h2 id="toc_6">为什么没有选择GraphQL API ？</h2>

<p>我们在上一个项目中对外提供了GraphQL API，其（在测试环境下）自身提供文档输出自托管机制，再结合方便的调试客户端，确实让后端开发和前端APP开发大大降低了频繁交流的频率，节省了若干流量，但前期还是需要较多的培训投入。</p>

<p>但在新项目中，一度想提供GraphQL API，遇到的问题如下：</p>

<ul>
<li>全新的项目数据结构属性变动太频繁</li>
<li>普遍求快，业务模型快速开发、调试</li>
<li>大家普遍对GraphQL API有些抵触，使用JSON输出格式的HTTP API是约定俗成的习惯选择</li>
</ul>

<p>毫无疑问，以最低成本快速构建较为完整的APP功能，HTTP API + JSON格式是最为舒服的选择。</p>

<p>虽然有些担心服务器端的输出，很多时候还是会浪费掉一些流量，客户端并不能够有效的利用返回数据的所有字段属性。但和进度以及人们已经习惯的HTTP API调用方式相比，又微乎其微了。</p>

<h2 id="toc_7">小结</h2>

<p>当前这一套HTTP API技术堆栈运行的还不错，希望能给有同样需要的同学提供一点点的参考价值  :))</p>

<p>当然没有一成不变的架构模型，随着业务的逐渐发展，后面相信会有很多的变动。但这是以后的事情了，谁知道呢，后面有空再次记录吧~</p>
<img src ="http://www.blogjava.net/yongboy/aggbug/433000.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/yongboy/" target="_blank">nieyong</a> 2018-01-02 20:53 <a href="http://www.blogjava.net/yongboy/archive/2018/01/02/433000.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>HTTP/2笔记之错误处理和安全</title><link>http://www.blogjava.net/yongboy/archive/2015/03/24/423791.html</link><dc:creator>nieyong</dc:creator><author>nieyong</author><pubDate>Tue, 24 Mar 2015 07:27:00 GMT</pubDate><guid>http://www.blogjava.net/yongboy/archive/2015/03/24/423791.html</guid><wfw:comment>http://www.blogjava.net/yongboy/comments/423791.html</wfw:comment><comments>http://www.blogjava.net/yongboy/archive/2015/03/24/423791.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/yongboy/comments/commentRss/423791.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/yongboy/services/trackbacks/423791.html</trackback:ping><description><![CDATA[<div class="wrap"> <h3 id="-">零。前言</h3> <p>这里整理了一下错误和安全相关部分简单记录。</p> <h3 id="-http-2-">一。HTTP/2错误</h3> <h4 id="1-">1. 错误定义</h4> <p>HTTP/2定义了两种类型错误：</p> <ul> <li>导致整个连接不可使用的错误为连接错误(connection error)  </li><li>单独出现在单个连接上的错误为流错误(stream error) </li></ul> <h4 id="2-">2. 错误代码</h4> <p>错误代码，32位正整数表示错误原因，RST_STREAM和GOAWAY帧中包含。</p> <p><img alt="" src="http://images.blogjava.net/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/http2_errors_thumb.png" /></p> <p>未知或不支持的错误代码可以选择忽略，或作为INTERNAL_ERROR错误对待都可以。</p> <h3 id="3-">3. 连接错误处理</h3> <p>一般来讲连接错误很严重，会导致处理进程无法进行下去，或影响到整个连接的状态。</p> <ul> <li>终端一旦遇上连接错误，需第一时间在最后一个可用流上发送包含错误原因GOAWAY帧过去，然后关闭连接  </li><li>GOAWAY有可能不被对端成功接收到，若成功接收可获得连接被终止的原因  </li><li>终端可在任何时间终止连接，也可以把流错误作为连接错误对待。但都应该在关闭连接之前发送一个GOAWAY帧告知对方 </li></ul> <h4 id="4-">4. 流错误</h4> <p>一般来讲具体流上的流错误不会影响到其它流的处理。</p> <ul> <li>终端检测到流错误，需要发送一个RST_STREAM帧，其包含了操作到错误流标识符  </li><li>RST_STREAM应当是发送错误流最后一个帧，内含错误原因。  </li><li>发送方在发送之后，需要准备接收对端将要或即将发送过来的帧数据，处理方式就是忽略之，除非是可以修改连接状态帧  </li><li>一般来讲，终端不应该发送多个RST_STREAM帧，但若在一个往返时间之后已关闭的流上能够继续接收帧，则需要发送再次发送一个RST_STREAM帧，处理这种行为不端的实现。  </li><li>终端在接收到RST_STREAM帧之后，不能响应一个RST_STREAM帧，避免死循环 </li></ul> <h4 id="5-">5. 连接终止</h4> <p>TCP连接被关闭或重置时仍有处于"open"或"half closed"的流将不能自动重试。</p> <h3 id="-http-2-">二。HTTP/2安全注意事项</h3> <h4 id="1-">1. 跨协议攻击</h4> <p>跨协议攻击，字面上理解就很简单，比如攻击者构建HTTP/1.1请求直接转发给仅仅支持HTTP/2的服务器，以期待获取攻击效果。</p> <p>这里有一篇讲解跨协议攻击的文章：<a href="http://www.freebuf.com/articles/web/19622.html">http://www.freebuf.com/articles/web/19622.html</a></p> <p>TLS的加密机制使得攻击者很难获得明文，另外TLS的ALPN协议扩展可以很轻松处理请求是否需要作为HTTP/2请求进行处理，总之可有效阻止对基于TLS的其它协议攻击。</p> <p>基于标准版TCP没有TLS和ALPN的帮忙，客户端所发送连接序言前缀为PRI字符串用来混淆HTTP/1.1服务器，但对其它协议没有提供保护，仅限于此。但在处理时，若接收到HTTP/1.1的请求，没有包含Upgrade升级字段，则需要认为是一个跨协议攻击。</p> <p>总之，程序要尽可能的健壮，容错，针对非法的请求，直接关闭对方连接。</p> <h4 id="2-">2. 中介端数据转换封装的攻击</h4> <p>中介所做的HTTP/1.1和HTTP/2之间转换，会存在攻击点：</p> <ol> <li>HTTP/2头字段名称编码允许使用HTTP/1.1没有使用到的头字段名称，中介在转换HTTP/2到HTTP/1.1时就容易出现包含非法请求头字段HTTP/1.1数据。  </li><li>HTTP/2允许头字段值可以是非法值，诸如回车(CR, ASCII 0xd), 换行 (LF, ASCII 0xa), 零字符 (NUL, ASCII 0x0)，这在逐字解析实现时是一个风险。 </li></ol> <p>解决方式，一旦发现非法头字段名称，以及非法头字段值，都作为不完整、残缺数据对待，或丢弃，或忽略。</p> <h4 id="3-">3. 推送内容的缓存</h4> <p>推送内容有保证的服务器提供，是否缓存由头字段Cache-Control控制。</p> <p>但若服务器上多租户形式（SAAS），每一个租户使用一小部分URL空间，比如 tenant1.domain.com，tenant2.domain.com，服务器需要确保没有授权的租户不能够推送超于预期的资源，覆盖已有内容。</p> <p>原始服务器没有被授权使用推送，既不能够违规发送推送，也不能够被缓存。</p> <h4 id="4-">4. 拒绝服务攻击注意事项</h4> <ul> <li>HTTP/2因为要为流、报头压缩、流量控制等特性占用资源较多，因此针对每一个连接的内存分配要设置限额，否则很少的连接占满内存，无法正常服务  </li><li>针对单个连接，规范对PUSH_PROMISE帧数量没有约束，但客户端需要设置一个上限值，这也是确定需要维护的"reserved (remote)"状态的数量，超出限额需要报ENHANCE_YOUR_CALM类型流错误  </li><li>SETTINGS帧有可能会被滥用导致对端需要花费时间解析处理设置限制等，滥用情况包括包含未定义的参数，以及同一个参数多次出现等，类似于WINDOW_UPDATE和PRIORITY帧都会存在滥用的情况；这些帧被滥用导致资源耗费情况严重  </li><li>大量小帧或空帧一样会被滥用，但又符合逻辑，耗费服务器资源在处理报文头部上面。比如空负载DATA帧，以及用于携带报文头部数据的CONTINUATION帧，都属于安全隐患  </li><li>报头压缩存在潜在风险，也会被滥用，详情可参考HPACK协议第七章：<a href="http://http2.github.io/http2-spec/compression.html#Security">http://http2.github.io/http2-spec/compression.html#Security</a>  </li><li>终端中途发送的SETTINGS帧所定义参数不是立即可以生效的，这会导致对端在实际操作时可能会超过最新的限制。建议直接在连接建立时在连接序言内包含设置值，就算如此，客户端也会存在超出服务器端连接序言中所设置的最新限定值。 </li></ul> <p>总之，诸如SETTINGS帧、小帧或空帧，报头压缩被合理滥用时，表明上看符合逻辑，会造成资源过度消耗。这需要服务器端监控跟踪到此种行为，并且设置使用数量的上限，一旦发现直接报ENHANCE_YOUR_CALM类型连接错误。</p> <h4 id="5-">5. 报头块大小限制</h4> <p>报头块过大导致实现需要维护大量的状态开销。另外，根据报头字段进行路由的情况，若此报头字段出现在一系列报头块帧的最后一个帧里面，可能会导致无法正常路由到目的地。若被缓存会导致耗费大量的内存。这需要设置SETTINGS_MAX_HEADER_LIST_SIZE参数限制报头最大值，以尽可能的避免出现以上情况。</p> <p>服务器一旦接收到超过报头限制请求，需要响应一个431（请求头过大） HTTP状态码，客户端呢可直接丢掉响应。</p> <h4 id="6-">6. 压缩使用的安全隐患</h4> <ul> <li>针对安全通道，不能使用同一个压缩字典压缩保密的关键数据和易受攻击者控制的数据  </li><li>来源数据不能确定为完全可靠，就不应该使用压缩机制  </li><li>通用流的压缩不能在基于TLS的HTTP/2上使用这一部分，可参考 <a href="http://http2.github.io/http2-spec/compression.html#Security">http://http2.github.io/http2-spec/compression.html#Security</a> </li></ul> <h4 id="7-">7. 填充使用的安全隐患</h4> <p>一般来讲，填充可用来混淆帧的真实负载长度，稍加保护，降低攻击的可能性。但若不当的填充策略：固定填充数、可轻松推导出填充规则等情况都会降低保护的力度，都有可能会被攻击者破解。</p> <p>中介设备应该保留DATA帧的填充（需要避免如上所述一些情况），但可丢弃HEADERS和PUSH_PROMISE帧的填充。</p> <h3 id="-tls">三。TLS</h3> <p>HTTP/2加密建立在TLS基础，关于TLS，维基百科上有解释：<a href="http://zh.wikipedia.org/wiki/%E5%82%B3%E8%BC%B8%E5%B1%A4%E5%AE%89%E5%85%A8%E5%8D%94%E8%AD%B0">http://zh.wikipedia.org/wiki/%E5%82%B3%E8%BC%B8%E5%B1%A4%E5%AE%89%E5%85%A8%E5%8D%94%E8%AD%B0</a></p> <p>摘取一张图，可说明基于ALPN协议扩展定义的协商流程：</p> <p><img alt="" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/image_thumb_14.png" /></p> <p>其它要求：</p> <ul> <li>只能基于TLS &gt;= 1.2版本。目前TLS 1.3为草案版本，正式版本目前尚未可知。目前只有TLS 1.2可选。  </li><li>必须支持Server Name Indication (SNI) [TLS-EXT]扩展，客户端在连接协商阶段需要携带上域名  </li><li>基于TLS 1.3或更高版本构建，仅需要支持SNI扩展。TLS 1.2要求较多  </li><li>基于TLS 1.2构建 <ul> <li>必须禁用压缩机制。不恰当压缩机制会导致信息外露，HTTP/2报头有压缩机制  </li><li>必须禁用重新协商机制。终端对待TLS 1.2重新协商作为PROTOCOL_ERROR类型连接错误对待；密码套件加密次数限制导致连接一直挂起等待不可用  </li><li>终端可以通过重新协商提供对客户端凭证保护功能在握手期间，重新协商必须发生在发送连接序言之前进行。服务器当看到重新协商请求时应该请求客户端证书在连接建立后  </li><li>当客户端请求受保护的特定资源时，服务器可以响应HTTP_1_1_REQUIRED错误，可有效阻止重新协商机制 </li></ul></li></ul> <h3 id="-">四。小结</h3> <p>这里简单记录HTTP/2错误和安全相关事项，本系列规范学习到此告一段落。</p></div><img src ="http://www.blogjava.net/yongboy/aggbug/423791.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/yongboy/" target="_blank">nieyong</a> 2015-03-24 15:27 <a href="http://www.blogjava.net/yongboy/archive/2015/03/24/423791.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>HTTP/2笔记之消息交换</title><link>http://www.blogjava.net/yongboy/archive/2015/03/23/423751.html</link><dc:creator>nieyong</dc:creator><author>nieyong</author><pubDate>Mon, 23 Mar 2015 08:45:00 GMT</pubDate><guid>http://www.blogjava.net/yongboy/archive/2015/03/23/423751.html</guid><wfw:comment>http://www.blogjava.net/yongboy/comments/423751.html</wfw:comment><comments>http://www.blogjava.net/yongboy/archive/2015/03/23/423751.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/yongboy/comments/commentRss/423751.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/yongboy/services/trackbacks/423751.html</trackback:ping><description><![CDATA[<div class="wrap"><h3 id="-">前言</h3> <p>无论是HTTP/1.*还是HTTP/2，HTTP的基本语义是不变的，比如方法语义（GET/PUST/PUT/DELETE），状态码（200/404/500等），Range Request，Cacheing，Authentication、URL路径。以前纯文本形式作为传输的载体，HTTP/2带来了与之不同的二进制传输语法语义。</p> <p>下面为HTTP/2消息交换方便笔记。</p> <h3 id="-">请求/响应流程</h3> <p>一个典型的HTTP消息包含请求/响应，组成如下：</p> <ul> <li>以响应为例，零或多个HEADERS帧（每一个HEADERS帧可能跟着&gt;=0个CONTINUATION帧，以补充单个HEADERS容量不够的情况）包含状态码为1xx的报文头响应</li> <li>或者一个HEADERS帧包含了完整的报文头（一般情况下）</li> <li>零个或多个DATA数据帧包含了具体的消息负载内容</li> <li>一个HEADERS帧，后面跟随零个或多个包含有报尾（trailer-part）的CONTINUATION帧，作为可选项</li> </ul> <p>注意事项：</p> <ul> <li>一个HEADERS帧携带有END_STREAM标志，后面可跟随有CONTINUATION帧用以补充剩余的包头块</li> <li>来自于其它流的类型帧是不能够出现在HEADERS帧和CONTINUATION帧中间</li> <li>DATA数据帧不支持分块传输编码（chunked transfer encoding）</li> <li>报尾字段（Trailing header field）当出现在报头块中时，可以终止当前流</li> <li>HEADERS帧以及关联的CONTINUATION帧只能够出现在一个流的开始或结束时</li> <li>HTTP请求-相应交换消耗一个流：请求准备HEADERS帧打开流，请求的帧包含有END_STREAM标志导致两端流处于半关闭状态,half closed(local/server)；响应一个HEADERS帧，若某响应帧包含有END_STREAM标志，流将被关闭</li> <li>一个HTTP响应完成指的响应帧是包含有END_STREAM标志，在服务器发送并且客户端接收成功。若响应不依赖于客户端的请求，服务器端可以在先于客户端发送请求之前发送完成，之后服务器通过再次发送一个RST_STREAM流（错误代码为NO_ERROR）请求客户端放弃发送请求。这要求客户端在接收到RST_STREAM帧后必须不能够丢弃响应，无论是处于什么谨慎原因。</li> </ul> <h4 id="1-">1. 不支持升级机</h4> <p>HTTP/2多路复用，以及自身也可以通过HTTP/1.1 101切换古来，因此不支持101切换协议（Switching Protocols）机制也是情理之中。</p> <h4 id="2-http-header-fields">2. HTTP Header Fields</h4> <p>HTTP/2报头字段注意点：</p> <ol> <li>和HTTP/1.x报头字段一样，都是ASCII字符表示</li> <li>字段要求全部小写，"Accept" -&gt; "accept"</li> <li>若大写，会被作为不完整数据对待，有被丢弃的风险</li> <li>新增伪报头字段，但不属于常规HTTP头部字段，不允许终端自己产生，只允许规范中所定义的5个<ul> <li>:method</li> <li>:scheme</li> <li>:authority</li> <li>:path</li> <li>:status</li> </ul> </li> <li>伪报头字段必须出现在常规HTTP报头字段之前</li> <li>连接属性专用字段（Connection-Specific Header Fields）不再被使用（但Transfer-Encoding可以允许出现在请求头中），比如Keep-Alive, Proxy-Connection, Transfer-Encoding和Upgrade等</li> </ol> <h4 id="3-">3. 简单示范</h4> <p>简单图片请求：</p> <p><img src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/image_thumb_15.png" alt="" /></p> <p>模拟一次提交，设置报头大于16KB（一般情况下，报头没有那么大，除非Cookie撑大）：</p> <p><img src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/image_thumb_18.png" alt="" /></p> <p>以上示例，可以帮助理解HTTP/1.x和HTTP/2在HTTP语义表述上的不同。</p> <h4 id="4-">4. 可靠性机制</h4> <p>HTTP/1.1，HTTP客户端无法重试非幂等请求，尤其在错误发生的时候，由于无法检测错误性质这会对重试带来不利的影响。</p> <p>而HTTP/2在这方面有所增强，提供了两种方式可判断请求是否被完成：</p> <ul> <li>GOAWAY帧会携带上流标识符的最大值，低于此值的请求已经被执行过，高于此值的请求帧，则可以再次放心重试</li> <li>包含有REFUSED_STREAM错误代码的RST_STREAM帧说明当前流早于任何处理发生之前就已经被关闭，因此发生在当前流上的请求可以安全重试。</li> </ul> <p>另外PING帧有利于客户端检测当前连接是否可用，可以理解为心跳保活机制，因为一些网关、负载设备会关闭空闲状态下的连接以节省资源。</p> <h3 id="-">服务器推送机制</h3> <p>HTTP/2新增特性，服务器根据客户端一次请求内容主动推送与之相关的请求过去，避免客户端在解析出初次请求页面内容时，再逐一发送资源请求，节省网络资源利用效率。 一些注意事项：</p> <ul> <li>客户端可以通过设置SETTINGS_ENABLE_PUSH为0值通知服务器端禁用推送</li> <li>承诺请求应该是可缓存、安全，并且不能够携带请求的负载内容，这需要客户端做检测</li> <li>推送的响应若不可缓存，客户端不能作为HTTP cache存储，这对单独的非浏览器环境特别适合</li> <li>服务器必须包含一个<code>:authority</code>伪头部字段，标明自身被授权。客户端若检测不到需要作为PROTOCOL_ERROR类型流错误对待</li> <li>中介设备接收到服务器的推送后，可以决定是否要转发给客户端，中介可以单独选择推送内容发送给客户端。这是一个特别需要注意的点</li> <li>客户端必须拒绝来自服务器端的对SETTINGS_ENABLE_PUSH属性非0值的修改，也就是说服务器不能要求客户端打开PUSH开关，客户端一旦遇到需要响应PROTOCOL_ERROR类型连接错误</li> <li>客户端不能够发送推送，PUSH_PROMISE帧只能够来自于服务器端（作为推送请求者发送），否则将会作为PROTOCOL_ERROR类型的连接错误对待</li> <li>PUSH_PROMISE需要包含伪头部:method，若客户端认为不安全，必须响应一个PROTOCOL_ERROR类型流错误</li> <li>服务器端应该尽可能早的发送PUSH_PROMISE帧，以避免与来自客户端对相同资源的请求两者产生冲突</li> <li>发送PUSH_PROMISE帧会创建一个新的流，然后处于两端的保留状态，reserved (local/remote)</li> <li>发送完PUSH_PROMISE帧，服务器需要马上发送具体DATA数据帧</li> <li>客户端接收完PUSH_PROMISE帧后，选择接收PUSH响应内容，这期间不能触发请求承诺的响应内容，直到承诺流关闭</li> <li>客户端不需要接收推送内容时，可以选择发送RST_STREAM帧，包含CANCEL/REFUSED_STREAM代码，以及PUSH流标识符发送给服务器端，重置推送流</li> <li>客户端可以通过设置SETTINGS_MAX_CONCURRENT_STREAMS限制响应数，值为0禁用。但不能阻止服务器发送PUSH_PROMISE帧</li> </ul> <p>比如，服务器接收到来自客户端的请求某个HTML文档资源，该文档包含了若干图片连接，服务器应该优先发送图片数据到客户端，这需要优先发送推送承诺早于包含完整HTML文档内容的DATA帧，这样客户端优先接收到承诺资源，后面接收到DATA数据帧进行解析出图片连接的时候，就避免再次发送图片资源请求嘛。</p> <h3 id="connect-">CONNECT方法</h3> <p>在HTTP原始语义中是没有CONNECT方法的，这个伪方法（pseudo-method）在HTTP/1.x，HTTP代理用作转换HTTP连接通过隧洞方式到远程主机，HTTPS方式交互。 HTTP/2与之类似，伪方法CONNECT被HTTP代理用作在一个单独的HTTP/2流之上建立一个到远程主机的隧道，要求如下：</p> <ul> <li>:method=CONNECT</li> <li>":scheme"和":path"被省略</li> <li>":authority"字段为代理要连接的远程主机和端口信息</li> </ul> <p>一旦不满足要求，会被视为不完整的需求。</p> <ul> <li>连接成功建立，代理发送给客户端一个2xx的状态码</li> <li>代理两端在HEADERS帧都发送完毕后，后续的DATA帧开始发送</li> <li>代理转发客户端发送的DATA数据帧到远程服务器</li> <li>代理接收到服务器数据组装成DATA数据帧</li> <li>非DATA类型数据帧，包括流管理类型的RST_STREAM、WINDOW_UPDATE、PRIORITY帧都是不能够在已经连接的流上发送的，否则会被当做流错误对待</li> <li>客户端接收到包含有END_STREAM标志位的DATA帧时，尽量也要发送一个包含有END_STREAM标志位的DATA帧</li> <li>DATA帧END_STREAM标志位被当做TCP FIN比特标志对待：<ul> <li>代理接收到DATA帧带有END_STREAM标志位，在转发时会设置TCP FIN比特位</li> <li>代理接收到TCP段包含有FIN比特位设置时，会转发一个DATA帧并携带END_STREAM标志位</li> <li>最后的TCP段或DATA帧可以为空</li> </ul> </li> <li>TCP连接错误以RST_STREAM帧关联</li> <li>代理对待在TCP连接中出现的错误，包括接收到一个包含有RST比特位的TCP段，作为CONNECT_ERROR类型的流错误抛出</li> <li>一旦检测到流或HTTP/2连接的错误，代理必须发送一个TCP段并且其RST标志被设置</li> <li>代理不能仅仅依靠SETTINGS_MAX_CONCURRENT_STREAMS属性值进行限制资源消耗</li> </ul> <h3 id="-">持久连接和重用</h3> <p>HTTP/2消息交换通过持久连接、重用实现，目的尽可能做到资源利用率最大化。</p> <ol> <li>HTTP/2为持久性连接，基于性能原因，规范建议客户端不要关闭已有连接除非不再需要和服务器保持通信。服务器端要是主动关闭连接的话，在请求量大的情况下，会导致系统出现大量的TIME_WAIT状态TCP，每一个TIME_WAIT状态默认情况下至少持续60秒，特别占用系统资源。因此最佳实践是客户端主动关闭连接，避免Linux服务器端出现TIME_WAIT。</li> <li>基于具体主机和端口，客户端应该只打开一个HTTP/2连接</li> <li>客户端可以额外创建连接作为替代补充：替换已耗尽可用流标识符，或刷新TLS连接，或替换遇到错误的连接</li> <li>当任一端想关闭连接的时候，都应该第一时间发送一个GOAWAY帧到对端，告知对方先前发送的帧已经被处理过，终止之后的一些剩余任务，终止可放心关闭</li> <li>有一些情况服务器若不希望客户端重用连接，可返回421 (Misdirected Request) 状态码作为响应，默认可缓存（POST方法或cache-control可控制），但代理不能够为客户端请求生成421状态码。</li> <li>HTTP代理与每一个服务器之间可以尽可能保持一个持久的连接方便专递客户端的请求；客户端到代理之间可以所有请求共享、重用一个连接</li> </ol> <h3 id="-">小结</h3> <p>以上为HTTP/2消息交换机制的一些简单梳理，需要注意点：</p> <ol> <li>HTTP/2不允许使用连接特定头部字段</li> <li>新增的5个头部</li> <li>推送机制的一些特性需求</li> <li>RST_STREAM等帧标志位的使用</li> </ol> </div><img src ="http://www.blogjava.net/yongboy/aggbug/423751.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/yongboy/" target="_blank">nieyong</a> 2015-03-23 16:45 <a href="http://www.blogjava.net/yongboy/archive/2015/03/23/423751.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>HTTP/2笔记之帧</title><link>http://www.blogjava.net/yongboy/archive/2015/03/20/423655.html</link><dc:creator>nieyong</dc:creator><author>nieyong</author><pubDate>Fri, 20 Mar 2015 01:24:00 GMT</pubDate><guid>http://www.blogjava.net/yongboy/archive/2015/03/20/423655.html</guid><wfw:comment>http://www.blogjava.net/yongboy/comments/423655.html</wfw:comment><comments>http://www.blogjava.net/yongboy/archive/2015/03/20/423655.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/yongboy/comments/commentRss/423655.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/yongboy/services/trackbacks/423655.html</trackback:ping><description><![CDATA[<div id="out"> <h3>零。前言</h3> <p>客户端和服务器端一旦握手协商成功接建立连接，端点之间可以基于HTTP/2协议传递交换帧数据了。</p> <h3>一。帧通用格式</h3> <p>下图为HTTP/2帧通用格式：帧头+负载的比特位通用结构：</p><pre>+-----------------------------------------------+
|                Length (24)                    |
+---------------+---------------+---------------+
|  Type (8)     |  Flags (8)    |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier (31)                       |
+=+=============================================================+
|                  Frame Payload (0...)                       ...
+---------------------------------------------------------------+</pre>
<p>帧头为固定的9个字节（(24+8+8+1+31)/8=9）呈现，变化的为帧的负载(payload)，负载内容是由帧类型（Type）定义。</p>
<ul>
<li>帧长度Length：无符号的自然数，24个比特表示，仅表示帧负载所占用字节数，不包括帧头所占用的9个字节。默认大小区间为为0~16,384(2^14)，一旦超过默认最大值2^14(16384)，发送方将不再允许发送，除非接收到接收方定义的SETTINGS_MAX_FRAME_SIZE（一般此值区间为2^14 ~ 2^24）值的通知。 
</li><li>帧类型Type：8个比特表示，定义了帧负载的具体格式和帧的语义，HTTP/2规范定义了10个帧类型，这里不包括实验类型帧和扩展类型帧 
</li><li>帧的标志位Flags：8个比特表示，服务于具体帧类型，默认值为0x0。有一个小技巧需要注意，一般来讲，8个比特可以容纳8个不同的标志，比如，PADDED值为0x8，二进制表示为00001000；END_HEADERS值为0x4，二进制表示为00000100；END_STREAM值为0X1，二进制为00000001。可以同时在一个字节中传达三种标志位，二进制表示为00001101，即0x13。因此，后面的帧结构中，标志位一般会使用8个比特表示，若某位不确定，使用问号?替代，表示此处可能会被设置标志位 
</li><li>帧保留比特为R：在HTTP/2语境下为保留的比特位，固定值为0X0 
</li><li>流标识符Stream Identifier：无符号的31比特表示无符号自然数。0x0值表示为帧仅作用于连接，不隶属于单独的流。 </li></ul>
<p>关于帧长度，需要稍加关注： - 0 ~ 2^14（16384）为默认约定长度，所有端点都需要遵守 - 2^14 (16,384) ~ 2^24-1(16,777,215)此区间数值，需要接收方设置SETTINGS_MAX_FRAME_SIZE参数单独赋值 - 一端接收到的帧长度超过设定上限或帧太小，需要发送FRAME_SIZE_ERR错误 - 当帧长错误会影响到整个连接状态时，须以连接错误对待之；比如HEADERS，PUSH_PROMISE，CONTINUATION，SETTINGS，以及帧标识符不该为0的帧等，都需要如此处理 - 任一端都没有义务必须使用完一个帧的所有可用空间 - 大帧可能会导致延迟，针对时间敏感的帧，比如RST_STREAM, WINDOW_UPDATE, PRIORITY，需要快速发送出去，以免延迟导致性能等问题</p>
<h3>二。报文头压缩和解压</h3>
<p>和HTTP/1一样，HTTP/2报头字段包含一个或多个相关的键值对。报头字段会在HTTP请求/响应报头和服务器推送操作中使用。原先为文本字段，现在需要使用HTTP报头压缩进行序列化成报头分块，作为HEADERS 、 PUSH_PROMISE、CONTINUATION等帧的负载传输出去。</p>
<p>解压缩采用的HPACK协议，具体可参考：<a href="http://http2.github.com/http2-spec/compression.html">http://http2.github.com/http2-spec/compression.html</a></p>
<p>接收端合并接收到的帧组装成报头分块，解压缩还原报头集合。</p>
<p>一个完整的报头分块包含： - 单个包含报头终止标记END_HEADERS的HEADERS、PUSH_PROMISE帧，或者 - HEADERS、PUSH_PROMISE帧不包含的END_HEADERS标记，后续跟随一个或多个CONTINUATION帧，最后一个CONTINUATION帧包含了END_HEADERS标记。</p>
<p>报头压缩是有状态的，在一个完整的连接中，一方的压缩上下文环境，另一方的解压的上下文环境，都是需要具备的。报头解码失败需要作为连接错误COMPRESSION_ERROR对待。</p>
<p>报头块彼此之间离散，作为连续的同一类型帧序列存在，不存在交错帧以及来自其他类型帧或流。举一个例子，一个连续的HEADERS/CONTINUATION/PUSH_PROMISE帧序列，最后一个帧包含了END_HEADERS标记，表示一个报头完结。一个报头块逻辑上是一个帧，但是否完整取决于同类型连续的帧的最后一个包含END_HEADERS标记。</p>
<p>报头块作为HEADERS/PUSH_PROMISE/CONTINUATION等帧负载被一端发向另一端。接收端需要从HEADERS/PUSH_PROMISE/CONTINUATION等帧负载中进行组装报头块，执行解压还原报头集合，不管帧需要不需要被丢弃。接收端在解压时若不能够正常解压报头块，需要回应COMPRESSION_ERROR错误，然后终止连接。</p>
<h3>三。HTTP/2定义的帧</h3>
<p>规范定义了10个正式使用到帧类型，扩展实验类型的ALTSVC、BLOCKED等不在介绍之列。下面按照优先使用顺序重新排排序。</p>
<h4>1. SETTINGS</h4><pre>+-----------------------------------------------+
|                Length (24)                    |
+---------------+---------------+---------------+
|     0x4 (8)   | 0000 000? (8) |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier/0x0 (32)                   |
+=+=============================+===============================+
|       Identifier (16)         |
+-------------------------------+-------------------------------+
|                        Value (32)                             |
+---------------------------------------------------------------+
|       Identifier (16)         |
+-------------------------------+-------------------------------+
|                        Value (32)                             |
+---------------------------------------------------------------+  </pre>
<p>设置帧，接收者向发送者通告己方设定，服务器端在连接成功后必须第一个发送的帧。</p>
<p>字段Identifier定义了如下参数： - SETTINGS_HEADER_TABLE_SIZE (0x1)，通知接收者报头表的字节数最大值，报头块解码使用；初始值为4096个字节，默认可不用设置 - SETTINGS_ENABLE_PUSH (0x2)，0：禁止服务器推送，1：允许推送；其它值非法，PROTOCOL_ERROR错误 - SETTINGS_MAX_CONCURRENT_STREAMS (0x3)，发送者允许可打开流的最大值，建议值100，默认可不用设置；0值为禁止创建新流 - SETTINGS_INITIAL_WINDOW_SIZE (0x4)，发送端流控窗口大小，默认值2^16-1 (65,535)个字节大小；最大值为2^31-1个字节大小，若溢出需要报FLOW_CONTROL_ERROR错误 - SETTINGS_MAX_FRAME_SIZE (0x5)，单帧负载最大值，默认为2^14（16384）个字节，两端所发送帧都会收到此设定影响；值区间为2^14（16384）-2^24-1(16777215) - SETTINGS_MAX_HEADER_LIST_SIZE (0x6)，发送端通告自己准备接收的报头集合最大值，即字节数。此值依赖于未压缩报头字段，包含字段名称、字段值以及每一个报头字段的32个字节的开销等；文档里面虽说默认值不受限制，因为受到报头集合大小不限制的影响，个人认为不要多于2 <em>SETTINGS_MAX_FRAME_SIZE（即2^14</em>2=32768），否则包头太大，隐患多多。</p>
<p>标志位： * ACK (0x1)，表示接收者已经接收到SETTING帧，作为确认必须设置此标志位，此时负载为空，否则需要报FRAME_SIZE_ERROR错误</p>
<p>注意事项： - 在连接开始阶段必须允许发送SETTINGS帧，但不一定要发送 - 在连接的生命周期内可以允许任一端点发送 - 接收者不需要维护参数的状态，只需要记录当前值即可 - SETTINGS帧仅作用于当前连接，不针对单个流，因此流标识符为0x0 - 不完整或不合规范的SETTINGS帧需要抛出PROTOCOL_ERROR类型连接错误 - 负载字节数为6个字节的倍数，6*N (N&gt;=0)</p>
<p>处理流程： - 发送端发送需要两端都需要携带有遵守的SETTINGS设置帧，不能够带有ACK标志位 - 接收端接收到无ACK标志位的SETTINGS帧，必须按照帧内字段出现顺序一一进行处理，中间不能够处理其它帧 - 接收端处理时，针对不受支持的参数需忽略 - 接收端处理完毕之后，必须响应一个包含有ACK确认标志位、无负载的SETTINGS帧 - 发送端接收到确认的SETTINGS帧，表示两端设置已生效 - 发送端等待确认若超时，报SETTINGS_TIMEOUT类型连接错误</p>
<h4>2. HEADER</h4><pre>+-----------------------------------------------+
|                Length (24)                    |
+---------------+---------------+---------------+
|    0x1 (8)    | 00?0 ??0? (8) |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier (31)                       |
+=+=============+===============================================+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E|                 Stream Dependency? (31)                     |
+-+-------------+-----------------------------------------------+
|  Weight? (8)  |
+-+-------------+-----------------------------------------------+
|                   Header Block Fragment (*)                 ...
+---------------------------------------------------------------+
|                           Padding (*)                       ...
+---------------------------------------------------------------+</pre>
<p>报头主要载体，请求头或响应头，同时呢也用于打开一个流，在流处于打开"open"或者远程半关闭"half closed (remote)"状态都可以发送。</p>
<p>字段列表： - Pad Length：受制于PADDED标志控制是否显示，8个比特表示填充的字节数。 - E：一个比特表示流依赖是否专用，可选项，只在流优先级PRIORITY被设置时有效 - Stream Dependency：31个比特表示流依赖，只在流优先级PRIORITY被设置时有效 Weight：8个比特（一个字节）表示无符号的自然数流优先级，值范围自然是(1~256)，或称之为权重。只在流优先级PRIORITY被设置时有效 - Header Block Fragment：报头块分片 - Padding：填充的字节，受制于PADDED标志控制是否显示，长度由Pad Length字段决定</p>
<p>所需标志位： <em>END_STREAM (0x1): 报头块为最后一个，意味着流的结束。后续可紧接着CONTINUATION帧在当前的流中，需要把CONTINUATION帧作为HEADERS帧的一部分对待 </em>END_HEADERS (0x4): 此报头帧不需分片，完整的一个帧。后续不再需要CONTINUATION帧帮忙凑齐。若没有此标志的HEADER帧，后续帧必须是以CONTINUATION帧传递在当前的流中，否则接收者需要响应PROTOCOL_ERROR类型的连接错误。 <em>PADDED (0x8): 需要填充的标志 </em>PRIORITY (0x20): 优先级标志位，控制独立标志位E，流依赖，和流权重。</p>
<p>注意事项： - 其负载为报头块分片，若内容过大，需要借助于CONTINUATION帧继续传输。若流标识符为0x0，结束段需要返回PROTOCOL_ERROR连接异常。HEADERS帧包含优先级信息是为了避免潜在的不同流之间优先级顺序的干扰。 - 其实一般来讲，报文头部不大的情况下，一个HEADERS就可以完成了，特殊情况就是Cookie字段超过16KiB大小，不常见。</p>
<h4>3. CONTINUATION</h4><pre>+-----------------------------------------------+
|                Length (24)                    |
+---------------+---------------+---------------+
|  0x9 (8)      |  0x0/0x4  (8) |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier (32)                       |
+=+=============================================================+
|                  Header Block Fragment (*)                    |
+---------------------------------------------------------------+</pre>
<p>字段列表： - Header Block Fragment，用于协助HEADERS/PUSH_PROMISE等单帧无法包含完整的报头剩余部分数据。</p>
<p>注意事项： - 一个HEADERS/PUSH_PROMISE帧后面会跟随零个或多个CONTINUATION，只要上一个帧没有设置END_HEADERS标志位，就不算一个帧完整数据的结束。 - 接收端处理此种情况，从开始的HEADERS/PUSH_PROMISE帧到最后一个包含有END_HEADERS标志位帧结束，合并的数据才算是一份完整数据拷贝 - 在HEADERS/PUSH_PROMISE（没有END_HEADERS标志位）和CONTINUATION帧中间，是不能够掺杂其它帧的，否则需要报PROTOCOL_ERROR错误</p>
<p>标志位： * END_HEADERS(0X4)：表示报头块的最后一个帧，否则后面还会跟随CONTINUATION帧。</p>
<h4>4. DATA</h4>
<p>一个或多个DATA帧作为请求、响应内容载体，较为完整的结构如下：</p><pre>+-----------------------------------------------+
|                Length (24)                    |
+---------------+---------------+---------------+
| 0x0 (8)       | 0000 ?00? (8) |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier (31)                       |
+=+=============+===============================================+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
|                            Data (*)                         ...
+---------------------------------------------------------------+
|                          Padding? (*)                       ...
+---------------------------------------------------------------+</pre>
<p>字段： <em>Pad Length: 一个字节表示填充的字节长度。取决于PADDED标志是否被设置. </em>Data: 这里是应用数据，真正大小需要减去其他字段（比如填充长度和填充内容）长度。 * Padding: 填充内容为若干个0x0字节，受PADDED标志控制是否显示。接收端处理时可忽略验证填充内容。若验证，可以对非0x0内容填充回应PROTOCOL_ERROR类型连接异常。</p>
<p>标志位： <em>END_STREAM (0x1): 标志此帧为对应标志流最后一个帧，流进入了半关闭/关闭状态。 </em>PADDED (0x8): 负载需要填充，Padding Length + Data + Padding组成。</p>
<p>注意事项： - 若流标识符为0x0，接收者需要响应PROTOCOL_ERROR连接错误 - DATA帧只能在流处于"open" or "half closed (remote)"状态时被发送出去，否则接收端必须响应一个STREAM_CLOSED的连接错误。若填充长度不小于负载长度，接收端必须响应一个PROTOCOL_ERROR连接错误。</p>
<h4>5. PUSH_PROMISE</h4><pre>+-----------------------------------------------+
|                Length (24)                    |
+---------------+---------------+---------------+
|  0x5 (8)      | 0000 ??00 (8) |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier (32)                       |
+=+=============================================================+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R|                Promised Stream ID (31)                      |
+-+-------------------------------------------------------------+
|                  Header Block Fragment (*)                . . .
+---------------------------------------------------------------+
|                           Padding (*)                     . . .
+---------------------------------------------------------------+</pre>
<p>服务器端通知对端初始化一个新的推送流准备稍后推送数据： - 要求推送流为打开或远端半关闭（half closed (remote)）状态，否则报PROTOCOL_ERROR错误： - 承诺的流不一定要按照其流打开顺序进行使用，仅用作稍后使用 - 受对端所设置SETTINGS_ENABLE_PUSH标志位决定是否发送，否则作为PROTOCOL_ERROR错误对待 - 接收端一旦拒绝接收推送，会发送RST_STREAM帧告知对方推送无效</p>
<p>字段列表： - Promised Stream ID，31个比特表示无符号的自然数，为推送保留的流标识符，后续适用于发送推送数据 - Header Block Fragment，请求头部字段值，可看做是服务器端模拟客户端发起一次资源请求</p>
<p>标志位： <em>END_HEADERS（0x4/00000010），此帧包含完整的报头块，不用后面跟随CONTINUATION帧了 </em>PADDED（0x8/00000100），填充开关，决定了下面的Pad Length和Padding是否要填充，具体和HEADERS帧内容一致，不多说</p>
<h4>6. PING</h4>
<p>优先级帧，类型值为0x6，8个字节表示。发送者测量最小往返时间，心跳机制用于检测空闲连接是否有效。</p><pre>+-----------------------------------------------+
|                0x8 (24)                       |
+---------------+---------------+---------------+
|  0x6 (8)      | 0000 000? (8) |
+-+-------------+---------------+-------------------------------+
|R|                          0x0 (32)                           |
+=+=============================================================+
|                        Opaque Data (64)                       |
+---------------------------------------------------------------+</pre>
<p>字段列表： - Opaque Data：8个字节负载，值随意填写。</p>
<p>标志位： * ACK(0x1)：一旦设置，表示此PING帧为接收者响应的PING帧，非发送者。</p>
<p>注意事项： - PING帧发送方ACK标志位为0x0，接收方响应的PING帧ACK标志位为0x1。否则直接丢弃。其优先级要高于其它类型帧。 - PING帧不和具体流相关联，若流标识符为0x0，接收方需要响应PROTOCOL_ERROR类型连接错误。 - 超过负载长度，接收者需要响应FRAME_SIZE_ERROR类型连接错误。</p>
<h4>7. PRIORITY</h4>
<p>优先级帧，类型值为0x2，5个字节表示。表达了发送方对流优先级权重的建议值，在流的任何状态下都可以发送，包括空闲或关闭的流。</p><pre>+-----------------------------------------------+
|                   0x5 (24)                    |
+---------------+---------------+---------------+
|   0x2 (8)     |    0x0 (8)    |
+-+-------------+---------------+-------------------------------+
|R|                  Stream Identifier (31)                     |
+=+=============================================================+
|E|                  Stream Dependency (31)                     |
+-+-------------+-----------------------------------------------+
| Weight (8)    |
+---------------+</pre>
<p>字段列表： - E：流是否独立 - Stream Dependency：流依赖，值为流的标识符，自然也是31个比特表示。 - Weight：权重/优先级，一个字节表示自然数，范围1~256</p>
<p>注意事项： - PRIORITY帧其流标识符为0x0，接收方需要响应PROTOCOL_ERROR类型的连接错误。 - PRIORITY帧可在流的任何状态下发送，但限制是不能够在一个包含有报头块连续的帧里面出现，其发送时刻需要，若流已经结束，虽然可以发送，但已经没有什么效果。 - 超过5个字节PRIORITY帧接收方响应FRAME_SIZE_ERROR类型流错误。</p>
<h4>8. WINDOW_UPDATE</h4><pre>+-----------------------------------------------+
|                0x4 (24)                       |
+---------------+---------------+---------------+
|   0x8 (8)     |    0x0 (8)    |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier (31)                       |
+=+=============================================================+
|R|              Window Size Increment (31)                     |
+-+-------------------------------------------------------------+</pre>
<p>流量控制帧，作用于单个流以及整个连接，但只能影响两个端点之间传输的DATA数据帧。但需注意，中介不转发此帧。</p>
<p>字段列表： - Window Size Increment，31个比特位无符号自然数，范围为1-2^31-1（2,147,483,647）个字节数，表明发送者可以发送的最大字节数，以及接收者可以接收到的最大字节数。</p>
<p>注意事项： - 目前流控只会影响到DATA数据帧 - 流标识符为0，影响整个连接，非单个流 - 流标识符不为空，具体流的标识符，将只能够影响到具体流 - WINDOW_UPDATE在某个携带有END_STREAM帧的后面被发送（当前流处于关闭或远程关闭状态），接收端可忽略，但不能作为错误对待 - 发送者不能发送一个窗口值大于接收者已持有（接收端已经拥有一个流控窗口值）可用空间大小的WINDOW_UPDATE帧 - 当流控窗口所设置可用空间已耗尽时，对端发送一个零负载带有END_STREAM标志位的DATA数据帧，这是允许的行为 - 流量控制不会计算帧头所占用的9个字节空间 - 若窗口值溢出，针对单独流，响应RST_STREAM（错误码FLOW_CONTROL_ERROR）帧；针对整个连接的，响应GOAWAY（错误码FLOW_CONTROL_ERROR）帧 - DATA数据帧的接收方在接收到数据帧之后，需要计算已消耗的流控窗口可用空间，同时要把最新可用窗口空间发送给对端 - DATA数据帧发送方接收到WINDOW_UPDATE帧之后，获取最新可用窗口值 - 接收方异步更新发送方窗口值，避免流停顿/失速 - 默认情况下流量控制窗口值为65535，除非接收到SETTINGS帧SETTINGS_INITIAL_WINDOW_SIZE参数，或者WINDOWS_UPDATE帧携带的窗口值大小，否则不会改变 - SETTINGS_INITIAL_WINDOW_SIZE值的改变会导致窗口可用空间不明晰，易出问题，发送者必须停止受流控影响的DATA数据帧的发送直到接收到WINDOW_UPDATE帧获得新的窗口值，才会继续发送。eg：客户端在连接建立的瞬间一口气发送了60KB的数据，但来自服务器SETTINGS设置帧的初始窗口值为16KB，客户端只能够等到WINDOW_UPDATE帧告知新的窗口值，然后继续发送传送剩下的44KB数据 - SETTINGS帧无法修改针对整个连接的流量控制窗口值 - 任一端点在处理SETTINGS_INITIAL_WINDOW_SIZE值时一旦导致流控窗口值超出最大值，都需要作为一个FLOW_CONTROL_ERROR类型连接错误对待</p>
<h4>9. RST_STREAWM</h4>
<p>优先级帧，类型值为0x3，4个字节表示。表达了发送方对流优先级权重的建议值，任何时间任何流都可以发送，包括空闲或关闭的流。</p><pre>+-----------------------------------------------+
|                0x4 (24)                       |
+---------------+---------------+---------------+
|  0x3  (8)     |  0x0 (8)      |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier (31)                       |
+=+=============================================================+
|                        Error Code (32)                        |
+---------------------------------------------------------------+</pre>
<p>字段列表： - Error Code：错误代码，32位无符号的自然数表示流被关闭的错误原因。</p>
<p>注意事项： - 接收到RST_STREAM帧，需要关闭对应流，因此流也要处于关闭状态。 - 接收者不能够在此流上发送任何帧。 - 发送端需要做好准备接收接收端接收到RST_STREAM帧之前发送的帧，这个空隙的帧需要处理。 - 若流标识符为0x0，接收方需要响应PROTOCOL_ERROR类型连接错误。 - 当流处于空闲状态idle状态时是不能够发送RST_STREAM帧，否则接收方会报以PROOTOCOL_ERROR类型连接错误。</p>
<h4>10. GOAWAY</h4><pre>+-----------------------------------------------+
|                Length (24)                    |
+---------------+---------------+---------------+
|  0x7 (8)      |     0x0 (8)   |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier (32)                       |
+=+=============================================================+
|R|                  Last-Stream-ID (31)                        |
+-+-------------------------------------------------------------+
|                      Error Code (32)                          |
+---------------------------------------------------------------+
|                  Additional Debug Data (*)                    |
+---------------------------------------------------------------+</pre>
<p>一端通知对端较为优雅的方式停止创建流，同时还要完成之前已建立流的任务。</p>
<ul>
<li>一旦发送，发送者将忽略接收到的流标识符大于Last-Stream-ID任何帧 
</li><li>接收者不能够在当前流上创建新流，若创建新流则创建新的连接 
</li><li>可用于服务器的管理行为，比如服务器进入维护阶段，不再准备接收新的连接 
</li><li>字段Last-Stream-ID为发送方取自最后一个正在处理或已经处理流的标识符 
</li><li>后续创建的流标识符高于Last-Stream-ID数据帧都不会被处理 
</li><li>终端应被鼓励在关闭连接之前发送GOAWAY隐式方式告知对方某些流是否已经被处理 
</li><li>终端可以选择关闭连接，针对行为不当的终端不发送GOAWAY帧 
</li><li>GOAWAY应用于当前连接，非具体流 
</li><li>没有处理任何流的情况下，Last-Stream-ID值可为0，也是合法 
</li><li>流（标识符小于或等于已有编号的标识符）在连接关闭之前没有被完全关闭，需要创建新的连接进行重试 
</li><li>发送端在发送GOAWAY时还有一些流任务没有完成，将保持连接为打开状态直到任务完成 
</li><li>终端可以在自身环境发生改变时发送多个GOAWAY帧，但Last-Stream-ID不允许增长 
</li><li>Additional Debug Data没有语义，仅用于联机测试诊断目的。若携带登陆或持久化调试数据，需要有安全保证避免未经授权访问。 </li></ul>
<h3>四。帧的扩展</h3>
<p>HTTP/2协议的扩展是允许存在的，在于提供额外服务。扩展包括： - 新类型帧，需要遵守通用帧格式 - 新的设置参数，用于设置新帧相关属性 - 新的错误代码，约定帧可能触发的错误</p>
<p>当定义一个新帧，需要注意 1. 规范建议新的扩展需要经过双方协商后才能使用 1. 在SETTINGS帧添加新的参数项，可在连接序言时发送给对端，或者适当机会发送 1. 双方协商成功，可以使用新的扩展</p>
<p>已知ALTSVC、BLOCKED属于扩展帧。</p>
<h4>1. ALTSVC</h4>
<p>服务器提供给客户端当前可用的替代服务，类似于CNAME，客户端不支持可用选择忽略</p><pre>+-----------------------------------------------+
|                Length (24)                    |
+---------------+---------------+---------------+
|  0xa (8)      |     0x0 (8)   |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier (32)                       |
+=+=============================+===============================+
|         Origin-Len (16)       | Origin? (*)                 ...
+-------------------------------+-------------------------------+
|                   Alt-Svc-Field-Value (*)                   ...
+---------------------------------------------------------------+</pre>
<p>字段列表： - Origin-Len: 16比特位整数，说明了Origin字段字节数 - Origin: ASCII字符串表示替代服务 - Alt-Svc-Field-Value: 包含了Alt-Svc HTTP Header Field，长度=Length (24) - Origin-Len (16)</p>
<p>需要注意： - 中介设备不能转发给客户端，原因就是中介自身替换处理，转发正常的业务数据给客户端就行</p>
<p>具体可参考：<a href="https://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-06">https://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-06</a></p>
<h4>2. BLOCKED</h4>
<p>一端告诉另一端因为受到流量控制的作用有数据但无法发送。</p><pre>+-----------------------------------------------+
|                Length (24)                    |
+---------------+---------------+---------------+
|  0xb (8)      |     0x0 (8)   |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier/0x0 (32)                   |
+=+=============================================================+</pre>
<ul>
<li>Stream Identifier若为0x0，则表示针对整个连接，否则针对具体流 
</li><li>在流量控制窗口生效之前不能发送BLOCKED 
</li><li>一旦遇到此项问题，说明我们的实现可能有缺陷，无法得到理想的传输速率 
</li><li>只能够在WINDOW_UPDATE帧接收之前或SETTINGS_INITIAL_WINDOW_SIZE参数增加之前发送 </li></ul>
<h3>五。小结</h3>
<p>以上记录了HTTP/2帧基本结构，10个文档定义的正式帧，以及额外的两个扩展帧。</p></div><img src ="http://www.blogjava.net/yongboy/aggbug/423655.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/yongboy/" target="_blank">nieyong</a> 2015-03-20 09:24 <a href="http://www.blogjava.net/yongboy/archive/2015/03/20/423655.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>HTTP/2笔记之流和多路复用</title><link>http://www.blogjava.net/yongboy/archive/2015/03/19/423611.html</link><dc:creator>nieyong</dc:creator><author>nieyong</author><pubDate>Thu, 19 Mar 2015 02:15:00 GMT</pubDate><guid>http://www.blogjava.net/yongboy/archive/2015/03/19/423611.html</guid><wfw:comment>http://www.blogjava.net/yongboy/comments/423611.html</wfw:comment><comments>http://www.blogjava.net/yongboy/archive/2015/03/19/423611.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/yongboy/comments/commentRss/423611.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/yongboy/services/trackbacks/423611.html</trackback:ping><description><![CDATA[<div class="wrap"> <h3 id="-">零。前言</h3> <p>本部分将讲解HTTP/2协议中对流的定义和使用，其实就是在说HTTP/2是若何做到多路复用的。</p> <h3 id="-">一。流和多路复用的关系</h3> <h4 id="1-">1. 流的概念</h4> <p>流（Stream），服务器和客户端在HTTP/2连接内用于交换帧数据的独立双向序列，逻辑上可看做一个较为完整的交互处理单元，即表达一次完整的资源请求-响应数据交换流程；一个业务处理单元，在一个流内进行处理完毕，这个流生命周期完结。</p> <p>特点如下：</p> <ul> <li>一个HTTP/2连接可同时保持多个打开的流，任一端点交换帧  </li><li>流可被客户端或服务器单独或共享创建和使用  </li><li>流可被任一端关闭  </li><li>在流内发送和接收数据都要按照顺序  </li><li>流的标识符自然数表示，1~2^31-1区间，有创建流的终端分配  </li><li>流与流之间逻辑上是并行、独立存在 </li></ul> <h4 id="2-">2. 多路复用</h4> <p>流的概念提出是为了实现多路复用，在单个连接上实现同时进行多个业务单元数据的传输。逻辑图如下：</p> <p><img alt="" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/one_http2_connection_thumb_1.png" /></p> <p>实际传输可能是这样的：</p> <p><img alt="" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/http2_multiplexing_real_thumb.png" /></p> <p>只看到帧(Frame)，没有流（Stream）嘛。</p> <p>需要抽象化一些，就好理解了：</p> <ol> <li>每一个帧可看做是一个学生，流可以认为是组（流标识符为帧的属性值），一个班级（一个连接）内学生被分为若干个小组，每一个小组分配不同的具体任务。  </li><li>HTTP/1.* 一次请求-响应，建立一个连接，用完关闭；每一个小组任务都需要建立一个班级，多个小组任务多个班级，1:1比例  </li><li>HTTP/1.1 Pipeling解决方式为，若干个小组任务排队串行化单线程处理，后面小组任务等待前面小组任务完成才能获得执行机会，一旦有任务处理超时等，后续任务只能被阻塞，毫无办法，也就是人们常说的线头阻塞  </li><li>HTTP/2多个小组任务可同时并行（严格意义上是并发）在班级内执行。一旦某个小组任务耗时严重，但不会影响到其它小组任务正常执行  </li><li>针对一个班级资源维护要比多个班级资源维护经济多了，这也是多路复用出现的原因 </li></ol> <p>这样简单梳理，就有些小清晰了。</p> <h4 id="3-">3. 流的组成</h4> <p>流的概念提出，就是为了实现多路复用。影响因素：</p> <ol> <li>流的优先级（priority）属性建议终端（客户端+服务器端）需要按照优先级值进行资源合理分配，优先级高的需要首先处理，优先级低的可以稍微排排队，这样的机制可保证重要数据优先处理。  </li><li>流的并发数（或者说同一时间存在的流的个数）初始环境下不少于100个  </li><li>流量控制阀协调网络带宽资源利用，由接收端提出发送端遵守其规则  </li><li>流具有完整的生命周期，从创建到最终关闭，经历不同阶段 </li></ol> <p>流总体组成如下：</p> <p><img alt="" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/http2_stream_thumb.png" /></p> <p>搞清楚了流和多路复用之间关系，下面稍微深入一点，学习流的一些细节。</p> <h3 id="-">二。流的属性</h3> <h4 id="1-">1. 流状态/生命周期</h4> <p><img alt="" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/http2%20stream%20status_thumb.png" /></p> <p>帧的行为以及END_STREAM标志位都会对流的状态的产生变化。因为流由各个端独立创建，没有协商，消极后果就是（两端无法匹配的流的状态）导致发送完毕RST_STREAM帧之后&#8220;关闭&#8221;状态受限，因为帧的传输和接收需要一点时间。</p> <p>帧的状态列表：</p> <ol> <li>idle，所有流的开始状态值 <ul> <li>发送/接收HEADERS帧，进入open状态  </li><li>PUSH_PROMISE帧只能在已有流上发送，导致创建的本地推送流处于"resereved(local)"状态  </li><li>在已有流上接收PUSH_PORMISE帧，导致本地预留一个流处于"resereved(remote)"状态  </li><li>HEADERS/PUSH_PROMISE帧以及后面的零个或多个CONTINUATION帧，只要携带有END_STREAM标志位，流状态将进入"half closed"状态  </li><li>只能接收HEADERS和PRIORITY，否则报PROTOCOL_ERROR类型连接错误 </li></ul> </li><li> <p>reserved，为推送保留一个流稍后使用</p> <ol> <li>reserved (local)，服务器端发送完PUSH_PROMISE帧本地预留的一个用于推送流所处于的状态 <ul> <li>只能发送HEADERS、RST_STREAM、PRIORITY帧  </li><li>只能接收RST_STREAM、PRIORITY、WINDOW_UPDATE帧 </li></ul> </li><li> <p>reserved (remote)，客户端接收到PUSH_PROMISE帧，本地预留的一个用于接收推送流所处于的状态</p> <ul> <li>只能发送WINDOW_UPDATE、RST_STREAM、PRIORITY帧  </li><li>只能接收RST_STREAM、PRIORITY、HEADERS帧 </li></ul> <p>不满足条件，需要报PROTOCOL_ERROR类型连接错误</p></li></ol> </li><li>open，用于两端发送帧，需要发送数据的对等端需要遵守流量控制的通告。 <ul> <li>每一端可以发送包含END_STREAM标志位的帧，导致流进入"half closed"状态  </li><li>每一端都可以发送RST_STREAM帧，流进入"closed"状态 </li></ul> </li><li> <p>half closed</p> <ol> <li>half closed (local)，发送包含有END_STREAM标志位帧的一端，流进入本地半关闭状态 <ul> <li>不能发送WINDOW_UPDATE，PRIORITY和RST_STREAM帧  </li><li>可以接收到任何类型帧  </li><li>接收者可以忽略WINDOW_UPDATE帧，后续可能会马上接收到包含有END_STREAM标志位帧  </li><li>接收到优先级PRIORITY帧，可用来变更依赖流的优先级顺序，有些小复杂了  </li><li>一旦接收到包含END_STREAM标志位的帧，将进入"closed"状态 </li></ul> </li><li> <p>half closed (remote)，接收到包含有END_STREAM标志位帧的一端，流进入远程半关闭状态</p> <ul> <li>对流量控制窗口可不用维护  </li><li>只能接收RST_STREAM、PRIORITY、WINDOW_UPDATE帧，否则报STREAM_CLOSED流错误  </li><li>终端可以发送任何类型帧，但需要遵守对端的当前流的流量控制限制  </li><li>一旦发送包含END_STREAM标志位的帧，将进入"closed"状态 </li></ul> <p>一旦接收或发送RST_STREAM帧，流将进入"closed"状态。</p></li></ol> </li><li>closed，流的最终关闭状态 <ul> <li>只允许发送PRIORITY帧，对依赖关闭的流进行重排序  </li><li>终端接收RST_STREAM帧之后，只能接收PRIORITY帧，否则报STREAM_CLOSED流错误  </li><li>接收的DATA/HEADERS帧包含有END_STREAM标志位，在一个很短的周期内可以接收WINDOW_UPDATE或RST_STREAM帧；超时后需要作为错误对待  </li><li>终端必须忽略WINDOW_UPDATE或RST_STREAM帧  </li><li>终端发送RST_STREAM帧之后，必须忽略任何接收到的帧  </li><li>在RST_STREAM帧被发送之后收到的流量受限DATA帧，转向流量控制窗口连接处理。尽管这些帧可以被忽略，因为他们是在发送端接收到RST_STREAM之前发送的，但发送端会认为这些帧与流量控制窗口不符。  </li><li>终端在发送RST_STREAM之后接收PUSH_PROMISE帧，尽管相关流已被重置，但推送帧也能使流变成&#8220;保留&#8221;状态。因此，可用RST_STREAM帧关闭一个不想要的承诺流 </li></ul></li></ol> <p>要求如下：</p> <ol> <li>针对具体状态中出现没有允许出现的帧，需要作为协议错误(PROTOCOL_ERROR)类型的连接错误处理  </li><li>在流的任何状态下，PRIORITY帧都可以被发送或接收  </li><li>未知帧可以被忽略 </li></ol> <h4 id="2-">2. 流标识符</h4> <ol> <li>31个字节表示无符号的整数，1~2^31-1  </li><li>客户端创建的流以奇数表示，服务器端创建流以偶数表示  </li><li>0x0用来表示连接控制信息流，不能够创建新流  </li><li>通过http/1.1 101 协议切换升级切换到HTTP/2，0x1所指代流处于"half closed(local)"，不能用于创建新流  </li><li>新建流的标识符要大于已有流和预留的流的标识符  </li><li>新建流第一次被使用时，低于此标识符的并且处于空闲"idle"状态的流都会被关闭  </li><li>已使用的流标识符不能被再次使用  </li><li>终端的流标识符若被耗尽的情况下 <ul> <li>若是客户端，需要关闭连接，创建新的连接创建新流  </li><li>若是服务器端，需要发送一个GOAWAY帧通知客户端，强迫其打开一个新连接 </li></ul></li></ol> <h4 id="3-">3. 流的并发数量</h4> <ol> <li>每一端都可以发送包含有SETTINGS_MAX_CONCURRENT_STREAMS参数的SETTINGS帧限制对等端流的最大并发量  </li><li>对等端接收之后遵守终端最大并发量限制约定  </li><li>状态为"open"或"half closed"的流需要计入限制总数  </li><li>保留态"reserved"流不算入限制总数内  </li><li>终端接收到HEADERS帧导致创建的流总数超过限制，需要响应PROTOCOL_ERROR或REFUSED_STREAM错误，具体哪一种错误，需要根据终端是否可以检测得到允许自动重复重试  </li><li>终端想降低SETTINGS_MAX_CONCURRENT_STREAMS设置的活动流的上限，若低于当前已经打开流的数值，可以选择光比溢出的流或者允许流继续存在直到完成 </li></ol> <h4 id="4-">4. 流的优先级</h4> <p>流的优先级在于允许终端向对端表达所期待的给予具体流更多资源支持的意见的表达，不能保证对端一定会遵守，非强制性需求建议；默认值16。在资源有限时，可以保证基本数据的传输。</p> <p>优先级改变：</p> <ol> <li>终端可在新建的流所传递HEADERS帧中包含优先级priority属性  </li><li>可单独通过PRIORITY帧专门设置流的优先级属性 </li></ol> <h4 id="5-">5. 流依赖</h4> <ol> <li>流与流之间存在依赖、被依赖关系。所有流默认依赖流0x0；推送流依赖于传输PUSH_PROMISE的关联流。  </li><li>依赖权重值1~256区间，对于依赖同一父级的子节点，应该根据权重比列进行分配资源。  </li><li>对于依赖同一个父级流的子节点被指定相关权重值，以及可用资源的分配比重。子节点之间顺序不固定。<pre><code>    A                 A
   / \      ==&gt;      /|\
  B   <span class="hljs-keyword">C</span>             B <span class="hljs-keyword">D</span> <span class="hljs-keyword">C</span>
</code></pre>
</li><li>一旦设置独家专属标志（exclusive flag）将为现有依赖插入一个水平的依赖关系，其父级流只能被插入的新流所依赖。比如流D设置专属标志并依赖于流A：<pre><code>                      A
    A                 |
   / \      ==&gt;       <span class="hljs-keyword">D</span>
  B   <span class="hljs-keyword">C</span>              / \
                    B   <span class="hljs-keyword">C</span>
</code></pre>
</li><li>流的依赖树形模型，底层的流只能等到上层流被关闭或无法正常运转/失效时，才会被分配到资源 
</li><li>流无法依赖自身，否则为PROTOCOL_ERROR流错误 
</li><li>在流依赖树形模型中，父节点优先级，以及专属依赖流的加入等，都会导致已有优先级重排序<pre><code>    ?                ?                ?                 ?
    |               / \               |                 |
    A              <span class="hljs-keyword">D</span>   A              <span class="hljs-keyword">D</span>                 <span class="hljs-keyword">D</span>
   / \            /   / \            / \                |
  B   <span class="hljs-keyword">C</span>     ==&gt;  F   B   <span class="hljs-keyword">C</span>   ==&gt;    F   A       OR      A
     / \                 |             / \             /|\
    <span class="hljs-keyword">D</span>   <span class="hljs-keyword">E</span>                <span class="hljs-keyword">E</span>            B   <span class="hljs-keyword">C</span>           B <span class="hljs-keyword">C</span> F
    |                                     |             |
    F                                     <span class="hljs-keyword">E</span>             <span class="hljs-keyword">E</span>
               (intermediate)   (non-exclusive)    (exclusive)
</code></pre></li></ol>
<h4 id="6-">6. 流优先级状态管理</h4>
<ol>
<li>流的依赖树形模型，任一节点被移除，都需要重建优先级顺序，重新分配资源 
</li><li>终端建议在流关闭一段时间内保留优先级信息，减少潜在的指派错误 
</li><li>处于"idle"状态流可被指派默认优先级16，这时可以变成其它流的父节点，可以指派新的优先级值 
</li><li>终端持有的流优先级信息不受SETTINGS_MAX_CONCURRENT_STREAMS限制，但可能会造成终端状态维护负担，其数量可以被限制不多于SETTINGS_MAX_CONCURRENT_STREAMS所定义数量 
</li><li>优先级状态信息的维持在负载较高时可以被丢弃，以减少资源占用。 
</li><li>终端若有能力保留足够状态，在接收到PRIORITY帧目的修改已被关闭流的优先级时，可以为其子节点重建优先级顺序 </li></ol>
<h4 id="7-">7. 流量控制</h4>
<p>多路复用会引入资源竞争，流量控制可以保证流之间不会严重影响到彼此。流量控制通过使用WINDOW_UPDATE帧实现，可作用于单个流以及整个的连接。一些原则如下：</p>
<ol>
<li>逐跳，具有方向性 
</li><li>不能够被禁止 
</li><li>初始窗口值为65535字节，针对单个流，以及整个连接都有效 
</li><li>基于WINDOW_UPDATE帧传输实现，接收端通告对端准备在流/连接上接收的字节数 
</li><li>接收端完全控制权限，接受端可通告针对流/连接的窗口值，发送者需要遵守 
</li><li>目前只有DATA帧可被流量控制，仅针对其有效负载计算；超出窗口值，其负载可以为空 </li></ol>
<p>需要注意事项：</p>
<ol>
<li>流量控制是为解决线头阻塞问题，同时在资源约束情况下保护一些操作顺利进行，针对单个连接，某个流可能被阻塞或处理缓慢，但同时不会影响到其它流上正在传输的数据 
</li><li>虽然流量控制可以用来限制一个对等端消耗的内存，但若在不知道网络带宽延迟乘积的情况下可能未必能够充分利用好网络资源 
</li><li>流量控制机制很复杂，需要考虑大量的细节，实现很困难 </li></ol>
<h3 id="-">三。小结</h3>
<p>HTTP/2规范中所定义的流概念、属性很复杂，在请求量很大以及应对海量并发的情况下，整个连接的流量控制+单个流的流量控制+流的状态+流优先级属性+优先级的状态+流依赖树形模型等一系列新特性，可能会造成：</p>
<ol>
<li>服务器端/客户端单个连接内存占用过高，维护一个长连接的成本比以往多了若干倍 
</li><li>流量控制是一个复杂功能，实现不好会导致一端流量窗口值已被耗尽，需要等待客户端发送新的流控窗口值，若有热数据进行发送，需要等待成本，无形中增加了额外的交互步骤 
</li><li>流依赖和优先级重排序等，无形中增加了程序的复杂度，处理不好触发潜在BUG 
</li><li>为了性能和内存考虑，很多知名应用不见得有动力实现全部特性，流的一些高级特性毕竟有些过于理想化，诸如当前实现列表：<a href="https://github.com/http2/http2-spec/wiki/Implementations">https://github.com/http2/http2-spec/wiki/Implementations</a>，可以看出一二 
</li><li>实际非浏览器环境，诸如HTTP API等，实际上仅需要部分关键特性，这属于情理之中的选择 
</li><li>凡是状态皆需要维护，无论横向还是纵向的扩展都需要倍加注意；无状态才是最有利于扩展 </li></ol></div><img src ="http://www.blogjava.net/yongboy/aggbug/423611.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/yongboy/" target="_blank">nieyong</a> 2015-03-19 10:15 <a href="http://www.blogjava.net/yongboy/archive/2015/03/19/423611.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>HTTP/2笔记之连接建立</title><link>http://www.blogjava.net/yongboy/archive/2015/03/18/423570.html</link><dc:creator>nieyong</dc:creator><author>nieyong</author><pubDate>Wed, 18 Mar 2015 05:54:00 GMT</pubDate><guid>http://www.blogjava.net/yongboy/archive/2015/03/18/423570.html</guid><wfw:comment>http://www.blogjava.net/yongboy/comments/423570.html</wfw:comment><comments>http://www.blogjava.net/yongboy/archive/2015/03/18/423570.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/yongboy/comments/commentRss/423570.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/yongboy/services/trackbacks/423570.html</trackback:ping><description><![CDATA[<div class="wrap"> <h3 id="-">前言</h3> <p>HTTP/2协议在TCP连接之初进行协商通信，只有协商成功，才会涉及到后续的请求-响应等具体的业务型数据交换。</p> <h3 id="http-">HTTP版本标识符</h3> <ul> <li>h2，基于TLS之上构建的HTTP/2，作为ALPN的标识符，两个字节表示，0x68, 0x32，即https</li> <li>h2c，直接在TCP之上构建的HTTP/2，缺乏安全保证，即http</li> <li>在HTTP/2 RFC文档出现之前，以上版本字段需要添加上草案版本号，类似于h2-11,h2c-17</li> </ul> <h4 id="http-2-">HTTP/2 请求过程</h4> <p>针对直接建立在标准TCP之上HTTP2，在未知服务器是否提供HTTP/2支持之前，可以依赖现有HTTP/1.1进行试探。</p> <h4 id="http-">HTTP版本的请求内容</h4> <ol> <li>客户端发起请求，只有请求报头：<pre><code>GET <span class="hljs-regexp">/ HTTP/</span><span class="hljs-number">1.</span> <span class="hljs-number">1</span>
<span class="hljs-string">Host:</span> server. example. com
<span class="hljs-string">Connection:</span> Upgrade, HTTP2-Settings
<span class="hljs-string">Upgrade:</span> h2c
HTTP2-<span class="hljs-string">Settings:</span> &lt;base64url encoding of HTTP/<span class="hljs-number">2</span> SETTINGS payload&gt;
</code></pre></li>
<li>服务器若不支持HTTP/2，直接按照HTTP/1.1响应即可<pre><code>HTTP/<span class="hljs-number">1.</span> <span class="hljs-number">1</span> <span class="hljs-number">200</span> OK
Content-<span class="hljs-string">Length:</span> <span class="hljs-number">243</span>
Content-<span class="hljs-string">Type:</span> text/html
. . .
</code></pre></li>
<li><p>服务器支持HTTP/2，通知客户端一起切换到HTTP/2协议下吧</p>
<pre><code><span class="hljs-status">HTTP/1. 1 <span class="hljs-number">101</span> Switching Protocols</span>
<span class="hljs-attribute">Connection</span>: <span class="hljs-string">Upgrade</span>
<span class="hljs-attribute">Upgrade</span>: <span class="hljs-string">h2c</span>

<span class="erlang">[ <span class="hljs-variable">HTTP</span>/<span class="hljs-number">2</span> connection . . .</span>
</code></pre></li>
<li>101响应空行之后，服务器必须发送的第一个帧为SETTINGS帧（其负载可能为空）作为连接序言</li>
<li>客户端接收到101响应后，也必须发送一个序言作为响应，其逻辑结构：<pre><code>PRI * HTTP/2.0<span class="hljs-command">\r</span><span class="hljs-command">\n</span><span class="hljs-command">\r</span><span class="hljs-command">\nSM</span><span class="hljs-command">\r</span><span class="hljs-command">\n</span><span class="hljs-command">\r</span><span class="hljs-command">\n</span> // 纯字符串表示，翻译成字节数为24个字节
SETTINGS帧                       // 其负载可能为空
</code></pre>服务器端和客户端所发送的连接序言有所不同。</li>
<li>客户端可以马上发送请求帧或其它帧过去，不用等待来自服务器端的SETTINGS帧</li>
<li>任一端接收到SETTINGS帧之后，都需要返回一个包含确认标志位SETTIGN作为确认</li>
<li>其它帧的正常传输</li>
</ol>
<h4 id="http-2-">HTTP/2的直接连接</h4>
<p>客户端预先知道服务器提供HTTP/2支持，可以免去101协议切换的流程开销。
具体流程：</p>
<ol>
<li>客户端必须首先发送一个连接序言，其逻辑结构：<pre><code>PRI * HTTP/2.0<span class="hljs-command">\r</span><span class="hljs-command">\n</span><span class="hljs-command">\r</span><span class="hljs-command">\nSM</span><span class="hljs-command">\r</span><span class="hljs-command">\n</span><span class="hljs-command">\r</span><span class="hljs-command">\n</span> // 纯字符串表示，翻译成字节数为24个字节
SETTINGS帧                       // 其负载可能为空
</code></pre></li>
<li>发送完毕序言之后，客户端可以不用等待来自服务器端响应，马上发送HTTP/2其它帧</li>
<li>服务器端接收到客户端的连接序言之后，需要发送一个SETTINGS帧作为连接序言</li>
<li>任一端接收到SETTINGS帧之后，都需要返回一个包含确认标志位SETTIGN作为确认</li>
<li>其它帧的正常传输</li>
</ol>
<h4 id="https-">HTTPS版本建立连接</h4>
<p>HTTP/2安全版本在TLS上构建，协商采用的ALPN扩展协议，采用&#8220;h2&#8221;作为协议标识符（http版本则是&#8220;h2c&#8221;）。一定程度上可认为不存在试探是否支持或直接连接的烦恼，因为这个过程直接在TLS层协商而成。</p>
<p>流程如下：</p>
<ol>
<li>客户端和服务器端TLS层协商</li>
<li>客户端发送连接序言（同上表示，PRI + SETTINGS）</li>
<li>接收到客户端连接序言之后，服务器端发送连接序言</li>
<li>双方各自确认SETTINGS帧</li>
<li>其它帧的正常传输</li>
</ol>
<h4 id="https-http-upgrade-">HTTPS和HTTP Upgrade方式协商</h4>
<p>HTTPS协商是强制，封装在TLS之上ALPN扩展实现，HTTP只有非直接连接方式才会存在通过101 协议切换方式进行升级。</p>
<p>这里有一张图形象说明其流程。</p>
<p><img src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/image_14.png" alt="" /></p>
<h4 id="-">统一的连接过程</h4>
<p>这里不论是HTTP还是HTTPS，在两端成功协商之后（或HTTP的直接连接），其连接过程都是一样的</p>
<p><img src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/image_thumb_6.png" alt="" /></p>
<h3 id="-">注意事项</h3>
<ol>
<li>客户端发起的HTTP/1.1请求，其流标识符为1，默认优先级；半关闭&#8220;half closed&#8221;状态，一旦完成HTTP/2的连接，将被应用于响应</li>
<li>文档提到的客户端可以通过HTTP Alternative Services（简称为[ALT-SVC]，类似于CNAME机制）获得通知服务器是否支持HTTP/2，但目前看来仅仅是草案建议而已</li>
<li>连接序言用于最后两端协商确认双方要使用HTTP/2协议，建立初始化的HTTP/2连接环境</li>
<li>客户端若知服务器支持HTTP/2，可免去通过HTTP/1.1 101协议切换方式进行升级，在建立连接后即可发送序言，否则只能在接收到服务器端101响应后发送序言</li>
<li>建立在TLS上的HTTP/2通过ALPN扩展协商机制取代101协议切换</li>
<li>连接序言所包含的SETTINGS帧其负载可以为空</li>
<li>针对一个TCP连接，服务器第一个要发送的帧必须是SETTINGS帧</li>
<li>为了避免不必要延迟，客户端可以在发送完毕序言之后发送帧数据，不用等待来自服务器端的序言SETTINGS帧</li>
<li>客户端接收到服务器端作为序言的SETTINGS帧，需要遵守其设定</li>
<li>在一些环境下需要提供一个顺序机制，允许服务器在客户端发送业务帧之前发送SETTINGS，这需要客户端配合</li>
<li>客户端和服务器端任何一方接收到无效连接序言需要抛出PROTOCOL_ERROR类型连接错误，若收到GOAWAY帧，可忽略</li>
</ol>
<h3 id="-">小结</h3>
<p>HTTP/2连接的建立协商机制比HTTP/1.1稍微复杂了一些。</p>
<p>对比明文版的HTTP/1.1和HTTP/2完成一次请求-响应：</p>
<ol>
<li>HTTP/1.1在建立建立之后，只需要发送请求报文数据</li>
<li>HTTP/2客户端需要在连接建立之初马上发送一个连接序言过去，然后才是正常请求</li>
<li>两端（客户端+服务器端）的两次完整的连接序言+确认的交互流程，多了两次往返过程</li>
</ol>
<p>在弱网络环境下，会不会加重网络负载，只能拭目一看了。</p>
<h3 id="-">引用</h3>
<ol>
<li>《Implementing HTTP/2 client in 60 minutes》</li>
</ol>
</div><img src ="http://www.blogjava.net/yongboy/aggbug/423570.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/yongboy/" target="_blank">nieyong</a> 2015-03-18 13:54 <a href="http://www.blogjava.net/yongboy/archive/2015/03/18/423570.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>HTTP/2笔记之开篇</title><link>http://www.blogjava.net/yongboy/archive/2015/03/17/423531.html</link><dc:creator>nieyong</dc:creator><author>nieyong</author><pubDate>Tue, 17 Mar 2015 06:50:00 GMT</pubDate><guid>http://www.blogjava.net/yongboy/archive/2015/03/17/423531.html</guid><wfw:comment>http://www.blogjava.net/yongboy/comments/423531.html</wfw:comment><comments>http://www.blogjava.net/yongboy/archive/2015/03/17/423531.html#Feedback</comments><slash:comments>3</slash:comments><wfw:commentRss>http://www.blogjava.net/yongboy/comments/commentRss/423531.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/yongboy/services/trackbacks/423531.html</trackback:ping><description><![CDATA[<div id="out"> <h3>前言</h3> <p>本系列基于HTTP/2第17个草案文档，地址就是：<a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-17。">https://tools.ietf.org/html/draft-ietf-httpbis-http2-17。</a></p> <p>HTTP/2规范已经通过发布批准，下面等待分配具体的RFC号码，不会有所较大的变动了。</p> <p>本笔记不是直接翻译，记录成笔记方便以后学习。</p> <h3>HTTP/1.1的问题</h3> <p>一张图可以很较为全面的概括了HTTP/1.*存在缺陷：</p> <p><img src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/image_thumb_1.png" border="0" height="286" width="616"  alt="" /></p> <p>前端一般采用： - CSS Spriting，小图合并成大图，CSS进行分割成小图显示 - Inlining，使用DataURL方式内嵌Base64编码格式图片 - JS Concatenation，多个JS文件合并成一个，缺陷是一旦有文件修改，需要重新合并 - Sharding，将资源/服务部署到多个机器上，均摊/分享请求压力</p> <p>HTTP/1.*没有充分利用TCP特性，再加上同一个站点打开多个连接等，导致网络资源利用率不高。</p> <h3>HTTP/2改进点</h3> <p>与HTTP/1相比，主要区别包括：</p> <ol> <li>HTTP/2采用二进制格式而非文本格式 <ul> <li>高效紧凑传输解析  </li><li>更少错误倾向，没有了所谓的空白行、大写、换行符、空连接等 </li></ul> </li><li>HTTP/2是完全多路复用的，而非有序并阻塞的 <ul> <li>HTTP/1.1默认情况下单个请求单个连接  </li><li>HTTP/1.1流水线化pipelining方式导致较大/较慢的响应阻塞后续任务  </li><li>HTTP/1.1无法解决线头阻塞的问题  </li><li>多路复用可以有效避免线头阻塞，多个请求-响应同一个连接内并行处理 </li></ul> </li><li>只需一个连接即可实现并行 <ul> <li>HTTP/1.*请求处理模型导致同一个站点资源客户端需要打开若4-8个连接请求  </li><li>HTTP/1.*多个连接会占用过多网路资源，导致TCP堵塞和数据重传  </li><li>HTTP/2单个连接内多个流（请求-响应）之间并行处理，减少网路资源占用，可避免了TCP频繁的打开、关闭 </li></ul> </li><li>使用报头压缩，HTTP/2降低了开销 <ul> <li>传统浏览器网路报头一般在80-1400字节大小  </li><li>压缩头部可让报头更紧凑，更快速传输，有利于移动网络环境等  </li><li>压缩算法使用HPACK，更为高效、安全 </li></ul> </li><li>HTTP/2让服务器可以将响应主动&#8220;推送&#8221;到客户端 <ul> <li>传统方式：客户端请求，服务器响应，客户端逐一解析需要后续请求的图片、样式等资源，再次一一发送资源请求  </li><li>HTTP/2服务器根据客户端请求，计算出响应内容所包含的资源，在客户端发起请求之前提前发送给客户端  </li><li>节省客户端主动发起请求的时间的往返时间 </li></ul></li></ol> <p>这里有一张图，可以总体上了解HTTP/2：</p> <p><img src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/5264ea818301_D4ED/image_thumb.png" border="0" height="286" width="616"  alt="" /></p> <h4>HTTP/2的解读</h4> <p>保留/兼容HTTP/1.1的所有语义，但传输语法（或者说传输方式）改变，目的在于更充分利用TCP更高效传输，多路复用是实现途径，低延迟是改进方向。</p> <h3>笔记提纲</h3> <p>以上为简单总体介绍了HTTP/2协议，要想深入其特性，需要阅读器规范。下面为围绕HTTP/2规范的各个方面，列出提纲，便于后面一一填充。</p> <ol> <li><a href="http://www.blogjava.net/yongboy/archive/2015/03/18/423570.html">HTTP/2的连接建立</a>  </li><li><a href="http://www.blogjava.net/yongboy/archive/2015/03/19/423611.html">HTTP/2的多路复用和流的属性</a>  </li><li><a href="http://www.blogjava.net/yongboy/archive/2015/03/20/423655.html">HTTP/2的帧定义</a>  </li><li><a href="http://www.blogjava.net/yongboy/archive/2015/03/23/423751.html">HTTP/2的消息交换</a>  </li><li><a href="http://www.blogjava.net/yongboy/archive/2015/03/24/423791.html">HTTP/2的错误处理和安全事项</a> </li></ol> <h3>名词解释</h3> <p>以下名词会在当前或以后笔记中出现，贴出来方便理解。</p> <ol> <li>中介（intermediation），指代包含代理、企业防火墙、反向代理和CDN等互联网设备  </li><li>ALPN，Application Layer Protocol Negotiation  </li><li>报头，报文头部，请求报文头部，或响应报文头部 </li></ol> <h3>小结</h3> <p>HTTP/2相比HTTP/1.1，可以做到更有效的充分利用TCP连接，避免了TCP连接的重复的创建（三次握手）、销毁（四次挥手）的过程。</p> <h3>引用</h3> <ol> <li>《HTTP/2 WebRTC all the things !》  </li><li>《Implementing HTTP/2 client in 60 minutes》 </li></ol></div><img src ="http://www.blogjava.net/yongboy/aggbug/423531.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/yongboy/" target="_blank">nieyong</a> 2015-03-17 14:50 <a href="http://www.blogjava.net/yongboy/archive/2015/03/17/423531.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>