认识自己的无知是认识世界最可靠的方法————无名之辈
序
当看到博客的时候就已经完成了一次网络会话,数据的传输依赖于网络通信和操作系统级别的IO操作。对于Java开发而言我们常用的IO模型:BIO、NIO、AIO 是对网络模型中的传输层进行了封装。为了更好的理解IO模型本身的逻辑,小胖决定按照BIO的方式活一天。
BIO 工作机理
服务端:ServerSocket注册端口,调用accept方法等待客户端的请求,如果一直没有客户端的连接就一直等待。 客户端: 通过Socket对象实现和ServerSocket连接,获取输出流将信息输出。 服务端:accept接受到客户端的连接,获取Socket对象,得到输入流,也就获取到了客户端的消息,到此一次通信结束。
被BIO控制的小胖
理解BIO的关键在于怎么理解同步和阻塞两个词。
同步 是消息通信机制的一个概念,指的是在发出消息之后一直等待消息的返回,如果消息不返回就一直等。
阻塞 是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
举个栗子来说:
上午12:00 小胖现在手上有三件事情要做:
- 点外卖(干饭的时间到了,等外卖大概30分钟,吃饭10分钟)
- 烧热水放到保温瓶(要冲杯奶茶午休起来喝,大概10分钟)
- 整理桌面(中午在桌子上午休,大概2分钟)
如果小胖按照BIO的原理来执行:
- 小胖打开app点了最喜欢的腰果鸡丁,此时小胖什么都不干,一直等外卖送到,外卖到了就开始吃饭,吃好饭现在是12:40分了。
- 小胖开始去准备冲奶茶的热水,热水开了倒进保温瓶。现在是12:50分。
- 小胖开始整理桌面,准备午休,完事之后是12:52。
分析这一过程: 小胖是一个线程,点外卖之后线程就一直处于等待的状态,等待的时间其实可以把去烧热水和整理桌面的,干等是无意义的,或许这就是小胖作为干饭人的执著吧。
Java 情景模拟
定义一个客户端,客户端从键盘输入表示和小胖交流,告诉小胖需要执行的任务
public class Client {
public static void main(String[] args) throws Exception{
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream outputStream = socket.getOutputStream();
PrintStream ps = new PrintStream(socket.getOutputStream());
Scanner scanner = new Scanner(System.in);
while (true){
System.out.print("please input:");
String msg = scanner.nextLine();
ps.println(msg);
ps.flush();
}
}
}
定义服务端,就是小胖本人了,任务进来就需要小胖处理,睡眠时间当做处理任务。
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(9999);
System.out.println("我是小胖,我准备好了");
while (true){
Socket socket = serverSocket.accept();
try{
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String msg;
while ((msg = bufferedReader.readLine()) != null){
System.out.println(msg);
System.out.println("正在:"+msg);
Thread.sleep(1000*10);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
运行程序,首先启动服务端,再启动客户端,在客户端分别输入上述三个任务,没有执行完当前任务,下个任务进来就会阻塞。线程同步到当前任务执行完毕。(ps: 执行一下代码印象会更深刻哦)
小胖优化 BIO
因为小胖午休时间不够,下午没精力搬砖,深夜0点才下班。小胖想出了优化模型的方案:
- 12:00-12:03 点完外卖,去烧水(耗时3分钟)
- 12:03-12:05 开始整理桌面,12:05 完成桌面整理,任务3结束
- 12:05-12:10 热水准备好了,任务2结束
- 12:10-12:30 等待...
- 12:30-12:40 干饭,任务1结束
Java 情景模拟
添加一个处理任务的线程类,线程随机设置睡眠时间,模拟任务的执行过程。
public class ServerThreadReader extends Thread {
private Socket socket;
ServerThreadReader(Socket socket){
this.socket = socket;
}
@Override
public void run(){
try{
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String msg;
while ((msg = bufferedReader.readLine()) != null){
int time = (int)(10+Math.random()*(20-10+1));
System.out.println("执行任务:"+msg+",耗时: "+time);
Thread.sleep(1000*time);
System.out.println(msg+" 执行完毕,"+" 耗时: "+time);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
服务端接到任务之后就交给线程处理
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(9999);
System.out.println("我是小胖,我准备好了...");
while (true){
Socket socket = serverSocket.accept();
new ServerThreadReader(socket).start();
}
}
}
客户端模输入任务
public class Client {
public static void main(String[] args) throws Exception{
Socket socket = new Socket("127.0.0.1", 9999);
PrintStream ps = new PrintStream(socket.getOutputStream());
Scanner scanner = new Scanner(System.in);
while (true){
System.out.print("please input:");
String msg = scanner.nextLine();
ps.println(msg);
ps.flush();
}
}
}
首先运行服务端,让小胖准备接收任务,之后开启三个客户端,模拟任务同时下达到的效果,运行结果如下,可见三个任务在19s的时候都完成了。
分析: 小胖还是那个可爱的小胖,并没有三个小胖,只不过小胖将等待的时间去处理了别的任务。抽象一下就如下图所示:一个服务端,每进来一个任务都会创建一个线程,去执行对应的任务。
特点:
- 每个Socket连接到服务端,服务端都会创建一个线程,每个线程都会占用CPU资源,线程的竞争和切换上下文影响性能。
- 并不是每个Socket都进行IO操作,存在无意义的线程处理。
- 客户端的并发增加时,服务端呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,创建线程失败,最终导致服进程宕机,从而不能对外提供服务。
小胖的分身(伪异步I/O)
加入线程池技术,防止客户端增加服务器呈现1:1的线程开销
中午只做3事情对于小胖来说还是轻松搞定的,但是回到网络模型本身,服务不可能只提供给三个客户端使用,如果连接的客户端是300台呢? 相当于小胖要短时间内处理300件事情,小胖不禁摸了摸自己的肾,想起还没造小人的自己... 小胖午休前在脑海里想:要是我能分身就好了,分身出50个自己,每个人处理6件事情,或许还有指望... 迷糊中小胖看见一群人在有条不紊的工作,定眼一看,嚯哦,竟然是自己的分身,怪不得远看上去就那么可爱。团结就是力量,干起活来真飒,小胖的分身陆续的就回到了体内,30分钟后小胖从梦中惊醒,嘘了口气,还好只是一场梦。
帮小胖圆梦
分身就是服务端,分身的数量有限,当任少于分身的时候分身会陆续的回到体内,调度分身的过程用一个线程池去维护,再合适不过了,任务放到线程池里面,有空闲的分身就去执行。模拟的话数据量设置小些,重点在实现思想。
public class HandlerSocketServerPool {
private ExecutorService executorService;
/**
* @param maxThreadNum 最大线程数
* @param queueSize 线程队列大小
*/
public HandlerSocketServerPool(int maxThreadNum, int queueSize) {
this.executorService = new ThreadPoolExecutor(3, maxThreadNum, 120, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
}
/**
* 提供一个方法提交任务到线程池的任务队列暂存,等待线程池来处理
* @param target
*/
public void execute(Runnable target){
this.executorService.execute(target);
}
}
维护分身本身,分身即一个线程。
public class ServerRunnableTarget implements Runnable {
private Socket socket;
ServerRunnableTarget(Socket socket){
this.socket = socket;
}
@Override
public void run() {
// 处理接受到的客户端通信需求
try{
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String msg;
while ((msg = bufferedReader.readLine()) != null){
System.out.println(msg);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
服务端代码,拿到线程池,接受到任务就通知线程池
public class Server {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(9999);
// 初始化一个线程池
HandlerSocketServerPool socketServerPool = new HandlerSocketServerPool(6,10);
while (true) {
Socket socket = serverSocket.accept();
Runnable target = new ServerRunnableTarget(socket);
socketServerPool.execute(target);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
客户端代码,复制布置任务
public class Client {
public static void main(String[] args) throws Exception{
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream outputStream = socket.getOutputStream();
PrintStream ps = new PrintStream(socket.getOutputStream());
Scanner scanner = new Scanner(System.in);
while (true){
System.out.print("please input:");
String msg = scanner.nextLine();
ps.println(msg);
ps.flush();
}
}
}
特点:
- 伪异步io采用了线程池实现,从而避免了为每一个请求创建独立的线程造成资源耗尽的问题,但由于底层依然采用同步阻塞模型,因此无法从根本上解决问题
- 如果单个消息处理缓慢,或者服务器线程中的全部线程都阻塞,那么后来的socket的IO消息都将在队列中排队,新的socket请求将被拒绝,客户端会发生大量连接超时。
- NIO 马上安排!
找合适的场景模拟技术属实不易,如果有帮助到您,希望能点个大拇哥,这就是我继续扯犊子的动力!