☰
Java实现CoAP协议栈:Android与Java服务端源码解析
2026/10/21 14:03:07 网站建设 项目流程

简介:这是一份面向物联网与Android开发者的CoAP协议Java实现源码包,适合需要搭建CoAP服务器或进行协议学习的初中级开发者。资源分为app与coaplib两个模块:app为Android服务器端代码,coaplib则包含Java服务器实现及Java客户端测试代码,可帮助读者理解CoAP基于UDP的双层事务机制、重传处理、资源发现与描述等核心特性,并实践GET、POST、PUT、DELETE等资源操作方式。压缩包共297个文件,以262个java源码为主体,辅以12个xml配置、10个png图示、3个properties与3个gradle构建文件,整体约582KB,结构紧凑便于阅读。目前已有504人学习下载,读者可借此快速掌握CoAP服务端与客户端的通信流程,对照源码理解资源发现、查询参数筛选等实现细节,为物联网通信开发提供可直接参考的代码基础。

1. 从一台没有公网IP的安卓设备说起:CoAPService 能帮你做什么

去年帮一个做工业传感器的团队看代码,他们的采集终端跑在安卓板子上,需要把温度、振动数据实时传给内网的一台 Java 服务端。HTTP 轮询太费电,MQTT 又要额外搭 Broker,最后他们选了一条冷门但很对路的路:CoAP。问题来了——安卓端做 CoAP 服务端、Java 端做 CoAP 服务端,现成的开源实现要么只给客户端,要么依赖一堆用不上的东西。翻到这份 CoAPService 源码时,我第一反应是「终于有人把 Android 服务端和 Java 服务端拆开写清楚了」。它是一套用 Java 实现的 CoAP 协议栈,同时提供 Android 端和标准 Java 端的服务端封装,适合做物联网设备接入、低功耗传感网络、内网设备发现这类场景。如果你正在找一份能直接跑、能改协议参数、能看懂报文怎么拼的 CoAP 源码,这份东西值得花一个下午拆一遍。

2. 技术选型与目录结构:为什么是 Java 写 CoAP,代码怎么分层

2.1 CoAP 协议本身决定了这套代码的形态

CoAP 全称 Constrained Application Protocol,是给资源受限设备设计的应用层协议,跑在 UDP 之上,报文头固定 4 字节,后面跟 Token、Options、Payload。它借鉴了 HTTP 的 REST 语义(GET/POST/PUT/DELETE),但把传输层换成了 UDP,还加了 Confirmable/Non-confirmable 消息类型来做轻量可靠传输。这意味着实现一套 CoAP 栈,核心工作集中在三块:报文的二进制编解码、消息重传与去重、资源路由。

这份 CoAPService 的代码结构基本就是按这三块切的。它没有引入 Netty 这种重型框架,而是直接用 Java 的DatagramSocket/DatagramPacket做 UDP 收发,好处是依赖极简,安卓端也能直接编译进去;代价是并发模型要自己管,后面讲线程模型时会细说。

2.2 目录结构:服务端、客户端、报文、资源四层

拿到源码后,我一般先看包结构,因为它直接暴露了作者的抽象思路。这份代码大致分成这么几层:

coapservice/ ├── core/ # 协议核心 │ ├── CoapMessage.java # 报文对象,含 Header/Token/Options/Payload │ ├── CoapEncoder.java # 报文序列化为字节数组 │ ├── CoapDecoder.java # 字节数组反序列化为报文 │ ├── CoapOption.java # Option 定义与编解码 │ └── CoapCode.java # 请求方法/响应码常量 ├── server/ # 服务端 │ ├── CoapServer.java # 服务端主类,绑定端口、启动接收循环 │ ├── CoapResource.java # 资源抽象,对应一个 URI 路径 │ └── RequestHandler.java # 请求分发与响应构造 ├── client/ # 客户端 │ └── CoapClient.java # 发送请求、等待响应、超时重传 ├── android/ # Android 适配层 │ └── AndroidCoapService.java # 封装 Service 生命周期 └── util/ # 工具 ├── ByteUtil.java └── CoapConstants.java

这个分层的好处是core完全不依赖 Android,可以单独拿去做 Java 服务端;android层只做生命周期和线程切换的适配。二次开发时,如果你只想改协议行为,动core就够了;如果要做安卓后台常驻,改android层。

2.3 依赖与构建:几乎没有第三方依赖

从源码的 import 看,除了 JDK 自带的java.net、java.nio、java.util.concurrent,没有引入任何第三方库。这对安卓端特别友好——不用处理依赖冲突,APK 体积也不会因为一个协议栈膨胀。构建方式常见做法是用 Maven 或 Gradle 把core和server打成 jar,android层作为 library module 引入。如果你拿到的是纯源码目录,直接javac编译core和server下的类就能跑 Java 服务端。

提示:安卓端从 Android 9 开始默认禁止明文流量,但 CoAP 走的是 UDP,不受这条限制;真正要注意的是安卓 10 以后后台 Service 的限制,长连接场景需要配合前台服务。

3. 核心模块拆解:一个 CoAP 请求从 UDP 字节到响应的完整链路

3.1 报文编解码:4 字节头 + Option 的 TLV 结构

CoAP 报文头是固定的 4 字节,格式如下:

0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Ver| T | TKL | Code | Message ID | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Token (if any, TKL bytes) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options (if any) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |1 1 1 1 1 1 1 1| Payload (if any) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Ver 是版本号固定为 1,T 是消息类型(0=CON, 1=NON, 2=ACK, 3=RST),TKL 是 Token 长度,Code 表示请求方法或响应码,Message ID 用于匹配 ACK 和去重。解码时先读第一个字节拿到 Ver/T/TKL,再读 Code 和 Message ID,然后按 TKL 读 Token,接着循环解析 Options,直到遇到 0xFF 分隔符,剩下的就是 Payload。

编码逻辑在CoapEncoder里,核心是把 Options 按编号升序排列后做 delta 编码——每个 Option 存的是「与上一个 Option 编号的差值」,这样能省字节。这部分是 CoAP 实现里最容易写错的地方,因为 delta 超过 12 时要扩展成 13/14 两种变长编码。

3.2 服务端启动与接收循环:DatagramSocket 怎么收包

CoapServer的主循环是整个服务端的入口,逻辑不复杂但有几个关键点:

public class CoapServer { private DatagramSocket socket; private volatile boolean running = true; private final Map<String, CoapResource> resources = new ConcurrentHashMap<>(); public void start(int port) throws SocketException { // 绑定 UDP 端口,安卓端注意用 5683 以外的端口避免权限问题 socket = new DatagramSocket(port); // 单独线程跑接收循环,避免阻塞调用方 new Thread(this::receiveLoop, "coap-receiver").start(); } private void receiveLoop() { byte[] buffer = new byte[1024]; // CoAP 报文通常很小,1KB 足够 while (running) { try { DatagramPacket packet = new DatagramPacket(buffer, buffer.length); socket.receive(packet); // 阻塞直到有数据 // 每个请求丢给线程池处理,避免慢请求卡住接收 executor.submit(() -> handlePacket(packet)); } catch (IOException e) { if (running) e.printStackTrace(); } } } }

这里有几个参数值得说:buffer大小设 1024 是因为 CoAP 设计上就是小报文,超过 MTU 会分块(Block-wise Transfer),这份代码没实现分块,所以别指望传大文件。executor是线程池,常见做法是用Executors.newFixedThreadPool(4)或newCachedThreadPool(),前者适合请求量稳定的场景,后者适合突发。socket.receive是阻塞的,所以必须放在独立线程里,否则start()调用方会被卡死。

3.3 资源路由:URI 怎么映射到 Handler

CoAP 的 URI 路径存在 Uri-Path 这个 Option 里,每个路径段是一个独立的 Option。比如coap://host/sensors/temp会编码成两个 Uri-Path Option,值分别是sensors和temp。RequestHandler的工作就是把这些段拼起来,去resourcesmap 里找对应的CoapResource:

private void handlePacket(DatagramPacket packet) { CoapMessage request = CoapDecoder.decode(packet.getData(), packet.getLength()); // 从 Options 里提取 Uri-Path,拼成 "/sensors/temp" 这种形式 String path = extractPath(request); CoapResource resource = resources.get(path); if (resource == null) { // 没找到资源返回 4.04 Not Found sendResponse(packet, CoapCode.NOT_FOUND, null); return; } // 按请求方法分发:GET/POST/PUT/DELETE CoapMessage response = resource.handle(request); sendResponse(packet, response.getCode(), response.getPayload()); }

extractPath要处理 Option 的顺序和重复,因为 Uri-Path 可能出现多次。resource.handle是业务扩展点——你自定义的资源继承CoapResource并覆写handle方法,就能接入自己的数据逻辑。响应发送时要注意:如果请求是 CON 类型,响应必须带相同的 Message ID 且类型为 ACK;如果是 NON,响应也用 NON。

3.4 客户端与重传:Confirmable 消息的可靠性怎么保证

CoAP 的可靠性靠 CON 消息 + ACK 实现。客户端发 CON 请求后启动定时器,超时没收到 ACK 就重传,重传间隔按指数退避(常见是 2s、4s、8s…),重传次数到上限就放弃。CoapClient里这段逻辑大致是:

public CoapMessage send(CoapMessage request, int maxRetry) { int retry = 0; long timeout = INITIAL_TIMEOUT; // 初始 2000ms while (retry <= maxRetry) { socket.send(encode(request)); CoapMessage response = waitForResponse(timeout); if (response != null) return response; retry++; timeout *= 2; // 指数退避 } throw new TimeoutException("CoAP request timeout after " + maxRetry + " retries"); }

INITIAL_TIMEOUT和maxRetry是两个关键参数。工业场景网络抖动大,我一般把初始超时设到 3s、重试 3 次;如果是局域网内,1s + 2 次就够。waitForResponse里要做 Message ID 匹配,因为 UDP 可能收到乱序或重复的包,不匹配的包要丢弃。

4. 跑起来:Java 服务端与 Android 服务端的最小可运行示例

4.1 Java 服务端:20 行代码起一个 CoAP 服务

先跑 Java 端,因为它不依赖安卓环境,验证协议栈最快。假设你已经把core和server编译好,写一个入口类:

public class DemoServer { public static void main(String[] args) throws Exception { CoapServer server = new CoapServer(); // 注册一个资源,路径 /hello server.addResource("/hello", new CoapResource() { @Override public CoapMessage handle(CoapMessage request) { // 不管什么方法,都返回一段文本 CoapMessage resp = new CoapMessage(); resp.setCode(CoapCode.CONTENT); // 2.05 Content resp.setPayload("hello coap".getBytes()); return resp; } }); server.start(5683); // CoAP 默认端口 System.out.println("CoAP server started on 5683"); } }

addResource的路径要以/开头,内部会按/拆成 Uri-Path Option 段。CoapCode.CONTENT对应响应码 2.05,表示成功返回内容。启动后可以用任意 CoAP 客户端(比如命令行工具或另一台设备)发coap://127.0.0.1:5683/hello测试。

注意:5683 是 CoAP 标准端口,但某些系统上低于 1024 的端口需要 root 权限,安卓上更是如此。开发阶段建议换成 56830 这类高位端口。

4.2 Android 服务端:Service 生命周期与线程切换

安卓端的封装在AndroidCoapService,它继承Service,在onCreate里启动 CoapServer,在onDestroy里关闭。关键是要把 UDP 接收循环放到工作线程,并且处理好网络状态变化:

public class AndroidCoapService extends Service { private CoapServer server; @Override public void onCreate() { super.onCreate(); server = new CoapServer(); server.addResource("/status", new StatusResource()); try { server.start(56830); // 高位端口避免权限问题 } catch (SocketException e) { Log.e("CoapService", "start failed", e); } } @Override public void onDestroy() { if (server != null) server.stop(); // 释放 socket,否则端口泄漏 super.onDestroy(); } @Override public IBinder onBind(Intent intent) { return null; } }

onDestroy里必须调server.stop(),否则DatagramSocket不关闭,下次启动会报端口占用。安卓 8.0 以后后台 Service 会被限制,如果要做常驻,常见做法是startForeground配合通知栏,或者用JobScheduler定期拉起。

4.3 联调验证:用抓包确认报文对不对

跑起来之后怎么确认协议栈没问题?我一般用两步:先在服务端handlePacket里打印收到的原始字节的十六进制,再对照 CoAP 报文格式手工解析一遍。比如一个 GET/hello的 CON 请求,字节大概是40 01 12 34开头——0x40表示 Ver=1、T=0(CON)、TKL=0,0x01是 Code(GET),0x12 0x34是 Message ID。如果打印出来对不上,问题多半在编码或解码的字节序处理上。

5. 避坑与排查:CoAP 服务端开发中最容易翻车的 5 个点

5.1 现象:客户端一直超时,服务端日志显示收到了包

原因通常是响应没发回去,或者发回去的 Message ID 和请求不匹配。CoAP 规定 ACK 响应的 Message ID 必须和请求一致,如果代码里重新生成了 Message ID,客户端会认为这不是它的响应而丢弃。解决方法是响应构造时直接复用请求的 Message ID,类型设为 ACK。

5.2 现象:安卓端跑一段时间后服务不可用

原因是DatagramSocket在弱网下可能进入异常状态,或者接收线程因为未捕获的异常退出了。解决方法是给receiveLoop加 try-catch 并在异常后重建 socket,同时用volatile标志位控制循环,确保stop()能真正终止线程。

5.3 现象:Options 解析错位,Payload 变成乱码

CoAP 的 Option 是 delta 编码,如果解析时没按编号升序处理,或者 delta 扩展位(13/14)算错,后面所有 Option 都会错位。排查方法是把每个 Option 的编号、长度、值都打出来,对照 RFC 7252 的表格逐个核对。常见错误是把 Option Length 的扩展和 delta 的扩展搞混。

5.4 现象:并发请求下响应串包

原因是多个线程共用了同一个DatagramPacket或byte[] buffer。socket.receive会往传入的 packet 里写数据,如果多个线程复用同一个 packet,数据会互相覆盖。解决方法是每次 receive 都 new 一个 packet 和 buffer,或者用线程本地变量。

5.5 现象:安卓端编译报错找不到java.net.DatagramSocket

这通常是因为混淆规则把java.net下的类裁掉了,或者 minSdkVersion 设得太低。检查proguard-rules.pro里是否保留了java.net.**,以及build.gradle里 minSdk 是否至少 21。另外安卓上 UDP 广播需要CHANGE_WIFI_MULTICAST_STATE权限,单播不需要。

6. 二次开发:把 CoAPService 改造成你自己的设备接入层

拆完这份代码,如果你要拿它做二次开发,我建议从三个方向入手。第一是加 Block-wise Transfer,因为现在的实现不支持大报文分块,传超过 1KB 的数据会丢;RFC 7959 定义了 Block1/Block2 两个 Option,实现思路是在CoapResource里加一个分块缓冲区,按 Block 编号拼接。第二是加 Observe 机制,CoAP 的 Observe(RFC 7641)能让客户端订阅资源变化,服务端主动推送,这对传感器场景特别有用——实现上是在资源里维护一个观察者列表,数据变化时遍历列表发 NON 通知。第三是换掉裸DatagramSocket,如果你的并发量上来了,可以引入 Netty 的NioDatagramChannel,把接收循环换成事件驱动,吞吐能提升一个量级,但要注意 Netty 的 ByteBuf 和现有 byte[] 之间的转换。

改造时验证方法很关键。我一般会写一个回环测试:客户端发 1000 个 CON 请求,统计成功率和平均延迟,改之前先跑一遍基线,改完再跑一遍对比。另外用 Wireshark 抓包看重传次数,如果重传率超过 5%,说明超时参数需要调。

有个血泪教训:我早期改这份代码时,为了图快直接在handlePacket里做数据库查询,结果一个慢查询把整个接收线程池占满,所有请求都超时。从那以后我每次改 CoAP 服务端,都强制把业务逻辑和协议处理分开——handlePacket只做解码和分发,耗时操作一律丢到独立线程池,并且给线程池设一个有界队列和拒绝策略,防止雪崩。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询