Java 网络编程
在互联网时代,网络编程已经成为每个Java开发者必须掌握的核心技能。无论是开发Web应用、微服务,还是构建分布式系统,都离不开网络通信的支撑。
网络编程本质上是解决不同主机间程序如何通信的问题。Java从诞生之初就内置了强大的网络编程能力,从传统的Socket
到现代的NIO
、AIO
,再到高性能框架Netty
,技术栈在不断演进。
本文将系统介绍Java网络编程的核心技术,包括Socket
编程和HTTP
编程两大部分。如果你对IO基础还不太熟悉,建议先阅读Java IO。
# 一、网络协议分层
在深入Java网络编程之前,理解网络协议分层模型至关重要。网络通信是个复杂的过程,涉及数据的封装、传输、路由和解析等多个环节。为了降低复杂度并实现模块化设计,网络协议采用了分层架构:
在实际开发中,我们主要接触TCP/IP
四层模型。每一层都有明确的职责分工:
- 数据链路层:负责相邻网络设备(如路由器、交换机)之间的数据传输,处理物理地址(
MAC
地址) - 网络层:实现不同网络中主机之间的通信,使用
IP
地址进行寻址和路由选择 - 传输层:为应用程序提供端到端的通信服务,主要协议有
TCP
(可靠传输)和UDP
(高效传输) - 应用层:直接为用户应用提供服务,包括
HTTP
、FTP
、SMTP
等协议
作为Java开发者,我们主要在传输层(Socket
编程)和应用层(HTTP
编程)工作。
# 二、Socket编程
Socket
(套接字)是网络编程的基础抽象,它代表了网络中两个程序通信的端点。可以把Socket
理解为两个程序之间的"电话线"——一端说话,另一端就能听到。
Socket
编程工作在传输层,直接使用TCP
或UDP
协议。在Java中,Socket
编程经历了从传统阻塞IO到NIO、AIO的演进过程,每种模式都有其适用场景。
让我们通过一个简单的例子来理解:客户端发送消息"我要吃肉",服务端响应"地主家没有余粮了"。
# 1、传统IO(BIO)
传统的Socket
编程采用阻塞IO(Blocking IO
)模型。"阻塞"意味着当程序执行accept()
或read()
操作时,如果没有连接到来或数据可读,线程会一直等待,无法做其他事情。
# 1.1、服务端实现
void server(){
try (ServerSocket server = new ServerSocket(8888)) {
while (true){
// accept()会阻塞等待客户端连接
Socket socket = server.accept();
// 获取输入输出流
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
Scanner scanner = new Scanner(is);
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(os, StandardCharsets.UTF_8), true);
// 处理客户端消息
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
if (line.startsWith("我要吃肉")){
pw.println("地主家没有余粮了");
}else {
pw.println("我不知道你在说什么");
}
if ("exit".equals(line)) {
pw.println("exit");
break;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
# 1.2、客户端实现
void client(){
try (Socket socket = new Socket("localhost", 8888);
InputStream is = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(outputStream, StandardCharsets.UTF_8), true);
Scanner scanner = new Scanner(is, StandardCharsets.UTF_8);
// 发送消息到服务端
pw.println("我要吃肉");
pw.println("exit");
// 接收服务端响应
boolean done = false;
while (!done && scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println("服务端响应: " + line);
if (line.equals("exit")){
done = true;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
# 1.3、多客户端支持
上面的服务端程序有个明显缺陷:只能为一个客户端服务。当一个客户端连接后,其他客户端必须等待。要支持多客户端并发访问,需要引入多线程:
// 使用线程池优化
ExecutorService executor = Executors.newFixedThreadPool(100);
try (ServerSocket server = new ServerSocket(8888)) {
while (true) {
Socket socket = server.accept();
// 为每个客户端分配一个线程处理
executor.submit(() -> {
try {
handleClient(socket);
} catch (IOException e) {
e.printStackTrace();
}
});
}
} catch (IOException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
这种"一个连接一个线程"的模式简单直观,但存在明显问题:
- 资源消耗大:每个线程占用一定内存(通常
1MB
左右) - 上下文切换开销:线程数量过多时,
CPU
在线程间切换的开销很大 - 扩展性差:系统能创建的线程数有限,难以支撑
C10K
(万级并发)场景
这些问题促使了NIO
的诞生。
# 2、NIO(非阻塞IO)
Java 1.4引入的NIO
(New IO
)采用了完全不同的设计思路。与BIO
的"一个连接一个线程"不同,NIO
可以用一个线程管理多个连接。
# 2.1、核心概念
NIO
的三大核心组件:
- Channel(通道):双向的数据传输通道,相当于
BIO
中的Stream
- Buffer(缓冲区):数据读写的中转站,所有数据都要经过
Buffer
- Selector(选择器):
IO
多路复用器,一个Selector
可以监控多个Channel
的事件
# 2.2、工作原理
NIO
采用了Reactor
模式,工作流程如下:
- 注册阶段:将
Channel
注册到Selector
上,指定关注的事件(连接、读、写等) - 轮询阶段:
Selector
通过select()
方法轮询注册的Channel
,这是唯一的阻塞点 - 处理阶段:当某个
Channel
就绪时,Selector
返回就绪的Channel
集合,逐个处理
这种模式的优势在于:一个线程可以管理成千上万个连接,极大提升了系统的并发能力。
# 2.3、服务端实现
void server() throws IOException {
// 1. 创建Selector多路复用器
Selector selector = Selector.open();
// 2. 创建ServerSocketChannel并配置为非阻塞
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
// 3. 注册到Selector,关注ACCEPT事件
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
// 4. 绑定端口
ServerSocket serverSocket = ssChannel.socket();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(address);
System.out.println("NIO服务器启动,监听端口:8888");
while (true) {
// 5. 阻塞等待就绪事件
selector.select();
// 6. 获取就绪的SelectionKey集合
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 处理连接请求
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);
// 新连接注册READ事件
sChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理读请求
SocketChannel sChannel = (SocketChannel) key.channel();
String line = readDataFromSocketChannel(sChannel);
if (line.startsWith("我要吃肉")){
sChannel.write(StandardCharsets.UTF_8.encode("地主家没有余粮了"));
}else {
sChannel.write(StandardCharsets.UTF_8.encode("我不知道你在说什么"));
}
sChannel.close();
}
// 移除已处理的key
keyIterator.remove();
}
}
}
// 辅助方法:从Channel读取数据
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();
while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n <= 0) {
break;
}
buffer.flip(); // 切换到读模式
data.append(StandardCharsets.UTF_8.decode(buffer));
}
return data.toString();
}
# 2.4、客户端实现
NIO的客户端可以继续使用传统的阻塞方式,也可以采用非阻塞方式:
void client() throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream out = socket.getOutputStream();
out.write("我要吃肉".getBytes());
Scanner scanner = new Scanner(socket.getInputStream(), StandardCharsets.UTF_8);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println("服务端响应: " + line);
}
out.close();
}
# 2.5、NIO的性能优化
有人可能会问:单线程处理所有请求,性能会不会成为瓶颈?
实际上,NIO
可以结合线程池进一步优化性能。常见的做法是:
- 主线程负责接收连接(
Accept
) - 工作线程池负责处理
IO
读写
这就是经典的主从Reactor
模式,Netty
等高性能框架都采用了这种设计。
# 3、AIO(异步IO)
NIO
虽然是非阻塞的,但本质上还是同步IO
——需要应用程序主动询问"数据好了吗"。Java 7引入的AIO
(Asynchronous IO
,也叫NIO.2
)实现了真正的异步IO
。
# 3.1、vs NIO的区别
- NIO:应用程序需要不断询问
OS
"数据准备好了吗"(通过select()
轮询) - AIO:应用程序告诉
OS
"数据好了叫我",然后就可以干别的事了(基于回调)
AIO
采用了"订阅-通知"模式:
- 应用程序发起
IO
请求,并注册回调函数 - 应用程序继续执行其他任务,不用等待
OS
完成IO
操作后,主动调用回调函数通知应用程序
# 3.2、服务端实现
public class AioSocketServer {
static final InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
private static final Object waitObject = new Object();
void server() throws IOException, InterruptedException {
// 创建异步服务端通道
AsynchronousServerSocketChannel serverSock =
AsynchronousServerSocketChannel.open().bind(address);
// 异步接受连接,注册回调
serverSock.accept(serverSock, new CompletionHandler<>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel,
AsynchronousServerSocketChannel serverSock) {
// 继续接受下一个连接
serverSock.accept(serverSock, this);
// 异步读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
socketChannel.read(byteBuffer, byteBuffer,
new ReadCompletionHandler(socketChannel));
}
@Override
public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
System.err.println("Accept failed: " + exc.getMessage());
}
});
System.out.println("AIO服务器启动,监听端口:8888");
// 保持主线程存活
synchronized(waitObject) {
waitObject.wait();
}
}
// 读取完成的回调处理器
class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel channel;
public ReadCompletionHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
@Override
public void completed(Integer result, ByteBuffer buffer) {
if (result == -1) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
buffer.flip();
String message = StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println("收到消息: " + message);
// 处理业务逻辑并响应
String response;
if (message.startsWith("我要吃肉")) {
response = "地主家没有余粮了";
} else {
response = "我不知道你在说什么";
}
// 异步写入响应
channel.write(StandardCharsets.UTF_8.encode(response));
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
System.err.println("Read failed: " + exc.getMessage());
}
}
}
# 3.3、AIO的适用场景
虽然AIO
看起来很美好,但实际应用中却不如NIO
普及,原因如下:
- 操作系统支持有限:
Windows
的IOCP
支持良好,但Linux
的AIO
支持不完善 - 编程复杂度高:异步回调的编程模式容易造成"回调地狱"
- 性能提升有限:对比
NIO
,AIO
的性能提升并不明显
因此,在实际项目中,NIO
(特别是通过Netty
封装后)仍然是主流选择。AIO
更适合于:
Windows
平台的高并发服务- 连接数不多但每个连接传输数据量大的场景
- 需要真正异步处理的特殊业务场景
# 4、Netty
原生的NIO API
虽然功能强大,但使用复杂、容易出错。实际项目中,我们通常会使用Netty
这样的高性能网络框架。
# 4.1、为什么选择Netty
- 易用性高:屏蔽了
NIO
的复杂性,提供简洁的API
- 功能完善:内置了编解码器、心跳检测、流量控制等常用功能
- 性能卓越:零拷贝、内存池、无锁设计等优化手段
- 协议支持广泛:
TCP
、UDP
、HTTP
、WebSocket
等协议开箱即用 - 社区活跃:被广泛应用于
Dubbo
、RocketMQ
、Elasticsearch
等知名项目
# 4.2、Netty的核心架构
Netty
基于主从Reactor
模式设计:
- Boss Group:负责接收连接,相当于传统的
Acceptor
- Worker Group:负责处理
IO
事件,相当于IO
处理线程池 - ChannelPipeline:责任链模式,用于处理入站和出站事件
- ChannelHandler:业务逻辑处理器
想深入学习Netty
,推荐查看官方示例 (opens new window)和Netty权威指南 (opens new window)。
# 5、高性能服务器的并发模型对比
了解了Java的各种IO
模型后,我们来对比一下业界主流高性能服务器的设计思路。
# 5.1、Nginx的并发模型
Nginx
采用多进程+单线程+非阻塞IO的架构:
- Master进程:负责管理
Worker
进程 - Worker进程:每个进程内部采用单线程+
epoll
的方式处理请求 - 优势:进程隔离性好,单个
Worker
崩溃不影响其他Worker
- 适用场景:静态文件服务、反向代理、负载均衡等
IO
密集型任务
# 5.2、vs Nginx:为什么设计不同?
很多人疑惑:既然Nginx
单线程就能处理高并发,为什么Netty
和Tomcat
还要用线程池?
关键在于应用场景的差异:
业务复杂度不同
Nginx
:主要做转发和静态文件服务,CPU
计算少Netty/Tomcat
:需要执行复杂的业务逻辑,CPU
密集型操作多
编程语言特性
Nginx
:C语言编写,没有GC
,内存管理可控Netty/Tomcat
:Java编写,有GC
停顿,需要考虑JVM
特性
扩展性需求
Nginx
:功能相对固定,通过配置和模块扩展Netty/Tomcat
:需要支持各种自定义业务逻辑
因此,Netty
采用线程池是为了:
- 充分利用多核
CPU
进行业务处理 - 避免业务逻辑阻塞影响其他连接
- 提供更好的扩展性和灵活性
# 三、HTTP编程
前面介绍的Socket
编程属于传输层,而HTTP
编程工作在应用层。HTTP
协议基于TCP
实现,为Web应用提供了标准的通信协议。
# 1、服务端
对于HTTP
服务端开发,Java生态提供了丰富的选择:
# 1.1、主流框架推荐
- Spring Boot WebFlux:响应式编程模型,适合高并发场景,推荐优先使用
- Spring Boot Web MVC:传统的
Servlet
模型,生态成熟,适合常规Web应用 - Netty HTTP Server:基于
Netty
的原生HTTP
实现,性能极致但需要手动处理很多细节 - Helidon:
Oracle
出品的云原生框架,轻量级、启动快
实际项目中,Spring Boot
凭借其强大的生态系统和开发效率,是大多数团队的首选。
# 2、客户端
# 2.1、HttpURLConnection(传统方式)
在Java 11之前,HttpURLConnection
是JDK内置的HTTP客户端:
String url = "https://www.baidu.com/";
URL obj = new URL(url);
HttpURLConnection connection = (HttpURLConnection) obj.openConnection();
// 设置请求属性
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000); // 连接超时
connection.setReadTimeout(5000); // 读取超时
// 发起连接
connection.connect();
// 处理响应
int responseCode = connection.getResponseCode();
if(responseCode == HttpURLConnection.HTTP_OK){
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String line;
StringBuilder response = new StringBuilder();
while ((line = reader.readLine()) != null) {
response.append(line);
}
System.out.println(response.toString());
}
}
connection.disconnect();
HttpURLConnection
的缺点很明显:
API
设计陈旧,使用繁琐- 不支持连接池,性能较差
- 缺少现代
HTTP
特性支持(如HTTP/2
)
# 2.2、HttpClient
业界广泛使用的是Apache HttpClient (opens new window),它提供了更强大的功能。
我个人推荐使用Unirest (opens new window),它基于Apache HttpClient
封装,API
更加简洁:
// 简洁的链式调用
HttpResponse<JsonNode> response = Unirest.post("http://localhost/post")
.header("accept", "application/json")
.queryString("apiKey", "123")
.field("parameter", "value")
.field("firstname", "Gary")
.asJson();
// 处理响应
int status = response.getStatus();
JsonNode body = response.getBody();
Unirest支持灵活的配置管理:
// 全局配置(影响所有请求)
Unirest.config()
.defaultBaseUrl("https://api.example.com")
.connectTimeout(10000)
.socketTimeout(30000)
.proxy("127.0.0.1", 8888)
.concurrency(200, 10); // 连接池配置
// 创建独立实例(不影响全局)
UnirestInstance customClient = Unirest.spawnInstance();
customClient.config()
.connectTimeout(5000)
.socketTimeout(15000);
// 请求级配置(优先级最高)
HttpResponse<String> response = Unirest.get("https://example.com")
.connectTimeout(3000) // 覆盖全局配置
.asString();
# 2.3、11 HTTP Client(现代方式)
Java 11引入的HttpClient
是一个全新设计的HTTP
客户端,支持HTTP/2
和异步编程:
// 创建HttpClient
HttpClient client = HttpClient.newBuilder()
.version(Version.HTTP_2) // 支持HTTP/2
.connectTimeout(Duration.ofSeconds(10))
.proxy(ProxySelector.of(new InetSocketAddress("127.0.0.1", 8888)))
.build();
// 构建请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.timeout(Duration.ofSeconds(30))
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString("{\"name\":\"test\"}"))
.build();
// 同步请求
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());
// 异步请求
CompletableFuture<HttpResponse<String>> future =
client.sendAsync(request, BodyHandlers.ofString());
future.thenAccept(res -> System.out.println("Async response: " + res.body()));
HttpClient
的优势:
- 原生支持
HTTP/2
- 支持同步和异步编程
- 内置连接池管理
- 支持
WebSocket
- 更现代的
API
设计
如需自定义线程池:
ExecutorService executor = Executors.newFixedThreadPool(10);
HttpClient client = HttpClient.newBuilder()
.executor(executor)
.build();
# a、代理设置详解
使用代理是企业开发中的常见需求,Java 11 HttpClient
提供了完善的代理支持:
// 基本代理配置
String proxyHost = "proxy.example.com";
int proxyPort = 8080;
String proxyUsername = "user";
String proxyPassword = "pass";
// 自定义ProxySelector
ProxySelector proxySelector = new ProxySelector() {
@Override
public List<Proxy> select(URI uri) {
// 可以根据不同的URI返回不同的代理
if (uri.getHost().contains("internal")) {
return Collections.singletonList(Proxy.NO_PROXY);
}
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHost, proxyPort);
return Collections.singletonList(new Proxy(Proxy.Type.HTTP, proxyAddress));
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
System.err.println("代理连接失败: " + uri);
}
};
// 配置认证
HttpClient httpClient = HttpClient.newBuilder()
.proxy(proxySelector)
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(proxyUsername, proxyPassword.toCharArray());
}
})
.build();
重要提示:如果遇到代理认证失败(407错误),可能需要配置JVM
系统属性:
// 启用Basic认证(默认被禁用)
System.setProperty("jdk.http.auth.proxying.disabledSchemes", "");
System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
这是因为从Java 8u111开始,出于安全考虑,JDK
默认禁用了Basic
认证。这些配置可以在$JAVA_HOME/conf/net.properties
中找到。
# 2.4、声明式HTTP客户端
声明式HTTP
客户端让我们可以像调用本地方法一样调用远程API
,大大简化了开发:
// 定义接口
public interface UserService {
@Request(
url = "https://api.example.com/users/{id}",
headers = "Accept: application/json"
)
User getUser(@Path("id") Long userId);
@PostRequest(url = "https://api.example.com/users")
User createUser(@Body User user);
}
// 使用接口
UserService userService = Forest.client(UserService.class);
User user = userService.getUser(123L); // 像调用本地方法一样简单
主流的声明式HTTP
客户端框架:
- Spring RestClient/WebClient:
Spring
生态原生支持,与Spring Boot
集成最好 - OpenFeign:
Spring Cloud
组件,微服务开发首选 - Forest:国产框架,功能丰富,文档友好
- Retrofit:
Square
出品,Android
开发广泛使用
选择建议:
Spring Boot
项目:优先使用Spring
原生方案或OpenFeign
- 非
Spring
项目:Forest
或Retrofit
都是不错的选择
# 四、总结
本文系统介绍了Java网络编程的核心技术,从底层的Socket
编程到上层的HTTP
编程,涵盖了BIO
、NIO
、AIO
等不同的IO
模型。
关键要点回顾:
- IO模型演进:
BIO
→NIO
→AIO
,每种模型都有其适用场景 - 框架选择:实际项目中优先使用成熟框架(
Netty
、Spring Boot
)而非原生API
- 性能优化:理解不同并发模型的原理,根据业务特点选择合适的方案
- HTTP客户端:Java 11的
HttpClient
是现代化的选择,声明式客户端能提升开发效率
网络编程是个庞大的领域,本文只是介绍了最核心的部分。如果你想深入学习,建议:
- 实践
Netty
开发,理解高性能网络编程 - 研究
Spring WebFlux
,掌握响应式编程 - 了解
gRPC
、WebSocket
等其他协议
希望这篇文章能帮助你建立Java网络编程的知识体系。如有疑问或补充,欢迎交流讨论!