? ? ??傳統(tǒng)的IO操作是同步阻塞IO模式(BIO)乱凿,數(shù)據(jù)的讀取寫入必須阻塞在一個線程內(nèi)等待其完成转培。NIO則是同步非阻塞IO模式。BIO面向流操作添祸,NIO面向緩沖區(qū)操作申尤。
? ? ??NIO主要有三大核心部分:Channel(通道)癌幕,Buffer(緩沖區(qū)), Selector。傳統(tǒng)IO基于字節(jié)流和字符流進行操作昧穿,而NIO基于Channel和Buffer(緩沖區(qū))進行操作勺远,數(shù)據(jù)總是從通道讀取到緩沖區(qū)中,或者從緩沖區(qū)寫入到通道中时鸵。Selector(選擇區(qū))用于監(jiān)聽多個通道的事件(比如:連接打開胶逢,數(shù)據(jù)到達)。因此饰潜,單個線程可以監(jiān)聽多個數(shù)據(jù)通道初坠。
一 Channel(通道)
? ? ? ?Chanel通道相當于IO操作的載體,數(shù)據(jù)通過Channel讀取和寫入彭雾,全雙工模式(雙向)碟刺。Channel類似流,但是又和流不同薯酝,流的讀寫是單向的比如InputStream半沽、OutputStream爽柒。但是Chanel既可以從通道里面讀取數(shù)據(jù)又可以把數(shù)據(jù)寫到通道里面去。通道中的數(shù)據(jù)總是要先讀到一個Buffer者填,或者總是要從一個Buffer中寫入浩村。
1.1 FileChannel
? ? ? ?FileChannel是一個基于文件的通道≌加矗可以通過文件通道讀寫文件心墅。有一點要注意FileChannel無法設置為非阻塞模式。它總是以阻止模式運行榨乎。
FileChannel提供的函數(shù)
方法 | 解釋 |
---|---|
open | 打開一個文件怎燥,把文件和通道關聯(lián)起來 |
read | 從當前通道讀取字節(jié)序列到給定的緩沖區(qū) |
write | 從緩沖區(qū)向該通道寫入字節(jié)序列 |
position | 跳轉到文件的指定位置 |
size | 獲取文件大小 |
truncate | 截取文件 |
force | 將通道里尚未寫入磁盤的數(shù)據(jù)強制寫到磁盤上 |
transferTo | 將字節(jié)從當前通道傳輸?shù)浇o定的可寫字節(jié)通道 |
transferFrom | 將給定的可讀字節(jié)通道上的字節(jié)傳輸?shù)疆斍巴ǖ乐?/td> |
map | 將當前通道某個區(qū)域直接映射到內(nèi)存中 |
lock | 獲取此通道文件的獨占鎖定 |
tryLock | 嘗試獲取此通道文件的給定區(qū)域的鎖定 |
? ? ? ?下面我們通過一個簡單的實例來看FileChannel怎么使用。
@Test
public void fileChannelRead() {
try {
// 開啟FileChannel
RandomAccessFile aFile = new RandomAccessFile("D:\\job\\git\\java-study\\nio\\src\\main\\resources\\fileChanel.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
// 從FileChannel通道讀取數(shù)據(jù)到緩沖區(qū)ByteBuffer
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("讀取到的數(shù)據(jù)長度 " + bytesRead);
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.clear();
// 繼續(xù)讀取文件信息
bytesRead = inChannel.read(buf);
}
aFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void fileChannelWrite() {
try {
// 開啟FileChannel
RandomAccessFile aFile = new RandomAccessFile("D:\\job\\git\\java-study\\nio\\src\\main\\resources\\fileChanelWrite.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
byte[] forWrite = "需要寫入的字符串谬哀。".getBytes(StandardCharsets.UTF_8);
buf.put(forWrite, 0, forWrite.length);
buf.flip();
// 寫入數(shù)據(jù)
while (buf.hasRemaining()) {
inChannel.write(buf);
}
aFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
1.2 DatagramChannel
? ? ? ?DatagramChannel主要是用來基于UDP通信的通道刺覆。
DatagramChannel方法介紹
DatagramChannel方法 | 返回值 | 解釋 |
---|---|---|
open() | DatagramChannel | 創(chuàng)建通道 |
bind(SocketAddress local) | DatagramChannel | 綁定端口 |
validOps() | int | 只支持OP_READ/OP_WRITE兩種操作 |
socket() | DatagramSocket | 獲取與其關聯(lián)的底層DatagramSocket |
isConnected() | boolean | 檢測是否已經(jīng)建立了Socket鏈接 |
connect(SocketAddress remote) | DatagramChannel | 鏈接remote端 |
disconnect() | DatagramChannel | 斷開通道連接 |
getRemoteAddress() | SocketAddress | 獲取遠程地址 |
receive(ByteBuffer dst) | SocketAddress | 接收數(shù)據(jù) |
send() | int | 發(fā)送數(shù)據(jù)严肪,向指定的地址發(fā)送數(shù)據(jù) |
read() | int | 必須在connect()之后調(diào)用史煎,接收數(shù)據(jù) |
write() | int | 必須在connect()之后調(diào)用,發(fā)送數(shù)據(jù) |
getLocalAddress() | SocketAddress | 獲取本地地址 |
注意驳糯,connect()篇梭、send()、read() 三個函數(shù)是配套使用的酝枢。
? ? ? ?接下來我們通過三個例子來說明DatagramChannel的用法恬偷,下面的例子都是阻塞模式的,等講到Selector的時候我們在講非阻塞的用法帘睦。
1.2.1 UDP服務端
? ? ? ?UDP服務端需要調(diào)用bind()函數(shù)綁定本地端口袍患。
/**
* UDP 服務端
*/
@Test
public void datagramChannelService() {
try {
// 獲取通道
DatagramChannel datagramChannel = DatagramChannel.open();
// 綁定端口8989,作為UDP服務端
datagramChannel.bind(new InetSocketAddress(8989));
// 分配Buffer,用于收發(fā)數(shù)據(jù)
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
// 等待接受客戶端發(fā)送數(shù)據(jù)
SocketAddress socketAddress = datagramChannel.receive(buffer);
if (socketAddress != null) {
buffer.flip();
byte[] b = new byte[buffer.limit()];
int bufferReceiveIndex = 0;
while (buffer.hasRemaining()) {
b[bufferReceiveIndex++] = buffer.get();
}
System.out.println("收到客戶端消息 " + socketAddress.toString() + ":" + new String(b, StandardCharsets.UTF_8));
// 接收到消息后給發(fā)送方回應
sendDataBack(socketAddress, datagramChannel);
}
}
} catch (IOException e) {
// ignore
}
}
/**
* 給socketAddress地址發(fā)送消息
*/
private void sendDataBack(SocketAddress socketAddress, DatagramChannel datagramChannel) throws IOException {
String message = "send back";
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(message.getBytes(StandardCharsets.UTF_8));
buffer.flip();
datagramChannel.send(buffer, socketAddress);
}
1.2.2 UDP客戶端
? ? ? ?如果UDP作為客戶端的話,可以直接往UDP服務端發(fā)送消息竣付,服務端接收到消息的時候同時獲取到了對應客戶端的地址信息诡延。又可以把消息發(fā)送回來。
// UDP客戶端
@Test
public void datagramChannelClient() {
try {
final DatagramChannel channel = DatagramChannel.open();
// 開一個線程一直接收UDP服務端發(fā)送過來的消息
new Thread(() -> {
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
SocketAddress socketAddress = channel.receive(buffer);
if (socketAddress != null) {
buffer.flip();
byte[] b = new byte[buffer.limit()];
int bufferReceiveIndex = 0;
while (buffer.hasRemaining()) {
b[bufferReceiveIndex++] = buffer.get();
}
System.out.println("收到消息 " + socketAddress.toString() + ":" + new String(b, StandardCharsets.UTF_8));
}
}
} catch (Exception e) {
// ignore
}
}).start();
int messageIndex = 0;
// 控制臺輸入數(shù)據(jù)古胆,然后發(fā)送給指定的地址
while (true) {
// 5S發(fā)送一次數(shù)據(jù)
Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS);
sendMessage(channel, new InetSocketAddress("192.168.5.14", 8989), String.valueOf(messageIndex++));
}
} catch (IOException e) {
// ignore
}
}
private void sendMessage(DatagramChannel channel, InetSocketAddress address, String mes) throws IOException {
if (mes == null || mes.isEmpty()) {
return;
}
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
buffer.put(mes.getBytes(StandardCharsets.UTF_8));
buffer.flip();
channel.send(buffer, address);
}
1.2.3 connect用法
? ? ? ?DatagramChannel的connect()方法可以和指定的地址綁定起來肆良,配合write()、read()函數(shù)在兩者之間收發(fā)消息逸绎。比如下面的實例我們和time-a.nist.gov建立連接獲取時間惹恃。
/**
* UDP connect() 在特定的地址上收發(fā)消息
*/
@Test
public void datagramChannelConnect() {
try {
// 獲取通道
DatagramChannel datagramChannel = DatagramChannel.open();
// 連接到特定的地址,time-a.nist.gov 獲取時間棺牧。只在這個地址間收發(fā)消息 write,read 方法
datagramChannel.connect(new InetSocketAddress("time-a.nist.gov", 37));
ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.put((byte) 0);
buffer.flip();
// 發(fā)送數(shù)據(jù)到 time-a.nist.gov
datagramChannel.write(buffer);
buffer.clear();
// 前四個字節(jié)補0
buffer.putInt(0);
// 從 time-a.nist.gov 讀取數(shù)據(jù)
datagramChannel.read(buffer);
buffer.flip();
// convert seconds since 1900 to a java.util.Date
long secondsSince1900 = buffer.getLong();
long differenceBetweenEpochs = 2208988800L;
long secondsSince1970 = secondsSince1900 - differenceBetweenEpochs;
long msSince1970 = secondsSince1970 * 1000;
Date time = new Date(msSince1970);
// 打印時間
System.out.println(time);
} catch (Exception e) {
// ignore
}
}
再次強調(diào)下巫糙,上面實例代碼我們都是用的阻塞模式實現(xiàn)的。等下面講到Selector的時候我們在講怎么用非阻塞的方式實現(xiàn)颊乘。
1.3 SocketChannel
? ? ? ?SocketChannel主要是用來基于TCP通信的通道曲秉,它一般用來作為客戶端的套接字采蚀,它有點類似于java中的Socket類。
SocketChannel方法介紹
SocketChannel方法 | 返回值 | 解釋 |
---|---|---|
open() | SocketChannel | 創(chuàng)建SocketChannel通道 |
validOps() | int | 通道支持的操作承二,OP_READ榆鼠、OP_WRITE、OP_CONNECT |
bind(SocketAddress local) | SocketChannel | 地址綁定 |
setOption(SocketOption<T> name, T value) | SocketChannel | Socket的選項配置亥鸠,StandardSocketOptions.SO_KEEPALIVE等 |
shutdownInput() | SocketChannel | 在沒有關閉通道的情況下妆够,關閉讀操作連接 |
shutdownOutput() | SocketChannel | 在沒有關閉通道的情況下,關閉到通道的寫操作連接 |
socket() | Socket | 獲取與通道關聯(lián)的socket |
isConnected() | boolean | 判斷通道的網(wǎng)絡socket是否連接 |
isConnectionPending() | boolean | 判斷通道是否正在進行操作連接.只有當尚未finishConnection且已經(jīng)調(diào)用connect時,返回true |
connect(SocketAddress remote) | boolean | 連接通道的socket |
finishConnect() | boolean | 完成到socket通道的連接任務负蚊,一般在非阻塞的情況下用到神妹, |
getRemoteAddress() | SocketAddress | 返回通道socket連接的遠端地址 |
read() | int or long | 接收數(shù)據(jù) |
write | int or long | 發(fā)送數(shù)據(jù) |
getLocalAddress() | SocketAddress | 返回通道socket連接的本地地址 |
? ? ? ?setOption():用于給socket設置一些選項配置,比如keep alive家妆。socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);等等鸵荠。具體可以看看StandardSocketOptions里面的一些標準選項。
? ? ? ?關于connect()伤极、isConnectionPending()蛹找、finishConnect()三個函數(shù)的關系我們稍微屢一下。分兩種情況來考慮:
- 阻塞模式:connect()是阻塞的哨坪,這個時候isConnectionPending()庸疾、finishConnect()兩個函數(shù)我覺得意義不大,因為你本來就是阻塞狀態(tài)的当编,connect()函數(shù)成功了届慈,這兩個函數(shù)值也就確定了:isConnectionPending()->false、finishConnect()->true(如果連接失敗他就是false)忿偷。
- 非阻塞模式:調(diào)用connect()方法底層socket建立連接的時候金顿。因為是非阻塞的如果連接立即建立成功,則返回true鲤桥,否則返回false揍拆。則此后一旦成功建立連接就必須通過調(diào)用finishConnect()方法來完成鏈接。在沒有調(diào)用finishConnect()之前isConnectionPending()->true芜壁,調(diào)用之后isConnectionPending()->false礁凡。
1.3.1 SocketChannel使用
/**
* TCP客戶端,阻塞模式
*/
@Test
public void socketChannelClient() {
try {
SocketChannel channel = SocketChannel.open();
// 這里使用的是阻塞模式
channel.connect(new InetSocketAddress("192.168.5.14", 6800));
// KEEP ALIVE setOption()函數(shù)的使用,一定要在連接成功之后設置
channel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int readLength = channel.read(buffer);
if (readLength >= 0) {
buffer.flip();
byte[] b = new byte[buffer.limit()];
int bufferReceiveIndex = 0;
while (buffer.hasRemaining()) {
b[bufferReceiveIndex++] = buffer.get();
}
System.out.println("收到消息 " + ":" + new String(b, StandardCharsets.UTF_8));
// 把收到的消息又發(fā)送回去
buffer.clear();
buffer.put(b);
buffer.flip();
channel.write(buffer);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
1.4 ServerSocketChannel
? ? ? ?ServerSocketChannel也是用來基于TCP通信的通道,它一般用來作為服務端的套接字慧妄,它有點類似于java中的ServerSocket類顷牌。一般用來接收客戶端的連接,在客戶端連接的的基礎之上做一些收發(fā)消息的處理塞淹。
ServerSocketChannel主要方法
ServerSocketChannel方法 | 返回值 | 解釋 |
---|---|---|
open() | ServerSocketChannel | 建立通道 |
validOps() | int | 當前通道支持的操作窟蓝,OP_ACCEPT |
bind(SocketAddress local) | ServerSocketChannel | 綁定到指定的端口,還可以指定最多多少個連接 |
setOption(SocketOption<T> name, T value) | ServerSocketChannel | Socket的選項配置饱普,StandardSocketOptions.SO_KEEPALIVE等 |
socket() | ServerSocket | 獲取與通道關聯(lián)的socket |
accept() | SocketChannel | 接收客戶端的連接 |
getLocalAddress() | SocketAddress | 返回通道socket連接的本地地址 |
? ? ? ?ServerSocketChannel是用于接收客戶端連接的运挫,在接收到(accept函數(shù))客戶端連接之后會拿到基于客戶端連接的SocketChannel状共。和每個客戶端的操作都是通過SocketChannel實現(xiàn)的。
1.4.1 ServerSocketChannel的使用
/**
* TCP服務端 -- 阻塞模式
*/
@Test
public void socketChannelServer() {
try {
ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress("192.168.5.14", 6800));
while (true) {
// 接收客戶端的連接,之后拿到的就是SocketChannel了谁帕,之后都是基于SocketChannel做相應的操作
SocketChannel clientSocketChannel = channel.accept();
clientSocketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
buffer.put("hello".getBytes(StandardCharsets.UTF_8));
buffer.flip();
// 給客戶端發(fā)送消息
clientSocketChannel.write(buffer);
// 在收下客戶端的消息
buffer.clear();
int readLength = clientSocketChannel.read(buffer);
if (readLength >= 0) {
buffer.flip();
byte[] b = new byte[buffer.limit()];
int bufferReceiveIndex = 0;
while (buffer.hasRemaining()) {
b[bufferReceiveIndex++] = buffer.get();
}
System.out.println("收到消息 " + ":" + new String(b, StandardCharsets.UTF_8));
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
二 Buffer(緩沖區(qū))
? ? ? ?Buffer用于和Channel通道進行交互峡继。如你所知,數(shù)據(jù)是從通道讀入緩沖區(qū)匈挖,從緩沖區(qū)寫入到通道中的碾牌。緩沖區(qū)本質(zhì)上是一塊可以寫入數(shù)據(jù),然后可以從中讀取數(shù)據(jù)的內(nèi)存儡循。這塊內(nèi)存被包裝成了NIO Buffer對象舶吗。
? ? ? ?我們先介紹先Buffer里面的三個屬性,接著在介紹下Buffer里面主要的方法择膝。
Buffer里面三個屬性:capacity誓琼、position、limit肴捉。
- capacity:作為一個內(nèi)存塊腹侣,Buffer有一個固定的大小值,也叫“capacity”.你只能往里寫capacity個byte每庆、long筐带,char等類型今穿。一旦Buffer滿了缤灵,需要將其清空(通過讀數(shù)據(jù)或者清除數(shù)據(jù))才能繼續(xù)往里寫數(shù)據(jù)。
- position:當你寫數(shù)據(jù)到Buffer中時蓝晒,position表示當前的位置腮出。初始的position值為0。當一個byte芝薇、long等數(shù)據(jù)寫到Buffer后胚嘲,position會向前移動到下一個可插入數(shù)據(jù)的Buffer單元。position最大可為capacity–1洛二;當讀取數(shù)據(jù)時馋劈,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式晾嘶,position會被重置為0妓雾。當從Buffer的position處讀取數(shù)據(jù)時,position向前移動到下一個可讀的位置垒迂。position簡單來說就相當于游標的作用械姻。
- limit:在寫模式下,Buffer的limit表示你最多能往Buffer里寫多少數(shù)據(jù)机断。寫模式下楷拳,limit等于Buffer的capacity绣夺;當切換Buffer到讀模式時,limit表示你最多能讀到多少數(shù)據(jù)欢揖。因此陶耍,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值她混。換句話說物臂,你能讀到之前寫入的所有數(shù)據(jù)(limit被設置成已寫數(shù)據(jù)的數(shù)量,這個值在寫模式下就是position)产上。
一定要注意position棵磷、limit在讀模式和謝模式下代表的含義,以及兩個模式之間切換的時候position晋涣、limit做了那些變化仪媒。
Buffer里面常用函數(shù)。
public abstract class Buffer {
/**
* 獲取當前緩沖區(qū)的容量 -- capacity
*/
public final int capacity();
/**
* 獲取當前緩沖區(qū)的位置 -- position
*/
public final int position();
/**
* 設置當前緩沖區(qū)的位置 -- position
*/
public final Buffer position(int newPosition);
/**
* 獲取當前緩沖區(qū)的限制 -- limit
*/
public final int limit();
/**
* 設置當前緩沖區(qū)的限制 -- limit
*/
public final Buffer limit(int newLimit);
/**
* mark(), reset()函數(shù)是配對使用的谢鹊,將當前緩沖區(qū)的標記(mark)設置在當前位置(position) -- mark
*/
public final Buffer mark();
/**
* 通過調(diào)用mark()方法算吩,可以標記Buffer中的一個特定position。
* 之后可以通過調(diào)用Buffer.reset()方法恢復到這個position
*/
public final Buffer reset();
/**
* 清除此緩存區(qū)佃扼。將position = 0;limit = capacity;mark = -1;一般在寫入數(shù)據(jù)之前調(diào)用
*/
public final Buffer clear();
/**
* flip()方法可以把Buffer從寫模式切換到讀模式偎巢。調(diào)用flip方法會把position歸零,
* 并設置limit為之前的position的值兼耀。
* 也就是說压昼,現(xiàn)在position代表的是讀取位置,limit標示的是已寫入的數(shù)據(jù)位置瘤运。
*/
public final Buffer flip();
/**
* 將position設回0窍霞,這個時候你可以重讀Buffer中的所有數(shù)據(jù)。
* limit保持不變拯坟,仍然表示能從Buffer中讀取多少個元素
*/
public final Buffer rewind();
/**
* return limit - position; 返回limit和position之間相對位置差
*/
public final int remaining();
/**
* return position < limit但金,返回是否還有未讀內(nèi)容
*/
public final boolean hasRemaining();
/**
* 判斷此緩沖區(qū)是否為只讀
*/
public abstract boolean isReadOnly();
/**
* 判斷此緩沖區(qū)是否由可訪問的數(shù)組支持
*/
public abstract boolean hasArray();
/**
* 返回支持此緩沖區(qū)的數(shù)組
*/
public abstract Object array();
/**
* 返回該緩沖區(qū)的緩沖區(qū)的第一個元素的背襯數(shù)組中的偏移量
*/
public abstract int arrayOffset();
/**
* 判斷個緩沖區(qū)是否為 direct
*/
public abstract boolean isDirect();
}
flip()、hasRemaining()郁季、clear()冷溃、rewind()、mark()梦裂、reset()幾個函數(shù)要著重理解下似枕。
? ? ? ?要想使用Buffer來讀寫數(shù)據(jù)一般遵循以下四個步驟:
- 寫數(shù)據(jù)到Buffer里面∪恚可以從Channel讀取出來寫入到緩沖區(qū)中菠净,也可以調(diào)用put方法寫入到緩沖區(qū)中。
- 調(diào)用flip()方法,切換到讀數(shù)據(jù)模式毅往。這個時候position指向第一個位置牵咙,limit指向?qū)懭霐?shù)據(jù)的最后位置。
- 從Buffer中讀取數(shù)據(jù)攀唯。一般從緩沖區(qū)讀取數(shù)據(jù)寫入到通道中洁桌,也可以調(diào)用get方法讀取到Buffer里面的數(shù)據(jù)。
- 調(diào)用clear()或者compact()方法清空緩沖區(qū)侯嘀。
? ? ? ?當向buffer寫入數(shù)據(jù)時另凌,buffer會記錄下寫了多少數(shù)據(jù)。一旦要讀取數(shù)據(jù)戒幔,需要通過flip()方法將Buffer從寫模式切換到讀模式吠谢。
? ? ? ?在讀模式下,可以讀取之前寫入到buffer的所有數(shù)據(jù)诗茎。一旦讀完了所有的數(shù)據(jù)工坊,就需要清空緩沖區(qū),讓它可以再次被寫入敢订。
? ? ? ?有兩種方式能清空緩沖區(qū):
- clear():方法會清空整個緩沖區(qū)王污。
- compact():這個方法在ByteBuffer、CharBuffer楚午、ShortBuffer等方法里面提供昭齐。 該方法只會清除已經(jīng)讀過的數(shù)據(jù)。任何未讀的數(shù)據(jù)都被移到緩沖區(qū)的起始處矾柜,新寫入的數(shù)據(jù)將放到緩沖區(qū)阱驾。
? ? ? ?我們使用Buffer緩沖區(qū)。一般使用的都是他的子類:ByteBuffer把沼、CharBuffer啊易、ShortBuffer吁伺、LongBuffer饮睬、FloatBuffer、DoubleBuffer這些篮奄。Buffer的這些子類都是基于JAVA一些基本數(shù)據(jù)類型實現(xiàn)的一個Buffer緩沖區(qū)捆愁。我們先看下這些子類一般都會有的一些方法(Buffer里面的方法他們都會有,Buffer里面的方法我們就不重復介紹了)窟却。
出Buffer提供的放方法之外昼丑,子類里面額外的方法。
Buffer子類方法 | 描述 | |
---|---|---|
allocate(int capacity) | Buffer實例化方法 | 從堆空間中分配一個容量大小為capacity的對應類型的數(shù)組作為緩沖區(qū)的數(shù)據(jù)存儲器 |
allocateDirect(int capacity) | Buffer實例化方法 | 不使用JVM堆棧而是通過操作系統(tǒng)來創(chuàng)建內(nèi)存塊用作緩沖區(qū)夸赫,它與當前操作系統(tǒng)能夠更好的耦合菩帝,因此能進一步提高I/O操作速度。但是分配直接緩沖區(qū)的系統(tǒng)開銷很大,因此只有在緩沖區(qū)較大并長期存在呼奢,或者需要經(jīng)常重用時宜雀,才使用這種緩沖區(qū) |
wrap(T[] array) | Buffer實例化方法 | 這個緩沖區(qū)的數(shù)據(jù)會存放在對應數(shù)組中,對應數(shù)組或buff緩沖區(qū)任何一方中數(shù)據(jù)的改動都會影響另一方握础。其實Buffer底層本來就有一個對應數(shù)組負責來保存buffer緩沖區(qū)中的數(shù)據(jù)辐董,通過allocate方法系統(tǒng)會幫你構造一個對應類型組 |
wrap(T[] array, int offset,intlength) | Buffer實例化方法 | 在上一個方法的基礎上可以指定偏移量和長度,這個offset也就是包裝后byteBuffer的position禀综,而length呢就是limit-position的大小简烘,從而我們可以得到limit的位置為length+position(offset) |
slice() | 常規(guī)方法 | 創(chuàng)建新的緩沖區(qū),其內(nèi)容是此緩沖區(qū)內(nèi)容的共享子序列 |
duplicate() | 常規(guī)方法 | 創(chuàng)建共享此緩沖區(qū)內(nèi)容的新的字節(jié)緩沖區(qū) |
asReadOnlyBuffer() | 常規(guī)方法 | 創(chuàng)建共享此緩沖區(qū)內(nèi)容的新的只讀字節(jié)緩沖區(qū) |
get() | 常規(guī)方法 | 從緩沖區(qū)獲取數(shù)據(jù) |
put() | 常規(guī)方法 | 把數(shù)據(jù)放入到緩沖區(qū)中 |
compact() | 常規(guī)方法 | 清除已經(jīng)讀過的數(shù)據(jù)定枷。任何未讀的數(shù)據(jù)都被移到緩沖區(qū)的起始處 |
? ? ? ?關于Buffer的使用孤澎,我們以ByteBuffer和CharBuffer來舉例說明。其他的Buffer子類適應也都是很簡單的欠窒。
ByteBuffer的使用
@Test
public void byteBufferTest() {
// 創(chuàng)建一個ByteBuffer實例
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 清空
buffer.clear();
// 寫入數(shù)據(jù)
byte[] putByteArray = "hello word!".getBytes(StandardCharsets.UTF_8);
buffer.put(putByteArray);
// 切換到讀模式
buffer.flip();
// 把數(shù)據(jù)讀取出來
buffer.slice();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
// 重新讀
buffer.rewind();
// ps: 這個時候buffer.limit()就是數(shù)組元素的個數(shù)
byte[] retByte = new byte[buffer.limit()];
buffer.get(retByte);
System.out.println(new String(retByte, StandardCharsets.UTF_8));
}
CharBuffer使用距離
@Test
public void charBufferTest() {
// 創(chuàng)建一個ByteBuffer實例
CharBuffer buffer = CharBuffer.allocate(1024);
// 清空
buffer.clear();
// 寫入數(shù)據(jù)
char[] putArray = "hello word!".toCharArray();
buffer.put(putArray);
// 切換到讀模式
buffer.flip();
// 把數(shù)據(jù)讀取出來
buffer.slice();
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
System.out.println();
// 重新讀
buffer.rewind();
// ps: 這個時候buffer.limit()就是數(shù)組元素的個數(shù)
char[] retByte = new char[buffer.limit()];
buffer.get(retByte);
System.out.println(String.valueOf(retByte));
}
三 Selector(多路復用器)
? ? ? ?Selector提供了選擇已就緒任務的能力亥至,Selector會不斷輪詢注冊在上面的Channel,某個Channel發(fā)生讀或?qū)懯录伲瑒t該Channel就處于就緒狀態(tài)姐扮,會被Selector輪詢出來衣吠。然后通過SelectionKey獲取就緒Channel的集合茶敏,進行后續(xù)IO操作。Selector允許單線程處理多個Channel缚俏。如果你的應用打開了多個連接(通道)惊搏,但每個連接的流量都很低,使用Selector就會很方便忧换。
Selector方法介紹
Selector方法 | 返回值 | 解釋 |
---|---|---|
open() | Selector | Selector的創(chuàng)建 |
isOpen() | boolean | 判斷此選擇器是否已打開 |
provider() | SelectorProvider | 返回創(chuàng)建此通道的提供程序 |
keys() | Set<SelectionKey> | 返回所有的SelectionKey |
selectedKeys() | Set<SelectionKey> | 返回已選擇的SelectionKey集合恬惯,要在select()之后調(diào)用 |
selectNow() | int | 非阻塞,返回有多少通道就緒 |
select(long timeout) | int | 阻塞到至少有一個通道在你注冊的事件上就緒了 |
select() | int | 阻塞到至少有一個通道在你注冊的事件上就緒了,返回值表示有多少通道就緒 |
wakeup() | Selector | Selector的喚醒 |
? ? ? ?wakeup() 函數(shù)稍微講下亚茬。Selector的選擇方式有三種:select()酪耳、select(timeout)、selectNow()刹缝。selectNow的選擇過程是非阻塞的碗暗,與wakeup沒有太大關系。select(timeout)和select()的選擇過程是阻塞的梢夯,其他線程如果想終止這個過程言疗,就可以調(diào)用wakeup來喚醒select()。
3.1 Selector創(chuàng)建
? ? ? ?通過調(diào)用Selector.open()方法創(chuàng)建一個Selector對象颂砸。
Selector selector = Selector.open();
3.2 把Channel注冊到Selector
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
? ? ? ?注冊到Selector的Channel是有前提添加的噪奄。Channel必須是非阻塞的死姚,必須是SelectableChannel的子類。所以FileChannel不適用Selector勤篮,因為FileChannel不能切換為非阻塞模式知允,更準確的來說是因為FileChannel沒有繼承SelectableChannel。
? ? ? ?把Channel注冊到Selector的時候還得指定監(jiān)聽事件叙谨。就是告訴Selector我這個Channel對什么事件感興趣温鸽。當Channel上有這個事件發(fā)送的時候這個Channel就會被輪詢出來。NIO提供了四個事件:
Channel注冊事件 | 解釋 |
---|---|
Selectionkey.OP_READ | 讀就緒 |
Selectionkey.OP_WRITE | 寫就緒 |
Selectionkey.OP_CONNECT | 連接就緒 |
Selectionkey.OP_ACCEPT | 接收就緒 |
? ? ? ?我們有兩種方式來設置Selector對Channel的哪些事件感興趣手负。一個是在把Channel注冊到Selector的時候設置涤垫。我們上面已經(jīng)講了這種情況。另一個是調(diào)用SelectionKey的interestOps()函數(shù)來修改Selector對Channel感興趣的事件竟终。
3.2.1 SelectionKey
? ? ? ?每個Channel向Selector注冊時,都將會創(chuàng)建一個SelectionKey對象蝠猬。一個SelectionKey鍵表示了一個特定的通道對象和一個特定的選擇器對象之間的注冊關系。并維護了Channel事件统捶。
SelectionKey方法介紹
方法 | 返回值 | 解釋 |
---|---|---|
channel() | SelectableChannel | 返回此選擇鍵所關聯(lián)的通道 |
selector() | Selector | 返回此選擇鍵所關聯(lián)的選擇器 |
isValid() | boolean | 檢測此key是否有效 |
cancel() | void | 請求將此鍵取消注冊.一旦返回成功,那么該鍵就是無效的 |
interestOps() | int | 判斷Selector對Channel的哪些事件感興趣榆芦,OP_READ、OP_WRITE等事件 |
interestOps(int ops) | SelectionKey | 設置Selector對Channel的哪些事件感興趣 |
readyOps() | int | 獲取此鍵上ready操作集合.即在當前通道上已經(jīng)就緒的事件 |
isReadable() | boolean | 檢測此鍵是否為"read"事件.等效于:k.readyOps() & OP_READ != 0 |
isWritable() | boolean | 檢測此鍵是否為"write"事件 |
isConnectable() | boolean | 檢測此鍵是否為"connect"事件 |
isAcceptable() | boolean | 檢測此鍵是否為"accept"事件 |
attach(Object ob) | Object | 將給定的對象作為附件添加到此key上.在key有效期間,附件可以在多個ops事件中傳遞 |
attachment() | Object | 獲取附件.一個channel的附件,可以再當前Channel(或者說是SelectionKey)生命周期中共享,但是attachment數(shù)據(jù)不會作為socket數(shù)據(jù)在網(wǎng)絡中傳輸 |
3.3 從Selector中選擇就緒的Channel
? ? ? ?從Selector中選擇就緒的Channel喘鸟,其實是去選擇SelectionKey匆绣,然后通過SelectionKey拿到對應的Channel。通過Channel做相應的操作什黑。
? ? ? ?從Selector中選擇就緒的Channel也很簡單崎淳。先調(diào)用Selecotor的select()方法選擇出已經(jīng)就緒的通道,Selector會幫助我們把這些就緒的通道放到一個就緒列表里目前愕把。然后我們在調(diào)用Selector的selectedKeys()方法把這些通道都拿出來拣凹。
3.4 Selecotr完整實例
@Test
public void tcpClient() {
try {
SocketChannel socketChannel = SocketChannel.open();
// 連接
socketChannel.connect(new InetSocketAddress("192.168.5.14", 6800));
ByteBuffer writeBuffer = ByteBuffer.allocate(32);
ByteBuffer readBuffer = ByteBuffer.allocate(32);
writeBuffer.put("hello".getBytes());
writeBuffer.flip();
while (true) {
writeBuffer.rewind();
socketChannel.write(writeBuffer);
readBuffer.clear();
socketChannel.read(readBuffer);
readBuffer.flip();
byte[] b = new byte[readBuffer.limit()];
int bufferReceiveIndex = 0;
while (readBuffer.hasRemaining()) {
b[bufferReceiveIndex++] = readBuffer.get();
}
System.out.println("received : " + new String(b));
}
} catch (Exception e) {
// ignore
}
}
@Test
public void tcpServer() {
try {
// 創(chuàng)建一個ServerSocketChannel通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 綁定6800端口
serverChannel.bind(new InetSocketAddress("192.168.5.14", 6800));
// 設置非阻塞
serverChannel.configureBlocking(false);
// Selector創(chuàng)建
Selector selector = Selector.open();
// 注冊 channel,并且指定感興趣的事件是 Accept
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer readBuff = ByteBuffer.allocate(1024);
ByteBuffer writeBuff = ByteBuffer.allocate(1024);
writeBuff.put("received".getBytes());
writeBuff.flip();
while (true) {
if (selector.select() > 0) {
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> readyKeyIterator = readyKeys.iterator();
while (readyKeyIterator.hasNext()) {
SelectionKey key = readyKeyIterator.next();
readyKeyIterator.remove();
if (key.isAcceptable()) {
// 連接
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 我們又給注冊到Selector里面去了恨豁,聲明這個channel只對讀操作感興趣嚣镜。
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 讀
SocketChannel socketChannel = (SocketChannel) key.channel();
readBuff.clear();
socketChannel.read(readBuff);
readBuff.flip();
byte[] b = new byte[readBuff.limit()];
int bufferReceiveIndex = 0;
while (readBuff.hasRemaining()) {
b[bufferReceiveIndex++] = readBuff.get();
}
System.out.println("received : " + new String(b));
// 修改selector對channel感興趣的事件
key.interestOps(SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
// 寫
writeBuff.rewind();
SocketChannel socketChannel = (SocketChannel) key.channel();
socketChannel.write(writeBuff);
// 修改selector對channel感興趣的事件
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
} catch (IOException e) {
// ignore
}
}
? ? ? ?以上,就是我們對JAVA NIO編程的一個簡單介紹橘蜜。最后我們用一個圖來做一個總結菊匿。