Californium

Let’s go!
引入Californium开源框架的依赖californium-core
启动服务端:

  1. public static void main(String[] args) {
  2. // 创建服务端
  3. CoapServer server = new CoapServer();
  4. // 启动服务端
  5. server.start();
  6. }

让我们从CoapServer这个类开始,对整个框架进行分析。首先让我们看看构造方法CoapServer()里面做了什么:

  1. public CoapServer(final NetworkConfig config, final int... ports) {
  2. // 初始化配置
  3. if (config != null) {
  4. this.config = config;
  5. } else {
  6. this.config = NetworkConfig.getStandard();
  7. }
  8. // 初始化Resource
  9. this.root = createRoot();
  10. // 初始化MessageDeliverer
  11. this.deliverer = new ServerMessageDeliverer(root);
  12. CoapResource wellKnown = new CoapResource(".well-known");
  13. wellKnown.setVisible(false);
  14. wellKnown.add(new DiscoveryResource(root));
  15. root.add(wellKnown);
  16. // 初始化EndPoints
  17. this.endpoints = new ArrayList<>();
  18. // 初始化线程池
  19. this.executor = Executors.newScheduledThreadPool(this.config.getInt(NetworkConfig.Keys.PROTOCOL_STAGE_THREAD_COUNT), new NamedThreadFactory("CoapServer#"));
  20. // 添加Endpoint
  21. for (int port : ports) {
  22. addEndpoint(new CoapEndpoint(port, this.config));
  23. }
  24. }

构造方法初始化了一些成员变量。其中,Endpoint负责与网络进行通信,MessageDeliverer负责分发请求,Resource负责处理请求。接着让我们看看启动方法start()又做了哪些事:

  1. public void start() {
  2. // 如果没有一个Endpoint与CoapServer进行绑定,那就创建一个默认的Endpoint
  3. ...
  4. // 一个一个地将Endpoint启动
  5. int started = 0;
  6. for (Endpoint ep:endpoints) {
  7. try {
  8. ep.start();
  9. ++started;
  10. } catch (IOException e) {
  11. LOGGER.log(Level.SEVERE, "Cannot start server endpoint [" + ep.getAddress() + "]", e);
  12. }
  13. }
  14. if (started==0) {
  15. throw new IllegalStateException("None of the server endpoints could be started");
  16. }
  17. }

启动方法很简单,主要是将所有的Endpoint一个个启动。至此,服务端算是启动成功了。让我们稍微总结一下几个类的关系
物联网协议之CoAP协议开发学习笔记之Californium开源框架分析(入门) - 图1
如上图,消息会从Network模块传输给对应的Endpoint节点,所有的Endpoint节点都会将消息推给MessageDeliverer,MessageDeliverer根据消息的内容传输给指定的Resource,Resource再对消息内容进行处理。
接下来,将让我们再模拟一个客户端发起一个GET请求,看看服务端是如何接收和处理的吧!客户端代码如下:

  1. public static void main(String[] args) throws URISyntaxException {
  2. // 确定请求路径
  3. URI uri = new URI("127.0.0.1");
  4. // 创建客户端
  5. CoapClient client = new CoapClient(uri);
  6. // 发起一个GET请求
  7. client.get();
  8. }

通过前面分析,我们知道Endpoint是直接与网络进行交互的,那么客户端发起的GET请求,应该在服务端的Endpoint中收到。框架中Endpoint接口的实现类只有CoapEndpoint,让我们深入了解一下CoapEndpoint的内部实现,看看它是如何接收和处理请求的。

CoapEndpoint类

CoapEndpoint类实现了Endpoint接口,其构造方法如下:

  1. public CoapEndpoint(Connector connector, NetworkConfig config, ObservationStore store) {
  2. this.config = config;
  3. this.connector = connector;
  4. if (store == null) {
  5. this.matcher = new Matcher(config, new NotificationDispatcher(), new InMemoryObservationStore());
  6. } else {
  7. this.matcher = new Matcher(config, new NotificationDispatcher(), store);
  8. }
  9. this.coapstack = new CoapStack(config, new OutboxImpl());
  10. this.connector.setRawDataReceiver(new InboxImpl());
  11. }

从构造方法可以了解到,其内部结构如下所示:
物联网协议之CoAP协议开发学习笔记之Californium开源框架分析(入门) - 图2
那么,也就是说客户端发起的GET请求将被InboxImpl类接收。InboxImpl类实现了RawDataChannel接口,该接口只有一个receiveData(RawData raw)方法,InboxImpl类的该方法如下:

  1. public void receiveData(final RawData raw) {
  2. // 参数校验
  3. ...
  4. // 启动线程处理收到的消息
  5. runInProtocolStage(new Runnable() {
  6. @Override
  7. public void run() {
  8. receiveMessage(raw);
  9. }
  10. });
  11. }

再往receiveMessage(RawData raw)方法里看:

  1. private void receiveMessage(final RawData raw) {
  2. // 解析数据源
  3. DataParser parser = new DataParser(raw.getBytes());
  4. // 如果是请求数据
  5. if (parser.isRequest()) {
  6. // 一些非关键操作
  7. ...
  8. // 消息拦截器接收请求
  9. for (MessageInterceptor interceptor:interceptors) {
  10. interceptor.receiveRequest(request);
  11. }
  12. // 匹配器接收请求,并返回Exchange对象
  13. Exchange exchange = matcher.receiveRequest(request);
  14. // Coap协议栈接收请求
  15. coapstack.receiveRequest(exchange, request);
  16. }
  17. // 如果是响应数据,则与请求数据一样,分别由消息拦截器、匹配器、Coap协议栈接收响应
  18. ...
  19. // 如果是空数据,则与请求数据、响应数据一样,分别由消息拦截器、匹配器、Coap协议栈接收空数据
  20. ...
  21. // 一些非关键操作
  22. ...
  23. }

接下来,我们分别对MessageInterceptor(消息拦截器)、Matcher(匹配器)、CoapStack(Coap协议栈)进行分析,看看他们接收到请求后做了什么处理。

MessageInterceptor接口

物联网协议之CoAP协议开发学习笔记之Californium开源框架分析(入门) - 图3
框架本身并没有提供该接口的任何实现类,我们可以根据业务需求实现该接口,并通过CoapEndpoint.addInterceptor(MessageInterceptor interceptor)方法添加具体的实现类。

Matcher类

物联网协议之CoAP协议开发学习笔记之Californium开源框架分析(入门) - 图4
我们主要看receiveRequest(Request request)方法,看它对客户端的GET请求做了哪些操作:

  1. public Exchange receiveRequest(Request request) {
  2. // 根据Request请求,填充并返回Exchange对象
  3. ...
  4. }

CoapStack类

物联网协议之CoAP协议开发学习笔记之Californium开源框架分析(入门) - 图5
CoapStack的类图比较复杂,其结构可以简化为下图:
物联网协议之CoAP协议开发学习笔记之Californium开源框架分析(入门) - 图6
有人可能会疑惑,这个结构图是怎么来,答案就在构造方法里:

  1. public CoapStack(NetworkConfig config, Outbox outbox) {
  2. // 初始化栈顶
  3. this.top = new StackTopAdapter();
  4. // 初始化栈底
  5. this.bottom = new StackBottomAdapter();
  6. // 初始化出口
  7. this.outbox = outbox;
  8. // 初始化ReliabilityLayer
  9. ...
  10. // 初始化层级
  11. this.layers =
  12. new Layer.TopDownBuilder()
  13. .add(top)
  14. .add(new ObserveLayer(config))
  15. .add(new BlockwiseLayer(config))
  16. .add(reliabilityLayer)
  17. .add(bottom)
  18. .create();
  19. }

回归正题,继续看CoapStack.receiveRequest(Exchange exchange, Request request)方法是怎么处理客户端的GET请求:

  1. public void receiveRequest(Exchange exchange, Request request) {
  2. bottom.receiveRequest(exchange, request);
  3. }

CoapStack在收到请求后,交给了StackBottomAdapter去处理,StackBottomAdapter处理完后就会依次向上传递给ReliabilityLayer、BlockwiseLayer、ObserveLayer,最终传递给StackTopAdapter。中间的处理细节就不详述了,直接看StackTopAdapter.receiveRequest(Exchange exchange, Request request)方法:

  1. public void receiveRequest(Exchange exchange, Request request) {
  2. // 一些非关键操作
  3. ...
  4. // 将请求传递给消息分发器
  5. deliverer.deliverRequest(exchange);
  6. }

可以看到,StackTopAdapter最后会将请求传递给MessageDeliverer,至此CoapEndpoint的任务也就算完成了,我们可以通过一张请求消息流程图来回顾一下,一个客户端GET请求最终是如何到达MessageDeliverer的:
物联网协议之CoAP协议开发学习笔记之Californium开源框架分析(入门) - 图7

MessageDeliverer接口

物联网协议之CoAP协议开发学习笔记之Californium开源框架分析(入门) - 图8
框架有ServerMessageDeliverer和ClientMessageDeliverer两个实现类。从CoapServer的构造方法里知道使用的是ServerMessageDeliverer类。那么就让我们看看ServerMessageDeliverer.deliverRequest(Exchange exchange)方法是如何分发GET请求的:

  1. public void deliverRequest(final Exchange exchange) {
  2. // 从exchange里获取request
  3. Request request = exchange.getRequest();
  4. // 从request里获取请求路径
  5. List<String> path = request.getOptions().getUriPath();
  6. // 找出请求路径对应的Resource
  7. final Resource resource = findResource(path);
  8. // 一些非关键操作
  9. ...
  10. // 由Resource来真正地处理请求
  11. resource.handleRequest(exchange);
  12. // 一些非关键操作
  13. ...
  14. }

当MessageDeliverer找到Request请求对应的Resource资源后,就会交由Resource资源来处理请求。(是不是很像Spring MVC中的DispatcherServlet,它也负责分发请求给对应的Controller,再由Controller自己处理请求)

Resource接口

物联网协议之CoAP协议开发学习笔记之Californium开源框架分析(入门) - 图9
还记得CoapServer构造方法里创建了一个RootResource吗?它的资源路径为空,而客户端发起的GET请求默认也是空路径。那么ServerMessageDeliverer就会把请求分发给RootResource处理。RootResource类没有覆写handleRequest(Exchange exchange)方法,所以我们看看CoapResource父类的实现:

  1. public void handleRequest(final Exchange exchange) {
  2. Code code = exchange.getRequest().getCode();
  3. switch (code) {
  4. case GET: handleGET(new CoapExchange(exchange, this)); break;
  5. case POST: handlePOST(new CoapExchange(exchange, this)); break;
  6. case PUT: handlePUT(new CoapExchange(exchange, this)); break;
  7. case DELETE: handleDELETE(new CoapExchange(exchange, this)); break;
  8. }
  9. }

由于我们客户端发起的是GET请求,那么将会进入到RootResource.handleGET(CoapExchange exchange)方法:

  1. public void handleGET(CoapExchange exchange) {
  2. // 由CoapExchange返回响应
  3. exchange.respond(ResponseCode.CONTENT, msg);
  4. }

再接着看CoapExchange.respond(ResponseCode code, String payload)方法:

  1. public void respond(ResponseCode code, String payload) {
  2. // 生成响应并赋值
  3. Response response = new Response(code);
  4. response.setPayload(payload);
  5. response.getOptions().setContentFormat(MediaTypeRegistry.TEXT_PLAIN);
  6. // 调用同名函数
  7. respond(response);
  8. }

看看同名函数里又做了哪些操作:

  1. public void respond(Response response) {
  2. // 参数校验
  3. ...
  4. // 设置Response属性
  5. ...
  6. // 检查关系
  7. resource.checkObserveRelation(exchange, response);
  8. // 由成员变量Exchange发送响应
  9. exchange.sendResponse(response);
  10. }

那么Exchange.sendResponse(Response response)又是如何发送响应的呢?

  1. public void sendResponse(Response response) {
  2. // 设置Response属性
  3. response.setDestination(request.getSource());
  4. response.setDestinationPort(request.getSourcePort());
  5. setResponse(response);
  6. // 由Endpoint发送响应
  7. endpoint.sendResponse(this, response);
  8. }

原来最终还是交给了Endpoint去发送响应了啊!之前的GET请求就是从Endpoint中来的。这真是和达康书记一样,从人民中来,再到人民中去。
在CoapEndpoint类一章节中我们有介绍它的内部结构。那么当发送响应的时候,将与之前接收请求相反,先由StackTopAdapter处理、再是依次ObserveLayer、BlockwiseLayer、ReliabilityLayer处理,最后由StackBottomAdapter处理,中间的细节还是老样子忽略,让我们直接看StackBottomAdapter.sendResponse(Exchange exchange, Response response)方法:

  1. public void sendResponse(Exchange exchange, Response response) {
  2. outbox.sendResponse(exchange, response);
  3. }

请求入口是CoapEndpoint.InboxImpl,而响应出口是CoapEndpint.OutboxImpl,简单明了。最后,让我们看看OutboxImpl.sendResponse(Exchange exchange, Response response)吧:

  1. public void sendResponse(Exchange exchange, Response response) {
  2. // 一些非关键操作
  3. ...
  4. // 匹配器发送响应
  5. matcher.sendResponse(exchange, response);
  6. // 消息拦截器发送响应
  7. for (MessageInterceptor interceptor:interceptors) {
  8. interceptor.sendResponse(response);
  9. }
  10. // 真正地发送响应到网络里
  11. connector.send(Serializer.serialize(response));
  12. }

通过一张响应消息流程图来回顾一下,一个服务端响应最终是如何传输到网络里去:
物联网协议之CoAP协议开发学习笔记之Californium开源框架分析(入门) - 图10

总结

通过服务端的创建和启动,客户端发起GET请求,服务端接收请求并返回响应流程,我们对Californium框架有了一个整体的了解。俗话说,师父领进门,修行看个人。在分析这个流程的过程中,我省略了很多的细节,意在让大家对框架有个概念上的理解,在以后二次开发或定位问题时更能抓住重点,着重针对某个模块。最后,也不得不赞叹一下这款开源框架代码逻辑清晰,模块职责划分明确,灵活地使用设计模式,非常值得我们学习!

RFC7252-《受限应用协议》中文版.md
californium 框架设计分析

如果对你有帮助,点赞收藏手有余香!