在這個充斥着雲的時代,咱們使用的軟件能夠說99%都是C/S架構的!html
C/S架構的軟件帶來的一個明顯的好處就是:只要有網絡,你能夠在任何地方幹同一件事。java
例如:你在家裏使用Office365編寫了文檔。到了公司,只要打開編輯地址就能夠看到在家裏編寫的文檔,進行展現或者繼續編輯。甚至在手機上進行閱讀與編輯。再也不須要U盤拷來拷去了。react
C/S架構能夠抽象爲以下模型:git
C/S架構之因此可以流行的一個主要緣由就是網速的提升以及費用的下降,特別是無線網絡速度的提升。試想在2G時代,你們最多就是看看文字網頁,小說什麼的。看圖片,那簡直就是奢侈!更別說看視頻了!github
網速的提升,使得愈來愈多的人使用網絡,例如:優酷,微信都是上億用戶量,更別說天貓雙11的瞬間訪問量了!這就對服務器有很高的要求!可以快速處理海量的用戶請求!那服務器如何能快速的處理用戶的請求呢?編程
高性能服務器至少要知足以下幾個需求:瀏覽器
而知足如上需求的一個基礎就是高性能的IO!安全
不管你是發郵件,瀏覽網頁,仍是看視頻~實際底層都是使用的TCP/IP,而TCP/IP的編程抽象就是Socket!服務器
我一直對Socket的中文翻譯很困惑,我的以爲是我所接觸的技術名詞翻譯裏最莫名其妙的,沒有之一!微信
Socket中文翻譯爲」套接字」!什麼鬼?在很長的時間裏我都沒法將其和網絡編程關聯上!後來專門找了一些資料,最後在知乎上找到了一個還算滿意的答案(具體連接,請見文末的參考資料連接)!
Socket的原意是插口,想表達的意思是插口與插槽的關係!」send socket」插到」receive socket」裏,創建了連接,而後就能夠通訊了!
套接字的翻譯,應該是參考了套接管(以下圖)!從這個層面上來看,是有那麼點意思!
套接字這個翻譯已是標準了,不糾結這個了!
咱們看一下Socket之間創建連接及通訊的過程!實際上就是對TCP/IP鏈接與通訊過程的抽象:
對於IO來講,咱們聽得比較多的是:
以及其組合:
那麼什麼是阻塞IO、非阻塞IO、同步IO、異步IO呢?
舉個不太恰當的例子 :好比你家網絡斷了,你打電話去中國電信報修!
本文只討論BIO和NIO,AIO使用度沒有前二者普及,暫不討論!
下面從代碼層面看看BIO與NIO的流程!
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來講,1.5之後,一個線程大體消耗1M內存!且隨着線程數量的增長,CPU切換線程上下文的消耗也隨之增長,在高過某個閥值後,繼續增長線程,性能不增反降!而一樣由於一個鏈接就新建一個線程,因此編碼模型很簡單!
就性能瓶頸這一點,就肯定了BIO並不適合進行高性能服務器的開發!像Tomcat這樣的Web服務器,從7開始就從BIO改爲了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);
|
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);
}
}
}
|
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);
|
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模型示例以下:
NIO的優缺點和BIO就徹底相反了!性能高,不用一個鏈接就建一個線程,能夠一個線程處理全部的鏈接!相應的,編碼就複雜不少,從上面的代碼就能夠明顯體會到了。還有一個問題,因爲是非阻塞的,應用沒法知道何時消息讀完了,就存在了半包問題!
簡單看一下下面的圖就能理解半包問題了!
咱們知道TCP/IP在發送消息的時候,可能會拆包(如上圖1)!這就致使接收端沒法知道何時收到的數據是一個完整的數據。例如:發送端分別發送了ABC,DEF,GHI三條信息,發送時被拆成了AB,CDRFG,H,I這四個包進行發送,接受端如何將其進行還原呢?在BIO模型中,當讀不到數據後會阻塞,而NIO中不會!因此須要自行進行處理!例如,以換行符做爲判斷依據,或者定長消息發生,或者自定義協議!
NIO雖然性能高,可是編碼複雜,且須要處理半包問題!爲了方便的進行NIO開發,就有了Reactor模型!
Reactor模型和AWT事件模型很像,就是將消息放到了一個隊列中,經過異步線程池對其進行消費!
對應上面的NIO代碼來看:
Reactor從線程池和Reactor的選擇上能夠細分爲以下幾種:
這個模型和上面的NIO流程很相似,只是將消息相關處理獨立到了Handler中去了!
雖然上面說到NIO一個線程就能夠支持全部的IO處理。可是瓶頸也是顯而易見的!咱們看一個客戶端的狀況,若是這個客戶端屢次進行請求,若是在Handler中的處理速度較慢,那麼後續的客戶端請求都會被積壓,致使響應變慢!因此引入了Reactor多線程模型!
Reactor多線程模型就是將Handler中的IO操做和非IO操做分開,操做IO的線程稱爲IO線程,非IO操做的線程稱爲工做線程!這樣的話,客戶端的請求會直接被丟到線程池中,客戶端發送請求就不會堵塞!
可是當用戶進一步增長的時候,Reactor會出現瓶頸!由於Reactor既要處理IO操做請求,又要響應鏈接請求!爲了分擔Reactor的負擔,因此引入了主從Reactor模型!
主Reactor用於響應鏈接請求,從Reactor用於處理IO操做請求!
Netty是一個高性能NIO框架,其是對Reactor模型的一個實現!
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>() {
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();
}
|
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 {
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();
}
}
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
|
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>() {
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();
}
|
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 {
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() {
public void operationComplete(ChannelFuture future) {
assert f == future;
ctx.close();
}
});
}
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
|
咱們從Netty服務器代碼來看,與Reactor模型進行對應!
具體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左右的提升!