NIO同步非阻塞,详细原理见代码注释。
- 服务器端代码
package niodemo.niochat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
//如下的代码就实现了服务器仅仅通过1个线程(没有创建新的线程就是主线程)监听多个客户端的请求,可以写客户端测试,也可以用telnet
public class NIOSelectorServer {
private static Charset charset = Charset.forName("unicode");
public static void main(String[] args) throws IOException {
//1. 创建ServerSocketChannel,通过open()获得channel实例
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2.绑定ip
InetSocketAddress inetSocketAddress = new InetSocketAddress(6666);
serverSocketChannel.bind(inetSocketAddress);
//3. 将serverSocketChannel设置成非阻塞的
serverSocketChannel.configureBlocking(false);
//4. 创建Selector
Selector selector = Selector.open();
//5. 将ServerSocketChannel注册到Selector上:注意:首次注册都是注册到selector上的所有的SelectionKey集合上
//【Selector上注册的都是channel,在服务器端就一个channel就是ServerSocketChannel】
//注册是以事件的方式注册到selector上,serverSocketChannel这个服务器端通道关注的事件是accept
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
/*
* (1) SelectionKey类是Selector类上的集合,Selector上有3个SelectionKey,分别是:所有的SelectionKey,
* 已选择的SelectionKey,和废弃的SelectionKey。
* (2) SelectionKey上有4中事件类型(注册到selector上的channel都是通过事件注册的)分别是:
* OP_ACCEPT, OP_CONNECT, OP_READ, OP_WRITE
* (3) 其中,服务器端的通道是ServerSocektChannel就一个,关注的事件是OP_ACCEPT
* 服务器接收客户端请求的通道是SocketChannel是多个的,一个客户端一个SocketChannel,关注的事件是OP_READ和OP_WRITE
* 写客户端的时候,客户端的SocketChannel关注的事件是OP_CONNECT
* */
//6. 在selector上使用select()方法轮询所有的SelectionKey集合
System.out.println("select()自身以阻塞的方式等待客户端连接......");
while (selector.select() > 0) { //select()的返回值大于0,说明所有的SelectionKey集合上有事件发生
//有事件发生的就将其从selector上所有的SelectionKey集合转移到已选择的SelectionKey集合上。
//7. 遍历已选择的SelectionKey集合,遍历是方法:迭代器,for循环
//需要注意的是:selector.selectedKeys()方法返回的是已选择的SelectionKey集合,当有事件发生的时候遍历的就是已选择的SelectionKey集合
//而selector.select()轮询的是所有的SelectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//使用迭代器遍历已选择的SelectionKey集合
Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
while (selectionKeyIterator.hasNext()) {
SelectionKey key = selectionKeyIterator.next();
try {
//key是拿到的每个selectionKey集合上的曾经注册的channel
//8. 情况1:如何已选择集合SelectionKey上发生的事件是accept,说明是一个新的客户端连接请求事件
if (key.isAcceptable()) {
System.out.println("有新的客户端连接!");
//9. 新的连接事件要为该客户端创建一个新的专属于这个客户端连接的channel
//客户端的channel通过SocketChannel创建,服务器的channel通过ServerSocketChannel创建
//就像客户端的socket通过Socket创建,服务器的socket通过ServerSocket创建
SocketChannel socketChannel = serverSocketChannel.accept(); //通过serverSocketChannel.accept()获得实例
//10. 将channel设置为非阻塞模式,否则使用NIO无意义【每一个channel在注册到selector之前都要设置为非阻塞模式】
socketChannel.configureBlocking(false);
//11. 将channel注册到selector的所有SelectionKey上,等待被轮询
socketChannel.register(selector, SelectionKey.OP_READ);
}
//12. 情况2:如果已选择集合SelectionKey上发生的事件是read, 说明是原来就已经在selector上的channel又有事件发生了
if (key.isReadable()) {
//13. 要获取与客户端唯一对应的channel,才能处理客户端的请求
/*SelectableChannel*/
SocketChannel selectableChannel = (SocketChannel) key.channel();
//返回值是SelectableChannel,获取channel的方法不是getChannel(),而是SelectionKey提供的channel()
//返回值SelectableChannel没有read和write方法,没法和buffer交互数据
//14. 读数据:程序要想读channel的数据必须通过内存中的buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//15. buffer从channel中读数据
int readByte = 0; //读到的字节数,read的返回值可以是int或者long
byteBuffer.clear(); //为向buffer中写入数据做准备
while ((readByte = selectableChannel.read(byteBuffer)) > 0) {
//buffer从channel中循环读取数据的时候:注意:flip()
byteBuffer.flip(); //为从buffer输出数据做准备
}
//读取到的内容就在buffer里,程序从buffer中取数据要从字节(二进制)转成字符串。
System.out.println("form client:" + new String(byteBuffer.array()));
}
//当select()返回值大于0时,表示所有SelectionKey集合中有事件发生,所以将发生事件的已经注册的channel
//从所有的SelectionKey集合中拿到已选择的SelectionKey中;
//当处理完已选择的selectionKey中的channel,就将其移到已废弃的selectionKey中
selectionKeys.remove(key);
//之前的处理IOException的方式是throw,坏处是:如果客户端先关闭就会报错:
//java.io.IOException: 远程主机强迫关闭了一个现有的连接。
} catch (IOException ioe) { //如果出现异常,就说明客户端异常,可能是意外关闭了
//所以将遍历的已选择的SelectionKey集合上的channel从selector集合上取消注册
key.cancel();
}
}
}
}
}
- 客户端代码
package client;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
import java.util.Set;
public class ClientDemo {
private String HOST = "127.0.0.1";
private static final int PORT = 6666;
private Selector selector;
private SocketChannel socketChannel;
private String username;
public ClientDemo() {
try {
this.selector = Selector.open();
//源码中:checkPort()是0-65535的整数0xFFFF,通常49152往上的端口可以自由使用
InetSocketAddress inetSocketAddress = new InetSocketAddress(HOST, PORT);
this.socketChannel = SocketChannel.open(inetSocketAddress);
socketChannel.configureBlocking(false);
socketChannel.register(this.selector, SelectionKey.OP_READ);
this.username = socketChannel.getRemoteAddress().toString().substring(1);
System.out.println(this.username + "登录成功");
} catch (IOException i) {
i.getStackTrace();
}
}
//向服务器发消息
public void sendMsgToServer(String info) {
info = this.username + "说" + info;
try {
// //从键盘获取数据
// Scanner scanner = new Scanner(System.in);
// while (scanner.hasNextLine()){
// System.out.println(scanner.next());
// //装入buffer
// ByteBuffer byteBuffer = ByteBuffer.wrap(scanner.next().getBytes());
// //写入channel
// socketChannel.write(byteBuffer);
this.socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
//接收服务器的消息
public void readFromServer() {
try {
//轮询selector是否有事件发生(这里关心的就是读事件,因为没有连接事件,不是服务器)
int select = selector.select();
if (select > 0) {
//遍历selector集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey key : selectionKeys) {
if (key.isReadable()) {
//获取channel
Channel channel = key.channel();
SocketChannel socketChannel2 = (SocketChannel) channel;
//创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//从channel中读数据到buffer
socketChannel2.read(byteBuffer);
//显示出来
System.out.println(new String(byteBuffer.array()));
}
//channel处理后必须要移除!!!!!!!!!1
selectionKeys.remove(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ClientDemo groupChatClient = new ClientDemo();
//匿名内部类用lambda表达式优化写法
new Thread(() -> {
//不停的读取
while (true) {
//客户端起多个线程读数据
groupChatClient.readFromServer();
try {
Thread.currentThread().sleep(3000);
} catch (InterruptedException ine) {
ine.printStackTrace();
}
}
}).start();
//客户端的一个主线程向服务器写数据
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String str = scanner.nextLine();
groupChatClient.sendMsgToServer(str);
}
}
}
- 运行结果