杰瑞科技汇

java socket 客户端 多线程

核心思想

在一个典型的客户端-服务器模型中,客户端通常需要同时处理两个任务:

java socket 客户端 多线程-图1
(图片来源网络,侵删)
  1. 发送数据:将用户输入(或其他业务逻辑产生的数据)发送给服务器。
  2. 接收数据:持续监听并接收服务器发来的消息(如聊天消息、数据更新等)。

如果这两个任务在同一个线程中执行,那么当客户端在等待接收数据时(socket.getInputStream().read() 是一个阻塞方法),它将无法处理用户输入,导致程序“卡死”,反之亦然。

多线程的解决方案

  • 主线程:负责启动客户端,并启动一个专门用于发送数据的线程。
  • 接收线程:负责接收数据

这样,发送和接收任务被分离到两个独立的线程中,它们可以并发执行,互不阻塞,从而实现一个功能完善、响应迅速的客户端。


完整代码示例

下面是一个完整的、可运行的 Java Socket 多线程客户端示例,这个客户端可以连接到一个简单的回显服务器,并允许用户同时发送和接收消息。

java socket 客户端 多线程-图2
(图片来源网络,侵删)

客户端代码 (MultiThreadClient.java)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class MultiThreadClient {
    private Socket socket;
    private BufferedReader in;  // 用于接收服务器消息
    private PrintWriter out;    // 用于向服务器发送消息
    private BufferedReader consoleReader; // 用于读取用户控制台输入
    public MultiThreadClient(String host, int port) {
        try {
            // 1. 创建一个 Socket 并连接到指定的服务器地址和端口
            socket = new Socket(host, port);
            System.out.println("成功连接到服务器 " + host + ":" + port);
            // 2. 获取输入流和输出流
            // in 用于读取服务器发送来的数据
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // out 用于向服务器发送数据,autoFlush=true 表示 println 后自动刷新缓冲区
            out = new PrintWriter(socket.getOutputStream(), true);
            // consoleReader 用于读取用户在控制台的输入
            consoleReader = new BufferedReader(new InputStreamReader(System.in));
            // 3. 启动接收线程
            // 这个线程会一直运行,监听并打印来自服务器的消息
            new Thread(new Receiver()).start();
            // 4. 主线程负责发送用户输入
            System.out.println("请输入消息 (输入 'exit' 退出):");
            String userInput;
            while (true) {
                // 读取用户从控制台输入的一行
                userInput = consoleReader.readLine();
                if ("exit".equalsIgnoreCase(userInput)) {
                    break; // 如果用户输入 exit,则退出循环
                }
                // 将用户输入发送给服务器
                out.println(userInput);
            }
        } catch (UnknownHostException e) {
            System.err.println("找不到主机: " + host);
        } catch (IOException e) {
            System.err.println("无法连接到服务器或发生 I/O 错误。");
            e.printStackTrace();
        } finally {
            // 5. 关闭资源
            closeResources();
        }
    }
    // 接收服务器消息的线程任务
    private class Receiver implements Runnable {
        @Override
        public void run() {
            try {
                String serverMessage;
                // 循环读取服务器发送来的消息,直到连接关闭或发生异常
                while ((serverMessage = in.readLine()) != null) {
                    System.out.println("服务器回复: " + serverMessage);
                }
            } catch (IOException e) {
                // 如果服务器关闭了连接,readLine() 会返回 null,循环结束,这里捕获异常并打印
                System.out.println("与服务器连接已断开。");
            } finally {
                System.out.println("接收线程已停止。");
            }
        }
    }
    // 关闭所有打开的资源
    private void closeResources() {
        try {
            if (consoleReader != null) consoleReader.close();
            if (out != null) out.close();
            if (in != null) in.close();
            if (socket != null && !socket.isClosed()) socket.close();
            System.out.println("所有资源已关闭,客户端退出。");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        // 服务器地址和端口
        String serverHost = "localhost"; // 如果服务器在同一台机器上,使用 localhost
        int serverPort = 8080;         // 确保服务器正在这个端口上监听
        // 启动客户端
        new MultiThreadClient(serverHost, serverPort);
    }
}

一个简单的服务器代码 (SimpleServer.java 用于测试)

为了让客户端能够运行起来,你需要一个简单的服务器,下面是一个基本的回显服务器,它会将收到的任何消息返回给客户端。

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.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleServer {
    public static void main(String[] args) {
        int port = 8080;
        ExecutorService threadPool = Executors.newCachedThreadPool(); // 使用线程池处理多个客户端
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器启动,正在监听端口 " + port + "...");
            while (true) {
                // 等待客户端连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("客户端已连接: " + clientSocket.getInetAddress());
                // 为每个客户端连接创建一个任务,并提交到线程池
                threadPool.execute(new ClientHandler(clientSocket));
            }
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
        } finally {
            threadPool.shutdown(); // 关闭线程池
        }
    }
}
// 处理单个客户端连接的任务
class ClientHandler implements Runnable {
    private final Socket clientSocket;
    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
    }
    @Override
    public void run() {
        try (
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
        ) {
            String inputLine;
            // 读取客户端发送的数据
            while ((inputLine = in.readLine()) != null) {
                System.out.println("收到来自客户端 " + clientSocket.getInetAddress() + " 的消息: " + inputLine);
                // 将收到的消息回显给客户端
                out.println("服务器: " + inputLine);
            }
        } catch (IOException e) {
            System.out.println("客户端 " + clientSocket.getInetAddress() + " 断开连接或发生错误。");
        } finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

代码分步解析

客户端主逻辑 (MultiThreadClient.java)

  • Socket socket = new Socket(host, port);

    • 这是客户端的核心,它尝试在指定的 hostport 上建立一个到服务器的 TCP 连接,这是一个阻塞操作,如果服务器未启动或地址/端口错误,会抛出 IOException
  • BufferedReader inPrintWriter out

    • in:包装了 socket.getInputStream(),用于读取从服务器流过来的数据。BufferedReader 提供了 readLine() 方法,可以方便地按行读取文本。
    • out:包装了 socket.getOutputStream(),用于向服务器写入数据。PrintWriter 提供了 println() 方法,并且我们设置了 autoFlush=true,这样每次调用 println 后都会自动刷新缓冲区,确保数据被立即发送。
  • BufferedReader consoleReader

    java socket 客户端 多线程-图3
    (图片来源网络,侵删)
    • 这个 BufferedReader 包装的是 System.in,用于读取用户在控制台(命令行)的输入。
  • new Thread(new Receiver()).start();

    • 这是多线程的关键,我们创建了一个新的 Thread,并将一个实现了 Runnable 接口的 Receiver 对象作为其任务,调用 start() 方法后,JVM 会启动一个新的执行流,专门用于执行 Receiverrun() 方法,主线程则继续向下执行。
  • 发送循环

    • 主线程进入一个 while 循环,通过 consoleReader.readLine() 等待用户输入。
    • 当用户输入一行文本并按回车后,readLine() 返回该文本。
    • out.println(userInput); 将文本发送给服务器。
    • 如果用户输入 exit,循环结束,finally 块中的资源关闭代码被执行。

接收线程 (Receiver 内部类)

  • implements Runnable
    • Receiver 是一个内部类,实现了 Runnable 接口,这意味着它包含
分享:
扫描分享到社交APP
上一篇
下一篇