Appearance
网络编程
为什么需要网络编程?
现代应用几乎都是分布式系统。前端通过 HTTP 请求后端 API,后端通过 RPC 调用其他微服务,消息队列传递异步消息,数据库连接池管理 TCP 连接——这些全部依赖网络编程。
Java 网络编程的核心能力:
- TCP 通信:可靠的双向字节流,用于 RPC、数据库连接、消息推送
- UDP 通信:低延迟的无连接传输,用于视频直播、DNS 查询
- HTTP 通信:应用层协议,Web 服务的基础
- NIO 高性能通信:非阻塞 IO,支撑高并发服务器
TCP/IP 模型
理解网络编程必须先理解 TCP/IP 四层模型:
| 层 | 协议 | 数据单元 | Java 对应 |
|---|---|---|---|
| 应用层 | HTTP、DNS、FTP、SMTP | 消息 | HttpURLConnection、HttpClient |
| 传输层 | TCP、UDP | 段/数据报 | Socket、ServerSocket、DatagramSocket |
| 网络层 | IP、ICMP、ARP | 数据包 | 操作系统处理 |
| 链路层 | Ethernet、WiFi | 帧 | 网卡驱动 |
TCP 三次握手和四次挥手是高频考点:
三次握手(建立连接):
客户端 → SYN → 服务器
客户端 ← SYN+ACK ← 服务器
客户端 → ACK → 服务器
四次挥手(断开连接):
客户端 → FIN → 服务器
客户端 ← ACK ← 服务器
客户端 ← FIN ← 服务器
客户端 → ACK → 服务器Socket 编程(TCP)
基本概念
Socket 是操作系统提供的网络编程接口,Java 通过 Socket 和 ServerSocket 类封装了底层的 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();
}
}
}关键要点
readLine()以换行符为分隔:发送方必须发送\n或\r\n,否则readLine()会一直阻塞- 流关闭顺序:先关闭输出流再关闭输入流,或使用 try-with-resources 自动管理
accept()是阻塞的:服务端需要多线程处理多个客户端setSoTimeout():设置read()超时,防止永久阻塞
UDP 编程
UDP 是无连接协议,不保证可靠送达,但延迟低、开销小。Java 通过 DatagramSocket 和 DatagramPacket 实现。
服务端
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
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠,保证送达和顺序 | 不可靠,可能丢包乱序 |
| 速度 | 较慢 | 较快 |
| 数据边界 | 字节流,无边界 | 数据报,保留边界 |
| 适用场景 | HTTP、文件传输、数据库 | 视频直播、DNS、游戏 |
| Java 类 | Socket / ServerSocket | DatagramSocket |
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)- 将多个 Channel(连接)注册到一个 Selector 上
- Selector 不断轮询:哪个 Channel 有数据可读?
- 只处理有事件的 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 的核心优势
- 统一 API:屏蔽了 NIO、AIO、OIO 的差异
- 零拷贝:减少数据在用户态和内核态之间的复制
- 内存池:
ByteBuf使用内存池复用,减少 GC 压力 - 响应式:基于事件驱动和回调,天然支持高并发
- 协议支持:内置 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 为什么快?
- Reactor 线程模型:少量线程处理海量连接,避免线程切换开销
- 零拷贝:
FileRegion和CompositeByteBuf减少数据复制 - 内存池:
PooledByteBufAllocator复用 ByteBuf,减少 GC - 无锁化设计:每个 Channel 绑定到同一个 EventLoop 线程,串行处理无竞争
- 批量处理:一次 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,而不是永久阻塞。这是防止线程无限等待的重要机制。
