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

目录

1.整体通信架构

[2.鸿蒙 ArkTS TCP 客户端实现](#2.鸿蒙 ArkTS TCP 客户端实现)

[2.1 完整代码](#2.1 完整代码)

[2.2 核心代码解析](#2.2 核心代码解析)

[3.Java 多线程 TCP 服务器实现](#3.Java 多线程 TCP 服务器实现)

[3.1 主服务类 Server.java](#3.1 主服务类 Server.java)

[3.2 工作线程类 WorkThread.java](#3.2 工作线程类 WorkThread.java)

[3.3 核心代码解析](#3.3 核心代码解析)

4.运行效果展示

[4.1 鸿蒙客户端界面](#4.1 鸿蒙客户端界面)

[4.2 Java 服务端控制台](#4.2 Java 服务端控制台)

5.核心知识点总结


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

1.整体通信架构

鸿蒙客户端(ArkTS)

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

Java服务端(Java)

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

2.鸿蒙 ArkTS TCP 客户端实现

2.1 完整代码

TypeScript 复制代码
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: string = ""
  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: string = ""

  // 绑定本地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 time = ""
    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 操作(绑定、连接、收发)都基于该实例。

TypeScript 复制代码
let tcpSocket: socket.TCPSocket = socket.constructTCPSocketInstance()

(2)绑定本地端口

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

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

(3)消息监听与解码

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

TypeScript 复制代码
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()**完美匹配。

TypeScript 复制代码
tcpSocket.send({ data: this.sendMsg + "\r\n" })

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

3.1 主服务类 Server.java

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

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)线程池管理多客户端

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

java 复制代码
threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

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

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

java 复制代码
try (
        BufferedReader clientReader = new BufferedReader(...);
        PrintWriter serverWriter = new PrintWriter(...);
        BufferedReader consoleReader = new BufferedReader(...)
) {
    // 业务逻辑
}

(3)UTF-8 编码统一

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

java 复制代码
new InputStreamReader(socket.getInputStream(), "UTF-8")
new OutputStreamWriter(socket.getOutputStream(), "UTF-8")

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

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

java 复制代码
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变量控制按钮可用状态,实现【绑定→连接→发送】的流程化交互。

相关推荐
笃行客从不躺平1 小时前
Token 复习
java·分布式·spring cloud
zly35001 小时前
VMware vCenter Converter Standalone 转换Linux系统,出现两个磁盘的处理
linux·运维·服务器
珠海西格2 小时前
1MW光伏项目“四可”装置改造:逆变器兼容性评估方法详解
大数据·运维·服务器·云计算·能源
..过云雨2 小时前
五种IO模型与非阻塞IO
网络·网络协议·tcp/ip
Albert Edison2 小时前
【Python】函数
java·linux·python·pip
General_G2 小时前
Linux中的信号
linux·运维·服务器
2301_818732062 小时前
项目启动报错,错误指向xml 已解决
xml·java·数据库·后端·springboot
诸神缄默不语2 小时前
当无法直接用apt instll时,Linux如何离线安装软件包(以make为例)
linux·运维·服务器
码农阿豪2 小时前
Oracle 到金仓数据库迁移实战:一次真正“落地”的国产替代之旅
java·数据库·oracle