• 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() }

    提取文件 不带路径的“文件名”

  • 相关阅读:
    C#之类和对象
    uml中关联与依赖
    uml中的各个关系
    数据挖掘聚类算法分类(转)
    (转)Client http persistent connection limit
    牛客网NOIP赛前集训营提高组(第七场)Solution
    训练题表
    CF1000赛后总结
    UVA3983 Robotruck 题解
    CF1034A Enlarge GCD
  • 原文地址:https://www.cnblogs.com/qhdsavoki/p/9568163.html
Copyright © 2020-2023  润新知