杰瑞科技汇

Java Socket如何实现网络通信?

Java Socket 编程完整教程

目录

  1. 什么是 Socket?
  2. 网络基础回顾
  3. Java Socket 编程核心类
  4. 第一部分:基于 TCP 的 Socket 编程(可靠、面向连接)
    • 1 通信流程概述
    • 2 编写一个简单的 Echo 服务器
    • 3 编写对应的客户端
    • 4 运行与测试
    • 5 多线程服务器处理多个客户端
  5. 第二部分:基于 UDP 的 Socket 编程(不可靠、无连接)
    • 1 通信流程概述
    • 2 编写一个简单的 UDP Echo 服务器
    • 3 编写对应的客户端
    • 4 运行与测试
  6. 关键知识点与最佳实践
    • 1 I/O 流管理
    • 2 异常处理
    • 3 关闭资源
    • 4 阻塞与非阻塞 I/O (NIO)

什么是 Socket?

你可以把 Socket(套接字) 想象成一个电话。

Java Socket如何实现网络通信?-图1
(图片来源网络,侵删)
  • 电话机:就是你的程序(客户端或服务器)。
  • 电话号码:IP 地址,用来在网络中唯一标识一台设备。
  • 分机号:就是端口号,用来在同一台设备上区分不同的应用程序。
  • 通话线路:就是网络连接。

Socket 就是网络编程中,应用程序为了进行网络通信而使用的一个“端点”,它封装了复杂的底层网络协议(如 TCP/IP),使得开发者可以方便地进行网络数据收发。

Java 通过 java.net 包提供了强大的 Socket 编程 API。


网络基础回顾

在开始编码前,需要了解两个核心概念:

  • IP 地址:网络中设备的唯一标识,如 168.1.1002001:0db8:85a3::8a2e:0370:7334,在 Java 中,常用 InetAddress 类来表示。
  • 端口号:一个 16 位的整数(0-65535),用于区分同一台主机上运行的不同服务,HTTP 服务通常使用 80 端口,HTTPS 使用 443 端口。
  • 协议:网络通信的规则,Socket 编程主要涉及两种:
    • TCP (Transmission Control Protocol)面向连接、可靠的协议,在数据传输前,客户端和服务器需要先建立一个连接(三次握手),数据会按顺序、无丢失地到达,适用于要求高可靠性的场景,如文件传输、网页浏览。
    • UDP (User Datagram Protocol)无连接、不可靠的协议,发送方直接把数据包(Datagram)发出去,不保证对方一定能收到,也不保证顺序,适用于对实时性要求高、能容忍少量丢包的场景,如视频会议、在线游戏。

Java Socket 编程核心类

类/接口 描述
java.net.Socket 客户端 Socket,客户端通过创建 Socket 对象来发起连接请求。
java.net.ServerSocket 服务器端 Socket,服务器通过创建 ServerSocket 对象来监听客户端的连接请求。
java.net.InetAddress 表示 IP 地址,没有构造方法,通过静态方法 getByName() 获取实例。
java.net.SocketImpl Socket 的具体实现类,一般由 JVM 自动创建。
java.io.InputStream 输入流,用于从 Socket 中读取数据。
java.io.OutputStream 输出流,用于向 Socket 中写入数据。
java.io.BufferedReader / java.io.InputStreamReader 用于包装输入流,可以方便地按行读取文本数据。
java.io.PrintWriter 用于包装输出流,可以方便地写入文本数据并自动处理换行符。

第一部分:基于 TCP 的 Socket 编程(可靠、面向连接)

TCP 是最常用的 Socket 编程方式,我们以一个经典的 Echo(回声)服务 为例:客户端发送一条消息,服务器原样返回这条消息。

Java Socket如何实现网络通信?-图2
(图片来源网络,侵删)

1 通信流程概述

服务器端:

  1. 创建 ServerSocket 对象,并绑定一个端口号,开始监听。
  2. 调用 accept() 方法,阻塞等待客户端连接,一旦有客户端连接,accept() 返回一个新的 Socket 对象,代表与该客户端的连接。
  3. 通过这个新的 Socket 对象获取 InputStreamOutputStream
  4. InputStream 读取客户端发送的数据。
  5. 将读取到的数据通过 OutputStream 写回给客户端。
  6. 关闭与当前客户端的连接(Socket 和相关的流)。
  7. 循环执行第 2 步,等待下一个客户端连接。

客户端:

  1. 创建 Socket 对象,指定服务器的 IP 地址和端口号,发起连接,这个过程是阻塞的,直到连接成功或超时。
  2. 连接成功后,通过 Socket 对象获取 InputStreamOutputStream
  3. 通过 OutputStream 向服务器发送数据。
  4. InputStream 读取服务器返回的数据。
  5. 关闭 Socket 和相关的流。

2 编写一个简单的 Echo 服务器

TCPEchoServer.java

import java.io.*;
import java.net.*;
public class TCPEchoServer {
    public static void main(String[] args) {
        int port = 6789; // 定义服务器监听的端口号
        // 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); // true表示自动刷新缓冲区
            String inputLine;
            // 从客户端读取数据,直到客户端关闭连接
            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();
        }
    }
}

3 编写对应的客户端

TCPEchoClient.java

Java Socket如何实现网络通信?-图3
(图片来源网络,侵删)
import java.io.*;
import java.net.*;
public class TCPEchoClient {
    public static void main(String[] args) {
        String hostname = "localhost"; // 或服务器的IP地址
        int port = 6789;
        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) {
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
                out.println(userInput); // 发送消息到服务器
                // 读取服务器返回的回声
                String response = in.readLine();
                System.out.println("服务器回声: " + response);
            }
        } catch (UnknownHostException e) {
            System.err.println("不知道的主机: " + hostname);
            System.exit(1);
        } catch (IOException e) {
            System.err.println "I/O error: " + e.getMessage());
            System.exit(1);
        }
    }
}

4 运行与测试

  1. 先运行服务器

    java TCPEchoServer

    你会看到控制台输出:服务器启动,监听端口 6789...

  2. 再运行客户端(可以在另一个终端窗口中运行):

    java TCPEchoClient

    客户端控制台会提示:已连接到服务器。请输入要发送的消息...

  3. 在客户端输入消息,"Hello, Server!",然后按回车。

    • 客户端会收到回声:服务器回声: Hello, Server!
    • 服务器端会打印:收到客户端消息: Hello, Server!
  4. 在客户端输入 exit 并回车,程序退出。

5 多线程服务器处理多个客户端

上面的服务器一次只能处理一个客户端,当它 accept() 一个客户端后,必须等该客户端断开连接后才能处理下一个,为了能同时处理多个客户端,我们需要使用多线程。

MultiThreadTCPEchoServer.java

import java.io.*;
import java.net.*;
public class MultiThreadTCPEchoServer implements Runnable {
    private final Socket clientSocket;
    public MultiThreadTCPEchoSocket(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);
        ) {
            System.out.println("处理新连接: " + clientSocket.getInetAddress().getHostAddress());
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                System.out.println("收到来自 " + clientSocket.getInetAddress().getHostAddress() + " 的消息: " + inputLine);
                out.println(inputLine);
            }
        } catch (IOException e) {
            // 客户端正常断开会触发 SocketException, 不算错误
            if (!(e instanceof SocketException)) {
                System.err.println("处理客户端时出错: " + e.getMessage());
            }
        } finally {
            try {
                clientSocket.close();
                System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 已断开。");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) throws IOException {
        int port = 6789;
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("多线程服务器启动,监听端口 " + port + "...");
            while (true) { // 无限循环,持续接受连接
                Socket clientSocket = serverSocket.accept();
                // 为每个客户端连接创建一个新线程
                new Thread(new MultiThreadTCPEchoServer(clientSocket)).start();
            }
        }
    }
}

第二部分:基于 UDP 的 Socket 编程(不可靠、无连接)

UDP 编程模型更简单,没有客户端和服务器的严格区分,双方都是平等的“数据包发送/接收方”。

1 通信流程概述

发送方:

  1. 创建 DatagramSocket 对象(可以指定端口号,也可以不指定,让系统分配)。
  2. 创建 DatagramPacket 对象,包含要发送的数据、接收方的 IP 地址和端口号。
  3. 调用 DatagramSocketsend() 方法发送数据包。

接收方:

  1. 创建 DatagramSocket 对象,并绑定一个端口号进行监听。
  2. 创建一个空的 DatagramPacket 对象,用于接收数据。
  3. 调用 DatagramSocketreceive() 方法阻塞等待数据包,接收到的数据会填充到空的 DatagramPacket 中。
  4. DatagramPacket 中解析出数据、发送方地址和端口。

2 编写一个简单的 UDP Echo 服务器

UDPEchoServer.java

import java.io.*;
import java.net.*;
public class UDPEchoServer {
    public static void main(String[] args) throws IOException {
        int port = 9876; // UDP服务器端口
        byte[] buffer = new byte[1024]; // 数据包缓冲区
        try (DatagramSocket serverSocket = new DatagramSocket(port)) {
            System.out.println("UDP服务器启动,监听端口 " + port + "...");
            while (true) { // 持续接收数据包
                // 创建一个空的DatagramPacket用于接收数据
                DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length);
                // receive()方法会阻塞,直到收到数据包
                serverSocket.receive(receivePacket);
                // 从数据包中提取数据
                String receivedMessage = new String(receivePacket.getData(), 0, receivePacket.getLength());
                System.out.println("收到来自 " + receivePacket.getAddress().getHostAddress() + ":" + receivePacket.getPort() + " 的消息: " + receivedMessage);
                // 创建回显数据包
                DatagramPacket sendPacket = new DatagramPacket(
                    receivedMessage.getBytes(), 
                    receivedMessage.getBytes().length, 
                    receivePacket.getAddress(), 
                    receivePacket.getPort()
                );
                // 发送回显数据包
                serverSocket.send(sendPacket);
            }
        }
    }
}

3 编写对应的客户端

UDPEchoClient.java

import java.io.*;
import java.net.*;
public class UDPEchoClient {
    public static void main(String[] args) throws IOException {
        String hostname = "localhost";
        int port = 9876;
        byte[] sendData;
        byte[] receiveData = new byte[1024];
        try (
            // 创建DatagramSocket,系统会分配一个可用端口
            DatagramSocket clientSocket = new DatagramSocket();
        ) {
            // 从控制台读取用户输入
            BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
            String sentence;
            while ((sentence = inFromUser.readLine()) != null) {
                if ("exit".equalsIgnoreCase(sentence)) {
                    break;
                }
                sendData = sentence.getBytes();
                // 创建要发送的数据包
                DatagramPacket sendPacket = new DatagramPacket(
                    sendData, 
                    sendData.length, 
                    InetAddress.getByName(hostname), 
                    port
                );
                // 发送数据包
                clientSocket.send(sendPacket);
                // 创建用于接收的空数据包
                DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
                // receive()方法会阻塞,直到收到服务器的响应
                clientSocket.receive(receivePacket);
                // 解析并打印服务器返回的消息
                String modifiedSentence = new String(receivePacket.getData(), 0, receivePacket.getLength());
                System.out.println("服务器回声: " + modifiedSentence);
            }
        }
    }
}

4 运行与测试

运行方式与 TCP 类似,先启动服务器,再启动客户端,你会发现通信是即时的,且不需要像 TCP 那样先建立连接。


关键知识点与最佳实践

1 I/O 流管理

  • 字节流 vs. 字符流SocketgetInputStream()getOutputStream() 返回的是原始的字节流,对于文本数据,最好使用 InputStreamReader 包装成字符流,再用 BufferedReader 进行缓冲,以高效地按行读写。
  • PrintWriter 的自动刷新:在创建 PrintWriter 时,将第二个参数设为 true (new PrintWriter(socket.getOutputStream(), true)),这样每次调用 println() 方法后,输出流会自动刷新,确保数据能立即发送出去,这对于交互式应用非常重要。

2 异常处理

网络操作充满了不确定性,必须妥善处理各种 IOException

  • SocketException:连接被重置、连接超时等。
  • BindException:端口被占用。
  • ConnectException:连接被拒绝(服务器未启动或地址错误)。
  • UnknownHostException:无法解析主机名。

3 关闭资源

  • 顺序:先关闭外层流,再关闭内层流(Socket),但 Socketclose() 方法会自动关闭其关联的输入/输出流,所以通常只需关闭 Socket 即可。
  • try-with-resources:这是 Java 7 引入的语法糖,强烈推荐使用,它能确保在 try 代码块执行完毕后,自动调用 close() 方法关闭实现了 AutoCloseable 接口(如 Socket, ServerSocket, InputStream, OutputStream 等)的资源,即使在 try 块中发生了异常。

4 阻塞与非阻塞 I/O (NIO)

传统的 Socket I/O 是阻塞式的,即 accept(), read(), write() 等方法在没有数据时会一直等待,这会占用线程资源。

对于高并发的场景,Java NIO (New I/O) 提供了非阻塞的解决方案。

  • 核心概念
    • Channel (通道):类似流,但可以双向读写。
    • Buffer (缓冲区):数据都存放在 Buffer 中。
    • Selector (选择器):一个线程可以管理多个 Channel,通过 Selector 查询哪些 Channel 已经准备好进行 I/O 操作。
  • 优点:用少量线程就能管理成千上万的连接,极大地提高了系统的吞吐量和扩展性。
  • 缺点:编程模型比传统的 BIO 复杂。

如果你需要构建高性能的网络服务(如聊天服务器、RPC 框架),应该学习 NIO,著名的 Netty 框架就是对 Java NIO 的高级封装。


特性 TCP (Socket/ServerSocket) UDP (DatagramSocket/DatagramPacket)
连接 面向连接,需要先建立连接 无连接,直接发送数据包
可靠性 可靠,保证数据顺序和完整性 不可靠,可能丢失、重复或乱序
速度 较慢,因为需要建立连接和维护状态 较快,开销小
应用场景 文件传输、网页浏览、邮件 视频会议、DNS、在线游戏
编程模型 流式,像读写文件 数据包式,像发快递
Java类 Socket, ServerSocket, InputStream, OutputStream DatagramSocket, DatagramPacket

通过本教程,你应该已经掌握了 Java Socket 编程的基础,从简单的 TCP 回声服务开始,逐步理解其工作原理,然后尝试实现自己的客户端/服务器应用,并最终探索更高级的 NIO 技术,网络编程是一个实践性很强的领域,多动手是最好的学习方式。

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