杰瑞科技汇

Java Socket通信实例如何实现?

核心概念

在开始编码前,我们先理解几个核心概念:

Java Socket通信实例如何实现?-图1
(图片来源网络,侵删)
  1. Socket (套接字):是网络通信的端点,你可以把它想象成一个电话,通过它来发送和接收数据。
  2. IP 地址:网络中设备的唯一标识,就像房子的地址。
  3. 端口号:设备上应用程序的标识,就像房子里的不同房间号,IP 地址 + 端口号 唯一确定了一个网络进程。
  4. 服务器:等待客户端连接,并提供服务的程序,它会创建一个 ServerSocket 来监听特定端口。
  5. 客户端:主动连接到服务器,并请求服务的程序,它会创建一个 Socket 来发起连接。
    • InputStream:用于从网络中读取数据(输入流)。
    • OutputStream:用于向网络中写入数据(输出流)。

简单的单次通信(同步阻塞)

这是最基础的例子,服务器和客户端只进行一次“你问我答”的通信,然后程序结束。

服务器端代码

服务器会监听一个端口,等待客户端连接,一旦客户端连接,它会读取客户端发送的消息,然后回复一条消息。

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;
public class SimpleServer {
    public static void main(String[] args) {
        int port = 12345; // 服务器监听的端口号
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,等待客户端连接...");
            // 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);
            // 读取客户端发送的消息
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                System.out.println("收到客户端消息: " + inputLine);
                // 如果客户端发送 "exit",则关闭连接
                if ("exit".equalsIgnoreCase(inputLine)) {
                    System.out.println("客户端请求退出。");
                    break;
                }
                // 向客户端回复消息
                String response = "服务器已收到你的消息: " + inputLine;
                out.println(response);
                System.out.println("已回复客户端: " + response);
            }
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("服务器已关闭。");
    }
}

客户端代码

客户端会连接到服务器的指定 IP 和端口,发送一条消息,然后读取服务器的回复。

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

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;
public class SimpleClient {
    public static void main(String[] args) {
        String hostName = "127.0.0.1"; // 服务器的IP地址,本地回环地址
        int port = 12345;             // 服务器的端口号
        try (
            // 创建一个Socket连接到指定的服务器和端口
            Socket socket = new Socket(hostName, port);
            // 获取输出流,用于向服务器发送数据
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            // 获取输入流,用于读取服务器发送的数据
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
        ) {
            System.out.println("已连接到服务器 " + hostName + ":" + port);
            // 向服务器发送消息
            String messageToSend = "你好,服务器!";
            out.println(messageToSend);
            System.out.println("已发送消息: " + messageToSend);
            // 读取服务器的回复
            String responseFromServer = in.readLine();
            System.out.println("收到服务器回复: " + responseFromServer);
            // 再次发送消息,触发服务器退出
            out.println("exit");
            System.out.println("已发送退出指令。");
        } 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. 首先编译两个 Java 文件:
    javac SimpleServer.java SimpleClient.java
  2. 先启动服务器
    java SimpleServer

    你会看到控制台输出:服务器已启动,等待客户端连接...

  3. 再启动客户端
    java SimpleClient

    你会看到客户端控制台输出连接和收到的回复。 服务器控制台会显示客户端连接信息和收到的消息。

这个例子的问题是,服务器一次只能处理一个客户端,当第一个客户端连接后,serverSocket.accept() 会阻塞,无法处理其他客户端的连接请求。


多线程服务器

为了解决单线程服务器无法处理多个客户端的问题,我们使用多线程,每当有新的客户端连接时,服务器就创建一个新的线程来专门处理这个客户端的通信。

服务器端代码

MultiThreadServer.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 MultiThreadServer {
    public static void main(String[] args) {
        int port = 12345;
        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());
            e.printStackTrace();
        }
    }
}
// 负责处理单个客户端通信的线程
class ClientHandler implements Runnable {
    private final Socket clientSocket;
    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
    }
    @Override
    public void run() {
        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("[" + clientSocket.getInetAddress() + "] 收到消息: " + inputLine);
                if ("exit".equalsIgnoreCase(inputLine)) {
                    System.out.println("[" + clientSocket.getInetAddress() + "] 客户端请求退出。");
                    break;
                }
                String response = "服务器已收到来自 " + clientSocket.getInetAddress() + " 的消息: " + inputLine;
                out.println(response);
            }
        } catch (IOException e) {
            System.err.println("处理客户端 " + clientSocket.getInetAddress() + " 时发生错误: " + e.getMessage());
        } finally {
            try {
                clientSocket.close();
                System.out.println("[" + clientSocket.getInetAddress() + "] 连接已关闭。");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端代码

客户端代码与实例一完全相同,无需修改。

如何运行

  1. 编译:
    javac MultiThreadServer.java SimpleClient.java
  2. 启动服务器:
    java MultiThreadServer
  3. 启动多个客户端(可以打开多个终端窗口):
    java SimpleClient

    你会发现,服务器可以同时为多个客户端服务,每个客户端的请求和响应都能被正确处理。


基于 NIO 的高性能服务器(进阶)

传统的 Socket I/O 是阻塞式的,一个线程只能处理一个连接,当有成千上万的连接时,创建大量线程会消耗大量内存和 CPU 上下文切换开销。

Java NIO (New I/O) 提供了非阻塞 I/O 模型,核心是 通道缓冲区选择器

  • 通道:类似流,但可以双向读写。
  • 缓冲区:数据读写都必须通过缓冲区。
  • 选择器:允许单个线程监视多个通道的事件(如连接、读、写),当一个或多个通道准备好时,选择器才会通知线程。

这是一个非常经典的 NIO 服务器实现,使用了 Reactor 模式

NIOServer.java

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private final int port = 12346;
    public void start() throws IOException {
        // 1. 创建选择器
        selector = Selector.open();
        // 2. 创建 ServerSocketChannel 并配置为非阻塞模式
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(port));
        // 3. 将 ServerSocketChannel 注册到选择器,监听连接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO服务器已启动,监听端口: " + port);
        // 4. 循环等待新的事件
        while (true) {
            // select() 会阻塞,直到至少有一个通道准备好
            selector.select();
            // 获取所有“已准备好”的 SelectionKey 集合
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                keyIterator.remove(); // 处理后从集合中移除
                try {
                    handleKey(key);
                } catch (IOException e) {
                    if (key != null) {
                        key.cancel();
                        if (key.channel() != null) {
                            key.channel().close();
                        }
                    }
                    System.err.println("处理 SelectionKey 时出错: " + e.getMessage());
                }
            }
        }
    }
    private void handleKey(SelectionKey key) throws IOException {
        // 5. 处理新连接
        if (key.isAcceptable()) {
            ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
            SocketChannel clientChannel = serverChannel.accept();
            clientChannel.configureBlocking(false);
            // 将新的客户端通道注册到选择器,监听读事件
            clientChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("新客户端连接: " + clientChannel.getRemoteAddress());
        }
        // 6. 处理读事件
        if (key.isReadable()) {
            SocketChannel clientChannel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int bytesRead = clientChannel.read(buffer);
            if (bytesRead == -1) {
                // 客户端关闭连接
                System.out.println("客户端断开连接: " + clientChannel.getRemoteAddress());
                key.cancel();
                clientChannel.close();
                return;
            }
            buffer.flip(); // 切换为读模式
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            String message = new String(data);
            System.out.println("收到来自 [" + clientChannel.getRemoteAddress() + "] 的消息: " + message);
            // 回复客户端
            String response = "NIO服务器已收到你的消息: " + message;
            ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
            clientChannel.write(responseBuffer);
        }
    }
    public static void main(String[] args) {
        try {
            new NIOServer().start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

配套的 NIO 客户端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
    public static void main(String[] args) throws IOException {
        String host = "127.0.0.1";
        int port = 12346;
        try (SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(host, port))) {
            socketChannel.configureBlocking(false);
            ByteBuffer sendBuffer = ByteBuffer.wrap("你好,NIO服务器!".getBytes());
            socketChannel.write(sendBuffer);
            System.out.println("消息已发送。");
            ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
            int bytesRead = socketChannel.read(receiveBuffer); // 非阻塞,可能立即返回0
            if (bytesRead > 0) {
                receiveBuffer.flip();
                byte[] data = new byte[receiveBuffer.remaining()];
                receiveBuffer.get(data);
                String response = new String(data);
                System.out.println("收到服务器回复: " + response);
            }
        }
    }
}
特性 实例一 (简单) 实例二 (多线程) 实例三 (NIO)
模型 阻塞I/O 阻塞I/O + 多线程 非阻塞I/O + 单线程(或少量线程)
并发能力 差,只能处理1个客户端 好,可处理多个客户端 极高,可处理成千上万个连接
资源消耗 低(但利用率低) 高(线程开销大) 低(线程数少)
编程复杂度 简单 中等 复杂
适用场景 简单测试、学习 中小型应用,并发量不高 大型应用、高并发、高性能网络服务

对于初学者和大多数常规应用,多线程服务器(实例二) 是一个非常好的选择,它在性能和开发复杂度之间取得了很好的平衡,NIO(实例三)则是构建高性能网络框架(如 Netty)的基础,适合需要处理海量连接的场景。

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