一. hpfeeds协议简介

  hpfeeds是一个轻量级的验证发布-订阅协议(authenticated publish-subscribe protocol)。

发布-订阅协议:发布/订阅协议定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。它是为了解决这样一种情况的发生,一个对象的改变需要同时改变其他对象,而且它不知道具体有多少对象需要改变。这就类似于发传单,目标发送通知,没有指定特定的对象,通知会自动传播,观察者自己决定是否需要看传单,发送者对于传单是否被看一无所知。

  hpfeeds把不同的数据用频道来划分,支持传送任意的二进制数据。由频道的使用者决定传送数据结构的形式。通道的验证通过Authkey来完成,它由两部分组成:ident和secret,相当于身份和密码。把密码和每次连接的一个随机数进行hash摘要算法然后一起发送给服务器,这样保证不会被窃听,所以它可以运行在SSL、TLS下。

  整个协议实现了三方的功能独立。使用这种方式降低了应用与业务逻辑之间的耦合,统一一个对外的发布接口,只需要关心监听的类型,不关心监听的具体处理人。频道的发布者只管发,不管订阅者有没有收到,很方便的建立一种一对多的依赖关系。在当一个对象的改变需要同时改变其他对象,而且它不知道具体有多少对象需要改变时,就可以使用订阅发布模式。

二. hpfeeds的原理

  hpfeeds协议通过以下几部分实现:hpfeeds server, hpfeeds client,mongodb数据库。

1. hpfeeds server: 

  • 负责为每个client的连接生成一个连接标志;
  • 检查请求连接的client的id和sha1(nonce+Authkey);
  • 检查client的请求类型,发布还是接收;

2. hpfeeds client: 

  每个hpfeeds client都即可以作为发布者也可以作为订阅者,发布者和订阅者并不要求必须同时存在。

3. Mongodb: 

  mongodb数据库用来存储每个client的id和secret,并且每当有client请求连接server时,server都会从mangodb中取出该client注册时的id和secret进行对比。 若对比一致则认证通过,client可以和server正常建立连接;若不一致则client与server建立连接失败。

4. client和server的认证过程:

  Client和server的认证及发布/订阅过程如下图1所示:  

   

hpfeeds协议建立连接及通信的过程: 

  1. Client发起连接请求;
  2. server为每个client的连接生成一个连接标志,并将其发送给请求连接的client;
  3. client发送自己的id和sha1(nonce+Authkey)到server进行认证;
  4. server从mongodb中取出相应的信息检查验证,若认证通过,保持连接并执行后续操作。否则,断开连接;
  5. client发起publish/subscribe请求;
  6. server检查client请求消息的类型,发布/订阅。

三. hpfeeds的消息格式

1. Wire Protocol: 

  每个hpfeeds协议消息都携带了一个固定格式的消息头,有两个参数:消息总长度和请求类型。如下代码所示。

 struct MsgHeader {
unit32_t_messageLength; // total message size, including this request type
unit8_t_opCode;
};

  请求类型有以下几种:

  • error(0): errormessage
  • info(1): server name, nonce  
  • auth (2): client id, sha1(nonce+authkey)
  • publish (3): client id, channelname, payload
  • subscribe (4): client id, channelname

  一个完整的发布类型的消息如下图所示,由消息头、client_id的长度、client_id、channelname的长度、channelname、传输内容payload。payload可以是任意类型的二进制数据。

四. hpfeeds源码解析

  Hpfeeds协议server与client的通讯主要也是使用TCP套接字的方式。

  Hpfeeds server采用了事件驱动的方式来处理client的请求。这样做是为了应对高连接数高吞吐量的client请求,使用这种方法可以同时接收数百、数千甚至数万个事件,无论它们是内部请求还是网络连接,都可以高效地处理它们的操作。同时还能够极大的降低资源占用,增大服务接待能力,并提高网络传输效率。

  Hpfeeds server与mongodb的连接及数据交互并没有使用Python自带的pymongo模块,而是使用了自己编写的一个基于事件驱动的MongoConn模块。这样做的目的也是为了处理高连接数的client请求。下面主要对hpfeeds的server和client的源码进行解析。

1. hpfeeds server 源码

  Hpfeeds server的工作方式,首先连接mongodb数据库,监听hpfeeds server的服务端口,设置事件监听器,关联相应处理函数,将事件监听器加入事件循环,启动事件循环进行监听。如果有client请求来,则会触发相应的事件,调用与事件相关联的函数进行处理操作。Hpfeeds server的主程序代码如下。

 #!/usr/bin/env python

 import sys

 import struct
import hashlib
import collections
import random import logging
logging.basicConfig(level=logging.INFO) from evnet import loop, unloop, listenplain, EventGen # 用于实现事件循环的模块
from evnet.mongodb import MongoConn
# 注意:Python本身有对mongodb进行操作的模块,但在hpfeeds server中没有使用,
# 这里它自己实现了一个对mongodb进行操作的模块MongoConn,为了实现使用事件循
# 的方式来对数据库进行操作 FBIP = '0.0.0.0' # hpfeeds server监听的地址和端口号
FBPORT = 10000
FBNAME = '@hp2'
MONGOIP = '127.0.0.1'
MONGOPORT = 27017 OP_ERROR = 0
OP_INFO = 1
OP_AUTH = 2
OP_PUBLISH = 3
OP_SUBSCRIBE = 4
OP_UNSUBSCRIBE = 5 MAXBUF = 10* (1024**2)
SIZES = {
OP_ERROR: 5+MAXBUF,
OP_INFO: 5+256+20,
OP_AUTH: 5+256+20,
OP_PUBLISH: 5+MAXBUF,
OP_SUBSCRIBE: 5+256*2,
OP_UNSUBSCRIBE: 5+256*2,
} class BadClient(Exception):
pass class FeedUnpack(object): # 对client传来的数据进行解码
def __init__(self):
self.buf = bytearray()
def __iter__(self):
return self
def next(self):
return self.unpack()
def feed(self, data): # 将client传来的数据存入self.buf
self.buf.extend(data)
def unpack(self):
if len(self.buf) < 5: # 如果self.buf的总长度小于5,说明请求消息为空
# 因为client各种请求类型的消息长度都是大于5的
raise StopIteration('No message.') ml, opcode = struct.unpack('!iB', buffer(self.buf,0,5))
# 解码出ml和opcode
if ml > SIZES.get(opcode, MAXBUF): # ml为hpfeeds消息的总长度
raise BadClient('Not respecting MAXBUF.') if len(self.buf) < ml: # self.buf中的数据长度小于该条消息的总长度,抛出异常
raise StopIteration('No message.') data = bytearray(buffer(self.buf, 5, ml-5))
del self.buf[:ml] # 删除self.buf中的数据
return opcode, data
# data中包含了len(client_id),client_id,length(channelname), channelname,payload class FeedConn(EventGen):
def __init__(self, conn, addr, db):
EventGen.__init__(self)
self.conn = conn
self.addr = addr
self.db = db
self.pubchans = set()
self.subchans = set()
self.idents = set()
self.delay = False self.rand = struct.pack('<I', random.randint(2**31,2**32-1)) # 产生一个随机数
self.fu = FeedUnpack() conn._on('read', self.io_in)
conn._on('close', self.closed) self.sendinfo() def sendinfo(self):
self.conn.write(self.msginfo()) def auth(self, ident, hash): # server和client的认证函数
p = self.db.query('hpfeeds.auth_key', {'identifier': str(ident)}, limit=1)
# 查询mongodb中的数据,返回的p为一个Promise()对象
p._when(self.checkauth, hash) # 调用checkauth函数对client进行认证 def dbexc(e): # mongodb查询异常处理函数
logging.critical('Database query exception. {0}'.format(e))
self.error('Database query exception.') p._except(dbexc) # 如果出现异常则执行响应的处理函数 self.delay = True def checkauth(self, r, hash): # server与client的认证处理函数
if len(r) > 0: # r是self._result
akobj = r[0]
akhash = hashlib.sha1('{0}{1}'.format(self.rand, akobj['secret'])).digest()
if akhash == hash: # 将数据库中取得的secret与self.rand进行hash摘要算法进行对比
self.pubchans.update(akobj.get('publish', [])) # 更新发布频道
self.subchans.update(akobj.get('subscribe', []))# 更新订阅频道
self.idents.add(akobj['identifier']) # 将认证成功的client_id添加到self.idents
logging.info('Auth success by {0}.'.format(akobj['identifier']))
else:
self.error('authfail.')
logging.info('Auth failure by {0}.'.format(akobj['identifier']))
else:
self.error('authfail.')
self.delay = False
self.io_in(b'') def closed(self, reason):
logging.debug('Connection closed, {0}'.format(reason))
self._event('close', self) def may_publish(self, chan):
return chan in self.pubchans def may_subscribe(self, chan):
return chan in self.subchans def io_in(self, data): # 传送请求发布和订阅的数据函数
self.fu.feed(data) # 数据存入self.buf
if self.delay: # 经FeedUnpack的实例处理后的数据为opcode, data
return # data中包含了len(client_id),client_id,length(channelname), channelname,payload
try:
for opcode, data in self.fu:
if opcode == OP_PUBLISH: # 处理发布请求
rest = buffer(data, 0) # 数据存入buffer
ident, rest = rest[1:1+ord(rest[0])], buffer(rest, 1+ord(rest[0]))
chan, rest = rest[1:1+ord(rest[0])], buffer(rest, 1+ord(rest[0]))
# 解码出发布请求包中的数据
if not ident in self.idents:
self.error('identfail.')
continue if not self.may_publish(chan):
self.error('accessfail.')
continue self._event('publish', self, chan, data) # 触发发布请求的处理事件
elif opcode == OP_SUBSCRIBE: # 处理订阅请求
rest = buffer(data, 0)
ident, chan = rest[1:1+ord(rest[0])], rest[1+ord(rest[0]):] if not ident in self.idents:
self.error('identfail.')
continue checkchan = chan
if chan.endswith('..broker'): checkchan = chan.rsplit('..broker', 1)[0] if not self.may_subscribe(checkchan):
self.error('accessfail.')
continue self._event('subscribe', self, chan, ident) # 触发订阅请求的处理事件
elif opcode == OP_UNSUBSCRIBE: # 处理取消订阅请求
rest = buffer(data, 0)
ident, chan = rest[1:1+ord(rest[0])], rest[1+ord(rest[0]):] if not ident in self.idents:
self.error('identfail.')
continue if not self.may_subscribe(chan):
self.error('accessfail.')
continue self._event('unsubscribe', self, chan, ident)# 触发取消订阅请求的事件
elif opcode == OP_AUTH: # 处理认证请求
rest = buffer(data, 0)
ident, hash = rest[1:1+ord(rest[0])], rest[1+ord(rest[0]):]
self.auth(ident, hash) # 认证函数
if self.delay:
return except BadClient:
self.conn.close() # 关闭客户端与服务器的连接
logging.warn('Disconnecting bad client: {0}'.format(self.addr))
def forward(self, data):
self.conn.write(self.msghdr(OP_PUBLISH, data)) def error(self, emsg):
self.conn.write(self.msgerror(emsg)) def msgerror(self, emsg):
return self.msghdr(OP_ERROR, emsg) def msginfo(self):
return self.msghdr(OP_INFO, '{0}{1}{2}'.format(chr(len(FBNAME)%0xff), FBNAME, self.rand)) def msghdr(self, op, data): # 对消息进行封包处理的函数
return struct.pack('!iB', 5+len(data), op) + data def msgpublish(self, ident, chan, data): # 发布消息预处理函数
return self.msghdr(OP_PUBLISH, struct.pack('!B', len(ident)) + ident + struct.pack('!B', len(chan)) + chan + data) def publish(self, ident, chan, data): # 发布消息函数
self.conn.write(self.msgpublish(ident, chan, data)) class FeedBroker(object):
def __init__(self):
self.ready = False self.db = None
self.initdb() # 初始化mongodb数据库 self.listener = listenplain(host=FBIP, port=FBPORT) # hpfeeds server 开始监听端口,返回的listener是一个监听事件
self.listener._on('close', self._lclose) # 为事件关联函数
self.listener._on('connection', self._newconn) # 有新的client连接则触发该事件 self.connections = set() # 连接的client集合
self.subscribermap = collections.defaultdict(list)
self.conn2chans = collections.defaultdict(list) def initdb(self):
self.db = MongoConn(MONGOIP, MONGOPORT) # 连接mongodb
self.db._on('ready', self._dbready) # 关联事件和回调函数
self.db._on('close', self._dbclose) def _dbready(self):
self.ready = True
logging.info('Database ready.') def _dbclose(self, e):
logging.critical('Database connection closed ({0}). Exiting.'.format(e))
unloop() def _lclose(self, e):
logging.critical('Listener closed ({0}). Exiting.'.format(e))
unloop() def _newconn(self, c, addr): # client请求连接server的处理函数
logging.debug('Connection from {0}.'.format(addr))
fc = FeedConn(c, addr, self.db) # 处理client的各种类型的请求的监听事件
self.connections.add(fc)
fc._on('close', self._connclose) # 为fc关联事件和回调函数
fc._on('subscribe', self._subscribe)
fc._on('unsubscribe', self._unsubscribe)
fc._on('publish', self._publish) def _connclose(self, c): # 关闭server与client连接
self.connections.remove(c)
for chan in self.conn2chans[c]:
self.subscribermap[chan].remove(c)
for ident in c.idents:
self._brokerchan(c, chan, ident, 0)
def _publish(self, c, chan, data):
logging.debug('broker publish to {0} by {1}'.format(chan, c.addr))
for c2 in self.subscribermap[chan]: # 该频道中的订阅者
if c2 == c: continue # 把发布者本身除外
c2.forward(data) # 向该频道的所有订阅者推送要发布的数据 def _subscribe(self, c, chan, ident): # 订阅请求的处理仅把订阅者添加到频道中,然后触发推送数据的循环
logging.debug('broker subscribe to {0} by {2} @ {1}'.format(chan, c.addr, ident))
self.subscribermap[chan].append(c)
self.conn2chans[c].append(chan)
self._brokerchan(c, chan, ident, 1) def _unsubscribe(self, c, chan, ident): # 某个客户端取消对某个频道的订阅
logging.debug('broker unsubscribe to {0} by {1}'.format(chan, c.addr))
self.subscribermap[chan].remove(c)
self.conn2chans[c].remove(chan)
self._brokerchan(c, chan, ident, 0) def _brokerchan(self, c, chan, ident, subscribe=0): # 触发推送数据循环
data = 'join' if subscribe else 'leave'
if self.subscribermap[chan+'..broker']:
for c2 in self.subscribermap[chan+'..broker']:
if c2 == c: continue
c2.publish(ident, chan+'..broker', data) def main():
fb = FeedBroker() loop() # 启动事件监听循环
return 0 if __name__ == '__main__':
sys.exit(main())

2. hpfeeds client源码

  Hpfeeds client的工作方式,与server成功建立连接后,开始相应的publish/subscribe操作。如果是做为订阅者,则会与server一直保持连接状态,不断读取订阅频道中的内容;如果是作为发布者,则每次推送完数据后,不管订阅者有没有收到信息,都立刻关闭与server的连接。

 #!/usr/bin/env python

 import sys
import optparse # optparse模块用于处理命令行参数
import datetime
import logging
import string import hpfeeds def log(msg):
print '[feedcli] {0}'.format(msg) def main(opts, action, pubdata=None):
outfd = None
if opts.output:
try: outfd = open(opts.output, 'a')
except:
log('could not open output file for message log.')
return 1
else:
outfd = sys.stdout try: hpc = hpfeeds.new(opts.host, opts.port, opts.ident, opts.secret, certfile=opts.certfile)
# 连接hpfeeds server,返回值hpc为hpfeeds模块中HPC类对象,
# 如果client与server成功连接,并认证成功,则程序继续往后执行;否则抛出异常,程序退出
except hpfeeds.FeedException, e:
log('Error: {0}'.format(e))
return 1 log('connected to {0}'.format(hpc.brokername)) if action == 'subscribe': # 订阅请求
def on_message(ident, chan, payload): # 显示记录收到的订阅信息
if [i for i in payload[:20] if i not in string.printable]:
log('publish to {0} by {1}: {2}'.format(chan, ident, payload[:20].encode('hex') + '...'))
else:
log('publish to {0} by {1}: {2}'.format(chan, ident, payload)) def on_error(payload): # 记录错误信息
log('Error message from broker: {0}'.format(payload))
hpc.stop() # 停止循环 hpc.subscribe(opts.channels)
try: hpc.run(on_message, on_error) # 接收server推送过来的数据,调用on_message(),on_error()进行处理
except hpfeeds.FeedException, e: # 抛出异常,程序退出
log('Error: {0}'.format(e))
return 1 elif action == 'publish': # 发布请求
hpc.publish(opts.channels, pubdata) # 推送数据
emsg = hpc.wait() # 若推送成功,返回None;否则,返回其它值
if emsg: print 'got error from server:', emsg elif action == 'sendfile':
pubfile = open(pubdata, 'rb').read()
hpc.publish(opts.channels, pubfile) log('closing connection.')
hpc.close() return 0 def opts(): # 获取命令行参数
usage = "usage: %prog -i ident -s secret --host host -p port -c channel1 [-c channel2, ...] <action> [<data>]"
parser = optparse.OptionParser(usage=usage)
parser.add_option("-c", "--chan", # 定义命令行参数
action="append", dest='channels', nargs=1, type='string',
help="channel (can be used multiple times)")
parser.add_option("-i", "--ident",
action="store", dest='ident', nargs=1, type='string',
help="authkey identifier")
parser.add_option("-s", "--secret",
action="store", dest='secret', nargs=1, type='string',
help="authkey secret")
parser.add_option("--host",
action="store", dest='host', nargs=1, type='string',
help="broker host")
parser.add_option("-p", "--port",
action="store", dest='port', nargs=1, type='int',
help="broker port")
parser.add_option("-o", "--output",
action="store", dest='output', nargs=1, type='string',
help="publish log filename")
parser.add_option("--certfile",
action="store", dest='certfile', nargs=1, type='string',
help="certfile for ssl verification (CA)", default=None)
parser.add_option("--debug",
action="store_const", dest='debug',
help="enable debug log output", default=False, const=True) options, args = parser.parse_args() # 定义好所有的命令行参数,调用 parse_args()来解析程序的命令行 if len(args) < 1:
parser.error('You need to give "subscribe" or "publish" as <action>.')
if args[0] not in ['subscribe', 'publish', 'sendfile']:
parser.error('You need to give "subscribe" or "publish" as <action>.')
if options.debug:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.CRITICAL) action = args[0]
data = None
if action == 'publish':
data = ' '.join(args[1:])
elif action == 'sendfile':
data = ' '.join(args[1:]) return options, action, data if __name__ == '__main__':
options, action, data = opts() # 获取命令行参数
try:
sys.exit(main(options, action, pubdata=data)) # 从main()函数开始执行
except KeyboardInterrupt:
sys.exit(0)

hpfeeds协议解析的更多相关文章

  1. ts 协议解析

    pes : http://wenku.baidu.com/link?url=KjcA0qXqZ1bWVQTa8i1YOmygofldSQL7Pjj-zGRw1e_6_LFmVLo5DIWF0SNwVn ...

  2. [转]netty对http协议解析原理

    本文主要介绍netty对http协议解析原理,着重讲解keep-alive,gzip,truncked等机制,详细描述了netty如何实现对http解析的高性能. 1 http协议 1.1 描述 标示 ...

  3. twemproxyRedis协议解析探索——剖析twemproxy代码正编

    这篇文章会对twemproxyRedis协议解析代码部分进行一番简单的分析,同时给出twemproxy目前支持的所有Redis命令.在这篇文章开始前,我想大家去简单地理解一下有限状态机,当然不理解也是 ...

  4. B/S 架构中,网络模型的分解与协议解析

    前言 如果是C/S专业毕业的或者是学过计算机网络课程的童鞋们,相信大家都知道网络模型的划分,本文首先来聊一聊目前对于B/S结构中,网络模型分解的两种方式. 没错,相信大家看到这个图片的时候就已经明白了 ...

  5. 详解BLE 空中包格式—兼BLE Link layer协议解析

    BLE有几种空中包格式?常见的PDU命令有哪些?PDU和MTU的区别是什么?DLE又是什么?BLE怎么实现重传的?BLE ACK机制原理是什么?希望这篇文章能帮你回答以上问题. 虽然BLE空中包(pa ...

  6. netty对http协议解析原理解析

    本文主要介绍netty对http协议解析原理,着重讲解keep-alive,gzip,truncked等机制,详细描述了netty如何实现对http解析的高性能. 1 http协议 1.1 描述 标示 ...

  7. MODBUS协议解析中常用的转换帮助类(C#)

    p{ text-align:center; } blockquote > p > span{ text-align:center; font-size: 18px; color: #ff0 ...

  8. AOSP中的HLS协议解析

    [时间:2018-04] [状态:Open] [关键词:流媒体,stream,HLS, AOSP, 源码分析,HttpLiveSource, LiveSession,PlaylistFetcher] ...

  9. WebSocket协议解析

    WebSocket协议解析 转载请注明出处:WebSocket解析 现在,很多网站为了实现推送技术,所用的技术都是轮询.轮询是指在特定的时间间隔(如每一秒),由浏览器对服务器发起HTTP请求,然后由服 ...

随机推荐

  1. MYSQL 问题

    MYSQL 问题 (1)mysql server has gone away 导数据时,如果脚本太大,会执行中断,这时需要修改最大允许包的大小: set global max_allowed_pack ...

  2. iOS 使用 AVCaptureVideoDataOutputSampleBufferDelegate获取实时拍照的视频流

    iOS 使用 AVCaptureVideoDataOutputSampleBufferDelegate获取实时拍照的视频流 可用于实时视频聊天 实时视频远程监控 #import <AVFound ...

  3. FPS计算New

    using UnityEngine; using System.Collections; public class CarGUI : MonoBehaviour { private const flo ...

  4. django+nginx+uwsgi 项目部署

    Django虽然自带一个Server,但只能作为开发时测试使用,我们需要一个可以稳定而持续的服务器对网站进行部署,比如Apache, Nginx, lighttpd等,本篇将利用nginx和uWSGI ...

  5. HTTP缓存策略 304

    1.图解缓存 示例: 200 (from disk cache): 200 (from memory cache) MemoryCache顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载 ...

  6. Jenkins高速上手

    http://www.cnblogs.com/puresoul/p/4813551.html .Jenkins下载安装 1.到官网下载jenkins.war包:http://jenkins-ci.or ...

  7. OpenERP(odoo)开发实例之搜索检索过去3个月的数据

    转自:http://www.chinamaker.net/ OpenERP(odoo)开发实例之搜索过滤:检索过去3个月的数据 解决这个问题的重点在于 relativedelta 的应用 示例代码如下 ...

  8. ubuntu使用du命令查看一级子目录存储空间大小

    命令如下: ls | xargs du -ksh 可以ls不同的目录以查看不同的目录下的一级子目录大小.直接使用ls为当前目录下的一级子目录大小. 查看其他目录的大小: ls -d dirname/* ...

  9. 【Linux】Ctentos下载

    百度输入:Download Centos 在百度搜索Download Centos然后进入Centos下载官网 点击上述标记的Download Centos,则会出现如下界面,并且点击"Mo ...

  10. PHP中根据IP地址判断所在城市等信息

    本篇文章由:http://xinpure.com/php-based-on-information-such-as-the-ip-address-in-your-city/ 获得IP地址 在 PHP ...