NIO 非阻塞网络编程快速入门
案例:
编写一个 NIO 入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)
目的:理解 NIO 非阻塞网络编程机制
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;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws Exception{
//创建ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selecor对象
Selector selector = Selector.open();
//绑定一个端口6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT pos_1
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1
//循环等待客户端连接
while (true) {
//这里我们等待1秒,如果没有事件发生, 返回
if(selector.select(1000) == 0) { //没有事件发生
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的>0, 就获取到相关的 selectionKey集合
//1.如果返回的>0, 表示已经获取到关注的事件
//2. selector.selectedKeys() 返回关注事件的集合
// 通过 selectionKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectionKeys 数量 = " + selectionKeys.size());
//遍历 Set<SelectionKey>, 使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
//获取到SelectionKey
SelectionKey key = keyIterator.next();
//根据key 对应的通道发生的事件做相应处理
if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
//该该客户端生成一个 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept(); //这里不会阻塞,会马上执行
System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
//将 SocketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
//关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
}
if(key.isReadable()) { //发生 OP_READ
//通过key 反向获取到对应channel
SocketChannel channel = (SocketChannel)key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("from 客户端 " + new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey, 防止重复操作
keyIterator.remove();
}
}
}
}
pos1:
1、对操作系统有一定了解的同学,就会大概知道这里监听的是一个Accept通道。这个通道的 作用就是监听,实际建立连接了还会有一个通道。
2、简单说一下为什么。因为客户端发请求的时候,服务器这边是肯定要先有一个监听通道,
监听某个端口是否有客户端要建立链接,如果有客户端想要建立链接,那么会再创建一个和 客户端真正通信的通道。
3、如果有其它客户端还想要建立链接,这个Accept监听端口监听到了,就会再创建几个真正 的通信通道。
4、也就是Server的一个端口可以建立多个TCP连接,因为IP层协议通过 目标地址+端口+源地址+源端口四个信息识别一个上下文
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws Exception{
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞
socketChannel.configureBlocking(false);
//提供服务器端的ip 和 端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
}
}
//...如果连接成功,就发送数据
String str = "hello, gujie~";
//Wraps a byte array into a buffer,根绝字节数组的大小来调节buffer的大小
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//发送数据,将 buffer 数据写入 channel
socketChannel.write(buffer);
System.in.read();
}
}
运行结果
注意,为了可以让IDEA让客户端和服务端同时运行可以选择以下方式
注意:selector.selectedKeys()和selector.keys()不同,一个选择的所有注册过的客户端链接,一个是选择发生过事件的客户端链接。
SelectionKey
1.SelectionKey,表示 Selector 和网络通道的注册关系,共四种:
int OP_ACCEPT:有新的网络连接可以 accept,值为 16
int OP_CONNECT:代表连接已经建立,值为 8
int OP_READ:代表读操作,值为 1
int OP_WRITE:代表写操作,值为 4
————————————————
源码中:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
2.SelectionKey 相关方法
* public abstract Selector selector(),得到与之,关联的Selector对象
* public abstract SelectableChannel channel(),得到与之关联的通道
* public final Object attachment(),得到与之关联的共享数据
* public abstract SelectionKey interestOps(int ops),设置或改变监听事件
* public final boolean isAcceptable(), 是否可以accept
* public final boolean isReadable(),是否可以读
* public final boolean isWritable(),是否可以写
ServerSocketChannel
1.ServerSocketChannel 在服务器端监听新的客户端 Socket 连接,负责监听,不负责实际的读写操作
2.相关方法如下
* public static ServerSocketChannel open(),得到- - 个ServerSocketChannel通道
* public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号
* public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值false 表示采用非阻塞模式
* public SocketChannel accept(),接受-一个连接,返回代表这个连接的通道对象
* public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件
SocketChannel
1.SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
2.相关方法如下
* public static ServerSocketChannel open(),得到一个ServerSocketChannel通道
* public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号
* public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值false 表示采用非阻塞模式
* public SocketChannel accept(),接受-一个连接,返回代表这个连接的通道对象
* public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件
ServerSocketChannel在服务端主要负责有一个连接来了,就生成一个socketChannel,而SocketChannel在客户端则主要负责数据的读写。