此笔记是基于B站-黑马老师的视频,强烈推荐此视频
NIO基础
1. 三大组件
Channel、Buffer、Selector
- Channel是一个读写数据的双向通道。而传统的BIO中有InputStream和OutputStream,只能读或写数据。
- Buffer是内存层面的一个缓冲区,Channel将数据读取或写入Buffer中。
- 常见的Channel:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
- 多线程版本的服务器:每个请求建立一个线程连接,线程太多可能导致内存溢出,线程上下文切换需要时间,只适合少量连接数的场景。
- 线程池班的服务器:在阻塞模式下,一个线程仅能处理一个Socket连接。如果一个连接没有事件发生,那么该线程只能等待,不能为其他连接服务。仅适合短链接的场景。
- Selector版本的设计:selector会配合一个线程来管理多个channel,获取这些channel上发生的事件,这些channel工作在非阻塞模式下,selector的select()阻塞到直到channel发生了读写事件,select()就会返回这些事件交给thread进行处理。
2. ByteBuffer的使用
/**
* 使用ByteBuffer读取字节文件
*/
@Test
public void testByteBuffer() {
// 读取一个文件
try (FileChannel channel = new FileInputStream("test.txt").getChannel()) {
while (true) {
// 创建一个缓冲区,接收文件
ByteBuffer allocate = ByteBuffer.allocate(10);
// 将文件读取到缓冲区
int i = channel.read(allocate);
// 读取到-1,文件读取完毕
if(i == -1) {
break;
}
// 切换到读模式
allocate.flip();
// 循环判断还有没有数据
while(allocate.hasRemaining()) {
System.out.println((char) allocate.get());
}
// 切换到写模式
allocate.clear();
}
} catch (IOException e) {
}
}
-
ByteBuffer的正确使用:
① 向ByteBuffer写入数据,channel.read(buffer)
② 使用flip()切换到读模式
③ 从ByteBuffer中读取数据,buffer.get()
④ 调用clear()或者compact()切换到写模式 -
rewind()
重头开始读取。 -
buffer.mark()
标记当前position的位置,和reset()
一起使用,回到mark的位置进行读取。
@Test
public void ByteBufferTest1() {
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put(new byte[]{'a', 'b', 'c', 'd'});
// 切换到读模式
buffer.flip();
// System.out.println((char) buffer.get());
// // 重置position到开头,重新进行读取
// buffer.rewind();
// ByteBufferUtils.debugAll(buffer);
// System.out.println((char) buffer.get());
// 使用mark标记,和reset方法
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
buffer.mark();
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
// position跳转到mark标记时的位置
buffer.reset();
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
}
3. ByteBuffer的分配
@Test
public void ByteBuffer3() {
ByteBuffer buffer = ByteBuffer.allocate(16);
ByteBuffer directBuffer = ByteBuffer.allocateDirect(16);
/**
* class java.nio.HeapByteBuffer
* 分配的堆内存,读写效率低,收到GC的影响
*/
System.out.println(buffer.getClass());
/**
* class java.nio.DirectByteBuffer
* 分配的直接内存,读写效率高(少一次拷贝),不受GC影响
*/
System.out.println(directBuffer.getClass());
}
4. ByteBuffer与字符串之间的转化
@Test
public void testByteBufferToString() {
// 1. 直接通过字符串的字节数组传递
ByteBuffer buffer1 = ByteBuffer.allocate(16);
buffer1.put("hello".getBytes());
ByteBufferUtils.debugAll(buffer1);
buffer1.put("中国".getBytes());
ByteBufferUtils.debugAll(buffer1);
// 要切换为写模式
buffer1.flip();
// 将byteBuffer解码成字符串
String str = StandardCharsets.UTF_8.decode(buffer1).toString();
System.out.println(str);
// 2. 使用 StandardCharsets.UTF_8.encode, 会自动切换到读模式
ByteBuffer buffer2 = StandardCharsets.UTF_8.encode("hello中国");
ByteBufferUtils.debugAll(buffer2);
String str2 = StandardCharsets.UTF_8.decode(buffer2).toString();
System.out.println(str2);
}
5. ByteBuffer处理粘包、半包
/**
* 处理粘包、半包
*/
@Test
public void testTcp() {
ByteBuffer buffer = ByteBuffer.allocate(60);
buffer.put("Hello,World\nI'm Hanmeimei\n中国".getBytes());
split(buffer);
buffer.put("你好\n".getBytes());
split(buffer);
}
/**
* 处理粘包、半包
* @param buffer
*/
public void split(ByteBuffer buffer) {
// 切换到读模式
buffer.flip();
// 遍历读取到的数据
for(int i = 0; i < buffer.limit(); i++) {
// get(index),不会移动position位置
if(buffer.get(i) == '\n') {
// 计算当前读取到的一条信息的长度
int length = i + 1 - buffer.position();
ByteBuffer buf = ByteBuffer.allocate(length);
for(int j = 0; j < length; j++) {
buf.put(buffer.get());
}
ByteBufferUtils.debugAll(buf);
buf.flip();
String str = StandardCharsets.UTF_8.decode(buf).toString();
System.out.println(str);
}
}
// 切换到写模式,并且留下没有读取的数据,解决半包
buffer.compact();
}
- FileChannel的零拷贝
@Test
public void transferTo() {
try (
FileChannel from = new FileInputStream("test.txt").getChannel();
FileChannel to = new FileOutputStream("data.txt").getChannel();
) {
// 使用操作系统的零拷贝,最大传输2G
from.transferTo(0, from.size(), to);
} catch (IOException e) {
e.printStackTrace();
};
}
/**
* 传输2G以上的大文件
*/
@Test
public void transferTo2() {
try (
FileChannel from = new FileInputStream("data.txt").getChannel();
FileChannel to = new FileOutputStream("to.txt").getChannel();
) {
// 总共多少字节
long size = from.size();
// 还剩多少字节没传输
long index = size;
while(index > 0) {
System.out.println("position: " + (size - index) + " size: " + index);
// 返回的是传输了多少字节
index -= from.transferTo(size - index, index, to);
}
} catch (IOException e) {
}
}
6. 使用Files遍历文件夹
public class TestFilesWalkFileTree {
public static void main(String[] args) throws IOException {
// 遍历目录下的所有目录以及文件
// walkFileTree();
// 过滤出jdk8目录下的jar文件
filterJar();
}
private static void filterJar() throws IOException {
AtomicInteger fileCount = new AtomicInteger();
Files.walkFileTree(Paths.get("D:\\java1\\jdk1.8"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if(file.toString().endsWith("jar")) {
System.out.println(file);
fileCount.incrementAndGet();
}
return super.visitFile(file, attrs);
}
});
System.out.println(fileCount);
}
private static void walkFileTree() throws IOException {
AtomicInteger dirCount = new AtomicInteger();
AtomicInteger fileCount = new AtomicInteger();
// 遍历文件夹下的所有目录和文件
Files.walkFileTree(Paths.get("D:\\java1\\jdk1.8"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("dir: " + dir);
// 统计目录数量
dirCount.incrementAndGet();
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("file: " + file);
fileCount.incrementAndGet();
return super.visitFile(file, attrs);
}
});
System.out.println("目录数量:" + dirCount);
System.out.println("文件数量:" + fileCount);
}
}
7. 使用Files删除多级目录
/**
* 删除多级目录
*/
public class TestFilesWalkFileTree2 {
public static void main(String[] args) throws IOException {
Files.walkFileTree(Paths.get("F:\\logs"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
// 打开文件之前做的操作
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
// 打开文件的时候,删除文件
Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
// 打开文件之后的操作
// 删除文件夹
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
}
}
8. 复制文件夹
/**
* 复制文件夹
*/
public class TestFilesWalkFileTree2 {
public static void main(String[] args) throws IOException {
String source = "F:\\web\\logs";
String target = "F:\\web\\logsaaa";
Files.walk(Paths.get(source)).forEach(path -> {
// 创建新的目录
String newPath = path.toString().replace(source, target);
try {
// 是不是目录
if(Files.isDirectory(path)) {
Files.createDirectory(Paths.get(newPath));
}
// 是文件
if(Files.isRegularFile(path)) {
// 复制文件
Files.copy(path, Paths.get(newPath));
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
9. 阻塞模式-单线程
- 阻塞模式,
ServerSocketChannel.accept()
方法是阻塞等待客户端连接,SocketChannel.read()
阻塞等待客户端的写入。 - 阻塞模式在多客户端连接的情况下,使用一个单线程处理,那么可能线程阻塞在accpet()方法,就无法处理read()方法。
@Slf4j
public class Service {
public static void main(String[] args) throws IOException {
// 创建一个服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定一个端口号
serverSocketChannel.bind(new InetSocketAddress(8080));
ByteBuffer buffer = ByteBuffer.allocate(16);
List<SocketChannel> socketChannelList = new ArrayList<>();
while(true) {
// 获取一个连接
log.debug("connecting ...");
/*
accept()是一个阻塞方法,线程会阻塞等待客户端连接
*/
SocketChannel socketChannel = serverSocketChannel.accept();
log.debug("connected ... {}", socketChannel);
socketChannelList.add(socketChannel);
// 遍历所有连接
for(SocketChannel channel : socketChannelList) {
log.debug("before read... {}", channel);
/*
read()是一个阻塞方法,等待客户端输入数据
*/
channel.read(buffer);
// 切换读模式
buffer.flip();
ByteBufferUtils.debugAll(buffer);
buffer.clear();
log.debug("after read... {}", channel);
}
}
}
}
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(8080));
// debug运行调试
System.out.println("waiting ...");
}
}
10. 非阻塞模式-单线程
ServerSocketChannel
和SocketChannel
开始非阻塞模式,都使用configureBlocking(false)
。- ServerSocketChannl的
accept()
方法和SocketChannel的read()
都不会阻塞。 - 在没有客户端连接之前accept()方法接收到的SocketChannel为null,read()读取到的字节数为0。
@Slf4j
public class Service {
public static void main(String[] args) throws IOException {
// 创建一个服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置成非阻塞模式,就不会在accept()阻塞
serverSocketChannel.configureBlocking(false);
// 绑定一个端口号
serverSocketChannel.bind(new InetSocketAddress(8080));
ByteBuffer buffer = ByteBuffer.allocate(16);
List<SocketChannel> socketChannelList = new ArrayList<>();
while(true) {
/**
* 不会阻塞在这里,等待客户端连接,没有连接之前socketChannel都是nul
*/
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel != null) {
// 设置为非阻塞模式,不会阻塞在read()方法
socketChannel.configureBlocking(false);
log.debug("connected ... {}", socketChannel);
socketChannelList.add(socketChannel);
}
// 遍历所有连接
for(SocketChannel channel : socketChannelList) {
/**
* read为0说明没有读取到数据
*/
int read = channel.read(buffer);
if(read > 0) {
// 切换读模式
buffer.flip();
ByteBufferUtils.debugAll(buffer);
buffer.clear();
log.debug("after read... {}", channel);
}
}
}
}
}
11. Selector
- SelectionKey的几个事件:
- accept : 有连接请求时触发。
- connect : 客户端,连接建立后触发。
- read : 可读事件。
- write : 可写事件。
- Selector是工作在非阻塞模式下的,所有Channel都必须要设置
configureBlocking(false)
。 - Selector的
select()
方法,会阻塞等待事件发生,但是如果有事件没有处理,就不会阻塞,所有事件一定要处理或者cancel()。 - 处理完事件之后,一定要移除SelectionKey。如果不移除,可能下一次发生了其他事件,该key上绑定的事件就为空,出现异常。
@Slf4j
public class SelectorTest {
public static void main(String[] args) throws IOException {
// 创建一个Selector
Selector selector = Selector.open();
// 创建一个服务器端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
/*
将服务器端的channel注册到selector上
第二个参数可以选择感兴趣的事件
返回一个SelectionKey
*/
SelectionKey serverKey = serverSocketChannel.register(selector, 0, null);
// 给key注册一个感兴趣的事件,连接事件accept
serverKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("{}", serverKey);
while(true) {
/*
阻塞等待事件发生
如果有事件未处理,那么select不会阻塞,可以调用cancel()方法
*/
log.debug("blocking ...");
selector.select();
log.debug("have an event");
// 获取所有发生事件的key
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
// 获取发生事件的SelectionKey
SelectionKey key = iterator.next();
/*
处理完key的事件一定要从selectionKey集合上删除
不然下次其他key发生了事件,以前的key还在,但是没有真正的发生监听的事件
可能就会报错
*/
iterator.remove();
log.debug("event selectionKey {}", key);
// 区分事件类型
if (key.isAcceptable()) {
// 客户端连接事件
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
// 获取连接服务端的channel
SocketChannel socketChannel = channel.accept();
log.debug("{}", socketChannel);
// 设置客户端channel非阻塞
socketChannel.configureBlocking(false);
// 将客户端channel注册到select中
SelectionKey clientKey = socketChannel.register(selector, 0, null);
// 关心读事件
clientKey.interestOps(SelectionKey.OP_READ);
} else if(key.isReadable()) {
// 可读事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
channel.read(buffer);
buffer.flip();
ByteBufferUtils.debugAll(buffer);
}
// 不处理事件
// key.cancel();
}
}
}
}
12. 处理客户端断开连接
- 不管是客户端强制退出,还是正常退出(
SocketChannel.close()
),都会触发一次可读事件。 - 客户端退出之后,要将客户端对应的key从SelectionKey中移除。
key.cancel()
,不然就会一直监听到可读事件。 - 客户端正常退出时,
SocketChannel.read()
方法会返回-1
。
@Slf4j
public class SelectorTest {
public static void main(String[] args) throws IOException {
// 创建一个Selector
Selector selector = Selector.open();
// 创建一个服务器端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
/*
将服务器端的channel注册到selector上
第二个参数可以选择感兴趣的事件
返回一个SelectionKey
*/
SelectionKey serverKey = serverSocketChannel.register(selector, 0, null);
// 给key注册一个感兴趣的事件,连接事件accept
serverKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("{}", serverKey);
while(true) {
/*
阻塞等待事件发生
如果有事件未处理,那么select不会阻塞,可以调用cancel()方法
*/
log.debug("blocking ...");
selector.select();
log.debug("have an event");
// 获取所有发生事件的key
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
// 获取发生事件的SelectionKey
SelectionKey key = iterator.next();
/*
处理完key的事件一定要从selectionKey集合上删除
不然下次其他key发生了事件,以前的key还在,但是没有真正的发生监听的事件
可能就会报错
*/
iterator.remove();
log.debug("event selectionKey {}", key);
// 区分事件类型
if (key.isAcceptable()) {
// 客户端连接事件
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
// 获取连接服务端的channel
SocketChannel socketChannel = channel.accept();
log.debug("{}", socketChannel);
// 设置客户端channel非阻塞
socketChannel.configureBlocking(false);
// 将客户端channel注册到select中
SelectionKey clientKey = socketChannel.register(selector, 0, null);
// 关心读事件
clientKey.interestOps(SelectionKey.OP_READ);
} else if(key.isReadable()) {
// 可读事件
// 不管是客户端正常断开连接,还是强制退出连接,都会触发一次可读事件
try {
SocketChannel channel = (SocketChannel) key.channel();
channel.close();
ByteBuffer buffer = ByteBuffer.allocate(16);
/**
* 如果客户端是正常退出,read()方法返回-1
*/
int read = channel.read(buffer);
if(read == -1) {
// 正常退出,断开连接
key.cancel();
} else {
buffer.flip();
ByteBufferUtils.debugAll(buffer);
}
} catch (IOException e) {
e.printStackTrace();
// 捕捉到客户端异常断开的异常,将key 取消
key.cancel();
}
}
}
}
}
}
13. 处理消息边界
-
SocketChannel.read(ByteBuffer)
如果ByteBuffer分配的字节数较少,消息的长度大于buffer的容量,那么会触发好几次可读事件。 -
常见处理消息边界的方法
① 服务器端固定消息长度,如果客户端传输的数据较小,可能浪费空间。
② 使用分隔符拆分,要每个字节进行遍历,效率较低。
③ Http2.0使用的TLV格式,使用几个字节表示传递消息的长度。 -
在Channel注册到Selector时,
register
方法的第三个参数,是添加一个附件,可以为每个Channel添加一个独立的ByteBuffer。 -
使用SelectionKey的
attachment()
方法获取附件。 -
SelectionKey的
attach()
添加一个附件。
@Slf4j
public class SelectorTest {
public static void main(String[] args) throws IOException {
// 创建一个Selector
Selector selector = Selector.open();
// 创建一个服务器端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
/*
将服务器端的channel注册到selector上
第二个参数可以选择感兴趣的事件
返回一个SelectionKey
*/
SelectionKey serverKey = serverSocketChannel.register(selector, 0, null);
// 给key注册一个感兴趣的事件,连接事件accept
serverKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("{}", serverKey);
while (true) {
/*
阻塞等待事件发生
如果有事件未处理,那么select不会阻塞,可以调用cancel()方法
*/
log.debug("blocking ...");
selector.select();
log.debug("have an event");
// 获取所有发生事件的key
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
// 获取发生事件的SelectionKey
SelectionKey key = iterator.next();
/*
处理完key的事件一定要从selectionKey集合上删除
不然下次其他key发生了事件,以前的key还在,但是没有真正的发生监听的事件
可能就会报错
*/
iterator.remove();
log.debug("event selectionKey {}", key);
// 区分事件类型
if (key.isAcceptable()) {
// 客户端连接事件
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
// 获取连接服务端的channel
SocketChannel socketChannel = channel.accept();
log.debug("{}", socketChannel);
// 设置客户端channel非阻塞
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(10);
/*
将客户端channel注册到select中,将buffer注册到key中
*/
SelectionKey clientKey = socketChannel.register(selector, 0, buffer);
// 关心读事件
clientKey.interestOps(SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 可读事件
// 不管是客户端正常断开连接,还是强制退出连接,都会触发一次可读事件
try {
SocketChannel channel = (SocketChannel) key.channel();
// 取出key中绑定的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
/**
* 如果客户端是正常退出,read()方法返回-1
*/
int read = channel.read(buffer);
if (read == -1) {
// 正常退出,断开连接
key.cancel();
} else {
// 处理客户端传递过来的消息
split(buffer);
// 判断是否解析出一条完整的消息
if (buffer.position() == buffer.limit()) {
/*
说明一条消息还没解析完,ByteBuffer就已经不够用了
扩容
*/
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
// 复制老buffer的数据
buffer.flip();
newBuffer.put(buffer);
// 绑定到key上
key.attach(newBuffer);
}
}
} catch (IOException e) {
e.printStackTrace();
// 捕捉到客户端异常断开的异常,将key 取消
key.cancel();
}
}
}
}
}
/**
* 处理粘包、半包
*
* @param buffer
*/
public static void split(ByteBuffer buffer) {
// 切换到读模式
buffer.flip();
// 遍历读取到的数据
for (int i = 0; i < buffer.limit(); i++) {
// get(index),不会移动position位置
if (buffer.get(i) == '\n') {
// 计算当前读取到的一条信息的长度
int length = i + 1 - buffer.position();
ByteBuffer buf = ByteBuffer.allocate(length);
for (int j = 0; j < length; j++) {
buf.put(buffer.get());
}
ByteBufferUtils.debugAll(buf);
buf.flip();
String str = StandardCharsets.UTF_8.decode(buf).toString();
System.out.println(str);
}
}
// 切换到写模式,并且留下没有读取的数据,解决半包
buffer.compact();
}
}
14. 写事件-写入内容太多
- 当写入大量数据时,有可能一次写不完。
SocketChannel.write()
返回一个int整型,代表实际写入的字节数据。 buffer.hasRemaining()
:buffer中是否还有数据没写出。
public class WriteServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
// 发送大量数据给客户端
StringBuilder builder = new StringBuilder();
for(int i = 0; i < 300000; i++) {
builder.append("a");
}
ByteBuffer buffer = Charset.defaultCharset().encode(builder.toString());
/*
数据量大可能一次写不完,返回的是实际写入的字节数
hasRemaining还有字节没发送就继续读取
*/
while(buffer.hasRemaining()) {
int write = socketChannel.write(buffer);
System.out.println(write);
}
}
}
}
}
}
15. 处理可写事件
public class WriteServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
SelectionKey socketChannelKey = socketChannel.register(selector, 0, null);
// 发送大量数据给客户端
StringBuilder builder = new StringBuilder();
for(int i = 0; i < 300000; i++) {
builder.append("a");
}
ByteBuffer buffer = Charset.defaultCharset().encode(builder.toString());
/*
数据量大可能一次写不完,返回的是实际写入的字节数
hasRemaining还有字节没发送就继续读取
*/
// while(buffer.hasRemaining()) {
// int write = socketChannel.write(buffer);
// System.out.println(write);
// }
if(buffer.hasRemaining()) {
// 如果buffer中还有数据,注册一个可写事件
socketChannelKey.interestOps(socketChannelKey.interestOps() + SelectionKey.OP_WRITE);
// 绑定一个buffer
socketChannelKey.attach(buffer);
}
} else if(key.isWritable()) {
// 如果读取到了write事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 向客户端写入
int write = channel.write(buffer);
System.out.println(write);
// 如果buffer中没有了数据,就移除可写事件
if(!buffer.hasRemaining()) {
key.attach(null);
key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
}
}
}
}
}
}
16. 多线程worker优化
selector.select()
方法会阻塞等待事件发生,channel.register()
如果在select之后,那么select阻塞做了,就不会处理register事件。- 可以使用
selector.wakeup()
唤醒select()的阻塞。 - 两个线程之间传递数据可以使用一个队列。
@Slf4j
public class MultiThreadServer {
public static void main(String[] args) throws IOException {
// 使用一个boss线程,专门处理accept连接
Thread.currentThread().setName("boss");
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
Selector selector = Selector.open();
SelectionKey serverKey = serverSocketChannel.register(selector, 0, null);
serverKey.interestOps(SelectionKey.OP_ACCEPT);
// 创建一个worker,监听读写事件
Worker worker = new Worker("work-0");
while(true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
log.debug("connect... , {}", channel.getRemoteAddress());
// 将channel注册到worker中
log.debug("before register... , {}", channel.getRemoteAddress());
worker.register(channel);
log.debug("after register... , {}", channel.getRemoteAddress());
}
}
}
}
/**
* 使用一个worker线程,专门处理可读可写事件
*/
static class Worker implements Runnable{
private Thread thread;
private Selector selector;
private String name;
private volatile boolean start = false;
// 使用一个队列传递数据
private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
public Worker(String name) {
this.name = name;
}
/**
* 初始化方法
*/
public void register(SocketChannel channel) throws IOException {
// 保证线程只初始化一次
if(!start) {
thread = new Thread(this, name);
selector = Selector.open();
thread.start();
}
/*
如果selector.select()方法先执行,那么就会阻塞做,register就不能注册到seletor中
*/
queue.add(() -> {
try {
channel.register(selector, SelectionKey.OP_READ, null);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
});
/**
* 唤醒selector的阻塞
*/
selector.wakeup();
}
@Override
public void run() {
while(true) {
try {
// worker线程监听
selector.select();
// 获取队列中的register事件
Runnable poll = queue.poll();
if(poll != null) {
poll.run();
}
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
SocketChannel channel = (SocketChannel) key.channel();
channel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16);
channel.read(buffer);
buffer.flip();
ByteBufferUtils.debugAll(buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
17. 多个Worker
@Slf4j
public class MultiThreadServer {
public static void main(String[] args) throws IOException {
// 使用一个boss线程,专门处理accept连接
Thread.currentThread().setName("boss");
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
Selector selector = Selector.open();
SelectionKey serverKey = serverSocketChannel.register(selector, 0, null);
serverKey.interestOps(SelectionKey.OP_ACCEPT);
// 多个worker
Worker[] workers = new Worker[2];
for(int i = 0; i < workers.length; i++) {
workers[i] = new Worker("work-" + i);
}
AtomicInteger index = new AtomicInteger();
while(true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
log.debug("connect... , {}", channel.getRemoteAddress());
// 将channel注册到worker中
log.debug("before register... , {}", channel.getRemoteAddress());
// 轮询注册到多个worker中
workers[index.incrementAndGet() % workers.length].register(channel);
log.debug("after register... , {}", channel.getRemoteAddress());
}
}
}
}
/**
* 使用一个worker线程,专门处理可读可写事件
*/
static class Worker implements Runnable{
private Thread thread;
private Selector selector;
private String name;
private volatile boolean start = false;
// 使用一个队列传递数据
private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
public Worker(String name) {
this.name = name;
}
/**
* 初始化方法
*/
public void register(SocketChannel channel) throws IOException {
// 保证线程只初始化一次
if(!start) {
thread = new Thread(this, name);
selector = Selector.open();
thread.start();
}
/*
如果selector.select()方法先执行,那么就会阻塞做,register就不能注册到seletor中
*/
queue.add(() -> {
try {
channel.register(selector, SelectionKey.OP_READ, null);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
});
/**
* 唤醒selector的阻塞
*/
selector.wakeup();
}
@Override
public void run() {
while(true) {
try {
// worker线程监听
selector.select();
// 获取队列中的register事件
Runnable poll = queue.poll();
if(poll != null) {
poll.run();
}
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
SocketChannel channel = (SocketChannel) key.channel();
channel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16);
log.debug("read ...{}", channel);
channel.read(buffer);
buffer.flip();
ByteBufferUtils.debugAll(buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
18. IO模型
阻塞IO
:Java程序切换到Linux内核空间调用操作系统的read读取方法,如果操作系统没有返回,线程就会阻塞。非阻塞IO
:Java程序在操作系统的等待数据阶段,是不阻塞的,如果读取到的字节数为0,也会立即返回。但是在操作系统复制数据阶段(从网卡将数据复制出来),线程还是需要阻塞等待数据。多路复用
:在阻塞模式中,如果线程阻塞在accept中,那么另一个channel的read读取事件,就只能在不阻塞后在处理。多路复用使用selector监听多个事件。异步阻塞
:异步就是通过另外一个线程处理事情,然后调用一个回调函数获取结果。异步的本质是没有阻塞的。异步非阻塞
。
19. 零拷贝
-
内部工作流程:
-
java本身是不具备IO读写能力的,调用read方法后,java程序要从用户态切换到核心态,调用操作系统的Kernel的读能力,将数据读到内核缓冲区,这期间,用户线程会阻塞,操作系统使用DMA实现文件,期间不会使用CPU.
- NIO的
ByteBuffer.allocateDirect(10)
,DirectByteBuffer
使用的是操作系统内存,少一次拷贝。
- java中的
FileChannel.transferTo
进一步优化,底层采用Linux2.1后提供的sendFile
方法。 - Java调用transferTo方法后,Java程序要从用户态切换到核心态,使用DMA将数据读入内核缓冲区,不会使用CPU。
- Linux2.4后进一步优化。
20. 异步IO-AIO
@Slf4j
public class AioFileChannel {
public static void main(String[] args) {
/*
获取一个异步IO的文件读取类
参数1:文件路径
参数2:操作类型(读写)
*/
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("F:\\IdeaProject\\github\\my-spring-boot-demo\\netty\\test.txt"), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(16);
/**
* 读取文件
* 参数1:接收文件的buffer
* 参数2:读取的起始位置
* 参数3:附件,如果没读取完,接着读取
* 参数4:异步读取之后的回调函数,回调是一个守护线程
*/
log.debug("read start...");
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
// 读取成功
@Override
public void completed(Integer result, ByteBuffer attachment) {
log.debug("read success... {}", result);
attachment.flip();
ByteBufferUtils.debugAll(attachment);
}
// 读取异常
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
}
});
log.debug("read end...");
// aio读取回调是一个守护线程,主线程不能停止
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Netty HelloWorld
- 服务器端
/**
* Hello World 服务器端
*/
public class HelloServer {
public static void main(String[] args) {
// 启动服务器,负责组装netty组件
new ServerBootstrap()
// 加入一个轮询事件组
.group(new NioEventLoopGroup())
// 服务器端ServerSocketChannel
.channel(NioServerSocketChannel.class)
// worker负责读写,执行read、write那些操作
.childHandler(new ChannelInitializer<NioSocketChannel>() {
// 数据读写的通道
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 解码handler:将ByteBuf转化为字符串
nioSocketChannel.pipeline().addLast(new StringDecoder());
// 自定义handler
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 触发了读事件
System.out.println(msg);
}
});
}
})
.bind(8888);
}
}
- 客户端
/**
* Hello World 客户端
*/
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
// 客户端启动器
new Bootstrap()
// 监听轮询分组
.group(new NioEventLoopGroup())
// 客户端的channel实现
.channel(NioSocketChannel.class)
// 处理器
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 添加一个编码处理
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
// 连接服务器
.connect(new InetSocketAddress("localhost", 8888))
// 阻塞方法,直到连接建立
.sync()
// 获取传输通道
.channel()
// 向服务器发送数据
.writeAndFlush("hello, world");
}
}
- Netty中数据的传输使用
ByteBuf
,是对JDK中的ByteBuffer
做了进一步封装。 - 服务器端启动后,
NioEventLoopGroup
中有一个select
会监听accept
事件,等待客户端连接,EventLoop就相当于Selector监听事件。
pipeline
(流水线),负责发布事件(读、读取完成…),传播给每个handler
,handler对感兴趣的事件进行处理。eventLoop
理解为处理数据的工人。- 工人可以管理多个channel的io操作,eventLoop会绑定一个channel,负责到底。
- eventLoop有任务队列。任务分为普通任务和定时任务。
- eventLoop底层使用了一个单线程的线程池。
Netty组件
1. EventLoop
- EventLoop本质上是一个单线程执行器(同时维护了一个
Selector
),里面有run方法,处理channel的io事件。 - EventLoop继承关系复杂:
- 一条线继承了JDK线程池中的
ScheduleExecutorService
(执行定时任务),因此包含线程池的所有方法。 - 另一条线继承Netty自己的
OrderedEventExecutor
。方法inEventLoop(Thread thread)
判断是否属于此EventLoop。parent()
方法,查看自己属于那个EventLoopGroup
。
2. EventLoopGroup
-
EventLoopGroup
就是一组EventLoop,Channel会调用EventLoopGroup中的register()
方法来绑定其中一个EventLoop
。后续,Channel发生了io事件,都有此EventLoop进行处理(为了保证线程安全)。 -
实现类:
NioEventLoopGroup
:能够处理IO事件、普通任务、定时任务。创建的默认线程数,max(1, cpu核心数*2)
DefaultEventLoopGroup
:处理普通任务、定时任务。
1. 执行普通任务、定时任务
- 从EventLoopGroup中可以获得EventLoop对象。EventLoop继承了线程池ScheduleExecutorService定时任务执行对象,可以执行普通任务和定时任务。
/**
* 测试EventLoop
*/
@Slf4j
public class TestEventLoop {
public static void main(String[] args) {
// 获取线程的核心数
// System.out.println(NettyRuntime.availableProcessors());
// 新建一个事件循环组
// 默认创建线程的数量:max(1, cpu核心数*2)
// private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
EventLoopGroup group = new NioEventLoopGroup(2);
// 获取事件循环对象EventLoop:指定2个对象,轮询获取
System.out.println(group.next());
System.out.println(group.next());
System.out.println(group.next());
System.out.println(group.next());
// 执行一个普通任务
// group.next().submit(() -> {
// try {
// Thread.sleep(2000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// log.debug("submit");
// });
// 执行一个定时任务,已一定频率执行
group.next().scheduleAtFixedRate(() -> {
log.debug("执行");
}, 0, 1, TimeUnit.SECONDS);
log.debug("main线程");
}
}
2. 执行IO任务
- 服务器端
@Slf4j
public class EventLoopServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 强转为ByteBuf对象
ByteBuf buf = (ByteBuf) msg;
// ByteBuf转化为String,指定字符集
log.debug(buf.toString(Charset.defaultCharset()));
}
});
}
})
.bind(8888);
}
}
- 客户端
@Slf4j
public class EventLoopClient {
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress(8888))
.sync()
.channel();
log.debug("channel: {}", channel);
// 断点停在这里,可以随便发数据,也可以启动多个客户端
System.out.println();
}
}
- DEBUG模式下,向服务器端发送数据。
3. 对EventLoopGroup进行细分
- 如果
NioEventLoopGroup
执行一个时间较长的任务,那会就会影响到其他Channel的处理。 - 可以新建一个
DefaultEventLoopGroup
,对时间长的任务进行单独处理,而不影响NioEventLoopGroup对其他Channel的处理。
@Slf4j
public class EventLoopServer {
public static void main(String[] args) {
// 创建一个EventLoop
EventLoopGroup group = new DefaultEventLoopGroup();
new ServerBootstrap()
// 创建两个事件轮询组,第一个只负责监听Accept事件,第二个负责读写事件
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 强转为ByteBuf对象
ByteBuf buf = (ByteBuf) msg;
// ByteBuf转化为String,指定字符集
log.debug(buf.toString(Charset.defaultCharset()));
// 传递给下面的handler链继续处理
ctx.fireChannelRead(msg);
}
})
// 指定group进行处理
.addLast(group, new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(Charset.defaultCharset()));
}
});
}
})
.bind(8888);
}
}
4. EventLoop之间的切换源码分析
- 在EventLoop之间切换就是切换线程,因为一个EventLoop绑定一个线程。
- 线程之间切换,数据是如何传输的
- 主要的代码在
io.netty.channel.AbstractChannelHandlerContext
类中。
// 参数1:AbstractChannelHandlerContext 通道处理上下文,可以获取通道channel
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
// 使用通道处理上下文,获得通道对应的EventLoop
EventExecutor executor = next.executor();
// 判断是不是同一个EventLoop,也就是判断是不是同一个线程处理
if (executor.inEventLoop()) {
// 如果是,就直接进行处理
next.invokeChannelRead(m);
} else {
// 如果不是EventLoop是间接继承了线程池
// ScheduledExecutorService,创建一个线程执行
executor.execute(new Runnable() {
public void run() {
next.invokeChannelRead(m);
}
});
}
}
3. Channel
close()
:关闭Channel。closeFuture()
:处理Channel的关闭。
sync()
:同步等待。(当前线程等待(阻塞),EventLoop的线程将客户端和服务器建立连接之后,唤醒当前线程继续运行)addListener()
:异步等待。(在EventLoop中的线程建立完连接之后,会回调加入的方法)
pipeline()
:添加处理器handler
。write()
:将数据写入,但是可能不会立即将数据通过网络发出去,有一个缓冲机制。可以调用flush()
将缓冲区的数据发送。writeAndFlush()
:将数据写入,并刷出。
1. sync和addListener
- 演示sync()代码:
@Slf4j
public class EventLoopClient {
public static void main(String[] args) throws InterruptedException {
// Future和Promise类型都是和异步方法配套使用
// 调用connect得到ChannelFuture对象
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
// connect是异步非阻塞的,连接操作是由NioEventLoop中的线程去连接客户端
// main线程继续向下执行
.connect(new InetSocketAddress(8888));
// main线程会等待nio线程将客户端和服务器之间建立连接
channelFuture.sync();
// 获取连接的channel对象:如果不调用sync()方法,可能另一个线程还没有建立连接
Channel channel = channelFuture.channel();
log.debug("channel: {}", channel);
// 发送数据
channel.writeAndFlush("123");
System.out.println();
}
}
- 演示addListen()代码:
@Slf4j
public class EventLoopClient {
public static void main(String[] args) throws InterruptedException {
// Future和Promise类型都是和异步方法配套使用
// 调用connect得到ChannelFuture对象
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
// connect是异步非阻塞的,连接操作是由NioEventLoop中的线程去连接客户端
// main线程继续向下执行
.connect(new InetSocketAddress(8888));
// 加入异步回调
channelFuture.addListener(new ChannelFutureListener() {
// EventLoop中的线程建立好与服务器之间的连接后,会回调此方法
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
// 获取channel
Channel channel = channelFuture.channel();
log.debug("channel {}", channel);
channel.writeAndFlush("13245600");
}
});
// // main线程会等待nio线程将客户端和服务器之间建立连接
channelFuture.sync();
// 获取连接的channel对象:如果不调用sync()方法,可能另一个线程还没有建立连接
Channel channel = channelFuture.channel();
log.debug("channel: {}", channel);
// 发送数据
channel.writeAndFlush("123");
System.out.println();
}
}
2. 使用ChannelFuture正确的关闭Channel
- channel.close()也是一个异步操作,由EventLoop里面的线程关闭Channel通道。
- 如果正确的在channel关闭之后,做一些操作。
- 可以使用上面的
sync()
等待关闭完之后,也可以使用addListener()
,将关闭后的任务,交给EventLoop的线程。
/**
* 从控制台输入内容,发送到服务器
*/
@Slf4j
public class CloseFutureClient {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
ChannelFuture channelFuture = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 设置打印日记,可以查看netty内部的打印
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
// 设置编码
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress(8888));
Channel channel = channelFuture.sync().channel();
// 新建一个线程处理控制台输入
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
// 循环从控制台输入
while(true) {
String line = scanner.nextLine();
if("ex".equals(line)) {
// 断开连接:close也是一个异步执行
channel.close();
break;
} else {
channel.writeAndFlush(line);
}
}
}, "线程1").start();
// 正确处理,channel.close():方式一
// 获取一个关闭channel对象
ChannelFuture closeFuture = channel.closeFuture();
log.debug("channelFuture : {}", closeFuture);
/*
// 阻塞等待channel关闭
closeFuture.sync();
// 下面就是正确执行,channel关闭之后的流程
log.debug("channel 关闭成功,退出连接");
*/
// 正确处理,channel.close():方式二
closeFuture.addListener(new ChannelFutureListener() {
// 这个方法是close()的线程回调执行
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
log.debug("channel 关闭成功,退出连接");
// 优雅的关闭EventLoopGroup,事件轮询组
// 会把没执行完的任务执行完
group.shutdownGracefully();
}
});
}
}
4. Future和Promise
- 才进行异步处理时,经常使用这两个接口。
- Netty中的Future继承子JDK中的Future,Promise对Netty中的Future进行了扩展。
- JDK中的Future只能同步等任务结束,才能得到结果。
- Netty中的Future可以同步等待任务结束得到结果,也可以异步获取结果,但是都要任务结束。
- Netty中的Promise有Future的功能,而且脱离任务存在,只作为两个线程之间传递结果的容器。
1. JDK中的Future演示:
/**
* jdk中的Future的测试
*/
@Slf4j
public class JDKFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 新建一个线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
Future<Integer> future = threadPool.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("执行计算");
Thread.sleep(1000);
return 50 - 10;
}
});
// future或阻塞等待,计算结果
log.debug("计算结果为:{}", future.get());
}
}
2. Netty中的Future演示:
/**
* 测试netty的Future
*/
@Slf4j
public class NettyFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 新建一个事件循环组
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
// 获取的一个EventLoop,相当于获得一个线程
EventLoop eventLoop = eventLoopGroup.next();
// 提交一个任务
// 这里的Future是netty包下的Future
Future<Integer> future = eventLoop.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("执行计算");
Thread.sleep(1000);
return 40 - 20;
}
});
// main线程等待结果
// log.debug("结果:{}", future.get());
// 异步获取结果:由执行计算的线程执行
future.addListener(new GenericFutureListener<Future<? super Integer>>() {
@Override
public void operationComplete(Future<? super Integer> future) throws Exception {
log.debug("异步获取结果:{}", future.getNow());
}
});
}
}
3. Promise的演示
/**
* Netty中Promise和Future比较相似,Future是线程执行完计算返回
* Promise可以主动创建,并且可以返回错误的信息
*/
@Slf4j
public class NettyPromise {
public static void main(String[] args) throws ExecutionException, InterruptedException {
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
EventLoop eventLoop = eventLoopGroup.next();
// 创建一个Promise对象
DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);
// 新建一个线程执行任务
new Thread(() -> {
try {
log.debug("开始计算");
Thread.sleep(1000);
// int i = 1 / 0;
// 使用promise填充计算结果
promise.setSuccess(80);
} catch (InterruptedException e) {
e.printStackTrace();
// 出现错误就填充一个异常对象
promise.setFailure(e);
}
}).start();
// 同步获取
log.debug("计算结果:{}", promise.get());
// 异步获取获取
// promise.addListener();
}
}
5. Handle和Pipeline
ChannelHandle
用来处理Channel上的各种事件,分为入站(数据读取操作)、出站(数据写入操作)两种。- 入站(
ChannelInboundHandler
)和出站(ChannelOutboundHandler
)之间有一个明显的区别:若数据是从用户应用程序到远程主机则是“出站(outbound)”,相反若数据时从远程主机到用户应用程序则是“入站(inbound)”。 - 所有的ChannelHandler连成一串,就是
Pipeline
。 - 入站处理器通常是
ChannelboundHandlerAdapter
的子类,主要用来读取客户端数据,写回结果。 - 出战处理器通常是
ChannelOutboundHandlerAdapter
的子类,主要对写数据加工。 - 入站处理器,是顺序执行。出战处理器,是逆序执行。
- 管道Channel调用write写出数据,出战处理器才会执行。
- 处理器默认有一个
head
和tail
,形成一条处理器链,调用处理器管道的writeAndFlush()
方法,整个出站处理器链调用从tail开始,如果是当前处理器链上的ChannelHandlerContext
调用了writeAndFlush()方法,那么就是向前执行出站处理器。 - 代码演示:
@Slf4j
public class PipelineTest {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
// 添加一个ChannelInboundHandlerAdapter入站处理器的实现类
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 拿到一个Pipeline
ChannelPipeline pipeline = nioSocketChannel.pipeline();
// 添加一些处理器,本来有一个 head -> tail
pipeline.addLast("handler1", new ChannelInboundHandlerAdapter() {
// 监听读取事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("handler1");
ByteBuf byteBuf = (ByteBuf) msg;
String str = byteBuf.toString(Charset.defaultCharset());
// 必须调用数据才会传递到下一个责任链
super.channelRead(ctx, str);
}
});
pipeline.addLast("handler2", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("handler2");
String str = (String) msg;
log.debug("2。。。" + str);
super.channelRead(ctx, msg);
}
});
pipeline.addLast("handler3", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("handler3");
super.channelRead(ctx, msg);
// 只有写入数据,下面的出战处理器才会被调用,会从tail开始向前寻找出站处理器链
// ctx.alloc().buffer() 分配一个ByteBuf
// nioSocketChannel.writeAndFlush(ctx.alloc().buffer().writeBytes("nihao".getBytes()));
// 只会从当前处理器,向前寻找出站处理器链
ctx.writeAndFlush(ctx.alloc().buffer().writeBytes("nihao".getBytes()));
}
});
// 添加一些出战处理器:出战处理器的调用顺序是相反的
pipeline.addLast("outHandler1", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("outHandler1");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("outHandler2", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("outHandler2");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("outHandler3", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("outHandler3");
super.write(ctx, msg, promise);
}
});
}
})
.bind(8888);
}
}
- 使用一个netty提供的,测试Handler的处理工具类
EmbeddedChannel
。
// 测试netty中的工具类,EmbeddedChannel,可以用来调试handler处理器
@Slf4j
public class EmbeddedChannelTest {
public static void main(String[] args) {
// 新建两个入站处理器
ChannelInboundHandlerAdapter inboundHandler1 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("in handler1");
super.channelRead(ctx, msg);
}
};
ChannelInboundHandlerAdapter inboundHandler2 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("in handler2");
super.channelRead(ctx, msg);
}
};
// 新建两个出站处理器
ChannelOutboundHandlerAdapter outboundHandler1 = new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("out handler1");
super.write(ctx, msg, promise);
}
};
ChannelOutboundHandlerAdapter outboundHandler2 = new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("out handler2");
super.write(ctx, msg, promise);
}
};
// 创建一个netty提供的测试Handler的工具类
EmbeddedChannel channel = new EmbeddedChannel(inboundHandler1, inboundHandler2, outboundHandler1, outboundHandler2);
// 模拟入站操作,查看处理链的执行流程
channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello".getBytes()));
// 模拟出站操作
channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("world".getBytes()));
}
}
6. ByteBuf
ByteBufAllocator.DEFALUT.buffer()
或ByteBufAllocator.DEFALUT.directBuffer()
:分配的是直接内存。ByteBufAllocator.DEFALUT.heapBuffer()
:分配的堆内存。- ByteBuf默认字节数是256。可以自动扩容。
- 默认是开启池化功能的。(类似于线程池、连接池)
public class ByteBufTest {
public static void main(String[] args) {
// 创建一个默认的ByteBuf:默认字节为256,可以自动扩容
// 是直接内存的,默认开启池化功能
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
System.out.println(byteBuf.getClass());
ByteBuf heapBuffer = ByteBufAllocator.DEFAULT.heapBuffer();
System.out.println(heapBuffer.getClass());
// 也可以创建一个指定容量的ByteBuf
// ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
// 写入数据
byteBuf.writeBytes("nihao".getBytes());
// 可以查看byteBuf的写索引、读索引、和容量
// System.out.println(byteBuf);
// 使用log工具方法,查看byteBuf内部
log(byteBuf);
}
private static void log(ByteBuf buffer) {
int length = buffer.readableBytes();
int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
StringBuilder buf = new StringBuilder(rows * 80 * 2)
.append("read index:").append(buffer.readerIndex())
.append(" write index:").append(buffer.writerIndex())
.append(" capacity:").append(buffer.capacity())
.append(NEWLINE);
appendPrettyHexDump(buf, buffer);
System.out.println(buf.toString());
}
}
1. ByteBuf的组成
2. 写入方法
writeBoolean(boolean value) | 写入 boolean 值 | 用一字节 01|00 代表 true|false |
---|---|---|
writeByte(int value) | 写入 byte 值 | |
writeShort(int value) | 写入 short 值 | |
writeInt(int value) | 写入 int 值 | Big Endian,即 0x250,写入后 00 00 02 50 |
writeIntLE(int value) | 写入 int 值 | Little Endian,即 0x250,写入后 50 02 00 00 |
writeLong(long value) | 写入 long 值 | |
writeChar(int value) | 写入 char 值 | |
writeFloat(float value) | 写入 float 值 | |
writeDouble(double value) | 写入 double 值 | |
writeBytes(ByteBuf src) | 写入 netty 的 ByteBuf | |
writeBytes(byte[] src) | 写入 byte[] | |
writeBytes(ByteBuffer src) | 写入 nio 的 ByteBuffer | |
int writeCharSequence(CharSequence sequence, Charset charset) | 写入字符串 | CharSequence是String、StringBuilder、StringBuffer的父接口 |
- 先写入4个字节
byteBuf.writeBytes(new byte[]{1,2,3,4});
log(byteBuf);
- ByteBuf的容量情况
read index:0 write index:4 capacity:10
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 |.... |
+--------+-------------------------------------------------+----------------+
- 在写入一个int,int是4个字节。
byteBuf.writeInt(5);
log(byteBuf);
- ByteBuf的容量情况
read index:0 write index:8 capacity:10
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 00 00 00 05 |........ |
+--------+-------------------------------------------------+----------------+
- 当在写入1个int时,容量不够(初始容量是10),就会触发扩容。
- 扩容规则:
- 如果写入数据大小没有超过512,就选择下一个16的整数,例如写入后是12,就选择16。
- 如果写入后的大小超过
512 = 2^9
,就选择下一个2^n
,例如写入后是513,那么扩容后就是2^10 = 1024
。 - 扩容超过最大容量(max capatity)就会报错,最大容量是
Integer.MAX_VALUE
。
3. 读取
- 读取一个字节
byteBuf.readByte();
- 读取
int
,在读取之间先做一个标记mark
。
byteBuf.markReaderIndex();
byteBuf.readInt();
- 想要重复读取,就重置到标记位置。
byteBuf.resetReaderIndex();
- 如果使用
get
方法,不会改变读取指针(read index)。
4. 释放ByteBuf的内存
-
Netty中有堆外内存的ByteBuf实现,堆外内存最好是手动释放。
-
UnpooledHeapByteBuf
使用的是JVM
的内存,只需要等GC
回收。 -
UnpooledDirectByteBuf
使用的是直接内存,需要调用特殊方法进行回收。 -
PooledByteBuf
和它的子类使用了池化的机制,需要更复杂的规则来回收。 -
Netty使用了引用计数器来控制回收内存,每个ByteBuf都实现了
ReferenceCounted
接口:
- 每个ByteBuf对象的初始计数为
1
。 - 调用
release
方法计数器减1,如果计数为0,ByteBuf内存被回收。 - 调用
retain
方法计数加1,表示调用者没使用完之前(ByteBuf可以沿着handler应用链向下传递),其他handler即使调用release也不会被回收。 - 当计数器为0时,底层内存会被回收,这是即使ByteBuf对象还在,其各个方法都无法正常使用。
- 因为pipeline的存在,一般会将ByteBuf向下传递给下一个ChannelHandler。
- 谁是最后使用者,谁就负责释放(release)ByteBuf的内存。
5. slice(切片)
- slice是零拷贝的应用之一。
- 可以将原始的ByteBuf切片成多个ByteBuf,切片后的ByteBuf没有发生内存复制,还是使用了原始ByteBuf的内存,切片后的ByteBuf维护独立的read、write指针。
public class SliceTest {
public static void main(String[] args) {
// 新建一个ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
// 加入数据
buffer.writeBytes(new byte[] {1,2,3,4,5,6,7,8,9,10});
log(buffer);
// 使用slice进行切片
ByteBuf b1 = buffer.slice(0, 5);
ByteBuf b2 = buffer.slice(5, 5);
log(b1);
log(b2);
System.out.println("---------------------------------");
// 切片后的ByteBuf和原ByteBuf共用底层内存
b1.setByte(0, 2);
log(buffer);
log(b1);
}
}
- 切片后的ByteBuf的最大容量就是切片的容量。
7. 服务器和客户端双向通信
/**
* 完成一个双向通信,实现回声服务器,客户端发什么,服务器端就回复什么
*/
public class EchoService {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 添加链式处理
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof ByteBuf) {
// 显示客户端发送来的消息
ByteBuf buf = (ByteBuf) msg;
System.out.println(buf.toString(Charset.defaultCharset()));
// 给客户端写回数据,获得一个ByteBuf
ByteBuf writeBuffer = ctx.alloc().buffer();
writeBuffer.writeBytes(buf);
ctx.writeAndFlush(writeBuffer);
}
}
});
}
})
.bind(8888);
}
}
/**
* 双向通信客户端,客户端写入什么数据,服务器端就返回什么
*/
public class EchoClient {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
Channel channel = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 添加一个写出编码器
nioSocketChannel.pipeline().addLast(new StringEncoder());
// 获取服务器端的读取事件
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(buf.toString(Charset.defaultCharset()));
}
});
}
})
.connect("127.0.0.1", 8888)
.sync()
.channel();
// 释放连接
channel.closeFuture().addListener(future -> {
group.shutdownGracefully();
});
// 新建一个线程从控制台写数据
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while(true) {
String str = scanner.nextLine();
if(!"ex".equals(str)) {
channel.writeAndFlush(str);
} else {
// 关闭客户端
channel.close();
System.out.println("退出!");
break;
}
}
}).start();
}
}
Netty进阶
1. Netty中的粘包、半包现象
/**
* 粘包、半包演示
*/
@Slf4j
public class HelloWorldService {
public static void main(String[] args) {
// 创建一个只处理连接的线程
NioEventLoopGroup boss = new NioEventLoopGroup(1);
// 创建处理读写数据的线程
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
// 修改服务器端接收缓冲区,一次接收10字节,演示半包现象
serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel socketChannel) throws Exception {
// 添加一个输出日志打印
socketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 当有客户端连接到服务器是,触发active事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("connected{}", ctx.channel());
super.channelActive(ctx);
}
// 当客户端断开连接时,触发此方法
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.debug("disconnect{}", ctx.channel());
super.channelInactive(ctx);
}
});
}
});
// 绑定端口
ChannelFuture channelFuture = serverBootstrap.bind(8888);
log.debug("{} binging", channelFuture.channel());
// 异步处理建立连接
channelFuture.sync();
log.debug("{} bound", channelFuture.channel());
// 调用关闭连接时,同步阻塞,等待关闭
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.debug("server error", e);
} finally {
// 关闭连接
boss.shutdownGracefully();
worker.shutdownGracefully();
log.debug("server stop");
}
}
}
/**
* 粘包、半包演示
*/
@Slf4j
public class HelloWorldClient {
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel socketChannel) throws Exception {
log.debug("connected...");
socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 客户端连接到服务器,执行此方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("sending...");
ByteBuf buffer = ctx.alloc().buffer();
// 客户端每次发送16字节,发送10次
for (int i = 0; i < 10; i++) {
// ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(new byte[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p'});
// 发送10次16字节的数据,演示粘包现象
// ctx.writeAndFlush(buffer);
}
// 一次发送160字节,演示半包现象
ctx.writeAndFlush(buffer);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888);
// 异步等待连接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.debug("connect... fail", e);
} finally {
// 关闭连接
worker.shutdownGracefully();
log.debug("exit...");
}
}
}
2. 解决黏包、半包现象:短连接
- 黏包、半包现象的本质是TCP消息没有边界。消息发送的大小可能会受到应用层ByteBuf、TCP层滑动窗口等的影响。
- 可以采用短连接,解决黏包,但是短连接不能解决半包。短链接就是发送一次数据就断开。
/**
* 短连接,只能解决粘包,不能解决半包的现象
*/
@Slf4j
public class HelloServer {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 设置TCP缓冲区的大小
// serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
// 设置当前netty的缓冲区(ByteBuf)大小,最小是16
serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16, 16, 16));
serverBootstrap.group(new NioEventLoopGroup());
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
});
serverBootstrap.bind(8080);
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
// 发送10次数据
send();
}
}
public static void send() {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel socketChannel) throws Exception {
log.debug("connected...");
socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 客户端连接到服务器,执行此方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("sending...");
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(new byte[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p'});
ctx.writeAndFlush(buffer);
// 使用短连接,发送完数据就断开连接,解决黏包现象
ctx.close();
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888);
// 异步等待连接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.debug("connect... fail", e);
} finally {
// 关闭连接
worker.shutdownGracefully();
log.debug("exit...");
}
}
3. 解决黏包、半包现象:定长解码器
- 让所有数据包的长度固定。(假设长度为10字节)服务器端加入
FixedLengthFrameDecoder(10)
定长解码器。 - 服务器端:
// 加入一个黏包、半包的定长处理器,每次处理10字节
socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(10));
// 添加一个输出日志打印
socketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
4. 解决黏包、半包现象:固定分隔符
- 使用换行符处理器对消息进行分割。
LineBasedFrameDecoder(1024)
,构造参数含义是:超过多少字节没有找到固定分隔符就报错。 - 也可以自定义固定分隔符
DelimiterBasedFrameDecoder
。
// 处理黏包、半包,已一个分隔符进行区分,使用换行符解码器
// LineBasedFrameDecoder:构造参数是 在多少字节还没有遇到分隔符就报错
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
// 添加一个输出日志打印
socketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
5. 解决黏包、半包现象:预设数据的长度
- 使用
LengthFieldBasedFrameDecoder
,将发送的数据分为 数据的长度 + 数据。 - 在LengthFieldBasedFrameDecoder中5个参数:
- 参数1:
maxFrameLength
,一次最大处理长度 - 参数2:
lengthFieldOffset
,长度的偏移量,从第几个字节开始解析 - 参数3:
lengthFieldLength
,表示前几个字节是代表数据长度的 - 参数4:
lengthAdjustment
,读取数据的偏移量(如果在代表数据长度和数据之间,加入了其他,例如版本号,就从偏移量开始读取数据) - 参数5:
initialBytesToStrip
,解析后的结果需要剥离几个字节(比如,解析之后不想要前面几个代表数据的字节,只保留数据)
/**
* 使用 内容长度 + 内容 的方式,解决黏包、半包问题
*/
public class LengthFieldDecoderTest {
public static void main(String[] args) {
// 使用EmbeddedChannel进行测试
EmbeddedChannel channel = new EmbeddedChannel(
// 加入一个预设长度的解析器:
// 参数1:一次最大处理长度
// 参数2:长度的偏移量,从第几个字节开始解析
// 参数3:表示前几个字节是代表数据长度的
// 参数4:读取数据的偏移量(如果在代表数据长度和数据之间,加入了其他,例如版本号,就从偏移量开始读取数据)
// 参数5:解析后的结果需要剥离几个字节(比如,解析之后不想要前面几个代表数据的字节,只保留数据)
new LengthFieldBasedFrameDecoder(1024, 0, 4, 1, 4),
// 打印日志处理器
new LoggingHandler(LogLevel.DEBUG)
);
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
String str1 = "Hello, World";
String str2 = "Hello, nihao";
fillData(buffer, str1);
fillData(buffer, str2);
// 发送数据
channel.writeInbound(buffer);
}
/**
* 填充数据
* @param buffer
* @param data
*/
private static void fillData(ByteBuf buffer, String data) {
byte[] bytes = data.getBytes();
// 获取发送数据的字节长度
int len = bytes.length;
// 发送的数据:前四个字节(int) 是发送数据的长度,后面是数据
// 前四个字节,表示数据的长度
buffer.writeInt(len);
// 在数据长度和数据之间,加入一个版本号
buffer.writeByte(1);
// 后面是数据
buffer.writeBytes(bytes);
}
}
协议设计与解析
1. 使用redis协议向redis服务器发送数据
- redis有自己的通信协议。我们可以利用redis的通信协议,向redis服务器发送数据。
- 代码演示:
/**
* 遵循redis的协议,与redis服务器通信
*/
public class RedisAgreementClient {
// 定义一个回车换行符 \n,13代表回车,10代表换行
private static final byte[] LINE = {13, 10};
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup worker = new NioEventLoopGroup();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
ChannelFuture channelFuture = null;
try {
channelFuture = bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 添加日志处理
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 与服务器建立连接后
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 向redis服务器发送 set name zhangsan
set(ctx);
}
// 读取redis服务器发送回来的数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(buf.toString(Charset.defaultCharset()));
}
});
}
})
// 连接redis客户端
.connect("192.168.122.1", 6379)
.sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 关闭客户端
worker.shutdownGracefully();
}
}
/**
* 向redis服务器发送数据
* @param ctx
*/
public static void set(ChannelHandlerContext ctx) {
ByteBuf buffer = ctx.alloc().buffer();
// 使用redis的协议,想redis服务器set数据
// 表示有三个参数 set name zhangsan
buffer.writeBytes("*3".getBytes());
// 加入一个回车换行
buffer.writeBytes(LINE);
// 表示第一个参数 set 有3个字节
buffer.writeBytes("$3".getBytes());
buffer.writeBytes(LINE);
// 写入3字节
buffer.writeBytes("set".getBytes());
buffer.writeBytes(LINE);
// 表示第二个参数 name 有4个字节
buffer.writeBytes("$4".getBytes());
buffer.writeBytes(LINE);
// 写入4字节
buffer.writeBytes("name".getBytes());
buffer.writeBytes(LINE);
// 表示第三个参数 zhangsan 有8个字节
buffer.writeBytes("$8".getBytes());
buffer.writeBytes(LINE);
// 写入8字节数据
buffer.writeBytes("zhangsan".getBytes());
buffer.writeBytes(LINE);
Channel channel = ctx.channel();
// 写到redis服务器
channel.writeAndFlush(buffer);
}
}
2. 解析HTTP协议
- netty中解析HTTP的处理器是
HttpServerCodec
,同时实现了入站和出站处理器。 SimpleChannelInboundHandler
可以只关心我们想要的handler处理过后的类型。- 代码演示:
/**
* 解析HTTP协议,服务器
*/
@Slf4j
public class HttpAgreementService {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
// 加入一个HTTP请求的handler编解码器:既是入站处理器(处理器请求),也是出站处理器(处理响应)
nioSocketChannel.pipeline().addLast(new HttpServerCodec());
// 如果我们只想关心处理器传过来的是我们想要的类型
// 可以使用SimpleChannelInboundHandler,只关心HttpRequest类型
nioSocketChannel.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) throws Exception {
// 获取uri
String uri = httpRequest.uri();
log.debug(uri);
// 返回响应:设置协议版本号、响应状态码
DefaultFullHttpResponse response = new DefaultFullHttpResponse(httpRequest.protocolVersion(), HttpResponseStatus.OK);
// 写回响应体
byte[] bytes = "<h1>Hello World</h1>".getBytes();
// 设置响应体的长度
// response.headers().setInt(CONTENT_LENGTH, bytes.length);
response.content().writeBytes(bytes);
// 写到通道,经过ttp的编解码处理器,也是一个出站处理器
channelHandlerContext.writeAndFlush(bytes);
}
});
// 自定义handler,查看http编解码器传过来的
// nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// @Override
// public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// // 查看Http编解码器传过来的是什么对象
// log.debug("{}", msg.getClass());
//
// // Http编解码器将请求封装成了两个对象
// // 请求头 + 请求协议:DefaultHttpRequest
// // 请求体:LastHttpContent(get请求请求体为空)
// }
// });
}
});
// 异步连接
ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
// 异步释放连接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("server error ", e);
} finally {
// 关闭连接
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
- 使用游览器访问8888端口,就可以对http协议解析。
3. 自定义协议
- 自定义协议的要素:
- 魔数,用来在第一时间判定是否是无效数据包
- 版本号,可以支持协议的升级
- 序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk
- 指令类型,是登录、注册、单聊、群聊… 跟业务相关
- 请求序号,为了双工通信,提供异步能力
- 正文长度
- 消息正文
- 自定义一个协议Message的编解码消息处理器
/**
* 自定义协议 编解码的协议
* 将ByteBuf转化为业务的自定义的Message类
* 既能做入站也能做出站
*/
@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {
// 出站的时候将message编码成ByteBuf
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {
// 自定义协议
// 4字节 定义魔数
byteBuf.writeBytes(new byte[]{1,2,3,4});
// 1字节 的协议版本
byteBuf.writeByte(1);
// 1字节表示,正文的编码类型 0表示java的序列化,1表示json
byteBuf.writeByte(0);
// 1字节的消息类型,自定义的
byteBuf.writeByte(message.getMessageType());
// 4字节的请求序号
byteBuf.writeInt(message.getSequenceId());
// 为了严谨,使得固定的字节数是2的整数倍,在加一个字节,对齐填充
byteBuf.writeByte(0xff);
// 将Message对象(序列化)转化为字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(message);
byte[] bytes = bos.toByteArray();
// 4字节 正文的长度
byteBuf.writeInt(bytes.length);
// 写入正文
byteBuf.writeBytes(bytes);
}
// 入站解码,将ByteBuf解码成Message
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
// 对自定义的协议进行解码
// 读取魔数 4字节
int magicNum = byteBuf.readInt();
// 读取版本号 1字节
byte version = byteBuf.readByte();
// 序列化算法的类型 1字节
byte serializerType = byteBuf.readByte();
// 消息类型 1字节
byte messageType = byteBuf.readByte();
// 请求序列号 4字节
int sequenceId = byteBuf.readInt();
// 对齐填充,无意义 1字节
byteBuf.readByte();
// 读取内容长度 4字节
int length = byteBuf.readInt();
byte[] bytes = new byte[length];
// 读取内容到字节数组中,我们使用的是jdk序列化
byteBuf.readBytes(bytes, 0, length);
// 反序列成对象
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Message message = (Message) ois.readObject();
log.debug("{},{},{},{},{},{}", magicNum, version, serializerType, messageType, sequenceId, length);
log.debug("{}", message);
// 解析出来一条消息,给下一个handler用
list.add(message);
}
}
- 测试
/**
* 自定义协议编解码测试
*/
public class MessageCodecTest {
public static void main(String[] args) throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(
new LoggingHandler(),
// 处理半包黏包的处理器
new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0),
new MessageCodec()
);
// 创建一个登录信息
LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123", "张三");
channel.writeOutbound(message);
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null, message, buffer);
// 演示黏包和半包,将buf切分成
// 切前面100个字节
ByteBuf s1 = buffer.slice(0, 100);
// 切100字节到最后
ByteBuf s2 = buffer.slice(100, buffer.readableBytes() - 100);
// 将引用计数器+1
s1.retain();
// 入站
channel.writeInbound(s1);
channel.writeInbound(s2);
}
}
4. @Sharable
@Sharable
注解只是一个标记,如果一个handler上标记了注解,证明已经充分考虑了线程安全性。- 比如
LoggingHandler
只是记录日志,可以被多个Pipele使用,所以可以标记为Sharable。 - 但是
LengthFieldBasedFrameDecoder
基于长度的解码器,解决粘包、半包的handler,如果没有解析到一条完整的消息,就会先把解析的消息保存下来,就不是线程安全的。 - 所以编解码的
ByteToMessageCodec
处理器,不能加上Sharable。在构造器上进行了限制。 - 要使用
MessageToMessageCodec
就可以加上Sharable注解。
5. 聊天服务器
@Slf4j
public class ChatService {
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
NioEventLoopGroup boss = new NioEventLoopGroup();
// 线程安全的handler加了Sharable注解,多个channel可以共用一个
LoggingHandler LOGGING_HANDLER = new LoggingHandler();
// 编解码自定义协议
MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0));
ch.pipeline().addLast(LOGGING_HANDLER);
ch.pipeline().addLast(MESSAGE_CODEC);
}
});
ChannelFuture future = serverBootstrap.bind(8080).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
log.error("服务器异常", e);
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
6. 聊天客户端和CountDownLatch
CountDownLatch
可以使用多个线程之间通信。
// 为0时,会唤醒await
CountDownLatch down = new CountDownLatch(1);
// 线程1等待
down.await();
// 线程2conutDown唤醒await的线程1
down.countDown();
@Slf4j
public class ChatClient {
public static void main(String[] args) {
NioEventLoopGroup group = new NioEventLoopGroup();
// 线程安全的handler加了Sharable注解,多个channel可以共用一个
LoggingHandler LOGGING_HANDLER = new LoggingHandler();
// 编解码自定义协议
MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();
// CountDownLatch减为0时,唤醒await()线程
CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1);
// 判断是否登录成功
AtomicBoolean LOGIN_SUCCESS = new AtomicBoolean();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ProcotolFrameDecoder());
// ch.pipeline().addLast(LOGGING_HANDLER);
ch.pipeline().addLast(MESSAGE_CODEC);
ch.pipeline().addLast(new SimpleChannelInboundHandler<LoginResponseMessage>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginResponseMessage response) throws Exception {
log.debug("{}", response);
// 读取登录返回信息
boolean success = response.isSuccess();
LOGIN_SUCCESS.set(success);
// CountDownLatch减为0,就会唤醒await
WAIT_FOR_LOGIN.countDown();
}
});
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 建立连接之后,触发active事件
new Thread(() -> {
// 进行登录
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();
LoginRequestMessage message = new LoginRequestMessage(username, password);
ctx.writeAndFlush(message);
try {
// 发送登录信息之后,等待服务器端是否登录成功
WAIT_FOR_LOGIN.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 判断是否登录成功
boolean success = LOGIN_SUCCESS.get();
if(!success) {
// 登录失败
System.out.println("登录失败");
ctx.channel().close();
return;
}
System.out.println("登录成功");
// 登录成功
while(true) {
System.out.println("==================================");
System.out.println("send [username] [content]");
System.out.println("gsend [group name] [content]");
System.out.println("gcreate [group name] [m1,m2,m3...]");
System.out.println("gmembers [group name]");
System.out.println("gjoin [group name]");
System.out.println("gquit [group name]");
System.out.println("quit");
System.out.println("==================================");
Scanner sca = new Scanner(System.in);
String msg = sca.nextLine();
}
}, "system-in").start();
super.channelActive(ctx);
}
});
}
});
ChannelFuture future = bootstrap.connect("localhost", 8080).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
log.error("client error", e);
} finally {
group.shutdownGracefully();
}
}
}
7. 服务器端检测客户端连接断开
- 客户端断开连接,
channl.close();
- 服务器端写handler进行处理,判断是正常断开还是异常断开,但是不管那种断开,都会触发
channelInactive
()事件。
@Slf4j
@ChannelHandler.Sharable
public class QuitChatMessageHandler extends ChannelInboundHandlerAdapter {
/**
* 正常断开 或 异常断开,都会触发channelInactive事件
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
SessionFactory.getSession().unbind(ctx.channel());
log.debug("断开连接:{}", ctx.channel());
}
// 异常断开
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// SessionFactory.getSession().unbind(ctx.channel());
log.debug("异常断开连接:{}", ctx.channel());
}
}
8. 连接假死,心跳检测
- netty提供
IdelStateHandler
检测读空闲事件,写空闲事件。 - 服务器端,检测读空闲事件,客户端没有发送数据就断开链接,避免占用服务器资源。
// 加入空闲检测:8秒还没有读取到数据,就会触发IdleState.READ_IDLE事件
ch.pipeline().addLast(new IdleStateHandler(8, 0, 0));
ch.pipeline().addLast(new ChannelDuplexHandler() {
// 用来触发特殊事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 触发读空闲事件
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
System.out.println("8秒钟没有收到读事件, 可能网络故障了,断开连接");
ctx.channel().close();
}
}
});
- 客户端代码,发送心跳,证明客户端正常。
// 发送心跳数据:5秒还没写入数据
ch.pipeline().addLast(new IdleStateHandler(0, 5, 0));
// ChannelDuplexHandler同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
// 用来触发特殊事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
if(IdleState.WRITER_IDLE == event.state()) {
// 触发了空闲事件
// System.out.println("5秒没有写入数据了,发送一个心跳包");
// 发送一个心跳包
ctx.writeAndFlush(new PingMessage());
}
}
});
参数调优
1. CONNECT_TIMEOUT_MILLS
- 客户端连接服务器超时时间。
- 在指定时间内客户端没有连接到服务器,就会报出
ConnectTimeOutException
异常。 - 但是如果确定服务器端没有开启,可能没到指定的连接超时时间就会报错,报java.net的
ConnectException
。
public class TestConnectTimeOut {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
/**
* 设置连接超时的参数
* 如果在服务端设置:option()设置ServerSocketChannel的参数,childOption()设置客户端的参数
*/
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(new NioEventLoopGroup());
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler());
}
});
ChannelFuture future = bootstrap.connect("localhost", 8080).sync();
future.channel().closeFuture().sync();
}
}
2. SO_BACKLOG
-
TCP三次握手
-
在Linux2.2之前,
backlog
参数的大小包括两个队列(半连接队列,全连接队列)的大小。(全连接队列就代表了这个服务器的连接客户端上限,如果全连接队列满了,server将发送一个拒绝连接的错误信息到client) -
Linux2.2之后,linux有两个系统配置文件,配置这两个参数的大小。
-
半连接队列(sync queue)
大小通过/proc/sys/net/ipv4/tcp_max_syn_backlog
指定。在synccookies
启用的情况下,逻辑上没有最大限制,这个设置便被忽略。 -
全连接队列(accept queue)
通过/proc/sys/net/core/somaxconn
指定,在客户端连接时,内核会根据传入的backlog
参数,与系统配置中的参数,取二者比较小的值。 -
Linux系统通过
ss -lnt
查看全连接队列的大小。
-
ServerSocket在构造函数中执行
-
netty中服务器端通过
option()
设置参数。 -
演示全连接队列满,测试服务器端:(服务器端要用
debug
模式启动)
/**
* 测试全连接队列backlog参数
* windows环境下,没有Linux系统的全连接配置文件,所以客户端设置多大,就是多大
*/
@Slf4j
public class TestBacklogServer {
public static void main(String[] args) {
ServerBootstrap bootstrap = new ServerBootstrap();
/**
* 通过SO_BACKLOG设置全连接队列的大小
* 但是不好进行演示,连接建立后,加入全连接队列
* 调用accept()之后,就会从队列中移除。
* 所以Netty要在NioEventLoop的accept处打一个断点
* 建立三次连接之后,就保持在全连接队列中
*/
bootstrap.option(ChannelOption.SO_BACKLOG, 2);
bootstrap.group(new NioEventLoopGroup());
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler());
}
});
bootstrap.bind(8080);
}
}
- 测试客户端
/**
* 测试全连接队列客户端
*/
@Slf4j
public class TestBacklogClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(new NioEventLoopGroup());
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler());
}
});
ChannelFuture future = bootstrap.connect("localhost", 8080);
future.sync().channel().closeFuture();
}
}
-
在
NioEventLoop
中的accept()
处打上断点,如果调用了accept()方法,那么就会从全连接队列中移除,无法演示。
-
第3个客户端连接上,就会抛出连接异常
ConnectException
。
3. ALLOCATOR
-
决定
ByteBuf
是池化还是非池化,直接内存还是堆内存。 -
netty默认是
池化的直接内存ByteBuf
-
如何设置是否池化,或者使用直接内存。跟踪源码实现。
-
在
ByteBufUtil
中进行分配。先读取配置信息,如果没有配置,就判断是否是安卓的操作系统,是就使用非池化,否则就是池化。 -
配置非池化和只用堆内存:
-Dio.netty.allocator.type=unpooled -Dio.netty.noPreferDirect=true
String allocType = SystemPropertyUtil.get(
"io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();
ByteBufAllocator alloc;
if ("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else if ("pooled".equals(allocType)) {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
}
DEFAULT_ALLOCATOR = alloc;
4. RCVBUF_ALLOCATOR
- 控制Netty的接收缓冲区的大小,负责对ByteBuf的大小进行分配。
ByteBufAllocator
决定了缓冲区是池化还是非池化,但是大小的分配由RecvByteBufAllocator
决定。并且IO操作的ByteBuf,强制使用直接内存。因为直接内存少一次在jvm虚拟机的内存复制,效率较高。
RPC
1. RPC请求、响应消息
- RPC请求需要知道的几个东西:
调用的接口的全限定类名 String interfaceName
调用接口中的方法 String methodName
方法的返回值类型 Class<?> returnType
方法的请求参数类型 Class[] paramterTypes
方法参数的数组 Objectp[] paramterValue
/**
* @author yihang
*/
@Getter
@ToString(callSuper = true)
public class RpcRequestMessage extends Message {
/**
* 调用的接口全限定名,服务端根据它找到实现
*/
private String interfaceName;
/**
* 调用接口中的方法名
*/
private String methodName;
/**
* 方法返回类型
*/
private Class<?> returnType;
/**
* 方法参数类型数组
*/
private Class[] parameterTypes;
/**
* 方法参数值数组
*/
private Object[] parameterValue;
public RpcRequestMessage(int sequenceId, String interfaceName, String methodName, Class<?> returnType, Class[] parameterTypes, Object[] parameterValue) {
super.setSequenceId(sequenceId);
this.interfaceName = interfaceName;
this.methodName = methodName;
this.returnType = returnType;
this.parameterTypes = parameterTypes;
this.parameterValue = parameterValue;
}
@Override
public int getMessageType() {
return RPC_MESSAGE_TYPE_REQUEST;
}
}
- 响应消息
返回值 returnValue
异常值 Exception
/**
* @author yihang
*/
@Data
@ToString(callSuper = true)
public class RpcResponseMessage extends Message {
/**
* 返回值
*/
private Object returnValue;
/**
* 异常值
*/
private Exception exceptionValue;
@Override
public int getMessageType() {
return RPC_MESSAGE_TYPE_RESPONSE;
}
}
2. RPC调用
- 创建一个需要远程调用的接口和实现类。
public interface HelloService {
String sayHello(String name);
}
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
return "你好" + name;
}
}
- 服务器端,使用反射调用方法。
@Slf4j
@ChannelHandler.Sharable
public class RpcRequestMessageHandler extends SimpleChannelInboundHandler<RpcRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequestMessage msg) throws Exception {
RpcResponseMessage response = new RpcResponseMessage();
try {
// 通过反射进行远程调用
HelloService service = (HelloService) ServicesFactory.getService(Class.forName(msg.getInterfaceName()));
Method method = service.getClass().getMethod(msg.getMethodName(), msg.getParameterTypes());
Object invoke = method.invoke(service, msg.getParameterValue());
response.setReturnValue(invoke);
ctx.writeAndFlush(response);
} catch (Exception e) {
log.error("rpc error", e);
response.setExceptionValue(e);
}
}
}
- 客户端发起远程调用
ChannelFuture future = bootstrap.connect("localhost", 8080);
Channel channel = future.sync().channel();
// 连接建立之后,发起远程调用
RpcRequestMessage request = new RpcRequestMessage(Message.RPC_MESSAGE_TYPE_REQUEST,
"netty2.rpc.service.HelloService",
"sayHello",
String.class,
new Class[]{String.class},
new Object[]{"张三"});
ChannelFuture writeFuture = channel.writeAndFlush(request);
3. Gson转化Class问题
- Gson序列化Class的时候,序列化失败,但是客户端没有打印异常信息,因为
WriteAndFlush
是另一个线程调用,监控改线程返回的Promise
判断错误信息。
ChannelFuture future = bootstrap.connect("localhost", 8080);
Channel channel = future.sync().channel();
// 连接建立之后,发起远程调用
RpcRequestMessage request = new RpcRequestMessage(Message.RPC_MESSAGE_TYPE_REQUEST,
"netty2.rpc.service.HelloService",
"sayHello",
String.class,
new Class[]{String.class},
new Object[]{"张三"});
ChannelFuture writeFuture = channel.writeAndFlush(request);
writeFuture.addListener(promise -> {
// 如果数据发送失败,打印错误信息
if (!promise.isSuccess()) {
Throwable cause = promise.cause();
log.debug("发送数据错误,", cause);
}
});
channel.closeFuture().sync();
- 自定义Gson对Class的序列化、反序列化
public class TestJson {
public static void main(String[] args) {
Gson gson = new GsonBuilder().registerTypeAdapter(Class.class, new ClassCoder()).create();
System.out.println(gson.toJson(String.class));
}
/**
* Gson将Class类进行json序列化的时候,会报错
* 自定义Class的序列化、反序列化
*/
static class ClassCoder implements JsonSerializer<Class<?>>, JsonDeserializer<Class<?>> {
@Override
public Class<?> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
try {
String str = jsonElement.getAsString();
// 将全限定类名转为Class类
return Class.forName(str);
} catch (ClassNotFoundException e) {
throw new JsonParseException(e);
}
}
@Override
public JsonElement serialize(Class<?> aClass, Type type, JsonSerializationContext jsonSerializationContext) {
// 将Class类 -> 转为全限定类名
return new JsonPrimitive(aClass.getName());
}
}
}
4. 使用动态代理改造RPC
- 问题分析:每次调用RPC都需要传入很多参数,封装对象,很不友好。
// 客户端每次远程调用,都要封装一个对象,不友好
channel.writeAndFlush(new RpcRequestMessage(Message.RPC_MESSAGE_TYPE_REQUEST,
"netty2.rpc.service.HelloService",
"sayHello",
String.class,
new Class[]{String.class},
new Object[]{"张三"}));
- 改造成,调用远程方法,就像调用本地方法一样
// 设置成直接调用方法,就可以实现远程调用
HelloService service = null;
service.sayHello("张三");
- 使用动态代理对方法进行封装
@Slf4j
public class RpcClientManager {
private static volatile Channel channel = null;
private static final Object lock = new Object();
public static void main(String[] args) {
// getProxyService获取一个代理类
HelloService service = getProxyService(HelloService.class);
service.sayHello("张三");
service.sayHello("李四");
}
/**
* 为一个接口,创建一个代理对象,自动封装成RpcRequestMessage对象
* @param clazz
* @param <T>
* @return
*/
private static <T> T getProxyService(Class<T> clazz) {
ClassLoader classLoader = clazz.getClassLoader();
Class<?>[] interfaces = new Class[]{clazz};
Object o = Proxy.newProxyInstance(classLoader, interfaces, (proxy, method, args) -> {
// 封装请求消息对象
RpcRequestMessage requestMessage = new RpcRequestMessage(SequenceIdGenerator.nextId(),
clazz.getName(),
method.getName(),
method.getReturnType(),
method.getParameterTypes(),
args);
// 获取channel,发送数据
getChannel().writeAndFlush(requestMessage);
// 暂时返回一个null
return null;
});
return (T) o;
}
/**
* 获取Channel对象
* @return
*/
public static Channel getChannel() {
// Channel只能加载一次,所以使用单例的双重检查
if(channel == null) {
synchronized (lock) {
if(channel == null) {
initChannel();
return channel;
}
}
}
return channel;
}
/**
* 初始化channel
*/
private static void initChannel() {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class);
ProcotolFrameDecoder procotolFrameDecoder = new ProcotolFrameDecoder();
LoggingHandler loggingHandler = new LoggingHandler();
MessageCodecSharable messageCodecSharable = new MessageCodecSharable();
RpcResponseMessageHandler rpcResponseHandler = new RpcResponseMessageHandler();
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 粘包半包处理
ch.pipeline().addLast(procotolFrameDecoder);
ch.pipeline().addLast(loggingHandler);
// 协议编码
ch.pipeline().addLast(messageCodecSharable);
ch.pipeline().addLast(rpcResponseHandler);
}
});
channel = bootstrap.connect("localhost", 8080).sync().channel();
// 如果调用了channel的close操作,异步关闭eventLoop
channel.closeFuture().addListener(future -> {
group.shutdownGracefully();
});
} catch (Exception e) {
log.error("客户端出错,", e);
}
}
}
5. 异步接收结果
- 调用sayHello方法是在主线程,而返回远程调用结果是在nio线程。两个线程之间的通信可以使用
Promise
。 - 在RpcResponseMessageHandler加入一个填充Promise消息的map
@ChannelHandler.Sharable
@Slf4j
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {
// 响应填充结果的Promise的Map
public static final Map<Integer, Promise<Object>> MAP = new ConcurrentHashMap<>();
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) throws Exception {
// 判断远程调用结果
Object returnValue = msg.getReturnValue();
Exception exceptionValue = msg.getExceptionValue();
// 取出响应消息对应的Promise,填充结果
Promise<Object> promise = MAP.remove(msg.getSequenceId());
if(exceptionValue != null) {
promise.setFailure(exceptionValue);
} else {
promise.setSuccess(returnValue);
}
log.debug("{}", msg);
}
}
- 调用方的Promise阻塞,等待nio线程返回数据。
@Slf4j
public class RpcClientManager {
private static volatile Channel channel = null;
private static final Object lock = new Object();
public static void main(String[] args) {
// getProxyService获取一个代理类
HelloService service = getProxyService(HelloService.class);
System.out.println(service.sayHello("张三"));
System.out.println(service.sayHello("李四"));
}
/**
* 为一个接口,创建一个代理对象,自动封装成RpcRequestMessage对象
* @param clazz
* @param <T>
* @return
*/
private static <T> T getProxyService(Class<T> clazz) {
ClassLoader classLoader = clazz.getClassLoader();
Class<?>[] interfaces = new Class[]{clazz};
Object o = Proxy.newProxyInstance(classLoader, interfaces, (proxy, method, args) -> {
// 封装请求消息对象
int sequenceId = SequenceIdGenerator.nextId();
RpcRequestMessage requestMessage = new RpcRequestMessage(sequenceId,
clazz.getName(),
method.getName(),
method.getReturnType(),
method.getParameterTypes(),
args);
// 获取channel,发送数据
getChannel().writeAndFlush(requestMessage);
// 创建一个Promise,用于接收结果
DefaultPromise<Object> promise = new DefaultPromise<>(getChannel().eventLoop());
RpcResponseMessageHandler.MAP.put(sequenceId, promise);
// 等待填充结果
promise.await();
if (promise.isSuccess()) {
return promise.getNow();
} else {
throw new RuntimeException(promise.cause());
}
});
return (T) o;
}
/**
* 获取Channel对象
* @return
*/
public static Channel getChannel() {
// Channel只能加载一次,所以使用单例的双重检查
if(channel == null) {
synchronized (lock) {
if(channel == null) {
initChannel();
return channel;
}
}
}
return channel;
}
/**
* 初始化channel
*/
private static void initChannel() {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class);
ProcotolFrameDecoder procotolFrameDecoder = new ProcotolFrameDecoder();
LoggingHandler loggingHandler = new LoggingHandler();
MessageCodecSharable messageCodecSharable = new MessageCodecSharable();
RpcResponseMessageHandler rpcResponseHandler = new RpcResponseMessageHandler();
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 粘包半包处理
ch.pipeline().addLast(procotolFrameDecoder);
ch.pipeline().addLast(loggingHandler);
// 协议编码
ch.pipeline().addLast(messageCodecSharable);
ch.pipeline().addLast(rpcResponseHandler);
}
});
channel = bootstrap.connect("localhost", 8080).sync().channel();
// 如果调用了channel的close操作,异步关闭eventLoop
channel.closeFuture().addListener(future -> {
group.shutdownGracefully();
});
} catch (Exception e) {
log.error("客户端出错,", e);
}
}
}
源码分析
1. Netty中bind()分析
- 连接主要做的几件事:会创建一个
NioServerSocketChannel
,作为一个附件传递到ServerSocketChannel
,客户端的连接事件,都由NioServerSocketChannel建立连接。
创建一个NioServerSocketChannel
NioServerSockerChannel nioSsc = new NioServerSocketChannel();
创建一个服务器
ServerSocketChannel ssc = new ServerSocketChannel();
创建一个多路复用器(这一步是在NioEventLoop中完成)
Selection selection = Selection.open();
服务器注册到多路复用器
SelectionKey key = ssc.register(selection, 0, nioSsc);
绑定端口
ssc.bind(8080, backlog);
监听客户端的连接事件
key.interestOps(SelectlionKey.OP_ACCEPT);
- 源码分析
private ChannelFuture doBind(final SocketAddress localAddress) {
// init初始化和注册
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
if (regFuture.isDone()) {
// 注册完成、成功
// At this point we know that the registration was complete and successful.
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
// 未注册成功,再次注册
// Registration future is almost always fulfilled already, but just in case it's not.
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
// 使用另一个线程进行注册和发起bind
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
// Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
// IllegalStateException once we try to access the EventLoop of the Channel.
promise.setFailure(cause);
} else {
// Registration was successful, so set the correct executor to use.
// See https://github.com/netty/netty/issues/2586
promise.registered();
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
// ServerSocketChannel注册一个多路复用器,将NioServerSocketChannel作为附件,进行关联,selectionKey上发生的accept事件,由附件进行连接操作。
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
- 完整流程
2. NioEventLoop
- NioEventLoop里面有一个单线程,一个Select,一个任务队列。
- 继承了线程池的定时任务,可以执行定时任务。单线程如果有多个任务,所有需要任务队列。
- NioEventLoop底层使用的SelectKey进行了重写,原生的SelectKey是基于Set的,Set是基于Map的,遍历,需要比较hash,有冲突需要遍历链表。Netty重写了一个基于数组的,替换了原生SelectKey中的参数,提高了遍历效率。
private SelectorTuple openSelector() {
final Selector unwrappedSelector;
try {
// 创建一个select
unwrappedSelector = provider.openSelector();
} catch (IOException e) {
throw new ChannelException("failed to open a new selector", e);
}
if (DISABLE_KEY_SET_OPTIMIZATION) {
return new SelectorTuple(unwrappedSelector);
}
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
return Class.forName(
"sun.nio.ch.SelectorImpl",
false,
PlatformDependent.getSystemClassLoader());
} catch (Throwable cause) {
return cause;
}
}
});
if (!(maybeSelectorImplClass instanceof Class) ||
// ensure the current selector implementation is what we can instrument.
!((Class<?>) maybeSelectorImplClass).isAssignableFrom(unwrappedSelector.getClass())) {
if (maybeSelectorImplClass instanceof Throwable) {
Throwable t = (Throwable) maybeSelectorImplClass;
logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, t);
}
return new SelectorTuple(unwrappedSelector);
}
final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
// 准备一个数组实现的SelectKey
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
// 获取原来的selectKey参数,原来的SelectKey是基于Set的,Set底层是基于Map的,遍历,
// 要比较hash,有冲突还要遍历链表,效率较低,Netty实现了一个基于数组的,遍历效率高。
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) {
// Let us try to use sun.misc.Unsafe to replace the SelectionKeySet.
// This allows us to also do this in Java9+ without any extra flags.
long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField);
long publicSelectedKeysFieldOffset =
PlatformDependent.objectFieldOffset(publicSelectedKeysField);
if (selectedKeysFieldOffset != -1 && publicSelectedKeysFieldOffset != -1) {
PlatformDependent.putObject(
unwrappedSelector, selectedKeysFieldOffset, selectedKeySet);
PlatformDependent.putObject(
unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet);
return null;
}
// We could not retrieve the offset, lets try reflection as last-resort.
}
Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
if (cause != null) {
return cause;
}
cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
if (cause != null) {
return cause;
}
// 替换原生SelectKey的两个参数
selectedKeysField.set(unwrappedSelector, selectedKeySet);
publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
return null;
} catch (NoSuchFieldException e) {
return e;
} catch (IllegalAccessException e) {
return e;
}
}
});
if (maybeException instanceof Exception) {
selectedKeys = null;
Exception e = (Exception) maybeException;
logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, e);
return new SelectorTuple(unwrappedSelector);
}
selectedKeys = selectedKeySet;
logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
return new SelectorTuple(unwrappedSelector,
new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
}
- select()空轮询,JDK在Linux系统下的select()会出现空轮询bug,select方法不会阻塞住,程序会一直在for循环中运行,导致CPU100%。
- Netty如何进行解决:
int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
// If a task was submitted when wakenUp value was true, the task didn't get a chance to call
// Selector#wakeup. So we need to check task queue again before executing select operation.
// If we don't, the task might be pended until select operation was timed out.
// It might be pended until idle timeout if IdleStateHandler existed in pipeline.
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
selector.selectNow();
selectCnt = 1;
break;
}
// 可能出现空轮询bug的地方
int selectedKeys = selector.select(timeoutMillis);
// 通过一个计数,如果计数快速增加超过了一个阈值,说明出现了空轮询bug
selectCnt ++;
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
// - Selected something,
// - waken up by user, or
// - the task queue has a pending task.
// - a scheduled task is ready for processing
break;
}
if (Thread.interrupted()) {
// Thread was interrupted so reset selected keys and break so we not run into a busy loop.
// As this is most likely a bug in the handler of the user or it's client library we will
// also log it.
//
// See https://github.com/netty/netty/issues/2426
if (logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely because " +
"Thread.currentThread().interrupt() was called. Use " +
"NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
}
selectCnt = 1;
break;
}
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
// timeoutMillis elapsed without anything selected.
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// 如果selectCnt >= 阈值(默认512),说明出现了空轮询bug
// The code exists in an extra method to ensure the method is not too big to inline as this
// branch is not very likely to get hit very frequently.
// 重建一个select,替换掉以前的,并把以前的状态复制给新的
selector = selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
if (logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
selectCnt - 1, selector);
}
}
} catch (CancelledKeyException e) {
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
selector, e);
}
// Harmless exception - log anyway
}
}
ioRatio
参数是控制处理IO事件所占用时间的比例。NioEventLoop是一个单线程,它可以处理io事件,也可以处理普通的任务,如果普通任务执行时间较长,就会影响io事件的处理。
// 默认50%
private volatile int ioRatio = 50;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
// 处理SelectKey事件,io事件
processSelectedKeys();
} finally {
// 如果将ioRatio设置成100%,反而会将普通任务执行完毕,在执行io任务
// Ensure we always run tasks.
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
// 处理SelectKey事件,io事件
processSelectedKeys();
} finally {
// Ensure we always run tasks.
// io运行的时间
final long ioTime = System.nanoTime() - ioStartTime;
// 运行一个普通任务的时间,如果没有执行完,下一次从任务队列中取。
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}