NIO 框架讓高並發、高效能的網路服務開發成為可能。主流框架有 MINA 與 Netty,Netty 經歷 3、4 版演進 (Netty 5 已停止開發),API 風格與 MINA 差異較大。本文提供一個基於 Netty 4 的 UDP 伺服器與標準 Java UDP 客戶端 (無第三方依賴)的雙向通訊完整範例,填補官方範例僅有簡單廣播的不足。
從 Netty 官網 下載最新 4.1 版,建議使用 netty-all-4.1.x.Final.jar (約 2MB) 簡化依賴管理。
public class EchoServer {
public static void main(String[] args) throws InterruptedException {
Bootstrap b = new Bootstrap();
EventLoopGroup group = new NioEventLoopGroup();
b.group(group)
.channel(NioDatagramChannel.class)
.handler(new EchoServerHandler());
// 綁定 9999 埠並阻塞等待關閉
b.bind(9999).sync().channel().closeFuture().await();
}
}
核心組件:Bootstrap (非阻塞啟動器)、NioEventLoopGroup (事件循環群組)、NioDatagramChannel (UDP 通道)、SimpleChannelInboundHandler 處理器。
public class EchoServerHandler extends SimpleChannelInboundHandler<DatagramPacket> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception {
// 讀取資料
ByteBuf buf = (ByteBuf) packet.copy().content();
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, CharsetUtil.UTF_8);
System.out.println("【NOTE】收到客戶端資料:" + body);
// 回覆客戶端
ctx.writeAndFlush(new DatagramPacket(
Unpooled.copiedBuffer("Hello,我是Server,時間戳:" + System.currentTimeMillis(), CharsetUtil.UTF_8),
packet.sender()
)).sync();
}
}
為保持程式碼簡潔,將 Socket 管理與資料接收分離為兩個工具類 (參考開源 MobileIMSDK 簡化)。
public class EchoClient {
public static void main(String[] args) throws Exception {
// 初始化本地 UDP Socket
LocalUDPSocketProvider.getInstance().initSocket();
// 啟動接收執行緒
LocalUDPDataReciever.getInstance().startup();
while (true) {
String msg = "Hi,我是客戶端,時間戳:" + System.currentTimeMillis();
byte[] data = msg.getBytes("UTF-8");
boolean ok = UDPUtils.send(data, data.length);
System.out.println(ok ? "發送成功" : "發送失敗!!!");
Thread.sleep(3000);
}
}
}
public class LocalUDPSocketProvider {
private static LocalUDPSocketProvider instance = null;
private DatagramSocket localUDPSocket = null;
public static LocalUDPSocketProvider getInstance() {
if (instance == null) instance = new LocalUDPSocketProvider();
return instance;
}
public void initSocket() {
try {
// 本地監聽埠 (0 表系統自動分配)
this.localUDPSocket = new DatagramSocket(ConfigEntity.localUDPPort);
// 連接遠端地址,後續 send 無需重複指定 IP/埠
// 注意:connect 必須在 receive() 前呼叫,否則 send 可能阻塞
this.localUDPSocket.connect(
InetAddress.getByName(ConfigEntity.serverIP), ConfigEntity.serverUDPPort);
this.localUDPSocket.setReuseAddress(true);
System.out.println("DatagramSocket 初始化完成");
} catch (Exception e) {
e.printStackTrace();
}
}
public DatagramSocket getLocalUDPSocket() { return this.localUDPSocket; }
}
public class LocalUDPDataReciever {
private static LocalUDPDataReciever instance = null;
private Thread thread = null;
public static LocalUDPDataReciever getInstance() {
if (instance == null) instance = new LocalUDPDataReciever();
return instance;
}
public void startup() {
this.thread = new Thread(() -> {
try {
System.out.println("本地 UDP 監聽中,埠=" + ConfigEntity.localUDPPort);
udpListeningImpl();
} catch (Exception e) {
System.out.println("UDP 監聽停止:" + e.getMessage());
}
});
this.thread.start();
}
private void udpListeningImpl() throws Exception {
while (true) {
byte[] data = new byte[1024];
DatagramPacket packet = new DatagramPacket(data, data.length);
DatagramSocket socket = LocalUDPSocketProvider.getInstance().getLocalUDPSocket();
if (socket == null || socket.isClosed()) continue;
socket.receive(packet); // 阻塞等待
String msg = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
System.out.println("【NOTE】收到伺服器訊息:" + msg);
}
}
}
客戶端輸出: 顯示每 3 秒發送一次,並印出伺服器回覆的時間戳。
伺服器端輸出: 印出收到的客戶端訊息及發送回覆紀錄。
(圖片略,請參考原文截圖)