简介:这份资源提供了一套完整的CoAP协议Java实现方案,面向物联网开发者和Java后端工程师,用于搭建支持CoAP协议的服务器与客户端。包内包含Android服务器端代码和Java服务器端代码,并附带Java客户端测试代码,可帮助读者理解CoAP基于UDP的轻量级通信机制、事务重传、资源发现与描述等核心特性。资源共297个文件,以262个Java源码为主,辅以12个XML配置、10张PNG图示、3个properties属性文件及Gradle构建脚本,压缩包约582KB,结构清晰便于按模块查阅。已有504人学习下载,适合需要快速掌握CoAP服务端实现、对照测试客户端进行联调或移植到Android平台的开发者参考。
1. 从一份 Java 版 CoAP 服务端源码说起:它到底能跑在哪些场景
如果你正在做物联网设备接入,大概率绕不开 CoAP 这个协议。它跑在 UDP 上,报文头只有 4 字节,比 HTTP 轻得多,天生适合 NB-IoT、LoRa 这类低带宽、低功耗的链路。但真到动手时,很多人卡在同一个地方:设备端好办,服务端要么用现成的云平台,要么找不到一份能直接读、能改、能塞进自己项目里的 Java 实现。这份 CoAPService 源码包解决的正是这个缺口——它同时给了 Android 端和标准 Java 服务端两套实现,等于把「设备侧怎么发」和「服务端怎么收」都摊开给你看。
它适合三类人:一是要在 Android 网关或工控屏上直接跑一个 CoAP 服务、不想依赖外部进程的开发者;二是需要在 Java 后端里内嵌 CoAP 监听、和现有 Spring 服务打通的工程师;三是想通过读源码搞懂 CoAP 报文结构、重传机制、观察者模式的学生或转行者。源码本身是「系统开源」性质的工程代码,不是玩具 Demo,消息层、请求层、资源管理这几块都有对应类,能直接编译运行,也能按需裁剪。下面我按「先搞懂它怎么组织、再跑起来、最后避坑」的顺序拆一遍。
2. CoAP 协议栈在 Java 里怎么落地:分层结构与核心类职责
2.1 为什么 CoAP 不能照搬 HTTP 的写法
CoAP 虽然借用了 HTTP 的语义(GET/PUT/POST/DELETE、状态码),但传输层是 UDP,这决定了它的实现思路和 HTTP 服务端完全不同。HTTP 服务端可以假设连接是可靠的,读写流就行;CoAP 必须自己处理三件事:报文可能丢失、可能乱序到达、可能重复。所以一份合格的 CoAP 实现里,必然有独立的「消息层」负责重传和去重,「请求/响应层」负责匹配 token,「资源层」负责路由到具体处理逻辑。这份源码的分包结构基本就是按这三层切的,读的时候先找到消息层,其他都好说。
常见做法是把消息层做成一个独立的MessageLayer,里面维护一个「已发送但未确认」的消息表,每条消息带一个 MessageID 和重传计时器。收到 ACK 就移除,超时就按指数退避重发。这个表用ConcurrentHashMap存,key 是 MessageID,value 是消息加时间戳。请求层则靠 token 做匹配,因为一个客户端可能同时发多个请求,服务端返回时得知道对应哪个。
2.2 核心类与职责对照
拿到源码先别急着编译,花十分钟把下面这张表对着目录看一遍,后面调起来心里有底。
| 类/模块 | 所在层 | 主要职责 | 改动频率 |
|---|---|---|---|
| CoapServer | 入口 | 启动 UDP 监听、注册资源、生命周期管理 | 低 |
| MessageLayer | 消息层 | 重传、去重、MessageID 分配 | 低 |
| RequestLayer | 请求层 | token 匹配、请求分发 | 中 |
| CoapResource | 资源层 | 资源树、路径路由、GET/PUT 处理 | 高 |
| CoapExchange | 交互 | 封装单次请求响应上下文 | 中 |
| Android 端 Service | 设备侧 | 后台常驻、收发报文 | 中 |
资源层是你改得最多的地方。每个业务资源继承CoapResource,重写handleGET、handlePUT等方法。比如一个温度上报资源,就在handleGET里返回当前缓存值,在handlePUT里更新。资源树用路径字符串挂载,server.add(new TemperatureResource())之后,客户端请求coap://host/temp就能命中。
2.3 报文编解码的关键参数
CoAP 报文头固定 4 字节:版本(2 bit)、类型(2 bit)、Token 长度(4 bit)、Code(8 bit)、MessageID(16 bit)。类型有四种:CON(需要确认)、NON(不需要确认)、ACK(确认)、RST(复位)。这份源码里编解码通常集中在一个CoapPacket或Message类里,用位运算拼字节。看这段典型写法:
// 构造 CoAP 报文头,4 字节 public byte[] encodeHeader() { byte[] header = new byte[4]; // 版本号固定为 1,占高 2 位;类型占接下来 2 位 header[0] = (byte) ((1 << 6) | (type << 4) | (token.length & 0x0F)); // Code 表示请求方法或响应状态,如 GET=0.01 header[1] = (byte) code; // MessageID 占两个字节,用于去重和匹配 ACK header[2] = (byte) ((messageId >> 8) & 0xFF); header[3] = (byte) (messageId & 0xFF); return header; }这里type取值 0 到 3,分别对应 CON、NON、ACK、RST。token.length最大 8,超过 8 的 token 是非法的,编码时会截断或抛异常,具体看源码里的校验。messageId是 16 位无符号数,从 0 到 65535 循环,服务端和客户端各自维护自己的序列。解码时反过来,先读第一字节的高 2 位判断版本,不是 1 就直接丢弃。这几个参数设错,表现就是对方收不到或一直重传,排查时优先看这里。
3. 把服务端跑起来:编译、注册资源、Android 端联调
3.1 标准 Java 服务端的启动流程
先确认 JDK 版本,源码一般用 JDK 8 或 11 编译,高版本可能有模块化报错。用 Maven 的话直接mvn clean package,没有 pom 就手动javac编译所有源文件。启动入口通常是CoapServer的main或一个start()方法,核心是绑定 UDP 端口,CoAP 默认端口是 5683,安全版 DTLS 是 5684。下面是一个最小启动示例:
public class ServerMain { public static void main(String[] args) throws Exception { // 创建服务端实例,绑定 5683 端口 CoapServer server = new CoapServer(5683); // 注册一个资源,路径为 /temp server.add(new CoapResource("temp") { @Override public void handleGET(CoapExchange exchange) { // 返回当前温度值,2.05 表示 Content,负载为文本 exchange.respond("25.3"); } }); // 启动,进入监听循环 server.start(); System.out.println("CoAP server listening on 5683"); } }CoapServer构造参数是端口,传 0 会随机分配,测试时有用。add方法把资源挂到根路径下,资源名就是 URL 路径段。handleGET里的exchange.respond会自动带上响应码和 token,不用手动拼报文。start()之后线程会阻塞在接收循环,实际项目里一般放到独立线程或交给容器管理。跑起来后用任意 CoAP 客户端(比如命令行工具)发coap://127.0.0.1/temp就能看到返回。
3.2 Android 端作为服务端的特殊处理
Android 上跑 CoAP 服务端和纯 Java 有两个差别:一是网络权限和后台限制,二是电量。源码里的 Android 部分通常是一个Service,在onStartCommand里启动 CoAP 监听。首先在AndroidManifest.xml里加权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />Android 8 以后后台服务受限,必须用前台服务加通知,否则锁屏几分钟就被杀。源码里如果没做前台适配,你得自己补一个startForeground调用,通知渠道在 8.0 以上还要动态创建。另外 UDP 广播在部分定制系统上会被拦截,如果设备发现用的是广播,记得在代码里显式设置setBroadcast(true)并处理NetworkOnMainThreadException——所有网络操作放子线程。
3.3 客户端与服务端的联调步骤
联调时最容易出现「服务端说发了,客户端说没收到」。按下面顺序排查能省不少时间。第一步,确认端口和地址:服务端绑的是0.0.0.0还是具体网卡 IP,客户端连的是不是同一个。第二步,用抓包工具看 UDP 包有没有到,到了但没响应就是解析问题,没到就是网络或防火墙。第三步,检查 MessageID 和 token 是否匹配,服务端回 ACK 时 MessageID 必须和请求一致,响应负载的 token 必须和请求 token 一致。第四步,看重传次数,默认ACK_TIMEOUT是 2 秒,MAX_RETRANSMIT是 4 次,如果日志里疯狂重传,多半是 ACK 没被正确识别。把这四步走完,九成问题能定位。
4. 避坑与排查:这份源码实际用起来会遇到的五个问题
4.1 现象:服务端启动正常,客户端请求超时无响应
原因通常是资源路径没匹配上。CoAP 的路径是大小写敏感的,/Temp和/temp是两个资源。另外资源树挂载时如果父资源没注册,子路径也访问不到。解决方法是打印资源树,确认请求路径逐段都能对上,或者直接在handleGET入口加日志,看有没有进到方法里。
4.2 现象:Android 端跑一会儿就收不到包了
这是后台限制导致的,不是代码 bug。Android 会冻结后台进程的 UDP socket。解决办法是改成前台服务,带一个常驻通知,并在onDestroy里正确释放 socket。如果设备是定制的工控机,可以在系统设置里把应用加入电池优化白名单,但通用做法还是前台服务。
4.3 现象:大负载响应被截断
CoAP 单包默认限制在 1152 字节左右,超过就要分块(Block-Wise Transfer)。这份源码如果没实现 Block1/Block2 选项,返回大 JSON 时对端只能收到前一段。解决方法是控制单次响应体积,或者自己补上 Block 选项的处理逻辑,按 1024 字节分片传输。
4.4 现象:并发请求下响应串了
这是 token 或 MessageID 复用导致的。如果服务端用固定 token 回复所有请求,客户端就分不清哪个响应对应哪个请求。检查RequestLayer里 token 的生成逻辑,确保每个请求的 token 唯一,响应时原样带回。MessageID 同理,同一时间窗口内不能重复。
4.5 现象:DTLS 握手失败
如果用了 5684 端口和 DTLS,握手失败多半是证书或密码套件不匹配。先确认双方支持的 cipher suite 有交集,再检查证书链是否完整。调试阶段可以临时用NON类型和明文端口排除加密因素,确认业务逻辑没问题后再开 DTLS。
5. 进阶用法:把 CoAP 服务端嵌进现有 Java 后端并做可观测
5.1 与 Spring 容器共存的线程模型
很多项目不想单独起一个 CoAP 进程,而是希望它和 Spring Boot 应用一起跑。做法是把CoapServer的启动放到一个@Bean的initMethod里,销毁时调stop()。注意 CoAP 的接收循环是阻塞的,必须放在独立线程池,不能占用 Web 容器的线程。我一般会用一个单线程ExecutorService专门跑server.start(),再暴露一个/coap/status的 HTTP 接口返回当前连接数和消息统计。这样运维能通过现有监控看到 CoAP 侧的状态,不用另开一套。
5.2 观察者模式与资源状态推送
CoAP 的 Observe 选项允许客户端订阅资源变化,服务端在值改变时主动推送。源码里如果实现了ObservableResource,用法是客户端请求时带 Observe=0,服务端记住这个客户端,之后每次changed()就推一条通知。这里有个坑:通知的 MessageID 要递增,token 要和订阅时一致,否则客户端会当成新响应。推送频率也要控制,低功耗设备经不起高频推送,我一般加一个最小间隔,比如 5 秒内同一资源只推一次。
5.3 用日志和计数器验证服务端行为
光看代码不放心,得让服务端自己「说话」。在消息层加两个AtomicLong,一个记收到的报文数,一个记发出的响应数,每隔 30 秒打一行日志。再在资源层每个handle方法入口打 debug 日志,带上 token 和来源地址。这样联调时一眼就能看出是没收到、收到了没处理、还是处理了没发出去。下面是一个简单的统计埋点:
// 消息层统计,用于观测服务端吞吐 private final AtomicLong receivedCount = new AtomicLong(); private final AtomicLong sentCount = new AtomicLong(); public void onReceive(byte[] data, InetSocketAddress source) { receivedCount.incrementAndGet(); // 解析并分发,异常时记录来源便于排查 try { dispatch(data, source); } catch (Exception e) { System.err.println("parse fail from " + source + ": " + e.getMessage()); } } public void send(byte[] data, InetSocketAddress target) { sentCount.incrementAndGet(); socket.send(new DatagramPacket(data, data.length, target)); }receivedCount和sentCount的差值能反映积压情况,如果收得多发得少,说明处理逻辑卡住了。来源地址打出来,能快速判断是哪个设备在刷。这套埋点不依赖任何外部库,加完重启就能用。
从那以后我每次拿到一份网络协议源码,都先不急着改业务,而是把收发计数和关键路径日志加上,跑通一条最小链路再动别的。这份 CoAPService 源码结构清晰,Android 和 Java 两端都有,适合拿来当起点而不是终点。希望帮到你。
本文还有配套的精品资源,点击获取