一、TCP协议

TCP协议---传输层协议。

1.TCP报文认识

对于传输层的报文,也就是报头加有效载荷,也被称为数据段。

tcp协议段的格式,也即报文格式如下。

linux之TCP协议_面向字节流

16位源端口号:本机网络的端口号

16位目的端口号:目的主机网络的端口号

32位序号:

tcp的特点之一是面向字节流,也就是说,不论是一个完整的报文,还是一个不完整的报文,在tcp协议看来,都是一个个以字节位基本单位的数据而已,tcp不关心报文是否完整。对于发送数据的一方,为了保证tcp在传输数据时,数据到达接收方时的有序性,tcp以字节位单位对数据进行编号,

32位确认序号:

tcp保证可靠性的核心是确认应答机制,32位序号是发送方对发送数据的编号,而32位是接收方在接收到发送方数据时给予发送方的响应应答。例如,发送方向接受发发送的是编号1-1000的数据,接收方在收到之后,以确认序号1001告诉发送方已经收到1001以前的数据。

4位首部长度:

用来表示tcp报文的报头大小,虽然是4位,但每一位的基本单位是4字节

16位窗口大小:

当发送方向接收方发送数据时,需要考虑网络因素以及接收方对数据的接收能力,16位窗口大小取二者的较小值作为实际的发送数据大小,尽量避免网络拥堵或者接收方接收数据能力达到上限时,仍然发送难以承受的数据大小,造成更加不稳定的情况。

16位校验和:检测当前收到的字节流是否准确

保留6位

linux之TCP协议_面向字节流_02

保留6位其实是6个标记位,它们的作用是区分不同类型(不同作用)的报文,分别对应的是

PSH:催促接收方尽快将数据从接收缓冲区取走

RST:发送方请求重新建立连接

SYN:请求建立连接(三次握手)

FIN:断开连接(4次挥手)

ACK:确认应答

URG:紧急指针是否有效

16位紧急指针:

指向紧急数据

32位选项:

2.TCP协议的特点

面向字节流

有连接

可靠性

3.面向字节流

TCP有两个缓冲区,发送缓冲区和接收缓冲区。数据在缓冲区以字节位基本单位,不对报文的完整性做区分。发送缓冲区什么时候发送数据,一次性发送多少,却决于协议本身(根据网络和对方接收缓冲区的接收能力)。

tcp协议在发送数据时,不考虑报文的完整性,发送缓冲区只管发送,接收缓冲区只负责接收,只要保证了缓冲区数据的顺序性就足以。

至于报文的完整性交给应运层处理即可,应运层不断的从接收缓冲区读取,如果不是一个完整的报文,用户层也就无法进行正确解析,如果是,正常解析即可。

而在应用层协议上,传输层的报文充当有效载荷,并添加新的报头,形成的新报文(也被称为请求和响应),可能就包含了请求正文的数据大小,以此正确解析数据。例如网络版本计算器,应用层协议是通信双方事先的约定,服务器只能对约定好的正文数据格式做出解析,否则解析失败丢掉即可。

4.确认应答机制(ACK)

确认应答机制是保证了TCP协议可靠性的一种。

其含义是,当发送方向接收方发送方数据后,接收方在接收到来自发送方的数据后,需要向发送发发送确认报文,表示自己收到了发送的数据,而只要发送发接收到了来自接受方对自己发送数据的确认报文,就能确定自己的数据成功被对方接收。

也就是说,确认应答机制,保证的是,收到确认应答之前的数据是成功发送的。

所以,只要通信双方都接收到了来自对方的确认报文,就能保证该确认报文之前的数据是成功被对方接收的。

5.超时重传机制

超时重传机制也是保证TCP可靠性的一种,当发送方发送数据后的一定时间内,发送发如果一直没有接收到来自接收方的确认报文,那么发送方就会默认数据丢失,会立即对数据进行重传。

6.流量控制

在发送方三次握手期间,发送方会对接收方接收缓冲区的接受能力以及网络情况对数据的承受能力,选取二者之中的较小值,作为发送方实际发送的数据大小。

流量控制的本质就是在动态的调整发送数据的大小,让数据得以安全且快速的进行传输。

7.滑动窗口

发送方向接收方发送数据时,不可能每一次都只发送一个报文,根据流量控制,发送方可能一次性会发送多个报文,这样可以极大的提高数据的传输效率。但是在传输过程中,也可能会出现丢包的情况,对于丢包,TCP协议除了超时重传机制外,还有快重传机制。

8.快重传

举例:发送方向接收方发送多个报文,报文的序号分别为1~1000,1001~2000,2001~3000,3001~4000等等(实际的序号有所区别)。当接收方成功接收到第一个报文时,立即向发送方发送确实报文,确认序号为1001,表示1001前的数据已经收到,当第二个报文也成功接收后,再次发送确认序号为2001的报文,表示2001前的数据已经接收。但是第三个报文在发送时,出现了丢包现象,接收方并没有接收到第三个报文,但是成功的接收到了第四个报文,这时候,接受方确认报文的确认序号会是2001,告诉发送方2001前的数据接收成功,当发送方继续发送后续报文时,因为第三个报文的丢失,所以接受方即使成功收到了后续的报文,但是确认报文中的确认序号一直都会是2001,当接收方接收到三次ACK的确认序号都是相同的时,就会对丢失的数据重新进行发送。

快重传和超时重传都是数据发生丢包时,能够对数据进行重新发送的机制,都保证的是数据传输的可靠性。相比超时重传,快重传提高了数据的传输效率,但是快重传只有在丢失报文后依然有报文发送时才可能生效,否则无法收到三次确认序号相同的ACK。

9.拥塞控制

流量控制考虑了接收方缓冲区和网络两个方面。

而拥塞控制则是只关心网络状况。

根据拥塞控制,得出多大的数据适合在此时的网络中传输。根据接收方接收缓冲区的大小,确定适合发送的数据大小,取二者中的较小值作为实际发送数据大小。

拥塞控制是如何了解网络状态的呢?以发送报文为例,拥塞控制

第一次发送报文时,只发送一个报文,第二次发送时,发送2个,第三次4个,第四次8个,也就是呈指数递增。同时会设置一个初始的阈值,当发送的报文数会超过阈值大小时,不能采用指数级增长,而是采用线性增长的方式,每次发送的报文时只能增加1。当发送的报文数会让网络拥堵时,必须从头开始,先发送一个报文,同时设置新的阈值为造成网络拥堵时发送报文数的一半,当报文数没有超过新的阈值时,指数增长,一旦会超过,就采用线性增长,循环往复。

10.捎带应答和延迟应答

捎带应答指的是,当接收方接收到来自发送方的数据时,需要ACK应答时,接收方也需要向发送方发送数据,此时ACK和接受方自身的数据会合并为一个报文,既有应答,也有自己的数据,这被称为捎带应答,这也是为什么TCP报文中,既有16位序号,又有16位确认序号的原因之一,通信双发的地位是对等的。

延迟应答指的是,接受方接收到来自发送方的报文,但是此时自身缓冲区的接收能力很弱,如果立即回应,那么发送方也只能发送很少的数据,所以接收方会等自身接收缓冲区能接收更多数据时,才进行应答。

二者都在一定程度上,提高的数据的传输效率。

11.有连接

linux之TCP协议_滑动窗口_03

有连接也是TCP协议保证数据可靠性的一种机制

11.1三次握手

linux之TCP协议_有连接_04

在通信双方真正开始数据通信前,会有三次握手进行通信双方连接的建立。

三次握手实际上是一种试探,客户端向服务器端发送一个请求建立连接的报文,可能只有报头而没有实际的数据。如果服务器端能接收到,就会进行捎带应答,将对客户端的应答ACK和自己请求与对方建立连接的报文合二为一,向客户端发去。客户端收到来自服务器的应答和连接请求后,也会发送给服务器端对于的确认ACK。

对于客户端来说,只要收到了应答,就代表自己已经与服务器建立了连接。而服务器端,只有收到来自客户端的应答之后才能确认自己和客户端建立了连接。

建立连接其实就是一种试探,如果通信双方都能成功的给对方发送消息,不就表明了通信是基本可靠的。

但是客户端收到的ACK应答只能保证在次之前的数据发送成功,而不能保证自身对于服务器端的ACK应答是否成功发送,一旦发送失败,客户端会处于已经建立连接的状态,而服务器端依旧处于连接未建立的状态,造成通信双方状态不一致,所以不能正常通信的情况。

server.cc如下

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>//sockaddr_in所在
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
#include<string>
//std::string serverip="8.130.20.129";//本身为uint32_t 点分10进制,但习惯以字符输入

int backlog=4;//设置服务器来连接connect listen上限
class sockmas
{
    public:
    int sockfd_;
    //uint32_t clientip_;
    //uint16_t clientport_;
    std::string clientip_;
    std::string clientport_;
};
void* start_route(void* args)//交给一个线程负责和一个客户端通信,线程需要知道通信的文件描述符
{
    sockmas* fd=static_cast<sockmas*>(args);
    //std::cout<<"clientip is :"<<fd->clientip_<<",clientport is:"<<fd->clientport_<<std::endl;

    printf("clientip is %s,clientport is:%s\n",fd->clientip_.c_str(),fd->clientport_.c_str());
    pthread_detach(pthread_self());
    while(1)
    {
        std::cout<<"client say:";
        char readbuff[128];
        ssize_t n=read(fd->sockfd_,readbuff,sizeof(readbuff));
        if(n<0)
        {
            std::cout<<"server read fail\n";
            continue;
        }
        readbuff[n]='\0';
        std::cout<<readbuff<<std::endl;
        //std::cout<<"server read success\n";
        std::cout<<"server please input:";
        std::string writebuff;
        getline(std::cin,writebuff);
        n=write(fd->sockfd_,writebuff.c_str(),writebuff.size());
        if(n<0)
        {
            std::cout<<"write  fail\n";
            continue;
        }
        std::cout<<"write  success\n";

    }
    close(fd->sockfd_);
    return nullptr;

}
void Usage(const std::string &proc)
{
    std::cout << "\n\rUsage: " << proc << " serverport\n"
              << std::endl;
}
int main(int argc, char *argv[])
{
     if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t serverport=std::stoi(argv[1]);
    //1.创建tcp套接字
    int listenfd=socket(AF_INET,SOCK_STREAM,0);//tcp,面向字节流
    if(listenfd<0)
    {
        std::cout<<"socket fail\n";
        return 1;
    }
     std::cout<<"socket success\n";
    //2.绑定IP地址和端口号
    sockaddr_in servermas;
    memset(&servermas,0,sizeof(servermas));
    servermas.sin_family=AF_INET;
    //servermas.sin_addr.s_addr=htonl(std::stoi(serverip.c_str()));
    //servermas.sin_addr.s_addr=inet_addr(serverip.c_str());//将字符串转为网络字节序//该方法bind失败
    servermas.sin_addr.s_addr=INADDR_ANY;
    servermas.sin_port=htons(serverport);
    socklen_t len=sizeof(servermas);
    if(bind(listenfd,(sockaddr*)&servermas,len)<0)
    {
        std::cout<<"bind fail\n";
        return 2;
    }
    std::cout<<"bind success\n";
    int n=listen(listenfd,backlog);
    if(n<0)
    {
        std::cout<<"listen fail\n";
        return 2;
    }
    std::cout<<"listen success\n";
    while(1)
    {
        // sockaddr_in clientmas;
        // socklen_t addrlen=sizeof(clientmas);
        // int sockfd=accept(listenfd,(sockaddr*)&clientmas,&len);//返回新的文件描述符,阻塞式等待connect
        // if(sockfd<0)
        // {
        //     std::cout<<"accept fail\n";
        //     continue;
        // }
        // std::cout<<"accept success\n";
        // //accept成功后,由子线程负责和客户端通信
        // pthread_t tid;
        // //socket fd;
        // sockmas* fd=new sockmas;//必须是new,fd是输出型参数,new的作用域才符合要求,
        // fd->sockfd_=sockfd;
        // fd->clientip_=std::to_string(ntohl(clientmas.sin_addr.s_addr));
        // fd->clientport_=std::to_string(ntohs(clientmas.sin_port));
        // pthread_create(&tid,nullptr,start_route,fd);

    }


    return 0;

}

client.cc

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>//sockaddr_in所在
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>

//本身为uint32_t 点分10进制,但习惯以字符输入
void Usage(const std::string &proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}
int main(int argc, char *argv[])
{
     if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip=argv[1];
    uint16_t serverport=std::stoi(argv[2]);//
    //1.创建tcp套接字
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        std::cout<<"client socket fail\n";
        return 1;
    }
    std::cout<<"client socket success\n";
    //2.客户端的绑定会在connect时自动绑定

    //3.连接服务器
    // sockaddr_in servermas;
    // memset(&servermas,0,sizeof(servermas));
    // servermas.sin_family=AF_INET;
    // servermas.sin_addr.s_addr=htonl((std::stoi(serverip.c_str())));
    // servermas.sin_port=htons(serverport);
    struct sockaddr_in servermas;
    memset(&servermas,0,sizeof(servermas));
    servermas.sin_family=AF_INET;
    //servermas.sin_addr.s_addr=inet_addr(serverip.c_str());//该方法会绑定成功
    //uint32_t ip=std::stoi(serverip.c_str());
   // servermas.sin_addr.s_addr=htonl(ip);//stoi就是string到int,该方法绑定失败

     inet_pton(AF_INET, serverip.c_str(), &(servermas.sin_addr));//该方法会绑定成功
    servermas.sin_port=htons(serverport);
    socklen_t addrlen=sizeof(servermas);
    int n=connect(sockfd,(struct sockaddr*)&servermas,addrlen);
    if(n<0)
    {
        std::cout<<"client connect fail\n";
        return 2;
    }
    std::cout<<"client connect success\n";

    //4.通信
    while(1)
    {
        std::cout<<"client please input:";
        std::string writebuff;
        getline(std::cin,writebuff);
        ssize_t n=write(sockfd,writebuff.c_str(),writebuff.size());
        if(n<0)
        {
            std::cout<<"write fail\n";
            continue;
        }
        std::cout<<"client write success\n";
        char readbuff[128];
        n=read(sockfd,readbuff,sizeof(readbuff));
        if(n<0)
        {
             std::cout<<"read fail\n";
            continue;
        }
        readbuff[n]='\0';
        std::cout<<"server say:";
        std::cout<<readbuff<<std::endl;
        //std::cout<<"client read success\n";

    }

    return 0;
}

server.cc和client.cc分别是TCP套接字通信的服务器端和客户端,但是服务区端特意将accept部分代码注释掉了。通信结果如下

linux之TCP协议_TCP_05

客户端的状态是ESTABLISHED,也就是已经建立连接的状态,而服务器端则是处于listen的监听状态。此时双方连接并没有建立成功,所以无法通信

linux之TCP协议_有连接_06

当上述服务器端代码全部放开之后,效果如下。

linux之TCP协议_有连接_07

双方的状态都是已经建立连接的状态,此时才可以进行正常通信。

11.2四次挥手

linux之TCP协议_TCP_08

四次回收,诠释的也是响应应答机制,但是并没有和三次握手一样使用捎带应答的方式,因为通信双方并不一定都想关闭连接。主动断开连接的内一方,只是不能在向对方发送数据,依然有接收数据的能力。

linux之TCP协议_TCP_09

当客户端作为先断开连接的一方,其状态为FIN_WAIT2,而客户端的状态为CLOSE_WAIT,此时再断开服务器端,查看双方状态。如下图

linux之TCP协议_TCP_10

可以看到,服务器端也退出之后,自身的状态为LAST_ACK。

再次测试,让服务器端先退出,客户端后退出。

双方建立连接,如下图

linux之TCP协议_滑动窗口_11

服务器端先退出,如下图

linux之TCP协议_可靠性_12

先退出的服务器的状态为,FIN_WAIT2,此时未退出的客户端的状态为CLOSE_WAIT

客户端也退出,如下图

linux之TCP协议_面向字节流_13

客户端也退出后,双方新的状态并没有查询到,但是理论上和上个示例是相同的。

12.粘包

TCP是面向字节流的,因此应用层如果想将字节流的数据转化为一个个的TCP报文,只能是通信双方使用同样的协议,制定协议规则,应用层才能顺利将TCP报文解析成一个个请求或者响应。但是如果双方没有事先约定,那么应用层在读取上来的字节流,可能不足一个报文,也可能多于一个报文,但是第二个报文不完整,像这样的少解析或者多解析的情况,被称为粘包问题。

粘包问题是应用层在解析TCP报文时遇到的问题,所以可以通过在应用层制定协议解决。

而协议中具体的解决方法有如下几种

1.定长报文,直接规定报文的长度

2.使用特殊字符,将特殊字符作为报文与报文之间的分界线

3.自描述字段+定长报头,将报头大小固定,并在报头中用一个字段标识数据部分大小

4.自描述字段+特殊字符,例如http协议中的自描述字段就是Content-length,KV式的表明的数据大小,再将换行作为特殊字符,以此区分正文数据部分和报头。

13.TCP异常情况

无论是三次握手还是四次挥手,上层调用之后的后续操作都是由操作系统自己完成,所以如果遇到进程终止或者电脑重启的情况,双方操作系统会自动进行四次挥手,断开双方连接。

但是网络出现问题,例如客户端断电断网,此时客户端的连接无疑是断开的,但是服务器端仍然认为连接存在,造成双方不一致问题。实际上如果服务器一直没有接收到来自客户端的信息,连接也会断开。

所谓的连接断开与否:指的是客户端和服务器是否还要维护双方通信的结构体信息。