高性能Server---Reactor模型

無處不在的C/S架構

在這個充斥着雲的時代,咱們使用的軟件能夠說99%都是C/S架構的!html

  • 你發郵件用的Outlook,Foxmail等
  • 你看視頻用的優酷,土豆等
  • 你寫文檔用的Office365,googleDoc,Evernote等
  • 你瀏覽網頁用的IE,Chrome等(B/S是特殊的C/S)
  • ……

C/S架構的軟件帶來的一個明顯的好處就是:只要有網絡,你能夠在任何地方幹同一件事。java

例如:你在家裏使用Office365編寫了文檔。到了公司,只要打開編輯地址就能夠看到在家裏編寫的文檔,進行展現或者繼續編輯。甚至在手機上進行閱讀與編輯。再也不須要U盤拷來拷去了。react

C/S架構能夠抽象爲以下模型:git

  • C就是Client(客戶端),上面的B是Browser(瀏覽器)
  • S就是Server(服務器):服務器管理某種資源,而且經過操做這種資源來爲它的客戶端提供某種服務

C/S架構之因此可以流行的一個主要緣由就是網速的提升以及費用的下降,特別是無線網絡速度的提升。試想在2G時代,你們最多就是看看文字網頁,小說什麼的。看圖片,那簡直就是奢侈!更別說看視頻了!github

網速的提升,使得愈來愈多的人使用網絡,例如:優酷,微信都是上億用戶量,更別說天貓雙11的瞬間訪問量了!這就對服務器有很高的要求!可以快速處理海量的用戶請求!那服務器如何能快速的處理用戶的請求呢?編程

高性能服務器

高性能服務器至少要知足以下幾個需求:瀏覽器

  • 效率高:既然是高性能,那處理客戶端請求的效率固然要很高了
  • 高可用:不能隨便就掛掉了
  • 編程簡單:基於此服務器進行業務開發須要足夠簡單
  • 可擴展:可方便的擴展功能
  • 可伸縮:可簡單的經過部署的方式進行容量的伸縮,也就是服務須要無狀態

而知足如上需求的一個基礎就是高性能的IO!安全

Socket

不管你是發郵件,瀏覽網頁,仍是看視頻~實際底層都是使用的TCP/IP,而TCP/IP的編程抽象就是Socket!服務器

我一直對Socket的中文翻譯很困惑,我的以爲是我所接觸的技術名詞翻譯裏最莫名其妙的,沒有之一!微信

Socket中文翻譯爲」套接字」!什麼鬼?在很長的時間裏我都沒法將其和網絡編程關聯上!後來專門找了一些資料,最後在知乎上找到了一個還算滿意的答案(具體連接,請見文末的參考資料連接)!

Socket的原意是插口,想表達的意思是插口與插槽的關係!」send socket」插到」receive socket」裏,創建了連接,而後就能夠通訊了!

套接字的翻譯,應該是參考了套接管(以下圖)!從這個層面上來看,是有那麼點意思!

套接字這個翻譯已是標準了,不糾結這個了!

咱們看一下Socket之間創建連接及通訊的過程!實際上就是對TCP/IP鏈接與通訊過程的抽象:

  • 服務端Socket會bind到指定的端口上,Listen客戶端的」插入」
  • 客戶端Socket會Connect到服務端
  • 當服務端Accept到客戶端鏈接後
  • 就能夠進行發送與接收消息了
  • 通訊完成後便可Close

對於IO來講,咱們聽得比較多的是:

  • BIO:阻塞IO
  • NIO:非阻塞IO
  • 同步IO
  • 異步IO

以及其組合:

  • 同步阻塞IO
  • 同步非阻塞IO
  • 異步阻塞IO
  • 異步非阻塞IO

那麼什麼是阻塞IO、非阻塞IO、同步IO、異步IO呢?

  • 一個IO操做其實分紅了兩個步驟:發起IO請求和實際的IO操做
  • 阻塞IO和非阻塞IO的區別在於第一步:發起IO請求是否會被阻塞,若是阻塞直到完成那麼就是傳統的阻塞IO;若是不阻塞,那麼就是非阻塞IO
  • 同步IO和異步IO的區別就在於第二個步驟是否阻塞,若是實際的IO讀寫阻塞請求進程,那麼就是同步IO,所以阻塞IO、非阻塞IO、IO複用、信號驅動IO都是同步IO;若是不阻塞,而是操做系統幫你作完IO操做再將結果返回給你,那麼就是異步IO

舉個不太恰當的例子 :好比你家網絡斷了,你打電話去中國電信報修!

  • 你撥號—客戶端鏈接服務器
  • 電話通了—鏈接創建
  • 你說:「我家網斷了,幫我修下」—發送消息
  • 說完你就在那裏等,那麼就是阻塞IO
  • 若是正好你有事,你放下帶電話,而後處理其餘事情了,過一會你來問下,修好了沒—那就是非阻塞IO
  • 若是客服說:「立刻幫你處理,你稍等」—同步IO
  • 若是客服說:「立刻幫你處理,好了通知你」,而後掛了電話—異步IO

本文只討論BIO和NIO,AIO使用度沒有前二者普及,暫不討論!

下面從代碼層面看看BIO與NIO的流程!

BIO

  • 客戶端代碼
1
2
3
4
5
6
7
8
9
10
//Bind,Connect
Socket client = new Socket("127.0.0.1",7777);
//讀寫
PrintWriter pw = new PrintWriter(client.getOutputStream());
BufferedReader br=
new BufferedReader(new InputStreamReader(System.in));
pw.write(br.readLine());
//Close
pw.close();
br.close();
  • 服務端代碼
1
2
3
4
5
6
7
8
9
10
11
Socket socket;
//Bind,Listen
ServerSocket ss = new ServerSocket(7777);
while (true) {
//Accept
socket = ss.accept();
//通常新建一個線程執行讀寫
BufferedReader br = new BufferedReader(
new InputStreamReader(socket .getInputStream()));
System.out.println( "you input is : " + br.readLine());
}
  • 上面的代碼能夠說是學習Java的Socket的入門級代碼了
  • 代碼流程和前面的圖能夠一一對上

模型圖以下所示:

BIO優缺點

  • 優勢
    • 模型簡單
    • 編碼簡單
  • 缺點
    • 性能瓶頸低

優缺點很明顯。這裏主要說下缺點:主要瓶頸在線程上。每一個鏈接都會創建一個線程。雖然線程消耗比進程小,可是一臺機器實際上能創建的有效線程有限,以Java來講,1.5之後,一個線程大體消耗1M內存!且隨着線程數量的增長,CPU切換線程上下文的消耗也隨之增長,在高過某個閥值後,繼續增長線程,性能不增反降!而一樣由於一個鏈接就新建一個線程,因此編碼模型很簡單!

就性能瓶頸這一點,就肯定了BIO並不適合進行高性能服務器的開發!像Tomcat這樣的Web服務器,從7開始就從BIO改爲了NIO,來提升服務器性能!

NIO

  • NIO客戶端代碼(鏈接)
1
2
3
4
5
6
7
8
//獲取socket通道
SocketChannel channel = SocketChannel.open();
channel.configureBlocking( false);
//得到通道管理器
selector=Selector.open();
channel.connect( new InetSocketAddress(serverIp, port));
//爲該通道註冊SelectionKey.OP_CONNECT事件
channel.register(selector, SelectionKey.OP_CONNECT);
  • NIO客戶端代碼(監聽)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
while(true){
//選擇註冊過的io操做的事件(第一次爲SelectionKey.OP_CONNECT)
selector.select();
while(SelectionKey key : selector.selectedKeys()){
if(key.isConnectable()){
SocketChannel channel=(SocketChannel)key.channel();
if(channel.isConnectionPending()){
channel.finishConnect(); //若是正在鏈接,則完成鏈接
}
channel.register(selector, SelectionKey.OP_READ);
} else if(key.isReadable()){ //有可讀數據事件。
SocketChannel channel = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate( 10);
channel.read(buffer);
byte[] data = buffer.array();
String message = new String(data);
System.out.println( "recevie message from server:, size:"
+ buffer.position() + " msg: " + message);
}
}
}
  • NIO服務端代碼(鏈接)
1
2
3
4
5
6
7
8
//獲取一個ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking( false);
serverChannel.socket().bind( new InetSocketAddress(port));
//獲取通道管理器
selector = Selector.open();
//將通道管理器與通道綁定,併爲該通道註冊SelectionKey.OP_ACCEPT事件,
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
  • NIO服務端代碼(監聽)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
while(true){
//當有註冊的事件到達時,方法返回,不然阻塞。
selector.select();
for(SelectionKey key : selector.selectedKeys()){
if(key.isAcceptable()){
ServerSocketChannel server =
(ServerSocketChannel)key.channel();
SocketChannel channel = server.accept();
channel.write(ByteBuffer.wrap(
new String("send message to client").getBytes()));
//在與客戶端鏈接成功後,爲客戶端通道註冊SelectionKey.OP_READ事件。
channel.register(selector, SelectionKey.OP_READ);
} else if(key.isReadable()){//有可讀數據事件
SocketChannel channel = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate( 10);
int read = channel.read(buffer);
byte[] data = buffer.array();
String message = new String(data);
System.out.println( "receive message from client, size:"
+ buffer.position() + " msg: " + message);
}
}
}

NIO模型示例以下:

  • Acceptor註冊Selector,監聽accept事件
  • 當客戶端鏈接後,觸發accept事件
  • 服務器構建對應的Channel,並在其上註冊Selector,監聽讀寫事件
  • 當發生讀寫事件後,進行相應的讀寫處理

NIO優缺點

  • 優勢
    • 性能瓶頸高
  • 缺點
    • 模型複雜
    • 編碼複雜
    • 需處理半包問題

NIO的優缺點和BIO就徹底相反了!性能高,不用一個鏈接就建一個線程,能夠一個線程處理全部的鏈接!相應的,編碼就複雜不少,從上面的代碼就能夠明顯體會到了。還有一個問題,因爲是非阻塞的,應用沒法知道何時消息讀完了,就存在了半包問題!

半包問題

簡單看一下下面的圖就能理解半包問題了!

咱們知道TCP/IP在發送消息的時候,可能會拆包(如上圖1)!這就致使接收端沒法知道何時收到的數據是一個完整的數據。例如:發送端分別發送了ABC,DEF,GHI三條信息,發送時被拆成了AB,CDRFG,H,I這四個包進行發送,接受端如何將其進行還原呢?在BIO模型中,當讀不到數據後會阻塞,而NIO中不會!因此須要自行進行處理!例如,以換行符做爲判斷依據,或者定長消息發生,或者自定義協議!

NIO雖然性能高,可是編碼複雜,且須要處理半包問題!爲了方便的進行NIO開發,就有了Reactor模型!

Reactor模型

  • AWT Events

Reactor模型和AWT事件模型很像,就是將消息放到了一個隊列中,經過異步線程池對其進行消費!

Reactor中的組件

  • Reactor:Reactor是IO事件的派發者。
  • Acceptor:Acceptor接受client鏈接,創建對應client的Handler,並向Reactor註冊此Handler。
  • Handler:和一個client通信的實體,按這樣的過程實現業務的處理。通常在基本的Handler基礎上還會有更進一步的層次劃分, 用來抽象諸如decode,process和encoder這些過程。好比對Web Server而言,decode一般是HTTP請求的解析, process的過程會進一步涉及到Listener和Servlet的調用。業務邏輯的處理在Reactor模式裏被分散的IO事件所打破, 因此Handler須要有適當的機制在所需的信息還不全(讀到一半)的時候保存上下文,並在下一次IO事件到來的時候(另外一半可讀了)能繼續中斷的處理。爲了簡化設計,Handler一般被設計成狀態機,按GoF的state pattern來實現。

對應上面的NIO代碼來看:

  • Reactor:至關於有分發功能的Selector
  • Acceptor:NIO中創建鏈接的那個判斷分支
  • Handler:消息讀寫處理等操做類

Reactor從線程池和Reactor的選擇上能夠細分爲以下幾種:

Reactor單線程模型

這個模型和上面的NIO流程很相似,只是將消息相關處理獨立到了Handler中去了!

雖然上面說到NIO一個線程就能夠支持全部的IO處理。可是瓶頸也是顯而易見的!咱們看一個客戶端的狀況,若是這個客戶端屢次進行請求,若是在Handler中的處理速度較慢,那麼後續的客戶端請求都會被積壓,致使響應變慢!因此引入了Reactor多線程模型!

Reactor多線程模型

Reactor多線程模型就是將Handler中的IO操做和非IO操做分開,操做IO的線程稱爲IO線程,非IO操做的線程稱爲工做線程!這樣的話,客戶端的請求會直接被丟到線程池中,客戶端發送請求就不會堵塞!

可是當用戶進一步增長的時候,Reactor會出現瓶頸!由於Reactor既要處理IO操做請求,又要響應鏈接請求!爲了分擔Reactor的負擔,因此引入了主從Reactor模型!

主從Reactor模型

主Reactor用於響應鏈接請求,從Reactor用於處理IO操做請求!

Netty

Netty是一個高性能NIO框架,其是對Reactor模型的一個實現!

  • Netty客戶端代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true);
b.handler( new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast( new TimeClientHandler());
}
});
 
ChannelFuture f = b.connect(host, port).sync();
 
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
  • Netty Client Handler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg;
try {
long currentTimeMillis =
(m.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println( new Date(currentTimeMillis));
ctx.close();
} finally {
m.release();
}
}
 
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
  • Netty服務端代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler( new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast( new TimeServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
  • Netty Server Handler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
 
@Override
public void channelActive(final ChannelHandlerContext ctx) {
final ByteBuf time = ctx.alloc().buffer(4);
time.writeInt(( int)
(System.currentTimeMillis() / 1000L + 2208988800L));
 
final ChannelFuture f = ctx.writeAndFlush(time);
f.addListener( new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
assert f == future;
ctx.close();
}
});
}
 
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

咱們從Netty服務器代碼來看,與Reactor模型進行對應!

  • EventLoopGroup就至關因而Reactor,bossGroup對應主Reactor,workerGroup對應從Reactor
  • TimeServerHandler就是Handler
  • child開頭的方法配置的是客戶端channel,非child開頭的方法配置的是服務端channel

具體Netty內容,請訪問Netty官網

Netty的問題

Netty開發中一個很明顯的問題就是回調,一是打破了線性編碼習慣,
二就是Callback Hell!

看下面這個例子:

1
2
3
a.doing1(); //1
a.doing2(); //2
a.doing3(); //3

1,2,3處代碼若是是同步的,那麼將按順序執行!可是若是不是同步的呢?我仍是但願2在1以後執行,3在2以後執行!怎麼辦呢?想一想AJAX!咱們須要寫相似以下這樣的代碼!

1
2
3
4
5
6
7
8
9
a.doing1( new Callback(){
public void callback(){
a.doing2( new Callback(){
public void callback(){
a.doing3();
}
})
}
});

那有沒有辦法解決這個問題呢?其實不難,實現一個相似Future的功能!當Client獲取結果時,進行阻塞,當獲得結果後再繼續往下走!實現方案,一個就是使用鎖了,還有一個就是使用RingBuffer。經測試,使用RingBuffer比使用鎖TPS有2000左右的提升!

參考資料

相關文章
相關標籤/搜索