上面一篇文章我们着重讲了Linux网络套接字编程和http的协议的编程。下面我们来谈谈传输层协议(TCP/UDP)。
目录
[认识知名端口号(Well-Know Port Number)](#认识知名端口号(Well-Know Port Number))
为什么要发明序号和确认序号两个呢?为什么在应答时不直接对序号进行覆盖呢?
传输层
负责数据能够从发送端传输接收端。
端口号
端口号(port)标识了一个主机上进行通信的不同的应用程序。

在TCP/IP协议中,用"源IP","源端口号","目的IP","目的端口号","协议号"这样一个五元组来标识一个通信(可以通过netstat -n 命令来查看)


端口号返回划分
0-1023:知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的。
1024-65535:操作系统动态分配的端口号,客户端程序的端口号,就是由操作系统从这个范围分配的。
认识知名端口号(Well-Know Port Number)
有些服务器是非常非常常用的,为了使用方便,人民约定一些常用的服务器,都是用以下这些固定的端口号:
1.ssh服务器,使用22号端口
2.ftp服务器,使用21号端口
3.telnet服务器,使用23号端口
4.http服务器,使用80号端口
5.https服务器,使用443号端口
使用cat /etc/services 命令可以看到知名端口号。
我们自己写一个程序使用端口号时,要避开这些知名的端口号。
一个端口号只能绑定一个进程。但是一个进程可以绑定多个端口号!注意不要搞混了!
netstat
netstat是一个用来查看网络状态的重要工具。
语法:netstat 【选项】
功能:查看网络状态。
常用选项:
1.n: 拒绝显示别名,能显示数字的全部转化成数字。
2.l :仅仅列出有在Listen(监听)的服务状态。
3.p:显示建立相关链接的程序名。
4.t(Tcp):仅仅显示Tcp相关选项。
5.u(Udp):仅仅显示udp相关选项。
6.a(all):显示所有选项,默认不显示Listen相关。
netstat -nltp看Tcp相关。netstat -nuap 查看udp相关。
pidof
在查看服务器的进程id时非常方便。
语法:pidof 【进程名】
功能:通过进程名,查看进程id。
**例如:**pidof 进程名 | xargs kill -9
UDP协议
UDP协议段格式

16位UDP长度,表示整个数据报(UDP首部+UDP数据)的最大长度。
如果校验和出错,就会直接丢弃。
UDP的特点
UDP传输的过程类似于寄信。
1.无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接。
2.不可靠:没有确认应答机制,没有重传机制,如果因为网络故障该段无法发送到对方,UDP协议层也不会给应用层返回任何错误信息。
3.面向数据报:不能够灵活的控制读写数据的次数和数量。
面向数据报
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并。
用UDP传输100个字节的数据:
如果发送端调用一次sendto,发送100个字节,那么接收端也必须调用对应的recvfrom,接受100个字节,而不能循环调用10次recvfrom,每次接受10个字节。
UDP的缓冲区
1.UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核数据传给网络层协议进行后续的传输动作。
2.UDP具有接受缓冲区,但是这个接受缓冲区不能保证收到UDP报的顺序和发送UDP报的顺序一致,如果缓冲区满了,再到达的UDP数据就会被丢弃。
UDP的socket既能读,也能写,是全双工的。

UDP使用注意事项
我们注意到,UDP协议首部中有一个16位的最大长度,也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)。
然而64K在当今的互联网环境下,是一个非常小的数字。
基于UDP的应用层协议
1.NFS:网络文件系统。
2.TFTP:简单文件传输协议。
3.DHCP:动态主机配置协议。
4.BOOTP:启动协议(用于无盘设备启动)。
5.DNS:域名解析协议。
当然,也包括你自己写UDP程序时自定义的应用层协议。
TCP协议
TCP全称为"传输控制协议(Transmission Control Protocol)"。对数据的传输进行一个详细的控制。

TCP是一个具有发送、接收缓冲区的,全双工通信的、进行数据控制的一种协议!
TCP协议段格式

源/目的端口号 :表示数据是从哪个进程来,到哪个进程去。
32位序号:标识当前数据段第一个字节在整个数据流中的位置(例如:发送端:1-1000发给接收端)。
32位确认号: 期望收到的下一个字节的序列号(表示此前的数据已收到) 。(填充的是收到的报文的序号+1)。
4位TCP报头长度(单位字节) :表示该TCP头部有多少个32位bit(有多少个4字节),所以TCP头部最大长度是15*4=60 字节。4位首部长度的大小为标准报头+选项的大小,范围为【20-60】字节。标准报头的大小为20字节,所以如果选项为0,那么4位首部长度的值为0101(十进制的5).
6位标志位:(为了区分tcp报文的类型!)
1.URG:紧急指针是否有效。
2.ACK:确认号是否有效。
3.PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走。
4.RST :对方要求重新建立连接,我们把携带RST标识的称为复位报文段。
5.SYN :请求建立连接,我们把携带SYN标识的称为同步报文段。
6.FIN :通知对方,本端要关闭了。我们把携带FIN标识的称为结束报文段。
16位校验和:发送端填充,CRC校验,接收端不通过,则认为数据有问题。此处的校验和不光包含TCP首部,也包含TCP数据部分。
16位紧急指针:标识哪部分数据是紧急数据。
16位窗口大小:填写的都是自己的接收缓冲区的中剩余空间的大小!!!因为是发给对方的(发送给对方完整的tcp报文)!
40字节头部选项:暂时忽略。

解释一下6位标记位

PSH标志位置为1,并把此次tcp报文发送给对方接受缓冲区的时候,就提示对方立即将数据交付给应用层。
RST标志位用于立即强制终止一个TCP连接。当RST=1时,表示连接出现异常,需要立即释放资源。
RST和FIN的区别:
|------|---------|------------|
| 特性 | RST(复位) | FIN(结束) |
| 目的 | 异常终止连接 | 正常关闭连接 |
| 方式 | 立即强制断开 | 优雅协商关闭 |
| 数据保证 | 可能丢失数据 | 保证数据完整 |
| 过程 | 单向通知 | 双向协商(四次挥手) |
| 资源释放 | 立即释放 | 有序释放 |
对于URG标志位:


确认应答(ACK)机制

TCP将每个字节的数据都进行了编号,即为序列号。

每一个ACK都带有对应的确认序号,意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里开始发。
乱序就是不可靠的!所以Tcp引入序列号和确认序号,client和server进行通信时,可以使用序列号和确认序号进行确认应答机制。进而保证了数据被对方收到了。

解释一下序号和确认序号:

为什么要发明序号和确认序号两个呢?为什么在应答时不直接对序号进行覆盖呢?
1.因为不确定应答的时候,应答有没有带数据回去(捎带应答)。
2.tcp是全双工的,在发送的时候,也可以接收!
超时重传机制

主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B。
如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发。
但是主机A未收到B发来的确认应答,也可能是因为ACK丢失了。

因此主机B会收到很多重复数据。那么TCP协议需要能够识别出哪些包是重复的包,并且把重复的丢弃掉。达到去重的效果,就需要利用到序列号!
那么超时的时间该如何界定呢?
1.最理想的情况下,找到一个最小的时间,保证"确认应答一定能在这个时间内返回"。
2.但是这个时间的长短,随着网络环境的不同,是有差异的。
3.如果超时时间设的太长,会影响整体的重传效率。
4.如果超时时间设的太短,有可能会频繁发送重复的包。
TCP为了保证无论在任何环境下都能有比较高性能的通信,因此会动态计算这个最大超时时间。
1.Linux中(BSD Unix和windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
2.如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传。
3.如果仍然得不到应答,等待4*500ms进行重传,以此类推,以指数形式递增。
4.累积到一定的重传次数,Tcp认为网络或者对端主机出现异常,强制关闭连接。
连接管理机制
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接。

服务端状态转化:
1.【CLOSED--->LISTEN】:服务器端调用Listen后进入Listen状态,开始监听指定端口,等待客户端连接请求(SYN报文)。
2.【LISTEN--->SYN_RCVD】 :当服务器在LISTEN状态下收到客户端发来的SYN连接请求报文后,它会回复SYN+ACK 报文,并进入SYN_RCVD 状态,表示已经收到并确认了连接请求,正在等待客户端的最终确认。
3.【SYN_RCVD--->ESTABLISHED】 :服务器在SYN_RCVD状态下,收到客户端对SYN_+ACK报文的ACK 确认后,三次握手完成 ,服务器进入ESTABLISHED状态 。此时双向连接已经建立,可以开始正常数据传输。
连接终止阶段(客户端主动关闭):
4.【ESTABLISHED--->CLOSE_WAIT】 :在通信过程中,当服务器收到客户端发来的FIN报文(表示客户端请求关闭连接)时 ,它会立即回复ACK进行确认 ,并进入CLOSE_WAIT状态。这表示服务器已经知道客户端没有数据要发了,但服务器本身可能还有数据需要发送给客户端。
5.【CLOSE_WAIT--->LAST_ACK】 :当服务器在CLOSE_WAIT状态下,将自己的剩余数据发送完毕并调用close()时,它会向客户端发送一个FIN报文 ,随后进入LAST_ACK状态,等待客户端对它的FIN报文的最终确认。
6.【LAST_ACK--->CLOSED】 :服务器在LAST_ACK状态 下,收到客户端对它的FIN报文 的ACK确认后 ,便彻底关闭连接,回到CLOSED状态。
连接终止阶段(服务器主动关闭):
理论上,服务器也可以主动发起关闭。流程是将:
【ESTABLISHED--->FIN_WAIT_1--->FIN_WAIT_2--->TIME_WAIT--->CLOSED】
客户端状态转化:
1.【CLOSE--->SYN_SENT】 :客户端调用connect()后 ,向服务器发送SYN报文 ,进入SYN_SENT状态 ,等待服务器的SYN+ACK响应。
2.【SYN_SENT--->ESTABLISHED】 :客户端在SYN_SENT状态 下收到服务器的SYN+ACK报文后 ,会回复ACK确认 。完成三次握手,进入ESTABLISHED状态 ,连接建立成功,可以开始数据传输。
3.【ESTABLISHED--->FIN_WAIT_1】 :客户端调用close()主动发起关闭 ,向服务器发送FIN报文 ,并进入FIN_WAIT_1状态 ,等待服务器对FIN的ACK确认。
4.【FIN_WAIT_1--->FIN_WAIT_2】 :客户端在FIN_WAIT_1状态 下,收到服务器对FIN的ACK确认后,进入FIN_WAIT_2状态,此时等待服务器发送它自己的FIN报文(即等待服务器关闭它的发送通道)。
5.【FIN_WAIT_2--->TIME_WAIT】 :客户端在FIN_WAIT_2状态下,收到服务器发来的FIN报文后,回复ACK确认,并立即进入TIME_WAIT状态。该状态会持续2MSL(两倍的最大报文段生存时间) ,以确保这个最终的ACK能可靠到达服务器,并让本次连接的所有报文在网络中消散,避免影响未来的新连接。
6.【TIME_WAIT--->CLOSED】:在TIME_WAIT状态等待2MSL时间超时后,客户端彻底关闭连接,回到CLOSED状态。
说明:在典型的"请求-响应"模型(如HTTP/1.0)中,客户端通常是连接的主动发起者和主动关闭者,因此客户端最常见的终止路径是经过TIME_WAIT状态的那一条。服务器则通常是连接的被动接受者和被动关闭者,其终止路径也不经过TIME_WAIT。
连接建立成功和上层的accept没有关系!
三次握手是双方操作系统自动完成的!与上层有没有accept函数没有任何关系!
解释一下关于Listen函数的第二个参数:
第二个参数backlog,backlog+1表示底层已经建立好的连接队列的最大长度。

listen的backlog参数不能设置太长,也不能没有这个参数。
原因是:不能太长:占用server端的资源,而不能及时使用掉。
不能没有:需要accept的时候,及时有,能处理。
服务端,不会长时间维护SYN_RECV,被建立连接的一方,处于SYN_RECV,半连接,放入半连接队列里,半连接的节点,不会长时间维护。
SYN洪水:
是client一直向服务端发送SYN请求,服务端的SYN_REVC的队列都已被打满了,client还在不断的向服务端发送SYN请求。
TCP规定主动断开连接的一方,在4次挥手完成之后,要进入TIME_WAIT状态,等待若干时间(2MSL)之后,自动释放。

想解决server端进入TIME_WAIT状态无法进行立即重启的问题,请看下面讲解。
为什么tcp需要三次握手?
1.防止已经失效的连接请求造成错误。
场景:客户端发送SYN后网络延迟,超时重发新的SYN建立连接。
问题:就得SYN延迟到达服务器,服务器以为是新连接,直接建立。
结果:服务器子与阿奴浪费,且客户端不任何此连接。
第三次握手让客户端确认:只有收到ACK才建立连接!
2.可靠的验证了tcp的全双工。
第一次握手:服务器知道客户端发送能力正常。
第二次握手:客户端知道服务器的接收和发送能力正常。(服务端捎带应答回去了)
第三次握手:服务器知道客户端接收能力正常。
三次握手后,双方确认彼此收发功能都正常!
3.同步初始序列号。
双方交换初始序列号,为数据传输做准备。
防止历史连接干扰。
为什么tcp要进行四次挥手?
1.tcp是全双工的,需要双向独立关闭。
每个方向都需要单独关闭。
发送FIN只表示"我不再发送数据",但是还可以接收数据。
2.第二次和第二次挥手不能合并的原因
因为一端关闭,另一端可能还有数据要发送呢。
3.TIME_WAIT状态的重要性
tcp规定了主动关闭方发送FIN收到对方发来的ACK后进入TIME_WAIT状态(2MSL时间)
作用1:确保ACK到达对方(对方超时会重发FIN)。
作用2:让旧连接的报文在网络中消失,避免影响新连接。
理解TIME_WAIT状态
现在做一个测试,首先启动server,然后启动client,然后用Ctrl+c使server终止,这时候马上再运行server,结果会是:
bind error:Address already in use

这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令看一下。发现tcpserver目前是TIME_WAIT阶段!(tcpserver是主动断开连接的!)

1.TCP协议规定。主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)时间后才能回到CLOSED状态。
2.我们使用Ctrl+c终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。
3.MSL在RFC1122中规定为两分钟,但是各个操作系统的实现不同,在Centos7上的配置的值为60s。
4.可以通过cat /proc / sys / net / ipv4 / tcp_fin_timeout 查看MSL的值。

为什么TIME_WAIT的时间是2MSL?
1.MSL是TCP报文的最大生存时间,因此TIME_WAIT持续存在2MSL的话,就能保证在两个传输方向上的尚未被接受或者迟到的报文段都已消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据是错误的!)。
2.同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失,那么服务器会再重发一个FIN,这时虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK)。
为什么需要TIME_WAIT?
1.可靠的实现tcp全双工连接的终止。
主要原因:确保被动关闭方收到最后的ACK!
场景:最后一个ACK丢失。客户端:接收到服务端发来的FIN后发送ACK,进入TIME_WAIT状态。此时ACK丢失了。
服务端:未收到客户端的ACK应答,然后服务端自己重发FIN(在LAST_ACK状态下)。
客户端:在TIME_WAIT状态下,收到重传的FIN,重发ACK应答给服务端。
如果没有TIME_WAIT状态,客户端发送ACK后立即关闭:
服务器重传FIN时,客户端已经关闭,会回复RST。
服务器收到RST会认为连接异常终止!
2.允许旧的报文在网络中消失(让通信双方的历史数据得以消散)
防止旧连接的报文被新连接接收,造成数据混乱。
TIME_WAIT的2MSL的等待确保了:
1)旧连接的所有报文在网络中消失(最多存活1MSL)
2)最后一个ACK在网络中消失(最多存活1MSL)
3)两倍MSL确保两个方向的数据包都已经消失。
解决TIME_WAIT状态引起的bind失败的方法
在server的TCP连接没有完全断开之前不允许重新监听,某些情况下是不合理的。
可以使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。

流量控制
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列的连锁反应。
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制。
1.接收端将自己可以接收的缓冲区大小放入TCP首部中的"窗口大小"字段,通过ACK端通知发送端。
2.窗口大小字段越大,说明网络的吞吐量越高。
3.接收端一旦发现自己的缓冲区快满了,就会将自己的窗口大小设置成一个更小的值通知给发送端。
4.发送端接收到这个窗口之后,就会慢慢减少自己的发送速度。
5.如果接收端缓冲区满了,就会告诉窗口设置为0,。这时发送发不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。


接收端如何把窗口大小告诉发送端呢?在tcp的首部中,有一个16位窗口字段,就是存放了窗口大小信息。那么问题来了,16位数字的最大表示65535,那么tcp窗口最大就是65535字节么?
实际上,tcp首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是 窗口字段的值左移M位!(窗口是可以扩大的,缓冲区不止65535!)
滑动窗口
滑动窗口是TCP协议的核心机制之一,主要解决流量控制和拥塞控制的问题,确保可靠传输的同时提高传输效率。
上面我们说了确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答。收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能比较差,尤其是数据往返的时间比较长的时候。

既然这样一发一收的方式性能比较低。那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)。
(因为有滑动窗口区域,我们才可以一次向对方发送大量的tcp报文!)
1.发送前四段的时候,不需要等待任何ACK,直接发送。
2.收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据,以此类推。
3.操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前海鸥哪些数据没有应答,只有确认应答过的数据,才能从缓冲区删掉。
4.窗口越大,则网络的吞吐率就越高。

那么如果出现了丢包,如何进行重传呢?这里分两种情况讨论。
情况一:数据包已经抵达,ACK丢了。
这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认。
情况二:数据包直接丢了。

1.当某一段报文段丢失之后,发送端会一直收到(例如)1001这样的ACK,就像是在提醒发送端"我想要的是1001"一样。
2.如果发送端主机连续三次收到了同样一个"1001"这样的应答,就会将对应的数据1001~2000重新发送。----(这种机制被称为"高速重发控制",也叫做"快重传")。
3.这个时候接收端收到了1001之后,再次返回的ACK就是7001了,因为(2001-7000接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中)。
1.滑动窗口在哪里?---->滑动窗口就是我们的发送缓冲区的一部分!
2.滑动窗口,本质就是指针右移!


对于丢包的数据,滑动窗口不会跳过这个区间,而是停在这里。直到丢失的数据被确认。(超时重传or快重传)
我们对于确认序号的意义:确认序号是x,说明x之前的数据我们已经全部收到了!(允许少量ACK丢失)--->保证了滑动窗口,线性的连续的向后更新,不会出现跳跃的情况。
快重传机制:条件:发送方收到连续3个同样的确认应答时则进行重发。
为什么有了快重传机制,还需要超时重传机制呢?因为快重传机制是有条件的,超时重传没有条件,用来兜底的。
注意:
滑动窗口的大小是动态变化的,(变大/变小/不变),会根据左右指针的变化来变化,当滑动窗口大小变化到0时,双方开始进行窗口探测。滑动窗口只会向右边移动!
滑动窗口的实际大小=min(接收窗口(rwnd),拥塞窗口(cwnd));
拥塞窗口是tcp考虑到了网络的情况!
滑动窗口不会在发送缓冲区里越界,tcp采用了类似环状算法。
拥塞控制
虽然TCP有了滑动窗口这个大武器,能够高效可靠的发送大量的数据,但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。
因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下,贸然发送大量的数据是很有可能引起雪上加霜的。
TCP引入慢启动机制,先发少量的数据探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
慢启动原理:指数增长,快速探测网络容量。
拥塞窗口是发送方根据网络拥塞状况自行维护的一个窗口值,用于控制发送速率,避免网络过载。


像上面这样的拥塞窗口增长速度,是指数级别的。"慢启动"只是初始时候慢,但是增长速度非常快。
为了不增长的这么快,因此不能使拥塞窗口单纯的加倍。
此处引入一个叫做慢启动的阈值。当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。

当tcp开始启动的时候,慢启动阈值等于窗口最大值。
在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1.
少量的丢包,我们仅仅是触发超时重传,大量的丢包,我们就认为是网络拥塞了。
当TCP通信开始后,网络吞吐量会逐渐上升。随着网络发生拥堵,吞吐量会立刻下降。拥塞控制,归根结底是TCP协议想尽快的把数据传输给对方,但是又要避免网络造成太大压力的这种方案。
延迟应答
如果接收数据的主机立刻返回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从接收缓冲区拿数据。
另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区 ,那么对于这一个连接,既可以读数据,也可以写数据,这个概念叫做全双工。
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节。
读100个字节数据时候,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次。

粘包问题
粘包问题:上层应用层读取字节时候,不按照规律,不做字符串分析,读取一个半报文,或者半个报文的问题。
首先要明确,粘包问题中的"包",是指的应用层的数据包。
在TCP的协议头中,没有如同UDP一样的"报文长度"这样的字段,但是有一个序号这样的字段。
站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。
站在应用层的角度,看到的只是一串连续的字节数据。
那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
那么如何避免粘包问题呢?归根结底就是一句话:明确两个包之间的界。
对于定长的包,保证每次都按固定大小读取即可。例如上面的Request结构,是固定大小的,那么就从缓冲区从头开始按照sizeof(Request)依次读取即可。
对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。
对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来决定的,只要保证分隔符不和正文冲突即可)。
那么对于UDP协议来说,存不存在"粘包问题"呢?--->UDP不存在!
对于UDP来说,如果还没有上层交付数据,UDP的报文长度仍然在,同时,UDP是一个一个把数据交付给应用层,就有很明确的数据边界。
站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现"半个"的情况。

TCP异常情况
1.进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有什么区别。
2.机器重启:和进程终止的情况相同。
3.机器掉电/网线断开 :接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会向对方发送RST(复位)包来强制关闭连接 。此外,TCP协议本身也提供保活定时器机制,用于检测长时间无响应的连接并自动清理。
4.应用层协议的连接维持机制:除了TCP自身的机制外,某些应用层协议也实现了连接状态检测,例如:HTTP长连接(会定时发送探测请求以确认对方是否存活)。QQ等即时通讯软件(在检测到连接断开后,会周期性的尝试重连以恢复通信)。
TCP小结
可靠性:
1.校验和。
2.序列号(按序到达)
3.确认应答。
4.超时重发。
5.连接管理机制。
6.流量控制。
7.拥塞控制。
提高性能:
1.滑动窗口。
2.快速重传。
3.延迟应答。
4.捎带应答。
其他:
定时器(超时重传定时器,保活定时器,TIME_WAIT定时器等)。
基于TCP应用层协议
HTTP,HTTPS,SSH,Telnet,FTP,SMTP等等,当然也包括你自己写TCP程序自定义的应用层协议。
TCP/UDP对比
TCP是可靠的连接的,那么是不是TCP一定就优于UDP呢?TCP和UDP之间的优点和缺点,不能简单绝对的进行比较。
TCP用于可靠传输的情况,应用于文件传输,重要状态更新等场景。
UDP用于高速传输和实时性要求较高的通信领域,例如:早期的QQ,视频传输等。另外UDP可以用于广播。
归根结底,TCP和UDP都是程序员的工具,什么时机用,具体怎么用,还是要根据具体的需求场景去判定。
用UDP实现可靠传输(经典面试题)
参考TCP的可靠性机制,在应用层实现类似的逻辑:(聊具体场景)
例如:
1.引入序列号,保证数据有序到达。
2.引入确认应答,确保对方收到了数据。
3.引入超时重传机制,如果隔一段时间没有应答,就重发数据。
......
