原文链接:https://wangwei.one/posts/net...

前面 ,我们分析了Netty Pipeline的初始化及节点添加与删除逻辑。接下来,我们将来分析Pipeline的事件传播机制。

Netty版本:4.1.30

inBound事件传播

示例

我们通过下面这个例子来演示Netty Pipeline的事件传播机制。

public class NettyPipelineInboundExample {public static void main(String[] args) {EventLoopGroup group = new NioEventLoopGroup(1);ServerBootstrap strap = new ServerBootstrap();strap.group(group).channel(NioServerSocketChannel.class).localAddress(new InetSocketAddress(8888)).childOption(ChannelOption.TCP_NODELAY, true).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new InboundHandlerA());ch.pipeline().addLast(new InboundHandlerB());ch.pipeline().addLast(new InboundHandlerC());}});try {ChannelFuture future = strap.bind().sync();future.channel().closeFuture().sync();} catch (InterruptedException e) {e.printStackTrace();} finally {group.shutdownGracefully();}}
}class InboundHandlerA extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println("InboundHandler A : " + msg);// 传播read事件到下一个channelhandlerctx.fireChannelRead(msg);}}class InboundHandlerB extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println("InboundHandler B : " + msg);// 传播read事件到下一个channelhandlerctx.fireChannelRead(msg);}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// channel激活,触发channelRead事件,从pipeline的heandContext节点开始往下传播ctx.channel().pipeline().fireChannelRead("Hello world");}
}class InboundHandlerC extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println("InboundHandler C : " + msg);// 传播read事件到下一个channelhandlerctx.fireChannelRead(msg);}
}

源码

通过 telnet 来连接上面启动好的netty服务,触发channel active事件:

$ telnet 127.0.0.1 8888

按照InboundHandlerA、InboundHandlerB、InboundHandlerC的添加顺序,控制台输出如下信息:

InboundHandler A : Hello world
InboundHandler B : Hello world
InboundHandler C : Hello world

若是调用它们的添加顺序,则会输出对应顺序的信息,e.g:

...ch.pipeline().addLast(new InboundHandlerB());
ch.pipeline().addLast(new InboundHandlerA());
ch.pipeline().addLast(new InboundHandlerC());...

输出如下信息:

InboundHandler B : Hello world
InboundHandler A : Hello world
InboundHandler C : Hello world

源码分析

强烈建议 下面的流程,自己通过IDE的Debug模式来分析

待netty启动成功,通过telnet连接到netty,然后通过telnet终端输入任意字符(这一步才开启Debug模式),进入Debug模式。

触发channel read事件,从下面的入口开始调用

public class DefaultChannelPipeline implements ChannelPipeline {...// 出发channel read事件@Overridepublic final ChannelPipeline fireChannelRead(Object msg) {// 从head节点开始往下传播read事件AbstractChannelHandlerContext.invokeChannelRead(head, msg);return this;}...}

调用 AbstractChannelHandlerContext 中的 invokeChannelRead(head, msg) 接口:

abstract class AbstractChannelHandlerContext extends DefaultAttributeMapimplements ChannelHandlerContext, ResourceLeakHint {...// 调用channel readstatic void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {// 获取消息final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);// 获取 EventExecutorEventExecutor executor = next.executor();// trueif (executor.inEventLoop()) {// 调用下面的invokeChannelRead接口:invokeChannelRead(Object msg)next.invokeChannelRead(m);} else {executor.execute(new Runnable() {@Overridepublic void run() {next.invokeChannelRead(m);}});}}private void invokeChannelRead(Object msg) {if (invokeHandler()) {try {// handler():获取当前遍历到的channelHandler,第一个为HeandContext,最后为TailContext// 调用channel handler的channelRead接口((ChannelInboundHandler) handler()).channelRead(this, msg);} catch (Throwable t) {notifyHandlerException(t);}} else {fireChannelRead(msg);}}...@Overridepublic ChannelHandlerContext fireChannelRead(final Object msg) {// 调回到上面的 invokeChannelRead(final AbstractChannelHandlerContext next, Object msg)invokeChannelRead(findContextInbound(), msg);return this;}...// 遍历出下一个ChannelHandlerprivate AbstractChannelHandlerContext findContextInbound() {AbstractChannelHandlerContext ctx = this;do {//获取下一个inbound类型的节点ctx = ctx.next;// 必须为inbound类型} while (!ctx.inbound);return ctx;}...
}

Pipeline中的第一个节点为HeadContext,它对于channelRead事件的处理,是直接往下传播,代码如下:

final class HeadContext extends AbstractChannelHandlerContext...@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// HeadContext往下传播channelRead事件,// 调用HeandlerContext中的接口:fireChannelRead(final Object msg)ctx.fireChannelRead(msg);}...
}

就这样一直循环下去,依次会调用到 InboundHandlerA、InboundHandlerB、InboundHandlerC 中的 channelRead(ChannelHandlerContext ctx, Object msg) 接口。

到最后一个TailContext节点,它对channelRead事件的处理如下:

public class DefaultChannelPipeline implements ChannelPipeline {final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {...@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// 调用onUnhandledInboundMessage接口onUnhandledInboundMessage(msg);}      ...}...// 对未处理inbound消息做最后的处理protected void onUnhandledInboundMessage(Object msg) {try {logger.debug("Discarded inbound message {} that reached at the tail of the pipeline. Please check your pipeline configuration.", msg);} finally {// 对msg对象的引用数减1,当msg对象的引用数为0时,释放该对象的内存ReferenceCountUtil.release(msg);}}...}

以上就是pipeline对inBound消息的处理流程。

SimpleChannelInboundHandler

在前面的例子中,假如中间有一个ChannelHandler未对channelRead事件进行传播,就会导致消息对象无法得到释放,最终导致内存泄露。

我们还可以继承 SimpleChannelInboundHandler 来自定义ChannelHandler,它的channelRead方法,对消息对象做了msg处理,防止内存泄露。

public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {...@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {boolean release = true;try {if (acceptInboundMessage(msg)) {@SuppressWarnings("unchecked")I imsg = (I) msg;channelRead0(ctx, imsg);} else {release = false;ctx.fireChannelRead(msg);}} finally {if (autoRelease && release) {// 对msg对象的引用数减1,当msg对象的引用数为0时,释放该对象的内存ReferenceCountUtil.release(msg);}}}...}    

outBound事件传播

接下来,我们来分析Pipeline的outBound事件传播机制。代码示例如下:

示例

public class NettyPipelineOutboundExample {public static void main(String[] args) {EventLoopGroup group = new NioEventLoopGroup(1);ServerBootstrap strap = new ServerBootstrap();strap.group(group).channel(NioServerSocketChannel.class).localAddress(new InetSocketAddress(8888)).childOption(ChannelOption.TCP_NODELAY, true).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new OutboundHandlerA());ch.pipeline().addLast(new OutboundHandlerB());ch.pipeline().addLast(new OutboundHandlerC());}});try {ChannelFuture future = strap.bind().sync();future.channel().closeFuture().sync();} catch (InterruptedException e) {e.printStackTrace();} finally {group.shutdownGracefully();}}
}class OutboundHandlerA extends ChannelOutboundHandlerAdapter {@Overridepublic void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {// 输出消息System.out.println("OutboundHandlerA: " + msg);// 传播write事件到下一个节点ctx.write(msg, promise);}
}class OutboundHandlerB extends ChannelOutboundHandlerAdapter {@Overridepublic void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {// 输出消息System.out.println("OutboundHandlerB: " + msg);// 传播write事件到下一个节点ctx.write(msg, promise);}@Overridepublic void handlerAdded(ChannelHandlerContext ctx) throws Exception {// 待handlerAdded事件触发3s后,模拟触发一个ctx.executor().schedule(() -> {
//            ctx.write("Hello world ! ");ctx.channel().write("Hello world ! ");}, 3, TimeUnit.SECONDS);}
}class OutboundHandlerC extends ChannelOutboundHandlerAdapter {@Overridepublic void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {// 输出消息System.out.println("OutboundHandlerC: " + msg);// 传播write事件到下一个节点ctx.write(msg, promise);}
}

源码

通过 telnet 来连接上面启动好的netty服务,触发channel added事件:

$ telnet 127.0.0.1 8888

按照OutboundHandlerA、OutboundHandlerB、OutboundHandlerC的添加顺序,控制台输出如下信息:

OutboundHandlerC: Hello world !
OutboundHandlerB: Hello world !
OutboundHandlerA: Hello world ! 

输出的顺序正好与ChannelHandler的添加顺序相反。

若是调用它们的添加顺序,则会输出对应顺序的信息,e.g:

...ch.pipeline().addLast(new InboundHandlerB());
ch.pipeline().addLast(new InboundHandlerA());
ch.pipeline().addLast(new InboundHandlerC());...

输出如下信息:

OutboundHandlerC: Hello world !
OutboundHandlerA: Hello world !
OutboundHandlerB: Hello world ! 

源码分析

强烈建议 下面的流程,自己通过IDE的Debug模式来分析

从channel的write方法开始,往下传播write事件:

public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {...@Overridepublic ChannelFuture write(Object msg) {// 调用pipeline往下传播wirte事件return pipeline.write(msg);}...}

接着来看看Pipeline中的write接口:

public class DefaultChannelPipeline implements ChannelPipeline {...@Overridepublic final ChannelFuture write(Object msg) {// 从tail节点开始传播return tail.write(msg);}...    }

调用ChannelHandlerContext中的write接口:

abstract class AbstractChannelHandlerContext extends DefaultAttributeMapimplements ChannelHandlerContext, ResourceLeakHint {...@Overridepublic ChannelFuture write(Object msg) {// 往下调用write接口return write(msg, newPromise());}@Overridepublic ChannelFuture write(final Object msg, final ChannelPromise promise) {if (msg == null) {throw new NullPointerException("msg");}try {if (isNotValidPromise(promise, true)) {ReferenceCountUtil.release(msg);// cancelledreturn promise;}} catch (RuntimeException e) {ReferenceCountUtil.release(msg);throw e;}// 往下调用write接口write(msg, false, promise);return promise;}    ...private void write(Object msg, boolean flush, ChannelPromise promise) {// 寻找下一个outbound类型的channelHandlerContextAbstractChannelHandlerContext next = findContextOutbound();final Object m = pipeline.touch(msg, next);EventExecutor executor = next.executor();if (executor.inEventLoop()) {if (flush) {next.invokeWriteAndFlush(m, promise);} else {// 调用接口 invokeWrite(Object msg, ChannelPromise promise)next.invokeWrite(m, promise);}} else {AbstractWriteTask task;if (flush) {task = WriteAndFlushTask.newInstance(next, m, promise);}  else {task = WriteTask.newInstance(next, m, promise);}safeExecute(executor, task, promise, m);}}// 寻找下一个outbound类型的channelHandlerContextprivate AbstractChannelHandlerContext findContextOutbound() {AbstractChannelHandlerContext ctx = this;do {ctx = ctx.prev;} while (!ctx.outbound);return ctx;}private void invokeWrite(Object msg, ChannelPromise promise) {if (invokeHandler()) {// 继续往下调用invokeWrite0(msg, promise);} else {write(msg, promise);}}private void invokeWrite0(Object msg, ChannelPromise promise) {try {// 获取当前的channelHandler,调用其write接口// handler()依次会返回 OutboundHandlerC OutboundHandlerB OutboundHandlerA((ChannelOutboundHandler) handler()).write(this, msg, promise);} catch (Throwable t) {notifyOutboundHandlerException(t, promise);}}...    }        

最终会调用到HeadContext的write接口:

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {// 调用unsafe进行写数据操作unsafe.write(msg, promise);
}

异常传播

了解了Pipeline的入站与出站事件的机制之后,我们再来看看Pipeline的异常处理机制。

示例

public class NettyPipelineExceptionCaughtExample {public static void main(String[] args) {EventLoopGroup group = new NioEventLoopGroup(1);ServerBootstrap strap = new ServerBootstrap();strap.group(group).channel(NioServerSocketChannel.class).localAddress(new InetSocketAddress(8888)).childOption(ChannelOption.TCP_NODELAY, true).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new InboundHandlerA());ch.pipeline().addLast(new InboundHandlerB());ch.pipeline().addLast(new InboundHandlerC());ch.pipeline().addLast(new OutboundHandlerA());ch.pipeline().addLast(new OutboundHandlerB());ch.pipeline().addLast(new OutboundHandlerC());}});try {ChannelFuture future = strap.bind().sync();future.channel().closeFuture().sync();} catch (InterruptedException e) {e.printStackTrace();} finally {group.shutdownGracefully();}}static class InboundHandlerA extends ChannelInboundHandlerAdapter {@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {System.out.println("InboundHandlerA.exceptionCaught:" + cause.getMessage());ctx.fireExceptionCaught(cause);}}static class InboundHandlerB extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {throw new Exception("ERROR !!!");}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {System.out.println("InboundHandlerB.exceptionCaught:" + cause.getMessage());ctx.fireExceptionCaught(cause);}}static class InboundHandlerC extends ChannelInboundHandlerAdapter {@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {System.out.println("InboundHandlerC.exceptionCaught:" + cause.getMessage());ctx.fireExceptionCaught(cause);}}static class OutboundHandlerA extends ChannelOutboundHandlerAdapter {@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {System.out.println("OutboundHandlerA.exceptionCaught:" + cause.getMessage());ctx.fireExceptionCaught(cause);}}static class OutboundHandlerB extends ChannelOutboundHandlerAdapter {@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {System.out.println("OutboundHandlerB.exceptionCaught:" + cause.getMessage());ctx.fireExceptionCaught(cause);}}static class OutboundHandlerC extends ChannelOutboundHandlerAdapter {@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {System.out.println("OutboundHandlerC.exceptionCaught:" + cause.getMessage());ctx.fireExceptionCaught(cause);}}}

源码

通过 telnet 来连接上面启动好的netty服务,并在控制台发送任意字符:

$ telnet 127.0.0.1 8888

触发channel read事件并抛出异常,控制台输出如下信息:

InboundHandlerB.exceptionCaught:ERROR !!!
InboundHandlerC.exceptionCaught:ERROR !!!
OutboundHandlerA.exceptionCaught:ERROR !!!
OutboundHandlerB.exceptionCaught:ERROR !!!
OutboundHandlerC.exceptionCaught:ERROR !!!

可以看到异常的捕获与我们添加的ChannelHandler顺序相同。

源码分析

在我们的示例中,InboundHandlerB的ChannelRead接口抛出异常,导致从InboundHandlerA将ChannelRead事件传播到InboundHandlerB的过程中出现异常,异常被捕获。

abstract class AbstractChannelHandlerContext extends DefaultAttributeMapimplements ChannelHandlerContext, ResourceLeakHint {...@Overridepublic ChannelHandlerContext fireExceptionCaught(final Throwable cause) {//调用invokeExceptionCaught接口invokeExceptionCaught(next, cause);return this;}static void invokeExceptionCaught(final AbstractChannelHandlerContext next, final Throwable cause) {ObjectUtil.checkNotNull(cause, "cause");EventExecutor executor = next.executor();if (executor.inEventLoop()) {// 调用下一个节点的invokeExceptionCaught接口next.invokeExceptionCaught(cause);} else {try {executor.execute(new Runnable() {@Overridepublic void run() {next.invokeExceptionCaught(cause);}});} catch (Throwable t) {if (logger.isWarnEnabled()) {logger.warn("Failed to submit an exceptionCaught() event.", t);logger.warn("The exceptionCaught() event that was failed to submit was:", cause);}}}}...private void invokeChannelRead(Object msg) {if (invokeHandler()) {try {// 抛出异常((ChannelInboundHandler) handler()).channelRead(this, msg);} catch (Throwable t) {// 异常捕获,往下传播notifyHandlerException(t);}} else {fireChannelRead(msg);}}// 通知Handler发生异常事件private void notifyHandlerException(Throwable cause) {if (inExceptionCaught(cause)) {if (logger.isWarnEnabled()) {logger.warn("An exception was thrown by a user handler " +"while handling an exceptionCaught event", cause);}return;}// 往下调用invokeExceptionCaught接口invokeExceptionCaught(cause);}private void invokeExceptionCaught(final Throwable cause) {if (invokeHandler()) {try {// 调用当前ChannelHandler的exceptionCaught接口// 在我们的案例中,依次会调用InboundHandlerB、InboundHandlerC、// OutboundHandlerA、OutboundHandlerB、OutboundHandlChandler().exceptionCaught(this, cause);} catch (Throwable error) {if (logger.isDebugEnabled()) {logger.debug("An exception {}" +"was thrown by a user handler's exceptionCaught() " +"method while handling the following exception:",ThrowableUtil.stackTraceToString(error), cause);} else if (logger.isWarnEnabled()) {logger.warn("An exception '{}' [enable DEBUG level for full stacktrace] " +"was thrown by a user handler's exceptionCaught() " +"method while handling the following exception:", error, cause);}}} else {fireExceptionCaught(cause);}}...}

最终会调用到TailContext节点的exceptionCaught接口,如果我们中途没有对异常进行拦截处理,做会打印出一段警告信息!

public class DefaultChannelPipeline implements ChannelPipeline {...final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {...@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {onUnhandledInboundException(cause);}...protected void onUnhandledInboundException(Throwable cause) {try {logger.warn("An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +"It usually means the last handler in the pipeline did not handle the exception.",cause);} finally {ReferenceCountUtil.release(cause);}}}    ...}

在实际的应用中,一般会定一个ChannelHandler,放置Pipeline末尾,专门用来处理中途出现的各种异常。

最佳异常处理实践

单独定义ExceptionCaughtHandler来处理异常:


...class ExceptionCaughtHandler extends ChannelInboundHandlerAdapter {@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {if (cause instanceof Exception) {// TODOSystem.out.println("Successfully caught exception ! ");} else {// TODO}}
}...ch.pipeline().addLast(new ExceptionCaughtHandler());...

输出:

InboundHandlerB.exceptionCaught:ERROR !!!
InboundHandlerC.exceptionCaught:ERROR !!!
OutboundHandlerA.exceptionCaught:ERROR !!!
OutboundHandlerB.exceptionCaught:ERROR !!!
OutboundHandlerC.exceptionCaught:ERROR !!!
Successfully caught exception !  // 成功捕获日志

Pipeline回顾与总结

至此,我们对Pipeline的原理的解析就完成了。

  • Pipeline是在什么时候创建的?
  • Pipeline添加与删除节点的逻辑是怎么样的?
  • netty是如何判断ChannelHandler类型的?
  • 如何处理ChannelHandler中抛出的异常?
  • 对于ChannelHandler的添加应遵循什么样的顺序?

参考资料

  • Java读源码之Netty深入剖析

Netty Pipeline源码分析(2)相关推荐

  1. Netty学习笔记(一)Netty客户端源码分析

    最近在学些BIO,NIO相关的知识,也学习了下Netty和它的源码,做个记录,方便以后继续学习,如果有错误的地方欢迎指正 如果不了解BIO,NIO这些基础知识,可以看下我的如下博客 IO中的阻塞.非阻 ...

  2. Netty技术细节源码分析-FastThreadLocal源码分析

    本文是该篇的修正版 本文的github地址:点此 Netty 的 FastThreadLocal 源码解析 该文中涉及到的 Netty 源码版本为 4.1.6. Netty 的 FastThreadL ...

  3. Netty Channel源码分析

    原文:https://wangwei.one/posts/netty-channel-source-analyse.html 前面,我们大致了解了Netty中的几个核心组件.今天我们就来先来介绍Net ...

  4. Netty技术细节源码分析-ByteBuf的内存泄漏原因与检测

    本文的github地址:点此 该文所涉及的netty源码版本为4.1.6. Netty中的ByteBuf为什么会发生内存泄漏 在Netty中,ByetBuf并不是只采用可达性分析来对ByteBuf底层 ...

  5. Netty技术细节源码分析-Recycler对象池原理分析

    本文是该篇的修正版 本文的github地址:点此 该文所涉及的netty源码版本为4.1.6. Netty的对象池Recycler是什么 Recycler是Netty中基于ThreadLocal的轻量 ...

  6. Netty技术细节源码分析-内存池之PoolChunk设计与实现

    该文所涉及的netty源码版本为4.1.16. 在一开始需要明确的几个概念 在Netty的内存池的PoolChunk中,先要明确以下几个概念. page: page是chunk中所能申请到的最小内存单 ...

  7. Netty技术细节源码分析-HashedWheelTimer时间轮原理分析

    本文是该篇的修正版 本文的github地址:点此 该文所涉及的netty源码版本为4.1.6. Netty时间轮HashedWheelTimer是什么 Netty的时间轮HashedWheelTime ...

  8. Netty技术细节源码分析-MpscLinkedQueue队列原理分析

    本文的github地址:点此 该文所涉及的netty源码版本为4.1.6. MpscLinkedQueue是什么 在Netty的核心中的核心成员NioEventLoop中,其中任务队列的实现taskQ ...

  9. apache dubbo 源码分析系列汇总

    Dubbo(读音[ˈdʌbəʊ])是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成.后面捐献给了知名的开源社区 ...

最新文章

  1. 2021年大数据Spark(八):环境搭建集群模式 Standalone HA
  2. [转]WinForm下Splash(启动画面)制作
  3. 架构设计|异步请求如何同步处理?
  4. 对现有代码的分析方法随想
  5. w7系统装天联高级版服务器,w7系统有几个版本你都知道吗?
  6. 搭量化数据库——互联网金融之三
  7. SQL基础【十九、触发器】(不建议使用触发器的原因)
  8. js微信小程序页面左上角返回跳转指定页面
  9. java bip-39_Java中对XML的解析详解
  10. Ubuntu18.04 安装nextcloud
  11. cmd-bat批处理命令延时方法
  12. 31、栈的压入、弹出序列(Python)
  13. 如何利用网管软件管控网络设备
  14. 基于深度学习的图像匹配技术一览
  15. idea java配色方案_IDEA 主题配色方案+字体
  16. Bug解决-RuntimeError: Sizes of tensors must match except in dimension 2. Got 320 and 160 (The offendin
  17. html输入QQ自动获取QQ头像,代码实现WordPress评论框输入QQ号码自动获取QQ头像和昵称...
  18. asp.net中调用javascript函数实现多功能日期控件示例
  19. 还不错,字母成熟了些!
  20. 常见离散型随机变量比较

热门文章

  1. Oracle 查询结果去重保留一项
  2. tcp和udp多线程的epoll服务器+客户端源代码 - brucema的个人空间 - 开源中国社区
  3. 使用 SCons 轻松建造程序
  4. webkit qt版快速编译 支持wml版本
  5. jsonp跨域ajax跨域get方法
  6. 使用boostrap组件结合PageHelper完成javaweb网页的分页功能
  7. App.config的典型应用
  8. Apache Struts2(S2-045)漏洞反思总结
  9. 轻松精通数据库管理之道——运维巡检系列
  10. iptables 实现地址转换与安全控制