【四火】 你真的理解什么是全栈开发吗?

抱歉近几个月博客文章更新不够频繁,也有朋友问过,现在我想告诉大家,那是因为我写专栏去了。今天,专栏 《全栈工程师修炼指南》 终于上线了,下面的内容,就是我将这些年来,关于全栈开发,自己的一些经验、心得、感悟,总结起来,并且酝酿思考了很久,撰写的这个专栏。我想,目前市面上某一项具体技术的教程通常好找,但是系统的全栈技术关系树,包含这些技术之间的演进、权衡和本质介绍,并引发思考的学习材料却并不好找。 值得一提的是,这个专栏中我将 全程朗读所有的技术文章 ,为的就是能够尽可能地把原汁原味的技术内容传达给读者,希望你可以从中享受技术单纯原始的快乐。

下面的内容就是关于这个专栏的宣传介绍,你也可以直接拖动到文章底部,查看目录,点击 链接进入极客时间 购买,或扫描微信二维码购买。

 

提起“全栈工程师”,你最先想到的是什么?大神?全能?还是无用?

许多人对全栈的评价褒贬不一,不同人的理解也天差地别。有些人以为全栈是中小公司鼓吹的,有些人觉得大厂才招全栈,那么全栈究竟是做什么的?对于工程师而言,是全栈好,还是专注一个领域好?

我们先来看一个数据。下图来自 2018 Developer Skills Report,在开发者评价自己角色的时候,多数人投给了“全栈开发者”。

该如何学习成为一名全栈工程师?

很多人膜拜“全栈”,却在面对大量的技术栈时没有有效的学习路径和方法,尤其基于 Web 的全栈技术五花八门,涉及面广,迭代迅猛等等,我经常听到这样的困惑:

  • 想学 Web 全栈技术,期待能独立交付产品,但真的很迷茫;
  • 具体某项技术还好说,可全栈包含了那么多技术,怎么选?
  • 我该从哪里开始,遵循哪些原则,学习哪些技术?

为了帮大家解决这些问题,我在极客时间开了专栏 《全栈工程师修炼指南》,希望给你一条从碎片化到整体把握、清晰高效的学习路径,帮你系统掌握 Web 全栈的关键技术,真正从入门到技能实践。

我是谁?

我是熊燚,网上大家都叫我四火,现在在西雅图甲骨文(Oracle)的云计算部门就职,职位是首席软件工程师,负责云基础设施的分布式工作流引擎设计与开发,曾就职于华为、亚马逊(Amazon)。

最早我曾是华为某大型视频门户和视频平台的初创人员。后来加入了亚马逊,负责过数千万商品销量预测系统和成本利润计算平台的研发,重新设计并开发了数据分析和可视化系统,还维护和优化过数据分发的高可用服务,也改进过核算平台的分布式计算架构和工作流引擎。这些多领域的工作让我快速成长,并积累了大量的宝贵经验。

作为全栈工程的实践者,为了帮你更好的理解我所讲解的内容,特此给大家整理了一张「全栈开发核心知识框架图」,让你清晰地了解我们应该掌握的关键技术是什么。

我会如何讲解这个专栏?学完后能收获什么?

在专栏中,我会聚焦基于 Web 的全栈技术, 围绕“网络协议、MVC 架构、前端技术、持久层技术“等核心领域 ,梳理学习路径,对比剖析代表性技术,立足最佳实践、实战专题,带你从技术本质上理解、全面掌握全栈技能,培养“全栈高手思维”。

我在专栏中案例所用语言主要是 Java 和 JavaScript,由于全栈本身技术种类多、同类技术多的特点,专栏着重于讲原理、技术之间的演进、权衡和对本质的分析,并辅以非常多的实际项目和技术应用的案例。

  • 内容广度:我会选择每个核心领域的代表性技术来介绍,它们一定典型、常用,且深刻;
  • 内容深度:控制在合适的位置,让入门到进阶的工程师都有收获,我设计的“选修课堂”和“扩展阅读”,可以帮助你快速提升,一定不能略过。
  • 注重实践:我会引入最佳实践及自恰性强的专题,比如网站的性能优化、分页技术等,带你边学边做强化收获。

学习完后,希望你可以收获:

  1. 系统掌握 Web 全栈技能树
  2. 网络、前后端、持久化等核心技术解析
  3. 全栈开发的技术比较和选型
  4. 拓宽技术视野,培养全栈思维

1 分钟看看目录,你会发现你想要的

限时订阅福利

早鸟优惠¥68,原价¥99,你可以微信扫码购买,或者 点击此链接 进入极客时间购买。

这个世界需要专家,但更需要通晓各个层面知识,能够独立、快速解决问题的人。希望“全栈工程师”能成为你职业上升通道上的一个驿站,成为你的一个人生选择。

如果想领取高清版「全栈开发核心知识框架图 + 100 本架构师文集」,可以在「极客时间」公众号后台回复「全栈」领取。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 https://ift.tt/2ZQxYPd

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 闲话登月程序和程序员

2019年7月20日,是有纪念意义的一天,这天不是因为广大网民帮周杰伦在新浪微博上的超话刷到第一,而是阿波罗登月的50周年的纪念日。早在几年前,在Github上放出了当成Apollo飞船使用的源代码(当然是汇编的),但完全不明白为什么这几天会有一些中国的小朋友到这个github的issue里用灌水……,人类历史上这么伟大的一件事,为什么不借这个机会学习一下呢?下面是一些阿波罗登月与程序员相关的小故事,顺着这些东西,你可以把你的周末和精力用得更有价值。

首先,要说的是Apollo 11导航的源代码,这些代码的设计负责人叫Margaret Heafield Hamilton ,是一个女程序员,专业是数学和哲学,1960年得到一个MIT麻省理工大学的临时的软件开发职位,负责在PDP-1和LGP-30上运行天气预报的软件(注:在计算机历史上,PDP系统机器被称为Hack文化的重要推手,PDP-11且推了Unix操作系统,而Unix操作系统则是黑客文化的重要产品。参看《Unix传奇》)。然后,又为美国空军编写探测知敌方飞行的软件,之后,于1965年的时候,她加入了MIT仪器实验室,并成为了这个实验室的主管,这个实验实就是Apollo计划的一部分,她负责编写全新的月球登录的导航软件,以及后来该软件在其他项目中的各个版本。

上图是Hamilton站在她和她的麻省理工团队为阿波罗项目制作的导航软件源代码旁边,在Github上的开源的代码 – Apollo-11 (2016年开源)。我们可以看到,有两个重要的目录,一个目录叫“Comanche055”,一个目录叫“Luminary099”,前是指挥舱用的(英文为 Command Module )后者为登月舱用的(英文为 Lunar Module),这里需要说明一下的是,指挥舱是把登录舱推到月球上,在返回的时候,登录舱是被抛弃掉的,而返回到地球的是指挥舱。如果你想看这两份源代码的纸版,你可以访问这两个链接:Comanche 55 AGC Program ListingLuminary 99 REv.1 AGC Program Listing

如果你仔细比较一下这两个目录中的文件,你会发现有些文件是一样的,不但文件名一样,而且内容也一样。这说明这两个模块中的一些东西是相似的。

这些代码应该是很难读了,因为当时写这些代码的时候,C语言都没有被发明,所以基本上来说都是汇编代码了,而且还可以发现,这些代码的源文件全是以agc后缀结尾的, 看来这还不是我们平时所了解的汇编,所谓的AGC代表了运行这些代码的计算机 – Apollo Guideance Computer 。沿着这个Wikipedia的链接,你可以看到AGC这个电脑的指令是什么样的,看懂那几条指令后,这些源代码也就能读懂了。当然,因为是写成汇编的,所以,读起来还是要费点神的。不过,其中有一个文件是 THE_LUNAR_LANDING.apc 你会不会很好奇想去看看?

打开源文件,你还可以看到每个文件都有很多很多的注释,非常友好,但是也有一些注释比较有趣。这里有一组短视频带你读这些代码 – Exploring the Apollo Guidance Computer(AGC) Code,一供10个小视频,每个2分钟左右,如果你英文听边还行,可以看看,了解一下AGC的工作方式,挺有趣意思的。

下面是AGC在Apollo 1指挥舱里的样子(图片截自上面的视频),这个高质量的3D扫描来自 Simithsonian 3D: Apollo 11 Command Module

这个AGC的操作界又叫DSKY – Display 和 Keyboard的缩写,下图是一个 AGC 模拟器,其官方主页在 https://www.ibiblio.org/apollo/源代码在 Github/VirtualAGC。在这个界面上我们可以看到:下面的键盘上左边有两个键,一个是动词Verb一个是名词Noun,Verb指定操作类型,Noun指定要由Verb命令修改的数据。右边的显示器下面有三个5位的数字,这三个数值显示表示航天器姿态的矢量,以及所需速度变化的显示矢量。是的,当年的导航就靠这三个数字和里面的程序了。

 

如果你想了解AGC更多的细节,你可以看看 这篇 AGC for Dummies

另外,我在Youtube上找到了一个讲当时Apollo电脑的纪录片 – Navigation Computer,太有趣了。比如:21分51秒开始讲存储用的 Rope Memory 绕线内存,Hamilton 也出来讲了一下在这种内存上编程,画内切到一个人用个金属棍在一个像主板一样的东西上,左右穿梭,就像刺绣一样,但是绣的不是图案,而是程序……

看完上面这个纪录篇,我是非常之惊叹,惊叹于50年前的工程能力,惊叹于50年前这些人面对技术的的一丝不苟,对技术的尊重和严谨的这种精神和方法,一点都不比较今天差。

不过,最牛的还不是这个,我在Hamilton的Wikipedia词条上找到了他说的一个事件—— 当年Apollo登陆雷达开关放在了错误的位置,导致AGC收到了不少错误的信号。结果就是AGC既得执行着陆必须的计算,又要接受这些占用其15%时间的额外数据。但是AGC的程序居然可以用高优先级的任务打断低优先级的任务,于是,AGC自动剔除了低级别的任务以保证了重要的任务完成。Hamilton 原话说—— 如果当时的程序不能识别错误并从错误中恢复,我怀疑阿波罗不能成功登月。f the computer hadn’t recognized this problem and taken recovery action, I doubt if Apollo 11 would have been the successful moon landing it was。

看到这里,你有没有觉得——“这个女程序员的一小步,是整个人类的一大步”?

三年前,Apollo的源代码被开源时候,Twitter有个叫 Lin Clark 的人发了一条推:“我妈50年前的代码被放到Github上了”,是的,她是 Hamilton 的女儿,也是一个程序员,目前在 Mozilla工作,Staff Engineer,专长 WebAssembly, Rust, 和 JavaScript 。当她在Twitter上这么自豪地发了一条这样的推后,我不知道各位有什么想法?

 

尤其是那些到Apollo源代码的issue里灌水的人,你们是不是想让你们的孩子在登月100周年纪念的时候说——50年前我爹那个傻叉在Apollo的github的issue列表时灌了水?

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 July 21, 2019 at 07:00PM
from 酷 壳 – CoolShell https://ift.tt/2Z4Julw

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 如何超过大多数人

当你看到这篇文章的标题,你一定对这篇文章产生了巨大的兴趣,因为你的潜意识在告诉你,这是一篇“人生秘籍”,而且还是左耳朵写的,一定有很有参考意义,只要读完,一定可以找到超过大多数人的快车道和捷径……然而,当你看到我这样开篇时,你一定会觉得我马上就要有个转折,告诉你这是不可能的,一切都需要付出和努力……然而,你错了,这篇文章还真就是一篇“秘籍”,只要你把这些“秘籍”用起来,你就一定可以超过大多数人。而且,这篇文章只有我这个“人生导师”可以写得好。毕竟,我的生命过到了十六进制2B的年纪,踏入这个社会已超过20年,舍我其谁呢?!

P.S. 这篇文章借鉴于《如何写出无法维护的代码》一文的风格……嘿嘿

相关技巧和最佳实践

要超过别人其实还是比较简单的,尤其在今天的中国,更是简单。因为,你只看看中国的互联网,你就会发现,他们基本上全部都是在消费大众,让大众变得更为地愚蠢和傻瓜。所以,在今天的中国,你基本上不用做什么,只需要不使用中国互联网,你就很自然地超过大多数人了。当然,如果你还想跟他们彻底拉开,甩他们几个身位,把别人打到底层,下面的这些“技巧”你要多多了解一下。

在信息获取上,你要不断地向大众鼓吹下面的这些事:

  • 让大家都用百度搜索引擎查找信息,订阅微信公众号或是到知乎上学习知识……要做到这一步,你就需要把“百度一下”挂在嘴边,然后要经常在群或朋友圈中转发微信公众号的文章,并且转发知乎里的各种“如何看待……”这样的文章,让他们爱上八卦,爱上转发,爱上碎片。
  • 让大家到微博或是知识星球上粉一些大咖,密切关注他们的言论和动向……是的,告诉大家,大咖的任何想法一言一行都可以在微博、朋友圈或是知识星球上获得,让大家相信,你的成长和大咖的见闻和闲扯非常有关系,你跟牛人在一个圈子里你也会变牛。
  • 把今日头条和抖音这样的APP推荐给大家……你只需要让你有朋友成功地安装这两个APP,他们就会花大量的时间在上面,而不能自拔,要让他们安装其实还是很容易的,你要不信你就装一个试玩一会看看(嘿嘿嘿)。
  • 让大家热爱八卦,八卦并不一定是明星的八卦,还可以是你身边的人,比如,公司的同事,自己的同学,职场见闻,社会热点,争议话题,……这些东西总有一些东西会让人心态有很多微妙的变化,甚至花大量的时间去搜索和阅读大量的观点,以及花大量时间与人辩论争论,这个过程会让人上瘾,让人欲罢不能,然而这些事却和自己没有半毛钱关系。你要做的事就是转发其中一些SB或是很极端的观点,造成大家的一睦讨论后,就早早离场……
  • 利用爱国主义,让大家觉得不用学英文,不要出国,不要翻墙,咱们已经是强国了……这点其实还是很容易做到的,因为学习是比较逆人性的,所以,只要你鼓吹那些英文无用论,出国活得更惨,国家和民族都变得很强大,就算自己过得很底层,也有大国人民的感觉。

然后,在知识学习和技能训练上,让他们不得要领并产生幻觉

  • 让他们混淆认识和知识,以为开阔认知就是学习,让他们有学习和成长的幻觉……
  • 培养他们要学会使用碎片时间学习。等他们习惯利用碎片时间吃快餐后,他们就会失去精读一本书的耐性……
  • 不断地给他们各种各样“有价值的学习资料”,让他们抓不住重点,成为一个微信公众号或电子书“收藏家”……
  • 让他们看一些枯燥无味的基础知识和硬核知识,这样让他们只会用“死记硬背”的方式来学习,甚至直接让他们失去信心,直接放弃……
  • 玩具手枪是易用的,重武器是难以操控的,多给他们一些玩具,这样他们就会对玩具玩地得心应手,觉得玩玩具就是自己的专业……
  • 让他们喜欢直接得到答案的工作和学习方式,成为一个伸手党,从此学习再也不思考……
  • 告诉他们东西做出来就好了,不要追求做漂亮,做优雅,这样他们就会慢慢地变成劳动密集型……
  • 让他们觉得自己已经很努力了,剩下的就是运气,并说服他们去‘及时行乐’,然后再也找不到高阶和高效率学习的感觉……
  • 让他们觉得“读完书”、“读过书”就行了,不需要对书中的东西进行思考,进行总结,或是实践,只要囫囵吞枣尽快读完就等同于学好了……

最后,在认知和格局上,彻底打垮他们,让他们变成韭菜。

  • 让他们不要看到大的形势,只看到眼前的一亩三分地,做好一个井底之蛙。其实这很简单,比如,你不要让他们看到整个计算机互联网技术改变人类社会的趋势,你要多让他看到,从事这一行业的人有多苦逼,然后再说一下其它行业或职业有多好……
  • 宣扬一夜暴富以及快速挣钱的案例,最好让他们进入“赌博类”或是“传销类”的地方,比如:股市、数字货币……要让他们相信各种财富神话,相信他们就是那个幸运儿,他们也可以成为巴菲特,可以成为马云……
  • 告诉他们,一些看上去很难的事都是有捷径的,比如:21天就能学会机器学习,用区块链就能颠覆以及重构整个世界等等……
  • 多跟他们讲一些小人物的励志的故事,这样让他们相信,不需要学习高级知识,不需要掌握高级技能,只需要用低等的知识和低级的技能,再加上持续不断拼命重复现有的工作,终有一天就会成功……
  • 多让他们跟别人比较,人比人不会气死人,但是会让人变得浮躁,变得心急,变得焦虑,当一个人没有办法控制自己的情绪,没有办法让自己静下心来,人会失去耐性和坚持,开始好大喜欢功,开始装逼,开始歪门邪道剑走偏锋……
  • 让他们到体制内的一些非常稳定的地方工作,这样他们拥有不思进取、怕承担责任、害怕犯错、喜欢偷懒、得过且过的素质……
  • 让他们到体制外的那些喜欢拼命喜欢加班的地方工作,告诉他们爱拼才会赢,努力加班是一种福报,青春就是用来拼的,让他们喜欢上使蛮力的感觉……
  • 告诉他们你的行业太累太辛苦,干不到30岁。让他们早点转行,不要耽误人生和青春……
  • 当他们要做决定的时候,一定要让他们更多的关注自己会失去的东西,而不是会得到的东西。培养他们患得患失心态,让他们认识不到事物真正的价值,失去判断能力……(比如:让他们觉得跟对人拍领导的马屁忠于公司比自我的成长更有价值)
  • 告诉他们,你现有的技能和知识不用更新,就能过好一辈子,新出来的东西没有生命力的……这样他们就会像我们再也不学习的父辈一样很快就会被时代所抛弃……
  • 每个人都喜欢在一些自己做不到的事上找理由,这种能力不教就会,比如,事情太多没有时间,因为工作上没有用到,等等,你要做的就是帮他们为他们做不到的事找各种非常合理的理由,比如:没事的,一切都是最好的安排;你得不到的那个事没什么意思;你没有面好主要原因是那个面试官问的问题都是可以上网查得到的知识,而不没有问到你真正的能力上;这些东西学了不用很快会忘了,等有了环境再学也不迟……

最后友情提示一下,上述的这些“最佳实践”你要小心,是所谓,贩毒的人从来不吸毒,开赌场的人从来不赌博!所以,你要小心别自己也掉进去了!这就是“欲练神功,必先自宫”的道理。

相关原理和思维模型

对于上面的这些技巧还有很多很多,你自己也可以发明或是找到很多。所以,我来讲讲这其中的一些原理。

一般来说,超过别人一般来说就是两个维度:

  1. 在认知、知识和技能上。这是一个人赖以立足社会的能力(参看《程序员的荒谬之言还是至理名言?》和《21天教你学会C++》)
  2. 在领导力上。所谓领导力就是你跑在别人前面,你得要有比别人更好的能力更高的标准(参看《技术人员发展之路》)

首先,我们要明白,人的技能是从认识开始,然后通过学校、培训或是书本把“零碎的认知”转换成“系统的知识”,而有要把知识转换成技能,就需要训练和实践,这样才能完成从:认识 -> 知识 -> 技能 的转换。这个转换过程是需要耗费很多时间和精力的,而且其中还需要有强大的学习能力和动手能力,这条路径上有很多的“关卡”,每道关卡都会过滤掉一大部分人。比如:对于一些比较枯燥的硬核知识来说,90%的人基本上就倒下来,不是因为他们没有智商,而是他们没有耐心。

认知

要在认知上超过别人,就要在下面几个方面上做足功夫:

1)信息渠道。试想如果别人的信息源没有你的好,那么,这些看不见信息源的人,只能接触得到二手信息甚至三手信息,只能获得被别人解读过的信息,这些信息被三传两递后必定会有错误和失真,甚至会被传递信息的中间人hack其中的信息(也就是“中间人攻击”),而这些找不出信息源的人,只能“被人喂养”,于是,他们最终会被困在信息的底层,永世不得翻身。(比如:学习C语言,放着原作者K&R的不用,硬要用错误百出谭浩强的书,能有什么好呢?)

2)信息质量。信息质量主要表现在两个方面,一个是信息中的燥音,另一个是信息中的质量等级,我们都知道,在大数据处理中有一句名言,叫 garbage in garbage out,你天天看的都是垃圾,你的思想和认识也只有垃圾。所以,如果你的信息质量并不好的话,你的认知也不会好,而且你还要花大量的时间来进行有价值信息的挖掘和处理。

3)信息密度。优质的信息,密度一般都很大,因为这种信息会逼着你去干这么几件事,a)搜索并学习其关联的知识,b)沉思和反省,c)亲手去推理、验证和实践……一般来说,经验性的文章会比知识性的文章会更有这样的功效。比如,类似于像 Effiective C++/Java,设计模式,Unix编程艺术,算法导论等等这样的书就是属于这种密度很大的书,而像Netflix的官方blogAWS CTO的blog等等地方也会经常有一些这样的文章。

知识

要在知识上超过别人,你就需要在下面几个方面上做足功夫:

1)知识树(图)。任何知识,只在点上学习不够的,需要在面上学习,这叫系统地学习,这需要我们去总结并归纳知识树或知识图,一个知识面会有多个知识板块组成,一个板块又有各种知识点,一个知识点会导出另外的知识点,各种知识点又会交叉和依赖起来,学习就是要系统地学习整个知识树(图)。而我们都知道,对于一棵树来说,“根基”是非常重要的,所以,学好基础知识也是非常重要的,对于一个陌生的地方,有一份地图是非常重要的,没有地图的你只会乱窜,只会迷路、练路、走冤枉路!

2)知识缘由。任何知识都是有缘由的,了解一个知识的来龙去脉和前世今生,会让你对这个知识有非常强的掌握,而不再只是靠记忆去学习。靠记忆去学习是一件非常糟糕的事。而对于一些操作性的知识(不需要了解由来的),我把其叫操作知识,就像一些函数库一样,这样的知识只要学会查文档就好了。能够知其然,知其所以然的人自然会比识知识到表皮的人段位要高很多。

3)方法套路。学习不是为了找到答案,而是找到方法。就像数学一样,你学的是方法,是解题思路,是套路,会用方程式解题的和不会用方程式解题的在解题效率上不可比较,而在微积分面前,其它的解题方法都变成了渣渣。你可以看到,掌握高级方法的人比别人的优势有多大,学习的目的就是为了掌握更为高级的方法和解题思路

技能

要在技能上超过别人,你就需要在下面几个方面做足功夫:

1)精益求精。如果你想拥有专业的技能,你要做不仅仅是拼命地重复一遍又一遍的训练,而是在每一次重复训练时你都要找到更好的方法,总结经验,让新的一遍能够更好,更漂亮,更有效率,否则,用相同的方法重复,那你只不过在搬砖罢了。

2)让自己犯错。犯错是有利于成长的,这是因为出错会让人反思,反思更好的方法,反思更完美的方案,总结教训,寻求更好更完美的过程,是技能升级的最好的方式。尤其是当你在出错后,被人鄙视,被人嘲笑后,你会有更大的动力提升自己,这样的动力才是进步的源动力。当然,千万不要同一个错误重复地犯!

3)找高手切磋。下过棋,打个球的人都知道,你要想提升自己的技艺,你必需找高手切磋,在和高手切磋的过程中你会感受到高手的技能和方法,有时候你会情不自禁地哇地一下,我靠,还可以这么玩!

领导力

最后一个是领导力,要有领导力或是影响力这个事并不容易,这跟你的野心有多大,好胜心有多强 ,你愿意付出多少很有关系,因为一个人的领导力跟他的标准很有关系,因为有领导力的人的标准比绝大多数人都要高。

1)识别自己的特长和天赋。首先,每个人DNA都可能或多或少都会有一些比大多数人NB的东西(当然,也可能没有),如果你有了,那么在你过去的人生中就一定会表现出来了,就是那种大家遇到这个事会来请教你的寻求你帮助的现象。那种,别人要非常努力,而且毫不费劲的事。一旦你有了这样的特长或天赋,那你就要大力地扩大你的领先优势,千万不要进到那些会限制你优势的地方。你是一条鱼,你就一定要把别人拉到水里来玩,绝对不要去陆地上跟别人拼,不断地在自己的特长和天赋上扩大自己的领先优势,彻底一骑绝尘。

2)识别自己的兴趣和事业。没有天赋也没有问题,还有兴趣点,都说兴趣是最好的老师,当年,Linus就是在学校里对minx着迷了,于是整出个Linux来,这就是兴趣驱动出的东西,一般来说,兴趣驱动的事总是会比那些被动驱动的更好。但是,这里我想说明一下什么叫“真∙兴趣”,真正的兴趣不是那种三天热度的东西,而是那种,你愿意为之付出一辈子的事,是那种无论有多大困难有多难受你都要死磕的事,这才是“真∙兴趣”,这也就是你的“野心”和“好胜心”所在,其实上升到了你的事业。相信我,绝大多数人只有职业而没有事业的。

3)建立高级的习惯和方法。没有天赋没有野心,也还是可以跟别人拼习惯拼方法的,只要你有一些比较好的习惯和方法,那么你一样可以超过大多数人。对此,在习惯上你要做到比较大多数人更自律,更有计划性,更有目标性,比如,每年学习一门新的语言或技术,并可以参与相关的顶级开源项目,每个月训练一个类算法,掌握一种算法,每周阅读一篇英文论文,并把阅读笔记整理出来……自律的是非常可怕的。除此之外,你还需要在方法上超过别人,你需要满世界的找各种高级的方法,其中包括,思考的方法,学习的方法、时间管理的方法、沟通的方法这类软实力的,还有,解决问题的方法(trouble shooting 和 problem solving),设计的方法,工程的方法,代码的方法等等硬实力的,一开始照猫画虎,时间长了就可能会自己发明或推导新的方法。

4)勤奋努力执着坚持。如果上面三件事你都没有也没有能力,那还有最后一件事了,那就是勤奋努力了,就是所谓的“一万小时定律”了(参看《21天教你学会C++》中的十年学编程一节),我见过很多不聪明的人,悟性也不够(比如我就是一个),别人学一个东西,一个月就好了,而我需要1年甚至更长,但是很多东西都是死的,只要肯花时间就有一天你会搞懂的,耐不住我坚持十年二十年,聪明的人发明个飞机飞过去了,笨一点的人愚公移山也过得去,因为更多的人是懒人,我不用拼过聪明人,我只用拼过那些懒人就好了。

好了,就这么多,如果哪天你变得消极和不自信,你要来读读我的这篇文章,子曰:温故而知新。

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 June 22, 2019 at 01:47PM
from 酷 壳 – CoolShell http://bit.ly/2Y5Op5d

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 分析运行中的 Python 进程

在 Java 中打印当前线程的方法栈,可以用 kill -3 命令向 JVM 发送一个 OS 信号,JVM 捕捉以后会自动 dump 出来;当然,也可以直接使用 jstack 工具完成,这些方法好几年前我在 这篇性能分析的文章 中介绍过。这样的需求可以说很常见,比如定位死锁,定位一个不工作的线程到底卡在哪里,或者定位为什么 CPU 居高不下等等问题。

现在工作中我用的是 Python,需要线上问题定位的缘故,也有了类似的需求——想要知道当前的 Python 进程“在干什么”。但是没有了 JVM 的加持,原有的命令或者工具都不再适用。传统的 gdb 的 debug 大法在线上也不好操作。于是我寻找了一些别的方法,来帮助定位问题,我把它们记录在这里。

signal

在代码中,我们可以使用 signal 为进程预先注册一个信号接收器,在进程接收到特定信号的时候,可以打印方法栈:

import traceback, signal

class Debugger():
    def __init__(self, logger):
        self._logger = logger
    
    def log_stack_trace(self, sig, frame):
        d={'_frame':frame}
        d.update(frame.f_globals)
        d.update(frame.f_locals)
    
        messages  = "Signal received. Stack trace:\n"
        messages += ''.join(traceback.format_stack(frame))
        self._logger.warn(messages)
    
    def listen(self):
        signal.signal(signal.SIGUSR1, self.log_stack_trace)

通过调用上面的 listen 方法(比如 new Debug(logger).listen()),就将一个可以接收 SIGUSR1 并打印方法栈的接收器注册到当前进程了。

那么怎么向进程发送信号呢?和 JVM 的方法类似,可以通过操作系统命令来发送:

kill -30 pid

这里的信号为什么是 30?这是因为 SIGUSR1 被当前操作系统定义成 30(请注意不同的操作系统这个映射表是可能不同的),这点可以通过 man signal 查看:

No Name Default Action Description
1 SIGHUP terminate process terminal line hangup
2 SIGINT terminate process interrupt program
3 SIGQUIT create core image quit program
4 SIGILL create core image illegal instruction
5 SIGTRAP create core image trace trap
6 SIGABRT create core image abort program (formerly SIGIOT)
7 SIGEMT create core image emulate instruction executed
8 SIGFPE create core image floating-point exception
9 SIGKILL terminate process kill program
10 SIGBUS create core image bus error
11 SIGSEGV create core image segmentation violation
12 SIGSYS create core image non-existent system call invoked
13 SIGPIPE terminate process write on a pipe with no reader
14 SIGALRM terminate process real-time timer expired
15 SIGTERM terminate process software termination signal
16 SIGURG discard signal urgent condition present on socket
17 SIGSTOP stop process stop (cannot be caught or ignored)
18 SIGTSTP stop process stop signal generated from keyboard
19 SIGCONT discard signal continue after stop
20 SIGCHLD discard signal child status has changed
21 SIGTTIN stop process background read attempted from control terminal
22 SIGTTOU stop process background write attempted to control terminal
23 SIGIO discard signal I/O is possible on a descriptor (see fcntl(2))
24 SIGXCPU terminate process cpu time limit exceeded (see setrlimit(2))
25 SIGXFSZ terminate process file size limit exceeded (see setrlimit(2))
26 SIGVTALRM terminate process virtual time alarm (see setitimer(2))
27 SIGPROF terminate process profiling timer alarm (see setitimer(2))
28 SIGWINCH discard signal Window size change
29 SIGINFO discard signal status request from keyboard
30 SIGUSR1 terminate process User defined signal 1
31 SIGUSR2 terminate process User defined signal 2

当然,也可以写一点点 python 脚本来发送这个信号:

import os, signal
os.kill($PID, signal.SIGUSR1)

原理是一样的。

strace

如果进程已经无响应了,或者上面的信号接收器没有注册,那么就要考虑别的方法来或者“进程在干什么”这件事情了。其中,一个有用的命令是 strace:

strace -p pid

比如,我自己写了一个测试脚本 t.py,使用 python 执行,然后调用 sleep,再给它发送一个 SIGUSR1 的消息,它打印方法栈并退出。这整个过程,我使用 strace 可以得到这样的结果:

strace -p 9157
strace: Process 9157 attached
select(0, NULL, NULL, NULL, {9999943, 62231}) = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=9273, si_uid=9007} ---
rt_sigreturn({mask=[]})                 = -1 EINTR (Interrupted system call)
stat("t.py", {st_mode=S_IFREG|0644, st_size=1281, ...}) = 0
open("t.py", O_RDONLY)                  = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=1281, ...}) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=1281, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f631e866000
read(3, "import traceback, signal, time\n "..., 8192) = 1281
read(3, "", 4096)                       = 0
close(3)                                = 0
munmap(0x7f631e866000, 4096)            = 0
stat("t.py", {st_mode=S_IFREG|0644, st_size=1281, ...}) = 0
write(1, "Signal received. Stack trace:\n  "..., 134) = 134
write(1, "\n", 1)                       = 1
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f631e06f5d0}, {0x7f631e392680, [], SA_RESTORER, 0x7f631e06f5d0}, 8) = 0
rt_sigaction(SIGUSR1, {SIG_DFL, [], SA_RESTORER, 0x7f631e06f5d0}, {0x7f631e392680, [], SA_RESTORER, 0x7f631e06f5d0}, 8) = 0
exit_group(0)                           = ?
+++ exited with 0 +++

可以看到从 strace attached 开始,到进程退出,所有重要的调用都被打印出来了。

在 iOS 下,没有 strace,但是可以使用类似的(更好的)命令 dtruss。

lsof

lsof 可以打印某进程打开的文件,而 Linux 下面一切都是文件,因此查看打开的文件列表有时可以获取很多额外的信息。比如,打开前面提到的这个测试进程:

lsof -p 16872
COMMAND   PID  USER   FD   TYPE DEVICE   SIZE/OFF     NODE NAME
Python  16872 xxx  cwd    DIR    1,5       2688  1113586 /Users/xxx
Python  16872 xxx  txt    REG    1,5      51744 10627527 /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
Python  16872 xxx  txt    REG    1,5      52768 10631046 /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/_locale.so
Python  16872 xxx  txt    REG    1,5      65952 10631134 /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/time.so
Python  16872 xxx  txt    REG    1,5     841440 10690598 /usr/lib/dyld
Python  16872 xxx  txt    REG    1,5 1170079744 10705794 /private/var/db/dyld/dyld_shared_cache_x86_64h
Python  16872 xxx    0u   CHR   16,2    0t39990      649 /dev/ttys002
Python  16872 xxx    1u   CHR   16,2    0t39990      649 /dev/ttys002
Python  16872 xxx    2u   CHR   16,2    0t39990      649 /dev/ttys002

它有几个参数很常用,比如-i,用来指定网络文件(如果是“-i: 端口号”这样的形式还可以指定端口)。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2Xpz0zB

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 谈谈 Ops(最终篇):工具和实践

除了主要内容——工具和实践,这篇文章也对“谈谈 Ops”系列做一个汇总,提供一个访问入口。之前几篇,从一个纯粹 dev 狭窄的视角,谈了谈自己对 Ops 的一些认识:

在往下继续以前,如果没有看过前面的文字,不妨移步阅读,因为上面的内容对下面的内容做了一定程度的铺垫。

现在在写的这一篇文字,我准备是最后一篇,主要谈论这样几个事情:一个是工具,另一个是实践。我依然还是从 dev 的视角,而不是从一个专业运维的视角来记叙。

工欲善其事,必先利其器。我在主要且通用的工具中挑了几个,和最佳实践放在一起介绍,并且按照功能和阶段来划分,而不执着于列出具体的工具名称。 其中最主要的阶段,包括开发阶段,集成测试阶段,以及线上部署维护阶段。 顺便也再强调一次,Ops 远不只有线上系统的维护。

Pipeline

把 Pipeline 放在最先讲,是因为它是集成下面各路神仙(工具)的核心,通过 pipeline,可以把一系列自动化的工具结合起来,而这一系列工具,往往从编译过程就开始,到部署后的验证执行结束。

Pipeline 最大的作用是对劳动力的解放,程序来控制代码从版本库到线上的行进流程,而非人。因此,对于那些一个 pipeline 上面设置了数个需要人工审批的暂停点,使得这样的事情失去了意义。这样的罪过往往来自于那些缺少技术背景的负责人,其下反映的是对流程的崇拜(前面关于流程和人的文章已经提到),而更进一步的原因是不懂技术,就无法相信代码,进而无法信任程序员,他们觉得,只有把生杀大权掌握在自己手里,才能得到对质量最好的控制。这样的问题从二十多年前的软件流程中就出现了,到现在愈演愈烈。我只能说,这无疑是一种悲哀。

我见过懂技术的老板,也见过不怎么懂技术的老板,还见过完全不懂技术的老板。但是最可怕的,是那种不太懂或完全不懂,却又非常想要插一手,对程序员在软件流程方面指手画脚的老板。为什么是流程?因为技术方面他们不懂,没法插手,却又觉得失去了掌控,只好搞流程了。

我曾经遇到过这样一件事情:程序有一个 bug,因为在一个判断中,状态集合中少放了一个枚举值,导致了一个严重的线上问题。后来,程序员修正了问题,老板和程序员有了这样的对话:

老板:“你怎么保证未来的发布不会有这样的问题?”

程序员:“我修正了啊。”

老板:“我怎么知道你修正了?”

程序员:“我发布了代码改动,我使用单元测试覆盖了改动。”

老板:“好,我相信你开发机的代码改动做了。可我怎么知道你发布到线上的版本没有问题?”

程序员:“……发布的代码就是我提交的啊。”

老板:“你怎么能保证代码从你提交到线上发布的过程中没有改动?”

程序员:“……(心中一千头草泥马奔腾而过)我可以到线上发布的 Python 包里面查看一下该行是不是已经得到修改。”

老板:“好。我们能不能在 pipeline 里面,添加这样一个步骤——执行部署的人到发布包里面去检查该行代码是不是正确的。”

程序员:“……(现在变成一万头了)不要把,这样一个额外的检查会浪费时间啊。”

老板:“检查一下需要多少时间?”

程序员:“5 分钟吧”

老板:“花费 5 分钟,避免一个严重的问题,难道不值得吗?”

程序员:“……(现在数不清多少头了)如果这一行要校对的话,为什么其它几十万行代码不用肉眼校对?”

老板:“就这一行需要。因为这一行代码曾经引发过严重问题,所以需要。”

程序员:“……”

如果你也见过这样的情形,不妨告诉我你的应对办法是什么。

依赖管理

以 Java 为例,有个搞笑的说法是“没有痛不欲生地处理过 Jar 包冲突的 Java 程序员不是真正的 Java 程序员”,一定程度上说明了依赖管理有多重要。尤其是茁壮发展的 Java 社区,副作用就是版本多如牛毛,质量良莠不齐,包和类的命名冲突简直是家常便饭。我用过几个依赖管理的工具,比如 Python 的 pip,比如 Java 的 Maven,但是最好的还是 Amazon 内部的那一个,很可惜没有开源。注意这里说的依赖,即便对于 Java 来说,也不一定是 Jar 包,可以是任何文件夹和文件。一个好的依赖管理的工具,有这么几点核心特性需要具备:

  1. 支持基于包和包组的依赖配置。目标软件可以依赖于配置的 Jar 包,而若干个 Jar 包也可以配置成一个组来简化依赖配置。
  2. 支持基于版本的递归依赖。比如 A 依赖于 B,B 依赖于 C,那么只需要在 A 的依赖文件中配置 B,C 就会被自动引入。
  3. 支持版本冲突的选择。比如 A 依赖于 B 和 C,B 依赖于 D 1.0,C 依赖于 D 2.0,那么通过配置可以选择在最终引入依赖的时候引入 D 1.0 还是 2.0。
  4. 支持不同环境的不同依赖配置,比如编译期的依赖,测试期的依赖和运行期的依赖都可能不一样。

当然,还有许许多多别的特性,比如支持冲突包的删除等等,只是没有那么核心。

自动化测试

代码检查、编译和单元测试(Unit Test)。这一步还属于代码层面的行为活动,代码库特定分支上的变动,触发这一行为,只有在这样的执行成功以后,后面的步骤才能得到机会运行。单元测试通常都是是 dev 写的,即便是在有独立的测试团队的环境中。因为单元测试重要的一个因素就是要保证它能够做到白盒覆盖。单元测试要求易于执行,由于需要反复执行和根据结果修改代码,快速的反馈是非常重要的,几十秒内必须得到结果。我见过有一些团队的单元测试跑一遍要十分钟以上,那么这种情况就要保证能够跑增量的测试,换言之,改动了什么内容,能够重跑改动的那一部分,而不是所有的测试集合。

集成测试(Integration Test)。这一步最主要的事情,就是自动部署代码到一个拟真的环境,之行端到端的测试。比如说,发布的产品是远程的 API,UT 关注的是功能单元,测试的对象是具体的类和方法;而在 IT 中,更关心暴露的远程接口,既包括功能,也包括性能。集成测试的成熟程度,往往是一个项目质量的一个非常好的体现。在某些团队中,集成测试通过几个不同的环境来完成,比如α环境、β环境、γ环境等等,依次递进,越来越接近生产环境。比如α环境是部署在开发机上的,而γ环境则是线上环境的拷贝,连数据库的数据都是从线上定期同步而来的。

冒烟测试(Smoke Testing)。冒烟测试最关心的不是功能的覆盖,而是对重要功能,或者核心功能的保障。到了这一步,通常在线上部署完成后,为了进一步确保它是一次成功的部署,需要有快速而易于执行的测试来覆盖核心测试用例。这就像每年的常规体检,不可能事无巨细地做各种各样侵入性强的检查,而是通过快速的几项,比如血常规、心跳、血压等等来执行核心的几项检查。在某些公司,冒烟测试还被称作“Sanity Test”,从字面意思也可以得知,测试的目的仅仅是保证系统“没有发疯”。除了功能上的快速冒烟覆盖,在某些系统中,性能是一个尤其重要的关注点,那么还会划分出 Soak Testing 这样的针对性能的测试来,当然,它对系统的影响可能较大,有时候不会部署在生产环境,而是在前面提到的镜像环境中。

部署工具

曾经使用过各种用于部署的工具,有开源的,也有内部开发的。这方面以前写过 Ant 脚本,在华为有内部工具;在 Amazon 也有一个内部工具,它几乎是我见过的这些个中,最强大,而且自动化程度最高的。部署工具我认为必须具备的功能包括:

  • 自动下载并同步指定版本的文件系统到环境中。这是最最基本的功能,没有这个谈不上部署工具。
  • 开发、测试、线上环境同质化。这指的是通过一定程度的抽象,无论软件部署到哪里,对程序员来说都是一样的,可以部署在开发机(本地)用于开发调试,可以部署到测试环境,也可以部署到线上环境。
  • 快速的本地覆盖和还原。这个功能非常有用。对于一个软件环境来说,可能 1000 个文件都不需要修改,但是又 3 个文件是当前我正在开发的文件,这些文件的修改需要及时同步到环境中去,以便得到快速验证。这个同步可能是本地的,也可能是远程的。比如我曾经把开发环境部署在云上,因为云机器的性能好,但是由于是远程,使用 GUI 起来并不友好,于是我采用的办法是在本地写代码,但是代码通过工具自动同步到云机器上。我也尝试过一些其他的同步场景,比如把代码 自动同步到本地虚拟机上
  • 环境差异 diff。这个功能也非常有用。开发人员很喜欢说的一句话是,“在本地没问题啊?”,因此如果这个工具可以快速比较两个环境中文件的不同,可以帮助找到环境差异,从而定位问题。

当然,还有其它很有用的功能,我这里只谈了一些印象深刻的。

监控工具

我工作过的三家公司,华为、Amazon,还是 Oracle,它们的监控工具各有特点,但做得都非常出色。且看如下的功能:

  • 多维度、分级别、可视化的数据统计和监控。核心性能的统计信息既包括应用的统计信息,包括存储,比如数据库的统计信息,还包括容器(比如 docker)或者是 host 机器本身的统计信息。监控信息的分级在数据量巨大的时候显得至关重要,信息量大而缺乏组织就是没有信息。通常,有一个主 dashboard,可以快速获知核心组件的健康信息,这个要求在一屏以内,以便可以一眼就得到。其它信息可以在不同的子 dashboard 中展开。
  • 基于监控信息的自动化操作。最常见的例子就是告警。CPU 过高了要告警、IO 过高了要告警、失败次数超过阈值要告警。使用监控工具根据这些信息可以很容易地配置合理的告警规则,要做一个完备的告警系统,规则可以非常复杂。告警和上面说的监控一样,也要分级。小问题自动创建低优先级的问题单,大问题创建高优先级的问题单,紧急问题电话、短信 page oncall。
  • 告警模块的系统化定义和重用。在上面说到这些复杂的需求的时候,如果一切都从头开始做无疑是非常耗时费力的。因而和软件代码需要组织和重构一样,告警的配置和规则也是。

对于其它的工具,比如日志工具,安全工具,审计工具,我这里不多叙述了。这并非是说它们不必须。

糟糕的实践

上面是我的理解,但是结合这些工具,我相信每个有追求的程序员,都对 Ops 的最佳实践有着自己的理解。于是,有一些实践在我看来,是非常糟糕的,它们包括:

  • SSH+命令/脚本。这大概是最糟糕的了,尤其是线上的运维,在实际操作中,一定是最好更相信工具,而不是人。如果没有工具,只能手工操作,只能使用命令+脚本来解决问题,于是各种吓人的误操作就成了催命符。你可以看看 《手滑的故事》,我相信很多人都经历过。最好的避免这样事情发生的方式是什么?限制权限?层层审批?都不是,最好的方式是自动化。人工命令和脚本的依赖程度和 Ops 的成熟度成逆相关。
  • 流程至上。这里我不是否认流程的作用,我的观点在 这篇文章 中已经说过了。其中一个最典型的操作就是堆人,发现问题了,就靠加人,增加一环审批来企图避免问题。
  • 英雄主义。这是很多公司的通病,一个写优质代码的工程师不会起眼,只有埋 bug 造灾难,再挺身而出力挽狂澜,从而拯救线上产品的“英雄”才受人景仰。正所谓,没有困难制造困难也要上。
  • 背锅侠。这和上面的英雄主义正好相反,却又相辅相成。找运维不规范操作背锅(可事实呢,考虑到复杂性、枯燥性等原因,几乎没法“规范”操作,人都是有偷懒和走捷径的本性的),找开发埋地雷,测试漏覆盖背锅。当场批评,事后追责。
  • 用户投诉驱动开发,线上事故驱动开发。这一系列通过糟糕的结果来反向推动的运维反馈开发的方式(其它各种奇葩的驱动开发方式,看 这里)。
  • 把研发的时间精力投入 ops。这是恶性循环最本质的一条, 没时间做好需求分析,没时间做好设计,没时间做好测试,没时间写好代码,什么都没时间,因为全都去 Ops 解线上问题去了 。结果呢,糟糕的上游造就了更糟糕的下游,问题频出,于是更多的人花更多的人去 ops。如此恶性循环……

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2Zv5beC

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【喵神】 SwiftUI 的一些初步探索 (二)

我已经计划写一本关于 SwiftUI 和 Combine 编程的书籍,希望能通过一些实践案例帮助您快速上手 SwiftUI 及 Combine 响应式编程框架,掌握下一代客户端 UI 开发技术。现在这本书已经开始预售,预计能在 10 月左右完成。如果您对此有兴趣,可以查看 ObjC 中国的产品页面了解详情及购买。十分感谢!

上一篇继续对 SwiftUI 的教程进行一些解读。

教程 2 – Building Lists and Navigation

Section 4 – Step 2: 静态 List

var body: some View {
    List {
        LandmarkRow(landmark: landmarkData[0])
        LandmarkRow(landmark: landmarkData[1])
    }
}

这里的 ListHStack 或者 VStack 之类的容器很相似,接受一个 view builder 并采用 View DSL 的方式列举了两个 LandmarkRow。这种方式构建了对应着 UITableView 的静态 cell 的组织方式。

public init(content: () -> Content)

我们可以运行 app,并使用 Xcode 的 View Hierarchy 工具来观察 UI,结果可能会让你觉得很眼熟:

实际上在屏幕上绘制的 UpdateCoalesingTableView 是一个 UITableView 的子类,而两个 cell ListCoreCellHost 也是 UITableViewCell 的子类。对于 List 来说,SwiftUI 底层直接使用了成熟的 UITableView 的一套实现逻辑,而并非重新进行绘制。相比起来,像是 Text 或者 Image 这样的单一 ViewUIKit 层则全部统一由 DisplayList.ViewUpdater.Platform.CGDrawingView 这个 UIView 的子类进行绘制。

不过在使用 SwiftUI 时,我们首先需要做的就是跳出 UIKit 的思维方式,不应该去关心背后的绘制和实现。使用 UITableView 来表达 List 也许只是权宜之计,也许在未来也会被另外更高效的绘制方式取代。由于 SwiftUI 层只是 View 描述的数据抽象,因此和 React 的 Virtual DOM 以及 Flutter 的 Widget 一样,背后的具体绘制方式是完全解耦合,并且可以进行替换的。这为今后 SwiftUI 更进一步留出了足够的可能性。

Section 5 – Step 2: 动态 ListIdentifiable

List(landmarkData.identified(by: \.id)) { landmark in
    LandmarkRow(landmark: landmark)
}

除了静态方式以外,List 当然也可以接受动态方式的输入,这时使用的初始化方法和上面静态的情况不一样:

public struct List<Selection, Content> where Selection : SelectionManager, Content : View {
    public init<Data, RowContent>(
        _ data: Data, action: @escaping (Data.Element.IdentifiedValue) -> Void,
        rowContent: @escaping (Data.Element.IdentifiedValue) -> RowContent) 
    where 
        Content == ForEach<Data, Button<HStack<RowContent>>>, 
        Data : RandomAccessCollection, 
        RowContent : View, 
        Data.Element : Identifiable
        
    //...
}

这个初始化方法的约束比较多,我们一行行来看:

  • Content == ForEach<Data, Button<HStack<RowContent>>> 因为这个函数签名中并没有出现 ContentContent 仅只 List<Selection, Content> 的类型声明中有定义,所以在这与其说是一个约束,不如说是一个用来反向确定 List 实际类型的描述。现在让我们先将注意力放在更重要的地方,稍后会再多讲一些这个。
  • Data : RandomAccessCollection 这基本上等同于要求第一个输入参数是 Array
  • RowContent : View 对于构建每一行的 rowContent 来说,需要返回是 View 是很正常的事情。注意 rowContent 其实也是被 @ViewBuilder 标记的,因此你也可以把 LandmarkRow 的内容展开写进去。不过一般我们会更希望尽可能拆小 UI 部件,而不是把东西堆在一起。
  • Data.Element : Identifiable 要求 Data.Element (也就是数组元素的类型) 上存在一个可以辨别出某个实例的满足 Hashable 的 id。这个要求将在数据变更时快速定位到变化的数据所对应的 cell,并进行 UI 刷新。

关于 List 以及其他一些常见的基础 View,有一个比较有趣的事实。在下面的代码中,我们期望 List 的初始化方法生成的是某个类型的 View

var body: some View {
    List {
        //...
    }
}

但是你看遍 List 的文档,甚至是 Cmd + Click 到 SwiftUI 的 interface 中查找 View 相关的内容,都找不到 List : View 之类的声明。

难道是因为 SwiftUI 做了什么手脚,让本来没有满足 View 的类型都可以“充当”一个 View 吗?当然不是这样…如果你在运行时暂定 app 并用 lldb 打印一下 List 的类型信息,可以看到下面的下面的信息:

(lldb) type lookup List
...
struct List<Selection, Content> : SwiftUI._UnaryView where ...

进一步,_UnaryView 的声明是:

protocol _UnaryView : View where Self.Body : _UnaryView {
}

SwiftUI 内部的一元视图 _UnaryView 协议虽然是满足 View 的,但它被隐藏起来了,而满足它的 List 虽然是 public 的,但是却可以把这个协议链的信息也作为内部信息隐藏起来。这是 Swift 内部框架的特权,第三方的开发者无法这样在在两个 public 的声明之间插入一个私有声明。

最后,SwiftUI 中当前 (Xcode 11 beta 1) 只有对应 UITableViewList,而没有 UICollectionView 对应的像是 Grid 这样的类型。现在想要实现类似效果的话,只能嵌套使用 VStackHStack。这是比较奇怪的,因为技术层面上应该和 table view 没有太多区别,大概是因为工期不太够?相信今后应该会补充上 Grid

教程 3 – Handling User Input

Section 3 – Step 2: @StateBinding

@State var showFavoritesOnly = true

var body: some View {
    NavigationView {
        List {
            Toggle(isOn: $showFavoritesOnly) {
                Text("Favorites only")
            }
    //...
            if !self.showFavoritesOnly || landmark.isFavorite {

这里出现了两个以前在 Swift 里没有的特性:@State$showFavoritesOnly

如果你 Cmd + Click 点到 State 的定义里面,可以看到它其实是一个特殊的 struct

@propertyWrapper public struct State<Value> : DynamicViewProperty, BindingConvertible {

    /// Initialize with the provided initial value.
    public init(initialValue value: Value)

    /// The current state value.
    public var value: Value { get nonmutating set }

    /// Returns a binding referencing the state value.
    public var binding: Binding<Value> { get }

    /// Produces the binding referencing this state value
    public var delegateValue: Binding<Value> { get }
}

@propertyWrapper 标注和上一篇中提到@_functionBuilder 类似,它修饰的 struct 可以变成一个新的修饰符并作用在其他代码上,来改变这些代码默认的行为。这里 @propertyWrapper 修饰的 State 被用做了 @State 修饰符,并用来修饰 View 中的 showFavoritesOnly 变量。

@_functionBuilder 负责按照规矩“重新构造”函数的作用不同,@propertyWrapper 的修饰符最终会作用在属性上,将属性“包裹”起来,以达到控制某个属性的读写行为的目的。如果将这部分代码“展开”,它实际上是这个样子的:

// @State var showFavoritesOnly = true
   var showFavoritesOnly = State(initialValue: true)
    
var body: some View {
    NavigationView {
        List {
//          Toggle(isOn: $showFavoritesOnly) {
            Toggle(isOn: showFavoritesOnly.binding) {
                Text("Favorites only")
            }
    //...
//          if !self.showFavoritesOnly || landmark.isFavorite {
            if !self.showFavoritesOnly.value || landmark.isFavorite {

我把变化之前的部分注释了一下,并且在后面一行写上了展开后的结果。可以看到 @State 只是声明 State struct 的一种简写方式而已。State 里对具体要如何读写属性的规则进行了定义。对于读取,非常简单,使用 showFavoritesOnly.value 就能拿到 State 中存储的实际值。而原代码中 $showFavoritesOnly 的写法也只不过是 showFavoritesOnly.binding 的简化。binding 将创建一个 showFavoritesOnly 的引用,并将它传递给 Toggle。再次强调,这个 binding 是一个引用类型,所以 Toggle 中对它的修改,会直接反应到当前 View 的 showFavoritesOnly 去设置它的 value。而 State 的 value didSet 将触发 body 的刷新,从而完成 State -> View 的绑定。

在 Xcode 11 beta 1 中,Swift 中使用的修饰符名字是 @propertyDelegate,不过在 WWDC 上 Apple 提到这个特性时把它叫做了 @propertyWrapper。根据可靠消息,在未来正式版中应该也会叫做 @propertyWrapper,所以大家在看各种资料的时候最好也建议一个简单的映射关系。

如果你想要了解更多关于 @propertyWrapper 的细节,可以看看相关的提案论坛讨论。比较有意思的细节是 Apple 在将相应的 PR merge 进了 master 以后又把这个提案的打回了“修改”的状态,而非直接接受。除了 @propertyWrapper 的名称修正以外,应该还会有一些其他的细节修改,但是已经公开的行为模式上应该不会太大变化了。

SwiftUI 中还有几个常见的 @ 开头的修饰,比如 @Binding@Environment@EnvironmentObject 等,原理上和 @State 都一样,只不过它们所对应的 struct 中定义读写方式有区别。它们共同构成了 SwiftUI 数据流的最基本的单元。对于 SwiftUI 的数据流,如果展开的话足够一整篇文章了。在这里还是十分建议看一看 Session 226 – Data Flow Through SwiftUI 的相关内容。

教程 5 – Animating Views and Transitions

Section 2 – Step 4: 两种动画的方式

在 SwiftUI 中,做动画变的十分简单。Apple 的教程里提供了两种动画的方式:

  1. 直接在 View 上使用 .animation modifier
  2. 使用 withAnimation { } 来控制某个 State,进而触发动画。

对于只需要对单个 View 做动画的时候,animation(_:) 要更方便一些,它和其他各类 modifier 并没有太大不同,返回的是一个包装了对象 View 和对应的动画类型的新的 Viewanimation(_:) 接受的参数 Animation 并不是直接定义 View 上的动画的数值内容的,它是描述的是动画所使用的时间曲线,动画的延迟等这些和 View 无关的东西。具体和 View 有关的,想要进行动画的数值方面的变更,由其他的诸如 rotationEffectscaleEffect 这样的 modifier 来描述。

在上面的 教程 5 – Section 1 – Step 5 里有这样一段代码:

Button(action: {
    self.showDetail.toggle()
}) {
    Image(systemName: "chevron.right.circle")
        .imageScale(.large)
        .rotationEffect(.degrees(showDetail ? 90 : 0))
        .animation(nil)
        .scaleEffect(showDetail ? 1.5 : 1)
        .padding()
        .animation(.spring())
}

要注意,SwiftUI 的 modifier 是有顺序的。在我们调用 animation(_:) 时,SwiftUI 做的事情等效于是把之前的所有 modifier 检查一遍,然后找出所有满足 Animatable 协议的 view 上的数值变化,比如角度、位置、尺寸等,然后将这些变化打个包,创建一个事物 (Transaction) 并提交给底层渲染去做动画。在上面的代码中,.rotationEffect 后的 .animation(nil) 将 rotation 的动画提交,因为指定了 nil 所以这里没有实际的动画。在最后,.rotationEffect 已经被处理了,所以末行的 .animation(.spring()) 提交的只有 .scaleEffect

withAnimation { } 是一个顶层函数,在闭包内部,我们一般会触发某个 State 的变化,并让 View.body 进行重新计算:

Button(action: {
    withAnimation {
        self.showDetail.toggle()
    }
}) { 
  //...
}

如果需要,你也可以为它指定一个具体的 Animation

withAnimation(.basic()) {
    self.showDetail.toggle()
}

这个方法相当于把一个 animation 设置到 View 数值变化的 Transaction 上,并提交给底层渲染去做动画。从原理上来说,withAnimation 是统一控制单个的 Transaction,而针对不同 Viewanimation(_:) 调用则可能对应多个不同的 Transaction

教程 7 – Working with UI Controls

Section 4 – Step 2: 关于 View 的生命周期

ProfileEditor(profile: $draftProfile)
    .onDisappear {
        self.draftProfile = self.profile
    }

在 UIKit 开发时,我们经常会接触一些像是 viewDidLoadviewWillAppear 这样的生命周期的方法,并在里面进行一些配置。SwiftUI 里也有一部分这类生命周期的方法,比如 .onAppear.onDisappear,它们也被“统一”在了 modifier 这面大旗下。

但是相对于 UIKit 来说,SwiftUI 中能 hook 的生命周期方法比较少,而且相对要通用一些。本身在生命周期中做操作这种方式就和声明式的编程理念有些相悖,看上去就像是加上了一些命令式的 hack。我个人比较期待 ViewCombine 能再深度结合一些,把像是 self.draftProfile = self.profile 这类依赖生命周期的操作也用绑定的方式搞定。

相比于 .onAppear.onDisappear,更通用的事件响应 hook 是 .onReceive(_:perform:),它定义了一个可以响应目标 Publisher 的任意的 View,一旦订阅的 Publisher 发出新的事件时,onReceive 就将被调用。因为我们可以自行定义这些 publisher,所以它是完备的,这在把现有的 UIKit View 转换到 SwiftUI View 时会十分有用。

June 11, 2019 at 11:32AM via OneV’s Den http://bit.ly/2MMh8eb

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【喵神】 SwiftUI 的一些初步探索 (一)

总览

如果你想要入门 SwiftUI 的使用,那 Apple 这次给出的官方教程绝对给力。这个教程提供了非常详尽的步骤和说明,网页的交互也是一流,是觉得值得看和动手学习的参考。

不过,SwiftUI 中有一些值得注意的细节在教程里并没有太详细提及,也可能造成一些困惑。这篇文章以我的个人观点对教程的某些部分进行了补充说明,希望能在大家跟随教程学习 SwiftUI 的时候有点帮助。这篇文章的推荐阅读方式是,一边参照 SwiftUI 教程实际动手进行实现,一边在到达对应步骤时参照本文加深理解。在下面每段内容前我标注了对应的教程章节和链接,以供参考。

在开始学习 SwiftUI 之前,我们需要大致了解一个问题:为什么我们会需要一个新的 UI 框架。

为什么需要 SwiftUI

UIKit 面临的挑战

对于 Swift 开发者来说,昨天的 WWDC 19 首日 Keynote 和 Platforms State of the Union 上最引人注目的内容自然是 SwiftUI 的公布了。从 iOS SDK 2.0 开始,UIKit 已经伴随广大 iOS 开发者经历了接近十年的风风雨雨。UIKit 的思想继承了成熟的 AppKit 和 MVC,在初出时,为 iOS 开发者提供了良好的学习曲线。

UIKit 提供的是一套符合直觉的,基于控制流的命令式的编程方式。最主要的思想是在确保 View 或者 View Controller 生命周期以及用户交互时,相应的方法 (比如 viewDidLoad 或者某个 target-action 等) 能够被正确调用,从而构建用户界面和逻辑。不过,不管是从使用的便利性还是稳定性来说,UIKit 都面临着巨大的挑战。我个人勉强也能算是 iOS 开发的“老司机”了,但是「掉到 UIKit 的坑里」这件事,也几乎还是我每天的日常。UIKit 的基本思想要求 View Controller 承担绝大部分职责,它需要协调 model,view 以及用户交互。这带来了巨大的 side effect 以及大量的状态,如果没有妥善安置,它们将在 View Controller 中混杂在一起,同时作用于 view 或者逻辑,从而使状态管理愈发复杂,最后甚至不可维护而导致项目失败。不仅是作为开发者我们自己写的代码,UIKit 本身内部其实也经常受困于可变状态,各种奇怪的 bug 也频频出现。

声明式的界面开发方式

近年来,随着编程技术和思想的进步,使用声明式或者函数式的方式来进行界面开发,已经越来越被接受并逐渐成为主流。最早的思想大概是来源于 Elm,之后这套方式被 ReactFlutter 采用,这一点上 SwiftUI 也几乎与它们一致。总结起来,这些 UI 框架都遵循以下步骤和原则:

  1. 使用各自的 DSL 来描述「UI 应该是什么样子」,而不是用一句句的代码来指导「要怎样构建 UI」。

    比如传统的 UIKit,我们会使用这样的代码来添加一个 “Hello World” 的标签,它负责“创建 label”,“设置文字”,“将其添加到 view 上”:

     func viewDidLoad() {
         super.viewDidLoad()
         let label = UILabel()
         label.text = "Hello World"
         view.addSubview(label)
         // 省略了布局的代码
     }
    

    而相对起来,使用 SwiftUI 我们只需要告诉 SDK 我们需要一个文字标签:

     var body: some View {
         Text("Hello World")
     }
    
  2. 接下来,框架内部读取这些 view 的声明,负责将它们以合适的方式绘制渲染。

    注意,这些 view 的声明只是纯数据结构的描述,而不是实际显示出来的视图,因此这些结构的创建和差分对比并不会带来太多性能损耗。相对来说,将描述性的语言进行渲染绘制的部分是最慢的,这部分工作将交由框架以黑盒的方式为我们完成。

  3. 如果 View 需要根据某个状态 (state) 进行改变,那我们将这个状态存储在变量中,并在声明 view 时使用它:

     @State var name: String = "Tom"
     var body: some View {
         Text("Hello \(name)")
     }
    

    关于代码细节可以先忽略,我们稍后会更多地解释这方面的内容。

  4. 状态发生改变时,框架重新调用声明部分的代码,计算出新的 view 声明,并和原来的 view 进行差分,之后框架负责对变更的部分进行高效的重新绘制。

SwiftUI 的思想也完全一样,而且实际处理也不外乎这几个步骤。使用描述方式开发,大幅减少了在 app 开发者层面上出现问题的机率。

一些细节解读

官方教程中对声明式 UI 的编程思想有深刻的体现。另外,SwiftUI 中也采用了非常多 Swift 5.1 的新特性,会让习惯了 Swift 4 或者 5 的开发者“耳目一新”。接下来,我会分几个话题,对官方教程的一些地方进行解释和探索。

教程 1 – Creating and Combining Views

Section 1 – Step 3: SwiftUI app 的启动

创建 app 之后第一件好奇的事情是,SwiftUI app 是怎么启动的。

教程示例 app 在 AppDelegate 中通过 application(_:configurationForConnecting:options) 返回了一个名为 “Default Configuration” 的 UISceneConfiguration 实例:

func application(
    _ application: UIApplication,
    configurationForConnecting connectingSceneSession: UISceneSession,
    options: UIScene.ConnectionOptions) -> UISceneConfiguration
{
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}

这个名字的 Configuration 在 Info.plist 的 “UIApplicationSceneManifest -> UISceneConfigurations” 中进行了定义,指定了 Scene Session Delegate 类为 $(PRODUCT_MODULE_NAME).SceneDelegate。这部分内容是 iOS 13 中新加入的通过 Scene 管理 app 生命周期的方式,以及多窗口支持部分所需要的代码。这部分不是我们今天的话题。在 app 完成启动后,控制权被交接给 SceneDelegate,它的 scene(_:willConnectTo:options:) 将会被调用,进行 UI 的配置:

func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions)
    {
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UIHostingController(rootView: ContentView())
        self.window = window
        window.makeKeyAndVisible()
    }

这部分内容就是标准的 iOS app 启动流程了。UIHostingController 是一个 UIViewController 子类,它将负责接受一个 SwiftUI 的 View 描述并将其用 UIKit 进行渲染 (在 iOS 下的情况)。UIHostingController 就是一个普通的 UIViewController,因此完全可以做到将 SwiftUI 创建的界面一点点集成到已有的 UIKit app 中,而并不需要从头开始就是基于 SwiftUI 的构建。

由于 Swift ABI 已经稳定,SwiftUI 是一个搭载在用户 iOS 系统上的 Swift 框架。因此它的最低支持的版本是 iOS 13,可能想要在实际项目中使用,还需要等待一两年时间。

Section 1 – Step 4: 关于 some View

struct ContentView: View {
    var body: some View {
        Text("Hello World")
    }
}

一眼看上去可能会对 some 比较陌生,为了将明白这件事,我们先从 View 说起。

View 是 SwiftUI 的一个最核心的协议,代表了一个屏幕上元素的描述。这个协议中含有一个 associatedtype:

public protocol View : _View {
    associatedtype Body : View
    var body: Self.Body { get }
}

这种带有 associatedtype 的协议不能作为类型来使用,而只能作为类型约束使用:

// Error
func createView() -> View {

}

// OK
func createView<T: View>() -> T {
    
}

这样一来,其实我们是不能写类似这种代码的:

// Error,含有 associatedtype 的 protocol View 只能作为类型约束使用
struct ContentView: View {
    var body: View {
        Text("Hello World")
    }
}

想要 Swift 帮助自动推断出 View.Body 的类型的话,我们需要明确地指出 body 的真正的类型。在这里,body 的实际类型是 Text

struct ContentView: View {
    var body: Text {
        Text("Hello World")
    }
}

当然我们可以明确指定出 body 的类型,但是这带来一些麻烦:

  1. 每次修改 body 的返回时我们都需要手动去更改相应的类型。
  2. 新建一个 View 的时候,我们都需要去考虑会是什么类型。
  3. 其实我们只关心返回的是不是一个 View,而对实际上它是什么类型并不感兴趣。

some View 这种写法使用了 Swift 5.1 的 Opaque return types 特性。它向编译器作出保证,每次 body 得到的一定是某一个确定的,遵守 View 协议的类型,但是请编译器“网开一面”,不要再细究具体的类型。返回类型确定单一这个条件十分重要,比如,下面的代码也是无法通过的:


let someCondition: Bool

// Error: Function declares an opaque return type, 
// but the return statements in its body do not have 
// matching underlying types.
var body: some View {
    if someCondition {
        // 这个分支返回 Text
        return Text("Hello World")
    } else {
        // 这个分支返回 Button,和 if 分支的类型不统一
        return Button(action: {}) {
            Text("Tap me")
        }
    }
}

这是一个编译期间的特性,在保证 associatedtype protocol 的功能的前提下,使用 some 可以抹消具体的类型。这个特性用在 SwiftUI 上简化了书写难度,让不同 View 声明的语法上更加统一。

Section 2 – Step 1: 预览 SwiftUI

SwiftUI 的 Preview 是 Apple 用来对标 RN 或者 Flutter 的 Hot Reloading 的开发工具。由于 IBDesignable 的性能上的惨痛教训,而且得益于 SwiftUI 经由 UIKit 的跨 Apple 平台的特性,Apple 这次选择了直接在 macOS 上进行渲染。因此,你需要使用搭载有 SwiftUI.framework 的 macOS 10.15 才能够看到 Xcode Previews 界面。

Xcode 将对代码进行静态分析 (得益于 SwiftSyntax 框架),找到所有遵守 PreviewProvider 协议的类型进行预览渲染。另外,你可以为这些预览提供合适的数据,这甚至可以让整个界面开发流程不需要实际运行 app 就能进行。

笔者自己尝试下来,这套开发方式带来的效率提升相比 Hot Reloading 要更大。Hot Reloading 需要你有一个大致界面和准备相应数据,然后运行 app,停在要开发的界面,再进行调整。如果数据状态发生变化,你还需要 restart app 才能反应。SwiftUI 的 Preview 相比起来,不需要运行 app 并且可以提供任何的 dummy 数据,在开发效率上更胜一筹。

经过短短一天的使用,Option + Command + P 这个刷新 preview 的快捷键已经深入到我的肌肉记忆中了。

Section 3 – Step 5: 关于 ViewBuilder

创建 Stack 的语法很有趣:

VStack(alignment: .leading) {
    Text("Turtle Rock")
        .font(.title)
    Text("Joshua Tree National Park")
        .font(.subheadline)
}

一开始看起来好像我们给出了两个 Text,似乎是构成的是一个类似数组形式的 [View],但实际上并不是这么一回事。这里调用了 VStack 类型的初始化方法:

public struct VStack<Content> where Content : View {
    init(
        alignment: HorizontalAlignment = .center, 
        spacing: Length? = nil, 
        content: () -> Content)
}

前面的 alignmentspacing 没啥好说,最后一个 content 比较有意思。看签名的话,它是一个 () -> Content 类型,但是我们在创建这个 VStack 时所提供的代码只是简单列举了两个 Text,而并没有实际返回一个可用的 Content

这里使用了 Swift 5.1 的另一个新特性:Funtion builders。如果你实际观察 VStack这个初始化方法的签名,会发现 content 前面其实有一个 @ViewBuilder 标记:

init(
    alignment: HorizontalAlignment = .center, 
    spacing: Length? = nil, 
    @ViewBuilder content: () -> Content)

ViewBuilder 则是一个由 @_functionBuilder 进行标记的 struct:

@_functionBuilder public struct ViewBuilder { /* */ }

使用 @_functionBuilder 进行标记的类型 (这里的 ViewBuilder),可以被用来对其他内容进行标记 (这里用 @ViewBuildercontent 进行标记)。被用 function builder 标记过的 ViewBuilder 标记以后,content 这个输入的 function 在被使用前,会按照 ViewBuilder 中合适的 buildBlock 进行 build 后再使用。如果你阅读 ViewBuilder文档,会发现有很多接受不同个数参数的 buildBlock 方法,它们将负责把闭包中一一列举的 Text 和其他可能的 View 转换为一个 TupleView,并返回。由此,content 的签名 () -> Content 可以得到满足。

实际上构建这个 VStack 的代码会被转换为类似下面这样:

// 等效伪代码,不能实际编译。
VStack(alignment: .leading) { viewBuilder -> Content in
    let text1 = Text("Turtle Rock").font(.title)
    let text2 = Text("Joshua Tree National Park").font(.subheadline)
    return viewBuilder.buildBlock(text1, text2)
}

当然这种基于 funtion builder 的方式是有一定限制的。比如 ViewBuilder 就只实现了最多十个参数buildBlock,因此如果你在一个 VStack 中放超过十个 View 的话,编译器就会不太高兴。不过对于正常的 UI 构建,十个参数应该足够了。如果还不行的话,你也可以考虑直接使用 TupleView 来用多元组的方式合并 View

TupleView<(Text, Text)>(
    (Text("Hello"), Text("Hello"))
)

除了按顺序接受和构建 ViewbuildBlock 以外,ViewBuilder 还实现了两个特殊的方法:buildEitherbuildIf。它们分别对应 block 中的 if...else 的语法和 if 的语法。也就是说,你可以在 VStack 里写这样的代码:

var someCondition: Bool

VStack(alignment: .leading) {
    Text("Turtle Rock")
        .font(.title)
    Text("Joshua Tree National Park")
        .font(.subheadline)
    if someCondition {
        Text("Condition")
    } else {
        Text("Not Condition")
    }
}

其他的命令式的代码在 VStackcontent 闭包里是不被接受的,下面这样也不行:

VStack(alignment: .leading) {
    // let 语句无法通过 function builder 创建合适的输出
    let someCondition = model.condition
    if someCondition {
        Text("Condition")
    } else {
        Text("Not Condition")
    }
}

到目前为止,只有以下三种写法能被接受 (有可能随着 SwiftUI 的发展出现别的可接受写法):

  • 结果为 View 的语句
  • if 语句
  • if...else... 语句

Section 4 – Step 7: 链式调用修改 View 的属性

教程到这一步的话,相信大家已经对 SwiftUI 的超强表达能力有所感悟了。

var body: some View {
    Image("turtlerock")
        .clipShape(Circle())
        .overlay(
            Circle().stroke(Color.white, lineWidth: 4))
        .shadow(radius: 10)
}

可以试想一下,在 UIKit 中要动手撸一个这个效果的困难程度。我大概可以保证,99% 的开发者很难在不借助文档或者 copy paste 的前提下完成这些事情,但是在 SwiftUI 中简直信手拈来。在创建 View 之后,用链式调用的方式,可以将 View 转换为一个含有变更后内容的对象。这么说比较抽象,我们可以来看一个具体的例子。比如简化一下上面的代码:

let image: Image = Image("turtlerock")
let modified: _ModifiedContent<Image, _ShadowEffect> = image.shadow(radius: 10)

image 通过一个 .shadow 的 modifier,modified 变量的类型将转变为 _ModifiedContent<Image, _ShadowEffect>。如果你查看 View 上的 shadow 的定义,它是这样的:

extension View {
    func shadow(
        color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33), 
        radius: Length, x: Length = 0, y: Length = 0) 
    -> Self.Modified<_ShadowEffect>
}

ModifiedView 上的一个 typealias,在 struct Image: View 的实现里,我们有:

public typealias Modified<T> = _ModifiedContent<Self, T>

_ModifiedContent 是一个 SwiftUI 的私有类型,它存储了待变更的内容,以及用来实施变更的 Modifier

struct _ModifiedContent<Content, Modifier> {
    var content: Content
    var modifier: Modifier
}

Content 遵守 ViewModifier 遵守 ViewModifier 的情况下,_ModifiedContent 也将遵守 View,这是我们能够通过 View 的各个 modifier extension 进行链式调用的基础:

extension _ModifiedContent : _View 
    where Content : View, Modifier : ViewModifier 
{
}

shadow 的例子中,SwiftUI 内部会使用 _ShadowEffect 这个 ViewModifier,并把 image 自身和 _ShadowEffect 实例存放到 _ModifiedContent 里。不论是 image 还是 modifier,都只是对未来实际视图的描述,而不是直接对渲染进行的操作。在最终渲染前,ViewModifierbody(content: Self.Content) -> Self.Body 将被调用,以给出最终渲染层所需要的各个属性。

更具体来说,_ShadowEffect 是一个满足 EnvironmentalModifier 协议的类型,这个协议要求在使用前根据使用环境将自身解析为具体的 modifier。

其他的几个修改 View 属性的链式调用与 shadow 的原理几乎一致。

小结

上面是对 SwiftUI 教程的第一部分进行的一些说明,在之后的一篇文章里,我会对剩余的几个教程中有意思的部分再做些解释。

虽然公开还只有一天,但是 SwiftUI 已经经常被用来和 Flutter 等框架进行比较。试用下来,在 view 的描述表现力上和与 app 的结合方面,SwiftUI 要胜过 Flutter 和 Dart 的组合很多。Swift 虽然开源了,但是 Apple 对它的掌控并没有减弱。Swift 5.1 的很多特性几乎可以说都是为了 SwiftUI 量身定制的,我们已经在本文中看到了一些例子,比如 Opaque return types 和 Function builder 等。在接下来对后面几个教程的解读中,我们还会看到更多这方面的内容。

另外,Apple 在背后使用 Combine.framework 这个响应式编程框架来对 SwiftUI.framework 进行驱动和数据绑定,相比于现有的 RxSwift/RxCocoa 或者是 ReactiveSwift 的方案来说,得到了语言和编译器层级的大力支持。如果有机会,我想我也会对这方面的内容进行一些探索和介绍。

June 04, 2019 at 02:32PM via OneV’s Den http://bit.ly/2X1KAAI

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 HTTPS 升级

昨晚花了几个钟头,把 blog 的 HTTP 升级成 HTTPS 了,虽然这件事做的晚了一点。为什么要升级,不是我说明的重点,想了解的朋友可以阅读 这篇文章 。我记录的是我升级的过程,踩到的坑。

备份

首先,文章中有许多以 http://www.raychase.net 开头的 URL,比如某些图片和链接,可以把它们改成 https 的,也可以全部改成相对路径,这样的适用性更广。

UPDATE xx_posts SET post_content = REPLACE(post-content, 'http://www.raychase.net', '/');

到浏览器里面访问看看,似乎没有什么问题。

安装

WordPress 管理台

在 Wordress 管理台的设置里面,把本站 URL 中的 http 替换成 https。

证书申请安装

certbot 申请证书,选好了代理和操作系统一个,照着 guide 操作。

wget https://dl.eff.org/certbot-auto
sudo mv certbot-auto /usr/local/bin/certbot-auto
sudo chown root /usr/local/bin/certbot-auto
sudo chmod 0755 /usr/local/bin/certbot-auto

接着自动安装,中途退出了:

sudo /usr/local/bin/certbot-auto --nginx
...
To use Certbot, packages from the EPEL repository need to be installed.
Enable the EPEL repository and try running Certbot again.

EPEL

既然是没有 EPEL,那就安装一个:

sudo yum install epel-release
Loaded plugins: fastestmirror
Setting up Install Process
Loading mirror speeds from cached hostfile
Error: Cannot retrieve metalink for repository: epel. Please verify its path and try again

失败了,从错误中看是 mirror 地址上没法访问到,于是研究了一下,修改 /etc/yum.repos.d/epel.repo 和 /etc/yum.repos.d/epel-testing.repo ,注释掉全部以 mirrorlist= 开始的行,再还原全部以 baseurl= 开始的行。这样就去原始地址,而不是镜像地址下载了。

果然就安装成功了。

Nginx 配置

于是继续刚才失败的操作:

sudo /usr/local/bin/certbot-auto --nginx

挂在了真正安装配置的过程中:

Error while running nginx -c /etc/nginx/nginx.conf -t.
nginx: [emerg] open() "/etc/nginx/nginx.conf" failed (2: No such file or directory)
nginx: configuration file /etc/nginx/nginx.conf test failed

原来是找不到 nginx 的配置文件,因为 nginx 安装的路径并非/etc 下面。于是就干脆建立一个软链接:

ln -s /usr/local/nginx/conf /etc/nginx

这一步过了,结果又挂在了证书 challenge 的过程(challenge 是域名验证的一环,在 这里 有介绍):

Performing the following challenges:
http-01 challenge for www.raychase.net
Waiting for verification...
Challenge failed for domain www.raychase.net

研究一下发现,它需要占用 80 端口,因为这个 challenge 的路径是 http://www.raychase.net/.well-known/acme-challenge/xxxxx,而网站应用已经占了 80,于是把 lnmp 先停掉,再操作。

终于提示成功了,注意到它在 nginx 的配置文件目录下的子目录 vhost 下面生成了一个增量配置文件:www.raychase.net.conf,里面配置了一些 ssl_cewrtificate 之类的 key 的路径,把它放到 nginx 的配置目录里面。

验证

命令行验证

尝试了一下 curl http://bit.ly/2YN6zZB 可以访问,于是就在 SSL Labs 可以验证证书的情况,结果提示失败。

接着在外网使用上述的 curl HTTPS 命令,但是却 timeout,可是 curl http://www.raychase.net,得到了正确的 301 重定向的响应。

首先怀疑 nginx,仔细研究了 nginx 的配置,没发现有什么问题。接着就怀疑防火墙,查看 /etc/sysconfig/iptables 发现可能是防火墙的问题。于是命令行添加规则:

iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT

果然,可以访问了。

再 curl http://www.raychase.net,得到了 301 重定向的响应。

浏览器验证

接着再到浏览器里面访问验证,大致没问题,可是,我注意到访问网站首页的时候,URL 左侧的小图标不是一般 HTTPS 的“锁”的图标,而是一个圈“i”的图标,点击以后可以看到“Your connection to this site is not fully secure”这样的文字。

原来是因为网页上包含了“mixed content”,即有指向当前站的 http 的链接。查看源代码,原来在网站中还有一些其它配置包含有当前站的 http 链接,逐一修复,果然,小锁图标出来了,而文字变成了“Connection is secure”。

改进

HTTP2

在 nginx 中配置打开 http2:

listen 443 ssl http2;

可是在修改完毕,重新加载 nginx 的时候,它提示 http2 不认识,于是就检查了,发现是 nginx 版本太老的原因。

于是使用 yum 来升级,之后提示已经是最新版本了,还是不支持。

原来 yum 的默认 repo 版本还是太老,必须要使用 nginx 自己的 repo。于是增加文件:

/etc/yum.repos.d/nginx.repo

写入:

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=0
enabled=1

再重启 nginx 就好了。

证书 renew 自动化

证书每几个月就会过期,因此必须自动续上,而不是手动登上服务器来操作。我尝试了一下 renew 证书的过程:

/usr/local/bin/certbot-auto renew

发现又出现了前面 challenge 的过程。当时我停掉了 lnmp,可是我总不能为了续个证书停掉网站吧?于是考虑解决 challenge 的端口冲突问题。发现其实可以用 HTTP 的 80 端口,但是 challenge 的 URI 冲突才是问题的核心。这样问题就好解决了,把导致冲突的 URI 放进来就好了,在 nginx 里面配置:

location /.well-known/acme-challenge/.* {
    proxy_pass http://127.0.0.1:80;
}

接着配置 cron task,使得这个过程自动化:

crontab -e

加入:

0 0 25 * * /usr/local/bin/certbot-auto renew
10 0 25 * * /sbin/service nginx restart

昨天是 25 号,因此我设置成每天的 25 号来尝试 renew。如果证书还远没到期,它会提示“Cert not yet due for renewal”并会退出这个过程,因此这个命令可以安全地运行。

可是我并不知道运行结果啊,总不能每次都登陆上来,跑到/var/log/cron 去看日志吧?

其中一个解决办法是发 email,把运行输出发 email 给我。

Email 通知结果

于是先要配置 ssmtp,编辑/etc/ssmtp/ssmtp.conf:

root=xxx@gmail.com
mailhub=smtp.gmail.com:587
RewriteDomain=gmail.com
Hostname=xxx
FromLineOverride=YES
UseTLS=Yes
UseSTARTTLS=Yes
TLS_CA_File=/etc/pki/tls/certs/ca-bundle.crt
AuthUser=xxx@gmail.com
AuthPass=yyy
AuthMethod=LOGIN

接着把地址信息添加到/etc/ssmtp/revaliases。

我使用的是 Gmail 的 SMTP,为了让 gmail 允许这样的操作,还必须在账户选项的 security 里面,开启 less secure app access 的设置。

搞定以后尝试发一封邮件试试:

echo -n 'test' | sendmail -v raychase1986@gmail.com

没问题!那就可以配置 cron task 让它发邮件了,加上:

MAILTO=RayChase1986@gmail.com

这个变量加上以后,应该就可以自动发邮件了。

crontab 没有立即执行,从而验证的功能,于是为了立即验证,加了一行(每分钟都会执行一次):

* * * * * echo "test"

验证以后把这行删掉就好了。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2HBt6CJ

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 不要让业务牵着鼻子走

这篇文章算是要和之前写的 《程序员懂业务有多重要?》“唱反调”了。

从工作开始,我就不断被灌输着一种业务至上的观点,无论在中国的公司,还是美国的公司,衡量一个决定或者一个需求的价值,都是在业务上有多大的帮助,都说 business impact 是什么。我从不怀疑单纯这样做的初衷,但是我质疑单纯这样做的结果。我觉得,即便是一个业务驱动为主的团队,在决策的时候,技术的占比,应当占据显著的地位。因而我说,不要被业务牵着鼻子走。继续把这一点发扬光大,我认为它对团队发展,对个人发展,都是如此。

曾经认为,这样的观点应该是公认的,但是我越来越发现,事实并不是这样。应该说几乎所有程序员都看到了业务上面的价值,都明白所谓“用技术创造业务价值”的意义,但是其中却有 很多人忘掉了技术的根基,技术是程序员吃饭和安家立命之本,它不只是在实施过程中的一门工具和一种途径,它本身就有着决定项目和产品走向的能力。

自己的经历

我把做的项目,或者写的代码,非常粗略地,可以分为两类:

  • 一种是基于现有代码设施,来写基于业务逻辑的实现,包括新的接口、新的条件分支等等,这种代码被称为“业务代码”;
  • 另一种则是拓展基础设施,实现新的可复用特性,本身不混入业务概念,不直接“变现”,这种代码被称为“非业务代码”。

看起来前者更偏向于业务,而后者更偏向于技术,但是话说回来,在许多场景中,这二者是很难彻底分离开的。有时,要用非业务代码实现某种可被不同业务重用的基础特性,却是由业务驱动的。应该说,硬要分类的话,通常我们遇到的大多数情况,都属于前者。

我在业务和技术的团队中都呆过。在 2008 年的时候,我首先加入的是华为的彩铃团队,写的是彩铃服务端的业务代码,扩展 SOAP 接口和存储过程算是业务为主的代码,但是重新实现的定时任务功能算是用实现业务无关的基础设施。后来在连续几年的和视频平台有关的项目中,做过一个比较小的 service,也做过一个比较大的 portal,专职做过性能优化,由于基本都是重头来做,因此业务和非业务代码都必须涉及,而考虑到我所在的基线团队,以为定制团队提供基础设施为主,因而非业务代码写得更多一点。

在 Amazon 呆的时间比较久,做的东西也比较宽泛,做过 data visualization,big data 的采集和处理,分布式计算,以及分布式的 workflow,还维护过一个 service(更多内容我 在这里 写过)。这些代码一半属于技术代码,而业务的部分主要由团队中的 scientists 和 data analysts 们精通并持续研究,工程师在多数情况下只负责提供好用的工具。(其实这一点分工很难做好,Amazon 是做得相对不错的, 介绍过分工 ,也 吐槽过工具

在 Oracle,所在的大 team 下我也算参与过三个小团队了,一个前端的团队,一个后端 service 的团队,再到现在稳定在这个 distributed workflow 的团队。前两者都属于业务代码远大过非业务代码的团队。事实上,在 OCI(Oracle Cloud Infrastructure)多数团队都是如此,几乎都是业务驱动,所谓的纯技术团队(比如做出 lib 或者 service 这样的基础设施来给其它业务团队使用)比较少。很荣幸的是,目前我在的团队就是如此,我们维护一个 distributed workflow 的 software stack,也维护基于它最主要的 service。

总体来看,最近几年和以前比,所在的团队和参与的项目要更加偏重技术一点,日常工作也相对更有趣一点。

项目的角度

业务驱动可以让你赚钱,但是技术支撑不足的业务驱动,也足以毁掉这个项目,甚至它所属的产品。专注于业务和技术的人眼光是很不同的。 专注于业务的人可以让项目的工作围绕着核心价值和“挣钱”这样直接的目标而工作,但是专注于技术的人才可以让项目踏实并具备长远的可持续发展性。

业务驱动可以让用户得到引导,需求得到满足,但是技术驱动才可以让产品具备稳定性和扩展性这些软件工程层面的要素,从而真正发展起来,而不是三天两头地救火。这也是一个项目,最好具备 TPM 和 Dev 这样两种不同角色的其中一个原因。

在被质问道 business impact 的时候,程序员们一定在心里对技术上的优劣有杆公平的秤。毕竟,有那么多人从业务的角度帮你思考价值的问题,却只有你们自己能从技术的角度考虑和评估。下次再有人质疑有什么业务影响的时候,程序员也可以说,无论 xxx 有没有业务影响,它有着 yyy 的技术影响。

团队的角度

我记得以前说过, 技术并不一定只体现在产品本身,时髦的、有趣的的技术带来的影响力,还能帮助吸引人才。这是技术对团队的影响之一,也恰恰是很多人忽视的一点。 听起来很功利是不是?但是现实就是如此,我记得当时在北京 office 的时候,我们给新员工做宣传说,来我们组吧,我们做大数据,我们做分布式计算,我们做机器学习。我看到有的人眼睛就亮了。事实上,对于这些新员工来说,他们可能并不了解,这些事情并不好做,很多情况下都是苦差事,并且,业务影响可能还不如老老实实做传统业务的团队。

但,那又如何?我知道很多程序员的想法是,不只是完成工作,还想要学到东西,并且向学到那些技术前沿的东西。你不能责怪他们不能立足根本,不能责怪他们好高骛远,这是一个技术人自然而然的追求。最起码,我能看到对方敢于打破舒适区的勇气。

反之,如果一个程序员说,我做什么都可以,我不在乎我加入的团队使用什么技术,做什么不是挣钱吃饭?我反而会觉得这样百适的风格可能意味着没有方向上的品鉴和偏好,没有技术上的追求,也没有学习的动力。我对这样的程序员加入团队,反而感到担忧。

我希望看到团队在技术上的追求和氛围,考虑扩展性、复用性和稳定性等各个方面,争论各种工程层面的解决方案,而不是用业务价值衡量一切。

个人的角度

技术上,需要精进,但是在技术上的投资,总体来说是不容易跑偏的。技术需要持续积累的过程。如果你发现自己每天写的代码无非就是加个接口,写一堆一堆的 if-else 面条式的代码,天天折腾在复杂的业务逻辑里面,而没有什么设计,没有什么技术问题的分析,那我觉得你大概应该需要寻求改变了。如果能在自己的组里面找适合自己的项目可以,如果不行,去寻求外部的团队,甚至其他公司,也是一个选择。

要明确的是,这些业务上的知识,只有少数能够积累下来,积累下来的部分,叫做领域知识。而绝大多数,都没有长期的价值,换言之,到了一个新环境,只有技术的东西是自己的,其他的多数都用不着了。这里不是说业务知识没有用,而是说,多数业务知识,对个人的发展,并不能提供持续的价值。有少部分,或者在确定了领域的基础上,当然是有用的。比如我有长期做 billing 的同事,就有着相当的业务积累,但是,这样的人才也会面临跳槽选择面过窄的问题。

有人说,技术上也一样啊,比如几年前的技术,到现在就淘汰了。MFC 淘汰了,CGI 淘汰了,WAP 淘汰了,我们现在用的技术,很快也要淘汰了,而体力跟不上年轻人,学习速度跟不上刚毕业的年轻人,是不是程序员也要被淘汰了?

我认为,技术是个复杂的事情,不是几个知识点,学会了就搞定一切了。这里有积累,并且技术在变化的过程中,绝大多数内容都是相通的,如果一个老程序员,学习某一项相关新技术的时间,或者掌握的深度,和新程序员一样,那么我会怀疑这个老程序员是不是靠谱。

技术上的积累,并不只是在解决实际问题的时候带来有价值的思路,更能够在学习新技术的时候,可以类比。 在和更年轻的程序员竞争的过程中,老程序员的优势,应当是经验、眼界,而不是体力、反应。这也是为什么不要只写那些纯业务代码的原因 ,同样是写码拿钱,还是很不一样的,写纯业务代码,技术上积累的经验和眼界会偏少,并且照猫画虎实现就可以了,容易废弃掉了人思考的习惯,并且,招个年轻人就可以替代掉你。

如果程序员的你发现自己一直在忙于填充业务代码,不做设计,不做架构,如果自己还不对技术世界那么敏感的话,那么要小心了。如果这都不算“搬砖”,那还有什么是“搬砖”?这样长久的工作,会把自己对于技术的热情慢慢消磨掉。

我们都知道要懂得业务变现的重要性,不要变成迂腐的 nerd,就仿佛那“回字有三种写法”一般,而且似乎这个社会都在这样思考;可是我却说,我们更要看到,只有技术,才是程序员继续充满价值存在的最重要的理由。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2X3pr6d

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 LeetCode 题目解答—— 第 416 到 460 题

从第 416 到第 460 题,跳过了需要付费的题目。付费的题目会单独放在一篇里面。

416 Partition Equal Subset Sum 40.0% Medium
417 Pacific Atlantic Water Flow 36.9% Medium
419 Battleships in a Board 65.2% Medium
420 Strong Password Checker 17.9% Hard
421 Maximum XOR of Two Numbers in an Array 50.5% Medium
423 Reconstruct Original Digits from English 45.4% Medium
424 Longest Repeating Character Replacement 43.8% Medium
427 Construct Quad Tree 55.0% Easy
429 N-ary Tree Level Order Traversal 58.5% Easy
430 Flatten a Multilevel Doubly Linked List 40.9% Medium
432 All O`one Data Structure 29.0% Hard
433 Minimum Genetic Mutation 37.5% Medium
434 Number of Segments in a String 36.7% Easy
435 Non-overlapping Intervals 41.4% Medium
436 Find Right Interval 42.4% Medium
437 Path Sum III 42.1% Easy
438 Find All Anagrams in a String 36.7% Easy
440 K-th Smallest in Lexicographical Order 26.3% Hard
441 Arranging Coins 37.6% Easy
442 Find All Duplicates in an Array 60.1% Medium
443 String Compression 37.0% Easy
445 Add Two Numbers II 49.5% Medium
446 Arithmetic Slices II – Subsequence 29.9% Hard
447 Number of Boomerangs 49.4% Easy
448 Find All Numbers Disappeared in an Array 52.9% Easy
449 Serialize and Deserialize BST 46.1% Medium
450 Delete Node in a BST 39.4% Medium
451 Sort Characters By Frequency 55.8% Medium
452 Minimum Number of Arrows to Burst Balloons 46.2% Medium
453 Minimum Moves to Equal Array Elements 49.1% Easy
454 4Sum II 50.4% Medium
455 Assign Cookies 48.3% Easy
456 132 Pattern 27.4% Medium
457 Circular Array Loop 27.5% Medium
458 Poor Pigs 45.3% Hard
459 Repeated Substring Pattern 39.7% Easy
460 LFU Cache 28.6% Hard

Partition Equal Subset Sum
【题目】Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.

Note:

  1. Each of the array element will not exceed 100.
  2. The array size will not exceed 200.

Example 1:

Input: [1, 5, 11, 5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11].

Example 2:

Input: [1, 2, 3, 5]
Output: false
Explanation: The array cannot be partitioned into equal sum subsets.

【解答】要划分数组成两个部分,这两个部分各自的和相等。
其实这道题可以问 k 个部分,两个部分是一个强化了的条件。整个数组的和(sum)是可以很容易得到的,那么分成两个部分,每个部分的和(sum/2)也就可以很容易得到了。于是这道题就变成了,能不能从数组中找出一些数,使之和为 sum/2?搜索求解即可。

求解期间做一点小优化,创建一个<index, <target, partitionable>> 这样的结构,表示源数组的下标 index 开始往后到底的这一段,能否存在一个子数组,使得和为 target。

class Solution {
    public boolean canPartition(int[] nums) {
        if (nums == null || nums.length == 0)
            throw new IllegalArgumentException();
        
        int total = 0;
        for (int num : nums) {
            total += num;
        }
        
        if (total % 2 == 1)
            return false;
        
        int target = total / 2;
        // <index, <target, partitionable>>
        Map<Integer, Map<Integer, Boolean>> cache = new HashMap<>();
        
        return canPartition(nums, cache, target, 0);
    }
    
    private boolean canPartition(int[] nums, Map<Integer, Map<Integer, Boolean>> cache, int target, int index) {
        if (index == nums.length)
            return target == 0;
        
        if (!cache.containsKey(index)) {
            cache.put(index, new HashMap<>());
        }
        Map<Integer, Boolean> subMap = cache.get(index);
        if (subMap.containsKey(target))
            return subMap.get(target);
        
        // current is not selected
        boolean partitionable = canPartition(nums, cache, target, index + 1);
        subMap.put(target, partitionable);
        if (partitionable) {
            subMap.put(target, true);
            return true;
        }
        
        // current is selected
        if (nums[index] <= target) {
            partitionable = canPartition(nums, cache, target - nums[index], index + 1);
        }

        return partitionable;
    }
}

Pacific Atlantic Water Flow
【题目】Given an m x n matrix of non-negative integers representing the height of each unit cell in a continent, the “Pacific ocean” touches the left and top edges of the matrix and the “Atlantic ocean” touches the right and bottom edges.

Water can only flow in four directions (up, down, left, or right) from a cell to another one with height equal or lower.

Find the list of grid coordinates where water can flow to both the Pacific and Atlantic ocean.

Note:

  1. The order of returned grid coordinates does not matter.
  2. Both m and n are less than 150.

Example:

Given the following 5x5 matrix:

  Pacific ~   ~   ~   ~   ~ 
       ~  1   2   2   3  (5) *
       ~  3   2   3  (4) (4) *
       ~  2   4  (5)  3   1  *
       ~ (6) (7)  1   4   5  *
       ~ (5)  1   1   2   4  *
          *   *   *   *   * Atlantic

Return:

[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix).

【解答】太平洋在左上方,大西洋在右下方,要寻找二者的分界线。

太平洋从最左侧和最上方开始,认为数值不断变大就是可达的,寻找并标记所有可达区域。大西洋则是从最右侧和最下方开始,类似的标记。标记完毕以后找到二者重叠的部分,就是分界线。

class Solution {
    public List<int[]> pacificAtlantic(int[][] matrix) {
        if (matrix == null)
            throw new IllegalArgumentException();
        
        List<int[]> result = new ArrayList<>();
        if (matrix.length==0 || matrix[0].length==0)
            return result;
        
        // null: undetermined, true: reachable, false: unreachable
        Boolean[][] pacificReachable = new Boolean[matrix.length][matrix[0].length];
        Queue pacificQueue = new LinkedList<>();
        // top row
        for (int j=0; j<matrix[0].length; j++) {
            pacificReachable[0][j] = true;
            pacificQueue.offer(new Point(0, j));
        }
        // left column
        for (int i=0; i<matrix.length; i++) {
            pacificReachable[i][0] = true;
            pacificQueue.offer(new Point(i, 0));
        }
        // find all the nodes pacific water flow can reach
        iterate(matrix, pacificQueue, pacificReachable);
        
        Boolean[][] atlanticReachable = new Boolean[matrix.length][matrix[0].length];
        Queue atlanticQueue = new LinkedList<>();
        // bottom row
        for (int j=0; j<matrix[0].length; j++) {
            atlanticReachable[matrix.length-1][j] = true;
            atlanticQueue.offer(new Point(matrix.length-1, j));
        }
        // right column
        for (int i=0; i<matrix.length; i++) {
            atlanticReachable[i][matrix[0].length-1] = true;
            atlanticQueue.offer(new Point(i, matrix[0].length-1));
        }
        iterate(matrix, atlanticQueue, atlanticReachable);
        
        for (int i=0; i<matrix.length; i++) {
            for (int j=0; j<matrix[0].length; j++) {
                if (pacificReachable[i][j] == Boolean.TRUE && atlanticReachable[i][j] == Boolean.TRUE) {
                    result.add(new int[] {i, j});
                }
            }
        }
        
        return result;
    }
    
    private void iterate(int[][] matrix, Queue queue, Boolean[][] reachable) {
        while (!queue.isEmpty()) {
            Point p = queue.poll();
            
            if (this.mark(matrix, p.x, p.y, p.x-1, p.y, reachable))
                queue.add(new Point(p.x-1, p.y));
            if (this.mark(matrix, p.x, p.y, p.x, p.y-1, reachable))
                queue.add(new Point(p.x, p.y-1));
            if (this.mark(matrix, p.x, p.y, p.x+1, p.y, reachable))
                queue.add(new Point(p.x+1, p.y));
            if (this.mark(matrix, p.x, p.y, p.x, p.y+1, reachable))
                queue.add(new Point(p.x, p.y+1));
        }
    }
    
    private boolean mark(int[][] matrix, int x, int y, int nx, int ny, Boolean[][] reachable) {
        if (nx<0 || ny<0 || nx>=matrix.length || ny>=matrix[0].length || reachable[nx][ny] != null)
            return false;
        
        if (matrix[x][y] <= matrix[nx][ny]) {
            reachable[nx][ny] = true;
            return true;
        }
        
        return false;
    }
}

class Point {
    public int x;
    public int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Battleships in a Board
【题目】Given an 2D board, count how many battleships are in it. The battleships are represented with 'X's, empty slots are represented with '.'s. You may assume the following rules:

  • You receive a valid board, made of only battleships or empty slots.
  • Battleships can only be placed horizontally or vertically. In other words, they can only be made of the shape 1xN (1 row, N columns) or Nx1 (N rows, 1 column), where N can be of any size.
  • At least one horizontal or vertical cell separates between two battleships – there are no adjacent battleships.

Example:

X..X
...X
...X

In the above board there are 2 battleships.

Invalid Example:

...X
XXXX
...X

This is an invalid board that you will not receive – as battleships will always have a cell separating between them.

Follow up:
Could you do it in one-pass, using only O(1) extra memory and without modifying the value of the board?

【解答】要数有多少 battleship,并且要求使用 O(1) 的空间复杂度,还不能修改 board 上的数值。

一行一行遍历,每一行中从左往右遍历。对于每一个点,如果左侧和上方都不是 X,那就认为这是一艘新的 battleship。

class Solution {
    public int countBattleships(char[][] board) {
        int count = 0;
        
        for (int i=0; i<board.length; i++) {
            for (int j=0; j<board[0].length; j++) { if (board[i][j]=='.') continue; if (j>0 && board[i][j-1]=='X') continue;
                if (i>0 && board[i-1][j]=='X') continue;
                count++;
            }
        }
        
        return count;
    }
}

Strong Password Checker
【题目】A password is considered strong if below conditions are all met:

  1. It has at least 6 characters and at most 20 characters.
  2. It must contain at least one lowercase letter, at least one uppercase letter, and at least one digit.
  3. It must NOT contain three repeating characters in a row (“…aaa…” is weak, but “…aa…a…” is strong, assuming other conditions are met).

Write a function strongPasswordChecker(s), that takes a string s as input, and return the MINIMUM change required to make s a strong password. If s is already strong, return 0.

Insertion, deletion or replace of any one character are all considered as one change.

【解答】规则很容易理解:

  1. 6~20 个字符;
  2. 必须包含小写字符、大写字符和数字;
  3. 不能包含连续三个相同字符。

但是困难的地方在于,题目不是问一个字符串是否符合条件,而是要求怎样通过最少的增删改来使得密码符合规则。题目类似于求编辑距离,只不过编辑距离只有一头(源字符串)确定,另一头是不确定的。如果只有一个规则,也会简单一些,三个规则混到一起,互相影响,使得题目非常困难。

挣扎了一段时间,就去讨论区 看解答 去了:

class Solution {
    public int strongPasswordChecker(String s) {
        int res = 0, a = 1, A = 1, d = 1;
        char[] carr = s.toCharArray();
        int[] arr = new int[carr.length];

        for (int i = 0; i < arr.length;) {
            if (Character.isLowerCase(carr[i])) a = 0;
            if (Character.isUpperCase(carr[i])) A = 0;
            if (Character.isDigit(carr[i])) d = 0;

            int j = i;
            while (i < carr.length && carr[i] == carr[j]) i++;
            arr[j] = i - j;
        }

        int total_missing = (a + A + d);

        if (arr.length < 6) {
            res += total_missing + Math.max(0, 6 - (arr.length + total_missing));

        } else {
            int over_len = Math.max(arr.length - 20, 0), left_over = 0;
            res += over_len;

            for (int k = 1; k < 3; k++) {
                for (int i = 0; i < arr.length && over_len > 0; i++) {
                    if (arr[i] < 3 || arr[i] % 3 != (k - 1)) continue;
                    arr[i] -= Math.min(over_len, k);
                    over_len -= k;
                }
            }

            for (int i = 0; i < arr.length; i++) { if (arr[i] >= 3 && over_len > 0) {
                    int need = arr[i] - 2;
                    arr[i] -= over_len;
                    over_len -= need;
                }

                if (arr[i] >= 3) left_over += arr[i] / 3;
            }

            res += Math.max(total_missing, left_over);
        }

        return res;
    }
}

Maximum XOR of Two Numbers in an Array
【题目】Given a non-empty array of numbers, a0, a1, a2, … , an-1, where 0 ≤ ai < 231.

Find the maximum result of ai XOR aj, where 0 ≤ ij < n.

Could you do this in O(n) runtime?

Example:

Input: [3, 10, 5, 25, 2, 8]

Output: 28

Explanation: The maximum result is 5 ^ 25 = 28.

【解答】要在 O(n) 时间内找两个数 XOR 的最大值。

既然是 O(n) 时间,排序什么的就别想了。拿到手感觉没有什么思路,要去找全部的 XOR 结果暴力解法需要 n 平方的复杂度。再有一点,我觉得可以从高位往低位一位一位去分析,而每一位都是独立的,且优先级有着明确的区别。比如数值部分的最高位在 XOR 能得到 1,总共有 x 种组合,那么在分析次高位的时候,只需要考虑这 x 种中,寻找能够得到 XOR 结果为 1 的即可。但是思路再往下,又不太好继续了。

下面的思路借鉴自 讨论区 的一个解法。现在 Medium 的题目居然也需要看解答了,叹气。

class Solution {
    public int findMaximumXOR(int[] nums) {
        int max = 0, mask = 0;
        for (int i = 31; i >= 0; i--){
            // part 1:
            // prefix mask
            mask = mask | (1 << i);
            // prefix set
            Set set = new HashSet<>();
            for (int num : nums){
                set.add(num & mask);
            }
            
            // part 2:
            int tmp = max | (1 << i);
            for (int prefix : set){
                if(set.contains(tmp ^ prefix)) {
                    max = tmp;
                    break;
                }
            }
        }
        return max;
    }
}

我把思路整理了一下来说明上述代码。求解需要利用一个特性:若 a ^ b = c, a ^ c = b。循环体内的代码说明:

  • part 1:利用 mask 来取所有数的 prefix,第一次一个最高位,第二次为两个最高位,第三次为三个最高位…… 这些 prefix 组成一个 set。
  • part 2:现在假设第 n 次迭代所得到的最大值是 max,那么考虑第 n+1 次迭代:假设新确定的那一位是 1,那么设这个数为 tmp,然后把 tmp 和所有 prefix 进行 XOR 操作,如果得到的数还在这个 prefix set 里面,根据前面提到的特性,说明有两个 prefix 进行 XOR 以后可以得到这个 tmp,那么这个 tmp 就是新的最大值 max,否则这一位只能是 0,那么 max 不变。

Reconstruct Original Digits from English
【题目】Given a non-empty string containing an out-of-order English representation of digits 0-9, output the digits in ascending order.

Note:

  1. Input contains only lowercase English letters.
  2. Input is guaranteed to be valid and can be transformed to its original digits. That means invalid inputs such as “abc” or “zerone” are not permitted.
  3. Input length is less than 50,000.

Example 1:

Input: "owoztneoer"

Output: "012"

Example 2:

Input: "fviefuro"

Output: "45"

【解答】要从无序的英文字母串还原到数字。

考虑个数字的英文所包含字母的特性。比如说字母 z,就只可能在 zero 中出现,因此很容易就找到了 zero 的个数。有一些并不是 unique 的,比如 seven,每个字母都可能在别的英文数中出现,但是 s 只能再 six 和 seven 中出现,因此一旦知道了 six,把 s 的次数减去 six 的次数,就可以得到 seven 的次数了。

class Solution {
    public String originalDigits(String s) {
        // unique:
        // zero
        // 'z' => 0
        // two
        // 'w' => 2
        // four
        // 'u' => 4
        // six
        // 'x' => 6
        // eight
        // 'g' => 8
        
        // not unique:
        // seven = 's' - '6'
        // 's' => 7
        // five = 'v' - '7'
        // 'v' => 5
        // one = 'o' - '0' - '2' - '4'
        // 'o' => 1
        // three = 'h' - 8
        // 'h' => 3
        // nine = 'i' - '6' - '8' - '5'
        // 'i' => 9
        
        Map<Character, Integer> countMap = new HashMap<>();
        for (int i=0; i<s.length(); i++) {
            char ch = s.charAt(i);
            if (!countMap.containsKey(ch))
                countMap.put(ch, 0);
            countMap.put(ch, countMap.get(ch) + 1);
        }
        
        int[] digits = new int[10];
        Integer count = countMap.get('z');
        if (count != null) {
            digits[0] = count;
        }
        count = countMap.get('w');
        if (count != null) {
            digits[2] = count;
        }
        count = countMap.get('u');
        if (count != null) {
            digits[4] = count;
        }
        count = countMap.get('x');
        if (count != null) {
            digits[6] = count;
        }
        count = countMap.get('g');
        if (count != null) {
            digits[8] = count;
        }
        
        count = countMap.get('s');
        if (count != null) {
            digits[7] = count - digits[6];
        }
        count = countMap.get('v');
        if (count != null) {
            digits[5] = count - digits[7];
        }
        count = countMap.get('o');
        if (count != null) {
            digits[1] = count - digits[0] - digits[2] - digits[4];
        }
        count = countMap.get('h');
        if (count != null) {
            digits[3] = count - digits[8];
        }
        count = countMap.get('i');
        if (count != null) {
            digits[9] = count - digits[6] - digits[8] - digits[5];
        }
        
        StringBuilder sb = new StringBuilder();
        for (int i=0; i<digits.length; i++) {
            for (int j=0; j<digits[i]; j++)
                sb.append(i);
        }
        return sb.toString();
    }
}

Longest Repeating Character Replacement
【题目】Given a string that consists of only uppercase English letters, you can replace any letter in the string with another letter at most ktimes. Find the length of a longest substring containing all repeating letters you can get after performing the above operations.

Note:
Both the string’s length and k will not exceed 104.

Example 1:

Input:
s = "ABAB", k = 2

Output:
4

Explanation:
Replace the two 'A's with two 'B's or vice versa.

Example 2:

Input:
s = "AABABBA", k = 1

Output:
4

Explanation:
Replace the one 'A' in the middle with 'B' and form "AABBBBA".
The substring "BBBB" has the longest repeating letters, which is 4.

【解答】要求在字符串 s 中替换字符 k 次,得到最长的连续字符子串。

如果用回溯法或者动态规划复杂度比较高,而且不好优化。但是,把这个问题转化为一个 sliding window 的问题,一下就豁然开朗了:

  • 假设说有一个 sliding window,从左慢慢往右划,不断调整左右边界:左边吐出 char,右边吃进 char;
  • 这个 window 里面的不同 char 各有多少个,通过一个 map 来记录;
  • 这个 window 的左边界所对应的 char 作为所考察的 repeating char,在左边界固定的基础上不断右移右边界,记录 window 的最大值,如果其他 char 的数量超过了 k,那么需要右移左边界,吐出一个之前的 repeating char。

这类字符串寻找连续子串的问题,都可以考虑 sliding window 的方法。使用 sliding window 的一个好处有这么两个:

  1. 窗口内字符排列或数量增量变化。无论移动左侧还是右侧窗口边沿,都只变化一个字符。
  2. 窗口的左侧或者右侧固定,从而简化问题。像这道题就是总是考虑窗口左侧的 char 为 repeating char。
class Solution {
    public int characterReplacement(String s, int k) {
        if (s==null || k<0)
            throw new IllegalArgumentException();
        if (s.length()==0)
            return 0;
        
        int left=0, right=0;
        int max = 1;
        
        char[] counters = new char[26];
        counters[s.charAt(0) - 'A']++;
        
        while (right<s.length()) {
            int base = s.charAt(left) - 'A';
            int replacementCount = 0;
            for (int i=0; i<26; i++) {
                if (i!=base) {
                    replacementCount += counters[i];
                }
            }
            
            if (replacementCount>k) {
                counters[base]--;
                left++;
            } else {
                int newSize = right-left+1;
                max = Math.max(max, newSize);
                right++;
                if (right<s.length()) {
                    counters[s.charAt(right) - 'A']++;
                } else {
                    // special case: there are still change chances left
                    max = Math.max(max, newSize + k - replacementCount);
                }
            }
        }
        
        // never exceeds the length of s
        return Math.min(max, s.length());
    }
}

Construct Quad Tree
【题目】We want to use quad trees to store an N x N boolean grid. Each cell in the grid can only be true or false. The root node represents the whole grid. For each node, it will be subdivided into four children nodes until the values in the region it represents are all the same.

Each node has another two boolean attributes : isLeaf and valisLeaf is true if and only if the node is a leaf node. The val attribute for a leaf node contains the value of the region it represents.

Your task is to use a quad tree to represent a given grid. The following example may help you understand the problem better:

Given the 8 x 8 grid below, we want to construct the corresponding quad tree:

It can be divided according to the definition above:

 

The corresponding quad tree should be as following, where each node is represented as a (isLeaf, val) pair.

For the non-leaf nodes, val can be arbitrary, so it is represented as *.

Note:

  1. N is less than 1000 and guaranteened to be a power of 2.
  2. If you want to know more about the quad tree, you can refer to its wiki.

【解答】构造 Quad Tree。

没有太多可以说的。递归判断并合并节点,形成 quad tree。

class Solution {
    public Node construct(int[][] grid) {
        if (grid==null || grid.length==0 || grid.length!=grid[0].length)
            throw new IllegalArgumentException();
        
        return this.construct(grid, 0, 0, grid.length);
    }
    
    private Node construct(int[][] grid, int x, int y, int w) {
        if (w==1) {
            return new Node(grid[x][y]!=0, true, null, null, null, null);
        }
        
        Node topLeft = this.construct(grid, x, y, w/2);
        Node topRight = this.construct(grid, x, y+w/2, w/2);
        Node bottomLeft = this.construct(grid, x+w/2, y, w/2);
        Node bottomRight = this.construct(grid, x+w/2, y+w/2, w/2);
        
        // all leaves and equal, merge
        if (topLeft.isLeaf &&
            topRight.isLeaf &&
            bottomLeft.isLeaf &&
            bottomRight.isLeaf &&
            topLeft.val == topRight.val &&
            topLeft.val == bottomLeft.val &&
            topLeft.val == bottomRight.val
        ) {
            return new Node(topLeft.val, true, null, null, null, null);
        }
        
        // otherwise create a sub tree
        return new Node(false, false, topLeft, topRight, bottomLeft, bottomRight);
    }
}

N-ary Tree Level Order Traversal
【题目】

Given an n-ary tree, return the level order traversal of its nodes’ values. (ie, from left to right, level by level).

For example, given a 3-ary tree:

We should return its level order traversal:

[
     [1],
     [3,2,4],
     [5,6]
]

Note:

  1. The depth of the tree is at most 1000.
  2. The total number of nodes is at most 5000.

【解答】没有太多可以说的。循环内按层遍历就好。

class Solution {
    public List<List> levelOrder(Node root) {
        List<List> result = new ArrayList<>();
        if (root==null)
            return result;
        
        Queue level = new LinkedList<>();
        level.add(root);
        while (!level.isEmpty()) {
            Queue nextLevel = new LinkedList<>();
            List list = new ArrayList<>();
            for (Node node : level) {
                list.add(node.val);
                if (node.children != null)
                    nextLevel.addAll(node.children);
            }
            result.add(list);
            level = nextLevel;
        }
        
        return result;
    }
}

Flatten a Multilevel Doubly Linked List
【题目】You are given a doubly linked list which in addition to the next and previous pointers, it could have a child pointer, which may or may not point to a separate doubly linked list. These child lists may have one or more children of their own, and so on, to produce a multilevel data structure, as shown in the example below.

Flatten the list so that all the nodes appear in a single-level, doubly linked list. You are given the head of the first level of the list.

Example:

Input:
 1---2---3---4---5---6--NULL
         |
         7---8---9---10--NULL
             |
             11--12--NULL
Output:
1-2-3-7-8-11-12-9-10-4-5-6-NULL

Explanation for the above example:

Given the following multilevel doubly linked list:

We should return the following flattened doubly linked list:

【解答】要把多层的双向链表压平。
大致思路上应该说没有什么难的,但是细节处理的坑比较多。源链表节点的 next 始终要放到 stack 里面去,然后再看 child,如果 child 不为空,直接从 stack 里面取下一个节点;如果为空,则优先使用 child 为下一个节点。
在选中下一个节点之后,再重新链接的过程中,注意覆盖节点的所有 field,包括 next、prev、child。

class Solution {
    public Node flatten(Node head) {
        if (head==null)
            return null;
        
        Stack stack = new Stack<>();
        Node cur = head;
        while (true) {
            // always push the next node to stack
            if (cur.next != null)
                stack.push(cur.next);

            Node next = cur.child;
            if (next == null) {
                // make child as the next, but if child is null, pop the next from stack
                if (!stack.isEmpty()) {
                    next = stack.pop();
                } else {
                    cur.child = null;
                    cur.next = null;
                    break;
                }
            }
            
            // link current to next
            cur.child = null;
            next.prev = cur;
            cur.next = next;
            
            cur = next;
        }
        
        return head;
    }
}

All O`one Data Structure
【题目】Implement a data structure supporting the following operations:

  1. Inc(Key) – Inserts a new key with value 1. Or increments an existing key by 1. Key is guaranteed to be a non-empty string.
  2. Dec(Key) – If Key’s value is 1, remove it from the data structure. Otherwise decrements an existing key by 1. If the key does not exist, this function does nothing. Key is guaranteed to be a non-empty string.
  3. GetMaxKey() – Returns one of the keys with maximal value. If no element exists, return an empty string "".
  4. GetMinKey() – Returns one of the keys with minimal value. If no element exists, return an empty string "".

Challenge: Perform all these in O(1) time complexity.

【解答】实现一个数据结构,加一、减一,获取最大值的 key 和最小值的 key,都是 O(1) 的时间复杂度。

这类题目可以说遇到多次了,有一些通用思路:

  • 要 O(1),根据 key 去取 value 的,无非就两种数据结构,一个是 HashMap,一个是数组(下标访问)。
  • 如果有根据 key 取 value,那就需要从 key 到 value 的映射;如果有根据 value 取 key,那就需要 value 到 key 的映射。
  • 这道题看起来需要两者:
    • 根据 key 要获取 value 对象,从而进行 inc 和 dec 的操作;
    • 根据 value 的大小情况,来找到对象并返回相应的 key。
  • 要能 O(1) 得到最大值和最小值,肯定不能在需要的时候去现找,那就需要在平时维护一个有序列表,一头最大,一头最小。这个列表的大小比较实际只靠 value,具体这个 value 有多少个 key 对应并不重要。因而这个序号并不是 key 根据 value 的值实际的序号,而是互不重复的 value 进行比较得到的序号。
  • 在 inc 和 dec 的时候,由于变化只有 1,位置调整于是也只有 1。这就是为什么要根据互不重复的 value 来排序的原因,这样的情况一定能保证调整的幅度不超过 1。

有了这样的思路以后,建立一个 Item 元素,作为 value,里面需要存放前一个节点、后一个节点,实际取值,以及 key set。这里的前一个、后一个节点是用来保序用的。建立的 head 和 tail 是用于包住 value 形成的有序串,简化计算用的。

class Item {
    public int value;
    public Item next;
    public Item prev;
    public Set keys = new HashSet<>();;
}
class AllOne {
    
    private Map<String, Item> map = new HashMap<>();
    private Item head = new Item();
    private Item tail = new Item();
    

    /** Initialize your data structure here. */
    public AllOne() {
        this.head.next = this.tail;
        this.tail.prev = this.head;
        // dummy head and dummy tail
        this.head.value = Integer.MIN_VALUE;
        this.tail.value = Integer.MAX_VALUE;
    }
    
    private void link(Item left, Item right) {
        left.next = right;
        right.prev = left;
    } 
    
    /** Inserts a new key  with value 1. Or increments an existing key by 1. */
    public void inc(String key) {
        Item currentItem = this.map.get(key);
        if (currentItem != null) { // it's an existing key
            currentItem.keys.remove(key);
            int newValue = currentItem.value + 1;
            Item next = currentItem.next;
            if (next.value == newValue) {
                // the item for the newValue is already existed
                map.put(key, next);
                next.keys.add(key);
            } else {
                // there is no item for newValue, create one
                Item newItem = new Item();
                newItem.value = newValue;
                newItem.keys.add(key);
                
                this.link(currentItem, newItem);
                this.link(newItem, next);
                
                map.put(key, newItem);
            }
            
            // remove the node if there's no key mapped to its value
            if (currentItem.keys.isEmpty()) {
                this.link(currentItem.prev, currentItem.next);
            }
        } else { // it's a new key
            if (this.head.next.value == 1) {
                // the item for new key (value==1) is already existed
                this.head.next.keys.add(key);
                map.put(key, this.head.next);
            } else {
                // new item needed
                Item next = this.head.next;
                Item newItem = new Item();
                newItem.value = 1;
                newItem.keys.add(key);
                
                this.link(this.head, newItem);
                this.link(newItem, next);
                
                map.put(key, newItem);
            }
        }
    }
    
    /** Decrements an existing key by 1. If Key's value is 1, remove it from the data structure. */
    public void dec(String key) {
        Item currentItem = this.map.get(key);
        if (currentItem == null)
            return;
        
        Item prev = currentItem.prev;
        currentItem.keys.remove(key);
        // remove the item if no key mapped to it
        if (currentItem.keys.isEmpty()) {
            this.link(prev, currentItem.next);
        }

        if (currentItem.value == 1) {
            this.map.remove(key);
            return;
        }
        
        int newValue = currentItem.value - 1;
        // there's already an item existed for newValue
        if (prev.value == newValue) {
            prev.keys.add(key);
            this.map.put(key, prev);
            return;
        }
        
        // there is no item for newValue, create one
        Item newItem = new Item();
        newItem.value = newValue;
        newItem.keys.add(key);

        Item next = prev.next;
        this.link(prev, newItem);
        this.link(newItem, next);

        map.put(key, newItem);    
    }
    
    /** Returns one of the keys with maximal value. */
    public String getMaxKey() {
        if (this.head.next == this.tail)
            return "";
        return this.tail.prev.keys.iterator().next();
    }
    
    /** Returns one of the keys with Minimal value. */
    public String getMinKey() {
        if (this.head.next == this.tail)
            return "";
        return this.head.next.keys.iterator().next();
    }
}

Minimum Genetic Mutation
【题目】A gene string can be represented by an 8-character long string, with choices from "A""C""G""T".

Suppose we need to investigate about a mutation (mutation from “start” to “end”), where ONE mutation is defined as ONE single character changed in the gene string.

For example, "AACCGGTT" -> "AACCGGTA" is 1 mutation.

Also, there is a given gene “bank”, which records all the valid gene mutations. A gene must be in the bank to make it a valid gene string.

Now, given 3 things – start, end, bank, your task is to determine what is the minimum number of mutations needed to mutate from “start” to “end”. If there is no such a mutation, return -1.

Note:

  1. Starting point is assumed to be valid, so it might not be included in the bank.
  2. If multiple mutations are needed, all mutations during in the sequence must be valid.
  3. You may assume start and end string is not the same.

Example 1:

start: "AACCGGTT"
end:   "AACCGGTA"
bank: ["AACCGGTA"]

return: 1

Example 2:

start: "AACCGGTT"
end:   "AAACGGTA"
bank: ["AACCGGTA", "AACCGCTA", "AAACGGTA"]

return: 2

Example 3:

start: "AAAAACCC"
end:   "AACCCCCC"
bank: ["AAAACCCC", "AAACCCCC", "AACCCCCC"]

return: 3

【解答】维护一个当前考察的字符串集合和一个字典,每次都拿当前字符串去和字典里的所有候选字符串比较,如果只差 1,就把候选从字典里面拿出来,更新到当前考察的字符串集合中。直到发现解,或者字典为空,或者在最近一次比较中没有发现任何匹配,就表示算法已经结束。

class Solution {
    public int minMutation(String start, String end, String[] bank) {
        if (start==null || end==null || bank==null)
            throw new IllegalArgumentException();
        if (start.equals(end))
            return 0;
        if (start.length() != end.length() || bank.length==0)
            return -1;
        
        Set bankSet = new HashSet<>(Arrays.asList(bank));
        Set current = new HashSet<>();
        current.add(start);
        
        int count = 0;
        while (!current.isEmpty() && !bankSet.isEmpty()) {
            count++;
            Set newCurrent = new HashSet<>();
            for (String cur : current) {
                Set newBank = new HashSet<>();
                for (String b : bankSet) {
                    if (this.diffOne(cur, b)) {
                        if (b.equals(end))
                            return count;
                        else
                            newCurrent.add(b);
                    } else {
                        newBank.add(b);
                    }
                }
                bankSet = newBank;
            }
            current = newCurrent;
        }
        
        return -1;
    }
    
    private boolean diffOne(String left, String right) {
        if (left.length() != right.length())
            return false;
        
        boolean changed = false;
        for (int i=0; i<left.length(); i++) {
            if (left.charAt(i) != right.charAt(i)) {
                if (changed)
                    return false;
                else
                    changed = true;
            }
        }
        
        return true;
    }
}

Number of Segments in a String
【题目】Count the number of segments in a string, where a segment is defined to be a contiguous sequence of non-space characters.

Please note that the string does not contain any non-printable characters.

Example:

Input: "Hello, my name is John"
Output: 5

【解答】常规题。没有太多可说的,注意一些特殊 case 是否被覆盖到,比如首尾空格。

class Solution {
    public int countSegments(String s) {
        boolean isSpace = true;
        int count = 0;
        for (int i=0; i<s.length(); i++) {
            char ch = s.charAt(i);
            if (ch!=' ' && isSpace) {
                isSpace = false;
            } else if (ch==' ' && !isSpace) {
                count++;
                isSpace = true;
            }
        }
        
        if (!isSpace)
            count++;
        
        return count;
    }
}

Non-overlapping Intervals
【题目】Given a collection of intervals, find the minimum number of intervals you need to remove to make the rest of the intervals non-overlapping.

Note:

  1. You may assume the interval’s end point is always bigger than its start point.
  2. Intervals like [1,2] and [2,3] have borders “touching” but they don’t overlap each other.

Example 1:

Input: [ [1,2], [2,3], [3,4], [1,3] ]

Output: 1

Explanation: [1,3] can be removed and the rest of intervals are non-overlapping.

Example 2:

Input: [ [1,2], [1,2], [1,2] ]

Output: 2

Explanation: You need to remove two [1,2] to make the rest of intervals non-overlapping.

Example 3:

Input: [ [1,2], [2,3] ]

Output: 0

Explanation: You don't need to remove any of the intervals since they're already non-overlapping.

【解答】要求去掉最少的 interval 使得剩余的 interval 无重叠。

一开始,觉得这道题应该比较简单,扫描线问题嘛。上来先排序,完毕之后从左往右扫一遍——这也算是常规思路了。于是我把 intervals 按照 start 排序,然后尝试从中拿掉某些。结果超时了,于是使用一个二维数组优化,内存使用又超了:

class Solution {
    public int eraseOverlapIntervals(Interval[] intervals) {
        Arrays.sort(intervals, new Comparator(){
            @Override
            public int compare(Interval left, Interval right) {
                if (left.start != right.start)
                    return left.start - right.start;
                // this is to make sure when start is same, we can always remove the left one and keep the right one as the right one is shorter
                return right.end - left.end;
            }
        });
        
        Integer[][] cache = new Integer[intervals.length][intervals.length];
        return this.cal(intervals, 0, -1, cache);
    }
    
    private int cal(Interval[] intervals, int index, int last, Integer[][] cache) {
        if (index >= intervals.length)
            return 0;
        
        if (last!=-1 && cache[index][last] != null)
            return cache[index][last];
        
        int count = 0;
        for (int i=index; i<intervals.length; i++) {
            Interval interval = intervals[i];
            
            if (last==-1) {
                last = i;
            } else if (intervals[last].start == interval.start) {
                count++;
                last = i;
            } else if (intervals[last].end <= interval.start) {
                // no overlapping
                last = i;
            } else {
                // it's the most complicated case as we don't know which should be removed: "last" or "i"
                int c1 = this.cal(intervals, i+1, last, cache);
                int c2 = this.cal(intervals, i+1, i, cache);
                count++;
                count += Math.min(c1, c2);
                break;
            }
        }
        
        cache[index][last] = count;
        return count;
    }
}

现在换一个角度,反过来想,如果是从零开始,找尽量多的无重叠 interval 呢?把搜索问题变成构造问题。

考虑排序,可根据什么排呢?二者是很不一样的。用 end 来排序,而不是 start:

  • 如果按照 start 排序,从左到右扫描的过程中,start 大或者小并不代表该 interval 是否应该被选中,因为需要考虑几个重叠 interval 之间覆盖的竞争关系。对右侧的 interval 来说,和不同的左侧 interval 产生重叠,但 start 更大不代表这个重叠范围更大或者更小。因此单纯按照 start 排序没有意义。
  • 而如果按照 end 排序,从小到大扫描的时候,只要发现重叠就只需要忽略后扫描到的,而根本不需要考虑 start 在哪里。因为对于右侧的 interval 来说,左侧元素的 start 在哪里不重要,重要的是 end 在哪里,end 才是决定了重叠部分右边界位置的因素。我们当然希望这个右边界越靠左越好,因此我们在扫描的过程中,尽可能留下先遇到的 end。
class Solution {
    public int eraseOverlapIntervals(Interval[] intervals) {
        Arrays.sort(intervals, new Comparator(){
            @Override
            public int compare(Interval left, Interval right) {
                return left.end - right.end;
            }
        });
        
        int count = 0;
        Interval last = null;
        for (Interval interval : intervals) {
            // intervals are sorted by "end"
            if (last==null || interval.start >= last.end) {
                last = interval;
                count++;
            }
        }
        
        return intervals.length - count;
    }
}

Find Right Interval
【题目】Given a set of intervals, for each of the interval i, check if there exists an interval j whose start point is bigger than or equal to the end point of the interval i, which can be called that j is on the “right” of i.

For any interval i, you need to store the minimum interval j’s index, which means that the interval j has the minimum start point to build the “right” relationship for interval i. If the interval j doesn’t exist, store -1 for the interval i. Finally, you need output the stored value of each interval as an array.

Note:

  1. You may assume the interval’s end point is always bigger than its start point.
  2. You may assume none of these intervals have the same start point.

Example 1:

Input: [ [1,2] ]

Output: [-1]

Explanation: There is only one interval in the collection, so it outputs -1.

Example 2:

Input: [ [3,4], [2,3], [1,2] ]

Output: [-1, 0, 1]

Explanation: There is no satisfied "right" interval for [3,4].
For [2,3], the interval [3,4] has minimum-"right" start point;
For [1,2], the interval [2,3] has minimum-"right" start point.

Example 3:

Input: [ [1,4], [2,3], [3,4] ]

Output: [-1, 2, -1]

Explanation: There is no satisfied "right" interval for [1,4] and [3,4].
For [2,3], the interval [3,4] has minimum-"right" start point.

【解答】对于每个 interval,要找有多少 interval 在它的右边。

肯定不能死算。仔细想一下,其实对每个 interval 我只关心它的右边界,和剩余所有 interval 的左边界。

如果它的右边界确定的时候,要找所有比它小的左边界,这就是一个典型的使用 TreeMap 的问题——key 是所有 interval 的左边界;value 是源数组中的 index。

class Solution {
    public int[] findRightInterval(Interval[] intervals) {
        if (intervals == null)
            throw new IllegalArgumentException();
        
        TreeMap<Integer, Integer> map = new TreeMap<>();
        for (int i=0; i<intervals.length; i++) {
            Interval interval = intervals[i];
            map.put(interval.start, i);
        }
        
        int[] result = new int[intervals.length];
        for (int i=0; i<intervals.length; i++) {
            Interval interval = intervals[i];
            Map.Entry<Integer, Integer> entry = map.ceilingEntry(interval.end);
            if (entry==null)
                result[i] = -1;
            else
                result[i] = entry.getValue();
        }
        
        return result;
    }
}

Path Sum III
【题目】You are given a binary tree in which each node contains an integer value.

Find the number of paths that sum to a given value.

The path does not need to start or end at the root or a leaf, but it must go downwards (traveling only from parent nodes to child nodes).

The tree has no more than 1,000 nodes and the values are in the range -1,000,000 to 1,000,000.

Example:

root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8

      10
     /  \
    5   -3
   / \    \
  3   2   11
 / \   \
3  -2   1

Return 3. The paths that sum to 8 are:

1.  5 -> 3
2.  5 -> 2 -> 1
3. -3 -> 11

【解答】递归求解。但是在递归的过程中,不需要记录每个解分别是由那几个数累加起来的,但是需要记录都可以得到哪些和,不需要去重。因为不同的节点加起来可以得到同样的和,但是实际是算作不同的解。

class Solution {
    public int pathSum(TreeNode root, int sum) {
        if (root==null) return 0;
        return travel(root, new ArrayList<>(), sum);
    }
    
    private int travel(TreeNode root, List list, int sum) {
        if (root==null) return 0;
        
        int total = 0;
        if (root.val == sum) total++;
        
        List newList = new ArrayList<>();
        newList.add(root.val);
        for (int num : list) {
            int added = num + root.val;
            newList.add(added);
            if (added==sum) total++;
        }
        
        int left = travel(root.left, newList, sum);
        int right = travel(root.right, newList, sum);
        
        total += left;
        total += right;
        
        return total;
    }
}

Find All Anagrams in a String
【题目】Given a string s and a non-empty string p, find all the start indices of p‘s anagrams in s.

Strings consists of lowercase English letters only and the length of both strings s and p will not be larger than 20,100.

The order of output does not matter.

Example 1:

Input:
s: "cbaebabacd" p: "abc"

Output:
[0, 6]

Explanation:
The substring with start index = 0 is "cba", which is an anagram of "abc".
The substring with start index = 6 is "bac", which is an anagram of "abc".

Example 2:

Input:
s: "abab" p: "ab"

Output:
[0, 1, 2]

Explanation:
The substring with start index = 0 is "ab", which is an anagram of "ab".
The substring with start index = 1 is "ba", which is an anagram of "ab".
The substring with start index = 2 is "ab", which is an anagram of "ab".

【解答】要找成为 anagram 的子串,基本上最直接的方法就是使用滑动窗口了。而且是个固定大小的滑动窗口。我当时的做法不是特别好,虽然也解出来了,但没有充分利用固定大小这个特性,于是代码写得有些啰嗦。

class Solution {
    private void updateMap(Map<Character, Integer> charMap, char ch, int count) {
        if (count==0)
            charMap.remove(ch);
        else
            charMap.put(ch, count);
    }
    
    public List findAnagrams(String s, String p) {
        if (s==null || p==null || p.length()==0)
            throw new IllegalArgumentException();
        
        List result = new ArrayList<>();
        if (s.length()==0)
            return result;
        
        Map<Character, Integer> charMap = new HashMap<>();
        for (int i=0; i<p.length(); i++) {
            char ch = p.charAt(i);
            int count = charMap.getOrDefault(ch, 0);
            charMap.put(ch, ++count);
        }
        
        int left=0, right=0;
        while (right<s.length()) {
            // always load s[right] at the beginning of each iteration
            char cr = s.charAt(right);
            int count = charMap.getOrDefault(cr, 0) - 1;
            this.updateMap(charMap, cr, count);
            
            // anagram found
            if (charMap.isEmpty()) {
                result.add(left);
                // move both boundaries
                charMap.put(s.charAt(left), 1);
                left++;
                right++;
                continue;
            }
            
            // now try moving left only, and break the loop when:
            // the amount of cr is matched, or left moves beyond right (no matches found)
            while (charMap.getOrDefault(cr, 0)==-1 && left<=right) {
                char cl = s.charAt(left++);
                count = charMap.getOrDefault(cl, 0) + 1;
                this.updateMap(charMap, cl, count);
            }
            
            right++;
        }
        
        return result;
    }
}

K-th Smallest in Lexicographical Order
【题目】Given integers n and k, find the lexicographically k-th smallest integer in the range from 1 to n.

Note: 1 ≤ k ≤ n ≤ 109.

Example:

Input:
n: 13   k: 2

Output:
10

Explanation:
The lexicographical order is [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9], so the second smallest number is 10.

【解答】要找从 1 到 n 这连续的数中,第 k 小的那一个。

老实说,这一类题我是不太擅长做的。尝试了几种思路都没有发现特别好的解法。在讨论区我找到了我认为 最清晰的解法 ,而且这种解法对于类似的这种问题有一定启发意义。大致思路是使用引入十叉数,因为每一个数字的后面,最多可能有从 0 到 9 这十个可能。于是从根往叶子方向一层一层遍历,直到找到第 k 个数为止。

class Solution {
    public int findKthNumber(int n, int k) {
        int curr = 1;
        k = k - 1;
        while (k > 0) {
            int steps = calSteps(n, curr, curr + 1);
            if (steps <= k) {
                curr += 1;
                k -= steps;
            } else {
                curr *= 10;
                k -= 1;
            }
        }
        return curr;
    }
    
    /**
     * how many steps from n1 to n2
     */
    public int calSteps(int n, long n1, long n2) {
        int steps = 0;
        while (n1 <= n) {
            steps += Math.min(n + 1, n2) - n1;
            n1 *= 10;
            n2 *= 10;
        }
        return steps;
    }
}

说明,从根节点开始往下一层一次统计,curr 指向当前节点,如果 curr 和 curr+1 之间的 steps 小于 k,那 curr 就在当前层前进;否则,curr 下潜到下一层去,这种情况下 k 需要-1 因为原节点已经遍历过了。

关于 steps 的计算(calSteps):考虑边界情况,n2 和 n+1 二者中小的那个区减掉 n1,这里的+1 主要是考虑要把 n 这个数也算进去。

Arranging Coins
【题目】You have a total of n coins that you want to form in a staircase shape, where every k-th row must have exactly k coins.

Given n, find the total number of full staircase rows that can be formed.

n is a non-negative integer and fits within the range of a 32-bit signed integer.

Example 1:

n = 5

The coins can form the following rows:
¤
¤ ¤
¤ ¤

Because the 3rd row is incomplete, we return 2.

Example 2:

n = 8

The coins can form the following rows:
¤
¤ ¤
¤ ¤ ¤
¤ ¤

Because the 4th row is incomplete, we return 3.

【解答】要求硬币组成的楼梯形状的层数,不完整的层要舍弃。

我觉得是一个数学问题。第一层是有 1 枚硬币,第 x 层最多有 x 枚,等差数列,那么最多一共有 (1+x)*x/2 枚,这个数必须要大于等于 n,这是个一元二次方程,拿求根公式解一下就好。

class Solution {
    public int arrangeCoins(int n) {
        // (1 + x) * x / 2 >= n
        // so x^2 + x - 2n >= 0
        // [-b±(b^2-4ac)^(1/2)]/(2a)
        // avoid overflow
        double result = (-1 + Math.sqrt(1 - 4*(-2*(double)n))) / 2;
        return (int) result;
    }
}

Find All Duplicates in an Array
【题目】Given an array of integers, 1 ≤ a[i] ≤ n (n = size of array), some elements appear twice and others appear once.

Find all the elements that appear twice in this array.

Could you do it without extra space and in O(n) runtime?

Example:

Input:
[4,3,2,7,8,2,3,1]

Output:
[2,3]

【解答】要从大小为 1~n 的长度为 n 数组中找出出现了两次的数,而且要 O(1) 的空间和 O(n) 的时间复杂度,这就意味着一般的搜索和排序之类方法的都可以靠边站了 。
利用这些数范围的特性,在找一个数的时候,把这个数 x 减去 1 作为一个 index,让数组在该 index 上的数取负数,如果发现该数已经是负数了,那就说明这个数 x 就是重复的数。

class Solution {
    public List findDuplicates(int[] nums) {
        if (null==nums)
            throw new IllegalArgumentException();
        
        // to save as much space as possible LinkedList is used, not ArrayList
        List result = new LinkedList<>();
        for (int i=0; i<nums.length; i++) {
            int index = Math.abs(nums[i]) - 1;
            if (nums[index]<0) { // if it's already <0 this is the second time to be hit
                result.add(index + 1);
            } else {
                nums[index] = -nums[index];
            }
        }
        
        return result;
    }
}

String Compression
【题目】Given an array of characters, compress it in-place.

The length after compression must always be smaller than or equal to the original array.

Every element of the array should be a character (not int) of length 1.

After you are done modifying the input array in-place, return the new length of the array.

Follow up:
Could you solve it using only O(1) extra space?

Example 1:

Input:
["a","a","b","b","c","c","c"]

Output:
Return 6, and the first 6 characters of the input array should be: ["a","2","b","2","c","3"]

Explanation:
"aa" is replaced by "a2". "bb" is replaced by "b2". "ccc" is replaced by "c3".

Example 2:

Input:
["a"]

Output:
Return 1, and the first 1 characters of the input array should be: ["a"]

Explanation:
Nothing is replaced.

Example 3:

Input:
["a","b","b","b","b","b","b","b","b","b","b","b","b"]

Output:
Return 4, and the first 4 characters of the input array should be: ["a","b","1","2"].

Explanation:
Since the character "a" does not repeat, it is not compressed. "bbbbbbbbbbbb" is replaced by "b12".
Notice each digit has it's own entry in the array.

Note:

  1. All characters have an ASCII value in [35, 126].
  2. 1 <= len(chars) <= 1000.

【解答】字符串压缩。快慢双指针方式求解。

class Solution {
    public int compress(char[] chars) {
        if (chars==null)
            throw new IllegalArgumentException();
        if (chars.length==0)
            return 0;
        
        // slow / fast: the star/end of the consecutive sub char array
        // recording: the pointer recording char + number
        int slow=0, fast=0, recording=0;
        while (slow<chars.length) {
            if (fast==chars.length || chars[fast]!=chars[slow]) {
                // recording always starts with the char
                chars[recording++] = chars[slow];
                
                if (fast-slow > 1) {
                    String num = "" + (fast-slow);
                    for (char n : num.toCharArray())
                        chars[recording++] = n;
                }
                slow = fast;
            }
            
            fast++;
        }
        
        return recording;
    }
}

Add Two Numbers II
【题目】You are given two non-empty linked lists representing two non-negative integers. The most significant digit comes first and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.

You may assume the two numbers do not contain any leading zero, except the number 0 itself.

Follow up:
What if you cannot modify the input lists? In other words, reversing the lists is not allowed.

Example:

Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 8 -> 0 -> 7

【解答】两个用链表表示的数相加。常规操作,包括 fake/dummy head,进位符号的处理等等。

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        if (l1==null || l2==null)
            throw new IllegalArgumentException();
        
        Stack s1 = new Stack<>();
        Stack s2 = new Stack<>();
        
        ListNode cur = l1;
        while (cur!=null) {
            s1.push(cur);
            cur = cur.next;
        }
        cur = l2;
        while (cur!=null) {
            s2.push(cur);
            cur = cur.next;
        }
        
        Stack res = new Stack<>();
        boolean carry = false;
        while (!s1.isEmpty() || !s2.isEmpty()) {
            ListNode l = s1.isEmpty() ? null : s1.pop();
            ListNode r = s2.isEmpty() ? null : s2.pop();
            int lv = l==null ? 0 : l.val;
            int rv = r==null ? 0 : r.val;
            int sum = lv + rv;
            if (carry)
                sum++;
            
            if (sum>=10) {
                sum -= 10;
                carry = true;
            } else {
                carry = false;
            }
            
            res.push(new ListNode(sum));
        }
        if (carry)
            res.push(new ListNode(1));
        
        ListNode dummyHead = new ListNode(0);
        cur = dummyHead;
        while (!res.isEmpty()) {
            cur.next = res.pop();
            cur = cur.next;
        }
        
        return dummyHead.next;
    }
}

Arithmetic Slices II – Subsequence
【题目】A sequence of numbers is called arithmetic if it consists of at least three elements and if the difference between any two consecutive elements is the same.

For example, these are arithmetic sequences:

1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9

The following sequence is not arithmetic.

1, 1, 2, 5, 7

A zero-indexed array A consisting of N numbers is given. A subsequence slice of that array is any sequence of integers (P0, P1, …, Pk) such that 0 ≤ P0 < P1 < … < Pk < N.

subsequence slice (P0, P1, …, Pk) of array A is called arithmetic if the sequence A[P0], A[P1], …, A[Pk-1], A[Pk] is arithmetic. In particular, this means that k ≥ 2.

The function should return the number of arithmetic subsequence slices in the array A.

The input contains N integers. Every integer is in the range of -231 and 231-1 and 0 ≤ N ≤ 1000. The output is guaranteed to be less than 231-1.

Example:

Input: [2, 4, 6, 8, 10]

Output: 7

Explanation:
All arithmetic subsequence slices are:
[2,4,6]
[4,6,8]
[6,8,10]
[2,4,6,8]
[4,6,8,10]
[2,4,6,8,10]
[2,6,10]

【解答】我最开始的思路是动态规划,划分成子问题。每个子问题是:

对于所有从 index 为 i 开始的 subsequence,在 interval 是 k 的情况下,解集用 c[i][k] 来表示,这个解集合我不需要知道具体内容,但我需要知道每个 subsequence 在包含特定个元素个数 p 的情况下个有多少种可能。那么对于一个新的 index j 且 j<i,考察 A[j] 和 A[i]:
A[j]-A[i] 为新的 interval,寻求解集 c[j][A[j]-A[i]]。看起来这个方法似乎是可以走得通的,但是明显无论从实现层面还是时间复杂度层面都过高了。为了缓存已得到的结果,我需要建立一个这样的数据结构来避开重复计算:Map<Integer, Map<Integer, Map<Integer, Integer>>>,含义是:<start_index, <interval, <sub_seq_len, count>>。因此这是一个三维的动态规划,我们一般只做一维到二维,而三维的话一般有更好的解决办法。[后记:其实第三维 sub_seq_len 应该是不需要的,当时的想法有点问题。]

下面的解答来自 讨论区 。简单思路:

  • 对于任意 0<=j<i<A.length, A[j] 和 A[i] 之间的差是 d,那么:map[i][d] 表示:以 A[i] 结尾的这些 subsequence 且元素两两差值为 d 的情况下,这样的这些 subsequence 有多少个。
  • 这样在每次迭代中,先对于当前的元素 A[i] 拿到当前的值 c1=map[i][d],再看它前面的元素 A[j],拿到它的值 c2=map[j][d],对于 map[i][d] 来说,新的值就是 c1+c2+1,这里面的“+1”是因为这个新的这些 subsequence:A[j],A[i];
  • 在考虑总数的时候,每次都把 c2 加进去,因为 c2 来自于前一个元素 A[j] 的统计,这些 subsequence 们最少也有两个元素,现在各自再加上一个 A[i] 就都成为了合法的结果(至少三个元素)。
class Solution {
    public int numberOfArithmeticSlices(int[] A) {
        if (A==null)
            throw new IllegalArgumentException();
        
        int res = 0;
        Map<Integer, Integer>[] map = new Map[A.length];

        for (int i = 0; i < A.length; i++) {
            map[i] = new HashMap<>(i);

            for (int j = 0; j < i; j++) {
                long diff = (long)A[i] - A[j];
                if (diff <= Integer.MIN_VALUE || diff > Integer.MAX_VALUE) continue;

                int d = (int)diff;
                int c1 = map[i].getOrDefault(d, 0);
                int c2 = map[j].getOrDefault(d, 0);
                res += c2;
                map[i].put(d, c1 + c2 + 1);
            }
        }

        return res;
    }
}

Number of Boomerangs
【题目】Given n points in the plane that are all pairwise distinct, a “boomerang” is a tuple of points (i, j, k) such that the distance between i and j equals the distance between i and k (the order of the tuple matters).

Find the number of boomerangs. You may assume that n will be at most 500 and coordinates of points are all in the range [-10000, 10000] (inclusive).

Example:

Input:
[[0,0],[1,0],[2,0]]

Output:
2

Explanation:
The two boomerangs are [[1,0],[0,0],[2,0]] and [[1,0],[2,0],[0,0]]

【解答】i 到 j 的距离必须等于 i 到 k 的距离。

基本思路很简单,遍历每一个点,并以之为 i,再以它到所有其他点距离的平方为 key,放到 map 里面去,value 为统计该距离出现的次数。

之后对于 map 中的每组 entry,value 的个数就表示了在选定 i 的情况下,有多少组距离相等,如果它是 x 的话,那么 x*(x-1) 就意味着有多少种组合。(我在注释里面列出了排列组合的两个基本公式,也是它的本质由来)

class Solution {
    public int numberOfBoomerangs(int[][] points) {
        if (points==null)
            throw new IllegalArgumentException();
        
        int total = 0;
        for (int i=0; i<points.length; i++) {
            int[] point = points[i];
            // point is chosen as the center (first element of the tri-tuple)
            // key: distance^2, value: count
            Map<Integer, Integer> map = new HashMap<>();
            for (int j=0; j<points.length; j++) {
                int[] p = points[j];
                if (point==p)
                    continue;
                
                int distance = (int) (Math.pow(point[0]-p[0], 2) + Math.pow(point[1]-p[1], 2));
                int count = map.getOrDefault(distance, 0);
                map.put(distance, ++count);
            }
            
            for (int count : map.values())
                total += this.perm(count);
        }
        
        return total;
    }
    
    // Permutation: P(m, n) = n! / (n-m)!
    // Combination: C(m, n) = P(m, n) / m! = n! / ((n-m)!*m!)
    private int perm(int count) {
        // here m=2, so C(m, n) = n * (n-1);
        return count * (count-1);
    }
}

Find All Numbers Disappeared in an Array
【题目】Given an array of integers where 1 ≤ a[i] ≤ n (n = size of array), some elements appear twice and others appear once.

Find all the elements of [1, n] inclusive that do not appear in this array.

Could you do it without extra space and in O(n) runtime? You may assume the returned list does not count as extra space.

Example:

Input:
[4,3,2,7,8,2,3,1]

Output:
[5,6]

【解答】应该算是常规题了。把每个数都尝试换到它应该去的位置,完毕之后,扫一遍看哪些位置上的数和 index 不匹配(index 应当等于 n-1)。

class Solution {
    public List findDisappearedNumbers(int[] nums) {
        if (nums==null)
            throw new IllegalArgumentException();
        
        int i=0;
        while (i<nums.length) {
            int currentVal = nums[i];
            int indexShouldBe = nums[i]-1;
            if (indexShouldBe != i) {
                if (nums[indexShouldBe] != currentVal) {
                    swap(nums, i, indexShouldBe);
                    continue;
                }
            }
            
            i++;
        }
        
        List result = new ArrayList<>();
        for (i=0; i<nums.length; i++) {
            if (nums[i]-1 != i)
                result.add(i+1);
        }
        
        return result;
    }
    
    private void swap(int[] nums, int i, int j) {
        if (i==j)
            return;
        
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

Serialize and Deserialize BST
【题目】Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment.

Design an algorithm to serialize and deserialize a binary search tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary search tree can be serialized to a string and this string can be deserialized to the original tree structure.

The encoded string should be as compact as possible.

Note: Do not use class member/global/static variables to store states. Your serialize and deserialize algorithms should be stateless.

【解答】要把一棵 BST 序列化和反序列化。

应该说有很多做法。我的做法是把根放在序列化后字符串的头部,然后是递归得到的左子树串,再是右子树串。这样一来,反序列化的时候,字符串的第一个字符总是根,后面的字符只要比它小就是左子树,再往后就是右子树。

public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if (root==null)
            return "";
        
        // pre-order traversal
        String result = "" + root.val;
        if (root.left!=null)
            result += "," + serialize(root.left);
        if (root.right!=null)
            result += "," + serialize(root.right);
        
        return result;
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if (data.length()==0)
            return null;
        
        String[] arr = data.split(",");
        int[] tokens = new int[arr.length];
        for (int i=0; i<arr.length; i++)
            tokens[i] = Integer.parseInt(arr[i]);
        return deserialize(tokens, 0, tokens.length-1);
    }
    
    private TreeNode deserialize(int[] tokens, int start, int end) {
        TreeNode root = new TreeNode(tokens[start]);
        Integer left = null;
        Integer right = null;
        for (int i=start+1; i<=end; i++) {
            if (tokens[i]root.val && right==null) {
                right = i;
                break;
            }
        }
        
        if (left!=null)
            root.left = right==null ? deserialize(tokens, left, end) : deserialize(tokens, left, right-1);
        if (right!=null)
            root.right = deserialize(tokens, right, end);
        
        return root;
    }
}

Delete Node in a BST
【题目】Given a root node reference of a BST and a key, delete the node with the given key in the BST. Return the root node reference (possibly updated) of the BST.

Basically, the deletion can be divided into two stages:

  1. Search for a node to remove.
  2. If the node is found, delete the node.

Note: Time complexity should be O(height of tree).

Example:

root = [5,3,6,2,4,null,7]
key = 3

    5
   / \
  3   6
 / \   \
2   4   7

Given key to delete is 3. So we find the node with value 3 and delete it.

One valid answer is [5,4,6,2,null,null,7], shown in the following BST.

    5
   / \
  4   6
 /     \
2       7

Another valid answer is [5,2,6,null,4,null,7].

    5
   / \
  2   6
   \   \
    4   7

【解答】要从一棵 BST 上面删除一个节点。分成几种情况考虑:

如果要删除的节点在左子树,递归调用左子树;

右子树同理;

最麻烦的是删除根节点,删掉了之后,如果原本有左子树,那就从左子树取最大的节点放到根上;反之,从右子树取最小的节点放到根上(不需要实际的节点替换,替换节点的数值即可)。

class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root==null) return null;
        if (key<root.val) {
            root.left = this.deleteNode(root.left, key);
            return root;
        }
        if (key>root.val) {
            root.right = this.deleteNode(root.right, key);
            return root;
        }
        if (root.left==null && root.right==null) return null;
        if (root.left==null) return root.right;
        if (root.right==null) return root.left;
        
        TreeNode smallest = this.getSmallest(root.right);
        root.val = smallest.val;
        root.right = deleteNode(root.right, smallest.val);
        return root;
    }
    
    private TreeNode getSmallest(TreeNode node){
        while(node.left != null){
            node = node.left;
        }
        return node;
    }
}

Sort Characters By Frequency
【题目】Given a string, sort it in decreasing order based on the frequency of characters.

Example 1:

Input:
"tree"

Output:
"eert"

Explanation:
'e' appears twice while 'r' and 't' both appear once.
So 'e' must appear before both 'r' and 't'. Therefore "eetr" is also a valid answer.

Example 2:

Input:
"cccaaa"

Output:
"cccaaa"

Explanation:
Both 'c' and 'a' appear three times, so "aaaccc" is also a valid answer.
Note that "cacaca" is incorrect, as the same characters must be together.

Example 3:

Input:
"Aabb"

Output:
"bbAa"

Explanation:
"bbaA" is also a valid answer, but "Aabb" is incorrect.
Note that 'A' and 'a' are treated as two different characters.

【解答】按照字符的出现频率排序。常规题目,使用一个 map 来统计次数,然后实现比较接口来排序。

class Solution {
    public String frequencySort(String s) {
        if (s==null) throw new IllegalArgumentException();
        
        Map<Character, Item> map = new HashMap<>();
        List list = new ArrayList<>();
        for (int i=0; i<s.length(); i++) {
            char ch = s.charAt(i);
            if (!map.containsKey(ch)) {
                Item item = new Item(ch, 1);
                map.put(ch, item);
                list.add(item);
            } else {
                Item item = map.get(ch);
                item.count = item.count + 1;
            }
        }
        
        Collections.sort(list);
        StringBuilder sb = new StringBuilder();
        for (Item item : list) {
            for (int i=1; i<=item.count; i++) {
                sb.append(item.ch);
            }
        }
        return sb.toString();
    }
}

class Item implements Comparable {
    public char ch;
    public int count;
    
    public Item(char ch, int count) {
        this.ch = ch;
        this.count = count;
    }
    
    @Override
    public int compareTo(Item that) {
        return that.count - this.count;
    }
}

Minimum Number of Arrows to Burst Balloons
【题目】There are a number of spherical balloons spread in two-dimensional space. For each balloon, provided input is the start and end coordinates of the horizontal diameter. Since it’s horizontal, y-coordinates don’t matter and hence the x-coordinates of start and end of the diameter suffice. Start is always smaller than end. There will be at most 104 balloons.

An arrow can be shot up exactly vertically from different points along the x-axis. A balloon with xstart and xend bursts by an arrow shot at x if xstart ≤ x ≤ xend. There is no limit to the number of arrows that can be shot. An arrow once shot keeps travelling up infinitely. The problem is to find the minimum number of arrows that must be shot to burst all balloons.

Example:

Input:
[[10,16], [2,8], [1,6], [7,12]]

Output:
2

Explanation:
One way is to shoot one arrow for example at x = 6 (bursting the balloons [2,8] and [1,6]) and another arrow at x = 11 (bursting the other two balloons).

【解答】要寻找看起来还是某种扫描线问题。在从左往右扫描的过程中,既然要尽量使得射出的箭少,那就要尽量到不得不射箭的时候再出手,即到最先出现的未打爆气球右边沿再射出箭,而这一箭要打爆其中所有碰到的气球。反复如此操作。(既然要考察右边沿,因此排序的时候必须按照 end,而不是 start 来排序。)

class Solution {
    public int findMinArrowShots(int[][] points) {
        if (points==null)
            throw new IllegalArgumentException();
        
        List<Point> el = new ArrayList<>(points.length);
        for (int[] point : points) {
            Point p = new Point(point[0], point[1]);
            el.add(p);
        }
        
        Collections.sort(el, new Comparator<Point>() {
            @Override
            public int compare(Point left, Point right) {
                return left.end - right.end;
            }
        });
        
        int index = 0;
        int count = 0;
        Integer rangeStart = null;
        while (index<el.size()) {
            Point point = el.get(index);
            
            if (rangeStart==null || point.start>rangeStart) {
                rangeStart = point.end; // keep the range start as far as possible
                count++;
            }
            
            index++;
        }
        
        return count;
    }
}

class Point {
    public int start;
    public int end;
    public Point(int start, int end) {
        this.start = start;
        this.end = end;
    }
}

Minimum Moves to Equal Array Elements
【题目】Given a non-empty integer array of size n, find the minimum number of moves required to make all array elements equal, where a move is incrementing n – 1 elements by 1.

Example:

Input:
[1,2,3]

Output:
3

Explanation:
Only three moves are needed (remember each move increments two elements):

[1,2,3]  =>  [2,3,3]  =>  [3,4,3]  =>  [4,4,4]

【解答】每次只能移动 n-1 个元素且移动的方法是+1,要尽量让所有元素靠拢,那本质上其实意味着每次只能移动一个元素且移动方法是-1。这样,只需要找到其中的最小数,然后累加所有其他数和最小值的差值即可。顺便提一句,如果题目修改一下,移动的方法可以是+1 也可以是-1,那就不是找最小数,而是找中位数了。

class Solution {
    public int minMoves(int[] nums) {
        Integer min = null;
        for (int num : nums) {
            if (min==null || num<min)
                min = num;
        }
        
        int total = 0;
        for (int num : nums)
            total += (num-min);
        
        return total;
    }
}

4Sum II
【题目】Given four lists A, B, C, D of integer values, compute how many tuples (i, j, k, l) there are such that A[i] + B[j] + C[k] + D[l] is zero.

To make problem a bit easier, all A, B, C, D have same length of N where 0 ≤ N ≤ 500. All integers are in the range of -228 to 228 – 1 and the result is guaranteed to be at most 231 – 1.

Example:

Input:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]

Output:
2

Explanation:
The two tuples are:
1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

【解答】四个数组,各取一个数,要求加起来为 0。似乎没有特别好的办法,死算肯定嵌套层数太多,时间复杂度可以到 n 的四次方。于是,折中的方法是,A 和 B 嵌套,找出两两配对所有的和的出现次数;再拿 C 和 D 嵌套,找出两两配对所有和的出现次数。再把这两个结果合并考察,找到加起来结果为 0 的组合。整体时间复杂度为 n 的平方,空间复杂度也为 n 的平方。

class Solution {
    public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
        if (A==null || B==null || C==null || D==null)
            throw new IllegalArgumentException();
        
        Map<Integer, Integer> sumMap = new HashMap<>();
        for (int a : A) {
            for (int b : B) {
                int count = sumMap.getOrDefault(a+b, 0);
                count++;
                sumMap.put(a+b, count);
            }
        }
        
        int total = 0;
        for (int c : C) {
            for (int d : D) {
                total += sumMap.getOrDefault(-(c+d), 0);
            }
        }
        
        return total;
    }
}

Assign Cookies
【题目】Assume you are an awesome parent and want to give your children some cookies. But, you should give each child at most one cookie. Each child i has a greed factor gi, which is the minimum size of a cookie that the child will be content with; and each cookie j has a size sj. If sj >= gi, we can assign the cookie j to the child i, and the child i will be content. Your goal is to maximize the number of your content children and output the maximum number.

Note:
You may assume the greed factor is always positive.
You cannot assign more than one cookie to one child.

Example 1:

Input: [1,2,3], [1,1]

Output: 1

Explanation: You have 3 children and 2 cookies. The greed factors of 3 children are 1, 2, 3. 
And even though you have 2 cookies, since their size is both 1, you could only make the child whose greed factor is 1 content.
You need to output 1.

Example 2:

Input: [1,2], [1,2,3]

Output: 2

Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2. 
You have 3 cookies and their sizes are big enough to gratify all of the children, 
You need to output 2.

【解答】分饼干问题。

本质上还是一个贪心问题,把饼干根据大小排序,孩子根据贪心程度排序。两个指针分别从这两个数组的左侧开始往右扫描。只要能满足孩子,两个指针都分别往后移动,否则只移动指向饼干的指针。

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        if (g==null || s==null)
            throw new IllegalArgumentException();
        if (g.length==0 || s.length==0)
            return 0;
        
        Arrays.sort(g);
        Arrays.sort(s);
        
        int gi = 0, si = 0;
        int count = 0;
        while (gi<g.length && si<s.length) {
            if (g[gi]<=s[si]) {
                count++;
                gi++;
                si++;
            } else {
                si++;
            }
        }
        
        return count;
    }
}

132 Pattern
【题目】Given a sequence of n integers a1, a2, …, an, a 132 pattern is a subsequence ai, aj, ak such that i < j < k and ai < ak < aj. Design an algorithm that takes a list of n numbers as input and checks whether there is a 132 pattern in the list.

Note: n will be less than 15,000.

Example 1:

Input: [1, 2, 3, 4]

Output: False

Explanation: There is no 132 pattern in the sequence.

Example 2:

Input: [3, 1, 4, 2]

Output: True

Explanation: There is a 132 pattern in the sequence: [1, 4, 2].

Example 3:

Input: [-1, 3, 2, 0]

Output: True

Explanation: There are three 132 patterns in the sequence: [-1, 3, 2], [-1, 3, 0] and [-1, 2, 0].

【解答】要寻找一串数中是否存在“小-大-中”这样的模式。

这道题看起来似乎不难,却让我思考了很长时间。最开始的想法是,看起来是一道引入双指针,创建几个 O(1) 的变量,从左到右简单扫描一遍,就可以得到结果的题目。后来越想越发现不是这样的,并且如果只使用 O(1) 的变量,似乎是无法解答的。因为对于这道题中 ai、aj、ak 三个变量的寻找,在拿到候选的 aj 的时候,ai 和 ak 使用不同的组合,aj 有可行和不可行这样不同的可能,因此为了遍历 ai 和 ak 不同的组合而形成的可能性,需要使用至少 O(n) 的空间来创建一个类似数组的结构。

这个空间就是使用 stack 来实现,而这个 stack 存放的是 ak 的候选。从右往左扫描,考虑到 i、j、k 三个变数,ai 在 aj 的左侧,aj 在 ak 的左侧,因此扫描遍历到的数:

  • 首先考虑能不能成为 ai,如果能,那就返回结果,因为 ai<ak,而 ak<aj 是已经存在了的;
  • 如果不能,那把它列为 aj 的候选,要求 aj 必须>ak;
  • 如果再不能,那就把它列为 ak 的候选,此处没有要求。
class Solution {
    public boolean find132pattern(int[] nums) {
        if (nums==null) throw new IllegalArgumentException();
        if (nums.length<3) return false;
        
        Integer num2 = null;
        Stack stack = new Stack<>();
        for (int i=nums.length-1; i>=0; i--) {
            int num1 = nums[i];
            // check the case if num1 can be ai -
            // if num1 is smaller than num2 we find the result
            if (num2!=null && num1<num2)
                return true;
            
            // so num1 can't be ai, let's check if num1 can be aj -
            // if so the numbers in the stack must be smaller than num1 because ak < aj
            // if there are multiple we only keep the last one because:
            // to match ai<ak<aj, since ak is smaller than aj, we want ak to be as largest as possible
            while (!stack.isEmpty() && stack.peek()<num1)
                num2 = stack.pop();
            
            // so num1 can't be aj either, let num1 be an ak candidate -
            stack.push(num1);
        }
        
        return false;
    }
}

Circular Array Loop
【题目】

You are given a circular array nums of positive and negative integers. If a number k at an index is positive, then move forward k steps. Conversely, if it’s negative (-k), move backward k steps. Since the array is circular, you may assume that the last element’s next element is the first element, and the first element’s previous element is the last element.

Determine if there is a loop (or a cycle) in nums. A cycle must start and end at the same index and the cycle’s length > 1. Furthermore, movements in a cycle must all follow a single direction. In other words, a cycle must not consist of both forward and backward movements.

 

Example 1:

Input: [2,-1,1,2,2]
Output: true
Explanation: There is a cycle, from index 0 -> 2 -> 3 -> 0. The cycle's length is 3.

Example 2:

Input: [-1,2]
Output: false
Explanation: The movement from index 1 -> 1 -> 1 ... is not a cycle, because the cycle's length is 1. By definition the cycle's length must be greater than 1.

Example 3:

Input: [-2,1,-1,-2,-2]
Output: false
Explanation: The movement from index 1 -> 2 -> 1 -> ... is not a cycle, because movement from index 1 -> 2 is a forward movement, but movement from index 2 -> 1 is a backward movement. All movements in a cycle must follow a single direction.

Note:

  1. -1000 ≤ nums[i] ≤ 1000
  2. nums[i] ≠ 0
  3. 1 ≤ nums.length ≤ 5000

Follow up:

Could you solve it in O(n) time complexity and O(1) extra space complexity?

【解答】我解是解出来了,但是属于基础解法,并没有做到上面 Follow Up 中要求的复杂度。思路是,每一个元素都尝试作为循环的开头,使用 set 来标记运行轨迹,同时也建立一个 boolean 型的变量来标记当前尝试是正向还是逆向。一旦发现遇到遍历过的元素,表示这次尝试失败;如果遇到起始元素,返回 true。

class Solution {
    public boolean circularArrayLoop(int[] nums) {
        if (nums==null) throw new IllegalArgumentException();
        if (nums.length==0 || nums.length==1) return false;
        
        for (int i=0; i<nums.length; i++) {
            Set<Integer> forwardIndices = new HashSet<>();
            Set<Integer> backwardIndices = new HashSet<>();
            Integer originalItem = i;
            boolean forward = nums[originalItem] > 0;
            if (nums[originalItem] == 0)
                continue;
            if (forward && forwardIndices.contains(originalItem))
                continue;
            if (!forward && backwardIndices.contains(originalItem))
                continue;
            
            if (this.getNextIndex(nums, i)==i) {
                forwardIndices.add(originalItem);
                backwardIndices.add(originalItem);
                continue;
            }
            
            Integer item = originalItem;
            if (forward)
                forwardIndices.add(originalItem);
            else
                backwardIndices.add(originalItem);
            
            while (true) {
                item = this.getNextIndex(nums, item);
                if (item.equals(originalItem))
                    return true;
                
                if (forward) {
                    if (nums[item]<=0 || forwardIndices.contains(item))
                        break;
                    forwardIndices.add(item);
                } else {
                    if (nums[item]>=0 || backwardIndices.contains(item))
                        break;
                    backwardIndices.add(item);
                }
            }
        }
        
        return false;
    }
    
    private int getNextIndex(int[] nums, int index) {
        index = nums[index] + index;
        while (index >= nums.length) {
            index -= nums.length;
        }
        while (index < 0) {
            index += nums.length;
        }
        return index;
    }
}

Poor Pigs
【题目】There are 1000 buckets, one and only one of them is poisonous, while the rest are filled with water. They all look identical. If a pig drinks the poison it will die within 15 minutes. What is the minimum amount of pigs you need to figure out which bucket is poisonous within one hour?

Answer this question, and write an algorithm for the general case.

General case:

If there are n buckets and a pig drinking poison will die within m minutes, how many pigs (x) you need to figure out the poisonous bucket within p minutes? There is exactly one bucket with poison.

Note:

  1. A pig can be allowed to drink simultaneously on as many buckets as one would like, and the feeding takes no time.
  2. After a pig has instantly finished drinking buckets, there has to be a cool down time of minutes. During this time, only observation is allowed and no feedings at all.
  3. Any given bucket can be sampled an infinite number of times (by an unlimited number of pigs).

【解答】1000 个桶,只有一个桶里面有毒。如果猪喝了毒药,15 分钟内死掉。问要在 1 小时内找到哪个桶,最少需要多少头猪。

这道题貌似是一道数学题,也许在很久以前在一本数学有关的书上见过。一个小时,如果把喝的水分成 5 部分,每部分都混合起来让猪喝,可以喝+观察 4 次,但是可以得出 5 部分的结果,因为 4 次如果死了分别可以代表该次喝的水有毒,如果没死则按照排除法,得知剩下那一部分有毒。一头猪的存活情况,代表了 5 种可能,这就像是 5 进制,那么两头猪就可以表示 5^5=25 种,所以题中例子要求的是 5^x >= 1000,求 x 的最小值。用这样的算法推广可以得到:

class Solution {
    public int poorPigs(int buckets, int minutesToDie, int minutesToTest) {
        long rounds = minutesToTest / minutesToDie + 1;
        // rounds ^ pigs >= buckets
        long pigs = 0;
        while (Math.pow(rounds, pigs)<buckets) {
            pigs++;
        }
        
        return (int) pigs;
    }
}

Repeated Substring Pattern
【题目】Given a non-empty string check if it can be constructed by taking a substring of it and appending multiple copies of the substring together. You may assume the given string consists of lowercase English letters only and its length will not exceed 10000.

Example 1:

Input: "abab"
Output: True
Explanation: It's the substring "ab" twice.

Example 2:

Input: "aba"
Output: False

Example 3:

Input: "abcabcabcabc"
Output: True
Explanation: It's the substring "abc" four times. (And the substring "abcabc" twice.)

【解答】要判断一个字符串是否有多个子字符串拼接而成。使用最直白的做法,考虑每一个字符为重复串开始的位置进行递归,如果字符串全部遍历完毕,而又没有多余的字符,就返回 true。

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        if (s==null)
            throw new IllegalArgumentException();
        
        if (s.length()<2)
            return false;
        
        for (int len=1; len<=s.length()-len; len++) {
            if (s.length() % len == 0) {
                int start = len;
                boolean matched = true;
                while (start<s.length()) {
                    if (!this.check(s, len, start)) {
                        matched = false;
                        break;
                    }
                    start += len;
                }
                if (matched)
                    return true;
            }
        }
        
        return false;
    }
    
    private boolean check(String s, int len, int start) {
        int i=0;
        while (i<len) {
            if (s.charAt(i) != s.charAt(start + i))
                return false;
            i++;
        }
        return true;
    }
}

LFU Cache
【题目】Design and implement a data structure for Least Frequently Used (LFU) cache. It should support the following operations: get and put.

get(key) – Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(key, value) – Set or insert the value if the key is not already present. When the cache reaches its capacity, it should invalidate the least frequently used item before inserting a new item. For the purpose of this problem, when there is a tie (i.e., two or more keys that have the same frequency), the least recently used key would be evicted.

Follow up:
Could you do both operations in O(1) time complexity?

Example:

LFUCache cache = new LFUCache( 2 /* capacity */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // returns 1
cache.put(3, 3);    // evicts key 2
cache.get(2);       // returns -1 (not found)
cache.get(3);       // returns 3.
cache.put(4, 4);    // evicts key 1.
cache.get(1);       // returns -1 (not found)
cache.get(3);       // returns 3
cache.get(4);       // returns 4

【解答】LFU 的缓存设计。

和 LRU 类似,思路是使用一个普通 map 来处理 key-value 映射的问题,但是为了能够根据使用频率来淘汰元素,就引入了有序的链表,这样在访问的时候位置可能会发生邻近的移动。思路基本上没有什么特殊的,但是实现起来比较啰嗦。很遗憾下面这个解停在巨长的一个 test case,还不是超时,而是结果错误,这个 case 的步骤是如此之长,1000+的操作步骤,大概前 2/3 这段代码的执行是没问题的,之后不一致,我很难去分析原因了。

class LFUCache2 {
    private Map<Integer, Item> map = new HashMap<>();
    private Item dummyHead = new Item(0, 0, -1); // the list is sorted ascendingly
    private Item dummyTail = new Item(0, 0, Integer.MAX_VALUE);
    private int size = 0;
    private int capacity;
    
    public LFUCache2(int capacity) {
        if (capacity<0) throw new IllegalArgumentException();
        dummyHead.next = dummyTail;
        dummyTail.prev = dummyHead;
        this.capacity = capacity;
    }
    
    public int get(int key) {
        if (!map.containsKey(key))
            return -1;
        
        Item item = map.get(key);
        int returnValue = item.value;
        item.frequence = item.frequence + 1;
        while (item.next != null && item.next.frequence<=item.frequence) {
            this.swapWithNext(item);
            item = item.next;
        }
        
        return returnValue;
    }
    
    public void put(int key, int value) {
        if (capacity==0)
            return;
        
        if (map.containsKey(key)) {
            Item item = map.get(key);
            item.value = value;
            item.frequence = item.frequence + 1;
            
            this.moveForwardForSameFrequence(item);

            return;
        }
        
        if (size == this.capacity) {
            Item item = dummyHead.next;
            map.remove(item.key);
            
            item.key = key;
            item.value = value;
            item.frequence = 1;
            
            map.put(key, item);
            
            this.moveForwardForSameFrequence(item);
        } else {
            Item current = dummyHead.next;
            Item item = new Item(key, value, 1);
            
            dummyHead.next = item;
            current.prev = item;
            
            item.prev = dummyHead;
            item.next = current;
            
            this.moveForwardForSameFrequence(item);
            
            map.put(key, item);
            size++;
        }
    }
    
    private void moveForwardForSameFrequence(Item item) {
        while (item.next.frequence<=item.frequence) {
            this.swapWithNext(item);
        }
    }
    
    private void swapWithNext(Item l) {
        Item r = l.next;
        Item prev = l.prev;
        Item next = r.next;
        
        prev.next = r;
        next.prev = l;
        
        l.next = next;
        l.prev = r;
        
        r.next = l;
        r.prev = prev;
    }
}

class Item {
    public Integer key;
    public Integer value;
    public Integer frequence;
    public Item next;
    public Item prev;
    public Item(Integer key, Integer value, Integer frequence) {
        this.key = key;
        this.value = value;
        this.frequence = frequence;
        this.next = null;
        this.prev = null;
    }
}

我也看了 讨论区高票 的解法,对 LinkedHashSet 使用很合适,把根据访问频率挪位置等等操作都简化了。主要思路:

三个 HashMap:

  • 第一个是最常规的,存放 LFUCache 的 key 和 value;
  • 第二个存放 key 和频率(出现次数);
  • 那么余下的问题是怎么实现淘汰机制,即在有 map 以外的 key 到来的时候,找到最近最不使用的 key 来,于是第三个使用的是一个值为 LinkedHashSet 的 map,这个 map 的 key 是前面提到的出现次数,而把实际的外部看起来的 LFUCache 的 key 放到了这个 LinkedHashSet 里面。

在淘汰机制触发的时候,就看到 LinkedHashSet 优于普通 HashSet 的好处了——数据是存放在它内部的一个 LinkedList 里面的,因而是严格有序的。这个 list 上面所有的数,对应的 count 都是一致的,添加的时候总是在尾部添加,而淘汰的时候总是从头部淘汰,这就像是一个队列。那为什么不直接使用 LinkedList,而要使用 LinkedHashSet 呢?这就要用到 Set 的机制,当添加的元素已经在 Set 内存在了,就要把它挪到队尾,而不是添加重复元素。

除了以上三个 map,还需要一个 int min,用来记录最小的 count,因为淘汰首先要找到当前最小的 count,其次才在属于同一个 count 的 LinkedHashSet 里面找出需要淘汰的那个 key。

class LFUCache {
    HashMap<Integer, Integer> vals;
    HashMap<Integer, Integer> counts;
    HashMap<Integer, LinkedHashSet<Integer>> lists;
    int cap;
    int min = -1;
    public LFUCache(int capacity) {
        cap = capacity;
        vals = new HashMap<>();
        counts = new HashMap<>();
        lists = new HashMap<>();
        lists.put(1, new LinkedHashSet<>());
    }
    
    public int get(int key) {
        if(!vals.containsKey(key))
            return -1;
        int count = counts.get(key);
        counts.put(key, count+1);
        lists.get(count).remove(key);
        if(count==min && lists.get(count).size()==0)
            min++;
        if(!lists.containsKey(count+1))
            lists.put(count+1, new LinkedHashSet<>());
        lists.get(count+1).add(key);
        return vals.get(key);
    }
    
    public void put(int key, int value) {
        if(cap<=0)
            return;
        if(vals.containsKey(key)) {
            vals.put(key, value);
            get(key);
            return;
        } 
        if(vals.size() >= cap) {
            int evit = lists.get(min).iterator().next();
            lists.get(min).remove(evit);
            vals.remove(evit);
        }
        vals.put(key, value);
        counts.put(key, 1);
        min = 1;
        lists.get(1).add(key);
    }
}

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2VWLVZj

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 HTTP API 认证授权术

我们知道,HTTP是无状态的,所以,当我们需要获得用户是否在登录的状态时,我们需要检查用户的登录状态,一般来说,用户的登录成功后,服务器会发一个登录凭证(又被叫作Token),就像你去访问某个公司,在前台被认证过合法后,这个公司的前台会给你的一个访客卡一样,之后,你在这个公司内去到哪都用这个访客卡来开门,而不再校验你是哪一个人。在计算机的世界里,这个登录凭证的相关数据会放在两种地方,一个地方在用户端,以Cookie的方式(一般不会放在浏览器的Local Storage,因为这很容易出现登录凭证被XSS攻击),另一个地方是放在服务器端,又叫Session的方式(SessonID存于Cookie)。

但是,这个世界还是比较复杂的,除了用户访问,还有用户委托的第三方的应用,还有企业和企业间的调用,这里,我想把业内常用的一些 API认证技术相对系统地总结归纳一下,这样可以让大家更为全面的了解这些技术。注意,这是一篇长文!

本篇文章会覆盖如下技术:

  • HTTP Basic
  • Digest Access
  • App Secret Key + HMAC
  • JWT – JSON Web Tokens
  • OAuth 1.0 – 3 legged & 2 legged
  • OAuth 2.0 – Authentication Code & Client Credential

HTTP Basic

HTTP Basic 是一个非常传统的API认证技术,也是一个比较简单的技术。这个技术也就是使用 usernamepassword 来进行登录。整个过程被定义在了 RFC 2617 中,也被描述在了 Wikipedia: Basic Access Authentication 词条中,同时也可以参看 MDN HTTP Authentication

其技术原理如下:

  1. usernamepassword 做成  username:password 的样子(用冒号分隔)
  2. 进行Base64编码。Base64("username:password") 得到一个字符串(如:把 haoel:coolshell 进行base64 后可以得到 aGFvZW86Y29vbHNoZWxsCg
  3. aGFvZW86Y29vbHNoZWxsCg放到HTTP头中 Authorization 字段中,形成 Authorization: Basic aGFvZW86Y29vbHNoZWxsCg,然后发送到服务端。
  4. 服务端如果没有在头里看到认证字段,则返回401错,以及一个个WWW-Authenticate: Basic Realm='HelloWorld' 之类的头要求客户端进行认证。之后如果没有认证通过,则返回一个401错。如果服务端认证通过,那么会返回200。

我们可以看到,使用Base64的目的无非就是为了把一些特殊的字符给搞掉,这样就可以放在HTTP协议里传输了。而这种方式的问题最大的问题就是把用户名和口令放在网络上传,所以,一般要配合TLS/SSL的安全加密方式来使用。我们可以看到 JIRA Cloud 的API认证支持HTTP Basic 这样的方式。

但我们还是要知道,这种把用户名和密码同时放在公网上传输的方式有点不太好,因为Base64不是加密协议,保是编码协议,所以就算是有HTTPS作为安全保护,给人的感觉还是不放心。

Digest Access

中文称“HTTP 摘要认证”,最初被定义在了 RFC 2069 文档中(后来被 RFC 2617 引入了一系列安全增强的选项;“保护质量”(qop)、随机数计数器由客户端增加、以及客户生成的随机数)。

其基本思路是,请求方把用户名口令和域做一个MD5 –  MD5(username:realm:password) 然后传给服务器,这样就不会在网上传用户名和口令了,但是,因为用户名和口令基本不会变,所以,这个MD5的字符串也是比较固定的,因此,这个认证过程在其中加入了两个事,一个是 nonce 另一个是 qop

  • 首先,调用方发起一个普通的HTTP请求。比如:GET /coolshell/admin/ HTTP/1.1
  • 服务端自然不能认证能过,服务端返回401错误,并且在HTTP头里的 WWW-Authenticate 包含如下信息:
 WWW-Authenticate: Digest realm="testrealm@host.com",
                        qop="auth,auth-int",
                        nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                        opaque="5ccc069c403ebaf9f0171e9517f40e41"
  • 其中的 nonce 为服务器端生成的随机数,然后,客户端做 HASH1=MD5(MD5(username:realm:password):nonce:cnonce) ,其中的 cnonce 为客户端生成的随机数,这样就可以使得整个MD5的结果是不一样的。
  • 如果 qop 中包含了 auth ,那么还得做  HASH2=MD5(method:digestURI) 其中的 method 就是HTTP的请求方法(GET/POST…),digestURI 是请求的URL。
  • 如果 qop 中包含了 auth-init ,那么,得做  HASH2=MD5(method:digestURI:MD5(entityBody)) 其中的 entityBody 就是HTTP请求的整个数据体。
  • 然后,得到 response = MD5(HASH1:nonce:nonceCount:cnonce:qop:HASH2) 如果没有 qopresponse = MD5(HA1:nonce:HA2)
  • 最后,我们的客户端对服务端发起如下请求—— 注意HTTP头的 Authorization: Digest ...
GET /dir/index.html HTTP/1.0
Host: localhost
Authorization: Digest username="Mufasa",
                     realm="testrealm@host.com",
                     nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                     uri="%2Fcoolshell%2Fadmin",
                     qop=auth,
                     nc=00000001,
                     cnonce="0a4f113b",
                     response="6629fae49393a05397450978507c4ef1",
                     opaque="5ccc069c403ebaf9f0171e9517f40e41"

维基百科上的 Wikipedia: Digest access authentication 词条非常详细地描述了这个细节。

摘要认证这个方式会比之前的方式要好一些,因为没有在网上传递用户的密码,而只是把密码的MD5传送过去,相对会比较安全,而且,其并不需要是否TLS/SSL的安全链接。但是,别看这个算法这么复杂,最后你可以发现,整个过程其实关键是用户的password,这个password如果不够得杂,其实是可以被暴力破解的,而且,整个过程是非常容易受到中间人攻击——比如一个中间人告诉客户端需要一个

App Secret Key + HMAC

先说HMAC技术,这个东西来自于MAC – Message Authentication Code,是一种用于给消息签名的技术,也就是说,我们怕消息在传递的过程中被人修改,所以,我们需要用对消息进行一个MAC算法,得到一个摘要字串,然后,接收方得到消息后,进行同样的计算,然后比较这个MAC字符串,如果一致,则表明没有被修改过(整个过程参看下图)。而HMAC – Hash-based Authenticsation Code,指的是利用Hash技术完成这一工作,比如:SHA-256算法。

 

(图片来自 Wikipedia – MAC 词条

我们再来说App ID,这个东西跟验证没有关系,只是用来区分,是谁来调用API的,就像我们每个人的身份证一样,只是用来标注不同的人,不是用来做身份认证的。与前面的不同之处是,这里,我们需要用App ID 来映射一个用于加密的密钥,这样一来,我们就可以在服务器端进行相关的管理,我们可以生成若干个密钥对(AppID, AppSecret),并可以有更细粒度的操作权限管理。

把AppID和HMAC用于API认证,目前来说,玩得最好最专业的应该是AWS了,我们可以通过S3的API请求签名文档看到AWS是怎么玩的。整个过程还是非常复杂的,可以通过下面的图片流程看个大概。基本上来说,分成如下几个步骤:

  1. 把HTTP的请求(方法、URI、查询字串、头、签名头,body)打个包叫 CanonicalRequest,作个SHA-256的签名,然后再做一个base16的编码
  2. 把上面的这个签名和签名算法 AWS4-HMAC-SHA256、时间戳、Scop,再打一个包,叫 StringToSign
  3. 准备签名,用 AWSSecretAccessKey来对日期签一个 DataKey,再用 DataKey 对要操作的Region签一个 DataRegionKey ,再对相关的服务签一个DataRegionServiceKey ,最后得到 SigningKey.
  4. 用第三步的 SigningKey来对第二步的 StringToSign 签名。

 

最后,发出HTTP Request时,在HTTP头的 Authorization字段中放入如下的信息:

Authorization: AWS4-HMAC-SHA256 
               Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, 
               SignedHeaders=content-type;host;x-amz-date, 
               Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7

 

其中的  AKIDEXAMPLE 是 AWS Access Key ID, 也就是所谓的 AppID,服务器端会根据这个AppID来查相关的 Secret Access Key,然后再验证签名。如果,你对这个过程有点没看懂的话,你可以读一读这篇文章——《Amazon S3 Rest API with curl》这篇文章里有好些代码,代码应该是最有细节也是最准确的了。

这种认证的方式好处在于,AppID和AppSecretKey,是由服务器的系统开出的,所以,是可以被管理的,AWS的IAM就是相关的管理,其管理了用户、权限和其对应的AppID和AppSecretKey。但是不好的地方在于,这个东西没有标准 ,所以,各家的实现很不一致。比如: Acquia 的 HMAC微信的签名算法 (这里,我们需要说明一下,微信的API没有遵循HTTP协议的标准,把认证信息放在HTTP 头的 Authorization 里,而是放在body里)

JWT – JSON Web Tokens

JWT是一个比较标准的认证解决方案,这个技术在Java圈里应该用的是非常普遍的。JWT签名也是一种MAC(Message Authentication Code)的方法。JWT的签名流程一般是下面这个样子:

  1. 用户使用用户名和口令到认证服务器上请求认证。
  2. 认证服务器验证用户名和口令后,以服务器端生成JWT Token,这个token的生成过程如下:
    • 认证服务器还会生成一个 Secret Key(密钥)
    • 对JWT Header和 JWT Payload分别求Base64。在Payload可能包括了用户的抽象ID和的过期时间。
    • 用密钥对JWT签名 HMAC-SHA256(SecertKey, Base64UrlEncode(JWT-Header)+'.'+Base64UrlEncode(JWT-Payload));
  3. 然后把 base64(header).base64(payload).signature 作为 JWT token返回客户端。
  4. 客户端使用JWT Token向应用服务器发送相关的请求。这个JWT Token就像一个临时用户权证一样。

当应用服务器收到请求后:

  1. 应用服务会检查 JWT  Token,确认签名是正确的。
  2. 然而,因为只有认证服务器有这个用户的Secret Key(密钥),所以,应用服务器得把JWT Token传给认证服务器。
  3. 认证服务器通过JWT Payload 解出用户的抽象ID,然后通过抽象ID查到登录时生成的Secret Key,然后再来检查一下签名。
  4. 认证服务器检查通过后,应用服务就可以认为这是合法请求了。

我们可以看以,上面的这个过程,是在认证服务器上为用户动态生成 Secret Key的,应用服务在验签的时候,需要到认证服务器上去签,这个过程增加了一些网络调用,所以,JWT除了支持HMAC-SHA256的算法外,还支持RSA的非对称加密的算法。

使用RSA非对称算法,在认证服务器这边放一个私钥,在应用服务器那边放一个公钥,认证服务器使用私钥加密,应用服务器使用公钥解密,这样一来,就不需要应用服务器向认证服务器请求了,但是,RSA是一个很慢的算法,所以,虽然你省了网络调用,但是却费了CPU,尤其是Header和Payload比较长的时候。所以,一种比较好的玩法是,如果我们把header 和 payload简单地做SHA256,这会很快,然后,我们用RSA加密这个SHA256出来的字符串,这样一来,RSA算法就比较快了,而我们也做到了使用RSA签名的目的。

最后,我们只需要使用一个机制在认证服务器和应用服务器之间定期地换一下公钥私钥对就好了。

这里强烈建议全文阅读 Anglar 大学的 《JSW:The Complete Guide to JSON Web Tokens

OAuth 1.0

OAuth也是一个API认证的协议,这个协议最初在2006年由Twitter的工程师在开发OpenID实现的时候和社交书签网站Ma.gnolia时发现,没有一种好的委托授权协议,后来在2007年成立了一个OAuth小组,知道这个消息后,Google员工也加入进来,并完善有善了这个协议,在2007年底发布草案,过一年后,在2008年将OAuth放进了IETF作进一步的标准化工作,最后在2010年4月,正式发布OAuth 1.0,即:RFC 5849 (这个RFC比起TCP的那些来说读起来还是很轻松的),不过,如果你想了解其前身的草案,可以读一下 OAuth Core 1.0 Revision A ,我在下面做个大概的描述。

根据RFC 5849,可以看到 OAuth 的出现,目的是为了,用户为了想使用一个第三方的网络打印服务来打印他在某网站上的照片,但是,用户不想把自己的用户名和口令交给那个第三方的网络打印服务,但又想让那个第三方的网络打印服务来访问自己的照片,为了解决这个授权的问题,OAuth这个协议就出来了。

  • 这个协议有三个角色:
    • User(照片所有者-用户)
    • Consumer(第三方照片打印服务)
    • Service Provider(照片存储服务)
  • 这个协义有三个阶段:
    • Consumer获取Request Token
    • Service Provider 认证用户并授权Consumer
    • Consumer获取Access Token调用API访问用户的照片

整个授权过程是这样的:

  1. Consumer(第三方照片打印服务)需要先上Service Provider获得开发的 Consumer Key 和 Consumer Secret
  2. 当 User 访问 Consumer 时,Consumer 向 Service Provide 发起请求请求Request Token (需要对HTTP请求签名)
  3. Service Provide 验明 Consumer 是注册过的第三方服务商后,返回 Request Token(oauth_token)和 Request Token Secret (oauth_token_secret
  4. Consumer 收到 Request Token 后,使用HTTP GET 请求把 User 切到 Service Provide 的认证页上(其中带上Request Token),让用户输入他的用户和口令。
  5. Service Provider 认证 User 成功后,跳回 Consumer,并返回 Request Token (oauth_token)和 Verification Code(oauth_verifier
  6. 接下来就是签名请求,用Request Token 和 Verification Code 换取 Access Token (oauth_token)和 Access Token Secret (oauth_token_secret)
  7. 最后使用Access Token 访问用户授权访问的资源。

下图附上一个Yahoo!的流程图可以看到整个过程的相关细节。

因为上面这个流程有三方:User,Consumer 和 Service Provide,所以,又叫 3-legged flow,三脚流程。OAuth 1.0 也有不需要用户参与的,只有Consumer 和 Service Provider 的, 也就是 2-legged flow 两脚流程,其中省掉了用户认证的事。整个过程如下所示:

  1. Consumer(第三方照片打印服务)需要先上Service Provider获得开发的 Consumer Key 和 Consumer Secret
  2. Consumer 向 Service Provide 发起请求请求Request Token (需要对HTTP请求签名)
  3. Service Provide 验明 Consumer 是注册过的第三方服务商后,返回 Request Token(oauth_token)和 Request Token Secret (oauth_token_secret
  4. Consumer 收到 Request Token 后,直接换取 Access Token (oauth_token)和 Access Token Secret (oauth_token_secret)
  5. 最后使用Access Token 访问用户授权访问的资源。

最后,再来说一说OAuth中的签名。

  • 我们可以看到,有两个密钥,一个是Consumer注册Service Provider时由Provider颁发的 Consumer Secret,另一个是 Token Secret。
  • 签名密钥就是由这两具密钥拼接而成的,其中用 &作连接符。假设 Consumer Secret 为 j49sk3j29djd 而 Token Secret 为dh893hdasih9那个,签名密钥为:j49sk3j29djd&dh893hdasih9
  • 在请求Request/Access Token的时候需要对整个HTTP请求进行签名(使用HMAC-SHA1和HMAC-RSA1签名算法),请求头中需要包括一些OAuth需要的字段,如:
    • Consumer Key : 也就是所谓的AppID
    • Token: Request Token 或 Access Token
    • Signature Method :签名算法比如:HMAC-SHA1
    • Timestamp:过期时间
    • Nonce:随机字符串
    • Call Back:回调URL

下图是整个签名的示意图:

图片还是比较直观的,我就不多解释了。

OAuth 2.0

在前面,我们可以看到,从Digest Access, 到AppID+HMAC,再到JWT,再到OAuth 1.0,这些个API认证都是要向Client发一个密钥(或是用密码)然后用HASH或是RSA来签HTTP的请求,这其中有个主要的原因是,以前的HTTP是明文传输,所以,在传输过程中很容易被篡改,于是才搞出来一套的安全签名机制,所以,这些个认证的玩法是可以在HTTP明文协议下玩的。

这种使用签名方式大家可以看到是比较复杂的,所以,对于开发者来说,也是很不友好的,在组织签名的那些HTTP报文的时候,各种,URLEncode和Base64,还要对Query的参数进行排序,然后有的方法还要层层签名,非常容易出错,另外,这种认证的安全粒度比较粗,授权也比较单一,对于有终端用户参与的移动端来说也有点不够。所以,在2012年的时候,OAuth 2.0 的 RFC 6749 正式放出。

OAuth 2.0依赖于TLS/SSL的链路加密技术(HTTPS),完全放弃了签名的方式,认证服务器再也不返回什么 token secret 的密钥了,所以,OAuth 2.0是完全不同于1.0 的,也是不兼容的。目前,Facebook 的 Graph API 只支持OAuth 2.0协议,Google 和 Microsoft Azure 也支持Auth 2.0,国内的微信和支付宝也支持使用OAuth 2.0。

下面,我们来重点看一下OAuth 2.0的两个主要的Flow:

  • 一个是Authorization Code Flow, 这个是 3 legged 的
  • 一个是Client Credential Flow,这个是 2 legged 的。
Authorization Code Flow

Authorization Code 是最常使用的OAuth 2.0的授权许可类型,它适用于用户给第三方应用授权访问自己信息的场景。这个Flow也是OAuth 2.0四个Flow中我个人觉得最完整的一个Flow,其流程图如下所示。

 

下面是对这个流程的一个细节上的解释:

1)当用户(Resource Owner)访问第三方应用(Client)的时候,第三方应用会把用户带到认证服务器(Authorization Server)上去,主要请求的是 /authorize API,其中的请求方式如下所示。

https://login.authorization-server.com/authorize?
        client_id=6731de76-14a6-49ae-97bc-6eba6914391e
        &response_type=code
        &redirect_uri=http%3A%2F%2Fexample-client.com%2Fcallback%2F
        &scope=read
        &state=xcoiv98CoolShell3kch

其中:

    • client_id为第三方应用的App ID
    • response_type=code为告诉认证服务器,我要走Authorization Code Flow。
    • redirect_uri意思是我跳转回第三方应用的URL
    • scope意是相关的权限
    • state 是一个随机的字符串,主要用于防CSRF攻击。

2)当Authorization Server收到这个URL请求后,其会通过 client_id来检查 redirect_uriscope是否合法,如果合法,则弹出一个页面,让用户授权(如果用户没有登录,则先让用户登录,登录完成后,出现授权访问页面)。

3)当用户授权同意访问以后,Authorization Server 会跳转回 Client ,并以其中加入一个 Authorization Code。 如下所示:

https://example-client.com/callback?
        code=Yzk5ZDczMzRlNDEwYlrEqdFSBzjqfTG
        &state=xcoiv98CoolShell3kch

我们可以看到,

    • 请流动的链接是第 1)步中的 redirect_uri
    • 其中的 state 的值也和第 1)步的 state一样。

4)接下来,Client 就可以使用 Authorization Code 获得 Access Token。其需要向 Authorization Server 发出如下请求。

POST /oauth/token HTTP/1.1
Host: authorization-server.com
 
code=Yzk5ZDczMzRlNDEwYlrEqdFSBzjqfTG
&grant_type=code
&redirect_uri=https%3A%2F%2Fexample-client.com%2Fcallback%2F
&client_id=6731de76-14a6-49ae-97bc-6eba6914391e
&client_secret=JqQX2PNo9bpM0uEihUPzyrh

5)如果没什么问题,Authorization 会返回如下信息。

{
  "access_token": "iJKV1QiLCJhbGciOiJSUzI1NiI",
  "refresh_token": "1KaPlrEqdFSBzjqfTGAMxZGU",
  "token_type": "bearer",
  "expires": 3600,
  "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciO.eyJhdWQiOiIyZDRkM..."
}

其中,

    • access_token就是访问请求令牌了
    • refresh_token用于刷新 access_token
    • id_token 是JWT的token,其中一般会包含用户的OpenID

6)接下来就是用 Access Token 请求用户的资源了。

GET /v1/user/pictures
Host: https://example.resource.com

Authorization: Bearer iJKV1QiLCJhbGciOiJSUzI1NiI

 

 Client Credential Flow

Client Credential 是一个简化版的API认证,主要是用于认证服务器到服务器的调用,也就是没有用户参与的的认证流程。下面是相关的流程图。

这个过程非常简单,本质上就是Client用自己的 client_idclient_secret向Authorization Server 要一个 Access Token,然后使用Access Token访问相关的资源。

请求示例

POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=czZCaGRSa3F0Mzpn
&client_secret=7Fjfp0ZBr1KtDRbnfVdmIw

返回示例

{
  "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
  "token_type":"bearer",
  "expires_in":3600,
  "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk",
  "scope":"create"
}

这里,容我多扯一句,微信公从平台的开发文档中,使用了OAuth 2.0 的 Client Credentials的方式(参看文档“微信公众号获取access token”),我截了个图如下所谓。我们可以看到,微信公众号使用的是GET方式的请求,把AppID和AppSecret放在了URL中,虽然这也符合OAuth 2.0,但是并不好,因为大多数网关代理会把整个URI请求记到日志中。我们只要脑补一下腾讯的网关的Access Log,里面的日志一定会有很多的各个用户的AppID和AppSecret……

 

小结

讲了这么多,我们来小结一下(下面的小结可能会有点散)

两个概念和三个术语
  • 区分两个概念:Authentication(认证) 和 Authorization (授权),前者是证明请求者是身份,就像身份证一样,后者是为了获得权限。身份是区别于别人的证明,而权限是证明自己的特权。Authentication为了证明操作的这个人就是他本人,需要提供密码、短信验证码,甚至人脸识别。Authorization 则是不需要在所有的请求都需要验人,是在经过Authorization后得到一个Token,这就是Authorization。就像护照和签证一样。
  • 区分三个概念:编码Base64Encode、签名HMAC、加密RSA。编码是为了更的传输,等同于明文,签名是为了信息不能被篡改,加密是为了不让别人看到是什么信息。
明白一些初衷
  • 使用复杂地HMAC哈希签名方式主要是应对当年没有TLS/SSL加密链路的情况。
  • JWT把 uid 放在 Token中目的是为了去掉状态,但不能让用户修改,所以需要签名。
  • OAuth 1.0区分了两个事,一个是第三方的Client,一个是真正的用户,其先拿Request Token,再换Access Token的方法主要是为了把第三方应用和用户区分开来。
  • 用户的Password是用户自己设置的,复杂度不可控,服务端颁发的Serect会很复杂,但主要目的是为了容易管理,可以随时注销掉。
  • OAuth 协议有比所有认证协议有更为灵活完善的配置,如果使用AppID/AppSecret签名的方式,又需要做到可以有不同的权限和可以随时注销,那么你得开发一个像AWS的IAM这样的账号和密钥对管理的系统。
相关的注意事项
  • 无论是哪种方式,我们都应该遵循HTTP的规范,把认证信息放在 Authorization HTTP 头中。
  • 不要使用GET的方式在URL中放入secret之类的东西,因为很多proxy或gateway的软件会把整个URL记在Access Log文件中。
  • 密钥Secret相当于Password,但他是用来加密的,最好不要在网络上传输,如果要传输,最好使用TLS/SSL的安全链路。
  • HMAC中无论是MD5还是SHA1/SHA2,其计算都是非常快的,RSA的非对称加密是比较耗CPU的,尤其是要加密的字符串很长的时候。
  • 最好不要在程序中hard code 你的 Secret,因为在github上有很多黑客的软件在监视各种Secret,千万小心!这类的东西应该放在你的配置系统或是部署系统中,在程序启动时设置在配置文件或是环境变量中。
  • 使用AppID/AppSecret,还是使用OAuth1.0a,还是OAuth2.0,还是使用JWT,我个人建议使用TLS/SSL下的OAuth 2.0。
  • 密钥是需要被管理的,管理就是可以新增可以撤销,可以设置账户和相关的权限。最好密钥是可以被自动更换的。
  • 认证授权服务器(Authorization Server)和应用服务器(App Server)最好分开。

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 May 09, 2019 at 09:37PM
from 酷 壳 – CoolShell http://bit.ly/2JdiUCG

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 打造高效的工作环境 – Shell 篇

注:本文由雷俊(Javaer/Emacser)和我一起编辑,所以文章版权归雷俊与我共同所有,转载者必需注明出处和我们两位作者。原文最早发于酷壳微信公众号,后来我又做了一些修改,再发到博客这边。

程序员是一个很懒的群体,总想着能够让代码为自己干活,他们不断地把工作生活中的一些事情用代码自动化了,从而让整个社会的效率运作地越来越高。所以,程序员在准备去优化这个世界的时候,都会先要优化自己的工作环境,是所谓“工欲善其事,必先利其器”。

我们每个程序员都应该打造一套让自己更为高效的工作环境。那怕就是让你少输入一次命令,少按一次键,少在鼠标和键盘间切换一次,都会让程序员的工作变得更为的高效。所以,程序员一般需要一台性能比较好,不会因为开了太多的网页或程序就卡得不行的电脑,还要配备多个显示器,一个显示器写代码,一个查文档,一个测试运行结果,而不必在各种窗口来来回回的切换……在大量的窗口间切换经常会迷路,而且也容易出错(分不清线上或测试环境)……

除了硬件上的装备,软件上也是能够提升程序员生产力的地方,在软件层面提升程序员生产力的东西有一个很重要的事就是命令行和脚本,使用鼠标和图形界面则会大大降低程序员的生产力。酷壳以前也写过一些,如《你可能不知道的Shell》和《 应该知道的Linux技巧》,但是Unix/Linux Shell就是一个大宝库,怎么写也写不完,不然,怎么会有“Where is the Shell, there is a way”。

命令行

在不同的操作系统下,都有着很不错的命令行工具,比如 Mac 下的 Iterm2,Linux 下的原生命令行,如果你是在 Windows 下工作,问题也不大,因为 Windows 下现在有了 WSL。WSL 提供了一个由微软开发的Linux兼容的内核接口(不包含Linux内核代码),然后可以在其上运行GNU用户空间,例如 Ubuntu,openSUSE,SUSE Linux Enterprise Server,Debian和Kali Linux。这样的用户空间可能包含 Bash shell 和命令语言,使用本机 GNU/Linux 命令行工具(sed,awk 等),编程语言解释器(Ruby,Python 等),甚至是图形应用程序(使用主机端的X窗口系统)。

使用命令行可以完成所有日常的操作,新建文件夹(mkdir)、新建文件(touch)、移动(mv)、复制(cp)、删除(rm)等等。而且使用 Linux/Unix 命令行最好的方式是可以用 awksedgrepxargsfindsort 等等这样的命令,然后用管道把其串起来,就可以完成一个你想要的功能,尤其是一些简单的数据统计功能。这是Linux命令行不可比拟的优势。比如:

  • 查看连接你服务器 top10 用户端的 IP 地址:

netstat -nat | awk '{print $5}' | awk -F ':' '{print $1}' | sort | uniq -c | sort -rn | head -n 10

  • 查看一下你最常用的10个命令:

cat .bash_history | sort | uniq -c | sort -rn | head -n 10 (or cat .zhistory | sort | uniq -c | sort -rn | head -n 10

(注:awk 和 sed 是两大神器,所以,我以前的也有两篇文章来介绍它们——《awk简明教程》和《sed简明教程》,你可以前往一读)

在命令行中使用 alias 可以将使用频率很高命令或者比较复杂的命令合并成一个命令,或者修改原生的命令。

下面这几个命令,可能是你天天都在敲的。所以,你应该设置成 alias 来提高效率

alias nis="npm install --save "
alias svim='sudo vim'
alias mkcd='foo(){ mkdir -p "$1"; cd "$1" }; foo '
alias install='sudo apt get install'
alias update='sudo apt-get update; sudo apt-get upgrade'
alias ..="cd .."
alias ...="cd ..; cd .."
alias www='python -m SimpleHTTPServer 8000'
alias sock5='ssh -D 8080 -q -C -N -f user@your.server'

你还可以参考如下的一些文章,看看别人是怎么用好 alias 的

命令行中除了原生的命令之外,还有很多可以提升使用体验的工具。下面罗列一些很不错的命令,把原生的命令增强地很厉害:

  • fasd 增强了 cd 命令 。
  • bat 增强了 cat 命令 。如果你想要有语法高亮的 cat,可以试试 ccat 命令。
  • exa 增强了 ls 命令,如果你需要在很多目录上浏览各种文件 ,ranger 命令可以比 cd 和 cat 更有效率,甚至可以在你的终端预览图片。
  • fd 是一个比 find 更简单更快的命令,他还会自动地忽略掉一些你配置在 .gitignore 中的文件,以及 .git 下的文件。
  • fzf 会是一个很好用的文件搜索神器,其主要是搜索当前目录以下的文件,还可以使用 fzf --preview 'cat {}'边搜索文件边浏览内容。
  • grep 是一个上古神器,然而,ackag 和 rg 是更好的grep,和上面的 fd一样,在递归目录匹配的时候,会使用你配置在 .gitignore 中的规则。
  • rm 是一个危险的命令,尤其是各种 rm -rf …,所以,trash 是一个更好的删除命令。
  • man 命令是好读文档的命令,但是man的文档有时候太长了,所以,你可以试试 tldr 命令,把文档上的一些示例整出来给你看。
  • 如果你想要一个图示化的ping,你可以试试 prettyping 。
  • 如果你想搜索以前打过的命令,不要再用 Ctrl +R 了,你可以使用加强版的 hstr  。
  • htop  是 top 的一个加强版。然而,还有很多的各式各样的top,比如:用于看IO负载的 iotop,网络负载的 iftop, 以及把这些top都集成在一起的 atop
  • ncdu  比 du 好用多了用。另一个选择是 nnn
  • 如果你想把你的命令行操作建录制成一个 SVG 动图,那么你可以尝试使用 asciinema 和 svg-trem 。
  • httpie 是一个可以用来替代 curlwget 的 http 客户端,httpie 支持 json 和语法高亮,可以使用简单的语法进行 http 访问: http -v github.com
  • tmux 在需要经常登录远程服务器工作的时候会很有用,可以保持远程登录的会话,还可以在一个窗口中查看多个 shell 的状态。
  • Taskbook 是可以完全在命令行中使用的任务管理器 ,支持 ToDo 管理,还可以为每个任务加上优先级。
  • sshrc 是个神器,在你登录远程服务器的时候也能使用本机的 shell 的 rc 文件中的配置。
  • goaccess  这个是一个轻量级的分析统计日志文件的工具,主要是分析各种各样的 access log。

关于这些增加命令,主要是参考自下面的这些文章

  1. 10 Tools To Power Up Your Command Line
  2. 5 More Tools To Power Up Your Command Line (Part 2 Of Series)
  3. Power Up Your Command Line, Part 3
  4. Power Up Your Command Line
  5. Hacker Tools

Shell 和脚本

shell 是可以与计算机进行高效交互的文本接口。shell 提供了一套交互式的编程语言(脚本),shell的种类很多,比如 shbashzsh 等。

shell 的生命力很强,在各种高级编程语言大行其道的今天,很多的任务依然离不开 shell。比如可以使用 shell 来执行一些编译任务,或者做一些批处理任务,初始化数据、打包程序等等。

现在比较流行的是 zsh + oh-my-zsh + zsh-autosuggestions 的组合,你也可以试试看。其中 zsh 和 oh-my-zsh 算是常规操作了,但是 zsh-autosuggestions 特别有用,可以超级快速的帮你补全你输入过的命令,让命令行的操作更加高效。

另外,fish 也是另外一个牛逼的shell,比如:命令行自动完成(根据历史记录),命令行命令高亮,当你要输入命令行参数的时候,自动提示有哪些参数…… fish在很多地方也是用起来很爽的。和上面的 oh-my-zsh 有点不分伯仲了。

你也许会说,用 Python 脚本或 PHP 来写脚本会比 Shell 更好更没有 bug,但我要申辩一下:

  • 其一,如果你有一天要维护线上机器的时候,或是到了银行用户的系统(与外网完全隔离,而且服务器上没有安装 Python/PHP 或是他们的的高级库,那么,你只有 Shell 可以用了)。
  • 其二,而且,如果要跟命令行交互很多的话,Shell 是不二之选,试想一下,如果你要去 100 台远程的机器上查access.log 日志中有没有某个错误,完成这个工作你是用 PHP/Python 写脚本快还是用 Shell 写脚本快呢?

所以,我们还要学会只使用传统的grep/awk/sed等等这些POSIX的原生的系统默认安装的命令

当然,要写好一个脚本并不容易,下面有一些小模板供你参考:

处理命令行参数的一个样例

while [ "$1" != "" ]; do
    case $1 in
        -s  )   shift	
		SERVER=$1 ;;  
        -d  )   shift
		DATE=$1 ;;
	--paramter|p ) shift
		PARAMETER=$1;;
        -h|help  )   usage # function call
                exit ;;
        * )     usage # All other parameters
                exit 1
    esac
    shift
done 

命令行菜单的一个样例

#!/bin/bash
# Bash Menu Script Example

PS3='Please enter your choice: '
options=("Option 1" "Option 2" "Option 3" "Quit")
select opt in "${options[@]}"
do
    case $opt in
        "Option 1")
            echo "you chose choice 1"
            ;;
        "Option 2")
            echo "you chose choice 2"
            ;;
        "Option 3")
            echo "you chose choice $REPLY which is $opt"
            ;;
        "Quit")
            break
            ;;
        *) echo "invalid option $REPLY";;
    esac
done

颜色定义,你可以使用 echo -e "${Blu}blue ${Red}red ${RCol}etc...." 进行有颜色文本的输出

RCol='\e[0m'    # Text Reset

# Regular           Bold                Underline           High Intensity      BoldHigh Intens     Background          High Intensity Backgrounds
Bla='\e[0;30m';     BBla='\e[1;30m';    UBla='\e[4;30m';    IBla='\e[0;90m';    BIBla='\e[1;90m';   On_Bla='\e[40m';    On_IBla='\e[0;100m';
Red='\e[0;31m';     BRed='\e[1;31m';    URed='\e[4;31m';    IRed='\e[0;91m';    BIRed='\e[1;91m';   On_Red='\e[41m';    On_IRed='\e[0;101m';
Gre='\e[0;32m';     BGre='\e[1;32m';    UGre='\e[4;32m';    IGre='\e[0;92m';    BIGre='\e[1;92m';   On_Gre='\e[42m';    On_IGre='\e[0;102m';
Yel='\e[0;33m';     BYel='\e[1;33m';    UYel='\e[4;33m';    IYel='\e[0;93m';    BIYel='\e[1;93m';   On_Yel='\e[43m';    On_IYel='\e[0;103m';
Blu='\e[0;34m';     BBlu='\e[1;34m';    UBlu='\e[4;34m';    IBlu='\e[0;94m';    BIBlu='\e[1;94m';   On_Blu='\e[44m';    On_IBlu='\e[0;104m';
Pur='\e[0;35m';     BPur='\e[1;35m';    UPur='\e[4;35m';    IPur='\e[0;95m';    BIPur='\e[1;95m';   On_Pur='\e[45m';    On_IPur='\e[0;105m';
Cya='\e[0;36m';     BCya='\e[1;36m';    UCya='\e[4;36m';    ICya='\e[0;96m';    BICya='\e[1;96m';   On_Cya='\e[46m';    On_ICya='\e[0;106m';
Whi='\e[0;37m';     BWhi='\e[1;37m';    UWhi='\e[4;37m';    IWhi='\e[0;97m';    BIWhi='\e[1;97m';   On_Whi='\e[47m';    On_IWhi='\e[0;107m';

取当前运行脚本绝对路径的示例:(注:Linux下可以用 dirname $(readlink -f $0) )

FILE="$0"
while [[ -h ${FILE} ]]; do
    FILE="`readlink "${FILE}"`"
done
pushd "`dirname "${FILE}"`" &gt; /dev/null
DIR=`pwd -P`
popd &gt; /dev/null

如何在远程服务器运行一个本地脚本

#无参数
ssh user@server 'bash -s' &lt; local.script.sh

#有参数
ssh user@server ARG1="arg1" ARG2="arg2" 'bash -s' &lt; local_script.sh

如何检查一个命令是否存在,用 which 吗?最好不要用,因为很多操作系统的 which 命令没有设置退出状态码,这样你不知道是否是有那个命令。所以,你应该使用下面的方式。

# POSIX 兼容:
command -v &lt;the_command&gt;


# bash 环境:
hash &lt;the_command&gt; 
type &lt;the_command&gt;

# 示例:
gnudate() {
    if hash gdate 2&gt; /dev/null; then
        gdate "$@"
    else
        date "$@"
    fi
}

然后,如果要写出健壮性更好的脚本,下面是一些相关的技巧:

  • 使用 -e 参数,如:set -e 或是 #!/bin/sh -e,这样设置会让你的脚本出错就会停止运行,这样一来可以防止你的脚本在出错的情况下还在拼拿地干活停不下来。
  • 使用 -u 参数,如: set -eu,这意味着,如果你代码中有变量没有定义,就会退出。
  • 对一些变理,你可以使用默认值。如:${FOO:-'default'}
  • 处理你代码的退出码。这样方便你的脚本跟别的命令行或脚本集成。
  • 尽量不要使用 ; 来执行多个命令,而是使用 &&,这样会在出错的时候停止运行后续的命令。
  • 对于一些字符串变量,使用引号括起,避免其中有空格或是别的什么诡异字符。
  • 如果你的脚有参数,你需要检查脚本运行是否带了你想要的参数,或是,你的脚本可以在没有参数的情况下安全的运行。
  • 为你的脚本设置 -h 和 --help 来显示帮助信息。千万不要把这两个参数用做为的功能。
  • 使用 $() 而不是 “ 来获得命令行的输出,主要原因是易读。
  • 小心不同的平台,尤其是 MacOS 和 Linux 的跨平台。
  • 对于 rm -rf 这样的高危操作,需要检查后面的变量名是否为空,比如:rm -rf $MYDIDR/* 如果 $MYDIR为空,结果是灾难性的。
  • 考虑使用 “find/while” 而不是 “for/find”。如:for F in $(find . -type f) ; do echo $F; done 写成 find . -type f | while read F ; do echo $F ; done 不但可以容忍空格,而且还更快。
  • 防御式编程,在正式执行命令前,把相关的东西都检查好,比如,文件目录有没有存在。

你还可以使用ShellCheck 来帮助你检查你的脚本。

最后推荐一些 Shell 和脚本的参考资料。

各种有意思的命令拼装,一行命令走天涯:

下面是一些脚本集中营,你可以在里面淘到各种牛X的脚本:

甚至写脚本都可以使用框架:

Google的Shell脚本的代码规范:

最后,别忘了几个和shell有关的索引资源:

最后,如果你还有什么别的更好的玩的东西,欢迎在评论区留言,或是到 coolshellx/ariticles @ github 修改本文。

 


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 March 17, 2019 at 01:53PM
from 酷 壳 – CoolShell https://ift.tt/2W6efEU

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 谈谈我的“三观”

也许是人到了四十多了,敢写这么大的命题,我也醉了,不过,我还是想把我的想法记录下来,算是对我思考的一个snapshot,给未来的我看看,要么被未来的我打脸,要么打未来我的脸。无论怎么样,我觉得对我自己都很有意义。注意,这篇文章是长篇大论。

三观是世界观、人生观和价值观,

  • 世界观代表你是怎么看这个世界的。是左还是右,是激进还是保守,是理想还是现实,是乐观还是悲观……
  • 人生观代表你要想成为什么样的人。是成为有钱人,还是成为人生的体验者,是成为老师,还是成为行业专家,是成为有思想的人,还是成为,
  • 价值观则是你觉得什么对你来说更重要。是名是利,是过程还是结果,是付出还是索取,是国家还是自己,是家庭还是职业……

人的三观其实是会变的,回顾一下我的过去,我感觉我的三观至少有这么几比较明显的变化,学生时代、刚走上社会的年轻时代,三十岁后的时代,还有现在。估计都差不多。

  • 学生时代的三观更多的是学校给的,用各种标准答案给的,是又红又专的
  • 刚走上社会后发现完全不是这么一回事,但学生时代的三观根深蒂固,三观开始分裂,内心开始挣扎
  • 三十岁后,不如意的事越来越多,对社会越来越了解,一部分人屈从现实,一部分人不服输继续奋斗,而一部分人已展露才能,分裂的三观开始收敛
  • 四十岁时,经历过的事太多,发现留给自己的时间不多,世界太复杂,而还有好多事没做,从而变得与世无争,也变得更为地自我。

年轻的时候,抵制过日货,虽然没上过街,但是也激动过,一次是1999南斯拉夫大使馆被炸,一次是2005反日示威,以前,我也是一个爱国愤青。但是后来,有过各种机会出国长时间生活工作,加拿大、英国、美国、日本……随着自己的经历和眼界的开阔,自己的三观自己也随着有了很多的变化,发现有些事并不是自己一开始所认识的那样,而且还是截然相反的。我深深感觉到,要有一个好的世界观,你需要亲身去经历和体会这个世界,而不是听别人说。所以,当我看到身边的人情绪激动地要抵制这个国家,搞死那个民族的时候,我都会建议他去趟那个国家最好在在那个国家呆上一段时间,亲自感受一下。

再后来发现,要抵制的越来越多,小时候的美英帝国主义,然后是日本,再后面是法国、韩国、菲利宾、印度、德国、瑞典、加拿大……从小时候的台独到现在的港独、藏独、疆独……发现再这样下去,基本上来说,自己的人生也不用干别的事了……另外,随着自己的成长,越来越明白,抵制这个抵制那个只不过是幼稚和狭隘的爱国主义,真想强国,想别让他人看得起,就应该把时间和精力放在努力学习放在精益求精上,做出比他们更好的东西来。另外,感觉用对内的爱国主义解决对外的外交问题也有点驴唇不对马嘴,无非也就是转移一下内部的注意力罢了,另外还发现爱国主义还可以成为消费营销手段……不是我不爱国,是我觉得世道变复杂了,我只是一个普通的老百姓,能力有限,请不要赋予我那么大的使命,我只想在我的专业上精进,能力所能及地帮助身边的人,过一个简单纯粹安静友善的生活……

另外,为什么国与国之间硬要比个你高我低,硬要分个高下,硬要争出个输赢,我也不是太理解,世界都已经发展到全球化的阶段了,很多产品早就是你中有我,我中有你的情况了。举个例子,一部手机中的元件,可能来自全世界数十个国家,我们已经说不清楚一部手机是究竟是哪个国家生产的了。即然,整个世界都在以一种合作共赢全球化的姿态下运作,认准自己的位置,拥抱世界,持续向先进国家学习,互惠互利,不好吗?你可能会说,不是我们不想这样,是别人不容我们发展……老实说,大的层面我也感受不到,但就我在的互联网计算机行业方面,我觉得整个世界的开放性越来越好,开源项目空前地繁荣,世界上互联网文化也空前的开放,在计算机和互联网行业,我们享受了太多的开源和开放的红利,人家不开放,我们可能在很多领域还落后数十年。然而现在很多资源我们都访问不了,用个VPN也非法,你说是谁阻碍了发展?我只想能够流畅地访问互联网,让我的工作能够更有效率,然而,我在自己的家里却像做贼一样去学习新知识新技术,随时都有可能被抓进监狱……

随着自己的经历越多,发现这个世界越复杂,也发现自己越渺小,很多国家大事并不是我不关心,是我觉得那根本不是我这个平头老百姓可以操心的事,这个世界有这个世界运作的规律和方法,而还有很多事情超出了我能理解的范围,也超出了我能控制的范围,我关心不关心都一个样,这些大事都不会由我的意志所决定的。而所谓的关心,无非就是喊喊口号,跟人争论一下,试图改变其它老百姓的想法,然而,对事情的本身的帮助却没有多大意义。过上几天,生活照旧,人家该搞你还不是继续搞你,而你自己并不因为做这些事而过得更好。

我对国与国之间的关系的态度是,有礼有节,不卑不亢,对待外国人,有礼貌但也要有节气,既不卑躬屈膝,也不趾高气昂,整体上,我并不觉得我们比国外有多差,但我也不觉得我们比国外有多好,我们还在成长,还需要帮助和协作,四海之内皆兄弟,无论在哪个国家,在老百姓的世界里,哪有那么多矛盾。有机会多出去走走,多结交几个其它民族的朋友,你会觉得,在友善和包容的环境下,你的心情和生活可以更好

我现在更多关心的是和我生活相关的东西,比如:上网、教育、医疗、食品、治安、税务、旅游、收入、物价、个人权益、个人隐私……这些东西对我的影响会更大一些,也更值得关注,可以看到过去的几十年,我们国家已经有了长足的进步,这点也让我让感到很开心和自豪的,在一些地方也不输给强国。虽然依然有一些事的仍然没有达到我的预期,并也超出了我个人的控制范围无法改变,但整体都在变好。不过,未来的变数谁也不好说,我的安全感似乎还不足够,所以,我还是要继续努力,以便我可以有更多的选项。有选项总比没得选要好。所以,我想尽一切办法,努力让选项多起来,无法改变无法影响,那就只能提高自己有可选择的可能性。

另外,在网上与别人对一些事或观点的争论,我觉得越来越无聊,以前被怼了,一定要怼回去,现在不会了,视而不见,不是怕了,是因为,网络上的争论在我看来大多数都是些没有章法,逻辑混乱的争论。

  • 很多讨论不是说事,直接就是怼人骂人。随意就给人扣个帽子。
  • 非黑即白的划分,你说这个不是黑的,他们就把你划到白的那边。
  • 飘移观点,复杂化问题。东拉西扯,牵强附会,还扯出其它不相关的事来混淆。
  • 杠精很多,不关心你的整体观点,抓住一个小辫子大作文章。

很明显,与其花时间教育这些人,不如花时间提升自己,让自己变得更优秀,这样就有更高的可能性去接触更聪明更成功更高层次的人。因为,一方面,你改变不了他们,另外,改变他们对你自己也没什么意义,改变自己,提升自己,让自己成长才有意义。时间是宝贵的,那些人根本不值得花时间,应该花时间去结交更有素质更聪明的人,做更有价值的事。

美国总统富兰克林·罗斯福妻子埃莉诺·罗斯福(Eleanor Roosevelt)说过下面的一句话。

Great minds discuss ideas;
Average minds discuss events;
Small minds discuss people

把时间多放在一些想法上,对自己对社会都是有意义的,把时间放在八卦别人,说长到短,你也不可能改善自己的生活,你批评这个批评那个,看不上这个看不起那个,不会让你有成长,也不会提升你的影响力,你的影响力不是你对别人说长道短的能力,而是别人信赖你并希望得到你的帮助的现象。多交一些有想法的朋友,多把自己的想法付诸实践,那怕没有成功,你的人生也会比别人过得有意义。

如果你看过我以前的文章,你会看到一些吐槽性质的文章,而后面就再也没有了。另外,我也不再没有针对具体的某个人做出评价,因为人太复杂的了,经历的越多,你就会发现你很难评价人,与其花时间在评论人和事上,不如把时间花在做一些力所能及的事来改善自己或身边的环境。所以,我建议大家少一些对人的指责和批评,通过对一件事来引发你的思考,想一想有什么可以改善,有什么方法可以做得更好,有哪些是自己可以添砖加瓦的?你会发现,只要你坚持这么做,你个人的提升和对社会的价值会越来越大,而你的影响力也会越来越大

现在的我,即不是左派也不是右派,我不喜欢爱国主义,我也不喜欢崇洋媚外,我更多的时候是一个自由派,哪边我都不站,我站我自己。因为,生活在这样的一个时代,能让自己过好都是一些比较奢望的事了。

《教父》里有这样的人生观:第一步要努力实现自我价值,第二步要全力照顾好家人,第三步要尽可能帮助善良的人,第四步为族群发声,第五步为国家争荣誉。事实上作为男人,前两步成功,人生已算得上圆满,做到第三步堪称伟大,而随意颠倒次序的那些人,一般不值得信任。这也是古人的“修身齐家治国平天下”!所以,在你我准备要开始要“平天下”的时候,也得先想想,自己的生活有没有过好了,家人照顾好了么,身边有哪些力所能及的事是可以去改善的……

穷则独善其身,达则兼济天下。提升自己,实现自我,照顾好自己的家人,帮助身边的人。这已经很不错了!

什么样的人干什么样的事,什么样的阶段做什么样的选择,有人的说,选择比努力更重要的,我深以为然,而且,我觉得选择和决定,比努力更难,努力是认准了一个事后不停地发力,而要决定去认谁哪一个事则是令人彷徨和焦虑的(半途而废的人也很多)。面对人生,你每天都在作一个一个的决定,在做一个又一个的选择,有的决定大,有的决定小,你的人生的轨迹就是被这一个一个的决定和选择所走走出来的。

我在24岁放弃了一房子离开银行到小公司的时候,我就知道,人生的选择就是一个翘翘板,你要一头就没有另一头,选择是有代价的,你不选择的代价更大;选择是要冒险的,你不敢冒险的风险更大;选择是需要放弃的,因为无论怎么选你都要放弃什么。想想你老了以后,回头一看,好多事情在年轻的时候都不敢做,而你再也没有机会,你就知道不敢选择不敢冒险的代价有多大了。选择就是一种 trade-off,这世上根本不会有什么完美,只要你想做事,你有雄心壮志,你的人生就是一个坑接着一个坑,你所以做的就是找到你喜欢的方向跳坑。

所以, 你要想清楚你要什么,不要什么,而且还不能要得太多,这样你才好做选择。否则,你影响你的因子太多,决定不好做,也做不好。

就像最前面说的一样,你是激进派还是保守派,你是喜欢领导还是喜欢跟从,你是注重长期还是注重短期,你是注重过程还是注重结果……等等,你对这些东西的坚持和守护,成为了你的“三观”,而你的三观则影响着你的选择,而你的选择影响着你的人生。

下面是一些大家经常在说,可能也是大多数人关心的问题,就这些问题,我也谈谈我的价值取向。

挣钱。挣钱是一个大家都想做的事,但你得解决一个很核心的问题,那就是为什么别人愿意给你钱?对于挣钱的价值观从我大学毕业到现我就没怎么变过,那就是我更多关注的是怎么提高自己的能力,让自己值那个价钱,让别人愿意付钱。另外一方面,我发现,越是有能力的人,就越不计较一些短期得失,越计较短期得失的人往往都是很平庸的人。有能力的人不会关心自己的年终奖得拿多少,会不会晋升,他们更多的关心自己真正的实力有没有超过更多的人,更多的关注的是自己长远的成长,而不是一时的利益。聪明的人从来不关心眼前的得失,不会关心表面上的东西,他们更多关心的是长期利益,关心长期利益的人一定不是投机者,一定是投资者,投资会把自己的时间精力金钱投资在能让自己成长和提升的地方,那些让自己可以操更大的盘的地方,他们培养自己的领导力和影响力,而投机者在职场上会通过溜须拍马讨好领导,在学习上追求速成,在投资上使用跟随策略,在创业上甚至会不择手段,当风险来临时,投机者是几乎完全没有抗风险能力的,他们所谓的能力只不过因为形势好。

 

技术。对于计算机技术来说,要学的东西实在是太多,我并不害怕要学的东西很多,因为学习能力是一个好的工程师必需具备的事,我不惧怕困难和挑战。我觉得在语言和技术争论谁好谁坏是一种幼稚的表现, 没有完美的技术,Engineering 玩的是 Tradeoff。所以,我对没有完美的技术并不担心,但是我反而担心的是,当我们进入到一些公司后,这些公司会有一些技术上的沉淀也就是针对公司自己的专用技术,比如一些中间件,一些编程框架,lib库什么的。老实说,我比较害怕公司的专用技术,因为一旦失业,我建立在这些专用技术上的技能也会随之瓦解,有时候,我甚至害怕把我的技术建立在某一个平台上,小众的不用说了,大众的我也比较担扰,比如Windows或Unix/Linux上,因为一旦这个平台不流行或是被取代,那么我也会随之淘汰(过去的这20年已经发生过太多这样的事了)。为了应对这样的焦虑,我更愿意花时间在技术的原理和技术的本质上,这导致我需要了解各种各样的技术的设计方法,以及内在原理。所以,当国内的绝大多数程序员们更多的关注架构性能的今天,我则花更多的时间去了解编程范式,代码重构,软件设计,计算机系统原理,领域设计,工程方法……因为只有原理、本质和设计思想才可能让我不会被绑在某个专用技术或平台上,除非,我们人类的计算机这条路没走对。

 

职业。在过去20多年的职业生涯中,我从基层工程师做到管理,很多做技术的人都会转管理,但我却还是扎根技术,就算是在今天,还是会抠很多技术细节,包括写代码。因为我心里觉得,不写代码的人一定是做不好技术管理的,因为做技术管理有人要做技术决定,从不上手技术的人是做不好技术决定的,另一方面,我觉得管理是支持性的工作,不是产出性的工作,大多数的管理者无非是因为组织大了,所以需要管人管事,所以,必然要花大量的时间和精力处理各种问题,甚至办公室政治,然而,如果有一天失业了,大环境变得不好了,一个管理者和一个程序员要出去找工作,程序员会比管理者更能自食其力。所以,我并不觉得管理者这个职业有意思,我还是觉得程序员这个有创造性的职业更有趣。通常来说,管理者的技能力需要到公司和组织里才能展现,而有创造力的技能的人是可以自己独立的能力,所以,我觉得程序员的技能比管理者的技能能让我更稳定更自地活着。所以,我更喜欢“电影工作组”那样的团队和组织形式。

 

打工。对于打工,也就是加入一家公司工作,无论是在一家小公司还是一家大公司工作,都会有好的和不好的,任何公司都有其不完美的地方,这个需要承认。首先第一的肯定是完成公司交给你的任务,然后我会尽我所能在工作找到可以提高效率的地方进行改善。在推动公司/部门/团队在一技术和工程方面进步并不是一件很容易的事,因为进步是需要成本的,这成本并不一定是公司和团队愿意接授的,而另外,从客观规律上来说,一件事的进步一定是会有和现状有一些摩擦的。有的人害怕有摩擦而忍了,而我则不是,我觉得与别人的摩擦并不可怕,因为大家的目标都是基本一致的,只是做事的标准和方式不一样,这是可能沟通的,始终是会相互理解的。而如果你没有去推动一个事,我觉得对于公司对于我个人来说,都是一种对人生的浪费,敬业也好,激情也好,其就是体现在你是否愿意冒险去推动一件于公于私都有利的事,而不是成为一个“听话”、“随大流”、“懒政”的人,即耽误了公司也耽误了自己。所以,我更信仰的是《做正确的事情,等着被开除》,这些东西,可参看《我看绩效考核》,以及我在Gitchat上的一些问答

 

创业。前两天,有个小伙来跟我说,说他要离开BAT要去创业公司了,说在那些更自由一些,没有大公司的种种问题。我毫不犹豫地教育了他一下,我说,你选择这个创业公司的动机不对啊,你无非就是在逃避一些东西罢了,你把创业公司当做是一个避风港,这是不对的,创业公司的问题可能会更多,去创业公司的更好的心态是,这个创业公司在干的事业是不是你的事业?说白了,如果你是为了你的事业,为了解决个什么,为了改进个什么,那么,创业是适合你的,也只有在做自己事业的时候,你才能不惧困难,才会勇敢地面对一切那种想找一个安稳的避风港呆着的心态是不会让你平静地,你要知道世界本来就是不平静的,找了自己的归宿和目标才可能让你真正的平静。所以,在我现的创业团队,我不要求大家加班,我也不洗脑,对于加入的人,我会跟他讲各种问题和各种机遇,并一直在拷问他的“灵魂”,我们在做的事是不是你自己的事业诉求?可不可以更好?每个人都应该为自己的事业为自己的理想去活一次,追逐自己的事业和理想并不容易,需要有很大的付出,而也只有你心底里的那个理想值得这么大的付出……

 

客户。基于上述的价值观,在我现在创业的时候,我在面对客户的时候,也是一样的,我并不会完全的迁就于客户,我的一些银行客户和互联网客户应该体会到我的做的方式了,我并不觉得迁就用户,用户要什么我就应该给什么,用户想听什么,我就说什么,虽然这样可以省着精力,更圆滑,但这都不是我喜欢的,我更愿意鲜明地表达我的观点,并拉着用户跟我一起成长,因为我并不觉得完成客户的项目有成就感,我的成就感来自客户的成长。所以,面对客户有些做得不对有问题有隐患的地方,或是我有什么做错的事,我基本上都是直言不讳地说出来,因为我觉得这是对客户和对自己最大的尊重。我并不是在这里装,因为,我也想做一些更高级更有技术含量的事,所以,对于一些还达到的客户,我如果不把他们拉上来,我也对不起自己。

 

在我“不惑之年”形成了这些价值观体系,也许未来还会变,也许还不成熟,总之,我不愿跟大多数人一样,因为大多数人都是随遇而安随大流的,因为这样风险最小,而我想走一条属于自己的路,做真正的自己,就像我24岁从银行里出来时想的那样,我选择对了一个正确的专业(计算机科学),呆在了一个正确的年代(信息化革命),这样的“狗屎运”几百年不遇,如果我还患得患失,那我岂不辜负活在这样一个刺激的时代?!我所要做的就是在这个时代中做有价值的事就好了!这个时代真的是太好了!

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 February 26, 2019 at 04:02PM
from 酷 壳 – CoolShell https://ift.tt/2U7TXdJ

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【喵神】 Swift ABI 稳定对我们到底意味着什么

Swift 社区最近最重大的新闻应该就是 ABI 稳定了。这个话题虽然已经讨论了有一阵子了,但随着 Xcode 10.2 beta 的迭代和 Swift 5 的 release 被提上日程,最终 Swift ABI 稳定能做到什么程度,我们开发者能做些什么,需要做些什么,就变成了一个重要的话题。Apple 在这个月接连发布了 ABI Stability and MoreEvolving Swift On Apple Platforms After ABI Stability 两篇文章来阐述 Swift 5 发布以后 ABI 相关的内容所带来的改变。虽然原文不是很长,但是有些地方上下文没有说太清楚,可能不太容易理解。本文希望对这个话题以问答的形式进行一些总结,让大家能更明白将要发生的事情。

我是一个 app 开发者,Swift 5 发布以后会怎么样?

简单说,安装 Xcode 10.2,然后正常迁移就可以了,和以往 Swift 3 到 Swift 4 需要做的事情差不多。单论 Swift 5 这个版本,不会对你的开发造成什么影响,直到下一个版本 (比如 Swift 5.1) 之前,你几乎不需要关心 ABI 稳定这件事。关于下个 Swift 版本,我们稍后会提到这件事情。

我还是想知道什么是 ABI 稳定?

就是 binary 接口稳定,也就是在运行的时候只要是用 Swift 5 (或以上) 的编译器编译出来的 binary,就可以跑在任意的 Swift 5 (或以上) 的 runtime 上。这样,我们就不需要像以往那样在 app 里放一个 Swift runtime 了,Apple 会把它弄到 iOS 和 macOS 系统里。

所以说 app 尺寸会变小?

是的,但是这是 Apple 通过 App Thinning 帮我们完成的,不需要你操心。在提交 app 时,Apple 将会按照 iOS 系统创建不同的下载包。对于 iOS 12.2 的系统,因为它们预装了 Swift 5 的 runtime,所以不再需要 Swift 的库,它们会被从 app bundle 中删掉。对于 iOS 12.2 以下的系统,外甥打灯笼,照旧。

一个新创建的空 app,针对 iOS 12.2 打包出来压缩后的下载大小是 26KB而对 iOS 12.0 则是 2.4MB。如果你使用了很多标准库里的东西,那这个差距会更大 (因为没有用到的标准库的符号会被 strip 掉),对于一个比较有规模的 app 来说,一般可以减小 10M 左右的体积。

还有什么其他好处么?

因为系统集成了 Swift,所以大家都用同一个 Swift 了,app 启动的时候也就不需要额外加载 Swift,所以在新系统上会更快更省内存。当然啦,只是针对新系统。

另外,对于 Apple 的工程师来说,他们终于能在系统的框架里使用 Swift 了。这样一来,很多东西就不必通过 Objective-C wrap 一遍,这会让代码运行效率提高很多。虽然在 iOS 12.2 中应该还没有 Swift 编写的框架,但是我们也许能在不久的将来看到 Swift 被 Apple 自己所使用。等今年 WWDC 的消息吧。

我还想用一段时间的 Xcode 10.1,不太想这么快升级

Xcode 10.1 里的是 Swift 4.2 的编译器,出来的 binary 不是 ABI 稳定的,而且必定打包了 Swift runtime。新的系统发现 app 包中有 Swift runtime 后,就会选择不去使用系统本身的 Swift runtime。这种情况下一切保持和现在不变。旧版本的 Xcode 只有旧版本的 iOS SDK,所以自然你也没有办法用到新系统的 Swift 写的框架,系统肯定不需要在同一个进程中跑两个 Swift runtime。

简单说,你还可以一直使用 Xcode 10.1 直到 Apple 不再接受它打包的 app。不过这样的话,你不能使用新版本 Swift 的任何特性,也不能从 ABI 稳定中获得任何好处。

我升级了 Xcode 10.2,但是还想用 Swift 4 的兼容模式,会怎么样?

首先你需要弄清楚 Swift 的编译器版本语言兼容版本的区别:

编译器版本 语言兼容版本 对应的 Xcode 版本
Swift 5.0 Swift 5.0, 4.2, 4.0 Xcode 10.2
Swift 4.2 Swift 4.2, 4.0, 3.0 Xcode 10.0, Xcode 10.1
更多历史版本 …    

同一个 Xcode 版本默认使用的编译器版本只有一个 (在你不更换 toolchain 的前提下),当我们在说到“使用 Xcode10.2 的 Swift 4 兼容模式”时,我们其实指的是,使用 Xcode 10.2 搭载的 Swift 5.0 版本的编译器,它提供了 4.2 的语法兼容,可以让我们不加修改地编译 Swift 4.2 的代码。即使你在 Xcode 10.2 中选择语言为 Swift 4,你所得到的二进制依然是 ABI 稳定的。ABI 和你的语言是 Swift 4 还是 Swift 5 无关,只和你的编译器版本,或者说 Xcode 版本有关。

多提一句,即使你选择了 Swift 4 的语言兼容,只要编译器版本 (当然,以及对应的标准库版本) 是 5.0 以上,你依然可以使用 Swift 5 的语法特性 (比如新增加的类型等)。

看起来 ABI 稳定很美好,那么代价呢?

Good question! 我们在第一个问题里就提到过,一切都会很美好,直到下一个版本。因为 Swift runtime 现在被放到 iOS 系统里了,所以想要升级就没那么容易了。

在 ABI 稳定之前,Swift runtime 是作为开发工具的一部分,被作为库打包到 app 中的。这样一来,在开发时,我们可以随意使用新版本 Swift 的类型或特性,因为它们的版本是开发者自己决定的。不过,当 ABI 稳定后,Swift runtime 变为了用户系统的一部分,它从开发工具,变为了运行的环境,不再由我们开发者唯一决定。比如说,对应 iOS 13 的 Swift 6 的标准库中添加了某个类型 A,但是在 iOS 12.2 这个只搭载了 Swift 5 的系统中,并没有这个类型。这意味着我们需要在使用 Swift 的时候考虑设备兼容的问题:如果你需要兼容那些搭载了旧版本 Swift 的系统,那你将无法在代码里使用新版本的 Swift runtime 特性。

这和我们一直以来适配新系统的 API 时候的情况差不多,在 Swift 5 以后,我们需要等到 deploy target 升级到对应的版本,才能开始使用对应的 Swift 特性。这意味着,我们可能会需要写一些这样的兼容代码:

// 假如 Swift 6.0 是 iOS 13.0 的 Swift 版本
if #available(iOS 13.0, *) {
    // Swift 6.0 标准库中存在 A
    let a = A()
} else {
    // 不存在 A 时的处理
}

对于“新添加的某个类型”这种程度的兼容,我们可以用上面的方式处理。但是对于更靠近语言层面的一些东西 (比如现在已有的 Codable 这样的特性),恐怕适配起来就没有那么简单了。在未来,Deployment target 可能会和 Swift 语言版本挂钩,新的语言特性出现后,我们可能需要等待一段时间才能实际用上。而除了那些纯编译期间的内容外,任何与 Swift runtime 有关的特性,都会要遵守这个规则。

可以像现在一样打包新版本的 Swift runtime 到 app 里,然后指定用打包的 Swift 版本么

不能,对于包含有 Swift runtime 的系统,如果运行的 binary 是 ABI 稳定的,那么就必须使用系统提供的 Swift。这里的主要原因是,Apple 想要保留使用 Swift 来实现系统框架的可能性:

  1. 如果允许两个 Swift runtime (系统自带,以及 app 打包的),那么这两个运行时将无法互相访问,app 也无法与系统的 Swift 框架或者第三方的 ABI 稳定的框架进行交互。
  2. 如果允许完全替换 Swift runtime,系统的 Swift 框架将执行用户提供的 Swift 标准库中的代码,这将造成重大的安全隐患。

有任何可能性让我能无视系统版本,去使用 Swift 的新特性么

有,但是相对麻烦,很大程度上也依赖 Apple 是否愿意支持。如果你还记得 iOS 5.0 引入 ARC 时,Apple 为了让 iOS 4.3 和之前的系统也能使用 ARC 的代码,在 deployment target 选到 iOS 4.3 或之前时,会用 static link 的方式打包一个叫做 libarclite 的库,其中包含了 ARC 所需要的一些 runtime 方法。对于 ABI 稳定后的 Swift,也许可以采用类似做法,来提供兼容。

这种做法在感觉上和 Android 的 Support Library Packages 的方式类似,但是 Apple 似乎不是很倾向于提供这样的官方支持。所以之后要看有没有机会依靠社区力量来提供 Swift 的兼容支持了。

不能第一时间用上新的语言特性,必然会打击大家进行适配和使用新特性的积极性,也势必会影响到语言的发展和快速迭代,可以说这一限制是相当不利的。

所以,对于一般的 app 开发者来说,ABI 稳定其实就是一场博弈:你现在有更小的 app 尺寸,但是却被限制了无法使用最新的语言特性,除非你提升 app 的 depolyment target。

我是框架开发者,ABI 稳定后我可以用 binary 形式来发布了么?

还不能。ABI 稳定是使用 binary 发布框架的必要非充分条件。框架的 binary 在不同的 runtime 是兼容了,但是作为框架,现在是依靠一个 .swiftmodule 的二进制文件来描述 API Interface 的,这个二进制文件中包含了序列化后的 AST (更准确说,是 interface 的 SIL),以及编译这个 module 时的平台环境 (Swift 编译器版本等)。

ABI 稳定并不意味着编译工具链的稳定,对于框架来说,想要用 binary 的方式提供框架,除了 binary 本身稳定以外,还需要描述 binary 的方式 (也就是现在的 swiftmodule) 也稳定,而这正在开发中。将来,Swift 将为 module 提供文本形式的 .swiftinterface 作为框架 API 描述,然后让未来的编译器根据这个描述去“编译”出对应的 .swiftmodule 作为缓存并使用。

这一目标被称为 module stability,当达到 module stability 后,你就可以使用 binary 来发布框架了 (当然,这种 binary 框架只支持带有 ABI 稳定的 Swift runtime 的平台,也就是 iOS 12.2 及以上)。

能总结一下 ABI 稳定,或者展望一下未来么?

ABI 稳定最大的受益者应该是 Apple,这让 Apple 在自己的生态系统中,特别是系统框架中,可以使用 Swift 来进行实现。在我看来,Swift ABI 稳定为 Apple 开发平台的一场革命奠定了基础。在接下来的几年里,如果你还想要关注 Apple 平台,可能下面几件事情会特别重要:

  1. Apple 什么时候发布第一个 Swift 写的系统框架
  2. Apple 什么时候开始提供第一个 Swift only 的 API
  3. Apple 什么时候开始“锁定” Objective-C 的 SDK,不再为它增加新的 API
  4. Apple 什么时候开始用 Swift 特性更新现有的 Objective-C SDK

这些事情也许会在未来几年陆续发生。面对微软从 Win32 API 向 .Net 一路迁移,到今天的 UWP (Universal Windows Platform),Google 来势汹汹的 Fuchsia 和 Dart,Swift 是 Apple 唯一能与它们抗衡的答案。相比于微软提供的泛型和并行编程模型,Google 的 Flutter 的跨平台的先天优势,Apple 平台基于 Objective-C 的 API 的易用性已然被抛开很远。虽然 Apple 在 2014 年承诺过依然维护 Objective-C,但是经过 Swift 这五年的发展,随着 Swift ABI 的稳定,什么时候如果 Objective-C 成为了继续发展的阻碍,相信 Apple 已经有足够的理由将它抛弃。

作为 Apple 平台的从业者,我们也许正处在另一个时代变革的开端。

February 21, 2019 at 09:28AM via OneV’s Den https://ift.tt/2SfbgI7

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 谈谈 Ops(二):流程和人

第二部分,我想谈一谈流程,依然来源于我的理解。Ops 的实践上面,有两部分内容紧密结合,不但共同显示了 Ops 的生产力,也在相当程度上体现了 Ops 的技术水平。这第一部分就是流程,也是今天要说的内容,另一部分是工具(也包括和使用工具相关的技能),下一次再说。

我认为 Ops 可以分为几个层次,最次的的一层,其特点是重度依赖于的人的直接“操作”。风险管理、因果行为,都通过流程来统一把控,并且遗憾的是只有流程——除了它基本没有可靠有效的工具,或是其他办法。

其实,流程本是个好东西。有时候 某些工程师被散漫和自由主义惯坏了,听到流程就反感。事实上,流程在很多情况下都有着举足轻重的作用。它们很容易控制,也很容易实施,基本是立竿见影 ,在不想要深入,不想要挖掘原因和研究对策的时候,流程上做一点改进,很快就能看到效果。

举个例子来说,多数的公司和团队,在线上代码发生变更的时候,都需要进行风险管理,这里面几乎一定会有流程。比如或最有名的叫做“变更管理”(change management)的请求,一般由开发人员撰写这样的请求,然后由项目经理和 Ops 的责任人审批。这样的请求中,需要包括诸如变更内容、必要性、风险、部署步骤、验证方法、回滚方式等等内容。这样的目的,在于尽量把变化的因素变成预期内的、可控的因素,尽早发现可能存在的问题,降低风险。

但是, 流程这样的方式,也有着负面效应。最大的效应就是,它单纯地固化行为,而拒绝人主动的思考。 就像几年前国内热炒的“敏捷”一样,流程不应该是本质,工具也不应该是本质,只有人才是本质。

在我曾经的一个团队中,在项目发布以前的最后阶段,有限的时间里面(一般都是一个晚上),需要把最重要和最核心功能过一遍,这个功能清单叫做 checklist。为什么不把所有的测试案例都覆盖了?因为时间有限。这就是一个很简单也很容易执行的流程。但是,随着时间推演,问题变得很多。比如产品发布了以后,发现有一个比较大的问题,于是研发团队就要回溯问题,发现问题以后,为了杜绝问题的再次发生,就打算采取某些措施。(到目前为止做法上面都没有什么问题,可接下去就有争议了。)于是一条用于检测这个问题的识别项被加到这个 checklist 当中。这里面有很多检查项事实上在问题修复以后是不会再出现的了,也有一些检查项明显是用于覆盖位于边角的 corner case,而不是主要的 case,但是既然出过问题,为了保险起见,还是都加进去了。就这样,随着时间和版本的演进,这个 checklist 变得越来越长,某些验证项的执行难度颇为复杂,在几年以后,已经到了几个小时都无法过完这个 checklist 的地步,于是这个流程就变成了一个越来越难以执行的累赘。

造成这一问题的原因是什么,就是 流程太简便了,太有效了,以至于这些聪明人不再思考应该采用什么样的方式来从根本上彻底地解决问题

现在我不想进一步分析上面的问题,而是来看看这样一个争议。这个“古老”的争议和流程关系密切,到底应该保留单独的测试团队,还是应该让开发来做测试?

无论你对这个争议的观点如何,无论二者取舍的利弊如何,无论这两种结论的场景适用性如何,这个争议本质上反映了一件事——我们是应该用更多流程加上多个单一职责的团队来解决问题,还是用更少流程加上单个承担多种职责的团队来解决问题?

在回到上面 checklist 的那个问题上,这个问题恰恰出在开发和测试团队单独运作的体系之中。有人问,这些新增加的问题中,既然有一些检查项是不会再出现的,既然有一些检查项覆盖的是边角非主要用例,为什么大家还要同意加上去?这就和上面说的测试和开发团队分离的情况有密切关系。因为测试团队多数都做不到白盒,不知道实现,只想用输入输出覆盖黑盒用例的方法来保证正确性,因而所谓的问题“不再出现”就无从谈起;而团队和人一样,一朝被蛇咬,十年怕井绳,出过问题,不知道实现,也自然没有人愿意承担这个再出问题的风险,哪怕它曾经说一个边角的 case。

事实上,这个争议的观点可以扩展到更多角色。不要以为只有测试团队遇到这样的困惑。Ops 也是如此——到底应该保留单独的运维团队,还是应该让开发来做运维?

于是,我听过 Ops 团队的朋友说过这样的话,听起来很有意思:

如果线上问题少,boss 说,要你们何用?

如果线上问题多,boss 说,要你们何用?

当然,这些争议,最终都需要达成某种平衡,没有一种方法是放在所有场景下的万全之策。比方说,一些 AWS 服务中,Ops 的比重居然占到了 85% 以上。且不说其最终的合理性,市场和人才等方面的策略永远制约着团队去寻找最优雅的解决方案,而是选择最“合理”的解决方案,即便如此,和其庞大的基础设施业务相比,其 Ops 团队依然是小而优秀的。这些单独的 Ops 可能在整个服务的漫长生命周期中始终无可替代,没有他们,开发团队也无法专注于核心功能,而要被大量的 Ops 事务困扰。这也是为什么许多互联网大公司在推行小团队和综合型团队,强调工程师职责需要覆盖 Development、QA 和 Ops 三部分的同时,依然保留少量的独立 QA 团队和独立 Ops 团队。

再从公司和团队发展壮大的角度观察流程在 Ops 中的变化。

在一家公司还小的时候,团队更为原始,但是 Ops 却更容易聚焦在核心问题上面。用户有困难?解决困难。产品有问题?解决问题。没有繁荣缛节,也没有太多可以复制的模式和需要遵循的流程。慢慢地流程多了起来,人做的事情也更加专业,这里可能会达到一个最佳的平衡点。因为再往后,因为那些流程的过度复杂性和开销,使得效率和质量的平衡被打破,一切向着低效和臃肿慢慢滑落。

从这个角度说,互联网公司在这方面要比传统企业更懂得简化和合理化流程的重要性。即便在公司壮大以后,依然有一些自下而上的反馈行为帮助这件事情发生。

总的来说,Ops 和 Dev 一样,兼具影响力、效率,以及风险。和 Dev 比起来,Ops 往往更为枯燥,不可控性更多,有时候不得不响应一些紧急的事情。对于从这三者的角度看来, 流程更多地,是用来在效率损失可以接受的情况下,控制风险,从而导向正向的影响力 。对于一些服务更 critical 的团队来说,风险控制相对地,更为重要,因而流程的比重可以适当增加;反之,流程需要简化,保证效率在一个高标准之上。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2SaoXvD

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 谈谈 Ops(一):我的运维经历

偶然地,在会看这些年写的文章的时候,发现涉及到软件工程方方面面的内容,但是关于 Ops 的内容却非常少。我觉得这是不太合适的,因为在实际工作中,Ops 显而易见地占据了一大块比重。于是我调整了分类目录,增加了这个单独的分类,并且这一次,我想零零散散地讲一讲我关于 Ops 的一些经历,以及关于 Ops 的一些观点。

所谓 Ops,指的就是 Operations,在中文翻译上看,我觉得“运维”这个词可能是最恰当的。作为一个软件工程师,Ops 有时会特指 DevOps,关于它的定义,在维基百科上有这样一张图片,我觉得基本正确地描述了 DevOps 涵盖的内容(见右侧图)。

可以看到三方面的内容,可是,由于我们会把 Development 单独拿出来讲,会把 QA 单独拿出来讲,因此当我们讲起 Ops 的时候,我们更多的指的就是上图中下面那个紫色的环。

我们都希望代码可以一蹴而就,测试可以面面俱到,软件和产品都可以顺利而愉快地跑起来,可这些都是一厢情愿。我们需要把新版本软件安全快速地部署上去,在出了问题之后需要采取措施减小影响,分析问题,并修复问题……这些问题都需要 Ops 这样不可替代的环节。由于我个人认识的局限性,在以往,Ops 显然是被我轻视的环节。当然,这可能也和我工作以前接受的教育也有关。读书的时候,我们学开发,我们学测试,可是我们从来都不学运维。可以说,上面这三个环的于我而言的重视程度,显然 Development > QA > Operations,这是不可否认的。我猜测着对于许多人来说也是如此。但是,随着工作经验的增加,我越来越意识到 Ops 本身的重要性。于是这些内容,打算分为几篇来讲,来自一个并不喜欢运维的软件工程师之手。

我在华为的经历

我工作的第一家公司是华为,这是一家对于 Ops 有着深刻理解和丰富经验的通讯软件公司。有意思的是,这也是在我熟悉的公司中,在 Ops 上花专人投入比例最高的一家。有许多项目的发布和运行,都是基于不同物理地区和物理站点的,这也是电信软件非常典型的一种部署模式。具体来说,就是华为的工程师,需要出差到电信运营商那里去,去安装部署。如果是一个全新的项目,这个过程叫做开局;如果是从别的服务提供商那里把项目接过来(比如一个项目,本来用的是中兴的软件,现在用户体验基本不变的基础上,挪到华为的平台上),这个过程叫做割接。其他我在十年前第一次出差,在中国联通 3G 机房,位置大概在北京上地,就是去开局。和开局花费的时长相比,割接通常更为迅速,但是压力更大,因为割接的场景通常意味着目标设备只有非常短的服务停止的时间,甚至要求无服务中断;另外,有时候竞争对手的关系,没有办法得到之前系统完整的信息,有许多决策需要根据经验来判断,甚至猜测——比如原数据库要从前一家竞争对手的老库迁移到华为的新库,数据转移的时候,某一列原数据库表里的字段在原始文档里没有,需要根据其内容和结构来推理其实际含义,再转移到新库恰当的位置上。

这种方式明显更为传统一些,从客户的角度来说,好处在于,我们跑到客户的地盘上,在他们的设备厂房里做文章,很多事情他们都更方便控制。对于一些复杂项目,需要有多家服务提供商协同合作的情况,甲方客户可以和各家乙方面对面工作,沟通和组织都会更加顺畅。至于这种方式的弊端之一——成本,对于国内的三大电信运营商来说,通常都不是一个问题。

上述成本包括人力成本,毕竟这样的方式需要有专职的运维人员看守。出于安全、审查等等角度的考量,设备是隔离的,网络是隔离的,运维人员需要在现场驻守,以处理预期内或预期外的各种问题。我们把那里叫做前方。而研发基地的工程师团队需要和运维团队沟通协作,解决各种各样的问题。这就是后方。通常后方的研发工程师没有办法直接动手,而是需要和运维工程师沟通以便采集数据和分析问题,在特殊情况下,为了增进效率,后方的研发工程师会出差前往前方现场,直接处理问题。

我还记得在那次开局的过程中,要和所有周边系统协同调试工作,这个过程叫做联调。由于整个 3G 系统庞大,我所负责的视频运营平台,以省为单位,要和不同的服务接口,比如计费服务等等,而甲方客户期望分散风险,每个省份的服务又都是独立的,而且实现厂商都是根据招标结果而来,每个省份都是独立的。因此和我们联调的服务来自各家厂商,这样的协同合作变得无比困难。这里面多数都非软件的问题,而是管理和沟通的问题。

出于成本的考量,如今互联网公司的很多机器设备多为普通性能的廉价设备,而硬件损坏的容错是作为整个分布式系统设计常规的一部分进行的。可那个时候,电信运营商的机器设备和如今互联网公司是大不一样的。我还记得我们的软件是部署在整个机架中间的数块单板上的,存储系统是部署在磁盘阵列上的,而数据库是部署在 IBM 小型机内的……于是我们许多的 Ops 操作,都是基于单台机器进行的。加上联调的许多工作,存在大量重复性的劳动。为此我写了很多 shell 脚本,来帮助提高工作效率,当时觉得很自豪。可是后来才慢慢感受到,真正优秀的 Ops 流程,是不需要自己现场去手写这些脚本的,工具应该帮我们干了几乎所有的事情。手动脚本是介于手动命令和工具之间的手段,但总体来说依然是一个容易犯错而且缺乏延续性的做法。一个成熟的运维流程,应该把这些犯错的可能减到最小。

这种 Ops 的模式可以让研发团队的工程师更专注于本职的研发工作,但是也会带来和实际场景脱节的问题。比如说,我在那个联通项目之后,转去做某一个新产品的基线版本了,基线版本多数情况并不直接上线,而是需要由定制团队进行本地化和具体项目化的定制,之后才能发布上线。这就足以见得我们离实际产品部署有多么遥远了。由此产生的其中一个典型问题就是,当时我们做的那个产品中,网站的那部分,由于搜索引擎优化等等问题,居然被 Google 爬虫给爬死了。这样严重的事情等一层一层分析、讨论和传播上来,等我们获知这样变体了多次的消息的时候,已经过去了很长一段时间,有一些具体的有价值的信息也丢失了,这让我们觉得离实际的产品仿佛很遥远。

我在 Amazon 的经历

Amazon 的库房遍布世界各地,而且更为零散和复杂,因而这种需要奔赴现场才能进行 Ops 的模式,多数情况下都是不适用的。Amazon 的不同团队会负责不同的服务,多数服务只用中心数据库或者数量有限的几个区域数据库,少数服务才在当地库房里面设置数据库。在 Amazon 内部,有一套专门负责版本管理和部署的工具,软件版本、依赖项目、包管理、分析、编译、测试、部署,等等等等,全部都由这套工具来完成。无论是 1 台机器,还是 100 台机器,软件工程师要做的,就是在适当的时候,盯着这套工具提供的界面,把软件按照指定的某种方案,部署到实际的机器上面去。任意两台机器具体部署的代码版本,通过工具就可以对比,如果要打补丁,要升级系统,要改权限,这套工具就可以完成。换言之,多数情况下都不需要 ssh 登陆到具体的机器上去。

我有一些互联网大公司的朋友,包括国内国外,从侧面了解过,再加上如今我所在的公司,综合比较起来,Amazon 内部提供给工程师用于 Ops 的工具应该说多数都非常先进,有些能够领先业界好几年,而这套部署工具尤其可以说极少公司能出其右(我以前的一位老板打趣说过它是 Amazon 内部“四大金刚”工具之一)。其实,极少有公司愿意在没有必要的情况下把一个内部工具功能做得无比强大,更何况是以 frugality 作为领导力准则之一的 Amazon。其原因之一就是,工程师的成本。招聘运维团队的工程师,其实并不容易,而如果能够用尽量少的研发工程师团队,“顺便”去把运维的事情做了,这无疑是很节约成本的事情。从这个角度说,资源的限制才能促进创新和发展。

有了一系列 Ops 工具,Amazon 不需要招特别多的专职 Ops 团队,而多数 Ops 工作自然由不同的工程师完成。其中一个最典型的事情就是 oncall。所谓 oncall,就是值班,一般研发团队里的工程师轮值,一旦出现严重的线上问题,警报就会想起,这个过程叫做 page,这种情况一旦出现,不管是上班时间还是下班时间,都要立刻投身问题应急处理的行动中去。事实上,Google 也好、Facebook 也好,Netflix 也好,专职 Ops 团队的人数相对研发整体来说都比较小,但是我依然认为 Amazon 是其中最不容易的一个,因为 Amazon 的许多产品和服务尤其需要繁重的 Ops 工作,在如今的公司做了将近一年的云设施的工作才慢慢了解,和其它一些互联网服务比起来,提供基础设施的 AWS 需要的运维工作量非常非常巨大。

有人说,让研发工程师去做运维,能做好吗?不是应该让专业的人去做专门的事情吗?这个观点是两说的,运维技能的缺乏可以通过优秀的运维工具来环节;而另一方面,每多一种“专业的人”,就意味着整个工作系统中,多了一个角色,多了一个 N 个需要沟通的环节,这些都是内耗。我在 这篇文章 里面曾经比较过使用专业运维人员和使用研发人员来代理运维工作的情形。这种方式下,出现的问题能够最快速度和最大程度地引起开发人员的注意,有反向强化软件质量的作用。我相信多数软件开发工程师都不喜欢 Ops,这也容易理解,但是不参与 Ops 是很难想象能够做好产品的。

说一个具体事例。我记得在 Amazon 的销量预测团队工作的时候,有一次我 oncall 被 page 醒,因为新发布的软件本身暴露出来了一个问题,于是着手回滚到上一个版本。可是经过 rollback 之后,发现问题并未解决,调查获知原因是客户端缓存了前一个版本的某些有问题的信息,于是连夜赶补丁,刷新客户端内的信息,从而修复问题。事后,我们团队排查了类似的问题,相当于吃一堑长了一智。这样严重的问题不经过 oncall 这样典型的 Ops 经历,是很快速难反向强化回代码上的。

在我目前的公司中,Ops 方面所采用的方式和 Amzon 是类似的,Ops 在每个研发团队中的占比不同,我见过 10% 的,我也见过 80% 的。在我目前的项目团队,由于种种原因,Ops 的比重大概占到 40% 左右,这比我今年在前一个项目组中的 Ops 高了近一倍,也比我在 Amazon 期间最后一个团队的 Ops 工作量 30% 高,以我的理解来说,这明显偏高。其中的原因比较复杂,我们希望我们能够努力把它降到 25% 左右。当然,这并不是一件容易的事情,我对此也有一些思考,有关的内容等合适的时候再说吧。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2MiFoAX

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 写在曼联主教练又一次更迭之际

作为一名好多年的曼联球迷,在后弗格森时代,第三任教练下课的时间点上,总有很多感慨,也有不少想说的话。英格兰足坛浮浮沉沉,进化了这么多年,那个老四强时代已经过去,那个群雄乱起的时代也接近尾声。这两年看来,可以说强弱集团军已经分明。第一集团属于曼城和利物浦,去年在联赛中不可一世的曼城,在今年也遇到了真正的竞争对手。第二集团包括阿森纳、热刺、切尔西和曼联,其中热刺现在位于第一和第二集团军之间,但是我认为从长远来看,加之阵容厚度考量,它是属于第二集团军的。整体上看,英超在世界的格局中目前是往上走的,这些进入欧冠的豪门也已经闯入淘汰赛了。这个时间点上可以谈的事情似乎很多,我想按照自己的理解来谈谈几个敏感的观点。

首先,弗格森退休以后,我们才感受到了弗格森在位的手腕和能力,这没有错,可是也要看到,英格兰联赛,早已不是弗格森时代的样子了。

特别是近几年,克洛普和瓜迪奥拉等新锐教练带来的理念变革,着着实实地帮助英格兰以往那种糙快猛的足球细腻化,如今的攻防体系更加多元和成熟。我从 2004 年开始看曼联的球,老实说,到弗格森退休那一年的班底,是冠军班底没错,但如今来看,我不相信他们还能继续拿冠军。事实上,在弗格森最后几年,在和一些世界强队的交手过程中,我已经看到曼联处在明显的下风。竞技体育的魅力在于结果的不确定性,因而一旦赢球,就会掩盖许多问题。一个神奇的教练,可以让球员发挥 120% 的能力,比较出色的球员可以变得非常出色。但是,球员的班底始终是强弱的主导因素,就如同你没法带领一支英超的中小球队多年竞争于联赛第一集团。应该说,从弗格森最后两三年开始,曼联的球员班底已经不是英超最强了,可能大多数时间里连前三都排不上,因而总是期望于联赛冠军是不甚合理的。我记得好些年前哈维曾经说过,世界上最大的俱乐部,对球员吸引力最大的俱乐部,除去巴萨皇马,接着就是曼联。可是现在看看,曼联的名字,早就排到了远远的位置,因而从这个客观环境的角度看转会市场,对于曼联俱乐部而言,难度明显更大了。

然而,球员班底的建设,在如今足坛,已经不是主教练一人之功了。主教练更迭的频率,已经让主教练主导的青训和交易难以为继。我们看到后弗格森时代,第一任更偏向传统和继承的莫耶斯带来了保守的引援,最后以执教生涯的失败告终;第二任性格乖张的范加尔,带来了数个让人看不懂的引援,提拔能力还远达不到以往一线队标准的青年军,除去足总杯,也以失败告终;第三任倔强和强硬的穆里尼奥,引援是否糟糕另议,但不能很好使用引援大家却都看见了,第一年带来了两个有一定分量的冠军(社区盾毕竟分量太轻),第三年在更衣室一锅粥的情况下还是狼狈离任。一个保持球队建设一致性的角色,包括青年队建设的青训主管,以及转会市场上负责进出的经理,但在曼联后者从未得到任命,让主教练去负责转会市场几乎就意味着一定糟糕的转会判断和风险控制,而主教练的更迭也让诸多引援变成了笑话。现在的引援模式不但不够合理,甚至听起来就觉得有些荒唐——主教练给一个转会清单,然后俱乐部派人去挨个谈判,这样就意味着人选被局限在清单以内了,无论这个清单是来自被信任的主教练还是其他人,这种方式几乎可以说不被宰都不可能。如果有优秀的球员,价格合适,但不在清单上,算多大数,买还是不买?事实上,如今的转会市场,早比十年前、二十年前复杂得多。让懂行的专人来负责几乎是每家大俱乐部的唯一选择。

其次,和在切尔西的成功相比,穆里尼奥真的退步了吗?

我看未必。大体上,穆里尼奥从未改变过他最核心的风格和思路。他的足球永远是立足于传统的攻防体系,压缩防线,针对性部署,支点中锋,边前卫回收防守等等,并且非常看重球员的精神品质。我有时候觉得,穆里尼奥对于精神品质的看重,是否已经到了走火入魔的地步,这也许和他的非球员出身有关。球员毕竟是去踢球的,而不是去打仗的。再激烈的同城德比,也只是一场足球比赛。我不认为从两度入主切尔西都拿了联赛冠军,到来到曼联,他就改变了多少。他的足球一直都是“难看”的,尤其在进攻端,除去球星个人能力,可供分析讨论的进攻套路并不多,而对于收缩防线的刻意追求,又进一步使得生涩的进攻体系彻底难产。我记得有这么一个事情,他公开表扬卢卡库从中锋位置回追到右边卫防守,我相信这是某种优秀的精神品质,但对于这样一个体重的中锋,我是持有明显的保留意见的。卢卡库进球难的问题,一大笔帐要记到这一类防守的部署上面。

在以前,他可以用他的办法赢球,但是足球也在变革和进化,如今的他,我们假想一下,拿着以往成功的办法,再去以往成功的球队,比如第三次入主切尔西,一样给他两年的时间,他还能带来冠军吗?我认为不能。因为瓜迪奥拉和克洛普席卷而来的高位逼抢和出球体系,已经让穆里尼奥的战术显得难以为继。他要找得到多么合适的球员才可能带来成功,而这个难度,已经无法和几年前相提并论了。所以说,不是穆里尼奥不行了,而是他的战术打法已经不足以维系他在如今的英超环境里再拿联赛冠军了。哪怕在弗格森时代,我也注意到,他本人虽说上了年纪了,某些方面相对刻板,却总体来说依然是一个善于不断改进的教练。比如当年他引入香川真司,打菱形 442,虽说最后也不能够算很成功,但能够在阵型上抛开最擅长的边锋,使用真真正正的前腰,也是一个主动的尝试。

看穆里尼奥的人员管理,其实也是类似的。他还是遵循他倔强的从严治军的方针,我不认为他有任何大的改变。可是如今的球员已经不一样了,他们更有主见,更接近市场,也把自己的利益放到更高的位置。就好像你对现在孩子们讲革命时代的故事一样,多半落得个冷场的结局。有不少人对“哄球员开心”嗤之以鼻,可事实就是,它不但重要,还极大地影响了球员的工作绩效。看看他主导的引援,多数都失败了:姆希塔良冷落了半个赛季,断断续续用了数个月,拿去换桑切斯了;可桑切斯呢,签了个毒药合同,变了个人似的,状态一落千丈;卢卡库连续不进球,效率低得可怕;弗雷德,打不上常规主力;拜利,犯了几个错,于是也打不上常规主力了。以前看到大牌球员,或者新球员要赢得和保持范加尔的信任很困难,如今看来他的弟子穆里尼奥更甚。这种情况下,俱乐部否决了穆里尼奥对一些接近三十岁的球员购买的要求,以及要求卖掉马夏尔的要求,也情有可原。俱乐部这种做法很难说最佳,但这件事情上明显是更合理的一方。

最后,现在曼联的这批人怎么样,是否真的不堪?

俱乐部层面球员班底建设的落后,确实存在,但也并非落得很远。无论是索尔斯克亚留任,还是以波切蒂诺为代表的进攻派教练带队,在人选方面,我们都可以从后到前一层一层来分别看看情况,并做一些猜测。

首先是门将,首要任务是和德赫亚续约,我认为他是属于那种训练一般,但天赋极高的门将。他在门线上的表现,也许就是世界最佳。损失德赫亚,在市场上极难找得到相近能力的替代者。

后防线上,问题其实是比较多的。从索尔斯克亚的用人来看,中卫上排第一的是林德洛夫,事实上,这也是符合他战术理念的。中卫出球能力极其重要,林德洛夫事实上单对单防守很不稳固,头球更是能用“糟糕”来形容。但是对于这些极度重视进攻的教练来说,在现代的防守体系中,一个出球能力强的中卫,有着无可替代的作用。有点遗憾布林德已经离开,要不然他可能是队中出球最好的中卫,也自然能得到一定的机会。罗霍和琼斯的出球能力都尚可,比拜利强,但是总体来说都不够稳固,强点也不足够强,属于平庸的中后卫。斯马林属于典型的英式中卫,头球好,可脚下的活要命。如果和林德洛夫搭档,一个脚下技术过得去但侵略性、上抢都很出色的中卫会是更合适的选择,而让林德洛夫去避开对抗的弱势,去补位打覆盖,从这个角度看,没有一个合适的人选,因而中卫的引援是必要的。左后卫上面,我认为目前问题不大,如果要培养卢克肖,那就要找有合适的轮换,如果目前的身体素质没有明显退化,目前罗霍偏防守而阿什利杨偏重进攻,是很好的搭配。如果阿什利杨今夏离队,需要买人替代。右后卫的问题最大,无论是阿什利杨还是瓦伦西亚,都远远达不到要求(虽说曾经的瓦伦西亚有那样的能力,作为全能型的右后卫中,说是世界前五都不为过)。其原因在于,曼联的前场是严重左倾的,即是习惯使然,也是战术使然,无论是马塔还是林加德,都不甘心待在右路和套边的右后卫打配合。因此右后卫需要一个有能力自己拿球上下的人选,这样的人事实上在市场上很难找,找到了也是比较高的价格。

中场在我看来大体上不太需要进补,而且我看到不同风格的球员,能带来不少战术可行性。马蒂奇属于覆盖能力不强,但是出球和组织能力好的后腰。在后卫线出球遇到困难的时候,我们经常看到他坠后协助出球,在后卫线压力小,往上顶的时候,他又可以把球直接送到禁区前沿。埃雷拉属于奔跑能力强的 B2B 中场,覆盖广泛。目前看这两人搭配,都偏重防守,可以给博格巴带来最大的助益,让他多深入禁区。曼联实力是真正的世界级球员不多,可能除了德赫亚,就是博格巴了,我不管他有什么其他新闻,只要能帮助博格巴发挥出色,几乎就意味着进攻端解决了一半的问题。还有一个值得一提的人是费莱尼,我看他在索尔斯克亚的人选中排名比较靠后,而事实上,我数次看到,除了众所周知的高球战术,在中场推进困难的时候,他的作用也是无可替代的。比如超一流的胸部卸球能力,不能指望他传出什么杀伤力强的球,但是失误少,拿得下球,是个很重要的补充。至于其他人选,要么实力差太远,要么有严重的问题。比如弗雷德,半个多赛季了,依然适应不了英超的节奏和对抗,拿球和出球失误率太高,而且对防守端贡献似乎是个天生的短板,似乎能期待的就是他能够靠自己超过常人的跑动了,在当前的体系下,很可能是一个平庸的引援。

锋线上,我认为可以不补充新人,如果需要,那就是一个正儿八经的右边锋。世界上顶级的右边锋本来就很少,相反左边锋可能是最容易出球星的位置,这也是普利西奇能够卖到如此高价的原因之一。右边锋的缺乏让整个左侧过于拥挤,而对右边后卫的攻防要求都增加。左侧的马夏尔和桑切斯显得位置重叠,而拉什福德的最佳位置是在锋线,他在边路的时候倒是能拉开宽度,但是只有在中锋位置的时候他的跑动优势能够发挥得最佳,因此即便他打右边路,他只有游弋到禁区内才最有威胁。卢卡库的触球是个灾难,但他现在最大的问题还不是触球,而是体重。他需要减重,尤其是在绝大多数进攻教练的体系里面,中锋的跑动能力,尤其是单前锋的战术下,至关重要。以目前这样的体型,要保持第一时间压迫,很难支撑一整场比赛保持高强度跑动。

所以,总体来说,一个有独立拿球能力的右后卫是最需要补充的,其次是能拉开宽度的右边锋和一个盯人中卫。

看了那么多年曼联的足球了,心态以及渐渐平和,也更关注俱乐部长期的发展,而非一场比赛的得失。弗格森时代的结束,带来的不是阵痛,而是长期的痛楚,这个影响看起来远比当初想象的深远。看起来阿森纳也在慢慢开始经历类似的境遇,当然可能会缓和得多。英超如今的竞争,要比十年前良性得多,越是群雄并起的“乱世”,越能给更多球迷带来竞技体育未知性的魅力和体验。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2SH3jfy

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 倔强的程序员

对于程序员来说,大多数人公司都有技术和管理两条发展路线,通常在同一家公司,管理路线的发展可能性,要相对广阔一些;但是技术路线也有技术路线的好处,比如相对而言更依赖于硬实力,因而工作机会丰富。我相信有不少程序员都和我一样,坚守着技术路线,无论是进还是退,都对管理者的岗位没有什么兴趣。

兴许大家都听到软实力和硬实力的概念。对于一个技术人来说,硬实力大致上可以认为是计算机和软件工程相关的技术能力,1 还是 0,是还是非,会不会算法,懂不懂设计,清清楚楚,明明白白; 而软实力则反过来,听起来挺抽象,挺模糊,比如沟通能力,自我管理能力,但是却扮演者重要的角色,甚至随着职业生涯的发展,它的影响力越来越大。而性格,是软实力中一个很特别的影响因素。

下面我讲的是在程序员技术发展路线中,“倔强”性格的影响这一个窄窄的范围,而且是就我的认知而言的。显而易见它不可能是很客观的。我相信会有很多人持有不同的看法。

我想大家都认可的是,基本上每个团队里面都有各种脾气性格的人。记得我刚工作的时候,团队里面平和和好说话的人更多。多数人性格都比较平和,这可能和资历、眼界等等因素也有关。以前读过一篇文章,说一个团队里面,有各种各样的角色,有牛、有猪,有狗、有猴子等等等等,分别代表着不同的性格。随着时间的试炼,大家发展的情况各不相同。在讨论方案和问题的时候,肯定有人不同意,但是只要多数人决定了做法,或者是几个强硬派决定了做法,大多数人也就不再计较,因此 commitment 比较容易做出,且朝着一致的方向。

但是随着工作年头的增加,我发现团队里面个人的性格,普遍是越来越倔了。无论什么时候我们讨论问题,观点不同是司空见惯的。可现在不同的是,要达成一致,并不是那么容易的事情。好吧,大多数人支持方案 A,少数人支持 B,兴许几年前这票支持 B 的就表示愿意按照多数派的 A 来实施了;可是现在呢,少数派一定要争论下去,技术方案的选择不是少数服从多数的选举,为什么要 A,我们来给 A 和 B 做做分析,我们来激烈地争论吧……

以前我认为,职业生涯的发展,到一定阶段,高级别一些的工程师,想必也是性格各异的吧,应该有的人比较强硬,有的人比较容易 pushover 的吧,性格这东西嘛,分布都是有随机性的。可是如今我接触到的情况呢,却恰恰相反。这些发展比较好的程序员,相对于其年龄和资历,级别比较高的程序员,居然性格几乎无一例外的“倔强”。而那些性格比较“好”的呢,相对来说发展普遍都没有那么好。看到的案例多了,似乎可以粗略地得到这样的结论:走技术路线的程序员中,性格倔强的人不一定发展得快,但是性格平和的人肯定不行。

虽然我能看得到的案例数量并不大,但我依然觉得这个现象有一定代表性。我觉得这里有这么几个因素:

  • 倔强的程序员,往往也是较真的程序员,他们会追求最佳的解决方案,他们会追求最合理的代码实现,他们可能抠一点某些人看来无足轻重的东西,但是就是这些东西把软件的质量提高。
  • 倔强的程序员,懂得维护自己认为正确的观点,而为了维护这个观点,会反复思考和分析。我没有见过一个能把 trade-off 做得好的人对维护自己的观点抱无所谓的态度。
  • 倔强的程序员,遇到困难也不那么容易退缩。这也是显而易见的,性格软弱的人,通常也不愿意坚持己见。

但是,物极必反,倔强的程序员,也可能死得特别惨。我见过一些被踢出团队的程序员,大致分为两类。一类是能力实在不足,绩效特别差,比如代码写得又慢 bug 又多;还有一类就是这类硬骨头,倔强到难以维持基本客观的程度,到处树敌,太过拖累整个团队的工作。

再结合程序员工作中的许多具体事情,再进一步谈一谈这些倔强的程序员们。就说个有趣的事情吧。我们把他们中的其中一个,叫做大 Z(这个字母看着就很霸气),而相对不那么“难搞”的程序员,叫做小 s。

在一次的设计讨论会议上大 Z 说对小 s 说,我认为你的方案不如我的好,理由是 xxxxx,于是大 Z 和小 s 来来回回一番争论,刚开始还算可控,但是大 Z 说,“我觉得你缺少扩展性的常识”。有经验的人可能马上意识到,大 Z 的这句话已经从“对事”变成了“对人”,这明显是不对的。于是这句话一冒出来,小 s 马上就不高兴了,再不痛不痒辩论几句以后,没有继续争论下去,显得很失落。

这个场景看起来是不是很熟悉?哪怕小 s 是更在理的一方,也放弃了继续争论下去的欲望,反而落得自己不爽好几天,每次和大 Z 沟通都会想着当时的场景,甚至觉得大 Z 还会有意无意针对他。有人可能会觉得,那大 Z 会不会事后觉得自己过分呢?我想说,大多数情形下,不会的,以大 Z 的性格来说,他冒犯了小 s,他也许意识到了,也许没意识到,可是这样的事情他根本就不会放在心上。回到事情本身,谁的方案更合理很难讲,但这件事情本身伤害到了团队中的成员,影响了团队的氛围。我们可能见到类似的事情到处都是,甚至在某些沟通强烈的地方尤为严重,比如 code review。

多数情况下,我们撇开技术本身的因素,谁的发展更好呢?却是大 Z。虽然有少数情况并非如此,但是多数情况下,大 Z 却有着更更为广泛的影响力,而某些情况下争论所显露出来的 backbone 会盖过他在争论和为人上面的“恶霸”属性。这也从某种角度说明,为什么到了一定级别的程序员,且不论技术如何,心理承受能力和沟通的技巧,都是有一定造诣的,那些敏感而脆弱的呢,已经挂在晋升的半路上了。

交流和沟通本身就是一个说不清道不明的复杂体,很多人可能会想要安安心心做技术,我相信也有很多公司希望提供这样环境。可事实是,绝大多数情况下,越是这样想的人,就越会发现,这只是一种美好的愿望,不可避免地,有很多为人处世上的“屁事”,未必要上升到“职场政治”那么高的程度,却依然会考验你的心理,磨炼你的性格。

最后,从团队管理的角度来说,哪一种人更合适呢?

其实,“合适”这个词的定位很难讲,但是倔强的程序员通常更难管理,这倒是真的。可是,换一个角度想这个问题,为什么要“管”,管理又要做到怎样的侵入性?理想的状况是,虽然有一些性格似乎比较“强硬”的程序员,但是他们是讲原则,讲道理的,如果团队的成员在总的目标上大致是一致的,团队就能够具备一定的兼容性。可理想毕竟是理想,团队中的磕磕碰碰遇到谁都能喝一壶的。特别是,如果管理者想成为那个决策绝大多数事情的人,碰到这些倔强的“大爷”们,很可能就会碰一鼻子灰。

在我的职业生涯中,待过好些团队,我见过这种管理者和倔强的程序员们之间的碰撞,有在挣扎和妥协中寻求平衡的,有程序员滚蛋的,也有管理者扫地出门的,甚至有两败俱伤,鱼死网破的。这里面也有很多有趣的故事,下次再说吧。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 https://ift.tt/2SOaylG

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 记一次Kubernetes/Docker网络排障

昨天周五晚上,临下班的时候,用户给我们报了一个比较怪异的Kubernetes集群下的网络不能正常访问的问题,让我们帮助查看一下,我们从下午5点半左右一直跟进到晚上十点左右,在远程不能访问用户机器只能远程遥控用户的情况找到了的问题。这个问题比较有意思,我个人觉得其中的调查用到的的命令以及排障的一些方法可以分享一下,所以写下了这篇文章。

问题的症状

用户直接在微信里说,他们发现在Kuberbnetes下的某个pod被重启了几百次甚至上千次,于是开启调查这个pod,发现上面的服务时而能够访问,时而不能访问,也就是有一定概率不能访问,不知道是什么原因。而且并不是所有的pod出问题,而只是特定的一两个pod出了网络访问的问题。用户说这个pod运行着Java程序,为了排除是Java的问题,用户用 docker exec -it 命令直接到容器内启了一个 Python的 SimpleHttpServer来测试发现也是一样的问题。

我们大概知道用户的集群是这样的版本,Kuberbnetes 是1.7,网络用的是flannel的gw模式,Docker版本未知,操作系统CentOS 7.4,直接在物理机上跑docker,物理的配置很高,512GB内存,若干CPU核,上面运行着几百个Docker容器。

问题的排查

问题初查

首先,我们排除了flannel的问题,因为整个集群的网络通信都正常,只有特定的某一两个pod有问题。而用 telnet ip port 的命令手工测试网络连接时有很大的概率出现 connection refused 错误,大约 1/4的概率,而3/4的情况下是可以正常连接的。

当时,我们让用户抓个包看看,然后,用户抓到了有问题的TCP连接是收到了 SYN 后,立即返回了 RST, ACK

我问一下用户这两个IP所在的位置,我们知道了,10.233.14.129 是 docker010.233.14.145 是容器内的IP。所以,这基本上可以排除了所有和kubernets或是flannel的问题,这就是本地的Docker上的网络的问题。

对于这样的情况,在 telnet 上会显示 connection refused 的错误信息,对于我个人的经验,这种 SYN完直接返回 RST, ACK的情况只会有三种情况:

  1.  TCP链接不能建立,绝大多数情况都是服务端没有相关的端口号
  2. TCP链接建错误,有可能是因为修改了TCP参数
  3. 有防火墙iptables的设置

因为当时还在开车,在等红灯的时候,我感觉到有点像 NAT 的网络中服务端开启了 tcp_tw_recycle 和 tcp_tw_reuse 的症况(详细参看《TCP的那些事(上)》),所以,让用户查看了一上TCP参数,发现用户一个TCP的参数都没有改,全是默认的,于是我们排除了TCP参数的问题。

然后,我也不觉得容器内还会设置上iptables,所以,我怀疑容器内的端口号没有侦听上,但是马上又好了,这可能会是应用的问题。于是我让用户那边看一下,应用的日志,并用 kublet describe看一下运行的情况,并把宿主机的 iptables 看一下。

然而,我们发现并没有任何的问题。这时,我们失去了所有的调查线索,感觉不能继续下去了……

重新梳理

这个时候,回到家,大家吃完饭,和用户通了一个电话,把所有的细节再重新梳理了一遍,这个时候,用户提供了一个比较关键的信息—— “抓包这个事,在 docker0 上可以抓到,然而到了容器内抓不到容器返回 RST, ACK ” !然而,根据我的知识,我知道在 docker0 和容器内的 veth 网卡上,中间再也没有什么网络设备了(参看《Docker基础技术:LINUX NAMESPACE(下)》)!

于是这个事把我们逼到了最后一种情况 —— IP地址冲突了!

Linux下看IP地址冲突还不是一件比较简单事的,而在用户的生产环境下没有办法安装一些其它的命令,所以只能用已有的命令,这个时候,我们发现用户的机器上有 arping 于是我们用这个命令来检测有没有冲突的IP地址。使用了下面的命令:

$ arping -D -I docker0 -c 2 10.233.14.145
$ echo $?

根据文档,-D 参数是检测IP地址冲突模式,如果这个命令的退状态是 0 那么就有冲突。结果返回了 1 。而且,我们用 arping IP的时候,没有发现不同的mac地址。 这个时候,似乎问题的线索又断了

因为客户那边还在处理一些别的事情,所以,我们在时断时续的情况下工作,而还一些工作都需要用户完成,所以,进展有点缓慢,但是也给我们一些时间思考问题。

柳暗花明

现在我们知道,IP冲突的可能性是非常大的,但是我们找不出来是和谁的IP冲突了。而且,我们知道只要把这台机器重启一下,问题一定就解决掉了,但是我们觉得这并不是解决问题的方式,因为重启机器可以暂时的解决掉到这个问题,而如果我们不知道这个问题怎么发生的,那么未来这个问题还会再来。而重启线上机器这个成本太高了。

于是,我们的好奇心驱使我们继续调查。我让用户 kubectl delete 其中两个有问题的pod,因为本来就服务不断重启,所以,删掉也没有什么问题。删掉这两个pod后(一个是IP为 10.233.14.145 另一个是 10.233.14.137),我们发现,kubernetes,在其它机器上重新启到了这两个服务的实例。然而,在问题机器上,这两个IP地址居然还可以ping得通

好了,IP地址冲突的问题可以确认了。因为10.233.14.xxx 这个网段是docker 的,所以,这个IP地址一定是在这台机器上。所以,我们想看看所有的 network namespace 下的 veth 网卡上的IP。

在这个事上,我们费了点时间。

  • 首先,我们到 /var/run/netns目录下查看系统的network namespace,发现没有。
  • 然后,我们到 /var/run/docker/netns 目录下查看Docker的namespace,发现有好些。
  • 于是,我们用指定位置的方式查看Docker的network namespace里的IP地址
$ ls /var/run/docker/netns | xargs -I {} nsenter --net=/var/run/docker/netns/{} ip addr 

我们发现了比较诡异的事情。

  • 10.233.14.145 我们查到了,docker的namespace下还有这个IP。
  • 10.233.14.137,这个IP没有在docker的network namespace下查到。

于是我上网查了一下,发现了一个docker的bug – 在docker remove/stop 一个容器的时候,没有清除相应的network namespace,这个问题被报告到了 Issue#31597 然后被fix在了 PR#31996,并Merge到了 Docker的 17.05版中。而用户的版本是 17.09,应该包含了这个fix。

要查看所有network namespace,只有最后一条路了,那就是到 /proc/ 目录下,把所有的pid下的 /proc/<pid>/ns 目录给穷举出来。好在这里有一个比较方便的命令可以干这个事 – lsns

于是我写下了如下的命令:

$ lsns -t net | awk ‘{print $4}' | xargs -t -I {} nsenter -t {} -n ip addr | grep -C 4 "10.233.14.137"

解释一下。

  • lsns -t net 列出所有开了network namespace的进程,其每4列是进程PID
  • 把所有开过network namespace的进程PID拿出来,转给 xargs 命令
  • xargs 命令把这些PID 依次传给 nsenter 命令,
    • xargs -t 的意思是会把相关的执行命令打出来,这样我知道是那个PID。
    • xargs -I {}  是声明一个占位符来替换相关的PID

最后,我们发现,虽然在 /var/run/docker/netns 下没有找到 10.233.14.137 ,但是在 lsns 中找到了三个进程,其用了10.233.14.137 这个IP,而且他们的MAC地址全是一样的!冲突了这么多。通过ps 命令,可以查到这三个进程,有两个是java的,还有一个是/pause (这个应该是kubernetes的沙盒)。

我们继续乘胜追击,穷追猛打,用pstree命令把整个进程树打出来。发现上述的三个进程的父进程都在多个同样叫 docker-contiane 的进程下!

这明显还是docker的,但是在docker ps 中却找不道相应的容器,什么鬼!我快崩溃了……

继续看进程树,发现,这些 docker-contiane 的进程的父进程不在 dockerd 下面,而是在 systemd 这个超级父进程PID 1下,我靠!进而发现了一堆这样的野进程(这种野进程或是僵尸进程对系统是有害的,至少也是会让系统进入亚健康的状态,因为他们还在占着资源)。

docker-contiane 应该是 dockerd 的子进程,被挂到了 pid 1 只有一个原因,那就是父进程“飞”掉了,只能找 pid 1 当养父。这说明,这台机器上出现了比较严重的 dockerd 进程退出的问题,而且是非常规的,因为 systemd 之所以要成为 pid 1,其就是要监管所有进程的子子孙孙,居然也没有管理好,说明是个非常规的。(注,关于 systemd,请参看《Linux PID 1 和 Systemd 》,关于父子进程的事,请参看《Unix高级环境编程》一书)

接下来就要看看 systemddockerd 记录的日志了……

总结

通过这个调查,可以总结一下,

1) 对于问题调查,需要比较扎实的基础知识,知道问题的成因和范围。

2)如果走不下去了,要生新梳理,回头看一下过的一些蛛丝马迹,但认真推敲每一个细节。

3) 各种诊断工具要比较熟悉,这会让你事半功倍。

4)系统维护和做清洁比较类似,你需要经党看看系统中是否有一些僵尸进程或是一些垃圾东西,这些东西要及时清理掉。

最后,多说一下,很多人都说,Docker适合放在物理机内运行,这并不完全对,因为他们只考虑到了性能成本,没有考虑到运维成本,在这样512GB中启动几百个容器的玩法,其实并不好,因为这本质上是个大单体,因为你一理要重启某些关键进程或是机器,你的影响面是巨大的

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 December 08, 2018 at 11:57AM
from 酷 壳 – CoolShell https://ift.tt/2G500Nn

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【喵神】 与 JOSE 战斗的日子 – 写给 iOS 开发者的密码学入门手册 (实践)

概述

这是关于 JOSE 和密码学的三篇系列文章中的最后一篇,你可以在下面的链接中找到其他部分:

  1. 基础 – 什么是 JWT 以及 JOSE
  2. 理论 – JOSE 中的签名和验证流程
  3. 实践 – 如何使用 Security.framework 处理 JOSE 中的验证 (本文)

这一篇中,我们会在 JOSE 基础篇和理论篇的知识架构上,使用 iOS (或者说 Cocoa) 的相关框架来完成对 JWT 的解析,并利用 JWK 对它的签名进行验证。在最后,我会给出一些我自己在实现和学习这些内容时的思考,并把一些相关工具和标准列举一下。

解码 JWT

JWT,或者更精确一点,JWS 中的 Header 和 Payload 都是 Base64Url 编码的。为了获取原文内容,先需要对 Header 和 Payload 解码。

Base64Url

Base64 相信大家都已经很熟悉了,随着网络普及,这套编码有一个很大的“缺点”,就是使用了 +/=。这些字符在 URL 里是很不友好的,在作为传输时需要额外做 escaping。Base64Url 就是针对这个问题的改进,具体来说就是:

  1. + 替换为 /
  2. / 替换为 _
  3. 将末尾的 = 干掉。

相关代码的话非常简单,为 DataString 分别添加 extension 来相互转换就好:

extension String {
    // Returns the data of `self` (which is a base64 string), with URL related characters decoded.
    var base64URLDecoded: Data? {
        let paddingLength = 4 - count % 4
        // Filling = for %4 padding.
        let padding = (paddingLength < 4) ? String(repeating: "=", count: paddingLength) : ""
        let base64EncodedString = self
            .replacingOccurrences(of: "-", with: "+")
            .replacingOccurrences(of: "_", with: "/")
            + padding
        return Data(base64Encoded: base64EncodedString)
    }
}

extension Data {
    // Encode `self` with URL escaping considered.
    var base64URLEncoded: String {
        let base64Encoded = base64EncodedString()
        return base64Encoded
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }
}

结合使用 JSONDecoder 和 Base64Url 来处理 JWT

因为 JWT 的 Header 和 Payload 部分实际上是有效的 JSON,为了简单,我们可以利用 Swift 的 Codable 来解析 JWT。为了简化处理,可以封装一个针对以 Base64Url 表示的 JSON 的 decoder:

class  Base64URLJSONDecoder: JSONDecoder {
    override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
        guard let string = String(data: data, encoding: .ascii) else {
            // 错误处理
        }
        
        return try decode(type, from: string)
    }
    
    func decode<T>(_ type: T.Type, from string: String) throws -> T where T : Decodable {
        guard let decodedData = string.base64URLDecoded else {
            // 错误处理
        }
        return try super.decode(type, from: decodedData)
    }
}

Base64URLJSONDecoder 将 Base64Url 的转换封装到解码过程中,这样一来,我们只需要获取 JWT,将它用 . 分割开,然后使用 Base64URLJSONDecoder 就能把 Header 和 Payload 轻易转换了,比如:

struct Header: Codable {
    let algorithm: String
    let tokenType: String?
    let keyID: String?

    enum CodingKeys: String, CodingKey {
        case algorithm = "alg"
        case tokenType = "typ"
        case keyID = "kid"
    }
}

let jwtRaw = "eyJhbGciOiJSUzI1NiI..." // JWT 字符串,后面部分省略了
let rawComponents = text.components(separatedBy: ".")
let decoder = Base64JSONDecoder()
let header = try decoder.decode(Header.self, from: rawComponents[0])

guard let keyID = header.keyID else { /* 验证失败 */ }

在 Header 中,我们应该可以找到指定了验证签名所需要使用的公钥的 keyID。如果没有的话,验证失败,登录过程终止。

对于签名,我们将解码后的原始的 Data 保存下来,稍后使用。同样地,我们最好也保存一下 {Header}.{Payload} 的部分,它在验证中也会被使用到:

let signature = rawComponents[2].base64URLDecoded!
let plainText = "\(rawComponents[0]).\(rawComponents[1])"

这里的代码基本都没有考虑错误处理,大部分是直接让程序崩溃。实际的产品中验证签名过程中的错误应该被恰当处理,而不是粗暴挂掉。

在 Security.framework 中处理签名

我们已经准备好签名的数据和原文了,万事俱备,只欠密钥。

处理密钥

通过 keyID,在预先设定的 JWT Host 中我们应该可以找到以 JWK 形式表示的密钥。我们计划使用 Security.framework 来处理密钥和签名验证,首先要做的就是遵守框架和 JWA 的规范,通过 JWK 的密钥生成 Security 框架喜欢的 SecKey 值。

在其他大部分情况下,我们可能会从一个证书 (certificate,不管是从网络下载的 PEM 还是存储在本地的证书文件) 里获取公钥。像是处理 HTTPS challenge 或者 SSL Pinning 的时候,大部分情况下我们拿到的是完整的证书数据,通过 SecCertificateCreateWithData 使用 DER 编码的数据创建证书并获取公钥:

guard let cert = SecCertificateCreateWithData(nil, data as CFData) else {
    // 错误处理
    return
}

let policy = SecPolicyCreateBasicX509()
var trust: SecTrust? = nil
SecTrustCreateWithCertificates(cert, policy, &trust)
guard let t = trust, let key: SecKey = SecTrustCopyPublicKey(t) else {
    // 错误处理
    return
}
print(key)

但是,在 JWK 的场合,我们是没有 X.509 证书的。JWK 直接将密钥类型和参数编码在 JSON 中,我们当然可以按照 DER 编码规则将这些信息编码回一个符合 X.509 要求的证书,然后使用上面的方法再从中获取证书。不过这显然是画蛇添足,我们完全可以直接通过这些参数,使用特定格式的数据来直接生成 SecKey

有可能有同学会迷惑于“公钥”和“证书”这两个概念。一个证书,除了包含有公钥以外,还包含有像是证书发行者,证书目的,以及其他一些元数据的信息。因此,我们可以从一个证书中,提取它所存储的公钥。

另外,证书本身一般会由另外一个私钥进行签名,并由颁发机构或者受信任的机构进行验证保证其真实性。

使用 SecKeyCreateWithData 就可以直接通过公钥参数来生成了:

func SecKeyCreateWithData(_ keyData: CFData, 
                          _ attributes: CFDictionary, 
                          _ error: UnsafeMutablePointer<Unmanaged<CFError>?>?) -> SecKey?

第二个参数 attributes 需要的是密钥种类 (RSA 还是 EC),密钥类型 (公钥还是私钥),密钥尺寸 (数据 bit 数) 等信息,比较简单。

关于所需要的数据格式,根据密钥种类不同,而有所区别。在这个风马牛不相及的页面 以及 SecKey 源码 的注释中有所提及:

The method returns data in the PKCS #1 format for an RSA key. For an elliptic curve public key,
the format follows the ANSI X9.63 standard using a byte string of 04 || X || Y. … All of
these representations use constant size integers, including leading zeros as needed.

The requested data format depend on the type of key (kSecAttrKeyType) being created:

kSecAttrKeyTypeRSA               PKCS#1 format, public key can be also in x509 public key format
kSecAttrKeyTypeECSECPrimeRandom  ANSI X9.63 format (04 || X || Y [ || K])

JWA – RSA

简单说,RSA 的公钥需要遵守 PKCS#1,使用 X.509 编码即可。所以对于 RSA 的 JWK 里的 ne,我们用 DER 按照 X.509 编码成序列后,就可以扔给 Security 框架了:

extension JWK {
    struct RSA {
        let modulus: String
        let exponent: String
    }
}

let jwk: JWK.RSA = ...
guard let n = jwk.modulus.base64URLDecoded else { ... }
guard let e = jwk.exponent.base64URLDecoded else { ... }

var modulusBytes = [UInt8](n)            
if let firstByte = modulusBytes.first, firstByte >= 0x80 {
    modulusBytes.insert(0x00, at: 0)
}
let exponentBytes = [UInt8](e)

let modulusEncoded = modulusBytes.encode(as: .integer)
let exponentEncoded = exponentBytes.encode(as: .integer)
let sequenceEncoded = (modulusEncoded + exponentEncoded).encode(as: .sequence)

let data = Data(bytes: sequenceEncoded)

关于 DER 编码部分的代码,可以在这里找到。对于 modulusBytes,首位大于等于 0x80 时需要追加 0x00 的原因,也已经在第一篇中提及。如果你不知道我在说什么,建议回头仔细再看一下前两篇的内容。

使用上面的 data 就可以获取 RSA 的公钥了:

let sizeInBits = data.count * MemoryLayout<UInt8>.size
let attributes: [CFString: Any] = [
    kSecAttrKeyType: kSecAttrKeyTypeRSA,
    kSecAttrKeyClass: kSecAttrKeyClassPublic,
    kSecAttrKeySizeInBits: NSNumber(value: sizeInBits)
]
var error: Unmanaged<CFError>?
guard let key = SecKeyCreateWithData(data as CFData, attributes as CFDictionary, &error) else {
    // 错误处理
}
print(key)

// 一切正常的话,打印类似这样:
// <SecKeyRef algorithm id: 1, key type: RSAPublicKey, version: 4, 
// block size: 1024 bits, exponent: {hex: 10001, decimal: 65537}, 
// modulus: DD95AB518D18E8828DD6A238061C51D82EE81D516018F624..., 
// addr: 0x6000027ffb00>

JWA – ECSDA

按照说明,对于 EC 公钥,期望的数据是符合 X9.63 中未压缩的椭圆曲线点座标:04 || X || Y。不过,虽然在文档说明里提及:

All of these representations use constant size integers, including leading zeros as needed.

但事实是 SecKeyCreateWithData 并不喜欢在首位追加 0x00 的做法。这里的 XY 必须是满足椭圆曲线对应要求的密钥位数的整数值,如果在首位大于等于 0x80 的值前面追加 0x00,反而会导致无法创建 SecKey。所以,在组织数据时,不仅不需要添加 0x00,我们反而最好检查一下获取的 JWK,如果首位有不必要的 0x00 的话,应该将其去除:

extension JWK {
    struct RSA {
        let x: String
        let y: String
    }
}

let jwk: JWK.RSA = ...
guard let decodedXData = jwk.x.base64URLDecoded else { ... }
guard let decodedYData = jwk.y.base64URLDecoded else { ... }

let xBytes: [UInt8]
if decodedXData.count == curve.coordinateOctetLength {
    xBytes = [UInt8](decodedXData)
} else {
    xBytes = [UInt8](decodedXData).dropFirst { $0 == 0x00 }
}
            
let yBytes: [UInt8]
if decodedYData.count == curve.coordinateOctetLength {
    yBytes = [UInt8](decodedYData)
} else {
    yBytes = [UInt8](decodedYData).dropFirst { $0 == 0x00 }
}

let uncompressedIndicator: [UInt8] = [0x04]
let data = Data(bytes: uncompressedIndicator + xBytes + yBytes)

创建公钥时和 RSA 类似:

let sizeInBits = data.count * MemoryLayout<UInt8>.size
let attributes: [CFString: Any] = [
    kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
    kSecAttrKeyClass: kSecAttrKeyClassPublic,
    kSecAttrKeySizeInBits: NSNumber(value: sizeInBits)
]
var error: Unmanaged<CFError>?
guard let key = SecKeyCreateWithData(data as CFData, attributes as CFDictionary, &error) else {
    // 错误处理
}
print(key)

// 一切正常的话,打印类似这样:
// <SecKeyRef curve type: kSecECCurveSecp256r1, algorithm id: 3, 
// key type: ECPublicKey, version: 4, block size: 256 bits, 
// y: 3D4F8B27B29E5C77FCF877367245F3D75C2FBA806C54A0A0C05807E1B536E68A, 
// x: FFB00CF903B79BB0F6C049208A59C448049BE0A2A1AF4692C486085CBD9057EF, 
// addr: 0x7fcafd80ced0>

验证签名

Security 框架中为使用公钥进行签名验证准备了一个方法:SecKeyVerifySignature

func SecKeyVerifySignature(_ key: SecKey, 
                         _ algorithm: SecKeyAlgorithm, 
                         _ signedData: CFData, 
                         _ signature: CFData, 
                         _ error: UnsafeMutablePointer<Unmanaged<CFError>?>?) -> Bool

key 我们已经拿到了,signedData 就是之前我们准备的 {Header}.{Payload} 的字符串的数据表示 (也就是 plainText.data(using: .ascii)。注意,这里的 plainText 不是一个 Base64Url 字符串,JWS 签名所针对的就是这个拼凑后的字符串的散列值)。我们需要为不同的签名算法指定合适的 SecKeyAlgorithm,通过访问 SecKeyAlgorithm 的静态成员,就可以获取 Security 框架预先定义的算法了。比如常用的:

let ecdsa256 = SecKeyAlgorithm.ecdsaSignatureMessageX962SHA256
let rsa256 = SecKeyAlgorithm.rsaSignatureDigestPKCS1v15SHA256

你可以在 Apple 的文档里找到所有支持的算法的定义,但是不幸的是,这些算法都只有名字,没有具体说明,也没有使用范例。想要具体知道某个算法的用法,可能需要在源码级别去参考注释。为了方便,对于签名验证相关的一些常用算法,我列了一个表说明对应关系:

算法 输入数据 (signedData) 签名 (signature) 对应 JWT 算法
rsaSignatureDigestPKCS1v15SHA{x} 原数据的 SHA-x 摘要 PKCS#1 v1.5 padding 的签名 RS{x}
rsaSignatureMessagePKCS1v15SHA{x} 原数据本身,框架负责计算 SHA-x 摘要 PKCS#1 v1.5 padding 的签名 RS{x}
rsaSignatureDigestPSSSHA{x} 原数据的 SHA-x 摘要 使用 PSS 的 PKCS#1 v2.1 签名 PS{x}
rsaSignatureMessagePSSSHA{x} 原数据本身,框架负责计算 SHA-x 摘要 使用 PSS 的 PKCS#1 v2.1 签名 PS{x}
ecdsaSignatureDigestX962SHA{x} 原数据的 SHA-x 摘要 DER x9.62 编码的 r 和 s ES{x}
ecdsaSignatureMessageX962SHA{x} 原数据本身,框架负责计算 SHA-x 摘要 DER x9.62 编码的 r 和 s ES{x}

不难看出,这些签名算法基本就是 {算法类型} + {数据处理方式} + {签名格式} 的组合。另外还有一些更为泛用的签名算法,像是 .ecdsaSignatureRFC4754 或者 .rsaSignatureRaw,你需要按照源码注释给入合适的输入,不过一般来说还是直接使用预设的散列的 __Message__SHA___ 这类算法最为方便。

SecKeyAlgorithm 中除了签名算法,也包括了使用 RSA 和 EC 进行加密的相关算法。整体上和签名算法的命名方式类似,有兴趣和需要相关内容的同学可以自行研究。

对于 JWT 来说,RS 算法的签名已经是 PKCS#1 v1.5 padding 的了,所以直接将 signedDatasignature 配合使用 rsaSignatureMessagePKCS1v15SHA{x} 就可以完成验证。

var error: Unmanaged<CFError>?
let result = SecKeyVerifySignature(
    key, 
    .rsaSignatureMessagePKCS1v15SHA256, 
    signedData as CFData, 
    signature as CFData, &error)

对于 ES 的 JWT 来说,事情要麻烦一些。我们收到的 JWT 里的签名只是 {r, s} 的简单连接,所以需要预先进行处理。按照 X9.62 中对 signature 的编码定义:

ECDSA-Sig-Value ::= SEQUENCE {
    r INTEGER,
    s INTEGER }

因此,在调用 SecKeyVerifySignature 之前,先处理签名:

let count = signature.count
guard count != 0 && count % 2 == 0 else {
    // 错误,签名应该是两个等长的整数
}
var rBytes = [UInt8](signature[..<(count / 2)])
var sBytes = [UInt8](signature[(count / 2)...])

// 处理首位,我们已经做过很多次了。
if rBytes.first! >= UInt8(0x80) {
    rBytes.insert(0x00, at: 0)
}

if sBytes.first! >= UInt8(0x80) {
    sBytes.insert(0x00, at: 0)
}

// 完成签名的 DER 编码
let processedSignature = Data(bytes: 
    (rBytes.encode(as: .integer) + sBytes.encode(as: .integer))
    .encode(as: .sequence))

var error: Unmanaged<CFError>?
let result = SecKeyVerifySignature(
    key, 
    .ecdsaSignatureMessageX962SHA256, 
    signedData as CFData, 
    processedSignature as CFData, &error)

上面 RSA 和 ECDSA 的验证,都假设了使用 SHA-256 作为散列算法。如果你采用的是其他的散列算法,记得替换。

验证 Payload 内容

签名正确完成验证之后,我们就可以对 JWT Payload 里的内容进行验证了:包括但不限于 “iss”,”sub”,”exp”,”iat” 这些保留值是否正确。当签名和内容都验证无误后,就可以安心使用这个 JWT 了。

一些问题

至此,我们从最初的 JWT 定义开始,引伸出 JWA,JWK 等一系列 JOSE 概念。然后我们研究了互联网安全领域的通用编码方式和几种最常见的密钥的构成。最后,我们使用这些知识在 Security 框架的帮助下,完成了 JWT 的签名验证的整个流程。

事后看上去没有太大难度,但是由于涉及到的名词概念很多,相关标准错综复杂,因此初上手想要把全盘都弄明白,还是会有一定困难。希望这系列文章能够帮助你在起步阶段就建立相对清晰的知识体系,这样在阅读其他的相关信息时,可以对新的知识进行更好的分类整理。

最后,是一些我自己在学习和实践中的考虑。在此一并列出,以供参考。如果您有什么指正和补充,也欢迎留言评论。

为什么不用已有的相关开源框架

现存的和这个主题相关的 iOS 或者 Swift 框架有一些,比如 JOSESwiftJSONWebToken.swiftSwift-JWTvaper/jwt 等等。来回比较考察,它们现在 (2018 年 12 月) 或多或少存在下面的不足:

  • 没有一个从 JWK 开始到 JWT 的完整方案。JWT 相关的框架基本都是从本地证书获取公钥进行验证,而我需要从 JWK 获取证书
  • 支持 JWK 的框架只实现了部分算法,比如只有 RSA,没有 ECDSA 支持。
  • 一些框架依赖关系太复杂,而且大部分实现是面向 Swift Server Side,而非 iOS 的。

LINE SDK 中,我们需要,且只需要在 iOS 上利用 Security 框架完成验证。同时 Server 可能会变更配置,所以我们需要同时支持 RSA 和 ECDSA (当前默认使用 ECDSA)。另外,本身作为一个提供给第三方开发者的 SDK,我们不允许引入不可靠的复杂依赖关系 (最理想的情况是零依赖,也就是 LINE SDK 的现状)。基于这些原因,我没有使用现有的开源代码,而是自己从头进行实现。

为什么不把你做的相关内容整理开源

在 LINE SDK 中的方案是不完备的,它是 JOSE 中满足我们的 JWT 解析和验证需求的最小子集,因此没有很高的泛用性,不适合作为单独项目开源。不过因为 LINE SDK 整个项目是开源的,JOSE 部分的代码其实也都是公开且相对独立的。如果你感兴趣,可以在 LINE SDK 的 Crypto 文件夹下找到所有相关代码。

为什么要用非对称算法,各算法之间有什么优劣

不少 JWT 使用 HS 的算法 (HMAC)。和 RSA 或 ECDSA 不同,HMAC 是对称加密算法。对称算法加密和解密比较简单,因为密钥相同,所以比较适合用在 Server to Server 这种双方可信的场合。如果在客户端上使用对称算法,那就需要将这个密钥存放在客户端上,这显然是不可接受的。对于 Client – Server 的通讯,非对称算法应该是毋庸置疑的选择。

相比与 RSA,ECDSA 可以使用更短的密钥实现和数倍长于自己的 RSA 相同的安全性能。

For example, at a security level of 80 bits (meaning an attacker requires a maximum of about
2^80 operations to find the private key) the size of an ECDSA public key would be 160 bits, whereas the size of a DSA public key is at least 1024 bits.

由于 ECDSA 是专用的 DSA 算法,只能用于签名,而不能用作加密和密钥交换,所以它比 RSA 要快很多。另外,更小的密钥也带来了更小的计算量。这些特性对于减少 Server 负担非常重要。关于 ECDSA 的优势和它相对于 RSA 的对比,可以参考 Cloudflare 的这篇文章

签名的安全性

JWT 签名的伪造一直是一个困扰人的问题。因为 JWT 的 Header 和 Payload 内容一旦确定的话,它的签名也就确定了 (虽然 ECDSA 会产生随机数使签名每次都不同,但是这些签名都可以通过验证)。这带来一个问题,攻击者可以通过截取以前的有效的 JWT,然后把它作为新的响应发给用户。这类 JWT 依然可以正确通过签名验证。

因此,我们必须每次生成不同的 JWT,来防止这种替换攻击。最简单的方式就是在内存中存储随机值,发送 JWT 请求时附带这个随机值,然后 Server 将这个随机值嵌入在返回的 JWT 的 Payload 中。Client 收到后,再与内存中保存的值进行比对。这样保证了每次返回的 JWT 都不相同,让签名验证更加安全。

OpenSSL 版本的问题

macOS 上自带的 OpenSSL 版本一般比较旧,而大部分 Linux 系统的 OpenSSL 更新一些。不同版本的 OpenSSL (或者其他的常用安全框架) 实现细节上会有差异,比如有些版本会在负数首位补 0x00 等。在测试时,最好让 Server 的小伙伴确认一下使用的 OpenSSL 版本,这样能在验证和使用密钥上避免一些不必要的麻烦。(请不要问我细节!都是泪)

JWT 可以用来做什么,应该用来做什么

JWT 最常见的使用场景有两个:

  • 授权:用户登录后,在后续的请求中带上一个有效的 JWT,其中包含该用户可以访问的路径或权限等。服务器验证 JWT 有效性后对访问进行授权。相比于传统像是 OAuth 的 token 来说,服务器并不需要存储这些 token,可以实现无状态的授权,因此它的开销较小,也更容易实现和理解。另外,由于 JWT 不需要依赖 Cookie 的特性,跨站或者跨服务依然可能使用,这让单点登录非常简单。
  • 信息交换:LINE SDK 中对用户信息进行签名和验证,就属于信息交换的范畴。依赖 JWT 的签名特性,接收方可以确保 JWT 中的内容没有被篡改,是一种安全的信息交换方式。

最近有非常多的关于反对使用 JWT 进行授权的声音,比如这篇文章这篇文章。JWT 作为授权 token 来使用,最大的问题在于无法过期或者作废,另外,一些严格遵守标准的实现,反而可能引入严重的安全问题

不过对于第二种用法,也就是信息交换来说,JWT 所提供的便捷和安全性是无人质疑的。

我也想读读看相关标准

如你所愿,我整理了一下涉及到的标准。祝武运昌隆!

关于编码和算法
关于 JOSE
杂项

验证和速查工具汇总

你的这篇文章或者代码好像有问题!

我是初学者,文章中的纰漏请不吝赐教指出!

关于代码方面的不足,LINE SDK 欢迎各种 PR。但是如果您发现的问题涉及安全漏洞,或者会导致比较严重后果的话,还请先不要公开公布。如果能按照这里的说明给我们发送邮件联系的话,实在感激不尽。

December 07, 2018 at 09:28AM via OneV’s Den https://ift.tt/2PogrDO

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share