关于netty:Netty源码解析-零拷贝机制与PoolArena

本文分享Netty中零拷贝机制与PoolArena的实现原理。 源码剖析基于Netty 4.1 Netty中的零拷贝首先看一下Netty中实现零拷贝的机制 1.文件传输类DefaultFileRegion#transferTo,调用FileChannel#transferTo,间接将文件缓冲区的数据发送到指标Channel,缩小用户缓冲区的拷贝(通过linux的sendfile函数)。 应用read 和 write DMA拷贝 拷贝,切换到用户态 拷贝,切换到内核态硬盘 --------> 内核缓冲区(内核态) ----------------> 用户缓冲区(用户态) ----------------> socket缓冲区 应用sendfile DMA拷贝 拷贝硬盘 --------> 内核缓冲区(内核态) ----> socket缓冲区 缩小用户态,内核态切换,以及数据拷贝 可参考: 操作系统和Web服务器那点事儿 2.Unpooled#wrappedBuffer办法,将byte数据,(jvm)ByteBuffer转换为ByteBufCompositeByteBuf#addComponents办法,合并ByteBufByteBuf#slice办法,提取ByteBuf中局部数据片段这些办法都是基于对象援用的操作,并没有内存拷贝 3.应用堆外内存(jvm)ByteBuffer对Socket读写。如果应用JVM的堆内存进行Socket读写,JVM会将堆内存拷贝一份到间接内存中,而后才写入Socket中。应用堆外内存能够防止该拷贝操作。留神,这里从内核缓冲区拷贝到用户缓冲区的操作并不能省略,毕竟咱们须要对数据进行操作,所以还是要拷贝到用户态的。可参考: 知乎--Java NIO中,对于DirectBuffer,HeapBuffer的疑难 接口关系ByteBufAllocator,内存分配器,负责为ByteBuf分配内存, 线程平安。PooledByteBufAllocator,默认的ByteBufAllocator,事后从操作系统中申请一大块内存,在该内存上分配内存给ByteBuf,能够进步性能和减小内存碎片。UnPooledByteBufAllocator,非池化内存分配器,每次都从操作系统中申请内存。 RecvByteBufAllocator,接管内存分配器,为Channel读入的IO数据调配一块大小正当的buffer空间。具体性能交由外部接口Handle定义。它次要是针对Channel读入场景增加一些操作,如guess,incMessagesRead,lastBytesRead等等。 ByteBuf,代表一个内存块,提供程序拜访和随机拜访,是一个或多个Byte数组或NIO Buffers的形象视图。ByteBuf次要能够分为堆外内存DirectByteBuf和堆内存HeapByteBuf。Netty4中ByteBuf调整为抽象类,从而晋升吞吐量。 上面只关注PooledByteBufAllocator,它是Netty中默认的内存调配策略(unsafe反对),也是了解Netty内存机制的难点。 内存调配后面文章《ChannelPipeline与Read,Write,Connect事件处理》中解析的read事件处理,NioByteUnsafe#read public final void read() { ... final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle(); allocHandle.reset(config); ByteBuf byteBuf = null; boolean close = false; ... byteBuf = allocHandle.allocate(allocator); allocHandle.lastBytesRead(doReadBytes(byteBuf)); ...}recvBufAllocHandle办法返回AdaptiveRecvByteBufAllocator.HandleImpl。(AdaptiveRecvByteBufAllocator,PooledByteBufAllocator都在DefaultChannelConfig中初始化) ...

September 13, 2020 · 4 min · jiezi

关于netty:netty-in-action学习笔记第四章

netty提供了对立的API进行传输数据,这个相比于JDK的形式不便很多。比方上面是一个不必netty而应用原生的阻塞IO进行传输的例子。 public class PlainOioServer { public void serve(int port) throws IOException { final ServerSocket socket = new ServerSocket(port); try { for(;;) { final Socket clientSocket = socket.accept(); System.out.println( "Accepted connection from " + clientSocket); new Thread(new Runnable() { @Override public void run() { OutputStream out; try { out = clientSocket.getOutputStream(); out.write("Hi!rn".getBytes( Charset.forName("UTF-8"))); out.flush(); clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } finally { try { clientSocket.close(); } catch (IOException ex) { // ignore on close } } } }).start(); } } catch (IOException e) { e.printStackTrace(); } }} 代码很好了解,为每一个新来的连贯创立一个线程解决。这种形式有个比拟大的问题是,客户端连接数受限于服务器所能接受的线程数。为了改良这个问题咱们能够应用异步模式来重写这段代码,然而你会发现,简直所有的代码都要重写。原生的OIO和NIO的API简直齐全不能复用。不信你看看上面这段NIO的代码, ...

September 9, 2020 · 3 min · jiezi

关于netty:netty-in-action学习笔记第二章-编写你的第一个netty程序

【netty in action】学习笔记-第二章 编写你的第一个netty程序 这一章简略粗犷,整个章节都是讲一个例子,例子很简略,然而麻雀虽小五脏俱全。通过这个示例你会对编写基于netty的应用程序有个直观的意识。 我先上代码,前面再剖析。 先看看服务端的示例, public class EchoServer { public int port; public EchoServer(int port) { this.port = port; } public void start() { EventLoopGroup group = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(group) .channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new EchoServerHandler()); } }); ChannelFuture f = b.bind().sync(); System.out.println(EchoServer.class.getName() + "started and listen on " + f.channel().localAddress()); f.channel().closeFuture().sync(); }catch (Exception e) { }finally { group.shutdownGracefully(); } } public static void main(String[] args) { new EchoServer(8888).start(); }}public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(Unpooled.copiedBuffer("netty rocks", CharsetUtil.UTF_8)); } @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf msg) throws Exception { ByteBuf recieveMsg=(ByteBuf) msg; String result = ByteBufUtil.hexDump(recieveMsg).toUpperCase();//将bytebuf中的可读字节 转换成16进制数字符串 String result2 = ByteBufUtil.hexDump(msg.readBytes(msg.readableBytes())); //看下两种形式输入的后果有什么区别 System.out.println("client received:" + result); System.out.println("client received:" + result2); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); }}而后是客户端的示例, ...

August 30, 2020 · 2 min · jiezi

关于netty:netty-in-action学习笔记第一章-了解java-NIO2

【netty in action】学习笔记-第一章 理解java NIO(2) 上一篇文章理解了java nio的一些个性和根本用法。本篇持续来看看java nio有哪些问题以及netty如何解决这些问题。 跨平台和兼容性问题java nio有nio和nio2两个版本,后者只反对jdk7。而且java nio自身属于比拟low level的api,有时候会遇到在linux运行良好然而在windows上却有问题。 netty提供对立的api,你不须要关注java的版本,也不须要关注操作系统。 ByteBuffer的扩大通过后面的示例,你能看进去java nio的ByteBuffer并不好用,比方还有本人切换读写模式。netty扩大了ByteBuffer提供更加易用的API。具体的用法在前面章节的笔记中会具体阐明。 内存透露的问题nio有个Scattering and Gathering的概念,就是扩散读取,集中写入。 scatter(扩散)是指数据从一个channel读取到多个buffer中。比方上面的例子: ByteBuffer header = ByteBuffer.allocate(128);ByteBuffer body = ByteBuffer.allocate(1024);ByteBuffer[] bufferArray = { header, body }channel.read(bufferArray);read()办法依照buffer在数组中的程序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 集中读的概念就是反过来,多个buffer的数据写入到同一个channel。示例如下: ByteBuffer header = ByteBuffer.allocate(128);ByteBuffer body = ByteBuffer.allocate(1024);ByteBuffer[] bufferArray = { header, body };channel.write(bufferArray);java nio提供了专门的接口来解决Scattering and Gathering, public interface ScatteringByteChannel extends ReadableByteChannel{ public long read(ByteBuffer[] dsts) throws IOException; public long read(ByteBuffer[] dsts, int offset, int length) throws IOException;}public interface GatheringByteChannel extends WritableByteChannel{ public long write(ByteBuffer[] srcs) throws IOException; public long write(ByteBuffer[] srcs, int offset, int length) throws IOException;}比方SocketChannel就实现了这两个接口。 ...

August 29, 2020 · 1 min · jiezi

关于netty:netty-in-action学习笔记第一章-了解java-NIO1

【netty in action】学习笔记-第一章 理解java NIO(1) 学习netty,java nio是根底,因为前者是对后者的封装,当然又不只是封装。随着学习的深刻你会了解这句话的含意。 下图是netty的架构图,让你对netty波及的模型,传输,协定有个根本印象。 netty的个性能够总结为一下几点: 统计的API操作阻塞和非阻塞的socket接口易用线程模型简略而弱小链式调用逻辑,复用性搞高文档和示例丰盛除了JDK之外不依赖别的组件相比于java api有更好的吞吐量以及低提早缩小不必要的内存占用,不会再因为疾速的,慢速的或者超负荷的连贯导致OOM反对SSL/TLS社区沉闷两种实现异步API罕用的设计 基于callbacks FetchCalback.java public interface FetchCalback { void onData(Data data); void onError(Throwable cause);}Fetcher.java public interface Fetcher { void fetchData(FetchCalback fetchCalback);}MyFetcher.java public class MyFetcher implements Fetcher{ @Override public void fetchData(FetchCalback fetchCalback) { try { //模仿获取数据 Thread.sleep(1000); Data data = new Data(1, 2); fetchCalback.onData(data); } catch (Exception e) { fetchCalback.onError(e); } }}Worker.java public class Worker { public void doWorker() { Fetcher fetcher = new MyFetcher(); fetcher.fetchData(new FetchCalback() { @Override public void onData(Data data) { System.out.println("获取到数据:" + data); } @Override public void onError(Throwable cause) { System.err.println(cause.getMessage()); } }); } public static void main(String[] args) { Worker worker = new Worker(); worker.doWorker(); }}Data.java ...

August 29, 2020 · 2 min · jiezi

关于netty:Netty实战-02手把手教你实现自己的第一个-Netty-应用新手也能搞懂

大家好,我是 「后端技术进阶」 作者,一个酷爱技术的少年。 @[toc] 感觉不错的话,欢送 star!( ´・・` )比心 Netty 从入门到实战系列文章地址:https://github.com/Snailclimb/netty-practical-tutorial 。RPC 框架源码地址:https://github.com/Snailclimb/guide-rpc-framework上面,我会带着大家搭建本人的第一个 Netty 版的 Hello World 小程序。 首先,让咱们来创立服务端。 服务端咱们能够通过 ServerBootstrap 来疏导咱们启动一个简略的 Netty 服务端,为此,你必须要为其指定上面三类属性: 线程组(_个别须要两个线程组,一个负责接解决客户端的连贯,一个负责具体的 IO 解决_)IO 模型(_BIO/NIO_)自定义 ChannelHandler (_解决客户端发过来的数据并返回数据给客户端_)创立服务端/** * @author shuang.kou * @createTime 2020年05月14日 20:28:00 */public final class HelloServer { private final int port; public HelloServer(int port) { this.port = port; } private void start() throws InterruptedException { // 1.bossGroup 用于接管连贯,workerGroup 用于具体的解决 EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //2.创立服务端启动疏导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给疏导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup) // (非必备)打印日志 .handler(new LoggingHandler(LogLevel.INFO)) // 4.指定 IO 模型 .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { ChannelPipeline p = ch.pipeline(); //5.能够自定义客户端音讯的业务解决逻辑 p.addLast(new HelloServerHandler()); } }); // 6.绑定端口,调用 sync 办法阻塞晓得绑定实现 ChannelFuture f = b.bind(port).sync(); // 7.阻塞期待直到服务器Channel敞开(closeFuture()办法获取Channel 的CloseFuture对象,而后调用sync()办法) f.channel().closeFuture().sync(); } finally { //8.优雅敞开相干线程组资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws InterruptedException { new HelloServer(8080).start(); }}简略解析一下服务端的创立过程具体是怎么的: ...

August 27, 2020 · 3 min · jiezi

关于netty:Netty-01从-BIONIO-聊到-Netty最后还要实现个-RPC-框架

大家好,我是 「后端技术进阶」 作者,一个酷爱技术的少年。 @[toc] 感觉不错的话,欢送 star!( ´・・` )比心 Netty 从入门到实战系列文章地址:https://github.com/Snailclimb/netty-practical-tutorial 。RPC 框架源码地址:https://github.com/Snailclimb/guide-rpc-framework老套路,学习某一门技术或者框架的时候,第一步当然是要理解上面这几样货色。 是什么?有哪些特点?有哪些利用场景?有哪些胜利应用的案例?.....为了让你更好地理解 Netty 以及它诞生的起因,先从传统的网络编程说起吧! 还是要从 BIO 说起传统的阻塞式通信流程晚期的 Java 网络相干的 API(java.net包) 应用 Socket(套接字)进行网络通信,不过只反对阻塞函数应用。 要通过互联网进行通信,至多须要一对套接字: 运行于服务器端的 Server Socket。运行于客户机端的 Client SocketSocket 网络通信过程如下图所示: https://www.javatpoint.com/so... Socket 网络通信过程简略来说分为上面 4 步: 建设服务端并且监听客户端申请客户端申请,服务端和客户端建设连贯两端之间能够传递数据敞开资源对应到服务端和客户端的话,是上面这样的。 服务器端: 创立 ServerSocket 对象并且绑定地址(ip)和端口号(port): server.bind(new InetSocketAddress(host, port))通过 accept()办法监听客户端申请连贯建设后,通过输出流读取客户端发送的申请信息通过输入流向客户端发送响应信息敞开相干资源客户端: 创立Socket 对象并且连贯指定的服务器的地址(ip)和端口号(port):socket.connect(inetSocketAddress)连贯建设后,通过输入流向服务器端发送申请信息通过输出流获取服务器响应的信息敞开相干资源一个简略的 demo为了便于了解,我写了一个简略的代码帮忙各位小伙伴了解。 服务端: public class HelloServer { private static final Logger logger = LoggerFactory.getLogger(HelloServer.class); public void start(int port) { //1.创立 ServerSocket 对象并且绑定一个端口 try (ServerSocket server = new ServerSocket(port);) { Socket socket; //2.通过 accept()办法监听客户端申请, 这个办法会始终阻塞到有一个连贯建设 while ((socket = server.accept()) != null) { logger.info("client connected"); try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) { //3.通过输出流读取客户端发送的申请信息 Message message = (Message) objectInputStream.readObject(); logger.info("server receive message:" + message.getContent()); message.setContent("new content"); //4.通过输入流向客户端发送响应信息 objectOutputStream.writeObject(message); objectOutputStream.flush(); } catch (IOException | ClassNotFoundException e) { logger.error("occur exception:", e); } } } catch (IOException e) { logger.error("occur IOException:", e); } } public static void main(String[] args) { HelloServer helloServer = new HelloServer(); helloServer.start(6666); }}ServerSocket 的 accept() 办法是阻塞办法,也就是说 ServerSocket 在调用 accept()期待客户端的连贯申请时会阻塞,直到收到客户端发送的连贯申请才会持续往下执行代码,因而咱们须要要为每个 Socket 连贯开启一个线程(能够通过线程池来做)。 ...

August 27, 2020 · 3 min · jiezi

关于netty:Netty权威指南-第2版-带书签目录-完整版

Netty权威指南 第2版 带书签目录 完整版 下载地址: https://pan.baidu.com/s/12h96bKAdKEGXHdqOskK3nw 扫码上面二维码关注公众号回复100019 获取分享码 本书目录构造如下: 第1章 Java 的I/O 演进之路 1.1 I/O 根底入门 1.2 Java 的I/O 演进 1.3 总结 第2章 NIO 入门 2.1 传统的BIO 编程 2.2 伪异步I/O 编程 2.3 NIO 编程 2.4 AIO 编程 2.5 4 种I/O 的比照 2.6 抉择Netty 的理由 2.7 总结 入门篇 Netty NIO 开发指南 第3章 Netty 入门利用 3.1 Netty 开发环境的搭建 3.2 Netty 服务端开发 3.3 Netty 客户端开发 3.4 运行和调试 3.5 总结 ...

August 11, 2020 · 2 min · jiezi

关于netty:基于netty的基础小案例

server端代码import com.chinadaas.bio.chinadaasbio.netty.handler.ServerHandler;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelOption;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;public class Server { public static void main(String[] args) throws InterruptedException { NioEventLoopGroup boosGroup = new NioEventLoopGroup(); NioEventLoopGroup workerGroup = new NioEventLoopGroup(); try { // 定义服务端启动类 ServerBootstrap start = new ServerBootstrap() .group(boosGroup, workerGroup) //设置两个线程组 .channel(NioServerSocketChannel.class) // 应用nioServer .option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列失去的链接个数 .childOption(ChannelOption.SO_KEEPALIVE, true) // 设置放弃流动连贯状态 .childHandler(new ChannelInitializer<SocketChannel>() { // 创立一个通道测试对象(匿名对象) @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new ServerHandler()); } }); System.out.println("服务器 is ready"); // 绑定一个端口 并且同步生成一个ChannelFuture // 启动服务器(并绑定端口) ChannelFuture channelFuture = start.bind(8886).sync(); // 对敞开通道进行监听 channelFuture.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { boosGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }}client端代码import com.chinadaas.bio.chinadaasbio.netty.handler.ClientHandler;import io.netty.bootstrap.Bootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;public class Client { public static void main(String[] args) { NioEventLoopGroup workerGroup = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap() .group(workerGroup) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new ClientHandler()); } }); System.out.println("客户端 is ready"); ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8886).sync(); // 敞开通道进行监听 channelFuture.channel().closeFuture().sync(); }catch (Exception e){ e.printStackTrace(); }finally { workerGroup.shutdownGracefully(); } }}serverHandler 代码import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import io.netty.util.CharsetUtil;public class ServerHandler extends ChannelInboundHandlerAdapter { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("有异样"); ctx.close(); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf byteBuf = (ByteBuf) msg; System.out.println("服务器收到的音讯:" + byteBuf.toString(CharsetUtil.UTF_8)); System.out.println("客戶端的地址:" + ctx.channel().remoteAddress()); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(Unpooled.copiedBuffer("hello 客户端", CharsetUtil.UTF_8)); }}clientHandler 代码public class ClientHandler extends ChannelInboundHandlerAdapter { /** * 通道就绪后所触发的办法 */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(Unpooled.copiedBuffer("hello 服务器", CharsetUtil.UTF_8)); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf byteBuf = (ByteBuf) msg; System.out.println("客户端收到音讯:" + byteBuf.toString(CharsetUtil.UTF_8)); System.out.println("服务器的地址:" + ctx.channel().remoteAddress()); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); }}

August 7, 2020 · 2 min · jiezi

关于netty:Netty之旅你想要的NIO知识点这里都有

高清思维导图原件(xmind/pdf/jpg)能够关注公众号:一枝花算不算浪漫 回复nio即可。 前言道歉好久没更原创文章了,看了下上篇更新工夫,曾经拖更一个多月了。 这段时间也始终在学习Netty相干常识,因为波及知识点比拟多,也走了不少弯路。目前网上对于Netty学习材料玲琅满目,不知如何下手,其实大家都是一样的,学习办法和技巧都是总结进去的,咱们在没有找到很好的办法之前不如循序渐进先从根底开始,个别从总分总的渐进形式,既观森林,又见草木。 之前凑巧跟杭州一个敌人小飞也提到过,两者在这方面的初衷是统一的,也心愿更多的敌人可能退出一起学习和探讨。(PS:本篇文章是和小飞一起学习整顿所得~) Netty是一款提供异步的、事件驱动的网络应用程序框架和工具,是基于NIO客户端、服务器端的编程框架。所以这里咱们先以NIO和依赖相干的根底铺垫来进行分析解说,从而作为Netty学习之旅的一个开始。 一、网络编程根底回顾1. SocketSocket自身有“插座”的意思,不是Java中特有的概念,而是一个语言无关的规范,任何能够实现网络编程的编程语言都有Socket。在Linux环境下,用于示意过程间网络通信的非凡文件类型,其本质为内核借助缓冲区造成的伪文件。既然是文件,那么天经地义的,咱们能够应用文件描述符援用套接字。 与管道相似的,Linux零碎将其封装成文件的目标是为了对立接口,使得读写套接字和读写文件的操作统一。区别是管道次要利用于本地过程间通信,而套接字多利用于网络过程间数据的传递。 能够这么了解:Socket就是网络上的两个应用程序通过一个双向通信连贯实现数据交换的编程接口API。 Socket通信的根本流程具体步骤如下所示: (1)服务端通过Listen开启监听,期待客户端接入。 (2)客户端的套接字通过Connect连贯服务器端的套接字,服务端通过Accept接管客户端连贯。在connect-accept过程中,操作系统将会进行三次握手。 (3)客户端和服务端通过write和read发送和接收数据,操作系统将会实现TCP数据的确认、重发等步骤。 (4)通过close敞开连贯,操作系统会进行四次挥手。 针对Java编程语言,java.net包是网络编程的根底类库。其中ServerSocket和Socket是网络编程的根底类型。 SeverSocket是服务端利用类型。Socket是建设连贯的类型。当连贯建设胜利后,服务器和客户端都会有一个Socket对象示例,能够通过这个Socket对象示例,实现会话的所有操作。对于一个残缺的网络连接来说,Socket是平等的,没有服务器客户端分级状况。 2. IO模型介绍对于一次IO操作,数据会先拷贝到内核空间中,而后再从内核空间拷贝到用户空间中,所以一次read操作,会经验两个阶段: (1)期待数据筹备 (2)数据从内核空间拷贝到用户空间 基于以上两个阶段就产生了五种不同的IO模式。 阻塞IO:从过程发动IO操作,始终期待上述两个阶段实现,此时两阶段一起阻塞。非阻塞IO:过程始终询问IO筹备好了没有,筹备好了再发动读取操作,这时才把数据从内核空间拷贝到用户空间。第一阶段不阻塞但要轮询,第二阶段阻塞。多路复用IO:多个连贯应用同一个select去询问IO筹备好了没有,如果有筹备好了的,就返回有数据筹备好了,而后对应的连贯再发动读取操作,把数据从内核空间拷贝到用户空间。两阶段离开阻塞。信号驱动IO:过程发动读取操作会立刻返回,当数据筹备好了会以告诉的模式通知过程,过程再发动读取操作,把数据从内核空间拷贝到用户空间。第一阶段不阻塞,第二阶段阻塞。异步IO:过程发动读取操作会立刻返回,等到数据筹备好且曾经拷贝到用户空间了再告诉过程拿数据。两个阶段都不阻塞。这五种IO模式不难发现存在这两对关系:同步和异步、阻塞和非阻塞。那么略微解释一下: 同步和异步同步: 同步就是发动一个调用后,被调用者未解决完申请之前,调用不返回。异步: 异步就是发动一个调用后,立即失去被调用者的回应示意已接管到申请,然而被调用者并没有返回后果,此时咱们能够解决其余的申请,被调用者通常依附事件,回调等机制来告诉调用者其返回后果。同步和异步的区别最大在于异步的话调用者不须要期待处理结果,被调用者会通过回调等机制来告诉调用者其返回后果。 阻塞和非阻塞阻塞: 阻塞就是发动一个申请,调用者始终期待申请后果返回,也就是以后线程会被挂起,无奈从事其余工作,只有当条件就绪能力持续。非阻塞: 非阻塞就是发动一个申请,调用者不必始终等着后果返回,能够先去干其余事件。阻塞和非阻塞是针对过程在拜访数据的时候,依据IO操作的就绪状态来采取的不同形式,说白了是一种读取或者写入操作方法的实现形式,阻塞形式下读取或者写入函数将始终期待,而非阻塞形式下,读取或者写入办法会立刻返回一个状态值。 如果组合后的同步阻塞(blocking-IO)简称BIO、同步非阻塞(non-blocking-IO)简称NIO和异步非阻塞(asynchronous-non-blocking-IO)简称AIO又代表什么意思呢? BIO (同步阻塞I/O模式): 数据的读取写入必须阻塞在一个线程内期待其实现。这里应用那个经典的烧开水例子,这里假如一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去解决下一个水壶。然而实际上线程在期待水壶烧开的时间段什么都没有做。NIO(同步非阻塞): 同时反对阻塞与非阻塞模式,但这里咱们以其同步非阻塞I/O模式来阐明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程一直的轮询每个水壶的状态,看看是否有水壶的状态产生了扭转,从而进行下一步的操作。AIO(异步非阻塞I/O模型): 异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态扭转,在相应的状态扭转后,零碎会告诉对应的线程来解决。对应到烧开水中就是,为每个水壶下面装了一个开关,水烧开之后,水壶会主动告诉我水烧开了。java 中的 BIO、NIO和AIO了解为是 Java 语言在操作系统层面对这三种 IO 模型的封装。程序员在应用这些 封装API 的时候,不须要关怀操作系统层面的常识,也不须要依据不同操作系统编写不同的代码,只须要应用Java的API就能够了。由此,为了使读者对这三种模型有个比拟具体和递推式的理解,并且和本文主题NIO有个清晰的比照,上面持续延长。 Java BIOBIO编程形式通常是是Java的上古产品,自JDK 1.0-JDK1.4就有的货色。编程实现过程为:首先在服务端启动一个ServerSocket来监听网络申请,客户端启动Socket发动网络申请,默认状况下SeverSocket会建设一个线程来解决此申请,如果服务端没有线程可用,客户端则会阻塞期待或受到回绝。服务器实现模式为一个连贯一个线程,即客户端有连贯申请时服务器端就须要启动一个线程进行解决。大抵构造如下: 如果要让 BIO 通信模型可能同时解决多个客户端申请,就必须应用多线程(次要起因是 socket.accept()、socket.read()、 socket.write() 波及的三个次要函数都是同步阻塞的),也就是说它在接管到客户端连贯申请之后为每个客户端创立一个新的线程进行链路解决,解决实现之后,通过输入流返回应答给客户端,线程销毁。这就是典型的 一申请一应答通信模型 。咱们能够构想一下如果这个连贯不做任何事件的话就会造成不必要的线程开销,不过能够通过线程池机制改善,线程池还能够让线程的创立和回收老本绝对较低。应用线程池机制改善后的 BIO 模型图如下: BIO形式实用于连贯数目比拟小且固定的架构,这种形式对服务器资源要求比拟高,并发局限于利用中,是JDK1.4以前的惟一抉择,但程序直观简略易懂。Java BIO编程示例网上很多,这里就不进行coding举例了,毕竟前面NIO才是重点。 Java NIONIO(New IO或者No-Blocking IO),从JDK1.4 开始引入的非阻塞IO,是一种非阻塞+ 同步的通信模式。这里的No Blocking IO用于辨别下面的BIO。 ...

August 7, 2020 · 5 min · jiezi

关于netty:Netty应用入门及重要组件

1.Netty简介Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以疾速开发高性能、高可靠性的网络服务器和客户端程序。 2.为什么应用Netty尽管 JAVA NIO 框架提供了多路复用 IO 的反对,然而并没有提供下层“信息格式”的良好封装。例如前两者并没有提供针对 Protocol Buffer、JSON 这些信息格式的封装,然而 Netty 框架提供了这些数据格式封装(基于责任链模式的编码和解码性能); 2、NIO 的类库和 API 相当简单,应用它来开发,须要十分熟练地把握 Selector、ByteBuffer、ServerSocketChannel、SocketChannel 等,须要很多额定的编程技能来辅助应用 NIO,例如,因为 NIO 波及了 Reactor 线程模型,所以必须必须对多线程和网络编程十分相熟能力写出高质量的 NIO 程序 3、要编写一个牢靠的、易保护的、高性能的 NIO 服务器利用。除了框架自身要兼容实现各类操作系统的实现外。更重要的是它应该还要解决很多下层特有服务,例如:客户端的权限、还有下面提到的信息格式封装、简略的数据读取,断连重连,半包读写,心跳等等,这些 Netty 框架都提供了响应的反对。 4、JAVA NIO 框架存在一个 poll/epoll bug:Selector doesn’t block on Selector.select(timeout),不能 block 意味着 CPU 的使用率会变成 100%(这是底层 JNI 的问题,下层要解决这个异样实际上也好办)。当然这个 bug 只有在 Linux内核上能力重现 3.Netty重要组件3.1 Channel接口在Java 的网络编程中,其根本的结构是类 Socket。Netty 的 Channel 接口所提供的 API,被用于所有的 I/O 操作。大大地升高了间接应用 Socket类的复杂性。此外,Channel也是领有许多预约义的、专门化实现的宽泛类层次结构的根。 channel生命周期ChannelUnregistered :Channel 曾经被创立,但还未注册EventLoopChannelRegistered :Channel 曾经被注册到了 EventLoopChannelActive :Channel 处于活动状态(曾经连贯到它的近程节点)。它当初能够接管和发送数据了ChannelInactive :Channel 没有连贯到近程节点当这些状态产生扭转时,将会生成对应的事件。这些事件将会被转发给 ChannelPipeline中的 ChannelHandler,其能够随后对它们做出响应。 ...

August 1, 2020 · 3 min · jiezi

关于netty:Netty-草稿

Reactor开发模式SocketChannel 在client端监听op_connect,op_write,op_read事件,在server只监听op_write,op_read事件,ServerSocketChannel在server端运行,只监听op_accept事件 netty对Reactor模式的实现首先ServerSocketChannel是能够创立子的socketchannel,创立是由BootstrapChannelFactory这个工厂类依据传入的class反射来创立,所以serverBootstrap.group().channel(xxx.class) .channel这个时候就是由下面的工厂类利用反射动静的创立一个channel,绑定在对应的group上 serverBootstrap.group(bossGroup,workerGroup) .channel传进来的会创立一个channel绑定在bossGroup上,而后传进来的ServerSocketChannel能够创立子socketchannel绑定在wokergroup上 再和下面的reactor开发模式对应,就是serversocketchannel绑定在bossGroup上来负责监听op_accept事件,socketchannel绑定在workergroup上来负责监听其余read write事件 粘包和半包 TCP为什么会呈现粘包和半包(UDP其实没有这个问题):次要是因为TCP是流协定 罕用的解决这种问题的计划:要么短链接,要么长链接中采纳封装成桢framing技术 netty对应用Framing粘包半包的反对:固定长度的形式:肯定就按固定长度的来传,不够就补空,解码FixedLengthFrameDecoder宰割符的形式:以指定的宰割符来宰割,但要思考内容中如果有这个宰割符须要本义,解码DelimiterBasedFrameDecoder固定长度字段存内容的长度信息:要思考预留多少位来存储这个长度,解码器是LengthFieldBasedFramedDecoder,编码器LengthFieldPrepender 下面的编码器都继承了ByteToMessageDecoder这个抽象类 ByteToMessageDecoder 继承 ChannelInboundHandlerAdapterChannelInboundHandlerAdapter 中有一个channelRead办法解决数据,ByteToMessageDecoder 中就具体实现了channelRead办法ByteToMessageDecoder中保护了一个ByteBuf类型的数据积攒器cumulation,如果是第一笔数据间接赋值给cumulation,不是第一笔的就追加在cumulation前面,而后调 callDecode(ctx, cumulation, out); //其中callDecode中会调 decodeRemovalReentryProtection(ctx, in, out); //而后这个又会调decode(ctx, in, out);//这个decode是ByteToMessageDecoder中提供的形象办法,具体由下面的各种Decoder来实现 以FixedLengthFrameDecoder为例子 public class FixedLengthFrameDecoder extends ByteToMessageDecoder { private final int frameLength; public FixedLengthFrameDecoder(int frameLength) { if(frameLength <= 0) { throw new IllegalArgumentException("frameLength must be a positive integer: " + frameLength); } else { this.frameLength = frameLength; } } protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { Object decoded = this.decode(ctx, in); if(decoded != null) { out.add(decoded);//每一次解进去的数据放在out中 } } protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { return in.readableBytes() < this.frameLength?null:in.readSlice(this.frameLength).retain(); //如果以后积攒器就是这里的in中的数据小于定义的长度,不做任何操作,这个时候数据也不够,超过定义的长度,取frameLength长度的数据进去,剩下的就还在积攒器中 }

July 31, 2020 · 1 min · jiezi

netty系列4Reactor模式转载

本文转载自《Reactor模式》。 1.为什么是Reactor模式写多了代码的兄弟们都知道,JAVA代码由于到处面向接口及高度抽象,用到继承多态和设计模式,程序的组织不是按照正常的理解顺序来的,对代码跟踪很是个问题。所以,在阅读别人的源码时,如果不了解代码的组织方式,往往是晕头转向,不知在何处。尤其是阅读经典代码的时候,更是如此。 反过来,如果先了解代码的设计模式,再来去代码,就会阅读的很轻松,不会那么难懂。 像netty这样的精品中的极品,肯定也是需要先从设计模式入手的。netty的整体架构,基于了一个著名的模式——Reactor模式。Reactor模式,是高性能网络编程的必知必会模式。 首先熟悉Reactor模式,一定是磨刀不误砍柴工。 2.Reactor模式简介Netty是典型的Reactor模型结构,关于Reactor的详尽阐释,本文站在巨人的肩膀上,借助 Doug Lea(就是那位让人无限景仰的大爷)的“Scalable IO in Java”中讲述的Reactor模式。 “Scalable IO in Java”的地址是:http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf Reactor模式也叫反应器模式,大多数IO相关组件如Netty、Redis在使用的IO模式,为什么需要这种模式,它是如何设计来解决高性能并发的呢? 3.多线程IO的致命缺陷最最原始的网络编程思路就是服务器用一个while循环,不断监听端口是否有新的套接字连接,如果有,那么就调用一个处理函数处理,类似: while(true){socket = accept();handle(socket)}这种方法的最大问题是无法并发,效率太低,如果当前的请求没有处理完,那么后面的请求只能被阻塞,服务器的吞吐量太低。 之后,想到了使用多线程,也就是很经典的connection per thread,每一个连接用一个线程处理,类似: package com.crazymakercircle.iodemo.base;import com.crazymakercircle.config.SystemConfig;import java.io.IOException;import java.net.ServerSocket;import java.net.Socket;class BasicModel implements Runnable { public void run() { try { ServerSocket ss = new ServerSocket(SystemConfig.SOCKET\_SERVER\_PORT); while (!Thread.interrupted()) new Thread(new Handler(ss.accept())).start(); //创建新线程来handle // or, single-threaded, or a thread pool } catch (IOException ex) { /\* ... \*/ } } static class Handler implements Runnable { final Socket socket; Handler(Socket s) { socket = s; } public void run() { try { byte\[\] input = new byte\[SystemConfig.INPUT\_SIZE\]; socket.getInputStream().read(input); byte\[\] output = process(input); socket.getOutputStream().write(output); } catch (IOException ex) { /\* ... \*/ } } private byte\[\] process(byte\[\] input) { byte\[\] output=null; /\* ... \*/ return output; } }}对于每一个请求都分发给一个线程,每个线程中都独自处理上面的流程。 ...

June 17, 2020 · 5 min · jiezi

netty系列1-netty框架介绍

1. 前言Netty 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络I0 程序,是由JBOSS提供的。Netty本质是- 个NIO框架,适用于服务器通讯相关的多种应用场景,主要针对在TCP协议下的使用。 我原本想自己介绍netty框架,但是碍于目前能力和精力有限,写出来的东西没有别人好。所以本文基本都是转载一位大神的文章《新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析》。还有一位叫正号先生写的《Netty学习笔记》,也受益颇多。我也在kindle上看过两本netty教程的书,但烟火气不够,看完后困惑更多,反倒是在博客里面看到的这些文章,实实在在的触动了我。 担心原作者哪天把博客里面的文章删了,我就再也找不到了,所以这里就开始无耻的抄袭。 2. jdk原生nio的问题DK 原生也有一套网络应用程序 API,但是存在一系列问题,主要如下: NIO 的类库和 API 繁杂,使用麻烦:你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。需要具备其他的额外技能做铺垫:例如熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的 NIO 程序。可靠性能力补齐,开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。官方声称在 JDK 1.6 版本的 update 18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生概率降低了一些而已,它并没有被根本解决。3. netty的特点Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题。Netty的主要特点有: 设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池;真正的无连接数据报套接字支持(自 3.1 起)。使用方便:详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了。高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。安全:完整的 SSL/TLS 和 StartTLS 支持。社区活跃、不断更新:社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入。4. netty高性能的原因一个高性能、异步事件驱动的NIO框架2.支持TCP、UDP和文件传输。 ...

May 31, 2020 · 4 min · jiezi

从零开始手写-dubbo-rpc-框架

rpcrpc 是基于 netty 实现的 java rpc 框架,类似于 dubbo。 主要用于个人学习,由渐入深,理解 rpc 的底层实现原理。 前言工作至今,接触 rpc 框架已经有很长时间。 但是对于其原理一直只是知道个大概,从来没有深入学习过。 以前一直想写,但由于各种原因被耽搁。 技术准备Java 并发实战学习 TCP/IP 协议学习笔记 Netty 权威指南学习 这些技术的准备阶段,花费了比较长的时间。 也建议想写 rpc 框架的有相关的知识储备。 其他 rpc 框架使用的经验此处不再赘述。 快速迭代原来一直想写 rpc,却不行动的原因就是想的太多,做的太少。 想一下把全部写完,结果就是啥都没写。 所以本次的开发,每个代码分支做的事情实际很少,只做一个功能点。 陆陆续续经过近一个月的完善,对 rpc 框架有了自己的体会和进一步的认知。 代码实现功能,主要参考 Apache Dubbo 文档文档文档将使用 markdown 文本的形式,补充 code 层面没有的东西。 代码注释代码有详细的注释,便于阅读和后期维护。 测试目前测试代码算不上完善。后续将陆续补全。 rpc 模块rpc-common 公共代码 rpc-server 服务端 rpc-client 客户端 rpc-register 注册中心 rpc-test 测试模块 代码分支release_0.0.1-server 服务端启动 release_0.0.2-client 客戶端启动 release_0.0.3-客户端调用服务端 release_0.0.4-p2p 客户端主动调用服务端 release_0.0.5-serial 序列化 ...

November 2, 2019 · 1 min · jiezi

为什么Netty的FastThreadLocal速度快

前言最近在看netty源码的时候发现了一个叫FastThreadLocal的类,jdk本身自带了ThreadLocal类,所以可以大致想到此类比jdk自带的类速度更快,主要快在什么地方,以及为什么速度更快,下面做一个简单的分析; 性能测试ThreadLocal主要被用在多线程环境下,方便的获取当前线程的数据,使用者无需关心多线程问题,方便使用;为了能说明问题,分别对两个场景进行测试,分别是:多个线程操作同一个ThreadLocal,单线程下的多个ThreadLocal,下面分别测试: 1.多个线程操作同一个ThreadLocal分别对ThreadLocal和FastThreadLocal使用测试代码,部分代码如下: public static void test2() throws Exception { CountDownLatch cdl = new CountDownLatch(10000); ThreadLocal<String> threadLocal = new ThreadLocal<String>(); long starTime = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { new Thread(new Runnable() { @Override public void run() { threadLocal.set(Thread.currentThread().getName()); for (int k = 0; k < 100000; k++) { threadLocal.get(); } cdl.countDown(); } }, "Thread" + (i + 1)).start(); } cdl.await(); System.out.println(System.currentTimeMillis() - starTime + "ms"); }以上代码创建了10000个线程,同时往ThreadLocal设置,然后get十万次,然后通过CountDownLatch来计算总的时间消耗,运行结果为:1000ms左右;下面再对FastThreadLocal进行测试,代码类似: ...

October 14, 2019 · 4 min · jiezi

net的快递鸟物流单号自动识别查询api接口demo实例

背景: 不久前,自己对接调用实现了中通快递api的功能,发现如果换了其它快递再重新对接,岂不是会浪费太多的时间,物流这个接口对接是一个难题,要么需要逐一连接多家快递公司进行发货每对接一个快递公司就要开发十余个接口,开发工作量繁琐复杂。 所以选个第三方提供的快递API是最为合理的,下面给出快递鸟的api接口的设计实现。 1、应用场景(1)PC端、移动端应用或网站应用集成运单物流信息查询功能时,只需要录入单号即可完成查询,无需用户输入快递公司。(2)电商网站要在快递鸟查询或者订阅运单时,可通过单号识别先行判断物流公司后,再订阅到快递鸟。2、是否需要授权要Free申请服务3、接口描述/说明API ID:点击获取API Key:点击获取示例(1)请求示例JSON格式(1)该接口仅对运单号做出识别,识别可能属于的一家或多家快递公司。(2)接口并不返回物流轨迹,用户可结合即时查询接口和订阅查询接口完成轨迹查询、订阅的动作。(3)接口识别会返回一家或者多家快递公司,返回的数据根据快递鸟大数据分析结果排序,排名靠前的命中率更高。(4)若识别失败,快递鸟返回的匹配结果为空。(5)接口支持的消息接收方式为HTTP POST,请求方法的编码格式(utf-8):“application/x-www-form-urlencoded;charset=utf-8”。(6)请求系统级参数说明: 请求示例: show sourceview sourceprint?1 {2 "LogisticCode": "3967950525457"3 }(2)返回示例JSON格式show source01 { 02 "EBusinessID": "1257021", 03 "Success": true, 04 "LogisticCode": "3967950525457",05 "Shippers": [ 06 { 07 "ShipperCode": "YD",08 "ShipperName": "韵达快递"09 } 10 ]11 }返回示例: using System;using System.Collections.Generic;using System.IO;using System.Linq;using System.Net;using System.Text;using System.Web;namespace KdGoldAPI{ public class KdApiOrderDistinguish { //电商ID private string EBusinessID = "请到快递鸟官网申请http://www.kdniao.com/ServiceApply.aspx"; //电商加密私钥,快递鸟提供,注意保管,不要泄漏 private string AppKey = "请到快递鸟官网申请http://www.kdniao.com/ServiceApply.aspx"; //请求url //测试环境 private string ReqURL = "http://testapi.kdniao.cc:8081/Ebusiness/EbusinessOrderHandle.aspx"; //正式环境 //private string ReqURL = "http://api.kdniao.cc/Ebusiness/EbusinessOrderHandle.aspx"; /// <summary> /// Json方式 单号识别 /// </summary> /// <returns></returns> public string orderTracesSubByJson() { string requestData = "{'LogisticCode': '3967950525457'}"; Dictionary<string, string> param = new Dictionary<string, string>(); param.Add("RequestData", HttpUtility.UrlEncode(requestData, Encoding.UTF8)); param.Add("EBusinessID", EBusinessID); param.Add("RequestType", "2002"); string dataSign = encrypt(requestData, AppKey, "UTF-8"); param.Add("DataSign", HttpUtility.UrlEncode(dataSign, Encoding.UTF8)); param.Add("DataType", "2"); string result = sendPost(ReqURL, param); //根据公司业务处理返回的信息...... return result; } /// <summary> /// Post方式提交数据,返回网页的源代码 /// </summary> /// <param name="url">发送请求的 URL</param> /// <param name="param">请求的参数集合</param> /// <returns>远程资源的响应结果</returns> private string sendPost(string url, Dictionary<string, string> param) { string result = ""; StringBuilder postData = new StringBuilder(); if (param != null && param.Count > 0) { foreach (var p in param) { if (postData.Length > 0) { postData.Append("&"); } postData.Append(p.Key); postData.Append("="); postData.Append(p.Value); } } byte[] byteData = Encoding.GetEncoding("UTF-8").GetBytes(postData.ToString()); try { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.ContentType = "application/x-www-form-urlencoded"; request.Referer = url; request.Accept = "*/*"; request.Timeout = 30 * 1000; request.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)"; request.Method = "POST"; request.ContentLength = byteData.Length; Stream stream = request.GetRequestStream(); stream.Write(byteData, 0, byteData.Length); stream.Flush(); stream.Close(); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream backStream = response.GetResponseStream(); StreamReader sr = new StreamReader(backStream, Encoding.GetEncoding("UTF-8")); result = sr.ReadToEnd(); sr.Close(); backStream.Close(); response.Close(); request.Abort(); } catch (Exception ex) { result = ex.Message; } return result; } ///<summary> ///电商Sign签名 ///</summary> ///<param name="content">内容</param> ///<param name="keyValue">Appkey</param> ///<param name="charset">URL编码 </param> ///<returns>DataSign签名</returns> private string encrypt(String content, String keyValue, String charset) { if (keyValue != null) { return base64(MD5(content + keyValue, charset), charset); } return base64(MD5(content, charset), charset); } ///<summary> /// 字符串MD5加密 ///</summary> ///<param name="str">要加密的字符串</param> ///<param name="charset">编码方式</param> ///<returns>密文</returns> private string MD5(string str, string charset) { byte[] buffer = System.Text.Encoding.GetEncoding(charset).GetBytes(str); try { System.Security.Cryptography.MD5CryptoServiceProvider check; check = new System.Security.Cryptography.MD5CryptoServiceProvider(); byte[] somme = check.ComputeHash(buffer); string ret = ""; foreach (byte a in somme) { if (a < 16) ret += "0" + a.ToString("X"); else ret += a.ToString("X"); } return ret.ToLower(); } catch { throw; } } /// <summary> /// base64编码 /// </summary> /// <param name="str">内容</param> /// <param name="charset">编码方式</param> /// <returns></returns> private string base64(String str, String charset) { return Convert.ToBase64String(System.Text.Encoding.GetEncoding(charset).GetBytes(str)); } }}

September 9, 2019 · 2 min · jiezi

一个异步无限发送的Netty实例

本博客 猫叔的博客,转载请申明出处阅读本文约 “4分钟” 适读人群:Java-Netty 初级 无限异步发送数据流版本:netty 4.1.*申明:本文旨在重新分享讨论Netty官方相关案例,添加部分个人理解与要点解析。 这个是InChat的案例地址,里面补充了详细的注释,比起官方会容易看一点。 官方案例地址:https://netty.io/4.1/xref/io/... 正文DiscardClient(客户端)DiscardClientHandlerDiscardServer(服务端)DiscardServerHandler要点介绍ChannelInboundHandlerAdapter 官方介绍实现了抽象基类ChannelInboundHandler,因此也提供其所有方法的实现,子类可以重写方法实现来改变它。 注意:channelRead(ChannelHandlerContext, Object) 方法自动返回后不会释放消息。如果您正在寻找ChannelInboundHandler自动发布收到的消息的实现,请参阅SimpleChannelInboundHandlerSimpleChannelInboundHandler 官方介绍允许显式只处理特定类型的消息 writeZero(int length) 官方介绍从当前开始 用(0x00)填充此缓冲区writerIndex并按writerIndex指定值增加length。如果this.writableBytes小于length,ensureWritable(int) 将调用以尝试扩展容量以适应。 directBuffer(int initialCapacity) 官方介绍使用给定的初始值给ByteBuf分配直接值 release() 官方介绍每一个新分配的ByteBuf的引用计数值为1,每对这个ByteBuf对象增加一个引用,需要调用ByteBuf.retain()方法,而每减少一个引用,需要调用ByteBuf.release()方法。当这个ByteBuf对象的引用计数值为0时,表示此对象可回收 retainedDuplicate() 官方介绍返回保留的缓冲区,该缓冲区共享此缓冲区的整个区域。修改返回的缓冲区或此缓冲区的内容会影响彼此的内容,同时它们会维护单独的索引和标记。此方法的行为类似于此方法,duplicate().retain()但此方法可能返回产生较少垃圾的缓冲区实现。 ChannelFutureListener 官方介绍监听一个ChannelFuture的结果。Channel一旦通过调用添加此侦听器,将以ChannelFuture.addListener(GenericFutureListener)异步I / O的操作通知结果。 项目源码DiscardClient(客户端)/** * @ClassName DiscardClient * @Description TODO * @Author MySelf * @Date 2019/8/19 20:38 * @Version 1.0 **/public final class DiscardClient { //判断是否加密 static final boolean SSL = System.getProperty("ssl") != null; //监听本地服务 static final String HOST = System.getProperty("host", "127.0.0.1"); //监听端口 static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); //发送消息的大小,用于EchoClientHandler static final int SIZE = Integer.parseInt(System.getProperty("size", "256")); public static void main(String[] args) throws Exception { //公共抽象类,安全套接字协议实现充当工厂SSLEngine和SslHandler。在内部,它通过JDK SSLContext或OpenSSL 实现SSL_CTX final SslContext sslCtx; if (SSL){ //用于配置新SslContext以进行创建的构建器 sslCtx = SslContextBuilder.forClient() //用于验证远程端点证书的可信管理器 //InsecureTrustManagerFactory:在TrustManagerFactory没有任何验证的情况下信任所有X.509证书的不安全因素 //注:切勿TrustManagerFactory在生产中使用它。它纯粹是出于测试目的,因此非常不安全。 .trustManager(InsecureTrustManagerFactory.INSTANCE).build(); }else { sslCtx = null; } //事件循环 EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY,true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel sc) throws Exception { ChannelPipeline p = sc.pipeline(); //了解SslContext的用法 if (sslCtx != null){ p.addLast(sslCtx.newHandler(sc.alloc(),HOST,PORT)); } p.addLast(new DiscardClientHandler()); } }); ChannelFuture f = b.connect(HOST,PORT).sync(); f.channel().closeFuture().sync(); }finally { group.shutdownGracefully(); } }}DiscardClientHandler/** * @ClassName DiscardClientHandler * @Description TODO * @Author MySelf * @Date 2019/8/19 20:38 * @Version 1.0 **/public class DiscardClientHandler extends SimpleChannelInboundHandler<Object> { private ByteBuf content; private ChannelHandlerContext ctx; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { this.ctx = ctx; // 初始化消息 content = ctx.alloc().directBuffer(DiscardClient.SIZE).writeZero(DiscardClient.SIZE); // 发送初始信息 generateTraffic(); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { //引用计数为0,释放 content.release(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // 引发异常时关闭连接 cause.printStackTrace(); ctx.close(); } @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception { // 服务器应该不发送任何内容,但如果它发送什么,丢弃它。 } // 生成数据 private void generateTraffic(){ // 将出站缓冲区刷新到套接字 // 刷新后,再次生成相同数量的流量 ctx.writeAndFlush(content.retainedDuplicate()).addListener(trafficGenerator); } // 数据触发 private final ChannelFutureListener trafficGenerator = new ChannelFutureListener() { //完成操作后的方法调用,即只要成功无限调用generateTraffic() @Override public void operationComplete(ChannelFuture channelFuture) throws Exception { if (channelFuture.isSuccess()){ generateTraffic(); }else { channelFuture.cause().printStackTrace(); channelFuture.channel().close(); } } };}DiscardServer(服务端)/** * @ClassName DiscardServer * @Description TODO * @Author MySelf * @Date 2019/8/19 20:38 * @Version 1.0 **/public class DiscardServer { static final boolean SSL = System.getProperty("ssl") != null; static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); public static void main(String[] args) throws Exception { final SslContext sslCtx; if (SSL){ //SelfSignedCertificate:生成临时自签名证书以进行测试 SelfSignedCertificate ssc = new SelfSignedCertificate(); sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); }else{ sslCtx = null; } EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup,workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG,100) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); if (sslCtx != null){ p.addLast(sslCtx.newHandler(ch.alloc())); } p.addLast(new DiscardServerHandler()); } }); // 绑定并开始接受传入连接 ChannelFuture f = b.bind(PORT).sync(); // 等待服务器套接字关闭 // 这个例子,这不会发生,但你可以这样优雅的做 f.channel().closeFuture().sync(); }finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }}DiscardServerHandler/** * @ClassName DiscardServerHandler * @Description TODO * @Author MySelf * @Date 2019/8/19 20:39 * @Version 1.0 **/public class DiscardServerHandler extends SimpleChannelInboundHandler<Object> { @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception { // discard } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // 引发异常时关闭连接 cause.printStackTrace(); ctx.close(); }}公众号:Java猫说学习交流群:728698035 ...

August 19, 2019 · 3 min · jiezi

一个简单的NettyEchoDemo

本博客 猫叔的博客,转载请申明出处阅读本文约 “4分钟” 适读人群:Java-Netty 初级 Echo简易通讯案例版本:netty 4.1.*申明:本文旨在重新分享讨论Netty官方相关案例,添加部分个人理解与要点解析。 这个是InChat的案例地址,里面补充了详细的注释,比起官方会容易看一点。 官方案例地址:https://netty.io/4.1/xref/io/... 正文EchoClient(客户端)EchoClientHandlerEchoServer(服务端)EchoServerHandler要点介绍SslContext官方介绍:https://netty.io/4.1/api/io/n... 公共抽象类,安全套接字协议实现充当工厂SSLEngine和SslHandler。在内部,它通过JDK SSLContext或OpenSSL 实现SSL_CTX,还有关于它的使用方式,如果你需要ssl加密的话 SslContextBuilder官方介绍:https://netty.io/4.1/api/io/n... 用于配置新SslContext以进行创建的构建器,其中包含多个方法这里就不多补充,大家可以去看看 InsecureTrustManagerFactory官方介绍:https://netty.io/4.1/api/io/n... 在TrustManagerFactory没有任何验证的情况下信任所有X.509证书的不安全因素 注意:切勿TrustManagerFactory在生产中使用它。它纯粹是出于测试目的,因此非常不安全。SelfSignedCertificate官方介绍:https://netty.io/4.1/api/io/n... 生成临时自签名证书以进行测试 注意:切勿在生产中使用此类生成的证书和私钥。它纯粹是出于测试目的,因此非常不安全。它甚至使用不安全的伪随机生成器在内部更快地生成项目源码EchoClient/** * @ClassName EchoClient * @Description 一个简单的应答通讯的实例 * @Author MySelf * @Date 2019/8/17 17:56 * @Version 1.0 **/public final class EchoClient { //判断是否加密 static final boolean SSL = System.getProperty("ssl") != null; //监听本地服务 static final String HOST = System.getProperty("host", "127.0.0.1"); //监听端口 static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); //发送消息的大小,用于EchoClientHandler static final int SIZE = Integer.parseInt(System.getProperty("size", "256")); public static void main(String[] args) throws Exception { //公共抽象类,安全套接字协议实现充当工厂SSLEngine和SslHandler。在内部,它通过JDK SSLContext或OpenSSL 实现SSL_CTX final SslContext sslCtx; if (SSL){ //用于配置新SslContext以进行创建的构建器 sslCtx = SslContextBuilder.forClient() //用于验证远程端点证书的可信管理器 //InsecureTrustManagerFactory:在TrustManagerFactory没有任何验证的情况下信任所有X.509证书的不安全因素 //注:切勿TrustManagerFactory在生产中使用它。它纯粹是出于测试目的,因此非常不安全。 .trustManager(InsecureTrustManagerFactory.INSTANCE).build(); }else { sslCtx = null; } //事件循环 EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY,true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel sc) throws Exception { ChannelPipeline p = sc.pipeline(); //了解SslContext的用法 if (sslCtx != null){ p.addLast(sslCtx.newHandler(sc.alloc(),HOST,PORT)); } p.addLast(new EchoClientHandler()); } }); //这个sync后的代码均会执行 ChannelFuture f = b.connect(HOST,PORT).sync(); System.out.println("before-----"); //这个sync后的代码不会执行 f.channel().closeFuture().sync(); System.out.println("after-----"); }finally { group.shutdownGracefully(); } }}EchoClientHandler/** * @ClassName EchoClientHandler * @Description TODO * @Author MySelf * @Date 2019/8/17 18:06 * @Version 1.0 **/public class EchoClientHandler extends ChannelInboundHandlerAdapter { private final ByteBuf firstMessage; public EchoClientHandler(){ //获取EchoClient的SIZE //Unpooled:ByteBuf通过分配新空间或通过包装或复制现有字节数组,字节缓冲区和字符串来创建新的 firstMessage = Unpooled.buffer(EchoClient.SIZE); for (int i = 0; i < firstMessage.capacity(); i++){ firstMessage.writeByte((byte)i); } } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(firstMessage); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ctx.write(msg); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); }}EchoServer/** * @ClassName EchoServer * @Description 服务端 * @Author MySelf * @Date 2019/8/17 18:15 * @Version 1.0 **/public final class EchoServer { static final boolean SSL = System.getProperty("ssl") != null; static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); public static void main(String[] args) throws Exception { final SslContext sslCtx; if (SSL){ //SelfSignedCertificate:生成临时自签名证书以进行测试 SelfSignedCertificate ssc = new SelfSignedCertificate(); sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); }else{ sslCtx = null; } EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); final EchoServerHandler serverHandler = new EchoServerHandler(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup,workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG,100) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); if (sslCtx != null){ p.addLast(sslCtx.newHandler(ch.alloc())); } p.addLast(serverHandler); } }); ChannelFuture f = b.bind(PORT).sync(); f.channel().closeFuture().sync(); }finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }}EchoServerHandler/** * @ClassName EchoServerHandler * @Description TODO * @Author MySelf * @Date 2019/8/17 18:14 * @Version 1.0 **/@ChannelHandler.Sharablepublic class EchoServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ctx.write(msg); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); }}公众号:Java猫说学习交流群:728698035 ...

August 18, 2019 · 3 min · jiezi

设计一个全局异常处理器

前言最近稍微闲了一点于是把这个半年都没更新的开源项目 cicada 重新捡了起来。 一些新关注的朋友应该还不知道这项目是干啥的?先来看看官方介绍吧(其实就我自己写的????) cicada: 基于 Netty4 实现的快速、轻量级 WEB 框架;没有过多的依赖,核心 jar 包仅 30KB。 针对这个轮子以前也写过相关的介绍,感兴趣的可以再翻回去看看: 「造个轮子」——cicada(轻量级 WEB 框架)「造个轮子」——cicada 源码分析「造个轮子」——cicada 设计一个配置模块「造个轮子」——cicada 设计全局上下文利用责任链模式设计一个拦截器设计一个可拔插的 IOC 容器这些都看完了相信对这个小玩意应该会有更多的想法。 效果广告打完了,回到正题;大家平时最常用的 MVC 框架当属 SpringMVC 了,而在搭建脚手架的时候相信全局异常处理是必不可少的。 Spring 用法通常我们的做法如下: 传统 Spring 版本: 实现一个 Spring 自带的接口,重写其中的方法,最后的异常处理便在此处。将这个类配置在 Spring 的 xml ,当做一个 bean 注册到 Spring 容器中。public class CustomExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { //自定义处理}<bean class="ssm.exception.CustomExceptionResolver"></bean> 当然现在流行的 SpringBoot 也有对应的简化版本: @ControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(value = Exception.class) public Object defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { //自定义处理 }}全部都换为注解形式,但本质上还是一样的。 ...

July 15, 2019 · 1 min · jiezi

解码器-与-编码器

解码器ByteToMessageDecoder 抽象类将字节解码为消息(或者另一个字节序列), Netty 为它提供了一个抽象的基类: ByteToMessageDecoder. 由于你不可能知道远程节点是否会一次性地发送一个完整的消息, 所以这个类会对入站数据进行缓冲, 直到它准备好处理. 只是将消息进行缓冲, 并不会进行解码操作. 如何缓冲的下面会说. 下面这张图说明了在网络传输中可能出现的情况. ByteToMessageDecoder 抽象类有两个重要方法. 方 法描 述decode(ChannelHandlerContext ctx,ByteBuf in,List<Object> out)必须实现的唯一抽象方法. 方法被调用时传入一个包含新数据的ByteBuf, 和一个添加解码消息的List. 对方法的调用会重复进行, 直到没有新元素被添加到List, 或ByteBuf中没有更多可读取的字节. 如果List不为空, 它的内容会被传递给 ChannelPipeline 中的下一个 ChannelInboundHandler.decodeLast(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)简单调用 decode() 方法, 当 Channel 状态为非活动时, 这个方法会被调用一次. 可以重写该方法已提供特殊处理.ByteToMessageDecoder 的属性: cumulation属性: 用来保存累计读取到的字节. 我们读到的新字节会保存(缓冲)在这里.cumulator属性: 用来做累计的. 负责将读到的新字节写入 cumulation. 有两个实现 MERGE_CUMULATOR 和 COMPOSITE_CUMULATOR.singleDecode: 设置为true后, 单个解码器只会解码出一个结果.decodeWasNull: 解码结果为空.first: 是否是第一次读取数据.discardAfterReads: 多少次读取后, 丢弃数据 默认16次.numReads: 已经累加了多少次数据了. 重点我们实现 ByteToMessageDecoder 接口时, 最主要的方法就是 decode, 当有新数据进入时, 会先缓冲数据然后将缓冲后的数据传递给我们. ...

June 30, 2019 · 2 min · jiezi

史上最强Java-NIO入门担心从入门到放弃的请读这篇

本文原题“《NIO 入门》,作者为“Gregory M. Travis”,他是《JDK 1.4 Tutorial》等书籍的作者。 1、引言Java NIO是Java 1.4版加入的新特性,虽然Java技术日新月异,但历经10年,NIO依然为Java技术领域里最为重要的基础技术栈,而且依据现实的应用趋势,在可以预见的未来,它仍将继续在Java技术领域占据重要位置。 网上有关Java NIO的技术文章,虽然写的也不错,但通常是看完一篇马上懵逼。接着再看!然后,会更懵逼。。。 哈哈哈! 本文作者厚积薄发,以远比一般的技术博客或技术作者更深厚的Java技术储备,为你由浅入深,从零讲解到底什么是Java NIO。本文即使没有多少 Java 编程经验的读者也能很容易地开始学习 NIO。 (本文同步发布于:http://www.52im.net/thread-26...) 2、关于作者Gregory M. Travis:技术顾问、多产的技术作家,现居纽约。他从Java语言发布的第1天起,就已经是Java程序员啦! Gregory M. Travis是《JDK 1.4 Tutorial》一书的作者,Java程序员应该都清楚,能写好JDK Tutorial这种书籍或手册的,除了SUN(现在是Oracle)公司的Java创建者们,余下的也只有各路实打实的Java大牛们才能hold住。 3、在开始之前3.1 关于本教程 新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。通过定义包含数据的类,以及通过以块的形式处理这些数据,NIO 不用使用本机代码就可以利用低级优化,这是原来的 I/O 包所无法做到的。 在本教程中,我们将讨论 NIO 库的几乎所有方面,从高级的概念性内容到底层的编程细节。除了学习诸如缓冲区和通道这样的关键 I/O 元素外,您还有机会看到在更新后的库中标准 I/O 是如何工作的。您还会了解只能通过 NIO 来完成的工作,如异步 I/O 和直接缓冲区。 在本教程中,我们将使用展示 NIO 库的不同方面的代码示例。几乎每一个代码示例都是一个大的 Java 程序的一部分,您可以在本文末的附件中下载到这个 Java 程序。在做这些练习时,我们推荐您在自己的系统上下载、编译和运行这些程序。在您学习了本教程以后,这些代码将为您的 NIO 编程努力提供一个起点。 本教程是为希望学习更多关于 Java NIO 库的知识的所有程序员而写的。为了最大程度地从这里的讨论中获益,您应该理解基本的 Java 编程概念,如类、继承和使用包。多少熟悉一些原来的 I/O 库(来自java.io.* 包)也会有所帮助。 ...

June 29, 2019 · 9 min · jiezi

少啰嗦一分钟带你读懂Java的NIO和经典IO的区别

1、引言很多初涉网络编程的程序员,在研究Java NIO(即异步IO)和经典IO(也就是常说的阻塞式IO)的API时,很快就会发现一个问题:我什么时候应该使用经典IO,什么时候应该使用NIO? 在本文中,将尝试用简明扼要的文字,阐明Java NIO和经典IO之间的差异、典型用例,以及这些差异如何影响我们的网络编程或数据传输代码的设计和实现的。 本文没有复杂理论,也没有像网上基它文章一样千篇一律的复制粘贴,有的只是接地气的通俗易懂,希望能给你带来帮助。 (本文同步发布于:http://www.52im.net/thread-26...) 2、相关文章《Java新一代网络编程模型AIO原理及Linux系统AIO介绍》《Java NIO基础视频教程、MINA视频教程、Netty快速入门视频》 3、Java NIO和IO的主要区别下表总结了Java NIO和IO之间的主要区别。我将在表格后面的部分中详细介绍每个区别。 3.1 Stream Oriented vs. Buffer OrientedJava NIO和IO之间的第一个重要区别是IO是面向流的,其中NIO是面向缓冲区的。那么,这意味着什么? 面向流的Java IO意味着您可以从流中一次读取一个或多个字节。你对读取的字节做什么取决于你。它们不会缓存在任何地方。此外,您无法在流中的数据中前后移动。如果需要在从流中读取的数据中前后移动,则需要先将其缓存在缓冲区中。 Java NIO的面向缓冲区的方法略有不同。数据被读入缓冲区,稍后处理该缓冲区。你可以根据需要在缓冲区中前后移动。这使你在处理过程中具有更大的灵活性。但是,你还需要检查缓冲区是否包含完整处理所需的所有数据。并且,你需要确保在将更多数据读入缓冲区时,不要覆盖尚未处理的缓冲区中的数据。 3.2 Blocking vs. Non-blocking IOJava IO的各种流都是blocking的。这意味着,当线程调用read()或write()时,该线程将被阻塞,直到有一些数据要读取,或者数据被完全写入,在此期间,该线程无法执行任何其他操作。 Java NIO的非阻塞模式允许线程请求从通道读取数据,并且只获取当前可用的内容,或者根本没有数据,如果当前没有数据可用。线程可以继续使用其他内容,而不是在数据可供读取之前保持阻塞状态。 非阻塞写入也是如此,线程可以请求将某些数据写入通道,但不要等待它完全写入。然后线程可以继续并在同一时间做其他事情。 线程在IO调用中没有阻塞时花费空闲时间,通常在此期间在其他通道上执行IO。也就是说,单个线程现在可以管理多个输入和输出通道。 4、SelectorsJava NIO的选择器允许单个线程监视多个输入通道。你可以使用选择器注册多个通道,然后使用单个线程“选择”具有可用于处理的输入的通道,或者选择准备写入的通道。这种选择器机制使单个线程可以轻松管理多个通道。 5、NIO和经典IO如何影响应用程序的设计?选择NIO或IO作为IO工具包可能会影响应用程序设计的以下方面: 1)API调用NIO或IO类; 2)处理数据; 3)用于处理数据的线程数。 5.1 API调用当然,使用NIO时的API调用看起来与使用IO时不同。这并不奇怪。而不是仅仅从例如InputStream读取字节的数据字节,必须首先将数据读入缓冲区,然后从那里进行处理。 5.2 数据处理使用纯NIO设计与IO设计时,数据处理也会受到影响。 在IO设计中,您从InputStream或Reader中读取字节的数据字节。想象一下,您正在处理基于行的文本数据流。 例如: Name: AnnaAge: 25Email: [url=mailto:anna@mailserver.com]anna@mailserver.com[/url]Phone: 1234567890这个文本行流可以像这样处理: InputStream input = ... ; // get the InputStream from the client socketBufferedReader reader = newBufferedReader(newInputStreamReader(input));String nameLine = reader.readLine();String ageLine = reader.readLine();String emailLine = reader.readLine();String phoneLine = reader.readLine();注意处理状态是如何,由程序执行的程度决定的。换句话说,一旦第一个reader.readLine()方法返回,您就确定已经读取了整行文本。readLine()会阻塞直到读取整行,这就是原因。您还知道此行包含名称。同样,当第二个readLine()调用返回时,您知道此行包含年龄等。 ...

June 25, 2019 · 1 min · jiezi

Netty之EventLoop与EventLoopGroup

Netty解决的事情Netty主要解决两个相应关注领域。(1)异步和事件驱动的实现。(2)一组设计模式,将应用逻辑与网络层解耦。 EventLoop接口用于处理连接的生命周期中所发生的事件。 一个EventLoopGroup包含一个或者多个EventLoop一个EventLoop在它的生命周期内只和一个Thread绑定所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理一个Channel在它的生命周期内只注册于一个EventLoop一个EventLoop可能会被分配给一个或者多个ChannelNetty NIO 客户端// 创建一个 EventLoopGroup 对象EventLoopGroup group = new NioEventLoopGroup();// 创建 Bootstrap 对象Bootstrap b = new Bootstrap();// 设置使用的 EventLoopGroupb.group(group);Netty只创建一个EventLoop一个EventLoop可以对应一个Reactor一个 Bootstrap 的启动,只能发起对一个远程的地址。所以只会使用一个 NIO Selector ,也就是说仅使用一个 Reactor 。即使,我们在声明使用一个 EventLoopGroup ,该 EventLoopGroup 也只会分配一个 EventLoop 对 IO 事件进行处理。Netty NIO 服务端// 创建两个 EventLoopGroup 对象EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 创建 boss 线程组 用于服务端接受客户端的连接EventLoopGroup workerGroup = new NioEventLoopGroup(); // 创建 worker 线程组 用于进行 SocketChannel 的数据读写// 创建 ServerBootstrap 对象ServerBootstrap b = new ServerBootstrap();// 设置使用的 EventLoopGroupb.group(bossGroup, workerGroup);bossGroup 对应 Reactor 模式的 mainReactor ,用于服务端接受客户端的连接。比较特殊的是,传入了方法参数 nThreads = 1 ,表示只使用一个 EventLoop ,即只使用一个 Reactor 。这个也符合我们上面提到的,“通常,mainReactor 只需要一个,因为它一个线程就可以处理”。workerGroup 对应 Reactor 模式的 subReactor ,用于进行 SocketChannel 的数据读写。对于 EventLoopGroup ,如果未传递方法参数 nThreads ,表示使用 CPU 个数 Reactor 。这个也符合我们上面提到的,“通常,subReactor 的个数和 CPU 个数相等,每个 subReactor 独占一个线程来处理”。Netty中的Reactor模型 ...

June 23, 2019 · 1 min · jiezi

NettyByteBuf-一

欢迎关注公众号:【爱编码】如果有需要后台回复2019赠送1T的学习资料哦!! 简介所有的网路通信都涉及字节序列的移动,所以高效易用的数据结构明显是必不可少的。Netty的ByteBuf实现满足并超越了这些需求。 ByteBuf结构ByteBuf维护了两个不同的索引:一个是用于读取,一个用于写入。当你从ByteBuf读取是,它的readerIndex将会被递增已经被读取的字节数。同样地,当你写入ByteBuf时,它的witerIndex也会被递增。 作为一个容器,源码中的如下。有三块区域discardable bytes:无效空间(已经读取过的空间),可丢弃字节的区域,由readerIndex指针控制readable bytes:内容空间,可读字节的区域,由readerIndex和writerIndex指针控制控制writable bytes:空闲空间,可写入字节的区域,由writerIndex指针和capacity容量控制 * <pre> * +-------------------+------------------+------------------+ * | discardable bytes | readable bytes | writable bytes | * | | (CONTENT) | | * +-------------------+------------------+------------------+ * | | | | * 0 <= readerIndex <= writerIndex <= capacity * </pre>ByteBuf使用模式总体分类划分是可根据JVM堆内存来区分的。 1.堆内内存(JVM堆空间内)2.堆外内存(本机直接内存)3.复合缓冲区(以上2种缓冲区多个混合)1.堆内内存最常用的ByteBuf模式是将数据存储在JVM的堆空间中。它能在没有使用池化的情况下提供快速的分配和释放。 2.堆外内存JDK允许JVM实现通过本地调用来分配内存。主要是为了避免每次调用本地I/O操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。**最大的特点:它的内容将驻留在常规的会被垃圾回收的堆之外。最大的缺点:相对于堆缓冲区,它的分配和释放都是较为昂贵的。** 3.复合缓冲区常用类:CompositeByteBuf,它为多个ByteBuf提供一个聚合视图,将多个缓冲区表示为单个合并缓冲区的虚拟表示。比如:HTTP协议:头部和主体这两部分由应用程序的不同模块产生。这个时候把这两部分合并的话,选择CompositeByteBuf是比较好的。 ByteBuf分类主要分为三大类 Pooled和Unpooled (池化)unsafe和非unsafe ()Heap和Direct (堆内和堆外) Pooled和UnpooledPooled:每次都从预先分配好的内存中去取出一段连续内存封装成一个ByteBuf给应用程序使用Unpooled:每次分配内存的时候,直接调用系统api,向操作系统申请一块内存 Heap和Direct:Head:是调用jvm的堆内存进行分配的,需要被gc进行管理Direct:是调用jdk的api进行内存分配,不受jvm控制,不会参与到gc的过程 Unsafe和非Unsafejdk中有Unsafe对象可以直接拿到对象的内存地址,并且基于这个内存地址进行读写操作。那么对应的分类的区别就是是否可以拿到jdk底层的Unsafe进行读写操作了。 Java为什么会引入及如何使用Unsafe 内存分配ByteBufAllocator这个接口实现负责分配缓冲区并且是线程安全的。从下面的接口方法以及注释可以总结出主要是围绕上面的三种ByteBuf内存模式:堆内,堆外以及复合型的内存分配。 /** * Implementations are responsible to allocate buffers. Implementations of this interface are expected to be * thread-safe. */public interface ByteBufAllocator { ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR; /** * Allocate a {@link ByteBuf}. If it is a direct or heap buffer * depends on the actual implementation. */ ByteBuf buffer(); /** * Allocate a {@link ByteBuf} with the given initial capacity. * If it is a direct or heap buffer depends on the actual implementation. */ ByteBuf buffer(int initialCapacity); /** * Allocate a {@link ByteBuf} with the given initial capacity and the given * maximal capacity. If it is a direct or heap buffer depends on the actual * implementation. */ ByteBuf buffer(int initialCapacity, int maxCapacity); /** * Allocate a {@link ByteBuf}, preferably a direct buffer which is suitable for I/O. */ ByteBuf ioBuffer(); /** * Allocate a {@link ByteBuf}, preferably a direct buffer which is suitable for I/O. */ ByteBuf ioBuffer(int initialCapacity); /** * Allocate a {@link ByteBuf}, preferably a direct buffer which is suitable for I/O. */ ByteBuf ioBuffer(int initialCapacity, int maxCapacity); /** * Allocate a heap {@link ByteBuf}. */ ByteBuf heapBuffer(); /** * Allocate a heap {@link ByteBuf} with the given initial capacity. */ ByteBuf heapBuffer(int initialCapacity); /** * Allocate a heap {@link ByteBuf} with the given initial capacity and the given * maximal capacity. */ ByteBuf heapBuffer(int initialCapacity, int maxCapacity); /** * Allocate a direct {@link ByteBuf}. */ ByteBuf directBuffer(); /** * Allocate a direct {@link ByteBuf} with the given initial capacity. */ ByteBuf directBuffer(int initialCapacity); /** * Allocate a direct {@link ByteBuf} with the given initial capacity and the given * maximal capacity. */ ByteBuf directBuffer(int initialCapacity, int maxCapacity); /** * Allocate a {@link CompositeByteBuf}. * If it is a direct or heap buffer depends on the actual implementation. */ CompositeByteBuf compositeBuffer(); /** * Allocate a {@link CompositeByteBuf} with the given maximum number of components that can be stored in it. * If it is a direct or heap buffer depends on the actual implementation. */ CompositeByteBuf compositeBuffer(int maxNumComponents); /** * Allocate a heap {@link CompositeByteBuf}. */ CompositeByteBuf compositeHeapBuffer(); /** * Allocate a heap {@link CompositeByteBuf} with the given maximum number of components that can be stored in it. */ CompositeByteBuf compositeHeapBuffer(int maxNumComponents); /** * Allocate a direct {@link CompositeByteBuf}. */ CompositeByteBuf compositeDirectBuffer(); /** * Allocate a direct {@link CompositeByteBuf} with the given maximum number of components that can be stored in it. */ CompositeByteBuf compositeDirectBuffer(int maxNumComponents); /** * Returns {@code true} if direct {@link ByteBuf}'s are pooled */ boolean isDirectBufferPooled(); /** * Calculate the new capacity of a {@link ByteBuf} that is used when a {@link ByteBuf} needs to expand by the * {@code minNewCapacity} with {@code maxCapacity} as upper-bound. */ int calculateNewCapacity(int minNewCapacity, int maxCapacity); }其中ByteBufAllocator 的具体实现可以查看其子类,如下图 ...

June 18, 2019 · 10 min · jiezi

EventLoop-和-线程模型

关于 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 的主从多线程模型. ...

June 10, 2019 · 3 min · jiezi

Netty如何接入新连接

欢迎关注公众号:【爱编程】如果有需要后台回复2019赠送1T的学习资料哦!!前文再续,书接上一回【NioEventLoop】。在研究NioEventLoop执行过程的时候,检测IO事件(包括新连接),处理IO事件,执行所有任务三个过程。其中检测IO事件中通过持有的selector去轮询事件,检测出新连接。这里复用同一段代码。 Channel的设计在开始分析前,先了解一下Channel的设计 顶层Channel接口定义了socket事件如读、写、连接、绑定等事件,并使用AbstractChannel作为骨架实现了这些方法。查看器成员变量,发现大多数通用的组件,都被定义在这里 第二层AbstractNioChannel定义了以NIO,即Selector的方式进行读写事件的监听。其成员变量保存了selector相关的一些属性。 第三层内容比较多,定义了服务端channel(左边继承了AbstractNioMessageChannel的NioServerSocketChannel)以及客户端channel(右边继承了AbstractNioByteChannel的NioSocketChannel)。 如何接入新连接?本文开始探索一下Netty是如何接入新连接?主要分为四个部分 1.检测新连接2.创建NioSocketChannel3.分配线程和注册Selector4.向Selector注册读事件1.检测新连接Netty服务端在启动的时候会绑定一个bossGroup,即NioEventLoop,在bind()绑定端口的时候注册accept(新连接接入)事件。扫描到该事件后,便处理。因此入口从:NioEventLoop#processSelectedKeys()开始。 private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); //省略代码 // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead // to a spin loop //如果当前NioEventLoop是workGroup 则可能是OP_READ,bossGroup是OP_ACCEPT if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { //新连接接入以及读事件处理入口 unsafe.read(); } }关键的新连接接入以及读事件处理入口unsafe.read(); a).这里的unsafe是在Channel创建过程的时候,调用了父类AbstractChannel#AbstractChannel()的构造方法,和pipeline一起初始化的。 protected AbstractChannel(Channel parent) { this.parent = parent; id = newId(); unsafe = newUnsafe(); pipeline = newChannelPipeline(); }服务端:unsafe 为NioServerSockeChannel的父类AbstractNioMessageChannel#newUnsafe()创建,可以看到对应的是AbstractNioMessageChannel的内部类NioMessageUnsafe; ...

June 7, 2019 · 7 min · jiezi

Netty服务端和客户端

欢迎关注公众号:【爱编程】如果有需要后台回复2019赠送1T的学习资料哦!! 本文是基于Netty4.1.36进行分析 服务端Netty服务端的启动代码基本都是如下: private void start() throws Exception { final EchoServerHandler serverHandler = new EchoServerHandler(); /** * NioEventLoop并不是一个纯粹的I/O线程,它除了负责I/O的读写之外 * 创建了两个NioEventLoopGroup, * 它们实际是两个独立的Reactor线程池。 * 一个用于接收客户端的TCP连接, * 另一个用于处理I/O相关的读写操作,或者执行系统Task、定时任务Task等。 */ EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup childGroup = new NioEventLoopGroup(); try { //ServerBootstrap负责初始化netty服务器,并且开始监听端口的socket请求 ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, childGroup) .channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception {// 为监听客户端read/write事件的Channel添加用户自定义的ChannelHandler socketChannel.pipeline().addLast(serverHandler); } }); ChannelFuture f = b.bind().sync(); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { bossGroup.shutdownGracefully().sync(); childGroup.shutdownGracefully().sync(); } }从上图的代码可以总结为以下几个步骤: ...

June 3, 2019 · 2 min · jiezi

ChannelPipeline-和-ChannelHandler

ChannelHandlerChannelChannel 概念与 java.nio.channel 概念一致, 用以连接IO设备 (socket, 文件等) 的纽带. 例如将网络的读、写, 客户端发起连接, 主动关闭连接, 链路关闭, 获取通信双方的网络地址等. Channel 的 IO 类型主要有两种: 非阻塞IO (NIO) 以及阻塞IO(OIO). 数据传输类型有两种: 按事件消息传递 (Message) 以及按字节传递 (Byte). 适用方类型也有两种: 服务器(ServerSocket) 以及客户端(Socket). 还有一些根据传输协议而制定的的Channel, 如: UDT、SCTP等. Netty 按照类型逐层设计相应的类. 最底层的为抽象类 AbstractChannel, 再以此根据IO类型、数据传输类型、适用方类型实现. 类图可以一目了然, 如下图所示: Channel 状态 channelRegistered 状态 /** * The {@link Channel} of the {@link ChannelHandlerContext} was registered with its {@link EventLoop} */void channelRegistered(ChannelHandlerContext ctx) throws Exception;从注释里面可以看到是在 Channel 绑定到 Eventloop 上面的时候调用的. ...

May 3, 2019 · 4 min · jiezi

八问WebSocket协议为你快速解答WebSocket热门疑问

一、引言WebSocket是一种比较新的协议,它是伴随着html5规范而生的,虽然还比较年轻,但大多主流浏览器都已经支持。它使用方面、应用广泛,已经渗透到前后端开发的各种场景中。 对http一问一答中二式流程(就是从所周之的“长轮询”技要啦)的不满,催生了支持双向通信的WebSocket诞生。WebSocket是个不太干净协议。 本文将从8个常见的疑问入手,为还不了解WebSocket协议的开发者快速普及相关知识,从而节省您学习WebSocket的时间。 另外,如果您对Web端的即时通讯技术还完全不了解,那么《新手入门贴:详解Web端即时通讯技术的原理》、《Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE》这两篇文章请您务必抽时间读一读。 学习交流: 即时通讯/推送技术开发交流4群:101279154[推荐]移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》(本文同步发布于:http://www.52im.net/thread-24...) 二、参考文章《WebSocket详解(一):初步认识WebSocket技术》 《WebSocket详解(二):技术原理、代码演示和应用案例》 《WebSocket详解(三):深入WebSocket通信协议细节》 《WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)》 《WebSocket详解(五):刨根问底HTTP与WebSocket的关系(下篇)》 《WebSocket详解(六):刨根问底WebSocket与Socket的关系》 《理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性》 三、更多资料Web端即时通讯新手入门贴: 《新手入门贴:详解Web端即时通讯技术的原理》 Web端即时通讯技术盘点请参见: 《Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE》 关于Ajax短轮询: 找这方面的资料没什么意义,除非忽悠客户,否则请考虑其它3种方案即可。 有关Comet技术的详细介绍请参见: 《Comet技术详解:基于HTTP长连接的Web端实时通信技术》 《WEB端即时通讯:HTTP长连接、长轮询(long polling)详解》 《WEB端即时通讯:不用WebSocket也一样能搞定消息的即时性》 《开源Comet服务器iComet:支持百万并发的Web端即时通讯方案》 更多WebSocket的详细介绍请参见: 《新手快速入门:WebSocket简明教程》 《Socket.IO介绍:支持WebSocket、用于WEB端的即时通讯的框架》 《socket.io和websocket 之间是什么关系?有什么区别?》 有关SSE的详细介绍文章请参见: 《SSE技术详解:一种全新的HTML5服务器推送事件技术》 更多WEB端即时通讯文章请见: http://www.52im.net/forum.php... 四、1问WebSocket:WebSocket协议只能浏览器发起么?不是。目前此协议的受众的也不仅仅是web开发者。 WebSocket只是一种协议,它和http协议一样,使用类似okhttp的组件,可以在任何地方进行调用,甚至可以借助WebSocket实现RPC框架。 五、2问WebSocket:WebSocket和HTTP什么关系?WebSocket和http一样,都是处于OSI模型中的最高层:应用层。 WebSocket借助http协议进行握手,握手成功后,就会变身为TCP通道,从此与http不再相见。 使用netstat或者ss,能够看到对应的连接,它与处于抽象层的socket,在外观上没有区别。 更多WebSocket和HTTP的关系,以及与Socket的区别,可进一步阅读以下文章: 《WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)》 《WebSocket详解(五):刨根问底HTTP与WebSocket的关系(下篇)》 《WebSocket详解(六):刨根问底WebSocket与Socket的关系》 六、3问WebSocket:WebSocket和长轮询有什么区别?长轮询,就是客户端发送一个请求,服务端将一直在这个连接上等待(当然有一个超长的超时时间),直到有数据才返回,它依然是一个一问一答的模式。比如著名的comted。 WebSocket在握手成功后,就是全双工的TCP通道,数据可以主动从服务端发送到客户端,处于链接两端的应用没有任何区别。 WebSocket创建的连接和Http的长连接是不一样的。由于Http长连接底层依然是Http协议,所以它还是一问一答,只是Hold住了一条命长点的连接而已。 长轮询和Http长连接是阻塞的I/O,但WebSocket可以是非阻塞的(具体是多路复用)。 这方面更深入的资料,请进一步学习: 《Comet技术详解:基于HTTP长连接的Web端实时通信技术》 《WEB端即时通讯:HTTP长连接、长轮询(long polling)详解》 七、4问WebSocket:如何创建一个WebSocket连接?WebSocket的连接创建是借助Http协议进行的。这样设计主要是考虑兼容性,在浏览器中就可以很方便的发起请求,看起来比较具有迷惑性。 下图是一个典型的由浏览器发起的ws请求,可以看到和http请求长的是非常相似的。 但是,它只是请求阶段长得像而已: 请求的地址,一般是:ws://***,或者是使用了SSL/TLS加密的安全协议wss:,用来标识是WebSocket请求。 1)首先,通过Http头里面的Upgrade域,请求进行协议转换。如果服务端支持的话,就可以切换到WebSocket协议。简单点讲:连接已经在那了,通过握手切换成ws协议,就是切换了连接的一个状态而已。 2)Connection域可以认为是与Upgrade域配对的头信息。像nginx等代理服务器,是要先处理Connection,然后再发起协议转换的。 3)Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。如此操作,可以尽量避免普通 HTTP 请求被误认为 WebSocket 协议。 其他的,像Sec-WebSocket*字样的头信息,表明了客户端支持的子协议以及其他信息。像loT中很流行的mqtt,就可以作为WebSocket的子协议。 ...

April 25, 2019 · 1 min · jiezi

Netty-ByteBuf

Java NIO 提供了 ByteBuffer 作为它的字节容器, 但是这个类使用起来过于复杂, 而且也有些繁琐. Netty 的 ByteBuffer 的代替品是 ByteBuf. ByteBuf 的 API Netty 的数据处理 API 通过两个组件暴露 public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf>public interface ByteBufHolder extends ReferenceCounted下面是 ByteBuf API 的优点: 它可以被用户自定义的缓冲区类扩展;通过内置的复合缓冲区类型实现了透明的零拷贝;容量可以按需增长;在读和写这两种模式之间雀环不需要调用 ByteBuffer 的 flip() 方法;读和写试用了不同的索引;支持方法的链式调用;支持引用计数;支持池化.它是如何工作的ByteBuf 维护了两个不同的索引: 一个用于读取, 一个用于写入. 当你从 ByteBuf 读取时, 它的 readerIndex 将会被递增到读取的字节数. 同样的, 当你写入 ByteBuf 时, 它的 writerIndex 也会被递增. 要了解这些索引两两之间的关系, 请考虑一下, 如果打算读取字节直到 readerIndex 达到和 writerIndex 同样的值时会发生什么. 在那时, 你将会到达 "可以读取的" 数据的末尾. 就如同试图读取超出数组末尾的数据一样, 会触发一个 IndexOutOfBoundsException. ...

April 23, 2019 · 5 min · jiezi

netty学习总结(一)

netty学习总结(一)netty是什么?netty是一个异步的,事件驱动的网络编程框架。netty的技术基础netty是对Java NIO和Java线程池技术的封装netty解决了什么问题使用Java IO进行网络编程,一般一个用户一个线程,无法处理海量用户使用Java NIO进行网络编程,编程复杂性太高,如果没有深厚的NIO网络编程基础,写出的程序可能还不如Java IO写的程序至于Java AIO,目前还没有弄清楚其与netty孰优孰劣netty架构netty架构是基于Reactor和责任链模式进行设计的。reactor关于reactor的原理,参考“【NIO系列】——之Reactor模型”netty的reactor是多reactor多线程模型,其中reactor在netty中以eventloop的形式出现。责任链模式netty通过popeline将handler组装起来,通过向pipeline里添加handler来监听处理发生的事件。netty服务端编程模式// 用于监听客户端链接的eventloop池,一般只有一个eventloopNioEventLoopGroup bossGroup = new NioEventLoopGroup();// 用于处理客户端IO的eventloop池NioEventLoopGroup workGroup = new NioEventLoopGroup();// 辅助类,帮助初始化服务器ServerBootStrap bootstrap = new ServerBootStrap();bootstrap.group(bossGroup, workGroup) // bossGroup和workGroup可以是同一个 .channel(NioServerSocketChannel.class) // 设置服务端监听套接字的channel类型 .option(ChannelOption.SO_BACKLOG, 1024) // 设置监听套接字的参数 .handler(new LoggingHandler()) // 设置监听套接字的handler .childHandler(new ChannelInitializer<SocketChannel>(){ // 设置客户端套接字的handler public void initChannle(SocketChannel ch){ // 向pipleline中添加handler,用于处理客户端IO ch.pipeline().addLast(…); } });int port = 8080;try{ ChannelFuture f = bootstrap.bind(port).sync(); f.channel().closeFuture().sync();}catch(IOException e){ e.printStacktrac();}finally{ bossGroup.shutdownGracefully(); workGroup.shutdownGracefully();}netty客户端编程模型// 用于处理与服务端IO的eventloop池NioEventLoopGroup group = new NioEventLoopGroup();// 辅助类,帮助初始化客户端BootStrap bootstrap = new BootStrap();bootstrap.group(group) .channel(NioSocketChannel.class) // 设置客户端套接字的channel类型 .option(ChannelOption.NO_DELAY, true) // 设置客户端套接字的参数 .handler(new ChannelInitializer<SocketChannel>(){ // 设置客户端套接字的handler public void initChannel(SocketChannel ch){ // 向pipleline中添加handler,用于处理客户端IO ch.pipeline().addLast(…); } });String host = “127.0.0.1”;int port = 8080;try{ ChannelFuture f = bootstrap.connect(host, port).sync(); f.channel().closeFuture().sync();}catch(IOException e){ e.printStacktrac();}finally{ group.shutdownGracefully();}buffernetty认为Java NIO的Buffer太难用了,因此自己实现了一套Buffer。相比于Java NIO的netty的buffer不仅易用,而且还支持自动扩容。netty的buffer可以抽象为三个指针readIndex, writeIndex, limit.读buffer增加readIndex,写buffer会增加writeIndex,如果写的数据量超过limit,则会增加buffer容量。netty buffer也支持随机读写netty中buffer一般通过Unpooled工具类创建,有三大类buffer:在JVM堆上分配的buffer。优点是分配快速,易于回收,缺点是最终还是要将数据复制到直接缓存中直接缓冲区,直接通过系统调用malloc分配的内存,优点是减少数据移动,缺点是分配慢,回收麻烦组合缓冲区,即将不同种类的buffer进行封装,访问时就像访问一个buffer,可以通过这个方式对缓冲区进行划分,但是会增加访问时间handlerhandler分为处理入站事件的handler和出站事件的handler。通过实现相应的方法来监听相应的事件netty也提供了一些adapter类来减少开发者的工作量。入站handler入站事件一般是由外部触发的,如收到数据。基类为ChannelInboundHandler出栈handler出站事件由内部触发,如写数据基类为ChannelOutboundHandler ...

April 14, 2019 · 1 min · jiezi

零拷贝总结(zero copy)

零拷贝总结(zero copy)前言在学习netty的过程中,发现Netty对缓存做了很多优化,其中零拷贝一直令我迷惑,所以在网上找了一些博客进行学习,并作此总结定义零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。[1]应用场景[2]在web应用中,经常需要传输一些静态文件(css, js, 图片等),可讲这一过程抽象为下面两个系统调度:read(file, tmp_buf, len); write(socket, tmp_buf, len);系统调用会进行上下文切换,共两次上下文切换:用户态 -> 内核态 -> 用户态上面的传输过程中,共涉及4次上下文切换,4次数据复制,其流程如下图上图中分为两部分,上半部分表示执行上下文(PC,寄存器等),下半部分表示数据区(堆栈等)上班部分描绘了这一过程的上下文切换,下半部分描述了数据移动。在第一个区间里(处于用户态),执行read系统调度,读取文件数据到tmp_buf中,因此会进行上下文切换,切换到内核态(第二区间),然后内核从磁盘中读取文件数据到内核的缓冲区中(红线所示,这种复制是利用DMA直接从磁盘复制到内存,不经过CPU的寄存器,所以称为DMA copy),接下来在将文件数据从内核缓冲区中复制到用户缓冲区tmp_buf(蓝色虚线所示,这种复制是先从内存到寄存器,然后寄存器到内核缓冲区,所以称为CPU copy),并切换回用户态(第三个区间),此时read调用就完成了,文件数据存在tmp_buf中。接下来进行write系统调用(处于第三个区间,用户态),讲tmp_buf的数据写入socket中,首先进行上下文切换,进入内核态(第四个区间),然后讲tmp_buf的数据复制到socket缓冲区中(蓝色虚线所示),最后利用DMA从socket缓冲区将数据复制到网卡,然后切换回用户态(第五个区间),此时wirte执行完成,但是数据可能还没有发送完成。当需要进行大量的上述操作时,上下文切换和内存复制就会严重的影响性能,为此Linux内核提供了sendFile来解决这个文件。为什么read调用,需要先将数据从磁盘复制到内核缓冲区,然后再从kernel buffer复制到user buffer呢?首先kernel buffer和user buffer大小不一定相等。kernel buffer的作用是预读,每次调用read时不一定会触发磁盘读,只用当kernel buffer中的数据都被读取完后,才会触发,然后从磁盘中多读取一些数据到kernel buffer(从磁盘的读取数据的大小,并不完全取决于用户执行的大小,还取决于kernel buffer的大小,以及文件的大小)。这样当用户多次调用read读取同一文件的数据时,就不需要每次都直接从硬盘中读取,因为每次会多读一些数据到kernel buffer中,这样read就可以直接从kernel buffer中读取数据,然后复制到user buffer,这种做法在每次读取数据量小于kernel buffer的大小时,可以显著的提高效率,但是当每次读取数据量超过kernel buffer大小时,就会显得低效了。为什么write调用,需要先将数据从user buffer复制到kernel buffer,然后在从kernel buffer复制到网卡呢?这样做的原因是为了快速返回,从user buffer复制到kernel buffer的速度应该是快于从kernel buffer到网卡的,如果直接冲user buffer复制到网卡,会影响程序的性能。在将数据从user buffer复制到kernel buffer后,write就可以返回了,剩下的就是利用DMA从kernel buffer读取数据到网卡。我认为,这是阻塞io的做法,对于一些异步io,是可以直接将user buffer的数据复制到网卡的(存疑,待以后解决)。sendfileLinux2.1在Linux2.1中,sendfile的执行过程如下图:这里只有一次系统调用,所以只有两次上下文切换:用户态 -> 内核态 -> 用户态数据复制有3次:磁盘 -> 内核缓冲区 -> socket缓冲区 -> 网卡上述的复制种,从内核缓冲区到socket缓冲区显得比较多余,因此在Linux2.4对其进行了优化Linux2.4在Linux2.4种,sendfile的执行过程如下图:关键的改动在于,将kernel buffer的信息添加到socket buffer,然后协议引擎利用这个信息,直接从kernel buffer读取数据。但是这种操作,需要网卡支持gather操作。问题零拷贝,虽然对提升程序性能有很大的帮助,但是对传输的文件的安全性没有保证,相当于明文传输。由于文件直接从磁盘读取到网络,所以无法对文件进行加密。因此对于需要对传输的数据进行加密的程序,如HTTPS,并不适用。参考文献[1] 零复制[2] 什么是Zero-Copy?[3] Zero-Copy&sendfile浅析[4] Efficient data transfer through zero copy[5] Zero Copy I: User-Mode Perspective

April 14, 2019 · 1 min · jiezi

聊聊reactor-netty的AccessLogHandlerH2

序本文主要研究一下reactor-netty的AccessLogHandlerH2AccessLogHandlerH2reactor-netty-0.8.5.RELEASE-sources.jar!/reactor/netty/http/server/AccessLogHandlerH2.javafinal class AccessLogHandlerH2 extends ChannelDuplexHandler { static final String H2_PROTOCOL_NAME = “HTTP/2.0”; AccessLog accessLog = new AccessLog(); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof Http2HeadersFrame){ final Http2HeadersFrame requestHeaders = (Http2HeadersFrame) msg; final SocketChannel channel = (SocketChannel) ctx.channel() .parent(); final Http2Headers headers = requestHeaders.headers(); accessLog = new AccessLog() .address(channel.remoteAddress().getHostString()) .port(channel.localAddress().getPort()) .method(headers.method()) .uri(headers.path()) .protocol(H2_PROTOCOL_NAME); } super.channelRead(ctx, msg); } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { boolean lastContent = false; if (msg instanceof Http2HeadersFrame) { final Http2HeadersFrame responseHeaders = (Http2HeadersFrame) msg; final Http2Headers headers = responseHeaders.headers(); lastContent = responseHeaders.isEndStream(); accessLog.status(headers.status()) .chunked(true); } if (msg instanceof Http2DataFrame) { final Http2DataFrame data = (Http2DataFrame) msg; lastContent = data.isEndStream(); accessLog.increaseContentLength(data.content().readableBytes()); } if (lastContent) { ctx.write(msg, promise) .addListener(future -> { if (future.isSuccess()) { accessLog.log(); } }); return; } ctx.write(msg, promise); }}AccessLogHandlerH2是HTTP2的access log的实现,具体针对Http2HeadersFrame及Http2DataFrame进行了判断HttpServerBindreactor-netty-0.8.5.RELEASE-sources.jar!/reactor/netty/http/server/HttpServerBind.javafinal class HttpServerBind extends HttpServer implements Function<ServerBootstrap, ServerBootstrap> { //…… static void addStreamHandlers(Channel ch, ConnectionObserver listener, boolean readForwardHeaders, ServerCookieEncoder encoder, ServerCookieDecoder decoder) { if (ACCESS_LOG) { ch.pipeline() .addLast(NettyPipeline.AccessLogHandler, new AccessLogHandlerH2()); } ch.pipeline() .addLast(new Http2StreamBridgeHandler(listener, readForwardHeaders, encoder, decoder)) .addLast(new Http2StreamFrameToHttpObjectCodec(true)); ChannelOperations.addReactiveBridge(ch, ChannelOperations.OnSetup.empty(), listener); if (log.isDebugEnabled()) { log.debug(format(ch, “Initialized HTTP/2 pipeline {}”), ch.pipeline()); } } //……HttpServerBind的addStreamHandlers静态方法用于判断是否开启access log,开启的话会创建AccessLogHandlerH2并添加到pipeline;Http1OrH2CleartextCodec的initChannel方法以及Http2StreamInitializer的initChannel方法均调用到了此方法小结AccessLogHandlerH2是HTTP2的access log的实现,具体针对Http2HeadersFrame及Http2DataFrame进行了判断HttpServerBind的addStreamHandlers静态方法用于判断是否开启access log,开启的话会创建AccessLogHandlerH2并添加到pipelineHttp1OrH2CleartextCodec的initChannel方法以及Http2StreamInitializer的initChannel方法均调用到了此方法docSpring Boot Reactor Netty Configuration ...

April 6, 2019 · 1 min · jiezi

聊聊reactor-netty的AccessLog

序本文主要研究一下reactor-netty的AccessLog开启access log对于使用tomcat的spring boot应用,可以server.tomcat.accesslog.enabled=true来开启对于使用jetty的spring boot应用,可以server.jetty.accesslog.enabled=true来开启对于使用undertow的spring boot应用,可以server.undertow.accesslog.enabled=true来开启对于使用webflux的应用,没有这么对应的配置,但是可以通过-Dreactor.netty.http.server.accessLogEnabled=true来开启ReactorNettyreactor-netty-0.8.5.RELEASE-sources.jar!/reactor/netty/ReactorNetty.java/** * Internal helpers for reactor-netty contracts * * @author Stephane Maldini /public final class ReactorNetty { //…… // System properties names /* * Default worker thread count, fallback to available processor * (but with a minimum value of 4) / public static final String IO_WORKER_COUNT = “reactor.netty.ioWorkerCount”; /* * Default selector thread count, fallback to -1 (no selector thread) / public static final String IO_SELECT_COUNT = “reactor.netty.ioSelectCount”; /* * Default worker thread count for UDP, fallback to available processor * (but with a minimum value of 4) / public static final String UDP_IO_THREAD_COUNT = “reactor.netty.udp.ioThreadCount”; /* * Default value whether the native transport (epoll, kqueue) will be preferred, * fallback it will be preferred when available / public static final String NATIVE = “reactor.netty.native”; /* * Default max connections, if -1 will never wait to acquire before opening a new * connection in an unbounded fashion. Fallback to * available number of processors (but with a minimum value of 16) / public static final String POOL_MAX_CONNECTIONS = “reactor.netty.pool.maxConnections”; /* * Default acquisition timeout (milliseconds) before error. If -1 will never wait to * acquire before opening a new * connection in an unbounded fashion. Fallback 45 seconds / public static final String POOL_ACQUIRE_TIMEOUT = “reactor.netty.pool.acquireTimeout”; /* * Default SSL handshake timeout (milliseconds), fallback to 10 seconds / public static final String SSL_HANDSHAKE_TIMEOUT = “reactor.netty.tcp.sslHandshakeTimeout”; /* * Default value whether the SSL debugging on the client side will be enabled/disabled, * fallback to SSL debugging disabled / public static final String SSL_CLIENT_DEBUG = “reactor.netty.tcp.ssl.client.debug”; /* * Default value whether the SSL debugging on the server side will be enabled/disabled, * fallback to SSL debugging disabled / public static final String SSL_SERVER_DEBUG = “reactor.netty.tcp.ssl.server.debug”; /* * Specifies whether the Http Server access log will be enabled. * By default it is disabled. */ public static final String ACCESS_LOG_ENABLED = “reactor.netty.http.server.accessLogEnabled”; //……}ReactorNetty定义了ACCESS_LOG_ENABLED常量,其值为reactor.netty.http.server.accessLogEnabledHttpServerBindreactor-netty-0.8.5.RELEASE-sources.jar!/reactor/netty/http/server/HttpServerBind.javafinal class HttpServerBind extends HttpServer implements Function<ServerBootstrap, ServerBootstrap> { static final HttpServerBind INSTANCE = new HttpServerBind(); static final Function<DisposableServer, DisposableServer> CLEANUP_GLOBAL_RESOURCE = DisposableBind::new; static final boolean ACCESS_LOG = Boolean.parseBoolean(System.getProperty(ACCESS_LOG_ENABLED, “false”)); //…… static final class Http1Initializer implements BiConsumer<ConnectionObserver, Channel> { final int line; final int header; final int chunk; final boolean validate; final int buffer; final int minCompressionSize; final BiPredicate<HttpServerRequest, HttpServerResponse> compressPredicate; final boolean forwarded; final ServerCookieEncoder cookieEncoder; final ServerCookieDecoder cookieDecoder; Http1Initializer(int line, int header, int chunk, boolean validate, int buffer, int minCompressionSize, @Nullable BiPredicate<HttpServerRequest, HttpServerResponse> compressPredicate, boolean forwarded, ServerCookieEncoder encoder, ServerCookieDecoder decoder) { this.line = line; this.header = header; this.chunk = chunk; this.validate = validate; this.buffer = buffer; this.minCompressionSize = minCompressionSize; this.compressPredicate = compressPredicate; this.forwarded = forwarded; this.cookieEncoder = encoder; this.cookieDecoder = decoder; } @Override public void accept(ConnectionObserver listener, Channel channel) { ChannelPipeline p = channel.pipeline(); p.addLast(NettyPipeline.HttpCodec, new HttpServerCodec(line, header, chunk, validate, buffer)); if (ACCESS_LOG) { p.addLast(NettyPipeline.AccessLogHandler, new AccessLogHandler()); } boolean alwaysCompress = compressPredicate == null && minCompressionSize == 0; if (alwaysCompress) { p.addLast(NettyPipeline.CompressionHandler, new SimpleCompressionHandler()); } p.addLast(NettyPipeline.HttpTrafficHandler, new HttpTrafficHandler(listener, forwarded, compressPredicate, cookieEncoder, cookieDecoder)); } } //……} HttpServerBind有个ACCESS_LOG属性,它读取ReactorNetty的ACCESS_LOG_ENABLED(reactor.netty.http.server.accessLogEnabled)的属性,读取不到默认为false;HttpServerBind有个Http1Initializer类,它的accept方法会判断ACCESS_LOG是否为true,如果为true则会往Channel的pipeline添加名为accessLogHandler(NettyPipeline.AccessLogHandler)的AccessLogHandlerAccessLogHandlerreactor-netty-0.8.5.RELEASE-sources.jar!/reactor/netty/http/server/AccessLogHandler.javafinal class AccessLogHandler extends ChannelDuplexHandler { AccessLog accessLog = new AccessLog(); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpRequest) { final HttpRequest request = (HttpRequest) msg; final SocketChannel channel = (SocketChannel) ctx.channel(); accessLog = new AccessLog() .address(channel.remoteAddress().getHostString()) .port(channel.localAddress().getPort()) .method(request.method().name()) .uri(request.uri()) .protocol(request.protocolVersion().text()); } super.channelRead(ctx, msg); } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { if (msg instanceof HttpResponse) { final HttpResponse response = (HttpResponse) msg; final HttpResponseStatus status = response.status(); if (status.equals(HttpResponseStatus.CONTINUE)) { ctx.write(msg, promise); return; } final boolean chunked = HttpUtil.isTransferEncodingChunked(response); accessLog.status(status.codeAsText()) .chunked(chunked); if (!chunked) { accessLog.contentLength(HttpUtil.getContentLength(response, -1)); } } if (msg instanceof LastHttpContent) { accessLog.increaseContentLength(((LastHttpContent) msg).content().readableBytes()); ctx.write(msg, promise) .addListener(future -> { if (future.isSuccess()) { accessLog.log(); } }); return; } if (msg instanceof ByteBuf) { accessLog.increaseContentLength(((ByteBuf) msg).readableBytes()); } if (msg instanceof ByteBufHolder) { accessLog.increaseContentLength(((ByteBufHolder) msg).content().readableBytes()); } ctx.write(msg, promise); }}AccessLogHandler继承了ChannelDuplexHandler;在channelRead的时候创建了AccessLog对象,在write的时候更新AccessLog对象;当msg为LastHttpContent时,则添加了一个listener,在成功回调时执行accessLog.log()AccessLogreactor-netty-0.8.5.RELEASE-sources.jar!/reactor/netty/http/server/AccessLog.javafinal class AccessLog { static final Logger log = Loggers.getLogger(“reactor.netty.http.server.AccessLog”); static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(“dd/MMM/yyyy:HH:mm:ss Z”, Locale.US); static final String COMMON_LOG_FORMAT = “{} - {} [{}] "{} {} {}" {} {} {} {} ms”; static final String MISSING = “-”; final String zonedDateTime; String address; CharSequence method; CharSequence uri; String protocol; String user = MISSING; CharSequence status; long contentLength; boolean chunked; long startTime = System.currentTimeMillis(); int port; AccessLog() { this.zonedDateTime = ZonedDateTime.now().format(DATE_TIME_FORMATTER); } AccessLog address(String address) { this.address = Objects.requireNonNull(address, “address”); return this; } AccessLog port(int port) { this.port = port; return this; } AccessLog method(CharSequence method) { this.method = Objects.requireNonNull(method, “method”); return this; } AccessLog uri(CharSequence uri) { this.uri = Objects.requireNonNull(uri, “uri”); return this; } AccessLog protocol(String protocol) { this.protocol = Objects.requireNonNull(protocol, “protocol”); return this; } AccessLog status(CharSequence status) { this.status = Objects.requireNonNull(status, “status”); return this; } AccessLog contentLength(long contentLength) { this.contentLength = contentLength; return this; } AccessLog increaseContentLength(long contentLength) { if (chunked) { this.contentLength += contentLength; } return this; } AccessLog chunked(boolean chunked) { this.chunked = chunked; return this; } long duration() { return System.currentTimeMillis() - startTime; } void log() { if (log.isInfoEnabled()) { log.info(COMMON_LOG_FORMAT, address, user, zonedDateTime, method, uri, protocol, status, (contentLength > -1 ? contentLength : MISSING), port, duration()); } }}AccessLog的log方法直接通过logger输出日志,其日志格式为COMMON_LOG_FORMAT({} - {} [{}] “{} {} {}” {} {} {} {} ms),分别是address, user, zonedDateTime, method, uri, protocol, status, contentLength, port, duration小结对于使用webflux的应用,可以通过-Dreactor.netty.http.server.accessLogEnabled=true来开启access logHttpServerBind有个ACCESS_LOG属性,它读取ReactorNetty的ACCESS_LOG_ENABLED(reactor.netty.http.server.accessLogEnabled)的属性,读取不到默认为false;HttpServerBind有个Http1Initializer类,它的accept方法会判断ACCESS_LOG是否为true,如果为true则会往Channel的pipeline添加名为accessLogHandler(NettyPipeline.AccessLogHandler)的AccessLogHandlerAccessLogHandler继承了ChannelDuplexHandler;在channelRead的时候创建了AccessLog对象,在write的时候更新AccessLog对象;当msg为LastHttpContent时,则添加了一个listener,在成功回调时执行accessLog.log();AccessLog的log方法直接通过logger输出日志,其日志格式为COMMON_LOG_FORMAT({} - {} [{}] “{} {} {}” {} {} {} {} ms),分别是address, user, zonedDateTime, method, uri, protocol, status, contentLength, port, durationdocSpring Boot Reactor Netty Configuration ...

April 5, 2019 · 5 min · jiezi

聊聊netty的ResourceLeakDetector

序本文主要研究一下netty的ResourceLeakDetectorLEAK异常2019-04-02 15:23:17.026 ERROR 1 — [reactor-http-epoll-2] io.netty.util.ResourceLeakDetector : LEAK: ByteBuf.release() was not called before it’s garbage-collected. See http://netty.io/wiki/reference-counted-objects.html for more information.Recent access records: #1: io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:286) io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:253) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1408) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930) io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:799) io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:427) io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:328) io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905) java.base/java.lang.Thread.run(Thread.java:835)#2: io.netty.buffer.AdvancedLeakAwareByteBuf.forEachByte(AdvancedLeakAwareByteBuf.java:670) io.netty.handler.codec.http.HttpObjectDecoder$HeaderParser.parse(HttpObjectDecoder.java:801) io.netty.handler.codec.http.HttpObjectDecoder.readHeaders(HttpObjectDecoder.java:601) io.netty.handler.codec.http.HttpObjectDecoder.decode(HttpObjectDecoder.java:227) io.netty.handler.codec.http.HttpClientCodec$Decoder.decode(HttpClientCodec.java:202) io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502) io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:441) io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278) io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:253) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1408) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930) io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:799) io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:427) io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:328) io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905) java.base/java.lang.Thread.run(Thread.java:835)#3: io.netty.buffer.AdvancedLeakAwareByteBuf.forEachByte(AdvancedLeakAwareByteBuf.java:670) io.netty.handler.codec.http.HttpObjectDecoder$HeaderParser.parse(HttpObjectDecoder.java:801) io.netty.handler.codec.http.HttpObjectDecoder.readHeaders(HttpObjectDecoder.java:581) io.netty.handler.codec.http.HttpObjectDecoder.decode(HttpObjectDecoder.java:227) io.netty.handler.codec.http.HttpClientCodec$Decoder.decode(HttpClientCodec.java:202) io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502) io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:441) io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278) io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:253) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1408) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930) io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:799) io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:427) io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:328) io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905) java.base/java.lang.Thread.run(Thread.java:835)#4: io.netty.buffer.AdvancedLeakAwareByteBuf.forEachByte(AdvancedLeakAwareByteBuf.java:670) io.netty.handler.codec.http.HttpObjectDecoder$HeaderParser.parse(HttpObjectDecoder.java:801) io.netty.handler.codec.http.HttpObjectDecoder$LineParser.parse(HttpObjectDecoder.java:850) io.netty.handler.codec.http.HttpObjectDecoder.decode(HttpObjectDecoder.java:208) io.netty.handler.codec.http.HttpClientCodec$Decoder.decode(HttpClientCodec.java:202) io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502) io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:441) io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278) io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:253) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1408) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930) io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:799) io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:427) io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:328) io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905) java.base/java.lang.Thread.run(Thread.java:835)#5: io.netty.buffer.AdvancedLeakAwareByteBuf.getUnsignedByte(AdvancedLeakAwareByteBuf.java:160) io.netty.handler.codec.http.HttpObjectDecoder.skipControlCharacters(HttpObjectDecoder.java:566) io.netty.handler.codec.http.HttpObjectDecoder.decode(HttpObjectDecoder.java:202) io.netty.handler.codec.http.HttpClientCodec$Decoder.decode(HttpClientCodec.java:202) io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502) io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:441) io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278) io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:253) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1408) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930) io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:799) io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:427) io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:328) io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905) java.base/java.lang.Thread.run(Thread.java:835)#6: Hint: ‘reactor.left.httpCodec’ will handle the message from this point. io.netty.channel.DefaultChannelPipeline.touch(DefaultChannelPipeline.java:116) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:345) io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1408) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930) io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:799) io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:427) io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:328) io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905) java.base/java.lang.Thread.run(Thread.java:835)#7: Hint: ‘DefaultChannelPipeline$HeadContext#0’ will handle the message from this point. io.netty.channel.DefaultChannelPipeline.touch(DefaultChannelPipeline.java:116) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:345) io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930) io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:799) io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:427) io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:328) io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905) java.base/java.lang.Thread.run(Thread.java:835)Created at: io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:339) io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:185) io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:176) io.netty.channel.unix.PreferredDirectByteBufAllocator.ioBuffer(PreferredDirectByteBufAllocator.java:53) io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:114) io.netty.channel.epoll.EpollRecvByteAllocatorHandle.allocate(EpollRecvByteAllocatorHandle.java:77) io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:784) io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:427) io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:328) io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905) java.base/java.lang.Thread.run(Thread.java:835): 9 leak records were discarded because the leak record count is targeted to 4. Use system property io.netty.leakDetection.targetRecords to increase the limit.ResourceLeakDetectornetty-common-4.1.33.Final-sources.jar!/io/netty/util/ResourceLeakDetector.javapublic class ResourceLeakDetector<T> { private static final String PROP_LEVEL_OLD = “io.netty.leakDetectionLevel”; private static final String PROP_LEVEL = “io.netty.leakDetection.level”; private static final Level DEFAULT_LEVEL = Level.SIMPLE; private static final String PROP_TARGET_RECORDS = “io.netty.leakDetection.targetRecords”; private static final int DEFAULT_TARGET_RECORDS = 4; private static final String PROP_SAMPLING_INTERVAL = “io.netty.leakDetection.samplingInterval”; // There is a minor performance benefit in TLR if this is a power of 2. private static final int DEFAULT_SAMPLING_INTERVAL = 128; private static final int TARGET_RECORDS; static final int SAMPLING_INTERVAL; /** * Represents the level of resource leak detection. / public enum Level { /* * Disables resource leak detection. / DISABLED, /* * Enables simplistic sampling resource leak detection which reports there is a leak or not, * at the cost of small overhead (default). / SIMPLE, /* * Enables advanced sampling resource leak detection which reports where the leaked object was accessed * recently at the cost of high overhead. / ADVANCED, /* * Enables paranoid resource leak detection which reports where the leaked object was accessed recently, * at the cost of the highest possible overhead (for testing purposes only). / PARANOID; /* * Returns level based on string value. Accepts also string that represents ordinal number of enum. * * @param levelStr - level string : DISABLED, SIMPLE, ADVANCED, PARANOID. Ignores case. * @return corresponding level or SIMPLE level in case of no match. / static Level parseLevel(String levelStr) { String trimmedLevelStr = levelStr.trim(); for (Level l : values()) { if (trimmedLevelStr.equalsIgnoreCase(l.name()) || trimmedLevelStr.equals(String.valueOf(l.ordinal()))) { return l; } } return DEFAULT_LEVEL; } } private static Level level; private static final InternalLogger logger = InternalLoggerFactory.getInstance(ResourceLeakDetector.class); static { final boolean disabled; if (SystemPropertyUtil.get(“io.netty.noResourceLeakDetection”) != null) { disabled = SystemPropertyUtil.getBoolean(“io.netty.noResourceLeakDetection”, false); logger.debug("-Dio.netty.noResourceLeakDetection: {}", disabled); logger.warn( “-Dio.netty.noResourceLeakDetection is deprecated. Use ‘-D{}={}’ instead.”, PROP_LEVEL, DEFAULT_LEVEL.name().toLowerCase()); } else { disabled = false; } Level defaultLevel = disabled? Level.DISABLED : DEFAULT_LEVEL; // First read old property name String levelStr = SystemPropertyUtil.get(PROP_LEVEL_OLD, defaultLevel.name()); // If new property name is present, use it levelStr = SystemPropertyUtil.get(PROP_LEVEL, levelStr); Level level = Level.parseLevel(levelStr); TARGET_RECORDS = SystemPropertyUtil.getInt(PROP_TARGET_RECORDS, DEFAULT_TARGET_RECORDS); SAMPLING_INTERVAL = SystemPropertyUtil.getInt(PROP_SAMPLING_INTERVAL, DEFAULT_SAMPLING_INTERVAL); ResourceLeakDetector.level = level; if (logger.isDebugEnabled()) { logger.debug("-D{}: {}", PROP_LEVEL, level.name().toLowerCase()); logger.debug("-D{}: {}", PROP_TARGET_RECORDS, TARGET_RECORDS); } } /* * @deprecated Use {@link #setLevel(Level)} instead. / @Deprecated public static void setEnabled(boolean enabled) { setLevel(enabled? Level.SIMPLE : Level.DISABLED); } /* * Returns {@code true} if resource leak detection is enabled. / public static boolean isEnabled() { return getLevel().ordinal() > Level.DISABLED.ordinal(); } /* * Sets the resource leak detection level. / public static void setLevel(Level level) { if (level == null) { throw new NullPointerException(“level”); } ResourceLeakDetector.level = level; } /* * Returns the current resource leak detection level. / public static Level getLevel() { return level; } /* the collection of active resources / private final Set<DefaultResourceLeak<?>> allLeaks = Collections.newSetFromMap(new ConcurrentHashMap<DefaultResourceLeak<?>, Boolean>()); private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>(); private final ConcurrentMap<String, Boolean> reportedLeaks = PlatformDependent.newConcurrentHashMap(); private final String resourceType; private final int samplingInterval; //…… /* * Creates a new {@link ResourceLeakTracker} which is expected to be closed via * {@link ResourceLeakTracker#close(Object)} when the related resource is deallocated. * * @return the {@link ResourceLeakTracker} or {@code null} / @SuppressWarnings(“unchecked”) public final ResourceLeakTracker<T> track(T obj) { return track0(obj); } private DefaultResourceLeak track0(T obj) { Level level = ResourceLeakDetector.level; if (level == Level.DISABLED) { return null; } if (level.ordinal() < Level.PARANOID.ordinal()) { if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) { reportLeak(); return new DefaultResourceLeak(obj, refQueue, allLeaks); } return null; } reportLeak(); return new DefaultResourceLeak(obj, refQueue, allLeaks); } private void reportLeak() { if (!logger.isErrorEnabled()) { clearRefQueue(); return; } // Detect and report previous leaks. for (;;) { @SuppressWarnings(“unchecked”) DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll(); if (ref == null) { break; } if (!ref.dispose()) { continue; } String records = ref.toString(); if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) { if (records.isEmpty()) { reportUntracedLeak(resourceType); } else { reportTracedLeak(resourceType, records); } } } } /* * This method is called when a traced leak is detected. It can be overridden for tracking how many times leaks * have been detected. / protected void reportTracedLeak(String resourceType, String records) { logger.error( “LEAK: {}.release() was not called before it’s garbage-collected. " + “See http://netty.io/wiki/reference-counted-objects.html for more information.{}”, resourceType, records); } /* * This method is called when an untraced leak is detected. It can be overridden for tracking how many times leaks * have been detected. / protected void reportUntracedLeak(String resourceType) { logger.error(“LEAK: {}.release() was not called before it’s garbage-collected. " + “Enable advanced leak reporting to find out where the leak occurred. " + “To enable advanced leak reporting, " + “specify the JVM option ‘-D{}={}’ or call {}.setLevel() " + “See http://netty.io/wiki/reference-counted-objects.html for more information.”, resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this)); } //……}ResourceLeakDetector使用Level枚举定义了四种不同的leak detection级别,分别是DISABLED、SIMPLE、ADVANCED、PARANOID;默认level为SIMPLE;可以使用-Dio.netty.leakDetection.level=advanced来进行设置ResourceLeakDetector的静态代码块会读取io.netty.noResourceLeakDetection系统属性,如果显示设置为false,则变更defaultLevel为DISABLED;如果没有设置,则默认disabled为false,defaultLevel为SIMPLE;ResourceLeakDetector还有TARGET_RECORDS(io.netty.leakDetection.targetRecords)及SAMPLING_INTERVAL(io.netty.leakDetection.samplingInterval)两个属性,其中targetRecords默认为4,samplingInterval默认为128ResourceLeakDetector提供了track方法用于创建ResourceLeakTracker;track方法内部调用track0方法,如果level为PARANOID则立即调用reportLeak,创建DefaultResourceLeak,否则利用随机数来判断(PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0)是否调用reportLeak并创建DefaultResourceLeak;reportLeak方法有个for循环,不断从refQueue取DefaultResourceLeak,然后调用reportUntracedLeak或者reportTracedLeak进行errorDefaultResourceLeaknetty-common-4.1.33.Final-sources.jar!/io/netty/util/ResourceLeakDetector.java private static final class DefaultResourceLeak<T> extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak { @SuppressWarnings(“unchecked”) // generics and updaters do not mix. private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, Record> headUpdater = (AtomicReferenceFieldUpdater) AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, Record.class, “head”); @SuppressWarnings(“unchecked”) // generics and updaters do not mix. private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater = (AtomicIntegerFieldUpdater) AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, “droppedRecords”); @SuppressWarnings(“unused”) private volatile Record head; @SuppressWarnings(“unused”) private volatile int droppedRecords; private final Set<DefaultResourceLeak<?>> allLeaks; private final int trackedHash; DefaultResourceLeak( Object referent, ReferenceQueue<Object> refQueue, Set<DefaultResourceLeak<?>> allLeaks) { super(referent, refQueue); assert referent != null; // Store the hash of the tracked object to later assert it in the close(…) method. // It’s important that we not store a reference to the referent as this would disallow it from // be collected via the WeakReference. trackedHash = System.identityHashCode(referent); allLeaks.add(this); // Create a new Record so we always have the creation stacktrace included. headUpdater.set(this, new Record(Record.BOTTOM)); this.allLeaks = allLeaks; } @Override public void record() { record0(null); } @Override public void record(Object hint) { record0(hint); } /* * This method works by exponentially backing off as more records are present in the stack. Each record has a * 1 / 2^n chance of dropping the top most record and replacing it with itself. This has a number of convenient * properties: * * <ol> * <li> The current record is always recorded. This is due to the compare and swap dropping the top most * record, rather than the to-be-pushed record. * <li> The very last access will always be recorded. This comes as a property of 1. * <li> It is possible to retain more records than the target, based upon the probability distribution. * <li> It is easy to keep a precise record of the number of elements in the stack, since each element has to * know how tall the stack is. * </ol> * * In this particular implementation, there are also some advantages. A thread local random is used to decide * if something should be recorded. This means that if there is a deterministic access pattern, it is now * possible to see what other accesses occur, rather than always dropping them. Second, after * {@link #TARGET_RECORDS} accesses, backoff occurs. This matches typical access patterns, * where there are either a high number of accesses (i.e. a cached buffer), or low (an ephemeral buffer), but * not many in between. * * The use of atomics avoids serializing a high number of accesses, when most of the records will be thrown * away. High contention only happens when there are very few existing records, which is only likely when the * object isn’t shared! If this is a problem, the loop can be aborted and the record dropped, because another * thread won the race. / private void record0(Object hint) { // Check TARGET_RECORDS > 0 here to avoid similar check before remove from and add to lastRecords if (TARGET_RECORDS > 0) { Record oldHead; Record prevHead; Record newHead; boolean dropped; do { if ((prevHead = oldHead = headUpdater.get(this)) == null) { // already closed. return; } final int numElements = oldHead.pos + 1; if (numElements >= TARGET_RECORDS) { final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30); if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) { prevHead = oldHead.next; } } else { dropped = false; } newHead = hint != null ? new Record(prevHead, hint) : new Record(prevHead); } while (!headUpdater.compareAndSet(this, oldHead, newHead)); if (dropped) { droppedRecordsUpdater.incrementAndGet(this); } } } boolean dispose() { clear(); return allLeaks.remove(this); } @Override public boolean close() { if (allLeaks.remove(this)) { // Call clear so the reference is not even enqueued. clear(); headUpdater.set(this, null); return true; } return false; } @Override public boolean close(T trackedObject) { // Ensure that the object that was tracked is the same as the one that was passed to close(…). assert trackedHash == System.identityHashCode(trackedObject); try { return close(); } finally { // This method will do synchronized(trackedObject) and we should be sure this will not cause deadlock. // It should not, because somewhere up the callstack should be a (successful) trackedObject.release, // therefore it is unreasonable that anyone else, anywhere, is holding a lock on the trackedObject. // (Unreasonable but possible, unfortunately.) reachabilityFence0(trackedObject); } } /* * Ensures that the object referenced by the given reference remains * <a href=“package-summary.html#reachability”><em>strongly reachable</em></a>, * regardless of any prior actions of the program that might otherwise cause * the object to become unreachable; thus, the referenced object is not * reclaimable by garbage collection at least until after the invocation of * this method. * * <p> Recent versions of the JDK have a nasty habit of prematurely deciding objects are unreachable. * see: https://stackoverflow.com/questions/26642153/finalize-called-on-strongly-reachable-object-in-java-8 * The Java 9 method Reference.reachabilityFence offers a solution to this problem. * * <p> This method is always implemented as a synchronization on {@code ref}, not as * {@code Reference.reachabilityFence} for consistency across platforms and to allow building on JDK 6-8. * <b>It is the caller’s responsibility to ensure that this synchronization will not cause deadlock.</b> * * @param ref the reference. If {@code null}, this method has no effect. * @see java.lang.ref.Reference#reachabilityFence / private static void reachabilityFence0(Object ref) { if (ref != null) { // Empty synchronized is ok: https://stackoverflow.com/a/31933260/1151521 synchronized (ref) { } } } @Override public String toString() { Record oldHead = headUpdater.getAndSet(this, null); if (oldHead == null) { // Already closed return EMPTY_STRING; } final int dropped = droppedRecordsUpdater.get(this); int duped = 0; int present = oldHead.pos + 1; // Guess about 2 kilobytes per stack trace StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE); buf.append(“Recent access records: “).append(NEWLINE); int i = 1; Set<String> seen = new HashSet<String>(present); for (; oldHead != Record.BOTTOM; oldHead = oldHead.next) { String s = oldHead.toString(); if (seen.add(s)) { if (oldHead.next == Record.BOTTOM) { buf.append(“Created at:”).append(NEWLINE).append(s); } else { buf.append(’#’).append(i++).append(’:’).append(NEWLINE).append(s); } } else { duped++; } } if (duped > 0) { buf.append(”: “) .append(duped) .append(” leak records were discarded because they were duplicates”) .append(NEWLINE); } if (dropped > 0) { buf.append(”: “) .append(dropped) .append(” leak records were discarded because the leak record count is targeted to “) .append(TARGET_RECORDS) .append(”. Use system property “) .append(PROP_TARGET_RECORDS) .append(” to increase the limit.”) .append(NEWLINE); } buf.setLength(buf.length() - NEWLINE.length()); return buf.toString(); } }DefaultResourceLeak是ResourceLeakDetector定义的私有静态类,它继承了WeakReference类,同时实现了ResourceLeakTracker(定义了record、close方法)接口;record方法内部调用的是record0方法,它会更新newHead为新的Record;close方法会移除allLeaks,allLeaks由ResourceLeakDetector创建DefaultResourceLeak时传入,每创建一个DefaultResourceLeak,DefaultResourceLeak会把自己加入到allLeaks中SimpleLeakAwareByteBufnetty-netty-4.1.33.Final/buffer/src/main/java/io/netty/buffer/SimpleLeakAwareByteBuf.javaclass SimpleLeakAwareByteBuf extends WrappedByteBuf { /* * This object’s is associated with the {@link ResourceLeakTracker}. When {@link ResourceLeakTracker#close(Object)} * is called this object will be used as the argument. It is also assumed that this object is used when * {@link ResourceLeakDetector#track(Object)} is called to create {@link #leak}. */ private final ByteBuf trackedByteBuf; final ResourceLeakTracker<ByteBuf> leak; SimpleLeakAwareByteBuf(ByteBuf wrapped, ByteBuf trackedByteBuf, ResourceLeakTracker<ByteBuf> leak) { super(wrapped); this.trackedByteBuf = ObjectUtil.checkNotNull(trackedByteBuf, “trackedByteBuf”); this.leak = ObjectUtil.checkNotNull(leak, “leak”); } SimpleLeakAwareByteBuf(ByteBuf wrapped, ResourceLeakTracker<ByteBuf> leak) { this(wrapped, wrapped, leak); } //…… @Override public boolean release() { if (super.release()) { closeLeak(); return true; } return false; } @Override public boolean release(int decrement) { if (super.release(decrement)) { closeLeak(); return true; } return false; } private void closeLeak() { // Close the ResourceLeakTracker with the tracked ByteBuf as argument. This must be the same that was used when // calling DefaultResourceLeak.track(…). boolean closed = leak.close(trackedByteBuf); assert closed; } private ByteBuf unwrappedDerived(ByteBuf derived) { // We only need to unwrap SwappedByteBuf implementations as these will be the only ones that may end up in // the AbstractLeakAwareByteBuf implementations beside slices / duplicates and “real” buffers. ByteBuf unwrappedDerived = unwrapSwapped(derived); if (unwrappedDerived instanceof AbstractPooledDerivedByteBuf) { // Update the parent to point to this buffer so we correctly close the ResourceLeakTracker. ((AbstractPooledDerivedByteBuf) unwrappedDerived).parent(this); ResourceLeakTracker<ByteBuf> newLeak = AbstractByteBuf.leakDetector.track(derived); if (newLeak == null) { // No leak detection, just return the derived buffer. return derived; } return newLeakAwareByteBuf(derived, newLeak); } return newSharedLeakAwareByteBuf(derived); } //……}SimpleLeakAwareByteBuf继承了WrappedByteBuf,它的构造器要求传入ResourceLeakTrackerSimpleLeakAwareByteBuf覆盖了WrappedByteBuf的retainedSlice、retainedDuplicate、readRetainedSlice方法,它们内部都会调用unwrappedDerived方法,unwrappedDerived方法在unwrappedDerived对象是AbstractPooledDerivedByteBuf类型时会调用AbstractByteBuf.leakDetector.track进行trackSimpleLeakAwareByteBuf也覆盖了WrappedByteBuf的release方法,在调用父类release成功时会再调用closeLeak方法,使用leak.close(trackedByteBuf)来释放trackedByteBufAdvancedLeakAwareByteBufnetty-netty-4.1.33.Final/buffer/src/main/java/io/netty/buffer/AdvancedLeakAwareByteBuf.javafinal class AdvancedLeakAwareByteBuf extends SimpleLeakAwareByteBuf { private static final String PROP_ACQUIRE_AND_RELEASE_ONLY = “io.netty.leakDetection.acquireAndReleaseOnly”; private static final boolean ACQUIRE_AND_RELEASE_ONLY; private static final InternalLogger logger = InternalLoggerFactory.getInstance(AdvancedLeakAwareByteBuf.class); static { ACQUIRE_AND_RELEASE_ONLY = SystemPropertyUtil.getBoolean(PROP_ACQUIRE_AND_RELEASE_ONLY, false); if (logger.isDebugEnabled()) { logger.debug("-D{}: {}”, PROP_ACQUIRE_AND_RELEASE_ONLY, ACQUIRE_AND_RELEASE_ONLY); } ResourceLeakDetector.addExclusions( AdvancedLeakAwareByteBuf.class, “touch”, “recordLeakNonRefCountingOperation”); } AdvancedLeakAwareByteBuf(ByteBuf buf, ResourceLeakTracker<ByteBuf> leak) { super(buf, leak); } AdvancedLeakAwareByteBuf(ByteBuf wrapped, ByteBuf trackedByteBuf, ResourceLeakTracker<ByteBuf> leak) { super(wrapped, trackedByteBuf, leak); } static void recordLeakNonRefCountingOperation(ResourceLeakTracker<ByteBuf> leak) { if (!ACQUIRE_AND_RELEASE_ONLY) { leak.record(); } } //…… @Override public ByteBuf order(ByteOrder endianness) { recordLeakNonRefCountingOperation(leak); return super.order(endianness); } @Override public ByteBuf slice() { recordLeakNonRefCountingOperation(leak); return super.slice(); } @Override public ByteBuf slice(int index, int length) { recordLeakNonRefCountingOperation(leak); return super.slice(index, length); } //…… @Override public ByteBuf retain() { leak.record(); return super.retain(); } @Override public ByteBuf retain(int increment) { leak.record(); return super.retain(increment); } @Override public boolean release() { leak.record(); return super.release(); } @Override public boolean release(int decrement) { leak.record(); return super.release(decrement); } @Override public ByteBuf touch() { leak.record(); return this; } @Override public ByteBuf touch(Object hint) { leak.record(hint); return this; } //……}AdvancedLeakAwareByteBuf继承了SimpleLeakAwareByteBuf,它对方法进行了覆盖,这些覆盖的方法要么内部通过recordLeakNonRefCountingOperation调用leak.record,要么直接调用leak.record小结ResourceLeakDetector使用Level枚举定义了四种不同的leak detection级别,分别是DISABLED、SIMPLE、ADVANCED、PARANOID;默认level为SIMPLE;可以使用-Dio.netty.leakDetection.level=advanced来进行设置;ResourceLeakDetector还有TARGET_RECORDS(io.netty.leakDetection.targetRecords)及SAMPLING_INTERVAL(io.netty.leakDetection.samplingInterval)两个属性,其中targetRecords默认为4,samplingInterval默认为128ResourceLeakDetector提供了track方法用于创建ResourceLeakTracker;track方法内部调用track0方法,如果level为PARANOID则立即调用reportLeak,创建DefaultResourceLeak,否则利用随机数来判断(PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0)是否调用reportLeak并创建DefaultResourceLeak;reportLeak方法有个for循环,不断从refQueue取DefaultResourceLeak,然后调用reportUntracedLeak或者reportTracedLeak进行errorDefaultResourceLeak是ResourceLeakDetector定义的私有静态类,它继承了WeakReference类,同时实现了ResourceLeakTracker(定义了record、close方法)接口;record方法内部调用的是record0方法,它会更新newHead为新的Record;close方法会移除allLeaks,allLeaks由ResourceLeakDetector创建DefaultResourceLeak时传入,每创建一个DefaultResourceLeak,DefaultResourceLeak会把自己加入到allLeaks中SimpleLeakAwareByteBuf继承了WrappedByteBuf,它的构造器要求传入ResourceLeakTracker;SimpleLeakAwareByteBuf覆盖了WrappedByteBuf的retainedSlice、retainedDuplicate、readRetainedSlice方法,它们内部都会调用unwrappedDerived方法,unwrappedDerived方法在unwrappedDerived对象是AbstractPooledDerivedByteBuf类型时会调用AbstractByteBuf.leakDetector.track进行track;SimpleLeakAwareByteBuf也覆盖了WrappedByteBuf的release方法,在调用父类release成功时会再调用closeLeak方法,使用leak.close(trackedByteBuf)来释放trackedByteBufAdvancedLeakAwareByteBuf继承了SimpleLeakAwareByteBuf,它对方法进行了覆盖,这些覆盖的方法要么内部通过recordLeakNonRefCountingOperation调用leak.record,要么直接调用leak.record;另外有SimpleLeakAwareCompositeByteBuf与AdvancedLeakAwareCompositeByteBuf,它们对leak detect的支持类似SimpleLeakAwareByteBuf与AdvancedLeakAwareByteBufdocNetty 的资源泄露探测机制A Netty ByteBuf Memory Leak Story and the Lessons LearnedIn 4.0.23.Final, Seeing io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it’s garbage-collected #2774 ...

April 3, 2019 · 12 min · jiezi

聊聊netty的maxDirectMemory

序本文主要研究一下netty的maxDirectMemoryPlatformDependentnetty-common-4.1.33.Final-sources.jar!/io/netty/util/internal/PlatformDependent.javapublic final class PlatformDependent { private static final InternalLogger logger = InternalLoggerFactory.getInstance(PlatformDependent.class); private static final Pattern MAX_DIRECT_MEMORY_SIZE_ARG_PATTERN = Pattern.compile( “\s*-XX:MaxDirectMemorySize\s*=\s*([0-9]+)\s*([kKmMgG]?)\s*$”); private static final boolean IS_WINDOWS = isWindows0(); private static final boolean IS_OSX = isOsx0(); private static final boolean MAYBE_SUPER_USER; private static final boolean CAN_ENABLE_TCP_NODELAY_BY_DEFAULT = !isAndroid(); private static final Throwable UNSAFE_UNAVAILABILITY_CAUSE = unsafeUnavailabilityCause0(); private static final boolean DIRECT_BUFFER_PREFERRED; private static final long MAX_DIRECT_MEMORY = maxDirectMemory0(); //…… static { if (javaVersion() >= 7) { RANDOM_PROVIDER = new ThreadLocalRandomProvider() { @Override public Random current() { return java.util.concurrent.ThreadLocalRandom.current(); } }; } else { RANDOM_PROVIDER = new ThreadLocalRandomProvider() { @Override public Random current() { return ThreadLocalRandom.current(); } }; } // Here is how the system property is used: // // * < 0 - Don’t use cleaner, and inherit max direct memory from java. In this case the // “practical max direct memory” would be 2 * max memory as defined by the JDK. // * == 0 - Use cleaner, Netty will not enforce max memory, and instead will defer to JDK. // * > 0 - Don’t use cleaner. This will limit Netty’s total direct memory // (note: that JDK’s direct memory limit is independent of this). long maxDirectMemory = SystemPropertyUtil.getLong(“io.netty.maxDirectMemory”, -1); if (maxDirectMemory == 0 || !hasUnsafe() || !PlatformDependent0.hasDirectBufferNoCleanerConstructor()) { USE_DIRECT_BUFFER_NO_CLEANER = false; DIRECT_MEMORY_COUNTER = null; } else { USE_DIRECT_BUFFER_NO_CLEANER = true; if (maxDirectMemory < 0) { maxDirectMemory = MAX_DIRECT_MEMORY; if (maxDirectMemory <= 0) { DIRECT_MEMORY_COUNTER = null; } else { DIRECT_MEMORY_COUNTER = new AtomicLong(); } } else { DIRECT_MEMORY_COUNTER = new AtomicLong(); } } logger.debug("-Dio.netty.maxDirectMemory: {} bytes", maxDirectMemory); DIRECT_MEMORY_LIMIT = maxDirectMemory >= 1 ? maxDirectMemory : MAX_DIRECT_MEMORY; int tryAllocateUninitializedArray = SystemPropertyUtil.getInt(“io.netty.uninitializedArrayAllocationThreshold”, 1024); UNINITIALIZED_ARRAY_ALLOCATION_THRESHOLD = javaVersion() >= 9 && PlatformDependent0.hasAllocateArrayMethod() ? tryAllocateUninitializedArray : -1; logger.debug("-Dio.netty.uninitializedArrayAllocationThreshold: {}", UNINITIALIZED_ARRAY_ALLOCATION_THRESHOLD); MAYBE_SUPER_USER = maybeSuperUser0(); if (!isAndroid()) { // only direct to method if we are not running on android. // See https://github.com/netty/netty/issues/2604 if (javaVersion() >= 9) { CLEANER = CleanerJava9.isSupported() ? new CleanerJava9() : NOOP; } else { CLEANER = CleanerJava6.isSupported() ? new CleanerJava6() : NOOP; } } else { CLEANER = NOOP; } // We should always prefer direct buffers by default if we can use a Cleaner to release direct buffers. DIRECT_BUFFER_PREFERRED = CLEANER != NOOP && !SystemPropertyUtil.getBoolean(“io.netty.noPreferDirect”, false); if (logger.isDebugEnabled()) { logger.debug("-Dio.netty.noPreferDirect: {}", !DIRECT_BUFFER_PREFERRED); } /* * We do not want to log this message if unsafe is explicitly disabled. Do not remove the explicit no unsafe * guard. / if (CLEANER == NOOP && !PlatformDependent0.isExplicitNoUnsafe()) { logger.info( “Your platform does not provide complete low-level API for accessing direct buffers reliably. " + “Unless explicitly requested, heap buffer will always be preferred to avoid potential system " + “instability.”); } } private static long maxDirectMemory0() { long maxDirectMemory = 0; ClassLoader systemClassLoader = null; try { systemClassLoader = getSystemClassLoader(); // When using IBM J9 / Eclipse OpenJ9 we should not use VM.maxDirectMemory() as it not reflects the // correct value. // See: // - https://github.com/netty/netty/issues/7654 String vmName = SystemPropertyUtil.get(“java.vm.name”, “”).toLowerCase(); if (!vmName.startsWith(“ibm j9”) && // https://github.com/eclipse/openj9/blob/openj9-0.8.0/runtime/include/vendor_version.h#L53 !vmName.startsWith(“eclipse openj9”)) { // Try to get from sun.misc.VM.maxDirectMemory() which should be most accurate. Class<?> vmClass = Class.forName(“sun.misc.VM”, true, systemClassLoader); Method m = vmClass.getDeclaredMethod(“maxDirectMemory”); maxDirectMemory = ((Number) m.invoke(null)).longValue(); } } catch (Throwable ignored) { // Ignore } if (maxDirectMemory > 0) { return maxDirectMemory; } try { // Now try to get the JVM option (-XX:MaxDirectMemorySize) and parse it. // Note that we are using reflection because Android doesn’t have these classes. Class<?> mgmtFactoryClass = Class.forName( “java.lang.management.ManagementFactory”, true, systemClassLoader); Class<?> runtimeClass = Class.forName( “java.lang.management.RuntimeMXBean”, true, systemClassLoader); Object runtime = mgmtFactoryClass.getDeclaredMethod(“getRuntimeMXBean”).invoke(null); @SuppressWarnings(“unchecked”) List<String> vmArgs = (List<String>) runtimeClass.getDeclaredMethod(“getInputArguments”).invoke(runtime); for (int i = vmArgs.size() - 1; i >= 0; i –) { Matcher m = MAX_DIRECT_MEMORY_SIZE_ARG_PATTERN.matcher(vmArgs.get(i)); if (!m.matches()) { continue; } maxDirectMemory = Long.parseLong(m.group(1)); switch (m.group(2).charAt(0)) { case ‘k’: case ‘K’: maxDirectMemory = 1024; break; case ’m’: case ‘M’: maxDirectMemory = 1024 * 1024; break; case ‘g’: case ‘G’: maxDirectMemory = 1024 * 1024 * 1024; break; } break; } } catch (Throwable ignored) { // Ignore } if (maxDirectMemory <= 0) { maxDirectMemory = Runtime.getRuntime().maxMemory(); logger.debug(“maxDirectMemory: {} bytes (maybe)”, maxDirectMemory); } else { logger.debug(“maxDirectMemory: {} bytes”, maxDirectMemory); } return maxDirectMemory; } / * Returns the maximum memory reserved for direct buffer allocation. / public static long maxDirectMemory() { return DIRECT_MEMORY_LIMIT; } //……}netty的PlatformDependent有个静态属性MAX_DIRECT_MEMORY,它是根据maxDirectMemory0方法来计算的maxDirectMemory0方法会根据jvm的类型来做不同处理,如果是IBM J9 / Eclipse OpenJ9的话,就不能使用VM.maxDirectMemory()来获取,正常hotspot则采用VM.maxDirectMemory()来获取(VM.maxDirectMemory是读取-XX:MaxDirectMemorySize配置,如果有设置且大于0则使用该值,如果没有设置该参数则默认值为0,则默认是取的Runtime.getRuntime().maxMemory())static代码块里头设置了DIRECT_MEMORY_LIMIT;它首先从系统属性读取io.netty.maxDirectMemory到maxDirectMemory,如果maxDirectMemory值小于0,则设置maxDirectMemory为MAX_DIRECT_MEMORY;DIRECT_MEMORY_LIMIT = maxDirectMemory >= 1 ? maxDirectMemory : MAX_DIRECT_MEMORY;maxDirectMemory方法直接返回DIRECT_MEMORY_LIMITByteBuffer.allocateDirectjava.base/java/nio/ByteBuffer.javapublic abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>{ //…… / * Allocates a new direct byte buffer. * * <p> The new buffer’s position will be zero, its limit will be its * capacity, its mark will be undefined, each of its elements will be * initialized to zero, and its byte order will be * {@link ByteOrder#BIG_ENDIAN BIG_ENDIAN}. Whether or not it has a * {@link #hasArray backing array} is unspecified. * * @param capacity * The new buffer’s capacity, in bytes * * @return The new byte buffer * * @throws IllegalArgumentException * If the {@code capacity} is a negative integer / public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); } //……}ByteBuffer.allocateDirect方法实际是创建了DirectByteBufferDirectByteBufferjava.base/java/nio/DirectByteBuffer.javaclass DirectByteBuffer extends MappedByteBuffer implements DirectBuffer { //…… // Primary constructor // DirectByteBuffer(int cap) { // package-private super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { base = UNSAFE.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } UNSAFE.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; } //……}DirectByteBuffer的构造器里头会调用Bits.reserveMemory,出现OutOfMemoryError,则调用Bits.unreserveMemory(size, cap),然后抛出OutOfMemoryErrorBits.reserveMemoryjava.base/java/nio/Bits.java/ * Access to bits, native and otherwise. */class Bits { // package-private private Bits() { } // – Direct memory management – // A user-settable upper limit on the maximum amount of allocatable // direct buffer memory. This value may be changed during VM // initialization if it is launched with “-XX:MaxDirectMemorySize=<size>”. private static volatile long MAX_MEMORY = VM.maxDirectMemory(); private static final AtomicLong RESERVED_MEMORY = new AtomicLong(); private static final AtomicLong TOTAL_CAPACITY = new AtomicLong(); private static final AtomicLong COUNT = new AtomicLong(); private static volatile boolean MEMORY_LIMIT_SET; // max. number of sleeps during try-reserving with exponentially // increasing delay before throwing OutOfMemoryError: // 1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s) // which means that OOME will be thrown after 0.5 s of trying private static final int MAX_SLEEPS = 9; //…… // These methods should be called whenever direct memory is allocated or // freed. They allow the user to control the amount of direct memory // which a process may access. All sizes are specified in bytes. static void reserveMemory(long size, int cap) { if (!MEMORY_LIMIT_SET && VM.initLevel() >= 1) { MAX_MEMORY = VM.maxDirectMemory(); MEMORY_LIMIT_SET = true; } // optimist! if (tryReserveMemory(size, cap)) { return; } final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess(); boolean interrupted = false; try { // Retry allocation until success or there are no more // references (including Cleaners that might free direct // buffer memory) to process and allocation still fails. boolean refprocActive; do { try { refprocActive = jlra.waitForReferenceProcessing(); } catch (InterruptedException e) { // Defer interrupts and keep trying. interrupted = true; refprocActive = true; } if (tryReserveMemory(size, cap)) { return; } } while (refprocActive); // trigger VM’s Reference processing System.gc(); // A retry loop with exponential back-off delays. // Sometimes it would suffice to give up once reference // processing is complete. But if there are many threads // competing for memory, this gives more opportunities for // any given thread to make progress. In particular, this // seems to be enough for a stress test like // DirectBufferAllocTest to (usually) succeed, while // without it that test likely fails. Since failure here // ends in OOME, there’s no need to hurry. long sleepTime = 1; int sleeps = 0; while (true) { if (tryReserveMemory(size, cap)) { return; } if (sleeps >= MAX_SLEEPS) { break; } try { if (!jlra.waitForReferenceProcessing()) { Thread.sleep(sleepTime); sleepTime <<= 1; sleeps++; } } catch (InterruptedException e) { interrupted = true; } } // no luck throw new OutOfMemoryError(“Direct buffer memory”); } finally { if (interrupted) { // don’t swallow interrupts Thread.currentThread().interrupt(); } } } private static boolean tryReserveMemory(long size, int cap) { // -XX:MaxDirectMemorySize limits the total capacity rather than the // actual memory usage, which will differ when buffers are page // aligned. long totalCap; while (cap <= MAX_MEMORY - (totalCap = TOTAL_CAPACITY.get())) { if (TOTAL_CAPACITY.compareAndSet(totalCap, totalCap + cap)) { RESERVED_MEMORY.addAndGet(size); COUNT.incrementAndGet(); return true; } } return false; } //……}Bits.reserveMemory方法会先调用tryReserveMemory尝试分配direct memory,不成功则继续往下执行do while(refprocActive)refprocActive这段循环是不断尝试allocation直到分配成功,或者直到没有引用来处理且分配失败如果refprocActive循环没有分配成功,则调用System.gc(),然后进入最后一段循环尝试分配;最后这段循环如果分配成功则返回,分配不成功且sleeps大于等于MAX_SLEEPS,则跳出循环,最后抛出OutOfMemoryError(“Direct buffer memory”)异常小结netty的PlatformDependent有个静态属性MAX_DIRECT_MEMORY,它是根据maxDirectMemory0方法来计算的;maxDirectMemory0方法会根据jvm的类型来做不同处理,如果是IBM J9 / Eclipse OpenJ9的话,就不能使用VM.maxDirectMemory()来获取,正常hotspot则采用VM.maxDirectMemory()来获取(VM.maxDirectMemory是读取-XX:MaxDirectMemorySize配置,如果有设置且大于0则使用该值,如果没有设置该参数则默认值为0,则默认是取的Runtime.getRuntime().maxMemory())static代码块里头设置了DIRECT_MEMORY_LIMIT;它首先从系统属性读取io.netty.maxDirectMemory到maxDirectMemory,如果maxDirectMemory值小于0,则设置maxDirectMemory为MAX_DIRECT_MEMORY;DIRECT_MEMORY_LIMIT = maxDirectMemory >= 1 ? maxDirectMemory : MAX_DIRECT_MEMORY;maxDirectMemory方法直接返回DIRECT_MEMORY_LIMITByteBuffer.allocateDirect方法实际是创建了DirectByteBuffer;DirectByteBuffer的构造器里头会调用Bits.reserveMemory,出现OutOfMemoryError,则调用Bits.unreserveMemory(size, cap),然后抛出OutOfMemoryError;Bits.reserveMemory方法会先调用tryReserveMemory尝试分配direct memory,不成功则继续往下执行do while(refprocActive);refprocActive这段循环是不断尝试allocation直到分配成功,或者直到没有引用来处理且分配失败;如果refprocActive循环没有分配成功,则调用System.gc(),然后进入最后一段循环尝试分配;最后这段循环如果分配成功则返回,分配不成功且sleeps大于等于MAX_SLEEPS,则跳出循环,最后抛出OutOfMemoryError(“Direct buffer memory”)异常doc聊聊jvm的-XX:MaxDirectMemorySizeIn Netty 4, do I need to set option -XX:MaxDirectMemorySize?Netty之Java堆外内存扫盲贴直播一次问题排查过程Change default value of io.netty.maxDirectMemory ? #6349LEAK: ByteBuf.release() was not called before it’s garbage-collected #422 ...

April 2, 2019 · 7 min · jiezi

TCP/IP的底层队列

自从上次学习了TCP/IP的拥塞控制算法后,我越发想要更加深入的了解TCP/IP的一些底层原理,搜索了很多网络上的资料,看到了陶辉大神关于高性能网络编程的专栏,收益颇多。今天就总结一下,并且加上自己的一些思考。 我自己比较了解Java语言,对Java网络编程的理解就止于Netty框架的使用。Netty的源码贡献者Norman Maurer对于Netty网络开发有过一句建议,“Never block the event loop, reduce context-swtiching”。也就是尽量不要阻塞IO线程,也尽量减少线程切换。我们今天只关注前半句,对这句话感兴趣的同学可以看一下[蚂蚁通信框架实践](https://mp.weixin.qq.com/s/JR…。 为什么不能阻塞读取网络信息的IO线程呢?这里就要从经典的网络C10K开始理解,服务器如何支持并发1万请求。C10K的根源在于网络的IO模型。Linux 中网络处理都用同步阻塞的方式,也就是每个请求都分配一个进程或者线程,那么要支持1万并发,难道就要使用1万个线程处理请求嘛?这1万个线程的调度、上下文切换乃至它们占用的内存,都会成为瓶颈。解决C10K的通用办法就是使用I/O 多路复用,Netty就是这样。 Netty有负责服务端监听建立连接的线程组(mainReactor)和负责连接读写操作的IO线程组(subReactor),还可以有专门处理业务逻辑的Worker线程组(ThreadPool)。三者相互独立,这样有很多好处。一是有专门的线程组负责监听和处理网络连接的建立,可以防止TCP/IP的半连接队列(sync)和全连接队列(acceptable)被占满。二是IO线程组和Worker线程分开,双方并行处理网络I/O和业务逻辑,可以避免IO线程被阻塞,防止TCP/IP的接收报文的队列被占满。当然,如果业务逻辑较少,也就是IO 密集型的轻计算业务,可以将业务逻辑放在IO线程中处理,避免线程切换,这也就是Norman Maurer话的后半部分。 TCP/IP怎么就这么多队列啊?今天我们就来细看一下TCP/IP的几个队列,包括建立连接时的半连接队列(sync),全连接队列(accept)和接收报文时的receive、out_of_order、prequeue以及backlog队列。建立连接时的队列 如上图所示,这里有两个队列:syns queue(半连接队列)和accept queue(全连接队列)。三次握手中,服务端接收到客户端的SYN报文后,把相关信息放到半连接队列中,同时回复SYN+ACK给客户端。 第三步的时候服务端收到客户端的ACK,如果这时全连接队列没满,那么从半连接队列拿出相关信息放入到全连接队列中,否则按tcp_abort_on_overflow的值来执行相关操作,直接抛弃或者过一段时间在重试。接收报文时的队列 相比于建立连接,TCP在接收报文时的处理逻辑更为复杂,相关的队列和涉及的配置参数更多。 应用程序接收TCP报文和程序所在服务器系统接收网络里发来的TCP报文是两个独立流程。二者都会操控socket实例,但是会通过锁竞争来决定某一时刻由谁来操控,由此产生很多不同的场景。例如,应用程序正在接收报文时,操作系统通过网卡又接收到报文,这时该如何处理?若应用程序没有调用read或者recv读取报文时,操作系统收到报文又会如何处理? 我们接下来就以三张图为主,介绍TCP接收报文时的三种场景,并在其中介绍四个接收相关的队列。接收报文场景一上图是TCP接收报文场景一的示意图。操作系统首先接收报文,存储到socket的receive队列,然后用户进程再调用recv进行读取。1) 当网卡接收报文并且判断为TCP协议时,经过层层调用,最终会调用到内核的tcp_v4_rcv方法。由于当前TCP要接收的下一个报文正是S1,所以tcp_v4_rcv函数将其直接加入到receive队列中。receive队列是将已经接收到的TCP报文,去除了TCP头部、排好序放入的、用户进程可以直接按序读取的队列。由于socket不在用户进程上下文中(也就是没有用户进程在读socket),并且我们需要S1序号的报文,而恰好收到了S1报文,因此,它进入了receive队列。2) 接收到S3报文,由于TCP要接收的下一个报文序号是S2,所以加入到out_of_order队列,所有乱序的报文会放在这里。3) 接着,收到了TCP期望的S2报文,直接进入recevie队列。由于此时out_of_order队列不为空,需要检查一下。4) 每次向receive队列插入报文时都会检查out_of_order队列,由于接收到S2报文后,期望的的序号为S3,所以out_of_order队列中的S3报文会被移到receive队列。5) 用户进程开始读取socket,先在进程中分配一块内存,然后调用read或者recv方法。socket有一系列的具有默认值的配置属性,比如socket默认是阻塞式的,它的SO_RCVLOWAT属性值默认为1。当然,recv这样的方法还会接收一个flag参数,它可以设置为MSG_WAITALL、MSG_PEEK、MSG_TRUNK等等,这里我们假定为最常用的0。进程调用了recv方法。6) 调用tcp_recvmsg方法7) tcp_recvmsg方法会首先锁住socket。socket是可以被多线程使用的,而且操作系统也会使用,所以必须处理并发问题。要操控socket,就先获取锁。8) 此时,receive队列已经有3个报文了,将第一个报文拷贝到用户态内存中,由于第五步中socket的参数并没有带MSG_PEEK,所以将第一个报文从队列中移除,从内核态释放掉。反之,MSG_PEEK标志位会导致receive队列不会删除报文。所以,MSG_PEEK主要用于多进程读取同一套接字的情形。9) 拷贝第二个报文,当然,执行拷贝前都会检查用户态内存的剩余空间是否足以放下当前这个报文,不够时会直接返回已经拷贝的字节数。10) 拷贝第三个报文。11) receive队列已经为空,此时会检查SO_RCVLOWAT这个最小阈值。如果已经拷贝字节数小于它,进程会休眠,等待更多报文。默认的SO_RCVLOWAT值为1,也就是读取到报文就可以返回。12) 检查backlog队列,backlog队列是用户进程正在拷贝数据时,网卡收到的报文会进这个队列。如果此时backlog队列有数据,就顺带处理下。backlog队列是没有数据的,因此释放锁,准备返回用户态。13) 用户进程代码开始执行,此时recv等方法返回的就是从内核拷贝的字节数。接收报文场景二 第二张图给出了第二个场景,这里涉及了prequeue队列。用户进程调用recv方法时,socket队列中没有任何报文,而socket是阻塞的,所以进程睡眠了。然后操作系统收到了报文,此时prequeue队列开始产生作用。该场景中,tcp_low_latency为默认的0,套接字socket的SO_RCVLOWAT是默认的1,仍然是阻塞socket,如下图。 其中1,2,3步骤的处理和之前一样。我们直接从第四步开始。4) 由于此时receive,prequeue和backlog队列都为空,所以没有拷贝一个字节到用户内存中。而socket的配置要求至少拷贝SO_RCVLOWAT也就是1字节的报文,因此进入阻塞式套接字的等待流程。最长等待时间为SO_RCVTIMEO指定的时间。socket在进入等待前会释放socket锁,会使第五步中,新来的报文不再只能进入backlog队列。5) 接到S1报文,将其加入prequeue队列中。6) 插入到prequeue队列后,会唤醒在socket上休眠的进程。7) 用户进程被唤醒后,重新获取socket锁,此后再接收到的报文只能进入backlog队列。8) 进程先检查receive队列,当然仍然是空的;再去检查prequeue队列,发现有报文S1,正好是正在等待序号的报文,于是直接从prequeue队列中拷贝到用户内存,再释放内核中的这个报文。9) 目前已经拷贝了一个字节的报文到用户内存,检查这个长度是否超过了最低阈值,也就是len和SO_RCVLOWAT的最小值。10) 由于SO_RCVLOWAT使用了默认值1,拷贝字节数大于最低阈值,准备返回用户态,顺便会查看一下backlog队列中是否有数据,此时没有,所以准备放回,释放socket锁。11) 返回用户已经拷贝的字节数。接收报文场景三 在第三个场景中,系统参数tcp_low_latency为1,socket上设置了SO_RCVLOWAT属性值。服务器先收到报文S1,但是其长度小于SO_RCVLOWAT。用户进程调用recv方法读取,虽然读取到了一部分,但是没有到达最小阈值,所以进程睡眠了。与此同时,在睡眠前接收的乱序的报文S3直接进入backlog队列。然后,报文S2到达,由于没有使用prequeue队列(因为设置了tcp_low_latency),而它起始序号正是下一个待拷贝的值,所以直接拷贝到用户内存中,总共拷贝字节数已满足SO_RCVLOWAT的要求!最后在返回用户前把backlog队列中S3报文也拷贝给用户。1) 接收到报文S1,正是准备接收的报文序号,因此,将它直接加入到有序的receive队列中。2) 将系统属性tcp_low_latency设置为1,表明服务器希望程序能够及时的接收到TCP报文。用户调用的recv接收阻塞socket上的报文,该socket的SO_RCVLOWAT值大于第一个报文的大小,并且用户分配了足够大的长度为len的内存。3) 调用tcp_recvmsg方法来完成接收工作,先锁住socket。4) 准备处理内核各个接收队列中的报文。5) receive队列中有报文可以直接拷贝,其大小小于len,直接拷贝到用户内存。6) 在进行第五步的同时,内核又接收到S3报文,此时socket被锁,报文直接进入backlog队列。这个报文并不是有序的。7) 在第五步时,拷贝报文S1到用户内存,它的大小小于SO_RCVLOWAT的值。由于socket是阻塞型,所以用户进程进入睡眠状态。进入睡眠前,会先处理backlog队列的报文。因为S3报文是失序的,所以进入out_of_order 队列。用户进程进入休眠状态前都会先处理一下backlog队列。8) 进程休眠,直到超时或者receive队列不为空。9) 内核接收到报文S2。注意,此时由于打开了tcp_low_latency标志位,所以报文是不会进入prequeue队列等待进程处理。10) 由于报文S2正是要接收的报文,同时,一个用户进程在休眠等待该报文,所以直接将报文S2拷贝到用户内存。11) 每处理完一个有序报文后,无论是拷贝到receive队列还是直接复制到用户内存,都会检查out_of_order队列,看看是否有报文可以处理。报文S3拷贝到用户内存,然后唤醒用户进程。12) 唤醒用户进程。13) 此时会检查已拷贝的字节数是否大于SO_RCVLOWAT,以及backlog队列是否为空。两者皆满足,准备返回。 总结一下四个队列的作用。receive队列是真正的接收队列,操作系统收到的TCP数据包经过检查和处理后,就会保存到这个队列中。backlog是“备用队列”。当socket处于用户进程的上下文时(即用户正在对socket进行系统调用,如recv),操作系统收到数据包时会将数据包保存到backlog队列中,然后直接返回。prequeue是“预存队列”。当socket没有正在被用户进程使用时,也就是用户进程调用了read或者recv系统调用,但是进入了睡眠状态时,操作系统直接将收到的报文保存在prequeue中,然后返回。out_of_order是“乱序队列”。队列存储的是乱序的报文,操作系统收到的报文并不是TCP准备接收的下一个序号的报文,则放入out_of_order队列,等待后续处理。后记 如果你觉得本篇文章对你有帮助,请点个赞。同时欢迎订阅本人的微信公众号。个人博客: Remcarpediem参考http://www.voidcn.com/article…https://blog.csdn.net/russell...https://ylgrgyq.github.io/201…

March 10, 2019 · 1 min · jiezi

使用Netty,我们到底在开发些什么?

在java界,netty无疑是开发网络应用的拿手菜。你不需要太多关注复杂的nio模型和底层网络的细节,使用其丰富的接口,可以很容易的实现复杂的通讯功能。和golang的网络模块相比,netty还是太过臃肿。不过java类框架就是这样,属于那种离了IDE就无法存活的编码语言。最新的netty版本将模块分的非常细,如果不清楚每个模块都有什么内容,直接使用netty-all即可。单纯从使用方面来说,netty是非常简单的,掌握ByteBuf、Channel、Pipeline、Event模型等,就可以进行开发了。你会发现面试netty相关知识,没得聊。但Netty与其他开发模式很大不同,最主要的就是其异步化。异步化造成的后果就是编程模型的不同,同时有调试上的困难,对编码的要求比较高,因为bug的代价与业务代码的bug代价不可同日而语。但从项目来说,麻雀虽小五脏俱全,从业务层到服务网关,以及各种技术保障,包括监控和配置,都是需要考虑的因素。netty本身占比很小。本文将说明使用netty开发,都关注哪些通用的内容,然后附上单机支持100w连接的linux配置。本文并不关注netty的基础知识。协议开发网络开发中最重要的就是其通讯格式,协议。我们常见的protobuf、json、avro、mqtt等,都属于此列。协议有语法、语义、时序三个要素。我见过很多中间件应用,采用的是redis协议,而后端落地的却是mysql;也见过更多的采用mysql协议实现的各种自定义存储系统,比如proxy端的分库分表中间件、tidb等。我们常用的redis,使用的是文本协议;mysql等实现的是二进制协议。放在netty中也是一样,实现一套codec即可(继承Decoder或Encoder系列)。netty默认实现了dns、haproxy、http、http2、memcache、mqtt、redis、smtp、socks、stomp、xml等协议,可以说是很全了,直接拿来用很爽。一个可能的产品结构会是这样的,对外提供一致的外观,核心存储却不同:文本协议在调试起来是比较直观和容易的,但安全性欠佳;而二进制协议就需要依赖日志、wireshark等其他方式进行分析,增加了开发难度。传说中的粘包拆包,就在这里处理。而造成粘包的原因,主要是由于缓冲区的介入,所以需要约定双方的传输概要等信息,netty在一定程度上解决了这个问题。每一个想要开发网络应用的同学,心里都埋了一颗重新设计协议的梦想种子。但协议的设计可以说是非常困难了,要深耕相应业务,还要考虑其扩展性。如没有特别的必要,建议使用现有的协议。连接管理功能做Netty开发,连接管理功能是非常重要的。通信质量、系统状态,以及一些黑科技功能,都是依赖连接管理功能。无论是作为服务端还是客户端,netty在创建连接之后,都会得到一个叫做Channel的对象。我们所要做的,就是对它的管理,我习惯给它起名叫做ConnectionManager。管理类会通过缓存一些内存对象,用来统计运行中的数据。比如面向连接的功能:包发送、接收数量;包发送、接收速率;错误计数;连接重连次数;调用延迟;连接状态等。这会频繁用到java中concurrent包的相关类,往往也是bug集中地。但我们还需要更多,管理类会给予每个连接更多的功能。比如,连接创建后,想要预热一些功能,那这些状态就可以参与路由的决策。通常情况下,将用户或其他元信息也attach到连接上,能够多维度的根据条件筛选一些连接,进行批量操作,比如灰度、过载保护等,是一个非常重要的功能。管理后台可以看到每个连接的信息,筛选到一个或多个连接后,能够开启对这些连接的流量录制、信息监控、断点调试,你能体验到掌控一切的感觉。管理功能还能够看到系统的整个运行状态,及时调整负载均衡策略;同时对扩容、缩容提供数据依据。心跳检测应用协议层的心跳是必须的,它和tcp keepalive是完全不同的概念。应用层协议层的心跳检测的是连接双方的存活性,兼而连接质量,而keepalive检测的是连接本身的存活性。而且后者的超时时间默认过长,完全不能适应现代的网络环境。心跳就是靠轮训,无论是服务端,还是客户端比如GCM等。保活机制会在不同的应用场景进行动态的切换,比如程序唤起和在后台,轮训的策略是不一样的。Netty内置通过增加IdleStateHandler产生IDLE事件进行便捷的心跳控制。你要处理的,就是心跳超时的逻辑,比如延迟重连。但它的轮训时间是固定的,无法动态修改,高级功能需要自己定制。在一些客户端比如Android,频繁心跳的唤起会浪费大量的网络和电量,它的心跳策略会更加复杂一些。边界优雅退出机制Java的优雅停机通常通过注册JDK ShutdownHook来实现。Runtime.getRuntime().addShutdownHook();一般通过kill -15进行java进程的关闭,以便在进程死亡之前进行一些清理工作。注意:kill -9 会立马杀死进程,不给遗言的机会,比较危险。虽然netty做了很多优雅退出的工作,通过EventLoopGroup的shutdownGracefully方法对nio进行了一些状态设置,但在很多情况下,这还不够多。它只负责单机环境的优雅关闭。流量可能还会通过外层的路由持续进入,造成无效请求。我的通常做法是首先在外层路由进行一次本地实例的摘除,把流量截断,然后再进行netty本身的优雅关闭。这种设计非常简单,即使没有重试机制也会运行的很好,前提是在路由层需要提前暴露相关接口。异常处理功能netty由于其异步化的开发方式,以及其事件机制,在异常处理方面就显得异常重要。为了保证连接的高可靠性,许多异常需要静悄悄的忽略,或者在用户态没有感知。netty的异常会通过pipeline进行传播,所以在任何一层进行处理都是可行的,但编程习惯上,习惯性抛到最外层集中处理。为了最大限度的区别异常信息,通常会定义大量的异常类,不同的错误会抛出不同的异常。发生异常后,可以根据不同的类型选择断线重连(比如一些二进制协议的编解码紊乱问题),或者调度到其他节点。功能限制指令模式网络应用就该干网络应用的事,任何通讯都是昂贵的。在《Linux之《荒岛余生》(五)网络篇》中,我们谈到百万连接的服务器,广播一个1kb消息,就需要1000M的带宽,所以并不是什么都可以放在网络应用里的。 一个大型网络应用的合理的思路就是值发送相关指令。客户端在收到指令以后,通过其他方式,比如http,进行大型文件到获取。很多IM的设计思路就是如此。指令模式还会让通讯系统的扩展性和稳定性得到保证。增加指令可以是配置式的,立即生效,服务端不需要编码重启。稳定性保证网络应用的流量一般都是非常大的,并不适合全量日志的开启。应用应该只关注主要事件的日志,关注异常情况下的处理流程,日志要打印有度。网络应用也不适合调用其他缓慢的api,或者任何阻塞I/O的接口。一些实时的事件,也不应该通过调用接口吐出数据,可以走高速mq等其他异步通道。缓存可能是网络应用里用的最多的组件。jvm内缓存可以存储一些单机的统计数据,redis等存储一些全局性的统计和中间态数据。网络应用中会大量使用redis、kv、高吞吐的mq,用来快速响应用户请求。总之,尽量保持通讯层的清爽,你会省去很多忧虑。单机支持100万连接的Linux配置单机支持100万连接是可行的,但带宽问题会成为显著的瓶颈。启用压缩的二进制协议会节省部分带宽,但开发难度增加。和《LWP进程资源耗尽,Resource temporarily unavailable》中提到的ES配置一样,优化都有类似的思路。这份配置,可以节省你几天的时间,请收下!操作系统优化更改进程最大文件句柄数ulimit -n 1048576修改单个进程可分配的最大文件数echo 2097152 > /proc/sys/fs/nr_open修改/etc/security/limits.conf文件* soft nofile 1048576* hard nofile 1048576* soft nproc unlimitedroot soft nproc unlimited记得清理掉/etc/security/limits.d/*下的配置网络优化打开/etc/sysctl.conf,添加配置然后执行,使用sysctl生效#单个进程可分配的最大文件数fs.nr_open=2097152#系统最大文件句柄数fs.file-max = 1048576#backlog 设置net.core.somaxconn=32768net.ipv4.tcp_max_syn_backlog=16384net.core.netdev_max_backlog=16384#可用知名端口范围配置net.ipv4.ip_local_port_range=‘1000 65535’#TCP Socket 读写 Buffer 设置net.core.rmem_default=262144net.core.wmem_default=262144net.core.rmem_max=16777216net.core.wmem_max=16777216net.core.optmem_max=16777216net.ipv4.tcp_rmem=‘1024 4096 16777216’net.ipv4.tcp_wmem=‘1024 4096 16777216’#TCP 连接追踪设置net.nf_conntrack_max=1000000net.netfilter.nf_conntrack_max=1000000net.netfilter.nf_conntrack_tcp_timeout_time_wait=30#TIME-WAIT Socket 最大数量、回收与重用设置net.ipv4.tcp_max_tw_buckets=1048576# FIN-WAIT-2 Socket 超时设置net.ipv4.tcp_fin_timeout = 15总结netty的开发工作并不集中在netty本身,更多体现在保证服务的高可靠性和稳定性上。同时有大量的工作集中在监控和调试,减少bug修复的成本。深入了解netty是在系统遇到疑难问题时能够深入挖掘进行排查,或者对苛刻的性能进行提升。但对于广大应用开发者来说,netty的上手成本小,死挖底层并不会产生太多收益。它只是个工具,你还能让它怎样啊。0.jpeg

February 27, 2019 · 1 min · jiezi

Netty源码解析-概述篇

本文是由code4craft发表在博客上的,原文基于Netty3.7的版本,源码部分对buffer、Pipeline、Reactor模式等进行了部分讲解,个人又继续新增了后续的几个核心组件的源码解读,新增了具体的案例。Netty的源码非常好,质量极高,是Java中质量最高的开源项目之一,(比Spring系列源码高几层楼…)我十分建议大家花上一周时间自习读一读。概述Netty是什么大概用Netty的,无论新手还是老手,都知道它是一个“网络通讯框架”。所谓框架,基本上都是一个作用:基于底层API,提供更便捷的编程模型。那么"通讯框架"到底做了什么事情呢?回答这个问题并不太容易,我们不妨反过来看看,不使用netty,直接基于NIO编写网络程序,你需要做什么(以Server端TCP连接为例,这里我们使用Reactor模型):监听端口,建立Socket连接建立线程,处理内容读取Socket内容,并对协议进行解析进行逻辑处理回写响应内容如果是多次交互的应用(SMTP、FTP),则需要保持连接多进行几次交互关闭连接建立线程是一个比较耗时的操作,同时维护线程本身也有一些开销,所以我们会需要多线程机制,幸好JDK已经有很方便的多线程框架了,这里我们不需要花很多心思。此外,因为TCP连接的特性,我们还要使用连接池来进行管理:建立TCP连接是比较耗时的操作,对于频繁的通讯,保持连接效果更好对于并发请求,可能需要建立多个连接维护多个连接后,每次通讯,需要选择某一可用连接连接超时和关闭机制想想就觉得很复杂了!实际上,基于NIO直接实现这部分东西,即使是老手也容易出现错误,而使用Netty之后,你只需要关注逻辑处理部分就可以了。体验Netty这里我们引用Netty的example包里的一个例子,一个简单的EchoServer,它接受客户端输入,并将输入原样返回。其主要代码如下: public void run() { // Configure the server. ServerBootstrap bootstrap = new ServerBootstrap( new NioServerSocketChannelFactory( Executors.newCachedThreadPool(), Executors.newCachedThreadPool())); // Set up the pipeline factory. bootstrap.setPipelineFactory(new ChannelPipelineFactory() { public ChannelPipeline getPipeline() throws Exception { return Channels.pipeline(new EchoServerHandler()); } }); // Bind and start to accept incoming connections. bootstrap.bind(new InetSocketAddress(port)); }这里EchoServerHandler是其业务逻辑的实现者,大致代码如下: public class EchoServerHandler extends SimpleChannelUpstreamHandler { @Override public void messageReceived( ChannelHandlerContext ctx, MessageEvent e) { // Send back the received message to the remote peer. e.getChannel().write(e.getMessage()); } }还是挺简单的,不是吗?Netty背后的事件驱动机制完成了以上一段代码,我们算是与Netty进行了第一次亲密接触。如果想深入学习呢?阅读源码是了解一个开源工具非常好的手段,但是Java世界的框架大多追求大而全,功能完备,如果逐个阅读,难免迷失方向,Netty也并不例外。相反,抓住几个重点对象,理解其领域概念及设计思想,从而理清其脉络,相当于打通了任督二脉,以后的阅读就不再困难了。理解Netty的关键点在哪呢?我觉得,除了NIO的相关知识,另一个就是事件驱动的设计思想。什么叫事件驱动?我们回头看看EchoServerHandler的代码,其中的参数:public void messageReceived(ChannelHandlerContext ctx, MessageEvent e),MessageEvent就是一个事件。这个事件携带了一些信息,例如这里e.getMessage()就是消息的内容,而EchoServerHandler则描述了处理这种事件的方式。一旦某个事件触发,相应的Handler则会被调用,并进行处理。这种事件机制在UI编程里广泛应用,而Netty则将其应用到了网络编程领域。在Netty里,所有事件都来自ChannelEvent接口,这些事件涵盖监听端口、建立连接、读写数据等网络通讯的各个阶段。而事件的处理者就是ChannelHandler,这样,不但是业务逻辑,连网络通讯流程中底层的处理,都可以通过实现ChannelHandler来完成了。事实上,Netty内部的连接处理、协议编解码、超时等机制,都是通过handler完成的。当博主弄明白其中的奥妙时,不得不佩服这种设计!下图描述了Netty进行事件处理的流程。Channel是连接的通道,是ChannelEvent的产生者,而ChannelPipeline可以理解为ChannelHandler的集合。开启Netty源码之门理解了Netty的事件驱动机制,我们现在可以来研究Netty的各个模块了。Netty的包结构如下:org└── jboss └── netty ├── bootstrap 配置并启动服务的类 ├── buffer 缓冲相关类,对NIO Buffer做了一些封装 ├── channel 核心部分,处理连接 ├── container 连接其他容器的代码 ├── example 使用示例 ├── handler 基于handler的扩展部分,实现协议编解码等附加功能 ├── logging 日志 └── util 工具类在这里面,channel和handler两部分比较复杂。我们不妨与Netty官方的结构图对照一下,来了解其功能。具体的解释可以看这里:http://netty.io/3.7/guide/#architecture。图中可以看到,除了之前说到的事件驱动机制之外,Netty的核心功能还包括两部分:Zero-Copy-Capable Rich Byte Buffer零拷贝的Buffer。为什么叫零拷贝?因为在数据传输时,最终处理的数据会需要对单个传输层的报文,进行组合或者拆分。NIO原生的ByteBuffer无法做到这件事,而Netty通过提供Composite(组合)和Slice(切分)两种Buffer来实现零拷贝。这部分代码在org.jboss.netty.buffer包中。这里需要额外注意,不要和操作系统级别的Zero-Copy混淆了, 操作系统中的零拷贝主要是用户空间和内核空间之间的数据拷贝, NIO中通过DirectBuffer做了实现.Universal Communication API统一的通讯API。这个是针对Java的Old I/O和New I/O,使用了不同的API而言。Netty则提供了统一的API(org.jboss.netty.channel.Channel)来封装这两种I/O模型。这部分代码在org.jboss.netty.channel包中。此外,Protocol Support功能通过handler机制实现。接下来的文章,我们会根据模块,详细的对Netty源码进行分析。参考资料:Netty 3.7 User Guide http://netty.io/3.7/guide/What is Netty? http://ayedo.github.io/netty/2013/06/19/what-is-netty.html ...

February 22, 2019 · 1 min · jiezi

宜人贷蜂巢API网关技术解密之Netty使用实践

宜人贷蜂巢团队,由Michael创立于2013年,通过使用互联网科技手段助力金融生态和谐健康发展。自成立起一直致力于多维度数据闭环平台建设。目前团队规模超过百人,涵盖征信、电商、金融、社交、五险一金和保险等用户授信数据的抓取解析业务,辅以先进的数据分析、挖掘和机器学习等技术对用户信用级别、欺诈风险进行预测评定,全面对外输出金融反欺诈、社交图谱、自动化模型定制等服务或产品。目前宜人贷蜂巢基于用户授权数据实时抓取解析技术,并结合顶尖大数据技术,快速迭代和自主的创新,已形成了强大而领先的聚合和输出能力。为了适应完成宜人贷蜂巢强大的服务输出能力,蜂巢设计开发了自己的API网关系统,集中实现了鉴权、加解密、路由、限流等功能,使各业务抓取团队关注其核心抓取和分析工作,而API网关系统更专注于安全、流量、路由等问题,从而更好的保障蜂巢服务系统的质量。今天带着大家解密API网关的Netty线程池技术实践细节。API网关作为宜人贷蜂巢数据开放平台的统一入口,所有的客户端及消费端通过统一的API来使用各类抓取服务。从面向对象设计的角度看,它与外观模式类似,包装各类不同的实现细节,对外表现出统一的调用形式。本文首先,简要地介绍API网关的项目框架,其次对比BIO和NIO的特点,再引入Netty作为项目的基础框架,然后介绍Netty线程池的原理,最后深入Netty线程池的初始化、ServerBootstrap的初始化与启动及channel与线程池的绑定过程,让读者了解Netty在承载高并发访问的设计路思。项目框架图1 - API网关项目框架图中描绘了API网关系统的处理流程,以及与服务注册发现、日志分析、报警系统、各类爬虫的关系。其中API网关系统接收请求,对请求进行编解码、鉴权、限流、加解密,再基于Eureka服务注册发现模块,将请求发送到有效的服务节点上;网关及抓取系统的日志,会被收集到elk平台中,做业务分析及报警处理。BIO vs NIOAPI网关承载数倍于爬虫的流量,提升服务器的并发处理能力、缩短系统的响应时间,通信模型的选择是至关重要的,是选择BIO,还是NIO?Streamvs Buffer & 阻塞 vs 非阻塞BIO是面向流的,io的读写,每次只能处理一个或者多个bytes,如果数据没有读写完成,线程将一直等待于此,而不能暂时跳过io或者等待io读写完成异步通知,线程滞留在io读写上,不能充分利用机器有限的线程资源,造成server的吞吐量较低,见图2。而NIO与此不同,面向Buffer,线程不需要滞留在io读写上,采用操作系统的epoll模式,在io数据准备好了,才由线程来处理,见图3。图2 – BIO 从流中读取数据图3 – NIO 从Buffer中读取数据SelectorsNIO的selector使一个线程可以监控多个channel的读写,多个channel注册到一个selector上,这个selector可以监测到各个channel的数据准备情况,从而使用有限的线程资源处理更多的连接,见图4。所以可以这样说,NIO极大的提升了服务器接受并发请求的能力,而服务器性能还是要取决于业务处理时间和业务线程池模型。图4 – NIO 单一线程管理多个连接而BIO采用的是request-per-thread模式,用一个线程负责接收TCP连接请求,并建立链路,然后将请求dispatch给负责业务逻辑处理的线程,见图5。一旦访问量过多,就会造成机器的线程资源紧张,造成请求延迟,甚至服务宕机。图5 – BIO 一连接一线程对比JDK NIO与诸多NIO框架后,鉴于Netty优雅的设计、易用的API、优越的性能、安全性支持、API网关使用Netty作为通信模型,实现了基础框架的搭建。Netty线程池考虑到API网关的高并发访问需求,线程池设计,见图6。图6 – API网关线程池设计Netty的线程池理念有点像ForkJoinPool,不是一个线程大池子并发等待一条任务队列,而是每条线程都有一个任务队列。而且Netty的线程,并不只是简单的阻塞地拉取任务,而是在每个循环中做三件事情:先SelectKeys()处理NIO的事件然后获取本线程的定时任务,放到本线程的任务队列里最后执行其他线程提交给本线程的任务每个循环里处理NIO事件与其他任务的时间消耗比例,还能通过ioRatio变量来控制,默认是各占50%。可见,Netty的线程根本没有阻塞等待任务的清闲日子,所以也不使用有锁的BlockingQueue来做任务队列了,而是使用无锁的MpscLinkedQueue(Mpsc 是Multiple Producer, Single Consumer的缩写)NioEventLoopGroup初始化下面分析下Netty线程池NioEventLoopGroup的设计与实现细节,NioEventLoopGroup的类层次关系见图7图7 –NioEvenrLoopGroup类层次关系其创建过程——方法调用,见下图图8 –NioEvenrLoopGroup创建调用关系NioEvenrLoopGroup的创建,具体执行过程是执行类MultithreadEventExecutorGroup的构造方法/** * Create a new instance. * * @param nThreads the number of threads that will be used by this instance. * @param executor the Executor to use, or {@code null} if the default should be used. * @param chooserFactory the {@link EventExecutorChooserFactory} to use. * @param args arguments which will passed to each {@link #newChild(Executor, Object…)} call */protected MultithreadEventExecutorGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, Object… args) { if (nThreads <= 0) { throw new IllegalArgumentException(String.format(“nThreads: %d (expected: > 0)”, nThreads)); } if (executor == null) { executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); } children = new EventExecutor[nThreads]; for (int i = 0; i < nThreads; i ++) { boolean success = false; try { children[i] = newChild(executor, args); success = true; } catch (Exception e) { throw new IllegalStateException(“failed to create a child event loop”, e); } finally { if (!success) { for (int j = 0; j < i; j ++) { children[j].shutdownGracefully(); } for (int j = 0; j < i; j ++) { EventExecutor e = children[j]; try { while (!e.isTerminated()) { e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); } } catch (InterruptedException interrupted) { // Let the caller handle the interruption. Thread.currentThread().interrupt(); break; } } } } } chooser = chooserFactory.newChooser(children); final FutureListener<Object> terminationListener = new FutureListener<Object>() { @Override public void operationComplete(Future<Object> future) throws Exception { if (terminatedChildren.incrementAndGet() == children.length) { terminationFuture.setSuccess(null); } } }; for (EventExecutor e: children) { e.terminationFuture().addListener(terminationListener); } Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length); Collections.addAll(childrenSet, children); readonlyChildren = Collections.unmodifiableSet(childrenSet);}其中,创建细节见下:线程池中的线程数nThreads必须大于0;如果executor为null,创建默认executor,executor用于创建线程(newChild方法使用executor对象);依次创建线程池中的每一个线程即NioEventLoop,如果其中有一个创建失败,将关闭之前创建的所有线程;chooser为线程池选择器,用来选择下一个EventExecutor,可以理解为,用来选择一个线程来执行task;chooser的创建细节,见下DefaultEventExecutorChooserFactory根据线程数创建具体的EventExecutorChooser,线程数如果等于2^n,可使用按位与替代取模运算,节省cpu的计算资源,见源码@SuppressWarnings(“unchecked”)@Overridepublic EventExecutorChooser newChooser(EventExecutor[] executors) { if (isPowerOfTwo(executors.length)) { return new PowerOfTowEventExecutorChooser(executors); } else { return new GenericEventExecutorChooser(executors); }} private static final class PowerOfTowEventExecutorChooser implements EventExecutorChooser { private final AtomicInteger idx = new AtomicInteger(); private final EventExecutor[] executors; PowerOfTowEventExecutorChooser(EventExecutor[] executors) { this.executors = executors; } @Override public EventExecutor next() { return executors[idx.getAndIncrement() & executors.length - 1]; } } private static final class GenericEventExecutorChooser implements EventExecutorChooser { private final AtomicInteger idx = new AtomicInteger(); private final EventExecutor[] executors; GenericEventExecutorChooser(EventExecutor[] executors) { this.executors = executors; } @Override public EventExecutor next() { return executors[Math.abs(idx.getAndIncrement() % executors.length)]; } }newChild(executor, args)的创建细节,见下MultithreadEventExecutorGroup的newChild方法是一个抽象方法,故使用NioEventLoopGroup的newChild方法,即调用NioEventLoop的构造函数 @Override protected EventLoop newChild(Executor executor, Object… args) throws Exception { return new NioEventLoop(this, executor, (SelectorProvider) args[0], ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); }在这里先看下NioEventLoop的类层次关系NioEventLoop的继承关系比较复杂,在AbstractScheduledEventExecutor 中, Netty 实现了 NioEventLoop 的 schedule 功能, 即我们可以通过调用一个 NioEventLoop 实例的 schedule 方法来运行一些定时任务. 而在 SingleThreadEventLoop 中, 又实现了任务队列的功能, 通过它, 我们可以调用一个NioEventLoop 实例的 execute 方法来向任务队列中添加一个 task, 并由 NioEventLoop 进行调度执行.通常来说, NioEventLoop 肩负着两种任务, 第一个是作为 IO 线程, 执行与 Channel 相关的 IO 操作, 包括调用 select 等待就绪的 IO 事件、读写数据与数据的处理等; 而第二个任务是作为任务队列, 执行 taskQueue 中的任务, 例如用户调用 eventLoop.schedule 提交的定时任务也是这个线程执行的.具体的构造过程,见下创建任务队列tailTasks(内部为有界的LinkedBlockingQueue)创建线程的任务队列taskQueue(内部为有界的LinkedBlockingQueue),以及任务过多防止系统宕机的拒绝策略rejectedHandler其中tailTasks和taskQueue均是任务队列,而优先级不同,taskQueue的优先级高于tailTasks,定时任务的优先级高于taskQueue。ServerBootstrap初始化及启动了解了Netty线程池NioEvenrLoopGroup的创建过程后,下面看下API网关服务ServerBootstrap的是如何使用线程池引入服务中,为高并发访问服务的。API网关ServerBootstrap初始化及启动代码,见下serverBootstrap = new ServerBootstrap();bossGroup = new NioEventLoopGroup(config.getBossGroupThreads());workerGroup = new NioEventLoopGroup(config.getWorkerGroupThreads());serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class) .option(ChannelOption.TCP_NODELAY, config.isTcpNoDelay()) .option(ChannelOption.SO_BACKLOG, config.getBacklogSize()) .option(ChannelOption.SO_KEEPALIVE, config.isSoKeepAlive()) // Memory pooled .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .childHandler(channelInitializer); ChannelFuture future = serverBootstrap.bind(config.getPort()).sync();log.info(“API-gateway started on port: {}”, config.getPort());future.channel().closeFuture().sync();API网关系统使用netty自带的线程池,共有三组线程池,分别为bossGroup、workerGroup和executorGroup(使用在channelInitializer中,本文暂不作介绍)。其中,bossGroup用于接收客户端的TCP连接,workerGroup用于处理I/O、执行系统task和定时任务,executorGroup用于处理网关业务加解密、限流、路由,及将请求转发给后端的抓取服务等业务操作。Channel与线程池的绑定ServerBootstrap初始化后,通过调用bind(port)方法启动Server,bind的调用链如下AbstractBootstrap.bind ->AbstractBootstrap.doBind -> AbstractBootstrap.initAndRegister其中,ChannelFuture regFuture = config().group().register(channel);中的group()方法返回bossGroup,而channel在serverBootstrap的初始化过程指定channel为NioServerSocketChannel.class,至此将NioServerSocketChannel与bossGroup绑定到一起,bossGroup负责客户端连接的建立。那么NioSocketChannel是如何与workerGroup绑定到一起的?调用链AbstractBootstrap.initAndRegister -> AbstractBootstrap. init-> ServerBootstrap.init ->ServerBootstrapAcceptor.ServerBootstrapAcceptor ->ServerBootstrapAcceptor.channelReadpublic void channelRead(ChannelHandlerContext ctx, Object msg) { final Channel child = (Channel) msg; child.pipeline().addLast(childHandler); for (Entry<ChannelOption<?>, Object> e: childOptions) { try { if (!child.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) { logger.warn(“Unknown channel option: " + e); } } catch (Throwable t) { logger.warn(“Failed to set a channel option: " + child, t); } } for (Entry<AttributeKey<?>, Object> e: childAttrs) { child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue()); } try { childGroup.register(child).addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { forceClose(child, future.cause()); } } }); } catch (Throwable t) { forceClose(child, t); }}其中,childGroup.register(child)就是将NioSocketChannel与workderGroup绑定到一起,那又是什么触发了ServerBootstrapAcceptor的channelRead方法?其实当一个 client 连接到 server 时, Java 底层的 NIO ServerSocketChannel 会有一个SelectionKey.OP_ACCEPT 就绪事件, 接着就会调用到 NioServerSocketChannel.doReadMessages方法@Overrideprotected int doReadMessages(List<Object> buf) throws Exception { SocketChannel ch = javaChannel().accept(); try { if (ch != null) { buf.add(new NioSocketChannel(this, ch)); return 1; } } catch (Throwable t) { … } return 0;}javaChannel().accept() 会获取到客户端新连接的SocketChannel,实例化为一个 NioSocketChannel, 并且传入 NioServerSocketChannel 对象(即 this), 由此可知, 我们创建的这个NioSocketChannel 的父 Channel 就是 NioServerSocketChannel 实例 .接下来就经由 Netty 的 ChannelPipeline 机制, 将读取事件逐级发送到各个 handler 中, 于是就会触发前面我们提到的 ServerBootstrapAcceptor.channelRead 方法啦。至此,分析了Netty线程池的初始化、ServerBootstrap的启动及channel与线程池的绑定过程,能够看出Netty中线程池的优雅设计,使用不同的线程池负责连接的建立、IO读写等,为API网关项目的高并发访问提供了技术基础。总结至此,对API网关技术的Netty实践分享就到这里,各位如果对中间的各个环节有什么疑问和建议,欢迎大家指正,我们一起讨论,共同学习提高。参考http://tutorials.jenkov.com/j…http://netty.io/wiki/user-gui...http://netty.io/http://www.tuicool.com/articl...https://segmentfault.com/a/11...https://segmentfault.com/a/11…作者:蜂巢团队 宜信技术学院 ...

February 20, 2019 · 3 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App(六)

Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍。Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接。本章完整代码链接。本章将给聊天App_PigChat加上心跳机制。为什么要实现心跳机制如果没有特意的设置某些选项或者实现应用层心跳包,TCP空闲的时候是不会发送任何数据包。也就是说,当一个TCP的socket,客户端与服务端谁也不发送数据,会一直保持着连接。这其中如果有一方异常掉线(例如死机、路由被破坏、防火墙切断连接等),另一端如果没有发送数据,永远也不可能知道。这对于一些服务型的程序来说,是灾难性的后果,将会导致服务端socket资源耗尽。举个简单的例子,当我们因为特殊情况打开飞行模式 ,在处理完事件之后再关闭飞行模式,这时候如果再进入应用程序中,我们将以新的channel进入,但是之前的channel还是会保留。因此,为了保证连接的有效性、及时有效地检测到一方的非正常断开,保证连接的资源被有效的利用,我们就会需要一种保活的机制,通常改机制两种处理方式:1、利用TCP协议层实现的Keepalive;2、自己在应用层实现心跳包。实现心跳机制新建一个HeartBeatHandler用于检测channel的心跳。继承ChannelInboundHandlerAdapter,并重写其userEventTriggered方法。当客户端的所有ChannelHandler中4s内没有write事件,则会触发userEventTriggered方法。首先我们判断evt是否是IdleStateEvent的实例,IdleStateEvent用于触发用户事件,包含读空闲/写空闲/读写空闲。对evt进行强制履行转换后,通过state判断其状态,只有当其该channel处于读写空闲的时候才将这个channel关闭。/** * @Description: 用于检测channel的心跳handler * 继承ChannelInboundHandlerAdapter,从而不需要实现channelRead0方法 */public class HeartBeatHandler extends ChannelInboundHandlerAdapter { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { // 判断evt是否是IdleStateEvent(用于触发用户事件,包含 读空闲/写空闲/读写空闲 ) if (evt instanceof IdleStateEvent) { IdleStateEvent event = (IdleStateEvent)evt; // 强制类型转换 if (event.state() == IdleState.READER_IDLE) { System.out.println(“进入读空闲…”); } else if (event.state() == IdleState.WRITER_IDLE) { System.out.println(“进入写空闲…”); } else if (event.state() == IdleState.ALL_IDLE) { System.out.println(“channel关闭前,users的数量为:” + ChatHandler.users.size()); Channel channel = ctx.channel(); // 关闭无用的channel,以防资源浪费 channel.close(); System.out.println(“channel关闭后,users的数量为:” + ChatHandler.users.size()); } } } }增加心跳支持在原来的WSServerInitialzer中增加心跳机制的支持。 // ====================== 增加心跳支持 start ====================== // 针对客户端,如果在1分钟时没有向服务端发送读写心跳(ALL),则主动断开 // 如果是读空闲或者写空闲,不处理 pipeline.addLast(new IdleStateHandler(8, 10, 12)); // 自定义的空闲状态检测 pipeline.addLast(new HeartBeatHandler()); // ====================== 增加心跳支持 end ====================== ...

February 19, 2019 · 1 min · jiezi

引入Redis|tensorflow实现 聊天AI--PigPig养成记(3)

引入Redis项目github链接在集成Netty之后,为了提高效率,我打算将消息存储在Redis缓存系统中,本节将介绍Redis在项目中的引入,以及前端界面的开发。引入Redis后,完整代码链接。想要直接得到训练了13000步的聊天机器人可以直接下载链接中这三个文件,以及词汇表文件然后直接运行连接中的py脚本进行测试即可。最终实现效果如下:在Netty中引入Redisimport java.io.BufferedReader;import java.io.BufferedWriter;import java.io.File;import java.io.FileNotFoundException;import java.io.FileReader;import java.io.FileWriter;import java.io.IOException;import java.time.LocalDateTime;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.SimpleChannelInboundHandler;import io.netty.channel.group.ChannelGroup;import io.netty.channel.group.DefaultChannelGroup;import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;import io.netty.util.concurrent.GlobalEventExecutor;import redis.clients.jedis.Jedis;public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{ private static ChannelGroup clients= new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { System.out.println(“channelRead0…”); //连接redis Jedis jedis=new Jedis(“localhost”); System.out.println(“连接成功…”); System.out.println(“服务正在运行:"+jedis.ping()); //得到用户输入的消息,需要写入文件/缓存中,让AI进行读取 String content=msg.text(); if(content==null||content==”") { System.out.println(“content 为null”); return ; } System.out.println(“接收到的消息:"+content); //写入缓存中 jedis.set(“user_say”, content+":user”); Thread.sleep(1000); //读取AI返回的内容 String AIsay=null; while(AIsay==“no”||AIsay==null) { //从缓存中读取AI回复的内容 AIsay=jedis.get(“ai_say”); String [] arr=AIsay.split(":"); AIsay=arr[0]; } //读取后马上向缓存中写入 jedis.set(“ai_say”, “no”); //没有说,或者还没说 if(AIsay==null||AIsay=="") { System.out.println(“AIsay==null||AIsay==""”); return; } System.out.println(“AI说:"+AIsay); clients.writeAndFlush( new TextWebSocketFrame( “AI_PigPig在”+LocalDateTime.now() +“说:"+AIsay)); } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { System.out.println(“add…”); clients.add(ctx.channel()); } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { System.out.println(“客户端断开,channel对应的长id为:” +ctx.channel().id().asLongText()); System.out.println(“客户端断开,channel对应的短id为:” +ctx.channel().id().asShortText()); } }在Python中引入Rediswith tf.Session() as sess:#打开作为一次会话 # 恢复前一次训练 ckpt = tf.train.get_checkpoint_state(’.’)#从检查点文件中返回一个状态(ckpt) #如果ckpt存在,输出模型路径 if ckpt != None: print(ckpt.model_checkpoint_path) model.saver.restore(sess, ckpt.model_checkpoint_path)#储存模型参数 else: print(“没找到模型”) r.set(‘user_say’,’no’) #测试该模型的能力 while True: line=‘no’ #从缓存中进行读取 while line==‘no’: line=r.get(‘user_say’).decode() #print(line) list1=line.split(’:’) if len(list1)==1: input_string=‘no’ else: input_string=list1[0] r.set(‘user_say’,’no’) # 退出 if input_string == ‘quit’: exit() if input_string != ’no’: input_string_vec = []#输入字符串向量化 for words in input_string.strip(): input_string_vec.append(vocab_en.get(words, UNK_ID))#get()函数:如果words在词表中,返回索引号;否则,返回UNK_ID bucket_id = min([b for b in range(len(buckets)) if buckets[b][0] > len(input_string_vec)])#保留最小的大于输入的bucket的id encoder_inputs, decoder_inputs, target_weights = model.get_batch({bucket_id: [(input_string_vec, [])]}, bucket_id) #get_batch(A,B):两个参数,A为大小为len(buckets)的元组,返回了指定bucket_id的encoder_inputs,decoder_inputs,target_weights _, _, output_logits = model.step(sess, encoder_inputs, decoder_inputs, target_weights, bucket_id, True) #得到其输出 outputs = [int(np.argmax(logit, axis=1)) for logit in output_logits]#求得最大的预测范围列表 if EOS_ID in outputs:#如果EOS_ID在输出内部,则输出列表为[,,,,:End] outputs = outputs[:outputs.index(EOS_ID)] response = “".join([tf.compat.as_str(vocab_de[output]) for output in outputs])#转为解码词汇分别添加到回复中 print(‘AI-PigPig > ’ + response)#输出回复 #向缓存中进行写入 r.set(‘ai_say’,response+’:AI’)下一节将讲述通信规则的制定,以规范应用程序。 ...

February 18, 2019 · 2 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App(五)

Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍。Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接。本章完整代码链接。本章主要讲的是聊天App_PigChat中关于聊天功能的实现。移除方法与处理异常方法的重写在ChatHandler中重写其移除channel的方法handlerRemoved,以及处理异常的方法exceptionCaught。 @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { String channelId = ctx.channel().id().asShortText(); System.out.println(“客户端被移除,channelId为:” + channelId); // 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel users.remove(ctx.channel()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); // 发生异常之后关闭连接(关闭channel),随后从ChannelGroup中移除 ctx.channel().close(); users.remove(ctx.channel()); }定义消息的实体类public class ChatMsg implements Serializable { private static final long serialVersionUID = 3611169682695799175L; private String senderId; // 发送者的用户id private String receiverId; // 接受者的用户id private String msg; // 聊天内容 private String msgId; // 用于消息的签收 public String getSenderId() { return senderId; } public void setSenderId(String senderId) { this.senderId = senderId; } public String getReceiverId() { return receiverId; } public void setReceiverId(String receiverId) { this.receiverId = receiverId; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public String getMsgId() { return msgId; } public void setMsgId(String msgId) { this.msgId = msgId; } }对实体类再做一层包装public class DataContent implements Serializable { private static final long serialVersionUID = 8021381444738260454L; private Integer action; // 动作类型 private ChatMsg chatMsg; // 用户的聊天内容entity private String extand; // 扩展字段 public Integer getAction() { return action; } public void setAction(Integer action) { this.action = action; } public ChatMsg getChatMsg() { return chatMsg; } public void setChatMsg(ChatMsg chatMsg) { this.chatMsg = chatMsg; } public String getExtand() { return extand; } public void setExtand(String extand) { this.extand = extand; }}定义发送消息的动作的枚举类型public enum MsgActionEnum { CONNECT(1, “第一次(或重连)初始化连接”), CHAT(2, “聊天消息”), SIGNED(3, “消息签收”), KEEPALIVE(4, “客户端保持心跳”), PULL_FRIEND(5, “拉取好友”); public final Integer type; public final String content; MsgActionEnum(Integer type, String content){ this.type = type; this.content = content; } public Integer getType() { return type; } }定义记录用户与channel关系的类/** * @Description: 用户id和channel的关联关系处理 /public class UserChannelRel { private static HashMap<String, Channel> manager = new HashMap<>(); public static void put(String senderId, Channel channel) { manager.put(senderId, channel); } public static Channel get(String senderId) { return manager.get(senderId); } public static void output() { for (HashMap.Entry<String, Channel> entry : manager.entrySet()) { System.out.println(“UserId: " + entry.getKey() + “, ChannelId: " + entry.getValue().id().asLongText()); } }}接受与处理消息方法的重写重写ChatHandler读取消息的channelRead0方法。具体步骤如下:(1)获取客户端发来的消息;(2)判断消息类型,根据不同的类型来处理不同的业务;(2.1)当websocket 第一次open的时候,初始化channel,把用的channel和userid关联起来;(2.2)聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收];然后实现消息的发送,首先从全局用户Channel关系中获取接受方的channel,然后当receiverChannel不为空的时候,从ChannelGroup去查找对应的channel是否存在,若用户在线,则使用writeAndFlush方法向其发送消息;(2.3)签收消息类型,针对具体的消息进行签收,修改数据库中对应消息的签收状态[已签收];(2.4)心跳类型的消息 // 用于记录和管理所有客户端的channle public static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { System.out.println(“read……….”); // 获取客户端传输过来的消息 String content = msg.text(); Channel currentChannel = ctx.channel(); // 1. 获取客户端发来的消息 DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class); Integer action = dataContent.getAction(); // 2. 判断消息类型,根据不同的类型来处理不同的业务 if (action == MsgActionEnum.CONNECT.type) { // 2.1 当websocket 第一次open的时候,初始化channel,把用的channel和userid关联起来 String senderId = dataContent.getChatMsg().getSenderId(); UserChannelRel.put(senderId, currentChannel); // 测试 for (Channel c : users) { System.out.println(c.id().asLongText()); } UserChannelRel.output(); } else if (action == MsgActionEnum.CHAT.type) { // 2.2 聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收] ChatMsg chatMsg = dataContent.getChatMsg(); String msgText = chatMsg.getMsg(); String receiverId = chatMsg.getReceiverId(); String senderId = chatMsg.getSenderId(); // 保存消息到数据库,并且标记为 未签收 UserService userService = (UserService)SpringUtil.getBean(“userServiceImpl”); String msgId = userService.saveMsg(chatMsg); chatMsg.setMsgId(msgId); DataContent dataContentMsg = new DataContent(); dataContentMsg.setChatMsg(chatMsg); // 发送消息 // 从全局用户Channel关系中获取接受方的channel Channel receiverChannel = UserChannelRel.get(receiverId); if (receiverChannel == null) { // TODO channel为空代表用户离线,推送消息(JPush,个推,小米推送) } else { // 当receiverChannel不为空的时候,从ChannelGroup去查找对应的channel是否存在 Channel findChannel = users.find(receiverChannel.id()); if (findChannel != null) { // 用户在线 receiverChannel.writeAndFlush( new TextWebSocketFrame( JsonUtils.objectToJson(dataContentMsg))); } else { // 用户离线 TODO 推送消息 } } } else if (action == MsgActionEnum.SIGNED.type) { // 2.3 签收消息类型,针对具体的消息进行签收,修改数据库中对应消息的签收状态[已签收] UserService userService = (UserService)SpringUtil.getBean(“userServiceImpl”); // 扩展字段在signed类型的消息中,代表需要去签收的消息id,逗号间隔 String msgIdsStr = dataContent.getExtand(); String msgIds[] = msgIdsStr.split(”,”); List<String> msgIdList = new ArrayList<>(); for (String mid : msgIds) { if (StringUtils.isNotBlank(mid)) { msgIdList.add(mid); } } System.out.println(msgIdList.toString()); if (msgIdList != null && !msgIdList.isEmpty() && msgIdList.size() > 0) { // 批量签收 userService.updateMsgSigned(msgIdList); } } else if (action == MsgActionEnum.KEEPALIVE.type) { // 2.4 心跳类型的消息 System.out.println(“收到来自channel为[” + currentChannel + “]的心跳包…”); } } 获取未签收的消息列表的接口在controller中添加获取未签收的消息列表的接口getUnReadMsgList。 /* * * @Description: 用户手机端获取未签收的消息列表 */ @PostMapping("/getUnReadMsgList") public IMoocJSONResult getUnReadMsgList(String acceptUserId) { // 0. userId 判断不能为空 if (StringUtils.isBlank(acceptUserId)) { return IMoocJSONResult.errorMsg(""); } // 查询列表 List<com.imooc.pojo.ChatMsg> unreadMsgList = userService.getUnReadMsgList(acceptUserId); return IMoocJSONResult.ok(unreadMsgList); }测试 ...

February 18, 2019 · 3 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App详解(四)

Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍。Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接。本章完整代码链接。本章内容(1) 查询好友列表的接口(2)通过或忽略好友请求的接口(3)添加好友功能展示查询好友列表的接口 /** * @Description: 查询我的好友列表 / @PostMapping("/myFriends") public IMoocJSONResult myFriends(String userId) { // 0. userId 判断不能为空 if (StringUtils.isBlank(userId)) { return IMoocJSONResult.errorMsg(""); } // 1. 数据库查询好友列表 List<MyFriendsVO> myFirends = userService.queryMyFriends(userId); return IMoocJSONResult.ok(myFirends); }通过或忽略好友请求的接口定义枚举类型/* * * @Description: 忽略或者通过 好友请求的枚举 /public enum OperatorFriendRequestTypeEnum { IGNORE(0, “忽略”), PASS(1, “通过”); public final Integer type; public final String msg; OperatorFriendRequestTypeEnum(Integer type, String msg){ this.type = type; this.msg = msg; } public Integer getType() { return type; } public static String getMsgByType(Integer type) { for (OperatorFriendRequestTypeEnum operType : OperatorFriendRequestTypeEnum.values()) { if (operType.getType() == type) { return operType.msg; } } return null; } }controller中提供通过或忽略好友请求的接口 /* * @Description: 接受方 通过或者忽略朋友请求 */ @PostMapping("/operFriendRequest") public IMoocJSONResult operFriendRequest(String acceptUserId, String sendUserId, Integer operType) { // 0. acceptUserId sendUserId operType 判断不能为空 if (StringUtils.isBlank(acceptUserId) || StringUtils.isBlank(sendUserId) || operType == null) { return IMoocJSONResult.errorMsg(""); } // 1. 如果operType 没有对应的枚举值,则直接抛出空错误信息 if (StringUtils.isBlank(OperatorFriendRequestTypeEnum.getMsgByType(operType))) { return IMoocJSONResult.errorMsg(""); } if (operType == OperatorFriendRequestTypeEnum.IGNORE.type) { // 2. 判断如果忽略好友请求,则直接删除好友请求的数据库表记录 userService.deleteFriendRequest(sendUserId, acceptUserId); } else if (operType == OperatorFriendRequestTypeEnum.PASS.type) { // 3. 判断如果是通过好友请求,则互相增加好友记录到数据库对应的表 // 然后删除好友请求的数据库表记录 userService.passFriendRequest(sendUserId, acceptUserId); } // 4. 数据库查询好友列表 List<MyFriendsVO> myFirends = userService.queryMyFriends(acceptUserId); // 5. 将查询到的好友列表返回给前端 return IMoocJSONResult.ok(myFirends); }添加好友功展示通过搜索好友姓名添加好友通过扫描二维码添加好友 ...

February 17, 2019 · 1 min · jiezi

集成Netty|tensorflow实现 聊天AI--PigPig养成记(2)

集成Netty项目github链接通过上一节的学习我们已经可以训练得到一只傲娇的聊天AI_PigPig了。本章将介绍项目关于Netty的集成问题,将其我们的AI_PigPig可以通过web应用与大家日常互撩。由于只是一个小测试,所以不考虑性能方面的问题,在下一章我们将重点处理效率难关,集成Redis。关于Netty的学习大家可以看我的另一篇文章,本节中关于Netty部分的代码改编自该文章中的netty聊天小练习,文章中会有详细的讲解。Python代码改动首先对测试训练结果的代码进行改动,将输入输出流重定向自作为中间媒介的测试文件中。完整代码链接with tf.Session() as sess:#打开作为一次会话 # 恢复前一次训练 ckpt = tf.train.get_checkpoint_state(’.’)#从检查点文件中返回一个状态(ckpt) #如果ckpt存在,输出模型路径 if ckpt != None: print(ckpt.model_checkpoint_path) model.saver.restore(sess, ckpt.model_checkpoint_path)#储存模型参数 else: print(“没找到模型”) #测试该模型的能力 while True: #从文件中进行读取 #input_string = input(‘me > ‘) #测试文件输入格式为"[内容]:[名字]" #eg.你好:AI【表示AI的回复】 #你好:user【表示用户的输入】 with open(’./temp.txt’,‘r+’,encoding=‘ANSI’) as myf: #从文件中读取用户的输入 line=myf.read() list1=line.split(’:’) #长度为一,表明不符合输入格式,设置为"no",则不进行测试处理 if len(list1)==1: input_string=‘no’ else: #符合输入格式,证明是用户输入的 #input_string为用户输入的内容 input_string=list1[0] myf.seek(0) #清空文件 myf.truncate() #写入"no",若读到"no",则不进行测试处理 myf.write(’no’) # 退出 if input_string == ‘quit’: exit() #若读到"no",则不进行测试处理 if input_string != ’no’: input_string_vec = []#输入字符串向量化 for words in input_string.strip(): input_string_vec.append(vocab_en.get(words, UNK_ID))#get()函数:如果words在词表中,返回索引号;否则,返回UNK_ID bucket_id = min([b for b in range(len(buckets)) if buckets[b][0] > len(input_string_vec)])#保留最小的大于输入的bucket的id encoder_inputs, decoder_inputs, target_weights = model.get_batch({bucket_id: [(input_string_vec, [])]}, bucket_id) #get_batch(A,B):两个参数,A为大小为len(buckets)的元组,返回了指定bucket_id的encoder_inputs,decoder_inputs,target_weights _, _, output_logits = model.step(sess, encoder_inputs, decoder_inputs, target_weights, bucket_id, True) #得到其输出 outputs = [int(np.argmax(logit, axis=1)) for logit in output_logits]#求得最大的预测范围列表 if EOS_ID in outputs:#如果EOS_ID在输出内部,则输出列表为[,,,,:End] outputs = outputs[:outputs.index(EOS_ID)] response = “".join([tf.compat.as_str(vocab_de[output]) for output in outputs])#转为解码词汇分别添加到回复中 print(‘AI-PigPig > ’ + response)#输出回复 #将AI的回复以要求的格式进行写入,方便Netty程序读取 with open(’./temp1.txt’,‘w’,encoding=‘ANSI’) as myf1: myf1.write(response+’:AI’)Netty程序完整代码参见链接netty包下。在原本的ChatHandler类中添加了从文件中读取数据的方法readFromFile,以及向文件中覆盖地写入数据的方法writeToFile。 //从文件中读取数据 private static String readFromFile(String filePath) { File file=new File(filePath); String line=null; String name=null; String content=null; try { //以content:name的形式写入 BufferedReader br=new BufferedReader(new FileReader(file)); line=br.readLine(); String [] arr=line.split(”:"); if(arr.length==1) { name=null; content=null; }else { content=arr[0]; name=arr[1]; } br.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return content; } //向文件中覆盖地写入 private static void writeToFile(String filePath,String content) { File file =new File(filePath); try { FileWriter fileWriter=new FileWriter(file); fileWriter.write(""); fileWriter.flush(); fileWriter.write(content); fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } }对原来的channelRead0方法进行修改,将输入输出流重定向到临时文件中。 @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { System.out.println(“channelRead0”); //得到用户输入的消息,需要写入文件/缓存中,让AI进行读取 String content=msg.text(); if(content==null||content=="") { System.out.println(“content 为null”); return ; } System.out.println(“接收到的消息:"+content); //写入 writeToFile(writeFilePath, content+":user”); //给AI回复与写入的时间,后期会增对性能方面进行改进 Thread.sleep(1000); //读取AI返回的内容 String AIsay=readFromFile(readFilePath); //读取后马上写入 writeToFile(readFilePath,“no”); //没有说,或者还没说 if(AIsay==null||AIsay==""||AIsay==“no”) { System.out.println(“AIsay为空或no”); return; } System.out.println(“AI说:"+AIsay); clients.writeAndFlush( new TextWebSocketFrame( “AI_PigPig在”+LocalDateTime.now() +“说:"+AIsay)); } 客户端代码<!DOCTYPE html><html> <head> <meta charset=“utf-8” /> <title></title> </head> <body> <div>发送消息:</div> <input type=“text” id=“msgContent”/> <input type=“button” value=“点我发送” onclick=“CHAT.chat()”/> <div>接受消息:</div> <div id=“receiveMsg” style=“background-color: gainsboro;"></div> <script type=“application/javascript”> window.CHAT = { socket: null, init: function() { if (window.WebSocket) { CHAT.socket = new WebSocket(“ws://192.168.0.104:8088/ws”); CHAT.socket.onopen = function() { console.log(“连接建立成功…”); }, CHAT.socket.onclose = function() { console.log(“连接关闭…”); }, CHAT.socket.onerror = function() { console.log(“发生错误…”); }, CHAT.socket.onmessage = function(e) { console.log(“接受到消息:” + e.data); var receiveMsg = document.getElementById(“receiveMsg”); var html = receiveMsg.innerHTML; receiveMsg.innerHTML = html + “<br/>” + e.data; } } else { alert(“浏览器不支持websocket协议…”); } }, chat: function() { var msg = document.getElementById(“msgContent”); CHAT.socket.send(msg.value); } }; CHAT.init(); </script> </body></html>测试结果客户端发送消息用户与AI日常互撩 ...

February 16, 2019 · 2 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App详解(三)

Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍。Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接。本章完整代码链接。本节主要讲解聊天App PigChat中关于好友申请的发送与接受。包含以下内容:(1)搜索好友接口(2)发送添加好友申请的接口(3)接受添加好友申请的接口搜索好友接口定义枚举类型 SearchFriendsStatusEnum,表示添加好友的前置状态 SUCCESS(0, “OK”), USER_NOT_EXIST(1, “无此用户…”), NOT_YOURSELF(2, “不能添加你自己…”), ALREADY_FRIENDS(3, “该用户已经是你的好友…”);在service中定义搜索朋友的前置条件判断的方法preconditionSearchFriends。传入的是用户的Id以及搜索的用户的名称。【1】首先根据搜索的用户的名称查找是否存在这个用户。【2】如果搜索的用户不存在,则返回[无此用户]。【3】如果搜索的用户是你自己,则返回[不能添加自己]。【4】如果搜索的用户已经是你的好友,则返回[该用户已经是你的好友]。【5】否则返回成功。 @Transactional(propagation = Propagation.SUPPORTS) @Override public Integer preconditionSearchFriends(String myUserId, String friendUsername) { //1. 查找要添加的朋友是否存在 Users user = queryUserInfoByUsername(friendUsername); // 2. 搜索的用户如果不存在,返回[无此用户] if (user == null) { return SearchFriendsStatusEnum.USER_NOT_EXIST.status; } // 3. 搜索账号是你自己,返回[不能添加自己] if (user.getId().equals(myUserId)) { return SearchFriendsStatusEnum.NOT_YOURSELF.status; } // 4. 搜索的朋友已经是你的好友,返回[该用户已经是你的好友] Example mfe = new Example(MyFriends.class); Criteria mfc = mfe.createCriteria(); mfc.andEqualTo(“myUserId”, myUserId); mfc.andEqualTo(“myFriendUserId”, user.getId()); MyFriends myFriendsRel = myFriendsMapper.selectOneByExample(mfe); if (myFriendsRel != null) { return SearchFriendsStatusEnum.ALREADY_FRIENDS.status; } //返回成功 return SearchFriendsStatusEnum.SUCCESS.status; }在controller中创建搜索好友接口 searchUser。传入的是用户的Id,以及要搜索的用户的名字。【0】首先判断传入的参数是否为空。【1】通过userService的preconditionSearchFriends方法得到前置条件。【2】如果搜索前置条件为成功,则向前端返回搜索用户的信息。【3】否则搜索失败。 /** * @Description: 搜索好友接口, 根据账号做匹配查询而不是模糊查询 / @PostMapping("/search") public IMoocJSONResult searchUser(String myUserId, String friendUsername) throws Exception { // 0. 判断 myUserId friendUsername 不能为空 if (StringUtils.isBlank(myUserId) || StringUtils.isBlank(friendUsername)) { return IMoocJSONResult.errorMsg(""); } // 前置条件 - 1. 搜索的用户如果不存在,返回[无此用户] // 前置条件 - 2. 搜索账号是你自己,返回[不能添加自己] // 前置条件 - 3. 搜索的朋友已经是你的好友,返回[该用户已经是你的好友] //1. 得到前置条件状态 Integer status = userService.preconditionSearchFriends(myUserId, friendUsername); //2. 搜索成功,返回搜索用户的信息 if (status == SearchFriendsStatusEnum.SUCCESS.status) { Users user = userService.queryUserInfoByUsername(friendUsername); UsersVO userVO = new UsersVO(); BeanUtils.copyProperties(user, userVO); return IMoocJSONResult.ok(userVO); } else { //3. 搜索失败 String errorMsg = SearchFriendsStatusEnum.getMsgByKey(status); return IMoocJSONResult.errorMsg(errorMsg); } }发送添加好友申请的接口在service中定义添加好友请求记录,保存到数据库的sendFriendRequest方法。传入的是添加好友记录的发送方——用户的Id,以及记录的接收方——想要添加的朋友的名称。【1】首先根据用户名把朋友信息查询出来。【2】然后查询发送好友请求记录表。【3】如果不是你的好友,并且好友记录没有添加,则新增好友请求记录。这样可以保证打你发送了两次请求之后,数据库中仍然只记录一次请求的数据。 @Transactional(propagation = Propagation.REQUIRED) @Override public void sendFriendRequest(String myUserId, String friendUsername) { // 1. 根据用户名把朋友信息查询出来 Users friend = queryUserInfoByUsername(friendUsername); // 2. 查询发送好友请求记录表 Example fre = new Example(FriendsRequest.class); Criteria frc = fre.createCriteria(); frc.andEqualTo(“sendUserId”, myUserId); frc.andEqualTo(“acceptUserId”, friend.getId()); FriendsRequest friendRequest = friendsRequestMapper.selectOneByExample(fre); if (friendRequest == null) { // 3. 如果不是你的好友,并且好友记录没有添加,则新增好友请求记录 String requestId = sid.nextShort(); FriendsRequest request = new FriendsRequest(); request.setId(requestId); request.setSendUserId(myUserId); request.setAcceptUserId(friend.getId()); request.setRequestDateTime(new Date()); friendsRequestMapper.insert(request); } }在controller中创建发送添加好友请求的接口。传入的是添加好友记录的发送方——用户的Id,以及记录的接收方——想要添加的朋友的名称。【0】首先判断传入参数不为空。【1】然后判断前置条件,若为成功则通过userService的sendFriendRequest方法发送好友请求,否则返回失败。 /* * @Description: 发送添加好友的请求 / @PostMapping("/addFriendRequest") public IMoocJSONResult addFriendRequest(String myUserId, String friendUsername) throws Exception { // 0. 判断 myUserId friendUsername 不能为空 if (StringUtils.isBlank(myUserId) || StringUtils.isBlank(friendUsername)) { return IMoocJSONResult.errorMsg(""); } // 前置条件 - 1. 搜索的用户如果不存在,返回[无此用户] // 前置条件 - 2. 搜索账号是你自己,返回[不能添加自己] // 前置条件 - 3. 搜索的朋友已经是你的好友,返回[该用户已经是你的好友] // 1. 判断前置条件 Integer status = userService.preconditionSearchFriends(myUserId, friendUsername); if (status == SearchFriendsStatusEnum.SUCCESS.status) { userService.sendFriendRequest(myUserId, friendUsername); } else { String errorMsg = SearchFriendsStatusEnum.getMsgByKey(status); return IMoocJSONResult.errorMsg(errorMsg); } return IMoocJSONResult.ok(); }最终实现效果接受添加好友申请的接口在service中定义查询好友请求列表的queryFriendRequestList方法。 @Transactional(propagation = Propagation.SUPPORTS) @Override public List<FriendRequestVO> queryFriendRequestList(String acceptUserId) { return usersMapperCustom.queryFriendRequestList(acceptUserId); }在controller中定义接受添加好友请求的接口queryFriendRequests。 /* * @Description: 发送添加好友的请求 */ @PostMapping("/queryFriendRequests") public IMoocJSONResult queryFriendRequests(String userId) { // 0. 判断不能为空 if (StringUtils.isBlank(userId)) { return IMoocJSONResult.errorMsg(""); } // 1. 查询用户接受到的朋友申请 return IMoocJSONResult.ok(userService.queryFriendRequestList(userId)); } 最终实现效果 ...

February 16, 2019 · 2 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App详解(一)

Netty学习Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍:https://segmentfault.com/a/11…Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接:https://github.com/ShimmerPig…本章练习完整代码链接:https://github.com/ShimmerPig…IO编程与NIO编程传统IO编程性能分析IO编程模型在客户端较少的情况下运行良好,但是对于客户端比较多的业务来说,单机服务端可能需要支撑成千上万的连接,IO模型可能就不太合适了。这是因为在传统的IO模型中,每个连接创建成功之后都需要一个线程来维护,每个线程包含一个while死循环,那么1w个连接对应1w个线程,继而1w个while死循环,这就带来如下几个问题:1.线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起。2.线程切换效率低下:单机cpu核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。3.除了以上两个问题,IO编程中,我们看到数据读写是以字节流为单位,效率不高。为了解决这三个问题,JDK在1.4之后提出了NIO。下面简单描述一下NIO是如何解决以上三个问题的。线程资源受限NIO编程模型中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接所有的读写都由这个线程来负责。这个过程的实现归功于NIO模型中selector的作用,一条连接来了之后,现在不创建一个while死循环去监听是否有数据可读了,而是直接把这条连接注册到selector上,然后,通过检查这个selector,就可以批量监测出有数据可读的连接,进而读取数据。线程切换效率低下由于NIO模型中线程数量大大降低,线程切换效率因此也大幅度提高。IO读写以字节为单位NIO解决这个问题的方式是数据读写不再以字节为单位,而是以字节块为单位。IO模型中,每次都是从操作系统底层一个字节一个字节地读取数据,而NIO维护一个缓冲区,每次可以从这个缓冲区里面读取一块的数据。hello netty完整代码链接:https://github.com/ShimmerPig…首先定义一对线程组——主线程bossGroup与从线程workerGroup。bossGroup——用于接受客户端的连接,但是不做任何处理,跟老板一样,不做事。workerGroup——bossGroup会将任务丢给他,让workerGroup去处理。//主线程EventLoopGroup bossGroup = new NioEventLoopGroup();//从线程EventLoopGroup workerGroup = new NioEventLoopGroup();定义服务端的启动类serverBootstrap,需要设置主从线程,NIO的双向通道,与子处理器(用于处理workerGroup),这里的子处理器后面我们会手动创建。// netty服务器的创建, ServerBootstrap 是一个启动类 ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) // 设置主从线程组 .channel(NioServerSocketChannel.class) // 设置nio的双向通道 .childHandler(new HelloServerInitializer()); // 子处理器,用于处理workerGroup启动服务端,绑定8088端口,同时设置启动的方式为同步的,这样我们的Netty就会一直等待,直到该端口启动完毕。ChannelFuture channelFuture = serverBootstrap.bind(8088).sync();监听关闭的通道channel,设置为同步方式。channelFuture.channel().closeFuture().sync();将两个线程优雅地关闭。bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();创建管道channel的子处理器HelloServerInitializer,用于处理workerGroup。HelloServerInitializer里面只重写了initChannel方法,是一个初始化器,channel注册后,会执行里面相应的初始化方法。在initChannel方法中通过SocketChannel获得对应的管道,通过该管道添加相关助手类handler。HttpServerCodec是由netty自己提供的助手类,可以理解为拦截器,当请求到服务端,我们需要做解码,响应到客户端做编码。添加自定义的助手类customHandler,返回"hello netty~“ChannelPipeline pipeline = channel.pipeline();pipeline.addLast(“HttpServerCodec”, new HttpServerCodec());pipeline.addLast(“customHandler”, new CustomHandler());创建自定义的助手类CustomHandler继承SimpleChannelInboundHandler,返回hello netty重写channelRead0方法,首先通过传入的上下文对象ChannelHandlerContext获取channel,若消息类型为http请求,则构建一个内容为"hello netty“的http响应,通过上下文对象的writeAndFlush方法将响应刷到客户端。if (msg instanceof HttpRequest) { // 显示客户端的远程地址 System.out.println(channel.remoteAddress()); // 定义发送的数据消息 ByteBuf content = Unpooled.copiedBuffer(“Hello netty~”, CharsetUtil.UTF_8); // 构建一个http response FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); // 为响应增加数据类型和长度 response.headers().set(HttpHeaderNames.CONTENT_TYPE, “text/plain”); response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); // 把响应刷到客户端 ctx.writeAndFlush(response);}访问8088端口,返回"hello netty~“netty聊天小练习完整代码链接:https://github.com/ShimmerPig…服务器定义主从线程与服务端的启动类public class WSServer { public static void main(String[] args) throws Exception { EventLoopGroup mainGroup = new NioEventLoopGroup(); EventLoopGroup subGroup = new NioEventLoopGroup(); try { ServerBootstrap server = new ServerBootstrap(); server.group(mainGroup, subGroup) .channel(NioServerSocketChannel.class) .childHandler(new WSServerInitialzer()); ChannelFuture future = server.bind(8088).sync(); future.channel().closeFuture().sync(); } finally { mainGroup.shutdownGracefully(); subGroup.shutdownGracefully(); } } }创建channel的子处理器WSServerInitialzer加入相关的助手类handlerpublic class WSServerInitialzer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); // websocket 基于http协议,所以要有http编解码器 pipeline.addLast(new HttpServerCodec()); // 对写大数据流的支持 pipeline.addLast(new ChunkedWriteHandler()); // 对httpMessage进行聚合,聚合成FullHttpRequest或FullHttpResponse // 几乎在netty中的编程,都会使用到此hanler pipeline.addLast(new HttpObjectAggregator(1024*64)); // ====================== 以上是用于支持http协议 ====================== // ====================== 以下是支持httpWebsocket ====================== /** * websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : /ws * 本handler会帮你处理一些繁重的复杂的事 * 会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳 * 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同 */ pipeline.addLast(new WebSocketServerProtocolHandler("/ws”)); // 自定义的handler pipeline.addLast(new ChatHandler()); }}创建自定义的助手类ChatHandler,用于处理消息。TextWebSocketFrame:在netty中,是用于为websocket专门处理文本的对象,frame是消息的载体。创建管道组ChannelGroup,用于管理所有客户端的管道channel。private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);重写channelRead0方法,通过传入的TextWebSocketFrame获取客户端传入的内容。通过循环的方法对ChannelGroup中所有的channel进行回复。@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { // 获取客户端传输过来的消息 String content = msg.text(); System.out.println(“接受到的数据:” + content);// for (Channel channel: clients) {// channel.writeAndFlush(// new TextWebSocketFrame(// “[服务器在]” + LocalDateTime.now() // + “接受到消息, 消息为:” + content));// } // 下面这个方法,和上面的for循环,一致 clients.writeAndFlush( new TextWebSocketFrame( “[服务器在]” + LocalDateTime.now() + “接受到消息, 消息为:” + content));}重写handlerAdded方法,当客户端连接服务端之后(打开连接),获取客户端的channle,并且放到ChannelGroup中去进行管理。@Overridepublic void handlerAdded(ChannelHandlerContext ctx) throws Exception { clients.add(ctx.channel());}重写handlerRemoved方法,当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel。@Overridepublic void handlerRemoved(ChannelHandlerContext ctx) throws Exception { // 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel // clients.remove(ctx.channel()); System.out.println(“客户端断开,channle对应的长id为:” + ctx.channel().id().asLongText()); System.out.println(“客户端断开,channle对应的短id为:” + ctx.channel().id().asShortText());}客户端<!DOCTYPE html><html> <head> <meta charset=“utf-8” /> <title></title> </head> <body> <div>发送消息:</div> <input type=“text” id=“msgContent”/> <input type=“button” value=“点我发送” onclick=“CHAT.chat()”/> <div>接受消息:</div> <div id=“receiveMsg” style=“background-color: gainsboro;"></div> <script type=“application/javascript”> window.CHAT = { socket: null, init: function() { if (window.WebSocket) { CHAT.socket = new WebSocket(“ws://192.168.1.4:8088/ws”); CHAT.socket.onopen = function() { console.log(“连接建立成功…”); }, CHAT.socket.onclose = function() { console.log(“连接关闭…”); }, CHAT.socket.onerror = function() { console.log(“发生错误…”); }, CHAT.socket.onmessage = function(e) { console.log(“接受到消息:” + e.data); var receiveMsg = document.getElementById(“receiveMsg”); var html = receiveMsg.innerHTML; receiveMsg.innerHTML = html + “<br/>” + e.data; } } else { alert(“浏览器不支持websocket协议…”); } }, chat: function() { var msg = document.getElementById(“msgContent”); CHAT.socket.send(msg.value); } }; CHAT.init(); </script> </body></html>测试 ...

February 13, 2019 · 2 min · jiezi

netty极简教程(一):从helloworld到编写一个聊天室

<!– more –>netty介绍Nowadays we use general purpose applications or libraries to communicate with each other. For example, we often use an HTTP client library to retrieve information from a web server and to invoke a remote procedure call via web services. However, a general purpose protocol or its implementation sometimes does not scale very well. It is like how we don’t use a general purpose HTTP server to exchange huge files, e-mail messages, and near-realtime messages such as financial information and multiplayer game data. What’s required is a highly optimized protocol implementation that is dedicated to a special purpose. For example, you might want to implement an HTTP server that is optimized for AJAX-based chat application, media streaming, or large file transfer. You could even want to design and implement a whole new protocol that is precisely tailored to your need. Another inevitable case is when you have to deal with a legacy proprietary protocol to ensure the interoperability with an old system. What matters in this case is how quickly we can implement that protocol while not sacrificing the stability and performance of the resulting application.这是netty的官方介绍,大概意思就是:我们经常希望我们的应用能够和其它应用互相通信。例如,我们经常使用http请求去查询信息或者使用rpc调用webservice,但是对于这种特定的协议(http,ftp等)来说,是不易于专门针对自己应用程序进行扩展的。比方说我们不会使用http协议去传输大文件,邮件,即时通讯(金融信息),这需要对现有协议做出较大的优化!这样我们就可以使用netty定制属于你自己的协议!为什么要学netty?这里借用知乎上一个回答:作为一个学Java的,如果没有研究过Netty,那么你对Java语言的使用和理解仅仅停留在表面水平,会点SSH,写几个MVC,访问数据库和缓存,这些只是初等Java程序员干的事。如果你要进阶,想了解Java服务器的深层高阶知识,Netty绝对是一个必须要过的门槛。有了Netty,你可以实现自己的HTTP服务器,FTP服务器,UDP服务器,RPC服务器,WebSocket服务器,Redis的Proxy服务器,MySQL的Proxy服务器等等。如果你想知道Nginx是怎么写出来的,如果你想知道Tomcat和Jetty,Dubbo是如何实现的,如果你也想实现一个简单的Redis服务器,那都应该好好理解一下Netty,它们高性能的原理都是类似的。 while ture events = takeEvents(fds) // 获取事件,如果没有事件,线程就休眠 for event in events { if event.isAcceptable { doAccept() // 新链接来了 } elif event.isReadable { request = doRead() // 读消息 if request.isComplete() { doProcess() } } elif event.isWriteable { doWrite() // 写消息 } } }NIO的流程大致就是上面的伪代码描述的过程,跟实际真实的代码有较多差异,不过对于初学者,这样理解也是足够了。Netty是建立在NIO基础之上,Netty在NIO之上又提供了更高层次的抽象。在Netty里面,Accept连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理。Accept连接和读写操作也可以使用同一个线程池来进行处理。而请求处理逻辑既可以使用单独的线程池进行处理,也可以跟放在读写线程一块处理。线程池中的每一个线程都是NIO线程。用户可以根据实际情况进行组装,构造出满足系统需求的并发模型。Netty提供了内置的常用编解码器,包括行编解码器[一行一个请求],前缀长度编解码器[前N个字节定义请求的字节长度],可重放解码器[记录半包消息的状态],HTTP编解码器,WebSocket消息编解码器等等Netty提供了一些列生命周期回调接口,当一个完整的请求到达时,当一个连接关闭时,当一个连接建立时,用户都会收到回调事件,然后进行逻辑处理。Netty可以同时管理多个端口,可以使用NIO客户端模型,这些对于RPC服务是很有必要的。Netty除了可以处理TCP Socket之外,还可以处理UDP Socket。在消息读写过程中,需要大量使用ByteBuffer,Netty对ByteBuffer在性能和使用的便捷性上都进行了优化和抽象。总之,Netty是Java程序员进阶的必备神奇。如果你知其然,还想知其所以然,一定要好好研究下Netty。如果你觉得Java枯燥无谓,Netty则是重新开启你对Java兴趣大门的钥匙。总结:程序员水平进阶的利器!实践note: 对于本例中除了非常重要的核心类会讲解外,其他类不会过多讲解,本章只做入门,其它章节会重点讲解!我们已经知道了netty的作用(灵活优化定制你自己的协议),以及为什么要学习netty。那接下来我们就一步一步来定制自己的协议最后完成聊天室!print协议既然我们取名print协议,那就是打印的意思:服务端接受客服端的信息并且打印!首先我们编写一个ChannelInboundHandlerAdapter,用于处理接收到的消息,我们首先分析下这个类的作用,继承关系如下:它的作用简单概括就是:用于处理 I/O事件的处理器,所以本例我们自然是用它来处理消息,于是乎有了如下类:PrintServerHandler:public class EchoServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2) ByteBuf byteBuf = (ByteBuf) msg; System.out.println(byteBuf.toString(Charset.forName(“utf-8”))); ctx.writeAndFlush(msg); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4) // Close the connection when an exception is raised. cause.printStackTrace(); ctx.close(); }}收到消息后打印,接着继续编写一个启动类,用于启动一个开启我们自己协议的服务,PrintServerApp:public class EchoServerApp { private int port; public EchoServerApp(int port) { this.port = port; } public void run() throws Exception { NioEventLoopGroup bossLoopGroup = new NioEventLoopGroup(); NioEventLoopGroup workLoopGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossLoopGroup, workLoopGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new EchoServerHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture channelFuture = serverBootstrap.bind(port).sync(); channelFuture.channel().closeFuture().sync(); } finally { bossLoopGroup.shutdownGracefully(); workLoopGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { new EchoServerApp(8080).run(); }}启动。然后我们使用win自带的telnet工具来测试(控制面板-》程序和控制-》开启或关闭window功能,勾选telnet)。打开cmd,输入telnet localhost 8080测试成功,我们完成了第一个demo,实现了自己的print协议。接下来我们把客户端也换成netty编写。目的:启动客户端,获取服务端时间,叫time协议。Time Protocol首先同上面一样,写一个TimeServerHandler:public class TimeServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ByteBuf timeBuf = ctx.alloc().buffer(); timeBuf.writeBytes(new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”).format(new Date()).getBytes()); ChannelFuture channelFuture = ctx.writeAndFlush(timeBuf); channelFuture.addListener(new GenericFutureListener<Future<? super Void>>() { @Override public void operationComplete(Future<? super Void> future) throws Exception { assert channelFuture == future; // ctx.close(); } }); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4) // Close the connection when an exception is raised. cause.printStackTrace(); ctx.close(); }}启动类同上,接下来,编写客户端TimeClientHandler:public class TimeClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { try { ByteBuf byteBuf = (ByteBuf) msg; int length = byteBuf.readableBytes(); byte[] buff = new byte[1024]; byteBuf.readBytes(buff, 0, length); System.out.println(“current time: " + new String(buff, 0, length)); ctx.close(); } finally { ReferenceCountUtil.release(msg); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // Close the connection when an exception is raised. cause.printStackTrace(); ctx.close(); }}分别启动服务端,客户端。测试结果如图,客户端启动后拿到了服务端的时间,这样我们就实现了自己的time protocol,接下来继续扩展,编写一个客户端与服务端通信的聊天室:chatroom server首先,客户端与服务端通信的信息我们抽象出一个对象,Message以及工具类:@Data@NoArgsConstructor@AllArgsConstructorpublic class Message { private String username; private Date sentTime; private String msg;}public class Utils { public static String encodeMsg(Message message) { return message.getUsername() + “” + (new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”).format(message.getSentTime())) + “” + message.getMsg(); } public static String formatDateTime(Date time) { return new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”).format(time); } public static Date parseDateTime(String time) { try { return new SimpleDateFormat(“yyyy-MM-dd Hh:mm:ss”).parse(time); } catch (ParseException e) { return null; } } public static void printMsg(Message msg) { System.out.println("=================================================================================================”); System.out.println(" " + Utils.formatDateTime(msg.getSentTime()) + " “); System.out.println(msg.getUsername() + “: " + msg.getMsg()); System.out.println("=================================================================================================”); }}三个属性分别代表用户名,发送时间,消息内容,接着编写一个用于处理输入消息的handler,用于将byte消息转换成Message,ServerTransferMsgHandler:public class ServerTransferMsgHandler extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { String totalMsg = in.readCharSequence(in.readableBytes(), Charset.forName(“utf-8”)).toString(); String[] content = totalMsg.split("”); out.add(new Message(content[0], Utils.parseDateTime(content[1]), content[2])); }}接着,编写一个处理接收消息的Handler,用于打印客户端发送过来的消息,ServerMsgHandler:public class ServerMsgHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println(“jsbintask-client进入聊天室。”); Message message = new Message(Constants.SERVER, new Date(), “Hello, I’m jsbintask-server side.”); ByteBuf buffer = ctx.alloc().buffer(); String content = Utils.encodeMsg(message); buffer.writeBytes(content.getBytes()); ctx.writeAndFlush(buffer); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg1) throws Exception { try { Message msg = (Message) msg1; Utils.printMsg(msg); Scanner scanner = new Scanner(System.in); System.out.print(“jsbintask-server, please input msg: “); String reply = scanner.nextLine(); Message message = new Message(Constants.SERVER, new Date(), reply); ctx.writeAndFlush(message); } finally { ReferenceCountUtil.release(msg1); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); }}知道注意的是,channelActive方法,在客户端链接的时候,率先给客户端发送了一条消息,最后,在编写一个用户将服务端Message转成Byte消息的handler,MessageEncoder:public class MessageEncoder extends MessageToByteEncoder<Message> { @Override protected void encode(ChannelHandlerContext ctx, Message message, ByteBuf out) throws Exception { ByteBuf buffer = ctx.alloc().buffer(); String content = Utils.encodeMsg(message); buffer.writeBytes(content.getBytes(StandardCharsets.UTF_8)); ctx.writeAndFlush(buffer); }}最后,编写server端启动类,ChatroomServerApp:public class ChatroomServerApp { public static void main(String[] args) throws Exception { NioEventLoopGroup bossGroup = new NioEventLoopGroup(); NioEventLoopGroup workGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer() { @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast(new MessageEncoder(), new ServerTransferMsgHandler(), new ServerMsgHandler()); } }) .option(ChannelOption.SO_BACKLOG, 1024 * 10) .childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture channelFuture = serverBootstrap.bind(8888).sync(); channelFuture.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } }}启动Server,继续编写ChatroomClient。chatroom client同server端一样,client的关键也是handler,ClientMsgHandler如下:public class ClientMsgHandler extends SimpleChannelInboundHandler<Message> { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { super.channelActive(ctx); } @Override protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception { try { Utils.printMsg(msg); Scanner scanner = new Scanner(System.in); System.out.print(“jsbintask-client, please input msg: “); String reply = scanner.nextLine(); Message message = new Message(Constants.CLIENT, new Date(), reply); ByteBuf buffer = ctx.alloc().buffer(); String content = message.getUsername() + “” + Utils.formatDateTime(message.getSentTime()) + “” + message.getMsg(); buffer.writeBytes(content.getBytes(StandardCharsets.UTF_8)); ctx.writeAndFlush(buffer); } finally { ReferenceCountUtil.release(msg); } }}接着,同样有将byte转换成Message的转换器,CliengMsgHandler:public class ClientTransferMsgHandler extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { byte[] buff = new byte[2024]; int length = in.readableBytes(); in.readBytes(buff, 0, length); String totalMsg = new String(buff, 0, length, StandardCharsets.UTF_8); String[] content = totalMsg.split("”); out.add(new Message(content[0], Utils.parseDateTime(content[1]), content[2])); }}最后,启动类ChatroomClientApp:public class ChatroomClientApp { public static void main(String[] args) throws Exception { NioEventLoopGroup workLoopGroup = new NioEventLoopGroup(); try { Bootstrap clientBootstrap = new Bootstrap(); clientBootstrap.group(workLoopGroup) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new ClientTransferMsgHandler(), new ClientMsgHandler()); } }) .option(ChannelOption.SO_KEEPALIVE, true); ChannelFuture channelFuture = clientBootstrap.connect(“localhost”, 8888).sync(); channelFuture.channel().closeFuture().sync(); } finally { workLoopGroup.shutdownGracefully(); } }}同样启动client,观察控制台。首先,server端提示client进入了聊天室,并且客户端看到了server端发送过来的”招呼“信息:这样就代表我们的链接建立完毕,接着,客户端,服务端相互发送消息:如图,这样,我们的聊天室也就编写成功了,完整demo如下:总结本章,我们开启了学习netty的大门,首先介绍了netty,为什么要学netty,并且通过三个案例一步一步实现了聊天室,成功踏入了netty的大门,下一章,我们就来学习一下netty的架构!例子源码:https://github.com/jsbintask22/netty-learning.git,欢迎fork,star学习修改。本文原创地址:https://jsbintask.cn/2019/01/30/netty/netty-chatroom/,转载请注明出处。如果你觉得本文对你有用,欢迎关注,分享! ...

January 31, 2019 · 5 min · jiezi

netty搭建web聊天室(3)单聊

上节课讲了群聊,这次来说说单聊,单聊要比群聊复杂点,但是代码也不是很多,主要是前端显示比较麻烦点。效果:登陆首先一个新的用户,需要先登陆,输入自己的昵称,然后点击登陆。后端服务会把你的用户名和当前的线程进行邦定,这样就可以通过你的用户名找到你的线程。登陆成功,后端返回定义好的消息 success,前端判断记录CHAT.me,这样给别人发消息时就可以携带自己的信息。查找用户在输入框输入用户名,就可以返回对应的用户的线程,这样你就可以把消息发送给你要聊天的对象。如果不存在,后端回返回消息给前端,该用户不存在。如果存在,就记录此用户名到CHAT.to中,这样你发送消息的时候就可以发送给对应用户了。开始聊天发送聊天信息时me:to:消息,这样后端就知道是谁要发给谁,根据用户名去找到具体的线程去单独推送消息,实现单聊。前端待完善左侧聊天列表没有实现,每搜索一个在线用户,应该动态显示在左侧,点击该用户,动态显示右侧聊天窗口进行消息发送。现在是你和所有人的单聊消息都会显示在右侧,没有完成拆分,因为这是一个页面,处理起来比较麻烦,我一个后端就不花时间搞了,感兴趣的可以自己去实现。前端代码因为注视比较详细,就直接复制整个代码到这里,大家自己看。<!DOCTYPE html><html> <head> <meta charset=“utf-8”> <title>单人聊天</title> <link rel=“stylesheet” href=“https://cdnjs.cloudflare.com/ajax/libs/zui/1.8.1/css/zui.min.css"> <link rel=“stylesheet” href=“zui-theme.css”> </head> <body> <div class=“container”> <div class=“row”> <h1>mike单人聊天室,等你来聊</h1></div> <div class=“row”> <div class=“input-control has-icon-left has-icon-right” style=“width:50%;"> <input id=“userName” type=“text” class=“form-control” placeholder=“聊天昵称”> <label for=“inputEmailExample1” class=“input-control-icon-left”><i class=“icon icon-user”></i></label> <label for=“inputEmailExample1” class=“input-control-icon-right”><a onclick=“login()">登陆</a></label> </div> </div> <br> <div class=“row”> <div class=“input-control search-box search-box-circle has-icon-left has-icon-right” id=“searchUser”> <input id=“inputSearch” type=“search” class=“form-control search-input” placeholder=“输入在线好友昵称聊天…enter开始查找”> <label for=“inputSearchExample1” class=“input-control-icon-left search-icon”><i class=“icon icon-search”></i></label> <a href=”#” class=“input-control-icon-right search-clear-btn”><i class=“icon icon-remove”></i></a> </div> </div> <hr> <div class=“row”> <div class=“col-lg-3”> <p class=“with-padding bg-success”>聊天列表</p> <div class=“list-group”><a href=”#" class=“list-group-item”><h4 class=“list-group-item-heading”><i class=“icon-user icon-2x”></i>&nbsp;&nbsp;may</h4></a><a href="#" class=“list-group-item active”><h4 class=“list-group-item-heading”><i class=“icon-user icon-2x”></i>&nbsp;&nbsp;steve</h4></a> </div> </div> <div class=“col-lg-1”></div> <div class=“col-lg-8”> <div class=“comments”> <section class=“comments-list” id=“chatlist”> </section> <footer> <div class=“reply-form” id=“commentReplyForm1”> <a href="###" class=“avatar”><i class=“icon-user icon-2x”></i></a> <form class=“form”> <div class=“form-group”> <textarea id=“inputMsg” class=“form-control new-comment-text” rows=“2” value="" placeholder=“开始聊天… 输入enter 发送消息”></textarea> </div> </form> </div> </footer> </div> </div> </div> </div> <!– ZUI Javascript 依赖 jQuery –> <script src=“https://cdnjs.cloudflare.com/ajax/libs/zui/1.8.1/lib/jquery/jquery.js"></script> <!– ZUI 标准版压缩后的 JavaScript 文件 –> <script src=“https://cdnjs.cloudflare.com/ajax/libs/zui/1.8.1/js/zui.min.js"></script> <script type=“text/javascript”> window.CHAT = { isLogin: false, to: “”, me: “”, WS:{}, init: function () { if (window.WebSocket) { this.WS = new WebSocket(“ws://A156B7L58CCNY4B:8090/ws”); this.WS.onmessage = function(event) { var data = event.data; console.log(“收到数据:” + data); //返回搜索消息 if(data.indexOf(“search”) != -1){ new $.zui.Messager(‘提示消息:’+data, { type: ‘info’ // 定义颜色主题 }).show(); if(data.indexOf(“已找到”)){ //可以进行会话 CHAT.to = data.split(”:”)[1]; } } //返回登陆消息 if(data == “success”){ CHAT.isLogin = true; new $.zui.Messager(‘提示消息:登陆成功’, { type: ‘success’ // 定义颜色主题 }).show(); //连接成功不再修改昵称 $("#userName").attr(“disabled”,“disabled”); CHAT.me = $("#userName").val(); } //返回聊天信息 if (data.split(":").length==3 && CHAT.me == data.split(":")[1]) { CHAT.to = data.split(":")[0]; //设置对话 appendOtherchat(data); } }, this.WS.onclose = function(event) { console.log(“连接关闭”); CHAT.isLogin = false; $("#userName").removeAttr(“disabled”); new $.zui.Messager(‘提示消息:聊天中断’, { type: ‘danger’ // 定义颜色主题 }).show(); }, this.WS.onopen = function(evt) { console.log(“Connection open …”); }, this.WS.onerror = function(event) { console.log(“连接失败….”); CHAT.isLogin = false; $("#userName").removeAttr(“disabled”); new $.zui.Messager(‘提示消息:聊天中断’, { type: ‘danger’ // 定义颜色主题 }).show(); } } else { alert(“您的浏览器不支持聊天,请更换浏览器”); } }, chat:function (msg) { this.WS.send(msg); } } CHAT.init(); function login() { var userName = $("#userName").val(); if (userName != null && userName !=’’) { //初始化聊天 CHAT.chat(“init:"+userName); } else { alert(“请输入用户名登录”); } } function Trim(str) { return str.replace(/(^\s*)|(\s*$)/g, “”); } function appendMy (msg) { //拼接自己的聊天内容 document.getElementById(‘chatlist’).innerHTML+="<div class=‘comment’><a class=‘avatar pull-right’><i class=‘icon-user icon-2x’></i></a><div class=‘content pull-right’><div><strong>我</strong></div><div class=‘text’>"+msg+"</div></div></div>”; } function appendOtherchat(msg) { //拼接别人的聊天信息到聊天室 var msgs = msg.split(":"); document.getElementById(‘chatlist’).innerHTML+="<div class=‘comment’><a class=‘avatar’><i class=‘icon-user icon-2x’></i></a><div class=‘content’><div><strong>"+msgs[0]+"</strong></div><div class=‘text’>"+msgs[2]+"</div></div></div>"; } //搜索在线人员发送消息 document.getElementById(“inputSearch”).addEventListener(‘keyup’, function(event) { if (event.keyCode == “13”) { //回车执行查询 CHAT.chat(“search:"+$(’#inputSearch’).val()); } }); //发送聊天消息 document.getElementById(‘inputMsg’).addEventListener(‘keyup’, function(event) { if (event.keyCode == “13”) { //回车执行查询 var inputMsg = $(’#inputMsg’).val(); if (inputMsg == null || Trim(inputMsg) == "” ) { alert(“请输入聊天消息”); } else { var userName = $(’#userName’).val(); if (userName == null || userName == ‘’) { alert(“请输入聊天昵称”); } else { //发送消息 定义消息格式 me:to:[消息] CHAT.chat(userName+":"+CHAT.to+":"+inputMsg); appendMy(inputMsg); //发送完清空输入 document.getElementById(‘inputMsg’).focus(); document.getElementById(‘inputMsg’).value=""; } } } }); </script> </body></html>后端改造加入一个UserMap,邦定user和Channelpackage netty;import java.util.HashMap;import java.util.Map;import io.netty.channel.Channel;/** * The class UserMap /public class UserMap { private HashMap<String, Channel> users = new HashMap(); private static UserMap instance; public static UserMap getInstance () { if (instance == null) { instance = new UserMap(); } return instance; } private UserMap () { } public void addUser(String userId, Channel ch) { this.users.put(userId, ch); } public Channel getUser (String userId) { return this.users.get(userId); } public void deleteUser (Channel ch) { for (Map.Entry<String, Channel> map: users.entrySet()) { if (map.getValue() == ch) { users.remove(map.getKey()); break; } } }}ChatHandler改造package netty;import java.time.LocalDateTime;import io.netty.buffer.Unpooled;import io.netty.channel.Channel;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.SimpleChannelInboundHandler;import io.netty.channel.group.ChannelGroup;import io.netty.channel.group.DefaultChannelGroup;import io.netty.handler.codec.http.DefaultFullHttpResponse;import io.netty.handler.codec.http.FullHttpRequest;import io.netty.handler.codec.http.FullHttpResponse;import io.netty.handler.codec.http.HttpHeaderValues;import io.netty.handler.codec.http.HttpResponseStatus;import io.netty.handler.codec.http.HttpVersion;import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;import io.netty.handler.codec.http.websocketx.WebSocketFrame;import io.netty.util.concurrent.GlobalEventExecutor;/* * /public class ChatHandler extends SimpleChannelInboundHandler{ public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); public static UserMap usermap = UserMap.getInstance(); /* * 每当从服务端收到新的客户端连接时,客户端的 Channel 存入ChannelGroup列表中,并通知列表中的其他客户端 Channel / @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { Channel incoming = ctx.channel(); for (Channel channel : channels) { channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + " 加入\n"); } channels.add(ctx.channel()); } /* * 每当从服务端收到客户端断开时,客户端的 Channel 移除 ChannelGroup 列表中,并通知列表中的其他客户端 Channel / @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { Channel incoming = ctx.channel(); for (Channel channel : channels) { channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + " 离开\n"); } channels.remove(ctx.channel()); } /* * 会话建立时 / @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // (5) Channel incoming = ctx.channel(); System.out.println(“ChatClient:"+incoming.remoteAddress()+“在线”); } /* * 会话结束时 / @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { // (6) Channel incoming = ctx.channel(); System.out.println(“ChatClient:"+incoming.remoteAddress()+“掉线”); //清除离线用户 this.usermap.deleteUser(incoming); } /* * 出现异常 / @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (7) Channel incoming = ctx.channel(); System.out.println(“ChatClient:"+incoming.remoteAddress()+“异常”); // 当出现异常就关闭连接 cause.printStackTrace(); ctx.close(); } /* * 读取客户端发送的消息,并将信息转发给其他客户端的 Channel。 */ @Override protected void channelRead0(ChannelHandlerContext ctx, Object request) throws Exception { if (request instanceof FullHttpRequest) { //是http请求 FullHttpResponse response = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1,HttpResponseStatus.OK , Unpooled.wrappedBuffer(“Hello netty” .getBytes())); response.headers().set(“Content-Type”, “text/plain”); response.headers().set(“Content-Length”, response.content().readableBytes()); response.headers().set(“connection”, HttpHeaderValues.KEEP_ALIVE); ctx.channel().writeAndFlush(response); } else if (request instanceof TextWebSocketFrame) { // websocket请求 //此处id为neety自动分配给每个对话线程的id,有两种,一个长id一个短id,长id唯一,短id可能会重复 String userId = ctx.channel().id().asLongText(); //客户端发送过来的消息 String msg = ((TextWebSocketFrame)request).text(); System.out.println(“收到客户端”+userId+”:"+msg); //发送消息给所有客户端 群聊 //channels.writeAndFlush(new TextWebSocketFrame(msg)); // 邦定user和channel // 定义每个上线用户主动发送初始化信息过来,携带自己的name,然后完成绑定 模型 init:[usrname] // 实际场景中应该使用user唯一id if (msg.indexOf(“init”) != -1) { String userNames[] = msg.split(”:”); if (“init”.equals(userNames[0])) { // 记录新的用户 this.usermap.addUser(userNames[1].trim(), ctx.channel()); ctx.channel().writeAndFlush(new TextWebSocketFrame(“success”)); } } //搜索在线用户 消息模型 search:[username] if (msg.indexOf(“search”) != -1) { Channel ch = this.usermap.getUser(msg.split(":")[1].trim()); if (ch != null) { //此用户存在 ctx.channel().writeAndFlush(new TextWebSocketFrame(“search:"+msg.split(”:")[1].trim()+":已找到")); } else { // 此用户不存在 ctx.channel().writeAndFlush(new TextWebSocketFrame(“search:"+msg.split(”:")[1].trim()+":未找到")); } } //发送消息给指定的用户 消息模型 me:to:[msg] if (msg.split(":").length == 3) { //判断是单聊消息 this.usermap.getUser(msg.split(":")[1].trim()).writeAndFlush(new TextWebSocketFrame(msg)); } //ctx.channel().writeAndFlush(new TextWebSocketFrame(((TextWebSocketFrame)request).text())); } }}注释很详细,自己看总结消息模型应该定义一个单独的类来管理,我目前是用的String字符串来判断,提前规定了一些模型,通过判断来响应前端的请求,比较简单。还有就是没有使用数据库,前端不能显示聊天记录,不能实现消息的已读未读。实际场景中应该对消息进行加密存储,且不能窥探用户隐私。前端可以使用localstorage来存储聊天记录,自己可以扩展。前端的显示可能有点问题,自己可以调。其实主要是学习netty后端的搭建别忘了关注我 mike啥都想搞求关注啊。 ...

January 23, 2019 · 4 min · jiezi

长连接的心跳及重连设计

前言说道“心跳”这个词大家都不陌生,当然不是指男女之间的心跳,而是和长连接相关的。顾名思义就是证明是否还活着的依据。什么场景下需要心跳呢?目前我们接触到的大多是一些基于长连接的应用需要心跳来“保活”。由于在长连接的场景下,客户端和服务端并不是一直处于通信状态,如果双方长期没有沟通则双方都不清楚对方目前的状态;所以需要发送一段很小的报文告诉对方“我还活着”。同时还有另外几个目的:服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道,让它下线。客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。正好借着在 cim有这样两个需求来聊一聊。心跳实现方式心跳其实有两种实现方式:TCP 协议实现(keepalive 机制)。应用层自己实现。由于 TCP 协议过于底层,对于开发者来说维护性、灵活度都比较差同时还依赖于操作系统。所以我们这里所讨论的都是应用层的实现。如上图所示,在应用层通常是由客户端发送一个心跳包 ping 到服务端,服务端收到后响应一个 pong 表明双方都活得好好的。一旦其中一端延迟 N 个时间窗口没有收到消息则进行不同的处理。客户端自动重连先拿客户端来说吧,每隔一段时间客户端向服务端发送一个心跳包,同时收到服务端的响应。常规的实现应当是:开启一个定时任务,定期发送心跳包。收到服务端响应后更新本地时间。再有一个定时任务定期检测这个“本地时间”是否超过阈值。超过后则认为服务端出现故障,需要重连。这样确实也能实现心跳,但并不友好。在正常的客户端和服务端通信的情况下,定时任务依然会发送心跳包;这样就显得没有意义,有些多余。所以理想的情况应当是客户端收到的写消息空闲时才发送这个心跳包去确认服务端是否健在。好消息是 Netty 已经为我们考虑到了这点,自带了一个开箱即用的 IdleStateHandler 专门用于心跳处理。来看看 cim 中的实现:在 pipeline 中加入了一个 10秒没有收到写消息的 IdleStateHandler,到时他会回调 ChannelInboundHandler 中的 userEventTriggered 方法。所以一旦写超时就立马向服务端发送一个心跳(做的更完善应当在心跳发送失败后有一定的重试次数);这样也就只有在空闲时候才会发送心跳包。但一旦间隔许久没有收到服务端响应进行重连的逻辑应当写在哪里呢?先来看这个示例:当收到服务端响应的 pong 消息时,就在当前 Channel 上记录一个时间,也就是说后续可以在定时任务中取出这个时间和当前时间的差额来判断是否超过阈值。超过则重连。同时在每次心跳时候都用当前时间和之前服务端响应绑定到 Channel 上的时间相减判断是否需要重连即可。也就是 heartBeatHandler.process(ctx); 的执行逻辑。伪代码如下:@Overridepublic void process(ChannelHandlerContext ctx) throws Exception { long heartBeatTime = appConfiguration.getHeartBeatTime() * 1000; Long lastReadTime = NettyAttrUtil.getReaderTime(ctx.channel()); long now = System.currentTimeMillis(); if (lastReadTime != null && now - lastReadTime > heartBeatTime){ reconnect(); }}IdleStateHandler 误区一切看起来也没毛病,但实际上却没有这样实现重连逻辑。最主要的问题还是对 IdleStateHandler 理解有误。我们假设下面的场景:客户端通过登录连上了服务端并保持长连接,一切正常的情况下双方各发心跳包保持连接。这时服务端突入出现 down 机,那么理想情况下应当是客户端迟迟没有收到服务端的响应从而 userEventTriggered 执行定时任务。判断当前时间 - UpdateWriteTime > 阈值 时进行重连。但却事与愿违,并不会执行 2、3两步。因为一旦服务端 down 机、或者是与客户端的网络断开则会回调客户端的 channelInactive 事件。IdleStateHandler 作为一个 ChannelInbound 也重写了 channelInactive() 方法。这里的 destroy() 方法会把之前开启的定时任务都给取消掉。所以就不会再有任何的定时任务执行了,也就不会有机会执行这个重连业务。靠谱实现因此我们得有一个单独的线程来判断是否需要重连,不依赖于 IdleStateHandler。于是 cim 在客户端感知到网络断开时就会开启一个定时任务:之所以不在客户端启动就开启,是为了节省一点线程消耗。网络问题虽然不可避免,但在需要的时候开启更能节省资源。在这个任务重其实就是执行了重连,限于篇幅具体代码就不贴了,感兴趣的可以自行查阅。同时来验证一下效果。启动两个服务端,再启动客户端连接上一台并保持长连接。这时突然手动关闭一台服务,客户端可以自动重连到可用的那台服务节点。启动客户端后服务端也能收到正常的 ping 消息。利用 :info 命令查看当前客户端的链接状态发现连的是 9000端口。:info 是一个新增命令,可以查看一些客户端信息。这时我关掉连接上的这台节点。kill -9 2142这时客户端会自动重连到可用的那台节点。这个节点也收到了上线日志以及心跳包。服务端自动剔除离线客户端现在来看看服务端,它要实现的效果就是延迟 N 秒没有收到客户端的 ping 包则认为客户端下线了,在 cim 的场景下就需要把他踢掉置于离线状态。消息发送误区这里依然有一个误区,在调用 ctx.writeAndFlush() 发送消息获取回调时。其中是 isSuccess 并不能作为消息发送成功与否的标准。也就是说即便是客户端直接断网,服务端这里发送消息后拿到的 success 依旧是 true。这是因为这里的 success 只是告知我们消息写入了 TCP 缓冲区成功了而已。和我之前有着一样错误理解的不在少数,这是 Netty 官方给的回复。相关 issue:https://github.com/netty/netty/issues/4915同时感谢 95老徐以及闪电侠的一起排查。所以我们不能依据此来关闭客户端的连接,而是要像上文一样判断 Channel 上绑定的时间与当前时间只差是否超过了阈值。以上则是 cim 服务端的实现,逻辑和开头说的一致,也和 Dubbo 的心跳机制有些类似。于是来做个试验:正常通信的客户端和服务端,当我把客户端直接断网时,服务端会自动剔除客户端。总结这样就实现了文初的两个要求。服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道,让它下线。客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。同时也踩了两个误区,坑一个人踩就可以了,希望看过本文的都有所收获避免踩坑。本文所有相关代码都在此处,感兴趣的可以自行查看:https://github.com/crossoverJie/cim如果本文对你有所帮助还请不吝转发。 ...

January 23, 2019 · 1 min · jiezi

netty搭建web聊天室(2)群聊

上节课完成了netty的后端搭建,搞定了简单的http请求响应,今天来结合前端websocket来完成群聊功能。话不多说先上图:前端构建不使用复杂构建工具直接静态页面走起使用了zui样式库 http://zui.sexy/?#/,非常不错,有好多模板。我使用的是聊天模板改造 <link rel=“stylesheet” href=“https://cdnjs.cloudflare.com/ajax/libs/zui/1.8.1/css/zui.min.css"> <link rel=“stylesheet” href=“zui-theme.css”>主体部分<div class=“container”> <h1>mike多人聊天室,等你来聊</h1> <div class=“comments”> <section class=“comments-list” id=“chatlist”> <div class=“comment”> <a href=”###" class=“avatar”> <i class=“icon-user icon-2x”></i> </a> <div class=“content”> <div><strong>其他人</strong></div> <div class=“text”>其他人的聊天内容</div> </div> </div> <div class=“comment”> <a href="###" class=“avatar pull-right”> <i class=“icon-user icon-2x”></i> </a> <div class=“content pull-right”> <div><strong>我</strong></div> <div class=“text”>我说话的内容</div> </div> </div> </section> <footer> <div class=“reply-form” id=“commentReplyForm1”> <form class=“form”> <div class=“form-group”> <div class=“input-control has-label-left”> <input id=“userName” type=“text” class=“form-control” placeholder=""> <label for=“inputAccountExample2” class=“input-control-label-left”>昵称:</label> </div> </div> </form> <a href="###" class=“avatar”><i class=“icon-user icon-2x”></i></a> <form class=“form”> <div class=“form-group”> <textarea id=“inputMsg” class=“form-control new-comment-text” rows=“2” value="" placeholder=“开始聊天… 输入enter 发送消息”></textarea> </div> </form> </div> </footer></div></div>引入依赖js <!– ZUI Javascript 依赖 jQuery –> <script src=“https://cdnjs.cloudflare.com/ajax/libs/zui/1.8.1/lib/jquery/jquery.js"></script> <!– ZUI 标准版压缩后的 JavaScript 文件 –> <script src=“https://cdnjs.cloudflare.com/ajax/libs/zui/1.8.1/js/zui.min.js"></script>websocket的js代码以及业务代码 <script type=“text/javascript”> window.CHAT = { me: “”, WS:{}, init: function () { if (window.WebSocket) { this.WS = new WebSocket(“ws://A156B7L58CCNY4B:8090/ws”); this.WS.onmessage = function(event) { var data = event.data; console.log(“收到数据:” + data); //显示其他人的聊天信息 console.log(CHAT.me); console.log(data.split(”:”)[0]); if(CHAT.me != data.split(":")[0]) { appendOtherchat(data); } }, this.WS.onclose = function(event) { console.log(“连接关闭”); }, this.WS.onopen = function(evt) { console.log(“Connection open …”); }, this.WS.onerror = function(event) { console.log(“连接失败….”); } } else { alert(“您的浏览器不支持聊天,请更换浏览器”); } }, chat:function (msg) { this.WS.send(msg); } } CHAT.init(); function Trim(str) { return str.replace(/(^\s*)|(\s*$)/g, “”); } function appendMy (msg) { //拼接自己的聊天内容 document.getElementById(‘chatlist’).innerHTML+="<div class=‘comment’><a class=‘avatar pull-right’><i class=‘icon-user icon-2x’></i></a><div class=‘content pull-right’><div><strong>我</strong></div><div class=‘text’>"+msg+"</div></div></div>"; } function appendOtherchat(msg) { //拼接别人的聊天信息到聊天室 var msgs = msg.split(":"); document.getElementById(‘chatlist’).innerHTML+="<div class=‘comment’><a class=‘avatar’><i class=‘icon-user icon-2x’></i></a><div class=‘content’><div><strong>"+msgs[0]+"</strong></div><div class=‘text’>"+msgs[1]+"</div></div></div>"; } document.getElementById(‘inputMsg’).addEventListener(‘keyup’, function(event) { if (event.keyCode == “13”) { //回车执行查询 var inputMsg = document.getElementById(‘inputMsg’).value; if (inputMsg == null || Trim(inputMsg) == "" ) { alert(“请输入聊天消息”); } else { var userName = document.getElementById(‘userName’).value; if (userName == null || userName == ‘’) { alert(“请输入聊天昵称”); } else { //发送消息 定义消息格式 用户名:[消息] CHAT.chat(userName+":"+inputMsg); //记录我的昵称 CHAT.me = userName; appendMy(inputMsg); //发送完清空输入 document.getElementById(‘inputMsg’).focus(); document.getElementById(‘inputMsg’).value=""; } } } }); </script>都有注释就不解释了自己看后端服务改造ChatHandler改造,判断websocket响应 /** * 读取客户端发送的消息,并将信息转发给其他客户端的 Channel。 / @Override protected void channelRead0(ChannelHandlerContext ctx, Object request) throws Exception { if (request instanceof FullHttpRequest) { //是http请求 FullHttpResponse response = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1,HttpResponseStatus.OK , Unpooled.wrappedBuffer(“Hello netty” .getBytes())); response.headers().set(“Content-Type”, “text/plain”); response.headers().set(“Content-Length”, response.content().readableBytes()); response.headers().set(“connection”, HttpHeaderValues.KEEP_ALIVE); ctx.channel().writeAndFlush(response); } else if (request instanceof TextWebSocketFrame) { // websocket请求 String userId = ctx.channel().id().asLongText(); System.out.println(“收到客户端”+userId+":"+((TextWebSocketFrame)request).text()); //发送消息给所有客户端 channels.writeAndFlush(new TextWebSocketFrame(((TextWebSocketFrame)request).text())); //发送给单个客户端 //ctx.channel().writeAndFlush(new TextWebSocketFrame(((TextWebSocketFrame)request).text())); } } ChatServerInitializer改造,加入WebSocket @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); //websocket协议本身是基于http协议的,所以这边也要使用http解编码器 pipeline.addLast(new HttpServerCodec()); //以块的方式来写的处理器 pipeline.addLast(new ChunkedWriteHandler()); //netty是基于分段请求的,HttpObjectAggregator的作用是将请求分段再聚合,参数是聚合字节的最大长度 pipeline.addLast(new HttpObjectAggregator(102410241024)); //ws://server:port/context_path //ws://localhost:9999/ws //参数指的是contex_path pipeline.addLast(new WebSocketServerProtocolHandler("/ws",null,true,65535)); //自定义handler pipeline.addLast(new ChatHandler()); System.out.println(“ChatClient:"+ch.remoteAddress() +“连接上”); }改造完成启动后端服务,访问你的前端静态页面就可以和小伙伴聊天了。其实后端群聊很简单,就是把一个用户的输入消息,返回给所有在线客户端,前端去负责筛选显示。自己动手照着搞10分钟就能完成。实现功能输入聊天昵称开始聊天聊天消息不为空才能发送发送完自动清空输入,且聚焦输入框自己的消息显示在左侧,其他人的消息在右侧别忘了关注我 mike啥都想搞求关注啊。 ...

January 22, 2019 · 2 min · jiezi

为自己搭建一个分布式 IM 系统二【从查找算法聊起】

前言最近这段时间确实有点忙,这篇的目录还是在飞机上敲出来了的。言归正传,上周更新了 cim 第一版;没想到反响热烈,最高时上了 GitHub Trending Java 版块的首位,一天收到了 300+ 的 star。现在总共也有 1.3K+ 的 star,有几十个朋友参加了测试,非常感谢大家的支持。在这过程中也收到一些 bug 反馈,feature 建议;因此这段时间我把一些影响较大的 bug 以及需求比较迫切的 feature 调整了,本次更新的 v1.0.1 版本:客户端超时自动下线。新增 AI 模式。聊天记录查询。在线用户前缀模糊匹配。下面谈下几个比较重点的功能。客户端超时自动下线 这个功能涉及到客户端和服务端的心跳设计,比较有意思,也踩了几个坑;所以准备留到下次单独来聊。AI 模式大家应该还记得这个之前刷爆朋友圈的 估值两个一个亿的 AI 核心代码。和我这里的场景再合适不过了。于是我新增了一个命令用于一键开启 AI 模式,使用情况大概如下。欢迎大家更新源码体验,融资的请私聊我????。聊天记录聊天记录也是一个比较迫切的功能。使用命令 :q 关键字 即可查询与个人相关的聊天记录。这个功能其实比较简单,只需要在消息发送及接收消息时保存即可。但要考虑的一点是,这个保存消息是 IO 操作,不可避免的会有耗时;需要尽量避免对消息发送、接收产生影响。异步写入消息因此我把消息写入的过程异步完成,可以不影响真正的业务。实现起来也挺简单,就是一个典型的生产者消费者模式。主线程收到消息之后直接写入队列,另外再有一个线程一直源源不断的从队列中取出数据后保存聊天记录。大概的代码如下:写入消息的同时会把消费消息的线程打开:而最终存放消息记录的策略,考虑后还是以最简单的方式存放在客户端,可以降低复杂度。简单来说就是根据当前日期+用户名写入到磁盘里。当客户端关闭时利用线程中断的方式停止了消费队列的线程。这点的设计其实和 logback 写日志的方式比较类似,感兴趣的可以去翻翻 logback 的源码,更加详细。回调接口至于收到其他客户端发来的消息时则是利用之前预留的消息回调接口来写入日志。收到消息后会执行自定义的回调接口。于是在这个回调方法中实现写入逻辑即可,当后续还有其他的消息处理逻辑时也能在这里直接添加。当处理逻辑增多时最好是改为责任链模式,更加清晰易维护。查找算法接下来是本文着重要讨论的一个查找算法,准确的说是一个前缀模糊匹配的算法。实现的效果如下:使用命令 :qu prefix 可以按照前缀的方式搜索用户信息。当然在命令行中其实意义不大,但是在移动端中确是比较有用的。类似于微信按照用户名匹配:因为后期打算出一个移动端 APP,所以就先把这个功能实现了。从效果也看得出来:就是按照输入的前缀匹配字符串(目前只支持英文)。在没有任何限制的条件下最快、最简单的实现方式可以直接把所有的字符串存放在一个容器中 (List、Set),查询时则挨个遍历;利用 String.startsWith(“prefix”) 进行匹配。但这样会有几个问题:存储资源比较浪费,不管是 list 还是 Set 都会有额外的损耗。查询效率较低,需要遍历集合后再遍历字符串的 char 数组(String.startsWith 的实现方式)。字典树基于以上的问题我们可以考虑下:假设我需要存放 java,javascript,jsp,php 这些字符串时在 ArrayList 中会怎么存放?很明显,会是这样完整的存放在一个数组中;同时这个数组还可能存在浪费,没有全部使用完。但其实仔细观察这些数据会发现有一些共同特点,比如 java,javascript 有共同的前缀 java;和 jsp 有共同的前缀 j。那是否可以把这些前缀利用起来呢?这样就可以少存储一份。比如写入 java,javascript 这两个字符串时存放的结构如下:当再存入一个 jsp 时:最后再存入 jsf 时:相信大家应该已经看明白了,按照这样的存储方式可以节省很多内存,同时查询效率也比较高。比如查询以 jav 开头的数据,只需要从头结点 j 开始往下查询,最后会查询到 ava 以及 script 这两个个结点,所以整个查询路径所经历的字符拼起来就是查询到的结果java+javascript。如果以 b 开头进行查询,那第一步就会直接返回,这样比在 list 中的效率高很多。但这个图还不完善,因为不知道查询到啥时候算是匹配到了一个之前写入的字符串。比如在上图中怎么知道 j+ava 是一个我们之前写入的 java 这个字符呢。因此我们需要对这种是一个完整字符串的数据打上一个标记:比如这样,我们将 ava、script、p、f 这几个节点都换一个颜色表示。表明查询到这个字符时就算是匹配到了一个结果。而查到 s 这个字符颜色不对,代表还需要继续往下查。比如输入关键字 js 进行匹配时,当它的查询路径走到 s 这里时判断到 s 的颜色不对,所以不会把 js 作为一个匹配结果。而是继续往下查,发现有两个子节点 p、f 颜色都正确,于是把查询的路径 jsp 和 jsf 都作为一个匹配结果。而只输入 j,则会把下面所有有色的字符拼起来作为结果集合。这其实就一个典型的字典树。具体实现下面则是具体的代码实现,其实算法不像是实现一个业务功能这样好用文字分析;具体还是看源码多调试就明白了。谈下几个重点的地方吧:字典树的节点实现,其中的 isEnd 相当于图中的上色。利用一个 Node[] children 来存放子节点。为了可以区分大小写查询,所以子节点的长度相当于是 26*2。写入数据这里以一个单测为例,写入了三个字符串,那最终形成的数据结构如下:图中有与上图有几点不同:每个节点都是一个字符,这样树的高度最高为52。每个节点的子节点都是长度为 52 的数组;所以可以利用数组的下标表示他代表的字符值。比如 0 就是大 A,26 则是小 a,以此类推。有点类似于之前提到的布隆过滤器,可以节省内存。debug 时也能看出符合上图的数据结构:所以真正的写入步骤如下:把字符串拆分为 char 数组,并判断大小写计算它所存放在数组中的位置 index。将当前节点的子节点数组的 index 处新增一个节点。如果是最后一个字符就将新增的节点置为最后一个节点,也就是上文的改变节点颜色。最后将当前节点指向下一个节点方便继续写入。查询总的来说要麻烦一些,其实就是对树进行深度遍历;最终的思想看图就能明白。所以在 cim 中进行模糊匹配时就用到了这个结构。字典树的源码在此处:https://github.com/crossoverJie/cim/blob/master/cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/TrieTree.java其实利用这个结构还能实现判断某个前缀的单词是否在某堆数据里、某个前缀的单词出现的次数等。总结目前 cim 还在火热内测中(虽然群里只有20几人),感兴趣的朋友可以私聊我拉你入伙☺️ 再没有新的 BUG 产生前会着重把这些功能完成了,不出意外下周更新 cim 的心跳重连等机制。完整源码:https://github.com/crossoverJie/cim如果这篇对你有所帮助还请不吝转发。 ...

January 14, 2019 · 1 min · jiezi

我是怎样阅读 Netty Channel 源码的

Channel 功能说明我在使用 ServerBootstrap 来创建服务的时候通过 channel(NioServerSocketChannel.class) 来设置 Channel, 那么 Channel 的主要作用是什么呢?在分析 Channel 之前, 需要先弄懂 NioServerSocketChannel 类, 它的功能对于 JDK NIO 类库中的 ServerSocketChannel 类, 它是用来监听TCP连接的(并不管读写). 而 NioServerSocketChannel 最主要就是实现 Channel 接口. Channel 接口, 采用 Facade 模式进行统一封装, 将网络的读、写, 客户端发起连接, 主动关闭连接, 链路关闭, 获取通信双方的网络地址等.Channel 接口下有一个重要的抽象类 AbstractChannel, 一些公共的基础方法都在这个抽象类中实现, 而特定一些功能, 都可以通过各个不同的实现类去实现, 这样最大程度的实现功能和接口的重用.为什么不实用 JDK NIO 原生的 Channel 而要另起炉灶呢?JDK SocketChannel 和 ServerSocketChannel 没有统一的 Channel.JDK 的 SocketChannel 和 ServerSocketChannel 的主要职责就是网络 IO 操作, 由于他们的 SPI 类接口, 由具体的虚拟机厂家提供, 所以通过继承 SPI 功能类来扩展其功能的难度很大; SocketChannel 和 ServerSocketChannel 抽象类, 其工作量和重新开发一个新的 Channel 功能类是差不多的.Netty 的 Channel 需要能够跟 Netty 的整体架构融合在一起, 例如 IO 模型、基于 ChannelPipeline 的定制模型, 以及基于元数据描述配置化的 TCP 参数等, 这些 JDK 的 SocketChannel 和 ServerSocketChannel 都没有提供, 需要重新封装.自定义的 Channel, 功能实现更加灵活.Channel 方法介绍Channel read(): 从当前的 Channel 中读取数据到第一个 inbound 缓冲区中, 如果数据被成功读取, 触发 ChannelHandler.channelRead(ChannelHandlerContext, Object) 事件.读取操作 API 调用完成之后, 紧接着会触发 ChannelHandler.channelReadComplete(ChannelHandlerContext) 事件, 这样业务的 ChannelHandler 可以决定是否需要继承读取数据. 如果已经有读操作请求被挂起, 则后续的读操作会被忽略.ChannelFuture write(Object): 请求将当前的 msg 通过 ChannelPipeline 写入到目标 Channel 中. 注意, write 操作只是将消息存入到消息发送环数组中, 并没有真正被发送, 只有调用 flush 操作才会被写入到 Channel 中, 发送给对方.ChannelFuture write(Object, ChannelPromise): 功能与 Channel read() 相同, 但是携带了 ChannelPromise 参数负责设置写入操作的结果.ChannelFuture writeAndFlush(Objec, ChannelPromise): 与上面方法功能类似, 不同之处在于它会将消息写入 Channel 中发送, 等价于单独调用 write 和 flush 操作的组合.Channel flush(): 将之前写入到发送环形中的消息全部写入到目标 Channel 中, 发送给通信对方.ChannelFuture close(ChannelPromise): 主动关闭当前连接, 通过 ChannelPromise 设置操作结果并执行结果通知, 无论操作是否成功, 都可以通过 ChannelPromise 获取操作结果. 该操作会级联触发 ChannelPipeline 中所有 ChannelHandler 的 ChannelHandler.close(ChannelHandlerContext, ChannelPromise) 事件.ChannelFuture disconnect(ChannelPromise): 请求断开与远程通信对端的连接并使用 ChannelPromise 来获取操作结果的通知消息. 该方法会级联触发 ChannelHandler.disconnect(ChannelHandlerContext, ChannelPromise) 事件.ChannelFuture connect(SocketAddress remoteAddress): 客户端使用指定的服务端地址 remoteAddress 发起连接请求, 如果连接因为应答超时而失败, ChannelFuture 中的操作结果就是 ConnectTimeoutException 异常; 如果连接被拒绝, 操作结果为 ConnectException. 该方法会级联触发 ChannelHandler.connect(ChannelHandlerContext, SocketAddress, SocketAddress, ChannelPromise) 事件.ChannelFuture bind(SocketAddress localAddress): 绑定指定的本地 Socket 地址 localAddress, 该方法会级联触发 ChannelHandler.bind(ChannelHandlerContext, SocketAddress, ChannelPromise) 事件.ChannelConfig config(): 获取当前 Channel 的配置信息, 例如 CONNECT_TIMEOUT_MILLIS.boolean isOpen(): 判断当前 Channel 是否已经打开.boolean isRegistered(): 判断当前 Channel 是否已经注册到 EventLoop 上.boolean isActive(): 判断当前 Channel 是否已经处于激活状态.ChannelMetadata metadata(): 获取当前 Channel 的元数据描述信息, 包括 TCP 参数配置等.SocketAddress localAddress(): 获取当前 Channel 的本地绑定地址.SocketAddress remoteAddress(): 获取当前 Channel 通信的远程 Socket 地址.eventLoop(): Channel 需要注册到 EventLoop 的多路复用器上, 用于处理 IO 事件, 通过 eventLoop() 方法可以获取到 Channel 上注册的 EventLoop. EventLoop 本质上就是处理网络读写事件的 Reactor 线程. 也可以用来执行定时任务和用户自定义 NioTask 等任务.id(): 它返回 ChannelId 对象, 是 Channel 的唯一标识, 它的可能生成策略如下:机器的 MAC 地址(EUI-48 或 EUI-64) 等可以代表全局唯一的信息;当前的进程 ID;当前系统的毫秒– System.currentTimeMillis();当前系统的纳秒– System.nanoTime();32 位的随机整数;32 位自增的序列数.AbstractChannel 源码分析AstractChannel 聚合了所有 Channel 使用到的对象, 由 AbstractChannel 提供初始化和统一封装, 如果功能和具体子类相关, 则定义成抽象方法由子类具体实现.static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException();static final NotYetConnectedException NOT_YET_CONNECTED_EXCEPTION = new NotYetConnectedException();static { CLOSED_CHANNEL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); NOT_YET_CONNECTED_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);}private MessageSizeEstimator.Handle estimatorHandle;private final Channel parent;private final ChannelId id = DefaultChannelId.newInstance();private final Unsafe unsafe;private final DefaultChannelPipeline pipeline;private final ChannelFuture succeededFuture = new SucceededChannelFuture(this, null);private final VoidChannelPromise voidPromise = new VoidChannelPromise(this, true);private final VoidChannelPromise unsafeVoidPromise = new VoidChannelPromise(this, false);private final CloseFuture closeFuture = new CloseFuture(this);….protected AbstractChannel(Channel parent, EventLoop eventLoop) { this.parent = parent; this.eventLoop = validate(eventLoop); unsafe = newUnsafe(); pipeline = new DefaultChannelPipeline(this);}每一个 Channel 中包含一个 ChannelPipeline. 在 AbstractChannel 的构造方法中初始化. AbstractChannel 也提供了一些公共 API 的具体实现, 例如 localAddress() 和 remoteAddress().当 Channel 进行 IO 操作时会产生对应的 IO 事件, 然后驱动事件在 ChannelPipeline 中传播, 由对应的 ChannelHandler 对事件进行拦截和处理, 不关心的事件可以直接忽略.网络 IO 操作直接调用 DefaultChannelPipeline 的相关方法, 由 DefaultChannelPipeline 中对应的 ChannelHandler 进行具体逻辑的处理. ...

January 3, 2019 · 2 min · jiezi

为自己搭建一个分布式 IM(即时通讯) 系统

前言大家新年快乐!新的一年第一篇技术文章希望开个好头,所以元旦三天我也没怎么闲着,希望给大家带来一篇比较感兴趣的干货内容。老读者应该还记得我在去年国庆节前分享过一篇《设计一个百万级的消息推送系统》;虽然我在文中有贴一些伪代码,依然有些朋友希望能直接分享一些可以运行的源码;这么久了是时候把坑填上了。目录结构:本文较长,高能预警;带好瓜子板凳。于是在之前的基础上我完善了一些内容,先来看看这个项目的介绍吧:CIM(CROSS-IM) 一款面向开发者的 IM(即时通讯)系统;同时提供了一些组件帮助开发者构建一款属于自己可水平扩展的 IM 。借助 CIM 你可以实现以下需求:IM 即时通讯系统。适用于 APP 的消息推送中间件。IOT 海量连接场景中的消息透传中间件。完整源码托管在 GitHub : https://github.com/crossoverJie/cim演示本次主要涉及到 IM 即时通讯,所以特地录了两段视频演示(群聊、私聊)。点击下方链接可以查看视频版 Demo。YouTubeBilibili群聊 私聊群聊 私聊也在公网部署了一套演示环境,想要试一试的可以联系我加入内测群获取账号一起尬聊????。架构设计下面来看看具体的架构设计。CIM 中的各个组件均采用 SpringBoot 构建。采用 Netty + Google Protocol Buffer 构建底层通信。Redis 存放各个客户端的路由信息、账号信息、在线状态等。Zookeeper 用于 IM-server 服务的注册与发现。整体主要由以下模块组成:cim-serverIM 服务端;用于接收 client 连接、消息透传、消息推送等功能。支持集群部署。cim-forward-route消息路由服务器;用于处理消息路由、消息转发、用户登录、用户下线以及一些运营工具(获取在线用户数等)。cim-clientIM 客户端;给用户使用的消息终端,一个命令即可启动并向其他人发起通讯(群聊、私聊);同时内置了一些常用命令方便使用。流程图整体的流程也比较简单,流程图如下:客户端向 route 发起登录。登录成功从 Zookeeper 中选择可用 IM-server 返回给客户端,并保存登录、路由信息到 Redis。客户端向 IM-server 发起长连接,成功后保持心跳。客户端下线时通过 route 清除状态信息。所以当我们自己部署时需要以下步骤:搭建基础中间件 Redis、Zookeeper。部署 cim-server,这是真正的 IM 服务器,为了满足性能需求所以支持水平扩展,只需要注册到同一个 Zookeeper 即可。部署 cim-forward-route,这是路由服务器,所有的消息都需要经过它。由于它是无状态的,所以也可以利用 Nginx 代理提高可用性。cim-client 真正面向用户的客户端;启动之后会自动连接 IM 服务器便可以在控制台收发消息了。更多使用介绍可以参考快速启动。详细设计接下来重点看看具体的实现,比如群聊、私聊消息如何流转;IM 服务端负载均衡;服务如何注册发现等等。IM 服务端先来看看服务端;主要是实现客户端上下线、消息下发等功能。首先是服务启动:由于是在 SpringBoot 中搭建的,所以在应用启动时需要启动 Netty 服务。从 pipline 中可以看出使用了 Protobuf 的编解码(具体报文在客户端中分析)。注册发现需要满足 IM 服务端的水平扩展需求,所以 cim-server 是需要将自身数据发布到注册中心的。这里参考之前分享的《搞定服务注册与发现》有具体介绍。所以在应用启动成功后需要将自身数据注册到 Zookeeper 中。最主要的目的就是将当前应用的 ip + cim-server-port+ http-port 注册上去。上图是我在演示环境中注册的两个 cim-server 实例(由于在一台服务器,所以只是端口不同)。这样在客户端(监听这个 Zookeeper 节点)就能实时的知道目前可用的服务信息。登录当客户端请求 cim-forward-route 中的登录接口(详见下文)做完业务验证(就相当于日常登录其他网站一样)之后,客户端会向服务端发起一个长连接,如之前的流程所示:这时客户端会发送一个特殊报文,表明当前是登录信息。服务端收到后就需要将该客户端的 userID 和当前 Channel 通道关系保存起来。同时也缓存了用户的信息,也就是 userID 和 用户名。离线当客户端断线后也需要将刚才缓存的信息清除掉。同时也需要调用 route 接口清除相关信息(具体接口看下文)。IM 路由从架构图中可以看出,路由层是非常重要的一环;它提供了一系列的 HTTP 服务承接了客户端和服务端。目前主要是以下几个接口。注册接口由于每一个客户端都是需要登录才能使用的,所以第一步自然是注册。这里就设计的比较简单,直接利用 Redis 来存储用户信息;用户信息也只有 ID 和 userName 而已。只是为了方便查询在 Redis 中的 KV 又反过来存储了一份 VK,这样 ID 和 userName 都必须唯一。登录接口这里的登录和 cim-server 中的登录不一样,具有业务性质,登录成功之后需要判断是否是重复登录(一个用户只能运行一个客户端)。登录成功后需要从 Zookeeper 中获取服务列表(cim-server)并根据某种算法选择一台服务返回给客户端。登录成功之后还需要保存路由信息,也就是当前用户分配的服务实例保存到 Redis 中。为了实现只能一个用户登录,使用了 Redis 中的 set 来保存登录信息;利用 userID 作为 key ,重复的登录就会写入失败。类似于 Java 中的 HashSet,只能去重保存。获取一台可用的路由实例也比较简单:先从 Zookeeper 获取所有的服务实例做一个内部缓存。轮询选择一台服务器(目前只有这一种算法,后续会新增)。当然要获取 Zookeeper 中的服务实例前自然是需要监听 cim-server 之前注册上去的那个节点。具体代码如下:也是在应用启动之后监听 Zookeeper 中的路由节点,一旦发生变化就会更新内部缓存。这里使用的是 Guava 的 cache,它基于 ConcurrentHashMap,所以可以保证清除、新增缓存的原子性。群聊接口这是一个真正发消息的接口,实现的效果就是其中一个客户端发消息,其余所有客户端都能收到!流程肯定是客户端发送一条消息到服务端,服务端收到后在上文介绍的 SessionSocketHolder 中遍历所有 Channel(通道)然后下发消息即可。服务端是单机倒也可以,但现在是集群设计。所以所有的客户端会根据之前的轮询算法分配到不同的 cim-server 实例中。因此就需要路由层来发挥作用了。路由接口收到消息后首先遍历出所有的客户端和服务实例的关系。路由关系在 Redis 中的存放如下:由于 Redis 单线程的特质,当数据量大时;一旦使用 keys 匹配所有 cim-route:* 数据,会导致 Redis 不能处理其他请求。所以这里改为使用 scan 命令来遍历所有的 cim-route:*。接着会挨个调用每个客户端所在的服务端的 HTTP 接口用于推送消息。在 cim-server 中的实现如下:cim-server 收到消息后会在内部缓存中查询该 userID 的通道,接着只需要发消息即可。在线用户接口这是一个辅助接口,可以查询出当前在线用户信息。实现也很简单,也就是查询之前保存 ”用户登录状态的那个去重 set “即可。私聊接口之所以说获取在线用户是一个辅助接口,其实就是用于辅助私聊使用的。一般我们使用私聊的前提肯定得知道当前哪些用户在线,接着你才会知道你要和谁进行私聊。类似于这样:在我们这个场景中,私聊的前提就是需要获得在线用户的 userID。所以私聊接口在收到消息后需要查询到接收者所在的 cim-server 实例信息,后续的步骤就和群聊一致了。调用接收者所在实例的 HTTP 接口下发信息。只是群聊是遍历所有的在线用户,私聊只发送一个的区别。下线接口一旦客户端下线,我们就需要将之前存放在 Redis 中的一些信息删除掉(路由信息、登录状态)。IM 客户端客户端中的一些逻辑其实在上文已经谈到一些了。登录第一步也就是登录,需要在启动时调用 route 的登录接口,获得 cim-server 信息再创建连接。登录过程中 route 接口会判断是否为重复登录,重复登录则会直接退出程序。接下来是利用 route 接口返回的 cim-server 实例信息(ip+port)创建连接。最后一步就是发送一个登录标志的信息到服务端,让它保持客户端和 Channel 的关系。自定义协议上文提到的一些登录报文、真正的消息报文这些其实都是在我们自定义协议中可以区别出来的。由于是使用 Google Protocol Buffer 编解码,所以先看看原始格式。其实这个协议中目前一共就三个字段:requestId 可以理解为 userId。reqMsg 就是真正的消息。type 也就是上文提到的消息类别。目前主要是三种类型,分别对应不同的业务:心跳为了保持客户端和服务端的连接,每隔一段时间没有发送消息都需要自动的发送心跳。目前的策略是每隔一分钟就是发送一个心跳包到服务端:这样服务端每隔一分钟没有收到业务消息时就会收到 ping 的心跳包:内置命令客户端也内置了一些基本命令来方便使用。命令描述:q退出客户端:olu获取所有在线用户信息:all获取所有命令:更多命令正在开发中。。比如输入 :q 就会退出客户端,同时会关闭一些系统资源。当输入 :olu(onlineUser 的简写)就会去调用 route 的获取所有在线用户接口。群聊群聊的使用非常简单,只需要在控制台输入消息回车即可。这时会去调用 route 的群聊接口。私聊私聊也是同理,但前提是需要触发关键字;使用 userId;;消息内容 这样的格式才会给某个用户发送消息,所以一般都需要先使用 :olu 命令获取所以在线用户才方便使用。消息回调为了满足一些定制需求,比如消息需要保存之类的。所以在客户端收到消息之后会回调一个接口,在这个接口中可以自定义实现。因此先创建了一个 caller 的 bean,这个 bean 中包含了一个 CustomMsgHandleListener 接口,需要自行处理只需要实现此接口即可。自定义界面由于我自己不怎么会写界面,但保不准有其他大牛会写。所以客户端中的群聊、私聊、获取在线用户、消息回调等业务(以及之后的业务)都是以接口形式提供。也方便后面做页面集成,只需要调这些接口就行了;具体实现不用怎么关心。总结cim 目前只是第一版,BUG 多,功能少(只拉了几个群友做了测试);不过后续还会接着完善,至少这一版会给那些没有相关经验的朋友带来一些思路。后续计划:完整源码:https://github.com/crossoverJie/cim如果这篇对你有所帮助还请不吝转发。 ...

January 2, 2019 · 2 min · jiezi

Netty之旅总览

一篇文章入门Netty ByteBuf详解 ChannelHandler流程详解 EventLoop流程详解 Bootstrap使用详解 ChannelHandler做测试 数据格式转换与自带Channel工具 Netty Hello world版启动源码分析

December 31, 2018 · 1 min · jiezi

基于Netty实现ModbusTCP协议的测试工具

Netty搭建服务端 我们首选采用Netty框架搭建一个服务端程序。这里在IDE中shiyMaven创建了一个新的工程。首先写一个Server类,先开看看服务端的核心代码: static class Server{ private int port; public Server(int port) { this.port = port; for(int i=0;i<1024;i++) buffer[i]=0; } public void run() throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { //ch.pipeline().addLast(new FixedLengthFrameDecoder(12) ); ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,4,2)); ch.pipeline().addLast(new MyInHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); // 绑定端口,开始接收进来的连接 //bind(b,9000); ChannelFuture f = b.bind(port).sync(); System.out.println(“Server start listen at " + port ); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } }其中的MyInHandler类是我们实现Modbus协议的核心,我们继续看。2、MyInHandler类的实现 MyInHandler类是我们处理ModbusTCP协议的基础,下面我们来看看怎么实现这个类的。 static class MyInHandler extends ChannelInboundHandlerAdapter{ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf byteBuf = (ByteBuf) msg; System.out.println(”—————start process msg——————–"); System.out.println(“readable bytes is:"+byteBuf.readableBytes()); short TransActionId = byteBuf.readShort(); short protocal = byteBuf.readShort(); short msg_len = byteBuf.readShort(); byte slave_id = byteBuf.readByte(); byte funcotion_code = byteBuf.readByte(); if(funcotion_code ==4 )//如果功能码是4,也就是读请求,我们要返回结果 { //输出 short start_address = byteBuf.readShort(); short ncount = byteBuf.readShort(); System.out.println(“TransactionID is:"+ TransActionId); System.out.println(“protocal id is:"+protocal); System.out.println(“msg len is:"+msg_len); System.out.println(“slave id is:"+slave_id); System.out.println(“function code is:"+funcotion_code); System.out.println(“start address is:"+start_address); System.out.println(“count is:"+ncount); //返回响应消息报文 ByteBuf out = ctx.alloc().directBuffer(110); out.writeShort(0);//Transaction ID 2 out.writeShort(0);//protocal id 2 out.writeShort(95);//msg len 2 out.writeByte(1);//slave id 1 out.writeByte(4);//function code 1 //out.writeShort(0);//start address 2 out.writeByte(46);//46个寄存器 462 for(int i=0;i<92;i++) out.writeByte(buffer[i]); ctx.channel().writeAndFlush(out); } else if(funcotion_code == 0x10) { short start_address = byteBuf.readShort(); short nWords = byteBuf.readShort(); byte ncount = byteBuf.readByte(); //更新本地buffer for(int i=0;i<ncount;i++) buffer[start_address2+i] = byteBuf.readByte(); //printMsg(); //返回响应消息 ByteBuf out = ctx.alloc().directBuffer(93); out.writeShort(0);//Transaction ID 2 out.writeShort(0);//protocal id 2 out.writeShort(0);//msg len 2 out.writeByte(1);//slave id 1 out.writeByte(0x10);//function code 1 out.writeShort(start_address);//46个寄存器 46*2 out.writeShort(ncount);//ncuont 2 ctx.channel().writeAndFlush(out); //System.out.println(“response write success,write words is:"+out.readableBytes()); //out.release(); } else{ System.out.println(“error function”); } //System.out.println(”—————end process msg——————–”); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // 当出现异常就关闭连接 //cause.printStackTrace(); ctx.close(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println(“客户端已经连接!”); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { System.out.println(“客户端退出!”); ctx.close(); } private void printMsg(){ for(int i=0;i<200;i++){ if(i%20==0) System.out.println(); System.out.print( buffer[i]+” “); } } }3、界面实现 为了方便调试,我们这边实现了一个简单的界面。static class NewFrame{ NewFrame(){} private void start(){ JFrame frame = new JFrame(); // 4.设置窗体对象的属性值:标题、大小、显示位置、关闭操作、布局、禁止调整大小、可见、… frame.setTitle(“PSD-Test-Tool”);// 设置窗体的标题 frame.setSize(400, 450);// 设置窗体的大小,单位是像素 frame.setDefaultCloseOperation(3);// 设置窗体的关闭操作;3表示关闭窗体退出程序;2、1、0 frame.setLocationRelativeTo(null);// 设置窗体相对于另一个组件的居中位置,参数null表示窗体相对于屏幕的中央位置 frame.setResizable(false);// 设置禁止调整窗体大小 // 实例化FlowLayout流式布局类的对象,指定对齐方式为居中对齐,组件之间的间隔为5个像素 FlowLayout fl = new FlowLayout(FlowLayout.LEFT, 10, 10); // 实例化流式布局类的对象 frame.setLayout(fl); // 5.实例化元素组件对象,将元素组件对象添加到窗体上(组件添加要在窗体可见之前完成)。 // 实例化ImageIcon图标类的对象,该对象加载磁盘上的图片文件到内存中,这里的路径要用两个\ ImageIcon icon = new ImageIcon(””); // 用标签来接收图片,实例化JLabel标签对象,该对象显示icon图标 JLabel labIcon = new JLabel(icon); //设置标签大小 //labIcon.setSize(30,20);setSize方法只对窗体有效,如果想设置组件的大小只能用 Dimension dim = new Dimension(400,30); labIcon.setPreferredSize(dim); // 将labIcon标签添加到窗体上 frame.add(labIcon); //显示寄存器界面 final JTextArea registView = new JTextArea(); Dimension d = new Dimension(400,200); registView.setPreferredSize(d); frame.add(registView); // 实例化JLabel标签对象,该对象显示"账号:” JLabel labName = new JLabel(“地址:”); // 将labName标签添加到窗体上 frame.add(labName); // 实例化JTextField标签对象 final JTextField textName = new JTextField(); Dimension dim1 = new Dimension(350,30); //textName.setSize(dim);//setSize这方法只对顶级容器有效,其他组件使用无效。 textName.setPreferredSize(dim1);//设置除顶级容器组件其他组件的大小 // 将textName标签添加到窗体上 frame.add(textName); //实例化JLabel标签对象,该对象显示"密码:” JLabel labpass= new JLabel(“值 :”); //将labpass标签添加到窗体上 frame.add(labpass); //实例化JPasswordField final JTextField textword=new JTextField(); //设置大小 textword.setPreferredSize(dim1);//设置组件大小 //添加textword到窗体上 frame.add(textword); //实例化JButton组件 JButton button=new JButton(); //设置按钮的显示内容 Dimension dim2 = new Dimension(150,30); button.setText(“发送”); //设置按钮的大小 button.setSize(dim2); frame.add(button); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { //textword.getAccessibleContext(); int register_index = Integer.parseInt(textName.getText()); int value = Integer.parseInt(textword.getText()); System.out.println(“register_index="+register_index+";value="+value); if(register_index>200 || register_index<0) return; if(value>255 || value<0) return; buffer[register_index] =(byte)(value&0xff); //registView.setText(); //printMsg(); } }); frame.setVisible(true);// 设置窗体为可视化 new Timer(1000, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { registView.setText(””); StringBuilder sb = new StringBuilder(); int time = 0; for (int i = 0; i < 200; i++) { if (i % 20 == 0) { int start = time * 20; int end = time * 20 + 19; sb.append("\r\n reg[” + start + “-” + end + “]”); if (time == 0) sb.append(” “); if (start < 100 && time > 0) sb.append(” “); time++; } if (i % 10 == 0) sb.append(” “); sb.append(Integer.toHexString(buffer[i]&0xff) + " “);//转换成16进制显示 } registView.setText(sb.toString()); } }).start(); } }5、主模块 public static void main(String[] args) throws Exception{ NewFrame newFrame = new NewFrame(); newFrame.start(); Server server = new Server(9000); server.run(); }4、运行结果5、小结 这是第一版代码,其中在MyHandler类可以继续提取代码。 ...

December 29, 2018 · 3 min · jiezi

dubbo源码解析(十七)远程通信——Netty4

远程通讯——Netty4目标:介绍基于netty4的来实现的远程通信、介绍dubbo-remoting-netty4内的源码解析。前言netty4对netty3兼容性不是很好,并且netty4在很多的术语和api也发生了改变,导致升级netty4会很艰辛,网上应该有很多相关文章,高版本的总有高版本的优势所在,所以dubbo也需要与时俱进,又新增了基于netty4来实现远程通讯模块。下面讲解的,如果跟上一篇文章有重复的地方我就略过去了。关键还是要把远程通讯的api那几篇看懂,看这几篇实现才会很简单。下面是包的结构:源码分析(一)NettyChannel该类继承了AbstractChannel,是基于netty4的通道实现类1.属性/** * 通道集合 /private static final ConcurrentMap<Channel, NettyChannel> channelMap = new ConcurrentHashMap<Channel, NettyChannel>();/* * 通道 /private final Channel channel;/* * 属性集合 /private final Map<String, Object> attributes = new ConcurrentHashMap<String, Object>();属性跟netty3实现的通道类属性几乎一样,我就不讲解了。2.getOrAddChannelstatic NettyChannel getOrAddChannel(Channel ch, URL url, ChannelHandler handler) { if (ch == null) { return null; } // 首先从集合中取通道 NettyChannel ret = channelMap.get(ch); // 如果为空,则新建 if (ret == null) { NettyChannel nettyChannel = new NettyChannel(ch, url, handler); // 如果通道还活跃着 if (ch.isActive()) { // 加入集合 ret = channelMap.putIfAbsent(ch, nettyChannel); } if (ret == null) { ret = nettyChannel; } } return ret;}该方法是获得通道,如果集合中没有找到对应通道,则创建一个,然后加入集合。3.send@Overridepublic void send(Object message, boolean sent) throws RemotingException { super.send(message, sent); boolean success = true; int timeout = 0; try { // 写入数据,发送消息 ChannelFuture future = channel.writeAndFlush(message); // 如果已经发送过 if (sent) { // 获得超时时间 timeout = getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); // 等待timeout的连接时间后查看是否发送成功 success = future.await(timeout); } // 获得异常 Throwable cause = future.cause(); // 如果异常不为空,则抛出异常 if (cause != null) { throw cause; } } catch (Throwable e) { throw new RemotingException(this, “Failed to send message " + message + " to " + getRemoteAddress() + “, cause: " + e.getMessage(), e); } if (!success) { throw new RemotingException(this, “Failed to send message " + message + " to " + getRemoteAddress() + “in timeout(” + timeout + “ms) limit”); }}该方法是发送消息,调用了channel.writeAndFlush方法,与netty3的实现只是调用的api不同。4.close@Overridepublic void close() { try { super.close(); } catch (Exception e) { logger.warn(e.getMessage(), e); } try { // 移除通道 removeChannelIfDisconnected(channel); } catch (Exception e) { logger.warn(e.getMessage(), e); } try { // 清理属性集合 attributes.clear(); } catch (Exception e) { logger.warn(e.getMessage(), e); } try { if (logger.isInfoEnabled()) { logger.info(“Close netty channel " + channel); } // 关闭通道 channel.close(); } catch (Exception e) { logger.warn(e.getMessage(), e); }}该方法就是操作了四个步骤,比较清晰。(二)NettyClientHandler该类继承了ChannelDuplexHandler,是基于netty4实现的客户端通道处理实现类。这里的设计与netty3实现的通道处理器有所不同,netty3实现的通道处理器是被客户端和服务端统一使用的,而在这里服务端和客户端使用了两个不同的Handler来处理。并且netty3的NettyHandler是基于netty3的SimpleChannelHandler设计的,而这里是基于netty4的ChannelDuplexHandler。/* * url对象 /private final URL url;/* * 通道 /private final ChannelHandler handler;该类的属性只有两个,下面实现的方法也都是调用了handler的方法,我就举一个例子:@Overridepublic void disconnect(ChannelHandlerContext ctx, ChannelPromise future) throws Exception { // 获得通道 NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler); try { // 断开连接 handler.disconnected(channel); } finally { // 从集合中移除 NettyChannel.removeChannelIfDisconnected(ctx.channel()); }}可以看到分了三部,获得通道对象,调用handler方法,最后检测一下通道是否活跃。其他方法也是差不多。(三)NettyServerHandler该类继承了ChannelDuplexHandler,是基于netty4实现的服务端通道处理实现类。/* * 连接该服务器的通道数 key为ip:port /private final Map<String, Channel> channels = new ConcurrentHashMap<String, Channel>(); // <ip:port, channel>/* * url对象 /private final URL url;/* * 通道处理器 /private final ChannelHandler handler;该类有三个属性,比NettyClientHandler多了一个属性channels,下面的实现方法也是一样的,都是调用了handler方法,来看一个例子:@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception { // 激活事件 ctx.fireChannelActive(); // 获得通道 NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler); try { // 如果通道不为空,则加入集合中 if (channel != null) { channels.put(NetUtils.toAddressString((InetSocketAddress) ctx.channel().remoteAddress()), channel); } // 连接该通道 handler.connected(channel); } finally { // 如果通道不活跃,则移除通道 NettyChannel.removeChannelIfDisconnected(ctx.channel()); }}该方法是通道活跃的时候调用了handler.connected,差不多也是常规套路,就多了激活事件和加入到通道中。其他方法也差不多。(四)NettyClient该类继承了AbstractClient,是基于netty4实现的客户端实现类。1.属性/* * NioEventLoopGroup对象 /private static final NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup(Constants.DEFAULT_IO_THREADS, new DefaultThreadFactory(“NettyClientWorker”, true));/* * 客户端引导类 /private Bootstrap bootstrap;/* * 通道 /private volatile Channel channel; // volatile, please copy reference to use属性里的NioEventLoopGroup对象是netty4中的对象,什么用处请看netty的解析。2.doOpen@Overrideprotected void doOpen() throws Throwable { // 创建一个客户端的通道处理器 final NettyClientHandler nettyClientHandler = new NettyClientHandler(getUrl(), this); // 创建一个引导类 bootstrap = new Bootstrap(); // 设置可选项 bootstrap.group(nioEventLoopGroup) .option(ChannelOption.SO_KEEPALIVE, true) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) //.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getTimeout()) .channel(NioSocketChannel.class); // 如果连接超时时间小于3s,则设置为3s,也就是说最低的超时时间为3s if (getConnectTimeout() < 3000) { bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000); } else { bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConnectTimeout()); } // 创建一个客户端 bootstrap.handler(new ChannelInitializer() { @Override protected void initChannel(Channel ch) throws Exception { // 编解码器 NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this); // 加入责任链 ch.pipeline()//.addLast(“logging”,new LoggingHandler(LogLevel.INFO))//for debug .addLast(“decoder”, adapter.getDecoder()) .addLast(“encoder”, adapter.getEncoder()) .addLast(“handler”, nettyClientHandler); } });}该方法还是做了创建客户端,并且打开的操作,其中很多的参数设置操作。其他方法跟 dubbo源码解析(十六)远程通信——Netty3中写到的NettyClient实现一样。(五)NettyServer该类继承了AbstractServer,实现了Server。是基于netty4实现的服务器类private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);/* * 连接该服务器的通道集合 key为ip:port /private Map<String, Channel> channels; // <ip:port, channel>/* * 服务器引导类 /private ServerBootstrap bootstrap;/* * 通道 /private io.netty.channel.Channel channel;/* * boss线程组 /private EventLoopGroup bossGroup;/* * worker线程组 /private EventLoopGroup workerGroup;属性相较netty3而言,新增了两个线程组,同样也是因为netty3和netty4的设计不同。2.doOpen@Overrideprotected void doOpen() throws Throwable { // 创建服务引导类 bootstrap = new ServerBootstrap(); // 创建boss线程组 bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory(“NettyServerBoss”, true)); // 创建worker线程组 workerGroup = new NioEventLoopGroup(getUrl().getPositiveParameter(Constants.IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS), new DefaultThreadFactory(“NettyServerWorker”, true)); // 创建服务器处理器 final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this); // 获得通道集合 channels = nettyServerHandler.getChannels(); // 设置ventLoopGroup还有可选项 bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE) .childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE) .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { // 编解码器 NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this); // 增加责任链 ch.pipeline()//.addLast(“logging”,new LoggingHandler(LogLevel.INFO))//for debug .addLast(“decoder”, adapter.getDecoder()) .addLast(“encoder”, adapter.getEncoder()) .addLast(“handler”, nettyServerHandler); } }); // bind 绑定 ChannelFuture channelFuture = bootstrap.bind(getBindAddress()); // 等待绑定完成 channelFuture.syncUninterruptibly(); // 设置通道 channel = channelFuture.channel();}该方法是创建服务器,并且开启。如果熟悉netty4点朋友应该觉得还是很好理解的。其他方法跟《 dubbo源码解析(十六)远程通信——Netty3》中写到的NettyClient实现一样,处理close中要多关闭两个线程组(六)NettyTransporter该类跟 《dubbo源码解析(十六)远程通信——Netty3》中的NettyTransporter一样的实现。(七)NettyCodecAdapter该类是基于netty4的编解码器。1.属性/* * 编码器 /private final ChannelHandler encoder = new InternalEncoder();/* * 解码器 /private final ChannelHandler decoder = new InternalDecoder();/* * 编解码器 /private final Codec2 codec;/* * url对象 /private final URL url;/* * 通道处理器 /private final com.alibaba.dubbo.remoting.ChannelHandler handler;属性跟基于netty3实现的编解码一样。2.InternalEncoderprivate class InternalEncoder extends MessageToByteEncoder { @Override protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception { // 创建缓冲区 com.alibaba.dubbo.remoting.buffer.ChannelBuffer buffer = new NettyBackedChannelBuffer(out); // 获得通道 Channel ch = ctx.channel(); // 获得netty通道 NettyChannel channel = NettyChannel.getOrAddChannel(ch, url, handler); try { // 编码 codec.encode(channel, buffer, msg); } finally { // 检测通道是否活跃 NettyChannel.removeChannelIfDisconnected(ch); } }}该内部类是编码器的抽象,主要的编码还是调用了codec.encode。3.InternalDecoderprivate class InternalDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf input, List<Object> out) throws Exception { // 创建缓冲区 ChannelBuffer message = new NettyBackedChannelBuffer(input); // 获得通道 NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler); Object msg; int saveReaderIndex; try { // decode object. do { // 记录读索引 saveReaderIndex = message.readerIndex(); try { // 解码 msg = codec.decode(channel, message); } catch (IOException e) { throw e; } // 拆包 if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) { message.readerIndex(saveReaderIndex); break; } else { //is it possible to go here ? if (saveReaderIndex == message.readerIndex()) { throw new IOException(“Decode without read data.”); } // 读取数据 if (msg != null) { out.add(msg); } } } while (message.readable()); } finally { NettyChannel.removeChannelIfDisconnected(ctx.channel()); } }}该内部类是解码器的抽象类,其中关键的是调用了codec.decode。(八)NettyBackedChannelBuffer该类是缓冲区类。/* * 缓冲区 */private ByteBuf buffer;其中的方法几乎都调用了该属性的方法。而ByteBuf是netty4中的字节数据的容器。(九)FormattingTuple和MessageFormatter这两个类是用于用于格式化的,是从netty4中复制出来的,其中并且稍微做了一下改动。我就不再讲解了。后记该部分相关的源码解析地址:https://github.com/CrazyHZM/i…该文章讲解了基于netty4的来实现的远程通信、介绍dubbo-remoting-netty4内的源码解析,关键需要对netty4有所了解。下一篇我会讲解基于p2p形式实现远程通信部分。 ...

December 28, 2018 · 4 min · jiezi

dubbo源码解析(十六)远程通信——Netty3

远程通讯——Netty3目标:介绍基于netty3的来实现的远程通信、介绍dubbo-remoting-netty内的源码解析。前言现在dubbo默认的网络传输Transport接口默认实现的还是基于netty3实现的网络传输,不过马上后面默认实现就要改为netty4了。由于netty4对netty3对兼容性不是很好,所以保留了两个版本的实现。下面是包结构:源码分析(一)NettyChannel该类继承了AbstractChannel类,是基于netty3实现的通道。1.属性/** * 通道集合 /private static final ConcurrentMap<org.jboss.netty.channel.Channel, NettyChannel> channelMap = new ConcurrentHashMap<org.jboss.netty.channel.Channel, NettyChannel>();/* * 通道 /private final org.jboss.netty.channel.Channel channel;/* * 属性集合 /private final Map<String, Object> attributes = new ConcurrentHashMap<String, Object>();2.getOrAddChannelstatic NettyChannel getOrAddChannel(org.jboss.netty.channel.Channel ch, URL url, ChannelHandler handler) { if (ch == null) { return null; } // 首先从集合中取通道 NettyChannel ret = channelMap.get(ch); // 如果为空,则新建 if (ret == null) { NettyChannel nc = new NettyChannel(ch, url, handler); // 如果通道连接着 if (ch.isConnected()) { // 加入集合 ret = channelMap.putIfAbsent(ch, nc); } if (ret == null) { ret = nc; } } return ret;}该方法是获得通道,当通道在集合中没有的时候,新建一个通道。3.removeChannelIfDisconnectedstatic void removeChannelIfDisconnected(org.jboss.netty.channel.Channel ch) { if (ch != null && !ch.isConnected()) { channelMap.remove(ch); }}该方法是当通道没有连接的时候,从集合中移除它。4.send@Overridepublic void send(Object message, boolean sent) throws RemotingException { super.send(message, sent); boolean success = true; int timeout = 0; try { // 写入数据,发送消息 ChannelFuture future = channel.write(message); // 如果已经发送过 if (sent) { // 获得超时时间 timeout = getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); // 等待timeout的连接时间后查看是否发送成功 success = future.await(timeout); } // 看是否有异常 Throwable cause = future.getCause(); if (cause != null) { throw cause; } } catch (Throwable e) { throw new RemotingException(this, “Failed to send message " + message + " to " + getRemoteAddress() + “, cause: " + e.getMessage(), e); } if (!success) { throw new RemotingException(this, “Failed to send message " + message + " to " + getRemoteAddress() + “in timeout(” + timeout + “ms) limit”); }}该方法是发送消息,其中用到了channe.write方法传输消息,并且通过返回的future来判断是否发送成功。5.close@Overridepublic void close() { try { super.close(); } catch (Exception e) { logger.warn(e.getMessage(), e); } try { // 如果通道断开,则移除该通道 removeChannelIfDisconnected(channel); } catch (Exception e) { logger.warn(e.getMessage(), e); } try { // 清空属性 attributes.clear(); } catch (Exception e) { logger.warn(e.getMessage(), e); } try { if (logger.isInfoEnabled()) { logger.info(“Close netty channel " + channel); } // 关闭通道 channel.close(); } catch (Exception e) { logger.warn(e.getMessage(), e); }}该方法是关闭通道,做了三个操作,分别是从集合中移除、清除属性、关闭通道。其他实现方法比较简单,我就讲解了。(二)NettyHandler该类继承了SimpleChannelHandler类,是基于netty3的通道处理器,而该类被加上了@Sharable注解,也就是说该处理器可以从属于多个ChannelPipeline1.属性/* * 通道集合,key是主机地址 ip:port /private final Map<String, Channel> channels = new ConcurrentHashMap<String, Channel>(); // <ip:port, channel>/* * url对象 /private final URL url;/* * 通道 /private final ChannelHandler handler;该类的属性比较简单,并且该类中实现的方法都是调用了属性handler的方法,我举一个例子来讲,其他的可以自己查看源码,比较简单。@Overridepublic void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { // 获得通道实例 NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler); try { if (channel != null) { // 保存该通道,加入到集合中 channels.put(NetUtils.toAddressString((InetSocketAddress) ctx.getChannel().getRemoteAddress()), channel); } // 连接 handler.connected(channel); } finally { NettyChannel.removeChannelIfDisconnected(ctx.getChannel()); }}该方法是通道连接的方法,其中先获取了通道实例,然后吧该实例加入到集合中,最好带哦用handler.connected来进行连接。(三)NettyClient该类继承了AbstractClient,是基于netty3实现的客户端类。1.属性private static final Logger logger = LoggerFactory.getLogger(NettyClient.class);// ChannelFactory’s closure has a DirectMemory leak, using static to avoid// https://issues.jboss.org/browse/NETTY-424/* * 通道工厂,用static来避免直接缓存区的一个OOM问题 /private static final ChannelFactory channelFactory = new NioClientSocketChannelFactory(Executors.newCachedThreadPool(new NamedThreadFactory(“NettyClientBoss”, true)), Executors.newCachedThreadPool(new NamedThreadFactory(“NettyClientWorker”, true)), Constants.DEFAULT_IO_THREADS);/* * 客户端引导对象 /private ClientBootstrap bootstrap;/* * 通道 /private volatile Channel channel; // volatile, please copy reference to use上述属性中ChannelFactory用了static修饰,为了避免netty3中会有直接缓冲内存泄漏的现象,具体的讨论可以访问注释中的讨论。2.doOpen@Overrideprotected void doOpen() throws Throwable { // 设置日志工厂 NettyHelper.setNettyLoggerFactory(); // 实例化客户端引导类 bootstrap = new ClientBootstrap(channelFactory); // config // @see org.jboss.netty.channel.socket.SocketChannelConfig // 配置选择项 bootstrap.setOption(“keepAlive”, true); bootstrap.setOption(“tcpNoDelay”, true); bootstrap.setOption(“connectTimeoutMillis”, getConnectTimeout()); // 创建通道处理器 final NettyHandler nettyHandler = new NettyHandler(getUrl(), this); // 设置责任链路 bootstrap.setPipelineFactory(new ChannelPipelineFactory() { /* * 获得通道 * @return / @Override public ChannelPipeline getPipeline() { // 新建编解码 NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this); // 获得管道 ChannelPipeline pipeline = Channels.pipeline(); // 设置解码器 pipeline.addLast(“decoder”, adapter.getDecoder()); // 设置编码器 pipeline.addLast(“encoder”, adapter.getEncoder()); // 设置通道处理器 pipeline.addLast(“handler”, nettyHandler); // 返回通道 return pipeline; } });}该方法是创建客户端,并且打开,其中的逻辑就是用netty3的客户端引导类来创建一个客户端,如果对netty不熟悉的朋友可以先补补netty知识。3.doConnect@Overrideprotected void doConnect() throws Throwable { long start = System.currentTimeMillis(); // 用引导类连接 ChannelFuture future = bootstrap.connect(getConnectAddress()); try { // 在超时时间内是否连接完成 boolean ret = future.awaitUninterruptibly(getConnectTimeout(), TimeUnit.MILLISECONDS); if (ret && future.isSuccess()) { // 获得通道 Channel newChannel = future.getChannel(); // 异步修改此通道 newChannel.setInterestOps(Channel.OP_READ_WRITE); try { // Close old channel 关闭旧的通道 Channel oldChannel = NettyClient.this.channel; // copy reference if (oldChannel != null) { try { if (logger.isInfoEnabled()) { logger.info(“Close old netty channel " + oldChannel + " on create new netty channel " + newChannel); } // 关闭 oldChannel.close(); } finally { // 移除通道 NettyChannel.removeChannelIfDisconnected(oldChannel); } } } finally { // 如果客户端关闭 if (NettyClient.this.isClosed()) { try { if (logger.isInfoEnabled()) { logger.info(“Close new netty channel " + newChannel + “, because the client closed.”); } // 关闭通道 newChannel.close(); } finally { NettyClient.this.channel = null; NettyChannel.removeChannelIfDisconnected(newChannel); } } else { NettyClient.this.channel = newChannel; } } } else if (future.getCause() != null) { throw new RemotingException(this, “client(url: " + getUrl() + “) failed to connect to server " + getRemoteAddress() + “, error message is:” + future.getCause().getMessage(), future.getCause()); } else { throw new RemotingException(this, “client(url: " + getUrl() + “) failed to connect to server " + getRemoteAddress() + " client-side timeout " + getConnectTimeout() + “ms (elapsed: " + (System.currentTimeMillis() - start) + “ms) from netty client " + NetUtils.getLocalHost() + " using dubbo version " + Version.getVersion()); } } finally { // 如果客户端没有连接 if (!isConnected()) { // 取消future future.cancel(); } }}该方法是客户端连接服务器的方法。其中调用了bootstrap.connect。后面的逻辑是用来检测是否连接,最后如果未连接,则会取消该连接任务。4.doClose@Overrideprotected void doClose() throws Throwable { /try { bootstrap.releaseExternalResources(); } catch (Throwable t) { logger.warn(t.getMessage()); }/}在这里不能关闭是因为channelFactory 是静态属性,被多个 NettyClient 共用。所以不能释放资源。(四)NettyServer该类继承了AbstractServer,实现了Server,是基于netty3实现的服务器类。1.属性/* * 连接该服务器的通道集合 /private Map<String, Channel> channels; // <ip:port, channel>/* * 服务器引导类对象 /private ServerBootstrap bootstrap;/* * 通道 /private org.jboss.netty.channel.Channel channel;2.doOpen@Overrideprotected void doOpen() throws Throwable { // 设置日志工厂 NettyHelper.setNettyLoggerFactory(); // 创建线程池 ExecutorService boss = Executors.newCachedThreadPool(new NamedThreadFactory(“NettyServerBoss”, true)); ExecutorService worker = Executors.newCachedThreadPool(new NamedThreadFactory(“NettyServerWorker”, true)); // 新建通道工厂 ChannelFactory channelFactory = new NioServerSocketChannelFactory(boss, worker, getUrl().getPositiveParameter(Constants.IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS)); // 新建服务引导类对象 bootstrap = new ServerBootstrap(channelFactory); // 新建通道处理器 final NettyHandler nettyHandler = new NettyHandler(getUrl(), this); // 获得通道集合 channels = nettyHandler.getChannels(); // https://issues.jboss.org/browse/NETTY-365 // https://issues.jboss.org/browse/NETTY-379 // final Timer timer = new HashedWheelTimer(new NamedThreadFactory(“NettyIdleTimer”, true)); // 禁用nagle算法,将数据立即发送出去。纳格算法是以减少封包传送量来增进TCP/IP网络的效能 bootstrap.setOption(“child.tcpNoDelay”, true); // 设置管道工厂 bootstrap.setPipelineFactory(new ChannelPipelineFactory() { /* * 获得通道 * @return / @Override public ChannelPipeline getPipeline() { // 新建编解码器 NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this); // 获得通道 ChannelPipeline pipeline = Channels.pipeline(); /int idleTimeout = getIdleTimeout(); if (idleTimeout > 10000) { pipeline.addLast(“timer”, new IdleStateHandler(timer, idleTimeout / 1000, 0, 0)); }/ // 设置解码器 pipeline.addLast(“decoder”, adapter.getDecoder()); // 设置编码器 pipeline.addLast(“encoder”, adapter.getEncoder()); // 设置通道处理器 pipeline.addLast(“handler”, nettyHandler); // 返回通道 return pipeline; } }); // bind 绑定地址,也就是启用服务器 channel = bootstrap.bind(getBindAddress());}该方法是创建服务器,并且打开服务器。同样创建服务器的方式跟正常的用netty创建服务器方式一样,只是新加了编码器和解码器。还有一个注意点就是这里ServerBootstrap 的可选项。3.doClose@Overrideprotected void doClose() throws Throwable { try { if (channel != null) { // unbind.关闭通道 channel.close(); } } catch (Throwable e) { logger.warn(e.getMessage(), e); } try { // 获得所有连接该服务器的通道集合 Collection<com.alibaba.dubbo.remoting.Channel> channels = getChannels(); if (channels != null && !channels.isEmpty()) { // 遍历通道集合 for (com.alibaba.dubbo.remoting.Channel channel : channels) { try { // 关闭通道连接 channel.close(); } catch (Throwable e) { logger.warn(e.getMessage(), e); } } } } catch (Throwable e) { logger.warn(e.getMessage(), e); } try { if (bootstrap != null) { // release external resource. 回收资源 bootstrap.releaseExternalResources(); } } catch (Throwable e) { logger.warn(e.getMessage(), e); } try { if (channels != null) { // 清空集合 channels.clear(); } } catch (Throwable e) { logger.warn(e.getMessage(), e); }}该方法是关闭服务器,一系列的操作很清晰,我就不多说了。4.getChannels@Overridepublic Collection<Channel> getChannels() { Collection<Channel> chs = new HashSet<Channel>(); for (Channel channel : this.channels.values()) { // 如果通道连接,则加入集合,返回 if (channel.isConnected()) { chs.add(channel); } else { channels.remove(NetUtils.toAddressString(channel.getRemoteAddress())); } } return chs;}该方法是返回连接该服务器的通道集合,并且用了HashSet保存,不会重复。(五)NettyTransporterpublic class NettyTransporter implements Transporter { public static final String NAME = “netty”; @Override public Server bind(URL url, ChannelHandler listener) throws RemotingException { // 创建一个NettyServer return new NettyServer(url, listener); } @Override public Client connect(URL url, ChannelHandler listener) throws RemotingException { // 创建一个NettyClient return new NettyClient(url, listener); }}该类就是基于netty3的Transporter实现类,同样两个方法也是分别创建了NettyServer和NettyClient。(六)NettyHelper该类是设置日志的工具类,其中基于netty3的InternalLoggerFactory实现类一个DubboLoggerFactory。这个我就不讲解了,比较好理解,不理解也无伤大雅。(七)NettyCodecAdapter该类是基于netty3实现的编解码类。1.属性/* * 编码者 /private final ChannelHandler encoder = new InternalEncoder();/* * 解码者 /private final ChannelHandler decoder = new InternalDecoder();/* * 编解码器 /private final Codec2 codec;/* * url对象 /private final URL url;/* * 缓冲区大小 /private final int bufferSize;/* * 通道对象 /private final com.alibaba.dubbo.remoting.ChannelHandler handler;InternalEncoder和InternalDecoder属性是该类的内部类,分别掌管着编码和解码2.构造方法public NettyCodecAdapter(Codec2 codec, URL url, com.alibaba.dubbo.remoting.ChannelHandler handler) { this.codec = codec; this.url = url; this.handler = handler; int b = url.getPositiveParameter(Constants.BUFFER_KEY, Constants.DEFAULT_BUFFER_SIZE); // 如果缓存区大小在16字节以内,则设置配置大小,如果不是,则设置8字节的缓冲区大小 this.bufferSize = b >= Constants.MIN_BUFFER_SIZE && b <= Constants.MAX_BUFFER_SIZE ? b : Constants.DEFAULT_BUFFER_SIZE;}你会发现对于缓存区大小的规则都是一样的。3.InternalEncoder@Sharableprivate class InternalEncoder extends OneToOneEncoder { @Override protected Object encode(ChannelHandlerContext ctx, Channel ch, Object msg) throws Exception { // 动态分配一个1k的缓冲区 com.alibaba.dubbo.remoting.buffer.ChannelBuffer buffer = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.dynamicBuffer(1024); // 获得通道对象 NettyChannel channel = NettyChannel.getOrAddChannel(ch, url, handler); try { // 编码 codec.encode(channel, buffer, msg); } finally { NettyChannel.removeChannelIfDisconnected(ch); } // 基于buteBuffer创建一个缓冲区,并且写入数据 return ChannelBuffers.wrappedBuffer(buffer.toByteBuffer()); }}该内部类实现类编码的逻辑,主要调用了codec.encode。4.InternalDecoderprivate class InternalDecoder extends SimpleChannelUpstreamHandler { private com.alibaba.dubbo.remoting.buffer.ChannelBuffer buffer = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER; @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent event) throws Exception { Object o = event.getMessage(); // 如果消息不是一个ChannelBuffer类型 if (!(o instanceof ChannelBuffer)) { // 转发事件到与此上下文关联的处理程序最近的上游 ctx.sendUpstream(event); return; } ChannelBuffer input = (ChannelBuffer) o; // 如果可读数据不大于0,直接返回 int readable = input.readableBytes(); if (readable <= 0) { return; } com.alibaba.dubbo.remoting.buffer.ChannelBuffer message; if (buffer.readable()) { // 判断buffer是否是动态分配的缓冲区 if (buffer instanceof DynamicChannelBuffer) { // 写入数据 buffer.writeBytes(input.toByteBuffer()); message = buffer; } else { // 需要的缓冲区大小 int size = buffer.readableBytes() + input.readableBytes(); // 动态生成缓冲区 message = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.dynamicBuffer( size > bufferSize ? size : bufferSize); // 把buffer数据写入message message.writeBytes(buffer, buffer.readableBytes()); // 把input数据写入message message.writeBytes(input.toByteBuffer()); } } else { // 否则 基于ByteBuffer通过buffer来创建一个新的缓冲区 message = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.wrappedBuffer( input.toByteBuffer()); } NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler); Object msg; int saveReaderIndex; try { // decode object. do { saveReaderIndex = message.readerIndex(); try { // 解码 msg = codec.decode(channel, message); } catch (IOException e) { buffer = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER; throw e; } // 拆包 if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) { message.readerIndex(saveReaderIndex); break; } else { // 如果已经到达读索引,则没有数据可解码 if (saveReaderIndex == message.readerIndex()) { buffer = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER; throw new IOException(“Decode without read data.”); } // if (msg != null) { // 将消息发送到指定关联的处理程序最近的上游 Channels.fireMessageReceived(ctx, msg, event.getRemoteAddress()); } } } while (message.readable()); } finally { // 如果消息还有可读数据,则丢弃 if (message.readable()) { message.discardReadBytes(); buffer = message; } else { buffer = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER; } NettyChannel.removeChannelIfDisconnected(ctx.getChannel()); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { ctx.sendUpstream(e); }}该内部类实现了解码的逻辑,其中大部分逻辑都在对数据做读写,关键的解码调用了codec.decode。(八)NettyBackedChannelBufferFactory该类是创建缓冲区的工厂类。它实现了ChannelBufferFactory接口,也就是实现类它的三种获得缓冲区的方法。public class NettyBackedChannelBufferFactory implements ChannelBufferFactory { /* * 单例 */ private static final NettyBackedChannelBufferFactory INSTANCE = new NettyBackedChannelBufferFactory(); public static ChannelBufferFactory getInstance() { return INSTANCE; } @Override public ChannelBuffer getBuffer(int capacity) { return new NettyBackedChannelBuffer(ChannelBuffers.dynamicBuffer(capacity)); } @Override public ChannelBuffer getBuffer(byte[] array, int offset, int length) { org.jboss.netty.buffer.ChannelBuffer buffer = ChannelBuffers.dynamicBuffer(length); buffer.writeBytes(array, offset, length); return new NettyBackedChannelBuffer(buffer); } @Override public ChannelBuffer getBuffer(ByteBuffer nioBuffer) { return new NettyBackedChannelBuffer(ChannelBuffers.wrappedBuffer(nioBuffer)); }}可以看到,都是创建了一个NettyBackedChannelBuffer,下面讲解NettyBackedChannelBuffer。(九)NettyBackedChannelBuffer该类是基于netty3的buffer重新实现的缓冲区,它实现了ChannelBuffer接口,并且有一个属性:private org.jboss.netty.buffer.ChannelBuffer buffer;那么其中的几乎所有方法都是调用了这个buffer的方法,因为我在dubbo源码解析(十一)远程通信——Buffer中写到ChannelBuffer接口方法定义跟netty中的缓冲区定义几乎一样,连注释都几乎一样。所有知识单纯的调用了buffer的方法。具体的代码可以查看我的GitHub后记该部分相关的源码解析地址:https://github.com/CrazyHZM/i…该文章讲解了基于netty3的来实现的远程通信、介绍dubbo-remoting-netty内的源码解析,关键需要对netty有所了解。下一篇我会讲解基于netty4实现远程通信部分。 ...

December 27, 2018 · 8 min · jiezi

Netty WebSocket 协议

HTTP 协议的弊端HTTP 协议为半双工协议. 半双工协议指数据可以在客户端和服务端两个方向上传输, 但是不能同时传输. 它意味这同一时刻, 只有一个方向上的数据传输; 客户端发送请求, 服务器等待, 直到收到完整的请求. 然后发送回应, 客户端和服务器无法同时发送.HTTP 消息冗长而繁琐. HTTP 消息包含消息头、消息头、换行符等, 通常情况下采用文本方式传输, 相比于其他的二进制通信协议, 冗长而繁琐;针对服务器推送的黑客攻击. 例如长时间轮询.WebSocket 入门webSocket 是 HTML5 开始提供的一种浏览器于服务器间进行全双工通信的技术.在 WebSocket API 中, 浏览器和服务器只需要做一个握手的动作, 然后, 浏览器和服务器之间就形成了一条快速通道, 两者就可以直接相互传送数据了. WebSocket 基于 TCP 双向全双工进行消息传递, 在同一时刻, 既可以发送消息, 也可以接收消息, 相比 HTTP 的半双工协议, 性能得到很大提升.WebSocket 的特点:单一的 TCP 连接, 采用全双工模式通信;对代理、防火墙和路由器透明;无头部信息、Cookie和身份验证;无安全开销;通过 ping/pong 帧保持链路激活;服务器可以主动传递消息给客户端, 不再需要客户端轮询.WebSocket 连接建立建立 webSocket 连接时, 需要通过客户端或浏览器发出握手请求, 类似下面的 http 报文.这个请求和通常的 HTTP 请求不同, 包含了一些附加头信息, 其中附加头信息 Upgrade:WebSocket 表明这是一个申请协议升级的 HTTP 请求.服务器解析这些附加的头信息, 然后生成应答信息返回给客户端, 客户端和服务端的 WebSocket 连接就建立起来了, 双方可以通过这个连接通道自由的传递信息, 并且这个连接会持续存在直到客户端或服务端的某一方主动关闭连接.服务端返回给客户端的应答消息, 类似如下报文请求消息中的 Sec-WebSocket-Key 是随机的, 服务端会用这些数据来构造出一个 SHA-1 的信息摘要, 把 Sec-WebSocket-Key 加上一个魔幻字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11. 使用 SHA-1 加密, 然后进行 BASE-64 编码, 将结果做为 Sec-WebSocket-Accept 头的值, 返回给客户端.WebSocket 生命周期握手成功之后, 服务端和客户端就可以通过 messages 的方式进行通讯, 一个消息由一个或多个帧组成.帧都有自己对应的类型, 属于同一个消息的多个帧具有相同类型的数据. 从广义上讲, 数据类型可以是文本数据(UTF-8文字)、二进制数据和控制帧(协议级信令, 例如信号).WebSocket 连接生命周期如下:Netty WebSocket 协议开发示例代码public class TimeServer { public void bind(int port) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .handler(new LoggingHandler(LogLevel.DEBUG)) .childHandler(new ChildChannelHandler()); // 绑定端口, 同步等待成功 ChannelFuture f = b.bind(port).sync(); // 等待服务端监听端口关闭 f.channel().closeFuture().sync(); } finally { System.out.println(“shutdownGracefully”); bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(“http-codec”, new HttpServerCodec()); ch.pipeline().addLast(“aggregator”, new HttpObjectAggregator(65536)); ch.pipeline().addLast(“http-chunked”, new ChunkedWriteHandler()); ch.pipeline().addLast(“handler”, new WebSOcketServerHandler()); } } private class WebSOcketServerHandler extends SimpleChannelInboundHandler<Object> { private WebSocketServerHandshaker handshaker; @Override protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception { // 传统的 HTTP 接入 if (msg instanceof FullHttpRequest) { System.out.println(“传统的 HTTP 接入”); handleHttpRequest(ctx, (FullHttpRequest) msg); } // WebSocket 接入 else if (msg instanceof WebSocketFrame) { System.out.println(“WebSocket 接入”); handleWebSocketFrame(ctx, (WebSocketFrame) msg); } } private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { // 如果 HTTP 解码失败, 返回HTTP异常 if (!req.getDecoderResult().isSuccess() || (!“websocket”.equalsIgnoreCase(req.headers().get(“Upgrade”)))) { sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)); return; } // 构造握手响应返回, 本机测试 WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(“ws://localhost:8080/websocket”, null, false); handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel()); } else { handshaker.handshake(ctx.channel(), req); } } private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { // 判断是否是关闭链路的指令 if (frame instanceof CloseWebSocketFrame) { handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); return; } // 判断是否是 ping 信息 if (frame instanceof PingWebSocketFrame) { ctx.channel().write(new PongWebSocketFrame(frame.content().retain())); return; } // 本例程仅支持文本消息, 不支持二进制消息 if (!(frame instanceof TextWebSocketFrame)) { throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName())); } // 返回应答信息 String request = ((TextWebSocketFrame) frame).text(); ctx.channel().write(new TextWebSocketFrame(request + " , 欢迎使用 Netty WebSocket 服务, 现在时刻: " + new java.util.Date().toString())); } private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { if (res.getStatus().code() != 200) { ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8); res.content().writeBytes(buf); buf.release(); setContentLength(res, res.content().readableBytes()); } // 如果是非 Keep-Alive, 关闭连接 ChannelFuture f = ctx.channel().writeAndFlush(res); if (!isKeepAlive(req) || res.getStatus().code() != 200) { f.addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } }}HttpServerCodec: 将请求和应答消息编码或解码为 HTTP 消息.HttpObjectAggregator: 它的目的是将 HTTP 消息的多个部分组合成一条完整的 HTTP 消息. Netty 可以聚合 HTTP 消息, 使用 FullHttpResponse 和 FullHttpRequest 到 ChannelPipeline 中的下一个 ChannelHandler, 这就消除了断裂消息, 保证了消息的完整.ChunkedWriteHandler: 来向客户端发送 HTML5 文件, 主要用于支持浏览器和服务端进行 WebSocket 通信.第一次握手请求消息由 HTTP 协议承载, 所以它是一个 HTTP 消息, 执行 handleHttpRequest 方法来处理 WebSocket 握手请求. 通过判断请求消息判断是否包含 Upgrade 字段或它的值不是 websocket, 则返回 HTTP 400 响应.握手请求校验通过之后, 开始构造握手工厂, 创建握手处理类 WebSocketServerHandshaker, 通过它构造握手响应消息返回给客户端.添加 WebSocket Encoder 和 WebSocket Decoder 之后, 服务端就可以自动对 WebSocket 消息进行编解码了, 后面的 handler 可以直接对 WebSocket 对象进行操作.handleWebSocketFrame 对消息进行判断, 首先判断是否是控制帧, 如果是就关闭链路. 如果是维持链路的 Ping 消息, 则构造 Pong 消息返回. 由于本例程的 WebSocket 通信双方使用的都是文本消息, 所以对请求新消息的类型进行判断, 而不是文本的抛出异常.最后, 从 TextWebSocketFrame 中获取请求消息字符串, 对它处理后通过构造新的 TextWebSocketFrame 消息返回给客户端, 由于握手应答时, 动态增加了 TextWebSocketFrame 的编码类, 所以可以直接发送 TextWebSocketFrame 对象. ...

December 24, 2018 · 3 min · jiezi

Netty中的Channel之数据冲刷与线程安全(writeAndFlush)

本文首发个人博客:猫叔的博客 | MySelfGitHub项目地址InChat一个轻量级、高效率的支持多端(应用与硬件Iot)的异步网络应用通讯框架前言本文预设读者已经了解了一定的Netty基础知识,并能够自己构建一个Netty的通信服务(包括客户端与服务端)。那么你一定使用到了Channel,这是Netty对传统JavaIO、NIO的链接封装实例。那么接下来让我们来了解一下关于Channel的数据冲刷与线程安全吧。数据冲刷的步骤1、获取一个链接实例@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //获取链接实例 Channel channel = ctx.channel();}我将案例放在初学者最熟悉的channelRead方法中,这是一个数据接收的方法,我们自实现Netty的消息处理接口时需要重写的方法。即客户端发送消息后,这个方法会被触发调用,所以我们在这个方法中进行本次内容的讲解。由上一段代码,其实目前还是很简单,我们借助ChannelHandlerContext(这是一个ChannelHandler与ChannelPipeline相交互并对接的一个对象。如下是源码的解释)来获取目前的链接实例Channel。/* Enables a {@link ChannelHandler} to interact with its {@link ChannelPipeline} * and other handlers. Among other things a handler can notify the next {@link ChannelHandler} in the * {@link ChannelPipeline} as well as modify the {@link ChannelPipeline} it belongs to dynamically. / public interface ChannelHandlerContext extends AttributeMap, ChannelInboundInvoker, ChannelOutboundInvoker { //…… }2、创建一个持有数据的ByteBuf@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //获取链接实例 Channel channel = ctx.channel(); //创建一个持有数据的ByteBuf ByteBuf buf = Unpooled.copiedBuffer(“data”, CharsetUtil.UTF_8);}ByteBuf又是什么呢?它是Netty框架自己封装的一个字符底层对象,是一个对 byte[] 和 ByteBuffer NIO 的抽象类,更官网的说就是“零个或多个字节的随机和顺序可访问的序列。”,如下是源码的解释/* * A random and sequential accessible sequence of zero or more bytes (octets). * This interface provides an abstract view for one or more primitive byte * arrays ({@code byte[]}) and {@linkplain ByteBuffer NIO buffers}. / public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> { //…… }由上一段源码可以看出,ByteBuf是一个抽象类,所以我们不能通过 new 的形式来创建一个新的ByteBuf对象。那么我们可以通过Netty提供的一个 final 的工具类 Unpooled(你将其看作是一个创建ByteBuf的工具类就好了)。/* * Creates a new {@link ByteBuf} by allocating new space or by wrapping * or copying existing byte arrays, byte buffers and a string. / public final class Unpooled { //…… }这真是一个有趣的过程,那么接下来我们仅需要再看看 copiedBuffer 这个方法了。这个方法相对简单,就是我们将创建一个新的缓冲区,其内容是我们指定的 UTF-8字符集 编码指定的 “data” ,同时这个新的缓冲区的读索引和写索引分别是0和字符串的长度。3、冲刷数据@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //获取链接实例 Channel channel = ctx.channel(); //创建一个持有数据的ByteBuf ByteBuf buf = Unpooled.copiedBuffer(“data”, CharsetUtil.UTF_8); //数据冲刷 channel.writeAndFlush(buf);}我相信大部分人都是直接这么写的,因为我们经常理所当然的启动测试,并在客户端接受到了这个 “data” 消息。那么我们是否应该注意一下,这个数据冲刷会返回一个什么值,我们要如何才能在服务端知道,这次数据冲刷是成功还是失败呢?那么其实Netty框架已经考虑到了这个点,本次数据冲刷我们将得到一个 ChannelFuture 。@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //获取链接实例 Channel channel = ctx.channel(); //创建一个持有数据的ByteBuf ByteBuf buf = Unpooled.copiedBuffer(“data”, CharsetUtil.UTF_8); //数据冲刷 ChannelFuture cf = channel.writeAndFlush(buf);}是的,他就是 Channel 异步IO操作的结果,它是一个接口,并继承了Future<V>。(如下为源码的解释)/* * The result of an asynchronous {@link Channel} I/O operation. / public interface ChannelFuture extends Future<Void> { //…… }既然如此,那么我们可以明显的知道我们可以对其添加对应的监听。4、异步回调结果监听@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //获取链接实例 Channel channel = ctx.channel(); //创建一个持有数据的ByteBuf ByteBuf buf = Unpooled.copiedBuffer(“data”, CharsetUtil.UTF_8); //数据冲刷 ChannelFuture cf = channel.writeAndFlush(buf); //添加ChannelFutureListener以便在写操作完成后接收通知 cf.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { //写操作完成,并没有错误发生 if (future.isSuccess()){ System.out.println(“successful”); }else{ //记录错误 System.out.println(“error”); future.cause().printStackTrace(); } } });}好的,我们可以简单的从代码理解到,我们将通过对异步IO的结果监听,得到本次运行的结果。我想这才是一个相对完整的 数据冲刷(writeAndFlush)。测试线程安全的流程对于线程安全的测试,我们将模拟多个线程去执行数据冲刷操作,我们可以用到 Executor 。我们可以这样理解 Executor ,是一种省略了线程启用与调度的方式,你只需要传递一个 Runnable 给它即可,你不再需要去 start 一个线程。(如下是源码的解释)/* * An object that executes submitted {@link Runnable} tasks. This * interface provides a way of decoupling task submission from the * mechanics of how each task will be run, including details of thread * use, scheduling, etc. An {@code Executor} is normally used * instead of explicitly creating threads. For example, rather than * invoking {@code new Thread(new(RunnableTask())).start()} for each * of a set of tasks, you might use:… / public interface Executor { //…… }那么我们的测试代码,大致是这样的。final Channel channel = ctx.channel();//创建要写数据的ByteBuffinal ByteBuf buf = Unpooled.copiedBuffer(“data”,CharsetUtil.UTF_8).retain();//创建将数据写到Channel的RunnableRunnable writer = new Runnable() { @Override public void run() { ChannelFuture cf = channel.writeAndFlush(buf.duplicate()); cf.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { //写操作完成,并没有错误发生 if (future.isSuccess()){ System.out.println(“successful”); }else{ //记录错误 System.out.println(“error”); future.cause().printStackTrace(); } } }); }};//获取到线程池的Executor的引用Executor executor = Executors.newCachedThreadPool();//提交到某个线程中执行executor.execute(writer);//提交到另一个线程中执行executor.execute(writer);这里,我们需要注意的是:创建 ByteBuf 的时候,我们使用了 retain 这个方法,他是将我们生成的这个 ByteBuf 进行保留操作。在 ByteBuf 中有这样的一种区域: 非保留和保留派生缓冲区。这里有点复杂,我们可以简单的理解,如果调用了 retain 那么数据就存在派生缓冲区中,如果没有调用,则会在调用后,移除这一个字符数据。(如下是 ByteBuf 源码的解释)/<h4>Non-retained and retained derived buffers</h4> * * Note that the {@link #duplicate()}, {@link #slice()}, {@link #slice(int, int)} and {@link #readSlice(int)} does NOT * call {@link #retain()} on the returned derived buffer, and thus its reference count will NOT be increased. If you * need to create a derived buffer with increased reference count, consider using {@link #retainedDuplicate()}, * {@link #retainedSlice()}, {@link #retainedSlice(int, int)} and {@link #readRetainedSlice(int)} which may return * a buffer implementation that produces less garbage. */好的,我想你可以自己动手去测试一下,最好再看看源码,加深一下实现的原理印象。这里的线程池并不是现实线程安全,而是用来做测试多线程的,Netty的Channel实现是线程安全的,所以我们可以存储一个到Channel的引用,并且每当我们需要向远程节点写数据时,都可以使用它,即使当时许多线程都在使用它,消息也会被保证按顺序发送的。结语最后,介绍一下,个人的一个基于Netty的开源项目:InChat一个轻量级、高效率的支持多端(应用与硬件Iot)的异步网络应用通讯框架参考资料: 《Netty实战》 ...

December 24, 2018 · 3 min · jiezi

netty 中使用 Protobuf

private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(“protobufFrameDecoder”, new ProtobufVarint32FrameDecoder()); ch.pipeline().addLast(“protobuf decoder”, new ProtobufDecoder(SubscribeReqPeoro.SubscribeReq.getDefaultInstance())); ch.pipeline().addLast(“LengthFieldPrepender”, new ProtobufVarint32LengthFieldPrepender()); ch.pipeline().addLast(“protobuf encoder”, new ProtobufEncoder()); ch.pipeline().addLast(new TimeServerHandler()); } }向 ChannelPipeline 添加 ProtobufVarint32FrameDecoder, 主要用于半包处理, 后续添加 ProtobufDecoder 解码器, 它的参数是 com.google.protobuf.MessageLite 实际上就是告诉 ProtobufDecoder 需要解码的目标类是什么. ProtobufVarint32LengthFieldPrepender: 因为 ProtobufEncoder 只是将 message 的各个 filed 按照规则输出, 并没有 serializedSize, 所以 socket 无法判定 package(封包). 这个 Encoder 的作用就是在 ProtobufEncoder 生成的字节数组前, 设置 varint32 数字, 表示 serializedSize.

December 23, 2018 · 1 min · jiezi

Protobuf3语言指南

定义一个消息类型先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的 .proto 文件了:syntax = “proto3”;message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3;}文件的第一行指定了你正在使用proto3语法:如果你没有指定这个,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。指定字段类型在上面的例子中,所有字段都是标量类型:两个整型(page_number和result_per_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。分配标识号正如你所见,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]( (从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber))的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。指定字段规则所指定的消息字段修饰符必须是如下之一:singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。在proto3中,repeated的标量域默认情况虾使用packed。你可以了解更多的pakced属性在Protocol Buffer 编码添加更多消息类型在一个.proto文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用——例如,如果想定义与SearchResponse消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中,如:message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3;}message SearchResponse { …}添加注释向.proto文件添加注释,可以使用C/C++/java风格的双斜杠(//) 语法格式,如:message SearchRequest { string query = 1; int32 page_number = 2; // Which page number do we want? int32 result_per_page = 3; // Number of results to return per page.}保留标识符(Reserved)如果你通过删除或者注释所有域,以后的用户可以重用标识号当你重新更新类型的时候。如果你使用旧版本加载相同的.proto文件这会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是指定保留标识符(and/or names, which can also cause issues for JSON serialization不明白什么意思),protocol buffer的编译器会警告未来尝试使用这些域标识符的用户。message Foo { reserved 2, 15, 9 to 11; reserved “foo”, “bar”;}注:不要在同一行reserved声明中同时声明域名字和标识号从.proto文件生成了什么?当用protocol buffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。对go来说,编译器会位每个消息类型生成了一个.pd.go文件。对于Ruby来说,编译器会为每个消息类型生成了一个.rb文件。javaNano来说,编译器输出类似域java但是没有Builder类对于Objective-C来说,编译器会为每个消息类型生成了一个pbobjc.h文件和pbobjcm文件,.proto文件中的每一个消息有一个对应的类。对于C#来说,编译器会为每个消息类型生成了一个.cs文件,.proto文件中的每一个消息有一个对应的类。你可以从如下的文档链接中获取每种语言更多API(proto3版本的内容很快就公布)。API Reference枚举当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)并且为每个可能的值定义一个常量就可以了。在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段:message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } Corpus corpus = 4;}如你所见,Corpus枚举的第一个常量映射为0:每个枚举类型必须将其第一个类型映射为0,这是因为:必须有有一个0值,我们可以用这个0值作为默认值。这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。你可以通过将不同的枚举常量指定位相同的值。如果这样做你需要将allow_alias设定位true,否则编译器会在别名的地方产生一个错误信息。enum EnumAllowingAlias { option allow_alias = true; UNKNOWN = 0; STARTED = 1; RUNNING = 1;}enum EnumNotAllowingAlias { UNKNOWN = 0; STARTED = 1; // RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.}枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。如上例所示,可以在 一个消息定义的内部或外部定义枚举——这些枚举可以在.proto文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同 的消息中使用它——采用MessageType.EnumType的语法格式。当对一个使用了枚举的.proto文件运行protocol buffer编译器的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对 Python来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。在反序列化的过程中,无法识别的枚举值会被保存在消息中,虽然这种表示方式需要依据所使用语言而定。在那些支持开放枚举类型超出指定范围之外的语言中(例如C++和Go),为识别的值会被表示成所支持的整型。在使用封闭枚举类型的语言中(Java),使用枚举中的一个类型来表示未识别的值,并且可以使用所支持整型来访问。在其他情况下,如果解析的消息被序列号,未识别的值将保持原样。更新一个消息类型如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。不要更改任何已有的字段的数值标识。如果你增加新的字段,使用旧格式的字段仍然可以被你新产生的代码所解析。你应该记住这些元素的默认值这样你的新代码就可以以适当的方式和旧代码产生的数据交互。相似的,通过新代码产生的消息也可以被旧代码解析:只不过新的字段会被忽视掉。注意,未被识别的字段会在反序列化的过程中丢弃掉,所以如果消息再被传递给新的代码,新的字段依然是不可用的(这和proto2中的行为是不同的,在proto2中未定义的域依然会随着消息被序列化)非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。string和bytes是兼容的——只要bytes是有效的UTF-8编码。嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。枚举类型与int32,uint32,int64和uint64相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化之后他们可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但是他的表示方式会依照语言而定。int类型的字段总会保留他们的AnyAny类型消息允许你在没有指定他们的.proto定义的情况下使用消息作为一个嵌套类型。一个Any类型包括一个可以被序列化bytes类型的任意消息,以及一个URL作为一个全局标识符和解析消息类型。为了使用Any类型,你需要导入 import google/protobuf/any.protoimport “google/protobuf/any.proto”;message ErrorStatus { string message = 1; repeated google.protobuf.Any details = 2;}对于给定的消息类型的默认类型URL是 type.googleapis.com/packagename.messagenameOneof如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存.Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它字段。 你可以使用 case() 或者 WhichOneof() 方法检查哪个 oneof 字段被设置, 看你使用什么语言了.使用Oneof为了在.proto定义Oneof字段, 你需要在名字前面加上oneof关键字, 比如下面例子的test_oneof:message SampleMessage { oneof test_oneof { string name = 4; SubMessage sub_message = 9; }}然后你可以增加oneof字段到 oneof 定义中. 你可以增加任意类型的字段, 但是不能使用repeated 关键字. ...

December 23, 2018 · 2 min · jiezi

Google Protobuf 编解码

Google Protobuf 优点:在谷歌内部长期使用, 产品成熟度高.跨语言、支持多种语言, 包括 C++、Java 和 Python.编码后的消息更小, 更加有利于存储和传输.编解码的性能非常高.支持不同协议版本的前向兼容.支持定义可选和必选字段.Protobuf 的入门Protobuf 是一个灵活、高效、结构化的数据序列化框架, 相比与 xml 等传统的序列化工具, 它更小、更快、更简单.Protobuf 支持数据结构化一次可以到处使用, 甚至跨语言使用, 通过代码生成工具可以自动生成不同语言版本的源代码, 甚至可以在使用不同版本的数据结构进程间进行数据传递, 实现数据结构前向兼容.定义消息类型syntax = “proto3”;message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3;}该文件的第一行指定使用 proto3 语法, 如果不写的话表示 proto2.分配字段编号string query = 1; 1 就是字段编号, 字段号主要用来标识二进制格式字段的. 1 到 15 字段号占一个字节. 16 到 2047 字段号需要两个字节.我们将对象转换为报文的时候, 是按照字段编号进行报文封装的; 我们接收到数据之后框架会帮我们按照字段号进行赋值.不能使用数字19000到19999, 因为它们是为 Google Protobuf 保留的.字段类型对应.proto TypeNotesC++ TypeJava Typedouble doubledoublefloat floatfloatint32使用可变长度编码, 对负数编码效率低下如果您的字段可能有负值, 则使用sint32代替.int32intint64使用可变长度编码, 对负数编码效率低下如果您的字段可能有负值, 则使用sint64代替.int64longuint32使用可变长度编码uint32intuint64使用可变长度编码uint64 longsint32使用可变长度编码有符号的int值这些编码比常规int32更有效地编码负数uint32intsint64使用可变长度编码有符号的int值这些编码比常规int64更有效地编码负数int64longfixed32四个字节, 如果值通常大于2的28次方, 则比uint32更有效uint32intfixed64四个字节, 如果值通常大于2的56次方, 则比uint64更有效uint64longsfixed32四个字节int32intsfixed64四个字节int64longbool boolbooleanstring字符串必须始终包含UTF-8编码或7位ASCII文本stringStringbytes字符串必须始终包含UTF-8编码或7位ASCII文本stringByteString默认值对于字符串, 默认值是空字符串.对于字节, 默认值为空字节.对于bool, 默认值为false.对于数字类型, 默认值为零.对于枚举, 默认值是第一个定义的枚举值, 必须为0.还请注意, 如果消息字段设置为默认值, 则该值将不会序列化.允许嵌套Protocol Buffers 定义 message 允许嵌套组合成更加复杂的消息message SearchResponse { repeated Result results = 1;}message Result { string url = 1; string title = 2; repeated string snippets = 3;}更多的例子:message SearchResponse { message Result { string url = 1; string title = 2; repeated string snippets = 3; } repeated Result results = 1;}message SomeOtherMessage { SearchResponse.Result result = 1;}message Outer { // Level 0 message MiddleAA { // Level 1 message Inner { // Level 2 int64 ival = 1; bool booly = 2; } } message MiddleBB { // Level 1 message Inner { // Level 2 int32 ival = 1; bool booly = 2; } }}导入定义可以在文件的顶部添加一个import语句:import “myproject/other_protos.proto”;未知字段未知字段就是解析器无法识别的字段. 例如, 当服务端使用新消息发送数据, 客户端使用旧消息解析数据, 那么这些新字段将成为旧消息中的未知字段.在3.5和更高版本中, 未知字段在解析过程中被保留, 并包含在序列化中输出.Map 类型repeated 类型可以用来表示数组, Map 类型则可以用来表示字典.map<key_type, value_type> map_field = N;map<string, Project> projects = 3;key_type 可以是任何 int 或者 string 类型(任何的标量类型, 具体可以见上面标量类型对应表格, 但是要除去 float、double 和 bytes)枚举值也不能作为 key.key_type 可以是除去 map 以外的任何类型.需要特别注意的是:map 是不能用 repeated 修饰的.map 迭代顺序的是不确定的, 所以你不能确定 map 是一个有序的.为 .proto 生成文本格式时, map 按 key 排序. 数字的 key 按数字排序.从数组中解析或合并时, 如果有重复的 key, 则使用所看到的最后一个 key(覆盖原则).从文本格式解析映射时, 如果有重复的 key, 解析可能会失败.Protocol Buffer 虽然不支持 map 类型的数组, 但是可以转换一下, 用以下思路实现 maps 数组:message MapFieldEntry { key_type key = 1; value_type value = 2;}repeated MapFieldEntry map_field = N;上述写法和 map 数组是完全等价的,所以用 repeated 巧妙的实现了 maps 数组的需求.Protocol Buffer 命名规范message 采用驼峰命名法. message 首字母大写开头. 字段名采用下划线分隔法命名.message SongServerRequest { required string song_name = 1;}枚举类型采用驼峰命名法. 枚举类型首字母大写开头. 每个枚举值全部大写, 并且采用下划线分隔法命名.enum Foo { FIRST_VALUE = 0; SECOND_VALUE = 1;}每个枚举值用分号结束, 不是逗号.服务名和方法名都采用驼峰命名法. 并且首字母都大写开头.service FooService { rpc GetSomething(FooRequest) returns (FooResponse);}总结message SubscribeReq { int32 subReqID = 1; string userName = 2; string productName = 3; string address = 4;}默认值比如我们创建了上面的消息类型, 我们在代码中设置 builder.setSubReqID(0); 为 0, 零是数值类型的默认值; 所以我们会看到序列化后的数据中, 没有对此字段进行序列化.byte[] arry = builder.build().toByteArray();arry 长度为 0. 对于字段类型是 string 类型的也是一样的; 也就是说显示赋值默认值也不会对其进行序列化.保留字段message SubscribeReq { reserved 2; int32 subReqID = 1; string userName = 2; string productName = 3; string address = 4;}顾名思义, 就是此字段会被保留可能在以后会使用此字段. 使用关键字 reserved 表示我要保留字段数 2.上面代码我们在生成 Java 文件的时候会出现 ubscribeReqPeoro.proto: Field “userName” uses reserved number 2 错误信息, 所以我们需要将 string userName = 2; 注释, 或者删除.保留后我们无法对其设置或序列化和反序列化. ...

December 21, 2018 · 2 min · jiezi

netty 基于 protobuf 协议 实现 websocket 版本的简易客服系统

结构netty 作为服务端protobuf 作为序列化数据的协议websocket 前端通讯演示netty 服务端实现Server.java 启动类import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.EventLoopGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.logging.LogLevel;import io.netty.handler.logging.LoggingHandler;import java.net.InetSocketAddress;//websocket长连接示例public class Server { public static void main(String[] args) throws Exception{ // 主线程组 EventLoopGroup bossGroup = new NioEventLoopGroup(); // 从线程组 EventLoopGroup wokerGroup = new NioEventLoopGroup(); try{ ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup,wokerGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ServerChannelInitializer()); ChannelFuture channelFuture = serverBootstrap.bind(new InetSocketAddress(8899)).sync(); channelFuture.channel().closeFuture().sync(); }finally { bossGroup.shutdownGracefully(); wokerGroup.shutdownGracefully(); } }}ServerChannelInitializer.javaimport com.example.nettydemo.protobuf.MessageData;import com.google.protobuf.MessageLite;import com.google.protobuf.MessageLiteOrBuilder;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelPipeline;import io.netty.channel.socket.SocketChannel;import io.netty.handler.codec.MessageToMessageDecoder;import io.netty.handler.codec.MessageToMessageEncoder;import io.netty.handler.codec.http.HttpObjectAggregator;import io.netty.handler.codec.http.HttpServerCodec;import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;import io.netty.handler.codec.http.websocketx.WebSocketFrame;import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;import io.netty.handler.codec.protobuf.ProtobufDecoder;import io.netty.handler.stream.ChunkedWriteHandler;import java.util.List;import static io.netty.buffer.Unpooled.wrappedBuffer;public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); // HTTP请求的解码和编码 pipeline.addLast(new HttpServerCodec()); // 把多个消息转换为一个单一的FullHttpRequest或是FullHttpResponse, // 原因是HTTP解码器会在每个HTTP消息中生成多个消息对象HttpRequest/HttpResponse,HttpContent,LastHttpContent pipeline.addLast(new HttpObjectAggregator(65536)); // 主要用于处理大数据流,比如一个1G大小的文件如果你直接传输肯定会撑暴jvm内存的; 增加之后就不用考虑这个问题了 pipeline.addLast(new ChunkedWriteHandler()); // WebSocket数据压缩 pipeline.addLast(new WebSocketServerCompressionHandler()); // 协议包长度限制 pipeline.addLast(new WebSocketServerProtocolHandler("/ws", null, true)); // 协议包解码 pipeline.addLast(new MessageToMessageDecoder<WebSocketFrame>() { @Override protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> objs) throws Exception { ByteBuf buf = ((BinaryWebSocketFrame) frame).content(); objs.add(buf); buf.retain(); } }); // 协议包编码 pipeline.addLast(new MessageToMessageEncoder<MessageLiteOrBuilder>() { @Override protected void encode(ChannelHandlerContext ctx, MessageLiteOrBuilder msg, List<Object> out) throws Exception { ByteBuf result = null; if (msg instanceof MessageLite) { result = wrappedBuffer(((MessageLite) msg).toByteArray()); } if (msg instanceof MessageLite.Builder) { result = wrappedBuffer(((MessageLite.Builder) msg).build().toByteArray()); } // ==== 上面代码片段是拷贝自TCP ProtobufEncoder 源码 ==== // 然后下面再转成websocket二进制流,因为客户端不能直接解析protobuf编码生成的 WebSocketFrame frame = new BinaryWebSocketFrame(result); out.add(frame); } }); // 协议包解码时指定Protobuf字节数实例化为CommonProtocol类型 pipeline.addLast(new ProtobufDecoder(MessageData.RequestUser.getDefaultInstance())); // websocket定义了传递数据的6中frame类型 pipeline.addLast(new ServerFrameHandler()); }}ServerFrameHandler.javaimport com.example.nettydemo.protobuf.MessageData;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.Channel;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.SimpleChannelInboundHandler;import io.netty.channel.group.ChannelGroup;import io.netty.channel.group.DefaultChannelGroup;import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;import io.netty.handler.codec.http.websocketx.WebSocketFrame;import io.netty.util.concurrent.GlobalEventExecutor;import java.util.List;//处理文本协议数据,处理TextWebSocketFrame类型的数据,websocket专门处理文本的frame就是TextWebSocketFramepublic class ServerFrameHandler extends SimpleChannelInboundHandler<MessageData.RequestUser> { private final ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); //读到客户端的内容并且向客户端去写内容 @Override protected void channelRead0(ChannelHandlerContext ctx, MessageData.RequestUser msg) throws Exception { // channelGroup.add(); Channel channel = ctx.channel(); System.out.println(msg.getUserName()); System.out.println(msg.getAge()); System.out.println(msg.getPassword()); MessageData.ResponseUser bank = MessageData .ResponseUser.newBuilder() .setUserName(“你好,请问有什么可以帮助你!”) .setAge(18).setPassword(“11111”).build(); channel.writeAndFlush(bank); } //每个channel都有一个唯一的id值 @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { //打印出channel唯一值,asLongText方法是channel的id的全名 // System.out.println(“handlerAdded:"+ctx.channel().id().asLongText()); } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { // System.out.println(“handlerRemoved:” + ctx.channel().id().asLongText()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println(“异常发生”); ctx.close(); } }protobuf 文件的使用proto 文件syntax =“proto2”;package com.example.nettydemo.protobuf;//optimize_for 加快解析的速度option optimize_for = SPEED;option java_package = “com.example.nettydemo.protobuf”;option java_outer_classname=“MessageData”;// 客户端发送过来的消息实体message RequestUser{ optional string user_name = 1; optional int32 age = 2; optional string password = 3;}// 返回给客户端的消息实体message ResponseUser{ optional string user_name = 1; optional int32 age = 2; optional string password = 3;}生成 proto 的Java 类批量生成工具,直接找到这个 bat 或者 sh 文件,在对应的平台执行就可以了具体可以自行百度 protobuf 怎么使用Windows 版本set outPath=../../javaset fileArray=(MessageDataProto ATestProto)# 将.proto文件生成java类for %%i in %fileArray% do ( echo generate cli protocol java code: %%i.proto protoc –java_out=%outPath% ./%%i.proto)pausesh 版本 地址: https://github.com/lmxdawn/ne...#!/bin/bashoutPath=../../javafileArray=(MessageDataProto ATestProto)for i in ${fileArray[@]};do echo “generate cli protocol java code: ${i}.proto” protoc –java_out=$outPath ./$i.protodonewebsocket 实现<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>WebSocket客户端</title></head><body><script src=“protobuf.min.js”></script><script type=“text/javascript”> var socket; //如果浏览器支持WebSocket if (window.WebSocket) { //参数就是与服务器连接的地址 socket = new WebSocket(“ws://localhost:8899/ws”); //客户端收到服务器消息的时候就会执行这个回调方法 socket.onmessage = function (event) { var ta = document.getElementById(“responseText”); // 解码 responseUserDecoder({ data: event.data, success: function (responseUser) { var content = “客服小姐姐: " + responseUser.userName + “, 小姐姐年龄: " + responseUser.age + “, 密码: " + responseUser.password; ta.value = ta.value + “\n” + content; }, fail: function (err) { console.log(err); }, complete: function () { console.log(“解码全部完成”) } }) } //连接建立的回调函数 socket.onopen = function (event) { var ta = document.getElementById(“responseText”); ta.value = “连接开启”; } //连接断掉的回调函数 socket.onclose = function (event) { var ta = document.getElementById(“responseText”); ta.value = ta.value + “\n” + “连接关闭”; } } else { alert(“浏览器不支持WebSocket!”); } //发送数据 function send(message) { if (!window.WebSocket) { return; } // socket.binaryType = “arraybuffer”; // 判断是否开启 if (socket.readyState !== WebSocket.OPEN) { alert(“连接没有开启”); return; } var data = { userName: message, age: 18, password: “11111” }; requestUserEncoder({ data: data, success: function (buffer) { console.log(“编码成功”); socket.send(buffer); }, fail: function (err) { console.log(err); }, complete: function () { console.log(“编码全部完成”) } }); } /** * 发送的消息编码成 protobuf / function requestUserEncoder(obj) { var data = obj.data; var success = obj.success; // 成功的回调 var fail = obj.fail; // 失败的回调 var complete = obj.complete; // 成功或者失败都会回调 protobuf.load(”../proto/MessageDataProto.proto”, function (err, root) { if (err) { if (typeof fail === “function”) { fail(err) } if (typeof complete === “function”) { complete() } return; } // Obtain a message type var RequestUser = root.lookupType(“com.example.nettydemo.protobuf.RequestUser”); // Exemplary payload var payload = data; // Verify the payload if necessary (i.e. when possibly incomplete or invalid) var errMsg = RequestUser.verify(payload); if (errMsg) { if (typeof fail === “function”) { fail(errMsg) } if (typeof complete === “function”) { complete() } return; } // Create a new message var message = RequestUser.create(payload); // or use .fromObject if conversion is necessary // Encode a message to an Uint8Array (browser) or Buffer (node) var buffer = RequestUser.encode(message).finish(); if (typeof success === “function”) { success(buffer) } if (typeof complete === “function”) { complete() } }); } /* * 接收到服务器二进制流的消息进行解码 */ function responseUserDecoder(obj) { var data = obj.data; var success = obj.success; // 成功的回调 var fail = obj.fail; // 失败的回调 var complete = obj.complete; // 成功或者失败都会回调 protobuf.load(”../proto/MessageDataProto.proto”, function (err, root) { if (err) { if (typeof fail === “function”) { fail(err) } if (typeof complete === “function”) { complete() } return; } // Obtain a message type var ResponseUser = root.lookupType(“com.example.nettydemo.protobuf.ResponseUser”); var reader = new FileReader(); reader.readAsArrayBuffer(data); reader.onload = function (e) { var buf = new Uint8Array(reader.result); var responseUser = ResponseUser.decode(buf); if (typeof success === “function”) { success(responseUser) } if (typeof complete === “function”) { complete() } } }); }</script><h1>欢迎访问客服系统</h1><form onsubmit=“return false”> <textarea name=“message” style=“width: 400px;height: 200px”></textarea> <input type=“button” value=“发送数据” onclick=“send(this.form.message.value);"> <h3>回复消息:</h3> <textarea id=“responseText” style=“width: 400px;height: 300px;"></textarea> <input type=“button” onclick=“javascript:document.getElementById(‘responseText’).value=’’” value=“清空数据”></form></body></html>扩展阅读spring boot 实现的后台管理系统vue + element-ui 实现的后台管理界面,接入 spring boot API接口 ...

December 20, 2018 · 5 min · jiezi

MessagePack 编解码

MessagePack 是一个高效的二进制序列化框架, 它像 JSON 一样支持不同语言间的数据交换, 但是它的性能更快, 序列化之后的码流更小.MessagePack 的特点如下:编解码高效, 性能高.序列化之后的码流小.支持跨语言.MessagePack 编码器和解码器开发<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>5.0.0.Alpha1</version></dependency>MessagePack 编码器开发public class MsgpackEncoder extends MessageToMessageEncoder<Object> { @Override protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception { MessagePack msgpack = new MessagePack(); byte[] bytes = msgpack.write(msg); out.add(bytes); }}负责将 Object 类型的 POJO 对象编码为 byte 数组, 然后添加到集合中.MessagePack 解码器开发public class MsgpackDecoder extends MessageToMessageDecoder<ByteBuf> { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception { int length = msg.readableBytes(); byte[] array = new byte[length]; msg.getBytes(msg.readerIndex(), array, 0 , length); MessagePack msgpack = new MessagePack(); out.add(msgpack.read(array)); }}首先从数据报 msg 中获取需要解码的 byte 数组, 然后调用 MessagePack 的 read 方法将其反序列化为 Objcet 对象, 将解码后的对象加入到 List 集合中. 这样就完成了 MessagePack 的解码操作.粘包/半包支持ch.pipeline().addLast(“frameDecoder”, new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));ch.pipeline().addLast(“msgpack decoder”, new MsgpackDecoder());ch.pipeline().addLast(“frameEncoder”, new LengthFieldPrepender(2));ch.pipeline().addLast(“msgpack encoder”, new MsgpackEncoder());利用 LengthFieldBasedFrameDecoder 和 LengthFieldPrepender, 结合新开发的 MessagePack 编解码框架, 实现对 TCP 粘包/半包支持.在 MessagePack 编码器之前增加 LengthFieldPrepender, 它将在 ByteBuf 之前增加 2 个字节的消息长度字段.+—————-+ +——–+—————-+| “HELLO, WORLD” |—>+ 0x000C | “HELLO, WORLD” |+—————-+ +——–+—————-+在 MessagePack 解码器之前增加 LengthFieldBasedFrameDecoder, 用于处理半包消息, 这样后面的 MsgpackDecoder 接收到的永远是整包消息.+——–+—————-+ +—————-++ 0x000C | “HELLO, WORLD” |—>| “HELLO, WORLD” |+——–+—————-+ +—————-+ ...

December 20, 2018 · 1 min · jiezi

编解码技术

基于 Java 提供的对象输入/输出流 ObjectInputStream 和 ObjectOutputStream, 可以直接把 Java 对象作为可存储的字节数组写入文件, 也可以传输到网络上. Java 序列化的目的主要有两个:网络传输对象持久化当进行远程跨进程服务调用时, 需要把被传输的 Java 对象编码为字节数组或者 ByteBuffer 对象. 而当远程服务读取到 ByteBuffer 对象或字节数组时, 需要将其解码为发送时的 Java 对象. 这被称为 Java 对象编解码技术.Java 序列化缺点Java 序列化仅仅是 Java 编解码技术的一种, 由于它的种种缺陷, 衍生除了多种解码器技术和框架.无法跨语言对于跨进程的服务调用, 服务提供者可能会使用 C++ 或其他语言开发, 当我们需要和其他语言交互时, 由于 Java 序列化技术是 Java 语言内部的私有协议, 其他语言并不支持, 所以无法对其进行反序列化.序列化后的码流太大下面我们通过一个实例看下 Java 序列化后的字节数组大小.public class UserInfo implements Serializable { private static final long serialVersionUID = 1L; private String userName; private int userID; public byte[] codeC() { ByteBuffer buffer = ByteBuffer.allocate(1024); byte[] value = this.userName.getBytes(); buffer.put(value); buffer.putInt(this.userID); buffer.flip(); value = null; byte[] result = new byte[buffer.remaining()]; buffer.get(result); return result; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public int getUserID() { return userID; } public void setUserID(int userID) { this.userID = userID; }}public class App { public static void main( String[] args ) throws Exception { UserInfo userInfo = new UserInfo(); userInfo.setUserID(100); userInfo.setUserName(“Welcome to Netty”); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(userInfo); objectOutputStream.flush(); objectOutputStream.close(); byte[] bytes = byteArrayOutputStream.toByteArray(); System.out.println(“The jdk serializable length is: " + bytes.length); System.out.println(“The byte array serializable length is: " + userInfo.codeC().length); }}测试结果The jdk serializable length is: 102The byte array serializable length is: 20测试结果令人震惊, 采用 JDK 序列化机制编码后的二进制数组大小尽然是二进制编码的 5.1 倍. 在同等情况下, 编码后的字节数组越大, 存储的时候就越占空间, 存储的硬件成本就越高, 并且在网络传输时更占带宽, 导致系统的吞吐量降低.序列化性能太低可以让创建代码循环 100 万次, 然后在前后加入获取系统时间.业界主流的编解码框架Google 的 Protobuf 介绍Protobuf 全称 Google Protocole Buffers, 它由谷歌开源而来, 在谷歌内部久经考验. 它将数据结构以 .proto 文件进行描述, 通过代码生成工具可以生成对应数据结构的 POJO 对象和 Protobuf 相关的属性和方法.它的特点如下.结构化数据存储格式(XML JSON等);高效的编解码性能;语言无关、平台无关、扩展性好;官方支持 Java、C++ 和 Python 三种语言.为什么不使用 xml. 尽管 xml 的可读性和可扩展性非常好, 也非常适合描述数据结构, 但是 xml 解析的时间开销和 xml 为了可读性而牺牲的空间开销都非常大, 因此不适合做高性能的通信协议. Protobuf 使用二进制编码, 在空间和性能上具有更大的优势.Protobuf 另一个比较吸引人的地方就是它的 数据描述文件和代码生成机制, 利用数据描述文件对数据结构进行说明的优点如下.文本化的数据结构描述语言, 可以实现语言和平台无关, 特别适合异构系统间的集成.通过标识字段的顺序, 可以实现协议的前向兼容;自动代码生成, 不需要手工编写同样数据结构的 C++ 和 Java 版本;方便后续的管理和维护. 相比于代码, 结构化的文档更容易管理和维护.总结我们判断一个编码器框架的优劣时, 往往会考虑以下几个因素.是否支持跨语言, 支持的语言种类是否丰富;编码后的码流大小;编解码的性能;类库是否小巧, API 使用是否方便;使用者需要手工开发的工作量和难度. ...

December 19, 2018 · 2 min · jiezi

分隔符和定长解码器的应用

TCP 以流的方式进行数据传输, 上层的应用协议为了对消息进行区分, 往往采用如下 4 中方式.消息长度固定, 累计读取到长度总和为定长 LEN 的报文后, 就认为读到了一个完整的消息; 将计数器置位, 重新开始读取下一个数据报;将回车换行符作为消息结束符, 例如 FTP 协议, 这种方式在文本协议中应用比较广泛.将特殊的分隔符最为消息的结束标志, 回车换行符就是一种特殊的结束分隔符;通过在消息头中定义长度字段来标识消息的总长度.Netty 对上面 4 种应用做了统一的抽象, 提供了 4 种解码器来解决对应用的问题.之前我写了 LineBasedFrameDecoder 解码器的使用, 今天开始学习如何使用 DelimiterBasedFrameDecoder 和 FixedLengthFrameDecoder.DelimiterBasedFrameDecoder 应用开发通过对 DelimiterBasedFrameDecoder 的使用, 我们可以自动完成以分隔符作为码流结束标识的消息的解码.还是修改 initChannel 方法ByteBuf delimiter = Unpooled.copiedBuffer("$".getBytes());ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));ch.pipeline().addLast(new StringDecoder());ch.pipeline().addLast(new TimeServerHandler());我们使用 $ 作为分隔符, 创建 DelimiterBasedFrameDecoder 对象, 将其加入到 ChannelPipeline 中. DelimiterBasedFrameDecoder 有多个构造方法, 这里我们传递两个参数: 第一个 1024 表示单条消息的最大长度, 当达到该长度后仍然没有检查到分隔符, 将抛出 TooLongFrameException 异常, 防止由于异常码流缺失分隔符导致的内存溢出, 这是 Netty 解码器的可靠性保护; 第二个参数就是分隔符缓冲对象.由于我们设置 DelimiterBasedFrameDecoder 过滤掉了分隔符, 所以返回给客户端时需要在请求消息尾部拼接分隔符 $_.FixedLengthFrameDecoder 应用开发FixedLengthFrameDecoder 是固定长度解码器, 它能够按照指定的长度对消息进行自动解码.ch.pipeline().addLast(new FixedLengthFrameDecoder(10));ch.pipeline().addLast(new StringDecoder());ch.pipeline().addLast(new TimeServerHandler());利用 FixedLengthFrameDecoder 解码器, 无论一次接收到多少数据报, 他都会按照构造函数中设置的固定长度进行解码, 如果是半包消息, FixedLengthFrameDecoder 会缓存半包消息并等待下个包到达后进行拼包, 直到读取到一个完整的包. ...

December 18, 2018 · 1 min · jiezi

使用 LineBasedFrameDecoder 和 StringDecoder 解决半包粘包问题

修改之前的 Netty 服务端开发 代码, 修改为下面代码public class TimeServer { public void bind(int port) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ChildChannelHandler()); // 绑定端口, 同步等待成功 ChannelFuture f = b.bind(port).sync(); // 等待服务端监听端口关闭 f.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new TimeServerHandler()); } } private class TimeServerHandler extends ChannelHandlerAdapter { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String body = (String) msg; System.out.println(body); ByteBuf resp = Unpooled.copiedBuffer(“6666”.getBytes()); ctx.write(resp); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } }}主要修改了 initChannel 和 channelRead 方法.LineBasedFrameDecoder 和 StringDecoder 原理分析LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节, 判断看是否有 \n 或 \r\n, 如果有, 就以此位置为结束位置, 从可读索引到结束位置区间的字节就组成了一行.它是以换行符为结束标志的解码器, 支持携带结束符或者不携带结束符两种解码方式, 同时支持配置单行的最大长度. 如果连续读取到最大长度后仍然没有发现换行符, 就会抛出异常, 同时忽略之前读到的异常码流.StringDecoder 的功能非常简单, 就是将收到的对象转换成字符串, 然后继续调用后面的 Handler.LineBasedFrameDecoder + StringDecoder 组合就是按行切换的文本解码器, 它被设计用来支持 TCP 的粘包和拆包. ...

December 18, 2018 · 1 min · jiezi

Netty 客户端

示例代码public class TimeClient { public void connect(int port, String host) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeClientHandler()); } }); ChannelFuture f = b.connect(host, port).sync(); f.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); } } private class TimeClientHandler extends ChannelHandlerAdapter { private ByteBuf firstMessage = null; public TimeClientHandler() { byte[] req = “66666”.getBytes(); firstMessage = Unpooled.buffer(req.length); firstMessage.writeBytes(req); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(firstMessage); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, “UTF-8”); System.out.println(body); } }}首先创建客户端处理 IO 读写的 NioEventLoopGroup 线程组, 然后继续创建客户端辅助启动类 Bootstrap, 随后需要对其进行配置.与服务端不同的是, 它的 Channel 需要设置为 NioSocketChannel, 然后为其添加 Handler. 此处为了简单直接创建匿名内部类, 实现 initChannel 方法, 其作用是当创建 NioSocketChannel 成功之后, 在进行初始化时, 将它的 ChannelHandler 设置到 ChannelPipeline 中, 用于处理网络 IO 事件.当客户端和服务端 TCP 链路建立成功之后, Netty 的 NIO 线程会调用 channelActive 方法, 发送查询时间的指令给服务端.当服务端返回应答消息时, channelRead 方法被调用. 发生异常时, 调用 exceptionCaught 方法. ...

December 17, 2018 · 1 min · jiezi

BIO、伪异步 IO、AIO和NIO

BIO采用 BIO 通信模型的服务端, 通常由一个独立的 Acceptor 线程负责监听客户端的连接, 它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理, 处理完成之后, 通过输出流返回应答给客户端, 线程销毁. 这就是典型的一请求一应答通信模型.该模型最大的问题就是缺乏弹性伸缩能力, 当客户端并发访问量增加后, 服务端的线程个数和客户端并发访问数是 1:1 的关系.当线程数过多之后, 系统性能就会下降, 系统也会发生线程堆栈溢出、创建新线程失败等问题, 最终导致进程宕机或者僵死, 不能对外提供服务.BIO 通信模型图伪异步 IO后端通过维护一个消息队列和 N 个活跃线程, 来处理多个客户端的请求接入, 当有新的客户端接入时, 将客户端的 Socket 封装成一个 Task (java.lang.Runnable 接口) 放入后端线的线程池进行处理.由于线程池可以设置消息队列的大小和最大线程数, 因此它的资源占用是可控的, 无论多少个客户端并发访问, 都不会导致资源耗尽和宕机.客户端个数 M, 线程池最大线程数 N 的比例关系, 其中 M 可以远远大于 N.注意: 当对 Socket 的输入流进行读取操作的时候,它会一直阻塞辖区, 直到发生如下三种事件:有数据可读.可用数据已经读取完毕.发生空指针或IO异常.伪异步 IO 模型图弊端当对方发送请求或应答消息比较缓慢, 或者网络传输比较慢时, 读取输入流一方的通信线程将被长时间阻塞, 如果对方要 60s 才能将数据发送完成, 读取一方的 IO 线程也将会被同步阻塞 60s, 在此期间, 其它接入消息只能在消息队列中排队.假如所有的可用线程都被故障服务器阻塞, 那后续所有的 IO 消息都将在队列中排队.由于线程池采用阻塞队列实现, 当队列积满之后, 后续入队列的操作将被阻塞.由于前端只有一个 Accptor 线程接收客户端接入, 它被阻塞在线程池的同步阻塞队列之后, 新的客户端请求消息将被拒绝, 客户端会发生大量的连接超时.NIO与 Socket 类和 ServerSocket 类相对应, NIO 也提供了 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现. 这两种新增的通道都支持阻塞和非阻塞两种模式.一般来说, 低负载、低并发的应用程序可以选择同步阻塞IO以降低编程复杂度; 对于高负载、高并发的网络模型应用, 需要使用NIO的非阻塞模式进行开发.缓冲区 BufferBuffer 是一个对象, 它包含一些要写入或要读出的数据. 在NIO库中, 所有数据都是用缓冲区处理的. 在读取数据时, 它是直接读取到缓冲区中的; 在写入数据时, 写入到缓冲区中. 任何时候访问NIO中的数据, 都是通过缓冲区进行操作的.缓冲区实质上是一个数组. 通常它是一个字节数组, 也可以使用其他种类的数组. 但是一个缓冲区不仅仅是一个数组, 缓冲区提供了对数据的结构化访问以及维护读写位置等信息.常用缓冲区是 ByteBuffer, 一个 ByteBuffer 提供了一种功能用于操作 byte 数组. 除了 ByteBuffer, 还有其他的一些缓冲区.ByteBuffer: 字节缓冲区CharBuffer: 字符缓冲区ShortBuffer: 短整形缓冲区IntBuffer: 整形缓冲区LongBuffer: 长整形缓冲区FloatBuffer: 浮点型缓冲区DoubleBuffer: 双精度浮点型缓冲区每一个 Buffer 类都是 Buffer 接口的一个子实例. 除了 ByteBuffer, 每个 Buffer 类都有完全一样的操作, 只是它们所处理的类型不一样.通道 ChannelChannel 是一个通道, 网络数据通过 Channel 读取和写入. 通道与流的不同之处在于通道是双向的, 流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream), 而通道可以用于读、写或者二者同时进行.Java NIO中最重要的几个Channel的实现:FileChannel: 用于文件的数据读写DatagramChannel: 用于UDP的数据读写SocketChannel: 用于TCP的数据读写. 一般是客户端实现ServerSocketChannel: 允许我们监听TCP链接请求, 每个请求会创建会一个SocketChannel. 一般是服务器实现多路复用器 Selector多路复用器提供选择已经就绪的任务的能力. 简单来讲, Selector 会不断的轮询注册在其上的 Channel, 如果某个 Channel 上面发生读或写事件, 这个 Channel 就处于就绪状态, 会被 Selector 轮询出来, 然后通过 SelectionKey 可以获取就绪 Channel 的集合, 进行后续的 IO 操作.一个多路复用器 Selector 可以同时轮询多个 Channel, 由于 JDK 使用了 epoll() 代替传统的 select 实现, 所以它并没有最大连接句柄 1024/2048 的限制. 这也意味着只需要一个线程负责 Selector 的轮询, 就可以接入成千上万的客户端.NIO 服务端序列图NIO创建的 TimeServer 源码分析public class MultiplexerTimeServer implements Runnable { private Selector selector; private ServerSocketChannel servChannel; private volatile boolean stop; /** * 初始化多路复用器、绑定监听端口 * * @param port / public MultiplexerTimeServer(int port) { try { // 创建多路复用器 selector = Selector.open(); // 打开 ServerSocketChannel 用来监听客户端的连接, 它是所有客户端连接的父管道. servChannel = ServerSocketChannel.open(); // 设置 ServerSocketChannel 为异步非阻塞模式 servChannel.configureBlocking(false); // 绑定地址和端口 servChannel.socket().bind(new InetSocketAddress(port), 1024); // 将 ServerSocketChannel 注册到 Reactor 线程的多路复用器 Selector 上, 监听 ACCEPT 事件 servChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println(“The time server is start in port : " + port); } catch (IOException e) { e.printStackTrace(); System.exit(1); } } public void stop() { this.stop = true; } / * (non-Javadoc) * * @see java.lang.Runnable#run() */ @Override public void run() { while (!stop) { try { selector.select(1000); Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> it = selectedKeys.iterator(); SelectionKey key = null; while (it.hasNext()) { key = it.next(); it.remove(); try { handleInput(key); } catch (Exception e) { if (key != null) { key.cancel(); if (key.channel() != null) { key.channel().close(); } } } } } catch (Throwable t) { t.printStackTrace(); } } // 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源 if (selector != null) { try { selector.close(); } catch (IOException e) { e.printStackTrace(); } } } private void handleInput(SelectionKey key) throws IOException { if (key.isValid()) { // 处理新接入的请求消息 if (key.isAcceptable()) { // Accept the new connection ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel sc = ssc.accept(); sc.configureBlocking(false); // Add the new connection to the selector sc.register(selector, SelectionKey.OP_READ); } if (key.isReadable()) { // Read the data SocketChannel sc = (SocketChannel) key.channel(); ByteBuffer readBuffer = ByteBuffer.allocate(1024); int readBytes = sc.read(readBuffer); if (readBytes > 0) { readBuffer.flip(); byte[] bytes = new byte[readBuffer.remaining()]; readBuffer.get(bytes); String body = new String(bytes, “UTF-8”); System.out.println(“The time server receive order : " + body); String currentTime = “QUERY TIME ORDER” .equalsIgnoreCase(body) ? new java.util.Date( System.currentTimeMillis()).toString() : “BAD ORDER”; doWrite(sc, currentTime); } else if (readBytes < 0) { // 对端链路关闭 key.cancel(); sc.close(); } else { ; // 读到0字节,忽略 } } } } private void doWrite(SocketChannel channel, String response) throws IOException { if (response != null && response.trim().length() > 0) { byte[] bytes = response.getBytes(); ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); writeBuffer.put(bytes); writeBuffer.flip(); channel.write(writeBuffer); } }}selector.select(1000); 休眠时间为1S, 无论是否有读写等事件发生, selector 每隔 1S 都被唤醒一次, selector 也提供了一个无参的 select 方法. 当有处于就绪状态的 Channel 时, selector 将返回就绪状态的 Channel 的 SelectionKey 集合, 我们通过对就绪状态的 Channel 集合进行迭代, 就可以进行网络的异步读写操作.key.isAcceptable() 来判断 当前 SelectionKey 的通道是否已准备好接受新的套接字连接(处理新接入的客户端请求消息). 通过 ServerSocketChannel 的 accept 接收客户端的连接请求并创建 SocketChannel 实例, 完成上述操作后, 相当于完成了TCP的三次握手, TCP物理链路正式建立. 注意,我们需要将新创建的 SocketChannel 设置为异步非阻塞, 同时也可以对其TCP参数进行设置, 例如TCP接收和发送缓冲区的大小等.根据 SelectionKey 的操作位进行判断即可获知网络事件的类型, 如 isAcceptable() 表示为 OP_ACCEPT key.isAcceptable() 来判断 当前 SelectionKey 的通道是否已准备好进行读取(读取客户端的请求消息). 首先创建一个 ByteBuffer, 由于我们事先无法得知客户端发送的码流大小, 作为例程, 我们开辟一个1M的缓冲区. 然后调用 SocketChannel 的 read 方法读取请求码流, 注意, 由于我们已经将 SocketChannel 设置为异步非阻塞模式, 因此它的 read 是非阻塞的. 使用返回值进行判断, 看读取到的字节数, 返回值有三种可能的结果:返回值大于0: 读到了字节, 对字节进行编解码;返回值等于0: 没有读取到字节, 属于正常场景, 忽略;返回值为-1: 链路已经关闭, 需要关闭SocketChannel, 释放资源.当读取到码流以后, 我们进行解码, 首先对 readBuffer 进行 flip 操作, 它的作用是将缓冲区当前的limit 设置为 position, position 设置为0, 用于后续对缓冲区的读取操作. 然后根据缓冲区可读的字节个数创建字节数组, 调用 ByteBuffer 的 get 操作将缓冲区可读的字节数组拷贝到新创建的字节数组中, 最后调用字符串的构造函数创建请求消息体并打印. 如果请求指令是 ”QUERY TIME ORDER” 则把服务器的当前时间编码后返回给客户端, 下面我们看看如果异步发送应答消息给客户端.doWrite 方法将消息异步发送给客户端, 首先将字符串编码成字节数组, 根据字节数组的长度创建 ByteBuffer, 调用 ByteBuffer 的 put 操作将字节数组拷贝到缓冲区中, 然后对缓冲区进行flip操作, 最后调用 SocketChannel 的 write 方法将缓冲区中的字节数组发送出去.需要指出的是, 由于 SocketChannel 是异步非阻塞的, 它并不保证一次能够把需要发送的字节数组发送完, 此时会出现“写半包”问题, 我们需要注册写操作, 不断轮询 Selector 将没有发送完的 ByteBuffer 发送完毕, 可以通过 ByteBuffer 的 hasRemain() 方法判断消息是否发送完成.AIO 编程NIO 2.0 引入了新的异步通道的概念, 并提供了异步文件通道和异步套接字通道的实现. 异步通道提供以下两种方式获取操作结果.通过 java.util.concurrent.Future 类来表示异步操作的结果.在执行异步操作的时候传入一个 java.nio.channelsCompletionHandler 接口的实现类作为操作完成的回调.NIO 2.0 的异步套接字通道是真正的异步非阻塞 IO , 对应与 UNIX 网络编程中的事件驱动 IO. 它不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写, 从而简化了 NIO 的编程模型. ...

December 14, 2018 · 4 min · jiezi

彻底理解Netty,这一篇文章就够了

Netty到底是什么从HTTP说起有了Netty,你可以实现自己的HTTP服务器,FTP服务器,UDP服务器,RPC服务器,WebSocket服务器,Redis的Proxy服务器,MySQL的Proxy服务器等等。我们回顾一下传统的HTTP服务器的原理1、创建一个ServerSocket,监听并绑定一个端口2、一系列客户端来请求这个端口3、服务器使用Accept,获得一个来自客户端的Socket连接对象4、启动一个新线程处理连接4.1、读Socket,得到字节流4.2、解码协议,得到Http请求对象4.3、处理Http请求,得到一个结果,封装成一个HttpResponse对象4.4、编码协议,将结果序列化字节流 写Socket,将字节流发给客户端5、继续循环步骤3HTTP服务器之所以称为HTTP服务器,是因为编码解码协议是HTTP协议,如果协议是Redis协议,那它就成了Redis服务器,如果协议是WebSocket,那它就成了WebSocket服务器,等等。 使用Netty你就可以定制编解码协议,实现自己的特定协议的服务器。NIO上面是一个传统处理http的服务器,但是在高并发的环境下,线程数量会比较多,System load也会比较高,于是就有了NIO。他并不是Java独有的概念,NIO代表的一个词汇叫着IO多路复用。它是由操作系统提供的系统调用,早期这个操作系统调用的名字是select,但是性能低下,后来渐渐演化成了Linux下的epoll和Mac里的kqueue。我们一般就说是epoll,因为没有人拿苹果电脑作为服务器使用对外提供服务。而Netty就是基于Java NIO技术封装的一套框架。为什么要封装,因为原生的Java NIO使用起来没那么方便,而且还有臭名昭著的bug,Netty把它封装之后,提供了一个易于操作的使用模式和接口,用户使用起来也就便捷多了。说NIO之前先说一下BIO(Blocking IO),如何理解这个Blocking呢?客户端监听(Listen)时,Accept是阻塞的,只有新连接来了,Accept才会返回,主线程才能继读写socket时,Read是阻塞的,只有请求消息来了,Read才能返回,子线程才能继续处理读写socket时,Write是阻塞的,只有客户端把消息收了,Write才能返回,子线程才能继续读取下一个请求传统的BIO模式下,从头到尾的所有线程都是阻塞的,这些线程就干等着,占用系统的资源,什么事也不干。那么NIO是怎么做到非阻塞的呢。它用的是事件机制。它可以用一个线程把Accept,读写操作,请求处理的逻辑全干了。如果什么事都没得做,它也不会死循环,它会将线程休眠起来,直到下一个事件来了再继续干活,这样的一个线程称之为NIO线程。用伪代码表示:while true { events = takeEvents(fds) // 获取事件,如果没有事件,线程就休眠 for event in events { if event.isAcceptable { doAccept() // 新链接来了 } elif event.isReadable { request = doRead() // 读消息 if request.isComplete() { doProcess() } } elif event.isWriteable { doWrite() // 写消息 } }}Reactor线程模型Reactor单线程模型一个NIO线程+一个accept线程:Reactor多线程模型Reactor主从模型主从Reactor多线程:多个acceptor的NIO线程池用于接受客户端的连接Netty可以基于如上三种模型进行灵活的配置。总结Netty是建立在NIO基础之上,Netty在NIO之上又提供了更高层次的抽象。在Netty里面,Accept连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理。Accept连接和读写操作也可以使用同一个线程池来进行处理。而请求处理逻辑既可以使用单独的线程池进行处理,也可以跟放在读写线程一块处理。线程池中的每一个线程都是NIO线程。用户可以根据实际情况进行组装,构造出满足系统需求的高性能并发模型。为什么选择Netty如果不用netty,使用原生JDK的话,有如下问题:1、API复杂2、对多线程很熟悉:因为NIO涉及到Reactor模式3、高可用的话:需要出路断连重连、半包读写、失败缓存等问题4、JDK NIO的bug而Netty来说,他的api简单、性能高而且社区活跃(dubbo、rocketmq等都使用了它)什么是TCP 粘包/拆包现象先看如下代码,这个代码是使用netty在client端重复写100次数据给server端,ByteBuf是netty的一个字节容器,里面存放是的需要发送的数据public class FirstClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) { for (int i = 0; i < 1000; i++) { ByteBuf buffer = getByteBuf(ctx); ctx.channel().writeAndFlush(buffer); } } private ByteBuf getByteBuf(ChannelHandlerContext ctx) { byte[] bytes = “需要更多资料加群:586446657”.getBytes(Charset.forName(“utf-8”)); ByteBuf buffer = ctx.alloc().buffer(); buffer.writeBytes(bytes); return buffer; }}从client端读取到的数据为:从服务端的控制台输出可以看出,存在三种类型的输出一种是正常的字符串输出。一种是多个字符串“粘”在了一起,我们定义这种 ByteBuf 为粘包。一种是一个字符串被“拆”开,形成一个破碎的包,我们定义这种 ByteBuf 为半包。透过现象分析原因应用层面使用了Netty,但是对于操作系统来说,只认TCP协议,尽管我们的应用层是按照 ByteBuf 为 单位来发送数据,server按照Bytebuf读取,但是到了底层操作系统仍然是按照字节流发送数据,因此,数据到了服务端,也是按照字节流的方式读入,然后到了 Netty 应用层面,重新拼装成 ByteBuf,而这里的 ByteBuf 与客户端按顺序发送的 ByteBuf 可能是不对等的。因此,我们需要在客户端根据自定义协议来组装我们应用层的数据包,然后在服务端根据我们的应用层的协议来组装数据包,这个过程通常在服务端称为拆包,而在客户端称为粘包。拆包和粘包是相对的,一端粘了包,另外一端就需要将粘过的包拆开,发送端将三个数据包粘成两个 TCP 数据包发送到接收端,接收端就需要根据应用协议将两个数据包重新组装成三个数据包。如何解决在没有 Netty 的情况下,用户如果自己需要拆包,基本原理就是不断从 TCP 缓冲区中读取数据,每次读取完都需要判断是否是一个完整的数据包 如果当前读取的数据不足以拼接成一个完整的业务数据包,那就保留该数据,继续从 TCP 缓冲区中读取,直到得到一个完整的数据包。 如果当前读到的数据加上已经读取的数据足够拼接成一个数据包,那就将已经读取的数据拼接上本次读取的数据,构成一个完整的业务数据包传递到业务逻辑,多余的数据仍然保留,以便和下次读到的数据尝试拼接。而在Netty中,已经造好了许多类型的拆包器,我们直接用就好:选好拆包器后,在代码中client段和server端将拆包器加入到chanelPipeline之中就好了:如上实例中:客户端:ch.pipeline().addLast(new FixedLengthFrameDecoder(31));服务端:ch.pipeline().addLast(new FixedLengthFrameDecoder(31));Netty 的零拷贝传统意义的拷贝是在发送数据的时候,传统的实现方式是:File.read(bytes)Socket.send(bytes)这种方式需要四次数据拷贝和四次上下文切换:数据从磁盘读取到内核的read buffer数据从内核缓冲区拷贝到用户缓冲区数据从用户缓冲区拷贝到内核的socket buffer数据从内核的socket buffer拷贝到网卡接口(硬件)的缓冲区零拷贝的概念明显上面的第二步和第三步是没有必要的,通过java的FileChannel.transferTo方法,可以避免上面两次多余的拷贝(当然这需要底层操作系统支持)调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer接着DMA从内核read buffer将数据拷贝到网卡接口buffer上面的两次操作都不需要CPU参与,所以就达到了零拷贝。Netty中的零拷贝主要体现在三个方面:1、bytebufferNetty发送和接收消息主要使用bytebuffer,bytebuffer使用对外内存(DirectMemory)直接进行Socket读写。原因:如果使用传统的堆内存进行Socket读写,JVM会将堆内存buffer拷贝一份到直接内存中然后再写入socket,多了一次缓冲区的内存拷贝。DirectMemory中可以直接通过DMA发送到网卡接口2、Composite Buffers传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。3、对于FileChannel.transferTo的使用Netty中使用了FileChannel的transferTo方法,该方法依赖于操作系统实现零拷贝。Netty 内部执行流程服务端:1、创建ServerBootStrap实例2、设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel3、设置并绑定服务端的channel4、5、创建处理网络事件的ChannelPipeline和handler,网络时间以流的形式在其中流转,handler完成多数的功能定制:比如编解码 SSl安全认证6、绑定并启动监听端口7、当轮训到准备就绪的channel后,由Reactor线程:NioEventLoop执行pipline中的方法,最终调度并执行channelHandler客户端总结以上就是我对Netty相关知识整理,如果有不同的见解,欢迎讨论! ...

November 24, 2018 · 1 min · jiezi

设计一个可拔插的 IOC 容器

前言磨了许久,借助最近的一次通宵上线 cicada 终于更新了 v2.0.0 版本。之所以大的版本号变为 2,确实是向下不兼容了;主要表现为:修复了几个反馈的 bug。灵活的路由方式。可拔插的 IOC 容器选择。其中重点是后面两个。新的路由方式先来看第一个:路由方式的更新。在之前的版本想要写一个接口必须的实现一个 WorkAction;而且最麻烦的是一个实现类只能做一个接口。因此也有朋友给我提过这个 issue。于是改进后的使用方式如下:是否有点似曾相识的感觉????。如上图所示,不需要实现某个特定的接口;只需要使用不同的注解即可。同时也支持自定义 pojo, cicada 会在调用过程中对参数进行实例化。拿这个 getUser 接口为例,当这样请求时这些参数就会被封装进 DemoReq 中.http://127.0.0.1:5688/cicada-example/routeAction/getUser?id=1234&name=zhangsan同时得到响应:{“message”:“hello =zhangsan”}实现过程也挺简单,大家查看源码便会发现;这里贴一点比较核心的步骤。扫描所有使用 @CicadaAction 注解的类。扫描所有使用 @CicadaRoute 注解的方法。将他们的映射关系存入 Map 中。请求时根据 URL 去 Map 中查找这个关系。反射构建参数及方法调用。扫描类以及写入映射关系请求时查询映射关系反射调用这些方法是否需要 IOC 容器上面那几个步骤其实我都是一把梭写完的,但当我写到执行具体方法时感觉有点意思了。大家都知道反射调用方法有两个重要的参数:obj 方法执行的实例。args.. 自然是方法的参数。我第一次写的时候是这样的:method.invoke(method.getDeclaringClass().newInstance(), object);然后一测试,也没问题。当我写完之后 review 代码时发现不对:这样这里每次都会创建一个新的实例,而且反射调用 newInstance() 效率也不高。这时我不自觉的想到了 Spring 中 IOC 容器,和这里场景也非常的类似。在应用初始化时将所有的接口实例化并保存到 bean 容器中,当需要使用时只需要从容器中获取即可。这样只是会在启动时做很多加载工作,但造福后代啊。可拔插的 IOC 容器于是我打算自己实现一个这样的 bean 容器。但在实现之前又想到一个 feature:不如把实现 bean 容器的方案交给使用者选择,可以选择使用 bean 容器,也可以就用之前的每次都创建新的实例,就像 Spring 中的 prototype 作用域一样。甚至可以自定义容器实现,比如将 bean 存放到数据库、Redis 都行;当然一般人也不会这么干。和 SPI 的机制也有点类似。要实现上述的需求大致需要以下步骤:一个通用的接口,包含了注册容器、从容器中获取实例等方法。BeanManager 类,由它来管理具体使用哪种 IOC 容器。所以首先定义了一个接口;CicadaBeanFactory:包含了注册和获取实例的接口。同时分别有两个不同的容器实现方案。默认实现;CicadaDefaultBean:也就是文中说道的,每次都会创建实例;由于这种方式其实根本就没有 bean 容器,所以也不存在注册了。接下来是真正的 IOC 容器;CicadaIoc:它将所有的实例都存放在一个 Map 中。当然也少不了刚才提到的 CicadaBeanManager,它会在应用启动的时候将所有的实例注册到 bean 容器中。重点是图中标红的部分:需要根据用户的选择实例化 CicadaBeanFactory 接口。将所有的实例注册到 CicadaBeanFactory 接口中。同时也提供了一个获取实例的方法:就是直接调用 CicadaBeanFactory 接口的方法。然后在上文提到的反射调用方法处就变为:从 bean 容器中获取实例了;获取的过程可以是每次都创建一个新的对象,也可以是直接从容器中获取实例。这点对于这里的调用者来说并不关心。所以这也实现了标题所说的:可拔插。为了实现这个目的,我将 CicadaIoc 的实现单独放到一个模块中,以 jar 包的形式提供实现。所以如果你想要使用 IOC 容器的方式获取实例时只需要在你的应用中额外加入这个 jar 包即可。<dependency> <groupId>top.crossoverjie.opensource</groupId> <artifactId>cicada-ioc</artifactId> <version>2.0.0</version></dependency>如果不使用则是默认的 CicadaDefaultBean 实现,也就是每次都会创建对象。这样有个好处:当你自己想实现一个 IOC 容器时;只需要实现 cicada 提供的 CicadaBeanFactory 接口,并在你的应用中只加入你的 jar 包即可。其余所有的代码都不需要改变,便可随意切换不的容器。当然我是推荐大家使用 IOC 容器的(其实就是单例),牺牲一点应用启动时间带来后续性能的提升是值得的。总结cicada 的大坑填的差不多了,后续也会做一些小功能的迭代。还没有关注的朋友赶紧关注一波:https://github.com/TogetherOS/cicadaPS:虽然没有仔细分析 Spring IOC 的实现,但相信看完此篇的朋友应该对 Spring IOC 以及 SpringMVC 会有一些自己的理解。你的点赞与分享是对我最大的支持 ...

November 15, 2018 · 1 min · jiezi

简易RPC框架:序列化机制

概述在上一篇文章《简易RPC框架:基于 netty 的协议编解码》中谈到对于协议的 decode 和 encode,在谈 decode 之前,必须先要知道 encode 的过程是什么,它把什么东西转化成了二进制协议。由于我们还未谈到具体的 RPC 调用机制,因此暂且认为 encode 就是把一个包含了调用信息的 Java 对象,从 client 经过序列化,变成一串二进制流,发送到了 server 端。这里需要明确的是,encode 的职责是拼协议,它不负责序列化,同样,decode 只是把整个二进制报文分割,哪部分是报文头,哪部分是报文体,诚然,报文体就是被序列化成二进制流的一个 Java 对象。对于调用方来说,先将调用信息封装成一个 Java 对象,经过序列化后形成二进制流,再经过 encode 阶段,拼接成一个完整的遵守我们定好的协议的报文。对于被调用方来说,则是收取完整的报文,在 decode 阶段将报文中的报文头,报文体分割出来,在序列化阶段将报文体反序列化为一个 Java 对象,从而获得调用信息。本文探讨序列化机制。基于 netty handler由于这个 RPC 框架基于 netty 实现,因此序列化机制其实体现在了 netty 的 pipeline 上的 handler 上。例如对于调用方,它需要在 pipeline 上加上一个 序列化 encode handler,用来序列化发出去的请求,同时需要加上一个反序列化的 decode handler, 以便反序列化调用结果。如下所示: protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new ProtocolEncoder()) .addLast(new ProtocolDecoder()) .addLast(new SerializationHandler(serialization)) .addLast(new DeserializationHandler(serialization)); }其中的 SerializationHandler 和 DeserializationHandler 就是上文提到的序列化 encode handler 和反序列化 decode handler。同样,对于被调用方来说,它也需要这两个handler,与调用方的 handler 编排顺序一致。其中,serialization 这个参数的对象代表具体的序列化机制策略。序列化机制上文中,SerializationHandler 和 DeserializationHandler 这两个对象都需要一个 serialization 对象作为参数,它是这么定义的:private ISerialization serialization = SerializationFactory.getSerialization(ServerDefaults.DEFAULT_SERIALIZATION_TYPE);采用工厂模式来创建具体的序列化机制:/** * 序列化工厂 * * @author beanlam * @version 1.0 /public class SerializationFactory { private SerializationFactory() { } public static ISerialization getSerialization(SerializationType type) { if (type == SerializationType.JDK) { return new JdkSerialization(); } return new HessianSerialization(); }}这里暂时只支持 JDK 原生序列化 和 基于 Hessian 的序列化机制,日后若有其他效率更高更适合的序列化机制,则可以在工厂类中进行添加。这里的 hessian 序列化是从 dubbo 中剥离出来的一块代码,感兴趣可以从 dubbo 的源码中的 com.caucho.hessian 包中获得。以 HessianSerialization 为例:/* * @author beanlam * @version 1.0 */public class HessianSerialization implements ISerialization { private ISerializer serializer = new HessianSerializer(); private IDeserializer deserializer = new HessianDeserializer(); @Override public ISerializer getSerializer() { return serializer; } @Override public IDeserializer getDeserializer() { return deserializer; } @Override public boolean accept(Class<?> clazz) { return Serializable.class.isAssignableFrom(clazz); }}根据 Hessian 的 API, 分别返回一个 hessian 的序列化器和反序列化器即可。 ...

November 11, 2018 · 1 min · jiezi

简易RPC框架:基于 netty 的协议编解码

概述在《简易RPC框架:需求与设计》这篇文章中已经给出了协议的具体细节,协议类型为二进制协议,如下: ———————————————————————— | magic (2bytes) | version (1byte) | type (1byte) | reserved (7bits) | ———————————————————————— | status (1byte) | id (8bytes) | body length (4bytes) | ———————————————————————— | | | body ($body_length bytes) | | | ————————————————————————协议的解码我们称为 decode,编码我们成为 encode,下文我们将直接使用 decode 和 encode 术语。decode 的本质就是讲接收到的一串二进制报文,转化为具体的消息对象,在 Java 中,就是将这串二进制报文所包含的信息,用某种类型的对象存储起来。encode 则是将存储了信息的对象,转化为具有相同含义的一串二进制报文,然后网络收发模块再将报文发出去。无论是 rpc 客户端还是服务端,都需要有一个 decode 和 encode 的逻辑。消息类型rpc 客户端与服务端之间的通信,需要通过发送不同类型的消息来实现,例如:client 向 server 端发送的消息,可能是请求消息,可能是心跳消息,可能是认证消息,而 server 向 client 发送的消息,一般就是响应消息。利用 Java 中的枚举类型,可以将消息类型进行如下定义:/** * 消息类型 * * @author beanlam * @version 1.0 / public enum MessageType { REQUEST((byte) 0x01), HEARTBEAT((byte) 0x02), CHECKIN((byte) 0x03), RESPONSE( (byte) 0x04), UNKNOWN((byte) 0xFF); private byte code; MessageType(byte code) { this.code = code; } public static MessageType valueOf(byte code) { for (MessageType instance : values()) { if (instance.code == code) { return instance; } } return UNKNOWN; } public byte getCode() { return code; }}在这个类中设计了 valueOf 方法,方便进行具体的 byte 字节与具体的消息枚举类型之间的映射和转换。调用状态设计client 主动发起的一次 rpc 调用,要么成功,要么失败,server 端有责任告知 client 此次调用的结果,client 也有责任去感知调用失败的原因,因为不一定是 server 端造成的失败,可能是因为 client 端在对消息进行预处理的时候,例如序列化,就已经出错了,这种错误也应该作为一次调用的调用结果返回给 client 调用者。因此引入一个调用状态,与消息类型一样,它也借助了 Java 语言里的枚举类型来实现,并实现了方便的 valueOf 方法:/* * 调用状态 * * @author beanlam * @version 1.0 /public enum InvocationStatus { OK((byte) 0x01), CLIENT_TIMEOUT((byte) 0x02), SERVER_TIMEOUT( (byte) 0x03), BAD_REQUEST((byte) 0x04), BAD_RESPONSE( (byte) 0x05), SERVICE_NOT_FOUND((byte) 0x06), SERVER_SERIALIZATION_ERROR( (byte) 0x07), CLIENT_SERIALIZATION_ERROR((byte) 0x08), CLIENT_CANCELED( (byte) 0x09), SERVER_BUSY((byte) 0x0A), CLIENT_BUSY( (byte) 0x0B), SERIALIZATION_ERROR((byte) 0x0C), INTERNAL_ERROR( (byte) 0x0D), SERVER_METHOD_INVOKE_ERROR((byte) 0x0E), UNKNOWN((byte) 0xFF); private byte code; InvocationStatus(byte code) { this.code = code; } public static InvocationStatus valueOf(byte code) { for (InvocationStatus instance : values()) { if (code == instance.code) { return instance; } } return UNKNOWN; } public byte getCode() { return code; }}消息实体设计我们将 client 往 server 端发送的统一称为 rpc 请求消息,一个请求对应着一个响应,因此在 client 和 server 端间流动的信息大体上其实就只有两种,即要么是请求,要么是响应。我们将会定义两个类,分别是 RpcRequest 和 RpcResponse 来代表请求消息和响应消息。另外由于无论是请求消息还是响应消息,它们都有一些共同的属性,例如说“调用上下文ID”,或者消息类型。因此会再定义一个 RpcMessage 类,作为父类。RpcMessage/* * rpc消息 * * @author beanlam * @version 1.0 /public class RpcMessage { private MessageType type; private long contextId; private Object data; public long getContextId() { return this.contextId; } public void setContextId(long id) { this.contextId = id; } public Object getData() { return this.data; } public void setData(Object data) { this.data = data; } public void setType(byte code) { this.type = MessageType.valueOf(code); } public MessageType getType() { return this.type; } public void setType(MessageType type) { this.type = type; } @Override public String toString() { return “[messageType=” + type.name() + “, contextId=” + contextId + “, data=” + data + “]”; }}RpcRequestimport java.util.concurrent.atomic.AtomicLong;/* * rpc请求消息 * * @author beanlam * @version 1.0 /public class RpcRequest extends RpcMessage { private static final AtomicLong ID_GENERATOR = new AtomicLong(0); public RpcRequest() { this(ID_GENERATOR.incrementAndGet()); } public RpcRequest(long contextId) { setContextId(contextId); setType(MessageType.REQUEST); }}RpcResponse/* * * rpc响应消息 * * @author beanlam * @version 1.0 /public class RpcResponse extends RpcMessage { private InvocationStatus status = InvocationStatus.OK; public RpcResponse(long contextId) { setContextId(contextId); setType(MessageType.RESPONSE); } public InvocationStatus getStatus() { return this.status; } public void setStatus(InvocationStatus status) { this.status = status; } @Override public String toString() { return “RpcResponse[contextId=” + getContextId() + “, status=” + status.name() + “]”; }}netty 编解码介绍netty 是一个 NIO 框架,应该这么说,netty 是一个有良好设计思想的 NIO 框架。一个 NIO 框架必备的要素就是 reactor 线程模型,目前有一些比较优秀而且开源的小型 NIO 框架,例如分库分表中间件 mycat 实现的一个简易 NIO 框架,可以在这里看到。netty 的主要特点有:微内核设计、责任链模式的业务逻辑处理、内存和资源泄露的检测等。其中编解码在 netty 中,都被设计成责任链上的一个一个 Handler。decode 对于 netty 来说,它提供了 ByteToMessageDecoder,它也提供了 MessageToByteEncoder。借助 netty 来实现协议编解码,实际上就是去在这两个handler里面实现编解码的逻辑。decode在实现 decode 逻辑时需要注意的一个问题是,由于二进制报文是在网络上发送的,因此一个完整的报文可能经过多个分组来发送的,什么意思呢,就是当有报文进来后,要确认报文是否完整,decode逻辑代码不能假设收到的报文就是一个完整报文,一般称这为“TCP半包问题”。同样,报文是连着报文发送的,意味着decode代码逻辑还要负责在一长串二进制序列中,分割出一个一个独立的报文,这称之为“TCP粘包问题”。netty 本身有提供一些方便的 decoder handler 来处理 TCP 半包和粘包的问题。不过一般情况下我们不会直接去用它,因为我们的协议比较简单,自己在代码里处理一下就可以了。完整的 decode 代码逻辑如下所示:import cn.com.agree.ats.rpc.message.;import cn.com.agree.ats.util.logfacade.AbstractPuppetLoggerFactory;import cn.com.agree.ats.util.logfacade.IPuppetLogger;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.ByteToMessageDecoder;import java.util.List;/** * 协议解码器 * * @author beanlam * @version 1.0 /public class ProtocolDecoder extends ByteToMessageDecoder { private static final IPuppetLogger logger = AbstractPuppetLoggerFactory .getInstance(ProtocolDecoder.class); private boolean magicChecked = false; @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> list) throws Exception { if (!magicChecked) { if (in.readableBytes() < ProtocolMetaData.MAGIC_LENGTH_IN_BYTES) { return; } magicChecked = true; if (!(in.getShort(in.readerIndex()) == ProtocolMetaData.MAGIC)) { logger.warn( “illegal data received without correct magic number, channel will be close”); ctx.close(); magicChecked = false; //this line of code makes no any sense, but it’s good for a warning return; } } if (in.readableBytes() < ProtocolMetaData.HEADER_LENGTH_IN_BYTES) { return; } int bodyLength = in .getInt(in.readerIndex() + ProtocolMetaData.BODY_LENGTH_OFFSET); if (in.readableBytes() < bodyLength + ProtocolMetaData.HEADER_LENGTH_IN_BYTES) { return; } magicChecked = false;// so far the whole packet was received in.readShort(); // skip the magic in.readByte(); // dont care about the protocol version so far byte type = in.readByte(); byte status = in.readByte(); long contextId = in.readLong(); byte[] body = new byte[in.readInt()]; in.readBytes(body); RpcMessage message = null; MessageType messageType = MessageType.valueOf(type); if (messageType == MessageType.RESPONSE) { message = new RpcResponse(contextId); ((RpcResponse) message).setStatus(InvocationStatus.valueOf(status)); } else { message = new RpcRequest(contextId); } message.setType(messageType); message.setData(body); list.add(message); }}可以看到,我们解决半包问题的时候,是判断有没有收到我们期望收到的报文,如果没有,直接在 decode 方法里面 return,等有更多的报文被收到的时候,netty 会自动帮我们调起 decode 方法。而我们解决粘包问题的思路也很清晰,那就是一次只处理一个报文,不去动后面的报文内容。还需要注意的是,在 netty 中,对于 ByteBuf 的 get 是不会消费掉报文的,而 read 是会消费掉报文的。当不确定报文是否收完整的时候,我们都是用 get开头的方法去试探性地验证报文是否接收完全,当确定报文接收完全后,我们才用 read 开头的方法去消费这段报文。encode直接贴代码,参考前文提到的协议格式阅读以下代码:/* * * 协议编码器 * * @author beanlam * @version 1.0 */public class ProtocolEncoder extends MessageToByteEncoder<RpcMessage> { @Override protected void encode(ChannelHandlerContext ctx, RpcMessage rpcMessage, ByteBuf out) throws Exception { byte status; byte[] data = (byte[]) rpcMessage.getData(); if (rpcMessage instanceof RpcRequest) { RpcRequest request = (RpcRequest) rpcMessage; status = InvocationStatus.OK.getCode(); } else { RpcResponse response = (RpcResponse) rpcMessage; status = response.getStatus().getCode(); } out.writeShort(ProtocolMetaData.MAGIC); out.writeByte(ProtocolMetaData.VERSION); out.writeByte(rpcMessage.getType().getCode()); out.writeByte(status); out.writeLong(rpcMessage.getContextId()); out.writeInt(data.length); out.writeBytes(data); }} ...

November 6, 2018 · 4 min · jiezi

Netty Channel源码分析

原文地址:https://wangwei.one/posts/net…前面,我们大致了解了Netty中的几个核心组件。今天我们就来先来介绍Netty的网络通信组件,用于执行网络I/O操作 —— Channel。Netty版本:4.1.30概述数据在网络中总是以字节的形式进行流通。我们在进行网络编程时选用何种传输方式编码(OIO、NIO等)决定了这些字节的传输方式。在没有Netty之前,为了提升系统的并发能力,从OIO切换到NIO时,需要对代码进行大量的重构,因为相应的Java NIO 与 IO API大不相同。而Netty在这些Java原生API的基础上做了一层封装,对用户提供了高度抽象而又统一的API,从而让传输方式的切换不在变得困难,只需要直接使用即可,而不需要对整个代码进行重构。Netty Channel UMLnetty channel族如下:整个族群中,AbstractChannel 是最为关键的一个抽象类,从它继承出了AbstractNioChannel、AbstractOioChannel、AbstractEpollChannel、LocalChannel、EmbeddedChannel等类,每个类代表了不同的协议以及相应的IO模型。除了 TCP 协议以外,Netty 还支持很多其他的连接协议,并且每种协议还有 NIO(异步 IO) 和 OIO(Old-IO,即传统的阻塞 IO) 版本的区别. 不同协议不同的阻塞类型的连接都有不同的 Channel 类型与之对应。下面是一些常用的 Channel 类型:NioSocketChannel:代表异步的客户端 TCP Socket 连接NioServerSocketChannel:异步的服务器端 TCP Socket 连接NioDatagramChannel:异步的 UDP 连接NioSctpChannel:异步的客户端 Sctp 连接NioSctpServerChannel:异步的 Sctp 服务器端连接OioSocketChannel:同步的客户端 TCP Socket 连接OioServerSocketChannel:同步的服务器端 TCP Socket 连接OioDatagramChannel:同步的 UDP 连接OioSctpChannel:同步的 Sctp 服务器端连接OioSctpServerChannel:同步的客户端 TCP Socket 连接Channel API我们先来看下最顶层接口 channel 主要的API,常用的如下:接口名描述eventLoop()Channel需要注册到EventLoop的多路复用器上,用于处理I/O事件,通过eventLoop()方法可以获取到Channel注册的EventLoop。EventLoop本质上就是处理网络读写事件的Reactor线程。在Netty中,它不仅仅用来处理网络事件,也可以用来执行定时任务和用户自定义NioTask等任务。pipeline()返回channel分配的ChannelPipelineisActive()判断channel是否激活。激活的意义取决于底层的传输类型。例如,一个Socket传输一旦连接到了远程节点便是活动的,而一个Datagram传输一旦被打开便是活动的localAddress()返回本地的socket地址remoteAddress()返回远程的socket地址flush()将之前已写的数据冲刷到底层Channel上去write(Object msg)请求将当前的msg通过ChannelPipeline写入到目标Channel中。注意,write操作只是将消息存入到消息发送环形数组中,并没有真正被发送,只有调用flush操作才会被写入到Channel中,发送给对方。writeAndFlush()等同于调用write()并接着调用flush()metadate()熟悉TCP协议的读者可能知道,当创建Socket的时候需要指定TCP参数,例如接收和发送的TCP缓冲区大小,TCP的超时时间。是否重用地址等。在Netty中,每个Channel对应一个物理链接,每个连接都有自己的TCP参数配置。所以,Channel会聚合一个ChannelMetadata用来对TCP参数提供元数据描述信息,通过metadata()方法就可以获取当前Channel的TCP参数配置。read()从当前的Channel中读取数据到第一个inbound缓冲区中,如果数据被成功读取,触发ChannelHandler.channelRead(ChannelHandlerContext,Object)事件。读取操作API调用完成后,紧接着会触发ChannelHander.channelReadComplete(ChannelHandlerContext)事件,这样业务的ChannelHandler可以决定是否需要继续读取数据。如果已经有操作请求被挂起,则后续的读操作会被忽略。close(ChannelPromise promise)主动关闭当前连接,通过ChannelPromise设置操作结果并进行结果通知,无论操作是否成功,都可以通过ChannelPromise获取操作结果。该操作会级联触发ChannelPipeline中所有ChannelHandler的ChannelHandler.close(ChannelHandlerContext,ChannelPromise)事件。parent()对于服务端Channel而言,它的父Channel为空;对于客户端Channel,它的父Channel就是创建它的ServerSocketChannel。id()返回ChannelId对象,ChannelId是Channel的唯一标识。Channel创建对Netty Channel API以及相关的类有了一个初步了解之后,接下来我们来详细了解一下在Netty的启动过程中Channel是如何创建的。服务端Channel的创建过程,主要分为四个步骤:1)Channel创建;2)Channel初始化;3)Channel注册;4)Channel绑定。我们以下面的代码为例进行解析:// 创建两个线程组,专门用于网络事件的处理,Reactor线程组// 用来接收客户端的连接,EventLoopGroup bossGroup = new NioEventLoopGroup();// 用来进行SocketChannel的网络读写EventLoopGroup workGroup = new NioEventLoopGroup();// 创建辅助启动类ServerBootstrap,并设置相关配置:ServerBootstrap b = new ServerBootstrap();// 设置处理Accept事件和读写操作的事件循环组b.group(bossGroup, workGroup) // 配置Channel类型 .channel(NioServerSocketChannel.class) // 配置监听地址 .localAddress(new InetSocketAddress(port)) // 设置服务器通道的选项,设置TCP属性 .option(ChannelOption.SO_KEEPALIVE, Boolean.TRUE) // 设置建立连接后的客户端通道的选项 .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // channel属性,便于保存用户自定义数据 .attr(AttributeKey.newInstance(“UserId”), “60293”) .handler(new LoggingHandler(LogLevel.INFO)) // 设置子处理器,主要是用户的自定义处理器,用于处理IO网络事件 .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(serverHandler); } });// 调用bind()方法绑定端口,sync()会阻塞等待处理请求。这是因为bind()方法是一个异步过程,会立即返回一个ChannelFuture对象,调用sync()会等待执行完成ChannelFuture f = b.bind().sync();// 获得Channel的closeFuture阻塞等待关闭,服务器Channel关闭时closeFuture会完成f.channel().closeFuture().sync();调用channel()接口设置 AbstractBootstrap 的成员变量 channelFactory,该变量顾名思义就是用于创建channel的工厂类。源码如下:…public B channel(Class<? extends C> channelClass) { if (channelClass == null) { throw new NullPointerException(“channelClass”); } // 创建 channelFactory return channelFactory(new ReflectiveChannelFactory<C>(channelClass));}…public B channelFactory(ChannelFactory<? extends C> channelFactory) { if (channelFactory == null) { throw new NullPointerException(“channelFactory”); } if (this.channelFactory != null) { throw new IllegalStateException(“channelFactory set already”); } this.channelFactory = channelFactory; return (B) this;}…channelFactory 设置为 ReflectiveChannelFactory ,在我们这个例子中 clazz 为 NioServerSocketChannel ,我们可以看到其中有个 newChannel() 接口,通过反射的方式来调用,这个接口的调用处我们后面会介绍到。源码如下:// Channel工厂类public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> { private final Class<? extends T> clazz; public ReflectiveChannelFactory(Class<? extends T> clazz) { if (clazz == null) { throw new NullPointerException(“clazz”); } this.clazz = clazz; } @Override public T newChannel() { try { // 通过反射来进行常见Channel实例 return clazz.newInstance(); } catch (Throwable t) { throw new ChannelException(“Unable to create Channel from class " + clazz, t); } } @Override public String toString() { return StringUtil.simpleClassName(clazz) + “.class”; }}接下来我们来看下 NioServerSocketChannel 的构造函数,主要就是:生成ServerSocketChannel对象。NioServerSocketChannel创建时,首先使用SelectorProvider的openServerSocketChannel打开服务器套接字通道。SelectorProvider是Java的NIO提供的抽象类,是选择器和可选择通道的服务提供者。具体的实现类有SelectorProviderImpl,EPollSelectorProvide,PollSelectorProvider。选择器的主要工作是根据操作系统类型和版本选择合适的Provider:如果LInux内核版本>=2.6则,具体的SelectorProvider为EPollSelectorProvider,否则为默认的PollSelectorProvider。设置 ServerSocketChannelConfig 成员变量。private static ServerSocketChannel newSocket(SelectorProvider provider) { try { // 调用JDK底层API生成 ServerSocketChannel 对象实例 return provider.openServerSocketChannel(); } catch (IOException e) { throw new ChannelException(“Failed to open a server socket.”, e); }}private final ServerSocketChannelConfig config;public NioServerSocketChannel() { this(newSocket(DEFAULT_SELECTOR_PROVIDER));}public NioServerSocketChannel(SelectorProvider provider) { this(newSocket(provider));}public NioServerSocketChannel(ServerSocketChannel channel) { // 调用 AbstractNioChannel 构造器,创建 NioServerSocketChannel,设置SelectionKey为ACCEPT super(null, channel, SelectionKey.OP_ACCEPT); // 创建ChannleConfig对象,主要是TCP参数配置类 config = new NioServerSocketChannelConfig(this, javaChannel().socket());}AbstractNioChannel 的构造器如下:protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) { // 调用 AbstractChannel 构造器 super(parent); this.ch = ch; // 从上一步过来,这里设置为 SelectionKey.OP_ACCEPT this.readInterestOp = readInterestOp; try { // 设置为非阻塞状态 ch.configureBlocking(false); } catch (IOException e) { try { ch.close(); } catch (IOException e2) { if (logger.isWarnEnabled()) { logger.warn(“Failed to close a partially initialized socket.”, e2); } } throw new ChannelException(“Failed to enter non-blocking mode.”, e); }}在 AbstractChannel 构造器中,会设Channel关联的三个核心对象:ChannelId、ChannelPipeline、Unsafe。初始化ChannelId,ChannelId是一个全局唯一的值;创建 NioMessageUnsafe 实例,该类为Channel提供了用于完成网络通讯相关的底层操作,如connect(),read(),register(),bind(),close()等;为Channel创建DefaultChannelPipeline,初始事件传播管道。关于Pipeline的分析,请看 后文 的分析。protected AbstractChannel(Channel parent) { this.parent = parent; // 设置ChannelId id = newId(); // 设置Unsafe unsafe = newUnsafe(); // 设置Pipeline pipeline = newChannelPipeline();}从 NioServerSocketChannelConfig 的构造函数追溯下去,在 DefaultChannelConfig 会设置channel成员变量。public DefaultChannelConfig(Channel channel) { this(channel, new AdaptiveRecvByteBufAllocator());}protected DefaultChannelConfig(Channel channel, RecvByteBufAllocator allocator) { setRecvByteBufAllocator(allocator, channel.metadata()); // 绑定channel this.channel = channel;}以上就是channel创建的过程,总结一下:通过 ReflectiveChannelFactory 工厂类,以反射的方式对channel进行创建;channel创建的过程中,会创建四个重要的对象:ChannelId、ChannelConfig、ChannelPipeline、Unsafe。Channel初始化主要分为以下两步:将启动器(Bootstrap)设置的选项和属性设置到NettyChannel上面向Pipeline添加初始化Handler,供注册后使用我们从 AbstractBootstrap 的 bind() 接口进去,调用链:bind() —> doBind(localAddress) —> initAndRegister() —> init(Channel channel),我们看下 ServerBootstrap 中 init() 接口的实现:final ChannelFuture initAndRegister() { Channel channel = null; try { // 调用Channel工程类的newChannel()接口,创建channel,就是前面我们讲的部分内容 channel = channelFactory.newChannel(); // 初始化channel init(channel); } catch (Throwable t) { ….}初始化Channel,我们来重点看下 init(channel) 接口:void init(Channel channel) throws Exception { // 获取启动器 启动时配置的option参数,主要是TCP的一些属性 final Map<ChannelOption<?>, Object> options = options0(); // 将获得到 options 配置到 ChannelConfig 中去 synchronized (options) { setChannelOptions(channel, options, logger); } // 获取 ServerBootstrap 启动时配置的 attr 参数 final Map<AttributeKey<?>, Object> attrs = attrs0(); // 配置 Channel attr,主要是设置用户自定义的一些参数 synchronized (attrs) { for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) { @SuppressWarnings(“unchecked”) AttributeKey<Object> key = (AttributeKey<Object>) e.getKey(); channel.attr(key).set(e.getValue()); } } // 获取channel中的 pipeline,这个pipeline使我们前面在channel创建过程中设置的 pipeline ChannelPipeline p = channel.pipeline(); // 将启动器中配置的 childGroup 保存到局部变量 currentChildGroup final EventLoopGroup currentChildGroup = childGroup; // 将启动器中配置的 childHandler 保存到局部变量 currentChildHandler final ChannelHandler currentChildHandler = childHandler; final Entry<ChannelOption<?>, Object>[] currentChildOptions; final Entry<AttributeKey<?>, Object>[] currentChildAttrs; // 保存用户设置的 childOptions 到局部变量 currentChildOptions synchronized (childOptions) { currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size())); } // 保存用户设置的 childAttrs 到局部变量 currentChildAttrs synchronized (childAttrs) { currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size())); } p.addLast(new ChannelInitializer<Channel>() { @Override public void initChannel(final Channel ch) throws Exception { final ChannelPipeline pipeline = ch.pipeline(); // 获取启动器上配置的handler ChannelHandler handler = config.handler(); if (handler != null) { // 添加 handler 到 pipeline 中 pipeline.addLast(handler); } ch.eventLoop().execute(new Runnable() { @Override public void run() { // 用child相关的参数创建出一个新连接接入器ServerBootstrapAcceptor // 通过 ServerBootstrapAcceptor 可以将一个新连接绑定到一个线程上去 // 每次有新的连接进来 ServerBootstrapAcceptor 都会用child相关的属性对它们进行配置,并注册到ChaildGroup上去 pipeline.addLast(new ServerBootstrapAcceptor( ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); } }); } });}对于新连接接入器 ServerBootstrapAcceptor 的分析 ,请查看 后文Channel注册在channel完成创建和初始化之后,接下来就需要将其注册到事件轮循器Selector上去。我们回到 initAndRegister 接口上去:final ChannelFuture initAndRegister() { … // 获取 EventLoopGroup ,并调用它的 register 方法来注册 channel ChannelFuture regFuture = config().group().register(channel); if (regFuture.cause() != null) { if (channel.isRegistered()) { channel.close(); } else { channel.unsafe().closeForcibly(); } } return regFuture;}最终会向下调用到 SingleThreadEventLoop 中的 register 接口:如何调用到这里,里面的细节需要等到后面文章讲到 MultithreadEventExecutorGroup 再详细说明@Overridepublic ChannelFuture register(final ChannelPromise promise) { ObjectUtil.checkNotNull(promise, “promise”); // 调用unsafe的register接口 promise.channel().unsafe().register(this, promise); return promise;}代码跟踪下去,直到 AbstractChannel 中的 AbstractUnsafe 这个类中的 register 接口。@Overridepublic final void register(EventLoop eventLoop, final ChannelPromise promise) { if (eventLoop == null) { throw new NullPointerException(“eventLoop”); } if (isRegistered()) { promise.setFailure(new IllegalStateException(“registered to an event loop already”)); return; } if (!isCompatible(eventLoop)) { promise.setFailure( new IllegalStateException(“incompatible event loop type: " + eventLoop.getClass().getName())); return; } // 将该Channel与eventLoop 进行绑定,后续与该channel相关的IO操作都由eventLoop来处理 AbstractChannel.this.eventLoop = eventLoop; // 初次注册时 eventLoop.inEventLoop() 返回false if (eventLoop.inEventLoop()) { // 调用实际的注册接口register0 register0(promise); } else { try { eventLoop.execute(new Runnable() { @Override public void run() { // 调用实际的注册接口register0 register0(promise); } }); } catch (Throwable t) { logger.warn( “Force-closing a channel whose registration task was not accepted by an event loop: {}”, AbstractChannel.this, t); closeForcibly(); closeFuture.setClosed(); safeSetFailure(promise, t); } }}register0接口主要分为以下三段逻辑:doRegister();pipeline.invokeHandlerAddedIfNeeded();pipeline.fireChannelRegistered();private void register0(ChannelPromise promise) { try { if (!promise.setUncancellable() || !ensureOpen(promise)) { return; } boolean firstRegistration = neverRegistered; // 调用 doRegister() 接口 doRegister(); neverRegistered = false; registered = true; // 通过pipeline的传播机制,触发handlerAdded事件 pipeline.invokeHandlerAddedIfNeeded(); safeSetSuccess(promise); // 通过pipeline的传播机制,触发channelRegistered事件 pipeline.fireChannelRegistered(); // 还没有绑定,所以这里的 isActive() 返回false. if (isActive()) { if (firstRegistration) { pipeline.fireChannelActive(); } else if (config().isAutoRead()) { beginRead(); } } } catch (Throwable t) { closeForcibly(); closeFuture.setClosed(); safeSetFailure(promise, t); }}我们来看 AbstractNioChannel 中的 doRegister()接口,最终调用的就是Java JDK底层的NIO API来注册。@Overrideprotected void doRegister() throws Exception { boolean selected = false; for (;;) { try { // eventLoop().unwrappedSelector():获取selector,将在后面介绍 EventLoop 创建时会讲到 // 将selector注册到Java NIO Channel上 // ops 设置为 0,表示不关心任何事件 // att 设置为 channel自身,表示后面还会将channel取出来用作它用(后面文章会讲到) selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); return; } catch (CancelledKeyException e) { if (!selected) { eventLoop().selectNow(); selected = true; } else { throw e; } } }}Channel绑定在完成创建、初始化以及注册之后,接下来就是Channel绑定操作。本小节涉及到的pipeline事件传播机制,我们放到后面的文章中去讲解。从启动器的bind()接口开始,往下调用 doBind() 方法:private ChannelFuture doBind(final SocketAddress localAddress) { // 初始化及注册 final ChannelFuture regFuture = initAndRegister(); final Channel channel = regFuture.channel(); if (regFuture.cause() != null) { return regFuture; } if (regFuture.isDone()) { // At this point we know that the registration was complete and successful. ChannelPromise promise = channel.newPromise(); // 调用 doBind0 doBind0(regFuture, channel, localAddress, promise); return promise; } else { …. }}doBind 方法又会调用 doBind0() 方法,在doBind0()方法中会通过EventLoop去执行channel的bind()任务,关于EventLoop的execute接口的分析,请看后面的 文章 。private static void doBind0( final ChannelFuture regFuture, final Channel channel, final SocketAddress localAddress, final ChannelPromise promise) { channel.eventLoop().execute(new Runnable() { @Override public void run() { if (regFuture.isSuccess()) { // 调用channel.bind接口 channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } else { promise.setFailure(regFuture.cause()); } } });}doBind0() 方法往下会条用到 pipeline.bind(localAddress, promise); 方法,通过pipeline的传播机制,最终会调用到 AbstractChannel.AbstractUnsafe.bind() 方法,这个方法主要做两件事情:调用doBind():调用底层JDK API进行Channel的端口绑定。调用pipeline.fireChannelActive():关于Pipeline的传播机制,请看 后文@Overridepublic final void bind(final SocketAddress localAddress, final ChannelPromise promise) { …. // wasActive 在绑定成功前为 false boolean wasActive = isActive(); try { // 调用doBind()调用JDK底层API进行端口绑定 doBind(localAddress); } catch (Throwable t) { safeSetFailure(promise, t); closeIfClosed(); return; } // 完成绑定之后,isActive() 返回true if (!wasActive && isActive()) { invokeLater(new Runnable() { @Override public void run() { // 触发channelActive事件 pipeline.fireChannelActive(); } }); } safeSetSuccess(promise);}我们这里看服务端 NioServerSocketChannel 实现的 doBind方法,最终会调用JDK 底层 NIO Channel的bind方法:@Overrideprotected void doBind(SocketAddress localAddress) throws Exception { if (PlatformDependent.javaVersion() >= 7) { javaChannel().bind(localAddress, config.getBacklog()); } else { javaChannel().socket().bind(localAddress, config.getBacklog()); }}调用 pipeline.fireChannelActive(),开始传播active事件,pipeline首先就会调用HeadContext节点进行事件传播,会调用到 DefaultChannelPipeline.HeadContext.channelActive() 方法:@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception { // 触发heanlder 的 ChannelActive 方法 ctx.fireChannelActive(); // 调用接口readIfIsAutoRead readIfIsAutoRead();}private void readIfIsAutoRead() { if (channel.config().isAutoRead()) { // 调用channel.read() channel.read(); }}channel.read() 方法往下会调用到 AbstractChannelHandlerContext.read() 方法:@Overridepublic ChannelHandlerContext read() { // 获取下一个ChannelHandlerContext节点 final AbstractChannelHandlerContext next = findContextOutbound(); // 获取EventExecutor EventExecutor executor = next.executor(); if (executor.inEventLoop()) { // 调用下一个节点的invokeRead接口 next.invokeRead(); } else { Runnable task = next.invokeReadTask; if (task == null) { next.invokeReadTask = task = new Runnable() { @Override public void run() { next.invokeRead(); } }; } executor.execute(task); } return this;}通过pipeline的事件传播机制,最终会调用到 AbstractChannel.AbstractUnsafe.beginRead() 方法:@Overridepublic final void beginRead() { assertEventLoop(); if (!isActive()) { return; } try { // 调用 doBeginRead(); doBeginRead(); } catch (final Exception e) { invokeLater(new Runnable() { @Override public void run() { pipeline.fireExceptionCaught(e); } }); close(voidPromise()); }}我们看下 AbstractNioChannel 对doBeginRead接口的实现逻辑:// 注册一个OP_ACCEPT@Overrideprotected void doBeginRead() throws Exception { // Channel.read() or ChannelHandlerContext.read() was called // 获取channel注册是的设置的 selectionKey final SelectionKey selectionKey = this.selectionKey; // selectionKey无效则返回 if (!selectionKey.isValid()) { return; } readPending = true; // 前面讲到channel在注册的时候,这是 interestOps 设置的是 0 final int interestOps = selectionKey.interestOps(); // readInterestOp 在前面讲到channel创建的时候,设置值为 SelectionKey.OP_ACCEPT if ((interestOps & readInterestOp) == 0) { // 最终 selectionKey 的兴趣集就会设置为 SelectionKey.OP_ACCEPT // 表示随时可以接收新连接的接入 selectionKey.interestOps(interestOps | readInterestOp); }}总结至此,我们就分析完了Channel的创建、初始化、注册、绑定的流程。其中涉及到的EventLoopGroup和Pipeline事件传播机制的知识点,我们放到后面的文章中去讲解。参考资料Java读源码之Netty深入剖析https://www.kancloud.cn/ssj23…https://segmentfault.com/a/11...https://segmentfault.com/a/11… ...

November 1, 2018 · 7 min · jiezi

Netty NioEventLoop 创建过程源码分析

原文链接:https://wangwei.one/posts/net…前面 ,我们分析了Netty中的Channel组件,本篇我们来介绍一下与Channel关联的另一个核心的组件 —— EventLoop。Netty版本:4.1.30概述EventLoop定义了Netty的核心抽象,用于处理网络连接生命周期中所有发生的事件。我们先来从一个比较高的视角来了解一下Channels、Thread、EventLoops、EventLoopGroups之间的关系。上图是表示了拥有4个EventLoop的EventLoopGroup处理IO的流程图。它们之间的关系如下:一个 EventLoopGroup包含一个或多个EventLoop一个 EventLoop在它的生命周期内只和一个Thread绑定所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理一个Channel在它的生命周期内只注册于一个EventLoop一个EventLoop可能会被分配给一个或多个ChannelEventLoop 原理下图是Netty EventLoop相关类的UML图。从中我们可以看到EventLoop相关的类都是实现了 java.util.concurrent 包中的 ExecutorService 接口。我们可以直接将任务(Runable 或 Callable) 提交给EventLoop去立即执行或定时执行。例如,使用EventLoop去执行定时任务,样例代码:public static void scheduleViaEventLoop() { Channel ch = new NioSocketChannel(); ScheduledFuture<?> future = ch.eventLoop().schedule( () -> System.out.println(“60 seconds later”), 60, TimeUnit.SECONDS);}Thread 管理Netty线程模型的高性能主要取决于当前所执行线程的身份的确定。一个线程提交到EventLoop执行的流程如下:将Task任务提交给EventLoop执行在Task传递到execute方法之后,检查当前要执行的Task的线程是否是分配给EventLoop的那个线程如果是,则该线程会立即执行如果不是,则将线程放入任务队列中,等待下一次执行其中,Netty中的每一个EventLoop都有它自己的任务队列,并且和其他的EventLoop的任务队列独立开来。Thread 分配服务于Channel的I/O和事件的EventLoop包含在EventLoopGroup中。根据不同的传输实现,EventLoop的创建和分配方式也不同。NIO传输在NIO传输方式中,使用尽可能少的EventLoop就可以服务多个Channel。如图所示,EventLoopGroup采用顺序循环的方式负责为每一个新创建的Channel分配EventLoop,每一个EventLoop会被分配给多个Channels。一旦一个Channel被分配给了一个EventLoop,则这个Channel的生命周期内,只会绑定这个EventLoop。这就让我们在ChannelHandler的实现省去了对线程安全和同步问题的担心。OIO传输与NIO方式的不同在于,一个EventLoop只会服务于一个Channel。NioEventLoop & NioEventLoopGroup 创建初步了解了 EventLoop 以及 EventLoopGroup 的工作机制,接下来我们以 NioEventLoopGroup 为例,来深入分析 NioEventLoopGroup 是如何创建的,又是如何启动的,它的内部执行逻辑又是怎样的等等问题。MultithreadEventExecutorGroup 构造器我们从 NioEventLoopGroup 的构造函数开始分析:EventLoopGroup acceptorEventLoopGroup = new NioEventLoopGroup(1);NioEventLoopGroup构造函数会调用到父类 MultithreadEventLoopGroup 的构造函数,默认情况下,EventLoop的数量 = 处理器数量 x 2:public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup { private static final InternalLogger logger = InternalLoggerFactory.getInstance(MultithreadEventLoopGroup.class); private static final int DEFAULT_EVENT_LOOP_THREADS; // 默认情况下,EventLoop的数量 = 处理器数量 x 2 static { DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt( “io.netty.eventLoopThreads”, NettyRuntime.availableProcessors() * 2)); if (logger.isDebugEnabled()) { logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS); } } protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object… args) { super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args); } …}继续调用父类,会调用到 MultithreadEventExecutorGroup 的构造器,主要做三件事情:创建线程任务执行器 ThreadPerTaskExecutor通过for循环创建数量为 nThreads 个的 EventLoop创建 EventLoop 选择器 EventExecutorChooserprotected MultithreadEventExecutorGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, Object… args) { if (nThreads <= 0) { throw new IllegalArgumentException(String.format(“nThreads: %d (expected: > 0)”, nThreads)); } // 创建任务执行器 ThreadPerTaskExecutor if (executor == null) { executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); } // 创建 EventExecutor 数组 children = new EventExecutor[nThreads]; // 通过for循环创建数量为 nThreads 个的 EventLoop for (int i = 0; i < nThreads; i ++) { boolean success = false; try { // 调用 newChild 接口 children[i] = newChild(executor, args); success = true; } catch (Exception e) { // TODO: Think about if this is a good exception type throw new IllegalStateException(“failed to create a child event loop”, e); } finally { if (!success) { for (int j = 0; j < i; j ++) { children[j].shutdownGracefully(); } for (int j = 0; j < i; j ++) { EventExecutor e = children[j]; try { while (!e.isTerminated()) { e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); } } catch (InterruptedException interrupted) { // Let the caller handle the interruption. Thread.currentThread().interrupt(); break; } } } } } // 创建选择器 chooser = chooserFactory.newChooser(children); final FutureListener<Object> terminationListener = new FutureListener<Object>() { @Override public void operationComplete(Future<Object> future) throws Exception { if (terminatedChildren.incrementAndGet() == children.length) { terminationFuture.setSuccess(null); } } }; for (EventExecutor e: children) { e.terminationFuture().addListener(terminationListener); } Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length); Collections.addAll(childrenSet, children); readonlyChildren = Collections.unmodifiableSet(childrenSet);}创建线程任务执行器 ThreadPerTaskExecutorif (executor == null) { executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());}线程任务执行器 ThreadPerTaskExecutor 源码如下,具体的任务都由 ThreadFactory 去执行:public final class ThreadPerTaskExecutor implements Executor { private final ThreadFactory threadFactory; public ThreadPerTaskExecutor(ThreadFactory threadFactory) { if (threadFactory == null) { throw new NullPointerException(“threadFactory”); } this.threadFactory = threadFactory; } // 使用 threadFactory 执行任务 @Override public void execute(Runnable command) { threadFactory.newThread(command).start(); }}来看看 newDefaultThreadFactory 方法:protected ThreadFactory newDefaultThreadFactory() { return new DefaultThreadFactory(getClass());}DefaultThreadFactory接下来看看 DefaultThreadFactory 这个类,实现了 ThreadFactory 接口,我们可以了解到:EventLoopGroup的命名规则具体的线程为 FastThreadLocalThreadpublic class DefaultThreadFactory implements ThreadFactory { // 线程池ID编号自增器 private static final AtomicInteger poolId = new AtomicInteger(); // 线程ID自增器 private final AtomicInteger nextId = new AtomicInteger(); // 线程名称前缀 private final String prefix; // 是否为守护进程 private final boolean daemon; // 线程优先级 private final int priority; // 线程组 protected final ThreadGroup threadGroup; public DefaultThreadFactory(Class<?> poolType) { this(poolType, false, Thread.NORM_PRIORITY); } … // 获取线程名,返回结果:nioEventLoopGroup public static String toPoolName(Class<?> poolType) { if (poolType == null) { throw new NullPointerException(“poolType”); } String poolName = StringUtil.simpleClassName(poolType); switch (poolName.length()) { case 0: return “unknown”; case 1: return poolName.toLowerCase(Locale.US); default: if (Character.isUpperCase(poolName.charAt(0)) && Character.isLowerCase(poolName.charAt(1))) { return Character.toLowerCase(poolName.charAt(0)) + poolName.substring(1); } else { return poolName; } } } public DefaultThreadFactory(String poolName, boolean daemon, int priority, ThreadGroup threadGroup) { if (poolName == null) { throw new NullPointerException(“poolName”); } if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) { throw new IllegalArgumentException( “priority: " + priority + " (expected: Thread.MIN_PRIORITY <= priority <= Thread.MAX_PRIORITY)”); } // nioEventLoopGroup-2- prefix = poolName + ‘-’ + poolId.incrementAndGet() + ‘-’; this.daemon = daemon; this.priority = priority; this.threadGroup = threadGroup; } public DefaultThreadFactory(String poolName, boolean daemon, int priority) { this(poolName, daemon, priority, System.getSecurityManager() == null ? Thread.currentThread().getThreadGroup() : System.getSecurityManager().getThreadGroup()); } @Override public Thread newThread(Runnable r) { // 创建新线程 nioEventLoopGroup-2-1 Thread t = newThread(FastThreadLocalRunnable.wrap(r), prefix + nextId.incrementAndGet()); try { if (t.isDaemon() != daemon) { t.setDaemon(daemon); } if (t.getPriority() != priority) { t.setPriority(priority); } } catch (Exception ignored) { // Doesn’t matter even if failed to set. } return t; } // 创建新线程 FastThreadLocalThread protected Thread newThread(Runnable r, String name) { return new FastThreadLocalThread(threadGroup, r, name); } }创建NioEventLoop继续从 MultithreadEventExecutorGroup 构造器开始,创建完任务执行器 ThreadPerTaskExecutor 之后,进入for循环,开始创建 NioEventLoop:for (int i = 0; i < nThreads; i ++) { boolean success = false; try { // 创建 nioEventLoop children[i] = newChild(executor, args); success = true; } catch (Exception e) { // TODO: Think about if this is a good exception type throw new IllegalStateException(“failed to create a child event loop”, e); } … } NioEventLoopGroup类中的 newChild() 方法:@Overrideprotected EventLoop newChild(Executor executor, Object… args) throws Exception { return new NioEventLoop(this, executor, (SelectorProvider) args[0], ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);}NioEventLoop 构造器:public final class NioEventLoop extends SingleThreadEventLoop{ … NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider, SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) { // 调用父类 SingleThreadEventLoop 构造器 super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler); if (selectorProvider == null) { throw new NullPointerException(“selectorProvider”); } if (strategy == null) { throw new NullPointerException(“selectStrategy”); } // 设置 selectorProvider provider = selectorProvider; // 获取 SelectorTuple 对象,里面封装了原生的selector和优化过的selector final SelectorTuple selectorTuple = openSelector(); // 设置优化过的selector selector = selectorTuple.selector; // 设置原生的selector unwrappedSelector = selectorTuple.unwrappedSelector; // 设置select策略 selectStrategy = strategy; } … }接下来我们看看 获取多路复用选择器 方法—— openSelector() ,// selectKey 优化选项flagprivate static final boolean DISABLE_KEYSET_OPTIMIZATION = SystemPropertyUtil.getBoolean(“io.netty.noKeySetOptimization”, false);private SelectorTuple openSelector() { // JDK原生的selector final Selector unwrappedSelector; try { // 通过 SelectorProvider 创建获得selector unwrappedSelector = provider.openSelector(); } catch (IOException e) { throw new ChannelException(“failed to open a new selector”, e); } // 如果不优化,则直接返回 if (DISABLE_KEYSET_OPTIMIZATION) { return new SelectorTuple(unwrappedSelector); } // 通过反射创建 sun.nio.ch.SelectorImpl 对象 Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { try { return Class.forName( “sun.nio.ch.SelectorImpl”, false, PlatformDependent.getSystemClassLoader()); } catch (Throwable cause) { return cause; } } }); // 如果 maybeSelectorImplClass 不是 selector 的一个实现,则直接返回原生的Selector if (!(maybeSelectorImplClass instanceof Class) || // ensure the current selector implementation is what we can instrument. // 确保当前的选择器实现是我们可以检测的 !((Class<?>) maybeSelectorImplClass).isAssignableFrom(unwrappedSelector.getClass())) { if (maybeSelectorImplClass instanceof Throwable) { Throwable t = (Throwable) maybeSelectorImplClass; logger.trace(“failed to instrument a special java.util.Set into: {}”, unwrappedSelector, t); } return new SelectorTuple(unwrappedSelector); } // maybeSelectorImplClass 是selector的实现,则转化为 selector 实现类 final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass; // 创建新的 SelectionKey 集合 SelectedSelectionKeySet,内部采用的是 SelectionKey 数组的形 // 式,而非 set 集合 final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet(); Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { try { // 通过反射的方式获取 sun.nio.ch.SelectorImpl 的成员变量 selectedKeys Field selectedKeysField = selectorImplClass.getDeclaredField(“selectedKeys”); // 通过反射的方式获取 sun.nio.ch.SelectorImpl 的成员变量 publicSelectedKeys Field publicSelectedKeysField = selectorImplClass.getDeclaredField(“publicSelectedKeys”); if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) { // Let us try to use sun.misc.Unsafe to replace the SelectionKeySet. // This allows us to also do this in Java9+ without any extra flags. long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField); long publicSelectedKeysFieldOffset = PlatformDependent.objectFieldOffset(publicSelectedKeysField); if (selectedKeysFieldOffset != -1 && publicSelectedKeysFieldOffset != -1) { PlatformDependent.putObject( unwrappedSelector, selectedKeysFieldOffset, selectedKeySet); PlatformDependent.putObject(unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet); return null; } // We could not retrieve the offset, lets try reflection as last-resort. } // 设置字段 selectedKeys Accessible 为true Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true); if (cause != null) { return cause; } // 设置字段 publicSelectedKeys Accessible 为true cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true); if (cause != null) { return cause; } selectedKeysField.set(unwrappedSelector, selectedKeySet); publicSelectedKeysField.set(unwrappedSelector, selectedKeySet); return null; } catch (NoSuchFieldException e) { return e; } catch (IllegalAccessException e) { return e; } } }); if (maybeException instanceof Exception) { selectedKeys = null; Exception e = (Exception) maybeException; logger.trace(“failed to instrument a special java.util.Set into: {}”, unwrappedSelector, e); return new SelectorTuple(unwrappedSelector); } // 设置 SelectedSelectionKeySet selectedKeys = selectedKeySet; logger.trace(“instrumented a special java.util.Set into: {}”, unwrappedSelector); // 返回包含了原生selector和优化过的selector的SelectorTuple return new SelectorTuple(unwrappedSelector, new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));}优化后的 SelectedSelectionKeySet 对象,内部采用 SelectionKey 数组的形式:final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> { SelectionKey[] keys; int size; SelectedSelectionKeySet() { keys = new SelectionKey[1024]; } // 使用数组,来替代HashSet,可以降低时间复杂度为O(1) @Override public boolean add(SelectionKey o) { if (o == null) { return false; } keys[size++] = o; if (size == keys.length) { increaseCapacity(); } return true; } @Override public boolean remove(Object o) { return false; } @Override public boolean contains(Object o) { return false; } @Override public int size() { return size; } @Override public Iterator<SelectionKey> iterator() { return new Iterator<SelectionKey>() { private int idx; @Override public boolean hasNext() { return idx < size; } @Override public SelectionKey next() { if (!hasNext()) { throw new NoSuchElementException(); } return keys[idx++]; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } void reset() { reset(0); } void reset(int start) { Arrays.fill(keys, start, size, null); size = 0; } // 扩容 private void increaseCapacity() { SelectionKey[] newKeys = new SelectionKey[keys.length << 1]; System.arraycopy(keys, 0, newKeys, 0, size); keys = newKeys; }}SingleThreadEventLoop 构造器public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor implements EventLoop { … protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor, boolean addTaskWakesUp, int maxPendingTasks, RejectedExecutionHandler rejectedExecutionHandler) { // 调用 SingleThreadEventExecutor 构造器 super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler); tailTasks = newTaskQueue(maxPendingTasks); } …}SingleThreadEventExecutor 构造器,主要做两件事情:设置线程任务执行器。设置任务队列。前面讲到EventLoop对于不能立即执行的Task会放入一个队列中,就是这里设置的。public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor { … protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor, boolean addTaskWakesUp, int maxPendingTasks, RejectedExecutionHandler rejectedHandler) { super(parent); this.addTaskWakesUp = addTaskWakesUp; this.maxPendingTasks = Math.max(16, maxPendingTasks); // 设置线程任务执行器 this.executor = ObjectUtil.checkNotNull(executor, “executor”); // 设置任务队列 taskQueue = newTaskQueue(this.maxPendingTasks); rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, “rejectedHandler”); } … }NioEventLoop 中对 newTaskQueue 接口的实现,返回的是 JCTools 工具包 Mpsc 队列。后面我们写文章单独介绍 JCTools 中的相关队列。Mpsc:Multi Producer Single Consumer (Lock less, bounded and unbounded)多个生产者对单个消费者(无锁、有界和无界都有实现)public final class NioEventLoop extends SingleThreadEventLoop { … @Override protected Queue<Runnable> newTaskQueue(int maxPendingTasks) { // This event loop never calls takeTask() return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue() : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks); } …}创建线程执行选择器chooser接下来,我们看看 MultithreadEventExecutorGroup 构造器的最后一个部分内容,创建线程执行选择器chooser,它的主要作用就是 EventLoopGroup 用于从 EventLoop 数组中选择一个 EventLoop 去执行任务。// 创建选择器chooser = chooserFactory.newChooser(children);EventLoopGroup 中定义的 next() 接口:public interface EventLoopGroup extends EventExecutorGroup { … // 选择下一个 EventLoop 用于执行任务 @Override EventLoop next(); …}MultithreadEventExecutorGroup 中对 next() 的实现:@Overridepublic EventExecutor next() { // 调用 DefaultEventExecutorChooserFactory 中的next() return chooser.next();}DefaultEventExecutorChooserFactory 对于如何从数组中选择任务执行器,也做了巧妙的优化。public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory { public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory(); private DefaultEventExecutorChooserFactory() { } @SuppressWarnings(“unchecked”) @Override public EventExecutorChooser newChooser(EventExecutor[] executors) { if (isPowerOfTwo(executors.length)) { return new PowerOfTwoEventExecutorChooser(executors); } else { return new GenericEventExecutorChooser(executors); } } // 判断线程任务执行的个数是否为 2 的幂次方。e.g: 2、4、8、16 private static boolean isPowerOfTwo(int val) { return (val & -val) == val; } // 幂次方选择器 private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser { private final AtomicInteger idx = new AtomicInteger(); private final EventExecutor[] executors; PowerOfTwoEventExecutorChooser(EventExecutor[] executors) { this.executors = executors; } @Override public EventExecutor next() { // 通过二级制进行 & 运算,效率更高 return executors[idx.getAndIncrement() & executors.length - 1]; } } // 普通选择器 private static final class GenericEventExecutorChooser implements EventExecutorChooser { private final AtomicInteger idx = new AtomicInteger(); private final EventExecutor[] executors; GenericEventExecutorChooser(EventExecutor[] executors) { this.executors = executors; } @Override public EventExecutor next() { // 按照最普通的取模的方式从index=0开始向后开始选择 return executors[Math.abs(idx.getAndIncrement() % executors.length)]; } }}小结通过本节内容,我们了解到了EventLoop与EventLoopGroup的基本原理,EventLoopGroup与EventLoop的创建过程:创建线程任务执行器 ThreadPerTaskExecutor创建EventLoop创建任务选择器 EventExecutorChooser参考资料Java读源码之Netty深入剖析《Netty in action》 ...

November 1, 2018 · 8 min · jiezi

利用责任链模式设计一个拦截器

前言近期在做 Cicada 的拦截器功能,正好用到了责任链模式。这个设计模式在日常使用中频率还是挺高的,借此机会来分析分析。责任链模式先来看看什么是责任链模式。引用一段维基百科对其的解释:责任链模式在面向对象程式设计里是一种软件设计模式,它包含了一些命令对象和一系列的处理对象。每一个处理对象决定它能处理哪些命令对象,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。该模式还描述了往该处理链的末尾添加新的处理对象的方法。光看这段描述可能大家会觉得懵,简单来说就是该设计模式用于对某个对象或者请求进行一系列的处理,这些处理逻辑正好组成一个链条。下面来简单演示使用与不使用责任链模式有什么区别和优势。责任链模式的应用传统实现假设这样的场景:传入了一段内容,需要对这段文本进行加工;比如过滤敏感词、错别字修改、最后署上版权等操作。常见的写法如下:public class Main { public static void main(String[] args) { String msg = “内容内容内容” ; String result = Process.sensitiveWord() .typo() .copyright(); }}这样看似没啥问题也能解决需求,但如果我还需要为为内容加上一个统一的标题呢?在现有的方式下就不得不新增处理方法,并且是在这个客户端(Process)的基础上进行新增。显然这样的扩展性不好。责任链模式实现这时候就到了责任链模式发挥作用了。该需求非常的符合对某一个对象、请求进行一系列处理的特征。于是我们将代码修改:这时 Process 就是一个接口了,用于定义真正的处理函数。public interface Process { /** * 执行处理 * @param msg / void doProcess(String msg) ;}同时之前对内容的各种处理只需要实现该接口即可:public class SensitiveWordProcess implements Process { @Override public void doProcess(String msg) { System.out.println(msg + “敏感词处理”); }}public class CopyrightProcess implements Process { @Override public void doProcess(String msg) { System.out.println(msg + “版权处理”); }}public class CopyrightProcess implements Process { @Override public void doProcess(String msg) { System.out.println(msg + “版权处理”); }}然后只需要给客户端提供一个执行入口以及添加责任链的入口即可:public class MsgProcessChain { private List<Process> chains = new ArrayList<>() ; /* * 添加责任链 * @param process * @return / public MsgProcessChain addChain(Process process){ chains.add(process) ; return this ; } /* * 执行处理 * @param msg */ public void process(String msg){ for (Process chain : chains) { chain.doProcess(msg); } }}这样使用起来就非常简单:public class Main { public static void main(String[] args) { String msg = “内容内容内容==” ; MsgProcessChain chain = new MsgProcessChain() .addChain(new SensitiveWordProcess()) .addChain(new TypoProcess()) .addChain(new CopyrightProcess()) ; chain.process(msg) ; }}当我需要再增加一个处理逻辑时只需要添加一个处理单元即可(addChain(Process process)),并对客户端 chain.process(msg) 是无感知的,不需要做任何的改动。可能大家没有直接写过责任链模式的相关代码,但不经意间使用到的却不少。比如 Netty 中的 pipeline 就是一个典型的责任链模式,它可以让一个请求在整个管道中进行流转。通过官方图就可以非常清楚的看出是一个责任链模式:用责任链模式设计一个拦截器对于拦截器来说使用责任链模式再好不过了。下面来看看在 Cicada 中的实现:首先是定义了和上文 Process 接口类似的 CicadaInterceptor 抽象类:public abstract class CicadaInterceptor { public boolean before(CicadaContext context,Param param) throws Exception{ return true; } public void after(CicadaContext context,Param param) throws Exception{}}同时定义了一个 InterceptProcess 的客户端:其中的 loadInterceptors() 会将所有的拦截器加入到责任链中。再提供了两个函数分别对应了拦截前和拦截后的入口:实际应用现在来看看具体是怎么使用的吧。在请求的 handle 中首先进行加载(loadInterceptors(AppConfig appConfig)),也就是初始化责任链。接下来则是客户端的入口;调用拦截前后的入口方法即可。由于是拦截器,那么在 before 函数中是可以对请求进行拦截的。只要返回 false 就不会继续向后处理。所以这里做了一个返回值的判断。同时对于使用者来说只需要创建拦截器类继承 CicadaInterceptor 类即可。这里做了一个演示,分别有两个拦截器:记录一个业务 handle 的执行时间。在 after 里打印了请求参数。同时可在第一个拦截器中返回 false 让请求被拦截。先来做前两个试验:这样当我请求其中一个接口时会将刚才的日志打印出来:接下来我让打印执行时间的拦截器中拦截请求,同时输入向前端输入一段文本:请求接口可以看到如下内容:同时后面的请求参数也没有打印出来,说明请求确实被拦截下来。同时我也可以调整拦截顺序,只需要在 @Interceptor(order = 1) 注解中定义这个 order 属性即可(默认值是 0,越小越先执行)。之前是打印请求参数的拦截器先执行,这次我手动将它的 order 调整为 2,而打印时间的 order 为 1 。再次请求接口观察后台日志:发现打印执行时间的拦截器先执行。那这个执行执行顺序如何实现自定义配置的呢?其实也比较简单,有以下几步:在加载拦截器时将注解里的 order 保存起来。设置拦截器到责任链中时通过反射将 order 的值保存到各个拦截器中。最终通过排序重新排列这个责任链的顺序。贴一些核心代码。扫描拦截器时保存 order 值:保存 order 值到拦截器中:重新对责任链排序:总结整个责任链模式已经讲完,希望对这个设计模式还不了解的朋友带来些帮助。上文中的源码如下:https://github.com/TogetherOS/cicada:一个高性能、轻量 HTTP 框架https://git.io/fxKid欢迎关注公众号一起交流: ...

October 22, 2018 · 2 min · jiezi

Netty堆外内存泄露排查与总结

导读Netty 是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了 TCP 和 UDP 套接字服务器等网络编程。Netty 底层基于 JDK 的 NIO,我们为什么不直接基于 JDK 的 NIO 或者其他NIO框架:使用 JDK 自带的 NIO 需要了解太多的概念,编程复杂。Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动。Netty自带的拆包解包,异常检测等机制让我们从 NIO 的繁重细节中脱离出来,只需关心业务逻辑即可。Netty解决了JDK 的很多包括空轮训在内的 Bug。Netty底层对线程,Selector 做了很多细小的优化,精心设计的 Reactor 线程做到非常高效的并发处理。自带各种协议栈,让我们处理任何一种通用协议都几乎不用亲自动手。Netty社区活跃,遇到问题随时邮件列表或者 issue。Netty已经历各大RPC框架(Dubbo),消息中间件(RocketMQ),大数据通信(Hadoop)框架的广泛的线上验证,健壮性无比强大。背景最近在做一个基于 Websocket 的长连中间件,服务端使用实现了 Socket.IO 协议(基于WebSocket协议,提供长轮询降级能力) 的 netty-socketio 框架,该框架为 Netty 实现,鉴于本人对 Netty 比较熟,并且对比同样实现了 Socket.IO 协议的其他框架,Netty 的口碑都要更好一些,因此选择这个框架作为底层核心。诚然,任何开源框架都避免不了 Bug 的存在,我们在使用这个开源框架时,就遇到一个堆外内存泄露的 Bug。美团的价值观一直都是“追求卓越”,所以我们就想挑战一下,找到那只臭虫(Bug),而本文就是遇到的问题以及排查的过程。当然,想看结论的同学可以直接跳到最后,阅读总结即可。问题某天早上,我们突然收到告警,Nginx 服务端出现大量5xx。我们使用 Nginx 作为服务端 WebSocket 的七层负载,5xx的爆发通常表明服务端不可用。由于目前 Nginx 告警没有细分具体哪台机器不可用,接下来,我们就到 CAT(美团点评统一监控平台,目前已经开源)去检查一下整个集群的各项指标,就发现如下两个异常:某台机器在同一时间点爆发 GC(垃圾回收),而且在同一时间,JVM 线程阻塞。接下来,我们就就开始了漫长的堆外内存泄露“排查之旅”。排查过程阶段1: 怀疑是log4j2因为线程被大量阻塞,我们首先想到的是定位哪些线程被阻塞,最后查出来是 Log4j2 狂打日志导致 Netty 的 NIO 线程阻塞(由于没有及时保留现场,所以截图缺失)。NIO 线程阻塞之后,因我们的服务器无法处理客户端的请求,所以对Nginx来说就是5xx。接下来,我们查看了 Log4j2 的配置文件。我们发现打印到控制台的这个 appender 忘记注释掉了,所以初步猜测:因为这个项目打印的日志过多,而 Log4j2 打印到控制台是同步阻塞打印的,所以就导致了这个问题。那么接下来,我们把线上所有机器的这行注释掉,本以为会“大功告成”,但没想到仅仅过了几天,5xx告警又来“敲门”。看来,这个问题并没我们最初想象的那么简单。阶段2:可疑日志浮现接下来,我们只能硬着头皮去查日志,特别是故障发生点前后的日志,于是又发现了一处可疑的地方:可以看到:在极短的时间内,狂打 failed to allocate 64(bytes) of direct memory(…)日志(瞬间十几个日志文件,每个日志文件几百M),日志里抛出一个 Netty 自己封装的 OutOfDirectMemoryError。说白了,就是堆外内存不够用,Netty 一直在“喊冤”。堆外内存泄露,听到这个名词就感到很沮丧。因为这个问题的排查就像 C 语言内存泄露一样难以排查,首先能想到的就是,在 OOM 爆发之前,查看有无异常。然后查遍了 CAT 上与机器相关的所有指标,查遍了 OOM 日志之前的所有日志,均未发现任何异常!这个时候心里已经“万马奔腾”了……阶段3:定位OOM源没办法,只能看着这堆讨厌的 OOM 日志发着呆,希望答案能够“蹦到”眼前,但是那只是妄想。一筹莫展之际,突然一道光在眼前一闪而过,在 OOM 下方的几行日志变得耀眼起来(为啥之前就没想认真查看日志?估计是被堆外内存泄露这几个词吓怕了吧 ==!),这几行字是 ….PlatformDepedeng.incrementMemory()…。原来,堆外内存是否够用,是 Netty 这边自己统计的,那么是不是可以找到统计代码,找到统计代码之后我们就可以看到 Netty 里面的对外内存统计逻辑了?于是,接下来翻翻代码,找到这段逻辑,就在 PlatformDepedent 这个类里面。这个地方,是一个对已使用堆外内存计数的操作,计数器为 DIRECT_MEMORY_COUNTER,如果发现已使用内存大于堆外内存的上限(用户自行指定),就抛出一个自定义 OOM Error,异常里面的文本内容正是我们在日志里面看到的。接下来,就验证一下这个方法是否是在堆外内存分配的时候被调用。果然,在 Netty 每次分配堆外内存之前,都会计数。想到这,思路就开始慢慢清晰,而心情也开始从“秋风瑟瑟”变成“春光明媚”。阶段4:反射进行堆外内存监控CAT 上关于堆外内存的监控没有任何异常(应该是没有统计准确,一直维持在 1M),而这边我们又确认堆外内存已快超过上限,并且已经知道 Netty 底层是使用的哪个字段来统计。那么接下来要做的第一件事情,就是反射拿到这个字段,然后我们自己统计 Netty 使用堆外内存的情况。堆外内存统计字段是 DIRECT_MEMORY_COUNTER,我们可以通过反射拿到这个字段,然后定期 Check 这个值,就可以监控 Netty 堆外内存的增长情况。于是我们通过反射拿到这个字段,然后每隔一秒打印,为什么要这样做?因为,通过我们前面的分析,在爆发大量 OOM 现象之前,没有任何可疑的现象。那么只有两种情况,一种是突然某个瞬间分配了大量的堆外内存导致OOM;一种是堆外内存缓慢增长,到达某个点之后,最后一根稻草将机器压垮。在这段代码加上去之后,我们打包上线。阶段5:到底是缓慢增长还是瞬间飙升?代码上线之后,初始内存为 16384k(16M),这是因为线上我们使用了池化堆外内存,默认一个 chunk 为16M,这里不必过于纠结。但是没过一会,内存就开始缓慢飙升,并且没有释放的迹象,二十几分钟之后,内存使用情况如下:走到这里,我们猜测可能是前面提到的第二种情况,也就是内存缓慢增长造成的 OOM,由于内存实在增长太慢,于是调整机器负载权重为其他机器的两倍,但是仍然是以数K级别在持续增长。那天刚好是周五,索性就过一个周末再开看。周末之后,我们到公司第一时间就连上了跳板机,登录线上机器,开始 tail -f 继续查看日志。在输完命令之后,怀着期待的心情重重的敲下了回车键:果然不出所料,内存一直在缓慢增长,一个周末的时间,堆外内存已经飙到快一个 G 了。这个时候,我竟然想到了一句成语:“只要功夫深,铁杵磨成针”。虽然堆外内存以几个K的速度在缓慢增长,但是只要一直持续下去,总有把内存打爆的时候(线上堆外内存上限设置的是2G)。此时,我们开始自问自答环节:内存为啥会缓慢增长,伴随着什么而增长?因为我们的应用是面向用户端的WebSocket,那么,会不会是每一次有用户进来,交互完之后离开,内存都会增长一些,然后不释放呢?带着这个疑问,我们开始了线下模拟过程。阶段6:线下模拟本地起好服务,把监控堆外内存的单位改为以B为单位(因为本地流量较小,打算一次一个客户端连接),另外,本地也使用非池化内存(内存数字较小,容易看出问题),在服务端启动之后,控制台打印信息如下在没有客户端接入的时候,堆外内存一直是0,在意料之中。接下来,怀着着无比激动的心情,打开浏览器,然后输入网址,开始我们的模拟之旅。我们的模拟流程是:新建一个客户端链接->断开链接->再新建一个客户端链接->再断开链接。如上图所示,一次 Connect 和 Disconnect 为一次连接的建立与关闭,上图绿色框框的日志分别是两次连接的生命周期。我们可以看到,内存每次都是在连接被关闭的的时候暴涨 256B,然后也不释放。走到这里,问题进一步缩小,肯定是连接被关闭的时候,触发了框架的一个Bug,而且这个Bug在触发之前分配了 256B 的内存,随着Bug被触发,内存也没有释放。问题缩小之后,接下来开始“撸源码”,捉虫!阶段7:线下排查接下来,我们将本地服务重启,开始完整的线下排查过程。同时将目光定位到 netty-socketio 这个框架的 Disconnect 事件(客户端WebSocket连接关闭时会调用到这里),基本上可以确定,在 Disconnect 事件前后申请的内存并没有释放。在使用 idea debug 时,要选择只挂起当前线程,这样我们在单步跟踪的时候,控制台仍然可以看到堆外内存统计线程在打印日志。在客户端连接上之后然后关闭,断点进入到 onDisconnect 回调,我们特意在此多停留了一会,发现控制台内存并没有飙升(7B这个内存暂时没有去分析,只需要知道,客户端连接断开之后,我们断点hold住,内存还未开始涨)。接下来,神奇的一幕出现了,我们将断点放开,让程序跑完:Debug 松掉之后,内存立马飙升了!!此时,我们已经知道,这只“臭虫”飞不了多远了。在 Debug 时,挂起的是当前线程,那么肯定是当前线程某个地方申请了堆外内存,然后没有释放,继续“快马加鞭“,深入源码。其实,每一次单步调试,我们都会观察控制台的内存飙升的情况。很快,我们来到了这个地方:在这一行没执行之前,控制台的内存依然是 263B。然后,当执行完该行之后,立刻从 263B涨到519B(涨了256B)。于是,Bug 范围进一步缩小。我们将本次程序跑完,释然后客户端再来一次连接,断点打在 client.send() 这行, 然后关闭客户端连接,之后直接进入到这个方法,随后的过程有点长,因为与 Netty 的时间传播机制有关,这里就省略了。最后,我们跟踪到了如下代码,handleWebsocket:在这个地方,我们看到一处非常可疑的地方,在上图的断点上一行,调用 encoder 分配了一段内存,调用完之后,我们的控制台立马就彪了 256B。所以,我们怀疑肯定是这里申请的内存没有释放,它这里接下来调用 encoder.encodePacket() 方法,猜想是把数据包的内容以二进制的方式写到这段 256B的内存。接下来,我们追踪到这段 encode 代码,单步执行之后,就定位到这行代码:这段代码是把 packet 里面一个字段的值转换为一个 char。然而,当我们使用 idea 预执行的时候,却抛出类一个愤怒的 NPE!!也就是说,框架申请到一段内存之后,在 encoder 的时候,自己 GG 了,还给自己挖了个NPE的深坑,最后导致内存无法释放(最外层有堆外内存释放逻辑,现在无法执行到了)。而且越攒越多,直到被“最后一根稻草”压垮,堆外内存就这样爆了。这里的源码,有兴趣的读者可以自己去分析一下,限于篇幅原因,这里就不再展开叙述了。阶段8:Bug解决既然 Bug 已经找到,接下来就要解决问题了。这里只需要解决这个NPE异常,就可以 Fix 掉。我们的目标就是,让这个 subType 字段不为空。于是我们先通过 idea 的线程调用栈,定位到这个 packet 是在哪个地方定义的:我们找到 idea 的 debugger 面板,眼睛盯着 packet 这个对象不放,然后上线移动光标,便光速定位到。原来,定义 packet 对象这个地方在我们前面的代码其实已经出现过,我们查看了一下 subType 这个字段,果然是 null。接下来,解决 Bug 就很容易了。我们给这个字段赋值即可,由于这里是连接关闭事件,所以我们给他指定了一个名为 DISCONNECT 的字段(可以改天深入去研究 Socket.IO 的协议),反正这个 Bug 是在连接关闭的时候触发的,就粗暴一点了 !解决这个 Bug 的过程是:将这个框架的源码下载到本地,然后加上这一行,最后重新 Build一下,pom 里改了一下名字,推送到我们公司的仓库。这样,项目就可以直接进行使用了。改完 Bug 之后,习惯性地去 GitHub上找到引发这段 Bug 的 Commit:好奇的是,为啥这位 dzn commiter 会写出这么一段如此明显的 Bug,而且时间就在今年3月30号,项目启动的前夕!阶段9:线下验证一切准备就绪之后,我们就来进行本地验证,在服务起来之后,我们疯狂地建立连接,疯狂地断开连接,并观察堆外内存的情况:Bingo!不管我们如何断开连接,堆外内存不涨了。至此,Bug 基本 Fix,当然最后一步,我们把代码推到线上验证。阶段10:线上验证这次线上验证,我们避免了比较土的打日志方法,我们把堆外内存的这个指标“喷射”到 CAT 上,然后再来观察一段时间的堆外内存的情况:过完一段时间,堆外内存已经稳定不涨了。此刻,我们的“捉虫之旅”到此结束。最后,我们还为大家做一个小小的总结,希望对您有所帮助。总结遇到堆外内存泄露不要怕,仔细耐心分析,总能找到思路,要多看日志,多分析。如果使用了 Netty 堆外内存,那么可以自行监控堆外内存的使用情况,不需要借助第三方工具,我们是使用的“反射”拿到的堆外内存的情况。逐渐缩小范围,直到 Bug 被找到。当我们确认某个线程的执行带来 Bug 时,可单步执行,可二分执行,定位到某行代码之后,跟到这段代码,然后继续单步执行或者二分的方式来定位最终出 Bug 的代码。这个方法屡试不爽,最后总能找到想要的 Bug。熟练掌握 idea 的调试,让我们的“捉虫”速度快如闪电(“闪电侠”就是这么来的)。这里,最常见的调试方式是预执行表达式,以及通过线程调用栈,死盯某个对象,就能够掌握这个对象的定义、赋值之类。最后,祝愿大家都能找到自己的“心仪已久” Bug!作者简介闪电侠,2014年加入美团点评,主要负责美团点评移动端统一长连工作,欢迎同行进行技术交流。招聘目前我们团队负责美团点评长连基础设施的建设,支持美团酒旅、外卖、到店、打车、金融等几乎公司所有业务的快速发展。加入我们,你可以亲身体验到千万级在线连接、日吞吐百亿请求的场景,你会直面互联网高并发、高可用的挑战,有机会接触到 Netty 在长连领域的各个场景。我们诚邀有激情、有想法、有经验、有能力的同学,和我们一起并肩奋斗!欢迎感兴趣的同学投递简历至 chao.yu#dianping.com 咨询。参考文献Netty 是什么Netty 源码分析之服务端启动全解析 ...

October 19, 2018 · 2 min · jiezi

设计一个百万级的消息推送系统

前言首先迟到的祝大家中秋快乐。最近一周多没有更新了。其实我一直想憋一个大招,分享一些大家感兴趣的干货。鉴于最近我个人的工作内容,于是利用这三天小长假憋了一个出来(其实是玩了两天????)。先简单说下本次的主题,由于我最近做的是物联网相关的开发工作,其中就不免会遇到和设备的交互。最主要的工作就是要有一个系统来支持设备的接入、向设备推送消息;同时还得满足大量设备接入的需求。所以本次分享的内容不但可以满足物联网领域同时还支持以下场景:基于 WEB 的聊天系统(点对点、群聊)。WEB 应用中需求服务端推送的场景。基于 SDK 的消息推送平台。技术选型要满足大量的连接数、同时支持双全工通信,并且性能也得有保障。在 Java 技术栈中进行选型首先自然是排除掉了传统 IO。那就只有选 NIO 了,在这个层面其实选择也不多,考虑到社区、资料维护等方面最终选择了 Netty。最终的架构图如下:现在看着蒙没关系,下文一一介绍。协议解析既然是一个消息系统,那自然得和客户端定义好双方的协议格式。常见和简单的是 HTTP 协议,但我们的需求中有一项需要是双全工的交互方式,同时 HTTP 更多的是服务于浏览器。我们需要的是一个更加精简的协议,减少许多不必要的数据传输。因此我觉得最好是在满足业务需求的情况下定制自己的私有协议,在我这个场景下其实有标准的物联网协议。如果是其他场景可以借鉴现在流行的 RPC 框架定制私有协议,使得双方通信更加高效。不过根据这段时间的经验来看,不管是哪种方式都得在协议中预留安全相关的位置。协议相关的内容就不过讨论了,更多介绍具体的应用。简单实现首先考虑如何实现功能,再来思考百万连接的情况。注册鉴权在做真正的消息上、下行之前首先要考虑的就是鉴权问题。就像你使用微信一样,第一步怎么也得是登录吧,不能无论是谁都可以直接连接到平台。所以第一步得是注册才行。如上面架构图中的 注册/鉴权 模块。通常来说都需要客户端通过 HTTP 请求传递一个唯一标识,后台鉴权通过之后会响应一个 token,并将这个 token 和客户端的关系维护到 Redis 或者是 DB 中。客户端将这个 token 也保存到本地,今后的每一次请求都得带上这个 token。一旦这个 token 过期,客户端需要再次请求获取 token。鉴权通过之后客户端会直接通过TCP 长连接到图中的 push-server 模块。这个模块就是真正处理消息的上、下行。保存通道关系在连接接入之后,真正处理业务之前需要将当前的客户端和 Channel 的关系维护起来。假设客户端的唯一标识是手机号码,那就需要把手机号码和当前的 Channel 维护到一个 Map 中。这点和之前 SpringBoot 整合长连接心跳机制 类似。同时为了可以通过 Channel 获取到客户端唯一标识(手机号码),还需要在 Channel 中设置对应的属性:public static void putClientId(Channel channel, String clientId) {channel.attr(CLIENT_ID).set(clientId);}获取时手机号码时:public static String getClientId(Channel channel) {return (String)getAttribute(channel, CLIENT_ID);}这样当我们客户端下线的时便可以记录相关日志:String telNo = NettyAttrUtil.getClientId(ctx.channel());NettySocketHolder.remove(telNo);log.info(“客户端下线,TelNo=” + telNo);这里有一点需要注意:存放客户端与 Channel 关系的 Map 最好是预设好大小(避免经常扩容),因为它将是使用最为频繁同时也是占用内存最大的一个对象。消息上行接下来则是真正的业务数据上传,通常来说第一步是需要判断上传消息输入什么业务类型。在聊天场景中,有可能上传的是文本、图片、视频等内容。所以我们得进行区分,来做不同的处理;这就和客户端协商的协议有关了。可以利用消息头中的某个字段进行区分。更简单的就是一个 JSON 消息,拿出一个字段用于区分不同消息。不管是哪种只有可以区分出来即可。消息解析与业务解耦消息可以解析之后便是处理业务,比如可以是写入数据库、调用其他接口等。我们都知道在 Netty 中处理消息一般是在 channelRead() 方法中。在这里可以解析消息,区分类型。但如果我们的业务逻辑也写在里面,那这里的内容将是巨多无比。甚至我们分为好几个开发来处理不同的业务,这样将会出现许多冲突、难以维护等问题。所以非常有必要将消息解析与业务处理完全分离开来。这时面向接口编程就发挥作用了。这里的核心代码和 「造个轮子」——cicada(轻量级 WEB 框架) 是一致的。都是先定义一个接口用于处理业务逻辑,然后在解析消息之后通过反射创建具体的对象执行其中的处理函数即可。这样不同的业务、不同的开发人员只需要实现这个接口同时实现自己的业务逻辑即可。伪代码如下:想要了解 cicada 的具体实现请点击这里:https://github.com/TogetherOS/cicada上行还有一点需要注意;由于是基于长连接,所以客户端需要定期发送心跳包用于维护本次连接。同时服务端也会有相应的检查,N 个时间间隔没有收到消息之后将会主动断开连接节省资源。这点使用一个 IdleStateHandler 就可实现,更多内容可以查看 Netty(一) SpringBoot 整合长连接心跳机制TCP-Heartbeat/#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%BF%83%E8%B7%B3)。消息下行有了上行自然也有下行。比如在聊天的场景中,有两个客户端连上了 push-server,他们直接需要点对点通信。这时的流程是:A 将消息发送给服务器。服务器收到消息之后,得知消息是要发送给 B,需要在内存中找到 B 的 Channel。通过 B 的 Channel 将 A 的消息转发下去。这就是一个下行的流程。甚至管理员需要给所有在线用户发送系统通知也是类似:遍历保存通道关系的 Map,挨个发送消息即可。这也是之前需要存放到 Map 中的主要原因。伪代码如下:具体可以参考:https://github.com/crossoverJie/netty-action/分布式方案单机版的实现了,现在着重讲讲如何实现百万连接。百万连接其实只是一个形容词,更多的是想表达如何来实现一个分布式的方案,可以灵活的水平拓展从而能支持更多的连接。再做这个事前首先得搞清楚我们单机版的能支持多少连接。影响这个的因素就比较多了。服务器自身配置。内存、CPU、网卡、Linux 支持的最大文件打开数等。应用自身配置,因为 Netty 本身需要依赖于堆外内存,但是 JVM 本身也是需要占用一部分内存的,比如存放通道关系的大 Map。这点需要结合自身情况进行调整。结合以上的情况可以测试出单个节点能支持的最大连接数。单机无论怎么优化都是有上限的,这也是分布式主要解决的问题。架构介绍在将具体实现之前首先得讲讲上文贴出的整体架构图。先从左边开始。上文提到的 注册鉴权 模块也是集群部署的,通过前置的 Nginx 进行负载。之前也提过了它主要的目的是来做鉴权并返回一个 token 给客户端。但是 push-server 集群之后它又多了一个作用。那就是得返回一台可供当前客户端使用的 push-server。右侧的 平台 一般指管理平台,它可以查看当前的实时在线数、给指定客户端推送消息等。推送消息则需要经过一个推送路由(push-server)找到真正的推送节点。其余的中间件如:Redis、Zookeeper、Kafka、MySQL 都是为了这些功能所准备的,具体看下面的实现。注册发现首先第一个问题则是 注册发现,push-server 变为多台之后如何给客户端选择一台可用的节点是第一个需要解决的。这块的内容其实已经在 分布式(一) 搞定服务注册与发现 中详细讲过了。所有的 push-server 在启动时候需要将自身的信息注册到 Zookeeper 中。注册鉴权 模块会订阅 Zookeeper 中的节点,从而可以获取最新的服务列表。结构如下:以下是一些伪代码:应用启动注册 Zookeeper。对于注册鉴权模块来说只需要订阅这个 Zookeeper 节点:路由策略既然能获取到所有的服务列表,那如何选择一台刚好合适的 push-server 给客户端使用呢?这个过程重点要考虑以下几点:尽量保证各个节点的连接均匀。增删节点是否要做 Rebalance。首先保证均衡有以下几种算法:轮询。挨个将各个节点分配给客户端。但会出现新增节点分配不均匀的情况。Hash 取模的方式。类似于 HashMap,但也会出现轮询的问题。当然也可以像 HashMap 那样做一次 Rebalance,让所有的客户端重新连接。不过这样会导致所有的连接出现中断重连,代价有点大。由于 Hash 取模方式的问题带来了一致性 Hash算法,但依然会有一部分的客户端需要 Rebalance。权重。可以手动调整各个节点的负载情况,甚至可以做成自动的,基于监控当某些节点负载较高就自动调低权重,负载较低的可以提高权重。还有一个问题是:当我们在重启部分应用进行升级时,在该节点上的客户端怎么处理?由于我们有心跳机制,当心跳不通之后就可以认为该节点出现问题了。那就得重新请求注册鉴权模块获取一个可用的节点。在弱网情况下同样适用。如果这时客户端正在发送消息,则需要将消息保存到本地等待获取到新的节点之后再次发送。有状态连接在这样的场景中不像是 HTTP 那样是无状态的,我们得明确的知道各个客户端和连接的关系。在上文的单机版中我们将这个关系保存到本地的缓存中,但在分布式环境中显然行不通了。比如在平台向客户端推送消息的时候,它得首先知道这个客户端的通道保存在哪台节点上。借助我们以前的经验,这样的问题自然得引入一个第三方中间件用来存放这个关系。也就是架构图中的存放路由关系的 Redis,在客户端接入 push-server 时需要将当前客户端唯一标识和服务节点的 ip+port 存进 Redis。同时在客户端下线时候得在 Redis 中删掉这个连接关系。这样在理想情况下各个节点内存中的 map 关系加起来应该正好等于 Redis 中的数据。伪代码如下:这里存放路由关系的时候会有并发问题,最好是换为一个 lua 脚本。推送路由设想这样一个场景:管理员需要给最近注册的客户端推送一个系统消息会怎么做?结合架构图假设这批客户端有 10W 个,首先我们需要将这批号码通过平台下的 Nginx 下发到一个推送路由中。为了提高效率甚至可以将这批号码再次分散到每个 push-route 中。拿到具体号码之后再根据号码的数量启动多线程的方式去之前的路由 Redis 中获取客户端所对应的 push-server。再通过 HTTP 的方式调用 push-server 进行真正的消息下发(Netty 也很好的支持 HTTP 协议)。推送成功之后需要将结果更新到数据库中,不在线的客户端可以根据业务再次推送等。消息流转也许有些场景对于客户端上行的消息非常看重,需要做持久化,并且消息量非常大。在 push-sever 做业务显然不合适,这时完全可以选择 Kafka 来解耦。将所有上行的数据直接往 Kafka 里丢后就不管了。再由消费程序将数据取出写入数据库中即可。其实这块内容也很值得讨论,可以先看这篇了解下:强如 Disruptor 也发生内存溢出?后续谈到 Kafka 再做详细介绍。分布式问题分布式解决了性能问题但却带来了其他麻烦。应用监控比如如何知道线上几十个 push-server 节点的健康状况?这时就得监控系统发挥作用了,我们需要知道各个节点当前的内存使用情况、GC。以及操作系统本身的内存使用,毕竟 Netty 大量使用了堆外内存。同时需要监控各个节点当前的在线数,以及 Redis 中的在线数。理论上这两个数应该是相等的。这样也可以知道系统的使用情况,可以灵活的维护这些节点数量。日志处理日志记录也变得异常重要了,比如哪天反馈有个客户端一直连不上,你得知道问题出在哪里。最好是给每次请求都加上一个 traceID 记录日志,这样就可以通过这个日志在各个节点中查看到底是卡在了哪里。以及 ELK 这些工具都得用起来才行。总结本次是结合我日常经验得出的,有些坑可能在工作中并没有踩到,所有还会有一些遗漏的地方。就目前来看想做一个稳定的推送系统其实是比较麻烦的,其中涉及到的点非常多,只有真正做过之后才会知道。看完之后觉得有帮助的还请不吝转发分享。欢迎关注公众号一起交流: ...

September 25, 2018 · 1 min · jiezi

「造个轮子」——cicada 源码分析

前言两天前写了文章[《「造个轮子」——cicada(轻量级 WEB 框架)》](https://crossoverjie.top/2018… 向大家介绍了 cicada 之后收到很多反馈,也有许多不错的建议。同时在 GitHub 也收获了 80 几颗 小♥♥(绝对不是刷的。。)也有朋友希望能出一个源码介绍,本文就目前的 v1.0.1 版本来一起分析分析。没有看错,刚发布就修复了一个 bug,想要试用的请升级到 1.0.1 吧。技术选型一般在做一个新玩意之前都会有技术选型的过程,但这点在做 cicada 的时候却异常简单。因为我的需求是想提供一个高性能的 HTTP 服务,纵观整个开源界其实选择不多。加上最近我在做 Netty 相关的开发,所以自然而然就选择了它。同时 Netty 自带了对 HTTP 协议的编解码器,可以非常简单快速的开发一个 HTTP 服务器。我只需要把精力放在参数处理、路由等业务处理上即可。同时 Netty 也是基于 NIO 实现,性能上也有保证。关于 Netty 相关内容可以参考这里。下面来重点分析其中的各个过程。路由规则最核心的自然就是 HTTP 的处理 handle,对应的就是 HttpHandle 类。查看源码其实很容易看出具体的步骤,注释也很明显。这里只分析重点功能。先来考虑下需求。首先作为一个 HTTP 框架,自然是得让使用者能有地方来实现业务代码;就像咱们现在使用 SpringMVC 时写的 controller 一样。其实当时考虑过三种方案:像 SpringMVC 一样定义注解,只要声明了对应注解我就认为这是一个业务类。用过 Struts2 的同学应该有印象,它的业务类 Action 都是配置到一个 XML 中;在里面配置接口对应的业务处理类。同样的思路,只是把 XML 文件换成 properties 配置文件,在里面编写 JSON 格式的对应关系。这时就得分析各个方案的优缺点了。方案二和三其实就是 XML 和 json 的对比了;XML 会让维护者感到结构清晰,同时便于维护和新增。JSON 就不太方便处理了,并且在这样的场景并不用于传输自然也发挥不出优势。最后考虑到现在流行的 SpringBoot 都在去 XML,要是再搞一个依赖于 XML 的东西也跟不上大家的使用习惯。于是就采用类似于 SpringMVC 这样的注解形式。既然采用了注解,那框架怎么知道用户访问某个接口时能对应到业务类呢?所以首先第一步自然是需要将加有注解的类全部扫描一遍,放到一个本地缓存中。这样才能方便后续的路由定位。路由策略其中核心的源码在 routeAction 方法中。首先会全局扫描使用了 @CicadaAction 的注解,然后再根据请求地址找到对应的业务类。全局扫描代码:首先是获取到项目中自定义的所有类,然后判断是否加有 @CicadaAction 注解。是目标类则把他缓存到一个本地 Map 中,方便下次访问时可以不再扫描直接从缓存中获取即可(反射很耗性能)。执行完 routeAction 后会获得真正的业务类类型。Class<?> actionClazz = routeAction(queryStringDecoder, appConfig);传参方式拿到业务类的类类型之后就成功一大半了,只需要反射生成它的对象然后执行方法即可。在执行方法之前又要涉及到一个问题,参数我该怎么传递呢?考虑到灵活性我采用了最简答 Map 方式。因此定义了一个通用的 Param 接口并继承了 Map 接口。public interface Param extends Map<String, Object> { /** * get String * @param param * @return / String getString(String param); /* * get Integer * @param param * @return / Integer getInteger(String param); /* * get Long * @param param * @return / Long getLong(String param); /* * get Double * @param param * @return / Double getDouble(String param); /* * get Float * @param param * @return / Float getFloat(String param); /* * get Boolean * @param param * @return */ Boolean getBoolean(String param) ;}其中封装了几种基本类型的获取方式。同时在 buildParamMap() 方法中,将接口中的参数封装到这个 Map 中。Param paramMap = buildParamMap(queryStringDecoder);业务执行最后只需要执行业务即可;由于在上文已经获取到业务类的类类型,所以这里通过反射即可调用。同时也定义了一个业务类需要实现的一个通用接口 WorkAction,想要实现具体业务只要实现它就行。而这里的方法参数自然就是刚才定义的参数接口 Param。由于所有的业务类都是实现了 WorkAction,所以在反射时都可以定义为 WorkAction 对象。WorkAction action = (WorkAction) actionClazz.newInstance();WorkRes execute = action.execute(paramMap);最后将构建好的参数 map 传入即可。响应返回有了请求那自然也得有响应,观察刚才定义的 WorkAction 接口可以发现其实定义了一个 WorkRes 响应类。所有的响应数据都需要封装到这个对象中。这个没啥好说的,都是一些基本数据。最后在 responseMsg() 方法中将响应数据编码为 JSON 输出即可。拦截器设计拦截器也是一个框架基本的功能,用处非常多。cicada 的实现原理非常简单,就是在 WorkAction 接口执行业务逻辑之前调用一个方法、执行完毕之后调用另一个方法。也是同样的思路需要定义一个接口 CicadaInterceptor,其中有两个方法。看方法名字自然也能看出具体作用。同时在这两个方法中执行具体的调用。这里重点要看看 interceptorBefore 方法。其中也是加入了一个缓存,尽量的减少反射操作。适配器就这样的拦截器接口是够用了,但并不是所有的业务都需要实现两个接口。因此也提供了一个适配器 AbstractCicadaInterceptorAdapter。它作为一个抽象类实现了 CicadaInterceptor 接口,这样后续的拦截业务也可继承该接口选择性的实现方法即可。类似于这样:总结v1.0.1 版本的 cicada 就介绍完毕了,其中的原理和源码都比较简单。大量使用了反射和一些设计模式、多态等应用,这方面经验较少的朋友可以参考下。同时也有很多不足;比如传参后续会考虑更加优雅的方式、拦截器目前写的比较死,后续会利用动态代理实现自定义拦截。其实 cicada 只是利用周末两天时间做的,bug 肯定少不了;也欢迎大家在 GitHub 上提 issue 参与。最后贴下项目地址:https://github.com/TogetherOS/cicada你的点赞与转发是最大的支持。 ...

September 5, 2018 · 2 min · jiezi