Netty
一些问题
1、阻塞与非阻塞
阻塞和非阻塞指的是执行一个操作是等操作结束再返回,还是马上返回
举例:在 BIO 案例的 handler 方法中,如果读取不到数据就会阻塞在 read() 方法处
// 与客户端通信
public static void handler(Socket socket) {
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream();
while (true) {
int read = inputStream.read(bytes); // 如果读不到数据,会阻塞在此处
if (read != -1) {
System.out.println(new String(bytes,0,read));
} else {
break;
}
}
}
2、NIO 非阻塞的实现原理
NIO 通过其 Buffer 组件来实现非阻塞,线程非阻塞去处理别的通道(channel),未被线程处理的连接可以将数据继续写入 Buffer
**举例说明:**作为服务员的你不用一直在窗口等,非阻塞的逻辑是"等可以读(写)了告诉你",但是完成读(写)工作的还是调用者(线程)服务员的你等菜到窗口了还是要你亲自去拿
3、非阻塞和异步区别
**非阻塞:可以做别的事,你要做的事最后还是要自己做**只是意味着方法调用不阻塞,就是说作为服务员的你不用一直在窗口等,非阻塞的逻辑是"等可以读(写)了告诉你",但是完成读(写)工作的还是调用者(线程)服务员的你等菜到窗口了还是要你亲自去拿。
异步:可以做别的事,你要做的事别人帮你做。意味这你可以不用亲自去做读(写)这件事,你的工作让别人(别的线程)来做,你只需要发起调用,别人把工作做完以后,或许再通知你,它的逻辑是“我做完了 告诉/不告诉 你”,他和非阻塞的区别在于一个是"已经做完"另一个是"可以去做"。
4、BIO、NIO、AIO 比较
BIO | NIO | |
---|---|---|
处理数据方式 | 流(字节流、字符流) | 数据块(Buffer) |
流向 | 单向 | 双向 |
阻塞方式 | 阻塞 | 非阻塞 |
复用方式 | 一个连接一个线程 | 多路复用(Selector) |
BIO | NIO | AIO | |
---|---|---|---|
IO模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
编程难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | d低 | 高 | 高 |
举例说明 :
① **同步阻塞:**到理发店理发,就一直等理发师,直到轮到自己理发。
② **同步非阻塞:**到理发店理发,发现前面有其它人理发,给理发师说 下,先干其他事情,一会过来看是否轮到自己.
③ **异步非阻塞:**给理发师打电话,让理发师上门服务,自己干其它事情,理发师自己来家给你理发
5、select、selectedKeys、keys
方法 | 说明 |
---|---|
select | 返回被监听到事件发生的 channel 数目 |
selectKeys | 返回被监听到事件发生的 channel 对应的 SelectionKey |
keys | 返回所有被注册到 Selector 的 channel 的 SelectionKey |
6、SelectionKey 和 SocketChannel 关系
SelectionKey 相当于 SocketChannel 的身份证,Selector 是管理的公安局
7、三种线程模型对比
类型 | 说明 |
---|---|
单 Reactor 单线程 | 前台接待员和服务员是同一个人,全程为顾客服 |
单 Reactor 多线程 | 1 个前台接待员,多个服务员,接待员只负责接待 |
主从 Reactor 多线程 | 多个服务生 |
8、什么是出入站
什么是出入站:从 Handler 头部到尾部为入站,反之出站
9、Channel、ChannelPipeline、ChannelHandler、ChannelHandlerContextr 关系:
Channel、ChannelPipeline、ChannelHandler、ChannelHandlerContext 关系:
- 一个 Channel 对应一个 ChannelPipeline
- 一个 ChannelPipeline 包含多个 ChannelHandler
- 一个 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表
- 一个 ChannelHandlerContext 中又关联着一个 ChannelHandler,并保存了 Channel 相关信息
一、IO模型
1、模型介绍
-
I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
-
Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO
-
BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
-
NIO : 同步非阻塞(非阻塞依靠 Buffer ),服务器实现模式为一个线程处理多个请求(连接),即客户端发 送的连接请求都会注册到多路复用器(Selector)上,多路复用器轮询到连接有I/O请求就进行处理
-
AIO(NIO.2) : 异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程
2、模型图解
2.1、BIO
2.2、NIO
3、适用场景
- BIO: 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高, 并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
- NIO: 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。
- AIO: 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持
二、BIO
1、代码演示
① 使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯。
② 要求使用线程池机制改善,可以连接多个客户端.
③ 服务器端可以接收客户端发送的数据(telnet)
public class BIOServer {
public static void main(String[] args) throws Exception {
// 线程池机制
// 思路
// 1. 创建一个线程池
// 2. 如果有客户端连接,就创建一个线程,与之通信(单独写一个方法
// 实现
// 创建线程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
// 创建 ServerSocket,并指定端口号:6666
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器已启动");
while (true) {
// 监听,等待客户端连接
System.out.println("等待连接...");
// 获取连接
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
// 如果发现有连接就创建一个线程,与之通信(单独一个方法)
newCachedThreadPool.submit(() -> { handler(socket); });
}
}
// 与客户端通信
public static void handler(Socket socket) {
try {
byte[] bytes = new byte[1024];
// 通过 socket 获取输入流
InputStream inputStream = socket.getInputStream();
// 循环读取客户端发送的消息
while (true) {
int read = inputStream.read(bytes);
if (read != -1) {
// 如果有数据,则输出数据
System.out.println(Thread.currentThread().getName() + ":" + new String(bytes,0,read));
} else {
// 如果读完了则退出循环
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("关闭和 client 的连接");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2、缺陷
- 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write
- 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞 在 Read 操作上,造成线程资源浪费
// 与客户端通信
public static void handler(Socket socket) {
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream();
while (true) {
int read = inputStream.read(bytes); // 如果读不到数据,会阻塞在此处
if (read != -1) {
System.out.println(new String(bytes,0,read));
} else {
break;
}
}
}
三、NIO
1、介绍
- Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出 的新特性,被统称为 NIO(即 New IO),是同步非阻塞的
- NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写
- NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
- NIO是 面向缓冲区 ,或者面向 块 编程的。数据读取到一个 它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式(通过 Buffer 实现)的高伸缩性网络
2、NIO 和 BIO 比较
BIO | NIO | |
---|---|---|
处理数据方式 | 流(字节流、字符流) | 数据块(Buffer) |
流向 | 单向 | 双向 |
阻塞方式 | 阻塞 | 非阻塞 |
复用方式 | 一个连接一个线程 | 多路复用(Selector) |
3、Buffer【缓存】
缓冲区(Buffer):本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法
3.1、Buffer 四属性
mark <= position <= limit <= capacity
属性 | 描述 | 是否可变 |
---|---|---|
capacity | 容量 | 不可变 |
limit | 最大读写范围 | 可变 |
position | 下一个读写的位置 | 可变 |
mark | 标记 | 可变 |
3.2、常用的 Buffer
类型 | 作用 |
---|---|
ByteBuffer | 存储字节数据到缓冲区 |
ShortBuffer | 存储字符串数据到缓冲区 |
CharBuffer | 存储字符数据到缓冲区 |
IntBuffer | 存储整数数据到缓冲区 |
LongBuffer | 存储长整型数据到缓冲区 |
DoubleBuffer | 存储小数到缓冲区 |
FloatBuffer | 存储小数到缓冲区 |
以 IntBuffer 为例,写一个小案例
public class BasicBuffer {
public static void main(String[] args) {
// 创建一个 Buffer , 大小为5, 即可以存放5个int
IntBuffer intBuffer = IntBuffer.allocate(5);
// 向 buffer 中存放数据
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i);
}
/*
Buffer 的 flip() 方法:
作用:将 buffer 进行读写切换,比如上方是写,要读则要切换
做了哪些事:
limit = position; //读数据不能超过5
position = 0;
mark = -1;
*/
intBuffer.flip();
// 改变属性
//intBuffer.position(1);
//System.out.println(intBuffer.get());
//intBuffer.limit(3);
// 读取数据
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
3.3、Buffer 相关方法
4、Channel【通道】
4.1、介绍
- NIO的通道类似于流,但有些区别如下:通道可以同时进行读写,而流只能读或者只能写 ,通道可以实现异步读写数据 ,通道可以从缓冲读数据,也可以写数据到缓冲:
- BIO 中的 stream 是单向的,例如 FileInputStream 对 象只能进行读取数据的操作,而 NIO 中的通道 (Channel)是双向的,可以读操作,也可以写操作。
- Channel在NIO中是一个接口 public interface Channel extends Closeable{}
4.2、常用Channel
类型 | 说明 |
---|---|
FileChannel | 用于本地文件的数据读写 |
DatagramChannel | 用于 UDP 的数据读写 |
ServerSocketChannel SocketChannel | 用于 TCP 的数据读写 ServerSocketChannel(类似 JDK 的 ServerSocket) SocketChannel(类似 JDK 的 Socket) 关系:ServerSocketChannel 用于监听,从而创建 SocketChannel |
4.3、FileChannel 类
[1] 常用方法
public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道
中复制数据到当前通道
public long transferTo(long position, long count, WritableByteChannel target),把数据从当
前通道复制给目标通道
[2] 代码演示
需求:
使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 “hello,尚硅谷” 写入 到 file01.txt 中
文件不存在就创建
public class FileChannelWrite {
public static void main(String[] args) throws Exception {
String str = "hello,尚硅谷";
// 创建文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
//通过 fileOutputStream 获取 对应的 FileChannel
//这个 fileChannel 真实 类型是 FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
// 创建 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将数据放入 ByteBuffer 中
byteBuffer.put(str.getBytes());
// 读写反转
byteBuffer.flip();
// 将 Buffer 中的数据写入 Channel
fileChannel.write(byteBuffer);
// 关闭流
fileOutputStream.close();
}
}
需求:
使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 file01.txt 中的数据读入到程序,并显示在控制台屏幕
假定文件已经存在
public class FileChannelRead {
public static void main(String[] args) throws Exception {
// 创建文件输入流
File file = new File("d:\\file01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
// 通过fileInputStream 获取对应的FileChannel -> 实际类型 FileChannelImpl
FileChannel fileChannel = fileInputStream.getChannel();
// 创建 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
// 将 Channel 中的数据 读入到 Buffer 中
fileChannel.read(byteBuffer);
// 打印文件内容
System.out.println(new String(byteBuffer.array()));
}
}
需求:
使用 FileChannel(通道) 和 方法 read , write,完成文件的拷贝
拷贝一个文本文件 1.txt , 放在项目下即可
public class FileChannelWriteRead {
public static void main(String[] args) throws Exception {
// 通过输入流获取 Channel
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileChannel inputStreamChannel = fileInputStream.getChannel();
// 通过输出流获取 Channel
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel outputStreamChannel = fileOutputStream.getChannel();
// 创建 Buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true) { // 循环读取
// 将 Channel 中的数据读入 Buffer
int readCount = inputStreamChannel.read(byteBuffer);
if (readCount == -1) {
// 说明没有数据了,退出
break;
}
// 读写反转
byteBuffer.flip();
// 将 Buffer 中的数据写入 Channel 中,保存在 2.txt 中
outputStreamChannel.write(byteBuffer);
// 清空 Buffer 中的数据
byteBuffer.clear();
}
// 关闭流
inputStreamChannel.close();
outputStreamChannel.close();
}
}
使用 FileChannel(通道) 和 方法 transferFrom ,完成文件的拷贝
拷贝一张图片
public class TransferFromTest {
public static void main(String[] args) throws Exception {
//创建相关流
FileInputStream fileInputStream = new FileInputStream("d:\\a1.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.jpg");
//获取各个流对应的FileChannel
FileChannel sourceCh = fileInputStream.getChannel();
FileChannel destCh = fileOutputStream.getChannel();
//使用transferForm完成拷贝
destCh.transferFrom(sourceCh,0,sourceCh.size());
//关闭相关通道和流
sourceCh.close();
destCh.close();
fileInputStream.close();
fileOutputStream.close();
}
}
4.4、ReadOnlyBuffer
可以将一个普通Buffer 转成只读 Buffer
public class ReadOnlyBuffer {
public static void main(String[] args) {
//创建一个buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
for(int i = 0; i < 64; i++) {
buffer.put((byte)i);
}
//读取
buffer.flip();
//得到一个只读的Buffer
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println(readOnlyBuffer.getClass());
//读取
while (readOnlyBuffer.hasRemaining()) {
System.out.println(readOnlyBuffer.get());
}
readOnlyBuffer.put((byte)100); // ReadOnlyBufferException 此处报错
}
}
4.5、MappedByteBuffer
可以让文件直接在内存(堆外的内存)中进 行修改, 而如何同步到文件由NIO 来完成
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
//获取对应的通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数1: FileChannel.MapMode.READ_WRITE 使用的读写模式
* 参数2: 0 : 可以直接修改的起始位置
* 参数3: 5: 是映射到内存的大小(不是索引位置) ,即将 1.txt 的多少个字节映射到内存
* 可以直接修改的范围就是 0-5
* 实际类型 DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'H');
mappedByteBuffer.put(3, (byte) '9');
randomAccessFile.close();
System.out.println("修改成功~~");
}
}
4.6、Scattering(离散) 和 Gathering(聚合)
前面我们讲的读写操作,都是通过一个Buffer 完成的,NIO 还支持 通过多个 Buffer (即 Buffer 数组) 完成读写操作,即 Scattering 和 Gathering
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws Exception {
//使用 ServerSocketChannel 和 SocketChannel 网络
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
//绑定端口到socket ,并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//创建buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
//等客户端连接(telnet)
SocketChannel socketChannel = serverSocketChannel.accept();
int messageLength = 8; //假定从客户端接收8个字节
//循环的读取
while (true) {
int byteRead = 0;
while (byteRead < messageLength ) {
long count = socketChannel.read(byteBuffers);
byteRead += count; //累计读取的字节数
System.out.println("byteRead=" + byteRead);
//使用流打印, 看看当前的这个buffer的position 和 limit
Arrays.asList(byteBuffers).stream()
.map(buffer -> "position=" + buffer.position() + ", limit=" + buffer.limit())
.forEach(System.out::println);
}
//将所有的buffer进行flip
Arrays.asList(byteBuffers).forEach(Buffer::flip);
//将数据读出显示到客户端
long byteWirte = 0;
while (byteWirte < messageLength) {
long count = socketChannel.write(byteBuffers);
byteWirte += count;
}
//将所有的buffer 进行clear
Arrays.asList(byteBuffers).forEach(Buffer::clear);
System.out.println("byteRead:=" + byteRead + " byteWrite=" + byteWirte + ", messageLength" + messageLength);
}
}
}
5、Selector【选择器】
5.1、介绍
- Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连 接,就会使用到Selector(选择器)
- Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然 后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个 通道,也就是管理多个连接和请求
- 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少 了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
- 避免了多线程之间的上下文切换导致的开销
5.2、相关方法
public static Selector open();//得到一个选择器对象
public int select(long timeout);//监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
public Set<SelectionKey> selectedKeys();//从内部集合中得到所有的 SelectionKey
select()//阻塞
select(1000);//阻塞1000毫秒,在1000毫秒后返回
wakeup();//唤醒selector
selectNow();//不阻塞,立马返还
方法 | 说明 |
---|---|
select | 返回被监听到事件发生的 channel 数目 |
selectKeys | 返回被监听到事件发生的 channel 对应的 SelectionKey |
keys | 返回所有被注册到 Selector 的 channel 的 SelectionKey |
5.3、IO非阻塞原理分析
- 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
- Selector 进行监听 select 方法, 返回有事件发生的通道的个数.
- 将socketChannel注册到Selector上, register(Selector sel, int ops), 一个 selector上可以注册多个SocketChannel
- 注册后返回一个 SelectionKey, 会和该 Selector 关联(集合)
- 进一步得到各个 SelectionKey (有事件发生)
- 在通过 SelectionKey 反向获取 SocketChannel , 方法 channel()
- 可以通过 得到的 channel , 完成业务处理
5.4、小案例
需求: 编写一个 NIO 入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)
[1] 服务端
public class SelectorServer {
public static void main(String[] args) throws Exception {
//创建ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selecor对象
Selector selector = Selector.open();
//绑定一个端口6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1
//循环等待客户端连接
while (true) {
//这里我们等待1秒,如果没有事件发生, 返回
if (selector.select(1000) == 0) { //没有事件发生
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的>0, 就获取到相关的 selectionKey集合
//1.如果返回的>0, 表示已经获取到关注的事件
//2. selector.selectedKeys() 返回关注事件的集合
// 通过 selectionKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectionKeys 数量 = " + selectionKeys.size());
//遍历 Set<SelectionKey>, 使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
//获取到SelectionKey
SelectionKey key = keyIterator.next();
//根据key 对应的通道发生的事件做相应处理
if (key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
//该该客户端生成一个 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
//将 SocketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
//关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
}
if (key.isReadable()) { //发生 OP_READ
//通过key 反向获取到对应channel
SocketChannel channel = (SocketChannel) key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("form 客户端 " + new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey, 防止重复操作
keyIterator.remove();
}
}
}
}
[2] 客户端
public class SelectorClient {
public static void main(String[] args) throws Exception{
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞
socketChannel.configureBlocking(false);
//提供服务器端的ip 和 端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
}
}
//...如果连接成功,就发送数据
String str = "hello, 尚硅谷~";
//Wraps a byte array into a buffer
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//发送数据,将 buffer 数据写入 channel
socketChannel.write(buffer);
System.in.read();
}
}
6、SelectionKey、ServerSocketChannel、SocketChannel
6.1、SelectionKey
[1] 四种事件
类型 | 说明 |
---|---|
OP_ACCEPT | 有新的网络连接可以 accept,值为 16 |
OP_CONNECT | 代表连接已经建立,值为 8 |
OP_READ | 代表读操作,值为 1 |
OP_WRITE | 代表写操作,值为 4 |
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4
[2] 常用方法
public abstract class SelectionKey {
public abstract Selector selector();//得到与之关联的
Selector 对象
public abstract SelectableChannel channel();//得到与之关
联的通道
public final Object attachment();//得到与之关联的共享数
据
public abstract SelectionKey interestOps(int ops);//设置或改
变监听事件
public final boolean isAcceptable();//是否可以 accept
public final boolean isReadable();//是否可以读
public final boolean isWritable();//是否可以写
}
6.2、ServerSocketChannel
ServerSocketChannel 在服务器端监听新的客户端 Socket 连接
public abstract class ServerSocketChannel {
public static ServerSocketChannel open()//得到一个 ServerSocketChannel 通道
public final ServerSocketChannel bind(SocketAddress local)//设置服务器端端口号
public final SelectableChannel configureBlocking(boolean block)//设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public SocketChannel accept()//接受一个连接,返回代表这个连接的通道对象
public final SelectionKey register(Selector sel, int ops)//注册一个选择器并设置监听事件
}
6.3、SocketChannel
SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通 道,或者把通道里的数据读到缓冲区
public abstract class SocketChannel{
public static SocketChannel open();//得到一个 SocketChannel 通道
public final SelectableChannel configureBlocking(boolean block);//设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public boolean connect(SocketAddress remote);//连接服务器
public boolean finishConnect();//如果上面的方法连接失败,接下来就要通过该方法完成连接操作
public int write(ByteBuffer src);//往通道里写数据
public int read(ByteBuffer dst);//从通道里读数据
public final SelectionKey register(Selector sel, int ops, Object att);//注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
public final void close();//关闭通道
}
7、群聊案例
7.1、服务端
public class GroupChatServer {
//定义属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6667;
//构造器
//初始化工作
public GroupChatServer() {
try {
//得到选择器
selector = Selector.open();
//ServerSocketChannel
listenChannel = ServerSocketChannel.open();
//绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞模式
listenChannel.configureBlocking(false);
//将该listenChannel 注册到selector
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
}catch (IOException e) {
e.printStackTrace();
}
}
//监听
public void listen() {
System.out.println("监听线程: " + Thread.currentThread().getName());
try {
//循环处理
while (true) {
int count = selector.select();
if(count > 0) {//有事件处理
//遍历得到selectionKey 集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
//取出selectionkey
SelectionKey key = iterator.next();
//监听到accept
if(key.isAcceptable()) {
SocketChannel sc = listenChannel.accept();
sc.configureBlocking(false);
//将该 sc 注册到seletor
sc.register(selector, SelectionKey.OP_READ);
//提示
System.out.println(sc.getRemoteAddress() + " 上线 ");
}
if(key.isReadable()) { //通道发送read事件,即通道是可读的状态
//处理读 (专门写方法..)
readData(key);
}
//当前的key 删除,防止重复处理
iterator.remove();
}
} else {
System.out.println("等待....");
}
}
}catch (Exception e) {
e.printStackTrace();
}finally {
//发生异常处理....
}
}
//读取客户端消息
private void readData(SelectionKey key) {
//取到关联的channle
SocketChannel channel = null;
try {
//得到channel
channel = (SocketChannel) key.channel();
//创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
//根据count的值做处理
if(count > 0) {
//把缓存区的数据转成字符串
String msg = new String(buffer.array());
//输出该消息
System.out.println("form 客户端: " + msg);
//向其它的客户端转发消息(去掉自己), 专门写一个方法来处理
sendInfoToOtherClients(msg, channel);
}
}catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress() + " 离线了..");
//取消注册
key.cancel();
//关闭通道
channel.close();
}catch (IOException e2) {
e2.printStackTrace();;
}
}
}
//转发消息给其它客户(通道)
private void sendInfoToOtherClients(String msg, SocketChannel self ) throws IOException{
System.out.println("服务器转发消息中...");
System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName());
//遍历 所有注册到selector 上的 SocketChannel,并排除 self
for(SelectionKey key: selector.keys()) {
//通过 key 取出对应的 SocketChannel
Channel targetChannel = key.channel();
//排除自己
if(targetChannel instanceof SocketChannel && targetChannel != self) {
//转型
SocketChannel dest = (SocketChannel)targetChannel;
//将msg 存储到buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将buffer 的数据写入 通道
dest.write(buffer);
}
}
}
public static void main(String[] args) {
//创建服务器对象
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
//可以写一个Handler
class MyHandler {
public void readData() {
}
public void sendInfoToOtherClients(){
}
}
7.2、客户端
public class GroupChatClient {
//定义相关的属性
private final String HOST = "127.0.0.1"; // 服务器的ip
private final int PORT = 6667; //服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String username;
//构造器, 完成初始化工作
public GroupChatClient() throws IOException {
selector = Selector.open();
//连接服务器
socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//将channel 注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
//得到username
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + " is ok...");
}
//向服务器发送消息
public void sendInfo(String info) {
info = username + " 说:" + info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
}catch (IOException e) {
e.printStackTrace();
}
}
//读取从服务器端回复的消息
public void readInfo() {
try {
int readChannels = selector.select();
if(readChannels > 0) {//有可以用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if(key.isReadable()) {
//得到相关的通道
SocketChannel sc = (SocketChannel) key.channel();
//得到一个Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取
sc.read(buffer);
//把读到的缓冲区的数据转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
}
iterator.remove(); //删除当前的selectionKey, 防止重复操作
} else {
//System.out.println("没有可以用的通道...");
}
}catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
//启动我们客户端
GroupChatClient chatClient = new GroupChatClient();
//启动一个线程, 每个3秒,读取从服务器发送数据
new Thread() {
public void run() {
while (true) {
chatClient.readInfo();
try {
Thread.currentThread().sleep(3000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
//发送数据给服务器端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String s = scanner.nextLine();
chatClient.sendInfo(s);
}
}
}
四、零拷贝
Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。
零拷贝也就是没有 CPU 拷贝
拷贝类型 | 说明 |
---|---|
DMA拷贝 | 从硬盘到内存的拷贝 |
CPU拷贝 | CPU参与的拷贝(重复数据) |
1、传统IO读写
类型 | 上下文切换次数 | 拷贝次数 |
---|---|---|
传统IO读写 | 3 | 4 |
File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
2、mmap 优化
mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝 次数
类型 | 上下文切换次数 | 拷贝次数 |
---|---|---|
传统IO读写 | 3 | 3 |
3、sendFile优化
3.1、Linux 2.1 版本
Linux 2.1 版本 提供了 sendFile 函数,其基本 原理如下:数据根本不 经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用 户态完全无关,就减少 了一次上下文切换
类型 | 上下文切换次数 | 拷贝次数 |
---|---|---|
传统IO读写 | 2 | 3 |
3.2、Linux 2.4 版本
Linux 在 2.4 版本中,做了 一些修改,避免了从内核 缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少 了数据拷贝。
类型 | 上下文切换次数 | 拷贝次数 |
---|---|---|
传统IO读写 | 2 | 2 |
3.3、案例【transferTo】
需求:
使用传统的IO 方法传递一个大文件
使用NIO 零拷贝方式传递(transferTo)一个大文件
看看两种传递方式耗时时间分别是多少
[1] 服务端
//服务器
public class NewIOServer {
public static void main(String[] args) throws Exception {
InetSocketAddress address = new InetSocketAddress(7001);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(address);
//创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
int readcount = 0;
while (-1 != readcount) {
try {
readcount = socketChannel.read(byteBuffer);
}catch (Exception ex) {
// ex.printStackTrace();
break;
}
//
byteBuffer.rewind(); //倒带 position = 0 mark 作废
}
}
}
}
[2] 客户端
public class NewIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 7001));
String filename = "protoc-3.6.1-win32.zip";
//得到一个文件channel
FileChannel fileChannel = new FileInputStream(filename).getChannel();
//准备发送
long startTime = System.currentTimeMillis();
//在linux下一个transferTo 方法就可以完成传输
//在windows 下 一次调用 transferTo 只能发送8m , 就需要分段传输文件, 而且要主要
//传输时的位置 =》 课后思考...
//transferTo 底层使用到零拷贝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送的总的字节数 =" + transferCount + " 耗时:" + (System.currentTimeMillis() - startTime));
//关闭
fileChannel.close();
}
}
五、Netty
1、概述
- Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序
- Netty 可以帮助你快速、简单的开发出一个网络应用,相当于简化和流程化了 NIO 的 开发过程
- Netty 是目前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的 Elasticsearch 、Dubbo 框架内部都采 用了 Netty。
1.1、NIO缺点
- NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、 SocketChannel、ByteBuffer 等。
- 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
- 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失 败缓存、网络拥塞和异常流的处理等等。
- JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导 致 CPU 100%。直到 JDK 1.7 版本该问题仍旧存在,没有被根本解决。
1.2、netty优点
Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题:
- 设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展 的事件模型,可以清晰地分离关注点;高度可定制的线程模型 ---->单线程,一个或多个 线程池.
- 使用方便:详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了。
- 高性能、吞吐量更高;延迟更低;减少资源消耗;最小化不必要的内存复制。
- 安全:完整的 SSL/TLS 和 StartTLS 支持。
- 社区活跃、不断更新:社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复, 同时,更多的新功能会被加入
2、Netty高性能架构设计
Netty 线程模式(Netty 主要基于主从 Reactor 多线程模型做了一定的改进,其中主从 Reactor 多线程模型有多个 Reactor)
2.1、线程模型
传统阻塞 I/O 服务模型 | Reactor 模式 |
---|---|
单 Reactor 单线程 | |
单 Reactor 多线程; | |
主从 Reactor 多线程 |
2.2、传统阻塞 I/O 服务模型
模型特点:
采用阻塞IO模式获取输入的数据
每个连接都需要独立的线程完成数据的输入,业务处理, 数据返回
问题分析:
当并发数很大,就会创建大量的线程,占用很大系统资源
连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read 操作,造成线程资源浪费
2.3、Reactor 模式
Reactor模式解决了传统模式哪些问题:
- 基于 I/O 复用模型:多个连接共用一个阻塞对象(ServiceHandler),应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接
- 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理 任务分配给线程进行处理,一个线程可以处理多个连接的业务。
对上图解读:
- Reactor 模式,通过一个或多个输入同时传递给服务处理器的模式 (基于事件驱动)
- 服务器端程序处理传入的多个请求, 并将它们同步分派到相应的处理线 程, 因此Reactor模式也叫 Dispatcher模式
- Reactor 模式使用IO复用监听事件, 收到事件后,分发给某个线程(进程), 这点就是网络服务器高并发处理关键
Reactor 模式类型:单 Reactor 单线程 、单 Reactor 多线程 、主从 Reactor 多线程
Reactor 核心组件:
- Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序(Handler)来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;
- Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行 非阻塞操作
Reactor 模式优点:
- 响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的
- 可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程 的切换开销
- 扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源
- 复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性
2.4、单Reactor单线程
如何理解单线程:处理 Handler1 和处理 Handler2 使用的是同一个线程
对上图说明:
- Select 是前面 I/O 复用模型介绍的标准网络编程 API,可以实现应用程序通过一个阻塞对象监听多路连接请求
- Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发
- 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理
- 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应
- Handler 会完成 Read→业务处理→Send 的完整业务流程 结合实例:服务器端用一个线程通过多路复用搞定所有的 IO 操作(包括连接,读、写 等),编码简单,清晰明了,但是如果客户端连接数量较多,将无法支撑,前面的 NIO 案例就属于这种模型
**优点:**模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
缺点:
- 性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。Handler 在处理某 个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈
- 可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
使用场景:客户端的数量有限,业务处理非常快速,比如 Redis 在业务处理的时间复杂度 O(1)
2.5、单Reactor多线程
- Reactor 对象通过select 监控客户端请求 事件, 收到事件后,通过dispatch进行分发
- 如果建立连接请求, 则右 Acceptor 通过 accept 处理连接请求, 然后创建一个 Handler 对象处理完成连接后的各种事件
- 如果不是连接请求,则由 reactor 分发调用连接对应的 handler 来处理
- handler 只负责响应事件,不做具体的业务处理, 通过 read 读取数据后,会分发给后面的 worker 线程池的某个线程处理业务
- worker 线程池会分配独立线程完成真正的业务, 并将结果返回给handler
- handler收到响应后,通过send 将结果返回给 client
**优点:**可以充分的利用多核 cpu 的处理能力
缺点:Reactor 仍是单线程,高并发时 Reactor 分发会遇到瓶颈
2.6、主从Reactor多线程
- Reactor主线程 MainReactor 对象通过 select 监听连接事件, 收到事件后,通过Acceptor 处理连接事件
- 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor
- subreactor 将连接加入到连接队列进行监听,并创建handler 进行各种事件处理
- 当有新事件发生时, subreactor 就会调用对应的handler处理
- handler 通过 read 读取数据,分发给后面的 worker 线程处理
- worker 线程池分配独立的 worker 线程进行业务处理,并返 回结果
- handler 收到响应的结果后,再通过 send 将结果返回给 client
优点:
- 父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理
- 父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据
**缺点:**编程复杂度较高
结合实例:这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型, Memcached 主从多线程,Netty 主从多线程模型的支持
2.7、Netty模型
上图说明:
- Netty抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写
- BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop
- NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个 NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯
- NioEventLoopGroup 可以有多个线程, 即可以含有多个 NioEventLoop
- 每个Boss NioEventLoop 循环执行的步骤有3步
- 轮询 accept 事件
- 处理 accept 事件 , 与 client 建立连接 , 生成NioScocketChannel , 并将其注册到某个 worker NIOEventLoop 上的 selector
- 处理任务队列的任务 , 即 runAllTasks
- 每个 Worker NIOEventLoop 循环执行的步骤
- 轮询read, write 事件
- 处理 i/o 事件, 即 read , write 事件,在对应NioScocketChannel 处理
- 处理任务队列的任务 , 即 runAllTasks
- 每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道), pipeline 中包含了 channel,即通过 pipeline 可以获取到对应 channel,管道中维护了很多的 handler
[1] 案例
需求:
- 实例要求:使用 IDEA 创建Netty项目
- Netty 服务器在 6668 端口监听,客户端能发送消息给服务器 “hello, 服务器~”
- 服务器可以回复消息给客户端 “hello, 服务器~”
**代码见:**simple
① 服务端
public class NettyServer {
public static void main(String[] args) throws Exception {
//创建BossGroup 和 WorkerGroup
//说明
//1. 创建两个线程组 bossGroup 和 workerGroup
//2. bossGroup 只是处理连接请求 , 真正的和客户端业务处理,会交给 workerGroup完成
//3. 两个都是无限循环
//4. bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
// 默认实际 cpu核数 * 2
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行设置
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
// .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)
//给pipeline 设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("客户socketchannel hashcode=" + ch.hashCode()); //可以使用一个集合管理 SocketChannel, 再推送消息时,可以将业务加入到各个channel 对应的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
ch.pipeline().addLast(new NettyServerHandler());
}
}); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器
System.out.println(".....服务器 is ready...");
//绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//给cf 注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口 6668 成功");
} else {
System.out.println("监听端口 6668 失败");
}
}
});
//对关闭通道进行监听
cf.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
② 服务端Handler
/*
说明
1. 我们自定义一个Handler 需要继续netty 规定好的某个HandlerAdapter(规范)
2. 这时我们自定义一个Handler , 才能称为一个handler
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据实际(这里我们可以读取客户端发送的消息)
/*
1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
2. Object msg: 就是客户端发送的数据 默认Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器读取线程 " + Thread.currentThread().getName() + " channle =" + ctx.channel());
System.out.println("server ctx =" + ctx);
System.out.println("看看channel 和 pipeline的关系");
Channel channel = ctx.channel();
ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链接, 出站入站
//将 msg 转成一个 ByteBuf
//ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer.
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:" + channel.remoteAddress());
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//writeAndFlush 是 write + flush
//将数据写入到缓存,并刷新
//一般讲,我们对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵1", CharsetUtil.UTF_8));
}
//处理异常, 一般是需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
③ 客户端
public class NettyClient {
public static void main(String[] args) throws Exception {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler()); //加入自己的处理器
}
});
System.out.println("客户端 ok..");
//启动客户端去连接服务器端
//关于 ChannelFuture 要分析,涉及到netty的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
④ 客户端 Handler
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client " + ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
}
//当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址: "+ ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
[2] Netty中Task的三种使用场景
**Task 的作用:**当我们有一个耗时的业务操作,它将会导致线程阻塞。如果将该操作放入 Task 进行异步处理将得以解决
**举例:**在上述案例中,Server 端的 Handler 类中有 channelRead()【读取数据业务操作】和 channelReadComplete() 【数据读取完成后的业务操作】,当 channelRead() 是一个耗时操作,那么将导致 channelReadComplete() 被阻塞;
类型:
- 用户程序自定义的普通任务【任务放在 taskQueue 中】
- 用户自定义定时任务 【任务放在 scheduleTaskQueue 中】
- 非当前 Reactor 线程调用 Channel 的各种方法
- 例如在推送系统的业务线程里面,根据用户的标识,找到对应的 Channel 引用,然后 调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的 Write 会提交到 任务队列中后被异步消费
① 用户程序自定义的普通任务
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//比如这里我们有一个非常耗时长的业务-> 异步执行 -> 提交该channel 对应的
//NIOEventLoop 的 taskQueue中,
//解决方案1 用户程序自定义的普通任务
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
});
}
}
② 用户自定义定时任务
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//比如这里我们有一个非常耗时长的业务-> 异步执行 -> 提交该channel 对应的
//NIOEventLoop 的 taskQueue中,
//解决方案2 : 用户自定义定时任务 -》 该任务是提交到 scheduleTaskQueue中
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
}, 5, TimeUnit.SECONDS);
}
}
2.8、异步模型
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者
Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture。
调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获 取或者通过通知机制获得 IO 操作结果
Netty 的异步模型是建立在 future 和 callback 的之上的。callback 就是回调。重点说 Future,它的核心思想是:假设一个方法 fun,计算过程可能非常耗时,等待 fun返回显然不合适。那么可以在调用 fun 的时候,立马返回一个 Future,后续可以通过 Future去监控方法 fun 的处理过程(即 : Future-Listener 机制)
[1] Future-Listener 机制
当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作。举例:我找理发师剪头发,但理发师在忙(说明无法立马返回),于是理发师给了我一个秘书(Future),如果理发师剪好了会通知我,而我可以去做别的事
常见有如下操作:
通过 isDone 方法来判断当前操作是否完成;
通过 isSuccess 方法来判断已完成的当前操作是否成功;
通过 getCause 方法来获取已完成的当前操作失败的原因;
通过 isCancelled 方法来判断已完成的当前操作是否被取消;
通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知 指定的监听器;如果 Future 对象已完成,则通知指定的监听器
[2] 举例
在 Netty 模型中的案例 Server 端体现:
public class NettyServer {
public static void main(String[] args) throws Exception {
.......
//绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//给cf 注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口 6668 成功");
} else {
System.out.println("监听端口 6668 失败");
}
}
});
//对关闭通道进行监听
cf.channel().closeFuture().sync();
........
}
}
2.9、Netty实现Http服务
需求:
- 实例要求:使用IDEA 创建Netty项目
- Netty 服务器在 6668 端口监听,浏览器发出请求 "http://localhost:8081/ "
- 服务器可以回复消息给客户端 "Hello! 我是服务器 5 " , 并对特定请求资源进行过滤(浏览器会发送两个请求,一个请求网站图标,一个真实请求)
[1] 服务端
public class TestServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new TestServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(8081).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
[2] Handler
/*
说明
1. SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter
2. HttpObject 客户端和服务器端相互通讯的数据被封装成 HttpObject
*/
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
//channelRead0 读取客户端数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
System.out.println("对应的channel=" + ctx.channel() + " pipeline=" + ctx
.pipeline() + " 通过pipeline获取channel" + ctx.pipeline().channel());
System.out.println("当前ctx的handler=" + ctx.handler());
//判断 msg 是不是 httprequest请求
if(msg instanceof HttpRequest) {
System.out.println("ctx 类型="+ctx.getClass());
System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + " TestHttpServerHandler hash=" + this.hashCode());
System.out.println("msg 类型=" + msg.getClass());
System.out.println("客户端地址" + ctx.channel().remoteAddress());
//获取到
HttpRequest httpRequest = (HttpRequest) msg;
//获取uri, 过滤指定的资源
URI uri = new URI(httpRequest.uri());
if("/favicon.ico".equals(uri.getPath())) {
System.out.println("请求了 favicon.ico, 不做响应");
return;
}
//回复信息给浏览器 [http协议]
ByteBuf content = Unpooled.copiedBuffer("hello, 我是服务器", CharsetUtil.UTF_8);
//构造一个http的相应,即 httpresponse
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
//将构建好 response返回
ctx.writeAndFlush(response);
}
}
}
[3] 服务初始化类
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//向管道加入处理器
//得到管道
ChannelPipeline pipeline = ch.pipeline();
//加入一个netty 提供的httpServerCodec codec =>[coder - decoder]
//HttpServerCodec 说明
//1. HttpServerCodec 是netty 提供的处理http的 编-解码器
pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
//2. 增加一个自定义的handler
pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());
System.out.println("ok~~~~");
}
}
3、Netty核心模块组件
3.1、Bootstrap、ServerBootstrap
- Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类, ServerBootstrap 是服务端启动引导类
- 常见的方法有:
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup),该方法
用于服务器端,用来设置两个 EventLoop
public B group(EventLoopGroup group) ,该方法用于客户端,用来设置一个 EventLoop
public B channel(Class<? extends C> channelClass),该方法用来设置一个服务器端的通道实现
public <T> B option(ChannelOption<T> option, T value),用来给 ServerChannel 添加配置
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value),用来给接收到的
通道添加配置
public ServerBootstrap childHandler(ChannelHandler childHandler),该方法用来设置业务处理类
(自定义的 handler)
public ChannelFuture bind(int inetPort) ,该方法用于服务器端,用来设置占用的端口号
public ChannelFuture connect(String inetHost, int inetPort) ,该方法用于客户端,用来连接服务器
3.2、Future、ChannelFuture
Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFuture,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。【举例】:我找理发师剪头发,但理发师在忙(说明无法立马返回),于是理发师给了我一个秘书(Future),如果理发师剪好了会通知我,而我可以去做别的事
常见方法:
- Channel channel(),返回当前正在进行 IO 操作的通道
- ChannelFuture sync(),等待异步操作执行完毕
3.3、Channel
- Netty 网络通信的组件,能够用于执行网络 I/O 操作
- 通过Channel 可获得当前网络连接的通道的状态
- 通过Channel 可获得 网络连接的配置参数 (例如接收缓冲区大小)
- Channel 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着 任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成
- 调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方
- 支持关联 I/O 操作与对应的处理程序
- 不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,常用的 Channel 类型:
Channel 类型 | 作用 |
---|---|
NioSocketChannel | 异步的客户端 TCP Socket 连接 |
NioServerSocketChannel | 异步的服务器端 TCP Socket 连接 |
NioDatagramChannel | 异步的 UDP 连接 |
NioSctpChannel | 异步的客户端 Sctp 连接 |
NioSctpServerChannel | 异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO |
3.4、Selector
- Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件
- 当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询 (Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接 完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel
3.5、ChannelHandler及其实现类
ChannelHandler 是真实处理业务的组件,一个 Channel 对应一个 Pipeline,一个 Pipeline 中含有多个 ChannelHandler ,它们是以链式的方式串联执行的。举例:接力赛
Handler 类型 | 作用 |
---|---|
ChannelInboundHandler | 用于处理入站 I/O 事件 |
ChannelOutboundHandler | 用于 处理出站 I/O 操作 |
ChannelInboundHandlerAdapter | 用于处理入站 I/O 事件 |
ChannelOutboundHandlerAdapt | 用于处理出站 I/O 操作 |
ChannelDuplexHandler | 用于处理 入站和出站事件 |
什么是出入站:从 Handler 头部到尾部为入站,反之出站
3.6、Pipeline、ChannelPipeline
ChannelPipeline 用于处理或拦截 Channel 的入站事件和出站操作
Channel、ChannelPipeline、ChannelHandler、ChannelHandlerContextr 关系:
- 一个 Channel 对应一个 ChannelPipeline
- 一个 ChannelPipeline 包含多个 ChannelHandler
- 一个 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表
- 一个 ChannelHandlerContext 中又关联着一个 ChannelHandler,并保存了 Channel 相关信息
// 常用方法
ChannelPipeline addFirst(ChannelHandler... handlers); //把一个业务处理类(handler) 添加到链中的第一个位置
ChannelPipeline addLast(ChannelHandler... handlers); //把一个业务处理类(handler) 添加到链中的最后一个位置
3.7、ChannelHandlerContext
- 保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象,同时 ChannelHandlerContext 中也绑定了对应的 pipeline 和 Channel 的信息,方便对 ChannelHandler 进行调用
- 常用方法:
方法 | 说明 |
---|---|
close() | 关闭通道 |
flush() | flush() |
writeAndFlush(Object msg) | 将数据写到 ChannelPipeline 中当前 ChannelHandler 的下一个 ChannelHandler 开始处理(出站) |
3.8、ChannelOption
- Netty 在创建 Channel 实例后,一般都需要设置 ChannelOption 参
- ChannelOption 参数如下:
参数 | 作用 |
---|---|
ChannelOption.SO_BACKLOG | 对应 TCP/IP 协议 listen 函数中的 backlog 参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户 端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog 参数指定了队列的大小 |
ChannelOption.SO_KEEPALIVE | 一直保持连接活动状态 |
3.8、EventLoopGroup、NioEventLoopGroup
- EventLoopGroup 是一组 EventLoop 的抽象,Netty 为了更好的利用多核 CPU 资源, 一般会有多个 EventLoop 同时工作,每个 EventLoop 维护着一个 Selector 实例。
- EventLoopGroup 提供 next 接口,可以从组里面按照一定规则获取其中一个 EventLoop来处理任务。在 Netty 服务器端编程中,我们一般都需要提供两个 EventLoopGroup,例如:BossEventLoopGroup 和 WorkerEventLoopGroup。
类型 | 说明 |
---|---|
BossEventLoopGroup | 通常是一个单线程的 EventLoop,EventLoop 维护着一 个注册了ServerSocketChannel 的 Selector 实例BossEventLoop 不断轮询Selector 将连接事件分离出来 、通常是 OP_ACCEPT 事件,然后将接收到的 SocketChannel交给 WorkerEventLoopGroup |
WorkerEventLoopGroup | 会由 next 选择其中一个 EventLoop 来将这个 SocketChannel 注册到其维护的 Selector 并对其后续的 IO 事件进行处理 |
// 常用方法
public NioEventLoopGroup(); //构造方法
public Future<?> shutdownGracefully(); //断开连接,关闭线程
3.9、Unpooled 类
**作用:**Netty 提供一个专门用来操作缓冲区(Buffer)(即Netty的数据容器)的工具类
[1] ByteBuf
0 <= readIndex <= writeIndex <= capacity
属性 | 说明 |
---|---|
capacity | 容量 |
readIndex | 下一个读的位置 |
writeIndex | 下一个写的位置 |
[2] 两种方式获取ByteBuf对比
方式 | 区别 |
---|---|
ByteBuf buffer = Unpooled.buffer(10); | capacity = 10 |
ByteBuf byteBuf = Unpooled.copiedBuffer(xxx,xxx) | capacity = 36 |
[3] ByteBuffer、ByteBuf对比
ByteBuffer | ByteBuf | |
---|---|---|
提供方 | NIO | Netty |
作用 | 写出写出数据 | 写出写出数据 |
属性 | capacity:容量 limit:最大读写范围 position:下一个读写的位置 mark:标记 | capacity:容量 readIndex:下一个读的位置 writeIndex:下一个写的位置 |
属性关系 | mark <= position <= limit <= capacity | 0 <= readIndex <= writeIndex <= capacity |
[4] 代码演示
public class NettyByteBuf01 {
public static void main(String[] args) {
//创建一个ByteBuf
//说明
//1. 创建 对象,该对象包含一个数组arr , 是一个byte[10]
//2. 在netty 的buffer中,不需要使用flip 进行反转
// 底层维护了 readerindex 和 writerIndex
//3. 通过 readerindex 和 writerIndex 和 capacity, 将buffer分成三个区域
// 0---readerindex 已经读取的区域
// readerindex---writerIndex , 可读的区域
// writerIndex -- capacity, 可写的区域
ByteBuf buffer = Unpooled.buffer(10);
for(int i = 0; i < 10; i++) {
buffer.writeByte(i);
}
System.out.println("capacity=" + buffer.capacity());//10
//输出
//for(int i = 0; i<buffer.capacity(); i++) {
// System.out.println(buffer.getByte(i));
//}
for(int i = 0; i < buffer.capacity(); i++) {
System.out.println(buffer.readByte());
}
System.out.println("执行完毕");
}
}
public class NettyByteBuf02 {
public static void main(String[] args) {
//创建ByteBuf
ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world!", Charset.forName("utf-8"));
//使用相关的方法
if(byteBuf.hasArray()) { // true
byte[] content = byteBuf.array();
//将 content 转成字符串
System.out.println(new String(content, Charset.forName("utf-8")));
System.out.println("byteBuf=" + byteBuf);
System.out.println(byteBuf.arrayOffset()); // 0
System.out.println(byteBuf.readerIndex()); // 0
System.out.println(byteBuf.writerIndex()); // 12
System.out.println(byteBuf.capacity()); // 36
//System.out.println(byteBuf.readByte()); // redaIndex + 1
System.out.println(byteBuf.getByte(0)); // readIndex 不变
int len = byteBuf.readableBytes(); //可读的字节数 12
System.out.println("len=" + len);
//使用for取出各个字节
for(int i = 0; i < len; i++) {
System.out.println((char) byteBuf.getByte(i));
}
//按照某个范围读取【从哪儿开始,读几个】
System.out.println(byteBuf.getCharSequence(0, 4, Charset.forName("utf-8")));
System.out.println(byteBuf.getCharSequence(4, 6, Charset.forName("utf-8")));
}
}
}
3.10、群聊系统
需求:
- 编写一个 Netty 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
- 实现多人群聊
- 服务器端:可以监测用户上线,离线,并实现消息转发功能
- 客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用 户发送的消息(有服务器转发得到)
[1] 服务端
public class GroupChatServer {
private int port; //监听端口
public GroupChatServer(int port) {
this.port = port;
}
//编写run方法,处理客户端的请求
public void run() throws Exception{
//创建两个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8个NioEventLoop
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//获取到pipeline
ChannelPipeline pipeline = ch.pipeline();
//向pipeline加入解码器
pipeline.addLast("decoder", new StringDecoder());
//向pipeline加入编码器
pipeline.addLast("encoder", new StringEncoder());
//加入自己的业务处理handler
pipeline.addLast(new GroupChatServerHandler());
}
});
System.out.println("netty 服务器启动");
ChannelFuture channelFuture = b.bind(port).sync();
//监听关闭
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new GroupChatServer(7000).run();
}
}
[2] 服务端Handler
public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {
// 私聊方案:使用一个 hashmap 管理
//public static Map<String, Channel> channels = new HashMap<String,Channel>();
//public static Map<User, Channel> channels = new HashMap<String,Channel>();
//定义一个channel 组,管理所有的channel
//GlobalEventExecutor.INSTANCE) 是全局的事件执行器,是一个单例
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//handlerAdded 表示连接建立,一旦连接,第一个被执行
//将当前channel 加入到 channelGroup
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
//将该客户加入聊天的信息推送给其它在线的客户端
/*
该方法会将 channelGroup 中所有的channel 遍历,并发送 消息,
我们不需要自己遍历
*/
channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 加入聊天" + sdf.format(new java.util.Date()) + " \n");
channelGroup.add(channel);
}
//断开连接, 将xx客户离开信息推送给当前在线的客户
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 离开了\n");
System.out.println("channelGroup size" + channelGroup.size());
}
//表示channel 处于活动状态, 提示 xx上线
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " 上线了~");
}
//表示channel 处于不活动状态, 提示 xx离线了
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " 离线了~");
}
//读取数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
//获取到当前channel
Channel channel = ctx.channel();
//这时我们遍历channelGroup, 根据不同的情况,回送不同的消息
channelGroup.forEach(ch -> {
if(channel != ch) { //不是当前的channel,转发消息
ch.writeAndFlush("[客户]" + channel.remoteAddress() + " 发送了消息" + msg + "\n");
}else {//回显自己发送的消息给自己
ch.writeAndFlush("[自己]发送了消息" + msg + "\n");
}
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//关闭通道
ctx.close();
}
}
[3] 客户端
public class GroupChatClient {
//属性
private final String host;
private final int port;
public GroupChatClient(String host, int port) {
this.host = host;
this.port = port;
}
public void run() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//得到pipeline
ChannelPipeline pipeline = ch.pipeline();
//加入相关handler
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
//加入自定义的handler
pipeline.addLast(new GroupChatClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
//得到channel
Channel channel = channelFuture.channel();
System.out.println("-------" + channel.localAddress() + "--------");
//客户端需要输入信息,创建一个扫描器
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
//通过channel 发送到服务器端
channel.writeAndFlush(msg + "\r\n");
}
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new GroupChatClient("127.0.0.1", 7000).run();
}
}
[4] 客户端Handler
public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(msg.trim());
}
}
3.11、心跳检测机制
**心跳检测机制:**当一个事件(读、写、读写)在指定时间没有发生时,会触发心跳检测,并根据具体的事件做一些处理
**提问:**类似于 SimpleChannelInboundHandler 类提供了 handlerRemoved()、handlerAdded() 等针对事件的方法,为什么还要心跳检测
**回答:**如果在手机端,我突然重启手机,通过上述方法并不一定能检测到
名称 | 作用 |
---|---|
IdleStateHandler | 当连接的空闲时间(read 或 write)太长时,将会触发一个 IdleStateEvent 事件。然后,你可以通过你的 ChannelInboundHandler 中重写 userEventTrigged方法来处理该事件 |
ReadTimeoutHandler | 如果在指定的事件没有发生 read事件,就会抛出这个以常,并自动关闭这个连接,你可以在 exceptionCaught方法中处理这个异常 |
WriteTimeoutHandler | 当一个 write操作不能在一定的时间内完成时,抛出异常,并关闭连接。你同时可以在 exceptionCaught方法中处理这个异常 |
[1] IdleStateHandler 类
IdleStateHandler 参数 | 说明 |
---|---|
readerIdleTime | 表示多长时间没有读, 就会发送一个心跳检测包检测是否连接 |
writerIdleTime | 表示多长时间没有写, 就会发送一个心跳检测包检测是否连接 |
allIdleTime | 表示多长时间没有读写, 就会发送一个心跳检测包检测是否连接 |
[2] 代码举例
**说明:**以 3.10 群聊系统为例
需求:
- 编写一个 Netty 心跳检测机制案例, 当服务器超过 3 秒没有读时,就提示读空闲
- 当服务器超过 5 秒没有写操作时,就提示写空闲
- 实现当服务器超过 7 秒没有读或者写操作时,就提示读写空闲
public class MyServer {
public static void main(String[] args) throws Exception{
.......
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//加入一个netty 提供 IdleStateHandler
/*
说明
1. IdleStateHandler 是netty 提供的处理空闲状态的处理器
2. long readerIdleTime : 表示多长时间没有读, 就会发送一个心跳检测包检测是否连接
3. long writerIdleTime : 表示多长时间没有写, 就会发送一个心跳检测包检测是否连接
4. long allIdleTime : 表示多长时间没有读写, 就会发送一个心跳检测包检测是否连接
5. 当 IdleStateEvent 触发后 , 就会传递给管道 的下一个handler去处理
通过调用(触发)下一个handler 的 userEventTiggered , 在该方法中去处理 IdleStateEvent(读空闲,写空闲,读写空闲)
*/
pipeline.addLast(new IdleStateHandler(7000,7000,10, TimeUnit.SECONDS));
//加入一个对空闲检测进一步处理的handler(自定义)
pipeline.addLast(new MyServerHandler());
}
});
......
}
}
public class MyServerHandler extends ChannelInboundHandlerAdapter {
/**
*
* @param ctx 上下文
* @param evt 事件
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent) {
//将 evt 向下转型 IdleStateEvent
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()) {
case READER_IDLE:
eventType = "读空闲";
break;
case WRITER_IDLE:
eventType = "写空闲";
break;
case ALL_IDLE:
eventType = "读写空闲";
break;
}
System.out.println(ctx.channel().remoteAddress() + "--超时时间--" + eventType);
System.out.println("服务器做相应处理..");
//如果发生空闲,我们关闭通道
// ctx.channel().close();
}
}
}
3.12、Netty通过WebSocket实现客户端长连接案例
**为什么使用长连接:**Http协议是无状态的, 浏览器和服务器间的请求响应一次,下一次会重新创建连接.
需求:
- 实现基于webSocket的长连接的全双工的交互
- 改变Http协议多次请求的约束,实现长连接了, 服务器可以发送消息给浏览器
- 客户端浏览器和服务器端会相互感 知,比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器 会感知
[1] 服务端
public class MyServer {
public static void main(String[] args) throws Exception{
//创建两个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8个NioEventLoop
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//因为基于http协议,使用http的编码和解码器
pipeline.addLast(new HttpServerCodec());
//是以块方式写,添加ChunkedWriteHandler处理器
pipeline.addLast(new ChunkedWriteHandler());
/*
说明
1. http数据在传输过程中是分段, HttpObjectAggregator ,就是可以将多个段聚合
2. 这就就是为什么,当浏览器发送大量数据时,就会发出多次http请求
*/
pipeline.addLast(new HttpObjectAggregator(8192));
/*
说明
1. 对应websocket ,它的数据是以帧(frame) 形式传递
2. 可以看到WebSocketFrame 下面有六个子类
3. 浏览器请求时 ws://localhost:7000/hello 表示请求的uri
4. WebSocketServerProtocolHandler 核心功能是将 http协议升级为 ws协议 , 保持长连接
5. 是通过一个 状态码 101
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
//自定义的handler ,处理业务逻辑
pipeline.addLast(new MyTextWebSocketFrameHandler());
}
});
//启动服务器
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
[2] 处理器
//这里 TextWebSocketFrame 类型,表示一个文本帧(frame)
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("服务器收到消息 " + msg.text());
//回复消息
ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间" + LocalDateTime.now() + " " + msg.text()));
}
//当web客户端连接后, 触发方法
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
//id 表示唯一的值,LongText 是唯一的 ShortText 不是唯一
System.out.println("handlerAdded 被调用" + ctx.channel().id().asLongText());
System.out.println("handlerAdded 被调用" + ctx.channel().id().asShortText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved 被调用" + ctx.channel().id().asLongText());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常发生 " + cause.getMessage());
ctx.close(); //关闭连接
}
}
[3] 前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
var socket;
//判断当前浏览器是否支持websocket
if (window.WebSocket) {
//go on
socket = new WebSocket("ws://localhost:7000/hello");
//相当于channelReado, ev 收到服务器端回送的消息
socket.onmessage = function (ev) {
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + ev.data;
}
//相当于连接开启(感知到连接开启)
socket.onopen = function (ev) {
var rt = document.getElementById("responseText");
rt.value = "连接开启了.."
}
//相当于连接关闭(感知到连接关闭)
socket.onclose = function (ev) {
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + "连接关闭了.."
}
} else {
alert("当前浏览器不支持websocket")
}
//发送消息到服务器
function send(message) {
if (!window.socket) { //先判断socket是否创建好
return;
}
if (socket.readyState == WebSocket.OPEN) {
//通过socket 发送消息
socket.send(message)
} else {
alert("连接没有开启");
}
}
</script>
<form onsubmit="return false">
<textarea name="message" style="height: 300px; width: 300px"></textarea>
<input type="button" value="发送消息" onclick="send(this.form.message.value)">
<textarea id="responseText" style="height: 300px; width: 300px"></textarea>
<input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
</form>
</body>
</html>
4、Google Protobu
4.1、编码和解码
- 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送 数据时就需要编码,接收数据时就需要解码 [示意图]
- codec(编解码器) 的组成部分有两个:decoder(解码器)和 encoder(编码器)。encoder 负责把业务数据转换成字节码数据,decoder 负责把字节码数据转换成 业务数据
4.2、Netty本身编解码器机制和问题
编码器 | 解码器 |
---|---|
StringEncoder:对字符串数据进行编码 ObjectEncoder:对 Java 对象进行编码 ······· | StringDecoder:对字符串数据进行解码 ObjectDecoder:对 Java 对象进行解 ······· |
**机制:**Netty 本身自带的 ObjectDecoder 和 ObjectEncoder 可以用来实现 POJO 对象或各种业务对象的编码和解码,底层使用的仍是 Java 序列化技术 , 而Java 序列化技术本身效率就不高
存在问题:
- 无法跨语言
- 序列化后的体积太大,是二进制编码的 5 倍多。
- 序列化性能太低
4.2、Protobuf 介绍
- Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers,是一种轻便高 效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做 数据存储或 RPC[远程过程调用 remote procedure call ] 数据交换格式 。 目前很多公司 http+json → tcp+protobuf
- 参考文档 : https://developers.google.com/protocol-buffers/docs/proto 语言指南
- Protobuf 是以 message 的方式来管理数据的.
- 支持跨平台、跨语言,即[客户端和服务器端可以是不同的语言编写的] (支持目前绝 大多数语言,例如 C++、C#、Java、python 等)
- 高性能,高可靠性
- 使用 protobuf 编译器能自动生成代码,Protobuf 是将类的定义使用.proto 文件进行描 述。说明,在idea 中编写 .proto 文件时,会自动提示是否下载 .ptotot 编写插件. 可 以让语法高亮。
- 然后通过 protoc.exe 编译器根据.proto 自动生成.java 文件
4.3、案例演示
[1] 传输单一固定类型
需求:
- 客户端可以发送一个Student PoJo 对象到服 务器 (通过 Protobuf 编码)
- 服务端能接收Student PoJo 对象,并显示信 息(通过 Protobuf 解码)
步骤:
- 编写 .proto 文件
- 通过 protoc.exe 编译器根据 .proto 自动生成 .java 文件
- 将 .java 文件复制到项目中
- 编写业务代码
① Student.proto
syntax = "proto3"; //版本
option java_outer_classname = "StudentPOJO";//生成的外部类名,同时也是文件名
//protobuf 使用message 管理数据
message Student { //会在 StudentPOJO 外部类生成一个内部类 Student, 他是真正发送的POJO对象
int32 id = 1; // Student 类中有 一个属性 名字为 id 类型为int32(protobuf类型) 1表示属性序号,不是值
string name = 2;
}
② 生成 .java 文件
# 格式
protoc.exe --java_out=. 文件名
# 举例
protoc.exe --java_out=. Student.proto
③ 拷贝到项目中
④ 服务端
public class NettyServer {
public static void main(String[] args) throws Exception {
//创建BossGroup 和 WorkerGroup
//说明
//1. 创建两个线程组 bossGroup 和 workerGroup
//2. bossGroup 只是处理连接请求 , 真正的和客户端业务处理,会交给 workerGroup完成
//3. 两个都是无限循环
//4. bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
// 默认实际 cpu核数 * 2
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行设置
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
// .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)
//给pipeline 设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//在pipeline加入ProtoBufDecoder
//指定对哪种对象进行解码
pipeline.addLast("decoder", new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
pipeline.addLast(new NettyServerHandler());
}
}); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器
System.out.println(".....服务器 is ready...");
//绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//给cf 注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口 6668 成功");
} else {
System.out.println("监听端口 6668 失败");
}
}
});
//对关闭通道进行监听
cf.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
⑤ 服务端 Handler
/*
说明
1. 我们自定义一个Handler 需要继续netty 规定好的某个HandlerAdapter(规范)
2. 这时我们自定义一个Handler , 才能称为一个handler
*/
//public class NettyServerHandler extends ChannelInboundHandlerAdapter {
public class NettyServerHandler extends SimpleChannelInboundHandler<StudentPOJO.Student> {
//读取数据实际(这里我们可以读取客户端发送的消息)
/*
1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
2. Object msg: 就是客户端发送的数据 默认Object
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, StudentPOJO.Student msg) throws Exception {
//读取从客户端发送的StudentPojo.Student
System.out.println("客户端发送的数据 id=" + msg.getId() + " 名字=" + msg.getName());
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//writeAndFlush 是 write + flush
//将数据写入到缓存,并刷新
//一般讲,我们对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵1", CharsetUtil.UTF_8));
}
//处理异常, 一般是需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
⑥ 客户端
public class NettyClient {
public static void main(String[] args) throws Exception {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//在pipeline中加入 ProtoBufEncoder
pipeline.addLast("encoder", new ProtobufEncoder());
pipeline.addLast(new NettyClientHandler()); //加入自己的处理器
}
});
System.out.println("客户端 ok..");
//启动客户端去连接服务器端
//关于 ChannelFuture 要分析,涉及到netty的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
⑦ 客户端 Handler
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//发生一个Student 对象到服务器
StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setId(4).setName("智多星 吴用").build();
//Teacher , Member ,Message
ctx.writeAndFlush(student);
}
//当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址: "+ ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
[2] 随机传输多种类型
客户端可以随机发送Student PoJo/ Worker PoJo 对象到服务器 (通过 Protobuf 编码)
服务端能接收Student PoJo/ Worker PoJo 对象(需要判断是哪种类型),并显示信息(通过 Protobuf 解码)
① Student.proto
syntax = "proto3";
option optimize_for = SPEED; // 加快解析
option java_package="netty.codec2"; //指定生成到哪个包下
option java_outer_classname="MyDataInfo"; // 外部类名, 文件名
//protobuf 可以使用message 管理其他的message
message MyMessage {
//定义一个枚举类型
enum DataType {
StudentType = 0; //在proto3 要求enum的编号从0开始
WorkerType = 1;
}
//用data_type 来标识传的是哪一个枚举类型
DataType data_type = 1;
//表示每次枚举类型最多只能出现其中的一个, 节省空间
oneof dataBody {
Student student = 2;
Worker worker = 3;
}
}
message Student {
int32 id = 1;//Student类的属性
string name = 2; //
}
message Worker {
string name=1;
int32 age=2;
}
② 生成 .java 文件
③ 拷贝到项目中
④ 服务端
public class NettyServer {
public static void main(String[] args) throws Exception {
//创建BossGroup 和 WorkerGroup
//说明
//1. 创建两个线程组 bossGroup 和 workerGroup
//2. bossGroup 只是处理连接请求 , 真正的和客户端业务处理,会交给 workerGroup完成
//3. 两个都是无限循环
//4. bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
// 默认实际 cpu核数 * 2
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行设置
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
// .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)
//给pipeline 设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//在pipeline加入ProtoBufDecoder
//指定对哪种对象进行解码
pipeline.addLast("decoder", new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance()));
pipeline.addLast(new NettyServerHandler());
}
}); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器
System.out.println(".....服务器 is ready...");
//绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//给cf 注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口 6668 成功");
} else {
System.out.println("监听端口 6668 失败");
}
}
});
//对关闭通道进行监听
cf.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
⑤ 服务端Handler
public class NettyServerHandler extends SimpleChannelInboundHandler<MyDataInfo.MyMessage> {
//读取数据实际(这里我们可以读取客户端发送的消息)
/*
1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
2. Object msg: 就是客户端发送的数据 默认Object
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, MyDataInfo.MyMessage msg) throws Exception {
//根据dataType 来显示不同的信息
MyDataInfo.MyMessage.DataType dataType = msg.getDataType();
if(dataType == MyDataInfo.MyMessage.DataType.StudentType) {
MyDataInfo.Student student = msg.getStudent();
System.out.println("学生id=" + student.getId() + " 学生名字=" + student.getName());
} else if(dataType == MyDataInfo.MyMessage.DataType.WorkerType) {
MyDataInfo.Worker worker = msg.getWorker();
System.out.println("工人的名字=" + worker.getName() + " 年龄=" + worker.getAge());
} else {
System.out.println("传输的类型不正确");
}
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//writeAndFlush 是 write + flush
//将数据写入到缓存,并刷新
//一般讲,我们对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵1", CharsetUtil.UTF_8));
}
//处理异常, 一般是需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
⑥ 客户端
public class NettyClient {
public static void main(String[] args) throws Exception {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//在pipeline中加入 ProtoBufEncoder
pipeline.addLast("encoder", new ProtobufEncoder());
pipeline.addLast(new NettyClientHandler()); //加入自己的处理器
}
});
System.out.println("客户端 ok..");
//启动客户端去连接服务器端
//关于 ChannelFuture 要分析,涉及到netty的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
⑦ 客户端Handler
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//随机的发送Student 或者 Workder 对象
int random = new Random().nextInt(3);
MyDataInfo.MyMessage myMessage = null;
if(0 == random) { //发送Student 对象
myMessage = MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.StudentType).setStudent(MyDataInfo.Student.newBuilder().setId(5).setName("玉麒麟 卢俊义").build()).build();
} else { // 发送一个Worker 对象
myMessage = MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.WorkerType).setWorker(MyDataInfo.Worker.newBuilder().setAge(20).setName("老李").build()).build();
}
ctx.writeAndFlush(myMessage);
}
//当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址: "+ ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
5、Netty编解码机制
详细的使用请看 【Handler 调用机制】中的案例
类型 | 需重写的方法 | |
---|---|---|
ByteToMessageEncoder | 编码器 | decode() |
ByteToMessageDecoder | 解码器 | encode() |
ReplayingDecoder | 解码器 | decode() |
5.1、说明(以解码器为例)
- 编解码器本质是一个 Handler
- 以入站为例,对于每个从入站 Channel 读取的消息,这个方法会被调用。随后, 它将调用由解码器所提供的 decode() 方法进行解码,并将已经解码的字节转发给 ChannelPipeline 中的下一个 Handler (链式调用),链式调用—见 Handler 机制
- 由于不可能知道远程节点是否会一次性发送一个完整的信息, TCP 有可能出现粘包拆包的问题, 这个类会对入站数据进行缓冲, 直到它准备好被处理,见粘包拆包章节
5.2、代码演示(以解码器为例)
public class ToIntegerDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception{
if (in.readableBytes() >= 4) {
out.add(in.readInt());
}
}
}
**文字说明:**这个例子,每次入站从 ByteBuf 中读取 4 字节(因为 int 占 4 个字节),将其解码为一个 int,然后将它添加到下一个 List中。 当没有更多元素可以被添加到该 List 中时,它的内容将会被发送给下一个 ChannelInboundHandler 。 int在被添加到 List 中时,会被自动装箱为 Integer。在调用 readInt() 方法前必须验证所输入的 ByteBuf 是否具有足够的数据
5.3、ReplayingDecoder
说明:ReplayingDecoder 扩展了 ByteToMessageDecoder 类,使用这个类,我们不必调用 readableBytes() 方法。参数 S 指定了用户状态管理的类型,其中 Void 代表不需要状态管理(状态管理:解码的类型,如 int、long)
代码演示(对 5.2 的改进):
public class MyByteToLongDecoder2 extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//在 ReplayingDecoder 不需要判断数据是否足够读取,内部会进行处理判断
out.add(in.readInt());
}
}
5.4、其它解码器
类型 | 说明 |
---|---|
LineBasedFrameDecoder | 这个类在Netty内 部也有使用,它使用行尾控制字符(\n或者\r\n) 作为分隔符来解析数据 |
DelimiterBasedFrameDecoder | 使用自定义 的特殊字符作为消息的分隔符 |
HttpObjectDecoder | 一个HTTP数据的解码器 |
LengthFieldBasedFrameDecoder | 通过指定 长度来标识整包消息,这样就可以自动的处理 黏包和半包消息 |
6、Handler 调用机制
Handler 采用链式调用执行的机制。举例:接力赛
需求:
- 客户端发送 long 型数据 ----> 服务器
- 服务端发送 long 型数据 ----> 客户端
**代码见:**inoutboundhandler
6.1、服务端
[1] Server
public class MyServer {
public static void main(String[] args) throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new MyServerInitializer()); //自定义一个初始化类
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
[2] 初始化器
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();//一会下断点
//入站的handler进行解码 MyByteToLongDecoder
//pipeline.addLast(new MyByteToLongDecoder());
pipeline.addLast(new MyByteToLongDecoder2());
//出站的handler进行编码
pipeline.addLast(new MyLongToByteEncoder());
//自定义的handler 处理业务逻辑
pipeline.addLast(new MyServerHandler());
System.out.println("xx");
}
}
[3] 处理器
public class MyServerHandler extends SimpleChannelInboundHandler<Long> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
System.out.println("从客户端" + ctx.channel().remoteAddress() + " 读取到long " + msg);
//给客户端发送一个long
ctx.writeAndFlush(98765L);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
6.2、服务端
[1] Client
public class MyClient {
public static void main(String[] args) throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class)
.handler(new MyClientInitializer()); //自定义一个初始化类
ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
[2] 初始化器
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//加入一个出站的handler 对数据进行一个编码
pipeline.addLast(new MyLongToByteEncoder());
//这时一个入站的解码器(入站handler )
//pipeline.addLast(new MyByteToLongDecoder());
pipeline.addLast(new MyByteToLongDecoder2());
//加入一个自定义的handler , 处理业务
pipeline.addLast(new MyClientHandler());
}
}
[3] 处理器
public class MyClientHandler extends SimpleChannelInboundHandler<Long> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
System.out.println("服务器的ip=" + ctx.channel().remoteAddress());
System.out.println("收到服务器消息=" + msg);
}
//重写channelActive 发送数据
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("MyClientHandler 发送数据");
//ctx.writeAndFlush(Unpooled.copiedBuffer(""))
ctx.writeAndFlush(123456L); //发送的是一个long
}
}
6.3、编解码器
[1] 编译器
public class MyLongToByteEncoder extends MessageToByteEncoder<Long> {
//编码方法
@Override
protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception {
System.out.println("MyLongToByteEncoder encode 被调用");
System.out.println("msg=" + msg);
out.writeLong(msg);
}
}
[2] 解码器
public class MyByteToLongDecoder2 extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("MyByteToLongDecoder2 被调用");
//在 ReplayingDecoder 不需要判断数据是否足够读取,内部会进行处理判断
out.add(in.readLong());
}
}
7、TCP粘包和拆包及解决方案
**简而言之:**假设我说了两句话 “吕弘”,“刘璐”,两句话间隔很短【粘包】;听的人可能就听成了 “吕”,“弘刘璐”
- TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端) 都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发 给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
- 由于TCP无消息保护边界, 需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题
- TCP粘包、拆包图解:
7.1、代码演示粘包拆包现象
**关键代码:**客户端处理器中 channelActive() 循环发送的数据
public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
private int count;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//使用客户端发送10条数据 hello,server 编号
for(int i= 0; i< 10; ++i) {
ByteBuf buffer = Unpooled.copiedBuffer("hello,server " + i, Charset.forName("utf-8"));
ctx.writeAndFlush(buffer);
}
}
.........
}
[1] 服务端
① Server
public class MyServer {
public static void main(String[] args) throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new MyServerInitializer()); //自定义一个初始化类
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
② 初始化器
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyServerHandler());
}
}
③ 处理器
public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf>{
private int count;// 标记接收次数
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//cause.printStackTrace();
ctx.close();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
byte[] buffer = new byte[msg.readableBytes()];
msg.readBytes(buffer);
//将buffer转成字符串
String message = new String(buffer, Charset.forName("utf-8"));
System.out.println("服务器接收到数据 " + message);
System.out.println("服务器接收到消息量=" + (++this.count));
//服务器回送数据给客户端, 回送一个随机id ,
ByteBuf responseByteBuf = Unpooled.copiedBuffer(UUID.randomUUID().toString() + " ", Charset.forName("utf-8"));
ctx.writeAndFlush(responseByteBuf);
}
}
[2] 客户端
① Client
public class MyClient {
public static void main(String[] args) throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class)
.handler(new MyClientInitializer()); //自定义一个初始化类
ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
② 初始化器
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyClientHandler());
}
}
③ 处理器
public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
private int count;// 标记接收次数
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//使用客户端发送10条数据 hello,server 编号
for(int i= 0; i< 10; ++i) {
ByteBuf buffer = Unpooled.copiedBuffer("hello,server " + i, Charset.forName("utf-8"));
ctx.writeAndFlush(buffer);
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
byte[] buffer = new byte[msg.readableBytes()];
msg.readBytes(buffer);
String message = new String(buffer, Charset.forName("utf-8"));
System.out.println("客户端接收到消息=" + message);
System.out.println("客户端接收消息数量=" + (++this.count));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
7.2、解决方案
解决方案:自定义协议 + 编解码器。解码时,在解码器中获取数据长度,只读取该数据长度的数据
需求(对 7.1 进行改进):
- 要求客户端发送 5 个 Message 对象, 客户端每次发送一个 Message 对象
- 服务器端每次接收一个 Message, 分 5 次进行解码, 每读取到 一个 Message , 会回 复一个 Message 对象 给客户端
**代码位置:**protocoltcp
[1] 服务端
① Server
public class MyServer {
public static void main(String[] args) throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new MyServerInitializer()); //自定义一个初始化类
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
② 初始化器
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyMessageDecoder());//解码器
pipeline.addLast(new MyMessageEncoder());//编码器
pipeline.addLast(new MyServerHandler());
}
}
③ 处理器
//处理业务的handler
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol>{
private int count;
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//cause.printStackTrace();
ctx.close();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
//接收到数据,并处理
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println();
System.out.println();
System.out.println("服务器接收到信息如下");
System.out.println("长度=" + len);
System.out.println("内容=" + new String(content, Charset.forName("utf-8")));
System.out.println("服务器接收到消息包数量=" + (++this.count));
//回复消息
String responseContent = UUID.randomUUID().toString();
int responseLen = responseContent.getBytes("utf-8").length;
byte[] responseContent2 = responseContent.getBytes("utf-8");
//构建一个协议包
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(responseLen);
messageProtocol.setContent(responseContent2);
ctx.writeAndFlush(messageProtocol);
}
}
[2] 客户端
① Client
public class MyClient {
public static void main(String[] args) throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class)
.handler(new MyClientInitializer()); //自定义一个初始化类
ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
② 初始化器
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyMessageEncoder()); //加入编码器
pipeline.addLast(new MyMessageDecoder()); //加入解码器
pipeline.addLast(new MyClientHandler());
}
}
③ 处理器
public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
private int count;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//使用客户端发送10条数据 "今天天气冷,吃火锅" 编号
for(int i = 0; i< 5; i++) {
String mes = "今天天气冷,吃火锅";
byte[] content = mes.getBytes(Charset.forName("utf-8"));
int length = mes.getBytes(Charset.forName("utf-8")).length;
//创建协议包对象
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
ctx.writeAndFlush(messageProtocol);
}
}
........
}
[3] 自定义协议
//协议包
public class MessageProtocol {
private int len; //关键:数据长度
private byte[] content; // 要发送的数据
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
}
8、Netty 核心源码剖析
8.1、启动过程源码剖析
说明:
- 源码需要剖析到Netty 调用 doBind 方法, 追踪到 NioServerSocketChannel 的 doBind
- 并且要 Debug 程序到 NioEventLoop类
启动过程:
- 创建 2 个 EventLoopGroup 线程池数组。数组默认大小CPU*2,方便 chooser 选择 线程池时提高性能
- BootStrap 将 boss 设置为 group 属性,将 worker 设置为 childer 属性
- 通过 bind 方法启动,内部重要方法为 initAndRegister 和 dobind 方法
- initAndRegister 方法会反射创建 NioServerSocketChannel 及其相关的 NIO 的对象, pipeline , unsafe,同时也为 pipeline 初始了 head 节点和 tail 节点。
- 在 register0 方法成功以后调用在 dobind 方法中调用 doBind0 方法,该方法会调 NioServerSocketChannel 的 doBind 方法对 JDK 的 channel 和端口进行绑定, 完成 Netty 服务器的所有启动,并开始监听连接事件
8.2、接受请求过程源码剖析
说明:
- 从之前服务器启动的源码中,我们得知,服务器最终注册了一个 Accept 事件等待客户端的连接。我们也知道,NioServerSocketChannel 将自己注册到了 boss 单例线程池(reactor 线程)上,也就是 EventLoop
- 先简单说下 EventLoop 的逻辑(后面我们详细讲解EventLoop),EventLoop 的作用是一个死循环,而这个循环中做 3 件事情:
- 有条件的等待 Nio 事件
- 处理 Nio 事件
- 处理消息队列中的任务
- 仍用前面的项目来分析:进入到 NioEventLoop 源码中后,在private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) 方法开始调试
- 最终我们要分析到AbstractNioChannel 的 doBeginRead 方法, 当到这个方法时, 针对于这个客户端的连接就完成了,接下来就可以监听读事件了
总体流程:接收连接 -----> 创建一个新的 NioSocketChannel -----> 注册到一个 worker EventLoop 上 ----> 注册selecot Read 事件
详细流程:
- 服务器轮询 Accept 事件,获取事件后调用 unsafe 的 read 方法,这个 unsafe 是 ServerSocket 的内部类,该方法内部由 2 部分组成
- doReadMessages 用于创建 NioSocketChannel 对象,该对象包装 JDK 的 Nio Channel 客户端。该方法会像创建 ServerSocketChanel 类似创建相关的 pipeline , unsafe,config
- 随后执行 执行 pipeline.fireChannelRead 方法,并将自己绑定到一个 chooser 选择器选择的 workerGroup 中的 一个 EventLoop。并且注册一个0,表示注册成功,但并没有注册读(1)事件
8.3、Pipeline、Handler、HandlerContext创建
- 每当创建 ChannelSocket(就是 Channel) 的时候都会创建一个绑定的 Pipeline,一对一的关系,创建 pipeline 的时候也会创建 tail 节点和 head 节点,形成最初的链表。
- 在调用 Pipeline的 addLast 方法的时候,会根据给定的 Handler创建一个 Context, 然后,将这个 Context 插入到链表的尾端(tail 前面)。
- Context 包装 Handler,多个 Context 在 Pipeline中形成了双向链表
- 入站方向叫 inbound,由 head 节点开始,出站方法叫 outbound ,由 tail
8.4、ChannelPipeline 调度 Handler
上图解释:
Pipeline 首先会调用 Context 的静态方法 fireXXX,并传入 Context
然后,静态方法调用 Context 的 invoker 方法,而 invoker 方法内部会调用该 Context 所包含的 Handler 的真正的 XXX 方法,调用结束后,如果还需要继续向后传递,就调用 Context 的 fireXXX2 方法,循环往复
流程梳理:
- Context 包装 Handler,多个 Context 在 Pipeline中形成了双向链表,入站方向叫 inbound,由 head 节点开始,出站方法叫 outbound ,由 tail 节点开始。
- 而节点中间的传递通过 AbstractChannelHandlerContext 类内部的 fire 系列方法,找到当前节点的下一个节点不断的循环传播。是一个过滤器形式完成对 Handler 的调度
8.5、心跳服务
名称 | 作用 |
---|---|
IdleStateHandler | 当连接的空闲时间(read 或 write)太长时,将会触发一个 IdleStateEvent 事件。然后,你可以通过你的 ChannelInboundHandler 中重写 userEventTrigged方法来处理该事件 |
ReadTimeoutHandler | 如果在指定的事件没有发生 read事件,就会抛出这个以常,并自动关闭这个连接,你可以在 exceptionCaught方法中处理这个异常 |
WriteTimeoutHandler | 当一个 write操作不能在一定的时间内完成时,抛出异常,并关闭连接。你同时可以在 exceptionCaught方法中处理这个异常 |
流程梳理:
- IdleStateHandler 可以实现心跳功能,当服务器和客户端没有任何读写交互时,并超过了给定的时间,则会 触发用户 handler 的 userEventTriggered 方法。用户可以在这个方法中尝试向对方发送信息,如果发送失败,则关闭连接。
- IdleStateHandler 的实现基于 EventLoop 的定时任务,每次读写都会记录一个值,在定时任务运行的时候, 通过计算当前时间和设置时间和上次事件发生时间的结果,来判断是否空闲。
- 内部有 3 个定时任务,分别对应读事件,写事件,读写事件。通常用户监听读写事件就足够了
- 同时,IdleStateHandler 内部也考虑了一些极端情况:客户端接收缓慢,一次接收数据的速度超过了设置的空闲时间。Netty 通过构造方法中的 observeOutput 属性来决定是否对出站缓冲区的情况进行判断
- 如果出站缓慢,Netty 不认为这是空闲,也就不触发空闲事件。但第一次无论如何也是要触发的。因为第一 次无法判断是出站缓慢还是空闲。当然,出站缓慢的话,可能造成 OOM , OOM 比空闲的问题更大
- 所以,当你的应用出现了内存溢出,OOM 之类,并且写空闲极少发生(使用了 observeOutput 为 true), 那么就需要注意是不是数据出站速度过慢
- 还有一个注意的地方:就是 ReadTimeoutHandler ,它继承自 IdleStateHandler,当触发读空闲事件的时候, 就触发 ctx.fireExceptionCaught 方法,并传入一个 ReadTimeoutException,然后关闭 Socket。
- 而 WriteTimeoutHandler 的实现不是基于 IdleStateHandler 的,他的原理是,当调用 write 方法的时候,会创建一个定时任务,任务内容是根据传入的 promise 的完成情况来判断是否超出了写的时间。当定时任务根据指定时间开始运行,发现 promise 的 isDone 方法返回 false,表明还没有写完,说明超时了,则抛出异常。当 write 方法完成后,会打断定时任务。
8.6、EventLoop 源码剖析
流程梳理:
- 每次执行 ececute 方法都是向队列中添加任务。当第一次添加时就启动线程,执行 run 方法,而 run 方法是整个 EventLoop 的核心,就像 EventLoop 的名字一样,Loop Loop ,不停的 Loop ,Loop 做什么呢?做 3 件 事情:
- 调用 selector 的 select 方法,默认阻塞一秒钟,如果有定时任务,则在定时任务剩余时间的基础上在加上 0.5 秒进行阻塞。当执行 execute 方法的时候,也就是添加任务的时候,唤醒 selecor,防止 selecotr 阻塞时间过 长。
- 当 selector 返回的时候,回调用 processSelectedKeys 方法对 selectKey 进行处理。
- 当 processSelectedKeys 方法执行结束后,则按照 ioRatio 的比例执行 runAllTasks 方法,默认是 IO 任务时间 和非 IO 任务时间是相同的,你也可以根据你的应用特点进行调优 。比如非 IO 任务比较多,那么你就将 ioRatio 调小一点,这样非 IO 任务就能执行的长一点,防止队列积攒过多的任务
8.7、Handler加入线程池、Context添加线程池
回顾(Netty高性能架构设计 ——> Netty 模型 ——> Netty中Task的三种使用场景):
- 在面对耗时业务,可以将其放入 Worker EventLoopGroup 的 Task 中去非阻塞执行,这种方式即:Handler 中加入线程池
方式 | 区别 |
---|---|
Handler 加入线程池 | 【执行加入 Task 的代码、未加入 Task 的代码】实际使用的是同一个线程 |
Context 添加线程池 | 【执行加入 Task 的代码使用的自建线程池中的线程】 【未加入 Task 的代码使用的 Worker 线程池中的 EventLoop 线程】 总结:分别一个线程 |
[1] Handler加入线程池
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("EchoServer Handler 的线程是=" + Thread.currentThread().getName());
//按照原来的方法处理耗时任务
//解决方案1 用户程序自定义的普通任务
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
//输出线程名
System.out.println("EchoServerHandler execute 线程是=" + Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
});
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
//输出线程名
System.out.println("EchoServerHandler execute 线程2是=" + Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
});
}
}
[2] Context添加线程池
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
// group 就是充当业务线程池,可以将任务提交到该线程池
// 这里我们创建了16个线程
static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("EchoServer Handler 的线程是=" + Thread.currentThread().getName());
//将任务提交到 group线程池
group.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
//接收客户端信息
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String body = new String(bytes, "UTF-8");
//休眠10秒
Thread.sleep(10 * 1000);
System.out.println("group.submit 的 call 线程是=" + Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
return null;
}
});
//将任务提交到 group线程池
group.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
//接收客户端信息
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String body = new String(bytes, "UTF-8");
//休眠10秒
Thread.sleep(10 * 1000);
System.out.println("group.submit 的 call 线程是=" + Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
return null;
}
});
}
9、Netty 实现 Dubbo RPC
9.1、公共接口
//这个是接口,是服务提供方和服务消费方都需要
public interface HelloService {
String hello(String mes);
}
9.2、消费方
public class ClientBootstrap {
//这里定义协议头
public static final String providerName = "HelloService#hello#";
public static void main(String[] args) throws Exception{
//创建一个消费者
NettyClient customer = new NettyClient();
//创建代理对象
HelloService service = (HelloService) customer.getBean(HelloService.class, providerName);
for (;; ) {
Thread.sleep(2 * 1000);
//通过代理对象调用服务提供者的方法(服务)
String res = service.hello("你好 dubbo~");
System.out.println("调用的结果 res= " + res);
}
}
}
9.3、提供方
[1] 公共接口实现类
public class HelloServiceImpl implements HelloService {
private static int count = 0;
//当有消费方调用该方法时, 就返回一个结果
@Override
public String hello(String mes) {
System.out.println("收到客户端消息=" + mes);
//根据mes 返回不同的结果
if(mes != null) {
return "你好客户端, 我已经收到你的消息 [" + mes + "] 第" + (++count) + " 次";
} else {
return "你好客户端, 我已经收到你的消息 ";
}
}
}
[2] 启动器
//ServerBootstrap 会启动一个服务提供者,就是 NettyServer
public class ServerBootstrap {
public static void main(String[] args) {
NettyServer.startServer("127.0.0.1", 7000);
}
}
9.4、Netty 部分
[1] 消费端
① Client
public class NettyClient {
//创建线程池
private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private static NettyClientHandler client;
private int count = 0;
//编写方法使用代理模式,获取一个代理对象
public Object getBean(final Class<?> serivceClass, final String providerName) {
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[]{serivceClass}, (proxy, method, args) -> {
System.out.println("(proxy, method, args) 进入...." + (++count) + " 次");
//{} 部分的代码,客户端每调用一次 hello, 就会进入到该代码
if (client == null) {
initClient();
}
//设置要发给服务器端的信息
//providerName 协议头 args[0] 就是客户端调用api hello(???), 参数
client.setPara(providerName + args[0]);
//
return executor.submit(client).get();
});
}
//初始化客户端
private static void initClient() {
client = new NettyClientHandler();
//创建EventLoopGroup
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(client);
}
}
);
try {
bootstrap.connect("127.0.0.1", 7000).sync();
} catch (Exception e) {
e.printStackTrace();
}
}
}
② 处理器
public class NettyClientHandler extends ChannelInboundHandlerAdapter implements Callable {
private ChannelHandlerContext context;//上下文
private String result; //返回的结果
private String para; //客户端调用方法时,传入的参数
//与服务器的连接创建后,就会被调用, 这个方法是第一个被调用(1)
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(" channelActive 被调用 ");
context = ctx; //因为我们在其它方法会使用到 ctx
}
//收到服务器的数据后,调用方法 (4)
//
@Override
public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(" channelRead 被调用 ");
result = msg.toString();
notify(); //唤醒等待的线程
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
//被代理对象调用, 发送数据给服务器,-> wait -> 等待被唤醒(channelRead) -> 返回结果 (3)-》5
@Override
public synchronized Object call() throws Exception {
System.out.println(" call1 被调用 ");
context.writeAndFlush(para);
//进行wait
wait(); //等待channelRead 方法获取到服务器的结果后,唤醒
System.out.println(" call2 被调用 ");
return result; //服务方返回的结果
}
//(2)
void setPara(String para) {
System.out.println(" setPara ");
this.para = para;
}
}
[2] 提供端
① Server
public class NettyServer {
public static void startServer(String hostName, int port) {
startServer0(hostName,port);
}
//编写一个方法,完成对NettyServer的初始化和启动
private static void startServer0(String hostname, int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new NettyServerHandler()); //业务处理器
}
}
);
ChannelFuture channelFuture = serverBootstrap.bind(hostname, port).sync();
System.out.println("服务提供方开始提供服务~~");
channelFuture.channel().closeFuture().sync();
}catch (Exception e) {
e.printStackTrace();
}
finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
② 处理器
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//获取客户端发送的消息,并调用服务
System.out.println("msg=" + msg);
//客户端在调用服务器的api 时,我们需要定义一个协议
//比如我们要求 每次发消息是都必须以某个字符串开头 "HelloService#hello#你好"
if(msg.toString().startsWith(ClientBootstrap.providerName)) {
String result = new HelloServiceImpl().hello(msg.toString().substring(msg.toString().lastIndexOf("#") + 1));
ctx.writeAndFlush(result);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}