凌峰创科服务平台

Java如何搭建socket服务器?

核心概念

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

Java如何搭建socket服务器?-图1
(图片来源网络,侵删)
  1. ServerSocket (服务器套接字): 这是服务器端的“监听器”,它在指定的网络端口上监听客户端的连接请求,一旦有客户端尝试连接,ServerSocket 就会创建一个新的 Socket 对象来与该客户端进行通信。
  2. Socket (套接字): 这是通信的端点,每个客户端连接到服务器后,服务器都会为该连接创建一个 Socket 对象,服务器通过这个 Socket 的输入流读取客户端发来的数据,通过输出流向客户端发送数据。
  3. I/O 流 (输入/输出流):
    • InputStream / OutputStream: 用于原始字节的读写。
    • BufferedReader / PrintWriter (或 DataOutputStream): 用于更方便地读写文本行或格式化数据,我们通常会用它们来包装 Socket 的流。

基础单线程服务器

这是最简单的实现方式,服务器一次只能处理一个客户端连接,处理完一个客户端后才能接受下一个。

特点:

  • 代码简单,易于理解。
  • 缺点: 性能极差,如果前一个客户端长时间不发送数据,后续所有客户端都无法连接。

代码实现

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 BasicSingleThreadServer {
    public static void main(String[] args) {
        // 定义服务器监听的端口号
        int port = 8080;
        // try-with-resources 语句,确保 ServerSocket 被自动关闭
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,正在监听端口 " + port + "...");
            // 阻塞式方法,等待客户端连接,没有客户端连接时,程序会停在这里。
            Socket clientSocket = serverSocket.accept();
            System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
            // 为客户端的输入流和输出流创建包装器
            // BufferedReader 用于读取客户端发送的文本行
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            // PrintWriter 用于向客户端发送文本,autoFlush=true 表示在调用 println 后自动刷新缓冲区
            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 responseMessage = "服务器回复: " + inputLine.toUpperCase();
                out.println(responseMessage);
            }
            // 关闭客户端连接
            System.out.println("与客户端的连接已关闭。");
            // clientSocket 会在 try-with-resources 语句块结束时自动关闭
        } catch (IOException e) {
            System.err.println("服务器发生错误: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("服务器已关闭。");
    }
}

多线程服务器(最常用)

为了解决单线程服务器性能差的问题,我们为每个客户端连接创建一个独立的线程来处理,这样,服务器就可以同时与多个客户端通信。

特点:

Java如何搭建socket服务器?-图2
(图片来源网络,侵删)
  • 并发处理: 可以同时服务多个客户端。
  • 资源利用: 充分利用多核 CPU 的优势。
  • 缺点: 如果有大量并发连接,可能会因为创建过多线程而导致资源耗尽(内存、CPU 切换开销)。

代码实现

我们将服务器逻辑拆分成两部分:

  1. Server 类:负责启动 ServerSocket 并在接收到新连接时,创建一个 ClientHandler 线程。
  2. ClientHandler 类:一个实现了 Runnable 接口的类,专门用于处理单个客户端的通信逻辑。

ClientHandler.java (客户端处理线程)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
 * 客户端处理线程
 */
public class ClientHandler implements Runnable {
    private final 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);
        ) {
            System.out.println("处理新客户端连接: " + clientSocket.getInetAddress().getHostAddress());
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                System.out.println("从 " + clientSocket.getInetAddress().getHostAddress() + " 收到消息: " + inputLine);
                if ("exit".equalsIgnoreCase(inputLine)) {
                    System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 请求退出。");
                    break;
                }
                String responseMessage = "服务器回复: " + inputLine.toUpperCase();
                out.println(responseMessage);
            }
        } catch (IOException e) {
            System.err.println("处理客户端 " + clientSocket.getInetAddress().getHostAddress() + " 时发生错误: " + e.getMessage());
        } finally {
            try {
                // 确保客户端连接被关闭
                if (clientSocket != null && !clientSocket.isClosed()) {
                    clientSocket.close();
                    System.out.println("与客户端 " + clientSocket.getInetAddress().getHostAddress() + " 的连接已关闭。");
                }
            } catch (IOException e) {
                System.err.println("关闭客户端连接时出错: " + e.getMessage());
            }
        }
    }
}

MultiThreadServer.java (主服务器类)

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 多线程 Socket 服务器
 */
public class MultiThreadServer {
    // 使用线程池来管理客户端处理线程,避免无限制创建线程
    private final ExecutorService threadPool = Executors.newCachedThreadPool();
    public static void main(String[] args) {
        int port = 8080;
        MultiThreadServer server = new MultiThreadServer();
        server.start(port);
    }
    public void start(int port) {
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("多线程服务器已启动,正在监听端口 " + port + "...");
            while (!serverSocket.isClosed()) {
                // 阻塞等待客户端连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("接受到新客户端连接: " + clientSocket.getInetAddress().getHostAddress());
                // 为每个客户端连接创建一个任务,并提交给线程池执行
                ClientHandler clientHandler = new ClientHandler(clientSocket);
                threadPool.execute(clientHandler);
            }
        } catch (IOException e) {
            // serverSocket 被外部关闭, accept() 会抛出 SocketException,这是正常的
            if (e.getMessage() != null && e.getMessage().contains("Socket closed")) {
                System.out.println("服务器正在关闭...");
            } else {
                System.err.println("服务器发生错误: " + e.getMessage());
                e.printStackTrace();
            }
        } finally {
            // 优雅关闭线程池
            shutdownThreadPool();
        }
    }
    private void shutdownThreadPool() {
        System.out.println("正在关闭线程池...");
        threadPool.shutdown(); // 停止接受新任务
        // (可选) 可以在这里添加等待现有任务完成的逻辑
        // try {
        //     if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
        //         threadPool.shutdownNow();
        //     }
        // } catch (InterruptedException e) {
        //     threadPool.shutdownNow();
        //     Thread.currentThread().interrupt();
        // }
        System.out.println("线程池已关闭。");
    }
}

更优的方案 - NIO (New I/O)

对于需要处理成千上万个并发连接的高性能场景,传统的阻塞式 I/O (BIO) 多线程模型会消耗大量内存和 CPU,Java NIO (Non-blocking I/O) 提供了更高效的解决方案。

NIO 核心组件:

  1. Channel (通道): 类似于 Stream,但双向的,可以同时进行读写。
  2. Buffer (缓冲区): 数据的容器,所有读写操作都通过 Buffer 进行。
  3. Selector (选择器): 这是 NIO 的核心,一个 Selector 可以同时监控多个 Channel 的状态(如连接、读、写),当某个 Channel 准备好时,Selector 会通知你,从而避免了线程的阻塞等待,极大地提高了效率。

NIO 服务器代码示例

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 {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8080));
        serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
        // 将 ServerSocketChannel 注册到 Selector,监听 "连接" 事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO 服务器已启动,正在监听端口 8080...");
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true) {
            // 阻塞等待,直到至少有一个通道准备好进行 I/O 操作
            selector.select();
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();
            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                // 处理新连接
                if (key.isAcceptable()) {
                    handleAccept(serverSocketChannel, selector);
                }
                // 处理读数据
                if (key.isReadable()) {
                    handleRead(key, buffer);
                }
                iter.remove(); // 处理完后,从集合中移除
            }
        }
    }
    private static void handleAccept(ServerSocketChannel serverSocket, Selector selector) throws IOException {
        SocketChannel clientChannel = serverSocket.accept();
        if (clientChannel != null) {
            clientChannel.configureBlocking(false);
            // 将新的客户端通道注册到 Selector,监听 "读" 事件
            clientChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("新客户端连接: " + clientChannel.getRemoteAddress());
        }
    }
    private static void handleRead(SelectionKey key, ByteBuffer buffer) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        buffer.clear(); // 清空缓冲区,准备读取新数据
        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.limit()];
        buffer.get(data);
        String message = new String(data);
        System.out.println("收到来自 " + clientChannel.getRemoteAddress() + " 的消息: " + message);
        // 简单回写
        String response = "NIO服务器回复: " + message.toUpperCase();
        ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
        clientChannel.write(responseBuffer);
    }
}

总结与对比

特性 单线程服务器 多线程服务器 NIO 服务器
并发能力 差(只能处理1个) 好(可处理多个) 极高(单线程处理成千上万)
资源消耗 高(线程多) (基于少量线程和事件驱动)
实现复杂度 简单 中等 复杂
适用场景 学习、简单测试 中小型应用、并发量适中 高性能、高并发网络服务

如何选择?

  • 初学者或简单应用: 从 单线程服务器 开始,理解基本流程。
  • 大多数实际应用: 多线程服务器 是一个非常好的平衡点,实现相对简单且性能足够,使用 ExecutorService 管理线程是关键。
  • 高性能/高并发服务: NIO 服务器 是不二之选,但代码更复杂,许多成熟的网络框架(如 Netty)底层都基于 NIO。
Java如何搭建socket服务器?-图3
(图片来源网络,侵删)
分享:
扫描分享到社交APP
上一篇
下一篇