杰瑞科技汇

java socket通信 客户端

Socket 通信是网络编程的基础,它允许运行在不同计算机上的程序进行数据交换,Java 提供了非常简单易用的 java.net 包来实现 Socket 通信。

java socket通信 客户端-图1
(图片来源网络,侵删)

我们将分步进行,从最基础的概念到完整的代码示例,再到更高级的用法。


核心概念

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

  • IP 地址: 网络上每台计算机的唯一标识,0.0.1 (本机地址) 或 168.1.100 (局域网内某台电脑的地址)。
  • 端口号: 计算机上运行的应用程序(服务)的标识,一个 IP 地址可以提供多种服务,端口号用于区分这些服务,范围是 0 到 65535,Web 服务常用 80 端口,HTTPS 服务常用 443 端口,在客户端/服务器模型中,服务器需要在一个固定的、众所周知的端口上监听客户端的连接请求。
  • 客户端: 主动发起连接请求的一方,它会知道服务器的 IP 地址和端口号,并尝试连接到服务器。
  • 服务器: 在一个指定的 IP 地址和端口上监听,等待客户端连接的一方。
  • Socket (套接字): 它是网络通信的端点,客户端和服务器建立连接后,都会得到一个 Socket 对象,通过这个对象,双方就可以进行 I/O 操作(输入/输出),就像读写文件一样。

客户端编程步骤

Java 客户端编程通常遵循以下步骤:

  1. 创建 Socket 对象: 指定服务器的 IP 地址和端口号,向服务器发起连接请求,如果连接成功,客户端和服务器之间就建立了一条通信线路。
  2. 获取输入/输出流:
    • 通过 socket.getOutputStream() 获取一个 OutputStream,用于向服务器发送数据。
    • 通过 socket.getInputStream() 获取一个 InputStream,用于接收从服务器发来的数据。
  3. 使用流进行数据收发:
    • 我们会将字节流包装成更高级的流,如 OutputStreamWriterInputStreamReader 来处理字符,或者直接使用 DataOutputStreamDataInputStream 来处理基本数据类型。
  4. 关闭资源: 通信结束后,必须按照创建的相反顺序关闭所有打开的资源,即先关闭流,再关闭 Socket,以释放系统资源。

简单的客户端示例(字符串收发)

下面是一个最基础的客户端示例,它可以连接到服务器,向服务器发送一行字符串,然后接收服务器返回的一行字符串。

java socket通信 客户端-图2
(图片来源网络,侵删)

服务器代码 (为了演示,我们先写一个简单的服务器)

// SimpleServer.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 SimpleServer {
    public static void main(String[] args) {
        int port = 8888; // 服务器监听的端口
        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); // autoFlush=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("服务器已关闭。");
    }
}

客户端代码

// SimpleClient.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 = "127.0.0.1"; // 服务器的IP地址,这里用本机地址
        int port = 8888;              // 服务器的端口号
        try (
            // 1. 创建Socket对象,尝试连接服务器
            Socket socket = new Socket(hostname, port);
            // 2. 获取输出流,用于向服务器发送数据
            // 使用 PrintWriter 包装 OutputStream,方便发送字符串
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            // 3. 获取输入流,用于接收服务器的响应
            // 使用 BufferedReader 包装 InputStream,方便接收字符串
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 为了方便从控制台获取用户输入
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
        ) {
            System.out.println("已连接到服务器 " + hostname + ":" + port);
            System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
            String userInput;
            // 4. 循环收发数据
            while ((userInput = stdIn.readLine()) != null) {
                // 向服务器发送消息
                out.println(userInput);
                // 如果用户输入 exit,则退出循环
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
                // 从服务器接收响应
                String response = in.readLine();
                System.out.println("服务器响应: " + response);
            }
        } catch (UnknownHostException e) {
            System.err.println("不知道的主机: " + hostname);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O 发生错误: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("客户端已关闭。");
    }
}

如何运行

  1. 编译: 将 SimpleServer.javaSimpleClient.java 编译成 .class 文件。
    javac SimpleServer.java SimpleClient.java
  2. 运行服务器: 先在终端运行服务器。
    java SimpleServer

    你会看到输出:服务器启动,正在监听端口 8888...

  3. 运行客户端: 再打开另一个终端,运行客户端。
    java SimpleClient
  4. 交互:
    • 在客户端的控制台输入任意文本,Hello Server,然后按回车。
    • 客户端会立即收到服务器的响应:服务器收到了你的消息: Hello Server
    • 在服务器终端,你会看到:收到客户端消息: Hello Server
    • 在客户端输入 exit 并按回车,客户端和服务器都会关闭连接并退出程序。

代码解析

try-with-resources 语句

try (Socket socket = ...) {
    // ... 使用 socket
} // socket 和其他在 try() 中声明的资源会自动关闭

我们使用了 try-with-resources 语句,这是 Java 7 引入的一个非常方便的特性,只要实现了 AutoCloseable 接口的资源(如 Socket, InputStream, OutputStream 等)都可以在这里声明,当 try 块执行完毕后(无论正常结束还是发生异常),这些资源都会被自动 close(),从而避免了资源泄漏。

PrintWriterBufferedReader

  • PrintWriter: 包装了 OutputStream,提供了 println(), print(), printf() 等方便的方法来打印各种数据类型,它默认不自动刷新缓冲区,所以我们在构造时传入了 true 作为第二个参数 (autoFlush),这样每次调用 println() 后都会自动刷新缓冲区,确保数据被立即发送。
  • BufferedReader: 包装了 InputStreamReader,提供了 readLine() 方法,可以高效地按行读取文本数据。

更健壮的客户端(多线程处理)

上面的例子中,客户端在等待服务器响应时是阻塞的,无法同时发送和接收消息,在实际应用中,我们通常使用多线程来处理收发,使客户端可以同时进行输入和输出。

一个常见的模式是:

java socket通信 客户端-图3
(图片来源网络,侵删)
  • 主线程: 负责从控制台读取用户输入并发送给服务器。
  • 一个独立的接收线程: 负责持续监听并打印服务器的消息。

下面是一个改进后的多线程客户端示例。

多线程客户端代码

// MultiThreadClient.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 MultiThreadClient {
    public static void main(String[] args) {
        String hostname = "127.0.0.1";
        int port = 8888;
        try (
            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("已连接到服务器 " + hostname + ":" + port);
            System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
            // 1. 创建并启动一个接收消息的线程
            ReceiverThread receiverThread = new ReceiverThread(in);
            receiverThread.start();
            // 2. 主线程负责发送消息
            String userInput;
            while ((userInput = stdIn.readLine()) != null) {
                out.println(userInput);
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
            }
            // 等待接收线程结束
            receiverThread.join();
        } catch (UnknownHostException e) {
            System.err.println("不知道的主机: " + hostname);
        } catch (IOException e) {
            System.err.println("I/O 发生错误: " + e.getMessage());
        } catch (InterruptedException e) {
            System.err.println("接收线程被中断: " + e.getMessage());
            Thread.currentThread().interrupt(); // 恢复中断状态
        }
        System.out.println("客户端已关闭。"
    }
}
// 专门用于接收服务器消息的线程
class ReceiverThread extends Thread {
    private BufferedReader in;
    public ReceiverThread(BufferedReader in) {
        this.in = in;
    }
    @Override
    public void run() {
        try {
            String response;
            // 持续读取服务器的消息,直到连接关闭
            while ((response = in.readLine()) != null) {
                System.out.println("服务器响应: " + response);
            }
        } catch (IOException e) {
            // 如果连接断开,readLine() 会返回 null,线程会自然结束
            // System.err.println("接收消息时发生错误: " + e.getMessage());
        } finally {
            System.out.println("接收消息的线程已结束。");
        }
    }
}

这个版本的客户端可以让你在发送消息的同时,也能实时看到服务器发来的消息,体验更好。


总结与注意事项

  • 异常处理: 网络编程充满了不确定性,必须妥善处理 IOExceptionUnknownHostException 等异常。
  • 资源管理: 务必关闭所有打开的流和 Socket,推荐使用 try-with-resources
  • 阻塞行为: Socketconnect(), InputStreamread(), ServerSocketaccept() 等方法都是阻塞的,会一直等待直到有结果,多线程是解决阻塞问题的常用手段。
  • 数据格式: 上述例子中我们使用换行符 \n 作为消息的结束标记(readLine()),在更复杂的场景下,你可能需要定义自己的协议,例如使用长度前缀来标识消息的边界,以避免“粘包”或“半包”问题。
  • 编码: 如果要传输非英文字符,务必在 InputStreamReaderOutputStreamWriter 中指定字符编码,new InputStreamReader(socket.getInputStream(), "UTF-8"),以避免乱码。
分享:
扫描分享到社交APP
上一篇
下一篇