几年前用NIO写了个仿qq软件,最近回顾时有点懵逼,当时囫囵吞枣、不求甚解,现在只能连蒙带猜,所以说时学时记真的不算浪费时间。
废话不多说,这篇博客将直接从代码的角度来解释NIO通信,用一个server端和一个client端的代码把整个NIO通信流程过一遍。详读代码,不懂再看解释,才是最正确的学习姿势。
什么是NIO
以下两张图分别是传统阻塞IO通信模型和NIO通信模型。
阻塞 I/O 的编程模型较为简单明晰,但性能相对较差。每个客户端连接进入服务器后生成一个 Socket 对象,服务器必须生成一个 Thread 对象在线程中处理这个 Socket 对象上数据的读取,即每线程对象对应与一个连接对象。当连接数增多时,仅创建大量线程,就会导致服务器非常慢甚至内存溢出。
而NIO的服务器端,并不需要对每个连接(ScoketChannel)生成一个线程处理。每个连接进入后,将连接上需要处理的事件注册在通道管理器 Selector 对象上,只需要一个线程监视,并处理对应的 I/O 事件即可。
阻塞 I/O 是主动地调用 read()方法读取数据,会一直阻塞。在 NIO 中,仅当数据到达时,处理线程被动地被触发相应的 I/O 事件。而在NIO 体系中,连接的建立、通道数据的到达(readable)、通道可写(writeable)都基于事件通知机制实现,为非阻塞。
NIOServer.java
/**
* NIOServer.java
*/
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
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 NIOServer {
private Selector selector; //通道选择器对象
public void initServer(int port) throws IOException {
//创建 ServerSocket 通道
ServerSocketChannel server = ServerSocketChannel.open();
//绑定到指定端口
server.socket().bind(new InetSocketAddress(port));
//非阻塞模型
server.configureBlocking(false);
selector = Selector.open(); //初始化通道管理器
//在通道上注册 accept 事件
server.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 监听端口
*/
public void ioAction() {
try {
while(true) { //轮询等待事件发生
//System.out.println("1.在此等待 I/O 事件发生 ...");
//select 可理解为监听系统事件发生,此处阻塞直到事件发生
selector.select();
//System.out.println("2.有 I/O 事件发生");
Iterator iter = selector.selectedKeys().iterator();
while (iter.hasNext()){
SelectionKey key = (SelectionKey) iter.next();
iter.remove(); //移除这个事件
//System.out.println("3.处理这个 I/O 进入的事件 ");;
//1.如果是一个连接,处理一个进入的连接事件,
if (key.isAcceptable()) {//是客户端的连接请求到达
//System.out.println("4.一个连接进入事件发生");
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//得到一个 Socket 通道(与客户端连接的).
SocketChannel channel = server.accept();
//设置非阻塞模式
channel.configureBlocking(false);
//发送一条欢迎消息
String outMsg="你好,欢迎测试 NIO\r\n";
ByteBuffer resBuffer=ByteBuffer.wrap(outMsg.getBytes());
channel.write(resBuffer);
//注册有数据到达时可读的事件
channel.register(selector, SelectionKey.OP_READ);
//System.out.println("有连接进入: "+channel.socket().toString());
}
//2.如果是可读取事件(一个字节到达)
else if (key.isReadable()){
//System.out.println("5.一个可读数据事件发生(客户发了数据来)");
processRead(key);
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
protected void processRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel)key.channel(); //通过当前key获得对应channel
//创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);//分配空间
int count = channel.read(buffer);
byte[] data=buffer.array();
String msg=new String(data).trim();
msg = channel.socket().getRemoteSocketAddress()+" 说:"+msg;
System.out.println("读到数据: "+msg);
//回送给客户端
ByteBuffer outBuffer=ByteBuffer.wrap(msg.getBytes());
channel.write(outBuffer);//将消息回送给客户端
}
//主函数
public static void main(String[] args) {
int port = 8888;
try {
NIOServer server = new NIOServer();
server.initServer(port);
System.out.println("Server on " + port);
server.ioAction();
} catch (IOException e) {
e.printStackTrace();
}
}
}
从main函数开始,首先指定服务器开的端口(port),并对其进行初始化(initServer)。
initServer:创建一个ServerSocketChannel,绑定到port端口,初始化通道管理器Selector,并在创建好的ServerSocketChannel上注册accept事件(同时创建一个选择键key)。
目前,服务器上拥有了一个服务器通道,一个selector和一个在selector上注册的accept事件。
紧接着开启消息循环(ioAction)。
ioAction:在轮询等待事件发生的过程中,selector阻塞等待某个通道传来的特定类型的消息(当然此时通道只有ServerSocket通道和其上的accept事件)。当客户端A请求连接时,从selector中得到所有已选择键集,然后循环处理每一个键key。通过key可以获得其通道的状态,并对对应状态进行相应的操作。例如,如果通道已准备好接受新的套接字连接(key.isAcceptable()),那么通过该键获取其通道(这里就是初始化服务器时创建的ServerSocket通道),使用accpet方法创建与客户端A的SocketChannel,然后在这个客户端A通道上注册read事件。紧接着,当客户端A通道上有可读取事件到达时(key.isReadable()),从当前键值即可获取客户端A的channel,再针对该channel做读取写入处理等,即使以后出现客户端BCDEF,也不会存在搞不清channel的问题,因为key告诉了我们他是谁的key。
NIOClient.java
/**
* NIOClient.java
*/
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NIOClient {
private Selector selector; //通道选择器对象
public void initConn(String serverIp,int port) throws IOException {
//服务器地址对象
InetSocketAddress address=new InetSocketAddress(serverIp,8888);
//创建与服务器连接的 Socket 通道对象
SocketChannel sc=SocketChannel.open();
//非阻塞模型
sc.configureBlocking(false);
selector = Selector.open(); //可开始监听连接进入
//在通道上注册连接成功事件
sc.register(selector, SelectionKey.OP_CONNECT); ;
sc.connect(address);
}
/**
* 监听端口
*/
public void listen() {
try {
while(true) {
//System.out.println("在此等待 I/O 事件发生 ...");
selector.select();//select 可理解为监听系统事件发生,此处阻塞直到事件发生
//System.out.println("有 I/O 事件发生");
Iterator iter = selector.selectedKeys().iterator();
while(iter.hasNext()) {
SelectionKey key = (SelectionKey)iter.next();
iter.remove(); //移除这个事件
//System.out.println("处理这个io事件");
if(key.isConnectable()) {
//System.out.println("isConnectable");
SocketChannel channel = (SocketChannel)key.channel(); //通过当前key获得对应channel
//如果连接正在进行中则完成这个连接
if(channel.isConnectionPending()) {
channel.finishConnect();
}
channel.configureBlocking(false); //设置使用非阻塞模式
//发送一条欢迎消息
String outMsg = "你好,我是NIOClient\r\n";
ByteBuffer buffer = ByteBuffer.wrap(outMsg.getBytes());
channel.write(buffer);
//在通道上注册连接成功事件
channel.register(selector, SelectionKey.OP_READ);
//System.out.println("连接服务器成功:"+channel.socket().getRemoteSocketAddress());
}else if(key.isReadable()) {
//System.out.println("isreadable");
processRead(key);
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
protected void processRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel)key.channel(); //通过当前key获得对应channel
//创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);//分配空间
int count = channel.read(buffer);
byte[] data=buffer.array();
String msg=new String(data).trim();
System.out.println("读到数据: "+msg);
//回送给服务器
//ByteBuffer outBuffer=ByteBuffer.wrap(msg.getBytes());
//channel.write(outBuffer);//将消息回送给对方
}
//主函数
public static void main(String[] args) {
int port = 8888;
try {
NIOClient client = new NIOClient();
client.initConn("localhost",port);
System.out.println("listening on " + port);
client.listen();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端与服务器端的代码非常相似,这里我不在赘述,相信以上代码加注释,再配合某度和jdk文档已足以让你搞懂NIO通信的流程了。