BIO NIO 网络编程笔记

BIO

阻塞IO,是指线程在访问IO资源的时候,如果资源不存在也会一直等待。使用线程池的BIO的并发能力基本就是跟定义的线程池的大小一致,甚至更糟。

BIO编程主要用的就是java net api和io api,这2种api都是阻塞的

服务端:

public class BIOSocketServer {
    public static void main(String[] args) throws Exception{
        ServerSocket serverSocket = new ServerSocket(8081);
        System.out.println("服务器启动成功,端口8081。");
        while (!serverSocket.isClosed()) {
            Socket request = serverSocket.accept(); //阻塞线程的api
            System.out.println("收到新连接:" + request.toString());
            try{
                System.out.println("开始打印接受信息:");
                // 开始io操作
                InputStream inputStream = request.getInputStream();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
                String msg;
                while ((msg = bufferedReader.readLine()) != null) { // 阻塞线程的api
                    if (msg.length() == 0) {
                        break;
                    }
                    System.out.println(msg);
                }
                System.out.println("数据来自:" + request.toString());
            }catch (IOException e) {
                e.printStackTrace();
            }finally {
                request.close(); // 关闭此次连接
            }
        }
    }
}

客户端:

public class BIOSocketClient {
    private static Charset charset = Charset.forName("UTF-8");

    public static void main(String[] args) throws IOException {
        String url = "localhost";
        int port = 8081;
        System.out.println("请求连接 "+ url + ":" + port);
        Socket socket = new Socket(url, port);
        OutputStream out = socket.getOutputStream();

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入:");
        String msg = scanner.nextLine(); // 阻塞线程io

        System.out.println("开始发送数据...");
        out.write(msg.getBytes(charset)); // 阻塞线程io

        scanner.close();
        socket.close();
        System.out.println("数据发送完毕。");
    }
}

BIO的代码是直接面向底层网络接口的,要想实现应用级的功能是非常麻烦的,相当于自己要设计访问协议(比如请求、断开、状态管理怎样控制),多线程管理等。

NIO

非阻塞IO,是指线程在访问IO资源的时候,如果资源不存在就会返回一个标识,线程就可以去做别事情而不是傻傻的等。从而可以实现单线程处理多网络连接。

NIO的出现就是为了替代原始的BIO这种直接操作Java Networking和Java IO的方式。

NIO有三个组件:缓冲区(Buffer),通道(Channel),选择器(Selector)。主要思路就是通过缓存来解决线程阻塞的问题,从而解放线程提高效率。

缓冲区 Buffer

Buffer本质就是一个内存块,我们可以通过相关api方便的存取数据。

Buffer的3个主要属性:
1)容量 capacity:代表这个内存块的固定大小。
2)位置 position:写入模式代表写入的位置;读取模式代表读取的位置。
3)限制 limit:写入模式等价于容量大小;读取模式等价于之前写入的数据量。

buffer的2种内存:

1)Direct内存,也叫堆外内存:
特点是直接跟操作系统交互,IO操作的时候相比堆内存少了一次copy,速度更快(不经过jvm堆,也不受GC整理影响)。
虽然GC不能直接回收对外内存,但是jvm帮我们实现好了方法,DirectByteBuffer中有个cleaner对象,通过虚引用关联着堆外内存,GC的时候是可以回收DirectByteBuffer对象的,cleaner对象被回收之前会执行clean方法,调用DirectByteBuffer内定义好的回收堆外内存的方法。
堆外内存适合性能要求较高,频繁使用并且占用空间较大的情况。

2)非直接内存,也叫堆内存:特点是直接由JVM管理,适用于绝大多数场景。


image.png

image.png

通道 Channel

channel是nio包封装的对象,用于简化网络开发。相比bio需要socket和io2套api去操作,使用nio网络读写只需要使用channel一套api。

nio最大的特点是支持非阻塞,也因为这个特点,nio的io操作需要在循环中判断执行(因为调用的时候不见得就立即会执行,所以需要不停的判断)。


image.png

选择器 Selector:

可以看到,以上nio程序里有各种网络状态相关的循环和判断。先不讨论写法,这种单线程不停循环的方式在处理高并发场景下效率也是低下的,因为会有很多无效的执行。

selector使用了事件驱动机制,底层原理是操作系统的多路复用机制。他可以监听多个channel的网络状态,状态改变就会触发相关事件回调,实现了单线程管理多channel。

selector监听的的事件:

1)SelectionKey.OP_CONNECT: connect连接
2)SelectionKey.OP_ACCEPT: accept准备就绪
3)SelectionKey.OP_READ: read读
4)SelectionKey.OP_WRITE: write写

public void testChannelWithSelector () throws IOException {
        // 1.创建服务端channel
        int port = 8081;
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); // 设置非阻塞

        // 2.使用selector选择器注册channel
        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel);
        // 表示对sserverSocketChannel上的accept事件关注,serverSocketChannel只能注册accept事件。
        selectionKey.interestOps(selectionKey.OP_ACCEPT);

        // 3。绑定端口,开启通道
        serverSocketChannel.socket().bind(new InetSocketAddress(port));

        // 循环检查selector
        while(true) {
            selector.select(); // 这是一个阻塞方法,有事件通知才会执行,因为只注册了accept事件,所以有新连接才会往下执行

            // 获取事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            // 使用迭代器遍历
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                // 处理accept事件,受益于事件机制,这里服务端accept一定会拿到socketChannel
                // 同时注册read事件,方便后续读数据
                if (key.isAcceptable()) {
                    // 先拿到当前ServerSocketChannel
                    ServerSocketChannel server = (ServerSocketChannel) key.attachment();

                    // 获取socketChannel
                    SocketChannel clientChannel = server.accept();
                    clientChannel.configureBlocking(false);

                    // 使用selector注册read事件
                    clientChannel.register(selector, SelectionKey.OP_READ, clientChannel);

                    System.out.println("收到新连接:"+clientChannel.getRemoteAddress());
                }

                // 处理read事件
                if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.attachment();

                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    while (socketChannel.isOpen() && socketChannel.read(byteBuffer) != -1) {
                        // TODO 长连接情况下需要手动判断是否读结束,此处暂时简单处理
                        if (byteBuffer.position() > 0) {
                            break;
                        }
                    }

                    // 如果读不到数据则跳出此次事件处理
                    if (byteBuffer.position() == 0) continue;

                    // 缓存读模式
                    byteBuffer.flip();
                    byte[] content = new byte[byteBuffer.limit()];
                    byteBuffer.get(content);
                    System.out.println("收到"+socketChannel.getRemoteAddress()+"数据:" + new String(content));

                    // 模拟响应
                    String response = "HTTP/1.1 200 OK\r\n"+
                            "Content-Length: 1\r\n\r\n"+
                            "Hello World";

                    ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
                    while (responseBuffer.hasRemaining()) {
                        socketChannel.write(responseBuffer); // 非阻塞写入通道
                    }
                }
            }

        }
    }

上述代码最大特点就是使用循环检查selector事件监听代替了循环accept,利用事件驱动机制提高了执行效率;同时读取请求数据的逻辑也使用了selector进行了调整,优化了实现。

NIO 进一步改进

以上的NIO的写法是基于单线程的,能尽量提高单线程的利用率。但是俗话说”双拳难敌四手“,面对现实生产场景中的海量并发,一个线程再怎么优化,执行效率也是有瓶颈的,而且目前的cpu大多都是多核,只是单线程也不能充分利用硬件资源。解决这个问题的方案就是大名鼎鼎的React设计模式。

Dog Lea 的React设计思想:React的设计思路主要是在NIO(非阻塞)的基础上使用发布订阅模式进一步优化了网络连接(accept)、网络IO(read,send),并且把业务线程和网络线程隔离开,各自分工,尽可能放大各环节的效率,尽量保证系统的网络并发能力。

打个比方,一个顾客要去逛商城,开门,导购,买卖交易如果始终是一个人在服务的话,这个商城也接纳不了几个顾客。各自分工专业,各个环节都变的效率了,整体效率才会提高。

下图的重要的几个环节是:
1)mainReactor:接收线程组,专注连接接收,并且通过注册分发给网络io线程组处理。
2)subReactor:网络io线程组,专注处理网络io,同时把网络数据注册给工作线程组处理
3)workerThreads:工作线程组,专注处理业务。

image.png

参考文章:
ByteBuffer常用操作
NIO技术1
NIO技术2

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

推荐阅读更多精彩内容