1.1 I/O基础入门
io的缺陷:只有输入输出流,同步阻塞(bio),导致通信线程被长时间阻塞,字符集有限,硬件可移植行不好。
1.1.1、linux的网络IO模型简介
1)阻塞io模型,默认情况下,所有文件操作都是阻塞的。2)非阻塞模型,从应用数据到内核,3)IO复用模型,进程通过一个或多个fd传递给select或poll系统调用,阻塞在select操作上,select判断是否就绪,是,立即回调rollback,4)信号驱动io模型,通过系统调用sigaction执行一个信号处理函数,5)异步io,
2.1、传统的bio编程
网络编程的基本模型是client/server模型,也就是两个进程之间进行相互通信,其中服务器提供位置信息,(地址和端口),客户端通过连接操作向服务端监听的地址发起连接,通过三次握手建立连接,双方通过socket进行通信。
我们先看看bio的通信模型图
解释:bio通信 由一个独立的acceptor的线程负责监听客户端的链接,它接受到客户端连接请求后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流应该给客户端,线程销毁,为1对1的 请求应答模型。
服务端线程和客户端线程比例呈1:1的正比关系,当线程或并发访问量增大时候,系统可能会发生线程堆栈溢出,创建失败,宕机僵死等问题。
下面是对以上预测的实战推导
2.1.2同步阻塞io创建源码分析
public class TimeServer { public static void main(String[] args) throws IOException { int port=8080; if (args != null && args.length > 0) { port = Integer.valueOf(args[0]); } ServerSocket serverSocket=null; try { serverSocket = new ServerSocket(port); System.out.println("the time server is start in port:"+port); Socket socket=null; while (true){ socket=serverSocket.accept(); new Thread(new TimeServerHandler(socket)).start(); } } catch (IOException e) { e.printStackTrace(); }finally { if(serverSocket!=null){ System.out.println("the time server close"); serverSocket.close(); serverSocket=null; } } } }
TimeServer根据传入的参数设置监听端口,如果没有入参,使用默认值8080,通过构造函数创建一个serversocket,端口合法,则监听成功,通过一个死循环来监听客户端的链接。若无客户端,则一直阻塞
public class TimeServerHandler implements Runnable{ private Socket socket; public TimeServerHandler(Socket socket) { this.socket = socket; } @Override public void run() { BufferedReader in = null; PrintWriter out = null; try { in = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); out = new PrintWriter(this.socket.getOutputStream(), true); String currentTime = null; String body = null; while (true) { body = in.readLine(); if (body == null) break; System.out.println("The time server receive order : " + body); currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date( System.currentTimeMillis()).toString() : "BAD ORDER"; out.println(currentTime); } } catch (Exception e) { if (in != null) { try { in.close(); } catch (IOException e1) { e1.printStackTrace(); } } if (out != null) { out.close(); out = null; } if (this.socket != null) { try { this.socket.close(); } catch (IOException e1) { e1.printStackTrace(); } this.socket = null; } } } }
body = in.readLine();若读到输入流的尾部,则返回null,读到非空,则对内容判断,有标志关键字,则通过printwriter发送给客户端,退出循环
启动后服务端线程一直在运行
2.1.3 同步阻塞io创建TimeClient
客户端通过socket创建,发送查询时间服务器的"QUERY TIME ORDER"指令,然后读取服务端的响应将结果打印出来,随后关闭链接,释放资源。
public class TimeClient { public static void main(String[] args) { int port = 8080; if (args != null && args.length > 0) { port = Integer.valueOf(args[0]); } Socket socket = null; BufferedReader in = null; PrintWriter out = null; try { socket = new Socket("127.0.0.1", port); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(), true); out.println("QUERY TIME ORDER"); System.out.println("Send order 2 server succeed."); String resp = in.readLine(); System.out.println("Now is : " + resp); } catch (Exception e) { //不需要处理 } finally { if (out != null) { out.close(); out = null; } if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } in = null; } if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } socket = null; } } } }
客户端
服务端
以上数据,就可发现BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足高性能、高并发接入的场景。
为了改进一线程一连接模型,后来又演进出了一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞IO,所以被称为 “伪异步”,
2.2.1伪异步io测试和分析
再以上的TimeServer新增了两行代码,其他不变,同时新增了一个线程池类,timeServerHandlerExecutepool
public class TimeServerHandlerExecutePool { private ExecutorService executor; public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) { executor = new ThreadPoolExecutor(Runtime.getRuntime() .availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue(queueSize)); } public void execute(java.lang.Runnable task) { executor.execute(task); } }
参数解释:
corePoolSize
核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true。
maximumPoolSize
线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。
keepAliveTime
非核心线程的闲置超时时间,超过这个时间就会被回收。
unit
指定keepAliveTime的单位,如TimeUnit.SECONDS。当将allowCoreThreadTimeOut设置为true时对corePoolSize生效。
workQueue
线程池中的任务队列.
常用的有三种队列,SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue。
threadFactory
线程工厂,提供创建新线程的功能。ThreadFactory是一个接口,只有一个方法
以上方法,由于线程池和消息队列都是有界的,因此客户端并发连接数多大,都不会导致服务端内存溢出,但是底层通信依然是同步阻塞模型,
总结:同步阻塞模型的io,在对socket的输入流进行读取操作的时候,它会一直阻塞直到发生,有数据可读或,可用数据已经读取完毕,或发生空指针或io异常,才会结束,但是在网络传送上,我们无法保证网络状况和对端应用程序足够快,所以,同步阻塞的消息传送无法满足。