首页 > 其他分享 >Netty应用之入门实例

Netty应用之入门实例

时间:2022-12-19 14:02:16浏览次数:67  
标签:Netty 调用 入门 阻塞 缓冲区 实例 线程 内核 进程


一、​​Linux​​​五大网络​​IO​​ 模型

我们在学些​​netty​​​我们需要了解下​​linux​​​的​​IO​​​模型,我们的​​java​​​的​​IO​​模型也是在此基础上搭建的。

1.1. 阻塞​​I/O​​模型

常用的I/O模型就是阻塞I/O模型,缺省情形下,所有文件操
作都是阻塞的。我们在使用套接字接口是,在进程空间中调用​​recvform​​​,其系统调用直到数据包到达且被复制到应用进程的缓冲区或者发生错误才返回,期间一直会等待,进程从调用​​recvfrom​​开始到它返回的这段时间都是被阻塞的。

Netty应用之入门实例_客户端

1.1.1. 优点:
  1. 能够及时的返回数据,无延迟;
  2. 程序简单,进程挂起基本不会消耗​​CPU​​时间;
1.1.2. 缺点:
  1. ​I/O​​等待对性能影响较大;
  2. 每个连接需要独立的一个进程/线程处理,当并发请求量较大时为了维护程序,内存、线程和CPU上下文切换开销较大,因此较少在开发环境中使用。
1.2. 非阻塞​​I/O​​模型

​recvfrom​​​从应用层到内核的时候,如果该缓冲区没有数据的话,
就直接返回一个​​​EWOULDBLOCK​​错误,一般都对非阻塞I/O模型进行轮询检査这个状态,
看内核是不是有数据到来。

Netty应用之入门实例_客户端_02

1.2.1. 具体过程:

第二阶段(非阻塞):

  1. 进程向内核发起IO调用请求,内核接收到进程的I/O调用后准备处理并返回​​EWOULDBLOCK​​的信息给进程;此后每隔一段时间进程都会想内核发起询问是否已处理完,即轮询,此过程称为为忙等待。
  2. 内核收到进程的系统调用请求后,此时的数据包并未准备好,此时内核会给进程发送error信息,直到磁盘中的数据加载至内核缓冲区。

第二阶段(阻塞):

  1. 内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段,进程阻塞),直到数据复制完成。
  2. 内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理。
1.2.2. 优点:

进程在等待当前任务完成时,可以同时执行其他任务;进程不会被阻塞在内核等待数据过程,每次发起的I/O请求会立即返回,具有较好的实时性;

1.2. 3. 缺点:

不断的轮询将占用大量的CPU时间,系统资源利用率大打折扣,影响性能,整体数据的吞吐量下降;该模型不适用web服务器;

1.3. ​​I/O​​复用模型

​I/O​​​复用模型也叫作事件驱动​​I/O​​​模型。这个模型中,每一个网络连接,都是非阻塞的;进程会调用​​select()​​​、​​poll()​​​、​​epoll()​​​发起系统调用请求,​​select()​​​、​​poll()​​​、​​epoll()​​​相当于内核代理,进程所有请求都会先请求这几个函数中的某一个;这个时候一个进程可以同时处理多个网络连接​​I/O​​​,这个几个函数会不断轮询负责的所有的​​socket​​​,当某一个​​socket​​有数据报准备好了,就会返回可读信号通知给进程。

用户进程调用​​select/poll/epoll​​​后,进程实际上是被阻塞的,同时,内核会监视所有​​select/poll/epoll​​​所负责的​​socket​​​,当其中任意一个数据准备好了,就会通知进程。只不过进程是阻塞在​​select/poll/epoll​​​之上,而不是被内核准备数据过程中阻塞。此时,进程再发起​​recvfrom​​系统调用,将数据中内核缓冲区拷贝到内核进程,这个过程是阻塞的。

虽然​​select/poll/epoll​​​可以使得进程看起来是非阻塞的,因为进程可以处理多个连接,但是最多只有1024个网络连接的​​I/O​​​;本质上进程还是阻塞的,只不过它可以处理更多的网络连接的​​I/O​​而已。

Netty应用之入门实例_数据_03

1.3.1. 从图上我们可以看到:

第一阶段(阻塞在​​select/poll​​之上):

  1. 进程向内核发起​​select/poll/epoll​​的系统调用,​​select​​将该调用通知内核开始准备数据,而内核不会返回任何通知消息给进程,但进程可以继续处理更多的网络连接​​I/O​​;
  2. 内核收到进程的系统调用请求后,此时的数据包并未准备好,此时内核亦不会给进程发送任何消息,直到磁盘中的数据加载至内核缓冲区;而后通过​​select()/poll()​​函数将​​socket​​的可读条件返回给进程

第二阶段(阻塞):

  1. 进程在收到​​SIGIO​​信号程序之后,进程向内核发起系统调用(​​recvfrom​​);
  2. 内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行​​IO​​过程的阶段),直到数据复制完成。
  3. 内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理;处理完成后,此时的进程解除不可中断睡眠态,执行下一个​​I/O​​操作。
1.3.2. 优点:
  1. I/O复用技术的优势在于,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,所以它也是很大程度上减少了资源占用。
  2. 另外I/O复用技术还可以同时监听不同协议的套接字
1.3.3. 缺点:
  1. 在只处理连接数较小的场合,使用select的服务器不一定比多线程+阻塞I/O模型效率高,可能延迟更大,因为单个连接处理需要2次系统调用,占用时间会有增加。
1.3.4. ​​select​​​、​​poll​​​和​​epoll​​区别

Linux 提供了​​select​​​、​​poll​​​和​​epoll​​帮助我们。一个线程可以对多个 IO 端口进行监听,当 socket 有读写事件时分发到具体的线程进行处理。

一个进程打开连接数

IO 效率

消息传递方式

​select​

32 位机器 1024 个,64 位 2048 个

IO 效率低

内核需要将消息传递到用户空间,都需要内核拷贝动作

​poll​

无限制,原因基于链表存储

IO 效率低

内核需要将消息传递到用户空间,都需要内核拷贝动作

​epoll​

有上限,但很大,2G 内存 20W 左右

只有活跃的 socket 才调用 callback,IO 效率高

通过内核与用户空间共享一块内存来实现

1.4. 信号量驱动​​I/O​​模型

信号驱动式​​I/O​​是指进程预先告知内核,使得某个文件描述符上发生了变化时,内核使用信号通知该进程。

在信号驱动式​​I/O​​​模型,进程使用​​socket​​​进行信号驱动​​I/O​​​,并建立一个​​SIGIO​​​信号处理函数,当进程通过该信号处理函数向内核发起​​I/O​​​调用时,内核并没有准备好数据报,而是返回一个信号给进程,此时进程可以继续发起其他​​I/O​​​调用。也就是说,在第一阶段内核准备数据的过程中,进程并不会被阻塞,会继续执行。当数据报准备好之后,内核会递交​​SIGIO​​​信号,通知用户空间的信号处理程序,数据已准备好;此时进程会发起​​recvfrom​​​的系统调用,这一个阶段与阻塞式​​I/O​​无异。也就是说,在第二阶段内核复制数据到用户空间的过程中,进程同样是被阻塞的。

Netty应用之入门实例_数据_04

1.4.1. 整体过程

第一阶段(非阻塞):

  1. 进程使用​​socket​​进行信号驱动​​I/O​​,建立​​SIGIO​​信号处理函数,向内核发起系统调用,内核在未准备好数据报的情况下返回一个信号给进程,此时进程可以继续做其他事情;
  2. 内核将磁盘中的数据加载至内核缓冲区完成后,会递交​​SIGIO​​信号给用户空间的信号处理程序;

第二阶段(阻塞):

  1. 进程在收到​​SIGIO​​信号程序之后,进程向内核发起系统调用(​​recvfrom​​);
  2. 内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行​​I/O​​过程的阶段),直到数据复制完成。
  3. 内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理;处理完成后,此时的进程解除不可中断睡眠态,执行下一个​​I/O​​操作。
1.4.2. 优点
  1. 很明显,我们的线程并没有在等待数据时被阻塞,可以提高资源的利用率
1.4.3. 缺点
  1. 信号I/O在大量IO操作时可能会因为信号队列溢出导致没法通知——这个是一个非常严重的问题。
1.5. 异步​​I/O​​模型

我们在上面了解的4种I/O模型都可以划分为同步​​I/O​​​方法,我们可以注意到,在数据从内核缓冲区复制到用户缓冲区时,都需要进程显示调用​​recvfrom​​​,并且这个复制过程是阻塞的。
也就是说真正​​​I/O​​​过程(这里的​​I/O​​有点狭义,指的是内核缓冲区到用户缓冲区)是同步阻塞的,不同的是各个I/O模型在数据报准备好之前的动作不一样。

异步I/O可以说是在信号驱动式​​I/O​​模型上改进而来。

在异步​​I/O​​​模型中,进程会向内核请求​​air_read​​​(异步读)的系统调用操作,会把套接字描述符、缓冲区指针、缓冲区大小和文件偏移一起发给内核,当内核收到后会返回“已收到”的消息给进程,此时进程可以继续处理其他​​I/O​​​任务。也就是说,在第一阶段内核准备数据的过程中,进程并不会被阻塞,会继续执行。第二阶段,当数据报准备好之后,内核会负责将数据报复制到用户进程缓冲区,这个过程也是由内核完成,进程不会被阻塞。复制完成后,内核向进程递交​​aio_read​​的指定信号,进程在收到信号后进行处理并处理数据报向外发送。

在进程发起I/O调用到收到结果的过程,进程都是非阻塞的。

Netty应用之入门实例_客户端_05

1.5.1. 整体过程

第一阶段(非阻塞):

  1. 进程向内核请求​​air_read​​(异步读)的系统调用操作,会把套接字描述符、缓冲区指针、缓冲区大小和文件偏移一起发给内核,当内核收到后会返回“已收到”的消息给进程
  2. 内核将磁盘中的数据加载至内核缓冲区,直到数据报准备好;

第二阶段(非阻塞):

  1. 内核开始复制数据,将准备好的数据报复制到进程内存空间,知道数据报复制完成
  2. 内核向进程递交​​aio_read​​的返回指令信号,通知进程数据已复制到进程内存中;
1.5.2. 优点:
  1. 能充分利用​​DMA​​​的特性,将​​I/O​​操作与计算重叠,提高性能、资源利用率与并发能力
1.5.3 缺点:
  1. 在程序的实现上比较困难;
  2. 要实现真正的异步​​I/O​​,操作系统需要做大量的工作。目前 ​​Windows​​下通过 ​​IOCP​​ 实现了真正的异步 ​​I/O​​。而在​​Linux​​系统下,​​Linux 2.6​​才引入,目前 ​​AIO​​并不完善,因此在​​Linux​​下实现高并发网络编程时都是以 复用式​​I/O​​模型为主。

二、​​Java​​​的​​I/O​​模型

2.1. 我们在​​Java​​​中使用的是​​BIO​​​、​​NIO​​​ 和​​AIO​​三种:
  • ​BIO​​:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程并处理,如果这个连接不做任何事情会造成不必要的开销,当然可以通过线程池机制改善。
  • ​NIO​​:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。
  • ​AIO​​:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理
2.2. 三种模型的使用场景:
  • ​BIO​​:适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
  • ​NIO​​​:适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,​​JDK1.4​​开始支持。
  • ​AIO​​​:使用于连接数目多且连接比较长(重操作)的架构,比如文件服务器,充分调用OS参与并发操作,编程比较复杂,​​JDK7​​开始支持。

三、从​​NIO​​​到​​Netty​

一般可以根
据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻
塞I/O以降低编程复杂度,但是对于高负载、高并发的网络应用,需要使用​​NIO​​的非阻塞
模式进行开发。

3.1. 为啥不使用​​nio​
  1. ​NIO​​​的类库和​​API​​繁杂,使用麻烦,你需要熟练掌握​​Selector​​、​​ServerSocketChannel​​
、​​SocketChannel​​、​​ByteBuffer​​ 等
  2. 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为​​NIO​​编程涉
及到​​Reactor​​模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的​​NIO​​程序。
  3. 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、
半包读写、失败缓存、网络拥塞和异常码流的处理等问题,​​NIO​​编程的特点是功能开发相
对容易,但是可靠性能力补齐的工作量和难度都非常大。
  4. ​JDK NIO​​​的``BUG​​,例如臭名昭著的​​epoll bug​​,它会导致​​Selector​​空轮询,最终导
致CPU 100%。官方声称在​​JDKL6​​版本的​​update 18​​修复了该问题,但是直到​​JDK1.7`版本
该问题仍旧存在,只不过该BUG发生概率降低了一些而已,它并没有被根本解决。
3.2. 选择​​netty​​的原因:
  1. ​API​​使用简单,开发门槛低;
  2. 功能强大,预置了多种编解码功能,支持多种主流协议
  3. 定制能力强,可以通过​​ChanneJHandler​​对通信框架进行灵活地扩展
  4. 性能高,通过与其他业界主流的​​NIO​​框架对比,Netty的综合性能最优
  5. 成熟、稳定,Netty修复了已经发现的所有​​JDK NIO BUG​​​,业务开发人员不需要
再为​​​NIO​​​的​​BUG​​而烦恼;
社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功
能会加入;
  6. 经历了大规模的商业应用考验,质量得到验证,在互联网、大数据、网络游戏、
企业应用、电信软件等众多行业得到成功商用,证明了它已经完全能够满足不同
行业的商业应用了。

四、​​netty​​​入门​​demo​

4.1. 服务器端
public class DemoServer {

private final int port;

public DemoServer(int port) {
this.port = port;
}

public void start() throws InterruptedException {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {

@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ServerHandler());
}
});
System.out.println(">>> 启动服务器成功......");
ChannelFuture future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();

} finally {
worker.shutdownGracefully().sync();
boss.shutdownGracefully().sync();
}
}

public static void main(String[] args) throws InterruptedException {
new DemoServer(9999).start();
}
}
上面关键位置

​NioEventLoopGroup​​​ 是处理I/O操作的线程池,其中​​boss​​​主要用于处理客户端连接,​​worker​​用于处理客户端的数据读写工作。

​ServerBootstrap​​​是启动​​NIO​​服务端的辅助启动类,目的是为了降低服务端的开发复杂度。

​group​​​会将两个​​NIO​​​线程组当做入参传递到​​ServerBootstrap​​中。

​channel​​​指定所使用的​​NIO​​​传输 ​​Channel​​。

​ServerHandler​​​用户处理​​I/O​​事件处理,例如日志、编码和解码等。

​bind​​​用于绑定监听端口,然后调用同步阻塞方法等待绑定完成,完成之后会返回一个​​ChannelFuture​​对象,主要用户异步通知回调。

​closeFuture​​等待服务端链路关闭之后主线程才退出。

​shutdownGracefully​​将会释放跟其关联的资源。

public class ServerHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("服务器接收的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
ctx.write(byteBuf);
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}

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

​ServerHandler​​​继承​​ChannelInboundHandlerAdapter​​​,它用于对网络事件进行读写操作。一般来说只需要关注​​channelRead​​​和​​exceptionCaught​​。

​channelRead​​:接受消息,做处理

​channelReadComplete​​:channelRead()执行完成后,关闭channel连接。

​exceptionCaught​​:发生异常之后,打印堆栈,关闭通道。

4.2. 客户端
public class DemoClient {

private final String host;

private final int post;

public DemoClient(String host, int post) {
this.host = host;
this.post = post;
}

public void start() throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup(); // 1 第一步
try {
Bootstrap bootstrap = new Bootstrap(); // 2 第二部
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() { // 第三步

@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ClientHandler());
}
});
ChannelFuture future = bootstrap.connect(host, post).sync(); // 第四步
future.channel().closeFuture().sync(); // 第五步
} finally {
group.shutdownGracefully().sync(); // 第六步
}
}

public static void main(String[] args) throws InterruptedException {
new DemoClient("127.0.0.1", 9999).start();
}
}
客户端创建过程:

第一步,首先创建客户端处理I/O读写的​​NioEventLoop
Group​​​线程组,然后继续创建客户端辅助启动类​​Bootstrap​​,随后需要对其进行配置。

第二部,将​​Channel​​​需要设置为​​NioSocketChanneL​​​然后为其添加​​handler​

第三步,此处
创建匿名内部类,实现​​initChannel​​​方法,其作用是当创建​​NioSocketChannel​​​成功之后,在初始化它的时候将它的​​ChannelHandler​​​设置到​​ChannelPipeline​​​中,用于处理
网络​​​I/O​​事件。

第四步,客户端启动辅助类设置完成之后,调用​​connect​​方法发起异步连接,然后调用同步方法等待连接成功。

第五步,当客户端连接关闭之后,客户端主函数退出,在退出之前,释放​​NIO​​线程组的相关资源。

public class ClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
System.out.println("客户端收到消息:" + byteBuf.toString());
}

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8));
}

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

具体处理​​I/O​​​的​​ClientHandler​​​,里面有三个重要的方法,​​channelRead0​​​、​​channelActive​​​和​​exceptionCaught​​。

当客户端
和服务端​​TCP​​​链路建立成功之后,​​Netty​​​的​​NIO​​​线程会调用​​channelActive​​​方法,发送​​Hello world​​​指令给服务端,调用​​ChannelHandlerContext​​​的​​writeAndFlush​​方法将请求消息发送给服务器端。

接着当服务器端返回应答信息的时候,​​channelRead0​​​将被调用。如果发生异常,​​exceptionCaught​​将被调用。

4.3. 结果

Netty应用之入门实例_数据_06

五、相关参考:

Linux系统I/O模型详解 ​​https://blog.51cto.com/ccschan/2357207​

Linux下的I/O模型以及各自的优缺点 ​​https://www.linuxidc.com/Linux/2017-09/146682.htm​

​netty​​权威指南


标签:Netty,调用,入门,阻塞,缓冲区,实例,线程,内核,进程
From: https://blog.51cto.com/luckyqilin/5952259

相关文章

  • Elasticsearch 入门实战(6)--索引生命周期管理
    索引生命周期管理(Indexcyclemanagement:ILM) 是在 Elasticsearch 6.7版正式推出的一项功能,它是Elasticsearch的一部分,主要用来帮助管理索引。1、简介如果你要处......
  • 【实践】高性能PHP应用容器workerman快速入门
    前言workerman--极简、稳定、高性能、分布式workerman是什么workerman是一款开源高性能PHP应用容器,它大大突破了传统PHP应用范围,被广泛的用于互联网、即时通讯、APP开发、硬......
  • 产品经理快速入门指南之常见问题篇
    产品经理作为近几年互联网最炙手可热的岗位,可谓是赚足了眼球。其实呢,产品经理是很难定义的一个角色,如果非要一句话定义,那么产品经理是为终端用户服务,负责产品整个生命周期的......
  • Spring boot 入门 ---(一)
    环境JavaSDKv1.6或更高版本。这里使用maven作为构建工具下面是一个典型的pom.xml文件:配置pom.xml文件<?xmlversion="1.0"encoding="utf-8"?><projectxmlns="http://mav......
  • zookeeper入门
    1.ZooKeeper是什么?ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交......
  • 基于 Dubbo Admin 临时踢除问题服务实例
    Dubbo提供临时踢除问题服务实例的服务治理能力,可以在无需重启应用的情况下,临时踢除问题服务实例。Dubbo可以通过XML配置,注解配置,动态配置实现临时踢除问题服务实例,这里主......
  • Prometheus Metrics设计的最佳实践和应用实例
    使用Promethues实现应用监控的一些实践在这篇文章中我们介绍了如何利用Prometheus监控应用。在后续的工作中随着监控的深入,我们结合自己的经验和官方文档总结了一些Met......
  • 探究:普通人都是怎么入门编程
    目录前景提要研究思考1.术语才是编程入门的领路人2.马上着手胜于做个目标,写个计划3.动起手来,比用眼睛看更有作用4.开发语言林立,你的武道何去何从总结前景提要很多人......
  • webservice简介&CXF入门
    WebService它是一种跨编程语言和跨操作系统平台的远程调用技术即跨平台远程调用技术. 面向服务架构规范及三要素 1)           SOAP协议:webservice的传输协议,......
  • Linux入门到精通
    时间同步chrony1、ntp服务器搭建yum-yinstallchronyvi/etc/chrony.conf....#server用户客户端指向,上层NTP服务器server0.centos.pool.ntp.orgiburstserver......