简介:面向物联网与Android开发者的CoAP协议Java实现源码包,聚焦轻量级UDP通信场景下的服务端搭建与客户端联调。包内共297个文件,以262个java源文件为核心,辅以12个xml配置、3个properties与3个gradle构建脚本,另有少量png图示与jar依赖,压缩包约582KB,体量轻便易于导入。内容分为app与coaplib两部分:app为Android服务器端代码,coaplib则包含Java服务器实现及Java客户端测试代码,可对照验证请求响应流程。源码围绕CoAP双层事务机制展开,涉及资源URI寻址、/.well-known/core资源发现、按rt类型筛选查询,以及GET、POST、PUT、DELETE操作方法,并体现基于UDP的重传与载荷压缩思路。已有506人学习,适合需要理解CoAP服务端结构、进行协议对接或二次开发的读者参考。
1. 一套能跑通 CoAP 服务端的 Java 源码,到底解决了谁的燃眉之急
做物联网接入的同行大概都遇到过这种局面:设备端用的是 UDP 上的 CoAP,功耗低、报文小、天生适合受限网络,可一到服务端就卡壳——现成的框架要么绑死在某套云平台里,要么只给了个客户端 Demo,服务端那半截得自己从 RFC 7252 一行行啃。这套 CoAPService 源码包就是冲着这个缺口来的:它同时给了 Android 端和纯 Java 端的 CoAP 服务器实现,把资源注册、请求分发、消息编解码这些脏活都铺好了,你拿到手能直接起一个能被真实设备打通的 CoAP 服务。适合谁?一类是手里有 NB-IoT、传感器节点要对接、又不想被商业 IoT 平台绑住的后端;另一类是需要在 Android 设备上做本地网关、让手机自己当 CoAP 服务端接周边节点的嵌入式方向开发者。系统开源这件事在这里的意义不是口号,而是你能顺着源码把 CoAP 的 Confirmable 消息重传、Token 匹配这些黑匣子拆开看,改起来不用等谁发版。
2. CoAP 服务端的骨架:从消息层到资源树怎么搭
CoAP 不是「HTTP 换了个 UDP 传输」这么简单,它的消息模型和资源模型是两套东西,源码包把这两层分得比较清楚,先把这个结构吃透,后面改代码才不会乱。
2.1 消息层:Confirmable、Non-confirmable 与 Token 匹配
CoAP 跑在 UDP 上,UDP 本身不保证送达,所以协议自己在消息头里塞了 Type 字段来区分四种消息:CON(Confirmable,需要 ACK)、NON(Non-confirmable,不需要 ACK)、ACK(确认)、RST(复位)。服务端收到一个 CON 请求,处理完必须回一个 ACK,而且响应可以捎带在 ACK 里(piggybacked response),也可以先回一个空的 ACK 再单独发响应(separate response)。这套源码里消息编解码的核心就在处理这个分支,你翻Message相关的类会看到 Type、Code、Message ID、Token 四个字段被反复读写。
Token 是另一条容易翻车的线。客户端发请求时带一个 Token,服务端响应必须原样带回同一个 Token,客户端靠它把响应和请求对上号——因为 UDP 上多个请求可能乱序到达,光靠 Message ID 不够。源码里维护了一张 Token 到待处理请求的映射表,请求进来先登记,响应发出时按 Token 取回上下文。我一般会重点看这张表的清理逻辑,如果只在响应时删、异常路径没删,跑久了就是内存泄漏。
// 消息类型与响应模式的核心分支(示意,按源码结构整理) switch (request.getType()) { case CON: // 可确认消息:必须回 ACK,响应可捎带 if (canRespondImmediately(request)) { response.setType(Type.ACK); // piggybacked response.setMessageId(request.getMessageId()); } else { sendEmptyAck(request); // 先确认,避免客户端重传 response.setType(Type.CON); // separate response response.setMessageId(nextMessageId()); } break; case NON: // 不可确认消息:直接回 NON 响应,不占用 ACK response.setType(Type.NON); response.setMessageId(nextMessageId()); break; default: // ACK / RST 属于响应或复位,不进业务分发 return; } response.setToken(request.getToken()); // Token 必须原样带回这段逻辑说明三件事:第一,CON 请求的响应模式取决于你能不能立刻给出结果,能就捎带、不能就先 ACK 再单发,源码里这个判断通常和资源处理是否阻塞挂钩;第二,NON 请求不要傻乎乎回 ACK,回了反而违规;第三,Token 的透传是硬约束,任何一条响应路径漏了它,客户端就会认为超时。参数上要盯的是 Message ID 的分配,它只有 16 位,回绕很快,源码里一般用一个自增计数器加掩码处理,你改并发逻辑时别把它改成随机数,否则重传去重会失效。
2.2 资源层:把 URI 路径映射成可处理对象
CoAP 的资源模型是「URI 路径 → 资源对象」,跟 REST 的思路接近,但注册方式是显式的。源码里通常有一个资源容器,启动时把/sensors/temp、/actuators/led这类路径和对应的处理类注册进去,请求进来后按 Uri-Path 选项逐段匹配。这里的关键是 Uri-Path 在 CoAP 里是分段编码的选项(Option Number 11),不是一整条字符串,所以匹配逻辑要按段拼。
// 资源注册与路径匹配(示意) CoapServer server = new CoapServer(5683); // 默认 CoAP 端口 server.add(new CoapResource("sensors") { @Override public void handleGET(CoapExchange exchange) { // 读取传感器当前值,返回 JSON 或纯文本 exchange.respond(CoapResponseCode.CONTENT, readSensorPayload()); } @Override public void handlePOST(CoapExchange exchange) { // 接收上报数据,注意 payload 大小受 UDP MTU 限制 byte[] body = exchange.getRequestPayload(); storeReading(body); exchange.respond(CoapResponseCode.CHANGED); } }); server.start();注册这段代码看着简单,但有两个参数要心里有数。端口 5683 是 CoAP 的默认端口,DTLS 加密走 5684,源码包如果没带 DTLS 就别硬套。另一个是 payload 大小,CoAP 单包受 UDP MTU 约束,实际可用通常压在 1024 字节以内,超过就得走 Block-Wise Transfer(Option 23/27),源码里如果没实现分块,你传大 JSON 会直接截断或丢包,这是后面避坑章要展开的点。资源树的层级也别铺太深,Uri-Path 每多一段就多一个选项,报文头会膨胀,受限设备上不划算。
2.3 Android 端与纯 Java 端的差异点
这套源码同时给了 Android 和 Java 两个服务端,别以为只是换个工程结构。Android 端的坑集中在网络权限和后台限制:CoAP 走 UDP,AndroidManifest.xml里要声明INTERNET权限,Android 9 以后明文流量策略对 UDP 影响不大,但后台服务被系统限制唤醒是实打实的,设备连着但服务端收不到包,先查是不是进程被挂起了。纯 Java 端反而干净,跑在服务器上就是普通 Socket 编程,重点在端口占用和防火墙放行 UDP 5683。
<!-- Android 端必须声明的权限 --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />纯 Java 端起服务前先确认端口没被占,netstat -anu | grep 5683看一眼,UDP 端口冲突不像 TCP 那样立刻报错,经常是绑上了但收不到包,排查起来很费劲。两端共用的消息编解码和资源容器代码建议抽成独立模块,Android 工程和 Java 工程各自引一份,别复制粘贴两套,改一处漏一处是血泪经验。
3. 把服务端跑起来:编译、配置与一次完整请求验证
源码拿到手,第一步不是急着改业务,而是先让它按原样跑通一次请求-响应闭环,确认环境没问题再动刀。
3.1 编译与依赖梳理
这类 CoAP 源码包常见依赖是 slf4j 做日志、junit 做测试,Android 端还会带 support 或 androidx 库。先看构建文件确认依赖版本,Maven 工程直接mvn clean package -DskipTests先出包,Gradle 工程用./gradlew assemble。如果源码里依赖了某个特定版本的 CoAP 库(比如某个 californium 版本),别随手升级到最新,消息 API 在不同大版本间改过签名,升了编译不过还算好的,编译过但行为变了才难查。
# Maven 工程先出包,跳过测试快速验证编译 mvn clean package -DskipTests # 确认产物 ls target/*.jar # 纯 Java 端启动(端口按源码默认或自行指定) java -jar target/coap-server.jar --port 5683启动参数里端口是最常改的,测试环境多实例并存时给每个实例分不同端口。日志级别建议先开到 DEBUG,CoAP 的收发报文在 DEBUG 下能看到 Type、Code、Message ID,第一次联调全靠它定位是没收到还是收到了没回。
3.2 用客户端发一次 GET/POST 验证闭环
服务端起来后,别用自己写的客户端验证,先用成熟的命令行工具打一发,排除客户端自身 bug 的干扰。常见做法是用coap-client这类工具,GET 一个已注册资源看返回码是不是 2.05 Content,POST 一条数据看是不是 2.04 Changed。
# GET 请求,观察返回码与 payload coap-client -m get coap://127.0.0.1:5683/sensors/temp # POST 上报一条数据 coap-client -m post -e '{"v":23.5}' coap://127.0.0.1:5683/sensors/temp # 带 Confirmable 标志,验证 ACK 与重传路径 coap-client -m get -N coap://127.0.0.1:5683/sensors/temp返回码要对得上:GET 成功是 2.05,POST 成功是 2.04,资源不存在是 4.04,方法不支持是 4.05。如果客户端一直等到超时,先看服务端 DEBUG 日志有没有收到报文——收到了但没回,问题在资源匹配或响应构造;压根没收到,问题在绑定地址或防火墙。-N参数控制是否用 NON 消息,用它对比 CON 和 NON 两条路径的响应行为,能快速验证 2.1 节讲的消息分支有没有写对。
3.3 观察者模式(Observe)的注册与通知
CoAP 的 Observe(RFC 7641)是它相对 HTTP 的一大优势:客户端注册一次,资源变化时服务端主动推。源码里如果实现了 Observe,资源对象上会维护一个观察者列表,GET 请求带 Observe 选项(Option 6,值 0 表示注册)时把客户端加进去,资源更新时遍历列表逐个通知。
// 观察者注册与通知(示意) public void handleGET(CoapExchange exchange) { if (exchange.getRequestOptions().hasObserve()) { // 客户端注册观察,加入通知列表 observers.add(exchange.advanced().getRequest().getSourceContext()); exchange.respond(CoapResponseCode.CONTENT, currentPayload()); } else { exchange.respond(CoapResponseCode.CONTENT, currentPayload()); } } // 资源变化时触发 public void notifyObservers() { for (Endpoint ep : observers) { // 通知消息带 Observe 选项,序号递增用于客户端判断新旧 sendNotification(ep, currentPayload(), nextObserveSeq()); } }这里有个容易忽略的参数:Observe 序号。每次通知要带一个递增的序号,客户端靠它判断这条通知是不是比上一条新,序号回绕时按 RFC 的规则比较。源码里如果序号写死成 0,客户端可能把新通知当旧消息丢掉,表现就是「数据变了但界面不刷新」,这种玄学问题查起来很折磨。观察者列表的清理也要做,客户端发 RST 或者长时间不响应就该摘掉,不然列表只增不减。
4. 踩坑记录:UDP 上跑服务端最容易翻车的四个地方
CoAP 服务端在本地跑通不代表能上生产,下面这几条是我在类似源码上反复撞过的墙,按「现象 → 原因 → 解决」摆出来。
4.1 客户端一直超时,服务端日志却显示已响应
现象:客户端等不到响应,服务端 DEBUG 日志里明明有「send response」的记录。原因多半是响应发到了错误的源地址,或者 NAT 环境下服务端绑的是内网地址、回包出不去。UDP 是无连接的,服务端回包靠的是请求报文里的源 IP 和端口,如果代码里把响应目标写成了配置里的固定地址而不是请求的 SourceContext,回包就飞了。解决:响应一律用请求携带的源上下文回,别自己拼地址;跨网段部署时确认服务端绑的是0.0.0.0而不是某个具体网卡地址。
4.2 大 payload 被截断或直接丢包
现象:POST 一个稍大的 JSON,服务端收到的内容不完整,或者客户端报错。原因是没有实现 Block-Wise Transfer,CoAP 单包受 UDP MTU 限制,超过约 1024 字节就得靠 Block1/Block2 选项分块传输。源码里如果只处理了单包,大报文必然出问题。解决:要么在应用层约束 payload 大小、把大数据拆成多次小请求,要么补上分块逻辑——先确认源码有没有相关选项处理,没有的话这是要自己填的坑,别指望它开箱即用。
4.3 Android 端后台收不到包
现象:Android 设备当前台时一切正常,切后台或锁屏一段时间后 CoAP 服务端不再响应。原因是系统对后台进程的网络和 CPU 做了限制,UDP socket 还在但进程被挂起,包到了没人处理。解决:用前台服务(Foreground Service)把 CoAP 服务端挂住,配常驻通知;同时确认没有开启省电模式把网络掐了。这不是代码 bug,是平台行为,改代码解决不了,得从服务生命周期入手。
4.4 重传导致业务被重复执行
现象:网络抖动时,同一条 POST 数据被处理了两次,数据库里出现重复记录。原因是客户端没收到 ACK 会重传 CON 消息,服务端如果只按 Message ID 去重但去重表清理太早,重传包会被当新请求再处理一遍。解决:服务端维护一个短时的 Message ID 去重窗口,窗口内收到相同 Message ID 直接回上次的响应而不重新执行业务;窗口大小按客户端的重传间隔来定,太短挡不住重传,太长占内存。业务层最好也做幂等,双保险。
5. 从能跑到好用:资源热更新与一次压测验证
源码跑通、坑填完,接下来是让它经得起真实设备量。这里分享两个我每次落地都会走的动作,一个改资源注册方式,一个做最小压测。
先说资源热更新。硬编码server.add(new CoapResource(...))在资源少的时候没问题,设备类型一多,每加一种传感器就改代码重新发版,不现实。我一般会把资源路径和处理类的映射抽到配置里,启动时反射加载,新增资源只改配置不动主流程。
// 配置驱动的资源注册(示意) Properties props = loadConfig("coap-resources.properties"); for (String path : props.stringPropertyNames()) { String className = props.getProperty(path); // 反射实例化处理类,统一实现 CoapHandler 接口 CoapHandler handler = (CoapHandler) Class.forName(className) .getDeclaredConstructor().newInstance(); server.add(new DynamicResource(path, handler)); }配置里一行sensors/temp=com.example.TempHandler就挂一个资源,处理类实现统一接口,GET/POST 各自覆写。这样设备协议扩展时只加类和配置,服务端重启即生效。参数上注意路径别带前导斜杠,Uri-Path 分段时容易多出一个空段导致匹配不上,这个细节翻车过不止一次。
再说压测。CoAP 服务端的瓶颈往往不在业务逻辑,而在 UDP 收包和 Token 表并发。最小验证不用上专业工具,写个循环发 CON 请求的脚本,逐步加并发看响应率和延迟。
| 并发数 | 请求总数 | 预期响应率 | 观察重点 |
|---|---|---|---|
| 10 | 1000 | 接近 100% | 基线延迟、有无丢包 |
| 50 | 5000 | 95% 以上 | Token 表是否膨胀、GC 频率 |
| 100 | 10000 | 看衰减拐点 | 重传率、线程池是否打满 |
跑的时候盯两个指标:响应率随并发上升的衰减拐点在哪,以及 Token 表大小是否随请求数线性增长不回落。后者不回落基本就是 2.1 节说的清理逻辑有漏,跑得越久内存越吃紧。压测脚本里请求要带不同 Token,别复用同一个,否则测不出 Token 匹配的真实开销。
从那以后我每次接一套新的 CoAP 服务端源码,都强制先跑一遍「单请求闭环 → 大 payload → 重传去重 → 最小压测」这四步,任何一步没过就不往下写业务。这套 CoAPService 源码的价值在于它把消息层和资源层的骨架给全了,省掉的是从 RFC 啃起的时间,剩下分块传输、幂等、热更新这些工程化的事,还是得自己补。希望帮到你。
本文还有配套的精品资源,点击获取