从BIO到NIO到多路复用器的“进化论”

首先抛出一个步骤,同步IO(BIO,NIO,多路复用器)都是同步的,它们在服务端有相同的启用端口开启监听的步骤。

  1. socket 得到一个文件描述符sfd
  2. bind 在服务端绑定端口
  3. listen 开启监听
  4. accept 获取客户端连接
  5. recv 获取客户端发来的数据(读写)

前三步在BIO,NIO,多路复用器里大同小异,差异在后面两步accept与recv的处理方法。通过几个例子来说明。

1.BIO(Blocking IO)

同步阻塞,⼀个请求过来,应⽤程序开了⼀个线程,等 IO 准备好,IO 操作也是自己干; 采用 BIO 模型的服务端,由⼀个独立的 Acceptor 线程负责进行监听;在 while(true) 循环中调用accept() 方法,等待 客户端的请求; 一旦接收到请求,就可以建立套接字开始进行读写操作,这时候不再接收其他的请求,直到读写完成; 为了让 BIO 能够同时处理多个请求,那么就需要使用多线程处理;当服务端接收到请求,就为客户端创建⼀个线程进行处理,处理完成后再做线程销毁;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
 1. BIO 多个连接,多个线程
 2. */
public class BIOSocket {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8081);
        System.out.println("第一步:new ServerSocket(8081)");
        while (true){
            Socket client = serverSocket.accept();
            System.out.println("第二步:client端口  "+client.getPort());

            new Thread(new Runnable() {
                Socket ss;
                public Runnable setSS(Socket s){
                    ss=s;
                    return this;
                }
                @Override
                public void run() {
                    try {
                        InputStream in = ss.getInputStream();
                        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                        while (true){
                            System.out.println(reader.readLine());
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }.setSS(client)
            ).start();
        }
    }
}

启动程序

  1. 当没有客户端连接,程序会在Socket client = serverSocket.accept();处阻塞,等待客服端的连接。(accept)
  2. 当获取到客户端连接后,新开一条线程获取客户端发来的数据,如果没有数据过来,此时线程会阻塞。(recv)

下图为在accept处阻塞,服务端8081端口处于监听状态,等待客户端连接。
在这里插入图片描述
新建一个客户端,连接服务端端口8081。

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

public class Client1 {
    public static void main(String[] args) throws IOException {
        Socket client=new Socket("127.0.0.1",8081);
        System.out.println("连接服务器成功!"+client.getLocalSocketAddress());
    }
}

下图为连接客户端成功,开了一个52565端口作为客户端,此时客户端没有数据发送,程序发生阻塞(recv)
在这里插入图片描述在这里插入图片描述
总结BIO:
优势:可以连接很多的线程
问题:线程内存浪费,cpu调度消耗cpu时间片 (根源就是accept,recv会阻塞)

2.NIO

NIO在Java中是New IO,在操作系统中是NonBlocking IO(非阻塞IO)
同步非阻塞,不用等待 IO 准备,准备好了会通知,不过 IO 操作还是要自己干。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
/**
* NIO 一个线程可以解决很多个客户端连接
* */
public class NIOSocket {
    public static void main(String[] args) throws IOException, InterruptedException {
        //用一个linkedList集合存放连接完成的客户端
        LinkedList<SocketChannel> clients = new LinkedList<>();

        ServerSocketChannel ss = ServerSocketChannel.open();
        ss.bind(new InetSocketAddress(8082));//绑定服务端端口8082
        ss.configureBlocking(false);//设置内核为NonBlocking

        while (true){
            Thread.sleep(1000);
            //尝试获取客户端连接,没有客户端连接就返回null,不会阻塞,在BIO的时候就会一直阻塞
            //如果客户端来连接了,accept返回的是这个客户端的fd
            SocketChannel client = ss.accept();

            if (client==null){
                System.out.println("null..");
            }else{
                /*
                 * 对客户端设置非阻塞,分两种情况:
                   1.客户端有数据过来->正常情况;
                   2.客户端没有数据过来->也不会阻塞,操作系统上返回状态-1,Java里返回null
                 * */
                client.configureBlocking(false);
                int port = client.socket().getPort();
                System.out.println("client port:"+port);
                clients.add(client);
            }

            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4096);
            //遍历连接的客户端能不能读写数据
            for (SocketChannel c : clients) {
                int num=c.read(byteBuffer);
                if (num>0){
                    byteBuffer.flip();
                    byte[] bytes = new byte[byteBuffer.limit()];
                    byteBuffer.get(bytes);

                    String s = new String(bytes);
                    System.out.println(c.socket().getPort()+" : "+s);
                    byteBuffer.clear();
                }
            }
        }
    }
}

启动程序,由于设置了非阻塞–> ss.configureBlocking(false) 在内核中将阻塞设置为非阻塞,client.configureBlocking(false) 客户端也设置为非阻塞。即使没有客户端连接,或者是没有数据发送过来,程序也不会阻塞,而是在Java中返回null,在操作系统中返回-1状态,程序继续循环,单线程轮询。
在这里插入图片描述
总结:
优势:规避多线程,减少线程内存浪费
问题:不断轮询,假设有10000个连接而只有一个连接发来数据,每循环一次,其实必须向内核发送10000次的recv系统调用(代码中的for循环),那么这里有9999次是无意义的,CPU空转造成消耗资源。
如何解决? 多路复用器…

3.多路复用器

多路复用器就是通过系统内核返回状态,告诉你哪些连接可读可写,具体的读写还是要用户程序自己干。三种方式如下图:
在这里插入图片描述
简单来讲select和poll通过一次系统调用,把fds传给内核,内核遍历,减少了系统调用的次数,这是优势,问题在每次select、poll都要重新遍历fd,所以这里就引出了epoll,在内核中开辟空间保存fd。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值