鸿蒙与Java跨平台Socket通信实战

鸿蒙与Java跨平台Socket通信实战

 目录

1.整体通信架构

2.鸿蒙 ArkTS TCP 客户端实现

2.1 完整代码

2.2 核心代码解析

3.Java 多线程 TCP 服务器实现

3.1 主服务类 Server.java

3.2 工作线程类 WorkThread.java

3.3 核心代码解析

4.运行效果展示

4.1 鸿蒙客户端界面

4.2 Java 服务端控制台

5.核心知识点总结


        本篇博客将从零实现一个鸿蒙 ArkTS TCP 客户端Java 多线程 TCP 服务器的双向聊天功能,涵盖【绑定端口→建立连接→持续收发→资源释放】全流程,代码可直接运行,适配鸿蒙 5.0 + 与 Java 8 + 环境。

1.整体通信架构

鸿蒙客户端(ArkTS)

 绑定本地端口 → 连接Java服务端 → 发送消息(带换行符做边界)

Java服务端(Java)

 监听端口(ServerSocket)→ 线程池分配工作线程 → 读取客户端消息(按行解析) → 控制台输入回发消息 → 客户端接收并展示

2.鸿蒙 ArkTS TCP 客户端实现

2.1 完整代码

import { socket } from "@kit.NetworkKit" import { BusinessError } from "@kit.BasicServicesKit" // 导入鸿蒙工具库,这里主要用其编解码能力(TextDecoder) import util from "@ohos.util" // 构建TCP套接字实例,这是整个TCP通信的核心对象,所有TCP操作都基于该实例 let tcpSocket: socket.TCPSocket = socket.constructTCPSocketInstance() @Entry @Component struct Index { // 本地绑定的端口号,默认9990 @State localPort: number = 9990 // 消息历史记录,拼接所有收发消息 @State msgHistory: scroller: Scroller = new Scroller() // 远程TCP服务器的IP地址,默认局域网IP @State serverIP: string = "192.168.247.1" // 远程TCP服务器的端口号 @State serverPort: number = 9980 // 控制“连接服务器”按钮的可用状态:绑定本地端口成功后才启用 @State visibleFlag: boolean = false // 控制“发送消息”按钮的可用状态:连接服务器成功后才启用 @State sendFlag: boolean = false // 输入框中待发送的消息内容 @State sendMsg: // 绑定本地IP和端口 async bindPort() { // 定义本地地址对象:address为0.0.0.0表示绑定本机“所有网卡”的该端口 let localAddress: socket.NetAddress = { address: "0.0.0.0", port: this.localPort } await tcpSocket.bind(localAddress).then(() => { this.msgHistory = '绑定服务成功' + "\r\n" this.visibleFlag = true }).catch((e: BusinessError) => { this.msgHistory = "绑定服务失败" + "\r\n" }) // 注册TCP的message事件监听:服务器发送消息时,触发该回调 tcpSocket.on("message", async (value) => { console.log("鸿蒙接受到服务器传递的消息") // value.message是服务器发送的“二进制缓冲区”,鸿蒙的TCPSocket接收的消息是ArrayBuffer类型,必须通过util.TextDecoder解码为字符串才能展示 let buffer = value.message // 创建UTF-8解码器(鸿蒙标准编解码API) let textDecoder = util.TextDecoder.create("UTF-8") // 将二进制缓冲区转为Uint8Array,再解码为UTF-8字符串 let str = textDecoder.decodeToString(new Uint8Array(buffer)) // 拼接消息历史:服务器消息+时间戳+换行,实现日志式展示 this.msgHistory +="服务器发送的消息为:["+this.getCurrentTimeString()+"]:"+str+"\r\n" // 滚动器自动滚到底部,显示最新的服务器消息 this.scroller.scrollEdge(Edge.Bottom) }) } // 连接远程 TCP 服务器 async connServer() { // 封装服务器地址对象:由UI输入框的serverIP/serverPort赋值 let serverAddress: socket.NetAddress = { address: this.serverIP, port: this.serverPort } // 异步连接服务器:TCP的三次握手过程,IO操作需异步处理 await tcpSocket.connect({ address: serverAddress }).then(() => { this.msgHistory = "连接服务器成功" + "\r\n" this.sendFlag = true }).catch(() => { this.msgHistory = "连接服务器失败" + "\r\n" }) } // 补零工具函数 padZero = (n: number) => n < 10 ? "0" + n : n // 获取当前时间戳 getCurrentTimeString(): string { let let date = new Date() // time = date.getHours().toString() + ":" + date.getMinutes().toString() + ":" + date.getSeconds().toString() // 加上补零处理后的时间戳 time = this.padZero(date.getHours()) + ":" + this.padZero(date.getMinutes()) + ":" + this.padZero(date.getSeconds()) return time } // 向服务器发送消息 sendMessageServer() { // TCP是面向字节流的协议,没有“消息边界”,服务器无法区分连续发送的多条消息,因此添加换行符作为消息分隔符,是 TCP 字节流通信的通用解决方案。 tcpSocket.send({ data: this.sendMsg + "\r\n" }) .then(() => { this.msgHistory += "我:[" + this.getCurrentTimeString() + "]:" + this.sendMsg + "\r\n" }).catch(() => { this.msgHistory += "我:发送失败" + "\r\n" }) } build() { Column({ space: 20 }) { Text("鸿蒙套接字通信示例").width("100%").textAlign(TextAlign.Center).fontWeight(FontWeight.Bold) // 本地端口绑定区 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { Text("本地IP和端口:").width("30%").fontSize(12) // 数字类型输入框,绑定localPort,输入变化时更新变量 TextInput({ text: this.localPort.toString() }).type(InputType.Number).width("40%").onChange((value) => { this.localPort = Number(value) console.log("输入本地的端口为:" + this.localPort) }) Button("绑定IP和端口").onClick(() => { this.bindPort() }).width("30%").fontSize(12) } // 服务器连接区 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { Text("服务器地址:").width("20%").fontSize(12) TextInput({ text: this.serverIP }).width("25%").onChange((value) => { this.serverIP = value console.log("输入服务器IP地址为:" + this.serverIP) }) TextInput({ text: this.serverPort.toString() }).width("25%").onChange((value) => { this.serverPort = Number(value) console.log("输入服务器PORT为:" + this.serverPort) }) // 连接按钮:仅visibleFlag为true时可用,点击触发connServer() Button("连接服务器").enabled(this.visibleFlag).width("30%").onClick(() => { this.connServer() }) } // 消息发送区 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { TextInput({ placeholder: "请输入要发送的消息:" }).onChange((value) => { this.sendMsg = value }) // 发送按钮:仅sendFlag为true时可用,点击触发sendMessageServer() Button("发送消息").enabled(this.sendFlag).width("30%").onClick(() => { this.sendMessageServer() }) } // 消息历史展示区 Scroll(this.scroller) { Text(this.msgHistory) .textAlign(TextAlign.Start) .padding(10).width("100%") } .align(Alignment.Top) .height(300) .backgroundColor(0xeeeeee) .scrollable(ScrollDirection.Vertical) .scrollBar(BarState.On) .scrollBarWidth(20) }.width("100%").height("100%") } }

2.2 核心代码解析

(1)TCP 套接字初始化

鸿蒙提供的TCPSocket是核心通信对象,所有 TCP 操作(绑定、连接、收发)都基于该实例。

let tcpSocket: socket.TCPSocket = socket.constructTCPSocketInstance()

(2)绑定本地端口

0.0.0.0表示绑定本机所有网卡,确保局域网内其他设备可连接。绑定成功后启用【连接服务器】按钮。

let localAddress: socket.NetAddress = { address: "0.0.0.0", port: this.localPort } await tcpSocket.bind(localAddress)

(3)消息监听与解码

服务器消息是二进制ArrayBuffer,需用TextDecoder解码为 UTF-8 字符串,自动滚动到底部保证最新消息可见。

tcpSocket.on("message", async (value) => { let buffer = value.message let textDecoder = util.TextDecoder.create("UTF-8") let str = textDecoder.decodeToString(new Uint8Array(buffer)) this.msgHistory += "服务器发送的消息为:[" + this.getCurrentTimeString() + "]:" + str + "\r\n" this.scroller.scrollEdge(Edge.Bottom) })

(4)发送消息(带边界)

TCP 是字节流协议,无天然消息边界,添加\r\n作为分隔符,与 Java 服务端的readLine()完美匹配。

tcpSocket.send({ data: this.sendMsg + "\r\n" })

3.Java 多线程 TCP 服务器实现

3.1 主服务类 Server.java

package com.pp.chapter1; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Server { // 服务器端的核心套接字对象,用于监听客户端的TCP连接请求 private ServerSocket serverSocket; // 固定线程池:管理工作线程,避免线程泛滥 private ExecutorService threadPool; // 服务器绑定端口(与鸿蒙客户端默认端口一致) private static final int SERVER_PORT = 9980; // 线程池核心线程数 private static final int THREAD_POOL_SIZE = 10; // 构造方法:初始化服务、启动监听、线程池 public Server() { try { // 初始化固定线程池 threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE); // 创建ServerSocket并绑定端口 serverSocket = new ServerSocket(SERVER_PORT); // 避免端口被占用 serverSocket.setReuseAddress(true); System.out.println("=== TCP服务器启动成功 ==="); System.out.println("监听端口:" + SERVER_PORT); System.out.println("线程池初始化完成,核心线程数:" + THREAD_POOL_SIZE); // 死循环:持续监听客户端连接 while(true) { // 阻塞方法:调用后程序会暂停执行,直到有客户端发起 TCP连接请求并完成三次握手,才会返回Socket对象并继续执行 Socket socket = serverSocket.accept(); // 获取客户端IP+端口并打印 String clientInfo = socket.getInetAddress().getHostAddress() + ":" + socket.getPort(); System.out.println("【新客户端连接】" + clientInfo); // 将通信任务提交到线程池 threadPool.execute(new WorkThread(socket)); } } catch (IOException e) { System.err.println("=== TCP服务器启动失败 ==="); System.err.println("失败原因:端口" + SERVER_PORT + "被占用/权限不足," + e.getMessage()); System.exit(1); // 启动失败直接退出程序 } } public static void main(String[] args) { new Server(); } } 

3.2 工作线程类 WorkThread.java

package com.pp.chapter1; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; public class WorkThread implements Runnable { // 与单个客户端通信的Socket对象(由Server的accept()返回) private final Socket socket; // 客户端IP+端口(用于日志打印) private String clientInfo; // 构造方法:接收客户端Socket public WorkThread(Socket socket) { this.socket = socket; this.clientInfo = socket.getInetAddress().getHostAddress() + ":" + socket.getPort(); } @Override public void run() { // try-with-resources语法:自动关闭所有实现AutoCloseable的资源 // 一次性创建流,循环复用,避免重复创建;统一指定UTF-8编码,解决跨语言乱码 try ( // 客户端消息输入流:字节流→字符流(UTF-8)→缓冲流,一次创建持续使用 BufferedReader clientReader = new BufferedReader( new InputStreamReader(socket.getInputStream(), "UTF-8") ); // 服务器向客户端输出流:字节流→打印流(UTF-8+自动刷新),无需手动flush PrintWriter serverWriter = new PrintWriter( new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true // 自动刷新 ); // 服务器控制台输入流:读取控制台回发消息,UTF-8编码 BufferedReader consoleReader = new BufferedReader( new InputStreamReader(System.in, "UTF-8") ) ) { System.out.println("【通信线程启动】" + clientInfo + ",开始监听客户端消息..."); String clientMsg; // 循环读取客户端消息 // readLine()返回null → 客户端主动断开连接(鸿蒙应用关闭/网络断开) while ((clientMsg = clientReader.readLine()) != null) { // 过滤客户端空消息(避免无效处理) if (clientMsg.trim().isEmpty()) { System.out.println("【空消息忽略】" + clientInfo + "发送了空消息"); continue; } // 打印客户端消息:线程名+客户端信息+消息 System.out.printf("[%s] 【客户端消息】%s:%s%n", Thread.currentThread().getName(), clientInfo, clientMsg); // 服务器控制台输入回发消息 System.out.print("请输入要回发给" + clientInfo + "的消息:"); String serverMsg = consoleReader.readLine(); // 过滤服务器空消息,避免发送空内容给客户端 if (serverMsg == null || serverMsg.trim().isEmpty()) { serverMsg = "【服务器】消息不能为空,已忽略"; } // 发送消息给客户端:PrintWriter开启自动刷新,直接println即可 serverWriter.println(serverMsg); System.out.printf("[%s] 【服务器回发】%s:%s%n", Thread.currentThread().getName(), clientInfo, serverMsg); } } catch (IOException e) { // 精细化异常日志:区分客户端正常断开/异常断开 if (socket.isClosed() || e.getMessage().contains("Connection reset")) { System.out.println("【客户端断开】" + clientInfo + "(正常/异常断开)"); } else { System.err.println("【通信异常】" + clientInfo + ",原因:" + e.getMessage()); } } finally { // 确保Socket关闭(即使try-with-resources出问题) try { if (socket != null && !socket.isClosed()) { socket.close(); } } catch (IOException e) { System.err.println("【Socket关闭失败】" + clientInfo + ",原因:" + e.getMessage()); } System.out.println("【通信线程销毁】" + clientInfo + ",释放所有通信资源\n"); } } } 

3.3 核心代码解析

(1)线程池管理多客户端

固定线程池避免大量客户端连接导致的线程泛滥,保证服务端稳定性,核心线程数可根据业务调整。

threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

(2)try-with-resources自动释放资源

JVM 自动关闭所有实现AutoCloseable的资源,彻底解决 IO 流泄漏问题,关闭顺序与声明顺序逆序(先关输出流,再关输入流)。

try ( BufferedReader clientReader = new BufferedReader(...); PrintWriter serverWriter = new PrintWriter(...); BufferedReader consoleReader = new BufferedReader(...) ) { // 业务逻辑 }

(3)UTF-8 编码统一

所有流转换显式指定 UTF-8,与鸿蒙客户端编码一致,避免跨语言乱码。

new InputStreamReader(socket.getInputStream(), "UTF-8") new OutputStreamWriter(socket.getOutputStream(), "UTF-8")

(4)循环监听客户端消息

readLine()\r\n分割消息,返回null表示客户端断开连接,自动退出循环并释放资源。

while ((clientMsg = clientReader.readLine()) != null) { // 处理消息 }

4.运行效果展示

4.1 鸿蒙客户端界面

  • 绑定本地端口(9990)→ 连接服务器(192.168.247.1:9980)
  • 输入消息发送,服务端回发
  • 消息面板自动滚动,展示带时间戳的收发记录

4.2 Java 服务端控制台

  • 服务端启动后监听 9980 端口,线程池初始化完成
  • 客户端连接后打印 IP + 端口,分配工作线程处理通信
  • 接收客户端消息后,控制台输入回发内容,自动发送给客户端

5.核心知识点总结

跨语言通信关键:统一 UTF-8 编码 + 换行符做消息边界,保证 ArkTS 与 Java 的字节流解析一致。

TCP 服务端架构ServerSocket监听 + 线程池管理 +WorkThread处理单客户端。

资源管理:Java 用try-with-resources自动释放 IO 流,鸿蒙用异步 API 避免阻塞主线程。

UI 状态联动:鸿蒙客户端用@State变量控制按钮可用状态,实现【绑定→连接→发送】的流程化交互。

Read more

【Linux篇章】再续传输层协议TCP:用技术隐喻重构网络世界的底层逻辑,用算法演绎‘网络因果律’的终极推演(通俗理解TCP协议,这一篇就够了)!

【Linux篇章】再续传输层协议TCP:用技术隐喻重构网络世界的底层逻辑,用算法演绎‘网络因果律’的终极推演(通俗理解TCP协议,这一篇就够了)!

📌本篇摘要 * 本篇将根据TCP协议报文的格式来对TCP更深入的了解,学习它的三次握手,四次挥手,滑动窗口等等,到最后能更加深入理解之前写TCP通信的时候,底层到底是如何进行的,读完本篇将会对之前TCP网络通信编程有更深入的认识。 🏠欢迎拜访🏠:点击进入博主主页 📌本篇主题📌:再续TCP协议 📅制作日期📅:2025.12.20 🧭隶属专栏🧭:点击进入所属Linux专栏 一.TCP协议格式 -TCP 全称为 传输控制协议(Transmission Control Protocol). 人如其名, 要对数据的传输进行一个详细的控制。 下面看TCP报文的格式: 下面我们来一个个介绍下这些字段及作用: 1. 🔍十六位窗口大小 * 这里我们知道对于tcp来说,如果接收缓冲区满了,再发送机会被丢弃,因此发送前需要知道对的的接收缓冲区的剩余长度。 * 按量按需发送,必须知道对方的接受缓冲区中剩余空间的大小,因此每次发送的tcp报文都要带有自己剩余接收缓冲区的长度! 2.🔍4位首部长度 * 首先我们要知道tcp光报头就至少20字节(不包含

By Ne0inhk
coding ability 展开第七幕(前缀和算法——进阶巩固)超详细!!!!

coding ability 展开第七幕(前缀和算法——进阶巩固)超详细!!!!

文章目录 * 前言 * 和为k的子数组 * 思路 * 和可被k整除的子数组 * 思路 * 连续数组 * 思路 * 矩阵区域和 * 思路 * 总结 * 总结 前言 本专栏上篇博客带大家了解了前缀和的有关模版以及习题的训练 从一维到二维的拓展,今天我们继续来练习一下前缀和的有关算法 fellow me 和为k的子数组 思路 设 i 为数组中的任意位置,用 sum[i] 表示 [0, i] 区间内所有元素的和。 想知道有多少个**「以 i 为结尾的和为 k 的子数组」,就要找到有多少个起始位置为 x1, x2,x3… 使得 [x, i] 区间内的所有元素的和为 k 。那么 [0, x] 区间内的和是不是就是sum[i]

By Ne0inhk
LeetCode128:哈希集合巧解最长连续序列

LeetCode128:哈希集合巧解最长连续序列

一、题目回顾 LeetCode 128 题「最长连续序列」是一道中等难度的数组题,核心要求如下:给定一个未排序的整数数组 nums,找出其中数字连续的最长序列(不要求序列元素在原数组中连续)的长度,且必须设计时间复杂度为 O (n) 的算法。 示例直观理解: * 输入 nums = [100,4,200,1,3,2],输出 4(最长序列是 [1,2,3,4]); * 输入 nums = [0,3,7,2,5,8,4,6,0,1],输出 9(完整连续序列 0-8)。 二、

By Ne0inhk
图的寻路算法详解:基于深度优先搜索(DFS)的实现

图的寻路算法详解:基于深度优先搜索(DFS)的实现

图的寻路算法详解:基于深度优先搜索DFS的实现 * 一、寻路算法概述 * DFS寻路示例 * 二、算法核心思想 * 数据结构设计 * 三、算法实现详解 * 1. 核心数据结构 * 2. 构造函数初始化 * 3. DFS实现 * 4. 路径查询方法 * 四、完整代码实现 * 五、算法测试与应用 * 测试代码 * 输出结果 * 六、算法分析与优化 * 时间复杂度分析 * 空间复杂度 * 优化方向 * 七、DFS寻路与BFS寻路对比 * 八、实际应用场景 * 九、总结 🌺The Begin🌺点点关注,收藏不迷路🌺 一、寻路算法概述 图的寻路算法是图论中的基础算法之一,用于找到从一个顶点到另一个顶点的路径。深度优先搜索(DFS)是实现寻路算法的一种有效方法,它沿着图的深度方向尽可能远的搜索路径。 DFS寻路示例 0123456 从顶点0到顶点6的DFS路径可能是:

By Ne0inhk