杰瑞科技汇

Java socket如何实现点对点通信?

“点对点”(Peer-to-Peer, P2P)在 Socket 编程中通常指的是两个独立的程序(客户端和服务器)直接建立连接,进行双向数据交换,这与“客户端-服务器”模型非常相似,因为本质上任何 TCP 通信都是两个端点之间的直接连接。

Java socket如何实现点对点通信?-图1
(图片来源网络,侵删)

我们将分步实现一个简单的 P2P 聊天程序,它包含两个部分:

  1. 服务器端:监听来自客户端的连接请求。
  2. 客户端:主动连接服务器,并与服务器进行双向通信。

核心概念回顾

在开始编码前,快速回顾一下几个关键的 Java Socket 类:

  1. java.net.ServerSocket:服务器端使用,它监听指定的端口,等待客户端的连接请求,当一个客户端请求连接时,ServerSocket 会创建一个新的 Socket 对象来代表这个连接。
  2. java.net.Socket:客户端和服务器端都使用,它代表一个网络连接的两端,通过 Socket,我们可以获取输入流 (InputStream) 来读取数据,和输出流 (OutputStream) 来发送数据。
  3. java.io.InputStream / java.io.OutputStream:用于在连接上传输原始字节。
  4. java.io.BufferedReader / java.io.PrintWriter:为了方便处理文本数据,我们通常将字节流包装成字符流。BufferedReader 可以高效地读取一行文本,PrintWriter 可以方便地打印各种数据类型并自动处理换行符。

项目结构

我们将创建两个独立的 Java 文件:

P2P-Chat/
├── Server.java
└── Client.java

第一步:实现服务器端

服务器端的工作流程是:

  1. 在指定端口上创建一个 ServerSocket 并开始监听。
  2. 等待客户端连接,这是一个阻塞方法,直到有客户端连接进来。
  3. 一旦客户端连接,获取与该客户端通信的 Socket
  4. 为这个 Socket 创建输入流和输出流。
  5. 在一个无限循环中,不断读取客户端发来的消息,并回显给客户端。
  6. 如果客户端关闭连接,则捕获异常并关闭资源。

Server.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 Server {
    public static void main(String[] args) {
        int port = 12345; // 服务器监听的端口号
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,正在监听端口 " + port + "...");
            // 等待客户端连接,这是一个阻塞方法
            Socket clientSocket = serverSocket.accept();
            System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
            // 为客户端连接创建输入流和输出流
            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);
                // 如果客户端发送 "bye",则结束通信
                if ("bye".equalsIgnoreCase(inputLine)) {
                    System.out.println("客户端请求断开连接。");
                    break;
                }
                // 向客户端回送消息
                out.println("服务器收到: " + inputLine);
            }
        } catch (IOException e) {
            System.out.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("服务器已关闭。");
    }
}

第二步:实现客户端

客户端的工作流程是:

  1. 知道服务器的 IP 地址和端口号。
  2. 创建一个 Socket 并尝试连接到服务器,这也是一个阻塞方法,直到连接成功或失败。
  3. 连接成功后,获取与服务器通信的 Socket
  4. 为这个 Socket 创建输入流和输出流。
  5. 启动一个单独的线程来持续监听服务器发来的消息,这样就不会阻塞主线程发送消息。
  6. 在主线程中,通过控制台读取用户输入,并发送给服务器。
  7. 如果用户输入 "bye",则关闭连接和资源。

Client.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 Client {
    public static void main(String[] args) {
        String hostname = "localhost"; // 服务器地址,如果是本地则为 "localhost"
        int port = 12345;             // 服务器端口号
        try (Socket socket = new Socket(hostname, port)) {
            System.out.println("已成功连接到服务器 " + hostname + ":" + port);
            // 为服务器连接创建输入流和输出流
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // --- 启动一个新线程来接收服务器的消息 ---
            // 这样可以避免在接收消息时阻塞发送消息
            Thread receiveThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        String serverResponse;
                        while ((serverResponse = in.readLine()) != null) {
                            System.out.println("服务器回复: " + serverResponse);
                        }
                    } catch (IOException e) {
                        // 当服务器关闭连接时,in.readLine() 会返回 null,这里会抛出 IOException
                        System.out.println("与服务器连接已断开。");
                    }
                }
            });
            receiveThread.start();
            // --- 主线程用于发送消息 ---
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
            String userInput;
            System.out.println("请输入消息 (输入 'bye' 退出):");
            while ((userInput = stdIn.readLine()) != null) {
                out.println(userInput);
                if ("bye".equalsIgnoreCase(userInput)) {
                    break;
                }
            }
        } catch (UnknownHostException e) {
            System.err.println("无法找到主机: " + hostname);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("无法连接到 " + hostname + ":" + port);
            e.printStackTrace();
        }
        System.out.println("客户端已关闭。");
    }
}

第三步:如何运行

  1. 编译代码:打开终端或命令提示符,进入 P2P-Chat 目录,运行:

    javac Server.java Client.java
  2. 运行服务器:首先启动服务器程序,它会在后台等待连接。

    java Server

    你会看到输出:

    服务器已启动,正在监听端口 12345...
  3. 运行客户端:打开一个新的终端窗口,启动客户端程序。

    java Client

    你会看到输出:

    已成功连接到服务器 localhost:12345
    请输入消息 (输入 'bye' 退出):
  4. 开始聊天

    • 在客户端的终端输入 你好,服务器!,然后按回车。
    • 在服务器的终端,你会看到:
      客户端已连接: 127.0.0.1
      客户端说: 你好,服务器!
    • 在客户端的终端,你会看到服务器的回复:
      服务器回复: 服务器收到: 你好,服务器!
    • 你可以在服务器端输入回复(修改 Server.java 代码,让它也能从控制台读取并发送),或者让客户端继续发送消息。
  5. 结束会话:在客户端输入 bye 并按回车。

    客户端和服务器都会检测到 "bye" 消息,关闭连接,并打印各自的关闭信息。


代码讲解与注意事项

  1. 双向通信与多线程

    • Client.java 中,我们使用了多线程,这是实现双向通信的关键,一个线程负责发送(主线程),另一个线程负责接收(receiveThread),如果只用一个线程,比如先发送后接收,那么程序在执行到 in.readLine() 时会一直等待,导致无法发送新的消息。
    • Server.java 的例子是同步的,它一次只处理一个客户端,并且每次收到消息后才回复,如果要支持多个客户端,需要为每个连接创建一个新线程(或使用线程池)。
  2. 资源管理

    • 我们使用了 try-with-resources 语句(try (Socket ...)),这是一个非常推荐的做法,它能确保 Socket 以及相关的 InputStreamOutputStream 在代码块执行完毕后被自动关闭,即使发生了异常。
  3. readLine() 的阻塞特性

    • BufferedReader.readLine() 方法会一直等待,直到它从流中读取到一行完整的文本(以换行符 \n 或回车换行 \r\n ,如果另一端关闭了连接,readLine() 会返回 null,如果网络中断,它会抛出 IOException
  4. PrintWriterautoFlush

    • 在创建 PrintWriter 时,我们传入了 true 作为第二个参数:new PrintWriter(socket.getOutputStream(), true),这开启了自动刷新功能,当你调用 println() 方法时,输出缓冲区会自动刷新,确保数据被立即发送出去,这对于交互式应用非常重要。

进阶:真正的 P2P(Peer-to-Peer)

上面的例子是经典的“客户端-服务器”模型,在真正的 P2P 网络中,每个节点既是客户端也是服务器,要实现这一点,你需要:

  1. 每个节点都启动一个 ServerSocket,以便其他节点可以主动连接它。
  2. 需要一个“发现机制”,当一个节点想要连接另一个节点时,它必须知道对方的 IP 地址和端口号,这可以通过一个中央服务器(Tracker)来维护所有在线节点的列表,或者通过广播/组播等技术来实现。

这个简单的例子已经为你打下了坚实的基础,理解了这个模型,你就可以进一步构建更复杂的分布式应用了。

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