Netty 学习
记录学习Netty的过程
文章目录
- Netty 学习
- 1.前言
- 第一章NIO基础
- 2.nio三大组件--channel-buffer
- 3.nio三大组件--服务器设计--多线程版
- 4.nio三大组件--服务器设计--线程池版
- 5.nio三大组件--服务器设计--selector
- 6.bytebuffer基本使用
- 7.bytebuffer--内部结构
- 8.bytebuffer--方法演示1
- 9.bytebuffer--方法演示2
- 10.bytebuffer--方法演示3
- 11.bytebuffer--方法演示4
- 12.bytebuffer--分散读集中写
- 13.bytebuffer--黏包分析
- 14.bytebuffer--黏包半包解析
- 15.filechannel--方法简介
- 16.filechannel--传输数据
- 17.filechannel--传输数据大于2g
- 18.path&flies
- 19.files--walkfiletree
- 20.files--walkfiletree--删除多级目录
- 21.files--walk-拷贝多级目录
- 22.nio--阻塞模式
- 23.nio--阻塞模式-调式1
- 24.nio--阻塞模式-调式2
- 25.nio--非组设模式
- 26.nio--非阻塞模式调式
- 27.nio-selector-处理accept
- 28.nio-selector-cancel
- 29.nio-selector-处理read
- 30.nio-selector--用完key为什么要remove
- 31.nio-selector--处理客户端断开
- 32.nio-selector-消息边界问题
- 33.nio-selector--处理消息边界
- 34.nio-selector--处理消息边界--容量超出
- 35.nio-selector--处理消息边界--附件与扩容
- 36.nio-selector--bytebuffer大小分配
- 37.nio-selector--写入内容过多问题
- 38.nio-selector--处理可写事件
- 39.nio--网络编程小结
- 40.nio--多线程优化--分析
- 41.nio-多线程优化-worker编写
- 42.nio-多线程优化-worker关联
- 43.nio-多线程优化-问题分析
- 44.nio-多线程优化-问题解决
- 45.nio-多线程优化-问题解决wake-up
- 46.nio-多线程优化-多worker
- 47.nio-概念剖析-stream vs channel
- 48.nio-概念剖析-io-模型-阻塞非阻塞
- 49.nio-概念剖析-io模型-多路复用
- 50.nio-概念剖析-io模型-异步
- 51.nio-概念剖析-零拷贝
- 52.nio-概念剖析-io模式-异步例子
- 第二章 Netty入门
- 53.netty-入门-概述
- 54.netty入门-hello-server
- 55.netty入门-hello-client
- 56.netty入门-hello-流程分析
- 57.netty入门-hello-正确观念
- 58.netty入门-eventloop概述
- 59.netty 入门eventloop-普通-定时任务
- 60.netty入门eventloop-io任务
- 61 eventLoop-分工细化
- 62 eventLoop-分工细化2
- 63 eventLoop-切换线程
- 64 channel
- 65 channelFuture-连接问题
- 66 channelFuture-处理结果
- 67 channelFuture-关闭问题
- 68 channelFuture-处理关闭
- 69 channelFuture-处理关闭2
- 70 为什么要异步
- 71 future-promise-概述
- 72 jdk-future
- 73 netty-future
- 74 netty-promise
- 75 pipeline
- 76 inbound-handler
- 77 outbound-handler
- 78 embedded-channel
- 79 bytebuf-创建
- 80 bytebuf-是否池化
- 81 bytebuf-组成
- 82 bytebuf-写入
- 83 bytebuf-获取
- 84 bytebuf-内存释放
- 85 bytebuf-头尾释放
- 86 bytebuf-零拷贝-slice
- 87 bytebuf-零拷贝-slice2
- 88 bytebuf-零拷贝-composite
- 90 思考问题
- 第三章 Netty 进阶
- 91 黏包半包-现象演示
- 92 黏包半包-滑动窗口
- 93 黏包半包-分析
- 94 黏包半包-解决-短连接
- 95 黏包半包-解决-定长解码器
- 96 黏包半包-解决-行解码器
- 97 黏包半包-解决-LTC解码器
- 98 黏包半包-解决-LTC解码器2
- 99 协议设计与解析-redis
- 100 协议设计与解析-http
- 101 协议设计与解析-自定义
- 102 协议设计与解析-编码
- 103 协议设计与解析-解码
- 104 协议设计与解析-测试
- 105 协议设计与解析-测试2
- 106 协议设计与解析-@sharable
- 107 协议设计与解析-@sharable
- 108 聊天业务-介绍
- 109 聊天业务-包结构
- 110 聊天业务-登录
- 111 聊天业务-登录-线程通信
- 112 聊天业务-业务消息推送
- 小结
1.前言
为什么要学习Netty?
1.现在的互联网环境下,分布式系统大行其道,而分布式系统的根基在于网络编程,而Netty恰恰是Java领域网络编程的王者。如果要致力于开发高性能的服务器程序,高性能的客户端程序,必须掌握Netty.
提示:以下是本篇文章正文内容,下面案例可供参考
第一章NIO基础
2.nio三大组件–channel-buffer
non-blocking io 非阻塞io(或new IO)
1.1 Channel & Buffer
1.Channel: channel有一点类似于stream,它就是读写数据的双向通道,可以从channel 将数据读入buffer,也可以将buffer的数据写入channel,而之前的stream要么是输入,要么是输出,channel 比stream 更为底层
常见的Channel 有:
- FileChannel (文件数据传输通道)
- DatagramChannel (UDP网络编程时的数据通道)
- SocketChannel (Tcp服务器/客户端传输通道)
- ServerSocketChannel(Tcp服务器端传输通道)
2.Buffer用来缓存读写数据
常见的buffer有:
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- CharBuffer
3.nio三大组件–服务器设计–多线程版
Selector
selector 单从字面意思不好理解,需要结合服务器的设计演化来理解它的用途
在NIO出现之前我们怎么开发一个服务端的程序呢?
服务器开发肯定要处理多个客户端的通信,所以我们可以使用多线程设计,也就是一个客户端来了,在我们的代码中表现为socket 针对socket 就可以进行读写操作(每一个线程都对应一个socket)
多线程设计
多线程版缺点
- 内存占用高
- 线程上下文切换成本高
- 只适合连接数少的场景
4.nio三大组件–服务器设计–线程池版
线程池版设计
线程池版缺点
- 阻塞模式下,线程仅能处理一个socket连接
- 仅适合短连接场景(http请求)
- 由于socket在阻塞模式下,线程的利用率不高
5.nio三大组件–服务器设计–selector
Selector 版设计
selector 的作用就是配合一个线程来管理多个channel,获取这些channel上发生的事件,这些channel工作在非阻塞模式下,不会让线程吊死在一个channel上适合连接数特别多,但流量低(low traffic)
调用selector的select()会阻塞到channel发生了读写就绪事件,这些事件发生,select 方法就会返回这些事件交给thread来处理
6.bytebuffer基本使用
使用byteBuffer和Channel读取文件
public static void main(String[] args) {
//文件的读取
// FileChannel 的获取:1.输入输出流, 2.RandomAccessFile
try (FileChannel channel = new FileInputStream("D:\\a.txt").getChannel()) {
//准备缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
//向buffer写
channel.read(byteBuffer);
//logback.xml
//切换到读模式
byteBuffer.flip();
//缓冲区最多读取10个字节因为上面设置了为10
//多次读取
while (byteBuffer.hasRemaining()) {
byte b = byteBuffer.get();
System.out.println((char) b);
}
} catch (IOException e) {
}
}
public static void main2(String[] args) {
try (FileChannel channel = new FileInputStream("D:\\a.txt").getChannel()) {
//准备缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
while (true) {
//向buffer写
int len = channel.read(byteBuffer);
log.debug("读取到的字节数为{len}",len);
//证明已经读取完了
if (len == -1) {
return;
}
//切换到读模式
byteBuffer.flip();
//缓冲区最多读取10个字节因为上面设置了为10
//多次读取
while (byteBuffer.hasRemaining()) {
byte b = byteBuffer.get();
System.out.println((char) b);
}
//读取完依次以后切换为写模式
byteBuffer.clear();
}
} catch (IOException e) {
}
}
7.bytebuffer–内部结构
2.1 ByteBuffer正确使用姿势
1.向buffer写入数据,例如调用channel.read(buffer)
2.调用flip()切换至读模式
3.从buffer读取数据,例如调用buffer.get()
4.调用clear() 或compact()切换至写模式
5.重复1~4步骤
2.2 ByteBuffer结构
ByteBuffer 有以下重要属性
- capacity 容量
- position 读写指针(索引下标)
- limit 读写限制
一开始
写模式下,position 是写入位置,limit等于容量,下图表示写入了4个字节后的状态
filp动作发生后,position 切换为读模式,limit 切换为读取限制
读取了四个字节后,状态
clear 动作发生后,状态
compact 方法,是把未读完的部分向前压缩,然后切换至写模式
8.bytebuffer–方法演示1
ByteBufferUtil(工具类)
package com.netty.c1;
import java.nio.ByteBuffer;
import io.netty.util.internal.MathUtil;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.MathUtil.*;
/**
* @author Panwen Chen
* @date 2021/4/12 15:59
*/
public class ByteBufferUtil {
private static final char[] BYTE2CHAR = new char[256];
private static final char[] HEXDUMP_TABLE = new char[256 * 4];
private static final String[] HEXPADDING = new String[16];
private static final String[] HEXDUMP_ROWPREFIXES = new String[65536 >>> 4];
private static final String[] BYTE2HEX = new String[256];
private static final String[] BYTEPADDING = new String[16];
static {
final char[] DIGITS = "0123456789abcdef".toCharArray();
for (int i = 0; i < 256; i++) {
HEXDUMP_TABLE[i << 1] = DIGITS[i >>> 4 & 0x0F];
HEXDUMP_TABLE[(i << 1) + 1] = DIGITS[i & 0x0F];
}
int i;
// Generate the lookup table for hex dump paddings
for (i = 0; i < HEXPADDING.length; i++) {
int padding = HEXPADDING.length - i;
StringBuilder buf = new StringBuilder(padding * 3);
for (int j = 0; j < padding; j++) {
buf.append(" ");
}
HEXPADDING[i] = buf.toString();
}
// Generate the lookup table for the start-offset header in each row (up to 64KiB).
for (i = 0; i < HEXDUMP_ROWPREFIXES.length; i++) {
StringBuilder buf = new StringBuilder(12);
buf.append(StringUtil.NEWLINE);
buf.append(Long.toHexString(i << 4 & 0xFFFFFFFFL | 0x100000000L));
buf.setCharAt(buf.length() - 9, '|');
buf.append('|');
HEXDUMP_ROWPREFIXES[i] = buf.toString();
}
// Generate the lookup table for byte-to-hex-dump conversion
for (i = 0; i < BYTE2HEX.length; i++) {
BYTE2HEX[i] = ' ' + StringUtil.byteToHexStringPadded(i);
}
// Generate the lookup table for byte dump paddings
for (i = 0; i < BYTEPADDING.length; i++) {
int padding = BYTEPADDING.length - i;
StringBuilder buf = new StringBuilder(padding);
for (int j = 0; j < padding; j++) {
buf.append(' ');
}
BYTEPADDING[i] = buf.toString();
}
// Generate the lookup table for byte-to-char conversion
for (i = 0; i < BYTE2CHAR.length; i++) {
if (i <= 0x1f || i >= 0x7f) {
BYTE2CHAR[i] = '.';
} else {
BYTE2CHAR[i] = (char) i;
}
}
}
/**
* 打印所有内容
* @param buffer
*/
public static void debugAll(ByteBuffer buffer) {
int oldlimit = buffer.limit();
buffer.limit(buffer.capacity());
StringBuilder origin = new StringBuilder(256);
appendPrettyHexDump(origin, buffer, 0, buffer.capacity());
System.out.println("+--------+-------------------- all ------------------------+----------------+");
System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), oldlimit);
System.out.println(origin);
buffer.limit(oldlimit);
}
/**
* 打印可读取内容
* @param buffer
*/
public static void debugRead(ByteBuffer buffer) {
StringBuilder builder = new StringBuilder(256);
appendPrettyHexDump(builder, buffer, buffer.position(), buffer.limit() - buffer.position());
System.out.println("+--------+-------------------- read -----------------------+----------------+");
System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), buffer.limit());
System.out.println(builder);
}
private static void appendPrettyHexDump(StringBuilder dump, ByteBuffer buf, int offset, int length) {
if (MathUtil.isOutOfBounds(offset, length, buf.capacity())) {
throw new IndexOutOfBoundsException(
"expected: " + "0 <= offset(" + offset + ") <= offset + length(" + length
+ ") <= " + "buf.capacity(" + buf.capacity() + ')');
}
if (length == 0) {
return;
}
dump.append(
" +-------------------------------------------------+" +
StringUtil.NEWLINE + " | 0 1 2 3 4 5 6 7 8 9 a b c d e f |" +
StringUtil.NEWLINE + "+--------+-------------------------------------------------+----------------+");
final int startIndex = offset;
final int fullRows = length >>> 4;
final int remainder = length & 0xF;
// Dump the rows which have 16 bytes.
for (int row = 0; row < fullRows; row++) {
int rowStartIndex = (row << 4) + startIndex;
// Per-row prefix.
appendHexDumpRowPrefix(dump, row, rowStartIndex);
// Hex dump
int rowEndIndex = rowStartIndex + 16;
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);
}
dump.append(" |");
// ASCII dump
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);
}
dump.append('|');
}
// Dump the last row which has less than 16 bytes.
if (remainder != 0) {
int rowStartIndex = (fullRows << 4) + startIndex;
appendHexDumpRowPrefix(dump, fullRows, rowStartIndex);
// Hex dump
int rowEndIndex = rowStartIndex + remainder;
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);
}
dump.append(HEXPADDING[remainder]);
dump.append(" |");
// Ascii dump
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);
}
dump.append(BYTEPADDING[remainder]);
dump.append('|');
}
dump.append(StringUtil.NEWLINE +
"+--------+-------------------------------------------------+----------------+");
}
private static void appendHexDumpRowPrefix(StringBuilder dump, int row, int rowStartIndex) {
if (row < HEXDUMP_ROWPREFIXES.length) {
dump.append(HEXDUMP_ROWPREFIXES[row]);
} else {
dump.append(StringUtil.NEWLINE);
dump.append(Long.toHexString(rowStartIndex & 0xFFFFFFFFL | 0x100000000L));
dump.setCharAt(dump.length() - 9, '|');
dump.append('|');
}
}
public static short getUnsignedByte(ByteBuffer buffer, int index) {
return (short) (buffer.get(index) & 0xFF);
}
}
ByteBuffer buffer = ByteBuffer.allocate(10);
//put字节/字节数组
buffer.put((byte) 0x61); //'a'
debugAll(buffer);
buffer.put(new byte[]{0x62, 0x63, 0x64});
debugAll(buffer);
buffer.flip();
System.out.println(buffer.get());
//把
buffer.compact();
debugAll(buffer);
buffer.put(new byte[]{0x65, 0x6f});
Bytebuffer buf = ByteBuffer.allocate(16)
直接看例子:
9.bytebuffer–方法演示2
2.3 ByteBuffer常见方法
分配空间
可以使用allcate 方法为ByteBuffer 分配空间,其他buffer类也有该方法
Bytebuffer buf = ByteBuffer.allocate(16);
package com.netty.c1;
import java.nio.ByteBuffer;
public class TestByteBUfferAllocate {
public static void main(String[] args) {
//byteBuffer不能动态的调整容量
ByteBuffer.allocate(16);
System.out.println(ByteBuffer.allocate(16).getClass());
System.out.println(ByteBuffer.allocateDirect(16).getClass());
//class java.nio.HeapByteBuffer -java堆内存,读写效率较低,受到垃圾回收的影响
//class java.nio.DirectByteBuffer -直接内存,读写效率高(少一次数据的拷贝),使用的是系统内存
//因为是系统内存,要调用操作系统分配函数,所以分配速度慢,使用不当就会造成内存泄漏
}
}
向buffer写入数据
有两种办法
- 调用channel的read方法
- 调用buffer自己的put方法
int readBytes = channel.read(buf);
和
buf.put((byte)127);
从buffer读取数据
同样有两种办法
- 调用channel 的write方法
- 调用buffer的get方法
int writeBytes = channel.write(buf);
和
byte b = buf.get();
get 方法会让positon 读指针向后走,如果想重复读取数据
- 可以调用rewind方法将positon 重新置为0
- 或者调用get(int i)方法获取索引 i的内容,它不会移动读指针
10.bytebuffer–方法演示3
package com.netty.c1;
import java.nio.ByteBuffer;
import static com.netty.c1.ByteBufferUtil.debugAll;
public class TestBytefferRead {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put(new byte[]{'a', 'b', 'c', 'd'});
buffer.flip();
//从头开始读
buffer.get(new byte[4]);
debugAll(buffer);
buffer.rewind();
System.out.println(buffer.get());
//mark & reset
//mark 就是做一个标记,记录position 位置
//reset 重置到mark的位置
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());
buffer.reset();
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
//get(i)
//它的position不会变的
System.out.println((char) buffer.get(3));
}
}
11.bytebuffer–方法演示4
字符串与ByteBuffer互转
package com.netty.c1;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import static com.netty.c1.ByteBufferUtil.debugAll;
public class TestByteBufferString {
public static void main(String[] args) {
//1.字符串转换为ByteBuffer
ByteBuffer bu = ByteBuffer.allocate(16);
//用的是操作系统的字符集
bu.put("hello".getBytes());
debugAll(bu);
//2. CharSet,自动切换到读模式
ByteBuffer hello = StandardCharsets.UTF_8.encode("hello");
debugAll(hello);
// 3. wrap 把一个字节数组包装成ByteBuffer
//自动切换到读模式
ByteBuffer wrap = ByteBuffer.wrap("hello".getBytes());
debugAll(wrap);
//转字符串
String str1 = StandardCharsets.UTF_8.decode(hello).toString();
}
}
12.bytebuffer–分散读集中写
2.4 Scattering Reads
分散读取,有一个文本文件3parts.txt
onetwothree
使用如下方式读取,可以将数据填充至多个buffer中(分散读):
package com.netty.c1;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import static com.netty.c1.ByteBufferUtil.debugAll;
//随机读取
public class TestScatteringReads {
public static void main(String[] args) {
try (FileChannel r = new RandomAccessFile("D:\\3parts.txt", "r").getChannel()) {
ByteBuffer b1 = ByteBuffer.allocate(3);
ByteBuffer b2 = ByteBuffer.allocate(3);
ByteBuffer b3 = ByteBuffer.allocate(5);
r.read(new ByteBuffer[]{b1, b2, b3});
//切换读模式
b1.flip();
b2.flip();
b3.flip();
debugAll(b1);
debugAll(b2);
debugAll(b3);
} catch (IOException e) {
}
}
}
集中写:
package com.netty.c1;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
//集中写入
//两种思路,把多个byteBuffer组合到大的byteBuffer中,然后把大的buffer写入文件
//第二个:把三个byteBuffer组合到一起,组成一个整体
public class TestGatheringWrite {
public static void main(String[] args) {
ByteBuffer b1 = StandardCharsets.UTF_8.encode("hello");
ByteBuffer b2 = StandardCharsets.UTF_8.encode("world");
//你好UTF-8编码,6个字节
ByteBuffer b3 = StandardCharsets.UTF_8.encode("你好");
try (FileChannel rw = new RandomAccessFile("D:\\words2.txt", "rw").getChannel()) {
rw.write(new ByteBuffer[]{b1, b2, b3});
} catch (IOException e) {
}
}
}
13.bytebuffer–黏包分析
先来看一个问题:网络上有多条数据发送给服务端,数据之间使用\n进行分割但由于某种原因这些数据在接受时,被进行了重新组合,例如原始数据有3条为Hello,world\n I’m zhangsan\n How are you?\n 变成了 下面两个byteBuffer Hello,world\nI’m(粘包) zhangsan\nHo w are you?\n (半包)
粘包的原因:等缓冲区满了,才发送给服务器,一次性发送多条消息造成了服务器接收的时候接收多条消息
半包的原因:服务器缓冲区大小决定的,一次性接收不了这么多数据,剩余的第二次接收
14.bytebuffer–黏包半包解析
看个demo:
package com.netty.c1;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import static com.netty.c1.ByteBufferUtil.debugAll;
public class TestByteBufferExam {
//这个demo是演示粘包和半包问题
public static void main(String[] args) {
//先创建一个32字节的ByteBuffer对象
ByteBuffer source = ByteBuffer.allocate(32);
//byteBuffer接收消息
source.put("Hello,world\nI'mzhangsan\nHo".getBytes());
//解决粘包问题
split(source);
//再次接收消息
source.put("w are you?\n".getBytes());
//再次解决粘包问题
split(source);
}
private static void split(ByteBuffer source) {
//读取ByteBuffer我们首先切换到读模式
source.flip();
//怎么接收呢?遇到\n符号就输出呗
//一直遍历到最后还没有遇到\n符号直接把
//这次还没完全读取完的数据进行保留再次读取就行了
//limit 写入时的position移动的最终位置
for (int i = 0; i < source.limit(); i++) {
//首先geti是不会移动position指针
//get是会的,等于'\n'是一个完成的包我们就可以
//进行读取了
if (source.get(i) == '\n') {
//范围是多少呢?
// i-position+1
int size = i - source.position() + 1;
for (int j = 0; j < size; j++) {
System.out.print((char) source.get());
}
System.out.println();
}
}
//读取到最后的时候有可能不会有\n还有剩余的
//把未读取的前移动,并且position指针移动到没有byte的
//后一位
source.compact();
}
}
15.filechannel–方法简介
3.文件编程
⚠️ 3.1 FIleChannel
FileChannel 只能工作在阻塞模式下
获取
不能直接打开FileChannel,必须通过FileInputStream、FileOutputStream 或者 RandomAccessFile来获取FileChannel,他们都有getChannel方法
- 通过FileInputStream获取的channel 只能读
- 通过FileOutputStream获取的channel只能写
- 通过RandomAccessFile是否能读写根据构造RandomAccessFile 时的读写模式决定
读取
会从channel 读取数据填充ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾
int readBytes = channel.read(buffer);
写入
写入的正确姿势如下,SocketChannel
ByteBuffer buffer=…;
buffer.put(…);//存入数据
buffer.flip(); //切换读模式
while(buffer.hasRemaining()){
channel.write(buffer);
}
在while 中调用channel.write 是因为wirte 方法并不能保证一次将buufer中的内容全部写入channel
关闭
channel 必须关闭,不过调用了FileInputStream、FileOutputStream 或者RandomAccessFile的close 方法会间接地调用channel地close方法
位置
获取当前位置
long pos = channel.position();
设置当前位置:
long newPos = …;
channel.position(newPos);
设置当前位置时,如果设置为文件的末尾
- 这时读取会返回-1
- 这时写入,会追加内容,但要注意如果position超过了文件末尾,再写入时再新内容和原末尾之间会有空洞(00)
大小
FileChannel 使用size方法获取文件的大小
强制写入
操作系统系统出于性能的考虑,FileChannel会将数据缓冲,不是立刻写入磁盘。可以调用fore(true)方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘
16.filechannel–传输数据
3.2两个Channel的文件传输
package com.netty.c1;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class TestFileChannelTransferTo {
public static void main(String[] args) {
try {
//读取数据
FileChannel from = new FileInputStream("D:\\from.txt").getChannel();
//写入数据
FileChannel to = new FileOutputStream("D:\\to.txt").getChannel();
// 起始位置,传多少数据,to往那传数据
//底层利用操作系统的零拷贝进行优化
from.transferTo(0, from.size(), to);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
17.filechannel–传输数据大于2g
传输大文件的时候分次传输
try {
FileChannel from = new FileInputStream("D:\\from.txt").getChannel();
FileChannel to = new FileOutputStream("D:\\to.txt").getChannel();
//当文件较大时,多次传输
long size = from.size();
//left 代表还剩余多少没有传输
for (long let = size; let > 0; ) {
//transferTo方法返回实际传输的字节数
long realSize = from.transferTo(size - let, size, to);
//我们总的字节数-实际传输的字节数=剩余的字节数
let = let - realSize;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
18.path&flies
3.3Path
jdk7引入了Path 和Paths类
- Path 用来表示文件路径
- Paths是工具类,用来获取Path实例
Path source = Paths.get("1.txt");//相对路径 使用user.dir 环境变量来定位1.txt
Path source = Paths.get("d:\\1.txt");相对路径代表了d:\1.txt
Path source = Paths.get("d:\1.txt");//绝对路径,同样代表了d:\1.txt
Path projects = Paths.get("d:\\data","projects");//代表了d:\data\projects
- . 代表了当前路径
- … 代表了上一级路径
例如目录结构如下:
d:
|-data
|-projects
|-a
|-b
代码
Path path = Paths.get("d:\\data\\projects\\a..\\b");
System.out.println(path);
System.out.println(path.normalize());//正常化路径解析. 或者..
3.4Files(jdk1.7新增的)
检查文件是否存在
Path path = Paths.get("helloworld/data.txt");
System.out.println(Files.exists(path));
创建一级目录
Path path = Paths.get("helloword/d1");
Files.crerateDirectory(path);
- 如果目录已存在,会抛出FileAlreadyExistsException
- 不能一次创建多级目录,否则会抛出NoSuchFileException
创建多级目录
Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);
拷贝文件
Path source =Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");
//也是底层用了操作系统的实现也比较快但是没有使用零拷贝技术
Files.copy(source,target);
- 如果文件已经存在,会抛出异常FileAlreadyExistsException
如果希望用source 覆盖掉target,需要用StandardCopyOption来控制
Files.copy(source,target,StandardCopyOption.REPLACE_EXISTING");
移动文件
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");
Files.move(source,target,StandardCopyOption.ATOMIC_MOVE);
- StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性
删除文件
Path target = Path.get("helloword/target.txt");
Files.delete(target);
- 如果文件不存在,会抛出NoSuchFileException
删除目录
Path target = Path.get("helloword/d1");
Files.delete(target);
- 如果目录还有内容,会抛出DirectoryNotEmptyException
19.files–walkfiletree
jdk1.7以前我们只能通过自己写递归来遍历目录,1.7以后出现了Files 我们可以通过Files来遍历目录
看代码
//统计jdk中jar包的个数
private static void m2() throws IOException {
AtomicInteger jarCount = new AtomicInteger();
Files.walkFileTree(Paths.get("D:\\jdk1.8.0_181"), new SimpleFileVisitor<Path>() {
int counts;
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.toString().endsWith(".jar")) {
System.out.println(file);
jarCount.incrementAndGet();
}
return super.visitFile(file, attrs);
}
});
System.out.println("jarCount:" + jarCount.get());
}
//遍历jdk目录并统计jdk的目录和文件数
private static void m1() throws IOException {
AtomicInteger dirCount = new AtomicInteger();
AtomicInteger fileCount = new AtomicInteger();
//23中设计模式,访问者模式
//遍历jdk目录下的所有文件夹和所有文件
Files.walkFileTree(Paths.get("D:\\jdk1.8.0_181"), 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 {
System.out.println(file);
fileCount.incrementAndGet();
return super.visitFile(file, attrs);
}
//从这个文件出来后的方法
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return super.postVisitDirectory(dir, exc);
}
//遍历文件失败时的方法
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return super.visitFileFailed(file, exc);
}
});
System.out.println("dir Count:" + dirCount);
System.out.println("file Count:" + fileCount);
}
20.files–walkfiletree–删除多级目录
上代码
private static void m3() throws IOException {
//删除多级目录
//小提醒用程序删除非常危险因为它不走回收站
Files.walkFileTree(Paths.get("D:\\Snipaste-1.16.2-x64-副本"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("====>进入" + dir);
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("====>遍历" + file);
Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("<====退出" + dir);
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
}
21.files–walk-拷贝多级目录
22.nio–阻塞模式
4.1非阻塞 vs 阻塞
阻塞
- 在沒有数据可读时,包括数据复制过程中,线程必须阻塞等待,不会占用cpu,但线程相当于闲置
- 32位jvm一个线程320k,64位jvm一个线程1024k,为了减少线程数,需要采用线程池技术
- 但即使使用了线程池,如果有很多连接建立,但长时间inactive,会阻塞线程池中所有线程
非阻塞
- 在某个Channel 没有可读事件时,线程不必阻塞,它可以去处理其他有可读事件的Channel
- 数据复制过程中,线程实际还是阻塞的(AIO改进的地方)
- 写数据时,线程只是等待数据写入Channel即可,无需等Channel通过网络把数据发送出去
看个例子:
服务器端
package com.netty.c4;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import static com.netty.c1.ByteBufferUtil.debugRead;
@Slf4j
public class Server {
public static void main(String[] args) throws IOException {
//使用nio来理解阻塞模式
ByteBuffer byteBuffer = ByteBuffer.allocate(36);
//创建了服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
//服务器绑定监听端口8080
ssc.bind(new InetSocketAddress(8080));
//类型判定
List<SocketChannel> clientList = new ArrayList<>();
while (true) {
log.debug("connecting...");
//当有客户端发送消息时,accept阻塞
//accept建立与客户端的连接
//Channel是数据的通道,当和客户端建立了连接以后使用
//SocketChannel与客户端进行通信也就是读写操作
//SocketChannel也就是客户端发送报文后写入到我们服务端的SocketChannel(读缓冲区)
SocketChannel accept = ssc.accept();
log.debug("connectted..." + accept);
//把客户端加入到我们的客户端集合中去
clientList.add(accept);
//读取客户端的消息,读取到我们的ByteBuffer中保存
for (SocketChannel channel : clientList) {
log.debug("before read...");
//一开始ByteBuffer为写模式
int read = channel.read(byteBuffer);
//ByteBuffer切换到读模式
byteBuffer.flip();
//使用工具类debugRead()进行读取
debugRead(byteBuffer);
//读取完以后清空byteBuffer的数据,切换到写模式
byteBuffer.clear();
log.debug("after read...");
}
// //每次读取完以后我们都要清空一下clientList以便于下次读取
// clientList.clear();
}
}
}
客户端:
package com.netty.c4;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
public class Client {
public static void main(String[] args) throws IOException {
//创建客户端
SocketChannel sl = SocketChannel.open();
//绑定端口号
sl.bind(new InetSocketAddress(8080));
System.out.println("waitting...");
}
}
23.nio–阻塞模式-调式1
通过对22代码的调式总结了两点:
阻塞的含义是:
- 1.当没有客户端新连接的时候accept()阻塞
- 当没有读取到数据时read方法阻塞
24.nio–阻塞模式-调式2
再对22进行debug时我们发现,客户端发完一次消息以后,再次发送消息服务器端仍然阻塞,通过对日志的观察可以得到是因为阻塞在accept方法了,因为再次发送消息没有建立新的连接所以accept方法阻塞
25.nio–非组设模式
看个非阻塞模式的例子:
package com.netty.c4;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import static com.netty.c1.ByteBufferUtil.debugRead;
/**
* @author DQF
*/
@Slf4j
public class Server {
public static void main(String[] args) throws IOException {
m2();
return;
}
//非阻塞模式
private static void m2() throws IOException {
//使用nio来理解阻塞模式
ByteBuffer byteBuffer = ByteBuffer.allocate(36);
//创建了服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
//设置非阻塞模式
//设置了非阻塞以后影响了accpet方法
//accept变成了非阻塞了
//非阻塞模式下,如果没有连接建立,但是SocketChannel返回null
ssc.configureBlocking(false);
//服务器绑定监听端口8080
ssc.bind(new InetSocketAddress(8080));
//类型判定
List<SocketChannel> clientList = new ArrayList<>();
while (true) {
log.debug("connecting...");
//当有客户端发送消息时,accept阻塞
//accept建立与客户端的连接
//Channel是数据的通道,当和客户端建立了连接以后使用
//SocketChannel与客户端进行通信也就是读写操作
//SocketChannel也就是客户端发送报文后写入到我们服务端的SocketChannel(读缓冲区)
SocketChannel accept = ssc.accept();
if (accept != null) {
log.debug("connectted..." + accept);
//设置SocketChannel设置为非阻塞
accept.configureBlocking(false);
//把客户端加入到我们的客户端集合中去
clientList.add(accept);
}
//读取客户端的消息,读取到我们的ByteBuffer中保存
for (SocketChannel channel : clientList) {
log.debug("before read...");
//一开始ByteBuffer为写模式
//非阻塞模式下,read没有读到数据返回0
int read = channel.read(byteBuffer);
if(read>0){
//ByteBuffer切换到读模式
byteBuffer.flip();
//使用工具类debugRead()进行读取
debugRead(byteBuffer);
//读取完以后清空byteBuffer的数据,切换到写模式
byteBuffer.clear();
log.debug("after read...");
}
}
// //每次读取完以后我们都要清空一下clientList以便于下次读取
// clientList.clear();
}
}
//阻塞模式
private static void m1() throws IOException {
//使用nio来理解阻塞模式
ByteBuffer byteBuffer = ByteBuffer.allocate(36);
//创建了服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
//服务器绑定监听端口8080
ssc.bind(new InetSocketAddress(8080));
//类型判定
List<SocketChannel> clientList = new ArrayList<>();
while (true) {
log.debug("connecting...");
//当有客户端发送消息时,accept阻塞
//accept建立与客户端的连接
//Channel是数据的通道,当和客户端建立了连接以后使用
//SocketChannel与客户端进行通信也就是读写操作
//SocketChannel也就是客户端发送报文后写入到我们服务端的SocketChannel(读缓冲区)
SocketChannel accept = ssc.accept();
log.debug("connectted..." + accept);
//把客户端加入到我们的客户端集合中去
clientList.add(accept);
//读取客户端的消息,读取到我们的ByteBuffer中保存
for (SocketChannel channel : clientList) {
log.debug("before read...");
//一开始ByteBuffer为写模式
//read是等待读入数据没有数据的话就阻塞
int read = channel.read(byteBuffer);
//ByteBuffer切换到读模式
byteBuffer.flip();
//使用工具类debugRead()进行读取
debugRead(byteBuffer);
//读取完以后清空byteBuffer的数据,切换到写模式
byteBuffer.clear();
log.debug("after read...");
}
// //每次读取完以后我们都要清空一下clientList以便于下次读取
// clientList.clear();
}
}
}
做个总结:
- 1.ServerSocketChannel设置非阻塞影响的是accept()方法,当设置非阻塞后,如果没有新的连接建立
- 2.SocketChannel设置非阻塞影响的是read方法,如果read中没有数据它不会阻塞了会返回0
26.nio–非阻塞模式调式
通过对非阻塞模式的debug可以看出,读取和接收数据相互不会受到影响,阻塞模式下线程比较少,干一件事情的时候就不会去干另一件事情了,比如说accept单线程阻塞,但是虽然非阻塞模式比阻塞模式好,但是线程太过于劳累了,就算我很长时间没有建立连接没有发送数据了,这个线程一直在循环,太过于繁忙了,如果是能发生这么一种效果,有连接事件发送时,我们再去处理,这样会更好,改进的方法就是使用Selector
27.nio-selector-处理accept
看代码:
package com.netty.c4;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@Slf4j
public class TestSelector {
public static void main(String[] args) throws IOException {
//1.创建selector,管理多个Channel
//怎么知道要管理哪些Channel呢?
//就是建立Selector和Channel之间的联系
//就是把Channel注册到Selector
Selector selector = Selector.open();
//接收的ByteBuffer
ByteBuffer by = ByteBuffer.allocate(32);
ServerSocketChannel ssc = ServerSocketChannel.open();
//设置非阻塞模式
ssc.configureBlocking(false);
//把Channel注册到Selector
//selectionkey就是事件发生后,通过它可以知道
//事件和哪个Channel发生的事件.
//这里注册的SelectionKey是管理的ServerSocketChannel
//0表示不关注任何事件
SelectionKey sscKey = ssc.register(selector, 0, null);
//表明selector只关注accpet事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key:{}", sscKey);
//服务器绑定端口
ssc.bind(new InetSocketAddress(8080));
List<SocketChannel> channels = new ArrayList<>();
while (true) {
//这个是解决我们的非阻塞的方法的
//如果没有事件发生我们就让线程阻塞
//有线程发生了才会恢复运行,解决了cpu空转的问题
selector.select();
//处理事件
//拿到所有可用事件集合,内部包含了所有发生的事件.
//我们不能用增强for来遍历,而是用迭代器去遍历
//为什么selecotor.selectedKeys是返回set,使用了
//增强for遍历我们遍历完还要删除会报一个ConcurrentModifyExcception
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
log.debug("key:{}", key);
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
log.debug("{}", sc);
}
}
}
}
事件的类型
- accept:会在有连接请求时触发
- connect:这是客户端发起的,连接一旦建立就会触发connect事件
- read: 可读事件
- write: 可写事件(为什么是可写而不是直接写呢)?
28.nio-selector-cancel
看代码:
package com.netty.c4;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/*
事件的类型
accept:会在有连接请求时触发
connect:这是客户端发起的,连接一旦建立就会触发connect事件
read: 可读事件
write: 可写事件(为什么是可写而不是直接写呢)?
*/
@Slf4j
public class TestSelectors2 {
public static void main(String[] args) throws IOException {
//1.创建selector,管理多个Channel
//怎么知道要管理哪些Channel呢?
//就是建立Selector和Channel之间的联系
//就是把Channel注册到Selector
Selector selector = Selector.open();
//接收的ByteBuffer
ByteBuffer by = ByteBuffer.allocate(32);
ServerSocketChannel ssc = ServerSocketChannel.open();
//设置非阻塞模式
ssc.configureBlocking(false);
//把Channel注册到Selector
//selectionkey就是事件发生后,通过它可以知道
//事件和哪个Channel发生的事件.
//这里注册的SelectionKey是管理的ServerSocketChannel
//0表示不关注任何事件
SelectionKey sscKey = ssc.register(selector, 0, null);
//表明selector只关注accpet事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key:{}", sscKey);
//服务器绑定端口
ssc.bind(new InetSocketAddress(8080));
List<SocketChannel> channels = new ArrayList<>();
while (true) {
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
//取消事件
key.cancel();
}
}
}
}
⚠️ 注意selector.select()这个方法,事件必须处理,如果对事件没有处理,那么下次selector.select();的时候方法不会阻塞,有selector.select();监听的事件发生时,要么处理使用,要么调用cancel()方法进行取消
29.nio-selector-处理read
看代码:我们在客户端建立连接后,发送数据发现服务器出现了⚠️空指针:
package com.netty.c4;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import static com.netty.c1.ByteBufferUtil.debugRead;
/*
事件的类型
accept:会在有连接请求时触发
connect:这是客户端发起的,连接一旦建立就会触发connect事件
read: 可读事件
write: 可写事件(为什么是可写而不是直接写呢)?
*/
@Slf4j
public class TestSelectors2 {
public static void main(String[] args) throws IOException {
//1.创建selector,管理多个Channel
//怎么知道要管理哪些Channel呢?
//就是建立Selector和Channel之间的联系
//就是把Channel注册到Selector
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
//设置非阻塞模式
ssc.configureBlocking(false);
//把Channel注册到Selector
//selectionkey就是事件发生后,通过它可以知道
//事件和哪个Channel发生的事件.
//这里注册的SelectionKey是管理的ServerSocketChannel
//0表示不关注任何事件
SelectionKey sscKey = ssc.register(selector, 0, null);
//表明selector只关注accpet事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key:{}", sscKey);
//服务器绑定端口
ssc.bind(new InetSocketAddress(8080));
while (true) {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
//区分事件类型
if (key.isAcceptable()) {
ServerSocketChannel scl = (ServerSocketChannel) key.channel();
SocketChannel sc = scl.accept();
sc.configureBlocking(false);
SelectionKey scKey = sc.register(selector, 0, null);
scKey.interestOps(SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(16);
channel.read(buf);
buf.flip();
debugRead(buf);
}
}
}
}
}
30.nio-selector–用完key为什么要remove
31.nio-selector–处理客户端断开
还是看我们的代码:
假设客户端异常断开,如下图:这时服务器端会抛出一个异常,服务器端也停止了,这
和我们实际的作用是不一样了,如果客户端断开了,那么服务器端不受影响才是,发生的原因是当客户端异常断开连接时会给服务端发送消息,也就是触发服务器端的read事件,
解决的第一个方案是加上try,catch块来捕捉异常
直接捕获异常(如下图),但是还是有问题的,你还要处理事件,不然selector不会阻塞住
捕获异常加取消事件(取消事件说白了就是把当前的选择键SelectionKey 从selector的集合中移除这样的话selcotr在select()方法时就不会调用当前的SelectionKey,也就不会处理事件了
但是如果我们客户端正常断开的话也会触读事件,没有抛出异常,所以我们还是要处理一下,当正常断开连接时read事件的返回值为-1,如果返回值为-1,我们就直接取消这个事件.
32.nio-selector-消息边界问题
看个例子:当我们的处理读取事件的时候,设置ByteBuffer的长度为4
接着客户端发送一个"hi" 一个"中国"的字符串看一下服务端接收的结果
发送了hi因为是两个字节,byteBuffer是四个字节没有越界所以一次能读取到
发送了中国是三个字节(win10系统默认是utf-8编码,一个汉字被编码了三个字节),由由于byteBuffer一共就四个字节,这就导致了一次不能接收,要读取两次,第一次读取四个字节,第二次读取两个字节,所以说字节数是不完整的,这就导致了两次读取 中? ??
33.nio-selector–处理消息边界
解决方法:
服务器端和客户端约定一个固定长度,比方说125字节,那么服务器端每次接收时都分配125字节就行,而客户端也分配125字节进行传输,缺点,如果客户端实际传输的长度不够125字节,就会浪费空间以及带宽
34.nio-selector–处理消息边界–容量超出
//出现问题,客户端发送数据过大
private static void m3() throws IOException {
//1.创建selector,管理多个Channel
//怎么知道要管理哪些Channel呢?
//就是建立Selector和Channel之间的联系
//就是把Channel注册到Selector
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
//设置非阻塞模式
ssc.configureBlocking(false);
//把Channel注册到Selector
//selectionkey就是事件发生后,通过它可以知道
//事件和哪个Channel发生的事件.
//这里注册的SelectionKey是管理的ServerSocketChannel
//0表示不关注任何事件
SelectionKey sscKey = ssc.register(selector, 0, null);
//表明selector只关注accpet事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
//log.debug("register key:{}", sscKey);
//服务器绑定端口
ssc.bind(new InetSocketAddress(8080));
while (true) {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
try {
//区分事件类型
if (key.isAcceptable()) {
ServerSocketChannel scl = (ServerSocketChannel) key.channel();
SocketChannel sc = scl.accept();
sc.configureBlocking(false);
SelectionKey scKey = sc.register(selector, 0, null);
scKey.interestOps(SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(16);
//这里会抛出一个异常,原因是当clent异常连接后会触发一个读事件.
int read = channel.read(buf);
if (read == -1) {
log.debug("客户端正常断开....");
key.cancel();
} else {
//buf.flip();
//System.out.println(Charset.defaultCharset().decode(buf));
split(buf);
}
}
} catch (Exception e) {
log.debug("客户端异常断开...");
key.cancel();
}
}
}
}
private static void split(ByteBuffer source) {
//读取ByteBuffer我们首先切换到读模式
source.flip();
//怎么接收呢?遇到\n符号就输出呗
//一直遍历到最后还没有遇到\n符号直接把
//这次还没完全读取完的数据进行保留再次读取就行了
//limit 写入时的position移动的最终位置
for (int i = 0; i < source.limit(); i++) {
//首先geti是不会移动position指针
//get是会的,等于'\n'是一个完成的包我们就可以
//进行读取了
if (source.get(i) == '\n') {
//范围是多少呢?
// i-position+1
int size = i - source.position() + 1;
for (int j = 0; j < size; j++) {
System.out.print((char) source.get());
}
System.out.println();
}
}
//读取到最后的时候有可能不会有\n还有剩余的
//把未读取的前移动,并且position指针移动到没有byte的
//后一位
source.compact();
}
35.nio-selector–处理消息边界–附件与扩容
解决34的问题:ByteBuffer扩容
private static void m4() throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
//设置非阻塞模式
ssc.configureBlocking(false);
SelectionKey sscKey = ssc.register(selector, 0, null);
//表明selector只关注accpet事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
//log.debug("register key:{}", sscKey);
//服务器绑定端口
ssc.bind(new InetSocketAddress(8080));
while (true) {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
try {
//区分事件类型
if (key.isAcceptable()) {
ServerSocketChannel scl = (ServerSocketChannel) key.channel();
SocketChannel sc = scl.accept();
sc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(4 << 2);
//将ByteBuffer作为附件关联到我们的ByteBuffer中
//每个时间都会关联一个SelectionKey
SelectionKey scKey = sc.register(selector, 0, buffer);
scKey.interestOps(SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
//这里会抛出一个异常,原因是当clent异常连接后会触发一个读事件.
//获取附件(ByteBuffer)
ByteBuffer buffer = (ByteBuffer) key.attachment();
int read = channel.read(buffer);
if (read == -1) {
log.debug("客户端正常断开....");
key.cancel();
} else {
//buf.flip();
//System.out.println(Charset.defaultCharset().decode(buf));
split(buffer);
if (buffer.position() == buffer.limit()) {
ByteBuffer newByteBuffer = ByteBuffer.allocate(buffer.capacity() << 1);
buffer.flip();
newByteBuffer.put(buffer);
//debugAll(newByteBuffer);
key.attach(newByteBuffer);
}
}
}
} catch (Exception e) {
log.debug("客户端异常断开...");
key.cancel();
}
}
}
}
private static void split(ByteBuffer source) {
//读取ByteBuffer我们首先切换到读模式
source.flip();
//怎么接收呢?遇到\n符号就输出呗
//一直遍历到最后还没有遇到\n符号直接把
//这次还没完全读取完的数据进行保留再次读取就行了
//limit 写入时的position移动的最终位置
for (int i = 0; i < source.limit(); i++) {
//首先geti是不会移动position指针
//get是会的,等于'\n'是一个完成的包我们就可以
//进行读取了
if (source.get(i) == '\n') {
//范围是多少呢?
// i-position+1
int size = i - source.position() + 1;
for (int j = 0; j < size; j++) {
System.out.print((char) source.get());
}
System.out.println();
}
}
//读取到最后的时候有可能不会有\n还有剩余的
//把未读取的前移动,并且position指针移动到没有byte的
//后一位
source.compact();
}
36.nio-selector–bytebuffer大小分配
37.nio-selector–写入内容过多问题
看个例子:
1.客户端: while(true) 循环接收服务器端,发过来的数据
package com.netty.c4;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class WriteClient {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost", 8080));
int count = 0;
//接收循环
while (true) {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
count += sc.read(buffer);
System.out.println(count);
buffer.clear();
}
}
}
2.服务器端:在建立连接后向客户端发送数据
package com.netty.c4;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
public class WriteServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while (true) {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
SocketChannel accept = ssc.accept();
accept.configureBlocking(false);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 30000000; i++) {
sb.append("a");
}
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
while (buffer.hasRemaining()) {
//write 方法不能保证一次性全部字节数
//write()返回值返回实际返回的字节数
int write = accept.write(buffer);
System.out.println(write);
}
}
}
}
}
}
从上图和代码中我们看出服务器端发送数据的时候多次发送并且阻塞在这里,这样在发送的时候,只会处理这一个事件了,从异步变成了同步,所以要重新改正.
38.nio-selector–处理可写事件
public static void m2() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while (true) {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
SocketChannel accept = ssc.accept();
accept.configureBlocking(false);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 30000000; i++) {
sb.append("a");
}
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
int write = accept.write(buffer);
//打印向发送缓冲区写了多少
System.out.println(write);
//3.判断是否有剩余内容
//就是为了避免写入数据的时候一直阻塞在这里
//当发送缓冲区空了可以写内容了我们就可以写内容了
if (buffer.hasRemaining()) {
//4.关注一个可写事件
key.interestOps(SelectionKey.OP_WRITE | key.interestOps());
//把未写完的数据挂载到key中
key.attach(buffer);
}
//发送缓冲区空了就可以写内容了
//key.isWriteable说明发送缓冲区是可写的
//再次写数据
} else if (key.isWritable()) {
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel sc = (SocketChannel) key.channel();
int wirte = sc.write(buffer);
System.out.println(wirte);
//数据写完了把附件清理掉
if (!buffer.hasRemaining()) {
key.attach(null);
//把可写事件也清理掉
key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
}
}
}
}
}
思路:37向发送缓冲区写入大量数据,又使用while进行循环,那么当发送缓冲区满了以后,我们的线程一直还是停留在那,直到buffer中的数据传输完成,再此期间,我们只处理了一个这个写事件,那么当发送缓冲区满了以后,我们还要等待,所以我们使用if语句先判断第一次有没有往发送缓冲区写完,如果没有写完我们再次注册一个写事件,监听写事件,这样即使我们的发送端缓冲区,满了处于阻塞状态时,我们还可以处理其他事件(注意当bufffer用完以后要记得删除,否则导致内存泄漏,同样写事件的SelectKey我们也删除掉即可,因为数据已经写完了)
39.nio–网络编程小结
4.1非阻塞 vs 阻塞
阻塞
-
阻塞模式下,相关方法都会到线程暂停
- ServerSocketChannel.accept 会在没有连接建立时让线程暂停
- SocketChannel.read 会在没有数据可读时让线程暂停
- 阻塞的表现其实就是线程暂停了,暂停期间不会占用cpu,单线程 相当于闲置 -
单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
-
但多线程下,有新的问题,体现在以下方面
- 32位虚拟机一个线程320k,64位jvm 一个线程1024k,如果连接数过多,必然导致OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低
- 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接
非阻塞
- 非阻塞模式下,相关方法都不会让线程暂停
- 在ServerSocketChannel.accept()在没有连接建立时,会返回null,继续执行
- SocketChannel.read 在没有数据可读时,会返回0,但线程不必阻塞,可以去执行其他SocketChannel的read 或是去执行ServeSocketChannel.accept
- 写数据时,线程只是等待数据写入Channel即可,无需等Channel通过网络把数据发送回去 - 但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然不断运行,白白浪费了cpu
- 数据复制过程中,线程实际还是阻塞的(AIO改进的地方)
多路复用
单线程可以配合Selector完成对多个Channel可读写事件的监控,这称之为多路复用
-
多路复用仅针对网络IO、普通文件IO没法利用多路复用
-
如果不用Selector的非阻塞模式,线程大部分事件都在做无用功,而Selector能够保证
-
有可连接事件才去连接
-
有可读事件才去读取
-
有可写事件采取写入
- 限于网络传输能力,Channel未必时时可写,一旦Channel可写,会触发Selector的可写事件
监听Channel事件
可以通过下面三种方法来监听是否有事件发送,方法的返回值代表有多少channel发送了事件
方法1,阻塞直到绑定事件发送
int count = selector.select();
方法2,阻塞直到绑定事件发送,或是超时(事件单位为ms)
int count = selector.select(long timeout);
方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件
int count = selector.selectNow();
select 何时不阻塞
- 事件发生时
- 客户端发起连接请求,会触发accept事件
- 客户端发送数据过来,客户端正常、异常关闭时,都会触发read事件,另外如果发送的数据大于buffer缓冲区,会多次触发读取事件
- channel 可写,会触发write事件
- 在linux下nio bug 发送时
- 调用selecto.wakeup()
- 调用selector.close();
- selcet 所在线程interupt
40.nio–多线程优化–分析
4.6更进一步
现在都是多核cpu,设计时要充分考虑别让cpu的力量被白白浪费
前面的代码只有一个选择器,没有充分利用多核cpu,如何改进呢?
额外:redfis在设计的时候虽然不是Java写的但是也用的是单线程,也就是网络和线程模型这用了类似于NIO,Selector 所以redis有一个缺点就是某一个操作耗时其他操作也会受影响(推荐redis一定要用时间复杂度低的方法,而不能用耗时太长的方法)
分两组选择器:如下图
- 单线程配一个选择器,专门处理accept事件
- 创建cpu核心数的线程,每个线程配一个选择器,轮流处理read事件
41.nio-多线程优化-worker编写
直接上代码:编写workder
package com.netty.c4;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class MultiThreadServer {
public static void main(String[] args) throws IOException {
//boss 线程负责连接事件
Thread.currentThread().setName("boss");
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector boss = Selector.open();
SelectionKey bossKey = ssc.register(boss, 0, null);
bossKey.interestOps(SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while (true) {
boss.select();
Iterator<SelectionKey> iter = boss.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
}
}
}
}
//Worker 负责读写事件
class Worker implements Runnable {
private Thread thread;
private Selector selector;
private String name;
private volatile boolean flag = false;
//初始化线程和Selector
public Worker(Thread thread, Selector selector, String name) {
this.name = name;
}
//初始化线程,和selector
public void register() throws IOException {
if (!flag) {
//每一次都需要创建吗?不是的,是都要一次创建
Thread thread = new Thread(this, name);
thread.start();
Selector readAndWrite = Selector.open();
flag = true;
}
}
@Override
public void run() {
}
}
}
42.nio-多线程优化-worker关联
接着上面的代码:使worker与accept关联
package com.netty.c4;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import static com.netty.c1.ByteBufferUtil.debugRead;
@Slf4j
public class MultiThreadServer {
public static void main(String[] args) throws IOException {
//boss 线程负责连接事件
Thread.currentThread().setName("boss");
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector boss = Selector.open();
SelectionKey bossKey = ssc.register(boss, 0, null);
bossKey.interestOps(SelectionKey.OP_ACCEPT);
//1.创建固定的worker
Worker worker = new Worker("worder-0");
//初始化变量
worker.register();
ssc.bind(new InetSocketAddress(8080));
while (true) {
boss.select();
Iterator<SelectionKey> iter = boss.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//workder不能在这里创建,假设有10个连接
//那么就会有10个workder,这样就是一个连接
//对应一个worker 而我们的cpu是有限的,一个worker
//要对应多个read 或者write 事件而不是一个worker对应一个事件
log.debug("connected..{}", sc.getRemoteAddress());
log.debug("before register...{}", sc.getRemoteAddress());
//关联worer 的selector
sc.register(worker.selector, SelectionKey.OP_READ, null);
log.debug("after register...{}", sc.getRemoteAddress());
}
}
}
}
//Worker 负责读写事件
static class Worker implements Runnable {
private Thread thread;
private Selector selector;
private String name;
private volatile boolean flag = false;
//初始化线程和Selector
public Worker(String name) {
this.name = name;
}
//初始化线程,和selector
public void register() throws IOException {
if (!flag) {
//每一次都需要创建吗?不是的,是都要一次创建
Thread thread = new Thread(this, name);
selector = Selector.open();
thread.start();
flag = true;
}
}
@SneakyThrows
@Override
public void run() {
while (true) {
//处理可读事件
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey sKey = iter.next();
iter.remove();
if (sKey.isReadable()) {
SocketChannel channel = (SocketChannel) sKey.channel();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ, null);
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
log.debug("read...{}", channel.getRemoteAddress());
channel.read(byteBuffer);
byteBuffer.flip();
debugRead(byteBuffer);
} else if (sKey.isWritable()) {
}
}
}
}
}
}
运行服务器/客户端以后,我们发现客户端有写事件,但是worder没有输出(如下图)?原因在43节
43.nio-多线程优化-问题分析
通过Debug我们发现阻塞是发生在 sl.register(worker.selector, SelectionKey.OP_READ, null);这个方法上,是因为先运行的worker对象的selector.select()方法,在这个方法上阻塞住了,没有释放锁,而我们的Boss线程进行向worker中selector注册读事件时用的是同一把锁,导致了,sl.register()阻塞了,解决的核心就是就是在我们worker线程之前注册该可读事件或者让它停止阻塞,注册可读事件,让我们来看一下代码吧
第一个错误的思路是把worker.register()方法放到相邻的sc.regsiter()方法上
看一下打印:发现客户端的写请求,worker.selector能够读取到
⚠️ 但是这种做法是不行的,因为离得进可能是还没有执行worker.run()方法先执行的是我们的sc.register()方法,但是这样有偶然性,第二个就是,我这次的worker.select()方法读取完了,阻塞住了,此时如果有其他的连接请求来了,它还是必然阻塞住的,还是我们在42分析的那样,怎么解决呢?看一下44就知道喽
44.nio-多线程优化-问题解决
我们先来复习一下线程之间的通信有哪几种方式:
- 1.wait 和notify/notify
- 2.使用Lock 和 Condition
- 3.使用阻塞队列/消息队列控制线程通信
- 4.使用管道流进行线程通信(被阻塞队列代替)
我们先使用消息队列异步通信来解决这个问题:
package com.netty.c4;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import static com.netty.c1.ByteBufferUtil.debugRead;
@Slf4j
public class MultiThreadServer2 {
public static void main(String[] args) throws IOException {
Thread.currentThread().setName("Boss");
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
ssc.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey key = ssc.register(selector, 0, null);
key.interestOps(SelectionKey.OP_ACCEPT);
//先使用一个Worker
Worker worker = new Worker();
while (true) {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey sKey = iter.next();
iter.remove();
if (sKey.isAcceptable()) {
ByteBuffer bufffer = ByteBuffer.allocate(16);
SocketChannel sl = ssc.accept();
sl.configureBlocking(false);
log.debug("before register....");
//把sl注册到worker中去
worker.register(sl);
log.debug("after register....");
}
}
}
}
static class Worker implements Runnable {
private String name;
private Selector selector;
private Thread thread;
private final ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue();
private volatile boolean flag = false;
public Worker() {
this.name = "worker-00";
}
//初始化selector 以及 thread
public void register(SocketChannel sc) throws IOException {
if (!flag) {
selector = Selector.open();
thread = new Thread(this, name);
thread.start();
flag = true;
}
queue.add(() -> {
try {
//注册作为消息队列传递过去,异步消息,
sc.register(selector, SelectionKey.OP_READ, null);//boss
} catch (ClosedChannelException e) {
e.printStackTrace();
}
});
//唤醒selector
selector.wakeup();
}
@SneakyThrows
@Override
public void run() {
while (true) {
//真正执行的
this.selector.select();
Runnable target = queue.poll();
if (target != null) {
target.run();
}
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey next = iter.next();
iter.remove();
if (next.isReadable()) {
SocketChannel scl = (SocketChannel) next.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
scl.read(byteBuffer);
byteBuffer.flip();
debugRead(byteBuffer);
}
}
}
}
}
}
45.nio-多线程优化-问题解决wake-up
第二种解决方案是直接在register()中直接加wakeup();
然后再sc.register()
分析:
有三种可能:
第一种可能:
selector.select();先阻塞,然后被唤醒,执行sc.register()能注册成功.
第二种可能:
先wakeup()相当于有了许可证,再selector.select()就不会阻塞,然后,sc.register()仍然可以注册成功
第三种情况:
和第二种情况是一样的
46.nio-多线程优化-多worker
多Worker版本:
package com.netty.c4;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import static com.netty.c1.ByteBufferUtil.debugRead;
@Slf4j
//多个Worker
public class MultiThreadServer3 {
public static void main(String[] args) throws IOException {
Thread.currentThread().setName("Boss");
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
ssc.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey key = ssc.register(selector, 0, null);
key.interestOps(SelectionKey.OP_ACCEPT);
//这个发给发的作用是拿到cpu个数,
//如果工作在docker 容器下,因为容器不是物理隔离的,
//会拿到物理cpu个数,而不是容器申请时的个数
//这个问题直到jdk10才修复,使用jvm参数UseContainerSupport配置,默认开启
Runtime.getRuntime().availableProcessors();
Worker[] workers = new Worker[2];
for (int i = 0; i < workers.length; i++) {
workers[i] = new Worker("worker-" + i);
}
//先使用一个Worker
Worker worker = new Worker();
AtomicInteger index = new AtomicInteger();
while (true) {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey sKey = iter.next();
iter.remove();
if (sKey.isAcceptable()) {
ByteBuffer bufffer = ByteBuffer.allocate(16);
SocketChannel sl = ssc.accept();
sl.configureBlocking(false);
log.debug("before register....");
//把sl注册到worker中去
//round robin 轮询算法
workers[index.getAndIncrement() % workers.length].register(sl);
log.debug("after register....");
}
}
}
}
static class Worker implements Runnable {
private String name;
private Selector selector;
private Thread thread;
private final ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue();
private volatile boolean flag = false;
public Worker(String name) {
this.name = name;
}
public Worker() {
this.name = "worker-00";
}
//初始化selector 以及 thread
public void register(SocketChannel sc) throws IOException {
if (!flag) {
selector = Selector.open();
thread = new Thread(this, name);
thread.start();
flag = true;
}
queue.add(() -> {
try {
//注册作为消息队列传递过去,异步消息,
sc.register(selector, SelectionKey.OP_READ, null);//boss
} catch (ClosedChannelException e) {
e.printStackTrace();
}
});
//唤醒selector,wakeup相当于LockSupport.park() unpark()
selector.wakeup();
sc.register(selector, SelectionKey.OP_READ, null);
}
@SneakyThrows
@Override
public void run() {
while (true) {
//真正执行的
this.selector.select();
/* Runnable target = queue.poll();
if (target != null) {
target.run();
}*/
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey next = iter.next();
iter.remove();
if (next.isReadable()) {
SocketChannel scl = (SocketChannel) next.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
scl.read(byteBuffer);
byteBuffer.flip();
debugRead(byteBuffer);
}
}
}
}
}
}
47.nio-概念剖析-stream vs channel
5.NIO vs BIO
5.1 stream vs channel
- stream 不会自动缓冲数据,channel 会利用系统提供的发送缓冲区、接收缓冲区(更为底层)
- stream 仅支持阻塞API,chanel 同时支持阻塞、非阻塞API,网络channel 可配合selector 实现多路复用
- 二者均为全双工,即读写可以同时进行
48.nio-概念剖析-io-模型-阻塞非阻塞
同步阻塞、同步非阻塞、多路复用、异步阻塞、异步非阻塞
1.阻塞IO
当用户通过调用read方法来读取发过来的数据时,此时发生由用户空间到内核空间的转换,然后内核空间调用read函数,此时可能数据还没有发过来,阻塞用户空间和内核空间等待数据发送过来,当用户发送数据过来的时候,我们要把网卡的数据复制到我们的内存中,然后操作系统由内核态切换为用户态,在数据等待发送的过程中一直用户线程一直都是阻塞的,就是什么事情都干不了
2.非阻塞IO
用户线程调用read 方法后,此时操作系统会从用户态切换为内核态,内核空间调用read方法,此时数据还没有传输过来,直接返回,用户空间不阻塞,系统从内核态切换到用户态,此时用户线程可以执行其他操作,但是用户线程要想读取到发送过来的信息,必然要一直调用read方法,假设再次调用read方法,此时操作系统从用户态切换为内核态,此时内核空间调用read函数,数据发送过来来,还是要把数据从网卡中读取到内存中,这时候,用户线程还是阻塞的,直到读取完毕,操作系统从内核态切换到用户态,这样一次非阻塞IO 就读取数据完毕,⚠️ 非阻塞IO也不一定好,因为它频繁的切换操作系统的状态,由用户态->内核态->用户态… 浪费了时间
49.nio-概念剖析-io模型-多路复用
3.多路复用:
首先当客户端发送数据过来的时候,先使用selector对事件进行监听此时selector会使用户线程阻塞住,系统由用户空间切换到内核空间,此时假如说,客户端发送过来一个请求了,如read请求,操作系统由内核态切换为用户态,然后因为监听到了read事件,所以调用read 方法,此时操作系统由用户态切换到内核态,内核态把数据从网卡中复制到内存中,最后由内核态切换到用户态
通过分析我们可知,在使用多路复用的情况下,用户线程一共阻塞了两次,分别是selector监听事件以及复制数据的时候,而我们的阻塞IO同样也是阻塞了两次,一次read 一次复制数据,多路复用 用户态->内核态转换更频繁为什么还要使用多路复用技术呢?
看一下阻塞模式下更为复杂的模式:
一次性把多个事件处理完毕,不用等待
50.nio-概念剖析-io模型-异步
同步:线程自己去获取结果(一个线程)
异步:线程自己不去获取结果,而是由其他线程送结果(至少两个线程)
阻塞IO:是同步IO
非阻塞IO:是同步IO
多路复用:是同步IO
异步阻塞(没有这种情况)
异步非阻塞(异步只有非阻塞)
51.nio-概念剖析-零拷贝
5.3零拷贝
传统IO问题
传统的IO 将一个文件通过socket写出
File f = new File("D:\\hello.txt");
RandomAccessFile file = new RandomAccessFile(f, "r");
byte[] bytes = new byte[(int) f.length()];
file.read(bytes);
Socket socket = new Socket();
socket.getOutputStream().write(bytes);
内容工作流程是这样的:
1.java本身并不具备IO读写能力,因此read 方法调用后,要从java程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入到内核缓冲区,这期间用户线程阻塞,读到内核缓存区以后再复制到用户缓存区(byte数组),至此read方法完成,这时候由操作系统的内核态再次切换到操作系统的用户态
2.write方法再由用户态切换到操作系统的内核态,先把我们的byte数组写到我们的Socket缓存区,最后由socket缓冲区数据再复制一次写到网卡发送
3.这期间数据复制了四次,内核态和用户态的切换了三次
可以看到中间环节较多,java的IO实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的
- 用户态与内核态的切换发送了三次,这个操作比较重量级
- 数据拷贝了4次
NIO优化
通过DirectByteBuffer
- ByteBuffer.allocate(10) HeapByteBuffer 使用的还是Java内存
- ByteBuffer.allocateDirect(10) DirectByteBuffer使用的是操作系统内存
大部分步骤与优化前相同,不再赘述。唯有一点:java可以使用DirectByteBuffer将堆外内存映射到jvm内存直接方法
这样read方法在复制时只复制了三次,少了一次写回到用户缓冲区
- 这块内存不受jvm垃圾回收的影响,因此内存地址固定,有助于IO读写
- java中的DirectByteBuffer对象仅维护了此内存的虚引用,内存回收分为两步
- DirectByteBuf对象被垃圾回收,将虚引用加入引用队列
- 通过专门线程访问引用队列,根据虚引用释放堆外内存
进一步优化(底层采用了linux 2.1 后提供的sendFile方法),java中对应着两次channel调用transferTo/transferFrom 方法拷贝数据``
1.java 调用transferTo 方法后,要从java 程序的用户态,切换至内核态,,使用DMA将数据读入内核缓冲区,不会使用cpu
2.数据从内核缓冲区直接传输到socket 缓冲区,cpu会参与拷贝
3.最后使用DMA将socket 缓冲区的数据写入网卡,不会使用cpu
可以看到:
- 只发生了一次用户态与内核态的切换
- 数据拷贝了3次
进一步优化(linux2.4)
1.java调用transferTo 方法后,要从java程序的用户态切换至内核态,使用DMA将数据读入内核缓冲区,不会使用cpu
2.只会将一些offset 和length 信息拷贝到socket缓冲区,几乎无消耗
3.使用DMA将内核缓冲区的数据写入网卡,不会使用cpu
整个过程仅发生了一次用户态与内核态的切换,数据拷贝了2次。所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到jvm内存中,零拷贝的优点:
- 更少的用户态与内核态的切换
- 不利用cpu计算,减少cpu缓冲伪共享
- 零拷贝适合小文件传输不适合大文件的传输,怎么理解呢?如果你内容比较大,大量的数据读入到内核缓冲区,缓冲区是要反复的获取数据,零拷贝是从缓冲区直接发送到网卡,这个文件从头到尾也就使用了一次.读取了一次,没有充分发挥缓冲区缓冲的效果,反而由于你的文件较大,你把内核缓冲区的内容都占了反而影响了其他的读取
52.nio-概念剖析-io模式-异步例子
5.3AIO
AIO 用来解决数据复制阶段的阻塞问题
- 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置
- 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方法由另外的线程获取结果
- Windows 系统通过IOCP实现了真正的异步IO
- Linux 系统异步IO在2.6版本引入,但其底层还是使用了多路复用模拟了异步IO,性能没有优势
看代码:
package com.netty.c4;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Paths;
import static com.netty.c1.ByteBufferUtil.debugAll;
@Slf4j
public class AioFileChannel {
public static void main(String[] args) throws IOException, InterruptedException {
AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("D:\\hello.txt"));
/*
参数1: ByteBuffer
参数2:读取的起始位置
参数3:附件
参数4:回调方法
*/
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
//守护线程
channel.read(byteBuffer, 0, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override//read 成功
public void completed(Integer result, ByteBuffer attachment) {
log.debug("read completed...");
attachment.flip();
debugAll(byteBuffer);
}
@Override //read 失败
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
log.debug("read end...");
//我们的异步回调的线程是Daemon Threads
Thread.sleep(10000000L);
}
}
第二章 Netty入门
53.netty-入门-概述
1.1 Netty 是什么?
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high per fromance protocol servers & clients
Netty 是一个异步的,基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端
1.2 Netty的作者
54.netty入门-hello-server
我们先来编写服务端,使用Netty:
package com.netty.netty1;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
public class HelloServer {
public static void main(String[] args) {
//1.服务器端的启动器,负责组装netty组件,
//启动服务器
new ServerBootstrap()
//一个selector + thread =EventLoop
//添加组件 BoosEventLoop,WorkerEventLoop(selector,thread)
.group(new NioEventLoopGroup())
//OIO(BIO) 选择服务器端的ServerSocket实现
.channel(NioServerSocketChannel.class)
//4.BOSS处理连接, worker(child)负责处理读写
//你要告诉作为worker的EventLoop 应该做哪些事情
//职责:决定了worker(child)能执行哪些操作
//这些操作统一叫Handler
.childHandler(
//5.代表和客户端进行数据读写的通道Initializer 初始化,
//负责添加别的Handler
new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//解码,将传输过来的ByteBuf转换乘子符串
ch.pipeline().addLast(new StringDecoder());
//我们的业务处理代码
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
}
});
}
});
}
}
55.netty入门-hello-client
编写客户端的代码:
package com.netty.netty1;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
//1.启动类
new Bootstrap()
//2.添加EventLoop
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
//初始化器会在连接建立后被调用,
//调用以后会执行initChannel这个方法
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
}).connect(new InetSocketAddress("localhost", 8080))
.sync()
.channel()
//像服务端发送数据
.writeAndFlush("hello,world");
}
}
56.netty入门-hello-流程分析
分析一下流程:
57.netty入门-hello-正确观念
58.netty入门-eventloop概述
3.组件
3.1 EVentLoop
事件循环对象
EventLoop 本质是一个单线程执行器(同时维护了一个Selector),里面有run方法处理Channel 上源源不断的IO事件。
它的继承关系比较复杂
- 一条线是继承自juc.ScheduledExecutorService 因此包含了线程池中所有的方法
- 另一条是继承自netty自己的OrderedEventExceutor.
- 提供了boolen inEventLoop(Thread thread) 方法判断一个线程是否属于此EventLoop
- 提供了parent方法来看看自己属于哪个EventLoopGroup
事件循环组
EventLoopGroup是一组EventLoop,Channel一般调用EventLoopGroup 的register方法来绑定其中的一个EventLoop,后序这个Channel上的IO事件都用此EventLoop来处理(保证了io事件处理时的线程安全)
59.netty 入门eventloop-普通-定时任务
package com.netty.netty1;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.TestEventLoop")
public class TestEventLoop {
public static void main(String[] args) {
//1.创建事件循环组
NioEventLoopGroup group = new NioEventLoopGroup(2);//io 事件,普通任务,定时任务
// DefaultEventLoopGroup group2 = new DefaultEventLoopGroup();//普通任务,定时任务
//2.获取下一个循环对象
System.out.println(group.next());
System.out.println(group.next());
System.out.println(group.next());
// 3. 执行普通任务
/**
* 执行普通任务的意义:
* 1.可以做一个异步处理,某些耗时的工作不想做出,交给事件循环的线程去处理
* 2.在做一些事件分发的时候,任务的执行权从一额线程到一下线程
*/
group.next().submit(() -> {
log.info("ok");
});
//4.执行定时任务,用户做keeAlive的时候
group.next().scheduleAtFixedRate(() -> {
log.info("ok");
}, 0, 1, TimeUnit.SECONDS);
}
}
60.netty入门eventloop-io任务
客户端:
package com.netty.netty1;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
public class EventLoopClient {
public static void main(String[] args) throws InterruptedException {
//1.启动类
Channel channel = new Bootstrap()
//2.添加EventLoop
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
//初始化器会在连接建立后被调用,
//调用以后会执行initChannel这个方法
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
}).connect(new InetSocketAddress("localhost", 8080))
.sync()
.channel();
System.out.println("");
}
}
服务器端:
package com.netty.netty1;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
//处理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 ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override //ByteBuf
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
log.info(buffer.toString(Charset.defaultCharset()));
}
});
}
}).bind(8080);
}
}
61 eventLoop-分工细化
Boss 处理Accept时间,Worker处理读写事件
package com.netty.netty1;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.Charset;
//处理io事件
@Slf4j
public class EventLoopServer1 {
public static void main(String[] args) {
new ServerBootstrap()
//boss 和 worker
//boss只负责accept事件(ServerSocketChannel),worker 只负责socketChannel上的读写
//因为ServerSocketChannel只有一个需不要要把线程数设置为1呢?
//不需要,因为在注册事件的时候,ServerSocketChannel只有一个他只会找到
//EventGroup组中的一个进行绑定
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("handler1", new ChannelInboundHandlerAdapter() {
@Override //ByteBuf
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
log.info(buffer.toString(Charset.defaultCharset()));
//将消息传递给下一个Handler
ctx.fireChannelRead(msg);
}
});
}
}).bind(8080);
}
}
62 eventLoop-分工细化2
如果Channel 的Handler执行时间比较长,会影响其他Channel的执行,因为selector绑定了多个Channel
package com.netty.netty1;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.Charset;
//处理io事件
@Slf4j
public class EventLoopServer {
public static void main(String[] args) {
//只能执行普通任务和定时任务
EventLoopGroup group = new DefaultEventLoopGroup();
new ServerBootstrap()
//boss 和 worker
//boss只负责accept事件(ServerSocketChannel),worker 只负责socketChannel上的读写
//因为ServerSocketChannel只有一个需不要要把线程数设置为1呢?
//不需要,因为在注册事件的时候,ServerSocketChannel只有一个他只会找到
//EventGroup组中的一个进行绑定
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("handler1", new ChannelInboundHandlerAdapter() {
@Override //ByteBuf
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
log.info(buffer.toString(Charset.defaultCharset()));
//将消息传递给下一个Handler,使用ctx.fireChannelRead();
ctx.fireChannelRead(msg);
}
}).addLast(group, "handler2", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
log.info(buffer.toString(Charset.defaultCharset()));
//将消息传递给下一个Handler
ctx.fireChannelRead(msg);
}
});
}
}).bind(8080);
}
}
从控制台打印的情况我们可以看到,使用了其他线程来处理Handler
63 eventLoop-切换线程
回看刚才的例子,如果多个Handler之前使用的是不同的eventLoop那么我们是怎么切换的
64 channel
channel 的主要作用
- close()可以用来关闭channel
- closeFuture()用来处理channel 的关闭
- sync方法作用是同步等待channel关闭
- 而addListener方法是异步等待channel关闭
- pipeline()方法添加处理器
- write()方法将数据写入
- writeAndFlush()方法将数据写入并刷出
65 channelFuture-连接问题
如果我们客户端不加sync:
package com.netty.netty1;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
@Slf4j
public class EventLoopClient2 {
public static void main(String[] args) {
ChannelFuture connect = new Bootstrap()
.group(new NioEventLoopGroup())
//NioSocketChannel.class
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
}).connect(new InetSocketAddress("localhost", 8080));
Channel channel = connect.channel();
channel.writeAndFlush("1");
}
}
最后发送数据的时候发送不过去.
原因是因为:connect方法是个异步非阻塞方法,也就是执行这个方法后,直接返回,交给其他线程去处理连接,然后方法回调,加了sync以后,等到建立了连接才会停止阻塞,没加的话直接获取channel根本不是建立好连接的Channel对象,所以你发送数据根本没有向服务器发送
66 channelFuture-处理结果
channelFuture 的作用就是用来异步处理结果的,有两种方式:
- 1.使用sync同步进行阻塞,当建立连接后停止阻塞
- 2.使用addLister()方法,会等连接建立后,异步获取结果
package com.netty.netty1;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
@Slf4j
public class EventLoopClient2 {
public static void main(String[] args) throws InterruptedException {
log.info("1234");
//只要带future promise 都是跟异步方法一块使用的
ChannelFuture connect = new Bootstrap()
.group(new NioEventLoopGroup())
//NioSocketChannel.class
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
}).connect(new InetSocketAddress("localhost", 8080));
//2.1 使用sync 方法同步处理结果
//connect.sync(); //阻塞住,等连接建立好了就停止阻塞
//2.2 使用addListner 方法异步处理结果
//等结果的也就是channel 不是main线程
connect.addListener(new ChannelFutureListener() {
@Override
//在nio 线程连接建立好之后,会调用 operationComplete
public void operationComplete(ChannelFuture future) throws Exception {
Channel channel = future.channel();
log.info("{}", channel);
channel.writeAndFlush("hello,world");
}
});
}
}
67 channelFuture-关闭问题
客户端接收用户的输入把客户端接收到的信息源源不断地发送给服务器端(当不想发送信息了,输入一个q表示退出):
package com.netty.netty1;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.Scanner;
@Slf4j
public class closeFutureClient {
public static void main(String[] args) throws InterruptedException {
ChannelFuture channelFuture = new Bootstrap()
.channel(NioSocketChannel.class)
.group(new NioEventLoopGroup())
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
}).connect(new InetSocketAddress("localhost", 8080));
channelFuture.sync();
Channel ch = channelFuture.channel();
Scanner scanner = new Scanner(System.in);
new Thread(() -> {
while (true) {
String line = scanner.nextLine();
if ("q".equals(line)) {
ch.close();
//2.把事情写到close之后,也不行,close也是异步操作,可能close还没有真正关闭
//执行下面善后的事情了
break;
}
ch.writeAndFlush(line);
}
}, "input").start();
//1.处理关闭后的事情,把事情写到这:不行因为是异步操作
log.info("12345");
}
}
68 channelFuture-处理关闭
- 1.使用closeFuture.sync();使main线程阻塞,同步关闭
- 2.使用closeFuture.addLister(future->{处理关闭})异步处理
package com.netty.netty1;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.Scanner;
@Slf4j
public class closeFutureClient {
public static void main(String[] args) throws InterruptedException {
ChannelFuture channelFuture = new Bootstrap()
.channel(NioSocketChannel.class)
.group(new NioEventLoopGroup())
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//这个Handler是帮助我们调式的,执行流程
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new StringEncoder());
}
}).connect(new InetSocketAddress("localhost", 8080));
channelFuture.sync();
Channel ch = channelFuture.channel();
log.info("{}", ch);
Scanner scanner = new Scanner(System.in);
new Thread(() -> {
while (true) {
String line = scanner.nextLine();
if ("q".equals(line)) {
ch.close();
//2.把事情写到close之后,也不行,close也是异步操作,可能close还没有真正关闭
//执行下面善后的事情了
break;
}
ch.writeAndFlush(line);
}
}, "input").start();
// 获取CloseFuture 对象,1)同步处理关闭. 2)异步处理关闭
ChannelFuture closeFuture = ch.closeFuture();
log.info("waiting close...");
//让主线程阻塞住
closeFuture.sync();
log.info("处理关闭的操作");
// 2.异步处理关闭的操作,也就是某个线程在处理完Channel.close()之后,再添加一个这个方法
closeFuture.addListener((ChannelFutureListener) future -> log.info("处理关闭之后的操作"));
}
}
69 channelFuture-处理关闭2
当我们的输出结束以后,channel关了,自己创建的线程也关了但是客户端的java进程没有结束是因为group中的某些线程没有关闭
package com.netty.netty1;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.Scanner;
@Slf4j
public class closeFutureClient {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
ChannelFuture channelFuture = new Bootstrap()
.channel(NioSocketChannel.class)
.group(group)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//这个Handler是帮助我们调式的,执行流程
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new StringEncoder());
}
}).connect(new InetSocketAddress("localhost", 8080));
channelFuture.sync();
Channel ch = channelFuture.channel();
log.info("{}", ch);
Scanner scanner = new Scanner(System.in);
new Thread(() -> {
while (true) {
String line = scanner.nextLine();
if ("q".equals(line)) {
ch.close();
//2.把事情写到close之后,也不行,close也是异步操作,可能close还没有真正关闭
//执行下面善后的事情了
break;
}
ch.writeAndFlush(line);
}
}, "input").start();
// 获取CloseFuture 对象,1)同步处理关闭. 2)异步处理关闭
ChannelFuture closeFuture = ch.closeFuture();
log.info("waiting close...");
/* //让主线程阻塞住
closeFuture.sync();
log.info("处理关闭的操作");*/
// 2.异步处理关闭的操作,也就是某个线程在处理完Channel.close()之后,再添加一个这个方法
closeFuture.addListener((ChannelFutureListener) future -> {
log.info("处理关闭之后的操作");
//拒绝接受新的任务,把没有发的数据发送完以后就停止
group.shutdownGracefully();
});
}
}
70 为什么要异步
Netty为什么使用异步的方式去这样做,比如说建立连接时,建立连接的是其他线程。在一个线程里做连接不更简单嘛?
到底Netty使用异步提升的是什么?看个例子:
要点:
- 单线程没法异步提升效率,必须配合多线程、多核cpu才能发挥异步的优势
- 异步并没有缩短响时间,反而有所增加
- 合理进行任务拆分,也是利用异步的关键
- 提高了单位时间内处理请求的个数也就是吞吐量
71 future-promise-概述
在异步处理时,经常用到这两个接口
首先要说明netty中的Future 与jdk中的Future同名,但是两个接口,netty的Future 继承自jdk的Future,而Promise 又对netty Future 进行了扩展
- jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果
- netty Future 可以同步等待结束得到结果,也可以异步方式得到结果,但都是要等任务结束
- netty Promise 不仅有netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
72 jdk-future
package com.netty.netty1;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
/**
* @author DQF
*/
@Slf4j
public class TestJdkFuture {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(2);
Future<Integer> result = service.submit(() -> {
log.info("等待结果...");
return 1;
});
log.info("结果是:{}", result.get());
}
}
73 netty-future
package com.netty.netty1;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.EventLoop;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@Slf4j
public class TestNettyFuture {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//EventLoop中就有一个线程
NioEventLoopGroup group = new NioEventLoopGroup();
EventLoop eventLoop = group.next();
Future<Integer> submit = eventLoop.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.info("执行结果..");
Thread.sleep(1000);
return 70;
}
});
/*
1.同步等待结果,阻塞
log.info("等待结果");
log.info("结果是{}", submit.get());*/
//2.异步获取结果,回调方法
submit.addListener(new GenericFutureListener<Future<? super Integer>>() {
@Override
public void operationComplete(Future<? super Integer> future) throws Exception {
log.info("结果信息:{}", future.getNow());
log.info("结果信息:{}", future.get());
}
});
}
}
74 netty-promise
前面看了两个future都不是我们创建的,future的创建权和结果都不是我们设置的,对此我们使用promise来进行操作
package com.netty.netty1;
import io.netty.channel.EventLoop;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.DefaultPromise;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutionException;
@Slf4j
public class TestNettyPromise {
public static void main(String[] args) throws ExecutionException, InterruptedException {
EventLoop eventLoop = new NioEventLoopGroup().next();
//2.可以主动创建promise,结果容器
DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);
new Thread(() -> {
//3.任意一个线程执行计算,计算完毕后向promise 填充结果
log.info("开始计算...");
try {
int test = 1 / 0;
Thread.sleep(1000);
//promise 可以填充结果并且promise是由我们主动创建的
promise.setSuccess(80);
} catch (Exception e) {
//e.printStackTrace();
promise.setFailure(e);
System.out.println("出错啦...");
}
}).start();
//4.接收结果的线程
log.info("等待结果...");
log.info("结果是:{}", promise.get());
}
}
75 pipeline
ChannelHandler 用来处理Channel 上的各种事件,分为入站、出站两种。所有ChannelHandler被连成一串,就是Pipeline
- 入站处理器通常是ChannelInboundHandlerAdapter的子类,主要用来读取客户端数据,写回结果
- 出站处理器通常是ChannelOutboundHandlerAdapter的子类,主要对写回结果进行加工
打个比喻,每个Channel是一个产品的加工车间,Pipeline 是车间中的流水线,ChannelHandler 就是流水线上的各道工序,而后面要讲的ByteBuf是原材料,经过很多工序的加工,先经过一道道入站工序,再经过一道道出站工序最终变成产品
76 inbound-handler
入站处理器:
package com.netty.netty1;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.Charset;
@Slf4j
public class TestPipeline {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//1.通过 channel 拿到pipeline
ChannelPipeline pipeline = ch.pipeline();
//添加处理器 head-> ->tail 加在head的后面,tail的前面
pipeline.addLast("h1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("1");
ByteBuf buf = (ByteBuf) msg;
String name = buf.toString(Charset.defaultCharset());
super.channelRead(ctx, name);
}
});
pipeline.addLast("h2", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object name) throws Exception {
log.info("2");
Student student = new Student(name.toString());
//将数据传给下一个Handler
ctx.fireChannelRead(student);
}
});
pipeline.addLast("h3", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("3,结果{},class:{}", msg, msg.getClass());
super.channelRead(ctx, msg);
ch.writeAndFlush(ctx.alloc().buffer().writeBytes("server...".getBytes()));
}
});
pipeline.addLast("h4", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
//出栈是从后往前出站
log.info("4");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("h5", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.info("5");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("h6", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.info("h6");
super.write(ctx, msg, promise);
}
});
}
})
.bind(8080);
}
@Data
@AllArgsConstructor
@ToString
static class Student {
private String name;
}
}
77 outbound-handler
出站处理器:
package com.netty.netty1;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.Charset;
@Slf4j
public class TestPipeline {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//1.通过 channel 拿到pipeline
ChannelPipeline pipeline = ch.pipeline();
//添加处理器 head-> ->tail 加在head的后面,tail的前面
pipeline.addLast("h1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("1");
ByteBuf buf = (ByteBuf) msg;
String name = buf.toString(Charset.defaultCharset());
super.channelRead(ctx, name);
}
});
pipeline.addLast("h2", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object name) throws Exception {
log.info("2");
Student student = new Student(name.toString());
//将数据传给下一个Handler
ctx.fireChannelRead(student);
}
});
pipeline.addLast("h3", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("3,结果{},class:{}", msg, msg.getClass());
super.channelRead(ctx, msg);
//todo: 由ch换成 ctx
//ch.writeAndFlush(ctx.alloc().buffer().writeBytes("server...".getBytes()));
//ctx.writeAndFlush从当前由后向前找处理器.
ctx.writeAndFlush(ctx.alloc().buffer().writeBytes("server...".getBytes()));
}
});
pipeline.addLast("h4", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
//出栈是从后往前出站
log.info("4");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("h5", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.info("5");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("h6", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.info("h6");
super.write(ctx, msg, promise);
}
});
}
})
.bind(8080);
}
@Data
@AllArgsConstructor
@ToString
static class Student {
private String name;
}
}
78 embedded-channel
原来测试的时候都是要提供客户端和服务器端,Netty提供了测试的EmbeddedChannel
package com.netty.netty1;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.channel.embedded.EmbeddedChannel;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TestEmbeddedChannel {
public static void main(String[] args) {
ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("1");
super.channelRead(ctx, msg);
}
};
ChannelInboundHandlerAdapter h2 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("2");
super.channelRead(ctx, msg);
}
};
ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.info("3");
super.write(ctx, msg, promise);
}
};
ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.info("4");
super.write(ctx, msg, promise);
}
};
//创建channel
EmbeddedChannel channel = new EmbeddedChannel(h1, h2, h3, h4);
//模拟入站操作
channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello".getBytes()));
//模拟出站操作
channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("world".getBytes()));
}
}
79 bytebuf-创建
bytebuf的创建:
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
System.out.println(buf);
//字符串拼接
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 32; i++) {
sb.append("a");
}
buf.writeBytes(sb.toString().getBytes());
//System.out.println(buf);
log(buf);
log调式工具类:
//调式的工具方法
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());
}
80 bytebuf-是否池化
81 bytebuf-组成
ByteBuf由四个变量构成:分别是:
- 1.读指针:读取时读指针向右移动到写指针为止
- 2.写指针:写的时候写指针向右移动达到容量时进行扩容
- 3.容量:初始默认容量,能够扩容的部分就是容量和最大容量的区间
- 4.最大容量:一般比我们物理容量要大
ByteBuf由四部分组成:分别是:
- 1.废弃部分
- 2.读部分
- 3.写部分
- 4.扩容部分
82 bytebuf-写入
注意:
- 这些方法的未指明返回值的,其返回值都是ByteBuf,意味着可以链式调用
- 网络传输,默认习惯是Big Endian
先写入4个字节
buffer.writeBytes(new byte[]{1,2,3,4});
log(buffer);
结果是:
再写入int 型:
buffer.writeInt(5);
log(buffer);
结果是:
还有一类方法是set 开头的一系列方法,也可以写入数据,但不会改变写指针的位置
扩容
再写入一个int 整数时,容量不够了(初始容量是10),这时会引发扩容
buffer.writeInt(6);
log(buffer);
结果是:
扩容规则是:
- 如何写入后数据大小未超过512,则选择下一个16的整数倍,例如写入大小后大小为12,则扩容后capacity是16
- 如果写入后数据大小超过512,则选择下一个2n,例如写入后大小为513,则扩容后capacity是102=1024(2^9=512 已经不够了)
- 扩容不能超过max capacity 会报错
83 bytebuf-获取
7)读取
例如读了4次,每次一个字节
System.out.println(buffer.readByte());
System.out.println(buffer.readByte());
System.out.println(buffer.readByte());
System.out.println(buffer.readByte());
log(buffer);
读过的内容,就属于废弃部分了,再读只能读那些尚未读取的部分
84 bytebuf-内存释放
85 bytebuf-头尾释放
看一下尾部释放的源码:
头部释放/出站源代码:
86 bytebuf-零拷贝-slice
package com.netty.netty1;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import static com.netty.netty1.TestByteBuf.log;
/**
* @author DQF
*/
public class TestSlice {
public static void main(String[] args) {
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
buf.writeBytes(new byte[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'});
log(buf);
ByteBuf slice = buf.slice(0, 5);
ByteBuf slice1 = buf.slice(5, 5);
log(slice);
log(slice1);
slice.setByte(0, 'z');
log(slice);
log(buf);
}
}
打印结果:
87 bytebuf-零拷贝-slice2
slice切片要注意的点:
1.切片后会自动限制切片的长度(如果再向里面添加东西,就会报异常
2.在使用切片后我们要自己retain使计数器+1,原始的不用了要release 如果只是原始的release的话,也就是原始的不使了。切片要再使用的话要报异常因为他们实际用的一块内存
88 bytebuf-零拷贝-composite
把小的ByteBuf 组合成大的ByteBuf
1.使用ByteBuf原始组合:优点简单,缺点了内存复制太多效率低
2.使用CompositeByteBuf
好处:避免了内存的复制,坏处:带来了更复杂的维护
89 bytebuf-小结
最后一种零拷贝是个工具类:
🌟 ByteBuf 优势
- 池化,可以重用池中ByteBuf实例,更节约内存,减少内存溢出的可能
- 读写指针分离,不需要像ByteBuffer 一样切换读写模式
- 可以自动扩容
- 支持链式调用,使用更流畅
- 很多地方体现零拷贝,例如slice、duplicate、CompositeByteBuf
90 思考问题
写一个简单的Demo:客户端发送什么服务端服务器端就向客户端响应什么
echo server
服务器端代码:
package com.netty.netty1;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.Charset;
@Slf4j
public class TestDemoServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = msg instanceof ByteBuf ? (ByteBuf) msg : null;
if (buf != null) {
System.out.println(((ByteBuf) msg).toString(Charset.defaultCharset()));
ctx.writeAndFlush(buf);
} else {
log.info("出错了....");
}
}
});
}
}).bind(8080);
}
}
客户端端代码块:
package com.netty.netty1;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.Scanner;
@Slf4j(topic = "TestDemoClient")
public class TestDemoClient {
public static void main(String[] args) throws InterruptedException {
Channel sending = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffers = ctx.alloc().buffer();
ByteBuf buffer = (ByteBuf) msg;
System.out.println(((ByteBuf) msg).toString(Charset.defaultCharset()));
}
});
}
}).connect(new InetSocketAddress("localhost", 8080))
.sync().channel();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String s = scanner.nextLine();
sending.writeAndFlush(s);
}
}
}
1.思考问题:ByteBuf是否需要手动释放?
不需要:当我们读取数据时,因为你传递的就是ByteBuf 它会一直传递到tail节点进行释放
2.注意点:
3 读和写的误解
我最初在认识上有这样的误区,认为只有在netty,nio这样的多路复用IO模型时,读写才不会相互阻塞,才可以实现高效的双向通信,但实际上,Java Socket 是全双工的;在任意时刻,线路上存在A 到B 和 B 到 A的双向信号传输,即使是阻塞IO,读和写是可以同时进行的,只要分别采用读线程和写线程即可,读不会阻塞写,写也不会阻塞读
第三章 Netty 进阶
91 黏包半包-现象演示
先看个例子:
1.客户端代码:每次发16个字节发送10次
package com.netty.netty2;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import java.util.logging.Logger;
@Slf4j
public class HelloWorldClient {
//static final Logger log = (Logger) 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<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//在连接建立好以后会触发channelActive事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
ByteBuf buf = ctx.alloc().buffer(16);
buf.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16});
ctx.writeAndFlush(buf);
}
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
channelFuture.channel().close().sync();
} catch (Throwable e) {
log.error("client error", e);
} finally {
worker.shutdownGracefully();
}
}
}
2.服务器端代码:
package com.netty.netty2;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.nio.channels.ServerSocketChannel;
import static com.netty.netty1.TestByteBuf.log;
@Slf4j
public class HelloWroldServer {
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<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
黏包现象一次把160B字节全都接收:
半包现象:
92 黏包半包-滑动窗口
怎么样解决这个问题呢?TCP协议中使用了滑动窗口
93 黏包半包-分析
黏包
- 现象,发送aabc def,接收abcdef
- 原因
- 应用层:接收方ByteBuf设置太大(Netty 默认1024)
- 滑动窗口:假设发送方256bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这256bytes字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会黏包
- Nagle算法:会造成黏包(我们在发送数据的时候会在网络层和传输层加上报头信息,假设你要发送1byte 你的报头信息总共有40byte 那么你发送的数据远远没有你的报头信息多,为了造成这种发送信息的浪费和提高效率,tcp采用Nagle算法来尽可能使数据多的时候再发送数据
半包:
- 现象,发送abcdef,接收 abc def
- 原因:
- 应用层:接收方ByteBuf 小于实际发送数据量
- 滑动窗口:假设接收方的窗口只剩了128bytes,发送方的报文信息大小为256bytes,这时放不下了,只能先发送前128bytes,等待ack后才能发送剩余部分,这就造成了半包
- MSS限制:不同的网卡/网络设备对数据包的大小是有限制的,当发送的数据超过MSS 限制后,会将数据切分发送,就会造成半包
94 黏包半包-解决-短连接
采用短连接方式也就是我客户端发送了一条消息后就断开连接但是短连接不能避免半包
package com.netty.netty2;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import java.util.logging.Logger;
@Slf4j
public class HelloWorldClient {
//static final Logger log = (Logger) LoggerFactory.getLogger(HelloWorldClient.class);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
sned1();
}
System.out.println("finsh...");
}
private static void sned1() {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//在连接建立好以后会触发channelActive事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
ByteBuf buf = ctx.alloc().buffer(16);
buf.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16});
ctx.writeAndFlush(buf);
//1.短链接方式也就是客户端发送了一次消息后就断开连接了
//为了channel close 后能关闭其他线程我们使用了
//try{}finlly块语句再finally块语句中关闭NioEventLoopGroup的线程
ctx.channel().close();
}
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
channelFuture.channel().close().sync();
} catch (Throwable e) {
log.error("client error", e);
} finally {
worker.shutdownGracefully();
}
}
}
package com.netty.netty2;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.nio.channels.ServerSocketChannel;
import static com.netty.netty1.TestByteBuf.log;
@Slf4j
public class HelloWroldServer {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
//设置接收缓存区为10个字节,如果我们不设置滑动窗口是自适应的
//erverBootstrap.option(ChannelOption.SO_RCVBUF,10);
serverBootstrap.group(boss, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
95 黏包半包-解决-定长解码器
2.使用定长解密器FixedLengthFrameDecoder 也就是我不管客户端发送多少个字节,服务端统一定长解密,假设为10,就不会出现黏包半包问题,可是这样要发送两个字节的消息其他8个字节还要用特殊符号表示浪费资源
客户端代码
package com.netty.netty2;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import java.net.InetSocketAddress;
import java.util.Random;
@Slf4j
public class HelloWorldClient2 {
static NioEventLoopGroup worker = new NioEventLoopGroup();
public static void main(String[] args) throws InterruptedException {
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buffer = ctx.alloc().buffer();
char c = '0';
Random r = new Random();
for (int i = 0; i < 10; i++) {
byte[] bytes = fill10Bytes(c, r.nextInt(10) + 1);
c++;
buffer.writeBytes(bytes);
}
ctx.writeAndFlush(buffer);
}
});
}
});
ChannelFuture localhost = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync();
ChannelFuture close = localhost.channel().close().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
public static byte[] fill10Bytes(char res, int length) {
byte[] bytes = new byte[10];
for (int i = 0; i < length; i++) {
bytes[i] = (byte) res;
}
for (int i = length; i < 10; i++) {
bytes[i] = '-';
}
return bytes;
}
@Test
public void test() {
byte[] bytes = fill10Bytes('a', 5);
}
}
服务器端代码:
package com.netty.netty2;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import static com.netty.netty1.TestByteBuf.log;
@Slf4j
public class HelloWorldServer2 {
static NioEventLoopGroup boss = new NioEventLoopGroup();
static NioEventLoopGroup worker = new NioEventLoopGroup();
public static void main(String[] args) {
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
ChannelFuture sync = serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//使用定长解密器
ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
}
}).bind(8080).sync();
sync.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
boss.shutdownGracefully();
}
}
}
96 黏包半包-解决-行解码器
1.以换行符为解码器
2.可以自己定义分隔符
package com.netty.netty2;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HelloWorldServer3 {
static NioEventLoopGroup boss = new NioEventLoopGroup();
static NioEventLoopGroup worker = new NioEventLoopGroup();
public static void main(String[] args) {
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
ChannelFuture sync = serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//使用定长解密器
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
}
}).bind(8080).sync();
sync.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
boss.shutdownGracefully();
}
}
}
package com.netty.netty2;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import java.net.InetSocketAddress;
import java.util.Random;
@Slf4j
public class HelloWorldClient3 {
static NioEventLoopGroup worker = new NioEventLoopGroup();
public static void main(String[] args) throws InterruptedException {
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buffer = ctx.alloc().buffer();
char c = '0';
Random r = new Random();
for (int i = 0; i < 10; i++) {
StringBuilder stringBuilder = makeString(c, r.nextInt(256));
c++;
buffer.writeBytes(stringBuilder.toString().getBytes());
}
ctx.writeAndFlush(buffer);
}
});
}
});
ChannelFuture localhost = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync();
ChannelFuture close = localhost.channel().close().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
public static StringBuilder makeString(char c, int len) {
StringBuilder sb = new StringBuilder(len + 2);
for (int i = 0; i < len; i++) {
sb.append(c);
}
sb.append("\n");
return sb;
}
}
97 黏包半包-解决-LTC解码器
基于长度字段的帧解码器
98 黏包半包-解决-LTC解码器2
package com.netty.netty2;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class TestLengthFieldDecoder {
public static void main(String[] args) {
EmbeddedChannel channel = new EmbeddedChannel(
new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4),
new LoggingHandler(LogLevel.INFO)
);
//4 个字节的内容长度,实际内容
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
send(buf, "Hello ,world");
send(buf, "Hi!");
//
channel.writeOutbound(buf);
}
private static void send(ByteBuf buffer, String content) {
byte[] bytes = content.getBytes();//实际内容
int length = bytes.length;
//以大端表示的writeInt
buffer.writeInt(length);
//假设有一个版本号
//buffer.writeByte(1);
buffer.writeBytes(bytes);
}
}
99 协议设计与解析-redis
package com.netty.netty2;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.Charset;
@Slf4j
/**
* set key value
* set name zhangsan
* 3 先发送一个长度
* $3
* set
* $4
* name
* $8
* zhangsan
*/
public class TestRedis {
public static void main(String[] args) {
NioEventLoopGroup woker = new NioEventLoopGroup();
//13 回车 10 换行
final byte[] LIKE = {13, 10};
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(woker);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = ctx.alloc().buffer();
buf.writeBytes("*3".getBytes());
buf.writeBytes(LIKE);
buf.writeBytes("$3".getBytes());
buf.writeBytes(LIKE);
buf.writeBytes("set".getBytes());
buf.writeBytes(LIKE);
buf.writeBytes("$4".getBytes());
buf.writeBytes(LIKE);
buf.writeBytes("name".getBytes());
buf.writeBytes(LIKE);
buf.writeBytes("$8".getBytes());
buf.writeBytes(LIKE);
buf.writeBytes("zhangsan".getBytes());
buf.writeBytes(LIKE);
ctx.writeAndFlush(buf);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
String string = buf.toString(Charset.defaultCharset());
System.out.println(string);
}
}
);
}
});
ChannelFuture localhost = bootstrap.connect("localhost", 6379).sync();
localhost.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
woker.shutdownGracefully();
}
}
}
100 协议设计与解析-http
package com.netty.netty2;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TestHttp {
public static void main(String[] args) {
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<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
//编码/解码处理器
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("{}", msg.getClass());
if (msg instanceof HttpRequest) {
//是请求行和请求体
} else if (msg instanceof HttpContent) {
//请求体
}
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
//closeFuture是异步的,所以要同步释放后再运行主线程
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭掉boss worker的线程
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
然后发送一条数据到我们的8080端口:虽然发送了一条数据但是到我们的Netty端它会解析成两个部分:请求行和请求体
package com.netty.netty2;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@Slf4j
public class TestHttp {
public static void main(String[] args) {
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<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
//编码/解码处理器
ch.pipeline().addLast(new HttpServerCodec());
//请求的时候被Netty解析为两个对象,请求行对象,以及请求体对象,
// 是HttpRequest请求行和请求头我们就进行处理
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
//获取请求
log.info(msg.uri());
//返回响应
DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
//响应内容
response.content().writeBytes("<h1 >Hello World!</h1>".getBytes());
//写回响应
ctx.writeAndFlush(response);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
//closeFuture是异步的,所以要同步释放后再运行主线程
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭掉boss worker的线程
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
但是通过查看浏览器我们发现它一直转圈我们没有告诉浏览器响应长度有多少那么它会一直接收响应,所以我们应该在响应头里添加响应长度告诉浏览器我响应的长度有多少,你就不用一直转圈读了
101 协议设计与解析-自定义
102 协议设计与解析-编码
@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
//1.4个字节魔术(Java CafeBebe)
out.writeBytes(new byte[]{1, 2, 3, 4});
//2. 1个字节的版本 可以支持协议的升级
out.writeByte(1);
//3. 1字节的序列化方式 jdk 0,json 1
out.writeByte(0);
//4. 1字节的指令类型
//某一个子类对象msg
out.writeByte(msg.getMessageType());
//5. 4个字节请求序号
out.writeInt(msg.getSequenceId());
//对其填充
out.writeByte(0xff);
//6 .获取内容的字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(bos);
os.writeObject(msg);
byte[] bytes = bos.toByteArray();
//7. 长度
out.writeInt(bytes.length);
//8.写入内容
out.writeBytes(bytes);
}
}
103 协议设计与解析-解码
public class MessageCodec extends ByteToMessageCodec<Message> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int magicNum = in.readInt();
//readByte让读指针向后走而get不会改变读写指针
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.info("{},{},{},{},{},{},{}", magicNum, version, serializerType, messageType, sequenceId);
log.info("{}", message);
//解码后的结果要放到List<Object> out中
out.add(message);
}
}
使用:EmbeddedChannel完成出站入站的测试:
package com.netty.protocol;
import com.netty.message.LoginRequestMessage;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class TestMessageCodec {
public static void main(String[] args) {
//既能做入站又能做出站
EmbeddedChannel channel = new EmbeddedChannel(
new LoggingHandler(LogLevel.INFO)
,new MessageCodec());
LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123", "张三");
channel.writeOutbound(message);
}
}
看一下输出的结果:
104 协议设计与解析-测试
测试一下decode:
package com.netty.protocol;
import com.netty.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
/**
* @author DQF
*/
@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
//1.4个字节魔术(Java CafeBebe)
out.writeBytes(new byte[]{1, 2, 3, 4});
//2. 1个字节的版本 可以支持协议的升级
out.writeByte(1);
//3. 1字节的序列化方式 jdk 0,json 1
out.writeByte(0);
//4. 1字节的指令类型
//某一个子类对象msg
out.writeByte(msg.getMessageType());
//5. 4个字节请求序号
out.writeInt(msg.getSequenceId());
//对其填充
out.writeByte(0xff);
//6 .获取内容的字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(bos);
os.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();
//readByte让读指针向后走而get不会改变读写指针
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.info("{},{},{},{},{},{},{}", magicNum, version, serializerType, messageType, sequenceId);
log.info("{}", message);
//解码后的结果要放到List<Object> out中
out.add(message);
}
}
因为接收缓冲区可能不足以容纳一个消息编码后的所有字节,所以会有半包使用LengthFieldBasedFrameDecoder帧解码器来解决这个问题
package com.netty.protocol;
import com.netty.message.LoginRequestMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class TestMessageCodec {
public static void main(String[] args) throws Exception {
//既能做入站又能做出站
EmbeddedChannel channel = new EmbeddedChannel(
// new LengthFieldBasedFrameDecoder(1024,12,4,0,0),
new LoggingHandler(LogLevel.INFO)
, new MessageCodec());
LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123", "张三");
//出站,出站时调用MessageCode()最终编译成ByteBuf发出去
//测试encode方法
channel.writeOutbound(message);
//decode 入站
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null, message, buf);
ByteBuf slice = buf.slice(0, 100);
ByteBuf slice2 = buf.slice(100, buf.readableBytes() - 100);
//入站
channel.writeInbound(slice);
}
}
接下来我们运行一下:
105 协议设计与解析-测试2
我们来测试一下不加帧解码器出现半包的的效果:
要读取223个字节(获取的长度)但是现在的字节数没有223不完整的数据就报错了反序列化失败
如果加上帧解码器呢?表示它要读223但是还没有到先保存一块读取
加上帧解码器全部写入到入站的结果正确
106 协议设计与解析-@sharable
我们原来使用Handler都是使用的时候就创建一个实例我们可不可以创建一个Handler实例将来把它添加到Handler里面去?答案是具体情况具体分析
我们先来尝试一下:运行一下没什么问题
但是我们仔细分析一下:1.LengthFieldBasedFrameDecoder的职责就是做黏包半包的处理,现在我们代码中只创建了一个实例,但是将来可能会被多个EventLoop所用到,EventLoop就好比工人,而我们的Handler是我们工人执行的一道工序,假设工人1收到消息时出现了半包,它把半包的消息保存起来,此时工人2又来了也发了半条数据,对于LengthFieldBasedFrameDecoder来说它把EvnetLoop1 和eventLoop2的消息拼凑起来,这样实际上就不对了,总结就是它是有状态信息的
而我们的2.LoggingHandler只打印信息无状态所以它是线程安全的那什么时候判断是可以共享的Handler呢?netty给我们提供了一个注解sharable只要你加了@sharable就可以只创建一个对象
107 协议设计与解析-@sharable
我们先写一个服务端:
我们分析MessageCodec只是做了读取信息和写入信息的事情,它没有保存状态,但是他不是共享的因为他继承了ByteToMessageCodec而ByteToMessage有明确规定子类是不能被共享的所以我们又写了一个可以资源共享的MessageCodec:
MessageCodecSharable.Java
package com.netty.protocol;
import com.netty.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
@ChannelHandler.Sharable
@Slf4j
/**
* 必须和 LengthFieldBasedFrameDecoder 使用
* 确保消息是完整的
*/
/**
* 总结: 类上能不能加@Sharable 是要根据你的业务决定的
* 比如说你的业务没有保存状态的功能,那么就可以加@Saralbe
*/
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个字节魔术(Java CafeBebe)
out.writeBytes(new byte[]{1, 2, 3, 4});
//2. 1个字节的版本 可以支持协议的升级
out.writeByte(1);
//3. 1字节的序列化方式 jdk 0,json 1
out.writeByte(0);
//4. 1字节的指令类型
//某一个子类对象msg
out.writeByte(msg.getMessageType());
//5. 4个字节请求序号
out.writeInt(msg.getSequenceId());
//对其填充
out.writeByte(0xff);
//6 .获取内容的字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(bos);
os.writeObject(msg);
byte[] bytes = bos.toByteArray();
//7. 长度
out.writeInt(bytes.length);
//8.写入内容
out.writeBytes(bytes);
//结果放到outList 中传给下一个处理器
outList.add(out);
}
/*
in不会有线程安全分析,默认编码解码的上一步就是帧解码器先收集到完整消息
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int magicNum = in.readInt();
//readByte让读指针向后走而get不会改变读写指针
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.info("{},{},{},{},{},{},{}", magicNum, version, serializerType, messageType, sequenceId);
log.info("{}", message);
//解码后的结果要放到List<Object> out中
out.add(message);
}
}
ChatServer.java
package com.netty.protocol;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
/**
* @author DQF
*/
@Slf4j
public class ChatServer {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup woker = new NioEventLoopGroup();
LoggingHandler LOGGING_HANDLE = new LoggingHandler(LogLevel.INFO);
MessageCodecSharable MESSAGECODEC = new MessageCodecSharable();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, woker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//MessageCodec:它是不能够被共享的
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 0, 0, 0));
ch.pipeline().addLast(LOGGING_HANDLE);
ch.pipeline().addLast(MESSAGECODEC);
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
log.error("{}", e);
} finally {
boss.shutdownGracefully();
woker.shutdownGracefully();
}
}
}
108 聊天业务-介绍
看几个类:
UserService 用户管理接口:用户的登录功能
/**
* 用户管理接口
*/
public interface UserService {
/**
*
* @param username 用户名
* @param password 密码
* @return 登录成功返回true 否则返回false
*/
boolean login(String username, String password);
}
UserServiceMemoryImpl 实现类
package com.netty.server.service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author DQF
*/
public class UserServiceMemoryImpl implements UserService {
private Map<String, String> allUserMap = new ConcurrentHashMap<>();
{
allUserMap.put("zhangsan", "123");
allUserMap.put("lisi", "123");
allUserMap.put("zhaoliu", "123");
allUserMap.put("qianqi", "123");
}
@Override
public boolean login(String username, String password) {
String pass = allUserMap.get(username);
if (pass == null) {
return false;
}
return pass.equals(password);
}
}
Session会话管理接口记录用户登录的状态
package com.netty.server.session;
import java.nio.channels.Channel;
public interface Session {
/**
* 绑定会话
*
* @param channel 哪个channel 要绑定会话
* @param userName 会话绑定用户
*/
void bind(Channel channel, String userName);
/**
* 解除会话
*
* @param channel 哪个会话要解绑会话
*/
void unbind(Channel channel);
/**
* 获取属性
*
* @param channel
* @param name
* @return
*/
Object getAttribute(Channel channel, String name);
/**
* 设置属性
*
* @param channel channel
* @param name 用户名
* @param value 密码
*/
void setAttribute(Channel channel, String name, Object value);
/**
* 根据用户名获取channel
*
* @param userName 用户名
* @return
*/
Channel getChannel(String userName);
}
实现类:
package com.netty.server.session;
import java.nio.channels.Channel;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SessionMemoryImpl implements Session {
private final Map<String, Channel> usernameChannelMap = new ConcurrentHashMap<>();
private final Map<Channel, String> channelUsernameMap = new ConcurrentHashMap<>();
private final Map<Channel, Map<String, Object>> channelAttributesMap = new ConcurrentHashMap<>();
@Override
public void bind(Channel channel, String userName) {
usernameChannelMap.put(userName, channel);
channelUsernameMap.put(channel, userName);
channelAttributesMap.put(channel, new ConcurrentHashMap<>());
}
@Override
public void unbind(Channel channel) {
String username = channelUsernameMap.remove(channel);
usernameChannelMap.remove(username);
}
@Override
public Object getAttribute(Channel channel, String name) {
return channelAttributesMap.get(channel).get(name);
}
@Override
public void setAttribute(Channel channel, String name, Object value) {
ConcurrentHashMap<String, Object> nameVal = new ConcurrentHashMap<>();
nameVal.put(name, value);
channelAttributesMap.put(channel, nameVal);
}
@Override
public Channel getChannel(String userName) {
return usernameChannelMap.get(userName);
}
}
聊天组管理:
package com.netty.server.session;
import io.netty.channel.Channel;
import java.util.List;
import java.util.Set;
/**
* 聊天室会话管理接口
*/
public interface GroupSession {
/**
* 创建一个聊天组, 如果不存在才能创建成功, 否则返回 null
* @param name 组名
* @param members 成员
* @return 成功时返回组对象, 失败返回 null
*/
Group createGroup(String name, Set<String> members);
/**
* 加入聊天组
* @param name 组名
* @param member 成员名
* @return 如果组不存在返回 null, 否则返回组对象
*/
Group joinMember(String name, String member);
/**
* 移除组成员
* @param name 组名
* @param member 成员名
* @return 如果组不存在返回 null, 否则返回组对象
*/
Group removeMember(String name, String member);
/**
* 移除聊天组
* @param name 组名
* @return 如果组不存在返回 null, 否则返回组对象
*/
Group removeGroup(String name);
/**
* 获取组成员
* @param name 组名
* @return 成员集合, 如果群不存在或没有成员会返回 empty set
*/
Set<String> getMembers(String name);
/**
* 获取组成员的 channel 集合, 只有在线的 channel 才会返回
* @param name 组名
* @return 成员 channel 集合
*/
List<Channel> getMembersChannel(String name);
}
109 聊天业务-包结构
看一下聊天业务包结构:
client:客户端代码
message:使用java对象实现客户端和服务器端通信的消息进行封装,方便我们业务处理
protocol:编解码器,以及和协议相关的类
server: 1.service包进行用户管理的 2.session包:进行会话管理的
再来编写一下我们的客户端:
package com.netty.client;
import com.netty.protocol.MessageCodecSharable;
import com.netty.protocol.ProcotolFrameDecode;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
* @author DQF
*/
public class ChatClient {
public static NioEventLoopGroup group = new NioEventLoopGroup();
public static void main(String[] args) {
LoggingHandler LOGGING_HANDLE = new LoggingHandler(LogLevel.INFO);
MessageCodecSharable MESSAGECODEC = new MessageCodecSharable();
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 ProcotolFrameDecode());
ch.pipeline().addLast(LOGGING_HANDLE);
ch.pipeline().addLast(MESSAGECODEC);
}
});
ChannelFuture localhost = bootstrap.connect("localhost", 8080).sync();
localhost.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
110 聊天业务-登录
客户端代码:
package com.netty.client;
import com.netty.message.LoginRequestMessage;
import com.netty.protocol.MessageCodecSharable;
import com.netty.protocol.ProcotolFrameDecode;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Scanner;
/**
* @author DQF
*/
@Slf4j
public class ChatClient {
public static NioEventLoopGroup group = new NioEventLoopGroup();
public static void main(String[] args) {
//日志打印
LoggingHandler LOGGING_HANDLE = new LoggingHandler(LogLevel.INFO);
//线程共享的处理编码
MessageCodecSharable MESSAGECODEC = new MessageCodecSharable();
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 ProcotolFrameDecode());
ch.pipeline().addLast(LOGGING_HANDLE);
ch.pipeline().addLast(MESSAGECODEC);
//入站处理器,写入了就会触发出站操作
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("msg:{}", msg);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//在连接建立后会触发这个消息
new Thread(() -> {
//负责接收用户在控制台的输入,负责向服务器发送各种消息
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名!");
String userName = scanner.nextLine();
String password = scanner.nextLine();
//构造消息对象
LoginRequestMessage message = new LoginRequestMessage(userName, password);
//发送消息
ctx.writeAndFlush(message);
System.out.println("等待后序操作");
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}, "loginThread").start();
}
});
}
});
ChannelFuture localhost = bootstrap.connect("localhost", 8080).sync();
localhost.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
服务端代码:
package com.netty.server;
import com.netty.message.LoginRequestMessage;
import com.netty.message.LoginResponseMessage;
import com.netty.protocol.MessageCodecSharable;
import com.netty.protocol.ProcotolFrameDecode;
import com.netty.server.service.UserServiceFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
/**
* @author DQF
*/
@Slf4j
public class ChatServer {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup woker = new NioEventLoopGroup();
LoggingHandler LOGGING_HANDLE = new LoggingHandler(LogLevel.INFO);
MessageCodecSharable MESSAGECODEC = new MessageCodecSharable();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, woker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//MessageCodec:它是不能够被共享的
ch.pipeline().addLast(new ProcotolFrameDecode());
ch.pipeline().addLast(LOGGING_HANDLE);
ch.pipeline().addLast(MESSAGECODEC);
ch.pipeline().addLast(new SimpleChannelInboundHandler<LoginRequestMessage>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception {
//读取到的
String username = msg.getUsername();
String password = msg.getPassword();
boolean login = UserServiceFactory.getUserService().login(username, password);
LoginResponseMessage message;
if (login) {
message = new LoginResponseMessage(true, "登录成功");
} else {
message = new LoginResponseMessage(false, "用户名或密码不正确");
}
ctx.writeAndFlush(message);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
log.error("{}", e);
} finally {
boss.shutdownGracefully();
woker.shutdownGracefully();
}
}
}
111 聊天业务-登录-线程通信
当客户端向服务器端发送了消息以后:服务端会回给客户端响应
此时客户端会触发channelRead事件但是要注意,channelRead事件
是在我们Nio的线程里被调用的,而我们在等待服务器端结果的线程是
loginThread线程,那么怎么样让这两个线程进行通信呢?这里有多种方法:
1.使用countDwonLatch倒计时锁
package com.netty.client;
import com.netty.message.LoginRequestMessage;
import com.netty.message.LoginResponseMessage;
import com.netty.protocol.MessageCodecSharable;
import com.netty.protocol.ProcotolFrameDecode;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author DQF
*/
@Slf4j
public class ChatClient {
public static NioEventLoopGroup group = new NioEventLoopGroup();
public static void main(String[] args) {
//日志打印
LoggingHandler LOGGING_HANDLE = new LoggingHandler(LogLevel.INFO);
//线程共享的处理编码
MessageCodecSharable MESSAGECODEC = new MessageCodecSharable();
//当计数减为0的时候它就会继续向下进行了,否则就会等待
CountDownLatch WAIT_FORLOGIN = new CountDownLatch(1);
AtomicBoolean LOGIN = new AtomicBoolean(false);
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 ProcotolFrameDecode());
//ch.pipeline().addLast(LOGGING_HANDLE);
ch.pipeline().addLast(MESSAGECODEC);
//入站处理器,写入了就会触发出站操作
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("msg:{}", msg);
if (msg instanceof LoginRequestMessage) {
LoginResponseMessage response = (LoginResponseMessage) msg;
if (response.isSuccess()) {
LOGIN.set(true);
}
}
WAIT_FORLOGIN.countDown();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//在连接建立后会触发这个消息
new Thread(() -> {
//负责接收用户在控制台的输入,负责向服务器发送各种消息
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名!");
String userName = scanner.nextLine();
String password = scanner.nextLine();
//构造消息对象
LoginRequestMessage message = new LoginRequestMessage(userName, password);
//发送消息
ctx.writeAndFlush(message);
System.out.println("等待后序操作");
try {
WAIT_FORLOGIN.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//登录失败
if (!LOGIN.get()) {
//关闭channel,成功后会通知channelFuture
ctx.channel().close();
return;
}
Scanner scanner1 = new Scanner(System.in);
String command = scanner1.nextLine();
while (!command.equals("quit")) {
command = scanner1.nextLine();
System.out.println("=======================================");
System.out.println("send ");
System.out.println("gsend ");
System.out.println("gcreate ");
System.out.println("gmembers");
System.out.println("gjoin [group name]");
System.out.println("gquit[group name]");
System.out.println("quit [group name]");
System.out.println("======================================");
}
}, "loginThread").start();
}
});
}
});
ChannelFuture localhost = bootstrap.connect("localhost", 8080).sync();
localhost.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
112 聊天业务-业务消息推送
小结
海到无边天作崖,山登绝顶我为峰----林则徐
Netty的第一次学习记录就到这了,后序后序章节的阅读请看Netty2,后续有源码分析,或者要改的地方还会持续更新文章,谢谢大家阅读