文章目录
1.延迟确认应答
接收数据的主机如果每次都立刻回复确认应答的话,可能会返回一个较小的窗口。那是因为刚接收完数据,缓冲区已满。
当某个接收端收到这个小窗口的通知以后,会以它为上限发送数据,从而又降低了网络的利用率(这其实是窗口控制特有的问题,专门术语叫做糊涂窗口综合征(SWS:Silly Window Syndrome)。) 。为此,引入了一个方法,那就是收到数据以后并不立即返回确认应答,而是延迟一段时间的机制。
窗口越大(接收缓冲区大小),网络的吞吐量就越大,效率就越高,我们的目标是保证在网络不拥塞的情况下,尽量的提高传输的效率
延迟应答的两种方法:
1.数量限制:每隔N个包就应答一次
2.时间限制:超过最大延迟时间就应答一次
具体时间和超时时间,依据操作系统的不同也有差异;
一般N取2,超时时间为0.2s(这个时间越小、CPU的负荷会越高,性能也下降。反之,这个时间越长,越有可能触发发送主机的重发处理,而窗口为只有1个数据段的时候,性能也会下降)
事实上,大可不必为每一个数据段都进行一次确认应答。TCP采用滑动窗口的控制机制,因此通常确认应答少一些也无妨。TCP文件传输中,绝大多数是每两个数据段返回一次确认应答
2.捎带应答
根据应用层协议,发送出去的消息到达对端,对端进行处理以后,会返回一个回执
在此类通信当中,TCP的确认应答(将ACK标志位置为1即可)和回执数据可以通过一个包发送。这种方式叫做捎带应答。通过这种机制,可以使收发的数据量减少。
捎带应答是指在同一个TCP包中既发送数据又发送确认应答的一种机制。由此,网络的利用率会提高,计算机的负荷也会减轻。不过,确认应答必须得等到应用处理完数据并将作为回执的数据返回为止,才能进行捎带应答。
接收数据以后如果立刻返回确认应答,就无法实现捎带应答。而是将所接收的数据传给应用处理生成返回数据以后进再进行发送请求为止,必须一直等待确认应答的发送。也就是说,如果没有启用延迟确认应答就无法实现捎带应答。延迟确认应答是能够提高网络利用率从而降低计算机处理负荷的一种较优的处理机制。
由于捎带应答的存在,四次挥手,可以合并成三次挥手
3.面向字节流
TCP发送和接收数据是通过缓冲区来完成的
发送:
发送的时候,并不是直接将数据发送至网络之中,而是通过系统调send(write)用将数据写入发送缓存区之中,然后操作系统将缓冲区的内容通过网卡驱动程序发送出去
如果一个TCP数据包太长,那么这个数据包会被拆分成多次发送,如果太短,会被写入缓冲区之中,等待达到缓冲区的最低水位线,再进行发送,所以发送的数据没有边界,不一定是完整的
接收:
接收的时候,也不是直接从网络之中接收数据,而是通过网卡驱动程序从网络中接收数据,然后写入缓冲区之中,再调用recv(read)从缓冲区中读取数据。因此读取的时候并不一定一次将一个数据读完,而是有可能一个数据包分多次读完
由于缓冲区的存在,读和写不需要匹配进行,这种就叫做面向字节流
UDP需要保证数据的完整性(16位UDP长度,如果不完整会被直接丢弃),一个数据要么发,要么不发,所以叫做面向数据报
4.粘包问题
是什么:
在传输层的缓冲区之中数据都是一个个的字节,TCP不关心这些字节代表什么意思
如果上层(应用层)读取数据的时候,多读了,或者少读了,对下一次的数据产生影响的情况就叫做粘包问题
怎么避免:
应用层做了明确报文与报文边界的工作:
第一个读上去的肯定是请求的的第一个字节,一直往后面读取,读到空行,说明请求的报头读取完毕了
再读有可能是下一个报文的开始,也有可能是当前报文的正文。报头之中包含了Content-Length,标识了正文的长度,通过这个长度即可完整的读取一数个完整的数据报
HTTP之中的空行,Content-Length本质是在应用层明确报文以报文之间的边界
UDP是否存在粘包?
不存在,这是因为UDP报头之中,包含了一个首部长度(定长)和总长度
5.TCP异常情况
1.进程终止:套接字在内核之中对应的是一个文件描述符,即套接字本质上也是一个文件。文件的声明周期是随进程的,进程终止会释放文件描述符,相对应的套接字也会被关闭。并且自动触发四次挥手,断开连接
2.机器重启:和进程终止的情况相同
3.机器掉电/网线断开:一瞬间网线断开,客户端没有机会和服务器进行四次挥手。
此时接收端还处在连接的状态,当接收端进行写入的时候,一旦接收端发现连接已经不在了,就会进行reset
即是接收端没有进行写入操作,TCP也内置了一个保活定时器,会定期的询问对方是否还在,如果对方不在,也会将链接给释放到
除此之外,应用层的某些协议,也有一些这样的检测机制。比如,HTTP长连接中,也会定期检测对方的状态
6.理解listen的第二个参数
6.1全链接队列和半链接队列
listen的第二个参数为链接队列长度,分为全链接队列和半链接队列
1.半链接队列:用来保存SYN_SENT和SYN_RECV状态的请求(三次握手没有完成)
2.全链接队列:accpetd队列,用来保存established状态,但是应用层没有调accept取走请求
全链接队列长度为listen第二个参数+1,全链接队列满了,就无法继续让当前连接的状态进入established状态了
6.2为什么需要全链接队列和半链接队列
全链接队列:
全链接队列的存在,保证操作系统随时有任务可以执行,即当前任务执行完毕,随时可以从链接队列之中提取任务进行补充,提高线程的利用率,以及效率
但是链接队列的长度也不能太长,因为保存这些链接也是需要消耗资源的,如果链接的长度太长,资源消耗的太多,那不如多创建一些线程来执行这些任务
半链接队列:
半链接队列保存的是三次握手没有完成的一些状态,这些链接是允许丢失的,不同的系统设置的是不一样的
6.3实验验证
#include <iostream>
using namespace std;
#include <string>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class Server
{
private:
int port;
int lsock;
public:
Server(int _port)
:port(_port)
,lsock(-1)
{}
void InitServer()
{
signal(SIGCHLD,SIG_IGN);
lsock=socket(AF_INET,SOCK_STREAM,0);
if(lsock < 0)
{
cerr<<"sock error"<<endl;
exit(2);
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=INADDR_ANY;
int opt=1;
setsockopt(lsock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
cerr<<"bind error"<<endl;
exit(3);
}
if(listen(lsock,0) < 0)//全链接队列长度设置为0
{
cerr<<"listen error"<<endl;
}
}
void start()//不进行任何操作
{
while(true)
{
sleep(100);
}
}
~Server()
{
if(lsock!=-1)
close(lsock);
}
};
6.4原码验证
7.TCP小结
7.1为什么TCP这么复杂?
因为要保证可靠,同时又尽可能的提高效率问题
可靠性:
校验和
序列号(按序到达)
确认应答(只有确认了应答,才保证数据可靠到达了)
超时重发
连接管理
流量控制
拥塞控制
提高效率:
滑动窗口
快速重传
延迟应答
捎带应答
其它:
定时器(超时重传定时器、保活定时器(链接之后什么都不干,超过一段时间之后自动断开)、TIME_WAIT定时器)
7.2基于TCP应用层协议
HTTP、HTTPS、SSH、Telnet、FTP、SMTP