杰瑞科技汇

Java Socket通信实例如何实现?

核心概念

在开始之前,我们先快速回顾一下 Socket 编程的基本概念:

Java Socket通信实例如何实现?-图1
(图片来源网络,侵删)
  1. 服务器端:

    • ServerSocket: 服务器端使用它来“监听”某个特定的端口,等待客户端的连接请求。
    • Socket: 当一个客户端连接成功后,ServerSocket 会返回一个新的 Socket 实例,这个 Socket 代表了与那个特定客户端的通信链路。
  2. 客户端:

    • Socket: 客户端使用 Socket 来主动尝试连接服务器的 IP 地址和端口号。
  3. 通信流:

    • 一旦连接建立,双方就可以通过 Socket 获取输入流和输出流来进行数据的读写。
    • InputStream (用于读取数据) 和 OutputStream (用于发送数据) 是字节流,通常我们会把它们包装成更高级的流,如 InputStreamReader / OutputStreamWriter (处理字符) 和 BufferedReader / BufferedWriter (提供缓冲功能,提高效率)。

简单的单线程回显服务器

这个例子非常基础,服务器一次只能处理一个客户端的连接,当它处理当前客户端时,其他客户端必须等待。

Java Socket通信实例如何实现?-图2
(图片来源网络,侵删)

服务器端代码 (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) {
        // 定义服务器要监听的端口号
        int port = 12345;
        try (// 创建一个 ServerSocket,并绑定到指定的端口
             ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,正在监听端口 " + port + "...");
            // 使用 accept() 方法阻塞,等待客户端连接
            // 当有客户端连接时,accept() 方法会返回一个 Socket 对象
            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);
            String inputLine;
            // 循环读取客户端发送的数据
            // readLine() 也会阻塞,直到收到一行数据或流关闭
            while ((inputLine = in.readLine()) != null) {
                System.out.println("收到客户端消息: " + inputLine);
                // 将收到的消息回显给客户端
                out.println("服务器回显: " + inputLine);
                // 如果客户端发送 "exit",则关闭连接
                if ("exit".equalsIgnoreCase(inputLine)) {
                    System.out.println("客户端请求关闭连接。");
                    break;
                }
            }
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("服务器已关闭。");
    }
}

客户端代码 (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) {
        // 服务器的 IP 地址 (localhost 表示本机) 和端口号
        String hostName = "localhost";
        int port = 12345;
        try (// 创建一个 Socket 连接到指定的服务器地址和端口
             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("已连接到服务器。");
            System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
            String userInput;
            // 循环读取用户输入
            while ((userInput = stdIn.readLine()) != null) {
                // 将用户输入发送给服务器
                out.println(userInput);
                // 读取服务器返回的回显消息
                String response = in.readLine();
                System.out.println("服务器响应: " + response);
                // 如果用户输入 "exit",则退出循环
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
            }
        } catch (UnknownHostException e) {
            System.err.println("无法找到主机: " + hostName);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O 发生异常: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("客户端已关闭。");
    }
}

如何运行

  1. 编译代码:
    javac EchoServer.java EchoClient.java
  2. 启动服务器:
    java EchoServer

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

  3. 启动客户端: 打开一个新的终端窗口,运行:
    java EchoClient

    你会看到客户端输出:已连接到服务器。

  4. 测试通信:
    • 在客户端的终端输入任意文本,Hello, Server!,然后按回车。
    • 客户端会收到服务器的回显:服务器响应: 服务器回显: Hello, Server!
    • 在服务器端,你会看到:收到客户端消息: Hello, Server!
    • 输入 exit 即可关闭连接。

多线程服务器

上面的例子有一个很大的问题:服务器一次只能为一个客户端服务,当服务器与一个客户端通信时,其他客户端必须排队等待,为了解决这个问题,我们可以使用多线程。

服务器为每一个连接的客户端都创建一个新的线程来处理通信,主线程则继续等待新的客户端连接。

多线程服务器端代码 (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 = 12345;
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("多线程服务器已启动,正在监听端口 " + port + "...");
            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());
            e.printStackTrace();
        }
    }
}
// 定义一个内部类,用于处理单个客户端的通信逻辑
class ClientHandler implements Runnable {
    private Socket clientSocket;
    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
    }
    @Override
    public void run() {
        // 使用 try-with-resources 确保流和 socket 被正确关闭
        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().getId() + "] 收到客户端 " + clientSocket.getInetAddress().getHostAddress() + " 的消息: " + inputLine);
                out.println("服务器回显: " + inputLine);
                if ("exit".equalsIgnoreCase(inputLine)) {
                    System.out.println(" [线程 " + Thread.currentThread().getId() + "] 客户端请求关闭连接。");
                    break;
                }
            }
        } catch (IOException e) {
            // 如果客户端异常断开,会抛出 SocketException,这里可以捕获并打印信息
            System.out.println(" [线程 " + Thread.currentThread().getId() + "] 与客户端 " + clientSocket.getInetAddress().getHostAddress() + " 的连接已断开。");
        } finally {
            try {
                clientSocket.close();
                System.out.println(" [线程 " + Thread.currentThread().getId() + "] 已关闭与客户端的连接。");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端代码

客户端代码与实例一完全相同,无需修改,你可以直接使用 EchoClient.java

如何运行

  1. 编译代码:
    javac MultiThreadEchoServer.java EchoClient.java
  2. 启动多线程服务器:
    java MultiThreadEchoServer
  3. 启动多个客户端: 打开两个或多个新的终端窗口,在每个窗口中都运行:
    java EchoClient
  4. 测试:
    • 在第一个客户端窗口输入消息,服务器端会显示一个线程ID。
    • 在第二个客户端窗口输入消息,服务器端会显示另一个线程ID。
    • 你会发现,服务器可以同时响应来自不同客户端的消息,而不会互相阻塞,这就是多线程服务器的优势。

总结与最佳实践

特性 单线程服务器 多线程服务器
并发处理 不能,一次只处理一个客户端 能,每个客户端一个线程
资源消耗 低,只有一个线程 高,每个连接一个线程
实现复杂度 简单 稍复杂,需要管理线程
适用场景 简单应用、学习目的 高并发、生产环境应用

进阶建议:

  • 线程池: 在实际生产环境中,直接为每个连接创建新线程是非常消耗资源的,更好的方式是使用线程池 (ExecutorService) 来复用线程,避免频繁创建和销毁线程带来的开销。
  • NIO (New I/O): 对于更高性能、更高并发的场景,可以使用 Java NIO (非阻塞 I/O) 模型,NIO 使用一个或少数几个线程来管理成千上万的连接,效率远高于传统的多线程模型,但这会显著增加代码的复杂性。
  • 使用框架: 对于复杂的网络应用,直接使用原生 Socket API 开发会非常繁琐,可以考虑使用成熟的网络框架,如 NettyMinaVert.x,它们提供了更高级的抽象和更强大的功能,能让你更专注于业务逻辑。

希望这两个实例能帮助你理解 Java Socket 通信的基本原理!

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