前语
本篇文章会对Java中的网络IO模型的概念进行解说,并给出详细的Java代码完成,主要涉及如下部分。
- BIO(同步堵塞IO模型)的概念和Java编程完成;
- Non Blocking IO(同步非堵塞IO模型)的概念和Java编程完成;
- IO多路复用的概念;
- NIO(New IO)的概念和Java编程完成。
在开始本篇文章内容之前,有一个简略的关于Socket的常识需求阐明:在进行网络通信的时候,需求一对Socket,一个运转于客户端,一个运转于服务端,一起服务端还会有一个服务端Socket,用于监听客户端的衔接。下图进行一个简略示意。
那么整个通信流程如下所示。
- 服务端运转后,会在服务端创立listen-socket,listen-socket会绑定服务端的ip和port,然后服务端进入监听状况;
- 客户端恳求服务端时,客户端创立connect-socket,connect-socket描绘了其要衔接的服务端的listen-socket,然后connect-socket向listen-socket建议衔接恳求;
- connect-socket与listen-socket成功衔接后(TCP三次握手成功),服务端会为已衔接的客户端创立一个代表该客户端的client-socket,用于后续和客户端进行通信;
- 客户端与服务端通过socket进行网络IO操作,此刻就完成了客户端和服务端中的不同进程的通信。
需求知道的便是,在客户端与服务端通信的过程中,出现了三种socket,分别是。
- listen-socket。是服务端用于监听客户端建立衔接的socket;
- connect-socket。是客户端用于衔接服务端的socket;
- client-socket。是服务端监听到客户端衔接恳求后,在服务端生成的与客户端衔接的socket。
(注:上述中的socket,能够被称为套接字,也能够被称为文件描绘符。)
正文
一. BIO
BIO,即同步堵塞IO模型。用户进程调用read时建议IO操作,此刻用户进程由用户态转换到内核态,只有在内核态中将IO操作履行完后,才会从内核态切换回用户态,这期间用户进程会一向堵塞。
BIO示意图如下。
简略的BIO的Java编程完成如下。
服务端完成
public class BioServer {
public static void main(String[] args) throws IOException {
// 创立listen-socket
ServerSocket listenSocket = new ServerSocket(8080);
// 进入监听状况,是一个堵塞状况
// 有客户端衔接时从监听状况回来
// 并创立代表这个客户端的client-socket
Socket clientSocket = listenSocket.accept();
// 获取client-socket输入流
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
// 读取客户端发送的数据
// 假如数据没准备好,会进入堵塞状况
String data = bufferedReader.readLine();
System.out.println(data);
// 获取client-socket输出流
BufferedWriter bufferedWriter = new BufferedWriter(
new OutputStreamWriter(clientSocket.getOutputStream()));
// 服务端向客户端发送数据
bufferedWriter.write("来自服务端的回来数据\n");
// 改写流
bufferedWriter.flush();
}
}
客户端完成
public class BioClient {
public static final String SERVER_IP = "127.0.0.1";
public static final int SERVER_PORT = 8080;
public static void main(String[] args) throws IOException {
// 客户端创立connect-socket
Socket connectSocket = new Socket(SERVER_IP, SERVER_PORT);
// 获取connect-socket输出流
BufferedWriter bufferedWriter = new BufferedWriter(
new OutputStreamWriter(connectSocket.getOutputStream()));
// 客户端向服务端发送数据
bufferedWriter.write("来自客户端的恳求数据\n");
// 改写流
bufferedWriter.flush();
// 获取connect-socket输入流
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(connectSocket.getInputStream()));
// 读取服务端发送的数据
String returnData = bufferedReader.readLine();
System.out.println(returnData);
}
}
BIO的问题就在于服务端在accept时是堵塞的,并且在主线程中,一次只能accept一个Socket,accept到Socket后,读取客户端数据时又是堵塞的。
二. Non Blocking IO
Non Blocking IO,即同步非堵塞IO。是用户进程调用read时,用户进程由用户态转换到内核态后,此刻假如没有体系资源数据能够被读取到内核缓冲区中,回来read失利,并从内核态切换回用户态。也便是用户进程建议IO操作后会当即得到一个操作成果。
Non Blocking IO示意图如下所示。
简略的Non Blocking IO的Java编程完成如下。
public class NonbioServer {
public static final List<SocketChannel> clientSocketChannels = new ArrayList<>();
public static void main(String[] args) throws Exception {
// 客户端创立listen-socket管道
// 管道支撑非堵塞形式和一起读写
ServerSocketChannel listenSocketChannel = ServerSocketChannel.open();
// 设置为非堵塞形式
listenSocketChannel.configureBlocking(false);
// 绑定监听的端口号
listenSocketChannel.socket().bind(new InetSocketAddress(8080));
// 在子线程中遍历clientSocketChannels并读取客户端数据
handleSocketChannels();
while (true) {
// 非堵塞方式监听客户端衔接
// 假如无客户端衔接则回来空
// 有客户端衔接则创立代表这个客户端的client-socket管道
SocketChannel clientSocketChannel = listenSocketChannel.accept();
if (clientSocketChannel != null) {
// 设置为非堵塞形式
clientSocketChannel.configureBlocking(false);
// 添加到clientSocketChannels中
// 用于子线程遍历并读取客户端数据
clientSocketChannels.add(clientSocketChannel);
} else {
LockSupport.parkNanos(1000 * 1000 * 1000);
}
}
}
public static void handleSocketChannels() {
new Thread(() -> {
while (true) {
// 遍历每一个client-socket管道
Iterator<SocketChannel> iterator = clientSocketChannels.iterator();
while (iterator.hasNext()) {
SocketChannel clientSocketChannel = iterator.next();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = 0;
try {
// 将客户端发送的数据读取到ByteBuffer中
// 这一步的操作也对错堵塞的
read = clientSocketChannel.read(byteBuffer);
} catch (IOException e) {
// 移除产生反常的client-socket管道
iterator.remove();
e.printStackTrace();
}
if (read == 0) {
System.out.println("客户端数据未安排妥当");
} else {
System.out.println("客户端数据为:" + new String(byteBuffer.array()));
}
}
LockSupport.parkNanos(1000 * 1000 * 1000);
}
}).start();
}
}
上述是Non Blocking IO的一个简略服务端的完成,相较于BIO,服务端在accept时对错堵塞的,在读取客户端数据时也对错堵塞的,但是还是存在如下问题。
- 一次只能accept一个Socket;
- 需求在用户进程中遍历一切的SocketChannel并调用read() 办法获取客户端数据,此刻假如客户端数据未准备安排妥当,那么这一次的read() 操作的开销便是浪费的。
三. IO多路复用
在上述的BIO和Non Blocking IO中,一次体系调用,只会获取一个IO的状况,而假如采纳IO多路复用机制,则能够一次体系调用获取多个IO的状况。
也便是获取多个IO的状况能够复用一次体系调用。
最简略的IO多路复用方式是依据select模型完成,过程如下。
- 在用户进程中将需求监控的IO文件描绘符(Socket)注册到IO多路复用器中;
- 履行select操作,此刻用户进程由用户态转换到内核态(一次体系调用),然后在内核态中会轮询注册到IO多路复用器中的IO是否准备安排妥当,并得到一切准备安排妥当的IO的文件描绘符列表,最终回来这些文件描绘符列表;
- 用户进程在select操作回来前会一向堵塞,直至select操作回来,此刻用户进程就获得了一切安排妥当的IO的文件描绘符列表;
- 用户进程获得了安排妥当的IO的文件描绘符列表后,就能够对这些IO进行相应的操作了。
换言之,IO多路复用中,只需求一次体系调用,IO多路复用器就能够告诉用户进程,哪些IO现已准备安排妥当能够进行操作了,而假如不选用IO多路复用,则需求用户进程自己遍历每个IO并调用accept() 或许read() 办法去判别,且一次accept() 或许read() 办法调用只能判别一个IO。
四. NIO
NIO,即New IO。关于NIO,有如下三大组件。
- channel(管道)。介于buffer(字节缓冲区)和Socket(套接字)之间,用于数据的读写操作;
- buffer(字节缓冲区)。是用户程序和channel(管道)之间进行读写数据的中心区域;
- selector(IO多路复用器)。服务端的listen-socket和client-socket,客户端的connect-socket,都能够注册在selector上,注册的时候还需求指定监听的事情,比如为listen-socket指定监听的事情为ACCEPT事情,该事情产生则表明客户端建立了衔接,还比如为client-socket指定监听的事情为READ事情,该事情产生则表明客户端发送的数据现已可读。
NIO的代码完成如下所示。
服务端完成
public class NioServer {
private static Selector selector;
public static void main(String[] args) {
try {
// 敞开并得到多路复用器
selector = Selector.open();
// 服务端创立listen-socket管道
ServerSocketChannel listenSocketChannel = ServerSocketChannel.open();
// 设置为非堵塞形式
listenSocketChannel.configureBlocking(false);
// 为管道绑定端口
listenSocketChannel.socket().bind(new InetSocketAddress(8080));
// 将listen-socket管道注册到多路复用器上,并指定监听ACCEPT事情
listenSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 获取产生的事情,这个操作是堵塞的
selector.select();
// 拿到有事情产生的SelectionKey调集
// SelectionKey表明管道与多路复用器的绑定联系
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历每个产生的事情,然后判别事情类型
// 依据事情类型,进行不同的处理
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()) {
// 处理客户端衔接事情
handlerAccept(selectionKey);
} else if (selectionKey.isReadable()) {
// 处理客户端数据可读事情
handlerRead(selectionKey);
}
}
LockSupport.parkNanos(1000 * 1000 * 1000);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handlerAccept(SelectionKey selectionKey) {
// 从事情中获取到listen-socket管道
ServerSocketChannel listenSocketChannel = (ServerSocketChannel) selectionKey.channel();
try {
// 为衔接的客户端创立client-socket管道
SocketChannel clientSocketChannel = listenSocketChannel.accept();
// 设置为非堵塞形式
clientSocketChannel.configureBlocking(false);
// 将client-socket管道注册到多路复用器上,并指定监听READ事情
clientSocketChannel.register(selector, SelectionKey.OP_READ);
// 给客户端发送数据
clientSocketChannel.write(ByteBuffer.wrap("衔接已建立\n".getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handlerRead(SelectionKey selectionKey) {
// 从事情中获取到client-socket管道
SocketChannel clientSocketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
// 读取客户端数据
int read = clientSocketChannel.read(byteBuffer);
if (read <= 0) {
// 封闭管道
clientSocketChannel.close();
// 从多路复用器移除绑定联系
selectionKey.cancel();
} else {
System.out.println(new String(byteBuffer.array()));
}
} catch (IOException e1) {
try {
// 封闭管道
clientSocketChannel.close();
} catch (IOException e2) {
e2.printStackTrace();
}
// 从多路复用器移除绑定联系
selectionKey.cancel();
e1.printStackTrace();
}
}
}
客户端完成
public class NioClient {
private static Selector selector;
public static final String SERVER_IP = "127.0.0.1";
public static final int SERVER_PORT = 8080;
public static void main(String[] args) {
try {
// 敞开并得到多路复用器
selector = Selector.open();
// 创立connect-socket管道
SocketChannel connectSocketChannel = SocketChannel.open();
// 设置为非堵塞形式
connectSocketChannel.configureBlocking(false);
// 设置服务端IP和端口
connectSocketChannel.connect(new InetSocketAddress(SERVER_IP, SERVER_PORT));
// 将connect-socket管道注册到多路复用器上,并指定监听CONNECT事情
connectSocketChannel.register(selector, SelectionKey.OP_CONNECT);
while (true) {
// 获取产生的事情,这个操作是堵塞的
selector.select();
// 拿到有事情产生的SelectionKey调集
// SelectionKey表明管道与多路复用器的绑定联系
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历每个产生的事情,然后判别事情类型
// 依据事情类型,进行不同的处理
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isConnectable()) {
// 处理衔接建立事情
handlerConnect(selectionKey);
} else if (selectionKey.isReadable()) {
// 处理服务端数据可读事情
handlerRead(selectionKey);
}
}
LockSupport.parkNanos(1000 * 1000 * 1000);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handlerConnect(SelectionKey selectionKey) throws IOException {
// 拿到connect-socket管道
SocketChannel connectSocketChannel = (SocketChannel) selectionKey.channel();
if (connectSocketChannel.isConnectionPending()) {
connectSocketChannel.finishConnect();
}
// 设置为非堵塞形式
connectSocketChannel.configureBlocking(false);
// 将connect-socket管道注册到多路复用器上,并指定监听READ事情
connectSocketChannel.register(selector, SelectionKey.OP_READ);
// 向服务端发送数据
connectSocketChannel.write(ByteBuffer.wrap("客户端发送的数据\n".getBytes()));
}
private static void handlerRead(SelectionKey selectionKey) {
// 拿到connect-socket管道
SocketChannel connectSocketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
// 读取服务端数据
int read = connectSocketChannel.read(byteBuffer);
if (read <= 0) {
// 封闭管道
connectSocketChannel.close();
// 从多路复用器移除绑定联系
selectionKey.cancel();
} else {
System.out.println(new String(byteBuffer.array()));
}
} catch (IOException e1) {
try {
// 封闭管道
connectSocketChannel.close();
} catch (IOException e2) {
e2.printStackTrace();
}
// 从多路复用器移除绑定联系
selectionKey.cancel();
e1.printStackTrace();
}
}
}
总结
本篇文章中所评论的IO模型,都是同步IO模型,而何谓同步异步,何谓堵塞非堵塞,能够总结如下。
- 同步和异步。同步IO表明需求在用户进程中自动的去问询操作体系数据是否准备好,假如没有准备好,还需求持续的问询,直到数据准备好停止;而异步IO则是在用户进程中只需求问询操作体系一次,后续数据准备好后操作体系会自动的将数据给到用户进程;
- 堵塞和非堵塞。堵塞IO便是建议一次体系调用后,会一向堵塞直到有成果回来;而非堵塞IO便是建议一次体系调用后,会当即得到一个回来成果。
实际上在Java中是有对异步IO(AIO)做支撑,但是AIO依赖操作体系的底层完成,而目前Linux对AIO的支撑不成熟,所以AIO的运用并不多,像主流的网络使用结构Netty也都没有运用到AIO。