Skip to content

网络编程

为什么需要网络编程?

现代应用几乎都是分布式系统。前端通过 HTTP 请求后端 API,后端通过 RPC 调用其他微服务,消息队列传递异步消息,数据库连接池管理 TCP 连接——这些全部依赖网络编程。

Java 网络编程的核心能力:

  1. TCP 通信:可靠的双向字节流,用于 RPC、数据库连接、消息推送
  2. UDP 通信:低延迟的无连接传输,用于视频直播、DNS 查询
  3. HTTP 通信:应用层协议,Web 服务的基础
  4. NIO 高性能通信:非阻塞 IO,支撑高并发服务器

TCP/IP 模型

理解网络编程必须先理解 TCP/IP 四层模型:

协议数据单元Java 对应
应用层HTTP、DNS、FTP、SMTP消息HttpURLConnectionHttpClient
传输层TCP、UDP段/数据报SocketServerSocketDatagramSocket
网络层IP、ICMP、ARP数据包操作系统处理
链路层Ethernet、WiFi网卡驱动

TCP 三次握手和四次挥手是高频考点:

三次握手(建立连接):
  客户端  →  SYN           →  服务器
  客户端  ←  SYN+ACK       ←  服务器
  客户端  →  ACK           →  服务器

四次挥手(断开连接):
  客户端  →  FIN           →  服务器
  客户端  ←  ACK           ←  服务器
  客户端  ←  FIN           ←  服务器
  客户端  →  ACK           →  服务器

Socket 编程(TCP)

基本概念

Socket 是操作系统提供的网络编程接口,Java 通过 SocketServerSocket 类封装了底层的 TCP 协议。一个 Socket 连接由四元组唯一标识:(源IP, 源端口, 目标IP, 目标端口)

服务端

java
public class TcpServer {
    public static void main(String[] args) throws IOException {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("服务器启动,监听端口 8080");

            while (true) {
                // accept() 阻塞,直到有客户端连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("客户端连接: " + clientSocket.getInetAddress());

                // 为每个客户端分配一个线程处理(生产环境用线程池!)
                new Thread(() -> handleClient(clientSocket)).start();
            }
        }
    }

    private static void handleClient(Socket socket) {
        try (
            BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            PrintWriter out = new PrintWriter(
                socket.getOutputStream(), true)  // autoFlush
        ) {
            String line;
            while ((line = in.readLine()) != null) {
                System.out.println("收到: " + line);
                out.println("Echo: " + line);  // 回显
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try { socket.close(); } catch (IOException e) {}
        }
    }
}

客户端

java
public class TcpClient {
    public static void main(String[] args) {
        try (
            Socket socket = new Socket("127.0.0.1", 8080);
            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("已连接到服务器,输入消息(输入 exit 退出):");
            String userInput;
            while ((userInput = stdIn.readLine()) != null) {
                if ("exit".equalsIgnoreCase(userInput)) break;
                out.println(userInput);
                System.out.println("服务器回复: " + in.readLine());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

关键要点

  1. readLine() 以换行符为分隔:发送方必须发送 \n\r\n,否则 readLine() 会一直阻塞
  2. 流关闭顺序:先关闭输出流再关闭输入流,或使用 try-with-resources 自动管理
  3. accept() 是阻塞的:服务端需要多线程处理多个客户端
  4. setSoTimeout():设置 read() 超时,防止永久阻塞

UDP 编程

UDP 是无连接协议,不保证可靠送达,但延迟低、开销小。Java 通过 DatagramSocketDatagramPacket 实现。

服务端

java
public class UdpServer {
    public static void main(String[] args) throws IOException {
        try (DatagramSocket socket = new DatagramSocket(9090)) {
            byte[] buffer = new byte[1024];
            System.out.println("UDP 服务器启动,监听端口 9090");

            while (true) {
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                socket.receive(packet);  // 阻塞等待数据

                String message = new String(packet.getData(), 0, packet.getLength());
                System.out.println("收到: " + message + " 来自: " +
                    packet.getAddress() + ":" + packet.getPort());

                // 回复
                String response = "ACK: " + message;
                byte[] responseData = response.getBytes();
                DatagramPacket responsePacket = new DatagramPacket(
                    responseData, responseData.length,
                    packet.getAddress(), packet.getPort());
                socket.send(responsePacket);
            }
        }
    }
}

客户端

java
public class UdpClient {
    public static void main(String[] args) throws IOException {
        try (DatagramSocket socket = new DatagramSocket()) {
            String message = "Hello UDP";
            byte[] data = message.getBytes();

            DatagramPacket packet = new DatagramPacket(
                data, data.length,
                InetAddress.getByName("127.0.0.1"), 9090);
            socket.send(packet);

            // 接收回复
            byte[] buffer = new byte[1024];
            DatagramPacket responsePacket = new DatagramPacket(buffer, buffer.length);
            socket.receive(responsePacket);
            System.out.println("服务器回复: " +
                new String(responsePacket.getData(), 0, responsePacket.getLength()));
        }
    }
}

TCP vs UDP

维度TCPUDP
连接面向连接(三次握手)无连接
可靠性可靠,保证送达和顺序不可靠,可能丢包乱序
速度较慢较快
数据边界字节流,无边界数据报,保留边界
适用场景HTTP、文件传输、数据库视频直播、DNS、游戏
Java 类Socket / ServerSocketDatagramSocket

HTTP 通信

HttpURLConnection(JDK 内置)

java
public class HttpClient {
    public static String get(String urlStr) throws IOException {
        URL url = new URL(urlStr);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(5000);
        conn.setReadTimeout(5000);

        int responseCode = conn.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {
            try (BufferedReader in = new BufferedReader(
                    new InputStreamReader(conn.getInputStream()))) {
                StringBuilder response = new StringBuilder();
                String line;
                while ((line = in.readLine()) != null) {
                    response.append(line);
                }
                return response.toString();
            }
        }
        return null;
    }

    public static String post(String urlStr, String jsonBody) throws IOException {
        URL url = new URL(urlStr);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setRequestProperty("Content-Type", "application/json");
        conn.setDoOutput(true);  // 允许输出

        try (OutputStream os = conn.getOutputStream()) {
            os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
        }

        try (BufferedReader in = new BufferedReader(
                new InputStreamReader(conn.getInputStream()))) {
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = in.readLine()) != null) {
                response.append(line);
            }
            return response.toString();
        }
    }
}

Java 11 HttpClient(推荐)

Java 11 引入了现代化的 HttpClient,支持 HTTP/2、异步请求:

java
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(10))
    .build();

// 同步 GET
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Accept", "application/json")
    .GET()
    .build();

HttpResponse<String> response = client.send(request,
    HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

// 异步 POST
HttpRequest postRequest = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"张三\"}"))
    .build();

client.sendAsync(postRequest, HttpResponse.BodyHandlers.ofString())
    .thenApply(HttpResponse::body)
    .thenAccept(System.out::println)
    .join();

为什么 NIO 更好?

BIO 的瓶颈

传统 BIO(Blocking IO)每个连接需要一个线程:

客户端1 → 线程1(阻塞在 read())
客户端2 → 线程2(阻塞在 read())
客户端3 → 线程3(阻塞在 read())
...
客户端10000 → 线程10000(线程栈内存爆炸,上下文切换开销巨大)

当连接数达到数万时,线程资源成为瓶颈。大部分线程其实在等待数据,浪费了系统资源。

NIO 的核心思想

NIO(Non-blocking IO)用一个线程管理多个连接

         Selector(多路复用器)
       /          |          \
   Channel1    Channel2    Channel3
(客户端1)    (客户端2)    (客户端3)
  1. 将多个 Channel(连接)注册到一个 Selector 上
  2. Selector 不断轮询:哪个 Channel 有数据可读?
  3. 只处理有事件的 Channel,不阻塞在没有数据的连接上

NIO 核心组件

组件作用对应 BIO
Channel双向读写通道Socket / ServerSocket
Buffer数据缓冲区,所有数据通过 Buffer 读写字节流
Selector多路复用器,监听多个 Channel 的事件

NIO 服务端示例

java
public class NioServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080));
        serverChannel.configureBlocking(false);  // 非阻塞模式
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("NIO 服务器启动,监听端口 8080");

        while (true) {
            selector.select();  // 阻塞,直到有事件发生
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iter = keys.iterator();

            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                iter.remove();  // 必须手动移除,否则会重复处理

                if (key.isAcceptable()) {
                    // 新连接
                    SocketChannel clientChannel = serverChannel.accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("新连接: " + clientChannel.getRemoteAddress());

                } else if (key.isReadable()) {
                    // 可读事件
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int bytesRead = clientChannel.read(buffer);
                    if (bytesRead == -1) {
                        // 客户端断开连接
                        clientChannel.close();
                    } else {
                        buffer.flip();
                        byte[] data = new byte[buffer.remaining()];
                        buffer.get(data);
                        String message = new String(data);
                        System.out.println("收到: " + message);

                        // 回显
                        buffer.clear();
                        buffer.put(("Echo: " + message).getBytes());
                        buffer.flip();
                        clientChannel.write(buffer);
                    }
                }
            }
        }
    }
}

Netty 简介

为什么选择 Netty?

虽然 NIO 解决了 BIO 的并发瓶颈,但原生 NIO 编程复杂、容易出错。Netty 封装了 NIO,提供了更简洁、高性能、稳定可靠的网络编程框架。

Netty 的核心优势

  1. 统一 API:屏蔽了 NIO、AIO、OIO 的差异
  2. 零拷贝:减少数据在用户态和内核态之间的复制
  3. 内存池ByteBuf 使用内存池复用,减少 GC 压力
  4. 响应式:基于事件驱动和回调,天然支持高并发
  5. 协议支持:内置 HTTP、WebSocket、Protobuf 等编解码器

核心概念:EventLoop 和 Pipeline

BossGroup(EventLoop 1个):
  负责 accept 新连接,注册到 WorkerGroup

WorkerGroup(EventLoop N个,通常为 CPU 核数 * 2):
  每个 EventLoop 绑定一个线程,管理多个 Channel

Pipeline(每个 Channel 一条):
  Head → Decoder → BusinessHandler → Encoder → Tail
  数据入站:Head → Decoder → BusinessHandler
  数据出站:BusinessHandler → Encoder → Tail

简单 Echo 服务器

java
public class NettyEchoServer {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ch.pipeline()
                            .addLast(new StringDecoder())
                            .addLast(new StringEncoder())
                            .addLast(new SimpleChannelInboundHandler<String>() {
                                @Override
                                protected void channelRead0(ChannelHandlerContext ctx,
                                                            String msg) {
                                    System.out.println("收到: " + msg);
                                    ctx.writeAndFlush("Echo: " + msg);
                                }
                            });
                    }
                });

            ChannelFuture future = bootstrap.bind(8080).sync();
            System.out.println("Netty 服务器启动");
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

Netty 为什么快?

  1. Reactor 线程模型:少量线程处理海量连接,避免线程切换开销
  2. 零拷贝FileRegionCompositeByteBuf 减少数据复制
  3. 内存池PooledByteBufAllocator 复用 ByteBuf,减少 GC
  4. 无锁化设计:每个 Channel 绑定到同一个 EventLoop 线程,串行处理无竞争
  5. 批量处理:一次 select 处理多个 Channel 的事件

网络编程常见陷阱

半包和粘包

TCP 是字节流协议,没有消息边界。发送方两次 write("ABC")write("DEF"),接收方可能:

  • 一次 read() 收到 "ABCDEF"(粘包)
  • 两次 read() 收到 "AB""CDEF"(半包)

解决方案:

方案原理适用场景
定长消息每个消息固定长度,不足补零简单协议
分隔符消息以特殊字符结尾(如 \n文本协议
长度字段消息头包含消息体长度二进制协议
Netty 编解码器LengthFieldBasedFrameDecoder生产环境推荐
java
// Netty 中解决粘包半包——基于长度字段
ch.pipeline()
    .addLast(new LengthFieldBasedFrameDecoder(
        65535,  // maxFrameLength
        0,      // lengthFieldOffset(长度字段偏移量)
        4,      // lengthFieldLength(长度字段占4字节)
        0,      // lengthAdjustment
        4       // initialBytesToStrip(跳过长度字段本身)
    ));

心跳机制

TCP 连接可能因为网络中断、机器宕机等原因"假死"——双方都以为连接正常,但实际已断开。

java
// 客户端定期发送心跳
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
    try {
        out.println("HEARTBEAT");
        if (out.checkError()) {
            // 连接断开,重新连接
            reconnect();
        }
    } catch (Exception e) {
        reconnect();
    }
}, 0, 30, TimeUnit.SECONDS);
java
// Netty 内置心跳支持
ch.pipeline().addLast(new IdleStateHandler(
    60,   // 读超时秒数
    30,   // 写超时秒数
    0     // 读写超时秒数
));

连接超时和读超时

不设置超时会导致线程永久阻塞:

java
Socket socket = new Socket();
socket.connect(new InetSocketAddress("127.0.0.1", 8080), 5000);  // 连接超时
socket.setSoTimeout(10000);  // 读超时 10 秒

资源释放

网络资源必须正确关闭,否则会导致文件描述符泄漏:

java
// 推荐:try-with-resources(Java 7+)
try (Socket socket = new Socket("127.0.0.1", 8080);
     BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
     PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
    // 使用 socket
}  // 自动关闭所有资源

常见面试题

Q1: TCP 和 UDP 的区别?

TCP 面向连接、可靠、有序、基于字节流;UDP 无连接、不可靠、可能乱序、基于数据报。TCP 适用于文件传输、HTTP;UDP 适用于实时通信、视频直播。

Q2: 什么是三次握手和四次挥手?

三次握手是 TCP 建立连接的过程,确保双方收发能力正常。四次挥手是断开连接的过程,因为 TCP 是全双工的,双方都需要发送 FIN 和确认 ACK。

Q3: 什么是粘包和半包?如何解决?

TCP 是字节流协议,没有消息边界。粘包是多个消息粘在一起,半包是一个消息被拆成多次接收。解决方案:定长消息、分隔符、消息头+消息体(长度字段)、使用 Netty 的编解码器。

Q4: BIO、NIO、AIO 的区别?

模型阻塞模式线程模型适用场景
BIO同步阻塞一个连接一个线程连接数少的架构
NIO同步非阻塞一个线程多个连接高并发连接
AIO异步非阻塞回调通知连接数多且长连接

Q5: Netty 为什么高性能?

Reactor 线程模型减少线程切换、零拷贝减少数据复制、内存池减少 GC 压力、无锁化串行设计、批量处理事件。

Q6: 什么是心跳机制?为什么需要?

客户端和服务端定期发送心跳包确认对方存活。TCP 连接可能"假死"(网络中断但未正常关闭),没有心跳无法及时发现,导致资源泄漏。

Q7: Selector 的工作原理是什么?

Selector 是 NIO 的多路复用器。多个 Channel 注册到 Selector 上,Selector 通过底层操作系统的 select/poll/epoll 机制监听所有 Channel 的事件。当有事件发生时,select() 返回,程序遍历 selectedKeys 处理就绪的 Channel。

Q8: 如何实现一个简单的 HTTP 服务器?

使用 ServerSocket 监听 80 端口,解析 HTTP 请求行和头部,按 HTTP 协议格式返回响应。生产环境推荐使用 Netty 或嵌入式 Servlet 容器(如 Tomcat embed)。

Q9: Socket.setSoTimeout() 的作用是什么?

设置 read() 方法的超时时间。如果超过指定时间没有数据可读,read() 会抛出 SocketTimeoutException,而不是永久阻塞。这是防止线程无限等待的重要机制。