阻塞io,在C/S端接收请求会阻塞.如下代码.监听连接和接收数据都是阻塞方法.当一个连接建立成功,而不发送数据将导致接收数据处于阻塞状态.无法对后续请求连接做处理.
即便使用多线程,将建立连接后的数据接收用子线程处理.但线程也是有性能瓶颈的,核心线程数不超过cpu核心数,普通线程也要根据机器硬件选择开启数量.而且开启的线程处理都是长连接操作的话.,面对海量用户请求依然无法有空余处理.并发效率低这是BIO的痛点.
public class SocketTest {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);
while (true) {
System.out.println("等待连接");
// 阻塞方法
Socket socket = serverSocket.accept();
System.out.println("连接成功");
handler(socket);
}
}
private static void handler(Socket socket) {
byte[] bytes = new byte[1024];
//通过socket获取输入流
System.out.println("read data..");
try {
// 接收客户端的数据,阻塞方法,没有数据可读时就一直等待
int read = socket.getInputStream().read(bytes);
if (read != -1) {
System.out.println("read data:" + new String(bytes, 0, read));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
NIO有三大核心部分: Channel(通道),Buffer(缓冲区),Selector(选择器)
NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络.
解决BIO的阻塞问题.但依然存在的问题是.请求过多,而真正发送数据的连接很少.导致循环接收请求有效次占比不高.浪费资源. 死循环程序占用cpu资源高.
一个线程管理多个sockchannel
分散读
public static void main(String[] args) {
try (FileChannel channel = new RandomAccessFile("words.txt", "r").getChannel()) {
// 分散读 words->onetwothree
ByteBuffer buffer1 = ByteBuffer.allocate(3);
ByteBuffer buffer2 = ByteBuffer.allocate(3);
ByteBuffer buffer3 = ByteBuffer.allocate(5);
channel.read(new ByteBuffer[]{buffer1, buffer2, buffer3});
buffer1.flip();
buffer2.flip();
buffer3.flip();
System.out.println(new String(buffer1.array(), 0, buffer1.limit()));
System.out.println(new String(buffer2.array(), 0, buffer2.limit()));
System.out.println(new String(buffer3.array(), 0, buffer3.limit()));
} catch (IOException e) {
System.out.println(e.getMessage());
};
}
集中写
public static void main(String[] args) {
ByteBuffer buffer1 = StandardCharsets.UTF_8.encode("hello");
ByteBuffer buffer2 = StandardCharsets.UTF_8.encode("world");
ByteBuffer buffer3 = StandardCharsets.UTF_8.encode("java");
try (FileChannel channel = new RandomAccessFile("helloword.txt", "rw").getChannel()) {
channel.write(new ByteBuffer[]{buffer1, buffer2, buffer3});
} catch (IOException e) {
System.out.println(e.getMessage());
};
}
粘包:
public static void main(String[] args) {
/**
* 模拟粘包
* Hello,world\n
* I'm zhangsan\n
* How are you?\n
* 粘包后
* Hello,world\nI'm zhangsan\nHo
* w are you?\n
*/
ByteBuffer source = ByteBuffer.allocate(32);
source.put("Hello,world\nI'm zhangsan\nHo".getBytes());
split(source);
source.put("w are you?\n".getBytes());
split(source);
}
private static void split(ByteBuffer source) {
// 读当前这一条数据
source.flip();
for (int i = 0; i < source.limit(); i++) {
// 找到一条完整的数据 不移动position 粘包处理
if(source.get(i) == '\n'){
// 把这条消息存入新的buffer
int length = i + 1 - source.position();
ByteBuffer target = ByteBuffer.allocate(length);
// 从source中读取数据到target
for (int j = 0; j < length; j++) {
// 读取数据后,position会自动+1
target.put(source.get());
}
};
}
// 切换写模式,写下一条数据. 不能用clear.需要接收上一条数据的剩余数据 半包处理
source.compact();
}
channel数据拷贝,底层使用操作系统0拷贝.一次最多传2G
public static void main(String[] args) throws IOException {
FileChannel from = new FileInputStream("data.txt").getChannel();
FileChannel to = new FileOutputStream("to.txt").getChannel();
// 从0开始,读取from.size()个字节,写入到to中
// channel数据拷贝,底层使用操作系统0拷贝.一次最多传2G
long size = from.size();
// count > 0 说明还有数据没有拷贝
for(long count = size; count > 0;){
// 从from的size-count位置开始,读取count个字节,写入到to中
count -= from.transferTo((size-count), count, to);
}
from.transferTo(0, from.size(), to);
}
遍历文件夹
public static void main(String[] args) throws IOException {
AtomicInteger dirCount = new AtomicInteger();
AtomicInteger fileCount = new AtomicInteger();
Files.walkFileTree(Paths.get(System.getProperty("user.dir")),new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("====>"+dir);
dirCount.incrementAndGet();
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.toString().endsWith(".java")){
fileCount.incrementAndGet();
System.out.println(file);
}
return FileVisitResult.CONTINUE;
}
});
System.out.println("dirCount:"+dirCount.get());
System.out.println("fileCount:"+fileCount.get());
}
递归删除,危险代码
public static void main(String[] args) throws IOException {
Files.walkFileTree(Paths.get(System.getProperty("user.dir")),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);
}
});
}
递归拷贝
public static void main(String[] args) throws IOException {
String source = "D:\\test\\";
String target = "D:\\test1\\";
Files.walk(Paths.get(source)).forEach(path -> {
String targetName = path.toString().replace(source, target);
try {
if (Files.isDirectory(path)) { // 是文件夹
Files.createDirectory(Paths.get(targetName));
} else if (Files.isRegularFile(path)) { // 是文件
Files.copy(path, Paths.get(targetName));
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
客户端-服务端网络通信demo
分两组选择器
IO频繁,cpu使用不频繁参考阿姆达尔定律
@Slf4j
public class WriteClient {
public static void main(String[] args) throws IOException {
new BossEventLoop().register();
}
@Slf4j
static class BossEventLoop implements Runnable {
// boss线程 用于接收客户端连接 连接
private Selector boss;
// worker线程 用于处理客户端请求 读写
private WorkerEventLoop[] workers;
// 避免重复创建线程
private volatile boolean start = false;
AtomicInteger index = new AtomicInteger();
public void register() throws IOException {
if (!start) {
// 创建一个ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定端口
ssc.bind(new InetSocketAddress(8080));
// 设置为非阻塞
ssc.configureBlocking(false);
// 创建一个boss线程用于接收客户端连接
boss = Selector.open();
SelectionKey ssckey = ssc.register(boss, 0, null);
// 设置为接收连接事件
ssckey.interestOps(SelectionKey.OP_ACCEPT);
// 创建worker线程用于处理客户端请求
workers = initEventLoops();
// 启动线程
new Thread(this, "boss").start();
log.debug("boss start...");
start = true;
}
}
/**
* 初始化worker线程
* @return
*/
public WorkerEventLoop[] initEventLoops() {
// EventLoop[] eventLoops = new EventLoop[Runtime.getRuntime().availableProcessors()];
WorkerEventLoop[] workerEventLoops = new WorkerEventLoop[2];
for (int i = 0; i < workerEventLoops.length; i++) {
// 创建worker线程
workerEventLoops[i] = new WorkerEventLoop(i);
}
return workerEventLoops;
}
@Override
public void run() {
while (true) {
try {
// 阻塞等待事件
boss.select();
// 获取事件
Iterator<SelectionKey> iter = boss.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
ServerSocketChannel c = (ServerSocketChannel) key.channel();
// 获取客户端连接
SocketChannel sc = c.accept();
sc.configureBlocking(false);
log.debug("{} connected", sc.getRemoteAddress());
// 将客户端连接注册到worker线程
workers[index.getAndIncrement() % workers.length].register(sc);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@Slf4j
static class WorkerEventLoop implements Runnable {
private Selector worker;
private volatile boolean start = false;
private int index;
private final ConcurrentLinkedQueue<Runnable> tasks = new ConcurrentLinkedQueue<>();
public WorkerEventLoop(int index) {
this.index = index;
}
public void register(SocketChannel sc) throws IOException {
if (!start) {
worker = Selector.open();
new Thread(this, "worker-" + index).start();
start = true;
}
// 线程间通信 通过队列
tasks.add(() -> {
try {
SelectionKey sckey = sc.register(worker, 0, null);
sckey.interestOps(SelectionKey.OP_READ);
worker.selectNow();
} catch (IOException e) {
e.printStackTrace();
}
});
// 唤醒worker线程 防止被select阻塞
worker.wakeup();
}
@Override
public void run() {
while (true) {
try {
// 阻塞等待事件
worker.select();
// 线程间通信 通过队列
Runnable task = tasks.poll();
if (task != null) {
task.run();
}
Set<SelectionKey> keys = worker.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(128);
try {
int read = sc.read(buffer);
if (read == -1) {
key.cancel();
sc.close();
} else {
buffer.flip();
log.debug("{} message:", sc.getRemoteAddress());
System.out.println(buffer);
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();
sc.close();
}
}
iter.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
TLV即像计算机网络数据报文.分为消息头和消息体.
在2.3中我们曾用分隔符解析粘包边界问题
public static void main(String[] args) throws IOException {
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
// 注册到selector,等待连接
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
socketChannel.bind(new InetSocketAddress(8080));
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
if (selectionKey.isAcceptable()) {
SocketChannel client = socketChannel.accept();
client.configureBlocking(false);
SelectionKey registerClient = client.register(selector, SelectionKey.OP_READ);
// 向客户端发送大量消息
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 30000000; i++) {
sb.append("a");
}
ByteBuffer buffer = ByteBuffer.wrap(sb.toString().getBytes());
// 返回值为写入的字节数
int write = client.write(buffer);
System.out.println(write);
// 判断是否写完
if (buffer.hasRemaining()) {
// 关注可写事件
registerClient.interestOps(SelectionKey.OP_WRITE + SelectionKey.OP_READ);
// 将未写完的数据放入到SelectionKey中
registerClient.attach(buffer);
}
}else if (selectionKey.isWritable()) {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
int write = client.write(buffer);
System.out.println(write);
// 清理操作
if(!buffer.hasRemaining()) {
selectionKey.attach(null);
selectionKey.interestOps(selectionKey.interestOps() - SelectionKey.OP_WRITE);
}
}
}
selectionKeys.clear();
}
}
selector多路复用选择器底层调用jdk DefaultSelectorProvider.在window,linux,unix,等中,jdk内部实现不同,但函数名相同.
windows
Linux
调用本地方法,c语言函数
是linux内核epoll_create()系统函数.epoll结构体调用存储对象信息.
man epoll_create
同步: 发送方单线程处理一个事件的请求与响应.
异步: 发送方一个线程负责请求,接收方一个线程负责响应.
BIO属于同步阻塞IO
NIO属于同步多路复用IO
AIO属于异步非阻塞IO
详见看一遍就理解:IO模型详解
零拷贝: 不需要在java内存中缓存数据. 内核态线程直接从磁盘拷贝到网卡
传统的 IO 将一个文件通过 socket 写出
File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");
byte[] buf = new byte[(int)f.length()];
file.read(buf);
Socket socket = ...;
socket.getOutputStream().write(buf);
Netty 是一个异步(非异步IO,指多线程)的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端
server端
public class Server {
public static void main(String[] args) {
// 服务端启动引导类
new ServerBootstrap()
.group(new NioEventLoopGroup()) // 服务端接收连接线程组 BossEventLoop,WorkerEventLoop
.channel(NioServerSocketChannel.class) // 指定服务端通道ServerSocketChannel实现类型
.childHandler(new ChannelInitializer<NioSocketChannel>() { // 指定客户端通道类型,决定了workerEventLoop执行的任务
/**
* 客户端通道初始化 用于添加处理器
* @param nioSocketChannel
* @throws Exception
*/
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringDecoder()); // 字符串解码器 将ByteBuf转换为字符串
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){ // 自定义处理器
@Override // 读取客户端发送的数据
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 打印上一步解码后的字符串
System.out.println(msg);
}
});
}
}).bind(8080); // 绑定端口
}
}
client端
public class Client {
public static void main(String[] args) throws InterruptedException {
// 客户端启动引导类
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()); // 字符串编码器 将字符串转换为ByteBuf
}
})
// 异步非阻塞 main发起了调用,真正执行connect 是 nio线程
.connect(new InetSocketAddress("localhost", 8080))// 连接服务端
.sync()// 主线程阻塞等待NIO线程连接成功
.channel() // 获取客户端通道
.writeAndFlush("hello,world"); // 向服务端发送数据
}
}
事件循环对象
EventLoop 本质是一个单线程执行器(同时维护了一个 Selector),里面有 run 方法处理 Channel 上源源不断的 io 事件。
它的继承关系比较复杂
事件循环组
EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全) 也实现了轮询公平分发.
NioEventLoop 处理 io 事件:
两个NioEventLoop 线程轮流处理 channel,但一旦建立连接,NioEventLoop线程与 channel 之间进行了绑定
server
new ServerBootstrap()
.group(new NioEventLoopGroup(1), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf byteBuf = msg instanceof ByteBuf ? ((ByteBuf) msg) : null;
if (byteBuf != null) {
byte[] buf = new byte[16];
ByteBuf len = byteBuf.readBytes(buf, 0, byteBuf.readableBytes());
log.debug(new String(buf));
}
}
});
}
}).bind(8080).sync();
// 由NIO异步线程调用
// channelFuture.addListener((ChannelFutureListener) future -> {
// System.out.println(future.channel()); // 2
//});
server
// 处理耗时长的任务
DefaultEventLoopGroup normalWorkers = new DefaultEventLoopGroup(2);
// 服务端启动引导类
new ServerBootstrap()
// 指定服务端线程组 1个用于接收客户端连接 2个用于处理客户端连接
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
// 指定服务端通道类型
.channel(NioServerSocketChannel.class)
// 指定服务端处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
// 添加处理器 用于处理客户端发送的数据
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
// 用额外的线程池处理耗时长的任务,防止阻塞当前线程处理后续客户端事件
ch.pipeline().addLast(normalWorkers, "myhandler",
new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf byteBuf = msg instanceof ByteBuf ? ((ByteBuf) msg) : null;
if (byteBuf != null) {
byte[] buf = new byte[16];
ByteBuf len = byteBuf.readBytes(buf, 0, byteBuf.readableBytes());
System.out.println(new String(buf));
}
// 通知后续处理器处理
ctx.fireChannelRead(msg);
}
});
}
}).bind(8080).sync();
channel 的主要作用
关闭是异步操作,所以需要注意善后操作执行
@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 ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress("localhost", 8080));
Channel channel = channelFuture.sync().channel();
log.debug("{}", channel);
new Thread(()->{
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if ("q".equals(line)) {
channel.close(); // close 异步操作 1s 之后
// log.debug("处理关闭之后的操作"); // 不能在这里善后
break;
}
channel.writeAndFlush(line);
}
}, "input").start();
// 获取 CloseFuture 对象, 1) 同步处理关闭, 2) 异步处理关闭
ChannelFuture closeFuture = channel.closeFuture();
/*log.debug("waiting close...");
closeFuture.sync();
log.debug("处理关闭之后的操作");*/
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
log.debug("处理关闭之后的操作");
// NIO关闭后,还需关闭其他NioEventLoopGroup中的线程
group.shutdownGracefully();
}
});
}
}
从上述代码我们可以看到,netty大多方法为异步方法.不能想当然的认为下一行代码就一定比当前行后执行. 都需要异步回调或者当前线程阻塞等待其他线程完成处理.那引入异步操作更麻烦了,为啥还要交给其他线程处理呢?
首先说明: 异步线程并没有实质上提高效率,但提高了吞吐量.
解释: 负责连接任务的线程将关闭连接任务,事件监听任务和事件处理任务交给工作线程处理.这意味着连接线程只用处理连接任务.则他的吞吐量将变的非常高.会更难因为连接数量过多而熔断. 效率没提高是因为处理一个连接的流程是有序的,不会因为异步而少做什么.所以相同时间段效率是相同的.只是异步接收了更多的任务.但剩余那部分任务流程还没处理完.而且因为线程间任务交付还可能增加时间.
DefaultEventLoop eventExecutors = new DefaultEventLoop();
DefaultPromise<Integer> promise = new DefaultPromise<>(eventExecutors);
eventExecutors.execute(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("set success, {}",10);
promise.setSuccess(10);
});
log.debug("start...");
log.debug("{}",promise.getNow()); // 还没有结果
log.debug("{}",promise.get());
DefaultEventLoop eventExecutors = new DefaultEventLoop();
DefaultPromise<Integer> promise = new DefaultPromise<>(eventExecutors);
// 设置回调,异步接收结果
promise.addListener(future -> {
// 这里的 future 就是上面的 promise
log.debug("{}",future.getNow());
});
// 等待 1000 后设置成功结果
eventExecutors.execute(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("set success, {}",10);
promise.setSuccess(10);
});
log.debug("start...");
ChannelHandler 用来处理 Channel 上的各种事件,分为入站、出站两种。所有 ChannelHandler 被连成一串,就是 Pipeline
super.channelRead(ctx, msg) ;
唤醒下一个入栈处理器.如下图所示必须使用ctx的Channel对象写操作.拿入栈的ctx只会从当前位置向前找出栈处理器
.如下图所示上文我们讲过零拷贝.直接内存就是只在系统线程进行的操作. 堆内存则需要将系统数据读到用户线程,再由用户线程写入系统线程.
可以使用下面的代码来创建池化基于堆的 ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
也可以使用下面的代码来创建池化基于直接内存的 ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
池化的最大意义在于可以重用 ByteBuf,优点有
池化功能是否开启,可以通过下面的系统环境变量来设置
-Dio.netty.allocator.type={unpooled|pooled}
ByteBuff读写指针共用一个指针.需要切换读写模式.
ByteBuf则读写两个指针,还支持动态扩容.但不能超过最大容量限制
扩容规则是
读过的内容,就属于废弃部分了,再读只能读那些尚未读取的部分.
如果需要重复读取 int 整数 5,怎么办?
可以在 read 前先做个标记 mark
buffer.markReaderIndex();
System.out.println(buffer.readInt());
log(buffer);
这时要重复读取的话,重置到标记位置 reset
buffer.resetReaderIndex();
log(buffer);
内存释放
由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收。
GC用的可达性分析. 而Netty 这里采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口
注意:
因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性(当然,如果在这个 ChannelHandler 内这个 ByteBuf 已完成了它的使命,那么便无须再传递)
基本规则是,谁是最后使用ByteBuf
,谁负责 release,不能只交给tail处理器.因为向后传的可能是ByteBuf的转化内容.就不会对ByteBuf进行释放,详细分析如下
Splice对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 并没有发生内存复制,还是使用原始 ByteBuf 的内存,切片后的 ByteBuf 维护独立的 read,write 指针,并非真正拷贝成两段.而是具备逻辑指针.如下图
无参 slice 是从原始 ByteBuf 的 read index 到 write index 之间的内容进行切片,切片后的 max capacity 被固定为这个区间的大小,因此不能追加 write.否则由于浅拷贝,会造成数据错乱,影响其他切片内容.因此如下图使用
截取了原始 ByteBuf 所有内容,并且没有 max capacity 的限制,也是与原始 ByteBuf 使用同一块底层内存,只是读写指针是独立的
会将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关
而CompositeByteBuf可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免拷贝.如下代码
浅拷贝注意内存释放.引用计数器
CompositeByteBuf buf3 = ByteBufAllocator.DEFAULT.compositeBuffer();
// true 表示增加新的 ByteBuf 自动递增 write index, 否则 write index 会始终为 0
buf3.addComponents(true, buf1, buf2);
原因: NIO底层使用TCP通信协议.
TCP是面向连接的运输层协议,提供可靠交付服务,提供全双工通信,面向字节流.
TCP协议的可靠传输是通过: (1)以字节为单位的滑动窗口 (2)改良Karn超时重传机制 (3)流量控制 (4)拥塞控制 来提供可靠交付服务.
其中滑动窗口受流量控制和拥塞控制的影响.为接收方和发送方累计确认一组数据的大小做了约束.因此会出现粘包和半包问题.
TCP原理:
详见
<<计算机网络>>(第8版)谢希仁 P219
<
粘包
半包
1,2,3我们很容易理解解决半包和粘包.
那4怎么解决半包的呢? 下面是对LTC预取值解码器的介绍
在Netty中,所有的数据都被封装成ByteBuf对象进行传输。当使用LengthFieldBasedFrameDecoder解码器时,它会监听ByteBuf输入事件,也就是有新的数据到达时会触发decode()方法进行解码处理。
具体来说,LengthFieldBasedFrameDecoder在decode()方法中会先读取指定长度的消息头,这个长度可以通过参数来设置。消息头通常包含了消息体的长度信息,因此我们需要先从消息头中提取出消息体的长度,然后再根据消息体的长度对TCP流进行拆包操作。
接下来,让我们来看一下LengthFieldBasedFrameDecoder的源代码:
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 首先判断是否已经读取到了消息头的长度
if (this.discardingTooLongFrame) {
this.discardedBytes += in.readableBytes();
if (this.discardedBytes > this.maxFrameLength) {
this.fail(ctx);
return;
}
// 如果还没有读取够消息头的长度,则继续读取
this.partiallyDiscardReadBytes(in);
} else {
// 读取消息头的长度
if (this.bytesToDiscard == 0L) {
if (in.readableBytes() < this.lengthFieldEndOffset) {
return;
}
// 根据指定的长度字段类型从消息头中读取消息体的长度
this.extractFrameLength(in, in.readerIndex() + this.lengthFieldOffset, this.lengthFieldLength, this.byteOrder);
}
// 根据消息体的长度对TCP流进行拆包操作
if (in.readableBytes() >= this.bytesToDiscard) {
int actualLengthFieldOffset = in.readerIndex() + this.lengthFieldOffset;
byte[] frame = new byte[(int)this.bytesToDiscard];
in.getBytes(actualLengthFieldOffset, frame);
out.add(Unpooled.wrappedBuffer(frame));
in.skipBytes((int)this.bytesToDiscard);
this.bytesToDiscard = 0L;
} else {
// 如果还未读取到整个消息,则先缓存起来等待下一次数据传输
this.bytesToDiscard -= (long)in.readableBytes();
out.add(in.readRetainedSlice(in.readableBytes()));
}
}
}
在这段代码中,我们可以看到LengthFieldBasedFrameDecoder在decode()方法中根据指定的参数进行了如下操作:
1.如果已经读取到了消息头的长度,则直接根据消息体的长度对TCP流进行拆包操作。
2.如果还没有读取够消息头的长度,则继续读取字节数据,并从中获取消息体的长度。如果一直等不到,触发超时重传机制.tcp协议懂得都懂.
3.如果当前可读字节数小于消息体的长度,则将收到的字节暂时缓存下来,等待下一次数据传输。
4.如果收到的字节数超过了最大允许长度,则触发fail()方法进行异常处理。
综上所述,通过对消息头中的长度字段进行读取和解析,LengthFieldBasedFrameDecoder能够有效地处理TCP流中的拆包和粘包情况,从而解决半包问题。同时,它也提供了多种参数来控制缓存大小、消息头长度等,以便更好地适应不同的业务场景。
// 最大长度,长度偏移,长度占用字节,长度调整,剥离字节数
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));
客户端代码
public class HelloWorldClient {
static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
log.debug("connetted...");
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("sending...");
Random r = new Random();
char c = 'a';
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
byte length = (byte) (r.nextInt(16) + 1);
// 先写入长度
buffer.writeByte(length);
// 再
for (int j = 1; j <= length; j++) {
buffer.writeByte((byte) c);
}
c++;
}
ctx.writeAndFlush(buffer);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("192.168.0.103", 9090).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("client error", e);
} finally {
worker.shutdownGracefully();
}
}
}
nioSocketChannel.pipeline().addLast(new StringEncoder()); // 字符串编码器 将字符串转换为ByteBuf
nioSocketChannel.pipeline().addLast(new RedisEncoder()); // redis编码器 将redis命令转换为ByteBuf
nioSocketChannel.pipeline().addLast(new RedisDecoder()); // redis解码器 将ByteBuf转换为redis命令
nioSocketChannel.pipeline().addLast(new HttpClientCodec()); // http编码器 将http请求转换为ByteBuf
nioSocketChannel.pipeline().addLast(new HttpServerCodec()); // http解码器 将ByteBuf转换为http请求
NioEventLoopGroup worker = new NioEventLoopGroup();
byte[] LINE = {13, 10};
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 会在连接 channel 建立成功后,会触发 active 事件
@Override
public void channelActive(ChannelHandlerContext ctx) {
set(ctx);
get(ctx);
}
private void get(ChannelHandlerContext ctx) {
ByteBuf buf = ctx.alloc().buffer();
buf.writeBytes("*2".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("get".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("aaa".getBytes());
buf.writeBytes(LINE);
ctx.writeAndFlush(buf);
}
/**
* set name 张三
* *3 元素个数为3
* $3 set字节长度
* set set字符串
* $4 name字节长度
* name name字符串
* $6 张三字节长度 UTF-8编码
* 张三 张三字符串
*/
private void set(ChannelHandlerContext ctx) {
ByteBuf buf = ctx.alloc().buffer();
buf.writeBytes("*3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("set".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("aaa".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("bbb".getBytes());
buf.writeBytes(LINE);
ctx.writeAndFlush(buf);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(buf.toString(Charset.defaultCharset()));
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 6379).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("client error", e);
} finally {
worker.shutdownGracefully();
}
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
// 获取请求行
System.out.println(msg.uri());
// 返回响应 http协议版本,状态码
DefaultFullHttpResponse response =
new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
byte[] bytes = "Hello, world!
".getBytes();
// 设置响应头内容长度
response.headers().setInt(CONTENT_LENGTH, bytes.length);
response.content().writeBytes(bytes);
// 写回响应
ctx.writeAndFlush(response);
}
});
/*ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("{}", msg.getClass());
if (msg instanceof HttpRequest) { // 请求行,请求头
} else if (msg instanceof HttpContent) { //请求体
}
}
});*/
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error"+e);
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
根据上面的要素,设计一个登录请求消息和登录响应消息,并使用 Netty 完成收发
@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
// 1. 4 字节的魔数
out.writeBytes(new byte[]{1, 2, 3, 4});
// 2. 1 字节的版本,
out.writeByte(1);
// 3. 1 字节的序列化方式 jdk 0 , json 1
out.writeByte(0);
// 4. 1 字节的指令类型
out.writeByte(msg.getMessageType());
// 5. 4 个字节
out.writeInt(msg.getSequenceId());
// 无意义,对齐填充
out.writeByte(0xff);
// 6. 获取内容的字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(msg);
byte[] bytes = bos.toByteArray();
// 7. 长度
out.writeInt(bytes.length);
// 8. 写入内容
out.writeBytes(bytes);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int magicNum = in.readInt();
byte version = in.readByte();
byte serializerType = in.readByte();
byte messageType = in.readByte();
int sequenceId = in.readInt();
in.readByte();
int length = in.readInt();
byte[] bytes = new byte[length];
in.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);
out.add(message);
}
}
测试
EmbeddedChannel channel = new EmbeddedChannel(
new LoggingHandler(),
// 解决半包
new LengthFieldBasedFrameDecoder(
1024, 12, 4, 0, 0),
new MessageCodec()
);
// encode
LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123", "张三");
// channel.writeOutbound(message);
// decode
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null, message, buf);
ByteBuf s1 = buf.slice(0, 100);
ByteBuf s2 = buf.slice(100, buf.readableBytes() - 100);
s1.retain(); // 引用计数 2
channel.writeInbound(s1); // release 1
channel.writeInbound(s2);
@Slf4j
@ChannelHandler.Sharable
/**
* 必须和 LengthFieldBasedFrameDecoder 一起使用,确保接到的 ByteBuf 消息是完整的
*/
public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf, Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> outList) throws Exception {
ByteBuf out = ctx.alloc().buffer();
// 1. 4 字节的魔数
out.writeBytes(new byte[]{1, 2, 3, 4});
// 2. 1 字节的版本,
out.writeByte(1);
// 3. 1 字节的序列化方式 jdk 0 , json 1
out.writeByte(0);
// 4. 1 字节的指令类型
out.writeByte(msg.getMessageType());
// 5. 4 个字节
out.writeInt(msg.getSequenceId());
// 无意义,对齐填充
out.writeByte(0xff);
// 6. 获取内容的字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(msg);
byte[] bytes = bos.toByteArray();
// 7. 长度
out.writeInt(bytes.length);
// 8. 写入内容
out.writeBytes(bytes);
outList.add(out);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int magicNum = in.readInt();
byte version = in.readByte();
byte serializerType = in.readByte();
byte messageType = in.readByte();
int sequenceId = in.readInt();
in.readByte();
int length = in.readInt();
byte[] bytes = new byte[length];
in.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);
out.add(message);
}
}
原因
问题
服务器端解决
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 5s 内如果没有收到 channel 的数据,会触发一个 IdleState#READER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(5, 0, 0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
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) {
log.debug("已经 5s 没有读到数据了");
ctx.channel().close();
}
}
});
客户端定时心跳
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 3s 内如果没有向服务器写数据,会触发一个 IdleState#WRITER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(0, 3, 0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
// 用来触发特殊事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{
IdleStateEvent event = (IdleStateEvent) evt;
// 触发了写空闲事件
if (event.state() == IdleState.WRITER_IDLE) {
// log.debug("3s 没有写数据了,发送一个心跳包");
ctx.writeAndFlush(new PingMessage());
}
}
});
属于 SocketChannal 参数
用在客户端建立连接时,如果在指定毫秒内无法连接,会抛出 timeout 异常
SO_TIMEOUT 主要用在阻塞 IO,阻塞 IO 中 accept,read 等都是无限等待的,如果不希望永远阻塞,使用它调整超时时间
@Slf4j
public class TestConnectionTimeout {
public static void main(String[] args) {
// new ServerBootstrap().option() 是给ServerSocketChannel配置参数
// new ServerBootstrap().childOption() 给SocketChannel配置参数
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(group)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 300)
.channel(NioSocketChannel.class)
.handler(new LoggingHandler());
ChannelFuture future = bootstrap.connect("127.0.0.1", 8080);
future.sync().channel().closeFuture().sync(); // 断点1
} catch (Exception e) {
e.printStackTrace();
log.debug("timeout");
} finally {
group.shutdownGracefully();
}
}
}
其中
在 linux 2.2 之前,backlog 大小包括了两个队列的大小,在 2.2 之后,分别用下面两个参数来控制
sync queue - 半连接队列
syncookies
启用的情况下,逻辑上没有最大值限制,这个设置便被忽略accept queue - 全连接队列
netty 中可以通过 option(ChannelOption.SO_BACKLOG, 值) 来设置大小