MQTT 3.1.1,值得升级的6个新特性

前言

以前看英文文章或资料,看完之后,摘要或者忘记。这一次选择感兴趣的MQTT 3.1.1介绍文章资料,引文见文末,作为练手;非完全翻译,去除掉一些广告性描述,若侵权,请告知。

在沉寂了四年之后,QTT 3.1.1规范于2014年10月30号正式发布,与此同时MQTT 3.1.1已成为OASIS(结构化信息标准促进组织)开放物联网消息传递协议标准(连接1 连接2),换种说法就是MQTT 3.1.1已升级为国际物联网标准。

正如HTTP为人们通过万维网分享信息铺平了道路一样,MQTT能将几十亿低成本、嵌入式数据采集遥测设备连接到网络。

与MQTT 3.1(还不是国际物联网标准呢)规范相比,MQTT 3.1.1目标在于消除歧义,尽可能的向后兼容,事实上一些大众所需的新特性被包含在这个版本(更多的是物联网标准推动),因此不仅是一个维护版本,也是一种巨大的进步呢。除了概念的澄清和陈旧规范重写外,有一些很有趣的变化是值得注意的。

会话表示标志(Session Present Flag)

如果一个终端与服务器之间建立一个持久会话连接(假设这个终端没有使用到一个“clean session”标记清除已有回话标志), 一个新增的“Session Present”标志(会话表示标志,逻辑值为true或false)会在CONNACK中出现,表明MQTT服务器已经拥有当前客户端上次连接会话信息,比如订阅的主题,排队信息和其它信息等。

会话表示标志若为true,客户端可减少了一次发送订阅SUBSCRIBLE交互步骤,有助于更有效的数据通信;为false,客户端需要再次发送订阅SUBSCRIBLE消息,不可略过。

新增订阅失败代码反馈

MQTT 3.1.1之前,终端连接之后无法知道其发送的订阅主题是否被MQTT服务器接受与否。此新特性较适用于细粒度权限MQTT主题管理;若无授权,服务器会把错误代码(0x80)附加在SUBACK中,客户端就可以知道订阅失败。

MQTT匿名客户端

需要支持临时或匿名?客户端仅仅需要在发送CONNECT时把客户端标识符( client identifier )置空(零长度)即可,MQTT服务器会为此类请求生成一个随机、唯一客户端标记符。但这要求客户端必须设置Clean Session标记为1,否则服务器端会直接返回包含0x02 (Identifier rejected)代码的CONNACK,同时关闭连接。

可用于后端程序(不需要维护回话状态)向终端发送消息的客户端,MQTT服务器程序可区别对待。

快速发布无等待

这是一个新增的特别有用的特性,客户端可以在发送CONNECT之后,可无须等待MQTT服务器返回的CONNACK,根据需要即刻发送PUBLISH、SUBSCRIBLE、DISCONNEECT等消息,可避免客户端资源等待。此特性也适用于突发模式(burst-mode)客户端需求,只关心数据要尽快的发送出去,而不是去担心是否需要维护一个长连接。

这需要MQTT服务器实现在分发消息之前检查客户端是否有权限发布到这些主题上。

客户端标识符可以变长一些

MQTT 3.1针对客户端标识符( client identifier)限制是23个字节,实际环境下会有所不便,已有遗留系统可能使用UUID作为客户端的标识符,这样服务器端需要做一些彼此之间的MAP映射。 MQTT 3.1.1中上限为65535个字节,毕竟成为业界标准,需要兼容大量的遗留设备和基础设施。

其它小改变

  1. CONNECT消息可变头部协议名称MQIsdp被改为MQTT,语义更准确
  2. 所有字符串明确规定使用UTF-8编码,包括客户端标识符(Client Identifier)
  3. CONNECT消息可变头部协议版本号,由0x03变成了0x04 QoS 0类型PUBLISH消息DUP标记必须被设置为0
  4. MQTT Over WebSocket 被定义,互联网地址编码分配机构(Internet Assigned Numbers Authority)分配标识符为mqtt。虽在MQTT 3.1规范通篇没有提到WebSocket,但因其二进制属性可以很容易的在WebSocket通道传输。

术语变化

  • MQTT代理 -> MQTT服务器(MQTT Broker is now MQTT Server)
  • 消息ID -> 包ID(Message ID is now Packet ID)
  • 消息类型 -> 包类型(Message Type is now Packet Type)
  • 主题路径 -> 主题名称(Subscribe and Unsubscribe take Topic Paths, rather than Topic names)
  • 以前在固定头部,现在在包类型中( Flags in the fixed header are now specific to the packet type
  • 0字节保留信息需要清除 (A zero byte retained message MUST NOT be stored as a retained message on the Server )

小结

当前MQTT 3.1.1已经在很多活跃开源项目/商业产品得到支持。比如Eclipse Paho,Mosquitto,JBoss A-MQ 6.1, Apache ActiveMQ 5.10-SNAPSHOT,Apache Camel 2.13.0,HiveMQ等。

关于Eclipse Paho:

  1. Eclipse Paho 1.0支持MQTT 3.1.1和MQTT 3.1规范
  2. Eclipse Paho 0.9仅支持 MQTT 3.1规范

包含MQTT 3.1.1和MQTT 3.1的客户端可以混合使用,彼此可以共存于同一个MQTT服务器下,在基本消息传输层面没有多大修改,同样的PUBLISH消息可以在MQTT客户端中自由流转,这个需要服务器端编码支持。

已有的MQTT 3.1客户端可以用着急升级,但升级之后可以从新增特性中收益良多。

转自:http://www.blogjava.net/yongboy/archive/2014/12/16/421460.html

发表在 message | 标签为 | MQTT 3.1.1,值得升级的6个新特性已关闭评论

五.MQTT协议笔记之订阅

前言

记忆不太好的时候,只能翻看以前的文章/笔记重新温习一遍,但找不到MQTT协议有关订阅部分的描述,好不容易从Evernote中找到贴出来,这样整个MQTT协议笔记,就比较齐全了。

SUBSCRIBE

一般来讲,客户端在成功建立TCP连接之后,发送CONNECT消息,在得到服务器端授权允许建立彼此连接的CONNACK消息之后,客户端会发送SUBSCRIBE消息,订阅感兴趣的Topic主题列表(至少一个主题),一个完整示范如下:


Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message Type(8) DUP flag QoS level RETAIN
    1 0 0 0 0 0 1 x
byte 2 Remaining Length
Variable header/可变头部
Message Identifier
byte 1 Message ID MSB (0) 0 0 0 0 0 0 0 0
byte 2 Message ID LSB (10) 0 0 0 0 1 0 1 0
Playload/消息体
Topic name
byte 1 Length MSB (0) 0 0 0 0 0 0 0 0
byte 2 Length LSB (3) 0 0 0 0 0 0 1 1
byte 3 'a' (0x61) 0 1 1 0 0 0 0 1
byte 4 '/' (0x2F) 0 0 1 0 1 1 1 1
byte 5 'b' (0x62) 0 1 1 0 0 0 1 0
Requested QoS
byte 6 Requested QoS (1) x x x x x x 0 1
Topic Name
byte 7 Length MSB (0) 0 0 0 0 0 0 0 0
byte 8 Length LSB (3) 0 0 0 0 0 0 1 1
byte 9 'c' (0x63) 0 1 1 0 0 0 1 1
byte 10 '/' (0x2F) 0 0 1 0 1 1 1 1
byte 11 'd' (0x64) 0 1 1 0 0 1 0 0
Requested QoS
byte 12 Requested QoS (2) x x x x x x 1 0

固定头部

Qos Level,可根据实际情况进行调整为0/1/2等。一般设为0表示最多一次。客户端可设置OoS Level值。 DUP flag,值为0表示第一次发送。

可变头部

因为上面示范QoS level值为1,因此需要客户端传递消息ID,16位,无符号的short类型。

消息体

订阅的主题名称采用修改版UTF-8编码,然后紧跟着对应的QoS值。下面的次序,可能更为形象:

Topic name "a/b"
Requested QoS 1
Topic name "c/d"
Requested QoS 2

订阅者的Topic name支持通配符#和+ :

  1. #支持一个主题内任意级别话题
  2. +只匹配一个主题级别的通配符

eg:

finance/stock/#
finance/sotkc/ibm/+

都是有效,更具体规则,请参阅协议附加部分。

在服务器接收处理时,按照顺序读取即可:

String topicName = readUTF();
int qosVal = read();

服务器可以发送QoS不大于客户端设置OoS的消息,尤其是服务器不提供良好的持久化机制的时候。

SUBACK

服务器会对发出SUBSCRIBE的消息返回一个确认消息。


Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (9) DUP flag QoS flags RETAIN
    1 0 0 1 x x x x
byte 2   Remaining Length
Variable header/可变头部
Message Identifier
byte 1 Message ID MSB (0) 0 0 0 0 0 0 0 0
byte 2 Message ID LSB (10) 0 0 0 0 1 0 1 0
Playload/消息体
byte 1 Granted QoS (0) x x x x x x 0 0
byte 1 Granted QoS (2) x x x x x x 1 0

可变头部

Message Identifier,服务器需要附加,客户端需要处理。

消息体

QoS,为服务器根据实际情况授予的QoS级别列表,和客户端发送的SUBSCRIBE的订阅Topic Name顺序完全一致。

客户端订阅几个TOPIC,服务器端一一给出各个TOPIC的QoS具体值。

UNSUBSCRIBE

服务器需要支持客户端取消订阅功能,UNSUBSCRIBE消息格式和SUBSCRIBE消息格式差不多,除了消息类型不同,消息体中没有了QoS字节,其它没有区别。


Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message Type(10) DUP flag QoS level RETAIN
    1 0 1 0 0 0 1 x
byte 2 Remaining Length
Variable header/可变头部
Message Identifier
byte 1 Message ID MSB (0) 0 0 0 0 0 0 0 0
byte 2 Message ID LSB (10) 0 0 0 0 1 0 1 0
Playload/消息体
Topic name
byte 1 Length MSB (0) 0 0 0 0 0 0 0 0
byte 2 Length LSB (3) 0 0 0 0 0 0 1 1
byte 3 'a' (0x61) 0 1 1 0 0 0 0 1
byte 4 '/' (0x2F) 0 0 1 0 1 1 1 1
byte 5 'b' (0x62) 0 1 1 0 0 0 1 0
Topic Name
byte 6 Length MSB (0) 0 0 0 0 0 0 0 0
byte 7 Length LSB (3) 0 0 0 0 0 0 1 1
byte 8 'c' (0x63) 0 1 1 0 0 0 1 1
byte 9 '/' (0x2F) 0 0 1 0 1 1 1 1
byte 10 'd' (0x64) 0 1 1 0 0 1 0 0

可变头部的消息ID的出现还是由固定头部的QoS Level(1)决定是否存在。

一般来讲,客户端发布退订,服务器端需要返回退订确认。

MQTT没讲是否允许客户端退订所有TOPIC。

UNSUBACK

服务器返回的UNSUBSCRIBE消息UNSUBACK相应很简单,没有消息体。


Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (9) DUP flag QoS flags RETAIN
    1 0 1 1 x x x x
byte 2   Remaining length (2)
    0 0 0 0 0 0 1 0
Variable header/可变头部
Message Identifier
byte 1 Message ID MSB (0) 0 0 0 0 0 0 0 0
byte 2 Message ID LSB (10) 0 0 0 0 1 0 1 0

小结

订阅部分,共有四个消息,分别一一对应。

命令 响应 备注 建议
SUBSCRIBE SUBACK 协议没有涉及最多运行订阅TOPIC数目,隐藏的隐患 建议至多10个
UNSUBSCRIBE UNSUBACK 是否可以退订所有订阅,不详 建议保留至少一个Topic

 

转自:http://www.blogjava.net/yongboy/archive/2014/04/12/412351.html

发表在 message | 标签为 | 五.MQTT协议笔记之订阅已关闭评论

四.MQTT协议笔记之消息流

前言

前面的笔记已把所有消息类型都过了一遍,这里从消息流的角度尝试解读一下。

网络故障

在任何网络环境下,都会出现一方连接失败,比如离开公司大门那一刻没有了WIFI信号。但持续连接的另一端-服务器可能不能立即知道对方已断开。类似网络异常情况,都有可能在消息发送的过程中出现,消息发送出去,就丢失了。

MQTT协议假定客户端和服务器端稳定情况一般,彼此之通信管道不可靠,一旦客户端网络断开,情况就会很严重,很难恢复原状。

但别忘记,很多客户端会有永久性存储设备支持,比如闪存ROM、存储卡等,在通信出现异常的情况下可以用于保存关键数据或状态信息等。

总之,异常网络情况很复杂,只能小心处理之。

消息重发策略

QoS > 0情况下,PUBLISH、PUBREL、SUBSCRIBE、UNSUBSCRIBE等类型消息在发送者发送完之后,需要等待一个响应消息,若在一个指定时间段内没有收到,发送者可能需要重试。重发的消息,要求DUP标记要设置为1.

等待响应的超时应该在消息成功发送之后开始算起,并且等待超时应该是可以配置选项,以便在下一次重试的时候,适当加大。比如第一次重试超时10秒,下一次可能为20秒,再一次重试可能为60秒呢。当然,还要有一个重试次数限制的。

还有一种情况,客户端重新连接,但未在可变头部中设置clean session标记,但双方(客户端和服务器端)都应该重试先前未发送的动态消息(in-flight messages)。客户端不被强制要求发送未被确认的消息,但服务器端就得需要重发那些未被去确认的消息。

QoS level决定的消息流

QoS level为Quality of Service level的缩写,翻译成中文,服务质量等级。

MQTT 3.1协议在"4.1 Quality of Service levels and flows"章节中,仅仅讨论了客户端到服务器的发布流程,不太完整。因为决定消息到达率,能够提升发送质量的,应该是服务器发布PUBLISH消息到订阅者这一消息流方向。

QoS level 0

至多发送一次,发送即丢弃。没有确认消息,也不知道对方是否收到。

Client Message and direction Server
QoS = 0 PUBLISH 

---------->
Action: Publish message to subscribers then Forget
Reception: <=1

针对的消息不重要,丢失也无所谓。

网络层面,传输压力小。

QoS level 1

所有QoS level 1都要在可变头部中附加一个16位的消息ID。

SUBSCRIBE和UNSUBSCRIBE消息使用QoS level 1。

针对消息的发布,Qos level 1,意味着消息至少被传输一次。

发送者若在一段时间内接收不到PUBACK消息,发送者需要打开DUB标记为1,然后重新发送PUBLISH消息。因此会导致接收方可能会收到两次PUBLISH消息。针对客户端发布消息到服务器的消息流:

Client Message and direction Server
QoS = 1

DUP = 0
Message ID = x

Action: Store message

PUBLISH 

---------->
Actions:

  • Store message

  • Publish message to subscribers
  • Delete message

Reception: >=1
Action: Discard message PUBACK 

<----------
Message ID = x

针对服务器发布到订阅者的消息流:

Server Message and direction Subscriber
QoS = 1

DUP = 0

Message ID = x
PUBLISH 

---------->
Actions:

  • Store message

  • Make message available                       
Reception: >=1


PUBACK 

<----------
Message ID = x

发布者(客户端/服务器)若因种种异常接收不到PUBACK消息,会再次重新发送PUBLISH消息,同时设置DUP标记为1。接收者以服务器为例,这可能会导致服务器收到重复消息,按照流程,broker(服务器)发布消息到订阅者(会导致订阅者接收到重复消息),然后发送一条PUBACK确认消息到发布者。

在业务层面,或许可以弥补MQTT协议的不足之处:重试的消息ID一定要一致接收方一定判断当前接收的消息ID是否已经接受过

但一样不能够完全确保,消息一定到达了。

QoS level 2

仅仅在PUBLISH类型消息中出现,要求在可变头部中要附加消息ID。

级别高,通信压力稍大些,但确保了仅仅传输接收一次。

先看协议中流程图,Client -> Server方向,会有一个总体印象:

Client Message and direction Server
QoS = 2

DUP = 0
Message ID = x

Action: Store message

PUBLISH 

---------->
Action(a) Store message

or

Actions(b):

  • Store message ID
  • Publish message to subscribers
  PUBREC 

<----------
Message ID = x
Message ID = x PUBREL 

---------->
Actions(a):

  • Publish message to subscribers
  • Delete message

or

Action(b): Delete message ID

Action: Discard message PUBCOMP 

<----------
Message ID = x

Server -> Subscriber

Server Message and direction Subscriber
QoS = 2

DUP = 0

Message ID = x
PUBLISH 

---------->
Action: Store message
  PUBREC 

<----------
Message ID = x
Message ID = x PUBREL 

---------->
Actions:

  • Make message available                       


PUBCOMP 

<----------
Message ID = x

Server端采取的方案a和b,都包含了何时消息有效,何时处理消息。两个方案二选一,Server端自己决定。但无论死采取哪一种方式,都是在QoS level 2协议范畴下,不受影响。若一方没有接收到对应的确认消息,会从最近一次需要确认的消息重试,以便整个(QoS level 2)流程打通。

消息顺序

消息顺序会受许多因素的影响,但对于服务器程序,必须保证消息传递流程的每个阶段要和开始的顺序一致。例如,在QoS level 2定义的消息流中,PUBREL流必须和PUBLISH流具有相同的顺序发送:

Client Message and direction Server
  PUBLISH 1

---------->
PUBLISH 2
---------->
PUBLISH 3
---------->
 
  PUBREC 1

<----------
PUBREC 2
<----------
 
  PUBREL 1

---------->
 
  PUBREC 3

<----------
 
  PUBREL 2

---------->
 
  PUBCOMP 1

<----------
 
  PUBREL 3

---------->
 
  PUBCOMP 2

<----------
PUBCOMP 3
<----------
 

流动消息(in-flight messages)数量允许有一个可保证的效果:

  • 在流动消息(in-flight)窗口1中,每个传递流在下一个流开始之前完成。这保证消息以提交的顺序传递
  • 在流动消息(in-flight)大于1的窗口,只能在QoS level内被保证消息的顺序

消息的持久化

在MQTT协议中,PUBLISH消息固定头部RETAIN标记,只有为1才要求服务器需要持久保存此消息,除非新的PUBLISH覆盖。

对于持久的、最新一条PUBLISH消息,服务器不但要发送给当前的订阅者,并且新的订阅者(new subscriber,同样需要订阅了此消息对应的Topic name)会马上得到推送。

Tip:新来乍到的订阅者,只会取出最新的一个RETAIN flag = 1的消息推送,不是所有。

消息流的编码/解码

MQTT协议中,由目前定义的14种类型消息在客户端和服务器端之间数据进行交互。若以JAVA语言构建MQTT服务器,可选择Netty作为基础。

在Netty中,数据的进入和流出,代表了一次完整的交互。无论是要进入的还是要流出的数据(单独以服务器为例),都可看做字节流。若把每种类型消息抽象为一个具体对象,那么处理起来就不难了。

客户端->服务器,进入的字节流,逐个字节/单位读取,可还原成一个具体的消息对象(解码的过程)。

要发送到客户端的消息对象,转换(编码)成字节流,然后由TCP通道流转到接收者。

小结

断断续续记录了MQTT 3.1协议的若干阅读笔记,总之是把协议个人认为不够清晰,或者我不好理解的地方,着重进行了分析。也便于自己以后回过来头来翻阅,不是那么快的忘却。

转自:http://www.blogjava.net/yongboy/archive/2014/02/15/409893.html

发表在 message | 标签为 | 四.MQTT协议笔记之消息流已关闭评论

三.MQTT协议笔记之发布流程

前言

这次要讲到客户端/服务器的发布消息行为,与PUBLISH相关的消息类型,会在这里看到。

PUBLISH

客户端发布消息经由服务器分发到所有对应的订阅者那里。一个订阅者可以订阅若干个主题(Topic name),但一个PUBLISH消息只能拥有一个主题。

消息架构一览:

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message Type(3) DUP flag QoS level RETAIN
    0 0 1 1 0 0 1 0
byte 2 Remaining Length
Variable header/可变头部
Topic name
byte 1 Length MSB (0) 0 0 0 0 0 0 0 0
byte 2 Length LSB (3) 0 0 0 0 0 0 1 1
byte 3 'a' (0x61) 0 1 1 0 0 0 0 1
byte 4 '/' (0x2F) 0 0 1 0 1 1 1 1
byte 5 'b' (0x62) 0 1 1 0 0 0 1 0
Message Identifier
byte 6 Message ID MSB (0) 0 0 0 0 0 0 0 0
byte 7 Message ID LSB (10) 0 0 0 0 1 0 1 0
Playload/消息体
BLOB,二进制对象形式。二进制具体包含的内容和格式,可有应用程序自身定义。若消息体为空(0长度)也是可能的。

固定头部

DUP flag,设为0,表示当前为第一次发送。

RETAIN flag,只有在PUBLISH消息中才有效。

  • 1:表示发送的消息需要一直持久保存,不但要发送给当前的订阅者,并且以后新来的订阅了此Topic name的订阅者会马上得到推送。 备注:新来乍到的订阅者,只会取出最新的一个RETAIN flag = 1的消息推送,不是所有。
  • 0:仅仅为当前订阅者推送此消息。

可变头部

Topic name,UTF-8编码字符串形式,不支持通配符!

消息体

一般作为UTF-8编码写入接口,但不排除自定义的消息格式。

空的消息体(zero-length)的PUBLISH消息也可以是合法的。

当服务器接收到空消息体(zero-length payload)、retain = 1、具有topic name的一个PUBLISH特殊消息,表示同时满足retain = 1、相同topic name的这两个特征的被持久化PUBLISH消息,可被删除。

Response/响应

固定头部QoS level决定了消息中间件针对发布者具体需要响应的内容:

QoS Level Expected response
QoS 0 None
QoS 1 PUBACK
QoS 2 PUBREC

备注:仅仅针对发布PUBLISH消息的发布者。

Actions:

无论是订阅者还是服务器接收到PUBLISH消息之后,需要根据QoS level执行不同动作。

QoS Level Expected Action
QoS 0 发送到所有感兴趣者
QoS 1 持久化记录下来,发送到所有感兴趣的参与者,返回一个PUBACK消息给发送者
QoS 2 持久化记录下来,暂时不发送所有感兴趣的参与者,返回一个PUBREC消息给发送者

如果服务器收到PUBLISH消息,参与者指的是订阅者。如果订阅者收到PUBLISH消息,参与者就是服务器。 需要注意:

  1. 发布者发布的PUBLISH消息发送到服务器,在payload/消息体处可能夹带有私货,可能含有自定义的数据 格式
  2. 若兼容MQTT客户端,经由服务器分发到所有对应订阅者处只能是规规矩矩的PUBLISH消息,并且固定头部的RETAIN标志不能被设置成有效值1

授权

未经授权的发布者提交的PUBLISH消息,服务器会忽略掉,客户端不会被通知。

PUBACK

作为订阅者/服务器接收(QoS level = 1)PUBLISH消息之后对发送者的响应,整个消息不复杂。

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (4) DUP flag QoS flags RETAIN
    0 1 0 0 x x x x
byte 2   Remaining Length (2)
    0 0 0 0 0 0 1 0
Variable header/可变头部
Message Identifier
byte 1 Message ID MSB (0) 0 0 0 0 0 0 0 0
byte 2 Message ID LSB (10) 0 0 0 0 1 0 1 0

虽没有消息体,但可变头部附加一个16位的无符号short类型。

PUBREC

字面意思为Assured publish received,作为订阅者/服务器对QoS level = 2的发布PUBLISH消息的发送方的响应,确认已经收到,为QoS level = 2消息流的第二个消息。 和PUBACK相比,除了消息类型不同外,其它都是一样。

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (5) DUP flag QoS flags RETAIN
    0 1 0 1 x x x x
byte 2   Remaining Length (2)
    0 0 0 0 0 0 1 0
Variable header/可变头部
Message Identifier
byte 1 Message ID MSB (0) 0 0 0 0 0 0 0 0
byte 2 Message ID LSB (10) 0 0 0 0 1 0 1 0

无论是订阅者还是服务器,在消费PUBREC消息之后需要发送一个PUBREL消息给发送者(和PUBREC具有同样的消息ID),确认已收到。

PUBREL

Qos level = 2的协议流的第三个消息,有PUBLISH消息的发布者发送,参与方接收。完整示范如下:

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (6) DUP flag QoS flags RETAIN
    0 1 1 0 0 0 1 x
byte 2   Remaining Length (2)
    0 0 0 0 0 0 1 0
Variable header/可变头部
Message Identifier
byte 1 Message ID MSB (0) 0 0 0 0 0 0 0 0
byte 2 Message ID LSB (10) 0 0 0 0 1 0 1 0

QoS level 1,PUBREL消息要求如此。

DUP flag 为0,表示消息第一次被发送。

毫无疑问,剩余长度为2个byte长度。

可变头部中,消息ID和发布者接收到的PUBREC所包含的消息ID是一致的。

动作:

  1. 服务器接收到发布者(a)的PUBREL消息,此时服务器让发布者(a)刚才发布PUBLISH消息可用,发送此PUBLISH消息给所有订阅此主题的订阅者,然后发送PUBCOMP消息给发布者(a)
  2. 可变头部包含消息ID和服务器接收的PUBREL消息ID是一致的。 一个订阅者接收到PUBREL消息,订阅者使PUBLISH消息可用,然后反馈一个PUBCOMP消息给服务器

PUBCOMP

作为QoS level = 2消息流第四个,也是最后一个消息,由收到PUBREL的一方向另一方做出的响应消息。

完整的消息一览,和PUBREL一致,除了消息类型。

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (7) DUP flag QoS flags RETAIN
    0 1 1 1 x x x x
byte 2   Remaining Length (2)
    0 0 0 0 0 0 1 0
Variable header/可变头部
Message Identifier
byte 1 Message ID MSB (0) 0 0 0 0 0 0 0 0
byte 2 Message ID LSB (10) 0 0 0 0 1 0 1 0

当客户端接收一个PUBCOMP消息时,客户端摒弃原来的消息,因为它已经成功发送消息到服务器。

小结

消息的发布和确认等一些流程,主要是跟消息发布者所设定的QoS level有关;稍加整理,绘制了下面一张图,理解起来可能会更清晰些:

点击查看原图

上图针对的是客户端发布消息到服务器端的方向。

为了确保消息已经成功传递过去,只有收到了确认,才会让人特别放心。

在QoS level = 2时,通信双方都需要知道各自的确认流程以及所处阶段等,交互很多,数据量大的情况下,可能会造成数据线路传递拥塞。服务器选择QoS = 0/1,大部分情况都是可以应对的。 比如重要消息,就要确保对方都要收到,然后彼此确认,OK,这个消息是真实、有效的。

无论Qos level为0、1,还是2,服务器(具备所有条件都满足之后)总要把收到的具体内容和topic组装成一个新的PUBLISH Message(也不一定要重新构造,但要求推送的PUBLISH消息,一定要具有明确的主题和内容,RETAIN标志不能设置为1)推送到所有感兴趣的订阅者。

嗯,消息的发布来源别忘记还有可能来自CONNECT消息中的Will Topic和Will Message,若是设置了Will flag标记的话。

  

转自:http://www.blogjava.net/yongboy/archive/2014/02/10/409689.html

发表在 message | 标签为 | 三.MQTT协议笔记之发布流程已关闭评论

二.MQTT协议笔记之连接和心跳

前言

本篇会把连接(CONNECT)、心跳(PINGREQ/PINGRESP)、确认(CONNACK)、断开连接(DISCONNECT)和在一起。

CONNECT

像前面所说,MQTT有关字符串部分采用的修改版的UTF-8编码,CONNECT可变头部中协议名称、消息体都是采用修改版的UTF-8编码。前面基本上可变头部内容不多,下面是一个较为完整的CONNECT消息结构:

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部

  Message Type(1) DUP flag QoS level RETAIN
byte 1
  0 0 0 1 x x x x
byte 2 Remaining Length
Variable header/可变头部
Protocol Name
byte 1 Length MSB (0) 0 0 0 0 0 0 0 0
byte 2 Length LSB (6) 0 0 0 0 0 1 1 0
byte 3 'M' 0 1 0 0 1 1 0 1
byte 4 'Q' 0 1 0 1 0 0 0 1
byte 5 'I' 0 1 0 0 1 0 0 1
byte 6 's' 0 1 1 1 0 0 1 1
byte 7 'd' 0 1 1 0 0 1 0 0
byte 8 'p' 0 1 1 1 0 0 0 0
Protocol Version Number
byte 9 Version (3) 0 0 0 0 0 0 1 1
Connect Flags

User Name Flag Password Flag Will Retain Will QoS Will Flag Clean Session Reserved
byte 10
1 1 0 0 1 1 1 x
Keep Alive timer
byte 11 Keep Alive MSB (0) 0 0 0 0 0 0 0 0
byte 12 Keep Alive LSB (10) 0 0 0 0 1 0 1 0
Payload/消息体

Client Identifier(客户端ID)

1-23个字符长度,客户端到服务器的全局唯一标志,如果客户端ID超出23个字符长度,服务器需要返回码为2,标识符被拒绝响应的CONNACK消息。

处理QoS级别1和2的消息ID中,可以使用到。

必填项。

Will Topic

Will Flag值为1,这里便是Will Topic的内容。QoS级别通过Will QoS字段定义,RETAIN值通过Will RETAIN标识,都定义在可变头里面。

Will Message

Will Flag若设为1,这里便是Will Message定义消息的内容,对应的主题为Will Topic。如果客户端意外的断开触发服务器PUBLISH此消息。
长度有可能为0。

在CONNECT消息中的Will Message是UTF-8编码的,当被服务器发布时则作为二进制的消息体。

User Name

如果设置User Name标识,可以在此读取用户名称。一般可用于身份验证。协议建议用户名为不多于12个字符,不是必须。

Password

如果设置Password标识,便可读取用户密码。建议密码为12个字符或者更少,但不是必须。

可变头部

协议名称和协议版本都是固定的。

连接标志(Connect Flags)

一个字节表示,除了第1位是保留未使用,其它7位都具有不同含义。

业务上很重要,对消息总体流程影响很大,需要牢记。

Clean Session

0,表示如果订阅的客户机断线了,要保存为其要推送的消息(QoS为1和QoS为2),若其重新连接时,需将这些消息推送(若客户端长时间不连接,需要设置一个过期值)。 1,断线服务器即清理相关信息,重新连接上来之后,会再次订阅。

Will Flag

定义了客户端(没有主动发送DISCONNECT消息)出现网络异常导致连接中断的情况下,服务器需要做的一些措施。

简而言之,就是客户端预先定义好,在自己异常断开的情况下,所留下的最后遗愿(Last Will),也称之为遗嘱(Testament)。 这个遗嘱就是一个由客户端预先定义好的主题和对应消息,附加在CONNECT的可变头部中,在客户端连接出现异常的情况下,由服务器主动发布此消息。

只有在Will Flag位为1时,Will Qos和Will Retain才会被读取,此时消息体payload中要出现Will Topic和Will Message具体内容,否则,Will QoS和Will Retain值会被忽略掉。

Will Qos

两位表示,和PUBLISH消息固定头部的QoS level含义一样。这里先掠过,到PUBLISH消息再回过头来看看,会更明白些。

若标识了Will Flag值为1,那么Will QoS就会生效,否则会被忽略掉。

Will RETAIN

如果设置Will Flag,Will Retain标志就是有效的,否则它将被忽略。

当客户端意外断开服务器发布其Will Message之后,服务器是否应该继续保存。这个属性和PUBLISH固定头部的RETAIN标志含义一样,这里先掠过。

User name 和 password Flag:

用于授权,两者要么为0要么为1,否则都是无效。都为0,表示客户端可自由连接/订阅,都为1,表示连接/订阅需要授权。

Payload/消息体

消息体定义的消息顺序(如上表所示),约定俗成,不得更改,否则将可能引起混乱。

若Will Flag值为0,那么在payload中,Client Identifer后面就不会存在Will Topic和Will Message内容。

若User Name和Password都为0,意味着Payload/消息体中,找不到User Name和password的值,就算有,也是无效。标志决定着是否读取与否。

心跳时间(Keep Alive timer)

以秒为单位,定义服务器端从客户端接收消息的最大时间间隔。一般应用服务会在业务层次检测客户端网络是否连接,不是TCP/IP协议层面的心跳机制(比如开启SOCKET的SO_KEEPALIVE选项)。 一般来讲,在一个心跳间隔内,客户端发送一个PINGREQ消息到服务器,服务器返回PINGRESP消息,完成一次心跳交互,继而等待下一轮。若客户端没有收到心跳反馈,会关闭掉TCP/IP端口连接,离线。 16位两个字节,可看做一个无符号的short类型值。最大值,2^16-1 = 65535秒 = 18小时。最小值可以为0,表示客户端不断开。一般设为几分钟,比如微信心跳周期为300秒。

Will Message编码

Will Message在CONNECT Payload/息体中,使用UTF-8编码。假设内容为“abcd”,大概如下:

  Description 7 6 5 4 3 2 1 0
byte 1 Length MSB (0) 0 0 0 0 0 0 0 0
byte 2 Length LSB (4) 0 0 0 0 0 1 0 0
byte 3 'a' (0x61) 0 1 1 0 0 0 0 1
byte 4 'b' (0x62) 0 1 1 0 0 0 1 0
byte 5 'c' (0x63) 0 1 1 0 0 0 1 1
byte 6 'd' (0x64) 0 1 1 0 0 1 0 0

有一点需要记住,PUBLISH的Payload/消息体中以二进制编码保存。

某刻客户端异常关闭触发服务器会PUBLISH此消息。那么服务器会直接把byte3-byte6之间字符取出,保存为二进制,附加到PUBLISH消息体中,大概存储如下:

  Description 7 6 5 4 3 2 1 0
byte 1 'a' (0x61) 0 1 1 0 0 0 0 1
byte 2 'b' (0x62) 0 1 1 0 0 0 1 0
byte 3 'c' (0x63) 0 1 1 0 0 0 1 1
byte 4 'd' (0x64) 0 1 1 0 0 1 0 0

另外,MQTT 3.1协议对Will message的说明很容易引起误解,3.1.1草案已经得到修正。

相关说明:

http://mqtt.org/wiki/doku.php/willmessageutf8_support

https://tools.oasis-open.org/issues/browse/MQTT-2

连接异常中断通知机制

CONNECT消息一旦设置在可变头部设置了Will flag标记,那就启用了Last-Will-And-Testament特性,此特性很赞。

一旦客户端出现异常中断,便会触发服务器发布Will Message消息到Will Topic主题上去,通知Will Topic订阅者,对方因异常退出。

接收CONNECT后的响应动作

接收到CONNECT消息之后,服务器应该返回一个CONNACK消息作为响应:

  1. 若客户端绕过CONNECT消息直接发送其它类型消息,服务器应关闭此非法连接 若客户端发送CONNECT之后未收到CONNACT,需要关闭当前连接,然后重新连接
  2. 相同Client ID客户端已连接到服务器,先前客户端必须断开连接后,服务器才能完成新的客户端CONNECT连接 客户端发送无效非法CONNECT消息,服务器需要关闭

CONNACK

一个完整的CONNACK消息大致如下:

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (2) DUP flag QoS flags RETAIN
    0 0 1 0 x x x x
byte 2   Remaining Length (2)
    0 0 0 0 0 0 1 0
Variable header/可变头部
Topic Name Compression Response
byte 1 Reserved values. Not used. x x x x x x x x
Connect Return Code
byte 2 Return Code                

可变头部第一个字节为保留,无甚用处。第二个字节为连接握手返回码:

返回值 16进制 含义
0 0x00 Connection Accepted
1 0x01 Connection Refused: unacceptable protocol version
2 0x02 Connection Refused: identifier rejected
3 0x03 Connection Refused: server unavailable
4 0x04 Connection Refused: bad user name or password
5 0x05 Connection Refused: not authorized
6-255   Reserved for future use

只有0-5目前被使用到,其他值有待日后使用。一般返回值为0x00,表示连接建立。非法的请求,需要返回相应的数值。

从上面看出,一个CONNACT,四个字节表示。一个正常的CONNACT消息实际内容可能如下: 0x20 0x02 0x00 0x00

若是在私有协议中,两个字节就足够了。

很多时候,客户端和服务器端在没有消息传递时,会一直保持着连接。虽然不能依靠TCP心跳机制(比如SO_KEEPALIVE选项),业务层面定义心跳机制,会让连接状态检测、控制更为直观。

PINGREQ

由客户端发送到服务器端,证明自己还在一直连接着呢。两个字节,固定值。

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (12) DUP flag QoS flags RETAIN
    1 1 0 0 x x x x
byte 2   Remaining Length (0)
    0 0 0 0 0 0 0 0

客户端会在一个心跳周期内发送一条PINGREQ消息到服务器端。

心跳频率在CONNECT可变头部“Keep Alive timer”中定义时间,单位为秒,无符号16位short表示。

PINGRESP

服务器收到PINGREQ请求之后,会立即响应一个两个字节固定格式的PINGRESP消息。

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (13) DUP flag QoS flags RETAIN
    1 1 0 1 x x x x
byte 2   Remaining Length (0)
    0 0 0 0 0 0 0 0

服务器一般若在1.5倍的心跳周期内接收不到客户端发送的PINGREQ,可考虑关闭客户端的连接描述符。此时的关闭连接的行为和接收到客户端发送DISCONNECT消息的处理行为一致,但对客户端的订阅不会产生影响(不会清除客户端订阅数据),这个需要牢记。

若客户端发送PINGREQ之后的一个心跳周期内接收不到PINGRESP消息,可考虑关闭TCP/IP套接字连接。

DISCONNECT

客户端主动发送到服务器端,表明即将关闭TCP/IP连接。此时要求服务器要完整、干净的进行断开处理,不能仅仅类似于关闭连接描述符类似草草处理之。 需要两个字节,值固定:

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (14) DUP flag QoS flags RETAIN
    1 1 1 0 x x x x
byte 2   Remaining Length (0)
    0 0 0 0 0 0 0 0

服务器要根据先前此客户端在发送CONNECT消息可变头部Connect flag中的“Clean session flag”所设置值,再次复习一下:

  1. 值为0,服务器必须在客户端断开之后继续存储/保持客户端的订阅状态。这些状态包括:

    • 存储订阅的消息QoS1和QoS2消息
    • 正在发送消息期间连接丢失导致发送失败的消息
    • 以便当客户端重新连接时以上消息可以被重新传递。
  2. 值为1,服务器需要立刻清理连接状态数据。

有一点需要牢记,服务器在接收到客户端发送的DISCONNECT消息之后,需要主动关闭TCP/IP连接。

转自:http://www.blogjava.net/yongboy/archive/2014/02/09/409630.html

发表在 message | 标签为 | 二.MQTT协议笔记之连接和心跳已关闭评论

一.MQTT协议笔记之头部信息

前言

MQTT(Message Queue Telemetry Transport),遥测传输协议,提供订阅/发布模式,更为简约、轻量,易于使用,针对受限环境(带宽低、网络延迟高、网络通信不稳定),可以简单概括为物联网打造,官方总结特点如下:

1.使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合。
2. 对负载内容屏蔽的消息传输。
3. 使用 TCP/IP 提供网络连接。
4. 有三种消息发布服务质量:
    “至多一次”,消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。
    “至少一次”,确保消息到达,但消息重复可能会发生。
    “只有一次”,确保消息到达一次。这一级别可用于如下情况,在计费系统中,消息重复或丢失会导致不正确的结果。
5. 小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量。
6. 使用 Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。

MQTT 3.1协议在线版本: http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html

官方下载地址: http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/MQTT_V3.1_Protocol_Specific.pdf

PDF版本,42页,不算多。

另外,目前MQTT大家都用在了手机推送,可能还有很多的使用方式,有待进一步的探索。

协议方面,以前曾简单实现过一点HTTP协议,基于HTTP上构建若干种通信管道的socket.io协议,不过socket.io 0.9版本的协议才两三页而已。面对领域不同,自然解决的方式也不一样。

阅读完毕MQTT协议,有一个想法,其实可以基于MQTT协议,打造更加私有、精简(协议一些地方,略显多余)的传输协议,比如一个字节的传输开销。有时间,会详细说一下。

固定头部

固定头部,使用两个字节,共16位:

bit 7 6 5 4 3 2 1 0
byte 1 Message Type DUP flag QoS level RETAIN
byte 2 Remaining Length

第一个字节(byte 1)

消息类型(4-7),使用4位二进制表示,可代表16种消息类型:

Mnemonic Enumeration Description
Reserved 0 Reserved
CONNECT 1 Client request to connect to Server
CONNACK 2 Connect Acknowledgment
PUBLISH 3 Publish message
PUBACK 4 Publish Acknowledgment
PUBREC 5 Publish Received (assured delivery part 1)
PUBREL 6 Publish Release (assured delivery part 2)
PUBCOMP 7 Publish Complete (assured delivery part 3)
SUBSCRIBE 8 Client Subscribe request
SUBACK 9 Subscribe Acknowledgment
UNSUBSCRIBE 10 Client Unsubscribe request
UNSUBACK 11 Unsubscribe Acknowledgment
PINGREQ 12 PING Request
PINGRESP 13 PING Response
DISCONNECT 14 Client is Disconnecting
Reserved 15 Reserved

除去0和15位置属于保留待用,共14种消息事件类型。

DUP flag(打开标志)

保证消息可靠传输,默认为0,只占用一个字节,表示第一次发送。不能用于检测消息重复发送等。只适用于客户端或服务器端尝试重发PUBLISH, PUBREL, SUBSCRIBE 或 UNSUBSCRIBE消息,注意需要满足以下条件:

 当QoS > 0
 消息需要回复确认

此时,在可变头部需要包含消息ID。当值为1时,表示当前消息先前已经被传送过。

QoS(Quality of Service,服务质量)

使用两个二进制表示PUBLISH类型消息:

QoS value bit 2 bit 1 Description
0 0 0 至多一次 发完即丢弃 <=1
1 0 1 至少一次 需要确认回复 >=1
2 1 0 只有一次 需要确认回复 =1
3 1 1 待用,保留位置

RETAIN(保持)

仅针对PUBLISH消息。不同值,不同含义:

1:表示发送的消息需要一直持久保存(不受服务器重启影响),不但要发送给当前的订阅者,并且以后新来的订阅了此Topic name的订阅者会马上得到推送。

备注:新来乍到的订阅者,只会取出最新的一个RETAIN flag = 1的消息推送。

0:仅仅为当前订阅者推送此消息。

假如服务器收到一个空消息体(zero-length payload)、RETAIN = 1、已存在Topic name的PUBLISH消息,服务器可以删除掉对应的已被持久化的PUBLISH消息。

如何解析

因为java使用有符号(最高位为符号位)数据表示,byte范围:-128-127。该字节的最高位(左边第一位),可能为1。若直接转换为byte类型,会出现负数,这是一个雷区。DataInputStream提供了int readUnsignedByte()读取方式,请注意。下面演示了,如何从一个字节中,获取到所有定义的信息,同时绕过雷区:

public static void main(String[] args) {
    byte publishFixHeader = 50;// 0 0 1 1 0 0 1 0

    doGetBit(publishFixHeader);
    int ori = 224;//1110000,DISCONNECT ,Message Type (14)
    byte flag = (byte) ori; //有符号byte       
    doGetBit(flag);
    doGetBit_v2(ori);
}


public static void doGetBit(byte flags) {
    boolean retain = (flags & 1) > 0;
    int qosLevel = (flags & 0x06) >> 1;
    boolean dupFlag = (flags & 8) > 0;
    int messageType = (flags >> 4) & 0x0f;

    System.out.format(
            "Message type:%d, DUP flag:%s, QoS level:%d, RETAIN:%s\n",
            messageType, dupFlag, qosLevel, retain);
}

public static void doGetBit_v2(int flags) {
    boolean retain = (flags & 1) > 0;
    int qosLevel = (flags & 0x06) >> 1;
    boolean dupFlag = (flags & 8) > 0;
    int messageType = flags >> 4;

    System.out.format(
            "Message type:%d, DUP flag:%s, QoS level:%d, RETAIN:%s\n",
            messageType, dupFlag, qosLevel, retain);
}

处理Remaining Length(剩余长度)

在当前消息中剩余的byte(字节)数,包含可变头部和负荷(称之为内容/body,更为合适)。单个字节最大值:01111111,16进制:0x7F,10进制为127。单个字节为什么不能是11111111(0xFF)呢?因为MQTT协议规定,第八位(最高位)若为1,则表示还有后续字节存在。同时MQTT协议最多允许4个字节表示剩余长度。那么最大长度为:0xFF,0xFF,0xFF,0x7F,二进制表示为:11111111,11111111,11111111,01111111,十进制:268435455 byte=261120KB=256MB=0.25GB 四个字节之间值的范围:

Digits From To
1 0 (0x00) 127 (0x7F)
2 128 (0x80, 0x01) 16 383 (0xFF, 0x7F)
3 16 384 (0x80, 0x80, 0x01) 2 097 151 (0xFF, 0xFF, 0x7F)
4 2 097 152 (0x80, 0x80, 0x80, 0x01) 268 435 455 (0xFF, 0xFF, 0xFF, 0x7F)

如何换算成十进制呢 ? 使用java语言表示如下:

public static void main(String[] args) throws IOException {
    // 模拟客户端写入
   ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
   DataOutputStream dataOutputStream = new DataOutputStream(arrayOutputStream);
   dataOutputStream.write(0xff);
   dataOutputStream.write(0xff);
   dataOutputStream.write(0xff);
   dataOutputStream.write(0x7f);

   InputStream arrayInputStream = new ByteArrayInputStream(arrayOutputStream.toByteArray());

    // 模拟服务器/客户端解析
   System. out.println( "result is " + bytes2Length(arrayInputStream));
}

/**
* 转化字节为 int类型长度
* @param in
* @return
* @throws IOException
*/
private static int bytes2Length(InputStream in) throws IOException {
    int multiplier = 1;
    int length = 0;
    int digit = 0;
    do {
        digit = in.read(); //一个字节的有符号或者无符号,转换转换为四个字节有符号 int类型
        length += (digit & 0x7f) * multiplier;
        multiplier *= 128;
   } while ((digit & 0x80) != 0);

    return length;
}

一般最后一个字节小于127(01111111),和0x80(10000000)进行&操作,最终结果都为0,因此计算会终止。代理中间件和请求者,中间传递的是字节流Stream,自然要从流中读取,逐一解析出来。

那么如何将int类型长度解析为不确定的字节值呢?

public static void main(String[] args) throws IOException {
    // 模拟服务器/客户端写入
   ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
   DataOutputStream dataOutputStream = new DataOutputStream(
             arrayOutputStream);

    // 模拟服务器/客户端解析
    length2Bytes(dataOutputStream, 128);
}

/**
* int类型长度解析为1-4个字节
* @param out
* @param length
* @throws IOException
*/
private static void length2Bytes(OutputStream out, int length)
         throws IOException {
    int val = length;
    do {
         int digit = val % 128;
        val = val / 128;
         if (val > 0)
             digit = digit | 0x80;

        out.write(digit);
   } while (val > 0);
}

digit对val求模,最大值可能是127,一旦127 | 10000000 = 11111111 = 0xff = 255 请注意:剩余长度,只在固定头部中,无论是一个字节,还是四个字节,不能被算作可变头部中。

可变头部

固定头部仅定义了消息类型和一些标志位,一些消息的元数据,需要放入可变头部中。可变头部内容字节长度 + Playload/负荷字节长度 = 剩余长度,这个是需要牢记的。可变头部,包含了协议名称,版本号,连接标志,用户授权,心跳时间等内容,这部分和后面要讲到的CONNECT消息类型,有重复,暂时略过。

Playload/消息体/负荷

消息体主要是为配合固定/可变头部命令(比如CONNECT可变头部User name标记若为1则需要在消息体中附加用户名称字符串)而存在。

CONNECT/SUBSCRIBE/SUBACK/PUBLISH等消息有消息体。PUBLISH的消息体以二进制形式对待。

请记住,MQTT协议只允许在PUBLISH类型消息体中使用自定义特性,在固定/可变头部想加入自定义私有特性,就免了吧。这也是为了协议免于流于形式,变得很分裂也为了兼顾现有客户端等。比如支持压缩等,那就可以在Playload中定义数据支持,在应用中进行读取处理。

这部分会在后面详细论述。

消息标识符/消息ID

固定头中的QoS level标志值为1或2时才会在:PUBLISH,PUBACK,PUBREC,PUBREL,PUBCOMP,SUBSCRIBE,SUBACK,UNSUBSCRIBE,UNSUBACK等消息的可变头中出现。

一个16位无符号位的short类型值(值不能为 0,0做保留作为无效的消息ID),仅仅要求在一个特定方向(服务器发往客户端为一个方向,客户端发送到服务器端为另一个方向)的通信消息中必须唯一。比如客户端发往服务器,有可能存在服务器发往客户端会同时存在重复,但不碍事。

可变头部中,需要两个字节的顺序是MSB(Most Significant Bit) LSB(Last/Least Significant Bit),翻译成中文就是,最高有效位,最低有效位。最高有效位在最低有效位左边/上面,表示这是一个大端字节/网络字节序,符合人的阅读习惯,高位在最左边。

bit 7 6 5 4 3 2 1 0
  Message Identifier MSB
  Message Identifier LSB

但凡如此表示的,都可以视为一个16位无符号short类型整数,两个字节表示。在JAVA中处理比较简单:

DataInputStream.readUnsignedShort

或者

in.read() * 0xFF + in.read();

最大长度可为: 65535

UTF-8编码

有关字符串,MQTT采用的是修改版的UTF-8编码,一般形式为如下,需要牢记:

bit 7 6 5 4 3 2 1 0
byte 1 String Length MSB
byte 2 String Length LSB
bytes 3 ... Encoded Character Data


比如AVA,使用writeUTF()方法写入一串文字“OTWP”,头两个字节为一个完整的无符号数字,代表字符串字节长度,后面四个字节才是字符串真正的长度,共六个字节:

bit 7 6 5 4 3 2 1 0
byte 1 Message Length MSB (0x00)
  0 0 0 0 0 0 0 0
byte 2 Message Length LSB (0x04)
  0 0 0 0 0 1 0 0
byte 3 'O' (0x4F)
  0 1 0 0 1 1 1 1
byte 4 'T' (0x54)
  0 1 0 1 0 1 0 0
byte 5 'W' (0x57)
  0 1 0 1 0 1 1 1
byte 6 'P' (0x50)
  0 1 0 1 0 0 0 0

这点,在程序中,可不用单独处理默认,直接使用readUTF()方法,可自动省去了处理字符串长度的麻烦。当然,可以手动读取字符串:

// 模拟写入
dataOutputStream.writeUTF( "abcd");// 2 + 4 = 6 byte
......
// 模拟读取 
int decodedLength = dataInputStream.readUnsignedShort();//2 byte
byte[] decodedString = new byte[decodedLength]; // 4 bytes
dataInputStream.read(decodedString);
String target = new String(decodedString, "UTF-8");

等同于:

String target = dataInputStream.readUTF();

MQTT无论是可变头部还是消息体中,只要是字符串部分,都是采用了修改版的UTF-8编码,读取和写入,借助DataInputStream/DataOutputStream的帮助,一行语句,略去了手动处理的麻烦。

小结

总之,掌握固定头部的QoS level、RETAIN标记、可变头部的Connect flags作用和意义,对总体理解MQTT作用很大。

 

转自:http://www.blogjava.net/yongboy/archive/2014/02/07/409587.html

 

发表在 message | 标签为 | 一.MQTT协议笔记之头部信息已关闭评论

mqtt笔记

MQTT(Message Queue Telemetry Transport),遥测传输协议,提供订阅/发布模式,更为简约、轻量,易于使用,针对受限环境(带宽低、网络延迟高、网络通信不稳定),可以简单概括为物联网打造,官方总结特点如下:

1.使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合。
2. 对负载内容屏蔽的消息传输。
3. 使用 TCP/IP 提供网络连接。
4. 有三种消息发布服务质量:
    “至多一次”,消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。
    “至少一次”,确保消息到达,但消息重复可能会发生。
    “只有一次”,确保消息到达一次。这一级别可用于如下情况,在计费系统中,消息重复或丢失会导致不正确的结果。
5. 小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量。
6. 使用 Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。

MQTT 3.1协议在线版本: http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html

官方下载地址: http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/MQTT_V3.1_Protocol_Specific.pdf

PDF版本,42页,不算多。

目前MQTT大家都用在了手机推送,可能还有很多的使用方式,有待进一步的探索。

 

固定头部

固定头部,使用两个字节,共16位:

bit 7 6 5 4 3 2 1 0
byte 1 Message Type DUP flag QoS level RETAIN
byte 2 Remaining Length

第一个字节(byte 1)

消息类型(4-7),使用4位二进制表示,可代表16种消息类型:

Mnemonic Enumeration Description
Reserved 0 Reserved
CONNECT 1 Client request to connect to Server
CONNACK 2 Connect Acknowledgment
PUBLISH 3 Publish message
PUBACK 4 Publish Acknowledgment
PUBREC 5 Publish Received (assured delivery part 1)
PUBREL 6 Publish Release (assured delivery part 2)
PUBCOMP 7 Publish Complete (assured delivery part 3)
SUBSCRIBE 8 Client Subscribe request
SUBACK 9 Subscribe Acknowledgment
UNSUBSCRIBE 10 Client Unsubscribe request
UNSUBACK 11 Unsubscribe Acknowledgment
PINGREQ 12 PING Request
PINGRESP 13 PING Response
DISCONNECT 14 Client is Disconnecting
Reserved 15 Reserved

除去0和15位置属于保留待用,共14种消息事件类型。

DUP flag(打开标志)

保证消息可靠传输,默认为0,只占用一个字节,表示第一次发送。不能用于检测消息重复发送等。只适用于客户端或服务器端尝试重发PUBLISH, PUBREL, SUBSCRIBE 或 UNSUBSCRIBE消息,注意需要满足以下条件:

 当QoS > 0
 消息需要回复确认

此时,在可变头部需要包含消息ID。当值为1时,表示当前消息先前已经被传送过。

QoS(Quality of Service,服务质量)

使用两个二进制表示PUBLISH类型消息:

QoS value bit 2 bit 1 Description
0 0 0 至多一次 发完即丢弃 <=1
1 0 1 至少一次 需要确认回复 >=1
2 1 0 只有一次 需要确认回复 =1
3 1 1 待用,保留位置

RETAIN(保持)

仅针对PUBLISH消息。不同值,不同含义:

1:表示发送的消息需要一直持久保存(不受服务器重启影响),不但要发送给当前的订阅者,并且以后新来的订阅了此Topic name的订阅者会马上得到推送。

备注:新来乍到的订阅者,只会取出最新的一个RETAIN flag = 1的消息推送。

0:仅仅为当前订阅者推送此消息。

假如服务器收到一个空消息体(zero-length payload)、RETAIN = 1、已存在Topic name的PUBLISH消息,服务器可以删除掉对应的已被持久化的PUBLISH消息。

Remaining Length(剩余长度)

在当前消息中剩余的byte(字节)数,包含可变头部和负荷(内容)。

单个字节最大值:01111111,16进制:0x7F,10进制为127。

MQTT协议规定,第八位(最高位)若为1,则表示还有后续字节存在。

MQTT协议最多允许4个字节表示剩余长度。最大长度为:0xFF,0xFF,0xFF,0x7F,二进制表示为:11111111,11111111,11111111,01111111,十进制:268435455 byte=261120KB=256MB=0.25GB 四个字节之间值的范围:

Digits From To
1 0 (0x00) 127 (0x7F)
2 128 (0x80, 0x01) 16 383 (0xFF, 0x7F)
3 16 384 (0x80, 0x80, 0x01) 2 097 151 (0xFF, 0xFF, 0x7F)
4 2 097 152 (0x80, 0x80, 0x80, 0x01) 268 435 455 (0xFF, 0xFF, 0xFF, 0x7F)

其实换个方式理解:第1字节的基数是1,而第2字节的基数:128,以此类推,第三字节的基数是:128*128=2的14次方,第四字节是:128*128*128=2的21次方;

例如,需要表达321=2*128+65.(2字节):10100001 0000 0011.

(和我们理解的低位运算放置顺序不一样,第一个字节是低位,后续字节是高位,但字节内部本身是低位右边,高位左边)。

可变头部

固定头部仅定义了消息类型和一些标志位,一些消息的元数据,需要放入可变头部中。可变头部内容字节长度 + Playload/负荷字节长度 = 剩余长度。

可变头部,包含了协议名称,版本号,连接标志,用户授权,心跳时间等内容。

可变头部居于固定头部和payload中间。

可变剩余长度(remaing length)不是可变头部的一部分,当然该长度值也是从可变头部开始计算,包含可变头部的长度+payload的长度。

可变头部的字段如下:

协议名称: MQTT CONNECT message. UTF编码:如 MQIsdp, capitalized.

协议版本:8位无符号,当前使用:3 (0x03),如下:

bit 7 6 5 4 3 2 1 0
  Protocol Version
  0 0 0 0 0 0 1 1

Connect flags

Clean session, Will, Will QoS, Retain flags 该字段的设置

一个字节表示,除了第1位是保留未使用,其它7位都具有不同含义。

业务上很重要,对消息总体流程影响很大,需要牢记。

 

Clean session flag

Position: bit 1 ,连接标志.

0:server需要存储client的订阅。包括存储Qos 1和2的订阅主题(当client重连时能将消息发送);当连接丢失的时候 服务器必须维护正在发送的消息的状态直到客户端重新连接到服务器。

1:server MUST忽略之前维护关于client的信息,并且将该connection当成clean的。server MUST 忽略任何client断开的状态。

 

原文翻译不能,所以参考了下一位大牛的表达:

0,表示如果订阅的客户机断线了,要保存为其要推送的消息(QoS为1和QoS为2),若其重新连接时,需将这些消息推送(若客户端长时间不连接,需要设置一个过期值)。 
1,断线服务器即清理相关信息,重新连接上来之后,会再次订阅。

Will Flag

定义了客户端(没有主动发送DISCONNECT消息)出现网络异常导致连接中断的情况下,服务器需要做的一些措施。

简而言之,就是客户端预先定义好,在自己异常断开的情况下,所留下的最后遗愿(Last Will),也称之为遗嘱(Testament)。 这个遗嘱就是一个由客户端预先定义好的主题和对应消息,附加在CONNECT的可变头部中,在客户端连接出现异常的情况下,由服务器主动发布此消息。

只有在Will Flag位为1时,Will Qos和Will Retain才会被读取,此时消息体Playload中要出现Will Topic和Will Message具体内容,否则,Will QoS和Will Retain值会被忽略掉。

Will Qos

两位表示,和PUBLISH消息固定头部的QoS level含义一样。

若标识了Will Flag值为1,那么Will QoS就会生效,否则会被忽略掉。

Will RETAIN

如果设置Will Flag,Will Retain标志就是有效的,否则它将被忽略。

当客户端意外断开服务器发布其Will Message之后,服务器是否应该继续保存。这个属性和PUBLISH固定头部的RETAIN标志含义一样,这里先掠过。

User name 和 password Flag:

用于授权,两者要么为0要么为1,否则都是无效。都为0,表示客户端可自由连接/订阅,都为1,表示连接/订阅需要授权。

 

bit 7 6 5 4 3 2 1 0
  User Name Flag Password Flag Will Retain Will QoS Will Flag Clean Session Reserved
  x x x x x x   x

 

Playload/消息体/负荷

消息体主要是为配合固定/可变头部命令(比如CONNECT可变头部User name标记若为1则需要在消息体中附加用户名称字符串)而存在。

CONNECT/SUBSCRIBE/SUBACK/PUBLISH等消息有消息体。PUBLISH的消息体以二进制形式对待。

MQTT协议只允许在PUBLISH类型消息体中使用自定义特性,在固定/可变头部想加入自定义私有特性是不允许的。

这也是为了协议免于流于形式,变得很分裂也为了兼顾现有客户端等。比如支持压缩等,那就可以在Playload中定义数据支持,在应用中进行读取处理。

这部分会在后面详细论述。

消息标识符/消息ID

固定头中的QoS level标志值为1或2时才会在:PUBLISH,PUBACK,PUBREC,PUBREL,PUBCOMP,SUBSCRIBE,SUBACK,UNSUBSCRIBE,UNSUBACK等消息的可变头中出现。

一个16位无符号位的short类型值(值不能为 0,0做保留作为无效的消息ID),仅仅要求在一个特定方向(服务器发往客户端为一个方向,客户端发送到服务器端为另一个方向)的通信消息中必须唯一。比如客户端发往服务器,有可能存在服务器发往客户端会同时存在重复,但不碍事。

可变头部中,需要两个字节的顺序是MSB(Most Significant Bit) LSB(Last/Least Significant Bit),翻译成中文就是,最高有效位,最低有效位。最高有效位在最低有效位左边/上面,表示这是一个大端字节/网络字节序,符合人的阅读习惯,高位在最左边。

bit 7 6 5 4 3 2 1 0
  Message Identifier MSB
  Message Identifier LSB

最大长度可为: 65535

UTF-8编码

有关字符串,MQTT采用的是修改版的UTF-8编码,一般形式为如下:

bit 7 6 5 4 3 2 1 0
byte 1 String Length MSB
byte 2 String Length LSB
bytes 3 ... Encoded Character Data

 

 最后的结构如下:

 

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
    Message Type(1) DUP flag QoS level RETAIN
byte 1
  0 0 0 1 x x x x
byte 2 Remaining Length
Variable header/可变头部
Protocol Name
byte 1 Length MSB (0) 0 0 0 0 0 0 0 0
byte 2 Length LSB (6) 0 0 0 0 0 1 1 0
byte 3 'M' 0 1 0 0 1 1 0 1
byte 4 'Q' 0 1 0 1 0 0 0 1
byte 5 'I' 0 1 0 0 1 0 0 1
byte 6 's' 0 1 1 1 0 0 1 1
byte 7 'd' 0 1 1 0 0 1 0 0
byte 8 'p' 0 1 1 1 0 0 0 0
Protocol Version Number
byte 9 Version (3) 0 0 0 0 0 0 1 1
Connect Flags
  User Name Flag Password Flag Will Retain Will QoS Will Flag Clean Session Reserved
byte 10
1 1 0 0 1 1 1 x
Keep Alive timer
byte 11 Keep Alive MSB (0) 0 0 0 0 0 0 0 0
byte 12 Keep Alive LSB (10) 0 0 0 0 1 0 1 0
Playload/消息体

Client Identifier(客户端ID)

1-23个字符长度,客户端到服务器的全局唯一标志,如果客户端ID超出23个字符长度,服务器需要返回码为2,标识符被拒绝响应的CONNACK消息。

处理QoS级别1和2的消息ID中,可以使用到。

必填项。

Will Topic

Will Flag值为1,这里便是Will Topic的内容。QoS级别通过Will QoS字段定义,RETAIN值通过Will RETAIN标识,都定义在可变头里面。

Will Message

Will Flag若设为1,这里便是Will Message定义消息的内容,对应的主题为Will Topic。如果客户端意外的断开触发服务器PUBLISH此消息。
长度有可能为0。
在CONNECT消息中的Will Message是UTF-8编码的,当被服务器发布时则作为二进制的消息体。

User Name

如果设置User Name标识,可以在此读取用户名称。一般可用于身份验证。协议建议用户名为不多于12个字符,不是必须。

Password

如果设置Password标识,便可读取用户密码。建议密码为12个字符或者更少,但不是必须。

 

心跳时间(Keep Alive timer)

以秒为单位,定义服务器端从客户端接收消息的最大时间间隔。一般应用服务会在业务层次检测客户端网络是否连接,不是TCP/IP协议层面的心跳机制(比如开启SOCKET的SO_KEEPALIVE选项)。 一般来讲,在一个心跳间隔内,客户端发送一个PINGREQ消息到服务器,服务器返回PINGRESP消息,完成一次心跳交互,继而等待下一轮。若客户端没有收到心跳反馈,会关闭掉TCP/IP端口连接,离线。 16位两个字节,可看做一个无符号的short类型值。最大值,2^16-1 = 65535秒 = 18小时。最小值可以为0,表示客户端不断开。一般设为几分钟,比如微信心跳周期为300秒。

Will Message编码

Will Message在CONNECT Payload/消息体中,使用UTF-8编码。假设内容为“abcd”,大概如下:

  Description 7 6 5 4 3 2 1 0
byte 1 Length MSB (0) 0 0 0 0 0 0 0 0
byte 2 Length LSB (4) 0 0 0 0 0 1 0 0
byte 3 'a' (0x61) 0 1 1 0 0 0 0 1
byte 4 'b' (0x62) 0 1 1 0 0 0 1 0
byte 5 'c' (0x63) 0 1 1 0 0 0 1 1
byte 6 'd' (0x64) 0 1 1 0 0 1 0 0

有一点需要记住,PUBLISH的Payload/消息体中以二进制编码保存。

某刻客户端异常关闭触发服务器会PUBLISH此消息。那么服务器会直接把byte3-byte6之间字符取出,保存为二进制,附加到PUBLISH消息体中,大概存储如下:

  Description 7 6 5 4 3 2 1 0
byte 1 'a' (0x61) 0 1 1 0 0 0 0 1
byte 2 'b' (0x62) 0 1 1 0 0 0 1 0
byte 3 'c' (0x63) 0 1 1 0 0 0 1 1
byte 4 'd' (0x64) 0 1 1 0 0 1 0 0

另外,MQTT 3.1协议对Will message的说明很容易引起误解,3.1.1草案已经得到修正。

相关说明:

http://mqtt.org/wiki/doku.php/willmessageutf8_support

https://tools.oasis-open.org/issues/browse/MQTT-2

连接异常中断通知机制

CONNECT消息一旦设置在可变头部设置了Will flag标记,那就启用了Last-Will-And-Testament特性,此特性很赞。

一旦客户端出现异常中断,便会触发服务器发布Will Message消息到Will Topic主题上去,通知Will Topic订阅者,对方因异常退出。

接收CONNECT后的响应动作

接收到CONNECT消息之后,服务器应该返回一个CONNACK消息作为响应:

  1. 若客户端绕过CONNECT消息直接发送其它类型消息,服务器应关闭此非法连接 若客户端发送CONNECT之后未收到CONNACT,需要关闭当前连接,然后重新连接
  2. 相同Client ID客户端已连接到服务器,先前客户端必须断开连接后,服务器才能完成新的客户端CONNECT连接 客户端发送无效非法CONNECT消息,服务器需要关闭

CONNACK

一个完整的CONNACK消息大致如下:

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (2) DUP flag QoS flags RETAIN
    0 0 1 0 x x x x
byte 2   Remaining Length (2)
    0 0 0 0 0 0 1 0
Variable header/可变头部
Topic Name Compression Response
byte 1 Reserved values. Not used. x x x x x x x x
Connect Return Code
byte 2 Return Code                

可变头部第一个字节为保留,无甚用处。第二个字节为连接握手返回码:

返回值 16进制 含义
0 0x00 Connection Accepted
1 0x01 Connection Refused: unacceptable protocol version
2 0x02 Connection Refused: identifier rejected
3 0x03 Connection Refused: server unavailable
4 0x04 Connection Refused: bad user name or password
5 0x05 Connection Refused: not authorized
6-255   Reserved for future use

只有0-5目前被使用到,其他值有待日后使用。一般返回值为0x00,表示连接建立。非法的请求,需要返回相应的数值。

从上面看出,一个CONNACT,四个字节表示。一个正常的CONNACT消息实际内容可能如下: 0x20 0x02 0x00 0x00

若是在私有协议中,两个字节就足够了。

很多时候,客户端和服务器端在没有消息传递时,会一直保持着连接。虽然不能依靠TCP心跳机制(比如SO_KEEPALIVE选项),业务层面定义心跳机制,会让连接状态检测、控制更为直观。

 

PINGREQ

由客户端发送到服务器端,证明自己还在一直连接着呢。两个字节,固定值。

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (12) DUP flag QoS flags RETAIN
    1 1 0 0 x x x x
byte 2   Remaining Length (0)
    0 0 0 0 0 0 0 0

客户端会在一个心跳周期内发送一条PINGREQ消息到服务器端。

心跳频率在CONNECT可变头部“Keep Alive timer”中定义时间,单位为秒,无符号16位short表示。

PINGRESP

服务器收到PINGREQ请求之后,会立即响应一个两个字节固定格式的PINGRESP消息。

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (13) DUP flag QoS flags RETAIN
    1 1 0 1 x x x x
byte 2   Remaining Length (0)
    0 0 0 0 0 0 0 0

服务器一般若在1.5倍的心跳周期内接收不到客户端发送的PINGREQ,可考虑关闭客户端的连接描述符。此时的关闭连接的行为和接收到客户端发送DISCONNECT消息的处理行为一致,但对客户端的订阅不会产生影响(不会清除客户端订阅数据),这个需要牢记。

若客户端发送PINGREQ之后的一个心跳周期内接收不到PINGRESP消息,可考虑关闭TCP/IP套接字连接。

DISCONNECT

客户端主动发送到服务器端,表明即将关闭TCP/IP连接。此时要求服务器要完整、干净的进行断开处理,不能仅仅类似于关闭连接描述符类似草草处理之。 需要两个字节,值固定:

  Description 7 6 5 4 3 2 1 0
Fixed header/固定头部
byte 1   Message type (14) DUP flag QoS flags RETAIN
    1 1 1 0 x x x x
byte 2   Remaining Length (0)
    0 0 0 0 0 0 0 0

服务器要根据先前此客户端在发送CONNECT消息可变头部Connect flag中的“Clean session flag”所设置值,再次复习一下:

  1. 值为0,服务器必须在客户端断开之后继续存储/保持客户端的订阅状态。这些状态包括:

    • 存储订阅的消息QoS1和QoS2消息
    • 正在发送消息期间连接丢失导致发送失败的消息
    • 以便当客户端重新连接时以上消息可以被重新传递。
  2. 值为1,服务器需要立刻清理连接状态数据。

有一点需要牢记,服务器在接收到客户端发送的DISCONNECT消息之后,需要主动关闭TCP/IP连接。

转自: http://www.cnblogs.com/leeying/p/3791077.html

发表在 message | 标签为 | mqtt笔记已关闭评论

TCP/IP 端口分配的信息

端口号

端口号可以分为三个范围:“已知端口”、“注册端口”以及“动态和/或专用端口”。


  • “已知端口”是从 0 到 1023 的端口。
  • “注册端口”是从 1024 到 49151 的端口。
  • “动态和/或专用端口”是从 49152 到 65535 的端口。

已知端口号

“已知端口”由 IANA 分配,并且在大多数系统中只能由系统(或根)进程或有特权的用户所执行的程序使用。TCP [RFC793] 中使用的端口用于命名进行长期对话的逻辑连接末端。为了向未知的呼叫方提供服务,系统定义了一个服务联系端口。



联系端口有时也称为“已知端口”。为了尽可能利用这些端口,UDP [RFC768] 使用了同样的端口分配。分配的端口只使用了一小部分可用的端口号。很多年以来,分配的端口一直处在 0-255 的范围内。最近,由 IANA 管理的已分配端口的范围扩展到了 0-1023。

注册端口号

“注册端口”由 IANA 列出,并且在大多数系统上可以由普通用户进程或普通用户所执行的程序使用。TCP [RFC793] 中使用的端口用于命名进行长期对话的逻辑连接末端。为了向未知的呼叫方提供服务,系统定义了一个服务联系端口。



IANA 会注册这些端口的使用情况,从而向社区提供方便。为了尽可能利用这些端口,UDP [RFC768] 使用了同样的端口分配。“注册端口”的范围为 1024-49151。



有关特定 TCP/IP 端口分配的信息,请参见 Information Sciences Institute 网站中以下位置的 RFC 1700:

Microsoft 提供了第三方联系信息以便于您寻求技术支持。这些联系信息如有更改,恕不另行通知。Microsoft 不保证这些第三方联系信息的准确性。

 

 

文章来源: https://support.microsoft.com/zh-cn/kb/174904

发表在 technologys | TCP/IP 端口分配的信息已关闭评论

HTML空格占位符

来源于网络:  未验证正确性

&#32; == 普通的英文半角空格

&#160; == &nbsp; == &#xA0; == no-break space (普通的英文半角空格但不换行)

&#12288; == 中文全角空格 (一个中文宽度)

&#8194; == &ensp; == en空格 (半个中文宽度)

&#8195; == &emsp; == em空格 (一个中文宽度)

&#8197; == 四分之一em空格 (四分之一中文宽度)

相比平时的空格(&#32;),nbsp拥有不间断(non-breaking)特性。即连续的nbsp会在同一行内显示。即使有100个连续的nbsp,浏览器也不会把它们拆成两行。

发表在 html | HTML空格占位符已关闭评论

MQTT协议

MQTT - MQ Telemetry Transport

  • 轻量级的 machine-to-machine 通信协议。
  • publish/subscribe模式。
  • 基于TCP/IP。
  • 支持QoS。
  • 适合于低带宽、不可靠连接、嵌入式设备、CPU内存资源紧张。
  • 是一种比较不错的Android消息推送方案。
  • FacebookMessenger采用了MQTT。
  • MQTT有可能成为物联网的重要协议。

消息体

 
点击查看原图

MessageType

点击查看原图
CONNECT
TCP连接建立完毕后,Client向Server发出一个Request。
如果一段时间内接收不到Server的Response,则关闭socket,重新建立一个session连接。
如果一个ClientID已经与服务器连接,则持有同样ClientID的旧有连接必须由服务器关闭后,新建立才能建立。
CONNACK
Server发出Response响应。
0x00 Connection Accepted

0x01 Connection Refused: unacceptable protocol version

0x02 Connection Refused: identifier rejected

0x03 Connection Refused: server unavailable

0x04 Connection Refused: bad user name or password

0x05 Connection Refused: not authorized
PUBLISH 发布消息
Client/Servier均可以进行PUBLISH。
publish message 应该包含一个TopicName(Subject/Channel),即订阅关键词。
关于Topic通配符
/:用来表示层次,比如a/b,a/b/c。
#:表示匹配>=0个层次,比如a/#就匹配a/,a/b,a/b/c。
单独的一个#表示匹配所有。
不允许 a#和a/#/c。
+:表示匹配一个层次,例如a/+匹配a/b,a/c,不匹配a/b/c。
单独的一个+是允许的,a+不允许,a/+/b不允许
PUBACK 发布消息后的确认
QoS=1时,Server向Client发布该确认(Client收到确认后删除),订阅者向Server发布确认。
PUBREC / PUBREL / PUBCOMP
QoS=2时
1. Server->Client发布PUBREC(已收到);
2. Client->Server发布PUBREL(已释放);
3. Server->Client发布PUBCOMP(已完成),Client删除msg;
订阅者也会向Server发布类似过程确认。
PINGREQ / PINGRES 心跳
Client有责任发送KeepAliveTime时长告诉给Server。在一个时长内,发送PINGREQ,Server发送PINGRES确认。
Server在1.5个时长内未收到PINGREQ,就断开连接。
Client在1个时长内未收到PINGRES,断开连接。
一般来说,时长设置为几个分钟。最大18hours,0表示一直未断开。

QoS

点击查看原图
QoS=0:最多一次,有可能重复或丢失。
QoS=1:至少一次,有可能重复。
Client[Qos=1,DUP=0/*重复次数*/,MessageId=x] --->PUBLISH--> Server收到后,存储Message,发布,删除,向Client回发PUBACK
Client收到PUBACK后,删除Message;如果未收到PUBACK,设置DUP++,重新发送,Server端重新发布,所以有可能重复发送消息。
QoS=2:只有一次,确保消息只到达一次(用于比较严格的计费系统)。

Clean Session

如果为false(flag=0),Client断开连接后,Server应该保存Client的订阅信息。
如果为true(flag=1),表示Server应该立刻丢弃任何会话状态信息。

QoS图例:

QoS = 0: 至多一次,不保证消息到达,可能会丢失或重复。server没有response。

QoS = 1: 至少一次,确保消息到达,但是可能会有重复。server向client发送PUBACK(Publish Acknowledgement)


QoS = 2: 只有一次,确保消息到达并只有一次。消息类型有PUBREC(Publish Received 已收到)、PUBREL(Publish Released 已释放)、PUBCOMP(Publish Completed 已完成)。

发表在 message | 标签为 | MQTT协议已关闭评论

SQUID 正向代理

本文摘自:http://www.williamsang.com/archives/1645.html

快速安装

执行yum install squid
复制最下面的配置文件覆盖默认配置文件squid.conf
生成密码文件(见下图)
启动squid:service squid start

配置文件介绍

#################################
###   acl和http_pass访问控制   ###
#################################
acl manager proto cache_object
acl localhost src 127.0.0.1/32 ::1
acl to_localhost dst 127.0.0.0/8 0.0.0.0/32 ::1
 
# 以下定义了局域网地址localnet(需要了解一些网络里面的私有地址概念)
acl localnet src 10.0.0.0/8
acl localnet src 172.16.0.0/12
acl localnet src 192.168.0.0/16
acl localnet src fc00::/7
acl localnet src fe80::/10
 
# 定义SSL_ports为443
acl SSL_ports port 443
 
# 定义了Safe_ports所代表的端口
acl Safe_ports port 80      # http
acl Safe_ports port 21      # ftp
acl Safe_ports port 443     # https
acl Safe_ports port 70      # gopher
acl Safe_ports port 210     # wais
acl Safe_ports port 1025-65535  # unregistered ports
acl Safe_ports port 280     # http-mgmt
acl Safe_ports port 488     # gss-http
acl Safe_ports port 591     # filemaker
acl Safe_ports port 777     # multiling http
 
# 定义CONNECT代表http里的CONNECT请求方法
acl CONNECT method CONNECT
 
# 允许本机管理缓存
http_access allow manager localhost
# 拒绝其他地址管理缓存
http_access deny manager
 
# 拒绝不安全端口请求
http_access deny !Safe_ports
 
# 不允许连接非安全SSL_ports端口
http_access deny CONNECT !SSL_ports
 
# 拒绝连接到本地服务器提供的服务
# (用于保护本机一些只有本机用户才能访问的服务)
# http_access deny to_localhost
 
# 允许局域网用户的请求
http_access allow localnet
# 允许本机用户的请求
http_access allow localhost
 
# 拒绝其他所有请求
http_access deny all
 
#################################
###   squid 服务器基本配置      ###
#################################
 
# Squid的监听端口
http_port 3128
 
#################################
###   squid 缓存配置           ###
#################################
 
# 出现cgi-bin或者?的URL不予缓存
hierarchy_stoplist cgi-bin ?
 
# 磁盘缓存目录
#cache_dir ufs /var/spool/squid 100 16 256
 
# squid挂掉后,临终遗言要放到哪里
coredump_dir /var/spool/squid
 
# 刷新缓存规则
refresh_pattern ^ftp:       1440    20% 10080
refresh_pattern ^gopher:    1440    0%  1440
refresh_pattern -i (/cgi-bin/|\?) 0 0%  0
refresh_pattern .       0   20% 4320

acl控制格式:
acl 列表名称 列表类型 列表值1 列表值2 ...
列表名称:自己定义的,用于下面进行权限控制(allow/deny)。
列表类型:

  • proto:指明URL访问/传输协议。如:http、https、ftp、gopher、urn、whois、cache_object。
  • src:指明IP地址。
  • dst:指明最终服务器IP。
  • port:单独定义端口/端口范围。
  • method:指http的请求方法。包括GET、POST、PUT、HEAD、CONNECT等。

http_access deny/allow acl列表名称
这里需要注意的是:http_access按照定义的顺序来过滤请求的,只要前面被allow/deny了下面就不再判断了。

举个例子:
acl manager proto cache_object
http_access allow manager localhost
http_access deny manager

第一句指明manager为缓存管理,后面限定只能本机进行缓存管理。

squid服务器自身配置

http_port 3128
设置squid监听端口,可以自己更换,客户端链接的时候需要指定这个端口。

缓存配置:

cache_dir ufs /var/spool/squid 100 16 256
这里设置磁盘缓存的目录以及空间大小,目录结构等,格式为:
cache_dir schema directory size L1 L2 [option]

  • schema:默认为ufs,是一种存储机制,一般不修改。
  • directory:指定磁盘缓存存放目录。
  • size:指定cache目录的大小,单位M。
  • L1 L2:一级目录数量和二级目录数量。默认为16 256不需要更改。
  • [option]:可以指定只读和最大文件大小。(一般不用设置)

coredump_dir /var/spool/squid
squid挂掉后,临终遗言要放到哪里,一般也不需要处理

hierarchy_stoplist
后跟着字符串列表,请求的URL里面包含字符串列表里的任何一个字符就不予缓存。
hierarchy_stoplist cgi-bin ?
上述语句表示:任何包含?或cgi-bin字符串的请求的URL不予缓存直接把最终服务器结果返回给用户。

refresh_pattern
使用正则表达式来确定资源过期时间。
refresh_pattern <最小时间> <百分比> <最大时间>

如何判断过期?

如下是squid的refresh_pattern算法的简单描述:
假如响应年龄超过refresh_pattern的max值,该响应过期;
假如LM-factor少于refresh_pattern百分比值,该响应存活;
假如响应年龄少于refresh_pattern的min值,该响应存活;
其他情况下,响应过期。

LM-factor算法:
resource age =对象进入cache的时间-对象的last_modified
response age =当前时间-对象进入cache的时间
LM-factor=(response age)/(resource age)

这里多介绍点额外选项:
设定DNS服务器

  • dns_nameserers 8.8.8.8

日志记录

默认都在/var/log/squid目录下,默认安装时候就配置好了日志轮转,查看/etc/logrotate.d/squid 如果自己更改日志存放位置,最好重新配置自动轮转,关于logrotate参看:logrotate使用

  • cache_log /var/log/cache.log:包括状态性和调试性消息。如果启动失败、报错,可以查看本文件。
  • cache_access_log /var/log/access.log:记录每个终端请求日志,可以设定为/dev/null 不记录日志。
  • cache_store_log /var/log/store.log:对大多数cache管理员来说很有用。包含了进入和离开缓存的每个目标的记录。

启动squid PID 的用户和群组,默认都为squid

  • cache_effective_user squid
  • cache_effective_group squid

内存缓冲大小:设定额外提供多少内存给squid使用。
cache_mem 20M

总内存占用计算方法

squid自身进程占用(10~20M)+ Cache目录放在内存的索引(500M目录大约20M索引)+cache_mem
官方建议可用内存大小要是这里计算量的两倍以上。因此这里的cache_mem不能设置太大。当然内存充足的话,越大越好。

Cache目录放在内存的索引(500M目录大约20M索引)这里其实是估算,没法准确计算,一般64位系统中,每个缓存索引占用112字节内存。这样如果一个缓存文件大小在1K到20K之间则,大概内存花费为:50M~3M之间。其实通过查看/var/spool/squid 目录下的缓存文件大小你可以发现基本都是几K的偏多。所以取20M还是比较合理的。

密码登录
一般我们使用代理都是有密码的,不然谁都可以用,服务器还不得挂掉,怎么在squid中是用密码验证呢?

先设定验证相关的验证参数:
# 密码存储路径,设定通过ncsa_auth程序来读取
auth_param basic program /usr/lib64/squid/ncsa_auth /etc/squid/htpasswd
auth_param basic children 5
# 设定通过验证时,呈现给用户的欢迎信息,可以不写
auth_param basic realm “Welcome to proxy web server”
# 验证一次,可以持续访问多长时间
auth_param basic credentialsttl 12 hours
auth_param basic casesensitive off

# 设定acl密码用户
acl squid_user proxy_auth REQUIRED
# 允许密码用户登录
http_access allow squid_user

生成用户名密码文件,这里使用william作为用户名,test1234作为密码,见下图:
touch /etc/squid/htpasswd
htpasswd /etc/squid/htpasswd william

这里密码最好放在/etc/squid/目录下,不然很容易有权限问题,导致squid启动失败

点击查看原图

注:这里的htpasswd命令是apache服务器自带的。你执行下面的名利安装apache

发表在 web server | 标签为 | SQUID 正向代理已关闭评论

UserAgent

桌面

============================================

IE

  而IE各个版本典型的userAgent如下:

  Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0)

  Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2)

  Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)

  Mozilla/4.0 (compatible; MSIE 5.0; Windows NT)

  其中,版本号是MSIE之后的数字。

注:

MSIE后面跟的数字为IE的版本号,如MSIE 8.0代表IE8, Windows NT 6.1 对应操作系统 windows 7

Windows NT 6.0 对应操作系统 windows vista  

Windows NT 5.2 对应操作系统 windows 2003   

Windows NT 5.1 对应操作系统 windows xp   

Windows NT 5.0 对应操作系统 windows 2000   

UNIX/LINUX下的为X11代替,具体可以从网上找下,百度百科上也有的。

Firefox

  Firefox几个版本的userAgent大致如下:

  Mozilla/5.0 (Windows; U; Windows NT 5.2) Gecko/2008070208 Firefox/3.0.1

  Mozilla/5.0 (Windows; U; Windows NT 5.1) Gecko/20070309 Firefox/2.0.0.3

  Mozilla/5.0 (Windows; U; Windows NT 5.1) Gecko/20070803 Firefox/1.5.0.12  其中,版本号是Firefox之后的数字。

注:N: 表示无安全加密   I: 表示弱安全加密   U: 表示强安全加密     上面的U代表加密等级

Opera

  Opera典型的userAgent如下:

  Opera/9.27 (Windows NT 5.2; U; zh-cn)

  Opera/8.0 (Macintosh; PPC Mac OS X; U; en)

  Mozilla/5.0 (Macintosh; PPC Mac OS X; U; en) Opera 8.0 

  其中,版本号是靠近Opera的数字。

Safari

  Safari典型的userAgent如下:

  Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13

  Mozilla/5.0 (iPhone; U; CPU like Mac OS X) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/4A93 Safari/419.3

  其版本号是Version之后的数字。

Chrome

  目前,Chrome的userAgent是:

Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13 

  其中,版本号在Chrome之后的数字。

Navigator

目前,Navigator的userAgent是:

Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.12) Gecko/20080219 Firefox/2.0.0.12 Navigator/9.0.0.6

其中,版本号在Navigator之后的数字。

360SE                                                                                                                                                                  Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; 360SE)

360

[USER_AGENT] => Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; 360SE)

360极速浏览器

[USER_AGENT] => Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ;  QIHU 360EE)

傲游浏览器

[USER_AGENT] => Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; Maxthon/3.0)

TT

[USER_AGENT] => Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; TencentTraveler 4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) )

safari

[USER_AGENT] => Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.5 Safari/534.55.3

==============================

移动

==============================

安卓 QQ浏览器

Mozilla/5.0 (Linux; U; Android 4.0.3; zh-cn; M032 Build/IML74K) AppleWebKit/533.1 (KHTML, like Gecko)Version/4.0 MQQBrowser/4.1 Mobile Safari/533.1

安卓 原生浏览器

Mozilla/5.0 (Linux; U; Android 4.0.3; zh-cn; M032 Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30

安卓 UC

Mozilla/5.0 (Linux; U; Android 4.0.3; zh-cn; M032 Build/IML74K) UC AppleWebKit/534.31 (KHTML, like Gecko) Mobile Safari/534.31

安卓 Opera

Opera/9.80 (Android 4.0.3; Linux; Opera Mobi/ADR-1210241554) Presto/2.11.355 Version/12.10

三星手机

SAMSUNG-SGH-G508E/G508EZCIG2 SHP/VPP/R5 NetFront/3.4 Qtv5.3 SMM-MMS/1.2.0 profile/MIDP-2.0 configuration/CLDC-1.1

iphone safria

Mozilla/5.0 (iPhone; CPU iPhone OS 5_1_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3

iphone QQ

MQQBrowser/38 (iOS 4; U; CPU like Mac OS X; zh-cn)

iphone UC

IUC(U;iOS 5.1.1;Zh-cn;320*480;)/UCWEB8.9.1.271/42/800

塞班 自带浏览器

Nokia5320/04.13 (SymbianOS/9.3; U; Series60/3.2 Mozilla/5.0; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/413 (KHTML, like Gecko) Safari/413

塞班 QQ浏览器

Nokia5320(19.01)/SymbianOS/9.1 Series60/3.0

发表在 technologys | 标签为 | UserAgent已关闭评论

pfsense 企业应用实例

原文: http://www.pppei.net/blog/post/331

从萌生更换公司网关的想法,到选择、测试、部署陆陆续续用时两个月有余。选择的标准是open and free。这期间不断在查阅一些资料,测试了7、8个各开源防火墙产品。这些产品中大多是基于linux,少量基于BSD。基于linux的给我印象比较深的有ipfire、Zentyal。ipfire很轻量,功能上也能满足,但由于限速是基于linux 的tc,且并没有对tc的操作进行抽象,设置起来反而不如直接使用命令行脚本,所以只是把ipfire列入了候选名单。
zentyal 是基于Ubuntu的,看上去就是Ubuntu+webmin,安装后连X桌面都有,显的很庞大,功能自然不用说,linux有的功能它全有了,包括文件共享、邮件、IM等等,这样看来的确给中小企业节约了时间和成本,如果公司只有50-100来个人这种方案也是不错的选择,一台服务器涵盖企业常见应用。随着企业的成长可能这样的模式会带来一些问题,比如性能、存储空间等等 ,为了需求的增长去升级网关的硬件,显得有点不和谐。过度的集成恐怕日后会有不少问题要面对。既然目的是要选一款网关产品,就不要过度的想让它集成太多与“网关”无关的功能,免除一些不必要的麻烦。
最后目光集中到了pfsense身上,pfsense基于BSD,前身是m0n0wall,它专注构建企业级的安全网关,口碑也不错,国内还有pfsense的社区。pfsense虽然功能强大易用,可还是需要有一定的基础去驾驭它。经常看到有人会遇到各种问题无法解决,最终甚至放弃了pfsense,原因多是不能正确认识pfsense,不能自行分析解决问题。一般企来对网关产品的需求无非是NAT、防火墙、流量管理、流量监控、上网行为管理、vpn、热备、负载均衡。这些功能pfsense都可以做到,而且大多都是强项,下面我结合自身的一点实践,介绍一下我关注过的一些功能,并分享一点点经验。

1.NAT(PAT)

NAT不用过多阐述就是能让所有员工共用公司网络连接互联网。这里不得不提下pfsense NAT功能的一个软肋,就是对PPTP的支持有限,内网同一时刻只能有一个用户连接到外网某台特定的pptp服务器,这时其它用户不能再连接这台pptp服务器。如果其它用户连接这台服务器之外的其它pptp服务器,是不存在问题的。这是因为pptp传输数据时用了GRE(通用路由封装)协议,GRE自身和TCP/UDP处于同一级别(但GRE没有实现OSI中对4层定义的功能,不能认为它是4层协议),GRE和tcp/udp一样自己的数据被封装在ip包中,用IP来承载传输。通常网关对TCP/UDP进行NAT后会保留一个含有源、目的端口的一张状态表,避免内网多个用户使用同一源端口时发生冲突,也为返回的数据提供可以被准确转发到内网用户的依据。特殊的是GRE没有类似端口这样的信息,pfsense只是简单的转换GRE数据包中的IP地址,NAT转换表中不会出现端口信息。当内网某用户已经与外网pptp服务器建立了连接,再有用户尝试连接,服务器端返回的GRE协商数据到达pfsense网关后,这个数据包会按照 之前建立的NAT表转发,于是这个包没有被发送到发起连接的用户,而是转发给了第一个与该pptp服务器建立连接的用户。简单来说就是没有端口信息,NAT表中只能有一条到某一IP的转换条目,无法区分出内网不同用户,也就导致了其它用户无法再连接该pptp服务器。

pfsense的NAT、策略路由、防火墙等功能是通过BSD系统的 pf (packet filter)来实现的,归根结底这是 pf 的一个缺陷。目前这个问题没办法直接解决,只能用个变通方法,即将pptp 1723端口和gre协议的流量重定向到一台Linux服务器。linux 有专门的内核模块解决这个问题, 使用modprobe 加载 ip_conntrack_pptp、nf_nat_pptp 两个模块(iptables的nat 可以对服务器端和客户端的GRE协议的Call id进行转换,实现多个内网用户连接同一台pptp服务器),然后在pfsense中将此linux服务器的IP加到pfsense gateway中,在再firewall rule 的高级里将tcp 1723 和gre协议流量转发给这台linux服务器。

 

2.防火墙

前面提到了pfsense的防火墙、策略路由、NAT等功能都是用BSD下的PF(packet filter)实现的,这也可能是为什么这个项目叫做pfsense的原因。我的防火墙规则是这样的,pfsense网关共三个网卡分别用于WAN、LAN、DMZ,WAN只允许访问DMZ区域中服务器的80等服务端口,DMZ只允许访问互连网,LAN允许访问anywhere。

防火墙规则的高级选项中有很多实用功能,比如7层过滤、重定向、连接数限制、速度限制等,我也是使用了这个方法,对用户进行限速的,请转下一节。

 

3.流量管理

pfsense 有三种流量管理方法。
一是Queue,这种方式适合做QOS(服务质量保证),对数据进行分类,根据不同数据的不同特性,提供不同的服务质量,比如IP电话的语音数据,需要低网络延时,而某用户的下载流量只关心总带宽,这时就可以将ip电话的数据标记出来放到低延时的队列里,下载的流量放到只保证带宽的队列里,网关会用不同的算法转发相应的数据包,达到预定的服务质量。如果要实现对每个IP分别限速,就要为每个ip都建立一个队列,然后将每个IP的数据对应到唯这个队列上;
二是使用Captive portal,这种方式更适合用在无线网络环境中,用户需要打开浏览器输入用户名密码之后才能正常上网,同时为每个用户分配固定的带宽。只要开启了Captive portal,即使关闭了认证,也还是要打开浏览器在弹出的页面上点击确定后才能上网,用在有线环境里太影响用户体验,而且目前Captive portal 只有全局一个实例,2.1版可以针对不同的端口启用不同的实例;
三是使用防火墙的高功特性(这种方法是根据IP地址分别进行限速的)。在防火墙规则中限速有一个缺点,限速和访问控制策略偶合在了一块,失去了灵活性,配置的时候要谨慎一点不然可能会出现限速失效的情况,举个例子,为说明简单这里限速指的是下载限速:
①——-permit tcp any  to host 1.1.1.1 from lan————————
②——-permit any to any and speed limit 2M from Lan——

策略①中没有做流量限制,②中每用户的下载都限制在了2M。防火墙规则匹配是自上到下,匹配成功就不再向下进行,因此去往1.1.1.1的流量匹配了第一条策略,没做限速,第二条规则中的限速也就失效了。

另外再对速度做个说明,同样的策略在第一条上加上限速:
①——-permit tcp any  to host 1.1.1.1 and speed limit at 2M from lan——-
②——-permit any to any and speed limit 2M from Lan——

去往1.1.1.1的流量还会匹配策略①,其它的流量匹配策略②,那么如果用户同时有从1.1.1.1下载的流量和其它从其它地方下载的流量,从1.1.1.1下载.限制在2M,从其它地方下载也被限制在2M,因为是同时产生的流量,所以加起来用户得到了4M带宽。其实结果并不是这样的,在防火墙规则中限速时要先定义limiter,然后在规则中引用。内部是这样实现的,BSD的 PF(packet filter)没有限速功能,定义出来的limiter其实是另一个防火墙实现:IPFW中的组件,这个组件实现了限速,而且这个组件支持PIPE(管道)。PF中将匹配到的下载流量通过PIPE送到IPFW的Limiter中,limiter会根据定义时规定的参照值(源、目的ip,下载参照目的IP),为每个ip生成一个bukket,数据通过bukket的速度是它所属Limiter的速率。回到上面的例子规则 ①②中下载2M的limiter为同一个,同时匹配了两条规则的数据,通过PIPE被送到同一个Limiter中,这个limiter会根据目的ip生成一个BUKKET,这个bukket最大速度2M,用户得到的带宽最大也只是2M。如果规则①中使用了其它limiter比如下载3M的limiter,那么用户最终得到的带宽就是5M了。

  实施方法前边也都说过了,很简单了Firewall ->Traffic Shaper -> limiter 下创建Limiter(需单独为上传下载创建limiter) ,然后在Firewall -> rule->Lan 规则的高级特性 In/Out 中应用limiter。

这里再对Limiter 的源地址和目的地址做个说明,因为limiter是被应用在Lan接口的Rule里,相对pfsense来说,用户发往 Lan口的流量为In,pfsense通过Lan口发给用户的流量为OUT,因此限制上传的limiter因该应用在In方向,limiter的参照值应该为“源IP”,下载的Limiter应该应用在OUT方向,因为是转发给用户的所以这个limiter的参数值应该是“目的IP”。

还有人可能会有疑问应用在Lan口只是转发给用户的速度慢了,从Wan口进来的流量一样不受限制。这种想法是错的,因为TCP有流控机质,UDP的话就要看上层应用的质量了。

在应用了新策略后,之前已经建立的连接是不会受这些规则影响的,可能得不到你想要的效果,需要在Diagnostics->States -> reset status 下清理一下pfsense的状态,中断用户的连接,就能看到效果了。

如果你更习惯在命令行操作,给出一些常用命令,不用和web界面死磕了:

pfctl -sn   #显示nat规则
pfctl -sr   #显示filter规则
pfctl -sa  #显示所有规则
pfctl -ss  #显示防火墙状态
pfctl -F nat #重置防火墙状态(清空NAT状态表)
ipfw pipe show #显示limiter的状态  其中0000x 为limiter的编号 BKT一列就是为每个ip分配的bukket编号

 

4.流量监控

pfsense 自身可以显示出当前每个ip的流量情况,web页面中默认只能显示流量最高的10个,并根据接口的实时流量绘制一张曲线图。我对页面进行了一点修改现在可以显示30个ip的流量,目前足够用了,还想实现按ip、上传、下载流量排序的功能,试了试有点超出我能力范围了,如果懂js就不是什么难事了,pfssense的php代码放在/usr/local/www/ ,函数库放在/etc/inc下,配置文件放在/conf下,想改就自己动手吧。

如果要充计历史流量数据,需要安装附加的package,有两个选择,一个是bandwidthd,另一个是ntop,前者很轻量,后者有点笨重,cpu占用也有点高,而且生成的数据我很难看懂,相反bandwidthd的数据很直观。目前两者都在用 bandwidthd统计Lan口,ntop统计 Wan和DMZ口,看不懂也无所谓,嘿嘿。

从命令行查看每IP实时流量可以用以下命令:
rate -i <接口> -nlq 1 -Aba 30 -c 192.168.1.0/24

 

5.上网行为管理

上网行为管理我觉得应该包括URL过滤、应用层协议识别控制。
首先说URL过滤,pfsense URL过滤用squid + squid guard实现的(需从package中自行安装 ),squid guard 支持正则表达式,我想我就不用说太多了吧,想过滤哪些url 、怎么过滤不都是自己说了算吗。
如果启用url过滤,建议打开Transparent proxy 的选项,这样会在PF的TRANSLATION RULES 中把发往 80的流量重定向到squid的3128端口,不用在用户端做任何的设置。

这里可能会有一个问题,看到社区中有人提到,就是使用了squid 进行 url 过滤,防火墙Rule里的限速就失效了。因为我对PF并不是很精通,我估计原因是这样的,www数据进入Lan接口先通过 TRANSLATION RULES ,数据包在TRANSLATION RULES 里把目的地址改写成了127.0.0.1,端口改写成了3128,然后数据才会继续通过filter rule ,由于某种不能了解的特殊性,没有匹配到任何规则,导致了限速失效。原因明白了对策就很简单了,在Lan接口上新建一个策略源地址为Lan subnet 目的地址为127.0.0.1 端口为3128,高级特性的In/Out中应用对应的limiter就可以了。

应用层协议识别不是pfsense强项,我试图用layer7的特性过滤p2p应用,在上班时间cpu占用到了P(双核),忘记是哪个进程了,所以我个人建议不要用pfsense的这个功能。如果非要对7层进行控制,可以用国产的免费产品panabit ,也是其于freebsd 的,功能非常丰富强大,可以集成到pfsense中去,不过也会有cpu占用率到100%的问题,且免费版有200用户(250?)的限制。

 

6.vpn

这个在企业应用中很普遍了,pfsense可以构建remote vpn(可以用pptp l2tp openvpn) ,供用户在公司以外的地方可以访问公司内网资源,还可以构建site to site vpn 互联公司分部(site2site只能用openvpn)。用openvpn 做sitetosite 有很多优势,比如可以选择性下发路由信息,可以选择使用tcp还是udp,不会像pptp 那样对NAT不友好。

我已经测试并使用了openvpn 集成 windows AD验证。pptp 和l2tp 不能直接使用AD验证,要通过radius间接实现,测试过程中我在AD上安装了radius服务,radius从Ad中进行用户验证。
另外强调下,pfsense是不能用作pptp 或l2tp的客户端的,只能用在当运营商线路需要pptp或l2tp验证的情况。

 

7.热备

有两个角度理解热备,1.线路热备 2.硬件热备。

这部分我没有测试就简单说一下吧,硬件failover通过CARP协议实现的,使用一个虚拟的IP做为用户网关,活动网关拥有该ip的控制权,活动网关死机备用网关自动接管该IP,用户端基本不会有感知。pfsense提供了一个强大的特性,它可以做到两台网关的配置同步,只需修改一台网关的设置,备用网关会自动同步活动网关的配置,方便至极。
线路热备建议配合firewall rule让一部分用户走其中一台线,另一部分用户走另一条线,同时检测运营商线路质量,如果延时大于预设值或丢包大于预设值更或者是网关不可用了,将该线路视为不可用,该规则自动失效所有用户数据自动切换到另一条线路。这样即没有浪费又达到了热备的效果。

 

8.负载均衡

一样的,包括线路负载均衡和硬件负载均衡,除此还多出一个服务器负载均衡。

同样这部分功能没有测试,简单概括一下了。服务器负载均衡用在公司有多台服务器对向提供相同服务时,定义一个服务器池,将这些服务器加入池中,用户连接按照指定的算法分配到池中的不同服务器上。
线路负载均衡上一节中提到了,如果不用firewall rule分配流量,而是通过两条指向不同运营商的路由来实现会存在这样一个问题,数据在发送方被拆为更小的包,在接收方要按顺序进行排序重组,如果一条线路快一线路慢或一条质量好一条质量差,用户会有很明显示的感觉,被慢的线路托慢了。所以建议使用第7节中所说的方法,将用户手动的分配到不同的线路中去。

 

9.其它应用

snort:入侵检测,实际应用中没有看到任何效果,alert中一直是空的,没有深究。
nrpe:使用内网的nagios对pfsense进行磁盘、负载、外网线路延时等数据进行监控报警。

 

总结:

pfsense 将BSD系统的网络功能优势发辉到了极致,虽然它并没有强大到无所不能、没有任何瑕疵,但作为一款开源免费的防火墙产品已经可以让我内牛满面了。在使用pfsense前,最好对自己的需求进行分析,如果觉得可以满足目前和将来的需求,我强列推荐您使用pfsense。同时希望这篇文章可以对你了解使用pfsense起到帮助作用,有也就知足了。最后提醒您远离ISA、远离昂贵商业产品。

发表在 technologys | pfsense 企业应用实例已关闭评论

收集的一些nginx,apache测试参数

本文摘取自以下文件的局部内容:http://zyan.cc/post/366/

 

在高并发连接的情况下,Nginx是Apache服务器不错的替代品。Nginx同时也可以作为7层负载均衡服务器来使用。根据我的测试结果,Nginx 0.7.51 + PHP 5.2.8 (FastCGI) 可以承受3万以上的并发连接数,相当于同等环境下Apache的10倍。

  根据我的经验,4GB内存的服务器+Apache(prefork模式)一般只能处理3000个并发连接,因为它们将占用3GB以上的内存,还得为系统预留1GB的内存。我曾经就有两台Apache服务器,因为在配置文件中设置的MaxClients为4000,当Apache并发连接数达到3800时,导致服务器内存和Swap空间用满而崩溃。

  而这台 Nginx 0.7.51 + PHP 5.2.8 (FastCGI) 服务器在3万并发连接下,开启的10个Nginx进程消耗150M内存(15M*10=150M),开启的64个php-cgi进程消耗1280M内存(20M*64=1280M),加上系统自身消耗的内存,总共消耗不到2GB内存。如果服务器内存较小,完全可以只开启25个php-cgi进程,这样php-cgi消耗的总内存数才500M。

  在3万并发连接下,访问Nginx 0.7.51 + PHP 5.2.8 (FastCGI) 服务器的PHP程序,仍然速度飞快。下图为Nginx的状态监控页面,显示的活动连接数为28457(关于Nginx的监控页配置,会在本文接下来所给出的Nginx配置文件中写明):

  点击在新窗口中浏览此图片

点击查看原图

  我生产环境下的两台Nginx + PHP5(FastCGI)服务器,跑多个一般复杂的纯PHP动态程序,单台Nginx + PHP5(FastCGI)服务器跑PHP动态程序的处理能力已经超过“700次请求/秒”,相当于每天可以承受6000万(700*60*60*24=60480000)的访问量(更多信息见此),而服务器的系统负载也不高:

  点击在新窗口中浏览此图片

点击查看原图

发表在 technologys | 标签为 , | 收集的一些nginx,apache测试参数已关闭评论

FastDFS目录及文件名的一些资料

根据网上的资料,一般单目录下的文件个数一般限制不能够超过3万;同样的,一个目录下面的目录数也最好不要超过这个数。

但实际上,为了安全考虑,一般都不要存储这么多的内容。

假定,一个目录下面,存储1000个文件,每个文件的平均大小为10KB,则单目录下面可存储的容量是10MB。这个容量太小了,所以我们要多个目录,假定有1000个目录,每个目录存储10MB,则可以存储10GB的内容;这对于目前磁盘的容量来说,利用率还是不够的。再想办法,转成两级目录,这样的话,就是第一层目录有1000个子目录,每一级子目录下面又有1000级的二级子目录,每个二级子目录,可以存储10MB的内容,此时就可以存储10T的内容,这基本上超过了目前单机磁盘的容量大小了。

所以,使用二级子目录的办法,是平衡存储性能和利用存储容量的办法。

 

 

 

 

 

服务器收到了文件内容后,如何选择要存储在哪个目录下呢?这个选择要保证均衡性,即尽量保证文件能够均匀地分散在所有的目录下。

负载均衡性很重要的就是哈希,例如,在PHP中常用的md5,其返回一个32个字符,即16字节的输出,即128位。哈希后要变成桶,才能够分布,自然就有了如下的问题:

1- 如何得到哈希值?md5还是SHA1

2- 哈希值得到后,如何构造哈希桶

3- 根据文件名称如何定位哈希桶

首先来回答第3个问题,根据文件名称如何定位哈希桶。很简单,此时我们只有一个文件名称作为输入,首先要计算哈希值,只有一个办法了,就是根据文件名称来得到哈希值。这个函数可以用整个文件名称作为哈希的输入,也可以根据文件名称的一部分来完成。结合上面说的两级目录,而且每级目录不要超过1000.很简单,如果用32位的字符输出后,可以取出实现上来说,由于文件上传是防止唯一性,所以如果根据文件内容来产生哈希,则比较好的办法就是截取其中的4位,例如:

md5sum fdfs_storaged.pid

52edc4a5890adc59cec82cb60f8af691 fdfs_storaged.pid

上面,这个fdfs_storage.pid中,取出最前面的4个字符,即52和ed。这样的话,假如52是一级目录的名称,ed是二级目录的名称。因为每一个字符有16个取值,所以第一级目录就有16 * 16 = 256个。总共就有256 * 256 = 65526个目录。如果每个目录下面存放1000个文件,每个文件30KB,都可以有1966G,即2TB左右。这样的话,足够我们用好。如果用三个字符,即52e作为一级目录,dc4作为二级目录,这样子的目录数有4096,太多了。所以,取二个字符比较好。

这样的话,上面的第2和第3个问题就解决了,根据文件名称来得到md5,然后取4个字符,前面的2个字符作为一级目录名称,后面的2个字符作为二级目录的名称。服务器上,使用一个专门的目录来作为我们的存储根目录,然后下面建立这么多子目录,自然就很简单了。

这些目录可以在初始化的时候创建出来,而不用在存储文件的时候才建立。

也许你会问,一个目录应该不够吧,实际上很多的廉价机器一般都配置2块硬盘,一块是操作系统盘,一块是数据盘。然后这个数据盘挂在一个目录下面,以这个目录作为我们的存储根目录就好了。这样也可以很大程度上减少运维的难度。

现在就剩下最后一个问题了,就是上传文件时候,如何分配一个唯一的文件名称,避免同以前的文件产生覆盖。

如果没有变量作为输入,很显然,只能够采用类似于计数器的方式,即一个counter,每次加一个文件就增量。但这样的方式会要求维护一个持久化的counter,这样比较麻烦。最好不要有历史状态的纪录。

string md5 ( string $str [, bool $raw_output = false ] )

Calculates the MD5 hash of str using the » RSA Data Security, Inc. MD5 Message-Digest Algorithm, and returns that hash.

raw_output

If the optional raw_output is set to TRUE, then the md5 digest is instead returned in raw binary format with a length of 16.

Return Values ¶

Returns the hash as a 32-character hexadecimal number.

为了尽可能地生成唯一的文件名称,可以使用文件长度(假如是100MB的话,相应的整型可能会是4个字节,即不超过2^32, 即uint32_t,只要程序代码中检查一下即可)。但是长度并不能够保证唯一,为了填充尽可能有用的信息,CRC32也是很重要的,这样下载程序后,不用做额外的交互就可以知道文件的内容是否正确。一旦发现有问题,立马要报警,并且想办法修复。这样的话,上传的时候也要注意带上CRC32,以防止在网络传输和实际的硬盘存储过程中出现问题(文件的完整性至关重要)。再加上时间戳,即long型的64位,8个字节。最后再加上计数器,因为这个计数器由storage提供,这样的话,整个结构就是:len + crc32 + timestamp + uint32_t = 4 + 4 + 8 + 4 = 20个字节,这样生成的文件名就算做base64计算出来,也就不是什么大问题了。而且,加上计数器,每秒内只要单机不上传超过1万的文件 ,就都不是问题了。这个还是非常好解决的

 

// TODO 如何避免文件重复上传? md5吗? 还是文件的计算可以避免此问题?这个信息存储在tracker服务器中吗?

FastDFS中给我们一个非常好的例子,请参考下面的代码:

// 参考FastDFS的文件名称生成算法

/**
1 byte: store path index
8 bytes: file size
FDFS_FILE_EXT_NAME_MAX_LEN bytes: file ext name, do not include dot (.)
file size bytes: file content
**/
static int storage_upload_file(struct fast_task_info *pTask, bool bAppenderFile)
{
 StorageClientInfo *pClientInfo;
 StorageFileContext *pFileContext;
 DisconnectCleanFunc clean_func;
 char *p;
 char filename[128];
 char file_ext_name[FDFS_FILE_PREFIX_MAX_LEN + 1];
 int64_t nInPackLen;
 int64_t file_offset;
 int64_t file_bytes;
 int crc32;
 int store_path_index;
 int result;
 int filename_len;
 pClientInfo = (StorageClientInfo *)pTask->arg;
 pFileContext = &(pClientInfo->file_context);
 nInPackLen = pClientInfo->total_length - sizeof(TrackerHeader);
 if (nInPackLen < 1 + FDFS_PROTO_PKG_LEN_SIZE +
   FDFS_FILE_EXT_NAME_MAX_LEN)
 {
  logError("file: "__FILE__", line: %d, " \
   "cmd=%d, client ip: %s, package size " \
   "%"PRId64" is not correct, " \
   "expect length >= %d", __LINE__, \
   STORAGE_PROTO_CMD_UPLOAD_FILE, \
   pTask->client_ip, nInPackLen, \
   1 + FDFS_PROTO_PKG_LEN_SIZE + \
   FDFS_FILE_EXT_NAME_MAX_LEN);
  return EINVAL;
 }
 p = pTask->data + sizeof(TrackerHeader);
 store_path_index = *p++;
 if (store_path_index == -1)
 {
  if ((result=storage_get_storage_path_index( \
   &store_path_index)) != 0)
  {
   logError("file: "__FILE__", line: %d, " \
    "get_storage_path_index fail, " \
    "errno: %d, error info: %s", __LINE__, \
    result, STRERROR(result));
   return result;
  }
 }
 else if (store_path_index < 0 || store_path_index >= \
  g_fdfs_store_paths.count)
 {
  logError("file: "__FILE__", line: %d, " \
   "client ip: %s, store_path_index: %d " \
   "is invalid", __LINE__, \
   pTask->client_ip, store_path_index);
  return EINVAL;
 }
 file_bytes = buff2long(p);
 p += FDFS_PROTO_PKG_LEN_SIZE;
 if (file_bytes < 0 || file_bytes != nInPackLen - \
   (1 + FDFS_PROTO_PKG_LEN_SIZE + \
    FDFS_FILE_EXT_NAME_MAX_LEN))
 {
  logError("file: "__FILE__", line: %d, " \
   "client ip: %s, pkg length is not correct, " \
   "invalid file bytes: %"PRId64 \
   ", total body length: %"PRId64, \
   __LINE__, pTask->client_ip, file_bytes, nInPackLen);
  return EINVAL;
 }
 memcpy(file_ext_name, p, FDFS_FILE_EXT_NAME_MAX_LEN);
 *(file_ext_name + FDFS_FILE_EXT_NAME_MAX_LEN) = '\0';
 p += FDFS_FILE_EXT_NAME_MAX_LEN;
 if ((result=fdfs_validate_filename(file_ext_name)) != 0)
 {
  logError("file: "__FILE__", line: %d, " \
   "client ip: %s, file_ext_name: %s " \
   "is invalid!", __LINE__, \
   pTask->client_ip, file_ext_name);
  return result;
 }
 pFileContext->calc_crc32 = true;
 pFileContext->calc_file_hash = g_check_file_duplicate;
 pFileContext->extra_info.upload.start_time = g_current_time;
 strcpy(pFileContext->extra_info.upload.file_ext_name, file_ext_name);
 storage_format_ext_name(file_ext_name, \
   pFileContext->extra_info.upload.formatted_ext_name);
 pFileContext->extra_info.upload.trunk_info.path. \
    store_path_index = store_path_index;
 pFileContext->extra_info.upload.file_type = _FILE_TYPE_REGULAR;
 pFileContext->sync_flag = STORAGE_OP_TYPE_SOURCE_CREATE_FILE;
 pFileContext->timestamp2log = pFileContext->extra_info.upload.start_time;
 pFileContext->op = FDFS_STORAGE_FILE_OP_WRITE;
 if (bAppenderFile)
 {
  pFileContext->extra_info.upload.file_type |= \
     _FILE_TYPE_APPENDER;
 }
 else
 {
  if (g_if_use_trunk_file && trunk_check_size( \
   TRUNK_CALC_SIZE(file_bytes)))
  {
   pFileContext->extra_info.upload.file_type |= \
      _FILE_TYPE_TRUNK;
  }
 }
 if (pFileContext->extra_info.upload.file_type & _FILE_TYPE_TRUNK)
 {
  FDFSTrunkFullInfo *pTrunkInfo;
  pFileContext->extra_info.upload.if_sub_path_alloced = true;
  pTrunkInfo = &(pFileContext->extra_info.upload.trunk_info);
  if ((result=trunk_client_trunk_alloc_space( \
   TRUNK_CALC_SIZE(file_bytes), pTrunkInfo)) != 0)
  {
   return result;
  }
  clean_func = dio_trunk_write_finish_clean_up;
  file_offset = TRUNK_FILE_START_OFFSET((*pTrunkInfo));
    pFileContext->extra_info.upload.if_gen_filename = true;
  trunk_get_full_filename(pTrunkInfo, pFileContext->filename, \
    sizeof(pFileContext->filename));
  pFileContext->extra_info.upload.before_open_callback = \
     dio_check_trunk_file_when_upload;
  pFileContext->extra_info.upload.before_close_callback = \
     dio_write_chunk_header;
  pFileContext->open_flags = O_RDWR | g_extra_open_file_flags;
 }
 else
 {
  char reserved_space_str[32];
  if (!storage_check_reserved_space_path(g_path_space_list \
   [store_path_index].total_mb, g_path_space_list \
   [store_path_index].free_mb - (file_bytes/FDFS_ONE_MB), \
   g_avg_storage_reserved_mb))
  {
   logError("file: "__FILE__", line: %d, " \
    "no space to upload file, "
    "free space: %d MB is too small, file bytes: " \
    "%"PRId64", reserved space: %s", \
    __LINE__, g_path_space_list[store_path_index].\
    free_mb, file_bytes, \
    fdfs_storage_reserved_space_to_string_ex( \
      g_storage_reserved_space.flag, \
          g_avg_storage_reserved_mb, \
      g_path_space_list[store_path_index]. \
      total_mb, g_storage_reserved_space.rs.ratio,\
      reserved_space_str));
   return ENOSPC;
  }
  crc32 = rand();
  *filename = '\0';
  filename_len = 0;
  pFileContext->extra_info.upload.if_sub_path_alloced = false;
  if ((result=storage_get_filename(pClientInfo, \
   pFileContext->extra_info.upload.start_time, \
   file_bytes, crc32, pFileContext->extra_info.upload.\
   formatted_ext_name, filename, &filename_len, \
   pFileContext->filename)) != 0)
  {
   return result;
  }
  clean_func = dio_write_finish_clean_up;
  file_offset = 0;
    pFileContext->extra_info.upload.if_gen_filename = true;
  pFileContext->extra_info.upload.before_open_callback = NULL;
  pFileContext->extra_info.upload.before_close_callback = NULL;
  pFileContext->open_flags = O_WRONLY | O_CREAT | O_TRUNC \
      | g_extra_open_file_flags;
 }
  return storage_write_to_file(pTask, file_offset, file_bytes, \
   p - pTask->data, dio_write_file, \
   storage_upload_file_done_callback, \
   clean_func, store_path_index);
}
 
static int storage_get_filename(StorageClientInfo *pClientInfo, \
 const int start_time, const int64_t file_size, const int crc32, \
 const char *szFormattedExt, char *filename, \
 int *filename_len, char *full_filename)
{
 int i;
 int result;
 int store_path_index;
 store_path_index = pClientInfo->file_context.extra_info.upload.
    trunk_info.path.store_path_index;
 for (i=0; i<10; i++)
 {
  if ((result=storage_gen_filename(pClientInfo, file_size, \
   crc32, szFormattedExt, FDFS_FILE_EXT_NAME_MAX_LEN+1, \
   start_time, filename, filename_len)) != 0)
  {
   return result;
  }
  sprintf(full_filename, "%s/data/%s", \
   g_fdfs_store_paths.paths[store_path_index], filename);
  if (!fileExists(full_filename))
  {
   break;
  }
  *full_filename = '\0';
 }
 if (*full_filename == '\0')
 {
  logError("file: "__FILE__", line: %d, " \
   "Can't generate uniq filename", __LINE__);
  *filename = '\0';
  *filename_len = 0;
  return ENOENT;
 }
 return 0;
}
static int storage_gen_filename(StorageClientInfo *pClientInfo, \
  const int64_t file_size, const int crc32, \
  const char *szFormattedExt, const int ext_name_len, \
  const time_t timestamp, char *filename, int *filename_len)
{
 char buff[sizeof(int) * 5];
 char encoded[sizeof(int) * 8 + 1];
 int len;
 int64_t masked_file_size;
 FDFSTrunkFullInfo *pTrunkInfo;
 pTrunkInfo = &(pClientInfo->file_context.extra_info.upload.trunk_info);
 int2buff(htonl(g_server_id_in_filename), buff);
 int2buff(timestamp, buff+sizeof(int));
 if ((file_size >> 32) != 0)
 {
  masked_file_size = file_size;
 }
 else
 {
  COMBINE_RAND_FILE_SIZE(file_size, masked_file_size);
 }
 long2buff(masked_file_size, buff+sizeof(int)*2);
 int2buff(crc32, buff+sizeof(int)*4);
 base64_encode_ex(&g_fdfs_base64_context, buff, sizeof(int) * 5, encoded, \
   filename_len, false);
 if (!pClientInfo->file_context.extra_info.upload.if_sub_path_alloced)
 {
  int sub_path_high;
  int sub_path_low;
  storage_get_store_path(encoded, *filename_len, \
   &sub_path_high, &sub_path_low);
  pTrunkInfo->path.sub_path_high = sub_path_high;
  pTrunkInfo->path.sub_path_low = sub_path_low;
  pClientInfo->file_context.extra_info.upload. \
    if_sub_path_alloced = true;
 }
 len = sprintf(filename, FDFS_STORAGE_DATA_DIR_FORMAT"/" \
   FDFS_STORAGE_DATA_DIR_FORMAT"/", \
   pTrunkInfo->path.sub_path_high,
   pTrunkInfo->path.sub_path_low);
 memcpy(filename+len, encoded, *filename_len);
 memcpy(filename+len+(*filename_len), szFormattedExt, ext_name_len);
 *filename_len += len + ext_name_len;
 *(filename + (*filename_len)) = '\0';
 return 0;
}



 

根据上面分析的结果,我们看到,当上传一个文件的时候,我们会获取到如下的信息

1- 文件的大小(通过协议中包的长度字段可以知道,这样的好处在于服务端实现的时候简单,不用过于担心网络缓冲区的问题)

2- CRC32(也是协议包中传输,以便确定网络传输是否出错)

3- 时间戳(获取服务器的当前时间)

4- 计数器(服务器自己维护)

根据上面的4个数据,组织成base64的编码,然后生成此文件名称。根据此文件名称的唯一性,就不会出现被覆盖的情况。同时,唯一性也使得接下来做md5运算后,得到的HASH值离散性得么保证。得到了MD5的哈希值后,取出最前面的2部分,就可以知道要定位到哪个目录下面去。哈希桶的构造是固定的,即二级00-ff的目录情况

发表在 technologys | FastDFS目录及文件名的一些资料已关闭评论