Java文件的簡(jiǎn)單讀寫(xiě)、隨機(jī)讀寫(xiě)、NIO讀寫(xiě)與怎么使用MappedByteBuffer讀寫(xiě)

這篇文章主要介紹“Java文件的簡(jiǎn)單讀寫(xiě)、隨機(jī)讀寫(xiě)、NIO讀寫(xiě)與怎么使用MappedByteBuffer讀寫(xiě)”,在日常操作中,相信很多人在Java文件的簡(jiǎn)單讀寫(xiě)、隨機(jī)讀寫(xiě)、NIO讀寫(xiě)與怎么使用MappedByteBuffer讀寫(xiě)問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”Java文件的簡(jiǎn)單讀寫(xiě)、隨機(jī)讀寫(xiě)、NIO讀寫(xiě)與怎么使用MappedByteBuffer讀寫(xiě)”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

成都網(wǎng)站制作、做網(wǎng)站、外貿(mào)營(yíng)銷(xiāo)網(wǎng)站建設(shè)的開(kāi)發(fā),更需要了解用戶,從用戶角度來(lái)建設(shè)網(wǎng)站,獲得較好的用戶體驗(yàn)。成都創(chuàng)新互聯(lián)多年互聯(lián)網(wǎng)經(jīng)驗(yàn),見(jiàn)的多,溝通容易、能幫助客戶提出的運(yùn)營(yíng)建議。作為成都一家網(wǎng)絡(luò)公司,打造的就是網(wǎng)站建設(shè)產(chǎn)品直銷(xiāo)的概念。選擇成都創(chuàng)新互聯(lián),不只是建站,我們把建站作為產(chǎn)品,不斷的更新、完善,讓每位來(lái)訪用戶感受到浩方產(chǎn)品的價(jià)值服務(wù)。

簡(jiǎn)單文件讀寫(xiě)

FileOutputStream

由于流是單向的,簡(jiǎn)單文件寫(xiě)可使用FileOutputStream,而讀文件則使用FileInputStream。

任何數(shù)據(jù)輸出到文件都是以字節(jié)為單位輸出,包括圖片、音頻、視頻。以圖片為例,如果沒(méi)有圖片格式解析器,那么圖片文件其實(shí)存儲(chǔ)的就只是按某種格式存儲(chǔ)的字節(jié)數(shù)據(jù)罷了。

FileOutputStream指文件字節(jié)輸出流,用于將字節(jié)數(shù)據(jù)輸出到文件,僅支持順序?qū)懭搿⒅С忠宰芳臃绞綄?xiě)入,但不支持在指定位置寫(xiě)入。

打開(kāi)一個(gè)文件輸出流并寫(xiě)入數(shù)據(jù)的示例代碼如下。

public class FileOutputStreamStu{     public void testWrite(byte[] data) throws IOException {                             try(FileOutputStream fos = new FileOutputStream("/tmp/test.file",true)) {             fos.write(data);             fos.flush();         }     } }

注意,如果不指定追加方式打開(kāi)流,new  FileOutputStream時(shí)會(huì)導(dǎo)致文件內(nèi)容被清空,而FileOutputStream的默認(rèn)構(gòu)建函數(shù)是以非追加模式打開(kāi)流的。

FileOutputStream的參數(shù)1為文件名,參數(shù)2為是否以追加模式打開(kāi)流,如果為true,則字節(jié)將寫(xiě)入文件的尾部而不是開(kāi)頭。

調(diào)用flush方法目的是在流關(guān)閉之前清空緩沖區(qū)數(shù)據(jù),實(shí)際上使用FileOutputStream并不需要調(diào)用flush方法,此處的刷盤(pán)指的是將緩存在JVM內(nèi)存中的數(shù)據(jù)調(diào)用系統(tǒng)函數(shù)write寫(xiě)入。如BufferedOutputStream,在調(diào)用BufferedOutputStream方法時(shí),如果緩存未滿,實(shí)際上是不會(huì)調(diào)用系統(tǒng)函數(shù)write的,如下代碼所示。

public class BufferedOutputStream extends FilterOutputStream {     public synchronized void write(byte b[], int off, int len) throws IOException {         if (len >= buf.length) {             flushBuffer();             out.write(b, off, len);             return;         }         if (len > buf.length - count) {             flushBuffer();         }         System.arraycopy(b, off, buf, count, len); // 只寫(xiě)入緩存         count += len;     } }

FileInputStream

FileInputStream指文件字節(jié)輸入流,用于將文件中的字節(jié)數(shù)據(jù)讀取到內(nèi)存中,僅支持順序讀取,不可跳躍讀取。

打開(kāi)一個(gè)文件輸入流讀取數(shù)據(jù)的案例代碼如下。

public class FileInputStreamStu{     public void testRead() throws IOException {             try (FileInputStream fis = new FileInputStream("/tmp/test/test.log")) {             byte[] buf = new byte[1024];             int realReadLength = fis.read(buf);         }     } }

其中buf數(shù)組中下標(biāo)從0到realReadLength的字節(jié)數(shù)據(jù)就是實(shí)際讀取的數(shù)據(jù),如果realReadLength返回-1,則說(shuō)明已經(jīng)讀取到文件尾并且未讀取到任何數(shù)據(jù)。

當(dāng)然,我們還可以一個(gè)字節(jié)一個(gè)字節(jié)的讀取,如下代碼所示。

public class FileInputStreamStu{     public void testRead() throws IOException {              try (FileInputStream fis = new FileInputStream("/tmp/test/test.log")) {             int byteData = fis.read(); // 返回值取值范圍:[-1,255]             if (byteData == -1) {                 return; // 讀取到文件尾了             }             byte data = (byte) byteData;             // data為讀取到的字節(jié)數(shù)據(jù)         }     } }

至于讀取到的字節(jié)數(shù)據(jù)如何使用就需要看你文件中存儲(chǔ)的是什么數(shù)據(jù)了。

如果整個(gè)文件存儲(chǔ)的是一張圖片,那么需要將整個(gè)文件讀取完,再按格式解析成圖片,而如果整個(gè)文件是配置文件,則可以一行一行讀取,遇到\n換行符則為一行,代碼如下。

public class FileInputStreamStu{     @Test     public void testRead() throws IOException {         try (FileInputStream fis = new FileInputStream("/tmp/test/test.log")) {             ByteBuffer buffer = ByteBuffer.allocate(1024);             int byteData;             while ((byteData = fis.read()) != -1) {                 if (byteData == '\n') {                     buffer.flip();                     String line = new String(buffer.array(), buffer.position(), buffer.limit());                     System.out.println(line);                     buffer.clear();                     continue;                 }                 buffer.put((byte) byteData);             }         }     } }

Java基于InputStream、OutputStream還提供了很多的API方便讀寫(xiě)文件,如BufferedReader,但如果懶得去記這些API的話,只需要記住FileInputStream與FileOutputStream就夠了。

隨機(jī)訪問(wèn)文件讀寫(xiě)

RandomAccessFile相當(dāng)于是FileInputStream與FileOutputStream的封裝結(jié)合,即可以讀也可以寫(xiě),并且RandomAccessFile支持移動(dòng)到文件指定位置處開(kāi)始讀或?qū)憽?/p>

RandomAccessFile的使用如下。

public class RandomAccessFileStu{     public void testRandomWrite(long index,long offset){         try (RandomAccessFile randomAccessFile = new RandomAccessFile("/tmp/test.idx", "rw")) {             randomAccessFile.seek(index * indexLength());             randomAccessFile.write(toByte(index));             randomAccessFile.write(toByte(offset));         }     } }

RandomAccessFile構(gòu)建方法:參數(shù)1為文件路徑,參數(shù)2為模式,'r'為讀,'w'為寫(xiě);

seek方法:在linux、unix操作系統(tǒng)下就是調(diào)用系統(tǒng)的lseek函數(shù)。

RandomAccessFile的seek方法通過(guò)調(diào)用native方法實(shí)現(xiàn),源碼如下。

JNIEXPORT void JNICALL Java_java_io_RandomAccessFile_seek0(JNIEnv *env,                     jobject this, jlong pos) {     FD fd;     fd = GET_FD(this, raf_fd);     if (fd == -1) {         JNU_ThrowIOException(env, "Stream Closed");         return;     }     if (pos < jlong_zero) {         JNU_ThrowIOException(env, "Negative seek offset");     }     // #define IO_Lseek lseek     else if (IO_Lseek(fd, pos, SEEK_SET) == -1) {         JNU_ThrowIOExceptionWithLastError(env, "Seek failed");     } }

Java_java_io_RandomAccessFile_seek0函數(shù)的參數(shù)1表示RandomAccessFile對(duì)象,參數(shù)2表示偏移量。函數(shù)中調(diào)用的IO_Lseek方法實(shí)際是操作系統(tǒng)的lseek方法。

RandomAccessFile提供的讀、寫(xiě)、指定偏移量其實(shí)都是通過(guò)調(diào)用操作系統(tǒng)函數(shù)完成的,包括前面介紹的文件輸入流和文件輸出流也不例外。

NIO文件讀寫(xiě)-FileChannel

Channel(通道)表示IO源與目標(biāo)打開(kāi)的連接,Channel類(lèi)似于傳統(tǒng)的流,但Channel本身不能直接訪問(wèn)數(shù)據(jù),只能與Buffer進(jìn)行交互。Channel(通道)主要用于傳輸數(shù)據(jù),從緩沖區(qū)的一側(cè)傳到另一側(cè)的實(shí)體(如File、Socket),支持雙向傳遞。

正如SocketChannel是客戶端與服務(wù)端通信的通道,F(xiàn)ileChannel就是我們讀寫(xiě)文件的通道。FileChannel是線程安全的,也就是一個(gè)FileChannel可以被多個(gè)線程使用。對(duì)于多線程操作,同時(shí)只會(huì)有一個(gè)線程能對(duì)該通道所在文件進(jìn)行修改。如果需要確保多線程的寫(xiě)入順序,就必須要轉(zhuǎn)為隊(duì)列寫(xiě)入。

FileChannel可通過(guò)FileOutputStream、FileInputStream、RandomAccessFile獲取,也可以通過(guò)FileChannel#open方法打開(kāi)一個(gè)通道。

以通過(guò)FileOutputStream獲取FileChannel為例,通過(guò)FileOutputStream或RandomAccessFile獲取FileChannel方法相同,代碼如下。

public class FileChannelStu{     public void testGetFileCahnnel(){         try(FileOutputStream fos = new FileOutputStream("/tmp/test.log");             FileChannel fileChannel = fos.getChannel()){            // do....            }catch (IOException exception){         }     } }

需要注意,通過(guò)FileOutputStream獲取的FileChannel只能執(zhí)行寫(xiě)操作,通過(guò)FileInputStream獲取的FileChannel只能執(zhí)行讀操作,原因可查看getChannel方法源碼。

通過(guò)FileOutputStream或FileInputStream或RandomAccessFile打開(kāi)的FileChannel,在流關(guān)閉時(shí)也會(huì)被關(guān)閉,可查看這幾個(gè)類(lèi)的close方法源碼。

若想要獲取一個(gè)同時(shí)支持讀和寫(xiě)的FileChannel需要通過(guò)open方法打開(kāi),代碼如下。

public class FileChannelStu{     public void testOpenFileCahnnel(){         FileChannel channel = FileChannel.open(                             Paths.get(URI.create("file:" + rootPath + "/" + postion.fileName)),                             StandardOpenOption.READ,StandardOpenOption.WRITE);         // do....         channel.close();     } }

open方法第二個(gè)變長(zhǎng)參數(shù)傳StandardOpenOption.READ和StandardOpenOption.WRITE即可打開(kāi)一個(gè)雙向讀寫(xiě)的通道。

FileChannel允許對(duì)文件加鎖,文件鎖是進(jìn)程級(jí)別的,不是線程級(jí)別的,文件鎖可以解決多個(gè)進(jìn)程并發(fā)訪問(wèn)、修改同一個(gè)文件的問(wèn)題。文件鎖會(huì)被當(dāng)前進(jìn)程持有,一旦獲取到文件鎖就要調(diào)用一次release釋放鎖,當(dāng)關(guān)閉對(duì)應(yīng)的FileChannel對(duì)象時(shí)或當(dāng)前JVM進(jìn)程退出時(shí),鎖也會(huì)自動(dòng)被釋鎖。

文件鎖的使用案例代碼如下。

public class FileChannelStu{     public void testFileLock(){         FileChannel channel = this.channel;         FileLock fileLock = null;         try {             fileLock = channel.lock();// 獲取文件鎖             // 執(zhí)行寫(xiě)操作             channel.write(...);             channel.write(...);         } finally {             if (fileLock != null) {                 fileLock.release(); // 釋放文件鎖             }         }     } }

當(dāng)然,只要我們能確保同時(shí)只有一個(gè)進(jìn)程對(duì)文件執(zhí)行寫(xiě)操作,那么就不需要鎖文件。RocketMQ也并沒(méi)有使用文件鎖,因?yàn)槊總€(gè)Broker有自己數(shù)據(jù)目錄,即使一臺(tái)機(jī)器上部署多個(gè)Broker也不會(huì)有多個(gè)進(jìn)程對(duì)同一個(gè)日記文件操作的情況。

上面例子去掉文件鎖后代碼如下。

public class FileChannelStu{     public void testWrite(){         FileChannel channel = this.channel;         channel.write(...);         channel.write(...);     } }

這里還存在一個(gè)問(wèn)題,就是并發(fā)寫(xiě)數(shù)據(jù)問(wèn)題。雖然FileChannel是線程安全的,但兩次write并不是原子性操作,如果要確保兩次write是連續(xù)寫(xiě)入的,還必須要加鎖。在RocketMQ中,通過(guò)引用計(jì)數(shù)器替代了鎖。

FileChannel提供的force方法用于刷盤(pán),即調(diào)用操作系統(tǒng)的fsync函數(shù),使用如下。

public class FileChannelStu{     public void closeChannel(){         this.channel.force(true);         this.channel.close();     }         }

force方法的參數(shù)表示除強(qiáng)制寫(xiě)入內(nèi)容更改外,文件元數(shù)據(jù)的更改是否也強(qiáng)制寫(xiě)入。后面使用MappedByteBuffer時(shí),可直接使用MappedByteBuffer的force方法。

FileChannel的force方法最終調(diào)用的C方法源碼如下:

JNIEXPORT jint JNICALL Java_sun_nio_ch_FileDispatcherImpl_force0(JNIEnv *env, jobject this,                                           jobject fdo, jboolean md) {     jint fd = fdval(env, fdo);     int result = 0;     if (md == JNI_FALSE) {         result = fdatasync(fd);     } else {         result = fsync(fd);     }     return handle(env, result, "Force failed"); }

參數(shù)md對(duì)應(yīng)調(diào)用force方法傳遞的metaData參數(shù)。

使用FileChannel支持seek(position)到指定位置讀或?qū)憯?shù)據(jù),代碼如下。

public class FileChannelStu{     public void testSeekWrite(){         FileChannel channel = this.channel;         synchronized (channel) {              channel.position(100);             channel.write(ByteBuffer.wrap(toByte(index)));             channel.write(ByteBuffer.wrap(toByte(offset)));         }     } }

上述例子的作用是將指針移動(dòng)到物理偏移量100byte位置處,順序?qū)懭雐ndex和offset。讀取同理,代碼如下。

public class FileChannelStu{     public void testSeekRead(){         FileChannel channel = this.channel;         synchronized (channel) {              channel.position(100);             ByteBuffer buffer = ByteBuffer.allocate(16);             int realReadLength = channel.read(buffer);              if(realReadLength==16){                 long index = buffer.getLong();                 long offset = buffer.getLong();             }         }     } }

其中read方法返回的是實(shí)際讀取的字節(jié)數(shù),如果返回-1則代表已經(jīng)是文件尾部了,沒(méi)有剩余內(nèi)容可讀取。

使用MappedByteBuffer讀寫(xiě)文件

MappedByteBuffer是Java提供的基于操作系統(tǒng)虛擬內(nèi)存映射(MMAP)技術(shù)的文件讀寫(xiě)API,底層不再通過(guò)read、write、seek等系統(tǒng)調(diào)用實(shí)現(xiàn)文件的讀寫(xiě)。

我們需要通過(guò)FileChannel#map方法將文件的一個(gè)區(qū)域映射到內(nèi)存中,代碼如下。

public class MappedByteBufferStu{   @Test   public void testMappedByteBuffer() throws IOException {       FileChannel fileChannel = FileChannel.open(Paths.get(URI.create("file:/tmp/test/test.log")),                 StandardOpenOption.WRITE, StandardOpenOption.READ);       MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);       fileChannel.close();       mappedByteBuffer.position(1024);       mappedByteBuffer.putLong(10000L);       mappedByteBuffer.force();       } }

上面代碼的功能是通過(guò)FileChannel將文件[0~4096)區(qū)域映射到內(nèi)存中,調(diào)用FileChannel的map方法返回MappedByteBuffer,在映射之后關(guān)閉通道,隨后在指定位置處寫(xiě)入一個(gè)8字節(jié)的long類(lèi)型整數(shù),最后調(diào)用force方法將寫(xiě)入數(shù)據(jù)從內(nèi)存寫(xiě)回磁盤(pán)(刷盤(pán))。

映射一旦建立了,就不依賴于用于創(chuàng)建它的文件通道,因此在創(chuàng)建MappedByteBuffer之后我們就可以關(guān)閉通道了,對(duì)映射的有效性沒(méi)有影響。

實(shí)際上將文件映射到內(nèi)存比通過(guò)read、write系統(tǒng)調(diào)用方法讀取或?qū)懭霂资甂B的數(shù)據(jù)要昂貴,從性能的角度來(lái)看,MappedByteBuffer適合用于將大文件映射到內(nèi)存中,如上百M(fèi)、上GB的大文件。

FileChannel的map方法有三個(gè)參數(shù):

  • MapMode:映射模式,可取值有READ_ONLY(只讀映射)、READ_WRITE(讀寫(xiě)映射)、PRIVATE(私有映射),READ_ONLY只支持讀,READ_WRITE支持讀寫(xiě),而PRIVATE只支持在內(nèi)存中修改,不會(huì)寫(xiě)回磁盤(pán);

  • position和size:映射區(qū)域,可以是整個(gè)文件,也可以是文件的某一部分,單位為字節(jié)。

需要注意的是,如果FileChannel是只讀模式,那么map方法的映射模式就不能指定為READ_WRITE。如果文件是剛剛創(chuàng)建的,只要映射成功,文件的大小就會(huì)變成(0+position+size)。

通過(guò)MappedByteBuffer讀取數(shù)據(jù)示例如下:

public class MappedByteBufferStu{     @Test     public void testMappedByteBufferOnlyRead() throws IOException {         FileChannel fileChannel = FileChannel.open(Paths.get(URI.create("file:/tmp/test/test.log")),                     StandardOpenOption.READ);         MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, 4096);         fileChannel.close();         mappedByteBuffer.position(1024);         long value = mappedByteBuffer.getLong();         System.out.println(value);     } }

mmap繞過(guò)了read、write系統(tǒng)函數(shù)調(diào)用,繞過(guò)了一次數(shù)據(jù)從內(nèi)核空間到用戶空間的拷貝,即實(shí)現(xiàn)零拷貝,MappedByteBuffer使用直接內(nèi)存而非JVM的堆內(nèi)存。

mmap只是在虛擬內(nèi)存分配了地址空間,只有在第一次訪問(wèn)虛擬內(nèi)存的時(shí)候才分配物理內(nèi)存。在mmap之后,并沒(méi)有將文件內(nèi)容加載到物理頁(yè)上,而是在虛擬內(nèi)存中分配地址空間,當(dāng)進(jìn)程在訪問(wèn)這段地址時(shí),通過(guò)查找頁(yè)表,發(fā)現(xiàn)虛擬內(nèi)存對(duì)應(yīng)的頁(yè)沒(méi)有在物理內(nèi)存中緩存則產(chǎn)生缺頁(yè)中斷,由內(nèi)核的缺頁(yè)異常處理程序處理,將文件對(duì)應(yīng)內(nèi)容以頁(yè)為單位(4096)加載到物理內(nèi)存中。

由于物理內(nèi)存是有限的,mmap在寫(xiě)入數(shù)據(jù)超過(guò)物理內(nèi)存時(shí),操作系統(tǒng)會(huì)進(jìn)行頁(yè)置換,根據(jù)淘汰算法,將需要淘汰的頁(yè)置換成所需的新頁(yè),所以mmap對(duì)應(yīng)的內(nèi)存是可以被淘汰的,被淘汰的內(nèi)存頁(yè)如果是臟頁(yè)(有過(guò)寫(xiě)操作修改頁(yè)內(nèi)容),則操作系統(tǒng)會(huì)先將數(shù)據(jù)回寫(xiě)磁盤(pán)再淘汰該頁(yè)。

數(shù)據(jù)寫(xiě)過(guò)程如下:

  • 1.將需要寫(xiě)入的數(shù)據(jù)寫(xiě)到對(duì)應(yīng)的虛擬內(nèi)存地址;

  • 2.若對(duì)應(yīng)的虛擬內(nèi)存地址未對(duì)應(yīng)物理內(nèi)存,則產(chǎn)生缺頁(yè)中斷,由內(nèi)核加載頁(yè)數(shù)據(jù)到物理內(nèi)存;

  • 3.數(shù)據(jù)被寫(xiě)入到虛擬內(nèi)存對(duì)應(yīng)的物理內(nèi)存;

  • 4.在發(fā)生頁(yè)淘汰或刷盤(pán)時(shí)由操作系統(tǒng)將臟頁(yè)回寫(xiě)到磁盤(pán)。

RocketMQ正是利用MappedByteBuffer實(shí)現(xiàn)索引文件的讀寫(xiě),實(shí)現(xiàn)一個(gè)基于文件系統(tǒng)的HashMap。

RocketMQ在創(chuàng)建新的CommitLog文件并通過(guò)FileChannel獲取MappedByteBuffer時(shí)會(huì)做一次預(yù)熱操作,即每個(gè)虛擬內(nèi)存頁(yè)(Page  Cache)都寫(xiě)入四個(gè)字節(jié)的0x00,并強(qiáng)制刷盤(pán)將數(shù)據(jù)寫(xiě)到文件中。這個(gè)動(dòng)作的用處是通過(guò)讀寫(xiě)操作把MMAP映射全部加載到物理內(nèi)存中。并且在預(yù)熱之后還做了一個(gè)鎖住內(nèi)存的操作,這是為了避免磁盤(pán)交換,防止操作系統(tǒng)把預(yù)熱過(guò)的頁(yè)臨時(shí)保存到swap區(qū),防止程序再次讀取交換出去的數(shù)據(jù)頁(yè)時(shí)產(chǎn)生缺頁(yè)中斷。

到此,關(guān)于“Java文件的簡(jiǎn)單讀寫(xiě)、隨機(jī)讀寫(xiě)、NIO讀寫(xiě)與怎么使用MappedByteBuffer讀寫(xiě)”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

網(wǎng)頁(yè)標(biāo)題:Java文件的簡(jiǎn)單讀寫(xiě)、隨機(jī)讀寫(xiě)、NIO讀寫(xiě)與怎么使用MappedByteBuffer讀寫(xiě)
當(dāng)前路徑:http://muchs.cn/article22/jpihjc.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供品牌網(wǎng)站建設(shè)、用戶體驗(yàn)、面包屑導(dǎo)航外貿(mào)建站、定制開(kāi)發(fā)移動(dòng)網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

成都app開(kāi)發(fā)公司