面向连接,可靠传输,面向字节流—建立连接之后才能进行通信。
16位源端端口,16对端端口,32位序号,32位确认序号,4位报头长度,6位保留,6位标志位,16位窗口大小,16位校验和,16位紧急指针(20字节最小长度),0~40字节选项数据。
一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的。
无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端。
可靠,有序,安全,双向,基于连接的以字节为单位的传输。
字节流传输,不管是发送还是交付,都可以以字节为单位,不像udp那么死板,整条传输整条交付;
发送了1000个字节,可以一次全部接收,也可以分成多次进行接收,传输比较灵活。
服务端listen进入监听状态:
1. 客户端发送连接建立SYN请求;
2. 服务端收到SYN请求后回复SYN+ACK报文;
3. 客户端收到ACK+SYN后回复ACK。
1. 主动关闭方发送FIN请求;
2. 被动关闭方收到FIN请求,回复ACK;
3. 被动关闭方发送FIN请求;
4. 主动关闭方收到FIN请求后,回复ACK;
FIN包只能表示自己不再给对方发送数据了。
客户端:SYN_SENT -> ESTABLISHED。
服务端:SYN_RECV -> ESTABLISHED。
主动方:FIN_WAIT1 -> FIN_WAIT2 ->TIME_WAIT。
被动方:CLOSE_WAIT ->LAST_ACK。
1.三次握手与四次挥手过程?
2.为什么握手是三次,而挥手是四次?
3.TIME_WAIT状态有什么用?
4.一台主机出现大量的CLOSE_WAIT状态套接字原因?
5.一台主机上出现大量TIME_WAIT状态套接字原因?
6.握手失败两端是如何处理的?
int socket(int domain,int type,int protocol)
domain:地址域类型-AF_INET-表示IPV4版本通信;
type:套接字编程-SOCK_STREAM-表示流式套接字;
protocl:协议类型-0默认在SOCK_STREAM下表示tcp协议,IPPROTO_TCP;
返回值:成功返回一个非负整数—操作句柄—套接字描述符;失败返回-1。
int bind(int sockfd,struct sockaddr *addr,socklen_t len);
sockfd:套接字描述符-创建套接字返回的操作句柄;
addr:要绑定的地址信息,ipv4通信应该使用struct sockaddr_in结构;
len:地址信息长度;
返回值:成功返回0;失败返回-1。
int listen(int sockfd,int backlog);
sockfd:套接字描述符;
backlog:同一时刻最大的并发连接数(防止SYN泛洪攻击);
(当前服务端在同一时间能处理的最多客户端连接的请求数量)
返回值:成功返回0;失败返回-1。
int connect(int sockfd,struct sockaddr *addr,socklen_t len);
sockfd:套接字描述符;
addr:服务端的地址信息,ipv4通信使用struct sockaddr_in结构;
len:地址信息长度;
返回值:成功返回0;失败返回-1。
int accept(int sockfd,struct sockaddr *addr, socklen_t *len)
sockfd:监听套接字描述符(监听套接字近用于监听以及获取新建连接);
addr:客户端地址信息(描述当前获取的新建连接句柄是与哪个客户端通信的);
len:输入输出型参数,指定要获取的地址长度,以及返回实际获取的地址长度);
返回值:新建连接的套接字描述符—操作句柄—用于后续与客户端进行通信;失败返回-1。
ssize_t send(int sockfd,void *data,int len,int flag)
sockfd:套接字描述符(对于服务端来说,一定是accept获取到的新建连接的描述符);
data:要发送的数据首地址;
len:要发送的数据长度;
flag:标志位-通常置0,表示阻塞发送(把数据放到缓冲区,系统进行封装发送-放不进去则等待);
返回值:成功返回实际发送的数据长度;失败返回-1。
ssize_t recv(int sockfd,void *buf,int len,int flag)
sockfd:套接字描述符;
buf:一个缓冲区空间首地址,用于放置接收的数据;
len:想要获取的数据长度,但是不能大于buf的缓冲区大小;
flag:标志位-通常置0,表示阻塞接收(socket接收缓冲区中没有数据则阻塞);
返回值:成功返回实际获取的数据长度;失败返回-1;连接断开返回0。
(当服务端recv返回0时,确实没有接收到数据,但是更多为了表示连接断开)
int close(int fd);
部分关闭连接:但是要注意—并没有完全释放资源。
int shutdown(int sockfd,int how);
sockfd:套接字描述符;
how:要关闭的操作类型—SHUT_RD、SHUT_WR、SHUT_RDWR。
1.封装一个TcpSocket类,通过这个类的成员接口可以更加简单的完成客户端与服务器的搭建;
2.使用TcpSocket类实例化的对象搭建一个tcp客户端;
3.使用TcpSocket类实例化的对象搭建一个tcp服务端 。
class TcpSocket{
private:
int _sockfd;
public:
TcpSocket():_sockfd(-1){}
bool Socket();//创建套接字
bool Bind(const std::string &ip,int port);//绑定地址信息
bool Listen(int backlog =MAX_LISEN);//服务端开始监听
bool Connect(const std::string &srv_ip,int srv_port);//向服务端发起连接请求
bool Accept(TcpSocket *new_sock,std::string *cli_ip, int *cli_port);//获取新建连接
bool Send(const std::string &data);//发送数据
bool Recv(std::string *buf);//接收数据
bool Close();
}
在当前单执行流TCP服务器代码中无法解决,
因为我们不知道什么时候有新建连接到来,也不知道哪个客户端什么时候会发送数据,
因此流程是固定(先获取新建连接,接收数据,发送数据)。
让主执行流只负责一个功能:获取新建连接;
一旦新建连接获取成功,
则创建一个执行流,将新建连接的套接字传入,
让这个执行流专门负责与一个客户端通信。
一个执行流只负责一件事情,在完成这件事情的过程中阻塞也不会影响其他执行流的运行。
主线程负责获取新建连接,获取之后创建普通线程,将套接字传入;
普通线程负责与指定的客户端进行持续通信。
子进程复制父进程,父子进程代码共享,数据独有,因此可以通过子进程与客户端通信。
先确保双方具有数据收发的能力;
确认应答机制—发送的每一条数据都要求对端收到之后进行确认应答;
超时重传机制—等待确认应答的时候等待超时了都没有收到应答,则对数据进行重传;
进行数据包序管理,保证数据有序发送,有序交付;
二进制反码求和算法,校验收到的数据与发送的数据是否一致,不一致则丢弃要求对方重传。
注意:
在连续传输中,如果先收到了后边的数据,这时候不能进行确认回复:
因为每个确认序号都要保证之前的所有数据都已经收到了,
这样是为了避免因为确认应答丢失而导致重传。
tcp为了实现可靠传输,用了很多机制,损失了很多传输性能,
但是有些性能是没必要损失的, 因此要尽量挽回。
发送方延迟发送数据,因为每次发送数据都会涉及到硬件的操作,效率较低,
如果在发送大量短小数据的时候就不划算,
因此延迟发送会把多个小数据在发送缓冲区中堆积成为一个大数据进行一次发送。
如果发送的一个数据包丢了,发送方如果总是要等到超时才能重传,效率较低,
这时应采用快速重传机制。
接收方接收数据的时候,接收到的数据不是从预期起始开始的,
则认为前边的数据可能丢失了,
则这时候回复前边预期起始序号的确认应答作为重传请求,并且间隔连续发送三次。
(三次是为了避免因为延迟而到达导致的重传,三次期间如果收到则不满三条不需要重传)
发送方发送数据过多,而接收方上层获取太慢,导致接收方的接收缓冲区满了,
则多出来的数据就会被丢弃,而丢弃的就会导致重传,效率降低。
主要是基于协议字段中的窗口大小字段实现流量控制。
接收方每接收一条数据就会进行确认应答,在确认应答的时候就会通过窗口大小字段告诉对方,最多在发送多少数据就不要发送了(这个窗口大小不会大于接收缓存区的剩余空间大小),
这样就可以避免因为发送数据过多而导致丢包的情况。
MSS:最大数据段大小,通过下层的数据报大小限制所计算出来一个最大传输数据长度,tcp在发送数据的时候,每个报文都不会大于mss大小,而是在发送缓冲区中截取合适长度数据发送。
当回复窗口为0后,则每隔一段时间,发送方都会发送一个探测包。
实现细节:实际上通信双方都会维护两个窗口,一个是发送窗口,一个是接收窗口。
窗口有前沿和后沿(后沿起始序号,前沿结束序号)
发送窗口后沿:发送的起始数据序号;
发送窗口前沿:发送的数据结束序号;
接收窗口后沿:接收的起始数据序号;
接收窗口前沿:接收的数据结束序号。
发送一条数据后,必须等收到确认应答才会发送下一条数据。(网络较差时)
如果一条数据丢失,则会将丢失数据序号开始的数据都进行重传。(网络一般时)
哪条丢了,重传哪条。(网络较好时)
针对不同的网络状况有不同的选择—不同的适用场景。
如果发送数据过程中,网络状况突然变差,这时候发送的数据越多越快,则丢失的数据就越多,
最终导致大量重传降低效率。
拥塞机制 是发送方所维护的一个机制(拥塞窗口-发送数据大小的限制)
拥塞窗口—开始很小,但是涨幅非常快(指数级的增长),以这种形式进行网络探测,
当然也会有个阈值-窗口大小,
一旦传输过程中出现丢包,则会重新开始拥塞控制。
因为接收方收到数据后都会进行确认回复,如果立即进行回复,不可避免大概率窗口都会变小,
则发送方的发送数据量就会变小,吞吐量小了,传输性能就低了,
因此采用延迟应答:
收到数据后不立即进行回复,而是等待一段时间,而这段时间内,上层就有可能将数据从缓冲区取出,
则窗口大小会不变甚至变大,保持吞吐量。
接收方会为发送方发送的每个数据进行确认回复,而一个确认回复就是一个空报头(确认序号…)。
而空报头的传输会导致占用宽带…影响,
如果这时候刚好要给对方发送数据,
那就将这个确认回复和要发送的数据合在一起进行发送。
毕竟确认回复只是一个头部信息,而这样可以提高传输效率。
字节流传输比较灵活,数据可以在缓冲区堆积,想要多少给多少,但是又存在其他缺陷:
有可能将多条数据当做一条数据进行处理;
数据在缓冲区中可以堆积,传输比较灵活,但是有可能将多条数据当做一条数据进行处理产生粘包问题。
TCP在传输层对数据的边界不敏感,因此程序员在上层进行数据边界管理。
udp根本不会粘包----本身有边界管理类整条交付。
如果tcp在传输过程中,网络中断,而自己有很长时间没有发送数据,
则不知道连接已经断开,套接字实际上没用了,但是依然占据资源。
服务器在长时间没有数据通信时(默认7200s),
则间隔一定时间(默认75s)发送一个保活探测数据包给对方,
如果多次(默认9次)没有收到探测响应,则认为连接断开,释放资源。
这些默认数据都是可配置的,通过设置套接字选项操作就可以进行单独设置。setsockopt()
(程序中如何感知连接断开?)
连接断开,则recv接收完数据之后,继续recv则不再阻塞,而是返回0。
(recv返回0就是连接断开)
如果是send,则在连接断开后,会触发异常(SIGPIPE),导致进程退出。
如果不想因为连接断开而导致发送数据的时候程序异常退出,
则需要对SIGPIPE信号进行自定义或者忽略处理。