目录
Java I/O 基础
Java JDK 1.4 推出 Java NIO 之前,基于 Java 的所有 Socket 通信都采用了同步阻塞IO(Blocking IO—BIO),这种一请求一应答的通信模型简化了上层的应用开发,但是在性能和可靠性方面却存在着巨大的瓶颈。平时使用的 TCP 编程就是典型的 BIO 模型!
BIO(Blocking IO)方式导致开发人员在开发高性能的 I/O 程序时,会面临一些巨大的挑战与困难,主要问题如下:
1)没有数据缓冲区,I/O 性能存在问题
2)没有 C 或者 C++ 中的 Channel(通道)概念,只有输入和输出流
3)同步阻塞式 I/O 通信(BIO—Blocking IO),通常会导致通信线程被长时间则塞
4)支持的字符集有限,硬件可移植性不好
于是在 Java 支持异步 I/O 之前的很长一段时间内,高性能服务器开发领域一直被 C++ 和 C 语言长期占据,因为它们可以直接使用操作系统提供的异步 I/O(AIO) 能力。
当并发访问量增大、响应时间延迟增大之后,采用 Java BIO (Blocking IO)开发的服务器软件只能通过硬件的不断扩容来满足高并发和低延迟,这极大地增加了企业的成本,并且随着集群规模的不断膨胀,系统的可维护性也面临巨大的挑战,只能通过采购性能更高的硬件服务器来解决问题,这就导致了恶性循环。
正式由于 Java 传统 BIO (Blocking IO) 的拙劣表现,最终在 Java JDK 1.4 版本提供了新的 NIO(New IO) 类库,Java 也终于开始支持非阻塞 I/O。
Java NIO 发展简史
Java JDK 从 1.0 到 1.3,Java 的 I/O 类库都非常原始,很多 Unix 网络编程中的概念或者接口在 I/O 类库中都没有体现,例如 Pipe、Channel、Buffer、Selector 等。
2002 年发布 Java JDK 1.4 时,NIO(New IO) 以 JSR-51 的身份正式随 JDK 发布,新增了 java.nio 包,提供了很多进行异步 I/O 开发的 API 和类库,主要的类和接口如下:
1)进行异步 I/O 操作的缓冲区 ByteBuffer 等
2)支持异步 I/O 操作的管道 Pipe
3)进行各种 I/O 操作(异步或者同步)的 Channel ,包括 ServerSocketChannel、SocketChannel
4)多种字符集的编码能力和解码能力;
5)实现非阻塞 I/O 操作的多路复用器 Selector
6)基于流行的 Perl 实现的正则表达式类库
7)文件通道 FileChannel
JDK 1.4 新增的 NIO (New IO)类库极大地促进了基于 Java 的异步非则塞编程的发展与应用,但是仍然有不完善的地方,特别是对文件系统的处理能力不足,主要问题如下:
1)没有统一的文件属性(例如读写权限)
2)API 能力比较弱,例如目录的级联创建和递归遍历,往往需要自己实现
3)底层存储系统的一些高级 API 无法使用
4)所有的文件操作都是同步则塞调用,不支持异步文件读写操作
2011 年 7 月 28 日,Java JDK 1.7 正式发布,它的一大亮点就是将原来的 NIO(New IO) 类库进行了升级,被称为 NIO2.0。NIO2.0 由 JSR-203 演进而来,它主要提供了如下方面的改进:
1)提供了能够批量获取文件属性的 API,这些 API 具有平台无关性,不与特定的文件系统耦合。另外还提供了标准的文件系统的 SPI (Serial Peripheral Interface—串行外设接口),供各种服务提供商扩展实现
2)提供 AIO( Asynchronous IO—异步非阻塞IO)功能,支持基于文件的异步IO操作和针对网络套接字的异步操作
3)完成 JSR-52 定义的通道功能,包括对配置和多播数据报的支持等
传统 BIO 编程
1、网络编程的基本模型时 Client/Server 模型,也就是两个进程之间相互通信其中服务端提供位置信息(绑定的 IP 地址与监听的端口),客户端通过连接操作向服务器监听地址发起连接骑请求,通过三次握手连接,如果连接建立成功,双方就可以通过网络套接字(Socket) 进行通信。
2、传统的同步阻塞模型(BIO)开发中,ServerSocket 负责绑定 IP 地址,启动监听端口,Socket 负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。
BIO 通信模型的服务端通常由一个独立的 Accpetor(接受者)线程负责监听客户端的连接,它接收到客户端连接请求后为每个客户端创建一个新的线程进行单独处理,处理完成后,通过输出流返回给客户端,线程销毁,这就是典型的一请求一应答通信模型。
BIO 模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1正比关系,由于线程是 Java 虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急骤下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。
BIO 编 码:这里以一个例子进行说明,客户端往服务器发送数据,服务器回复数据。
·服务端·
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by Administrator on 2018/10/14 0014.
* 时间服务器
*/
public class TimeServer {
public static void main(String[] args) {
tcpAccept();
}
public static void tcpAccept() {
ServerSocket serverSocket = null;
try {
/**Tcp 服务器监听端口,ip 默认为本机地址*/
serverSocket = new ServerSocket(8080);
/**循环监听客户端的连接请求
* accept 方法会一直阻塞,直到 客户端连接成功,主线程才继续往后执行*/
Socket socket = null;
while (true) {
System.out.println("等待客户端连接..........");
socket = serverSocket.accept();
System.out.println("客户端连接成功..........");
/**
* 为每一个客户端连接都新开线程进行处理
*/
new Thread(new TimeServerHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
/**发生意外时,关闭服务端*/
if (serverSocket != null && !serverSocket.isClosed()) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
对每个客户端连接新开线程单独处理:
import java.io.*;
import java.net.Socket;
import java.util.Date;
/**
* Created by Administrator on 2018/10/14 0014.
* 为每个 TCP 客户端新开线程进行处理
*/
public class TimeServerHandler implements Runnable {
private Socket socket = null;
/**
* 将每个 TCP 连接的 Socket 通过构造器传入
*
* @param socket
*/
public TimeServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
DataInputStream dataInputStream = null;
DataOutputStream dataOutputStream = null;
try {
/**读客户端数据*/
InputStream inputStream = socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
String message = dataInputStream.readUTF();
System.out.println(Thread.currentThread().getName() + " 收到客户端消息:" + message);
/**往客户端写数据*/
OutputStream outputStream = socket.getOutputStream();
dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeUTF(new Date().toString());
dataOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
/**操作完成,关闭流*/
if (dataOutputStream != null) {
try {
dataOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (dataInputStream != null) {
try {
dataInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**操作完成,关闭连接,线程自动销毁*/
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
·客户端·
import java.io.*;
import java.net.Socket;
/**
* Created by Administrator on 2018/10/14 0014.
* 时间 客户端
*/
public class TtimeClient {
public static void main(String[] args) {
/**
* 3个线程模拟3个客户端
*/
for (int i = 0; i < 3; i++) {
new Thread() {
@Override
public void run() {
tcpSendMessage();
}
}.start();
}
}
/**
* Tcp 客户端连接服务器并发送消息
*/
public static void tcpSendMessage() {
Socket socket = null;
DataOutputStream dataOutputStream = null;
DataInputStream dataInputStream = null;
try {
/**
* Socket(String host, int port):
* host)被连接的服务器 IP 地址
* port)被连接的服务器监听的端口
* Socket(InetAddress address, int port)
* address)用于设置 ip 地址的对象
* 此时如果 TCP 服务器未开放,或者其它原因导致连接失败,则抛出异常:
* java.net.ConnectException: Connection refused: connect
*/
socket = new Socket("127.0.0.1", 8080);
System.out.println("连接成功.........." + Thread.currentThread().getName());
/**往服务端写数据*/
OutputStream outputStream = socket.getOutputStream();
dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeUTF("我是长城" + Thread.currentThread().getName());
dataOutputStream.flush();
/**读服务端数据*/
InputStream inputStream = socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
String message = dataInputStream.readUTF();
System.out.println("收到服务器消息:" + message);
} catch (IOException e) {
e.printStackTrace();
} finally {
/**关闭流,释放资源*/
if (dataOutputStream != null) {
try {
dataOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (dataInputStream != null) {
try {
dataInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/** 操作完毕关闭 socket*/
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
服务端输出:
等待客户端连接..........
客户端连接成功..........
等待客户端连接..........
客户端连接成功..........
等待客户端连接..........
客户端连接成功..........
等待客户端连接..........
Thread-0 收到客户端消息:我是长城Thread-2
Thread-1 收到客户端消息:我是长城Thread-0
Thread-2 收到客户端消息:我是长城Thread-1客户端输出:
连接成功..........Thread-1
连接成功..........Thread-2
连接成功..........Thread-0
收到服务器消息:Sun Oct 14 18:38:53 CST 2018
收到服务器消息:Sun Oct 14 18:38:53 CST 2018
收到服务器消息:Sun Oct 14 18:38:53 CST 2018
总 结
BIO 阻塞式编程如上所示说明完毕,BIO 主要的问题在于每当有一个新的客户端请求连接时,服务器端必须新建线程进行处理,一个线程只能处理一个客户端连接。
在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足需求。为了改进这种一个线程一个连接的模型,后来又演进出了一种通过线程池或者消息队列实现一个或者多个线程处理 N 个客户端的模型,由于它的底层通信机制仍然使用同步阻塞 I/O ,所以被称为 “伪异步”。