Netty TCP Long‑Connection Demo for IoT Messaging with Redis and Spring Boot
This article presents a complete Netty‑based TCP client/server demo for IoT long‑connection messaging, explains the project architecture, module layout, business flow, and provides detailed Java code for a local message queue, multithreaded processing, client creation, handler logic, and testing endpoints.
Project Background
The author needed a reliable long‑connection solution for an IoT project that uses socket communication; after encountering many bugs, they created an open‑source demo that strips away business‑specific code and focuses on the core networking logic.
Architecture
The demo combines netty , redis and springboot 2.2.0 . The main modules are:
netty-tcp-core : utility classes shared by client and server.
netty-tcp-server : a simple server used only for testing.
netty-tcp-client : the focus of the article, implementing the client side.
Business Flow
In production the system would use RocketMQ; the demo replaces it with a local BlockingQueue . The flow is: Producer → Message Queue → Consumer (client) → TCP channel → Server → TCP channel → Client. When a message arrives, the consumer checks whether a TCP channel for the device already exists; if not, it creates one, otherwise it reuses the existing channel.
Code Details
1. Message Queue
A static ArrayBlockingQueue holds NettyMsgModel objects. A dedicated thread continuously takes messages from the queue and hands them to MessageProcessor for asynchronous handling.
package org.example.client;
import org.example.client.model.NettyMsgModel;
import java.util.concurrent.ArrayBlockingQueue;
/**
* Local queue demo (replace with RocketMQ or RabbitMQ in production)
*/
public class QueueHolder {
private static final ArrayBlockingQueue<NettyMsgModel> queue = new ArrayBlockingQueue<>(100);
public static ArrayBlockingQueue<NettyMsgModel> get() { return queue; }
}2. Execution Class
The LoopThread creates a thread pool and repeatedly pulls messages from the queue, invoking MessageProcessor.process() . It uses take() to block when the queue is empty.
public class LoopThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < MAIN_THREAD_POOL_SIZE; i++) {
executor.execute(() -> {
while (true) {
try {
NettyMsgModel nettyMsgModel = QueueHolder.get().take();
messageProcessor.process(nettyMsgModel);
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
}
});
}
}
}3. Client
The NettyClient is a prototype‑scoped Spring bean. It receives the device IMEI, business data map, an EventLoopGroup , and a handler class. The client builds a Bootstrap , configures decoders, encoders, idle‑state handling, and connects to the server with a retry mechanism (max 2 retries). After a successful connection it stores the instance in NettyClientHolder and sends the pending message.
public class NettyClient implements Runnable {
@Value("${netty.server.port}") private int port;
@Value("${netty.server.host}") private String host;
private String imei;
private Map
bizData;
private EventLoopGroup workGroup;
private Class<BaseClientHandler> clientHandlerClass;
private ChannelFuture channelFuture;
// constructor, run(), init(), connect() etc.
}4. Handler
DemoClientHandler extends a base handler, holds a reference to its NettyClient , and implements lifecycle callbacks:
channelActive : notifies the waiting thread that the channel is ready.
channelRead : logs incoming messages and processes business logic; a special "shutdown" message triggers client closure.
userEventTriggered : monitors idle events and closes the channel after three consecutive idle periods.
public class DemoClientHandler extends BaseClientHandler {
private final String imei;
private final Map
bizData;
private final NettyClient nettyClient;
private int allIdleCounter = 0;
private static final int MAX_IDLE_TIMES = 3;
// constructor, channelActive(), channelRead(), userEventTriggered(), exceptionCaught()
}5. Client Cache
NettyClientHolder stores active client instances in a ConcurrentHashMap<String, NettyClient> keyed by IMEI, allowing quick lookup without persisting Netty channels.
public class NettyClientHolder {
private static final ConcurrentHashMap<String, NettyClient> clientMap = new ConcurrentHashMap<>();
public static ConcurrentHashMap<String, NettyClient> get() { return clientMap; }
}Testing
A Spring‑Boot controller exposes three endpoints:
/demo/testOne : sends two messages with a 5‑second pause, demonstrating client creation and reuse.
/demo/testTwo : allows arbitrary IMEI and message parameters.
/demo/testThree : sends two messages for the same device back‑to‑back, showing the lock‑based re‑queueing when the client is already processing.
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping("testOne")
public void testOne() { /* ... */ }
@GetMapping("testTwo")
public void testTwo(@RequestParam String imei, @RequestParam String msg) { /* ... */ }
@GetMapping("testThree")
public void testThree() { /* ... */ }
}Log screenshots show the first message triggering client creation, the second message being sent through the existing channel, and the idle‑requeue behavior for the third test.
Source Code
The full project is available at https://gitee.com/jaster/netty-tcp-demo .
Conclusion
The demo is intended for learning and can be extended for production use; readers are encouraged to ask questions or contribute improvements.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.