Netty 入门学习

7 篇文章 0 订阅
1 篇文章 0 订阅

Netty 入门学习

@author lisiwen

本文参考 https://bugstack.cn/itstack-demo-netty/itstack-demo-netty-1.html、https://www.jianshu.com/p/b9f3f6a16911

1. 认识Netty

1.1. 什么是Netty

Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke’s Choice Award,见https://www.java.net/dukeschoice/2011)。它活跃和成长于用户社区,像大型公司 Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其强大的对于网络抽象的核心代码。

1.2. Netty特点

1) 高并发

Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架。

Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。

2)传输快

Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。

零拷贝描述的是CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输一个文件时以减少CPU周期和内存带宽。

3)封装好

Netty封装了NIO操作的很多细节,提供易于使用的API。

1.3.Netty和Tomcat有什么区别?

​ Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器,但是Netty不一样,他能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能,这就是netty和tomcat最大的不同。

2. Netty核心组件

2.1.Bootstrap 和 ServerBootstrap

​ BootStarp 和 ServerBootstrap 被称为引导类,指对应用程序进行配置,并使他运行起来的过程。Netty处理引导的方式是使你的应用程序和网络层相隔离。

​ BootStrap 是客户端的引导类,Bootstrap 在调用 bind()(连接UDP)和 connect()(连接TCP)方法时,会新创建一个 Channel,仅创建一个单独的、没有父 Channel 的 Channel 来实现所有的网络交换。

在这里插入图片描述

​ ServerBootstrap 是服务端的引导类,ServerBootstarp 在调用 bind() 方法时会创建一个 ServerChannel 来接受来自客户端的连接,并且该 ServerChannel 管理了多个子 Channel 用于同客户端之间的通信。

在这里插入图片描述

2.2. Channel

Channel是Java NIO的一个基本构造。可以看作是传入或传出数据的载体。因此,它可以被打开或关闭,连接或者断开连接。以下是常用的Channel:

– EmbeddedChannel
– LocalServerChannel
– NioDatagramChannel
– NioSctpChannel
– NioSocketChannel

2.3. 回调

当一个回调被触发时,相应的事件可以被一个interface-ChannelHandler的实现处理。

2.4. Future

Netty中所有的I/O操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种在之后的某个时间点确定其结果的方法。

Future 和 回调 是相互补充的机制,提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。

Netty 提供了ChannelFuture,用于在执行异步操作的时候使用。每个Netty的出站I/O操作都会返回一个ChannelFuture。ChannelFuture能够注册一个或者多个ChannelFutureListener 实例。监听器的回调方法operationComplete(),将会在对应的操作完成时被调用。

2.5.ChannelHandler

Netty 的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。

Netty 使用不同的事件来通知我们状态的改变或者是操作的状态,每个事件都可以被分发给ChannelHandler类中某个用户实现的方法。Netty提供了大量预定义的可以开箱即用的ChannelHandler实现,包括用于各种协议的ChannelHandler。

现在,事件可以被分发给ChannelHandler类中某个用户实现的方法。那么,如果 ChannelHandler 处理完成后不直接返回给客户端,而是传递给下一个ChannelHandler 继续处理呢?那么就要说到 ChannelPipeline !

ChannelPipeline 提供了 ChannelHandler链 的容器,并定义了用于在该链上传播入站和出站事件流的API。使得事件流经 ChannelPipeline 是 ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行他们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler:

1、一个ChannelInitializer的实现被注册到了ServerBootstrap中。
2、当 ChannelInitializer.initChannel()方法被调用时, ChannelInitializer将在 ChannelPipeline 中安装一组自定义的 ChannelHandler。
3、ChannelInitializer 将它自己从 ChannelPipeline 中移除。

在这里插入图片描述

2.6. EventLoop

EventLoop 定义了Netty的核心抽象,用来处理连接的生命周期中所发生的事件,在内部,将会为每个Channel分配一个EventLoop。

EventLoop本身只由一个线程驱动,其处理了一个Channel的所有I/O事件,并且在该EventLoop的整个生命周期内都不会改变。这个简单而强大的设计消除了你可能有的在ChannelHandler实现中需要进行同步的任何顾虑。

在这里插入图片描述

这里需要说到,EventLoop的管理是通过EventLoopGroup来实现的。还要一点要注意的是,客户端引导类是 Bootstrap,只需要一个EventLoopGroup。服务端引导类是 ServerBootstrap,通常需要两个 EventLoopGroup,一个用来接收客户端连接,一个用来处理 I/O 事件(也可以只使用一个 EventLoopGroup,此时其将在两个场景下共用同一个 EventLoopGroup)。

在这里插入图片描述

1、一个 EventLoopGroup 包含一个或者多个 EventLoop;
2、一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
3、所有由 EventLoop 处理的 I/O 事件都将在它专有的Thread 上被处理;
4、一个 Channel 在它的生命周期内只注册于一个EventLoop;
5、NIO中,一个 EventLoop 分配给多个 Channel(面对多个Channel,一个 EventLoop 按照事件触发,顺序执行); OIO中,一个 EventLoop 分配给一个 Channel。

tips:Netty 应用程序的一个一般准则:尽可能的重用 EventLoop,以减少线程创建所带来的开销。

3.项目代码示例

3.1.服务端部分

NettyServer.java

/**
 * netty服务端
 *
 * @author: lisiwen
 * @date: 2020/9/12 10:33
 **/
public class NettyServer {

    public static void main(String[] args) {
        new NettyServer().bing(8080);
    }

    private void bing(int port) {
        //配置服务端NIO线程组
        EventLoopGroup parentGroup = new NioEventLoopGroup();
        EventLoopGroup childGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(parentGroup, childGroup)
                    //非阻塞模式
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childHandler(new MyChannelInitializer());
            ChannelFuture f = b.bind(port).sync();
            System.out.println("服务端启动成功");
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            childGroup.shutdownGracefully();
            parentGroup.shutdownGracefully();
        }

    }
}

MyChannelInitializer.java

/**
 * 管道初始化
 *
 * @author: lisiwen
 * @date: 2020/9/12 10:44
 **/
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        //对象传输处理
        socketChannel.pipeline().addLast(new ObjDecoder(SendMessage.class));
        socketChannel.pipeline().addLast(new ObjEncoder(FeedBackMessage.class));
        // 在管道中添加我们自己的接收数据实现方法
        socketChannel.pipeline().addLast(new MyServerHandler());
    }
}

MyServerHandler.java

/**
 * 服务端处理
 *
 * @author: lisiwen
 * @date: 2020/9/12 10:44
 **/
public class MyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        SocketChannel channel = (SocketChannel) ctx.channel();
        System.out.println("链接报告开始");
        System.out.println("链接报告信息:有一客户端链接到本服务端。channelId:" + channel.id());
        System.out.println("链接报告IP:" + channel.localAddress().getHostString());
        System.out.println("链接报告Port:" + channel.localAddress().getPort());
        System.out.println("链接报告完毕");
        //通知客户端链接建立成功
        String str = "通知客户端链接建立成功" + " " + new Date() + " " + channel.localAddress().getHostString() + "\r\n";
        ctx.writeAndFlush(FeedBackMessage.SuccessMessage(str, channel.id().toString()));
    }

    /**
     * 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        System.out.println("客户端断开链接" + ctx.channel().localAddress().toString());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //接收msg消息{与上一章节相比,此处已经不需要自己进行解码}
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息类型:" + msg.getClass());
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息内容:" + JSONUtil.toJsonStr(msg));
    }

    /**
     * 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
        System.out.println("异常信息:\r\n" + cause.getMessage());
    }
}

3.2.客户端部分

NettyClient.java

/**
 * netty客户端
 *
 * @author: lisiwen
 * @date: 2020/9/12 10:32
 **/
public class NettyClient {
    public static void main(String[] args) {
        new NettyClient().connect("127.0.0.1", 8080);
    }

    private void connect(String inetHost, int inetPort) {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.AUTO_READ, true);
            b.handler(new MyChannelInitializer());
            ChannelFuture f = b.connect(inetHost, inetPort).sync();
            System.out.println("客户端启动成功");

            f.channel().writeAndFlush(SendMessage.build("你好,使用protobuf通信格式的服务端,我是客户端。", f.channel().id().toString()));

            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

MyChannelInitializer.java

/**
 * 管道初始化
 *
 * @author: lisiwen
 * @date: 2020/9/12 10:44
 **/
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) {
        //对象传输处理
        socketChannel.pipeline().addLast(new ObjDecoder(FeedBackMessage.class));
        socketChannel.pipeline().addLast(new ObjEncoder(SendMessage.class));
        // 在管道中添加我们自己的接收数据实现方法
        socketChannel.pipeline().addLast(new MyClientHandler());
    }
}

MyClientHandler.java

/**
 * 客户端处理
 *
 * @author: lisiwen
 * @date: 2020/9/12 10:44
 **/
public class MyClientHandler extends ChannelInboundHandlerAdapter {
    /**
     * 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        SocketChannel channel = (SocketChannel) ctx.channel();
        System.out.println("链接报告开始");
        System.out.println("链接报告信息:本客户端链接到服务端。channelId:" + channel.id());
        System.out.println("链接报告IP:" + channel.localAddress().getHostString());
        System.out.println("链接报告Port:" + channel.localAddress().getPort());
        System.out.println("链接报告完毕");
        //通知客户端链接建立成功
        String str = "通知服务端链接建立成功" + " " + new Date() + " " + channel.localAddress().getHostString();
        ctx.writeAndFlush(FeedBackMessage.SuccessMessage(channel.id().toString(), str));
    }

    /**
     * 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("断开链接" + ctx.channel().localAddress().toString());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //接收msg消息{与上一章节相比,此处已经不需要自己进行解码}
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息类型:" + msg.getClass());
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息内容:" + JSONUtil.toJsonStr(msg));
    }

    /**
     * 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
        System.out.println("异常信息:\r\n" + cause.getMessage());
    }
}

3.3.编码解码器部分

ObjDecoder.java

/**
 * 对象解码器
 *
 * @author: lisiwen
 * @date: 2020/9/12 11:09
 **/
public class ObjDecoder extends ByteToMessageDecoder {

    private Class<?> genericClass;

        public ObjDecoder(Class<?> genericClass) {
        this.genericClass = genericClass;
    }


    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        if (byteBuf.readableBytes() < 4) {
            return;
        }
        byteBuf.markReaderIndex();
        int dataLength = byteBuf.readInt();
        if (byteBuf.readableBytes() < dataLength) {
            byteBuf.resetReaderIndex();
            return;
        }
        byte[] data = new byte[dataLength];
        byteBuf.readBytes(data);
        list.add(ObjectUtil.unserialize(data));

    }
}

ObjEncoder.java

/**
 * 对象编码器
 *
 * @author: lisiwen
 * @date: 2020/9/12 11:09
 **/
public class ObjEncoder  extends MessageToByteEncoder {
    private Class<?> genericClass;

    public ObjEncoder(Class<?> genericClass) {
        this.genericClass = genericClass;
    }


    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
        if (genericClass.isInstance(o)) {
            byte[] data = ObjectUtil.serialize(o);
            byteBuf.writeInt(data.length);
            byteBuf.writeBytes(data);
        }
    }
}

3.4.传输对象部分

SendMessage.java

/**
 * netty发送对象
 *
 * @author: lisiwen
 * @date: 2020/9/12 11:39
 **/
@Data
public class SendMessage implements Serializable {
    private String message;
    private String channelId;

    public SendMessage() {}

    public SendMessage(String message, String channelId) {
        this.message=message;
        this.channelId=channelId;
    }

    public static SendMessage build(String message, String channelId) {
        return new SendMessage(message, channelId);
    }
}

FeedBackMessage.java

/**
 * netty反馈消息对象
 *
 * @author: lisiwen
 * @date: 2020/9/12 11:41
 **/
@Data
public class FeedBackMessage<T> implements Serializable {

    private String code;
    private String message;
    private String channelId;
    private T data;

    public FeedBackMessage() {

    }

    public FeedBackMessage(String code, String message) {
        this.code = code;
        this.message = message;
    }


    public static <T> FeedBackMessage SuccessMessage() {
        return new FeedBackMessage("200", "请求成功");
    }

    public static <T> FeedBackMessage SuccessMessage(T data) {
        FeedBackMessage<T> feedBackMessage = SuccessMessage();
        feedBackMessage.setData(data);
        return feedBackMessage;
    }

    public static <T> FeedBackMessage SuccessMessage(T data, String channelId) {
        FeedBackMessage<T> feedBackMessage = SuccessMessage(data);
        feedBackMessage.setChannelId(channelId);
        return feedBackMessage;
    }

}

3.5.项目启动结果

服务端日志:
服务端启动成功
链接报告开始
链接报告信息:有一客户端链接到本服务端。channelId:d9273a36
链接报告IP:127.0.0.1
链接报告Port:8080
链接报告完毕
2020-09-14 13:49:45 接收到消息类型:class nettytest.object.SendMessage
2020-09-14 13:49:45 接收到消息内容:{"message":"你好,服务端,我是客户端。","channelId":"b6d87929"}

客户端日志:  
客户端启动成功
链接报告开始
链接报告信息:本客户端链接到服务端。channelId:b6d87929
链接报告IP:127.0.0.1
链接报告Port:52002
链接报告完毕
2020-09-14 13:49:45 接收到消息类型:class nettytest.object.FeedBackMessage
2020-09-14 13:49:45 接收到消息内容:{"code":"200","data":"通知客户端链接建立成功 Mon Sep 14 13:49:45 CST 2020 127.0.0.1\r\n","message":"请求成功","channelId":"d9273a36"}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值