8.Netty HTTP服务器(HttpServer)

目录


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


Netty HTTP服务器(HttpServer)

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

启动和停止

要启动HTTP服务器,必须创建和配置HttpServe实例。默认情况下,host为任何本地地址配置,并且系统在bind调用操作时选择一个临时端口。

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()  // 创建一个准备配置的HttpServer实例
				          .bindNow(); // 以阻塞方式启动服务器并等待它完成初始化

		server.onDispose()
		      .block();
	}
	
}

返回的DisposableServer提供了一个简单的服务器API,包括disposeNow(),它以阻塞方式关闭服务器


主机和端口

要在特定的host和上提供服务port,可以将以下配置应用到HTTP服务器:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				          .host("localhost") // 配置HTTP服务器主机

				          .port(8080)  // 配置HTTP服务器端口   
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

要在多个地址上提供服务,在配置HttpServer之后,可以多次绑定它以获得单独的可处置服务器。所有创建的服务器都将共享诸如“LoopResources”之类的资源,因为它们在引擎盖下使用相同的配置实例。

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class MultiAddressApplication {

	public static void main(String[] args) {
		HttpServer httpServer = HttpServer.create();
		DisposableServer server1 = httpServer
				.host("localhost")  // 配置第一台HTTP服务器主机
				.port(8080)        // 配置第一个HTTP服务器端口
				.bindNow();

		DisposableServer server2 = httpServer
				.host("0.0.0.0")  // 配置第二台HTTP服务器主机
				.port(8081)     // 配置第二个HTTP服务器端口
				.bindNow();

		Mono.when(server1.onDispose(), server2.onDispose())
				.block();
	}
	
}

急切初始化

默认情况下,HttpServer资源的初始化是按需进行的。这意味着bind operation吸收了初始化和加载所需的额外时间:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		HttpServer httpServer =
				HttpServer.create()
				          .handle((request, response) -> request.receive().then());

		httpServer.warmup()   // 初始化并加载事件循环组、本机传输库和用于安全性的本机库
		          .block();

		DisposableServer server = httpServer.bindNow();

		server.onDispose()
		      .block();
	}
	
}

路由HTTP

为HTTP服务器定义路由需要配置提供的HttpServerRoutes构建器:
注意:服务器路由是唯一的,并且只调用声明顺序中的第一个匹配项。

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				          .route(routes -> Routes
				              // 提供GET请求/hello并返回Hello World
                            .get("/hello", (request, response) -> response.sendString(Mono.just("Hello World!")))
                            // 为POST请求提供服务/echo并将接收到的请求正文作为响应返回
				            .post("/echo", (request, response) -> response.send(request.receive().retain()))
                            // 提供GET请求/path/{param}并返回路径参数的值
				            .get("/path/{param}", (request, response) -> response.sendString(Mono.just(request.param("param"))))
                           // 为websocket提供服务/ws,并将接收到的传入数据作为传出数据返回
                           .ws("/ws", (wsInbound, wsOutbound) -> wsOutbound.send(wsInbound.receive().retain())))
                           .bindNow();

		server.onDispose()
		      .block();
	}
	
}

SSE:数据流单指令序列扩展

SSE是“因特网数据流单指令序列扩展(Internet Streaming SIMD Extensions的缩写。SSE除保持原有的MMX指令外,又新增了70条指令,在加快浮点运算的同时,改善了内存的使用效率,使内存速度更快。它对游戏性能的改善十分显著,按Intel的说法,SSE对下述几个领域的影响特别明显:3D几何运算及动画处理、图形处理(如Photoshop)、视频编辑/压缩/解压(如MPEG和DVD)、语音识别以及声音压缩和合成等

配置HTTP服务器以提供服务Server-Sent Events:

import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.HttpServerRequest;
import reactor.netty.http.server.HttpServerResponse;

import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.function.BiFunction;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				          .route(routes -> routes.get("/sse", serveSse()))
				          .bindNow();

		server.onDispose()
		      .block();
	}

	/**
	 * Prepares SSE response
	 * The "Content-Type" is "text/event-stream"
	 * The flushing strategy is "flush after every element" emitted by the provided Publisher
	 */
	private static BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> serveSse() {
		Flux<Long> flux = Flux.interval(Duration.ofSeconds(10));
		return (request, response) ->
		        response.sse()
		                .send(flux.map(Application::toByteBuf), b -> true);
	}

	/**
	 * Transforms the Object to ByteBuf following the expected SSE format.
	 */
	private static ByteBuf toByteBuf(Object any) {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		try {
			out.write("data: ".getBytes(Charset.defaultCharset()));
			MAPPER.writeValue(out, any);
			out.write("\n\n".getBytes(Charset.defaultCharset()));
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
		return ByteBufAllocator.DEFAULT
		                       .buffer()
		                       .writeBytes(out.toByteArray());
	}

	private static final ObjectMapper MAPPER = new ObjectMapper();
}

静态资源(Static Resources)

配置HTTP服务器以提供静态资源:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Application {

	public static void main(String[] args) throws URISyntaxException {
		Path file = Paths.get(Application.class.getResource("/logback.xml").toURI());
		DisposableServer server =
				HttpServer.create()
				          .route(routes -> routes.file("/index.html", file))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

写入数据

要将数据发送到连接的客户端,必须使用handle(…)或route(…)附加I/O处理程序。I/O处理程序有权访问HttpServerResponse,以便能够写入数据:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
                   // hello向连接的客户端发送字符串
				   .handle((request, response) -> response.sendString(Mono.just("hello"))) 
				   .bindNow();

		server.onDispose()
		      .block();
	}
	
}

(1)添加标题和其他元数据

当向连接的客户端发送数据时,可能需要发送额外的headers(头)、cookies、status code(状态码)和其他元数据。可以使用HttpServerResponse:

import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				          .route(routes ->
				              routes.get("/hello",
				                  (request, response) ->
				                      response.status(HttpResponseStatus.OK)
				                              .header(HttpHeaderNames.CONTENT_LENGTH, "12")
				                              .sendString(Mono.just("Hello World!"))))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

(2)压缩:Compression

可以将HTTP服务器配置为发送压缩响应,具体取决于请求标头Accept-Encoding

Reactor Netty提供了三种不同的压缩输出数据的策略:
①:compress(boolean):是否压缩
根据提供的布尔值,启用(true)或禁用(false)压缩

②:compress(int):压缩大小值
一旦响应大小超过给定值(以字节为单位),就会执行压缩

③:compress(BiPredicate<HttpServerRequest, HttpServerResponse>):是否立即压缩
如果谓词返回,则执行压缩true

使用compress方法(设置为true)启用压缩:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Application {

	public static void main(String[] args) throws URISyntaxException {
		Path file = Paths.get(Application.class.getResource("/logback.xml").toURI());
		DisposableServer server =
				HttpServer.create()
				          .compress(true)  // 是否启用压缩
				          .route(routes -> routes.file("/index.html", file))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

消费数据

要从连接的客户端接收数据,必须使用handle(…)或route(…)附加I/O处理程序 。I/O处理程序有权访问HttpServerRequest,以便能够读取数据。

使用该handle(…)方法:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
                          // 从连接的客户端接收数据
				          .handle((request, response) -> request.receive().then()) 
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

读取Headers、URI参数和其他元数据

当从连接的客户端接收数据时,可能需要检查请求标头、参数和其他元数据。可以使用HttpServerRequest获取此附加元数据

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				          .route(routes ->
				              routes.get("/{param}", (request, response) -> {
				                      if (request.requestHeaders().contains("Some-Header")) {
				                          return response.sendString(Mono.just(request.param("param")));
				                      }
				                      return response.sendNotFound();
				                  }))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

读取Post表单和文件数据(Multipart)

当从连接的客户端接收数据时,可能想要访问POST form(application/x-www-form-urlencoded)或multipart(multipart/form-data)数据。可以使用HttpServerRequest获取此数据

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				          .route(routes ->
                              // 接收POST表单/多部分数据
				              routes.post("/multipart", (request, response) -> response.sendString(
				                      request.receiveForm()
				                             .flatMap(data -> Mono.just('[' + data.getName() + ']')))))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

当需要更改默认设置时,可以配置HttpServer或者可以根据请求提供配置:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
                          // HttpServer指定数据仅存储在磁盘上的配置
				          .httpFormDecoder(builder -> builder.maxInMemorySize(0))                  
				          .route(routes ->
				              routes.post("/multipart", (request, response) -> response.sendString(
                          			  // 每个请求的配置,指定如果数据大小超过指定大小,则将数据存储在磁盘上
				                      request.receiveForm(builder -> builder.maxInMemorySize(256)) 
				                             .flatMap(data -> Mono.just('[' + data.getName() + ']')))))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

可用配置

(1)baseDirectory:磁盘存储目录

配置在磁盘上存储数据的目录。默认为生成的临时目录。

(2)charset:字符集编码

配置charset数据语言编码。默认为StandardCharsets#UTF_8.

(3)maxInMemorySize:每个数据的最大内存大小

配置每个数据的最大内存大小,即如果大小大于maxInMemorySize,则将数据写入磁盘,否则它在内存中。如果设置为-1全部内容存储在内存中。如果设置为0整个内容存储在磁盘上。默认为16kb.

(4)maxSize:每个数据的最大值

配置每个数据的最大值。当达到限制时,会引发异常。如果设置为“-1”这意味着没有限制。默认:-1,无限制。

(5)scheduler:解码阶段卸载磁盘操作的调度程序

配置用于在解码阶段卸载磁盘操作的调度程序。默认为Schedulers#boundedElastic()

(6)streaming:数据是否直接从解析的输入缓冲区流中流式传输

当设置为true时,数据直接从解析的输入缓冲区流中流式传输,这意味着它既不存储在内存中也不存储在文件中。
当false时,部分由内存和/或文件存储支持。默认:false

注意:启用流式传输后,提供的数据可能不会处于完整状态,即HttpData#isCompleted()必须进行检查。另外请注意,启用此属性有效地忽略maxInMemorySize、baseDirectory和scheduler。

获取远程(客户端)地址

除了可以从请求中获取的元数据外,还可以接收host(server)地址、remote(client)地址和scheme。根据所选的工厂方法,可以直接从通道或使用Forwarded或X-Forwarded-* HTTP请求标头检索信息:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
                          // 如果可能,指定从Forwarded和X-Forwarded-* HTTP请求标头获取有关连接的信息
				          .forwarded(true) 
				          .route(routes ->
				              routes.get("/clientip",
				                  (request, response) ->
                                      // 返回远程(客户端)对等方的地址
				                      response.sendString(Mono.just(request.remoteAddress().getHostString()))))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

也可以自定义Forwarded或X-Forwarded-*标头处理程序的行为:

import java.net.InetSocketAddress;

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.transport.AddressUtils;

public class CustomForwardedHeaderHandlerApplication {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				          .forwarded((connectionInfo, request) -> {  
                               // 添加自定义标头处理程序
				              String hostHeader = request.headers().get("X-Forwarded-Host");
				              if (hostHeader != null) {
				                  String[] hosts = hostHeader.split(",", 2);
				                  InetSocketAddress hostAddress = AddressUtils.createUnresolved(
				                      hosts[hosts.length - 1].trim(),
				                      connectionInfo.getHostAddress().getPort());
				                  connectionInfo = connectionInfo.withHostAddress(hostAddress);
				              }
				              return connectionInfo;
				          })
				          .route(routes ->
				              routes.get("/clientip",
				                  (request, response) ->
                                       // 返回远程(客户端)对等方的地址
				                      response.sendString(Mono.just(request.remoteAddress() 
		                                                           .getHostString()))))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

HTTP请求解码器

默认情况下,Netty为传入的请求配置一些限制:HttpRequestDecoder和HttpServerUpgradeHandler
(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; // 默认验证标头
public static final int DEFAULT_INITIAL_BUFFER_SIZE = 128; // 默认初始缓冲区大小
// 默认允许重复内容长度
public static final boolean DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS = false; 

HttpRequestDecoderSpec.java

// HTTP/2.0明文升级请求内容的最大长度。默认情况下,服务器将拒绝包含非空内容的升级请求,因为升级请求很可能是GET请求。
public static final int DEFAULT_H2C_MAX_CONTENT_LENGTH = 0;

当需要更改这些默认设置时,可以HTTP按如下方式配置服务器:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
                           // 所有标头的最大长度将为16384. 当超过此值时, 会引发TooLongFrameException
				          .httpRequestDecoder(spec -> spec.maxHeaderSize(16384)) 
				          .handle((request, response) -> response.sendString(Mono.just("hello")))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

生命周期回调

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

回调函数描述
doOnBind在服务器通道即将绑定时调用
doOnBound在绑定服务器通道时调用
doOnChannelInit初始化通道时调用
doOnConnection连接远程客户端时调用
doOnUnbound当服务器通道未绑定时调用

使用doOnConnection和doOnChannelInit回调:

import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import java.util.concurrent.TimeUnit;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
                           // Netty管道ReadTimeoutHandler在连接远程客户端时扩展
				          .doOnConnection(conn ->
				              conn.addHandler(new ReadTimeoutHandler(10, TimeUnit.SECONDS))) 
                           // Netty管道LoggingHandler在初始化通道时扩展
				          .doOnChannelInit((observer, channel, remoteAddress) ->
				              channel.pipeline()
				                     .addFirst(new LoggingHandler("reactor.netty.examples")))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

TCP-level配置

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

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

import io.netty.channel.ChannelOption;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
                           // TCP级别配置
				          .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

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

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

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

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

		server.onDispose()
		      .block();
	}
	
}

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

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

当需要更改默认格式化程序时,可以按如下方式进行配置:

import io.netty.handler.logging.LogLevel;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.transport.logging.AdvancedByteBufFormat;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
                           // 启用连线记录,AdvancedByteBufFormat#TEXTUAL用于打印内容
				          .wiretap("logger-name", LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL) 
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

SSL和TLS

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

以下示例使用SslContextBuilder:

import reactor.netty.DisposableServer;
import reactor.netty.http.Http11SslContextSpec;
import reactor.netty.http.server.HttpServer;
import java.io.File;

public class Application {

	public static void main(String[] args) {
		File cert = new File("certificate.crt");
		File key = new File("private.key");

		Http11SslContextSpec http11SslContextSpec = Http11SslContextSpec.forServer(cert, key);

		DisposableServer server =
				HttpServer.create()
				          .secure(spec -> spec.sslContext(http11SslContextSpec))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

(1)服务器名称指示

可以配置HTTP多个SslContext映射到特定域的服务器。配置SNI映射时可以使用确切的域名或包含通配符的域名。

使用包含通配符的域名:

import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

import java.io.File;

public class Application {

	public static void main(String[] args) throws Exception {
		File defaultCert = new File("default_certificate.crt");
		File defaultKey = new File("default_private.key");

		File testDomainCert = new File("default_certificate.crt");
		File testDomainKey = new File("default_private.key");

		SslContext defaultSslContext = SslContextBuilder.forServer(defaultCert, defaultKey).build();
		SslContext testDomainSslContext = SslContextBuilder.forServer(testDomainCert, testDomainKey).build();

		DisposableServer server =
				HttpServer.create()
				          .secure(spec -> spec.sslContext(defaultSslContext)
				                .addSniMapping("*.test.com",
				                   testDomainSpec -> testDomainSpec.sslContext(testDomainSslContext)))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

HTTP访问日志

可以HTTP以编程方式或通过配置启用访问日志。默认情况下是禁用的。

①可以通过配置“-Dreactor.netty.http.server.accessLogEnabled=true”来启用HTTP访问日志:
以编程方式启用它:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				          .accessLog(true)  // 调用此方法优先于系统属性配置
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

②可以使用以下配置(用于Logback或类似的日志框架)来拥有单独的HTTP访问日志文件:

<appender name="accessLog" class="ch.qos.logback.core.FileAppender">
    <file>access_log.log</file>
    <encoder>
        <pattern>%msg%n</pattern>
    </encoder>
</appender>
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="accessLog" />
</appender>

<logger name="reactor.netty.http.server.AccessLog" level="INFO" additivity="false">
    <appender-ref ref="async"/>
</logger>

默认情况下,日志格式为Common Log Format,但可以指定自定义格式作为参数:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.logging.AccessLog;

public class CustomLogAccessFormatApplication {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				          .accessLog(true, x -> AccessLog.create("method={}, uri={}", x.method(), x.uri()))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

还可以HTTP使用该AccessLogFactory#createFilter方法过滤访问日志:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.logging.AccessLogFactory;

public class FilterLogAccessApplication {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				   .accessLog(true,
 AccessLogFactory.createFilter(p -> !String.valueOf(p.uri()).startsWith("/health/")))
				   .bindNow();

		server.onDispose()
		      .block();
	}
	
}

请注意,此方法也可以采用自定义格式参数:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.logging.AccessLog;
import reactor.netty.http.server.logging.AccessLogFactory;

public class CustomFormatAndFilterAccessLogApplication {

	public static void main(String[] args) {
		DisposableServer server =
			HttpServer.create()
                 // 指定要使用的过滤谓词
				.accessLog(true, AccessLogFactory.createFilter(p -> !String.valueOf(p.uri()).startsWith("/health/"), 
						          x -> AccessLog.create("method={}, uri={}", x.method(), x.uri()))) // 指定要应用的自定义格式
						          
				.bindNow();

		server.onDispose()
		      .block();
	}
	
}

HTTP/2

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

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

例如:netty-tcnative-boringssl-static

什么是HTTP/2?

HTTP/2通过引入标头字段压缩并允许在同一连接上进行多个并发交换,从而更有效地使用网络资源并减少延迟感知。它还引入了从服务器到客户端的主动推送。

HTTP/2由两个规范组成:
(1)超文本传输协议版本2(Hypertext) - RFC7540
(2)HPACK - HTTP/2的头压缩 - RFC7541

(1)简单的H2例子:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.Http2SslContextSpec;
import reactor.netty.http.HttpProtocol;
import reactor.netty.http.server.HttpServer;
import java.io.File;

public class H2Application {

	public static void main(String[] args) {
		File cert = new File("certificate.crt");
		File key = new File("private.key");
		Http2SslContextSpec http2SslContextSpec = Http2SslContextSpec.forServer(cert, key);

		DisposableServer server =
				HttpServer.create()
				          .port(8080)
                           // 将服务器配置为仅支持 HTTP/2
				          .protocol(HttpProtocol.H2)                            
                           // 配置SSL
				          .secure(spec -> spec.sslContext(http2SslContextSpec)) 
				          .handle((request, response) -> response.sendString(Mono.just("hello")))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

(2)简单的H2C例子:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.HttpProtocol;
import reactor.netty.http.server.HttpServer;

public class H2CApplication {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				          .port(8080)
				          .protocol(HttpProtocol.H2C)
				          .handle((request, response) -> response.sendString(Mono.just("hello")))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

协议选择: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
}

Metrics(监控指标)

HTTP服务器支持内置集成的Micrometer,公开了前缀为reactor.netty.http.server的所有指标

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

始终为带有URI标记的计量器应用上限。在无法对真实URI进行模板化的情况下,,配置Metrics数量上限会有所帮助。可以在maximumAllowableTags上找到更多信息。

import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.config.MeterFilter;
import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

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

		DisposableServer server =
				HttpServer.create()
                           // 模板化URI将尽可能用作URI标记值
				          .metrics(true, s -> {
				              if (s.startsWith("/stream/")) { 
				                  return "/stream/{n}";
				              }
				              else if (s.startsWith("/bytes/")) {
				                  return "/bytes/{n}";
				              }
				              return s;
				          }) 
                           // 启用与 Micrometer 的内置集成
				          .route(r ->
				              r.get("/stream/{n}",
				                   (req, res) -> res.sendString(Mono.just(req.param("n"))))
				               .get("/bytes/{n}",
				                   (req, res) -> res.sendString(Mono.just(req.param("n")))))
				          .bindNow();

		server.onDispose()
		      .block();
	}
}

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

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.HttpServerMetricsRecorder;

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

public class Application {

	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
							// 启用HTTP服务器指标并提供HttpServerMetricsRecorder实现
				          .metrics(true, CustomHttpServerMetricsRecorder::new) 
				          .route(r ->
				              r.get("/stream/{n}",
				                   (req, res) -> res.sendString(Mono.just(req.param("n"))))
				               .get("/bytes/{n}",
				                   (req, res) -> res.sendString(Mono.just(req.param("n")))))
				          .bindNow();

		server.onDispose()
		      .block();
	}
	
}

(1)HTTP服务器指标的信息

Metrics名称类型描述
reactor.netty.http.server.data.receivedDistributionSummary接收的数据量,以字节为单位
reactor.netty.http.server.data.sentDistributionSummary发送的数据量,以字节为单位
reactor.netty.http.server.errorsCounter发生的错误数
reactor.netty.http.server.data.received.timeTimer消耗传入数据所花费的时间
reactor.netty.http.server.data.sent.timeTimer发送传出数据所花费的时间
reactor.netty.http.server.response.timeTimer请求/响应的总时间

(2)ByteBufAllocator指标

Metrics名称类型描述
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)

(3)EventLoop指标

Metrics名称类型描述
reactor.netty.eventloop.pending.tasksGauge事件循环中待处理的任务数
  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Netty是一个基于Java的异步事件驱动的网络应用框架,它提供了高性能、可扩展的网络编程能力。Netty的核心是NIO(非阻塞I/O)模型,它允许在单个线程中处理多个并发连接,从而提高了网络应用的吞吐量和性能。 Netty提供了许多开箱即用的组件,其中包括HTTP服务器。通过使用NettyHTTP服务器组件,您可以轻松地构建和部署自己的HTTP服务器。 下面是一个简单的示例,演示如何使用Netty构建一个简单的HTTP服务器: ```java import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; public class HttpServerExample { public static void main(String[] args) throws Exception { // 创建两个EventLoopGroup,一个用于接收连接,一个用于处理连接的I/O操作 EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { // 创建ServerBootstrap实例,用于引导和绑定服务器 ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { // 添加HTTP编解码器 ch.pipeline().addLast(new HttpServerCodec()); // 添加自定义的HTTP请求处理器 ch.pipeline().addLast(new HttpServerHandler()); } }); // 绑定端口并启动服务器 ChannelFuture future = serverBootstrap.bind(8080).sync(); System.out.println("Netty HTTP Server started on port 8080."); // 等待服务器关闭 future.channel().closeFuture().sync(); } finally { // 关闭EventLoopGroup bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ``` 在上面的示例中,我们创建了一个`ServerBootstrap`实例,并配置了两个`EventLoopGroup`,一个用于接收连接,一个用于处理连接的I/O操作。然后,我们指定了服务器的通道类型为`NioServerSocketChannel`,并添加了一个`LoggingHandler`用于打印日志。接下来,我们创建了一个`ChannelInitializer`,并在其中添加了一个`HttpServerCodec`用于处理HTTP编解码,以及一个自定义的`HttpServerHandler`用于处理HTTP请求。最后,我们绑定了服务器的端口并启动服务器。 请注意,上述示例中的`HttpServerHandler`是一个自定义的处理器,您可以根据自己的需求来实现它。它负责处理接收到的HTTP请求,并返回相应的HTTP响应。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

未禾

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

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

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

打赏作者

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

抵扣说明:

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

余额充值