网络编程|网络基本功(TCP报文及可靠性保证)

目录
1、概念了解
2、Socket通信的基本流程
3、TCP报文协议格式
4、TCP的三次握手
5、TCP的四次挥手
6、TCP的数据重传
7、滑动窗口
8、其他
9、小结
10、代码实现
1、概念了解 HTTP (hypertest transfer protocol) http协议是建议在tcp协议 之上,工作在应用层 ;是web互联的基础,也是手机联网常用的协议之一。 UDP: 建立在IP协议之上,工作在传输层;效率高,不可靠,允许丢失和重发;常用于视屏聊天,丢帧可以接受; TCP: 建立在IP协议之上,工作在传输层;可靠传输协议,三次握手;常用于文件可靠传输; IP协议: IP协议是TCP/IP 的核心协议,工作在网络层,所有的TCP.UDP.ICMP.IGMP都是按照这种格式发送数据的,IP提供了一种“ 尽力而为”的服务; SOCKET套接字:是支持 TCP/IP协议的网络通信的基本单元, scoket是通信的基石。 网络通信的五元素:他是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:

  • 连接使用的协议;
  • 本地主机的ip地址;
  • 本地进程的协议端口;
  • 远地主机的ip地址;
  • 远地进程的协议端口;
通信方式 :单工,半双工,全双工。 网络编程|网络基本功(TCP报文及可靠性保证)
文章图片
2、Socket通信的基本流程 网络编程|网络基本功(TCP报文及可靠性保证)
文章图片
3、TCP报文协议格式 网络编程|网络基本功(TCP报文及可靠性保证)
文章图片
TCP常见flag:
  • URG 当为1时,用来保证TCP连接不被中断, 并且将该次TCP内容数据的紧急程度提升(就是告诉电脑,你丫赶快把这个给resolve了)
  • ACK 用来应答的,通常是服务器端返回的。 用来表示应答是否有效。 1为有效,0为无效
  • PSH 表示,当数据包得到后,立马给应用程序使用(PUSH到最顶端)
  • RST 用来确保TCP连接的安全。 该flag用来表示 一个连接复位的请求。 如果发生错误连接,则reset一次,重新连。当然也可以用来拒绝非法数据包。
  • SYN 用来同步的, 通常是由客户端发送,用来建立连接的。第一次握手时: SYN:1 , ACK:0. 第二次握手时: SYN:1 ACK:1
  • FIN 用来表示是否结束该次TCP连接。 通常当你的数据发送完后,会自动带上FIN 然后断开连接
  • Seq:包头的序列号;
4、TCP的三次握手 思考:Tcp为啥是3次,不是2次或者4次? 三次握手过程(Three-wayhandshake)。在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。 (1). 第一次握手: 建立连接时,客户端发送SYN包((SYN=i)到服务器,并进入SYN SEND状态,等待服务器确认; (2) . 第二次握手: 服务器收到SYN包,必须确认客户的SYN (ACK=i+1 ),同时自己也发送一个SYN包((SYN=j)}即SYN+ACK包,此时服务器进入SYN_RECV状态; (3). 第三次握手: 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ACK=j+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手,客户端与服务器开始传送数据。 三次的原因:
  • (1)只有三次握手才能保证,客户端和服务端的相互确认,从而进行可靠的数据传输;
  • (2)防止实效的连接被server接受,产生较多的无效的半连接;
因为两次握手对于客户端是OK的,两次握手后处于Established状态,但是对于服务端若客户端发送的请求超时了,或者是客户端进行恶意攻击,发起很多无效的请求,此时服务端都是Establish,造成大量的无效半连接状态,造成资源浪费。
  • (3)三次是保证客户端和服务端进行可靠传输的最低值,大于三次就多余了。
5、TCP的四次挥手 思考:为什么需要4次挥手? 因为tcp是全双工工作模式,既可以接受也可以发送,所以为了 数据的完整性,需要将a ck和 fin分两次来进行.
  • 第一步:客户端发起关闭,只能证明客户端没有数据要发送了;
  • 第二步:server端收到fin信号,回复一个ack表示收到关闭通知了,但是server端可能还有数据要发送;
  • 第三步:server端发送数据完毕,发送fin信号告诉客户端可以关闭了;
  • 第四步:client收到fin信号后等待2ms后关闭;
四次挥手保证了两端数据传输的完整性和可靠性。 6、TCP的数据重传 TCP片段重传:主要用到一个TCP片段重传 超时计时器以及 重传队列。 检测丢失片段并对之重传的方法概念是很简单的:每次发送一个片段,就开启一个重传计时器。计时器有一个初始值,并随时间递减。如果在片段接收到确认之前计时器超时,就重传片段。 tcp使用了这一基本技术,但实现方法稍有不同,原因在于为了提高效率需要一次处理多个未被确认的片段,以保证每一个在恰当的时间重传。 TCP按照以下特定顺序工作: 放置于重传队列中,计时器开始,包含数据的片段一经发送,片段的一份复制就放在名为重传队列的数据结构中,此时启动重传计时器,因此,在某些时间点,每一个片段都会放在队列里。队列按照重传计时器的剩余时间来排列,因此TCP软件可追踪那几个计时器在最短时间内超时。 确认处理:如果在计时器超时之前收到了确认消息,则该片段从重传队列中删除。 重传超时:如果在计时器超时之前没有收到确认消息,则发生重传超时,片段自动重传。当然,相比于原片段,对于重传片段并没有更多的保护机制。因此,重传之后该片段还是保留在重传队列里。重传计时器被重启,冲洗开始倒计时; 如果重传之后没有收到确认,则片段会再次重传并重复这个过程。在某次而情况下重传也会失败,我们不想要TCP永远重传下去,因此TCP只会重传一定数量的次数,并判断出现故障终止连接。 TCP是积累确认机制 但是我们怎么知道一个片段被完全确认呢:重传是基于片段的,而TCP确认该信息是基于序列号积累的。每次当设备A发送片段给设备B时,设备B查看该片段的确认号字段。所有低于该字段的序列号都已经被设备A接收了。因此当片段中所发送的所有字节序列号都比设备A到设备B的最后一个确认号小的时候,一个从设备B发到设备A的片段被认为是确认了。这是通过计算片段中最后一个序列号结合片段的数据字段来实现的。 TCP为什么会重传: tcp是一种可靠的协议,在网络交互中,TCP报文是封装在IP协议中的,IP协议的无连接性导致其可能在交互的过程中丢失,在这种情况下,TCP协议如何保障其传输的可靠性呢?TCP通过在发送数据报文时设置一个超时定时器来解决这种问题,如果在定时器溢出时还没有收到来自对端对发送报文的确认,它就重传该数据报。 导致重传的情况: 1.数据报在传输中丢失,发送端的数据报文在网络传输中,被中间链路或中间设备丢弃,因为IP使用的四个关键技术:服务类型,生存时间,选项和包头校验,其中的生存时间是数据报可以生存的时间上限。他由发送者设置,由经过路由的地方处理,如果未到达时生存时间为0,则抛弃该数据,所以导致该数据丢失。 2.接收端的ACK确认报文在传输途中途丢失。也就是说发送端发送的数据已经被接收端接收,接收端也针对接收到的报文发送了相应的ACK,但是该ACK确认报文被中间链路或中间设备丢弃了,这时候会导致文件的重传,最后TCP也会对就收到的报文进行整理,抛弃重复的数据; 3.接收端异常未响应ACK或被接收端丢弃。发送端发送的数据报文到达了接收端,但是,接收端由于种种原因,直接忽略了该数据报文,或者接收到报文但并没有发送针对该报文的ACK确认报文。 报文重传的次数: TCP报文重传的次数也根据系统设置的不同而有区分,一个报文只会被重传3次,如果重传三次后还未收到该报文的确认,那么就不在尝试重传,直接reset重置该TCP连接。 重传主要保障了业务的可靠性,另一方面也反应了网络的通讯状态; 如何判断一个报文是重传报文:
  • (1)序列号突然下降,一般是TCP重传,因为重传是基于积累确认机制,当发现某个数据包丢失后,所有其后面的数据包都不会被确认接收,所以必须重传该数据包,那么它的序列号一定比某些数据包的序列号小。
  • (2)如果发现某一时刻出现了两个相同的数据包,包括数据的长度,序列号甚至应用数据一样,则说明,该数据包是重传数据包。
7、滑动窗口 TCP的滑动窗口主要有两个作用, 一是提供TCP的可靠性,二是提供TCP的流控特性。同时滑动窗口机制还体现了TCP面向 字节流的设计思路。 滑动窗口:
  • “滑动”则是指这段“允许发送的范围”是可以随着发送的过程而变化的,变化的方式就是按顺序“滑动”;
  • “窗口”对应的是一段可以被发送者发送的字节序列,其连续的范围称之为“窗口”;
滑动窗口协议是传输层进行流控的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的速度,从而达到防止发送方发送速度过快导致网络故障的目的。 滑动窗口的基本原理 TCP使用窗口机制进行流量控制。当连接建立后,发送方和接收方都要分配一块自己的缓冲区来存储接收的数据,为了防止接收方已经没有缓冲区进行接收而发送方在在继续发送,出现网络拥塞和故障,所以用滑动窗口进行了流控。 具体实现:接收方将缓冲区剩余的尺寸和期待接收的下一个字节序号在确认信息中发送给发送方,发送方根据接收方剩余的大小空间来定量的发送合适的数据给接收方,它也是建立在“确认重传”的基础之上。 滑动窗口的原理图如下所示: 网络编程|网络基本功(TCP报文及可靠性保证)
文章图片
网络编程|网络基本功(TCP报文及可靠性保证)
文章图片
滑动窗口动态效果演示: https://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/index.html 对ACK的认识 ACK通常被理解为收到数据或某种请求后发送一个ACK确认信息,其实ACK包含两个重要的信息: 一、期望接收到的下一个字节的序号n,该n代表接收方已经接收到了前n-1个字节,此时如果接收到的是n+1而不是n个字节,接收方式不会发送n+2的ACK的。如上图所示,如果收到发送端发来的5,6,7两个字节,而没有收到4这个字节接收端是不会发送ACK=8的,而依旧发送ACK=4,因为这样就会激活超时消息重传功能,确保4能正常的接收。 二、当前的窗口大小m,如此发送方在接收到ACK包含的这两个数据后就可以计算出还可以发送多少个字节给对方,假定当前发送方已经发送到第n个字节,期待下一个要发送的字节的序号为X,则发送端可以发送的字节数为:send=m-(n-X); 窗口的大小属性 TCP的window是一个16bit位字段,它代表的是窗口的字节容量,也就是TCP标准窗口最大为2^16-1=65535个字节;另外在TCP的选项字段中还包含了一个TCP窗口的扩展因子,option-kind为:3,option-length为3个字节,option-data取值范围0-14。窗口扩大因子用来扩大TCP窗口,可把原来的16bit的窗口,扩大为31bit。 发送窗口和接收窗口: TCP是双工的协议,会话的双方都可以同时接收、发送数据。TCP会话的双方都各自维护一个“发送窗口”和一个“接收窗口”。其中各自的“接收窗口”大小取决于应用、系统、硬件的限制,但是tcp的传输速率不能大于应用的数据处理能力,否则就会缓冲区淹没。各自的“发送窗口”则取决于对端通告的“接收窗口”,也就是取决于接收的缓冲区的大小。 发送窗口只有在收到对端对于本段发送窗口内字节的ACK确认,才会移动发送窗口的左边界。 接收窗口只有在前面的所有数据确认收到的情况下,才会移动左边的窗口。当在前面还有未接受的字节,而后面的字节已接收的情况下,窗口也不会移动,并不会对后续的字节进行确认。一次确保对端会对这些未收到的数据进行重传。主要涉及到接收端的累积确认接收机制。 8、其他 (1)Dos攻击 或者 SYN攻击? DenialOfService 拒绝服务攻击: 利用大量无效的ip请求,第二次握手后,服务端处于SYN_RECV状态,server不断的超时重发确认,造成第三次握手失败创造大量的半连接请求,导致半连接队列被无效请求占满,致使正常的syn请求被拒绝,目标系统运行变慢,引起网络拥塞及系统瘫痪。 监测方法:
  • 当服务器上有大量的半连接状态的连接,就说明遭遇了syn攻击了;
实例举例: 街头的餐馆是为大众提供餐饮服务,如果一群地痞流氓要DoS餐馆的话,手段会很多,比如霸占着餐桌不结账,堵住餐馆的大门不让路,骚扰餐馆的服务员或厨子不能干活,甚至更恶劣。 (2)为什么 TIME_WAIT 状态需要经过 2MSL(最大报文段生存时间)才能返回到 CLOSE 状态? 答:虽然按道理,四个报文都发送完毕,我们可以直接进入 CLOSE 状态了,但是我们必须假 象网络是不可靠的,有可以最后一个 ACK 丢失。所以 TIME_WAIT 状态就是用来重发可能丢 失的 ACK 报文。 (3)、前端的优化方案? a. 减少http请求个数, 合并js,css代码(我们把js会合一个文件) 如果项目比较大,则需要合并多个。 b. 静态资源 cdn加速 CDN(Content Delivery Network)即内容分发网络,减少网络的传输距离,从而达到加速的目的。 c. 反向代理 设置缓存nginx,提高响应速度,静态资源的配置。图片等 d. 使用浏览器 缓存,配置 Etag,Expireshttp头信息,减少http 的请求次数; e. 避免 重定向; f. 数据的 压 缩 gzip
  • http的header头数据的压缩:content-encoding:gzip;
  • node端很简单,只要加上compress模块即可;
  • nginx:gzip on 打开即可
9、小结 tcp的可靠性保证: 1. 确认连接;TCP和UDP就像是打电话和发短信,打电话就是必须等待对方接电话,并且确认是我们要找的那个人才开始交谈正事,否则无法进行。发短信就是我不用管对方能不能看,网络时候通畅,信息已经发出去了,对于结果是未知的!确认连接通过TCP的三次挥手和四次握手; 2 .消息重传:TCP是一种可靠的连接机制,但是TCP的报文是封装在IP协议中的,IP协议的责任就是将数据从原地址发送到目的地址,选择传送道路,也即路由功能,不保证消息传递的可靠性,tcp则利用了超时定时器和重传队列以及积累确认机制来实现丢失数据的重传功能,一保证数据的安全准确到达; 3. 滑动窗口:TCP的滑动窗口主要有两个作用:保证TCP传输的数据顺序和可靠,二是提供TCP的流控特性; 4. 拥塞避免算法机制防止网络拥塞; 10、代码实现 服务器端实现:
#include #include #include #include #include #include #include #define BACKLOG 5//完成三次握手但没有accept的队列的长度 #define CONCURRENT_MAX 8//应用层同时可以处理的连接 #define SERVER_PORT 11332 #define BUFFER_SIZE 1024 #define QUIT_CMD ".quit" int client_fds[CONCURRENT_MAX]; int main(int argc, const char * argv[]) { int i; char input_msg[BUFFER_SIZE]; char recv_msg[BUFFER_SIZE]; //本地地址 struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); bzero(&(server_addr.sin_zero), 8); //创建-socket int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0); if(server_sock_fd == -1) { perror("socket error"); return 1; }//绑定-socket int bind_result = bind(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); if(bind_result == -1) { perror("bind error"); return 1; }//侦听-listen if(listen(server_sock_fd, BACKLOG) == -1) { perror("listen error"); return 1; }//fd_set fd_set server_fd_set; int max_fd = -1; struct timeval tv; //超时时间设置 while(1) { tv.tv_sec = 20; tv.tv_usec = 0; FD_ZERO(&server_fd_set); FD_SET(STDIN_FILENO, &server_fd_set); if(max_fd0) { int index = -1; for( i = 0; i < CONCURRENT_MAX; i++) { if(client_fds[i] == 0) { index = i; client_fds[i] = client_sock_fd; break; } } if(index >= 0) { printf("新客户端(%d)加入成功 %s:%d\n", index, inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port)); } else { bzero(input_msg, BUFFER_SIZE); strcpy(input_msg, "服务器加入的客户端数达到最大值,无法加入!\n"); send(client_sock_fd, input_msg, BUFFER_SIZE, 0); printf("客户端连接数达到最大值,新客户端加入失败 %s:%d\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port)); } } } for( i =0; i < CONCURRENT_MAX; i++) { if(client_fds[i] !=0) { if(FD_ISSET(client_fds[i], &server_fd_set)) { bzero(recv_msg, BUFFER_SIZE); //处理某个客户端过来的消息 long byte_num = recv(client_fds[i], recv_msg, BUFFER_SIZE, 0); if (byte_num > 0) { if(byte_num > BUFFER_SIZE) { byte_num = BUFFER_SIZE; } recv_msg[byte_num] = '\0'; printf("客户端(%d):%s\n", i, recv_msg); } else if(byte_num < 0) { printf("从客户端(%d)接受消息出错.\n", i); } else { FD_CLR(client_fds[i], &server_fd_set); client_fds[i] = 0; printf("客户端(%d)退出了\n", i); } } } } } } return 0; }

客户端实现:
#include #include #include #include #include #include #include #define BUFFER_SIZE 1024int main(int argc, const char * argv[]) { struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(11332); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); bzero(&(server_addr.sin_zero), 8); int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0); if(server_sock_fd == -1) { perror("socket error"); return 1; } char recv_msg[BUFFER_SIZE]; char input_msg[BUFFER_SIZE]; if(connect(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) == 0) { fd_set client_fd_set; struct timeval tv; while(1) { tv.tv_sec = 20; tv.tv_usec = 0; FD_ZERO(&client_fd_set); FD_SET(STDIN_FILENO, &client_fd_set); FD_SET(server_sock_fd, &client_fd_set); select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv); if(FD_ISSET(STDIN_FILENO, &client_fd_set)) { bzero(input_msg, BUFFER_SIZE); fgets(input_msg, BUFFER_SIZE, stdin); if(send(server_sock_fd, input_msg, BUFFER_SIZE, 0) == -1) { perror("发送消息出错!\n"); } } if(FD_ISSET(server_sock_fd, &client_fd_set)) { bzero(recv_msg, BUFFER_SIZE); long byte_num = recv(server_sock_fd, recv_msg, BUFFER_SIZE, 0); if(byte_num > 0) { if(byte_num > BUFFER_SIZE) { byte_num = BUFFER_SIZE; } recv_msg[byte_num] = '\0'; printf("服务器:%s\n", recv_msg); } else if(byte_num < 0) { printf("接受消息出错!\n"); } else { printf("服务器端退出!\n"); exit(0); } } } } return 0; }

资料参考:
【网络编程|网络基本功(TCP报文及可靠性保证)】《TCP/IP详解》

    推荐阅读