JAVA NIO(二)基礎 記憶體管理 檔案鎖定 Socket伺服器用戶端通訊,niosocket
NIO簡介
nio的包主要有四個,分別是
1. 緩衝區包:java.nio 出現版本:Java SE1.4
2. 通道包:java.nio.channels 出現版本:Java SE1.4
3. 字元集包:java.nio.charset 出現版本:Java SE1.4
4. 檔案處理包:java nio.file 出現版本:Java SE1.7
a)通道資料的進出,都要經過緩衝區
b)通道是一種新的原生I/O抽象概念,好比串連兩端資料的管道,用於資料的互動
c)字元集包:大多數字元集的集合,處理位元組字元之間相互轉換
d)檔案包:處理目錄和檔案。包含io中File類功能,比之更加強大,更具有名字等價的意義
1.緩衝Buffer
所有的基礎資料型別 (Elementary Data Type)都有相應的緩衝器(布爾型除外),但位元組是作業系統及其 I/O裝置使用的基礎資料型別 (Elementary Data Type),所以唯一與通道互動的緩衝器是ByteBuffer。後面再講字元集轉換時會用到CharBuffer
Buffer基礎
有幾個標誌位,用於操作緩衝區
容量(capacity):緩衝區大小(byte),讀出、寫入值都不會變
位置(position):下一個位元組被讀出寫入的位置,注意:position永遠小於limit
界限(limit) :寫入時(界限=容量),讀出時(上次寫入時position的位置)
標記(mark) :初始化為-1,調用reset()可回到標記位置
public static void main(String[] args) {// 初始方法化一:直接用封裝的數組作為緩衝區,不再分配空間ByteBuffer temp = ByteBuffer.wrap("位元組數組".getBytes());// 初始方法化二,初始化一個容量大小為10的 緩衝區。注意: 初始化時,為寫入狀態ByteBuffer bb = ByteBuffer.allocate(10);bb.capacity();// 容量:10bb.position();// 位置:0bb.limit();// 界限:10// 寫入位元組bb.put("345".getBytes());// position = 3// 倒回 執行position = 0 ; mark = -1bb.rewind();// 寫入位元組bb.put("012".getBytes());// position = 3// 做個標記(mark)bb.mark();// mark = position = 3;// 繼續寫入bb.put("678".getBytes());// position = 6// 重設:回到記號處bb.reset();// position = mark = 3// 繼續寫入bb.put("345".getBytes());// position = 6// 切換到寫入狀態,調用下面方法bb.flip();// 執行:limit = position = 6 ; position = 0 ; mark = -1;// position=0 可以從頭開始讀出資料。因為bb中只有6個位元組,所以被設定讀出的界限為6//剩下可讀取的位元組數bb.remaining();while (bb.hasRemaining()) {// 是否還有沒有讀取的位元組// 讀出1個位元組bb.get();}// position = 6// 繼續從頭讀bb.rewind();// position = 0// 設定position = 3bb.position(3);System.out.println((char) bb.get()); // 輸出3// 包含索引的get(),不會改變position的值System.out.println((char) bb.get(1)); // 輸出1// position = 4// 建立一個唯讀緩衝區,兩個緩衝區共用資料元素ByteBuffer readOnly = bb.asReadOnlyBuffer();// 複製一個緩衝區,兩個緩衝共用資料元素,有各自的位置、標記等。// 如果原始的緩衝區為唯讀,或者為直接緩衝區,新的緩衝區將繼承這些屬性ByteBuffer duplicate = bb.duplicate();// 建立一個從原始緩衝區的當前位置開始的新緩衝區// 其容量是原始緩衝區的剩餘元素數量(limit-position)ByteBuffer slice = bb.slice();// 判斷緩衝區是否是唯讀readOnly.isReadOnly(); // true// 切換到寫入狀態bb.clear();// position = 0 ; limit=10}
間接緩衝區:
ByteBuffer bb = ByteBuffer.allocate(1024);CharBuffer cb = CharBuffer.allocate(1024);// 對緩衝區的修改會影響到wrap的數組ByteBuffer bbw = ByteBuffer.wrap("wrap".getBytes());CharBuffer cbw = CharBuffer.wrap("aaa");//還有其他非boolean的基本類型可以這樣建立
直接緩衝區:
// 建立一個直接緩衝區,只有位元組緩衝區有這個Factory 方法ByteBuffer bb = ByteBuffer.allocateDirect(1024);// 構建一個通道FileChannel fc = new FileInputStream(new File("檔案路徑")).getChannel();// 將資料從磁碟讀到直接緩衝區fc.read(bb);
記憶體對應檔:
測試一下3種緩衝記憶體使用量情況
通道channel
FileChannel
public static void main(String[] args) throws IOException {File file = new File("D:\\TEXT.txt");// 只能用於讀的通道FileChannel readCh = new FileInputStream(file).getChannel();// 只能用於寫的通道FileChannel writCh = new FileOutputStream(file).getChannel();// 可以讀也可以寫的通道FileChannel rwCh = new RandomAccessFile(file, "rw").getChannel();// 在Java SE1.7中提供了新的初始化方法// 這個Path後面會介紹Path path = Paths.get("D:", "TEXT.txt");// 只能用於讀的通道FileChannel nReadCh = FileChannel.open(path, StandardOpenOption.READ);// 只能用於寫的通道FileChannel nWriteCh = FileChannel.open(path, StandardOpenOption.WRITE);// 可以讀也可以寫的通道,第二個參數可以是個數組FileChannel nReadWrite = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.READ);}
5. 基本方法
public static void main(String[] args) throws IOException {File file = new File("D:\\t2.txt");// 如果檔案不存,就會直接建立一個空檔案FileChannel rw = new RandomAccessFile(file, "rw").getChannel();// 初始化一個緩衝區ByteBuffer bb = ByteBuffer.wrap("temp".getBytes());// 注意 通道也有一個讀寫的位置,而且是從底層的檔案描述符獲得的// 這也就意味著一個對象對該position的更新可以被另一個對象看到rw.position();// 向通道道寫入資料while (bb.hasRemaining()) {// position放在末尾,write會自動對檔案進行擴容rw.write(bb);}// 先清空緩衝區bb.clear();// 通道的讀寫檔案位置在末尾,設定到0位置rw.position(0);// 讀資料到緩衝區rw.read(bb);// 通道關聯檔案的大小(位元組)rw.size();// 截斷檔案,只保留前三個位元組,其他刪掉rw.truncate(3);// 所有的現代檔案系統都會快取資料和延遲磁碟檔案更新以提高效能。調用force()方法要求檔案的所有待定修改立即同步到磁碟// boolean參數設定 中繼資料 是否要寫到磁碟// 中繼資料:指檔案所有者、存取權限、最後一次修改時間等資訊// 同步中繼資料要求作業系統至少一次的I/O操作,為了提高效能,可以不同步中繼資料,同時也不會犧牲資料完整性rw.force(false);// 關閉通道if (rw.isOpen())rw.close();}
6. Channel-to-Channel傳輸
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // 這裡僅列出部分API public abstract long transferTo (long position, long count, WritableByteChannel target) public abstract long transferFrom (ReadableByteChannel src, long position, long count)}
transferTo()和transferFrom()方法允許將一個通道交叉串連到另一個通道,而不需要通過一個中間緩衝區來傳遞資料。只有FileChannel類有這兩個方法,因此Channel-to-Channel傳輸中通道之一必須是FileChannel。不能在socket通道之間直接傳輸資料,不過socket通道實現WritableByteChannel和ReadableByteChannel介面,因此檔案的內容可以用transferTo()方法傳輸給一個socket通道,或者也可以用transferFrom()方法將資料從一個socket通道直接讀取到一個檔案中。
public static void main(String[] args) throws IOException {// 將兩個通道相連傳輸資料File inFile = new File("D:" + File.separator + "temp.png");File outFile = new File("E:" + File.separator + "temp.png");FileChannel in = new FileInputStream(inFile).getChannel();FileChannel out = new FileOutputStream(outFile).getChannel();// 將inFile檔案資料拷貝到outFileout.transferFrom(in, 0, in.size());in.transferTo(0, in.size(), out);}
7. 通道可以向緩衝區數組寫入資料,並按順序填充每個緩衝區直到所有緩衝區滿或者沒有資料可讀為止。聚集寫也是以類似的方式完成,資料從列表中的每個緩衝區中順序取出來發送到通道就好像順序寫入一樣
檔案鎖定
socket通道
SocketChannel和DatagramChannel都實現了讀寫功能的介面,而ServerSocketChannel並沒有實現。ServerSocketChannel只負責監聽傳入的串連和建立SocketChannel對象。
真正的就緒選擇必須由作業系統來做。作業系統的一項最重要的功能就是處理I/O請求並通知各個線程它們的資料已經準備好了。選取器類提供了這種抽象,使用Java代碼能夠以可移植的方式,請求底層的作業系統提供就緒選擇服務。
可以只用一個線程監控通道的就緒狀態並使用一個協調好的背景工作執行緒池來處理共接收到的資料。根據部署的條件,線程池的大小是可以調整的(或者它自己進行動態調整)。
package i.io.socket;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.CharBuffer;import java.nio.channels.ClosedChannelException;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;import java.util.Iterator;public class SelectSockets {/** * 伺服器端 */public static class ServerSocketListen implements Runnable {@Overridepublic void run() {try {server(1234);} catch (Exception e) {e.printStackTrace();}}public void server(int... port) throws Exception {// 初始化一個選取器Selector selector = Selector.open();// 監聽多個連接埠for (int pt : port) {System.out.println("Listening on port " + pt);// 初始化一個伺服器通訊端通道ServerSocketChannel serverChannel = ServerSocketChannel.open();// 設定連接埠伺服器通道監聽的連接埠serverChannel.bind(new InetSocketAddress(pt));// 設定監聽通訊端的非阻塞模式serverChannel.configureBlocking(false);// 註冊ServerSocketChannel選取器serverChannel.register(selector, SelectionKey.OP_ACCEPT);}while (true) {// 這個可能阻塞很長時間,返回後選擇集包含準備好的通道鍵if (selector.select() == 0)continue;// 處理準備好的通道handleChannel(selector);}}/** * 註冊通道和通道感興趣的業務到選取器 */protected void handleChannel(Selector selector) throws Exception {// 得到選擇鍵的迭代器Iterator<SelectionKey> it = selector.selectedKeys().iterator();while (it.hasNext()) {SelectionKey key = it.next();// 有新的串連if (key.isAcceptable()) {// 得到伺服器通道ServerSocketChannel server = (ServerSocketChannel) key.channel();SocketChannel channel = server.accept();// 設定通道為非阻塞channel.configureBlocking(false);channel.register(selector, SelectionKey.OP_READ);}// 有可讀取資料的通道if (key.isReadable()) {readDataFromSocket(key);}it.remove();}}/** * 讀取通道的資料 */protected void readDataFromSocket(SelectionKey key) throws Exception {SocketChannel socketChannel = (SocketChannel) key.channel();// 初始化緩衝區ByteBuffer buffer = ByteBuffer.allocate(1024);// 將資料讀到緩衝區while (socketChannel.read(buffer) > 0) {// 切換到緩衝區到讀模式buffer.flip();// 以字元視圖開啟緩衝CharBuffer cb = buffer.asCharBuffer();StringBuilder messClient = new StringBuilder();while (cb.hasRemaining()) {messClient.append(cb.get());}System.err.println("2-伺服器端接收用戶端資料:" + messClient.toString());buffer.clear();}// 回寫System.err.println("3-反饋資料到用戶端:go");Charset charset = Charset.forName("gbk");socketChannel.write(charset.encode("還可以吧"));}}/** * 用戶端類 */public static class ClientSocketListen {public void client() throws IOException {Selector selector = Selector.open();SocketChannel sc = SocketChannel.open();sc.configureBlocking(false);sc.register(selector, SelectionKey.OP_CONNECT);sc.connect(new InetSocketAddress(1234));while (true) {if (selector.select() == 0)continue;handleChannel(selector);}}protected void handleChannel(Selector selector) throws ClosedChannelException, IOException {Iterator<SelectionKey> it = selector.selectedKeys().iterator();while (it.hasNext()) {SelectionKey key = it.next();int op = key.readyOps();SocketChannel channel = (SocketChannel) key.channel();// 監聽這個通道,用於接收伺服器端的反饋資料if ((op & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT) {if (channel.finishConnect()) {//一個選取器只能有一個當前通道的執行個體,// channel.register(selector, SelectionKey.OP_READ);key.interestOps(SelectionKey.OP_READ);System.out.println("1-用戶端發資料到伺服器:go");ByteBuffer bb = ByteBuffer.allocate(8);bb.asCharBuffer().put("4個字元");channel.write(bb);bb.clear();}}// 接收伺服器端反饋資料if ((op & SelectionKey.OP_READ) == SelectionKey.OP_READ) {ByteBuffer bb = ByteBuffer.allocate(1024);while (channel.read(bb) > 0) {bb.flip();Charset charset = Charset.forName("gbk");System.out.println("4-用戶端接收伺服器反饋資料:" + charset.decode(bb));}}it.remove();}}}public static void main(String[] argv) throws Exception {// 先用一個線程啟動伺服器端new Thread(new SelectSockets.ServerSocketListen()).start();// 用戶端調用new SelectSockets.ClientSocketListen().client();}}
DatagramChannel是面向UDP的,DatagramChannel對象既可以充當伺服器(監聽者)也可以充當用戶端(寄件者), 不同於SocketChannel(必須串連了才有用並且只能串連一次),DatagramChannel對象可以任意次數地進行串連或中斷連線。每次串連都可以到一個不同的遠程地址。調用disconnect()方法可以配置通道,以便它能再次接收來自安全管理器(如果已安裝)所允許的任意遠程地址的資料或發送資料到這些地址上。
列出幾種使用資料包的情況
程式可以承受資料丟失或無序的資料。
希望「發射後不管」(fire and forget)而不需要知道您發送的包是否已接收。
資料輸送量比可靠性更重要。
您需要同時發送資料給多個接受者(多播或者廣播)。
包隱喻比流隱喻更適合手邊的任務。