杰瑞科技汇

Java TCP/IP代码如何实现?

目录

  1. TCP/IP 基础回顾
  2. Java TCP 编程核心类
    • java.net.ServerSocket (服务器端)
    • java.net.Socket (客户端 & 服务器端连接)
    • java.io.InputStream / java.io.OutputStream (数据流)
  3. 完整代码示例
      1. TCP 服务器 (SimpleServer.java)
      1. TCP 客户端 (SimpleClient.java)
  4. 代码详细解析
    • 服务器端流程
    • 客户端流程
  5. 关键点与最佳实践
    • 多线程处理 (服务器如何同时服务多个客户端)
    • 异常处理
    • 资源关闭 (非常重要!)
    • 使用 try-with-resources
  6. 如何运行示例
  7. 进阶主题

TCP/IP 基础回顾

在写代码前,快速回顾一下 TCP 的核心特点,这有助于理解代码逻辑:

Java TCP/IP代码如何实现?-图1
(图片来源网络,侵删)
  • 面向连接:通信前必须先通过“三次握手”建立一个稳定的连接。
  • 可靠传输:通过序列号、确认应答、重传机制和流量控制,确保数据无差错、不丢失、不重复且按序到达。
  • 全双工通信:连接建立后,双方可以同时进行数据的发送和接收。
  • 字节流服务:TCP 将应用程序交付的数据看作是一连串的无结构的字节流,它不关心消息的边界,由应用程序自己定义消息的格式(用换行符 \n 作为分隔符)。

Java TCP 编程核心类

Java 使用 java.net 包中的类来简化网络编程。

类/接口 作用 关键方法
ServerSocket 服务器端使用,用于监听客户端的连接请求。 int getLocalPort()
Socket accept() (阻塞方法,等待连接)
void close()
Socket 客户端使用,用于发起连接;服务器端 accept() 返回的 Socket 代表与一个客户端的连接。 InputStream getInputStream()
OutputStream getOutputStream()
void close()
InetAddress getInetAddress()
InputStream / OutputStream 通过 Socket 获取,用于在连接上进行数据的读取和写入。 read() / write() (及其重载方法)
BufferedReader / PrintWriter InputStreamOutputStream 进行包装,提供更方便的字符读写操作。 readLine()
println()

完整代码示例

下面是一个简单的“回显服务器”(Echo Server)和客户端的例子,客户端发送一行文本,服务器原样返回这行文本。

TCP 服务器 (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;
/**
 * 一个简单的 TCP 服务器。
 * 它监听指定端口,等待客户端连接。
 * 当客户端连接后,它会读取客户端发送的数据,并将其回显给客户端。
 */
public class SimpleServer {
    public static void main(String[] args) {
        int port = 8888; // 服务器监听的端口号
        // try-with-resources 语句,确保 ServerSocket 在结束后被自动关闭
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,正在监听端口 " + port + "...");
            // accept() 是一个阻塞方法,它会一直等待直到有客户端连接
            // 当有客户端连接时,accept() 返回一个 Socket 对象,代表与该客户端的连接
            Socket clientSocket = serverSocket.accept();
            System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
            // 为这个客户端连接创建输入流和输出流
            // 使用 try-with-resources 确保 BufferedReader 和 PrintWriter 也会被关闭
            try (
                BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
            ) {
                String inputLine;
                // 循环读取客户端发送的数据
                // readLine() 也是阻塞的,当客户端关闭输出流时,它会返回 null
                while ((inputLine = in.readLine()) != null) {
                    System.out.println("收到客户端消息: " + inputLine);
                    // 将收到的消息回显给客户端
                    out.println("服务器回复: " + inputLine);
                }
            }
            System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 已断开连接。");
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

TCP 客户端 (SimpleClient.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;
/**
 * 一个简单的 TCP 客户端。
 * 它连接到指定的服务器和端口,向服务器发送消息,并接收服务器的回复。
 */
public class SimpleClient {
    public static void main(String[] args) {
        String hostname = "localhost"; // 或服务器的 IP 地址,如 "192.168.1.100"
        int port = 8888;
        // try-with-resources 语句,确保 Socket 和相关流在结束后被自动关闭
        try (
            Socket socket = new Socket(hostname, port);
            // 从服务器读取数据的输入流
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 向服务器发送数据的输出流
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            // 从用户控制台读取输入的 BufferedReader
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
        ) {
            System.out.println("已连接到服务器 " + hostname + ":" + port);
            System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
            String userInput;
            // 循环读取用户从控制台输入
            while ((userInput = stdIn.readLine()) != null) {
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
                // 将用户输入发送给服务器
                out.println(userInput);
                // 读取并打印服务器的回复
                String response = in.readLine();
                System.out.println("服务器回复: " + response);
                System.out.println("请输入下一条消息 (输入 'exit' 退出):");
            }
        } 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("客户端已关闭。");
    }
}

代码详细解析

服务器端流程

  1. 创建 ServerSocket:

    • ServerSocket serverSocket = new ServerSocket(port);
    • 这行代码在指定的 port 上创建一个服务器套接字,并开始监听该端口,如果端口已被占用,会抛出 IOException
  2. 等待连接 (accept):

    Java TCP/IP代码如何实现?-图2
    (图片来源网络,侵删)
    • Socket clientSocket = serverSocket.accept();
    • 这是服务器端的核心。accept() 方法会阻塞(程序暂停执行),直到有一个客户端尝试连接到这个端口,一旦有连接,它会返回一个新的 Socket 对象,这个 Socket 专门用于与这个新连接的客户端进行通信,原来的 serverSocket 继续监听,等待新的客户端连接。
  3. 通信:

    • BufferedReader in = new BufferedReader(...):通过 clientSocket.getInputStream() 获取输入流,用于读取客户端发送过来的数据。
    • PrintWriter out = new PrintWriter(...):通过 clientSocket.getOutputStream() 获取输出流,用于向客户端发送数据。true 参数表示自动刷新(每次调用 println 后都会自动调用 flush())。
    • while ((inputLine = in.readLine()) != null):循环读取客户端发送的每一行数据。readLine() 会一直阻塞,直到客户端发送一行数据(以 \n 或关闭连接(此时返回 null)。
    • out.println(...):将处理后的数据(这里是回显)发送回客户端。
  4. 关闭连接:

    • readLine() 返回 null 时,表示客户端已经关闭了连接,循环结束,try-with-resources 会自动调用 inoutclose() 方法,进而关闭 clientSocket

客户端流程

  1. 创建 Socket 并连接:

    • Socket socket = new Socket(hostname, port);
    • 这行代码会尝试创建一个 Socket 并连接到指定 hostnameport 的服务器,如果连接成功,程序才会继续执行;如果连接失败(如服务器未启动、IP错误),会抛出 IOException,这个连接过程就是 TCP 的“三次握手”。
  2. 通信:

    Java TCP/IP代码如何实现?-图3
    (图片来源网络,侵删)
    • BufferedReader in = new BufferedReader(...):通过 socket.getInputStream() 获取输入流,用于读取服务器的回复。
    • PrintWriter out = new PrintWriter(...):通过 socket.getOutputStream() 获取输出流,用于向服务器发送数据。
    • BufferedReader stdIn = new BufferedReader(...):这是一个特殊的 BufferedReader,它从标准输入(即你的键盘/控制台)读取数据。
    • while ((userInput = stdIn.readLine()) != null):循环读取用户在控制台输入的每一行。
    • out.println(userInput):将用户输入发送给服务器。
    • String response = in.readLine()阻塞,等待并读取服务器发回的一行数据。
    • System.out.println("服务器回复: " + response):将服务器的回复打印到控制台。
  3. 关闭连接:

    • 当用户输入 exit 时,循环结束。try-with-resources 会按相反的顺序自动关闭 stdIn, out, in, 和 socket,关闭 socket 会触发 TCP 的“四次挥手”,断开与服务器的连接。

关键点与最佳实践

多线程处理

上面的 SimpleServer 一次只能处理一个客户端,如果另一个客户端在第一个客户端通信期间连接,它必须等待,在生产环境中,服务器必须能够同时处理多个客户端。

解决方案:每当 accept() 返回一个 clientSocket,就启动一个新的线程来处理这个客户端的通信。

// 在 SimpleServer 的 main 方法中,将通信部分放入一个新线程
// ... (serverSocket.accept() 之后)
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 启动一个新线程来处理这个客户端
new Thread(new ClientHandler(clientSocket)).start();
// ClientHandler 可以是一个实现 Runnable 的类
class ClientHandler implements Runnable {
    private Socket clientSocket;
    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
    }
    @Override
    public void run() {
        // 将原来服务器端通信的代码放在这里
        try (BufferedReader in = new BufferedReader(...); PrintWriter out = new PrintWriter(...)) {
            // ... 通信逻辑 ...
        } catch (IOException e) {
            // ...
        }
    }
}

异常处理

网络编程充满了不确定性(网络延迟、连接中断、端口被占等),所有涉及网络操作的方法(new Socket(), serverSocket.accept(), in.readLine(), out.println())都应该被 try-catch 块包围,妥善处理可能发生的 IOException

资源关闭

这是 Java 编程中最重要也最容易出错的一点!

每个网络连接(Socket)和每个数据流(InputStream, OutputStream)都是系统资源,如果不关闭,会导致资源泄漏,最终可能耗尽系统资源,使程序崩溃。

最佳实践:使用 try-with-resources

从 Java 7 开始,try-with-resources 是推荐的资源管理方式,只要一个类实现了 AutoCloseable 接口(Socket, ServerSocket, InputStream, OutputStream, Reader, Writer 等都实现了),就可以在 try 语句中声明它,当代码块执行完毕后(无论是正常结束还是发生异常),这些资源都会被自动调用 close() 方法关闭,非常安全和方便。

// 正确方式
try (Socket s = new Socket(...); BufferedReader in = ...) {
    // ...
} // s, in 自动关闭
// 错误方式(不要这样做!)
Socket s = null;
BufferedReader in = null;
try {
    s = new Socket(...);
    in = new BufferedReader(...);
    // ...
} finally {
    if (in != null) try { in.close(); } catch (IOException e) {}
    if (s != null) try { s.close(); } catch (IOException e) {}
}
// 手动关闭代码繁琐且容易遗漏

如何运行示例

  1. 保存代码:将上面的两个 Java 类 (SimpleServer.javaSimpleClient.java) 保存到同一个目录下。

  2. 编译:打开终端或命令提示符,进入该目录,运行 javac 命令:

    javac SimpleServer.java SimpleClient.java

    这会生成 SimpleServer.classSimpleClient.class 文件。

  3. 启动服务器:在一个终端窗口中运行服务器:

    java SimpleServer

    你会看到输出:服务器已启动,正在监听端口 8888...

  4. 启动客户端:在另一个终端窗口中运行客户端:

    java SimpleClient

    你会看到输出:已连接到服务器 localhost:8888请输入要发送的消息 (输入 'exit' 退出):

  5. 交互

    • 在客户端的终端输入任意文本,你好,服务器!,然后按回车。
    • 客户端会立即收到服务器的回复:服务器回复: 你好,服务器!
    • 服务器的终端也会显示:收到客户端消息: 你好,服务器!
    • 你可以继续输入消息进行交互,当客户端输入 exit 并回车时,客户端程序会退出,服务器的对应连接也会断开。

进阶主题

  • NIO (New I/O):对于高并发、高性能的服务器,传统的 I/O(阻塞式)模型效率不高,Java NIO 提供了非阻塞 I/O 和选择器(Selector)机制,允许一个线程管理多个连接,极大地提高了服务器的吞吐量。java.nio.channels 包是其核心。
  • Netty:一个成熟的、高性能的异步事件驱动的网络框架,基于 NIO,它极大地简化了网络服务器和客户端的开发,是目前业界最流行的 Java 网络编程框架之一。
  • 协议设计:上面的例子使用简单的文本行,在实际应用中,你需要设计自己的应用层协议,例如使用长度前缀(Length-Prefix)来明确消息的边界,或者使用像 Protobuf、JSON、XML 这样的序列化格式来结构化你的数据。
分享:
扫描分享到社交APP
上一篇
下一篇