我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Java实现CoAP协议栈:Android与Java服务端源码解析

Java实现CoAP协议栈:Android与Java服务端源码解析 简介这是一份面向物联网与Android开发者的CoAP协议Java实现源码包适合需要搭建CoAP服务器或进行协议学习的初中级开发者。资源分为app与coaplib两个模块app为Android服务器端代码coaplib则包含Java服务器实现及Java客户端测试代码可帮助读者理解CoAP基于UDP的双层事务机制、重传处理、资源发现与描述等核心特性并实践GET、POST、PUT、DELETE等资源操作方式。压缩包共297个文件以262个java源码为主体辅以12个xml配置、10个png图示、3个properties与3个gradle构建文件整体约582KB结构紧凑便于阅读。目前已有504人学习下载读者可借此快速掌握CoAP服务端与客户端的通信流程对照源码理解资源发现、查询参数筛选等实现细节为物联网通信开发提供可直接参考的代码基础。1. 从一台没有公网IP的安卓设备说起CoAPService 能帮你做什么去年帮一个做工业传感器的团队看代码他们的采集终端跑在安卓板子上需要把温度、振动数据实时传给内网的一台 Java 服务端。HTTP 轮询太费电MQTT 又要额外搭 Broker最后他们选了一条冷门但很对路的路CoAP。问题来了——安卓端做 CoAP 服务端、Java 端做 CoAP 服务端现成的开源实现要么只给客户端要么依赖一堆用不上的东西。翻到这份 CoAPService 源码时我第一反应是「终于有人把 Android 服务端和 Java 服务端拆开写清楚了」。它是一套用 Java 实现的 CoAP 协议栈同时提供 Android 端和标准 Java 端的服务端封装适合做物联网设备接入、低功耗传感网络、内网设备发现这类场景。如果你正在找一份能直接跑、能改协议参数、能看懂报文怎么拼的 CoAP 源码这份东西值得花一个下午拆一遍。2. 技术选型与目录结构为什么是 Java 写 CoAP代码怎么分层2.1 CoAP 协议本身决定了这套代码的形态CoAP 全称 Constrained Application Protocol是给资源受限设备设计的应用层协议跑在 UDP 之上报文头固定 4 字节后面跟 Token、Options、Payload。它借鉴了 HTTP 的 REST 语义GET/POST/PUT/DELETE但把传输层换成了 UDP还加了 Confirmable/Non-confirmable 消息类型来做轻量可靠传输。这意味着实现一套 CoAP 栈核心工作集中在三块报文的二进制编解码、消息重传与去重、资源路由。这份 CoAPService 的代码结构基本就是按这三块切的。它没有引入 Netty 这种重型框架而是直接用 Java 的DatagramSocket/DatagramPacket做 UDP 收发好处是依赖极简安卓端也能直接编译进去代价是并发模型要自己管后面讲线程模型时会细说。2.2 目录结构服务端、客户端、报文、资源四层拿到源码后我一般先看包结构因为它直接暴露了作者的抽象思路。这份代码大致分成这么几层coapservice/ ├── core/ # 协议核心 │ ├── CoapMessage.java # 报文对象含 Header/Token/Options/Payload │ ├── CoapEncoder.java # 报文序列化为字节数组 │ ├── CoapDecoder.java # 字节数组反序列化为报文 │ ├── CoapOption.java # Option 定义与编解码 │ └── CoapCode.java # 请求方法/响应码常量 ├── server/ # 服务端 │ ├── CoapServer.java # 服务端主类绑定端口、启动接收循环 │ ├── CoapResource.java # 资源抽象对应一个 URI 路径 │ └── RequestHandler.java # 请求分发与响应构造 ├── client/ # 客户端 │ └── CoapClient.java # 发送请求、等待响应、超时重传 ├── android/ # Android 适配层 │ └── AndroidCoapService.java # 封装 Service 生命周期 └── util/ # 工具 ├── ByteUtil.java └── CoapConstants.java这个分层的好处是core完全不依赖 Android可以单独拿去做 Java 服务端android层只做生命周期和线程切换的适配。二次开发时如果你只想改协议行为动core就够了如果要做安卓后台常驻改android层。2.3 依赖与构建几乎没有第三方依赖从源码的 import 看除了 JDK 自带的java.net、java.nio、java.util.concurrent没有引入任何第三方库。这对安卓端特别友好——不用处理依赖冲突APK 体积也不会因为一个协议栈膨胀。构建方式常见做法是用 Maven 或 Gradle 把core和server打成 jarandroid层作为 library module 引入。如果你拿到的是纯源码目录直接javac编译core和server下的类就能跑 Java 服务端。提示安卓端从 Android 9 开始默认禁止明文流量但 CoAP 走的是 UDP不受这条限制真正要注意的是安卓 10 以后后台 Service 的限制长连接场景需要配合前台服务。3. 核心模块拆解一个 CoAP 请求从 UDP 字节到响应的完整链路3.1 报文编解码4 字节头 Option 的 TLV 结构CoAP 报文头是固定的 4 字节格式如下0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -------------------------------- |Ver| T | TKL | Code | Message ID | -------------------------------- | Token (if any, TKL bytes) ... -------------------------------- | Options (if any) ... -------------------------------- |1 1 1 1 1 1 1 1| Payload (if any) ... --------------------------------Ver 是版本号固定为 1T 是消息类型0CON, 1NON, 2ACK, 3RSTTKL 是 Token 长度Code 表示请求方法或响应码Message ID 用于匹配 ACK 和去重。解码时先读第一个字节拿到 Ver/T/TKL再读 Code 和 Message ID然后按 TKL 读 Token接着循环解析 Options直到遇到 0xFF 分隔符剩下的就是 Payload。编码逻辑在CoapEncoder里核心是把 Options 按编号升序排列后做 delta 编码——每个 Option 存的是「与上一个 Option 编号的差值」这样能省字节。这部分是 CoAP 实现里最容易写错的地方因为 delta 超过 12 时要扩展成 13/14 两种变长编码。3.2 服务端启动与接收循环DatagramSocket 怎么收包CoapServer的主循环是整个服务端的入口逻辑不复杂但有几个关键点public class CoapServer { private DatagramSocket socket; private volatile boolean running true; private final MapString, CoapResource resources new ConcurrentHashMap(); public void start(int port) throws SocketException { // 绑定 UDP 端口安卓端注意用 5683 以外的端口避免权限问题 socket new DatagramSocket(port); // 单独线程跑接收循环避免阻塞调用方 new Thread(this::receiveLoop, coap-receiver).start(); } private void receiveLoop() { byte[] buffer new byte[1024]; // CoAP 报文通常很小1KB 足够 while (running) { try { DatagramPacket packet new DatagramPacket(buffer, buffer.length); socket.receive(packet); // 阻塞直到有数据 // 每个请求丢给线程池处理避免慢请求卡住接收 executor.submit(() - handlePacket(packet)); } catch (IOException e) { if (running) e.printStackTrace(); } } } }这里有几个参数值得说buffer大小设 1024 是因为 CoAP 设计上就是小报文超过 MTU 会分块Block-wise Transfer这份代码没实现分块所以别指望传大文件。executor是线程池常见做法是用Executors.newFixedThreadPool(4)或newCachedThreadPool()前者适合请求量稳定的场景后者适合突发。socket.receive是阻塞的所以必须放在独立线程里否则start()调用方会被卡死。3.3 资源路由URI 怎么映射到 HandlerCoAP 的 URI 路径存在 Uri-Path 这个 Option 里每个路径段是一个独立的 Option。比如coap://host/sensors/temp会编码成两个 Uri-Path Option值分别是sensors和temp。RequestHandler的工作就是把这些段拼起来去resourcesmap 里找对应的CoapResourceprivate void handlePacket(DatagramPacket packet) { CoapMessage request CoapDecoder.decode(packet.getData(), packet.getLength()); // 从 Options 里提取 Uri-Path拼成 /sensors/temp 这种形式 String path extractPath(request); CoapResource resource resources.get(path); if (resource null) { // 没找到资源返回 4.04 Not Found sendResponse(packet, CoapCode.NOT_FOUND, null); return; } // 按请求方法分发GET/POST/PUT/DELETE CoapMessage response resource.handle(request); sendResponse(packet, response.getCode(), response.getPayload()); }extractPath要处理 Option 的顺序和重复因为 Uri-Path 可能出现多次。resource.handle是业务扩展点——你自定义的资源继承CoapResource并覆写handle方法就能接入自己的数据逻辑。响应发送时要注意如果请求是 CON 类型响应必须带相同的 Message ID 且类型为 ACK如果是 NON响应也用 NON。3.4 客户端与重传Confirmable 消息的可靠性怎么保证CoAP 的可靠性靠 CON 消息 ACK 实现。客户端发 CON 请求后启动定时器超时没收到 ACK 就重传重传间隔按指数退避常见是 2s、4s、8s…重传次数到上限就放弃。CoapClient里这段逻辑大致是public CoapMessage send(CoapMessage request, int maxRetry) { int retry 0; long timeout INITIAL_TIMEOUT; // 初始 2000ms while (retry maxRetry) { socket.send(encode(request)); CoapMessage response waitForResponse(timeout); if (response ! null) return response; retry; timeout * 2; // 指数退避 } throw new TimeoutException(CoAP request timeout after maxRetry retries); }INITIAL_TIMEOUT和maxRetry是两个关键参数。工业场景网络抖动大我一般把初始超时设到 3s、重试 3 次如果是局域网内1s 2 次就够。waitForResponse里要做 Message ID 匹配因为 UDP 可能收到乱序或重复的包不匹配的包要丢弃。4. 跑起来Java 服务端与 Android 服务端的最小可运行示例4.1 Java 服务端20 行代码起一个 CoAP 服务先跑 Java 端因为它不依赖安卓环境验证协议栈最快。假设你已经把core和server编译好写一个入口类public class DemoServer { public static void main(String[] args) throws Exception { CoapServer server new CoapServer(); // 注册一个资源路径 /hello server.addResource(/hello, new CoapResource() { Override public CoapMessage handle(CoapMessage request) { // 不管什么方法都返回一段文本 CoapMessage resp new CoapMessage(); resp.setCode(CoapCode.CONTENT); // 2.05 Content resp.setPayload(hello coap.getBytes()); return resp; } }); server.start(5683); // CoAP 默认端口 System.out.println(CoAP server started on 5683); } }addResource的路径要以/开头内部会按/拆成 Uri-Path Option 段。CoapCode.CONTENT对应响应码 2.05表示成功返回内容。启动后可以用任意 CoAP 客户端比如命令行工具或另一台设备发coap://127.0.0.1:5683/hello测试。注意5683 是 CoAP 标准端口但某些系统上低于 1024 的端口需要 root 权限安卓上更是如此。开发阶段建议换成 56830 这类高位端口。4.2 Android 服务端Service 生命周期与线程切换安卓端的封装在AndroidCoapService它继承Service在onCreate里启动 CoapServer在onDestroy里关闭。关键是要把 UDP 接收循环放到工作线程并且处理好网络状态变化public class AndroidCoapService extends Service { private CoapServer server; Override public void onCreate() { super.onCreate(); server new CoapServer(); server.addResource(/status, new StatusResource()); try { server.start(56830); // 高位端口避免权限问题 } catch (SocketException e) { Log.e(CoapService, start failed, e); } } Override public void onDestroy() { if (server ! null) server.stop(); // 释放 socket否则端口泄漏 super.onDestroy(); } Override public IBinder onBind(Intent intent) { return null; } }onDestroy里必须调server.stop()否则DatagramSocket不关闭下次启动会报端口占用。安卓 8.0 以后后台 Service 会被限制如果要做常驻常见做法是startForeground配合通知栏或者用JobScheduler定期拉起。4.3 联调验证用抓包确认报文对不对跑起来之后怎么确认协议栈没问题我一般用两步先在服务端handlePacket里打印收到的原始字节的十六进制再对照 CoAP 报文格式手工解析一遍。比如一个 GET/hello的 CON 请求字节大概是40 01 12 34开头——0x40表示 Ver1、T0CON、TKL00x01是 CodeGET0x12 0x34是 Message ID。如果打印出来对不上问题多半在编码或解码的字节序处理上。5. 避坑与排查CoAP 服务端开发中最容易翻车的 5 个点5.1 现象客户端一直超时服务端日志显示收到了包原因通常是响应没发回去或者发回去的 Message ID 和请求不匹配。CoAP 规定 ACK 响应的 Message ID 必须和请求一致如果代码里重新生成了 Message ID客户端会认为这不是它的响应而丢弃。解决方法是响应构造时直接复用请求的 Message ID类型设为 ACK。5.2 现象安卓端跑一段时间后服务不可用原因是DatagramSocket在弱网下可能进入异常状态或者接收线程因为未捕获的异常退出了。解决方法是给receiveLoop加 try-catch 并在异常后重建 socket同时用volatile标志位控制循环确保stop()能真正终止线程。5.3 现象Options 解析错位Payload 变成乱码CoAP 的 Option 是 delta 编码如果解析时没按编号升序处理或者 delta 扩展位13/14算错后面所有 Option 都会错位。排查方法是把每个 Option 的编号、长度、值都打出来对照 RFC 7252 的表格逐个核对。常见错误是把 Option Length 的扩展和 delta 的扩展搞混。5.4 现象并发请求下响应串包原因是多个线程共用了同一个DatagramPacket或byte[] buffer。socket.receive会往传入的 packet 里写数据如果多个线程复用同一个 packet数据会互相覆盖。解决方法是每次 receive 都 new 一个 packet 和 buffer或者用线程本地变量。5.5 现象安卓端编译报错找不到java.net.DatagramSocket这通常是因为混淆规则把java.net下的类裁掉了或者 minSdkVersion 设得太低。检查proguard-rules.pro里是否保留了java.net.**以及build.gradle里 minSdk 是否至少 21。另外安卓上 UDP 广播需要CHANGE_WIFI_MULTICAST_STATE权限单播不需要。6. 二次开发把 CoAPService 改造成你自己的设备接入层拆完这份代码如果你要拿它做二次开发我建议从三个方向入手。第一是加 Block-wise Transfer因为现在的实现不支持大报文分块传超过 1KB 的数据会丢RFC 7959 定义了 Block1/Block2 两个 Option实现思路是在CoapResource里加一个分块缓冲区按 Block 编号拼接。第二是加 Observe 机制CoAP 的 ObserveRFC 7641能让客户端订阅资源变化服务端主动推送这对传感器场景特别有用——实现上是在资源里维护一个观察者列表数据变化时遍历列表发 NON 通知。第三是换掉裸DatagramSocket如果你的并发量上来了可以引入 Netty 的NioDatagramChannel把接收循环换成事件驱动吞吐能提升一个量级但要注意 Netty 的 ByteBuf 和现有 byte[] 之间的转换。改造时验证方法很关键。我一般会写一个回环测试客户端发 1000 个 CON 请求统计成功率和平均延迟改之前先跑一遍基线改完再跑一遍对比。另外用 Wireshark 抓包看重传次数如果重传率超过 5%说明超时参数需要调。有个血泪教训我早期改这份代码时为了图快直接在handlePacket里做数据库查询结果一个慢查询把整个接收线程池占满所有请求都超时。从那以后我每次改 CoAP 服务端都强制把业务逻辑和协议处理分开——handlePacket只做解码和分发耗时操作一律丢到独立线程池并且给线程池设一个有界队列和拒绝策略防止雪崩。希望帮到你。本文还有配套的精品资源点击获取
返回列表