NIO 及其在 Golang 网络库中的应用

2021年9月25日 406点热度 0人点赞 0条评论

【导读】NIO 是如何让 go 语言 web 库速度快的?本文对 NIO 和 golang 网络库做了详细介绍。

NIO(Non-blocking I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,是现今主流的大流量、高并发IO有效解决方案。

五种IO模型

在UNIX下,IO模式分为五类,分别是:阻塞式IO(bloking IO)、非阻塞式IO(non-blocking IO)、多路复用IO模型(multiplexing IO)、信号驱动IO模型(signal-driven IO)以及异步IO模型(asynchronous IO)。其中又以前三种模式最为常见。

图片

5种IO模型对比图

传统的BIO模式

在阐述选择NIO的原因之前,首先说明一下阻塞和非阻塞的概念。阻塞和非阻塞的核心区别就在于,在IO就绪态(读就绪、写就绪、有新连接)到来之前是否会阻塞等待。

在最初的网络编程中,我们使用BIO模式构建编程模型,如下面的伪代码所示,这是经典的per thread per connection模型。这段代码的核心部分在于accept()、socket.read()、socket.write()三个函数,这三个函数在等待IO就绪态到来的过程中都将阻塞各自的线程。当连接数量达到一定程度之后,这样的阻塞、对线程资源的无效占用就变得不可容忍。后续的优化包括建立线程池,进行线程扩容,但这并没有根本解决问题。而NIO+多路复用的网络模型很好的解决了这个问题。

//BIO JAVA伪代码示意
class Server {
    public static void main() {
        while(true){    
            socket = server.accept();
            executor.submit(new ConnectIOHandler(socket));
        }
    }
}

class ConnectIOnHandler implements Runnable{
    private Socket socket;
    public ConnectIOnHandler(Socket socket){
       this.socket = socket;
    }

    @Override
    public void run() {
        while (!Thread.currentThread.isInturruted() && socket.isClosed()) {
            //读取数据
            String data = socket.read()....
            if (data != null) {
                //处理数据
                dosomething();
                //写数据
                socket.write()...
            }
        }
    }

NIO + multiplexing IO

现今的高性能网络库基本采用了NIO+多路复用的模式构建,例如著名的netty,那么这是为什么呢?我们都知道,在网络IO最耗时的部分就在于等待IO就绪的过程,而真正的IO操作是一个高性能的过程,而NIO有一个重要的特点:socket的主要读、写、注册和接收函数,在等待就绪态前都是非阻塞的,只有在进行真正的IO操作时是同步阻塞的。结合多路复用带来的事件通知特性,就可以构建一套更高性能的网络模型。

读到这里你可能会有疑问,为什么是NIO + multiplexing IO,而不是BIO + multiplexing IO呢?以Linux下的Epoll举例,我们向Epoll的selector中注册一个socket并标示可读事件,当epoll_wait返回可读事件EPOLLIN到来,我们只知道socket可读,但不会知道有多少的数据可读,如果我们多次调用read函数将可能导致阻塞事件发生,所以如果是BIO+ multiplexing IO,我们必须每次read过后就马上返回epoll_wait,这种要求是苛刻的,在某些业务场景下也是不允许的,所以在实际的应用中,BIO + multiplexing IO的组合几乎不会出现。

golang的NIO

这是一个典型的Golang TCP Server示例

package main

import (
"fmt"
"net"
)

func main() {
listen, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Println("listen error: ", err)
return
}

for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept error: ", err)
break
}

// start a new goroutine to handle the new connection
go HandleConn(conn)
}
}
func HandleConn(conn net.Conn) {
defer conn.Close()
packet := make([]byte, 1024)
for {
// 如果没有可读数据,也就是读 buffer 为空,则阻塞
_, _ = conn.Read(packet)
// 同理,不可写则阻塞
_, _ = conn.Write(packet)
}
}

这段代码看起来和上文中的JAVA BIO代码很类似。那么这段Go代码里就绪态等待也会阻塞线程么?答案是并不会。相比与java,golang应用直接调用的是更为轻量级的协程goroutine,当socket在进行就绪态等待的时候,会阻塞协程,但是并不会阻塞线程。同时,golang的原生网络库底层同样实现了一套NIO + multiplexing IO的网络模型(netpoll),我们以Linux环境举例,在Linux下,netpoll的底层实现是Epoll, 我们的连接套接字被创建后会被设置为NO-BLOCK模式,而后加入到Epoll中进行监听,当读写就绪态到来之后,套接字阻塞的goroutine会被加入到可运行队列中,等待golang调度器的调度运行。

转自:

fbelisk.github.io/

 - EOF -

推荐阅读(点击标题可打开)

1、谈谈自己对GO的RWMutex的理解

2、go 标准库 net/url 学习笔记

3、Golang Map 内部实现

Go 开发大全

参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。

图片

关注后获取

回复 Go 获取6万star的Go资源库

分享、点赞和在看

支持我们分享更多好文章,谢谢!

9100NIO 及其在 Golang 网络库中的应用

root

这个人很懒,什么都没留下

文章评论