上一篇博文我们讲了非阻塞模式的IO。以Socket通信为例,总结来说,非阻塞IO就是通过为ServerSocket的accept方法和Socket的read方法设置等待时间,避免应用程序在获取连接和读取数据的时候一直等待。但是这种方法虽然解决了程序级别的阻塞,但操作系统底层的操作还是“同步”的,所以并没有彻底解决“等待”问题。
本文要介绍的仍旧是一种同步通信模型,多路复用IO。java中对多路复用IO的支持是通过Channel,Selector,Buffer这三个核心因素实现的(这也就是java中的NIO(new IO))。
1.Channel:应用程序和操作系统交互事件,传递内容的通道(所以多路复用IO模型需要操作系统支持)。应用程序可以通过通道向操作系统写数据,也可以通过通道从操作系统读取数据。不管是读数据还是写数据都是通过Buffer来操作的。(我的个人理解:一般的Socket通信,服务端会先创建ServerSocket,然后调用ServerSocket对象的socket方法获取对应的Socket对象,对应过来就是一个Channel就可以看做是一个盛放ServerSocket和Socket的容器)(每个通道都有一个文件状态描述符。所谓文件描述符,是计算机科学的一个术语,表示指向文件的引用的抽象化概念。文件描述符在表现形式上是一个非负整数。实际上,它是一个索引值。文件描述符适合于Unix,Linux这样的操作系统,一般程序的编写不会涉及)。
JDK中的channel有以下这些:
可以看到Channel是一个接口,它提供了两个方法,一个close方法,用于关闭一个通道;一个isOpen方法,用于返回当前通道是开放或关闭。
常用到的有ServerSocketChannel(应用服务器程序的监听通道,只有通过这个通道,应用程序才能向操作系统注册支持多路复用IO的端口监听,同时支持TCP和UDP)和SocketChannel(TCP Socket套接字的监听通道),让我们看看这两个Channel的结构。
这里说一下ServerSocketChannel中常见的一些方法。
accept方法用来获取连接到这个通道的SocketChannel。
bind方法用来向这个通道绑定一个地址,可以是本地,也可以是某个网络地址。
socket用于获取这个通道的ServerSocket对象。
validOps用来获取这个通道支持的事件,比如ServerSocketChannel支持SeletionKey.OP_ACCEPT事件,SocketChannel支持SeletionKey.OP_READ,SeletionKey.OP_WRITE,SeletionKey.OP_CONNECT事件。
可以看到SocketChannel中的大部分方法都是类似于Socket中的读和写的方法。
2.Selector:选择器,可以理解为通道管理器。我们新建一个通道后,会为通道注册一些监听事件,而这个通道管理器,就是监听通道注册的事件什么时候发生。这样就不需要程序(应用程序)通过阻塞或者非阻塞去询问操作系统,而是这个通道管理器代替程序去询问操作系统关心的事件是否发生。那么如何为通道注册关心的事件呢?如何判断通道管理器管理哪个通道呢?先看下面这幅图。
可以看到SelectableChannel类中提供了一个register(Selector,int)方法。怎么理解呢?看本篇文章中的第一幅图,可以看到常用的通道都是继承自SelectableChannel,所以要为要让一个选择器,也就是通道管理器管理一个通道,并且为通道注册关心的事件可以通过register方法来实现。如下:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
常用的通道关心的事件在SelectionKey这个类中都有定义,比如ServerSocketChannel关心的事件和SocketChannel关心的事件都在SelectionKey中有定义。
注:通道管理器只能管理继承了SelectableChannel的Channel。
还记得多路复用IO通信模型是需要操作系统支持的吗?不同操作系统的支持主要是通过不同的Selector来达到的。看下图:
我在写这篇文章的时候是在windows系统上写的,因为最初安装jdk的时候安装的就是windows版本的,所以在代码中只能看到selector在windows中的实现,即WindowsSelectorImpl这个类。在Linux中的实现类为PollSelectorImpl,EpollSelectorImpl。
3.Buffer:为了方便通过Channel读写数据,java NIO采用Buffer,而且为每一个提供读写数据功能的通道都集成了Buffer,比如SocketChannel。Buffer中有三个比较重要的概念,如下:
private int position = 0; //正在操作的数据位于缓冲区的位置
private int limit; //缓冲区最大可操作的位置
private int capacity; //缓冲区的容量
4.多路复用IO的实现方式:select,poll,epoll,kqueue。
方式 | 设计模式 | 操作系统 | java支持 |
select | 反应器(Reactor) | Windows/Linux | Windows对于同步IO的支持都是这种方式的 |
poll | 反应器 | Linux | Linux下的java NIO采用这种方式 |
epoll | 前摄器(Proactor),反应器 | Linux | Windows下有IOCP提供真正的异步支持,而Linux下用epoll模拟异步支持 |
kqueue | 前摄器 | Linux | java还没有采用这种方式 |
5.Java实例:
服务端代码:
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class MyTest {
public static void main(String[] args) throws Exception {
//创建通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//切换成非阻塞模式
serverSocketChannel.configureBlocking(false);
//获取ServerSocket
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.setReuseAddress(true);
//绑定端口
serverSocket.bind(new InetSocketAddress(8999));
//创建选择器
Selector selector = Selector.open();
//为通道注册选择器和监听事件,ServerSocketChannel只监听accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//
while(true){
if(selector.select(5000)==0){
//3秒之内没有客户端连接请求
//可以利用CPU先处理其他事情
System.out.println("没有客户端请求");
continue;
}
//表示有客户端请求
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
/***
* selector.keys()和selector.selectedKeys()是有区别的:
* selector.selectedKeys().iterator()得到的iterator在下面remove的时候不会报错,
* 而selector.keys().iterator()得到的iterator在remove的时候会报错,UnsupportedOperationException
* 这个地方涉及一个知识点:java集合权限,感兴趣的同学可以在百度UnsupportedOperationException或者Collections.unmodifiableList(List list)
* Collections.unmodifiableList(List list)是修改集合的权限
*/
try{
while(iterator.hasNext()){
//遍历每一个客户端请求
SelectionKey selectionKey = iterator.next();
//将这个客户端请求从集合中移除掉,不然这一批客户端请求处理完之后,下一批客户端请求过来的时候,还会处理一遍这批的客户端请求
iterator.remove();
//获取当前处理的这个客户端和服务端的channel
SelectableChannel selectableChannel = selectionKey.channel();
if(selectionKey.isValid()&&selectionKey.isAcceptable()){
//表示客户端连接请求已经收到
//获取该客户端和服务端连接的ServerSocketChannel,可以理解为socket通信中的ServerSocket
ServerSocketChannel serverSocketChannelNow = (ServerSocketChannel) selectableChannel;
//获取该客户端和服务端连接的SocketChannel,可以理解为socket通信中的Socket
SocketChannel socketChannel = serverSocketChannelNow.accept();
//为通道注册选择器和监听事件,SocketChannel监听read事件(也可以监听write事件和connect事件)
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("当前通道注册read事件成功");
}else if(selectionKey.isValid()&&selectionKey.isConnectable()){
//表示与客户端的连接已经建立
System.out.println("客户端与服务端连接已建立");
}else if(selectionKey.isValid()&&selectionKey.isReadable()){
//表示可以读取客户端传输的数据
//获取当前的socket通道
SocketChannel socketChannel = (SocketChannel) selectableChannel;
//获取客户端地址
InetSocketAddress inetSocketAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
//获取客户端端口
int port = inetSocketAddress.getPort();
//拿到SocketChannel的缓冲区,准备读取数据
ByteBuffer contextBytes = (ByteBuffer) selectionKey.attachment();
int length = -1;
try {
length = socketChannel.read(contextBytes);
} catch (Exception e) {
//发生异常表示客户端因为某种原因停止运行了,这时要关闭channel
socketChannel.close();
return ;
}
if(length==-1){
//表示缓冲区没有数据
return;
}
byte[] bytes = contextBytes.array();
String msgEncode = new String(bytes,"UTF-8");
String msg = URLDecoder.decode(msgEncode, "UTF-8");
System.out.println("客户端传输的内容为:"+msg);
//如果收到客户端发送“over”,则清空缓冲区,并且回传客户端处理结果
if(msg.indexOf("over")!=-1){
contextBytes.clear();
//******************************
// 真正的处理客户端请求的过程
//******************************
String sendMsg = URLEncoder.encode("返回处理结果over", "UTF-8");
ByteBuffer sendBuffer = ByteBuffer.wrap(sendMsg.getBytes());
socketChannel.write(sendBuffer);
socketChannel.close();
}else{
contextBytes.position(length);
contextBytes.limit(contextBytes.capacity());
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
}
客户端代码:
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URLDecoder;
public class Client {
public static void main(String[] args) {
Socket socket = null;
InputStream is = null;
OutputStream os = null;
try {
//客户端请求建立连接
socket = new Socket("localhost", 8997);
is = socket.getInputStream();
os = socket.getOutputStream();
byte[] buffer = new byte[1024];
String msg = "来自客户端的请求over";
buffer = msg.getBytes();
os.write(buffer);
os.flush();
byte[] receiveMsg = new byte[1024];
String str = "";
while(is.read()!=-1){
is.read(receiveMsg);
str += new String(receiveMsg);
if(str.indexOf("over")!=-1){
break;
}
}
str = URLDecoder.decode(str, "UTF-8");
System.out.println("服务器端返回的信息为:"+str);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
try {
is.close();
os.close();
socket.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
6.总结:多路复用IO还是同步IO模型,但是它在操作系统级别进行了优化,也解决了阻塞。而且一个端口可以处理多种协议。