Netty应用之入门实例

一、Linux五大网络IO 模型

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

1.1. 阻塞I/O模型

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

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

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

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而已。

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. selectpollepoll区别

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

一个进程打开连接数IO 效率消息传递方式
select32 位机器 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无异。也就是说,在第二阶段内核复制数据到用户空间的过程中,进程同样是被阻塞的。

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调用到收到结果的过程,进程都是非阻塞的。

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模型为主。

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

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

3.1. 为啥不使用nio
  1. NIO的类库和API繁杂,使用麻烦,你需要熟练掌握SelectorServerSocketChannel
、SocketChannelByteBuffer

  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,业务开发人员不需要
再为NIOBUG而烦恼;
社区活跃,版本迭代周期短,发现的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,它用于对网络事件进行读写操作。一般来说只需要关注channelReadexceptionCaught

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/OClientHandler,里面有三个重要的方法,channelRead0channelActiveexceptionCaught

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

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

4.3. 结果

五、相关参考:

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

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

netty权威指南

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值