Netty 1-1 入门实例

NettyServer AND ServerChannelHandler 

创建Netty服务端

创建服务端的都是模板代码,

1.设置group,需要设置两个EventLoopGroup。bossGroup用于监听客户端Channel连接的线程组,Selector作用。workGroup用于处理网络IO,可以自定义线程数。

2.设置服务端的ServerSocketChannel类型,这里使用了NioServerSocketChannel。需要注意的是,客户端设置要对应为NioSocketChannel

3.设置option参数,主要是TCP协议的一些参数

4.设置childHandler。指的是 NioServerSocketChannel产生的子Channel,对设置初始化的Handler处理器。

5.绑定端口,默认是异步的。调用sync方法进行阻塞

6.调用closeFuture,等待关闭(服务端永远不会关闭)

7.回收EventLoopGroup资源

接受请求的能力增加,处理能力不一定增加。

import io.netty.bootstrap.ServerBootstrap;
 
 
import io.netty.channel.ChannelFuture;
 
 
import io.netty.channel.ChannelInitializer;
 
 
import io.netty.channel.ChannelOption;
 
 
import io.netty.channel.EventLoopGroup;
 
 
import io.netty.channel.nio.NioEventLoopGroup;
 
 
import io.netty.channel.socket.SocketChannel;
 
 
import io.netty.channel.socket.nio.NioServerSocketChannel;
 
 
 
  
 
 
/**
 
 
 * 对于ChannelOption.SO_BACKLOG的解释:tcp连接缓存区
 
 
 * 服务器端TCP内核模块维护有两个队列。我们称之为A和B。
 
 
 * 客户端想服务端connect的时候,会发送带有SYN标志的包(第一次握手)
 
 
 * 服务器收到客户端的SYN时,向客户端发送SYN ACK确认(第二次握手)。TCP内核将完成两次握手的连接加入到队列A,等待客户端发来ACK。
 
 
 * 收到客户端ACK(第三次握手)。TCP内核模块将连接从A队列转移到B队列,连接完成,应用程序的accept就会返回。
 
 
 * 也就是说accept方法从队列B中取出完成三次握手的连接。
 
 
 * A队列和B队列的长度之和就是backlog。当队列长度之和大于backlog时,新连接会被TCP内核拒绝。
 
 
 * 所以backlog的值过小,会出现accept的速度跟不上新连接加入的速度,AB队列满了,新的客户端无法连接
 
 
 * 注意:backlog对程序支持的线程数并无影响,只会影响没有被accept取出的连接数
 
 
 *
 
 
 */
 
 
public class NettyServer {
 
 
    public static void main(String[] args) {
 
 
 
  
 
 
        try {
 
 
            EventLoopGroup bossGroup = new NioEventLoopGroup();   //用于监听客户端Channel连接的线程组,Selector作用,默认一个线程
 
 
            EventLoopGroup workGroup = new NioEventLoopGroup(5);  //进行网络IO读写的线程组,可自定义线程数
 
 
            ServerBootstrap b = new ServerBootstrap();   //服务端引导类,整合Selector线程、IO工作线程、Channel、ChaneelPipeline
 
 
            b.group(bossGroup,workGroup)                 //绑定线程组
 
 
                    .channel(NioServerSocketChannel.class)   //确定服务端的ServerSocketChannel
 
 
                    .option(ChannelOption.SO_BACKLOG,2)   //设置tcp缓冲区
 
 
                    .option(ChannelOption.SO_SNDBUF,8*1024)  //设置发送缓冲区大小
 
 
                    .option(ChannelOption.SO_RCVBUF,8*1024)  //设置接收缓冲区大小
 
 
                    .option(ChannelOption.SO_KEEPALIVE,true) //保持连接,默认为true
 
 
                    .childHandler(new ChannelInitializer<SocketChannel>() {  //定义Channel创建 Handler处理器
 
 
                        @Override
 
 
                        protected void initChannel(SocketChannel ch) throws Exception {
 
 
                            ch.pipeline().addLast(new StringToByteEncode());
 
 
                            ch.pipeline().addLast(new ServerHelloInboundHandler()); //添加入站handler
 
 
                        }
 
 
                    });
 
 
 
  
 
 
            ChannelFuture cf = b.bind(12345).sync();  //异步的绑定端口,调用sync方法阻塞等待绑定完成。
 
 
            ChannelFuture cf2 = b.bind(12346).sync(); //可以开放多个端口
 
 
            System.out.println("Netty Server start Success");
 
 
            cf.channel().closeFuture().sync(); //异步等待channel关闭,调用sync方法阻塞。也就是服务端关闭。
 
 
            cf2.channel().closeFuture().sync();
 
 
 
  
 
 
            bossGroup.shutdownGracefully();  //释放线程组资源
 
 
            workGroup.shutdownGracefully();
 
 
        } catch (Exception e){
 
 
            e.printStackTrace();
 
 
        }
 
 
 
  
 
 
    }
 
 
 
  
 
 
}

创建服务端Handler处理器。

1.继承ChannelInboundHandlerAdapter 父类,作为Handler处理的入口。重写channelRead方法。

2.接受Netty默认的ByteBuf。

因为作为服务端处理的第一个Handler,之前没有解码器Handler。所以传入的Object msg就是ByteBuf。

因为代码是基于TCP协议的,TCP使用字节流进行传输数据,所以客户端发出的数据和服务端接受的数据 必须是ByteBuf。 如果想对Channel写入对象,就需要添加对应的编码器,将Java对象转为字节数组。(序列化)

3.对获取到的ByteBuf进行处理。

这里因为客户端传来String(转为ByteBuf后的String)。所以就直接打印了。

这是因为Handler处理器没有区分长连接中的不同次的请求,两次长连接flush数据,可能会同时冲刷到同一个ByteBuf中,也就是服务端只会处理一次。需要专门的解码器来实现 同一个长连接,不同请求的ByteBuf数据切割。

专业说法:因为TCP是基于流的传输,基于流的传输并不是一个数据包队列,而是一个字节队列。即使你发送了2个独立的数据包,操作系统也不会作为2个消息处理而仅仅是作为一连串的字节而言。因此这是不能保证你远程写入的数据就会准确地读取。这是后就需要涉及TCP 粘包拆包。

4.Channel写入数据,冲刷数据到客户端。。

ChannelHandlerContext,向CHannel写入数据。WriteAndFlush方法冲刷数据到客户端Channel。这里需要注意,需要冲刷的数据必须是ByteBuf,否则无法传输。 如果需要传输String,则需要添加自定义的 StringToByteEncode 编码器,将String在发送之前编码写入到ByteBuf。

5.添加ChannelFutureListner,关闭Channel。实现长连接和短连接。

Netty的读写操作都是异步的,调用WriteAndFlush会返回一个ChannelFuture,可以添加监听器,实现关闭Channel,这边关闭的Channel是客户端和服务端通信的CHannel。添加了监听器后,客户端的cf.channel().closeFuture().sync(); 同步阻塞结束,客户端关闭。(服务端关闭Channel)

客户端想实现Channel,就必须在Handler中添加ChannelFutureListener.CLOSE。(客户端确定本次请求完毕,主动关闭连接)。

可以通过是否添加ChannelFutureListener.CLOSE,来实现长连接和短连接。

java 基于netty的框架 netty框架教程_客户端

import io.netty.buffer.ByteBuf;
  
import io.netty.buffer.Unpooled;
  
import io.netty.channel.ChannelHandlerContext;
  
import io.netty.channel.ChannelInboundHandlerAdapter;
  
import io.netty.util.ReferenceCountUtil;
  
   
  
public class ServerHelloInboundHandler extends ChannelInboundHandlerAdapter {
  
    @Override
  
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  
        try {
  
            //Handler接受到的Object 如果没有进行解码的话,默认是ByteBuf。
  
            if (msg instanceof ByteBuf){
  
                ByteBuf byteBuf = (ByteBuf) msg;
  
                byte[] buf = new byte[byteBuf.readableBytes()];
  
                byteBuf.readBytes(buf);
  
                String body = new String (buf,"utf-8");
  
                System.out.println("get request Message:" +body);
  
   
  
                String response = new String("hello "+body);
  
                ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
  
                ctx.writeAndFlush("return a string");
  
   
  
                //添加监听器,关闭Channel。客户端的cf.channel().closeFuture().sync()就会往下执行,关闭客户端连接。
  
                //通过添加ChannelFutureListener.CLOSE 实现长连接和短连接
  
                //关闭Channel 可以在服务端可以在客户端。                     //ctx.writeAndFlush(Unpooled.copiedBuffer("".getBytes())).addListener(ChannelFutureListener.CLOSE);
  
            }
  
        } catch (Exception e) {
  
            e.printStackTrace();
  
        } finally {
  
            ReferenceCountUtil.release(msg);
  
            super.channelRead(ctx, msg);
  
        }
  
    }
  
}
 
  
 
  
 
NettyClient AND ClientChannelHandler

创建NettyClient 

1.创建过程 和Server端类似

2.添加客户端超时时间 ChannelOption.CONNECT_TIMEOUT_MILLIS

3.启动客户端连接。分为两种同步、异步两种方式。使用异步方式,需要使用CountDownLatch。

4 .通过write和flush方法冲刷数据,这边因为连续冲刷了mdq 和 mdq second。可能导致服务端接受到一次ByteBuf。(TCP粘包拆包问题)

这边一个骚操作就是:把第一次(channelActive方法中)和第二次之间 sleep了3s。 这样从时间上实现了拆包。本质是因为,Netty服务已经处理完数据,将ByteBuf 字节流重置刷新为空了。

注意:这不是正常操作。这不是正常操作。这不是正常操作。

5.closeFuture().sync()阻塞主线程。通过ClientChannelHandler完成 Channel的读写操作。

import io.netty.bootstrap.Bootstrap;
  
import io.netty.buffer.Unpooled;
  
import io.netty.channel.*;
  
import io.netty.channel.nio.NioEventLoopGroup;
  
import io.netty.channel.socket.SocketChannel;
  
import io.netty.channel.socket.nio.NioSocketChannel;
  
import java.util.concurrent.CountDownLatch;
  
   
  
public class NettyClient {
  
   
  
    public static void main(String[] args) {
  
        EventLoopGroup group = new NioEventLoopGroup();
  
        Bootstrap b = new Bootstrap();
  
        try {
  
            b.group(group)
  
                    .channel(NioSocketChannel.class)
  
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS,3000)  //设置连接超时时间
  
                    .handler(new ChannelInitializer<SocketChannel>() {
  
                        @Override
  
                        protected void initChannel(SocketChannel ch) throws Exception {
  
                            ch.pipeline().addLast(new StringToByteEncode());
  
                            ch.pipeline().addLast(new ClientHelloInboundHandler());
  
   
  
                        }
  
                    });
  
            final CountDownLatch connectedLatch = new CountDownLatch(1);
  
            //因为使用了的异步连接方式,需要CountDownLatch进行阻塞,确保连接完成。如果使用了sync同步,则不需要使用CountDownLatch
  
            //如果设置了sync同步方法,出现连接错误时,需要在finally代码块中释放EventLoopGroup的资源。不然无法释放线程。
  
            ChannelFuture cf = b.connect("127.0.0.1",12345).sync();
  
            //监听Channel是否建立成功
  
            cf.addListener(new ChannelFutureListener() {
  
                @Override
  
                public void operationComplete(ChannelFuture future) throws Exception {
  
                    if (future.isSuccess()) {
  
                        //若Channel建立成功,保存建立成功的标记
  
                        System.out.println("Netty client connection Success");
  
                    } else {
  
                        //若Channel建立失败,保存建立失败的标记
  
                        System.out.println("Netty client connection Failed");
  
                        future.cause().printStackTrace();
  
                    }
  
                    connectedLatch.countDown();
  
                }
  
            });
  
            connectedLatch.await();
  
   
  
            Channel channel = cf.channel();
  
            Thread.sleep(3000);
  
            channel.write("mdq");
  
            channel.flush();
  
            channel.writeAndFlush(Unpooled.copiedBuffer("mdq second".getBytes()));
  
            cf.channel().closeFuture().sync();
  
        } catch (Exception e){
  
            e.printStackTrace();
  
        } finally {
  
            group.shutdownGracefully();
  
        }
  
   
  
    }
  
}
 
  
 
  
 
ClientChannelHandler

1.接受到服务的数据,默认也是ByteBuf

2.对ByteBuf 进行处理,客户端可会出现和服务端出现一样的情况。 服务端多次冲刷的数据,在客户端只接收到一个ByteBuf。

3.可以通过ctx.writeAndFlush 继续想服务端写入数据。

4.可以添加监听器,客户端主动关闭连接

import io.netty.buffer.ByteBuf;
  
import io.netty.buffer.Unpooled;
  
import io.netty.channel.ChannelHandlerContext;
  
import io.netty.channel.ChannelInboundHandlerAdapter;
  
import io.netty.util.ReferenceCountUtil;
  
   
  
public class ClientHelloInboundHandler extends ChannelInboundHandlerAdapter {
  
   
  

           //连接创建的时候执行 
  
  
    @Override
  
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
  
        ctx.writeAndFlush(Unpooled.copiedBuffer("channelActive".getBytes()));
  
    }
  
   
  
    @Override
  
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  
        try {
  
            if (msg instanceof ByteBuf){
  
                ByteBuf byteBuf = (ByteBuf) msg;
  
                byte[] buf = new byte[byteBuf.readableBytes()];
  
                byteBuf.readBytes(buf);
  
                String response = new String (buf,"utf-8");
  
                System.out.println("get response Message:" +response);
  
//客户端确认请求结束,主动关闭连接。
  
//ctx.writeAndFlush(Unpooled.copiedBuffer("".getBytes())).addListener(ChannelFutureListener.CLOSE);
  
            }
  
        } catch (Exception e) {
  
            e.printStackTrace();
  
        } finally {
  
            ReferenceCountUtil.release(msg);
  
            super.channelRead(ctx, msg);
  
        }
  
    }
  
}
 
  
 
  
 
 
TCP

java 基于netty的框架 netty框架教程_服务端_02

在服务端和客户端保持长连接的过程中,客户端可以不断向服务端冲刷数据。,服务端可以不断的接收数据并向客户端返回数据。

Netty中TCP传输数据的对象必须是ByteBuf,如果需要传输其他对象需要添加编码器和解码器。也就是说客户端最后发出的必须是ByteBuf,而服务端首先接受的到也必定是ByteBuf。

在正常应用中:需要客户端和服务端中设置编码器和解码器(MessageToByteEncoder ByteToMessageDecoder)。  编码器的实现比较简单,只需要将Java对象转化为byte数组(序列化、String.getBytes),并将byte数组到ByteBuf out中即可。 解码器的功能就比较重要了,需要判断当前的ByteBuf中的数据是否能反序列化成一个完整的对象。如果ByteBuf中数据不够怎么处理、如果ByteBuf中包含了下一个长连接请求的数据该如何处理(这个还是粘包和解包的问题)。

创建编码器 MessageToByteEncoder

因为 Netty TCP需要传输 ByteBuf进行传输数据,这边添加简单的自定义编码器,将String对象 编码为ByteBuf,然后进行传输。

import io.netty.buffer.ByteBuf;
  
import io.netty.channel.ChannelHandlerContext;
  
import io.netty.handler.codec.MessageToByteEncoder;
  
   
  
public class StringToByteEncode extends MessageToByteEncoder {
  
   
  
    @Override
  
    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
  
        if (msg instanceof String){
  
            String s = (String) msg;
  
            out.writeBytes(s.getBytes());
  
        } else if (msg instanceof  ByteBuf){
  
            ByteBuf buf = (ByteBuf) msg;
  
            byte[] b = new byte[buf.readableBytes()];
  
            buf.readBytes(b) ;
  
            out.writeBytes(b);
  
        }
  
    }
  
}

参考资料  http://ifeve.com/netty5-user-guide/