前言
当当当当,我又来了,趁热打铁,这次肝下Java NiO的demo。毕竟技术精通的各位大佬都知道Netty也是基于Java NIO做的封装。如果要学习Netty,肯定要对Java NIO有一定的了解啦。
注意, 这节基本就是代码了,介意的同学就先说声抱歉啦。
1. 服务端
- NioServer
public class NioServer {
public static void main(String[] args) {
int port = 8080;
NioServerHandler nioServerHandler = new NioServerHandler(port);
new Thread(nioServerHandler, "server").start();
}
}
这个是服务端的入口,这里可以说是啥正经的都没干啊,就是启动了一个NioServerHandler这个线程而已,还有把8080这个端口,传给了这个线程。
- NioServerHandler
public class NioServerHandler implements Runnable {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private volatile boolean stop;
private List<SocketChannel> socketChannelList = new ArrayList<>();
public NioServerHandler(int port) {
try {
// 创建多路复用器
selector = Selector.open();
// 创建 ServerSocketChannel
serverSocketChannel = ServerSocketChannel.open();
// 设置为异步非阻塞模式
serverSocketChannel.configureBlocking(false);
// 绑定端口,并且设置最大连接数量为1024个
serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
// 注册感兴趣的事件,这里注册的建立请求事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (!stop) {
try {
// 无论有没有事件,多路复用器1s都会被唤醒1次, 如果不设置时间,就会一直等到有就绪的事件
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
SelectionKey key = null;
while (iterator.hasNext()) {
key = iterator.next();
// 删掉已经处理过的key
iterator.remove();
try {
handlerInput(key);
} catch (IOException e) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void handlerInput(SelectionKey selectionKey) throws IOException {
if (selectionKey.isValid()) {
// 处理接入的请求消息
if (selectionKey.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = ssc.accept();
// 客户端的socket也需要设置成非阻塞的
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
socketChannelList.add(socketChannel);
}
if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = socketChannel.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "utf-8");
System.out.println("server received :" + body);
socketChannelList.forEach(sc -> {
try {
doWrite(sc, body);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
}
}
private void doWrite(SocketChannel socketChannel, String message) throws IOException {
byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
socketChannel.write(writeBuffer);
}
public void stop() {
this.stop = true;
}
}
这个就是服务端主要的代码了,这里可以看到NioServerHandler这个类里面做了很多的事。没关系,我们一步步来。
- 构造函数,看一个类的代码,构造函数肯定是必须要看的函数之一了。我们可以看到这个类的构造函数用了一个
Selector.open()的函数,这个Selector就是java里面对I/O多路复用的一个抽象了,对应的就是前面一节的select/poll/epoll的系统调用。然后是ServerSocketChannel.open(),这个方法其实就是创建一个套接字了,可以类比于ServerSocket。再然后就是serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);, 这个代码是啥意思呢?其实就是告诉前面创建的多路复用器,如果有客户端连接事件,你就告诉我, 这里的SelectionKey.OP_ACCEPT表示的就是服务端收到客户端的连接事件,与之对应的则是SelectionKey.OP_CONNECT,表示的是客户端连接服务端成功的事件。 run方法, 既然NioServerHandler实现了Runable接口,那么run方法肯定也是特别重要的方法,但是这里的run方法其实挺简单的,就是调用selector.select()方法,这个方法的含义就是阻塞当前线程,直到注册的事件发生。然后遍历需要处理的SelectionKey。(看这个类的源码的注释的第一行可以知道,SelectionKey表示SelectableChannel在Selector上的注册关系)handlerInput方法,这个方法则主要是对selectionKey进行判断了,看看当前的selectionKey代表的是客户端的接入事件,还是数据可读的事件。针对客户端的接入事件,将客户端的对象也绑定注册到selector上,并绑定SelectionKey.OP_READ,这样当客户端的发送来的数据包准备好之后,又会被selector.select()方法筛选出来,从而走到数据可读事件中。针对数据可读事件,这里就是读取SocketChannel中的数据,并向客户端发送了数据。doWrite方法,这个方法就是单纯的向客户端发送数据,应该没啥要讲的了。
3. 客户端
- NioClient
public class NioClient {
public static void main(String[] args) {
new Thread(new NioClientHandler("localhost", 8080), "`server").start();
}
}
这里是客户端启动的入口,和服务端的入口类似,就是把需要连接的地址和端口传给了NioClientHandler, 主要的业务逻辑都是在这个NioClientHandler中。
2. NioClientHandler
public class NioClientHandler implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
public NioClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (!stop) {
try {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
SelectionKey key = null;
while (iterator.hasNext()) {
key = iterator.next();
iterator.remove();
handleInput(key);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
} else {
System.exit(1);
}
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "utf-8");
System.out.println("receive :" + body);
}
}
}
}
public void doWrite(SocketChannel sc) {
new Thread(() -> {
while (true) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要发送的消息");
String message = scanner.next();
byte[] req = message.getBytes(StandardCharsets.UTF_8);
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
try {
sc.write(writeBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
public void stop() {
this.stop = true;
}
}
我们也和前面一样,一个方法一个看下吧
- 构造方法,这里和服务端类似,首先创建了一个
Selector, 由于这里是客户端,所有后面创建的是SocketChannel, 并将SocketChannel设置为非阻塞。后面调用socketChannel.connect(new InetSocketAddress(host, port))连接服务端。看这个方法的注释有如下一段文字:
If this channel is in non-blocking mode then an invocation of this method initiates a non-
blocking connection operation. If the connection is established immediately, as can happen with
a local connection, then this method returns true. Otherwise this method returns false and the
connection operation must later be completed by invoking the finishConnect method.
这段话的意思是,如果这个chanel是在非阻塞的模式下,这个方法调用会发起一个非阻塞的连接操作。如果这个连接马上建立成功了,比如是个本地连接,这个方法就会返回true,但是如果不能马上成功就会发挥false,然后这个连接操作成功之后,会去调用finishConnect方法。
所以我们这里,其实if成立的情况只有在本地连接才会发生,大概率是走到else的逻辑的也就是将socketChannel绑定连接事件。if成立的时候,也就是连接成功了,这里客户端是可以发起读/写的操作的,我们这里是先绑定到SelectionKey.OP_READ事件的,也就是绑定了读事件。
run方法,也和服务端类似,就是遍历可以处理的SelectionKey,交给我们的handleInput方法做处理。handleInput方法,这里也是需要判断事件类型,是连接服务端成功的事件,还是数据可读事件。如果是连接服务端成功了,则需要转换成绑定数据可读事件。如果是数据可读事件,则去读取数据。doWrite方法,这个方法就是起了个线程将控制台输入的消息,通过SocketChannel发送到客户端而已。
到这里,特别简单的Java NiO的代码demo就完成了,分别启动服务端和客户端后,在客户端的控制台输入文字并回车后,服务端就会打印出对应的字符了。
这里我们总结下,NIO开发的常见套路吧:
- 创建多路复用器
Selector.open() - 创建channel
ServerSocketChannel/SocketChannel,并设置成非阻塞。 - 绑定事件, 初始的时候,服务端需要绑定
SelectionKey.OP_ACCEPT,而客户端需要绑定SelectionKey.OP_CONNECT。 - 事件处理:
- 针对
SelectionKey.OP_ACCEPT,在连接成功后,需要转成绑定SelectionKey.OP_READ事件 - 针对
SelectionKey.OP_CONNECT,在连接成功后,需要转成绑定SelectionKey.OP_READ事件 - 针对
SelectionKey.OP_READ事件, 表示数据已经准备好了,可以做读操作了。 - 针对
SelectionKey.OP_WRITE事件, 这个一般使用场景为数据过大,channel的发送缓冲区不够,需要多次写入的情况。
- 针对