杰瑞科技汇

Java 客户端 Socket 如何实现连接?

Socket 编程是网络编程的基础,它允许两个不同设备上的程序进行通信,Java 提供了非常成熟的 java.net 包来支持 Socket 编程。

Java 客户端 Socket 如何实现连接?-图1
(图片来源网络,侵删)

我们将分步进行,从最基础的例子开始,然后逐步完善,加入更健壮的特性,如异常处理和资源释放。


核心概念

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

  • Socket (套接字):可以看作是网络通信的“端点”,每个 Socket 都有一个 IP 地址和一个端口号,通过这个组合,网络中的其他程序可以找到它并发送数据。
  • 客户端:主动发起连接请求的一方,它需要知道服务器的 IP 地址和端口号。
  • 服务器:被动等待连接的一方,它在一个固定的 IP 地址和端口上监听客户端的连接请求。
  • IP 地址:网络中设备的唯一标识,0.0.1(本机地址)或 168.1.100
  • 端口号:设备上应用程序的标识,范围是 0-65535,一个 IP 地址上的不同端口可以对应不同的服务(Web 服务通常在 80 端口)。

Java 中,客户端主要使用 Socket 类。


基础 Socket 客户端示例

这是一个最简单的客户端,它连接到服务器,发送一条消息,然后接收服务器的响应。

Java 客户端 Socket 如何实现连接?-图2
(图片来源网络,侵删)

服务器端代码(用于测试)

为了测试我们的客户端,我们需要一个简单的服务器,这里提供一个使用 Python 写的简单回显服务器,因为它非常简洁。

Python 服务器代码 (server.py):

# server.py
import socket
# 创建一个 TCP socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定到本机的 8888 端口
server_socket.bind(('127.0.0.1', 8888))
# 开始监听,允许 1 个客户端排队等待连接
server_socket.listen(1)
print("服务器启动,等待客户端连接...")
# 接受一个客户端连接
client_socket, addr = server_socket.accept()
print(f"客户端 {addr} 已连接")
# 接收客户端数据
data = client_socket.recv(1024).decode('utf-8')
print(f"收到来自客户端的消息: {data}")
# 将接收到的消息再发送回去
client_socket.sendall("服务器已收到你的消息!".encode('utf-8'))
# 关闭连接
client_socket.close()
server_socket.close()

如何运行服务器:

  1. 将上述代码保存为 server.py
  2. 打开终端,运行 python server.py
  3. 你会看到 "服务器启动,等待客户端连接..." 的提示。

Java 客户端代码

我们编写 Java 客户端来连接这个 Python 服务器。

Java 客户端代码 (SimpleClient.java):

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class SimpleClient {
    public static void main(String[] args) {
        // 服务器的 IP 地址和端口号
        String serverHost = "127.0.0.1";
        int serverPort = 8888;
        // try-with-resources 语句,确保资源在使用后被自动关闭
        try (Socket socket = new Socket(serverHost, serverPort);
             OutputStream out = socket.getOutputStream();
             InputStream in = socket.getInputStream()) {
            System.out.println("已连接到服务器 " + serverHost + ":" + serverPort);
            // 1. 发送数据到服务器
            String messageToSend = "你好,服务器!";
            out.write(messageToSend.getBytes());
            System.out.println("已发送消息: " + messageToSend);
            // 2. 从服务器接收数据
            byte[] buffer = new byte[1024];
            int bytesRead = in.read(buffer);
            String receivedMessage = new String(buffer, 0, bytesRead);
            System.out.println("收到服务器响应: " + receivedMessage);
        } catch (UnknownHostException e) {
            System.err.println("找不到主机: " + serverHost);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O 错误: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("客户端连接已关闭。");
    }
}

代码解析

  1. new Socket(serverHost, serverPort):

    • 这是客户端的核心,这行代码会尝试创建一个 Socket 对象并连接到指定的服务器地址和端口。
    • 如果连接成功,程序才会继续执行,如果服务器未启动或地址/端口错误,会抛出 IOException
    • 注意: 0.0.1 是一个特殊的 IP 地址,代表“本机”,如果你的客户端和服务器在同一台电脑上运行,就使用这个地址。
  2. OutputStream out = socket.getOutputStream():

    • 通过 Socket 获取输出流,用于向服务器发送数据。
  3. out.write(messageToSend.getBytes()):

    • String.getBytes() 将字符串转换为字节数组。
    • write() 方法将字节数组通过 Socket 发送到服务器。
  4. InputStream in = socket.getInputStream():

    • 通过 Socket 获取输入流,用于从服务器读取数据。
  5. in.read(buffer):

    • read() 方法会阻塞,直到从输入流中读取到数据。
    • 它读取的数据被存入 buffer 字节数组中,并返回实际读取的字节数。
    • 如果服务器关闭了连接,read() 会返回 -1
  6. try-with-resources:

    • try (Socket socket = ...; OutputStream out = ...; InputStream in = ...) 这种写法非常推荐。
    • 它会自动在 try 块执行完毕后(无论是否发生异常)调用 socket.close(), out.close(), in.close(),确保网络资源被正确释放,避免资源泄漏。

进阶:使用 BufferedReaderPrintWriter

直接使用字节流 (InputStream/OutputStream) 虽然灵活,但处理文本数据比较麻烦,更常见的做法是使用字符流包装器,以便直接读写字符串。

  • PrintWriter: 可以方便地将字符串写入输出流,并自动处理换行符。
  • BufferedReader: 可以高效地从输入流中读取一行文本。

改进后的 Java 客户端 (AdvancedClient.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 AdvancedClient {
    public static void main(String[] args) {
        String serverHost = "127.0.0.1";
        int serverPort = 8888;
        try (Socket socket = new Socket(serverHost, serverPort);
             // 使用 PrintWriter 包装 OutputStream,方便写入字符串
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             // 使用 BufferedReader 包装 InputStream,方便读取一行
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
            System.out.println("已连接到服务器 " + serverHost + ":" + serverPort);
            // 发送消息
            String messageToSend = "你好,服务器!这次用 PrintWriter 发送。";
            out.println(messageToSend); // println 会自动添加换行符
            System.out.println("已发送消息: " + messageToSend);
            // 接收服务器返回的一行
            String serverResponse = in.readLine();
            System.out.println("收到服务器响应: " + serverResponse);
        } catch (UnknownHostException e) {
            System.err.println("找不到主机: " + serverHost);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O 错误: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("客户端连接已关闭。");
    }
}

改进点

  1. PrintWriter(socket.getOutputStream(), true):

    • 我们将 OutputStream 包装成 PrintWriter
    • 第二个参数 true 表示启用“自动刷新”模式,这样,当我们调用 println()printf() 时,输出缓冲区会自动刷新,确保数据立即被发送出去。
  2. BufferedReader(new InputStreamReader(socket.getInputStream())):

    • 我们将 InputStream 包装成 InputStreamReader(字节流到字符流的桥梁),再包装成 BufferedReader
    • 这样就可以使用 in.readLine() 方法,它会读取一行直到遇到换行符,非常方便。

完整示例:交互式客户端

下面是一个更完整的客户端,它可以循环地与服务器进行交互,直到用户输入 "exit" 为止。

交互式 Java 客户端 (InteractiveClient.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 InteractiveClient {
    public static void main(String[] args) {
        String serverHost = "127.0.0.1";
        int serverPort = 8888;
        try (
            Socket socket = new Socket(serverHost, serverPort);
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 用于从控制台读取用户输入
            stdIn = new BufferedReader(new InputStreamReader(System.in))
        ) {
            System.out.println("已连接到服务器 " + serverHost + ":" + serverPort);
            System.out.println("输入消息发送给服务器,输入 'exit' 退出。");
            String userInput;
            // 循环读取用户输入
            while (true) {
                System.out.print("请输入消息: ");
                userInput = stdIn.readLine();
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
                // 发送消息到服务器
                out.println(userInput);
                // 从服务器接收响应
                String serverResponse = in.readLine();
                if (serverResponse == null) {
                    // 如果服务器关闭了连接,readLine() 会返回 null
                    System.out.println("服务器已关闭连接。");
                    break;
                }
                System.out.println("服务器响应: " + serverResponse);
            }
        } catch (UnknownHostException e) {
            System.err.println("找不到主机: " + serverHost);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O 错误: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("客户端程序结束。");
    }
}

这个客户端展示了如何构建一个可以持续与服务器对话的应用,是实际应用开发中更常见的模式。


总结与关键点

  1. 连接是第一步:客户端的核心是 new Socket(ip, port),这是建立通信的入口。
  2. 流是数据通道
    • getOutputStream() 用于发送数据。
    • getInputStream() 用于接收数据。
  3. 字符流更方便:使用 PrintWriterBufferedReader 可以简化文本数据的读写。
  4. 资源管理至关重要务必关闭 Socket 和相关的流。try-with-resources 是现代 Java 中最推荐的方式。
  5. 异常处理:网络操作不可靠,必须妥善处理 IOExceptionUnknownHostException 等异常。
  6. 阻塞行为read()readLine() 是阻塞方法,程序会一直等待,直到有数据到达或连接关闭,在交互式应用中,这通常是期望的行为。
分享:
扫描分享到社交APP
上一篇
下一篇