java IO,偽非同步IO以及NIO網路程式設計 簡單實現原始碼以及區別
JAVA網路程式設計有三種方式,IO也就是BIO,BIO的偽非同步方式,和NIO,原理都是通過socket(套接字進行通訊)
套接字:就是ip+port ip就是計算機的地址 在java中預設是本地ip 127.0.0.1,port是埠號,每一個應用程式都有自己的埠號。每一臺電腦的ip都不一樣,每一臺電腦上不能同時存在兩個埠相同的程式,這樣就可以確保網路程式設計通訊的準確性,在其他程式語言中也是一樣。
首先我們先通過看幾個deme 來實現網路程式設計,再來比較幾種方式的不同。
IO:
server服務端,首先建立一個serverSocket 其實也就是socket,java中在服務端喜歡用serversocket來表示服務端的socket,ip是預設的埠號是9876,io方式中採用阻塞的方式,當客戶端有連線進來後才會結束阻塞返回一個socket也就是客戶端的socket,並啟動一個執行緒來監聽這個socket,接著會繼續阻塞等待下一個客戶端socket的到來
public class IOServer { //埠 private static final int port = 9876; public static void main(String[] args) { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(port); //阻塞 Socket socket = serverSocket.accept(); //建立一個新的執行緒來監聽 new Thread(new IOServerHandler(socket) ).start();; } catch (IOException e) { e.printStackTrace(); }finally{ try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
在來看一下IOServerHandler的程式碼,因為這個類是通過一個執行緒來監聽的 所以一定要實現runnable介面,io是讀寫分離的,inputStream是讀的流,outputstream是寫的流,讀到的和寫出的都是通過byte陣列的來讀寫的,陣列的長度決定一次讀的大小,當讀到的返回值是-1時,表示沒有資料可讀,切記inputStream的read方法是阻塞的當讀完資料的時候如果沒有新的資料可讀,程式不會往下走會阻塞到這裡 直到讀到新的資料,使用完之後記得關閉所有的流
public class IOServerHandler implements Runnable{ private Socket socket; public IOServerHandler(Socket socket){ this.socket = socket; } public void run() { InputStream in = null; OutputStream out = null; try { in = socket.getInputStream(); out = socket.getOutputStream(); //讀取客戶端的 訊息 byte[] temp = new byte[1024]; String pri = ""; int length = 0; while(true){ length = in.read(temp); if(length == -1){ break; } pri = new String(temp,0,length); System.out.println(pri); String wri = "伺服器收到資訊。。"; out.write(wri.getBytes()); } } catch (IOException e) { e.printStackTrace(); }finally{ try { in.close(); } catch (IOException e) { e.printStackTrace(); } try { out.flush(); } catch (IOException e1) { e1.printStackTrace(); } try { out.close(); } catch (IOException e) { e.printStackTrace(); } try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
再來看客戶端程式碼:客戶端想要和服務端進行通訊 一定要知道服務端的ip和埠號22,並建立連線,當new了一個socket之後,這個socket就會進入服務端的accept處,將這個socket返回到服務端,建立連線。流的方式和服務端得使用是一樣的 讀寫分離
public class Client {
private static final int port = 9876;
private static final String host = "127.0.0.1";
public static void main(String[] args) {
Socket socket = null;
InputStream in = null;
OutputStream out = null;
try {
socket = new Socket(host,port);
in = socket.getInputStream();
out = socket.getOutputStream();
out.write(new String("連線到伺服器。。。").getBytes());
byte[] temp = new byte[1024];
int length = 0;
String pri = "";
while(true){
length = in.read(temp);
if(length == -1){
break;
}
pri = new String(temp,0,length);
System.out.println(pri);
}
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
看完服務端和客戶端的 程式碼之後,大家想必明白當有一個客戶端進行連線的時候會建立一個新的執行緒,除非舊的連線斷開執行緒才會結束,這樣來說如果你的電腦最多隻能承受1000個執行緒,那麼你的伺服器最多隻能承載1000個使用者。
偽非同步的io網路程式設計是通過執行緒池的方式來管理執行緒,並可設定佇列等待的執行緒數,相對的可以增加伺服器的承載力,我們來看一下服務端程式碼:
和上面一樣是建立一個serversocket 也是阻塞之後返回一個socket連線,不同的是這裡並沒有直接new一個執行緒,而是通過執行緒池來管理連線
public class PoolIOServer {
private static final int port = 9876;
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
Socket socket = null;
ServerHandlerExecutePool executePool = new ServerHandlerExecutePool(50,100);
while(true){
socket = serverSocket.accept();
executePool.execute(new IOServerHandler(socket));
}
} catch (IOException e) {
e.printStackTrace();
} finally{
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
看一下執行緒池的程式碼:使用工廠模式來建立執行緒,方便對執行緒的管理
Runtime.getRuntime().availableProcessors() 是coreThreadSize表示的是核心的執行緒數,也就是我們cup的最大執行緒數,比如說電腦配置的4核8執行緒,maxPoolSize是我們設定得到最大執行緒數,120L是表示120秒的等待時間,當一個執行緒120秒沒有活躍會自動斷開給其他客戶端騰出位置,secnds是秒的意思就是120的單位,queuesieze佇列的大小,排隊的大小
public class ServerHandlerExecutePool {
private ExecutorService executor;
public ServerHandlerExecutePool(int maxPoolSize,int queueSize){
//Runtime.getRuntime().availableProcessors()本地方法 cpu最大的執行緒數
//maxPoolSize 最大執行緒數
//120L保持活躍的時間
//TimeUnit.SECONDS 單位
//queueSize 佇列的容量
this.executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L,
TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize),new ExecuteThreadFactory("executePool"));
}
public void execute(Runnable task){
executor.execute(task);
}
}
可以顯而易見相比較與第一種方式用執行緒池的方式管理 可以略微的提高效能
在看一下nio 的方式;首先我們 要了解nio的三個基礎概念:
buffer(緩衝區)中定義了大量的put get flip 方法,position mark等引數,大大方便了對讀寫內容的操作
channel(通道)不存在inputstream和outputStream,直接通過channel就可以實現兩端的通訊
selector是選擇器,每個channel會都會註冊的奧selector中得到一個key會不斷輪詢找到可以進行通訊的channel進行呼叫
通過channel提升了傳輸效率,通過selector可以大大利用起閒置的執行緒,能承載1000執行緒的電腦不在是支援1000個玩家,而是能支援1000個執行緒去併發。看一下伺服器程式碼:
public class NioServer implements Runnable{
//buffer:緩衝區 本質是一個數組
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
//多路複用器
private Selector selector;
public NioServer(int port){
try{
//1.開啟多路複用器
this.selector = Selector.open();
//2.開啟伺服器端的通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3.設定通道的阻塞模式
ssc.configureBlocking(false);
//4.繫結地址
ssc.bind(new InetSocketAddress(port));
//5.把伺服器通道註冊到多路複用器 監聽的狀態
ssc.register(this.selector,SelectionKey.OP_ACCEPT);
}catch(IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new NioServer(9876)).start();;
}
public void run() {
while(true){
try {
//1.必須讓多路複用器 開始監聽
this.selector.select();
//2.返回多路複用器所有註冊的key selectedKeys()返回值是set
Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();
//3.遍歷獲取的key
while(it.hasNext()){
//4.接收key的值
SelectionKey key = it.next();
//5.從容器中移除已經被選中的key 緩解壓力刪掉沒有意義的key
it.remove();
//6.驗證操作 判斷key是否有效 true是有效
if(key.isValid()){
//7.如果 為阻塞狀態
if(key.isAcceptable()){
this.accept(key);
}
//8.如果是可讀狀態
if(key.isReadable()){
this.read(key);
}
//9.如果是可寫狀態
if(key.isWritable()){
this.write(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void write(SelectionKey key) {
}
private void read(SelectionKey key) {
try{
//1.對緩衝區進行清空
this.readBuffer.clear();
//2.獲取之前註冊的socketchannel通道物件
SocketChannel sc = (SocketChannel)key.channel();
//3.從通道里獲取資料放入緩衝區
int index = sc.read(this.readBuffer);
if(index == -1){
key.channel().close();
key.cancel();
return;
}
}catch(IOException e){
e.printStackTrace();
}
//讀取readbuffer資料 然後列印到控制檯
//4.由於sc通道里的資料到readbuffer容器中 ,所以readbuffer裡面的position一定發生了變化 要進行復位
this.readBuffer.flip();
byte[] bytes = new byte[this.readBuffer.remaining()];
this.readBuffer.get(bytes);
String body = new String(bytes).trim();
System.out.println("伺服器讀取資料:"+body);
}
/**
* 監聽阻塞狀態方法執行
* @param key
*/
private void accept(SelectionKey key) {
try {
//1.由於目前是server端 ,那麼一定是server端啟動並且處於阻塞狀態 所喲煀阻塞狀態的key 一定是:serversocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
//2.通過呼叫accept方法 返回一個具體的客戶端連線控制代碼
SocketChannel sc = serverSocketChannel.accept();
//3.設定客戶端通道為非阻塞
sc.configureBlocking(false);
//4.設定當前獲取的客戶端連線狀態為可讀狀態位
sc.register(this.selector,SelectionKey.OP_READ);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
通過上面的內容我們做出如下總結:
1.io是面向流 nio是面向緩衝
2.io是阻塞 nio是非阻塞
3.io 一個執行緒管理一個連線 nio一個執行緒管理多個連線(一個執行緒管理一個selector,一個selector管理所有通道),使nio可容納的客戶端更多
4.io是讀寫分離的 nio讀寫可以使用一個buffer 一個channel使nio傳輸速度更快
5.nio唯一的劣勢,難度相比於io要大很多,但是過netty的架構來簡化程式碼