The introduction to Netty programming is super detailed, and this article is enough

foreword

This article mainly describes some features and important components of the Netty framework. I hope that after reading it, I can have a more intuitive feeling about the Netty framework. I hope it can help readers get started with Netty quickly and reduce some detours.

1. Introduction

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high-performance protocol servers and clients.

2. Why use Netty

From the official website, Netty is a network application framework for developing servers and clients. That is, a framework for network programming. Since it is network programming, Socket will not be discussed, why not use NIO?

2.1 Disadvantages of NIO

  • The class library and API of NIO are complicated, and the learning cost is high. You need to be proficient in Selector, ServerSocketChannel, SocketChannel, ByteBuffer, etc.
  • Familiarity with Java multithreaded programming is required. This is because NIO programming involves Reactor mode, you must be very familiar with multithreading and network programming in order to write high-quality NIO programs.
  • The infamous epoll bug. It causes the Selector to poll empty, which eventually leads to 100% CPU. Until the JDK1.7 version has not been fundamentally resolved.

2.2 Advantages of Netty

In contrast, Netty has many advantages:

  • The API is easy to use and the learning cost is low.
  • Powerful, built-in a variety of decoding encoders, support for a variety of protocols.
  • High performance. Compared with other mainstream NIO frameworks, Netty has the best performance.
  • The community is active, BUG will be repaired in time, the iterative version cycle is short, and new functions are constantly added.
  • Both Dubbo and Elasticsearch use Netty, and the quality has been verified.

3. Structure diagram

insert image description here

  • The green core core module includes zero copy, API library, and extensible event model.
  • The orange part is Protocol Support protocol support, including Http protocol, webSocket, SSL (Secure Socket Protocol), - - Google Protobuf protocol, zlib/gzip compression and decompression, Large File Transfer large file transfer, etc.
  • The red part is Transport Services transmission services, including Socket, Datagram, Http Tunnel and so on.
    It can be seen from the above that Netty's functions, protocols, and transmission methods are relatively complete and powerful.

4. Forever Hello Word

First build a HelloWord project, familiarize yourself with the API, and pave the way for later learning. Based on the picture below:
insert image description here

4.1 Introducing Maven dependencies

The version used is 4.1.20, a relatively stable version.

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.20.Final</version>
</dependency>

4.2 Create a server startup class

public class MyServer {
    
    
    public static void main(String[] args) throws Exception {
    
    
        //创建两个线程组 boosGroup、workerGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
    
    
            //创建服务端的启动对象,设置参数
            ServerBootstrap bootstrap = new ServerBootstrap();
            //设置两个线程组boosGroup和workerGroup
            bootstrap.group(bossGroup, workerGroup)
                //设置服务端通道实现类型    
                .channel(NioServerSocketChannel.class)
                //设置线程队列得到连接个数    
                .option(ChannelOption.SO_BACKLOG, 128)
                //设置保持活动连接状态    
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                //使用匿名内部类的形式初始化通道对象    
                .childHandler(new ChannelInitializer<SocketChannel>() {
    
    
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
    
    
                            //给pipeline管道设置处理器
                            socketChannel.pipeline().addLast(new MyServerHandler());
                        }
                    });//给workerGroup的EventLoop对应的管道设置处理器
            System.out.println("java技术爱好者的服务端已经准备就绪...");
            //绑定端口号,启动服务端
            ChannelFuture channelFuture = bootstrap.bind(6666).sync();
            //对关闭通道进行监听
            channelFuture.channel().closeFuture().sync();
        } finally {
    
    
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

4.3 Create a server-side processor

/**
 * 自定义的Handler需要继承Netty规定好的HandlerAdapter
 * 才能被Netty框架所关联,有点类似SpringMVC的适配器模式
 **/
public class MyServerHandler extends ChannelInboundHandlerAdapter {
    
    

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
        //获取客户端发送过来的消息
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("收到客户端" + ctx.channel().remoteAddress() + "发送的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    
    
        //发送消息给客户端
        ctx.writeAndFlush(Unpooled.copiedBuffer("服务端已收到消息,并给你发送一个问号?", CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
        //发生异常,关闭通道
        ctx.close();
    }
}

4.4 Create a client startup class

public class MyClient {
    
    

    public static void main(String[] args) throws Exception {
    
    
        NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
        try {
    
    
            //创建bootstrap对象,配置参数
            Bootstrap bootstrap = new Bootstrap();
            //设置线程组
            bootstrap.group(eventExecutors)
                //设置客户端的通道实现类型    
                .channel(NioSocketChannel.class)
                //使用匿名内部类初始化通道
                .handler(new ChannelInitializer<SocketChannel>() {
    
    
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
    
    
                            //添加客户端通道的处理器
                            ch.pipeline().addLast(new MyClientHandler());
                        }
                    });
            System.out.println("客户端准备就绪,随时可以起飞~");
            //连接服务端
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
            //对通道关闭进行监听
            channelFuture.channel().closeFuture().sync();
        } finally {
    
    
            //关闭线程组
            eventExecutors.shutdownGracefully();
        }
    }
}

4.5 Create a client handler

public class MyClientHandler extends ChannelInboundHandlerAdapter {
    
    

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
        //发送消息到服务端
        ctx.writeAndFlush(Unpooled.copiedBuffer("歪比巴卜~茉莉~Are you good~马来西亚~", CharsetUtil.UTF_8));
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
        //接收服务端发送过来的消息
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("收到服务端" + ctx.channel().remoteAddress() + "的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
    }
}

4.6 Testing

Start the server first, then start the client, and you can see the result:

MyServer print result:
insert image description here
MyClient print result:
insert image description here

5. Features and important components of Netty

5.1 taskQueue task queue

If the Handler processor has some long-term business processing, it can be handed over to taskQueue for asynchronous processing. How to use it, please see the code demo:

public class MyServerHandler extends ChannelInboundHandlerAdapter {
    
    

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
        //获取到线程池eventLoop,添加线程,执行
        ctx.channel().eventLoop().execute(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    //长时间操作,不至于长时间的业务操作导致Handler阻塞
                    Thread.sleep(1000);
                    System.out.println("长时间的业务处理");
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        });
    }
}

When we make a debug debug, we can see that the added taskQueue has a task.
insert image description here

5.2 scheduleTaskQueue delayed task queue

The delayed task queue is very similar to the task queue described above, except that there is an additional setting that can delay execution for a certain period of time. Please see the code demonstration:

ctx.channel().eventLoop().schedule(new Runnable() {
    
    
    @Override
    public void run() {
    
    
        try {
    
    
            //长时间操作,不至于长时间的业务操作导致Handler阻塞
            Thread.sleep(1000);
            System.out.println("长时间的业务处理");
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
},5, TimeUnit.SECONDS);//5秒后执行

Still open debug to debug and view, we can have a scheduleTaskQueue task to be executed

insert image description here

5.3 Future asynchronous mechanism

When building the HelloWord project, we saw a line of code like this:

ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666);

Many operations return this ChannelFuture object. What is this ChannelFuture object used for?

ChannelFuture provides a way to be notified asynchronously when an operation is complete. Generally, in Socket programming, waiting for the response result is blocked synchronously, but Netty will not cause blocking, because ChannelFuture obtains the result in a form similar to the observer mode. Please see a code demo:

//添加监听器
channelFuture.addListener(new ChannelFutureListener() {
    
    
    //使用匿名内部类,ChannelFutureListener接口
    //重写operationComplete方法
    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
    
    
        //判断是否操作成功    
        if (future.isSuccess()) {
    
    
            System.out.println("连接成功");
        } else {
    
    
            System.out.println("连接失败");
        }
    }
});

5.4 Bootstrap与ServerBootStrap

Bootstrap and ServerBootStrap are a factory class provided by Netty to create client and server starters. Using this factory class is very convenient to create startup classes. According to some examples above, it can be seen that it can greatly reduce the difficulty of development. . First look at a class diagram:
insert image description here
It can be seen that they are all inherited from the AbstractBootStrap abstract class, so the configuration methods are roughly the same.

In general, the steps to create a starter with Bootstrap can be divided into the following steps:
insert image description here

5.4.1 group()

In the previous article "Reactor Mode", we said that the server needs to use two thread groups:

bossGroup is used to monitor client connections, and is responsible for creating connections with clients and registering connections to the Selector of workerGroup.
workerGroup is used to handle read and write events for each connection.
Generally, create a thread group and use the following new directly to finish the job:

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

I am a little curious, since it is a thread group, what is the default number of threads? In-depth source code:

//使用一个常量保存
    private static final int DEFAULT_EVENT_LOOP_THREADS;

    static {
    
    
        //NettyRuntime.availableProcessors() * 2,cpu核数的两倍赋值给常量
        DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
                "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

        if (logger.isDebugEnabled()) {
    
    
            logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
        }
    }
    
    protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
    
    
        //如果不传入,则使用常量的值,也就是cpu核数的两倍
        super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
    }

As you can see from the source code, the default number of threads is twice the number of CPU cores. Suppose you want to customize the number of threads, you can use a parameterized constructor:

//设置bossGroup线程数为1
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//设置workerGroup线程数为16
EventLoopGroup workerGroup = new NioEventLoopGroup(16);

5.4.2 channel()

This method is used to set the channel type. When the connection is established, the corresponding Channel instance will be created according to this setting.
insert image description here
Using the debug mode, you can see that
insert image description here
the channel types are as follows:

NioSocketChannel: Asynchronous non-blocking client TCP Socket connection.

NioServerSocketChannel: Asynchronous non-blocking server-side TCP Socket connection.

These two channel types are commonly used because they are asynchronous and non-blocking. So it is the first choice.

OioSocketChannel: Synchronous blocking client TCP Socket connection.

OioServerSocketChannel: Synchronous blocking server-side TCP Socket connection.

I have debugged it locally, but it is different from Nio in use, because it is blocked, so the API calls are also different. Because it is blocking IO, few people will choose to use Oio, so it is difficult to find examples. I thought about it for a while, and after several error reports, I finally got it through. code show as below:

//server端代码,跟上面几乎一样,只需改三个地方
//这个地方使用的是OioEventLoopGroup
EventLoopGroup bossGroup = new OioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup)//只需要设置一个线程组boosGroup
        .channel(OioServerSocketChannel.class)//设置服务端通道实现类型

//client端代码,只需改两个地方
//使用的是OioEventLoopGroup
EventLoopGroup eventExecutors = new OioEventLoopGroup();
//通道类型设置为OioSocketChannel
bootstrap.group(eventExecutors)//设置线程组
        .channel(OioSocketChannel.class)//设置客户端的通道实现类型

NioSctpChannel: Asynchronous client Sctp (Stream Control Transmission Protocol, Stream Control Transmission Protocol) connection.

NioSctpServerChannel: Asynchronous Sctp server connection.

It failed to start locally. I read some comments from netizens on the Internet, saying that it can only be started under the Linux environment. From the error message: SCTP not supported on this platform, does not support this platform. Because my computer is a window system, what netizens said makes sense.

5.4.3 option()与childOption()

Let me first talk about the difference between the two.

option() sets the server to receive incoming connections, that is, the boosGroup thread.

childOption() is provided to the connection received by the parent pipeline, that is, the workerGroup thread.

After figuring it out, let's take a look at some commonly used settings:

SocketChannel parameters, which are commonly used parameters of childOption():

SO_RCVBUF Socket parameter, TCP data receiving buffer size.

TCP_NODELAY TCP parameter, send data immediately, the default value is True.

SO_KEEPALIVE Socket parameter, connection keep alive, the default value is False. When this function is enabled, TCP will actively detect the validity of idle connections.

ServerSocketChannel parameters, which are common parameters of option():

SO_BACKLOG Socket parameter, the queue length for the server to accept connections, if the queue is full, the client connection will be rejected. The default value is 200 for Windows and 128 for others.

Due to space limitations, I won’t list others. You can go to the Internet to find information and have a look.

5.4.3 option()与childOption()

Let me first talk about the difference between the two.

option() sets the server to receive incoming connections, that is, the boosGroup thread.

childOption() is provided to the connection received by the parent pipeline, that is, the workerGroup thread.

After figuring it out, let's take a look at some commonly used settings:

SocketChannel parameters, which are commonly used parameters of childOption():

SO_RCVBUF Socket parameter, TCP data receiving buffer size.

TCP_NODELAY TCP parameter, send data immediately, the default value is True.

SO_KEEPALIVE Socket parameter, connection keep alive, the default value is False. When this function is enabled, TCP will actively detect the validity of idle connections.

ServerSocketChannel parameters, which are common parameters of option():

SO_BACKLOG Socket parameter, the queue length for the server to accept connections, if the queue is full, the client connection will be rejected. The default value is 200 for Windows and 128 for others.

Due to space limitations, I won’t list others. You can go to the Internet to find information and have a look.

5.4.4 Setting up the pipeline (emphasis)

ChannelPipeline is the chain of responsibility for Netty to process requests, and ChannelHandler is the processor that specifically processes requests. In fact, each channel has a pipeline of processors.

In Bootstrap, the childHandler() method needs to initialize the channel and instantiate a ChannelInitializer. At this time, it is necessary to rewrite the method of initChannel() to initialize the channel. The assembly line is carried out in this place.

//使用匿名内部类的形式初始化通道对象
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    
    
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
    
    
        //给pipeline管道设置自定义的处理器
        socketChannel.pipeline().addLast(new MyServerHandler());
    }
});

Processor Handler is mainly divided into two types:

ChannelInboundHandlerAdapter (inbound handler), ChannelOutboundHandler (outbound handler)

Inbound refers to the data from the underlying java NIO Channel to the Netty Channel.

Outbound refers to operating the underlying java NIO Channel through Netty's Channel.

Commonly used events for ChannelInboundHandlerAdapter processors are:

  • Register event fireChannelRegistered.
  • Connection establishment event fireChannelActive.
  • Read events and read completion events fireChannelRead, fireChannelReadComplete.
  • Exception notification event fireExceptionCaught.
  • User-defined event fireUserEventTriggered.
  • Channel writable state change event fireChannelWritabilityChanged.
  • Connection closed event fireChannelInactive.

Commonly used events for ChannelOutboundHandler processors are:

  • Port binding bind.
  • Connect to the server connect.
  • Write event write.
  • Refresh time flush.
  • Read event read.
  • Actively disconnect disconnect.
  • Close the channel event close.
  • There is also a similar handler(), which is mainly used to assemble the parent channel, which is the bossGroup thread. Under normal circumstances, this method is not used.

5.4.5 bind()

Provide the address and port number for the server or client to bind the server. The default is asynchronous startup. If the sync() method is added, it is synchronous.

There are five overloaded methods with the same name, all of which are used to bind the address port number. Not introduced one by one.

5.4.6 Close EventLoopGroup gracefully

//释放掉所有的资源,包括创建的线程
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

All child Channels will be closed. After closing, the underlying resources are released.

5.5 Channel

What is a Channel? Take a look at the description of the official documentation:

A nexus to a network socket or a component which is capable of I/O operations such as read, write, connect, and bind

Translation idea: A component that connects to a network socket or can perform I/O operations such as reading, writing, connecting, and binding.

If the above paragraph is more abstract, here is another paragraph:

A channel provides a user:
the current state of the channel (e.g. is it open? is it connected?),
the configuration parameters of the channel (e.g. receive buffer size),
the I/O operations that the channel supports (e.g. read, write, connect, and bind), and
the ChannelPipeline which handles all I/O events and requests associated with the channel.

Translation to the effect:

The channel provides users with:

  • The current state of the channel (e.g. is it open? or connected?)
  • Channel configuration parameters (such as the size of the receive buffer)
  • The IO operations supported by the channel (such as read, write, connect, and bind), and the ChannelPipeline that handles all IO events and requests associated with the channel.

5.5.1 Get channel status

boolean isOpen(); //如果通道打开,则返回true
boolean isRegistered();//如果通道注册到EventLoop,则返回true
boolean isActive();//如果通道处于活动状态并且已连接,则返回true
boolean isWritable();//当且仅当I/O线程将立即执行请求的写入操作时,返回true

The above are the methods to obtain the four states of the channel.

5.5.2 Get channel configuration parameters

To obtain a single piece of configuration information, use getOption(), code demonstration:

ChannelConfig config = channel.config();//获取配置参数
//获取ChannelOption.SO_BACKLOG参数,
Integer soBackLogConfig = config.getOption(ChannelOption.SO_BACKLOG);
//因为我启动器配置的是128,所以我这里获取的soBackLogConfig=128

To get multiple pieces of configuration information, use getOptions(), code demonstration:

ChannelConfig config = channel.config();
Map<ChannelOption<?>, Object> options = config.getOptions();
for (Map.Entry<ChannelOption<?>, Object> entry : options.entrySet()) {
    
    
    System.out.println(entry.getKey() + " : " + entry.getValue());
}
/**
SO_REUSEADDR : false
WRITE_BUFFER_LOW_WATER_MARK : 32768
WRITE_BUFFER_WATER_MARK : WriteBufferWaterMark(low: 32768, high: 65536)
SO_BACKLOG : 128
以下省略...
*/

5.5.3 IO operations supported by channel

Write operation, here demonstrates writing a message from the server to the client:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
    ctx.channel().writeAndFlush(Unpooled.copiedBuffer("这波啊,这波是肉蛋葱鸡~", CharsetUtil.UTF_8));
}

Client console:

//收到服务端/127.0.0.1:6666的消息:这波啊,这波是肉蛋葱鸡~

Connection operation, code demo:

ChannelFuture connect = channelFuture.channel().connect(new InetSocketAddress("127.0.0.1", 6666));//一般使用启动器,这种方式不常用

Get ChannelPipeline through channel and do related processing:

//获取ChannelPipeline对象
ChannelPipeline pipeline = ctx.channel().pipeline();
//往pipeline中添加ChannelHandler处理器,装配流水线
pipeline.addLast(new MyServerHandler());

5.6 Selector

In NioEventLoop, there is a member variable selector, which is the Selector of the nio package. In the previous "NIO Introduction", I have already talked about the Selector.

The Selector in Netty is also the same as the Selector in NIO, which is used to listen to events, manage channels registered in the Selector, and implement multiplexers.
insert image description here

5.7 PiPeline与ChannelPipeline

When introducing Channel earlier, we know that ChannelHandler pipeline processors can be assembled in the channel. It is impossible for a channel to have only one ChannelHandler processor. There must be many. Since many ChannelHandlers work in a pipeline, there must be an order.

So the pipeline appeared, and the pipeline is equivalent to the container of the processor. When initializing the channel, install the channelHandler in the pipeline in order, and then the channelHandler can be executed in order.
insert image description here
In a Channel, there is only one ChannelPipeline. The pipeline is created when the Channel is created. ChannelPipeline contains a list of ChannelHanders, and all ChannelHandlers will be registered to ChannelPipeline.

5.8 ChannelHandlerContext

In Netty, the Handler processor is defined by us, as mentioned above, it is realized by integrating the inbound processor or the outbound processor. At this time, if we want to get the pipeline object or channel object in the Handler, how to get it.

So Netty designed this ChannelHandlerContext context object, you can get channels, pipelines and other objects, and you can read and write operations.
insert image description here
Through the class diagram, ChannelHandlerContext is an interface, and there are three implementation classes below.

In fact, ChannelHandlerContext is in the form of a linked list in the pipeline. Look at a piece of source code to understand:

//ChannelPipeline实现类DefaultChannelPipeline的构造器方法
protected DefaultChannelPipeline(Channel channel) {
    
    
    this.channel = ObjectUtil.checkNotNull(channel, "channel");
    succeededFuture = new SucceededChannelFuture(channel, null);
    voidPromise =  new VoidChannelPromise(channel, true);
    //设置头结点head,尾结点tail
    tail = new TailContext(this);
    head = new HeadContext(this);
    
    head.next = tail;
    tail.prev = head;
}

Let me use a picture to show it, it will be more clear:
insert image description here

5.9 EventLoopGroup

Let's take a look at the class diagram of EventLoopGroup:
insert image description here
it includes the commonly used implementation class NioEventLoopGroup. OioEventLoopGroup is also used in previous examples.

From the architecture diagram of Netty, we can know that the server needs two thread groups to work together, and the interface of this thread group is EventLoopGroup.

Each EventLoopGroup includes one or more EventLoops, and each EventLoop maintains a Selector instance.

5.9.1 Implementation Principle of Polling Mechanism

Let's take a look at the source code of DefaultEventExecutorChooserFactory:

private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;

@Override
public EventExecutor next() {
    
    
    //idx.getAndIncrement()相当于idx++,然后对任务长度取模
    return executors[idx.getAndIncrement() & executors.length - 1];
}

This code can confirm that the execution method is the polling mechanism. Next, debug and debug:
insert image description here
there is another judgment here. If the number of threads is not 2 to the Nth power, the modulo algorithm is used to realize it.

@Override
public EventExecutor next() {
    
    
    return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}

Refer to the Netty official website documentation: API documentation
is not easy to create, if you think it is useful, please like it.

Guess you like

Origin blog.csdn.net/zch981964/article/details/130631162