Java NIO中,一個socket連接使用一個Channel(通道)來表示。對應到不同的網(wǎng)絡傳輸協(xié)議類型,在Java中都有不同的NIO Channel(通道) 相對應。其中最為重要的四種Channel(通道)實現(xiàn): FileChannel、 SocketChannel、 ServerSocketChannel、 DatagramChannel :
-
FileChannel
文件通道,用于文件的數(shù)據(jù)讀寫; (管文件的傳輸通道) -
SocketChannel
套接字通道,用于Socket套接字TCP連接的數(shù)據(jù)讀寫; (管TCP數(shù)據(jù)傳輸?shù)耐ǖ溃?/li> -
ServerSocketChannel
服務器套接字通道(或服務器監(jiān)聽通道),允許我們監(jiān)聽TCP連接請求,為每個監(jiān)聽到的請求,創(chuàng)建一個SocketChannel套接字通道; (只管與服務器 TCP連接 的通道) -
DatagramChannel
數(shù)據(jù)報通道,用于UDP協(xié)議的數(shù)據(jù)讀寫。 (管UDP數(shù)據(jù)傳輸?shù)耐ǖ?
我在學習Channel的時候,老是搞不清楚ServerSocketChannel
和SocketChannel
的關系,這次我不允許我的讀者也搞不清。用大白話講就是通道你可以理解為數(shù)據(jù)傳輸?shù)墓艿?,這個管道是雙向傳輸?shù)?,即既可以通過Channel向文件或者網(wǎng)絡客戶端寫數(shù)據(jù)也可以從文件或者網(wǎng)絡客戶端讀數(shù)據(jù)。如果你要讀取文件的數(shù)據(jù),使用FileChannel
;如果需要建立網(wǎng)絡連接,在服務器使用ServerSocketChannel
來作為客戶端連接請求的通道,也就是說它只負責服務器端的連接請求的數(shù)據(jù)傳輸。通過ServerSocketChannel
就可以和服務器建立連接,然后通過ServerSocketChannel
創(chuàng)建SocketChannel
通道進行TCP數(shù)據(jù)傳輸。下面分別介紹每一個通道的用法。
FileChannel文件通道
FileChannel是專門操作文件的通道。通過FileChannel,既可以從一個文件中讀取數(shù)據(jù),也可以將數(shù)據(jù)寫入到文件中。特別申明一下, FileChannel為阻塞模式,不能設置為非阻塞模式。不說你也知道,學習IO操作可以首先要獲取FileChannel通道 、然后讀取FileChannel通道中的數(shù)據(jù)或者將數(shù)據(jù)寫入FileChannel通道,然后關閉通道。最后補充一個就是強制將通道的數(shù)據(jù)刷盤到磁盤的方法即可,那么就按照上面的步驟開始吧!
獲取到FileChannel對象
獲取FileChannel對象有三種方式,第一種方式可以通過文件的輸入流、輸出流獲取FileChannel文件通道,代碼如下:
//創(chuàng)建一個文件輸入流
FileInputStream fis = new FileInputStream("word.txt");
//獲取文件流的通道,只能從通道中讀取數(shù)據(jù),不能寫入數(shù)據(jù)
FileChannel inChannel = fis.getChannel();
//創(chuàng)建一個文件輸出流
FileOutputStream fos = new FileOutputStream("word.txt");
//獲取文件流的通道,只能向通道中寫入數(shù)據(jù),不能讀取數(shù)據(jù)
FileChannel outchannel = fos.getChannel();
也可以通過RandomAccessFile文件隨機訪問類,獲取FileChannel文件通道實例,代碼如下:
// 創(chuàng)建 既可以寫也可以讀的隨機訪問類 RandomAccessFile 隨機訪問對象
// 參數(shù)"rw"表示可讀可寫,如果只讀可以給"r",只寫給"w"即可
RandomAccessFile rFile = new RandomAccessFile("word.txt", "rw");
//獲取文件流的通道(可讀可寫)
FileChannel channel = rFile.getChannel();
從FileChannel中讀取數(shù)據(jù)
下面給出標準的讀取數(shù)據(jù)的代碼,具體解釋在注釋中,代碼中channel.read(buffer)將通道的數(shù)據(jù)讀到緩沖區(qū)上,雖然是讀取通道的數(shù)據(jù),對于通道來說是讀取模式,但是對于ByteBuffer緩沖區(qū)來說則是寫入數(shù)據(jù),這時, ByteBuffer緩沖區(qū)處于寫入模式 ,而buffer.get()才是從通道讀取數(shù)據(jù),需要flip()切換讀模式:
try(FileChannel channel = new RandomAccessFile("word.txt", "rw").getChannel()){
// 準備緩沖區(qū),分配10字節(jié)的空間
ByteBuffer buffer = ByteBuffer.allocate(10);
int len = -1;
while ((len=channel.read(buffer))!=-1){ // 將channel中的數(shù)據(jù)讀取到緩存區(qū)中,返回讀到的數(shù)據(jù)長度,沒讀到數(shù)據(jù)返回-1
buffer.flip(); // 切換讀取模式,左右指針指向已存數(shù)據(jù)首位
while (buffer.hasRemaining()){// 如果position<limit,即還可以讀
byte b = buffer.get();//讀取字節(jié)流,讀指針向后移動一個位置,補充buffer.get(i)可以讀取指定坐標的字節(jié)
log.debug("讀取到的字節(jié):"+(char)b); // log.debug 可以換成 System.out.println
}
buffer.clear(); // 讀完了buffer,將buffer的指針重新回歸buffer首尾
//buffer.compact(); // 如果未讀完, 壓縮,將未讀的放在左邊
}
}catch (IOException e){
log.error("文件未找到");
}
上面將byte轉(zhuǎn)為char類型需要解釋一下:字節(jié)是8位,而char
是16位,因此在將字節(jié)轉(zhuǎn)換為char
時,只有低8位的數(shù)據(jù)被使用,高8位的數(shù)據(jù)被丟棄。這意味著字節(jié)的范圍[-128, 127]將被映射到char
的范圍[0, 255],只看整數(shù)部分相當于int類型轉(zhuǎn)為long類型,上轉(zhuǎn)型。如果字節(jié)表示的是ASCII字符,那么這種轉(zhuǎn)換通常是安全的,因為ASCII字符的范圍是0到127。因為wold.txt中的只有英文字符所以沒問題,有漢字不行。一般不會這樣使用,只是舉下例子!
輸出結(jié)果:輸出wold.txt中的字符:
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 讀取到的字節(jié):h
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 讀取到的字節(jié):e
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 讀取到的字節(jié):l
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 讀取到的字節(jié):l
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 讀取到的字節(jié):o
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 讀取到的字節(jié):
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 讀取到的字節(jié):w
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 讀取到的字節(jié):o
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 讀取到的字節(jié):r
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 讀取到的字節(jié):l
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 讀取到的字節(jié):d
向FileChannel通道中寫數(shù)據(jù)
寫入數(shù)據(jù)到通道,在大部分應用場景,都會調(diào)用通道的write(ByteBuffer)方法,此方法的參數(shù)是一個ByteBuffer緩沖區(qū)實例,是待寫數(shù)據(jù)的來源。write(ByteBuffer)方法的作用,是從ByteBuffer緩沖區(qū)中讀取數(shù)據(jù),然后寫入到通道自身,而返回值是寫入成功的字節(jié)數(shù)。如果 buffer 處于寫入模式(如剛寫完數(shù)據(jù)),需要 flip 翻轉(zhuǎn) buffer,使其變成讀取模式,代碼如下:
@Test
public void test2(){
// wrap 方法執(zhí)行完自動切換wrapBuffer為讀模式
ByteBuffer wrapBuffer = ByteBuffer.wrap("你好世界!".getBytes());
try(FileChannel channel = new RandomAccessFile("word.txt","rw").getChannel()){
int len = 0;
while ((len = channel.write(wrapBuffer))!=0){
System.out.println("已經(jīng)寫入字節(jié)數(shù)為:"+len); //已經(jīng)寫入字節(jié)數(shù)為:15
}
}catch (IOException e){
log.error("文件未找到");
}
}
// 關閉通道
channel.close();
//強制刷新到磁盤
channel.force(true);
注意,寫入數(shù)據(jù)會將word.txt原有的數(shù)據(jù)擦除!當通道使用完成后,必須將其關閉。關閉非常簡單,調(diào)用close( )方法即可 。如果在將緩沖數(shù)據(jù)寫入通道時,需要保證數(shù)據(jù)能立即寫入到磁盤,可以在寫入后調(diào)用一下FileChannel的force()方法。關于FileChannel通道需要掌握的大概就是上面這些,那么下面的內(nèi)容是作為開發(fā)中的補充內(nèi)容。
文件操作補充內(nèi)容
字符串與ByteBuffer
緩存的相互轉(zhuǎn)換
// 字符串轉(zhuǎn)為ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.put("hello world".getBytes());
debugAll(buffer);
buffer.flip(); //這種方式需要切換讀模式才可以
CharBuffer hw = StandardCharsets.UTF_8.decode(buffer);
System.out.println(hw.toString());
// 使用Charset類
ByteBuffer encodeBuffer = StandardCharsets.UTF_8.encode("hello");
debugAll(encodeBuffer);
CharBuffer decode = StandardCharsets.UTF_8.decode(encodeBuffer);
System.out.println(decode.toString());
// 使用wrap
ByteBuffer wrapBuffer = ByteBuffer.wrap("hello".getBytes());
debugAll(wrapBuffer);
通道與通道直接發(fā)送數(shù)據(jù)(零拷貝)
public static void main(String[] args) {
try(
FileChannel from = new FileInputStream("world.txt").getChannel();
FileChannel to = new FileOutputStream("to.txt").getChannel()
) {
long size = from.size();
for (long left = size; left >0 ; ) {
// 該方法每次最多傳輸2g的數(shù)據(jù)量
long n = from.transferTo((size-left), from.size(), to);
left -= n;
}
}catch (IOException ie){
ie.printStackTrace();
}
}
NIO
提供的關于File
的操作
遍歷目錄文件:
public class FileTest {
public static void main(String[] args) throws IOException {
// 訪問文件夾
visitorFile();
// 拷貝文件夾
copyFile();
// 刪除文件夾
deleteFile();
}
private static void copyFile() throws IOException {
String source = "C:\\Users\\cheney\\Documents\\CFSystem";
String target = "C:\\Users\\cheney\\Documents\\CFSystem_bak";
Files.walk(Paths.get(source)).forEach(path -> {
// 替換成新的路徑
String targetName = path.toString().replace(source, target);
try {
if(Files.isDirectory(path)){
Files.createDirectory(Paths.get(targetName));
}else if(Files.isRegularFile(path)){
Files.copy(path,Paths.get(targetName));
}
}catch (IOException e){
e.printStackTrace();
}
});
}
private static void deleteFile() throws IOException {
Files.walkFileTree(Paths.get("C:\\Users\\cheney\\Documents\\CFSystem_bak"),new SimpleFileVisitor<Path>(){
@Override
// 在訪問目錄之前被調(diào)用。你可以在這里執(zhí)行預處理操作。
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
// 進入文件夾時不能刪除文件夾,因為里面還有文件
System.out.println("進入------>"+dir);
return super.preVisitDirectory(dir, attrs);
}
@Override
// 在訪問某個目錄的文件時被調(diào)用。你可以在這里執(zhí)行對文件的操作。
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
// 刪除文件夾
System.out.println("刪除xxxxxx:"+file);
Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
// 在訪問文件失敗時被調(diào)用。例如,由于權限問題或其他原因,無法訪問文件。
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return super.visitFileFailed(file, exc);
}
@Override
// 在訪問目錄之后被調(diào)用。你可以在這里執(zhí)行后處理操作。
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
// 如果退出之前遍歷刪除過文件,那么可以刪除文件夾
System.out.println("退出<------"+dir);
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
}
private static void visitorFile() throws IOException {
AtomicInteger dirCount = new AtomicInteger();
// 遍歷文件夾,SimpleFileVisitor訪問者模式
Files.walkFileTree(Paths.get("C:\\Users\\cheney\\Documents\\CFSystem"),new SimpleFileVisitor<Path>(){
// 文件訪問之前的操作,即訪問到文件夾
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("==========>"+dir);
dirCount.incrementAndGet();
return super.preVisitDirectory(dir, attrs);
}
// 文件訪問時操作,即訪問到文件
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("==========>"+file);
return super.visitFile(file, attrs);
}
});
System.out.println("文件夾個數(shù):"+dirCount.get());
}
}
檢查文件是否存在
Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));
創(chuàng)建一級目錄
Path path = Paths.get("helloword/d1");
Files.createDirectory(path);
- 如果目錄已存在,會拋異常 FileAlreadyExistsException
- 不能一次創(chuàng)建多級目錄,否則會拋異常 NoSuchFileException
創(chuàng)建多級目錄用
Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);
拷貝文件
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");
Files.copy(source, target);
- 如果文件已存在,會拋異常 FileAlreadyExistsException
如果希望用 source 覆蓋掉 target,需要用 StandardCopyOption 來控制
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
移動文件
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
- StandardCopyOption.ATOMIC_MOVE 保證文件移動的原子性
刪除文件
Path target = Paths.get("helloword/target.txt");
Files.delete(target);
- 如果文件不存在,會拋異常 NoSuchFileException
刪除目錄
Path target = Paths.get("helloword/d1");
Files.delete(target);
- 如果目錄還有內(nèi)容,會拋異常 DirectoryNotEmptyException
SocketChannel和ServerSocketChannel套接字通道
很多人都搞不拎清這兩個通道的區(qū)別,它們都是涉及網(wǎng)絡連接的通道,SocketChannel負責連接的數(shù)據(jù)傳輸,另一個是ServerSocketChannel負責連接的監(jiān)聽。要想和服務器建立TCP通信,必須先連接服務器,而ServerSocketChannel就是符合客戶端連接請求的通道,只有三次握手連接完成了才可以使用SocketChannel進行通信。ServerSocketChannel僅僅應用于服務器端,而SocketChannel則同時處于服務器端和客戶端,所以,對應于一個連接,兩端都有一個負責傳輸?shù)腟ocketChannel傳輸通道。同樣下面講解將按照獲取通道、讀取通道數(shù)據(jù)、數(shù)據(jù)寫入到通道中、關閉通道等步驟介紹。
獲取SocketChannel傳輸通道
在客戶端,先通過SocketChannel靜態(tài)方法open()獲得一個套接字傳輸通道;然后,將socket套接字設置為非阻塞模式;最后,通過connect()實例方法,對服務器的IP和端口發(fā)起連接。
//獲得一個套接字傳輸通道
SocketChannel socketChannel = SocketChannel.open();
//設置為非阻塞模式
socketChannel.configureBlocking(false);
//對服務器的 IP 和端口發(fā)起連接
socketChannel.connect(new InetSocketAddress("127.0.0.1", 80));
在服務器端,需要在連接建立的事件到來時,服務器端的ServerSocketChannel能成功地查詢出這個新連接事件,并且通過調(diào)用服務器端ServerSocketChannel監(jiān)聽套接字的accept()方法,來獲取新連接的套接字通道:
//新連接事件到來,首先通過事件,獲取服務器監(jiān)聽通道,這個key如何來的后面Selector會介紹
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//獲取新連接的套接字通道
SocketChannel socketChannel = server.accept();
//設置為非阻塞模式
socketChannel.configureBlocking(false);
看見了沒,服務器端需要先使用ServerSocketChannel建立連接才能使用套接字傳輸通道!
讀取SocketChannel傳輸通道
當SocketChannel傳輸通道可讀時,可以從SocketChannel讀取數(shù)據(jù),具體方法與前面的文件通道讀取方法是相同的。調(diào)用read方法,將數(shù)據(jù)讀入緩沖區(qū)ByteBuffer。 這部分和前面文件傳輸通道FileChannel是一樣的,都是通過緩沖區(qū)從通道讀取和寫入數(shù)據(jù),如下:
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
在讀取時,因為是異步的,因此我們必須檢查read的返回值,以便判斷當前是否讀取到了數(shù)據(jù)。 read()方法的返回值是讀取的字節(jié)數(shù),如果返回-1,那么表示讀取到對方的輸出結(jié)束標志,對方已經(jīng)輸出結(jié)束,準備關閉連接。實際上,通過read方法讀數(shù)據(jù),本身是很簡單的,比較困難的是,在非阻塞模式下,如何知道通道何時是可讀的呢?這就需要用到NIO的新組件——Selector通道選擇器,稍后介紹。
向SocketChannel傳輸通道寫入數(shù)據(jù)
//寫入前需要讀取緩沖區(qū),要求 ByteBuffer 是讀取模式
buffer.flip();
socketChannel.write(buffer);
關閉通道
//調(diào)用終止輸出方法,向?qū)Ψ桨l(fā)送一個輸出的結(jié)束標志
socketChannel.shutdownOutput();
//關閉套接字連接
IOUtil.closeQuietly(socketChannel);
DatagramChannel數(shù)據(jù)報通道
在Java中使用UDP協(xié)議傳輸數(shù)據(jù),比TCP協(xié)議更加簡單。和Socket套接字的TCP傳輸協(xié)議不同, UDP協(xié)議不是面向連接的協(xié)議。使用UDP協(xié)議時,只要知道服務器的IP和端口,就可以直接向?qū)Ψ桨l(fā)送數(shù)據(jù)。在Java NIO中,使用DatagramChannel數(shù)據(jù)報通道來處理UDP協(xié)議的數(shù)據(jù)傳輸。
獲取DatagramChannel數(shù)據(jù)報通道
//獲取 DatagramChannel 數(shù)據(jù)報通道
DatagramChannel channel = DatagramChannel.open();
//設置為非阻塞模式
datagramChannel.configureBlocking(false);
如果需要接收數(shù)據(jù),還需要調(diào)用bind方法綁定一個數(shù)據(jù)報的監(jiān)聽端口,具體如下://調(diào)用 bind 方法綁定一個數(shù)據(jù)報的監(jiān)聽端口```
channel.socket().bind(new InetSocketAddress(18080));
讀取DatagramChannel數(shù)據(jù)報通道數(shù)據(jù)
當DatagramChannel通道可讀時,可以從DatagramChannel讀取數(shù)據(jù)。和前面的SocketChannel讀取方式不同,這里不調(diào)用read方法,而是調(diào)用receive(ByteBufferbuf)方法將數(shù)據(jù)從DatagramChannel讀入,再寫入到ByteBuffer緩沖區(qū)中。通道讀取receive(ByteBufferbuf)方法雖然讀取了數(shù)據(jù)到buf緩沖區(qū),但是其返回值是SocketAddress類型,表示返回發(fā)送端的連接地址(包括IP和端口)。通過receive方法讀取數(shù)據(jù)非常簡單,但是,在非阻塞模式下,如何知道DatagramChannel通道何時是可讀的呢?和SocketChannel一樣,同樣需要用到NIO的新組件—Selector通道選擇器,稍后介紹。
//創(chuàng)建緩沖區(qū)
ByteBuffer buf = ByteBuffer.allocate(1024);
//從 DatagramChannel 讀入,再寫入到 ByteBuffer 緩沖區(qū)
SocketAddress clientAddr= datagramChannel.receive(buf);
寫入DatagramChannel數(shù)據(jù)報通道
向DatagramChannel發(fā)送數(shù)據(jù),和向SocketChannel通道發(fā)送數(shù)據(jù)的方法也是不同的。這里不是調(diào)用write方法,而是調(diào)用send方法。由于UDP是面向非連接的協(xié)議,因此,在調(diào)用send方法發(fā)送數(shù)據(jù)的時候,需要指定接收方的地址(IP和端口)。 示例代碼如下:文章來源:http://www.zghlxwxcb.cn/news/detail-809213.html
//把緩沖區(qū)翻轉(zhuǎn)到讀取模式
buffer.flip();
//調(diào)用 send 方法,把數(shù)據(jù)發(fā)送到目標 IP+端口
dChannel.send(buffer, new InetSocketAddress("127.0.0.1",18899));
//清空緩沖區(qū),切換到寫入模式
buffer.clear();
//簡單關閉即可
dChannel.close();
至此,幾種通道基本用法就介紹完畢了,如果不過癮是因為沒有結(jié)合Selector來講,結(jié)合Selector才是最知識盛宴。在Selector中將結(jié)合Channel和Buffer全面進行介紹。文章來源地址http://www.zghlxwxcb.cn/news/detail-809213.html
到了這里,關于Java-NIO篇章(3)——Channel通道類詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!