编程语言
首页 > 编程语言> > JavaIO(二)-BIO详解

JavaIO(二)-BIO详解

作者:互联网

啥也不说先上代码,这是一个很简单的从本地文件中读取数据的程序

public class FileBioTest {
    public static void main(String[] args) throws Exception {
        BufferedReader reader = null;
        try {
            //2号参数可以指定缓冲区大小 默认8192
            reader = new BufferedReader(new FileReader("E:\\test.txt"));
            StringBuilder content = new StringBuilder();
            reader.lines().forEach(content::append);
            System.out.println(content);
        } finally {
            if (Objects.nonNull(reader)) {
                reader.close();
            }
        }
    }
}

文件内容:

你要悄悄的优秀,
然后惊艳所有人。

控制台输出:

 

这是一个很简单的文件内容读取程序,但是它可能代表不了BIO,他只能代表IO。因为文件读取总是阻塞的,现代操作系统都有复杂的缓存和预取机制,使得本地磁盘 I/O 操作延迟很少,这样就使得非阻塞 这一操作意义不大。所以BIO,NIO概念更多的针对的都是网络数据传输。

BIO被叫做同步阻塞IO,阻塞强调等待,同步强调顺序,阻塞强调资源获取方式,同步强调处理资源的业务逻辑。

堵塞:当资源不可用时,IO会一直等待,直到超时或者反馈

非堵塞:当资源不可用时,IO请求会直接返回,并标识资源不可用,但是不妨碍我下一次接着请求。

同步:当前IO操作执行完才会执行下一个操作。

异步:当前IO操作执行的同时,处理别的操作。当操作完成,告知用户即可。

那么阻塞占用CPU资源吗?其实不占用或者占用很少,这里的阻塞是强调IO阻塞,不是线程阻塞,在IO设备与CPU交互的方式中有一种名为中断的交互方式。IO阻塞,线程挂起不再执行,等待中断信号,唤醒然后继续执行。这就跟我们前边说的 阻塞强调等待,强调资源获取方式对应起来了。

之前我们说只有网络IO才有阻塞和非阻塞的区别,那我们开始写一个程序来体验一下阻塞。

public class BioBaseServer {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(9090);
            System.out.println("create socket, listen 9090");
            Socket client = serverSocket.accept();
            System.out.println("a new socket client connection, port : "
                    + client.getPort());
            InputStream inputStream = client.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(
                    new InputStreamReader(inputStream));
            System.out.println(bufferedReader.readLine());
        } catch (IOException e) {
            System.out.println("Socket Exception " + e.getMessage());
        } finally {
            try {
                if (serverSocket != null) {
                    serverSocket.close();
                }
            } catch (IOException e) {
                System.out.println("Socket Exception " + e.getMessage());
            }
        }
    }
}

我们运行以上程序

 

首先我们发现程序并没有结束,而且那么多输出,在第一个输出的位置停了下来,这也就证明

Socket client = serverSocket.accept();

是阻塞的,回想之前IO基础,我们对主线程执行流程进行拆解

 

我们在服务器上编译运行这个Java程序

[root@edu javaTest]# javac BioBaseServer.java
//strace表示对某个进程进行线程监控,并以文件的形式输出出来
[root@edu javaTest]# strace -ff -o out java BioBaseServer

运行,找到主线程文件,并查看关键信息

得到的系统打印如下: 

 

socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 5
......
bind(5, {sa_family=AF_INET6, sin6_port=htons(9090), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0
listen(5, 50)                           = 0
write(1, "create socket, listen 9090", 26) = 26
write(1, "\n", 1)                       = 1
......
poll([{fd=5, events=POLLIN|POLLERR}], 1, -1

第一步:创建一个socket

 

第二步:bind,将这个文件描述符绑定9090端口

第三步:listen,监听这个端口

以上三步,对应了代码的

serverSocket = new ServerSocket(9090);

第四步:发起write系统调用,打印文字到控制台

第五步:阻塞,即poll,poll的作用是把当前的文件指针挂到等待队列。对应前边阻塞不消耗CPU资源,等待队列等待触发,不需要消耗CPU资源。现在开始我们手动跟程序建立一个连接

nc localhost 9090

先看控制台

 

第二个输出语句进行了输出,然后又阻塞了

然后继续查看线程文件的系统打印

poll([{fd=5, events=POLLIN|POLLERR}], 1, -1) = 1 ([{fd=5, revents=POLLIN}])
accept(5, {sa_family=AF_INET6, sin6_port=htons(57402), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 6
......
recvfrom(6, 

首先poll阻塞不阻塞了,并调用了accept方法,此时已经跟一个客户端建立了连接,但是我们无法确定数据何时到来,所以只能等待。所以再次在revc处等待,对应了代码的

bufferedReader.readLine()

我们向客户端,发送一个“nihao”

 

控制台输出nihao,程序结束

 

对应线程日志,先接收到"nihao",然后写入到了控制台,之后调用close方法关闭。

recvfrom(6, "nihao\n", 8192, 0, NULL, NULL) = 6
write(1, "nihao", 5)                    = 5
write(1, "\n", 1)                       = 1
dup2(4, 5)                              = 5
close(5)                                = 0

因为我们没有对客户端做出回应,所以没有send,其余的是不是和之前的流程图一模一样,所以这个流程成了所有Socket网络编程的基石。

然后我们发现了一个致命的问题,由于这个程序过于简单,这个Socket长连接在执行完之后就关闭了,而且只能传输一次数据,那我们建立TCP长连接的意义何在?

回想我们真实的使用场景,那我们现在要解决几个问题:

  1. 在明确的客户端要求断开或者是超时之前,长连接要一直保持。
  2. 保持长连接的同时要保证服务端能一直接收消息。
  3. 可以同时保持多个长连接

那么为此我们需要作出的努力,对应1,服务端不主动关闭连接,并设置超时间,长时间没有数据传入则断开连接;对应2,我们要做分离,接收到连接之后,循环读取流数据;对应3,我们要实现多线程,一个线程处理一个连接。

我设置了超时时间为30秒代码如下

public class BioBaseServer {
    private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 4,
            1L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(9090);
        System.out.println("create serverSocket, listen 9090");
        while (!serverSocket.isClosed()) {
            Socket socket = serverSocket.accept();
            socket.setSoTimeout(30000);
            System.out.println("a new socket client connection: " + socket.toString());
            threadPool.execute(() -> {
                try {
                    // 接收数据、打印
                    InputStream inputStream = socket.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
                    String msg;
                    while ((msg = reader.readLine()) != null) {
                        if (msg.length() == 0) {
                            break;
                        }
                        System.out.println("client" + socket.getPort() + " send:" + msg);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        serverSocket.close();
    }
}

顺便写了个简单的客户端

public class BioBaseClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket();
        try {
            socket.connect(new InetSocketAddress("localhost", 9090), 20000);
            Scanner scanner = new Scanner(System.in);
            OutputStream outputStream = socket.getOutputStream();
            while (!socket.isClosed()) {
                System.out.println("请输入:");
                String msg = scanner.nextLine();
                outputStream.write((msg + "\n").getBytes(Charset.defaultCharset()));
                outputStream.flush();
                scanner.reset();
            }
            scanner.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            socket.close();
        }
    }
}

这里我利用了线程池,将建立连接和读取数据分离,那么此时我使用多客户端连接会发生什么?

我们先把线程池调小,进行进一步的尝试

private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 3, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1));

如图所示我开启了5个客户端,来建立Socket连接,看看会发生什么

建立4个连接后,开启第五个客户端报错了,也就是说我有多少线程才能处理多少连接,也就是说线程和连接时一对一的关系,那么线程是CPU开辟的,是要耗费资源的,那如果我有10万个连接,CPU可能直接蒙了。而生产中往往这个连接都不会少,一个mysql数据库几百个很正常,一个集群几万个都有可能,而一个线程最小16KB,JVM默认xss是1MB,恐怖!那么为了应对这种情况,NIO应运而生。

 

 

标签:BIO,socket,serverSocket,阻塞,JavaIO,详解,线程,new,out
来源: https://blog.csdn.net/MiracleWW/article/details/115145606