﻿<?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-聂永的博客-随笔分类-移动后端</title><link>http://www.blogjava.net/yongboy/category/54840.html</link><description>记录工作/学习的点点滴滴。</description><language>zh-cn</language><lastBuildDate>Thu, 25 Feb 2021 04:44:55 GMT</lastBuildDate><pubDate>Thu, 25 Feb 2021 04:44:55 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>《环信支持千万并发即使通讯的技术要点》阅读摘要</title><link>http://www.blogjava.net/yongboy/archive/2014/10/30/419289.html</link><dc:creator>nieyong</dc:creator><author>nieyong</author><pubDate>Thu, 30 Oct 2014 08:15:00 GMT</pubDate><guid>http://www.blogjava.net/yongboy/archive/2014/10/30/419289.html</guid><wfw:comment>http://www.blogjava.net/yongboy/comments/419289.html</wfw:comment><comments>http://www.blogjava.net/yongboy/archive/2014/10/30/419289.html#Feedback</comments><slash:comments>2</slash:comments><wfw:commentRss>http://www.blogjava.net/yongboy/comments/commentRss/419289.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/yongboy/services/trackbacks/419289.html</trackback:ping><description><![CDATA[<h3><font style="font-weight: bold">零。前言</font></h3> <p>一天早上起来，偶然机会看到《环信支持千万并发即使通讯的技术要点》演示文档，简单翻阅之后，感觉干货很多，于是快速记下以下笔记。  </p><h3><font style="font-weight: bold">一。IM协议和IM Server </font></h3> <p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/870671719_2.png"><img title="870671719" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" alt="870671719" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/870671719_thumb.png" border="0" height="665" width="860" /></a>  </p><p>XMPP确实很传统，WhatsApp选用了，同时经过压缩、精简（比如说user字符串使用u字符替代）处理，让XMPP轻量不少。  </p><p>MQTT，如何实现群组、好友呢，这个是业务层面上事情，大家都订阅某一个主题Topic好了，属于业务拓展。  </p><p>SIP，接触少。  </p><p>微信私有协议ActivitySync，以前在博客上分享过。  </p><p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/881148668_2.png"><img title="881148668" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" alt="881148668" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/881148668_thumb.png" border="0" height="664" width="873" /></a>  </p><p>正确拼写是WhatsApp，呵呵。  </p><p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/881243517_2.png"><img title="881243517" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" alt="881243517" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/881243517_thumb.png" border="0" height="606" width="836" /></a>  </p><p>针对XMPP协议的改进，很有参考价值。  </p><p>心跳单向四个字节，在XMPP协议下，估计应该是极限了吧。在私有协议协议下，一来一往两个字节足够。  </p><p>文件传输方式，这是业界通用方式。  </p><p>移动互联网环境下，用户永远在线，大家的共识。可是取决于手机有没有连接到服务器端，这是无法逾越的障碍。  </p><p>Muc聊天室，业务层面改进。  </p><p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/881482479_2.png"><img title="881482479" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" alt="881482479" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/881482479_thumb.png" border="0" height="615" width="846" /></a>  </p><p>这个针对使用OpenFire的同学，很有参考意义。  </p><p>话说，我以前也安装过OpenFire，定制过在线聊天/咨询的代码。  </p><h3><font style="font-weight: bold">二。移动网络环境下的网络、电量等客户端优化</font></h3> <p><strong></strong><br /><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/868742751_2.png"><img title="868742751" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" alt="868742751" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/868742751_thumb.png" border="0" height="379" width="685" /></a>  </p><p>IM或推送，建立长连接是必须的，可以节省TCP来回创建的开销，但断线之后，是否需要即刻重连，尤其是处于地铁、WIFI边缘地带，可能会造成重连风暴，需要添加稍加延迟连接机制。  </p><p>针对发送的消息的回执，客户端一定要发送回执反馈，否则不知道是否发送成功与否。  </p><p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/868780347_2.png"><img title="868780347" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" alt="868780347" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/868780347_thumb.png" border="0" height="512" width="691" /></a>  </p><p>流量跑马测速，心跳智能，压缩传输。  </p><p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/868832358_2.png"><img title="868832358" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" alt="868832358" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/868832358_thumb.png" border="0" height="515" width="684" /></a>  </p><p>Android手机端优化措施，干货、细节很实用，可以直接拿来用，分享很给力，呵呵！  </p><p>批量、合并数据请求/发送，增量更新，这是一个大家都应该使用的常规节省流量手段，应牢记！  </p><p>难得的是，分享了：  </p><p>2G 文件上传最佳buffer size 1024个字节，3G/4G下直接设置为10K  </p><p>2G 文件下载最佳buffer size 2048个字节，3G/4G下 30K  </p><p>绝对经验的总结，赞！  </p><p>心很细，给出了频繁的属性访问直接声明protected/publish了，不创建新的Java对象只能static类型了。记得很久以前写J2ME程序时，就用过这样的方式。  </p><p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/868936208_2.png"><img title="868936208" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" alt="868936208" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/868936208_thumb.png" border="0" height="432" width="657" /></a>  </p><p>实践中走过多少弯路才总结出来的同步、绘图、渲染页面惊艳，尤其是支持离线方式的数据同步机制，很受用。  </p><h3><strong>三。百万级架构经验分享</strong> </h3> <p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/868995894_2.png"><img title="868995894" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" alt="868995894" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/868995894_thumb.png" border="0" height="364" width="671" /></a>  </p><p>虽然很简单，熟悉TCP/IP协议，这是毫无疑问。  </p><p>无状态协议交互，才能够很容易水平横向扩展，也是当今共识。  </p><p>让所有操作都要尽可能的异步  </p><p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/869119306_2.png"><img title="869119306" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" alt="869119306" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/869119306_thumb.png" border="0" height="966" width="1192" /></a>  </p><p>典型的按照机房进行完整部署，若不能够在DNS层面做到按照用户ID进行指派（HTTP DNS进行接入IP分配），那么就必须处理用户乱入机房的问题，必然要做到一些数据的跨机房的同步，大家都会采用增量式同步方式，跨机房一般常见30毫秒的延迟，批量形式增量同步，那都不叫事。  </p><p>可以看到业务垂直切分，各个业务之间水平扩展。  </p><p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/869214857_2.png"><img title="869214857" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" alt="869214857" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/a80247aad248_DCBB/869214857_thumb.png" border="0" height="901" width="1205" /></a>  </p><p>貌似可以看到单元化CELL/SET架构的影子，但这只是自己的瞎猜而已。  </p><p>PPT上架构图文字细节不甚清晰，再加上自己近视，分辨不太老好的，可以看到消息存储使用了Kafka（和数据库集群之间存定时拉取关系），分布式锁基于Zookeeper，前端LVS做负载均衡，业务非常垂直。  </p><p>在PPT上只看到了两个机房，若用户量级上亿，可能需要扩展到第三个、第三机房，可能跨机房同步的压力就就凸显出来了。  </p><h3><font style="font-weight: bold">四。小结 </font></h3> <p>总之，干货不少，很给力，再次对刘少壮大牛表示感谢！  </p><p>原PPT下载地址：<a href="http://vdisk.weibo.com/s/A0GI9rXObFMd">http://vdisk.weibo.com/s/A0GI9rXObFMd</a>  </p><p>PS: 若有侵权，请及时告知。 </p><img src ="http://www.blogjava.net/yongboy/aggbug/419289.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> 2014-10-30 16:15 <a href="http://www.blogjava.net/yongboy/archive/2014/10/30/419289.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>微信协议简单调研笔记</title><link>http://www.blogjava.net/yongboy/archive/2014/03/05/410636.html</link><dc:creator>nieyong</dc:creator><author>nieyong</author><pubDate>Wed, 05 Mar 2014 06:15:00 GMT</pubDate><guid>http://www.blogjava.net/yongboy/archive/2014/03/05/410636.html</guid><wfw:comment>http://www.blogjava.net/yongboy/comments/410636.html</wfw:comment><comments>http://www.blogjava.net/yongboy/archive/2014/03/05/410636.html#Feedback</comments><slash:comments>22</slash:comments><wfw:commentRss>http://www.blogjava.net/yongboy/comments/commentRss/410636.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/yongboy/services/trackbacks/410636.html</trackback:ping><description><![CDATA[<h3>前言</h3> <p>微信可调研点很多，这里仅仅从协议角度进行调研，会涉及到微信协议交换、消息收发等。所谓&#8220;弱水三千，只取一瓢&#8221;吧。</p> <p>杂七杂八的，有些长，可直接拉到最后看结论好了。</p> <h3>一。微信协议概览</h3> <p>微信传输协议，官方公布甚少，在微信技术总监所透漏PPT《微信之道&#8212;至简》文档中，有所体现。</p> <p>纯个人理解：</p><pre><code>因张小龙做邮箱Foxmail起家，继而又做了QQ Mail等，QQ Mail是国内第一个支持Exchange ActiveSync协议的免费邮箱，基于其从业背景，微信从一开始就采取基于ActiveSync的修改版状态同步协议Sync，也就再自然不过了。
</code></pre>
<p>一句话：增量式、按序、可靠的状态同步传输的微信协议。</p>
<p>大致交换简图如下：</p>
<p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/215cc736c7b0_A31F/Image(9)_2.png"><img title="Image(9)" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" alt="Image(9)" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/215cc736c7b0_A31F/Image(9)_thumb.png" border="0" height="772" width="690" /></a></p>
<p>如何获取新数据呢：</p>
<ol>
<li>服务器端通知，客户端获取 
</li><li>客户端携带最新的SyncKey，发起数据请求 
</li><li>服务器端生成最新的SyncKey连同最新数据发送给客户端 
</li><li>基于版本号机制同步协议，可确保数据增量、有序传输 
</li><li>SyncKey，由服务器端序列号生成器生成，一旦有新消息产生，将会产生最新的SyncKey。类似于版本号 </li></ol>
<p>服务器端通知有状态更新，客户端主动获取自从上次更新之后有变动的状态数据，增量式，顺序式。</p>
<h3>二。微信Web端简单调试</h3>
<p>在线版本微信：</p>
<p><a href="https://webpush.weixin.qq.com/">https://webpush.weixin.qq.com/</a></p>
<p>通过Firefox + Firebug组合调试，也能证实了微信大致通过交换SyncKey方式获取新数据的论述。</p>
<h4>1. 发起GET长连接检测是否存在新的需要同步的数据</h4>
<p>会携带上最新SyncKey</p><pre><code>https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?callback=jQuery18306073923335455973_1393208247730&amp;r=1393209241862&amp;sid=s7c%2FsxpGRSihgZAA&amp;uin=937355&amp;deviceid=e542565508353877&amp;synckey=1_620943725%7C2_620943769%7C3_620943770%7C11_620942796%7C201_1393208420%7C202_1393209127%7C1000_1393203219&amp;_=1393209241865
</code></pre>
<p>返回内容：</p><pre><code> window.synccheck={retcode:"0",selector:"2"}
</code></pre>
<p>selector值大于0，表示有新的消息需要同步。</p>
<p>据目测，心跳周期为27秒左右。</p>
<h4>2. 一旦有新数据，客户端POST请求主动获取同步的数据</h4><pre><code>https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=s7c%2FsxpGRSihgZAA&amp;r=1393208447375
</code></pre>
<p>携带消息体：</p><pre><code>{"BaseRequest":{"Uin":937355,"Sid":"s7c/sxpGRSihgZAA"},"SyncKey":{"Count":6,"List":[{"Key":1,"Val":620943725},{"Key":2,"Val":620943767},{"Key":3,"Val":620943760},{"Key":11,"Val":620942796},{"Key":201,"Val":1393208365},{"Key":1000,"Val":1393203219}]},"rr":1393208447374}
</code></pre>
<p>会携带上最新的SyncKey，会返回复杂结构体JSON内容。</p>
<p>但浏览端收取到消息之后，如何通知服务器端已确认收到了？Web版本微信，没有去做。</p>
<p>在以往使用过程中，曾发现WEB端有丢失消息的现象，但属于偶尔现象。但Android微信客户端（只要登陆连接上来之后）貌似就没有丢失过。</p>
<h4>3. 发送消息流程</h4>
<ol>
<li>
<p>发起一个POST提交，用于提交用户需要发送的消息</p>
<p>https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?sid=lQ95vHR52DiaLVqo&amp;r=1393988414386</p></li></ol>
<p>发送内容：</p><pre><code>{"BaseRequest":{"Uin":937355,"Sid":"lQ95vHR52DiaLVqo","Skey":"A6A1ECC6A7DE59DEFF6A05F226AA334DECBA457887B25BC6","DeviceID":"e937227863752975"},"Msg":{"FromUserName":"yongboy","ToUserName":"hehe057854","Type":1,"Content":"hello","ClientMsgId":1393988414380,"LocalID":1393988414380}}
</code></pre>
<p>相应内容：</p><pre><code>{
"BaseResponse": {
"Ret": 0,
"ErrMsg": ""
}
,
"MsgID": 1020944348,
"LocalID": "1393988414380"
}
</code></pre>
<ol>
<li>
<p>再次发起一个POST请求，用于申请最新SyncKey</p>
<p>https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=lQ95vHR52DiaLVqo&amp;r=1393988414756</p></li></ol>
<p>发送内容：</p><pre><code>{"BaseRequest":{"Uin":937355,"Sid":"lQ95vHR52DiaLVqo"},"SyncKey":{"Count":6,"List":[{"Key":1,"Val":620944310},{"Key":2,"Val":620944346},{"Key":3,"Val":620944344},{"Key":11,"Val":620942796},{"Key":201,"Val":1393988357},{"Key":1000,"Val":1393930108}]},"rr":1393988414756}
</code></pre>
<p>响应的（部分）内容：</p><pre><code>"SKey": "8F8C6A03489E85E9FDF727ACB95C93C2CDCE9FB9532FC15B"  
</code></pre>
<ol>
<li>
<p>终止GET长连接，使用最新SyncKey再次发起一个新的GET长连接</p>
<p>https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?callback=jQuery183024581008965208218<em>1393988305564&amp;r=1393988415015&amp;sid=lQ95vHR52DiaLVqo&amp;uin=937355&amp;deviceid=e937227863752975&amp;synckey=1</em>620944310%7C2<em>620944348%7C3<em>620944344%7C11</em>620942796%7C201</em>1393988357%7C1000<em>1393930108&amp;</em>=1393988415016</p></li></ol>
<h3>三。微信Android简单分析</h3>
<p>Windows桌面端Android虚拟机中运行最新版微信(5.2)，通过tcpdump/Wireshark组合封包分析，以下为分析结果。</p>
<h4>0. 初始连接记录</h4>
<p>简单记录微信启动之后请求：</p><pre><code>11:20:35 dns查询 
dns.weixin.qq.com
返回一组IP地址

11:20:35 DNS查询
long.weixin.qq.com
返回一组IP地址，本次通信中，微信使用了最后一个IP作为TCP长连接的连接地址。

11:20:35
http://dns.weixin.qq.com/cgi-bin/micromsg-bin/newgetdns?uin=0&amp;clientversion=620888113&amp;scene=0&amp;net=1
用于请求服务器获得最优IP路径。服务器通过结算返回一个xml定义了域名:IP对应列表。仔细阅读，可看到微信已经开始了国际化的步伐：香港、加拿大、韩国等。
具体文本，请参考：https://gist.github.com/yongboy/9341884

11:20:35
获取到long.weixin.qq.com最优IP，然后建立到101.227.131.105的TCP长连接

11:21:25
POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/getprofile HTTP/1.1  (application/octet-stream)
返回一个名为&#8220;micromsgresp.dat&#8221;的附件，估计是未阅读的离线消息

11:21:31
POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/whatsnews HTTP/1.1  (application/octet-stream)
大概是资讯、订阅更新等

中间进行一些资源请求等，类似于
GET http://wx.qlogo.cn/mmhead/Q3auHgzwzM7NR4TYFcoNjbxZpfO9aiaE7RU5lXGUw13SMicL6iacWIf2A/96
图片等一些静态资源都会被分配到wx.qlogo.cn域名下面

不明白做什么用途
POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/downloadpackage HTTP/1.1  (application/octet-stream)
输出为micromsgresp.dat文件

11:21:47
GET http://support.weixin.qq.com/cgi-bin/mmsupport-bin/reportdevice?channel=34&amp;deviceid=A952001f7a840c2a&amp;clientversion=620888113&amp;platform=0&amp;lang=zh_CN&amp;installtype=0 HTTP/1.1 
返回chunked分块数据

11:21:49
POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/reportstrategy HTTP/1.1  (application/octet-stream)
</code></pre>
<h4>1. 心跳频率约为5分钟</h4>
<p>上次使用Wireshark分析有误（得出18分钟结论），再次重新分析，心跳频率在5分钟左右。</p>
<h4>2. 登陆之后，会建立一个长连接，端口号为8080</h4>
<p>简单目测为HTTP，初始以为是双通道HTTP，难道是自定义的用于双通道通信的HTTP协议吗，网络上可见资料都是模棱两可、语焉不详。</p>
<p>具体查看长连接初始数据通信，没有发现任何包含"HTTP"字样的数据，以为是微信自定义的TCP/HTTP通信格式。据分析，用于可能用于获取数据、心跳交换消息等用途吧。这个后面会详谈微信是如何做到的。</p>
<h5>2.0 初始消息传输</h5>
<p>个人资料、离线未阅读消息部分等通过 POST HTTP短连接单独获取。</p>
<h5>2.1 二进制简单分析</h5>
<p>抽取微信某次HTTP协议方式通信数据，16进制表示，每两个靠近的数字为一个byte字节：</p>
<p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/215cc736c7b0_A31F/2014-03-03_15h07_30_2.png"><img title="2014-03-03_15h07_30" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" alt="2014-03-03_15h07_30" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/215cc736c7b0_A31F/2014-03-03_15h07_30_thumb.png" border="0" height="342" width="625" /></a></p>
<p>微信协议可能如下：</p><pre><code>一个消息包 = 消息头 + 消息体
</code></pre>
<p>消息头固定16字节长度，消息包长度定义在消息头前4个字节中。</p>
<p>单纯摘取第0000行为例，共16个字节的头部:</p><pre><code>00 00 00 10 00 10 00 01 00 00 00 06 00 00 00 0f
</code></pre>
<p>16进制表示，每两个紧挨着数字代表一个byte字节。</p>
<p>微信消息包格式： 1. 前4字节表示数据包长度，可变 值为16时，意味着一个仅仅包含头部的完整的数据包（可能表示着预先定义好的业务意义），后面可能还有会别的消息包 2. 2个字节表示头部长度，固定值，0x10 = 16 3. 2个字节表示谢意版本，固定值，0x01 = 1 4. 4个字节操作说明数字，可变 5. 序列号，可变 6. 头部后面紧跟着消息体，非明文，加密形式 7. 一个消息包，最小16 byte字节</p>
<p>通过上图（以及其它数据多次采样）分析：</p>
<ol>
<li>0000 - 0040为单独的数据包 
</li><li>0050行为下一个数据包的头部，前四个字节值为0xca = 202，表示包含了从0050-0110共202个字节数据 
</li><li>一次数据发送，可能包含若干子数据包 
</li><li>换行符\n，16进制表示为0x0a，在00f0行，包含了两个换行符号 
</li><li>一个数据体换行符号用于更细粒度的业务数据分割 是否蒙对，需要问问做微信协议的同学 
</li><li>所有被标记为HTTP协议通信所发送数据都包含换行符号 </li></ol>
<h5>2.2 动手试试猜想，模拟微信TCP长连接</h5>
<p>开始很不解为什么会出现如此怪异的HTTP双通道长连接请求，难道基于TCP通信，然后做了一些手脚？很常规的TCP长连接，传输数据时(不是所有数据传输)，被wireshark误认为HTTP长连接。这个需要做一个实验证实一下自己想法，设想如下：</p>
<p>写一个Ping-Pong客户端、服务器端程序，然后使用Wireshark看一下结果，是否符合判断。</p>
<p>Java版本的请求端，默认请求8080端口： <script src="https://gist.github.com/yongboy/9319660.js"></script></p>
<p>C语言版本的服务器程序，收到什么发送什么，没有任何逻辑，默认绑定8080端口： <script src="https://gist.github.com/yongboy/9341037.js"></script></p>
<p>这里有一个现场图：</p>
<p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/215cc736c7b0_A31F/2014-03-03_14h53_19_2.png"><img title="2014-03-03_14h53_19" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" alt="2014-03-03_14h53_19" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/215cc736c7b0_A31F/2014-03-03_14h53_19_thumb.png" border="0" height="702" width="1052" /></a></p>
<p>可以尝试稍微改变输出内容，去除换行符&#8220;\n&#8221;，把端口换成9000，试试看，就会发现Wireshark输出不同的结果来。</p>
<h5>2.3 结论是什么呢？</h5>
<p>若使用原始TCP进行双向通信，则需要满足以下条件，可以被类似于Wireshark协议拦截器误认为是HTTP长连接：</p>
<ol>
<li>使用80/8080端口（81/3128/8000经测试无效） 也许8080一般被作为WEB代理服务端口，微信才会享用这个红利吧。 
</li><li>输出的内容中，一定要包含换行字符"\n" </li></ol>
<p>因此，可以定性为微信使用了基于8080端口TCP长连接，一旦数据包中含有换行"\n"符号，就会被Wireshark误认为HTTP协议。可能微信是无心为之吧。</p>
<h4>3. 新消息获取方式</h4>
<ol>
<li>TCP长连接接收到服务器通知有新消息需要获取 
</li><li>APP发起一个HTTP POST请求获取新状态消息，会带上当前SyncKey 地址为：http://short.weixin.qq.com/cgi-bin/micromsg-bin/reportstrategy HTTP/1.1，看不到明文 
</li><li>APP获取到新的消息，会再次发起一次HTTP POST请求，告诉服务器已确认收到，同时获取最新SyncKey 地址为：http://short.weixin.qq.com/cgi-bin/micromsg-bin/kvreport，看不到明文 
</li><li>接受一个消息，TCP长连接至少交互两次，客户端发起两次HTTP POST请求 
<br />具体每次交互内容是什么，有些模糊<br /></li><li>服务器需要支持：状态消息获取标记，状态消息确认收取标记。只有被确认收到，此状态消息才算是被正确消费掉 
</li><li>多个不同设备同一账号同时使用微信，同一个状态消息会会被同时分发到多个设备上 </li></ol>
<p>此时消息请求截图如下：</p>
<p><a href="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/215cc736c7b0_A31F/2014-03-03_15h58_15_2.png"><img title="2014-03-03_15h58_15" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" alt="2014-03-03_15h58_15" src="http://www.blogjava.net/images/blogjava_net/yongboy/Windows-Live-Writer/215cc736c7b0_A31F/2014-03-03_15h58_15_thumb.png" border="0" height="371" width="1422" /></a></p>
<h4>4. 发送消息方式</h4>
<p>发送消息走已经建立的TCP长连接通道，发送消息到服务器，然后接受确认信息等，产生一次交互。</p>
<p>小伙伴接收到信息阅读也都会收到服务器端通知，产生一次交互等。</p><!--![](wg/2014-03-03_16h19_14.png)-->
<p>可以确定，微信发送消息走TCP长连接方式，因为不对自身状态数据产生影响，应该不交换SyncKey。</p>
<ul>
<li>在低速网络下，大概会看到消息发送中的提示，属于消息重发机制 
</li><li>网络不好有时客户端会出现发送失败的红色感叹号 
</li><li>已发送到服务器但未收到确认的消息，客户端显示红色感叹号，再次重发，服务器作为重复消息处理，反馈确认</li><li>上传图片，会根据图片大小，分割成若干部分（大概1.5K被划分为一部分），同一时间点，客户端会发起若干次POST请求，各自上传成功之后，服务器大概会合并成一个完整图片，返回一个缩略图，显示在APP聊天窗口内。APP作为常规的文字消息发送到服务器端</li><li>上传音频，则单独走TCP通道，一个两秒的录制音频，客户端录制完毕，分为两块传输，一块最大1.5K左右，服务端响应一条数据通知确认收到。共三次数据传输。<br />音频和纯文字信息一致，都是走TCP长连接，客户端发送，服务器端确认。</li></ul>
<h3>四。微信协议小结</h3>
<ol>
<li>发布的消息对应一个ID（只要单个方向唯一即可，服务器端可能会根ID判断重复接收），消息重传机制确保有限次的重试，重试失败给予用户提示，发送成功会反馈确认，客户端只有收到确认信息才知道发送成功。发送消息可能不会产生新SyncKey。 
</li><li>基于版本号（SynKey）的状态消息同步机制，增量、有序传输需求水到渠成。长连接通知/短连接获取、确认等，交互方式简单，确保了消息可靠谱、准确无误到达。 
</li><li>客户端/服务器端都会存储消息ID处理记录，避免被重复消费客户端获取最新消息，但未确认，服务器端不会认为该消息被消费掉。下次客户端会重新获取，会查询当前消息是否被处理过。根据一些现象猜测。 
</li><li>总体上看，微信协议跨平台（TCP或HTPP都可呈现，处理方式可统一），通过&#8220;握手&#8221;同步，很可靠，无论哪一个平台都可以支持的很好 
</li><li>微信协议最小成本为16字节，大部分时间若干个消息包和在一起，批量传输。微信协议说不上最简洁，也不是最节省流量，但是非常成功的。 </li><li><div>若服务器检测到一些不确定因素，可能会导致微启用安全套接层SSL协议进行常规的TCP长连接传输。短连接都没有发生变化</div><br /></li></ol>
<p>以上，根据有限资料和数据拦截观察总结得出，啰啰嗦嗦，勉强凑成一篇，会存在一些不正确之处，欢迎给予纠正。在多次</p>
<h3>五。附录</h3>
<p>Microsoft Exchange Active Sync协议，简称EAS，分为folderrsync(同步文件夹目录，即邮箱内有哪几个文件夹)和sync（每个文件夹内有哪些文档）两部分。</p>
<p>某网友总结的协议一次回话大致示范：</p><pre><code>Client:   synckey=0 //第一次key为0
Server:  newsynckey=1235434    //第一次返回新key
Client:   synckey=1235434   //使用新key查询
Server:  newsynckey=1647645,data=*****//第一次查询，得到新key和数据
Client:   synckey=1647645
Server:  newsynckey=5637535,data=null //第二次查询，无新消息
Client:   synckey=5637535
Server: newsynckey=8654542, data=****//第三次查询，增量同步
</code></pre>
<ul>
<li>上页中的相邻请求都是隔固定时间的，如两分钟 
</li><li>客户端每次使用旧key标记自己的状态，服务端每次将新key和增量数据一起返回。 
</li><li>key是递增的，但不要求连续 
</li><li>请求的某个参数决定服务器是否立即返回 </li></ul><img src ="http://www.blogjava.net/yongboy/aggbug/410636.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> 2014-03-05 14:15 <a href="http://www.blogjava.net/yongboy/archive/2014/03/05/410636.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>