Linux——TCP协议1

目录

三次握手和四次挥手

三次握手

四次挥手

setsockopt

 Log.hpp

Sock.hpp

main.cc


 

        TCP通信的时候,客户端发送信息既不能太快也不能太慢,如何保证发送方发送数据既不快又不慢呢?

        服务器把自己的同步接收能力告诉客户端,而服务器的接收能力又由什么决定?接收缓冲区中剩余空间的大小。

        根据对方接收能力,控制数据发送速度,这种策略叫流量控制。16位窗口大小就是接收缓冲区剩余空间大小。16位窗口大小中填的是自己的接收缓冲区大小。

Linux——TCP协议1_第1张图片

这是TCP报头当中的6个标记位

        6个标记位:是按照1个比特位表示某种含义的,为什么需要多个标记位?

        我们可能会给对方发送各种类型的TCP报文。服务端会收到大量的不同的报文,这些标记位的本质就是来标记报文类型的。利用标记位区分报文类型。

Linux——TCP协议1_第2张图片

         各个标记位都是什么含义?     

        SYN:该报文是一个连接请求报文。

        FIN:该报文是一个断开连接请求的报文。  

        ACK:确认应答标志位,凡是该报文具有应答特征,该标志位都会被设置为1,大部分网络报文ACK都是被设置为1的。第一个连接请求报文不会被置为1.

三次握手和四次挥手

三次握手

1.如何理解连接?

        因为有大量的client将来可能连接server,所以server端一定会存在大量的连接,OS管理这些连接的方式:先描述,再组织。

        所谓的连接:本质其实就是内核的一种数据结构类型,建立连接成功的时候,就是在内核中创建对应的连接对象。再对多个连接对象进行某种数据结构的组织,对连接的管理也就成了对某种数据结构的增删查改。

        维护连接是有成本的:内存+CPU资源。

 2.如何理解三次握手?

握手的时候发的全是报文,甚至可能携带数据。并不仅仅是标志位,只不过是标志位被置为了1.

客户端发送SYN的时候,客户端的状态是SYN SENT,服务端收到一个请求而且将自己的SYN+ACK发出了,服务端的状态就是SYN RCVD,SYN SENT和SYN RCVD都是宏,当数据发出后,客户端和服务端状态其实就是1.

Linux——TCP协议1_第3张图片

客户端一旦收到SYN+ACK并且将ACK发出,客户端就认为连接建立好了。状态变为ESTABUSHED

服务端收到ACK,服务端对应的三次握手就完成了,连接就建立好了,状态也变为ESTABUSHED。

Linux——TCP协议1_第4张图片

         三次握手期间有一个隐形的成本就是时间,未来收的时候一定晚于发送的时候。

        三次握手对客户端和服务端都要起效。必须保证双方各自通信了三次

Linux——TCP协议1_第5张图片

        三次握手不一定要保证成功。只能保证较大概率握手成功,如客户端发送,服务器关机了。而且三次握手时,前俩次都是有应答的,最后一次客户端发出去之后没有应答,所以对于最后一次数据是否发送成功,客户端无法知晓。

        如果最后一次报文丢失了,客户端只要把报文经过ACK发出,自己的状态就变为了ESTABUSHED,即客户端认为连接建立好了,但实际上,服务端可能会没收到这条消息。

        1.为什么要三次握手?而不是一次,俩次,四次?

        如果是一次握手,客户端可能会一次性给服务器发送大量的SYN,由于维护连接是由成本的。有可能会一次性把服务器整崩溃,一次发送大量SYN,叫做SYN洪水。

        如果是俩次握手,客户端给服务端发消息,服务端给客户端返回SYN+ACK,俩次握手,服务器无法保证自己发送的SYN+ACK被对方收到,也无法感知,而服务器一旦发送处去SYN+ACK就认为自己的连接成功了,如果服务器一开始收到了大量的SYN,服务器再发送SYN+ACK,这就会导致服务器挂了大量的连接。又跟一次握手一样。

        三次握手,当客户端发送最后一次ACK的时候,只要客户端把ACK发出了,就认为连接建立好了,服务器必须得收到ACK,保证三次握手完成才建立连接。如果一开始发送大量得SYN请求时,一次和俩次握手(俩次握手会出现服务器发送SYN+ACK时,客户端直接将SYN+ACK丢弃)都是服务器挂满了大量连接,而客户端可能会出现甚至没连接得情况。三次握手,服务器上有连接时,客户端必须建立好连接,服务器的连接数和客户端的连接数是等价的。三次握手以最小的成本在建立连接时,不会过度消耗服务器资源。当服务器上有连接时,客户端必须维护好连接。如果客户端攻击服务器,服务器上的连接数和客户端上的连接数一定是等价的,即俩者的资源损失是一样的。

        三次握手并不能很好的保证安全问题。

        通过三次握手,服务端可以嫁接同等的成本给client,验证全双工。

        如果是四次握手,最后一次握手一定是服务端给客户端发的,当客户端直接丢弃最后一次ACK,此时所有的连接又都由服务端承担。

        奇数次握手最后的成本都在客户端, 偶数次握手连接建立时成本都在服务端。

        在握手时,尽量选择奇数次握手,的握手的目的在于把成本加在客户端,验证全双工,没必要执行五次,其次握手。

        当三次握手,客户端最后一次发送ACK后立马发送数据,由于丢包服务端没收到ACK,此时客户端认为连接建立成功,一旦发送消息,由于三次握手没成功,服务端看到数据后,意识到了连接建立异常,服务器就立马给客户端回复一个报文,这个报文就是TCP报头,并且将标记位RST置为1.客户端此时关闭连接,重新开始三次握手。

        RST:reset,连接重置。

 RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段。

 连接建立好了,如果对方发送消息,而我们的窗口大小是0,此时对方就不能发数据了,对方只能等,等到我们缓冲区有空间了,再继续发送消息。但如果我们缓冲区一直满,对方也不能一直等,发送方此时会设定PSH标志位。

        PSH:督促对方尽快向上交付数据。

PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走。

因为TCP是具有按序到达机制的(优点),我们发送的时候,被对方上层读取到,必须得有先后顺序,如果想插队呢?

URG:紧急标志位,要配合报头中的16位紧急指针(特定的数据在有效载荷位置的偏移量),该指针所指的地方只有一个字节被称为紧急数据。

 URG: 紧急指针是否有效。

四次挥手

         如何理解四次挥手?

        FIN是断开连接的标志位。当某一方要断开连接时,让FIN置1,把报文发给对方。而且这一方向另一方再也不会发送有效数据了。

        Linux——TCP协议1_第6张图片

ACK和FIN也可以被同时设置,服务器可以将ACK和FIN压缩在一起,即四次挥手可能会变成三次挥手。

        断开连接就一定能成功吗?

        也不一定,客户端首次发出断开连接的请求时,客户端会进入FIN_WAIT_1状态,服务端收到FIN之后给对方进行ACK,服务端会进入CLOSE_WAIT状态,客户端收到后进入FIN_WAIT_2状态。之后服务器发出FIN,状态变为LAST_ACK,客户端一收到FIN并且给对方ACK,客户端进入TIME-WAIT状态,服务端收到最后的ACK进入CLOSED状态。

Linux——TCP协议1_第7张图片

        CLOSE_WAIT时连接没关,因为四次回收没完成。如果服务器有大量的CLOSE_WAIT状态的连接的时候,原因是没有发FIN,即服务器引用层写的有BUG,忘了关闭对应的连接sockfd。

我们写一个简单的程序,并且不关闭sockfd

Linux——TCP协议1_第8张图片

 我们再用另一台机器访问

Linux——TCP协议1_第9张图片

 再查看状态,此时第一行local address并不是第一台服务器的公网IP地址,因为我们机器的公网IP并不一定是真实的,而是模拟出来的,现在图片里的IP才是真的IP。

Linux——TCP协议1_第10张图片

Linux——TCP协议1_第11张图片

 我们让客户端主动断开(第二台机器充当客户端)。此时服务端的状态是CLOSE_WAIT,因为服务器没有关闭这个文件描述符,如果此时来了大量的连接,服务器上会充满大量的close——wait。

Linux——TCP协议1_第12张图片

 最后会导致服务器服务越来越差

Linux——TCP协议1_第13张图片

 四次挥手若正常完成,主动断开的一方会进入TIME_WAIT状态。

我们关闭sockfd,我们想让服务端断开连接。Linux——TCP协议1_第14张图片

 我们看到服务器主动断开连接(主动关闭了文件描述符),底层自动进行四次挥手,进入了TIME_WAITLinux——TCP协议1_第15张图片

我们让服务器只获取连接

Linux——TCP协议1_第16张图片

 当连接上之后我们直接把服务干掉,由于文件描述符是跟随进程的,进程被关闭,文件描述符也随之被关。

Linux——TCP协议1_第17张图片

 此时自动触发了四次挥手,服务器成了主动断开连接的一方,服务器和客户端连接就处于TIME_WAIT状态。此时重新启动服务器,我们发现服务器无法重新启动,此时退出码是3

Linux——TCP协议1_第18张图片

 一段时间后8080端口服务已经没了

Linux——TCP协议1_第19张图片

 再重启服务器,此时才能重启成功。

Linux——TCP协议1_第20张图片

我们根据Sock 代码,退出码为3代表绑定失败。

        这是因为主动断开的一方要维持一个Timewait状态。 虽然四次挥手已经完成,但是主动断开连接的一方要维持一段时间的TIME_WAIT状态,在该状态下连接其实已经结束,但是地址信息ip,port依旧是被占用的。

setsockopt

Linux——TCP协议1_第21张图片

即使服务器有time_wait状态,但TIME_WAIT状态不会再被使用, 服务器可以绕过TIME_WAIT的判断,直接让服务器绑定成功。

Linux——TCP协议1_第22张图片

 虽然是TIME_WAIT,但此时依旧可以重启

         为什么主动断开的一方要处于TIME-WAIT?

        当双方在协商断开连接时,可能会有一些报文滞留在网络中,还没有送达客户端或服务器,所以我们需要一个状态,来等等这些滞留在网络中的报文。这个时间一般是2倍的MSL(最大报文的生存时间),以确保历史数据从网络中消散,因为这些报文可能会影响之后建立的通信,而且等一段时间当主动断开的一方最后一次发出ACK时,如果ACK丢了,则对方会补发FIN,此时如果主动断开的一方不经过TIME_WAIT直接进入CLOSED状态,则会收不到对方的FIN,但有了TIME_WAIT之后,收到对方的FIN,继而重新发送ACK。

 Log.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 

// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./threadpool.log"

// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if(level== DEBUG) return;
#endif
    // va_list ap;
    // va_start(ap, format);
    // while()
    // int x = va_arg(ap, int);
    // va_end(ap); //ap=nullptr
    char stdBuffer[1024]; //标准部分
    time_t timestamp = time(nullptr);
    // struct tm *localtime = localtime(×tamp);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024]; //自定义部分
    va_list args;
    va_start(args, format);
    // vprintf(format, args);
    vsnprintf(logBuffer, sizeof logBuffer, format, args);
    va_end(args);

    //FILE *fp = fopen(LOGFILE, "a");
    printf("%s%s\n", stdBuffer, logBuffer);
    //fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    //fclose(fp);
}

Sock.hpp

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include"Log.hpp"
class Sock
{
    private:
        const static int gbacklog=20;//一般不能太大也不能太小,后面会解释,这时listen的第二个参数
    public:
        Sock()
        {}
        int Socket()//创建套接字
        {
           int  listensock=socket(AF_INET,SOCK_STREAM,0);//返回值照样是文件描述符
            //参数含义第一个:网络通信 第二个:流式通信
            if(listensock<0)//创建套接字失败
            {
                logMessage(FATAL,"create socket error,%d:%s",errno,strerror(errno));//打印报错信息
                exit(2);
            }
            logMessage(NORMAL,"create socket success,sock:%d",listensock);//打印套接字,它的文件描述符是3
            int opt=1;//标记位为打开状态
            setsockopt(listensock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof opt);
            return listensock;
        }
        void Bind(int sock,uint16_t port,std::string ip="0.0.0.0")
        {
            //bind目的是让IP和端口进行绑定
            //我们需要套接字,和sockaddr(这个里面包含家族等名称)
            //绑定——文件和网络
            struct sockaddr_in local;
            memset(&local,0,sizeof local);//初始化local
            local.sin_family=AF_INET;
            local.sin_port=htons(port);//端口号
            local.sin_addr.s_addr=ip.empty()?INADDR_ANY:inet_addr(ip.c_str());
            //IP地址,由于我们构造的时候是IP是个空的字符串
            //所以我们可以绑定任意IP
            //我们一般推荐绑定0号地址或特殊IP
            //填充的时候IP是空的,就用INADDR_ANY否则用inet_addr
            if(bind(sock,(struct sockaddr*)&local,sizeof local)<0)
            {
                //走到这就绑定失败了,我们打印错误信息
                logMessage(FATAL,"bind error,%d:%s",errno,strerror(errno));
                exit(3);
            }
        }
        void Listen(int sock)//将套接字设置为listen状态
        {
            //因为TCP是面向连接的,当我们正式通信的时候需要先建立连接。
            if(listen(sock,gbacklog)<0)
            {
                logMessage(FATAL,"listen error,%d:%s",errno,strerror(errno));
                exit(4);
            }
            logMessage(NORMAL,"init server success");
        }
        //一般经验
        //const std;;string &::输入型参数
        //std::string *:输出型参数
        //std::string &:输入输出型参数
        int Accept(int listensock,std::string *ip,uint16_t *port)//获取连接
        {
            //从套接字中获取到客户端相关的信息
                struct sockaddr_in src;
                socklen_t len=sizeof(src);
                int servicesock=accept(listensock,(struct sockaddr*)&src,&len);
                if(servicesock<0)
                {
                    //获取连接失败
                    logMessage(ERROR,"accept error,%d:%s",errno,strerror(errno));
                    return -1;
                }
                if(port) *port=ntohs(src.sin_port);//如果port被设置,获取新的port
                //客户端端口号在src
                //由于是网络发送过来得套接字信息
                //所以要把信息进行网络转主机
                if(ip) *ip=inet_ntoa(src.sin_addr);//如果Ip被设置,获取新的Ip
                //我们需要将四字节网络序列的IP地址,转换成字符串风格的点分十进制的IP地址
                //到这里我们拿到了IP和端口号
                return servicesock;
        }
bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)//进行连接
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
        else return false;
    }
        ~Sock(){}
};

main.cc

#include"Sock.hpp"
int main()
{
    Sock sock;
    int listensock=sock.Socket();
    sock.Bind(listensock,8080);
    sock.Listen(listensock);
    while(true)
    {
        std::string clientip;
        uint16_t clientport;
        int sockfd=sock.Accept(listensock,&clientip,&clientport);
        if(sockfd>0)
        {
            std::cout<<"["<

        

你可能感兴趣的:(Linux,tcp/ip,服务器,网络)