Java-网络编程-NIO模型(二)

NIO模型

Java网络编程模型中,从JDK1.4 之后提出了NIO模型。我们来描述一下NIO是如何处理多个客户端连接的。

NIO模型

在NIO模型中,服务器端可以仅创建一个线程,在这个线程中使用Java NIO提供的多路复用器(Selector)来为客户端的多个连接提供服务。

NIO模型原理

Selector内部维护着一个列表, 当一个客户端连接到来时,不再为每个连接单独创建一个线程,而是把该客户端连接注册到selector上,这样在某个时刻,selector上可能已经被注册了多个连接。然后开始检查该selector,如果该selector所维护的列表中有一个或多个连接有数据可读,该selector则返回数据已经就绪的连接列表。这样用一个线程便可轮询该列表,读出多个连接上的数据。

理解了NIO模型的原理之后,我们来看下用Java所提供的API如何来完成NIO服务端的开发,仍然沿用阻塞模型中的客户端和服务端 PING消息 发送流程。


image.png

Java NIO 服务器

NIOServer

public class NIOServer {

    private static final int port = 8000;

    public static void main(String[] args) {

        try {
            Selector serverSelector = Selector.open();

            ServerSocketChannel listenChannel = ServerSocketChannel.open();
            listenChannel.socket().bind(new InetSocketAddress(port));
            // (1) 配置为非阻塞模式
            listenChannel.configureBlocking(false);

            // (2) 监听新的客户端连接事件(ACCEPT)
            listenChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

            int counter = 0;
            while (true) {
                int num = 0;
                try {
                    System.out.println("select ... " + counter++);
                    num = serverSelector.select();
                } catch (Exception e) {
                    e.printStackTrace();
                    break;
                }

                if (num > 0) {
                    Set<SelectionKey> set = serverSelector.selectedKeys();
                    Iterator<SelectionKey> it = set.iterator();

                    while (it.hasNext()) {
                        SelectionKey key = it.next();
                        it.remove();
                        if (key.isAcceptable()) {
                            try {
                                // (3) 接收到新的客户端连接
                                SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();

                                // (4) 新的客户端连接配置为非阻塞模式
                                clientChannel.configureBlocking(false);

                                // (5) 监听新客户端连接的读事件
                                clientChannel.register(serverSelector, SelectionKey.OP_READ);

                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                        if (key.isReadable()) {
                            try {
                                // (6) 客户端有数据可读
                                SocketChannel clientChannel = (SocketChannel) key.channel();
                                ByteBuffer buf = ByteBuffer.allocate(4096);
                                int count = clientChannel.read(buf);
                                if (count > 0) {
                                    buf.flip();
                                    // (7) 输出接收到的数据
                                    System.out.println(Charset.defaultCharset().newDecoder().decode(buf).toString());
                                } else {
                                    key.cancel();
                                    clientChannel.close();
                                }
                            } catch (Exception e) {
                                key.cancel();
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  1. 服务端监听socket设置为非阻塞模式。
  2. 服务端监听socket,注册ACCEPT事件,来关注该事件,当有新的客户端连接到来时,会得到通知。
  3. 从多路复用器selector中获取所有的连接,如果该连接isAcceptable()为真,则表明监听socket上有新的客户端连接到来。
  4. 新的客户端连接仍然要配置为非阻塞模式。
  5. 为客户端连接注册OP_READ读事件,当该客户端socket有数据可读时,会得到通知。
  6. 该连接的socket有读事件发生,从通道中读取数据。
  7. 把该数据输出到终端。

服务端输出

首先运行NIO服务器,客户端仍然沿用之前的阻塞IO客户端IOClient.java,运行后,服务端输出结果如下:
Select ... 1
Sat Oct 26 13:20:56 CST 2019: PING
select ... 2
Sat Oct 26 13:20:58 CST 2019: PING
select ... 3
Sat Oct 26 13:21:00 CST 2019: PING
select ... 4
Sat Oct 26 13:21:02 CST 2019: PING
select ... 5
Sat Oct 26 13:21:04 CST 2019: PING
select ... 6
Sat Oct 26 13:21:06 CST 2019: PING

NIO编程思考

哇哦,我们用Java NIO模型实现了高效率的服务器,而且解决了之前阻塞模型BIO的问题,看起来好像不错。但你有没有发现,这里直接使用了原来的阻塞模型的客户端逻辑IOClient来与NIO服务器交互,而没有实现NIO模型的客户端。 是因为使用Java原生API来完成NIO模型,复杂度实在是有些高。

  1. 编程复杂。
  2. JDK底层的epoll实现, 存在导致CPU占用100%的Bug。
  3. 想完全依赖Java原生NIO API,来实现一个高效的NIO模型框架,需要有足够的耐心和底层基础知识,很难保证没有Bug。

那么问题来了, 如果既想使用NIO模型的高性能优势, 又不想处理Java原生NIO编程的复杂性,该怎么办呢? 于是来了个牛哄哄的家伙: Netty, 我们慢慢道来......

进入Netty之前

在进入更高层的NIO框架之前,先简单看一下计算机编程的整体框架和底层的基础知识,做到知其然而知其所以然,这样在编程时, 才知道各个逻辑单元在整个大厦中所处的位置。

整套Java API框架的实现包含了很多IO操作,包括各种文件IO和网络socket IO, 但这些Java API不能直接和外设交互,必须依赖操作系统提供的基础服务,于是乎,负责处理Java语言的虚拟机JVM在执行Java字节码的时候,需要与操作系统交互来实现IO操作, 而大部分操作系统都是由原生的宿主native语言(C语言)来开发实现,因此这之间的交互需要有一个叫jni (Java Native Interface)的通讯桥梁来打通。

NIO调用栈

整个调用栈的流程如上图,现在自底向上来看下各个部分。

  1. IO硬件。这里指各种外部输入输出设备,着重说明实现网络通讯的网卡设备。
  2. 操作系统。其实在操作系统和硬件设备之间有一层驱动(Driver)层,以Linux操作系统为例,驱动开发人员,可以遵循Linux的驱动框架,比如字符设备,块设备(网卡属于块设备), 并参考相关硬件设备厂商的规范手册,来开发驱动程序,并把驱动程序集成并入加载到操作系统中,和操作系统一起运行在权限最高的特权级别,来最终实现和IO硬件的通讯。
  3. epoll。这里就是NIO底层网络的核心实现机制,基于事件驱动机制,在操作系统内核层注册各种关心的事件,当这些事件就绪时,会向关注者发送通知。
系统调用 epoll_create
系统调用 epoll_ctl
系统调用 epoll_wait
  1. 如上三个epoll相关的系统调用,就是操作系统内核提供给我们实现高效NIO编程的制胜法宝。核心思想是,通过epoll_create来创建一个epoll内核对象,然后通过epoll_ctl可以动态注册事件,修改事件,删除事件,也就是说把所关心的事件都通过该系统调用交给内核来维护,操作系统内核内部会用一个高效的数据结构来维护该注册信息, 当网卡驱动有数据到来时,操作系统内核会收到通知,把这些处于就绪状态的事件转移到另一个就绪列表中。 当外部通过epoll_wait来获取就绪事件时,内核可直接把该就绪列表返回,不可谓不高效。

  2. 运行时库。操作系统提供的系统调用一般是该操作系统,最基础的,最关键的,必不可少的核心API,仅仅提供原子的基础能力,这样能够最大力度简化内核模块的设计与开发。但系统调用在用户的易用性上不一定能满足需求,于是运行时库担负起了这一职责,在linux系统上也就是glibc运行时库。该运行时库会对系统调用进行封装,提供更高层次的API。

  3. JNI。看了操作系统底层最关键的基础设施之后,这里就是Java语言和下层本地操作系统的基础通讯桥梁。

  4. NIO 框架。通过操作系统提供的基础NIO能力,Netty在这里完成了NIO类库的核心逻辑,和JVM携手联合在一起,在Java世界提供了一整套完善的NIO网络编程框架。

  5. 应用App。 这里才是上层的应用开发逻辑,包括实际的业务系统, 以及各种中间件系统,RPC框架等等都可以在这里实现。海深凭鱼跃,天高任鸟飞,凭借Netty提供的高效且方便的NIO框架,我们马上要开始后续的征程,奔跑吧,少年...... (见下一篇)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容