TCP三次握手

    TCP面向连接单播,通过三次握手建立连接,四次挥手关闭一个连接。通信双方必须建立一条连接(所谓连接其实就是 内存内存一份关于对方的IP 端口信息)。TCP可视为字节流,会处理IP层或以下层的丢包重复以及错误,在连接建立过程中,双方需要交换一些连接的参数 这些参数可以放在TCP头部。
    tcp头部结构
    0 15 16 31
    |16位源端口号 | 16位目的端口号.|
    32 63
    | 32位序号 |
    64 95
    | 32位确认号 |
    96 99 100 105 106 107 108 109 110 111 112 127
    |四位头部长度|6位保留|URG|ACK|PSH|RST|SYN|FIN|16位窗口大小|
    128 143 159
    | 16位校验和 | 16位紧急指针|
    160
    | 选项最多40字节 |
    序列号(SN):标识当前TCP报文中的第一个字节在对应方向的传输中对于的字节序号。一次TCP通信(从TCP连接建立到断开)过程中会对某一个传输方向上的字节流中的每一个字节编号,例如A要向B发送一个包含500000字节的文件,并且规定最大报文段长度MSS为1000字节,数据流首字节编号为0,该TCP将为数据流构建500个报文段,给第一个报文段分配序列号0(0-999),第二个报文段分配序列号1000(1000-1999),第3个报文段分配序列号2000…。通过序列号TCP接收端可以识别出重复接受到的TCP包,从而丢弃重复包,同时对于乱序数据包也可以依靠序列号进行重排序组合。
    当SYN标志位有效时此次报文的序列号也叫做ISN(initial sequence number) ISN会用一种随机算法来计算 不一定是0, 如果AB之间的新旧连接使用了相同的端口号,如果用0这种顺序ISN可能会出现 已终止的连接(上次连接)的报文 对当前连接产生影响 如果用时间产生随机数来做ISN 这种情况就可以避免。

    确认序号(ACK):期望收到对方下一个报文段的第一个字节的序号,举个例子 假设A已经收到了来自B编号为0-535的所有字节,之后A打算发一个报文给B,则A会在其确认序号字段填上536。再举一个例子,A已经收到了来自B包含字节0-535的报文段,以及另一个包含字节900-1000的报文段。A为了重构B的字节流,A需要含536-899的B报文,所以在A向B发报文段的确认序号会填536(TCP只确认流中到第一个丢失的字节为止之前的字节 即只会确认到535)。900-1000这个报文段又叫失序报文段,如何处理这种报文段,程序员可以自己规定,可以丢弃这个报文段,也可以保留并等待缺失的报文(536-899)(实际中一般采用后面这种)。

    首部长度(数据偏移):以4字节位单位,最大1111— TCP首部最长154字节60字节
    保留:6位 保留为今后使用 目前置为0
    确认ACK: 为1 ,表示确认号字段有效,TCP规定在建立连接(三次握手后)后所传达的所有报文段都必须把ACK置为1
    RST:置1 复位相应的TCP连接,标识要求对方重新建立连接
    同步SYN:*仅在三次握手建立TCP连接时有效
    ,当SYN=1 ACK=0时表示这是一个连接请求报文段,对方若同意建立连接,则应在响应报文中令SYN=1 ACK=1表示同意建立连接。即SYN=1表示这是一个连接请求或连接接受报文。
    终止FIN:置1 表明此报文段的发送方的数据已经发送完毕,并要求释放这个连接关系
    窗口:指发送本报文段的这方的接收窗口(而不是自己的发送窗口),是TCP流量控制的一个手段,它告诉对方本端TCP接收缓冲区还能容纳多少字节的数据,这样对方可以控制发送数据的速度。
    校验和:检验首部和数据两部分。在计算校验和时需要加上12字节的伪头部

    选项中有一个四字节的MSS,mss字段在TCP数据包的首部,是告知对方自己能一次能接收的最大数据部分的长度(不含头部长度),MSS只出现在SYN数据报中。

    客户端在connect函数的底层会做3次握手的操作
    图片1.png
    第一次握手,建立连接时客户端先发SYN=1,序列号为x(ISN)的包给服务器,并且进入SYN_SENT状态(连接请求已发送),等待服务器确认。(客户端能确认自己的发ok)
    第二次握手,服务器收到SYN=1的包,必须确认客户端SYN包(确认ACK要置为1 ),同时自己也发送一个SYN包(SYN要置为1 ),服务器进入SYN_RECV状态表示同步收到。(收到信息服务器能确认自己的收ok,客户端发ok 服务器发信息 能确认自己发ok) 服务器在收到SYN包后会为该TCP连接发配缓存和变量。
    前两次握手报文不能携带数据
    第三次握手,客户端收到服务器的SYN=1,ACK=1的包向服务器发送确认包(ACK=1),此包发送完毕,客户端和服务器进入ESTABLISHED 已建立连接状态。(客户端收到 确认自己收ok以及服务器发ok 服务器收到ack后确认客户端收ok)
    客户端在收到服务器的应答报文后,会为该TCP连接分配缓存和变量。
    第三次握手报文可以携带数据

    序列号(SN):标识当前TCP报文中的第一个字节在对应方向的传输中对于的字节序号。一次TCP通信(从TCP连接建立到断开)过程中会对某一个传输方向上的字节流中的每一个字节编号,例如A要向B发送一个包含500000字节的文件,并且规定最大报文段长度MSS为1000字节,数据流首字节编号为0,该TCP将为数据流构建500个报文段,给第一个报文段分配序列号0(0-999),第二个报文段分配序列号1000(1000-1999),第3个报文段分配序列号2000…。通过序列号TCP接收端可以识别出重复接受到的TCP包,从而丢弃重复包,同时对于乱序数据包也可以依靠序列号进行重排序组合。
    当SYN标志位有效时此次报文的序列号也叫做ISN(initial sequence number) ISN会用一种随机算法来计算 不一定是0, 如果AB之间的新旧连接使用了相同的端口号,如果用0这种顺序ISN可能会出现 已终止的连接(上次连接)的报文 对当前连接产生影响 如果用时间产生随机数来做ISN 这种情况就可以避免。

    确认序号(ACK):期望收到对方下一个报文段的第一个字节的序号,举个例子 假设A已经收到了来自B编号为0-535的所有字节,之后A打算发一个报文给B,则A会在其确认序号字段填上536。再举一个例子,A已经收到了来自B包含字节0-535的报文段,以及另一个包含字节900-1000的报文段。A为了重构B的字节流,A需要含536-899的B报文,所以在A向B发报文段的确认序号会填536(TCP只确认流中到第一个丢失的字节为止之前的字节 即只会确认到535)。900-1000这个报文段又叫失序报文段,如何处理这种报文段,程序员可以自己规定,可以丢弃这个报文段,也可以保留并等待缺失的报文(536-899)(实际中一般采用后面这种)。

    A收到B序列号为900长度为101字节(900-1000)的报文,于是A给B发的报文中把确认号设置为1000+1=1001

    SYN泛洪攻击
    在第二次握手中,服务器收到客户端发来的SYN报文会为这条TCP连接分配变量和内存。如果之后客户端不发送第三次握手的SYN ACK报文来完成第三次握手,最终(一分半左右)服务器将终止该连接并挥手资源。SYN泛洪攻击指的是,攻击者作为客户端 发送大量TCP SYN报文段消耗服务器的连接资源,但是不完成第三步握手, 大量的连接请求将会导致服务器的资源消耗殆尽。使用一种叫SYN cookie的方法来进行防御
    在服务器接受到SYN报文后不知道是合法用户还是SYN泛洪攻击的一部分,因此服务器不会为这个连接分配变量和内存(不生成半开连接),但服务器为这个连接生成一个TCP ISN(初始序列号 序列号由SYN报文中的源和目的IP 以及一个仅该服务器知道的秘密数计算生成的),这个ISN被称为SYN cookie,但是服务器并不记忆该cookie 以及任何与此SYN报文相关的信息。服务器以此ISN为序列号发送三次挥手中的第二步 SYN ACK报文。
    如果对方没有回ACK报文,则没关系因为没有为之前的SYN报文分配资源。
    如果对方回了ACK报文(合法用户),这个合法的ACK报文中的ack如果是正确的化因该是SYN cookie+1,服务器由这个客户端ACK报文的的源和目的IP 以及一个仅该服务器知道的秘密数计算生成的)去重算cookie,如果这个cookie == 此报文的ack-1则服务器认为此ACK确实是之前的那个SYN ACK是正常的,服务器再为这个连接分配资源。

    服务器接收到一个TCP报文其端口号和源IP与此服务器上运行的TCP连接的所有套接字都不匹配,则服务器会向源发送一个特殊重置报文(该报文RST置为1),相当于告诉源我没有那个报文对应的TCP套接字,不要再发送该报文段了。
    此机制可被攻击者用来测试服务器上哪些端口没有被防火强隔绝,攻击者向服务器发送某个端口的SYN报文,会有三种结果
    1 攻击者收到服务器的SYN ACK报文 这意味着服务器上确实有TCP节点在这个端口运行
    2 服务器接收到RST报文,这意味着SYN报文确实到达了服务器,并且没有TCP节点在使用我们发送的端口。但攻击者至少知道该报文没有被服务器的任何防火墙拦截。
    3 攻击者什么也没收到,则报文很可能被防火墙拦截了。

    TCP滑动窗口
    滑动窗口是一种流量控制计数,早期的网络通信中,通信双方不会考虑网络的拥挤情况,直接发送数据,由于大家都不知道网络的拥塞状况,同时发送数据导致中间节点阻塞掉包,谁也发不了数据。滑动窗口协议是用来改善吞吐量的技术,允许发送方再接收任何应答之前传送附加的包,,接收方告诉发送方再某时刻能发送

    TCP连接的两端都有对应的发送缓存和接收缓存,TCP收到数据后存入接收缓存,相应的进程去接收缓存中取数据,如果进程长时间不去接收缓存中取数据将导致接收缓存区溢出。TCP提供了流量控制服务,以消除接收方溢出的可能性,流量控制服务是个速度匹配服务,匹配接收方接收速率的发送方发送速率。(TCP发送方也可能因为网络的拥塞而被遏制,这方面的控制叫拥塞控制)

    假设下面的TCP过程中丢弃失序的报文段,TCP通过让发送方一个称为接收窗口的TCP头变量来进行流量控制。
    接收方会在头中存入当前接收缓存还有多少空间可用来提醒发送端。

    举个例子 A向B发送一个大文件B为A向B的TCP连接分配了一个接收缓存,用RECVBUFFER来表示其大小(B的接收缓存)。B进程不时地从缓存中读数据
    定义以下变量
    LAST_BYTE_READ B上的应用程序从接收缓存读出的数据流的最后一个字节编号
    LAST_BYTE_RCVD 从网络中到达B并被B的TCP将数据流放入接收缓存的最后一个字节编号(接收缓存中的最新的一个字节的编号)

    LAST_BYTE_RCVD - LAST_BYTE_READ<=RECVBUFFER 为了保证缓存不溢出需要满足

    当前滑动窗口的大小为 rwnd = RECVBUFFER - (LAST_BYTE_RCVD - LAST_BYTE_READ)B余下的接收缓存空间还有多少 ,B必须实时地跟踪LAST_BYTE_RCVD,LAST_BYTE_READ这两个变量的变化。并且将rwnd的大小存入TCP头来告诉A还余下多少空间。

    主机A也需要跟踪两个变量
    LAST_BYTE_SEND A发送的最后一个字节的编号
    LAST_BYTE_ACKED B确认收到的最后一个字节的编号
    LAST_BYTE_SEND - LAST_BYTE_ACKED就是A已经发出去了但还未被确认接收到的字节数
    只要A保证在连接过程中 LAST_BYTE_SEND - LAST_BYTE_ACKED<=rwnd B的接收缓存就不会溢出

    注意一个细节,当B rwnd=0接收缓存已满,在TCP将rwnd=0告诉A后 并且之后B没有任何数据要发给A,B上的进程慢慢将读缓存中数据读出来,此时如果TCP并不向主机A发送带有rwnd新值的报文(实际上TCP仅当在它有数据或有确认报文要发时才会发送报文段给另一方),A是不可能知道B的接收缓存又有空间了,继续再向B发数据。 此时A相当于被阻塞无法再发出数据。
    所以TCP规范中规定:当B的rwnd=0,主机A会继续发送只有一个字节数据的报文段 要B应答,在B的缓存开始清空 B将会应答新的rwnd信息给A。

    TCP四次挥手

    TCP连接中两端的任何一个进程都能终止该连接,当连接结束后TCP连接所用到的缓存和变量将被释放。举个例子
    1客户端进程发出连接释放报文,并停止发送数据。释放报文的首部FIN被置为1,释放报文可以携带数据,并且消耗一个序号seq=u为上次接收的最后一个字节编号+1。此时客户端进入FIN_WAIT_1状态。
    2服务器收到连接释放报文,发出确认报文ACK置为1,ack=u+1并带上自己的序列号seq=v,此时服务器进入CLOSE_WAIT状态,TCP向应用程序报告客户端申请释放连接了,客户端没有数据要发送了,但是再CLOSE_WAIT时间内服务器发送数据客户端依然要接收。
    3客户端收到服务器的确认报文后,客户端进入FIN_WAIT_2状态,等待服务器发送释放报文。在FIN_WAIT_2期间客户端依然要接受服务器发送的数据。
    4服务器在CLOSE_WAIT(FIN_WAIT_2)期间内将最后的所有数据发送完后,向客户端发送连接释放报文,FIN被置为1,ack=u+1,seq=w(w=v+剩余数据的大小),在释放报文发送完毕后服务器进入LAST_ACK最后确认状态等待客户端的确认。
    5客户端收到服务器的释放报文后,必须发出确认报文,ACK置为1 swq = u+1 ack=w+1
    此后客户端进入TIME_WAIT状态,注意此时TCP连接还没有释放,必须经过2*MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCP后 客户端进入CLOSED状态。
    客户端的释放ACK报文丢失(当客户端又收到服务器的FIN报文 则客户端知道之前的ACK报文丢失 会重传ACK并再次等待2MSL) 在TIME_WAIT状态内会重传释放ACK报文
    6服务器只要收到客户端发出的撤销确认报文后,立即撤销TCP 结束这次的TCP连接进入CLOSED状态
    可以看出服务器结束的时间比客户端早了一点
    图片2.png
    如果已经建立了连接,但是客户端突然出现故障了怎么办?
    TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

    多进程TCP并发

    多客户端连接服务器
    父进程等待并接收(accept)客户端的连接,每接受到一个连接fd 开一个子进程去通信的执行业务代码

    1. /* tcp服务器
    2. socket()->bind()->listen()->accept()->{recv send}->recv->close
    3. */
    4. #include <arpa/inet.h>
    5. #include <stdio.h>
    6. #include <unistd.h> //read write
    7. #include <string.h> // strlen
    8. #include <stdlib.h>
    9. #include <signal.h>
    10. #include <sys/wait.h>
    11. #include <errno.h>
    12. void sig_handler(int num);
    13. int main()
    14. {
    15. //1 创建用于监听的套接字
    16. int sock_listen = socket(PF_INET, SOCK_STREAM, 0); //ipv4 AF_INET SOCK_STREAM, 0tcp协议
    17. //socket fd 并非真实文件 而是内核中的一块读写缓冲区
    18. if (sock_listen == -1)
    19. {
    20. perror("socket:");
    21. exit(-1);
    22. }
    23. //2 绑定 socket 绑定 服务器ip和端口
    24. struct sockaddr_in sa_sever;
    25. sa_sever.sin_family = AF_INET; //ipv4协议族 用AF_INET也一样
    26. inet_pton(AF_INET, "192.168.91.0", &sa_sever.sin_addr.s_addr); //网络字节序整型ip
    27. // sa_sever.sin_addr.s_addr = 0;//INADDR_ANY = 0代表 0.0.0.0代表任意地址 其实是代表服务器上的多个网卡的ip地址 通过这多个网卡地址 都能访问服务器
    28. sa_sever.sin_port = htons(9999); //网络字节序整型端口 主机字节序转网络字节序
    29. int ret = bind(sock_listen, (struct sockaddr *)&sa_sever, sizeof(sa_sever));
    30. if (ret == -1)
    31. {
    32. perror("bind:");
    33. exit(-1);
    34. }
    35. //3 监听
    36. // cat / proc / sys / net / core / somaxconn 一个端口的最大监听队列的长度
    37. // 一个端口最多可以监听多少个可能连接的客户端 4096 将传入的socket变为被动的socket(socket函数创建的都是主动型的)
    38. // 这个socket将被accept函数用于监听是否有对应的连接请求(listen不阻塞 accept阻塞)
    39. // sockfd socket函数得到的文件描述符
    40. // backlog sockfd对应的等待连接队列的最大长度
    41. // 如果客户端请求时,服务器的等待连接队列满了,客户端的connect将会得到一个ECONNERFUSED错误。
    42. // 这个最大队列长度一般设定为 未连接(未完成三次握手队列) + 已连接队列(以完成三次握手队列)
    43. // 的最大长度 未连接队列最大长度 cat / proc / sys / net / ipv4 / tcp_max_syn_backlog
    44. // 成功返回0 失败返回 - 1
    45. ret = listen(sock_listen, 128);
    46. if (ret == -1)
    47. {
    48. perror("listen:");
    49. exit(-1);
    50. }
    51. //信号回调来实现子进程资源回收!!!
    52. sigset_t set;
    53. sigemptyset(&set);
    54. sigaddset(&set, SIGCHLD); //为了防止 回调函数还没注册成功 就已经有子进程死亡了 先阻塞SIGCHLD信号
    55. sigprocmask(SIG_BLOCK, &set, NULL);
    56. //捕捉子进程死亡时发送的SIGCHILD信号
    57. //注册SIGCHLD的信号的回调函数
    58. struct sigaction act;
    59. act.sa_flags = 0; //表示使用sa_handler来进行函数回调
    60. act.sa_handler = sig_handler;
    61. sigaction(SIGCHLD, &act, NULL);
    62. sigemptyset(&act.sa_mask); //清空临时阻塞信号集表示 在执行回调函数时不阻塞任何信号
    63. //注册完回调函数后 解除SG的阻塞
    64. sigprocmask(SIG_UNBLOCK, &set, NULL);
    65. //4 多个等待客户端连接 阻塞
    66. while (1) //主线程就用来等待和连接客户端 以及回收子进程资源
    67. {
    68. struct sockaddr_in client_addr; //用于接收客户端的socket地址信息
    69. socklen_t len = (socklen_t)sizeof(client_addr);
    70. int sock_com = accept(sock_listen, (struct sockaddr *)&client_addr, &len); //返回用于和客户端通信的 sockfd
    71. //注意 等待连接的过程中 如果有信号中断了系统调用(accept) 将会产生错误accept将不再阻塞 return EINTR错误!!!
    72. if (sock_com == -1)
    73. {
    74. if (errno == EINTR) //accept被信号中断才产生的错误 不用管继续等待连接
    75. continue;
    76. perror("accept:");
    77. exit(-1);
    78. }
    79. //为新的客户端连接 创造子线程 专用于与那个客户端通信
    80. pid_t pid = fork();
    81. if (pid == 0) //子进程
    82. {
    83. //输出连接上的客户端信息
    84. //accept得到的客户端信息
    85. char client_ip[16];
    86. inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)); //网络字节序的整型 需要先转成主机字节序的字符串
    87. unsigned short client_port = ntohs(client_addr.sin_port); //网络字节序整型 转为主机字节序整型
    88. printf("client ip:%s port:%d\n", client_ip, client_port); //客户端因为不需要bind所以客户端的os会随机为客户端分配一个端口 通信双方不需要端口相同 端口号只是标识了是那个ip主机的哪个进程在通信
    89. //5 通信
    90. //获取客户端数据
    91. char recvbuf[1024] = {0};
    92. while (1)
    93. {
    94. int len_read = read(sock_com, recvbuf, sizeof(recvbuf)); //无数据 阻塞
    95. if (len_read == -1)
    96. {
    97. perror("read:");
    98. exit(-1);
    99. }
    100. else if (len_read == 0)
    101. {
    102. //客户端断开连接 写端关闭 则读端读返回0
    103. printf("client closed\n");
    104. }
    105. else if (len_read > 0)
    106. printf("recv from client %s\n", recvbuf);
    107. //向客户端发送数据 回射数据
    108. write(sock_com, recvbuf, strlen(recvbuf) + 1); //+1将/0也发送过去
    109. }
    110. close(sock_com);
    111. exit(0); //子进程结束 不可能正常结束因为是while1 也没有检测键盘
    112. }
    113. else if (pid == -1) //fork失败
    114. {
    115. perror("fork:");
    116. exit(-1);
    117. }
    118. }
    119. //6 关闭文件sock描述符
    120. // close(sock_com);
    121. close(sock_listen);
    122. return 0;
    123. }
    124. void sig_handler(int num)
    125. {
    126. printf("捕获到的信号编号为:%d\n", num); //num=14 SIGALRM
    127. //回收子进程资源
    128. if (num == SIGCHLD)
    129. {
    130. //wait(NULL); //回收子进程资源
    131. //20个信号这样 并不能完全回收
    132. //当多个信号连续到来会造成后面来的信号被忽略
    133. //在执行相依信号的回调函数体时 那个信号是被阻塞的
    134. //,所以当子进程死亡 发出多个SG信号 SG信号都是未决的 只有第一个信号
    135. //(1-31)信号集无法记录目前有多少个xxx信号未决或阻塞,
    136. //只能知道目前有xxx信号未决或阻塞。---不支持排队
    137. // 从开始注册信号到注册成功这段时间里,有n个SIGCHID信号产生的话,
    138. // 那么第一个产生的SIGCHID会抢先将未决位置为1,余下的n-1个SIGCHID被丢弃,
    139. // 然后当阻塞解除之后,信号处理函数发现这时候对应信号的未决位为1,继而执行函数处理该信号,
    140. // 处理函数中的while循环顺带将其他n-1子进程也一网打尽了,在这期间未决位的状态只经历了两次变化,即0->1->0
    141. while (1) // 一次信号触发为了防止 有连续的SG信号到来 有的信号没看到
    142. //这里用个循环回收连续(很快)死亡的子进程的资源
    143. {
    144. //死循环 快速地检查是否有子进程死亡 快速地回收
    145. //回调函数 占用的是父进程的资源 相当于暂时中断 去处理这个信号 处理完回到父进程
    146. //waitpid 和 SIGCHLD 没关系,即使是某个子进程对应的 SIGCHLD 丢失了,
    147. //只要父进程在任何一个时刻调用了 waitpid,那么这个进程还是可以被回收的
    148. int ret = waitpid(-1, NULL, WNOHANG); //回收所有子进程资源 不阻塞 一次调用回收一个子进程资源
    149. if (ret > 0) //回收到了一个子进程
    150. printf("child %d die\n", ret);
    151. else if (ret == 0) //还有子进程 在运行
    152. break; //中断结束 回到父进程
    153. else if (ret == -1) //无子进程在运行
    154. break; //中断结束 回到父进程
    155. }
    156. }