通过一个Socket连接的例子来认识 NIO

众所周知,NIO(new IO 也叫做 NoBlocking IO),非阻塞IO,是由BIO演变而来的,它的优势就在于数据传输过程中,由BIO的阻塞式变为非阻塞式,那么什么是阻塞和非阻塞?

先来一个BIO的例子。

/**
 * 服务端
 */
public class Server {
    static byte[] bytes = new byte[1024];
    public static void main(String[] args) {
        try {
            //serverSocket 用于监听连接
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(8080));
            while(true){
                System.out.println("Waiting  connect");
                //阻塞状态,如果没有连接就一直阻塞。这个socket对象用于与客户端进行通信
                Socket socket = serverSocket.accept();
                System.out.println("connect success");
                //阻塞状态, 读取客户端发来的数据。
                //read 表示读了多少字节的数据
                int read = socket.getInputStream().read(bytes);
                System.out.println("read data success");
                String content = new String(bytes);
                System.out.println("Data:"+content);
                System.out.println();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        try {
            //与服务器端进行通信
            Socket socket = new Socket("127.0.0.1",8080);

            Scanner scanner = new Scanner(System.in);
            String content = scanner.nextLine();
            socket.getOutputStream().write(content.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

从服务端代码中我们可以知道,服务器端有两重阻塞,第一重我姑且叫它连接阻塞,服务器在等待客户端连接,第二重叫它数据读取阻塞,服务器等待客户端发送的数据。 因此我们可以想象,当有一个用户连接上服务器,但他并没有发送数据(这就好像我登上百度,但是我并不检索,我就看看),那么服务器就会阻塞在数据读取阶段,那么此时如果有第二个用户来连接,他是连接不上的。 因为阻塞就相当于线程放弃了CPU资源,CPU已经不再光顾这段服务端代码了。所以,在不考虑多线程的情况下,BIO是无法处理并发的情况的。

如果BIO要处理并发,一般来说就在连接阻塞(serverSocket.accept();)后面开一个线程。但是这样又有一个弊端。假设有10万的用户连接,那么就要创建10万个线程,但是只有2万的用户发送数据,那么剩下8万线程资源是不是就会浪费了。 所以在服务端,如果不活跃的线程比较多,还是推荐单线程。

所以又回到了那个问题,BIO还是不能很好地处理并发,因此出现了NIO。


由上分析我们知道了,单线程它之所以不能处理并发的重要原因是那两处阻塞,那如果我们将阻塞变为非阻塞是否可行呢?我稍微修改下代码:

public class Server {
    static byte[] bytes = new byte[1024];
    public static void main(String[] args) {
        try {
            //serverSocket 用于监听连接
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(8080));

            while(true){
                System.out.println("Waiting  connect");
                //阻塞状态,如果没有连接就一直阻塞。这个socket对象用于与客户端进行通信
                Socket socket = serverSocket.accept();
                System.out.println("connect success");
				/*******************************************************/                
                socket.setNoBlock(); //设置为非阻塞(实际上socket并没有这个API,这里演示一下)
                /*******************************************************/     
                //阻塞状态, 读取客户端发来的数据。
                //read 表示读了多少字节的数据
                if(read == 0){  //没有读取到数据
                    continue;
                }else{  //读取到了数据,执行逻辑
                    System.out.println("read data success");
                    String content = new String(bytes);
                    System.out.println("Data:"+content);
                    System.out.println();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

我将数据读取阻塞设置为非阻塞,那么此时客户端不管有没有发送数据,程序都会往下执行,那么又会出现一个问题。那就是数据滞后。比方说:
在这里插入图片描述
① 服务器s1启动,进入等待连接状态
② 此时客户端c1连接到服务器
③ 客户端发送数据,服务端执行逻辑
④ 客户端没有发送数据,服务端继续等待其他客户端连接(此时为连接阻塞状态)。

如果此时有第二个客户端c2连接进来。

在这里插入图片描述
⑤ 客户端c2连接服务器
⑥ 客户端发送数据,服务端执行逻辑
⑦ 客户端没有发送数据,服务端等待连接。(此时为连接阻塞状态)。

如果此时客户端c1发送数据,服务端能否接收到数据呢?
答案是不能,因为此时服务端处于阻塞状态,没有分配CPU资源,无法执行数据获取的代码,因此,连接阻塞状态我们也要设置为非阻塞。

public class Server {
    static byte[] bytes = new byte[1024];
    public static void main(String[] args) {
        try {
            //serverSocket 用于监听连接
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(8080));
            
            serverSocket.setNoBlock(); //设置为非阻塞(实际上socket并没有这个API,这里演示一下)
            while(true){
                System.out.println("Waiting  connect");
                //阻塞状态,如果没有连接就一直阻塞。这个socket对象用于与客户端进行通信
                Socket socket = serverSocket.accept();
                if(socket == null){ //无人连接
                    continue;
                }else{  //有人连接,执行逻辑
                    System.out.println("connect success");

                    socket.setNoBlock(); //设置为非阻塞(实际上socket并没有这个API,这里演示一下)
                    //阻塞状态, 读取客户端发来的数据。
                    //read 表示读了多少字节的数据
                    int read = socket.getInputStream().read(bytes);
                    if(read == 0){  
                        continue;
                    }else{
                        System.out.println("read data success");
                        String content = new String(bytes);
                        System.out.println("Data:"+content);
                        System.out.println();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这样子我们再模拟一下上面图片中的流程。
c1连接服务器不发送数据,服务器不等待连接,直接执行数据读取逻辑代码。
c2连接服务器不发送数据,服务器不等待连接,直接执行数据读取逻辑代码。
此时的服务器还是处于活跃状态,因为我们设置了非阻塞,代码中已经没有可以阻挡CPU资源的地方了。 那么此时 c1 发送了数据, 那么服务器是可以接收到数据的。

但是问题又来了,服务器接收到的数据,它是如何确定是c1发送的还是c2发送的呢?换句话说,此时的socket并不是c1的socket,因为属于c1的socket已经被属于c2的socket覆盖了,那么服务端就无法处理对应用户发送的数据,就会导致数据紊乱。
那么我们将每个连接的socket存起来是不是就解决了呢?


/**
 * 服务端
 */
public class Server {
    static List<Socket> list = new ArrayList<>();
    static byte[] bytes = new byte[1024];
    public static void main(String[] args) {
        try {
            //serverSocket 用于监听连接
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(8080));
            
            serverSocket.setNoBlock(); //将等待连接设置为非阻塞(实际上socket并没有这个API,这里演示一下)
            
            while(true){
                //阻塞状态,如果没有连接就一直阻塞。这个socket对象用于与客户端进行通信
                Socket socket = serverSocket.accept();
                if(socket != null)  //表示有人连接
                    list.add(socket);  //将这个人对应的socket存起来
                
                socket.setNoBlock(); //将数据读取设置为非阻塞(实际上socket并没有这个API,这里演示一下)

                for(Socket socket1: list) {
                    int read = socket1.getInputStream().read(bytes);
                    if (read == 0) {
                        continue;
                    } else {
                        String content = new String(bytes);
                        System.out.println(content);
                        System.out.println();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

我们用一个ArrayList来存储socket。
当有用户连接服务器时,就将这个用户的socket存起来。 不管有没有用户连接,服务器会通过遍历这个List来查找对应的socket是否有发送数据,这样就能保证获取到一个socket对应的数据。

我们再来模拟一下这个过程

c1连接服务器不发送数据,服务器将socketC1加入List, 服务器轮询List判断有没有客户端发送数据
c2连接服务器不发送数据,服务器将socketC2加入List, 服务器轮询List判断有没有客户端发送数据
此时c1发送数据, 服务器轮询List, 发现socketC1有数据流在传输,那么读取socketC1的数据,完成数据读取。

以上就是 单线程环境下实现并发的大体思路。以上都是基于能够将阻塞状态设置为非阻塞状态而进行的。但实际上BIO并没有Java API能够设置非阻塞,因此BIO是阻塞式的IO。

NIO为什么是非阻塞的呢?
缓冲区(Buffer):在Java NIO中负责数据的存取。缓冲区就是数组。用于存储不同数据类型的数据
通道(Channel) :在Java NIO中负责缓冲区中数据的传输。Channel本身不存储数据,因此需要配合缓冲区进行传输。

Java为NIO配了对应的类: ServerSocketChannel和SocketChannel;这两个类就可以实现将阻塞设置为非阻塞。


/**
 * 服务端
 */
public class ServerNIO {
    static List<SocketChannel> list = new ArrayList<>();
    static byte[] bytes = new byte[1024];
    static ByteBuffer dst = ByteBuffer.allocate(1024);

    public static void main(String[] args) {
        try {
            //serverSocketChannel 用于监听连接
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            serverSocketChannel.configureBlocking(false);  //设置非阻塞

            while (true) {
                //阻塞状态,如果没有连接就一直阻塞。这个socket对象用于与客户端进行通信
                SocketChannel socketChannel = serverSocketChannel.accept();

                if (socketChannel != null){                     //表示有人连接
                    socketChannel.configureBlocking(false);   //设置非阻塞
                    list.add(socketChannel);  //将这个人对应的socket存起来
                }


                for (SocketChannel socketChannel: list) {
                    int read = socketChannel.read(dst);
                    if (read > 0) {
                        dst.flip(); //转换为读数据模式
                        System.out.println(dst.toString());
                    }
                    dst.clear();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        try {
            //与服务器端进行通信
            Socket socket = new Socket("127.0.0.1",8080);

            Scanner scanner = new Scanner(System.in);
            while(true){
                String content = scanner.nextLine();
                socket.getOutputStream().write(content.getBytes());
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

你可以copy一个一模一样的客户端进行测试。


当然了,以上只是通过一个简单的例子来告诉大家BIO的阻塞和NIO的非阻塞到底是什么。

官方点,NIO其实相当于就是一个线程处理大量的客户端的请求,通过一个线程轮询大量的channel,每次就获取一批有事件的channel(其实就是上面的socketChannel),然后对每个请求启动一个线程处理即可。

实际上NIO中有一个Selector模式,原理就是我上文所述的,在非阻塞状态下通过selector,一个县城就可以不停轮询,所有客户端请求都不会阻塞,直接就会进来。

这里面优化BIO的核心就是,一个客户端并不是时时刻刻都有数据进行交互,没有必要死耗着一个线程不放,所以客户端选择了让线程歇一歇,只有客户端有相应的操作的时候才发起通知,创建一个线程来处理请求。

但是轮询是要对所有连接进行查找,这是非常消耗性能的。因此能否快速精确找出活跃的连接(有数据发送的socket)是非常有必要的。 像Linux系统中的epoll就能实现这个功能。(还不会,有待学习)

------------以上是通过一些视频和文章总结得到---------------
------------博主的个人理解,如有错误,希望大牛文明指出(祖安大哥出门右转)------------

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值