NIO基础入门案例
客户端代码:
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;
/**
* 获得一个ServerSocket通道,并对该通道做一些初始化的工作
*
* @param port
* 绑定的端口号
* @throws IOException
*/
public void initServer(int port) throws IOException {
// 获得一个ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置通道为非阻塞
serverChannel.configureBlocking(false);
// 将该通道对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 获得一个通道管理器
this.selector = Selector.open();
// 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
// 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*
* @throws IOException
*/
public void listen() throws IOException {
System.out.println("服务端启动成功!");
// 轮询访问selector
while (true) {
// 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<?> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
handler(key);
}
}
}
/**
* 处理请求
*
* @param key
* @throws IOException
*/
public void handler(SelectionKey key) throws IOException {
// 客户端请求连接事件
if (key.isAcceptable()) {
handlerAccept(key);
// 获得了可读的事件
} else if (key.isReadable()) {
handelerRead(key);
}
}
/**
* 处理连接请求
*
* @param key
* @throws IOException
*/
public void handlerAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
// 设置成非阻塞
channel.configureBlocking(false);
// 在这里可以给客户端发送信息哦
System.out.println("新的客户端连接");
// 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
}
/**
* 处理读的事件
*
* @param key
* @throws IOException
*/
public void handelerRead(SelectionKey key) throws IOException {
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if(read > 0){
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:" + msg);
//回写数据
ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}else{
System.out.println("客户端关闭");
key.cancel();
}
}
/**
* 启动服务端测试
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.initServer(30000);
server.listen();
}
}
服务端:
public class NClient
{
// 定义检测SocketChannel的Selector对象
private Selector selector = null;
static final int PORT = 8888;
// 定义处理编码和解码的字符集
private Charset charset = Charset.forName("UTF-8");
// 客户端SocketChannel
private SocketChannel sc = null;
public void init()throws IOException
{
selector = Selector.open();
InetSocketAddress isa = new InetSocketAddress("10.12.16.160", PORT);
// 调用open静态方法创建连接到指定主机的SocketChannel
sc = SocketChannel.open(isa);
// 设置该sc以非阻塞方式工作
sc.configureBlocking(false);
// 将SocketChannel对象注册到指定Selector
sc.register(selector, SelectionKey.OP_READ);
// 启动读取服务器端数据的线程
new ClientThread().start();
// 创建键盘输入流
Scanner scan = new Scanner(System.in);
while (scan.hasNextLine())
{
// 读取键盘输入
String line = scan.nextLine();
// 将键盘输入的内容输出到SocketChannel中
sc.write(charset.encode(line));
}
}
// 定义读取服务器数据的线程
private class ClientThread extends Thread
{
public void run()
{
try
{
while (selector.select() > 0) //①
{
// 遍历每个有可用IO操作Channel对应的SelectionKey
for (SelectionKey sk : selector.selectedKeys())
{
// 删除正在处理的SelectionKey
selector.selectedKeys().remove(sk);
// 如果该SelectionKey对应的Channel中有可读的数据
if (sk.isReadable())
{
// 使用NIO读取Channel中的数据
SocketChannel sc = (SocketChannel)sk.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
while(sc.read(buff) > 0)
{
sc.read(buff);
buff.flip();
content += charset.decode(buff);
}
// 打印输出读取的内容
System.out.println("聊天信息:" + content);
// 为下一次读取作准备
sk.interestOps(SelectionKey.OP_READ);
}
}
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
public static void main(String[] args)
throws IOException
{
new NClient().init();
}
}
调试过程
一、Debug启动服务端
单步执行:你会发现服务器端会先通过一个open方法创建一个ServerSocketChannel,然后绑定该ServerSocketChannel的地址与端口,ServerSocketChannel相当于BIO中的ServerSocket,并设置工作方式是非阻塞式的,最后将该ServerSocketChannel注册到Selector对象中。
如何大白话理解ServerSocketChannel、Selector?我们把ServerSocketChannel看做是一个餐厅的大门,把Selector当成服务员,当ServerSocketChannel这扇大门注册到某一个服务员Selector上后,这个门进来的所有客人都由该Selector服务员负责。
你会发现最终停留在selector.select()方法中,该方法你看不到源代码,底层是C语言实现,若Selector上注册的channel没有事件发生,会一直停留在此方法中。
二、启动telnet模拟客户端
确保打开了telnet程序,否则会提示’telnet’ 不是内部或外部命令,也不是可运行的程序或批处理文件。
cmd中输入如下命令:由于是本地启动服务端
就在你打开telnet同时,程序从selector()方法中返回,开始执行下一步。
继续往下执行,由于是首次连接,因此selector监测到的时间selectiongKey中是isAcceptable,所以,会执行handlerAccept函数,输出新的客户端连接字样。
连接后,程序仍然阻塞在select()方法中,除非输入数字,即会从该方法返回。当在telnet中输入hello时,继续debug下一步,可以发现,通过判断selector上就绪的SelectionKey sk,发现可读,然后接收并输出。若在telnet中的输入只看到下标闪烁,按ctrl+] 即可。
当断开客户端连接后,再次向到channel中读数据时报异常。
NIO理解的误区
从上面我们可以看出,如果Selector没有注册事件发生,会一直阻塞在select()方法中,那为什么说nio还是非阻塞的NIO?select()方法确实是阻塞的,但是你可以选择select(1000);方法,该方法超过一定时间后会返回。当然也可直接调用selector.selectNow()方法,立刻返回。
在nio的读方法中,需要对channel中读到的长度进行判断,否则在客户端断开连接后,容易出现客户端关闭的时候会抛出异常,死循环。
public void handelerRead(SelectionKey key) throws IOException {
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if(read > 0){
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:" + msg);
//回写数据
ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}else{
System.out.println("客户端关闭");
key.cancel();
}
}
SelectionKey.OP_WRITE使用的情况很少,OP_WRITE表示底层缓冲区是否有空间,是则响应返还true。
一个NIO不是只能有一个selector,一个selector不是只能注册一个ServerSocketChannel,可以多个。