前言
隨着互聯網的不斷髮展,用戶羣體越發龐大。
從互聯網初入中國,國內的上網用戶不過數萬,而上網也只能簡單的進行郵件,瀏覽新聞。到現在幾乎每個人都可以通過互聯網進行社交,娛樂,學習。中間不但有社會經濟的發展,java技術也隨之不斷髮展,究竟是科學技術的發展促使了社會的發展,還是社會經濟的發展促使了科學技術?這個問題我想大家應該都知道一句話:科學技術是第一生產力。
隨着用戶全體的龐大,對於網絡通信的要求越發高,BIO越發不適應需求,於是NIO應運而生。
NIO(New IO)又被業內稱之爲:Non Block IO,即非阻塞IO
一、傳統的BIO編程
1、BIO通信模型圖
BIO通信的服務器,對於每一個客戶端的連接,都會使用一個獨立的Acceptor線程,來對客戶端進行連接。
即,他收到客戶端請求後,會爲每一個客戶端創建一個新的線程進行鏈路處理,處理完成之後,通過輸出流返回應答給客戶端,最後線程銷燬。這就是典型的一請求一應答的通信模型。
線程是JVM非常寶貴的資源,當線程數過大後,隨着併發訪問數的繼續增大,系統將會產生堆棧移除,創建線程失敗等事故。最終導致服務器宕機或僵死,無法對外提供服務。
2、BIO的Server源碼分析
package com.xyp.iodemo.nio.server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Logger;
/**
* @author xuyuanpeng
* @version 1.0
* @date 2019-03-28 18:13
*/
public class TimeServer {
public static Logger XLogUtils = Logger.getLogger("TimeServer");
public static void main(String [] args) throws IOException {
Integer port=20022;
if(args!=null&&args.length>0){
try {
port=Integer.valueOf(args[0]);
}catch (Exception e){
}
}
ServerSocket serverSocket=null;
try {
serverSocket=new ServerSocket(port);
XLogUtils.info("The TimeServer is start in port:"+port);
Socket socket=null;
while (true){
socket=serverSocket.accept();
new Thread(new TimeServerHandler(socket)).start();
}
}finally {
if(serverSocket!=null){
XLogUtils.info("The Time is Close");
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
serverSocket=null;
}
XLogUtils.info("END>>>>");
}
}
}
TimeServer是一個很簡單的類,它負責創建ServerSocket,然後對客戶端的請求進行接受,這裏值得注意的一點就是,這裏的
serverSocket.accept()
是阻塞的,即sever等待客戶端請求接入,如果接入就創建線程處理請求,創建完畢後,繼續等待下一個客戶端的接入。
然後我們再來一起看一下具體針對線程的處理。
package com.xyp.iodemo.nio.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.logging.Logger;
/**
* @author xuyuanpeng
* @version 1.0
* @date 2019-03-28 19:13
*/
public class TimeServerHandler implements Runnable {
private Socket socket;
public static Logger logger = Logger.getLogger("TimeServerHandler");
public TimeServerHandler(Socket socket){
this.socket=socket;
logger.info("TimeServerHandler 構造函數");
}
@Override
public void run() {
BufferedReader in =null;
PrintWriter out=null;
try {
in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
out=new PrintWriter(socket.getOutputStream(),true);
String currentTime=null;
String body=null;
logger.info("TimeServerHandler run");
while (true){
logger.info("TimeServerHandler wait input");
body=in.readLine();
logger.info("TimeServerHandler wait finish:"+String.valueOf(body));
if(body== null){
break;
}
logger.info("The time server receive order : "+body);
currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?new java.util.Date(
System.currentTimeMillis()
).toString():"BAD ORDER";
out.println(currentTime);
}
}catch (Exception e){
if(in!=null){
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if(out!=null){
out.close();
out=null;
}
if(socket!=null){
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}finally {
socket=null;
}
}
}
}
}
3、Client的源碼分析
package com.xyp.iodemo.bio.client;
import java.io.*;
import java.net.Socket;
import java.util.logging.Logger;
/**
* @author xuyuanpeng
* @version 1.0
* @date 2019-04-01 18:21
*/
public class TimeClient {
public static Logger logger = Logger.getLogger("TimeClient");
public static void main(String [] args) throws IOException {
Integer port=20022;
if(args!=null&&args.length>0){
try {
port=Integer.valueOf(args[0]);
}catch (Exception e){
}
}
Socket socket=null;
BufferedReader in =null;
PrintWriter out = null;
try {
socket = new Socket("127.0.0.1", port);
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));//用戶獲取用戶輸入
OutputStream os = socket.getOutputStream();//用於向服務器輸出
System.out.println("請輸入要發送的文字:");
String input;
//等待cons
while ((input= reader.readLine()) != null) {
input = input+"\n";//手動加上回車
os.write(input.getBytes("utf-8"));
logger.info("向服務的寫入:"+input.toString());
}
}catch (Exception e){
}finally {
if(out==null){
out.close();
out=null;
}
if(in==null){
try {
in.close();
}catch (IOException e){
e.printStackTrace();
}
in=null;
}
if(socket!=null){
try {
socket.close();
}catch (IOException e2){
e2.printStackTrace();
}
socket=null;
}
}
}
}
等待控制檯的輸入,按下回車鍵,就會向服務器寫入字節。
4、運行結果
(1)啓動Server的console
四月 08, 2019 4:25:41 下午 com.xyp.iodemo.nio.server.TimeServer main
信息: The TimeServer is start in port:20022
(2)啓動Client的console,在其中輸入了 111後,按下回車
請輸入要發送的文字:
111
四月 08, 2019 4:27:40 下午 com.xyp.iodemo.bio.client.TimeClient main
信息: 向服務的寫入:111
(3)服務器響應輸出。輸出是分步的
當client啓動後,輸出
四月 08, 2019 4:27:30 下午 com.xyp.iodemo.nio.server.TimeServerHandler
信息: TimeServerHandler 構造函數
四月 08, 2019 4:27:30 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler run
四月 08, 2019 4:27:30 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler wait input
當客戶端按下回車鍵後
四月 08, 2019 4:27:40 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler wait finish:111
四月 08, 2019 4:27:40 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: The time server receive order : 111
四月 08, 2019 4:27:40 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler wait input
二、NIO基礎概念
1、緩衝區Buffer
Buffer是一個對象,他包含了一些要寫入的或者要讀出的數據。
在NIO中加入Buffer對象,體現了新庫與原始IO的一個重大區別,即,在面流的IO中,可以直接將數據寫入或者直接將數據讀到steam對象中。
Java NIO中的Buffer用於和NIO通道進行交互。如你所知,數據是從通道讀入緩衝區,從緩衝區寫入到通道中的。
緩衝區本質上是一塊可以寫入數據,然後可以從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。
2、通道Channel
Channel是一個通道,可以通過它進行數據的讀取和寫入。如同自來水管道,網絡數據在channel中可以進行雙向的流通,這點不同於steam,steam是單向的,即steam要麼是InputSteam,要麼是OutputSteam。
而通道可以用於讀、寫、或同時進行讀寫。
3、多路複用器 Selector
多路複用器,提供平選擇已就緒任務的能力。
簡單來說,selector會不斷輪詢註冊在其上的Channel,如果某個Channel上有新的Tcp接入,或者有發生讀寫事件,這個Channel就會處於就緒狀態,可以被Selector輪詢出來,然後通過selectedKey獲取就緒的Channel集合,以便進行後續的IO操作。
selector.selectedKeys()
一個多路複用器可以同時輪詢多個Channel。
4、NIO的Server的時序圖
5、NIO的Server源碼分析
(1)TimeServer
此類的作用主要是聲明端口,以及啓動線程
package com.xyp.iodemo.nio.server;
/**
* @author xuyuanpeng
* @version 1.0
* @date 2019-04-02 11:05
*/
public class TimeServer {
public static void main(String [] args){
Integer port=8085;
if(args!=null&&args.length>0){
try {
port=Integer.valueOf(args[0]);
}catch (Exception e){
}
}
MultiplexerTimeServer timeServer=new MultiplexerTimeServer(port);
new Thread(timeServer,"NIO-MultiplexerTimeServer-001").start();
}
}
(2)MultiplexerTimeServer
package com.xyp.iodemo.nio.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;
/**
* @author xuyuanpeng
* @version 1.0
* @date 2019-04-02 11:11
*/
public class MultiplexerTimeServer implements Runnable{
public static Logger logger = Logger.getLogger("MultiplexerTimeServer");
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private volatile Boolean stop;
public MultiplexerTimeServer(Integer port){
stop=false;
try {
//創建多路複用器
selector=Selector.open();
//打開ServerSocketChannel,用於監聽客戶端連接,他是所有的客戶端連接的父管道
serverSocketChannel=ServerSocketChannel.open();
//設置爲非阻塞
serverSocketChannel.configureBlocking(false);
//綁定監聽端口
serverSocketChannel.socket().bind(new InetSocketAddress(port),1024);
//將serverSocketChannel註冊到多路複用器,監聽ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
logger.info("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 {
//設置休眠時間爲1s,無論是否有讀寫事件發生,selector每1s都被喚醒一次
selector.select(1000);
// logger.info("selector被喚醒>>>>>>>");
//多路複用器無線循環獲取準備就緒的Key
Set<SelectionKey> keys=selector.selectedKeys();
// logger.info("keys len>>>>>>>"+keys.size());
Iterator<SelectionKey> it=keys.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 e){
e.printStackTrace();
}
}
if(selector!=null){
try {
selector.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
if(key.isAcceptable()){
//多路複用器監聽到有新的客戶端接入,處理新的接入請求,完成TCP的三次握手
//建立物理連接
ServerSocketChannel ssc= (ServerSocketChannel) key.channel();
logger.info("準備監聽客戶端連接");
SelectableChannel sc=ssc.accept();
logger.info("設置監聽完畢");
//設置客戶端鏈路,爲非阻塞模式
sc.configureBlocking(false);
sc.register(selector,SelectionKey.OP_READ);
}
if(key.isReadable()){
logger.info("key.isReadable");
SocketChannel sc= (SocketChannel) key.channel();
ByteBuffer byteBuffe=ByteBuffer.allocate(1024);
//異步讀取客戶端請求消息到緩存區
logger.info("異步讀取客戶端請求消息到緩存區>>"+byteBuffe.toString());
int readBytes=sc.read(byteBuffe);
if(readBytes>0){
//將緩衝區的當前limit設置爲postion,position設置爲0,用於後續對緩衝區的讀取操作
byteBuffe.flip();
byte [] bytes=new byte[byteBuffe.remaining()];
byteBuffe.get(bytes);
//對ByteBuffer進行編解碼,如果有半包消息指針reset,繼續讀取後續的報文
//將解碼成功的消息封裝成Task,投遞到業務線程池中,進行業務邏輯編排
String body=new String(bytes,"UTF-8");
logger.info("The Time server receive order : "+body);
String currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?
new java.util.Date(System.currentTimeMillis()).toString():"BAD ORDER";
//調用異步write接口,將消息異步發送到客戶端
doWrite(sc,currentTime);
}else if(readBytes<0){
key.channel();
sc.close();
}else {
}
}
}
}
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);
}
}
}
6、NIO的Client
1、NIO的Client的時序圖
2、Client源碼分析
(1)TimeClient
主要作用是聲明端口,啓動線程。
/**
* @author xuyuanpeng
* @version 1.0
* @date 2019-04-02 16:59
*/
public class TimeClient {
public static void main(String [] args){
Integer port=8085;
if(args!=null&&args.length>0){
try {
port=Integer.valueOf(args[0]);
}catch (Exception e){
}
}
TimeHandle timeServer=new TimeHandle("127.0.0.1",port);
new Thread(timeServer,"NIO-TimeHandle-001").start();
}
}
(2)TimeHandle
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;
/**
* @author xuyuanpeng
* @version 1.0
* @date 2019-04-02 17:01
*/
public class TimeHandle implements Runnable{
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile Boolean stop;
public static Logger logger = Logger.getLogger("TimeHandle");
public TimeHandle(String host,int port){
this.host=host==null?"127.0.0.1":host;
this.port=port;
this.stop=false;
try {
selector=Selector.open();
socketChannel=SocketChannel.open();
socketChannel.configureBlocking(false);
}catch (Exception e){
e.printStackTrace();
System.exit(1);
}
logger.info("TimeHandle構造完畢");
}
@Override
public void run() {
try {
logger.info("run >> 開始連接");
doConnect();
logger.info("run >> 連接完畢");
}catch (Exception e){
e.printStackTrace();
System.exit(1);
}
while (!stop){
try {
selector.select(1000);
// logger.info("selector被喚醒>>>>>>>");
Set<SelectionKey> selectionKeys=selector.selectedKeys();
// logger.info("keys len>>>>>>>"+selectionKeys.size());
Iterator<SelectionKey> it=selectionKeys.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);
}
}
if(selector!=null){
try {
selector.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
// logger.info("handleInput>>");
if(key.isValid()){
// logger.info("key.isValid>>");
SocketChannel sc= (SocketChannel) key.channel();
//測試此鍵的通道是否已完成其套接字連接操作。
if(key.isConnectable()){
logger.info("sc.isConnected>>");
if(sc.finishConnect()){
logger.info("sc.finishConnect>>");
sc.register(selector,SelectionKey.OP_READ);
doWrite(sc);
}else {
logger.info("Link fail exit...");
System.exit(1);
}
if(key.isReadable()){
ByteBuffer readBuffer=ByteBuffer.allocate(1024);
int readBytes=sc.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();
sc.close();
}else {
//ignore
}
}
}
}
}
private void doConnect() throws IOException {
logger.info("連接>>"+host+">>"+port);
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 channel) throws IOException {
logger.info("doWrite>>>>");
byte [] bytes="Hi Service O_O".getBytes();
ByteBuffer writeBuffer=ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
if(!writeBuffer.hasRemaining()){
logger.info("Send order 2 server succeed.");
}
}
}
3、運行結果
(1)啓動服務器
四月 08, 2019 5:09:25 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer
信息: The Time server is start in port: 8085
(2)啓動客戶端
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle
信息: TimeHandle構造完畢
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle run
信息: run >> 開始連接
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle doConnect
信息: 連接>>127.0.0.1>>8085
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle run
信息: run >> 連接完畢
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle handleInput
信息: sc.isConnected>>
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle handleInput
信息: sc.finishConnect>>
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle doWrite
信息: doWrite>>>>
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle doWrite
信息: Send order 2 server succeed.
(3)服務器響應
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: 準備監聽客戶端連接
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: 設置監聽完畢
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: key.isReadable
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: 異步讀取客戶端請求消息到緩存區>>java.nio.HeapByteBuffer[pos=0 lim=1024 cap=1024]
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: The Time server receive order : Hi Service O_O
以上就是NIO的運行流程。
參考文檔:
《Netty權威指南》