IO、NIO、Netty

2017/01/15 Java

File

文件和目录(文件夹)路径名的抽象表示形式

仅仅是一个路径的表示,不代码具体的事物一定是存在的。

list()方法

通过参数FilenameFilter限定符合要求的文件或目录

// 封装e判断目录
File file = new File("d:\\");

// 获取该目录下所有文件或者文件夹的String数组
// public String[] list(FilenameFilter filter)
String[] strArray = file.list(new FilenameFilter() {
	@Override
	public boolean accept(File dir, String name) {
		return new File(dir, name).isFile() && name.endsWith(".jpg");
	}
});

// 遍历
for (String s : strArray) {
	System.out.println(s);
}

递归遍历文件夹

private static void getAllJavaFilePaths(File srcFolder) {
	// 获取该目录下所有的文件或者文件夹的File数组
	File[] fileArray = srcFolder.listFiles();

	// 遍历该File数组,得到每一个File对象
	for (File file : fileArray) {
		// 判断该File对象是否是文件夹
		if (file.isDirectory()) {
			getAllJavaFilePaths(file);
		} else {
			// 继续判断是否以.java结尾
			if (file.getName().endsWith(".java")) {
				// 就输出该文件的绝对路径
				System.out.println(file.getAbsolutePath());
			}
		}
	}
}

IO流

分类

  • 按照数据流向
    • 输入流
    • 输出流
  • 按照数据类型(默认)
    • 字节流
      • 字节输入流
      • 字节输出流
    • 字符流
      • 字符输入流
      • 字符输出流

基类

  • 字节流的抽象基类及子类:
    • InputStream
      • FileInputStream
      • BufferedInputStream
    • OutputStream
      • FileOutputStream
      • BufferedOutputStream
  • 字符流的抽象基类及子类:
    • Reader
      • FileReader
      • BufferedReader
    • Writer
      • FileWriter
      • BufferedWriter

IO流小结图解

字节流

FileOutputStream和FileInputStream

// 基本字节流一次读写一个字节数组
public static void method2(String srcString, String destString)
		throws IOException {
	FileInputStream fis = new FileInputStream(srcString);
	FileOutputStream fos = new FileOutputStream(destString);

	byte[] bys = new byte[1024];
	int len = 0;
	while ((len = fis.read(bys)) != -1) {
		fos.write(bys, 0, len);
	}

	fos.close();
	fis.close();
}

// 基本字节流一次读写一个字节
public static void method1(String srcString, String destString)
		throws IOException {
	FileInputStream fis = new FileInputStream(srcString);
	FileOutputStream fos = new FileOutputStream(destString);

	int by = 0;
	while ((by = fis.read()) != -1) {
		fos.write(by);
	}

	fos.close();
	fis.close();
}

换行符

  • windows:\r\n
  • linux:\n
  • Mac:\r

追加文件

// 创建一个向具有指定 name 的文件中写入数据的输出文件流。
// 如果第二个参数为 true,则将字节写入文件末尾处,而不是写入文件开始处。
FileOutputStream fos = new FileOutputStream("fos3.txt", true);

创建字节输出流对象做了几件事情

  1. 调用系统功能去创建文件
  2. 创建fos对象
  3. 把fos对象指向这个文件

字节缓冲区流

BufferedOutputStream和BufferedInputStream

// 高效字节流一次读写一个字节数组:
public static void method4(String srcString, String destString)
		throws IOException {
	BufferedInputStream bis = new BufferedInputStream(new FileInputStream(
			srcString));
	//为什么不传递一个具体的文件或者文件路径,而是传递一个OutputStream对象?
	//因为字节缓冲区流仅仅提供缓冲区,为高效而设计的。真正的读写操作还是基本的流对象实现。

	//构造方法可以指定缓冲区的大小,但是我们一般不用,默认缓冲区大小就够了
	BufferedOutputStream bos = new BufferedOutputStream(
			new FileOutputStream(destString));

	byte[] bys = new byte[1024];
	int len = 0;
	while ((len = bis.read(bys)) != -1) {
		bos.write(bys, 0, len);
	}

	bos.close();
	bis.close();
}

// 高效字节流一次读写一个字节:
public static void method3(String srcString, String destString)
		throws IOException {
	BufferedInputStream bis = new BufferedInputStream(new FileInputStream(
			srcString));
	BufferedOutputStream bos = new BufferedOutputStream(
			new FileOutputStream(destString));

	int by = 0;
	while ((by = bis.read()) != -1) {
		bos.write(by);

	}

	bos.close();
	bis.close();
}

字符流

计算机是如何识别什么时候该把两个字节转换为一个中文呢?

在计算机中中文的存储分两个字节:

第一个字节肯定是负数。
第二个字节常见的是负数,可能有正数。但是没影响。

// String s = "abcde";
// // [97, 98, 99, 100, 101]

String s = "我爱你中国";
// [-50, -46, -80, -82, -60, -29, -42, -48, -71, -6]

byte[] bys = s.getBytes();
System.out.println(Arrays.toString(bys));

由于字节流操作中文不是特别方便,所以,java就提供了转换流。
转换流其实是一个字符流

字符流=字节流+编码表。

编码表

计算机只能识别二进制数据,早期由来是电信号。
为了方便应用计算机,让它可以识别各个国家的文字。
就将各个国家的文字用数字来表示,并一一对应,形成一张表。

ASCII:美国标准信息交换码。用一个字节的7位可以表示。
ISO8859-1:拉丁码表。欧洲码表用一个字节的8位表示。
GB2312:中国的中文编码表。
GBK:中国的中文编码表升级,融合了更多的中文文字符号。
GB18030:GBK的取代版本
BIG-5码 :通行于台湾、香港地区的一个繁体字编码方案,俗称“大五码”。
Unicode:国际标准码,融合了多种文字。所有文字都用两个字节来表示,Java语言使用的就是unicode
UTF-8:最多用三个字节来表示一个字符。

UTF-8不同,它定义了一种“区间规则”,这种规则可以和ASCII编码保持最大程度的兼容:
它将Unicode编码为00000000-0000007F的字符,用单个字节来表示
它将Unicode编码为00000080-000007FF的字符用两个字节表示
它将Unicode编码为00000800-0000FFFF的字符用3字节表示 

OutputStreamWriter和InputStreamReader

InputStreamReader isr = new InputStreamReader(new FileInputStream(
		"a.txt"));
// 封装目的地
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(
		"b.txt"));

// 读写数据
// 方式1
// int ch = 0;
// while ((ch = isr.read()) != -1) {
// osw.write(ch);
// }

// 方式2
char[] chs = new char[1024];
int len = 0;
while ((len = isr.read(chs)) != -1) {
	osw.write(chs, 0, len);
	// osw.flush();
}

// 释放资源
osw.close();
isr.close();

FileWriter和FileReader

使用默认本地编码表的InputStreamReader和OutputStreamReader封装类

// 基本字符流一次读写一个字符数组
private static void method2(String srcString, String destString)
		throws IOException {
	FileReader fr = new FileReader(srcString);
	FileWriter fw = new FileWriter(destString);

	char[] chs = new char[1024];
	int len = 0;
	while ((len = fr.read(chs)) != -1) {
		fw.write(chs, 0, len);
	}

	fw.close();
	fr.close();
}

// 基本字符流一次读写一个字符
private static void method1(String srcString, String destString)
		throws IOException {
	FileReader fr = new FileReader(srcString);
	FileWriter fw = new FileWriter(destString);

	int ch = 0;
	while ((ch = fr.read()) != -1) {
		fw.write(ch);
	}

	fw.close();
	fr.close();
}

BufferedReader和BufferedWriter

// 字符缓冲流一次读写一个字符串
private static void method5(String srcString, String destString,String encoding)
		throws IOException {
	BufferedReader br = new BufferedReader(
							new InputStreamReader(new FileInputStream(srcString),encoding));
	BufferedWriter bw = new BufferedWriter(
							new OutputStreamWriter(new FileOutputStream(destString),encoding));

	String line = null;
	while ((line = br.readLine()) != null) {
		bw.write(line);
		bw.newLine();
		bw.flush();
	}

	bw.close();
	br.close();
}

// 字符缓冲流一次读写一个字符数组
private static void method4(String srcString, String destString,String encoding)
		throws IOException {
	BufferedReader br = new BufferedReader(
							new InputStreamReader(new FileInputStream(srcString),encoding));
	BufferedWriter bw = new BufferedWriter(
							new OutputStreamWriter(new FileOutputStream(destString),encoding));

	char[] chs = new char[1024];
	int len = 0;
	while ((len = br.read(chs)) != -1) {
		bw.write(chs, 0, len);
	}

	bw.close();
	br.close();
}
// 字符缓冲流一次读写一个字符
private static void method3(String srcString, String destString,String encoding)
		throws IOException {
	BufferedReader br = new BufferedReader(
							new InputStreamReader(new FileInputStream(srcString),encoding));
	BufferedWriter bw = new BufferedWriter(
							new OutputStreamWriter(new FileOutputStream(destString),encoding));

	int ch = 0;
	while ((ch = br.read()) != -1) {
		bw.write(ch);
	}

	bw.close();
	br.close();
}

缓冲流与普通流区别

在FileInputStream里有一个说明是说此方法将阻塞,意思就是说在你读一个文件输入流的时候,当读到某个位置的时候,如果做一些其他处理(比如说接受一部分字节做一些处理等等)这个时候输入流在什么位置就是什么位置,不会继续往下读,而BufferedInputStream虽然也有一个read方法,但是从名字就可以看出,它带有一个缓冲区,它是一个非阻塞的方法,在你读到某个位置的时候,做一些处理的时候,输入流可能还会继续读入字节,这样就达到了缓冲的效果。

缓冲流默认的缓冲大小是8192,可以使用构造方法制定缓冲区大小,一般不用。
缓冲区是缓冲流内部的数组,与传入的数组参数无关,最终使用arraycopy返回

数据操作流

可以操作基本类型的数据

  • DataInputStream
  • DataOutputStream
private static void write() throws IOException {
	// DataOutputStream(OutputStream out)
	// 创建数据输出流对象
	DataOutputStream dos = new DataOutputStream(new FileOutputStream(
			"dos.txt"));

	// 写数据了
	dos.writeByte(10);
	dos.writeShort(100);
	dos.writeInt(1000);
	dos.writeLong(10000);
	dos.writeFloat(12.34F);
	dos.writeDouble(12.56);
	dos.writeChar('a');
	dos.writeBoolean(true);

	// 释放资源
	dos.close();

	// dos.txt
	// 0a00 6400 0003 e800 0000 0000 0027 1041
	// 4570 a440 291e b851 eb85 1f00 6101 
}

private static void read() throws IOException {
	// DataInputStream(InputStream in)
	// 创建数据输入流对象
	DataInputStream dis = new DataInputStream(
			new FileInputStream("dos.txt"));

	// 读数据
	byte b = dis.readByte();
	short s = dis.readShort();
	int i = dis.readInt();
	long l = dis.readLong();
	float f = dis.readFloat();
	double d = dis.readDouble();
	char c = dis.readChar();
	boolean bb = dis.readBoolean();

	// 释放资源
	dis.close();

	System.out.println(b);// 10
	System.out.println(s);// 100
	System.out.println(i);// 1000
	System.out.println(l);// 10000
	System.out.println(f);// 12.34
	System.out.println(d);// 12.56
	System.out.println(c);// a
	System.out.println(bb);// true
}

内存操作流

有些时候我们操作完毕后,未必需要产生一个文件,就可以使用内存操作流。

  • ByteArrayInputStream,ByteArrayOutputStream
  • CharArrayReader,CharArrayWriter
  • StringReader,StringWriter

打印流概述

  • 字节流打印流 PrintStream
  • 字符打印流 PrintWriter
// 复制文本文件
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
PrintWriter pw = new PrintWriter(new FileWriter("b.txt"),true);// 启动自动刷新

String line = null;
while((line=br.readLine())!=null) {
	pw.println(line);
}

pw.close();
br.close();

特点

  • 只有写数据的,没有读取数据。只能操作目的地,不能操作数据源。
  • 可以操作任意类型的数据
  • 如果启用了自动刷新,在调用println()方法的时候,能够换行并刷新
  • 可以直接操作文件

标准输入输出流

System类中的字段:in,out

它们各代表了系统标准的输入和输出设备。
默认输入设备是键盘,输出设备是显示器。

  • System.in的类型是InputStream.
  • System.out的类型是PrintStream,是OutputStream的子类FilterOutputStream 的子类.

三种键盘录入方式

  • main方法的args接收参数
  • System.in通过BufferedReader进行包装
    • BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
  • Scanner
    • Scanner sc = new Scanner(System.in);

输出语句的原理

System.out.println("helloworld");

PrintStream ps = System.out;
ps.println("helloworld");

// 把System.out用字符缓冲流包装一下使用
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

随机访问流

RandomAccessFile类不属于流,是Object类的子类。
但它融合了InputStream和OutputStream的功能。
支持对文件的随机访问读取和写入。

private static void write() throws IOException {
	// 创建随机访问流对象
	RandomAccessFile raf = new RandomAccessFile("raf.txt", "rw");

	// 怎么玩呢?
	raf.writeInt(100);
	raf.writeChar('a');
	raf.writeUTF("中国");

	raf.close();
}

private static void read() throws IOException {
	// 创建随机访问流对象
	RandomAccessFile raf = new RandomAccessFile("raf.txt", "rw");

	int i = raf.readInt();
	System.out.println(i);
	// 该文件指针可以通过 getFilePointer方法读取,并通过 seek 方法设置。
	System.out.println("当前文件的指针位置是:" + raf.getFilePointer());

	char ch = raf.readChar();
	System.out.println(ch);
	System.out.println("当前文件的指针位置是:" + raf.getFilePointer());

	String s = raf.readUTF();
	System.out.println(s);
	System.out.println("当前文件的指针位置是:" + raf.getFilePointer());

	// 我不想重头开始了,我就要读取a,怎么办呢?
	raf.seek(4);
	ch = raf.readChar();
	System.out.println(ch);
}

合并流

把多个输入流的数据写到一个输出流中

  • SequenceInputStream(InputStream s1, InputStream s2)
  • SequenceInputStream(Enumeration<? extends InputStream> e)
// 两个
InputStream s1 = new FileInputStream("ByteArrayStreamDemo.java");
InputStream s2 = new FileInputStream("DataStreamDemo.java");
SequenceInputStream sis = new SequenceInputStream(s1, s2);

// 多个
// SequenceInputStream(Enumeration e)
// 通过简单的回顾我们知道了Enumeration是Vector中的一个方法的返回值类型。
// Enumeration<E> elements()
Vector<InputStream> v = new Vector<InputStream>();
InputStream s1 = new FileInputStream("ByteArrayStreamDemo.java");
InputStream s2 = new FileInputStream("CopyFileDemo.java");
InputStream s3 = new FileInputStream("DataStreamDemo.java");
v.add(s1);
v.add(s2);
v.add(s3);
Enumeration<InputStream> en = v.elements();
SequenceInputStream sis = new SequenceInputStream(en);
BufferedOutputStream bos = new BufferedOutputStream(
		new FileOutputStream("Copy.java"));

// 如何写读写呢,其实很简单,你就按照以前怎么读写,现在还是怎么读写
byte[] bys = new byte[1024];
int len = 0;
while ((len = sis.read(bys)) != -1) {
	bos.write(bys, 0, len);
}

bos.close();
sis.close();

序列化流

可以把对象写入文本文件或者在网络中传输

  • 序列化流:把对象按照流一样的方式存入文本文件或者在网络中传输。对象 – 流数据(ObjectOutputStream)
  • 反序列化流:把文本文件中的流对象数据或者网络中的流对象数据还原成对象。流数据 – 对象(ObjectInputStream)
private static void read() throws IOException, ClassNotFoundException {
	// 创建反序列化对象
	ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
			"oos.txt"));

	// 还原对象
	Object obj = ois.readObject();

	// 释放资源
	ois.close();

	// 输出对象
	System.out.println(obj);
}

private static void write() throws IOException {
	// 创建序列化流对象
	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
			"oos.txt"));

	// 创建对象
	Person p = new Person("林青霞", 27);

	// public final void writeObject(Object obj)
	oos.writeObject(p);

	// 释放资源
	oos.close();
}

序列化和反序列化

对象序列化是将对象状态转换为可保持或传输的过程。一般的格式是与平台无关的二进制流,可以将这种二进制流持久保存在磁盘上,也可以通过网络将这种二进制流传输到另一个网络结点。
对象反序列化,是指把这种二进制流数据还原成对象。

如何实现序列化

  • 让被序列化的对象所属类实现序列化接口。
  • 该接口是一个标记接口。没有功能需要实现。
  • 使用transient关键字声明不需要序列化的成员变量

序列化数据后,再次修改类文件,读取数据会出问题,如何解决呢?

在类文件中,给出一个固定的序列化id值。

Properties集合

是一个集合类,Hashtable的子类

Properties和IO流的结合使用

  • 把键值对形式的文本文件内容加载到集合中
    • public void load(Reader reader)
    • public void load(InputStream inStream)
  • 把集合中的数据存储到文本文件中
    • public void store(Writer writer,String comments)
    • public void store(OutputStream out,String comments)
private static void myStore() throws IOException {
	// 创建集合对象
	Properties prop = new Properties();

	prop.setProperty("林青霞", "27");
	prop.setProperty("武鑫", "30");
	prop.setProperty("刘晓曲", "18");
	
	//public void store(Writer writer,String comments):把集合中的数据存储到文件
	Writer w = new FileWriter("name.txt");
	prop.store(w, "helloworld");
	w.close();
}

private static void myLoad() throws IOException {
	Properties prop = new Properties();

	// public void load(Reader reader):把文件中的数据读取到集合中
	// 注意:这个文件的数据必须是键值对形式
	Reader r = this.getClass().getResourceAsStream("prop.txt");// 这里提供过getResourceAsStream()方法获取
	prop.load(r);
	r.close();

	System.out.println("prop:" + prop);
}

NIO和Netty

JDK4出现NIO。NIO使用了不同的方式来处理输入输出,采用内存映射文件的方式,将文件或者文件的一段区域映射到内存中,就可以像访问内存一样的来访问文件了,这种方式效率比旧IO要高很多。

  • Path:路径
  • Paths:有一个静态方法返回一个路径
    • public static Path get(URI uri)
  • Files:提供了静态方法供我们使用
    • public static long copy(Path source,OutputStream out):复制文件
    • public static Path write(Path path,Iterable<? extends CharSequence> lines,Charset cs,OpenOption… options)
Files.copy(Paths.get("ByteArrayStreamDemo.java"), new FileOutputStream("Copy.java"));

ArrayList<String> array = new ArrayList<String>();
array.add("hello");
array.add("world");
array.add("java");
Files.write(Paths.get("array.txt"), array, Charset.forName("GBK"));

I/O基础入门

Java1.4之前的早期版本,Java对I/O的支持并不完善,开发人员在开发高性能I/O程序的时候,会面临一些巨大的挑战和困难。

  • 没有数据缓冲区,I/O性能存在问题
  • 没有C或者C++中的Channel概念,只有输入和输出流
  • 同步阻塞式I/O通信(BIO),通常会导致通信线程被长时间阻塞
  • 支持的字符集有限,硬件可移植性不好

Linux网络I/O模型

Linux的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)。 而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)。

根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型:

阻塞I/O模型:

最常用的I/O模型就是阻塞I/O模型,缺省情形下,所有文件操作都是阻塞的。以套接字接口为例来讲解此模型:

在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回, 在此期间一直会等待,进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞I/O模型。

阻塞I/O模型

非阻塞I/O模型:

recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。

非阻塞I/O模型

I/O复用模型:

Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。 select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。
Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数rollback。

I/O复用模型

信号驱动I/O模型:

首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。 当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。

信号驱动I/O模型

异步I/O:

告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。

这种模型与信号驱动模型的主要区别是:

信号驱动I/O由内核通知我们何时可以开始一个I/O操作;异步I/O模型由内核通知我们I/O操作何时已经完成。

异步I/O

I/O多路复用技术

在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。

I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。 与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。

I/O多路复用的主要应用场景:

  • 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字;
  • 服务器需要同时处理多种网络协议的套接字。

I/O多路复用的系统调用有select、pselect、poll、epoll,在Linux网络编程过程中,很长一段时间都使用select做轮询和网络事件通知, 然而select的一些固有缺陷导致了它的应用受到了很大的限制,最终Linux不得不在新的内核版本中寻找select的替代方案,最终选择了epoll。

epoll与select的原理比较类似,为了克服select的缺点,epoll作了很多重大改进:

  1. 支持一个进程打开的socket描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)。
    • select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。对于那些需要支持上万个TCP连接的大型服务器来说显然太少了。
    • epoll并没有这个限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于1024。
  2. I/O效率不会随着FD数目的增加而线性下降。
    • 传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,由于网络延时或者链路空闲,任一时刻只有少部分的socket是“活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。
    • epoll不存在这个问题,它只会对“活跃”的socket进行操作-这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的,那么,只有“活跃”的socket才会主动的去调用callback函数,其他idle状态socket则不会。在这点上,epoll实现了一个伪AIO。
    • 针对epoll和select性能对比的benchmark测试表明:如果所有的socket都处于活跃态-例如一个高速LAN环境,epoll并不比select/poll效率高太多;相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
  3. 使用mmap加速内核与用户空间的消息传递。
    • 无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存复制就显得非常重要,epoll是通过内核和用户空间mmap同一块内存实现。
  4. epoll的API更加简单。
    • 包括创建一个epoll描述符、添加监听事件、阻塞等待所监听的事件发生,关闭epoll描述符等。

用来克服select/poll缺点的方法不只有epoll,epoll只是一种Linux的实现方案。在freeBSD下有kqueue,而dev/poll是最古老的Solaris的方案,使用难度依次递增。

NIO入门

传统的BIO编程

网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口), 客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。

在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。

同步阻塞I/O服务端通信模型(一客户端一线程):

同步阻塞I/O服务端通信模型(一客户端一线程)

采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理, 处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答通信模型。

该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系, 由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大, 系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。

伪异步I/O编程

采用线程池和任务队列可以实现一种叫做伪异步的I/O通信框架。

当有新的客户端接入的时候,将客户端的Socket封装成一个Task投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理。 由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

伪异步I/O服务端通信模型(M:N):

伪异步I/O服务端通信模型(M:N)

伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。 但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。

伪异步I/O弊端分析

当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三种事件。

  • 有数据可读;
  • 可用数据已经读取完毕;
  • 发生空指针或者I/O异常。

这意味着当对方发送请求或者应答消息比较缓慢、或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞。

当调用OutputStream的write方法写输出流的时候,它将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。

读和写操作都是同步阻塞的,阻塞的时间取决于对方I/O线程的处理速度和网络I/O的传输速度。

NIO编程

NIO类库简介

NIO弥补了原来同步阻塞I/O的不足,它在标准Java代码中提供了高速的、面向块的I/O。通过定义包含数据的类,以及通过以块的形式处理这些数据,NIO不用使用本机代码就可以利用低级优化,这是原来的I/O包所无法做到的。

缓冲区Buffer

Buffer是一个对象,它包含一些要写入或者要读出的数据。

在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中。

在NIO库中,所有数据都是用缓冲区处理的。

  • 在读取数据时,它是直接读到缓冲区中的;
  • 在写入数据时,写入到缓冲区中。

任何时候访问NIO中的数据,都是通过缓冲区进行操作。

缓冲区实质上是一个数组。通常它是一个字节数组(ByteBuffer),也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。

  • ByteBuffer:字节缓冲区
  • CharBuffer:字符缓冲区
  • ShortBuffer:短整型缓冲区
  • IntBuffer:整形缓冲区
  • LongBuffer:长整形缓冲区
  • FloatBuffer:浮点型缓冲区
  • DoubleBuffer:双精度浮点型缓冲区

Buffer继承关系图

通道Channel

Channel是一个通道,可以通过它读取和写入数据,它就像自来水管一样,网络数据通过Channel读取和写入。 通道与流的不同之处在于通道是双向的,流只是在一个方向上移动,而且通道可以用于读、写或者同时用于读写。

因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

Channel继承关系类图

前三层主要是Channel接口,用于定义它的功能,后面是一些具体的功能类(抽象类),从类图可以看出,实际上Channel可以分为两大类:
分别是用于网络读写的SelectableChannel和用于文件操作的FileChannel。

多路复用器Selector

多路复用器提供选择已经就绪的任务的能力。

简单来讲,Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态, 会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。 这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。

NIO服务端序列图

NIO服务端通信序列图:

NIO服务端通信序列图

NIO客户端创建序列图:

NIO客户端创建序列图

使用NIO编程的优点总结如下。

  • 客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。
  • SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用。
  • 线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降,因此,它非常适合做高性能、高负载的网络服务器。

NIO进行服务端开发的步骤:

  1. 创建ServerSocketChannel,配置它为非阻塞模式;
  2. 绑定监听,配置TCP参数,例如backlog大小;
  3. 创建一个独立的I/O线程,用于轮询多路复用器Selector;
  4. 创建Selector,将之前创建的ServerSocketChannel注册到Selector上,监听SelectionKey.ACCEPT;
  5. 启动I/O线程,在循环体中执行Selector.select()方法,轮询就绪的Channel;
  6. 当轮询到了处于就绪状态的Channel时,需要对其进行判断,如果是OP_ACCEPT状态,说明是新的客户端接入,则调用ServerSocketChannel.accept()方法接受新的客户端;
  7. 设置新接入的客户端链路SocketChannel为非阻塞模式,配置其他的一些TCP参数;
  8. 将SocketChannel注册到Selector,监听OP_READ操作位;
  9. 如果轮询的Channel为OP_READ,则说明SocketChannel中有新的就绪的数据包需要读取,则构造ByteBuffer对象,读取数据包;
  10. 如果轮询的Channel为OP_WRITE,说明还有数据没有发送完成,需要继续发送。

没有考虑半包等问题的代码:

public static void main(String[] args) throws IOException {
	int port = 8080;
	if (args != null && args.length > 0) {
		try {
			port = Integer.valueOf(args[0]);
		} catch (NumberFormatException e) {
			// 采用默认值
		}
	}
	MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
	new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();
}
public class MultiplexerTimeServer implements Runnable {
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private volatile boolean stop;
    /**
     * 初始化多路复用器、绑定监听端口
     */
    public MultiplexerTimeServer(int port) {
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("The time server is start in port : " + port);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }
    public void stop() {
        this.stop = true;
    }
    @Override
    public void run() {
        while (!stop) {
            try {
                selector.select(1000);
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectedKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null)
                                key.channel().close();
                        }
                    }
                }
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
        // 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            // 处理新接入的请求消息
            if (key.isAcceptable()) {
                // Accept the new connection
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                // Add the new connection to the selector
                socketChannel.register(selector, SelectionKey.OP_READ);
            }
            if (key.isReadable()) {
                // Read the data
                SocketChannel socketChannel = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = socketChannel.read(readBuffer);
                if (readBytes > 0) {
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("The time server receive order : " + body);
                    String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
                            new java.util.Date(System.currentTimeMillis()).toString() :
                            "BAD ORDER";
                    doWrite(socketChannel, currentTime);
                } else if (readBytes < 0) {
                    // 对端链路关闭
                    key.cancel();
                    socketChannel.close();
                }
                // 读到0字节,忽略
            }
        }
    }
    private void doWrite(SocketChannel channel, String response)
            throws IOException {
        if (response != null && response.trim().length() > 0) {
            byte[] bytes = response.getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer);
        }
    }
}
public static void main(String[] args) {
	int port = 8080;
	if (args != null && args.length > 0) {
		try {
			port = Integer.valueOf(args[0]);
		} catch (NumberFormatException e) {
			// 采用默认值
		}
	}
	new Thread(new TimeClientHandle("127.0.0.1", port), "TimeClient-001").start();
}

public class TimeClientHandle implements Runnable {
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean stop;
    public TimeClientHandle(String host, int port) {
        this.host = host == null ? "127.0.0.1" : host;
        this.port = port;
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }
    @Override
    public void run() {
        try {
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        while (!stop) {
            try {
                selector.select(1000);
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectedKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null)
                                key.channel().close();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                System.exit(1);
            }
        }
        // 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            // 判断是否连接成功
            SocketChannel socketChannel = (SocketChannel) key.channel();
            if (key.isConnectable()) {
                if (socketChannel.finishConnect()) {
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    doWrite(socketChannel);
                } else {
                    System.exit(1);// 连接失败,进程退出
                }
            }
            if (key.isReadable()) {
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = socketChannel.read(readBuffer);
                if (readBytes > 0) {
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("Now is : " + body);
                    this.stop = true;
                } else if (readBytes < 0) {
                    // 对端链路关闭
                    key.cancel();
                    socketChannel.close();
                }
                // 读到0字节,忽略
            }
        }
    }
    private void doConnect() throws IOException {
        // 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
        if (socketChannel.connect(new InetSocketAddress(host, port))) {
            socketChannel.register(selector, SelectionKey.OP_READ);
            doWrite(socketChannel);
        } else {
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }
    private void doWrite(SocketChannel socketChannel) throws IOException {
        byte[] req = "QUERY TIME ORDER".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        socketChannel.write(writeBuffer);
        if (!writeBuffer.hasRemaining()) {
            System.out.println("Send order 2 server succeed.");
        }
    }

}

AIO编程

JDK1.7升级了NIO类库,升级后的NIO类库被称为NIO2.0,Java正式提供了异步文件I/O操作,同时提供了与UNIX网络编程事件驱动I/O对应的AIO。

NIO2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。异步通道提供两种方式获取获取操作结果。

  • 通过java.util.concurrent.Future类来表示异步操作的结果;
  • 在执行异步操作的时候传入一个java.nio.channels。

CompletionHandler接口的实现类作为操作完成的回调。

NIO2.0的异步套接字通道是真正的异步非阻塞I/O,它对应UNIX网络编程中的事件驱动I/O(AIO), 它不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。

由JDK底层的ThreadPoolExecutor进行调度并驱动读写操作,不需要开线程,因此更加简单。

没有考虑半包等问题的代码:

public static void main(String[] args) throws IOException {
	int port = 8080;
	if (args != null && args.length > 0) {
		try {
			port = Integer.valueOf(args[0]);
		} catch (NumberFormatException e) {
			// 采用默认值
		}
	}
	AsyncTimeServerHandler timeServer = new AsyncTimeServerHandler(port);
	new Thread(timeServer, "AIO-AsyncTimeServerHandler-001").start();
}
public class AsyncTimeServerHandler implements Runnable {
    private int port;
    CountDownLatch latch;
    AsynchronousServerSocketChannel asynchronousServerSocketChannel;
    public AsyncTimeServerHandler(int port) {
        this.port = port;
        try {
            asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
            asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
            System.out.println("The time server is start in port : " + port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        latch = new CountDownLatch(1);
        doAccept();
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void doAccept() {
        asynchronousServerSocketChannel.accept(this, new AcceptCompletionHandler());
    }
}
public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncTimeServerHandler> {
    @Override
    public void completed(AsynchronousSocketChannel result, AsyncTimeServerHandler asyncTimeServerHandler) {
        asyncTimeServerHandler.asynchronousServerSocketChannel.accept(asyncTimeServerHandler, this);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        result.read(buffer, buffer, new ReadCompletionHandler(result));
    }
    @Override
    public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
        exc.printStackTrace();
        attachment.latch.countDown();
    }
}

public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel channel;
    public ReadCompletionHandler(AsynchronousSocketChannel channel) {
        if (this.channel == null) {
            this.channel = channel;
        }
    }
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        attachment.flip();
        byte[] body = new byte[attachment.remaining()];
        attachment.get(body);
        try {
            String req = new String(body, "UTF-8");
            System.out.println("The time server receive order : " + req);
            String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(req) ? new java.util.Date(
                    System.currentTimeMillis()).toString() : "BAD ORDER";
            doWrite(currentTime);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
    private void doWrite(String currentTime) {
        if (currentTime != null && currentTime.trim().length() > 0) {
            byte[] bytes = (currentTime).getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer, writeBuffer,
                    new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            // 如果没有发送完成,继续发送
                            if (buffer.hasRemaining()) {
                                channel.write(buffer, buffer, this);
                            }
                        }
                        @Override
                        public void failed(Throwable exc, ByteBuffer attachment) {
                            try {
                                channel.close();
                            } catch (IOException e) {
                                // ingnore on close
                            }
                        }
                    });
        }
    }
    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        try {
            this.channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public static void main(String[] args) {
	int port = 8080;
	if (args != null && args.length > 0) {
		try {
			port = Integer.valueOf(args[0]);
		} catch (NumberFormatException e) {
			// 采用默认值
		}

	}
	new Thread(new AsyncTimeClientHandler("127.0.0.1", port),"AIO-AsyncTimeClientHandler-001").start();

}

public class AsyncTimeClientHandler implements CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
    private AsynchronousSocketChannel client;
    private String host;
    private int port;
    private CountDownLatch latch;
    public AsyncTimeClientHandler(String host, int port) {
        this.host = host;
        this.port = port;
        try {
            client = AsynchronousSocketChannel.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        latch = new CountDownLatch(1);
        client.connect(new InetSocketAddress(host, port), this, this);
        try {
            latch.await();
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void completed(Void result, AsyncTimeClientHandler attachment) {
        byte[] req = "QUERY TIME ORDER".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        client.write(writeBuffer, writeBuffer,
                new CompletionHandler<Integer, ByteBuffer>() {
                    @Override
                    public void completed(Integer result, ByteBuffer buffer) {
                        if (buffer.hasRemaining()) {
                            client.write(buffer, buffer, this);
                        } else {
                            ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                            client.read(
                                    readBuffer,
                                    readBuffer,
                                    new CompletionHandler<Integer, ByteBuffer>() {
                                        @Override
                                        public void completed(Integer result, ByteBuffer buffer) {
                                            buffer.flip();
                                            byte[] bytes = new byte[buffer.remaining()];
                                            buffer.get(bytes);
                                            String body;
                                            try {
                                                body = new String(bytes, "UTF-8");
                                                System.out.println("Now is : " + body);
                                                latch.countDown();
                                            } catch (UnsupportedEncodingException e) {
                                                e.printStackTrace();
                                            }
                                        }
                                        @Override
                                        public void failed(Throwable exc, ByteBuffer attachment) {
                                            try {
                                                client.close();
                                                latch.countDown();
                                            } catch (IOException e) {
                                                // ingnore on close
                                            }
                                        }
                                    });
                        }
                    }
                    @Override
                    public void failed(Throwable exc, ByteBuffer attachment) {
                        try {
                            client.close();
                            latch.countDown();
                        } catch (IOException e) {
                            // ingnore on close
                        }
                    }
                });
    }
    @Override
    public void failed(Throwable exc, AsyncTimeClientHandler attachment) {
        exc.printStackTrace();
        try {
            client.close();
            latch.countDown();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4种I/O对比

概念

异步非阻塞I/O

在早期的JDK1.4和1.5 update10版本之前,JDK的Selector基于select/poll模型实现,它是基于I/O复用技术的非阻塞I/O,不是异步I/O。
在JDK1.5 update10和Linux core2.6以上版本,Sun优化了Selctor的实现,它在底层使用epoll替换了select/poll,上层的API并没有变化,可以认为是JDK NIO的一次性能优化,但是它仍旧没有改变I/O的模型。
由JDK1.7提供的NIO2.0,新增了异步的套接字通道,它是真正的异步I/O,在异步I/O操作的时候可以传递信号变量,当操作完成之后会回调相关的方法,异步I/O也被称为AIO。

多路复用器Selector

Java NIO的实现关键是多路复用I/O技术,多路复用的核心就是通过Selector来轮询注册在其上的Channel,当发现某个或者多个Channel处于就绪状态后,从阻塞状态返回就绪的Channel的选择键集合,进行I/O操作。

伪异步I/O

在通信线程和业务线程之间做个缓冲区,这个缓冲区用于隔离I/O线程和业务线程间的直接访问,这样业务线程就不会被I/O线程阻塞。

对于后端的业务侧来说,将消息或者Task放到线程池后就返回了,它不再直接访问I/O线程或者进行I/O读写,这样也就不会被同步阻塞。

类似的设计还包括前端启动一组线程,将接收的客户端封装成Task,放到后端的线程池执行,用于解决一连接一线程问题。

对比

几种I/O模型的功能和特性对比:

几种I/O模型的功能和特性对比

具体选择什么样的I/O模型或者NIO框架,完全基于业务的实际应用场景和性能诉求:

  • 如果客户端并发连接数不多,周边对接的网元不多,服务器的负载也不重,那就完全没必要选择NIO做服务端;
  • 如果是相反情况,那就要考虑选择合适的NIO框架进行开发。

原生NIO与Netty对比

开发出高质量的NIO程序并不是一件简单的事情,除去NIO固有的复杂性和BUG不谈,作为一个NIO服务端,需要能够处理网络的闪断、客户端的重复接入、客户端的安全认证、消息的编解码、半包读写等情况, 如果你没有足够的NIO编程经验积累,一个NIO框架的稳定往往需要半年甚至更长的时间。更为糟糕的是,一旦在生产环境中发生问题,往往会导致跨节点的服务调用中断,严重的可能会导致整个集群环境都不可用,需要重启服务器,这种非正常停机会带来巨大的损失。

从可维护性角度看,由于NIO采用了异步非阻塞编程模型,而且是一个I/O线程处理多条链路,它的调试和跟踪非常麻烦,特别是生产环境中的问题,我们无法进行有效的调试和跟踪,往往只能靠一些日志来辅助分析,定位难度很大。

不选择原生NIO编程的原因
  1. NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
  2. 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序。
  3. 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大。
  4. JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而已,它并没有被根本解决。
为什么选择Netty

Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证。
例如Hadoop的RPC框架avro使用Netty作为底层通信框架;很多其他业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。

  • API使用简单,开发门槛低;
  • 功能强大,预置了多种编解码功能,支持多种主流协议;
  • 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
  • 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;
  • 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
  • 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;
  • 经历了大规模的商业应用考验,质量得到验证。
    • 在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全能够满足不同行业的商业应用了。

正是因为这些优点,Netty逐渐成为Java NIO编程的首选框架。

TCP粘包/拆包问题的解决

TCP粘包/拆包

TCP是个“流”协议,所谓流,就是没有界限的一串数据。

TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为, 一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

TCP粘包/拆包问题:

TCP粘包/拆包问题

  1. 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
  2. 服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
  3. 服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
  4. 服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
  5. 如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。

TCP粘包/拆包问题原因

  1. 应用程序write写入的字节大小大于套接口发送缓冲区大小;
  2. 进行MSS大小的TCP分段;
  3. 以太网帧的payload大于MTU进行IP分片。

TCP粘包/拆包问题原因

解决策略

由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的, 这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。

  1. 消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;
  2. 在包尾增加回车换行符进行分割,例如FTP协议;
  3. 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度;
  4. 更复杂的应用层协议。

利用LineBasedFrameDecoder解决TCP粘包问题

为了解决TCP粘包/拆包导致的半包读写问题,Netty默认提供了多种编解码器用于处理半包,只要能熟练掌握这些类库的使用,TCP粘包问题从此会变得非常容易, 你甚至不需要关心它们,这也是其他NIO框架和JDK原生的NIO API所无法匹敌的。

通过Netty的LineBasedFrameDecoder和StringDecoder来解决TCP粘包问题。

public class TimeServer {
    public void bind(int port) throws Exception {
        // 配置服务端的NIO线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childHandler(new ChildChannelHandler());
            // 绑定端口,同步等待成功
            ChannelFuture f = serverBootstrap.bind(port).sync();
            // 等待服务端监听端口关闭
            f.channel().closeFuture().sync();
        } finally {
            // 优雅退出,释放线程池资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
    private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
			// 编解码器
            socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
            socketChannel.pipeline().addLast(new StringDecoder());
            socketChannel.pipeline().addLast(new TimeServerHandler());
        }
    }
    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {
                // 采用默认值
            }
        }
        new TimeServer().bind(port);
    }
}

public class TimeServerHandler extends ChannelHandlerAdapter {
    private int counter;
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        String body = (String) msg;
        System.out.println("The time server receive order : " + body + " ; the counter is : " + ++counter);
        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)
                ? new java.util.Date(System.currentTimeMillis()).toString()
                : "BAD ORDER";
        currentTime = currentTime + System.getProperty("line.separator");
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.writeAndFlush(resp);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        ctx.close();
    }
}
public class TimeClient {
    public void connect(int port, String host) throws Exception {
        // 配置客户端NIO线程组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch)
                                throws Exception {
                            ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new TimeClientHandler());
                        }
                    });
            // 发起异步连接操作
            ChannelFuture f = bootstrap.connect(host, port).sync();
            // 当代客户端链路关闭
            f.channel().closeFuture().sync();
        } finally {
            // 优雅退出,释放NIO线程组
            group.shutdownGracefully();
        }
    }
    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {
                // 采用默认值
            }
        }
        new TimeClient().connect(port, "127.0.0.1");
    }
}

public class TimeClientHandler extends ChannelHandlerAdapter {
    private static final Logger LOGGER = Logger.getLogger(TimeClientHandler.class.getName());
    private int counter;
    private byte[] req;
    /**
     * Creates a client-side handler.
     */
    public TimeClientHandler() {
        req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ByteBuf message = null;
        for (int i = 0; i < 100; i++) {
            message = Unpooled.buffer(req.length);
            message.writeBytes(req);
            ctx.writeAndFlush(message);
        }
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        String body = (String) msg;
        System.out.println("Now is : " + body + " ; the counter is : " + ++counter);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 释放资源
        LOGGER.warning("Unexpected exception from downstream : " + cause.getMessage());
        ctx.close();
    }
}

LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf中的可读字节,判断看是否有”\n”或者”\r\n”, 如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。 它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支待配置单行的最大长度。 如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。

StringDecoder的功能非常简单,就是将接收到的对象转换成字符串,然后继续调用后面的Handler,LineBasedFrameDecoder+StringDecoder组合就是按行切换的文本解码器,它被设计用来⽀持TCP的粘包和拆包。

Netty提供了多种⽀持 TCP 粘包/拆包的解码器, 用来满⾜用户的不同诉求。

分隔符和定长解码器

DelimiterBasedFrameDecoder和FixedLengthFrameDecoder,前者可以自动完成以分隔符做结束标志的消息的解码,后者可以自动完成对定长消息的解码,它们都能解决TCP粘包/拆包导致的读半包问题。

只要将DelimiterBasedFrameDecoder或FixedLengthFrameDecoder添加到对应ChannelPipeline的起始位即可。

// 在传输数据时需要追加分隔符字符。
// server
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
		.group(bossGroup, workerGroup)
		.channel(NioServerSocketChannel.class)
		.option(ChannelOption.SO_BACKLOG, 100)
		.handler(new LoggingHandler(LogLevel.INFO))
		.childHandler(new ChannelInitializer<SocketChannel>() {
			@Override
			public void initChannel(SocketChannel ch)
					throws Exception {
				ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
				ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
				ch.pipeline().addLast(new StringDecoder());
				ch.pipeline().addLast(new EchoServerHandler());
			}
		});
// client
Bootstrap bootstrap = new Bootstrap();
bootstrap
		.group(group)
		.channel(NioSocketChannel.class)
		.option(ChannelOption.TCP_NODELAY, true)
		.handler(new ChannelInitializer<SocketChannel>() {
			@Override
			public void initChannel(SocketChannel ch) throws Exception {
				ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
				ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
				ch.pipeline().addLast(new StringDecoder());
				ch.pipeline().addLast(new EchoClientHandler());
			}
		});

使用DelimiterBasedFrameDecoder可以自动对采用分隔符做码流结束标识的消息进行解码。

ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
		.group(bossGroup, workerGroup)
		.channel(NioServerSocketChannel.class)
		.option(ChannelOption.SO_BACKLOG, 100)
		.handler(new LoggingHandler(LogLevel.INFO))
		.childHandler(new ChannelInitializer<SocketChannel>() {
			@Override
			public void initChannel(SocketChannel ch) throws Exception {
				ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
				ch.pipeline().addLast(new StringDecoder());
				ch.pipeline().addLast(new EchoServerHandler());
			}
		});

利用FixedLengthFrameDecoder解码器,无论一次接收到多少数据报,它都会按照构造函数中设置的固定长度进行解码, 如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。

编辑码技术

Java 序列化的目的主要有两个:

  • 网络传输
  • 对象持久化

当进行远程跨进程服务调用时,需要把被传输的Java对象编码为字节数组或者ByteBuffer对象。 而当远桯服务读取到ByteBuffer对象或者字节数组时,需要将其解码为发送时的Java对象。这被称为Java对象编解码技术。

评判一个编解码框架的优劣时,往往会考虑以下几个因素:

  • 是否支持跨语言,支持的语言种类是否丰富;
  • 一编码后的码流大小;
  • 编解码的性能;
  • 类库是否小巧,API使用是否方便;
  • 使用者需要手工开发的工作量和难度。

Java序列化的缺点

  • 无法跨语言
    • 最致命的问题
  • 序列化后码流太大
    • 1
  • 序列化性能太低
  • 序列化性能太低
public byte[] codeC() {
	ByteBuffer buffer = ByteBuffer.allocate(1024);
	byte[] value = this.userName.getBytes();
	buffer.putInt(value.length);
	buffer.put(value);
	buffer.putInt(this.userID);
	buffer.flip();
	byte[] result = new byte[buffer.remaining()];
	buffer.get(result);
	return result;
}

序列化性能对比图:

序列化性能对比图

业界主流的编解码框架

Google的Protobuf

Protobuf全称Google Protocol Buffers,它由谷歌开源而来,在谷歌内部久经考验。 它将数据结构以proto文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。

  • 结构化数据存储格式(XML,JSON等);
  • 高效的编解码性能;
  • 语言无关、平台无关、扩展性好;
  • 官方支持Java、C++和Python三种语言。

尽管XML的可读性和可扩展性非常好,也非常适合描述数据结构,但是XML解析的时间开销和XML为了可读性而牺牲的空间开销都非常大,因此不适合做高性能的通信协议。 Protobuf使用二进制编码,在空间和性能上具有更大的优势。

Protobuf另一个比较吸引人的地方就是它的数据描述文件和代码生成机制,利用数据描述文件对数据结构进行说明的优点如下:

  • 文本化的数据结构描述语言,可以实现语言和平台无关,特别适合异构系统间的集成;
  • 通过标识字段的顺序,可以实现协议的前向兼容;
  • 自动代码生成,不需要手工编写同样数据结构的C++和Java版本;
  • 方使后续的管理和维护。相比于代码,结构化的文档更容易管理和维护。

Protobuf编解码和其他几种序列化框架的响应时间对比:

Protobuf编解码和其他几种序列化框架的响应时间对比

Protobuf和其他几种序列化框架的宇节数对比:

Protobuf和其他几种序列化框架的宇节数对比

Protobuf的编解码性能远远高于其他几种序列化相架的序列化和反序列化,这也是很多RPC框架选用Protobuf做编解码框架的原因。

通过protoc.exe 命令行生成Java代码。

SubscribeResp.proto文件定义

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap
		.group(bossGroup, workerGroup)
		.channel(NioServerSocketChannel.class)
		.option(ChannelOption.SO_BACKLOG, 100)
		.handler(new LoggingHandler(LogLevel.INFO))
		.childHandler(new Channelinitializer<SocketChannel>() {
			@Override
			public void initChannel(SocketChannel ch) {
				// netty的粘包半包支持
				ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());
				ch.pipeline().addLast(new ProtobufDecoder(SubscribeReqProto.SubscribeReq.getDefaultinstance()));
				ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
				ch.pipeline().addLast(new ProtobufEncoder());
				ch.pipeline().addLast(new SubReqServerHandler());

			}
		});
Bootstrap bootstrap = new Bootstrap();
bootstrap
		.group(group)
		.channel(NioSocketChannel.class)
		.option(ChannelOption.TCPNODELAY, true)
		.handler(new Channelinitializer<SocketChannel>() {
			@Override
			public void initChannel(SocketChannel ch) throws Exception {
				ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());
				ch.pipeline().addLast(new ProtobufDecoder(SubscribeRespProto.SubscribeResp.getDefaultinstance()));
				ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
				ch.pipeline().addLast(new ProtobufEncoder());
				ch.pipeline().addLast(new SubReqClientHandler());
			}
		});
  1. 使用Netty提供的Protobutvarint32FrameDecoder,它可以处理半包消息;
  2. 继承Netty提供的通用半包解码器LengthFieldBasedFrameDecoder;
  3. 继承ByteToMessageDecoder类,自己处理半包消息。
Facebook的Thrift介绍

创造Thrift是为了解决Facebook各系统间大数据量的传输通信以及系统之间语言环境不同需要跨平台的特性, 因此Thrift可以支持多种程序语言,如C++、C#、Cocoa、Erlang、Haskell、Java、Ocami、Perl、PHP、Python、Ruby和Smalltalk。

在多种不同的语言之间通信,Thrift可以作为高性能的通信中间件使用,它支持数据(对象)序列化和多种类型的RPC服务。 Thrift适用于静态的数据交换,需要先确定好它的数据结构,当数据结构发生变化时,必须重新编辑IDL文件,生成代码和编译,这一点跟具他IDL工具相比可以视为是Thrift的弱项。 Thrift适用于搭建大型数据交换及存储的通用工具,对于大型系统中的内部数据传输,相对于JSON和XML在性能和传输大小上都有明显的优势。

Thrift主要由5部分组成。

  1. 语言系统以及IDL编译器:负责由用户给定的IDL文件生成相应语言的接口代码;
  2. TProtocol:RPC的协议层,可以选择多种不同的对象序列化方式,如JSON和Binary;
  3. TTransport:RPC的传输层,同样可以选择不同的传输层实现,如socket、NIO、MemoryBuffer等
  4. TProcessor:作为协议层和用户提供的服务实现之间的纽带,负责调用服务实现的接口:
  5. TServer:聚合TProtocol、TTransport和TProcessor等对象。

由于Thrift的RPC服务调用和编解码框架绑定在一起,所以,通常我们使用Thrift的时候会采取RPC框架的方式。 但是,它的TProtocol编解码框架还是可以以类库的方式独立使用的。

与Protobuf比较类似的是,Thrift通过IDL描述接口和数据结构定义,它支持8种Java基本类型、Map、Set和List,支持可选和必选定义,功能非常强大。 因为可以定义数据结构中字段的顺序,所以它也可以支待协议的前向兼容。

Thrift支持三种比较典型的编解码方式。

  • 通用的二进制编解码:
  • 压缩二进制编解码;
  • 优化的可选字段压缩编解码。

由于支持二进制压缩编解码,Thrift的编解码性能表现也相当优异,远远超过Java序列化和RMI。

Thrift性能测试对比图

JBoss Marshalling介绍

JBoss Marshalling是一个Java对象的序列化API包,修正了JDK自带的序列化包的很多问题,但又保待跟java.io.Serializable接口的兼容; 同时增加了一些可调的参数和附加的特性,并且这些参数和特性可通过工厂类进行配置。

相比于传统的Java序列化机制,它的优点如下:

  • 可插拔的类解析器,提供更加便捷的类加载定制策略,通过一个接口即可实现定制;
  • 可插拔的对象替换技术,不需要通过继承的方式;
  • 可插拔的预定义类缓存表,可以减小序列化的字节数组长度,提升常用类型的对象序列化性能;
  • 无须实现java.io.Serializable 接口,即可实现Java 序列化;
  • 通过缓存技术提升对象的序列化性能。

JBoss Marshalling更多是在JBoss内部使用,应用范围有限。

public final class MarshallingCodeCFactory {
    /**
     * 创建Jboss Marshalling解码器MarshallingDecoder
     */
    public static MarshallingDecoder buildMarshallingDecoder() {
        final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
        final MarshallingConfiguration configuration = new MarshallingConfiguration();
        configuration.setVersion(5);
        UnmarshallerProvider provider = new DefaultUnmarshallerProvider(marshallerFactory, configuration);
        MarshallingDecoder decoder = new MarshallingDecoder(provider, 1024);
        return decoder;
    }
    /**
     * 创建Jboss Marshalling编码器MarshallingEncoder
     */
    public static MarshallingEncoder buildMarshallingEncoder() {
        final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
        final MarshallingConfiguration configuration = new MarshallingConfiguration();
        configuration.setVersion(5);
        MarshallerProvider provider = new DefaultMarshallerProvider(marshallerFactory, configuration);
        MarshallingEncoder encoder = new MarshallingEncoder(provider);
        return encoder;
    }
}
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
		.group(bossGroup, workerGroup)
		.channel(NioServerSocketChannel.class)
		.option(ChannelOption.SO_BACKLOG, 100)
		.handler(new LoggingHandler(LogLevel.INFO))
		.childHandler(new ChannelInitializer<SocketChannel>() {
			@Override
			public void initChannel(SocketChannel ch) {
				ch.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
				ch.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
				ch.pipeline().addLast(new SubReqServerHandler());
			}
		});
Bootstrap bootstrap = new Bootstrap();
bootstrap
		.group(group)
		.channel(NioSocketChannel.class)
		.option(ChannelOption.TCP_NODELAY, true)
		.handler(new ChannelInitializer<SocketChannel>() {
			@Override
			public void initChannel(SocketChannel ch)
					throws Exception {
				ch.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
				ch.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
				ch.pipeline().addLast(new SubReqClientHandler());
			}
		});

Marshalling编解码器支持半包和粘包的处理。

MessagePack编解码

MessagcPack是一个高效的二进制序列化框架,它像JSON一样支持不同语言间的数据交换,但是它的性能更快,序列化之后的码流也更小。

MessagePack的特点如下:

  • 编解码高效,性能高;
  • 序列化之后的码流小;
  • 支持跨语言。
    • Java、Python、Ruby、Haskell、C#、OCaml、Lua、Go、C、C++等
<dependencies>
  <dependency>
    <groupId>org.msgpack</groupId>
    <artifactId>msgpack</artifactId>
    <version>${msgpack.version}</version>
  </dependency>
</dependencies>
// Create serialize objects.
List<String> src = new ArrayList<String>();
src.add("msgpack");
src.add("kumofs");
src.add("viver");
MessagePack msgpack = new MessagePack();
// Serialize
byte[] raw = msgpack.write(src);
// Deserialize directly using a template
List<String> dst1 = msgpack.read(raw, Templates.tList(Templates.TString));
// Or, Deserialze to Value then convert type.
Value dynamic = msgpack.read(raw);
List<String> dst2 = new Converter(dynamic).read(Templates.tList(Templates.TString));
Bootstrap bootstrap = new Bootstrap();
bootstrap
		.group(group)
		.channel(NioSocketChannel.class)
		.option(ChannelOption.TCP_NODELAY, true)
		.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
		.handler(new Channelinitializer<SocketChannel>() {
			@Override
			public void initChannel(SocketChannel ch) throws Exception {
				// 粘包、半包支持及编解码支持
				ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));
				ch.pipeline().addLast("msgpack decoder", new MsgpackDecoder());
				// 增加头
				ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2));
				ch.pipeline().addLast("msgpack encoder", new MsgpackEncoder());
				ch.pipeline().addLast(new EchoClientHandler(sendNumber));
			}
		});

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap
		.group(acceptorGroup, IOGroup)
		.channel(NioServerSocketChannel.class)
		.option(ChannelOption.SO_BACKLOG, 100)
		.handler(new LoggingHandler(LogLevel.INFO))
		.childHandler(new Channelinitializer<SocketChannel>() {
			@Override
			public void initChannel(SocketChannel ch) throws Exception {
				ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65535, O, 2, 0, 2));
				ch.pipeline().addLast("msgpack decoder", new MsgpaclcDecoder());
				ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2));
				ch.pipeline().addLast("msgpack encoder", new MsgpackEncoder());
				ch.pipeline().addLast(new EchoServerHandler());
			}

		});

在MessagePack编码器之前增加LengthFieldPrepender,它将在ByteBuf之前增加2个字节的消息长度字段。

LengtbfieldPrepender原理示意图

在MessagePack解码器之前增加LengthFieldBasedFrameDecoder,用于处理半包消息,这样后面的MsgpackDecoder接收到的永远是整包消息。

LengthFieldBasedFrameDecoder工作原理图

利用Netty的半包编码和解码器LengthFieldPrepender和LengthFieldBasedFrameDecoder,可以轻松地解决TCP粘包和半包问题。

Netty多协议开发

Http协议介绍

HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。

HTTP协议的主要特点:

  • 支持Client/Server模式;
  • 简单:客户向服务器请求服务时,只需指定服务URL,携带必要的请求参数或者消息体;
  • 灵活:HTTP允许传输任意类型的数据对象,传输的内容类型由HTTP消息头中的Content-Type加以标记;
  • 无状态:HTTP协议是无状态协议,无状态是指协议对于事务处理没有记忆能力。
    • 缺少状态意味着如果后续处理需要之前的信息,则它必须重传,这样可能导致每次连接传送的数据压增大。
    • 另一方面,在服务器不需要先前信息时它的应答就较快,负载较轻。
URL
http://host[":"port)(abs_path]
请求信息
  • HTTP请求行;
  • HTTP消息头;
  • HTTP请求正文。

请求行以一个方法符开头,以空格分开,后面跟着请求的URI和协议的版本,格式为:Method Request-URI HTTP-Version CRLF。

其中Method表示请求方法,Request-URI是一个统一资源标识符,HTTP-Version表示请求的HTTP协议版本,CRLF表示回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符)。

请求方法有多种,各方法的作用:

  • GET:请求获取Request-URI所标识的资源;
  • POST:在Request-URI所标识的资源后附加新的提交数据;
  • HEAD:请求获取由Request-URI所标识的资源的响应消息报头;
  • PUT:请求服务器存储一个资源,并用Request-URI作为其标识;
  • DELETE:诸求服务器删除Request-URI所标识的资源;
  • TRACE:请求服务器回送收到的请求信息,主要用于刹试或诊断;
  • CONNECT:保留将来使用;
  • OPTIONS:请求查询服务器的性能,或者查询与资源相关的选项和需求。

GET方法:以在浏览器的地址栏中输入网址的方式访问网页时,浏览器采用GET方法向服务器获取资源。

POST方法:要求被请求服务器接受附在请求后面的数据,常用于提交表单。

  1. 根据HTTP规范,GET用于信息获取,而且应该是安全的和幕等的;POST则表示可能改变服务器上的资源的请求。
  2. GET提交,请求的数据会附在URL之后,就是把数据放置在请求行(requestIine)中,以”?”分隔URL和传输数据,多个参数用”&”连接;而POST提交会把提交的数据放置在HTTP消息的包体中,数据不会在地址栏中显示出来。
  3. 传输数据的大小不同。特定浏览器和服务器对URL长度有限制,例如IE对URL长度的限制是2083字节(2KB+35B),因此GET携带的参数的长度会受到浏览器的限制;而POST由于不是通过URL传值,理论上数据长度不会受限。
  4. 安全性。POST的安全性要比GET的安全性商。比如通过GET提交数据,用户名和密码将明文出现在URL上。
    • 登录页面有可能被浏览器缓存,其他人查看浏览器的历史记录,那么别人就可以拿到你的账号和密码了。
    • 除此之外,使用GET提交数据还可能会造成Cross-site request forgery攻击。POST提交的内容由于在消息体中传输,因此不存在上述安全问题。

HTTP的部分请求消息头列表:

名称(KEY) 作用
Accept 用于指定客户端接受哪些类型的信息。
Accept-Charset 用于指定客户茹接受的字符集。
Accept-Encoding 用于指定可接受的内容编码。
Accept-Language 用于指定一种自然语言。
Authorization 主要用于证明客户端有权查看某个资源。服务器的响应代码为401,可以发送一个包含Authorization诸求报头域的诺求,要求服务器对其进行认证。
Host 指定被谐求资源的Internet主机和端口号,它通常是从HTTP URL中提取出来的。
User-Agent 允许客户端将它的操作系统、浏览器和其他屈性告诉服务器。
Content-Length 请求消息体的长度。
Content-Type 表示后面的文档属于什么MIME类型。
Connection 连接类型
响应消息

三个部分组成,分别是:状态行、消息报头、响应正文。

状态行的格式为:HTTP-Version Status-Code Reason-Phrase CRLF, 其中HTTP-Version表示服务器HTTP协议的版本,Status-Code表示服务器返回的响应状态代码。

状态代码由三位数字组成,第一个数字定义了响应的类别,它有5种可能的取值。

  1. lxx:指示信息。表示请求已接收,继续处理;
  2. 2xx:成功。表示请求已被成功接收、理解、接受:
  3. 3xx:重定向。要完成谓求必须进行更进一步的操作;
  4. 4xx:客户端错误。请求有语法错误或请求无法实现;
  5. 5xx:服务器端错误。服务器未能处理请求。
响应状态代码和描述信息
状态码 状态描述
200 OK:客户端诮求成功
400 Bad Request:客户端请求有语法错误,不能被服务器所理解
401 Unauthorized:诮求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden:服务器收到谐求,但是拒绝提供服务
404 Not Found:请求资源不存在
500 Internal Server Error:服务器发生不可预期的错误
503 Server Uaavailable:服务器当前不能处理客户站的诸求,一段时间后可能恢复正常

响应报头允许服务器传递不能放在状态行中的附加响应信息,以及关于服务器的信息和对Request-URI所标识的资源进行下一步访问的信息。

常用的响应报头:

名称(KEY) 作用
Location 用于重定向接收者到一个新的位置,Location响应报头域常用于更换域名的时候
Server 包含了服务器用来处理i音求的软件信息,与User-Agent音求报头域是相对应的
WWW-Authenticate 必须被包含在401(未授权的)响应消息中.客户端收到401响应消息,并发送Authorization报头域请求服务器对其进行验证时,服务端响应报头就包含该报头域

Netty HTTP服务器入门开发

由于Netty天生是异步事件驱动的架构,因此基于NIO TCP协议栈开发的HTTP协议栈也是异步非阻塞的。

Netty的HTTP协议栈无论在性能还是可靠性上,都表现优异,非常适合在非Web容器的场景下应用, 相比于传统的Tomcat、Jetty等Web容器,它更加轻址和小巧,灵活性和定制性也更好。

代码:

HttpFileServer.java

HttpFileServerHandler.java

Netty HTTP+XML协议栈开发

由于HTTP协议的通用性,很多异构系统间的通信交互采用HTTP协议,通过HTTP协议承载业务数据进行消息交互,例如非常流行的HTTP+XML或者RESTful+JSON。

  1. 需要一套通用、高性能的XML序列化框架,它能够灵活地实现POJO-XML的互相转换,最好能够通过工具自动生成绑定关系,或者通过XML的方式配置双方的映射关系;
  2. 作为通用的HTTP+XML协议栈,XML-POJO对象的映射关系应该非常灵活,支待命名空间和自定义标签;
  3. 提供HTTP+XML请求消息编码器,供HTTP客户端发送请求消息自动编码使用;
  4. 提供HTTP+XML请求消息解码器,供HTTP服务端对请求消息自动解码使用;
  5. 提供HTTP+XML响应消息编码器,供HTTP服务端发送响应消息自动编码使用;
  6. 提供HTTP+XML响应消息解码器,供HTTP客户端对应答消息进行自动解码使用;
  7. 协议栈使用者不需要关心HTTP+XML的编解码,对上层业务零侵入,业务只需要对上层的业务POJO对象进行编排。
高效的XML绑定框架JiBx

JiBx是一款非常优秀的XML(Extensible Markup Language)数据绑定框架。 它提供灵活的绑定映射文件,实现数据对象与XML文件之间的转换,并不需要修改既有的Java类。另外,它的转换效率是目前很多其他开源项目都无法比拟的。

  • 转换效率高
  • 配置绑定文件单
  • 不需要操作xpath文件
  • 不需要写属性的get/set方法
  • 对象属性名与XML文件element名可以不同,等等。

在运行程序之前,盆要先配胃绑定文件并进行绑定,在绑定过程中它将会动态地修改程序中相应的class文件, 主要是生成对应对象实例的方法和添加被绑定标记的属性JiBX_bindingList等。 它使用的技术是BCEL(Byte Code Engineering Library),BCEL是Apache Software Foundation的Jakarta项目的一部分, 也是目前Javaclassworking最广泛使用的一种框架,它可以让你深入JVM汇编语言进行类操作。

在JiBX运行时,它使用了目前比较流行的一个技术XPP(Xml Pull Parsing),这也正是JiBX如此高效的原因。

JiBx有两个比较重要的概念:Unmarshal(数据分解)和Marshal(数据编排)。 从字面意思也很容易理解,Unmarshal是将XML文件转换成Java对象,而Marshal则是将Java对象编排成规范的XML文件。

JiBX在Unmarshal/Marshal上如此高效,这要归功于使用了XPP技术,而不是使用基于树型Ctree-based)方式,将整个文档写入内存,然后进行操作的DOM(Document Object Model), 也不是使用基于事件流(event stream)的SAX(Simple API for Xml)。 XPP使用的是不断增加的数据流处理方式,同时允许在解析XML文件时中断。

通过JiBx提供的工具jar包,可以根据Schema自动生成POJO对象,也可以根据普通的POJO对象生成JiBx绑定文件和Schema定义XSD。

<!-- set classpath for compiling and running application with JiBX -->
<path id="classpath">
	<fileset dir="${jibx-home}/lib" includes="*.jar"/>
	<pathelement location="bin"/>
</path>
...
<!-- generate default binding and schema -->
<target name="bindgen">
	<echo message="Running BindGen tool"/>
	<java classpathref="classpath" fork="true" failonerror="true" classname="org.jibx.binding.generator.BindGen">
		<arg value="-s"/>
		<arg value="${baseDir}/src/main/com/songxp/pojo"/>
		<arg value="com.songxp.pojo.Order"/>
	</java>
</target>
<!-- 根据绑定文件和POJO对象的映射关系和规则动态修改POJO类 -->
<!-- Run JiBX binding compiler -->
<bind verbose="true" load="true" binding="binding.xml">
<classpath>
    <pathelement path="classes"/>
    <pathelement location="${jibx-lib}/jibx-run.jar"/>
</classpath>
</bind> 
private IBindingFactory factory = null;
private StringWriter writer = null;
private StringReader reader = null;
private final static String CHARSET_NAME = "UTF-8";
private String encode2Xml(Order order) throws JiBXException, IOException {
    factory = BindingDirectory.getFactory(Order.class);
    writer = new StringWriter();
    IMarshallingContext marshallingContext = factory.createMarshallingContext();
    marshallingContext.setIndent(2);
    marshallingContext.marshalDocument(order, CHARSET_NAME, null, writer);
    String xmlStr = writer.toString();
    writer.close();
    System.out.println(xmlStr);
    return xmlStr;
}
private Order decode2Order(String xmlBody) throws JiBXException {
    reader = new StringReader(xmlBody);
    IUnmarshallingContext unmarshallingContext = factory.createUnmarshallingContext();
    Order order = (Order) unmarshallingContext.unmarshalDocument(reader);
    return order;
}

HTTP+XML协议代码:

http

客户端:

在ChannelPipeline中新增了HttpResponseDecoder,它负责将二进制码流解码成为HTTP的应答消息; 随后新增了HttpObjectAggregator,它负责将1个HTTP请求消息的多个部分合并成一条完整的HTTP消息; 将开发的XML解码器HttpXmlResponseDecoder添加到ChannelPipelioe中,这样就实现了HTTP+XML应答消息的自动解码。

将HttpRequestEncoder编码器添加到ChannelPipeline中时,需要注意顺序, 编码的时候是按照从尾到头的顺序调度执行的,它后面放的是自定义开发的HTTP+XML请求消息编码器HttpXmIRequestEncoder。

最后是业务的逻辑编排类HttpXmlCiientHandle。

服务端:

ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .childHandler(
                new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch)
                            throws Exception {
                        ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());
                        ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
                        ch.pipeline().addLast("xml-decoder", new HttpXmlRequestDecoder(Order.class, true));
                        ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
                        ch.pipeline().addLast("xml-encoder", new HttpXmlResponseEncoder());
                        ch.pipeline().addLast("xmlServerHandler", new HttpXmlServerHandler());
                    }
                });
ChannelFuture future = serverBootstrap.bind(new InetSocketAddress(port)).sync();
  • 绑定HTTP请求消息解码器;
  • 将我们自定义的HttpXmlRequestDecoder添加到HTTP解码器;
  • 添加自定义的HttpXmlResponseEncoder编码器用于响应消息的编码。

本例开发的HTTP+XML协议栈是个高性能、通用的协议栈,但是,忽略了一些异常场景的处理、可扩展性的API和一些配置能力。 所以,如果在商用项目中使用HTTP+XML协议栈,仍需要做一些产品化的完善工作。

Http协议的弊端

长期以来存在着各种技术让服务器得知有新数据可用时,立即将数据发送到客户端。这些技术种类繁多,例如“推送”或Comet。 最常用的一种黑客手段是对服务器发起连接创建假象,被称为长轮询。利用长轮询,客户端可以打开指向服务器的HTTP连接,而服务器会一直保持连接打开,直到发送响应。 服务器只要实际拥有新数据,就会发送响应(其他技术包括Flash、XHR multipart请求和所谓的HTML Files)。 长轮询和其他技术都非常好用,在Gmail聊天等应用中会经常使用它们。
但是,这些解决方案都存在一个共同的问题:由于HTTP协议的开销,导致它们不适用于低延迟应用。

为了解决这些问题,WebSocket将网络套接字引入到了客户端和服务端,浏览器和服务器之间可以通过套接宇建立持久的连接, 双方随时都可以互发数据给对方,而不是之前由客户端控制的一请求一应答模式。

将HTTP协议的主要弊端总结如下。

  1. HTTP协议为半双工协议。半双工协议指数据可以在客户端和服务端两个方向上传输,但是不能同时传输。它意味着在同一时刻,只有一个方向上的数据传送;
  2. HTTP消息冗长而繁琐。HTTP消息包含消息头、消息体、换行符等,通常情况下采用文本方式传输,相比于其他的二进制通信协议,冗长而繁琐;
  3. 针对服务器推送的黑客攻击。例如长时间轮询。

在分布式组网环境下,每个Netty节点(Netty进程)之间建立长连接,使用Netty协议进行通信。Netty节点并没有服务端和客户端的区分,谁首先发起连接,谁就作为客户端,另一方自然就成为服务端。

现在,很多网站为了实现消息推送,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTPrequest,然后由服务器返回最新的数据给客户端浏览器。 这种传统的模式具有很明显的缺点,即浏览器需要不断地向服务器发出请求,然而HTTP request的Header是非常冗长的,里面包含的可用数据比例可能非常低,这会占用很多的带宽和服务器资源。

比较新的一种轮询技术是Comet,使用了AJAX。这种技术虽然可达到双向通信,但依然需要发出请求,而且在Comet中,普遍采用了长连接,这也会大世消耗服务器带宽和资源。

为了解决HTTP协议效率低下的问题,HTML5定义了WebSocket协议,能更好地节省服务器资源和带宽并达到实时通信。

WebSocket入门

在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道,两者就可以直接互相传送数据了。 WebSocket基于TCP双向全双工进行消息传递,在同一时刻,既可以发送消息,也可以接收消息,相比HTTP的半双工协议,性能得到很大提升。

WebSocket的特点:

  • 单一的TCP 连接,采用全双工模式通信;
  • 对代理、防火墙和路由器透明;
  • 无头部信息、Cookie 和身份验证;
  • 无安全开销;
  • 通过”ping/pong” 帧保持链路激活;
  • 服务器可以主动传递消息给客户端,不再需要客户端轮询。
WebSocket连接建立

为了建立一个WebSocket连接,客户端浏览器首先要向服务器发起一个HTTP请求,这个诸求和通常的HTTP请求不同,包含了一些附加头信息, 其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的HTTP请求。

WebSocket客户端握手请求消息

服务器端解析这些附加的头信息,然后生成应答信息返回给客户端,客户端和服务器端的WebSocket连接就建立起来了,双方可以通过这个连接通道自由地传递信息, 并且这个连接会持续存在直到客户端或者服务器端的某一方主动关闭连接。

WebSocket服务端返回的握手应答消息

WebSocket的生命周期

握手成功之后,服务端和客户端就可以通过”messages”的方式进行通信了,一个消息由一个或者多个帧组成,WebSocket的消息井不一定对应一个特定网络层的帧,它可以被分割成多个帧或者被合井。

帧都有自己对应的类型,属于同一个消息的多个帧具有相同类型的数据。从广义上讲,数据类型可以是文本数据(UTF-8[RFC3629]文字)、二进制数据和控制帧(协议级信令,如信号)。

WebSocket生命周期

连接关闭

为关闭WebSocket连接,客户端和服务端需要通过一个安全的方法关闭底层TCP连接以及TLS会话。如果合适,丢弃任何可能已经接收的字节,必要时(比如受到攻击)可以通过任何可用的手段关闭连接。

底层的TCP连接,在正常情况下,应该首先由服务器关闭。在异常情况下(例如在一个合理的时间周期后没有接收到服务器的TCP Close),客户端可以发起TCP Close。 因此,当服务器被指示关闭WebSocket连接时,它应该立即发起一个TCP Close操作;客户端应该等待服务器的TCP Close。

WebSocket的握手关闭消息带有一个状态码和一个可选的关闭原因,它必须按照协议要求发送一个Close控制帧,当对端接收到关闭控制帧指令时,需要主动关闭WebSocket连接。

Netty WebSocket协议开发

ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch)throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
                pipeline.addLast("http-codec", new HttpServerCodec());
                pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
                pipeline.addLast("http-chunked", new ChunkedWriteHandler());
                pipeline.addLast("handler", new WebSocketServerHandler());
            }
        });

Channel ch = serverBootstrap.bind(port).sync().channel();
  • 首先添加HttpServerCodec,将请求和应答消息编码或者解码为HTTP消息;
  • 增加HttpObjectAggregator,它的目的是将HTTP消息的多个部分组合成一条完整的HTTP消息;
  • 添加ChunkedWriteHandler,来向客户端发送HTML5文件,它主要用于支待浏览器和服务端进行WebSocket通信;
  • 最后增加WebSocket服务端Handler。
import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive;
import static io.netty.handler.codec.http.HttpHeaders.setContentLength;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.util.CharsetUtil;
import java.util.logging.Level;
import java.util.logging.Logger;
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
    private static final Logger logger = Logger.getLogger(WebSocketServerHandler.class.getName());
    private WebSocketServerHandshaker handshaker;
    @Override
    public void messageReceived(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        // 传统的HTTP接入
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        }
        // WebSocket接入
        else if (msg instanceof WebSocketFrame) {
            handleWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
        // 如果HTTP解码失败,返回HHTP异常
        if (!req.getDecoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
            return;
        }

        // 构造握手响应返回,本机测试
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:8080/websocket", null, false);
        handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(), req);
        }
    }
    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 判断是否是关闭链路的指令
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
            return;
        }
        // 判断是否是Ping消息
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        // 本例程仅支持文本消息,不支持二进制消息
        if (!(frame instanceof TextWebSocketFrame)) {
            throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName()));
        }
        // 返回应答消息
        String request = ((TextWebSocketFrame) frame).text();
        if (logger.isLoggable(Level.FINE)) {
            logger.fine(String.format("%s received %s", ctx.channel(), request));
        }
        ctx.channel().write(new TextWebSocketFrame(request + " , 欢迎使用Netty WebSocket服务,现在时刻:" + new java.util.Date().toString()));
    }
    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
        // 返回应答给客户端
        if (res.getStatus().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
            setContentLength(res, res.content().readableBytes());
        }
        // 如果是非Keep-Alive,关闭连接
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if (!isKeepAlive(req) || res.getStatus().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

第一次握手诸求消息由HTTP协议承载,所以它是一个HTTP消息,执行handleHttpRequest方法来处理WebSocket握手请求。 首先对握手请求消息进行判断,如果消息头中没有包含Upgrade字段或者它的值不是websocket,则返回HTTP400响应。

握手请求简单校验通过之后,开始构造握手工厂,创建握手处理类WebSocketServerHandshaker,通过它构造握手响应消息返回给客户端, 同时将WebSocket相关的编码和解码类动态添加到ChannelPipeline中,用于WebSocket消息的编解码。

// 源码
if (ctx == null) {
    // this means the user use a HttpServerCodec
    ctx = p.context(HttpServerCodec.class);
    if (ctx == null) {
        promise.setFailure(
                new IllegalStateException("No HttpDecoder and no HttpServerCodec in the pipeline"));
        return promise;
    }
    p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
    p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());
    encoderName = ctx.name();
} else {
    p.replace(ctx.name(), "wsdecoder", newWebsocketDecoder());
    encoderName = p.context(HttpResponseEncoder.class).name();
    p.addBefore(encoderName, "wsencoder", newWebSocketEncoder());
}
<!-- client -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    Netty WebSocket 时间服务器
</head>
<br>
<body>
<br>
<script type="text/javascript">
    var socket;
    if (!window.WebSocket) {
        window.WebSocket = window.MozWebSocket;
    }
    if (window.WebSocket) {
        socket = new WebSocket("ws://localhost:8080/websocket");
        socket.onmessage = function (event) {
            var ta = document.getElementById('responseText');
            ta.value = "";
            ta.value = event.data
        };
        socket.onopen = function (event) {
            var ta = document.getElementById('responseText');
            ta.value = "打开WebSocket服务正常,浏览器支持WebSocket!";
        };
        socket.onclose = function (event) {
            var ta = document.getElementById('responseText');
            ta.value = "";
            ta.value = "WebSocket 关闭!";
        };
    }
    else {
        alert("抱歉,您的浏览器不支持WebSocket协议!");
    }

    function send(message) {
        if (!window.WebSocket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            socket.send(message);
        } else {
            alert("WebSocket连接没有建立成功!");
        }
    }
</script>
<form onsubmit="return false;">
    <input type="text" name="message" value="Netty最佳实践"/>
    <br><br>
    <input type="button" value="发送WebSocket请求消息" onclick="send(this.form.message.value)"/>
    <hr color="blue"/>
    <h3>服务端返回的应答消息</h3>
    <textarea id="responseText" style="width:500px;height:300px;"></textarea>
</form>
</body>
</html>

私有协议开发

绝大多数的私有协议传输层都基于TCP/IP,所以利用Netty的NIO TCP协议栈可以非常方便地进行私有协议的定制和开发。

在传统的Java应用中,通常使用以下4种方式进行跨节点通信:

  1. 通过RMI进行远程服务调用;
  2. 通过Java的Socket+Java序列化的方式进行跨节点调用;
  3. 利用一些开源的RPC框架进行远程服务调用,例如Facebook的Thrift、Apache的Avro等;
  4. 利用标准的公有协议进行跨节点服务调用,例如HTTP+XML、RESTful+JSON或者WebService。

跨节点的远程服务调用,除了链路层的物理连接外,还需要对请求和响应消息进行编解码。 在请求和应答消息本身以外,也需要携带一些其他控制和管理类指令,例如链路建立的握手请求和响应消息、链路检测的心跳消息等。 当这些功能组合到一起之后,就会形成私有协议。

Netty协议栈

Netty协议栈用于内部各模块之间的通信,它基于TCP/IP协议栈,是一个类HTTP协议的应用层协议栈,相比于传统的标准协议栈,它更加轻巧、灵活和实用。

网络拓扑

在分布式组网环境下,每个Netty节点(Netty进程)之间建立长连接,使用Netty协议进行通信。Netty节点并没有服务端和客户端的区分,谁首先发起连接,谁就作为客户端,另一方自然就成为服务端。

Netty协议网络拓扑示意图

功能描述

Netty协议栈承载了业务内部各模块之间的消息交互和服务调用,它的主要功能如下:

  1. 基于Netty的NIO通信框架,提供高性能的异步通信能力;
  2. 提供消息的编解码框架,可以实现POJO的序列化和反序列化;
  3. 提供基于IP地址的白名单接入认证机制;
  4. 链路的有效性校验机制;
  5. 链路的断连重连机制。
通信模型

Netty协议栈通信交互图:

Netty协议栈通信交互图

具体步骤:

  1. Netty协议栈客户端发送握手请求消息,携带节点ID等有效身份认证信息;
  2. Netty协议栈服务端对握手请求消息进行合法性校验,包括节点ID有效性校验、节点重复登录校验和IP地址合法性校验,校验通过后,返回登录成功的握手应答消息;
  3. 链路建立成功之后,客户端发送业务消息;
  4. 链路成功之后,服务端发送心跳消息;
  5. 链路建立成功之后,客户端发送心跳消息;
  6. 链路建立成功之后,服务端发送业务消息;
  7. 服务端退出时,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接。

Netty协议通信双方链路建立成功之后,双方可以进行全双工通信,无论客户端还是服务端,都可以主动发送请求消息给对方,通信方式可以是TWO WAY或者ONE WAY。 双方之间的心跳采用Ping-Pong机制,当链路处于空闲状态时,客户端主动发送Ping消息给服务端,服务端接收到Ping消息后发送应答消息Pong给客户端, 如果客户端连续发送N条Ping消息都没有接收到服务端返回的Pong消息,说明链路已经挂死或者对方处于异常状态,客户瑞主动关闭连接,间隔周期T后发起重连操作,直到重连成功。

消息定义
  • 消息头;
  • 消息体。
名称 类型 长度 描述
header Header 变长 消息头定义
body Object 变长 对于请求消息,它是方法的参数(作为示例,只支持携带一个参数);对于响应消息,它是返问值

Netty协议消息头定义:

字段 类型 长度 描述
crcCode Int 32 Netty消息校验码,1. 0xAEF:固定值,表明该消息是Netty协议信息,2个字节;2. 主版本号: 1~255,1个字节;3. 次版本号: 1~255,1个字节;
Length Int 32 整个消息长度
sessionID Long 64 会话ID
Type Byte 8 0:业务请求消息;1:业务响应消息;2:业务one way消息;3握手请求消息;4握手应答消息;5:心跳请求消息;6:心跳应答消息
Priority Byte 8 消息优先级:0~255
Attachment Map<String,Object> 变长 可选字段,由于推展消息头

Netty协议支持的数据类型:

字段类型 备注说明
boolean 包括它的包装类型Integer
byte 包括它的包装类型Byte
int 对应于C/C++的int32
char 包括它的包装类型Character
shot 对应C/C++的in1l6
long 对应C/C++的int64
flat 包括它的包装类型Float
double 包括它的包装类型Double
string 对应C/C++的String
list 支持各种List的实现
array 支持各种数组的实现
map 支持Map的嵌套和泛型
set 支持Set的嵌套和泛型
Netty协议的编解码

编码:

  • crcCode:java.nio.ByteBuffer.putInt(int value),如果采用其他缓冲区实现,必须与其等价;
  • length:java.nio.ByteBuffer.putInt(int value),如果采用其他缓冲区实现,必须与其等价;
  • sessionID:java.nio.ByteBuffer.putLong(int value),如果采用其他缓冲区实现,必须与其等价;
  • type:java.nio.ByteBuffer.put(byte value),如果采用其他缓冲区实现,必须与其等价;
  • priority:java.nio.ByteBuffer.put(byte value),如果采用其他缓冲区实现,必须与其等价;
  • attachMent:
    • 如果attachment长度为0,表示没有可选附件,则将长度编码设为0,java.nio.ByteBuffer.putInt(0);
    • 如果大于0,说明有附件需要编码,首先对附件的个数进行编码,java.nio.ByteBuffer.putInt(attachment.size());然后对Key进行编码,先编码长度,再将它转换成byte数组之后编码内容。
  • Body的编码:通过JBoss Marshalling将其序列化为byte数组,然后调用java.nio.ByteBuffer.put(byte[] src)将其写入ByteBuffer缓冲区中。
String key = null;
byte[] value = null;
for (Map.Entry<String, Object> param : attachment.entrySet()) {
    key = param.getKey ();
    buffer.writeString (key) ;
    value = marshaller.writeObject(param.getValue());
    buffer.writeBinary(value) ;
}
key = null;
value = null ;

由于整个消息的长度必须等全部字段都编码完成之后才能确认,所以最后需要更新消息头中的length字段,将其重新写入ByteBuffer中。

解码:

相对于Netty的编码,仍旧以java.nio.ByteBuffer为例,给出Netty协议的解码规范。

  • crcCode:通过java.nio.ByteBuffer.getInt()获取校验码字段,其他缓冲区需要与其等价;
  • length:通过java.nio.ByteBuffer.getInt()获取校验码字段,其他缓冲区需要与其等价;
  • sessionID:通过java.nio.ByteBuffer.getLong()获取校验码字段,其他缓冲区需要与其等价;
  • type:通过java.nio.byteBuffer.get()获取消息类型,其他缓冲区需要与其等价;
  • priority:通过java.nio.byteBuffer.get()获取消息优先级,其他缓冲区需要与其等价;
  • attachment:它的剑麻规则为–首先创建一个新的attachment对象,调用java.nio.ByteBuffer.getInt()获取附件的长度,如果为0,说明附件为空,解码结束,继续解码消息头;如果非空,则根据长度通过for循环进行解码。
  • Body:通过Jboss的marshaller对其进行解码。
String key = null;
Object value = null;
for (int i = 0 ; i < size ; i++) {
    key = buffer.readString();
}
value = unmarshaller.readObject(buffer.readBinary());
this. attachment.put(key,value) ;
key = null;
value = null;
链路的建立

如果A节点需要调用B节点的服务,但是A和B之间还没有建立物理链路,则有调用方主动发起连接,此时,调用方为客户端,被调用方为服务端。

客户端与服务端链路建立成功之后,由客户端发送握手请求消息,服务端接收到客户端的握手请求消息之后,如果校验通过,返回握手成功应答消息给客户端,应用层链路建立成功。 链路建立成功之后,客户端和服务端就可以互相发送业务消息了。

链路的关闭

由于采用长连接通信,在正常的业务运行期间,双方通过心跳和业务消息维持链路,任何一方都不需要主动关闭连接。

但是,在以下情况下,客户端和服务端需要关闭连接:

  • 当对方宕机或者重启时,会主动关闭链路,另一方读取到操作系统的通知信号,得知对方REST链路,需要关闭连接,释放自身的句柄等资源。由于采用TCP全双工通信,通信双方都需要关闭连接,释放资源;
  • 消息读写过程中,发生了I/O异常,需要主动关闭连接;
  • 心跳消息读写过程发生了I/O异常,需要主动关闭连接;
  • 心跳超时,需要主动关闭连接;
  • 发生编码异常等不可恢复错误时,需要主动关闭连接。
可靠性设计
心跳机制

在凌晨等业务低谷时段,如果发生网络闪断、连接被Hang住等问题时,由于没有业务消息,应用程序很难发现。到了白天业务高峰期时,会发生大量的网络通信失败,严重的会导致一段时间进程内无法处理业务消息。 为了解决这个问题,在网络空闲时采用心跳机制来检测链路的互通性,一旦发现网络故障,立即关闭链路,主动重连。

具体的设计思路如下:

  1. 当网络处于空闲状态持续时间达到T(连续周期T没有读写消息)时,客户端主动发送Ping心跳消息给服务端;
  2. 如果在下一个周期T到来时客户端没有收到对方发送的Pong心跳应答消息或者读取到服务端发送的其他业务消息,则心跳失败计数器加1;
  3. 每当客户端接收到服务的业务消息或者Pong应答消息,将心跳失败计数器清零;当练习N次没有接收到服务端的Pong消息或者业务消息,则关闭链路,间隔INTERVAL时间后发起重连操作;
  4. 服务端网络空闲状态持续时间达到T后,服务端将心跳失败计数器加1;只要接收到客户端发送的Ping消息或者其他业务消息,计数器清零;
  5. 服务端连续N次没有接收到客户端的ping消息或者其他业务消息,则关闭链路,释放资源,等到客户端重连。

通过Ping-Pong双向心跳机制,可以保证无论通信哪一方出现网络故障,都能被及时的检查出来,为了防止由于对方短时间内繁忙没有及时返回应答造成的误判,只有连续N次心跳检查都失败才认定链路已经损害,需要关闭链路并重建链路。

当读或者写心跳消息发生I/O异常的时候,说明已经中断,此时需要立即关闭连接,如果是客户端,需要重新发起连接。如果是服务端,需要清空缓存的半包信息,等到客户端重连。

重连机制

如果链路中断,等到INTEVAL时间后,由客户端发起重连操作,如果重连失败,间隔周期INTERVAL后再次发起重连,直到重连成功。

为了保持服务端能够有充足的时间释放句柄资源,在首次断连时客户端需要等待INTERVAL时间之后再发起重连,而不是失败后立即重连。

为了保证句柄资源能够及时释放,无论什么场景下重连失败,客户端必须保证自身的资源被及时释放,包括但不现居SocketChannel、Socket等。

重连失败后,需要打印异常堆栈信息,方便后续的问题定位。

重复登录保护

当客户端握手成功之后,在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常状态下反复重连导致句柄资源被耗尽。

服务端接收到客户端的握手请求消息之后,首先对IP地址进行合法性校验,如果校验成功,在缓存的地址表中查看客户端是否已经登录,如果登录,则拒绝重复登录,返回错误码-1,同时关闭TCP链路,并在服务端的日志中打印握手失败的原因。

客户端接收到握手失败的应答消息之后,关闭客户端的TCP连接,等待INTERVAL时间之后,再次发起TCP连接,知道认证成功。

为了防止由服务端和客户端对链路状态理解不一致导致的客户端无法握手成功问题,当服务端连续N次心跳超时之后需要主动关闭链路,清空改客户端的地址缓存信息,以保证后续改客户端可以重连成功,防止被重复登录保护机制拒绝掉。

消息缓存重发

无论客户端还是服务端,当发生链路中断之后,在链路恢复之前,缓存的消息队列中待发送的消息不能丢失,等链路恢复之后,重新发送这些消息,保证链路中断期间消息不丢失。

考虑到内存溢出的风险,建议消息缓存队列设置上限,当达到上限之后,应该拒绝继续想该队列添加新的消息。

安全性设计

如果在公网中,需要更复杂的认证机制,如密钥和ASE加密的用户名+密码的认证机制,也可以采用SSL/TSL安全传输。

可扩展性设计

业务可以在消息头中自定义业务域字段。通过attachment字段,可以方便的扩展。

Netty协议栈架构需要具备一定的扩展能力,例如统一的消息拦截、接口日志、安全、加解密等可以被方便地添加和删除,不需要修改之前的逻辑代码, 类似Servlet的Filter Chain和AOP,但考虑到性能因素,不推荐通过AOP来实现功能的扩展。

Netty协议栈开发

源代码

public class NettyMessageDecoder extends LengthFieldBasedFrameDecoder {
    MarshallingDecoder marshallingDecoder;
    public NettyMessageDecoder(int maxFrameLength, int lengthFieldOffset,int lengthFieIdlength) throws IOException {
        super(maxFrameLength, lengthFieldOffset, lengthFieIdlength);
        marshallingDecoder = new MarshallingDecoder();
    }
    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in)throws Exception {
        // LengthFieldBasedFrameDecoder支持半包处理和粘包处理
        // 只要给出标识消息长度的字段偏移量和消息长度自身所占的字节数,Netty就能够自动实现对半包的处理。
        ByteBuf frame = (ByteBuf) super.decode(ctx, in);
        // 如果是null说名是半包直接返回继续由I/O线程读取后续的码流。
        if (frame == null) {
            return null;
        }
        NettyMessage message = new NettyMessage();
        Header header = new Header();
        header.setCrcCode(frame.readInt());
        header.setLength(frame.readInt());
        header.setSessionID(frame.readLong());
        header.setType(frame.readByte());
        header.setPriority(frame.readByte());

        int size = frame.readInt();
        if (size > 0) {
            Map<String, Object> attch = new HashMap<String, Object>(size);
            int keySize = 0;
            byte[] keyArray = null;
            String key = null;
            for (int i = 0; i < size; i++) {
                keySize = frame.readInt();
                keyArray = new byte[keySize];
                frame.readBytes(keyArray);
                key = new String(keyArray, "UTF-8");
                attch.put(key, marshallingDecoder.decode(frame));
            }
            keyArray = null;
            key = null;
            header.setAttachment(attch);
        }
        if (frame.readableBytes() > 4) {
            message.setBody(marshallingDecoder.decode(frame));
        }
        message.setHeader(header);
        return message;
    }
}
public class NettyClient {
    private static final Log LOGGER = LogFactory.getLog(NettyClient.class);
    private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
    EventLoopGroup group = new NioEventLoopGroup();
    public void connect(int port, String host) throws Exception {
        // 配置客户端NIO线程组
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch)
                                throws Exception {
                            // 防止由于单条消息过大导致内存溢出或者畸形码流导致解码错位引起内存分配失败
                            ch.pipeline().addLast(new NettyMessageDecoder(1024 * 1024, 4, 4));
                            ch.pipeline().addLast("MessageEncoder", new NettyMessageEncoder());
                            ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));
                            ch.pipeline().addLast("LoginAuthHandler", new LoginAuthReqHandler());
                            ch.pipeline().addLast("HeartBeatHandler", new HeartBeatReqHandler());
                        }
                    });
            // 发起异步连接操作
            ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port), new InetSocketAddress(NettyConstant.LOCALIP, NettyConstant.LOCAL_PORT)).sync();
            // 当对应的channel关闭的时候,就会返回对应的channel。
            // Returns the ChannelFuture which will be notified when this channel is closed. This method always returns the same future instance.
            future.channel().closeFuture().sync();
        } finally {
            // 所有资源释放完成之后,清空资源,再次发起重连操作
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        try {
                            // 发起重连操作
                            connect(NettyConstant.PORT, NettyConstant.REMOTEIP);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    public static void main(String[] args) throws Exception {
        new NettyClient().connect(NettyConstant.PORT, NettyConstant.REMOTEIP);
    }
}

利用Netty的ChannefPipeline和ChannelHandler机制,可以非常方便地实现功能解耦和业务产品的定制。 例如心跳定时器、握手请求和后端的业务处理可以通过不同的Handler来实现,类似于AOP。 通过HandlerChain的机制可以方便地实现切面拦截和定制,相比于AOP它的性能更高。

public class NettyServer {
    private static final Log LOG = LogFactory.getLog(NettyServer.class);
    public void bind() throws Exception {
        // 配置服务端的NIO线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 100)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch)
                            throws IOException {
                        ch.pipeline().addLast(new NettyMessageDecoder(1024 * 1024, 4, 4));
                        ch.pipeline().addLast(new NettyMessageEncoder());
                        // 实现超时主动关闭链路,这会触发handler的exceptionCaught方法释放资源
                        ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));
                        ch.pipeline().addLast(new LoginAuthRespHandler());
                        ch.pipeline().addLast("HeartBeatHandler", new HeartBeatRespHandler());
                    }
                });

        // 绑定端口,同步等待成功
        serverBootstrap.bind(NettyConstant.REMOTEIP, NettyConstant.PORT).sync();
        LOG.info("Netty server start ok : " + (NettyConstant.REMOTEIP + " : " + NettyConstant.PORT));
    }
    public static void main(String[] args) throws Exception {
        new NettyServer().bind();
    }
}

对于实际商用协议栈而言,是不足的。例如当链路断连的时候,已经放入发送队列中的消息不能丢失, 更加通用的做法是提供通知机制,将发送失败的消息通知给业务侧,由业务做决定:是丢弃还是缓存重发。

服务器端创建源码解析

Netty服务端创建需要的必备知识如下:

  1. 熟悉JDK NIO主要类库的使用,例如ByteBuffer、Selector、ServerSocketChannel等;
  2. 熟悉JDK的多线程编程;
  3. 了解Reactor模式。

Netty服务器端创建时序图

Netty服务端创建时序图

// 配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 100)
        .handler(new LoggingHandler(LogLevel.INFO))
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch)
                    throws IOException {
                ch.pipeline().addLast(new NettyMessageDecoder(1024 * 1024, 4, 4));
                ch.pipeline().addLast(new NettyMessageEncoder());
                ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));
                ch.pipeline().addLast(new LoginAuthRespHandler());
                ch.pipeline().addLast("HeartBeatHandler", new HeartBeatRespHandler());
            }
        });

// 绑定端口,同步等待成功
serverBootstrap.bind(NettyConstant.REMOTEIP, NettyConstant.PORT).sync();
  • 步骤1:创建ServerBootstrap实例。
    • ServerBootstrap是Netty服务端的启动辅助类,它提供了一系列的方法用于设置服务端启动相关的参数。
    • 底层通过门面模式对各种能力进行抽象和封装,尽量不需要用户跟过多的底层API打交道,以降低用户的开发难度。
    • 无参构造器加Builder模式
  • 步骤2:设置并绑定Reactor线程池。
    • Netty的Reactor线程池是EventLoopGroup,它实际就是EventLoop的数组。EventLoop的职责是处理所有注册到本线程多路复用器Selector上的Channel,Selector的轮询操作由绑定的EventLoop线程run方法驱动,在一个循环体内循环执行。
    • EventLoop的职责不仅仅是处理网络I/O事件,用户自定义的Task和定时任务Task也统一由EventLoop负责处理,这样线程模型就实现了统一。
    • 从调度层面看,也不存在从EventLoop线程中再启动其他类型的线程用于异步执行另外的任务,这样就避免了多线程并发操作和锁竞争,提升了I/O线程的处理和调度性能。
  • 步骤3:设置并绑定服务端Channel。
    • Netty对原生的NIO 类库进行了封装,对应实现是NioServerSocketChannel。
    • Netty通过工厂类,利用反射创建NioServerSocketChannel对象。
  • 步骤4:链路建立的时候创建并初始化ChannelPipeline。
    • 是一个负责处理网络事件的职责链,负责管理和执行ChannelHandler。
    • 网络事件以事件流的形式在ChannelPipeline中流转,由ChannelPipeline根据ChannelHandler的执行策略调度ChannelHandler的执行。
    • 典型的网络事件
      • 链路注册;
      • 链路激活;
      • 链路断开;
      • 接收到请求消息;
      • 诮求消息接收并处理完毕;
      • 发送应答消息:
      • 链路发生异常;
      • 发生用户自定义事件。
  • 步骤5:初始化ChannelPipeline完成之后,添加并设置ChannelHandler。
    • ChannelHandler是Netty提供给用户定制和扩展的关键接口。利用ChannelHandler用户可以完成大多数的功能定制,例如消息编解码、心跳、安全认证、TSL/SSL认证、流量控制和流量整形等。
    • Netty同时也提供了大量的系统ChannelHandler供用户使用
      • 系统编解码框架:ByteToMessageCodec
      • 通用基于长度的半包解码器:LengthFieldBasedFrameDecoder
      • 码流日志打印Handler:LoggingHandler
      • SSL安全认证Handler:SslHandler
      • 链路空闲检测Handler:IdleStateHandler
      • 流量整形Handler:ChannelTrafficShapingHandler
      • Base64编解码:Base64Decoder和Base64Encoder
  • 步骤6:绑定并启动监听端口。
    • 在绑定监听端口之前系统会做一系列的初始化和检测工作,完成之后,会启动监听端口,并将ServerSocketChannel注册到Selector上监听客户端连接。
  • 步骤7:Selector轮询。
    • 由Reactor线程NioEventLoop负责调度和执行Selector轮询操作,选择准备就绪的Channel集合
  • 步骤8:当轮询到准备就绪的Channel之后,就由Reactor线程NioEventLoop执行ChannelPipeline的相应方法,最终调度并执行ChannelHandler。
  • 步骤9:执行Netty系统ChannelHandler和用户添加定制的ChannelHandler。ChannelPipeline根据网络事件的类型,调度并执行ChannelHandler

Netty服务器端创建源码分析

首先通过构造函数创建ServerBootstrap实例,随后,通常会创建两个EventLoopGroup(并不是必须要创建两个不同的EventLoopGroup,也可以只创建一个并共享)。

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();

NioEventLoopGroup实际就是Reactor线程池,负责调度和执行客户端的接入、网络读写事件的处理、用户自定义任务和定时任务的执行。


通过指定Channel类型的方式创建Channel工厂。

ServerBootstrapChannelFactory是ServerBootstrap的内部静态类,职责是根据Channel的类型通过反射创建Channel的实例,服务端需要创建的是NioServerSocketChannel实例。

@Override
public T newChannel(EventLoop eventLoop, EventLoopGroup childGroup) {
    try {
        Constructor<? extends T> constructor = clazz.getConstructor(EventLoop.class, EventLoopGroup.class);
        return constructor.newInstance(eventLoop, childGroup);
    } catch (Throwable t) {
        throw new ChannelException("Unable to create Channel from class " + clazz, t);
    }
}

指定NioServerSocketChannel后,需要设置TCP的一些参数,作为服务端,主要是要设置TCP的 backlog参数,底层C的对应接口定义如下:

int listen(int fd, int backlog);

backlog指定了内核为此套接口排队的最大连接个数,对于给定的监听套接口,内核要维护两个队列,未链接队列和已连接队列,根据TCP三路握手过程中三个分节来分隔这两个队列。

服务器处于listen状态时收到客户端syn分节(connect)时在未完成队列中创建一个新的条目,然后用三路握手的第二个分节即服务器的syn响应及对客户端syn的ack,此条目在第三个分节到达前(客户端对服务器syn的ack)一直保留在未完成连接队列中, 如果三路握手完成,该条目将从未完成连接队列搬到已完成连接队列尾部。

当进程调用accept时,从已完成队列中的头部取出一个条目给进程,当已完成队列为空时进程将睡眠,直到有条目在已完成连接队列中才唤醒。

backlog被规定为两个队列总和的最大值,大多数实现默认值为5,但在高并发web服务器中此值显然不够,lighttpd中此值达到128*8。 需要设置此值更大一些的原因是未完成连接队列的长度可能因为客户端SYN的到达及等待三路握手第三个分节的到达延时而增大。 Netty默认的backlog为100,当然,用户可以修改默认值,用户需要根据实际场景和网络状况进行灵活设置。


TCP参数设置完成后,用户可以为启动辅助类和其父类分别指定Handler,两类Handler的用途不同,子类中的Hanlder是NioServerSocketChannel对应的ChannelPipeline的Handler,父类中的Hanlder是客户端新接入的连接SocketChannel对应的ChannelPipeline的Handler。

ServerBootstrap的Hanlder模型:

ServerBootstrap的Hanlder模型

本质区别就是:

  • ServerBootstrap中的Handler是NioServerSocketChannel使用的,所有连接该监听端口的客户端都会执行它;
  • 父类AbstractBootstrap中的Handler是个工厂类,它为每个新接入的客户端都创建一个新的Handler。
serverBootstrap
        .group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 100)
        // 父handler
        .handler(new LoggingHandler(LogLevel.INFO))
        // ServerBootstrap中的Handler
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) throws IOException {
                ch.pipeline().addLast(new NettyMessageDecoder(1024 * 1024, 4, 4));
                ch.pipeline().addLast(new NettyMessageEncoder());
                ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));
                ch.pipeline().addLast(new LoginAuthRespHandler());
                ch.pipeline().addLast("HeartBeatHandler", new HeartBeatRespHandler());
            }
        });

服务端启动的最后一步,就是绑定本地端口,启动服务。

private ChannelFuture doBind(final SocketAddress localAddress) {
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.cause() != null) {
        return regFuture;
    }
    final ChannelPromise promise;
    if (regFuture.isDone()) {
        promise = channel.newPromise();
        doBind0(regFuture, channel, localAddress, promise);
    } else {
        // Registration future is almost always fulfilled already, but just in case it's not.
        promise = new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE);
        regFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                doBind0(regFuture, channel, localAddress, promise);
            }
        });
    }
    return promise;
}
final ChannelFuture initAndRegister() {
    Channel channel;
    try {
        // 由ServerBootStrap实现
        channel = createChannel();
    } catch (Throwable t) {
        return VoidChannel.INSTANCE.newFailedFuture(t);
    }
    try {
        // 初始化
        init(channel);
    } catch (Throwable t) {
        channel.unsafe().closeForcibly();
        return channel.newFailedFuture(t);
    }
    ChannelPromise regFuture = channel.newPromise();
    // 注册到多路复用器上监听新客户端的接入
    channel.unsafe().register(regFuture);
    if (regFuture.cause() != null) {
        if (channel.isRegistered()) {
            channel.close();
        } else {
            channel.unsafe().closeForcibly();
        }
    }
    return regFuture;
}
@Override
Channel createChannel() {
    EventLoop eventLoop = group().next();
    return channelFactory().newChannel(eventLoop, childGroup);
}
  • 参数一是从父类的NIO线程池中顺序获取一个NioEventLoop,它就是服务端用于监听和接收客户端连接的Reactor线程。
  • 第二个参数就是所谓的workerGroup线程池,它就是处理IO读写的Reactor线程组。

NioServerSocketChannel创建成功后对它进行初始化,初始化工作主要有三点:

  • 设置Socket参数和NioServerSocketChannel的附加属性
  • 将AbstractBootstrap的Handler添加到NioServerSocketChannel的ChannelPipeline中
  • 将用于服务端注册的Handler ServerBootstrapAcceptor添加到ChannelPipeline中
void init(Channel channel) throws Exception {
    final Map<ChannelOption<?>, Object> options = options();
    synchronized (options) {
        channel.config().setOptions(options);
    }
    final Map<AttributeKey<?>, Object> attrs = attrs();
    // 设置参数和附加信息
    synchronized (attrs) {
        for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
            @SuppressWarnings("unchecked")
            AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
            channel.attr(key).set(e.getValue());
        }
    }
    ChannelPipeline p = channel.pipeline();
    if (handler() != null) {
        // 添加AbstractBootstrap的Handler
        p.addLast(handler());
    }
    final ChannelHandler currentChildHandler = childHandler;
    final Entry<ChannelOption<?>, Object>[] currentChildOptions;
    final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
    synchronized (childOptions) {
        currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
    }
    synchronized (childAttrs) {
        currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
    }
    p.addLast(new ChannelInitializer<Channel>() {
        @Override
        public void initChannel(Channel ch) throws Exception {
            // 添加服务器端注册的Handler
            ch.pipeline().addLast(new ServerBootstrapAcceptor(currentChildHandler, currentChildOptions,
                    currentChildAttrs));
        }
    });
}

NioServerSocketChannel的ChannelPipeline:

NioServerSocketChannel的ChannelPipeline


注册NioServerSocketChannel到Reactor线程的多路复用器上,然后轮询客户端连接事件。

@Override
public final void register(final ChannelPromise promise) {
    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            logger.warn(
                    "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                    AbstractChannel.this, t);
            closeForcibly();
            closeFuture.setClosed();
            promise.setFailure(t);
        }
    }
}
  • 首先判断是否是NioEventLoop自身发起的操作,如果是,则不存在并发操作,直接执行Channel注册;
  • 如果由其它线程发起,则封装成一个Task放入消息队列中异步执行。
    • 由于是由ServerBootstrap所在线程执行的注册操作,所以会将其封装成Task投递到NioEventLoop中执行
private void register0(ChannelPromise promise) {
    try {
        // check if the channel is still open as it could be closed in the mean time when the register
        // call was outside of the eventLoop
        if (!ensureOpen(promise)) {
            return;
        }
        // 注册
        doRegister();
        registered = true;
        // 注册成功
        promise.setSuccess();
        // 触发事件
        pipeline.fireChannelRegistered();
        // 传递完成后判断是否监听成功,如果成功出发active事件
        if (isActive()) {
            pipeline.fireChannelActive();
        }
    } catch (Throwable t) {
        // Close the channel directly to avoid FD leak.
        closeForcibly();
        closeFuture.setClosed();
        if (!promise.tryFailure(t)) {
            logger.warn(
                    "Tried to fail the registration promise, but it is complete already. " +
                            "Swallowing the cause of the registration failure:", t);
        }
    }
}
@Override
protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
            selectionKey = javaChannel().register(eventLoop().selector, 0, this);
            return;
        } catch (CancelledKeyException e) {
            if (!selected) {
                // Force the Selector to select now as the "canceled" SelectionKey may still be
                // cached and not removed because no Select.select(..) operation was called yet.
                eventLoop().selectNow();
                selected = true;
            } else {
                // We forced a select operation on the selector before but the SelectionKey is still cached
                // for whatever reason. JDK bug ?
                throw e;
            }
        }
    }
}

应该注册OP_ACCEPT(16)到多路复用器上,怎么注册0呢?0表示只注册,不监听任何网络操作。这样做的原因如下:

  • 注册方法是多态的,它既可以被NioServerSocketChannel用来监听客户端的连接接入,也可以用来注册SocketChannel,用来监听网络读或者写操作;
  • 通过SelectionKey的interestOps(int ops)方法可以方便的修改监听操作位。所以,此处注册需要获取SelectionKey并给AbstractNioChannel的成员变量selectionKey赋值。

注册成功之后,触发ChannelRegistered事件。

Netty的HeadHandler不需要处理ChannelRegistered事件,所以,直接调用下一个Handler,当ChannelRegistered事件传递到TailHandler后结束,TailHandler也不关心ChannelRegistered事件,因此是空实现。

ChannelRegistered事件传递完成后,判断ServerSocketChannel监听是否成功,如果成功,需要出发NioServerSocketChannel的ChannelActive事件。

isActive()也是个多态方法:

  • 如果是服务端,判断监听是否启动,
  • 如果是客户端,判断TCP连接是否完成。

ChannelActive事件在ChannelPipeline中传递,完成之后根据配置决定是否自动触发Channel的读操作。

@Override
public ChannelPipeline fireChannelActive() {
    head.fireChannelActive();
    if (channel.config().isAutoRead()) {
        channel.read();
    }
    return this;
}

AbstractChannel的读操作触发ChannelPipeline的读操作,最终调用到HeadHandler的读方法:

@Override
public void read(ChannelHandlerContext ctx) {
    unsafe.beginRead();
}

继续看AbstractUnsafe的beginRead方法:

@Override
public void beginRead() {
    if (!isActive()) {
        return;
    }
    try {
        doBeginRead();
    } catch (final Exception e) {
        invokeLater(new Runnable() {
            @Override
            public void run() {
                pipeline.fireExceptionCaught(e);
            }
        });
        close(voidPromise());
    }
}

由于不同类型的Channel对读操作的准备工作不同,因此,beginRead也是个多态方法,对于NIO通信,无论是客户端还是服务端,都是要修改网络监听操作位为自身感兴趣的。

对于NioServerSocketChannel感兴趣的操作是OP_ACCEPT(16),于是重新修改注册的操作位为OP_ACCEPT:

@Override
protected void doBeginRead() throws Exception {
    if (inputShutdown) {
        return;
    }
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }
    final int interestOps = selectionKey.interestOps();
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

在某些场景下,当前监听的操作类型和Chanel关心的网络事件是一致的,不需要重复注册,所以增加了&操作的判断,只有两者不一致,才需要重新注册操作位。

JDK SelectionKey有四种操作类型,分别为:

  • OP_READ = 1 « 0;
  • OP_WRITE = 1 « 2;
  • OP_CONNECT = 1 « 3;
  • OP_ACCEPT = 1 « 4。

由于只有四种网络操作类型,所以用4 bit就可以表示所有的网络操作位,由于JAVA语言没有bit类型,所以使用了整形来表示, 每个操作位代表一种网络操作类型,分别为:0001、0010、0100、1000,这样做的好处是可以非常方便的通过位操作来进行网络操作位的状态判断和状态修改,提升操作性能。

由于创建NioServerSocketChannel将readInterestOp设置成了OP_ACCEPT,所以,在服务端链路注册成功之后重新将操作位设置为监听客户端的网络连接操作。

public NioServerSocketChannel(EventLoop eventLoop, EventLoopGroup childGroup) {
    super(null, eventLoop, childGroup, newSocket(), SelectionKey.OP_ACCEPT);
    config = new DefaultServerSocketChannelConfig(this, javaChannel().socket());
}

客户端接入源码分析

负责处理网络读写、连接和客户端请求接入的Reactor线程是NioEventLoop。

当多路复用器检测到新的准备就绪的Channel时,默认执行processSelectedKeysOptimized方法(NioEventLoop的run()方法中入口)。

@Override
protected void run() {
    for (;;) {
        oldWakenUp = wakenUp.getAndSet(false);
        try {
            if (hasTasks()) {
                selectNow();
            } else {
                select();
                // 'wakenUp.compareAndSet(false, true)' is always evaluated
                // before calling 'selector.wakeup()' to reduce the wake-up
                // overhead. (Selector.wakeup() is an expensive operation.)
                //
                // However, there is a race condition in this approach.
                // The race condition is triggered when 'wakenUp' is set to
                // true too early.
                //
                // 'wakenUp' is set to true too early if:
                // 1) Selector is waken up between 'wakenUp.set(false)' and
                //    'selector.select(...)'. (BAD)
                // 2) Selector is waken up between 'selector.select(...)' and
                //    'if (wakenUp.get()) { ... }'. (OK)
                //
                // In the first case, 'wakenUp' is set to true and the
                // following 'selector.select(...)' will wake up immediately.
                // Until 'wakenUp' is set to false again in the next round,
                // 'wakenUp.compareAndSet(false, true)' will fail, and therefore
                // any attempt to wake up the Selector will fail, too, causing
                // the following 'selector.select(...)' call to block
                // unnecessarily.
                //
                // To fix this problem, we wake up the selector again if wakenUp
                // is true immediately after selector.select(...).
                // It is inefficient in that it wakes up the selector for both
                // the first case (BAD - wake-up required) and the second case
                // (OK - no wake-up required).
                if (wakenUp.get()) {
                    selector.wakeup();
                }
            }
            cancelledKeys = 0;
            final long ioStartTime = System.nanoTime();
            needsToSelectAgain = false;
            if (selectedKeys != null) {
                processSelectedKeysOptimized(selectedKeys.flip());
            } else {
                processSelectedKeysPlain(selector.selectedKeys());
            }
            final long ioTime = System.nanoTime() - ioStartTime;
            final int ioRatio = this.ioRatio;
            runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
            if (isShuttingDown()) {
                closeAll();
                if (confirmShutdown()) {
                    break;
                }
            }
        } catch (Throwable t) {
            logger.warn("Unexpected exception in the selector loop.", t);
            // Prevent possible consecutive immediate failures that lead to
            // excessive CPU consumption.
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // Ignore.
            }
        }
    }
}
private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
    for (int i = 0;; i ++) {
        final SelectionKey k = selectedKeys[i];
        if (k == null) {
            break;
        }
        final Object a = k.attachment();
        if (a instanceof AbstractNioChannel) {
            processSelectedKey(k, (AbstractNioChannel) a);
        } else {
            @SuppressWarnings("unchecked")
            NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
            processSelectedKey(k, task);
        }
        if (needsToSelectAgain) {
            selectAgain();
            selectedKeys = this.selectedKeys.flip();
            i = -1;
        }
    }
}

由于Channel的Attachment是NioServerSocketChannel,所以执行processSelectedKey方法,根据就绪的操作位,执行不同的操作, 由于监听的是连接操作,所以执行unsafe.read()方法,由于不同的Channel执行不同的操作,所以NioUnsafe被设计成接口,由不同的Channel内部的NioUnsafe实现类负责具体实现, read()方法的实现有两个,分别是NioByteUnsafe和NioMessageUnsafe,对于NioServerSocketChannel,它使用的是NioMessageUnsafe:

@Override
public void read() {
    assert eventLoop().inEventLoop();
    if (!config().isAutoRead()) {
        removeReadOp();
    }
    final ChannelConfig config = config();
    final int maxMessagesPerRead = config.getMaxMessagesPerRead();
    final boolean autoRead = config.isAutoRead();
    final ChannelPipeline pipeline = pipeline();
    boolean closed = false;
    Throwable exception = null;
    try {
        for (;;) {
            // 接收新的客户端连接并创建NioSocketChannel
            int localRead = doReadMessages(readBuf);
            if (localRead == 0) {
                break;
            }
            if (localRead < 0) {
                closed = true;
                break;
            }
            if (readBuf.size() >= maxMessagesPerRead | !autoRead) {
                break;
            }
        }
    } catch (Throwable t) {
        exception = t;
    }
    int size = readBuf.size();
    for (int i = 0; i < size; i ++) {
        // 接收到新的客户端连接后,触发ChannelPipeline的ChannelRead方法
        pipeline.fireChannelRead(readBuf.get(i));
    }
    readBuf.clear();
    // 触发事件
    pipeline.fireChannelReadComplete();
    if (exception != null) {
        if (exception instanceof IOException) {
            // ServerChannel should not be closed even on IOException because it can often continue
            // accepting incoming connections. (e.g. too many open files)
            closed = !(AbstractNioMessageChannel.this instanceof ServerChannel);
        }

        pipeline.fireExceptionCaught(exception);
    }
    if (closed) {
        if (isOpen()) {
            close(voidPromise());
        }
    }
}

@Override
protected int doReadMessages(List<Object> buf) throws Exception {
    SocketChannel ch = javaChannel().accept();
    try {
        if (ch != null) {
            buf.add(new NioSocketChannel(this, childEventLoopGroup().next(), ch));
            return 1;
        }
    } catch (Throwable t) {
        logger.warn("Failed to create a new channel from an accepted socket.", t);
        try {
            ch.close();
        } catch (Throwable t2) {
            logger.warn("Failed to close a socket.", t2);
        }
    }
    return 0;
}

执行headChannelHandlerContext的fireChannelRead方法,事件在ChannelPipeline中传递,执行ServerBootstrapAcceptor的channelRead方法:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    Channel child = (Channel) msg;
    // 设置childHandler到Pipeline
    child.pipeline().addLast(childHandler);
    for (Entry<ChannelOption<?>, Object> e: childOptions) {
        try {
            // 设置TCP参数
            if (!child.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
                logger.warn("Unknown channel option: " + e);
            }
        } catch (Throwable t) {
            logger.warn("Failed to set a channel option: " + child, t);
        }
    }
    for (Entry<AttributeKey<?>, Object> e: childAttrs) {
        child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
    }
    // 注册SocketChinnel到多路复用器
    child.unsafe().register(child.newPromise());
}

该方法包含三个主要步骤:

  • 第一步:将启动时传入的childHandler加入到客户端SocketChannel的ChannelPipeline中;
  • 第二步:设置客户端SocketChannel的TCP参数;
  • 第三步:注册SocketChannel到多路复用器。

NioSocketChannel的注册方法也是将Channel注册到Reactor线程的多路复用器上。

ChannelReadComplete在ChannelPipeline中的处理流程:

Netty的Header和Tail本身不关注ChannelReadComplete事件就直接透传,执行完ChannelReadComplete后,接着执行PipeLine的read()方法,最终执行HeadHandler的read()方法。

@Override
public ChannelPipeline fireChannelReadComplete() {
    head.fireChannelReadComplete();
    if (channel.config().isAutoRead()) {
        read();
    }
    return this;
}

创建NioSocketChannel的时候已经将AbstractNioChannel的readInterestOp设置为OP_READ,这样,执行selectionKey.interestOps(interestOps | readInterestOp)操作时就会把操作位设置为OP_READ。

客户端创建源码解析

Netty客户端创建时序图

Netty客户端创建时序图

Netty客户端流程分析

  • 步骤1:用户线程创建Bootstrap实例,通过API设置创建客户端相关的参数,异步发起客户端连接。
  • 步骤2:创建处理客户端连接、通过构造函数指定I/O线程的个数,I/O读写的Reactor线程组NioEventLoopGroup。可以默认为CPU内核数的2倍;
  • 步骤3:通过Bootstrap的ChannelFactory和用户指定的Channel类型创建用于客户端连接的NioSocketChannel,它的功能类似于JDK NIO类库提供的SocketChannel;
  • 步骤4:创建默认的Channel Handler Pipeline,用于调度和执行网络事件;
  • 步骤5:异步发起TCP连接,判断连接是否成功。如果成功,则直接将NioSocketChannel注册到多路复用器上,监听读操作位,用于数据报读取和消息发送;如果没有立即连接成功,则注册连接监听位到多路复用器,等待连接结果;
  • 步骤6:注册对应的网络监听状态位到多路复用器;
  • 步骤7:由多路复用器在I/O线程中轮询各Channel,处理连接结果;
  • 步骤8:如果连接成功,设置Future结果,发送连接成功事件,触发ChannelPipeline执行;
  • 步骤9:由ChannelPipeline调度执行系统和用户的ChannelHandler,执行业务逻辑。

Netty客户端创建源码分析

客户端连接辅助类Bootstrap

设置I/O线程组:

非阻塞I/O的特点就是一个多路复用器司以同时处理成百上千条链路,这就意味着使用NIO模式一个线程可以处理多个TCP连接。 考虑到I/O线程的处理性能,大多数NIO框架都采用线程池的方式处理I/O读写,Netty也不例外。客户端相对于服务端,只需要一个处理I/O读写的线程组即可。

由于Netty的NIO线程组默认采用EventLoopGroup接口,因此线程组参数使用EventLoopGroup。

TCP参数设置接口:

无论是异步NIO,还是同步BIO,创建客户端套接字的时候通常都会设置连接参数,例如接收和发送缓冲区大小、连接超时时间等。

Netty提供的主要TCP参数如下:

  • SO_TIMEOUT:控制读取操作将阻塞多少毫秒。如果返回值为O,计时器就被禁止了,该线程将无限期阻塞;
  • SOSNDBUF:套接字使用的发送缓冲区大小;
  • SORCVBUF:套接字使用的接收缓冲区大小;
  • SO_REUSEADDR:用于决定如果网络上仍然有数据向旧的ServerSocket传输数据,是否允许新的ServerSocket绑定到与旧的ServerSocket同样的端口上。
    • SO_REUSEADDR选项的默认值与操作系统有关,在某些操作系统中,允许重用端口,而在某些操作系统中不允许重用端口;
  • CONNECT_TIMEOUT_MILLIS:客户端连接超时时间,由于NIO原生的客户端并不提供设置连接超时的接口,因此,Netty采用的是自定义连接超时定时器负责检测和超时控制;
  • TCP_NODELAY:激活或禁止TCP_NODELAY套接字选项,它决定是否使用Nagle算法。如果是时延敏感型的应用,建议关闭Nagle算法。

channel接口:

用于指定客户端使用的channel接口,对于TCP客户端连接,默认使用NioSocketChannel。

public Bootstrap channel(Class<? extends Channel> channelClass) {
    if (channelClass == null) {
        throw new NullPointerException("channelClass");
    }
    return channelFactory(new BootstrapChannelFactory<Channel>(channelClass));
}

设置Handler接口:

Bootstrap为了简化Handler的编排,提供了Channellnitializer,它继承了ChannelHandlerAdapter,当TCP链路注册成功之后,调用initChannel接口,用于设置用户ChannelHandler。

@Override
@SuppressWarnings("unchecked")
public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
    ChannelPipeline pipeline = ctx.pipeline();
    boolean success = false;
    try {
        // 抽象接口
        initChannel((C) ctx.channel());
        pipeline.remove(this);
        ctx.fireChannelRegistered();
        success = true;
    } catch (Throwable t) {
        logger.warn("Failed to initialize a channel. Closing: " + ctx.channel(), t);
    } finally {
        if (pipeline.context(this) != null) {
            pipeline.remove(this);
        }
        if (!success) {
            ctx.close();
        }
    }
}

initChannel为抽象接口,用户可以在此方法中设置ChannelHandler。

.childHandler(
    new ChannelInitializer<SocketChannel>() {
        @Override
        public void initChannel(SocketChannel ch) throws IOException {
            ch.pipeline().addLast(new NettyMessageDecoder(1024 * 1024, 4, 4));
            ch.pipeline().addLast(new NettyMessageEncoder());
            // 实现超时主动关闭李娜路
            ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));
            ch.pipeline().addLast(new LoginAuthRespHandler());
            ch.pipeline().addLast("HeartBeatHandler", new HeartBeatRespHandler());
        }
    });
客户端连接操作
ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port), new InetSocketAddress(NettyConstant.LOCALIP, NettyConstant.LOCAL_PORT)).sync();

首先要创建和初始化NioSocketChannel:

private ChannelFuture doConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
    // 创建和初始化NioSocketChannel
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.cause() != null) {
        return regFuture;
    }
    final ChannelPromise promise = channel.newPromise();
    if (regFuture.isDone()) {
        doConnect0(regFuture, channel, remoteAddress, localAddress, promise);
    } else {
        regFuture.addListener(new ChannelFutureListener() {
            // 链路创建成功后发起异步TCP链接
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                doConnect0(regFuture, channel, remoteAddress, localAddress, promise);
            }
        });
    }
    return promise;
}

initAndRegister方法通过工厂方法创建NioSocketChannel。init后进行regist(参考服务器端创建部分)。

链路创建成功后发起异步TCP链接:

private static void doConnect0(
        final ChannelFuture regFuture, final Channel channel,
        final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
    // This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up
    // the pipeline in its channelRegistered() implementation.
    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (regFuture.isSuccess()) {
                if (localAddress == null) {
                    channel.connect(remoteAddress, promise);
                } else {
                    channel.connect(remoteAddress, localAddress, promise);
                }
                promise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

从doConnect0操作开始,连接操作切换到了Netty的NIO线程NioEveotLoop中进行,此时客户端返回,连接操作异步执行。

doConnect0最终调用HeadHandler的connect方法:

@Override
public void connect(
        ChannelHandlerContext ctx,
        SocketAddress remoteAddress, SocketAddress localAddress,
        ChannelPromise promise) throws Exception {
    unsafe.connect(remoteAddress, localAddress, promise);
}
if (doConnect(remoteAddress, localAddress)) {
    fulfillConnectPromise(promise, wasActive);
} else {
    // ...
}
@Override
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
    if (localAddress != null) {
        javaChannel().socket().bind(localAddress);
    }
    boolean success = false;
    try {
        boolean connected = javaChannel().connect(remoteAddress);
        if (!connected) {
            // 如果没有立即连上服务器,则注册为SelectionKey.OP_CONNECT
            selectionKey().interestOps(SelectionKey.OP_CONNECT);
        }
        success = true;
        return connected;
    } finally {
        if (!success) {
            doClose();
        }
    }
}

SocketChannel执行connect()操作后有以下三种结果:

  • 连接成功,返回True;
  • 暂时没有连接上,服务端没有返回ACK应答,连接结果不确定,返回False;
  • 连接失败,直接抛出I/O异常。

如果是第二种结果,需要将NioSocketChannel中的selectionKey设置为OPCONNECT,监听连接结果。

异步连接返回之后,需要判断连接结果,如果连接成功,则触发ChannelActive事件:

private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
    // trySuccess() will return false if a user cancelled the connection attempt.
    boolean promiseSet = promise.trySuccess();
    // Regardless if the connection attempt was cancelled, channelActive() event should be triggered,
    // because what happened is what happened.
    if (!wasActive && isActive()) {
        pipeline().fireChannelActive();
    }
    // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
    if (!promiseSet) {
        close(voidPromise());
    }
}

fireChannelActive最终会将NioSocketChannel中的selectKey设置为SelectionKey.OP_READ,用于监听网络读操作。

doConnect如果没有立即连上服务器,则注册为SelectionKey.OP_CONNECT;如果发生异常则doClose。

异步连接结果通知

NioEventLoop的Selector轮询客户端连接Channel,当服务端返回握手应答之后,对连接结果进行判断。

// NioEventLoop > run > processSelectedKeysOptimized > processSelectedKeyprocessSelectedKey
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
    // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
    // See https://github.com/netty/netty/issues/924
    int ops = k.interestOps();
    ops &= ~SelectionKey.OP_CONNECT;
    k.interestOps(ops);
    unsafe.finishConnect();
}

doFinishConnect用于判断JDK的SocketChannel的连接结果,如果返回true表示连接成功,其他值或者发生异常表示连接失败。

// AbstractNioUnsafe > finishConnect > doFinishConnect > doFinishConnect
@Override
protected void doFinishConnect() throws Exception {
    // jdk关闭连接
    if (!javaChannel().finishConnect()) {
        throw new Error();
    }
}

连接成功之后,调用fulfillConnectPromise方法,触发链路激活事件,该事件由ChannelPipeline进行传播:

// AbstractNioUnsafe > finishConnect > doFinishConnect > doFinishConnect > fulfillConnectPromise
private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
    // trySuccess() will return false if a user cancelled the connection attempt.
    boolean promiseSet = promise.trySuccess();
    // Regardless if the connection attempt was cancelled, channelActive() event should be triggered,
    // because what happened is what happened.
    if (!wasActive && isActive()) {
        // 传播active事件
        pipeline().fireChannelActive();
    }
    // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
    if (!promiseSet) {
        close(voidPromise());
    }
}

fireChannelActive主要修改网络监听位为读操作。

客户端连接超时机制

对于SocketChannel接口,JDK并没有提供连接超时机制,需要NIO框架或者用户自已扩展实现。Netty利用定时器提供了客户端连接超时控制功能。

首先,用户在创建Netty客户端的时候,可以通过ChannelOption.CONNECT_TIMEOUT_MILLIS配置项设置连接超时时间。

bootstrap
        .group(group)
        .channel(NioSocketChannel.class)
        .option(ChannelOption.TCP_NODELAY, true)
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)

如果没有直接连接成功,发起连接的同时,启动连接超时检测定时器:

// AbstractNioUnsafe > connect
if (doConnect(remoteAddress, localAddress)) {
    fulfillConnectPromise(promise, wasActive);
} else {
    connectPromise = promise;
    requestedRemoteAddress = remoteAddress;
    // Schedule connect timeout.
    int connectTimeoutMillis = config().getConnectTimeoutMillis();
    if (connectTimeoutMillis > 0) {
        // 添加超时处理
        connectTimeoutFuture = eventLoop().schedule(new Runnable() {
            @Override
            public void run() {
                ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
                ConnectTimeoutException cause =
                        new ConnectTimeoutException("connection timed out: " + remoteAddress);
                if (connectPromise != null && connectPromise.tryFailure(cause)) {
                    close(voidPromise());
                }
            }
        }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
    }
    promise.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            if (future.isCancelled()) {
                if (connectTimeoutFuture != null) {
                    connectTimeoutFuture.cancel(false);
                }
                connectPromise = null;
                close(voidPromise());
            }
        }
    });
}

一旦超时定时器执行,说明客户端连接超时,构造连接超时异常,将异常结果设置到connectPromise中,同时关闭客户端连接,释放句柄。

如果在连接超时之前获取到连接结果,则删除连接超时定时器,防止其被触发。

// AbstractNioUnsafe > finishConnect
@Override
public void finishConnect() {
    // Note this method is invoked by the event loop only if the connection attempt was
    // neither cancelled nor timed out.
    assert eventLoop().inEventLoop();
    assert connectPromise != null;
    try {
        boolean wasActive = isActive();
        doFinishConnect();
        fulfillConnectPromise(connectPromise, wasActive);
    } catch (Throwable t) {
        if (t instanceof ConnectException) {
            Throwable newT = new ConnectException(t.getMessage() + ": " + requestedRemoteAddress);
            newT.setStackTrace(t.getStackTrace());
            t = newT;
        }
        // Use tryFailure() instead of setFailure() to avoid the race against cancel().
        connectPromise.tryFailure(t);
        closeIfClosed();
    } finally {
        // Check for null as the connectTimeoutFuture is only created if a connectTimeoutMillis > 0 is used
        // See https://github.com/netty/netty/issues/1770
        if (connectTimeoutFuture != null) {
            // 取消超时定时任务
            connectTimeoutFuture.cancel(false);
        }
        connectPromise = null;
    }
}

无论连接是否成功,只要获取到连接结果,之后就删除连接超时定时器。

ByteBuf和相关辅助类

当我们进行数据传输的时候,往往需要使用到缓冲区,常用的缓冲区就是JDK NIO类库提供的java.nio.Buffer。

java.nio.Buffer继承关系图

7种基础类型(Boolean除外)都有自己的缓冲区实现。

对于NIO编程而言,我们主要使用的是ByteBuffer。从功能角度而言,ByteBuffer完全可以满足NIO编程的需要,但是由于NIO编程的复杂性,ByteBuffer也有其局限性:

  • ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发生索引越界异常;
  • ByteBuffer只有一个标识位控的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败;
  • ByteBuffer的API功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。

为了弥补这些不足,Netty提供了自己的ByteBuffer实现:ByteBuf。

ByteBuf的工作原理

ByteBuf与Jdk的ByteByffer比较

首先,ByteBuf依然是个Byte数组的缓冲区,它的基本功能应该与JDK的ByteBuffer一致,提供以下儿类基本功能:

  • 7种Java基础类型、byte数组、ByteBuffer(ByteBuf)等的读写:
  • 缓冲区自身的copy和slice等
  • 设置网络字节序;
  • 构造缓冲区实例;
  • 操作位置指针等方法。

由于JDK的ByteBuffer已经提供了这些基础能力的实现可以有两种策略。

  • 参考JDK ByteBuffer的实现,增加额外的功能,解决原ByteBuffer的缺点;
  • 聚合JDK ByteBuffer,通过Facade模式对其进行包装,可以减少自身的代码量,降低实现成本。

JDK ByteBuffer由于只有一个位置指针用于处理读写操作,因此每次读写的时候都需要额外调用flip()和clear()等方法,否则功能将出错。

ByteBuffer byteBuffer = ByteBuffer.allocate(88);
String value = "netty";
byteBuffer.put(value.getBytes());
byteBuffer.flip();
byte[] valueArray = new byte[byteBuffer.remaining()];
byteBuffer.get(valueArray);
String decodeVaule = new String(valueArray);

flip()操作之前:

flip()操作之前

如果不做flip操作,读取到的将是position到capacity之间的错误内容。

flip()操作之后:

flip()操作之后

当执行flip()操作之后,它的limit被设置为position,position设置为0,capacity不变。 由于读取的内容是从position到limit之间,因此,它能够正确地读取到之前写入缓冲区的内容。

BytcBuf通过两个位置指针来协助缓冲区的读写操作,读操作使用readerIndex,写操作使用writerIndex。

  • readerIndex和writerIndex的取值一开始都是0,随着数据的写入writerIndex会增加,读取数据会使readerIndex增加,但是它不会超过writerIndex。
  • 在读取之后,0~readerIndex就被视为discard的,调用discardReadBytes方法,可以释放这部分空间,它的作用类似ByteBuffer的compact方法。
  • readerIndex和writerIndex之间的数据是可读取的,等价于ByteBuffer的position和limit之间的数据。
  • writerIndex和capacity之间的空间是可写的,等价于ByteBuffer的limit和capacity之间的可用空间。

由于写操作不修改readerIndex指针,读操作不修改writerlndex指针,因此读写之间不再需要调整位置指针,这极大地简化了缓冲区的读写操作,避免了由于遗漏或者不熟悉flip()操作导致的功能异常。

初始分配的ByteBuf:

初始分配的ByteBuf

写入N个字节之后的ByteBuf:

写入N个字节之后的ByteBuf

读取M(<N)个字节之后的ByteBuf:

读取M(<N)个字节之后的ByteBuf

调用discardReadBytes操作之后的ByteBuf:

调用discardReadBytes操作之后的ByteBuf

调用clear操作之后的ByteBuf:

调用clear操作之后的ByteBuf

ByteBuf是如何实现动态扩展

通常情况下,当我们对ByteBuffer进行put操作的时候,如果缓冲区剩余可写空间不够,就会发生BufferOverflowException异常。

为了避免发生这个问题,通常在进行put操作的时候会对剩余可用空间进行校验,如果剩余空间不足,需要重新创建一个新的ByteBuffer, 并将之前的ByteBuffer复制到新创建的ByteBuffer中,最后释放老的ByteBuffer。

if(this.buffer.remaining() < needSize) {
    int toBeExtSize = needSize < 128 ? needSize : 128;
    ByteBuffer tmpBuffer = ByteBuffer.allocate(this.buffer.capacity() + toBeExtSize);
    this.buffer.flip();
    tmpBuffer.put(this.buffer);
    this.buffer = tmpBuffer;
}

为了解决这个问题,ByteBuf对write操作进行了封装,由ByteBuf的write操作负责进行剩余可用空间的校验,如果可用缓冲区不足,ByteBuf会自动进行动态扩展, 对于使用者而言,不需要关心底层的校验和扩展细节,只要不超过设置的最大缓冲区容量即可。

@Override
public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
    ensureWritable(length);
    setBytes(writerIndex, src, srcIndex, length);
    writerIndex += length;
    return this;
}

write操作时会对需要write的字节进行校验,如果可写的字节数小于需要写入的字节数,并且需要写入的字节数小于可写的最大字节数时,对缓冲区进行动态扩展。

@Override
public ByteBuf ensureWritable(int minWritableBytes) {
    if (minWritableBytes < 0) {
        throw new IllegalArgumentException(String.format(
                "minWritableBytes: %d (expected: >= 0)", minWritableBytes));
    }
    if (minWritableBytes <= writableBytes()) {
        return this;
    }
    if (minWritableBytes > maxCapacity - writerIndex) {
        throw new IndexOutOfBoundsException(String.format(
                "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                writerIndex, minWritableBytes, maxCapacity, this));
    }
    // Normalize the current capacity to the power of 2.
    int newCapacity = calculateNewCapacity(writerIndex + minWritableBytes);
    // Adjust to the new capacity.
    capacity(newCapacity);
    return this;
}

由于NIO的Channel读写的参数都是ByteBuffer,因此,Netty的ByteBuf接口必须提供API方便的将ByteBuf转换成ByteBuffer,或者将ByteBuffer包装成ByteBuf。 考虑到性能,应该尽量避免缓冲区的复制,内部实现的时候可以考虑聚合一个ByteBuffer的私有指针用来代表ByteBuffer。

ByteBuf的功能介绍

顺序读操作(read)
方法名称 返回值 功能说明 异常
readBoolean() boolean 从readerIndex开始读取1字节的数据 readableBytes<1
readByte() byte 从readerIndex开始读取1字节的数据 readableBytes<1
readUnsignedByte() short 从readerIndex开始读取1字节的数据(无符号字节值) readableBytes<1
readShort() short 从readerIndex开始读取16位的短整形值 readableBytes<2
readUnsignedShort() int 从readerIndex开始读取16位的无符号短整形值 readableBytes<2
readMedium() int 从readerIndex开始读取24位的整形值,(该类型并非java基本类型,通常不用) readableBytes<3
readUnsignedMedium() int 从readerIndex开始读取24位的无符号整形值,(该类型并非java基本类型,通常不用) readableBytes<3
readInt() int 从readerIndex开始读取32位的整形值 readableBytes<4
readUnsignedInt() long 从readerIndex开始读取32位的无符号整形值 readableBytes<4
readLong() long 从readerIndex开始读取64位的整形值 readableBytes<8
readChar() char 从readerIndex开始读取2字节的字符值 readableBytes<2
readFloat() float 从readerIndex开始读取32位的浮点值 readableBytes<4
readDouble() double 从readerIndex开始读取64位的浮点值 readableBytes<8
readBytes(int length) ByteBuf 将当前ByteBuf中的数据读取到新创建的ByteBuf中,从readerIndex开始读取length字节的数据。返回的ByteBuf readerIndex 为0,writeIndex为length。 readableBytes<length
readSlice(int length) ByteBuf 返回当前ByteBuf新创建的子区域,子区域和原ByteBuf共享缓冲区的内容,但独立维护自己的readerIndex和writeIndex,新创建的子区域readerIndex为0,writeIndex为length。 readableBytes<length
readBytes(ByteBuf dst) ByteBuf 将当前ByteBuf中的数据读取到目标ByteBuf (dst)中,从当前ByteBuf readerIndex开始读取,直到目标ByteBuf无可写空间,从目标ByteBuf writeIndex开始写入数据。读取完成后,当前ByteBuf的readerIndex+=读取的字节数。目标ByteBuf的writeIndex+=读取的字节数。 readableBytes<dst.writableBytes
readBytes(ByteBuf dst, int length) ByteBuf 将当前ByteBuf中的数据读取到目标ByteBuf (dst)中,从当前ByteBuf readerIndex开始读取,长度为length,从目标ByteBuf writeIndex开始写入数据。读取完成后,当前ByteBuf的readerIndex+=length,目标ByteBuf的writeIndex+=length this.readableBytes<length or dst.writableBytes<length
readBytes(ByteBuf dst, int dstIndex, int length) ByteBuf 将当前ByteBuf中的数据读取到目标ByteBuf (dst)中,从readerIndex开始读取,长度为length,从目标ByteBuf dstIndex开始写入数据。读取完成后,当前ByteBuf的readerIndex+=length,目标ByteBuf的writeIndex+=length dstIndex<0 or this.readableBytes<length or dst.capacity<dstIndex + length
readBytes(byte[] dst) ByteBuf 将当前ByteBuf中的数据读取到byte数组dst中,从当前ByteBuf readerIndex开始读取,读取长度为dst.length,从byte数组dst索引0处开始写入数据。 this.readableBytes<dst.length
readBytes(byte[] dst, int dstIndex, int length) ByteBuf 将当前ByteBuf中的数据读取到byte数组dst中,从当前ByteBuf readerIndex开始读取,读取长度为length,从byte数组dst索引dstIndex处开始写入数据。 dstIndex<0 or this.readableBytes<length or dst.length<dstIndex + length
readBytes(ByteBuffer dst) ByteBuf 将当前ByteBuf中的数据读取到ByteBuffer dst中,从当前ByteBuf readerIndex开始读取,直到dst的位置指针到达ByteBuffer 的limit。读取完成后,当前ByteBuf的readerIndex+=dst.remaining() this.readableBytes<dst.remaining()
readBytes(OutputStream out, int length) ByteBuf 将当前ByteBuf readerIndex读取数据到输出流OutputStream中,读取的字节长度为length this.readableBytes<length
readBytes(GatheringByteChannel out, int length) int 将当前ByteBuf readerIndex读取数到GatheringByteChannel 中,写入out的最大字节长度为length。GatheringByteChannel为非阻塞Channel,调用其write方法不能够保存将全部需要写入的数据均写入成功,存在半包问题。因此其写入的数据长度为【0,length】,如果操作成功,readerIndex+=实际写入的字节数,返回实际写入的字节数 this.readableBytes<length
顺序写操作(write)
方法名称 返回值 功能说明 异常
writeBoolean(boolean value) ByteBuf 将value写入到当前ByteBuf中。写入成功,writeIndex+=1 this.writableBytes<1
writeByte(int value) ByteBuf 将value写入到当前ByteBuf中。写入成功,writeIndex+=1 this.writableBytes<1
writeShort(int value) ByteBuf 将value写入到当前ByteBuf中。写入成功,writeIndex+=2 this.writableBytes<2
writeMedium(int   value) ByteBuf 将value写入到当前ByteBuf中。写入成功,writeIndex+=3 this.writableBytes<3
writeInt(int   value) ByteBuf 将value写入到当前ByteBuf中。写入成功,writeIndex+=4 this.writableBytes<4
writeLong(long  value) ByteBuf 将value写入到当前ByteBuf中。写入成功,writeIndex+=8 this.writableBytes<8
writeChar(int value) ByteBuf 将value写入到当前ByteBuf中。写入成功,writeIndex+=2 this.writableBytes<2
writeFloat(float value) ByteBuf 将value写入到当前ByteBuf中。写入成功,writeIndex+=4 this.writableBytes<4
writeDouble(double value) ByteBuf 将value写入到当前ByteBuf中。写入成功,writeIndex+=8 this.writableBytes<8
writeBytes(ByteBuf src) ByteBuf 将源ByteBuf src中从readerIndex开始的所有可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=src.readableBytes this.writableBytes<src.readableBytes
writeBytes(ByteBuf src, int length) ByteBuf 将源ByteBuf src中从readerIndex开始,长度length的可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=length this.writableBytes<length or src.readableBytes<length
writeBytes(ByteBuf src, int srcIndex, int length) ByteBuf 将源ByteBuf src中从srcIndex开始,长度length的可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=length srcIndex<0 or this.writableBytes<length or src.capacity<srcIndex + length
writeBytes(byte[] src) ByteBuf 将源字节数组src中所有可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=src.length this.writableBytes<src.length
writeBytes(byte[] src, int srcIndex, int length) ByteBuf 将源字节数组src中srcIndex开始,长度为length可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=length srcIndex<0 or this.writableBytes<src.length or src.length<srcIndex + length
writeBytes(ByteBuffer mignsrc) ByteBuf 将源ByteBuffer src中所有可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=src.remaining() this.writableBytes<src.remaining()
writeBytes(InputStream in, int length) int 将源InputStream in中的内容写入到当前ByteBuf,写入的最大长度为length,实际写入的字节数可能少于length。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=实际写入的字节数。返回实际写入的字节数 this.writableBytes<length
writeBytes(ScatteringByteChannel in, int length) int 将源ScatteringByteChannel in中的内容写入到当前ByteBuf,写入的最大长度为length,实际写入的字节数可能少于length。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=实际写入的字节数。返回实际写入的字节数 this.writableBytes<length
writeZero(int length) ByteBuf 将当前缓冲区的内容填充为NUL(0x00),当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=length this.writableBytes<length
readerIndex和writerIndex

Netty提供了两个指针变量用于支持顺序读取和写入操作:readerIndex用于标识读取索引,writerlndex用于标识写入索引。两个位置指针将ByteBuf缓冲区分割成三个区域:

+-------------------+------------------+------------------+
| discardable bytes |  readable bytes  |  writable bytes  |
|                   |     (CONTENT)    |                  |
+-------------------+------------------+------------------+
|                   |                  |                  |
0      <=      readerIndex   <=   writerIndex    <=    capacity

调用ByteBuf的read操作时,从readerIndex处开始读取。readerIndex到writerIndex之间的空间为可读的字节缓冲区;从writerfodex到capacity之间为可写的字节缓冲区; 0到reader]ndex之间是已经读取过的缓冲区,可以调用discardReadBytes操作来重用这部分空间,以节约内存,防止ByteBuf的动态扩张。

这在私有协议栈消息解码的时候非常有用,因为TCP底层可能粘包,几百个整包消息被TCP粘包后作为一个整包发送。 这样,通过discardReadBytes操作可以重用之前已经解码过的缓冲区,从而防止接收缓冲区因为容量不足导致的扩张。 但是,discardReadBytes操作是把双刃剑,不能滥用,因为这个操作涉及字节数组的内存复制,导致性能下降。

Discardable bytes

由于缓冲区的动态扩张需要进行字节数组的复制,它是个耗时的操作,因此,为了最大程度地提升性能,往往你要尽最大努力提升缓冲区的重用率。

discardReadBytes操作后的ByteBuf:

+------------------+-----------------------------------+
|  readable bytes  |  writable bytes (got more space)  |
+------------------+-----------------------------------+
|                  |                                   |
readerIndex(0) <= writerIndex(decreased)     <=    capacity

调用discardReadBytes会发生字节数组的内存复制,所以,频繁调用将会导致性能下降,因此在调用它之前要确认你确实需要这样做,例如牺牲性能来换取更多的可用内存。

调用discardReadBytes操作之后的writablebytes内容处理策略跟ByteBuf接口的具体实现有关。

@Override
public ByteBuf discardReadBytes() {
    ensureAccessible();
    if (readerIndex == 0) {
        return this;
    }
    if (readerIndex != writerIndex) {
        // 数组内存复制
        setBytes(0, this, readerIndex, writerIndex - readerIndex);
        writerIndex -= readerIndex;
        adjustMarkers(readerIndex);
        readerIndex = 0;
    } else {
        adjustMarkers(readerIndex);
        writerIndex = readerIndex = 0;
    }
    return this;
}
Readable bytes和Writable bytes
  • 可读空间段是数据实际存储的区域,以read或者skip开头的任何操作都将会从readerlndex开始读取或者跳过指定的数据,操作完成之后readerIndex增加了读取或者跳过的字节数长度。
    • 当新分配、包装或者复制一个新的ByteBuf对象时,它的readerlndex为0。
  • 可写空间段是尚未被使用可以填充的空闲空间,任何以write开头的操作都会从writerlndex开始向空闲空间写入字节,操作完成之后writerlndex增加了写入的字节数长度。
    • 新分配一个ByteBuf对象时,它的readerIndex为0。通过包装或者复制的方式创建一个新的ByteBuf对象时,它的writerlndex是ByteBuf的容量。
Clear操作

正如JDK ByteBuffer的clear操作,它并不会清空缓冲区内容本身,例如填充为NUL(OxOO)。它主要用来操作位置指针,例如position、limit和mark。 对于ByteBuf,它也是用来操作readerlndex和writerlndex,将它们还原为初始分配值。

clear操作后的缓冲区:

+------------------------------------------------------+
|          writable bytes (got more space)             |
+------------------------------------------------------+
|                                                      |
0 = readerIndex= writerIndex(decreased)     <=    capacity
Mark和Rest

当对缓冲区进行读写操作时,可能需要对之前的操作进行回滚。读操作并不会改变缓冲区的内容,回滚操作主要就是重新设置索引信息。 ByteBuf可通过调用mark操作将当前的位置指针备份到mark变量中,调用rest操作后,重新将指针的当前位置恢复为备份在mark变量的值。

对于JDK的ByteBuffer,调用mark操作会将当前的位置指针备份到mark变屈中,当调用rest操作之后,重新将指针的当前位置恢复为备份在mark中的值。

public final Buffer mark() {
    mark = position;
    return this;
}
public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

Netty的ByteBuf也有类似的rest和mark接口:

  • markReaderIndex:将当前的readerIndex备份到markedReaderindex中;
  • resetReaderIndex:将当前的readerIndex设置为markedReaderIndex:
  • markWriterIndex:将当前的writerIndex备份到markedWriterIndex;
  • resetWriterIndex:将当前的writerIndex设置为markedWriterIndex。
查找操作

很多时候,需要从ByteBuf中查找某个字符,例如通过”\r\n”作为文本字符串的换行符,利用”NUL(0x00)”作为分隔符。

ByteBuf提供了多种查找方法用于满足不同的应用场景:

方法名称 返回值 功能说明
indexOf(int fromIndex, int toIndex, byte value) int 从当前ByteBuf中查找首次出现value的位置,fromIndex<=查找范围<toIndex;查找成功返回位置索引,否则返回-1
bytesBefore(byte value) int 从当前ByteBuf中查找首次出现value的位置,readerIndex<=查找范围<writerIndex;查找成功返回位置索引,否则返回-1
bytesBefore(int length, byte value) int 从当前ByteBuf中查找首次出现value的位置,readerIndex<=查找范围<readerIndex+length;查找成功返回位置索引,否则返回-1
bytesBefore(int index, int length, byte value) int 从当前ByteBuf中查找首次出现value的位置,index<=查找范围<index+length;查找成功返回位置索引,否则返回-1
forEachByte(ByteBufProcessor processor); int 遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从readerIndex开始遍历直到writerIndex。如果满足条件,返回位置索引,否则返回-1
forEachByte(int index, int length, ByteBufProcessor processor)   遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从index开始遍历直到index+length。如果满足条件,返回位置索引,否则返回-1
forEachByteDesc(ByteBufProcessor processor)   逆序遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从writerIndex-1开始遍历直到readerIndex。如果满足条件,返回位置索引,否则返回-1
forEachByteDesc(int index, int length, ByteBufProcessor processor)   逆序遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从index+length-1开始遍历直到index。如果满足条件,返回位置索引,否则返回-1

在ByteBufProcessor接口中对常用的查找字节进行了抽象:

  • FIND_NUL:NUL(0x00)
  • FIND_CR:CR(‘\r’)
  • FIND_LF:LF(‘\n’)
  • FIND_CRLF:CR(‘\r’)或者CR(‘\n’)
  • FIND_LINEAR_WHITESPACE:’ ‘或者\t
Derived buffers

Derived Buffers类似于数据库视图,ByteBuf提供了多个接口用于创建某个ByteBuf的视图或者复制ByteBuf。

方法名称 返回值 功能说明
duplicate() ByteBuf 返回当前ByteBuf的复制对象,复制后的ByteBuf对象与当前ByteBuf对象共享缓冲区的内容,但是维护自己独立的readerIndex和writerIndex。该操作不修改原ByteBuf的readerIndex和writerIndex。
copy() ByteBuf 从当前ByteBuf复制一个新的ByteBuf对象,复制的新对象缓冲区的内容和索引均是独立的。该操作不修改原ByteBuf的readerIndex和writerIndex。(复制readerIndex到writerIndex之间的内容,其他属性与原ByteBuf相同,如maxCapacity,ByteBufAllocator)
copy(int index, int length) ByteBuf 从当前ByteBuf 指定索引index开始,字节长度为length,复制一个新的ByteBuf对象,复制的新对象缓冲区的内容和索引均是独立的。该操作不修改原ByteBuf的readerIndex和writerIndex。(其他属性与原ByteBuf相同,,如maxCapacity,ByteBufAllocator)
slice() ByteBuf 返回当前ByteBuf的可读子区域,起始位置从readerIndex到writerIndex,返回的ByteBuf对象与当前ByteBuf对象共享缓冲区的内容,但是维护自己独立的readerIndex和writerIndex。该操作不修改原ByteBuf的readerIndex和writerIndex。返回ByteBuf对象的长度为readableBytes()
slice(int index, int length) ByteBuf 返回当前ByteBuf的可读子区域,起始位置从index到index+length,返回的ByteBuf对象与当前ByteBuf对象共享缓冲区的内容,但是维护自己独立的readerIndex和writerIndex。该操作不修改原ByteBuf的readerIndex和writerIndex。返回ByteBuf对象的长度为length
转换成标准的ByteBuffer

当通过NIO的SocketChannel进行网络读写时,操作的对象为JDK的ByteBuffer,因此须在接口层支持netty ByteBuf到JDK的ByteBuffer的相互转换。

方法名称 返回值 功能说明 抛出异常
nioBuffer() ByteBuffer 将当前ByteBuf的可读缓冲区(readerIndex到writerIndex之间的内容)转换为ByteBuffer,两者共享共享缓冲区的内容。对ByteBuffer的读写操作不会影响ByteBuf的读写索引。注意:ByteBuffer无法感知ByteBuf的动态扩展操作。ByteBuffer的长度为readableBytes() UnsupportedOperationException
nioBuffer(int index, int length) ByteBuffer 将当前ByteBuf的可读缓冲区(index到index+length之间的内容)转换为ByteBuffer,两者共享共享缓冲区的内容。对ByteBuffer的读写操作不会影响ByteBuf的读写索引。注意:ByteBuffer无法感知ByteBuf的动态扩展操作。ByteBuffer的长度为length UnsupportedOperationException
随机读写(set和get)

除了顺序读写之外,ByteBuf还支持随机读写,它与顺序读写的最大差别在在于可以随机指定读写的索引位置。

读取操作的API列表

随机写操作的API列表

ByteBuf源码分析

ByteBuf的主要类继承关系

ByteBuf主要功能类继承关系图

ByteBuf主要功能类继承关系图

从内存分配的角度看,ByteBuf可以分为两类。

  • 堆内存(HeapByteBuf)字节缓冲区:
    • 内存的分配和回收速度快,可以被JVM自动回收;
    • 缺点就是如果进行Socket的I/O读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下降。
  • 直接内存(DirectByteBuf)字节缓冲区:
    • 将它写入或者从Socket Channel中读取时,由于少了一次内存复制,速度比堆内存快。
    • 非堆内存,它在堆外进行内存分配,相比于堆内存,它的分配和回收速度会慢一些。

Netty提供了多种ByteBuf供开发者使用,经验表明,ByteBuf的最佳实践是在I/O通信线程的读写缓冲区使用DirectByteBuf,后端业务消息的编解码模块使用HeapByteBuf,这样组合可以达到性能最优。

从内存回收角度看,ByteBuf也分为两类:基于对象池的ByteBuf和普通ByteBuf。

两者的主要区别就是基于对象池的ByteBuf可以重用ByteBuf对象,它自己维护了一个内存池,可以循环利用创建的ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁GC。

测试表明使用内存池后的Netty在高负载、大并发的冲击下内存和GC更加平稳。

尽管推荐使用基于内存池的ByteBuf,但是内存池的管理和维护更加复杂,使用起来也需要更加谨慎,因此,Netty提供了灵活的策略供使用者来做选择。

AbstractByteBuf源码分析
主要成员变量

首先,像读索引、写索引、mark 、最大容量等公共属性需要定义。

static final ResourceLeakDetector<ByteBuf> leakDetector = new ResourceLeakDetector<ByteBuf>(ByteBuf.class);
int readerIndex;
private int writerIndex;
private int markedReaderIndex;
private int markedWriterIndex;
private int maxCapacity;
private SwappedByteBuf swappedBuf;

leakDetector,它被定义为static,意味着所有的ByteBuf实例共享同一个ResourceLeakDetector对象。ResourceLeakDetector用于检测对象是否泄漏。

因为AbstractByteBuf并不清楚子类到底是基于堆内存还是直接内存,因此无法提前定义ByteBuf。

读操作簇

无论子类如何实现ByteBuf,例如UnpooledHeapByteBuf使用byte数组表示字节缓冲区,UnpooledDirectByteBuf直接使用ByteBuffer,它们的功能都是相同的,操作的结果是等价的。

因此,读操作以及其他的一些公共功能都由父类实现,差异化功能由子类实现,这也就是抽象和继承的价值所在。

读方法一览

@Override
public ByteBuf readBytes(byte[] dst, int dstIndex, int length) {
    // 校验可用空间
    checkReadableBytes(length);
    // 从当前读取的索引开始,复制length个字节到目标byte数组中。
    // 这个方法由子类实现
    getBytes(readerIndex, dst, dstIndex, length);
    // 读取成功对索引递增
    readerIndex += length;
    return this;
}
protected final void checkReadableBytes(int minimumReadableBytes) {
    ensureAccessible();
    // 读取长度小于0,抛出异常
    if (minimumReadableBytes < 0) {
        throw new IllegalArgumentException("minimumReadableBytes: " + minimumReadableBytes + " (expected: >= 0)");
    }
    // 可用字节数小于需要读取的长度,抛出异常
    if (readerIndex > writerIndex - minimumReadableBytes) {
        throw new IndexOutOfBoundsException(String.format(
                "readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s",
                readerIndex, minimumReadableBytes, writerIndex, this));
    }
}
写操作簇

写操作的公共行为在AbstractByteBuf中实现。

写操作一览

@Override
public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
    // 校验写入字节数组的长度是否合法
    ensureWritable(length);
    setBytes(writerIndex, src, srcIndex, length);
    writerIndex += length;
    return this;
}
@Override
public ByteBuf ensureWritable(int minWritableBytes) {
    // 校验写入长度
    if (minWritableBytes < 0) {
        throw new IllegalArgumentException(String.format(
                "minWritableBytes: %d (expected: >= 0)", minWritableBytes));
    }
    // 校验写入空间足够则返回
    if (minWritableBytes <= writableBytes()) {
        return this;
    }
    // 写入长度大于可以动态扩展的最大可以字节,抛出异常
    if (minWritableBytes > maxCapacity - writerIndex) {
        throw new IndexOutOfBoundsException(String.format(
                "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                writerIndex, minWritableBytes, maxCapacity, this));
    }
    // 计算动态扩展容量
    // Normalize the current capacity to the power of 2.
    int newCapacity = calculateNewCapacity(writerIndex + minWritableBytes);
    // 扩展复制内存
    // Adjust to the new capacity.
    capacity(newCapacity);
    return this;
}
private int calculateNewCapacity(int minNewCapacity) {
    final int maxCapacity = this.maxCapacity;
    final int threshold = 1048576 * 4; // 4 MiB page
    // 相等4M直接返回4M
    if (minNewCapacity == threshold) {
        return threshold;
    }
    // If over threshold, do not double but just increase by threshold.
    if (minNewCapacity > threshold) {
        int newCapacity = minNewCapacity / threshold * threshold;
        if (newCapacity > maxCapacity - threshold) {
            newCapacity = maxCapacity;
        } else {
            newCapacity += threshold;
        }
        return newCapacity;
    }
    // 低于4M,成倍扩张
    // Not over threshold. Double up to 4 MiB, starting from 64.
    int newCapacity = 64;
    while (newCapacity < minNewCapacity) {
        newCapacity <<= 1;
    }

    return Math.min(newCapacity, maxCapacity);
}

首先需要重新计算下扩展后的容量,它有一个参数,等于writerlndex + minWritableBytes,也就是满足要求的最小容量。
首先设置门限阈值为4M,当需要的新容量正好等于门限阈值,则使用阈值作为新的缓冲区容量。
如果新申请的内存空间大于阈值,不能采用倍增的方式(防止内存膨胀和浪费)扩张内存,采用每次步进4M的方式进行内存扩张。
扩张的时候需要对扩张后的内存和最大内存(maxCapacity)进行比较,如果大于缓冲区的最大长度,则使用maxCapacity作为扩容后的缓冲区容量。
如果扩容后的新容量小于阈值,则以64为计数进行倍增,直到倍增后的结果大于或等于需要的容量值。

采用倍增或者步进算法的原因

如果以minNewCapacity作为目标容量,则本次扩容后的可写字节数刚好够本次写入使用。写入完成后,它的可写字节数会变为0,下次做写入操作的时候,需要再次动态扩张。 这样就会形成第一次动态扩张后,每次写入操作都会进行动态扩张,由于动态扩张需要进行内存复制,频繁的内存复制会导致性能下降。

采用先倍增后步进的原因

当内存比较小的情况下,倍增操作并不会带来太多的内存浪费,例如64字节–>128字节–>256字节,这样的内存扩张方式对于大多数应用系统是可以接受的。 但是,当内存增长到一定阙值后,再进行倍增就可能会带来额外的内存浪费,例如1OM,采用倍增后变为20M,很有可能系统只需要12M,扩张到20M后会带来8M的内存浪费。 由于每个客户端连接都可能维护自己独立的接收和发送缓冲区,这样随着客户读的线性增长,内存浪费也会成比例的增加,因此,达到某个阐值后就需要以步进的方式对内存进行平滑地扩张。

重新计算完动态扩张后的目标容量后,需要重新创建个新的缓冲区,将原缓冲区的内容复制到新创建的ByteBuf中,最后设置读写索引和mark标签等。 由于不同的子类会对应不同的复制操作,所以该方法依然是个抽象方法,由子类负责实现。

操作索引

索引操作API

索引操作API

@Override
public ByteBuf readerIndex(int readerIndex) {
    // 如果索引小于0或者大于写索引就抛异常
    if (readerIndex < 0 || readerIndex > writerIndex) {
        throw new IndexOutOfBoundsException(String.format(
                "readerIndex: %d (expected: 0 <= readerIndex <= writerIndex(%d))", readerIndex, writerIndex));
    }
    this.readerIndex = readerIndex;
    return this;
}
重用缓冲区

通过discardReadBytes和discardSomeReadBytes方法重用已经读取过的缓冲区。

@Override
public ByteBuf discardReadBytes() {
    ensureAccessible();
    // 没有空间
    if (readerIndex == 0) {
        return this;
    }
    // 同时有丢弃空间和未读取内容
    if (readerIndex != writerIndex) {
        // 进行字节数组复制
        setBytes(0, this, readerIndex, writerIndex - readerIndex);
        // 重置索引位置
        writerIndex -= readerIndex;
        adjustMarkers(readerIndex);
        readerIndex = 0;
    } else {
        // 没有未读取内容,不需要内存复制
        adjustMarkers(readerIndex);
        // 都是0
        writerIndex = readerIndex = 0;
    }
    return this;
}
protected final void adjustMarkers(int decrement) {
    int markedReaderIndex = this.markedReaderIndex;
    // 如果小于writerIndex,设置为0
    if (markedReaderIndex <= decrement) {
        this.markedReaderIndex = 0;
        int markedWriterIndex = this.markedWriterIndex;
        // 如果小于readerIndex,设置为0
        if (markedWriterIndex <= decrement) {
            this.markedWriterIndex = 0;
        } else {
            // 减去整理的长度为新的索引位置
            this.markedWriterIndex = markedWriterIndex - decrement;
        }
    } else {
            // 减去整理的长度为新的索引位置
        this.markedReaderIndex = markedReaderIndex - decrement;
        markedWriterIndex -= decrement;
    }
}
skipBytes

在解码的时候,有时候需要丢弃非法的数据报,或者跳跃过不需要读取的字节或字节数组,此时,使用skipBytes方法就非常方便。 它可以忽略指定长度的字节数组,读操作时直接跳过这些数据读取后面的可读缓冲区。

@Override
public ByteBuf skipBytes(int length) {
    // 判断长度是否大于缓冲区可读字节数组长度
    checkReadableBytes(length);
    // 写索引判断
    int newReaderIndex = readerIndex + length;
    if (newReaderIndex > writerIndex) {
        throw new IndexOutOfBoundsException(String.format(
                "length: %d (expected: readerIndex(%d) + length <= writerIndex(%d))",
                length, readerIndex, writerIndex));
    }
    // 设置索引
    readerIndex = newReaderIndex;
    return this;
}
AbstractReferenceCountedByteBuf源码分析

从类的名字就可以看出该类主要是对引用进行计数,类似于JVM内存回收的对象引用计数器,用于跟踪对象的分配和销毁,做自动内存回收。

成员变量
 private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater =
        AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");

private static final long REFCNT_FIELD_OFFSET;

static {
    long refCntFieldOffset = -1;
    try {
        if (PlatformDependent.hasUnsafe()) {
            refCntFieldOffset = PlatformDependent.objectFieldOffset(
                    AbstractReferenceCountedByteBuf.class.getDeclaredField("refCnt"));
        }
    } catch (Throwable t) {
        // Ignored
    }

    REFCNT_FIELD_OFFSET = refCntFieldOffset;
}

@SuppressWarnings("FieldMayBeFinal")
private volatile int refCnt = 1;

refCntUpdater,它是AtomicintegerFieldUpdater类型变量,通过原子的方式对成员变量进行更新等操作,以实现线程安全,消除锁。

REFCNT_FIELD_OFFSET,它用于标识refCnt字段在AbstractReferenceCountedByteBuf中的内存地址,该内存地址的获取是JDK实现强相关的, 如果使用SUN的JDK,它通过sun.misc.Unsafe的objectFieldOffset接口来获得,ByteBuf的实现子类UnpooledUnsafeDirectByteBuf和PooledUnsafeDirectByteBuf会使用到这个偏移量。

最后定义了一个volatile修饰的refCnt字段用于跟踪对象的引用次数,使用volatile是为了解决多线程并发访问的可见性问题。

对象引用计数器

每调用一次retain方法,引用计数器就会加一,由于可能存在多线程并发调用的场景,所以它的累加操作必须是线程安全的:

@Override
public ByteBuf retain() {
    for (;;) {
        int refCnt = this.refCnt;
        // 最小为1,0则抛出异常
        if (refCnt == 0) {
            throw new IllegalReferenceCountException(0, 1);
        }
        // 达到最大值抛出越界异常
        if (refCnt == Integer.MAX_VALUE) {
            throw new IllegalReferenceCountException(Integer.MAX_VALUE, 1);
        }
        // 原子更新
        if (refCntUpdater.compareAndSet(this, refCnt, refCnt + 1)) {
            break;
        }
    }
    return this;
}
@Override
public final boolean release() {
    for (;;) {
        int refCnt = this.refCnt;
        if (refCnt == 0) {
            throw new IllegalReferenceCountException(0, -1);
        }
        if (refCntUpdater.compareAndSet(this, refCnt, refCnt - 1)) {
            // 为1意味着申请和释放相等,说明对象引用不可达,需要释放
            if (refCnt == 1) {
                // 释放对象,子类实现
                deallocate();
                return true;
            }
            return false;
        }
    }
}
UnpooledHeapByteBuf源码分析

UnpooledHeapByteBuf是基于堆内存进行内存分配的字节缓冲区,它没有基于对象池技术实现,这就意味着每次I/O的读写都会创建一个新的UnpooledHeapByteBuf, 频繁进行大块内存的分配和回收对性能会造成一定影响,但是相比于堆外内存的申请和释放,它的成本还是会低一些。

成员变量
// 用于内存分配
private final ByteBufAllocator alloc;
// 缓冲区
private byte[] array;
// 用于实现Netty ByteBuf与JDK ByteBuffer的转换
private ByteBuffer tmpNioBuf;

如果使用JDK的ByteBuffer替换byte数组也是可行的,直接使用byte数组的根本原因就是提升性能和更加便捷地进行位操作。JDK的ByteBuffer底层实现也是byte数组。

动态扩展缓冲区
@Override
public ByteBuf capacity(int newCapacity) {
    ensureAccessible();
    // 校验容量,是否大于上限或者小于0
    if (newCapacity < 0 || newCapacity > maxCapacity()) {
        throw new IllegalArgumentException("newCapacity: " + newCapacity);
    }
    int oldCapacity = array.length;
    // 如果大于当前容量需要扩展
    if (newCapacity > oldCapacity) {
        // 创建新的数组
        byte[] newArray = new byte[newCapacity];
        // 复制
        System.arraycopy(array, 0, newArray, 0, array.length);
        setArray(newArray);
    } else if (newCapacity < oldCapacity) {
        // 如果小于当前的缓冲区容量,不需要扩容
        // 但是需要截取当前缓冲区创建一个新的子缓冲区
        byte[] newArray = new byte[newCapacity];
        int readerIndex = readerIndex();
        // 读索引小于新容量
        if (readerIndex < newCapacity) {
            int writerIndex = writerIndex();
            // 并且写索引大于新容量
            if (writerIndex > newCapacity) {
                // 将写索引设置为容量值,防止越界
                writerIndex(writerIndex = newCapacity);
            }
            // 截取
            System.arraycopy(array, readerIndex, newArray, readerIndex, writerIndex - readerIndex);
        } else {
            // 没有需要复制到新缓冲区的字节
            setIndex(newCapacity, newCapacity);
        }
        setArray(newArray);
    }
    return this;
}
private void setArray(byte[] initialArray) {
    // 替换旧的字节数组
    array = initialArray;
    // 动态扩容完成后,将原来的视图tempNioBuf设置为空
    tmpNioBuf = null;
}
字节数组复制
@Override
public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) {
    // 校验index和length、srcIndex和srcCapacity,如果小于0则抛出异常。如果大于缓冲区容量则抛出异常
    checkSrcIndex(index, length, srcIndex, src.length);
    // 进行数组复制
    System.arraycopy(src, srcIndex, array, index, length);
    return this;
}
protected final void checkSrcIndex(int index, int length, int srcIndex, int srcCapacity) {
    checkIndex(index, length);
    if (srcIndex < 0 || srcIndex > srcCapacity - length) {
        throw new IndexOutOfBoundsException(String.format(
                "srcIndex: %d, length: %d (expected: range(0, %d))", srcIndex, length, srcCapacity));
    }
}
转换成ByteBuffer
@Override
public ByteBuffer nioBuffer(int index, int length) {
    ensureAccessible();
    // 直接调用Jdk原生转换
    // slice方法保证读写索引的独立性
    return ByteBuffer.wrap(array, index, length).slice();
}
// JDK源码
public static ByteBuffer wrap(byte[] array,int offset, int length) {
    try {
        return new HeapByteBuffer(array, offset, length);
    } catch (IllegalArgumentException x) {
        throw new IndexOutOfBoundsException();
    }
}
子类实现相关的方法

ByteBuf中的一些接口是跟具体子类实现相关的,不同的子类功能是不同:

  • isDirect方法:如果是基于堆内存实现的ByteBuf,它返回false。
  • hasArray方法:由于UnpooledHeapByteBuf基于字节数组实现,所以它的返回值是true。
  • array方法:由于UnpooledHeapByteBuf基于字节数组实现,所以它的返回值是内部的字节数组成员变量。
  • 其他本地相关的方法有:arrayOffset、hasMemoryAddress和memoryAddress。
    • 内存地址相关接口主要由UnsafeByteBuf使用,它基于SUN JDK的sun.misc.Unsafe方法实现。

UnpooledDirectByteBuf与UnpooledHeapByteBuf的实现原理相同,不同之处就是它内部缓冲区由java.nio.DirectByteBuffer(ByteBuffer的底层内存分配)实现。

PooledByteBuf内存池原理分析
PoolArena

Arena本身是指一块区域,在内存管理中,Memory Arena是指内存中的一大块连续的区域,PoolArena就是Netty的内存池实现类。

为了集中管理内存的分配和释放,同时提高分配和释放内存时候的性能,很多框架和应用都会通过预先申请一大块内存,然后通过提供相应的分配和释放接口来使用内存。 这样一来,对内存的管理就被集中到几个类或者函数中,由于不再频繁使用系统调用来申请和释放内存,应用或者系统的性能也会大大提高。 在这种设计思路下,预先申请的那一大块内存就被称为Memory Arena。

Netty的PoolArena是由多个Chunk组成的大块内存区域,而每个Chunk则由一个或者多个Page组成,因此,对内存的组织和管理也就主要集中在如何管理和组织Chunk和Page了。

final PooledByteBufAllocator parent;

private final int pageSize;
private final int maxOrder;
private final int pageShifts;
private final int chunkSize;
private final int subpageOverflowMask;

private final PoolSubpage<T>[] tinySubpagePools;
private final PoolSubpage<T>[] smallSubpagePools;

private final PoolChunkList<T> q050;
private final PoolChunkList<T> q025;
private final PoolChunkList<T> q000;
private final PoolChunkList<T> qInit;
private final PoolChunkList<T> q075;
private final PoolChunkList<T> q100;
PoolChunk

Chunk主要用来组织和管理多个Page的内存分配和释放,在Netty中,Chunk中的Page被构建成一棵二叉树。

假设一个Chunk由16个Page组成,Page的大小是4个字节,Chunk的大小是64个字节(4X16)。 整棵树有5层,第1层(也就是叶子节点所在的层)用来分配所有Page的内存,第4层用来分配2个Page的内存,依次类推。

每个节点都记录了自己在整个Memory Arena中的偏移地址,当一个节点代表的内存区域 被分配出去之后,这个节点就会被标记为已分配,自这个节点以下的所有节点在后面的内存分配请求中都会被忽略。

Chunk的数据结构

对树的遍历采用深度优先的算法,但是在选择哪个子节点继续遍历时则是随机的,并不像通常的深度优先算法中那样总是访问左边的子节点。

PoolSubpage

对于小于一个Page的内存,Netty在Page中完成分配。每个Page会被切分成大小相等的多个存储块,存储块的大小由第一次申请的内存块大小决定。

一个Page只能用于分配与第一次申请时大小相同的内存,比如,一个4字节的Page,如果第一次分配了1字节的内存,那么后面这个Page只能继续分配l字节的内存, 如果有一个申请2字节内存的请求,就需要在一个新的Page中进行分配。

Page中存储区域的使用状态通过一个long数组来维护,数组中每个long的每一位表示一个块存储区域的占用情况:0表示未占用,l表示以占用。

对于一个4字节的Page来说,如果这个Page用来分配1个字节的存储区域,那么long数组中就只有一个long类型的元素,这个数值的低4位用来指示各个存储区域的占用情况。 对于一个128字节的Page来说,如果这个Page也是用来分配1个字节的存储区域,那么long数组中就会包含2个元素,总共128位,每一位代表一个区域的占用情况。

final PoolChunk<T> chunk;
final int memoryMapIdx;
final int runOffset;
final int pageSize;
final long[] bitmap;

PoolSubpage<T> prev;
PoolSubpage<T> next;

boolean doNotDestroy;
int elemSize;
int maxNumElems;
int nextAvail;
int bitmapLength;
int numAvail;
内存回收策略

无论是Chunk还是Page,都通过状态位来标识内存是否可用,不同之处是Chunk通过在二叉树上对节点进行标识实现,Page是通过维护块的使用状态标识来实现。

PooledDirectByteBuf源码分析

PooledDirectByteBuf基于内存池实现,与UnPooledDirectByteBuf的唯一不同就是缓冲区的分配是销毁策略不同,其他功能都是等同的,也就是说,两者唯一的不同就是内存分配策略不同。

创建字节缓冲区实例

由于采用内存池实现,所以新创建PooledDirectByteBuf对象是不能直接new一个实例,而是从内存池中获取,然后设置引用计数器的值。

private static final Recycler<PooledDirectByteBuf> RECYCLER = new Recycler<PooledDirectByteBuf>() {
        @Override
        protected PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
            return new PooledDirectByteBuf(handle, 0);
        }
    };
static PooledDirectByteBuf newInstance(int maxCapacity) {
    PooledDirectByteBuf buf = RECYCLER.get();
    buf.setRefCnt(1);
    buf.maxCapacity(maxCapacity);
    return buf;
}

直接从内存池Recycler<PooledDirectByteBuf>中获取PooledDirectByteBuf对象,然后设置它的引用计数器为1,设置缓冲区最大容量后返回。

复制新的字节缓冲区实例

复制一个新的实例,与原来的PooledDirectByteBuf独立:

@Override
public ByteBuf copy(int index, int length) {
    checkIndex(index, length);
    // 调用PooledByteBufAllocator分配一个新的ByteBuf
    ByteBuf copy = alloc().directBuffer(length, maxCapacity());
    copy.writeBytes(this, index, length);
    return copy;
}
// AbstractByteBufAllocator
@Override
public ByteBuf directBuffer(int initialCapacity, int maxCapacity) {
    if (initialCapacity == 0 && maxCapacity == 0) {
        return emptyBuf;
    }
    validate(initialCapacity, maxCapacity);
    return newDirectBuffer(initialCapacity, maxCapacity);
}
// 如果是基于内存池的分配器,它会从内存池中获取可用的ByteBuf
@Override
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
    PoolThreadCache cache = threadCache.get();
    PoolArena<ByteBuffer> directArena = cache.directArena;

    ByteBuf buf;
    if (directArena != null) {
        buf = directArena.allocate(cache, initialCapacity, maxCapacity);
    } else {
        if (PlatformDependent.hasUnsafe()) {
            buf = new UnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
        } else {
            buf = new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }
    }

    return toLeakAwareBuffer(buf);
}
//  如果是非池,则直接创建新的ByteBuf
@Override
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
    ByteBuf buf;
    if (PlatformDependent.hasUnsafe()) {
        buf = new UnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
    } else {
        buf = new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
    }

    return toLeakAwareBuffer(buf);
}
子类实现相关的方法

UnpooledHeapByteBuf,PooledDirectByteBuf也有子类实现相关的功能,当我们操作子类实现相关的方法时,需要对是否支持这些操作进行判断,否则会导致异常。

@Override
public boolean hasArray() {
    return false;
}
@Override
public byte[] array() {
    throw new UnsupportedOperationException("direct buffer");
}
@Override
public int arrayOffset() {
    throw new UnsupportedOperationException("direct buffer");
}
@Override
public boolean hasMemoryAddress() {
    return false;
}
@Override
public long memoryAddress() {
    throw new UnsupportedOperationException();
}

ByteBuf相关的辅助类功能介绍

ByteBufHolder

ByteBufHolder是ByteBuf的容器。

由于不同的协议消息体可以包含不同的协议字段和功能,因此,需要对ByteBuf进行包装和抽象,不同的子类可以有不同的实现。 为了满足这些定制化的需求,Netty抽象出了ByteBufHolder对象,它包含了一个ByteBuf,另外还提供了一些其他实用的方法,使用者继承ByteBufHolder接口后可以按需封装自己的实现。

ByteBufHolder继承关系图

ByteBufAllocator

ByteBufAllocator是字节缓冲区分配器,按照Netty的缓冲区实现不同,共有两种不同的分配器:基于内存池的字节缓冲区分配器和普通的字节缓冲区分配器。

ByteBufAllocator继承关系图

ByteBufAllocator主要API功能列表

CompositeByteBuf

CompositeByteBuf允许将多个ByteBuf的实例组装到一起,形成一个统一的视图。

CompositeByteBuf在一些场景下非常有用,例如某个协议POJO对象包含两部分:消息头和消息体,它们都是ByteBuf对象。 当需要对消息进行编码的时候需要进行整合,如果使用JDK的默认能力,有以下两种方式。

  • 将某个ByteBuffer复制到另一个ByteBuffer中,或者创建一个新的ByteBuffer,将两者复制到新建的ByteBuffer中;
  • 通过List或数组等容器,将消息头和消息体放到容器中进行统一维护和处理。

上面的做法非常别扭,实际上我们遇到的问题跟数据库中视图解决的问题一致: 缓冲区有多个,但是需要统一展示和处理,必须有存放它们的统一容器。为了解决这个问题,Netty提供了CompositeByteBuf。

public class CompositeByteBuf extends AbstractReferenceCountedByteBuf {
    private final ResourceLeak leak;
    private final ByteBufAllocator alloc;
    private final boolean direct;
    private final List<Component> components = new ArrayList<Component>();
    private final int maxNumComponents;
    private static final ByteBuffer FULL_BYTEBUFFER = (ByteBuffer) ByteBuffer.allocate(1).position(1);
    private boolean freed;
}

它定义了一个Component类型的集合,实际上Component就是ByteBuf的包装实现类,它聚合了ByteBuf对象,维护了在集合中的位置偏移量信息等。

private final class Component {
    final ByteBuf buf;
    final int length;
    int offset;
    int endOffset;
    Component(ByteBuf buf) {
        this.buf = buf;
        length = buf.readableBytes();
    }
    void freeIfNecessary() {
        // Unwrap so that we can free slices, too.
        buf.release(); // We should not get a NPE here. If so, it must be a bug.
    }
}

向CompositeByteBuf中新增一个ByteBuf:

public CompositeByteBuf addComponent(ByteBuf buffer) {
    addComponent0(components.size(), buffer);
    consolidateIfNeeded();
    return this;
}
private int addComponent0(int cIndex, ByteBuf buffer) {
    checkComponentIndex(cIndex);
    if (buffer == null) {
        throw new NullPointerException("buffer");
    }
    int readableBytes = buffer.readableBytes();
    if (readableBytes == 0) {
        return cIndex;
    }
    // No need to consolidate - just add a component to the list.
    Component c = new Component(buffer.order(ByteOrder.BIG_ENDIAN).slice());
    if (cIndex == components.size()) {
        components.add(c);
        if (cIndex == 0) {
            c.endOffset = readableBytes;
        } else {
            Component prev = components.get(cIndex - 1);
            c.offset = prev.endOffset;
            c.endOffset = c.offset + readableBytes;
        }
    } else {
        components.add(cIndex, c);
        updateComponentOffsets(cIndex);
    }
    return cIndex;
}

删除增加的ByteBuf:

public CompositeByteBuf removeComponent(int cIndex) {
    checkComponentIndex(cIndex);
    components.remove(cIndex).freeIfNecessary();
    // 更新索引偏移量
    updateComponentOffsets(cIndex);
    return this;
}
public CompositeByteBuf removeComponents(int cIndex, int numComponents) {
    checkComponentIndex(cIndex, numComponents);

    List<Component> toRemove = components.subList(cIndex, cIndex + numComponents);
    for (Component c: toRemove) {
        c.freeIfNecessary();
    }
    toRemove.clear();

    updateComponentOffsets(cIndex);
    return this;
}
ByteBufUtil

ByteBufUtil是一个非常有用的工具类,它提供了一系列静态方法用于操作ByteBuf对象。

其中最有用的方法就是对字符串的编码和解码:

  • encodeString(ByteBufAllocator alloc,CharBuffer src,Charset charset)
    • 对需要编码的字符串src按照指定的字符集charset进行编码,利用指定的ByteBufAllocator生成一个新的ByteBuf。
  • decodeString(ByteBuffer src,Charset charset)
    • 使用指定的ByteBuffer和charset进行对ByteBuffer进行解码,获取解码后的字符串。

还有一个非常有用的方法就是hexDump,它能够将参数ByteBuf的内容以十六进制字符串的方式打印出来,用于输出日志或者打印码流,方便问题定位,提升系统的可维护性。 hexDump包含了一系列的方法,参数不同,输出的结果也不同。

Channel和Unsafe

JDK的NIO类库的重要组成部分,就是提供了java.nio.SocketChannel和java.nio.ServerSocketChannel,用于非阻塞的I/O操作。 类似于NIO的Channel,Netty提供了自己的Channel和其子类实现,用于异步I/O操作和其他相关的操作。

Unsafe是个内部接口,聚合在Channel中协助进行网络读写相关的操作,因为它的设计初衷就是Channel的内部辅助类,不应该被Netty框架的上层使用者调用,所以被命名为Unsafe。 这里不能仅从字面理解认为它是不安全的操作,而要从整个架构的设计层面体会它的设计初衷和职责。

Channel功能说明

io.netty.channel.Channel是Netty网络操作抽象类,它聚合了一组功能,包括但不限于网路的读、写,客户端发起连接、主动关闭连接,链路关闭,获取通信双方的网络地址等。 它也包含了Netty框架相关的一些功能,包括获取该Chanel的EventLoop,获取缓冲分配器ByteBufAllocator和pipeline等。

Channel的工作原理

Channel是Netty抽象出来的网络I/O读写相关的接口,为什么不使用JDK NIO原生的Channel而要另起炉灶呢:

  • JDK的SocketChannel和ServerSocketChannel没有统一的Channel接口供业务开发者使用,对于用户而言,没有统一的操作视图,使用起来并不方便。
  • JDK的SocketChannel和ServerSocketChannel的主要职责就是网络I/O操作,由于它们是SPI类接口,由具体的虚拟机厂家来提供,所以通过继承SPI功能类来扩展其功能的难度很大;直接实现ServerSocketChannel和SocketChannel抽象类,其工作量和重新开发一个新的Channel功能类是差不多的。
  • Netty的Channel需要能够跟Netty的整体架构融合在一起,例如I/O模型、基于ChannelPipeline的定制模型,以及基于元数据描述配置化的TCP参数等,这些JDK的SocketChannel和ServerSocketChannel都没有提供,需要重新封装。
  • 自定义的Channel,功能实现更加灵活。

Netty重新设计了Channel接口,并且给予了很多不同的实现。设计原理比较简单,但是功能却比较繁杂。

  • 在Channel接口层,采用Facade模式进行统一封装,将网络I/O操作、网络I/O相关联的其他操作封装起来,统一对外提供。
  • Channel接口的定义尽量大而全,为SocketChanoel和ServerSocketChannel提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度上实现功能和接口的重用。
  • 具体实现采用聚合而非包含的方式,将相关的功能类聚合在Channel中,由Channel统一负责分配和调度,功能实现更加灵活。
Channel的功能介绍

网络I/O操作:

  1. Channel read()
    • 从当前的Channel中读取数据到第一个inbound缓冲区中,如果数据被成功读取,触发ChannelHandler.channelRead(ChannelHandlerContext,Object)事件,读取操作API调用完成之后,紧接着会触发ChannelHandler.channelReadComplete(ChannelHandlerContext)事件,这样业务的ChannelHandler可以决定是否需要继续读取数据。如果已经有读操作请求被挂起,则后续的读操作会被忽略。
  2. ChannelFuture write(Object msg)
    • 请求将当前的msg通过ChannelPipeline写入到目标Channel中。注意,write操作只是将消息存入到消息发送环形数组中,并没有真正被发送,只有调用flush操作才会被写入到Channel中,发送给对方。
  3. ChannelFuture write(Object msg, ChannelPromise promise)
    • 功能与write(Object msg)相同,但是携带了ChannelPromise参数负责设置写入操作的结果。
  4. ChannelFuture writeAndFlush(Object msg, ChanoelPromise promise)
    • 与方法3功能类似,不同之处在于它会将消息写入到Channel中发送,等价于单独调用write和flush操作的组合。
  5. Channel Future writeAndFlush(Object msg)
    • 功能等同于方法4但是没有携带writeAndFlusb(Objectmsg)参数。
  6. Channel flush()
    • 将之前写入到发送环形数组中的消息全部写入到目标Chanel中,发送给通信对方。
  7. ChannelFuture close(ChannelPro皿se promise)
    • 主动关闭当前连接,通过ChannelPromise设置操作结果并进行结果通知,无论操作是否成功,都可以通过ChannelPromise获取操作结果。该操作会级联触发ChannelPipeline中所有ChannelHandler的ChannelHandler.close(ChannelHandlerContext,ChannelPromise)事件。
  8. ChannelFuture disconnect(ChannelPromise promise)
    • 请求断开与远程通信对端的连接并使用ChannelPrornise来获取操作结果的通知消息。该方法会级联触发ChannelHandler.disconnect(ChannelHandlerCootext,ChannelPromise)事件。
  9. ChannelFuture connect(SocketAddress remoteAddress)
    • 客户端使用指定的服务端地址remoteAddress发起连接请求,如果连接因为应答超时而失败,ChannelFuture中的操作结果就是ConnectTimeoutException异常,如果连接被拒绝,操作结果为ConnectException。该方法会级联触发ChannelHandler.connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise)事件。
  10. ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress)
    • 与方法9功能类似,唯一不同的就是先绑定指定的本地地址loca!Address,然后再连接服务端。
  11. ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise)
    • 与方法9功能类似,唯一不同的是携带了ChannelPromise参数用于写入操作结果。
  12. connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise)
    • 与方法11功能类似,唯一不同的就是绑定了本地地址。
  13. ChannelFuture bind(SocketAddress localAddress)
    • 绑定指定的本地Socket地址localAddress,该方法会级联触发ChannelHandler.bind(ChannelHandlerContext,SocketAddress,ChannelPromise)事件。
  14. ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise)
    • 与方法13功能类似,多携带了了一个ChannelPromise用于写入操作结果。
  15. ChannelConfig config()
    • 获取当前Channel的配置信息,例如CONNECTTIMEOUT_MILLIS。
  16. boolean isOpen()
    • 判断当前Channel是否已经打开。
  17. boolean isRegistered()
    • 判断当前Channel是否已经注册到EventLoop上。
  18. boolean isActive()
    • 判断当前Channel是否已经处于激活状态。
  19. ChannelMetadata metadata()
    • 获取当前Channel的元数据描述信息,包括TCP参数配置等。
  20. SocketAddress localAddress()
    • 获取当前Channel的本地绑定地址。
  21. SocketAddress remoteAddress()
    • 获取当前Channel通信的远程Socket地址。

其它常用API:

第一个比较重要的方法是eventLoop()。

Channel需要注册到EventLoop的多路复用器上,用于处理I/O事件,通过eveatLoop()方法可以获取到Channel注册的EventLoop。EventLoop本质上就是处理网络读写事件的Reactor线程。 在Netty中,它不仅仅用来处理网络事件,也可以用来执行定时任务和用户自定义NioTask等任务。

第二个比较常用的方法是metadata()方法。

当创建Socket的时候需要指定TCP参数,例如接收和发送的TCP缓冲区大小,TCP的超时时间,是否重用地址等等。 在Netty中,每个Channel对应一个物理连接,每个连接都有自己的TCP参数配置。所以,Channel会聚合一个ChannelMetadata用来对TCP参数提供元数据描述信息,通过metadata()方法就可以获取当前Channel的TCP参数配置。

第三个方法是parent()。

对于服务端Channel而言,它的父Channel为空;对于客户端Channel,它的父Channel就是创建它的ServerSocketChannel。

第四个方法是用户获取Channel标识的id()。

它返回Channelld对象,ChannelId是Channel的唯一标识,它的可能生成策略:

  • 机器的MAC地址(EUI-48或者EUI-64)等可以代表全局唯一的信息;
  • 当前的进程ID;
  • 当前系统时间的毫秒 System.currentTimeMillis();
  • 当前系统时间纳秒数 System.nanoTime();
  • 32位的随机整型数;
  • 32位自增的序列数。

Channel源码解析

Channel的主要继承关系类图

NioServerSocketChannel继承关系类图:

NioServerSocketChannel继承关系类图

NioSocketChannel继承关系类图:

NioSocketChannel继承关系类图

AbatractChannel源码解析
成员变量
public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
    private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractChannel.class);
    // 两个静态全局异常
    static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException();
    static final NotYetConnectedException NOT_YET_CONNECTED_EXCEPTION = new NotYetConnectedException();
    static {
        // 置空堆栈
        CLOSED_CHANNEL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
        NOT_YET_CONNECTED_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
    }
    // 用于采样预测下一个报文的大小
    private MessageSizeEstimator.Handle estimatorHandle;
    // 父Channel
    private final Channel parent;
    // 全局ID
    private final ChannelId id = DefaultChannelId.newInstance();
    // Unsafe
    private final Unsafe unsafe;
    // 当前Channel对应的DefualtChannelPipeline
    private final DefaultChannelPipeline pipeline;
    private final ChannelFuture succeededFuture = new SucceededChannelFuture(this, null);
    private final VoidChannelPromise voidPromise = new VoidChannelPromise(this, true);
    private final VoidChannelPromise unsafeVoidPromise = new VoidChannelPromise(this, false);
    private final CloseFuture closeFuture = new CloseFuture(this);

    private volatile SocketAddress localAddress;
    private volatile SocketAddress remoteAddress;
    // EventLoop
    private final EventLoop eventLoop;
    private volatile boolean registered;
    /** Cache for the string representation of this channel */
    private boolean strValActive;
    private String strVal;
}
核心API源码分析

网络读写操作操作时会触发ChannelPipeline中对应的事件方法。

Netty基于事件驱动,我们也可以理解为当Cbnanel进行I/O操作时会产生对应的I/O事件,然后驱动事件在ChannelPipeline中传播,由对应的ChannelHandler对事件进行拦截和处理,不关心的事件可以直接忽略。 采用事件驱动的方式可以非常轻松地通过事件定义来划分事件拦截切面,方便业务的定制和功能扩展,相比AOP,其性能更高,但是功能却基本等价。

网络I/O操作直接调用DefauItChannelPipeline的相关方法,由DefaultChannelPipeline中对应的ChannelHandler进行具体的逻辑处理:

@Override
public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
    return pipeline.connect(remoteAddress, localAddress);
}
@Override
public ChannelFuture disconnect() {
    return pipeline.disconnect();
}
@Override
public ChannelFuture close() {
    return pipeline.close();
}
@Override
public Channel flush() {
    pipeline.flush();
    return this;
}

AbstractChannel也提供了一些公共API的具体实现,如localAddress()和remoteAddress()方法。

AbatractNioChannel源码解析
成员变量定义
public abstract class AbstractNioChannel extends AbstractChannel {
    private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractNioChannel.class);
    private final SelectableChannel ch;
    protected final int readInterestOp;
    private volatile SelectionKey selectionKey;
    private volatile boolean inputShutdown;
    /**
     * The future of the current connection attempt.  If not null, subsequent
     * connection attempts will fail.
     */
    private ChannelPromise connectPromise;
    private ScheduledFuture<?> connectTimeoutFuture;
    private SocketAddress requestedRemoteAddress;
}
  • 由于NIO Channel、NioSocketChannel和NioServerSocketChannel需要共用,所以定义了一个java.nio.SocketChannel和java.nio.ServerSocketChannel的公共父类SelectableChannel,用于设置SelectableChannel参数和进行I/O操作。
  • readInterestOp,它代表了JDK SelectionKey的OP_READ。
  • volatile修饰的SelectionKey,该SelectionKey是Channel注册到EventLoop后返回的选择键。
  • 代表连接操作结果的ChannelPromise以及连接超时定时器ScheduledFuture和请求的通信地址信息。
核心API源码分析

Channel的注册:

@Override
protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
            selectionKey = javaChannel().register(eventLoop().selector, 0, this);
            return;
        } catch (CancelledKeyException e) {
            if (!selected) {
                // Force the Selector to select now as the "canceled" SelectionKey may still be
                // cached and not removed because no Select.select(..) operation was called yet.
                // 去除已经取消的key,下次循环重新注册
                eventLoop().selectNow();
                selected = true;
            } else {
                // We forced a select operation on the selector before but the SelectionKey is still cached
                // for whatever reason. JDK bug ?
                // 已经取消仍然无法重新注册则抛出异常
                throw e;
            }
        }
    }
}
// JDK注册方法定义
SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException;

注册Channel的时候需要指定监听的网络操作位来表示Channel对哪几类网络事件感兴趣:

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; // 服务端接收客户端连接操作位。

AbstractNioChannel注册的是0,说明对任何事件都不感兴趣,仅仅完成注册操作。 注册的时候可以指定附件,后续Channel接收到网络事件通知时可以从SelectionKey中重新获取之前的附件进行处理,此处将AbstractNioChannel的实现子类自身当作附件注册。
如果注册Channel成功,则返回selectionKey,通过selectionKey可以从多路复用器中获取Channel对象。

如果当前注册返回的selectionKey已经被取消,则抛出CancelledKeyException异常,捕获该异常进行处理。如果是第一次处理该异常,调用多路复用器的selectNow()方法将已经取消的selectionKey从多路复用器中删除掉。

操作成功之后,将selected置为true,说明之前失效的selectionKey已经被删除掉。继续发起下一次注册操作,如果成功则退出,如果仍然发生CancelledKeyException异常,说明我们无法删除已经被取消的selectionKey,按照JDK的API说明,这种意外不应该发生。 如果发生这种问题,则说明可能NIO的相关类库存在不可恢复的BUG,直接抛出CancelledKeyException异常到上层进行统一处理。

准备处理读操作之前需要设置网路操作位为读:

@Override
protected void doBeginRead() throws Exception {
    // 处于关闭状态直接返回
    if (inputShutdown) {
        return;
    }
    // 如果可用说明状态正常,进行修改,否则返回
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }
    final int interestOps = selectionKey.interestOps();
    // 等于0则没有设置读操作位,进行设置。
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}
// 读操作位的判断与JDK的SelectionKey的判断是等价的
public final boolean isReadable() {
    return (readyOps() & OP_READ) != 0;
}
AbatractNioByteChannel源码解析

成员变量只有一个Runnable类型的flushTask来负责继续写半包消息。

主要方法doWrite(ChannelOutboundBuffer in):

@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    int writeSpinCount = -1;
    for (;;) {
        // 从唤醒数组ChannelOutboundBuffer中弹出一条消息,如果空则说明发送完成
        
        Object msg = in.current(true);
        if (msg == null) {
            // Wrote all messages.
            // 并清除半包标识,退出循环
            clearOpWrite();
            break;
        }
        if (msg instanceof ByteBuf) {
            ByteBuf buf = (ByteBuf) msg;
            int readableBytes = buf.readableBytes();
            // 不可读删除继续
            if (readableBytes == 0) {
                in.remove();
                continue;
            }
            // 写半包标识
            boolean setOpWrite = false;
            // 是否全部发送标识
            boolean done = false;
            // 发送的总消息字节数
            long flushedAmount = 0;
            // 对发送次数进行判断
            if (writeSpinCount == -1) {
                // 获取循环发送次数
                // 循环发送次数是指一次发送没有完成时(写半包),继续循环发送的次数
                // 设置这个值防止没有写完成时I/O线程一直写无法处理其他操作。
                writeSpinCount = config().getWriteSpinCount();
            }
            for (int i = writeSpinCount - 1; i >= 0; i --) {
                // 调用doWriteBytes进行消息发送,子类实现
                int localFlushedAmount = doWriteBytes(buf);
                // 如果发送字节为0,则说明缓冲区已经满了,发生了ZERO_WINDOW,再次发送还可能是0字节
                if (localFlushedAmount == 0) {
                    // 空循环会耗费CPU,导致无法处理其他I/O操作,设置setOpWrite后退出,释放I/O线程
                    setOpWrite = true;
                    break;
                }
                flushedAmount += localFlushedAmount;
                // 缓冲区没有可读字节说明发送完成
                if (!buf.isReadable()) {
                    // 设置发送成功
                    done = true;
                    break;
                }
            }
            // 更新发送进度
            in.progress(flushedAmount);
            if (done) {
                // 完成从数组中删除
                in.remove();
            } else {
                // 未完成则设置写半包标识,启动刷新线程继续发送之前没有发送的半包消息(写半包)
                incompleteWrite(setOpWrite);
                break;
            }
        } else if (msg instanceof FileRegion) {
            FileRegion region = (FileRegion) msg;
            boolean setOpWrite = false;
            boolean done = false;
            long flushedAmount = 0;
            if (writeSpinCount == -1) {
                writeSpinCount = config().getWriteSpinCount();
            }
            for (int i = writeSpinCount - 1; i >= 0; i --) {
                long localFlushedAmount = doWriteFileRegion(region);
                if (localFlushedAmount == 0) {
                    setOpWrite = true;
                    break;
                }
                flushedAmount += localFlushedAmount;
                if (region.transfered() >= region.count()) {
                    done = true;
                    break;
                }
            }
            in.progress(flushedAmount);
            if (done) {
                in.remove();
            } else {
                incompleteWrite(setOpWrite);
                break;
            }
        } else {
            throw new UnsupportedOperationException("unsupported message type: " + StringUtil.simpleClassName(msg));
        }
    }
}
// 清除半包标识
protected final void clearOpWrite() {
    final SelectionKey key = selectionKey();
    final int interestOps = key.interestOps();
    // 如果是写操作位,说明是可写的,清除写标志位
    if ((interestOps & SelectionKey.OP_WRITE) != 0) {
        // 取反与,保留其它操作位,去除写操作位
        key.interestOps(interestOps & ~SelectionKey.OP_WRITE);
    }
}
// 半包发送任务方法
protected final void incompleteWrite(boolean setOpWrite) {
    // Did not write completely.
        // 首先判断是否需要设置写半包标识
    if (setOpWrite) {
        // 如果设置了写标识,多路复用器会不断轮询对应的Channel,用于处理没有发送的半包消息
        setOpWrite();
    } else {
        // Schedule flush again later so other tasks can be picked up in the meantime
        Runnable flushTask = this.flushTask;
        if (flushTask == null) {
            flushTask = this.flushTask = new Runnable() {
                @Override
                public void run() {
                    // 半包发送任务用于继续发送半包消息
                    // 直接调用flush来发送缓冲区数组中的消息。
                    flush();
                }
            };
        }
        eventLoop().execute(flushTask);
    }
}
// 写半包标识就是将key设置为可写的
protected final void setOpWrite() {
    final SelectionKey key = selectionKey();
    final int interestOps = key.interestOps();
    if ((interestOps & SelectionKey.OP_WRITE) == 0) {
        key.interestOps(interestOps | SelectionKey.OP_WRITE);
    }
}
AbatractNioMessageChannel源码解析

与AbatractNioByteChannel类似,一个发送的是ByteBuf或者FileRegion,它们可以被直接发送,另一个发送的是POJO对象。

protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    final SelectionKey key = selectionKey();
    final int interestOps = key.interestOps();

    for (;;) {
        Object msg = in.current();
        if (msg == null) {
            // Wrote all messages.
            if ((interestOps & SelectionKey.OP_WRITE) != 0) {
                key.interestOps(interestOps & ~SelectionKey.OP_WRITE);
            }
            break;
        }

        boolean done = false;
        for (int i = config().getWriteSpinCount() - 1; i >= 0; i --) {
            if (doWriteMessage(msg, in)) {
                done = true;
                break;
            }
        }

        if (done) {
            in.remove();
        } else {
            // Did not write all messages.
            if ((interestOps & SelectionKey.OP_WRITE) == 0) {
                key.interestOps(interestOps | SelectionKey.OP_WRITE);
            }
            break;
        }
    }
}
AbstractNioMessageServerChannel源码解析

它定义了一个EventLoopGroup类型的childGroup,用于给新接入的客户端NioSocketChannel分配EventLoop。

private final EventLoopGroup childGroup;

每当服务端接入一个新的客户端连接NioSocketChannel时,都会调用childEventLoopGroup方法获取EventLoopGroup线程组,用于给NioSocketChannel分配Reactor线程EventLoop。

protected int doReadMessages(List<Object> buf) throws Exception {
    SocketChannel ch = javaChannel().accept();
    try {
        if (ch != null) {
            // childEventLoopGroup方法进行线程分配
            buf.add(new NioSocketChannel(this, childEventLoopGroup().next(), ch));
            return 1;
        }
    } catch (Throwable t) {
        logger.warn("Failed to create a new channel from an accepted socket.", t);
        try {
            ch.close();
        } catch (Throwable t2) {
            logger.warn("Failed to close a socket.", t2);
        }
    }

    return 0;
}
NioServerSocketChannel源码解析
private static final ChannelMetadata METADATA = new ChannelMetadata(false);
// 用于配置ServerSocketChannel的TCP此参数。
private final ServerSocketChannelConfig config;

ServerSocketChannel接口实现:

@Override
public InetSocketAddress localAddress() {
    return (InetSocketAddress) super.localAddress();
}
@Override
public ChannelMetadata metadata() {
    return METADATA;
}
@Override
public ServerSocketChannelConfig config() {
    return config;
}
@Override
public boolean isActive() {
    // 调用isBound返回端口是否处于绑定状态
    return javaChannel().socket().isBound();
}
@Override
public InetSocketAddress remoteAddress() {
    // 返回空
    return null;
}
@Override
protected ServerSocketChannel javaChannel() {
    // 实现是ServerSocketChannel
    // 服务器端进行端口绑定时可以这的那个backlog,允许客户端排队的最大长度
    return (ServerSocketChannel) super.javaChannel();
}
@Override
protected SocketAddress localAddress0() {
    return javaChannel().socket().getLocalSocketAddress();
}
@Override
protected void doBind(SocketAddress localAddress) throws Exception {
    javaChannel().socket().bind(localAddress, config.getBacklog());
}

NioServerChanel的读取操作就是接收客户端的连接,创建NioSocketChannel对象。

@Override
protected int doReadMessages(List<Object> buf) throws Exception {
    // 接收客户端连接
    SocketChannel ch = javaChannel().accept();
    try {
        if (ch != null) {
            // 不为空则创建NioSocketChannel并返回成功
            buf.add(new NioSocketChannel(this, childEventLoopGroup().next(), ch));
            return 1;
        }
    } catch (Throwable t) {
        logger.warn("Failed to create a new channel from an accepted socket.", t);
        try {
            ch.close();
        } catch (Throwable t2) {
            logger.warn("Failed to close a socket.", t2);
        }
    }
    return 0;
}
NioSocketChannel源码解析
连接操作
@Override
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
    // 判断本地Socket地址是否为空
    if (localAddress != null) {
        // 绑定本地地址
        javaChannel().socket().bind(localAddress);
    }

    boolean success = false;
    try {
        // 发起连接,连接失败会抛出异常
        boolean connected = javaChannel().connect(remoteAddress);
        if (!connected) {
            // 暂时没连上,服务器没有返回ACK应答,连接结果不确定
            // 监听连接操作位
            selectionKey().interestOps(SelectionKey.OP_CONNECT);
        }
        success = true;
        // 连接成功返回true,失败返回false
        return connected;
    } finally {
        // 如果抛出了I/O异常,说明TCP握手请求被RESET或者被拒绝,关闭客户端连接。
        if (!success) {
            doClose();
        }
    }
}
写半包
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    for (;;) {
        // Do non-gathering write for a single buffer case.
        final int msgCount = in.size();
        if (msgCount <= 1) {
            // 如果待发送的ByteBuf个数小于等于1,调用父类AbstractNioByteChannel的方法后退出
            super.doWrite(in);
            return;
        }

        // Ensure the pending writes are made of ByteBufs only.
        ByteBuffer[] nioBuffers = in.nioBuffers();
        if (nioBuffers == null) {
            super.doWrite(in);
            return;
        }
        // 需要发送的数组个数
        int nioBufferCnt = in.nioBufferCount();
        long expectedWrittenBytes = in.nioBufferSize();

        final SocketChannel ch = javaChannel();
        long writtenBytes = 0;
        boolean done = false;// 是否完成
        boolean setOpWrite = false;// 是否写半包
        // 限制循环次数
        for (int i = config().getWriteSpinCount() - 1; i >= 0; i --) {
            final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
            // TCP发送缓冲区已满则跳出循环,并设置写半包
            if (localWrittenBytes == 0) {
                setOpWrite = true;
                break;
            }
            expectedWrittenBytes -= localWrittenBytes;
            writtenBytes += localWrittenBytes;
            // 完成则退出循环
            if (expectedWrittenBytes == 0) {
                done = true;
                break;
            }
        }
        // 循环结束后判断是否发送完成
        if (done) {
            // Release all buffers
            for (int i = msgCount; i > 0; i --) {
                in.remove();
            }

            // Finish the write loop if no new messages were flushed by in.remove().
            if (in.isEmpty()) {
                clearOpWrite();
                break;
            }
        } else {
            // Did not write all buffers completely.
            // Release the fully written buffers and update the indexes of the partially written buffer.
            // 遍历发送缓冲区,将消息发送结果进行判断
            for (int i = msgCount; i > 0; i --) {
                final ByteBuf buf = (ByteBuf) in.current();
                // 获取可读可写字节
                final int readerIndex = buf.readerIndex();
                final int readableBytes = buf.writerIndex() - readerIndex;
                // 如果可读字节数小于总发送字节数说明发送完成
                if (readableBytes < writtenBytes) {
                    in.progress(readableBytes);
                    in.remove();
                    writtenBytes -= readableBytes;
                } else if (readableBytes > writtenBytes) {
                    // 如果大于说明没有被完全发送
                    buf.readerIndex(readerIndex + (int) writtenBytes);
                    in.progress(writtenBytes);
                    break;
                } else { // readableBytes == writtenBytes
                    // 相等也是发送完成
                    in.progress(readableBytes);
                    in.remove();
                    break;
                }
            }

            incompleteWrite(setOpWrite);
            break;
        }
    }
}
读取操作

NioSocketChannel的读取操作实际上是基于NIO的SocketChannel和Netty的ByteBuf封装而成。

@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
    return byteBuf.writeBytes(javaChannel(), byteBuf.writableBytes());
}
@Override
public int writeBytes(ScatteringByteChannel in, int length) throws IOException {
    ensureWritable(length);
    int writtenBytes = setBytes(writerIndex, in, length);
    if (writtenBytes > 0) {
        writerIndex += writtenBytes;
    }
    return writtenBytes;
}
// UnpooledHeapByteBuf
@Override
public int setBytes(int index, ScatteringByteChannel in, int length) throws IOException {
    ensureAccessible();
    try {
        // 起始position为writeIndex,limit为writeIndex + length
        return in.read((ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length));
    } catch (ClosedChannelException e) {
        return -1;
    }
}

Unsafe源码分析

Unsafe接口实际上是Channel接口的辅助接口,它不应该被用户代码直接调用。实际的I/O读写操作都是由Unsafe接口负责完成的。

Unsafe API功能列表:

Unsafe API功能列表

Unsafe继承关系类图

Unsafe继承关系类图

AbstractUnsafe源码分析
register方法

register方法主要用于将当前Unsafe对应的Channel注册到EventLoop的多路复用器上, 然后调用DefaultChannelPipeline的fireChannelRegistered方法。 如果Channel被激活,则调用DefaultChannelPipeline的fireChanne1Active方法。

首先判断当前所在的线程是否是Channel对应的NioEventLoop线程:

  • 如果是同一个线程则不存在多线程并发操作问题,直接调用registe0进行注册;
  • 如果是由用户线程或者其他线程发起的注册操作,则将注册操作封装成Runnable,放到NioEventLoop任务队列中执行。

注意事项:如果直接执行register0方法,会存在多线程并发操作Channel的问题。

@Override
public final void register(final ChannelPromise promise) {
    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            logger.warn(
                    "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                    AbstractChannel.this, t);
            closeForcibly();
            closeFuture.setClosed();
            promise.setFailure(t);
        }
    }
}
private void register0(ChannelPromise promise) {
    try {
        // check if the channel is still open as it could be closed in the mean time when the register
        // call was outside of the eventLoop
        // 当前Channel是否打开
        if (!ensureOpen(promise)) {
            // 未打开无法注册直接返回
            return;
        }
        // AbstractNioChannel实现
        doRegister();
        registered = true;
        promise.setSuccess();
        pipeline.fireChannelRegistered();
        if (isActive()) {
            pipeline.fireChannelActive();
        }
    } catch (Throwable t) {
        // Close the channel directly to avoid FD leak.
        closeForcibly();
        closeFuture.setClosed();
        if (!promise.tryFailure(t)) {
            logger.warn(
                    "Tried to fail the registration promise, but it is complete already. " +
                            "Swallowing the cause of the registration failure:", t);
        }
    }
}
bind方法

bind方法主要用于绑定指定的端口,对于服务端,用于绑定监听端口,可以设置backlog参数;对于客户端,主要用于指定客户端Channel的本地绑定Socket地址。

boolean wasActive = isActive();
try {
    doBind(localAddress);
} catch (Throwable t) {
    promise.setFailure(t);
    closeIfClosed();
    return;
}
if (!wasActive && isActive()) {
    invokeLater(new Runnable() {
        @Override
        public void run() {
            pipeline.fireChannelActive();
        }
    });
}
promise.setSuccess();

doBind方法,对于NioSocketChannel和NioServerSocketChannel有不同的实现。

如果绑定本地端口发生异常,则将异常设置到ChannelPromise中用于通知ChannelFuture,随后调用closeIfClosed方法来关闭Channel。

disconnect方法

disconnect用于客户端或者服务端主动关闭连接。

操作过程与bind大体相同。

close方法

在链路关闭之前需要首先判断是否处于刷新状态,如果处于刷新状态说明还有消息尚未发送出去,需要等到所有消息发送完成再关闭链路,因此,将关闭操作封装成Runnable稍后再执行。

@Override
public final void close(final ChannelPromise promise) {
    if (inFlush0) {
        invokeLater(new Runnable() {
            @Override
            public void run() {
                close(promise);
            }
        });
        return;
    }
    // 判断关闭操作是否完成,如果完成直接返回
    if (closeFuture.isDone()) {
        // Closed already.
        promise.setSuccess();
        return;
    }
    // 执行关闭操作,将消息发送缓冲区数组设置为空
    boolean wasActive = isActive();
    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    this.outboundBuffer = null; // Disallow adding any messages and flushes to outboundBuffer.
    try {
        doClose();
        closeFuture.setClosed();
        promise.setSuccess();
    } catch (Throwable t) {
        closeFuture.setClosed();
        promise.setFailure(t);
    }

    // Fail all the queued messages
    try {
        outboundBuffer.failFlushed(CLOSED_CHANNEL_EXCEPTION);
        // 调用close方法释放缓冲区的消息
        outboundBuffer.close(CLOSED_CHANNEL_EXCEPTION);
    } finally {
        if (wasActive && !isActive()) {
            // 随后构造链路关闭通知消息
            invokeLater(new Runnable() {
                @Override
                public void run() {
                    pipeline.fireChannelInactive();
                }
            });
        }
        // 将Channel从多路复用器上取消注册。
        deregister();
    }
}
write方法

write方法实际上将消息添加到环形发送数组中,并不是真正的写Channel。

如果Channel没有处于激活状态,说明TCP链路还没有真正建立成功,当前Channel存在以下两种状态:

  • Channel打开,但是TCP链路尚未建立成功:NOT_YET_CONNECTED_EXCEPTION:
  • Channel已经关闭:CLOSED_CHANNEL_EXCEPTION。
@Override
public void write(Object msg, ChannelPromise promise) {
    if (!isActive()) {
        // 对链路状态进行判断,给ChannelPromise设置对应的异常
        // Mark the write request as failure if the channel is inactive.
        if (isOpen()) {
            promise.tryFailure(NOT_YET_CONNECTED_EXCEPTION);
        } else {
            promise.tryFailure(CLOSED_CHANNEL_EXCEPTION);
        }
        // release message now to prevent resource-leak
        // 调用ReferenceCountUtil的release方法释放发送的msg对象。
        ReferenceCountUtil.release(msg);
    } else {
        // 如果链路状态正常,则将需要发送的msg和promise放入发送缓冲区中(环形数组)。
        outboundBuffer.addMessage(msg, promise);
    }
}
flush方法

flush方法负责将发送缓冲区中待发送的消息全部写入到Channel中,并发送给通信对方。

@Override
public void flush() {
    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    if (outboundBuffer == null) {
        return;
    }
    // 首先将环形数组的unflushed指针修改为tail,标识本次要发送消息的缓冲区范围
    outboundBuffer.addFlush();
    // 发送
    flush0();
}
void addFlush() {
    unflushed = tail;
}
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    for (;;) {
        // Do non-gathering write for a single buffer case.
        final int msgCount = in.size();
        // 判断发送消息个数unflushed - flushed
        if (msgCount <= 1) {
            // 只有一个,调用父类AbstractNioByteChannel的方法
            super.doWrite(in);
            return;
        }
        // Ensure the pending writes are made of ByteBufs only.
        ByteBuffer[] nioBuffers = in.nioBuffers();
        if (nioBuffers == null) {
            super.doWrite(in);
            return;
        }
        int nioBufferCnt = in.nioBufferCount();
        long expectedWrittenBytes = in.nioBufferSize();
        final SocketChannel ch = javaChannel();
        long writtenBytes = 0;
        boolean done = false;
        boolean setOpWrite = false;
        for (int i = config().getWriteSpinCount() - 1; i >= 0; i --) {
            final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
            if (localWrittenBytes == 0) {
                setOpWrite = true;
                break;
            }
            expectedWrittenBytes -= localWrittenBytes;
            writtenBytes += localWrittenBytes;
            if (expectedWrittenBytes == 0) {
                done = true;
                break;
            }
        }
        if (done) {
            // Release all buffers
            for (int i = msgCount; i > 0; i --) {
                in.remove();
            }
            // Finish the write loop if no new messages were flushed by in.remove().
            if (in.isEmpty()) {
                clearOpWrite();
                break;
            }
        } else {
            // Did not write all buffers completely.
            // Release the fully written buffers and update the indexes of the partially written buffer.
            for (int i = msgCount; i > 0; i --) {
                final ByteBuf buf = (ByteBuf) in.current();
                final int readerIndex = buf.readerIndex();
                final int readableBytes = buf.writerIndex() - readerIndex;

                if (readableBytes < writtenBytes) {
                    in.progress(readableBytes);
                    in.remove();
                    writtenBytes -= readableBytes;
                } else if (readableBytes > writtenBytes) {
                    buf.readerIndex(readerIndex + (int) writtenBytes);
                    in.progress(writtenBytes);
                    break;
                } else { // readableBytes == writtenBytes
                    in.progress(readableBytes);
                    in.remove();
                    break;
                }
            }
            incompleteWrite(setOpWrite);
            break;
        }
    }
}

// flush0 -> doWrite -> super.doWrite
// AbstractNioByteChannel
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    int writeSpinCount = -1;
    for (;;) {
        // 只有一个消息,直接从环形数组中获取当前要发送的消息
        Object msg = in.current(true);// 查看下面的current方法
        if (msg == null) {
            // Wrote all messages.
            // 如果为空说明该消息已经被发送且回收
            clearOpWrite();
            break;
        }
        if (msg instanceof ByteBuf) {
            ByteBuf buf = (ByteBuf) msg;
            int readableBytes = buf.readableBytes();
            // 如果没有可读的字节,remove掉继续
            if (readableBytes == 0) {
                in.remove();// 查看下面的remove方法
                continue;
            }
            boolean setOpWrite = false;
            boolean done = false;
            long flushedAmount = 0;
            if (writeSpinCount == -1) {
                writeSpinCount = config().getWriteSpinCount();
            }
            for (int i = writeSpinCount - 1; i >= 0; i --) {
                int localFlushedAmount = doWriteBytes(buf);// 查看下面的doWriteBytes方法
                // TCP发送缓冲区已满,设置半包标识后退出循环。
                if (localFlushedAmount == 0) {
                    setOpWrite = true;
                    break;
                }

                flushedAmount += localFlushedAmount;
                if (!buf.isReadable()) {
                    done = true;
                    break;
                }
            }
            // 更新发送进度,发送的字节数和需要发送的字节数的比值
            in.progress(flushedAmount);

            if (done) {
                in.remove();
            } else {
                incompleteWrite(setOpWrite);
                break;
            }
        } else if (msg instanceof FileRegion) {
            FileRegion region = (FileRegion) msg;
            boolean setOpWrite = false;
            boolean done = false;
            long flushedAmount = 0;
            if (writeSpinCount == -1) {
                writeSpinCount = config().getWriteSpinCount();
            }
            for (int i = writeSpinCount - 1; i >= 0; i --) {
                long localFlushedAmount = doWriteFileRegion(region);
                if (localFlushedAmount == 0) {
                    setOpWrite = true;
                    break;
                }

                flushedAmount += localFlushedAmount;
                if (region.transfered() >= region.count()) {
                    done = true;
                    break;
                }
            }
            in.progress(flushedAmount);
            if (done) {
                in.remove();
            } else {
                incompleteWrite(setOpWrite);
                break;
            }
        } else {
            throw new UnsupportedOperationException("unsupported message type: " + StringUtil.simpleClassName(msg));
        }
    }
}
public Object current(boolean preferDirect) {
    if (isEmpty()) {
        return null;
    } else {
        // TODO: Think of a smart way to handle ByteBufHolder messages
        Object msg = buffer[flushed].msg;
        if (threadLocalDirectBufferSize <= 0 || !preferDirect) {
            return msg;
        }
        if (msg instanceof ByteBuf) {
            ByteBuf buf = (ByteBuf) msg;
            if (buf.isDirect()) {
                // 非堆内存,直接返回
                return buf;
            } else {
                int readableBytes = buf.readableBytes();
                if (readableBytes == 0) {
                    return buf;
                }
                // Non-direct buffers are copied into JDK's own internal direct buffer on every I/O.
                // We can do a better job by using our pooled allocator. If the current allocator does not
                // pool a direct buffer, we use a ThreadLocal based pool.
                ByteBufAllocator alloc = channel.alloc();
                ByteBuf directBuf;
                if (alloc.isDirectBufferPooled()) {
                    directBuf = alloc.directBuffer(readableBytes);
                } else {
                    directBuf = ThreadLocalPooledByteBuf.newInstance();
                }
                directBuf.writeBytes(buf, buf.readerIndex(), readableBytes);
                current(directBuf);
                return directBuf;
            }
        }
        return msg;
    }
}
public boolean remove() {
    if (isEmpty()) {
        return false;
    }
    Entry e = buffer[flushed];
    Object msg = e.msg;
    if (msg == null) {
        // 没有要发送的消息,返回false
        return false;
    }
    ChannelPromise promise = e.promise;
    int size = e.pendingSize;
    // 对Entry进行资源释放
    e.clear();
    // 对需要发送的索引进行更新。
    flushed = flushed + 1 & buffer.length - 1;
    safeRelease(msg);
    promise.trySuccess();
    // 减去已经发送的字节数,会进行发送低水位的判断和事件通知
    decrementPendingOutboundBytes(size);
    return true;
}
@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
    final int expectedWrittenBytes = buf.readableBytes();
    // readBytes方法将当前ByteBuf中的可写字节数组写入到指定的Channel中。
    final int writtenBytes = buf.readBytes(javaChannel(), expectedWrittenBytes);
    return writtenBytes;
}
AbstractNioUnsafe源码分析

AbstractNioUnsafe是AbstractUnsafe类的NIO实现,它主要实现了connect、finishConnect等方法。

connect方法

首先获取当前的连接状态进行缓存,然后发起连接操作:

SocketChannel执行connect()操作有三种可能的结果:

  • 连接成功,返回true;
  • 暂时没有连接上,服务端没有返回ACK应答,连接结果不确定,返回false;
  • 连接失败,直接抛出I/O异常。
// connect()
if (doConnect(remoteAddress, localAddress)) {
    // 成功后触发ChannelActive事件
    fulfillConnectPromise(promise, wasActive);
}else {
    connectPromise = promise;
    requestedRemoteAddress = remoteAddress;
    // Schedule connect timeout.
    int connectTimeoutMillis = config().getConnectTimeoutMillis();
    if (connectTimeoutMillis > 0) {
        // 设置定时任务判断超时
        connectTimeoutFuture = eventLoop().schedule(new Runnable() {
            @Override
            public void run() {
                // 超时之后触发校验,如果发现连接没有完成则关闭连接句柄,释放资源,设置异常堆栈并发起去注册
                ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
                ConnectTimeoutException cause =
                        new ConnectTimeoutException("connection timed out: " + remoteAddress);
                if (connectPromise != null && connectPromise.tryFailure(cause)) {
                    close(voidPromise());
                }
            }
        }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
    }
    promise.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            // 连接成功后如果被取消则关闭连接句柄,释放资源,发起取消注册
            if (future.isCancelled()) {
                if (connectTimeoutFuture != null) {
                    connectTimeoutFuture.cancel(false);
                }
                connectPromise = null;
                close(voidPromise());
            }
        }
    });
}
// NioSocketChannel
@Override
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
    if (localAddress != null) {
        javaChannel().socket().bind(localAddress);
    }

    boolean success = false;
    try {
        boolean connected = javaChannel().connect(remoteAddress);
        if (!connected) {
            selectionKey().interestOps(SelectionKey.OP_CONNECT);
        }
        success = true;
        return connected;
    } finally {
        if (!success) {
            doClose();
        }
    }
}
private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
    // trySuccess() will return false if a user cancelled the connection attempt.
    boolean promiseSet = promise.trySuccess();
    // Regardless if the connection attempt was cancelled, channelActive() event should be triggered,
    // because what happened is what happened.
    if (!wasActive && isActive()) {
        // 触发事件,最终设置OP_READ操作位
        pipeline().fireChannelActive();
    }
    // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
    if (!promiseSet) {
        close(voidPromise());
    }
}
finishConnect方法

客户端接收到服务端的TCP握手应答消息,通过SocketChannel的finishConnect方法对连接结果进行判断。

@Override
public void finishConnect() {
    // Note this method is invoked by the event loop only if the connection attempt was
    // neither cancelled nor timed out.
    assert eventLoop().inEventLoop();
    assert connectPromise != null;
    try {
        boolean wasActive = isActive();
        // 判断是否连接成功
        doFinishConnect();
        // 成功执行,负责修改为监听读操作位
        fulfillConnectPromise(connectPromise, wasActive);
    } catch (Throwable t) {
        // 连接失败,释放资源
        if (t instanceof ConnectException) {
            Throwable newT = new ConnectException(t.getMessage() + ": " + requestedRemoteAddress);
            newT.setStackTrace(t.getStackTrace());
            t = newT;
        }
        // Use tryFailure() instead of setFailure() to avoid the race against cancel().
        connectPromise.tryFailure(t);
        closeIfClosed();
    } finally {
        // Check for null as the connectTimeoutFuture is only created if a connectTimeoutMillis > 0 is used
        // See https://github.com/netty/netty/issues/1770
        // 最后对连接超时进行判断,如果连接超时时仍然没有接收到服务端的ACK应答消息,
        // 则由定时任务关闭客户端连接,将SocketChannel从Reactor线程的多路复用器上摘除,释放资源。
        if (connectTimeoutFuture != null) {
            connectTimeoutFuture.cancel(false);
        }
        connectPromise = null;
    }
}
// 连接成功返回true
// 连接失败返回false
// 发生链路被关闭、链路中断等异常,连接失败
@Override
protected void doFinishConnect() throws Exception {
    if (!javaChannel().finishConnect()) {
        throw new Error();
    }
}
private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
    // trySuccess() will return false if a user cancelled the connection attempt.
    boolean promiseSet = promise.trySuccess();
    // Regardless if the connection attempt was cancelled, channelActive() event should be triggered,
    // because what happened is what happened.
    if (!wasActive && isActive()) {
        pipeline().fireChannelActive();
    }
    // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
    if (!promiseSet) {
        close(voidPromise());
    }
}
NioByteUnsafe源码分析
@Override
public void read() {
    final ChannelConfig config = config();
    final ChannelPipeline pipeline = pipeline();
    final ByteBufAllocator allocator = config.getAllocator();
    final int maxMessagesPerRead = config.getMaxMessagesPerRead();
    // 两种实现:AdaptiveRecvByteBufAllocator、FixedRecvByteBufAllocator
    // 参看下面AdaptiveRecvByteBufAllocator
    RecvByteBufAllocator.Handle allocHandle = this.allocHandle;
    if (allocHandle == null) {
        // 获取TCP参数,首次调用时触发
        this.allocHandle = allocHandle = config.getRecvByteBufAllocator().newHandle();
    }
    if (!config.isAutoRead()) {
        removeReadOp();
    }

    ByteBuf byteBuf = null;
    int messages = 0;
    boolean close = false;
    try {
        // 计算下次预分配缓冲区的容量
        int byteBufCapacity = allocHandle.guess();
        int totalReadAmount = 0;
        do {
            // 分配容量
            byteBuf = allocator.ioBuffer(byteBufCapacity);
            int writable = byteBuf.writableBytes();
            // 异步读取
            int localReadAmount = doReadBytes(byteBuf);
            // 没有就绪可读消息或者发生了异常
            if (localReadAmount <= 0) {
                // not was read release the buffer
                byteBuf.release();
                // 发生了异常,需要关闭
                close = localReadAmount < 0;
                break;
            }
            // 完成一次异步读后,就执行一次触发事件
            // 但并不意味着读到了一条完整消息
            pipeline.fireChannelRead(byteBuf);
            byteBuf = null;

            if (totalReadAmount >= Integer.MAX_VALUE - localReadAmount) {
                // Avoid overflow.
                totalReadAmount = Integer.MAX_VALUE;
                break;
            }
            // 读取字节数累加
            totalReadAmount += localReadAmount;
            // 读取的字节数小于缓冲区可写容量,说明没有就绪的可读字节了
            if (localReadAmount < writable) {
                // Read less than what the buffer can hold,
                // which might mean we drained the recv buffer completely.
                break;
            }
            // 循环读,默认最多16次
        } while (++ messages < maxMessagesPerRead);
        // 读后触发事件
        pipeline.fireChannelReadComplete();
        // 调用接收缓冲区容量分配器的Handler的record方法,将本次读取的总字节数传入方法中进行缓冲区动态分配
        // 为下一次读取选取更合适的缓冲区容量
        allocHandle.record(totalReadAmount);

        if (close) {
            closeOnRead(pipeline);
            close = false;
        }
    } catch (Throwable t) {
        handleReadException(pipeline, byteBuf, t, close);
    }
}
// AdaptiveRecvByteBufAllocator指的是缓冲区大小可以动态调整的ByteBuf分配器。
// 最小缓冲区字节长度
static final int DEFAULT_MINIMUM = 64;
// 初始字节容量
static final int DEFAULT_INITIAL = 1024;
// 最大字节容量
static final int DEFAULT_MAXIMUM = 65536;
// 扩张步长索引
private static final int INDEX_INCREMENT = 4;
// 收缩步长索引
private static final int INDEX_DECREMENT = 1;
// 长度向量表
private static final int[] SIZE_TABLE;
// AdaptiveRecvByteBufAllocator,二分法根据size查找出容量索引
private static int getSizeTableIndex(final int size) {
    // ...
}
private static final class HandleImpl implements Handle {
    // 索引表最小索引
    private final int minIndex;
    // 索引表最大索引
    private final int maxIndex;
    // 当前索引
    private int index;
    // 下一次预分配的Buffer大小
    private int nextReceiveBufferSize;
    // 是否立即收缩
    private boolean decreaseNow;
    // ...
    // 当NioSocketChannel执行完读操作后,会计算获得本次轮询读取的总字节数,它就是参数actualReadBytes,
    // 执行record方法,根据实际读取的字节数对ByteBuf进行动态伸缩和扩张
    public void record(int actualReadBytes) {
        if (actualReadBytes <= SIZE_TABLE[Math.max(0, index - INDEX_DECREMENT - 1)]) {
            if (decreaseNow) {
                index = Math.max(index - INDEX_DECREMENT, minIndex);
                nextReceiveBufferSize = SIZE_TABLE[index];
                decreaseNow = false;
            } else {
                decreaseNow = true;
            }
        } else if (actualReadBytes >= nextReceiveBufferSize) {
            index = Math.min(index + INDEX_INCREMENT, maxIndex);
            nextReceiveBufferSize = SIZE_TABLE[index];
            decreaseNow = false;
        }
    }
}

向量数组的每个值都对应一个Buffer容量,当容量小于512的时候,由于缓冲区已经比较小,需要降低步进值,容量每次下调的幅度要小些; 当大于512时,说明需要解码的消息码流比较大,这时采用调大步进幅度的方式减少动态扩张的频率,所以它采用512的倍数进行扩张。

0-->16              16-->272
1-->32              17-->288
2-->48              18-->304
3-->64              19-->320
4-->80              20-->336
5-->96              21-->352
6-->112             22-->368
7-->120             23-->384
8-->144             24-->400
9-->160             25-->416
10-->176            26-->432
11-->l92            27-->448
12-->208            28-->464
13-->224            29-->480
14-->240            30-->496
15-->256            31-->512

32-->1024           43-->2097152
33-->2048           44-->4194304
34-->4096           45-->8388608
35-->8192           46-->16777216
36-->16384          47-->33554432
37-->32768          48-->67108864
38-->65536          49-->134217728
39-->131072         50-->268435456
40-->262144         51-->536870912
41-->524288         52-->1073741824
42-->1048576

record方法:

  • 首先,对当前索引做步进缩减,然后获取收缩后索引对应的容量,与实际读取的字节数进行比对,如果发现小于收缩后的容量,则重新对当前索引进行赋值,取收缩后的索引和最小索引中的较大者作为最新的索引。
  • 然后,为下一次缓冲区容量分配赋值一新的索引对应容量向量表中的容量。
  • 相反,如果当前实际读取的字节数大于之前预分配的初始容量,则说明实际分配的容量不足,需要动态扩张。
  • 重新计算索引,选取当前索引+扩张步进和最大索引中的较小作为当前索引值,然后对下次缓冲区的容量值进行重新分配,完成缓冲区容量的动态扩张。

使用动态缓冲区分配器的优点:

  • 不同的应用场景,传输的码流大小于差万别,无论初始化分配的是32K还是IM,都会随着应用场景的变化而变得不适应。
    • Netty作为一个通用的NIO框架,并不对用户的应用场景进行假设,可以使用它做流媒体传输,也可以用它做聊天工具。
    • 因此,Netty根据上次实际读取的码流大小对下次的接收Buffer缓冲区进行预测和调整,能够最大限度的满足不同行业的应用场景。
  • 性能更高
    • 容量过大会导致内存占用开销增加,后续的Buffer处理性能会下降;容量过小时需要频繁地内存扩张来接收大的请求消息,同样会导致性能下降。
  • 更节约内存。
    • 假如通常情况下请求消息平均值为1M左右,接收缓冲区大小为1.2M;突然某个客户发送了一个IOM的流媒体附件,接收缓冲区扩张为IOM以接纳该附件,如果缓冲区不能收缩,每次缓冲区创建都会分配IOM的内存,但是后续所有的消息都是lM左右,这样会导致内存的浪费,如果并发客户端过多,可能会发生内存溢出,最终宕机。

ChannelPipeline和ChannelHandler

Netty的ChannelPipeline和ChannelHandler机制类似于Serviet和Filter过滤器,这类拦截器实际上是职责链模式的一种变形,主要是为了方便事件的拦截和用户业务逻辑的定制。

Netty的Channel过滤器实现原理与Servlet Filter机制一致,它将Channel的数据管道抽象为ChannelPipeline,消息在ChannelPipeline中流动和传递。 ChannelPipeline持有I/O事件拦截器ChannelHandler的链表,由ChannelHandler对I/O事件进行拦截和处理, 可以方便地通过新增和删除ChannelHandler来实现不同的业务逻辑定制,不需要对已有的ChannelHandler进行修改,能够实现对修改封闭和对扩展的支持。

ChannelPipeline功能说明

ChannelPipeline是ChannelHandler的容器,它负责ChannelHandler的管理和事件拦截与调度。

ChannelPipeline的事件处理

消息的读取和发送处理全流程描述如下:

  • 底层的SocketChannel read()方法读取ByteBuf,触发ChannelRead事件,由I/O线程NioEventLoop调用ChannelPipeline的fireChannelRead(Object msg)方法,将消息(ByteBuf)传输到ChannelPipeline中;
  • 消息依次被HeadHandler、ChannelHandler1、ChannelHandler2……TailHandler拦截和处理,在这个过程中,任何ChannelHandler都可以中断当前的流程,结束消息的传递;
  • 调用ChannelHandlerContext的write方法发送消息,消息从TailHandler开始,途经ChannelHandlerN……ChannelHandler1、HeadHandler,最终被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息的传递,例如当编码失败时,就需要中断流程,构造异常的Future返回。

ChannelPipeline对事件流的拦截和处理流程:

												I/O Request
                                          via {@link Channel} or
                                      {@link ChannelHandlerContext}
                                                    |
+---------------------------------------------------+---------------+
|                           ChannelPipeline         |               |
|                                                  \|/              |
|    +----------------------------------------------+----------+    |
|    |                   ChannelHandler  N                     |    |
|    +----------+-----------------------------------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+-----------------------------------+----------+    |
|    |                   ChannelHandler N-1                    |    |
|    +----------+-----------------------------------+----------+    |
|              /|\                                  .               |
|               .                                   .               |
| ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
|          [method call]                      [method call]         |
|               .                                   .               |
|               .                                  \|/              |
|    +----------+-----------------------------------+----------+    |
|    |                   ChannelHandler  2                     |    |
|    +----------+-----------------------------------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+-----------------------------------+----------+    |
|    |                   ChannelHandler  1                     |    |
|    +----------+-----------------------------------+----------+    |
|              /|\                                  |               |
+---------------+-----------------------------------+---------------+
                |                                  \|/
+---------------+-----------------------------------+---------------+
|               |                                   |               |
|       [ Socket.read() ]                    [ Socket.write() ]     |
|                                                                   |
|  Netty Internal I/O Threads (Transport Implementation)            |
+-------------------------------------------------------------------+

Netty中的事件分为inbound事件和outbound事件。

  • inbound事件通常由I/O线程触发,例如TCP链路建立事件、链路关闭事件、读事件、异常通知事件等,它对应上图的左半部分。
    • 触发inbound事件的方法:
      • public ChannelPipeline fireChannelRegistered():Channel注册事件
      • public ChannelHandlerContext fireChannelActive():TCP链路建立成功,Channel激活事件
      • public ChannelHandlerContext fireChannelRead(Object msg):读事件
      • public ChannelHandlerContext fireChannelReadComplete() :读操作完成通知事件
      • public ChannelHandlerContext fireExceptionCaught(Throwable cause):异常通知事件
      • public ChannelHandlerContext fireUserEventTriggered(Object event):用户自定义事件
      • public ChannelHandlerContext fireChannelWritabilityChanged():Channel的可写状态变化通知事件
      • public ChannelHandlerContext fireChannelInactive():TCP连接关闭,链路不可用通知事件
  • outbound事件通常是由用户主动发起的网络I/O操作,例如用户发起的连接操作、绑定操作、消息发送等操作,它对应上图的右半部分。
    • 触发outbound事件的方法:
      • public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise):绑定本地地址事件
      • public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise):连接服务端事件
      • public ChannelFuture write(Object msg, ChannelPromise promise):发送事件
      • public ChannelHandlerContext flush():刷新事件
      • public ChannelHandlerContext read():读事件
      • public ChannelFuture disconnect(ChannelPromise promise):断开连接事件
      • public ChannelFuture close(ChannelPromise promise):关闭当前Channel事件
自定义拦截器

ChannelPipeline通过ChannelHandler接口来实现事件的拦截和处理,由于ChannelHandler中的事件种类繁多,不同的ChannelHandler可能只需要关心其中的某一个或者几个事件, 所以,通常ChannelHandler只需要继承ChannelHandlerAdapter类覆盖自己关心的方法即可。

// 链路建立成功
public class MyInboundHandler extends ChannelHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("TCP Connected");
        ctx.fireChannelActive();
    }
}
// 链路关闭释放资源
public class MyOutboundHandler extends ChannelHandlerAdapter {
    @Override
    public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
        System.out.println("TCP Closing");
        // do cleaning...
        ctx.close(promise);
    }
}
构建Pipeline

用户不需要自己创建pipeline,因为使用ServerBootstrap或者Bootstrap启动服务端或者客户端时, Netty会为每个Channel连接创建一个独立的pipeline。对于使用者而言,只需要将自定义的拦截器加入到pipeline中即可。

pipeline = ch.pipeline();
pipeline.addLast("decoder", new MyProtocolDecoder());
pipeline.addLasst("encoder ", new MyProtocolEncoder());

对于类似编解码这样的ChannelHandler,它存在先后顺序,如MessageToMessageDecoder,在它之前往往需要有ByteToMessageDecoder将ByteBuf解码为对象。 然后对对象做二次解码得到最终的POJO对象。Pipeline支持指定位置添加或者删除拦截器:

按顺序添加ChannelHandler

ChannelPipeline的主要特性

ChannelPipeline支持运行态动态的添加或者删除ChannelHandler,在某些场景下这个特性非常实用。

例如当业务高峰期需要对系统做拥塞保护时,就可以根据当前的系统时间进行判断,如果处于业务高峰期,则动态地将系统拥塞保护ChannelHandler添加到当前的ChannelPipeline中,当高峰期过去之后,就可以动态删除拥塞保护ChannelHandler了。

ChannelPipeline是线程安全的,这意味着N个业务线程可以并发地操作ChannelPipeline而不存在多线程并发问题。
但是,ChannelHandler却不是线程安全的,这意味着尽管ChannelPipeline是线程安全的,但是用户仍然需要自己保证ChannelHandler的线程安全。

ChannelPipeline源码分析

ChannelPipeline的代码相对比较简单,它实际上是一个ChannelHandler的容器,内部维护了一个ChannelHandler的链表和迭代器,可以方便地实现ChannelHandler查找、添加、替换和删除。

ChannelPipeline的类继承关系图

ChannelPipeline类继承关系图

ChannelPipeline对ChannelHandler的管理
@Override
public ChannelPipeline addBefore(ChannelHandlerInvoker invoker, 
                                    String baseName, final String name, ChannelHandler handler) {
    synchronized (this) {
        // 根据ChannelHandler名获取ChannelHandlerContext实例,关系由ChannelPipeline维护
        DefaultChannelHandlerContext ctx = getContextOrDie(baseName);
        // 重复性校验
        checkDuplicateName(name);
        // 新增一个Context
        DefaultChannelHandlerContext newCtx = new DefaultChannelHandlerContext(this, invoker, name, handler);
        addBefore0(name, ctx, newCtx);
    }
    return this;
}
private void addBefore0(final String name, DefaultChannelHandlerContext ctx, DefaultChannelHandlerContext newCtx) {
    // 重复性校验
    checkMultiplicity(newCtx);
    newCtx.prev = ctx.prev;
    newCtx.next = ctx;
    ctx.prev.next = newCtx;
    ctx.prev = newCtx;
    // map关系映射
    name2ctx.put(name, newCtx);
    // 发送新增ChannelHandlerContext通知消息
    callHandlerAdded(newCtx);
}
private static void checkMultiplicity(ChannelHandlerContext ctx) {
    ChannelHandler handler = ctx.handler();
    if (handler instanceof ChannelHandlerAdapter) {
        ChannelHandlerAdapter h = (ChannelHandlerAdapter) handler;
        // ChannelHandlerContext不可以在多个ChannelPipeline中共享
        if (!h.isSharable() && h.added) {
            throw new ChannelPipelineException(
                    h.getClass().getName() +
                    " is not a @Sharable handler, so can't be added or removed multiple times.");
        }
        h.added = true;
    }
}

ChannelHandlerContext位置指针迁移图:

ChannelHandlerContext位置指针迁移图

ChannelPipeline的inbound事件

当发生某个I/O事件的时候,例如链路建立、链路关闭、读取操作完成等,都会产生一个事件,事件在pipeline中得到传播和处理,它是事件处理的总入口。 由于网络I/O相关的事件有限,因此Netty对这些事件进行了统一抽象,Netty自身和用户的ChannelHandler会对感兴趣的事件进行拦截和处理。

pipeline中以fireXXX命名的方法都是从IO线程流向用户业务Handler的inbound事件,它们的实现因功能而异,但是处理步骤类似:

  • 调用HeadHandler对应的fireXXX方法;
  • 执行事件相关的逻辑操作。

以active为例:

// DefaultChannelPipeline
@Override
public ChannelPipeline fireChannelActive() {
    head.fireChannelActive();

    if (channel.config().isAutoRead()) {
        channel.read();
    }

    return this;
}
ChannelPipeline的outbound事件

由用户线程或者代码发起的I/O操作被称为outbound事件,inbound和outbound是Netty自身根据事件在pipeline中的流向抽象出来的术语,在其他NIO框架中并没有这个概念。

outbound事件相关方法:

inbound事件相关方法

Pipeline负责将I/O事件通过TailHandler进行调度和传播,最终调用Unsafe的I/O方法进行I/O操作:

// DefaultChannelPipeline
@Override
public ChannelFuture connect(SocketAddress remoteAddress) {
    // 直接调用tail的connect方法,最终会调用head的connect方法
    return tail.connect(remoteAddress);
}
// HeadHandler
@Override
public void connect(
        ChannelHandlerContext ctx,
        SocketAddress remoteAddress, SocketAddress localAddress,
        ChannelPromise promise) throws Exception {
    unsafe.connect(remoteAddress, localAddress, promise);
}

ChannelHandler功能说明

ChannelHandler类似于Servlet的Filter过滤器,负责对I/O事件或者I/O操作进行拦截和处理,它可以选择性地拦截和处理自己感兴趣的事件,也可以透传和终止事件的传递。

基于ChannelHandler接口,用户可以方便地进行业务逻辑定制,例如打印日志、统一封装异常信息、性能统计和消息编解码等。

ChannelHandler支持注解,目前支持的注解有两种:

  • Sharable:多个ChannelPipeline共用同一个ChannelHandler;
  • Skip:被Skip注解的方法不会被调用,直接被忽略。
ChannelHandlerAdapter功能说明

Netty提供了ChannelHandlerAdapter基类,它的所有接口实现都是事件透传,如果用户ChannelHandler关心某个事件,只需要覆盖ChannelHandlerAdapter对应的方法即可, 对于不关心的,可以直接继承使用父类的方法,这样子类的代码就会非常简洁和清晰。

这些透传方法被@Skip注解了,这些方法在执行的过程中会被忽略。

@Skip
@Override
public void read(ChannelHandlerContext ctx) throws Exception {
    ctx.read();
}
@Skip
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ctx.write(msg, promise);
}
ByteToMessageDecoder功能说明

利用NIO进行网络编程时,往往需要将读取到的字节数组或者字节缓冲区解码为业务可以使用的POJO对象。 为了方便业务将ByteBuf解码成业务POJO对象,Netty提供了ByteToMessageDecoder抽象工具解码类。

用户的解码器继承ByteToMessageDecoder,只需要实现void decode(ChannelHandlerContext ctx,ByteBufin,List<Object> out)抽象方法即可完成ByteBuf到POJO对象的解码。

由于ByteToMessageDecoder并没有考虑TCP粘包和组包等场景,读半包需要用户解码器自己负责处理。 正因为如此,对于大多数场景不会直接继承ByteToMessageDecoder,而是继承另外一些更高级的解码器来屏蔽半包的处理。

MessageToMessageDecoder功能说明

MessageToMessageDecoder实际上是Netty的二次解码器,它的职责是将一个对象二次解码为其他对象。

从SocketChannel读取到的TCP数据报是ByteBuffer,实际就是字节数组,我们首先需要将ByteBuffer缓冲区中的数据报读取出来,并将其解码为Java对象; 然后对Java对象根据某些规则做二次解码,将其解码为另一个POJO对象。因为MessageToMessageDecoder在ByteToMessageDecoder之后,所以称之为二次解码器。 例如第一次解码成Request对象,第二次将Request中的XML解码成POJO对象。

用户的解码器只需要实现void decode(ChannelHandlerContext ctx,I msg,List<Object> out)抽象方法即可,由于它是将一个POJO解码为另一个POJO,所以一般不会涉及到半包的处理,相对于ByteToMessageDecoder更加简单些。

LengthFieldBasedFrameDecoder功能说明

TCP的粘包导致解码的时候需要考虑如何处理半包的问题,Netty提供了半包解码器LineBasedFrameDecoder和DelirniterBasedFrameDecoder,第三种最通用的半包解码器LengthFieldBasedFrameDecoder。

如何区分一个整包消息,通常有4种做法:

  • 固定长度,例如每120个字节代表一个整包消息,不足的前面补零。解码器在处理这类定常消息的时候比较简单,每次读到指定长度的字节后再进行解码。
  • 通过回车换行符区分消息,例如FTP协议。这类区分消息的方式多用于文本协议。
  • 通过分隔符区分整包消息。
  • 通过指定长度来标识整包消息。

如果消息是通过长度进行区分的,LengthFieldBasedFrameDecoder都可以自动处理粘包和半包问题,只需要传入正确的参数,即可轻松搞定“读半包”问题。

// 长度标识字段的offset
lengthFieldOffset   = 1
// 长度标识字段的长度
lengthFieIdlength   = 2
// 读取字段时的offset,负数可以取到长度标识字段
lengthAdjustment    = 1
// 读取字段时的跳过的长度
initialBytesToStrip = 3

通过4个参数的不同组合,可以达到不同的解码效果,用户在使用过程中可以根据业务的实际情况进行灵活调整。

由于TCP存在粘包和组包问题,所以通常情况下必须自己处理半包消息。利用LengtbFieldBasedFrameDecoder解码器可以自动解决半包问题:

ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(1024, 0, 4));
pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8));

在pipeline中增加LengthFieldBasedFrameDecoder解码器,指定正确的参数组合,它可以将Netty的ByteBuf解码成单个的整包消息, 后面的业务解码器拿到的就是个完整的数据报,正常进行解码即可,不再需要额外考虑“读半包”问题,方便了业务消息的解码。

MessageToByteEncoder功能说明

MessageToByteEncoder负责将POJO对象编码成ByteBuf,用户的编码器继承MessageToByteEncoder,实现void encode(ChannelHandlerContext ctx,I msg,ByteBuf out)接口:

public class IntegerEncoder extends MessageToByteEncoder<Integer> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {
        out.writeInt(msg);
    }
}
MessageToMessageEncoder功能说明

将一个POJO对象编码成另一个对象,以HTTP+XML协议为例,它的一种实现方式是:

先将POJO对象编码成XML字符串,再将字符串编码为HTTP请求或者应答消息。 对于复杂协议,往往需要经历多次编码,为了便于功能扩展,可以通过多个编码器组合来实现相关功能。

用户的解码器继承MessageToMessageEncoder解码器,实现void encode(ChannelHandlerContext ctx, I msg, List<Object> out)方法即可。 注意,它与MessageToByteEncoder的区别是输出是对象列表而不是ByteBuf。

public class IntegerToStringEncoder extends MessageToMessageEncoder<Integer> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
        out.add(msg.toString());
    }
}
LengthFieldPrepender功能说明

如果协议中的第一个字段为长度字段,Netty提供了LengthFieldPrepender编码器,它可以计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中。

通过LengthFieldPrepender可以将待发送消息的长度写入到ByteBuf的前2个字节,编码后的消息组成为长度字段+原消息的方式。

通过设置LengthFieldPrepender为true,消息长度将包含长度本身占用的字节数。

ChannelHandler源码分析

ChannelHandler的类继承关系图

由于ChannelHandler是Netty框架和用户代码的主要扩展和定制点,所以它的子类种类繁多、功能各异。

系统ChannelHandler主要分类:

  • ChannelPipeline的系统ChannelHandler,用于I/O操作和对事件进行预处理,对于用户不可见,这类ChannelHandler主要包括HeadHandler和TailHandler;
  • 编解码ChannelHandler,包括ByteToMessageCodec、MessageToMessageDecoder等,这些编解码类本身又包含多种子类。
  • 其他系统功能性ChannelHandler,包括流量整型Handler、读写超时Handler、日志Handler等。

编解码ChannelHandler

编解码ChannelHandler子类继承关系图

ByteToMessageDecoder源码解析
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    // 只对ByteBuf解码
    if (msg instanceof ByteBuf) {
        RecyclableArrayList out = RecyclableArrayList.newInstance();
        try {
            ByteBuf data = (ByteBuf) msg;
            // 通过cumulation是否为空判断解码器是否缓存了没有解码完成的半包消息
            first = cumulation == null;
            if (first) {
                // 如果为空,说明是首次解码或者最近一次已经处理完了半包消息,没有缓存的半包消息需要处理,直接将需要解码的ByteBuf赋值给cumulation;
                cumulation = data;
            } else {
                // 如果cumulation缓存有上次没有解码完成的ByteBuL则进行复制操作,将需要解码的ByteBuf复制到cumulation中
                if (cumulation.writerIndex() > cumulation.maxCapacity() - data.readableBytes()) {
                    // 空间不足扩展
                    expandCumulation(ctx, data.readableBytes());
                }
                cumulation.writeBytes(data);
                data.release();
            }
            // 解码
            callDecode(ctx, cumulation, out);
        } catch (DecoderException e) {
            throw e;
        } catch (Throwable t) {
            throw new DecoderException(t);
        } finally {
            if (cumulation != null && !cumulation.isReadable()) {
                cumulation.release();
                cumulation = null;
            }
            int size = out.size();
            decodeWasNull = size == 0;

            for (int i = 0; i < size; i ++) {
                ctx.fireChannelRead(out.get(i));
            }
            out.recycle();
        }
    } else {
        ctx.fireChannelRead(msg);
    }
}

通过cumulation是否为空判断解码器是否缓存了没有解码完成的半包消息:

  • 如果为空,说明是首次解码或者最近一次已经处理完了半包消息,没有缓存的半包消息需要处理,直接将需要解码的ByteBuf赋值给cumulation;
  • 如果cumulation缓存有上次没有解码完成的ByteBuf则进行复制操作,将需要解码的ByteBuf复制到cumulation中

原理

// 扩展空间
private void expandCumulation(ChannelHandlerContext ctx, int readable) {
    ByteBuf oldCumulation = cumulation;
    // 重新分配一个
    cumulation = ctx.alloc().buffer(oldCumulation.readableBytes() + readable);
    // 复制
    cumulation.writeBytes(oldCumulation);
    // 释放
    oldCumulation.release();
}
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        // 可以read就循环解码
        while (in.isReadable()) {
            int outSize = out.size();
            int oldInputLength = in.readableBytes();
            // 子类解码器进行解码
            decode(ctx, in, out);
            // Check if this handler was removed before continuing the loop.
            // If it was removed, it is not safe to continue to operate on the buffer.
            //
            // See https://github.com/netty/netty/issues/1664
            // Context被移除就退出
            if (ctx.isRemoved()) {
                break;
            }
            // 长度没变化,说明解码没有成功
            if (outSize == out.size()) {
                // 如果用户解码器没有消费ByteBuf,说明半包需要退出循环
                if (oldInputLength == in.readableBytes()) {
                    break;
                } else {
                    // 消费了继续进行
                    continue;
                }
            }
            // 没有消费ByteBuf却解码出了对象,抛出异常
            if (oldInputLength == in.readableBytes()) {
                throw new DecoderException(
                        StringUtil.simpleClassName(getClass()) +
                        ".decode() did not read anything but decoded a message.");
            }
            // 如果是单条消息解码器,第一次完成就退出
            if (isSingleDecode()) {
                break;
            }
        }
    } catch (DecoderException e) {
        throw e;
    } catch (Throwable cause) {
        throw new DecoderException(cause);
    }
}
MessageToMessageDecoder源码解析

MessageToMessageDecoder负责将一个POJO对象解码成另一个POJO对象。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    RecyclableArrayList out = RecyclableArrayList.newInstance();
    try {
        if (acceptInboundMessage(msg)) {
            // 可接受类型就解码
            @SuppressWarnings("unchecked")
            I cast = (I) msg;
            try {
                decode(ctx, cast, out);
            } finally {
                ReferenceCountUtil.release(cast);
            }
        } else {
            // 否则加入到可循环利用的list
            out.add(msg);
        }
    } catch (DecoderException e) {
        throw e;
    } catch (Exception e) {
        throw new DecoderException(e);
    } finally {
        // 自己不能处理的交由后续Handler处理
        int size = out.size();
        for (int i = 0; i < size; i ++) {
            ctx.fireChannelRead(out.get(i));
        }
        // 释放
        out.recycle();
    }
}
LengthFieldBasedFrameDecoder源码解析

最通用和重要的解码器,继续消息长度的半包解码器。

@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    // 调用内部的解码器,如果成功,放入out中
    Object decoded = decode(ctx, in);
    if (decoded != null) {
        out.add(decoded);
    }
}
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
    // 判断是否需要丢弃到offset位置
    if (discardingTooLongFrame) {
        long bytesToDiscard = this.bytesToDiscard;
        // 丢弃大小不超过待处理缓冲区大小
        int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
        // skip计算好的大小
        in.skipBytes(localBytesToDiscard);
        // 减去已经忽略的长度
        bytesToDiscard -= localBytesToDiscard;
        this.bytesToDiscard = bytesToDiscard;
        // 判断是否已经达到需要忽略的字节数
        failIfNecessary(false);
    }
    // 小于偏移量,继续读取
    if (in.readableBytes() < lengthFieldEndOffset) {
        return null;
    }
    // 通过读索引和lengthFieldOffset计算获取实际的长度字段索引
    int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
    // 根据长度字段自身大小选区不同的get方式获取长度值
    long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieIdlength, byteOrder);
    // 合法性判断
    if (frameLength < 0) {
        in.skipBytes(lengthFieldEndOffset);
        throw new CorruptedFrameException(
                "negative pre-adjustment length field: " + frameLength);
    }
    // 进行长度修正
    frameLength += lengthAdjustment + lengthFieldEndOffset;
    // 修正后小于lengthFieldEndOffset,说明是非法数据
    if (frameLength < lengthFieldEndOffset) {
        in.skipBytes(lengthFieldEndOffset);
        throw new CorruptedFrameException(
                "Adjusted frame length (" + frameLength + ") is less " +
                "than lengthFieldEndOffset: " + lengthFieldEndOffset);
    }
    // 长度大于ByteBuf的最大容量
    if (frameLength > maxFrameLength) {
        // 长度是否小于总可读字节数,discard为继续丢弃的字节数
        long discard = frameLength - in.readableBytes();
        tooLongFrameLength = frameLength;
        if (discard < 0) {
            // buffer contains more bytes then the frameLength so we can discard all now
            in.skipBytes((int) frameLength);
        } else {
            // Enter the discard mode and discard everything received so far.
            // 需要设置标识继续丢弃,继续半包处理
            discardingTooLongFrame = true;
            bytesToDiscard = discard;
            in.skipBytes(in.readableBytes());
        }
        // 如果超出了上限,则报告失败
        failIfNecessary(true);
        return null;
    }
    // never overflows because it's less than maxFrameLength
    int frameLengthInt = (int) frameLength;
    // 可读小,是半包
    if (in.readableBytes() < frameLengthInt) {
        return null;
    }
    // 需要忽略的消息头字段大,则非法
    if (initialBytesToStrip > frameLengthInt) {
        in.skipBytes(frameLengthInt);
        throw new CorruptedFrameException(
                "Adjusted frame length (" + frameLength + ") is less " +
                "than initialBytesToStrip: " + initialBytesToStrip);
    }
    in.skipBytes(initialBytesToStrip);

    // extract frame
    int readerIndex = in.readerIndex();
    int actualFrameLength = frameLengthInt - initialBytesToStrip;
    // 获取解码后的整包消息缓冲区
    // 根据实际长度分配一个新的缓冲区,辅助数据到新缓冲区
    ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
    // 设置读索引
    in.readerIndex(readerIndex + actualFrameLength);
    return frame;
}
// 如果超出了上限,则报告失败。达到需要忽略的字节数后对discardingTooLongFrame等进行置位
private void failIfNecessary(boolean firstDetectionOfTooLongFrame) {
    if (bytesToDiscard == 0) {
        // Reset to the initial state and tell the handlers that
        // the frame was too large.
        long tooLongFrameLength = this.tooLongFrameLength;
        this.tooLongFrameLength = 0;
        discardingTooLongFrame = false;
        if (!failFast ||
            failFast && firstDetectionOfTooLongFrame) {
            fail(tooLongFrameLength);
        }
    } else {
        // Keep discarding and notify handlers if necessary.
        if (failFast && firstDetectionOfTooLongFrame) {
            fail(tooLongFrameLength);
        }
    }
}

实际不需要对LeogthFieldBasedFrameDecoder进行定制。只需要了解每个参数的用法,再结合用户的业务场景进行参数设置,即可实现半包消息的自动解码, 后面的业务解码器得到的是个完整的整包消息,不用再额外考虑如何处理半包。这极大地降低了开发难度,提升了开发效率。

MessageToByteEncoder源码解析

MessageToByteEncoder负责将用户的POJO对象编码成ByteBuf,以便通过网络进行传输。

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ByteBuf buf = null;
    try {
        // 是否接受消息
        if (acceptOutboundMessage(msg)) {
            @SuppressWarnings("unchecked")
            I cast = (I) msg;
            // 判断缓冲区类型进行分配缓冲区
            if (preferDirect) {
                buf = ctx.alloc().ioBuffer();
            } else {
                buf = ctx.alloc().heapBuffer();
            }
            // 编码
            try {
                encode(ctx, cast, buf);
            } finally {
                // 释放
                ReferenceCountUtil.release(cast);
            }
            // 写或者写空
            if (buf.isReadable()) {
                ctx.write(buf, promise);
            } else {
                buf.release();
                ctx.write(Unpooled.EMPTY_BUFFER, promise);
            }
            buf = null;
        } else {
            ctx.write(msg, promise);
        }
    } catch (EncoderException e) {
        throw e;
    } catch (Throwable e) {
        throw new EncoderException(e);
    } finally {
        if (buf != null) {
            buf.release();
        }
    }
}
MessageToMessageEncoder源码解析

MessageToMessageEncoder负责将一个POJO对象编码成另一个POJO对象。

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    RecyclableArrayList out = null;
    try {
        // 是否可接受
        if (acceptOutboundMessage(msg)) {
            out = RecyclableArrayList.newInstance();
            @SuppressWarnings("unchecked")
            I cast = (I) msg;
            try {
                // 编码
                encode(ctx, cast, out);
            } finally {
                ReferenceCountUtil.release(cast);
            }
            // 空则编码失败,释放资源抛出异常
            if (out.isEmpty()) {
                out.recycle();
                out = null;
                throw new EncoderException(
                        StringUtil.simpleClassName(this) + " must produce at least one message.");
            }
        } else {
            ctx.write(msg, promise);
        }
    } catch (EncoderException e) {
        throw e;
    } catch (Throwable t) {
        throw new EncoderException(t);
    } finally {
        if (out != null) {
            final int sizeMinusOne = out.size() - 1;
            if (sizeMinusOne >= 0) {
                for (int i = 0; i < sizeMinusOne; i ++) {
                    ctx.write(out.get(i));
                }
                ctx.write(out.get(sizeMinusOne), promise);
            }
            out.recycle();
        }
    }
}
LengthFieldPrepender源码解析

LengthFieldPrepender负责在待发送的ByteBuf消息头中增加一个长度字段来标识消息的长度,它简化了用户的编码器开发,使用户不需要额外去设置这个长度字段。

首先对长度字段进行设置,如果需要包含消息长度自身,则在原来长度的基础之上再加上lengthFieIdlength的长度。

@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
    int length = msg.readableBytes() + lengthAdjustment;
    if (lengthIncludesLengthFieIdlength) {
        length += lengthFieIdlength;
    }
    // 调整后小于0,非法
    if (length < 0) {
        throw new IllegalArgumentException(
                "Adjusted frame length (" + length + ") is less than zero");
    }
    // 对长度进行判断,以便使用正确的方法将长度字段写入到ByteBuf中
    // 根据字节数和限定范围写入,其它长度抛错
    switch (lengthFieIdlength) {
    case 1:
        if (length >= 256) {
            throw new IllegalArgumentException(
                    "length does not fit into a byte: " + length);
        }
        out.add(ctx.alloc().buffer(1).writeByte((byte) length));
        break;
    case 2:
        if (length >= 65536) {
            throw new IllegalArgumentException(
                    "length does not fit into a short integer: " + length);
        }
        out.add(ctx.alloc().buffer(2).writeShort((short) length));
        break;
    case 3:
        if (length >= 16777216) {
            throw new IllegalArgumentException(
                    "length does not fit into a medium integer: " + length);
        }
        out.add(ctx.alloc().buffer(3).writeMedium(length));
        break;
    case 4:
        out.add(ctx.alloc().buffer(4).writeInt(length));
        break;
    case 8:
        out.add(ctx.alloc().buffer(8).writeLong(length));
        break;
    default:
        throw new Error("should not reach here");
    }
    // 继续添加msg对象
    out.add(msg.retain());
}

EventLoop和EventLoopGroup

Netty框架的主要线程就是I/O线程,线程模型设计的好坏,决定了系统的吞吐量、并发性和安全性等架构质量属性。

Netty的线程模型被精心地设计,既提升了框架的并发性能,又能在很大程度避免锁,局部实现了无锁化设计。

Netty的线程模型

尽管不同的NIO框架对于Reactor模式的实现存在差异,但本质上还是遵循了Reactor的基础线程模型。

Reactor单线程模型

Reactor单线程模型,是指所有的I/O操作都在同一个NIO线程上面完成。NIO线程的职责:

  • 作为NIO服务端,接收客户端的TCP连接;
  • 作为NIO客户端,向服务端发起TCP连接;
  • 读取通信对端的请求或者应答消息;
  • 向通信对端发送消息请求或者应答消息。

Reactor单线程模型:

Reactor单线程模型

由于Reactor模式使用的是异步非阻塞I/O,所有的I/O操作都不会导致阻塞,理论上一个线程可以独立处理所有I/O相关的操作。 从架构层面看,一个NIO线程确实可以完成其承担的职责。

例如,通过Acceptor类接收客户端的TCP连接请求消息,当链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上,进行消息解码。 用户线程消息编码后通过NIO线程将消息发送给客户端。

在一些小容量应用场景下,可以使用单线程模型。但是这对于高负载、大并发的应用场景却不合适:

  • 一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送。
  • 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。
  • 可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
Reactor多线程模型

Rector多线程模型与单线程模型最大的区别就是有一组NIO线程来处理I/O操作:

Reactor多线程模型:

Reactor多线程模型

Reactor多线程模型的特点:

  • 有专门一个NIO线程:Acceptor线程用于监听服务端,接收客户端的TCP连接请求。
  • 网络I/O操作一读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送。
  • 一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题。

在绝大多数场景下,Reactor多线程模型可以满足性能需求。但是,在个别特殊场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。 例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。 在这类场景下,单独一个Acceptor线程可能会存在性能不足的问题,为了解决性能问题,产生了第三种Reactor线程模型-主从Reactor多线程模型。

主从Reactor线程模型

主从Reactor线程模型的特点是:服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池。

  • Acceptor接收到客户端TCP连接请求并处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到I/O线程池(sub reactor线程池)的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。
  • Acceptor线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的I/O线程上,由I/O线程负责后续的I/O操作。

主从Reactor线程模型:

主从Reactor线程模型

利用主从NIO线程模型,可以解决一个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在Netty的官方demo中,推荐使用该线程模型。

Netty的线程模型

Netty的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。 通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型、多线程模型和主从Reactor多线层模型。

Netty的线程模型:

Netty的线程模型

Netty 4.X之后的线程模型:

Netty 4.X之后的线程模型

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
//...

服务端启动的时候,创建了两个NioEventLoopGroup,它们实际是两个独立的Reactor线程池。 一个用于接收客户端的TCP连接,另一个用于处理I/O相关的读写操作,或者执行系统Task、定时任务Task等。

Netty用于接收客户端请求的线程池职责:

  • 接收客户端TCP连接,初始化Channel参数;
  • 将链路状态变更事件通知给ChannelPipeline。

Netty处理I/O操作的Reactor线程池职责:

  • 异步读取通信对端的数据报,发送读事件到ChannelPipeline;
  • 异步发送消息到通信对端,调用ChannelPipeline的消息发送接口;
  • 执行系统调用Task;
  • 执行定时任务Task,例如链路空闲状态监测定时任务。

通过调整线程池的线程个数、是否共享线程池等方式,Netty的Reactor线程模型可以在单线程、多线程和主从多线程间切换,这种灵活的配置方式可以最大程度地满足不同用户的个性化定制。

为了尽可能地提升性能,Netty在很多地方进行了无锁化的设计,例如在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降问题。 表面上看,串行化设计似乎CPU利用率不高,并发程度不够。 但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列一多个工作线程的模型性能更优。

Netty Reactor线程模型:

Netty Reactor线程模型

Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead(Object msg)。 只要用户不主动切换线程,一直都是由NioEventLoop调用用户的Handler,期间不进行线程切换。 这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。

最佳实践

Netty的多线程编程最佳实践:

  • 创建两个NioEventLoopGroup,用于逻辑隔离NIOAcceptor和NIO I/O线程。
  • 尽量不要在ChannelHandler中启动用户线程(解码后用于将POJO消息派发到后端业务线程的除外)。
  • 解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程中完成消息的解码。
  • 如果业务逻辑操作非常简单,没有复杂的业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作、数据库操作、网路操作等,可以直接在NIO线程上完成业务逻辑编排,不需要切换到用户线程。
  • 如果业务逻辑处理复杂,不要在NIO线程上完成,建议将解码后的POJO消息封装成Task,派发到业务线程池中由业务线程执行,以保证NIO线程尽快被释放,处理其他的I/O操作。

推荐的线程数量计算公式:

公式一:线程数量 = (线程总时间/瓶颈资源时间) x 瓶颈资源的线程并行数 公式二: QPS = 1OOO/线程总时间 x 线程数。

由于用户场景的不同,对于一些复杂的系统,实际上很难计算出最优线程配置,只能是根据测试数据和用户场景,结合公式给出一个相对合理的范围,然后对范围内的数据进行性能测试,选择相对最优值。

NioEventLoop源码分析

NioEventLoop设计原理

Netty的NioEventLoop并不是一个纯粹的I/O线程,它除了负责I/O的读写之外,还兼顾处理以下两类任务:

  • 系统Task:通过调用NioEventLoop的execute(Runnable task)方法实现,
    • Netty有很多系统Task,创建它们的主要原因是:当I/O线程和用户线程同时操作网络资源时,为了防止并发操作导致的锁竞争,将用户线程的操作封装成Task放入消息队列中,由I/O线程负责执行,这样就实现了局部无锁化。
  • 定时任务:通过调用NioEventLoop的schedule(Runnable command,long delay,TimeUnitunit)方法实现。

正是因为NioEventLoop具备多种职责,所以它的实现比较特殊,它并不是个简单的Runnable。

NioEventLoop继承关系

它实现了EventLoop接口、EventExecutorGroup接口和ScheduledExecutorService接口,正是因为这种设计,导致NioEventLoop和其父类功能实现非常复杂。

NioEventLoop

作为NIO框架的Reactor线程,NioEventLoop需要处理网络I/O读写事件,因此它必须聚合一个多路复用器对象。

Selector selector;
private SelectedSelectionKeySet selectedKeys;

private final SelectorProvider provider;

Selector的初始化非常简单,直接调用Selector.open()方法就能创建并打开一个新的Selector。

Netty对Selector的selectedKeys进行了优化,用户可以通过io.netty.noKeySetOptimization开关决定是否启用该优化项。默认不打开selectedKeys优化功能。

private Selector openSelector() {
    final Selector selector;
    try {
        selector = provider.openSelector();
    } catch (IOException e) {
        throw new ChannelException("failed to open a new selector", e);
    }
    // 没有打开优化开关直接返回
    if (DISABLE_KEYSET_OPTIMIZATION) {
        return selector;
    }
    // 如果打开了开关就通过反射替换原有的selectedKeys为包装类
    // ...
}
@Override
protected void run() {
    // 无限循环,直到接收到退出指令
    for (;;) {
        // 设置wakenUp为false并保存状态
        oldWakenUp = wakenUp.getAndSet(false);
        try {
            // 判断是否有消息尚未处理
            if (hasTasks()) {
                // 如果有立即进行一次select操作,看是否有就绪的Channel需要处理
                selectNow();
            } else {
                // 如果消息队列中没有消息需要处理,则执行select()方法,由Selector多路复用器轮询,看是否有准备就绪的Channel。
                select();

                if (wakenUp.get()) {
                    selector.wakeup();
                }
            }

            cancelledKeys = 0;
            // 轮询到了就绪状态的SocketChannel
            final long ioStartTime = System.nanoTime();
            needsToSelectAgain = false;
            if (selectedKeys != null) {
                processSelectedKeysOptimized(selectedKeys.flip());
            } else {
                // 没有开启优化,处理就绪的key
                processSelectedKeysPlain(selector.selectedKeys());
            }
            final long ioTime = System.nanoTime() - ioStartTime;

            final int ioRatio = this.ioRatio;
            // 执行非I/O操作的系统Task和定时任务,ioRatio为I/O何非I/O的执行时间比例,可定制
            runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
            // shutdown 判断是否进入优雅停机状态
            if (isShuttingDown()) {
                closeAll();
                if (confirmShutdown()) {
                    break;
                }
            }
        } catch (Throwable t) {
            logger.warn("Unexpected exception in the selector loop.", t);
            // Prevent possible consecutive immediate failures that lead to
            // excessive CPU consumption.
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // Ignore.
            }
        }
    }
}

Selector的selectNow()方法会立即触发Selector的选择操作,如果有准备就绪的Channel,则返回就绪Channel的集合,否则返回0。 选择完成之后,再次判断用户是否调用了Selector的wakeup方法,如果调用,则执行selector.wakeup()操作。

void selectNow() throws IOException {
    try {
        selector.selectNow();
    } finally {
        // restore wakup state if needed
        if (wakenUp.get()) {
            selector.wakeup();
        }
    }
}
private void select() throws IOException {
    Selector selector = this.selector;
    try {
        int selectCnt = 0;
        long currentTimeNanos = System.nanoTime();
        // 获得定时任务的触发事件
        long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
        for (;;) {
            // 加入0.5毫秒的调整值
            long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
            // 如果已经过时或者需要立即执行则selectNow()
            if (timeoutMillis <= 0) {
                if (selectCnt == 0) {
                    selector.selectNow();
                    // 设置为1并退出循环
                    selectCnt = 1;
                }
                break;
            }
            // 将定时任务的剩余时间作为超时时间进行select
            int selectedKeys = selector.select(timeoutMillis);
            selectCnt ++;
            // 有Channel处于就绪状态,selectedKeys不为0,说明有读写事件需要处理
            // oldWakenUp为true
            // 系统或者用户调用了wakeup操作,唤醒当前多路复用线程
            // 消息队列有新的任务需要处理
            if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks()) {
                // Selected something,
                // waken up by user, or
                // the task queue has a pending task.
                break;
            }
            // 对JDK的select空循环进行规避,通过采样检测和重建使系统恢复正常
            if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                    selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                // The selector returned prematurely many times in a row.
                // Rebuild the selector to work around the problem.
                logger.warn(
                        "Selector.select() returned prematurely {} times in a row; rebuilding selector.",
                        selectCnt);
                // 重建Selector,打开新的Selector,循环旧Selector上注册的Channel到新的Selector上,关闭旧Selector
                rebuildSelector();
                selector = this.selector;

                // Select again to populate selectedKeys.
                selector.selectNow();
                selectCnt = 1;
                break;
            }

            currentTimeNanos = System.nanoTime();
        }

        if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
            if (logger.isDebugEnabled()) {
                logger.debug("Selector.select() returned prematurely {} times in a row.", selectCnt - 1);
            }
        }
    } catch (CancelledKeyException e) {
        if (logger.isDebugEnabled()) {
            logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector - JDK bug?", e);
        }
        // Harmless exception - log anyway
    }
}
private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {
    // check if the set is empty and if so just return to not create garbage by
    // creating a new Iterator every time even if there is nothing to process.
    // See https://github.com/netty/netty/issues/597
    if (selectedKeys.isEmpty()) {
        return;
    }
    // 遍历SelectionKey进行网络读写
    Iterator<SelectionKey> i = selectedKeys.iterator();
    for (;;) {
        final SelectionKey k = i.next();
        final Object a = k.attachment();
        // 移除已选择的
        i.remove();

        if (a instanceof AbstractNioChannel) {
            // NioSocketChannel或者NioServerSocketChannel,进行I/O读写
            processSelectedKey(k, (AbstractNioChannel) a);
        } else {
            @SuppressWarnings("unchecked")
            // 是NioTask,Netty自身没有实现,一般是用户调用
            NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
            processSelectedKey(k, task);
        }

        if (!i.hasNext()) {
            break;
        }

        if (needsToSelectAgain) {
            selectAgain();
            selectedKeys = selector.selectedKeys();

            // Create the iterator again to avoid ConcurrentModificationException
            if (selectedKeys.isEmpty()) {
                break;
            } else {
                i = selectedKeys.iterator();
            }
        }
    }
}
private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
    final NioUnsafe unsafe = ch.unsafe();
    // 判断选择键是否可用
    if (!k.isValid()) {
        // close the channel if the key is not valid anymore
        unsafe.close(unsafe.voidPromise());
        return;
    }

    try {
        int readyOps = k.readyOps();
        // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
        // to a spin loop
        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
            // 读或者连接操作位,执行read方法
            // 对于NioServerSocketChannel,读操作是接受客户端的TCP连接
            // 对于NioSocketChannel,读取操作是从SocketChannel中读取ByteBuffer
            unsafe.read();
            if (!ch.isOpen()) {
                // Connection already closed - no need to handle write.
                return;
            }
        }
        // 半包消息未发送完成,继续调用flush方法进行发送
        if ((readyOps & SelectionKey.OP_WRITE) != 0) {
            // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
            ch.unsafe().forceFlush();
        }
        // 连接操作位需要对连接结果进行判断
        if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
            // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
            // See https://github.com/netty/netty/issues/924
            int ops = k.interestOps();
            ops &= ~SelectionKey.OP_CONNECT;
            k.interestOps(ops);

            unsafe.finishConnect();
        }
    } catch (CancelledKeyException e) {
        unsafe.close(unsafe.voidPromise());
    }
}
protected boolean runAllTasks(long timeoutNanos) {
    // 弹出Task进行处理,到达直接则放入立即执行的queue中,同时从延时队列中删除
    fetchFromDelayedQueue();
    Runnable task = pollTask();
    if (task == null) {
        // 没有则退出
        return false;
    }

    final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
    long runTasks = 0;
    long lastExecutionTime;
    // 循环执行需要执行的任务
    for (;;) {
        try {
            task.run();
        } catch (Throwable t) {
            logger.warn("A task raised an exception.", t);
        }

        runTasks ++;

        // Check timeout every 64 tasks because nanoTime() is relatively expensive.
        // XXX: Hard-coded value - will make it configurable if it is really a problem.
        // 每64执行判断一次时间,如果大于分配的之间就退出
        // 获取时间很耗时,所以64次判断一次
        if ((runTasks & 0x3F) == 0) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            if (lastExecutionTime >= deadline) {
                break;
            }
        }

        task = pollTask();
        if (task == null) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            break;
        }
    }

    this.lastExecutionTime = lastExecutionTime;
    return true;
}
// 遍历所有的Channel,调用它的Unsafe.close()方法关闭所有链路,
// 释放线程池、ChannelPipeline和ChannelHandler
private void closeAll() {
    selectAgain();
    Set<SelectionKey> keys = selector.keys();
    Collection<AbstractNioChannel> channels = new ArrayList<AbstractNioChannel>(keys.size());
    for (SelectionKey k: keys) {
        Object a = k.attachment();
        if (a instanceof AbstractNioChannel) {
            channels.add((AbstractNioChannel) a);
        } else {
            k.cancel();
            @SuppressWarnings("unchecked")
            NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
            invokeChannelUnregistered(task, k, null);
        }
    }

    for (AbstractNioChannel ch: channels) {
        ch.unsafe().close(ch.unsafe().voidPromise());
    }
}

Future和Promise

Future最早来源于JDK的java.util.concurrent.Future,它用于代表异步操作的结果。

Future功能

JDK Future的API列表

  • 可以通过get方法获取操作结果,如果操作尚未完成,则会同步阻塞当前调用的线程;
  • 如果不允许阻塞太长时间或者无限期阻塞,可以通过带超时时间的get方法获取结果;
  • 如果到达超时时间操作仍然没有完成,则抛出TimeoutException。
  • 通过isDone()方法可以判断当前的异步操作是否完成,如果完成,无论成功与否,都返回true,否则返回false。
  • 通过cancel可以尝试取消异步操作,它的结果是未知的,如果操作已经完成,或者发生其他未知的原因拒绝取消,取消操作将会失败。
ChannelFuture功能介绍

由于Netty的Future都是与异步I/O操作相关的,因此,命名为ChannelFuture,代表它与Channel操作相关。

ChannelFuture接口列表

在Netty中,所有的I/O操作都是异步的,这意味着任何I/O调用都会立即返回,而不是像传统BIO那样同步等待操作完成。异步操作会带来一个问题: 调用者如何获取异步操作的结果?ChannelFuture就是为了解决这个问题而专门设计的。

ChannelFuture有两种状态:uncompleted和completed。

当开始一个I/O操作时,一个新的ChannelFuture被创建,此时它处于uncompleted状态一非失败、非成功、非取消,因为I/O操作此时还没有完成。 一旦I/O操作完成,ChannelFuture将会被设置成completed,它的结果有三种可能,操作成功、操作失败、操作被取消。

                                      +---------------------------+
                                      | Completed successfully    |
                                      +---------------------------+
                                 +---->      isDone() = true      |
 +--------------------------+    |    |   isSuccess() = true      |
 |        Uncompleted       |    |    +===========================+
 +--------------------------+    |    | Completed with failure    |
 |      isDone() = false    |    |    +---------------------------+
 |   isSuccess() = false    |----+---->      isDone() = true      |
 | isCancelled() = false    |    |    |       cause() = non-null  |
 |       cause() = null     |    |    +===========================+
 +--------------------------+    |    | Completed by cancellation |
                                 |    +---------------------------+
                                 +---->      isDone() = true      |
                                      | isCancelled() = true      |
                                      +---------------------------+

ChannelFuture提供了一系列新的API用于获取操作结果、添加事件监听器、取消I/O操作、同步等待等。

public interface ChannelFuture extends Future<Void> {
    Channel channel();
    @Override
    ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> listener);
    @Override
    ChannelFuture addListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);
    @Override
    ChannelFuture removeListener(GenericFutureListener<? extends Future<? super Void>> listener);
    @Override
    ChannelFuture removeListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);
    @Override
    ChannelFuture sync() throws InterruptedException;
    @Override
    ChannelFuture syncUninterruptibly();
    @Override
    ChannelFuture await() throws InterruptedException;
    @Override
    ChannelFuture awaitUninterruptibly();
}

Netty强烈建议直接通过添加监听器的方式获取I/O操作结果,或者进行后续的相关操作。

ChannelFuture可以同时增加一个或者多个GenericFutureListener,也可以通过remove方法删除GenericFutureListener。

public interface GenericFutureListener<F extends Future<?>> extends EventListener {
    // future为ChannelFuture对象本身
    // 如果用户需要做上下文相关的操作,需要将上下文信息保存到对应的ChannelFuture中。
    void operationComplete(F future) throws Exception;
}

推荐通过GenericFutureListener代替ChannelFuture的get等方法的原因是:

当我们进行异步I/O操作时,完成的时间是无法预测的,如果不设置超时时间,它会导致调用线程长时间被阻塞,甚至挂死。 而设置超时时间,时间又无法精确预测。利用异步通知机制回调GenericFutureListener是最佳的解决方案,它的性能最优。

需要注意的是:

不要在ChannelHandler中调用ChannelFuture的await()方法,这会导致死锁。原因是发起I/O操作之后,由I/O线程负责异步通知发起I/O操作的用户线程, 如果I/O线程和用户线程是同一个线程,就会导致I/O线程等待自已通知操作完成,这就导致了死锁,这跟经典的两个线程互等待死锁不同,属于自己把自己挂死。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ChannelFuture channelFuture = ctx.channel().close();
    channelFuture.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            // perform post-closure operation
        }
    });
}

异步I/O操作有两类超时:

一个是TCP层面的I/O超时,另一个是业务逻辑层面的操作超时。两者没有必然的联系,但是通常情况下业务逻辑超时时间应该大于I/O超时时间,它们两者是包含的关系。

ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port), new InetSocketAddress(NettyConstant.LOCALIP, NettyConstant.LOCAL_PORT)).sync();
future.awaitUninterruptibly(10,TimeUnit.SECONDS);
if (future.isCancelled()){
    // canceled
}else if (!future.isSuccess()){
    future.cause().printStackTrace();
}else {
    //connect success
}

ChannelFuture超时并不代表I/O超时,这意味着ChannelFuture超时后,如果没有关闭连接资源,随后连接依旧可能会成功,这会导致严重的问题。 所以通常情况下,必须要考虑究竟是设置I/O超时还是ChannelFuture超时。

ChannelFuture源码分析

ChannelFUture的类继承关系

AbstractFuture

AbstractFuture实现Future接口,它不允许I/O操作被取消。

@Override
public V get() throws InterruptedException, ExecutionException {
    // 进行无限期等待,I/O操作完成后会被notify()
    await();
    // 检查是否发生了异常
    Throwable cause = cause();
    if (cause == null) {
        // 没有发生就获取并返回结果
        return getNow();
    }
    // 包装异常并抛出
    throw new ExecutionException(cause);
}
@Override
public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
    // 超时支持
    if (await(timeout, unit)) {
        Throwable cause = cause();
        if (cause == null) {
            return getNow();
        }
        throw new ExecutionException(cause);
    }
    throw new TimeoutException();
}

AbatractFuture继承关系:

AbatractFuture继承关系

Promise功能介绍

Promise是可写的Future,Future自身并没有写操作相关的接口,Netty通过Promise对Future进行扩展,用于设置I/O操作的结果。

Promise写操作相关的接口定义:

Promise写操作相关的接口定义

Netty发起I/O操作的时候,会创建一个新的Promise对象,例如调用ChannelHandlerContext的write(Objectobject)方法时,会创建一个新的ChanneLPromise。

@Override
public ChannelPromise newPromise() {
    return new DefaultChannelPromise(channel(), executor());
}

当I/O操作发生异常或者完成时,设置Promise的结果。

@Override
public void write(Object msg, ChannelPromise promise) {
    if (!isActive()) {
        // Mark the write request as failure if the channel is inactive.
        if (isOpen()) {
            promise.tryFailure(NOT_YET_CONNECTED_EXCEPTION);
        } else {
            promise.tryFailure(CLOSED_CHANNEL_EXCEPTION);
        }
        // release message now to prevent resource-leak
        ReferenceCountUtil.release(msg);
    } else {
        outboundBuffer.addMessage(msg, promise);
    }
}

Promise源码分析

Promise继承关系图

Promise继承关系图

DefaultPromise
@Override
public Promise<V> setSuccess(V result) {
    // 判断操作成功后调用Listener
    if (setSuccess0(result)) {
        notifyListeners();
        return this;
    }
    throw new IllegalStateException("complete already: " + this);
}

private boolean setSuccess0(V result) {
    // 判断是否已经设置,如果已经设置返回失败
    if (isDone()) {
        return false;
    }
    // 并发控制和二次判断
    synchronized (this) {
        // Allow only once.
        if (isDone()) {
            return false;
        }
        // 设置result
        if (result == null) {
            this.result = SUCCESS;
        } else {
            this.result = result;
        }
        // 唤醒等待I/O操作完成的用户线程或者其他系统线程
        // wait()和notify()方法必须在同步块内使用
        if (hasWaiters()) {
            notifyAll();
        }
    }
    return true;
}
@Override
public Promise<V> await() throws InterruptedException {
    // 已经被设置直接返回
    if (isDone()) {
        return this;
    }
    // 线程已经被中断,抛出异常
    if (Thread.interrupted()) {
        throw new InterruptedException(toString());
    }
    synchronized (this) {
        // 锁定判断防止意外唤醒
        while (!isDone()) {
            // 保护性校验
            // 防止在I/O线程中调用await()或者sync()方法导致死锁
            checkDeadLock();
            incWaiters();
            try {
                wait();
            } finally {
                decWaiters();
            }
        }
    }
    return this;
}

架构剖析

Netty逻辑架构

Netty逻辑架构

Reactor通信调度层

它由一系列辅助类完成,包括Reactor线程NioEventLoop及其父类,NioSocketChannel/NioServerSocketChannel及其父类, ByteBuffer以及由其衍生出来的各种Buffer,Unsafe以及其衍生出的各种内部类等。

该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区中,然后触发各种网络事件, 例如连接创建、连接激活、读事件、写事件等,将这些事件触发到Pipeline中,由Pipeline管理的职责链来进行后续的处理。

责任链ChannelPipe

它负责事件在职责链中的有序传播,同时负责动态地编排职责链。职责链可以选择监听和处理自己关心的事件,它可以拦截处理和向后/向前传播事件。

不同应用的Handler节点的功能也不同,通常情况下,往往会开发编解码HanIder用于消息的编解码,它可以将外部的协议消息转换成内部的POJO对象, 这样上层业务则只需要关心处理业务逻辑即可,不需要感知底层的协议差异和线程模型差异,实现了架构层面的分层隔离。

业务逻辑编排层(Service ChannelHandler)

业务逻辑编排层通常有两类:

  • 一类是纯粹的业务逻辑编排
  • 还有一类是其他的应用层协议插件,用于特定协议相关的会话和链路管理。
    • 例如CMPP协议,用于管理和中国移动短信系统的对接。

架构的不同层面,需要关心和处理的对象都不同,通常情况下,对于业务开发者,只需要关心职责链的拦截和业务Handler的编排。 因为应用层协议栈往往是开发一次,到处运行,所以实际上对于业务开发者来说,只需要关心服务层的业务逻辑开发即可。 各种应用协议以插件的形式提供,只有协议开发人员需要关注协议插件,对于其他业务开发人员来说,只需关心业务逻辑定制。 这种分层的架构设计理念实现了NIO框架各层之间的解耦,便于上层业务协议栈的开发和业务逻辑的定制。

关建架构质量属性

高性能

影响最终产品的性能因素非常多,其中软件因素:

  • 架构不合理导致的性能问题。
  • 编码实现不合理导致的性能问题,例如锁的不恰当使用导致性能瓶颈。

硬件因素:

  • 服务器硬件配置太低导致的性能问题。
  • 带宽、磁盘的IOPS等限制导致的I/O操作性能差。
  • 测试环境被共用导致被测试的软件产品受到影响。

尽管影响产品性能的因素非常多,但是架构的性能模型合理与否对性能的影响非常大。如果一个产品的架构设计得不好,无论开发如何努力,都很难开发出一个高性能、高可用的软件产品。

Netty的架构设计是如何实现高性能:

  • 采用异步非阻塞的I/O类库,基于Reactor模式实现,解决了传统同步阻塞I/O模式下一个服务端无法平滑地处理线性增长的客户端的问题。
  • TCP接收和发送缓冲区使用直接内存代替堆内存,避免了内存复制,提升了I/O读取和写入的性能。
  • 支待通过内存池的方式循环利用ByteBuf.避免了频繁创建和销毁ByteBuf带来的性能损耗。
  • 可配置的I/O线程数、TCP参数等,为不同的用户场景提供定制化的调优参数,满足不同的性能场景。
  • 采用环形数组缓冲区实现无锁化并发编程,代替传统的线程安全容器或者锁。
  • 合理地使用线程安全容器、原子类等,提升系统的并发处理能力。
  • 关键资源的处理使用单线程串行化的方式,避免多线程并发访问带来的锁竞争和额外的CPU资源消耗问题。
  • 通过引用计数器及时地申请释放不再被引用的对象,细粒度的内存管理降低了GC的频率,减少了频繁GC带来的时延增大和CPU损耗。
可靠性
链路有效性检测

由于长连接不需要每次发送消息都创建链路,也不需要在消息交互完成时关闭链路,因此相对于短连接性能更高。对于长连接,一旦链路建立成功便一直维系双方之间的链路,直到系统退出。

为了保证长连接的链路有效性,往往需要通过心跳机制周期性地进行链路检测。使用周期性心跳的原因是:

在系统空闲时,例如凌晨,往往没有业务消息。如果此时链路被防火墙Hang住,或者遭遇网络闪断、网络单通等,通信双方无法识别出这类链路异常。 等到第二天业务高峰期到来时,瞬间的海垃业务冲击会导致消息积压无法发送给对方,由于链路的重建需要时间,这期间业务会大量失败(集群或者分布式组网情况会好一些)。 为了解决这个问题,需要周期性的心跳对链路进行有效性检测,一旦发生问题,可以及时关闭链路,重建TCP连接。

当有业务消息时,无须心跳检测,可以由业务消息进行链路可用性检测。所以心跳消息往往是在链路空闲时发送的。

为了支持心跳,Netty提供了如下两种链路空闲检测机制。

  • 读空闲超时机制:
    • 当连续周期T没有消息可读时,触发超时Handler,用户可以基于读空闲超时发送心跳消息,进行链路检测;
    • 如果连续N个周期仍然没有读取到心跳消息,可以主动关闭链路。
  • 写空闲超时机制:
    • 当连续周期T没有消息要发送时,触发超时Handler,用户可以基于写空闲超时发送心跳消息,进行链路检测;
    • 如果连续N个周期仍然没有接收到对方的心跳消息,可以主动关闭链路。

为了满足不同用户场景的心跳定制,Netty提供了空闲状态检测平件通知机制,用户可以订阅空闲超时事件、写空闲超时事件、读或者写超时事件,在接收到对应的空闲事件之后,灵活地进行定制。

内存保护机制

Netty提供多种机制对内存进行保护:

  • 通过对象引用计数器对Netty的ByteBuf等内置对象进行细粒度的内存申请和释放,对非法的对象引用进行检测和保护。
  • 通过内存池来重用ByteBuf,节省内存。
  • 可设置的内存容量上限,包括ByteBuf、线程池线程数等。

AbstractReferenceCountedByteBuf的内存管理方法实现:

@Override
public ByteBuf retain() {
    for (;;) {
        int refCnt = this.refCnt;
        if (refCnt == 0) {
            throw new IllegalReferenceCountException(0, 1);
        }
        if (refCnt == Integer.MAX_VALUE) {
            throw new IllegalReferenceCountException(Integer.MAX_VALUE, 1);
        }
        if (refCntUpdater.compareAndSet(this, refCnt, refCnt + 1)) {
            break;
        }
    }
    return this;
}
@Override
public final boolean release() {
    for (;;) {
        int refCnt = this.refCnt;
        if (refCnt == 0) {
            throw new IllegalReferenceCountException(0, -1);
        }

        if (refCntUpdater.compareAndSet(this, refCnt, refCnt - 1)) {
            if (refCnt == 1) {
                deallocate();
                return true;
            }
            return false;
        }
    }
}

Byte Buf 的解码保护,防止非法码流导致内存溢出:

/**
  * Creates a new instance.
  *
  * @param maxFrameLength 超出最大长度会抛出异常
  *        the maximum length of the frame.  If the length of the frame is
  *        greater than this value, {@link TooLongFrameException} will be
  *        thrown.
  * @param lengthFieldOffset
  *        the offset of the length field
  * @param lengthFieIdlength
  *        the length of the length field
  */
public LengthFieldBasedFrameDecoder(
        int maxFrameLength,
        int lengthFieldOffset, int lengthFieIdlength) {
    this(maxFrameLength, lengthFieldOffset, lengthFieIdlength, 0, 0);
}
优雅停机

优雅停机功能指的是当系统退出时,JVM通过注册的ShutdownHook拦截到退出信号址,然后执行退出操作,释放相关模块的资源占用,将缓冲区的消息处理完成或者消空, 将待刷新的数据持久化到磁盘或者数据库中,等到资源回收和缓冲区消息处理完成之后,再退出。

优雅停机往往需个最大超时时间T,如果达到T后系统仍然没有退出,则通过Kill -9 pid强杀当前的进程。

Netty所有涉及到资源回收和释放的地方都增加了优雅退出的方法:

Netty重要资源优雅退出方法

可定制性
  • 责任链模式:ChannelPipeline基于责任链模式开发,便于业务逻辑的拦截、定制和扩展。
  • 基于接口的开发:关键的类库都提供了接口或者抽象类,如果Netty自身的实现无法满足用户的需求,可以由用户自定义实现相关接口。
  • 提供了大量工厂类,通过重载这些工厂类可以按需创建出用户实现的对象。
  • 提供了大量的系统参数供用户按需设置,增强系统的场采定制性。
可扩展性

基于Netty的基础NIO框架,可以方便地进行应用层协议定制,例如HTTP协议栈、Thrift协议栈、FTP协议栈等。这些扩展不需要修改Netty的源码,直接基于Netty的二进制类库即可实现协议的扩展和定制。

目前,业界存在大量的基于Netty框架开发的协议,例如基于Netty的HTTP协议、Dubbo协议、RocketMQ内部私有协议等。

Java多线程在Netty中的应用

Java内存模型

JVM规范定义了Java内存模型(Java Memory Model)来屏蔽掉各种操作系统、虚拟机实现厂商和硬件的内存访问差异, 以确保Java程序在所有操作系统和平台上能够实现一次编写、到处运行的效果。

Java内存模型的制定既要严谨,保证语义无歧义,还要尽量制定得宽松一些,允许各硬件和虚拟机实现厂商有足够的灵活性来充分利用硬件的特性提升Java的内存访问性能。 随着JDK的发展,Java的内存模型已经逐渐成熟起来。

工作内存和主内存

Java内存模型规定所有的变量都存储在主内存中(JVM内存的一部分),每个线程有自己独立的工作内存,它保存了被该线程使用的变量的主内存复制。 线程对这些变量的操作都在自己的工作内存中进行,不能直接操作主内存和其他工作内存中存储的变量或者变量副本。线程间的变量访问需通过主内存来完成,

Java内存访问模型:

Java内存访问模型

Java内存交互协议

Java内存模型定义了8种操作来完成主内存和工作内存的变量访问:

  • lock:主内存变量,把一个变量标识为某个线程独占的状态。
  • unlock:主内存变量,把一个处于锁定状态变量释放出来,被释放后的变量才可被其他线程锁定。
  • read:主内存变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load:工作内存变量,把read读取到的主内存中的变量值放入工作内存的变量副本中。
  • use:工作内存变量,把工作内存中变量的值传递给Java虚拟机执行引擎,每当虚拟机遇到一个需要使用到变量值的字节码指令时,将会执行该操作。
  • assign:工作内存变量,把从执行引擎接收到的变量的值赋值给工作变量,每当虚拟机遇到一个给变量赋值的字节码时,将会执行该操作。
  • store:工作内存变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write:主内存变量,把store操作从工作内存中得到的变量值放入主内存的变量中。
Java的线程

并发可以通过多种方式来实现,例如:单进程-单线程模型,通过在一台服务器上启动多个进程来实现多任务的并行处理。 但是在Java语言中,通常是通过单进程-多线程的模型进行多任务的并发处理。

线程是比进程更轻噩级的调度执行单元,它可以把进程的资源分配和调度执行分开,各个线程可以共享内存、I/O等操作系统资源, 但是又能够被操作系统发起的内核线程或者进程执行。各线程可以独立地启动、运行和停止,实现任务的解耦。

主流的操作系统提供了线程实现,目前实现线程的方式主要有三种:

  • 内核线程(KLT)实现,这种线程由内核来完成线程切换,内核通过线程调度器对线程进行调度,并负责将线程任务映射到不同的处理器上。
  • 用户线程实现(UT),通常情况下,用户线程指的是完全建立在用户空间线程库上的线程,用户线程的创建、启动、运行、销毁和切换完全在用户态中完成,不需要内核的帮助,因此执行性能更高。
  • 混合实现,将内核线程和用户线程池合在一起使用的方式。

由于虚拟机规范并没有强制规定Java的线程必须使用哪种方式实现,因此,不同的操作系统实现的方式也可能存在差异。 对于SUN的JDK,在Windows和Linux操作系统上采用了内核线程的实现方式,在Solaris版本的JDK中,提供了一些专有的虚拟机线程参数,用于设置使用哪种线程模型。

Netty的并发编程实战

对共享的可变数据进行正确的同步

由于ServerBootstrap是被外部使用者创建和使用的,无法保证它的方法和成员变量不被并发访问,因此,作为成员变量的options必须进行正确地同步。 由于考虑到锁的范围需要尽可能的小,对传参的option和value的合法性判断不需要加锁。因此,代码才对两个判断分支独立加锁,保证锁的范围尽可能的细粒度。

ServerBootstrap:

@SuppressWarnings("unchecked")
public <T> B option(ChannelOption<T> option, T value) {
    if (option == null) {
        throw new NullPointerException("option");
    }
    if (value == null) {
        synchronized (options) {
            options.remove(option);
        }
    } else {
        synchronized (options) {
            options.put(option, value);
        }
    }
    return (B) this;
}
正确使用锁

不满足条件时阻塞,满足条件时唤醒所有线程:

// ForkJoinTask
private int externalAwaitDone() {
    int s;
    ForkJoinPool cp = ForkJoinPool.common;
    if ((s = status) >= 0) {
        if (cp != null) {
            if (this instanceof CountedCompleter)
                s = cp.externalHelpComplete((CountedCompleter<?>)this);
            else if (cp.tryExternalUnpush(this))
                s = doExec();
        }
        if (s >= 0 && (s = status) >= 0) {
            boolean interrupted = false;
            do {
                // 不满足条件时阻塞,满足后继续运行
                if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
                    // 同步代码块中循环判断变量,并适时wait
                    synchronized (this) {
                        if (status >= 0) {
                            try {
                                wait();
                            } catch (InterruptedException ie) {
                                interrupted = true;
                            }
                        }
                        else
                            // 恰当的使用notifyAll方法唤醒线程
                            notifyAll();
                    }
                }
            //使用用循环调用wait方法防止意外唤醒
            } while ((s = status) >= 0);
            if (interrupted)
                Thread.currentThread().interrupt();
        }
    }
    return s;
}
volatile的正确使用

当一个变扭被volatile修饰后,它将具备以下两种特性:

  • 线程可见性:当一个线程修改了被volatile修饰的变量后,无论是否加锁,其他线程都可以立即看到最新的修改,而普通变量却做不到这点。
  • 禁止指令重排序优化,普通的变量仅仅保证在该方法的执行过程中所有依赖赋值结果的地方都能获取正确的结果,而不能保证变量赋值操作的顺序与程序代码的执行顺序一致。

volatile仅仅解决了可见性的问题,但是它并不能保证互斥性,也就是说多个线程井发修改某个变量时,依旧会产生多线程问题。因此,不能靠volatile来完全替代传统的锁。

根据经验总结,volatile最适合使用的是一个线程写,其他线程读的场合,如果有多个线程井发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替。

// NioEventLoop
private volatile int ioRatio = 50;
CAS指令和原子类

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能的额外损耗,因此这种同步被称为阻塞同步,它属于一种悲观的并发策略,称为悲观锁。

随着硬件和操作系统指令集的发展和优化,产生了非阻塞同步,称为乐观锁。 先进行操作,操作完成之后再判断操作是否成功,是否有并发问题,如果有则进行失败补偿,如果没有就算操作成功,这样就从根本上避免了同步锁的弊端。

private static final AtomicLongFieldUpdater<ChannelOutboundBuffer> TOTAL_PENDING_SIZE_UPDATER =
        AtomicLongFieldUpdater.newUpdater(ChannelOutboundBuffer.class, "totalPendingSize");

private volatile long totalPendingSize;
long oldValue = totalPendingSize;
long newWriteBufferSize = oldValue + size;
// 自旋更新直至成功
while (!TOTAL_PENDING_SIZE_UPDATER.compareAndSet(this, oldValue, newWriteBufferSize)) {
    // 失败获取新值重新尝试
    oldValue = totalPendingSize;
    newWriteBufferSize = oldValue + size;
}
线程安全类的应用

java.util.concurrent包中提供了一系列的线程安全集合、容器和线程池,利用这些新的线程安全类可以极大地降低Java多线程编程的难度,提升开发效率。

并发编程包中的工具可以分为4类:

  • 线程池Executor Framework以及定时任务相关的类库,包括Timer等。
  • 并发集合,包括List、Queue、Map和Set等。
  • 新的同步器,例如读写锁ReadWriteLock等。
  • 新的原子包装类,例如Atomiclnteger等。

NioEventLoop是I/O线程,负责网络读写操作,同时也执行一些非I/O的任务。例如事件通知、定时任务执行等,因此,它需要一个任务队列来缓存这些Task。

// 线程安全的,读写都不需要加锁
private final Queue<Runnable> taskQueue;
@Override
protected Queue<Runnable> newTaskQueue() {
    // This event loop never calls takeTask()
    return new ConcurrentLinkedQueue<Runnable>();
}

JDK的线程安全容器底层采用了CAS、volatile和ReadWriteLock实现,相比于传统重证级的同步锁,采用了更轻量、细粒度的锁,因此,性能会更高。 合理地应用这些线程安全容器,不仅能提升多线程并发访问的性能,还能降低开发难度。

SingleThreadEventExecutor中的线程池:

private final Executor executor;
private void startThread() {
    synchronized (stateLock) {
        if (state == ST_NOT_STARTED) {
            state = ST_STARTED;
            delayedTaskQueue.add(new ScheduledFutureTask<Void>(
                    this, delayedTaskQueue, Executors.<Void>callable(new PurgeTask(), null),
                    ScheduledFutureTask.deadlineNanos(SCHEDULE_PURGE_INTERVAL), -SCHEDULE_PURGE_INTERVAL));
            doStartThread();
        }
    }
}
private void doStartThread() {
    executor.execute(new Runnable() {
                @Override
                public void run() {
                    thread = Thread.currentThread();
                    if (interrupted) {
                        thread.interrupt();
                    }

                    boolean success = false;
                    updateLastExecutionTime();
                    try {
                        // 执行I/O、runalltask等
                        SingleThreadEventExecutor.this.run();
                        success = true;
                    } catch (Throwable t) {
                        logger.warn("Unexpected exception from an event executor: ", t);
                    } finally {
                        // ...
                    }
                }
    });
}
读写锁的应用
// HashedWheelTimer
final ReadWriteLock lock = new ReentrantReadWriteLock();
// 新增定时任务时使用了读锁
@Override
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
    start();
    if (task == null) {
        throw new NullPointerException("task");
    }
    if (unit == null) {
        throw new NullPointerException("unit");
    }
    long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
    // Add the timeout to the wheel.
    HashedWheelTimeout timeout;
    lock.readLock().lock();
    try {
        timeout = new HashedWheelTimeout(task, deadline);
        if (workerState.get() == WORKER_STATE_SHUTDOWN) {
            throw new IllegalStateException("Cannot enqueue after shutdown");
        }
        wheel[timeout.stopIndex].add(timeout);
    } finally {
        lock.readLock().unlock();
    }
    return timeout;
}
// 删除使用了写锁
private void fetchExpiredTimeouts(
                List<HashedWheelTimeout> expiredTimeouts, long deadline) {
    lock.writeLock().lock();
    try {
        fetchExpiredTimeouts(expiredTimeouts, wheel[(int) (tick & mask)].iterator(), deadline);
    } finally {
        tick ++;
        lock.writeLock().unlock();
    }
}

读写锁的使用场景:

主要用于读多写少的场报,用来替代传统的同步锁,以提升并发访问性能。

  • 读写锁是可重入、可降级的,一个线程获取读写锁后,可以继续递归获取:从写锁可以降级为读锁,以便快速释放锁资源。
  • ReentrantReadWriteLock支持获取锁的公平策略,在某些特殊的应用场景下,可以提升并发访问的性能,同时兼顾线程等待公平性。
  • 读写锁支持非阻塞的尝试获取锁,如果获取失败,直接返回false,而不是同步阻塞。
    • 例如多个线程同步读写某个资源,当发生异常或者需要释放资源的时候,由哪个线程释放是个难题。因为某些资源不能重复释放或者重复执行,这样,可以说明已经被其他线程占用,直接退出即可。
  • 获取锁之后一定要释放锁,否则会发生锁溢出异常。通常的做法是通过finally块释放锁。如果是tryLock,获取锁成功才需要释放锁。
线程安全性文档说明

在Netty中,对于一些关键的类库,给出了线程安全性的API DOC,如ChannelPipeline。

不要依赖线程的优先级

Netty的DefaultThreadFactory提供了线程优先级的字段,但是不要使用它,因为JDK无法跨平台的正确运行它。

高性能

RPC用性能模型分析

传统RPC调用的三宗罪

网络传输方式问题

传统的RPC框架或者基于RMI等方式的远程服务(过桯)调用采用了同步阻寒I/O,当客户端的并发压力或者网络时延增大之后, 同步阻塞I/O会由于频繁的wait导致I/O线程经常性的阻塞,由于线程无法高效的工作,I/O处理能力自然下降。

采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,接收到客户端连接之后, 为具创建一个新的线程处理诸求消息,处理完成之后,返回应答消息给客户端,线程销毁,这就是典型的一请求一应答模型。 该架构最大的问题就是不具备弹性伸缩能力,当并发访问量培加后,服务端的线程个数和并发访问数成线性正比, 由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能急剧下降, 随看并发狱的继续增加,可能会发生句柄溢出、线程堆栈溢出等问题,并导致服务器最终宕机。

序列化性能差,兼容性不好

  • Java序列化机制是Java内部的一种对象编解码技术,无法跨语言使用。例如对于异构系统之间的对接,Java序列化后的码流需要能够通过其他语言反序列化成原始对象(副本),目前很难支待。
  • 相比于具他开源的序列化框架,Java序列化后的码流太大,尤论是网络传输还是待久化到磁盘,都会导致额外的资源占用。
  • 序列化性能差,资源占用率高(主要是CPU资源占用商)。

线程模型问题

由于采用同步阻塞I/O,这会导致每个TCP连接都占用1个线程,由于线程资源是JVM虚拟机非常宝贵的资源, 当I/O读写阻塞导致线程无法及时释放时,会导致系统性能急剧下降,严重的甚至会导致虚拟机无法创建新的线程。

I/O通信性能三原则

从架构层面看主要有三个要素:

  • 传输:用什么样的通道将数据发送给对方。可以选择BIO、NIO或者AIO,I/O模型在很大程度上决定了通信的性能;
  • 协议:采用什么样的通信协议,HTTP等公有协议或者内部私有协议。协议的选择不同,性能也不同。相比于公有协议,内部私有协议的性能通常可以被设计得更优;
  • 线程:数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,Reactor线程模型的不同,对性能的影响也非常大。

Netty高性能之道

异步非阻塞通信

在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。

I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。 与传统的多线程/多进程模型比,I/0多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。

Netty的I/O线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端SocketChannel。 由于读写操作都是非阻塞的,这就可以充分提升I/O线程的运行效率,避免由频繁的I/O阻塞导致的线程挂起。 另外,由于Netty采用了异步通信模式,一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

高效的Reactor线程模型

参考《Netty的线程模型》

无锁化的串行设计

在大多数场景下,并行多线程处理可以提升系统的并发性能。 但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。 为了尽可能地避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。

为了尽可能提升性能,Netty采用了串行无锁化设计,在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降。 表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列—一多个上作线程模型性能更优。

参考《Netty的线程模型》

高效的并发编程

参考《Java多线程在Netty中的应用》

高性能的序列化框架

参考《业界主流的编解码框架》《Netty多协议开发》

零拷贝

Netty的接收和发送ByteBuffer采用DIRECTBUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。

如果使用传统的堆内存(HEAPBUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。 相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

static int write(FileDescriptor fileDescriptor, ByteBuffer paramByteBuffer, long length,
        NativeDispatcher nativeDispatcher) throws IOException {
    if (paramByteBuffer instanceof DirectBuffer) {
        // 直接内存直接write
        return writeFromNativeBuffer(fileDescriptor, paramByteBuffer, length, nativeDispatcher);
    } else {
        int i = paramByteBuffer.position();
        int j = paramByteBuffer.limit();
        assert i <= j;
        int k = i <= j ? j - i : 0;
        ByteBuffer localByteBuffer = Util.getTemporaryDirectBuffer(k);
        int paramByteBuffer0;
        try {
            // 内存拷贝
            localByteBuffer.put(paramByteBuffer);
            localByteBuffer.flip();
            paramByteBuffer.position(i);
            int m = writeFromNativeBuffer(fileDescriptor, localByteBuffer, length, nativeDispatcher);
            if (m > 0) {
                paramByteBuffer.position(i + m);
            }
            paramByteBuffer0 = m;
        } finally {
            Util.offerFirstTemporaryDirectBuffer(localByteBuffer);
        }
        return paramByteBuffer0;
    }
}

CompositeByteBuf是一个ByteBuf的装饰器,添加ByteBuf不需要做内存拷贝。

Netty的文件传输类DefaultFileRegion通过transferTo方法将文件发送到Channel中。

很多操作系统直接将文件缓冲区的内容发送到目标Channel中,而不需要通过循环拷贝的方式, 这是一种更加高效的传输方式,提升了传输性能,降低了CPU和内存占用,实现了文件传输的“零拷贝”。

内存池

随若JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。 但是对于缓冲区Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽址重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。

性能测试表明,采用内存池的ByteBuf相比于朝生夕灭的ByteBuf,性能高23倍左右(性能数据与使用场景强相关)。

灵活的TCP参数配置能力

对性能影响比较大的几个配置项:

  • SO_RCVBUF和SO_SNDBUF:通常建议值为128KB或者256KB;
  • SO_TCPNODELAY:NAGLE算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大垃小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法;
  • 软中断:如果Linux内核版本支持RPS(2.6.35以上版本),开启RPS后可以实现软中断,提升网络吞吐址。
    • RPS根据数据包的源地址,目的地址以及目的和源端口,计算出一个hash值,然后根据这个hash值来选择软中断运行的CPU。从上层来看,也就是说将每个连接和CPU绑定,并通过这个hash值,来均衡软中断在多个CPU上,提升网络并行处理性能。

主流NIO框架性能对比

无论是Netty的官方性能测试数据,还是携带业务实际场景的性能测试,Netty在各个NIO框架中综合性能是最高的。

可靠性

Netty的可靠性需求

Netty的主要应用场景:

  • RPC框架的基础网络通信框架,主要用于分布式节点之间的通信和数据交换,在各个业务领域均有典型的应用
    • 例如阿里的分布式服务框架Dubbo、消息队列RocketMQ、大数据处理Hadoop的基础通信和序列化框架Avro。
  • 私有协议的基础通信框架
    • 例如Thrift协议、Dubbo协议等。
  • 公有协议的基础通信框架
    • 例如HTTP协议、SMPP协议等。

从运行环境上看:

基于Netty开发的应用面临的是网络环境也不同,手游服务运行的GSM/3G/WIFI网络环境可靠性差,偶尔会出现闪断、网络单通等问题。
互联网应用在业务高峰期会出现网络拥堵,而且各地用户的网络环境差别也很大,部分地区网速和网络质炽不高。

从应用场景看:

Netty是基础的通信框架,一旦出现Bug,轻则需要重启应用,重则可能导致整个业务中断。 它的可靠性会影响整个业务集群的数据通信和交换,在当今以分布式为主的软件架构体系中,通信中断就总味看整个业务中断,分布式架构下对通信的可靠性要求非常高。

从运行环境看:

Netty会面临恶劣的网络环境,这就要求它自身的可靠性要足够好,平台能够解决的可靠性问题需要由Netty自身来解决, 否则会导致上层用户关注过多的底层故障,这将降低Netty的易用性,同时增加用户的开发和运维成本。

Netty高可靠性设计

网络通信类故障
客户端连接超时

在传统的同步阻塞编程模式下,客户端Socket发起网络连接,往往需要指定连接超时时间,这样做的目的主要有两个:

  • 在同步阻塞I/O模型中,连接操作是同步阻塞的,如果不设置超时时间,客户端I/O线程可能会被长时间阻塞,这会导致系统可用I/O线程数的减少。
  • 业务层需要:大多数系统都会对业务流程执行时间有限制,例如WEB交互类的响应时间要小于3S。客户端设置连接超时时间是为了实现业务层的超时。

对于NIO的SocketChannel,在非阻塞模式下,它会直接返回连接结果,如果没有连接成功,也没有发生IO异常,则需要将SocketChanoel注册到Selector上监听连接结果。 所以,异步连接的超时无法在API层面直接设置,而是需要通过定时器来主动监测。

JDK的NIO没有提供超时处理,Netty通过定时任务实现了超时控制。参考《客户端连接超时机制》

通信对端强制关闭连接

在客户端和服务端正常通信过程中,如果发生网络闪断、对方进程突然宕机或者其他非正常关闭链路事件时,TCP链路就会发生异常。 由于TCP是全双工的,通信双方都需要关闭和释放Socket句柄才不会发生句柄的泄涌。

在实际的NlO编程过程中,我们经常会发现由于句柄没有被及时关闭导致的功能和可靠性问题。究其原因总结如下:

  • IO的读写等操作并非仅仅集中在Reactor线程内部,用户上层的一些定制行为可能会导致IO操作的外逸,例如业务自定义心跳机制。这些定制行为加大了统一异常处理的难度,IO操作越发散,故障发生的概率就越大;
  • 一些异常分支没有考虑到,由于外部环境诱因导致程序进入这些分支,就会引起故啼。

在SocketChannel的read方法时发生了异常,从Channel中读取数据报到缓冲区中的代码如下:

// AbstractByteBuf.writeBytes() -> ByteBuf.setBytes()
/**
  * Transfers the content of the specified source channel to this buffer
  * starting at the specified absolute {@code index}.
  * This method does not modify {@code readerIndex} or {@code writerIndex} of
  * this buffer.
  *
  * @param length the maximum number of bytes to transfer
  *
  * 这里返回了-1由上层统一处理异常,NioByteUnsafe.closeOnRead()
  * @return the actual number of bytes read in from the specified channel.
  *         {@code -1} if the specified channel is closed.
  *
  * @throws IndexOutOfBoundsException
  *         if the specified {@code index} is less than {@code 0} or
  *         if {@code index + length} is greater than {@code this.capacity}
  * @throws IOException
  *         if the specified channel threw an exception during I/O
  */
public abstract int  setBytes(int index, ScatteringByteChannel in, int length) throws IOException;
链路关闭

对于短连接协议,例如HTTP协议,通信双方数据交互完成之后,通常按照双方的约定由服务端关闭连接,客户端获得TCP连接关闭谓求之后,关闭自身的Socket连接,双方正式断开连接。

在实际的NIO编程过程中,经常存在一种误区:认为只要是对方关闭连接,就会发生JO异常,捕获IO异常之后再关闭迕接即可。
实际上,连接的合法关闭不会发生IO异常,它是一种正常场景,如果遗漏了该场景的判断和处理就会导致连接句柄泄露。

// NioByteUnsafe.read()
// SocketChannel的read操作返回-1,说明已经关闭
int localReadAmount = doReadBytes(byteBuf);
if (localReadAmount <= 0) {
    // not was read release the buffer
    byteBuf.release();
    close = localReadAmount < 0;
    break;
}

如果SocketChannel被设置为阻塞,read操作可能返回三个值:

  • 大于0:读到了字节数
  • 等于0:没有读到字节数,可能TCP处于Keep-Alive状态,TCP握手消息
  • -1:连接已经被对方关闭
定制I/O故障

在大多数场景下,当底层网络发生故障的时候,应该由底层的NIO框架负责释放资源,处理异常等。上层的业务应用不需耍关心底层的处理细节。 但是,在一些特殊的场景下,用户可能需要感知这些异常,并针对这些异常进行定制处理:

  • 客户端的断连重连机制;
  • 消息的缓存重发;
  • 接口日志中详细记录故障细节;
  • 运维相关功能,例如告警、触发邮件、短信等

Netty的处理策略是发生I/O异常,底层的资源由它负责释放,同时将异常堆栈信息以事件的形式通知给上层用户,由用户对异常进行定制。

// ChannelHandlerAdapter
@Skip
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    ctx.fireExceptionCaught(cause);
}
链路的有效性检测

心跳检测机制分为三个层面:

  • TCP层面的心跳检测,即TCP的Keep-Alive机制,它的作用域是整个TCP协议栈;
  • 协议层的心跳检测,主要存在于长连接协议中。例如SMPP协议;
  • 应用层的心跳检测,它主要由各业务产品通过约定方式定时给对方发送心跳消息实现。

心跳检测的原理示意图

不同的协议,心跳检测机制也存在差异,归纳起来主要分为两类:

  • Ping-Pong型心跳:由通信一方定时发送Ping消息,对方接收到Ping消息之后,立即返回Pong应答消息给对方,属于请求-响应型心跳;
  • Ping-Ping型心跳:不区分心跳请求和应答,由通信双方按照约定定时向对方发送心跳Ping消息,它属于双向心跳。

心跳检测策略如下:

  • 连续N次心跳检测都没有收到对方的Pong应答消息或者Ping请求消息,则认为链路已经发生逻辑失效,这被称作心跳超时;
  • 读取和发送心跳消息的时候如何直接发生了IO异常,说明链路已经失效,这被称为心跳失败。

无论发生心跳超时还是心跳失败,都需要关闭链路,由客户端发起重连操作,保证链路能够恢复正常。

Netty的心跳机制利用链路空闲机制实现。参考《可靠性-链路有效性检测》

Netty提供的空闲检测机制分为三种:

  • 读空闲,链路持续时间t没有读取到任何消息;
  • 写空闲,链路持续时间t没有发送任何消息;
  • 读写空闲,链路持续时间t没有接收或者发送任何消息。

Netty的默认读写空闲机制是发生超时异常,关闭连接,但是,我们可以定制它的超时实现机制,以便支持不同的用户场景。

心跳检测的代码包路径

// WriteTimeoutHandler
protected void writeTimedOut(ChannelHandlerContext ctx) throws Exception {
    if (!closed) {
        ctx.fireExceptionCaught(WriteTimeoutException.INSTANCE);
        ctx.close();
        closed = true;
    }
}
// ReadTimeoutHandler
protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {
    if (!closed) {
        ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE);
        ctx.close();
        closed = true;
    }
}

链路空闲的时候井没有关闭链路,而是触发IdleStateEvent事件,用户订阅IdleStateEvent事件,用于自定义逻辑处理,例如关闭链路、客户端发起重新连接、告警和打印日志等。 利用Netty提供的链路空闲检测机制,可以非常灵活的实现协议层的心跳检测。

// IdleStateHandler
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
    ctx.fireUserEventTriggered(evt);
}
Reactor线程的保护
异常处理要谨慎
@Override
protected void run() {
    for (;;) {
        oldWakenUp = wakenUp.getAndSet(false);
        try {
            if (hasTasks()) {
                selectNow();
            } else {
                select();
                if (wakenUp.get()) {
                    selector.wakeup();
                }
            }
            // ...
        } catch (Throwable t) {
            // 循环体内捕获Throwable,防止发生意外导致循环失败
            logger.warn("Unexpected exception in the selector loop.", t);
            // Prevent possible consecutive immediate failures that lead to
            // excessive CPU consumption.
            // 防止可能导致CPU消耗过多的连续即时故障。
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // Ignore.
            }
        }
    }
}

如果仅仅捕获IO异常可能就会导致Reactor线程跑飞。为了防止发生这种意外,在循环体内一定要捕获Throwable,而不是IO异常或者Exception。
捕获Throwable之后,即便发生了意外未知对异常,线程也不会跑飞,它休眠1S,防止死循环导致的异常绕接,然后继续恢复执行。

这样处理的核心理念就是:

  • 某个消息的异常不应该导致整条链路不可用;
  • 某条链路不可用不应该导致其他链路不可用;
  • 某个进程不可用不应该导致其他集群节点不可用。
规避NIO BUG

参考《NioEventLoop》对JDK的select空循环进行规避,通过采样检测和重建使系统恢复正常。

内存保护

NIO通信的内存保护主要集中在如下几点:

  • 链路总数的控制:每条链路都包含接收和发送缓冲区,链路个数太多容易导致内存溢出;
  • 单个缓冲区的上限控制:防止非法长度或者消息过大导致内存溢出;
  • 缓冲区内存释放:防止因为缓冲区使用不当导致的内存泄露;
  • NIO消息发送队列的长度上限控制。
缓冲区的内存泄露保护

为了防止因为用户遗湍导致内存泄湍,Netty在Pipeline的尾Handler中自动对内存进行释放,TailHandler的内存回收代码如下:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    try {
        logger.debug(
                "Discarded inbound message {} that reached at the tail of the pipeline. " +
                        "Please check your pipeline configuration.", msg);
    } finally {
        ReferenceCountUtil.release(msg);
    }
}
// PooledByteBuf
@Override
protected final void deallocate() {
    if (handle >= 0) {
        final long handle = this.handle;
        this.handle = -1;
        memory = null;
        chunk.arena.free(chunk, handle);
        recycle();
    }
}

对于实现了AbstractReferenceCountedByteBuf的ByteBuf,内存申请、使用和释放的时候Netty都会自动进行引用计数检测,防止非法使用内存。

缓冲区溢出保护

对消息进行解码的时候,需要创建缓冲区。缓冲区的创建方式通常有两种:

  • 容量预分配,在实际读写过程中如果不够再扩展;
  • 根据协议消息长度创建缓冲区。

如果遇到畸形码流攻击、协议消息编码异常、消息丢包等问题时,可能会解析到一个超长的长度字段,导致分配时内存溢出。

Netty提供了编解码框架,因此对于解码缓冲区的上限保护就显得非常重要。

  • 首先,在内存分配的时候指定缓冲区长度上限;
  • 其次,在对缓冲区进行写入操作的时候,如果缓冲区容量不足需要扩展,首先对最大容量进行判断,如果扩展后的容量超过上限,则拒绝扩展;
  • 在消息解码的时候,对消息长度进行判断,如果超过最大容量上限,则抛出解码异常,拒绝分配内存。

参考《LengthFieldBasedFrameDecoder源码解析》

流量整形

大多数的商用系统都有多个网元或者部件组成,例如参与短信互动,会涉及手机、基站、短信中心、短信网关、SP/CP等网元。 不同网元或者部件的处理性能不同。为了防止因为浪涌业务或者下游网元性能低导致下游网元被压垮,有时候需要系统提供流足整形功能。

流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。

一个典型应用是基于下游网络结点的TP指标来控制本地流量的输出。流量整形与流量监管的主要区别在于, 流量整形对流量监管中需要丢弃的报文进行缓存——通常是将它们放入缓冲区或队列内,也称流量整形(Traffic Shaping,简称TS)。 当令牌桶有足够的令牌时,再均匀的向外发送这些被缓存的报文。
流量整形与流量监管的另一区别是,整形可能会增加延迟,而监管几乎不引入额外的延迟。

流量整形原理图

作为高性能的NIO框架,Netty的流量整形有两个作用:

  • 防止由于上下游网元性能不均衡导致下游网元被压垮,业务流程中断;
  • 防止由于通信模块接收消息过快,后端业务线程处理不及时导致的“撑死”问题。
全局流量整形

全局流量整形的作用范围是进程级的,无论你创建了多少个Channel,它的作用域针对所有的Channel。

用户可以通过参数设置:报文的接收速率、报文的发送速率、整形周期。

public GlobalTrafficShapingHandler(ScheduledExecutorService executor, long writeLimit,
        long readLimit, long checkInterval) {
    super(writeLimit, readLimit, checkInterval);
    createGlobalTrafficCounter(executor);
}

Netty流量整形的原理是:对每次读取到的ByteBuf可写字节数进行计算,获取当前的报文流量,然后与流量整形阈值对比。如果已经达到或者超过了阈值。 则计算等待时间delay,将当前的ByteBuf放到定时任务Task中缓存,由定时任务线程池在延迟delay之后继续处理该ByteBuf。

public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
    long size = calculateSize(msg);
    long curtime = System.currentTimeMillis();

    if (trafficCounter != null) {
        // 整形控制
        trafficCounter.bytesRecvFlowControl(size);
        if (readLimit == 0) {
            // no action
            ctx.fireChannelRead(msg);

            return;
        }
        // 获取延迟时间
        long wait = getTimeToWait(readLimit,
                    trafficCounter.currentReadBytes(),
                    trafficCounter.lastTime(), curtime);
        if (wait >= MINIMAL_WAIT) { // At least 10ms seems a minimal
            // time in order to
            // try to limit the traffic
            if (!isSuspended(ctx)) {
                ctx.attr(READ_SUSPENDED).set(true);

                // Create a Runnable to reactive the read if needed. If one was create before it will just be
                // reused to limit object creation
                Attribute<Runnable> attr  = ctx.attr(REOPEN_TASK);
                Runnable reopenTask = attr.get();
                if (reopenTask == null) {
                    reopenTask = new ReopenReadTimerTask(ctx);
                    attr.set(reopenTask);
                }
                // 放入队列中稍后处理
                ctx.executor().schedule(reopenTask, wait,
                        TimeUnit.MILLISECONDS);
            } else {
                // Create a Runnable to update the next handler in the chain. If one was create before it will
                // just be reused to limit object creation
                Runnable bufferUpdateTask = new Runnable() {
                    @Override
                    public void run() {
                        ctx.fireChannelRead(msg);
                    }
                };
                ctx.executor().schedule(bufferUpdateTask, wait, TimeUnit.MILLISECONDS);
                return;
            }
        }
    }
}
// 定时任务的延时时间根据检测周期T和流量整形阈值计算得来
private static long getTimeToWait