go语言学习笔记30------TCP通信

1.TCP通信过程

下图是一次TCP通讯的时序图。TCP连接建立断开。包含大家熟知的三次握手和四次握手。go语言学习笔记30------TCP通信_第1张图片
在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序。注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。

1.1三次握手

所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。
建立连接(三次握手)的过程:

1.客户端发送一个带SYN标志的TCP报文到服务器。这是上图中三次握手过程中的段1。客户端发出SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况。
另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。

mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。

2.服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。

服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。

3.客户必须再次回应服务器端一个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协议自动将发送缓冲区中的数据包重发。

1.2四次挥手

所谓四次挥手(Four-Way-Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务器任一方执行close来触发。
关闭连接(四次握手)的过程:
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

1.客户端发出段7,FIN位表示关闭连接的请求。
2.服务器发出段8,应答客户端的关闭连接请求。
3.服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
4.客户端发出段10,应答服务器的关闭连接请求。

建立连接的过程是三次握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。

1.3TCP状态转换

TCP状态图很多人都知道,它对排除和定位网络或系统故障时大有帮助。如果能熟练掌握这张图,了解图中的每一个状态,能大大提高我们对于TCP的理解和认识。下面对这张图的11种状态详细解析一下,以便加强记忆!不过在这之前,一定要熟练掌握TCP建立连接的三次握手过程,以及关闭连接的四次挥手过程。
go语言学习笔记30------TCP通信_第2张图片
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_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是:
FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了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过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。注意,TIME_WAIT状态一定出现在主动关闭这一方。

2.实现文件传输

2.1流程简析

借助TCP完成文件的传输,基本思路如下:
1:发送方(客户端)向服务端发送文件名,服务端保存该文件名。
2:接收方(服务端)向客户端返回一个消息ok,确认文件名保存成功。
3:发送方(客户端)收到消息后,开始向服务端发送文件数据。
4:接收方(服务端)读取文件内容,写入到之前保存好的文件中。
go语言学习笔记30------TCP通信_第3张图片

2.2发送端:

实现流程大致如下:
1.提示用户输入文件名。接收文件名path(含访问路径)
2.使用os.Stat()获取文件属性,得到纯文件名(去除访问路径)
3.主动连接服务器,结束时关闭连接
4.给接收端(服务器)发送文件名conn.Write()
5.读取接收端回发的确认数据conn.Read()
6.判断是否为“ok”。如果是,封装函数SendFile() 发送文件内容。传参path和conn
7.只读Open文件, 结束时Close文件
8.循环读文件,读到EOF终止文件读取
9.将读到的内容原封不动Write给接收端(服务器)
代码实现:

package main

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

func sendfile(path string, conn net.Conn) {
   f, _ := os.Open(path)
   defer f.Close()
   buf := make([]byte, 4096)
   for {
      n, err := f.Read(buf)
      if err != nil {
         if err == io.EOF {
            fmt.Println("读取文件结束")
            break
         } else {
            fmt.Println("read err", err)
            return
         }
      }
      conn.Write(buf[:n])
   }
}
func main() {
   fmt.Println("请输入需要传输的文件位置")
   var path string
   fmt.Scan(&path)
   fileinfo, err := os.Stat(path)
   if err != nil {
      fmt.Println("stat err", err)
      return
   }
   conn, err := net.Dial("tcp", "127.0.0.1")
   if err != nil {
      fmt.Println("dial err", err)
      return
   }
   defer conn.Close()
   conn.Write([]byte(fileinfo.Name()))
   buf := make([]byte, 4096)
   n, err := conn.Read(buf)
   if err != nil {
      fmt.Println("read err", err)
      return
   }
   if string(buf[:n]) == "ok" {
      sendfile(path, conn)
   }

}

2.3接收端

实现流程大致如下:
1.创建监听listener,程序结束时关闭。
2.阻塞等待客户端连接,程序结束时关闭conn。
3.读取客户端发送文件名。保存fileName。
4.回发“ok”给客户端做应答
5.封装函数 RecvFile接收客户端发送的文件内容。传参fileName 和conn
6.按文件名Create文件,结束时Close
7.循环Read客户端发送的文件内容,当读到EOF说明文件读取完毕。
8.将读到的内容原封不动Write到创建的文件中
代码实现:

package main

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

var path string

func main() {
   fmt.Println("请输入要存放的文件地址")
   fmt.Scan(&path)
   listen, err := net.Listen("tcp", "127.0.0.1:8009")
   if err != nil {
      fmt.Println("listen err", err)
      return
   }
   conn, err := listen.Accept()
   defer conn.Close()
   buf := make([]byte, 4096)
   n, err := conn.Read(buf)
   if err != nil {
      fmt.Println("read err", err)
      return
   }
   filename := string(buf[:n])
   conn.Write([]byte("ok"))
   Recefile(filename, conn)

}
func Recefile(filename string, conn net.Conn) {

   f, _ := os.Create(path + "/" + filename)
   defer f.Close()
   buf := make([]byte, 4096)
   for {
      n, err := conn.Read(buf)
      if n == 0 { // 判断是否读到文件末尾
         fmt.Println("读取文件结束") // 对端 Close
         break
      }
      if err != nil && err == io.EOF {
         fmt.Println("Read err:", err)
         return
      }
      f.Write(buf[:n])
   }
}

你可能感兴趣的:(Go语言与区块链)