Ccnet

早上搜索了下 清华大学开源软件俱乐部的网站,找到这点关于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-usage

ccnet-talk-01

目录

[显示]


概览

代码页面

几个设计原则

  • 提供机制,把策略留给上层。 Ccnet 底层是 P2P 网络。 每个节点向其他的节点提供一个功能列表(可调用的 processor 列表),通过互相之间调用这些功能可以实现许多不同的上层功能。
  • 关注于易用性
  • 尽量底层: 直接使用 TCP 提供的字节流服务。不使用 XML,不使用 DBUS。

为什么使用 C 和 GObject

  • 这是一个底层的代码,大量的 char *buf 操作
  • 易被理解易被扩展
  • 只在必要的时候使用面向对象编程功能

模块

Ccnet 层次结构

Ccnet 层次结构

 

Ccnet 运行流

Ccnet 运行流

 

Ccnet 模块
功能模块 文件 说明
工具模块
事件管理 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 有两种方式

  1. 主动添加: 通过邮件等方式得到一个 peer 的 info 文件,将该文件加入到 peer-db 中。
  2. 被动添加: 其他人向你发送一个 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

 

评论已关闭。