杰瑞科技汇

Java如何调用socket接口?

  1. 核心概念:了解 Socket 和 ServerSocket 的基本作用。
  2. 通信流程:一个典型的客户端-服务器通信步骤。
  3. 代码实现:分别提供客户端和服务器端的完整、可运行的 Java 代码。
  4. 进阶主题:多线程处理、IO 流的选择(BIO vs NIO)。
  5. 异常处理和资源关闭:非常重要的实践。

核心概念

在 Java 中,Socket 编程主要涉及两个核心类:

Java如何调用socket接口?-图1
(图片来源网络,侵删)
  • java.net.Socket:代表一个客户端,当你想连接到一个服务器时,你需要创建一个 Socket 对象,它封装了通信的端点,包含了 IP 地址(或域名)和端口号。
  • java.net.ServerSocket:代表一个服务器端,服务器用它来“监听”某个特定的端口,等待客户端的连接请求,当一个客户端连接成功时,ServerSocket 会返回一个新的 Socket 对象,专门用于与这个客户端进行通信。

一个简单的比喻:

  • ServerSocket 就像一家餐厅的前台,它坐在一个固定的位置(端口),等待客人(客户端)到来。
  • Socket 就像一位客人,客人知道餐厅的地址(IP)和前台的位置(端口),于是过去敲门。
  • 当前台(ServerSocket)听到敲门声,它会指派一位服务员(一个新的 Socket)来专门为这位客人服务。
  • 客人和服务员之间通过“说”和“听”(输入/输出流)来进行交流。

通信流程

一个典型的 TCP Socket 通信流程如下:

服务器端:

  1. 创建 ServerSocket 实例,并绑定一个端口号。
  2. 调用 accept() 方法,开始阻塞监听,等待客户端连接。
  3. 当客户端连接成功,accept() 返回一个新的 Socket 实例。
  4. 通过这个新的 Socket 实例,获取输入流和输出流。
  5. 通过输入流读取客户端发送的数据,通过输出流向客户端发送数据。
  6. 通信结束后,关闭相关的流和 Socket

客户端:

Java如何调用socket接口?-图2
(图片来源网络,侵删)
  1. 创建 Socket 实例,指定服务器的 IP 地址和端口号。
  2. 连接建立后,通过 Socket 实例获取输入流和输出流。
  3. 通过输出流向服务器发送数据,通过输入流读取服务器返回的数据。
  4. 通信结束后,关闭相关的流和 Socket

代码实现

下面是一个最简单的 "Echo"(回显)服务器和客户端的例子,客户端发送一句话,服务器原样返回这句话。

服务器端代码 (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-with-resources 语句,可以自动关闭资源
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,正在监听端口 " + port + "...");
            // 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);
                // 将收到的消息回显给客户端
                out.println("服务器回显: " + inputLine);
                // 如果客户端发送 "bye",则结束通信
                if ("bye".equalsIgnoreCase(inputLine)) {
                    break;
                }
            }
            System.out.println("客户端已断开连接。");
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

客户端代码 (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;
import java.util.Scanner;
public class EchoClient {
    public static void main(String[] args) {
        // 服务器的 IP 地址(如果是本地测试,可以用 "localhost" 或 "127.0.0.1")
        String host = "localhost";
        // 服务器监听的端口号,必须与服务器端一致
        int port = 12345;
        // try-with-resources 语句
        try (Socket socket = new Socket(host, port);
             // 用于从服务器读取数据
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             // 用于向服务器发送数据
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             // 从控制台读取用户输入
             Scanner scanner = new Scanner(System.in)) {
            System.out.println("已连接到服务器 " + host + ":" + port);
            System.out.println("请输入要发送的消息 (输入 'bye' 退出):");
            // 循环读取用户输入并发送给服务器
            while (true) {
                String userInput = scanner.nextLine();
                // 发送消息到服务器
                out.println(userInput);
                // 如果用户输入 "bye",则退出循环
                if ("bye".equalsIgnoreCase(userInput)) {
                    break;
                }
                // 读取服务器返回的回显消息
                String response = in.readLine();
                System.out.println("服务器响应: " + response);
            }
        } catch (UnknownHostException e) {
            System.err.println "未知的主机: " + host);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O 发生错误: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("客户端已关闭。");
    }
}

如何运行

  1. 启动服务器

    • 编译并运行 EchoServer.java
    • 控制台会显示:服务器已启动,正在监听端口 12345...,然后它会停在那里等待连接。
  2. 启动客户端

    • 编译并运行 EchoClient.java
    • 客户端控制台会显示:已连接到服务器 localhost:12345
    • 你可以在客户端输入任何文字,"Hello, Server!",然后按回车。
    • 客户端会立即收到服务器的回显:服务器响应: 服务器回显: Hello, Server!
    • 当你在客户端输入 "bye" 并回车后,客户端和服务器都会关闭连接,程序结束。

进阶主题

1 多线程处理服务器

上面的服务器一次只能处理一个客户端,如果多个客户端同时连接,后面的客户端必须等待前面的客户端断开,为了解决这个问题,服务器需要为每个客户端连接创建一个新的线程来处理。

改进后的服务器端 (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 handler = new ClientHandler(clientSocket);
                new Thread(handler).start();
            }
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
        }
    }
}
// 客户端处理任务,实现 Runnable 接口
class ClientHandler implements Runnable {
    private 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);
                out.println("服务器回显: " + inputLine);
                if ("bye".equalsIgnoreCase(inputLine)) {
                    break;
                }
            }
        } catch (IOException e) {
            // 如果客户端异常断开,这里会抛出异常,是正常现象
            System.out.println("客户端 " + clientSocket.getInetAddress() + " 已断开或发生错误。");
        } finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

2 IO 流的选择:BIO vs NIO

  • BIO (Blocking I/O - 阻塞式IO):上面我们使用的就是 BIO,当调用 read()accept() 时,线程会一直阻塞,直到有数据可读或有连接到来,这会导致线程资源浪费,在传统多线程模型下,高并发时性能会急剧下降。
  • NIO (New I/O / Non-blocking I/O - 非阻塞式IO):Java NIO 引入了 Channel(通道)、Buffer(缓冲区)和 Selector(选择器)。
    • Channel 类似于 Stream,但可以双向读写。
    • Buffer 是数据容器,所有读写都必须通过 Buffer。
    • Selector 允许一个线程管理多个 Channel,它会把多个 Channel 注册到 Selector 上,然后在一个循环中查询哪些 Channel 有事件(如连接就绪、数据可读),从而实现非阻塞。
    • NIO 适用于高并发场景,能以更少的线程处理更多的连接,性能远超 BIO,但对于初学者来说,理解和使用起来更复杂。

对于初学者和大多数中小型应用,BIO 已经足够,当需要处理成千上万的并发连接时,才需要转向 NIO 或 Netty 等高性能网络框架。


异常处理和资源关闭

这是 Java 编程中至关重要的一点。

  1. 为什么需要关闭?

    • 文件描述符:每个 Socket 连接都会占用一个操作系统的文件描述符,如果不关闭,当连接数增多时,可能会导致文件描述符耗尽,程序无法再创建新的连接。
    • 端口占用:服务器端的 ServerSocket 会绑定一个端口,如果程序异常退出而没有关闭 ServerSocket,这个端口可能会在一段时间内被占用,导致你无法立即重启服务器。
  2. 如何正确关闭?

    • try-with-resources 语句:这是 Java 7 引入的最佳实践,任何实现了 AutoCloseable 接口的对象都可以放在 try 后面的括号里,当代码块执行完毕(无论是正常结束还是发生异常),这些资源都会被自动关闭。
    • 关闭顺序:应该先关闭高级别的流(InputStream, OutputStream),再关闭底层的 Socket,因为关闭流时,Socket 通常也会被自动关闭,但显式地关闭 Socket 是一个好习惯。
    • 多线程中的关闭:在服务器端,为每个客户端服务的线程结束时,应该关闭它所持有的 Socket

通过遵循以上指南,你应该能够掌握 Java 中基本的 Socket 接口调用,并能够构建简单的网络应用程序。

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