杰瑞科技汇

Java TCP Socket编程如何实现可靠通信?

Java 提供了 java.net 包来支持网络编程,其中核心类包括:

Java TCP Socket编程如何实现可靠通信?-图1
(图片来源网络,侵删)
  • ServerSocket: 用于服务器端,监听客户端的连接请求。
  • Socket: 用于客户端,或者服务器端与客户端建立连接后,用于双向通信。

下面我将通过一个完整的、可运行的 “回显服务器”(Echo Server)客户端 的例子,来逐步讲解 TCP 编程的流程。


核心概念

  1. 服务器端:

    • 创建一个 ServerSocket 并绑定到一个特定的端口(8888),开始监听客户端连接。
    • 调用 accept() 方法,这是一个阻塞方法,它会一直等待,直到有客户端连接上来,一旦有客户端连接,accept() 方法会返回一个 Socket 对象,这个 Socket 代表了与那个客户端的连接通道。
    • 通过这个 Socket 对象的 getInputStream()getOutputStream() 获取输入流和输出流,用于读取客户端发送的数据和向客户端发送数据。
    • 通信结束后,关闭 SocketServerSocket
  2. 客户端:

    • 创建一个 Socket 对象,指定服务器的 IP 地址和端口号,这会尝试连接到服务器,Socket 的构造方法也是阻塞的,直到连接成功或失败。
    • 连接成功后,同样通过 getInputStream()getOutputStream() 获取输入流和输出流。
    • 通过输出流向服务器发送数据,通过输入流读取服务器返回的数据。
    • 通信结束后,关闭 Socket

服务器端代码

这个服务器会接收客户端发送的任何字符串,然后原封不动地发送回去。

Java TCP Socket编程如何实现可靠通信?-图2
(图片来源网络,侵删)
// EchoServer.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
    public static void main(String[] args) {
        int port = 8888; // 服务器监听的端口号
        // try-with-resources 语句,可以自动关闭资源
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,等待客户端连接...");
            System.out.println("服务器地址: " + InetAddress.getLocalHost().getHostAddress() + ":" + port);
            // accept() 方法会阻塞,直到有客户端连接
            Socket clientSocket = serverSocket.accept();
            System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
            // 获取输入流,用于读取客户端发送的数据
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            // 获取输出流,用于向客户端发送数据
            PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); // autoFlush=true
            String inputLine;
            // 循环读取客户端发送的数据
            while ((inputLine = in.readLine()) != null) {
                System.out.println("收到客户端消息: " + inputLine);
                // 将收到的消息回显给客户端
                out.println("服务器回显: " + inputLine);
                // 如果客户端发送 "bye",则退出循环
                if ("bye".equalsIgnoreCase(inputLine)) {
                    System.out.println("客户端请求断开连接。");
                    break;
                }
            }
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("服务器已关闭。");
    }
}

代码解释:

  • ServerSocket serverSocket = new ServerSocket(port): 在指定端口创建服务器套接字。
  • serverSocket.accept(): 阻塞,等待客户端连接,返回一个 Socket 对象,代表与客户端的连接。
  • new BufferedReader(new InputStreamReader(clientSocket.getInputStream())): 从 Socket 的输入流中读取数据。InputStreamReader 将字节流转换为字符流,BufferedReader 提供了高效的按行读取方法 (readLine())。
  • new PrintWriter(clientSocket.getOutputStream(), true): 从 Socket 的输出流中写入数据。PrintWriter 提供了方便的打印方法(如 println()),第二个参数 true 表示自动刷新,即每次调用 println() 后都会自动调用 flush(),确保数据被立即发送。
  • while ((inputLine = in.readLine()) != null): 循环读取客户端发送的每一行文本,当客户端关闭连接时,readLine() 会返回 null,从而退出循环。

客户端代码

这个客户端可以连接到服务器,从控制台读取用户输入,发送给服务器,并打印服务器的回显。

// 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 hostname = "localhost"; // 服务器地址,本地运行用 "localhost"
        int port = 8888;             // 服务器端口
        // try-with-resources 语句,可以自动关闭资源
        try (
            Socket socket = new Socket(hostname, port);
            // 从标准输入读取用户输入
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
            // 从Socket获取输入流,读取服务器返回的数据
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 从Socket获取输出流,向服务器发送数据
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
        ) {
            System.out.println("已连接到服务器 " + hostname + ":" + port);
            System.out.println("请输入要发送的消息 (输入 'bye' 退出):");
            String userInput;
            // 循环读取用户输入
            while ((userInput = stdIn.readLine()) != null) {
                // 将用户输入发送给服务器
                out.println(userInput);
                // 从服务器读取回显
                String response = in.readLine();
                System.out.println("服务器回显: " + response);
                // 如果用户输入 "bye",则退出循环
                if ("bye".equalsIgnoreCase(userInput)) {
                    break;
                }
                System.out.println("请输入下一行消息 (输入 'bye' 退出):");
            }
        } catch (UnknownHostException e) {
            System.err.println("不知道的主机: " + hostname);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O Error: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("客户端已关闭。");
    }
}

代码解释:

  • new Socket(hostname, port): 尝试连接到指定主机和端口,这是一个阻塞操作,直到连接建立或抛出异常。
  • stdIn = new BufferedReader(new InputStreamReader(System.in)): 创建一个 BufferedReader 来读取用户在控制台的输入。
  • in = new BufferedReader(new InputStreamReader(socket.getInputStream())): 读取服务器返回的数据。
  • out = new PrintWriter(socket.getOutputStream(), true): 向服务器发送数据。
  • while ((userInput = stdIn.readLine()) != null): 循环读取用户输入,并发送给服务器,然后读取并打印服务器的响应。

如何运行

  1. 编译代码:

    Java TCP Socket编程如何实现可靠通信?-图3
    (图片来源网络,侵删)
    javac EchoServer.java EchoClient.java
  2. 启动服务器: 打开一个终端窗口,运行服务器,它会进入阻塞状态,等待连接。

    java EchoServer

    你会看到输出:

    服务器已启动,等待客户端连接...
    服务器地址: 127.0.0.1:8888
  3. 启动客户端: 打开另一个终端窗口,运行客户端。

    java EchoClient

    你会看到输出:

    已连接到服务器 localhost:8888
    请输入要发送的消息 (输入 'bye' 退出):
  4. 进行通信:

    • 在客户端的终端输入 Hello Server,然后按回车。
    • 客户端会显示:
      服务器回显: Hello Server
    • 服务器端的终端会显示:
      客户端已连接: 127.0.0.1
      收到客户端消息: Hello Server
    • 继续输入更多消息进行测试。
  5. 关闭连接:

    • 在客户端输入 bye,然后按回车。
    • 客户端和服务器都会打印退出信息,并各自关闭程序。

重要进阶概念

1 多线程处理并发连接

上面的服务器一次只能处理一个客户端连接,当 accept() 阻塞时,它无法为新的客户端服务,为了处理多个并发客户端,服务器需要为每个客户端连接创建一个独立的线程。

改进后的多线程服务器代码:

// MultiThreadEchoServer.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 MultiThreadEchoServer {
    public static void main(String[] args) {
        int port = 8888;
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("多线程服务器已启动,等待客户端连接...");
            while (true) { // 无限循环,持续接受新连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("新客户端连接: " + clientSocket.getInetAddress().getHostAddress());
                // 为每个客户端连接创建一个新的线程来处理
                ClientHandler clientHandler = new ClientHandler(clientSocket);
                new Thread(clientHandler).start();
            }
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
        }
    }
}
// 客户端处理任务类
class ClientHandler implements Runnable {
    private final Socket clientSocket;
    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
    }
    @Override
    public void run() {
        // try-with-resources 确保流被关闭
        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("[" + Thread.currentThread().getName() + "] 收到客户端消息: " + inputLine);
                out.println("服务器回显: " + inputLine);
                if ("bye".equalsIgnoreCase(inputLine)) {
                    System.out.println("客户端请求断开连接。");
                    break;
                }
            }
        } catch (IOException e) {
            System.err.println("处理客户端时出错: " + e.getMessage());
        } finally {
            try {
                clientSocket.close();
                System.out.println("与客户端的连接已关闭。");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

这个版本的服务器主线程只负责 accept() 新连接,然后将每个连接交给一个 ClientHandler 线程去处理,从而实现了并发。

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

对于需要处理成千上万个并发连接的高性能场景,传统的阻塞式 I/O 和多线程模型(一个线程连接)会消耗大量资源,Java NIO 提供了非阻塞的 I/O 模型,它使用选择器通道,可以用一个或几个线程来管理成千上万个连接,大大提高了系统的可伸缩性和性能,这是一个更高级的话题,建议在掌握了 BIO 之后进行学习。

特性 服务器端 客户端
创建套接字 ServerSocket(port) Socket(host, port)
等待/建立连接 serverSocket.accept() (阻塞) new Socket() (阻塞)
获取 I/O 流 socket.getInputStream() / getOutputStream() socket.getInputStream() / getOutputStream()
关闭资源 关闭 SocketServerSocket 关闭 Socket

这个例子涵盖了 Java TCP Socket 编程的核心知识点,从基础的单连接模型到实用的多线程模型,希望能帮助你快速上手。

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