简介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 不合适CoAPConstrained 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里声明权限和 Serviceuses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / service android:namecom.coap.android.CoAPService android:exportedfalse /注意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 协议中CONConfirmable消息要求接收方必须回 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增强为支持CompletableFutureCoAPResponse的返回类型在handle分发时判断返回值类型异步的情况则用线程池轮询结果后回包。这样既兼容原有同步资源又不阻塞 UDP 接收线程。第二步是做鉴权过滤。CoAP 协议本身没有认证能力需要借助 Option 字段自定义扩展。可以解析客户端请求中携带的自定义 Token Option匹配资源注册时的权限标识。具体实现时在路由分发前加一层拦截器链借鉴 Web 框架的 Filter 思想。建议新增ResourceInterceptor接口服务初始化时注册拦截器列表请求进入路由前先经过拦截器链任一拦截器返回 false 则拒绝请求并回 4.01 Unauthorized。第三步是考虑接入 Observe 机制。CoAP 的 Observe 允许客户端订阅资源变化服务端在资源变更时主动推送。这是协议层最大的特色功能也是把服务端从「请求-响应」模式升级到「推送」模式的关键。实现思路是维护一个订阅表每个条目记录资源路径、客户端地址和最后发送的消息 ID资源更新时遍历订阅表批量发送 NON 响应期间注意按规范递增 Observe 序号字段。做完这三步改造你的网关节点就具备生产可用的能力了路由自由扩展、请求有鉴权、资源变化能主动通知。这个项目的源码量不大很适合一边读一边改把协议栈吃透。我在移植到自己的模拟项目X时把 Android 端 Service 生命周期换成 WorkManager 驱动彻底摆脱了进程被杀的问题——从那以后我每次集成 Android 网络服务都强制走一遍「前台服务 唤醒锁 电池白名单」三件套的验证流程少踩很多坑。希望帮到你。本文还有配套的精品资源点击获取