1、传统的BIO编程
网络编程的基本模型是Client/Server模型,也就是两个进程间相互通信。其中,服务端提供位置信息(绑定的IP地址和监听端口),客户端提供连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。
1.1、BIO通信模型图
BIO的服务端通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这是典型的一请求一应答通信模型。
该模型的最大问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系。随着并发访问量的增大,系统的性能将急剧下降,甚至发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或僵死,不能对外提供服务。
1.2、具体代码分析
TimeServer
package com.csdn.test.io.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO 服务端
*/
public class TimeServer
{
private static final int DEFAULT_LISTEN_PORT = 8080;
public static void main(String args[]) throws IOException
{
// 1、设置接收数据的端口
int port = DEFAULT_LISTEN_PORT;
if (null != args && args.length > 0)
{
try
{
port = Integer.parseInt(args[0]);
}
catch(NumberFormatException e)
{
port = DEFAULT_LISTEN_PORT;
}
}
// 2、启动socket erver
ServerSocket server = null;
try
{
// 创建服务端
server = new ServerSocket(port);
System.out.println("Server start in port: " + port);
Socket socket = null;
while(true)
{
// 3、启动服务端,接收客户端连接
// Listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made
socket = server.accept();
// 一个连接启动一个线程,无法应对大量的连接
new Thread(new TimeServer.TimeServerHandler(socket)).start();
}
}
finally
{
if (null != server)
{
System.out.println("Server close");
server.close();
server = null;
}
}
}
public static 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
{
// 1、 获取输入
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
// 2、构建输出, 第二个参数代表是否自动进行Flush动作
out = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while(true)
{
String retStr = "Bad in data";
body = in.readLine();
if (null == body)
{
// 3、非法输入,返回数据
out.print(retStr);
break;
}
System.out.println("Receive : " + body);
// 4、返回数据
out.println("Current Time :" + System.currentTimeMillis() +", Receive data: " + body);
}
}
catch(IOException e)
{
// 5、捕获异常,释放资源
e.printStackTrace();
}
finally
{
// 5、最终统一释放资源
close(in, out, this.socket);
}
}
private void close(BufferedReader in, PrintWriter out, Socket socket)
{
// 关闭输入流
if (null != in)
{
try
{
in.close();
}
catch (IOException e1)
{
e1.printStackTrace();
}
}
// 关闭输出流
if (null != out)
{
out.close();
out = null;
}
// 关闭socket
if (null != socket)
{
try
{
socket.close();
}
catch (IOException e1)
{
e1.printStackTrace();
}
socket = null;
}
}
}
}
TimeClient
package com.csdn.test.io.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class TimeClient
{
private static final int DEFAULT_SEND_PORT = 8080;
private static final String CONNECT_IP = "127.0.0.1";
private static final String TEST_SEND_STR = "client send str : str1";
public static void main(String args[]) throws IOException
{
// 1、设置接收数据的端口
int port = DEFAULT_SEND_PORT;
if (null != args && args.length > 0)
{
try
{
port = Integer.parseInt(args[0]);
}
catch(NumberFormatException e)
{
port = DEFAULT_SEND_PORT;
}
}
// 2、发送数据到服务端
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try
{
// 3、与客户端建立socket连接
socket = new Socket(CONNECT_IP, port);
// 4、 获取输入
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 5、构建输出, 第二个参数代表是否自动进行Flush动作
out = new PrintWriter(socket.getOutputStream(), true);
// 6、向服务端发送数据
String sendStr = TEST_SEND_STR;
out.println(sendStr);
System.out.println("Send string: " + sendStr);
// 7、收取回来的数据
String resp = in.readLine();
System.out.println("Receive response: " + resp);
}
catch(IOException e)
{
e.printStackTrace();
}
finally
{
close(in, out, socket);
}
}
private static void close(BufferedReader in, PrintWriter out, Socket socket)
{
// 关闭输入流
if (null != in)
{
try
{
in.close();
}
catch (IOException e1)
{
e1.printStackTrace();
}
}
// 关闭输出流
if (null != out)
{
out.close();
out = null;
}
// 关闭socket
if (null != socket)
{
try
{
socket.close();
}
catch (IOException e1)
{
e1.printStackTrace();
}
socket = null;
}
}
}
可以看出,BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。
为了改进这种情况,后面演进了一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型,由于它的底层通信机制仍然使用同步阻塞I/O,所以称为”伪异步”。
2、伪异步I/O编程
2.1、模型
采用线程池和任务队列可以实现一种叫做伪异步的I/O通信框架。当有新的客户端接入,将客户端的Socket封装成一个Task投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理。
2.2、具体代码分析
TimeServer (TimeClient代码同上,没有改变)
package com.csdn.test.io.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 伪异步I/O服务端
*/
public class TimeThreadPoolServer
{
private static final int DEFAULT_LISTEN_PORT = 8080;
public static void main(String args[]) throws IOException
{
// 1、设置接收数据的端口
int port = DEFAULT_LISTEN_PORT;
if (null != args && args.length > 0)
{
try
{
port = Integer.parseInt(args[0]);
}
catch(NumberFormatException e)
{
port = DEFAULT_LISTEN_PORT;
}
}
// 2、启动socket erver
ServerSocket server = null;
try
{
// 创建服务端
server = new ServerSocket(port);
System.out.println("Server start in port: " + port);
Socket socket = null;
// 创建I/O任务线程池
TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(50, 10000);
while(true)
{
// 3、启动服务端,接收客户端连接
// Listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made
socket = server.accept();
// 线程池中执行连接任务,数量巨大的连接时,不会使JVM资源耗尽崩溃
singleExecutor.execute(new TimeThreadPoolServer.TimeServerHandler(socket));
}
}
finally
{
if (null != server)
{
System.out.println("Server close");
server.close();
server = null;
}
}
}
public static class TimeServerHandlerExecutePool
{
private ExecutorService executor;
public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize)
{
executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), // 核心线程数
maxPoolSize, // 最大线程数
120l, // keepAliveTime 销毁线程时长
TimeUnit.SECONDS, // keepAliveTime 时长单位
new ArrayBlockingQueue<Runnable>(queueSize) // 任务队列
);
}
public void execute(Runnable task)
{
executor.execute(task);
}
}
public static 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
{
// 1、 获取输入
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
// 2、构建输出, 第二个参数代表是否自动进行Flush动作
out = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while(true)
{
String retStr = "Bad in data";
body = in.readLine();
if (null == body)
{
// 3、非法输入,返回数据
out.print(retStr);
break;
}
System.out.println("Receive : " + body);
// 4、返回数据
out.println("Current Time :" + System.currentTimeMillis() +", Receive data: " + body);
}
}
catch(IOException e)
{
// 5、捕获异常,释放资源
e.printStackTrace();
}
finally
{
// 5、最终统一释放资源
close(in, out, this.socket);
}
}
private void close(BufferedReader in, PrintWriter out, Socket socket)
{
// 关闭输入流
if (null != in)
{
try
{
in.close();
}
catch (IOException e1)
{
e1.printStackTrace();
}
}
// 关闭输出流
if (null != out)
{
out.close();
out = null;
}
// 关闭socket
if (null != socket)
{
try
{
socket.close();
}
catch (IOException e1)
{
e1.printStackTrace();
}
socket = null;
}
}
}
}
伪异步I/O弊端分析:
采用线程池实现,避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于底层通信依然采用同步阻塞模型,因此无法从根本解决问题。
这里着重关注下
A、对Socket的输入流进行读取操作(InputStream->read)的时候,它会一直阻塞下去,直到发生如下三种事件:
1、有数据可读。
2、可用数据已经读取完毕。
3、发生空指针或者I/O异常。
B、对输出流进行分析,当调用(OutputStream->write)方法写输出流时,它将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。如果接收方处理缓慢,不能及时从TCP缓冲区读取数据,将导致发送方的TCP window size不断减小,直到为0,双方处于Keep-Alive状态,消息发送方将不能再向TCP缓冲区写入消息,如果采用同步阻塞I/O,write操作将被无限期阻塞。
3、NIO编程 AIO编程
待续...
参考:
《Netty权威指南》