简介:CoAPService 是一套基于 Java 的 CoAP 协议实现,涵盖 Android 服务器端代码、Java 服务器端代码及 Java 客户端测试代码,适合物联网开发者、嵌入式通信学习者和需要搭建轻量级 RESTful 服务的技术人员参考。源码结合协议要点展开,覆盖 CoAP 基于 UDP 的双层传输结构、资源发现(/.well-known/core)、资源类型筛选以及 GET/POST/PUT/DELETE 操作映射,可帮助理解该协议与 HTTP 的异同及实际落地方式。资源共 297 个文件,以 262 个 Java 源码文件为核心,辅以 Android 布局 XML、图片资源、Gradle 构建脚本和 Properties 配置等,压缩包整体约 582KB,目录分层明确,便于按模块阅读与复用。目前已有 503 人学习下载,适合作为 CoAP 从入门到服务端实践的参考素材。
1. CoAPService 是拿来干嘛的:一块 Java 与 Android 共用的 CoAP 服务器端源码
一台闲置的 Android 手机,不需要改造硬件,就能当 CoAP 设备服务器端用;一台普通云主机,也能把已有业务逻辑通过 CoAP 端口暴露给内网设备。标题里的 CoAPService,就是这类源码的常见落地形态:一套 Java 实现的 CoAP 服务器端,分别适配 Android 环境和标准 JVM。它解决的是设备侧资源受限、HTTP 太重、而手头只有 Java 技术栈的场景。适合三批人:Android 开发者要模拟智能家居设备,IoT 后端想把管理面暴露给内网设备,以及正在啃 Java 面试八股文、想搞懂 CoAP 与 MQTT 该选谁的人。下面从协议层一路拆到能照着跑的代码。
2. CoAP 服务端的技术选型:消息模型、资源路由和一张可挂接口的源码骨架
2.1 CoAP 与 HTTP 的选型差异:UDP、请求类型和观察模式
先给结论:CoAP 是给受限设备准备的 HTTP 替代品,数据面走 UDP,控制面沿用 REST 语义。为什么选 CoAP 而不是直接把 HTTP 压成二进制?因为 UDP 的握手开销低,CoAP 头部最小只有 4 字节,一条消息能塞进单个数据报,几十 KB 内存的 MCU 也跑得动。落到 Java 服务器端时,UDP 还意味着你不需要像维护 Tomcat 那样维护大表 TCP 连接,资源占用轻很多。真正的难点从 TCP 的内核可靠交付,变成了协议栈层面的重传与超时,这也是这种源码里最值得读的部分。
CoAP 消息类型分为四类:CON(需要确认)、NON(无需确认)、ACK(确认响应)、RST(复位)。一个 CON 请求发出后,发送方在 ackTimeout 内等 ACK;超时未到就做指数退避重传,默认最多重传 4 次。很多 Java 八股文喜欢把 CoAP 简单说成“UDP + REST”,可一旦你真要用 DatagramSocket 实现一个服务器端,重传、去重、乱序全都得自己面对。好消息是 CoAPService 这类实现通常已经把这一层封装在 message 包里,你写业务资源时基本碰不到重传逻辑。
另一个和 HTTP 差异很大的机制是观察模式(Observe)。客户端用 GET 加上 Observe 选项订阅资源,服务器端在数据变化时主动推送通知,而不是让客户端反复轮询。这对传感器上报、开关状态同步特别有价值,后面验证章节会回到它的调用方式。
2.2 阅读 CoAPService 源码时的目录结构:transport、message、resource 分工
拿到这类源码包,先别急着翻类,先找入口。常见入口是CoapServer或MainActivity之类的类。我一般按三层结构去认:传输层、消息层、资源层。
CoAPService/ ├── transport/ # UDP 收发、连接与线程管理 ├── message/ # CoAP Message、Option、Codec 编解码 ├── resource/ # 资源注册、请求路由与回调 ├── server/ # CoapServer 生命周期与启动入口 └── android/ # Android Service/Activity 封装,仅 Android 版有transport 包最容易踩坑。Android 端有没有多网卡、UDP 是不是被系统切了、回包从哪个 socket 发出,都在这一层成问题。resource 包则是业务开发最常动的地方,下面的资源骨架代表了最小可注册单元:
public class HelloResource extends CoapResource { public HelloResource(String name) { super(name); setObservable(true); } @Override public void handleGET(CoapExchange exchange) { exchange.respond("hello from CoAPService"); } @Override public void handlePOST(CoapExchange exchange) { String payload = exchange.getRequestText(); exchange.respond("received:" + payload); } }这段代码的意图是暴露一个/hello资源。构造参数name会拼进资源路径;重写handleGET和handlePOST分别处理读和写。注意setObservable(true),它让该资源支持观察订阅,业务状态变化时主动调用changed()即可通知订阅端。
至于服务器端的启动,下面三行是 Java 端的通用骨架:
CoapServer server = new CoapServer(); server.add(new HelloResource("hello")); server.start();这里new CoapServer()会监听默认的 5683 端口,add方法完成资源挂载,start打开 UDP socket。一个最小闭环到这里就跑通了,客户端访问地址是coap://<host>:5683/hello。
如果要把这套源码当成课程设计或面试素材,协议选项的完整性比功能数量更值得看。我做了个自检表,你在 review 代码时也可以对照:
| CoAP 选项 | 用途 | 实现不完整时的表现 |
|---|---|---|
| Content-Format | 描述 payload 类型 | 客户端拿到乱码或直接丢弃响应 |
| Uri-Path / Uri-Query | 请求路由 | 路径参数丢失,资源匹配失败 |
| Block1 / Block2 | 大块数据分包传输 | 响应超过 UDP MTU 后被静默丢弃 |
| Observe | 订阅资源变化 | 客户端订阅后收不到任何推送 |
前面这些只是认知地基。真正的差异在实现方式上:现成源码往往有两种,一种协议层是自己用DatagramSocket从零写的,另一种是基于 Eclipse Californium 这类库做的业务封装。第一种读起来更像“coap 的 java 源码”,适合学习和面试讲原理;第二种更接近生产可用,后续换 DTLS 也省事。选择哪种不冲突,关键是你得知道参数位在哪、消息编解码在哪,不然调试时只能当黑匣子使。
3. 跑通 Java 服务器端:CoAPService 在普通 JVM 上的最小落地
3.1 Gradle 依赖:用现成库还是直接改源码?
在普通 JVM 上跑 CoAPService,第一步是确认 JDK 可用。这里谈的不是具体版本,而是JAVA_HOME环境变量配置。很多人在 Android Studio 里跑惯了,切回命令行就翻车:./gradlew找不到 JDK,或者 Java 与 Gradle 版本不匹配。我的习惯是先在终端里执行java -version,把类路径和构建工具的问题排除在业务代码之外。
如果项目源码用的是现成 CoAP 库,它的 Gradle 依赖块通常是这个形态:
plugins { id 'java' id 'application' } repositories { mavenCentral() } dependencies { implementation 'org.eclipse.californium:californium-core:3.x' // 需要安全传输时再引入 scandium implementation 'org.eclipse.californium:scandium:3.x' } application { mainClass = 'com.example.CoapLauncher' }这里的逻辑是:当源码自带完整协议栈时,可以完全去掉外部依赖;当源码只是业务壳时,californium-core负责 UDP 收发、消息编解码和重传,业务代码只管资源回调。版本选择上别追新,JDK 17 用 3.x,JDK 8 用 2.x,不然会出现 class version 错误。这类问题不是你代码写错,是工具链不一致。
配置好后,执行命令也简单:
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew run注意JAVA_HOME要指向实际安装路径。gradlew脚本只会读JAVA_HOME,不会自动发现 SDK 目录里的 JDK。Android Studio 自带 JBR,和命令行是两套环境,这是很多人本地跑不通的直接原因。
3.2 最小服务代码:端口、线程池和资源注册
下面的代码是 CoAPService 在 Java 服务器端最典型的主入口:
import org.eclipse.californium.core.CoapServer; import java.util.concurrent.Executors; public class CoapLauncher { public static void main(String[] args) { CoapServer server = new CoapServer(5683); server.setExecutors( Executors.newScheduledThreadPool(2), Executors.newFixedThreadPool(8) ); server.add(new HelloResource("hello")); server.start(); } }new CoapServer(5683)指定 UDP 监听端口。如果这里不写,默认也是 5683,但显式写出来会在排查网络问题时少一层猜测。setExecutors接受两个线程池,第一个用于调度重传定时任务,第二个是执行业务回调的 worker 线程。worker 数量直接决定接口并发上限,一旦某个资源里出现阻塞操作,8 个线程被占满,后面的请求就会全部堆积等待,最后表现为 CON 超时重传。
资源注册就是server.add(new HelloResource("hello"))。这里 “hello” 是资源名,也是请求路径的最后一段。服务器启动后,用任意 CoAP 客户端访问coap://127.0.0.1:5683/hello就能收到文本响应。
换到生产环境时,最需要调的是下面几个参数。它们不在同一个类里,但都属于“服务器端调参”的范畴:
| 参数 | 默认值 | 建议与说明 |
|---|---|---|
| 端口 | 5683 | 冲突时改 56830,但客户端也要同步改 |
| ackTimeout | 2000ms | 跨网段延迟大时调大,减少假重传 |
| worker 线程数 | 随系统配置 | 资源里有阻塞 IO 时至少翻一倍 |
| 资源路径 | 无 | 统一小写,避免跨端大小写不一致 |
这里分享一个经验:很多人会在资源回调里直接访问数据库,数据库慢一次,CoAP 重传就会把同一个请求再打进来。所以我写资源时都让回调尽快返回,最多把一个任务放进消息队列再去处理。这不是 CoAP 特有的问题,但 UDP 没有内核缓冲帮你平滑请求,worker 线程一满,问题会更快暴露出来。
4. 把 CoAP 服务搬到 Android:CoAPService 的 Android 服务器端适配
4.1 权限与线程限制:Android 上启动 CoAP 服务器的两个约束
从普通 Java 端转到 Android 端,CoAP 协议代码可以原样复用,但运行环境差异很大。Android 的 Activity 不是程序入口,而是一个生命周期组件。如果你在onCreate里直接 new 一个 CoapServer,Activity 一旦被系统回收,服务就没了。标准做法是把服务器端挂在一个 Service 上。
先处理权限。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" />INTERNET不配置,UDP 数据包根本发不出去,这是最常见的“配了权限还是没网”的根因。CoAP 走 UDP,在 Android 上不会额外限制协议本身,所以别在权限上多花心思。真正要紧的是线程:Android 不允许在主线程里执行网络操作,否则抛NetworkOnMainThreadException。不管你是用 DatagramSocket 自己写,还是用现成库,启动服务器的过程都必须放到子线程。
下面是最小的 Android Service 封装:
public class CoapService extends Service { private CoapServer server; @Override public void onCreate() { super.onCreate(); new Thread(() -> { server = new CoapServer(5683); server.add(new HelloResource("hello")); server.start(); }).start(); } @Override public void onDestroy() { if (server != null) { server.destroy(); } super.onDestroy(); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } }这里把服务器启动包进new Thread,避免阻塞onCreate。onDestroy里调用server.destroy()关闭 DatagramSocket,这是给服务端一个“后悔药”,否则下次启动会报端口被占用。如果只是 Demo,Service 的onStartCommand里启动也一样,但要注意清理路径必须一致。
在 Android Studio 里跑真机调试时还有个细节:模拟器里10.0.2.2指向宿主机,但真机上的 CoAP 客户端和服务器端同网段时,直接用局域网 IP 即可。Android 12 以上如果跑前台服务,还要在通知栏发一条前台服务通知,这部分因厂商定制差异大,建议在清单文件里声明android:foregroundServiceType,按官方模板做。
4.2 锁屏、后台休眠与进程回收:让 Android 服务器端能扛一会儿
Android 的进程回收策略不看你的 UDP 绑定,锁屏或者内存不足时系统照样回收进程。要让 CoAP 服务器端活得更久,通用做法是使用前台服务并申请 WakeLock。一个最小实现是在 Service 的onStartCommand里执行:
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "coap:server"); wakeLock.setReferenceCounted(false); wakeLock.acquire();PARTIAL_WAKE_LOCK能让 CPU 保持运行,从而保证 UDP socket 还能被内核唤醒。但它同时是耗电大户,用完必须释放。WiFi 锁同理,WifiManager.createWifiLock()能在屏幕关闭后维持 WiFi 连接,适合局域网调试场景。不过别把这些锁当成银弹,很多国产 ROM 的省电策略连前台服务都杀,你唯一能做的就是让服务被杀后能自动恢复。
恢复机制常见做法是粘性 Service 加网络变化监听。START_STICKY让系统在内存充足时重启服务,网络切换时通过ConnectivityManager.NetworkCallback判断当前网络是否可访问,监听可用后再拉起 CoapServer。我通常会做一个心跳通道,由外部客户端周期性探测端口,探测失败就告知用户服务不可用,而不是假装 Android 端能提供“真·7x24 小时”服务。
这部分要说句实在话:用 Android 做 CoAP 服务器端,适合开发调试、脱机演示和测评环境。真要拿来做生产设备接入,老老实实换 Linux 板或 ESP32。Android 系统限制很麻烦,但这不是写代码能完全绕开的,你只能做闪退后的自动拉起。
5. CoAPService 避坑与排查:UDP 不回包、worker 线程饱和和手机休眠断连
5.1 现象:客户端一直 CON 重传,服务端抓包却已经回了 ACK
现象是客户端反复发出 CON 请求,收不到 ACK,但服务端 tcpdump 或 Wireshark 上明明能看到已经回包。最常见原因是服务端绑定到了错误的本地地址。比如 Android 同时开着 WiFi 和热点,CoapServer 默认绑定到 0.0.0.0,回包时系统选了一个客户端无法到达的源地址;或者服务器端只监听了某个固定 IP,而客户端打到另一个网卡上。
原因是多网卡环境下的源 IP 选择问题,不是 CoAP 协议本身的问题。解决方法是显式指定监听地址,例如让服务器只监听当前活跃的 WiFi 网卡 IP,并且把路由和防火墙规则对齐。代码上不要只写端口,而是写成new IPAddress("192.168.x.x")配合端口构造;在 Android 端,最好在绑定前先通过ConnectivityManager拿到当前网卡的 IPv4,再传给CoapServer。
5.2 现象:锁屏两分钟后所有 CoAP 连接全部超时
现象很典型:手机亮屏时 CoAP 客户端一切正常,锁屏几分钟后请求全部超时。原因是 Android 进入深度休眠后,CPU 暂停、网络栈进入低功耗模式,UDP socket 不会触发业务回调。CoAP 又是基于 UDP,没有内核 TCP keepalive 帮你检测链路,所以问题会一直持续到手机重新亮屏。
解决方向有两个:一是给应用申请PARTIAL_WAKE_LOCK和 WiFi 锁,让系统保持联网状态;二是把 CoAP 服务提升为前台服务,降低被系统挂起的概率。前面 4.2 小节已经给了锁的代码,这里再补充一点:锁的 acquire 和 release 要配对,否则功耗会异常高。如果你发现锁屏后还是断,先查 logcat 里有没有系统 kill 掉进程的 tidy 记录,有就说明不是休眠,是被收回了。
5.3 现象:客户端收到 4.04 Not Found,但资源明明已经注册
现象是资源明明server.add了,客户端访问却收到 Not Found。多数原因是资源路径匹配不一致。CoAP 的Uri-Path是一个独立的 option,不像 HTTP 那样/?a=b直接用字符串解析。客户端如果传了%20这类编码,或者路径里多了一层目录,资源路由就匹配不上。
解决方法是先在handleUndefined()里打印请求 URI:
@Override public void handleUndefined(CoapExchange exchange) { System.out.println("Undefined request URI: " + exchange.getRequestURI()); exchange.respond(CoapResponseCode.NOT_FOUND); }打印内容会包含完整的请求路径,对比一下你注册时的 name,就能定位是大小写还是目录层级问题。我的习惯是把资源名全部定义成小写,客户端调用时也统一小写,从源头上减少这类排查成本。
5.4 现象:端点偶尔不响应,日志里全是线程池拒绝异常
现象是服务端跑一段时间后,资源回调开始不执行,日志冒出RejectedExecutionException。原因是 worker 线程池被占满,新的请求没有线程可处理。CoAP 是 UDP,没有背压机制,请求会直接堆在 socket 缓冲区;线程池一旦拒绝,消息就丢失,客户端只能重传。
解决方法是给线程池一个合理队列大小,并监控活跃线程数。代码上可以改用ThreadPoolExecutor并显式指定ArrayBlockingQueue,同时在资源回调里避免长时间阻塞;如果确实要同步查询,把耗时操作放到回调之外的线程池里,让 worker 只做快速响应和状态变更。这个坑在 Java 服务器端和 Android 端都会出现,只要并发请求一多就暴露。
这些坑有一个共同点:现象都像网络问题,根因都在编码或生命周期。排的时候要按“先抓包、再看线程、最后对路径”的顺序来,能省很多无用功。
6. 验证与进阶:抓包、观察模式和给 CoAP 加 DTLS
落地一个 CoAP 服务器端后,先用三件套验证:命令行客户端、Java 客户端、Wireshark 抓包。Wireshark 过滤器固定成udp.port == 5683 or udp.port == 5684,能看到 CON/NON/ACK 和 Observe 推送,比盲目调用日志直观得多。
命令行的验证命令是:
coap-client -m get coap://127.0.0.1:5683/hello如果返回 hello,说明资源路由正常。接着试试 Java 客户端侧:
CoapClient client = new CoapClient("coap://127.0.0.1:5683/hello"); CoapResponse response = client.get(); System.out.println(response.getResponseText());注意CoapClient每次都会分配新的 token,因此它天然适合做连通性验证。要深入验证观察模式,就在资源端设置:
resource.setObservable(true); // 状态变化时调用 resource.changed();changed()会主动把最新状态推给所有订阅者。抓包时如果看到 2.05 响应里带上 Observe 序号,说明联动正常。
如果要上生产环境,5683 端口裸奔不安全。Java 侧的常见做法是用 scandium 模块接入 DTLS,把普通 UDP 换掉。配置上至少调整两项:PSK 身份和会话超时时间。连接用 5684 端口,客户端握手后会拿到加密会话。每次上线前我都会先问自己:这个接口如果被人改一个 GET,客户端会死机吗?这么问过几次之后,我养成了先抓包再调参的习惯,很多自认为的“玄学”问题都变成了能看到根因的问题。希望帮到你。
本文还有配套的精品资源,点击获取