几个概念

阻塞IO非阻塞IO 这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题:前者等待;后者继续执行(但是使用线程一直轮询,直到有IO资源准备好了)。

同步IO异步IO,这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何响应程序的问题:前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。

同步阻塞模式(Blocking IO)

同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时如果数据没有准备号会被阻塞。

伪代码表示如下:

{
// 阻塞,直到有数据
read(socket, buffer);
process(buffer);
}

BIO通信方式的特点

  1. 一个线程负责连接,多线程则为每一个接入开启一个线程。
  2. 一个请求一个应答。
  3. 请求之后应答之前客户端会一直等待(阻塞)。

BIO通信方式在单线程服务器下一次只能处理一个请求,在处理完毕之前一直阻塞。因此不适用于高并发的情况。不过可以使用多线程稍微改进。

Java同步阻塞模式

Java中的阻塞模式BIO,就是在java.net包中的Socket套接字的实现,Socket套接字是TCP/UDP等传输层协议的实现。

Java同步阻塞模式编码

多线程客户端

为了测试服务端程序,可以先编写一个多线程客户端用于请求测试。


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.CountDownLatch; /**
* <p>
* BIO测试
* 模拟20个客户端并发请求,服务端则使用单线程。
*
* @Author niujinpeng
* @Date 2018/10/15 10:50
*/
public class SocketClient {
public static void main(String[] args) throws InterruptedException {
Integer clientNumber = 20;
CountDownLatch countDownLatch = new CountDownLatch(clientNumber); // 分别启动20个客户端
for (int index = 0; index < clientNumber; index++, countDownLatch.countDown()) {
SocketClientRequestThread client = new SocketClientRequestThread(countDownLatch, index);
new Thread(client).start();
} synchronized (SocketClient.class) {
SocketClient.class.wait();
}
}
} /**
* <p>
* 客户端,用于模拟请求
*
* @Author niujinpeng
* @Date 2018/10/15 10:53
*/
class SocketClientRequestThread implements Runnable { private CountDownLatch countDownLatch; /**
* 线程的编号
*/
private Integer clientIndex; public SocketClientRequestThread(CountDownLatch countDownLatch, Integer clientIndex) {
this.countDownLatch = countDownLatch;
this.clientIndex = clientIndex;
} @Override
public void run() {
Socket socket = null;
OutputStream clientRequest = null;
InputStream clientResponse = null;
try {
socket = new Socket("localhost", 83);
clientRequest = socket.getOutputStream();
clientResponse = socket.getInputStream(); //等待,直到SocketClientDaemon完成所有线程的启动,然后所有线程一起发送请求
this.countDownLatch.await(); // 发送请求信息
clientRequest.write(("这是第" + this.clientIndex + "个客户端的请求").getBytes());
clientRequest.flush(); // 等待服务器返回消息
System.out.println("第" + this.clientIndex + "个客户端请求发送完成,等待服务器响应");
int maxLen = 1024;
byte[] contentBytes = new byte[maxLen];
int realLen;
String message = ""; // 等待服务端返回,in和out不能cloese
while ((realLen = clientResponse.read(contentBytes, 0, maxLen)) != -1) {
message += new String(contentBytes, 0, realLen);
}
System.out.println("第" + this.clientIndex + "个客户端接受到来自服务器的消息:" + message); } catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
try {
if (clientRequest != null) {
clientRequest.close();
}
if (clientRequest != null) {
clientResponse.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

单线程服务端

因为Java中的Socket就是BIO的模式,因此我们可以很简单的编写一个BIO单线程服务端。

SocketServer.java


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket; /**
* BIO服务端
* <p>
* 单线程阻塞的服务器端
*
* @Author niujinpeng
* @Date 2018/10/15 11:17
*/
public class SocketServer { public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(83);
try {
while (true) {
// 阻塞,直到有数据准备完毕
Socket socket = serverSocket.accept(); // 开始收取信息
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
Integer sourcePort = socket.getPort();
int maxLen = 1024 * 2;
byte[] contextBytes = new byte[maxLen]; // 阻塞,直到有数据准备完毕
int realLen = input.read(contextBytes, 0, maxLen);
// 读取信息
String message = new String(contextBytes, 0, realLen); // 输出接收信息
System.out.println("服务器收到来自端口【" + sourcePort + "】的信息:" + message);
// 响应信息
output.write("Done!".getBytes()); // 关闭
output.close();
input.close();
socket.close(); }
} catch (Exception e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
serverSocket.close();
}
}
}
}

多线程服务端

单线程服务器,在处理请求时只能同时处理一条,也就是说如果在请求到来时发现有请求尚未处理完毕,只能等待处理,因此使用多线程改进服务端。

SocketServerThread.java


import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket; /**
* BIO服务端
* <p>
* 多线程的阻塞的服务端
* <p>
* 当然,接收到客户端的socket后,业务的处理过程可以交给一个线程来做。
* 但还是改变不了socket被一个一个的做accept()的情况。
*
* @Author niujinpeng
* @Date 2018/10/15 11:17
*/
public class SocketServerThread implements Runnable { /**
* 日志
*/
private static final Logger logger = LoggerFactory.getLogger(SocketServerThread.class); private Socket socket; public SocketServerThread(Socket socket) {
this.socket = socket;
} public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(83);
try {
while (true) {
Socket socket = serverSocket.accept();
//当然业务处理过程可以交给一个线程(这里可以使用线程池),并且线程的创建是很耗资源的。
//最终改变不了.accept()只能一个一个接受socket的情况,并且被阻塞的情况
SocketServerThread socketServerThread = new SocketServerThread(socket);
new Thread(socketServerThread).start();
}
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
if (serverSocket != null) {
serverSocket.close();
}
}
} @Override
public void run() {
InputStream in = null;
OutputStream out = null;
try {
//下面我们收取信息
in = socket.getInputStream();
out = socket.getOutputStream();
Integer sourcePort = socket.getPort();
int maxLen = 1024;
byte[] contextBytes = new byte[maxLen];
//使用线程,同样无法解决read方法的阻塞问题,
//也就是说read方法处同样会被阻塞,直到操作系统有数据准备好
int realLen = in.read(contextBytes, 0, maxLen);
//读取信息
String message = new String(contextBytes, 0, realLen); //下面打印信息
logger.info("服务器收到来自于端口:" + sourcePort + "的信息:" + message); //下面开始发送信息
out.write("回发响应信息!".getBytes());
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
//试图关闭
try {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
if (this.socket != null) {
this.socket.close();
}
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
}
}

看起来多线程增加了服务能力,但是很明显多线程改进之后仍有以下局限性

  • 接收和通知处理结果的过程依旧是单线程的。
  • 系统可以创建的线程数量有限。cat /proc/sys/kernel/threads-max可以查看可以创建的线程数量。
  • 如果线程较多,CPU需要更多的时间切换,处理真正业务的时间就会变少。
  • 创建线程会消耗较多资源,JVM创建一个线程都会默认分配128KB空间。
  • 多线程也无法解决因为调用底层系统同步IO而决定的同步IO机制。

同步阻塞模式总结

BIO模式因为进程的阻塞挂起,不会消耗过多的CPU资源,而且开发难度低,比较适合并发量小的网络应用开发。同时很容易发现因为请求IO会阻塞进程,所以不时候并发量大的应用。如果为每一个请求分配一个线程,系统开销就会过大。

同时在Java中,使用了多线程来处理阻塞模式,也无法解决程序在accept()read()时候的阻塞问题。因为accept()read()的IO模式支持是基于操作系统的,如果操作系统发现没有套接字从指定的端口传送过来,那么操作系统就会等待。这样accept()read()方法就会一直等待。


本文原发于个人博客https://www.codingme.net 转载请注明出处

GitHub 源码:https://github.com/niumoo/java-toolbox

此文参考文章:5种IO模型、阻塞IO和非阻塞IO、同步IO和异步IO

此文参考文章:架构设计:系统间通信(3)——IO通信模型和JAVA实践 上篇


欢迎关注公众号与我联系!

IO通信模型(一)同步阻塞模式BIO(Blocking IO)的更多相关文章

  1. stm32中阻塞模式和非阻塞模式 in blocking mode 与 in non-blocking mode区别

    阻塞模式和非阻塞模式...... 我的理解是:阻塞模式就像是一个延时函数,当这个函数没处理完那么,所有的按照流程需要执行的代码都不会被执行,要等到这个延时完成,类似 平时看书上写的LED灯闪烁,用的d ...

  2. Linux通信之同步阻塞模式

    [参考]韦东山 教学笔记 1. 原子操作原子操作指的是在执行过程中不会被别的代码路径所中断的操作.常用原子操作函数举例:atomic_t v = ATOMIC_INIT(0); //定义原子变量v并初 ...

  3. 【Java】同步阻塞式(BIO)TCP通信

    TCP BIO 背景 网络编程的基本模型是Clien/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接 ...

  4. IO通信模型(三)多路复用IO

    多路复用IO 从非阻塞同步IO的介绍中可以发现,为每一个接入创建一个线程在请求很多的情况下不那么适用了,因为这会渐渐耗尽服务器的资源,人们也都意识到了这个 问题,因此终于有人发明了IO多路复用.最大的 ...

  5. IO通信模型(二)同步非阻塞模式NIO(NonBlocking IO)

    同步非阻塞模式(NonBlocking IO) 在非阻塞模式中,发出Socket的accept()和read()操作时,如果内核中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个信息.也 ...

  6. AIO,BIO,NIO,IO复用,同步,异步,阻塞和非阻塞

    (1)什么是NIO(Non-blocked IO),AIO,BIO (2) 区别 (3)select 与 epoll,poll区别 1.什么是socket?什么是I/O操作? 什么是socket? 实 ...

  7. I/O模型系列之三:IO通信模型BIO NIO AIO

    一.传统的BIO 网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请 ...

  8. Java IO(2)阻塞式输入输出(BIO)的字节流与字符流

    在上文中<Java IO(1)基础知识——字节与字符>了解到了什么是字节和字符,主要是为了对Java IO中有关字节流和字符流有一个更好的了解. 本文所述的输出输出指的是Java中传统的I ...

  9. Java IO(2)阻塞式输入输出(BIO)

    在上文中<Java IO(1)基础知识——字节与字符>了解到了什么是字节和字符,主要是为了对Java IO中有关字节流和字符流有一个更好的了解. 本文所述的输出输出指的是Java中传统的I ...

随机推荐

  1. 文件操作,列表实例NiceHexSpiral

    fr = open('letter.txt',mode='r',encoding='utf-8') plaincode = fr.read() print('明文:' + plaincode) pri ...

  2. h5适配的解决方案

    一. 流程 设计师以750pt×1334pt尺寸进行设计(当然高度随内容变化),最后用该尺寸的设计稿进行标注.切图,前端采用淘宝的开源方案flexible进行适配. 二. flexible使用方法 F ...

  3. Deepin下配置JDK8

    下载JDK 首先在http://www.oracle.com/technetwork/java/javase/downloads/index.html下载对应的JDK 本人下载的是JDK8 解压tar ...

  4. gym 101628

    前几天感冒了三天没怎么写题...今天好很多了打个三星场找点手感. 不行啊我好菜啊.只会8个..补题的话,再说吧.G题感觉值得一补. 补了G,K不想写B不会. 说实话这个三星场还是很新人向的,知识点也蛮 ...

  5. 10. vue axios 请求未完成时路由跳转报错问题

    axios 请求未完成时路由跳转报错问题 前两天项目基本功能算是完成了,在公司测试时遇到了遇到了一个问题,那就是在请求未完成时进行路由跳转时会报错,想了几种办法来解决,例如加loading,请求拦截, ...

  6. bootstrap table 标题列重复

    使用bootstrap table生成表格,出现一个奇怪问题,标题列重复.查了一大堆资料,没有找到可以解决问题的. 以为是类库版本的问题,全部替换成了example中的引用,还是这个问题. 后来仔细查 ...

  7. 二分(HDU2289 Cup)

    贴代码: 题目意思:已知r水的下半径,R水的上半径,H为圆台高度,V为水的体积,求水的高度,如图: 水的高度一定在0-100,所以在这个区间逐步二分,对每次二分出的水的高度,计算相应的体积,看看计算出 ...

  8. QEMU KVM Libvirt手册(11): Managing Storage

    When managing a VM Guest on the VM Host Server itself, it is possible to access the complete file sy ...

  9. JS闭包解析

    三点注意事项 JS作用域传送门 JS没有块级作用域,只有全局作用域和局部作用域(函数作用域). JS中的作用域链,内部的作用域可以访问到外部作用域中的变量和方法,而外部作用域不能访问内部作用域的变量和 ...

  10. 源码级调试的XNU内核

    i春秋翻译小组-FWorldCodeZ 源码级调试的XNU内核 无论你是在开发内核扩展,进行漏洞研究,还是还有其他需要进入macOS / iOS内核,XNU,有时你需要附加调试器.当你这样做时,使用源 ...