在正式進入主題之前,先要看看一些基本的理論。這裏旨在明確這些基礎的概念,好更深刻的進一步理解Netty。
首先,什麼是IO?其實平常其實工作中用得也是比較多的了,這裏簡單做個總結。
I:InputStream,字節輸入流 ,用於讀取數據爲字節流《Reads the next byte of data from the input stream》
O:OutputStream,字節輸出流,用於將字節流寫入到流《Writes the specified byte to this output stream》
流,啥是流?日常生活中,聽過比較多的就是河流、水流、車流等等,這裏就拿車流做比喻。
假定小C要結婚了,請了一個豪華車隊,去新娘老家接新娘到酒店,於是就有了這麼個概念:這裏有一車隊從新娘家,經過一路到達到酒店。這裏有起始點,有結束點,可以理解爲數據的產出點和數據的接收點。這個過程,可以理解爲建立一個傳輸通道(車隊走過的一路),通過流的方式,將數據從一個地方傳輸到了另外一個地方。當有這麼一種情況,每一輛車只能載一個人,同時,一輛車裝完一個人,然後就開車出發,然後下一輛車,在上一個人,出發,持續這麼過程,可以理解爲字節輸入流(一個上車),到達目的地後,下一個人,可以理解爲字節輸出流(一個人下車)。這時候,可能就太慢了,於是新郎新娘決定,一輛車多上些人,把一家人的那種單獨上一輛車,此時,可以理解爲字符流(以字符的方式輸入輸出),與此同時,車隊上滿了之後,一個車隊一起走,理解這種情況爲緩衝流(一個車隊可能就一次把人都帶走了)。於是,流就可以簡單分爲字節流和字符流,同時兩者都可以通過包含緩衝區的方式傳輸,也就都存在字節緩衝流和字符緩衝流。
下面看下其相互依賴關係(這裏僅僅截取常用到的,實際並不僅僅這些,詳細請查看API)
字節流:
字符流:
那麼接下來家假設這麼一種情況,小C邀請其好友小D全程協助車隊出發到達這麼一個過程,把小D全程協助的過程,稱爲線程,也就是小D線程來做這件事,於是車隊上車開始,小D就只能在這裏逐個看着每個車上乘客,然後沿着一條路到達酒店,假定這一路屬於鄉村公路,也就是單行道,往酒店走的,則只能往酒店走,反之亦然。
IO(BIO)、NIO、NIO2(AIO)
有需要的同學,可關注公衆號“依蕁”,不定期分享技術乾貨
基於上述過程,可以理解爲傳統IO的一個操作,也叫BIO,即同步阻塞IO,同步在於,讀的時候不能寫,寫的時候不能讀(類似上述過程,單行道車流),當然這裏也可以用多線程去做一個僞異步,這裏不過這種探討;阻塞在於,讀和寫這個過程阻塞的,讀/寫需要線程把所有的數據全部讀/寫完畢,或者發生異常,這個線程才能結束(類似上述過程,小D全程協助時,在車隊上乘客的時候,小D只能等着乘客都上車完畢後,他才能繼續做下一步操作)。
同步阻塞IO在實際應用中,會存在很多弊端,用之前寫的小demo(詳情請見《基於socket通信編寫的聊天工具》)爲例,解釋下這種同步阻塞存在的一些問題和弊端
看服務端的主要代碼,在可以獲取多個客戶端連接的情況下,採用了多線程監聽:
如果存在大量客戶端連接的情況下,服務端是會開闢很多線程來監聽並獲取數據,首先多線程的線程間交替本身就很好系統性能,再者過多的服務端線程易造成堆棧溢出,創建線程失敗等情況的發生,嚴重甚至發生服務器宕機等情況的發生
由於讀寫操作屬於阻塞的(不僅僅是讀寫操作,還有上圖中,等待連接的時候,也是線程阻塞的),那麼就有可能存在大量連接的線程出現在一個阻塞的情況,服務端處理速度過慢,後續的客戶端連接容易出現連接不上服務端的情況。同時寫的時候不能讀,讀數據的時候不能寫,這也是一種很不良好的體驗。
如何比較良好的解決這些問題?
小C的新娘告訴他,現在政府新修建了高速公路,雙向車道,於是車可以從上面過,同時基於雙車道,車輛可以方便在接送切換,不需要再去遵循哪條路是來的路,哪條路是去的路,再者小D覺得這樣子挨個守着太累而且費時,於是留下每個車師傅的電話,輪詢的方式瞭解到車輛目前的狀態(是否上好了人,是否到達目的地等等),方便指揮車輛的下一個操作。
對於上述改進的接送人操作,映射到JDK1.4以後的NIO,同樣對傳統的IO操作,進行類似的改進,相對於傳統的流(單向鄉間小道),變成了現在的通道Channel(牛叉的雙向高速公路)傳輸數據,傳統的阻塞讀寫操作(守候車輛上下人),變成了選擇器-多路複用器Selector輪詢當前通道Channel中的數據狀態,進行後續IO操作(遠程輪詢車輛狀態,安排工作)
通道(Channel)、多路複用器(Selector)是NIO(官方稱爲new IO,也稱爲non-blocking IO)的三大重要概念中的2種了,下面介紹下另外一個概念–緩衝區(buffer)
傳統BIO數據讀寫主要針對字節/字符操作,而NIO讀寫數據則是針對緩衝區操作,任何的NIO都是操作緩衝區,緩衝區內部是數組,並且提供了對數據結構化訪問及其維護讀寫位置信息
以下是幾個常用的緩衝區的相互依賴關係:
這裏用代碼描述幾個核心屬性:
package com.lgli.nio.api;
import java.nio.ByteBuffer;
/**
* NioApi properties
* capacity:緩衝區中的最大容量
* limit:限制,緩衝區中最大可操作容量
* position:位置,當前操作的緩衝區的位置
* mark:標記,記錄讀取的緩衝區的位置
* @author lgli
* @since 1.0
*/
public class NioApi {
public static void main(String[] args) {
String str = "hello,世界";
// 1 分配一塊指定大小的非直接緩衝區
ByteBuffer allocate = ByteBuffer.allocate(1024);
// ByteBuffer allocates = ByteBuffer.allocateDirect(1024);
System.out.println("分配非直接緩衝區後緩衝區容量(capacity):"+allocate.capacity());
System.out.println("分配非直接緩衝區後緩衝區限制(limit):"+allocate.limit());
System.out.println("分配非直接緩衝區後緩衝區位置(position):"+allocate.position());
// 2 向緩衝區中放入數據
allocate.put(str.getBytes());
System.out.println("緩衝區中寫入數據後緩衝區容量(capacity):"+allocate.capacity());
System.out.println("緩衝區中寫入數據後緩衝區限制(limit):"+allocate.limit());
System.out.println("緩衝區中寫入數據後緩衝區位置(position):"+allocate.position());
// 3 切換讀寫模式,由前面的寫切換爲讀
allocate.flip();
System.out.println("切換讀取數據後緩衝區容量(capacity):"+allocate.capacity());
System.out.println("切換讀取數據後緩衝區限制(limit):"+allocate.limit());
System.out.println("切換讀取數據後緩衝區位置(position):"+allocate.position());
// 4 讀取緩衝區中的數據
ByteBuffer byteBuffer = allocate.get(str.getBytes(), allocate.position(), allocate.limit());
System.out.println("讀取緩衝區中的數據爲:"+new String(byteBuffer.array()));
System.out.println("讀取緩衝區後緩衝區容量(capacity):"+allocate.capacity());
System.out.println("讀取緩衝區後緩衝區限制(limit):"+allocate.limit());
System.out.println("讀取緩衝區後緩衝區位置(position):"+allocate.position());
//切換爲重新讀取緩衝區中的數據
allocate.rewind();
System.out.println("切換爲重新讀取緩衝區中的數據後緩衝區容量(capacity):"+allocate.capacity());
System.out.println("切換爲重新讀取緩衝區中的數據後緩衝區限制(limit):"+allocate.limit());
System.out.println("切換爲重新讀取緩衝區中的數據後緩衝區位置(position):"+allocate.position());
//再次讀取緩衝區中的數據
byte [] strArr = new byte[allocate.limit()];
byteBuffer.get(strArr);
System.out.println("再次讀取緩衝區中的數據:"+new String(strArr,0,allocate.limit()));
System.out.println("再次讀取緩衝區中的數據後緩衝區容量(capacity):"+allocate.capacity());
System.out.println("再次讀取緩衝區中的數據後緩衝區限制(limit):"+allocate.limit());
System.out.println("再次讀取緩衝區中的數據後緩衝區位置(position):"+allocate.position());
}
}
上述代碼同時也描述了NIO在讀寫文件操作的具體過程,即:
上述創建緩衝區的方式有2種:
java.nio.ByteBuffer#allocate
java.nio.ByteBuffer#allocateDirect
這兩種方式,均是獲取內存中指定大小的緩衝區,其中allocate是指在JVM堆內存中分配一塊兒緩衝區,如下源碼:
而allocateDirect是指在JVM堆外內存中分配內存空間,源碼如下:
點擊進去:
有興趣的朋友可以追根到底的瞭解下JVM堆內內存和堆外內存的具體詳細區別和差異,網上有對此做出大量分析的博文。這裏只做簡單說明(如果有不正確的請指出),Java的IO操作(不僅僅只是IO),通常會在JVM堆中和堆外存在2份數據,即JVM堆中存在一份數據,然後拷貝到堆外,真正操作系統層面所操作到的數據是堆外的數據,因爲JVM堆中存在的數據受到MinorGC影響較大,導致數據移動較大,同時Java的IO操作最底層的byte數組也不一定是一個連續的地址空間, 然後Java的IO操作,最終都會調用到操作系統的IO接口,而操作系統的IO操作,需要一個準確連續的數據地址,所以相對堆中的數據地址對於操作系統而言,極大程度來說就不是一個正確的數據地址空間,所以Java的IO操作,在堆內內存中需要一份操作的數據,然後複製一份到堆外內存中。
此時,allocate方法就是按照常規的在JVM堆內中分配空間,然後copy到堆外,供操作系統層面操作
而allocateDirect則是省掉了JVM對內分配空間然後拷貝到堆外內存的操作,直接在堆外內存分配內存
這個時候,有個疑問,爲啥需要在JVM堆內分配空間,然後copy,直接allocateDirect多好。其實兩者有弊有利。
allocateDirect,對於讀寫文件效率高,但是分配和取消內存所耗費成本較大。
allocate,效率偏低,但是直接操作在JVM內存中,其耗費承成本較低。
兩者差異其實在讀取字節數量級較小的時候,差距並不是那麼明顯,只有當Buffer容量到達一定的級別後,差距才比較突出,這裏引用別人的一張圖片描述下,就不做過多深究了
一般來說,應用程序在讀取文件操作,首先通過操作系統接口層面讀取文件接口將文件加載到物理內核中,在通過一個複製操作,將文件緩衝到應用程序內存,供應用程序操作,然後應用程序操作後,複製到物理內核中,最後將文件讀出到磁盤,即下面過程:
NIO基於文件操作,實現了一種利用內存映射文件的方式來讀取寫入文件,其實現機制主要通過直接操作內存映射文件,而不需要應用程序和系統接口層面同時拷貝操作文件,下面用圖解表示這一過程:
這裏用3段代碼描述下,同樣的複製一個文件,描述三者的區別:
package com.lgli.nio.copy;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* NioCopyFile
* 比較複製複製文件性能
* @author lgli
* @since 1.0
*/
public class NioCopyFile {
public static void main(String[] args) throws Exception{
// copyByAllocate();
// copyByAllocateDirect();
copyByAllocateDirectMapped();
}
/**
* 此方法通過分配非直接緩衝區,然後來複制文件
* @throws Exception Exception
*/
private static void copyByAllocate() throws Exception{
long start = System.currentTimeMillis();
//獲取文件讀取通道,方法需要傳入參數,路徑+文件模式《這裏表示讀取》
FileChannel in = FileChannel.open(Paths.get("F:\\entertainment\\movie\\馬達加斯加的企鵝.mkv"),
StandardOpenOption.READ);
//獲取文件寫入通道,方法傳入參數,路徑+文件模式《這裏可讀可寫,後面表示創建或者替換(已經存在則替換)》
FileChannel out = FileChannel.open(Paths.get("F:\\entertainment\\movie\\馬達加斯加的企鵝-01.mkv"),
StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
//讀取文件至緩衝區
while(in.read(byteBuffer)>0){
//切換至讀取模式
byteBuffer.flip();
//把緩衝區中的數據寫出
out.write(byteBuffer);
//清空緩衝區,再次讀取文件
byteBuffer.clear();
}
in.close();
out.close();
System.out.println("非直接緩衝區方式複製文件耗費:"+(System.currentTimeMillis()-start)+"毫秒!");
}
/**
* 此方法通過分配直接緩衝區複製文件
* @throws Exception Exception
*/
private static void copyByAllocateDirect() throws Exception{
long start = System.currentTimeMillis();
//獲取文件讀取通道,方法需要傳入參數,路徑+文件模式《這裏表示讀取》
FileChannel in = FileChannel.open(Paths.get("F:\\entertainment\\movie\\馬達加斯加的企鵝.mkv"),
StandardOpenOption.READ);
//獲取文件寫入通道,方法傳入參數,路徑+文件模式《這裏可讀可寫,後面表示創建或者替換(已經存在則替換)》
FileChannel out = FileChannel.open(Paths.get("F:\\entertainment\\movie\\馬達加斯加的企鵝-02.mkv"),
StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(Integer.MAX_VALUE);
//讀取文件至緩衝區
while(in.read(byteBuffer)>0){
//切換至讀取模式
byteBuffer.flip();
//把緩衝區中的數據寫出
out.write(byteBuffer);
//清空緩衝區,再次讀取文件
byteBuffer.clear();
}
in.close();
out.close();
System.out.println("直接緩衝區方式複製文件耗費:"+(System.currentTimeMillis()-start)+"毫秒!");
}
/**
* 方法通過內存映射文件複製文件
* @throws Exception Exception
*/
private static void copyByAllocateDirectMapped() throws Exception{
long start = System.currentTimeMillis();
//獲取文件讀取通道,方法需要傳入參數,路徑+文件模式《這裏表示讀取》
FileChannel in = FileChannel.open(Paths.get("F:\\entertainment\\movie\\馬達加斯加的企鵝.mkv"),
StandardOpenOption.READ);
//獲取文件寫入通道,方法傳入參數,路徑+文件模式《這裏可讀可寫,後面表示創建或者替換(已經存在則替換)》
FileChannel out = FileChannel.open(Paths.get("F:\\entertainment\\movie\\馬達加斯加的企鵝-03.mkv"),
StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
//獲取讀通道的緩衝區
MappedByteBuffer inMapped = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size());
//獲取寫通道緩衝區
MappedByteBuffer outMapped = out.map(FileChannel.MapMode.READ_WRITE, 0, in.size());
byte[] bytes = new byte[inMapped.limit()];
inMapped.get(bytes);
outMapped.put(bytes);
in.close();
out.close();
System.out.println("MappedByteBuffer映射文件方式複製文件耗費:"+(System.currentTimeMillis()-start)+"毫秒!");
}
}
運行上述代碼,通過時間對比可以看出,當操作的Buffer的容量不是很大的時候,其實非直接緩衝區和直接緩衝區複製文件差不多,但是利用內存映射文件的方式,效率是高了很多,但是內存映射文件呢存在的一個問題就是,在運行的時候,會佔用較大的CPU內存,易出現程序假死狀態(這時候文件複製其實已經完成)。
下面,基於NIO通信,對之前的聊天系統進行改造
其客戶端和服務端連接方式改爲如下:
基於服務端,主要思想如下:
輪詢期間,可根據不同的狀態,做不一樣的事,而不是排隊等待
客戶端,主要思想如下:
下面看代碼,服務端:
package com.lgli.nio.chart.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.Set;
/**
* NioChartService
* @author lgli
* @since 1.0
*/
public class NioChartServer {
//多路複用器
private Selector selector;
private int port = 8080;
public NioChartServer() {
try {
//打開多路複用器
selector = Selector.open();
//打開數據傳輸管道,服務端打開serversocket
ServerSocketChannel socketChannel = ServerSocketChannel.open();
//設置管道非阻塞
socketChannel.configureBlocking(false);
//將管道綁定到socket上,建立服務器監聽
socketChannel.bind(new InetSocketAddress(port));
//將數據傳輸管道註冊到多路複用器上,狀態爲等待連接
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
//輸出打印服務端提示
System.out.println("服務端啓動服務連接監聽,端口:"+port);
Runnable runnable = ()->{
while(true){
try{
//等待時間100毫秒,返回可用數據傳輸管道的數量
int channelCounts = selector.select(100);
if(channelCounts <= 0){
//沒有則繼續循環監聽
continue;
}
//獲取所有的選擇通道集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//循環處理通道中的事件
//java新特性寫法
//selectionKey是循環的每個參數對象;param是需要傳入的其他參數
//selectionKeys.forEach(this::dealWith);
selectionKeys.forEach(selectionKey->dealWith(selectionKey,socketChannel));
//清空處理過的事件
selectionKeys.clear();
}catch (Exception e){
e.printStackTrace();
}
}
};
new Thread(runnable).start();
Scanner scanner = new Scanner(System.in);
while(scanner.hasNextLine()){
//服務端發送數據到客戶端
String s = scanner.nextLine();
SocketChannel accept = socketChannel.accept();
System.out.println(accept);
accept.write(StandardCharsets.UTF_8.encode(s));
}
}catch (Exception e){
e.printStackTrace();
}
}
private void dealWith(SelectionKey selectionKey,ServerSocketChannel socketChannel) {
if(null == selectionKey){
return;
}
if(!selectionKey.isValid()){
//無效連接,直接退出
return;
}
if(selectionKey.isAcceptable()){
//管道處於可接受狀態,
try{
//獲取數據傳輸管道
SocketChannel sc = socketChannel.accept();
//設置爲非阻塞
sc.configureBlocking(false);
//註冊多路複用器,設置爲可讀取模式,即當前連接的數據傳輸管道,註冊到多路複用器上,之後好處理這個連接中的數據傳輸
sc.register(selector,SelectionKey.OP_READ);
//將對應的此管道設置爲其他連接可連接
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
//打印輸出,服務端獲取到此連接
System.out.println("服務端收到連接:"+sc.getRemoteAddress());
//歡迎語發送給客戶端
sc.write(StandardCharsets.UTF_8.encode("歡迎您:"+sc.getRemoteAddress()));
}catch (Exception e){
e.printStackTrace();
}
}else if(selectionKey.isReadable()){
//管道有數據傳輸過來,即,服務端可以讀取數據
//獲取到管道
SocketChannel sc = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
StringBuilder stringBuilder = new StringBuilder();
try{
while(sc.read(byteBuffer)>0){
//切換讀寫模式
byteBuffer.flip();
stringBuilder.append(StandardCharsets.UTF_8.decode(byteBuffer));
}
//打印客戶端回覆的數據
System.out.println("服務端收到"+sc.getRemoteAddress()+"說:"+stringBuilder.toString());
//將此對應的數據傳輸管道設置爲下一次可讀取數據
selectionKey.interestOps(SelectionKey.OP_READ);
}catch (Exception e){
selectionKey.cancel();
if(selectionKey.channel() != null){
try {
selectionKey.channel().close();
}catch (Exception es){
es.printStackTrace();
}
}
e.printStackTrace();
}
if(stringBuilder.length()>0){
//將客戶端發送的數據,發送給其他客戶端
System.out.println("準備發送數據.....");
Set<SelectionKey> keys = selector.keys();
keys.forEach(keyKeys-> {
try {
sendMsg(keyKeys,sc,sc.getRemoteAddress()+":"+stringBuilder.toString());
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
}
private void sendMsg(SelectionKey keyKeys, SocketChannel thisSc,String toString) {
//獲取所有註冊到多路複用器中的通道
Channel channel = keyKeys.channel();
if(!(channel instanceof SocketChannel)){
return;
}
SocketChannel sc = (SocketChannel) channel;
try {
//發送給非當前通道連接的客戶端
boolean isEq = sc.getRemoteAddress().toString().equals(thisSc.getRemoteAddress().toString());
if(isEq){
return;
}
sc.write(StandardCharsets.UTF_8.encode(toString));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new NioChartServer();
}
}
客戶端:
package com.lgli.nio.chart.client;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.Set;
/**
* NioChartClient
* @author lgli
* @since 1.0
*/
public class NioChartClient {
private Selector selector;
private SocketChannel socketChannel;
public NioChartClient(int port) {
try{
//打開多路複用器
selector = Selector.open();
//打開連接服務端的數據傳輸管道
socketChannel = SocketChannel.open(new InetSocketAddress("localhost",port));
//設置非阻塞
socketChannel.configureBlocking(false);
//將傳輸管道註冊到多路複用器上
socketChannel.register(selector, SelectionKey.OP_READ);
}catch (Exception e){
e.printStackTrace();
}
}
private void monitor() {
//接受服務端發過來的數據,這裏由於鍵盤輸入是線程阻塞的,所以這裏新起一個線程來進行這個操作
Runnable runnable = () ->{
while(true){
try{
int select = selector.select(100);
if(select <= 0){
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
selectionKeys.forEach(this :: dealWith);
selectionKeys.clear();
}catch (Exception e){
e.printStackTrace();
}
}
};
new Thread(runnable).start();
//監聽鍵盤輸入向服務端寫入數據,
Scanner scanner = new Scanner(System.in);
try{
while(scanner.hasNext()){
String next = scanner.next();
if(!"".equals(next)){
socketChannel.write(StandardCharsets.UTF_8.encode(next));
}
}
}catch (Exception e){
e.printStackTrace();
}
}
private void dealWith(SelectionKey sc) {
if(!sc.isValid()){
//無效退出
System.out.println("無效連接退出");
return;
}
if(sc.isReadable()){
//可讀,則讀取服務端發送來的數據
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
//獲取通道
SocketChannel scl = (SocketChannel)sc.channel();
StringBuilder sb = new StringBuilder();
try{
while(scl.read(byteBuffer)>0){
byteBuffer.flip();
sb.append(StandardCharsets.UTF_8.decode(byteBuffer));
}
if(sb.length()>0){
// System.out.println(scl.getRemoteAddress()+"說:"+sb.toString());
System.out.println(sb.toString());
}
}catch (Exception e){
e.printStackTrace();
}
sc.interestOps(SelectionKey.OP_READ);
}
}
public static void main(String[] args) {
new NioChartClient(8080).monitor();
}
}
然後運行服務端,接着運行多個客戶端,這裏用IntelliJ IDEA的小夥伴,提示下,可以並行跑相同代碼,只需要編輯配置即可(部分低版本的不行)
把這個勾上即可,於是達到了理想的狀態了
這兒本來有個視頻效果的,公衆號裏面可以看,這裏不能上傳視頻
下面簡單瞭解下NIO2,其實NIO是在傳統的BIO基礎上實現的同步非阻塞IO,那麼對於java1.7之後引入的NIO2(也叫AIO),就是一個異步非阻塞的IO,更加的優化了許多的操作,比較重大的改進是:1)比較全面的文件IO操作和對文件系統訪問的支持;2)異步通道的IO
對於NIO2的詳細介紹、及其Netty的引入將在後續中更新,本次若有不正確的地方,煩請指出。