java后端响应式编程从理论到实践

堵塞与非堵塞原理

传统硬件的堵塞(IO)如下,从内存中读取数据,然后写到磁盘,而CPU一直等到磁盘写完成,磁盘的写操作是慢的,这段时间CPU被堵塞不能发挥效率。
在这里插入图片描述

使用非堵塞(NIO)的DMA如下图:CPU只是发出写操作这样的指令,做一些初始化工作,DMA具体执行,从内存中读取数据,然后写到磁盘,当完成写后发出一个中断事件给CPU。这段时间CPU是空闲的,可以做别的事情。这个原理称为Zero.copy零拷贝。
在这里插入图片描述

非阻塞IO模型

几种 IO 模型比较

在这里插入图片描述

Linux 的 IO 多路复用模型

IO 多路复用模型又称为事件驱动模型。IO 多路复用通过把多个 IO 阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下,可以同时处理多个 client 请求。IO 多路复用实际上就是通过一种机制,一个进程可以监视多个描 fd,一旦某个 fd 就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,目前支持 IO 多路复用的系统有 select、pselect、poll、epoll,但它们本质上都是同步 IO。

多路复用三种模型的区别

类别 select poll epoll
支持的最大连接数 由 FD_SETSIZE 限制
1024(x86) or 2048(x64)
基于链表存储,没有限制 受系统最大句柄数限制
IO效率 每次调用进行线性变量,时间复杂度O(n) 同 select 使用“事件”通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdlist里面,这样epoll_wait返回的时候应用就拿到了就绪的fd。时间复杂度O(1)
fd 剧增的影响 线性扫描 fd 导致性能很低 同 select 基于 fd 上 callback 实现,没有性能下降的问题
消息传递机制 内核需要将消息传递到用户空间,需要内核拷贝 同 select epoll 通过内核与用户空间共享内存来实现

Java的NIO

Netty是一个高性能事件驱动的异步的非堵塞的IO(NIO)框架,用于建立TCP等底层的连接,基于Netty可以建立高性能的Http服务器。Netty的一个线程服务于很多请求,如下图,当从Java NIO获得一个Selector事件,将激活通道Channel。
在这里插入图片描述

WebClient

WebClient是spring的一个响应式客户端,它提供了RestTemplate的替代方法。它公开了一个功能齐全、流畅的API,非阻塞的、被动的客户端执行HTTP请求。WebClient使用与WebFlux服务器应用程序相同的编解码器,并与服务器功能Web框架共享公共基本包。默认情况下,它使用Reactor Netty作为HTTP客户端库,但其他人可以通过自定义ClientHttpConnector插入。

WebClient运行时核心类库
在这里插入图片描述
WebClient请求就是这么简单

WebClient.builder()
	.baseUrl("http://www.baidu.com")
	.build()
	.get()
	.exchange()
	.flatMap(response->response.bodyToMono(String.class))
	.subscribe(s->{
			System.out.println(s);
	});

运行时核心代码分析

  1. 订阅http请求响应
//reactor.core.publisher.Mono
public final Disposable subscribe(
		@Nullable Consumer<? super T> consumer,
		@Nullable Consumer<? super Throwable> errorConsumer,
		@Nullable Runnable completeConsumer,
		@Nullable Consumer<? super Subscription> subscriptionConsumer) {
	return subscribeWith(new LambdaMonoSubscriber<>(consumer, errorConsumer,
			completeConsumer, subscriptionConsumer));
}
//reactor.ipc.netty.http.client.MonoHttpClientResponse
public void subscribe(final CoreSubscriber<? super HttpClientResponse> subscriber) {
	ReconnectableBridge bridge = new ReconnectableBridge();
	bridge.activeURI = startURI;

	Mono.defer(() -> parent.client.newHandler(new HttpClientHandler(this, bridge),
			parent.options.getRemoteAddress(bridge.activeURI),
			HttpClientOptions.isSecure(bridge.activeURI),
			bridge))
	    .retry(bridge)
	    .cast(HttpClientResponse.class)
	    .subscribe(subscriber);
}
  1. 发起http请求
//reactor.ipc.netty.http.client.MonoHttpClientResponse
public Publisher<Void> apply(NettyInbound in, NettyOutbound out) {
	try {
		URI uri = bridge.activeURI;
		HttpClientOperations ch = (HttpClientOperations) in;
		String host = uri.getHost();
		int port = uri.getPort();
		if (port != -1 && port != 80 && port != 443) {
			host = host + ':' + port;
		}
		ch.getNettyRequest()
		  .setUri(uri.getRawPath() + (uri.getQuery() == null ? "" :
				  "?" + uri.getRawQuery()))
		  .setMethod(parent.method)
		  .setProtocolVersion(HttpVersion.HTTP_1_1)
		  .headers()
		  .add(HttpHeaderNames.HOST, host)
		  .add(HttpHeaderNames.ACCEPT, ALL);

		if (Objects.equals(parent.method, HttpMethod.GET)
				|| Objects.equals(parent.method, HttpMethod.HEAD)
				|| Objects.equals(parent.method, HttpMethod.DELETE)) {
			ch.chunkedTransfer(false);
		}

		if (parent.handler != null) {
			return parent.handler.apply(ch);
		}
		else {
			return ch.send();
		}
	}
	catch (Throwable t) {
		return Mono.error(t);
	}
}
  1. 接收http数据
    exchange()
//org.springframework.web.reactive.function.client.ExchangeFunctions
public Mono<ClientResponse> exchange(ClientRequest request) {
	return this.connector
		.connect(request.method(), request.url(),
				clientHttpRequest -> request.writeTo(clientHttpRequest, this.strategies))
		.doOnSubscribe(subscription -> logger.debug("Subscriber present"))
		.doOnRequest(n -> logger.debug("Demand signaled"))
		.doOnCancel(() -> logger.debug("Cancelling request"))
		.map(response -> {
			if (logger.isDebugEnabled()) {
				int status = response.getRawStatusCode();
				HttpStatus resolvedStatus = HttpStatus.resolve(status);
				logger.debug("Response received, status: " + status +
						(resolvedStatus != null ? " " + resolvedStatus.getReasonPhrase() : ""));
			}
			return new DefaultClientResponse(response, this.strategies);
		});
}
  1. 合并数据
//org.springframework.core.io.buffer.DataBufferUtils
public static Mono<DataBuffer> join(Publisher<DataBuffer> dataBuffers) {
	return Flux.from(dataBuffers)
			.collectList()
			.filter(list -> !list.isEmpty())
			.map(list -> {
				DataBufferFactory bufferFactory = list.get(0).factory();
				return bufferFactory.join(list);
			});
}
  1. 转换数据
    flatMap(response->response.bodyToMono(String.class));
public Mono<T> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType,
		@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
	return DataBufferUtils.join(inputStream)
			.map(buffer -> decodeDataBuffer(buffer, elementType, mimeType, hints));
}

protected String decodeDataBuffer(DataBuffer dataBuffer, ResolvableType elementType,
		@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
	Charset charset = getCharset(mimeType);
	CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer());
	DataBufferUtils.release(dataBuffer);
	return charBuffer.toString();
}

reactor core核心接口分析
构建

接口 说明 举例
from 从其他响应式流中创建reactor core的流,适用于所有不同reactor stream的实现间的适配 Mono.from(Publisher<? extends T> source)
defer 延迟建立新的流,defer一开始不会马上创建Publisher,直到有订阅者订阅时才会创建,且每次都创建全新的 Publisher。 Mono.defer(Supplier<? extends Mono<? extends T>> supplier)
just 基于对象创建流,参数是流将要发送的元素 Mono.just(T t)

转换

接口 说明 举例
map 将流的元素按指定的方法变换 map(o-> o.toString())
flatMap 异步转换当前流发出的项,返回另一个流发出的值(可能更改值类型) flatMap(s->Mono.just(s))

flatMap适用两种场景
a. 将当前流的元素转换为多个元素,并在新的流中逐一发出转换的多个元素。这里的flat平铺就是这个含义。
b. 由当前流的元素事件触发一个新的流。在http请求场景下,Mono<ClientResponse> clientResponse = request.exchange(); clientResponse这个流在http请求response header返回时得到了流元素ClientResponse,但此时response body还在管道中传输,ClientResponse实际没有拿到完整的body内容,所以ClientResponse将body封装到了一个新的流中,如果下游需要取到这个流,就必须通过flatMap获取。

参考文档
Unix 网络 IO 模型及 Linux 的 IO 多路复用模型: https://matt33.com/2017/08/06/unix-io/
Netty 之 Java 的 I/O 演进之路: https://www.jianshu.com/p/22f8586bb6f3
I/O多路复用和异步I/O:https://www.cnblogs.com/bigberg/p/8034629.html
I/O模型与多路复用: http://www.rainybowe.com/blog/2016/09/12/IO%E6%A8%A1%E5%9E%8B%E4%B8%8E%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/index.html
使用 Reactor 进行反应式编程
https://www.ibm.com/developerworks/cn/java/j-cn-with-reactor-response-encode/index.html?lnk=hmhm
Reactive响应式编程
https://www.jdon.com/reactive.html
那些年我们错过的响应式编程
http://www.open-open.com/lib/view/open1429151543096.html
Reactor Reactive编程介绍
https://luyiisme.github.io/2017/02/11/spring-reactor-programing/
Reactor 入门与实践
https://studygolang.com/articles/11341?fr=sidebar
Reactor 实例解析
https://www.infoq.cn/article/reactor-by-example
Netty原理和使用
https://www.jdon.com/concurrent/netty.html
使用 UT 玩转 defer 和 retryWhen
https://www.jianshu.com/p/d42793eba591
照虎画猫深入理解响应式流规范
http://blog.51cto.com/liukang/2090922
Spring 5 WebClient
https://www.baeldung.com/spring-5-webclient

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章