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

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

猜你喜欢

转载自www.cnblogs.com/qhdsavoki/p/9568163.html