文章目录
- 1 Netty介绍和应用场景
- 2 JavaBIO编程
- 3 JavaNIO编程
- 4 Netty概述
- 5 Netty高性能架构设计
- 6 Netty核心模块组件
- 7 Google Protobuf
- 8 TCP粘包和拆包及解决方案
1 Netty介绍和应用场景
1.1 Netty的介绍
- Netty是由JBOSS提供的一个Java开源框架,现为Github上的独立项目。
- Netty是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序。
- Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输的应用。
- Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
5)要透彻理解Netty,需要先学习NIO,这样我们才能阅读Netty的源码。
1.2 Netty的应用场景
1.2.1 互联网行业
- 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用。
- 典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信
1.2.2 互联网行业
- 无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用
- Netty作为高性能的基础通信组件,提供了TCP/UDP和HTTP协议栈,方便定制和开发私有协议栈,账号登录服务器
- 地图服务器之间可以方便的通过Netty进行高性能的通信
1.2.3 大数据领域
- 经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通信
- 它的NettyService基于Netty框架二次封装实现。
1.2.4 其它开源项目使用到Netty
网址:https://netty.io/wiki/related-projects.html
2 JavaBIO编程
2.1 I/O模型
2.1.1 I/O模型基本说明
- I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
- Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO
- JavaBIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销【简单示意图】
- JavaNIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理【简单示意图】
- JavaAIO(NIO.2):异步非阻塞,AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
- 我们依次展开讲解
2.2 BIO、NIO、AIO适用场景分析
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。
- AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
2.3 JavaBIO基本介绍
- JavaBIO就是传统的java io编程,其相关的类和接口在java.io
- BIO(blockingI/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。【后有应用实例】
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解
2.4 JavaBIO工作机制
对BIO编程流程的梳理
- 服务器端启动一个ServerSocket
- 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯
- 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝
- 如果有响应,客户端线程会等待请求结束后,再继续执行
2.5 JavaBIO应用实例
实例说明:
- 使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯。
- 要求使用线程池机制改善,可以连接多个客户端。
- 服务器端可以接收客户端发送的数据(telnet方式即可)。
- 代码演示
package com.wolfx.bio;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @description: bio服务器
* @author: sukang
* @date: 2020-06-11 15:41
*/
public class BIOServer {
public static void main(String[] args) throws Exception{
//线程池机制
//思路
//1.创建一个线程池
//2.如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService executorService = Executors.newCachedThreadPool();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true){
System.out.println("线程信息id=" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接...");
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//就创建一个线程,与之通讯(单独写一个方法)
executorService.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];
//通过socket获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true) {
System.out.println("线程信息id="+Thread.currentThread().getId()+"名字="+Thread.currentThread().getName());
System.out.println("read....");
int read = inputStream.read(bytes);
if(read != -1){
System.out.println(new String(bytes,0,read));//输出客户端发送的数据
}else{
break;
}
}
} catch ( IOException e ) {
e.printStackTrace();
}finally {
System.out.println("关闭和client的连接");
try{
socket.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
2.6 JavaBIO问题分析
- 每个请求都需要创建独立的线程,与对应的客户端进行数据Read,业务处理,数据Write。
- 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在Read操作上,造成线程资源浪费
3 JavaNIO编程
3.1 JavaNIO基本介绍
- Java NIO全称java non-blocking IO,是指JDK提供的新API。从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,被统称为NIO(即New IO),是同步非阻塞的
- NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。【基本案例】
- NIO有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
- NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
- Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。【后面有案例说明】
- 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。
- HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级
- 案例说明NIO的Buffer
package com.wolfx.nio;
import java.nio.IntBuffer;
/**
* @description:
* @author: sukang
* @date: 2020-06-11 16:34
*/
public class BasicBuffer {
public static void main(String[] args) {
//举例说明Buffer的使用(简单说明)
//创建一个Buffer,大小为5,即可以存放5个int
IntBuffer intBuffer= IntBuffer.allocate(5);
//向buffer存放数据
for(int i=0; i<intBuffer.capacity(); i++){
intBuffer.put(i*2);
}
//如何从buffer读取数据
//将buffer转换,读写切换(!!!)
intBuffer.flip();
while(intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
}
3.2 NIO和BIO的比较
- BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多
- BIO是阻塞的,NIO则是非阻塞的
- BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
3.3 NIO三大核心原理示意图
一张图描述NIO的Selector、Channel和Buffer的关系
3.3.1Selector、Channel和Buffer的关系图(简单版)
关系图的说明:
- 每个channel都会对应一个Buffer
- Selector对应一个线程,一个线程对应多个channel(连接)
- 该图反应了有三个channel注册到该selector//程序
- 程序切换到哪个channel是由事件决定的,Event就是一个重要的概念
- Selector会根据不同的事件,在各个通道上切换
- Buffer就是一个内存块,底层是有一个数组
- 数据的读取写入是通过Buffer,这个和BIO,BIO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写,需要flip方法切换,channel是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的.
3.4 缓冲区(Buffer)
3.4.1 基本介绍
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer,如图:【后面举例说明】
3.4.2 Buffer类及其子类
- 在NIO中,Buffer是一个顶层父类,它是一个抽象类,类的层级关系图:
2) Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息:
- Buffer类相关方法一览
3.4.3 ByteBuffer
从前面可以看出对于Java中的基本数据类型(boolean除外),都有一个Buffer类型与之相对应,最常用的自然是ByteBuffer类(二进制数据),该类的主要方法如下:
3.5 通道(Channel)
3.5.1 基本介绍
- NIO的通道类似于流,但有些区别如下:
通道可以同时进行读写,而流只能读或者只能写
通道可以实现异步读写数据
通道可以从缓冲读数据,也可以写数据到缓冲
- BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。
- Channel在NIO中是一个接口
public interface Channel extends Closeable{} - 常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel。【ServerSocketChanne类似ServerSocket,SocketChannel类似Socket】
- FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写。
- 图示
3.5.2 FileChannel类
- FileChannel主要用来对本地文件进行IO操作,常见的方法有
- 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),把数据从当前通道复制给目标通道\
3.5.3 应用实例1-本地文件写数据
实例要求:
- 使用前面学习后的ByteBuffer(缓冲)和FileChannel(通道),将"hello,尚硅谷"写入到file01.txt中
- 文件不存在就创建
- 代码演示
package com.wolfx.nio;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @description:
* @author: sukang
* @date: 2020-06-12 10:33
*/
public class NIOFileChannel01 {
public static void main(String[] args) throws Exception{
String str = "hello,你好";
//创建一个输出流->channel
FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
//通过fileOutputStream获取对应的FileChannel
//这个fileChannel真实类型是FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
//创建一个缓冲区ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将str放入byteBuffer
byteBuffer.put(str.getBytes());
//对byteBuffer进行flip
byteBuffer.flip();
//将byteBuffer数据写入到fileChannel
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
3.5.4 应用实例2-本地文件读数据
实例要求:
- 使用前面学习后的ByteBuffer(缓冲)和FileChannel(通道),将file01.txt中的数据读入到程序,并显示在控制台屏幕
- 假定文件已经存在
- 代码演示
package com.wolfx.nio;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @description:
* @author: sukang
* @date: 2020-06-12 11:10
*/
public class NIOFileChannel02 {
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.allocate((int) file.length());
//将通道的数据读入到缓冲区
fileChannel.read(byteBuffer);
//将byteBuffer的字节数据转成String
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
3.5.5 应用实例3-使用一个Buffer完成文件读取、写入
实例要求:
- 使用FileChannel(通道)和方法read,write,完成文件的拷贝
- 拷贝一个文本文件1.txt,放在项目下即可
- 代码演示
package com.wolfx.nio;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @description:
* @author: sukang
* @date: 2020-06-12 11:26
*/
public class NIOFileChannel03 {
public static void main(String[] args) throws Exception{
File file = new File("1.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel channel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel channel1 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true){ //循环读取
//这里有个重要的操作,一定不要忘了
/**
* public final Buffer clear(){
* position=0;
* limit=capacity;
* mark=-1;
* return this;
* }
*/
byteBuffer.clear();//清空buffer
int read = channel.read(byteBuffer);
System.out.println("read=" + read);
if(read == -1){//表示读完
break;
}
//将buffer中的数据写入到channel1 -- 2.txt
byteBuffer.flip();
channel1.write(byteBuffer);
}
fileInputStream.close();
fileOutputStream.close();
}
}
3.5.6 应用实例4-拷贝文件transferFrom方法
- 实例要求:
- 使用FileChannel(通道)和方法transferFrom,完成文件的拷贝
- 拷贝一张图片
- 代码演示
package com.wolfx.nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
/**
* @description:
* @author: sukang
* @date: 2020-06-12 13:48
*/
public class NIOFileChannel04 {
public static void main(String[] args) throws Exception{
//创建相关流
FileInputStream fileInputStream = new FileInputStream("a.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("b.jpg");
//创建相关的通道
FileChannel source = fileInputStream.getChannel();
FileChannel target = fileOutputStream.getChannel();
//使用transferFrom完成拷贝
target.transferFrom(source,0, source.size());
source.close();
target.close();
fileInputStream.close();
fileInputStream.close();
}
}
3.5.7 关于Buffer和Channel的注意事项和细节
- ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有BufferUnderflowException异常。[举例说明]
package com.wolfx.nio;
import java.nio.ByteBuffer;
/**
* @description:
* @author: sukang
* @date: 2020-06-12 14:00
*/
public class NIOByteBufferPutGet {
public static void main(String[] args) {
//创建一个Buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
//类型化方式放入数据
buffer.putInt(100);
buffer.putLong(9);
buffer.putChar('苏');
buffer.putShort((short) 4);
//取出
buffer.flip();
System.out.println();
System.out.println(buffer.getInt());
System.out.println(buffer.getLong());
System.out.println(buffer.getChar());
System.out.println(buffer.getShort());
}
}
- 可以将一个普通Buffer转成只读Buffer[举例说明]
package com.wolfx.nio;
import java.nio.ByteBuffer;
/**
* @description:
* @author: sukang
* @date: 2020-06-12 14:10
*/
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);
}
}
3.6 Selector(选择器)
3.6.1 基本介绍
- Java的NIO,用非阻塞的IO方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
- Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。【示意图】
- 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
- 避免了多线程之间的上下文切换导致的开销
3.6.2 Selector示意图和特点说明
说明如下:
- Netty的IO线程NioEventLoop聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
- 当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
- 线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
- 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起。
- 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
3.6.3 Selector类相关方法
Selector类是一个抽象类,常用方法和说明如下:
3.6.4 注意事项
- NIO中的ServerSocketChannel功能类似ServerSocket,SocketChannel功能类似Socket
- selector相关方法说明
selector.select()//阻塞
selector.select(1000);//阻塞1000毫秒,在1000毫秒后返回
selector.wakeup();//唤醒selector
selector.selectNow();//不阻塞,立马返还
3.7 NIO非阻塞网络编程原理分析图
NIO非阻塞网络编程相关的(Selector、SelectionKey、ServerScoketChannel和SocketChannel)关系梳理图
对上图的说明:
- 当客户端连接时,会通过ServerSocketChannel得到SocketChannel
- Selector进行监听select方法,返回有事件发生的通道的个数.
- 将socketChannel注册到Selector上,register(Selectorsel,intops),一个selector上可以注册多个SocketChannel
- 注册后返回一个SelectionKey,会和该Selector关联(集合)
- 进一步得到各个SelectionKey(有事件发生)
- 在通过SelectionKey反向获取SocketChannel,方法channel()
- 可以通过得到的channel,完成业务处理
3.8 NIO非阻塞网络编程快速入门
案例要求:
- 编写一个NIO入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)
- 目的:理解NIO非阻塞网络编程机制
- 代码演示
NIOServer
package com.wolfx.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* @description:
* @author: sukang
* @date: 2020-06-12 15:29
*/
public class NIOServer {
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);
//循环等待客户端连接
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();
//遍历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));
}
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();
}
}
}
}
NIOClient
package com.wolfx.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @description:
* @author: sukang
* @date: 2020-06-12 16:08
*/
public class NIOClient {
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...";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//发送数据,将buffer数据写入channel
socketChannel.write(buffer);
System.in.read();
}
}
3.9 SelectionKey
-
SelectionKey,表示Selector和网络通道的注册关系,共四种:
int OP_ACCEPT:有新的网络连接可以accept,值为16
int OP_CONNECT:代表连接已经建立,值为8
int OP_READ:代表读操作,值为1
int 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; -
SelectionKey相关方法
3.10 ServerSocketChannel
- ServerSocketChannel在服务器端监听新的客户端Socket连接
- 相关方法如下
3.11 SocketChannel
- SocketChannel,网络IO通道,具体负责进行读写操作。NIO把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
- 相关方法如下
3.12 NIO网络编程应用实例-群聊系统
实例要求:
- 编写一个NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
- 实现多人群聊
- 服务器端:可以监测用户上线,离线,并实现消息转发功能
- 客户端:通过channel可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)
- 目的:进一步理解NIO非阻塞网络编程机制
- 示意图分析和代码
代码:
package com.wolfx.nio.groupchat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* @description:
* @author: sukang
* @date: 2020-06-12 16:39
*/
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(){
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){
//取到关联的channel
SocketChannel channel = null;
try {
//得到channel
channel = (SocketChannel)key.channel();
//创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
if(count > 0){
//把缓存区的数据转成字符串
String msg = new String(buffer.array());
//输出该消息
System.out.println("from 客户端:" + msg);
//向其它的客户端转发消息(去掉自己),专门写一个方法来处理
sendInfoToOtherClients(msg,channel);
}
} catch ( IOException e ) {
try {
System.out.println(channel.getRemoteAddress() + "离线了..");
//取消注册
key.cancel();
//关闭通道
channel.close();
} catch ( IOException ex ) {
ex.printStackTrace();
}
}
}
//转发消息给其他客户(通道)
private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException{
System.out.println("服务器转发消息中...");
//遍历 所有注册到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();
}
}
package com.wolfx.nio.groupchat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
/**
* @description:
* @author: sukang
* @date: 2020-06-22 16:05
*/
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 ( IOException e ) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception{
//启动我们客户端
final GroupChatClient chatClient=new GroupChatClient();
//启动一个线程,每个3秒,读取从服务器发送数据
new Thread(){
@Override
public void run(){
while (true){
chatClient.readInfo();
try {
sleep(3000);
} catch ( InterruptedException e ) {
e.printStackTrace();
}
}
}
}.start();
//发送数据给服务器端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String s = scanner.nextLine();
chatClient.sendInfo(s);
}
}
}
3.13 NIO与零拷贝
3.13.1 零拷贝基本介绍
- 零拷贝是网络编程的关键,很多性能优化都离不开。
- 在Java程序中,常用的零拷贝有mmap(内存映射)和sendFile。那么,他们在OS里,到底是怎么样的一个的设计?我们分析mmap和sendFile这两个零拷贝
- 另外我们看下NIO中如何使用零拷贝
3.13.2 传统IO数据读写
初学 Java 时,我们在学习 IO 和 网络编程时,会使用以下代码:
File file = new File("index.html");
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);
我们会调用read方法读取index.html的内容—变成字节数组,然后调用write方法,将index.html字节流写到socket中,那么,我们调用者两个方法,在OS底层发生了什么呢?我这里借鉴了一张其他文字的图片,尝试解释这个过程
上图中,上半部分表示用户态和内核态的上下文切换。下半部分表示数据复制操作。下面说说他们的步骤:
- read 调用导致用户态到内核态的一次变化,同时,第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU 拷贝数据到内存,而是 DMA 引擎传输数据到内存,用于解放 CPU) 引擎从磁盘读取 index.html 文件,并将数据放入到内核缓冲区。
- 发生第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,同时,发生了一次用内核态到用户态的上下文切换。
- 发生第三次数据拷贝,我们调用 write 方法,系统将用户缓冲区的数据拷贝到 Socket 缓冲区。此时,又发生了一次用户态到内核态的上下文切换。
- 第四次拷贝,数据异步的从 Socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。这一段,不需要进行上下文切换。
- write 方法返回,再次从内核态切换到用户态。
如你所见,复制拷贝操作太多了。如何优化这些流程?
mmap 优化
mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。如下图:
如上图,user buffer 和 kernel buffer 共享 index.html。如果你想把硬盘的 index.html 传输到网络中,再也不用拷贝到用户空间,再从用户空间拷贝到 Socket 缓冲区。
现在,你只需要从内核缓冲区拷贝到 Socket 缓冲区即可,这将减少一次内存拷贝(从 4 次变成了 3 次),但不减少上下文切换次数。
sendFile
那么,我们还能继续优化吗? Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。
如上图,我们进行 sendFile 系统调用时,数据被 DMA 引擎从文件复制到内核缓冲区,然后调用,然后掉一共 write 方法时,从内核缓冲区进入到 Socket,这时,是没有上下文切换的,因为在一个用户空间。
最后,数据从 Socket 缓冲区进入到协议栈。
此时,数据经过了 3 次拷贝,3 次上下文切换。
那么,还能不能再继续优化呢? 例如直接从内核缓冲区拷贝到网络协议栈?
实际上,Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图:
现在,index.html 要从文件进入到网络协议栈,只需 2 次拷贝:第一次使用 DMA 引擎从文件拷贝到内核缓冲区,第二次从内核缓冲区将数据拷贝到网络协议栈;内核缓存区只会拷贝一些 offset 和 length 信息到 SocketBuffer,基本无消耗。
等一下,不是说零拷贝吗?为什么还是要 2 次拷贝?
答:首先我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据,sendFile 2.1 版本实际上有 2 份数据,算不上零拷贝)。例如我们刚开始的例子,内核缓存区和 Socket 缓冲区的数据就是重复的。
而零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。
再稍微讲讲 mmap 和 sendFile 的区别。
- mmap 适合小数据量读写,sendFile 适合大文件传输。
- mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
- sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
在这个选择上:rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。
Java 世界的例子
kafka 在客户端和 broker 进行数据传输时,会使用 transferTo 和 transferFrom 方法,即对应 Linux 的 sendFile。
tomcat 内部在进行文件拷贝的时候,也会使用 transferto 方法。
tomcat 在处理一下心跳保活时,也会调用该 sendFile 方法。
在 pulsar 项目中,下载文件时,也会使用 sendFile。如下图:
所以,如果你需要优化网络传输的性能,或者文件读写的速度,请尽量使用零拷贝。他不仅能较少复制拷贝次数,还能较少上下文切换,缓存行污染。
3.14 JavaAIO基本介绍
- JDK7引入了AsynchronousI/O,即AIO。在进行I/O编程中,常用到两种模式:Reactor和Proactor。Java的NIO就是Reactor,当有事件触发时,服务器端得到通知,进行相应的处理
- AIO即NIO2.0,叫做异步不阻塞的IO。AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
3.15 BIO、NIO、AIO对比表
4 Netty概述
4.1 原生NIO存在的问题
- NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
- 需要具备其他的额外技能:要熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序。
- 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
- JDK NIO的Bug:例如臭名昭著的Epoll Bug,它会导致Selector空轮询,最终导致CPU100%。直到JDK1.7版本该问题仍旧存在,没有被根本解决。
4.2 Netty官网说明
4.3 Netty的优点
Netty对JDK自带的NIO的API进行了封装,解决了上述问题。
- 设计优雅:适用于各种传输类型的统一API阻塞和非阻塞Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型-单线程,一个或多个线程池.
- 使用方便:详细记录的Javadoc,用户指南和示例;没有其他依赖项,JDK5(Netty3.x)或6(Netty4.x)就足够了。
- 高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。
- 安全:完整的SSL/TLS和StartTLS支持。
- 社区活跃、不断更新:社区活跃,版本迭代周期短,发现的Bug可以被及时修复,同时,更多的新功能会被加入
4.4 Netty版本说明
- netty版本分为netty3.x和netty4.x、netty5.x
- 因为Netty5出现重大bug,已经被官网废弃了,目前推荐使用的是Netty4.x的稳定版本
- 目前在官网可下载的版本netty3.xnetty4.0.x和netty4.1.x
- 在本套课程中,我们讲解Netty4.1.x版本
- netty下载地址:https://bintray.com/netty/downloads/netty/
5 Netty高性能架构设计
5.1 线程模型基本介绍
- 不同的线程模式,对程序的性能有很大影响,为了搞清Netty线程模式,我们来系统的讲解下各个线程模式,最后看看Netty线程模型有什么优越性.
- 目前存在的线程模型有:
传统阻塞I/O服务模型
Reactor模式 - 根据Reactor的数量和处理资源池线程的数量不同,有3种典型的实现
单Reactor单线程;
单Reactor多线程;
主从Reactor多线程 - Netty线程模式(Netty主要基于主从Reactor多线程模型做了一定的改进,其中主从Reactor多线程模型有多个Reactor)
5.2 传统阻塞I/O服务模型
5.2.1 工作原理图
- 黄色的框表示对象,蓝色的框表示线程
- 白色的框表示方法(API)
5.2.2 模型特点
- 采用阻塞IO模式获取输入的数据
- 每个连接都需要独立的线程完成数据的输入,业务处理,数据返回
5.2.3 问题分析
- 当并发数很大,就会创建大量的线程,占用很大系统资源
- 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程资源浪费
5.3 Reactor模式
5.3.1 针对传统阻塞I/O服务模型的2个缺点,解决方案:
- 基于I/O复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理Reactor对应的叫法:1.反应器模式 2.分发者模式(Dispatcher) 3.通知者模式(notifier)
- 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。
5.3.2 I/O复用结合线程池,就是Reactor模式基本设计思想,如图
对上图说明:
- Reactor模式,通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动)
- 服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,因此Reactor模式也叫Dispatcher模式
- Reactor模式使用IO复用监听事件,收到事件后,分发给某个线程(进程),这点就是网络服务器高并发处理关键
5.3.3 Reactor模式中核心组成:
- Reactor:Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;
- Handlers:处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作。
5.3.4 Reactor模式分类:
根据Reactor的数量和处理资源池线程的数量不同,有3种典型的实现
- 单Reactor单线程
- 单Reactor多线程
- 主从Reactor多线程
5.4 单Reactor单线程
原理图,并使用NIO群聊系统验证
5.4.1 方案说明:
- Select是前面I/O复用模型介绍的标准网络编程API,可以实现应用程序通过一个阻塞对象监听多路连接请求
- Reactor对象通过Select监控客户端请求事件,收到事件后通过Dispatch进行分发
- 如果是建立连接请求事件,则由Acceptor通过Accept处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理
- 如果不是建立连接事件,则Reactor会分发调用连接对应的Handler来响应
- Handler会完成Read→业务处理→Send的完整业务流程
结合实例:服务器端用一个线程通过多路复用搞定所有的IO操作(包括连接,读、写等),编码简单,清晰明了,但是如果客户端连接数量较多,将无法支撑,前面的NIO案例就属于这种模型。
5.4.2 方案优缺点分析:
- 优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
- 缺点:性能问题,只有一个线程,无法完全发挥多核CPU的性能。Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈
- 缺点:可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
- 使用场景:客户端的数量有限,业务处理非常快速,比如Redis在业务处理的时间复杂度O(1)的情况
5.5 单Reactor多线程
5.5.1 原理图
5.5.2 对上图的小结
- Reactor对象通过select监控客户端请求事件,收到事件后,通过dispatch进行分发
- 如果建立连接请求,则又Acceptor通过accept处理连接请求,然后创建一个Handler对象处理完成连接后的各种事件
- 如果不是连接请求,则由reactor分发调用连接对应的handler来处理
- handler只负责响应事件,不做具体的业务处理,通过read读取数据后,会分发给后面的worker线程池的某个线程处理业务
- worker线程池会分配独立线程完成真正的业务,并将结果返回给handler
- handler收到响应后,通过send将结果返回给client
5.5.3 方案优缺点分析:
- 优点:可以充分的利用多核cpu的处理能力
- 缺点:多线程数据共享和访问比较复杂,reactor处理所有的事件的监听和响应,在单线程运行,在高并发场景容易出现性能瓶颈.
5.6 主从Reactor多线程
5.6.1 工作原理图
针对单Reactor多线程模型中,Reactor在单线程中运行,高并发场景下容易成为性能瓶颈,可以让Reactor在多线程中运行
5.6.2 上图的方案说明
- Reactor主线程MainReactor对象通过select监听连接事件,收到事件后,通过Acceptor处理连接事件
- 当Acceptor处理连接事件后,MainReactor将连接分配给SubReactor
- subreactor将连接加入到连接队列进行监听,并创建handler进行各种事件处理
- 当有新事件发生时,subreactor就会调用对应的handler处理
- handler通过read读取数据,分发给后面的worker线程处理
- worker线程池分配独立的worker线程进行业务处理,并返回结果
- handler收到响应的结果后,再通过send将结果返回给client
- Reactor主线程可以对应多个Reactor子线程,即MainRecator可以关联多个SubReactor
5.6.3ScalableIOinJava对MultipleReactors的原理图解:
5.6.4方案优缺点说明:
- 优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
- 优点:父线程与子线程的数据交互简单,Reactor主线程只需要把新连接传给子线程,子线程无需返回数据。
- 缺点:编程复杂度较高
- 结合实例:这种模型在许多项目中广泛使用,包括Nginx主从Reactor多进程模型,Memcached主从多线程,Netty主从多线程模型的支持
5.7 Reactor模式小结
5.7.1 3种模式用生活案例来理解
- 单Reactor单线程,前台接待员和服务员是同一个人,全程为顾客服
- 单Reactor多线程,1个前台接待员,多个服务员,接待员只负责接待
- 主从Reactor多线程,多个前台接待员,多个服务生
5.7.2 Reactor模式具有如下的优点:
- 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的
- 可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销
- 扩展性好,可以方便的通过增加Reactor实例个数来充分利用CPU资源
- 复用性好,Reactor模型本身与具体事件处理逻辑无关,具有很高的复用性
5.8Netty模型
5.8.1 工作原理示意图1-简单版
Netty主要基于主从Reactors多线程模型(如图)做了一定的改进,其中主从Reactor多线程模型有多个Reactor
5.8.2 对上图说明
- BossGroup线程维护Selector,只关注Accecpt
- 当接收到Accept事件,获取到对应的SocketChannel,封装成NIOScoketChannel并注册到Worker线程(事件循环),并进行维护
- 当Worker线程监听到selector中通道发生自己感兴趣的事件后,就进行处理(就由handler),注意handler已经加入到通道
5.8.3 工作原理示意图2-进阶版
5.8.4 工作原理示意图-详细版
5.8.5 对上图的说明小结
- Netty抽象出两组线程池BossGroup专门负责接收客户端的连接,WorkerGroup专门负责网络的读写
- BossGroup和WorkerGroup类型都是NioEventLoopGroup
- NioEventLoopGroup相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是NioEventLoop
- NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket的网络通讯
- NioEventLoopGroup可以有多个线程,即可以含有多个NioEventLoop
- 每个BossNioEventLoop循环执行的步骤有3步
- 轮询accept事件
- 处理accept事件,与client建立连接,生成NioScocketChannel,并将其注册到某个workerNIOEventLoop上的selector
- 处理任务队列的任务,即runAllTasks
- 每个WorkerNIOEventLoop循环执行的步骤
- 轮询read,write事件
- 处理i/o事件,即read,write事件,在对应NioScocketChannel处理
- 处理任务队列的任务,即runAllTasks
- 每个WorkerNIOEventLoop处理业务时,会使用pipeline(管道),pipeline中包含了channel,即通过pipeline可以获取到对应通道,管道中维护了很多的处理器
5.8.6 Netty快速入门实例-TCP服务
- Netty服务器在6668端口监听,客户端能发送消息给服务器"hello,服务器~"
- 服务器可以回复消息给客户端"hello,客户端~"
- 目的:对Netty线程模型有一个初步认识,便于理解Netty模型理论
代码如下
package com.wolfx.netty.simple;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* @description:
* @author: sukang
* @date: 2020-06-23 14:43
*/
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();
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行设置
bootstrap.group(bossGroup, workerGroup)//设置两个线程组
.channel(NioServerSocketChannel.class)//使用NioSocketChanel作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128)//设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true)//设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>(){//创建一个通道测试对象(匿名对象)
//给pipeline设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyServerHandler());
}
});//给我们的workerGroup的EventLoop对应的管道设置处理器
System.out.println(".....服务器isready...");
//绑定一个端口并且同步,生成了一个ChannelFuture对象
//启动服务器(并绑定端口)
ChannelFuture cf=bootstrap.bind(6668).sync();
//对关闭通道进行监听
cf.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package com.wolfx.netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;
/**
* @description:
* 1.我们自定义一个Handler需要继续netty规定好的某个HandlerAdapter(规范)
* 2.这时我们自定义一个Handler,才能称为一个handler
* @author: sukang
* @date: 2020-06-23 16:01
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据实际(这里我们可以读取客户端发送的消息)
/**
* @description:
* 1.ChannelHandlerContextctx:上下文对象,含有管道pipeline,通道channel,地址
* 2.Object msg:就是客户端发送的数据默认Object
* @param ctx
* @param msg
* @return: void
* @author: sukang
* @date: 2020/6/23 16:03
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器读取线程" + Thread.currentThread().getName());
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,客户端~(>^ω^<)喵",CharsetUtil.UTF_8));
}
//处理异常,一般是需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
package com.wolfx.netty.simple;
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;
/**
* @description:
* @author: sukang
* @date: 2020-06-23 16:15
*/
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();
}
}
}
package com.wolfx.netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
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();
}
}
5.8.7 任务队列中的Task有3种典型使用场景
- 用户程序自定义的普通任务
- 用户自定义定时任务
- 非当前Reactor线程调用Channel的各种方法
例如在推送系统的业务线程里面,根据用户的标识,找到对应的Channel引用,然后调用Write类方法向该用户推送消息,就会进入到这种场景。最终的Write会提交到任务队列中后被异步消费 - 代码演示
package com.wolfx.netty.simple;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import java.util.concurrent.TimeUnit;
/*
说明
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 {
//比如这里我们有一个非常耗时长的业务-> 异步执行 -> 提交该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());
}
}
});
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵3", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
});
//解决方案2 : 用户自定义定时任务 -》 该任务是提交到 scheduleTaskQueue中
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵4", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
}, 5, TimeUnit.SECONDS);
System.out.println("go on ...");
// 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();
}
}
5.8.8 方案再说明
- Netty抽象出两组线程池,BossGroup专门负责接收客户端连接,WorkerGroup专门负责网络读写操作。
- NioEventLoop表示一个不断循环执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket网络通道。
- NioEventLoop内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由IO线程NioEventLoop负责
NioEventLoopGroup下包含多个NioEventLoop
每个NioEventLoop中包含有一个Selector,一个taskQueue
每个NioChannel只会绑定在唯一的NioEventLoop上
每个NioChannel都绑定有一个自己的ChannelPipeline
5.9 异步模型
5.9.1 基本介绍
- 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
- Netty中的I/O操作是异步的,包括Bind、Write、Connect等操作会简单的返回一个ChannelFuture。
- 调用者并不能立刻获得结果,而是通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果
- Netty的异步模型是建立在future和callback的之上的。callback就是回调。重点说Future,它的核心思想是:假设一个方法fun,计算过程可能非常耗时,等待fun返回显然不合适。那么可以在调用fun的时候,立马返回一个Future,后续可以通过Future去监控方法fun的处理过程(即:Future-Listener机制)
5.9.2 Future说明
- 表示异步的执行结果,可以通过它提供的方法来检测执行是否完成,比如检索计算等等.
- ChannelFuture是一个接口:publicinterfaceChannelFutureextendsFuture我们可以添加监听器,当监听的事件发生时,就会通知到监听器.案例说明
5.9.3 工作原理示意图
说明:
- 在使用Netty进行编程时,拦截操作和转换出入站数据只需要您提供callback或利用future即可。这使得链式操作简单、高效,并有利于编写可重用的、通用的代码。
- Netty框架的目标就是让你的业务逻辑从网络基础应用编码中分离出来、解脱出来
5.9.4 Future-Listener机制
- 当Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作。
- 常见有如下操作
通过isDone方法来判断当前操作是否完成;
通过isSuccess方法来判断已完成的当前操作是否成功;
通过getCause方法来获取已完成的当前操作失败的原因;
通过isCancelled方法来判断已完成的当前操作是否被取消;
通过addListener方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果Future对象已完成,则通知指定的监听器
5.10 快速入门实例-HTTP服务
- 实例要求:使用IDEA创建Netty项目
- Netty服务器在6668端口监听,浏览器发出请求"http://localhost:6668/"
- 服务器可以回复消息给客户端"Hello!我是服务器5",并对特定请求资源进行过滤.
- 目的:Netty可以做Http服务开发,并且理解Handler实例和客户端及其请求的关系.
- 看老师代码演示
package com.wolfx.netty.http;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* @description:
* @author: sukang
* @date: 2020-06-24 10:58
*/
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(6668).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package com.wolfx.netty.http;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
/**
* @description:
* @author: sukang
* @date: 2020-06-24 11:09
*/
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());
}
}
package com.wolfx.netty.http;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import java.net.URI;
/*
说明
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);
}
}
}
6 Netty核心模块组件
6.1 Bootstrap、ServerBootstrap
- Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类
- 常见的方法有
public ServerBootstrap group(EventLoopGroupparentGroup,EventLoopGroupchildGroup),该方法用于服务器端,用来设置两个EventLoop
public B group(EventLoopGroup group),该方法用于客户端,用来设置一个EventLoop
public B channel(Class<?extendsC> channelClass),该方法用来设置一个服务器端的通道实现
public B option(ChannelOptionoption, T value),用来给ServerChannel添加配置
public ServerBootstrap childOption(ChannelOption childOption,T value),用来给接收到的通道添加配置
public ServerBootstrap childHandler(ChannelHandler childHandler),该方法用来设置业务处理类(自定义的handler)
public ChannelFuture bind(int inetPort),该方法用于服务器端,用来设置占用的端口号
public ChannelFuture connect(String inetHost, int inetPort),该方法用于客户端,用来连接服务器端
6.2 Future、ChannelFuture
Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件
常见的方法有
Channel channel(),返回当前正在进行IO操作的通道
ChannelFuture sync(),等待异步操作执行完毕
6.3 Channel
- Netty网络通信的组件,能够用于执行网络I/O操作。
- 通过Channel可获得当前网络连接的通道的状态
- 通过Channel可获得网络连接的配置参数(例如接收缓冲区大小)
- Channel提供异步的网络I/O操作(如建立连接,读写,绑定端口),异步调用意味着任何I/O调用都将立即返回,并且不保证在调用结束时所请求的I/O操作已完成
- 调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以I/O操作成功、失败或取消时回调通知调用方
- 支持关联I/O操作与对应的处理程序
- 不同协议、不同的阻塞类型的连接都有不同的Channel类型与之对应,常用的Channel类型:
NioSocketChannel,异步的客户端TCPSocket连接。
NioServerSocketChannel,异步的服务器端TCPSocket连接。
NioDatagramChannel,异步的UDP连接。
NioSctpChannel,异步的客户端Sctp连接。
NioSctpServerChannel,异步的Sctp服务器端连接,这些通道涵盖了UDP和TCP网络IO以及文件IO。
6.4 Selector
- Netty基于Selector对象实现I/O多路复用,通过Selector一个线程可以监听多个连接的Channel事件。
- 当向一个Selector中注册Channel后,Selector内部的机制就可以自动不断地查询(Select)这些注册的Channel是否有已就绪的I/O事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个Channel
6.5 ChannelHandler及其实现类
- ChannelHandler是一个接口,处理I/O事件或拦截I/O操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。
- ChannelHandler本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类
- ChannelHandler及其实现类一览图(后)
- 我们经常需要自定义一个Handler类去继承ChannelInboundHandlerAdapter,然后通过重写相应方法实现业务逻辑,我们接下来看看一般都需要重写哪些方法
6.6 Pipeline和ChannelPipeline
ChannelPipeline是一个重点:
- ChannelPipeline是一个Handler的集合,它负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿Netty的链。(也可以这样理解:ChannelPipeline是保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作)
- ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互
- 在Netty中每个Channel都有且仅有一个ChannelPipeline与之对应,它们的组成关系如下
- 常用方法
ChannelPipeline addFirst(ChannelHandler…handlers),把一个业务处理类(handler)添加到链中的第一个位置
ChannelPipeline addLast(ChannelHandler…handlers),把一个业务处理类(handler)添加到链中的最后一个位置
6.7 ChannelHandlerContext
- 保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象
- 即ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler,同时ChannelHandlerContext中也绑定了对应的pipeline和Channel的信息,方便对ChannelHandler进行调用.
- 常用方法
6.8 ChannelOption
- Netty在创建Channel实例后,一般都需要设置ChannelOption参数。
- ChannelOption参数如下:
6.9 EventLoopGroup和其实现类NioEventLoopGroup
- EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例。
- EventLoopGroup提供next接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务。在Netty服务器端编程中,我们一般都需要提供两个EventLoopGroup,例如:BossEventLoopGroup和WorkerEventLoopGroup。
- 通常一个服务端口即一个ServerSocketChannel对应一个Selector和一个EventLoop线程。BossEventLoop负责接收客户端的连接并将SocketChannel交给WorkerEventLoopGroup来进行IO处理,如下图所示
- 常用方法
public NioEventLoopGroup(),构造方法
public Future<?> shutdownGracefully(),断开连接,关闭线程
6.10Unpooled类
- Netty提供一个专门用来操作缓冲区(即Netty的数据容器)的工具类
- 常用方法如下所示
7 Google Protobuf
7.1 编码和解码的基本介绍
- 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码
- codec(编解码器)的组成部分有两个:decoder(解码器)和encoder(编码器)。encoder负责把业务数据转换成字节码数据,decoder负责把字节码数据转换成业务数据
7.2 Netty本身的编码解码的机制和问题分析
- Netty自身提供了一些codec(编解码器)
- Netty提供的编码器
StringEncoder,对字符串数据进行编码
ObjectEncoder,对Java对象进行编码
… - Netty提供的解码器
StringDecoder,对字符串数据进行解码
ObjectDecoder,对Java对象进行解码
… - Netty本身自带的ObjectDecoder和ObjectEncoder可以用来实现POJO对象或各种业务对象的编码和解码,底层使用的仍是Java序列化技术,而Java序列化技术本身效率就不高,存在如下问题无法跨语言
序列化后的体积太大,是二进制编码的5倍多。
序列化性能太低 - =>引出新的解决方案[Google的Protobuf]
7.3 Protobuf
- Protobuf基本介绍和使用示意图
- Protobuf是Google发布的开源项目,全称GoogleProtocolBuffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或RPC[远程过程调用remoteprocedurecall]数据交换格式。
目前很多公司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文件
- protobuf使用示意图
8 TCP粘包和拆包及解决方案
8.1 TCP粘包和拆包基本介绍
- TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
- 由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题,看一张图
- 示意图TCP粘包、拆包图解
对图的说明:
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
- 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
- 服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包
- 服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包
- 服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。
8.3TCP粘包和拆包解决方案
- 使用自定义协议+编解码器来解决
- 关键就是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的TCP粘包、拆包。
8.4 看一个具体的实例:
- 要求客户端发送5个Message对象,客户端每次发送一个Message对象
- 服务器端每次接收一个Message,分5次进行解码,每读取到一个Message,会回复一个Message对象给客户端.