文章目录
- Netty
Netty
IO模型
1)I/O模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
2)Java共支持3种网络编程模型I/O模式:BIO,NIO,AIO
3)Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。假如我们现在有一个中心服务器,有很多个客户端同时往我们的服务器上发送请求,那么我们服务器就会为每一个请求都建立一个Thread线程,如下图:
这样如果有多个客户端同时向我们的服务器发送请求,那么我们的服务器就不得不创建多个线程,所以这就会使效率变得非常的低。
4)Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上(在java中就是一个Seletor选择器),多路复用器轮询到连接有I/O请求就进行处理。如下图:
我们的Selector选择器在做什么事情呢?它一直在做轮询,它一直在看我们的NIO的Channel通道中有什么事件发生变化了,它一直在观察,如果它观察到一个通道里面的事件发生了变化,那么它就会选择它进行相关的处理。这里体现了我们事件驱动和多路复用的特点。NIO的一个线程可以处理多个客户端的连接。当然我们也不会让我们的一个线程Thread去维护成千上万个客户端连接,我们一般会写多个Thread线程。
5)Java AIO(NIO.2):异步非阻塞,AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
Java BIO基本介绍
1)Java BIO就是传统的java.io编程,其相关的类和接口在java.io中
2)BIO(blocking I/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。
3)BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解。
BIO的工作机制原理图,如下:
一个Socket对应着一个Thread线程。
BIO编程的简单流程:
1)服务器端启动一个ServerSocket
2)客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯。
3)客户端发出请求后,它先咨询我们的服务器是否有线程响应,如果没有则会等待,或者被拒绝
4)如果有响应,客户端线程会等待请求结束后,在继续执行。
Java BIO应用实例
实例说明:
1)使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯。
2)要求使用线程池机制改善,可以连接多个客户端。
3)服务器端可以接收客户端发送的数据(使用telnet方式当做我们的客户端,我们就不特意编写我们的客户端了)
代码如下:
package com.atguigu.bio;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author 望轩
* @date 2022/11/14 22:14
*/
public class BIOServer {
public static void main(String[] args) throws Exception{
//线程池机制
//思路
//1.创建一个线程池
//2.如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true){
//监听,等待客户端连接,服务器生成一个Socket对象,服务器使用这个Socket对象可以和客户端进行通信,当服务器调用ServerSocket的accept()方法之后
//服务端会发生阻塞,阻塞到serverSocket.accept()代码的这个地方,等待客户端连接服务器。如果一直没有客户端连接服务器,服务器就会一直发生阻塞。
//直到客户端连接服务器之后,服务器才会阻塞通过,才会往下执行代码。
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() {
//可以和客户端通讯
handler(socket);
}
});
}
}
//编写一个handler方法,和客户端通讯
public static void handler(Socket socket){
try{
System.out.println("线程信息 id =" + Thread.currentThread().getId() + "名字=" + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true){
System.out.println("线程信息 id =" + Thread.currentThread().getId() + "名字=" + Thread.currentThread().getName());
//通过socket获取输入流。当进行IO流的read操作的时候也会发生阻塞。我们这里的InputStream输入流是通过前面在服务器里生成的用来和客户端通信
//的Socket对象中获取的。我们从Socket对象中拿到输入流的信息,也就是客户端发送给服务端的消息。如果当前inputStream输入流中没有信息,
//我们的当前线程会发生阻塞,等待读取信息。我们客户端发送的信息会存放到通道里面,我们服务端可以通过socket对象从通道里面获取信息,也就是
//获取inputStream输入流。如果客户端没有发送给服务器端信息,那么这个通道里面就不会有要读取的信息,因此服务器端通过Socket对象获取的输入流
//里面就什么都没有,这样当我们执行inputStream.read()操作的时候,线程就会发生阻塞,它要等待IO流的输入,才能正常运行。因此服务器端的线程
//里面的inputStream.read()这个方法也会发生阻塞。
int read = inputStream.read(bytes);
if(read != -1){
System.out.println(new String(bytes,0,read)); //输出客户端发送的数据
} else{
break;
}
}
} catch (Exception e){
e.printStackTrace();
} finally {
System.out.println("关闭和client的连接");
try {
//关闭客户端的连接,其实也就是把服务端的Socket对象给关闭掉就行了。因为我们的服务器端就是通过我们的Socket对象与其连接的,
//如果我们把Socket对象关闭了,那么服务器也就不能和客户端进行连接。
socket.close();
} catch (Exception e){
e.printStackTrace();
}
}
}
}
运行main方法之后,控制台里的输出结果为,如下图:
使用telnet命令模拟客户端连接服务器,如下图:
执行命令之后,idea控制台输出信息如下图:
现在我们在cmd命令窗口按ctrl加中右括号,然后使用send命令向服务器端发送消息,如下图:
控制台的输出信息如下图:
然后我们继续向服务端发送消息,如下图:
控制台的输出信息如下图:
接着我们重新开一个cmd命令窗口,然后使用telnet命令再模拟一个客户端向服务器端发送消息,如下图:
从上面的控制台输出信息中可以得出信息,对于我们的BIO,没多出一个客户端连接我们的服务器的时候,我们的服务器就会为我们的客户端连接创建一个新的线程。
从上面的示例中可以分析出Java BIO的问题之所在:
1)每个请求都需要创建独立的线程,与对应的客户端进行数据Read业务处理,数据Write。因此如果有很多个客户端同时的访问服务器,服务器需要创建很多个Thread线程。
2)当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
3)连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在Read操作上,造成线程资源浪费。
Java NIO基本介绍
1)Java NIO全称 java non-blocking IO,是指JDK提供的新API。从JDK 1.4开始,Java提供了一系列改进的输入/输出的新特性,被统称为NIO(即New IO),是同步非阻塞的。
2)NIO相关类都放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。
3)NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
4)NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
5)Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是像BIO那样当read的时候如果发现没有可读取的IO流就发生阻塞。所以直至数据变的可以读取之前,该线程可以继续做其它的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
6)通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个线程。
7)HTTP2.0使用了多路复用的技术,做到同一个连接并发处理流多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
NIO结构如下图:
上图中是NIO的Selector,Channel和Buffer的关系。说明:
1)每个channel都会对应一个Buffer
2)Selector对应一个线程,一个线程对应多个channel连接
3)该图反映了有三个channel注册到该selector中
4)程序切换到哪个channel是由事件决定的,Event就是一个重要的概念
5)Selector会根据不同的事件,在各个通道上切换
6)Buffer就是一个内存块,底层是有一个数组的
7)数据的读取写入是通过Buffer,这个和BIO是有区别的,BIO中要么是输入流或者是输出流,不能够双向,但是NIO的Buffer是双向的,它既是可以读的又是可以写的。但是需要使用flip切换方法。
8)Channel也是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的。
NIO的Buffer基本使用
NIO的Buffer的基本使用代码如下:
public class BasicBuffer {
public static void main(String[] args) {
//举例说明Buffer的使用
//创建一个Buffer,大小为5,既可以存放5个int类型
IntBuffer intBuffer = IntBuffer.allocate(5);
//向Buffer中存放数据
// intBuffer.put(10);
// intBuffer.put(11);
// intBuffer.put(12);
// intBuffer.put(13);
// intBuffer.put(14);
for (int i = 0; i < intBuffer.capacity(); i++){
intBuffer.put(i*2);
}
//如何从buffer里读取数据
//将buffer转换,读写切换
intBuffer.flip();
while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
}
NIO和BIO的比较:
1)BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多。
2)BIO是阻塞的,NIO是非阻塞的。
3)BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
Buffer的机制及子类
基本介绍
缓冲区
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件,网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer,如图:
通道(Channel)
NIO的通道类似于流,但有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写。
- 通道可以实现异步读写数据。
- 通道可以从缓冲区读取数据,也可以写数据到缓冲区。
- BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。
- Channel在NIO中是一个接口,public interface Channel extends Closeable{}
- 常用的Channel类有:FileChannel,DatagramChannel,ServerSocketChannel和SocketChannel
- FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写。
Channel应用实例1
实例要求:1)使用前面学习后的ByteBuffer(缓冲)和FileChannel(通道),将“hello,尚硅谷”写入到file01.txt中。2)文件不存在就创建。
public class NIOFileChannel01 {
public static void main(String[] args) throws IOException {
String str = "hello,尚硅谷";
//创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
//通过 输出流 获取对应的 FileChannel
FileChannel fileChannel = fileOutputStream.getChannel();
//创建一个缓冲区ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将str放入到byteBuffer
byteBuffer.put(str.getBytes());
//对byteBuffer进行反转
byteBuffer.flip();
//将byteBuffer数据写入到fileChannel
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
实例要求:1)使用前面学习后的ByteBuffer(缓冲)和FileChannel(通道),将file01.txt中的数据读入到程序,并显示在控制台屏幕。2)假定文件已经存在。
public class NIOFileChannel02 {
public static void main(String[] args) throws IOException {
//创建文件的输入流
File file = new File("d:\\file01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
//通过输入流对象获取对应的FileChannel
FileChannel fileChannel = fileInputStream.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//将通道的数据读入到Buffer
fileChannel.read(byteBuffer);
//将byteBuffer的字节转成字符串
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
从上面两个例子可以看出来,如果是程序里面的东西输出到电脑磁盘里面的某个文件里面,那么Buffer缓冲区需要先读取程序里面的内容,也即是先写,写完之后需要磁盘读取Buffer缓冲区,这个时候就是读了,因此中间需要使用flip方法切换一下读写,就如上面第一个代码一样;而第二个例子是只往Buffer缓冲区里面写数据,并没有从它里面读数据,因此中间没有使用flip方法。
Selector选择器介绍和原理
1)Java的NIO,用非阻塞的IO方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
2)Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个管道,也就是管理多个连接和请求。
3)只有在连接真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
4)避免了多线程之间的上下文切换导致的开销。
5)Netty的IO线程NIOEventLoop聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
6)当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
7)线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
8)由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起。
9)一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能,弹性伸缩能力和可靠性都得到了极大的提升。
NIO非阻塞网络编程快速入门
1)当客户端连接时,会通过ServerSocketChannel得到SocketChannel
2)将socketChannel注册到Selector上,register(Selector sel,int ops),一个selector上可以注册多个SocketChannel
3)注册后返回一个SelectionKey,会和该Selector关联(集合)
4)Selector进行监听select方法,返回有事件发生的通道的个数
5)进一步得到各个SelectionKey(有事件发生)
6)在通过SelectionKey反向获取SocketChannel,方法channel()
7)可以通过得到的channel,完成业务处理
实例要求:
1)编写一个NIO入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)
2)目的:理解NIO非阻塞网络编程机制
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selector对象
Selector selector = Selector.open();
//绑定一个端口6666,在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注册到 selector 关心事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while(true) {
//这里我们等待1秒,如果没有事件发生,返回
if(selector.select(1000) == 0) { //没有事件发生
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的>0, 就获取到相关的selectionKey集合
//1.如果返回的>0,表示已经获取到关注的事件
//2.select.selectedKeys()返回关注事件的集合
// 通过selectionKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历 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_ACCEPT, ByteBuffer.allocate(1024));
}
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("from客户端 " + new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey,防止重复操作
keyIterator.remove();
}
}
}
}
//客户端
public class NIOClient {
public static void main(String[] args) throws IOException {
//得到一个网络通道
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, 尚硅谷";
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
//发送数据,将 buffer 数据写入到 channel
socketChannel.write(byteBuffer);
System.in.read();
}
}
Netty概述
有了NIO,为什么还会有Netty呢?原因如下:
- NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector,ServerSocketChannel,SocketChannel,ByteByffer等。
- 需要具备其它的额外技能:需要熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序。
- 开发工作量和难度都非常大:例如客户端面临断连重连,网络闪断,半包读写,失败缓存,网络拥塞和异常流的处理等等。
- JDK NIO的Bug:例如臭名昭著的Epoll Bug,它会导致selector空轮询,最终导致CPU的100%。直到JDK 1.7 版本该问题仍旧存在,没有被根本解决。
Netty官网说明:
- Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的,基于事件驱动的网络应用程序框架,用以快速开发高性能,高可靠性的网络IO程序。
- Netty可以帮助你快速,简单的开发出一个网络应用,相当于简化和流程化了NIO的开发过程。
- Netty是目前最流行的NIO框架,Netty在互联网领域,大数据分布式计算领域,游戏行业,通信行业等获得了广泛的应用,知名的Elasticsearch,Dubbo框架内部都采用了Netty。
Netty的优点:
- 设计优雅:适用于各种传输类型的统一API阻塞和非阻塞Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型-单线程,一个或多个线程池。
- 使用方便:详细记录的javadoc,用户指南和示例;没有其他依赖项,JDK5(Netty 3.x)或6(Netty 4.x)就足够了。
- 高性能,吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存赋值。
- 安全:完整的SSL/TLS 和 Start TLS支持。
- 社区活跃,不断更新:社区活跃,版本迭代周期短,发现的Bug可以被及时修复,同时,更多的新功能会被加入。
Netty的线程模型概述
1)不同的线程模式,对程序的性能有很大影响,为了搞清Netty线程模式,我们来系统的讲解下各个线程模式,最后看看Netty线程模型有什么优越性。
2)目前存在的线程模型有:传统阻塞I/O服务模型,Reactor模式
3)根据Reactor的数量和处理资源池线程的数量不同,有3种典型的实现:1.单Reactor单线程;2.单Reactor多线程;主从Reactor多线程。
4)Netty线程模式(Netty主要基于主从Reactor多线程模型做了一定的改进,其中主从Reactor多线程模型有多个Reactor)
传统阻塞I/O服务模型
上图这种模型的特点:
- 采用阻塞IO模式获取客户端输入的数据。
- 每个连接都需要独立的线程完成数据的输入,业务处理和数据的返回。
问题分析:
- 当并发数很大时,就会创建大量的线程,这样就会占用很大的系统资源。
- 当连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程资源浪费。
Reactor服务模型
Reactor对应的叫法:1.反应器模式。2.分发者模式(Dispatcher)。3.通知者模式(notifier)
针对传统阻塞I/O的2个缺点,解决方案:
- 基于I/O复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
- 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程池(上图中的三个线程都在线程池里)进行处理,一个线程池可以处理多个连接的业务。
说明:Reactor模式,通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动);服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程。因此Reactor模式也叫做Dispatcher模式。Reactor模式使用IO复用监听事件,收到事件后,分发给某个线程,这点就是网络服务高并发处理的关键。
Reactor模式中核心组成:
- Reactor:Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转义到适当的联系人;
- Handlers:处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作。
Reactor模式分类:
- 单Reactor单线程。
- 单Reactor多线程。
- 主从Reactor多线程。
Netty服务模型
单Reactor单线程
1)Select是前面I/O复用模型介绍的标准网络编程API,可以实现应用程序通过一个阻塞对象监听多路连接请求。
2)Reactor对象通过Select监控客户端请求事件,收到事件后通过Dispatch进行分发
3)如果是建立连接请求事件,则由Acceptor通过Accept处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理。
4)如果不是建立连接事件,则Reactor会分发调用连接对应的Handler来响应
5)Handler会完成Read->业务处理->Send 的完整业务流程
结合实例:服务器端用一个线程通过多路复用搞定所有的IO操作(包括连接,读,写等),编码简单,清晰明了,但是如果客户端连接数量较多,将无法支撑,前面的NIO案例就属于这种模型。
优点:模型简单,没有多线程,进程通信,竞争的问题,全部都在一个线程中完成。
缺点:性能问题,只有一个线程,无法完全发挥多核CPU的性能。Handler在处理某个连接上的业务时,整个进程无法处理其它连接事件,很容易导致性能瓶颈。
缺点:可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
使用场景:客户端的数量有限,业务处理非常快速,比如Redis在业务处理的事件复杂度O(1)的情况。
单Reactor多线程
上图流程说明:
- Reactor对象通过select监控客户端请求事件,收到事件后,通过dispatch进行分发;
- 如果是建立连接请求,则Acceptor通过accept处理连接请求,然后创建一个Handler对象处理完成连接后的各种事件;
- 如果不是连接请求,则由reactor分发调用连接对应的handler来处理;
- handler只负责响应事件,不做具体的业务处理,通过read读取数据后,会分发给后面的worker线程池的某个线程处理业务;
- worker线程池会分配独立的线程完成真正的业务,并将结果返回给Handler;handler收到响应后,通过send将结果返回给客户端。
单Reactor多线程优点:
- 可以充分的利用多核cpu的处理能力。
单Reactor多线程缺点:
- 多线程数据共享和访问比价复杂,reactor处理所有的事件的监听和响应,在单线程运行,在高并发场景容易出现性能瓶颈。
主从Reactor多线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kf8NNUVZ-1672668344411)(null)]
- Reactor主线程MainReactor对象通过select监听连接事件,收到事件后,通过Acceptor处理连接事件;
- 当Acceptor处理连接事件后,MainReactor将连接分配给SubReactor;
- SubReactor将连接加入到连接队列进行监听,并创建handler进行各种事件处理;
- 当有新事件发生时,SubReactor就会调用对应的handler进行各种事件处理;
- handler通过read读取数据,分发给后面的worker线程处理;
- worker线程池分配独立的worker线程池进行业务处理,并返回结果;
- handler收到响应的结果后,再通过send将结果返回给客户端;
- Reactor主线程可以对应多个Reactor子线程,即MainReactor可以关联多个SubReactor;
优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
优点:父线程与子线程的数据交互简单,Reactor主线程只需要把新连接传给子线程,子线程无需返回数据。
缺点:编程复杂度较高。
结合实例:这种模型在许多项目中广泛使用,包括Nginx主从Reactor多进程模型,Memcached主从多线程,Netty主从多线程模型的支持。
Netty模型
简单版
- BossGroup 线程维护Selector , 只关注Accecpt
- 当接收到Accept事件,获取到对应的SocketChannel, 封装成 NIOScoketChannel并注册到Worker 线程(事件循环), 并进行维护
- 当Worker线程监听到selector 中通道发生自己感兴趣的事件后,就进行处理(就由handler), 注意handler 已经加入到通道
进阶版
Netty 主要基于主从Reactors 多线程模型(如图)做了一定的改进,其中主从 Reactor 多线程模型有多个 Reactor;
详细版
- Netty抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写
- BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是NioEventLoop
- NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯
- NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop
- 每个Boss NioEventLoop 循环执行的步骤有3步
a.轮询accept 事件
b.处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将其注册到某个worker NIOEventLoop 上的 selector
c.处理任务队列的任务 , 即 runAllTasks - 每个 Worker NIOEventLoop 循环执行的步骤
a.轮询read, write 事件
b.处理i/o事件, 即read , write 事件,在对应NioScocketChannel 处理
c.处理任务队列的任务 , 即 runAllTasks - 每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道), pipeline
Netty入门实例-TCP服务
1)实例要求:使用IDEA创建Netty项目;
2)Netty服务器在6668端口监听,客户端能发送消息给服务器“hello,服务器~”
3)服务器可以回复消息给客户端“hello,客户端~”
4)目的:对Netty线程模型有一个初步认识,便于理解Netty模型理论
说明:创建Maven项目,并引入netty包,引入netty坐标,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vjhnAME2-1672668343757)(C:\Users\86184\AppData\Roaming\Typora\typora-user-images\image-20230102113600089.png)]
服务端代码
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
//创建BossGroup和WorkerGroup
/**
* 说明
* 1.创建两个线程组bossGroup和workerGroup
* 2.bossGroup只是处理连接请求,真正的和客户端业务处理,会交给workerGroup完成
* 3.两个都是无限循环
* 4.bossGroup和workerGroup含有的子线程(NioEventLoop)的个数
* //默认实际cpu核数*2
*/
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行设置
bootstrap.group(bossGroup,workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG,128) //设置线程队列等待连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true) //设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>(){ //给我们的workerGroup的EventLoop对应的管道设置处理器
//创建一个通道初始化对象(匿名对象)
//给pipeline设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyServerHandler());
}
}); //给我们的wokerGroup的EventGroup对应的通道设置处理器
System.out.println("...服务器 is ready...");
//绑定一个端口并且同步,生成了一个ChannelFuture 对象
//启动服务器
ChannelFuture cf = bootstrap.bind(6668).sync();
//对关闭通道进行监听
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//优雅的关闭
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
/**
* 说明
* 1.我们自定义一个Handler需要继承netty规定好的某个HandlerAdapter
* <p>
* 2.这时我们自定义一个Handler,才能成为一个Handler
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据的事件(这里我们可以读取客户端发送的消息)
/**
* @param ctx :上下文对象,含有管道 piepline,通道channel,地址
* @param msg :就是客户端发送的数据,默认Object
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx=" + ctx);
//将msg转成一个ByteBuf
//ByteBuf是Netty提供的,不是NIO的ByteBuffer
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址" + ctx.channel().remoteAddress());
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//wirteAndFlush 是write+flush
//将数据写入到缓存,并刷新
//一般讲,我们对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端~", 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 InterruptedException {
//客户端需要一个事件循环1组
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();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
group.shutdownGracefully();
}
}
}
/**
* 说明
* 1.我们自定义一个Handler需要继承netty规定好的某个HandlerAdapter
* <p>
* 2.这时我们自定义一个Handler,才能成为一个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 {
ctx.close();
}
}
书上的netty入门代码
客户端发送“我是客户端”消息,然后服务器端接收此消息,接收之后再返回给客户端。
服务器代码
package org.example.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.net.InetSocketAddress;
/**
* @author xuan
* @create 2023/7/18
*/
public class EchoServer {
private final int port = 8080;
public static void main(String[] args) throws Exception {
new EchoServer().start();
}
public void start() throws Exception {
final EchoServerHandler serverHandler = new EchoServerHandler();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(serverHandler);
}
});
ChannelFuture future = bootstrap.bind().sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
}
package org.example.server;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.EventLoopGroup;
import io.netty.util.CharsetUtil;
/**
* @author xuan
* @create 2023/7/18
*/
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8));
ctx.write(in);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
客户端代码
package org.example.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress;
/**
* @author xuan
* @create 2023/7/18
*/
public class EchoClient {
private final String host = "127.0.0.1";
private final int port = 8080;
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture future = bootstrap.connect().sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
new EchoClient().start();
}
}
package org.example.client;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
/**
* @author xuan
* @create 2023/7/18
*/
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("我是客户端", CharsetUtil.UTF_8));
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
System.out.println("Client received: " + in.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
过程说明
先启动EchoServer服务器,如下图:
因为此时客户端还没有启动连接服务器,因此这个时候服务器一直是等待客户端连接的状态;
接着启动客户端,客户端成功连接上服务器,这个时候会执行EchoClientHandler客户端处理器的channelActive方法,如下图:
可以发现这里的channelActive方法里面会调用writeAndFlush方法,这个方法会给服务器的Socket发送信息,因此我们执行完这个方法之后,服务器的EchoServerHandler的channelRead方法会被调用,用来接收客户端发送过来的消息,如下图:
可以发现 server服务端成功接收到了客户端发送来的消息。
服务端接收完消息之后,会调用channelReadComplete方法给客户端返回消息,会调用channelReadComplete方法,如下图:
为什么我们这里是空的Buffer?因为在channelRead方法中的最后一句已经调用了write方法往Buffer中写入值了,因此我们只要调用writeAndFlush方法就可以把缓存中的值发送给客户端的Socket里面,这样客户端就可以读取了,客户端会调用channelRead0方法,如下图:
客户端成功接收。
writeAndFlush方法相当于是write和flush方法的组合体,write方法是把数据写到缓存中,flush方法是把缓存中的数据发送给客户端或者是服务端的Socket,这样就可以达到网络数据传输了,可以使客户端和服务端的数据进行交互。
Netty中的TaskQueue任务队列
首先看一个netty阻塞的例子,EchoClient和EchoServer还是用上面的两个类,但是我们把EchoClientHandler和EchoServerHandler简单的修改一下,如下:
/**
* @author xuan
* @create 2023/7/18
*/
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("我是客户端", CharsetUtil.UTF_8));
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
System.out.println("Client received: " + in.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
/**
* @author xuan
* @create 2023/7/18
*/
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//读取客户端发送来的消息
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送过来的消息是:" + buf.toString(CharsetUtil.UTF_8));
//比如这里我们有一个非常耗费时间的业务 -> 异步执行 -> 提交该channel对应的NIOEventLoop 的 taskQueue中
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端,喵喵喵2", CharsetUtil.UTF_8));
System.out.println("go on...");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端,喵喵喵1", CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
启动EchoServer之后,再启动EchoClient,当客户端连接成功之后,EchoClientHandler#channelActive方法会被调用,如下图:
接着服务端的EchoServerHandler#channelRead方法会被调用,如下图:
上面channelRead方法里面go on…的输出也会被阻塞10s。最后当channelReadComplete方法执行完毕之后,会执行
EchoClientHandler#channelRead0方法,客户端读取服务端回传的消息,如下图:
最后看下控制台输出结果,首先是EchoServer控制台,如下图:
再来看下EchoClient控制台,如下图:
上面这个过程有什么问题呢?问题就是我们虽然使用了netty,但是仍然会发生阻塞,仍然不是异步的,就是在channelRead方法中发生阻塞之后go on…的输出也阻塞了10s,因此现在仍然不是异步的。那么我们能不能达到一种异步效果呢?就是前面发生阻塞之后,我们仍然可以直接异步执行下面的内容,也就是直接输出go on…信息?解决方案:
- 把阻塞任务放到ChannelHandlerContext#pipeline#channel#eventLoop#taskQueue中,其实可以简单的理解成把我们的阻塞任务放到channel对应的EventLoop中的taskQueue任务中。
具体操作如下图:
看一下我们的输出结果,首先看下EchoServer中的输出结果,如下图:
当客户端连接服务端成功的时候,会直接输出go on…,不会再阻塞10s之后输出。再来看下EchoClient中的输出结果,如下图:
会先输出"hello,客户端,喵喵喵1",等待十秒之后会输出"hello,客户端,喵喵喵2"。
注意:放在这里面ChannelHandlerContext#pipeline#channel#eventLoop#taskQueue的任务,是队列任务,也就是前面的任务没有被执行的时候,后面的任务就不会被执行,如下图:
断点看下这两个任务是否真的被存储到了TaskQueue中,如下图:
从上图可知,我们上面的两个任务确实加入到了任务队列TaskQueue中,一次只能执行一个任务,一次会从任务队列里面取出队头的那个任务执行。
EventLoop的本质是一个线程池,并且这个线程池执行上面的两个任务的时候是在同一个线程里面执行的。
Netty模型
1)Netty抽象出两组线程池,BossGroup专门负责接收客户端连接,WorkerGroup专门负责网络读写操作;
2)NioEventLoop表示一个不断循环执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket网络通道。
3)NioEventLoop内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由IO线程NioEventLoop负责
4)NioEventLoopGroup下包含多个NioEventLoop
5)每个NioEventLoop中包含有一个Selector,一个taskQueue
6)每个NioEentLoop的Selector上可以注册监听多个NioChannel
7)每个NioChannel只会绑定在唯一的NioEventLoop上
8)每个NioChannel都绑定有一个自己的ChannelPipeline
Netty的异步模型
基本介绍:
- 异步的概念和同步相对,当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态,通知和回调来通知调用者。
- Netty中的I/O操作是异步的,包括Bind,Write,Connect等操作会简单的返回一个ChannelFuture。
- 调用者并不能立刻获得结果,而是通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。
- Netty的异步模型是建立在future和callback之上的,callback就是回调。重点说Future,它的核心思想是:假设一个方法fun,计算过程可能非常耗时,等待fun返回显然不合适,那么可以在调用fun的时候,立马返回一个Future,后续可以通过Future去监控方法fun的处理过程(即:Future-Listener机制)
Future说明:
- 表示异步的执行结果,可以通过它提供的方法来检查执行是否完成,比如检索计算等等。
- ChannelFuture是一个接口,interface ChannelFuture extends Future,我们可以添加监听器,当监听的事件发生时,就会通知到监听器。
工作原理示意图:
说明:
1.在使用Netty进行编程时,拦截操作和转换出入站数据只需要您提供callback或利用future即可。这使得链式操作简单,高效,并有利于编写可重用的,通用的代码。
2.Netty框架的目标就是让你的业务逻辑从网络基础应用编码中分离出来,解脱出来。
Future-Listener机制:
当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作。
常见有如下操作:
1.通过 isDone 方法来判断当前操作是否完成;
2.通过 isSuccess 方法来判断已完成的当前操作是否成功;
3.通过 getCause 方法来获取已完成的当前操作失败的原因;
4.通过 isCancelled 方法来判断已完成的当前操作是否被取消;
5.通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知 指定的监听器;如果 Future 对象已完成,则通知指定的监听器
在EchoClient类中,当客户端试图连接服务端的时候,因为连接需要时间是个异步操作,因此会先返回一个ChannelFuture对象,我们可以在这个对象中加上一个监听器,去判断最后异步执行的结果ChannelFuture中的结果是否成功,如下图:
控制台输出结果,如下图:
小结
相比于传统阻塞I/O,执行I/O操作后线程会被阻塞住,直到操作完成;异步处理的好处是不会造成线程阻塞,线程在I/O操作期间可以执行别的程序,在高并发情形下会更稳定和更高的吞吐量。
Http服务程序实例
快速入门实例HTTP服务
1)实例要求:使用IDEA创建Netty项目
2)Netty服务器在8080端口监听,浏览器发出请求http://localhost:8080
3)服务器可以恢复消息给客户端 “Hello!我是服务器!”,并对特定请求资源进行过滤
4)目的:Netty可以做Http服务开发,并且理解Handler实例和客户端及其请求的关系
首先看下TestServer类,如下图:
再来看下TestServerInitializer,如下图:
接着来看下我们自定义的Http相关的handler,可以用来处理客户端发送来的消息,如下图:
上面的三个类的代码如下:
/**
* @author xuan
* @create 2023/9/12
*/
public class TestServer {
private final int port = 8080;
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new TestServerInitializer());
ChannelFuture future = bootstrap.bind().sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
new TestServer().start();
}
}
/**
* @author xuan
* @create 2023/9/12
*/
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
//向管道加入处理器
//得到管道
ChannelPipeline pipeline = channel.pipeline();
//加入一个netty提供的HttpServerCodec 其中codec是coder编码和decoder解码的缩写
//HttpServerCodec说明:它是netty提供的处理http的编码解码器
pipeline.addLast("MyHttpServerCodec", new HttpServerCodec());
//增加一个自定义的handler
pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());
}
}
/**
* @author xuan
* @create 2023/9/12
* HttpObject表示客户端和服务器端相互通信的数据被封装成 HttpObject类型
*/
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
//此方法是用来读取客户端数据的
@Override
protected void channelRead0(ChannelHandlerContext context, HttpObject msg) throws Exception {
//判断 msg 是不是一个 httpRequest请求
if(msg instanceof HttpRequest) {
System.out.println("msg 类型:" + msg.getClass());
System.out.println("客户端地址" + context.channel().remoteAddress());
//回复信息给浏览器[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;charset=utf-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
//将构建好的 response返回
context.writeAndFlush(response);
}
}
}
看下浏览器访问效果,如下:
测试成功。但是存在一个问题,就是我们访问一次localhost:8080,但是控制台却输出两次channelRead0方法里面的内容,如下图:
上面之所以会输出两次,是因为客户端像服务端发了两次请求,如下图:
而两次请求都是HttpRequest请求,因此if判断都成立,如下图:
所以我们只需要过滤掉第二个请求图标的请求就可以了,如下图:
Pipeline和ChannelPipeline
ChannelPipeline是一个重点: ChannelPipeline是一个Handler 的集合,它负责处理和拦截inbound 或者 outbound 的事件和操作,相当于一个贯穿Netty 的链。(也可以这样理解: ChannelPipeline是保存ChannelHandler 的List,用于处理或拦截Channel 的入站事件和出站操作)。
ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。
在 Netty 中每个Channel 都有且仅有一个ChannelPipeline 与之对应,它们的组成关系如下:
从上图可知:
1)一个 Channel包含了一个 ChannelPipeline,而 ChannelPipeline中又维护了一个由ChannelHandlerContext组成的双向链表,并且每个ChannelHandlerContext中又关联着一个ChannelHandler。
2)入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的 handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的 handler互不干扰。
常用方法:
1)ChannelPipeline addFirst(ChannelHandler… handlers),把一个业务处理类(handler)添加到链中的第一个位置
2)ChannelPipeline addLast(ChannelHandler. handlers),把一个业务处理类(handler)添加到链中的最后一个位置
EventLoop组件
ChannelHandlerContext
1)保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象
2)即ChannelHandlerContext中包含一个具体的时间处理器ChannelHandler,同时ChannelHandlerContext中也绑定了对应的pipeline和Channel的信息,方便对ChannelHandler进行调用。
3)常用方法
ChannelFuture close(),关闭通道
ChannelOutboundnvoker flush(),刷新
ChannelFuture writeAndFlush(Object msg),讲数据写到ChannelPipeline中当前ChannelHandler的下一个ChannelHandler开始处理(出站)
ChannelOption
1)Netty在创建Channel实例后,一般都需要设置ChannelOption参数。
2)ChannelOption参数如下:
ChannelOption.SO_BACKLOG
对应TCP/IP协议listen函数中的backlog参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小。
ChannelOption.SO_KEEPALIVE
一直保持连接活动状态。
EventLoopGroup和它的实现类NioEventLoopGroup
1)EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例。
2)EventLoopGroup提供next接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务,在Netty服务器端编程中,我们一般都需要提供两个EventLoopGroup,例如:BossEventLoopGroup和WorkerEventLoopGroup
3)通常一个服务端口即一个 ServerSocketChannel对应一个Selector 和一个EventLoop 线程。BossEventLoop 负责接收客户端的连接并将 SocketChannel 交给 WorkerEventLoopGroup 来进行 IO 处理,如下图所示:
说明:
1.BossEventLoopGroup 通常是一个单线程的 EventLoop,EventLoop 维护着一个注册了ServerSocketChannel 的 Selector 实例,BossEventLoop 不断轮询 Selector 将连接事件分离出来
2.通常是 OP_ACCEPT 事件,然后将接收到的 SocketChannel 交给 WorkerEventLoopGroup
3.WorkerEventLoopGroup 会由 next 选择 其中一个 EventLoop来将这个 SocketChannel 注册到其维护的 Selector 并对其后续的 IO 事件进行处理 ,一个 EventLoop 可以处理多个 Channel
常用方法:
1.public NioEventLoopGroup(),构造方法
2.public Future<?> shutdownGracefully(),断开连接,关闭线程
Unpooled类
1.Netty提供一个专门用来操作缓冲区(即Netty的数据容器)的工具类。
2.常用方法如下所示:
public static ByteBuf copiedBuffer(CharSequence string, Charset charset)
通过给定的数据和字符编码返回一个ByteBuf对象(类似于NIO中的ByteBuffer 但有区别)
3.举例说明Unpooled获取Netty的数据容器ButeBuf的基本使用
体会一下上面的三个属性的作用,写一份代码,如下:
public class ByteBuf01 {
public static void main(String[] args) {
// 创建一个 byteBuf
/*
说明
1. 创建一个对象,该对象包含一个数组,是一个 byte[10]
2. Netty 的 Buf 存取数据,不需要像 NIO 一样使用 Filp 切换
Netty 底层维护了一个 ReaderIndex(下一个读的位置) 和 WriterIndex(下一个写的位置)
*/
ByteBuf buffer = Unpooled.buffer(10);
// 向 buf 存数据
for (int i = 0; i < 10; i++) {
buffer.writeByte(i);
}
System.out.println("写完数据后 {ReaderIndex: "+buffer.readerIndex()+", WriterIndex: "+buffer.writerIndex()+"}");
System.out.println("buf 的长度 - capacity :"+ buffer.capacity());
// 输出
for (int i = 0; i < buffer.capacity(); i++) {
// 读数据的方式-1 :直接 get 第几个 byte
//System.out.println(buffer.getByte(i));
// 读数据的方式-2 :通过移动 ReaderIndex 遍历
System.out.print(buffer.readByte() + " ");
}
System.out.println();
System.out.println("读完数据后 {ReaderIndex: "+buffer.readerIndex()+", WriterIndex: "+buffer.writerIndex()+"}");
}
}
Netty-Buf的常用API,代码示例2,如下:
public class ByteBuf02 {
public static void main(String[] args) {
// 用其他方式创建 Buf ,参数 :(存入 Buf 的文本 , 字符编码)
ByteBuf byteBuf = Unpooled.copiedBuffer("【呵呵】:Hello,Buf", CharsetUtil.UTF_8);
// 使用相关的 API
if (byteBuf.hasArray()){ // 如果有内容
// 获得 buf 中的数据
byte[] bytes = byteBuf.array();
// 转成 String 输出
System.out.println(new String(bytes, CharsetUtil.UTF_8));
// 查看 ByteBuf 中真正存的是什么
System.out.println("ByteBuf : "+ byteBuf);
// 数组的偏移量
System.out.println("偏移量 :"+ byteBuf.arrayOffset());
System.out.println("WriterIndex: "+byteBuf.writerIndex());
byteBuf.getByte(0);
System.out.println("getByte 后 :ReaderIndex: "+byteBuf.readerIndex()+",可读取的字节数 :" + byteBuf.readableBytes());
byteBuf.readByte();
System.out.println("readByte 后 :ReaderIndex: "+byteBuf.readerIndex()+",可读取的字节数 :" + byteBuf.readableBytes());
// 读取某一段,参数:(起点,终点,字符集编码)
System.out.println(byteBuf.getCharSequence(9, 24, CharsetUtil.UTF_8));
}
}
}
看一下我们之前的代码中使用Unpooled类的copiedBuffer方法的例子,如下图:
Netty群聊系统
一个客户端和服务器的连接,会对应着一个channel通道。
要求:
1.编写一个Netty群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
2.实现多人聊天
3.服务器端:可以监测用户上限,离线,并实现消息转发功能
4.客户端:通过channel可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)
目的:
进一步理解Netty非阻塞网络编程机制
代码,服务端代码和客户端代码如下:
/**
* @author xuan
* @create 2023/9/14
*/
public class GroupChatServer {
//监听端口
private int port;
public GroupChatServer(int port) {
this.port = port;
}
//编写一个run方法,处理客户端的请求
public void run() throws InterruptedException {
//创建两个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(8);
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.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 socketChannel) throws Exception {
// 获取 Pipeline
ChannelPipeline pipeline = socketChannel.pipeline();
// 通过 Pipeline 添加编、解码器(Netty 自带)
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
// 加入自己的 业务处理Handler
pipeline.addLast(new GroupChatServerHandler());
}
});
System.out.println("服务端准备完毕");
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
new GroupChatServer(8080).run();
}
}
/**
* @author xuan
* @create 2023/9/14
*/
public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {
/**
* 定义一个channel组,管理所有的channel
* GlobalEventExecutor.INSTANCE 是全局的事件执行器,是一个单例
* */
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* 定义一个时间的输出格式
*/
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 当连接建立之后,第一个被执行
* 一连接成功,就把当前的 Channel 加入到 ChannelGroup,并将上线消息推送给其他客户
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 获取当前 Channel
Channel channel = ctx.channel();
Date date = new Date(System.currentTimeMillis());
//将该客户加入聊天的信息推送给其它在线的客户端
//该方法会将channelGroup中所有的channel遍历,并发送消息,因此不需要我们遍历channelGroup中的所有channel
channelGroup.writeAndFlush("[客户端] ["+dateFormat.format(date)+"] "+channel.remoteAddress()+" 加入群聊~");
// 将当前 Channel 加入 ChannelGroup
channelGroup.add(channel);
}
/**
* 当断开连接之后,将 XXX 退出群聊消息推送给当前在线的客户
* 当某个 Channel 执行到这个方法,会自动从 ChannelGroup 中移除
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Date date = new Date(System.currentTimeMillis());
channelGroup.writeAndFlush("[客户端] ["+dateFormat.format(date)+"] "+ctx.channel().remoteAddress() + " 退出群聊~");
// 输出 ChannelGroup 的大小
System.out.println("==== ChannelGroup-Size : " + channelGroup.size());
}
/**
* 表示channel处于活动状态,可以提示 XXX 上线
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Date date = new Date(System.currentTimeMillis());
System.out.println("["+dateFormat.format(date)+"] "+ctx.channel().remoteAddress() + " 已上线~");
}
/**
* 当 Channel 处于不活动的状态激活,提示 XXX 离线
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Date date = new Date(System.currentTimeMillis());
System.out.println("["+dateFormat.format(date)+"] "+ctx.channel().remoteAddress() + " 已下线~");
}
/**
* 读取数据,并把读取到的数据转发给所有 客户
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
// 获取到 当前 Channel
Channel channel = channelHandlerContext.channel();
Date date = new Date(System.currentTimeMillis());
//遍历 ChannelGroup 根据不同的情况,推送不同的消息
channelGroup.forEach(ch -> {
if (ch != channel) {//遍历到的当前的 ch 不是发消息的 Channel
ch.writeAndFlush("[客户端] [" + dateFormat.format(date) + "] " + channel.remoteAddress() + " 发送了消息 :" + msg );
} else {
//回显自己发送的消息给自己
ch.writeAndFlush("[自己]发送了消息 [" + dateFormat.format(date) + "] " + msg);
}
});
}
/**
* 异常处理
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 关闭该通道
ctx.close();
}
}
/**
* @author xuan
* @create 2023/9/15
*/
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 InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//得到pipeline
ChannelPipeline pipeline = socketChannel.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 = channelFuture.channel();
System.out.println("-----------" + channel.localAddress() + "加入-----------------");
Scanner scanner=new Scanner(System.in);
//客户端输入信息
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
//发送数据到服务器端
channel.writeAndFlush(msg + "\r\n");
}
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
GroupChatClient client = new GroupChatClient("127.0.0.1", 8080);
client.run();
}
}
/**
* @author xuan
* @create 2023/9/15
*/
public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
System.out.println(msg.trim());
}
}
看一下服务器对应的ChannelHandler,首先服务器对应的handler要继承SimpleChannelInbouondHandler,这样才能让其变成一个ChannelHandler如下图:
再看一下里面我们重新的方法都是什么作用?当客户端成功连接服务端之后,会先执行handler里面的handlerAdded方法,然后再执行channelActive方法,如下图:
注意handlerAdded是在客户端与服务端第一次连接的时候被调用,因为此时相当于是把ChannelHandler假如到了Pipline通道里面了。
再来看下服务端ChannelHandler里面的channelRead0方法,如下图:
再来看下服务端ChannelHandler里面的handlerRemoved方法和channelInactive方法,这两个方法可以理解成作用相同,就是当连接在服务器上的客户端与服务器断开之后(比如客户端手动关闭),那么客户端与服务器之间的通道channel就会失活,这个时候服务端的channelInactive方法就会检测到,所以就会调用channelInactive方法,因此也就是说当断开客户端连接之后会执行channelInactive方法。而且我测试的当断开客户端连接之后不仅会执行channelInactive方法,还会执行handlerRemoved方法。
再来看下客户端对应的ChannelHandler处理器,如下图:
接下来分析完服务器和客户端对应的ChannelHandler之后,我们来看服务器对应的ChannelHandler中的一个非常重要的变量ChannelGroup,如下图:
为什么需要有个ChannelGroup放到服务器端呢?因为我们服务端必须知道所有连接它的客户端,这样服务端才能够给每个客户端发送消息,而服务器怎么保存客户端呢?每个客户端对应一个Channel通道,服务端就是保存的这个Channel从而识别连接它的客户端,它们之间是通过这个Channel通道发送数据的。而ChannelGroup可以理解成是一个保存Channel通道的集合,保存所有连接服务器的客户端。再来看下它里面两个重要的方法,一个是ChannelGroup#writeAndFlush方法,一个是ChannelGroup#add方法,如下图:
其中ChannelGroup#writeAndFlush方法会给集合里面所有的Channel通道也就是给所有的客户端都发送消息,这里我们无需遍历所有的Channel;而ChannelGroup#add方法是把当前成功连接到服务器的Channel假如到ChannelGroup当中;
接下来再看下如何遍历ChannelGroup里面的所有Channel,如下图:
遍历的时候是使用的forEach遍历的,并且Channel虽然是对象,但是它比较的时候却可以用!=符号进行比较,它的内部应该是实现了一些特殊规则,先这样理解。
最后我们测试一下上面的系统,打开一个服务器,打开三个客户端(注意这里要在idea里面设置一下让其允许同时打开多个main方法,因为idea默认只能打开一个main方法),如下图:
具体操作的时候可以打断点看看里面的方法的运行顺序,主要还是靠自己实际操作理解的比较牢固,因此这里就不做演示了,可以使用上面的代码自行演示。
Netty心跳机制实例
实例要求:
1.编写一个 Netty心跳检测机制案例, 当服务器超过3秒没有读时,就提示读空闲;
2.当服务器超过5秒没有写操作时,就提示写空闲;
3.实现当服务器超过7秒没有读或者写操作时,就提示读写空闲;
Netty的心跳机制的关键是:达到一定的时间后,如果没有读写,我们就执行一些操作,而我们执行的这些操作是通过ChannelHandler的userEventTriggered方法执行的。
服务端代码如下:
/**
* @author xuan
* @create 2023/9/18
*/
public class MyServer {
private static int port = 8080;
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO)) //在bossGroup增加一个日志处理器
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
ChannelPipeline pipeline = sc.pipeline();
//加入一个netty提供的 IdleStateHandler
//IdleStateHandler是netty提供的处理空闲状态的处理器
//参数:
// 1.readerIdleTime表示服务端多长时间没有读取客户端的数据了,会发送一个心跳检测包检测是否是连接状态
// 2.writerIdleTime表示服务端多长时间没有往客户端写入数据了,也会发送一个心跳检测包检测下是否是连接状态
// 3.allIdleTime表示多长时间既没有读也没有写,这个时候也会发送一个心跳检测包检测下是否是连接状态
//本例子中:3秒中没有读取到客户端数据就会触发netty的读事件,5秒钟没有写到客户端就会触发netty的写事件,7秒钟没有读写就会触发读写事件
//注:当IdleStateEvent心跳事件触发后,就会传递给管道的下一个handler去处理,通过调用下一个handler的userEventTriggered方法来实现
pipeline.addLast(new IdleStateHandler(3,5,7, TimeUnit.SECONDS));
//加入一个对空闲检测进一步处理的自定义的handler
pipeline.addLast(new MyServerHandler());
}
});
ChannelFuture future = serverBootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
再看一下心跳处理器Handler代码如下:
/**
* @author xuan
* @create 2023/9/18
*/
public class MyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
String strEventType="";
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent)evt;
switch (event.state()){
case ALL_IDLE:
strEventType="读写空闲";
break;
case READER_IDLE:
strEventType="读空闲";
break;
case WRITER_IDLE:
strEventType="写空闲";
break;
}
System.out.println(ctx.channel().remoteAddress() + "--超时事件--" + strEventType);
System.out.println("服务器处理中");
//如果不关闭 上述空闲会频发的发送
//ctx.channel().close();
}
}
}
上述开启了一个端口号为8080的本地服务器,在浏览器中输入127.0.0.1:8080之后就可以本地连接我们的服务器了。启动服务器之后,在浏览器输入地址后,看下idea控制台里面的输出内容,如下图:
心得:其实我们的netty主要是通过ChannelHandler处理器来解决问题的,首先我们开启一个Server服务器,然后我们用客户端连接这个服务器,最后会执行ChannelHandler处理器链。
WebSocket长连接开发
Netty通过WebSocket编程实现服务器和客户端长连接。
实例要求:
1.Http协议是无状态的,浏览器和服务器间的请求响应一下,下一次会重新创建连接。
2.要求:实现基于webSocket的长连接的全双工的交互。
3.改变Http协议多次请求的约束,实现长连接了,服务器可以发送消息给浏览器。所谓长连接也就是,服务器和客户端之间连接之后,在接下来的一段时间后,服务器向客户端发送请求的时候,他们之间无需再重新建立连接,可以直接发送。
4.客户端浏览器和服务端会相互感知,比如服务器关闭了。浏览器会感知,同样浏览器关闭了,服务器也会感知。
如果没有长连接的话,比如像我们在浏览器里面发送http请求的时候,如果想要给服务器发送数据,那么必须重新请求下http的地址,其实也就是必须重新建立客户端和服务端的连接,这样客户端才能把它的参数数据发送给服务端;但是如果要是有长连接的话,我们只发送一次url请求就行了,这样客户端就会和服务端保持连接了,我们客户端如果想要给服务端发送什么数据的话,直接发送就行了,必须要重新发送url请求建立连接。
下面是一个实际的例子,客户端发送来的是一个WebSocket长连接请求 ws://localhost:8080/hello,看一下服务端是怎么写ChannelHandler处理器的,如下图:
还有一个需要注意的地方,就是服务端在向客户端冲刷数据的时候,因为我们这里使用的是WebSocket协议,因此不能够直接冲刷,需要把信息包裹到TextWebSocketFrame里面再冲刷,如下图:
代码如下:
首先看下客户端代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
var socket;
//判断当前浏览器是否支持webSocket编程
if (window.WebSocket) {
//客户端通过WebSocket对象连接服务端
socket = new WebSocket("ws://localhost:8080/hello");
//相当于是channelRead0,ev 收到服务器端回送的消息。当服务端给客户端冲刷消息之后会调用这个方法
socket.onmessage = function(ev) {
let rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + ev.data;
}
//相当于是连接开启(感知到连接开启)。当客户端成功连接到服务端之后会调用这个方法
socket.onopen = function (ev) {
let rt = document.getElementById("responseText");
rt.value = "连接开启了";
}
//相当于是连接关闭(感知到连接关闭)。当客户端与服务端断开连接之后会调用这个方法。
socket.onclose = function (ev) {
let rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + "连接关闭了";
}
} else {
alert("当前浏览器不支持webSocket编程");
}
//发送消息到服务器
function send(message) {
//先判断socket是否创建好
if(!window.socket) {
return
}
if(socket.readyState == WebSocket.OPEN) {
//通过socket发送消息。通过WebSocket对象把信息从客户端发送到服务端
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>
再来看下服务端代码,如下:
/**
* @author xuan
* @create 2023/9/18
*/
public class MyServer {
private static int port = 8080;
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO)) //在bossGroup增加一个日志处理器
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
ChannelPipeline pipeline = sc.pipeline();
//因为基于http协议,因此我们要使用http的编码和解码器
pipeline.addLast(new HttpServerCodec());
//是以块方式写的,需要添加ChunkedWriteHandler处理器
pipeline.addLast(new ChunkedWriteHandler());
/**
* 说明:
* 1.因为http数据在传输过程中是会分段的,而HttpObjectAggregator可以将多个段聚合起来,这就是为什么当浏览器发送大量数据时,就会发出多次http请求
* */
pipeline.addLast(new HttpObjectAggregator(8192));
/**
* 说明:
* 1.对于WebSocket,它的数据是以帧(frame)的形式传递的,可以看到WebSocketFrame下面有六个子类
* 2.浏览器发送请求它的形式 ws://localhost:8080/hello 表示请求的url
* 3.WebSocketServerProtocolHandler的核心功能是将我们的http协议升级成ws协议,已实现保持长连接的作用
* */
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
//写一个自定义的handler,是我们真正的处理业务逻辑的,去处理客户端发送来的请求
pipeline.addLast(new MyTestWebSocketFrameHandler());
}
});
ChannelFuture future = serverBootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
下面是自定义的ChannelHandler处理器,如下:
/**
* @author xuan
* @create 2023/9/18
*
* TextWebSocketFrame这个类型表示的是一个文本帧
*/
public class MyTestWebSocketFrameHandler 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()));
}
@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();
}
}
结果如下图:
使用WebSocket协议的时候,客户端只需要和服务端建立一次连接,也就是上面的url地址只需要输入一次,后续可以不停的往服务端通过WebSocket冲刷信息,不需要刷新浏览器请求url;这和之前的http协议是有很大区别的,因为如果使用的是http协议,如果现在想要从客户端给服务端发送信息,那么每一次都要刷新下浏览器请求url去重新和客户端建立连接,就是发送一次消息建立一次连接。
netty编解码器机制简述
基本介绍:
1.编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码
2.codec(编码器)的组成部分有两个:decoder(解码器)负责把字节码数据转换成业务数据 和 encoder(编码器)负责把业务数据转换成字节码数据。
Netty提供的编码器
编码器 | 说明 |
---|---|
StringEncoder | 对字符串数据进行编码 |
ObjectEncoder | 对Java对象进行编码 |
Netty提供的解码器
解码器 | 说明 |
---|---|
StringDecoder | 对字符串数据进行解码 |
ObjectDecoder | 对Java对象进行解码 |
Netty本身的编码解码的机制和问题分析
1)Netty自身提供了一些codec(编解码器)
2)Netty提供的编码器
- StringEncoder,对字符串数据进行编码
- ObjectEncoder,对Java对象进行编码
3)Netty提供的解码器
- StringDecoder,对字符串进行解码
- ObjectDecoder,对Java对象进行解码
4)Netty本身自带的ObjectDecoder 和 ObjectEncoder 可以用来实现 POJO 对象或各种业务对象的编码和解码,底层使用的仍是 Java 序列化技术,而 Java 序列化技术本身效率就不高。存在如下问题:
- 无法跨语言
- 序列化后的体积太大,是二进制编码的5倍多
- 序列化性能太低
于是我们就提出了一个新的解决方案 【Google的protobuf】来处理上述的一些问题。
Protobuf相关知识
protobuf机制简述
- Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC[远程过程调用 remote procedure call ] 数据交换格式,protoBuf是一种语言无关,平台无关,可扩展的序列化结构数据的方法,它可用于(数据)通信协议,数据存储等。
- 目前很多公司 由http+json 转为了 tcp+protobuf,相比于json,protobuf占用的体积更小,更适合网络传输。所以在游戏开发中基本上都是使用tcp+protobuf进行数据传输,因为游戏中客户端和服务器端需要不断的传输信息。
- protoBuf序列化和反序列化的速度很快,要比json快很多。
- Protobuf 是以 message 的方式来管理数据的.
- 支持跨平台、跨语言,即[客户端和服务器端可以是不同的语言编写的] (支持目前绝大多数语言,例如 C++、 C#、Java、python 等)
- 高性能,高可靠性
- 使用 protobuf 编译器能自动生成代码,Protobuf 是将类的定义使用.proto 文件进行描述。说明,在 idea 中编 写 .proto 文件时,会自动提示是否下载 .ptotot 编写插件. 可以让语法高亮。
- 然后通过 protoc.exe 编译器根据.proto 自动生成.java 文件
- protoBuf是类似于json一样的数据描述语言(数据格式)
- protobuf 使用示意图如下:
参考文档:https://developers.google.com/protocol-buffers/docs/proto
Protobuf入门案例
编写程序,使用Protobuf完成如下功能:
1)客户端可以发送一个 Student PoJo 对象到服务器 (通过 Protobuf 编码)
2)服务端能接收 Student PoJo 对象,并显示信息(通过 Protobuf 解码)
第一步引入google的protobuf的依赖jar包,如下图:
第二步写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;
}
protoc.exe下载地址:https://github.com/protocolbuffers/protobuf/releases,如下图:
然后在bin目录下有个protoc.exe文件,如下图:
然后把我们想要执行的proto文件,这里是Student.proto文件和protoc.exe放到一起,如下图:
利用protoc.exe程序执行Student.proto生成新文件,如下图:
可以发现,我们生成了一个StudentPOJO.java类,而这个类的名字是根据Student.proto中的配置决定的,如下图:
Studnet.proto文件里面的每一个message,就表示一个java类,并且一个proto文件中可以写多个message,也就是一个StudentPOJO外部类中会包含很多个内部类,而我们的例子中先只写一个,看一下StudentPOJO类的结构,如下图:
可以发现目前是只有一个Student内部类。
接下来看下前端怎么通过protobuf给后端传输Student对象的相关数据,如下图:
可以发现前端会拿到外部类StudnetPOJO里面的内部类Student,然后给这个内部类赋值,赋完值之后把Student内部类冲刷给服务器;
再来看一下服务器里面是如何接收数据的,如下图:
相关代码如下:
/**
* @author xuan
* @create 2023/9/19
*/
public class Server {
public static void main(String[] args) throws IOException {
//创建BossGroup和WorkerGroup
//bossGroup和workerGroup含有的子线程的个数,默认为cpu核数*2
EventLoopGroup bossGroup=new NioEventLoopGroup(5);
EventLoopGroup workerGroup=new NioEventLoopGroup();
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap=new ServerBootstrap();
try {
//使用链式编程来进行设置
bootstrap.group(bossGroup,workerGroup) //设置两个线程
.channel(NioServerSocketChannel.class) //使用NioServerSocketChannel,作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG,128) //设置线程队列里连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true)//设置保持连接状态
.childHandler(new ChannelInitializer<SocketChannel>() { //handler对应的是boosGruop,childHandler对应的是workerGroup
//给pipeline设置处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//加入解码器
//指定对哪种对象进行解码
pipeline.addLast("decoder",new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
pipeline.addLast(new ServerHandler());
}
}); //给我们的workerGroup的EventLoop对应的管道设置处理器
System.out.println("服务器 is ready ...");
//绑定一个端口并且同步,生成一个ChannelFuture对象
ChannelFuture cf = bootstrap.bind(8080).sync();
//给 cf 注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if(future.isSuccess()){
System.out.println("监听端口成功");
}else{
System.out.println("监听端口失败");
}
}
});
//对关闭通道进行监听
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
/**
* @author xuan
* @create 2023/9/19
*/
public class ServerHandler extends SimpleChannelInboundHandler<StudentPOJO.Student> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, StudentPOJO.Student msg) throws Exception {
System.out.println("客户端发送的数据:"+msg.getId()+":"+msg.getName());
}
/**
* @Description 数据读取完毕
* @date 2020/7/23 10:46
* @param ctx
* @return void
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//将数据写入到缓冲并刷新
//一般来讲,我们队发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer(new Date().toString()+":"+"hello,客户端", CharsetUtil.UTF_8));
}
/**
* @Description 处理异常,一般是需要管理通道
* @date 2020/7/23 11:13
* @param ctx
* @param cause
* @return void
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.channel().close();
}
}
/**
* @author xuan
* @create 2023/9/19
*/
public class Client {
public static void main(String[] args) {
//客户端需要一个事件循环组
EventLoopGroup eventExecutors=new NioEventLoopGroup();
try {
//创建客户端启动对象
Bootstrap bootstrap=new Bootstrap();
//设置相关参数
bootstrap.group(eventExecutors) //设置线程组
.channel(NioSocketChannel.class) //设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//加入编码器
pipeline.addLast("encoder", new ProtobufEncoder());
pipeline.addLast(new ClientHandler()); //加入自己的处理器
}
});
System.out.println("客户端 ok...");
//启动客户端去连接服务器端
//关于ChannelFuture要分析,涉及到netty的异步模型
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("127.0.0.1", 8080)).sync();
//关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
eventExecutors.shutdownGracefully();
}
}
}
/**
* @author xuan
* @create 2023/9/19
*/
public class ClientHandler extends ChannelInboundHandlerAdapter {
/**
* @Description 当通道就绪时会触发该方法
* @date 2020/7/23 11:27
* @param ctx
* @return void
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
StudentPOJO.Student student = StudentPOJO.Student.newBuilder()
.setId(4)
.setName("重庆刘德华,香港徐大虾")
.build();
ctx.writeAndFlush(student);
}
/**
* @Description 当通道有读取事件时,会触发
* @date 2020/7/23 11:29
* @param ctx
* @param msg
* @return void
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf=(ByteBuf)msg;
System.out.println("服务器回复的消息:"+((ByteBuf) msg).toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址:"+ctx.channel().remoteAddress());
}
}