TCP/IP三次握手、四次挥手

来源:互联网 发布:软件著作权范文 编辑:程序博客网 时间:2024/06/07 17:39

TCP协议概述

TCP 和 UDP 处在同一层——传输层,但是它们有很多的不同。TCP 是 TCP/IP 系列协议中最复杂的部分,它具有以下特点:

  • TCP 提供 可靠的 数据传输服务,TCP 是 面向连接的 。应用程序在使用 TCP通信之前,先要建立连接,这是一个类似“打电话”的过程,通信结束后还要“挂电话”
  • TCP 连接是 点对点 的,一条 TCP 连接只能连接两个端点
  • TCP 提供可靠传输,无差错、不丢失、不重复、按顺序
  • TCP 提供 全双工 通信,允许通信双方任何时候都能发送数据,因为 TCP 连接的两端都设有发送缓存和接收缓存
  • TCP 面向 字节流 。TCP 并不知道所传输的数据的含义,仅把数据看作一连串的字节序列,它也不保证接收方收到的数据块和发送方发出的数据块具有大小对应关系

TCP报文段结构

TCP 是面向字节流的,而 TCP 传输数据的单元是 报文段 。一个 TCP 报文段可分为两部分:报头和数据部分。数据部分是上层应用交付的数据,而报头则是 TCP 功能的关键。

TCP 报文段的报头有前 20 字节的固定部分,后面 4n 字节是根据需要而添加的字段。如图则是 TCP 报文段结构:
这里写图片描述
20 字节的固定部分,各字段功能说明:

  1. 源端口和目的端口:各占 2 个字节,分别写入源端口号和目的端口号。这和 UDP 报头有类似之处,因为都是运输层协议。
  2. 序号:占 4 字节序,序号范围[0,2^32-1],序号增加到 2^32-1 后,下个序号又回到 0。 TCP 是面向字节流的,通过 TCP 传送的字节流中的每个字节都按顺序编号,而报头中的序号字段值则指的是本报文段数据的第一个字节的序号。
  3. 确认序号:占 4 字节,期望收到对方下个报文段的第一个数据字节的序号。
  4. 数据偏移:占 4 位,指 TCP 报文段的报头长度,包括固定的 20 字节和选项字段。
  5. 保留:占 6 位,保留为今后使用,目前为 0。
  6. 控制位:共有 6 个控制位,说明本报文的性质,意义如下: URG 紧急:当 URG=1 时,它告诉系统此报文中有紧急数据,应优先传送(比如紧急关闭),这要与紧急指针字段配合使用。 ACK 确认:仅当 ACK=1 时确认号字段才有效。建立 TCP 连接后,所有报文段都必须把 ACK 字段置为 1。 PSH 推送:若 TCP 连接的一端希望另一端立即响应,PSH 字段便可以“催促”对方,不再等到缓存区填满才发送。 RET 复位:若 TCP 连接出现严重差错,RST 置为 1,断开 TCP 连接,再重新建立连接。 SYN 同步:用于建立和释放连接,稍后会详细介绍。 FIN 终止:用于释放连接,当 FIN=1,表明发送方已经发送完毕,要求释放 TCP 连接。
  7. 窗口:占 2 个字节。窗口值是指发送者自己的接收窗口大小,因为接收缓存的空间有限。
  8. 检验和: 2 个字节。和 UDP 报文一样,有一个检验和,用于检查报文是否在传输过程中出差错。
  9. 紧急指针: 2 字节。当 URG=1 时才有效,指出本报文段紧急数据的字节数。
  10. 选项:长度可变,最长可达 40 字节。具体的选项字段,需要时再做介绍。

三次握手

TCP 是面向连接的,在传输 TCP 报文段之前先要创建连接,发起连接的一方被称为客户端,而响应连接请求的一方被称为服务端,而这个创建连接的过程被称为 三次握手 :
这里写图片描述
(1)客户端向服务端发出请求连接报文段,其中报头控制位SYN=1,初始序号seq=x。客户端进入SYN-SENT(同步已发送状态)
(2)服务端收到请求报文段后,向客户端发送确认报文段。确认报文段的首部中 SYN=1,ACK=1,确认号是 ack=x+1,同时为自己选择一个初始序号 seq=y。服务端进入 SYN-RCVD(同步收到)状态。
(3)客户端收到服务端的确认报文段后,还要给服务端发送一个确认报文段。这个报文段中 ACK=1,确认号 ack=y+1,而自己的序号 seq=x+1。这个报文段已经可以携带数据,如果不携带数据则不消耗序号,则下一个报文段序号仍为 seq=x+1。

至此 TCP 连接已经建立,客户端进入 ESTABLISHED(已建立连接)状态,当服务端收到确认后,也进入 ESTABLISHED 状态,它们之间便可以正式传输数据了。

为什么要三次握手?
假设是二次握手,C向S发送连接请求,S发送确认报文,S就认为连接已经建立了。举个例子:
C向S发送连接请求(第一次),而S没有回应,那么C就会接着发(第二次),这一次发送成功了,S和C建立连接。当S和C释放连接后,如果之前C发送的第一次连接请求发送到S,此时S认为连接建立了,但C仍然处于CLOSED状态,这样就白白浪费掉了S的资源。

如果连接次数多了后呢?著名的两军问题告诉我们,通信系统中其实没有完全可靠的连接。那么三次握手已经相对可靠了。

下面是使用tcpdump抓包结果:
注意:第三次握手seq=1,ack=1是相对值
这里写图片描述

四次挥手

当客户端和服务器通过三次握手建立了TCP连接以后,当数据传送完毕,肯定是要断开TCP连接的啊。那对于TCP的断开连接,这里就有了四次分手:
这里写图片描述
(1)主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;通过上图可以看出,seq和ack与最后一次数据传输有关系
(2)主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
(3)主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
(4)主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。

为什么要四次挥手?
摘自:http://www.jellythink.com/archives/705

那四次分手又是为何呢?TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。如果要正确的理解四次分手的原理,就需要了解四次分手过程中的状态变化。

  • FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。(主动方)
  • FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。(主动方)
  • CLOSE_WAIT:这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。(被动方)
  • LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。(被动方)
  • TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FINWAIT1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)
  • CLOSED: 表示连接中断。

下面是使用tcpdump抓包结果:
第一次挥手:
这里写图片描述
第二三次挥手:
这里写图片描述
第四次挥手:
这里写图片描述

附客户端和服务端代码:

客户端:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>#include <sys/types.h>#include <unistd.h>#define BUFLEN 1024int main(int argc, char *argv[]){    int sockfd;    struct sockaddr_in s_addr;    socklen_t len;    unsigned int port;    char buf[BUFLEN];        /*建立socket*/    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){        perror("socket");        exit(errno);    }    /*设置服务器端口*/        if(argv[2])        port = atoi(argv[2]);    else        port = 7777;    /*设置服务器ip*/    bzero(&s_addr, sizeof(s_addr));    s_addr.sin_family = AF_INET;    s_addr.sin_port = htons(port);    if (inet_aton(argv[1], (struct in_addr *)&s_addr.sin_addr.s_addr) == 0) {        perror(argv[1]);        exit(errno);    }    /*开始连接服务器*/        if(connect(sockfd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr)) == -1){        perror("connect");        exit(errno);    }else        printf("*****************client start***************\n");    while(1){        /******接收消息*******/        bzero(buf,BUFLEN);        len = recv(sockfd,buf,BUFLEN,0);        if(len > 0)            printf("receive massage:%s\n",buf);        else{            if(len < 0 )                printf("receive failed\n");            else                printf("server stop\n");            break;            }    _retry:            /******发送消息*******/            bzero(buf,BUFLEN);        printf("enter your words:");        /*fgets函数:从流中读取BUFLEN-1个字符*/        fgets(buf,BUFLEN,stdin);        /*打印发送的消息*/        //fputs(buf,stdout);        if(!strncasecmp(buf,"quit",4)){            printf("client stop\n");            break;        }        /*如果输入的字符串只有"\n",即回车,那么请重新输入*/        if(!strncmp(buf,"\n",1)){            goto _retry;        }        /*如果buf中含有'\n',那么要用strlen(buf)-1,去掉'\n'*/            if(strchr(buf,'\n'))            len = send(sockfd,buf,strlen(buf)-1,0);        /*如果buf中没有'\n',则用buf的真正长度strlen(buf)*/            else            len = send(sockfd,buf,strlen(buf),0);        if(len > 0)            printf("send successful\n");                    else{            printf("send failed\n");            break;                    }    }    /*关闭连接*/    close(sockfd);    return 0;}

服务端:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>#include <sys/types.h>#include <unistd.h>#define BUFLEN 1024int main(int argc, char *argv[]){    int sockfd, newfd;    struct sockaddr_in s_addr, c_addr;    char buf[BUFLEN];    socklen_t len;    unsigned int port, listnum;    /*建立socket*/    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){        perror("socket");        exit(errno);    }    /*设置服务器端口*/        if(argv[2])        port = atoi(argv[2]);    else        port = 7777;    /*设置侦听队列长度*/    if(argv[3])        listnum = atoi(argv[3]);    else        listnum = 3;    /*设置服务器ip*/    bzero(&s_addr, sizeof(s_addr));    s_addr.sin_family = AF_INET;    s_addr.sin_port = htons(port);    if(argv[1])        s_addr.sin_addr.s_addr = inet_addr(argv[1]);    else        s_addr.sin_addr.s_addr = INADDR_ANY;    /*把地址和端口帮定到套接字上*/    if((bind(sockfd, (struct sockaddr*) &s_addr,sizeof(struct sockaddr))) == -1){        perror("bind");        exit(errno);    }    /*侦听本地端口*/    if(listen(sockfd,listnum) == -1){        perror("listen");        exit(errno);        }    while(1){        printf("*****************server start***************\n");        len = sizeof(struct sockaddr);        if((newfd = accept(sockfd,(struct sockaddr*) &c_addr, &len)) == -1){            perror("accept");                    exit(errno);        }        while(1){        _retry:            /******发送消息*******/            bzero(buf,BUFLEN);            printf("enter your words:");            /*fgets函数:从流中读取BUFLEN-1个字符*/            fgets(buf,BUFLEN,stdin);            /*打印发送的消息*/            //fputs(buf,stdout);            if(!strncasecmp(buf,"quit",4)){                printf("server stop\n");                break;            }            /*如果输入的字符串只有"\n",即回车,那么请重新输入*/            if(!strncmp(buf,"\n",1)){                goto _retry;            }                /*如果buf中含有'\n',那么要用strlen(buf)-1,去掉'\n'*/                        if(strchr(buf,'\n'))                len = send(newfd,buf,strlen(buf)-1,0);            /*如果buf中没有'\n',则用buf的真正长度strlen(buf)*/                else                len = send(newfd,buf,strlen(buf),0);            if(len > 0)                printf("send successful\n");                        else{                printf("send failed\n");                break;                        }            /******接收消息*******/            bzero(buf,BUFLEN);            len = recv(newfd,buf,BUFLEN,0);            if(len > 0)                printf("receive massage:%s\n",buf);            else{                if(len < 0 )                    printf("receive failed\n");                else                    printf("client stop\n");                break;                    }        }        /*关闭聊天的套接字*/        close(newfd);        /*是否退出服务器*/        printf("exit?:y->yes;n->no ");        bzero(buf, BUFLEN);        fgets(buf,BUFLEN, stdin);        if(!strncasecmp(buf,"y",1)){            printf("server stop\n");            break;        }    }    /*关闭服务器的套接字*/    close(sockfd);    return 0;}
0 0
原创粉丝点击