Blog

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

字符编码-教程(5)-简体汉字编码与半角和圆角

一、单字节编码与多字节编码


1、单字节编码(SBCS)

用一个字节来编码的,比如前面提到的EBCDIC,ASCII ,EASCII,ISO-8859系列都是单字节编码。

ASCII 码

学过计算机的人都知道 ASCII 码,总共有 128 个,用一个字节的低 7 位表示,0~31 是控制字符如换行回车删除等;32~126 是打印字符,可以通过键盘输入并且能够显示出来。

ISO-8859-1

128 个字符显然是不够用的,于是 ISO 组织在 ASCII 码基础上又制定了一些列标准用来扩展 ASCII 编码,它们是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵盖了大多数西欧语言字符,所以应用的最广泛。ISO-8859-1 仍然是单字节编码,它总共能表示 256 个字符。

2、多字节编码(MBCS)

用多个字节来编码的,就是多字节编码。比如下面介绍的编码方案都是。

二、简体汉字编码

英文字母再加一些其他标点字符之类的也不会超过256个,用一个字节来表示一个字符就足够了(2^8 = 256)。但其他一些文字不止这么多字符,比如中文中的汉字就多达10多万个,一个字节只能表示256个字符,肯定是不够的,因此只能使用多个字节来表示一个字符。

1、GB2312

等到中国人们得到计算机时,单字节的ASCII码 及其拓展码,没有很多状态来保存 6000多个常用汉字。但是这难不倒智慧的中国人民,我们不客气地把那些127号之后的奇异符号们直接取消掉, 规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的“全角”字符,而原来在127号以下的那些就叫“半角”字符了。

中国人民看到这样很不错,于是就把这种汉字方案叫做 “GB2312”。GB2312 是对 ASCII 的中文扩展。

它的全称是《信息交换用汉字编码字符集 基本集》,它是双字节编码。它的第一个字节为128-255。系统可以据此判断,若第一个字节大于127,则把与该字节后紧接着的一个字节结合起来共两个字节组成一个中文字符。这种由多个字节存储一个字符的字符集叫多字节字符集(MultiByte Charsets),对应的象ASCII这种用一个字节存储一个字符的字符集叫单字节字符集(SingleByte Charsets)。在GB2312字符集中,ASCII字符仍然用一个字节存储,换句话说该ASCII是该字符集的子集。=========================================================

GB2312编码方案,即《信息交换用汉字编码字符集——基本集》,是由中国国家标准总局于1980年发布、1981年5月1日开始实施的一套国家标准,标准号为GB2312-1980。其中收录了6763个常用汉字和682个其它符号(6763+682=7445),并将该字符集分为94个区,每个区94位,每个位对应一个字符或零个字符(94×94=8836,8836-7745=1391说明有1391个位置是空的)。

GB2312编码适用于汉字处理、汉字通信等系统之间的信息交换,通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB2312。

区号 内容
01-09区 特殊符号,如:标点符号、数字序列、全角字符、日语假名、拼音音标等
10-15区
16-55区 一级汉字,按拼音排序
56-87区 二级汉字,按部首/笔画排序
88-94区

如果将GB2312看做是一种“字符集”,则可以用多种编码方式对它进行编码。比如“区位码”就是对GB2312字符集最简单的一种编码方式,它直接使用区号和位号组成一个编码值(例如:GB2312字符集中的第一个汉字“啊”,它的区号为16,位号为01,它的区位码就是1601)。这里GB2312 编码列出了GB2312区位码编码。

但通常,人们所说的GB2312指的是一种编码(并且不是指区位码),它是指通常采用EUC方法对GB2312字符集中的“区”和“位”进行处理后的编码。EUC方法的处理方式:区号和位号分别加上0xA0,结果分别作为GB2312编码的两个字节的值(例如:“啊”字的区号和位号分别为16和01,即十六进制0x10和0x01,分别加0xA0得到编码0xB0A1),这样做是为了兼容ASCII编码(GB2312编码的两字节都大于ASCII码的最大值)。这里GB2312简体中文编码表列出了通常更常用的GB2312编码。

GB 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时,除了汉字,GB 2312还收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符

可能是出于显示上视觉美观的考虑,除汉字之外的682个字符中,甚至包括了ASCII里本来就有的数字、标点、英文字母等字符。也就是说,这些ASCII里原来就有的单字节编码的字符,又再编了两个字节长的GB2312编码版本。这682个字符就是常说的“全角”字符,而这682个字符中所对应的ASCII字符就被称之为“半角”字符

但是中国的汉字太多了,我们很快就就发现有许多人的人名没有办法在这里打出来,特别是某些很会麻烦别人的国家领导人。于是我们不得不继续把 GB2312 没有用到的码位找出来老实不客气地用上。


【附:全角、半角

全角字符是中文显示及双字节中文编码的历史遗留问题。

早期的点阵显示器上由于像素有限,原先ASCII西文字符的显示宽度(比如8像素的宽度)用来显示汉字有些捉襟见肘(实际上早期的针式打印机在打印输出时也存在这个问题),因此就采用了两倍于ASCII字符的显示宽度(比如16像素的宽度)来显示汉字。

这样一来,ASCII西文字符在显示时其宽度为汉字的一半。或许是为了在西文字符与汉字混合排版时,让西文字符能与汉字对齐等视觉美观上的考虑,于是就设计了让西文字母、数字和标点等特殊字符在外观视觉上也占用一个汉字的视觉空间(主要是宽度),并且在内部存储上也同汉字一样使用2个字节进行存储的方案。这些与汉字在显示宽度上一样的字符就被称之为全角字符。

而原来ASCII中的西文字符由于在外观视觉上仅占用半个汉字的视觉空间(主要是宽度),并且在内部存储上使用1个字节进行存储,相对于全角字符,因而被称之为半角字符。

后来,其中的一些全角字符因为比较有用,就得到了广泛应用(比如全角的逗号“,”、问号“?”、感叹号“!”、空格“ ”等,这些字符在输入法中文输入状态下的半角与全角是一样的,英文输入状态下全角跟中文输入状态一样,但半角大约为全角的二分之一宽),专用于中日韩文本,成为了标准的中日韩标点字符。而其它的许多全角字符则逐渐失去了价值(现在很少需要让纯文本的中文和西文字字对齐了),就很少再用了。

现在全球字符编码的事实标准是Unicode字符集及基于此的UTF-8UTF-16等编码实现方式。Unicode吸纳了许多遗留(legacy)编码,并且为了兼容性而保留了所有字符。因此中文编码方案中的这些全角字符也保留下来了,而国家标准也仍要求字体和软件都支持这些全角字符。

不过,半角和全角字符的关系在UTF-8UTF-16等中不再是简单的1字节和2字节的关系了。具体参见后文。】


2、GB13000

GB2312-1980共收录6763个汉字,覆盖了中国大陆99.75%的使用频率,基本满足了汉字的计算机处理需要。但对于人名、古汉语等方面出现的罕用字、生僻字,GB2312不能处理,如部分在GB2312-1980推出以后才简化的汉字(如“啰”)、部分人名用字(如前总理朱镕基的“镕”字)、台湾及香港使用的繁体字、日语及朝鲜语汉字等,并未收录在内。

1、为了便于多个文种的同时处理,国际标准化组织下属编码字符集工作组制定了新的编码字符集标准——ISO/IEC 10646(与统一联盟制定的Unicode标准兼容,两者的关系详见后文)。

该标准第一次颁布是在1993年,当时只颁布了其第一部分,即ISO/IEC 10646.1:1993,收录中国大陆、台湾、日本及韩国通用字符集的汉字,总共20,902个。制定这个标准的目的是对世界上的所有字符统一编码,以实现世界上所有字符在计算机上的统一处理。

2、中国相应的国家标准是GB13000.1-93《信息技术通用多八位编码字符集(UCS) 第一部分:体系结构与基本多文种平面》。GB 13000.1-93”等同于Unicode 1.1版本( 即 ISO/IEC 10646.1:1993)。

2010年又发布了替代标准——GB13000-2010《信息技术通用多八位编码字符集(UCS)》,此标准等同于国际标准ISO/IEC 10646:2003《信息技术通用多八位编码字符集(UCS)》。

GB13000与国际标准ISO/IEC 10646及Unicode标准目前在基本平面(即BMP,详见后文)上保持一致。

GB13000建立了一个全新的编码体系。ISO/IEC 10646被称作”多八位”编码字符集,是因为它采用四个”八位”(即8 bit)编码。这四个字节被用来分别表示组、平面、行和字位。
GB2312规定的汉字为常用汉字,包括简化汉字三千余个。由于我国汉字数量巨大(约10万字),我国又陆续增加了六个辅助集。其中,基本集与第二、第四辅助集是简化汉字集,第一(即GB 12345)、第三、第五辅助集是繁体集,且基本集与第一、第二与第三、第四与第五辅助集分别有简、繁体字一一对应关系,(个别简、繁关系为一对多的汉字除外)。第七辅助集汉字的来源是GB13000.1的CJK统一汉字部分,为日本、韩国和台湾地区使用的汉字。七个字符集包含汉字共计约49,000字(简化字和繁体字分别编码)。
可以看出,GB13000的总编码位置高达2,147,483,648个(128组×256平面×256行×256字位)。目前实现的是00组的00平面,称为”基本多文种平面”(Basic Multilingual Plane, BMP),编码位置65536个。(由于基本多文种平面所有字符代码的前两个字节都是0(00组00平面XX行XX字位),因此,目前在默认情况下,基本多文种平面按照两字节处理。

3、GBK

1、话说1993年,Unicode 1.1推出时,收录了两万多个中日韩通用字符集的汉字,同一年我国也定制了相应的GB13000,但是一直未被业界采用。后来微软利用了GB2312中未使用的编码空间,并且收录了GB13000中的全部字符,从而定制了GBK编码(虽然收录了GB13000的全部字符,但是编码方式并不相同),并且实现于Windows95中文版中。GBK自身并非国家标准,不过1995年由国标局等机构确定为“技术规范指导性文件”。【也有一说:国家主动 利用GB2312-1980未使用的码点空间,收录GB13000.1-1993的全部字符,于1995年又发布了《汉字内码扩展规范(GBK)》(Guo-Biao Kuozhan国家标准扩展码,是根据GB13000.1-1993,对GB2312-1980的扩展;英文全称Chinese Internal Code Specification)。】

2、不过,虽然GBK收录了GB13000.1-1993的全部字符,但编码方式并不相同。

GBK跟GB2312一样是双字节编码,然而,GBK只要求第一个字节即高字节是大于127就固定表示这是一个汉字的开始(0~127当然表示的还是ASCII字符),不再要求第二个字节即低字节也必须是127号之后的编码。这样,作为同样是双字节编码的GBK才可以收录比GB2312更多字符。

GBK字符集向后完全兼容GB2312,还支持GB2312-1980不支持的部分中文简体、中文繁体、日文假名,还包括希腊字母以及俄语字母等字母(不过这个编码不支持韩国文字,也是其在实际使用中与Unicode编码相比欠缺的部分),共收录汉字21003个、符号883个,并提供1894个造字码位,简、繁体字融于一体。

GBK全称叫《汉字内码扩展规范》,是国家技术监督局为 windows95 所制定的新的汉字内码规范,它的出现是为了扩展 GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,还包含繁体中文字,日文字符和朝鲜字符。它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。值得注意的是GBK只是一个规范而不是国家标准,新的国家标准是GB18030-2000,它是比GBK包含字符更多的字符集。

GBK的编码框架(Code Scheme):其中GBK1收录除GB2312符号外的增补符号,GBK2收录GB2312汉字,GBK3收录CJK汉字,GBK4收录CJK汉字和增补汉字,GBK5为非中文字符集,UDC为用户自定义字符区

3、微软早在Windows 95简体中文版中就采用了GBK编码,也就是对微软内部之前的CP936字码表(Code Page 936)进行了扩展(之前CP936和GB2312-1980一模一样)。

微软的CP936通常被视为等同于GBK,连IANA(Internet Assigned Numbers Authority互联网号码分配局)也以“CP936”为“GBK”之别名。

但事实上比较起来,GBK定义之字符较CP936多出95个(15个非汉字及80个汉字),皆为当时未收入ISO 10646 / Unicode的符号。

【注:有说是微软在GB2312的基础上扩展制订了GBK,然后GBK才成为“国家标准”(也有说GBK不是国家标准,只是“技术规范指导性文件”);但网上也有资料说是先有GBK(由全国信息技术标准化技术委员会于1995年12月1日制定),然后微软才在其内部所用的CP936代码页中以GBK为参考进行了扩展。】

简单总结:GBK是从GB2312扩展而来的,支持繁体,并且兼容GB2312。例如:“啊”字的GB2312编码和GBK编码都为0xB0A1。


5、GB18030

1、中国国家质量技术监督局于2000年3月17日推出了GB18030-2000标准,以取代GBK。GB18030-2000除保留全部GBK编码汉字之外,在第二字节再度进行扩展,增加了大约一百个汉字及四位元组编码空间。

GB18030-2000《信息交换用汉字编码字符集基本集的补充》是我国继GB2312-1980和GB13000-1993之后最重要的汉字编码标准,是我国计算机系统必须遵循的基础性标准之一。

2、2005年,GB18030编码方案又进行了扩充,于是又有了GB18030-2005《信息技术中文编码字符集》。如前所述,GB18030-2000是GBK的取代版本,它的主要特点是在GBK基础上增加了CJK中日韩统一表意文字扩充A的汉字;而GB18030-2005的主要特点是在GB18030-2000基础上又增加了CJK中日韩统一表意文字扩充B的汉字。

GB18030-2005全称是《信息交换用汉字编码字符集》,是我国的强制标准,它可能是单字节、双字节或者四字节编码,它的编码与 GB2312 编码兼容,这个虽然是国家标准,但是实际应用系统中使用的并不广泛。

GB2312和GBK都是用两个字节来编码的,就算用完所有的位(256*256=65536)也不够为所有的汉字编码。于是就有了目前最新的GB18030,它采用类似UTF-8的编码方式进行编码(每个字符的编码可以是1、2或4个字节),拥有上百万个编码空间,足以支持中日韩三国所有汉字,并且还可以支持国内少数民族的文字。

微软也为GB18030定义了代码页(Code page):CP54936,但是这个代码页实际上并没有真正使用(在Windows 7的“控制面板”-“区域和语言”-“管理”-“非Unicode程序的语言”中没有提供选项;在Windows cmd命令行中可通过命令chcp 54936更改,之后在cmd可显示中文,但却不支持中文输入)。

各汉字(中文字符)编码方案之间的关系

三、汉字编码小结:双字节编码(DBCS)

中国的程序员们看到这一系列汉字编码的标准是好的,于是通称他们叫做 “DBCS”(Double Byte Charecter Set 双字节字符集)【当然不包括GB18030编码方案】。在DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处理,必须要注意字串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了。那时候凡是受过加持,会编程的计算机僧侣们都要每天念下面这个咒语数百遍:“一个汉字算两个英文字符!一个汉字算两个英文字符……”【其实这句话的本意是:在双字节中文编码中,兼容了ASCII,导致英文编码只占一个字节,中文编码占二个字节,所以一个汉字算两个英文】

四、繁体编码BIG5与日文编码SJIS

维基上说:Big5是由台湾财团法人信息产业策进会为五大中文套装软件(并因此得名Big-5)所设计的中文共通内码,在1983年12月完成公告。那个之前还没有繁体字编码,GB2312又不含繁体字,因此才有了Big-5。

传说Big5产生前,有着“中文电脑之父”之称的朱邦复也设计了一套中文编码,可容纳50000多字(包括繁体和简体),但是未被采纳。

我国的台湾地区使用的文字是繁体字,其字符集是BIG5,而日本采用的字符集则是SJIS。它们的编码方法与GB2312类似,它们的ASCII字符部分是兼容的,但扩展部分的编码则是不兼容的,比如这几种字符集中都有”中文”这两个字符,但他们在各自的字符集中的编码并不相同,这就是用GB2312写成的网页用BIG5浏览时,看到的是乱糟糟的信息的原因。(BIg5也是双字节编码)

六、CJK中日韩统一表意文字

1、CJK指的是中日韩统一表意文字(CJK Unified Ideographs),也称统一汉字(Unihan),目的是要把分别来自中文(包含壮文)、日文、韩文、越文中,起源相同、本义相同、形状一样或稍异的表意文字在Unicode标准及ISO/IEC 10646标准内赋予相同的码点值。(Unicode标准及ISO/IEC 10646标准后文有详细解释)

CJK是中文(Chinese)、日文(Japanese)、韩文(Korean)三国文字英文首字母的缩写。顾名思义,它能够支持这三种文字,但实际上,CJK能够支持包括中文(包含壮文)、日文、韩文、越文在内的多种亚洲双字节文字。

2、所谓“起源相同、本义相同、形状一样或稍异的表意文字”,主要为汉字,包括繁体字、简体字;但也有仿汉字,包括方块壮字、日本汉字(漢字/かんじ)、韩国汉字(漢字/한자)、越南的喃字(?喃/Chữ Nôm)与儒字(?儒/Chữ Nho)等。

此计划原本只包含中文、日文及韩文中所使用的汉字和仿汉字,统称中日韩(CJK)统一表意文字(Unified Ideographs)。后来,此计划才加入了越南文的喃字,所以又合称为中日韩越(CJKV)统一表意文字。

好吧,它并不是一种编码方式,而是中日韩统一表意文字(CJK Unified Ideographs)。在Unicode中,收集各国相同的汉字,并且进行合并相同的编码点(code point)上,可以避免相同文字重复编码,浪费编码空间。


参考编码:

http://www.mytju.com/classcode/tools/encode_gb2312.asp

https://www.qqxiuzi.cn/bianma/Unicode-UTF.php

https://www.qqxiuzi.cn/zh/hanzi-big5-bianma.php

参考编码:

http://www.cnblogs.com/malecrab/p/5300497.html

https://www.cnblogs.com/duguxiaobiao/p/9128817.html

https://www.cnblogs.com/malecrab/p/5300503.html

https://blog.csdn.net/garybrother/article/details/3988741

http://zzqhost.com/2017/07/05/%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81/

 

 

计算机寻址-按字节、位、字寻址

寻址

在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址,毕竟位(bit)所能表示的信息太有限了(0 or 1)。

例如,在java中一个int变量占4个字节,假设一个类型为int的变量a的地址为0x100,那么,a的4个字节将被存储在存储器的0x100、0x101、0x102和0x103的位置。


现代个人计算机(PC)的存储器编址,一般是以字节为单位的,称之为按字节编址,因此字节一般也是存储器的最小存取单元以及处理器的最小寻址单位(也有按位寻址、按字寻址等等,但在个人计算机上应用不普遍)。

按字节寻址,存储空间的最小编址单位是字节,
按字编址,存储空间的最小编址单位是字,
最小编址单位的选择,和存储容量、地址总线宽度都有关联 。同样的存储容量, 粒度小了,地址长度就需要更长 。
就是根据不同的方式寻找内存地址,计算机中大多数寄存器的尺寸是一个字长。计算机处理的典型数值也可能是以字长为单位。CPU和内存之间的数据传送单位也通常是一个字长。

字(word)      占2字节
字节(byte)    占8位
位(bit)      最小的单位

比特就是 bit ,就是位。

1字=2字节
1字节=8位
1字=2*8=16位

设有一个1mb容量的存储器,字长32位,

(i)问:按字节编址,地址寄存器、数据寄存器各为多少位?

1、按字节编址 1MB = 2^20B 1个字节=8bit=1B 2^20B/1B = 2^20 地址范围为0~2^20-1 也就是说至少需要二十根地址线,地址寄存器是用来存放地址的,与存储器容量及编址方式有关,可以简单的认为地址线的个数等于地址寄存器的位数,所以地址寄存器为20位。
2、数据寄存器用来存放CPU在一个存取周期内从存储器中一次性取出为二进制位数,也就是一个机器字长,本题中字长32位,所以数据寄存器为32位。
3、寻址空间。在此需要区分清楚寻址空间与寻址范围两个不同的概念,范围仅仅是一个数字范围,不带有单位,而寻址空间指能够寻址最大容量,单位一般用MB、B来表示;本题中寻址范围为0~2^20-1,寻址空间为1MB。

(ii)问:按字节编址,字编址的寻址范围以及各自的寻址范围大小?

如果按字节编址,则

1MB = 2^20B

1字节=1B=8bit

2^20B/1B = 2^20

地址范围为0~(2^20)-1,也就是说需要二十根地址线才能完成对1MB空间的编码,所以地址寄存器为20位,寻址范围大小为2^20=1M

如果按字编址,则

1MB=2^20B

1字=32bit=4B

2^20B/4B = 2^18

地址范围为0~2^18-1,也就是说我们至少要用18根地址线才能完成对1MB空间的编码。因此按字编址的寻址范围是2^18

以上题目注意几点:

1.区分寻址空间与寻址范围两个不同的概念,寻址范围仅仅是一个数字范围,不带有单位

而寻址范围的大小很明显是一个数,指寻址区间的大小

  而寻址空间指能够寻址最大容量,单位一般用MB、B来表示;本题中寻址范围为0~(2^20)-1,寻址空间为1MB。

2.按字节寻址,指的是存储空间的最小编址单位是字节,按字编址,是指存储空间的最小编址单位是字,以上题为例,总的存储器容量是一定的,按字编址和按字节编址所需要的编码数量是不同的,按字编址由于编址单位比较大(1字=32bit=4B),从而编码较少,而按字节编址由于编码单位较小(1字节=1B=8bit),从而编码较多。

3.区别M和MB。

                        M为数量单位。1024=1K,1024K=1M

                        MB指容量大小。1024B=1KB,1024KB=1MB.


Intel公司早期的CPU产品的地址总线和地址寄存器的宽度为20位,即CPU的寻址能力为2^20=1024*1024字节=1024K字节=1M字节;

INTEL1982年推出80286芯片(简称286),其内部和外部数据总线皆为16位(也就是数据寄存器16位,字长16位)。地址总线和地址寄存器是24位(也就是可寻址16MB内存 ,即  2^24=1024*4*1024*4B=4*1024*4KB=16M;)

Intel 80386,是英特尔(Intel)公司的一款x86系列CPU。80386处理器被广泛应用在1980年代中期到1990年代中期的IBM PC兼容机中。这些PC被称为“80386电脑”或“386电脑”,有时也简称“80386”或“386”。

80386的广泛应用,将PC从16位时代带入了32位时代。80386的强大运算能力也使PC机的应用领域得到巨大扩展,商业办公、科学计算、工程设计、多媒体处理等应用得到迅速发展。

80386DX的内部和外部数据总线是32位,地址总线也是32位,可以寻址到4GB存储器,并可以管理64MB的虚拟存储空间。

80386-DX:主流版本。内部和外部数据总线都是32 位,地址总线也是32位。
80386-SX:1988年末推出的廉价版本。外部数据总线16位,地址总线24位,与80286相同,从而方便80286电脑的升级。由于内部的32位结构及其他优化设计,80386-SX性能仍大大优于80286,而价格只相当于80386-DX的三分之一,因而很受市场的欢迎。与之相配的数学辅助处理器型号为80387-SX。
80386-SL:1990年推出的低功耗版本,基于80386-SX。增加了系统管理方式(SMM)工作模式,具有电源管理功能,可以自动降低运行速度乃至休眠状态以实现节能。
80386-DL:1990年推出的低功耗版本,基于80386-DX。与80386-SL类似。

Intel里面64位CPU到奔4的D,E系列才出,前面奔2,3,4都是32位的。


参考:http://blog.sina.com.cn/s/blog_6040778d0100xycp.html

参考:https://blog.csdn.net/lishuhuakai/article/details/8934540

计算机存储-计算机存储单元是字节

数据必须首先在计算机内被表示,然后才能被计算机处理。计算机表示数据的部件主要是存储设备;而存储数据的具体单位是存储单元;因此,了解存储单元的结构是十分必要的。
(1)”位”(Bit):是计算机中最小的信息单位。一”位”只能表示0和1中的一个,即一个二进制位,或存储一个二进制数位的单位。
(2)”字节”(Byte):是由相连8个位组成的信息存储单位。
字节是目前计算机最基本的存储单位;也是计算机存储设备容量最基本的计量单位。一个字节通常可以存储一个字符(如字母、数字等)【具体要看是什么字符编码,有些编码两个字节才表示一个字符】。只有字节才有地址的概念。对一种计算机的存储设备以字节为单位赋予的地址称为字节编址;也是目前计算机最基本的存储单元编址。

http协议-浏览器和服务器协商http协议版本的过程探讨

How does a HTTP 1.0 only server respond to a HTTP 1.1 request?

Fairly simple question (I think). If a client sends a message like GET /whatever HTTP/1.1 to a server that only supports HTTP 1.0, how does the server react? What are the rules for header fields added in HTTP 1.1 that a HTTP 1.0 server doesn’t recognise? Does the server simply ignore 1.1 requests, ignore headers it doesn’t understand, return an error, or something else?

answer:

HTTP/1.0 200 OK

All headers of HTTP/1.1 (that a HTTP 1.0 server doesn’t recognise) will be ignored.

总之:http 目前支持协议 协商的只有 http1.1 ,浏览器通过 https 协商过程 或者直接用 upgrade 请求头  与服务器 进行协议协商。其他情况,客户端 服务端支持的最高http协议如果相同,则以最高http协议通信。服务端比客户端支持的最高http协议低,服务器会直接 忽略不能识别的请求头。

目前的浏览器,都是采用 http1.1 协议  率先进行 通信,然后 看看服务器 能不能支持 协议 升级,通过http2.0 进行通信。