赞
踩
计算机网络知识扫盲:https://blog.csdn.net/hansionz/article/details/85224786
网络编程套接字:https://blog.csdn.net/hansionz/article/details/85226345
HTTP协议详解:https://blog.csdn.net/hansionz/article/details/86137260
前言:本篇博客介绍TCP协议和UDP
协议的各个知识点,这两个协议都是位于传输层的协议,我们首先从传输层谈起。
传输层: 传输层是TCP/IP协议五层模型中的第四层。它提供了应用程序
间的通信,它负责数据能够从发送端传输到接收端。其功能包括:一、格式化信息流;二、提供可靠传输。为实现后者,传输层协议规定接收端必须发回确认,并且假如分组丢失,必须重新发送。
再谈端口号: 在网络知识扫盲博客中谈到端口号标识了一个主机上进行通信的不同应用程序。在TCP/IP
协议中, 用"源IP", "源端口号", "目的IP", "目的端口号", "协议号"
这样一个五元组来标识一个通信
(可以通过 netstat -n查看,协议号指的是那个使用协议)。
一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定。
端口号范围划分:
HTTP、FTP、 SSH
等这些广为使用的应用层
协议他们的端口号
都是固定的,自己写的程序中,不能随意绑定知名端口号。端口号
。 客户端程序的端口号,就是由操作系统从这个范围分配
的。常见的知名端口号:
在Linux操作系统中使用命令cat /etc/services
可以看到所有的知名端口。
netstat工具: 用来查看网络状态。
Listen (监听)
的服务状态Socket
的程序识别码和程序名称tcp
相关选项udp
相关选项LISTEN
相关pidof [进程名]: 可以根据进程名直接查看服务器的进程id。例如:pidof sshd。
UDP协议报文格式:
(UDP首部+UDP数据)
的长度UDP协议的特点:
UDP
多长的报文, UDP原样发送既不会拆分,也不会合并。如果发送端调用一次sendto
, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个 字节,而不能循环调用10次recvfrom, 每次接收10个字节。所以UDP不能够灵活的控制读写数据的次数和数量。UDP的缓冲区:UDP存在接收缓冲区,但不存在发送缓冲区。
UDP没有发送缓冲区,在调用sendto
时会直接将数据交给内核,由内核将数据传给网络层协议
进行后续的传输动作。为什么UDP不需要发送缓冲区? 因为UDP不保证可靠性,它没有重传机制,当报文丢失时,UDP不需要重新发送,而TCP不同,他必须具备发送缓冲区,当报文丢失时,TCP必须保证重新发送,用户不会管,所以必须要具备发送缓冲区。
UDP具有接收缓冲区,但是这个接收缓冲区
不能保证收到的UDP报文的顺序和发送UDP报的顺序一致,如果缓冲区满了再到达的UDP数据报就会被丢弃。
UDP接收缓冲区和丢包问题:https://blog.csdn.net/ljh0302/article/details/49738191
UDP是一种全双工
通信协议。 UDP协议首部中有一个16位的大长度. 也就是说一个UDP能传输的报文长度是64K
(包含UDP首部)。如果我们需要传输的数据超过64K, 就需要在应用层手动的分包
, 多次发送, 并在接收端手动拼装。
常见的基于UDP的应用层协议:
配置
协议解析
协议TCP全称传输控制
协议,必须对数据的传输进行控制。
TCP协议报文格式:
源端口号/目的端口号:表示数据从哪个进程
来,要到那个进程去
32位序号:序号是可靠传输
的关键因素。TCP将要传输的每个字节都进行了编号
,序号是本报文段发送的数据组的第一个
字节的编号,序号可以保证传输信息的有效性。比如:一个报文段的序号为300,此报文段数据部分共有100字节,则下一个报文段的序号为401。
32位确认序号:每一个ACK
对应这一个确认号,它指明下一个期待收到的字节
序号,表明该序号之前的所有数据已经正确无误的收到。确认号只有当ACK
标志为1时才有效。比如建立连接时,SYN报文的ACK标志位为0。
4位首部长度(数据偏移): 表示该TCP头部有多少个32位bit
(有多少个4字节),所以TCP头部大长度是15 * 4 = 60
。根据该部分可以将TCP报头和有效载荷分离。TCP报文默认大小为20个字节。
6位标志位:
URG:它为了标志紧急指针是否有效。
ACK:标识确认号是否有效。
PSH:提示接收端应用程序立即将接收缓冲区的数据拿走。
RST:它是为了处理异常连接的, 告诉连接不一致的一方,我们的连接还没有建立好, 要求对方重新建立连接。我们把携带RST标识的称为复位报文段。
SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段。
FIN:通知对方, 本端要关闭连接了, 我们称携带FIN标识的为结束报文段。
紧急
指针:按序到达是TCP协议
保证可靠性的一种机制,但是也存在一些报文想优先被处理,这时就可以设置紧急指针
,指向该报文即可,同时将紧急指针有效位置位1
。大量数据
,接收方接收不过来,会导致大量数据丢失。然后接收方可以发送给发送发消息让发送方发慢一点,这是流量控制
。接收方将自己接收缓冲器
剩余空间的大小告诉发送方叫做16位窗口大小
。发送发可以根据窗口大小来适配
发送的速度和大小,窗口大小最大是2的16次方,及64KB,但也可以根据选项中的某些位置扩展,最大扩展1G。CRC
校验。如果接收端校验不通过, 则认为数据有问题(此处的检验和不光包含TCP首部
也包含TCP数据部分
)。确认应答机制:
接收端收到一条报文后,向发送端发送一条确认ACK
,此ACK的作用就是告诉发送端:接收端已经成功的收到了消息,并且希望收到下一条报文的序列号
是什么。这个确认号就是期望的下一个报文
的序号。
每一个ACK都带有对应的确认序列号
,意思是告诉发送者,我们已经收到了哪些数据,下一个发送数据应该从哪里开始。 如上图,主机A给主机B发送了1-1000的数据,ACK应答,携带了1001序列号。告诉主机A,我已经接受到了1-1000数据,下一次你从1001开始发送数据。
超时重传:
TCP在传输数据
过程中,还加入了超时重传
机制。假设主机A发送数据给主机B,主机B没有收到数据包,主机B自然就不会应答,如果主机A在一个特定时间间隔内没有收到主机B发来的确认应答,就会进行重发,这就是超时重传机制
。
当然还存在另一种可能就是主机A
未收到B
发来的确认应答,也可能是因为ACK
丢失了。
因此主机B会收到很多重复
数据,那么TCP协议需要能够识别出那些包是重复的包
, 并且把重复的包丢弃掉,这时候我们可以利用前面提到的16位序列号
, 就可以很容易做到去重
的效果。
超时重发的时间应该如何确定?
在理想的情况下,可以找到一个小的时间
来保证 "确认应答"一定能在这个时间内返回。但是这个时间的长短,随着网络环境
的不同是有差异的。如果超时时间设的太长,会影响整体的重传
效率。如果超时时间设的太短,有可能会频繁发送重复
的包。TCP
为了保证无论在任何环境下都能比较高性能
的通信,因此会动态计算
这个最大超时时间。
Linux中超时时间以500ms
为一个单位进行控制,每次判定超时重发的超时时间
都是500ms
的整数倍。如果重发一次之后,仍然得不到应答,等待2*500ms
后再进行重传。如果仍然得不到应答,等待4*500ms
进行重传。依次类推,以指数形式
递增,当累计到一定的重传次数,TCP
认为网络或者对端主机出现异常
,强制关闭连接。
在正常情况下, TCP要经过三次握手
建立连接,四次挥手
断开连接。
三次握手及四次挥手:https://mp.csdn.net/mdeditor/86495932
TIME_WAIT状态: 当我们实现一个TCP服务器时,我们把这个服务器运行起来然后将服务器关闭掉,再次重新启动服务器会发现一个问题:就是不能马上再次绑定这个端口号和ip,需要等一会才可以重新绑定,其实等的这一会就是TIME_WAIT
状态。
主动关闭连接
的一方要处于TIME_ WAIT
状态,等待两个MSL
的时间后才能回到CLOSED
状态。Ctrl-C
终止了server
,server是主动关闭连接的一方在TIME_WAIT期间仍然不能再次监听同样的server
端口。MSL
在RFC1122
中规定为两分钟(120s)
,但是各操作系统的实现不同,在Centos7
上默认配置的值是60s
可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout
查看MSL
的值。为什么TIME_WAIT时间一定是2MSL:
首先,TIME_WAIT
是为了防止最后一个ACK
丢失,如果没有TIME_WAIT
,那么主动断开连接的一方就已经关闭连接,但是另一方还没有断开连接,它收不到确认ACK
会认为自己上次发送的FIN
报文丢失会重发该报文,但是另一方已经断开连接了,这就会造成连接不一致的问题,所以TIME_WAIT
是必须的。
MSL
是TCP报文
在发送缓冲区的最大生存
时间,如果TIME_WAIT
持续存在2MSL
的话就能保证在两个传输方向上的尚未被接收或迟到的报文段
都已经消失。(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的)。同时也是在理论上保证最后一个报文
可靠到达。(假设最后一个ACK丢失, 那么服务器会再重发一个FIN,这时虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK,这就会导致问题)
解决TIME_WAIT状态引起的bind失败的方法:
在server
的TCP连接
没有完全断开之前不允许重新绑定,也就是TIME_WAIT
时间没有过,但是这样不允许立即绑定在某些情况下是不合理的:
大量
的客户端的连接 (每个连接的生存时间可能很短,但是每秒都有很大数量的客户 端来请求)主动关闭连接
(比如某些客户端不活跃,就需要被服务器端主动清理掉),这样服务器端就会产生大量TIME_WAIT
状态TIME_WAIT
的连接数很多,每个连接
都会占用一个通信五元组(源ip, 源端口, 目的ip, 目的端口, 协议)
。其中服务器的ip和端口和协议是固定的,如果新来的客户端连接的ip和端口号
和TIME_WAIT
占用的连接重复就造成等待。解决方法:使用setsockopt()
设置socket
描述符的选项SO_REUSEADDR
为1,表示允许创建端口号
相同但IP地址
不同的多个socket描述符。
关于setsockopt
:https://www.cnblogs.com/clschao/articles/9588313.html
服务器端CLOSE_WAIT状态: 如果客户端是主动断开连接的一方,在服务器端假设没有关闭新连接,这时服务器端就会产生一个CLOSE_WAIT
状态,因为服务器没有去关闭连接,所以这个CLOSE_WAIT
状态很容易测试出来,这时四次挥手没有结束,只完成了两次。
#include "tcp_socket.hpp" typedef void (*Handler)(string& req, string* res); class TcpServer { public: TcpServer(string ip, uint16_t port) :_ip(ip) ,_port(port) {} void Start(Handler handler) { //1.创建socket listen_sock.Socket(); //2.绑定ip和端口号 listen_sock.Bind(_ip, _port); //3.监听 listen_sock.Listen(5); while(1) { TcpSocket new_sock; string ip; uint16_t port; //4.接收连接 listen_sock.Accept(&new_sock, &ip, &port); cout <<"client:" << ip.c_str() << " connect" << endl; while(1) { //5.连接成功读取客户端请求 string req; bool ret = new_sock.Recv(&req); cout << ret << endl; if(!ret) { //此处服务器端不关闭新连接,导致CLOSE_WAIT状态 //new_sock.Close(); break; } //6.处理请求 string res; handler(req, &res); //写回处理结果 new_sock.Send(res); cout << "客户:" << ip.c_str() << " REQ:" << req << ". RES:" << res << endl; } } } private: TcpSocket listen_sock; string _ip; uint16_t _port; };
运行结果:
如果服务器上出现大量的CLOSE_WAIT
状态,原因就是服务器没有正确的关闭 socket
,导致四次挥手没有正确完成。这是可能是一个BUG,只需要加上对应的 close
即可解决问题。
滑动窗口:
确认应答策略对每一个发送的数据段都要给一个ACK确认应答
,接收方收到ACK
后再发送下一个数据段
,但是这样做有一个比较大的缺点,就是性能较差
,尤其是数据往返的时间较长的时候。
既然一发一收
的方式性能较低,那么我们考虑一次发送多条
数据,就可以大大的提高性能,它是将多个段的等待时间重叠
在一起。
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值
。上图的窗口大小就是4000
个字节(四个段)。发送前四个段的时候,不需要等待任何ACK
直接发送即可。当收到第一个ACK后滑动窗口向后
移动,继续发送第五个段
的数据,然后依次类推。操作系统内核为了维护这个滑动窗口
,需要开辟发送缓冲区
来记录当前还有哪些数据没有应答
。只有确认应答过的数据,才能从缓冲区删掉
,窗口越大,则网络的吞吐率
就越高。滑动窗口左边代表已经发送过并且确认,可以从发送缓冲区中删除了,滑动窗口里边代表发送出去但是没有确认,滑动窗口右边代表还没有发送的数据。
如果在这种情况中出现了丢包现象,应该如何重发呢?
数据到达接收方,但是应答报文丢失:可以更具后边的ACK确认。假设发送方发送1-1000的数据,接收方收到返回确认ACK,但是返回的ACK丢失了,另一边发送1001-2000收到的确认ACK 2001,就可以认为1-1000数据接收成功 。
数据包之间丢失: 当某一段报文段丢失之后,发送端会一直收到 1001
这样的ACK
,就像是在提醒发送端 "我想要的是 1001"
一样,如果发送端主机连续三次
收到了同样一个"1001"
这样的应答,就会将对应的数据 1001 - 2000
重新发送,这个时候接收端收到了 1001
之后, 再次返回的ACK就是7001
了。因为2001 - 7000
接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中。这种机制被称为 “高速重发控制”(也叫 "快重传"
)。
快重传要求接收方在收到一个失序
的报文段后就立即
发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带
确认。快重传算法规定,发送方只要一连收到三个重复
确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间
到期。由于不需要等待设置的重传计时器到期,能尽早重传未被确认的报文段,能提高整个网络的吞吐量
。
流量控制
接收端处理数据
的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被装满,这个时候如果发送端继续发送,就会造成丢包
,然后引起丢包重传等等一系列连锁反应。因此TCP
支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)
。
缓冲区大小
放入TCP首部中的"窗口大小"
字段,通过ACK
确认报文通知发送端吞吐量
越高,接收端一旦发现自己的缓冲区
快满了,就会将窗口大小设置成一个更小的值
通知给发送端减慢
自己的发送速度,如果接收端
缓冲区满了, 就会将窗口置为0
。这时发送方不再发送数据
,但是需要定期发送一个窗口
探测数据段,使接收端把窗口大小
告诉发送端。接收端如何把窗口大小告诉发送端呢? 在的TCP
首部中,有一个16位窗口
字段,就是存放了窗口大小
信息,16位数字大表示65535
,那么TCP
窗口大就是65535
字节吗? 实际上TCP
首部40字节
选项中还包含了一个窗口扩大因子M
,实际窗口大小是 窗口字段的值左移M
位。接收端窗口如果更新,会向发送端发送一个更新通知,如果这个更新通知在中途丢失了,会导致无法继续通信,所以发送端要定时发送窗口探测包。
拥塞控制:
虽然TCP
有了滑动窗口这个大杀器能够高效可靠
的发送大量的数据,但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题,因为网络上有很多的计算机,可能当前的网络状态
就已经比较拥堵,在不清楚当前网络状态下,贸然发送大量
的数据是很有可能引起雪上加霜的,造成网络更加堵塞
。
TCP引入慢启动
机制,先发少量的数据探探路,摸清当前的网络拥堵状态
,再决定按照多大的速度传输数据。
图中的cwnd
为拥塞窗口,在发送开始的时候定义拥塞窗口
大小为1,每次收到一个ACK应答拥塞窗口加1
。每次发送数据包的时候,将拥塞窗口和接收端主机
反馈的窗口大小做比较,取较小的值
作为实际发送的窗口。
像上面这样的拥塞窗口增长速度,是指数级别的。"慢启动"
只是指初使时慢,但是增长速度非常快。为了不增长的那么快,因此不能使拥塞窗口
单纯的加倍,此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候,不再按照指数方式增长, 而是按照线性方式
增长。
TCP
开始启动的时候,慢启动阈值
等于窗口最大值超时重发
的时候,慢启动阈值
会变成原来的一半同时拥塞窗口置回1
少量的丢包,我们仅仅是触发超时重传
。大量的丢包
,我们就认为网络拥塞
。当TCP
通信开始后,网络吞吐量
会逐渐上升。随着网络发生拥堵
,吞吐量会立刻下降。拥塞控制归根结底是TCP协议
想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
拥塞控制与流量控制的区别:
拥塞控制是防止过多的数据
注入到网络中,可以使网络中的路由器或链路不致过载,是一个全局性
的过程。 流量控制是点对点
通信量的控制,是一个端到端
的问题,主要就是权衡发送端发送数据
的速率,以便接收端
来得及接收。
拥塞控制的标志:
计时器
超时三个
重复确认拥塞避免:(按照线性规律增长)
避免拥塞
,在拥塞避免阶段将拥塞窗口
控制为按线性规律
增长,使网络比较不容易出现拥塞。cwnd
缓慢地增大,即每经过一个往返时间
RTT就把发送方的拥塞控制
窗口加一。无论是在慢开始阶段
还是在拥塞避免阶段
,只要发送方判断网络出现拥塞
(其根据就是没有收到确认,虽然没有收到确认
可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞
来处理),这时就把慢开始门限
设置为出现拥塞
时的门限的一半。然后把拥塞窗口
设置为1,执行慢开始
算法。
拥塞避免
算法后,拥塞窗口线性缓慢
增大,防止网络过早出现拥塞ssthresh
减半快恢复(与快重传配合使用)
快恢复
算法时,慢开始只在TCP
连接建立时和网络出现超时
时才使用。三个重复
确认时,就执行“乘法减小”
算法,把ssthresh
门限减半。但是接下去并不执行慢开始算法。拥塞
的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞
。所以此时不执行慢开始算法,而是将cwnd
设置为ssthresh
的大小,然后执行拥塞避免
算法。延迟应答
如果接收数据
的主机立刻返回ACK
应答,这时候返回的窗口
可能比较小。假设接收端缓冲区为1M
一次收到了500K
的数据。如果立刻应答,返回的窗口就是500K
。 但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了,在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些也能处理过来。如果接收端稍微
等一会再应答,比如等待200ms
再应答,那么这个时候返回的窗口大小就是1M
。
窗口越大,网络吞吐量
就越大,传输效率
就越高。我们的目标是在保证网络不拥塞
的情况下尽量提高传输效率。
N个包
就应答一次延迟时间
就应答一次注:具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2
, 超时时间取200ms
捎带应答:
在延迟应答
的基础上,存在很多情况下,客户端服务器在应用层也是"一发一收"
的。 意味着客户端给服务器说了"How are you"
, 服务器也会给客户端回一个"Fine, thank you"
。那么这个时候ACK
就可以搭顺风车,和服务器回应的 "Fine, thank you"
一起回给客户端
面向字节流:
当我们创建一个TCP
的socket
,同时在内核中创建一个发送缓冲区
和一个接收缓冲区
。
write
时,内核将数据会先写入发送缓冲区
中,如果发送的字节数
太长,会被拆分成多个TCP
的数据包发出,如果发送的字节数太短,就会先在缓冲区
里等待, 等到缓冲区
长度达到设置长度,然后等到其他合适的时机
发送出去。read
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区
。然后应用程序可以调用read
从接收缓冲区拿数据。TCP的一个连接,既有发送缓冲区, 也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据。所以是全双工的。由于缓冲区
的存在,TCP程序的读和写
不需要一一匹配。例如: 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节; 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次 read一个字节, 重复100次
粘包问题:
粘包问题中的 "包"
是指的应用层的数据包。在TCP
的协议头中,没有如同UDP
一样的 "报文长度"
这样的字段,但是有一个序号
这样的字段。站在传输层
的角度, TCP
是一个一个报文过来的,按照序号
排好序放在缓冲区
中,但是站在应用层
的角度,它看到的只是一串连续
的字节数据。应用程序
看到了这么一连串的字节数据, 就不知道从哪个部分
开始到哪个部分结束是一个完整的应用层数据包,这就是粘包问题
。
如何避免粘包问题呢?明确两个包之间的边界
定长
的包,保证每次都按固定大小
读取即可。例如一个Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可变长
的包,可以在包头
的位置,约定一个包总长度
的字段,从而就知道了包的结束位置
。变长
的包,还可以在包和包
之间使用明确的分隔符
(应用层协议是程序员自己来定义的, 只要保证分隔符不和正文冲突即可)。对于UDP协议,如果还没有上层交付数据
, UDP
的报文长度仍然在。 同时UDP
是一个一个
把数据交付给应用层,这样就有存在明确
的数据边界,站在应用层
的角度, 使用UDP
的时候要么收到完整的UDP
报文要么不收,不会出现"半个"
的情况。
TCP连接异常情况:
释放文件描述符
,仍然可以发送FIN
,和正常关闭没有什么区别。机器重启和进程终止一样。连接
还在,一旦接收端
有写入操作,接收端发现连接已经不在了,就会进行reset
。即使没有写入操作,TCP自己也内置了一个保活定时器
,会定期询问对方是否还在。如果对方不在,也会把连接释放。应用层的某些协议, 也有一些这样的检测机制.例如HTTP长连接中, 也会定期检测对方的状态.Q在QQ 断线之后, 也会定期尝试重新连接。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。