深入分析 Netty 源码(3)
EventLoop
一个Netty 程序启动时,至少要指定一个EventLoopGroup(如果使用到的是NIO, 那么通常是NioEventlLoopGroup),那么这个NioEventLoopGroup 在Netty中到底扮演着什么角色呢?我们知道,Netty是Reactor模型的一个实现,那么首先从Reactor的线程模型开始吧.
关于Reactor的线程模型
首先我们来看一下Reactor 的线程模型.
Reactor的线程模型有三种:
- 单线程模型
- 多线程模型
- 主从模型
首先看单线程模型:
所谓单线程,即acceptor 处理和handler 处理都在一个线程中处理.这个模型的坏处显而易见:当其中某个handler 阻塞时,会导致其他所有的client 的handler 都得不到执行,并且更严重的是,handler 的阻塞也会导致整个服务不能接收新的client 请求(因为acceptor也被阻塞了).因为有这么多的缺陷,因此单线程Reactor模型用的比较少.
那么什么是多线程模型呢? Reactor的多线程模型与单线程模型的区别就是acceptor 是一个单独的线程处理,并且有一组特定的NIO线程来负责各个客户端连接的IO操作.
Reactor 多线程模型如下:
Reactor多线程模型有如下特点:
- 有专门一个线程,即Acceptor 线程用于监听客户端的TCP连接请求.
- 客户端连接的IO操作都是由一个特定的NIO线程池负责.每个客户端连接都与一个特定的NIO线程绑定,因此在这个客户端连接中的所有IO操作都是在同一个线程中完成的.
- 客户端连接有很多,但是NIO线程数是比较少的,因此一个NIO 线程可以同时绑定到多个客户端连接中.
接下来我们再来看一下Reactor的主从多线程模型.
一般情况下,Reactor 的多线程模式已经可以很好的工作了,但是我们考虑一下如下情况:如果我们的服务器需要同时处理大量的客户端连接请求或我们需要在客户端连接时,进行一些权限的检查,那么单线程的Acceptor 很有可能就处理不过来,造成了大量的客户端不能连接到服务器.
Reactor的主从多线程模型就是在这样的情况下提出来的,它的特点是:服务器端接收客户端的连接请求不再是一个线程,而是由一个独立的线程池组成.它的线程模型如下:
可以看到,Reactor 的主从多线程模型和Reactor多线程模型很类似,只不过Reactor 的主从多线程模型的acceptor 使用了线程池来处理大量的客户端请求.
NioEventLoopGroup 与Reactor 线程模型的对应
我们介绍了三种Reactor 的线程模型,那么它们和NioEventLoopGroup 又有什么关系呢?其实,不同的设置NioEventLoopGroup的方式就对应了不同的Reactor 的线程模型.
单线程模型
来看一下下面的例子:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap server = new ServerBootstrap();
server.group( bossGroup);
注意,我们实例化了一个NioEventLoopGroup, 然后接着我们调用server.group (bossGroup)设置了服务器端的EventLoopGroup.有人可能会有疑惑:我记得在启动服务器端的Netty 程序时,是需要设置bossGroup 和workerGroup 的,为什么这里就只有一个bossGroup?其实很简单,ServerBootstrap重写了group方法:
public ServerBootstrap group(EventLoopGroup group) {return group(group, group);
}
因此当传入一个group 时,那么bossGroup和workerGroup 就是同一个NioEventLoopGroup了.
这时候呢,因为bossGroup 和workerGroup就是同一个NioEventLoopGroup, 并且这个NioEventLoopGroup只有一个线程,这样就会导致Netty中的acceptor 和后续的所有客户端连接的IO操作都是在一个线程中处理的.那么对应到Reactor 的线程模型中,我们这样设置NioEventLoopGroup时,就相当于Reactor 单线程模型.
多线程模型
同理,再来看一下下面的例子:
EventLoopGroup bossGroup = new NioEventLoopGroup(128) ;
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup);
将bossGroup的参数就设置为大于1的数,其实就是Reactor 多线程模型.
主从线程模型
实现主从线程模型的例子如下:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup)
bossGroup为主线程,而workerGroup中的线程是CPU 核心数乘以2,因此对应的到Reactor线程模型中,我们知道,这样设置的NioEventLoopGroup其实就是Reactor 主从多线程模型.
NioEventLoopGroup 类层次结构
NioEventLoopGroup 实例化过程
即:
- EventLoopGroup(其实是MultithreadEventExecutorGroup) 内部维护一个类型为EventExecutorchildren数组,其大小是nThreads, 这样就构成了一个线程池
- 如果我们在实例化NioEventLoopGroup时,如果指定线程池大小,则nThreads 就是指定的值,反之是处理器核心数*2
- MultithreadEventExecutorGroup中会调用newChild 抽象方法来初始化children数组
- 抽象方法newChild 是在NioEventLoopGroup 中实现的,它返回一个NioEventLoop 实例
- NioEventLoop 属性:
SelectorProvider provider 属性:NioEventLoopGroup 构造器通过SelectorProvider. provider() 获取一个 SelectorProvider
Selector selector 属性: NioEventLoop 构造器中通过调用 selector = provider. openSelector() 获取一个 selector 对象
NioEventLoop类层次结构
NioEventLoop继承于SingleThreadEventLoop,而SingleThreadEventLoop 又继承于SingleThreadEventExecutor. SingleThreadEventExecutor 是Netty 中对本地线程的抽象,它内部有一个Thread thread 属性,存储了一个本地Java 线程。因此我们可以认为,一个NioEventLoop其实和一个特定的线程绑定,并且在其生命周期内,绑定的线程都不会再改变.
NioEventLoop的类层次结构图还是比较复杂的,不过我们只需要关注几个重要的点即可.首先NioEventLoop的继承链如下:
NioEventLoop->SingleThreadEventLoop->SingleThreadEventExecutor->AbstractScheduledEventExecutor
在AbstractScheduledEventExecutor中,Netty 实现了NioEventLoop 的schedule 功能,即我们可以通过调用一个NioEventLoop 实例的schedule 方法来运行一些定时任务。而在SingleThreadEventLoop中,又实现了任务队列的功能,通过它,我们可以调用一个NioEventLoop实例的execute 方法来向任务队列中添加一个task,并由NioEventlLoop 进行调度执行.
通常来说,NioEventLoop肩负着两种任务,第一个是作为IO线程,执行与Channel 相关的IO操作,包括调用select 等待就绪的I0事件、读写数据与数据的处理等;而第二个任务是作为任务队列,执行taskQueue 中的任务,例如用户调用eventLoop. schedule提交的定时任务也是这个线程执行的.
NioEventLoop的实例化过程
从上图可以看到,SingleThreadEventExecutor 有一个名为 thread 的 Thread 类型字段,这个字段就代表了与 SingleThreadEventExecutor 关联的本地线程。
private void doStartThread() {assert thread == null;executor.execute(new Runnable() {@Overridepublic void run() {thread = Thread.currentThread();if (interrupted) {thread.interrupt();}boolean success = false;updateLastExecutionTime();try {SingleThreadEventExecutor.this.run();success = true;} catch (Throwable t) {logger.warn("Unexpected exception from an event executor: ", t);} finally {for (;;) {int oldState = STATE_UPDATER.get(SingleThreadEventExecutor.this);if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {break;}}// Check if confirmShutdown() was called at the end of the loop.if (success && gracefulShutdownStartTime == 0) {logger.error("Buggy " + EventExecutor.class.getSimpleName() + " implementation; " +SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must be called " +"before run() implementation terminates.");}try {// Run all remaining tasks and shutdown hooks.for (;;) {if (confirmShutdown()) {break;}}} finally {try {cleanup();} finally {STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);threadLock.release();if (!taskQueue.isEmpty()) {logger.warn("An event executor terminated with " +"non-empty task queue (" + taskQueue.size() + ')');}terminationFuture.setSuccess(null);}}}}});
}
EventLoop 与 Channel 的关联
Netty 中,每个 Channel 都有且仅有一个 EventLoop 与之关联,它们的关联过程如下:
从上图中我们可以看到,当调用了AbstractChannel$AbstractUnsafe.register后,就完成了Channel和EventLoop 的关联. register 实现如下:
public final void register(EventLoop eventLoop, final ChannelPromise promise) {...AbstractChannel.this.eventLoop = eventLoop;if (eventLoop.inEventLoop()) {register0(promise);} else {try {eventLoop.execute(new Runnable() {@Overridepublic void run() {register0(promise);}});} catch (Throwable t) {...}}
}
在 AbstractChannel¥AbstractUnsafe.register中,会将一个EventLoop赋值给AbstractChannel 内部的 eventLoop 字段,到这里就完成了 EventLoop 与 Channel 的关联过程。
EventLoop 的启动
在前面我们已经知道了,NioEventLoop本身就是一个SingleThreadEventExecutor
, 因此NioEventLoop的启动,其实就是NioEventLoop所绑定的本地Java线程的启动.
依照这个思想,我们只要找到在哪里调用了SingleThreadEventExecutor 的thread 字段的start()方法就可以知道是在哪里启动的这个线程了.
从代码中搜索,thread.start()被封装到SingleThreadEventExecutor.startThread() 方法中了:
private void startThread() {if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {doStartThread();}}
}
STATE_ UPDATER是SingleThreadEventExecutor内部维护的一个属性,它的作用是标识当前的thread的状态,在初始的时候,STATE_UPDATER == ST_NOT_STARTED,因此第一次调用startThread()方法时,就会进入到if语句内,进而调用到thread.start().
而这个关键的startThread() 方法又是在哪里调用的呢?经过方法调用关系搜索,我们发现,startThread是在SingleThreadEventExecutor.exqcute
方法中调用的:
public void execute(Runnable task) {if (task == null) {throw new NullPointerException("task");}boolean inEventLoop = inEventLoop();if (inEventLoop) {addTask(task);} else {// 调用 startThread 方法,启动EventLoop线程startThread();addTask(task);if (isShutdown() && removeTask(task)) {reject();}}if (!addTaskWakesUp && wakesUpForTask(task)) {wakeup(inEventLoop);}
}
既然如此,那现在我们的工作就变为了寻找在哪里第一次调用了SingleThreadEventExecutor. execute()方法.
在前面EventLoop 与Channel 的关联这一小节时,有提到到在注册channel 的过程中,会在AbstractChannelSAbstractUnsafe.register中调用eventlLoop.execute方法,在EventLoop 中进行Channel注册代码的执行,AbstractChannel$AbstractUnsafe.register部分代码如下:
public final void register(EventLoop eventLoop, final ChannelPromise promise) {...AbstractChannel.this.eventLoop = eventLoop;if (eventLoop.inEventLoop()) {register0(promise);} else {try {eventLoop.execute(new Runnable() {@Overridepublic void run() {register0(promise);}});} catch (Throwable t) {...}}
}
很显然,一路从Bootstrap.bind方法跟踪到AbstractChannel$AbstractUnsafe.register 方法,整个代码都是在主线程中运行的,因此上面的eventLoop.inEventLoop()就为false, 于是进入到else分支,在这个分支中调用了eventLoop.execute.eventLoop ,
是一个NioEventLoop 的实例,而NioEventLoop 没有实现execute方法,因此调用的是SingleThreadEventExecutor.execute。
我们已经分析过了,inEventLoop == false, 因此执行到else分支,在这里就调用了startThread()方法来启动SingleThreadEventExecutor内部关联的Java 本地线程了.
总结一句话,当EventLoop.execute第一次被调用时,就会触发startThread() 的调用,进而导致了EventLoop 所对应的Java线程的启动.
Promise 与 Future
java.util.concurrent.Future
是Java提供的接口,表示异步执行的状态,Future 的get方法会判断任务是否执行完成,如果完成就返回结果,否则阻塞线程,直到任务完成。
Netty扩展了Java的Future,最主要的改进就是增加了监听器Listener接口,通过监听器可以让异步执行更加有效率,不需要通过get来等待异步执行结束,而是通过监听器回调来精确地控制异步执行结束的时间点。
public interface Future<V> extends java.util.concurrent.Future<V> {boolean isSuccess();boolean isCancellable();Throwable cause();Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners);Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners);Future<V> sync() throws InterruptedException;Future<V> syncUninterruptibly();Future<V> await() throws InterruptedException;Future<V> awaitUninterruptibly();boolean await(long timeout, TimeUnit unit) throws InterruptedException;boolean await(long timeoutMillis) throws InterruptedException;boolean awaitUninterruptibly(long timeout, TimeUnit unit);boolean awaitUninterruptibly(long timeoutMillis);V getNow();@Overrideboolean cancel(boolean mayInterruptIfRunning);
}
ChannelFuture 接口扩展了Netty 的Future接口,表示一种没有返回值的异步调用,同时关联了 Channel,跟一个 Channel 绑定:
public interface ChannelFuture extends Future<Void> {Channel channel();ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> listener);ChannelFuture addListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);ChannelFuture removeListener(GenericFutureListener<? extends Future<? super Void>> listener);ChannelFuture removeListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);ChannelFuture sync() throws InterruptedException;ChannelFuture syncUninterruptibly();ChannelFuture await() throws InterruptedException;ChannelFuture awaitUninterruptibly();boolean isVoid();
}
Promise 接口也扩展了Future接口,它表示一种可写的Future,就是可以设置异步执行的结果:
public interface Promise<V> extends Future<V> {Promise<V> setSuccess(V result);boolean trySuccess(V result);Promise<V> setFailure(Throwable cause);boolean tryFailure(Throwable cause);boolean setUncancellable();Promise<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);Promise<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners);Promise<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);Promise<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners);Promise<V> await() throws InterruptedException;Promise<V> awaitUninterruptibly();Promise<V> sync() throws InterruptedException;Promise<V> syncUninterruptibly();
}
ChannelPromise接口扩展了Promise和ChannelFuture,绑定了Channel,又可写异步执行结构,又具备了监听者的功能,是Netty实际编程使用的表示异步执行的接口:
public interface ChannelPromise extends ChannelFuture, Promise<Void> {Channel channel();ChannelPromise setSuccess(Void result);ChannelPromise setSuccess();boolean trySuccess();ChannelPromise setFailure(Throwable cause);ChannelPromise addListener(GenericFutureListener<? extends Future<? super Void>> listener);ChannelPromise addListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);ChannelPromise removeListener(GenericFutureListener<? extends Future<? super Void>> listener);ChannelPromise removeListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);ChannelPromise sync() throws InterruptedException;ChannelPromise syncUninterruptibly();ChannelPromise await() throws InterruptedException;ChannelPromise awaitUninterruptibly();ChannelPromise unvoid();
}
DefaultChannelPromise是ChannelPromise的实现类,它是实际运行时的Promoise实例。
Netty使用addListener的方式来回调异步执行的结果。
看一下DefaultPromise的addListener方法,它判断异步任务执行的状态,如果执行完成,就理解通知监听者,否则加入到监听者队列通知监听者就是找一个线程来执行调用监听的回调函数。
public Promise<V> addListener(GenericFutureListener<? extends Future<? super V>> listener) {checkNotNull(listener, "listener");synchronized (this) {addListener0(listener);}if (isDone()) {notifyListeners();}return this;
}
private void addListener0(GenericFutureListener<? extends Future<? super V>> listener) {if (listeners == null) {listeners = listener;} else if (listeners instanceof DefaultFutureListeners) {((DefaultFutureListeners) listeners).add(listener);} else {listeners = new DefaultFutureListeners((GenericFutureListener<? extends Future<V>>) listeners, listener);}
}
private void notifyListeners() {EventExecutor executor = executor();if (executor.inEventLoop()) {final InternalThreadLocalMap threadLocals = InternalThreadLocalMap.get();final int stackDepth = threadLocals.futureListenerStackDepth();if (stackDepth < MAX_LISTENER_STACK_DEPTH) {threadLocals.setFutureListenerStackDepth(stackDepth + 1);try {notifyListenersNow();} finally {threadLocals.setFutureListenerStackDepth(stackDepth);}return;}}safeExecute(executor, new Runnable() {@Overridepublic void run() {notifyListenersNow();}});
}
再来看监听者接口,就是个方法,即等异步任务执行完后,拿到Future结果,执行回调逻辑:
public interface GenericFutureListener<F extends Future<?>> extends EventListener {void operationComplete(F future) throws Exception;
}
Handler
ChannelHandlerContext
每个 ChannelHandler 被添加到 ChannelPipeline 后, 都会创建一个 ChannelHandlerContext 并与之创建的ChannelHandler关联绑定。ChannelHandlerContext允许ChannelHandler与其他的ChannelHandler实现进行交互。ChannelHandlerContext不会改变添加到其中的ChannelHandler,因此它是安全的。
下图显示 ChannelHandlerContext,ChannelHandler,ChannelPipeline 的关系:
Channel 的状态模型
Netty有一个简单但强大的状态模型,并完美映射到Channel InboundHandler的各个方法。下面是Channel生命周期四个不同的状态:
- channelUnregistered
- channelRegistered
- channelActive
- channelInactive
Channel 的状态在其生命周期中变化,因为状态变化需要触发,下图显示了Channel状态变化:
ChannelHandler 和其子类
先看一张 Handler 的类继承图
ChannelHandler中的方法
Netty定义了良好的类型层次结构来表示不同的处理程序类型,所有的类型的父类是ChanneHandler。ChannelHandler 提供了在其生命周期内添加或从ChannelPipeline中删除的方法。
- handlerAdded, ChannelHandler 添加到实际,上下文中准备处理事件
- handlerRemoved, 将ChannelHandler从实际上下文中删除,不再处理事件
- exceptionCaught, 处理抛出的异常
Netty还提供了一个实现了ChannelHandler的抽象类ChannelHandlerAdapter。ChannelHandlerAdapter 实现了父类的所有方法, 基本上就是传递事件到 ChannelPipeline 中的下一个 ChannelHandler 直到结束。也可以直接继承于 ChannelHandlerAdapter, 然后重写里面的方法。
ChannelInboundHandler
ChannelInboundHandler 提供了一些方法再接收数据或 Channel 状态改变时被调用. 下面是 ChannelInboundHandler 的一些方法:
- channelRegistered, ChannelHandlerContext 的 Channel 被注册到 EventLoop
- channelUnregistered, ChannelHandlerContext 的 Channel 从 EventLoop 中注销
- channelActive, ChannelHandlerContext 的 Channel 已激活
- channelInactive,ChannelHanderContext 的 Channel 结束生命周期
- channelRead, 从当前 Channel 的对端读取消息
- channelReadComplete, 消息读取完成后执行
- userEventTriggered, 一个用户事件被触发
- channelWritabilityChanged, 改变通道的可写状态,可以使用 Channel.isWritable() 检查
- exceptionCaught, 重写父类 ChannelHandler 的方法,处理异常
Netty提供了一个实现了ChannelInboundHandler接口并继承ChannelHandlerAdapter的类:ChannelInboundHandlerAdapter
ChannelInboundHandlerAdapter 实现了 ChannelInboundHandler 的所有方法,作用就是处理消息并将消息转发到 ChannelPipeline 中的下一个 ChannelHandler. ChannelInboundHandlerAdapter 的 channelRead 方法处理完消息后会自动释放消息,若想自动释放收到的消息,可以使用SimpleChannelInboundHandler.看下面的代码:
public class UnreleaseHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// 手动释放消息ReferenceCountUtil.release(msg);}
}
SimpleChannelInboundHandler 会自动释放消息:
public class ReleaseHandler extends SimpleChannelInboundHandler<Object> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {// 不需要手动释放}
}
ChannelInitializer 用来初始化 ChannelHandler,将自定义的各种 ChannelHandler 添加到 ChannelPipeline 中。
数据翻译编码和解码
TCP 黏包/拆包
TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
粘包问题的解决策略
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过。上层的应用协议栈设计来解决。业界的主流协议的解决方案,可以归纳如下:
- 消息定长,报文大小固定长度,例如每个报文的长度固定为200字节,如果不够空位补空格
- 包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分
- 将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段
- 更复杂的自定义应用层协议。
编、解码技术
通常我们也习惯将编码(Encode) 称为序列化(serialization) ,它将对象序列化为字节数组,用于网络传输、数据持久化或者其它用途。
反之,解码(Decode) /反序列化(deserialization) 把从网络、磁盘等读取的字节数组还原成原始对象(通常是原始对象的拷贝),以方便后续的业务逻辑操作。进行远程跨进程服务调用时(例如RPC调用),需要使用特定的编解码技术,对需要进行网络传输的对象做编码或者解码,以便完成远程调用。
Netty为什么要提供编解码框架
作为一个高性能的异步、NIO通信框架,编解码框架是Netty的重要组成部分。尽管站在微内核的角度看,编解码框架并不是Netty微内核的组成部分,但是通过ChannelHandler定制扩展出的编解码框架却是不可或缺的。
然而,我们已经知道在Netty中,从网络读取的Inbound消息,需要经过解码,将二进制的数据报转换成应用层协议消息或者业务消息,才能够被上层的应用逻辑识别和处理;同理,用户发送到网络的Outbound业务消息,需要经过编码转换成二进制字节数组(对于Netty就是ByteBuf)才能够发送到网络对端。编码和解码功能是NIO框架的有机组成部分,无论是由业务定制扩展实现,还是NIO框架内置编解码能力,该功能是必不可少的。
为了降低用户的开发难度,Netty对常用的功能和API做了装饰,以屏蔽底层的实现细节。编解码功能的定制,对于熟悉Netty底层实现的开发者而言,直接基于ChannelHandler扩展开发,难度并不是很大。但是对于大多数初学者或者不愿意去了解底层实现细节的用户,需要提供给他们更简单的类库和API,而不是ChannelHandler.
Netty在这方面做得非常出色,针对编解码功能,它既提供了通用的编解码框架供用户扩展,又提供了常用的编解码类库供用户直接使用。在保证定制扩展性的基础之上,尽量降低用户的开发工作量和开发门槛,提升开发效率。
Netty预置的编解码功能列表如下: base64、 Protobuf、 JBoss Marshalling、spdy 等。
Netty粘包和拆包解决方案
Netty中常用的解码器
Netty提供了多个解码器,可以进行分包的操作,分别是:
- LineBasedFrameDecoder
- DelimiterBasedFrameDecoder (添加特殊分隔符报文来分包)
- FixedLengthFrameDecoder (使用定长的报文来分包)
- LengthFieldBasedFrameDecoder
LineBasedFrameDecoder解码器
LineBasedFrameDecoder是回车换行解码器,如果用户发送的消息以回车换行符作为消息结束的标识,则可以直接使用Netty的LineBasedFrameDecoder对消息进行解码,只需要在初始化Netty服务端或者客户端时将LineBasedFrameDecoder正确的添加到ChannelPipeline中即可,不需要自己重新实现一套换行解码器。
LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf中的可读字节,判断看是否有“\n”或者“\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。防止由于数据报没有携带换行符导致接收到ByteBuf无限制积压,引起系统内存溢出。
它的使用效果如下:
# 解码之前:
+------------------------------------------------------------------+接收到的数据报
“This is a netty example for using the nio framework.\r\n When you“
+------------------------------------------------------------------+
# 解码之后的ChannelHandler接收到的Object如下:
+------------------------------------------------------------------+解码之后的文本消息
“This is a netty example for using the nio framework.“
+------------------------------------------------------------------+
通常情况下,LineBasedFrameDecoder会和StringDecoder配合使用, 组合成按行切换的文本解码器,对于文本类协议的解析,文本换行解码器非常实用,例如对HTTP消息头的解析、FTP协议消息的解析等。
下面我们简单给出文本换行解码器的使用示例:
@Override
public void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new LineBasedFrameDecoder(1024));ch.pipeline().addLast(new StringDecoder());ch.pipeline().addLast(new MyHandler());
}
初始化Channel的时候,首先将LineBasedFrameDecoder添加到ChannelPipeline中,然后再依次添加字符串解码器StringDecoder,业务Handler。
DelimiterBasedFrameDecoder解码器
DelimiterBasedFrameDecoder是分隔符解码器,用户可以指定消息结束的分隔符,它可以自动完成以分隔符作为码流结束标识的消息的解码。回车换行解码器实际上是一种特殊的DelimiterBasedFrameDecoder解码器。
分隔符解码器在实际工作中也有很广泛的应用,很多简单的文本私有协议,都是以特殊的分隔符作为消息结束的标识,特别是对于那些使用长连接的基于文本的私有协议。分隔符的指定:与大家的习惯不同,分隔符并非以char或者string作为构造参数,而是ByteBuf,下面我们就结合实际例子给出它的用法。假如消息以“$_”作为分隔符,服务端或者客户端初始化ChannelPipeline的代码实例如下:
@Override
public void initChannel(SocketChannel ch) throws Exception {ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));ch.pipeline().addLast(new StringDecoder());ch.pipeline().addLast(new MyHandler());
}
首先将“$_”转换成ByteBuf对象,作为参数构造DelimiterBasedFrameDecoder,将其添加到ChannelPipeline中,然后依次添加字符串解码器(通 常用于文本解码)和用户Handler,请注意解码器和Handler的添加顺序,如果顺序颠倒,会导致消息解码失败。
DelimiterBasedFrameDecoder原理分析:解码时,判断当前已经读取的ByteBuf中是否包含分隔符ByteBuf,如果包含,则截取对应的ByteBuf返回,源码如下:
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {if (lineBasedDecoder != null) {return lineBasedDecoder.decode(ctx, buffer);}// Try all delimiters and choose the delimiter which yields the shortest frame.int minFrameLength = Integer.MAX_VALUE;ByteBuf minDelim = null;for (ByteBuf delim: delimiters) {int frameLength = indexOf(buffer, delim);if (frameLength >= 0 && frameLength < minFrameLength) {minFrameLength = frameLength;minDelim = delim;}}...
}
详细分析下indexOf(buffer, delim)方法的实现,代码如下:
private static int indexOf(ByteBuf haystack, ByteBuf needle) {for (int i = haystack.readerIndex(); i < haystack.writerIndex(); i ++) {int haystackIndex = i;int needleIndex;for (needleIndex = 0; needleIndex < needle.capacity(); needleIndex ++) {if (haystack.getByte(haystackIndex) != needle.getByte(needleIndex)) {break;} else {haystackIndex ++;if (haystackIndex == haystack.writerIndex() &&needleIndex != needle.capacity() - 1) {return -1;}}}if (needleIndex == needle.capacity()) {// Found the needle from the haystack!return i - haystack.readerIndex();}}return -1;
}
该算法与Java String中的搜索算法类似,对于原字符串使用两个指针来进行搜索,如果搜索成功,则返回索引位置,否则返回-1。
FixedLengthFrameDecoder解码器
FixedLengthFrameDecoder是固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包/拆包等问题,非常实用。
对于定长消息,如果消息实际长度小于定长,则往往会进行补位操作,它在一定程度上导致了空间和资源的浪费。但是它的优点也是非常明显的,编解码比较简单,因此在实际项目中仍然有一定的应用场景。
利用FixedLengthFrameDecoder解码器,无论一次接收到多少数据报, 它都会按照构造函数中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。假如单条消息的长度是20字节,使用FixedLengthFrameDecoder解码器的效果如下:
# 解码之前:
+------------------------------------------------------------------+接收到的数据报
“HELLO NETTY FOR USER DEVELOPER“
+------------------------------------------------------------------+
# 解码后:
+------------------------------------------------------------------+解码之后的文本消息
“HELLO NETTY FOR USER“
+------------------------------------------------------------------+
LengthFieldBasedFrameDecoder解码器
了解TCP通信机制的读者应该都知道TCP底层的粘包和拆包,当我们在接收消息的时候,显示不能认为读取到的报文就是个整包消息,特别是对于采用非阻塞I/O和长连接通信的程序。
如何区分一个整包消息,通常有如下4种做法:
- 固定长度,例如每120个字节代表一个整包消息,不足的前面补位。解码器在处理这类定常消息的时候比较简单,每次读到指定长度的字节后再进行解码
- 通过回车换行符区分消息,例如HTTP协议。 这类区分消息的方式多用于文本协议
- 通过特定的分隔符区分整包消息
- 通过在协议头/消息头中设置长度字段来标识整包消息。
前三种解码器之前的章节已经做了详细介绍,下面让我们来一起学习最后一种通用解码器LengthFieldBasedFrameDecoder.
大多数的协议(私有或者公有),协议头中会携带长度字段,用于标识消息体或者整包消息的长度,例如SMPP、 HTTP协议等。由于基于长度解码需求的通用性,以及为了降低用户的协议开发难度,Netty提供 了LengthFieldBasedFrameDecoder,自动屏蔽TCP底层的拆包和粘包问题,只需要传入正确的参数,即可轻松解决“读半包“问题。
下面我们看看如何通过参数组合的不同来实现不同的“半包”读取策略。第一种常用的方式是消息的第一一个字段是长度字段,后面是消息体,消息头中只包含一个长度字段。它的消息结构定义如下所示:
# 解码前的字节缓冲区(14字节)
+--------+----------------+
| Length | Actual Conyent |
| 0x000C | "HELLO, WORLD" |
+--------+----------------+
使用以下参数组合进行解码:
- lengthFieldOffset = 0
- lengthFieldLength = 2
- lengthAdjustment = 0
- initialBytesToStrip= 0
解码后的字节缓冲区内容如下所示:
# 解码后的字节缓冲区(14字节)
+--------+----------------+
| Length | Actual Conyent |
| 0x000C | "HELLO, WORLD" |
+--------+----------------+
通过ByteBuf.readableBytes(方法我们可以获取当前消息的长度,所以解码后的字节缓冲区可以不携带长度字段,由于长度字段在起始位置并且长度为2,所以将initialBytesToStrip设置为2,参数组合修改为:
- lengthFieldOffset = 0
- lengthFieldLength = 2
- lengthAdjustment = 0
- initialBytesToStrip= 2
解码后的字节缓冲区内容如下所示:
# 跳过长度字段解码后的字节缓冲区(12字节)
+----------------+
| Actual Conyent |
| "HELLO, WORLD" |
+----------------+
解码后的字节缓冲区丢弃了长度字段,仅仅包含消息体,对于大多数的协议,解码之后消息长度没有用处,因此可以丢弃。
在大多数的应用场景中,长度字段仅用来标识消息体的长度,这类协议通常由消息长度字段+消息体组成,如. 上图所示的几个例子。但是,对于某些协议,长度字段还包含了消息头的长度。在这种应用场景中,往往需要使用lengthAdjustment进行修正。由于整个消息(包含消息头)的长度往往大于消息体的长度,所以,lengthAdjustment为负数。图2-6展示了通过指定lengthAdjustment字段来包含消息头的长度:
- lengthFieldOffset = 0
- lengthFieldLength = 2
- lengthAdjustment = -2
- initialBytesToStrip = 0
# 解码之前的码流,包含长度字段自身的码流
+--------+----------------+
| Length | Actual Conyent |
| 0x000E | "HELLO, WORLD" |
+--------+----------------+
# 解码之后的码流
+--------+----------------+
| Length | Actual Conyent |
| 0x000E | "HELLO, WORLD" |
+--------+----------------+
由于协议种类繁多,并不是所有的协议都将长度字段放在消息头的首位,当标识消息长度的字段位于消息头的中间或者尾部时,需要使用lengthFieldOffset字段进行标识,下面的参数组合给出了如何解决消息长度字段不在首位的问题:
- lengthFieldOffset = 2
- lengthFieldLength= 3
- lengthAdjustment = 0;
- initialBytesToStrip= 0
其中lengthFieldOffset表示长度字段在消息头中偏移的字节数,lengthFieldLength表示长度字段自身的长度,解码效果如下:
# 解码之前,长度字段偏移的原始码流
+----------+----------+----------------+
| Header 1 | Length | Actual Conyent |
| 0xCAFE | 0x00000E | "HELLO, WORLD" |
+----------+----------+----------------+
# 解码之后,长度字段偏移解码后的码流
+----------+----------+----------------+
| Header 1 | Length | Actual Conyent |
| 0xCAFE | 0x00000E | "HELLO, WORLD" |
+----------+----------+----------------+
由于消息头1的长度为2,所以长度字段的偏移量为2;消息长度字段Length为3,所以lengthFieldL ength值为3。由于长度字段仅仅标识消息体的长度,所以lengthAdjustment和initialBytes’ ToStrip都为0。
最后一种场景是长度字段夹在两个消息头之间或者长度字段位于消息头的中间,前后都有其它消息头字段,在这种场景下如果想忽略长度字段以及其前面的其它消息头字段,则可以通过initialBytes ToStrip参数来跳过要忽略的字节长度,它的组合配置示意如下:
- lengthFieldOffset= 1
- lengthFieldLength = 2
- lengthAdjustment= 1
- initialBytesToStrip= 3
解码之前的码流(16字节) :
# 长度字段夹在消息头中间的原始码流(16字节)
+------+--------+------+----------------+
| HDR1 | Length | HDE2 | Actual Conyent |
| 0xCA | 0x000C | 0XFE | "HELLO, WORLD" |
+------+--------+------+----------------+
# 解码之后,13字节
+------+----------------+
| HDE2 | Actual Conyent |
| 0XFE | "HELLO, WORLD" |
+------+----------------+
由于HDR1的长度为1,所以长度字段的偏移量lengthFieldOffset为1;长度字段为2个字节,所以lengthFieldL ength为2。由于长度字段是消息体的长度,解码后如果携带消息头中的字段,则需要使用lengthAdjustment进行调整,此处它的值为1,代表的是HDR2的长度,最后由于解码后的缓冲区要忽略长度字段和HDR1部分,所以lengthAdjustment为3。解码后的结果为13个字节,HDR1和Length字段被忽略。
事实上,通过4个参数的不同组合,可以达到不同的解码效果,用户在使用过程中可以根据业务的实际情况进行灵活调整。
由于TCP存在粘包和组包问题,所以通常情况下用户需要自己处理半包消息。利用LengthFieldBasedFrameDecoder解码器可以自动解决半包问题,它的习惯用法如下:
ch.pipeline().addLast("freameDecoder", new LengthFieldBasedFrameDecoder(65536, 0, 2));
ch.pipeline().addLast("UserDecoder", new UserDecoder());
在pipeline中增加engthFieldBasedFrameDecoder解码器,指定正确的参数组合,它可以将Netty的ByteBuf解码成整 包消息,后面的用户解码器拿到的就是个完整的数据报,按照逻辑正常进行解码即可,不再需要额外考虑“读半包”问题,降低了用户的开发难度。
常用的编码器
Netty并没有提供与前面介绍的匹配的编码器,原因如下:
- 4种常用的解码器本质都是解析一一个完整的数据报给后端,主要用于解决TCP底层粘包和拆包;对于编码,就是将POJO对象序列化为ByteBuf,不需要与TCP层面打交道,也就不存在半包编码问题。从应用场景和需要解决的实际问题角度看,双方是非对等的
- 很难抽象出合适的编码器,对于不同的用户和应用场景,序列化技术不尽相同,在Netty底层统一 抽象封装也并不合适。
Netty默认提供了丰富的编解码框架供用户集成使用,本文对较常用的Java序列化编码器进行讲解。其它的编码器,实现方式大同小异。
ObjectEncoder编码器
ObjectEncoder是Java序列化编码器,它负责将实现Serializable接口的对象序列化为byte [],然后写入到ByteBuf中用于消息的跨网络传输。
下面我们一起分析下它的实现:
首先,我们发现它继承自MessageToByteEncoder,它的作用就是将对象编码成ByteBuf,如果要使用Java序列化,对象必须实现Serializable接口,因此,它的泛型类型为Serializable。MessageToByteEncoder的子类只需要实现encode(ChannelHandlerContext ctx,Serializable msg, ByteBuf out)方法即可,下面我们重点关注encode方法的实现:
public class ObjectEncoder extends MessageToByteEncoder<Serializable> {private static final byte[] LENGTH_PLACEHOLDER = new byte[4];@Overrideprotected void encode(ChannelHandlerContext ctx, Serializable msg, ByteBuf out) throws Exception {int startIdx = out.writerIndex();ByteBufOutputStream bout = new ByteBufOutputStream(out);bout.write(LENGTH_PLACEHOLDER);ObjectOutputStream oout = new CompactObjectOutputStream(bout);oout.writeObject(msg);oout.flush();oout.close();int endIdx = out.writerIndex();out.setInt(startIdx, endIdx - startIdx - 4);}
}
首先创建ByteBufOutputStream和0bjectOutputStream,用于将0bjec对象序列化到ByteBuf中,值得注意的是在writeObject之前需要先将长度字段(4个字节) 预留,用于后续长度字段的更新。
依次写入长度占位符(4字节) 、序列化之后的0bject对象,之后根据ByteBuf的writelndex计算序列化之后的码流长度,最后调用ByteBuf的setInt(int index, int value)更新长度占位符为实际的码流长度。有个细节需要注意,更新码流长度字段使用了setInt方法而不是writeInt,原因就是setInt方法只更新内容,并不修改readerIndex和writerIndex。
Netty编解码框架可定制性
尽管Netty预置了丰富的编解码类库功能,但是在实际的业务开发过程中,总是需要对编解码功能做一些定制。使用Netty的编解码框架,可以非常方便的进行协议定制。本章节将对常用的支持定制的编解码类库进行讲解,以期让读者能够尽快熟悉和掌握编解码框架。
解码器
ByteToMessageDecoder抽象解码器
使用NIO进行网络编程时,往往需要将读取到的字节数组或者字节缓冲区解码为业务可以使用的POJO对象。为了方便业务将ByteBuf解码成业务POJO对象,Netty提供了ByteToMessageDecoder抽象工具解码类。
用户自定义解码器继承Byte ToMessageDecoder,只需要实现void decode (ChannelHandler Context ctx, ByteBufin, List out)抽象方法即可完成ByteBuf到POJO对象的解码。
由于ByteToMessageDecoder并没有考虑TCP粘包和拆包等场景,用户自定义解码器需要自己处理“读半包”问题。正因为如此,大多数场景不会直接继承ByteToMessageDecoder,而是继承另外一些更 高级的解码器来屏蔽半包的处理。实际项目中,通常将L engthFieldBasedFrameDecoder和ByteToMessageDecoder组合使用,前者负责将网络读取的数据报解码为整包消息,后者负责将整包消息解码为最终的业务对象。
除了和其它解码器组合形成新的解码器之外,ByteToMessageDecoder也是很多基础解码器的父类,它的继承关系如下图所示:
MessageToMessageDecoder抽象解码器
MessageToMessageDecoder实际上是Netty的二次解码器,它的职责是将一个对象二次解码为其它对象。
为什么称它为二次解码器呢?我们知道,从SocketChannel读取到的TCP数据报是ByteBuffer,实际就是字节数组。我们首先需要将ByteBuffer缓冲区中的数据报读取出来,并将其解码为Java对象; 然后对Java对象根据某些规则做二次解码,将其解码为另一个POJO对象。 因为MessageToMessageDecoder在Byte ToMessageDecoder之后,所以称之为二次解码器。
二次解码器在实际的商业项目中非常有用,以HTTP+XML协议栈为例,第一次解码往往是将字节数组解码成HttpRequest对象,然后对HttpRequest消 息中的消息体字符串进行二次解码,将XML 格式的字符串解码为POJO对象,这就用到了二次解码器。类似这样的场景还有很多,不再一一枚举。
事实上,做一个超级复杂的解码器将多个解码器组合成- -个大而全的MessageToMessageDecoder解码器似乎也能解决多次解码的问题,但是采用这种方式的代码可维护性会非常差。例如,如果我们打算在HTTP+XML协议栈中增加一-个打印码流的功能,即首次解码获取HttpRequest对象之后打印XML格式的码流。如果采用多个解码器组合,在中间插入一个打印消息体的Handler即可,不需要修改原有的代码;如果做一个大而全的解码器,就需要在解码的方法中增加打印码流的代码,可扩展性和可维护性都会变差。
用户的解码器只需要实现void decode(ChannelHandlerContext ctx, I msg, List< Object > out)抽象方法即可,由于它是将一个POJO解码为另一个POJO,所以一般不会涉及到半包的处理,相对于ByteToMessageDecoder更加简单些。它的继承关系图如下所示:
编码器
MessageToByteEncoder抽象编码器
MessageToByteEncoder负责将POJO对象编码成ByteBuf,用户的编码器继承MessageToByteEncoder,实现void encode(ChannelHandlerContext ctx, | msg, ByteBuf out)接口接口,示例代码如下:
public class IntegerEncoder extends MessageToByteEncoder<Integer> {@Overrideprotected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {out.writeInt(msg); }
}
它的实现原理如下:调用write操作时,首先判断当前编码器是否支持需要发送的消息,如果不支持则直接透传;如果支持则判断缓冲区的类型,对于直接内存分配ioBuffer (堆外内存),对于堆内存通过heapBuffer方法分配,源码如下:
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {ByteBuf buf = null;try {if (acceptOutboundMessage(msg)) {@SuppressWarnings("unchecked")I cast = (I) msg;buf = allocateBuffer(ctx, cast, preferDirect);try {encode(ctx, cast, buf);} finally {ReferenceCountUtil.release(cast);}if (buf.isReadable()) {ctx.write(buf, promise);} else {buf.release();ctx.write(Unpooled.EMPTY_BUFFER, promise);}buf = null;} else {ctx.write(msg, promise);}} catch (EncoderException e) {throw e;} catch (Throwable e) {throw new EncoderException(e);} finally {if (buf != null) {buf.release();}}
}
编码使用的缓冲区分配完成之后,调用encode抽象方 法进行编码,方法定义如下:它由子类负责具体实现。
protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
编码完成之后,调用ReferenceCountUtil的release方 法释放编码对象msg。对编码后的ByteBuf进行以下判断:
- 如果缓冲区包含可发送的字节,则调用ChannelHandlerContext的write方法发送ByteBuf
- 如果缓冲区没有包含可写的字节,则需要释放编码后的ByteBuf,写入一个空的ByteBuf到ChannelHandlerContext中。
发送操作完成之后,在方法退出之前释放编码缓冲区ByteBuf对象。
MessageToMessageEncoder抽象编码器
将一个POJO对象编码成另一个对象,以HTTP+XML协议为例, 它的一种实现方式是:先将POJO对象编码成XML字符串,再将字符串编码为HTTP请求或者应答消息。对于复杂协议,往往需要经历多次编码,为了便于功能扩展,可以通过多个编码器组合来实现相关功能。
用户的解码器继承MessageToMessageEncoder解码器,实现void encode(Channel HandlerContext ctx,I msg, List out)方法即可。注意,它与MessageToByteEncoder 的区别是输出是对象列表而不是ByteFBuf:
public class IntegerToStringEncoder extends MessageToMessageEncoder<Integer> {@Overrideprotected void encode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {out.add(msg.toString());}
}
MessageToMessageEncoder编码器的实现原理与之前分析的MessageToByteEncoder相似,唯一的差 别是它编码后的输出是个中间对象,并非最终可传输的ByteBuf。
简单看下它的源码实现:创建RecyclableArrayList对象, 判断当前需要编码的对象是否是编码器可处理的类型,如果不是,则忽略,执行下一个ChannelHandler的write方法。
具体的编码方法实现由用户子类编码器负责完成,如果编码后的RecyclableArrayList为空,说明编码没有成功,释放RecyclableArrayList引用。
如果编码成功,则通过遍历RecyclableArrayList,循环发送编码后的POJO对象,代码如下所示:
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {CodecOutputList out = null;if (acceptOutboundMessage(msg)) {out = CodecOutputList.newInstance();@SuppressWarnings("unchecked")I cast = (I) msg;try {encode(ctx, cast, out);} finally {ReferenceCountUtil.release(cast);}if (out.isEmpty()) {out.recycle();out = null;throw new EncoderException(StringUtil.simpleClassName(this) + " must produce at least one message.");}} else {ctx.write(msg, promise);}
}
LengthFieldPrepender编码器
如果协议中的第一个字段为长度字段,Netty提供了LengthFieldPrepender编码器,它可以计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中,如图所示:
# 编码前(12 bytes) 编码后(14 bytes)
+---------------+ +--------+---------------+
| "HELLO,WORLD" | -----> + 0x000E | "HELLO,WORLD" |
+---------------+ +--------+---------------+
通过LengthFieldPrepender可以将待发送消息的长度写入到ByteBuf的前2个字节,编码后的消息组成为长度字段+原消息的方式。通过设置L engthFieldPrepender为true,消息长度将包含长度本身占用的字节数,打开LengthFieldPrepender后, 编码结果如下图所示:
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {int length = msg.readableBytes() + lengthAdjustment;if (lengthIncludesLengthFieldLength) {length += lengthFieldLength;}if (length < 0) {throw new IllegalArgumentException("Adjusted frame length (" + length + ") is less than zero");}switch (lengthFieldLength) {case 1:if (length >= 256) {throw new IllegalArgumentException("length does not fit into a byte: " + length);}out.add(ctx.alloc().buffer(1).order(byteOrder).writeByte((byte) length));break;case 2:if (length >= 65536) {throw new IllegalArgumentException("length does not fit into a short integer: " + length);}out.add(ctx.alloc().buffer(2).order(byteOrder).writeShort((short) length));break;case 3:if (length >= 16777216) {throw new IllegalArgumentException("length does not fit into a medium integer: " + length);}out.add(ctx.alloc().buffer(3).order(byteOrder).writeMedium(length));break;case 4:out.add(ctx.alloc().buffer(4).order(byteOrder).writeInt(length));break;case 8:out.add(ctx.alloc().buffer(8).order(byteOrder).writeLong(length));break;default:throw new Error("should not reach here");}out.add(msg.retain());
}
LengthFieldPrepender工作原理分析如下:首先对长度字段进行设置,如果需要包含消息长度自身,则在原来长度的基础之.上再加上lengthFieldLength的长度。如果调整后的消息长度小于0,则抛出参数非法异常。对消息长度自身所占的字节数进行判断,以便采用正确的方法将长度字段写入到ByteBuf中,共有以下6种可能:
- 长度字段所占字节为1:如果使用1个Byte字节代表消息长度,则最大长度需要小于256个字节。对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并 通过writeByte将长度值写入到ByteBuf中
- 长度字段所占字节为2:如果使用2个Byte字节代表消息长度,则最大长度需要小于65536个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并 通过writeShort将长度值写入到ByteBuf中
- 长度字段所占字节为3:如果使用3个Byte字节代表消息长度,则最大长度需要小于16777216个字节,对长度进行校验,如果校验失败,则抛出参数非法异.常;若校验通过,则创建新的ByteBuf并通过writeMedium将长度值写入到ByteBuf中
- 长度字段所占字节为4:创建新的ByteBuf,并通过writelnt将长度值写 入到ByteBuf中
- 长度字段所占字节为8:创建新的ByteBuf,并通过writel ong将长度值写入到ByteBuf中
- 其它长度值:直接抛出Error
最后将原需要发送的ByteBuf复制到List< Object > out中,完成编码。
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
相关文章
- 2020年了,授人以鱼不如授人以渔,Spring Boot 引入 Druid的超简单用法
目录新建一个Spring Boot项目修改pom文件修改配置文件 application.properties运行,看运行日志打开druid内置后台监控页面 新建一个Spring Boot项目 操作路径如下,Spring Initializr这里,默认的https://start.spring.io需要科学上网,建议改成自定义的,阿里提供的一个,秒出…...
2024/4/27 15:31:14 - 机器人导航两篇顶级会议论文解析
机器人导航两篇顶级会议论文解析 一.一种用于四旋翼无人机室内自主导航的卷积神经网络特征检测算法 标题:A Convolutional Neural Network Feature Detection Approach to Autonomous Quadrotor Indoor Navigation 作者:Adriano Garcia, Sandeep S. Mittal, Edward Kiewra a…...
2024/4/27 13:30:22 - 宋仁宗
腾讯卫视清平乐的热播,让很多人了解到了赵祯皇帝,他宋朝第六位皇帝,他是传说中“狸猫换太子”的主角,他是少年包青天以包拯为知己的卖画书生, 他是让“十全武功”的乾隆皇帝唯一敬佩的帝王,他是"诸事不会,只会做皇帝“的职场人,他的谥号是宋仁宗。乾隆为何离不开和…...
2024/4/27 13:50:34 - hive参数配置列表
注:该列表很多都用不到,如想查看作用,直接搜索即可。hive.exec.mode.local.auto=true 决定 Hive 是否应该自动地根据输入文件大小,在本地运行(在GateWay运行) cal.auto.inputbytes.max=134217728L 如果 hive.exec.mode.local.auto 为 true,当输入文件大小小于此阈值时可…...
2024/4/24 11:32:54 - Java中史上最全的JDBC编程详细教程,一篇就够啦~
创作不易,如果觉得这篇文章对你有帮助,欢迎各位老铁点个赞呗,您的支持是我创作的最大动力!文章目录1 前言2 JDBC概述2.1 JDBC定义2.2 JDBC接口调用方和实现方2.3 接口的作用2.4 连接数据库驱动2.5 JDBC原理3 使用JDBC编程之前,先介绍几个概念3.1 API3.2 URL3.3 SQL的分类4…...
2024/4/24 11:32:59 - 盘点人工智能十大经典应用领域、图解技术原理
来源:大数据DT本文约7695字,建议阅读15分钟。本文通过案例分门别类地深入探讨人工智能的实际应用。案例甚多,此处所列举的仅是九牛一毛。本该按行业或业务对这些案例进行分类,但相反我选择按在行业或业务中最可能应用的顺序来分类。本文将使用“算法”一词,以高度简化的方…...
2024/4/26 0:04:19 - Drools 规则引擎——向领域驱动进步
1.复杂事件处理到目前为止,我们已经看到如何使用规则,以基于数据(我们称呼它为fact)来做出决定。这个信息几乎是任何一组Java对象,它们描述了我们正在做决策的域的状态,但是它总是在一个特定的时间点上代表这个世界的状态。本章我们将会去看一些列的概念,配置和规则语法…...
2024/4/27 15:36:21 - 五子棋初始版
五子棋初始版 1、设计主框架,界面,drawchessboard方法画出棋盘。 2、利用ActionListener接口继承实现按钮事件的监听,利用MouseListener接口实现事件监听,并实现接口里的所有方法。 3、重新开始功能的实现 4、悔棋功能的实现 5、棋盘中棋子点类的定义(x,y,color)。 6,点…...
2024/4/25 3:32:59 - wordpress仿站笔记
最近接了一个用wordpress仿站的项目,本以为很简单,但实际上手后才发现了一些“小”问题,而且中文网上关于wordpress的教程不仅少,而且老,很多都已经不适用于5.x后的版本,现在就把我在开发过程中遇到问题整理成该文循环块 这大概是wordpress最实用的功能了,在把静态网页改…...
2024/4/24 10:47:34 - 学习笔记(01):零基础掌握 Python 入门到实战-一个圆点的何去何从(一)
立即学习:https://edu.csdn.net/course/play/26676/338762?utm_source=blogtoedu范式:面向对象、面向过程...
2024/4/27 15:01:02 - 学习笔记(01):10小时闪电上手Java编程-课程介绍
立即学习:https://edu.csdn.net/course/play/29050/405963?utm_source=blogtoedu讲java宏观,不太注重细节。...
2024/4/19 14:13:40 - 学习笔记(02):零基础掌握 Python 入门到实战-一个圆点的何去何从(二)
立即学习:https://edu.csdn.net/course/play/26676/338772?utm_source=blogtoedu内置函数:type() : 对象的类型id(): 对象的地址divmod(a,b): 除法help(): 函数文档...
2024/4/17 13:09:48 - 学习笔记(02):MySQL数据库从入门到实战应用-DML:插入、修改、删除数据
立即学习:https://edu.csdn.net/course/play/27328/362520?utm_source=blogtoeducreate table contacts(id int not null auto_increment primary key,name varchar(50),sex tinyint default 1,phone varchar(20) )desc contacts;insert into contacts(name,sex,phone) values…...
2024/4/18 5:33:14 - 学习笔记(01):程序员的数学:概率统计-巩固概率分布性质的掌握(下)
立即学习:https://edu.csdn.net/course/play/26113/323361?utm_source=blogtoedu...
2024/4/24 11:32:50 - 区块链技术原理、发展历史根由、应用场景
记账技术历史悠久,现代复式记账系统(Double Entry Bookkeeping)是由意大利数学家卢卡帕西奥利,1494年在《Summa de arithmetica, geometrica, proportioni et proportionalit》 一书中最早制定。复式记账法的基石是资产负债表等式,又称为会计恒等式。即任何一项经济业务的…...
2024/4/23 23:09:55 - 何为面对对象,面向对象的特性
我们都知道Java是一门面向对象的语言。什么是面向对象,它有什么特性呢,今天我们就来说一下这个"面向对象"到底是什么意思。面向对象简称 OO(Object Oriented),20 世纪 80 年代以后,其实就有了面向对象分析(OOA)、 面向对象设计(OOD)、面向对象程序设计(OO…...
2024/4/24 11:32:48 - 分享200个App移动端模板---总有一个适合你
链接:https://pan.baidu.com/s/1NjBHqIoq7ORuDfJoR6gOkA 密码:xn6x分享200个App移动端文件,总有一款适合你! 下面是名字,我放了一些图片,所有图片全都放进去是不行的图太多,大家下载后可以看到。收集整理不容易老铁支持我动力APP应用介绍网站模板 APP应用广场CSS网页模板…...
2024/4/24 11:32:50 - python编程从入门到实践
1、计算机核心基础 1.1 什么是语言?什么是编程语言?为何要有编程语言? 语言其实就是人与人之间沟通的介质,如英语,汉语,俄语等。 编程语言则是人与计算机之间沟通的介质, 编程的目的就是为了让计算机按照人类的思维逻辑(程序)自发地去工作从而把人力解放出来二 计算机组成…...
2024/4/24 11:32:49 - 【Netty】Netty 核心组件 ( ChannelPipeline 中的 ChannelHandlerContext 双向链表分析 )
文章目录一、 代码示例分析二、 ChannelHandlerContext 双向链表类型三、 Pipeline / ChannelPipeline 管道内双向链表分析四、 数据入站与出站接上一篇博客 【Netty】Netty 核心组件 ( Pipeline | ChannelPipeline ) 内容 , 在 debug 调试中 , 详细分析 ChannelPipeline 内部的…...
2024/4/26 1:15:43 - 【linux】frp内网穿透
项目地址https://github.com/fatedier/frp 服务器 上传frp_0.33.0_linux_amd64.tar.gz 快捷下载链接:https://github-production-release-asset-2e65be.s3.amazonaws.com/48378947/03a3af00-88ad-11ea-91e9-21b33a8d8ff6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credent…...
2024/4/24 11:32:47
最新文章
- 矩阵按列相乘运算的并行化实现方法
这两天一直在琢磨如下矩阵计算问题。 已知dm矩阵X和hq矩阵Y,求如下矩阵: 其中X(:,i), Y(:,j)分别表示矩阵X, Y的第i列和第j列,易知Z为dh矩阵。 如果直接串行计算矩阵Z,两个循环共有mq,则会很慢,能不能并行化…...
2024/4/27 16:54:48 - 梯度消失和梯度爆炸的一些处理方法
在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言,在此感激不尽。 权重和梯度的更新公式如下: w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...
2024/3/20 10:50:27 - 【LeetCode热题100】【二叉树】二叉树的中序遍历
题目链接:94. 二叉树的中序遍历 - 力扣(LeetCode) 中序遍历就是先遍历左子树再遍历根最后遍历右子树 class Solution { public:void traverse(TreeNode *root) {if (!root)return;traverse(root->left);ans.push_back(root->val);tra…...
2024/4/23 6:15:26 - Java-运算符
运算符 Java语言支持如下运算符: 算术运算符:,-,*,/,%,,--复制运算符:关系运算符:>, <, >, <, , !instanceof逻辑运算符:&&…...
2024/4/27 0:55:38 - 【外汇早评】美通胀数据走低,美元调整
原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...
2024/4/26 18:09:39 - 【原油贵金属周评】原油多头拥挤,价格调整
原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...
2024/4/26 20:12:18 - 【外汇周评】靓丽非农不及疲软通胀影响
原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...
2024/4/26 23:05:52 - 【原油贵金属早评】库存继续增加,油价收跌
原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...
2024/4/27 4:00:35 - 【外汇早评】日本央行会议纪要不改日元强势
原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...
2024/4/25 18:39:22 - 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响
原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...
2024/4/27 14:22:49 - 【外汇早评】美欲与伊朗重谈协议
原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...
2024/4/26 21:56:58 - 【原油贵金属早评】波动率飙升,市场情绪动荡
原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...
2024/4/27 9:01:45 - 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试
原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...
2024/4/26 16:00:35 - 【原油贵金属早评】市场情绪继续恶化,黄金上破
原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...
2024/4/25 18:39:16 - 【外汇早评】美伊僵持,风险情绪继续升温
原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...
2024/4/25 18:39:16 - 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势
原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...
2024/4/26 19:03:37 - 氧生福地 玩美北湖(上)——为时光守候两千年
原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...
2024/4/26 22:01:59 - 氧生福地 玩美北湖(中)——永春梯田里的美与鲜
原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...
2024/4/25 18:39:14 - 氧生福地 玩美北湖(下)——奔跑吧骚年!
原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...
2024/4/26 23:04:58 - 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!
原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...
2024/4/25 2:10:52 - 「发现」铁皮石斛仙草之神奇功效用于医用面膜
原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...
2024/4/25 18:39:00 - 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者
原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...
2024/4/26 19:46:12 - 广州械字号面膜生产厂家OEM/ODM4项须知!
原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...
2024/4/27 11:43:08 - 械字号医用眼膜缓解用眼过度到底有无作用?
原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...
2024/4/27 8:32:30 - 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...
解析如下:1、长按电脑电源键直至关机,然后再按一次电源健重启电脑,按F8健进入安全模式2、安全模式下进入Windows系统桌面后,按住“winR”打开运行窗口,输入“services.msc”打开服务设置3、在服务界面,选中…...
2022/11/19 21:17:18 - 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。
%读入6幅图像(每一幅图像的大小是564*564) f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...
2022/11/19 21:17:16 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...
win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面,在等待界面中我们需要等待操作结束才能关机,虽然这比较麻烦,但是对系统进行配置和升级…...
2022/11/19 21:17:15 - 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...
有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows,请勿关闭计算机”的提示,要过很久才能进入系统,有的用户甚至几个小时也无法进入,下面就教大家这个问题的解决方法。第一种方法:我们首先在左下角的“开始…...
2022/11/19 21:17:14 - win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...
置信有很多用户都跟小编一样遇到过这样的问题,电脑时发现开机屏幕显现“正在配置Windows Update,请勿关机”(如下图所示),而且还需求等大约5分钟才干进入系统。这是怎样回事呢?一切都是正常操作的,为什么开时机呈现“正…...
2022/11/19 21:17:13 - 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...
Win7系统开机启动时总是出现“配置Windows请勿关机”的提示,没过几秒后电脑自动重启,每次开机都这样无法进入系统,此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一:开机按下F8,在出现的Windows高级启动选…...
2022/11/19 21:17:12 - 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...
有不少windows10系统用户反映说碰到这样一个情况,就是电脑提示正在准备windows请勿关闭计算机,碰到这样的问题该怎么解决呢,现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法:1、2、依次…...
2022/11/19 21:17:11 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...
今天和大家分享一下win7系统重装了Win7旗舰版系统后,每次关机的时候桌面上都会显示一个“配置Windows Update的界面,提示请勿关闭计算机”,每次停留好几分钟才能正常关机,导致什么情况引起的呢?出现配置Windows Update…...
2022/11/19 21:17:10 - 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...
只能是等着,别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚,只能是考虑备份数据后重装系统了。解决来方案一:管理员运行cmd:net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...
2022/11/19 21:17:09 - 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?
原标题:电脑提示“配置Windows Update请勿关闭计算机”怎么办?win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢?一般的方…...
2022/11/19 21:17:08 - 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...
关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!关机提示 windows7 正在配…...
2022/11/19 21:17:05 - 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...
钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...
2022/11/19 21:17:05 - 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...
前几天班里有位学生电脑(windows 7系统)出问题了,具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面,长时间没反应,无法进入系统。这个问题原来帮其他同学也解决过,网上搜了不少资料&#x…...
2022/11/19 21:17:04 - 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...
本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法,并在最后教给你1种保护系统安全的好方法,一起来看看!电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中,添加了1个新功能在“磁…...
2022/11/19 21:17:03 - 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...
许多用户在长期不使用电脑的时候,开启电脑发现电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机。。.这要怎么办呢?下面小编就带着大家一起看看吧!如果能够正常进入系统,建议您暂时移…...
2022/11/19 21:17:02 - 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...
配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!配置windows update失败 还原更改 请勿关闭计算机&#x…...
2022/11/19 21:17:01 - 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...
不知道大家有没有遇到过这样的一个问题,就是我们的win7系统在关机的时候,总是喜欢显示“准备配置windows,请勿关机”这样的一个页面,没有什么大碍,但是如果一直等着的话就要两个小时甚至更久都关不了机,非常…...
2022/11/19 21:17:00 - 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...
当电脑出现正在准备配置windows请勿关闭计算机时,一般是您正对windows进行升级,但是这个要是长时间没有反应,我们不能再傻等下去了。可能是电脑出了别的问题了,来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...
2022/11/19 21:16:59 - 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...
我们使用电脑的过程中有时会遇到这种情况,当我们打开电脑之后,发现一直停留在一个界面:“配置Windows Update失败,还原更改请勿关闭计算机”,等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢࿰…...
2022/11/19 21:16:58 - 如何在iPhone上关闭“请勿打扰”
Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...
2022/11/19 21:16:57