hjava

hjava 查看完整档案

北京编辑华中科技大学  |  计算机学院 编辑美团网  |  高级前端工程师 编辑填写个人主网站
编辑

大象Web SDK负责人 && 大象小程序负责人

个人动态

hjava 评论了文章 · 2019-05-10

WebSocket系列之二进制数据设计与传输

概述

通过前三篇博客,我们能够了解在通过WebSocket发送数据之前,我们需要传递的数据是如何变成ArrayBuffer二进制数据的;在我们收到二进制数据之后,我们又如何将其变成了JavaScript中的常见数据类型。
本文作为WebSocket系列的第四篇内容,将会用一个简单的IM聊天应用把整个WebSocket传输二进制数据类型的内容连接起来,让用户对整个WebSocket传输二进制数据的方法有个了解。
本文的主要内容如下:

  • 如何设计一个二进制协议
  • WebSocket如何发送二进制数据
  • WebSocket如何处理接收的二进制数据

之前的博客我们介绍过了WebSocket基础知识,数字类型和字符串类型与二进制数据间的转换,如果没有相关的基础,建议先依次阅读以下文章:

如何设计一个二进制协议

什么是协议

协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。它的三要素是:语法、语义、时序。

通过百度百科中的介绍,我们对协议的概念有了一个基础的了解。通俗来说,协议就是通信双方约定好的一套规则。

为什么要设计协议

没有规矩不成方圆。通信双方只有通过协议,才能够识别对方发送的数据内容。

我们应该如何设计这套协议

首先,协议的设计应该能够区分不同的各个数据包;其次,它还需要具备一定的兼容性。
根据上述两点要求,我们设计了一套简单的IM聊天协议,支持文本、图片、文件三种消息。这套协议是按照最简单的思路来设计的,因此也只是给大家一个参考的观点,在真正的线上使用场景中,协议会比本文中的复杂和更加有层次。具体格式如下:

{
    "id": "short", // 消息类型,1是文本协议格式;2是图片协议格式;3是文件协议格式
    "sender": "long", // 发送用户唯一id
    "reciever": "long", // 接受用户唯一id
    "data": "string" // 消息内容,如果是文本协议则为文本内容;如果是图片协议则为图片地址;如果是文件协议则为文件地址
}

这套协议如何使用

发送消息

从协议格式可知,将上述数据按照上述固定顺序放入ArrayBuffer中,即可得到一个有特定含义的二进制数据。例如:

{
    "id": 1,
    "sender": "123",
    "reciever": "456",
    "data": "Hellow World"
}

当我们需要发送此消息时,只需要:

  1. 在前2个Byte放入id
  2. 接下来8个Byte中放入sender
  3. 再接下来8个Byte放入reciever
  4. 最后紧接着放入一个string类型(以WebSocket系列之字符串如何与二进制数据进行转换博客中的格式为例,先将字符串长度构造成一个int类型,放在前4个Byte中,接下来将string类型编码后放入)。

此数据就完全按照协议构造完成了。我们只需将次协议通过WebSocket发送即可。具体方法将会在后面章节中说明。

接收消息

从协议格式可知,当我们收到一条消息时,只需要按照协议规范来进行反向解析即可。例如:

{
    "id": 1,
    "sender": "123",
    "reciever": "456",
    "data": "Hellow World"
}

如果发送端发送的数据仍然为此消息,我们的解析顺序为:

  1. 先根据前2个Byte读取一个Short类型的id数值。
  2. 将接下来的8个Byte读取为Long类型的sender字段。
  3. 再接下来的8个Byte读取为Long类型的reciever字段。
  4. 接下来读取一个string类型(以发送消息这一节的数据为例,先读取4个Byte长度的int类型字符串长度,然后再根据长度读取字符串即可)。

扩展此协议

当此协议字段无法满足并且已经在线上使用时,我们应该如何扩展呢?
根据我们的写入和读取步骤,我们可以知道:每次我们读取的二进制数据可以认为是一个格式固定的数据(string类型在构造时会有长度信息,因此认为也是长度相对固定),所以我们在读取二进制数据时读取的长度也是固定的。因此,我们在扩展时只需要往协议后面增加字段即可。

  • 扩展前的应用仍然会读取之前已经确定的数据协议,只需要保证内容不变,那么功能也不会变。
  • 扩展后的应用能够解析扩展后的协议,因此得到更多的数据,从而可以实现更多的功能。

WebSocket如何发送二进制数据

通过如何设计一个二进制协议一章,我们知道了如何定义WebSocket传输的二进制数据格式。下面,我们来看下如何在WebSocket中发送二进制数据:

let arrayBuffer = getArrayBufferMessagesFromUser(); // 获取用户需要发送的消息数据,为一个ArrayBuffer对象
let webSocket = getWebSocket(); // 获取已经连接成功的WebSocket实例

websocket.binaryType = 'arraybuffer'; // 指定WebSocket接受ArrayBuffer实例作为参数

webSocket.send(arrayBuffer);

通过上面的示例我们可以知道,WebSocket在发送string类型的数据或者ArrayBuffer类型的数据时,使用的API接口都是send方法,我们只需要在WebSocket初始化后指定传输类型binaryType即可。

WebSocket如何处理接收的二进制数据

通过WebSocket如何发送二进制数据一章,我们知道了如何发送二进制数据。接下来,让我们开看下如何处理WebSocket接收到的二进制数据:

let webSocket = getWebSocket(); // 获取已经连接成功的WebSocket实例

websocket.binaryType = 'arraybuffer'; // 指定WebSocket接受ArrayBuffer实例作为参数

webSocket.addEventListener('message', (message) => {
    let arrayBuffer = message.data; // 获取用户接收到的消息数据,为一个ArrayBuffer对象
    let data = parseMessage(arrayBuffer); // 解析二进制数据
});

通过上面的示例我们可以知道,当我们在建立连接时指定了传输类型binaryType为ArrayBuffer之后,我们通过WebSocket收到的数据也是一个ArrayBuffer实例。我们只需要根据第一章讲解的方式进行解析即可。

总结

本文作为WebSocket系列的第四篇,通过一个IM聊天应用的示例将前三篇博客分享的内容串联起来,给读者完整介绍了在WebSocket中使用二进制数据进行传输的方法以及相关的数据类型转换。
通过前面4篇博客的内容,读者可以根据自己的需求快速的构造和封装WebSocket进行二进制数据传输,基本能够串联整个应用流程。
WebSocket系列下一篇文章将会介绍在线上环境中,如何保证WebSocket的连接,以及线上可能遇到的问题。通过应对WebSocket可能出现的问题,我们能够让整个长连接更加健壮。

查看原文

hjava 发布了文章 · 2019-03-13

如何实现一个虚拟 DOM——virtual-dom 源码分析

概述

本文通过对virtual-dom的源码进行阅读和分析,针对Virtual DOM的结构和相关的Diff算法进行讲解,让读者能够对整个数据结构以及相关的Diff算法有一定的了解。

Virtual DOM中Diff算法得到的结果如何映射到真实DOM中,我们将在下一篇博客揭晓。

本文的主要内容为:

  • Virtual DOM的结构
  • Virtual DOM的Diff算法

注:这个Virtual DOM的实现并不是React Virtual DOM的源码,而是基于virtual-dom)这个库。两者在原理上类似,并且这个库更加简单容易理解。相较于这个库,React对Virtual DOM做了进一步的优化和调整,我会在后续的博客中进行分析。

Virtual DOM的结构

VirtualNode

作为Virtual DOM的元数据结构,VirtualNode位于vnode/vnode.js文件中。我们截取一部分声明代码来看下内部结构:

function VirtualNode(tagName, properties, children, key, namespace) {
    this.tagName = tagName
    this.properties = properties || noProperties //props对象,Object类型
    this.children = children || noChildren //子节点,Array类型
    this.key = key != null ? String(key) : undefined
    this.namespace = (typeof namespace === "string") ? namespace : null
    
    ...

    this.count = count + descendants
    this.hasWidgets = hasWidgets
    this.hasThunks = hasThunks
    this.hooks = hooks
    this.descendantHooks = descendantHooks
}

VirtualNode.prototype.version = version //VirtualNode版本号,isVnode()检测标志
VirtualNode.prototype.type = "VirtualNode" // VirtualNode类型,isVnode()检测标志

上面就是一个VirtualNode的完整结构,包含了特定的标签名、属性、子节点等。

VText

VText是一个纯文本的节点,对应的是HTML中的纯文本。因此,这个属性也只有text这一个字段。

function VirtualText(text) {
    this.text = String(text)
}

VirtualText.prototype.version = version
VirtualText.prototype.type = "VirtualText"

VPatch

VPatch是表示需要对Virtual DOM执行的操作记录的数据结构。它位于vnode/vpatch.js文件中。我们来看下里面的具体代码:

// 定义了操作的常量,如Props变化,增加节点等
VirtualPatch.NONE = 0
VirtualPatch.VTEXT = 1
VirtualPatch.VNODE = 2
VirtualPatch.WIDGET = 3
VirtualPatch.PROPS = 4
VirtualPatch.ORDER = 5
VirtualPatch.INSERT = 6
VirtualPatch.REMOVE = 7
VirtualPatch.THUNK = 8

module.exports = VirtualPatch

function VirtualPatch(type, vNode, patch) {
    this.type = Number(type) //操作类型
    this.vNode = vNode //需要操作的节点
    this.patch = patch //需要操作的内容
}

VirtualPatch.prototype.version = version
VirtualPatch.prototype.type = "VirtualPatch"

其中常量定义了对VNode节点的操作。例如:VTEXT就是增加一个VText节点,PROPS就是当前节点有Props属性改变。

Virtual DOM的Diff算法

了解了虚拟DOM中的三个结构,那我们下面来看下Virtual DOM的Diff算法。

这个Diff算法是Virtual DOM中最核心的一个算法。通过输入初始状态A(VNode)和最终状态B(VNode),这个算法可以得到从A到B的变化步骤(VPatch),根据得到的这一连串步骤,我们就可以知道哪些节点需要新增,哪些节点需要删除,哪些节点的属性有了变化。在这个Diff算法中,又分成了三部分:

  • VNode的Diff算法
  • Props的Diff算法
  • Vnode children的Diff算法

下面,我们就来一个一个介绍这些Diff算法。

VNode的Diff算法

该算法是针对于单个VNode的比较算法。它是用于两个树中单个节点比较的场景。具体算法如下,如果不想直接阅读源码的同学也可以翻到下面,会有相关代码流程说明供大家参考:

function walk(a, b, patch, index) {
    if (a === b) {
        return
    }

    var apply = patch[index]
    var applyClear = false

    if (isThunk(a) || isThunk(b)) {
        thunks(a, b, patch, index)
    } else if (b == null) {

        // If a is a widget we will add a remove patch for it
        // Otherwise any child widgets/hooks must be destroyed.
        // This prevents adding two remove patches for a widget.
        if (!isWidget(a)) {
            clearState(a, patch, index)
            apply = patch[index]
        }

        apply = appendPatch(apply, new VPatch(VPatch.REMOVE, a, b))
    } else if (isVNode(b)) {
        if (isVNode(a)) {
            if (a.tagName === b.tagName &&
                a.namespace === b.namespace &&
                a.key === b.key) {
                var propsPatch = diffProps(a.properties, b.properties)
                if (propsPatch) {
                    apply = appendPatch(apply,
                        new VPatch(VPatch.PROPS, a, propsPatch))
                }
                apply = diffChildren(a, b, patch, apply, index)
            } else {
                apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
                applyClear = true
            }
        } else {
            apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
            applyClear = true
        }
    } else if (isVText(b)) {
        if (!isVText(a)) {
            apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
            applyClear = true
        } else if (a.text !== b.text) {
            apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
        }
    } else if (isWidget(b)) {
        if (!isWidget(a)) {
            applyClear = true
        }

        apply = appendPatch(apply, new VPatch(VPatch.WIDGET, a, b))
    }

    if (apply) {
        patch[index] = apply
    }

    if (applyClear) {
        clearState(a, patch, index)
    }
}

代码具体逻辑如下:

  1. 如果ab这两个VNode全等,则认为没有修改,直接返回。
  2. 如果其中有一个是thunk,则使用thunk的比较方法thunks
  3. 如果a是widget且b为空,那么通过递归将a和它的子节点的remove操作添加到patch中。
  4. 如果b是VNode的话,

    1. 如果a也是VNode,那么比较tagNamenamespacekey,如果相同则比较两个VNode的Props(用下面提到的diffProps算法),同时比较两个VNode的children(用下面提到的diffChildren算法);如果不同则直接将b节点的insert操作添加到patch中,同时将标记位置为true。
    2. 如果a不是VNode,那么直接将b节点的insert操作添加到patch中,同时将标记位置为true。
  5. 如果b是VText的话,看a的类型是否为VText,如果不是,则将VText操作添加到patch中,并且将标志位设置为true;如果是且文本内容不同,则将VText操作添加到patch中。
  6. 如果b是Widget的话,看a的类型是否为widget,如果是,将标志位设置为true。不论a类型为什么,都将Widget操作添加到patch中。
  7. 检查标志位,如果标识为为true,那么通过递归将a和它的子节点的remove操作添加到patch中。

这就是单个VNode节点的diff算法全过程。这个算法是整个diff算法的入口,两棵树的比较就是从这个算法开始的。

Prpps的Diff算法

看完了单个VNode节点的diff算法,我们来看下上面提到的diffProps算法。

该算法是针对于两个比较的VNode节点的Props比较算法。它是用于两个场景中key值和标签名都相同的情况。具体算法如下,如果不想直接阅读源码的同学也可以翻到下面,会有相关代码流程说明供大家参考:

function diffProps(a, b) {
    var diff

    for (var aKey in a) {
        if (!(aKey in b)) {
            diff = diff || {}
            diff[aKey] = undefined
        }

        var aValue = a[aKey]
        var bValue = b[aKey]

        if (aValue === bValue) {
            continue
        } else if (isObject(aValue) && isObject(bValue)) {
            if (getPrototype(bValue) !== getPrototype(aValue)) {
                diff = diff || {}
                diff[aKey] = bValue
            } else if (isHook(bValue)) {
                 diff = diff || {}
                 diff[aKey] = bValue
            } else {
                var objectDiff = diffProps(aValue, bValue)
                if (objectDiff) {
                    diff = diff || {}
                    diff[aKey] = objectDiff
                }
            }
        } else {
            diff = diff || {}
            diff[aKey] = bValue
        }
    }

    for (var bKey in b) {
        if (!(bKey in a)) {
            diff = diff || {}
            diff[bKey] = b[bKey]
        }
    }

    return diff
}

代码具体逻辑如下:

  1. 遍历a对象。

    1. 当key值不存在于b,则将此值存储下来,value赋值为undefined
    2. 当此key对应的两个属性都相同时,继续终止此次循环,进行下次循环。
    3. 当key值对应的value不同且key值对应的两个value都是对象时,判断Prototype值,如果不同则记录key对应的b对象的值;如果b对应的value是hook的话,记录b的值。
    4. 上面条件判断都不同且都是对象时,则继续比较key值对应的两个对象(递归)。
    5. 当有一个不是对象时,直接将b对应的value进行记录。
  2. 遍历b对象,将所有a对象中不存在的key值对应的对象都记录下来。

整个算法的大致流程如下,因为比较简单,就不画相关流程图了。如果逻辑有些绕的话,可以配合代码食用,效果更佳。

Vnode children的Diff算法

下面让我们来看下最后一个算法,就是关于两个VNode节点的children属性的diffChildren算法。这个个diff算法分为两个部分,第一部分是将变化后的结果b的children进行顺序调整的算法,保证能够快速的和a的children进行比较;第二部分就是将a的children与重新排序调整后的b的children进行比较,得到相关的patch。下面,让我们一个一个算法来看。

reorder算法

该算法的作用是将b节点的children数组进行调整重新排序,让ab两个children之间的diff算法更加节约时间。具体代码如下:

function reorder(aChildren, bChildren) {
    // O(M) time, O(M) memory
    var bChildIndex = keyIndex(bChildren)
    var bKeys = bChildIndex.keys  // have "key" prop,object
    var bFree = bChildIndex.free  //don't have "key" prop,array

    // all children of b don't have "key"
    if (bFree.length === bChildren.length) {
        return {
            children: bChildren,
            moves: null
        }
    }

    // O(N) time, O(N) memory
    var aChildIndex = keyIndex(aChildren)
    var aKeys = aChildIndex.keys
    var aFree = aChildIndex.free

    // all children of a don't have "key"
    if (aFree.length === aChildren.length) {
        return {
            children: bChildren,
            moves: null
        }
    }

    // O(MAX(N, M)) memory
    var newChildren = []

    var freeIndex = 0
    var freeCount = bFree.length
    var deletedItems = 0

    // Iterate through a and match a node in b
    // O(N) time,
    for (var i = 0 ; i < aChildren.length; i++) {
        var aItem = aChildren[i]
        var itemIndex

        if (aItem.key) {
            if (bKeys.hasOwnProperty(aItem.key)) {
                // Match up the old keys
                itemIndex = bKeys[aItem.key]
                newChildren.push(bChildren[itemIndex])

            } else {
                // Remove old keyed items
                itemIndex = i - deletedItems++
                newChildren.push(null)
            }
        } else {
            // Match the item in a with the next free item in b
            if (freeIndex < freeCount) {
                itemIndex = bFree[freeIndex++]
                newChildren.push(bChildren[itemIndex])
            } else {
                // There are no free items in b to match with
                // the free items in a, so the extra free nodes
                // are deleted.
                itemIndex = i - deletedItems++
                newChildren.push(null)
            }
        }
    }

    var lastFreeIndex = freeIndex >= bFree.length ?
        bChildren.length :
        bFree[freeIndex]

    // Iterate through b and append any new keys
    // O(M) time
    for (var j = 0; j < bChildren.length; j++) {
        var newItem = bChildren[j]

        if (newItem.key) {
            if (!aKeys.hasOwnProperty(newItem.key)) {
                // Add any new keyed items
                // We are adding new items to the end and then sorting them
                // in place. In future we should insert new items in place.
                newChildren.push(newItem)
            }
        } else if (j >= lastFreeIndex) {
            // Add any leftover non-keyed items
            newChildren.push(newItem)
        }
    }

    var simulate = newChildren.slice()
    var simulateIndex = 0
    var removes = []
    var inserts = []
    var simulateItem

    for (var k = 0; k < bChildren.length;) {
        var wantedItem = bChildren[k]
        simulateItem = simulate[simulateIndex]

        // remove items
        while (simulateItem === null && simulate.length) {
            removes.push(remove(simulate, simulateIndex, null))
            simulateItem = simulate[simulateIndex]
        }

        if (!simulateItem || simulateItem.key !== wantedItem.key) {
            // if we need a key in this position...
            if (wantedItem.key) {
                if (simulateItem && simulateItem.key) {
                    // if an insert doesn't put this key in place, it needs to move
                    if (bKeys[simulateItem.key] !== k + 1) {
                        removes.push(remove(simulate, simulateIndex, simulateItem.key))
                        simulateItem = simulate[simulateIndex]
                        // if the remove didn't put the wanted item in place, we need to insert it
                        if (!simulateItem || simulateItem.key !== wantedItem.key) {
                            inserts.push({key: wantedItem.key, to: k})
                        }
                        // items are matching, so skip ahead
                        else {
                            simulateIndex++
                        }
                    }
                    else {
                        inserts.push({key: wantedItem.key, to: k})
                    }
                }
                else {
                    inserts.push({key: wantedItem.key, to: k})
                }
                k++
            }
            // a key in simulate has no matching wanted key, remove it
            else if (simulateItem && simulateItem.key) {
                removes.push(remove(simulate, simulateIndex, simulateItem.key))
            }
        }
        else {
            simulateIndex++
            k++
        }
    }

    // remove all the remaining nodes from simulate
    while(simulateIndex < simulate.length) {
        simulateItem = simulate[simulateIndex]
        removes.push(remove(simulate, simulateIndex, simulateItem && simulateItem.key))
    }

    // If the only moves we have are deletes then we can just
    // let the delete patch remove these items.
    if (removes.length === deletedItems && !inserts.length) {
        return {
            children: newChildren,
            moves: null
        }
    }

    return {
        children: newChildren,
        moves: {
            removes: removes,
            inserts: inserts
        }
    }
}

下面,我们来简单介绍下这个排序算法:

  1. 检查ab中的children是否拥有key字段,如果没有,直接返回b的children数组。
  2. 如果存在,初始化一个数组newChildren,遍历a的children元素。

    1. 如果aChildren存在key值,则去bChildren中找对应key值,如果bChildren存在则放入新数组中,不存在则放入一个null值。
    2. 如果aChildren不存在key值,则从bChildren中不存在key值的第一个元素开始取,放入新数组中。
  3. 遍历bChildren,将所有achildren中没有的key值对应的value或者没有key,并且没有放入新数组的子节点放入新数组中。
  4. 将bChildren和新数组逐个比较,得到从新数组转换到bChildren数组的move操作patch(即remove+insert)。
  5. 返回新数组和move操作列表。

通过上面这个排序算法,我们可以得到一个新的b的children数组。在使用这个数组来进行比较厚,我们可以将两个children数组之间比较的时间复杂度从o(n^2)转换成o(n)。具体的方法和效果我们可以看下面的DiffChildren算法。

DiffChildren算法

function diffChildren(a, b, patch, apply, index) {
    var aChildren = a.children
    var orderedSet = reorder(aChildren, b.children)
    var bChildren = orderedSet.children

    var aLen = aChildren.length
    var bLen = bChildren.length
    var len = aLen > bLen ? aLen : bLen

    for (var i = 0; i < len; i++) {
        var leftNode = aChildren[i]
        var rightNode = bChildren[i]
        index += 1

        if (!leftNode) {
            if (rightNode) {
                // Excess nodes in b need to be added
                apply = appendPatch(apply,
                    new VPatch(VPatch.INSERT, null, rightNode))
            }
        } else {
            walk(leftNode, rightNode, patch, index)
        }

        if (isVNode(leftNode) && leftNode.count) {
            index += leftNode.count
        }
    }

    if (orderedSet.moves) {
        // Reorder nodes last
        apply = appendPatch(apply, new VPatch(
            VPatch.ORDER,
            a,
            orderedSet.moves
        ))
    }

    return apply
}

通过上面的重新排序算法整理了以后,两个children比较就只需在相同下标的情况下比较了,即aChildren的第N个元素和bChildren的第N个元素进行比较。然后较长的那个元素做insert操作(bChildren)或者remove操作(aChildren)即可。最后,我们将move操作再增加到patch中,就能够抵消我们在reorder时对整个数组的操作。这样只需要一次便利就得到了最终的patch值。

总结

整个Virtual DOM的diff算法设计的非常精巧,通过三个不同的分部算法来实现了VNode、Props和Children的diff算法,将整个Virtual DOM的的diff操作分成了三类。同时三个算法又互相递归调用,对两个Virtual DOM数做了一次(伪)深度优先的递归比较。

下面一片博客,我会介绍如何将得到的VPatch操作应用到真实的DOM中,从而导致HTML树的变化。

查看原文

赞 2 收藏 1 评论 0

hjava 发布了文章 · 2019-02-20

WebSocket 协议 RFC 文档(全中文翻译)

概述

经过半年的捣鼓,终于将 WebSocket 协议(RFC6455)全篇翻译完成。现在将所有章节全部整理到一篇文章中,方便大家阅读。如果大家想看具体的翻译文档,可以去我的GitHub中查看。

具体章节如下:

总结

通过翻译一篇文档,能够从头到尾精细的读完一篇文章,比较适合需要精读的文章和内容。大家有相关类型的需要,建议大家可以尝试下。

查看原文

赞 13 收藏 10 评论 0

hjava 发布了文章 · 2019-02-20

【译】 WebSocket 协议第六章——发送与接收消息(Sending and Receiving Data)

概述

本文为 WebSocket 协议的第六章,本文翻译的主要内容为 WebSocket 消息发送与接收相关内容。

发送与接收消息(协议正文)

6.1 发送数据

为了通过 WebSocket 连接发送一条 WebSocket 消息,终端必须遵循以下几个步骤:

  1. 终端必须保证 WebSocket 连接处于 OPEN 状态(见第 4.1 节和第 4.2.2 节)。如果 WebSocket 连接的任意一端的状态发生了改变,终端必须中止以下步骤。
  2. 终端必须将数据按照第 5.2 节定义的 WebSocket 帧进行封装。如果需要发送的数据过大或者在终端希望开始发消息时,如果数据在整体性这一点上不可用,那么终端可能会选择通过在第 5.4 节中定义的一系列帧来进行封装。
  3. 包含数据的第一帧操作码(帧操作码)必须根据第 5.2 节中的内容设置的合适的值,以便接收者将数据解析为文本或者二进制数据。
  4. 最后一个包含数据的帧的 FIN ( FIN 帧)字段必须和第 5.2 节中定义的一样设置为 1 。
  5. 如果数据被发送到了客户端,数据帧必须和第 5.3 节中定义的一样添加掩码。
  6. 如果在 WebsSocket 连接中有协商扩展(第 9 章),在这些扩展中的定义和注意事项也许要额外考虑。
  7. 被格式化的帧必须通过底层的网络连接进行传输。

6.2 接收数据

为了接收 WebSocket 数据,终端需要监听底层网络连接。输入的数据必须通过第 5.2 节定义的 WebSocket 帧进行解析。如果收到了一个控制帧(第 5.5 节),那么这个帧必须如 5.5 节中定义的方式进行处理。如果收到的是一个数据帧,那么终端必须注意 5.2 节中的定义在操作码(帧操作码)中的数据类型。在这一帧中的“应用数据”被定义为消息的数据。如果帧中包含未分片的数据(第 5.4 节),那么就认为:一条 WebSocket 消息的数据和类型被收到了。如果帧是分片数据的一部分,那么随后的帧包含的“应用数据”连起来就是数据的格式。当通过 FIN 字段(FIN帧)表示的最后一个片段被收到时,我们可以说:一条 WebSocket 消息的数据(由片段组装起来的“应用数据”数据组成)和类型(注意分片消息的第一帧)已经被收到了。接下来的数据帧必须是属于一条新的 WebSocket 消息。

扩展(第 9 章)可能改变数据如何理解的方式,具体包括消息的内容边界。扩展,除了在“应用数据”之前添加“扩展数据”之外,也可以修改“应用数据”(例如压缩它)。

像第 5.3 节中说的那样,服务端在收到客户端的数据帧时必须去除掩码。

查看原文

赞 2 收藏 1 评论 0

hjava 发布了文章 · 2019-02-20

【译】WebSocket协议第五章——数据帧(Data Framing)

概述

本文为WebSocket协议的第五章,本文翻译的主要内容为WebSocket传输的数据相关内容。

数据帧(协议正文)

5.1 概览

在WebSocket协议中,数据是通过一系列数据帧来进行传输的。为了避免由于网络中介(例如一些拦截代理)或者一些在第10.3节讨论的安全原因,客户端必须在它发送到服务器的所有帧中添加掩码(Mask)(具体细节见5.3节)。(注意:无论WebSocket协议是否使用了TLS,帧都需要添加掩码)。服务端收到没有添加掩码的数据帧以后,必须立即关闭连接。在这种情况下,服务端可以发送一个在7.4.1节定义的状态码为1002(协议错误)的关闭帧。服务端禁止在发送数据帧给客户端时添加掩码。客户端如果收到了一个添加了掩码的帧,必须立即关闭连接。在这种情况下,它可以使用第7.4.1节定义的1002(协议错误)状态码。(这些规则可能会在将来的规范中放开)。

基础的数据帧协议使用操作码、有效负载长度和在“有效负载数据”中定义的放置“扩展数据”与“引用数据”的指定位置来定义帧类型。特定的bit位和操作码为将来的协议扩展做了保留。

一个数据帧可以在开始握手完成之后和终端发送了一个关闭帧之前的任意一个时间通过客户端或者服务端进行传输(第5.5.1节)。

5.2 基础帧协议

在这节中的这种数据传输部分的有线格式是通过ABNFRFC5234来进行详细说明的。(注意:不像这篇文档中的其他章节内容,在这节中的ABNF是对bit组进行操作。每一个bit组的长度是在评论中展示的。在线上编码时,最高位的bit是在ABNF最左边的)。对于数据帧的高级的预览可以见下图。如果下图指定的内容和这一节中后面的ABNF指定的内容有冲突的话,以下图为准。

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

FIN: 1 bit

​ 表示这是消息的最后一个片段。第一个片段也有可能是最后一个片段。

RSV1,RSV2,RSV3: 每个1 bit

​ 必须设置为0,除非扩展了非0值含义的扩展。如果收到了一个非0值但是没有扩展任何非0值的含义,接收终端必须断开WebSocket连接。

Opcode: 4 bit

​ 定义“有效负载数据”的解释。如果收到一个未知的操作码,接收终端必须断开WebSocket连接。下面的值是被定义过的。

​ %x0 表示一个持续帧

​ %x1 表示一个文本帧

​ %x2 表示一个二进制帧

​ %x3-7 预留给以后的非控制帧

​ %x8 表示一个连接关闭包

​ %x9 表示一个ping包

​ %xA 表示一个pong包

​ %xB-F 预留给以后的控制帧

Mask: 1 bit

​ mask标志位,定义“有效负载数据”是否添加掩码。如果设置为1,那么掩码的键值存在于Masking-Key中,根据5.3节描述,这个一般用于解码“有效负载数据”。所有的从客户端发送到服务端的帧都需要设置这个bit位为1。

Payload length: 7 bits, 7+16 bits, or 7+64 bits

​ 以字节为单位的“有效负载数据”长度,如果值为0-125,那么就表示负载数据的长度。如果是126,那么接下来的2个bytes解释为16bit的无符号整形作为负载数据的长度。如果是127,那么接下来的8个bytes解释为一个64bit的无符号整形(最高位的bit必须为0)作为负载数据的长度。多字节长度量以网络字节顺序表示(译注:应该是指大端序和小端序)。在所有的示例中,长度值必须使用最小字节数来进行编码,例如:长度为124字节的字符串不可用使用序列126,0,124进行编码。有效负载长度是指“扩展数据”+“应用数据”的长度。“扩展数据”的长度可能为0,那么有效负载长度就是“应用数据”的长度。

Masking-Key: 0 or 4 bytes

​ 所有从客户端发往服务端的数据帧都已经与一个包含在这一帧中的32 bit的掩码进行过了运算。如果mask标志位(1 bit)为1,那么这个字段存在,如果标志位为0,那么这个字段不存在。在5.3节中会介绍更多关于客户端到服务端增加掩码的信息。

Payload data: (x+y) bytes

​ “有效负载数据”是指“扩展数据”和“应用数据”。

Extension data: x bytes

​ 除非协商过扩展,否则“扩展数据”长度为0 bytes。在握手协议中,任何扩展都必须指定“扩展数据”的长度,这个长度如何进行计算,以及这个扩展如何使用。如果存在扩展,那么这个“扩展数据”包含在总的有效负载长度中。

Application data: y bytes

​ 任意的“应用数据”,占用“扩展数据”后面的剩余所有字段。“应用数据”的长度等于有效负载长度减去“扩展应用”长度。

基础数据帧协议通过ABNF进行了正式的定义。需要重点知道的是,这些数据都是二进制的,而不是ASCII字符。例如,长度为1 bit的字段的值为%x0 / %x1代表的是一个值为0/1的单独的bit,而不是一整个字节(8 bit)来代表ASCII编码的字符“0”和“1”。一个长度为4 bit的范围是%x0-F的字段值代表的是4个bit,而不是字节(8 bit)对应的ASCII码的值。不要指定字符编码:“规则解析为一组最终的值,有时候是字符。在ABNF中,字符仅仅是一个非负的数字。在特定的上下文中,会根据特定的值的映射(编码)编码集(例如ASCII)”。在这里,指定的编码类型是将每个字段编码为特定的bits数组的二进制编码的最终数据。

ws-frame =

  • frame-fin; 长度为1 bit
  • frame-rsv1; 长度为1 bit
  • frame-rsv2; 长度为1 bit
  • frame-rsv3; 长度为1 bit
  • frame-opcode; 长度为4 bit
  • frame-masked; 长度为1 bit
  • frame-payload-length; 长度为7或者7+16或者7+64 bit
  • [frame-masking-key]; 长度为32 bit
  • frame-payload-data; 长度为大于0的n*8 bit(其中n>0)

frame-fin =

  • %x0,除了以下为1的情况
  • %x1,最后一个消息帧
  • 长度为1 bit

frame-rsv1 =

  • %x0 / %x1,长度为1 bit,如果没有协商则必须为0

frame-rsv2 =

  • %x0 / %x1,长度为1 bit,如果没有协商则必须为0

frame-rsv3 =

  • %x0 / %x1,长度为1 bit,如果没有协商则必须为0

frame-opcode =

  • frame-opcode-non-control
  • frame-opcode-control
  • frame-opcode-cont

frame-opcode-non-control

  • %x1,文本帧
  • %x2,二进制帧
  • %x3-7,保留给将来的非控制帧
  • 长度为4 bit

frame-opcode-control

  • %x8,连接关闭
  • %x9,ping帧
  • %xA,pong帧
  • %xB-F,保留给将来的控制帧
  • 长度为4 bit

frame-masked

  • %x0,不添加掩码,没有frame-masking-key
  • %x1,添加掩码,存在frame-masking-key
  • 长度为1 bit

frame-payload-length

  • %x00-7D,长度为7 bit
  • %x7E frame-payload-length-16,长度为7+16 bit
  • %x7F frame-payload-length-63,长度为7+64 bit

frame-payload-length-16

  • %x0000-FFFF,长度为16 bit

frame-payload-length-63

  • %x0000000000000000-7FFFFFFFFFFFFFFF,长度为64 bit

frame-masking-key

  • 4(%x00-FF),当frame-mask为1时存在,长度为32 bit

frame-payload-data

  • frame-masked-extension-data frame-masked-application-data,当frame-masked为1时
  • frame-unmasked-extension-data frame-unmasked-application-data,当frame-masked为0时

frame-masked-extension-data

  • *(%x00-FF),保留给将来的扩展,长度为n*8,其中n>0

frame-masked-application-data

  • *(%x00-FF),长度为n*8,其中n>0

frame-unmasked-extension-data

  • *(%x00-FF),保留给将来的扩展,长度为n*8,其中n>0

frame-unmasked-application-data

  • *(%x00-FF),长度为n*8,其中n>0

5.3 客户端到服务端添加掩码

添加掩码的数据帧必须像5.2节定义的一样,设置frame-masked字段为1。

掩码值像第5.2节说到的完全包含在帧中的frame-masking-key上。它是用于对定义在同一节中定义的帧负载数据Payload data字段中的包含Extension dataApplication data的数据进行添加掩码。

掩码字段是一个由客户端随机选择的32bit的值。当准备掩码帧时,客户端必须从允许的32bit值中须知你咋一个新的掩码值。掩码值必须是不可被预测的;因此,掩码必须来自强大的熵源(entropy),并且给定的掩码不能让服务器或者代理能够很容易的预测到后续帧。掩码的不可预测性对于预防恶意应用作者在网上暴露相关的字节数据至关重要。RFC 4086讨论了安全敏感的应用需要一个什么样的合适的强大的熵源。

掩码不影响Payload data的长度。进行掩码的数据转换为非掩码数据,或者反过来,根据下面的算法即可。这个同样的算法适用于任意操作方向的转换,例如:对数据进行掩码操作和对数据进行反掩码操作所涉及的步骤是相同的。

表示转换后数据的八位字节的i(transformed-octet-i )是表示的原始数据的i(original-octet-i)与索引i模4得到的掩码值(masking-key-octet-j)经过异或操作(XOR)得到的:

j = i MOD 4
transfromed-octed-i = original-octet-i XOR masking-key-octet-j

在规范中定义的位于frame-payload-length字段的有效负载的长度,不包括掩码值的长度。它只是Payload data的长度。如跟在掩码值后面的字节数组的数。

5.4 消息分片

消息分片的主要目的是允许发送一个未知长度且消息开始发送后不需要缓存的消息。如果消息不能被分片,那么一端必须在缓存整个消息,因此这个消息的长度必须在第一个字节发送前就需要计算出来。如果有消息分片,服务端或者代理可以选择一个合理的缓存长度,当缓存区满了以后,就想网络发送一个片段。

第二个消息分片使用的场景是不适合在一个逻辑通道内传输一个大的消息占满整个输出频道的多路复用场景。多路复用需要能够将消息进行自由的切割成更小的片段来共享输出频道。(注意:多路复用的扩展不在这个文档中讨论)。

除非在扩展中另有规定,否则帧没有语义的含义。如果客户端和服务的没有协商扩展字段,或者服务端和客户端协商了一些扩展字段,并且代理能够完全识别所有的协商扩展字段,在这些扩展字段存在的情况下知道如何进行帧的合并和拆分,代理就可能会合并或者拆分帧。这个的一个含义是指在缺少扩展字段的情况下,发送者和接收者都不能依赖特定的帧边界的存在。

消息分片相关的规则如下:

  • 一个未分片的消息包含一个设置了FIN字段(标记为1)的单独的帧和一个除0以外的操作码。
  • 一个分片的消息包含一个未设置的FIN字段(标记为0)的单独的帧和一个除0以外的操作码,然后跟着0个或者多个未设置FIN字段的帧和操作码为0的帧,然后以一个设置了FIN字段以及操作码为0的帧结束。一个分片的消息内容按帧顺序组合后的payload字段,是等价于一个单独的更大的消息payload字段中包含的值;然而,如果扩展字段存在,因为扩展字段定义了Extension data的解析方式,因此前面的结论可能不成立。例如:Extension data可能只出现在第一个片段的开头,并适用于接下来的片段,或者可能每一个片段都有Extension data,但是只适用于特定的片段。在Extension data不存在时,下面的示例演示了消息分片是如何运作的。
    示例:一个文本需要分成三个片段进行发送,第一个片段包含的操作码为0x1并且未设置FIN字段,第二个片段的操作码为0x0并且未设置FIN字段,第三个片段的操作码为0x0并且设置了FIN字段。
  • 控制帧(见5.5节)可能被插入到分片消息的中间。控制帧不能被分片。
  • 消息片段必须在发送端按照顺序发送给接收端。
  • 除非在扩展中定义了这种嵌套的逻辑,否则一条消息分的片不能与另一条消息分的片嵌套传输。
  • 终端必须有能力来处理在分片的消息中的控制帧。
  • 发送端可能会创建任意大小的非控制消息片段。
  • 客户端和服务端必须同时支持分片和不分片消息。
  • 控制帧不能被分片,并且代理不允许改变控制帧的片段。
  • 如果有保留字段被使用并且代理不能理解这些字段的值时,那么代理不能改变消息的片段。
  • 在扩展字段已经被协商过,但是代理不知道协商扩展字段的具体语义时,代理不能改变任意消息的片段。同样的,扩展不能看到WebSocket握手(并且得不到通知内容)导致WebSocket的连接禁止改变连接过程中任意的消息片段。
  • 作为这些规则的结论,所有的消息片段都是同类型的,并且设置了第一个片段的操作码(opccode)字段。控制帧不能被分片,所有的消息分片类型必须是文本或者二进制,或者是保留的任意一个操作码。

注:如果控制帧没有被打断,心跳(ping)的等待时间可能会变很长,例如在一个很大的消息之后。因此,在分片的消息传输中插入控制帧是有必要的。

实践说明:如果扩展字段不存在,接收者不需要使用缓存来存储下整个消息片段来进行处理。例如:如果使用一个流式API,再收到部分帧的时候就可以将数据交给上层应用。然而,这个假设对以后所有的WebSocket扩展可能不一定成立。

5.5 控制帧

控制帧是通过操作码最高位的值为1来进行区分的。当前已经定义的控制帧操作码包括0x8(关闭),0x9(心跳Ping)和0xA(心跳Pong)。操作码0xB-0xF没有被定义,当前被保留下来做为以后的控制帧。

控制帧是用于WebSocket的通信状态的。控制帧可以被插入到消息片段中进行传输。

所有的控制帧必须有一个126字节或者更小的负载长度,并且不能被分片。

5.5.1 关闭(Close)

控制帧的操作码值是0x8。

关闭帧可能包含内容(body)(帧的“应用数据”部分)来表明连接关闭的原因,例如终端的断开,或者是终端收到了一个太大的帧,或者是终端收到了一个不符合预期的格式的内容。如果这个内容存在,内容的前两个字节必须是一个无符号整型(按照网络字节序)来代表在7.4节中定义的状态码。跟在这两个整型字节之后的可以是UTF-8编码的的数据值(原因),数据值的定义不在此文档中。数据值不一定是要人可以读懂的,但是必须对于调试有帮助,或者能传递有关于当前打开的这条连接有关联的信息。数据值不保证人一定可以读懂,所以不能把这些展示给终端用户。

从客户端发送给服务端的控制帧必须添加掩码,具体见5.3节。

应用禁止在发送了关闭的控制帧后再发送任何的数据帧。

如果终端收到了一个关闭的控制帧并且没有在以前发送一个关闭帧,那么终端必须发送一个关闭帧作为回应。(当发送一个关闭帧作为回应时,终端通常会输出它收到的状态码)响应的关闭帧应该尽快发送。终端可能会推迟发送关闭帧直到当前的消息都已经发送完成(例如:如果大多数分片的消息已经发送了,终端可能会在发送关闭帧之前将剩余的消息片段发送出去)。然而,已经发送关闭帧的终端不能保证会继续处理收到的消息。

在已经发送和收到了关闭帧后,终端认为WebSocket连接以及关闭了,并且必须关闭底层的TCP连接。服务端必须马上关闭底层的TCP连接,客户端应该等待服务端关闭连接,但是也可以在收到关闭帧以后任意时间关闭连接。例如:如果在合理的时间段内没有收到TCP关闭指令。

如果客户端和服务端咋同一个时间发送了关闭帧,两个终端都会发送和接收到一条关闭的消息,并且应该认为WebSocket连接已经关闭,同时关闭底层的TCP连接。

5.5.2 心跳Ping

心跳Ping帧包含的操作码是0x9。

关闭帧可能包含“应用数据”。

如果收到了一个心跳Ping帧,那么终端必须发送一个心跳Pong 帧作为回应,除非已经收到了一个关闭帧。终端应该尽快恢复Pong帧。Pong帧将会在5.5.3节讨论。

终端可能会在建立连接后与连接关闭前中间的任意时间发送Ping帧。

注意:Ping帧可能是用于保活或者用来验证远端是否仍然有应答。

5.5.3 心跳Pong

心跳Ping帧包含的操作码是0xA。

5.5.2节详细说明了Ping帧和Pong帧的要求。

作为回应发送的Pong帧必须完整携带Ping帧中传递过来的“应用数据”字段。

如果终端收到一个Ping帧但是没有发送Pong帧来回应之前的pong帧,那么终端可能选择用Pong帧来回复最近处理的那个Ping帧。

Pong帧可以被主动发送。这会作为一个单项的心跳。预期外的Pong包的响应没有规定。

5.6 数据帧

数据帧(例如非控制帧)的定义是操作码的最高位值为0。当前定义的数据帧操作吗包含0x1(文本)、0x2(二进制)。操作码0x3-0x7是被保留作为非控制帧的操作码。

数据帧会携带应用层/扩展层数据。操作码决定了携带的数据解析方式:

文本

“负载字段”是用UTF-8编码的文本数据。注意特殊的文本帧可能包含部分UTF-8序列;然而,整个消息必须是有效的UTF-8编码数据。重新组合消息后无效的UTF-8编码数据处理见8.1节。

二进制

“负载字段”是任意的二进制数据,二进制数据的解析仅仅依靠应用层。

5.7 示例

  • 一个单帧未添加掩码的文本消息
    0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (内容为"Hello")
  • 一个单帧添加掩码的文本消息
    0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 (内容为Hello")
  • 一个分片的未添加掩码的文本消息
    0x01 0x03 0x48 0x65 0x6c (内容为"Hel")
    0x80 0x02 0x6c 0x6f (内容为”lo")
  • 未添加掩码的Ping请求和添加掩码的Ping响应(译者注:即Pong)
    0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f (包含内容为”Hello", 但是文本内容是任意的)
    0x8a 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 (包含内容为”Hello", 匹配ping的内容)
  • 256字节的二进制数据放入一个未添加掩码数据帧
    0x82 0x7E 0x0100 [256 bytes of binary data\]
  • 64KB二进制数据在一个非掩码帧中
    0x82 0x7F 0x0000000000010000 [65536 bytes of binary data\]

5.8 扩展性

这个协议的设计初衷是允许扩展的,可以在基础协议上增加能力。终端的连接必须在握手的过程中协商使用的所有扩展。在规范中提供了从0x3-0x7和0xB-0xF的操作码,在数据帧Header中的“扩展数据”字段、frame-rsv1、frame-rsv2、frame-rsv3字段都可以用于扩展。扩展的协商讨论将在以后的9.1节中详细讨论。下面是一些符合预期的扩展用法。下面的列表不完整,也不是规范中内容。

  • “扩展数据”可以放置在“负载数据“中的应用数据”之前的位置。
  • 保留的字段可以在每一帧需要时被使用。
  • 保留的操作码的值可以被定义。
  • 如果需要更多的操作码,那么保留的操作码字段可以被定义。
  • 保留的字段或者“扩展”操作码可以在“负载数据”之中的分配额外的位置来定义,这样可以定义更大的操作码或者更多的每一帧的字段。
查看原文

赞 1 收藏 0 评论 0

hjava 发布了文章 · 2019-02-20

【译】 WebSocket 协议第十二章——使用其他规范中的WebSocket协议

概述

本文为 WebSocket 协议的第十二章,本文翻译的主要内容为如何使用其他规范中的 WebSocket 协议。

使用其他规范中的WebSocket协议(协议正文)

WebSocket协议旨在由另一规范使用,以提供动态作者定义内容的通用机制。例如,在定义脚本 API 的规范中定义 WebSocket 协议。

例如一个规范首先需要建立 WebSocket 连接,提供该算法:

  • 目标资源,包含一个主机名(host)和一个端口(port)
  • 资源名称,允许在一个主机和端口上识别多个服务。
  • 安全标记,当这个值为 true 时,连接应该被加密,如果为 false 时则不需要。
  • 原始RFC6454的ASCII序列化,负责连接。
  • 可选的,基于 WebSocket 连接的通过一个字符串定义的协议。

主机端口资源名称安全标记通常是使用解析 WebSocket URI 组件,通过 URI 来获取。如果 URI 中没有指定这些 WebSocket 字段,那么这个解析将失败。

如果在任意时间连接被关闭了,那么规范需要使用关闭 WebSocket 连接算法(第 7.1.1 节)。

第 7.1.4 节定义了什么时候WebSocket 连接关闭

当连接打开时,文档需要处理收到一条 WebSocket 消息(第 6.2 节)的场景。

为了向已经建立的连接发送一些数据,文档需要处理发送 WebSocket 消息(第 6.1 节)。

查看原文

赞 2 收藏 2 评论 0

hjava 发布了文章 · 2019-02-20

【译】 WebSocket 协议第十一章——IANA 注意事项(IANA Considerations)

概述

本文为 WebSocket 协议的第十一章,本文翻译的主要内容为 WebSocket 的 IANA 相关注意事项。

IANA 注意事项(协议正文)

11.1 注册新 URI 协议

11.1.1 注册 “ws” 协议

ws URI 定义了 WebSocket 服务器和资源名称。

URI 协议名称

ws

状态

永久

URI 协议语法

使用 ABNF (RFC5234)语法和来自 URI 规范 RFC3986 的 ABNF 终端:

"ws:" "//" authority path-abempty [ "?" query ]

path-abemptyqueryRFC3986 部分组成了发送给服务端的资源名称,来标记需要的服务类型。其他的部分在 RFC3986 中定义了含义。

URI 协议含义

这个方案的唯一操作就是使用 WebSocket 协议打开一个连接。

编码注意事项

按照上面定义的语法排除的主机部分中的字符必须按照 RFC3987 中的规定从 Unicode 转换为 ASCII 或其替换字符。为了实现基于方案的规范化,国际化网域名称(IDN)主机组件的形式与 punycode 码之间的相互转化认为是等价的(见 RFC3987 第 5.3.3 节)。

除了由上面语法排除的字符外,其他组件的字符在第一次转换为 UTF-8 字符时,必须从 Unicode 码转化到 ASCII 码,然后使用百分比编码格式替换对应的定义在 URI RFC3896 字符和国际化资源标识符(IRI) RFC3987 规范。

应用/协议使用这个 URI 协议规范

WebSocket Protocol

互操作性注意事项

使用 WebSocket 时需要使用 1.1 或者更高版本的 HTTP 协议。

安全性注意事项

见”安全性注意事项”一节。

联系

HYBI WG <hybi@ietf.org\\\>

作者/更改控制者

IETF <iesg@ietf.org\\\>

关联

RFC 6455

11.1.2 注册 “wss”协议

一个 wss 的 URI 定义了一个 WebSocket 服务器和资源名称,表明通过这个链接的传输需要通过 TLS(包含标准的 TLS 能力例如数据保密性和完整性以及终端认证)来进行保护。

URI 协议名称

wss

状态

永久

URI 协议语法

使用 ABNF (RFC5234)语法和来自 URI 规范 RFC3986 的 ABNF 终端:

"wss:" "//" authority path-abempty [ "?" query ]

path-abemptyqueryRFC3986 部分组成了发送给服务端的资源名称,来标记需要的服务类型。其他的部分在 RFC3986 中定义了含义。

URI 协议含义

这个方案的唯一操作就是使用 WebSocket 协议打开一个连接,通过 TLS 加密。

编码注意事项

按照上面定义的语法排除的主机部分中的字符必须按照 RFC3987 中的规定从 Unicode 转换为 ASCII 或其替换字符。为了实现基于方案的规范化,国际化网域名称(IDN)主机组件的形式与 punycode 码之间的相互转化认为是等价的(见 RFC3987 第 5.3.3 节)。

除了由上面语法排除的字符外,其他组件的字符在第一次转换为 UTF-8 字符时,必须从 Unicode 码转化到 ASCII 码,然后使用百分比编码格式替换对应的定义在 URI RFC3896 字符和国际化资源标识符(IRI) RFC3987 规范。

应用/协议使用这个 URI 协议规范

WebSocket Protocol

互操作性注意事项

使用 WebSocket 时需要使用 1.1 或者更高版本的 HTTP 协议。

安全性注意事项

见”安全性注意事项”一节。

联系

HYBI WG <hybi@ietf.org\\\>

作者/更改控制者

IETF <iesg@ietf.org\\\>

关联

RFC 6455

11.2 注册“WebSocket”协议升级关键值

这一节描述一个在 HTTP 升级凭证注册的关键值,在 RFC2817 定义。

凭证名称

WebSocket

作者/更改控制者

IETF <iesg@ietf.org\\\>

关联

RFC 6455

11.3 注册新的 HTTP 头字段

Sec-WebSocket-Key

这一节描述一个注册在永久消息头字段名称中的头字段,在 RFC3864 定义。

头字段名称

Sec-WebSocket-Key

应用协议

http

状态

标准

作者/更改控制者

IETF

说明文档

RFC 6455

关联信息

这个头字段只用于 WebSocket 开始握手。

Sec-WebSocket-Key 头字段是用在 WebSocket 开始握手阶段。它是通过客户端发送给服务端,这部分信息用于服务端证明收到一个有效的 WebSocket 握手操作的认证。这可以帮助确认服务端不会接收可能被用来向 WebSocket 服务任意发送数据的非 WebSocket 客户端的连接(例如 HTTP 客户端)。

Sec-WebSocket-Key 头字段禁止在一个 HTTP 请求中出现多次。

11.3.2 Sec-WebSocket-Extensions

这一节描述一个注册在永久消息头字段名称中的头字段,在 RFC3864 定义。

头字段名称

Sec-WebSocket-Extensions

应用协议

http

状态

标准

作者/更改控制者

IETF

说明文档

RFC 6455

关联信息

这个头字段只用于 WebSocket 开始握手。

Sec-WebSocket-Extensions 头字段是用于 WebSocket 开始握手阶段。它最开始是通过客户端发送给服务端,然后通过服务端发送给客户端,来对一个在连接中的协议级的扩展进行协商。

Sec-WebSocket-Extensions 头字段可能会在一个 HTTP 请求中出现多次(这个逻辑是等价于一个单独的 Sec-WebSocket-Extensions 头字段包含所有值)。然而,Sec-WebSocket-Extensions 头字段在 HTTP 响应中不能出现超过1次。

11.3.3 Sec-WebSocket-Accept

这一节描述一个注册在永久消息头字段名称中的头字段,在 RFC3864 定义。

头字段名称

Sec-WebSocket-Accept

应用协议

http

状态

标准

作者/更改控制者

IETF

说明文档

RFC 6455

关联信息

这个头字段只用于 WebSocket 开始握手。

Sec-WebSocket-Accpet 头字段是用于 WebSocket 开始握手阶段。它是通过服务端发送给客户端,用来确认服务端会初始化一个 WebSocket 连接。

Sec-WebSocket-Accpet 头在一个 HTTP 响应中不允许出现超过1次。

11.3.4 Sec-WebSocket-Protocol

这一节描述一个注册在永久消息头字段名称中的头字段,在 RFC3864 定义。

头字段名称

Sec-WebSocket-Protocol

应用协议

http

状态

标准

作者/更改控制者

IETF

说明文档

RFC 6455

关联信息

这个头字段只用于 WebSocket 开始握手。

Sec-WebSocket-Protocol 头字段是用于 WebSocket 开始握手阶段。它是从客户端发送给服务端,然后从服务端返回给服务端来确认连接的子协议。这个机制能够让双方选择一个子协议,同时向服务端确认可以支持这个子协议。

Sec-WebSocket-Protocol 头字段可以在一个 HTTP 请求中出现多次(这个逻辑是等价于一个单独的 Sec-WebSocket-Protocol 头字段包含所有值)。然而, Sec-WebSocket-Protocol 头字段在 HTTP 响应中不能出现超过1次。

11.3.5 Sec-WebSocket-Version

这一节描述一个注册在永久消息头字段名称中的头字段,在 RFC3864 定义。

头字段名称

Sec-WebSocket-Version

应用协议

http

状态

标准

作者/更改控制者

IETF

说明文档

RFC 6455

关联信息

这个头字段只用于 WebSocket 开始握手。

Sec-WebSocket-Version 头字段是用于 WebSocket 开始握手阶段。它是从客户端发送给服务端来表示这个连接使用的协议版本。它能够让服务端正确的进行开始握手和接下来的数据发送,以及在服务端不能够在一个安全方式下正确解析数据时关闭连接。Sec-WebSocket-Version 头字段在服务端理解的版本不匹配从客户端收到的版本导致的 WebSocket 握手失败时,也从服务端发送给客户端。在这种情况下,这个头字段包含服务端支持的协议版本。

注意这里不期望更高的版本号需要向前兼容低版本号。

Sec-WebSocket-Version 头字段可以在一个 HTTP 响应中出现多次(这个逻辑等价于一个单独的Sec-WebSocket-Version包含所有的值)。然而,Sec-WebSocket-Version 头字段不能在 HTTP 请求中出现超过1次。

11.4 WebSocket 扩展名注册表

这个规范根据RFC5526中规定的原则为 WebSocket 协议创建了一个新的 IANA 注册表,用于 WebSocket 扩展名称。

作为此注册表的一部分,IANA 包含了一下信息:

扩展定义

这个扩展的定义,将在 Sec-WebSocket-Extensions 头字段中使用,在此规范的第 11.3.2 节注册。这个值必须满足在此规范第 9.1 节中定义的扩展凭证要求。

扩展通用名

扩展名称,一般称为扩展名。

扩展定义

对定义与 WebSocket 协议一起使用的扩展的文档的引用。

已知不兼容扩展

已知的不兼容的扩展定义列表。

WebSocket 扩展名是受到“先到先得” IANA 注册政策 RFC5226 限制的。

这个注册表里没有初始值。

11.5 WebSocket 子协议名注册表

这个规范根据RFC5526中规定的原则为 WebSocket 协议创建了一个新的 IANA 注册表,用于 WebSocket 扩展名称。

作为此注册表的一部分,IANA 包含了一下信息:

子协议定义

子协议的标识符将在 Sec-WebSocket-Protocol 头字段中使用,在此规范的第 11.3.4 节中注册。这个之必须符合此规范第 4.1 节中的第 10 项要求—换句话说,这个之必须是 RFC2616 中定义的凭证。

子协议通用名

子协议名称,通常被称为子协议。

子协议定义

对定义与 WebSocket 协议一起使用的子协议的文档的引用。

WebSocket 子协议名是受到“先到先得” IANA 注册政策 RFC5226 限制的。

11.6 WebSocket 版本号注册表

该规范根据 RFC5226 中规定的原则为 WebSocket 协议创建了一个新的 IANA 注册表,用于 WebSocket 版本号。

作为此注册表的一部分,IANA 包含了一下信息:

版本号

版本号是用于在此规范第 4.1 节中制定的 Sec-WebSocket-Version 字段。这个值必须是一个范围在 0 到 255(含)之间的非负整数。

参考

RFC 请求新版本号或者带有版本号的草稿名称(见下文)。

状态

临时或者标准。见下面描述。

版本号被指定为“临时”或者“标准”。

“标准”的版本号被记录在 RFC 文档中,被认为是一个重大、稳定的 WebSocket 协议版本,例如定义在这个 RFC 中的版本。“标准”版本号是受到 “IETF 评论” IANA 注册政策 RFC5526 限制的。

“临时”版本是记录在网络草案和用于帮助实现者识别 WebSocket 协议的已部署版本并与之互操作,例如开发后但是发布前的 RFC 版本。“临时”版本号是受到 “专家评论” IANA 注册政策 RFC5526 、 最初的指定专家如HYBI 工作组主席(或者,如果工作组关闭,那么是 IETF 应用领域的领域主任)限制的。

IANA 已经向注册表中添加了如下的初始值。

版本号引用状态
0draft-ietf-hybi-thewebsocketprotocol-00临时
1draft-ietf-hybi-thewebsocketprotocol-01临时
2draft-ietf-hybi-thewebsocketprotocol-02临时
3draft-ietf-hybi-thewebsocketprotocol-03临时
4draft-ietf-hybi-thewebsocketprotocol-04临时
5draft-ietf-hybi-thewebsocketprotocol-05临时
6draft-ietf-hybi-thewebsocketprotocol-06临时
7draft-ietf-hybi-thewebsocketprotocol-07临时
8draft-ietf-hybi-thewebsocketprotocol-08临时
9保留
10保留
11保留
12保留
13RFC6455

11.7 WebSocket 关闭码注册表

该规范根据 RFC5226 中规定的原则为 WebSocket 协议创建了一个新的 IANA 注册表,用于 WebSocket 关闭码。

作为此注册表的一部分,IANA 包含了一下信息:

状态码

状态码表示定义在此文档第 7.4 节中 WebSocket 连接关闭的原因。这个状态码是一个在 1000 到 4999(含)之间的整数。

含义

状态码含义。每一个状态码有一个特定的含义。

联系

保留状态代码的实体的联系人。

关联

请求状态码的固定文档和含义定义。对于 1000-2999 的状态码来说是必须的,推荐使用 3000-3999 范围的状态码。

WebSocket 关闭状态码根据它的范围有不同的注册要求。使用在这个协议和它的后续的版本或者扩展的请求版本号是受到“标准行为”、“规范要求”(这意味着“指定专家”)或者“IESG 评论” IANA注册表政策限制的,应该在 1000-2999 范围内授权。被类库、框架和应用使用的状态码是受限制于“先到先得”IANA 注册表政策,应该在 3000-3999 范围内授权。4000-4999 范围的状态码是私用的。请求应指明它们是否正在通过扩展、类库、框架或者应用使用请求WebSocket协议的状态代码(或者将来的协议的版本)。

IANA已经向注册表中添加了如下初始值。

状态码含义联系人关联
1000正常关闭hybi@ietf.orgRFC6455
1001离开hybi@ietf.orgRFC6455
1002协议错误hybi@ietf.orgRFC6455
1003不支持的数据类型hybi@ietf.orgRFC6455
1004保留hybi@ietf.orgRFC6455
1005没有收到状态码hybi@ietf.orgRFC6455
1006异常关闭hybi@ietf.orgRFC6455
1007无效的帧数据hybi@ietf.orgRFC6455
1008违反政策hybi@ietf.orgRFC6455
1009消息太大hybi@ietf.orgRFC6455
1010强制扩展hybi@ietf.orgRFC6455
1011内部服务器错误hybi@ietf.orgRFC6455
1015TLS握手hybi@ietf.orgRFC6455

11.8 WebSocket 操作码注册表

该规范根据 RFC5226 中规定的原则为 WebSocket 协议创建了一个新的 IANA 注册表,用于 WebSocket 操作码。

作为此注册表的一部分,IANA 包含了一下信息:

操作码

操作码表示定义在第 5.2 节中的 WebSocket 帧的帧类型。操作码是一个范围在 0 到 15(含)的数字。

含义

操作码的含义。

关联

请求操作码的规范。

WebSocket 操作码是受到“标准行为”IANA 注册表政策 RFC5266 限制的。

IANA 已经向注册表中注册了一下初始值。

操作码含义关联
0连续帧RFC6455
1文本帧RFC6455
2二进制帧RFC6455
8连接关闭帧RFC6455
9心跳 Ping 帧RFC6455
10心跳 Pong 帧RFC6455

11.9 WebSocket 帧头 bit 字段注册表

该规范根据 RFC5226 中规定的原则为 WebSocket 协议创建了一个新的 IANA 注册表,用于 WebSocket 帧头 bit 字段。这个注册表控制分配的 bit 位为第 5.2 节中的 RSV1、RSV2 和 RSV3。

这些 bit 位是保留给将来的版本或者文档中的扩展。

WebSocket 帧头 bit 字段是受到“标准行为”IANA 注册表政策 RFC5266 限制的。

查看原文

赞 6 收藏 3 评论 0

hjava 发布了文章 · 2019-02-20

【译】 WebSocket 协议第十章——安全性考虑(Security Considerations)

概述

本文为 WebSocket 协议的第九章,本文翻译的主要内容为 WebSocket 安全性相关内容。

10 安全性考虑(协议正文)

这一章描述了一些 WebSocket 协议的可用的安全性考虑。这一章的小节描述了这些特定的安全性考虑。

10.1 非浏览器客户端

WebSocket 协议防止在受信任的应用例如 Web 浏览器中执行的恶意 JavaScript 代码,例如通过检查Origin头字段(见下面)。见第 1.6 节去了解更多详情。这种假设在更有能力的客户端的情况下不成立。

这个协议可以被网页中的脚本使用,也可以通过宿主直接使用。这些宿主是代表自己的利益的,因此可以发送假的Origin头字段来欺骗服务端。因此服务端对于他们正在和已知的源的脚本直接通信的假设需要消息,并且必须认为他们可能通过没有预期的方式访问。特别地,服务端不应该相信任何输入都是有效的。

示例:如果服务端使用输入的内容作为一部分的 SQL 查询语句,所有的输入文本都必须在传递给 SQL 服务器时进行编码,以免服务端受到 SQL 注入攻击。

10.2 源考虑

只处理特定站点,不打算处理任何 Web 页面的数据服务器应该验证Origin字段是否是他们预期的。如果服务端收到的源字段是不接受的,那么他应该通过包含 HTTP 禁止状态码为 403 的请求响应作为 WebSocket 握手的响应。

当不信任的一方是 JavaScript 应用作者并存在受信任的客户端中运行时,Origin字段可以避免出现这种攻击的情况。客户端可以连接到服务端,通过协议中的Origin字段,确定是否开放连接的权限给 JavaScript 应用。这么做的目的不是组织非浏览器应用建立连接,而是保证在受信任的浏览器中可能运行的恶意 JavaScript 代码并不会构建一个假的 WebSocket 握手。

10.3 基础设施攻击(添加掩码)

除了终端可能会成为通过 WebSocket 被攻击的目标之外,网络基础设施的另外一部分,例如代理,也有可能是攻击的对象。

这个协议发展后,通过一个实验验证了部署在外部的缓存服务器由于一系列在代理上面的攻击导致投毒。一般形式的攻击就是在攻击者控制下建立一个与服务端的连接,实现一个与 WebSocket 协议建立连接相似的 HTTP UPGRADE 连接,然后通过升级以后的连接发送数据,看起来就像是针对已知的特定资源(在攻击中,这可能类似于广泛部署的脚本,用于跟踪广告服务网络上的点击或资源)进行 GET 请求。远端服务器可能会通过一些看上去像响应数据的来响应假的 GET 请求,然后这个响应就会按照非零百分比的已部署中介缓存,因此导致缓存投毒。这个攻击带来的影响就是,如果一个用户可以正常的访问一个攻击者控制的网网站,那么攻击者可以针对这个用户进行缓存投毒,在相同缓存的后面其他用户会运行其他源的恶意脚本,破坏 Web 安全模型。

为了避免对中介服务的此类攻击,使用不符合 HTTP 的数据帧中为应用程序的数据添加前缀是不够的,我们不可能详细的检查和测试每一个不合标准的中介服务有没有跳过这种非 HTTP 帧,或者对帧载荷处理不正确的情况。因此,采用的防御措施是对客户端发送给服务端的所有数据添加掩码,这样的话远端的脚本(攻击者)就不能够控制发送的数据如何出现在线路上,因此就不能够构造一条被中介误解的 HTPT请求。

客户端必须为每一帧选择一个新的掩码值,使用一个不能够被应用预测到的算法来进行传递数据。例如,每一个掩码值可以通过一个加密强随机数生成器来生成。如果相同的值已经被使用过或者已经存在一种方式能够判断出下一个值如何选择时,攻击这个可以发送一个添加了掩码的消息,来模拟一个 HTTP 请求(通过在线路上接收攻击者希望看到的消息,使用下一个被使用的掩码值来对数据进行添加掩码,当客户端使用它时,这个掩码值可以有效地反掩码数据)。

当从客户端开始传递第一帧时,这个帧的有效载荷(应用程序提供的数据)就不能够被客户端应用程序修改,这个策略是很重要的。否则,攻击者可以发送一个都是已知值(例如全部为 0)的初始值的很长的帧,计算收到第一部分数据时使用过的掩码,然后修改帧中尚未发送的数据,以便在添加掩码时显示为 HTTP 请求。(这与我们在之前的段落中描述的使用已知的值和可预测的值作为掩码值,实际上是相同的问题。)如果另外的数据已经发送了,或者要发送的数据有所改变,那么新的数据或者修改的数据必须使用一个新的数据帧进行发送,因此也需要选择一个新的掩码值。简短来说,一旦一个帧的传输开始后,内容不能够被远端的脚本(应用)修改。

受保护的威胁模型是客户端发送看似HTTP请求的数据的模型。因此,从客户端发送给服务端的频道数据需要添加掩码值。从服务端到客户端的数据看上去像是一个请求的响应,但是,为了完成一次请求,客户端也需要可以伪造请求。因此,我们不认为需要在双向传输上添加掩码。(服务端发送给客户端的数据不需要添加掩码)

尽管通过添加掩码提供了保护,但是不兼容的 HTTP 代理仍然由于客户端和服务端之间不支持添加掩码而受到这种类型的攻击。

10.4 指定实现的限制

在从多个帧重新组装后,对于帧大小或总消息大小具有实现和必须避免自己超过相关的多平台特定限制带来的影响。(例如:一个恶意的终端可能会尝试耗尽对端的内存或者通过发送一个大的帧(例如:大小为 2 ** 60)或发送一个长的由许多分片帧构成的流来进行拒绝服务攻击)。这些实现应该对帧的大小和组装过后的包的总大小有一定的限制。

10.5 WebSocket 客户端认证

这个协议在 WebSocket 握手时,没有规定服务端可以使用哪种方式进行认证。WebSocket 服务器可以使用任意 HTTP 服务器通用的认证机制,例如: Cookie、HTTP 认证或者 TLS 认证。

10.6 连接保密性和完整性

连接保密性是基于运行 TLS 的 WebSocket 协议(wss 的 URLs)。WebSocket 协议实现必须支持 TLS,并且应该在与对端进行数据传输时使用它。

如果在连接中使用 TLS,TLS带来的连接的收益非常依赖于 TLS 握手时的算法的强度。例如,一些 TLS 的加密算法不提供连接保密性。为了实现合理登记的保护措施,客户端应该只使用强 TLS 算法。“Web 安全:用户接口指南”(W3C.REC-wsc-ui-20100812)讨论了什么是强 TLS 算法。RFC5246附录 A.5附录 D.3提供了另外的指导。

10.7 处理无用数据

传入的数据必须经过客户端和服务端的认证。如果,在某个时候,一个终端面对它无法理解的数据或者违反了这个终端定义的输入安全规范和标准,或者这个终端在开始握手时没有收到对应的预期值时(在客户端请求中不正确的路径或者源),终端应该关闭 TCP 连接。如果在成功的握手后收到了无效的数据,终端应该在进入关闭 WebSocket流程前,发送一个带有合适的状态码(第 7.4 节)的关闭帧。使用一个合适的状态码的关闭帧有助于诊断这个问题。如果这个无效的数据是在 WebSocket 握手时收到的,服务端应该响应一个合适的 HTTP 状态码(RFC2616)。

使用错误的编码来发送数据是一类通用的安全问题。这个协议指定文本类型数据(而不是二进制或者其他类型)的消息使用 UTF-8 编码。虽然仍然可以得到长度值,但实现此协议的应用程序应使用这个长度来确定帧实际结束的位置,发送不合理的编码数据仍然会导致基于此协议构建的应用程序可能会导致从数据的错误解释到数据丢失或潜在的安全漏洞出现。

10.8 在 WebSocket 握手中使用 SHA-1

在这个文档中描述的 WebSocket 握手协议是不依赖任意 SHA-1 的安全属性,流入抗冲击性和对第二次前映像攻击的抵抗力(就像 RFC4270 描述的一样)。

查看原文

赞 1 收藏 1 评论 0

hjava 发布了文章 · 2019-02-20

【译】 WebSocket 协议第九章——扩展(Extension)

概述

本文为 WebSocket 协议的第九章,本文翻译的主要内容为 WebSocket 扩展相关内容。

扩展(协议正文)

WebSocket 可以请求该规范中提到的扩展,WebSocket 服务端可以接受其中一些或者所有的客户端请求的扩展。服务端禁止响应客户端没有请求过的扩展。如果扩展参数需要在客户端和服务端之间进行协商,这些参数必须根据参数所应用的扩展的规范来选择。

9.1 协商扩展

客户端通过 Sec-WebSocket-Extensions 请求头字段来请求扩展,请求头字段遵守 HTTP 的规则,它的值是通过 ABNF 定义的。注意这一节是通过 ABNF 语法/规则,包括“implied *LWS rule”。如果我们客户端或者服务端在协商扩展收到了一个没有符合下面的 ABNF 规则的值,接收到错误的数据的这一方需要立刻让 WebSocket 关闭连接

Sec-WebSocket-Extensions = extension-list
extension-list = 1#extension
extension = extension-token *( ";" extension-param )
extension-token = registered-token
registered-token = token
extension-param = token [ "=" (token | quoted-string) ]
    ; 使用带引号的语法变量时,在引号字符后面的变量的值必须符合`token`变量 ABNF规范。

注意,就像其他的 HTTP 请求头字段一样,这个请求头字段可以被切割成几行或者几行合并成一行。因此,下面这两段是等价的:

Sec-WebSocket-Extensions: foo
Sec-WebSocket-Extensions: bar; baz=2

是等价于:

Sec-WebSocket-Extensions: foo, bar; baz=2

任何一个扩展凭证都必须是一个注册过的凭证。(见底 11.4 节)。扩展所使用的任何参数都必须是定义给这个扩展的。注意,客户端只能建议使用任意存在的扩展而不能使用它们,除非服务端表示他们希望使用这个扩展。

注意扩展的顺序是重要的。多个扩展中的任意的互相作用都可以被定义在这个定义扩展的文档中。在没有此类定义的情况下,客户端在其请求中列出的头字段表示其希望使用的头字段的首选项,其中列出的第一个选项是最可取的。服务器在响应中列出的扩展表示连接实际使用的扩展。如果扩展修改了数据或者帧,对数据的操作顺序应该被假定为和链接开始握手的服务端响应的列举的扩展中的顺序相同。

例如,如果有两个扩展”foo”和”bar”,并且服务端发送的头字段Sec-WebSocket-Extensions的值为”foo,bar”,那么对数据的操作顺序就是bar(foo(data)),是对数据本身的更改(例如压缩)或者“堆叠”的帧的更改。

可接受的扩展标头字段的非规范性示例(请注意,长线被折叠以便于阅读)如下:

Sec-WebSocket-Extensions: deflate-stream
Sec-WebSocket-Extensions: mux; max-channels=4; flow-control, deflate-stream
Sec-WebSocket-Extensions: private-extension

服务端接受一个或者多个扩展字段,这些扩展字段是包含客户端请求的Sec-WebSocket-Extensions头字段扩展中的。任何通过服务端构成的能够响应来自客户端请求的参数的扩展参数,将由每个扩展定义。

9.2 已知扩展

扩展为实现方式提供了一个机制,即选择使用附加功能协议。这个文档中不定义任何扩展,但是实现跨越使用单独定义的扩展。

查看原文

赞 1 收藏 1 评论 0

hjava 发布了文章 · 2019-02-19

【译】 WebSocket 协议第八章——错误处理(Error Handling)

概述

本文为 WebSocket 协议的第八章,本文翻译的主要内容为 WebSocket 错误处理相关内容。

错误处理(协议正文)

8.1 处理 UTF-8 数据错误

当终端按照 UTF-8 的格式来解析一个字节流,但是发现这个字节流不是 UTF-8 编码,或者说不是一个有效的 UTF-8 字节流时,终端必须让 WebSocket 连接关闭。这个规则在建立连接开始握手和后续的数据交换时都生效。

查看原文

赞 1 收藏 1 评论 0

认证与成就

  • 获得 533 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-11-02
个人主页被 2.1k 人浏览