杰瑞科技汇

Java Socket如何高效传输数据?

核心概念:Socket 是什么?

想象一下打电话的过程:

Java Socket如何高效传输数据?-图1
(图片来源网络,侵删)
  1. 拨号:你拨对方的电话号码,建立连接。
  2. 通话:双方通过听筒和麦克风(输入/输出流)进行语音交流。
  3. 挂断:通话结束,双方挂断电话,释放资源。

Java Socket 的过程与此非常相似:

  • Socket (套接字):代表了网络连接中的一个端点,每个 Socket 都有一个 IP 地址和一个端口号,就像一个电话号码,通过 Socket 对象,你可以发起连接或接受连接。
  • IP 地址:网络中设备的唯一标识,168.1.100
  • 端口号:应用程序在设备上的唯一标识,范围是 0-65535,Web 服务通常使用 80 端口,HTTPS 使用 443 端口。
  • 输入流 / 输出流:一旦连接建立,Socket 就会提供 InputStreamOutputStream,你可以把它们想象成电话的听筒和麦克风,用来在网络上读写数据。

Java 中有两个主要的 Socket 类:

  • java.net.Socket客户端使用的类,用于发起连接。
  • java.net.ServerSocket服务器端使用的类,用于监听并接受客户端的连接。

数据传输的关键:字节流

Socket 传输的是原始的字节,而不是字符串或对象,这意味着:

  • 你需要将 Java 的 StringintObject 等类型转换为字节数组(序列化)。
  • 接收方需要将收到的字节数组再转换回原来的类型(反序列化)。

常用的转换方式有:

Java Socket如何高效传输数据?-图2
(图片来源网络,侵删)
  1. 字符串:使用 String.getBytes()new String(byte[])
  2. 基本数据类型:可以使用 DataInputStreamDataOutputStream,它们提供了方便的方法如 writeInt(), readInt(), writeUTF(), readUTF()
  3. 对象:使用 ObjectInputStreamObjectOutputStream,要求对象必须实现 Serializable 接口。

完整代码示例:一个简单的 Echo 服务器

下面我们创建一个经典的 "Echo"(回声)服务:客户端发送一条消息,服务器原样返回这条消息。

1 服务器端代码

服务器需要一直运行,等待客户端的连接。

EchoServer.java

import java.io.*;
import java.net.*;
public class EchoServer {
    public static void main(String[] args) {
        // 定义服务器监听的端口号
        int port = 12345;
        try ( // 使用 try-with-resources 自动关闭资源
              // 创建一个 ServerSocket,在指定端口上监听客户端连接
              ServerSocket serverSocket = new ServerSocket(port);
              // 等待客户端连接,accept() 是一个阻塞方法,直到有客户端连接才会返回
              Socket clientSocket = serverSocket.accept();
              // 获取客户端的输入流,用于读取客户端发送的数据
              BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
              // 获取客户端的输出流,用于向客户端发送数据
              PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)
        ) {
            System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
            String inputLine;
            // 循环读取客户端发送的数据
            // readLine() 也是一个阻塞方法,直到客户端发送一行数据或关闭连接
            while ((inputLine = in.readLine()) != null) {
                System.out.println("收到客户端消息: " + inputLine);
                // 将收到的消息回写给客户端
                out.println("服务器回声: " + inputLine);
            }
            System.out.println("客户端已断开连接。");
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

2 客户端代码

客户端主动连接服务器,发送消息并接收响应。

Java Socket如何高效传输数据?-图3
(图片来源网络,侵删)

EchoClient.java

import java.io.*;
import java.net.*;
public class EchoClient {
    public static void main(String[] args) {
        // 服务器的 IP 地址和端口号
        String serverAddress = "127.0.0.1"; // "localhost" 代表本机
        int port = 12345;
        try ( // 使用 try-with-resources 自动关闭资源
              // 创建一个 Socket 对象,连接到指定的服务器地址和端口
              Socket socket = new Socket(serverAddress, port);
              // 获取服务器的输入流,用于读取服务器返回的数据
              BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
              // 获取服务器的输出流,用于向服务器发送数据
              PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
              // 从控制台读取用户输入
              BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
        ) {
            System.out.println("已连接到服务器 " + serverAddress + ":" + port);
            System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
            String userInput;
            // 循环读取用户输入
            while ((userInput = stdIn.readLine()) != null) {
                if ("exit".equalsIgnoreCase(userInput)) {
                    break; // 用户输入 exit,则退出循环
                }
                // 将用户输入的消息发送给服务器
                out.println(userInput);
                // 读取服务器返回的回声
                String response = in.readLine();
                System.out.println("服务器响应: " + response);
            }
        } catch (UnknownHostException e) {
            System.err.println("无法找到主机: " + serverAddress);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O Error: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("客户端已关闭。");
    }
}

3 如何运行

  1. 编译:将两个 .java 文件放在同一个目录下,编译它们。
    javac EchoServer.java EchoClient.java
  2. 运行服务器:先运行服务器端程序,它会启动并等待连接。
    java EchoServer

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

  3. 运行客户端:在另一个新的终端窗口中,运行客户端程序。
    java EchoClient
  4. 交互
    • 客户端控制台会提示你输入消息。
    • 服务器端控制台会显示 客户端已连接...收到客户端消息: ...
    • 客户端输入 你好,按回车。
    • 客户端会立即收到服务器的响应:服务器响应: 服务器回声: 你好
    • 客户端输入 exit 并按回车,客户端程序会退出。
    • 服务器端会显示 客户端已断开连接。,并继续等待新的客户端连接。

更复杂的传输:发送和接收对象

上面的例子只能传输字符串,在实际开发中,我们更希望传输自定义的对象。

1 定义可序列化的对象

对象必须实现 java.io.Serializable 接口,它是一个标记接口,不需要实现任何方法。

User.java

import java.io.Serializable;
// 必须实现 Serializable 接口
public class User implements Serializable {
    private String username;
    private int age;
    // 构造函数
    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }
    // Getters
    public String getUsername() { return username; }
    public int getAge() { return age; }
    @Override
    public String toString() {
        return "User{username='" + username + "', age=" + age + "}";
    }
}

2 修改代码以传输对象

ObjectEchoServer.java

import java.io.*;
import java.net.*;
public class ObjectEchoServer {
    public static void main(String[] args) {
        int port = 12346;
        try (ServerSocket serverSocket = new ServerSocket(port);
             Socket clientSocket = serverSocket.accept();
             // 使用 ObjectInputStream 读取对象
             ObjectInputStream in = new ObjectInputStream(clientSocket.getInputStream());
             // 使用 ObjectOutputStream 发送对象
             ObjectOutputStream out = new ObjectOutputStream(clientSocket.getOutputStream())
        ) {
            System.out.println("客户端已连接,等待接收 User 对象...");
            // 读取客户端发送的 User 对象
            User receivedUser = (User) in.readObject();
            System.out.println("收到用户对象: " + receivedUser);
            // 修改对象并发回
            receivedUser.setAge(receivedUser.getAge() + 1);
            System.out.println("修改后并发回对象: " + receivedUser);
            out.writeObject(receivedUser);
        } catch (IOException e) {
            System.err.println("服务器 I/O 异常: " + e.getMessage());
        } catch (ClassNotFoundException e) {
            System.err.println("服务器: 无法找到类 " + e.getMessage());
        }
    }
}

ObjectEchoClient.java

import java.io.*;
import java.net.*;
public class ObjectEchoClient {
    public static void main(String[] args) {
        String serverAddress = "127.0.0.1";
        int port = 12346;
        try (Socket socket = new Socket(serverAddress, port);
             // 使用 ObjectOutputStream 发送对象
             ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
             // 使用 ObjectInputStream 读取对象
             ObjectInputStream in = new ObjectInputStream(socket.getInputStream())
        ) {
            // 创建一个 User 对象并发送给服务器
            User userToSend = new User("张三", 30);
            System.out.println("发送用户对象: " + userToSend);
            out.writeObject(userToSend);
            // 读取服务器返回的修改后的 User 对象
            User modifiedUser = (User) in.readObject();
            System.out.println("收到服务器返回的对象: " + modifiedUser);
        } catch (IOException e) {
            System.err.println("客户端 I/O 异常: " + e.getMessage());
        } catch (ClassNotFoundException e) {
            System.err.println("客户端: 无法找到类 " + e.getMessage());
        }
    }
}

重要注意事项和最佳实践

  1. 阻塞与非阻塞

    • ServerSocket.accept()InputStream.read() 等方法都是阻塞的,线程会一直等待,直到有事件发生(如连接到来、数据到达),对于简单的服务,这没问题,对于高并发的服务,需要使用多线程、NIO(New I/O)或 Netty 等框架。
  2. 资源管理

    • Socket 和相关的 I/O 流都是需要手动关闭的资源。强烈推荐使用 try-with-resources 语句,它能确保在代码块执行完毕后,资源被自动关闭,即使发生了异常。
  3. 字符编码

    • 当传输文本(如 String)时,务必注意编码问题,最好在创建 InputStreamReaderOutputStreamWriter 时明确指定编码,new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8),以避免因平台默认编码不同而导致的乱码。
  4. 网络异常处理

    • 网络是不可靠的,必须妥善处理各种 IOException,如连接中断、主机不可达、读写超时等。
  5. 协议设计

    • 对于复杂的应用,仅仅靠 readLine()readObject() 是不够的,你需要设计一个应用层协议
      • 长度前缀:在每个消息前加上一个固定长度的整数(4字节),表示消息体的长度,接收方先读取这4个字节,就知道接下来要读多少字节的数据。
      • 分隔符:使用特殊字符(如 \r\n\0)来分隔消息。
      • 序列化格式:使用 JSON、XML、Protocol Buffers 等格式来结构化你的数据,这比 Java 原生的序列化更通用、更高效。
角色 核心类 主要步骤 关键点
服务器 ServerSocket 创建 ServerSocket 并绑定端口。
accept() 阻塞等待连接。
通过 Socket 获取 InputStreamOutputStream
读写数据。
关闭资源。
accept() 是阻塞的,需要持续运行。
客户端 Socket 创建 Socket 并连接服务器 IP 和端口。
通过 Socket 获取 InputStreamOutputStream
读写数据。
关闭资源。
new Socket() 连接过程是阻塞的。

掌握 Java Socket 编程是理解网络编程的基础,从简单的字符串传输开始,逐步过渡到对象和自定义协议,你就能构建出强大的网络应用。

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