1、相關概念
1、Socket
Socket又稱“套接字”。網絡上的兩個程序通過一個雙向的通訊連接實現數據的交換,這個雙向鏈路的一端稱爲一個Socket。Socket通常用來實現客戶方和服務方的連接。Socket是TCP/IP協議的一個十分流行的編程界面,一個Socket由一個IP地址和一個端口號唯一確定。但是,Socket所支持的協議種類也不光TCP/IP一種,因此兩者之間是沒有必然聯繫的。在Java環境下,Socket編程主要是指基於TCP/IP協議的網絡編程。
套接字的連接過程可以分爲四個步驟:服務器監聽、客戶端請求服務器、服務器端連接確認、客戶端連接確認並進行通信。
2、同步和異步
同步:在發出一個功能調用時,在沒有得到結果之前,該調用就不返回。
異步:異步與同步相對,當一個異步過程調用發出後,調用者在沒有得到結果之前,就可以繼續執行後續操作。當這個調用完成後,一般通過狀態、通知和回調來通知調用者。對於異步調用,調用的返回並不受調用者控制。
3、阻塞和非阻塞
阻塞:阻塞調用是指服務器端被調用者調用結果返回之前,當前線程會被掛起。調用線程只有在得到結果之後纔會返回。
非阻塞:指在不能立即得到結果之前,該調用不會阻塞當前線程。
2、多種io的簡介
1、BIO(同步阻塞IO)
使用ServerSocket綁定IP地址和監聽端口,客戶端發起連接,通過三次握手建立連接,用socket來進行通信,通過輸入輸出流的方式來進行同步阻塞的通信。每次客戶端發起連接請求,都會啓動一個線程。
2、socket通訊代碼實現
服務器端:
package com.xyq.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
final static int PORT = 8765;
public static void main(String[] args) {
/* 1. 建立用於監聽和接受客戶端連接請求的套接字 */
ServerSocket server = null;
try {
server = new ServerSocket(PORT);
System.out.println("server start ..");
/* 2.進行阻塞 等待客戶端連接請求,在沒有客戶端連接請求到來之前程序會一直阻塞在這個函數裏。*/
Socket socket = server.accept();
/*3.新建一個線程執行客戶端的任務 */
new Thread(new ServerHandler(socket)).start();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
if(server != null){
try {
server.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
server = null;
}
}
}
服務器端響應工具類:
package com.xyq.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 (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
socket = null;
}
}
}
客戶端:
package com.xyq.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
//1、定義端口和地址
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("接收到客戶端請求數據");
String response = in.readLine();
System.out.println("Client " + response);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
socket = null;
}
}
}
3、傳統的socket通訊優化(採用僞異步)
傳統的Socket通訊是每次有客戶端請求服務器端都會創建一個線程,當線程過多時,服務器端可能會宕機。解決這個問題,可以使用JDK提供的線程池(僞異步)。
代碼實現(客戶端和服務端響應類保持不變)
server端
package com.xyq.bio2;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
final static int PORT = 8765;
public static void main(String[] args) {
//建立用於監聽和接受客戶端連接請求的套接字
ServerSocket server = null;
try {
server = new ServerSocket(PORT);
System.out.println("server start ..");
Socket socket = null;
HandlerExecutorPool handlerExecutorPool = new HandlerExecutorPool(50, 1000);
while(true){
socket = server.accept();
handlerExecutorPool.excute(new ServerHandler(socket));
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
if(server != null){
try {
server.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
server = null;
}
}
}
自定義線程池
package bhz.bio2;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class HandlerExecutorPool {
private ExecutorService executor;
public HandlerExecutorPool(int maxPoolSize, int queueSize){
this.executor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
maxPoolSize,
120L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(queueSize));
}
public void execute(Runnable task){
this.executor.execute(task);
}
}
2、NIO(同步非阻塞IO)
用NIO方式處理IO使用多路複用器Selector來輪詢每個通道Channel,當通道中有事件時就通知處理,不會阻塞。但是使用相當複雜。
3、AIO(真正的異步非阻塞IO)
NIO2.0引入了新的異步通道的概念,不需要使用多路複用器(Selector)對註冊通道進行輪詢即可實現異步讀寫,從而簡化了NIO編程模型。
3、BIO和NIO的區別
1、BIO(同步阻塞IO)
BIO是面向流意味着每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前後移動流中的數據。如果需要前後移動從流中讀取的數據,需要先將它緩存到一個緩衝區。
BIO的各種流是阻塞的。這意味着,當一個線程調用read()或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再幹任何事情了
2、NIO(同步非阻塞IO)
NIO的緩衝導向方法。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏尚未處理的數據。
NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取。而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。
4、NIO詳解
1、NIO的構成
NIO主要有三大核心部分:Buffer(緩衝區), Channel(通道), Selector。
Buffer:
Buffer是一個對象,它包含一些需要寫入或者讀取的數據。在NIO類庫中加入Buffer對象,體現了新類庫與原IO的一個重要區別。在面向流的IO中,可以直接將數據寫入或讀取到Stream對象中。在NIO類庫中,所有的數據都是用緩衝區處理的(讀寫)。緩衝區實質上是一個數組,通常它是一個字節數組(ByteBuffer),也可以使用其他類型的數組。這個數組爲緩衝區提供了訪問數據的讀寫等操作屬性,如位置、容量、上限等概念。
注 Buffer類型:最常使用的是ByteBuffer,實際上每一種java基本類型都對應了一種緩存區(除了Boolean類型)。ByteBuffer,CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer
Buffer常用API(以IntBuffer爲例)
package com.xyq.nio;
import java.nio.IntBuffer;
public class BufferTest {
public static void main(String[] args) {
// 1 基本操作
/*//創建指定長度的緩衝區
IntBuffer buffer = IntBuffer.allocate(10);
//裝載數據
buffer.put(10);// position位置:0 - > 1
buffer.put(20);// position位置:1 - > 2
buffer.put(30);// position位置:2 - > 3
System.out.println("未調用flip方法前的buffer " + buffer);//position位置爲3
buffer.flip(); //把位置復位爲0,也就是position位置由3->0
System.out.println("調用flip方法之後的buffer " + buffer);
System.out.println("buffer的容量 " + buffer.capacity());//容量一旦初始化後不允許改變(warp方法包裹數組除外)
System.out.println("buffer的限制 " + buffer.limit());//由於只裝載了三個元素,所以可讀取或者操作的元素爲3 則limit=3
System.out.println("獲取下標爲1的元素 " + buffer.get(1)); //調用get(index)方法,不會改變position的值
System.out.println("調用get(index)之後的buffer " + buffer);
//修改下標爲index的元素的值
buffer.put(1, 50);
System.out.println("調用put(int index, int i)之後的buffer " + buffer);//調用put(int index, int i)不會改變position的值
System.out.println("獲取下標爲1的元素 " + buffer.get(1));
//get()方法
for(int i=0; i<buffer.limit(); i++){
System.out.println(buffer.get() + "\t");//調用get方法會使其緩衝區位置(position)向後遞增一位
}
System.out.println("調用get()方法之後的buffer" + buffer);*/
/*// 2 wrap方法使用
// wrap方法會包裹一個數組: 一般這種用法不會先初始化緩存對象的長度,因爲沒有意義,最後還會被wrap所包裹的數組覆蓋掉。
// 並且wrap方法修改緩衝區對象的時候,數組本身也會跟着發生變化。
int []arr = new int[]{1, 2, 3};
IntBuffer buffer = IntBuffer.wrap(arr);
System.out.println("調用wrap(int[] array)之後的buffer" + buffer);
IntBuffer buffer2 = IntBuffer.wrap(arr, 0, 2);//這樣使用表示容量爲數組arr的長度,但是可操作的元素只有實際進入緩存區的元素長度
System.out.println("調用了wrap(int[] array, int offset, int length)之後的buffer" + buffer2);*/
// 3 其他方法
IntBuffer buffer = IntBuffer.allocate(10);
int []arr = new int[]{1, 2, 3};
buffer.put(arr);
System.out.println("調用了put(int[] src)之後的buffer" + buffer);
//複製
// IntBuffer buffer2 = buffer.duplicate(); //buffer2的pos、lim、cap與buffer的一樣
// System.out.println("buffer2" + buffer2);
// //buffer的位置屬性
// buffer.position(2);//手動設置pos的值
// System.out.println("調用了position(int newPosition)方法之後的buffer " + buffer);
//
// System.out.println("buffer的可讀數據 " + buffer.remaining());
int[] arr2 = new int[buffer.remaining()];
buffer.get(arr2);//將緩衝區數據放入arr2數組中去
for(int i : arr2){
System.out.println(Integer.toString(i) + ",");
}
}
}
Channel:
通道,被建立的一個應用程序和操作系統交互事件、傳遞內容的渠道(注意是連接到操作系統)。一個通道會有一個專屬的文件狀態描述符。那麼既然是和操作系統進行內容的傳遞,那麼說明應用程序可以通過通道讀取數據,也可以通過通道向操作系統寫數據。
Channel就像自來水管道一樣,網絡數據通過Channel讀取和寫入,通道與流的不同之處在於通道是雙向的,而流只能在一個方向上移動(一個流必須是InputStream或者OutputStream的子類),而通道可以用於讀、寫或者二者同時進行,最關鍵的是可以和多路複用器集合起來,有多種的狀態位,方便多路複用器去識別。通道分爲兩大類:一類是用於網絡讀寫的SelectableChannel,另一類是用於文件操作的FileChannel,我們使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子類。
在NIO中channel的實現主要有:1、FileChannel (文件IO)2、DatagramChannel (UDP)3、SocketChannel (TCP)4、ServerSocketChannel (TCP)
Selector:
多路複用器提供選擇已經就緒的任務的能力。簡單說,就是Selector會不斷的輪詢註冊在其上的通道(Channel),如果某個通道發生了讀寫操作,這個通道就處於就緒狀態,會被Selector輪詢出來,然後通過SelectionKey可以取得就緒的Channel集合,從而進行後續的IO操作。一個多路複用器(Selector)可以負責成千上萬的通道(Channel),沒有上限。這也是JDK使用了epoll代替傳統的select實現,獲得連接句柄(客戶端)沒有限制。那也就意味着我們只要一個線程負責Selector的輪詢,就可以接入成千上萬個客戶端,這是JDK NIO庫的巨大進步。
Selector線程類似一個管理者(Master),管理了成千上萬個管道,然後輪詢哪個管道的數據已經準備好了,通知CPU執行IO的讀取或寫入操作。
Selector模式:當IO事件(管道)註冊到選擇器以後,Selector會分配給每個管道一個key值,相當於標籤。Selector選擇器是以輪詢的方式進行查找註冊的所有IO事件(管道),當IO事件(管道)準備就緒後,Selector就會識別,會通過key值來找到相應的管道,進行相關的數據處理操作(從管道中讀取或寫入數據,寫到緩衝區中)。每個管道都會對選擇器進行註冊不同的事件狀態,以便選擇器查找。
事件狀態 SelectionKey.OP_CONNECT(連接狀態) SelectionKey.OP_ACCEPT(阻塞狀態) SelectionKey.OP_READ(可讀狀態) SelectionKey.OP_WRITE(可寫狀態)
2、NIO的結構
3、NIO通信模型圖解:
4、NIO通信代碼實例
服務器端
package com.xyq.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 selector;
//2 建立緩衝區
private ByteBuffer buffer = ByteBuffer.allocate(1024);
public static void main(String[] args) {
new Thread(new Server(8765)).start();
}
public Server(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);
System.out.println("server start, port " + port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
try {
//1 必須要讓多路複用器開始監聽
this.selector.select();
//2 返回多路複用器已經選擇的結果集
Iterator<SelectionKey> keys = this.selector.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);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 阻塞狀態是調用的方法
* @param key
*/
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.selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 可讀狀態時調用的方法
* @param key
*/
private void read(SelectionKey key){
try {
//1 清空緩衝區舊的數據
this.buffer.clear();
//2 獲取之前註冊的socket通道對象
SocketChannel sc = (SocketChannel) key.channel();
//3 讀取數據
int count = sc.read(this.buffer);
//4 如果沒有數據
if(count == -1){
key.channel().close();
key.channel();
return;
}
//5 有數據則進行讀取 讀取之前需要進行復位方法(把position 和limit進行復位)
this.buffer.flip();
//6 根據緩衝區的數據長度創建相應大小的byte數組,接收緩衝區的數據
byte[] bs = new byte[this.buffer.remaining()];
//7 接收緩衝區數據
this.buffer.get(bs);
//8 打印結果
String body = new String(bs).trim();
System.out.println("server : " + body);
} catch (IOException e) {
e.printStackTrace();
}
}
}
客戶端
package com.xyq.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
public static void main(String[] args) {
//創建連接的地址
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8765);
//聲明連接通道
SocketChannel sc = null;
//建立緩衝區
ByteBuffer buffer = ByteBuffer.allocate(1204);
try {
//打開通道
sc = SocketChannel.open();
//進行連接
sc.connect(address);
while(true){
//定義一個字節數組,然後使用系統錄入功能:
byte[] bs = new byte[1024];
//提示用戶輸入
System.out.println("請輸入你要輸入的信息");
//系統錄入
System.in.read(bs);
//把數據放到緩衝區中
buffer.put(bs);
//對緩衝區進行復位
buffer.flip();
//寫出數據
sc.write(buffer);
//清空緩衝區數據
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//關閉資源
if(sc != null){
try {
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
5、AIO
異步IO則是採用“訂閱-通知”模式:即應用程序向操作系統註冊IO監聽,然後繼續做自己的事情。當操作系統發生IO事件,並且準備好數據後,在主動通知應用程序,觸發相應的函數.
AIO是在NIO的基礎上引入了異步通道的概念,並提供了異步文件和異步套接字通道的實現,從而在真正意義上實現了異步非阻塞,之前的NIO只是非阻塞而並非異步。AIO不需要通過對多路複用器對註冊的通道進行輪詢操作即可實現異步讀寫,從而簡化NIO編程模型。
代碼示例
服務器端
package com.xyq.aio;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server {
//線程池
private ExecutorService es;
//線程組
private AsynchronousChannelGroup acg;
//服務器通道
public AsynchronousServerSocketChannel assc;
public static void main(String[] args) {
Server server = new Server(8765);
}
public Server(int port) {
try {
//創建一個緩存池
es = Executors.newCachedThreadPool();
//創建線程組
acg = AsynchronousChannelGroup.withCachedThreadPool(es, 1);
//創建服務器通道
assc = AsynchronousServerSocketChannel.open(acg);
//進行綁定
assc.bind(new InetSocketAddress(port));
System.out.println("server start, port " + port);
//進行阻塞
assc.accept(this, new ServerHandler());
//一直阻塞 不讓服務器停止
Thread.sleep(Integer.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}
}
服務器響應工具類
package com.xyq.aio;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
public class ServerHandler implements CompletionHandler<AsynchronousSocketChannel, Server>{
@Override
public void completed(AsynchronousSocketChannel asc, Server server) {
//當有下一個客戶端接入的時候 直接調用Server的accept方法,這樣反覆執行下去,保證多個客戶端都可以阻塞
server.assc.accept(server, this);
//執行讀方法
read(asc);
}
private void read(final AsynchronousSocketChannel asc){
//讀取數據
ByteBuffer buffer = ByteBuffer.allocate(1024);
asc.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
//進行讀取之後,重置標識位
buffer.flip();
//獲得讀取的字節數
System.out.println("server 收到客戶端的數據長度爲" + result);
//獲取讀取的數據
String resultDate = new String(buffer.array()).trim();
System.out.println("server 收到客戶端的數據爲" + resultDate);
//給客戶端響應信息
String response = "服務器響應 收到了客戶端的信息" + resultDate;
//調用寫方法
write(asc, response);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
private void write(AsynchronousSocketChannel asc, String response){
try {
//創建緩衝區
ByteBuffer buffer = ByteBuffer.allocate(1024);
//加載數據
buffer.put(response.getBytes());
//復位
buffer.flip();
//寫出數據
asc.write(buffer).get();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Server attachment) {
exc.printStackTrace();
}
}
客戶端
package com.xyq.aio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
public class Client implements Runnable{
//客戶端通道
AsynchronousSocketChannel asc;
public static void main(String[] args) throws Exception {
Client client = new Client();
client.connect();
Client client2 = new Client();
client2.connect();
Client client3 = new Client();
client3.connect();
new Thread(client, "client").start();
new Thread(client2, "client2").start();
new Thread(client3, "client3").start();
Thread.sleep(1000);
client.write("c1 aaa");
client2.write("c2 bbb");
client3.write("c3 ccc");
}
public Client() throws IOException {
//打開通道
asc = AsynchronousSocketChannel.open();
}
//建立連接
public void connect(){
asc.connect(new InetSocketAddress("127.0.0.1", 8765));
}
//寫出數據
public void write(String response){
try {
asc.write(ByteBuffer.wrap(response.getBytes())).get();
read();
} catch (Exception e) {
e.printStackTrace();
}
}
//讀入數據
public void read(){
//建立緩衝區
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
//讀數據
asc.read(buffer).get();
//復位
buffer.flip();
byte[] respon = new byte[buffer.remaining()];
buffer.get(respon);
System.out.println(new String(respon, "utf-8").trim());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
}
}
}