TCP三次握手和四次挥手

一、TCP 连接

TCP是一个面向连接的协议,是因为在一个应用进程开始向另一个进程发送数据时,这两个进程必须先建立连接,也称之为“握手”,即它们必须发送些预备报文段,以建立确保数据传输的参数。

TCP连接提供的是全双工服务,也就是单个发送方与单个接收方之间的连接。比如客户端的进程A和服务端的进程B存在一条TCP连接,那么A的数据可以发送到B,B的数据可以发送到A。

二、建立TCP连接(三次握手)

      1. 


TCP三次握手

三次握手步骤如下:

(1)Client的TCP先向Server的TCP发送连接请求。报文段TCP SYN中不含应用层要发送的数据。报文段首部标志位SYN置1。Client随机选择一个初始化序号ISN, 本例为Client_ISN,将此序号放在报文段TCP SYN 32位序号字段中。Client TCP状态: CLOSED ->SYN_SENT。

(2)一旦包含TCP SYN报文段的的IP数据报到达Server(这里假设到达)。Server会从IP数据报提取出TCP SYN报文段,为该TCP连接分配TCP缓存和变量,并向该客户TCP发送允许连接的报文段。在该报文中也不包含应用层数据,SYN被置1,TCP报文段首部32位确认序号字段被置为Client_ISN + 1,并且ACK也被置为1,对Client发来的报文段进行确认。Server也会随机选择一个初始化序号ISN,本例为Server_ISN。该报文段被称为SYN ACK报文段。Server TCP状态:Client TCP状态: CLOSED -> LISTEN -> SYN_RCVD。

(3)Client收到Server发送的SYN ACK报文段,Client TCP状态: SYN_SENT -> ESTABLISHED。Client也要为TCP连接分配缓存和变量。此时Client只需要确认Server发送的报文段,因此SYN 置0,ACK置1。报文段首部32位确认序号被置为Server_ISN + 1。此时TCP连接建立完成。Server TCP状态:Client TCP状态: SYN_RCVD -> ESTABLISHED.

1.  为什么需要三次握手,不是两次握手?

如果Client发送的请求连接报文段在某种情况下并没有到达Server,报文段并不是丢失了,而是在某个网络节点长时间滞留,以致延误到连接释放才到Server。这个报文段已经失效,但是Server却误认为是Client发送的请求连接报文。如果是两次握手,此时Server向Client发送ACK包,但Client不会理会Server发来的ACK包,也不会向Server发送ACK包。于是Server以为与Client已经建立连接,等待Client发送数据。此时会造成Server资源的浪费,只能等待Client发送数据。也就是说Client的TCP状态一开始为SYN_SENT状态,等待Server 的SYN + ACK 包进入ESTABLISHED状态。当连接失效时,Client处于CLOSED状态,尽管Server发来SYN + ACK包,Client也不会接收Server发来的包。如果在Server收到失效的SYN包,Client通过调用Connect变为SYN_SENT状态,此时接收到Server发送的SYN + ACK包,变为ESTABLISHED状态。但是Client重新调用Connect函数发送的SYN包会被Server丢弃。发送包由于seq是以被丢弃的SYN包的序号为准,而服务器接收序号是以那个延误旧连接SYN报文序号为准。

“三次握手”的目的是为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

三、TCP连接终止(四次挥手)

1.  因为TCP是全双工,因此每个方向必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。这利用了TCP的半关闭。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。


TCP四次挥手


(1)Client执行主动关闭,FIN置1,向Server发送FIN包,FIN序号与SYN序号,也占用TCP首部32位序号字段。用来关闭Client到Server的数据传送。

(2) Server接收到Client发送的FIN包,ACK置1,将FIN包32位序号Client_FIN + 1,放置到确认序号,向Client发送ACK包。

(3) Server再向Client发送FIN包,FIN置1。用来关闭Server到Client的数据传送。

(4) Client收到FIN包,ACK置1,并将FIN序号Server_FIN + 1,放置到确认序号字段。向Server发送ACK包。

1.  四次挥手的TCP状态

(1)FIN_WAIT1 当Client应用层想注动关闭连接,使用套接字通知TCP,让TCP发送一个FIN包给Server,此时Client TCP进入FIN_WAIT1状态,等待Server发送发送FIN包。

(2)CLOSE_WAIT 表示Server等待关闭。如果没有数据发送,向Client发送一个FIN包,关闭连接。如果Server还有数据发送,向Client发送一个ACK包,然后Server进入CLOSE_WAIT状态。

(3)FIN_WAIT2 此时Client收到Server发送的ACK包,此时处于半关闭状态,只有Client向Server主动关闭连接,Server还可以向Client发送数据。

(4)TIME_WAIT 也称为2MSL等待状态。每个具体的TCP实现必须选择一个最大生存时间MSL。它是任何报文段被丢弃前在网络内的最长时间。由于最后Server收到ACK包并不会向Client发送确认报文,因此需要等待一段时间希望Server收到ACK包,如果因为超时没有收到,Server会重发FIN包,此时Client还会发送ACK包。但之前的ACK包会被丢弃。由于处在TIME_WAIT状态下,实际TCP连接已经断开,但socket还会占用当前的端口,并不能被一个新的连接所使用。

2.  为什么TCP连接终止需要四次。

前面说过,三次握手是因为Client与Server都要确认对方发来的请求连接SYN包。并且Server向Client发送SYN + ACK包时,将SYN与ACK放在了同一个报文段。而连接终止时,Client主动关闭连接时,Server先回复ACK包,但可能还有一些数据没有向Client发送完,处于半连接,所以TCP状态 FIN_WAIT1 -> FIN_WAIT2。如果Server发送了FIN包,Server要与Client断开连接。

四、tcpdump获取TCP连接与终止状态

采用socket编写一个Client与Server,采用本地回环地址作为Client的IP。

Client

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "InOut.h"
#include "err.h"

#define    MAXLINE       4096

void err_sys(const char * fmt, ...)  __attribute__((noreturn));
void err_quit(const char * fmt, ...) __attribute__((noreturn));

int main(int argc, char ** argv) {

    struct sockaddr_in       ClientAddr;
    char   RecvLine[MAXLINE + 1];

    if (argc != 2) 
        err_quit("usage: a.out <IPaddress>");

    int ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);                   /* 定义套接字,使用TCP协议 */
    if (ClientSocket < 0) 
        err_sys("socket error");

    bzero(&ClientAddr, sizeof(ClientAddr));

    struct in_addr * addrptr = &ClientAddr.sin_addr;

    ClientAddr.sin_family = AF_INET;
    ClientAddr.sin_port = htons(8888);
    if (inet_pton(ClientAddr.sin_family, *(argv + 1), addrptr) < 0)                  
        err_quit("inet_pton error for %s", argv[1]);

    if (connect(ClientSocket, (struct sockaddr *)&ClientAddr, sizeof(ClientAddr)) < 0)  /* 与服务端建立连接 */
        err_sys("connect error");

    scanf("%s", RecvLine);
    if (writen(ClientSocket, RecvLine, strlen(RecvLine)) < 0)                            /* 向服务端发送数据 */
        err_sys("write error");

    close(ClientSocket);

    exit(0);
}
Server
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "InOut.h"
#include "err.h"

#define    MAXLINE       4096
#define    LISTENQ       1024
void err_sys(const char * fmt, ...) __attribute__((noreturn));

int main(int argc, char ** argv) {

    struct sockaddr_in    ServerAddr;
    struct sockaddr_in    ClientAddr;
    char   BuffSize[MAXLINE] = {0};
    int    SizeAddr = sizeof(ClientAddr);
    
    int    ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);        /* 定义监听套接字,使用TCP协议 */

    bzero(&ServerAddr, sizeof(ServerAddr));                                 /* 套接字结构地址初始化0 */
    bzero(&ClientAddr, sizeof(ClientAddr));

    ServerAddr.sin_family = AF_INET;                                              
    ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);                         
    ServerAddr.sin_port = htons(8888);

    if (bind(ListenSocket, (struct sockaddr *)&ServerAddr, sizeof(ServerAddr)) < 0)       /* 将监听套接字与端口,IP进行绑定 */
        err_sys("bind error");

    listen(ListenSocket, LISTENQ);                                                        /* 监听8888端口 */

//    for(; ;) {
    int ConnectSocket = accept(ListenSocket, (struct sockaddr*)&ClientAddr, &SizeAddr);   /* 接受客户端的请求连接 */
    if (ConnectSocket < 0)
        err_sys("accept error");

    printf("Client ip is %s\n", inet_ntoa(ClientAddr.sin_addr));
    printf("Client port is %d\n", ntohs(ClientAddr.sin_port));

    while (readn(ConnectSocket, BuffSize, MAXLINE) < 0) {                                  /* 读取客户端发送的数据 */
        err_sys("read error");
        
        if (!strcmp(BuffSize, "quit")) 
            break;
    }
//    }


    close(ConnectSocket);                                                                  /* 关闭TCP连接 */
    close(ListenSocket);

    exit(0);
}

使用tcpdump观察

1. TCP连接状态

root@Ubuntu:~# tcpdump -i lo tcp port 8888 and host 127.0.0.1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes
18:06:25.290805 IP localhost.36340 > localhost.8888: Flags [S], seq 1201714844, win 43690, options [mss 65495,sackOK,TS val 2372902 ecr 0,nop,wscale 7], length 0
18:06:25.290923 IP localhost.8888 > localhost.36340: Flags [S.], seq 2836719335, ack 1201714845, win 43690, options [mss 65495,sackOK,TS val 2372902 ecr 2372902,nop,wscale 7], length 0
18:06:25.290953 IP localhost.36340 > localhost.8888: Flags [.], ack 1, win 342, options [nop,nop,TS val 2372902 ecr 2372902], length 0
2. Client向Client发送数据
18:06:29.030547 IP localhost.36340 > localhost.8888: Flags [P.], seq 1:5, ack 1, win 342, options [nop,nop,TS val 2373837 ecr 2372902], length 4
3. TCP终止状态

18:06:29.030657 IP localhost.36340 > localhost.8888: Flags [F.], seq 5, ack 1, win 342, options [nop,nop,TS val 2373837 ecr 2372902], length 0
18:06:29.030715 IP localhost.8888 > localhost.36340: Flags [.], ack 5, win 342, options [nop,nop,TS val 2373837 ecr 2373837], length 0
18:06:29.030775 IP localhost.8888 > localhost.36340: Flags [F.], seq 1, ack 6, win 342, options [nop,nop,TS val 2373837 ecr 2373837], length 0
18:06:29.030788 IP localhost.36340 > localhost.8888: Flags [.], ack 2, win 342, options [nop,nop,TS val 2373837 ecr 2373837], length 0

由以上测试结果可以很明显的观察到TCP的三次握手和四次挥手。

总结

1.  在三次握手阶段考虑了为什么不能进行两次握手,对两次握手进行了分析。在两次握手,一定要注意失效的报文段可能并没有丢弃,可能会到达服务端。

2.  四次挥手的时候重点在于为什么是四次。对TIME_WAIT状态的分析,可能还有没有分析到的地方,会在以后找出来。








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值