前言
基於NIO的網絡編程實例
提示:以下是本篇文章正文內容,下面案例可供參考
一、NIO與BIO的比較
NIO與BIO的比較 :
- BIO是以流的方式處理的 , NIO是以塊的方式處理的(因爲有Buffer) , 塊I/O效率更高
- BIO是基於字節流字符流處理的,NIO是基於Channel和Buffer處理的,單個線程可監聽多個客戶端的通道
NIO三個核心組件之間的關係
- 每個Channel都對應一個Buffer , Channel是雙向的,可讀可寫;
- Selector對應一個線程 ;
- 一個線程對應多個Channel ;
- 程序切換到哪個Channel是由時間Event決定的 ;
- Selector會根據不同的事件在各個通道上切換 ;
- Buffer 就是一個內存塊 , 底層是一個數組 ;
- 數據的讀取/寫入是通過Buffer進行的 ,是可以讀也可以寫的 ,但需要flip方法做讀寫切換 ,與BIO有本質區別
二、Buffer的機制及其子類
1.Buffer的使用
Buffer的子類型 : 除了Boolean類型,其餘7個Java子類型Buffer都有
public static void main(String[] args) {
// buffer的使用
// 1. 創建一個Buffer
IntBuffer intBuffer = IntBuffer.allocate(5); // 創建一個容量爲5的Buffer
// 2. 向Buffer中存放數據
for (int i = 0; i < 5; i++) {
intBuffer.put(i*2);
}
// 3. 從Buffer中讀取數據
// 將Buffer做讀寫切換
intBuffer.flip();
while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get()); // get方法裏維護了一個索引
}
}
2.Buffer的四個基本類型
以InteBuffer爲例 , 真正的數據是存放在 final int[] hb;
數組裏的
Buffer中定義了所有緩衝區都具有的4個屬性
private int mark = -1; //標記,一般不被修改
private int position = 0; //下一個要被讀寫的元素的索引
private int limit; //緩衝區的當前終點 (數組索引的最大值)
private int capacity; // 最大容量
其中 , position不能超過limit , position可以被理解爲一個遊標 , 讀寫的時候是根據position的位置進行的
當調用了 flip()
函數反轉過後 , position會被置爲0
同時 , 上述的4個參數都有其對應的函數來修改他們的值
public final Buffer clear()
方法能將這4個參數恢復到初始狀態 , 但是數據不會真正的被擦除
三、Channel的使用
1. Channel的特徵
- 通道是可以同時讀寫的
- 可以實現異步讀寫數據
- 可以從Buffer中讀取數據 , 也可以向Buffer寫入數據
2. Channel的子類
Channel的子類 : FileChannel文件數據的讀寫 , DatagramChannel UDP數據的讀寫 , ServerScoketChannel和ScoketChannel用於TCP數據的讀寫
我們用於網絡編程最常用的當然就是ServerScoketChannel和ScoketChannel了
(1) FileChannel實例:
public static void main(String[] args) throws IOException {
String str = "hello world";
// 創建一個輸出流
FileOutputStream fileOutputStream = new FileOutputStream("d://file01.txt");
// 通過輸出流獲取對應的FileChannel , 其真實類型爲FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
//創建一個ButeBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 將str放到byte Buffer
byteBuffer.put(str.getBytes());
// filp反轉
byteBuffer.flip();
//將byteBuffer數據寫入fileChannel
fileChannel.write(byteBuffer);
fileOutputStream.close(); //關閉最底層的流
}
這裏需要注意 : 當需要buffer從讀轉爲寫時,需要調用flip
函數做讀寫切換
(2) 拷貝文件
/**
* 拷貝文件
* */
public static void copy() throws IOException {
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileChannel channel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel channel1 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true){
// 這裏必須要清空一次數據,將關鍵屬性重置
/**
* 如果這裏不做復位,read的值會一直是0,程序會一直讀取數據,進入死循環
* */
byteBuffer.clear();
int read = channel.read(byteBuffer);
if (read == -1){
break;
}
// 將buffer中的數據寫入到channel1
byteBuffer.flip();
channel1.write(byteBuffer);
}
fileInputStream.close();
fileOutputStream.close();
}
四、Buffer類型化和只讀
1. 類型化
所謂的類型化就是指 , 存進去的時什麼數據類型的 . 讀取的就要是什麼數據類型 , 否則會報錯
public static void main(String[] args) {
// 創建一個buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
for (int i = 0; i < 64; i++) {
byteBuffer.put((byte)i);
}
//反轉並讀取
byteBuffer.flip();
//獲取一個只讀的buffer
ByteBuffer readOnlyBuffer = byteBuffer.asReadOnlyBuffer();
while (readOnlyBuffer.hasRemaining()){
System.out.println(readOnlyBuffer.get());
}
}
2. Buffer的分散和聚合
Scattering: 將數據寫入到buffer中,可以採用buffer數組,依次寫入
Gathering : 從buffer讀取數據時 , 採用buffer數組以此讀
分散和聚合涉及到同時操作多個Buffer
五、MappedByteBuffer
操作系統級別 , 性能比較高
MappedByteBuffer可以直接在內存(堆外內存)中修改文件 , 操作系統不需要再拷貝一次;
public static void main(String[] args) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
FileChannel channel = randomAccessFile.getChannel();
/**
* p1 FileChannel.MapMode.READ_WRITE 使用讀寫模式
*
* p2 直接修改的起始位置
*
* p3 目標文件將多少個字節映射到內存中
* p2 p3 表示程序可以直接修改的範圍
* */
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
// 修改對應內容
mappedByteBuffer.put(0,(byte)'A');
mappedByteBuffer.put(3,(byte)9);
randomAccessFile.close();
}
六、Selector
能夠檢測多個註冊上來的通道中是否有時間發生
只有連接/通道上真正有讀寫事件發生時,纔會進行讀寫
避免了多線程上下文切換導致的開銷
1. SelectionKey在NIO體系中的作用
- 當客戶端連接時, 會通過ServerSocketChannel得到對應的SocketChannel;
- 將SocketChannel註冊到Selector上, 使用的是register(Selector sel, int ops), 一個selector上可以註冊多個SocketChannel;
- 註冊後會返回一個SelectionKey , 會和該Selector關聯起來
- Selector進行監聽selector方法, 返回有事件發生的channel;
- 進一步得到各個有事件發生的SelectionKey , 並通過SelectionKey反向獲取SocketChannel的channel
- 根據得到的channel完成業務處理
七、NIO非阻塞網絡編程的快速入門
服務器端
public class NIOServer {
public static void main(String[] args) throws IOException {
// 創建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//創建一個Selector對象,
Selector selector = Selector.open();
// 綁定端口6666, 在服務器端監聽
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 設置爲非阻塞
serverSocketChannel.configureBlocking(false);
// 把serverSocketChannel註冊到selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 循環等待用戶連接
while (true){
if (selector.select(1000) == 0){ //等待(阻塞)一秒, 沒有事件發生
// if (selector.selectNow() == 0){ // 也可以設置成非阻塞的
System.out.println("服務器等待了一秒,無連接");
continue;
}
// 如果返回的>0 , 就獲取相關的selectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 返回關注事件的集合
// 遍歷selectionKeys
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
// 獲取到selectionKey
SelectionKey key = keyIterator.next();
//根據key對應的通道獲取事件並做相應處理
if (key.isAcceptable()){
//如果是OP_ACCEPT, 表示有新的客戶端產生
//給該客戶端生成SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//將socketChannnel設置爲非阻塞
socketChannel.configureBlocking(false);
//將socketChannel註冊到selector上, 設置事件爲OP_READ,同時給socketChannel關聯一個buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()){
// 發生了OP_READ
SocketChannel channel=(SocketChannel)key.channel();
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("from 客戶端"+new String(buffer.array()));
}
// 手動從集合中移除當前的selectionKey, 防止多線程情況下的重複操作
keyIterator.remove();
}
}
}
}
客戶端
public class NIOClient {
public static void main(String[] args) throws IOException {
// 得到一個網絡通道
SocketChannel socketChannel = SocketChannel.open();
// 設置爲非阻塞
socketChannel.configureBlocking(false);
//設置服務器端ip和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
if (!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
//如果沒有連接成功,客戶端是非阻塞的,可以做其它工作
System.out.println("等待連接...");
}
}
// 如果連接成功,就發送數據
String str = "hello world";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
// 發送數據 , 將buffer中的數據寫入到channel中
socketChannel.write(buffer);
System.in.read();
}
}