Netty网络编程——Netty入门

1.原生NIO存在的问题

2.Netty介绍

3.Netty的工作模型

4.用Netty编写TCP服务

5.任务队列的三种经典使用场景

1.原生NIO存在的问题

前面我们通过NIO的原生API实现了服务端与客户端的交互:Netty网络编程——NIO编程介绍。我们在编程的过程中,发现了如下的问题:

1)我们会发现NIO的类库和API种类繁多,需要掌握Selector,ServerSocketChannel,SocketChannel,ByteBuffer等才能顺利编程。

2)我们还需要对多线程编程,网络编程等非常熟悉,我们才能编写高质量的NIO程序。

3)开发的难度非常大,假如说有:断线重连,网络闪断,半包读写,加密解密等等

4)NIO还会有epoll bug,导致selector空轮训,使cpu空轮训。

2.Netty介绍

为了针对上面这些问题,Netty对JDK自带的NIO进行了封装,解决了上述一系列问题。

1)操作简单,对于各种类型传输有统一的api,而且扩展方便,清晰地把变化的代码和不变的代码分离开来。

2)使用方便,有详细的文档,而且没有其它依赖项。

3)高性能,吞吐量更高,延迟降低。

4)安全

5)社区活跃,被发现的bug可以被及时修复。

3.Netty的工作模型

前面我们介绍了Netty网络编程——Reactor模式高性能架构设计原理,Netty也主要基于Reactor的多线程模型做了一定的修改。

Netty网络编程——Netty入门_第1张图片

1)netty线程模型抽象出了两组线程池BossGroup专门负责处理客户端连接WorkerGroup专门负责网络的读写

2)两组线程池的类型都是NioEventLoopGroup表示一个不断循环处理任务的线程组

3)每个NioEventLoop都有一个selector,用于监听绑定在其上的socket网络通讯

4)每个BossNioEventLoop循环分3步
4.1)轮询accept事件

4.2)处理accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某个workerNioEventLoop上。

4.3)处理任务队列的任务(可能会有很耗时的操作,放在任务队列中异步执行)

5)workerNioEventLoop的任务也分三步:
5.1)轮询read,write事件
5.2)处理IO事件
5.3)处理任务队列的任务可能会有很耗时的操作,放在任务队列中异步执行)

6)每个worker处理任务的时候,会使用pipeline(管道),pipeline中包含了channel,通过pipeline可以获得对应的通道。

4.用Netty编写TCP服务

我们初步编写一个程序:

Netty服务在6668端口进行监听,客户端能发送消息给服务器,服务器也能发送消息给客户端

NettyServer:

package com.example.demo.netty.nettyDemo;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;

/**
 * @author sulingfeng
 * @title: NettyServer
 * @projectName netty-learn
 * @description: TODO
 * @date 2022/7/7 17:45
 */
@Slf4j
public class NettyServer  {

    public static void main(String[] args) throws Exception {

        //创建两个线程组
        //bossGroup只处理连接请求
        //workerGroup负责处理业务逻辑
        //bossGroup和workerGroup含有的子线程个数默认实际为cpu核数*2
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();


        try{
            //服务器启动对象
            ServerBootstrap bootstrap = new ServerBootstrap();

            bootstrap.group(bossGroup,workerGroup)//设置两个线程组
                    .channel(NioServerSocketChannel.class)//使用NioSocketChannel作为通道的实现
                    .option(ChannelOption.SO_BACKLOG,128)//设置线程队列的连接数量上限
                    .childOption(ChannelOption.SO_KEEPALIVE,true)//保持活动连接状态
                    .childHandler(new ChannelInitializer() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //设置处理器,处理的业务逻辑在这里
                            log.info("客户端socketChannel hashCode = " + ch.hashCode());
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });
            log.info("服务器准备好了");
            //监听6668端口
            ChannelFuture cf = bootstrap.bind(6668).sync();

            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if(cf.isSuccess()){
                        log.info("监听端口 6668 成功");
                    }else{
                        log.info("监听端口 6668 失败");
                    }
                }
            });
            
            //对关闭通道事件  进行监听,如果关闭了就返回执行finally
            cf.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

NettyServerHandler:

package com.example.demo.netty.nettyDemo;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * @author sulingfeng
 * @title: NettyServerHandler
 * @projectName netty-learn
 * @description: TODO
 * @date 2022/7/8 13:35
 */
//我们自定义一个Handler,把业务逻辑全都放在这里
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    //服务器端读取客户端的数据

    /**
     * @param ctx 上下文对象,包含连接,管道
     * @param msg 客户端发送的数据
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("服务器读取线程"+Thread.currentThread().getName()+" channel="+ctx.channel());
        Channel channel = ctx.channel();
        ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链表
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("客户端地址:" + channel.remoteAddress());
    }

    //当服务器端完成
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~", CharsetUtil.UTF_8));
    }

    //当发生异常的时候
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

NettyClient:

package com.example.demo.netty.nettyDemo;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

/**
 * @author sulingfeng
 * @title: NettyClient
 * @projectName netty-learn
 * @description: TODO
 * @date 2022/7/8 13:46
 */
public class NettyClient {

    public static void main(String[] args) throws Exception {
        //客户端需要一个事件循环组
        NioEventLoopGroup group = new NioEventLoopGroup();

        try{
            //创建客户端启动对象
            Bootstrap bootstrap = new Bootstrap();

            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            //客户端处理逻辑类
                            socketChannel.pipeline().addLast(new NettyClientHandler());
                        }
                    });
            //启动客户端去连接服务器端
            //关于 ChannelFuture 要分析,涉及到netty的异步模型
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
            //对关闭通道事件  进行监听
            channelFuture.channel().closeFuture().sync();

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

NettyClientHandlerr:

package com.example.demo.netty.nettyDemo;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * @author sulingfeng
 * @title: NettyServerHandler
 * @projectName netty-learn
 * @description: TODO
 * @date 2022/7/8 13:35
 */
@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    //当通道就绪就会触发该方法
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client " + ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server", CharsetUtil.UTF_8));
    }

    //当通道有读取事件时,会触发
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf buf = (ByteBuf) msg;
        System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("服务器的地址: "+ ctx.channel().remoteAddress());
    }

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

5.任务队列的三种经典使用场景

我们在观察Netty架构图,发现有一个TaskQueue的东西,我们就来讲解一下它:

我们发现,在handler里,如果任务处理时间非常地长,势必会影响到其它线程的执行,这个时候我们可以放在TaskQueue里进行进行异步执行。

TaskQueue有两种:
1)用户自定义的普通任务,放入队列中,轮到这个任务的时候,会立刻执行。

2)用户自定义的定时任务,放入队列中,轮到这个任务的时候,会立刻执行,相比普通任务,多一个延迟的时间属性,到点自动执行。

package com.example.demo.netty.nettyDemo;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author sulingfeng
 * @title: NettyServerHandler
 * @projectName netty-learn
 * @description: TODO
 * @date 2022/7/8 13:35
 */
//我们自定义一个Handler,把业务逻辑全都放在这里
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    public static List channels = new ArrayList<>();

    //服务器端读取客户端的数据
    /**
     * @param ctx 上下文对象,包含连接,管道
     * @param msg 客户端发送的数据
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("服务器读取线程"+Thread.currentThread().getName()+" channel="+ctx.channel());
        Channel channel = ctx.channel();
        channels.add(channel);
        ByteBuf buf = (ByteBuf) msg;

        //解决方案1 : 用户定义的普通自定义任务
        ctx.channel().eventLoop().execute(()->{
            try {
                Thread.sleep(5000);
                ctx.writeAndFlush(Unpooled.copiedBuffer("hello, sleep 5000", CharsetUtil.UTF_8));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        //用户自定义定时任务,可以拥有延时(delay)的效果
        ctx.channel().eventLoop().schedule(()->{
            try {
                Thread.sleep(5000);
                ctx.writeAndFlush(Unpooled.copiedBuffer("hello, sechdule sleep 5000", CharsetUtil.UTF_8));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },5, TimeUnit.SECONDS);

        //把所有channel放在一个集合中,进行转发
        channels.stream().forEach(record->{
            record.writeAndFlush(Unpooled.copiedBuffer("群发消息", CharsetUtil.UTF_8));
        });

        System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("客户端地址:" + channel.remoteAddress());


    }

    //当服务器端完成
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~", CharsetUtil.UTF_8));
    }

    //当发生异常的时候
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

你可能感兴趣的:(netty入门)