BIO编程

最原始BIO

网络编程的基本模型是C/S模型,即两个进程间的通信。

服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。

传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
最原始BIO通信模型图:

存在的问题:

  • 同一时间,服务器只能接受来自于客户端A的请求信息;虽然客户端A和客户端B的请求是同时进行的,但客户端B发送的请求信息只能等到服务器接受完A的请求数据后,才能被接受。(acceptor只有在接受完client1的请求后才能接受client2的请求)
  • 由于服务器一次只能处理一个客户端请求,当处理完成并返回后(或者异常时),才能进行第二次请求的处理。很显然,这样的处理方式在高并发的情况下,是不能采用的。

一请求一线程BIO

那有没有方法改进呢? ,答案是有的。改进后BIO通信模型图:

此种BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理没处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通宵模型。

代码演示

服务端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket; import demo.com.test.io.nio.NioSocketServer; public class BioSocketServer {
//默认的端口号
private static int DEFAULT_PORT = 8083; public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
System.out.println("监听来自于"+DEFAULT_PORT+"的端口信息");
serverSocket = new ServerSocket(DEFAULT_PORT);
while(true) {
Socket socket = serverSocket.accept();
SocketServerThread socketServerThread = new SocketServerThread(socket);
new Thread(socketServerThread).start();
}
} catch(Exception e) { } finally {
if(serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} //这个wait不涉及到具体的实验逻辑,只是为了保证守护线程在启动所有线程后,进入等待状态
synchronized (NioSocketServer.class) {
try {
BioSocketServer.class.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
} class SocketServerThread implements Runnable {
private Socket socket;
public SocketServerThread (Socket socket) {
this.socket = socket;
}
@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); //下面打印信息
System.out.println("服务器收到来自于端口:" + sourcePort + "的信息:" + message); //下面开始发送信息
out.write("回发响应信息!".getBytes());
} catch(Exception e) {
System.out.println(e.getMessage());
} finally {
//试图关闭
try {
if(in != null) {
in.close();
}
if(out != null) {
out.close();
}
if(this.socket != null) {
this.socket.close();
}
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}

客户端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.concurrent.CountDownLatch; public class BioSocketClient{
public static void main(String[] args) throws Exception {
Integer clientNumber = 20;
CountDownLatch countDownLatch = new CountDownLatch(clientNumber); // 分别开始启动这20个客户端,并发访问
for (int index = 0; index < clientNumber; index++, countDownLatch.countDown()) {
ClientRequestThread client = new ClientRequestThread(countDownLatch, index);
new Thread(client).start();
} // 这个wait不涉及到具体的实验逻辑,只是为了保证守护线程在启动所有线程后,进入等待状态
synchronized (BioSocketClient.class) {
BioSocketClient.class.wait();
}
}
} /**
* 一个ClientRequestThread线程模拟一个客户端请求。
* @author keep_trying
*/
class ClientRequestThread implements Runnable { private CountDownLatch countDownLatch; /**
* 这个线程的编号
* @param countDownLatch
*/
private Integer clientIndex; /**
* countDownLatch是java提供的同步计数器。
* 当计数器数值减为0时,所有受其影响而等待的线程将会被激活。这样保证模拟并发请求的真实性
* @param countDownLatch
*/
public ClientRequestThread(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",8083);
clientRequest = socket.getOutputStream();
clientResponse = socket.getInputStream(); //等待,直到SocketClientDaemon完成所有线程的启动,然后所有线程一起发送请求
this.countDownLatch.await(); //发送请求信息
clientRequest.write(("这是第" + this.clientIndex + " 个客户端的请求。 over").getBytes());
clientRequest.flush(); //在这里等待,直到服务器返回信息
System.out.println("第" + this.clientIndex + "个客户端的请求发送完成,等待服务器返回信息");
int maxLen = 1024;
byte[] contextBytes = new byte[maxLen];
int realLen;
String message = "";
//程序执行到这里,会一直等待服务器返回信息(注意,前提是in和out都不能close,如果close了就收不到服务器的反馈了)
while((realLen = clientResponse.read(contextBytes, 0, maxLen)) != -1) {
message += new String(contextBytes , 0 , realLen);
}
//String messageEncode = new String(message , "UTF-8");
message = URLDecoder.decode(message, "UTF-8");
System.out.println("第" + this.clientIndex + "个客户端接收到来自服务器的信息:" + message);
} catch (Exception e) { } finally {
try {
if(clientRequest != null) {
clientRequest.close();
}
if(clientResponse != null) {
clientResponse.close();
}
} catch (IOException e) { }
}
}
}

存在的问题:

  • 虽然在服务器端,请求的处理交给了一个独立线程进行,但是操作系统通知accept()的方式还是单个的。也就是,实际上是服务器接收到数据报文后的“业务处理过程”可以多线程,但是数据报文的接受还是需要一个一个的来(acceptor只有在接受完client1的请求后才能接受client2的请求),下文会验证。
  • 在linux系统中,可以创建的线程是有限的。我们可以通过cat /proc/sys/kernel/threads-max命令查看可以创建的最大线程数。当然这个值是可以更改的,但是线程越多,CPU切换所需的时间也就越长,用来处理真正业务的需求也就越少。
  • 另外,如果您的应用程序大量使用长连接的话,线程是不会关闭的。这样系统资源的消耗更容易失控。

伪异步I/O编程

为了改进这种一连接一线程的模型,我们可以使用线程池来管理这些线程,实现1个或多个线程处理N个客户端的模型(但是底层还是使用的同步阻塞I/O),通常被称为“伪异步I/O模型“。

伪异步I/O模型图:

代码演示

只给出服务端,客户端和上面相同

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import demo.com.test.io.nio.NioSocketServer; public class BioSocketServerThreadPool {
//默认的端口号
private static int DEFAULT_PORT = 8083;
//线程池 懒汉式的单例
private static ExecutorService executorService = Executors.newFixedThreadPool(60); public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
System.out.println("监听来自于"+DEFAULT_PORT+"的端口信息");
serverSocket = new ServerSocket(DEFAULT_PORT);
while(true) {
Socket socket = serverSocket.accept();
//当然业务处理过程可以交给一个线程(这里可以使用线程池),并且线程的创建是很耗资源的。
//最终改变不了.accept()只能一个一个接受socket的情况,并且被阻塞的情况
SocketServerThreadPool socketServerThreadPool = new SocketServerThreadPool(socket);
executorService.execute(socketServerThreadPool);
}
} catch(Exception e) { } finally {
if(serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} //这个wait不涉及到具体的实验逻辑,只是为了保证守护线程在启动所有线程后,进入等待状态
synchronized (NioSocketServer.class) {
try {
BioSocketServerThreadPool.class.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
} class SocketServerThreadPool implements Runnable {
private Socket socket;
public SocketServerThreadPool (Socket socket) {
this.socket = socket;
}
@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); //下面打印信息
System.out.println("服务器收到来自于端口:" + sourcePort + "的信息:" + message); //下面开始发送信息
out.write("回发响应信息!".getBytes());
} catch(Exception e) {
System.out.println(e.getMessage());
} finally {
//试图关闭
try {
if(in != null) {
in.close();
}
if(out != null) {
out.close();
}
if(this.socket != null) {
this.socket.close();
}
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}

服务器端的执行效果

在 Socket socket = serverSocket.accept(); 处打了断点,有20个客户端同时发出请求,可服务端还是一个一个的处理,其它线程都处于阻塞状态

阻塞的问题根源

那么重点的问题并不是“是否使用了多线程、或是线程池”,而是为什么accept()、read()方法会被阻塞。API文档中对于 serverSocket.accept() 方法的使用描述:

Listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made.

服务器线程发起一个accept动作,询问操作系统 是否有新的socket套接字信息从端口xx发送过来。

注意,是询问操作系统。也就是说socket套接字的IO模式支持是基于操作系统的,那么自然同步IO/异步IO的支持就是需要操作系统级别的了。如下图:

如果操作系统没有发现有套接字从指定的端口xx来,那么操作系统就会等待。这样serverSocket.accept()方法就会一直等待。这就是为什么accept()方法为什么会阻塞:它内部的实现是使用的操作系统级别的同步IO。

  • 阻塞IO 和 非阻塞IO
    这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题:前者等待;后者继续执行(并且使用线程一直轮询,直到有IO资源准备好了)
  • 同步IO 和非同步IO
    这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何处理相应程序的问题:前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。

高级Java工程师必备 ----- 深入分析 Java IO (一)BIO的更多相关文章

  1. 高级Java工程师必备 ----- 深入分析 Java IO (二)NIO

    接着上一篇文章 高级Java工程师必备 ----- 深入分析 Java IO (一)BIO,我们来讲讲NIO 多路复用IO模型 场景描述 一个餐厅同时有100位客人到店,当然到店后第一件要做的事情就是 ...

  2. 高级Java工程师必备 ----- 深入分析 Java IO (三)

    概述 Java IO即Java 输入输出系统.不管我们编写何种应用,都难免和各种输入输出相关的媒介打交道,其实和媒介进行IO的过程是十分复杂的,这要考虑的因素特别多,比如我们要考虑和哪种媒介进行IO( ...

  3. 我最推荐的一张Java后端学习路线图,Java工程师必备

    前言 学习路线图往往是学习一样技术的入门指南.网上搜到的Java学习路线图也是一抓一大把. 今天我只选一张图,仅此一图,足以包罗Java后端技术的知识点.所谓不求最好,但求最全,学习Java后端的同学 ...

  4. Java工程师必备

    Java工程师必备 JAVA基础扎实,熟悉JVM,熟悉网络.多线程.分布式编程及性能调优 精通Java EE相关技术 熟练运用Spring/SpringBoot/MyBatis等基础框架 熟悉分布式系 ...

  5. java基础( 九)-----深入分析Java的序列化与反序列化

    序列化是一种对象持久化的手段.普遍应用在网络传输.RMI等场景中.本文通过分析ArrayList的序列化来介绍Java序列化的相关内容.主要涉及到以下几个问题: 怎么实现Java的序列化 为什么实现了 ...

  6. 从零开始搭建Java开发环境第一篇:Java工程师必备软件大合集

    1.JDK https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 目前主流的JDK版 ...

  7. 《Java Web开发实战》——Java工程师必备干货教材

    一年一度毕业季,又到了简历.offer漫天飞,失望与希望并存的时节.在IT行业,高校毕业生求职时,面临的第一道门槛就是技能与经验的考验,但学校往往更注重学生的理论知识,忽略了对学生实践能力的培养,因而 ...

  8. 转:Java工程师成神之路~(2018修订版)

    转: http://www.hollischuang.com/archives/489 阿里大牛珍藏架构资料,点击链接免费获取 针对本文,博主最近在写<成神之路系列文章> ,分章分节介绍所 ...

  9. Java工程师学习指南(入门篇)

    Java工程师学习指南 入门篇 最近有很多小伙伴来问我,Java小白如何入门,如何安排学习路线,每一步应该怎么走比较好.原本我以为之前的几篇文章已经可以解决大家的问题了,其实不然,因为我之前写的文章都 ...

随机推荐

  1. 小程序框架MpVue踩坑日记(一)

    小程序也做了几个小功能模块了,总觉得需要总结一下,踩坑什么的还是得记录一下啊. 好吧,其实是为了方便回顾 首先,说到小程序框架,大家都知道wepy,不过,我是没用过 美团开发团队到mpvue到是个实在 ...

  2. spring4 maven3 mybatis

    1 新建maven工程 http://www.cnblogs.com/quanyongan/archive/2013/04/21/3033838.html 如果在第三步中出现错误,比如类似: Coul ...

  3. 【Android】第三方库使用的问题集

    Google/百度地图Key的获取 百度地图UnsatisfiedLinkError错误 async-http-client中的FATAL EXCEPTION Google/百度地图Key的获取 无论 ...

  4. 【转】win7 任务计划 任务映像已损坏或篡改(异常来自HRESULT:0x80041321)

    请这样操作:1. 以管理员身份运行命令提示符并执行命令chcp 437schtasks /query /v | find /i "ERROR: Task cannot be loaded:& ...

  5. Android UI开发第四十三篇——使用Property Animation实现墨迹天气3.0引导界面及动画实现

    前面写过<墨迹天气3.0引导界面及动画实现>,里面完美实现了动画效果,那一篇文章使用的View Animation,这一篇文章使用的Property Animation实现.Propert ...

  6. 短信计时器Utils

    package com.lvshandian.partylive.utils; import android.content.Context;import android.os.CountDownTi ...

  7. myql 5.6 安装

    环境: centos 6.5  192.168.9.28  4核4G 虚拟机 一. 安装编译源码所需要的工具和库 [root@localhost ~]# yum -y install gcc gcc- ...

  8. UFLDL教程

    http://ufldl.stanford.edu/wiki/index.php/UFLDL%E6%95%99%E7%A8%8B

  9. Pentaho BIServer Community Edtion 6.1 使用教程 第一篇 软件安装

    一.简介: Pentaho BI Server 分为企业版和社区版两个版本.其中 社区版 CE(community edtion) 为免费版本. 二.下载CE版(CentOS): 后台下载命令: no ...

  10. discuz论坛搬家

    很多站长第一次做网站的时候,无奈选择了速度不是很稳定的空间,慢慢会发现有很多物美价廉速度相当快的空间 这个时候,站长在网站搬家的过程中就会遇到很多困难,今天老袋鼠给大家详细讲解一下discuz论坛搬家 ...