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,我们要实现多线程,一个线程处理一个连接。
我设置了超时时间为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