Netty系列(一):IO与NIO

介绍
这篇文章来介绍下IO与NIO的区别总结,相信好多人都碰到过IO,也听过NIO,不过对于NIO不太熟识,所有抽空了补录了一些,来记录自己的学习之路。
一、IO编程
来描述下我们的场景,客户端每隔一段时间发送一个"你好"给服务端,服务端收到并打印出来。

客户端

package com.guo.io;

import java.io.IOException;
import java.net.Socket;
import java.util.Date;

public class IOClient {

    public static void main(String[] args) {
        new Thread(()->{
            try {
                Socket socket = new Socket("127.0.0.1", 8000);
                while (true) {
                    try {
                        socket.getOutputStream().write((new Date() + ": 你好Netty").getBytes());
                        Thread.sleep(3000);
                    } catch (Exception e) {
                    }
                }
            } catch (IOException e) {
            }
        }).start();
    }
}

客户端的代码相对简单,连接上服务端 8000 端口之后,每隔 3 秒,我们向服务端写一个带有时间戳的 “你好Netty”。

服务端

package com.guo.io;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class IOServer {

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8000);
        // (1) 接收新连接线程
        new Thread(()->{
            while(true){
                try {
                    // (1) 阻塞方法获取新的连接
                    Socket socket = serverSocket.accept();
                    // (2) 每一个新的连接都创建一个线程,负责读取数据
                    new Thread(()->{
                        try {
                            int len;
                            byte[] data = new byte[1024];
                            InputStream inputStream = socket.getInputStream();
                            // (3) 按字节流方式读取数据
                            while((len = inputStream.read(data)) != -1){
                                System.out.println(new String(data, 0, len));
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }).start();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

Server 端首先创建了一个serverSocket来监听 8000 端口,然后创建一个线程,线程里面不断调用阻塞方法 serversocket.accept();获取新的连接,见(1),当获取到新的连接之后,给每条连接创建一个新的线程,这个线程负责从该连接中读取数据,见(2),然后读取数据是以字节流的方式,见(3)。

简单一个IO,其中应用了匿名类函数,1.8JDK新的特性lambda表达式。我们看下结果:
在这里插入图片描述
上面的代码中:我们可以看到每次创建成功后都会有一个线程来维护,每个线程包含一个while的死循环。那么 1w 个连接对应 1w 个线程,继而 1w 个 while 死循环,这就带来如下几个问题:
1.线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态时非常严重的资源浪费,操作系统耗费不起。
2.线程切换效率低下:单机cpu核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。
3.IO编程中我们看到数据读写以字节流为单位。
在这里插入图片描述
这就引出我们NIO的模型,NIO怎么来解决这个问题呢?
NIO编程模型中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接所有的读写都有这个线程来负责。

再来看下理论知识:

IO模型中一个连接来了会创建一个线程,对应一个死循环,死循环目的就是不断检测这条连接上是否有数据可读。大多数情况下,1W个连接中只有少量连接有数据可读,因此很多个循环都浪费掉了。
NIO模型中它把多while死循环变成一个死循环,这个死循环有一个线程控制。NIO模型中selector的作用,一条连接来了以后,现在不创建一个while死循环去监听是否有数据可读,而是直接把这条连接注册到selector上,然后,通过检查这个selector,就可以批量检测出有数据可读的连接,进而读取数据。
举个例子:
一家幼儿园有100个小朋友,由于小朋友太小,以至于上厕所需要老师来询问。
IO模型:每个小朋友配备一个老师,隔段时间老师来询问,每个小朋友上厕所都需要老师领着去。(一个连接对应一个线程)
NIO模型:所有的小朋友配置同一个老师,老师来询问,把每一时刻需要上厕所的,领着去。(所有连接对应一个线程,然后批量轮训)
NIO服务端

package com.guo.io;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {

    public static void main(String[] args) throws IOException {
        Selector serverSelector = Selector.open();
        Selector clientSelector = Selector.open();

        new Thread(() -> {
            try {
                // 对应IO编程中服务端启动
                ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                listenerChannel.socket().bind(new InetSocketAddress(8000));
                listenerChannel.configureBlocking(false);
                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

                while (true) {
                    // 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
                    if (serverSelector.select(1) > 0) {
                        Set<SelectionKey> set = serverSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isAcceptable()) {
                                try {
                                    // (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                    clientChannel.configureBlocking(false);
                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);
                                } finally {
                                    keyIterator.remove();
                                }
                            }

                        }
                    }
                }
            } catch (IOException ignored) {
            }
        }).start();

        new Thread(() -> {
            try {
                while (true) {
                    // (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
                    if (clientSelector.select(1) > 0) {
                        Set<SelectionKey> set = clientSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isReadable()) {
                                try {
                                    SocketChannel clientChannel = (SocketChannel) key.channel();
                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                    // (3) 面向 Buffer
                                    clientChannel.read(byteBuffer);
                                    byteBuffer.flip();
                                    System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
                                            .toString());
                                } finally {
                                    keyIterator.remove();
                                    key.interestOps(SelectionKey.OP_READ);
                                }
                            }
                        }
                    }
                }
            } catch (IOException ignored) {
            }
        }).start();
    }
}

NIO核心思想:
1.NIO模型通常会有两个线程,每个线程都绑定一个轮训器selector,例子中serverSelector负责轮训是否有新连接,clientSelector负责是否有数据可读。
2.服务端检测到新的连接后,不再创建一个新的线程,而是直接将新连接绑定到clientSelector上。这样就不用1W个死循环待。
3.clientSelector被一个while死循环包裹着,如果某一时刻有多条连接有数据可读,那么通过clientSelector.select(1)方法轮训出来,进而批量处理。
4.数据的读写面向buffer.

二、Netty编程
Netty是什么呢? 用一句简单的话来说就是:Netty 封装了 JDK 的 NIO,让你用得更爽,你不用再写一大堆复杂的代码了。 用官方正式的话来说就是:Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。

下面是大佬们总结的使用 Netty 不使用 JDK 原生 NIO 的原因
使用 JDK 自带的NIO需要了解太多的概念,编程复杂,一不小心 bug 横飞
Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动,改改参数,Netty可以直接从 NIO 模型变身为 IO 模型
Netty 自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,让你只需要关心业务逻辑
Netty 解决了 JDK 的很多包括空轮询在内的 Bug
Netty 底层对线程,selector 做了很多细小的优化,精心设计的 reactor 线程模型做到非常高效的并发处理
自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
Netty 社区活跃,遇到问题随时邮件列表或者 issue
Netty 已经历各大 RPC 框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大。

我们使用Netty来实现一遍上面的功能。
maven配置

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

NettyServer.java

package com.guo.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;

public class NettyServer {
    public static void main(String[] args) {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        serverBootstrap
                .group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    protected void initChannel(NioSocketChannel ch) {
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                System.out.println(msg);
                            }
                        });
                    }
                })
                .bind(8000);
    }
}
  1. boss 对应 IOServer.java 中的接受新连接线程,主要负责创建新连接。
  2. worker 对应 IOServer.java 中的负责读取数据的线程,主要用于读取数据以及业务逻辑处理

NettyClient.java

package com.guo.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Date;

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) {
                        ch.pipeline().addLast(new StringEncoder());
                    }
                });
        Channel channel = bootstrap.connect("127.0.0.1", 8000).sync().channel();
        while (true) {
            channel.writeAndFlush(new Date() + ":你好!Netty");
            Thread.sleep(3000);
        }
    }
}

在这里插入图片描述

总结:
IO 读写是面向流,一次性只能从流中,读取一个或者多个字节,并且读完后无法再读取,你需要自己缓存数据。
NIO读写是面向buffer的,你可以随意读取里面任何一个字节数据,不需要自己缓存数据,这一切只需要 移动读写指针。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值