说明
由于学习需要,本文是对STOMP协议规范1.2版的翻译记录。由于初学协议内容,翻译中如有不合适的地方,欢迎指正,谢谢!
STOMP-1.2原文地址:http://stomp.github.io/stomp-specification-1.2.html
摘要
STOMP是一个专为实现客户端之间通过中间服务器进行异步通信的简单可操作的协议,它为这些互相通信客户端和服务器定义了一种基于文本的消息通信格式。
STOMP协议已经被广泛使用很多年了,有很多配套的代理服务端和客户端库支持。该规范定义了STOMP1.2协议,是STOMP1.1协议的更新。
如果有任何问题,请反馈给stomp-spec@googlegroups.com邮件列表。
预览
背景
STOMP协议起源于需要一种协议连接使用脚本语言Ruby,Python,Perl的企业级消息代理服务。在这种环境下,它通常是逻辑简单的操作,例如实现“可靠地发送消息和断开连接”和“销毁目标上的所有消息”等。
它是一个对其他开放通讯协议的可用选择,例如替代AMQP协议和实现了特定线路协议的用于JMS代理的协议如OpenWire,它通过覆盖实现了一小部分常用的消息操作功能,而非提供了全面的消息传递API而独树一帜。
最近,STOMP已经成为了一个成熟的协议,提供了可以用于过去那些简单的线级别用例功能,但它仍旧保持着简单性和可操作性的核心设计思想。
协议预览
STOMP是一种基础Frame的协议,模仿HTTP的Frame。一个Frame由命令、一组可选择的信息头和一个信息体组成。STOMP协议是基于文本的,也可以传递二进制信息。协议默认UTF-8编码,也支持对信息体的其他特定可选择的编码。
STOMP服务器被定义为一组可以接收发送的信息的目标服务。STOMP视目标服务为一个不可读的字符串并且它的特定语义由服务端实现,而且STOMP没有规定传输的目标服务的语法,不同服务之间传递的信息的语义也可以不相同。这使得服务端可以在STOMP支持的语义中自由选择。
一个STOMP客户端是一个可以以两种模式运行的用户代理,可能是同时运行两种模式。
- 作为生产者,通过SEND框架将消息发送给服务器的某个服务
- 作为消费者,通过SUBSCRIBE制定一个目标服务,通过MESSAGE框架,从服务器接收消息。
协议变更
STOMP1.2最大程度上向下兼容1.1,只有如下两个不兼容的变更:
- 现在结束一行需要使用回车加换行,而不是只有换行。
- 消息确认已经被简化,而且使用一个专用的头。
除此之外,1.2版本没有引入新功能,但是特意对某些问题做出了澄清,例如:
- 重复框架头实体
- header中使用content-type和content-length的用法
- 要求服务器支持STOMP
- 延迟连接的介绍
- 范围和订阅的唯一性以及事物的标示符
- 把RECEIPT当做之前的帧
设计理念
STOMP的主要设计理念就是简易性和可操作性。
STOMP被设计成一个轻量级协议,这样很容易使用众多的开发语言实现客户端及服务器。这意味着,特别是对服务器的结构和特性没有过多的限制,例如命名规则和实现语法的可靠性。
在本说明书中,我们会发现STOMP1.2协议没有明确规定出服务器的功能,是故在你的STOMP服务器说明文档中应该考虑添加功能介绍及其实现细节。
一致性
在本文档中的关键字“MUST”,“MUST NOT”,“REQUIRED”,“SHALL”,“SHALL NOT”,“SHOULD”,“SHOULD NOT”,“RECOMMENDED”,“MAY”和“OPTIONAL”都被释义为RFC 2119中的描述。
这种一致性实现了约束了一些特定实现的自由输入,例如防止服务器拒绝攻击,防止内存耗尽,或者解决特性平台的限制。
本规范中定义的一致性是针对于STOMP客户端和服务端的。
STOMP指令集
STOMP是一个基于Frame的协议,它假定在底层存在一个可靠地双向数据流的网络协议,就像TCP协议一样。客户端和服务端通过STOMP协议定义的Frame进行通信,一个Frame的结构大致如下:
COMMAND
header1:value1
header2:value2
body^@
这个Frame以一个指令字符串开始,以换行符(EndOfLine EOL,即换行符)结束,指令由一个可选的回车(13字节)紧跟着一个必须添加的换行(10字节)组成。接着是零或者多个以<key>:<value>格式组成的头实体。每一个头实体均以EOL结束。一个空行,即一个额外的EOL表示头部信息结束及Body信息开始。Body后面跟随着一个八进制NULL字符,文档中的例子使用了^@,在ASCII码中的控制符-@,用它来表示NULL。NULL可以跟随多个EOL。更多关于如何解析STOMPFrame的信息,查看本文档的 补充反馈 章节。
文档中提到的所有的指令和头的名字都是对大小写敏感的。
编码
STOMP的头和指令都是以UTF-8编码,除了CONNECT和CONNECTED之外的Frame都应该避免在UTF-8编码之后的头中出现回车符、换行符和冒号。
自定义使用Header时,需要使header的key和value中避免包含那些被界定在Frame Header中的字符。CONNECT和CONNECTED的例外是为了兼容STOMP1.0版本。
在UTF-8编码的头中发现的回车符、 换行符和冒号都需要使用C风格的字符串转义。当解码Frame时,必须使用下面的转义规则:
- \r(92和114字节)转义为回车符(13字节)
- \n(92和110字节)转义为换行符(10字节)
- \c(92和99字节)转义为冒号(58字节)
- \\(92和92字节)转义为反斜杠(92字节)
没有被定义的字符序列如\t(92和116),必须被当做致命的协议错误处理。相反的,当编码协议头时,必须使用按照规则的反转义。
在1.0规范中包含了很多Frame的Header被垫的例子,许多客户端和服务端实现了对Frame头的垫和修理操作,当应用程序需要发送比应该修理的头时,这样的处理会导致很多问题。所以1.2规范中明确规定,客户端和服务器永远不应该使用空白去整理头内容。
数据体
只有SEND、MESSAGE和ERROR的Frame中可能有body消息体,其他的Frame不能包含body。
数据头标准
在大部分的Frame中,一些定义好的头可以被使用,表示一些特定的含义。
1.Header content-length
所有的Frame都可能包含content-length,这个头的值是一个表示body长度的字节数。如果信息头中包含content-length,那么读取body是必须足够读取对应value中的字符数长度,不考虑body中是否存在NULL字符。这个Frame中还需要使用一个NULL字符作为结束。
如果Frame中包含body,那么SEND、MESSAGE和ERROR应该包含content-length去减轻解析的任务。如果Frame的body中包含有NULL字符,那么Frame的header中必须包含content-length。
2.Header content-type
如果Frame中包含body,那么SEND、MESSAGE和ERROR应该包含content-type去帮助接受者理解Frame的body内容的类型,同时它的值必须是一个MIME类型的值。否则,接受者应该认为body是一个二进制对象。
以text/开头的MIME类型的隐含编码类型是UTF-8。如果你使用了一种基于文本的MIME类型,但是使用了一种不同的编码方式,那么你应该在MIME中添加;charset=<encoding>。例如text/html;charset=utf-16表示一个基于文本的UTF-16编码的html内容。;charset=<encoding>同样也应该被添加到不是以text/开头的但是可以被翻译为text的MIME中。下面是一个使用UTF-8编码的XML,MIME表示为application/xml;charset=utf-8;
所有的STOMP客户端和服务器都必须支持UTF-8编解码,所以为了最大程度的实现异构计算机环境的互通性,我们推荐基于文本的内容使用UTF-8编码。
3.Header receipt
客户端除了CONNECT之外所有的Frame都可以设定一个值为任意的值的receipt头。这样服务端就可以通过给客户端返回一个RECEIPT,确认对客户端的Frame的处理结果。更多细节查看 RECEIPT 章节。
SEND
destination:/queue/a
receipt:message-12345
hello queue a^@
重复的数据头
由于消息传递系统可以组织在具有存储和转发机制的拓扑结构中,在STOMP协议中也是相似的,一个消息在到达消费者之前可能穿过了多个消息服务器。STOMP服务器可能会在更新消息头内容时选择性的添加新的头信息或者在适当的位置修改头信息。
如果客户端或者服务端收到了重复的头域实体信息,只有第一个的头域实体信息被使用,随后的其他的头域信息应该被当做头与信息变化的历史记录,可以直接忽略。
例如,如果收到如下Frame,那么第二行foo:Hello应该只是被当做文本信息
MESSAGE
foo:World
foo:Hello
^@
大小限制
为了防止客户端恶意的分配利用服务端资源,服务端可能在如下方面设置最大限制:
- Frame中允许出现的最大header个数
- Frame中一个header的最大长度
- Frame中body的最大长度
如果超出了限制,那么服务端需要给客户端发送一个ERROR,然后断开链接。
链接延迟
STOMP服务端必须支持客户端的快速链接和断开。
这意味着服务端只允许已关闭的链接在重置链接之前延迟一小段时间。
因此,客户端在SCOKET重置之前可能无法收到服务端发出的最后一帧内容,例如一个ERROR或者一个回应断开链接的RECEIPT。
链接
STOMP客户端通过初始化一个数据流或者TCP链接发送CONNECT帧到服务端:
CONNECT
accept-version:1.2
host:stomp.github.org
^@
如果服务端接收了链接意图,它回回复一个CONNECTED帧:
CONNECTED
version:1.2
^@
服务端可以拒绝任何链接请求,但是需要回复一个ERROR帧表明拒绝的原因,然后关闭链接。
CONNECT帧和STOMP帧
STOMP服务端需要对STOMP帧做和CONNECT帧一致处理,1.2规范依旧使用CONNECT帧以兼容1.0版本。
使用STOMO帧而非CONNECT帧的客户端只能连接到1.2的服务器(以及1.1的服务器)上,好处是协议嗅探器/鉴别器能够从HTTP链接中区分出STOMP链接。
STOMP1.2服务器必须设置如下头域信息:
- accept-version:客户端支持的STOMP协议的版本,详情见 协议协商 。
- host:客户端希望链接的虚拟主机的名字。推荐设置主机名是使用SOCKET已经确定的或者任何他们选择可以使用的。如果头域中不是一个已知的主机名,服务器支持的虚拟主机会选择一个默认的主机名或者直接决绝链接。
STOMP1.2服务器可能需要设置如下头域信息:
- login:一个安全的STOMP服务器需要验证的用户标示符。
- passcode:一个安全的STOMP服务器需要验证的密码。
- heart-beat:心跳的设置。
CONNECTED帧
STOMP1.2服务器必须设置如下头域信息:
- version:这次通信过程中使用的STOMP协议版本号,详情见 协议协商 。
STOMP1.2服务器可能需要设置如下头域信息:
- heart-beat:心跳的设置。
- session:一次通信的标示,唯一标示本次通信。
- server:内容包含了STOMP服务端的一些信息。这个内容必须包含一个服务名称,然后紧跟着一个使用空格字符分隔的注释内容。
服务名称由一个名字token紧跟着一个可选的版本号token构成。格式:
server:name[“/”version] *(comment)
例如:
server:Apache/1.3.9 这是注释内容
协议协商
在STOMP1.1版本及以后,CONNECT帧必须包含accept-versiont头。它的值应该被设置为一个以逗号分隔依次递增的客户端支持的协议版本好列表。如果没有这个头信息,意味着客户端仅仅支持1.0协议版本。
在后续的通讯中使用的协议版本为客户端和服务端共同支持的最高版本。
例如:客户端发送:
CONNECT
accept-version:1.0,1.1,1.2
host:stomp.github.org
^@
服务端会回复一个服务端与客户端支持的版本列表中共同支持的最高版本:
CONNECTED
version:1.1
^@
如果客户端和服务端没有一个共同支持的版本,那么服务端需要返回一个与下面类似的ERROR并且关闭链接。
ERROR
version:1.2,2.1
content-type:text/plain
Supported protocol versions are 1.2,2.1^@
心跳
心跳是可选择实现的,用来确定底层TCP链接的健康性,确保远端是正常运行的。
为了实现心跳,每一方都需要生命他要做什么并且希望对方怎么做。这些动作发生在每次通信产生之前,通过在CONNECT和CONNECTED帧中添加heart-beat头。
当使用heart-beat头时,其内容应该包含用逗号分隔的两个正整数。
第一个数值表示发送Frame者能做什么(接收心跳)
- 0意味着它不能发出心跳。
- 否则该数值就是它确认可以发出的最小毫秒数心跳的间隔。
第二个数值表示发送Frame者希望收到的(接收心跳)
- 0意味着它不希望接受到心跳。
- 否则该数值就是它希望收到收到的心跳间隔毫秒数。
heart-beat是可以选的,如果没有实现则被当做“heart-beat:0,0”,意思是这一端不能发出心跳,也不接收心跳。
heart-beat应该提供足够多的信息以便双方都能确定心跳是否可用、心跳方向以及心跳频率。
通常,心跳的Frame看起来会是这个样子的:
CONNECT
heart-beat:<cx>,<cy>
CONNECTED
heart-beat:<sx>,<sy>
对于从客户端到服务端的心跳:
- 如果<cx>是0或者<sy>是0,那么就不会有心跳发生。
- 否则就会每MAX(<sx>,<cy>)毫秒接收到一次心跳。
从另一方面来说,<sx><cy>也是同样的道理。
对于心跳来说,从网络连接上接收到的任何数据都表示远端是正常运行的。在给定的方向上,如果希望心跳每<n>毫秒秒跳动一次,需要确保如下几点:
- 发送者必须至少每<n>毫秒通过建立的网络连接发送一次新的数据。
- 如果发送者没有STOMP帧要发送时,它需要发送一个EOL。
- 如果在至少<n>毫秒内接收者没有接收到任何数据,那么可以认为链接已经断开。
- 由于定时是不一定准确的,接受者应该容忍并且考虑一个误差范围。
客户端帧介绍
客户端如果发送了一个不在如下列表范围内的帧,对于1.2协议的服务器可能会返回一个ERROR,然后关闭连接。
客户端帧列表包括:SEND、SUBSCRIBE、UNSUBSCRIBE、ACK、NACK、BEGIN、COMMIT、ABORT、DISCONNECT。
SEND
SEND帧发送一个消息到消息系统中的某个目标服务。它有一个必须包含的头“destination”,表示应该把消息发送到的目的服务。SEND帧的body包含需要发送的消息内容。例如:
SEND
destination:/queue/a
content-type:text/plain
hello queue a
^@
这个例子给一个名为/queue/a的目标服务发送了一个消息。需要注意的是,STOMP把这个目标服务名看做一个不可读的字符串,并且没有传递任何关于目标服务名称命名的语义规则。你应该参考你的STOMP服务端的文档,找出如何组建一个目标服务名称,服务端的文档应该给你提供你的应用需要的传递语义。
消息内容语义的可靠性也是服务端特定的,并且它依靠于使用的destination的值和其他的消息头的内容例如transaction或者其它自定义的头信息。
SEND支持一个transaction头,允许发送的事务信息。
SEND应该包含content-length和content-type如果帧中存在body。
用户可以向SEND帧中添加任意用户自定义的头信息。用户自定义头一般来说用来允许用户通过SUBSCRIBE帧中的一个标示用户自定义的头信息的内容去过滤消息。用户自定义的头信息必须通过MESSAGE帧传递。
如果服务端因为任何原因不能成功的处理SEND帧内容,需要返回一个ERROR并且断开连接。
SUBSCRIBE
SUBSCRIBE帧被用于注册对一个目标服务的监听。跟SEND一样,SUBSCRIBE需要destination头标示需要订阅的目标服务。被订阅的目标服务收到的任何信息以后都会被以MESSAGE帧的形式发送给客户端。ack头表示信息的确认模式。
例如:
SUBSCRIBE
id:0
destination:/queue/foo
ack:client
^@
如果服务端不能成功生成订阅,应该返回一个ERROR并且断开连接。
STOMP服务端可以支持额外的服务端特定的头去定制目标服务的传递语义。更多信息查看服务端的说明文档。
1.id header
由于一个打开的连接可以订阅服务端上的多个目标服务,所以包含在一个帧中的id头的值必须是唯一标示一个订阅。这个id用来客户端和服务端处理与订阅消息和取消订阅相关的动作。
在同一个链接中,不同的订阅必须使用不同的标示符。
2.ack header
ack的有效值是auto、client以及client-individual.默认是auto。
当ack:auto时,客户端收到服务端信息之后不需要发送ACK帧。服务端假定自己发出信息时客户端就已经收到了该信息。这种模式下可能导致服务端向客户端发送的消息丢失。
当ack:client时,客户端收到服务端信息之后必须回复ACK帧。如果在收到客户端回复的ACK之前连接断开,服务端会认为这个消息没有被处理而改发给其他客户端。客户端回复的ACK会被当做累加的处理。这意味着对信息的确认操作不仅仅是确认了这单个的消息,还确认了这个订阅之前发送的所有消息(即接收到一个确认消息就会把之前的消息一起确认掉,批量操作)。
如果客户端没有处理收到的消息,它应该回复一个NACK告诉服务端他没有处理该消息。
当ack:client-individual时,确认操作就跟client模式一样,除了ACK和NACK不是累加的。这意味着当后来的一个消息得到ACK或NACK之后,之前的那个消息没有被ACK或NACK,它需要单独的确认。
UNSUBSCRIBE
UNSUBSCRIBE用来移除一个已经存在订阅,一旦一个订阅被从连接中取消,那么客户端就再也不会收到来自这个订阅的消息。
由于一个连接可以添加多个服务端的订阅,所以id头是UNSUBSCRIBE必须包含的,用来唯一标示要取消的是哪一个订阅。id的值必须是一个已经存在的订阅的标识。
例子:
UNSUBSCRIBE
id:0
^@
ACK
ACK是用来在client和client-individual模式下确认已经收到一个订阅消息的操作。在上述模式下任何订阅消息都被认为是没有被处理的,除非客户端通过回复ACK确认。
ACK中必须包含一个id头,头域内容来自对应的需要确认的MESSAGE的ack头。可以选择的指定一个transaction头,标示这个消息确认动作是这个事务内容的一部分。
ACK
id:12345
transaction:tx1
^@
NACK
NACK是ACK的反向,它告诉服务端客户端没有处理该消息。服务端可以选择性的处理该消息,重新发送到另一个客户端或者丢弃它或者把他放到无效消息队列中记录。
NACK包含和ACK相同的头信息:id(必须)和transaction(非必须)。
NACK适用于对一个单独的消息也适用于对所有的未被ACK或者NACK的消息。
BEGIN
BEGIN用于开启一个事务-transaction。这种情况下的事务适用于发送消息和确认已经收到的消息。在一个事务期间,任何发送和确认的动作都会被当做事务的一个原子操作。
BEGIN
transaction:tx1
^@
帧中transaction头是必须的,并且transaction的标示会被用在SEND、COMMIT、ABORT、ACK和NACK中,使之与该事务绑定。同一个链接中的不同事务必须使用不同的标示。
当客户端发送一个DISCONNECT或者TCP链接由于任何原因断开时,任何打开的但是还没有被提交的事务都会被默认的立即中断。
COMMIT
COMMIT用来提交一个事务到处理队列中。
COMMIT
transaction:tx1
^@
帧中的transaction头是必须得,用以标示是哪个事务被提交。
ABORT
ABORT用于中止正在执行的事务。
ABORT
transaction:tx1
^@
帧中的transaction头是必须得,用以标示是哪个事务被提交。
DISCONNECT
客户端可以通过关闭Socket而随时与服务端断开链接,但是这样不能确认先前发出的帧是否已经到达服务器。如果想要实现正常断开,即客户端已经知道之前的帧都已经成功被服务端接收,客户端应该做如下内容:
1.发送一个带有receipt头的DISCONNECT。
DISCONNECT
receipt:77
^@
2.等待服务端返回对DISCONNECT的RECEIPT。
RECEIPT
receipt-id:77
^@
3.关闭SOCKET。
需要注意的是,如果客户端过快的关闭SOCKET,那么它将收不到RECEIPT,详细内容查看链接延迟章节。
客户端在发送了DISCONNECT链接之后不应该在继续向服务端发送任何帧。
服务端帧介绍
服务端有时会发送除CONNECTED帧之外的下列帧到客户端:MESSAGE、RECEIPT及ERROR。
MESSAGE
MESSAGE用于传输从服务端订阅的消息到客户端。
MESSAGE中必须包含destionation头,用以表示这个消息应该发送的目标。如果这个消息被使用STOMP发送,那么这个destionation应该与相应的SEND帧中的目标一样。
MESSAGE中必须包含message-id头,用来唯一表示发送的是哪一个消息,以及subscription头用来表示接受这个消息的订阅的唯一标示。
如果收到的订阅消息明确表示需要确认,那么MESSAGE中应该包含一个任意值的ack头,这个值被用来在回复确认时标示这条信息。
MESSAGE如果有body内容,则必须包含content-length和content-type头。
例如
MESSAGE
content-length:100
content-type:text/plain
destination:/queue/a
message-id:007
subscription:0
Hello queue a^@
MESSAGE中还可以包含所有用户定义的头以服务端额外特别添加的头。查看你的服务端的文档了解服务端会特别添加的头信息内容。
RECEIPT
RECEIPT用于每当服务端收到来自客户端的需要receipt的帧时发送给客户端。RECEIPT中必须包含receipt-id头,用来表示对谁的回执,receipt-id的值就是需要回执的帧所带的receipt头的值。
RECEIPT
receipt-id:message-1234
^@
RECEIPT是服务端对于相应客户端需要回执的Frame的确认。由于STOMP是基于流的,回执依然是一种服务端对客户端所有需要回执帧的累计的确认,然而,可能之前的某些帧服务端还没有处理完。所以如果此时断开连接,之前服务端收到的帧会被继续处理。
ERROR
如果连接过程中出现什么错误,服务端就会发送ERROR。在这种情况下,服务端发出ERROR之后必须马上断开连接。更多信息查看 链接延迟 章节。
ERROR中应该包含一个message头,表示对错误的简单描述。可能会包含body,表示错误的详细信息。
ERROR
receipt-id:message-1234
content-length:171
content-type:text/plain
message:malformed frame reveived
The message:
————
MESSAGE
receipt:message-1234
destined:/queue/a
Hello queue a!
————
Don’t contain a destination header , which is REQUIRED for message propagation!
^@
如果ERROR关联到客户端发送的某个特别的帧,服务端应该在头域中增加额外的头信息以助于识别发生错误的帧。例如,如果出错的帧包含receipt头,那么服务端应该在头域信息中增加receipt-id头,它的值就是发生错误的帧的receipt的值。
如果ERROR中出现body,那么它应该包含content-type和content-length。
帧和头
对之前章节描述的标准头的扩充,以下是本文档为帧定义的所有头,包括必须和非必须的:
- CONNECT or STOMP
REQUIRED :accept-version,host;
OPTIONAL :login,passcode,heart-beat;
- CONNECTED
REQUIRED :version;
OPTIONAL :session,server,heart-beat;
- SEND
REQUIRED :destination;
OPTIONAL :transaction;
- SUBSCRIBE
REQUIRED :destination,id;
OPTIONAL :ack;
- UNSUBSCRIBE
REQUIRED :id,;
OPTIONAL :ack;
- ACK or NACK
REQUIRED :id;
OPTIONAL :transaction;
- BEGIN or COMMIT or ABORT
REQUIRED :transaction;
OPTIONAL :none
- DISCONNECTED
REQUIRED :none
OPTIONAL :receipt;
- MESSAGE
REQUIRED :destination,message-id,subscription;
OPTIONAL :ack;
- RECEIPT
REQUIRED :receipt-id
OPTIONAL :none
- ERROR
REQUIRED :none
OPTIONAL :message;
另外,SEND和MESSAGE可以包含任意的用户定义的头信息,这些信息可以被当做帧携带的信息的一部分。同样的,ERROR中应该包含有能够定位出出错帧信息的额外的头。
最后STOMP服务器端可以使用额外的头信息允许访问过期或者持久性存储特性。详细内容查看你的服务端的说明。
补充和反馈
一个STOMP回话可以使用HTTP/1.1(RFC 2616)协议中使用的Backus-Naur Form (BNF)语法格式加以描述。
NULL = <US-ASCII null (octet 0)>
LF = <US-ASCII line feed (aka newline) (octet 10)>
CR = <US-ASCII carriage return (octet 13)>
EOL = [CR] LF
OCTET = <any 8-bit sequence data>
frame-stream = 1*frame;
frame = command EOL
*(header EOL)
EOL
*OCTET
NULL
*(EOL)
command = client-command | server-command
client-command = “SEND” | “SUBSCRIBE” | “UNSUBSCRIBE” | “BEGIN” | “COMMIT” |
“ABORT” | “ACK” | “NACK” | “DISCONNECT” | “CONNECT” | “STOMP”
server-command = “CONNECTED” | “RECEIPT” | “ERROR” | “MESSAGE”
header = header-name “:” header-value
header-name = 1* <any of OCTET except CR or LF or “:”>
header-value = *<any of OCTET except CR or LF or “:”>
证书许可
本文档是在 Creative Commons Attribution v3.0 (创作共享署名3.0许可)下的规范。