netty入门

Netty是一款用于创建高性能网络应用程序的高级框架。

在第1章中,我们将首先小结Java网络编程的演化过程。在我们回顾了异步通信和事件驱动的处理的基本概念之后,我们将首先看一看Netty的核心组件。在第2章中,你将能够构建自己的第一款基于Netty的应用程序!在第3章中,你将开启对于Netty的细致探究之旅,从它的核心网络协议(第4章)以及数据处理层(第5章和第6章)到它的并发模型(第7章)。我们将把所有的这些细节组合在一起,对第一部分进行总结。你将看到:如何在运行时配置基于Netty的应用程序的各个组件,以使它们协同工作(第8章),Netty是如何帮助你测试你的应用程序的(第9章)。

第1章 Netty——异步和事件驱动

一个既是异步的又是事件驱动的系统会表现出一种特殊的、对我们来说极具价值的行为:它可以以任意的顺序响应在任意的时间点产生的事件。

异步和可伸缩性之间的联系又是什么呢?

  • 非阻塞网络调用使得我们可以不必等待一个操作的完成。完全异步的I/O正是基于这个特性构建的,并且更进一步:异步方法会立即返回,并且在它完成时,会直接或者在稍后的某个时间点通知用户。
  • 选择器使得我们能够通过较少的线程便可监视许多连接上的事件。

1.1 Netty的核心组件

在本节中我将要讨论Netty的主要构件块:

  • Channel
  • 回调;
  • Future
  • 事件和ChannelHandler

1.1.1 Channel

Channel是Java NIO的一个基本构造。

它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作.可以把Channel看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。

1.1.2 回调

一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用,即指向一个方法用来处理某个事件,一旦触动便执行。这使得后者可以在适当的时候调用前者。回调在广泛的编程场景中都有应用,而且也是在操作完成后通知相关方最常见的方式之一。

Netty在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个 interface-ChannelHandler的实现处理。

public class ConnectHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx)
        throws Exception {     --  当一个新的连接已经被建立时,channelActive(ChannelHandlerContext)将会被调用
        System.out.println(
            "Client " + ctx.channel().remoteAddress() + " connected");
    }
}

1.1.3 Future

Future提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。JDK预置了interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以Netty提供了它自己的实现——ChannelFuture,用于在执行异步操作的时候使用。

ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例。监听器的回调方法operationComplete(),将会在对应的操作完成时被调用。简而言之,由ChannelFutureListener提供的通知机制消除了手动检查对应的操作是否完成的必要。每个Netty的出站I/O操作都将返回一个ChannelFuture;也就是说,它们都不会阻塞。正如我们前面所提到过的一样,Netty完全是异步和事件驱动的。

ChannelFuture future = channel.connect(      --  异步地连接到远程节点
    new InetSocketAddress("192.168.0.1", 25));

这里,connect()方法将会直接返回,而不会阻塞,该调用将会在后台完成。这究竟什么时候会发生则取决于若干的因素,但这个关注点已经从代码中抽象出来了。因为线程不用阻塞以等待对应的操作完成,所以它可以同时做其他的工作,从而更加有效地利用资源。

Channel channel = ...;
// Does not block
ChannelFuture future = channel.connect(   -- 异步地连接到远程节点
    new InetSocketAddress("192.168.0.1", 25));
future.addListener(new ChannelFutureListener() {    --  注册一个ChannelFutureListener,以便在操作完成时获得通知
    @Override
    public void operationComplete(ChannelFuture future) {  --   检查操作
的状态
       if (future.isSuccess()){ 
            ByteBuf buffer = Unpooled.copiedBuffer(   -- 如果操作是成功的,则创建一个ByteBuf以持有数据
               "Hello",Charset.defaultCharset());
           ChannelFuture wf = future.channel()
                .writeAndFlush(buffer);    -- 将数据异步地发送到远程节点。
返回一个ChannelFuture
            ....
        } else {
            Throwable cause = future.cause();  ⇽ -- 如果发生错误,则访问描述原因的Throwable
            cause.printStackTrace();
        }
    }
});

首先,要连接到远程节点上。然后,要注册一个新的ChannelFutureListener到对connect()方法的调用所返回的ChannelFuture上。当该监听器被通知连接已经建立的时候,要检查对应的状态❶。如果该操作是成功的,那么将数据写到该Channel。否则,要从ChannelFuture中检索对应的Throwable。事实上,回调和Future是相互补充的机制;它们相互结合,构成了Netty本身的关键构件块之一。

1.1.4 事件和ChannelHandler

可以认为每个Channel-Handler的实例都类似于一种为了响应特定事件而被执行的回调。Netty使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。这些动作可能是:

  • 记录日志;
  • 数据转换;
  • 流控制;
  • 应用程序逻辑。

Netty是一个网络编程框架,所以事件是按照它们与入站或出站数据流的相关性进行分类的。可能由入站数据或者相关的状态更改而触发的事件包括:

  • 连接已被激活或者连接失活;
  • 数据读取;
  • 用户事件;
  • 错误事件。

出站事件是未来将会触发的某个动作的操作结果,这些动作包括:

  • 打开或者关闭到远程节点的连接;
  • 将数据写到或者冲刷到套接字。
每个事件都可以被分发给 ChannelHandler类中的某个用户实现的方法。

1.1.5 汇总

1.Future、回调和ChannelHandler

Netty的异步编程模型是建立在Future和回调的概念之上的, 而将事件派发到ChannelHandler的方法则发生在更深的层次上。拦截操作以及高速地转换入站数据和出站数据,都只需要你提供回调或者利用操作所返回的Future。这使得链接操作变得既简单又高效,并且促进了可重用的通用代码的编写。

2.选择器、事件和EventLoop

Netty通过触发事件将Selector从应用程序中抽象出来,消除了所有本来将需要手动编写的派发代码。在内部,将会为每个Channel分配一个EventLoop,用以处理所有事件,包括:

  • 注册感兴趣的事件;
  • 将事件派发给ChannelHandler
  • 安排进一步的动作。

EventLoop本身只由一个线程驱动,其处理了一个Channel的所有I/O事件,并且在该EventLoop的整个生命周期内都不会改变。这个简单而强大的设计消除了你可能有的在你的ChannelHandler中需要进行同步的任何顾虑,因此,你可以专注于提供正确的逻辑,用来在有感兴趣的数据要处理的时候执行。


2.1 编写Echo服务器

所有的Netty服务器都需要以下两部分。

  • 至少一个ChannelHandler——该组件实现了服务器对从客户端接收的数据的处理,即它的业务逻辑。
  • 引导——这是配置服务器的启动代码。至少,它会将服务器绑定到它要监听连接请求的端口上。
  • 下面我们将描述Echo服务器的业务逻辑以及引导代码。

2.1.1 ChannelHandler和业务逻辑

因为你的Echo服务器会响应传入的消息,所以它需要实现ChannelInboundHandler接口,用来定义响应入站事件的方法。这个简单的应用程序只需要用到少量的这些方法,所以继承Channel-InboundHandlerAdapter类也就足够了,它提供了ChannelInboundHandler的默认实现。

我们感兴趣的方法是:

  • channelRead()——对于每个传入的消息都要调用;
  • channelReadComplete()——通知ChannelInboundHandler最后一次对channel-Read()的调用是当前批量读取中的最后一条消息;
  • exceptionCaught()——在读取操作期间,有异常抛出时会调用。
@Sharable  ⇽--- 标示一个Channel- Handler可以被多个Channel安全地共享
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        System.out.println(
            "Server received: " + in.toString(CharsetUtil.UTF_8));     ⇽---  将消息记录到控制台       

        ctx.write(in);   ⇽--- 将接收到的消息写给发送者,而不冲刷出站消息

    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
            .addListener(ChannelFutureListener.CLOSE);   ⇽--- 将未决消息冲刷到远程节点,并且关闭该Channel
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
        Throwable cause) {
        cause.printStackTrace();     ⇽---  打印异常栈跟踪
        ctx.close();  ⇽--- 关闭该Channel
    }
}
目前,请记住下面这些关键点:
  • 针对不同类型的事件来调用ChannelHandler
  • 应用程序通过实现或者扩展ChannelHandler来挂钩到事件的生命周期,并且提供自定义的应用程序逻辑;
  • 在架构上,ChannelHandler有助于保持业务逻辑与网络处理代码的分离。

2.1.2 引导服务器

在讨论过由EchoServerHandler实现的核心业务逻辑之后,我们现在可以探讨引导服务器本身的过程了,具体涉及以下内容:

  • 绑定到服务器将在其上监听并接受传入连接请求的端口;
  • 配置Channel,以将有关的入站消息通知给EchoServerHandler实例。

注意:NIO传输大多数时候指的就是TCP传输。

public class EchoServer {
    private final int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println(
                "Usage: " + EchoServer.class.getSimpleName() +
                " ");
        }
        int port = Integer.parseInt(args[0]);   ⇽--- 设置端口值(如果端口参数的格式不正确,则抛出一个NumberFormatException
        new EchoServer(port).start();    ⇽---  调用服务器的start()方法
    }
    public void start() throws Exception {
        final EchoServerHandler serverHandler = new EchoServerHandler();
        EventLoopGroup group = new NioEventLoopGroup();    ⇽---   创建Event-LoopGroup
        try {
             ServerBootstrap b = new ServerBootstrap();    ⇽---    创建Server-Bootstrap
             b.group(group)
                 .channel(NioServerSocketChannel.class)   ⇽---  ❸ 指定所使用的NIO传输Channel
                 .localAddress(new InetSocketAddress(port))   ⇽---  ❹ 使用指定的端口设置套接字地址
                .childHandler(new ChannelInitializer(){    ⇽---   ❺添加一个EchoServer-
Handler到子ChannelChannelPipeline
                 @Override
                public void initChannel(SocketChannel ch)
                    throws Exception {
                         ch.pipeline().addLast(serverHandler);[4]   ⇽---  EchoServerHandler被标注为@Shareable,所以我们可以总是使用同样的实例
                    }
                 });
            ChannelFuture f = b.bind().sync();    ⇽---    异步地绑定服务器;调用sync()方法阻塞等待直到绑定完成
            f.channel().closeFuture().sync();  ⇽---  ❼ 获取ChannelCloseFuture,并且阻塞当前线程直到它完成
        } finally {
            group.shutdownGracefully().sync();    ⇽---    关闭EventLoopGroup,释放所有的资源
        }
    }
}

在➋处,你创建了一个ServerBootstrap实例。因为你正在使用的是NIO传输,所以你指定了NioEventLoopGroup➊来接受和处理新的连接,并且将Channel的类型指定为NioServer-SocketChannel➌。在此之后,你将本地地址设置为一个具有选定端口的InetSocket-Address➍。服务器将绑定到这个地址以监听新的连接请求。

在➎处,你使用了一个特殊的类——ChannelInitializer。这是关键。当一个新的连接被接受时,一个新的子Channel将会被创建,而ChannelInitializer将会把一个你的EchoServerHandler的实例添加到该ChannelChannelPipeline中。正如我们之前所解释的,这个ChannelHandler将会收到有关入站消息的通知。

虽然NIO是可伸缩的,但是其适当的尤其是关于多线程处理的配置并不简单。Netty的设计封装了大部分的复杂性,而且我们将在第3章中对相关的抽象(EventLoopGroupSocket-ChannelChannelInitializer)进行详细的讨论。

接下来你绑定了服务器➏,并等待绑定完成。(对sync()方法的调用将导致当前Thread阻塞,一直到绑定操作完成为止)。在➐处,该应用程序将会阻塞等待直到服务器的Channel关闭(因为你在ChannelClose Future上调用了sync()方法)。然后,你将可以关闭EventLoopGroup,并释放所有的资源,包括所有被创建的线程➑。

与此同时,让我们回顾一下你刚完成的服务器实现中的重要步骤。下面这些是服务器的主要代码组件:

  • EchoServerHandler实现了业务逻辑;
  • main()方法引导了服务器;

引导过程中所需要的步骤如下:

  • 创建一个ServerBootstrap的实例以引导和绑定服务器;
  • 创建并分配一个NioEventLoopGroup实例以进行事件的处理,如接受新连接以及读/写数据;
  • 指定服务器绑定的本地的InetSocketAddress
  • 使用一个EchoServerHandler的实例初始化每一个新的Channel
  • 调用ServerBootstrap.bind()方法以绑定服务器。
在这个时候,服务器已经初始化,并且已经就绪能被使用了。

2.2 编写Echo客户端

Echo客户端将会:

(1)连接到服务器;

(2)发送一个或者多个消息;

(3)对于每个消息,等待并接收从服务器发回的相同的消息;

(4)关闭连接。

编写客户端所涉及的两个主要代码部分也是业务逻辑和引导,和你在服务器中看到的一样。

@Sharable     ⇽---  标记该类的实例可以被多个Channel共享
public class EchoClientHandler extends
    SimpleChannelInboundHandler<ByteBuf> {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!",     ⇽---  当被通知Channel是活跃的时候,发送一条消息
        CharsetUtil.UTF_8));
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
        System.out.println(    ⇽---  记录已接收消息的转储
            "Client received: " + in.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,     ⇽---  在发生异常时,记录错误并关闭Channel 
        Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

首先,你重写了channelActive()方法,其将在一个连接建立时被调用。这确保了数据将会被尽可能快地写入服务器,其在这个场景下是一个编码了字符串"Netty rocks!"的字节缓冲区。

接下来,你重写了channelRead0()方法。每当接收数据时,都会调用这个方法。需要注意的是,由服务器发送的消息可能会被分块接收。也就是说,如果服务器发送了5字节,那么不能保证这5字节会被一次性接收。即使是对于这么少量的数据,channelRead0()方法也可能会被调用两次,第一次使用一个持有3字节的ByteBuf(Netty的字节容器),第二次使用一个持有2字节的ByteBuf。作为一个面向流的协议,TCP保证了字节数组将会按照服务器发送它们的顺序被接收。

SimpleChannelInboundHandler与ChannelInboundHandler的区别?

你可能会想:为什么我们在客户端使用的是SimpleChannelInboundHandler,而不是在Echo- ServerHandler中所使用的ChannelInboundHandlerAdapter呢?这和两个因素的相互作用有关:业务逻辑如何处理消息以及Netty如何管理资源。

在客户端,当channelRead0()方法完成时,你已经有了传入消息,并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler负责释放指向保存该消息的ByteBuf的内存引用。

EchoServerHandler中,你仍然需要将传入消息回送给发送者,而write()操作是异步的,直到channelRead()方法返回后可能仍然没有完成。为此,EchoServerHandler扩展了ChannelInboundHandlerAdapter,其在这个时间点上不会释放消息。消息在EchoServerHandlerchannelReadComplete()方法中,当writeAndFlush()方法被调用时被释放

引导客户端

引导客户端类似于引导服务器,不同的是,客户端是使用主机和端口参数来连接远程地址,也就是这里的Echo服务器的地址,而不是绑定到一个一直被监听的端口。

public class EchoClient {
    private final String host;
    private final int port;

    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws Exception {
       EventLoopGroup group = new NioEventLoopGroup();
        try {    ⇽---  创建Bootstrap
            Bootstrap b = new Bootstrap();     ⇽---  指定EventLoopGroup以处理客户端事件;需要适用于NIO的实现
            b.group(group)    
                 .channel(NioSocketChannel.class)     ⇽---  适用于NIO传输的Channel类型
                 .remoteAddress(new InetSocketAddress(host, port))     ⇽---  设置服务器的InetSocketAddr-ess
![](/api/storage/getbykey/screenshow?key=17043add7e9c14a5d3f7)                .handler(new ChannelInitializer<SocketChannel>() {    ⇽---  在创建Channel时,向ChannelPipeline中添加一个Echo-ClientHandler实例
                 @Override
                public void initChannel(SocketChannel ch)
                    throws Exception {
                   ch.pipeline().addLast(
                        new EchoClientHandler());
                    }
                });
            ChannelFuture f = b.connect().sync();     ⇽---  连接到远程节点,阻塞等待直到连接完成
            f.channel().closeFuture().sync();      ⇽---  阻塞,直到Channel关闭
        } finally {
            group.shutdownGracefully().sync();       ⇽---  关闭线程池并且释放所有的资源
        }
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 2) {
            System.err.println(
                "Usage: " + EchoClient.class.getSimpleName() +
                " <host> <port>");
            return;
        }

        String host = args[0];
        int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }
}

让我们回顾一下这一节中所介绍的要点:

  • 为初始化客户端,创建了一个Bootstrap实例;
  • 为进行事件处理分配了一个NioEventLoopGroup实例,其中事件处理包括创建新的连接以及处理入站和出站数据;
  • 为服务器连接创建了一个InetSocketAddress实例;
  • 当连接被建立时,一个EchoClientHandler实例会被安装到(该Channel的)ChannelPipeline中;
  • 在一切都设置完成后,调用Bootstrap.connect()方法连接到远程节点;

其他例子:

  1. public class Server {  
  2.   
  3.     private int port;  
  4.   
  5.     public Server(int port) {  
  6.         this.port = port;  
  7.     }  
  8.   
  9.     public void run() {  
  10.         EventLoopGroup bossGroup = new NioEventLoopGroup(); //用于处理服务器端接收客户端连接  
  11.         EventLoopGroup workerGroup = new NioEventLoopGroup(); //进行网络通信(读写)  
  12.         try {  
  13.             ServerBootstrap bootstrap = new ServerBootstrap(); //辅助工具类,用于服务器通道的一系列配置  
  14.             bootstrap.group(bossGroup, workerGroup) //绑定两个线程组  
  15.                     .channel(NioServerSocketChannel.class//指定NIO的模式  
  16.                     .childHandler(new ChannelInitializer<SocketChannel>() { //配置具体的数据处理方式  
  17.                         @Override  
  18.                         protected void initChannel(SocketChannel socketChannel) throws Exception {  
  19.                             socketChannel.pipeline().addLast(new ServerHandler());  
  20.                         }  
  21.                     })  
  22.                     /** 
  23.                      * 对于ChannelOption.SO_BACKLOG的解释: 
  24.                      * 服务器端TCP内核维护有两个队列,我们称之为A、B队列。客户端向服务器端connect时,会发送带有SYN标志的包(第一次握手),服务器端 
  25.                      * 接收到客户端发送的SYN时,向客户端发送SYN ACK确认(第二次握手),此时TCP内核模块把客户端连接加入到A队列中,然后服务器接收到 
  26.                      * 客户端发送的ACK时(第三次握手),TCP内核模块把客户端连接从A队列移动到B队列,连接完成,应用程序的accept会返回。也就是说accept 
  27.                      * 从B队列中取出完成了三次握手的连接。 
  28.                      * A队列和B队列的长度之和就是backlog。当A、B队列的长度之和大于ChannelOption.SO_BACKLOG时,新的连接将会被TCP内核拒绝。 
  29.                      * 所以,如果backlog过小,可能会出现accept速度跟不上,A、B队列满了,导致新的客户端无法连接。要注意的是,backlog对程序支持的 
  30.                      * 连接数并无影响,backlog影响的只是还没有被accept取出的连接 
  31.                      */  
  32.                     .option(ChannelOption.SO_BACKLOG, 128//设置TCP缓冲区  
  33.                     .option(ChannelOption.SO_SNDBUF, 32 * 1024//设置发送数据缓冲大小  
  34.                     .option(ChannelOption.SO_RCVBUF, 32 * 1024//设置接受数据缓冲大小  
  35.                     .childOption(ChannelOption.SO_KEEPALIVE, true); //保持连接  
  36.             ChannelFuture future = bootstrap.bind(port).sync();  
  37.             future.channel().closeFuture().sync();  
  38.         } catch (Exception e) {  
  39.             e.printStackTrace();  
  40.         } finally {  
  41.             workerGroup.shutdownGracefully();  
  42.             bossGroup.shutdownGracefully();  
  43.         }  
  44.     }  
  45.   
  46.     public static void main(String[] args) {  
  47.         new Server(8379).run();  
  48.     }  

  1. public class ServerHandler  extends ChannelHandlerAdapter {  
  2.   
  3.     @Override  
  4.     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
  5.       
  6.             //do something msg  
  7.             ByteBuf buf = (ByteBuf)msg;  
  8.             byte[] data = new byte[buf.readableBytes()];  
  9.             buf.readBytes(data);  
  10.             String request = new String(data, "utf-8");  
  11.             System.out.println("Server: " + request);  
  12.             //写给客户端  
  13.             String response = "我是反馈的信息";  
  14.             ctx.writeAndFlush(Unpooled.copiedBuffer("888".getBytes()));  
  15.             //.addListener(ChannelFutureListener.CLOSE);  
  16.               
  17.   
  18.     }  
  19.   
  20.     @Override  
  21.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
  22.         cause.printStackTrace();  
  23.         ctx.close();  
  24.     }  
  25.   
  26. }

客户端:

[java] view plain copy
  1. public class Client {  
  2.   
  3.     public static void main(String[] args) throws InterruptedException {  
  4.         EventLoopGroup workerGroup = new NioEventLoopGroup();  
  5.         Bootstrap bootstrap = new Bootstrap();  
  6.         bootstrap.group(workerGroup)  
  7.                 .channel(NioSocketChannel.class)  
  8.                 .handler(new ChannelInitializer<SocketChannel>() {  
  9.                     @Override  
  10.                     protected void initChannel(SocketChannel socketChannel) throws Exception {  
  11.                         socketChannel.pipeline().addLast(new ClientHandler());  
  12.                     }  
  13.                 });  
  14.         ChannelFuture future = bootstrap.connect("127.0.0.1"8379).sync();  
  15.         future.channel().writeAndFlush(Unpooled.copiedBuffer("777".getBytes()));  
  16.         future.channel().closeFuture().sync();  
  17.         workerGroup.shutdownGracefully();  
  18.     }  
  19.   

ClientHandler类:
[java] view plain copy
  1. public class ClientHandler extends ChannelHandlerAdapter {  
  2.   
  3.     @Override  
  4.     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
  5.         try {  
  6.             ByteBuf buf = (ByteBuf) msg;  
  7.             byte[] data = new byte[buf.readableBytes()];  
  8.             buf.readBytes(data);  
  9.             System.out.println("Client:" + new String(data).trim());  
  10.         } finally {  
  11.             ReferenceCountUtil.release(msg);  
  12.         }  
  13.     }  
  14.   
  15.   
  16.     @Override  
  17.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
  18.         cause.printStackTrace();  
  19.         ctx.close();  
  20.     }  
  21.   

TCP粘包、拆包问题

熟悉TCP编程的可能都知道,无论是服务器端还是客户端,当我们读取或者发送数据的时候,都需要考虑TCP底层的粘包/拆包机制。

TCP是一个“流”协议,所谓流就是没有界限的遗传数据。大家可以想象一下,如果河水就好比数据,他们是连成一片的,没有分界线,TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的具体情况进行包的划分,也就是说,在业务上一个完整的包可能会被TCP分成多个包进行发送,也可能把多个小包封装成一个大的数据包发送出去,这就是所谓的粘包/拆包问题。

解决方案:

①消息定长,例如每个报文的大小固定为200个字节,如果不够,空位补空格。

②在包尾部增加特殊字符进行分割,例如加回车等。

③将消息分为消息头和消息体,在消息头中包含表示消息总长度的字段,然后进行业务逻辑的处理。

Netty中解决TCP粘包/拆包的方法:

①分隔符类:DelimiterBasedFrameDecoder(自定义分隔符)

②定长:FixedLengthFrameDecoder


Netty编解码技术

通常我们也习惯将编码(Encode)成为序列化,它将数据序列化为字节数组,用于网络传输、数据持久化或者其他用途。反之,解码(Decode)/反序列化(deserialization)

把从网络、磁盘等读取的字节数组还原成原始对象(通常是原始对象的拷贝),以方便后续的业务逻辑操作。进行远程跨进程服务调用时(例如RPC调用),需要使用特定的编解码技术,对需要进行网络传输的对象做编码或者解码,以便完成远程调用。

主流的编解码框架:

JBossMarshalling

②google的Protobuf

③基于Protobuf的Kyro

④MessagePack框架

在initChannle方法中加入
socketChannel.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());

                            socketChannel.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());

                           socketChannel.pipeline().addLast(new ServerHandler());


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值