文章目录
一、传统的BIO编程
网络编程的基本模型是Clinet/Server
模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket
)进行通信。
在基于传统同步阻塞模型开发中,ServerSocket
负责绑定IP地址,启动监听端口,Socket
负责发起连接操作,连接成功之后,双方通过输入流和输出流进行同步阻塞式通信。
下面,我们就以经典的时间服务器(TimeServer)为例,通过代码分析来回顾和熟悉BIO编程。
采用BIO通信模型的服务端,通常由一个独立的Acceptor
(接收器)线程负责监听客户端的连接,它接收到客户端连接请求之后为每一个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流
返回应答给客户端,线程销毁,这就是经典的一请求一应答
通信模型。
1.1TimeServer
package com.lsh.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author :LiuShihao
* @date :Created in 2021/2/22 1:46 下午
* @desc : Netty权威指南
*
* 同步阻塞式I/O创建的TimeServer源码
*/
public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8888;
ServerSocket server = null;
try{
server = new ServerSocket(port);
System.out.println("The time server is start in port:"+port);
Socket socket = null;
while (true){
socket = server.accept();
new Thread(new TimeServerHandler(socket)).start();
}
} finally {
if (server != null){
System.out.println("The time server close");
server.close();
server = null;
}
}
}
}
1.2 TimeServerHandler
package com.lsh.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Date;
/**
* @author :LiuShihao
* @date :Created in 2021/2/22 2:00 下午
* @desc :同步阻塞I/O的TimeServerHandler
* 当有新的客户端接入的时候,以Socket为参数构造TimeServerhandler对象,TimerServerHandler是一个Runnable,
* 使用它为构造函数的参数创建一个新的客户端线程来处理这条Socket链路。
*
*/
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;
/**
* 通过BufferReader读取一行,如果已经读到了输入流的尾部,则返回值为null,退出循环,
* 如果读到非空值,则对内容进行判断,
* 如果请求消息为查询时间的指令"QUERY TIME ORDER" ,则获取当前系统最新的系统时间,通过PrintWriter的println的方法发送给客户端
* 最后退出循环
* 释放输入流、输出流和Socket套接字句柄资源
*/
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 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 {
socket.close();
}catch (IOException e1){
e1.printStackTrace();
}
this.socket = null;
}
}
}
}
1.3 TimeClient
package com.lsh.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* @author :LiuShihao
* @date :Created in 2021/2/22 4:20 下午
* @desc :同步阻塞式I/O创建的TimeClient源码
*
* 客户端通过socket创建,发送查询时间服务器的"QUERY TIME ORDER"指令,
* 然后读取服务端的响应并将结果打印出来,随后关闭连接,释放资源,程序退出执行
*
*/
public class TineClient {
public static void main(String[] args) {
int port = 8888;
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
/**
* 通过PrintWirter向服务器端发送"QUERY TIME ORDER"指令
* 然后通过BufferReader的readLine读取响应并打印
*/
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("Sned 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;
}
}
}
}
1.4 演示
首先启动TimeServer,然后启动TimeClient。
TimeServer
启动后通过server.accept()
方法等待客户端连接。TimeClient
启动后通过IP地址和端口号连接TimeServer。TimeServer
获得客户端连接后,创建一个新的线程进行通信。TimeClient
通过PrintWriter
输出"QUERY TIME ORDER"到服务器端TimeServer
通过BufferReader
读取输入流信息,返回时间信息在通过PrintWriter
写出。TimeClient
在通过BufferReader
读取输入流输出。
1.5 BIO总结
BIO的主要问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。
当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出
、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。
在高性能服务器应用领域,往往需要面对成千上万个客户端的并发连接,这种模型显然不能满足高性能、高并发连接的特点、
二、伪异步I/O编程
为了解决同步阻塞IO面临的一个链路需要一个线程处理的问题,后来有人对它进行了优化:
后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远大于N。通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
当有新的客户端接入时:将客户端的Socket封装成一个Task(java.lang.Runnable接口)投递到后端的线程池进行处理,JDK的线程池维护一个消息队列和N个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
2.1 对TimeServer代码进行改造
package com.lsh.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author :LiuShihao
* @date :Created in 2021/2/22 1:46 下午
* @desc : Netty权威指南 伪异步IO编程
* 伪异步IO编程TimeServer源码
*/
public class TimeServer2 {
public static void main(String[] args) throws IOException {
int port = 8888;
ServerSocket server = null;
try{
server = new ServerSocket(port);
System.out.println("The time server is start in port:"+port);
Socket socket = null;
//创建IO任务线程池
TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(50, 10000);
while (true){
socket = server.accept();
singleExecutor.execute(new TimeServerHandler(socket));
}
} finally {
if (server != null){
System.out.println("The time server close");
server.close();
server = null;
}
}
}
}
首先创建了一个时间服务器处理类的线程池,当接收到新的客户端连接时,将请求Socket封装成一个Task,然后调用线程池的execute
方法执行,从而避免了每一请求接入都创建一个新的线程。
2.2 TimeServerHandlerExecutePool
package com.lsh.bio;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author :LiuShihao
* @date :Created in 2021/2/23 12:40 下午
* @desc :伪异步IO编程 创建一个IO任务线程池
*/
public class TimeServerHandlerExecutePool {
private ExecutorService executor;
//创建一个线程池
public TimeServerHandlerExecutePool(int maxPollSize, int queueSize) {
executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
maxPollSize,120L, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(queueSize));
}
//执行
public void execute(Runnable task) {
executor.execute(task);
}
}
由于线程池和消息队列的创建都是有界的,因此无论客户端并发连接数多大,它都不会导致线程个数膨胀和内存溢出,相比于传统的一连接一线程模型,是一种改良。
2.3 TimeServerHandler
代码不变
2.4 TimeClient
代码不变,创建两个客户端:
package com.lsh.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* @author :LiuShihao
* @date :Created in 2021/2/22 4:20 下午
* @desc :同步阻塞式I/O创建的TimeClient源码
*
* 客户端通过socket创建,发送查询时间服务器的"QUERY TIME ORDER"指令,
* 然后读取服务端的响应并将结果打印出来,随后关闭连接,释放资源,程序退出执行
*
*/
public class TineClient1 {
public static void main(String[] args) {
int port = 8888;
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
/**
* 通过PrintWirter向服务器端发送"QUERY TIME ORDER"指令
* 然后通过BufferReader的readLine读取响应并打印
*/
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("TineClient1 Sned 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;
}
}
}
}
package com.lsh.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* @author :LiuShihao
* @date :Created in 2021/2/22 4:20 下午
* @desc :同步阻塞式I/O创建的TimeClient源码
*
* 客户端通过socket创建,发送查询时间服务器的"QUERY TIME ORDER"指令,
* 然后读取服务端的响应并将结果打印出来,随后关闭连接,释放资源,程序退出执行
*
*/
public class TineClient2 {
public static void main(String[] args) {
int port = 8888;
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
/**
* 通过PrintWirter向服务器端发送"QUERY TIME ORDER"指令
* 然后通过BufferReader的readLine读取响应并打印
*/
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("TineClient2 Sned 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;
}
}
}
}
2.5 伪异步I/O的弊端分析
伪异步I/O通信框架采用了线程池的实现,因此避免了为每个请求都创建一个独立线程造成线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。
要对伪异步I/O的弊端进行深入分析,首先我们看两个Java同步IO的API说明。
Java输入流InputStream
:
当对Socket的输入流进行读取
操作的时候,它会一直阻塞下去,直到发生如下三件事:
- 有数据可读;
- 可用数据已经读取完毕;
- 发生空指针或者I/O异常;
这意味着当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间的阻塞,如果对方需要60s才能将数据发送完成,读取一方的I/O线程也将会被同步阻塞60s,在此期间其他接入消息只能在消息队列中排队。
Java输入流OutputStream
:
当调用OutputStream
的write
方法写
输出流的时候,它将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。(学习过TCP/IP协议的应该知道,当消息的接受方处理缓慢的时候,将不能及时地从TPC缓冲区读取数据,这将会导致发送方的TCP window size
不断减小(),直到为0
。)双方处于Keep-alive状态,消息发送方将不能在向TCP缓冲区写入消息,这时如果采用的是同步阻塞I/O,write
操作将会被无限期阻塞,直到TCP 的window sizw 大于0或者发生异常。
ps:TCP header中有一个Window Size字段,它其实是指接收端的窗口,即接收窗口,用来告知发送端自己所能接收的数据量,从而达到一部分流控的目的。
通过对输入和输出流的API文档的分析,我们了解到读和写的操作都是同步阻塞的
,阻塞的时间取决于对方I/O线程的处理速度和网络I/O的传输速度
。
伪异步I/O实际上仅仅只是对之前的I/O线程模型的一个简单优化,它无法从根本上解决同步I/O导致的通信线程阻塞问题。