TCP協議,UDP,以及TCP通信服務器的文件傳輸


TCP通信過程

下圖是一次TCP通訊的時序圖。TCP連接建立斷開。包含大家熟知的三次握手和四次握手。

在這個例子中,首先客戶端主動發起連接、發送請求,然后服務器端響應請求,然后客戶端主動關閉連接。兩條豎線表示通訊的兩端,從上到下表示時間的先后順序。注意,數據從一端傳到網絡的另一端也需要時間,所以圖中的箭頭都是斜的。

三次握手 建立連接

建立連接(三次握手)的過程:

  1. 客戶端發送一個帶SYN標志的TCP報文到服務器。這是上圖中三次握手過程中的段1。客戶端發出SYN位表示連接請求。序號是1000,這個序號在網絡通訊中用作臨時的地址,每發一個數據字節,這個序號要加1,這樣在接收端可以根據序號排出數據包的正確順序,也可以發現丟包的情況。

另外,規定SYN位和FIN位也要占一個序號,這次雖然沒發數據,但是由於發了SYN位,因此下次再發送應該用序號1001

mss表示最大段尺寸,如果一個段太大,封裝成幀后超過了鏈路層的最大長度,就必須在IP層分片,為了避免這種情況,客戶端聲明自己的最大段尺寸,建議服務器端發來的段不要超過這個長度。

  1. 服務器端回應客戶端,是三次握手中的第2個報文段,同時帶ACK標志和SYN標志。表示對剛才客戶端SYN的回應;同時又發送SYN給客戶端,詢問客戶端是否准備好進行數據通訊。

服務器發出段2,也帶有SYN位,同時置ACK位表示確認,確認序號是1001,表示“我接收到序號1000及其以前所有的段,請你下次發送序號為1001的段”,也就是應答了客戶端的連接請求,同時也給客戶端發出一個連接請求,同時聲明最大尺寸為1024

  1. 客戶必須再次回應服務器端一個ACK報文,這是報文段3

客戶端發出段3,對服務器的連接請求進行應答,確認序號是8001。在這個過程中,客戶端和服務器分別給對方發了連接請求,也應答了對方的連接請求,其中服務器的請求和應答在一個段中發出。

因此一共有三個段用於建立連接,稱為“三方握手”。在建立連接的同時,雙方協商了一些信息,例如,雙方發送序號的初始值、最大段尺寸等。

數據傳輸的過程:

  1. 客戶端發出段4,包含從序號1001開始的20個字節數據。
  2. 服務器發出段5,確認序號為1021,對序號為1001-1020的數據表示確認收到,同時請求發送序號1021開始的數據,服務器在應答的同時也向客戶端發送從序號8001開始的10個字節數據。
  3. 客戶端發出段6,對服務器發來的序號為8001-8010的數據表示確認收到,請求發送序號8011開始的數據。

在數據傳輸過程中,ACK和確認序號是非常重要的,應用程序交給TCP協議發送的數據會暫存在TCP層的發送緩沖區中,發出數據包給對方之后,只有收到對方應答的ACK段才知道該數據包確實發到了對方,可以從發送緩沖區中釋放掉了,如果因為網絡故障丟失了數據包或者丟失了對方發回的ACK段,經過等待超時后TCP協議自動將發送緩沖區中的數據包重發。

總結:

3次握手:
1、主動: 發送 SYN 標志位。

2、被動:接收 SYN、同時回復 ACK 並且發送SYN

3、主動: 發送 ACK 標志位。 ―――――― Accpet() / Dial()

四次揮手

關閉連接(四次握手)的過程:

由於TCP連接是全雙工的,因此每個方向都必須單獨進行關閉。這原則是當一方完成它的數據發送任務后就能發送一個FIN來終止這個方向的連接。收到一個 FIN只意味着這一方向上沒有數據流動,一個TCP連接在收到一個FIN后仍能發送數據。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。

  1. 客戶端發出段7FIN位表示關閉連接的請求。
  2. 服務器發出段8,應答客戶端的關閉連接請求。
  3. 服務器發出段9,其中也包含FIN位,向客戶端發送關閉連接請求。
  4. 客戶端發出段10,應答服務器的關閉連接請求。

建立連接的過程是三次握手,而關閉連接通常需要4個段,服務器的應答和關閉連接請求通常不合並在一個段中,因為有連接半關閉的情況,這種情況下客戶端關閉連接之后就不能再發送數據給服務器了,但是服務器還可以發送數據給客戶端,直到服務器也關閉連接為止。

總結:

4次揮手:
1、主動關閉連接:發送 FIN 標志位。

2、被動關閉連接:接收 FIN、同時回復 ACK ―― 半關閉完成。

3、被動關閉連接:發送 FIN 標志位。

4、主動關閉連接:接收 FIN、同時回復 ACK ―― Close()/Close() ―― 4次揮手完成。

TCP狀態轉換

TCP狀態圖很多人都知道,它對排除和定位網絡或系統故障時大有幫助。如果能熟練掌握這張圖,了解圖中的每一個狀態,能大大提高我們對於TCP的理解和認識。下面對這張圖的11種狀態詳細解析一下,以便加強記憶!不過在這之前,一定要熟練掌握TCP建立連接的三次握手過程,以及關閉連接的四次揮手過程。

CLOSED表示初始狀態。

LISTEN該狀態表示服務器端的某個SOCKET處於監聽狀態,可以接受連接。

SYN_SENT這個狀態與SYN_RCVD遙相呼應,當客戶端SOCKET執行CONNECT連接時,它首先發送SYN報文,隨即進入到了SYN_SENT狀態,並等待服務端的發送三次握手中的第2個報文。SYN_SENT狀態表示客戶端已發送SYN報文。

SYN_RCVD: 該狀態表示接收到SYN報文,在正常情況下,這個狀態是服務器端的SOCKET在建立TCP連接時的三次握手會話過程中的一個中間狀態,很短暫。此種狀態時,當收到客戶端的ACK報文后,會進入到ESTABLISHED狀態。

ESTABLISHED表示連接已經建立。

FIN_WAIT_1:  FIN_WAIT_1FIN_WAIT_2狀態的真正含義都是表示等待對方的FIN報文。區別是:

FIN_WAIT_1狀態是當socketESTABLISHED狀態時,想主動關閉連接,向對方發送了FIN報文,此時該socket進入到FIN_WAIT_1狀態。

FIN_WAIT_2狀態是當對方回應ACK后,該socket進入到FIN_WAIT_2狀態,正常情況下,對方應馬上回應ACK報文,所以FIN_WAIT_1狀態一般較難見到,而FIN_WAIT_2狀態可用netstat看到。

FIN_WAIT_2主動關閉鏈接的一方,發出FIN收到ACK以后進入該狀態。稱之為半連接或半關閉狀態。該狀態下的socket只能接收數據,不能發。

TIME_WAIT: 表示收到了對方的FIN報文,並發送出了ACK報文,等2MSL后即可回到CLOSED可用狀態。如果FIN_WAIT_1狀態下,收到對方同時帶 FIN標志和ACK標志的報文時,可以直接進入到TIME_WAIT狀態,而無須經過FIN_WAIT_2狀態。

CLOSING: 這種狀態較特殊,屬於一種較罕見的狀態。正常情況下,當你發送FIN報文后,按理來說是應該先收到(或同時收到)對方的 ACK報文,再收到對方的FIN報文。但是CLOSING狀態表示你發送FIN報文后,並沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文。什么情況下會出現此種情況呢?如果雙方幾乎在同時close一個SOCKET的話,那么就出現了雙方同時發送FIN報文的情況,也即會出現CLOSING狀態,表示雙方都正在關閉SOCKET連接。

CLOSE_WAIT: 此種狀態表示在等待關閉。當對方關閉一個SOCKET后發送FIN報文給自己,系統會回應一個ACK報文給對方,此時則進入到CLOSE_WAIT狀態。接下來呢,察看是否還有數據發送給對方,如果沒有可以 close這個SOCKET,發送FIN報文給對方,即關閉連接。所以在CLOSE_WAIT狀態下,需要關閉連接。

LAST_ACK: 該狀態是被動關閉一方在發送FIN報文后,最后等待對方的ACK報文。當收到ACK報文后,即可以進入到CLOSED可用狀態。

2MSL (Maximum Segment Lifetime) 和與之對應的TIME_WAIT狀態,可以4次握手關閉流程更加可靠。4次握手的最后一個ACK是是由主動關閉方發送出去的,若這個ACK丟失,被動關閉方會再次發一個FIN過來。若主動關閉方能夠保持一個2MSLTIME_WAIT狀態,則有更大的機會讓丟失的ACK被再次發送出去。注意,TIME_WAIT狀態一定出現在主動關閉這一方

總結:

TCP狀態轉換:

1. 主動端:

CLOSE --> SYN --> SYN_SEND狀態 --> ESTABLISHED狀態(數據通信期間處於的狀態) ---> FIN --> FIN_WAIT_1狀態。

---> 接收 ACK ---> FIN_WAIT_2狀態 (半關閉―― 只出現在主動端) ---> 接收FIN、回ACK ――> TIME_WAIT (等2MSL)

---> 確保最后一個ACK能被對端收到。(只出現在主動端)
2. 被動端:

CLOSE --> LISTEN ---> ESTABLISHED狀態(數據通信期間處於的狀態) ---> 接收 FIN、回復ACK -->

CLOSE_WAIT(對應 對端處於 半關閉) --> 發送FIN --> LAST_ACK ---> 接收ACK ---> CLOSE

查看狀態命令:

windows:netstat -an | findstr 8001(端口號)

Linux: netstat -an | grep 8001

UDP通信

UDP服務器

由於UDP是“無連接”的,所以,服務器端不需要額外創建監聽套接字,只需要指定好IPport,然后監聽該地址,等待客戶端與之建立連接,即可通信。

創建監聽地址:
func ResolveUDPAddr(network, address string) (*UDPAddr, error) 
創建用戶通信的socket:
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error) 
接收udp數據:
func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)
寫出數據到udp:
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)

服務端完整代碼實現如下:

UDP簡單服務器:

1. 獲取 服務器的 UDP地址結構體 srvAddr := ResolveUDPAddr(“udp”,“IP+port”)

2. 創建 用於數據通信套接字。 conn := ListenUDP("udp", srvAddr )

3. 讀取客戶端發送數據。 n, cltAddr, err := conn.ReadFromUDP(buf)

4. 回寫數據給客戶端。 conn.WriteToUDP("數據內容", cltAddr )

package main

import (
   "fmt"
   "net"
)

func main() {
   //創建監聽的地址,並且指定udp協議
   udp_addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8002")
   if err != nil {
      fmt.Println("ResolveUDPAddr err:", err)
      return
   }
   conn, err := net.ListenUDP("udp", udp_addr)    //創建數據通信socket
   if err != nil {
      fmt.Println("ListenUDP err:", err)
      return
   }
   defer conn.Close()

   buf := make([]byte, 1024)
   n, raddr, err := conn.ReadFromUDP(buf)        //接收客戶端發送過來的數據,填充到切片buf中。
   if err != nil {
      return
   }
   fmt.Println("客戶端發送:", string(buf[:n]))

   _, err = conn.WriteToUDP([]byte("nice to see u in udp"), raddr) // 向客戶端發送數據
   if err != nil {
      fmt.Println("WriteToUDP err:", err)
      return
   }
}
View Code

UDP客戶端

udp客戶端的編寫與TCP客戶端的編寫,基本上是一樣的,只是將協議換成udp。注意只能使用小寫。

UDP客戶端:

與TCP通信客戶端實現手法一致。

net.Dial("udp", server 的IP+port)

代碼如下:

package main

import (
   "net"
   "fmt"
)

func main() {
   conn, err := net.Dial("udp", "127.0.0.1:8002") 
   if err != nil {
      fmt.Println("net.Dial err:", err)
      return
   }
   defer conn.Close()

   conn.Write([]byte("Hello! I'm client in UDP!"))

   buf := make([]byte, 1024)
   n, err1 := conn.Read(buf)
   if err1 != nil {
      return
   }
   fmt.Println("服務器發來:", string(buf[:n]))
}
View Code

並發

其實對於UDP而言,服務器不需要並發,只要循環處理客戶端數據即可。客戶端也等同於TCP通信並發的客戶端。

UDP並發服務器: ―――― UDP 默認支持並發。

1. 獲取 服務器的 UDP地址結構體 srvAddr := ResolveUDPAddr(“udp”,“IP+port”)

2. 創建 用於數據通信套接字。 conn := ListenUDP("udp", srvAddr )

3. for 循環 讀取客戶端發送的數據 for {
n, cltAddr, err := conn.ReadFromUDP(buf)
}

4. 創建 go 程 完成 寫操作,提高程序的並行效率。

go func() {
conn.WriteToUDP("數據內容", cltAddr )
}()

5.由於UDP沒有建立連接過程。所以 TCP 通信狀態 對於 UDP 無效。

服務器:

package main

import (
   "net"
   "fmt"
)

func main() {
   // 創建 服務器 UDP 地址結構。指定 IP + port
   laddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8003")
   if err != nil {
      fmt.Println("ResolveUDPAddr err:", err)
      return
   }
   // 監聽 客戶端連接
   conn, err := net.ListenUDP("udp", laddr)
   if err != nil {
      fmt.Println("net.ListenUDP err:", err)
      return
   }
   defer conn.Close()

   for {
      buf := make([]byte, 1024)
      n, raddr, err := conn.ReadFromUDP(buf)
      if err != nil {
         fmt.Println("conn.ReadFromUDP err:", err)
         return
      }
      fmt.Printf("接收到客戶端[%s]:%s", raddr, string(buf[:n]))

      conn.WriteToUDP([]byte("I-AM-SERVER"), raddr) // 簡單回寫數據給客戶端
   }
}
View Code

客戶端:

UDP並發客戶端:

並發讀取 鍵盤 和 conn。 編碼實現參考 TCP 並發客戶端實現。

修改內容: net.Dial("udp", server 的IP+port)

package main

import (
   "net"
   "os"
   "fmt"
)

func main() {
   conn, err := net.Dial("udp", "127.0.0.1:8003")
   if err != nil {
      fmt.Println("net.Dial err:", err)
      return
   }
   defer conn.Close()
   go func() {
      str := make([]byte, 1024)
      for {
         n, err := os.Stdin.Read(str) //從鍵盤讀取內容, 放在str
         if err != nil {
            fmt.Println("os.Stdin. err1 = ", err)
            return
         }
         conn.Write(str[:n])       // 給服務器發送
      }
   }()
   buf := make([]byte, 1024)
   for {
      n, err := conn.Read(buf)
      if err != nil {
         fmt.Println("conn.Read err:", err)
         return
      }
      fmt.Println("服務器寫來:", string(buf[:n]))
   }
}
View Code

                                                        UDPTCP的差異

TCP

UDP

面向連接

面向無連接

要求系統資源較多

要求系統資源較少

TCP程序結構較復雜

UDP程序結構較簡單

使用流式

使用數據包式

保證數據准確性

不保證數據准確性

保證數據順序

不保證數據順序

通訊速度較慢

通訊速度較快

文件傳輸

網絡文件傳輸:思路

發送端:(client)

1. 建立連接請求 net.Dial() ――> conn defer conn.Close()

2. 通過命令行參數,提取 文件名(帶路徑) os.Args

3. 獲取文件屬性 ,提取 文件名(不帶路徑)os.Stat()

4. 發送文件名 給 接收端 conn.Write

5. 接收對端回發的數據,確認是否是“ok”

6. 發送文件內容 給 接收端。封裝 sendFile(文件名, conn) 函數

1) 只讀方式打開 待發送文件

2) 創建 buf 讀文件,存入buf中

3) 借助 conn 寫 buf中的 數據到 接收端 ―― 讀多少、寫多少。

4) 判斷文件讀取、發送完畢。結束 conn 。斷開連接。

接收端:(sever)

1. 創建監聽套接字 listener := net.Listen()

2. 阻塞等待客戶端連接請求。 conn = listener.Accept()

3. 讀取發送端發送的文件名(不含路徑)-- 保存

4. 回復“ok”給發送端。

5. 接收文件內容,保存成一個新文件。封裝 RecvFile (文件名, conn) 函數

1) os.Create() 按文件名創建文件。 -- f

2) 從 conn 中讀取文件內容。

3) 使用 f 寫到本地新建文件中。 ―― 讀多少、寫多少

4) 判斷文件讀取完畢。結束 conn 。斷開連接。

首先獲取文件名。借助os包中的stat()函數來獲取文件屬性信息。在函數返回的文件屬性中包含文件名和文件大小。Stat參數name傳入的是文件訪問的絕對路徑。FileInfo中的Name()函數可以將文件名單獨提取出來。

func Stat(name string) (FileInfo, error)

type FileInfo interface {
   Name() string       

   Size() int64        

   Mode() FileMode     
   ModTime() time.Time
   IsDir() bool        
   Sys() interface{}   
}

獲取文件屬性示例:

package main

import (
   "os"
   "fmt"
)

func main()  {
   list := os.Args                        // 獲取命令行參數,存入list中
   if len(list) != 2 {            // 確保用戶輸入了一個命令行參數
      fmt.Println("格式為:xxx.go 文件名")
      return
   }
   fileName := list[1]                   // 從命令行保存文件名(含路徑)

   fileInfo, err := os.Stat(fileName)    //根據文件名獲取文件屬性信息 fileInfo
   if err != nil {
      fmt.Println("os.Stat err:", err)
      return
   }
   fmt.Println("文件name為:", fileInfo.Name())   // 得到文件名(不含路徑)
   fmt.Println("文件size為:", fileInfo.Size())   // 得到文件大小。單位字節
}
View Code

客戶端實現:

package main

import (
   "fmt"
   "os"
   "net"
   "io"
)

func SendFile(path string, conn net.Conn)  {
   // 以只讀方式打開文件
   f, err := os.Open(path)
   if err != nil {
      fmt.Println("os.Open err:", err)
      return
   }
   defer f.Close()                   // 發送結束關閉文件。

   // 循環讀取文件,原封不動的寫給服務器
   buf := make([]byte, 4096)
   for {
      n, err := f.Read(buf)        // 讀取文件內容到切片緩沖中
      if err != nil {
         if err == io.EOF {
            fmt.Println("文件發送完畢")
         } else {
            fmt.Println("f.Read err:", err)
         }
         return
      }
      conn.Write(buf[:n])  // 原封不動寫給服務器
   }
}

func main()  {
   // 提示輸入文件名
   fmt.Println("請輸入需要傳輸的文件:")
   var path string
   fmt.Scan(&path)

   // 獲取文件名   fileInfo.Name()
   fileInfo, err := os.Stat(path)
   if err != nil {
      fmt.Println("os.Stat err:", err)
      return
   }

   // 主動連接服務器
   conn, err := net.Dial("tcp", "127.0.0.1:8005")
   if err != nil {
      fmt.Println("net.Dial err:", err)
      return
   }
   defer conn.Close()

   // 給接收端,先發送文件名
   _, err = conn.Write([]byte(fileInfo.Name()))
   if err != nil {
      fmt.Println("conn.Write err:", err)
      return
   }

   // 讀取接收端回發確認數據 —— ok
   buf := make([]byte, 1024)
   n, err := conn.Read(buf)
   if err != nil {
      fmt.Println("conn.Read err:", err)
      return
   }

   // 判斷如果是ok,則發送文件內容
   if "ok" == string(buf[:n]) {
      SendFile(path, conn)   // 封裝函數讀文件,發送給服務器,需要path、conn
   }
}
客戶端
package main
import (
    "net"
    "fmt"
    "os"
    "io"
)
func filesend(filepath string,conn net.Conn){
    buf:=make([]byte,4096)
    f1,err:=os.OpenFile(filepath,os.O_RDONLY,0666)
    if err!=nil{
        fmt.Println("打開文件錯誤",err)
        return
    }
    defer f1.Close()
    for {
        n, err := f1.Read(buf)
        if err != nil {
            if err ==io.EOF{
                fmt.Println("讀取完畢")
                break
            }else{
            fmt.Println("read err", err)
            return
            }
        }
        _, err = conn.Write(buf[:n])
        if err != nil {
            if err==io.EOF{
                fmt.Println("文件發送完畢")
                break
            }
            fmt.Println("發送err", err)
            return
        }
    }
}
func main() {
    list:=os.Args
    filepath:=list[1]
    fileinfo,err:=os.Stat(filepath)
    if err!=nil{
        fmt.Println("stat err",err)
        return
    }
    str:=fileinfo.Name()
    //fmt.Println(str)
    buf:=make([]byte,4096)
    conn,err:=net.Dial("tcp","127.0.0.1:8000")
    if err!=nil{
        fmt.Println("conn err",err)
        return
    }
    defer conn.Close()
    n,err:=conn.Write([]byte(str))
    if err!=nil{
        fmt.Println("write err",err)
        return
    }
    fmt.Printf("發送的文件名%q",string(buf[:n]))
    //buf2:=make([]byte,4096)
    n,err=conn.Read(buf)
    if err!=nil{
        fmt.Println("服務器發來錯誤",err)
        return
    }
    if string(buf[:n])=="ok"{
        fmt.Println("服務器接收成功")
        filesend(filepath,conn)
    }
}
自己的思路

服務端實現:

package main

import (
   "net"
   "fmt"
   "os"
   "io"
)

func RecvFile(fileName string, conn net.Conn)  {
   // 創建新文件
   f, err := os.Create(fileName)
   if err != nil {
      fmt.Println("Create err:", err)
      return
   }
   defer f.Close()

   // 接收客戶端發送文件內容,原封不動寫入文件
   buf := make([]byte, 4096)
   for {
      n, err := conn.Read(buf)
      if err != nil {
         if err == io.EOF {
            fmt.Println("文件接收完畢")
         } else {
            fmt.Println("Read err:", err)
         }
         return
      }
      f.Write(buf[:n])   // 寫入文件,讀多少寫多少
   }
}

func main()  {
   // 創建監聽
   listener, err := net.Listen("tcp", "127.0.0.1:8005")
   if err != nil {
      fmt.Println("Listen err:", err)
      return
   }
   defer listener.Close()

   // 阻塞等待客戶端連接
   conn, err := listener.Accept()
   if err != nil {
      fmt.Println("Accept err:", err)
      return
   }
   defer conn.Close()

   // 讀取客戶端發送的文件名
   buf := make([]byte, 1024)
   n, err := conn.Read(buf)
   if err != nil {
      fmt.Println("Read err:", err)
      return
   }
   fileName := string(buf[:n])       // 保存文件名

   // 回復 0k 給發送端
   conn.Write([]byte("ok"))

   // 接收文件內容
   RecvFile(fileName, conn)      // 封裝函數接收文件內容, 傳fileName 和 conn
}
服務端
package main
import (
    "net"
    "fmt"
    "os"
    "io"
)
func main() {
    listener, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Println("listener err", err)
        return
    }
    defer listener.Close()
    conn, err := listener.Accept()
    if err != nil {
        fmt.Println("conn err", err)
        return
    }
    defer conn.Close()
    buf := make([]byte, 4096)
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Println("read err", )
        return
    }
    pathname := string(buf[:n])
    fmt.Println(pathname)
    _, err = conn.Write([]byte("ok"))
    if err != nil {
        fmt.Println("write err", err)
        return
    }
    recvfile(pathname,conn)

}
func recvfile(pathname string,conn net.Conn){
    str:="D:/1/"+pathname
    fmt.Println(str)
    f1,err:=os.Create(str)
    if err!=nil{
        fmt.Println("create err",err)
        return
    }
    defer f1.Close()
    buf:=make([]byte,4096)
    for {
        n,err:=conn.Read(buf)
        if err!=nil{
            if err==io.EOF{
                fmt.Println("文件接收完畢")
                break
            }
            fmt.Println("conn read err",err)
            break
        }
        f1.Write(buf[:n])
    }


}
自己的思路

小知識

獲取命令行參數:

os.Args 提取命令行參數,保存成 []string

使用格式: go run xxx.go arg1 arg2 arg3 arg4 ...

獲取命令行參數:

arg[0]: xxx.go ――> xxx.exe 的絕對路徑

arg[1]: arg1
arg[2]: arg2
arg[3]: arg3
....
獲取文件屬性:

os.Stat(文件訪問絕對路徑) ――> fileInfo interface { Name() Size() }

提取文件 不帶路徑的“文件名”


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM