NIO 网络编程
文章目录
阻塞模式
写一个简单的服务器
package com..nio.Netty.NetChannel;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
/**
* @Author:nioliu
* @DATE: 2021/9/8 9:25
*/
@Slf4j
public class Server {
public static void main(String[] args) throws IOException {
// 使用nio来理解阻塞模式
// 单线程处理
ByteBuffer buffer = ByteBuffer.allocate(16);
// 1. 创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
// 2. 绑定监听端口
ssc.bind(new InetSocketAddress(8080));
// 3. accept: 建立与客户端的连接
ArrayList<SocketChannel> socketChannels = new ArrayList<>();
while (true) {
// 4. 用于与客户端进行通信 SocketChannel
log.debug("connecting...");
SocketChannel channel = ssc.accept();// 阻塞(线程停止运行), 等待连接
log.debug("connected..."+channel);
socketChannels.add(channel);
for (SocketChannel socketChannel : socketChannels) {
// 5. 接收客户端发送的数据
log.debug("before read...");
socketChannel.read(buffer);// 阻塞方法, 线程停止运行(没有数据就一直等)
buffer.flip();
System.out.println(StandardCharsets.UTF_8.decode(buffer));
buffer.clear();
log.debug("afer read...");
}
}
}
}
写一个简单的客户端
package com..nio.Netty.NetChannel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
/**
* @Author:nioliu
* @DATE: 2021/9/8 9:33
*/
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost", 8080));
System.out.println("waiting....");
}
}
先启动服务器, 在启动客户端, 查看输出效果
[DEBUG] 2021-09-08 09:46:51,301 method:com..nio.Netty.NetChannel.Server.main(Server.java:34)
connecting...
[DEBUG] 2021-09-08 09:47:04,483 method:com..nio.Netty.NetChannel.Server.main(Server.java:36)
connected...java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:65430]
[DEBUG] 2021-09-08 09:47:04,484 method:com..nio.Netty.NetChannel.Server.main(Server.java:40)
before read...
hello
[DEBUG] 2021-09-08 09:48:58,606 method:com..nio.Netty.NetChannel.Server.main(Server.java:45)
afer read...
[DEBUG] 2021-09-08 09:48:58,606 method:com..nio.Netty.NetChannel.Server.main(Server.java:34)
connecting...
非阻塞模式
增加一行代码
// 使用非阻塞模式
ssc.configureBlocking(false);
查看输出
[DEBUG] 2021-09-08 09:46:51,301 method:com..nio.Netty.NetChannel.Server.main(Server.java:34)
connecting...
[DEBUG] 2021-09-08 09:47:04,483 method:com..nio.Netty.NetChannel.Server.main(Server.java:36)
connected...java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:65430]
[DEBUG] 2021-09-08 09:47:04,484 method:com..nio.Netty.NetChannel.Server.main(Server.java:40)
before read...
hello
[DEBUG] 2021-09-08 09:48:58,606 method:com..nio.Netty.NetChannel.Server.main(Server.java:45)
afer read...
[DEBUG] 2021-09-08 09:48:58,606 method:com..nio.Netty.NetChannel.Server.main(Server.java:34)
connecting...
非阻塞模式下, 线程不会停, 一直在循环执行, 没有数据则返回null, 消耗大量CPU
注意: 上述只是将ServerSocketChannel设置为非阻塞, 但是SocketChannel仍然是阻塞模式(即代码执行到read()仍然是停止运行, 等待数据进入
设置SocketChannel为非阻塞模式, read()返回0
package com..nio.Netty.NetChannel;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
/**
* @Author:nioliu
* @DATE: 2021/9/8 9:25
*/
@Slf4j
public class Server {
public static void main(String[] args) throws IOException {
// 单线程处理
ByteBuffer buffer = ByteBuffer.allocate(16);
// 1. 创建服务器 默认使用阻塞模式
ServerSocketChannel ssc = ServerSocketChannel.open();
// 使用非阻塞模式
ssc.configureBlocking(false);
// 2. 绑定监听端口
ssc.bind(new InetSocketAddress(8080));
// 3. accept: 建立与客户端的连接
ArrayList<SocketChannel> socketChannels = new ArrayList<>();
while (true) {
// 4. 用于与客户端进行通信 SocketChannel
// log.debug("connecting...");
SocketChannel channel = ssc.accept();
if (channel != null) {
log.debug("connected..."+channel);
channel.configureBlocking(false);
socketChannels.add(channel);
}
for (SocketChannel socketChannel : socketChannels) {// 每次遍历全部channel
// 5. 接收客户端发送的数据
log.debug("before read...");
int i = socketChannel.read(buffer);
if (i > 0) {
buffer.flip();
System.out.println(StandardCharsets.UTF_8.decode(buffer));
buffer.clear();
log.debug("afer read...");
}
}
}
}
}
显然, 这会非常浪费CPU(100%), 下面使用Selector来管理Channel, 监测有没有事件发生, 没有的时候就阻塞, 有的时候就运行
使用Selector
多路复用
Java中通过Selector实现多路复用
- 当没有事件是,调用select方法会被阻塞住
- 一旦有一个或多个事件发生后,就会处理对应的事件,从而实现多路复用
多路复用与阻塞IO的区别
- 阻塞IO模式下,若线程因accept事件被阻塞,发生read事件后,仍需等待accept事件执行完成后,才能去处理read事件
- 多路复用模式下,一个事件发生后,若另一个事件处于阻塞状态,不会影响该事件的执行
事件
accept事件: 客户端一旦发起连接请求, 服务器就会触发accept事件
connect事件: 客户端发起连接请求并与服务器建立连接后所发生的事件(客户端方)
read: 可读事件
write:可写事件
编写一个Selector服务器
package com..nio.Netty.NetChannel;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
import static com..nio.Netty.ByteBuffer.ByteBufferUtil.debugAll;
/**
* @Author:nioliu
* @DATE: 2021/9/8 10:13
*/
@Slf4j
public class SelectorServer {
public static void main(String[] args) throws IOException {
// 单线程处理
ByteBuffer buffer = ByteBuffer.allocate(16);
// 1. 创建selector, 管理多个channel
Selector selector = Selector.open();
// 2. 注册channel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 返回selectionKey(相当于管理员)为事件发生后, 通过这个key得到这个事件和相应channel
SelectionKey sscKey = ssc.register(selector, 0, null);
// 指明这个key用来关注accept事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key:{}",sscKey);
ssc.bind(new InetSocketAddress(8080));
while (true) {
// 3. select方法: 没有事件发生就会阻塞, 有四种事件的其中之一发生, 就会继续运行
// select在事件未处理的时候, 不会阻塞---->事件要么处理, 要么cancel(), 不能不管
selector.select();
// 4. 处理事件
// 拿到所有可用事件集合(即所有发生的事件)
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 使用迭代器遍历事件(为了可以删除事件)
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 每拿到一个key, 就要删除这个key, 因为SelectionKey不会自己删除, 只会增加, 而下次在遍历就会报空指针错误
iterator.remove();
log.debug("key:{}", key);
// 5. 区分事件类型
if (key.isAcceptable()) {// 如果是连接事件
ServerSocketChannel channel = (ServerSocketChannel)key.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);// 设置为非阻塞
// 注册channel
SelectionKey scKey = socketChannel.register(selector, 0, null);
// 关注读事件
scKey.interestOps(SelectionKey.OP_READ);
log.debug("{}",socketChannel);
} else if (key.isReadable()) {// 如果是read事件(断开也是一个读事件)
try {
SocketChannel channel = (SocketChannel) key.channel();
int read = channel.read(buffer);// 如果是正常断开, read()返回-1
if (read == -1) {// 处理正常断开(close())
key.cancel();
}else {
buffer.flip();
debugAll(buffer);
}
} catch (IOException e) {// 处理强制非正常断开
e.printStackTrace();
key.cancel();// 因为客户端断开了, 因此需要将key断开连接
}
}
}
}
}
}
下面展示了Selector和Keys的存储原理
处理消息边界
传输的文本可能有以下三种情况
- 文本大于缓冲区大小
- 此时需要将缓冲区进行扩容
- 发生半包现象
- 发生粘包现象
解决思路大致有以下三种
-
固定消息长度,数据包大小一样,服务器按预定长度读取,当发送的数据较少时,需要将数据进行填充,直到长度与消息规定长度一致。缺点是浪费带宽
-
另一种思路是按分隔符拆分,缺点是效率低,需要一个一个字符地去匹配分隔符
-
TLV 格式,即 Type 类型、Length 长度、Value 数据
(也就是在消息开头用一些空间存放后面数据的长度),如HTTP请求头中的Content-Type与
Content-Length
优点: 类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer
缺点: buffer 需要提前分配,如果内容过大,则影响 server 吞吐量
因为之前已经实现过Split()方法, 这里就不重复赘述了
附件
注册时, 可以将Buffer当做channel的属性注册到SelectionKey
ByteBuffer buffer = ByteBuffer.allocate(16);
SelectionKey sscKey = ssc.register(selector, 0, buffer);
获取附件使用key.attachment()方法并强转成需要的类型
ByteBuffer buffer =(ByteBuffer) key.attachment();// 取出附件
以上表示: 每一个accept建立后, 就给这个事件加一个独立的buffer, 之后有数据readable()就用这个独立的buffer, 而不是每次read都需要新建立一个buffer, 这样就可以保证每个channel的buffer独立并且数据连续, 并且buffer大小不够时还可以扩容(使用key.attach(Object)关联新的buffer)后拼接读取, 不会丢失数据.
全部代码
package com..nio.Netty.NetChannel;
import com..nio.Netty.ByteBuffer.TestByteBufferExam;
import lombok.extern.slf4j.Slf4j;
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;
import java.util.Set;
import static com..nio.Netty.ByteBuffer.ByteBufferUtil.debugAll;
/**
* @Author:nioliu
* @DATE: 2021/9/8 10:13
*/
@Slf4j
public class SelectorServer {
public static void main(String[] args) throws IOException {
// 单线程处理
// 1. 创建selector, 管理多个channel
Selector selector = Selector.open();
// 2. 注册channel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 返回selectionKey(相当于管理员)为事件发生后, 通过这个key得到这个事件和相应channel
SelectionKey sscKey = ssc.register(selector, 0, null);
// 指明这个key用来关注accept事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key:{}",sscKey);
ssc.bind(new InetSocketAddress(8080));
while (true) {
// 3. select方法: 没有事件发生就会阻塞, 有四种事件的其中之一发生, 就会继续运行
// select在事件未处理的时候, 不会阻塞---->事件要么处理, 要么cancel(), 不能不管
selector.select();
// 4. 处理事件
// 拿到所有可用事件集合(即所有发生的事件)
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 使用迭代器遍历事件(为了可以删除事件)
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 每拿到一个key, 就要删除这个key, 因为SelectionKey不会自己删除, 只会增加, 而下次在遍历就会报空指针错误
iterator.remove();
log.debug("key:{}", key);
// 5. 区分事件类型
if (key.isAcceptable()) {// 如果是连接事件
ServerSocketChannel channel = (ServerSocketChannel)key.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);// 设置为非阻塞
ByteBuffer buffer = ByteBuffer.allocate(16);
// 将buffer关联到selectionKey上
// 注册channel
SelectionKey scKey = socketChannel.register(selector, 0, buffer);
// 关注读事件
scKey.interestOps(SelectionKey.OP_READ);
log.debug("{}",socketChannel);
} else if (key.isReadable()) {// 如果是read事件(断开也是一个读事件)
try {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer =(ByteBuffer) key.attachment();// 取出附件
int read = channel.read(buffer);// 如果是正常断开, read()返回-1
if (read == -1) {// 处理正常断开(close())
key.cancel();
}else {
TestByteBufferExam.split(buffer);
if (buffer.limit() == buffer.position()) {// 如果不够, 那么扩容
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
newBuffer.flip();
newBuffer.put(buffer);// 把原来buffer的内容加进去
key.attach(newBuffer);
}
debugAll(buffer);
}
} catch (IOException e) {// 处理强制非正常断开
e.printStackTrace();
key.cancel();// 因为客户端断开了, 因此需要将key断开连接(从selector的keys集合中删除)
}
}
}
}
}
}
实际生产中, 要根据所接收到的消息进行动态分配ByteBuffer, 这也是Netty所优雅实现的
可写事件
package com..nio.Netty.NetChannel;
import lombok.extern.slf4j.Slf4j;
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.nio.charset.Charset;
import java.util.Iterator;
/**
* @Author:nioliu
* @DATE: 2021/9/8 16:26
*/
@Slf4j
public class WriterServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while (true) {
selector.select();
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey next = keyIterator.next();
keyIterator.remove();
if (next.isAcceptable()) {
SocketChannel socketChannel = ssc.accept();// 因为accept实际上就是从ssc拿到的, 所以这里可以直接这么写, 等同于next.channel()
socketChannel.configureBlocking(false);
// 向客户端写入大量数据
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 30000000; i++) {
stringBuilder.append("a");
}
ByteBuffer byteBuffer = Charset.defaultCharset().encode(stringBuilder.toString());
while (byteBuffer.hasRemaining()) {
int i = socketChannel.write(byteBuffer);// 返回实际写入的byte数
log.debug("{}", i);
}
}
}
}
}
}
上述代码通过多次写入来完成大数据的写入, 但是这种方法的While一直在试写, 而缓冲区可能还没准备好, 所以CPU资源会被浪费掉, 因此同样需要关注一个Write事件来解决这个问题.
package com..nio.Netty.NetChannel;
import lombok.extern.slf4j.Slf4j;
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;
/**
* @Author:nioliu
* @DATE: 2021/9/8 16:26
*/
@Slf4j
public class WriterServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while (true) {
selector.select();// 有事件时触发
// selector.select(3);// 超过3ms时阻塞
// selector.selectNow();// 非阻塞模式
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();// 这时可能已经聚集了多个事件
while (keyIterator.hasNext()) {
SelectionKey next = keyIterator.next();
keyIterator.remove();
if (next.isAcceptable()) {
SocketChannel socketChannel = ssc.accept();// 因为accept实际上就是从ssc拿到的, 所以这里可以直接这么写, 等同于next.channel()
socketChannel.configureBlocking(false);
SelectionKey selectionKey = socketChannel.register(selector, 0, null);
selectionKey.interestOps(SelectionKey.OP_WRITE);// 一个selectionKey可以关注多个事件
// 向客户端写入大量数据
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 30000000; i++) {
stringBuilder.append("a");
}
ByteBuffer byteBuffer = Charset.defaultCharset().encode(stringBuilder.toString());
int i = socketChannel.write(byteBuffer);// 返回实际写入的byte数
// 判断是否有剩余内容
if (byteBuffer.hasRemaining()) {
// 这里权限的表示就是数字, 和Linux的777权限一样, 通过加法来判断当前key可以管理到哪些事件
selectionKey.interestOps(selectionKey.interestOps()+SelectionKey.OP_WRITE);// 关注可写事件, 当发送缓冲区 可写时, 就调用这个方法
selectionKey.attach(byteBuffer);
}
} else if (next.isWritable()) {
ByteBuffer bytebuffer = (ByteBuffer) next.attachment();
SocketChannel channel = (SocketChannel)next.channel();
channel.write(bytebuffer);
// 清理操作
if (!bytebuffer.hasRemaining()) {
next.attach(null);
next.interestOps(next.interestOps()-SelectionKey.OP_WRITE);//如果内容都写完, 那么就不需要再关注可写事件了
}
}
}
}
}
}
Selector操作的一些小知识
💡select 何时不阻塞
事件发生时
调用selector.wakeup(): 唤醒阻塞的线程
调用selector.close()
selector所在线程interrupt