杰瑞科技汇

java socket服务器端

核心概念

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

  • Socket (套接字):是网络通信的端点,你可以把它想象成一个电话插座,服务器和客户端都需要一个 Socket 才能进行通信。
  • ServerSocket:这是服务器端使用的“总机”,它负责在指定的端口上监听客户端的连接请求,当一个客户端尝试连接时,ServerSocket 会接受这个请求,并返回一个新的 Socket 对象,用于与该客户端进行一对一的通信。
  • IP 地址 和 端口号:IP 地址标识了网络上的唯一一台计算机,端口号标识了该计算机上的一个特定服务,客户端需要知道服务器的 IP 地址和端口号才能发起连接。
  • 输入流 和 输出流:一旦 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 SimpleServer {
    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;
            // 循环读取客户端发送的数据
            while ((inputLine = in.readLine()) != null) {
                System.out.println("收到客户端消息: " + inputLine);
                // 如果客户端发送 "exit",则关闭连接
                if ("exit".equalsIgnoreCase(inputLine)) {
                    System.out.println("客户端请求退出连接。");
                    break;
                }
                // 将收到的消息回显给客户端
                out.println("服务器回显: " + inputLine);
            }
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("服务器已关闭。");
    }
}

代码解析

  1. ServerSocket serverSocket = new ServerSocket(12345);

    • 创建一个 ServerSocket 实例,并让它监听本机的 12345 端口。
    • 如果端口已被占用,会抛出 IOException
  2. serverSocket.accept();

    • 这是服务器端的关键方法,它会阻塞程序的执行,直到有一个客户端连接到这个端口。
    • 一旦有客户端连接,accept() 方法会返回一个 Socket 对象,这个 Socket 代表了与该客户端的专用连接通道。
  3. new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

    • clientSocket 获取输入流。InputStreamReader 将字节流转换为字符流,BufferedReader 提供了高效的按行读取功能。
  4. new PrintWriter(clientSocket.getOutputStream(), true);

    • clientSocket 获取输出流。
    • PrintWriter 提供了方便的 println() 方法来发送字符串。
    • 第二个参数 true 表示自动刷新,每当调用 println() 方法后,输出缓冲区会自动刷新,确保数据能立即发送出去,这对于实时交互非常重要。
  5. while ((inputLine = in.readLine()) != null)

    • 循环读取客户端发送的每一行数据。readLine() 会在读取到行末的换行符时返回,如果没有数据可读,则会阻塞,当客户端关闭连接时,readLine() 会返回 null,循环结束。

多线程服务器

单线程服务器有很大的局限性,如果一个客户端连接后长时间不发送数据,其他客户端就无法连接,为了解决这个问题,我们需要为每个客户端连接创建一个独立的线程来处理。

代码示例

我们将创建一个 ClientHandler 类来处理每个客户端的逻辑,然后在主服务器循环中为每个新连接启动一个 ClientHandler 线程。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MultiThreadServer {
    // 使用线程池来管理客户端连接线程,避免为每个连接都创建一个新线程
    private static final int THREAD_POOL_SIZE = 10;
    private final ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
    public static void main(String[] args) {
        new MultiThreadServer().start(12345);
    }
    public void start(int port) {
        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);
                threadPool.execute(clientHandler);
            }
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        } finally {
            // 关闭线程池
            threadPool.shutdown();
        }
    }
    // 内部类,用于处理单个客户端的通信
    private static 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("[" + Thread.currentThread().getName() + "] 收到客户端消息: " + inputLine);
                    if ("exit".equalsIgnoreCase(inputLine)) {
                        System.out.println("客户端请求退出连接。");
                        break;
                    }
                    out.println("服务器回显: " + inputLine);
                }
            } catch (IOException e) {
                // 如果客户端异常断开,这里会捕获到异常
                System.err.println("处理客户端时发生错误: " + e.getMessage());
            } finally {
                try {
                    // 确保关闭客户端连接
                    if (clientSocket != null && !clientSocket.isClosed()) {
                        clientSocket.close();
                    }
                    System.out.println("客户端连接已关闭。");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

代码解析

  1. ExecutorServiceExecutors

    • 我们不再直接创建 Thread 对象,而是使用 ExecutorService(线程池)来管理任务。
    • Executors.newFixedThreadPool(THREAD_POOL_SIZE) 创建了一个固定大小的线程池,这可以防止服务器因为海量客户端连接而耗尽系统资源。
    • 当有新连接时,我们创建一个 ClientHandler 任务(实现了 Runnable 接口),然后调用 threadPool.execute() 将其提交给线程池执行。
  2. ClientHandler 内部类

    • 这个类实现了 Runnable 接口,它的 run() 方法包含了处理单个客户端通信的所有逻辑(读取、处理、发送)。
    • 每个客户端连接都会在独立的线程中运行自己的 ClientHandler 实例,互不干扰。
  3. finally

    • finally 块中关闭 clientSocket 是一个非常好的实践,可以确保无论是否发生异常,客户端的连接资源都能被正确释放。

如何测试你的服务器

你需要一个客户端来连接服务器,你可以用 Java 写一个简单的客户端,或者使用任何网络工具,如 telnetnetcat

使用 telnet 测试 (Windows)

  1. 启动服务器:运行上面的 MultiThreadServer.java
  2. 打开命令提示符 (CMD)
  3. 连接服务器:输入 telnet localhost 12345 然后按回车。
    • localhost 是本地回环地址,代表你自己的电脑,如果你的服务器在另一台电脑上,请替换为服务器的 IP 地址。
  4. 发送消息:在 telnet 窗口中输入任何文本,然后按回车,你应该能在服务器的控制台看到收到的消息,同时在 telnet 窗口看到服务器的回显。
  5. 断开连接:输入 exit 然后按回车,客户端会断开连接。

一个简单的 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 = "localhost"; // 或服务器的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()));
            // 用于从控制台读取用户输入
            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);
                // 如果用户输入 "exit",则退出循环
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
                // 读取并打印服务器的响应
                String serverResponse = in.readLine();
                System.out.println("服务器响应: " + serverResponse);
            }
        } catch (UnknownHostException e) {
            System.err.println("不知道的主机: " + hostname);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O 发生错误: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("客户端已关闭。");
    }
}

更进一步的改进

  • 处理对象序列化:上面的例子只能传输字符串,如果需要传输 Java 对象,可以使用 ObjectInputStreamObjectOutputStream,对象必须实现 Serializable 接口。
  • 使用 NIO (New I/O):对于高性能、高并发的服务器,传统的阻塞式 I/O 可能会成为瓶颈,Java NIO 提供了非阻塞 I/O 和选择器等特性,可以更高效地管理大量连接。
  • 框架:在实际项目中,很少有人从零开始写 Socket 服务器,通常会使用成熟的网络框架,如 NettyMinaSpring Boot WebFlux (基于 Reactor),这些框架提供了更高级的抽象、更好的性能和更丰富的功能。

希望这个详细的指南能帮助你理解和使用 Java Socket 编程!

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