BIOとNIO実践的な用語の新しい理解

序文

誇大広告NIOの様々な概念で見つけブログをたくさん読んで、のようなBIOと一部のJava NIOの事で自分を見て、この時間は、明確かつ論理的に言った、非常に完全であると言うことができた後、しかし、全体の外観ダウン自分のNIOに少し状態の知識なので、この記事または概念の多くは言及しませんが、実用的な観点から、コンセプトを見に復帰した後、高い地位の練習の下で、NIOについての私自身の意見のいくつかを書くために、コンセプトはする必要があります私たちは、より良い理解を持っています。


シンプルなシングルスレッドのサーバを実装

BIOは、まず第一に、我々は彼らのサーバー、単純な、あまりにも複雑ではない、あなたはシングルスレッドすることができます実現する必要があり、理解し、NIOについて話しています。

なぜ、シングルスレッドのデモとして使用

あなたはシングルスレッド環境BIOとNIOの違いでうまく対比することができますので、もちろん、私は、実際の環境状況のスレッドを1つBIOと呼ばれるデモを要求します。

サーバー

パブリック クラスサーバー{
     パブリック 静的 ボイドメイン(文字列[]引数){
         バイト []バッファ= 新しい バイト [1024 ];
        してみてください{
            ServerSocketのServerSocketを = 新しい ServerSocketを(8080 );
            System.out.println(「サーバが起動されており、ポート8080でリッスン」);
             一方、真の){
                System.out.println();
                System.out.println( "サーバ接続を待っています..." );
                ソケットソケット = serverSocket.accept()。
                System.out.printlnは(「サーバ接続要求が受信されています...」);
                System.out.println();
                System.out.println( "サーバーのデータを待っています..." );
                socket.getInputStream()(バッファ)を読み取ります。
                System.out.printlnは(「サーバーはデータを受信しました」);
                System.out.println();
                文字列の内容 = 新しい文字列(バッファ)
                System.out.println(「受信データ:」+ コンテンツ)。
            }
        } キャッチ(IOExceptionを電子){
             // TODO自動生成キャッチブロック
            e.printStackTrace();
        }
    }
}

 

クライアント

パブリック クラスコンシューマー{
     公共の 静的な 無効メイン(文字列[] args)を{
         しようと{
            ソケットソケット = 新しいソケット( "127.0.0.1"、8080 );
            。Socket.getOutputStream()(ライト "サーバーにデータを送信" )(.getBytes)。
            socket.close();
        } キャッチ(IOExceptionを電子){
             // TODO自動生成キャッチブロック
            e.printStackTrace();
        }
    }
}

 

コード分​​析

私たちは、最初の8080クラスでSocketServerとバインドされたポートをインスタンス化実装サーバクラスを作成します。この方法を受け入れるように呼接続要求を受信した後、およびクライアントから送信されたデータを受信するためのreadメソッドを呼び出します。最後に、印刷データを受け取りました。

サーバー側の設計が完了した後、我々は127.0.0.1(機械)、ポート番号8080へのクライアント、Socketオブジェクトの最初のインスタンス、およびバインドIPを実装する必要があり、サーバーにデータを送信するためにwriteメソッドを呼び出します。

BIOとNIO実践的な用語の新しい理解

業績

私たちは、サーバーを起動しますが、次のようにクライアントがサーバーのコンソールに接続を開始しない場合:

BIOとNIO実践的な用語の新しい理解

クライアントがサーバにデータを送信するために起動すると、以下の結果をコンソール:

BIOとNIO実践的な用語の新しい理解

結論

以上の結果から実行すると、最初にすべての私たちは、少なくともサーバが起動された後、サーバーは、クライアントが接続サーバーを要求するまで受け入れる方法をブロックします呼び出すため、クライアントは、サーバーへの接続を持っていない、それを見ることができます。


クライアントの機能を拡張するために、

上記では、ソケットを確立するために主に達成するために、私たちのクライアントのロジックは - >サーバーに接続 - >サーバーへの接続がするとき、すぐに送信され、そして今、私たちはかつて拡張クライアントに来た後、私たちのデータをデータを送信します私たちは、サーバーに接続した後、データがすぐに送信されますが、手動でデータを入力するためのコンソールを待ち、その後、サーバーに送信されません。(サーバー側のコードは変更されません)

コード

パブリック クラスコンシューマー{
     公共の 静的な 無効メイン(文字列[] args)を{
         しようと{
            ソケットソケット = 新しいソケット( "127.0.0.1"、8080 );
            文字列メッセージ = NULL ;
            スキャナSC = 新しいスキャナ(System.in)。
            message = sc.next();
            socket.getOutputStream().write(message.getBytes());
            socket.close();
            sc.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

 

测试

当服务端启动,客户端还没有请求连接服务器时,控制台结果如下:

BIOとNIO実践的な用語の新しい理解

当服务端启动,客户端连接服务端,但没有发送数据时,控制台结果如下:

BIOとNIO実践的な用語の新しい理解

当服务端启动,客户端连接服务端,并且发送数据时,控制台结果如下:

BIOとNIO実践的な用語の新しい理解

结论

从上文的运行结果中我们可以看到,服务器端在启动后,首先需要等待客户端的连接请求(第一次阻塞),如果没有客户端连接,服务端将一直阻塞等待,然后当客户端连接后,服务器会等待客户端发送数据(第二次阻塞),如果客户端没有发送数据,那么服务端将会一直阻塞等待客户端发送数据。

服务端从启动到收到客户端数据的这个过程,将会有两次阻塞的过程。这就是BIO的非常重要的一个特点,BIO会产生两次阻塞,第一次在等待连接时阻塞,第二次在等待数据时阻塞。


BIO

在单线程条件下BIO的弱点

在上文中,我们实现了一个简易的服务器,这个简易的服务器是以单线程运行的,其实我们不难看出,当我们的服务器接收到一个连接后,并且没有接收到客户端发送的数据时,是会阻塞在read()方法中的,那么此时如果再来一个客户端的请求,服务端是无法进行响应的。换言之,在不考虑多线程的情况下,BIO是无法处理多个客户端请求的。

BIO如何处理并发

在刚才的服务器实现中,我们实现的是单线程版的BIO服务器,不难看出,单线程版的BIO并不能处理多个客户端的请求,那么如何能使BIO处理多个客户端请求呢。

其实不难想到,我们只需要在每一个连接请求到来时,创建一个线程去执行这个连接请求,就可以在BIO中处理多个客户端请求了,这也就是为什么BIO的其中一条概念是服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。

多线程BIO服务器简易实现

public class Server {
    public static void main(String[] args) {
        byte[] buffer = new byte[1024];
        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            System.out.println("服务器已启动并监听8080端口");
            while (true) {
                System.out.println();
                System.out.println("服务器正在等待连接...");
                Socket socket = serverSocket.accept();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("服务器已接收到连接请求...");
                        System.out.println();
                        System.out.println("服务器正在等待数据...");
                        try {
                            socket.getInputStream().read(buffer);
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        System.out.println("服务器已经接收到数据");
                        System.out.println();
                        String content = new String(buffer);
                        System.out.println("接收到的数据:" + content);
                    }
                }).start();

            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

 

运行结果

BIOとNIO実践的な用語の新しい理解

BIOとNIO実践的な用語の新しい理解

 

很明显,现在我们的服务器的状态就是一个线程对应一个请求,换言之,服务器为每一个连接请求都创建了一个线程来处理。

多线程BIO服务器的弊端

多线程BIO服务器虽然解决了单线程BIO无法处理并发的弱点,但是也带来一个问题:如果有大量的请求连接到我们的服务器上,但是却不发送消息,那么我们的服务器也会为这些不发送消息的请求创建一个单独的线程,那么如果连接数少还好,连接数一多就会对服务端造成极大的压力。所以如果这种不活跃的线程比较多,我们应该采取单线程的一个解决方案,但是单线程又无法处理并发,这就陷入了一种很矛盾的状态,于是就有了NIO。


NIO

NIO的引入

我们先来看看单线程模式下BIO服务器的代码,其实NIO需要解决的最根本的问题就是存在于BIO中的两个阻塞,分别是等待连接时的阻塞和等待数据时的阻塞。

public class Server {
    public static void main(String[] args) {
        byte[] buffer = new byte[1024];
        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            System.out.println("服务器已启动并监听8080端口");
            while (true) {
                System.out.println();
                System.out.println("服务器正在等待连接...");
                //阻塞1:等待连接时阻塞
                Socket socket = serverSocket.accept();
                System.out.println("服务器已接收到连接请求...");
                System.out.println();
                System.out.println("服务器正在等待数据...");
                //阻塞2:等待数据时阻塞
                socket.getInputStream().read(buffer);
                System.out.println("服务器已经接收到数据");
                System.out.println();
                String content = new String(buffer);
                System.out.println("接收到的数据:" + content);
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

 

我们需要再老调重谈的一点是,如果单线程服务器在等待数据时阻塞,那么第二个连接请求到来时,服务器是无法响应的。如果是多线程服务器,那么又会有为大量空闲请求产生新线程从而造成线程占用系统资源,线程浪费的情况。

那么我们的问题就转移到,如何让单线程服务器在等待客户端数据到来时,依旧可以接收新的客户端连接请求。

模拟NIO解决方案

如果要解决上文中提到的单线程服务器接收数据时阻塞,而无法接收新请求的问题,那么其实可以让服务器在等待数据时不进入阻塞状态,问题不就迎刃而解了吗?

第一种解决方案(等待连接时和等待数据时不阻塞)

public class Server {
    public static void main(String[] args) throws InterruptedException {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
            //Java为非阻塞设置的类
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            //设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            while(true) {
                SocketChannel socketChannel = serverSocketChannel.accept();
                if(socketChannel==null) {
                    //表示没人连接
                    System.out.println("正在等待客户端请求连接...");
                    Thread.sleep(5000);
                }else {
                    System.out.println("当前接收到客户端请求连接...");
                }
                if(socketChannel!=null) {
                    //设置为非阻塞
                    socketChannel.configureBlocking(false);
                    byteBuffer.flip();//切换模式  写-->读
                    int effective = socketChannel.read(byteBuffer);
                    if(effective!=0) {
                        String content = Charset.forName("utf-8").decode(byteBuffer).toString();
                        System.out.println(content);
                    }else {
                        System.out.println("当前未收到客户端消息");
                    }
                }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

 

运行结果

BIOとNIO実践的な用語の新しい理解

 

不难看出,在这种解决方案下,虽然在接收客户端消息时不会阻塞,但是又开始重新接收服务器请求,用户根本来不及输入消息,服务器就转向接收别的客户端请求了,换言之,服务器弄丢了当前客户端的请求。

解决方案二(缓存Socket,轮询数据是否准备好)

public class Server {
    public static void main(String[] args) throws InterruptedException {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        List<SocketChannel> socketList = new ArrayList<SocketChannel>();
        try {
            //Java为非阻塞设置的类
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            //设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            while(true) {
                SocketChannel socketChannel = serverSocketChannel.accept();
                if(socketChannel==null) {
                    //表示没人连接
                    System.out.println("正在等待客户端请求连接...");
                    Thread.sleep(5000);
                }else {
                    System.out.println("当前接收到客户端请求连接...");
                    socketList.add(socketChannel);
                }
                for(SocketChannel socket:socketList) {
                    socket.configureBlocking(false);
                    int effective = socket.read(byteBuffer);
                    if(effective!=0) {
                        byteBuffer.flip();//切换模式  写-->读
                        String content = Charset.forName("UTF-8").decode(byteBuffer).toString();
                        System.out.println("接收到消息:"+content);
                        byteBuffer.clear();
                    }else {
                        System.out.println("当前未收到客户端消息");
                    }
                }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

 

运行结果

BIOとNIO実践的な用語の新しい理解

BIOとNIO実践的な用語の新しい理解

代码解析

在解决方案一中,我们采用了非阻塞方式,但是发现一旦非阻塞,等待客户端发送消息时就不会再阻塞了,而是直接重新去获取新客户端的连接请求,这就会造成客户端连接丢失,而在解决方案二中,我们将连接存储在一个list集合中,每次等待客户端消息时都去轮询,看看消息是否准备好,如果准备好则直接打印消息。

可以看到,从头到尾我们一直没有开启第二个线程,而是一直采用单线程来处理多个客户端的连接,这样的一个模式可以很完美地解决BIO在单线程模式下无法处理多客户端请求的问题,并且解决了非阻塞状态下连接丢失的问题。

存在的问题(解决方案二)

从刚才的运行结果中其实可以看出,消息没有丢失,程序也没有阻塞。但是,在接收消息的方式上可能有些许不妥,我们采用了一个轮询的方式来接收消息,每次都轮询所有的连接,看消息是否准备好,测试用例中只是三个连接,所以看不出什么问题来,但是我们假设有1000万连接,甚至更多,采用这种轮询的方式效率是极低的。

另外,1000万连接中,我们可能只会有100万会有消息,剩下的900万并不会发送任何消息,那么这些连接程序依旧要每次都去轮询,这显然是不合适的。

真实NIO中如何解决

在真实NIO中,并不会在Java层上来进行一个轮询,而是将轮询的这个步骤交给我们的操作系统来进行,他将轮询的那部分代码改为操作系统级别的系统调用(select函数,在linux环境中为epoll),在操作系统级别上调用select函数,主动地去感知有数据的socket。


关于使用select/epoll和直接在应用层做轮询的区别

我们在之前实现了一个使用Java做多个客户端连接轮询的逻辑,但是在真正的NIO源码中其实并不是这么实现的,NIO使用了操作系统底层的轮询系统调用 select/epoll(windows:select,linux:epoll),那么为什么不直接实现而要去调用系统来做轮询呢?

select底层逻辑

BIOとNIO実践的な用語の新しい理解

假设有A、B、C、D、E五个连接同时连接服务器,那么根据我们上文中的设计,程序将会遍历这五个连接,轮询每个连接,获取各自数据准备情况,那么和我们自己写的程序有什么区别呢?

首先,我们写的Java程序其本质在轮询每个Socket的时候也需要去调用系统函数,那么轮询一次调用一次,会造成不必要的上下文切换开销。

而Select会将五个请求从用户态空间全量复制一份到内核态空间,在内核态空间来判断每个请求是否准备好数据,完全避免频繁的上下文切换。所以效率是比我们直接在应用层写轮询要高的。

如果select没有查询到到有数据的请求,那么将会一直阻塞(是的,select是一个阻塞函数)。如果有一个或者多个请求已经准备好数据了,那么select将会先将有数据的文件描述符置位,然后select返回。返回后通过遍历查看哪个请求有数据。

select的缺点:

  • 底层存储依赖bitmap,处理的请求是有上限的,为1024。
  • 文件描述符是会置位的,所以如果当被置位的文件描述符需要重新使用时,是需要重新赋空值的。
  • fd(文件描述符)从用户态拷贝到内核态仍然有一笔开销。
  • select返回后还要再次遍历,来获知是哪一个请求有数据。

poll函数底层逻辑

poll的工作原理和select很像,先来看一段poll内部使用的一个结构体。

struct pollfd{
    int fd;
    short events;
    short revents;
}

 

poll同样会将所有的请求拷贝到内核态,和select一样,poll同样是一个阻塞函数,当一个或多个请求有数据的时候,也同样会进行置位,但是它置位的是结构体pollfd中的events或者revents置位,而不是对fd本身进行置位,所以在下一次使用的时候不需要再进行重新赋空值的操作。poll内部存储不依赖bitmap,而是使用pollfd数组的这样一个数据结构,数组的大小肯定是大于1024的。解决了select 1、2两点的缺点。

epoll

epoll是最新的一种多路IO复用的函数。这里只说说它的特点。

epoll和上述两个函数最大的不同是,它的fd是共享在用户态和内核态之间的,所以可以不必进行从用户态到内核态的一个拷贝,这样可以节约系统资源;另外,在select和poll中,如果某个请求的数据已经准备好,它们会将所有的请求都返回,供程序去遍历查看哪个请求存在数据,但是epoll只会返回存在数据的请求,这是因为epoll在发现某个请求存在数据时,首先会进行一个重排操作,将所有有数据的fd放到最前面的位置,然后返回(返回值为存在数据请求的个数N),那么我们的上层程序就可以不必将所有请求都轮询,而是直接遍历epoll返回的前N个请求,这些请求都是有数据的请求。


Java中BIO和NIO的概念

通常一些文章都是在开头放上概念,但是我这次选择将概念放在结尾,因为通过上面的实操,相信大家对Java中BIO和NIO都有了自己的一些理解,这时候再来看概念应该会更好理解一些了。

概念整理于:

https://blog.csdn.net/guanghuichenshao/article/details/79375967

 

先来个例子理解一下概念,以银行取款为例:

  • 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写)。
  • 异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API)。
  • 阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回)。
  • 非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)。

 

Java对BIO、NIO的支持:

  • Java BIO (blocking I/O): 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
  • Java NIO (non-blocking I/O): 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

 

BIO、NIO适用场景分析:

  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

结语

この記事ではJavaBIO上のいくつかのNIOを説明し、ビューの実用的な動作点の独自の理解の一部から、私は個人的にBIOの概念を理解し、NIOは、私もを通じて、私の学生が自分を倒すことを願って光より深い理解があるだろうが表示されますことを考えますプログラムの実行結果は、JavaBIO NIOの自分の理解に来ます。

個人のブログへようこそます。http://blog.objectspace.cn/

おすすめ

転載: www.cnblogs.com/javazhiyin/p/11719132.html