Blog

架构设计随想-Unix哲学和Node.js

前几天,我在TxJS上发表了一篇演讲,其中提到Unix哲学是Node.js模式,观点和文化的重要组成部分。像往常一样,我可以在提供谈话视频之前在线提供我的幻灯片

出于某种原因,这篇简短的提及“Unix哲学”引发了一些人的愤怒。由于我只有25分钟,并且每张幻灯片都可能完全是它自己的谈话,所以我对阐述很轻松。有可能,视频不会添加太多的背景。但目标是激起对话,所以如果它引起批评,也许这是成功的。毕竟,不知情的拖钓只是教育的邀请,所以我想我会解释。

Eric S. Raymond在他的书“Unix的编程艺术”中收集了一些关于Unix哲学的最佳解释。他详细阐述了17个具体的原则,但我最喜欢的Unix哲学的解释是Doug McIlroy在“四分之一世纪的Unix”中引用的简洁解释:

这是Unix哲学:

编写完成一件事并做得好的程序。

编写程序以协同工作。

编写程序来处理文本流,因为这是一个通用接口。

McIlroy的原始4点配方略长一点:

(i)让每个项目做好一件事。要做一份新工作,重新构建而不是通过添加新功能使旧程序复杂化。

(ii)期望每个节目的输出成为另一个尚未知的节目的输入。不要使用无关信息使输出混乱。避免严格的柱状或二进制输入格式。不要坚持互动输入。

(iii)设计和构建软件,甚至是操作系统,以便在几周内尽早尝试。不要犹豫扔掉笨拙的部分并重建它们。

(iv)使用工具优先于不熟练的帮助来减轻编程任务,即使你不得不绕道去构建工具并期望在你完成它们之后把它们中的一些扔掉。

从事X Window系统工作的Mike Gancarz总结了以下9点的Unix哲学:

  1. 小是美丽的。
  2. 让每个程序做好一件事。
  3. 尽快建立原型。
  4. 选择便携性超过效率。
  5. 将数据存储在平面文本文件中。
  6. 使用软件杠杆对您有利。
  7. 使用shell脚本来提高杠杆率和可移植性。
  8. 避免强制用户界面。
  9. 使每个程序成为过滤器。

最后一点确实引起了瑞恩达尔经常说的话,“每个节目都是代理人。”前三个基本上是詹姆斯·哈利迪的生活规则。

很多时候,人们会对Unix哲学的错误方面感到困惑,并错过森林中的树木。Unix哲学不是关于特定的实现,也不是任何Unix操作系统或程序必需的。它不是关于文件描述符,管道,套接字或信号。这些抱怨就像是说除非他们说Pali,否则某人不是佛教徒。

Unix哲学是软件开发的前景,而不是软件中的任何特定技术开发。这是一种理想的接触,也许具有讽刺意味的是,它是一种理想,它指示我们偶尔避开理想主义以支持实用性。

在Node中,人们共享和交互的基本构建块不是命令行上的二进制文件,而是加载的模块require()。通用接口一个文本流,但它是一个JavaScript Stream对象,而不是一个stdio管道。(标准输入输出管道是由JavaScript流代表,当然,因为那是我们的通用接口,所以还有什么我们使用?)

那么,就Node.js而言,这就是我如何表达Unix哲学。唉,我不是麦克罗伊,我没有时间或技巧来写这个更短。

编写能很好地完成一件事的模块。编写一个新模块而不是使旧模块复杂化。

编写鼓励构图而不是扩展的模块。

编写处理数据流的模块,因为这是通用接口。

编写与输入源或输出目标无关的模块。

编写解决您知道的问题的模块,这样您就可以了解那些您不知道的问题。

写小的模块。快速迭代。无情地重构。勇敢地改写。

只需进行一些合规性测试,即可快速编写模块以满足您的需求。避免广泛的规格。为您修复的每个错误添加测试。

编写模块以供发布,即使您只是私下使用它们。您将来会欣赏文档。


工作胜于完美。

专注比功能更好。

兼容性优于纯度。

简单优于任何事物。

Unix哲学是一种实用主义的意识形态。它是关于平衡编写优秀软件和编写任何软件的双重需求。这是一套实用的建议,用于交易适度增加的开发成本,从而大幅降低维护成本。

在现实世界中,我们面临着在编写程序和调试程序时成为人类的完全不公平的约束,并且这些成本都不能减少到零。这种意识形态是上下文的,可以应用于堆栈的所有级别。这是一个公开承认我们实际上不够聪明,不知道如何编写我们第一次需要的软件,因为我们通常只有在完成解决后才能完全理解我们的问题。

这些规则都不是神圣不可侵犯的! 事实上,在许多情况下,他们可能存在分歧,甚至完全矛盾。但是,如果我们通过简单的通用接口保持我们的编程单元很小,我们可以发现将零碎结构作为质量棘轮,在我们进行时更换笨拙的部件。

关于Unix哲学的任何内容都没有明确涉及软件共享的文化。然而,它来自软件社区并不是一个谜,我们会详细讨论使我们的程序正常免费的最佳方法。根据这些原则开发的软件更易于共享,重用,重新利用和维护。


翻译自 :node.js 维护者

http://blog.izs.me/post/48281998870/unix-philosophy-and-nodejs

架构横向对比-精读前后端渲染之争

这是我在前端精度专栏发的一篇文章。精读的文章是:Here’s why Client-side Rendering Won

1 引言


 

我为什么要选这篇文章呢?

十年前,几乎所有网站都使用 ASP、Java、PHP 这类做后端渲染,但后来随着 jQuery、Angular、React、Vue 等 JS 框架的崛起,开始转向了前端渲染。从 2014 年起又开始流行了同构渲染,号称是未来,集成了前后端渲染的优点,但转眼间三年过去了,很多当时壮心满满的框架(rendrLazo)从先驱变成了先烈。同构到底是不是未来?自己的项目该如何选型?我想不应该只停留在追求热门和拘泥于固定模式上,忽略了前后端渲染之“争”的“核心点”,关注如何提升“用户体验”。

原文分析了前端渲染的优势,并没有进行深入探讨。我想以它为切入口来深入探讨一下。

明确三个概念:「后端渲染」指传统的 ASP、Java 或 PHP 的渲染机制;「前端渲染」指使用 JS 来渲染页面大部分内容,代表是现在流行的 SPA 单页面应用;「同构渲染」指前后端共用 JS,首次渲染时使用 Node.js 来直出 HTML。一般来说同构渲染是介于前后端中的共有部分。

2 内容概要


前端渲染的优势

  • 局部刷新。无需每次都进行完整页面请求
  • 懒加载。如在页面初始时只加载可视区域内的数据,滚动后rp加载其它数据,可以通过 react-lazyload 实现
  • 富交互。使用 JS 实现各种酷炫效果
  • 节约服务器成本。省电省钱,JS 支持 CDN 部署,且部署极其简单,只需要服务器支持静态文件即可
  • 天生的关注分离设计。服务器来访问数据库提供接口,JS 只关注数据获取和展现
  • JS 一次学习,到处使用。可以用来开发 Web、Serve、Mobile、Desktop 类型的应用

 

后端渲染的优势

  • 服务端渲染不需要先下载一堆 js 和 css 后才能看到页面(首屏性能)
  • SEO
  • 服务端渲染不用关心浏览器兼容性问题(随着浏览器发展,这个优点逐渐消失)
  • 对于电量不给力的手机或平板,减少在客户端的电量消耗很重要

以上服务端优势其实只有首屏性能和 SEO 两点比较突出。但现在这两点也慢慢变得微不足道了。React 这类支持同构的框架已经能解决这个问题,尤其是 Next.js 让同构开发变得非常容易。还有静态站点的渲染,但这类应用本身复杂度低,很多前端框架已经能完全囊括。

3 精读

本次提出独到观点的同学有:@javie007 @杨森 @流形 @camsong @Turbe Xue @淡苍 @留影@FrankFang @alcat2008 @xile611 @twobin @黄子毅 精读由此归纳。

大家对前端和后端渲染的现状基本达成共识。即前端渲染是未来趋势,但前端渲染遇到了首屏性能和SEO的问题。对于同构争议最多,在此我归纳一下。

前端渲染遇到的问题

前端渲染主要面临的问题有两个 SEO首屏性能

SEO 很好理解。由于传统的搜索引擎只会从 HTML 中抓取数据,导致前端渲染的页面无法被抓取。前端渲染常使用的 SPA 会把所有 JS 整体打包,无法忽视的问题就是文件太大,导致渲染前等待很长时间。特别是网速差的时候,让用户等待白屏结束并非一个很好的体验。

同构的优点

同构恰恰就是为了解决前端渲染遇到的问题才产生的,至 2014 年底伴随着 React 的崛起而被认为是前端框架应具备的一大杀器,以至于当时很多人为了用此特性而放弃 Angular 1 而转向 React。然而近3年过去了,很多产品逐渐从全栈同构的理想化逐渐转到首屏或部分同构。让我们再一次思考同构的优点真是优点吗?

  1. 有助于 SEO

首先确定你的应用是否都要做 SEO,如果是一个后台应用,那么只要首页做一些静态内容宣导就可以了。如果是内容型的网站,那么可以考虑专门做一些页面给搜索引擎
时到今日,谷歌已经能够可以在爬虫中执行 JS 像浏览器一样理解网页内容,只需要往常一样使用 JS 和 CSS 即可。并且尽量使用新规范,使用 pushstate 来替代以前的 hashstate。不同的搜索引擎的爬虫还不一样,要做一些配置的工作,而且可能要经常关注数据,有波动那么可能就需要更新。第二是该做 sitemap 的还得做。相信未来即使是纯前端渲染的页面,爬虫也能很好的解析。

  1. 共用前端代码,节省开发时间

其实同构并没有节省前端的开发量,只是把一部分前端代码拿到服务端执行。而且为了同构还要处处兼容 Node.js 不同的执行环境。有额外成本,这也是后面会具体谈到的。

  1. 提高首屏性能

由于 SPA 打包生成的 JS 往往都比较大,会导致页面加载后花费很长的时间来解析,也就造成了白屏问题。服务端渲染可以预先使到数据并渲染成最终 HTML 直接展示,理想情况下能避免白屏问题。在我参考过的一些产品中,很多页面需要获取十几个接口的数据,单是数据获取的时候都会花费数秒钟,这样全部使用同构反而会变慢。

同构并没有想像中那么美

  1. 性能

把原来放在几百万浏览器端的工作拿过来给你几台服务器做,这还是花挺多计算力的。尤其是涉及到图表类需要大量计算的场景。这方面调优,可以参考 walmart的调优策略

个性化的缓存是遇到的另外一个问题。可以把每个用户个性化信息缓存到浏览器,这是一个天生的分布式缓存系统。我们有个数据类应用通过在浏览器合理设置缓存,双十一当天节省了 70% 的请求量。试想如果这些缓存全部放到服务器存储,需要的存储空间和计算都是很非常大。

  1. 不容忽视的服务器端和浏览器环境差异

前端代码在编写时并没有过多的考虑后端渲染的情景,因此各种 BOM 对象和 DOM API 都是拿来即用。这从客观层面也增加了同构渲染的难度。我们主要遇到了以下几个问题:

  • document 等对象找不到的问题
  • DOM 计算报错的问题
  • 前端渲染和服务端渲染内容不一致的问题

由于前端代码使用的 window 在 node 环境是不存在的,所以要 mock window,其中最重要的是 cookie,userAgent,location。但是由于每个用户访问时是不一样的 window,那么就意味着你得每次都更新 window
而服务端由于 js require 的 cache 机制,造成前端代码除了具体渲染部分都只会加载一遍。这时候 window就得不到更新了。所以要引入一个合适的更新机制,比如把读取改成每次用的时候再读取。

export const isSsr = () => (
  !(typeof window !== 'undefined' && window.document && window.document.createElement && window.setTimeout)
);

原因是很多 DOM 计算在 SSR 的时候是无法进行的,涉及到 DOM 计算的的内容不可能做到 SSR 和 CSR 完全一致,这种不一致可能会带来页面的闪动。

  1. 内存溢出

前端代码由于浏览器环境刷新一遍内存重置的天然优势,对内存溢出的风险并没有考虑充分。
比如在 React 的 componentWillMount 里做绑定事件就会发生内存溢出,因为 React 的设计是后端渲染只会运行 componentDidMount 之前的操作,而不会运行 componentWillUnmount 方法(一般解绑事件在这里)。

  1. 异步操作

前端可以做非常复杂的请求合并和延迟处理,但为了同构,所有这些请求都在预先拿到结果才会渲染。而往往这些请求是有很多依赖条件的,很难调和。纯 React 的方式会把这些数据以埋点的方式打到页面上,前端不再发请求,但仍然再渲染一遍来比对数据。造成的结果是流程复杂,大规模使用成本高。幸运的是 Next.js 解决了这一些,后面会谈到。

  1. simple store(redux)

这个 store 是必须以字符串形式塞到前端,所以复杂类型是无法转义成字符串的,比如function。

总的来说,同构渲染实施难度大,不够优雅,无论在前端还是服务端,都需要额外改造。

首屏优化

再回到前端渲染遇到首屏渲染问题,除了同构就没有其它解法了吗?总结以下可以通过以下三步解决

  1. 分拆打包

现在流行的路由库如 react-router 对分拆打包都有很好的支持。可以按照页面对包进行分拆,并在页面切换时加上一些 loading 和 transition 效果。

  1. 交互优化

首次渲染的问题可以用更好的交互来解决,先看下 linkedin 的渲染

有什么感受,非常自然,打开渲染并没有白屏,有两段加载动画,第一段像是加载资源,第二段是一个加载占位器,过去我们会用 loading 效果,但过渡性不好。近年流行 Skeleton Screen 效果。其实就是在白屏无法避免的时候,为了解决等待加载过程中白屏或者界面闪烁造成的割裂感带来的解决方案。

  1. 部分同构

部分同构可以降低成功同时利用同构的优点,如把核心的部分如菜单通过同构的方式优先渲染出来。我们现在的做法就是使用同构把菜单和页面骨架渲染出来。给用户提示信息,减少无端的等待时间。

相信有了以上三步之后,首屏问题已经能有很大改观。相对来说体验提升和同构不分伯仲,而且相对来说对原来架构破坏性小,入侵性小。是我比较推崇的方案。

3 总结


我们赞成客户端渲染是未来的主要方向,服务端则会专注于在数据和业务处理上的优势。但由于日趋复杂的软硬件环境和用户体验更高的追求,也不能只拘泥于完全的客户端渲染。同构渲染看似美好,但以目前的发展程度来看,在大型项目中还不具有足够的应用价值,但不妨碍部分使用来优化首屏性能。做同构之前 ,一定要考虑到浏览器和服务器的环境差异,站在更高层面考虑。

附:Next.js 体验

Next.js 是时下非常流行的基于 React 的同构开发框架。作者之一就是大名鼎鼎的 Socket.io 的作者 Guillermo Rauch。它有以下几个亮点特别吸引我:

  1. 巧妙地用标准化的解决了请求的问题。同构和页面开发类似,异步是个大难题,异步中难点又在接口请求。Next.js 给组件新增了 getInitialProps 方法来专门处理初始化请求,再也不用手动往页面上塞 DATA 和调用 ReactDOMServer.renderToString
  2. 使用 styled-jsx 解决了 css-in-js 的问题。这种方案虽然不像 styled-component 那样强大,但足够简单,可以说是最小的成本解决了问题
  3. Fast by default。页面默认拆分文件方式打包,支持Prefetch页面预加载

全家桶式的的解决方案。简洁清晰的目录结构,这一点 Redux 等框架真应该学一学。不过全家桶的方案比较适合全新项目使用,旧项目使用要评估好成本

讨论地址是:前后端渲染之争 · Issue #5 · dt-fe/weekly


一些读者意见:

我还没有看【Here’s why Client-side Rendering Won】这篇文章,但是我对你的精读归纳并不赞同:

  1. 这里只是列出了一些前后端同构遇到的或大或小的问题,并非不能通过技术方案解决;
  2. 替代同构的方案里,拆分打包是 ‘自古以来’ 都在做的事情,但是即使是最小的包,也需要加载时间;
  3. 替代方案里的实现,并不是所有的网站都可以使用的,比如新闻门户网站,让用户等待一个loading动画?我觉得不符合大部分用户的使用习惯。
  4. SEO问题如何解决, 此处并没有给出方案,我们总不能把希望寄托在百度升级爬虫这种事情上吧?

作为一个读者,我希望读到的不是问题的对错,而是对于技术的改进方案。


来自:https://github.com/camsong/blog/issues/8

架构横向对比-多页应用与单页应用

多页应用

每一次页面跳转的时候,后台服务器都会给返回一个新的html文档,这种类型的网站也就是多页网站,也叫做多页应用。

多页应用
为什么多页应用的首屏时间快?

首屏时间叫做页面首个屏幕的内容展现的时间,当我们访问页面的时候,服务器返回一个html,页面就会展示出来,这个过程只经历了一个HTTP请求,所以页面展示的速度非常快。

为什么搜索引擎优化效果好(SEO)?

搜索引擎在做网页排名的时候,要根据网页内容才能给网页权重,来进行网页的排名。搜索引擎是可以识别html内容的,而我们每个页面所有的内容都放在Html中,所以这种多页应用,seo排名效果好。

但是它也有缺点,就是切换慢

因为每次跳转都需要发出一个http请求,如果网络比较慢,在页面之间来回跳转时,就会发现明显的卡顿。

单页应用

第一次进入页面的时候会请求一个html文件,刷新清除一下。切换到其他组件,此时路径也相应变化,但是并没有新的html文件请求,页面内容也变化了。
原理是:JS会感知到url的变化,通过这一点,可以用js动态的将当前页面的内容清除掉,然后将下一个页面的内容挂载到当前页面上,这个时候的路由不是后端来做了,而是前端来做,判断页面到底是显示哪个组件,清除不需要的,显示需要的组件。这种过程就是单页应用,每次跳转的时候不需要再请求html文件了。

我是单页应用
为什么页面切换快?

页面每次切换跳转时,并不需要做html文件的请求,这样就节约了很多http发送时延,我们在切换页面的时候速度很快。

缺点:首屏时间慢,SEO差

单页应用的首屏时间慢,首屏时需要请求一次html,同时还要发送一次js请求,两次请求回来了,首屏才会展示出来。相对于多页应用,首屏时间慢。
SEO效果差,因为搜索引擎只认识html里的内容,不认识js的内容,而单页应用的内容都是靠js渲染生成出来的,搜索引擎不识别这部分内容,也就不会给一个好的排名,会导致单页应用做出来的网页在百度和谷歌上的排名差。

有这些缺点,为什么还要使用Vue呢?

Vue还提供了一些其它的技术来解决这些缺点,比如说服务器端渲染技术(我是SSR),通过这些技术可以完美解决这些缺点,解决完这些问题,实际上单页面应用对于前端来说是非常完美的页面开发解决方案。

来自:八宝君    https://www.jianshu.com/p/4c9c29967dd6

架构演变-MV*模式的分析与演变

前言


做客户端开发、前端开发对MVC、MVP、MVVM这些名词不了解也应该大致听过,都是为了解决图形界面应用程序复杂性管理问题而产生的应用架构模式。网上很多文章关于这方面的讨论比较杂乱,各种MV模式之间的区别分不清,甚至有些描述都是错误的。本文追根溯源,从最经典的Smalltalk-80 MVC模式开始逐步还原图形界面之下最真实的MV模式。

GUI程序所面临的问题


图形界面的应用程序提供给用户可视化的操作界面,这个界面提供给数据和信息。用户输入行为(键盘,鼠标等)会执行一些应用逻辑,应用逻辑(application logic)可能会触发一定的业务逻辑(business logic)对应用程序数据的变更,数据的变更自然需要用户界面的同步变更以提供最准确的信息。例如用户对一个电子表格重新排序的操作,应用程序需要响应用户操作,对数据进行排序,然后需要同步到界面上。

在开发应用程序的时候,以求更好的管理应用程序的复杂性,基于**职责分离(Speration of Duties)**的思想都会对应用程序进行分层。在开发图形界面应用程序的时候,会把管理用户界面的层次称为View,应用程序的数据为Model(注意这里的Model指的是Domain Model,这个应用程序对需要解决的问题的数据抽象,不包含应用的状态,可以简单理解为对象)。Model提供数据操作的接口,执行相应的业务逻辑。

有了View和Model的分层,那么问题就来了:View如何同步Model的变更,View和Model之间如何粘合在一起。

带着这个问题开始探索MV模式,会发现这些模式之间的差异可以归纳为对这个问题处理的方式的不同。而几乎所有的MV模式都是经典的Smalltalk-80 MVC的修改版。

Smalltalk-80 MVC


历史背景

早在上个世纪70年代,美国的施乐公司(Xerox)的工程师研发了Smalltalk编程语言,并且开始用它编写图形界面的应用程序。而在Smalltalk-80这个版本的时候,一位叫Trygve Reenskaug的工程师设计了MVC图形应用程序的架构模式,极大地降低了图形应用程序的管理难度。而在四人帮(GoF)的设计模式当中并没有把MVC当做是设计模式,而仅仅是把它看成解决问题的一些类的集合。Smalltalk-80 MVC和GoF描述的MVC是最经典的MVC模式。

MVC的依赖关系

MVC出了把应用程序分成View、Model层,还额外的加了一个Controller层,它的职责为进行Model和View之间的协作(路由、输入预处理等)的应用逻辑(application logic);Model进行处理业务逻辑。Model、View、Controller三个层次的依赖关系如下:

 

Controller和View都依赖Model层,Controller和View可以互相依赖。在一些网上的资料Controller和View之间的依赖关系可能不一样,有些是单向依赖,有些是双向依赖,这个其实关系不大,后面会看到它们的依赖关系都是为了把处理用户行为触发的事件处理权交给Controller。

MVC的调用关系

用户的对View操作以后,View捕获到这个操作,会把处理的权利交移给Controller(Pass calls);Controller会对来自View数据进行预处理、决定调用哪个Model的接口;然后由Model执行相关的业务逻辑;当Model变更了以后,会通过观察者模式(Observer Pattern)通知View;View通过观察者模式收到Model变更的消息以后,会向Model请求最新的数据,然后重新更新界面。如下图:

 

看似没有什么特别的地方,但是由几个需要特别关注的关键点:

  1. View是把控制权交移给Controller,Controller执行应用程序相关的应用逻辑(对来自View数据进行预处理、决定调用哪个Model的接口等等)。
  2. Controller操作Model,Model执行业务逻辑对数据进行处理。但不会直接操作View,可以说它是对View无知的。
  3. View和Model的同步消息是通过观察者模式进行,而同步操作是由View自己请求Model的数据然后对视图进行更新。

需要特别注意的是MVC模式的精髓在于第三点:Model的更新是通过观察者模式告知View的,具体表现形式可以是Pub/Sub或者是触发Events。而网上很多对于MVC的描述都没有强调这一点。通过观察者模式的好处就是:不同的MVC三角关系可能会有共同的Model,一个MVC三角中的Controller操作了Model以后,两个MVC三角的View都会接受到通知,然后更新自己。保持了依赖同一块Model的不同View显示数据的实时性和准确性。我们每天都在用的观察者模式,在几十年前就已经被大神们整合到MVC的架构当中。

这里有一个MVC模式的JavaScript Demo,实现了一个小的TodoList应用程序。经典的Smalltalk-80 MVC不需要任何框架支持就可以实现。目前Web前端框架当中只有一个号称是严格遵循Smalltalk-80 MVC模式的:maria.js

MVC的优缺点

优点:

  1. 把业务逻辑和展示逻辑分离,模块化程度高。且当应用逻辑需要变更的时候,不需要变更业务逻辑和展示逻辑,只需要Controller换成另外一个Controller就行了(Swappable Controller)。
  2. 观察者模式可以做到多视图同时更新。

缺点:

  1. Controller测试困难。因为视图同步操作是由View自己执行,而View只能在有UI的环境下运行。在没有UI环境下对Controller进行单元测试的时候,应用逻辑正确性是无法验证的:Model更新的时候,无法对View的更新操作进行断言。
  2. View无法组件化。View是强依赖特定的Model的,如果需要把这个View抽出来作为一个另外一个应用程序可复用的组件就困难了。因为不同程序的的Domain Model是不一样的

MVC Model 2


在Web服务端开发的时候也会接触到MVC模式,而这种MVC模式不能严格称为MVC模式。经典的MVC模式只是解决客户端图形界面应用程序的问题,而对服务端无效。服务端的MVC模式又自己特定的名字:MVC Model 2,或者叫JSP Model 2,或者直接就是Model 2 。Model 2客户端服务端的交互模式如下:

 

服务端接收到来自客户端的请求,服务端通过路由规则把这个请求交由给特定的Controller进行处理,Controller执行相应的应用逻辑,对Model进行操作,Model执行业务逻辑以后;然后用数据去渲染特定的模版,返回给客户端。

因为HTTP协议是单工协议并且是无状态的,服务器无法直接给客户端推送数据。除非客户端再次发起请求,否则服务器端的Model的变更就无法告知客户端。所以可以看到经典的Smalltalk-80 MVC中Model通过观察者模式告知View更新这一环被无情地打破,不能称为严格的MVC。

Model 2模式最早在1998年应用在JSP应用程序当中,JSP Model 1应用管理的混乱诱发了JSP参考了客户端MVC模式,催生了Model 2。

 

后来这种模式几乎被应用在所有语言的Web开发框架当中。PHP的ThinkPHP,Python的Dijango、Flask,NodeJS的Express,Ruby的RoR,基本都采纳了这种模式。平常所讲的MVC基本是这种服务端的MVC。

MVP


MVP模式有两种:

  1. Passive View
  2. Supervising Controller

而大多数情况下讨论的都是Passive View模式。本文会对PV模式进行较为详细的介绍,而SC模式则简单提及。

历史背景

MVP模式是MVC模式的改良。在上个世纪90年代,IBM旗下的子公司Taligent在用C/C++开发一个叫CommonPoint的图形界面应用系统的时候提出来的。

MVP(Passive View)的依赖关系

MVP模式把MVC模式中的Controller换成了Presenter。MVP层次之间的依赖关系如下:

 

MVP打破了View原来对于Model的依赖,其余的依赖关系和MVC模式一致。

MVP(Passive View)的调用关系

既然View对Model的依赖被打破了,那View如何同步Model的变更?看看MVP的调用关系:

 

和MVC模式一样,用户对View的操作都会从View交移给Presenter。Presenter会执行相应的应用程序逻辑,并且对Model进行相应的操作;而这时候Model执行完业务逻辑以后,也是通过观察者模式把自己变更的消息传递出去,但是是传给Presenter而不是View。Presenter获取到Model变更的消息以后,通过View提供的接口更新界面

关键点:

  1. View不再负责同步的逻辑,而是由Presenter负责。Presenter中既有应用程序逻辑也有同步逻辑。
  2. View需要提供操作界面的接口给Presenter进行调用。(关键)

对比在MVC中,Controller是不能操作View的,View也没有提供相应的接口;而在MVP当中,Presenter可以操作View,View需要提供一组对界面操作的接口给Presenter进行调用;Model仍然通过事件广播自己的变更,但由Presenter监听而不是View。

MVP模式,这里也提供一个用JavaScript编写的例子

MVP(Passive View)的优缺点

优点:

  1. 便于测试。Presenter对View是通过接口进行,在对Presenter进行不依赖UI环境的单元测试的时候。可以通过Mock一个View对象,这个对象只需要实现了View的接口即可。然后依赖注入到Presenter中,单元测试的时候就可以完整的测试Presenter应用逻辑的正确性。这里根据上面的例子给出了Presenter的单元测试样例
  2. View可以进行组件化。在MVP当中,View不依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做到高度可复用的View组件。

缺点:

  1. Presenter中除了应用逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,造成Presenter比较笨重,维护起来会比较困难。

MVP(Supervising Controller)

上面讲的是MVP的Passive View模式,该模式下View非常Passive,它几乎什么都不知道,Presenter让它干什么它就干什么。而Supervising Controller模式中,Presenter会把一部分简单的同步逻辑交给View自己去做,Presenter只负责比较复杂的、高层次的UI操作,所以可以把它看成一个Supervising Controller。

Supervising Controller模式下的依赖和调用关系:

 

因为Supervising Controller用得比较少,对它的讨论就到这里为止。

MVVM


MVVM可以看作是一种特殊的MVP(Passive View)模式,或者说是对MVP模式的一种改良。

历史背景

MVVM模式最早是微软公司提出,并且了大量使用在.NET的WPF和Sliverlight中。2005年微软工程师John Gossman在自己的博客上首次公布了MVVM模式。

ViewModel

MVVM代表的是Model-View-ViewModel,这里需要解释一下什么是ViewModel。ViewModel的含义就是 “Model of View”,视图的模型。它的含义包含了领域模型(Domain Model)和视图的状态(State)。 在图形界面应用程序当中,界面所提供的信息可能不仅仅包含应用程序的领域模型。还可能包含一些领域模型不包含的视图状态,例如电子表格程序上需要显示当前排序的状态是顺序的还是逆序的,而这是Domain Model所不包含的,但也是需要显示的信息。

可以简单把ViewModel理解为页面上所显示内容的数据抽象,和Domain Model不一样,ViewModel更适合用来描述View。

MVVM的依赖

MVVM的依赖关系和MVP依赖,只不过是把P换成了VM。

MVVM的调用关系

MVVM的调用关系和MVP一样。但是,在ViewModel当中会有一个叫Binder,或者是Data-binding engine的东西。以前全部由Presenter负责的View和Model之间数据同步操作交由给Binder处理。你只需要在View的模版语法当中,指令式地声明View上的显示的内容是和Model的哪一块数据绑定的。当ViewModel对进行Model更新的时候,Binder会自动把数据更新到View上去,当用户对View进行操作(例如表单输入),Binder也会自动把数据更新到Model上去。这种方式称为:Two-way data-binding,双向数据绑定。可以简单而不恰当地理解为一个模版引擎,但是会根据数据变更实时渲染。

 

也就是说,MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交由框架所提供的Binder进行负责。只需要告诉Binder,View显示的数据对应的是Model哪一部分即可。

这里有一个JavaScript MVVM的例子,因为MVVM需要Binder引擎。所以例子中使用了一个MVVM的库:Vue.js

MVVM的优缺点

优点:

  1. 提高可维护性。解决了MVP大量的手动View和Model同步的问题,提供双向绑定机制。提高了代码的可维护性。
  2. 简化测试。因为同步逻辑是交由Binder做的,View跟着Model同时变更,所以只需要保证Model的正确性,View就正确。大大减少了对View同步更新的测试。

缺点:

  1. 过于简单的图形界面不适用,或说牛刀杀鸡。
  2. 对于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高。
  3. 数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的。

 

结语


可以看到,从MVC->MVP->MVVM,就像一个打怪升级的过程。后者解决了前者遗留的问题,把前者的缺点优化成了优点。同样的Demo功能,代码从最开始的一堆文件,优化成了最后只需要20几行代码就完成。MV*模式之间的区分还是蛮清晰的,希望可以给对这些模式理解比较模糊的同学带来一些参考和思路。

References

小广告:欢迎follow关注个人github:https://github.com/livoras

来自:戴嘉华   https://github.com/livoras/blog/issues/11

javascript高级-DOM-剖析并实现Virtual DOM

1 前言


本文会在教你怎么用 300~400 行代码实现一个基本的 Virtual DOM 算法,并且尝试尽量把 Virtual DOM 的算法思路阐述清楚。希望在阅读本文后,能让你深入理解 Virtual DOM 算法,给你现有前端的编程提供一些新的思考。

本文所实现的完整代码存放在 Github

2 对前端应用状态管理的思考


假如现在你需要写一个像下面一样的表格的应用程序,这个表格可以根据不同的字段进行升序或者降序的展示。

 

这个应用程序看起来很简单,你可以想出好几种不同的方式来写。最容易想到的可能是,在你的 JavaScript 代码里面存储这样的数据:

var sortKey = "new" // 排序的字段,新增(new)、取消(cancel)、净关注(gain)、累积(cumulate)人数
var sortType = 1 // 升序还是逆序
var data = [{...}, {...}, {..}, ..] // 表格数据

用三个字段分别存储当前排序的字段、排序方向、还有表格数据;然后给表格头部加点击事件:当用户点击特定的字段的时候,根据上面几个字段存储的内容来对内容进行排序,然后用 JS 或者 jQuery 操作 DOM,更新页面的排序状态(表头的那几个箭头表示当前排序状态,也需要更新)和表格内容。

这样做会导致的后果就是,随着应用程序越来越复杂,需要在JS里面维护的字段也越来越多,需要监听事件和在事件回调用更新页面的DOM操作也越来越多,应用程序会变得非常难维护。后来人们使用了 MVC、MVP 的架构模式,希望能从代码组织方式来降低维护这种复杂应用程序的难度。但是 MVC 架构没办法减少你所维护的状态,也没有降低状态更新你需要对页面的更新操作(前端来说就是DOM操作),你需要操作的DOM还是需要操作,只是换了个地方。

既然状态改变了要操作相应的DOM元素,为什么不做一个东西可以让视图和状态进行绑定,状态变更了视图自动变更,就不用手动更新页面了。这就是后来人们想出了 MVVM 模式,只要在模版中声明视图组件是和什么状态进行绑定的,双向绑定引擎就会在状态更新的时候自动更新视图(关于MV*模式的内容,可以看这篇介绍)。

MVVM 可以很好的降低我们维护状态 -> 视图的复杂程度(大大减少代码中的视图更新逻辑)。但是这不是唯一的办法,还有一个非常直观的方法,可以大大降低视图更新的操作:一旦状态发生了变化,就用模版引擎重新渲染整个视图,然后用新的视图更换掉旧的视图。就像上面的表格,当用户点击的时候,还是在JS里面更新状态,但是页面更新就不用手动操作 DOM 了,直接把整个表格用模版引擎重新渲染一遍,然后设置一下innerHTML就完事了。

听到这样的做法,经验丰富的你一定第一时间意识这样的做法会导致很多的问题。最大的问题就是这样做会很慢,因为即使一个小小的状态变更都要重新构造整棵 DOM,性价比太低;而且这样做的话,inputtextarea的会失去原有的焦点。最后的结论会是:对于局部的小视图的更新,没有问题(Backbone就是这么干的);但是对于大型视图,如全局应用状态变更的时候,需要更新页面较多局部视图的时候,这样的做法不可取。

但是这里要明白和记住这种做法,因为后面你会发现,其实 Virtual DOM 就是这么做的,只是加了一些特别的步骤来避免了整棵 DOM 树变更

另外一点需要注意的就是,上面提供的几种方法,其实都在解决同一个问题:维护状态,更新视图。在一般的应用当中,如果能够很好方案来应对这个问题,那么就几乎降低了大部分复杂性。

3 Virtual DOM算法


DOM是很慢的。如果我们把一个简单的div元素的属性都打印出来,你会看到:

而这仅仅是第一层。真正的 DOM 元素非常庞大,这是因为标准就是这么设计的。而且操作它们的时候你要小心翼翼,轻微的触碰可能就会导致页面重排,这可是杀死性能的罪魁祸首。

相对于 DOM 对象,原生的 JavaScript 对象处理起来更快,而且更简单。DOM 树上的结构、属性信息我们都可以很容易地用 JavaScript 对象表示出来:

var element = {
  tagName: 'ul', // 节点标签名
  props: { // DOM的属性,用一个对象存储键值对
    id: 'list'
  },
  children: [ // 该节点的子节点
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
  ]
}

上面对应的HTML写法是:

<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>

既然原来 DOM 树的信息都可以用 JavaScript 对象来表示,反过来,你就可以根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树。

之前的章节所说的,状态变更->重新渲染整个视图的方式可以稍微修改一下:用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对象结构。当然这样做其实没什么卵用,因为真正的页面其实没有改变。

但是可以用新渲染的对象树去和旧的树进行对比,记录这两棵树差异。记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。这样就可以做到:视图的结构确实是整个全新渲染了,但是最后操作DOM的时候确实只变更有不同的地方。

这就是所谓的 Virtual DOM 算法。包括几个步骤:

  1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
  2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  3. 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

4 算法实现


4.1 步骤一:用JS对象模拟DOM树

用 JavaScript 来表示一个 DOM 节点是很简单的事情,你只需要记录它的节点类型、属性,还有子节点:

element.js

function Element (tagName, props, children) {
  this.tagName = tagName
  this.props = props
  this.children = children
}

module.exports = function (tagName, props, children) {
  return new Element(tagName, props, children)
}

例如上面的 DOM 结构就可以简单的表示:

var el = require('./element')

var ul = el('ul', {id: 'list'}, [
  el('li', {class: 'item'}, ['Item 1']),
  el('li', {class: 'item'}, ['Item 2']),
  el('li', {class: 'item'}, ['Item 3'])
])

现在ul只是一个 JavaScript 对象表示的 DOM 结构,页面上并没有这个结构。我们可以根据这个ul构建真正的<ul>

Element.prototype.render = function () {
  var el = document.createElement(this.tagName) // 根据tagName构建
  var props = this.props

  for (var propName in props) { // 设置节点的DOM属性
    var propValue = props[propName]
    el.setAttribute(propName, propValue)
  }

  var children = this.children || []

  children.forEach(function (child) {
    var childEl = (child instanceof Element)
      ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
      : document.createTextNode(child) // 如果字符串,只构建文本节点
    el.appendChild(childEl)
  })

  return el
}

render方法会根据tagName构建一个真正的DOM节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。所以只需要:

var ulRoot = ul.render()
document.body.appendChild(ulRoot)

上面的ulRoot是真正的DOM节点,把它塞入文档中,这样body里面就有了真正的<ul>的DOM结构:

<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>

完整代码可见 element.js

4.2 步骤二:比较两棵虚拟DOM树的差异

正如你所预料的,比较两棵DOM树的差异是 Virtual DOM 算法最核心的部分,这也是所谓的 Virtual DOM 的 diff 算法。两个树的完全的 diff 算法是一个时间复杂度为 O(n^3) 的问题。但是在前端当中,你很少会跨越层级地移动DOM元素。所以 Virtual DOM 只会对同一个层级的元素进行对比:

上面的div只会和同一层级的div对比,第二层级的只会跟第二层级对比。这样算法复杂度就可以达到 O(n)。

4.2.1 深度优先遍历,记录差异

在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:

在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。

// diff 函数,对比两棵树
function diff (oldTree, newTree) {
  var index = 0 // 当前节点的标志
  var patches = {} // 用来记录每个节点差异的对象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 对两棵树进行深度优先遍历
function dfsWalk (oldNode, newNode, index, patches) {
  // 对比oldNode和newNode的不同,记录下来
  patches[index] = [...]

  diffChildren(oldNode.children, newNode.children, index, patches)
}

// 遍历子节点
function diffChildren (oldChildren, newChildren, index, patches) {
  var leftNode = null
  var currentNodeIndex = index
  oldChildren.forEach(function (child, i) {
    var newChild = newChildren[i]
    currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识
      ? currentNodeIndex + leftNode.count + 1
      : currentNodeIndex + 1
    dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点
    leftNode = child
  })
}

例如,上面的div和新的div有差异,当前的标记是0,那么:

patches[0] = [{difference}, {difference}, ...] // 用数组存储新旧节点的不同

同理ppatches[1]ulpatches[3],类推。

4.2.2 差异类型

上面说的节点的差异指的是什么呢?对 DOM 操作可能会:

  1. 替换掉原来的节点,例如把上面的div换成了section
  2. 移动、删除、新增子节点,例如上面div的子节点,把pul顺序互换
  3. 修改了节点的属性
  4. 对于文本节点,文本内容可能会改变。例如修改上面的文本节点2内容为Virtual DOM 2

所以我们定义了几种差异类型:

var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3

对于节点替换,很简单。判断新旧节点的tagName和是不是一样的,如果不一样的说明需要替换掉。如div换成section,就记录下:

patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}]

如果给div新增了属性idcontainer,就记录下:

patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}, {
  type: PROPS,
  props: {
    id: "container"
  }
}]

如果是文本节点,如上面的文本节点2,就记录下:

patches[2] = [{
  type: TEXT,
  content: "Virtual DOM2"
}]

那如果把我div的子节点重新排序呢?例如p, ul, div的顺序换成了div, p, ul。这个该怎么对比?如果按照同层级进行顺序对比的话,它们都会被替换掉。如pdivtagName不同,p会被div所替代。最终,三个节点都会被替换,这样DOM开销就非常大。而实际上是不需要替换节点,而只需要经过节点移动就可以达到,我们只需知道怎么进行移动。

这牵涉到两个列表的对比算法,需要另外起一个小节来讨论。

4.2.3 列表对比算法

假设现在可以英文字母唯一地标识每一个子节点:

旧的节点顺序:

a b c d e f g h i

现在对节点进行了删除、插入、移动的操作。新增j节点,删除e节点,移动h节点:

新的节点顺序:

a b c h d f g i j

现在知道了新旧的顺序,求最小的插入、删除操作(移动可以看成是删除和插入操作的结合)。这个问题抽象出来其实是字符串的最小编辑距离问题(Edition Distance),最常见的解决算法是 Levenshtein Distance,通过动态规划求解,时间复杂度为 O(M * N)。但是我们并不需要真的达到最小的操作,我们只需要优化一些比较常见的移动情况,牺牲一定DOM操作,让算法时间复杂度达到线性的(O(max(M, N))。具体算法细节比较多,这里不累述,有兴趣可以参考代码

我们能够获取到某个父节点的子节点的操作,就可以记录下来:

patches[0] = [{
  type: REORDER,
  moves: [{remove or insert}, {remove or insert}, ...]
}]

但是要注意的是,因为tagName是可重复的,不能用这个来进行对比。所以需要给子节点加上唯一标识key,列表对比的时候,使用key进行对比,这样才能复用老的 DOM 树上的节点。

这样,我们就可以通过深度优先遍历两棵树,每层的节点进行对比,记录下每个节点的差异了。完整 diff 算法代码可见 diff.js

4.3 步骤三:把差异应用到真正的DOM树上

因为步骤一所构建的 JavaScript 对象树和render出来真正的DOM树的信息、结构是一样的。所以我们可以对那棵DOM树也进行深度优先的遍历,遍历的时候从步骤二生成的patches对象中找出当前遍历的节点差异,然后进行 DOM 操作。

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
  var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  for (var i = 0; i < len; i++) { // 深度遍历子节点
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }

  if (currentPatches) {
    applyPatches(node, currentPatches) // 对当前节点进行DOM操作
  }
}

applyPatches,根据不同类型的差异对当前节点进行 DOM 操作:

function applyPatches (node, currentPatches) {
  currentPatches.forEach(function (currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        node.parentNode.replaceChild(currentPatch.node.render(), node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        node.textContent = currentPatch.content
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}

完整代码可见 patch.js

5 结语

Virtual DOM 算法主要是实现上面步骤的三个函数:elementdiffpatch。然后就可以实际的进行使用:

// 1. 构建虚拟DOM
var tree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: blue'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li')])
])

// 2. 通过虚拟DOM构建真正的DOM
var root = tree.render()
document.body.appendChild(root)

// 3. 生成新的虚拟DOM
var newTree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: red'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li'), el('li')])
])

// 4. 比较两棵虚拟DOM树的不同
var patches = diff(tree, newTree)

// 5. 在真正的DOM元素上应用变更
patch(root, patches)

当然这是非常粗糙的实践,实际中还需要处理事件监听等;生成虚拟 DOM 的时候也可以加入 JSX 语法。这些事情都做了的话,就可以构造一个简单的ReactJS了。

本文所实现的完整代码存放在 Github,仅供学习。

6 References


https://github.com/Matt-Esch/virtual-dom/blob/master/vtree/diff.js


来自:戴嘉华  https://github.com/livoras/blog/issues/13

https://github.com/livoras/blog/issues/created_by/livoras

html踩坑-坑爹的<hr/>与<hr />忽视空格问题

之前在编辑一个html页面时,想另起一行,写下一个篇章。
于是 顺手就写了 这个便签。

<hr />
#里面多了一个空格啊 ,正确的写法如下
<hr/>

然后 再 接着写后面的 文章。

等文章写完, 打开浏览器 验证 效果时,发现 布局出问题了。
然后我 分段 排查问题。最终发现,是

<hr />
# 标签里面多了一个空格引发的  布局惨案

里面的换行标签,多了一个空格,导致浏览器 无法识别。。。

http协议-headers-各字段详解

Cache-Control:must-revalidate, no-cache, private。

这个值告诉客户端,服务端不希望客户端缓存资源,在下次请求资源时,必须要从新请求服务器,不能从缓存副本中获取资源。
Cache-Control是响应头中很重要的信息,当客户端请求头中包含Cache-Control:max-age=0请求,明确表示不会缓存服务器资源时,Cache-Control作为作为回应信息,通常会返回no-cache,意思就是说,“不缓存就不缓存呗”;当客户端在请求头中没有包含Cache-Control时,服务端往往会定,不同的资源不同的缓存策略,比如说oschina在缓存图片资源的策略就是Cache-Control:max-age=86400,这个意思是,从当前时间开始,在86400秒的时间内,客户端可以直接从缓存副本中读取资源,而不需要向服务器请求。

Connection:keep-alive

这个字段作为回应客户端的Connection:keep-alive,告诉客户端服务器的tcp连接也是一个长连接,客户端可以继续使用这个tcp连接发送http请求。关于长连接的更多知识,后面我再详细讲。

Content-Encoding:gzip

告诉客户端,服务端发送的资源是采用gzip编码的,客户端看到这个信息后,应该采用gzip对资源进行解码。

Content-Type:text/html;charset=UTF-8

告诉客户端,资源文件的类型,还有字符编码,客户端通过utf-8对资源进行解码,然后对资源进行html解析。通常我们会看到有些网站是乱码的,往往就是服务器端没有返回正确的编码。

Content-type: text/html; charset=xxx的优先级要高于
<META http-equiv=”content-type” content=”text/html; charset=xxx”>
因为一个在headers中,一个在网页中。浏览器响应时,有时候是json文件,pdf文件等,这些文件里面没有设置编码,不像网页文件内部可以设置编码。所以headers中的>Content-type 优先级更高。

服务器可以设置响应文件的编码,其实功能相当于在 headers中设置 Content-type。如果headers已经有了Content-type,那就按服务器设置的方案,修改编码方式。

对于网页编码,headers中Content-type: text/html; charset=xxx;可以不用指定charset=xxx;因为网页内部也可以设置编码方案。

当时做了一个简易的web服务器,关键细节总结。

返回消息时,http 请求头和请求体 之间 需要 加 一个  【回车+换行】两个步骤。当然,java里面就一句  xxx.println();

当时,输出响应头时:

xxx.println(“Content-Type: text/css”); 这个css的类型,没有单独写。只是用if else 单独判断输出了   text/html   text/javascript   text/png    image/jpeg   image/gif  ,其他的 一律输出  application/octet-stream  类型。所以一开始,请求 css 文件 时,响应头是  application/octet-stream  类型。然后,浏览器加载网页时,居然没有加载 css 文件。因为它认为 下载 的css文件,类型不对。所以网页渲染时,页面布局 混乱了。后来改成 输出 Content-Type: text/css 时,页面渲染成功了。

ETAG:字符串(出现在http1.1协议中,可选字段)

ETag在HTTP头字段中的使用是可选的(不像HTTP/1.1的其他头字段是强制性的)。HTTP规范从未指定生成ETag的方法。

ETag又叫实体标签(entity tag)是HTTP协议提供的若干机制中的一种Web缓存验证机制,并且允许客户端进行缓存协商。这就使得缓存变得更加高效,而且节省带宽。如果资源的内容没有发生改变,Web服务器就不需要发送一个完整的响应。ETag也可用于乐观并发控制,作为一种防止资源同步更新而相互覆盖的方法。

ETag是一个不透明的标识符,由Web服务器根据URL上的资源的特定版本而指定。如果那个URL上的资源内容改变,一个新的不一样的ETag就会被分配。用这种方法使用ETag即类似于指纹,并且他们能够被快速地被比较,以确定两个版本的资源是否相同。ETag的比较只对同一个URL有意义——不同URL上的资源的ETag值可能相同也可能不同,从他们的ETag的比较中无从推断。

Date:Sun, 21 Sep 2014 06:18:21 GMT

这个是服务端发送资源时的服务器时间,刚开始我不知道GMT是格林尼治所在地的标准时间,以为是服务器的时间错了,还去服务器上查看过时间。http协议中发送的时间都是GMT的,这主要是解决在互联网上,不同时区在相互请求资源的时候,时间混乱问题。

Expires:Sun, 1 Jan 2000 01:00:00 GMT

这个响应头也是跟缓存有关的,告诉客户端在这个时间前,可以直接访问缓存副本,很显然这个值会存在问题,因为客户端和服务器的时间不一定会都是相同的,如果时间不同就会导致问题。所以这个响应头是没有Cache-Control:max-age=***这个响应头准确的,因为max-age=date中的date是个相对时间,不仅更好理解,也更准确。

Pragma:no-cache

这个含义与Cache-Control等同。

Server:Tengine/1.4.6

这个是服务器和相对应的版本,只是告诉客户端服务器信息,没有更多的意思。

Transfer-Encoding:chunked

这个响应头告诉客户端,服务器发送的资源的方式是分块发送的。一般分块发送的资源都是服务器动态生成的,在发送时还不知道发送资源的大小,所以采用分块发送,每一块都是独立的,独立的块都能标示自己的长度,最后一块是0长度的,当客户端读到这个0长度的块时,就可以确定资源已经传输完了。


HTTP Header 之Content-Type介绍

Content-Type 实体头部用于指示资源的MIME类型 media type 。

在响应中,Content-Type标头告诉客户端实际返回的内容的内容类型。浏览器会在某些情况下进行MIME查找,并不一定遵循此标题的值; 为了防止这种行为,可以将标题 X-Content-Type-Options 设置为 nosniff

在请求中 (如POST 或 PUT),客户端告诉服务器实际发送的数据类型。

Header type Entity header
Forbidden header name no
CORS-safelisted response-header yes
句法
Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=something
指令
media-type
资源或数据的 MIME type 。
charset
字符编码标准。
boundary
对于多部分实体,boundary 是必需的,其包括来自一组字符的1到70个字符,已知通过电子邮件网关是非常健壮的,而不是以空白结尾。它用于封装消息的多个部分的边界。
例子

Content-Type 在HTML表单中

在通过HTML form提交生成的POST请求中,请求头的Content-Type由<form>元素上的enctype属性指定

<form action="/" method="post" enctype="multipart/form-data">
  <input type="text" name="description" value="some text">
  <input type="file" name="myFile">
  <button type="submit">Submit</button>
</form>

请求头看起来像这样(在这里省略了一些 headers):

POST /foo HTTP/1.1
Content-Length: 68137
Content-Type: multipart/form-data; boundary=---------------------------974767299852498929531610575

---------------------------974767299852498929531610575
Content-Disposition: form-data; name="description" 

some text
---------------------------974767299852498929531610575
Content-Disposition: form-data; name="myFile"; filename="foo.txt" 
Content-Type: text/plain 

(content of the uploaded file foo.txt)
---------------------------974767299852498929531610575
规范
Specification Title
RFC 7233, section 4.1: Content-Type in multipart Hypertext Transfer Protocol (HTTP/1.1): Range Requests
RFC 7231, section 3.1.1.5: Content-Type Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content

 

参考:

https://my.oschina.net/manmao/blog/549123

字符编码-javascript中UTF-16字符的性质与计算

UTF-16是UCS-2的扩展。JavaScript引擎内部是自由的使用UCS-2或者UTF-16。大多数引擎使用的是UTF-16,无论它们使用什么方式实现,它只是一个具体的实现,这不将影响到语言的特性。 —–到时候,再探究

 

JavaScript中的字符串是以UTF-16为代码单元。通常我们使用的字符范围都在Unicode值0x10000以内,他们对应的UTF-16就是它们自身。但Unicode中也存在这个范围之外的字符,这时候就需要两个UTF-16字符来描述。这些字符在统计时会被作为两个字符。
比如下面这个字符,虽然只有一个字,但却会统计出两个字符。

<script>
alert("?".length); //2
</script>
 因为字符串的length表示的并不是字符个数,而是UTF-16的单元个数。Unicode为UTF-16预留了两块区域,称为“高位替代区”和“低位替代区”。这两个区域的字符单独使用是每一意义的,甚至在某些地方都无法单独使用他们,比如内置的URL转义函数就无法转换这样的字符

在正则表达式中,它也被作为两个字符来处理

使用charCodeAt的时候也会把它作为两个字符来处理。这些把它当做两个字符处理的情况会分别处理它的“高位替代符”和“低位替代符”。
这种四字节的UTF-16对应的Unicode值可以这样计算。这些替代符都有固定的格式,这些格式也决定了他们在Unicode中的区位。比如高位替代符的二进制表示就是 “110110xx xxxxxxxx”,低位替代符的二进制表示就是“110111yy yyyyyyyy”。把它们固定的头部去掉,取出内容部分再把这两个替代符的内容部分的二进制连接起来就可以得到“xxxx xxxxxxyy yyyyyyyy”,这是一个20位的二进制。实际上目前Unicode的最大码位是“0x10FFFF”,写成二进制就是“10000 11111111 11111111”,这是21位的。也就是说两个UTF-16的替代符中包含的数据要组成任意一个Unicode字符还差了一点。从上面二进制可以观察出,如果x和y全是1的话,还需要加上一个“0x10000”(写成二进制是“1 00000000 00000000”)才能让它达到Unicode最大码位。其实在把Unicode值转换到UTF-16时候就是减去这个值后转换过来的。因为双字节的UTF-16已经可以表示所有小于“0x10000”码位的字符了,四字节的UTF-16只要负责好大于它的部分字符就行。
另外,在发这篇文章的时候又遇到一些问题,MySQL的 utf8 general_ci 貌似不支持四字节字符。嘛,这个问题以后再研究啦。

来自:https://www.web-tinker.com/article/20468.html

字符编码-java内部编码探讨

一、Stack Overflow 上的提问

What is the Java’s internal represention for String? Modified UTF-8? UTF-16?
52down voteaccepted

Java uses UTF-16 for the internal text representation

The representation for String and StringBuilder etc in Java is UTF-16

https://docs.oracle.com/javase/8/docs/technotes/guides/intl/overview.html

How is text represented in the Java platform?

The Java programming language is based on the Unicode character set, and several libraries implement the Unicode standard. The primitive data type char in the Java programming language is an unsigned 16-bit integer that can represent a Unicode code point in the range U+0000 to U+FFFF, or the code units of UTF-16. The various types and classes in the Java platform that represent character sequences – char[], implementations of java.lang.CharSequence (such as the String class), and implementations of java.text.CharacterIterator – are UTF-16 sequences.

At the JVM level, if you are using -XX:+UseCompressedStrings (which is default for some updates of Java 6) The actual in-memory representation can be 8-bit, ISO-8859-1 but only for strings which do not need UTF-16 encoding.

http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

and supports a non-standard modification of UTF-8 for string serialization.

Serialized Strings use UTF-8 by default.

And how many bytes does Java use for a char in memory?

char is always two bytes, if you ignore the need for padding in an Object.

Note: a code point (which allows character > 65535) can use one or two characters, i.e. 2 or 4 bytes.

二、java内部编码是utf-16

代码一:

char c = '放';
System.out.printf("The value of char %c is %d.%n", c, (int) c);

String str = String.valueOf(c);
byte[] bys;
try {
	bys = str.getBytes("Unicode");
	for (int i = 0; i < bys.length; i++) {
		System.out.printf("%X ", bys[i]);
	}
	System.out.println();

	int unicode = (bys[2] & 0xFF) << 8 | (bys[3 & 0xFF]);
	System.out.printf("The unicode value of %c is %d.%n", c, unicode);

} catch (UnsupportedEncodingException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
}

输出结果如下:

		
The value of char 放 is 25918.
FE FF 65 3E 
The unicode value of 放 is 25918.

代码二:

		
                 String a = new String("放");
		 System.out.println(strTo16(a));

/******************** another method ******************************/

public static String strTo16(String s) {
		String str = "";

		System.out.println(s.length());
		for (int i = 0; i < s.length(); i++) {
			int ch = (int) s.charAt(i);
			String s4 = Integer.toHexString(ch);
			str = str + s4;
		}
		return str;
	}

输出结果如下:

		
1
653e

UTF-16 首先用两个字节,表示Unicode编码,两个字节的情况下,编码和Unicode一模一样。2个字节能编码的字一共有2^16 = 65536个字。这种情况下,一个字符占两个字节。那么 utf-16 如何表示 大于 Unicode编码 65536的字呢。这个时候,就是采用4个字节编码了。

维基百科 Unicode已经超过13万编码了   https://zh.wikipedia.org/wiki/Unicode

三、编码举例

字符串“I am 君山”用 UTF-16 编码,下面是编码结果:

用 UTF-16 编码将 char 数组放大了一倍,单字节范围内的字符,在高位补 0 变成两个字节,中文字符也变成两个字节。从 UTF-16 编码规则来看,仅仅将字符的高位和地位进行拆分变成两个字节。特点是编码效率非常高,规则很简单,由于不同处理器对 2 字节处理方式不同,Big-endian(高位字节在前,低位字节在后)或 Little-endian(低位字节在前,高位字节在后)编码,所以在对一串字符串进行编码是需要指明到底是 Big-endian 还是 Little-endian,所以前面有两个字节用来保存 BYTE_ORDER_MARK 值,UTF-16 是用2 字节或4字节来表示的 UCS-2 或 Unicode 转换格式,通过代理对来访问 BMP 之外的字符编码。

 


参考:https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html

字符编码-教程(4)-字节序、本地序、网络序和多字节码元

一、字节序含义


字节序,又称字节顺序,其英文为Byte-Order;另外英文中还可称为Endianness,因此也翻译为端序。

Endianness这个词,源自于1726年Jonathan Swift的名著:Gulliver’s Travels(格列佛游记)。在书中有一个童话故事,大意是指Lilliput小人国的国王下了一道指令,规定其人民在剥水煮蛋时必须从little-end(小端)开始剥。这个规定惹恼了一群觉得应该要从big-end(大端)开始剥的人。事情发展到后来演变成了一场战争。后来,支持小端的人被称为little-endian,反之则被称为big-endian(在英语中后缀“-ian”表示“xx人”之意)。

1980年,Danny Cohen在他的论文“On Holy Wars and a Plea for Peace”中,第一次使用了Big-endian和Little-endian这两个术语,最终它们成为了异构计算机系统之间进行通讯、交换数据时所要考虑的极其重要的一个问题。

(注:所谓异构是指不同架构、不同结构、不同构造等,而这里的异构计算机系统,主要指的是采用不同CPU和/或不同操作系统的计算机系统。)

字节序,具体来说,就是多字节数据(大于一个字节的数据)在计算机中存储、读取时其各个字节的排列顺序。

  • 大端序BE(Big-Endian,也称高尾端序、大尾),小端序LE(Little-Endian,也称为低尾端序、小尾)  。两者是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。如果将49写在前面,就是little endian。
  • BOM: Byte Order Mark, Unicode(后面教程会提到)规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。
  • 如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

二、字节序分类


字节序共分为三种:

  • 大端序BE(Big-Endian,也称高尾端序);
  • 小端序LE(Little-Endian,也称为低尾端序);
  • 中间序ME(Middle-Endian,也称为混合序),不太常用,本文不作介绍。

计算机硬件有两种储存数据的方式:大端字节序(big endian)和小端字节序(little endian)。

举例来说,数值0x2211使用两个字节储存:高位字节是0x22,低位字节是0x11

  • 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
  • 小端字节序:低位字节在前,高位字节在后,即以0x1122形式储存。

同理,0x1234567的大端字节序和小端字节序的写法如下图。

a) 小端序Little-Endian(低尾端序)

就是低位字节(即小端字节)存放在内存的低地址,而高位字节(即大端字节)存放在内存的高地址。

这是最符合人的直觉思维的字节序(但却不符合人的读写习惯),因为从人的第一观感来说,低位字节的值小,对应放在内存地址也小的地方,也即内存中的低位地址;反之,高位字节的值大,对应放在内存地址大的地方,也即内存中的高位地址。

b) 大端序Big-Endian(高尾端序)

就是高位字节(即大端字节)存放在内存的低地址,低位字节(即小端字节)存放在内存的高地址。

这是最符合人平时的读写习惯的字节序(但却不符合人的直觉思维),因为不用像在Little-Endian中还需考虑字节的高位、低位与内存的高地址、低地址的对应关系,只需把数值按照人通常的书写习惯,从高位到低位的顺序直接在内存中从左到右或从上到下(下图中就是从上到下)按照由低到高的内存地址,一个字节一个字节地填充进去。

 c) 中间序Middle-Endian(混合序Mixed-Endian

混合序具有更复杂的顺序。以PDP-11为例,32位的0x0A0B0C0D被存储为:

混合序较少见,常见的多为大端序和小端序。

三、字节序起因


字节序的选择:因为历史上设计不同计算机系统的人在当时基于各自的理由和原因(这里的理由和原因网上存在着各种不同的说法,但也或许根本就没有具体理由和原因,只是设计人员的个人偏好,甚至是随意决定的),在各自计算机系统的设计上作出了不同的选择。

我一直不理解,为什么要有字节序,每次读写都要区分,多麻烦!统一使用大端字节序,不是更方便吗?

上周,我读到了一篇文章,解答了所有的疑问。而且,我发现原来的理解是错的,字节序其实很简单。

1、首先,为什么会有小端字节序?

答案是,计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。

但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

2、计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节。它只知道按顺序读取字节,先读第一个字节,再读第二个字节。

如果是大端字节序,先读到的就是高位字节,后读到的就是低位字节。小端字节序正好相反。

理解这一点,才能理解计算机如何处理字节序。

四、字节序与编码单元(码元)


在多字节编码的字符方案中(后面会介绍比如GBK编码,UTF-16编码):

有些编码方案规定了多字节编码中前面的字节是怎么样后面的字节是怎么样(比如GBK编码),这种就叫单字节码元。【计算机读取时,只能用大端序或者小端序的一种,能够解码成功,另一种解码会失败,不会两种都能解码成功】

有些编码方案没有规定前面的字节怎么样后面的字节怎么样,只规定了这个字用了几个字节。就是多字节码元。【可以用大端序或小端序两种解码方法,会产生歧义,所以需要字节序】

总结:只有多字节码元才需要字节序。
以字节为单位就是说它是一个字节一个字节来的,utf16是以一个字一个字来的。学计算机基础的时候应该有说过字节byte、字word、双字double word(dword)之间的关系。一个字节一个字节来就没这个问题,一个字一个字来就要考虑这个字是哪个字节在前。
utf8的标准说了前面的字节是怎么样后面的字节是怎么样,gbk同理。但是utf16是“字”怎么样,这个不同。编码单元简称码元,码元有可能是单字节也有可能是多字节。

五、字节序与数据类型


实际上,int、short、long等数据类型一般是编程语言层面的概念,更进一步而言,这其实涉及到了机器硬件层面(即汇编语言)中的数据类型byte字节、word字、dword双字等在硬件中的表达与处理机制(实质上字节序跟CPU寄存器的位数、存放顺序密切相关)。具体可参看附文:《本质啊本质之一:数据类型的本质》、《寄存器与字、字节》。

【附:本质啊本质之一:数据类型的本质

CSDN博客 博主:band_of_brothers 发表于:2007-10-10 22:20

研究一个层面的问题,往往要从更深的层面找寻答案。这就如C语言与汇编、汇编与机器指令,然而终究要有个底限,这个底限以能使我们心安理得为准,就好比公理之于数学、三大定律之于宏观物理。

在这里就将机器指令作为最后的底限吧,尽管再深入下去还有微指令,但那毕竟是太机器了,可以了。以下所有从C代码编译生成汇编代码用的是命令:cl xxx..c /Fa /Ze。

类型的本质

类型这个概念,好多地方都有讲,但说实话,你真的理解吗?什么是类型?类型是一个抽象的概念还是一个真实的存在?嗯?

开始:

1、“好多相同或相似事物的综合”(辞海)。

2、X86机器的数据类型有byte、word、dword、fword、tword、qword,等等。

3、“给内存块一个明确的名字,就象邮件上的收件人一样。给其一个明确的数据类型,就好象说,邮件是一封信,还是一个包裹。”

4、类型就是一次可以操作的块的大小,就是一个单位,就像克、千克、吨一样。双字一次操作32位;字,一次操作16位;如果没有各种类型,机器只有一个类型单位——字节,那么当需要一个4字节大小的块时,就需要4次操作,而如果有双字这个类型单位,那么只需要一次操作就可以了。

5、类型,是机器层面支持的,不是软的,是硬的,有实实在在的机器码为证。

类型的反汇编

W32dasm反汇编出来的东西,可以看出不同的类型,机器码不同,说明类型是机器硬件级别支持的,不是通过软件实现的,更不是一个抽象的概念。

Opcodes上关于mov的机器码讲的更清楚:

需要说明的是,一些大的类型单位,如qword等,在mov等标准指令里是没有的,在一些特殊指令里才能用到,如浮点指令:fmul qword ptr [0067FB08] 机器码:DC0D08FB6700。】

【附:寄存器与字、字节

字节:记为byte,一个字节由8个比特(bit)组成,可以直接存在一个8位寄存器里

1 0 1 0 1 0 0 1

一个字节

字:记为word,一个字由2个字节(共16比特)组成,可以直接存在一个16位寄存器里

1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0

高位字节        低位字节

一个8位寄存器用2位十六进制数表示,只能存1个字节(1个byte类型的数据)

一个16位寄存器用4位十六进制数表示,可存1个字(1个word类型的数据)或2个字节(2个byte类型的数据)

一个32位寄存器用8位十六进制数表示,可存2个字(1个dword类型的数据或2个word类型的数据)或4个字节(4个byte类型的数据)】

六、网络字节序(网络序)


网络字节顺序(network byte order)是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。

不过,容易令人困惑的是,IP协议作为网络层协议,其面向的数据是报文,是没有字节的概念的,也就无关字节序了。因此,英文版wikipedia上说:

In fact, the Internet Protocol defines a standard big-endian network byte order. This byte order is used for all numeric values in the packet headers and by many higher level protocols and file formats that are designed for use over IP.

也就是说,IP协议里的字节序实际上是用在分组头里的数值上的,例如每个分组头会包含源IP地址和目标IP地址,在寻址和路由的时候是需要用到这些值的。

比如,4个字节的32 bit值以下面的次序传输:首先是高位的0~7bit,其次8~15bit,然后16~23bit,最后是低位的24~31bit。这种传输次序称作大端字节序。由于TCP/IP头部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。

再比如,以太网头部中2字节的“以太网帧类型”字段,表示是的后面所跟数据帧的类型。对于ARP请求或应答的以太网帧类型来说,在网络传输时,发送的顺序是以大端方式进行的:0x08,0x06。其在内存中的映象如下所示:

内存低地址

————————

0x08 — 高位字节

0x06 — 低位字节

————————

内存高地址

该字段的值为0x0806,也是以大端方式存放在内存中的。

其实,IP报文中的很多数据都是需要做字节序转换的,比如包长度、check sum校验和等,这些值大都是short(16bit)或者long(32bit)型,所以解析IP报文时也需要做网络->主机字节序转换,而生成报文字节流时则需要进行主机->网络字节序转换(计算机中的字节序被称之为主机字节序,简称主机序;相对于网络传输中的字节序被称之为网络字节序,简称网络序)。

为了进行网络字节序与主机字节序的转换,BSD sockets(Berkeley sockets)提供了四个转换的函数:htons、ntohs、htonl、ntohl,其中h是host、n是network、s是short、l是long:

htons把unsigned short类型从主机字节序转换到网络字节序

htonl把unsigned long类型从主机字节序转换到网络字节序

ntohs把unsigned short类型从网络字节序转换到主机字节序

ntohl把unsigned long类型从网络字节序转换到主机字节序

在使用little endian的系统中,这些函数会把字节序进行转换;在使用big endian类型的系统中,这些函数会定义成空宏。

Windows系统API中也提供了类似的转换函数。而在.Net中,网络字节序与主机字节序两者之间的转换,由IPAddress类的静态方法提供:HostToNetworkOrder和NetworkToHostOrder。】

七、本地序(主机序)


其实这个就是指本机操作系使用的字节序。

Intel和AMD的X86平台,以及DEC(Digital Equipment Corporation,后与Compaq合并,之后Compaq又与HP合并)采用的是Little-Endian,而像IBM、Sun的SPARC采用的就是Big-Endian。有的嵌入式平台是Big-Endian的。JAVA字节序也是Big-Endian的。

当然,这不代表所有情况。有的CPU即能工作于小端,又能工作于大端,比如ARM、Alpha、摩托罗拉的Power PC、SPARC V9、MIPS、PA-RISC和IA64等体系结构(具体情形可参考处理器手册),其字节序是可切换的,这种可切换的特性可以提高效率或者简化网络设备和软件的逻辑。

这种可切换的字节序被称为Bi-Endian(前缀“Bi-”表示“双边的、二重的、两个的”),用于硬件上意指计算机存储时具有可以使用两种不同字节序中任意一种的能力。具体这类CPU是大端还是小端,和具体设置有关。如Power PC可支持Little-Endian字节序,但其默认配置为Big-Endian字节序。

一般来说,大部分用户的操作系统,如windows、FreeBsd、Linux是Little-Endian的;少部分,如Mac OS是Big-Endian的。

具体参见下表:

七、字节序处理模拟


举例来说,处理器读入一个16位整数。如果是大端字节序,就按下面的方式转成值。

x = buf[offset] * 256 + buf[offset+1];

上面代码中,buf是整个数据块在内存中的起始地址,offset是当前正在读取的位置。第一个字节乘以256,再加上第二个字节,就是大端字节序的值,这个式子可以用逻辑运算符改写。

x = buf[offset]<<8 | buf[offset+1];

上面代码中,第一个字节左移8位(即后面添8个0),然后再与第二个字节进行或运算。

如果是小端字节序,用下面的公式转成值。

x = buf[offset+1] * 256 + buf[offset];

32位整数的求值公式也是一样的。

/* 大端字节序 */
i = (data[3]<<0) | (data[2]<<8) | (data[1]<<16) | (data[0]<<24);

/* 小端字节序 */
i = (data[0]<<0) | (data[1]<<8) | (data[2]<<16) | (data[3]<<24);

八、字节序与联通的故事

讲到这里,我们再顺便说说一个很著名的奇怪现象:当你在 windows 的记事本里新建一个文件,输入”联通”两个字之后,保存,关闭,然后再次打开,你会发现这两个字已经消失了,代之的是几个乱码!呵呵,有人说这就是联通之所以拼不过移动的原因。

其实这是因为GB2312编码与UTF8编码产生了编码冲撞的原因。

当一个软件打开一个文本时,它要做的第一件事是决定这个文本究竟是使用哪种字符集的哪种编码保存的。软件一般采用三种方式来决定文本的字符集和编码:

检测文件头标识,提示用户选择,根据一定的规则猜测

最标准的途径是检测文本最开头的几个字节,开头字节 Charset/encoding,如下表:

1 EF BB BF UTF-8 
2    
3 FF FE UTF-16/UCS-2, little endian 
4    
5 FE FF UTF-16/UCS-2, big endian 
6    
7 FF FE 00 00 UTF-32/UCS-4, little endian. 
8    
9 00 00 FE FF UTF-32/UCS-4, big-endian.

当你新建一个文本文件时,记事本的编码默认是ANSI(代表系统默认编码,在中文系统中一般是GB系列编码), 如果你在ANSI的编码输入汉字,那么他实际就是GB系列的编码方式,在这种编码下,”联通”的内码是:

1 c1 1100 0001 
2    
3 aa 1010 1010 
4    
5 cd 1100 1101 
6    
7 a8 1010 1000

注意到了吗?第一二个字节、第三四个字节的起始部分的都是”110″和”10″,正好与UTF8规则里的两字节模板是一致的,

于是当我们再次打开记事本时,记事本就误认为这是一个UTF8编码的文件,让我们把第一个字节的110和第二个字节的10去掉,我们就得到了”00001 101010″,再把各位对齐,补上前导的0,就得到了”0000 0000 0110 1010″,不好意思,这是UNICODE的006A,也就是小写的字母”j”,而之后的两字节用UTF8解码之后是0368,这个字符什么也不是。这就是只有”联通”两个字的文件没有办法在记事本里正常显示的原因。

而如果你在”联通”之后多输入几个字,其他的字的编码不见得又恰好是110和10开始的字节,这样再次打开时,记事本就不会坚持这是一个utf8编码的文件,而会用ANSI的方式解读之,这时乱码又不出现了。

造成这个问题的关键原因是:打开记事本写联通两字时,保存好时,默认是ASCII编码,打开文件时,没有BOM作为判断依据,所以系统默认 当成utf-8 解码了。记住:平时我们保存文件时 经常能看到 save as utf-8 和 save as utf-8 + BOM 两种选择。现在应该知道区别了吧。话说,Windows 记事本里直接 打联通 然后 另存为 utf-8  。等下次打开时,都没有问题的。估计是,系统偏向 utf-8 解码的原因。因为前面 ascii 编码后,系统 犹豫 用ASCII 解码还是 utf-8解码时,倾向后者,所以没有用 BOM标记的 utf-8 文件,解码也是正确的。

补充:


当读取(或写入)需要连续读取(或写入)超过一个字节数据时才需要考虑字节序问题。

“计算机的内部处理都是小端字节序”不正确……

虽然x86是小端的,不过也有很多CPU是大端的(比如powerpc)。另外小端虽然对加法器比较友好,但除法还是大端更合适,所以这种取舍其实只是一些历史问题吧……

小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。
大端模式 :符号位的判定固定为第一个字节,容易判断正负。

小端模式更适合系统内部,大端模式更适合网络数据传递,加上一些历史引领的原因,
导致现在两种字节序方式并存。


参考:

http://www.ruanyifeng.com/blog/2016/11/byte-order.html

https://www.cnblogs.com/benbenalin/p/6918634.html

https://blog.csdn.net/band_of_brothers/article/details/1819228

 

字节序问题:

https://baike.baidu.com/item/%E5%AD%97%E8%8A%82%E5%BA%8F/1457160

http://imweb.io/topic/57fe263b2a25000c315a3d8a

https://www.cnblogs.com/benbenalin/p/6918634.html

Request body 问题:

https://www.cnblogs.com/chyu/p/5517788.html