maemual

maemual 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

爱Google,爱喵。

个人动态

maemual 赞了文章 · 2016-09-10

HTTP 缓存的四种风味与缓存策略

本文从属于笔者的HTTP 理解与实践系列文章,对于HTTP的学习主要包含HTTP 基础HTTP 请求头与请求体HTTP 响应头与状态码HTTP 缓存这四个部分,而对于HTTP相关的扩展与引申,我们还需要了解HTTPS 理解与实践 HTTP/2 基础WebSocket 基础这些部分。本部分知识点同时也归纳于笔者的我的校招准备之路:从Web前端到服务端应用架构这篇综述。

HTTP Cache

通过网络获取内容既缓慢,成本又高:大的响应需要在客户端和服务器之间进行多次往返通信,这拖延了浏览器可以使用和处理内容的时间,同时也增加了访问者的数据成本。因此,缓存和重用以前获取的资源的能力成为优化性能很关键的一个方面。每个浏览器都实现了 HTTP 缓存! 我们所要做的就是,确保每个服务器响应都提供正确的 HTTP 头指令,以指导浏览器何时可以缓存响应以及可以缓存多久。服务器在返回响应时,还会发出一组 HTTP 头,用来描述内容类型、长度、缓存指令、验证令牌等。例如,在下图的交互中,服务器返回了一个 1024 字节的响应,指导客户端缓存响应长达 120 秒,并提供验证令牌(x234dff),在响应过期之后,可以用来验证资源是否被修改。

我们打开百度首页,可以看下百度的HTTP缓存的实现:

发现对于静态资源的访问都是返回的200状态码。

头部优势和特点劣势和问题
Expires1、HTTP 1.0 产物,可以在HTTP 1.0和1.1中使用,简单易用。2、以时刻标识失效时间。1、时间是由服务器发送的(UTC),如果服务器时间和客户端时间存在不一致,可能会出现问题。2、存在版本问题,到期之前的修改客户端是不可知的。
Cache-Control1、HTTP 1.1 产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题。2、比Expires多了很多选项设置。1、HTTP 1.1 才有的内容,不适用于HTTP 1.0 。2、存在版本问题,到期之前的修改客户端是不可知的。
Last-Modified1、不存在版本问题,每次请求都会去服务器进行校验。服务器对比最后修改时间如果相同则返回304,不同返回200以及资源内容。1、只要资源修改,无论内容是否发生实质性的变化,都会将该资源返回客户端。例如周期性重写,这种情况下该资源包含的数据实际上一样的。2、以时刻作为标识,无法识别一秒内进行多次修改的情况。3、某些服务器不能精确的得到文件的最后修改时间。
ETag1、可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况。2、不存在版本问题,每次请求都回去服务器进行校验。1、计算ETag值需要性能损耗。2、分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时发现ETag不匹配的情况。

Header

HTTP报文头部中与缓存相关的字段为:

1. 通用首部字段(就是请求报文和响应报文都能用上的字段)

2. 请求首部字段

3. 响应首部字段

4. 实体首部字段

Reference

HTTP 1.0: 基于Pragma&Expires的缓存实现

在 http1.0 时代,给客户端设定缓存方式可通过两个字段——“Pragma”和“Expires”来规范。虽然这两个字段早可抛弃,但为了做http协议的向下兼容,你还是可以看到很多网站依旧会带上这两个字段。

Pragma

当该字段值为“no-cache”的时候(事实上现在RFC中也仅标明该可选值),会知会客户端不要对该资源读缓存,即每次都得向服务器发一次请求才行。Pragma属于通用首部字段,在客户端上使用时,常规要求我们往html上加上这段meta元标签(仅对该页面有效,对页面上的资源无效):

<meta http-equiv="Pragma" content="no-cache">

它告诉浏览器每次请求页面时都不要读缓存,都得往服务器发一次请求才行。不过这种限制行为在客户端作用有限:

  1. 仅有IE才能识别这段meta标签含义,其它主流浏览器仅能识别“Cache-Control: no-store”的meta标签。

  2. 在IE中识别到该meta标签含义,并不一定会在请求字段加上Pragma,但的确会让当前页面每次都发新请求(仅限页面,页面上的资源则不受影响)。

另外,需要知道的是,Pragma的优先级是高于Cache-Control 的。譬如在下图这个例子中,我们使用Fiddler为图片资源额外增加以下头部信息:

前者用来设定缓存资源一天,后者禁用缓存,重新访问该页面会发现访问该资源会重新发起一次请求。

Expire

有了Pragma来禁用缓存,自然也需要有个东西来启用缓存和定义缓存时间,对http1.0而言,Expires就是做这件事的首部字段。Expires的值对应一个GMT(格林尼治时间),比如“Mon, 22 Jul 2002 11:12:01 GMT”来告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求。在客户端我们同样可以使用meta标签来知会IE(也仅有IE能识别)页面(同样也只对页面有效,对页面上的资源无效)缓存时间:

<meta http-equiv="expires" content="mon, 18 apr 2016 14:30:00 GMT">

如果希望在IE下页面不走缓存,希望每次刷新页面都能发新请求,那么可以把“content”里的值写为“-1”或“0”。注意的是该方式仅仅作为知会IE缓存时间的标记,你并不能在请求或响应报文中找到Expires字段。如果是在服务端报头返回Expires字段,则在任何浏览器中都能正确设置资源缓存的时间。

需要注意的是,响应报文中Expires所定义的缓存时间是相对服务器上的时间而言的,其定义的是资源“失效时刻”,如果客户端上的时间跟服务器上的时间不一致(特别是用户修改了自己电脑的系统时间),那缓存时间可能就没啥意义了。

HTTP 1.1 Cache-Control:相对过期时间

针对上述的“Expires时间是相对服务器而言,无法保证和客户端时间统一”的问题,http1.1新增了 Cache-Control 来定义缓存过期时间,若报文中同时出现了Expires 和 Cache-Control,会以 Cache-Control 为准。换言之,这三者的优先级顺序为:Pragma -> Cache-Control -> Expires。Cache-Control也是一个通用首部字段,这意味着它能分别在请求报文和响应报文中使用。在RFC中规范了 Cache-Control 的格式为:

"Cache-Control" ":" cache-directive

作为请求首部时,cache-directive 的可选值有:

作为响应首部时,cache-directive 的可选值有:

另外 Cache-Control 允许自由组合可选值,例如:

Cache-Control: max-age=3600, must-revalidate

它意味着该资源是从原服务器上取得的,且其缓存(新鲜度)的有效时间为一小时,在后续一小时内,用户重新访问该资源则无须发送请求。当然这种组合的方式也会有些限制,比如 no-cache 就不能和 max-age、min-fresh、max-stale 一起搭配使用。组合的形式还能做一些浏览器行为不一致的兼容处理。例如在IE我们可以使用 no-cache 来防止点击“后退”按钮时页面资源从缓存加载,但在 Firefox 中,需要使用 no-store 才能防止历史回退时浏览器不从缓存中去读取数据,故我们在响应报头加上如下组合值即可做兼容处理:

Cache-Control: no-cache, no-store

HTTP 1.1 缓存校验

上述的首部字段均能让客户端决定是否向服务器发送请求,比如设置的缓存时间未过期,那么自然直接从本地缓存取数据即可(在chrome下表现为200 from cache),若缓存时间过期了或资源不该直接走缓存,则会发请求到服务器去。我们现在要说的问题是,如果客户端向服务器发了请求,那么是否意味着一定要读取回该资源的整个实体内容呢?我们试着这么想——客户端上某个资源保存的缓存时间过期了,但这时候其实服务器并没有更新过这个资源,如果这个资源数据量很大,客户端要求服务器再把这个东西重新发一遍过来,是否非常浪费带宽和时间呢?答案是肯定的,那么是否有办法让服务器知道客户端现在存有的缓存文件,其实跟自己所有的文件是一致的,然后直接告诉客户端说“这东西你直接用缓存里的就可以了,我这边没更新过呢,就不再传一次过去了”。为了让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,Http1.1新增了几个首部字段来做这件事情。

Last-Modified:根据最后修改时间匹配

服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端。客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回304状态码即可。至于传递标记起来的最终修改时间的请求报文首部字段一共有两个:

1. If-Modified-Since: Last-Modified-value

示例为  If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT

该请求首部告诉服务器如果客户端传来的最后修改时间与服务器上的一致,则直接回送304 和响应报头即可。当前各浏览器均是使用的该请求首部来向服务器传递保存的 Last-Modified 值。

2. If-Unmodified-Since: Last-Modified-value

告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应当返回412(Precondition Failed) 状态码给客户端。

当遇到下面情况时,If-Unmodified-Since 字段会被忽略:

1. Last-Modified值对上了(资源在服务端没有新的修改);
2. 服务端需返回2XX和412之外的状态码;
3. 传来的指定日期不合法

Last-Modified 说好却也不是特别好,因为如果在服务器上,一个资源被修改了,但其实际内容根本没发送改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)

ETag:根据资源标识符匹配

为了解决上述Last-Modified可能存在的不准确的问题,Http1.1还推出了 ETag 实体首部字段。服务器会通过某种算法,给资源计算得出一个唯一标志符(比如md5标志),在把资源响应给客户端的时候,会在实体首部加上“ETag: 唯一标识符”一起返回给客户端。客户端会保留该 ETag 字段,并在下一次请求时将其一并带过去给服务器。服务器只需要比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。

那么客户端是如何把标记在资源上的 ETag 传去给服务器的呢?请求报文中有两个首部字段可以带上 ETag 值:

1. If-None-Match: ETag-value

示例为  If-None-Match: "56fcccc8-1699"

告诉服务端如果 ETag 没匹配上需要重发资源数据,否则直接回送304和响应报头即可。当前各浏览器均是使用的该请求首部来向服务器传递保存的 ETag 值。

2. If-Match: ETag-value

告诉服务器如果没有匹配到ETag,或者收到了“*”值而当前并没有该资源实体,则应当返回412(Precondition Failed) 状态码给客户端。否则服务器直接忽略该字段。If-Match 的一个应用场景是,客户端走PUT方法向服务端请求上传/更替资源,这时候可以通过 If-Match 传递资源的ETag。

需要注意的是,如果资源是走分布式服务器(比如CDN)存储的情况,需要这些服务器上计算ETag唯一值的算法保持一致,才不会导致明明同一个文件,在服务器A和服务器B上生成的ETag却不一样。

如果 Last-Modified 和 ETag 同时被使用,则要求它们的验证都必须通过才会返回304,若其中某个验证没通过,则服务器会按常规返回资源实体及200状态码。

在较新的 nginx 上默认是同时开启了这两个功能的:

上图的前三条请求是原始请求,接着的三条请求是刷新页面后的新请求,在发新请求之前我们修改了 reset.css 文件,所以它的 Last-Modified 和 ETag 均发生了改变,服务器因此返回了新的文件给客户端(状态值为200)

而 dog.jpg 我们没有做修改,其Last-Modified 和 ETag在服务端是保持不变的,故服务器直接返回了304状态码让客户端直接使用缓存的 dog.jpg 即可,没有把实体内容返回给客户端(因为没必要)

缓存策略

按照上面的决策树来确定您的应用使用的特定资源或一组资源的最优缓存策略。理想情况下,目标应该是在客户端上缓存尽可能多的响应、缓存尽可能长的时间,并且为每个响应提供验证令牌,以便进行高效的重新验证。

Cache-Control 指令说明
max-age=86400浏览器和任何中继缓存均可以将响应(如果是public的)缓存长达一天(60 秒 x 60 分 x 24 小时)
private, max-age=600客户端浏览器只能将响应缓存最长 10 分钟(60 秒 x 10 分)
no-store不允许缓存响应,每个请求必须获取完整的响应。

根据 HTTP Archive,在排名最高的 300,000 个网站中(Alexa 排名),所有下载的响应中,几乎有半数可以由浏览器进行缓存,对于重复性网页浏览和访问来说,这是一个巨大的节省! 当然,这并不意味着特定的应用会有 50% 的资源可以被缓存:有些网站可以缓存 90% 以上的资源, 而有些网站有许多私密的或者时间要求苛刻的数据,根本无法被缓存。
当我们在一个项目上做http缓存的应用时,我们还是会把上述提及的大多数首部字段均使用上,例如使用 Expires 来兼容旧的浏览器,使用 Cache-Control 来更精准地利用缓存,然后开启 ETag 跟 Last-Modified 功能进一步复用缓存减少流量。

那么这里会有一个小问题——Expires 和 Cache-Control 的值应设置为多少合适呢?

答案是不会有过于精准的值,均需要进行按需评估。

例如页面链接的请求常规是无须做长时间缓存的,从而保证回退到页面时能重新发出请求,百度首页是用的 Cache-Control:private,腾讯首页则是设定了60秒的缓存,即 Cache-Control:max-age=60。

而静态资源部分,特别是图片资源,通常会设定一个较长的缓存时间,而且这个时间最好是可以在客户端灵活修改的。以腾讯的某张图片为例:

http://i.gtimg.cn/vipstyle/vipportal/v4/img/common/logo.png?max_age=2592000

客户端可以通过给图片加上“max_age”的参数来定义服务器返回的缓存时间:

废弃和更新已缓存的响应

浏览器发出的所有 HTTP 请求会首先被路由到浏览器的缓存,以查看是否缓存了可以用于实现请求的有效响应。如果有匹配的响应,会直接从缓存中读取响应,这样就避免了网络延迟以及传输产生的数据成本。然而,如果我们希望更新或废弃已缓存的响应,该怎么办?

例如,假设我们已经告诉访问者某个 CSS 样式表缓存长达 24 小时 (max-age=86400),但是设计人员刚刚提交了一个更新,我们希望所有用户都能使用。我们该如何通知所有访问者缓存的 CSS 副本已过时,需要更新缓存? 这是一个欺骗性的问题 - 实际上,至少在不更改资源网址的情况下,我们做不到。

一旦浏览器缓存了响应,在过期以前,将一直使用缓存的版本,这是由 max-age 或者 expires 指定的,或者直到因为某些原因从缓存中删除,例如用户清除了浏览器缓存。因此,在构建网页时,不同的用户可能使用的是文件的不同版本;刚获取该资源的用户将使用新版本,而缓存过之前副本(但是依然有效)的用户将继续使用旧版本的响应。

所以,我们如何才能鱼和熊掌兼得:客户端缓存和快速更新? 很简单,在资源内容更改时,我们可以更改资源的网址,强制用户下载新响应。通常情况下,可以通过在文件名中嵌入文件的指纹码(或版本号)来实现 - 例如 style.x234dff.css。

当然这需要有一个前提——静态资源能确保长时间不做改动。如果一个脚本文件响应给客户端并做了长时间的缓存,而服务端在近期修改了该文件的话,缓存了此脚本的客户端将无法及时获得新的数据。

解决该困扰的办法也简单——把服务侧ETag的那一套也搬到前端来用——页面的静态资源以版本形式发布,常用的方法是在文件名或参数带上一串md5或时间标记符:

https://hm.baidu.com/hm.js?e23800c454aa573c0ccb16b52665ac26
http://tb1.bdstatic.com/tb/_/tbean_safe_ajax_94e7ca2.js
http://img1.gtimg.com/ninja/2/2016/04/ninja145972803357449.jpg

如果文件被修改了,才更改其标记符内容,这样能确保客户端能及时从服务器收取到新修改的文件。

因为能够定义每个资源的缓存策略,所以,我们可以定义’缓存层级’,这样,不但可以控制每个响应的缓存时间,还可以控制访问者看到新版本的速度。例如,我们一起分析一下上面的例子:

  • HTML 被标记成no-cache,这意味着浏览器在每次请求时都会重新验证文档,如果内容更改,会获取最新版本。同时,在 HTML 标记中,我们在 CSS 和 JavaScript 资源的网址中嵌入指纹码:如果这些文件的内容更改,网页的 HTML 也会随之更改,并将下载 HTML 响应的新副本。

  • 允许浏览器和中继缓存(例如 CDN)缓存 CSS,过期时间设置为 1 年。注意,我们可以放心地使用 1 年的’远期过期’,因为我们在文件名中嵌入了文件指纹码:如果 CSS 更新,网址也会随之更改。

  • JavaScript 过期时间也设置为 1 年,但是被标记为 private,也许是因为包含了 CDN 不应缓存的一些用户私人数据。

  • 缓存图片时不包含版本或唯一指纹码,过期时间设置为 1 天。

缓存检查表

不存在最佳的缓存策略。根据您的通信模式、提供的数据类型以及应用特定的数据更新要求,必须定义和配置每个资源最适合的设置以及整体的’缓存层级’。

在定义缓存策略时,要记住下列技巧和方法:

  1. 使用一致的网址:如果您在不同的网址上提供相同的内容,将会多次获取和存储该内容。提示:注意,网址区分大小写

  2. 确保服务器提供验证令牌 (ETag):通过验证令牌,如果服务器上的资源未被更改,就不必传输相同的字节。

  3. 确定中继缓存可以缓存哪些资源:对所有用户的响应完全相同的资源很适合由 CDN 或其他中继缓存进行缓存。

  4. 确定每个资源的最优缓存周期:不同的资源可能有不同的更新要求。审查并确定每个资源适合的 max-age。

  5. 确定网站的最佳缓存层级:对 HTML 文档组合使用包含内容指纹码的资源网址以及短时间或 no-cache 的生命周期,可以控制客户端获取更新的速度。

  6. 搅动最小化:有些资源的更新比其他资源频繁。如果资源的特定部分(例如 JavaScript 函数或一组 CSS 样式)会经常更新,应考虑将其代码作为单独的文件提供。这样,每次获取更新时,剩余内容(例如不会频繁更新的库代码)可以从缓存中获取,确保下载的内容量最少。

查看原文

赞 17 收藏 91 评论 2

maemual 赞了文章 · 2016-06-20

鼠须管输入法 傻瓜版配置 - 基于 rime_pro 增强包

简要说明

安装步骤:
第一步 下载并安装鼠须管输入法
第二步 备份现有的配置,打开终端输入 cp -a ~/Library/Rime ~/Library/Rime_ori_$(date +%Y%m%d%H%M%S) 会得到一个类似 ~/Library/Rime_ori_20170501225630 的文件夹 即为备份
第三步 下载并解压出 rime_pro 增强包 里的文件复制到 ~/Library/Rime 文件夹 覆盖默认文件
第四步 切换到鼠须管输入法,重新部署(约1分钟)

软件介绍:
以下是软件介绍。说明了鼠须管输入法的特点、rime_pro 增强包的作用和效果。rime_pro 增强包是鼠须管输入法的第三方增强包,仅由我业余爱好制作,和原版软件作者无关。如果想要卸载本增强包,那么把刚刚备份的 Rime_ori_20170501225630 文件夹 重命名为 Rime 即可。

鼠须管输入法 - 一个 OS X 平台上的开源输入法软件

鼠须管输入法是一个 OS X 平台的输入法软件,以功能强大和配置专业而著称,带来了极佳的文字输入体验,为众多用户推崇。本文是一个“傻瓜版”的配置办法 / 教程,旨在用简单的办法实现(准确地说是配置出)强大的功能,并保持自定义词库的可扩展性。

rime_pro 增强包,不折腾

鼠须管输入法是一个优秀的输入法软件,秉承开源软件精神,不仅支持一般输入法软件的除了云同步之外的所有功能,还支持强大的自定义配置。

鼠须管输入法不具备云同步词库功能,同时也不会上传你的文字输入历史,是完全断网操作的软件。

鼠须管输入法官方默认配置,实现了常用的最基础功能。通常在使用之前,需要(必须)自己动手(修改yaml格式的程序和输入法方案配置文件)自定义一些配置,把鼠须管配得更 “顺手”。这需要一点点代码知识。

rime_pro 增强包,让你在无需代码知识的情况下,也可快速上手使用鼠须管输入法。

rime_pro 增强包(非官方) 是鼠须管输入法软件的一个 预制的配置,已配好常用功能,在常用功能上完全可替代其他品牌输入法 (如搜狗输入法、百度输入法)。

安完直接用,不用再配置(所谓傻瓜版,也即不需要代码知识),即下载后可直接使用,免去逐条逐项自定义配置的繁琐和出错;也可作为后续自定义配置软件的基础 (自定义配置,请参考压缩包里的 f-customize.txt 一份简明的配置说明 )。

鼠须管输入法:

  • 开源软件

  • 一切输入法相关配置均可自定义

  • 记忆本机输入习惯作为词库积累

  • 支持词库的导入,实现词语联想

  • 支持输入法基本配置:候选词个数、候选词方向、中英切换快捷键、翻页快捷键、面板配色方案 / 皮肤、是否开启内嵌输入、拼音与五笔切换等

  • 支持双拼 (小鹤双拼)、五笔

  • 支持繁體輸入,更智能的繁體輸入

  • 支持方言语系输入(参考一参考二)

  • 支持自定义标点符号输入习惯,可启用「经典中文标点」

  • 支持更多功能:开启 emoji 和符号快捷输入、静默模式等功能,支持词语快捷输入(自动拼写更正、词语联想、中英混输)、支持添加个性词组、自定义配色方案等

  • 更多专业细节配置,是本软件强大所在,也是自定义配置的基础,见官方帮助文档

  • 配置方法:直接修改对应的 yaml 格式配置文件

  • 输入法中的输入法:配置完毕后,在常用功能上它丝毫不弱于百度输入法、搜狗输入法等;在隐私问题上它的开源保障了其安全性可靠性;在特殊输入需求方面(如经典中文标点、自定义短语、粤语输入、古音输入、输入码反查等),它会带给你惊喜

rime_pro 增强包(非官方):

  • 一份预设配置,常用功能已默认开启并配置好

  • 预置词库,强大的常用词联想

  • 自带 emoji ? + 常见符号输入 ⌘

  • 静默模式:输入法在某些应用下,默认用英文 (终端、Spotlight、Xcode等)

  • 安装简单:解压增强包,拷贝到 Rime 配置目录覆盖即可;拷贝完毕,即刻开用

  • 傻瓜版:安完直接用,不必再配置;省略配逐项置的繁琐,不需任何代码知识,need not touch any code

  • 熟悉的标点符号输入习惯 (和其他输入法软件默认的标点符号规则一致)
    (以上默认配置可自己改,改动位置和办法见 f-customize.txt )

  • 更多功能:缩写补全、自动更正拼写、个性短语 (见下文测试)

  • 即开即用:默认候选词 5 个,候选词横向,中英切换 shift,翻页 -+ ,不启用内嵌输入

后续高级自定义配置 (when really need):

  • 个性短语,自定义添加

  • 后续添加自定义短语 (个性短语):可扩展的词库,可以方便地增加自己的自定义词组;主要添加在 f_myphrases.dict.yamlf_mysecretphrases.dict.yaml 这2个文件

  • 所有软件功能和个人使用习惯,均可自定义配置

  • 后续逐项基础配置:候选词个数、翻页快捷键、面板配色方案 / 皮肤、是否开启内嵌输入、静默模式所应用于的软件、标点符号输入习惯等功能,均可自己修改,见 f-customize.txt 可作参考

  • 后续逐项高级配置:增强包压缩文件包括词库文件、已经配置好的鼠须管配置文件;如想更改,可在此基础上自己动手;主要修改 default.custom.yamlsquirrel.custom.yaml 这2个文件即可 (这2个文件会直接覆盖具体输入法方案如luna_pinyin_simp.custom.yaml内的配置)

( 配置细节:如有需求,可以参考配置文件里的注释和配置说明来自行修改并重新部署,尝试更多的自定义。参考文章附后 )

安装和卸载

第一步 安装鼠须管软件
第二步 解压出 rime_pro 增强包 里的文件扔 ~/Library/Rime 覆盖默认文件
第三步 重新部署(约1分钟) all done ?

1.下载鼠须管并安装

官方网站 http://rime.im/download/
双击pkg安装包,在安装的最后一步,需要电脑注销一次

2. 备份鼠须管初始配置

鼠须管配置目录 ~/Library/Rime
cp -a ~/Library/Rime ~/Library/Rime_ori_$(date +%Y%m%d%H%M%S)
会得到一个类似 ~/Library/Rime_ori_20170501225630 的文件夹 即为备份

3.软件增强包

rime_pro 增强包(非官方)
下载链接、重新部署

使用办法:
下载软件增强包,并把里面的东西拷贝到 ~/Library/Rime ,覆盖即可。然后重新部署

说明:
rime_pro 增强包(非官方),包括了词库文件和软件配置文件。理论上对各个平台的 Rime输入法是通用的。
对于 ~/Library/Rime 下的配置文件,重新部署后生效

词库说明:增强包里的词库,是鼠须管原生词库的 *.dict.yaml 格式的词库,收集自网络(参考词库附后 —— 如果更好的词库分享,请给本文留言)。采用鼠须管原生词库,而非基于其他输入法的细胞词库转制出的超大词库,所以基本不会卡顿或拖慢软件速度

4.重新部署

点击屏幕右上处的输入法小图标,选用鼠须管输入法✓
点击鼠须管输入法图标,选择 重新部署 (约1分钟)
选择简体中文输入:ctrl+` 并选择 《朙月拼音·简化字》方案

现在,你已经用上 OS X 平台最棒的中文输入法了,而且输入法常用功能中的 99% 已悉听尊便 ?

5.卸载办法

如何干净地卸载鼠须管输入法?
say goodbye Squirrel && killall Squirrel
系统偏好设置 - 键盘 - 输入源 - 鼠须管,移除
sudo rm -rf "/Library/Input Methods/Squirrel.app"
rm -rf ~/Library/Rime

测试

让我们来看看 (基于 rime_pro 增强包的) 鼠须管输入法能做什么?

测试1

简体中文输入:ctrl+` 并选择 《朙月拼音·简化字》方案
繁体中文输入:ctrl+` 并选择 《朙月拼音·臺灣正體》方案

测试2 emoji

huojian => ?
chuzuche => ? ?
jiong => ??
siyecao => ?
syc => ?

测试3 符号输入

duigou => ✓
sheshidu => ℃
cmd => ⌘
shift => ⇧
opt => ⌥

测试4 自定义词组 (词库、句库、词组记忆、词组联想)

yikesaiting => 亦可赛艇
modalademaliya => 抹大拉的玛丽亚

测试5 快捷输入 (自动更正拼写、短语)

rime => Rime
html => HTML
nba => NBA
wiki => Wiki
osx => OS X

测试6 快捷输入 (缩写补全)

md => Markdown
gsw => Golden State Warriors
ror => Ruby on Rails

测试7 自定义短语 (个性短语)

gmail => example@gmail.com

见 ~/Library/Rime/f_mysecretphrases.dict.yaml ,可替换为你自己的邮箱,作为个性短语实现快捷输入。注意事项见后

测试8 在特定软件里 默认使用英文输入 (静默模式)

文本文档,输入默认是中文;Spotlight 或 终端,输入默认是英文
添加静默模式应用于的软件,见 squirrel.custom.yaml

其他自定义

面板配色方案 / 皮肤 (见 squirrel.custom.yaml)
标点符号输入习惯 (见 default.custom.yaml)
静默模式的软件 (见 squirrel.custom.yaml)

简繁切换

临时繁体输入:ctrl+` 选择 漢字
一直繁体输入:ctrl+` 选择 朙月拼音·臺灣正體

模糊音

打开 luna_pinyin_simp.custom.yamlspeller/algebra 相关位置开启 即去掉相关位置的注释即可。
比如 开启 - derive/^r/l/ 之后
ruguo => 如果
luguo => 如果

*注意:如果在使用老版本时发现模糊音无法生效
请找到 enable_charset_filter 这句选项

translator:
  enable_charset_filter: true #启用罕见字過濾

这2句会和模糊音的定义发生冲突。如果发现模糊音无法生效,那么请注释掉这2句并重新部署即可
( 当前版本已修复这个问题,是默认注释掉它们的 )

「标点」标点符号配置

标点符号配置在 ~/Library/Rime/default.custom.yaml 文件里
如果需要,可依你所需,设置为经典中文或英文标点,或即选标点
依注释稍加修改即可,重新部署生效

更多词:在必要时,添加自己的自定义短语 (个性短语)

rime_pro 增强包的特点之一:保持自定义词库的可扩展性
如果觉得有必要,测试2~7 均可由你实现自定义。这需要一点点代码知识
如果觉得有必要,添加个性短语到 ~/Library/Rime 目录的以下文件

f_myphrases.dict.yaml
f_mysecretphrases.dict.yaml

如果觉得有必要,个性短语 可以显式地写在这里,方便快捷输入 (如果觉得有必要写)
如果觉得有必要,按照文件内注释修改之后重新部署即可,比如添加一行

One Piece    op    10000
波特卡斯·D·艾斯    a    10000
萨博    s    10000
蒙奇·D·路飞    l    10000

注意:分隔符 tab
注意 op 前后不是多个空格,是一个分隔符 tab (建议用 系统自带的TextEditor 或 TextWrangler编辑器 打开,因为 像SublimeText编辑器 会自动把分隔符转化为空格)。这样就得到自定义的词组啦,重新部署之后测试快捷输入:
op => One Piece
l => 蒙奇·D·路飞 (翻几页可见;多输几次会自动调整词频并优先显示)

如果觉得有必要,再比如 如何打出 diany=>电影 ?
图片描述
图片描述
diany=>电影
类似的快捷输入均可稍改配置文件来显式地实现
这正是鼠须管的方式和高明之处:通过自定义配置实现自定义功能
更详尽原理,见官方Wiki用户指南

稍微方便一点点而已。如果觉得有必要 再修改这个文件(并重新部署)。

P.S. 为什么说 “如果觉得有必要” 呢?
因为对大多数人在大多数情况下, 做个性短语是没必要的
依靠软件自带的词频记忆功能即可,比如:
词语多输入几次 (而不用自定义词组这么麻烦) 软件就会自动记住
然后打首字母就能出来 (dy=>电影);这点上和其他输入法是一样的,省时省事

重要配置文件

default.custom.yaml
squirrel.custom.yaml
luna_pinyin_simp.custom.yaml
luna_pinyin_tw.custom.yaml
double_pinyin_flypy.custom.yaml

感谢

一切配置和探索,均基于 RIME 中州韻輸入法引擎 和 鼠须管输入法 自身的强大

All credit goes to RIME developers
RIME | 中州韻輸入法引擎 官方网站 rime.imGitHub
中州韻輸入法引擎和鼠须管输入法软件作者 输入法开发专家 佛振
RIME 翰林院 GitHubBintray教学网站

官方Wiki
https://github.com/rime/home/... (官方用户指南)
https://github.com/rime/home/... (官方介绍、必知必会)
https://github.com/rime/home/...設定項速查手冊 (設定項速查手冊、Schema.yaml 詳解)
https://github.com/rime/home/...中的數據文件分佈及作用 (配置目录及文件作用)
https://gist.github.com/lotem... (更多输入方案)

感谢前人经验和配置细节
http://crossingmay.com/2016/0... (基础配置, 需要一点点代码知识)
https://github.com/rime-aca/d... (词库扩充原理)
https://github.com/rime/home/...導入其他來源的碼表 (词库扩充原理和词库编辑办法)
https://github.com/rime/home/...碼表與詞典 (词库扩充原理)
https://medium.com/@scomper/鼠須管-的调教笔记-3fdeb0e78814 (基础配置和词库编辑办法)
https://gist.github.com/lotem... (词库编辑办法)
https://www.v2ex.com/t/58428 (emoji)
http://blog.yesmryang.net/rim... (颜文字)
https://gist.github.com/zolun... (颜文字)
https://github.com/hitigon/me... (颜文字)
https://github.com/rime/home/...一例定製簡化字輸出 (正體中文輸入)
http://blog.yesmryang.net/rim... (五笔)
https://medium.com/@scomper/鼠須管-的调教笔记-3fdeb0e78814 (模糊音)
https://www.v2ex.com/t/58428 (模糊音)
https://www.zhihu.com/questio... (卸载办法)
https://github.com/iHavee/rim... (同步)
https://segmentfault.com/a/11... (软件作者访谈)

https://gist.github.com/lotem... (鼠须管支持的输入法方案,含五笔)
https://github.com/osfans/tri...小知识%281%29---Yaml文件开头注释是什么意思? (YAML文件)
https://medium.com/@scomper/鼠須管-的调教笔记-3fdeb0e78814 (YAML文件编辑器注意事项)
https://github.com/rime/home/... (定制过程参考)
https://github.com/osfans/tri...经典资料汇总-菜鸟书评

感谢前人词库
https://code.google.com/archi...
https://github.com/rime-aca/d...
https://medium.com/@scomper/鼠須管-的调教笔记-3fdeb0e78814
https://www.v2ex.com/t/58428 (emoji)

详尽配置原理和配置说明 (延伸阅读)

基于 rime_pro 你可以进行你自己的折腾,对配置文件进行改造
发挥 DIY 精神,打造符合你的要求的输入法
延伸阅读以下文章可能会有所帮助
官方Wiki
官方Wiki定制指南
官方Wiki用户指南
官方Rime方案製作詳解

-

查看原文

赞 19 收藏 46 评论 21

maemual 赞了文章 · 2016-04-16

全站 HTTPS 来了

!版权声明:本文为腾讯Bugly原创文章,转载请注明出处
腾讯Bugly特约作者:刘强

最近大家在使用百度、谷歌或淘宝的时候,是不是注意浏览器左上角已经全部出现了一把绿色锁,这把锁表明该网站已经使用了 HTTPS 进行保护。仔细观察,会发现这些网站已经全站使用 HTTPS。同时,iOS 9 系统默认把所有的 http 请求都改为 HTTPS 请求。随着互联网的发展,现代互联网正在逐渐进入全站 HTTPS 时代。

因此有开发同学在给腾讯Bugly的邮件中问:
全站 HTTPS 能够带来怎样的优势?HTTPS 的原理又是什么?同时,阻碍 HTTPS 普及的困难是什么?
为了解答大家的困惑,腾讯Bugly特地邀请到了我们的高级工程师刘强,为大家综合参考多种资料并经过实践验证,探究 HTTPS 的基础原理,分析基本的 HTTPS 通信过程,迎接全站 HTTPS 的来临。

1. HTTPS 基础

HTTPS(Secure Hypertext Transfer Protocol)安全超文本传输协议 它是一个安全通信通道,它基于HTTP开发,用于在客户计算机和服务器之间交换信息。它使用安全套接字层(SSL)进行信息交换,简单来说它是HTTP的安全版,是使用 TLS/SSL 加密的 HTTP 协议。

HTTP 协议采用明文传输信息,存在信息窃听、信息篡改和信息劫持的风险,而协议 TLS/SSL 具有身份验证、信息加密和完整性校验的功能,可以避免此类问题。

TLS/SSL 全称安全传输层协议 Transport Layer Security, 是介于 TCP 和 HTTP 之间的一层安全协议,不影响原有的 TCP 协议和 HTTP 协议,所以使用 HTTPS 基本上不需要对 HTTP 页面进行太多的改造。

2. TLS/SSL 原理

HTTPS 协议的主要功能基本都依赖于 TLS/SSL 协议,本节分析安全协议的实现原理。

TLS/SSL 的功能实现主要依赖于三类基本算法:散列函数 Hash、对称加密和非对称加密,其利用非对称加密实现身份认证和密钥协商,对称加密算法采用协商的密钥对数据加密,基于散列函数验证信息的完整性。

散列函数 Hash,常见的有 MD5、SHA1、SHA256,该类函数特点是函数单向不可逆、对输入非常敏感、输出长度固定,针对数据的任何修改都会改变散列函数的结果,用于防止信息篡改并验证数据的完整性;对称加密,常见的有 AES-CBC、DES、3DES、AES-GCM等,相同的密钥可以用于信息的加密和解密,掌握密钥才能获取信息,能够防止信息窃听,通信方式是1对1;非对称加密,即常见的 RSA 算法,还包括 ECC、DH 等算法,算法特点是,密钥成对出现,一般称为公钥(公开)和私钥(保密),公钥加密的信息只能私钥解开,私钥加密的信息只能公钥解开。因此掌握公钥的不同客户端之间不能互相解密信息,只能和掌握私钥的服务器进行加密通信,服务器可以实现1对多的通信,客户端也可以用来验证掌握私钥的服务器身份。

在信息传输过程中,散列函数不能单独实现信息防篡改,因为明文传输,中间人可以修改信息之后重新计算信息摘要,因此需要对传输的信息以及信息摘要进行加密;对称加密的优势是信息传输1对1,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和 N 个客户端通信,需要维持 N 个密码记录,且缺少修改密码的机制;非对称加密的特点是信息传输1对多,服务器只需要维持一个私钥就能够和多个客户端进行加密通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密速度慢。

结合三类算法的特点,TLS 的基本工作方式是,客户端使用非对称加密与服务器进行通信,实现身份验证并协商对称加密使用的密钥,然后对称加密算法采用协商密钥对信息以及信息摘要进行加密通信,不同的节点之间采用的对称密钥不同,从而可以保证信息只能通信双方获取。

3. PKI 体系

3.1 RSA 身份验证的隐患

身份验证和密钥协商是 TLS 的基础功能,要求的前提是合法的服务器掌握着对应的私钥。但 RSA 算法无法确保服务器身份的合法性,因为公钥并不包含服务器的信息,存在安全隐患:

  • 客户端 C 和服务器 S 进行通信,中间节点 M 截获了二者的通信;

  • 节点 M 自己计算产生一对公钥 pub_M 和私钥 pri_M;

  • C 向 S 请求公钥时,M 把自己的公钥 pub_M 发给了 C;

  • C 使用公钥 pub_M 加密的数据能够被 M 解密,因为 M 掌握对应的私钥 pri_M,而 C 无法根据公钥信息判断服务器的身份,从而 C 和 M 之间建立了"可信"加密连接;

  • 中间节点 M 和服务器S之间再建立合法的连接,因此 C 和 S 之间通信被M完全掌握,M 可以进行信息的窃听、篡改等操作。

另外,服务器也可以对自己的发出的信息进行否认,不承认相关信息是自己发出。

因此该方案下至少存在两类问题:中间人攻击和信息抵赖。

3.2 身份验证-CA 和证书

解决上述身份验证问题的关键是确保获取的公钥途径是合法的,能够验证服务器的身份信息,为此需要引入权威的第三方机构 CA。CA 负责核实公钥的拥有者的信息,并颁发认证"证书",同时能够为使用者提供证书验证服务,即 PKI 体系。

基本的原理为,CA 负责审核信息,然后对关键信息利用私钥进行"签名",公开对应的公钥,客户端可以利用公钥验证签名。CA 也可以吊销已经签发的证书,基本的方式包括两类 CRL 文件和 OCSP。CA 使用具体的流程如下:

  1. 服务方 S 向第三方机构CA提交公钥、组织信息、个人信息(域名)等信息并申请认证;

  2. CA 通过线上、线下等多种手段验证申请者提供信息的真实性,如组织是否存在、企业是否合法,是否拥有域名的所有权等;

  3. 如信息审核通过,CA 会向申请者签发认证文件-证书。

    • 证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA 的信息、有效时间、证书序列号等信息的明文,同时包含一个签名;

    • 签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用 CA 的私钥对信息摘要进行加密,密文即签名;

  4. 客户端 C 向服务器 S 发出请求时,S 返回证书文件;

  5. 客户端 C 读取证书中的相关的明文信息,采用相同的散列函数计算得到信息摘要,然后,利用对应 CA 的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性,即公钥合法;

  6. 客户端然后验证证书相关的域名信息、有效时间等信息;

  7. 客户端会内置信任 CA 的证书信息(包含公钥),如果CA不被信任,则找不到对应 CA 的证书,证书也会被判定非法。

在这个过程注意几点:

  1. 申请证书不需要提供私钥,确保私钥永远只能服务器掌握;

  2. 证书的合法性仍然依赖于非对称加密算法,证书主要是增加了服务器信息以及签名;

  3. 内置 CA 对应的证书称为根证书,颁发者和使用者相同,自己为自己签名,即自签名证书;

  4. 证书=公钥+申请者与颁发者信息+签名;

3.3 证书链

如 CA 根证书和服务器证书中间增加一级证书机构,即中间证书,证书的产生和验证原理不变,只是增加一层验证,只要最后能够被任何信任的CA根证书验证合法即可。

  1. 服务器证书 server.pem 的签发者为中间证书机构 inter,inter 根据证书 inter.pem 验证 server.pem 确实为自己签发的有效证书;

  2. 中间证书 inter.pem 的签发 CA 为 root,root 根据证书 root.pem 验证 inter.pem 为自己签发的合法证书;

  3. 客户端内置信任 CA 的 root.pem 证书,因此服务器证书 server.pem 的被信任。

服务器证书、中间证书与根证书在一起组合成一条合法的证书链,证书链的验证是自下而上的信任传递的过程。

二级证书结构存在的优势:

  1. 减少根证书结构的管理工作量,可以更高效的进行证书的审核与签发;

  2. 根证书一般内置在客户端中,私钥一般离线存储,一旦私钥泄露,则吊销过程非常困难,无法及时补救;

  3. 中间证书结构的私钥泄露,则可以快速在线吊销,并重新为用户签发新的证书;

  4. 证书链四级以内一般不会对 HTTPS 的性能造成明显影响。

证书链有以下特点:

  1. 同一本服务器证书可能存在多条合法的证书链。
    因为证书的生成和验证基础是公钥和私钥对,如果采用相同的公钥和私钥生成不同的中间证书,针对被签发者而言,该签发机构都是合法的 CA,不同的是中间证书的签发机构不同;

  2. 不同证书链的层级不一定相同,可能二级、三级或四级证书链。

中间证书的签发机构可能是根证书机构也可能是另一个中间证书机构,所以证书链层级不一定相同。

3.4 证书吊销

CA 机构能够签发证书,同样也存在机制宣布以往签发的证书无效。证书使用者不合法,CA 需要废弃该证书;或者私钥丢失,使用者申请让证书无效。主要存在两类机制:CRL 与 OCSP。

(a) CRL

Certificate Revocation List, 证书吊销列表,一个单独的文件。该文件包含了 CA 已经吊销的证书序列号(唯一)与吊销日期,同时该文件包含生效日期并通知下次更新该文件的时间,当然该文件必然包含 CA 私钥的签名以验证文件的合法性。

证书中一般会包含一个 URL 地址 CRL Distribution Point,通知使用者去哪里下载对应的 CRL 以校验证书是否吊销。该吊销方式的优点是不需要频繁更新,但是不能及时吊销证书,因为 CRL 更新时间一般是几天,这期间可能已经造成了极大损失。

(b) OCSP

Online Certificate Status Protocol, 证书状态在线查询协议,一个实时查询证书是否吊销的方式。请求者发送证书的信息并请求查询,服务器返回正常、吊销或未知中的任何一个状态。证书中一般也会包含一个 OCSP 的 URL 地址,要求查询服务器具有良好的性能。部分 CA 或大部分的自签 CA (根证书)都是未提供 CRL 或 OCSP 地址的,对于吊销证书会是一件非常麻烦的事情。

4. TLS/SSL握手过程

4.1 握手与密钥协商过程

基于 RSA 握手和密钥交换的客户端验证服务器为示例详解握手过程。

1. client_hello

客户端发起请求,以明文传输请求信息,包含版本信息,加密套件候选列表,压缩算法候选列表,随机数,扩展字段等信息,相关信息如下:

  • 支持的最高TSL协议版本version,从低到高依次 SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2,当前基本不再使用低于 TLSv1 的版本;

  • 客户端支持的加密套件 cipher suites 列表, 每个加密套件对应前面 TLS 原理中的四个功能的组合:认证算法 Au (身份验证)、密钥交换算法 KeyExchange(密钥协商)、对称加密算法 Enc (信息加密)和信息摘要 Mac(完整性校验);

  • 支持的压缩算法 compression methods 列表,用于后续的信息压缩传输;

  • 随机数 random_C,用于后续的密钥的生成;

  • 扩展字段 extensions,支持协议与算法的相关参数以及其它辅助信息等,常见的 SNI 就属于扩展字段,后续单独讨论该字段作用。

2. server_hello+server_certificate+sever_hello_done

  1. server_hello, 服务端返回协商的信息结果,包括选择使用的协议版本 version,选择的加密套件 cipher suite,选择的压缩算法 compression method、随机数 random_S 等,其中随机数用于后续的密钥协商;

  2. server_certificates, 服务器端配置对应的证书链,用于身份验证与密钥交换;

  3. server_hello_done,通知客户端 server_hello 信息发送结束;

3.证书校验

客户端验证证书的合法性,如果验证通过才会进行后续通信,否则根据错误情况不同做出提示和操作,合法性验证包括如下:

  • 证书链的可信性 trusted certificate path,方法如前文所述;

  • 证书是否吊销 revocation,有两类方式离线 CRL 与在线 OCSP,不同的客户端行为会不同;

  • 有效期 expiry date,证书是否在有效时间范围;

  • 域名 domain,核查证书域名是否与当前的访问域名匹配,匹配规则后续分析;

4. client_key_exchange+change_cipher_spec+encrypted_handshake_message

  1. client_key_exchange,合法性验证通过之后,客户端计算产生随机数字 Pre-master,并用证书公钥加密,发送给服务器;

  2. 此时客户端已经获取全部的计算协商密钥需要的信息:两个明文随机数 random_C 和 random_S 与自己计算产生的 Pre-master,计算得到协商密钥;

    enc_key=Fuc(random_C, random_S, Pre-Master)
  3. change_cipher_spec,客户端通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信;

  4. encrypted_handshake_message,结合之前所有通信参数的 hash 值与其它相关信息生成一段数据,采用协商密钥 session secret 与算法进行加密,然后发送给服务器用于数据与握手验证;

5. change_cipher_spec+encrypted_handshake_message

  1. 服务器用私钥解密加密的 Pre-master 数据,基于之前交换的两个明文随机数 random_C 和 random_S,计算得到协商密钥:enc_key=Fuc(random_C, random_S, Pre-Master);

  2. 计算之前所有接收信息的 hash 值,然后解密客户端发送的 encrypted_handshake_message,验证数据和密钥正确性;

  3. change_cipher_spec, 验证通过之后,服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信;

  4. encrypted_handshake_message, 服务器也结合所有当前的通信参数信息生成一段数据并采用协商密钥 session secret 与算法加密并发送到客户端;

6. 握手结束

客户端计算所有接收信息的 hash 值,并采用协商密钥解密 encrypted_handshake_message,验证服务器发送的数据和密钥,验证通过则握手完成;

7. 加密通信

开始使用协商密钥与算法进行加密通信。

注意:

  1. 服务器也可以要求验证客户端,即双向认证,可以在过程2要发送 client_certificate_request 信息,客户端在过程4中先发送 client_certificate与certificate_verify_message 信息,证书的验证方式基本相同,certificate_verify_message 是采用client的私钥加密的一段基于已经协商的通信信息得到数据,服务器可以采用对应的公钥解密并验证;

  2. 根据使用的密钥交换算法的不同,如 ECC 等,协商细节略有不同,总体相似;

  3. sever key exchange 的作用是 server certificate 没有携带足够的信息时,发送给客户端以计算 pre-master,如基于 DH 的证书,公钥不被证书中包含,需要单独发送;

  4. change cipher spec 实际可用于通知对端改版当前使用的加密通信方式,当前没有深入解析;

  5. alter message 用于指明在握手或通信过程中的状态改变或错误信息,一般告警信息触发条件是连接关闭,收到不合法的信息,信息解密失败,用户取消操作等,收到告警信息之后,通信会被断开或者由接收方决定是否断开连接。

4.2 会话缓存握手过程

为了加快建立握手的速度,减少协议带来的性能降低和资源消耗(具体分析在后文),TLS 协议有两类会话缓存机制:会话标识 session ID 与会话记录 session ticket。

session ID 由服务器端支持,协议中的标准字段,因此基本所有服务器都支持,服务器端保存会话ID以及协商的通信信息,Nginx 中1M 内存约可以保存4000个 session ID 机器相关信息,占用服务器资源较多;

session ticket 需要服务器和客户端都支持,属于一个扩展字段,支持范围约60%(无可靠统计与来源),将协商的通信信息加密之后发送给客户端保存,密钥只有服务器知道,占用服务器资源很少。

二者对比,主要是保存协商信息的位置与方式不同,类似与 http 中的 session 与 cookie。

二者都存在的情况下,(nginx 实现)优先使用 session_ticket。

握手过程如下图:

注意:虽然握手过程有1.5个来回,但是最后客户端向服务器发送的第一条应用数据不需要等待服务器返回的信息,因此握手延时是1*RTT。

1. 会话标识 session ID

  1. 如果客户端和服务器之间曾经建立了连接,服务器会在握手成功后返回 session ID,并保存对应的通信参数在服务器中;

  2. 如果客户端再次需要和该服务器建立连接,则在 client_hello 中 session ID 中携带记录的信息,发送给服务器;

  3. 服务器根据收到的 session ID 检索缓存记录,如果没有检索到货缓存过期,则按照正常的握手过程进行;

  4. 如果检索到对应的缓存记录,则返回 change_cipher_spec 与 encrypted_handshake_message 信息,两个信息作用类似,encrypted_handshake_message 是到当前的通信参数与 master_secret的hash 值;

  5. 如果客户端能够验证通过服务器加密数据,则客户端同样发送 change_cipher_spec 与 encrypted_handshake_message 信息;

  6. 服务器验证数据通过,则握手建立成功,开始进行正常的加密数据通信。

2. 会话记录 session ticket

  1. 如果客户端和服务器之间曾经建立了连接,服务器会在 new_session_ticket 数据中携带加密的 session_ticket 信息,客户端保存;

  2. 如果客户端再次需要和该服务器建立连接,则在 client_hello 中扩展字段 session_ticket 中携带加密信息,一起发送给服务器;

  3. 服务器解密 sesssion_ticket 数据,如果能够解密失败,则按照正常的握手过程进行;

  4. 如果解密成功,则返回 change_cipher_spec 与 encrypted_handshake_message 信息,两个信息作用与 session ID 中类似;

  5. 如果客户端能够验证通过服务器加密数据,则客户端同样发送 change_cipher_spec与encrypted_handshake_message 信息;

  6. 服务器验证数据通过,则握手建立成功,开始进行正常的加密数据通信。

4.3 重建连接

重建连接 renegotiation 即放弃正在使用的 TLS 连接,从新进行身份认证和密钥协商的过程,特点是不需要断开当前的数据传输就可以重新身份认证、更新密钥或算法,因此服务器端存储和缓存的信息都可以保持。客户端和服务器都能够发起重建连接的过程,当前 windows 2000 & XP 与 SSL 2.0不支持。

1. 服务器重建连接

服务器端重建连接一般情况是客户端访问受保护的数据时发生。基本过程如下:

(a) 客户端和服务器之间建立了有效 TLS 连接并通信;
(b) 客户端访问受保护的信息;
(c) 服务器端返回 hello_request 信息;
(d) 客户端收到 hello_request 信息之后发送 client_hello 信息,开始重新建立连接。

2. 客户端重建连接

客户端重建连接一般是为了更新通信密钥。

  1. 客户端和服务器之间建立了有效 TLS 连接并通信;

  2. 客户端需要更新密钥,主动发出 client_hello 信息;

  3. 服务器端收到 client_hello 信息之后无法立即识别出该信息非应用数据,因此会提交给下一步处理,处理完之后会返回通知该信息为要求重建连接;

  4. 在确定重建连接之前,服务器不会立即停止向客户端发送数据,可能恰好同时或有缓存数据需要发送给客户端,但是客户端不会再发送任何信息给服务器;

  5. 服务器识别出重建连接请求之后,发送 server_hello 信息至客户端;

  6. 客户端也同样无法立即判断出该信息非应用数据,同样提交给下一步处理,处理之后会返回通知该信息为要求重建连接;

  7. 客户端和服务器开始新的重建连接的过程。

4.4 密钥计算

上节提到了两个明文传输的随机数 random_C 和 random_S 与通过加密在服务器和客户端之间交换的 Pre-master,三个参数作为密钥协商的基础。本节讨论说明密钥协商的基本计算过程以及通信过程中的密钥使用。

1.计算 Key

涉及参数 random client 和 random server, Pre-master, Master secret, key material, 计算密钥时,服务器和客户端都具有这些基本信息,交换方式在上节中有说明,计算流程如下:

  1. 客户端采用 RSA 或 Diffie-Hellman 等加密算法生成 Pre-master;

  2. Pre-master 结合 random client 和 random server 两个随机数通过 PseudoRandomFunction(PRF)计算得到 Master secret;

  3. Master secret 结合 random client 和 random server 两个随机数通过迭代计算得到 Key material;

以下为一些重要的记录,可以解决部分爱深入研究朋友的疑惑,copy的材料,分享给大家:

(a) PreMaster secret 前两个字节是 TLS 的版本号,这是一个比较重要的用来核对握手数据的版本号,因为在 Client Hello 阶段,客户端会发送一份加密套件列表和当前支持的 SSL/TLS 的版本号给服务端,而且是使用明文传送的,如果握手的数据包被破解之后,攻击者很有可能串改数据包,选择一个安全性较低的加密套件和版本给服务端,从而对数据进行破解。所以,服务端需要对密文中解密出来对的 PreMaster 版本号跟之前 Client Hello 阶段的版本号进行对比,如果版本号变低,则说明被串改,则立即停止发送任何消息。(copy)

(b) 不管是客户端还是服务器,都需要随机数,这样生成的密钥才不会每次都一样。由于 SSL 协议中证书是静态的,因此十分有必要引入一种随机因素来保证协商出来的密钥的随机性。

对于 RSA 密钥交换算法来说,pre-master-key 本身就是一个随机数,再加上 hello 消息中的随机,三个随机数通过一个密钥导出器最终导出一个对称密钥。

pre master 的存在在于 SSL 协议不信任每个主机都能产生完全随机的随机数,如果随机数不随机,那么 pre master secret 就有可能被猜出来,那么仅适用 pre master secret 作为密钥就不合适了,因此必须引入新的随机因素,那么客户端和服务器加上 pre master secret 三个随机数一同生成的密钥就不容易被猜出了,一个伪随机可能完全不随机,可是三个伪随机就十分接近随机了,每增加一个自由度,随机性增加的可不是一。

2.密钥使用

Key 经过12轮迭代计算会获取到12个 hash 值,分组成为6个元素,列表如下:

  1. mac key、encryption key 和 IV 是一组加密元素,分别被客户端和服务器使用,但是这两组元素都被两边同时获取;

  2. 客户端使用 client 组元素加密数据,服务器使用 client 元素解密;服务器使用 server 元素加密,client 使用 server 元素解密;

  3. 双向通信的不同方向使用的密钥不同,破解通信至少需要破解两次;

  4. encryption key 用于对称加密数据;

  5. IV 作为很多加密算法的初始化向量使用,具体可以研究对称加密算法;

  6. Mac key 用于数据的完整性校验;

4.4 数据加密通信过程

  1. 对应用层数据进行分片成合适的 block;

  2. 为分片数据编号,防止重放攻击;

  3. 使用协商的压缩算法压缩数据;

  4. 计算 MAC 值和压缩数据组成传输数据;

  5. 使用 client encryption key 加密数据,发送给服务器 server;

  6. server 收到数据之后使用 client encrytion key 解密,校验数据,解压缩数据,重新组装。

注:MAC值的计算包括两个 Hash 值:client Mac key 和 Hash (编号、包类型、长度、压缩数据)。

4.5 抓包分析

关于抓包不再详细分析,按照前面的分析,基本的情况都能够匹配,根据平常定位问题的过程,个人提些认为需要注意的地方:

  1. 抓包 HTTP 通信,能够清晰的看到通信的头部和信息的明文,但是 HTTPS 是加密通信,无法看到 HTTP 协议的相关头部和数据的明文信息,

  2. 抓包 HTTPS 通信主要包括三个过程:TCP 建立连接、TLS 握手、TLS 加密通信,主要分析 HTTPS 通信的握手建立和状态等信息。

  3. client_hello

    • 根据 version 信息能够知道客户端支持的最高的协议版本号,如果是 SSL 3.0 或 TLS 1.0 等低版本协议,非常注意可能因为版本低引起一些握手失败的情况;

    • 根据 extension 字段中的 server_name 字段判断是否支持SNI,存在则支持,否则不支持,对于定位握手失败或证书返回错误非常有用;

    • 会话标识 session ID 是标准协议部分,如果没有建立过连接则对应值为空,不为空则说明之前建立过对应的连接并缓存;

    • 会话记录 session ticke t是扩展协议部分,存在该字段说明协议支持 sesssion ticket,否则不支持,存在且值为空,说明之前未建立并缓存连接,存在且值不为空,说明有缓存连接。

  4. server_hello

    • 根据 TLS version 字段能够推测出服务器支持的协议的最高版本,版本不同可能造成握手失败;

    • 基于 cipher_suite 信息判断出服务器优先支持的加密协议;

  5. ceritficate:服务器配置并返回的证书链,根据证书信息并于服务器配置文件对比,判断请求与期望是否一致,如果不一致,是否返回的默认证书。

  6. alert

告警信息 alert 会说明建立连接失败的原因即告警类型,对于定位问题非常重要。

5.HTTPS 性能与优化

5.1 HTTPS 性能损耗

前文讨论了 HTTPS 原理与优势:身份验证、信息加密与完整性校验等,且未对 TCP 和 HTTP 协议做任何修改。但通过增加新协议以实现更安全的通信必然需要付出代价,HTTPS 协议的性能损耗主要体现如下:

1. 增加延时

分析前面的握手过程,一次完整的握手至少需要两端依次来回两次通信,至少增加延时2 RTT,利用会话缓存从而复用连接,延时也至少1 RTT*。

2. 消耗较多的 CPU 资源

除数据传输之外,HTTPS 通信主要包括对对称加解密、非对称加解密(服务器主要采用私钥解密数据);压测 TS8 机型的单核 CPU:对称加密算法AES-CBC-256 吞吐量 600Mbps,非对称 RSA 私钥解密200次/s。不考虑其它软件层面的开销,10G 网卡为对称加密需要消耗 CPU 约17核,24核CPU最多接入 HTTPS 连接 4800;

静态节点当前10G 网卡的 TS8 机型的 HTTP 单机接入能力约为10w/s,如果将所有的 HTTP 连接变为HTTPS连接,则明显 RSA 的解密最先成为瓶颈。因此,RSA 的解密能力是当前困扰 HTTPS 接入的主要难题。

5.2 HTTPS 接入优化

1. CDN 接入

HTTPS 增加的延时主要是传输延时 RTT,RTT 的特点是节点越近延时越小,CDN 天然离用户最近,因此选择使用 CDN 作为 HTTPS 接入的入口,将能够极大减少接入延时。CDN 节点通过和业务服务器维持长连接、会话复用和链路质量优化等可控方法,极大减少 HTTPS 带来的延时。

2. 会话缓存

虽然前文提到 HTTPS 即使采用会话缓存也要至少1*RTT的延时,但是至少延时已经减少为原来的一半,明显的延时优化;同时,基于会话缓存建立的 HTTPS 连接不需要服务器使用RSA私钥解密获取 Pre-master 信息,可以省去CPU 的消耗。如果业务访问连接集中,缓存命中率高,则HTTPS的接入能力讲明显提升。当前 TRP 平台的缓存命中率高峰时期大于30%,10k/s的接入资源实际可以承载13k/的接入,收效非常可观。

3. 硬件加速

为接入服务器安装专用的 SSL 硬件加速卡,作用类似 GPU,释放 CPU,能够具有更高的 HTTPS 接入能力且不影响业务程序的。测试某硬件加速卡单卡可以提供 35k 的解密能力,相当于175核 CPU,至少相当于7台24核的服务器,考虑到接入服务器其它程序的开销,一张硬件卡可以实现接近10台服务器的接入能力。

4. 远程解密

本地接入消耗过多的 CPU 资源,浪费了网卡和硬盘等资源,考虑将最消耗 CPU 资源的RSA解密计算任务转移到其它服务器,如此则可以充分发挥服务器的接入能力,充分利用带宽与网卡资源。远程解密服务器可以选择 CPU 负载较低的机器充当,实现机器资源复用,也可以是专门优化的高计算性能的服务器。当前也是 CDN 用于大规模HTTPS接入的解决方案之一。

5. SPDY/HTTP2

前面的方法分别从减少传输延时和单机负载的方法提高 HTTPS 接入性能,但是方法都基于不改变 HTTP 协议的基础上提出的优化方法,SPDY/HTTP2 利用 TLS/SSL 带来的优势,通过修改协议的方法来提升 HTTPS 的性能,提高下载速度等。


想了解更多干货,请搜索关注公众号:腾讯Bulgy,或搜索微信号:weixinBugly,关注我们

腾讯Bugly

Bugly是腾讯内部产品质量监控平台的外发版本,支持iOS和Android两大主流平台,其主要功能是App发布以后,对用户侧发生的crash以及卡顿现象进行监控并上报,让开发同学可以第一时间了解到app的质量情况,及时修改。目前腾讯内部所有的产品,均在使用其进行线上产品的崩溃监控。
腾讯内部团队4年打磨,目前腾讯内部所有的产品都在使用,基本覆盖了中国市场的移动设备以及网络环境,可靠性有保证。使用Bugly,你就使用了和手机QQ、QQ空间、手机管家相同的质量保障手段

查看原文

赞 14 收藏 145 评论 2

maemual 赞了文章 · 2015-12-30

vue.js 2015 回顾(译)

clipboard.png

2015年对Vue.js来说是高速发展的一年。这个项目的发展已经超出了我的预期,所以我打算做一个回顾并阐述一些观点。

开头

使用情况

  • NPM下载量: 382,184, ~52k每月

  • GitHub Stars数量: 11,357

很遗憾,Bower和CDNs没有办法提供下载统计 - 应该至少与上面的数据相持平,因为有相当一部分的Vue.js使用者直接从CDN引用并将它用于非SPA页面。

GitHub star数量从二月份到现在有了7.6k+的增长。相比之下,Vue.js在发布的第一年(2014二月 至 2015 二月)只获得了~3.6kstars

Repo 活动记录

  • 版本数量: 54 (从 0.11.51.0.12 , 包含 alpha/beta/rc 版本)

  • 代码提交次数: 1,023

  • 关闭的Issues数量: 1,014

  • Pull Requests合并数量: 69个(从43个贡献者)

Vuejs.org 官网数据

  • 页面浏览次数: 3,761,728

  • 累计浏览人数: 363,365

  • 30天内活跃人数: 76,090

亮点

被Laravel社区采用

一切的开始来自于……

React学起来真是太费劲了。@vuejs看起来挺容易的,而且网站挺不错的。?

— Taylor Otwell (@taylorotwell) April 20, 2015

Taylor OtwellLaravel的作者,选用了Vue.js做为他新的前端库以代替React。不久之后Jeffrey Waylaracasts录制了教学视频用来安利Vue.js。现在很多的Vue.js活跃用户就是来自Laravel社区。现在有很多非常Cool的开源项目就是这两个技术的结晶,比如Koel

发布1.0

1.0版本的开发真是一个艰难的工作:认真考虑反对的声音,漫长的时间,倾听关于模板语法修改的讨论。但最终我相信我们做出了让大多数人满意的结果。1.0版本有着升级警告提示并且完全向后兼容,因此我对提供了无缝升级这件事十分的自豪。

1.0的发布对于这个项目的采用来说是一个不错的宣传。这个发布在HackerNews front页面呆了有段时间,收到了超过300个赞成票。GitHub star的数量激增,从那之后Vue.js几乎每天都呆在GitHub JavaScript trending的列表里。在Google统计中,Vue.js有着不错的增长率,最近超过了Backbone和Ember

不断壮大的生态系统

除了Vue.js这个核心,我们现在也提供了一整套的库和工具以用来构建大型应用:

当然也有很多社区贡献的项目 - 分享你创造的东西吧!

当嘉宾!

我在今年做了好多次的播客,主要是去谈论Vue.js的。这些播客谈到了Vue.js很多深层次的话题,如果你对这些技术细节有兴趣,那么非常值得一听!

思考 - The Progressive Framework

经常有人问我Vue.js和其他的框架相比有何优缺点。这当中有大量的技术细节,在我的播客中已经谈论的足够多了。最根本的问题在于为什么Vue.js存在,它的最终目标是什么。老实说,我也经常这样问我自己 - 尤其是在这个几乎人人都在高谈阔论React的2015年。撇掉React的优势不谈,有相当的人喜欢使用Vue.js - 事实上,人数越来越多。每隔几天我都可以在推特看到关于Vue.js如何改变了别人的开发方式。这使得我坚信Vue.js正在填补现有web开发的不足之处。

web开发覆盖的方向非常多,每一个web开发方向都非常的不同。从静态内容网站到复杂的企业应用,人们的构建方式几乎完全不同。每一个解决方案都是针对特定的问题而生的。例如,当尝试管理大型复杂应用的时候,侵入性强的框架往往会引入一些额外没有什么价值的复杂度到团队架构中,这些概念和工具使得简单的事情变得复杂。另一方面,当把一些大大小小的库组合在一起来处理大型应用时,各种调研、开发和配置的工作变得多的吓人。

我相信Vue.js是正确的,它解决了大多数基本的web开发问题 - 通过声明映射状态到DOM - 将侵入性降到最小。如果这正是你想要的,那么这种复杂程度能立马被控制住。当项目的规模开始膨胀,你可能会开始使用组件,但它并不一定必须是一个SPA的。对于真正的SPA来说,你可以使用vue-router,然后你可以考虑是否使用模块构建系统。最终,对于一个成熟的模块化的SPA来说,你还可以考虑是否用Vuex来管理状态……

这就是我所说的Progressive Framework: 关键在于我们是否能够让框架跟随项目的复杂需求一起增长。但你开始扩展的时候,你将不必在数不清的解决方案中苦苦筛选,因为这里有着官方解决方案并配有文档,这些解决方案本身就是被设计用于一起工作的。(当然,你也可以用点别的东西把他们替换掉)。在progressive framework中,你的框架相关的知识可以贯穿于整个项目,而不是只用到它的一小部分。

在2016年依旧有许多地方需要努力 - 但一定会向着好的方向发展 ;)

author: Evan You
date: Dec 20, 2015
via: http://blog.evanyou.me/2015/1...


希望我没有曲解作者的意思?

查看原文

赞 17 收藏 45 评论 7

maemual 赞了回答 · 2015-09-30

“指针”是成熟的编程语言必须具有的概念吗?

指针的本意是:在一个变量中保存另一个变量的地址,以提供将“地址”变量化的能力。如果没有指针,将无法用一个变量引用另一个变量(只能把变量的值拷贝一份赋给另一个变量)。

C语言中提供了完善的指针操作,包括为指针赋值、内存分配(malloc)、取变量地址、让指针可以参与运算等,这使得C程序员能够任意操作可用内存。

JavaJavascript)中也有指针,只不过与C相比,Java对程序员使用指针有着严格的限制,仅允许赋值操作,而且不是任意值,只能是通过new创建的对象引用或其他引用变量的值。不过Java一般不说指针,而是用引用reference)来称呼指向对象的指针,不过,Java中仍然可以找到一些指针存在的影子,例如,当一个对象为null时调用方法会导致null pointer异常,即所谓的空指针错误,可见Java内部使用的确实是指针。

很多基本的数据结构,例如链表、树、图等,都必须用指针来保存前驱或后继节点的地址,否则这些数据结构无法实现。

如果一个语言不提供指针,虽然在理论上它也具备完整的计算能力,但很多在其他语言中非常简单的问题都将变得极其复杂(本来想举个例子的,但一时想不起了,不过这个结论肯定是正确的)。

所以这个作者说的是对的,只是你需要理解指针的本质,不要错误地认为只有像C语言那样的指针才叫指针,真正的指针的概念请看我开头的那句。

关注 6 回答 5

maemual 赞了文章 · 2015-08-11

scala 从入门到入门+

新手向,面向刚从java过渡到scala的同学,目的是写出已已易于维护和阅读的代码.

写了一份更全面,带习题的文档在 https://fordeal-smalldata.git...
时间充裕的朋友可以试试

从语句到表达式

语句(statement): 一段可执行的代码
表达式(expression): 一段可以被求值的代码

在Java中语句和表达式是有区分的,表达式必须在return或者等号右侧,而在scala中,一切都是表达式.

一个例子:
假设我们在公司的内网和外网要从不同的域名访问一样的机器

//Java代码
String urlString = null;
String hostName = InetAddress.getLocalHost().getHostName();
if (isInnerHost(hostName)) {
  urlString = "http://inner.host";
} else {
  urlString = "http://outter.host";
}

刚转到scala的人很可能这么写

var urlString: String = null
var hostName = InetAddress.getLocalHost.getHostName
if (isInnerHost(hostName)) {
  urlString = "http://inner.host"
} else {
  urlString = "http://outter.host"
}

我们让它更像scala一点吧

val hostName = InetAddress.getLocalHost.getHostName
val urlString = if (isInnerHost(hostName)) {
  "http://inner.host"
} else {
  "http://outter.host"
}
这样做的好处都有啥?
  1. 代码简练,符合直觉
  2. urlString 是值而不是变量,有效防止 urlString 在后续的代码中被更改(编译时排错)

很多时候,我们编程时说的安全并不是指怕被黑客破坏掉,而是预防自己因为逗比而让程序崩了.

纯函数和非纯函数

纯函数(Pure Function)是这样一种函数——输入输出数据流全是显式(Explicit)的。
显式(Explicit)的意思是,函数与外界交换数据只有一个唯一渠道——参数和返回值;函数从函数外部接受的所有输入信息都通过参数传递到该函数内部;函数输出到函数外部的所有信息都通过返回值传递到该函数外部。

如果一个函数通过隐式(Implicit)方式,从外界获取数据,或者向外部输出数据,那么,该函数就不是纯函数,叫作非纯函数(Impure Function)。
隐式(Implicit)的意思是,函数通过参数和返回值以外的渠道,和外界进行数据交换。比如,读取全局变量,修改全局变量,都叫作以隐式的方式和外界进行数据交换;比如,利用I/O API(输入输出系统函数库)读取配置文件,或者输出到文件,打印到屏幕,都叫做隐式的方式和外界进行数据交换。

//一些例子
//纯函数
def add(a:Int,b:Int) = a + b
//非纯函数
var a = 1
def addA(b:Int) = a + b
 
def add(a:Int,b:Int) = {
  println(s"a:$a b:$b")
  a + b
}
def randInt() = Random.nextInt()

纯函数的好处(来自维基百科)

  • 无状态,线程安全,不需要线程同步.
  • 纯函数相互调用组装起来的函数,还是纯函数.
  • 应用程序或者运行环境(Runtime)可以对纯函数的运算结果进行缓存,运算加快速度.

纯函数的好处(来自我的经验)

  • 单元测试非常方便!
  • 分布式/并发环境下,断点调试的方式无以为继,你需要单元测试.

单元测试什么的,赶紧去 http://www.scalatest.org 试试吧

惰性求值/Call by name

维基百科中惰性求值的解释
惰性求值(Lazy Evaluation),又称惰性计算、懒惰求值,是一个计算机编程中的一个概念,它的目的是要最小化计算机要做的工作。它有两个相关而又有区别的含意,可以表示为“延迟求值”和“最小化求值”,本条目专注前者,后者请参见最小化计算条目。除可以得到性能的提升外,惰性计算的最重要的好处是它可以构造一个无限的数据类型。
惰性求值的相反是及早求值,这是一个大多数编程语言所拥有的普通计算方式。

惰性求值不是新鲜事

import scala.io.Source.fromFile
val iter: Iterator[String] =
  fromFile("sampleFile")
    .getLines()

文件迭代器就用到了惰性求值.
用户可以完全像操作内存中的数据一样操作文件,然而文件只有一小部分传入了内存中.

用lazy关键词指定惰性求值

lazy val firstLazy = {
  println("first lazy")
  1
}
lazy val secondLazy = {
  println("second lazy")
  2
} 
def add(a:Int,b:Int) = {
  a+b
}
//在 scala repl 中的结果
scala> add(secondLazy,firstLazy)
second lazy
first lazy
res0: Int = 3

res0: Int = 3

second lazy 先于 first lazy输出了

Call by value 就是函数参数的惰性求值

def firstLazy = {
  println("first lazy")
  1
}
def secondLazy = {
  println("second lazy")
  2
}
def chooseOne(first: Boolean, a: Int, b: Int) = {
  if (first) a else b
}
def chooseOneLazy(first: Boolean, a: => Int, b: => Int) = {
  if (first) a else b
}
chooseOne(first = true, secondLazy, firstLazy)
//second lazy
//first lazy
//res0: Int = 2
chooseOneLazy(first = true, secondLazy, firstLazy)
//second lazy
//res1: Int = 2

对于非纯函数,惰性求值会产生和立即求值产生不一样的结果.

一个例子,假设你要建立一个本地缓存

//需要查询mysql等,可能来自于一个第三方jar包
def itemIdToShopId: Int => Int  
var cache = Map.empty[Int, Int]
def cachedItemIdToShopId(itemId: Int):Int = {
  cache.get(itemId) match {
    case Some(shopId) => shopId
    case None =>
      val shopId = itemIdToShopId(itemId)
      cache += itemId -> shopId
      shopId
  }
}
  • 罗辑没什么问题,但测试的时候不方便连mysql怎么办?
  • 如果第三方jar包发生了改变,cachedItemIdToShopId也要发生改变.
//用你的本地mock来测试程序
def mockItemIdToSHopId: Int => Int
def cachedItemIdToShopId(itemId: Int): Int ={  
  cache.get(itemId) match {    
    case Some(shopId) => shopId
   case None =>    
      val shopId = mockItemIdToSHopId(itemId)
      cache += itemId -> shopId
     shopId    
  }    
}    
  • 在测试的时候用mock,提交前要换成线上的,反复测试的话要反复改动,非常令人沮丧.
  • 手工操作容易忙中出错.
//将远程请求的结果作为函数的一个参数
def cachedItemIdToShopId(itemId: Int, remoteShopId: Int): Int = {    
  cache.get(itemId) match {    
    case Some(shopId) => shopId    
    case None =>    
     val shopId = remoteShopId    
     cache += itemId -> shopId    
      shopId
  }    
}
//调用这个函数
cachedItemIdToShopId(itemId,itemIdToShopId(itemId))
  • 函数对mysql的依赖没有了
  • 不需要在测试和提交时切换代码
  • 貌似引入了新问题?

没错,cache根本没有起应有的作用,函数每次执行的时候都调用了itemIdToShopId从远程取数据

//改成call by name就没有这个问题啦
def cachedItemIdToShopId(itemId: Int, remoteShopId: =>Int): Int = {    
  cache.get(itemId) match {    
    case Some(shopId) => shopId    
    case None =>    
     val shopId = remoteShopId    
     cache += itemId -> shopId    
      shopId
  }    
}
//调用这个函数
cachedItemIdToShopId(itemId,itemIdToShopId(itemId))
  • 函数对mysql的依赖没有了
  • 不需要在测试和提交时切换代码
  • 只在需要的时候查询远程库

Tuple/case class/模式匹配

Tuple为编程提供许多便利

  • 函数可以通过tuple返回多个值
  • tuple可以存储在容器类中,代替java bean
  • 可以一次为多个变量赋值

使用tuple的例子

val (one, two) = (1, 2)        
one //res0: Int = 1    
two //res1: Int = 2            
def sellerAndItemId(orderId: Int): (Int, Int) =
   orderId match {    
    case 0 => (1, 2)    
 }            
val (sellerId, itemId) = sellerAndItemId(0)
sellerId // sellerId: Int = 1
itemId // itemId: Int = 2        
val sellerItem = sellerAndItemId(0)
sellerItem._1 //res4: Int = 1
sellerItem._2 //res5: Int = 2

用模式匹配增加tuple可读性

val sampleList = List((1, 2, 3), (4, 5, 6), (7, 8, 9))
sampleList.map(x => s"${x._1}_${x._2}_${x._3}")
//res0: List[String] = List(1_2_3, 4_5_6, 7_8_9)
sampleList.map {    
  case (orderId, shopId, itemId) =>
    s"${orderId}_${shopId}_$itemId"
}    
//res1: List[String] = List(1_2_3, 4_5_6, 7_8_9)

上下两个map做了同样的事情,但下一个map为tuple中的三个值都给了名字,增加了代码的可读性.

match和java和switch很像,但有区别

  1. match是表达式,会返回值
  2. match不需要”break”
  3. 如果没有任何符合要求的case,match会抛异常,因为是表达式
  4. match可以匹配任何东西,switch只能匹配数字或字符串常量
//case如果是常量,就在值相等时匹配.
//如果是变量,就匹配任何值.
def describe(x: Any) = x match {
   case 5 => "five"    
   case true => "truth"    
   case "hello" => "hi!"    
   case Nil => "the empty list"
   case somethingElse => "something else " + somethingElse    
}    

case class,tuple以及列表都可以在匹配的同时捕获内部的内容.

case class Sample(a:String,b:String,c:String,d:String,e:String)
def showContent(x: Any) =
 x match {        
  case Sample(a,b,c,d,e) =>    
  s"Sample $a.$b.$c.$d.$e"    
  case (a,b,c,d,e) =>    
  s"tuple $a,$b,$c,$d,$e"    
  case head::second::rest =>    
  s"list head:$head second:$second rest:$rest"
}

Case class

  1. 模式匹配过程中其实调用了类的unapply方法
  2. Case class 是为模式匹配(以及其他一些方面)提供了特别的便利的类
  3. Case class 还是普通的class,但是它自动为你实现了apply,unapply,toString等方法
  4. 其实tuple就是泛型的case class

用 option 代替 null

null 的问题

Map<String, String> map = ???
String valFor2014 = map.get(“1024”); // null

if (valFor1024 == null)
    abadon();
else doSomething();
  • null到底代表key找不到还是说1024对应的值就是null?
  • 某年某月某日,我把为null则abandon这段代码写了100遍.

option介绍

  • option可以看作是一个容器,容器的size是1或0
  • Size为1的时候就是一个Some[A](x: A),size为0的时候就是一个None

看看scala的map

def get(key: A): Option[B]

def getOrElse[B1 >: B](key: A, default: => B1): B1 = get(key) match {
  case Some(v) => v
  case None => default
}
  • 可以区分Map中到底又没有这个key.
  • 我见过许多java项目自己实现了getOrElse这个方法并放在一个叫做MapUtils的类里.
  • 为什么java经过这么多代演进,Map仍然没有默认包含这个方法,一直想不通.

(写完这段突然发现java8开始包含getOrDefault了)

好像没有太大区别?

确实能够区分Map是无值还是值为null了.
但是if(为null) 则 abandon 要写一百遍.
case Some(v) => v
case None => default
似乎也得写一百遍.

不,不是这样的
不要忘了option是个容器
http://www.scala-lang.org/api...

试试容器里的各种方法

val a: Option[String] = Some("1024")
val b: Option[String] = None
a.map(_.toInt)
//res0: Option[Int] = Some(1024)
b.map(_.toInt)
//res1: Option[Int] = None,不会甩exception
a.filter(_ == "2048")
//res2: Option[String] = None
b.filter(_ == "2048")
//res3: Option[String] = None
a.getOrElse("2048")
//res4: String = 1024
b.getOrElse("2048")
//res5: String = 2048
a.map(_.toInt)
  .map(_ + 1)
  .map(_ / 5)
  .map(_ / 2 == 0) //res6: Option[Boolean] = Some(false)
//如果是null,恐怕要一连check abandon四遍了

option配合其他容器使用

val a: Seq[String] =
  Seq("1", "2", "3", null, "4")
val b: Seq[Option[String]] =
  Seq(Some("1"), Some("2"), Some("3"), None, Some("4"))

a.filter(_ != null).map(_.toInt)
//res0: Seq[Int] = List(1, 2, 3, 4)
//如果你忘了检查,编译器是看不出来的,只能在跑崩的时候抛异常
b.flatMap(_.map(_.toInt))
//res1: Seq[Int] = List(1, 2, 3, 4)
  • option帮助你把错误扼杀在编译阶段
  • flatMap则可以在过滤空值的同时将option恢复为原始数据.

scala原生容器类都对option有良好支持

Seq(1,2,3).headOption
//res0: Option[Int] = Some(1)

Seq(1,2,3).find(_ == 5)
//res1: Option[Int] = None

Seq(1,2,3).lastOption
//res2: Option[Int] = Some(3)

Vector(1,2,3).reduceLeft(_ + _)
//res3: Int = 6

Vector(1,2,3).reduceLeftOption(_ + _)
//res4: Option[Int] = Some(6)
//在vector为空的时候也能用

Seq("a", "b", "c", null, "d").map(Option(_))
//res0: Seq[Option[String]] =
// List(Some(a), Some(b), Some(c), None, Some(d))
//原始数据转换成option也很方便

用Try类保存异常

传统异常处理的局限性

try {
  1024 / 0
} catch {
  case e: Throwable => e.printStackTrace()
}

用try-catch的模式,异常必须在抛出的时候马上处理.
然而在分布式计算中,我们很可能希望将异常集中到一起处理,来避免需要到每台机器上单独看错误日志的窘态.

 val seq = Seq(0, 1, 2, 3, 4)
 //seq: Seq[Int] = List(0, 1, 2, 3, 4)

val seqTry = seq.map(x => Try {
  20 / x
})
//seqTry: Seq[scala.util.Try[Int]] = List(Failure(java.lang.ArithmeticException: devide by zero),Success(20), Success(10), Success(6), Success(5))

val succSeq = seqTry.flatMap(_.toOption)
//succSeq: Seq[Int] = List(20, 10, 6, 5) Try可以转换成Option
val succSeq2 = seqTry.collect {
  case Success(x) => x
}
//succSeq2: Seq[Int] = List(20, 10, 6, 5) 和上一个是一样的
val failSeq: Seq[Throwable] = seqTry.collect {
  case Failure(e) => e
}
//failSeq: Seq[Throwable] = List(java.lang.ArithmeticException: devide by zero)

Try实例可以序列化,并且在机器间传送.

函数是一等公民

一个需求

  • 假设我们需要检查许多的数字是否符合某一范围
  • 范围存储在外部系统中,并且可能随时更改
  • 数字范围像这样存储着”>= 3,< 7”

一个java版本

List<String> params = new LinkedList<>();
List<Integer> nums = new LinkedList<>();
List<String> marks = new LinkedList<>();

public JavaRangeMatcher(List<String> params) {
    this.params = params;
    for (String param : params) {
        String[] markNum = param.split(" ");
        marks.add(markNum[0]);
        nums.add(Integer.parseInt(markNum[1]));
    }
}

public boolean check(int input) {
    for (int i = 0; i < marks.size(); i++) {
        int num = nums.get(i);
        String mark = marks.get(i);
        if (mark.equals(">") && input <= num) return false;
        if (mark.equals(">=") && input < num) return false;
        if (mark.equals("<") && input >= num) return false;
        if (mark.equals("<=") && input > num) return false;
    }
    return true;
}

List<String> paramsList = new LinkedList<String>() {{
    add(“>= 3”);
    add(“< 7”);
}};
JavaRangeMatcher matcher = new JavaRangeMatcher(paramsList);
int[] inputs = new int[]{1, 3, 5, 7, 9};
for (int input : inputs) {
    System.out.println(matcher.check(input));
}
//给自己有限的时间,想想又没有性能优化的余地
//我们一起来跑跑看

一个 scala 版本

def exprToInt(expr: String): Int => Boolean = {
  val Array(mark, num, _*) = expr.split(" ")
  val numInt = num.toInt
  mark match {
    case "<" => numInt.>
    case ">" => numInt.<
    case ">=" => numInt.<=
    case "<=" => numInt.>=
  } //返回函数的函数
}

case class RangeMatcher(range: Seq[String]) {
  val rangeFunc: Seq[(Int) => Boolean] = range.map(exprToInt)

  def check(input: Int) = rangeFunc.forall(_(input))
}

def main(args: Array[String]) {
  val requirements = Seq(">= 3", "< 7")
  val rangeMatcher = RangeMatcher(requirements)
  val results = Seq(1, 3, 5, 7, 9).map(rangeMatcher.check)
  println(results.mkString(","))
  //false,true,true,false,false
}

关于性能

这里有一个性能测试网站

我对于网站测试的结果,我总结的情况就是两点.

  1. 排在后面的基本都是动态类型语言,静态类型语言相对容易优化到性能差不多的结果.
  2. 同一个语言代码写得好差产生的性能差异,远远比各种语言最好的代码性能差异大.

总的来说,程序员越自由,程序性能就越差

不过也有反例,我们之前那个程序就是.

//java版本
public static void main(String[] args) {
    List<String> paramsList = new LinkedList<String>() {{
        add(">= 3");
        add("< 7");
    }};
    JavaRangeMatcher matcher = new JavaRangeMatcher(paramsList);
    Random random = new Random();
    long timeBegin = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        int input = random.nextInt() % 10;
        matcher.check(input);
    }
    long timeEnd = System.currentTimeMillis();
    System.out.println("java 消耗时间: " + (timeEnd - timeBegin) + " 毫秒");
    //java 消耗时间: 3263 毫秒
}
//scala版本
def main(args: Array[String]) {
  val requirements = Seq(">= 3", "< 7")
  val rangeMatcher = RangeMatcher(requirements)
  val timeBegin = System.currentTimeMillis()
  0 until 100000000 foreach {
    case _ =>
      rangeMatcher.check(Random.nextInt(10))
  }
  val timeEnd = System.currentTimeMillis()
  println(s"scala 消耗时间 ${timeEnd - timeBegin} 毫秒")
  //scala 消耗时间 2617 毫秒
}

想想这是为什么?

推荐资源

  • 尽情地使用worksheet吧!
  • 尽情地用IDE查看标准库的源代码吧!
  • 推荐coursera上的课程progfunreactive
  • 尽情地查看文档,推荐软件Dash
查看原文

赞 10 收藏 62 评论 4

maemual 关注了问题 · 2015-06-30

解决怎么保证对外暴露接口的安全性(调用频率限制)

如何限制接口调用者对接口的调用频率?

问题:对某个对外暴露的接口加一个限制:调用者一分钟之内调用次数不能超过100次,如果超过100次就直接返回给调用者失败的信息。

  • 给调用者一个SECRET,每次调用者需要调用接口的时候,都需要把这个SECRET带过来(为了安全需要对key进行一系列加密的措施)

  • 一个SECRET就代表一个调用者,把相应的SECRET的调用次数放入缓存中(必须确保次数增加的原子性),并且把SECRET当做缓存的SECRET(这里如果区分方法的话,可以把方法和KEY做一次加密)。

这里主要的难点就是,如何判断调用者1分钟之内调用次数是否超过100?也就是很难确实这个1分钟的开始时间。

我现在的想法是:分别把当前秒调用的次数存入缓存。比如说,当前调用者调用次数为3,那么我就往缓存中加入KEY=SECRET_1,VALUE=3;然后调用者在第二秒调用的次数为4,那么就往缓存中加入KEY=SECRET_2,VALUE=3;如此循环,当循环到61秒的时候替换KEY=SECRET_1中得VAALUE,每次调用的时候计算SECRET_1~SECRET_60的值来判断调用次数,是否超过100次。(这里具体一秒钟调用几次,需要通过时间戳来算出是第几秒。这里以60秒为时间周期,并且以秒为一个时间单位,当然如果要求不是很准确的话,时间单位可以调大一点)

问题 请问有没有别的更好方法或者想法可以实现这个调用频率的限制?

Update:基于令牌桶的开放平台限流框架:limiter,持续开发中...

关注 51 回答 11

maemual 赞了回答 · 2015-06-17

解决为什么说https会http更安全呢?

如果你要访问163,不是直接到163的,需要中间有很多的服务器路由器的转发。

假设你是可信的,163.com也可信,但是中间的节点就未必。

比如

C:\Users\rita>tracert www.163.com

通过最多 30 个跃点跟踪
到 163.xdwscache.glb0.lxdns.com [220.167.14.40] 的路由:

  1     5 ms     2 ms     4 ms  100.64.0.1
  2     3 ms     3 ms     3 ms  100.64.0.1
  3     *        *        *     请求超时。
  4     7 ms     5 ms     7 ms  171.208.203.85
  5    12 ms     8 ms     8 ms  171.208.203.146
  6    15 ms    14 ms    15 ms  118.121.1.42
  7     9 ms    11 ms     9 ms  118.121.1.5
  8     9 ms     9 ms     9 ms  220.167.14.40

这8个节点,任何一个都可以串改你的信息和发给你的信息。

实际上运营商劫持就是这些节点中一些小子为了盈利而修改发给你的html,嵌入他的广告。

如果是https,这些节点个个都不知道你和163到底在发些啥,也没有办法改,因为秘钥(公用的私有的)在你的163之间才有。

所以,https防止的是中间节点使坏。这在我们的场景下,是非常好有价值的。

关注 24 回答 10

maemual 赞了回答 · 2015-06-10

解决选择http协议还是tcp协议

HTTP 是应用层协议,TCP 是传输层协议(位于应用层之下),放在一起类比并不合适。
不过猜测楼主是想对比 “标准 HTTP 协议” 还是 “自定义的协议(基于 TCP Socket)” 。

一般来说,移动应用推荐使用 HTTP 协议,有很多优点:
HTTP 发展成熟
HTTP 几乎已经快成为一种通用的 Web 标准,Web Services、REST、Open API、OAuth 等等都是基于 HTTP 协议的。它已经不仅仅是 Hyper Text 的传输标准了,几乎所有数据的传输(多媒体、XML、JSON)都可以采用 HTTP。
后台复用
因为很多应用,除了有移动端,还有Web端,甚至桌面端。
Web 版中前后台交互,无论是页面请求还是 AJAX 请求,都是采用标准 HTTP 协议。那么其他的客户端没有理由重新设计一套协议。
HTML 5 应用
现在不少移动产品都采用或者半采用 HTML 5 技术,那么和服务器的交互又回归到 AJAX 上。不用说,还是离不开 HTTP。
但是也有一些局限性,比如以下场景就不适合 HTTP 协议:
实时数据推送
除了 iOS 开发提供有标准的 Apple 消息推送中心,其他移动产品可能还是要采用 Socket 长连接才能保证实时通讯。
比较常见的有很多即时通讯软件采用的 XMPP 协议。
流媒体
适用于音频播放、视频播放、语音会议等等,一般可能采用 RTMP 协议。

Http 是 TCP的上层协议,Http 是基于 TCP的,所以你用了HTTP,等同与你也在用TCP

所以,拿Http和TCP做优劣比较是一个不存在的问题。

当然,这问题提的很好,问的是相较基于tcp的自定义协议。

其实事实上,从宏观层面,已经自己回答了这个问题了。

为啥要自定义协议呢?很简单啊,http协议满足不了需求只好自定义协议啊。
也就是说,自定义协议可以满足很多http协议满足不了的需求啊。
那什么需求是http协议满足不了的呢?
这也很简单啊,可以查一下http协议的定义去看看它提供了什么样的包装和定义,落在它之外的就是满足不了的啊,要真的细说,那真是多了去了,比如:

例如:http是单工阻塞性质的协议,如果你需要一个全双工,无阻塞的双向传输,那http就满足不了

例如:http定义提供了很多种的请求方法,从get到post不一一列举了,但是你需要的请求应答模式和它定义的种种没有任何一种能够实现你需要的请求应答模式,你就需要自定义协议啊

例如:http定义自己的包头,你要是觉得传输效率极其重要,这样的包头太臃肿,你也需要自定义协议啊

要是http都能完全满足你的需求,那为啥要自定义协议呢?一个成熟的协议拿来就用明显是很好的选择啊。

现在REST一出,一改过去SOAP的复杂臃肿,HTTP协议本身一直也在扩充,因此适用的范围更广,更好用了。需要自定义协议的场景和需求也变少了。

如果要从微观层面去对比优劣,至少你得告诉你这个自定义协议是啥?
TCP上的自定义协议,那可是多如繁星,我拿哪个去做对比呢?

TCP长链接是一直连着不断开的。如果是TCP的话:
服务器端不是很好扩充,考验单台服务器的接入能力。服务器集群不是很好架设。
客户端,处理socket连接的那个线程要负责干各种事情,所有网络协议的逻辑集中在此,结构不太好搭。而http,结构就完全不同。

区别在于开发代价不同。http有大量现成架构,服务器,数据库,出了问题也不会全盘崩溃,调试代价小。
tcp必须自定义协议,然后自己处理;自己实现服务器,监听端口;遇到问题,自己打造一系列调试手段。自己动手造轮子,开发代价高了一个数量级。

最近正好在用http协议,是接手之前一个人做的,没办法代码重写,基于socket自定义协议对于移动开发快速迭代不合适,除非是一些比较底层的需求。估计像微信这样的也许会自定义协议,要不然带宽负荷太高。但是具体我也不了解。

所以能用http的地方,就不要用tcp。不过有的东西必须用tcp,比如网游,那是没办法的事情。

HTTP 协议的一个非常重要的优势在于穿越防火墙。
如果客户端到服务器之间有安全设备,那么可能唯一打开的端口就是TCP:80。

移动端的开发更是如此,你不想用户整天抱怨说访问不到你的服务器吧。

这篇文章 介绍的很详细

顺便打个广告, 七牛是国内golang做后端服务开发的先驱, 上传 下载,数据处理接口全是走的是http协议

另外也可以混用,携程的这个实践很有参考意义

安卓很多框架都是基于http的,像 android-async-http, okhttp,AndroidAsync,volley ...等

综上,建议使用http 协议,除非你有很特殊的需求

关注 12 回答 4

maemual 赞了回答 · 2015-06-10

Markdown 一般在什么时候进行解析

输出时解析, 顺便如果有伪静态的话, 可以生成真正的静态文件, 这样下次就去请求生成的html了, 不再读数据库和走解析的流程, 更新数据的时候只需要删除原来生成的静态文件, 再次触发访问 就可以重新生成.
如果没有伪静态可以采取使用cache(最简单的文件形式的)的方式,访问时先检测cache目录中是否有对应的已经生成好的html文件,如果有就直接输出,当在后台更新数据时,删除cache目录中相应的文件,或者直接清空cache目录,下次访问时就会重新生成.

使用前端jsMarkdown的内容进行解析,需要浏览器去加载相应的js,由js去完成转换,中间需要相应的时间,且页面多加载需要解析Markdown的js文件(多了一个HTTP请求), 不如后端解析后生成相应的html文件,下次直接使用,来得实在.
且不用考虑浏览器是否支持js :)

关注 10 回答 5

认证与成就

  • 获得 9 次点赞
  • 获得 25 枚徽章 获得 0 枚金徽章, 获得 12 枚银徽章, 获得 13 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2012-12-19
个人主页被 229 人浏览