一、NIO与BIO比较
同步非阻塞IO
与BIO比较:
1.在BIO中,每次建立一次连接,就会创建一个线程处理该连接,虽然可以使用线程池来管理,
但是每次建立TCP连接,销毁,消耗资源和性能,对应的线程也是如此。
2.与BIO相比,IO操作读写数据时,如果当前没有数据读写,线程会一直阻塞,直到有数据读写,不能做别的事情
对于第一点,NIO中采取channel来读写数据,提升性能
对于第二点,NIO中采取Reactor模式解决
二、NIO核心API

- Channels
- 通道是对原 I/O 包中的流的模拟,可以使用它对数据进行读写,这里读写是按块进行,而不像传统流式读写按照字节进行
- 通道既可以读取也可以写入,不像流只能单向读或者写
- 通道读写都是与缓冲区交互。
-
Buffers 缓冲区 三个变量
1.position 写数据表示当前位置 读数据 向前移动至下一个可读数据位置
2.limit 当前缓冲区空间,写模式下,limit等于Buffer的capacity,读模式时, limit表示你最多能读到多少数据
3.capacity 缓冲区容量 -
Selectors 选择器
通道向Selectors注册事件,Selector监听Channel所感兴趣的事件
事件通常使用SelectorKey表示:
- SelectionKey.OP_CONNECT 连接就绪
- SelectionKey.OP_ACCEPT 接收就绪
- SelectionKey.OP_READ 可读
- SelectionKey.OP_WRITE 可写
在Reactor模式中,通过事件轮询,不断遍历Selector,针对不同事件做不同处理,而这只需要一个线程就可以管理多个socket,不会让线程白白等待读写数据浪费时间。
在Proactor模式中,当检测到有事件发生时,会新起一个异步操作,然后交由内核线程去处理,当内核线程完成IO操作之后,发送一个通知告知操作已完成,异步IO模型采用的就是Proactor模式,也就是AIO。
三、NIO代码
客户端:
public class Client {
private static String DEFAULT_HOST = "127.0.0.1";
private static int DEFAULT_PORT = 6666;
private static ClientHandle clientHandle;
public static void start(){
start(DEFAULT_HOST,DEFAULT_PORT);
}
public static synchronized void start(String ip,int port){
if(clientHandle!=null)
clientHandle.stop();
clientHandle = new ClientHandle(ip,port);
new Thread(clientHandle,"Server").start();
}
public static boolean sendMsg(String msg) throws Exception{
if(msg.equals("q"))
return false;
clientHandle.sendMsg(msg);
return true;
}
public static void main(String[] args){
start();
}
}
public class ClientHandle implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean started;
private MsgUtil msgUtil = new MsgUtil();
public ClientHandle(String ip, int port) {
this.host = ip;
this.port = port;
try {
//创建选择器
selector = Selector.open();
//打开监听通道
socketChannel = SocketChannel.open();
//如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
socketChannel.configureBlocking(false);//开启非阻塞模式
started = true;
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop() {
started = false;
}
@Override
public void run() {
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
msgUtil.dealWithSelector(started, selector, null, this);
//selector关闭后会自动释放里面管理的资源
if (selector != null)
try {
selector.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) ;
else System.exit(1);
}
//读消息
if (key.isReadable()) {
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if (readBytes > 0) {
//将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String result = new String(bytes, "UTF-8");
System.out.println("客户端收到消息:" + result);
}
//释放资源
else if (readBytes < 0) {
key.cancel();
sc.close();
}
}
}
}
private void doConnect() throws IOException {
if (socketChannel.connect(new InetSocketAddress(host, port))) ;
else socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
//发送消息
public void sendMsg(String msg) throws Exception {
MsgUtil msgUtil = new MsgUtil();
socketChannel.register(selector, SelectionKey.OP_READ);
msgUtil.doWrite(socketChannel, msg);
}
}
服务端
public class Server {
private static int DEFAULT_PORT = 6666;
private static ServerHandle serverHandle;
public static void start(){
start(DEFAULT_PORT);
}
public static synchronized void start(int port){
if(serverHandle!=null)
serverHandle.stop();
serverHandle = new ServerHandle(port);
new Thread(serverHandle,"Server").start();
}
public static void main(String[] args){
start();
}
}
public class ServerHandle implements Runnable {
//选择器
private Selector selector;
//通道
private ServerSocketChannel serverChannel;
//是否遍
private volatile boolean started;
private MsgUtil msgUtil = new MsgUtil();
/**
* 构造方法
*
* @param port 指定要监听的端口号
*/
public ServerHandle(int port) {
try {
//创建选择器
selector = Selector.open();
//打开监听通道
serverChannel = ServerSocketChannel.open();
//如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
serverChannel.configureBlocking(false);//开启非阻塞模式
//绑定端口 backlog设为1024
serverChannel.socket().bind(new InetSocketAddress(port), 1024);
//监听客户端连接请求
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
//标记服务器已开启
started = true;
System.out.println("服务器已启动,端口号:" + port);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop() {
started = false;
}
@Override
public void run() {
//循环遍历selector
msgUtil.dealWithSelector(started, selector, this, null);
//selector关闭后会自动释放里面管理的资源
if (selector != null)
try {
selector.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
//处理新接入的请求消息
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//通过ServerSocketChannel的accept创建SocketChannel实例
//完成该操作意味着完成TCP三次握手,TCP物理链路正式建立
SocketChannel sc = ssc.accept();
//设置为非阻塞的
sc.configureBlocking(false);
//注册为读
sc.register(selector, SelectionKey.OP_READ);
}
//读消息
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if (readBytes > 0) {
//将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String expression = new String(bytes, "UTF-8");
String result = "服务端处理数据: " + expression;
System.out.println("服务器收到消息:" + expression);
msgUtil.doWrite(sc, result);
}
//释放资源
else if (readBytes < 0) {
key.cancel();
sc.close();
}
}
}
}
}
工具类:
public class MsgUtil {
//异步发送消息
public void doWrite(SocketChannel channel, String request) throws IOException {
//将消息编码为字节数组
byte[] bytes = request.getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
//发送缓冲区的字节数组
channel.write(writeBuffer);
}
public void dealWithSelector(boolean started, Selector selector, ServerHandle serverHandle, ClientHandle clientHandle) {
while (started) {
try {
//无论是否有读写事件发生,selector每隔1s被唤醒一次
selector.select(1000);
//阻塞,只有当至少一个注册的事件发生的时候才会继续.
// selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key;
//循环注册器事件.
while (it.hasNext()) {
key = it.next();
it.remove();
try {
if (serverHandle != null)
serverHandle.handleInput(key);
if (clientHandle != null)
clientHandle.handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}
}
public class Test {
//测试主方法
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
//运行服务器
Server.start();
//避免客户端先于服务器启动前执行代码
Thread.sleep(100);
//运行客户端
Client.start();
while (Client.sendMsg(new Scanner(System.in).nextLine())) ;
System.out.println("发送消息结束");
}
}
注:
NIO要想真正说清楚,可能需要写一本书,很多细节我也不懂,本文哪里有问题请指出,本文属于新手入门级别。