早上搜索了下 清华大学开源软件俱乐部的网站,找到这点关于Ccnet的资料,分享下。
Ccnet 是 common creative network 的简称,它的目标是给 Linux 桌面提供一个面向于 group 的通用的 P2P 服务,使得人们没有中心服务器的情况下也能协同工作。
使用上来说,与大部分现有的 P2P 系统相比,Ccnet 有两个主要的不同点。 首先,Ccnet 并不试图将所有的人连接成一个全球性的网络,而是从协作的视角出发,为个人桌面环境提供 P2P 服务,使得一个桌面系统可以和一个或多个 group 中所有的其他成员的桌面系统能联通起来。其次,Ccnet 本身是一个守护进程,为桌面上的其他程序提供服务,比如通过 Ccnet 提供的消息传输服务,一个桌面应用程序可以向一个或多个 peer 发送一条消息;这种简单易用的 P2P 服务将极大地扩展目前桌面应用程序的所能实现的功能。
技术上来说,Ccnet 包含了一个目前大多数 P2P 系统都没有的功能,即请求的延迟满足。 举个例子,如果主机 A 需要向一个不在线的主机 B 发送一个消息,而且主机 A 自己马上要下线,怎么来保证该请求得到满足? 该功能对于一个节点在线时间不确定的 P2P 系统是至关重要的。Ccnet 通过 Requirement 机制来实现该功能。
程序结构上来说,Ccnet 是极易扩展的。Ccnet 包含提供了两套机制: Processor 和 Requirement。 Processor 用于实现两个在线 peer 的交互功能,比如传文件、传消息。 Ccnet 框架提供了一个 Processor 基类,通过书写一个子类,我们就能实现一个新的交互功能。Requirement 也是如此,通过书写一个子类,我们就能实现一个新的种类的请求的延迟满足。
目录[显示] |
概览
代码页面
几个设计原则
- 提供机制,把策略留给上层。 Ccnet 底层是 P2P 网络。 每个节点向其他的节点提供一个功能列表(可调用的 processor 列表),通过互相之间调用这些功能可以实现许多不同的上层功能。
- 关注于易用性
- 尽量底层: 直接使用 TCP 提供的字节流服务。不使用 XML,不使用 DBUS。
为什么使用 C 和 GObject
- 这是一个底层的代码,大量的 char *buf 操作
- 易被理解易被扩展
- 只在必要的时候使用面向对象编程功能
模块
功能模块 | 文件 | 说明 |
---|---|---|
工具模块 | ||
事件管理 | trevent.c | |
log | log.c | |
底层 | ||
peer 信息管理(本地) | peer.c peer-mgr.c | 建立和维护 peer 数据库 (内存和磁盘) |
peer 互连 | peer.c peer-mgr.c handshake.c listen.c peer-io.c | |
packet IO | peer.c packet.h | |
group(本地) | ||
routing | routing-mgr.c getpeerinfo-proc.c | |
processor | processor.c session.c peer.c proc-factory.c | |
requirement | requirement.c response.c req-manager.c sendreq-proc.c receivereq-proc.c | |
中层 | ||
message 管理 | message.c message-manager.c sendmsg-proc rcvmsg-proc getmsg-proc putmsg-proc sendmsg-req | |
文件管理 | file-manager.c sendfile-proc receivefile-proc getfile-proc sendfile-proc | |
git db 管理 | ||
高层 | ||
peer 信息管理(网络化) | 依赖于 message 管理和文件管理 | |
group(网络化) | 依赖于 message 管理和 git db 管理 |
peer 管理
相关的类: CcnetPeer ccnet_peerMgr
信息存储: peer-db/peer-info.txt peer-db/<id>
身份标识
每个节点用自己的公钥的 SHA1 值作为 ID。
采用 RSA 公钥算法认证。
添加 peer
添加一个 peer 有两种方式
- 主动添加: 通过邮件等方式得到一个 peer 的 info 文件,将该文件加入到 peer-db 中。
- 被动添加: 其他人向你发送一个 auth-request,认证该 auth-request。
auth-request 工作流程如下 (A 请求 B 的认证):
- A 在一个对话框中输入认证附加消息 m,并创建一个 auth-req 实例。
- A 将该 auth-req 实例传给 B
- B 得到这个 auth-req 后,获得 m 和 A 的 info 文件 (通过 getmsg-proc 和 getfile-proc)
- B 在认证对话框中点击确认
- info 文件被加入到 peer-db 中
- 向 A 发送一个 auth-rsp。
路由模块
路由模块依赖于 peer 模块和 getpeerinfo-proc.c 。
组管理
创建与加入
主动操作
- 创建一个组
- 将其他 peer 加入该组,并发送邀请 (设置状态为 unconfirmed)
- 发出加入一个组的请求
- 收到确认,通过 processor-getgroupinfo 得到组信息
- 收到拒绝
被动操作
- 收到加入一个组的邀请
- 拒绝该邀请 (在对方数据的状态变为 rejected)
- 接受该邀请 (在对方数据的状态变为 confirmed)
- 创建 group 数据结构,通过 processor-getgroupinfo 得到组信息
其他模块
权限控制
权限列表
- 创建仓库的权限
- 对一个特定仓库中一个特定分支的修改权限
几个问题
- 什么时候一个 peer 有权向 client 查询其他 peer 的信息?
Packet IO
基于 libevent 库中的 bufferevent 和 evbuffer 实现,涉及的文件:
- net.c 对 socket API 的封装
- packet.c 定义报文的格式
- peer-io.c 提供两个之间相连 peer 之间面向 packet 的 IO
- peer.c 提供中转服务
上层应用应该调用 peer.h 提供的 IO 函数,以便自动选择是否需要启动中转服务。
P2P 拓扑
监听
每隔时间间隔 LISTEN_INTERVAL (listen.c),incomingPeersPulse() 函数回被调用,以检测是否有 TCP 连接请求到达。注意,由于是非阻塞 IO, 所以 ccnet_netAccept() 函数总会立即返回。 ccnet_peerMgrAddIncoming() 生成 peerIo 对象和 handshake 对象,前者是对 socket IO 的封装,后者负责连接的初始化,比如认证。初始化完成后, myHandshakeDoneCB() 会被调用。
static void
incomingPeersPulse (ccnet_handle * h)
{
for ( ;; ) /* check for new incoming peer connections */
{
int socket;
struct sockaddr_storage cliaddr;
size_t len = sizeof (struct sockaddr_storage);
if ((socket = ccnet_netAccept (h->bindSocket, &cliaddr, &len)) < 0)
break;
ccnet_peerMgrAddIncoming (h->peerMgr, &cliaddr, len, socket);
}
}
void
ccnet_peerMgrAddIncoming (ccnet_peerMgr *manager,
struct sockaddr_storage *cliaddr,
size_t addrlen,
int socket)
{
ccnet_peerIo *io;
ccnet_handshake *handshake;
managerLock (manager);
io = ccnet_peerIoNewIncoming (manager->handle, cliaddr, socket);
handshake = ccnet_handshakeNew (io, myHandshakeDoneCB, manager);
g_ptr_array_add (manager->incomingHandshakes, handshake);
managerUnlock( manager );
}
Handshake
见 handshake.c。 完成 Hankshake 后,双方都知道了对方的 ID。 接下去要采取的行为,比如是否要认证对方,各自决定,采用“请求-响应”语义进行。比如,A 发起认证 B 的过程,而 B 可以决定不去认证 A。
认证
Client 将所有已经认证的 peer 放到 peer-db/peer-info.txt 中。 启动时读取该文件。
Client 在与一个未认证的 peer 完成 Handshake 后, 将该 peer 放到未认证 peer 池中。 用户调用 processor-getpeerinfo 来从该 peer 得到更多的信息。 调用 ccnet_peerMgrAuthPeer(peer) 来将该 peer 放入到已认证 peer 池中。
用户可以决定是否把未认证 peer 池保存到磁盘文件中 (peer-db/peer-unauth.txt)。
相关函数
void ccnet_peerMgrAddUnauthPeer (ccnet_peerMgr *mgr, const char *peer_id, ccnet_peerIo *io);
void ccnet_peerMgrAcceptPeer (ccnet_peerMgr *mgr, const char *peer_id);
void ccnet_peerMgrRejectPeer (ccnet_peerMgr *mgr, const char *peer_id);
地址信息维护
Client 自身处于三种状态之一:
- Down (未连网)
- In Nat (在 Nat 内)
- Full (不在 Nat 内)
Client 用 CcnetPeerAtom 结构来保存 peer 的地址信息。 为了保持地址信息的有效性,需要
- Client 主动连接上一个 peer 后,需要更新 atom 结构中的 mtime (modify time)。
Processor
Processor 生命周期的管理并不使用 GObject 引用计数机制,而是通过下面的规范
- 上层通过 ccnet_proc_factory_create_processor() 来创建一个 processor
- ccnet_proc_factory_create_processor() 将 processor 放到对应的 peer 的 processor Hash 表中。
- ccnet_proc_factory_create_processor() 创建完一个 processor 后,发送 proc-create signal
- processor 根据自身运行情况,在需要结束的时候调用 ccnet_processor_done () 函数终止自身。
- 终止时需要做三件事, 1) 从 peer->processors Hash 表中去除; 2) 发送 done signal; 3) 释放内存
ccnet_processor_done () 只能由 processor 自己调用。 其他模块如果要关闭一个 processor, 使用 ccnet_processor_shutdown()
Requirement
Requirement 是一种可靠的对象传输机制,它可以在两个节点不同时在线的情况下可靠地将一个节点上的对象(消息、文件、认证请求等)传输到另一个节点上,Requirement 本身并不负责对象的处理,对象的处理交由上层负责。
生命周期管理
Requirement 的生命周期分为内存生命周期和状态生命周期,前者由引用计数机制管理,后者由自身管理。
在源节点和中继节点, Requirement 存在于 pending_reqrs 列表中。在目的节点,它存在于 check_reqrs 列表中。(在状态生命周期结束后,Requirement 有没有必要保存在一个特殊列表中以便查询?)
Requirement 状态机
Requirement 由事件驱动其运转。 函数 try_satisfy() 用于其他模块给 Requirement 一个初始的触发。 在发生以下事件的时候该函数被调用:
- 在源节点创建的时候
- 在中继节点或目的节点被接收的时候 (在目的节点需要先通过防重复检查)
- 备份后重新加载的时候
初始触发后,Requirement 自我运转,对其他模块封闭。
目前只考虑与消息传送有关的 Requirement。这里有两类 Requirement
- sendmsg-reqr:发送一个消息
- ack-reqr:确认消息已经收到
sendmsg-reqr 状态机
源节点
INIT:
- 事件:try_satify() 被调用;响应:转入 SENDING 或者 WAITING 状态。
- 转入 SENDING:设置事件监听函数,等待 relay 和 dest peer 上线。
- 转入 WAITING:设置重传计时器。
WAITING: 只在源节点存在,reqr 已经发给足够的中继节点。
- 事件: 重传计时器超时;响应:转入 INIT 状态,调用 try_satisfy() 函数。
- 事件:ack-reqr 到达;响应:转入 FINISH 状态。
SENDING: 只在源节点存在,reqr 尚未发给足够的中继节点。
- 事件: dest peer 的网络连接状态改变;响应:发送 reqr,转入 WAITING 状态,设置重传计时器。
- 事件: dest peer 的 req_proxy_list 改变;响应:发送 reqr,如果已经发送给足够的中继,转入 WAITING 状态,设置重传计时器。
- 事件: try_satify() 被调用;响应: 重新设置事件监听函数。
FINISH 状态: 由 ack-reqr 设置
- 从 pending_reqrs 列表删除,调用 ccnet_requirement_done()。
中继节点
INIT:
- 事件:try_satify() 被调用;响应:获取 message,成功获取后转入 MSG_RECV 状态。(如果获取 message 失败还需要考虑出错处理,需要增加一个中间状态)
- 如果获取 message 成功,并且当前 dest peer 在线,立即转发 reqr,转入 FINISH 状态。
- 如果获取 message 成功,当前 dest 不在线,转入 MSG_RECV 状态,设置事件监听函数,等待 dest 网络状态改变。
MSG_RECV:
- 事件:dest 状态改变;响应:发送 reqr,转入 FINISH 状态。
- 事件:try_satisfy() 被调用;响应:重新设置事件监听函数。
FINISH:
- 从 pending_reqrs 列表删除,调用 ccnet_requirement_done()。
目的节点
INIT:
- 事件:try_satify() 被调用;响应:利用 check_reqrs 列表检查重复
- 如果不是重复的 reqr,将它加入 check_reqrs 列表,设置超时,获取 message,成功获取后转入 FINISH 状态。(如果获取 message 失败还需要考虑出错处理,需要增加一个中间状态)
- 如果是重复的 reqr,直接转入 FINISH 状态。
FINISH:
- 生成一个 ack-reqr 并且发送。调用 ccnet_requirement_done()。
req-manager 创建与接收 requirement
- 上层创建 reqr, 将它加入到 pending_reqr 队列,调用 try_satisfy()
- 中继接收 reqr, 将它加入到 pending_reqr 队列,调用 try_satisfy()
- 程序重启后,restore reqr, 加入相应的队列,根据 Requirement 的不同状态恢复原来的超时计时器,调用 try_satisfy()
防止重复
当一个 requirement 到达目的节点之后,会自己根据 check_reqrs 列表自己判断是否是重复的 reqr(详细见 sendmsg-reqr 的状态机),不管是否重复,都需要回复一个 ack-reqr,这是因为重复的 reqr 有可能是源节点超时重传的结果,必须回复一个 ack。这里防止重复是对上层而言的,防止将重复的数据传给上层,而不是防止接收到重复的 reqr。
路由问题
Requirement 只能发给直接相连的邻居路由器。 路由采用泊松分布模型。每个节点估计一个到目的节点的 mu 值。对源节点来说,一个 requirement 在 SENDING 状态下,
- 如果和目的节点直接相连,那么直接把 requirement 发给对方,进入 WAITING。超时设为 100 s。
- 如果和目的节点间接相连,发给其中一个 proxy,进入 WAITING,超时设为 100 s。
- 如果和目的节点不同时在线, union_lambda 设为 0。对每个邻居和自身,如果 1/mu > 1/3 * union_lambda (mu < 3 * union_mu),把 requirement 发给该邻居,
union_lambda = union_lambda + (1/mu)。 (超时设为 1/union_lambda * 2)
中继节点的的超时设为 mu * 2
目的节点的的超时(保留在 check_reqrs 列表的时间)设置为一个较大的值,目前取 6 天。
文件对象传输
如果进一步考虑文件对象的传输,还需要设计一个 getfile-reqr,这个 requirement 只是发送一个获取文件的请求,当目的节点收到请求之后,会发送两个 requirement, 一个是 sendfile-reqr,另一个是 ack-reqr,之所以要将数据和 ack 分开是因为如果目的节点收到多个重复的 getfile-reqr,就需要回复多个 ack,如果数据和 ack 是绑定 在一起的话,数据也会被重复发送多次。
(是否能够将各种对象的传输统一起来,只用一套 requirement 来传输?)
Message 管理
中转的消息只有 server 程序内对其有引用计数,可以用引用计数来管理其生命期。 非中转的消息由于本地 server 无法知道哪些本地 client 需要或正在使用它,所以不能用引用计数来管理。现在的方案是全部保存。
先考虑如何处理中转消息。
- rcvmsg-proc 只能接收以本机为目的地的消息。
- getmsg-proc 用于主动得到一条消息,在此期间它对消息有一个引用。之后, 该引用被传递给 requirement。
- 在 server 重启的时候, 加载所有的 message(非本地), 然后加载所有的其他模块,引用计数得到恢复。然后所有的 message 的引用计数减 1。
系统中的并发
GIT 同步
Ccnet 提供以下服务
- 将 git 仓库中的一个分支可靠同步到整个网络上
GUI
- 一个全局变量 CcnetClientSession *session: 这样每个 GUI 类和函数可以很方便的访问到 session。
- main() 函数需要先构造和初始化 session, 再构造和初始化各个界面元素,然后启动 session。
API
P2P 模块
put (key, value)
get (key)
当前工作
见 http://www.thoss.org.cn/trac/ccnet/report/1
参考资料
A Universally Unique IDentifier (UUID) URN Namespace rfc4122
Pastry http://freepastry.rice.edu
http://en.wikipedia.org/wiki/Collanos
http://www.thoss.org.cn/mediawiki/index.php/Ccnet