首页 > Go语言 > Go语言网络编程 阅读:3,045

Go语言TCP协议

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.0\r\n\r\n"))
    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

爱面试的程序媛,一个分享面试经验的公众号。跟着站长一起学习,每天都有进步。

通俗易懂,深入浅出,定时分享程序员面试的那点事。

面试如何造火箭?工作如何拧螺丝?都在这个公号哦。

扫描二维码关注公众号,免费领取价值 1000 元的求职面试资料(限时免费)!

当你决定关注「爱面试的程序媛」,你已然超越了90%的程序员!

爱面试的程序媛二维码
微信扫描二维码关注