netty in action

NIO
程序启动时在第一个断点阻塞等待客户端连接,当有客户端连接接入时,如果没有发生数据读写,将阻塞在第三个断点。
NIO服务端
客户端可以通过telnet localhost 8080 ->ctrl+] ->send ss 进行测试

缺点:
1.数据的读取和写都是阻塞的,当且仅当有数据可读、可用数据已读取完毕、发生I/O异常时才会往下执行
2.一个线程对应一个客户端,即使使用线程池控制线程池大小(假设为500),如果500个客户端都不发送数据,那这500个线程将一直被占用(处于闲置状态)

基于Reactor模型统一调度的长连接和短链接协议栈,无论是性能、可靠性还是可维护性,都可以“秒杀”传统基于BIO开发的应用服务器和各种协议栈。

epoll所支持的FD(文件描述符)上线时操作系统的最大文件句柄数,具体的值可以通过cat /proc/sys/fs/filemax查看。

缓冲区Buffer
在面向流的I/O中,可以将数据直接写入或将数据直接读到Stream对象中。在NIO库中,加入了Buffer对象,所有数据都是用缓冲区处理的;在读取数据时,它是直接读到缓冲区中的:在写入数据时,写入到缓冲区中。缓冲区实质上是一个数组。通常它是一个字节数组(ByteBuffer),也可以使用其他种类的数据。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能用于操作byte数组。

通道Channel
Channel是一个通道,可以通过它读取和写入数据,它就想自来水管一样,网络数据通过Channel读取和写入。通道与流的不同指出在于通道时双向的,流只是在一个方向上移动(一个流必须时InputStream或者OutputStream的子类),而且通道可以用于读、写或者同时用于读写。因为Channel时全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。Channel可以分委两大类:分别时用于网络读写的SelectableChannel和用于文件操作的FileChannel。

多路复用器Selector
多路复用器提供选择已经就绪的任务的能力。Selector会不断地轮询注册在起上的Channel,如果某个Channel上面有新的TCP连接介入、读和写时间,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

NIO编程优点

  • 客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像BIO的客户端那样被同步阻塞。
  • SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需熬同步等待这个链路可用。
  • 线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,他没有连接句柄数限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。

NIO1.0实现

  • 服务端
public class TimeServer {
    
    
  public static void main(String[] args) {
    
    
    MultiplexerTimeServer server = new MultiplexerTimeServer(8080);
    new Thread(server,"NIO-multiplexer-001").start();
  }
}
public class MultiplexerTimeServer implements Runnable {
    
    
  private ServerSocketChannel ssc;

  private Selector selector;

  private volatile boolean stop;

  int port;

  public MultiplexerTimeServer(int port) {
    
    
    try {
    
    
      //创建多路复用器
      selector = Selector.open();
      //打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道
      ssc = ServerSocketChannel.open();
      //绑定监听端口
      //1024: requested maximum length of the queue of incoming connections,即最大连接数
      ssc.socket().bind(new InetSocketAddress(port), 1024);
      //设置连接为非阻塞模式
      ssc.configureBlocking(false);
      //将ServerSocketChannel注册到Reactor线程的多路复用器Selector上,监听ACCEPT事件。
      ssc.register(selector, SelectionKey.OP_ACCEPT);
      System.out.println("The time Server is start in port:" + port);

    } catch (IOException e) {
    
    
      e.printStackTrace();
    }

    this.port = port;
  }

  public void stop() {
    
    
    this.stop = false;
  }

  @Override
  public void run() {
    
    
    //轮询准备就绪的Key
    while (!stop) {
    
    
      SelectionKey key = null;
      try {
    
    
        selector.select(1000);
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> it = selectionKeys.iterator();
        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 (IOException e) {
    
    
        if (selector != null) {
    
    
          try {
    
    
            selector.close();
          } catch (IOException e1) {
    
    
            e1.printStackTrace();
          }
        }
      }
    }
  }

  private void handleInput(SelectionKey key) throws IOException {
    
    
    if (key.isValid()) {
    
    
      //处理新接入的请求
      if(key.isAcceptable()) {
    
    
        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
        //多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路
        SocketChannel sc = ssc.accept();
        //设置客户端链路为非阻塞模式
        sc.configureBlocking(false);
        sc.socket().setReuseAddress(true);
        //将新接入的客户端连接注册到Reactor线程的多路复用器上,监听读操作用来读取客户端发送的网络消息
        sc.register(selector, SelectionKey.OP_READ);
      }
      if(key.isReadable()) {
    
    
        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("Receive message from client:"+ body);
          String res = "hello client";
          doWrite(sc,res);
        }
      }

    }
  }

  private void doWrite(SocketChannel sc, String res) throws IOException {
    
    
    byte[] bytes = res.getBytes();
    ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
    writeBuffer.put(bytes);
    writeBuffer.flip();
    sc.write(writeBuffer);
  }

}


  • 客户端
public class TimeClient {
    
    
  public static void main(String[] args) {
    
    
    new Thread(new TimeClientHandler("127.0.0.1",8080),"NIO-TimeClient-001").start();
  }
}
public class TimeClientHandler implements Runnable {
    
    

  private String host;

  private int port;

  private Selector selector;

  private SocketChannel socketChannel;

  private volatile boolean stop;

  public TimeClientHandler(String host, int port) {
    
    
    this.host = host == null ? "127.0.0.1" : host;
    this.port = port;
    try {
    
    
      selector = Selector.open();
      //打开SocketChannel,绑定客户端本地地址
      socketChannel = SocketChannel.open();
      //设置SocketChannel为非阻塞模式
      socketChannel.configureBlocking(false);

    } catch (IOException e) {
    
    
      e.printStackTrace();
      System.exit(1);
    }
  }

  @Override
  public void run() {
    
    
    try {
    
    
      doConnect();
    } catch (IOException e) {
    
    
      e.printStackTrace();
      System.exit(1);
    }
    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 (Exception e) {
    
    
        e.printStackTrace();
        System.exit(1);
      }
    }

    // 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
    if (selector != null)
      try {
    
    
        selector.close();
      } catch (IOException e) {
    
    
        e.printStackTrace();
      }

  }

  private void handleInput(SelectionKey key) throws IOException {
    
    

    if (key.isValid()) {
    
    
      // 判断是否连接成功
      SocketChannel sc = (SocketChannel) key.channel();
      if (key.isConnectable()) {
    
    
        if (sc.finishConnect()) {
    
    
          sc.register(selector, SelectionKey.OP_READ);
          doWrite(sc);
        } else
          System.exit(1);// 连接失败,进程退出
      }
      if (key.isReadable()) {
    
    
        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("Receive message from server : " + body);
          this.stop = true;
        } else if (readBytes < 0) {
    
    
          // 对端链路关闭
          key.cancel();
          sc.close();
        } else
          ; // 读到0字节,忽略
      }
    }

  }

  //异步连接服务器
  private void doConnect() throws IOException {
    
    
    // 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
    if (socketChannel.connect(new InetSocketAddress(host, port))) {
    
    
      socketChannel.register(selector, SelectionKey.OP_READ);
      doWrite(socketChannel);
    } else
      //异步连接,返回false,说明客户端已经发送sync包,服务端没有返回ack包,物理链路还没有建立
      //向Reactor线程的多路复用注册OP_CONNECT状态位,监听服务端的TCP ACK应答
      socketChannel.register(selector, SelectionKey.OP_CONNECT);
  }

  private void doWrite(SocketChannel sc) throws IOException {
    
    
    byte[] req = "Hello Server".getBytes();
    ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
    writeBuffer.put(req);
    writeBuffer.flip();
    sc.write(writeBuffer);
    if (!writeBuffer.hasRemaining())
      System.out.println("Send order 2 server succeed.");
  }

}


NIO2.0引入新的异步通道的概念,并提供了异步文件通道和异步套接字痛殴感到的实现。异步通道提供两种方式获取操作结果。

  • 通过java.util.concurrent.Future类来表示异步操作的结果;
  • 在执行异步操作的时候 传入一个java.nio.channels。CompletionHandler接口的实现类作为操作完成的回调。
    NIO2.0的异步套接字通道是真正的异步非阻塞I/O,它不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。

NIO2.0实现

  • 服务端
public class TimeServer {
    
    
  public static void main(String[] args) {
    
    
    new Thread(new AsynTimerServerHandler(8080),"Aio-001").start();
  }
}
public class AsynTimerServerHandler implements Runnable {
    
    

  private int port;
  CountDownLatch latch = new CountDownLatch(1);
  AsynchronousServerSocketChannel serverChannel;
  
  
  public AsynTimerServerHandler(int port) {
    
    
    this.port = port;
    try {
    
    
      serverChannel = AsynchronousServerSocketChannel.open();
      serverChannel.bind(new InetSocketAddress(port));
      System.out.println("The server is start at port: "+port);
    } catch (IOException e) {
    
    
      e.printStackTrace();
    }
  }

  @Override
  public void run() {
    
    
    doAccept();
    try {
    
    
      latch.await();
    } catch (InterruptedException e) {
    
    
      e.printStackTrace();
    }
  }

  @SuppressWarnings("unchecked")
  private void doAccept() {
    
    
    serverChannel.accept(this,new AcceptCompletionHandler());
  }

}

public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsynTimerServerHandler> {
    
    

  @SuppressWarnings("unchecked")
  @Override
  public void completed(AsynchronousSocketChannel result, AsynTimerServerHandler attachment) {
    
    
    attachment.serverChannel.accept(attachment,this);
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    result.read(buffer,buffer,new ReadCompletionHandler(result));
  }

  @Override
  public void failed(Throwable t, AsynTimerServerHandler attachment) {
    
    
    t.printStackTrace();
    attachment.latch.countDown();
  }

}

public class ReadCompletionHandler implements CompletionHandler<Integer,ByteBuffer> {
    
    
  
  private AsynchronousSocketChannel channel;
  
  
  public ReadCompletionHandler(AsynchronousSocketChannel channel) {
    
    
    if(this.channel == null) {
    
    
      this.channel = channel;
    }
  }

  @Override
  public void completed(Integer result, ByteBuffer attachment) {
    
    
    attachment.flip();
    byte [] body = new byte[attachment.remaining()];
    attachment.get(body);
    try {
    
    
      String req = new String(body, "UTF-8");
      System.out.println("Receive message from client: "+ req);
      doWrite("Hello Client");
    } catch (UnsupportedEncodingException e) {
    
    
      e.printStackTrace();
    }
  }

  private void doWrite(String string) {
    
    
    byte[] bytes = string.getBytes();
    ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
    writeBuffer.put(bytes);
    writeBuffer.flip();
    channel.write(writeBuffer,writeBuffer,new CompletionHandler<Integer, ByteBuffer>() {
    
    

      @Override
      public void completed(Integer result, ByteBuffer buffer) {
    
    
        if(buffer.hasRemaining()) {
    
    
          channel.write(buffer,buffer,this);
        }
      }

      @Override
      public void failed(Throwable exc, ByteBuffer attachment) {
    
    
        try {
    
    
          channel.close();
        } catch (IOException e) {
    
    
          e.printStackTrace();
        }
      }
    });
  }

  @Override
  public void failed(Throwable exc, ByteBuffer attachment) {
    
    
    try {
    
    
      channel.close();
    } catch (IOException e) {
    
    
      e.printStackTrace();
    }
  }

}

  • 客户端
public class TimeClient {
    
    

  public static void main(String[] args) throws InterruptedException {
    
    
    test(1000);
  }

  public static void test(int nThread) throws InterruptedException {
    
    
    final CountDownLatch start = new CountDownLatch(1);
    final CountDownLatch end = new CountDownLatch(nThread);
    for (int i = 0; i < nThread; i++) {
    
    
      Thread t = new Thread() {
    
    
        public void run() {
    
    
          try {
    
    
            start.await();
          } catch (InterruptedException e) {
    
    
          }
          new AsyncTimeClientHandler("127.0.0.1", 8080).run();
          end.countDown();
        }
      };
      t.start();
    }
    long startTimie = System.nanoTime();
    start.countDown();
    end.await();
    long endTime = System.nanoTime();
    System.out.println((endTime - startTimie)/1000000+"μs");
  }
}

public class AsyncTimeClientHandler implements
    CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
    
    

  private AsynchronousSocketChannel client;

  private String host;

  private int port;

  private CountDownLatch latch;

  public AsyncTimeClientHandler(String host, int port) {
    
    
    this.host = host;
    this.port = port;
    try {
    
    
      client = AsynchronousSocketChannel.open();
    } catch (IOException e) {
    
    
      e.printStackTrace();
    }
  }

  @Override
  public void run() {
    
    

    latch = new CountDownLatch(1);
    client.connect(new InetSocketAddress(host, port), this, this);
    try {
    
    
      latch.await();
    } catch (InterruptedException e1) {
    
    
      e1.printStackTrace();
    }
    try {
    
    
      client.close();
    } catch (IOException e) {
    
    
      e.printStackTrace();
    }
  }

  @Override
  public void completed(Void result, AsyncTimeClientHandler attachment) {
    
    
    byte[] req = "Hello Server".getBytes();
    ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
    writeBuffer.put(req);
    writeBuffer.flip();
    client.write(writeBuffer,
        writeBuffer,
        new CompletionHandler<Integer, ByteBuffer>() {
    
    
          @Override
          public void completed(Integer result, ByteBuffer buffer) {
    
    
            if (buffer.hasRemaining()) {
    
    
              client.write(buffer, buffer, this);
            } else {
    
    
              ByteBuffer readBuffer = ByteBuffer.allocate(1024);
              client.read(
                  readBuffer,
                  readBuffer,
                  new CompletionHandler<Integer, ByteBuffer>() {
    
    
                    @Override
                    public void completed(Integer result,
                        ByteBuffer buffer) {
    
    
                      buffer.flip();
                      byte[] bytes = new byte[buffer
                          .remaining()];
                      buffer.get(bytes);
                      String body;
                      try {
    
    
                        body = new String(bytes,
                            "UTF-8");
                        System.out.println("Receive message from server : "
                            + body);
                        latch.countDown();
                      } catch (UnsupportedEncodingException e) {
    
    
                        e.printStackTrace();
                      }
                    }

                    @Override
                    public void failed(Throwable exc,
                        ByteBuffer attachment) {
    
    
                      try {
    
    
                        client.close();
                        latch.countDown();
                      } catch (IOException e) {
    
    
                        // ingnore on close
                      }
                    }
                  });
            }
          }

          @Override
          public void failed(Throwable exc, ByteBuffer attachment) {
    
    
            try {
    
    
              client.close();
              latch.countDown();
            } catch (IOException e) {
    
    
              // ingnore on close
            }
          }
        });
  }

  @Override
  public void failed(Throwable exc, AsyncTimeClientHandler attachment) {
    
    
    exc.printStackTrace();
    try {
    
    
      client.close();
      latch.countDown();
    } catch (IOException e) {
    
    
      e.printStackTrace();
    }
  }

}

基于Netty开发异步I/O
Netty相比于传统的NIO程序,代码更加简洁,开发难度更低,扩展性也好。

  • 服务端
public class TimeServer {
    
    
  public static void main(String[] args) {
    
    
    new TimeServer().bind(8080);
  }

  private void bind(int port) {
    
    
    //NioEventLoopGroup是个线程组,它包含了一组NIO线程,专门用于网络事件的处理,实际上他们
    //就是Reactor线程组。
    EventLoopGroup boosGroup = new NioEventLoopGroup();//接受客户端的连接
    EventLoopGroup workerGroup = new NioEventLoopGroup();//进行SocketChannel的网络读写
    //用于启动NIO服务端的辅助启动类,降低服务端的开发复杂度
    ServerBootstrap bootstrap = new ServerBootstrap();
    bootstrap.group(boosGroup, workerGroup)
        //设置创建的Channel为NioServerSocketChannel,对应JDK NIO类库中的ServerSocketChannel
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 1024)
        //绑定I/O事件的处理类,作用类似于Reactor模式中的handler,主要用于网络I/O事件,例如记录日志,对消息进行编解码等。
        .childHandler(new ChildChannelHandler());
    try {
    
    
      //绑定监听端口
      ChannelFuture future = bootstrap.bind(port).sync();
      //调用sync等待绑定操作完成
      future.channel().closeFuture().sync();
      
    } catch (InterruptedException e) {
    
    
      e.printStackTrace();
    } finally {
    
    
      boosGroup.shutdownGracefully();
      workerGroup.shutdownGracefully();
    }
  }
}
public class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
    
    

  @Override
  protected void initChannel(SocketChannel channel) throws Exception {
    
    
    channel.pipeline().addLast(new TimeServerHandler());
  }
}
/**
 *TimeServerHandler继承自ChannelHadnlerAdapter,它用于对网络事件进行读写操作
 */
public class TimeServerHandler extends ChannelHandlerAdapter {
    
    

  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
    //将message转换成Netty的ByteBuf对象,ByteBuf类似于JDK中的java.nio.ByteBuffer对象
    //不过提供了更加强大和灵活的功能。通过BYteBuf的readableBytes方法可以获取缓冲区可读
    //的字节数组。
    ByteBuf buf = (ByteBuf) msg;
    byte[] req = new byte[buf.readableBytes()];
    buf.readBytes(req);
    String body = new String(req, "UTF-8");
    System.out.println("Receive message from client: " + body);
    ByteBuf res = Unpooled.copiedBuffer("Hello Netty Client!".getBytes());
    ctx.write(res);
  }

  @Override
  public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    
    
    //将消息发送队列中的消息写入到SocketChannel中发送给对方。从性能的角度考虑
    //为了防止频繁地唤醒Selector进行消息发送,Netty的write方法并不是直接将消息
    //写入SocketChannel,调用write方法只是把待发送的消息放到发送缓存数组中,再
    //通过调用flush方法,将发送缓冲区中的消息全部写到SocketChannel中。
    ctx.flush();
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
    ctx.close();
  }

}
  • 客户端
public class TimeClient {
    
    

  public static void main(String[] args) throws InterruptedException {
    
    
    new TimeClient().connect(8080, "127.0.0.1");
  }

  public void connect(int port, String host) throws InterruptedException {
    
    
    EventLoopGroup group = new NioEventLoopGroup();
    try {
    
    
      Bootstrap b = new Bootstrap();
      //
      b.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(
          new ChannelInitializer<SocketChannel>() {
    
    
            //实现initChannel方法,作用是当创建NioSocketChannel成功之后,在初始化它的时候将它的
            //ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件。
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
    
    
              ch.pipeline().addLast(new TimeClientHandler());
            }
          });
      //调用connect方法发起异步连接,然后调用同步方法等待连接成功
      ChannelFuture f = b.connect(host, port).sync();
      //等待客户端链路关闭
      f.channel().closeFuture().sync();

    } finally {
    
    
      group.shutdownGracefully();
    }
  }
}

public class TimeClientHandler extends ChannelHandlerAdapter {
    
    

  private final ByteBuf firstMessage;

  public TimeClientHandler() {
    
    
    byte[] req = "Hello Netty Server".getBytes();
    this.firstMessage = Unpooled.buffer(req.length);
    firstMessage.writeBytes(req);
  }

  @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("Receive message from netty server:" + body);
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
    ctx.close();
  }
}

TCP粘包/拆包
TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

TCP粘包/拆包发生的原因

  • 应用程序write写入的字节大小大于套接口发送缓冲区大小
  • 进行MSS大小的TCP分段
  • 以太网帧的payload大于MTU进行IP分片

TCP粘包/拆包解决策略
由于底层TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下:

  • 消息定长,例如每个报文的大小固定长度200字节,如果不够,空格补齐
  • 在包尾增加回车换行符进行分割,例如FTP协议
  • 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度
  • 更复杂的协议

基于LineBasedFrameDecoder和StringDecoder解决粘包/拆包问题
LineBasedFrameDecoder的工作原理是它依次便利Bytebuf中的刻度字节,判断看是否有“\n”或者“\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。
StringDecoder就是将接收到的对象转换成字符串,然后继续调用后面的handler。这两者组合起来就是按行切换的文本解码器。

  • 服务端
public class TimeServer {
    
    
  public static void main(String[] args) {
    
    
    new TimeServer().bind(8080);
    
  }

  private void bind(int port) {
    
    
    EventLoopGroup boosGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    ServerBootstrap bootstrap = new ServerBootstrap();
    bootstrap.group(boosGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 1024)
        .childHandler(new ChildChannelHandler());
    try {
    
    
      ChannelFuture future = bootstrap.bind(port).sync();
      future.channel().closeFuture().sync();
      
    } catch (InterruptedException e) {
    
    
      e.printStackTrace();
    } finally {
    
    
      boosGroup.shutdownGracefully();
      workerGroup.shutdownGracefully();
    }
  }
  private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
    
    

    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
    
    
      channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
      channel.pipeline().addLast(new StringDecoder());
      channel.pipeline().addLast(new TimeServerHandler());
    }
  }

}

public class TimeServerHandler extends ChannelHandlerAdapter {
    
    

  private int counter;

  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
    String body = (String) msg;
    System.out.println("The time server receive order:" + body + ";the counter is:" + ++counter);
    String currentTime =
        "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
    currentTime = currentTime+System.getProperty("line.separator"); 
    ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
    ctx.writeAndFlush(resp);
  }


  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
    ctx.close();
  }

}

  • 客户端
public class TimeClient {
    
    

  public static void main(String[] args) throws InterruptedException {
    
    
    new TimeClient().connect(8080, "127.0.0.1");
  }
  public void connect(int port, String host) throws InterruptedException {
    
    

    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 channel) throws Exception {
    
    
              channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
              channel.pipeline().addLast(new StringDecoder());
              channel.pipeline().addLast(new TimeClientHandler());
            }
          });
      ChannelFuture f = b.connect(host, port).sync();
      f.channel().closeFuture().sync();

    } finally {
    
    
      group.shutdownGracefully();
    }
  }
}

public class TimeClientHandler extends ChannelHandlerAdapter {
    
    

  private byte[] req;

  private int counter;

  public TimeClientHandler() {
    
    
    req = ("Query Time Order"+System.getProperty("line.separator")).getBytes();
  }

  @Override
  public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
    ByteBuf message = null;
    for (int i = 0; i < 100; i++) {
    
    
      message = Unpooled.buffer(req.length);
      message.writeBytes(req);
      ctx.writeAndFlush(message);    
    }
  }

  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
    String body = (String)msg;
    System.out.println("Now is:"+ body + ";the counter is:"+ ++counter);
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
    ctx.close();
  }
}

此外,Netty还提供了DelimiterBasedFrameDecoder用于对使用分隔符结尾的消息进行自动解码,FixedLengthFrameDecoder用于对固定长度的消息进行自动解码。

Netty使用HTTP协议开发应用

HTTP(超文本传输协议)协议是建立在TCP传输协议之上的应用层协议,是目前Web开发的主流协议。
由于Netty的HTTP协议栈是基于Netty的NIO 通信框架开发的,因此Netty的HTTP协议也是异步非阻塞的。

HTTP协议的主要特点:

  • 支持Client/Server模式
  • 简单-客户像服务器请求服务时,只需指定服务URL,携带必要的请求参数或者消息体
  • 灵活–HTTP允许传输任意类型的数据对象,传输的内容类型由HTTP消息头中的Content-Type标记
  • 无状态–HTTP协议时无状态协议,无状态是值协议对于事物处理没有记忆能力。缺少状态以为这如果后续处理需要之前的信息,则它必须重传,这样可能导致每次连接传送的数据增大。

HTTP请求由三部分组成:

  • HTTP请求头
  • HTTP消息头
  • HTTP请求正文

HTTP文件服务器简单实现

public class FileServer {
    
    
  public static void main(String[] args) {
    
    
    new FileServer().run(8080, "/src/main/resources/");
  }
  public void run(final int port,final String url) {
    
    
    EventLoopGroup boosGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
    
    
    
    ServerBootstrap b = new ServerBootstrap();
    b.group(boosGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
    
    

      @Override
      protected void initChannel(SocketChannel ch) throws Exception {
    
    
        ch.pipeline().addLast("http-decoder",new HttpRequestDecoder());
        ch.pipeline().addLast("http-aggregator",new HttpObjectAggregator(65536));
        ch.pipeline().addLast("http-encoder",new HttpResponseEncoder());
        //支持异步发送大的码流(例如大的文件传输),但不占用过多的内存,防止发生Java内存溢出错误
        ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
        //用于文件服务器的业务逻辑处理
        ch.pipeline().addLast("fileServerHandler",new HttpFileServerHandler(url));
      }
    });
      ChannelFuture future = b.bind("192.168.91.1",port).sync();
      System.out.println("HTTP文件目录服务器启动,网址是:"+ "192.168.91.1:"+port+url);
      future.channel().closeFuture().sync();
    } catch (InterruptedException e) {
    
    
      e.printStackTrace();
    }finally {
    
    
      boosGroup.shutdownGracefully();
      workerGroup.shutdownGracefully();
    }
    
  }

}

import static io.netty.handler.codec.http.HttpHeaders.setContentLength;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaders.Names.LOCATION;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

public class HttpFileServerHandler extends
    SimpleChannelInboundHandler<FullHttpRequest> {
    
    
  private final String url;

  public HttpFileServerHandler(String url) {
    
    
    this.url = url;
  }

  @Override
  public void messageReceived(ChannelHandlerContext ctx,
      FullHttpRequest request) throws Exception {
    
    
    if (!request.getDecoderResult().isSuccess()) {
    
    
      sendError(ctx, BAD_REQUEST);
      return;
    }
    if (request.getMethod() != GET) {
    
    
      sendError(ctx, METHOD_NOT_ALLOWED);
      return;
    }
    final String uri = request.getUri();
    final String path = sanitizeUri(uri);
    if (path == null) {
    
    
      sendError(ctx, FORBIDDEN);
      return;
    }
    File file = new File(path);
    if (file.isHidden() || !file.exists()) {
    
    
      sendError(ctx, NOT_FOUND);
      return;
    }
    if (file.isDirectory()) {
    
    
      if (uri.endsWith("/")) {
    
    
        sendListing(ctx, file);
      } else {
    
    
        sendRedirect(ctx, uri + '/');
      }
      return;
    }
    if (!file.isFile()) {
    
    
      sendError(ctx, FORBIDDEN);
      return;
    }
    RandomAccessFile randomAccessFile = null;
    try {
    
    
      randomAccessFile = new RandomAccessFile(file, "r");// 以只读的方式打开文件
    } catch (FileNotFoundException fnfe) {
    
    
      sendError(ctx, NOT_FOUND);
      return;
    }
    long fileLength = randomAccessFile.length();
    HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
    setContentLength(response, fileLength);
    setContentTypeHeader(response, file);
    if (isKeepAlive(request)) {
    
    
      response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
    }
    ctx.write(response);
    ChannelFuture sendFileFuture;
    sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0,
        fileLength, 8192), ctx.newProgressivePromise());
    sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
    
    
      @Override
      public void operationProgressed(ChannelProgressiveFuture future,
          long progress, long total) {
    
    
        if (total < 0) {
    
     // total unknown
          System.err.println("Transfer progress: " + progress);
        } else {
    
    
          System.err.println("Transfer progress: " + progress + " / "
              + total);
        }
      }

      @Override
      public void operationComplete(ChannelProgressiveFuture future)
          throws Exception {
    
    
        System.out.println("Transfer complete.");
      }
    });
    ChannelFuture lastContentFuture = ctx
        .writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
    if (!isKeepAlive(request)) {
    
    
      lastContentFuture.addListener(ChannelFutureListener.CLOSE);
    }
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
      throws Exception {
    
    
    cause.printStackTrace();
    if (ctx.channel().isActive()) {
    
    
      sendError(ctx, INTERNAL_SERVER_ERROR);
    }
  }

  private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");

  private String sanitizeUri(String uri) {
    
    
    try {
    
    
      uri = URLDecoder.decode(uri, "UTF-8");
    } catch (UnsupportedEncodingException e) {
    
    
      try {
    
    
        uri = URLDecoder.decode(uri, "ISO-8859-1");
      } catch (UnsupportedEncodingException e1) {
    
    
        throw new Error();
      }
    }
    if (!uri.startsWith(url)) {
    
    
      return null;
    }
    if (!uri.startsWith("/")) {
    
    
      return null;
    }
    uri = uri.replace('/', File.separatorChar);
    if (uri.contains(File.separator + '.')
        || uri.contains('.' + File.separator) || uri.startsWith(".")
        || uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()) {
    
    
      return null;
    }
    return System.getProperty("user.dir") + File.separator + uri;
  }

  private static final Pattern ALLOWED_FILE_NAME = Pattern
      .compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");

  private static void sendListing(ChannelHandlerContext ctx, File dir) {
    
    
    FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
    response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
    StringBuilder buf = new StringBuilder();
    String dirPath = dir.getPath();
    buf.append("<!DOCTYPE html>\r\n");
    buf.append("<html><head><title>");
    buf.append(dirPath);
    buf.append(" 目录:");
    buf.append("</title></head><body>\r\n");
    buf.append("<h3>");
    buf.append(dirPath).append(" 目录:");
    buf.append("</h3>\r\n");
    buf.append("<ul>");
    buf.append("<li>链接:<a href=\"../\">..</a></li>\r\n");
    for (File f : dir.listFiles()) {
    
    
      if (f.isHidden() || !f.canRead()) {
    
    
        continue;
      }
      String name = f.getName();
      if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
    
    
        continue;
      }
      buf.append("<li>链接:<a href=\"");
      buf.append(name);
      buf.append("\">");
      buf.append(name);
      buf.append("</a></li>\r\n");
    }
    buf.append("</ul></body></html>\r\n");
    ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
    response.content().writeBytes(buffer);
    buffer.release();
    ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  }

  private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
    
    
    FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
    response.headers().set(LOCATION, newUri);
    ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  }

  private static void sendError(ChannelHandlerContext ctx,
      HttpResponseStatus status) {
    
    
    FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1,
        status, Unpooled.copiedBuffer("Failure: " + status.toString()
            + "\r\n", CharsetUtil.UTF_8));
    response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
    ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  }

  private static void setContentTypeHeader(HttpResponse response, File file) {
    
    
    MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
    response.headers().set(CONTENT_TYPE,
        mimeTypesMap.getContentType(file.getPath()));
  }
}

本博客基于李林峰老师著的《Netty权威指南》做的个人总结

猜你喜欢

转载自blog.csdn.net/sdkdeveloper/article/details/104456519
今日推荐