注:本文爲對Java socket詳解,看這一篇就夠了續的重排版,方便自己瀏覽。
接下來我們將進一步討論Java socket 異步通信,Java socket異步通信包主要位於是在Java.nio框架下,在講解Java socket異步通信前,我們先來回顧一下傳統socket通信的演進。
Blocking I/O 模式
BlockIng I/O模式下,主要缺點如下:
-
只能用於小規模下多個socket通信,因爲客戶端socket每次連接請求後,服務端ServerSocket都會創建一個線程來處理當前客戶端的連接請求,如果連接數非常大,以千萬級爲單位,那麼服務端的CPU資源開銷會是一個非常龐大的數據。
-
Read、Write讀寫資源問題,由於是阻塞的讀寫模式,如果大量線程處於空閒狀態沒有數據可讀寫,則會造成空閒socket的Read 、Write操作大量阻塞,對系統資源線程的開銷也會造成非常大的浪費。
接下來我們看看NIO(not Blocking I/O ,也有人叫他new IO)的工作原理,NIO主要實現機制於IO最大的區別在他通過選擇器與採用觀察者模式將之前大量連接採用一個線程即可搞定,同時通過通道的方式,可對流進行重複選擇的讀取,下面我們通過圖形來描述一下NIO的工作原理。
NIO模式下下的優點:
-
NIO採用channel 與selector結合方式,可以多次從通過讀寫或者寫入通道數據,並且可以讀取指定位置的數據,而傳統io方式,採用流的方式對數據進行讀取,一但打開流,那麼只能讀取到流的結尾,無法從流指定的位置進行讀取。我們把數據流比作打開的自來水管一樣,你沒法只獲取水流中的一部分數據。
-
socketor選擇器,在通過socketchannl將socket註冊到選擇器中,那麼就可以通過一個線程處理註冊進來的所有socket。socketor說的通俗一點就像飯店的點菜系統,比如說在傳統上,我們點菜的流程是這樣的,拿着菜單,把服務員叫過來,你在點菜,服務員在旁邊候着,形成的方式是客戶和服務員一對一的方式,如果飯店只有10個服務員,那麼我只能服務10個用戶,這樣是效率及其底下的。而使用socketor後,在你點菜的時候,服務員給你一個電子菜單(或者像海底撈的紙質可以選擇的菜單),你自己將需要點的菜在菜單上勾選,點好了直接給服務員就可以了,這樣加入飯店來了100個客人,那個10個服務員就只需要將菜單發給客戶,客戶自己選擇菜名後,交給服務員即可。
-
我們知道流的數據是單向的,而socketChannel則是雙向的,我們繼可以向通道中寫數據,也可以從通道中讀取數據,並且通道中的數據讀寫都是通過buffer實現的。
上面我們簡單的介紹了一下NIO中socket的應用原理,接下來我們詳細介紹一下NIO中socket相關的知識點,由於NIO框架下涉及的類和接口非常多,在這裏我們主要講解的是nio下的socket通信,所有我把nio下的關於socket相關的主要的幾個類和接口進行整理和分類一下,方便大家有個脈絡,其實,我們分析一下,nio下和socket通信相關的我們可以把大分爲三大類(其實應該是倆類,channel 與buffer,selector相關的也是在channel下,在這裏是主要是爲了給大家講解的清楚,我把selector拿出來了,進行單獨的分類),channel、buffer與selector三大類,每一種類型下面涉及到常用的類和接口我在大家整理一下,請看下面的這個思維導圖:
首先我們來看一下buffer、channel與selector這三者之間的區別和聯繫,channel通道,這裏我們可以把它理解爲傳統io的流,而buffer就是針對channel 的一個緩衝區,他就是一個連續的內存塊,是NIO數據的一箇中轉站。我們可以將channel中的數據讀取到buffer中,也可以將buffer中的數據寫入到channel中,所以channel是雙向的,可以進行讀寫操作,而傳統IO基於字節流的操作,讀和寫都是分開的,我們必須打開對應Input纔可以操作IO。
接下來,我們首先看channel包中的這幾個核心的類。在這裏我主要介紹一下服務端的socketChannel 與客戶端的socketchannel。其他的類大家可以自行閱讀API,結合源碼我詳細有更深入的瞭解。
ServerSocketChannel 類是有常用的幾個方法分別是:
-
abstract SocketChannel accept() 。接受來之Channel通道socket的連接。
-
ServerSocketChannel bind(SocketAddress local)。將通道的socket綁定到本地地址。
-
abstract ServerSocketChannel bind(SocketAddresslocal, int backlog)。是上一個方法的重載,也是剛通道的socket綁定到本地地址,第一個參數是本地地址,第二個表示掛起連接數的最大值。
-
abstract SocketAddress getLocalAddress()。返回當前通道socket綁定的本地地址
-
static ServerSocketChannel open()。 打開一個ServersocketChannel。
-
abstract ServerSocket socket()。檢索通道相關聯的socket
由於ServerSocketChannel 繼承了ServerSocketChannel 並且實現了NetworkChannel 的接口,所有他換有一些其他的方法可用,比如:
-
void close()。 關閉通道的方法。
-
abstract SelectableChannel configureBlocking(boolean block)。調整通道的阻塞模式。
-
SelectionKey register(Selectorsel, int ops)。將通道註冊到制定的選擇器上,
-
SelectorProvider provider()。返回創建通道的提供程序
SocketChannel 類是有常用的幾個方法分別是:
-
abstract SocketChannel bind(SocketAddresslocal)。 將通道的socket綁定到本地地址。
-
abstract boolean connect(SocketAddress remote)。
-
abstract SocketAddress getLocalAddress()。
-
abstract SocketAddress getRemoteAddress()。
-
abstract boolean isConnected()。
-
static SocketChannel open()。 打開一個socketChannel
-
static SocketChannel open(SocketAddress remote)。
-
abstract Socket socket()。 檢索與通道相關聯的socket
-
abstract SocketChannels hutdownInput()。 在不關閉通道的情況下,關閉連接已方便獲取數據。
Selector 類是有常用的幾個方法分別是:
-
abstract void close() 。 關閉當前選擇器
-
abstract boolean isOpen()。 當前選擇器是否打開
-
abstract Set keys()。返回當前選擇器中的key,是一個set集合
-
static Selector open()。 打開一個選擇器。
-
abstract int select()。當對的通道io準備好時選擇一組鍵,
-
abstract Set selectedKeys()。返回當前選擇器的selected-key set.集合
SelectionKey類有四個屬性,分別是:
static int OP_ACCEPT 接受socket
static int OP_CONNECT 開始連接
static int OP_READ 開始讀數據
static int OP_WRITE 開始寫數據
同時也有對應的幾個方法。分別是isAcceptable()、isConnectable()、isReadable() 和isWritable()。
上面我們對常用的幾個接口和方法進行進行了詳細的介紹,接下來我們就通過詳細的例子抽絲剝繭瞭解他們的原理,
先來一個簡單的例子:
ServerSocketChannel 服務端.
package SocketChannel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
public class ServerSocketChnnel1 {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9000));
serverSocketChannel.configureBlocking(false);
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
while (socketChannel!=null){
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int i = socketChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
}
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
客戶端:
package socket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Date;
public class ClientSocket {
public static void main(String[] args) {
Socket socket;
{
try {
socket = new Socket("127.0.0.1",9000);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("你好".getBytes());
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
運行後我們可以看到控制檯收到了客戶端的信息。
錯誤,我們先不需要關注,後面我們一步一步帶大家講解。
首先我們分析服務端程序:
第一步:通過ServerSocketCHannel.open()打開這個Channel通道,我們看一下他這個源碼:
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
}
他是調用SelectorProvider類的provider()方法,獲取SelectorProvider,然後在調用SelectorProvider的openServerSocketChannel()方法。其中provider()方法是一個線程安全的。
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
if (loadProviderFromProperty())
return provider;
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
通過上面我們可以看到,open()方法打開一個線程安全的ServerSocketChannel。
第二步:我們通過bind()方法綁定對應的端口。這個和我們普通的ServerSocket類似。
第三步:通過configureBlocking()方法設置阻塞方式。
第四步:就可以通過accept方法接受對應的請求了。
通過上面的小例子,我們簡單的描述了一下ServerSocketChannel最基本的概念和應用,讓大家有一個初步的認識,那麼在接下來的示例中,我會引入Selector 選擇器、ByteBuffer緩存、已經IO多路複用的幾種模式。
上面只是一個簡單的SocketChannel示例,一次接受一個一個socket請求,接下來我們對上面的是示例進一步細化。
服務端ServerSocketChannel 示例:
package SocketChannel;
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.nio.charset.Charset;
/**
* @創建人:zhangzhiqiang
* @創建時間:2019/09/03
* @描述:
* @聯繫方式:QQ:125717901
**/
public class ServerSocketChannel2 {
public static void main(String[] args) {
try {
// 第一步
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9000));
serverSocketChannel.configureBlocking(false);
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel!=null){
System.out.println("有新的客戶端進來連接");
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (socketChannel.read(byteBuffer)!=-1){
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
byteBuffer.clear();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客戶端SocketChannel示例:
package SocketChannel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @創建人:zhangzhiqiang
* @創建時間:2019/09/03
* @描述:
* @聯繫方式:QQ:125717901
**/
public class SocketChannel2 {
public static void main(String[] args) {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",9000));
socketChannel.configureBlocking(false);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String str = null;
while ((str=bufferedReader.readLine())!=null){
ByteBuffer byteBuffer = ByteBuffer.allocate(str.length());
byteBuffer.put(str.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在這裏,我們服務端和客戶端都是採用Channel的方式創建的,和傳統的Socket不一樣,服務端不通過的輪詢是否有信息進來,如果有信息進來我們就創建一個ByteBuffer用來接收信息,然後在打印出來。服務端我們設置的非阻塞的方式,所以在下面接收到客戶端socket的時候,需要進行一個判斷,當前socket是否爲null。
知識點:
- serverSocketChannel.configureBlocking(false); 設置通道的阻塞方式。
- if(socketChannel!=null) ;判斷當前socket是否爲空,如果不爲空我們才能進行後續的業務操作。
- ByteBuffer.allocate(1024); 分配指定大小的ByteBuffer,ByteBuffer.allocateDirect(1024)分配一個直接的ByteBuffer效率更高,對於將一些文件讀取到內存中處理來說可以使用該方法。
- byteBuffer.flip();用於將緩存區進行翻轉,通過我們將數據寫入Buffer中,如果想讀取的話,一般我們採用byteBuffer.flip()方法將緩存區進行翻轉,然後在讀取緩存區的數據,其實這個byteBuffer.flip()方法並不是真的把緩存區翻了一個,他只是將buffer中的mark、position、limit進行重新標記了一下,方便數據讀取。我們來看一下flip()方法的源碼:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
在這裏我們可以看到,重置了position 和limit 。
- ByteBuffer數據打印問題,由於採用的Unicode編碼,如果我們直接通過array方法打印可能出現亂碼的問題。
- byteBuffer.clear();清除Buffer,在數據讀取完畢後,我們一般會調用byteBuffer.clear();方便下次數據的寫入,如果不調用的話,每次讀數的數據都是在上次數據後的追加。其實byteBuffer.clear()方法也不是真的把buffer中的數據清除掉,他也是將buffer中的mark、position、limit進行重新標記了一下,方便下次數寫入,我們來看一下源碼:
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
他和flip()的區別就在與limit 的值,下面我們舉例說明:
比如說我們在Buffer中有一個“helloword"字符串。並且我們Buffer的 capacity設置的爲20,則:
當前buffer的各項值:
position = 9
limit = 20
capacity = 20
執行flip後:
position = 0
limit = 9
capacity = 20
執行clear 後:
position = 0
limit = 20
capacity = 20
package Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
/**
* @創建人:zhangzhiqiang
* @創建時間:2019/09/03
* @描述:
* @聯繫方式:QQ:125717901
**/
public class Buffer1 {
public static void main(String[] args) {
//1 將數據寫到buff
CharBuffer charBuffer = CharBuffer.allocate(20);
charBuffer.put("helloword");
System.out.println("capacity = "+charBuffer.capacity());
System.out.println("limit = " +charBuffer.limit());
System.out.println("position = "+charBuffer.position());
charBuffer.flip();
System.out.println("執行flip後:");
System.out.println("capacity = "+charBuffer.capacity());
System.out.println("limit = " +charBuffer.limit());
System.out.println("position = "+charBuffer.position());
System.out.println("執行clear後:");
charBuffer.clear();
System.out.println("capacity = "+charBuffer.capacity());
System.out.println("limit = " +charBuffer.limit());
System.out.println("position = "+charBuffer.position());
System.out.println("打印buffer中的數據 = "+ charBuffer.clear());
}
}
執行後的結果我們可以衝控制檯看到和我們上面分析的結果一致,執行clear後,我們換是可以將buffer的數據打印出來的。
下面是position 和limit的一個圖例:
通過上面的例子我們瞭解的CHannel、和Buffer的基本一些用法和原理,寫了一個簡單的例子,展示了一下客戶端socket通過System的in方式獲取數,然後發送到服務端,服務端是如何接受的,在這裏的基礎上,我們下一步引入selector選擇器,如果是沒有selector,我感覺Channel和傳統的socket沒有什麼大的區別,並且意義不大。接下來我們引入selector,還是通過詳細的示例一步一步給大家講解。
。。。。。。。。。。。。。