Go語言TCP協定

2020-07-16 10:05:20
TCP 是機器與機器間傳輸資訊的基礎協定,本節我們就來為大家介紹一下 TCP 協定。

TCP 協定簡介

TCP 傳輸控制協定(Transmission Control Protocol)是一種面向連線的、可靠的、基於位元組流的傳輸層通訊協定,TCP 協定主要是為了在不可靠的網際網路絡上提供可靠的端到端位元組流而專門設計的一個傳輸協定。

網際網路絡與單個網路有很大的不同,因為網際網路絡的不同部分可能有截然不同的拓撲結構、頻寬、延遲、封包大小和其他引數。TCP 的設計目標是能夠動態地適應網際網路絡的這些特性,而且具備面對各種故障時的健壯性。

不同主機的應用層之間經常需要可靠的、像管道一樣的連線,但是 IP 層不提供這樣的流機制,而是提供不可靠的包交換。

應用層向 TCP 層傳送用於網間傳輸的、用 8 位位元組表示的資料流,然後 TCP 把資料流分割區成適當長度的報文段(通常受該計算機連線的網路的資料鏈路層的最大傳輸單元(MTU)的限制)。之後 TCP 把結果包傳給 IP 層,由它來通過網路將包傳送給接收端實體的 TCP 層。

TCP 為了保證不發生丟包,就給每個包一個序號,同時序號也保證了傳送到接收端實體的包的按序接收。然後接收端實體對已成功收到的包發回一個相應的確認(ACK),如果傳送端實體在合理的往返時延(RTT)內未收到確認,那麼對應的封包就被假設為已丟失將會被進行重傳。TCP 用一個校驗和函數來檢驗資料是否有錯誤,在傳送和接收時都要計算校驗和。

每台支援 TCP 的機器都有一個 TCP 傳輸實體,TCP 實體可以是一個庫過程、一個使用者進程或者核心的一部分。在所有這些情形下,它管理 TCP 流以及與 IP 層之間的介面。

TCP 傳輸實體接受本地進程的使用者資料流,將它們分割成不超過 64KB(實際上去掉 IP 和 TCP 頭,通常不超過 1460 資料位元組)的分段,每個分段以單獨的 IP 資料包形式傳送。當包含 TCP 資料的資料包到達一台機器時,它們被遞交給 TCP 傳輸實體,TCP 傳輸實體重構出原始的位元組流。

IP 層並不保證資料包一定被正確地遞交到接收方,也不指示資料包的傳送速度有多快。正是 TCP 負責既要足夠快地傳送資料包,以便使用網路容量,但又不能引起網路擁塞,而且 TCP 超時後,要重傳沒有遞交的資料包。即使被正確遞交的資料包,也可能存在錯序的問題,這也是 TCP 的責任,它必須把接收到的資料包重新裝配成正確的順序。簡而言之,TCP 必須提供可靠性的良好效能,這正是大多數使用者所期望的而 IP 又沒有提供的功能。

TCP 封包主要包括:
  • SYN 包:請求建立連線的封包;
  • ACK 包:回應封包,表示接收到了對方的某個封包;
  • PSH 包:正常封包;
  • FIN 包:通訊結束包;
  • RST 包:重置連線,導致 TCP 協定傳送 RST 包的原因:
  • SYN 資料段指定的目的埠處沒有接收進程在等待;
  • TCP 協定想放棄一個已經存在的連線;
  • TCP 接收到一個資料段,但是這個資料段所標識的連線不存在;
  • 接收到 RST 資料段的 TCP 協定立即將這條連線非正常地斷開,並向應用程式報告錯誤。
  • URG 包:緊急指標。

TCP 三次握手

所謂三次握手(Three-Way Handshake)即建立 TCP 連線,就是指建立一個 TCP 連線時,需要用戶端和伺服器端總共傳送 3 個包以確認連線的建立,三次握手大致流程如下:

第一次握手

用戶端向伺服器發出連線請求報文,這時報文首部中的同部位 SYN=1,同時隨機生成初始序列號 seq=x,此時 TCP 用戶端進程進入了 SYN-SENT(同步已傳送狀態)狀態。

TCP 規定 SYN 報文段(SYN=1 的報文段)不能攜帶資料,但需要消耗掉一個序號。這是三次握手中的開始,表示用戶端想要和伺服器端建立連線。

第二次握手

TCP 伺服器收到請求報文後,如果同意連線,則發出確認報文。確認報文中應該 ACK=1、SYN=1,確認號是 ack=x+1,同時也要為自己隨機初始化一個序列號 seq=y,此時 TCP 伺服器進程進入了 SYN-RCVD(同步收到)狀態。

這個報文也不能攜帶資料,但是同樣要消耗一個序號,這個報文帶有 SYN(建立連線)和 ACK(確認)標誌,詢問用戶端是否準備好。

第三次握手

TCP 客戶進程收到確認後,還要向伺服器給出確認,確認報文的 ACK=1、ack=y+1,此時 TCP 連線建立,用戶端進入 ESTABLISHED(已建立連線)狀態。

TCP 規定 ACK 報文段可以攜帶資料,但是如果不攜帶資料則不消耗序號,這裡用戶端表示我已經準備好。

完成三次握手後,用戶端與伺服器即開始傳送資料。

TCP 四次揮手

所謂四次揮手(Four-Way-Wavehand)即終止 TCP 連線,就是指斷開一個 TCP 連線時,需要用戶端和伺服器端總共傳送 4 個包以確認連線的斷開。

在 socket 程式設計中,這一過程由用戶端或伺服器任一方執行 close 來觸發,大致流程如下:

第一次揮手 

TCP 傳送一個 FIN(結束),用來關閉客戶到伺服器端的連線。

用戶端進程發出連線釋放報文,並且停止傳送資料。釋放資料包文首部 FIN=1,其序列號為 seq=u(等於前面已經傳送過來的資料的最後一個位元組的序號加 1),此時用戶端進入 FIN-WAIT-1(終止等待 1)狀態。

TCP 規定,FIN 報文段即使不攜帶資料,也要消耗一個序號。

第二次揮手

伺服器端收到這個 FIN,它發回一個 ACK(確認),確認收到序號並為收到的序號 +1,和 SYN 相同一個 FIN 將佔用一個序號。

伺服器收到連線釋放報文,發出確認報文 ACK=1、ack=u+1,並且帶上自己的序列號 seq=v,此時伺服器端就進入了 CLOSE-WAIT(關閉等待)狀態。

TCP 伺服器通知高層的應用進程,用戶端向伺服器的方向就釋放了,這時候處於半關閉狀態,即用戶端已經沒有資料要傳送了,但是伺服器若傳送資料,用戶端依然要接受。這個狀態還要持續一段時間,也就是整個 CLOSE-WAIT 狀態持續的時間。

用戶端收到伺服器的確認請求後,此時用戶端就進入 FIN-WAIT-2(終止等待 2)狀態,等待伺服器傳送連線釋放報文(在這之前還需要接受伺服器傳送的最後的資料)。

第三次揮手

伺服器端傳送一個 FIN(結束)到用戶端,伺服器端關閉用戶端的連線。

伺服器將最後的資料傳送完畢後,就向用戶端傳送連線釋放報文 FIN=1、ack=u+1,由於在半關閉狀態,伺服器很可能又傳送了一些資料,假設此時的序列號為 seq=w,那麼伺服器就進入了 LAST-ACK(最後確認)狀態,等待用戶端的確認。

第四次揮手

用戶端傳送 ACK(確認)報文確認,並將確認的序號 +1,這樣關閉完成。

用戶端收到伺服器的連線釋放報文後,必須發出確認 ACK=1、ack=w+1,而自己的序列號是 seq=u+1,此時用戶端就進入了 TIME-WAIT(時間等待)狀態。

注意此時 TCP 連線還沒有釋放,必須經過 2∗∗MSL(最長報文段壽命)的時間後,當用戶端復原相應的 TCB 後,才進入 CLOSED 狀態。

伺服器只要收到了用戶端發出的確認,立即進入 CLOSED 狀態。同樣復原 TCB 後,就結束了這次的 TCP 連線。可以看到伺服器結束 TCP 連線的時間要比用戶端早一些。

為什麼建立連線是三次握手,而關閉連線卻是四次揮手

這是因為伺服器端在 LISTEN 狀態下,收到建立連線請求的 SYN 報文後,把 ACK 和 SYN 放在一個報文裡傳送給用戶端。

而關閉連線時,當收到對方的 FIN 報文時,僅僅表示對方不再傳送資料了但是還能接收資料,己方也未必將全部資料都傳送給了對方,所以己方可以立即 close,也可以傳送一些資料給對方後,再傳送 FIN 報文給對方來表示同意現在關閉連線,因此己方 ACK 和 FIN 一般都會分開傳送。

下面我們通過一個範例演示建立 TCP 連結來實現初步的 HTTP 協定,具體程式碼如下:
package main
import (
    "net"
    "os"
    "bytes"
    "fmt"
)
func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
        os.Exit(1)
    }
    service := os.Args[1]
    conn, err := net.Dial("tcp", service)
    checkError(err)
    _, err = conn.Write([]byte("HEAD / HTTP/1.0rnrn"))
    checkError(err)
    result, err := readFully(conn)
    checkError(err)
    fmt.Println(string(result))
    os.Exit(0)
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}
func readFully(conn net.Conn) ([]byte, error) {
    defer conn.Close()
    result := bytes.NewBuffer(nil)
    var buf [512]byte
    for {
        n, err := conn.Read(buf[0:])
        result.Write(buf[0:n])
        if err != nil {
            if err == io.EOF {
                break
            }
            return nil, err
        }
    }
    return result.Bytes(), nil
}
執行這段程式並檢視執行結果:

go run main.go baidu.com:80
HTTP/1.1 200 OK
Date: Thu, 02 Jan 2020 05:19:13 GMT
Server: Apache
Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
ETag: "51-47cf7e6ee8400"
Accept-Ranges: bytes
Content-Length: 81
Cache-Control: max-age=86400
Expires: Fri, 03 Jan 2020 05:19:13 GMT
Connection: Close
Content-Type: text/html