目录

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: 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 操作(绑定、连接、收发)都基于该实例。

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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐