杰瑞科技汇

Java 客户端 Socket 如何实现连接通信?

核心概念:Socket 是什么?

在计算机网络中,Socket (套接字) 是通信的端点,你可以把它想象成一个电话,通过这个电话,你的程序(客户端)可以和另一个程序(服务器)进行双向的数据交换。

Java 客户端 Socket 如何实现连接通信?-图1
(图片来源网络,侵删)

Java 使用 java.net 包中的类来处理网络通信,对于客户端,最核心的类是 Socket

一个简单的比喻:

  • 客户端 (Client):就像你拿起电话,拨打一个号码(服务器的 IP 地址和端口号)。
  • 服务器 (Server):就像电话另一端的人,已经接通了电话,准备与你通话。
  • IP 地址 (IP Address):电话号码,用来在网络中唯一标识一台服务器。
  • 端口号 (Port Number):分机号,因为一台服务器上可能同时运行多个服务,端口号用来区分具体是哪个服务在等待连接。
  • 输入/输出流 (Input/Output Stream):就像电话的听筒,一个用于接收(输入)对方说的话,一个用于发送(输出)你的话。

Java Socket 客户端编程步骤

创建一个 Java Socket 客户端通常遵循以下固定步骤:

  1. 创建 Socket 对象:指定服务器的 IP 地址和端口号,尝试连接服务器,如果服务器未开启或地址错误,这里会抛出异常。
  2. 获取输入/输出流:连接成功后,通过 Socket 对象获取 InputStreamOutputStream(或更方便的 InputStreamReader/OutputStreamWriterBufferedReader/PrintWriter)。
  3. 进行数据交换
    • 通过 OutputStream 向服务器发送数据。
    • 通过 InputStream 从服务器接收数据。
  4. 关闭资源:通信结束后,按照“后开先关”的原则,依次关闭流和 Socket 对象,释放系统资源。

核心 API 介绍

  • Socket(String host, int port)
    • 创建一个流套接字并将其连接到指定主机上的指定端口号。
    • host:服务器的主机名或 IP 地址("127.0.0.1""localhost")。
    • port:服务器正在监听的端口号(8080)。
  • InputStream getInputStream()

    返回此套接字的输入流,你可以从中读取服务器发送的数据。

    Java 客户端 Socket 如何实现连接通信?-图2
    (图片来源网络,侵删)
  • OutputStream getOutputStream()

    返回此套接字的输出流,你可以通过它向服务器写入数据。

  • void close()

    关闭此套接字,一旦关闭,它就不能再使用。

为了方便操作字符流,我们通常会使用包装类:

  • InputStreamReader: 将字节输入流 (InputStream) 转换为字符输入流。
  • OutputStreamWriter: 将字节输出流 (OutputStream) 转换为字符输出流。
  • BufferedReader: 为字符输入流提供缓冲,可以方便地按行读取 (readLine())。
  • PrintWriter: 为字符输出流提供便捷的打印方法 (println(), print()),并可以自动刷新缓冲区。

完整代码示例:一个简单的回显客户端

这个客户端会连接到一个回显服务器(服务器会将收到的任何消息原样返回给客户端),向服务器发送一行文本,然后接收并打印服务器的回复。

Java 客户端 Socket 如何实现连接通信?-图3
(图片来源网络,侵删)

客户端代码 (EchoClient.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 EchoClient {
    public static void main(String[] args) {
        // 服务器地址和端口
        String host = "localhost"; // 或 "127.0.0.1"
        int port = 8080;
        // try-with-resources 语句,可以自动关闭资源
        try (
            // 1. 创建 Socket 对象,连接服务器
            Socket socket = new Socket(host, port);
            // 2. 获取输入输出流,并包装成更易用的字符流
            // PrintWriter 用于向服务器发送数据,autoFlush=true 自动刷新缓冲区
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            // BufferedReader 用于从服务器接收数据
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // BufferedReader 用于从控制台读取用户输入
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
        ) {
            System.out.println("已连接到服务器 " + host + ":" + port);
            System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
            String userInput;
            // 3. 进行数据交换
            while ((userInput = stdIn.readLine()) != null) {
                // 发送消息到服务器
                out.println(userInput);
                // 如果用户输入 'exit',则退出循环
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
                // 从服务器接收回复
                String response = in.readLine();
                System.out.println("服务器回复: " + response);
            }
        } catch (UnknownHostException e) {
            System.err.println("找不到主机: " + host);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O 发生错误: " + e.getMessage());
            e.printStackTrace();
        }
        // 4. try-with-resources 会自动关闭 socket 和所有流
        System.out.println("客户端已断开连接。");
    }
}

如何运行这个示例?

  1. 你需要一个配套的服务器,下面是一个非常简单的回显服务器代码,你可以将它保存为 EchoServer.java 并在另一个终端或 IDE 中运行。

    // EchoServer.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;
    public class EchoServer {
        public static void main(String[] args) throws IOException {
            int port = 8080;
            try (ServerSocket serverSocket = new ServerSocket(port)) {
                System.out.println("服务器正在监听端口 " + port + "...");
                // 等待客户端连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("客户端已连接: " + clientSocket.getInetAddress());
                try (
                    PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
                    BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))
                ) {
                    String inputLine;
                    // 读取客户端发送的每一行数据
                    while ((inputLine = in.readLine()) != null) {
                        System.out.println("收到客户端消息: " + inputLine);
                        // 将消息回显给客户端
                        out.println(inputLine);
                    }
                }
            }
            System.out.println("服务器已关闭。");
        }
    }
  2. 运行步骤

    • 先编译并运行 EchoServer
    • 然后编译并运行 EchoClient
    • 在客户端的控制台输入任何文本,按回车,你会在客户端看到服务器的回显,输入 exit 退出。

进阶话题

1 多线程客户端

上面的示例是同步的,客户端在等待服务器回复时会阻塞,在实际应用中,一个客户端通常需要同时处理发送和接收,这时,我们可以使用多线程。

  • 一个线程(主线程):负责从 System.in 读取用户输入,并发送给服务器。
  • 另一个线程(新线程):负责从 Socket.getInputStream() 读取服务器发送来的消息,并打印到控制台。

多线程客户端示例 (MultiThreadEchoClient.java)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class MultiThreadEchoClient {
    public static void main(String[] args) {
        String host = "localhost";
        int port = 8080;
        try (Socket socket = new Socket(host, port)) {
            System.out.println("已连接到服务器 " + host + ":" + port);
            // 发送线程
            Thread sendThread = new Thread(new SendTask(socket));
            // 接收线程
            Thread receiveThread = new Thread(new ReceiveTask(socket));
            sendThread.start();
            receiveThread.start();
            // 等待发送线程结束(即用户输入 'exit')
            sendThread.join();
            // 然后关闭 socket,这会导致接收线程的 readLine() 抛出异常并结束
            socket.close();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}
// 负责发送数据的任务
class SendTask implements Runnable {
    private final Socket socket;
    public SendTask(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        try (PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
            String userInput;
            System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
            while ((userInput = stdIn.readLine()) != null) {
                out.println(userInput);
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
            }
        } catch (IOException e) {
            // socket 被关闭,这里会抛出异常,是正常的
            // System.err.println("发送线程出错: " + e.getMessage());
        }
    }
}
// 负责接收数据的任务
class ReceiveTask implements Runnable {
    private final Socket socket;
    public ReceiveTask(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
            String response;
            while ((response = in.readLine()) != null) {
                System.out.println("服务器回复: " + response);
            }
        } catch (IOException e) {
            // socket 被关闭,这里会抛出异常,是正常的
            // System.err.println("接收线程出错: " + e.getMessage());
        }
    }
}

2 异步 I/O (NIO - New I/O)

对于高性能、高并发的网络应用,传统的阻塞式 I/O (BIO) 效率很低,Java NIO 提供了非阻塞 I/O 的能力,它使用通道 (Channel)缓冲区 (Buffer)选择器 (Selector) 来实现。

  • Channel:类似流,但可以双向读写。
  • Buffer:数据被写入缓冲区,然后从缓冲区读取。
  • Selector:允许一个线程监视多个通道的事件(如连接、数据到达等),当某个通道有事件发生时,Selector 会通知线程进行处理。

NIO 的学习曲线比 BIO 更陡峭,但性能优势巨大,是实现高性能网络服务器(如聊天室、游戏服务器)的基础。


特性 同步阻塞式 (BIO) 多线程 BIO 异步非阻塞式 (NIO)
模型 一连接一线程 一连接一线程,但分离了收发 一个线程管理多个连接
优点 编程简单,逻辑清晰 能同时处理收发,响应性好 高并发,高吞吐,资源占用低
缺点 性能差,无法处理并发 线程创建和切换开销大 编程模型复杂,学习成本高
适用场景 简单工具、学习示例 中小型应用,对并发要求不高 大型、高性能网络应用

对于初学者,掌握同步阻塞式 (BIO) 客户端是第一步,理解其原理后,再学习多线程模型,最后根据需要探索 NIO,希望这份详细的指南能帮助你理解 Java Socket 客户端编程!

分享:
扫描分享到社交APP
上一篇
下一篇