在此之前先談論一下網絡io.當一個客戶端和服務端之間相互通信,交互我們稱之爲網絡io(網絡通訊).網絡通訊基本都是通過socket來通訊的。客戶端和服務端這樣建立連接:第一步客戶端發起建立連接的請求,第二部服務端收到請求建立連接的請求,並同意和該客戶端建立連接,並響應給客戶端,第三步客戶端收到服務端響應的建立連接的消息,並確認和服務端建立連接,通過這樣三部客戶端和服務端就真正的建立了連接,服務端和客戶端就可以開始通訊,交互了.通過這樣三次的握手交互服務端和客戶端就成功的建立了連接..
如圖所示:
辣麼在我們傳統的bio裏面服務端是怎麼處理客戶端的請求的呢,首先我們的服務端需要實例化一個socket給我們的該實例socket 綁定ip(本機)和端口,然後進行監聽,然後就會一直堵塞等待和客戶端stoket建立連接,此時客戶端也需要實例化一個socket來和我們服務端建立請求,具體操作和服務端一樣,綁定ip和端口(服務端監控的ip和port),然後通過三次握手確認和服務端建立連接通訊.成功建立連接之後服務端需要新建一個線程去處理該客戶端的通訊請求(io操作),然後主線程會一直堵塞等到下一個客戶端socket發起連接請求,在處理下一個,這就是最原始的堵塞式的bio,這種方式的最明顯的缺點就是,服務端與每個客戶端socket建立連接都需要實時的去創建一個線程去處理該客戶端操作,加入同時又1000個客戶端socket同時求情建立連接,那我們的服務端就需要同時創建1000個線程去處理,完全沒有一點點的緩衝也不能拒絕,顯然我們的服務器會吃不消,如果客戶端增加socket增加到2000個達到了我們的系統所承受的上線,那我們的服務端就會直接蹦掉.但是jdk1.5以後出現了線程池和隊列,能稍稍減輕一點點服務器的負擔,吧客戶端請求直接扔給我們的線程池去處理,再對我們的線程池進行配置最大線程處理數量和隊列處理請求排隊,等方式去解決服務器的併發過大的情況。
如圖所示:
java代碼實現
首先我們需要一個處理請求的類,就是通過輸入輸出流去讀取和反饋客戶端的這樣的一個類,這個類實現Runnable接口
package liuzw.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ServerHandler implements Runnable{
private Socket socket ;
public ServerHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while(true){
body = in.readLine();
if(body == null) break;
System.out.println("Server :" + body);
out.println("服務器端回送響的應數據.");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
然後就是用於接受客戶端請求的入口
package liuzw.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
final static int PROT = 8765;
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(PROT);
System.out.println(" server start .. ");
//進行阻塞
Socket socket = server.accept();
//新建一個線程執行客戶端的任務
new Thread(new ServerHandler(socket)).start();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(server != null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
server = null;
}
}
}
接下來就是我那的客戶端了
package liuzw.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
final static String ADDRESS = "127.0.0.1";
final static int PORT = 8765;
public static void main(String[] args) {
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket(ADDRESS, PORT);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
//向服務器端發送數據
out.println("接收到客戶端的請求數據...");
out.println("接收到客戶端的請求數據1111...");
String response = in.readLine();
System.out.println("Client: " + response);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
上面說了bio是一種堵塞式的同步的網絡io,接下來的講到了的nio是一種非堵塞式的網絡io但是最開始的時候也是同步的.我們前面介紹了bio是客戶端和服務端各自創建一個socket實例建立連接相互通信,並且通信是通過單向io客戶端發送數據的時候客戶端就必須同步去接收收據,同樣的服務端向客戶端相應數據的時候也必須同保持連接,如果說客戶端和服務端的網速非常慢,這樣就會導致客戶端和服務端的連接長時間不能關閉,從而浪費了很多資源。因爲是直連和單向io所以每請次求和響應客戶端和服務端都需要創建一個輸出流和輸入流,收網速的影響流長時間不能關閉那我們nio的改進方式是建立一個緩衝區buffer,這個是nio裏面特有的,有了這個緩衝區我們客戶端和服務端的輸出流和輸入流就不用直連了,之前是輸出流和輸入流都是單向的流(單向io),但是nio裏面的buffer可以當做雙向流的既可以寫數據有可以讀數據.同時nio和引入了管道的概念channel,和選擇器也叫多路複用器Selector.相比傳統的bio的建立連接的方式,nio的練級方式是在原有socket的基礎上進行了封裝和加強,通過管道註冊的方式去建立通訊,首先我們的服務端的socketChanenl(我們稱之爲通訊管道)註冊到我們的多路複用器上,然後客戶端的通訊管道也註冊到我們的多路複用器上,區別是服務端的管道狀態是堵塞狀態,而客戶端的管道是可讀狀態,在這裏補充一下管道的狀態有四種,分別是連接狀態(Connect),堵塞狀態(Accecp),可讀狀態(Read),可寫狀態(Write),連接狀態就是管道剛剛連接,堵塞就是一直堵塞(多半用於服務端管道),可讀狀態就是該管道可以讀取數據,可寫狀態是該管道可以寫入數據.那剛剛說到服務端管道一直堵塞在這裏,然後服務端會起一個線程去輪詢註冊器也就是多路複用器上已經註冊並且處於連接狀態的客戶端socketChannel,並和他簡歷管道通訊(注意這裏不是socket直連)而是通過管道和緩衝區做通訊交互.在根據通道的狀態變化去執行相應的操作,順帶說明一下管道註冊其實並不是吧管道本身註冊在多路複用器上,而是通過selectedKey去註冊的,可以理解爲selectedKeys是唯一識別指定管道的標識列,同樣Selector咋輪詢獲取的也不是管道本身,而是獲取的一組管道的key,然後建立通訊的時候通過key獲取該管道,再在次深挖一下每個socketChinnel底層必然對應一個socket實例,獲取該管道本身以後然後就開始根據管道的狀態執行對應的操作,這樣就達到了通訊。講完了nio這裏就簡單的對應一下之前的bio有哪些優勢和好處,最明顯的就是傳統的bio, 是一個服務端socket和一個客戶端socket建立直連,並且服務端需要爲每個客戶端新起一個線程去處理客戶端的通訊交互,這樣必然會無故的開銷服務端的很多資源.而我們的nio只需要一個選擇器和一個輪詢線程就能接入成千上萬甚至更多的客戶端連接,這點是nio相比bio最大的進步和改變.其次就是建立連接的方式,傳統的bio是通過客戶端服務端三次握手的方式建立tcp連接,而nio是客戶端直接把通道註冊到服務端的多路複用器上,然後服務端去輪詢,這就減少了三次握手請求響應的開銷。再次之就是緩衝區代碼直連流,傳統的bio請求和響應數據讀是通過一端創建輸出流直接向另一端輸出,而另一點穿件輸入流寫入數據,這樣就很依賴網絡,如果網絡不好就會導致流長時間不能關閉,從而導致資源無故浪費,增加開銷.而nio引入了緩衝區都數據寫數據都是直接向緩衝區讀寫,這樣就不依賴網絡,一端吧數據寫完到緩衝區就可以關閉寫入流,這時候只需要通知另一端去讀。另一端開啓讀取流快速的讀取緩衝區的數據,然後就可以快速的關閉.如果網絡不好情況向就不會開銷另一端的資源。
如圖所示:
nio代碼實現
首先我們的nio主要就通過多路複用器來對客戶的管道請求進行註冊然後服務端進行輪詢
package liuzw.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class Server implements Runnable{
//1 多路複用器(管理所有的通道)
private Selector seletor;
//2 建立緩衝區
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
//3
private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
public Server(int port){
try {
//1 打開路複用器
this.seletor = Selector.open();
//2 打開服務器通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3 設置服務器通道爲非阻塞模式
ssc.configureBlocking(false);
//4 綁定地址
ssc.bind(new InetSocketAddress(port));
//5 把服務器通道註冊到多路複用器上,並且監聽阻塞事件
ssc.register(this.seletor, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port :" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
try {
//1 必須要讓多路複用器開始監聽
this.seletor.select();
//2 返回多路複用器已經選擇的結果集
Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
//3 進行遍歷
while(keys.hasNext()){
//4 獲取一個選擇的元素
SelectionKey key = keys.next();
//5 直接從容器中移除就可以了
keys.remove();
//6 如果是有效的
if(key.isValid()){
//7 如果爲阻塞狀態
if(key.isAcceptable()){
this.accept(key);
}
//8 如果爲可讀狀態
if(key.isReadable()){
this.read(key);
}
//9 寫數據
if(key.isWritable()){
//this.write(key); //ssc
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void write(SelectionKey key){
//ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//ssc.register(this.seletor, SelectionKey.OP_WRITE);
}
private void read(SelectionKey key) {
try {
//1 清空緩衝區舊的數據
this.readBuf.clear();
//2 獲取之前註冊的socket通道對象
SocketChannel sc = (SocketChannel) key.channel();
//3 讀取數據
int count = sc.read(this.readBuf);
//4 如果沒有數據
if(count == -1){
key.channel().close();
key.cancel();
return;
}
//5 有數據則進行讀取 讀取之前需要進行復位方法(把position 和limit進行復位)
this.readBuf.flip();
//6 根據緩衝區的數據長度創建相應大小的byte數組,接收緩衝區的數據
byte[] bytes = new byte[this.readBuf.remaining()];
//7 接收緩衝區數據
this.readBuf.get(bytes);
//8 打印結果
String body = new String(bytes).trim();
System.out.println("Server : " + body);
// 9..可以寫回給客戶端數據
} catch (IOException e) {
e.printStackTrace();
}
}
private void accept(SelectionKey key) {
try {
//1 獲取服務通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//2 執行阻塞方法
SocketChannel sc = ssc.accept();
//3 設置阻塞模式
sc.configureBlocking(false);
//4 註冊到多路複用器上,並設置讀取標識
sc.register(this.seletor, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new Server(8765)).start();;
}
}
客戶端代碼:
package bhz.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
//需要一個Selector
public static void main(String[] args) {
//創建連接的地址
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8765);
//聲明連接通道
SocketChannel sc = null;
//建立緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
//打開通道
sc = SocketChannel.open();
//進行連接
sc.connect(address);
while(true){
//定義一個字節數組,然後使用系統錄入功能:
byte[] bytes = new byte[1024];
System.in.read(bytes);
//把數據放到緩衝區中
buf.put(bytes);
//對緩衝區進行復位
buf.flip();
//寫出數據
sc.write(buf);
//清空緩衝區數據
buf.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(sc != null){
try {
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}