您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 订阅
  捐助
californium 框架设计分析
 
来源:博客 发布于: 2017-6-7
  3782  次浏览      19
 

1. Californium 项目简介

Californium 是一款基于Java实现的Coap技术框架,该项目实现了Coap协议的各种请求响应定义,支持CON/NON不同的可靠性传输模式。

Californium 基于分层设计且高度可扩展,其内部模块设计及接口定义存在许多学习之处;

值得一提的是,在同类型的 Coap技术实现中,Californium的性能表现是比较突出的,如下图:

更多的数据可以参考Californium-可扩展云服务白皮书

本文以框架的源码分析为主,其他内容不做展开。

2. 项目结构

目前Californium 项目稳定版本为 2.0.0-M2,项目的托管地址在:

https://github.com/eclipse/californium

模块说明

~.californium-core

californium 核心模块,定义了一系列协议栈核心接口,并提供了Coap协议栈的完整实现,

~.element-connector

从core模块剥离的连接器模块,用于抽象网络传输层的接口,使得coap可以同时运行于udp和tcp多种传输协议之上;

~.scandium-core

Coap over DTLS 支持模块,提供了DTLS 传输的Connector实现;

~.californium-osgi

californium 的osgi 封装模块;

~.californium-proxy

coap 代理模块,用于支持coap2coap、coap2http、http2coap的转换;

~.demo-xxx

样例程序;

其中,californium-core和element-connector是coap技术实现最关键的模块,后面的分析将围绕这两个模块进行。

3. 分层设计

Californiium 定义了三层架构:

1 网络层,负责处理端口监听,网络数据收发;

2 协议层,负责Coap协议数据包解析及封装,实现消息的路由、可靠性传输、Token处理、观察者模型等等;

3 逻辑层,负责 Resource定义和映射,一个Resource 对应一个URL,可独立实现Coap 请求处理。

异步线程池

三层架构中都可以支持独立的线程池,其中网络层与协议层的线程池保持独立;

逻辑层可为每个Resource指定独立的线程池,并支持父级继承的机制,即当前Resource若没有定义则沿用父级Resource线程池;

若逻辑层未指定线程池,则默认使用协议层的线程池。

4. 包结构分析

4.1 californium-core

core 模块定义了协议栈相关的所有关键接口,根据功能职责的不同拆分为多个子 package;

根级 package定义的是Coap应用的一些入口类,如Client/Server实现、包括应用层CoapResource的定义。

4.1.1 package-coap

实现 coap协议 RFC7252 实体定义,包括消息类型、消息头、Observe机制等。

具体定义见下图

Coap 消息划分为Request/Response/EmptyMessage 三类;

MessageObserver 接口用于实现消息的状态跟踪,如重传、确认等。

4.1.2 package-network

network 是协议栈核心机制实现的关键模块,其涵盖了网络传输及协议层的定义及实现;

模块实现了一些关键接口定义,如将网络传输端点抽象为Endpoint,根据请求响应的关联模型定义了Exchange等。

协议栈的分层定义、消息编解码、拦截处理也由network包提供。

endpoins定义

Endpoint 定义为一个端点,通常与一个IP和端口对应,其屏蔽了client和server交互时的网络传输细节。

对于client来说,Endpoint代表通讯的服务端地址端口;而对于server来说则代表了绑定监听的地址及端口。

CoapEndpoint实现了Endpoint接口,通过RawDataChannel(见elements-connector部分)接口实现消息接收,通过Outbox接口实现消息发送。

通常CoapEndpoint 会关联一个Connector,以实现传输层的收发;

CoapStack对应了协议栈接口,用于处理CoapEndpoint上层的消息链路;

除此之外,CoapEndpoint 还应该包括消息编解码、拦截处理等功能。

exchange定义

Exchange描述了请求-响应模型,一个Exchange会对应一个Request,相应的Response,以及当前的Endpoint;

ExchangeObserver用于实现对Exchange状态的变更监听;

Exchange 通常存在于两种场景:

1 发送请求后初始化并存储,当接收到对应的响应之后变更为completed(执行清理工作)。

2 接收请求后初始化并存储,当发送响应时执行清理;

matcher定义

Matcher 是用于实现Exchange 生成及销毁的模块,提供了几个收发接口;

用于消息在进入协议栈CoapStack处理之前完成配对处理;

messagetool定义

MessageExchangeStore 实现了Exchange的查询、存储;

MessageIdProvider 用于提供Coap消息的MID,一个MID代表了一个唯一的消息(在消息生命周期内);

TokenProvider 用于提供Coap消息的Token,而Request及Response通过Token实现匹配;

network子模块

package-config

提供网络参数配置定义

package-deduplication

提供消息去重机制的实现

package-interceptors

提供消息传输拦截器定义

package-serialization

提供消息包的解析及编码实现

package-stack

提供协议栈分层定义及实现

4.1.3 package-server

应用层 server端实现的一些定义,包括Server接口、Resource定义。

CoapServer 可包含多个Endpoint,体现为一个Coap服务可架设在多个传输端口之上;

MessageDeliverer 是消息路由的接口,ServerMessageDelivery 实现了根据uri 查找Resource的功能;

ConcurrentCoapResource则为Resource 提供了一个独立线程池的执行方式。

4.1.4 package-observe

应用层 observe机制的定义,如下图

ObserveRelation 定义一个观察关系,对应一个观察者即观察目标Resource;

ObserveEndpoint 定义了一个观察者端点,并包含一个关系列表(一个观察者可以观察多个Resource);

ObserveManager 由CoapServer持有,用于管理观察者端点列表;

CoapResource 也会持有一个Relation集合以实现跟踪;其通过ObserveRelationFilter接口决定是否接受来自观察者的注册请求;

4.2 elements-connector

connector 模块由core模块剥离,用于实现网络传输层的抽象,这使得Coap协议可以运行于UDP、TCP、DTLS等多种协议之上。

Connector定义了连接器需实现的相关方法,包括启动停止、数据的收发;

RawData包含了网络消息包的原始字节数据,其解析和编码需要交由上层协议实现;

CorrelationContext 描述了上下文,用于支持传输协议的一些会话数据读写,如DTLS会话。

4.3. 核心接口

下面拟用一张关系图概括Californium 框架的全貌(部分内容未体现):

与分层设计对应,框架分为 transport 传输层、protocol 协议层、logic 逻辑层。

transport 传输层,由Connector 提供传输端口的抽象,UDPConnector是其主要实现;

数据包通过RawData对象封装;该层还提供了CorrelationContext 实现传输层会话数据的读写支持。

protocol 协议层,提供了Coap 协议栈机制的完整实现;CoapEndpoint是核心的操作类,数据的编解码通过 DataSerializer、DataParser实现,MessageInterceptor提供了消息收发的拦截功能,Request/Response的映射处理 由 Matcher实现,Exchange 描述了映射模型;协议栈CoapStack 是一个分层的内核实现,在这里完成分块、重传等机制。

logic 逻辑层,定义了CoapClient、CoapServer的入口,包括消息的路由机制,Resource的继承机制;

Observe机制的关系维护、状态管理由ObserveManager提供入口。

5. 关键机制

5.1 协议栈;

californium-core 采用了分层接口来定义协议栈,其中CoapStack 描述整个栈对象,Layer则对应分层的处理;

这相当于采用了过滤器模式,分层的定义使得特性间互不影响,子模块可保持独立的关注点;

CoapStack定义如下:

public interface CoapStack {
// delegate to top
void sendRequest(Request request);
// delegate to top
void sendResponse(Exchange exchange, Response response);
...
// delegate to bottom
void receiveRequest(Exchange exchange, Request request);
// delegate to bottom
void receiveResponse(Exchange exchange, Response response);

接口包括了几个消息收发函数,而Layer也定义了一样的接口。

一个CoapUdpStack 包括的分层如下图:

CoapUdpStack 构造函数与此对应:

public CoapUdpStack(final NetworkConfig config, final Outbox outbox) {
...
Layer layers[] = new Layer[] {
new ExchangeCleanupLayer(),
new ObserveLayer(config),
new BlockwiseLayer(config),
reliabilityLayer };
setLayers(layers);
}

StackTopLayer和StackBottomLayer由基础类BaseCoapStack提供,实现了协议栈顶层和底层逻辑;

MessageDeliver是胶合应用层的接口,其从StackTopLayer收到Coap消息之后将继续分发到Resource;

StackBottomLayer则胶合了传输层,通过Inbox/Outbox接口实现与Connector的交互。

其他Layer的功能

ExchangeCleanLayer 提供Exchange清理功能,当取消请求时触发Exchange的清理功能;

ObserveLayer 提供Coap Observe机制实现;

BlockwiseLayer 提供Coap 分块传输机制实现;

ReliabilityLayer 提供可靠性传输,实现自动重传机制;

5.2 Exchange生命周期

Exchange对应于请求/响应模型,其生命周期也由交互模型决定,一般在响应结束之后Exchange便不再存活;

然而在Observe场景下例外,一旦启动了Observe请求,Exchange会一直存活直到Observe被取消或中断。

1 LocalExchange,即本地的Exchange, 对应于本地请求对方响应的交互。

BaseCoapStack.StackTopLayer实现了初始化:

public void sendRequest(final Request request) {
Exchange exchange = new Exchange(request, Origin.LOCAL);
...

当接收响应时进行销毁,observe类型的请求在这里被忽略:

public void receiveResponse(final Exchange exchange, final Response response) {
if (!response.getOptions().hasObserve()) {
exchange.setComplete();
}

UdpMatcher 实现了销毁动作:

UdpMatcher--
public void sendRequest(final Exchange exchange, final Request request) {
exchange.setObserver(exchangeObserver);
exchangeStore.registerOutboundRequest (exchange);
if (LOGGER.isLoggable(Level.FINER)) {

这是在发送请求时为Exchange添加观察者接口,当exchange执行complete操作时触发具体的销毁工作:

UdpMatcher.ExchangeObserverImpl--
if (exchange.getOrigin() == Origin.LOCAL) {
// this endpoint created the Exchange by issuing a request
KeyMID idByMID = KeyMID.fromOutboundMessage (exchange.getCurrentRequest());
KeyToken idByToken = KeyToken.fromOutboundMessage (exchange.getCurrentRequest());
exchangeStore.remove(idByToken);
// in case an empty ACK was lost
exchangeStore.remove(idByMID);
...

值得一说的是,californium大量采用了观察者设计模式,这种方法在设计异步消息机制时非常有用.

此外,request的取消、中断操作(RST信号)、传输的超时都会导致exchange生命周期结束。

LocalExchange的生命周期如下图:

2 RemoteExchange,即远程的Exchange,对应于本地接收请求并返回响应的交互。

UdpMatcher实现了远程Exchange的初始化:

UdpMatcher--
public Exchange receiveRequest(final Request request) {
...
KeyMID idByMID = KeyMID.fromInboundMessage(request);
if (!request.getOptions().hasBlock1() && !request.getOptions(). hasBlock2()) {
Exchange exchange = new Exchange(request, Origin.REMOTE);
Exchange previous = exchangeStore.findPrevious(idByMID, exchange);
if (previous == null) {
exchange.setObserver(exchangeObserver);
...

在发送响应时,Exchange被销毁,仍然由UdpMatcher实现:

UdpMatcher--
public void sendResponse(final Exchange exchange, final Response response) {
response.setToken(exchange. getCurrentRequest().getToken());
...
// Only CONs and Observe keep the exchange active (CoAP server side)
if (response.getType() != Type.CON && response.isLast()) {
exchange.setComplete();
}

注意到这里对response进行了last属性的判断,该属性默认为true,而ObserveLayer将其置为false,使得observe响应不会导致Exchange结束:

ObserveLayer--
public void sendResponse(final Exchange exchange, Response response) {
...
response.setLast(false);

连接中断(RST信号)、传输超时会导致Exchange的结束,此外由客户端发起的observe取消请求也会产生一样的结果。

RemoteExchange的生命周期如下图所示:

5.3 分块传输;

分块传输一般用于发送较大的请求体或接受较大的响应体,比如上传下载固件包场景,由于受到MTU的限制,需要实现分块传输;

Coap定义了分块传输的方式,采用Block1/Block2机制

Option选项

BlockOption是用于描述分块信息的选项类型,选项值为0-3个字节,编码包含了3个字段:当前分块编号;是否结束;当前分块大小。

为区分请求和响应的不同,分别有block1和block2 两个选项:

block1:用于发送POST/PUT请求时传输较大的内容体;

block2:用于响应GET/POST/PUT请求时传输较大的内容体;

size1:指示请求体的总大小;

size2:指示响应体的总大小;

配置选项

maxMessageSize:消息大小阈值,当发送的消息大于该阈值时需采用分块传输,该值必须小于MTU;

preferredBlockSize:用于指示分块的大小;

maxResourceBodySize:最大资源内容体大小,用于限制接收的请求或响应的总大小,若超过将提示错误或取消处理;

blockLifeTime:分块传输的生命周期时长,若超过该时长分块传输未完成则视为失败;

BlockwiseLayer实现了分块传输的完整逻辑,其中sendRequest的代码片段:

public void sendRequest( final Exchange exchange, final Request request) {
BlockOption block2 = request. getOptions().getBlock2();
if (block2 != null && block2.getNum() > 0) {
//应用层指定的分块..
} else if (requiresBlockwise(request)) {
//自动计算分块
startBlockwiseUpload(exchange, request);
} else {
//不需要分块
exchange.setCurrentRequest(request);
lower().sendRequest(exchange, request);
}
}
...
//实现分块阈值判断
private boolean requiresBlockwise (final Request request) {
boolean blockwiseRequired = false;
if (request.getCode() == Code.PUT || request.getCode() == Code.POST) {
blockwiseRequired = request .getPayloadSize() > maxMessageSize;
}
...
//startBlockwiseUpload实现了request分块逻辑,通过在请求的Option中加入Block1作为标识
private void startBlockwiseUpload (final Exchange exchange, final Request request) {
BlockwiseStatus status = findRequestBlockStatus(exchange, request);
final Request block = getNextRequestBlock(request, status);
block.getOptions().setSize1( request.getPayloadSize());
...
lower().sendRequest(exchange, block);
}

接收端检测Request的Block1选项,返回continue响应码,直到所有分块传输完成后进行组装交由上层处理:

private void handleInboundBlockwiseUpload (final BlockOption block1, final Exchange exchange, final Request request) {
//检查是否超过限制
if (requestExceedsMaxBodySize(request)) {
Response error = Response.createResponse (request, ResponseCode.REQUEST_ENTITY_TOO_LARGE);
error.setPayload(String.format("body too large, can process %d bytes max", maxResourceBodySize));
error.getOptions().setSize1( maxResourceBodySize);
lower().sendResponse( exchange, error);
} else {
...
if (block1.getNum() == status.getCurrentNum()) {
if (status.hasContentFormat( request.getOptions().getContentFormat())) {
status.addBlock( request.getPayload());
status.setCurrentNum( status.getCurrentNum() + 1);

if ( block1.isM() ) {
//存在后面的block,返回Continue响应
Response piggybacked = Response.createResponse (request, ResponseCode.CONTINUE);
piggybacked.getOptions().setBlock1( block1.getSzx(), true, block1.getNum());
piggybacked.setLast(false);
exchange.setCurrentResponse(piggybacked);
lower().sendResponse(exchange, piggybacked);
} else {
...
//已经完成,组装后交由上层处理
Request assembled = new Request( request.getCode());
assembled.setSenderIdentity( request.getSenderIdentity());
assembleMessage(status, assembled);
upper().receiveRequest( exchange, assembled);
}

因此,一个请求体分块传输流程如下图所示:

响应体分块传输的逻辑与此类似,交互流程如下图:

5.4 消息重传;

Coap消息支持重传机制,当发送CON类型的消息时,要求接收端响应对应的ACK消息;如果在指定时间内没有收到响应,则进行重传。
基础消息重传由ReliabilityLayer实现,sendRequest 代码片段:

if (request.getType() == null) {
request.setType(Type.CON);
}
if (request.getType() == Type.CON) {
prepareRetransmission(exchange, new RetransmissionTask (exchange, request) {
public void retransmit() {
sendRequest(exchange, request);
}
});
}
lower().sendRequest(exchange, request);

当发送CON类型消息时,通过 prepareRetransmission函数实现重传准备:

int timeout;
if (exchange.getFailedTransmissionCount() == 0) {
timeout = getRandomTimeout(ack_timeout, (int) (ack_timeout * ack_random_factor));
} else {
timeout = (int) (ack_timeout_scale * exchange. getCurrentTimeout());
}
exchange.setCurrentTimeout(timeout);
ScheduledFuture<?> f = executor.schedule(task, timeout, TimeUnit.MILLISECONDS);
exchange.setRetransmissionHandle(f);

exchange.getFailedTransmissionCount() 返回0 代表第一次传输,采用的超时时间是:

**timeout = random(ack_timeout, act_timeout*ack_random_factor)**

//其中ack_timeout(超时起始值)、ack_random_factor(随机因子)由配置文件提供;

后续的重传时间将由上一次的timeout和ack_timeout_scale系数决定:

timeout = timeout * ack_timeout_scale

当接收ACK时,有必要取消重传处理,看看receiveResponse的实现:

@Override
public void receiveResponse(final Exchange exchange, final Response response) {
exchange.setFailedTransmissionCount(0);
exchange.getCurrentRequest(). setAcknowledged(true);
exchange.setRetransmissionHandle(null);
...

可以看到,接收到响应之后,将Request标记为ack状态,exchange.setRestransmissionHandler会导致上一次的重传schedu任务被取消。
最终重传任务由RetransmissionTask实现:

int failedCount = exchange. getFailedTransmissionCount() + 1;
exchange.setFailedTransmissionCount (failedCount);
if (message.isAcknowledged()) {
return;
} else if (message.isRejected()) {
return;
} else if (message.isCanceled()) {
return;
} else if (failedCount <= max_retransmit) {
// Trigger MessageObservers
message.retransmitting();
// MessageObserver might have canceled
if (!message.isCanceled()) {
retransmit();
}
} else {
exchange.setTimedOut();
message.setTimedOut(true);
}

满足重传的条件:

1 消息未被确认(收到ACK)或拒绝(收到RST)

2 消息未被取消;

3 消息未超过重传次数限制;

其中重传次数max_retransmit由配置提供,当超过该次数限制时消息将发生传输超时。

默认参数配置

ack_timeout=2s
ack_random_factor=1.5
ack_timeout_scale=2
max_retransmit=4

5.5 防止重复包;

由于存在重传机制,加上UDP传输的不稳定性,传输两端很可能会受到重复的消息包;

通常重复消息的检测要求实现消息容器以记录和匹配重复消息ID,然而执行时间越长,消息会越来越多,

因此消息容器必须具备清除机制,基于此点不同,californium 提供了两种实现机制:

5.5.1 标记清除

清除器维持一个消息容器,每个消息都保持一个初始的时间戳;

清除器定时进行扫描,发现太老的消息则将其清除。

SweepDeduplicator 提供了实现,清除代码片段:

private void sweep() {
final long oldestAllowed = System.currentTimeMillis() - exchangeLifetime;
final long start = System.currentTimeMillis();
for (Map.Entry<?, Exchange> entry : incomingMessages.entrySet()) {
Exchange exchange = entry.getValue();
if (exchange.getTimestamp() < oldestAllowed) {
incomingMessages.remove(entry.getKey());
}
}
...

其中incomingMessage采用了ConcurrentHashMap数据结构,这是一个并发性良好的线程安全集合;

然而从上面的代码也可以发现,sweep在这里是一个遍历操作,定时清除的老化时间默认为247s,假设1s内处理1000条消息,

那么每次清除时驻留的消息数量为247000,即需要遍历这么多的次数,对于CPU来说存在一定的开销。

采用这种方式,消息的存活时间基本上由exchangeLifetime参数和扫描间隔决定。

5.5.2 翻转清除

清除器维持三个消息容器,保持1、2、3三个索引分别指向相应消息容器,其中索引1、2代表了活动的消息容器,
索引3 代表老化的消息容器,如下图所示

消息索引首次会往 I1容器写入,同时也会往 I2容器存入拷贝;

查找消息时主要从I1 容器查找;

每个周期会执行一次翻转,几个容器指针发生置换(I1->I2,I2->I3,I3->I1),之后I3 指向的容器会被清理;

CropRotation 实现了翻转的逻辑,代码如下:

private void rotation() {
synchronized (maps) {
int third = first;
first = second;
second = (second+1)%3;
maps[third].clear();
}

基于上述的算法分析,I2容器的消息存活时间会小于一个周期,I1容器的消息则存活一个周期到两个周期之间,I3 容器则超过2个周期,是最老的容器;

基于这样的逻辑,翻转清除机制的消息存活时间是1-2个周期之间,而该机制相比标记清除的优点在于清除机制是整个容器一块清除,而不需要遍历操作,然而缺点是增加了存储开销。

JVM的垃圾回收机制也存在类似的设计,相信californium的开发者借鉴了一些思路。

至此,Californium框架的基本全貌已经分析完毕。如果希望对框架有更深入的理解,那么建议你直接在项目中直接使用它,并针对自己感兴趣的几个问题进行源码分析或调试,相信收获会更多。

   
3782 次浏览       19
相关文章

Java微服务新生代之Nacos
深入理解Java中的容器
Java容器详解
Java代码质量检查工具及使用案例
相关文档

Java性能优化
Spring框架
SSM框架简单简绍
从零开始学java编程经典
相关课程

高性能Java编程与系统性能优化
JavaEE架构、 设计模式及性能调优
Java编程基础到应用开发
JAVA虚拟机原理剖析