Netty 1-1 入门实例

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)。所以就直接打印了。
客户端代码中对服务端进行了三次请求,也就是冲刷了三次数据。 可以从打印结果中看到,第一个客户端,ServerHandler处理了三次。但是第二个客户端,ServerHandler只处理了两次。这是因为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,来实现长连接和短连接。





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




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

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);
        }
    }
}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章