天津做个网站需要多少钱,如何做网站水晶头,中企动力待遇怎么样,狂人采集器wordpress传输层协议—TCP协议 文章目录 传输层协议—TCP协议TCP协议段格式四位首部长度TCP协议如何根据目的端口号将数据传输给指定进程#xff1f;32位序列号和32位确认序列号可靠性问题 TCP报头标志位16位紧急指针16位检验和确认应答机制超时重传机制再谈三次握手四次挥手 连接管理机…传输层协议—TCP协议 文章目录 传输层协议—TCP协议TCP协议段格式四位首部长度TCP协议如何根据目的端口号将数据传输给指定进程32位序列号和32位确认序列号可靠性问题 TCP报头标志位16位紧急指针16位检验和确认应答机制超时重传机制再谈三次握手四次挥手 连接管理机制服务器状态转化客户端状态转化 解决TIME_WAIT状态引起的bind失败的方法流量控制滑动窗口拥塞控制延迟应答捎带应答面向字节流粘包问题TCP异常理解listen函数的第二个参数 TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名, 要对数据的传输进行一个详细的控制; 应用层将数据向下交付给传输层后其数据的发送和处理由传输层决定TCP是传输层协议即数据的发送和处理由TCP协议来决定因此TCP被称为传输控制协议。
TCP协议段格式 四位首部长度 四位首部长度标识报头大小。有时没有选项标准报头不含选项。因此标准报头大小为20字节。而四位首部长度范围为0000~1111即[0,15]。该范围并不能涵盖报头大小。因此报头大小4位首部长度*4字节。即tcp报头总长度范围[0,60]但标准报头占20字节因此tcp报头总长度范围[20,60]。超出20字节的部分即为选项部分。若此时报头大小为20字节此时4位首部长度为[0101]。
TCP协议如何根据目的端口号将数据传输给指定进程 在操作系统内以链表的形式对进程pcb做管理外还会以hash的方式对进程pcb做管理。以端口号为键值对进程pcb进行管理。接收到tcp报文后根据目的端口号去操作系统中管理进程pcb的hash数据结构中找到对应进程pcb。进程需要维护自己所管理的文件描述符表。进程pcb内有struct file*该指针指向进程文件描述符表。在进程文件描述符表内能够找到进程所管理的文件。文件内有自己的读写缓冲区。即在传输层中将tcp报头分离后直接将tcp的有效载荷发送到进程所管理的读写缓冲区。即以读取文件的方式读取到网络数据。
实际上tcp报头底层也是一个结构体其处理方式和处理udp方式大同小异。
struct tcp_hdr
{
uint32_t src_port;
uint32_t dst_port;
uint32_t req;
uint32_t ack_req;
uint32_t header_length;
......
};传输层接收到应用层向下交付的数据后。开辟一块空间在该空间内创建tcp_hdr结构体将结构体内的属性填充完整然后该空间应用层交付的数据tcp报文接着继续向下交付。
32位序列号和32位确认序列号 实际上不止TCP/IP四层模型中存在协议硬件中也存在协议。
内存和其他外设相连接的线称为IO总线。内存和CPU相连接的线称为系统总线。而设备之间通信必然是通过协议通信。而外设之间通信很少存在不可靠性问题其中原因包括外设之间距离狠心指令、数据传输不容易出现异常。而网络通信不是在本地单主机上通信而是主机之间通信而主机之间通信的桥梁是网络在该前提上数据通信就存在可靠性问题。
可靠性问题
网络通信不存在绝对的可靠性但存在相对的可靠性。 双方在通信历史信息的可靠性建立在收到响应的前提上。例如小蓝法信息给小绿“今天学习了吗”这条信息本身不具备可靠性。小绿回复“学了学了好久呢”即响应了小蓝发送给小绿的历史短信此时“今天学习了吗”这条信息才具备可靠性。历史的信息具备可靠性但最新消息不具备可靠性。小绿回复小蓝“忘了”该短信在没收到小蓝的回复之前都不具备可靠性。 通过32位序列号和32位确认序列号来直接确保可靠性 该场景没有涉及到超时重传机制只谈论序号和确认序号的作用
tcp报头中有序号和确认序号。序号标识发送出去的信息。由于tcp协议是无状态的因此需要序号标定状态和连续性。当收到报文时需要去查询报头的序号然后响应信息中包含确认序号。例如服务器收到客户端发送来的报文序号是10那么发送回去的响应的确认序号是11表示11之前的序号的报文接收成功且确认序号需要要求是连续的。例如客户端发送給服务器三条信息依次序号是101112但序号11标定的信息掉包了导致服务器没有收到因此服务器响应信息的确认序号只能是11。让客户端知道服务器没有收到序号为10之后的报文触发重传机制重新发送序号为10之后的报文。 而实际上客户端和服务器通信不是客户端发送一条请求给服务器服务器响应一个ack给客户端。而是并发式的相互交流。数据对于接收方而言数据乱序是一种不可靠的表现要通过序号来对数据进行排序保证数据的按序到达。即序号和确认序号保证了信息的顺序性和连续性。 tcp协议是全双工的因此需要两组序号来保证信息朝向的可靠性。在发送信息的同时给对方确认ack这样的报文提高了通信的效率。
TCP报头标志位
TCP报文具有类型区别区别在于其标志位的设置。实际上该标志位底层是位图若标志位需要被设置就由0置1。 SYN标志位标识请求报文 在三次次握手中客户端向服务器发送的请求报文中的YSN标志位就被置为1。 FIN标志位标识断开报文 在四次挥手中客户端向服务器发送挥手请求的报文中FIN标志位就被置为1。 ACK标志位标识确认报文 在网络通信中ACK标识的报文标识确认应答。 PSH标志位标识催促报文 客户端和服务器通信时可能会存在接收方处理数据不及时导致接收缓冲区满了发送方无法再次发送数据的情况。
由于TCP是全双工的通信双方都具备接收缓冲区和发送缓冲区。客户端向服务器发送数据服务器将受到的数据放到接收缓冲区。服务器上层调用read将数据从接收缓冲区读取到上层进行处理。会存在上层处理数据的速度慢客户端发送的数据快导致服务器的接收缓冲区早早满了。此时客户端再发送数据就会造成丢包问题而维护连接是需要消耗资源的通信双方不能由于不能发送数据而长期维护连接。
因此客户端可以将PSH标志位由0置1只将该标识PSH属性的报文发送给服务器通知催促服务器尽快处理数据给接收缓冲区腾出空间来接收新的报文。实际上报头的PSH标志位为1的报文都具备催促含义。 URG标志位标识需要紧急处理的数据 数据对于接收方而言数据乱序本身就是不可靠的表现。因此可以通过序号对报文标记对序号进行一定策略的排序保证数据的按序到达。而按序读取数据自然就产生了等待问题。对于某些需要特殊紧急处理的数据而言按序等待处理就成了问题。因此需要用URG标定报文含有需要紧急处理的数据即提示对方上层尽快将该数据读取进行处理。
实际上发送数据函数sendto就可以传递相关参数标识发送的报文具有需要紧急处理的含义 手册说明标志位 MSG_OOB
Sends out-of-band data on sockets that support this notion (e.g., of type SOCK_STREAM); the underlying protocol mustalso support out-of-band data.sendto的第四个参数传参MSG_OOB表示所发送的数据需要被紧急处理即out-of-band带外数据带外数据的处理策略与tcp流的完全分开的属于独立一套数据处理策略。
接收函数recv也可以传参MSG-OOB表示读取需要紧急处理的数据。 16位紧急指针 URG标定报文含有需要紧急处理的数据。16位紧急指针表示需要紧急处理的数据在有效载荷中的偏移量。
而该需要被紧急处理的数据大小只能为1字节即TCP的紧急指针只能传输1个字节的数据。 RST标志位标识复位发送给对方表示需要重置连接 客户端和服务器双方通信时先进行三次握手确定连接成功。然后客户端向服务发送数据。三次握手结束后服务器由于设备原因单方面掉线服务器重启后不再认定之前的三次握手即此时服务器单方面认为没有完成连接而客户端不知道服务器掉线因此单方面认为连接成功。然后客户端向服务器发送数据。服务器收到了客户端的数据但由于没有与该客户端建立好连接而收到了发送来的数据感到疑惑因此服务器需要发送一个具有RST标志位的报文给客户端表示重新与客户端进行三次握手即重置连接。
16位检验和
16位校验和: 发送端填充, CRC校验接收端校验不通过, 则认为数据有问题。此处的检验和不光包含TCP首部, 也包含TCP数据部分。
确认应答机制 在TCP协议中实际上通信双方的接收缓冲区和发送缓冲区是以数组的方式进行管理。数组天生具有下标即TCP报头的序号。而确认序号为序号1可以理解为以序号为下标之前的数据全部接收完毕下次发送数据给我以确认序号作为下标为起点往后开始发送。
超时重传机制 发送数据给对方对方超过一定时间没有响应应答自身重新发送数据给对方 客户端向服务器发送数据会有以下两种场景客户端认为服务器没有收到数据客户端重新发送数据给服务器。 场景一客户端向服务器发送数据数据在服务器收到之前丢包了即服务器没有收到数据因此服务器就没有向客户端响应ACK报文。经过一定时间后客户端触发超时重传机制重新向服务器发送数据。 场景二客户端向服务器发送数据服务器收到了数据向客户端响应了ACK报文但ACK报文掉包了即客户端没有收到服务器发送来的响应此时客户端会认为服务器没有收到数据经过一段时间后客户端会重新向服务器发送数据。 这个场景下服务器就会收到两份相同的报文收到重复的报文也是不可靠性的一种因此服务器需要对报文进行去重操作通过报文的序号进行去重。
客户端发送数据到重新发送数据期间有一个时间间隔。由于这两次发送的数据是相同的因此这份数据在收到应答之前应该在接收缓冲区中保存。客户端迟迟没有收到应答超过特定的时间间隔后重新发送数据给服务器。该决定超时重传机制的特定时间间隔不应该是固定的因为网络通信的时间长短不只由通信双方决定还由网络决定网络是变化的波动的因此可以认定该特定的时间间隔也是变化的波动的。Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时 时间都是500ms的整数倍。如果重发一次之后, 仍然得不到应答, 等待 2 * 500ms 后再进行重传。如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增。累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接。
再谈三次握手四次挥手 实际上三次握手所发送的是具备一定类型的TCP报头 客户端向服务器发送SYN实际上是发送一个TCP报头该TCP报头的SYN标记位被置为1表示请求。服务器响应客户端的SYNACK实际是发送一个TCP报头该报头的SYN和ACK标记位都被置为1表示请求和确认应答。客户端响应服务器ACK实际上是发送一个TCP报头该报头的ACK标记位被置为1表示确认应答。 三次握手不一定非得成功在三次握手中最后一个ACK才是最新消息因此前两条通信报文丢失了会触发重连或者重传而最后一个ACK就无法保证可靠性。 因此三次握手能够保证需要保证以下几点
客户端能够发送请求然后能够接收到服务器发送的响应即保证了客户端具备了发、收的能力。服务器接收到了客户端的第一通信即syn请求接收到了客户端第二通信即ack应答即保证了服务器也具备了发、收的能力。客户端和服务器都具备了发送数据、接收数据的能力才能保证tcp连接是全双工的。客户端先向服务器发送连接请求那么就要求客户端先确保能够建立好连接即优先确保客户端具备发送数据、接收数据的能力。服务器被客户端连接就要求服务器需要在客户端确保能够建立连接之后确保建立好连接。 建立连接的保证为什么是三次握手 操作系统中会有许多进程因此当该主机作为通信一方时操作系统中就会存在很多连接。连接是需要被管理起来的而维护一个连接是具有成本的。因此建立连接的方式尤为重要。一次握手就连接建立成功的话很容易导致恶意客户端多次向服务器发送syn请求建立多个连接导致服务器再也无法与其它需要建立连接的客户端通信即容易造成syn洪水问题。两次握手就建立成功的话当客户端发送出syn请求时保证了客户端建立好了连接服务器发送了ACKsyn响应时保证了服务器建立好了连接其原理跟一次握手无太大差异也容易造成syn洪水问题。三次握手是以最小成本验证全双工通信信道是通畅的。服务器受到攻击而三次握手是用来保证双工建立好连接的并不能解决受到攻击问题。
tcp通信需要建立连接建立连接保证了可靠性实际上连接并不能直接确保可靠性。经过三次握手后操作系统中会根据三次握手双方交互的信息建立连接结构体连接结构体能够保证连接管理机制、超时重连机制、流量控制等等这些机制直接保证了连接的可靠性。
连接管理机制 在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接。 服务器状态转化 [CLOSED - LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接。[LISTEN - SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文。[SYN_RCVD - ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了。[CLOSE_WAIT - LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)。[LAST_ACK - CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接。 客户端状态转化 [CLOSED - SYN_SENT] 客户端调用connect, 发送同步报文段。[SYN_SENT - ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据。[ESTABLISHED - FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入 FIN_WAIT_1。[FIN_WAIT_1 - FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段。[FIN_WAIT_2 - TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK。[TIME_WAIT - CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态。 通信双方经过三次握手后在操作系统中建立好了连接结构体。结构体内有位图记录连接的状态。
在四次挥手期间 断开连接是双方的事情需要征得双方同意。 先发送断开请求的一方进入FIN_WAIT 1状态。例如客户端主动与服务器断开连接客户端向服务器发送断开请求FIN后立刻进入FIN_WAIT 1状态。服务器收到断开请求后响应ACK然后进入CLOSE_WAIT状态。服务器在响应ACK后立刻发送FIN断开请求。客户端收到服务器发送过来的断开请求FIN立刻响应ACK报文同时进入TIME_WAIT状态。
总结一下
主动断开连接的一方四次挥手完成后最终状态是TIME_WAIT状态并维持一段是时间。被动断开连接的一方两次挥手完成后会进入CLOSE_WAIT状态。
实际上在客户端与服务器通信时可以让客户端主动与服务器断开连接然后让服务器不close sock也不退出进程那么服务器就处于CLOSE_WAIT状态。 httpserver.hpp #pragma once
#include iostream
#include string
#include cstring
#include cstdlib
#include unistd.h
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include sys/wait.h
#include signal.h
#include pthread.h
#include functional
#include protocol.hpp
#define NUM 1024
static const uint16_t gport 8080;
static const int gbacklog 5;
using namespace std;
namespace Server
{enum{USAGE_ERR 1,SOCK_ERR,BIND_ERR,LISTEN_ERR};class httpserver;using func_t functionbool(const HttpRequest , HttpResponse ); // 重定义func_tclass httpserver{public:httpserver(func_t func, const uint16_t port gport) : _port(port), _listensock(-1), _func(func) {}void inithttpserver(){// 1.创建套接字_listensock socket(AF_INET, SOCK_STREAM, 0);if (_listensock 0){exit(SOCK_ERR);}// 2.bind ip和portstruct sockaddr_in local;local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_listensock, (struct sockaddr *)local, sizeof(local)) 0) // 绑定失败{exit(BIND_ERR);}// 3.将套接字设置为监听模式if (listen(_listensock, gbacklog) 0){exit(LISTEN_ERR);}}void HandlerHttp(int sock){while(true){sleep(1);}}void start(){while (true){struct sockaddr_in cli;socklen_t len sizeof(cli);bzero(cli, len);int sock accept(_listensock, (struct sockaddr *)cli, len);if (sock 0){continue;}cout accept sock: sock endl;// 多进程版---pid_t id fork(); // 创建子进程if (id 0) // 子进程进入{close(_listensock); // 子进程不需要用于监听因此关闭该文件描述符if (fork() 0)exit(0);// //孙子进程HandlerHttp(sock); // 调用操作函数// close(sock);// exit(0);//不关闭sock也不退出进程}// 父进程close(sock); // 父进程不使用文件描述符就关闭waitpid(id, nullptr, 0);}}~httpserver() {}private:int _listensock; // 用于监听服务器的sock文件描述符uint16_t _port; // 端口号func_t _func;};}服务器不close文件描述符也不退出进程。 如果服务器出现大量的CLOSE_WAIT状态要么是服务器压力过大来不及执行close服务端还有数据没有推送完要么是你的close直接就是忘写了。 需要注意的是
断开连接的一方从TIME_WIAT状态到CLOSED状态会有一个超时机制该超时时间为2MSLMaximum Segment Lifetime—最长报文段寿命它是任何报文在网络上存在的最长的最长时间超过这个时间报文将被丢弃。。该时间设定为2MSL的原因有
**确保即使对方没有收到ACK也有足够的时间发送FIN使我方收到并且回复ACK让对方收到。**例如客户端主动向服务区断开连接。客户端经历三次挥手后进入TIME_WAIT状态并维持一段时间随后回复服务器ACK表示应答。然而由于网络问题使得该ACK丢包了服务器经过一段时间没有收到ACK应答后会触发重传机制从新向客户端发送FIN断开请求并等待客户端响应自己ACK应答。此时由于客户端没有立刻进入CLOSED状态还处于TIME_WAIT状态允许接收FIN请求并相应服务器ACK应答。**使得历史的滞留的报文消散。**例如客户端与服务器四次挥手中服务器向客户端一共发送了两次FIN请i去第一个请求丢失第二个到达了客户端。假如服务器向客户端发送的第一个报文没有丢失而是在某些网络节点停留了以至于延误到连接释放后的某个时间才到达客户端。这本来是已失效的报文段但客户端并不知道就会又建立一次连接。而等待的这2MSL就是为了解决这个问题的服务器在发送完最后一个确认报后在经过时间2MSL就可以使本链接持续时间内所产生的所有报文段都从网络中消失这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。
由于主动断开连接的一方最后会处于TIME_WAIT状态并维持一段时间。那么服务器主动断开连接时就不能立刻重启与客户端建立连接而是处于TIME_WAIT状态。而处于TIME_WAIT状态即说明TCP协议层的连接没有完全断开因此不能再次监听使用同样的端口号。
$ ./httpserverbind error:Address already in use可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout查看MSL的值。MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s。 解决TIME_WAIT状态引起的bind失败的方法 在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户 端来请求。这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产 生大量TIME_WAIT连接。由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip, 源端口, 目的ip, 目的端口, 协议). 其中服务器的ip和端口和协议是固定的. 如果新来的客户端连接的ip和 端口号和TIME_WAIT占用的链接重复了, 就会出现问题。
因此可以使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符。 socksetopt函数原型 #include sys/types.h
#include sys/socket.h
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);sockfd套接字描述符指定要设置选项的套接字。level选项的级别用于指定选项所属的协议族或套接字类型。常用的级别包括SOL_SOCKET通用套接字选项和IPPROTO_TCPTCP协议选项等。optname选项的名称用于指定要设置的具体选项。设置SO_REUSEADDR表示允许在套接字关闭后立即重用相同的地址和端口。optval指向存储选项值的缓冲区的指针。optlen选项值的长度。返回值返回值为0表示设置选项成功返回-1表示设置选项失败。 httpserver.hpp void inithttpserver(){// 1.创建套接字_listensock socket(AF_INET, SOCK_STREAM, 0);if (_listensock 0){exit(SOCK_ERR);}//2.1套接字关闭后立即重用相同的地址和端口int opt1;int ksetsockopt(_listensock,SOCK_STREAM,SO_REUSEADDR,opt,sizeof(opt));if(k0){perror(setsockopt error);exit(1);}// 2.2.bind ip和portstruct sockaddr_in local;local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_listensock, (struct sockaddr *)local, sizeof(local)) 0) // 绑定失败{exit(BIND_ERR);}// 3.将套接字设置为监听模式if (listen(_listensock, gbacklog) 0){exit(LISTEN_ERR);}}给setsockopt传参_listensock表示该参数为需要设置选项的套接字。传参SOCK _STREAM表示该套接字用于创建面向连接的可靠字节流套接字。传参选项SO_REUSEADDR表示允许在套接字关闭后立即重用相同的地址和端口。optval设置为非零值表示启用了该选项。 流量控制 接收端处理数据的速度是有限的如果发送端发的太快导致接收端的缓冲区被打满这个时候如果发送端继续发送就会造成丢包继而引起丢包重传等等一系列连锁反应。 因此TCP支持根据接收端的处理能力来决定发送端的发送速度这个机制就叫做流量控制Flow Control 、实际上在TCP首部有一个16位窗口大小该窗口大小存放了接收缓冲区大小字段。而16位数字最大表示64KB,因此TCP首部40字节的选项中还包含了一个窗口扩大因子M实际上窗口大小是窗口字段的值左移M位。
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端窗口大小字段越大, 说明网络的吞吐量越高发送端接受到这个窗口之后, 就会根据窗口大小去控制自己的发送的报文大小和发送速度如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数 据段, 使接收端把窗口大小告诉发送端。
滑动窗口 实际上通信双方并不是一条请求响应一个应答式的通信而是并发式的发送大量请求大量应答。而能够做到并发式的通信要基于滑动窗口机制。
由于TCP通信是全双工的因此通信双方在通信时会交换自己的接收缓冲区大小。这里以客户端发送数据服务器接收数据为例。客户端向服务器发送数据服务器响应应答。客户端发送数据的速度和数据大小基于服务器响应的接收缓冲区大小。在TCP协议中将发送缓冲区分为4个部分。 接收缓冲区会被分为已发送且已应答区域已发送但未应答区域未发送区域和没有数据即空区域。应用层自顶向下将数据写入到传输层的发送缓冲区由于前三个部分已存在数据因此是将数据写入到空区域。发送缓冲区自左向右将数据发送到通信对方的接收缓冲区。应用层自顶向下的将数据填充到空区域。 建模一数组 把接收缓冲区抽象成一个数组那么在滑动窗口的起始就可以有一个win_start指针作为起始点在末尾有一个win_end作为终点。滑动窗口是指在两个指针之间的区域。实际上win_start就是发送数据序号的起点win_end是发送数据序号的终点。滑动窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。
滑动窗口大小与以下几点相关
滑动窗口大小根据对方的接收缓冲区剩余大小有关。三次握手可以得知对方的16位窗口大小剩余接收缓冲区大小和确认序号。因此一开始win_start0win_endwin_starttcp_win(对方剩余接收缓冲区大小)。即未来滑动窗口怎么动使得对方都能够接收不会造成对方接收缓冲区满了还发送数据造成丢包问题。通信过后收到对方发送来的报文其中报头有确认序号ACK_SEQ该确认序号就是对方接收缓冲区下一次接收数据的起始地址。意味着小于该确认序号即之前的数据都接收且响应了此时win_startACK_SEQ(确认序号win_endwin_starttcp_win。因此窗口在滑动实际上是下标在进行更新。 滑动窗口可能会变大也可能会变小因此滑动窗口是动态变化的。例如对方上层一次性将接收缓冲区全部读取此时接收缓冲区空间因此发送过来的报文携带的tcp_win最大。也可能对方的接收缓冲区满了那么发送过来的报文携带的tcp_win0因此win_startwin_end即滑动窗口变小了。确认序号若为1001表示1001之前的所有数据都接收到且应答了若之前就丢包了那么响应的确认序号不会是1001而是在1001之前的确认序号且会触发重传机制。因此不需要担心存在滑动窗口中的两边数据对方收到但中间数据没有被对方收到的问题。划分发送出去但未收到应答的区域即滑动窗口是为了保证对方能够收到数据即使丢包也能够完成重传机制。 建模二环形队列 实际上发送缓冲区是一个环形队列。当应用层自顶向下填充数据到发送缓冲区中且空区域不够容纳数据时操作系统会将已发送且已应答区域划分为空区域将数据已覆盖式写入该区域。
拥塞控制
滑动窗口考虑到TCP通信双方但没有考虑到网络可能会出现问题。拥塞控制是用来解决TCP通信中网络出现问题的机制。
客户端向服务器发送数据发了1000条报文但服务器只收到了1条报文意味着999条报文丢失了。而大部分的报文丢失意味着可能并不是主机问题而是网络问题若通过重传机制重新发送报文这样会造成大量的报文在网络中堵塞只会加重网络的故障导致网络堵塞问题。因此应该不使用重传机制缓解网络的压力网络有自己的恢复机制等待网络的恢复。
TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据。 因此引入哟一个概念“拥塞窗口”该窗口是发送端根据网络接收能力定义的发送量。发送开始的时候, 定义拥塞窗口大小为1。每次收到一个ACK应答, 拥塞窗口加1。每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的16位窗口大小做比较, 取较小的值作为实际发送的窗口。即滑动窗口min(拥塞窗口接收端的16位窗口大小)
像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快。 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍。此处引入一个叫做慢启动的阈值ssthresh,即从指数增长到线性增长的阈值。当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长。 当TCP开始启动的时候, 慢启动阈值等于窗口最大值。 在ssthresh之前拥塞窗口已指数规律增长在ssthresh之后拥塞窗口以线性规律增长。 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1。 然后进行新一轮的拥塞窗口探测整个过程会呈现出周期性摆动但也不一定因为网络同时在波动因此整个过程就是一种探测行为。 少量的丢包, 我们仅仅是触发超时重传。而大量的丢包, 我们就认为网络拥塞。当TCP通信开始后, 网络吞吐量会逐渐上升。随着网络发生拥堵, 吞吐量会立刻下降。拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。该方法即保证了不失可靠性的同时提高了效率。
延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小。
例如
接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K。但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了。在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来。如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M。
总结一下延迟应答可以在上层处理数据极快的前提下扩大窗口大小即扩大每次通信的吞吐量。
窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输 效率。因此可以设定数量限制即每隔N个包应答一次也可以设定时间限制即超过最大延迟时间就应答一次。
捎带应答
在延迟应答的基础上我们发现客户端和服务器通信时发送数据是以一发一收的方式其实可以在发送数据时捎带ACK应答。 面向字节流
基于以上对TCP协议的学习现在重新认识一下字节流概念。 创建一个TCP的socket的同时在内核中创建一个发送缓冲区 和一个接收缓冲区。 调用write时, 数据会先写入发送缓冲区中。如果发送的字节数太长, 会被拆分成多个TCP的数据包发出。如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去。接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区。然后应用程序可以调用read从接收缓冲区拿数据。另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做全双工。 由于缓冲区的存在, TCP程序的读和写不需要一一匹配。 例如
写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节。读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read100个字节, 也可以一次 read一个字节, 重复100次。
粘包问题
首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包。而粘包问题即是读取到的不是一个完整的报文读到半个报文、一个半报文都是粘包问题。在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段标定数据报文的顺序性和独立性。站在接收方的传输层的角度, TCP是一个一个报文过来的按照序号排好序放在缓冲区中。站在接收方应用层的角度, 看到的只是一串连续的字节数据。因此应用程序看到了这么一连串的字节数据,就不知道哪个部分作为独立的数据报文,而是将一连串的字节数据看作是一个完整的应用层数据包。
要避免粘包问题就需要明确两个包之间的边界。
对于定长的包, 保证每次都按固定大小读取即可。对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置。也可以在包和包之间使用明确的分隔符(应用层协议, 是由我们自己来定的, 只要保证分隔符不和正文冲突即可)。对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在。同时, UDP是一个一个把数据交付给应用 层就有很明确的数据边界。站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收 不会出现半个的情况。而TCP是面向字节流的其接收端接收到报文是放到传输层的接收缓冲区任应用层读取不需要严格按照数据报文的规格读取而是自定义读取缓冲区的方式读取。
TCP异常
进程终止。操作系统会帮助进程的连接正常进行四次挥手从而断开连接。进程由于某些原因终止退出了此时进程的资源由操作系统回收。Linux下一切皆文件。连接也属于文件的一种也需要被操作系统管理因此进程终止了操作系统会正常给连接进行四次挥手关闭连接文件。机器重启。操作系统也会正常进行四次挥手关闭连接。客户端关机了操作系统会接管正在运行的进程的资源正常执行四次挥手关闭连接文件。机器掉电/网线断开。客户端并不知道网络掉线因此并不能做出对连接进行操作。而服务器端会定期询问客户端是否在线若在线会进行reset重新连接客户端不在线也会正常将连接关闭。
另外, 应用层的某些协议, 也有一些这样的检测机制例如HTTP长连接中, 也会定期检测对方的状态。 例如QQ, 在QQ 断线之后, 也会定期尝试重新连接。
理解listen函数的第二个参数 #include sys/types.h #include sys/socket.hint listen(int sockfd, int backlog);sockfd套接字描述符指定要设置为监听状态的套接字。backlog等待连接队列的最大长度。它指定了在调用accept之前可以排队等待连接的最大连接数。
在Linux系统中服务器所能接收的客户端全连接个数为backlog1。
服务器会将正在进行通信的连接放到连接队伍。当客户端和服务器需要进行连接个数超过backlog1时操作系统会将后来的连接放到半连接队伍。在半连接队伍中服务器会与客户端完成两次握手为短期内能够进入全连接队伍有空位提前做好准备。而在半连接队伍中超过一定时间后OS会自动断开与客户端的连接。 半链接队列用来保存处于SYN_SENT和SYN_RECV状态的请求。. 全连接队列accpetd队列用来保存处于established状态但是应用层没有调用accept取走的请求。