简介:CoAPService 是一套基于 Java 实现 CoAP 协议的开源源码工程,适合物联网开发、嵌入式通信及 Android 服务端开发者使用。工程按模块拆分:app 目录对应 Android 服务器端,coaplib 目录包含 Java 服务器代码与 Java 客户端测试代码,可同时覆盖服务端资源发布和客户端请求调试,便于快速集成 CoAP 通信或研究协议细节。整包共 297 个文件,压缩包仅 582KB,以 262 个 Java 源码文件为主,另有 12 个 XML 配置、多份 Gradle 构建脚本及 properties、说明文档等,结构清晰,便于直接导入工程或对照学习。目前已吸引 504 人学习下载。通过这套代码,读者可掌握 CoAP 基于 UDP 的轻量请求/响应模型,理解 GET、POST、PUT、DELETE 对资源的操作方式,以及 /.well-known/core 资源发现机制;Android 端和 Java 服务端的双层事务重传、资源描述等实现也能为自研物联网网关或智能硬件平台提供参考。
1. 当物联网设备调试遇上 UDP:这个 CoAP Java 服务端把服务器搬进了手机
局域网里有几十台传感器,上报用的却是轻量级 CoAP 协议,调试时总不能每回都开一台电脑跑服务。这个取名为 CoAPService 的开源项目,就是干这件事的——它用 Java 实现了 CoAP 协议的服务端核心,同时交付了纯 Java 服务器端和 Android 服务器端两个形态。前者可以部署在任何能跑 JVM 的设备上,后者直接把 CoAP 服务塞进 Android 应用里,手机开机即成为局域网内的 CoAP 节点。适用人群很明确:做物联网网关开发、嵌入式设备联调、或需要在移动端临时搭建 CoAP 测试环境的开发者。项目不依赖重量级框架,核心协议实现是自研的,对想研究 CoAP 协议栈源码的人同样有阅读价值。
2. 架构与设计:一个轻量协议服务端如何拆成 Java 和 Android 两套形态
2.1 技术选型:为什么 CoAP 适合物联网而 HTTP 不合适
CoAP(Constrained Application Protocol)是专门为资源受限设备设计的应用层协议,底层跑在 UDP 上,默认端口 5683。和 HTTP 相比,它把报文头压缩到极小的字节数,支持 GET/POST/PUT/DELETE 四种方法,也定义了类似 HTTP 状态码的响应码。物联网场景里设备内存可能只有几十 KB,跑完整 HTTP 栈不现实,CoAP 就成了事实标准。它的 REST 风格设计意味着,服务端的资源模型可以完全照搬 Web 开发里 URI 映射那套思路。
这个项目最特别的地方在于同时维护了 Java 和 Android 两个服务端入口。两者的核心协议栈是同一套代码,差异只在启动方式和生命周期管理上。纯 Java 版用 main 方法拉起服务,适合部署在树莓派、工控机或者云主机上;Android 版则封装成 Service 组件,跟随应用进程启动,适合做移动巡检工具或临时热点环境下的设备调试。这种「同一核心、双端适配」的结构,避免了维护两套协议实现的痛苦。
2.2 源码结构:core 与 server 分离的分层思路
拿到源码后先看目录结构,能快速理解作者的设计意图。项目采用 Maven 管理,源码基本按三个包组织:
src/main/java ├── com/coap/core // 协议核心:消息编解码、报文收发、资源路由 │ ├── message/ // CoAP 报文模型:Code、Option、Message 等 │ ├── network/ // UDP 收发、消息匹配、重传机制 │ ├── resource/ // 资源定义与路由表 │ └── server/ // 服务端引擎,负责启动和生命周期 ├── com/coap/server/ // Java 标准版入口,含 main 方法 └── com/coap/android/ // Android 版入口,Service 封装与回调接口core 包不依赖任何 Android API,这是关键设计。它只关心协议本身——报文怎么解析、请求怎么路由、响应怎么回。server 和 android 两个包分别做适配层:Java 版读配置文件启动,Android 版通过 Binder 或回调把请求事件抛给应用层。这样划分的好处是,如果想移植到别的平台,比如 iOS 或嵌入式 Java 环境,只需要重新写一个入口适配层,协议核心完全不用动。
2.3 资源模型与路由机制:CoAP 的 URI 映射怎么做
CoAP 的资源模型借鉴了 HTTP 的 URI 概念。服务端启动时注册一组资源路径,客户端请求的 URL 形如coap://192.168.1.100:5683/sensor/temperature,服务端解析出 path 部分,在路由表中查找对应的资源处理器,调用其handleGET、handlePOST等方法。路由表的实现用的是前缀匹配加通配符,支持/sensor/*这样的模式,相比严格相等匹配更灵活。
路由查找失败时返回 4.04 Not Found,这个语义和 HTTP 404 一致。Android 端和 Java 端共用同一套路由注册逻辑,区别仅在于 Java 端可以通过配置文件预置路由,Android 端则需要在代码里显式调用注册方法。资源对象的生命周期由服务端管理,每个请求进来时从资源池里取实例,处理完释放,避免并发下资源共享导致的数据竞争。
3. 跑起来:Java 服务器端与 Android 服务器端的启动全流程
3.1 Java 端启动:从编译到第一个请求
先把项目导入 IDE 或者用命令行编译。项目用 Maven 管理依赖,核心协议层没有第三方库依赖,唯一的外部依赖是 Android 编译需要的 SDK,但那是 android 模块才需要的。执行编译:
mvn clean package -DskipTests编译成功后进入target目录,找到带依赖的 jar 包,执行启动命令:
java -jar coap-server-1.0-jar-with-dependencies.jar默认监听 5683 端口,控制台输出启动日志,说明服务已就绪。如果端口被占用,启动会失败并报Address already in use,这时需要修改端口配置。Java 版支持通过命令行参数覆盖默认端口:
java -jar coap-server-1.0-jar-with-dependencies.jar --port 5684参数解析逻辑在处理入口的 main 方法里,遍历启动参数,识别--port后转换成 int 传入服务配置。除了端口,还支持--host参数指定绑定地址,默认绑定0.0.0.0即所有网卡接口。
3.2 Android 端集成:Service 注册与权限配置
Android 版的使用方式是在自己的应用工程里引入该模块。先在AndroidManifest.xml里声明权限和 Service:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <service android:name="com.coap.android.CoAPService" android:exported="false" />注意INTERNET权限是必须的,漏掉会在运行时抛出SecurityException。ACCESS_WIFI_STATE用于在日志里打印当前 WiFi 连接的 IP 地址,方便客户端拼 URL。Service 设计为前台服务,启动时创建通知栏常驻提醒,避免被系统回收。
启动服务的代码:
Intent intent = new Intent(context, CoAPService.class); intent.putExtra(CoAPService.EXTRA_PORT, 5683); context.startService(intent);EXTRA_PORT是可选的,不传默认 5683。Service 内部在onStartCommand里读取参数,构建服务配置,调用 core 包的服务引擎启动 UDP 监听。
3.3 联调验证:用什么客户端测试
服务启动后需要验证能不能收到请求。Linux 或 macOS 上可以用 libcoap 自带的命令行工具测试:
coap-client -m get coap://127.0.0.1:5683/sensor/temperatureWindows 上可以用 Copper 插件,它是 Firefox 的 CoAP 调试插件,图形界面能看到请求方法和响应码。如果没有现成工具,又不想装插件,也可以写个十几行 Java 代码用 DatagramSocket 手工发包验证,这在调试环境里很实用。第一次跑通时建议先请求根路径/.well-known/core,这是 CoAP 协议定义的服务发现入口,能列出服务端注册的所有资源路径,确认服务端活着且资源表正确。
4. 核心代码拆解:一个 CoAP 请求从 UDP 数据报到业务响应的完整链路
4.1 报文接收与解析:DatagramSocket 之上不做框架
服务端网络层的实现没有引入 Netty 或 Mina 这类重量级框架,而是直接用DatagramSocket配合线程池。这种方式够用且逻辑直观——CoAP 报文结构固定,头部只有 4 字节,解析成本低。核心接收代码如下:
// UDP 接收循环,运行在独立的接收线程中 public void start() throws IOException { socket = new DatagramSocket(serverConfig.getPort()); receiving = true; while (receiving) { byte[] buffer = new byte[1024]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); socket.receive(packet); // 每个请求交给线程池处理,避免阻塞接收循环 executor.execute(() -> handleIncomingPacket(packet)); } } private void handleIncomingPacket(DatagramPacket packet) { byte[] data = Arrays.copyOf(packet.getData(), packet.getLength()); CoAPMessage message = CoAPMessageCodec.decode(data); // 广播给事件监听器,Android 端在此处回调到应用层 dispatcher.dispatch(message, packet.getAddress(), packet.getPort()); }接收缓冲区的初始大小为 1024 字节,这个长度覆盖了绝大多数 CoAP 报文。如果客户端发送的请求超过这个大小,报文会被 UDP 层截断,后续解析就会出错。遇到这种情况,要么让客户端改用分块传输,要么调大缓冲区。
解析后的CoAPMessage对象包含了协议要求的全部字段:版本号、消息类型(CON/NON/ACK/RST)、Token、消息 ID、Method Code、Option 列表和 Payload。CoAPMessageCodec.decode方法负责从字节数组中按 CoAP 规范逐字节提取这些字段,它是对协议格式最直接的落地实现,也是理解 CoAP 报文格式最好的入口。
4.2 消息匹配与确认机制:处理 CON 消息的重传
CoAP 协议中,CON(Confirmable)消息要求接收方必须回 ACK,否则发送方会按指数退避策略重传。这块是协议实现里最容易出错的地方。服务端在收到 CON 请求后,需要立刻回 ACK 表示「我收到了」,然后再执行真正的业务逻辑,最后另发一条响应消息告知处理结果。代码如下:
public void handleRequest(CoAPMessage request, InetAddress clientAddr, int clientPort) { // 请求是 CON 类型,先回 ACK 确认,避免客户端超时重传 if (request.getType() == CoAPMessage.TYPE_CON) { CoAPMessage ack = new CoAPMessage(); ack.setType(CoAPMessage.TYPE_ACK); ack.setMessageId(request.getMessageId()); ack.setCode(CoAPMessage.Code.EMPTY); send(ack, clientAddr, clientPort); } // 业务处理,走资源路由 Resource resource = resourceTree.findResource(request.getUriPath()); CoAPResponse response = resource == null ? buildNotFoundResponse() : resource.handle(request); // 响应同样要带 Token,客户端据此匹配对应的请求 response.setToken(request.getToken()); response.setMessageId(generateMessageId()); // 处理较慢时用 CON 类型响应保证可靠到达 response.setType(CoAPMessage.TYPE_CON); send(response, clientAddr, clientPort); }这里有个细节值得注意:ACK 的 Message ID 必须和原请求一致,这是客户端匹配的基础;响应消息的 Message ID 是新生成的,不能复用 ACK 的 ID。Token 则原样带回,让客户端能把响应关联到具体请求。很多半成品的 CoAP 实现会在这一步翻车,把 Message ID 和 Token 混为一谈,导致客户端收到响应后无法匹配。
重传语义要区分场景。服务端处理时间短时,响应用 NON 消息发出即可,丢了客户端可以重新请求;如果业务处理超过几秒,响应应设为 CON 类型,靠协议层重传保证送达。这个权衡要根据业务实时性要求做,本项目的默认策略是响应一律 CON,保证不丢。
4.3 资源处理器:业务代码的入口定义
资源处理器是本项目留给二次开发的扩展点。一个温度传感器的资源定义如下:
public class TemperatureResource extends CoAPResource { public TemperatureResource() { super("/sensor/temperature"); } @Override public CoAPResponse handleGET(CoAPRequest request) { float temperature = readTemperatureFromSensor(); // 构造 2.05 Content 响应,Content-Format 指明 text/plain CoAPResponse response = new CoAPResponse(); response.setCode(CoAPMessage.Code.CONTENT_2_05); response.setPayload(String.valueOf(temperature).getBytes(StandardCharsets.UTF_8)); response.setOption(CoAPMessage.Option.CONTENT_FORMAT, "text/plain"); return response; } }资源注册分两步:先继承CoAPResource并实现对应方法,再在服务启动时把实例注册到资源树。Java 版和 Android 版注册方式略有不同,Java 版可以在静态初始化块里注册,Android 版建议在onCreate阶段注册,确保 Service 启动后路由表已就绪。
CoAPResource父类默认实现了handle方法做方法分发 —— 根据请求的 Method Code 调用对应的handleGET/handlePOST/handlePUT/handleDELETE。子类只覆写自己关心的方法即可。这层抽象大大降低了新增业务资源的成本,也是我多次二次开发中最顺手的地方。
5. 避坑排查:CoAP 服务端从「跑通」到「跑稳」的 5 个教训
5.1 现象:手机端启动服务后,电脑上的客户端永远收不到响应
原因排查下来大概率是 Android 前后台限制导致 UDP 接收线程被挂起。Android 6.0 以后引入了 Doze 模式,屏幕熄灭或长时间不动会限制网络访问;8.0 之后对后台 Service 的限制更严格。解决方法是把 CoAP 服务做成前台服务并持有唤醒锁,同时引导用户在电池优化白名单里加入本应用。如果只在调试阶段使用,也可以直接插着电源亮屏跑,绕过 Doze 限制。
5.2 现象:报文解析偶发抛异常,且多发生在高频率请求期间
异常堆栈指向CoAPMessageCodec.decode的数组越界,本质是并发问题。接收线程 recv 到的 DatagramPacket 是同一个对象的复用安全,但handleIncomingPacket里packet.getData()拿到的字节数组在多个线程间共享,某线程刚解析到一半,另一个线程的 recv 已经覆写了这块缓冲区。解决方法是每次 recv 后立刻Arrays.copyOf拷贝数据再交给线程池,绝不能让原始 buffer 逃逸出接收线程。这也是我在最初版本上看过的最经典的翻车现场。
5.3 现象:客户端报 4.04 Not Found,但资源路径明明注册过
检查路径是否以/开头且没有尾随空格。路由匹配用的是前缀算法,注册/sensor/temperature后请求/sensor/temperature/也会匹配失败。另一个常见原因是注册时机不对——Android 端在onStartCommand之后才注册资源,此时客户端如果已经发来了请求,自然匹配不到。注册动作务必发生在 UDP 监听启动之前,最简单的方式是在 Service 的onCreate阶段完成。
5.4 现象:同一客户端连续请求,偶发响应错配
这是 Token 生成规则的问题。Token 在协议中用来匹配请求与响应,如果服务端在响应中回填的 Token 不是原请求的 Token,客户端就会判定响应作废。本项目里请求对象本身就携带了原始 Token,处理时应直接引用而非重新生成。我在调试时一度以为是线程池的问题,打印日志后才发现是 Token 被新值覆盖了。牢记规则:ACK 必须带原 Message ID,响应必须带原 Token。
5.5 现象:Java 版服务偶发出现端口占用,重启失败
CoAP 基于 UDP,服务进程异常退出后操作系统释放端口需要时间,如果立即重启可能仍然报Address already in use。这不是代码 bug,而是 UDP 端口的 TIME_WAIT 状态残留。解决方法是重试启动或增加休眠间隔。生产环境建议用守护脚本监控进程,崩溃后延迟 2 秒再拉起。同样的现象如果发生在 Android 端,多见于 Serivce 被系统杀死后快速重建的场景,处理方式相同。
6. 从跑通到改造:把 CoAP 服务端嵌进你的物联网网关项目
这套源码最值得借鉴的是它的 core 包边界克制:协议解析、消息收发、路由分发三块职责清晰,完全独立于 Android API。如果你要把它集成到自己的网关项目里,不需要动 core 包,只需要做两件事:替换资源抽象层和调整生命周期管理。
第一步是扩展CoAPResource接口。原项目里资源处理方法是同步返回CoAPResponse的,但网关场景经常需要把请求转给消息队列再做异步响应。可以新增一个异步版本的接口,把CoAPResource增强为支持CompletableFuture<CoAPResponse>的返回类型,在handle分发时判断返回值类型,异步的情况则用线程池轮询结果后回包。这样既兼容原有同步资源,又不阻塞 UDP 接收线程。
第二步是做鉴权过滤。CoAP 协议本身没有认证能力,需要借助 Option 字段自定义扩展。可以解析客户端请求中携带的自定义 Token Option,匹配资源注册时的权限标识。具体实现时在路由分发前加一层拦截器链,借鉴 Web 框架的 Filter 思想。建议新增ResourceInterceptor接口,服务初始化时注册拦截器列表,请求进入路由前先经过拦截器链,任一拦截器返回 false 则拒绝请求并回 4.01 Unauthorized。
第三步是考虑接入 Observe 机制。CoAP 的 Observe 允许客户端订阅资源变化,服务端在资源变更时主动推送。这是协议层最大的特色功能,也是把服务端从「请求-响应」模式升级到「推送」模式的关键。实现思路是维护一个订阅表,每个条目记录资源路径、客户端地址和最后发送的消息 ID;资源更新时遍历订阅表批量发送 NON 响应,期间注意按规范递增 Observe 序号字段。
做完这三步改造,你的网关节点就具备生产可用的能力了:路由自由扩展、请求有鉴权、资源变化能主动通知。这个项目的源码量不大,很适合一边读一边改,把协议栈吃透。我在移植到自己的模拟项目X时,把 Android 端 Service 生命周期换成 WorkManager 驱动,彻底摆脱了进程被杀的问题——从那以后我每次集成 Android 网络服务,都强制走一遍「前台服务 + 唤醒锁 + 电池白名单」三件套的验证流程,少踩很多坑。希望帮到你。
本文还有配套的精品资源,点击获取