浅谈netty

要理解netty,我们需要先了解I/O Models和JAVA NIO,还有观察者模式、多Reactors线程模型等等这些内容。

I/O Models

在这里我们先要回顾一些操作系统的IO相关基础知识:

  • 用户空间与内核空间:
    现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
  • 系统调用:
    Linux/Unix内核中设置了一组用于实现各种系统功能的子程序,称为系统调用。用户可以通过系统调用命令在自己的应用程序中调用它们。从某种角度来看,系统调用和普通的函数调用非常相似。区别仅仅在于,系统调用由操作系统核心提供,运行于核心态;而普通的函数调用由函数库或用户自己提供,运行于用户态。
  • 进程切换:
    1.为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
    2.消耗CPU资源。
  • 进程的阻塞:
    1.正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。
    2.当进程进入阻塞状态,不消耗CPU资源。
  • 文件描述符:
    1.文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
    2.文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
  • 缓存 I/O:
    1.缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
    2.缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

当一个输入(input)操作发生时,这里会经历两个不同的阶段:
1.等待数据就绪。
2.将数据从内核的缓冲区拷贝到进程中。

下面是5种IO模型
1.blocking I/O
2.nonblocking I/O
3.I/O multiplexing (select and poll)
4.signal driven I/O (SIGIO)
5.asynchronous I/O (the POSIX aio_functions)

Blocking I/O Model
image.png

整个读取IO的数据是同步、阻塞的。

Nonblocking I/O Model
image.png

当一个应用程序像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询(polling),应用程序持续轮询内核,以查看某个操作是否就绪,这么做往往耗费大量CPU时间。通常是在专门提供某一种功能的系统中才有

I/O Multiplexing Model
image.png
  • 在IO复用中,我们调用select或者poll 然后在这两个系统调用中的一个中阻塞,而不是阻塞在真实的I/O系统调用上。
  • 优势:跟阻塞I/O模型比较使用select我们可以等待多个描述符就绪,也就是一个线程可以处理多个描述符。
signal driven I/O (SIGIO)
image.png
  • 我们也可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。
  • 优势:等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知(既可以是数据已准备好被处理,也可以是数据报已准备好被读取。)
asynchronous I/O (the POSIX aio_functions)

告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。通过状态、通知和回调来通知调用者的输入输出操作。

image.png
  • 优势:
    我们调用aio_read函数,给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,病告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等待I/O完成期间,我们的进程不被阻塞。
各种I/O模型的比较
image.png
  • 前4中模型的主要区别在于第一阶段,因为它们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。
  • 同步I/O操作导致请求进程阻塞,直到I/O操作完成,异步I/O操作不导致请求进程阻塞。前四种都是同步I/O模型,因为其中的I/O操作recvfrom讲阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配。

随着linux内核的不断发展,IO也在不断发展,所以后面有了IO多路复用模型。IO 多路复用是通过linux内核的select、poll、epoll这些来完成的。

select函数

该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
例如:我们可以调用select,告知内核仅在下列情况发生时才返回:

  • 集合{1,4,5}中的任何描述符准备好读;
  • 集合{2,7}中任何描述符准备好写;
  • 集合{1,4}中的任何描述符有异常条件待处理;
  • 已经历了10.2秒
    下面是select函数的简单结构
#include 
#include 

//返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
int select(int maxfdp1,fd_set *readset,fd_set *writeset, fd_set *exceptest,const struct timeval *timeout);

struct timeval{
    long tv_sec;       /* seconds */
    long tv_used;    /* microseconds */
}

select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求
pselect : 能够处理信号阻塞并提供了更高时间分辨率的select的增强版本

poll函数
#include 

//返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
int poll (struct pollfd *fdarray, unsigned long nfds, int timeout);

//一个pollfd结构体表示一个被监视的文件描述符
struct pollfd {
int fd; /* descriptor to check */
short events; /* events of interest on fd */
short revents; /* events that occurred on fd */
};

select机制的问题
1.每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
2.同时每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
3.为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024) 【poll用数组结构体解决了大小限制问题】

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一些缺点:

  • 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
  • poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll函数

epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

int epoll_create(int size);  // epoll_create 函数创建一个epoll句柄
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 函数注册要监听的事件类型
//  epoll_wait 函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
struct epoll_event {    
    __uint32_t events;  /* Epoll events */    
    epoll_data_t data;  /* User data variable */};

typedef union epoll_data {    
   void *ptr;   
    int fd;    
    __uint32_t u32;   
    __uint64_t u64;
} epoll_data_t;

epoll优点:
1.没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2.效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

  1. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

对比:


image.png
总结

综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。

1.表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

2.select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。

JAVA NIO

Java NIO提供了与标准IO不同的IO工作方式:

  • Channels and Buffers(通道和缓冲区):
    标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。

  • 非阻塞IO(Non-blocking IO):
    Java NIO可以让执行非阻塞IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。

  • 选择器(Selectors):
    Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

java nio 的几个重要组件:


image.png

Channels and Buffers

基本上,所有的 IO 在NIO 中都从一个Channel 开始。Channel 有点象流。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。


image.png

常见的几种channel:

  • FileChannel
  • DatagramChannel UDP连接
  • SocketChannel 客户端socket连接
  • ServerSocketChannel 服务端socket连接

常见的几种Buffer:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
  • Mappedyteuffer 表示内存映射文件 java nio 提供了这个在某种场景下极大提升效率
selector

Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。


image.png
             A Thread uses a Selector to handle 3 Channel's 
IO VS NIO
image.png

netty原理

Netty是一个事件驱动、异步IO的网络框架。高性能,吞吐量更高,延迟更低、高性能之处主要来自于其I/O 模型和线程处理模型(Reactor),前者决定如何收发数据,后者决定如何处理数据。


image.png
Reactor模式
image.png

Reactor模式(反应器模式)是一种处理一个或多个客户端并发交付服务请求的事件设计模式。当请求抵达后,服务处理程序使用I/O多路复用策略,然后同步地派发这些请求至相关的请求处理程序。

核心组件交互图如下:

image.png

  • Handle(句柄或描述符,在Windows下称为句柄,在Linux下称为描述符):本质上表示一种资源(比如说文件描述符,或是针对网络编程中的socket描述符),是由操作系统提供的;该资源用于表示一个个的事件,事件既可以来自于外部,也可以来自于内部。
  • Synchronous Event Demultiplexer(同步事件分离器):它本身是一个系统调用,用于等待事件的发生(事件可能是一个,也可能是多个)。调用方在调用它的时候会被阻塞,一直阻塞到同步事件分离器上有事件产生为止。对于Linux来说,同步事件分离器指的就是常用的I/O多路复用机制,比如说select、poll、epoll等。在Java NIO领域中,同步事件分离器对应的组件就是Selector;对应的阻塞方法就是select方法。
  • Event Handler(事件处理器):本身由多个回调方法构成,这些回调方法构成了与应用相关的对于某个事件的反馈机制。在Java NIO领域中并没有提供事件处理器机制让我们调用或去进行回调,是由我们自己编写代码完成的。Netty相比于Java NIO来说,在事件处理器这个角色上进行了一个升级,它为我们开发者提供了大量的回调方法,供我们在特定事件产生时实现相应的回调方法进行业务逻辑的处理,即ChannelHandler。ChannelHandler中的方法对应的都是一个个事件的回调。
  • Initiation Dispatcher(初始分发器):际上就是Reactor角色。它本身定义了一些规范,这些规范用于控制事件的调度方式,同时又提供了应用进行事件处理器的注册、删除等设施。它本身是整个事件处理器的核心所在,Initiation Dispatcher会通过Synchronous Event Demultiplexer来等待事件的发生。一旦事件发生,Initiation Dispatcher首先会分离出每一个事件,然后调用事件处理器,最后调用相关的回调方法来处理这些事件。Netty中ChannelHandler里的一个个回调方法都是由bossGroup或workGroup中的某个EventLoop来调用的。
Basic Reactor Design
image.png
NIO实现Reactor
image.png

image.png
image.png

image.png

image.png

上面是用java nio实现基本Reactor模式,需要自己写很多代码。

Worker Thread Pools 版 Reactor模式
image.png
多Reactor模式
image.png

netty就是使用的就是多Reactor模式:

image.png

Netty的异步处理:

image.png

常见操作:
image.png

netty功能特性
image.png
Netty核心组件
image.png
ChannelPipeline处理入站事件和出站操作
image.png

image.png

image.png
Netty Reactor 工作架构图
image.png

image.png

image.png

下面有一个基于netty的简单的IM demo,可以简单了解netty的编程方法和思想:
Server:

package com.yuanjia.im.netty.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.net.InetSocketAddress;

/**
 * Created by bruce on 2019/6/10.
 */
public class Server {
    private int port;

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

    public void start() {

        // netty服务端ServerBootstrap启动的时候,默认有两个eventloop分别是bossGroup和 workGroup

        EventLoopGroup boosGroup = new NioEventLoopGroup(1);   // bossGroup
        EventLoopGroup workerGroup = new NioEventLoopGroup();  // workGroup
        try {
            ServerBootstrap sbs = new ServerBootstrap().group(boosGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    .childHandler(new ChannelInitializer() {
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast("decoder", new StringDecoder());
                            ch.pipeline().addLast("encoder", new StringEncoder());
                            //ch.pipeline().addLast(new DiscardInboundHandler());
                            ch.pipeline().addLast(new ServerHandler());
                        };
                    }).option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture future = sbs.bind(port).sync();
            System.out.println("Server start listen at " + port);
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            boosGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8090;
        }
        new Server(port).start();
    }
}

Client:

package com.yuanjia.im.netty.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;

/**
 * Created by bruce on 2019/6/10.
 */
public class Client {

    //server 's ip 这里需要用户根据自己server的ip来做修改,例如我这里是10.1.132.194
    private static final String HOST = System.getProperty("host", "10.1.132.194");
    //port 8090
    private static final int PORT = Integer.parseInt(System.getProperty("port", "8090"));

    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer(){
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast("decoder", new StringDecoder());
                            p.addLast("encoder", new StringEncoder());
                            p.addLast(new ClientHandler());
                        }
                    });
            ChannelFuture future = b.connect(HOST, PORT).sync();
            //控制台输入消息给服务端让服务端转给给另外一个客户端
            //消息如:  认识你真高兴我的小伙伴@10.1.8.30
            //消息就转发给了10.1.8.30
            Scanner sc = new Scanner(System.in);
            while(sc.hasNext()){
                String message = sc.nextLine();
                future.channel().writeAndFlush(message);
            }
            future.channel().closeFuture().sync();
        } finally {
            group.spliterator();
        }
    }

}

ServerHandler:

package com.yuanjia.im.netty.server;

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.util.internal.PlatformDependent;

import java.util.concurrent.ConcurrentMap;

/**
 * Created by bruce on 2019/6/10.
 */
@ChannelHandler.Sharable
public class ServerHandler implements ChannelInboundHandler {

    //存放客户端和服务端之间的连接
    private static ConcurrentMap channelConcurrentMap = PlatformDependent.newConcurrentHashMap();


    @Override
    public void channelRegistered(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void channelUnregistered(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void channelActive(ChannelHandlerContext channelHandlerContext) throws Exception {
        //获取客户端的ip
        String hostString = ((SocketChannel)channelHandlerContext.channel()).remoteAddress().getHostString();
        System.out.println(hostString + " online");
        //将客户端和服务端之间的连接存放在concurrentHashMap中
        channelConcurrentMap.put(hostString,channelHandlerContext);
    }

    @Override
    public void channelInactive(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void channelRead(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
        System.out.println("ServerHandler channelRead....");
        //客户端通过Terminal连接后的输入格式为  message@ip ,这个消息接收者ip会收到message消息
        //例如:   你最近还好吗,[email protected]
        String messageString = o.toString();
        String[] messages = messageString.split("@");
        String message = messages[0];
        String targetHost = messages[1];
        System.out.println(channelHandlerContext.channel().remoteAddress()+"->Server :"+o.toString());
        ChannelHandlerContext targetChannelHandlerContext = channelConcurrentMap.get(targetHost);
        targetChannelHandlerContext.write(channelHandlerContext.channel().remoteAddress() + " say : " + message);
        targetChannelHandlerContext.flush();
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void userEventTriggered(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {

    }

    @Override
    public void channelWritabilityChanged(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void handlerAdded(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void handlerRemoved(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable throwable) throws Exception {

    }
}

ClientHandler

package com.yuanjia.im.netty.client;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * Created by bruce on 2019/6/10.
 */
public class ClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Client01Handler Active");
        //ctx.fireChannelActive();  // 若把这一句注释掉将无法将event传递给下一个ClientHandler
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println(msg);
    }

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

代码地址:https://github.com/bruceChi/nettyIM
参考资料:
1、http://tutorials.jenkov.com/java-nio
2、 UNIX Network Programming
3、 https://www.jianshu.com/p/63a006e5e22d
4、http://tutorials.jenkov.com/netty
5、http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf
6、 https://www.cnblogs.com/winner-0715/p/8733787.html
7、 http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf

你可能感兴趣的:(浅谈netty)