9.Netty HTTP客户端(HttpClient)

目录


Netty专栏目录(点击进入…)


Netty HTTP客户端(HttpClient)

Reactor Netty提供了易于使用和易于配置的HttpClient。它隐藏了创建HTTP客户端所需的大部分 Netty功能,并添加了Reactive Streams背压(Reactive Streams是具有无阻塞背压的异步流处理的标准)

连接

要将HTTP客户端连接到给定HTTP端点,必须创建并配置一个HttpClient实例:

import reactor.netty.http.client.HttpClient;

public class Application {

	public static void main(String[] args) {
		HttpClient client = HttpClient.create();  // 创建一个HttpClient实例

		client.get()        // 指定GET将使用的方法                
		      .uri("https://example.com/")  // 指定路径
		      .response()      // 获取响应HttpClientResponse            
		      .block();
	}
	
}

使用WebSocket:

import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
import reactor.core.publisher.Flux;
import reactor.netty.http.client.HttpClient;

public class Application {

	public static void main(String[] args) {
		HttpClient client = HttpClient.create();

		client.websocket()
		      .uri("wss://echo.websocket.org")
		      .handle((inbound, outbound) -> {
		          inbound.receive()
		                 .asString()
		                 .take(1)
		                 .subscribe(System.out::println);

		          final byte[] msgBytes = "hello".getBytes(CharsetUtil.ISO_8859_1);
		          return outbound.send(Flux.just(Unpooled.wrappedBuffer(msgBytes), Unpooled.wrappedBuffer(msgBytes)))
		                         .neverComplete();
		      })
		      .blockLast();
	}
	
}

主机和端口

为了连接到特定的主机和端口,可以将以下配置应用到HTTP客户端:

import reactor.netty.http.client.HttpClient;

public class Application {

	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
				          .host("example.com")  // 配置HTTP主机
				          .port(80);      // 配置HTTP端口      

		client.get()
		      .uri("/")
		      .response()
		      .block();
	}
	
}

急切初始化

默认情况下,HttpClient资源的初始化是按需进行的。这意味着first request吸收了初始化和加载所需的额外时间:
(1)事件循环组
(2)主机名解析器
(3)本机传输库(使用本机传输时)
(4)用于安全性的本机库(在OpenSsl的情况下)

当需要预加载这些资源时,可以进行HttpClient如下配置:

import reactor.core.publisher.Mono;
import reactor.netty.ByteBufFlux;
import reactor.netty.http.client.HttpClient;

public class Application {

	public static void main(String[] args) {
		HttpClient client = HttpClient.create();
        // 初始化并加载事件循环组、主机名解析器、本机传输库和用于安全性的本机库
		client.warmup() 
		      .block();

        // 主机名解析发生在第一个请求中。在此示例中,使用了连接池,因此在第一个请求中建立了到URL的连接,对同一URL的后续请求会重用连接池中的连接
		client.post()
		      .uri("https://example.com/")
		      .send(ByteBufFlux.fromString(Mono.just("hello")))
		      .response()
		      .block(); 
	}
	
}

写入数据

要将数据发送到给定的HTTP端点,可以使用send(Publisher)方法提供发布服务器。默认情况下,Transfer Encoding:chunked应用于那些需要请求主体的HTTP方法。通过请求头提供的内容长度禁用传输编码:必要时分块。以下示例发送hello:

import reactor.core.publisher.Mono;
import reactor.netty.ByteBufFlux;
import reactor.netty.http.client.HttpClient;

public class Application {

	public static void main(String[] args) {
		HttpClient client = HttpClient.create();

		client.post()
		      .uri("https://example.com/")
              // 将hello字符串发送到给定的HTTP端点
		      .send(ByteBufFlux.fromString(Mono.just("hello"))) 
		      .response()
		      .block();
	}
	
}

1.添加标题和其他元数据

向给定HTTP端点发送数据时,可能需要发送额外的headers、cookie和其他元数据:

import io.netty.handler.codec.http.HttpHeaderNames;
import reactor.core.publisher.Mono;
import reactor.netty.ByteBufFlux;
import reactor.netty.http.client.HttpClient;

public class Application {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
                           // 禁用Transfer-Encoding: chunked并提供Content-Length标头
				          .headers(h -> h.set(HttpHeaderNames.CONTENT_LENGTH, 5)); 

		client.post()
		      .uri("https://example.com/")
		      .send(ByteBufFlux.fromString(Mono.just("hello")))
		      .response()
		      .block();
	}
}

2.压缩

可以在HTTP客户端启用压缩,这意味着将请求标头Accept-Encoding添加到请求标头中:

import reactor.netty.http.client.HttpClient;

public class Application {

	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
				          .compress(true);

		client.get()
		      .uri("https://example.com/")
		      .response()
		      .block();
	}
	
}

3.自动重定向支持

可以配置HTTP客户端以启用自动重定向支持。

Reactor Netty提供了两种不同的自动重定向支持策略:
(1)followRedirect(boolean)
指定是否为statuses启用HTTP自动重定向支持301|302|307|308。

(2)followRedirect(BiPredicate<HttpClientRequest, HttpClientResponse>)
如果提供的谓词匹配,则启用自动重定向支持

使用followRedirect(true):

import reactor.netty.http.client.HttpClient;

public class Application {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
				          .followRedirect(true);

		client.get()
		      .uri("https://example.com/")
		      .response()
		      .block();
	}
}

消费数据

要从给定HTTP端点接收数据,可以使用HttpClient.ResponseReceiver,使用该responseContent():

import reactor.netty.http.client.HttpClient;

public class Application {
	public static void main(String[] args) {
		HttpClient client = HttpClient.create();

		client.get()
		      .uri("https://example.com/")
		      .responseContent()   // 从给定HTTP端点接收数据
		      .aggregate()    // 聚合数据   
		      .asString()      // 将数据转换为字符串
		      .block();
	}
}

1.读取标题和其他元数据

从给定HTTP端点接收数据时,可以检查响应标头、状态代码和其他元数据。可以使用HttpClientResponse获取此附加元数据:

import reactor.netty.http.client.HttpClient;

public class Application {
	public static void main(String[] args) {
		HttpClient client = HttpClient.create();

		client.get()
		      .uri("https://example.com/")
		      .responseSingle((resp, bytes) -> {
		          System.out.println(resp.status()); // 获取状态码
		          return bytes.asString();
		      })
		      .block();
	}
}

2.HTTP响应解码器

默认情况下,Netty为传入的响应配置一些限制。详细 -> HttpResponseDecoder
(1)初始行的最大长度
(2)所有标题的最大长度
(3)内容或每个块的最大长度

默认情况下,HTTP客户端配置有以下设置:
HttpDecoderSpec.java

public static final int DEFAULT_MAX_INITIAL_LINE_LENGTH = 4096; //默认最大初始行长度
public static final int DEFAULT_MAX_HEADER_SIZE = 8192; // 默认最大标题大小
public static final int DEFAULT_MAX_CHUNK_SIZE = 8192; // 默认最大块大小
public static final boolean DEFAULT_VALIDATE_HEADERS = true; // 默认验证Headers
public static final int DEFAULT_INITIAL_BUFFER_SIZE = 128; // 默认初始缓冲区大小
// 默认允许重复内容长度
public static final boolean DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS = false;

HttpResponseDecoderSpec.java

// 缺少响应时默认失败
public static final boolean DEFAULT_FAIL_ON_MISSING_RESPONSE = false;
// 连接请求后默认解析HTTP
public static final boolean DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST = false;
/**
 * HTTP/2.0明文升级请求内容的最大长度。默认情况下,客户端将允许升级请求,最多65536作为聚合内容的最大长度
 */
public static final int DEFAULT_H2C_MAX_CONTENT_LENGTH = 65536;

当需要更改这些默认设置时,可以HTTP按如下方式配置客户端:

import reactor.netty.http.client.HttpClient;

public class Application {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
// 所有标头的最大长度将为16384. 当超过此值时, 会引发TooLongFrameException
				          .httpResponseDecoder(spec -> spec.maxHeaderSize(16384)); 

		client.get()
		      .uri("https://example.com/")
		      .responseContent()
		      .aggregate()
		      .asString()
		      .block();
	}
}

生命周期回调

提供了以下生命周期回调以扩展HttpClient

回调函数描述
doAfterRequest在发送请求时调用
doAfterResolve在成功解析远程地址后调用
doAfterResponseSuccess在完全收到响应后调用
doOnChannelInit初始化通道时调用
doOnConnect当通道即将连接时调用
doOnConnected在通道连接后调用
doOnDisconnected在通道断开连接后调用
doOnError当请求尚未发送且未完全收到响应时调用
doOnRedirect当收到响应头并且请求即将被重定向时调用
doOnRequest在即将发送请求时调用
doOnRequestError在请求尚未发送时调用
doOnResolve在即将解析远程地址时调用
doOnResolveError在远程地址未成功解析的情况下调用
doOnResponse在收到响应标头后调用
doOnResponseError在未完全收到响应时调用

使用doOnConnected和doOnChannelInit回调:

import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import reactor.netty.http.client.HttpClient;
import java.util.concurrent.TimeUnit;

public class Application {

	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
                          // NettyReadTimeoutHandler当通道被连接时,管道被扩展
				          .doOnConnected(conn ->
				              conn.addHandler(new ReadTimeoutHandler(10, TimeUnit.SECONDS)))   
                          // Netty管道LoggingHandler在初始化通道时扩展
				          .doOnChannelInit((observer, channel, remoteAddress) ->
				              channel.pipeline()
				                     .addFirst(new LoggingHandler("reactor.netty.examples"))); 

		client.get()
		      .uri("https://example.com/")
		      .response()
		      .block();
	}
	
}

TCP-level配置

当需要TCP级别的配置时,可以使用以下代码段来扩展默认TCP客户端配置(添加选项、绑定地址等):配置与TCP配置一致

(1)Setting Channel Options - 设置通道参数选项

当需要在TCP级别更改配置时,可以使用以下代码段来扩展默认TCP服务器配置:

import io.netty.channel.ChannelOption;
import io.netty.channel.epoll.EpollChannelOption;
//import io.netty.channel.socket.nio.NioChannelOption;
//import jdk.net.ExtendedSocketOptions;
import reactor.netty.http.client.HttpClient;
import java.net.InetSocketAddress;

public class Application {

	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
				          .bindAddress(() -> new InetSocketAddress("host", 1234))
                           // 将连接建立超时配置为10秒
				          .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) 
                           // 启用TCP keepalive。这意味着TCPkeepalive在连接空闲一段时间后开始发送探测
				          .option(ChannelOption.SO_KEEPALIVE, true)      
                           // 在TCP开始发送keepalive探测之前,连接需要保持空闲5分钟      
				          .option(EpollChannelOption.TCP_KEEPIDLE, 300)
                           // 将各个keepalive探测之间的时间配置为1分钟   
				          .option(EpollChannelOption.TCP_KEEPINTVL, 60)
                           // 将TCPkeepalive探测的最大数量配置为8   
				          .option(EpollChannelOption.TCP_KEEPCNT, 8);          

		/*
		  以下选项仅在使用NIO传输(Java11)时可用
		.option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPIDLE), 300)
		.option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPINTERVAL), 60)
		.option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPCOUNT),
		以下选项仅在使用Epoll传输时可用
		  
		*/
		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()

(2)Wire Logger:连线日志记录

Reactor Netty提供线路日志记录,用于何时需要检查对等点之间的流量。默认情况下,连线日志记录处于禁用状态。要启用它,必须将记录器reactor.netty.http.client.HttpClient级别设置为DEBUG并应用以下配置:

Reactor Netty支持3种不同的格式化程序:

连线日志格式化描述
AdvancedByteBufFormat#HEX_DUM同时记录事件和内容。内容将采用十六进制格式(默认)
AdvancedByteBufFormat#SIMPLE使用此格式启用连线记录时,仅记录事件
AdvancedByteBufFormat#TEXTUAL同时记录事件和内容。内容将采用纯文本格式
import reactor.netty.http.client.HttpClient;

public class Application {

	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
				          .wiretap(true);  // 启用连线记录

		client.get()
		      .uri("https://example.com/")
		      .response()
		      .block();
	}
	
}

连接池

默认情况下,Reactor Netty client(客户端)使用一个“固定”连接池,其中500个为活动通道的最大数量(Channel),1000个为允许保持挂起状态的进一步通道采集尝试的最大数量(对于其余配置,请检查下面的系统属性或构建器配置)。这意味着,如果有人试图获取一个通道,只要创建的通道少于500个,并且由池管理,那么实现就会创建一个新的通道。当达到池中通道的最大数量时,最多会延迟1000次获取通道的新尝试(挂起),直到通道再次返回到池中,并且会因错误而拒绝进一步尝试。

默认“固定”连接池:
①500个为活动通道Channel)
②1000个为允许保持挂起状态的进一步通道采集尝试的最大数量

ReactorNetty.java

/**
 *默认最大连接数。退回到2个可用处理器(但最小值为16)
 */
public static final String POOL_MAX_CONNECTIONS = "reactor.netty.pool.maxConnections";
/**
 * Default acquisition timeout (milliseconds) before error. If -1 will never wait to
 * acquire before opening a new
 * connection in an unbounded fashion. Fallback 45 seconds
 */
public static final String POOL_ACQUIRE_TIMEOUT = "reactor.netty.pool.acquireTimeout";
/**
 * 默认最大空闲时间,回退-未指定最大空闲时间
 */
public static final String POOL_MAX_IDLE_TIME = "reactor.netty.pool.maxIdleTime";
/**
 * 默认最大使用寿命,默认未指定。
 */
public static final String POOL_MAX_LIFE_TIME = "reactor.netty.pool.maxLifeTime";
/**
 默认租赁策略(先进先出、后进先出),默认fifo-先进先出
fifo - 连接选择为先进先出
lifo - 连接选择为后进先出
 */
public static final String POOL_LEASING_STRATEGY = "reactor.netty.pool.leasingStrategy";
/**
 *默认使用SamplingAllocationStrategy}#getPermitsSamplingRate(介于0d和1d之间(百分比))
此策略包装了PoolBuilder#sizeBetween(int,int) 、AllocationStrategy、以及对AllocationStrategy#getpermissions(int)的调用示例。
*回退 - 未启用。
 */
public static final String POOL_GET_PERMITS_SAMPLING_RATE = "reactor.netty.pool.getPermitsSamplingRate";
/**
 * 默认使用SamplingAllocationStrategy#returnPermitsSamplingRate(介于0d和1d之间(百分比))
此策略包装了PoolBuilder#sizeBetween(int,int)、AllocationStrategy、以及对AllocationStrategy#returnpermissions(int)的调用示例
*回退-未启用采样。
 */
public static final String POOL_RETURN_PERMITS_SAMPLING_RATE = "reactor.netty.pool.returnPermitsSamplingRate";

当需要更改默认设置时,可以进行ConnectionProvider如下配置:

import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;

import java.time.Duration;

public class Application {
	public static void main(String[] args) {
		ConnectionProvider provider =
				ConnectionProvider.builder("custom")
				                  .maxConnections(50)
                                // 将连接保持空闲的最长时间配置为20秒
				                  .maxIdleTime(Duration.ofSeconds(20))    
                                // 将连接保持活动的最长时间配置为60秒       
				                  .maxLifeTime(Duration.ofSeconds(60))    
                                // 将挂起获取操作的最长时间配置为60秒       
				                  .pendingAcquireTimeout(Duration.ofSeconds(60)) 
                                // 每两分钟,将定期检查连接池中是否存在适用于删除的连接
				                  .evictInBackground(Duration.ofSeconds(120))    
				                  .build();

		HttpClient client = HttpClient.create(provider);

		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response);

		provider.disposeLater()
		        .block();
	}
}

当期望高负载时,请谨慎使用具有非常高的最大连接值的连接池。
reactor.netty.http.client.PrematureCloseException由于打开/获取的并发连接过多,可能会遇到 根本原因“连接超时”的异常。

需要禁用连接池,可以应用如下配置:

import reactor.netty.http.client.HttpClient;

public class Application {

	public static void main(String[] args) {
		HttpClient client = HttpClient.newConnection();

		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response);
	}
	
}

可用的配置:

(1)disposeInactivePoolsInBackground:检查连接池

启用此选项后,会在后台定期检查连接池,那些在指定时间内为空且不活动的连接池将有资格进行处理。默认情况下,禁用非活动池的这种后台处理。

(2)disposeTimeout:处理超时

当ConnectionProvider#dispose()或ConnectionProvider#disposeLater()被调用时,使用此宽限期超时触发连接池的正常关闭。从那时起,所有获取连接的调用都会快速失败,但会出现异常。但是,对于所提供的Duration,待处理的获取将有机会得到服务。注:拒绝新的获取和计时器立即启动风度,不论认购到Mono的返回ConnectionProvider#disposeLater()。随后的调用返回相同的Mono,有效地从第一个正常关闭调用中获取通知并忽略随后提供的超时。默认情况下,不指定处理超时。

(3)evictInBackground:定期检查符合删除条件的连接

启用此选项后,每个连接池都会根据驱逐标准定期检查符合删除条件的连接maxIdleTime。默认情况下,此后台驱逐是禁用的。

(4)fifo:默认租赁策略 - 先进先出

配置连接池,如果有空闲连接(即池未充分利用),下一次获取操作将获取Least Recently Used连接(LRU,即当前空闲连接中最先释放的连接)。默认租赁策略。

(5)lifo:租赁策略 - 后进先出

配置连接池,如果有空闲连接(即池未充分利用),下一次获取操作将获取Most Recently Used连接(MRU,即当前空闲连接中最后释放的连接)。

(6)maxConnections:连接数

开始挂起之前的最大连接数(每个连接池)。默认为 2 * 可用处理器数(但最小值为 16)。

(7)maxIdleTime:最大空闲时间

空闲时通道可以关闭的时间(分辨率:ms)。默认值:未指定最大空闲时间。

(8)maxLifeTime:最大寿命

通道有资格关闭的总生命周期(分辨率:ms)。默认值:未指定最大寿命。

(9)metrics:监控指标

启用/禁用与 Micrometer 的内置集成。ConnectionProvider.MeterRegistrar可以提供与另一个度量系统的集成。默认情况下,指标未启用。

(10)pendingAcquireMaxCount:挂起队列中的最大额外尝试次数

获取连接以保留在挂起队列中的最大额外尝试次数。如果指定 -1,则挂起队列没有上限。默认为 2 * 最大连接数。

(11)pendingAcquireTimeout:挂起获取必须完成或抛出TimeoutException之前的最长时间

挂起获取必须完成或抛出 TimeoutException 之前的最长时间(分辨率:毫秒)。如果指定了 -1,则不会应用此类超时。默认值:45秒


SSL和TLS

当需要SSL或TLS时,可以应用下一个示例中显示的配置。默认情况下,如果OpenSSL可用, 则使用SslProvider.OPENSSL提供程序作为提供程序。否则,将使用SslProvider.JDK提供程序您可以通过使用SslContextBuilder或通过设置来切换提供程序“-Dio.netty.handler.ssl.noOpenSsl=true”。

使用SslContextBuilder:

import reactor.netty.http.Http11SslContextSpec;
import reactor.netty.http.client.HttpClient;

public class Application {

	public static void main(String[] args) {
		Http11SslContextSpec http11SslContextSpec = Http11SslContextSpec.forClient();

		HttpClient client =
				HttpClient.create()
				          .secure(spec -> spec.sslContext(http11SslContextSpec));

		client.get()
		      .uri("https://example.com/")
		      .response()
		      .block();
	}
	
}

(1)服务器名称指示

默认情况下,HTTP客户端将远程主机名作为SNI服务器名发送。当需要更改此默认设置时,您可以HTTP按如下方式配置客户端:

import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import reactor.netty.http.client.HttpClient;

import javax.net.ssl.SNIHostName;

public class Application {
	public static void main(String[] args) throws Exception {
		SslContext sslContext = SslContextBuilder.forClient().build();

		HttpClient client =
				HttpClient.create()
				          .secure(spec -> spec.sslContext(sslContext)
				                              .serverNames(new SNIHostName("test.com")));

		client.get()
		      .uri("https://127.0.0.1:8080/")
		      .response()
		      .block();
	}
}

重试策略

默认情况下,HTTP如果请求在TCP级别上中止,客户端会重试一次请求


HTTP/2

默认情况下,HTTP客户端支持HTTP/1.1. 如果需要HTTP/2,可以通过配置获取。除了协议配置,如果需要H2但不需要H2C(cleartext),还必须配置SSL。

由于应用层协议协商(ALPN)不受JDK8的“开箱即用”支持(尽管一些供应商将ALPN向后移植到 JDK8),因此需要额外依赖支持它的本机库——例如:netty-tcnative-boringssl-static

(1)简单的H2例子:

import io.netty.handler.codec.http.HttpHeaders;
import reactor.core.publisher.Mono;
import reactor.netty.http.HttpProtocol;
import reactor.netty.http.client.HttpClient;
import reactor.util.function.Tuple2;

public class H2Application {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
				          .protocol(HttpProtocol.H2)  // 将客户端配置为仅支持HTTP/2
				          .secure();               // 配置SSL    

		Tuple2<String, HttpHeaders> response =
				client.get()
				      .uri("https://example.com/")
				      .responseSingle((res, bytes) -> bytes.asString()
				                                           .zipWith(Mono.just(res.responseHeaders())))
				      .block();

		System.out.println("Used stream ID: " + response.getT2().get("x-http2-stream-id"));
		System.out.println("Response: " + response.getT1());
	}
}

(2)简单的H2C例子:

import io.netty.handler.codec.http.HttpHeaders;
import reactor.core.publisher.Mono;
import reactor.netty.http.HttpProtocol;
import reactor.netty.http.client.HttpClient;
import reactor.util.function.Tuple2;

public class H2CApplication {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
				          .protocol(HttpProtocol.H2C);

		Tuple2<String, HttpHeaders> response =
				client.get()
				      .uri("http://localhost:8080/")
				      .responseSingle((res, bytes) -> bytes.asString()
				                                           .zipWith(Mono.just(res.responseHeaders())))
				      .block();

		System.out.println("Used stream ID: " + response.getT2().get("x-http2-stream-id"));
		System.out.println("Response: " + response.getT1());
	}
}

协议选择:HttpProtocol.java

public enum HttpProtocol {
	/**
	 * 默认支持HTTP协议httpserver和httpclient
	 */
	HTTP11,

	/**
	 * TLS支持HTTP/2.0如果与HTTP/1.1协议一起使用,HTTP/2.0将是首选协议。在协商应用程序级协议时,可以选择HTTP/2.0或HTTP/1.1。如果在没有使用HTTP/1.1协议的情况下使用,HTTP/2.0将始终作为通信协议提供,而不会回退到HTTP/1
	 */
	H2,

	/**
	 * HTTP/2.0支持明文。如果与HTTP/1.1协议一起使用,将支持H2C“升级”:首先作为HTTP/1.1请求或使用请求,查找HTTP/2.0头和{@literal Connection:upgrade}。如果升级成功或出现回退HTTP/1.1响应,服务器通常会回复成功101状态。成功后,客户端将开始发送HTTP/2.0流量。如果在没有HTTP/1.1协议的情况下使用,将支持H2C“先验知识”:不需要客户机和服务器之间的{@literal Connection:Upgrade}握手,但将不支持回退到HTTP/1.1。
	 */
	H2C
}

代理支持

Reactor Netty支持Netty提供的代理功能,并提供了通过ProxyProvider生成器指定非代理主机的方法。

Netty的HTTP代理支持始终使用CONNECT方法来建立到指定代理的隧道,而不考虑使用HTTP或https的方案。当方案为http时,某些代理可能不支持CONNECT方法,或者可能需要进行配置以支持这种通信方式。有时这可能是无法连接到代理的原因。考虑检查代理文档是否支持或需要附加配置以支持Connect方法。

使用ProxyProvider:

import reactor.netty.http.client.HttpClient;
import reactor.netty.transport.ProxyProvider;

public class Application {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
				          .proxy(spec -> spec.type(ProxyProvider.Proxy.HTTP)
				                             .host("proxy")
				                             .port(8080)
				                             .nonProxyHosts("localhost")
                         // 将连接建立超时配置为20秒
				                             .connectTimeoutMillis(20_000)); 

		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();
	}
}

Metrics监控指标

HTTP客户端支持与Micrometer,它公开了前缀为reactor.netty.http.client的所有指标。

在应用程序中,通常会记录日志以便事后分析,在很多情况下是产生了问题之后,再去查看日志,是一种事后的静态分析。在很多时候,可能需要了解整个系统在当前,或者某一时刻运行的情况,比如一个系统后台服务,可能需要了解一些实时监控的数据
1、每秒钟的请求数是多少(TPS)?
2、平均每个请求处理的时间?
3、请求处理的最长耗时?
4、请求处理的响应的直方图?
5、请求处理正确响应率?
6、等待处理的请求队列长度?
7、查看整个系统的的CPU使用率、内存占用、jvm运行情况;以及系统运行出错率等等一系列的实时数据采集时,最简单的方法就是在系统的入口、出口和关键位置设置埋点,然后将采集到的信息发送到实时监控平台或者存入到缓存和DB中做进一步的分析和展示。

Metrics作为一款监控指标的度量类库,提供了许多工具帮助开发者来完成各项数据的监控。
Metrics提供5种基本的度量类型:Meters、Gauges、Counters、Histograms和Timers

import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;

public class Application {
	public static void main(String[] args) {
		ConnectionProvider provider =
				ConnectionProvider.builder("custom")
				                  .maxConnections(50)
				                  .metrics(true)  // 启用与Micrometer的内置集成
				                  .build();

		HttpClient client = HttpClient.create(provider);

		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response);

		provider.disposeLater()
		        .block();
	}
}

(1)HTTP客户端指标的信息

Metric名称类型描述
reactor.netty.http.client.data.receivedDistributionSummary接收的数据量,以字节为单位
reactor.netty.http.client.data.sentDistributionSummary发送的数据量,以字节为单位
reactor.netty.http.client.errorsCounter发生的错误数
reactor.netty.http.client.tls.handshake.timeTimerTLS 握手所花费的时间
reactor.netty.http.client.connect.timeTimer连接到远程地址所花费的时间
reactor.netty.http.client.address.resolverTimer解析地址所花费的时间
reactor.netty.http.client.data.received.timeTimer消耗传入数据所花费的时间
reactor.netty.http.client.data.sent.timeTimer发送传出数据所花费的时间
reactor.netty.http.client.response.timeTimer请求/响应的总时间

(2)汇总ConnectionProvider指标

Metric名称类型描述
reactor.netty.connection.provider.total.connectionsGauge所有连接数,活动或空闲
reactor.netty.connection.provider.active.connectionsGauge已成功获取且正在使用中的连接数
reactor.netty.connection.provider.max.connectionsGauge允许的最大活动连接数
reactor.netty.connection.provider.idle.connectionsGauge空闲连接数
reactor.netty.connection.provider.pending.connectionsGauge等待连接的请求数
reactor.netty.connection.provider.max.pending.connectionsGauge等待就绪连接时将排队的最大请求数

HTTP客户端指标配置为服务HTTP/2流量时的信息:

Metric名称类型描述
reactor.netty.connection.provider.active.streamsGauge活动HTTP/2流的数量
reactor.netty.connection.provider.pending.streamsGauge等待打开 HTTP/2 流的请求数

(3)ByteBufAllocator指标

Metric名称类型描述
reactor.netty.bytebuf.allocator.used.heap.memoryGauge堆内存的字节数
reactor.netty.bytebuf.allocator.used.direct.memoryGauge直接内存的字节数
reactor.netty.bytebuf.allocator.used.heap.arenasGauge堆区域的数量(当PooledByteBufAllocator)
reactor.netty.bytebuf.allocator.used.direct.arenasGauge直接竞技场的数量(当PooledByteBufAllocator)
reactor.netty.bytebuf.allocator.used.threadlocal.cachesGauge线程本地缓存的数量(当PooledByteBufAllocator)
reactor.netty.bytebuf.allocator.used.small.cache.sizeGauge小缓存的大小(当PooledByteBufAllocator)
reactor.netty.bytebuf.allocator.used.normal.cache.sizeGauge正常缓存的大小(当PooledByteBufAllocator)
reactor.netty.bytebuf.allocator.used.chunk.sizeGauge竞技场的块大小(当PooledByteBufAllocator)

(4)EventLoop指标

Metric名称类型描述
reactor.netty.eventloop.pending.tasksGauge事件循环中待处理的任务数

启用该集成:

import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.config.MeterFilter;
import reactor.netty.http.client.HttpClient;

public class Application {

	public static void main(String[] args) {
		Metrics.globalRegistry  // 对带URI标签的仪表应用上限
		       .config()
		       .meterFilter(MeterFilter.maximumAllowableTags("reactor.netty.http.client", "URI", 100, MeterFilter.deny()));

		HttpClient client =
				HttpClient.create()
				          .metrics(true, s -> {
                              // 模板化 URI 将尽可能用作 URI 标记值
				              if (s.startsWith("/stream/")) { 
				                  return "/stream/{n}";
				              }
				              else if (s.startsWith("/bytes/")) {
				                  return "/bytes/{n}";
				              }
				              return s;
				          }); // 启用与Micrometer的内置集成

		client.get()
		      .uri("https://httpbin.org/stream/2")
		      .responseContent()
		      .blockLast();

		client.get()
		      .uri("https://httpbin.org/bytes/1024")
		      .responseContent()
		      .blockLast();
	}
}

避免启用指标的内存和CPU开销
为了避免启用指标的内存和CPU开销,重要的是尽可能将真实URI转换为模板化URI。如果不转换为类似模板的形式,每个不同的URI都会导致创建一个不同的标签,这会为指标占用大量内存。

始终为带有URI标记的仪表应用上限。在实际URI无法模板化的情况下,配置仪表(meters)数量上限会有所帮助。可以在maximumAllowableTags上找到更多信息。

当需要HTTP客户端指标与系统集成时,Micrometer或者想提供自己的集成Micrometer,可以提供自己的指标记录器:

import reactor.netty.http.client.HttpClient;
import reactor.netty.http.client.HttpClientMetricsRecorder;

import java.net.SocketAddress;
import java.time.Duration;

public class Application {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
        // 启用HTTP客户端指标并提供HttpClientMetricsRecorder实现
				          .metrics(true, CustomHttpClientMetricsRecorder::new); 

		client.get()
		      .uri("https://httpbin.org/stream/2")
		      .response()
		      .block();
	}
}

## Unix域套接字 HTTP当使用本机传输时,客户端支持Unix域套接字(UDS)

如何使用UDS支持:

import io.netty.channel.unix.DomainSocketAddress;
import reactor.netty.http.client.HttpClient;

public class Application {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
                        // 指定DomainSocketAddress将被使用
				          .remoteAddress(() -> new DomainSocketAddress("/tmp/test.sock")); 

		client.get()
		      .uri("/")
		      .response()
		      .block();
	}
}

主机名解析

默认情况下,HttpClient使用 Netty 的域名查找机制异步解析域名。这是JVM内置阻塞解析器的替代方案。

当需要更改默认设置时,可以进行HttpClient如下配置:

import reactor.netty.http.client.HttpClient;

import java.time.Duration;

public class Application {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
                          // 此解析器执行的每个DNS查询的超时将为500毫秒
				          .resolver(spec -> spec.queryTimeout(Duration.ofMillis(500))); 

		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response);
	}
}

切换到JVM内置解析器。进行HttpClient如下配置:

import io.netty.resolver.DefaultAddressResolverGroup;
import reactor.netty.http.client.HttpClient;

public class Application {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
                       // 设置JVM内置解析器。
				          .resolver(DefaultAddressResolverGroup.INSTANCE); 

		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response);
	}
}

可用的配置:

(1)cacheMaxTimeToLive:缓存的DNS资源记录的最大生存时间

缓存的DNS资源记录的最大生存时间(秒)。如果DNS服务器返回的DNS资源记录的生存时间大于此最大生存时间,则此解析器将忽略来自DNS服务器的生存时间并使用此最大生存时间。默认为Integer.MAX_VALUE.

(2)cacheMinTimeToLive:缓存的DNS资源记录的最小生存时间

缓存的DNS资源记录的最小生存时间(秒)。如果DNS服务器返回的DNS资源记录的生存时间小于此最小生存时间,则此解析器将忽略来自DNS服务器的生存时间并使用此最小生存时间。默认值:0。

(3)cacheNegativeTimeToLive:失败的DNS查询的缓存的生存时间

失败的DNS查询的缓存的生存时间(秒)。默认值:0。

(4)completeOncePreferredResolved:解析器查询完成后立即通知

启用此设置后,解析器会在首选地址类型的所有查询完成后立即通知。禁用此设置后,解析器会在所有可能的地址类型完成时发出通知。此配置适用于DnsNameResolver#resolveAll(String)。
默认情况下,启用此设置。

(5)disableOptionalRecord:禁用可选记录的自动包含

禁用可选记录的自动包含,该记录试图向远程DNS服务器提供有关解析器每个响应可以读取多少数据的提示。默认情况下,启用此设置。

(6)disableRecursionDesired:解析器是否必须发送设置了递归所需(RD)标志的DNS查询

指定此解析器是否必须发送设置了递归所需(RD)标志的DNS查询。默认情况下,启用此设置。

(7)hostsFileEntriesResolver:主机文件条目的自定义

设置HostsFileEntriesResolver用于主机文件条目的自定义。默认值:DefaultHostsFileEntriesResolver。

(8)maxPayloadSize:设置数据报包缓冲区的容量

设置数据报包缓冲区的容量(以byte字节为单位)。默认值:4096。

(9)maxQueriesPerResolve:解析主机名时允许发送的最大DNS查询数

设置解析主机名时允许发送的最大DNS查询数。默认值:16。

(10)ndots:进行初始绝对查询之前必须出现在名称中的点数

设置在进行初始绝对查询之前必须出现在名称中的点数。默认值:-1(确定来自Unix上的操作系统的值,否则使用值 1)。

(11)queryTimeout:解析器执行的每个DNS查询的超时时间

设置此解析器执行的每个DNS查询的超时时间(毫秒)。默认值:5000。

(12)resolvedAddressTypes:解析地址的协议族列表

解析地址的协议族列表

(13)bindAddressSupplier:绑定到的本地地址的供应商

要绑定到的本地地址的供应商

(14)roundRobinSelection:服务器提供的目的地地址循环选择

使一个AddressResolverGroup的DnsNameResolver如果多个由名称服务器提供的目的地地址的支持随机选择。见RoundRobinDnsAddressResolverGroup。默认:DnsAddressResolverGroup

(15)runOn:与给定的DNS服务器进行通信LoopResources

与给定的DNS服务器进行通信LoopResources。
默认情况下,使用在客户端级别指定的LoopResources。

(16)searchDomains:解析器的搜索域列表

解析器的搜索域列表。默认情况下,使用系统 DNS 搜索域填充有效搜索域列表。

(17)trace:解析器使用的特定记录器和日志级别

在解析失败的情况下生成详细跟踪信息时,此解析器将使用的特定记录器和日志级别。


超时配置

本节介绍可用于HttpClient. 配置适当的超时可能会改善或解决通信过程中的问题。配置选项可以分组如下:

1.连接池超时

默认情况下,HttpClient使用连接池。当一个请求成功完成并且该连接没有安排关闭时,该连接将返回到连接池,因此可以重新用于处理另一个请求。该连接可能会立即为另一个请求重用,或者可能会在连接池中闲置一段时间。

可用的超时配置选项:
默认情况下,在连接release或acquire操作时检查这些超时,如果达到某个超时,连接将关闭并从连接池中删除。但是,也可以通过设置evictInBackground来配置连接池,以对连接执行定期检查。

要自定义默认设置,可以HttpClient进行如下配置:

import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;

import java.time.Duration;

public class Application {

	public static void main(String[] args) {
		ConnectionProvider provider =
				ConnectionProvider.builder("custom")
							.maxConnections(50)
							// 将连接保持空闲的最长时间配置为20秒
							.maxIdleTime(Duration.ofSeconds(20))
							// 将连接保持活动的最长时间配置为60秒       
							.maxLifeTime(Duration.ofSeconds(60)) 
							// 将挂起获取操作的最长时间配置为60秒          
							.pendingAcquireTimeout(Duration.ofSeconds(60)) 
							// 每两分钟,将定期检查连接池中是否存在适用于删除的连接
							.evictInBackground(Duration.ofSeconds(120))    
							.build();

		HttpClient client = HttpClient.create(provider);

		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response);

		provider.disposeLater()
		        .block();
	}
}
(1)maxIdleTime:连接在连接池中保持空闲的最长时间

此连接在连接池中保持空闲的最长时间(ms)。默认情况下,maxIdleTime未指定。

配置时maxIdleTime,应考虑目标服务器上的空闲超时配置。选择等于或小于目标服务器上的配置。通过这样做,您可以减少由目标服务器关闭的连接引起的 I/O 问题。

(2)maxLifeTime:连接保持活动状态的最长时间

此连接保持活动状态的最长时间(ms)。默认情况下,maxLifeTime未指定。

(3)pendingAcquireTimeout:完成挂起的获取操作或引发PoolAcquireTimeoutException的最长时间

必须完成挂起的获取操作或引发PoolAcquireTimeoutException的最长时间(ms)。默认值:45秒。

2.HttpClient超时

Reactor Netty使用Reactor Core作为其Reactive Streams实现,可能想要使用timeout它Mono和Flux提供的运算符。但是请记住,最好使用Reactor Netty中可用的更具体的超时配置选项,因为它们为特定目的和用例提供了更多控制。相比之下,timeout操作员只能申请整个操作,从建立到远程对等点的连接到接收响应。
注意:提供有关HttpClient级别的各种超时配置选项的信息

(1)响应超时

HttpClient提供用于为所有请求配置默认响应超时的API。可以通过特定请求的API更改此默认响应超时。默认情况下,responseTimeout未指定。

自定义默认设置,可以HttpClient进行如下配置:

import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;

import java.time.Duration;

public class Application {

	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
                          // 将默认响应超时配置为1秒
				          .responseTimeout(Duration.ofSeconds(1));    

		String response1 =
				client.post()
				      .uri("https://example.com/")
				      .send((req, out) -> {  // 将特定请求的响应超时配置为2秒
				          req.responseTimeout(Duration.ofSeconds(2)); 
				          return out.sendString(Mono.just("body1"));
				      })
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response1);

		String response2 =
				client.post()
				      .uri("https://example.com/")
				      .send((req, out) -> out.sendString(Mono.just("body2")))
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response2);
	}
}
(2)连接超时

以下列表显示了所有可用的连接超时配置选项,但其中一些可能仅适用于特定传输。

①:CONNECT_TIMEOUT_MILLIS - 连接超时
如果到远程对等体的连接建立尝试未在配置的连接超时(ms)内完成,则连接建立尝试失败。默认值:30秒。

②:SO_KEEPALIVE - 连接空闲活动时间
当连接空闲一段时间(时间取决于实现,但默认通常为两个小时),TCP自动向keepalive远程对等方发送探测。默认情况下,SO_KEEPALIVE未启用。当您使用Epoll/ NIO(自Java 11起)传输运行时,还可以配置:

配置描述
TCP_KEEPIDLEkeepalive如果SO_KEEPALIVE已设置,此连接在 TCP 开始发送探测之前保持空闲的最长时间(分辨率:秒)。最长时间取决于实现,但默认值通常为两小时。
TCP_KEEPINTVL(Epoll)/ TCP_KEEPINTERVAL(NIO)各个keepalive探针之间的时间(分辨率:秒)。
TCP_KEEPCNT(Epoll)/ TCP_KEEPCOUNT(NIO)keepalive在断开连接之前 TCP 应该发送的最大探测数。

有时,在客户端和服务器之间,可能有一个网络组件,它会在不发送响应的情况下静默删除空闲连接。从Reactor Netty的角度来看,在这个用例中,远程对等方只是不响应。为了能够处理这样的用例,可以考虑配置SO_KEEPALIVE

自定义默认设置,可以HttpClient进行如下配置:

import io.netty.channel.ChannelOption;
import io.netty.channel.epoll.EpollChannelOption;
//import io.netty.channel.socket.nio.NioChannelOption;
//import jdk.net.ExtendedSocketOptions;
import reactor.netty.http.client.HttpClient;
import java.net.InetSocketAddress;

public class Application {
/*
以下选项仅在使用NIO传输(Java11)时可用
.option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPIDLE), 300)
.option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPINTERVAL), 60)	
.option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPCOUNT), 8);
*/
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
				          .bindAddress(() -> new InetSocketAddress("host", 1234))
                           // 将连接建立超时配置为10秒
				          .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) 
 // 启用TCP keepalive。这意味着TCP keepalive在连接空闲一段时间后开始发送探测
				          .option(ChannelOption.SO_KEEPALIVE, true)      
				          // 以下选项仅在使用Epoll传输时可用
                // 在TCP开始发送keepalive探测之前,连接需要保持空闲5分钟
				          .option(EpollChannelOption.TCP_KEEPIDLE, 300)      
               // 将各个keepalive探测之间的时间配置为 1 分钟  
				          .option(EpollChannelOption.TCP_KEEPINTVL, 60) 
                  // 将TCPkeepalive探测的最大数量配置为8       
				          .option(EpollChannelOption.TCP_KEEPCNT, 8);          

		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response);
	}
}
(3)SSL/TLS超时

HttpClient支持Netty提供的SSL/TLS功能。

可用的超时配置选项:
①handshakeTimeout
使用此选项配置SSL握手超时(ms)。默认值:10秒。当网络连接速度较慢时,应该考虑增加SSL握手超时。

②closeNotifyFlushTimeout
使用此选项配置SSLclose_notify刷新超时(ms)。默认值:3秒。

③closeNotifyReadTimeout
使用此选项配置SSLclose_notify读取超时(ms)。默认值:0s。

自定义默认设置,可以HttpClient进行如下配置:

import reactor.netty.http.Http11SslContextSpec;
import reactor.netty.http.client.HttpClient;

import java.time.Duration;

public class Application {
	public static void main(String[] args) {
		Http11SslContextSpec http11SslContextSpec = Http11SslContextSpec.forClient();

		HttpClient client =
				HttpClient.create()
				          .secure(spec -> spec.sslContext(http11SslContextSpec)
                          // 将SSL握手超时配置为30秒
				          .handshakeTimeout(Duration.ofSeconds(30))
                           // 将SSLclose_notify刷新超时配置为10秒        
				          .closeNotifyFlushTimeout(Duration.ofSeconds(10))
                           // 将SSLclose_notify读取超时配置为10秒 
				          .closeNotifyReadTimeout(Duration.ofSeconds(10))); 

		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response);
	}
}
(4)代理超时

HttpClient支持Netty提供的代理功能,并提供了一种指定连接建立超时的方法。如果到远程对等方的连接建立尝试未在超时内完成,则连接建立尝试失败。默认值:10秒。

要自定义默认设置,可以HttpClient进行如下配置:

import reactor.netty.http.client.HttpClient;
import reactor.netty.transport.ProxyProvider;

public class Application {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
				          .proxy(spec -> spec.type(ProxyProvider.Proxy.HTTP)
				                             .host("proxy")
				                             .port(8080)
				                             .nonProxyHosts("localhost")
                     // 将连接建立超时配置为20秒
				                             .connectTimeoutMillis(20_000)); 

		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response);
	}
}
(5)主机名解析超时

默认情况下,HttpClient使用Netty的域名查找机制来异步解析域名

可用的超时配置选项:
①cacheMaxTimeToLive - 缓存的DNS资源记录的最大生存时间
缓存的DNS资源记录的最大生存时间(s)。如果DNS服务器返回的DNS资源记录的生存时间大于此最大生存时间,则此解析器将忽略来自DNS服务器的生存时间并使用此最大生存时间。默认值:Integer.MAX_VALUE。

②cacheMinTimeToLive - 缓存的DNS资源记录的最短生存时间
缓存的DNS资源记录的最短生存时间(s)。如果DNS服务器返回的DNS资源记录的生存时间小于此最短生存时间,则此解析器将忽略来自DNS服务器的生存时间并使用此最短生存时间。默认值:0s。

③cacheNegativeTimeToLive - 失败的DNS查询的缓存的生存时间
失败的DNS查询的缓存的生存时间(s)。默认值:0s。

④queryTimeout - 解析器执行的每个DNS查询的超时时间
设置此解析器执行的每个DNS查询的超时时间(s)。默认值:5秒

自定义默认设置,可以HttpClient进行如下配置:

import reactor.netty.http.client.HttpClient;

import java.time.Duration;

public class Application {
	public static void main(String[] args) {
		HttpClient client =
				HttpClient.create()
							// 此解析器执行的每个DNS查询的超时将为500毫秒
							.resolver(spec -> spec.queryTimeout(Duration.ofMillis(500))); 

		String response =
				client.get()
				      .uri("https://example.com/")
				      .responseContent()
				      .aggregate()
				      .asString()
				      .block();

		System.out.println("Response " + response);
	}
}
  • 18
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未禾

您的支持是我最宝贵的财富!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值