第四部分:传输层

目录

1、端口号

1.1、端口号划分

1.2、知名端口号

2、UDP协议

2.1、UDP协议格式

2.2、UDP的特点

2.3、UDP的缓冲区

2.4、基于UDP的应用层协议

3、TCP协议

3.1、TCP协议格式

3.2、确认应答机制

3.3、捎带应答

3.4、超时重传机制

3.5、连接管理机制

3.5.1、accept

3.5.2、listen

3.5.3、TIME_WAIT

3.6、流量控制

3.7、滑动窗口

3.8、延迟应答

3.9、拥塞控制

3.10、TCP的特点

3.10.1、面向字节流

2.10.2、粘包问题

2.10.3、TCP异常情况

3.10.4、基于TCP的应用层协议

3.11、用UDP实现可靠传输

4、文件与网络套接字


1、端口号

端口号 标识了一个主机上进行通信的不同的应用程序。在TCP/IP协议中,用源IP,源端口号,目的IP,目的端口号,协议号(也就是指协议)这样一个五元组来标识一个通信

1.1、端口号划分

0 - 1023:知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的。

1024 - 65535:其他端口号。客户端程序的端口号,就是由操作系统从这个范围分配的。

注:也有一些比较知名的服务,使用的不是知名端口号,比如MySQL使用3306为默认端口号。

1.2、知名端口号

有些服务器是非常常用的,为了使用方便,人们约定一些常用的服务器,都是用以下这些固定的端口号,例如:

ssh服务器,使用22端口。

ftp服务器,使用21端口。

telnet服务器,使用23端口。

http服务器,使用80端口。

https服务器,使用443。

执行下面的命令,可以看到知名端口号是被哪些服务使用的:

bash 复制代码
cat /etc/services

注意:一个进程是可以bind多个端口号的,但一个端口号不可以被多个进程bind。

另外再介绍一个命令叫pidof,该命令的作用是通过进程名查看进程ID,如下:

语法:pidof 进程名

功能:通过进程名,查看进程id

运行结果如下图所示:

另外还有一个命令叫iostat,可以用来查看一些网络的IO情况,运行结果如下图所示:

netstat是一个用来查看网络状态的重要工具,如下:

语法:netstat [选项]

功能:查看网络状态

常用选项:

-n:拒绝显示别名,能显示数字的全部转化成数字。

-l:列出有在 Listen (监听) 的服务状态。

-p:显示建立相关链接的程序名。

-t:(tcp)仅显示tcp相关选项。

-u:(udp)仅显示udp相关选项。

-a:(all)显示所有选项,默认只显示已连接状态的套接字。

注:上面的unix表示域间套接字,用于本地通信。

2、UDP协议

2.1、UDP协议格式

UDP协议的格式如下图所示:

其中源端口和目的端口不再多说;UDP长度是指整个UDP报文的长度,也就是报头加上数据的长度;

UDP校验和的核心作用是检测 UDP 数据报在传输过程中是否发生了比特错误(比如因网络干扰导致的 0/1 翻转),如果效验和出错,就会直接丢弃报文,否则进行向上交付;

注意:UDP是定长报头,也就是固定的8字节。我们注意到,UDP协议首部中有一个16位的最大长度,也就是说一个UDP能传输的数据最大长度是64KB(包含UDP报头),然而64KB在当今的互联网环境下,是一个非常小的数字,如果我们需要传输的数据超过64KB,就需要在应用层手动的分包,多次发送,并在接收端手动拼装。

2.2、UDP的特点

UDP传输的过程类似于寄信,有以下特点:

无连接:知道对端主机的IP和端口号就直接进行传输,不需要先建立连接。

不可靠:没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息。

面向数据报:以独立的、不可分割的数据单元(数据报)为单位进行传输,数据报之间彼此独立,没有关联。应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并;用UDP传输100个字节的数据:如果发送端调用一次sendto,发送100个字节,那么接收端也必须调用对应的一次recvfrom,接收100个字节,而不能循环调用10次recvfrom,每次接收10个字节。在UDP这里就不用担心是否会读取了一个完整的报文,因为读到的一定是一个完整的未被拆分的报文。

2.3、UDP的缓冲区

UDP没有发送缓冲区,调用接口发送数据会直接交给内核,由内核进行后续的传输动作。

UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致(比如:发送三个报文,这三个报文分别叫1,2,3,数据的发送顺序为1,2,3,但是接收的顺序不一定是1,2,3,可能是乱序的);另外如果缓冲区满了,再到达的UDP的数据就会被丢弃。

注:同一时间,操作系统中可能会存在很多要发送或者接收的UDP报文,操作系统也是采用先描述,再组织的方式管理这些UDP报文的)。

UDP的socket既能读,也能写,这个概念叫做全双工

2.4、基于UDP的应用层协议

NFS:网络文件系统协议。

TFTP:简单文件传输协议。

DHCP:动态主机配置协议。

BOOTP:启动协议(用于无盘设备启动)。

DNS:域名解析协议。

当然,也包括自己写UDP程序时自定义的应用层协议。

3、TCP协议

TCP全称为传输控制协议,人如其名,要对数据的传输进行一个详细的控制。

3.1、TCP协议格式

TCP协议要比UDP协议麻烦很多,主要的原因就是TCP需要保证可靠性以及效率,TCP协议格式如下图所示:

其中源端口目的端口不再多说;

TCP 传输过程中,发送端字节流内的每一个数据字节都拥有唯一的32位编号,报文的序号即对应其数据部分第一个字节的编号。

当 SYN=1 时,处于连接建立阶段,此时的序号为随机算法生成的初始序号(ISN),需要注意的是,SYN=1 的报文段即使不带数据,也会消耗 1 个序号;

当 SYN=0 时,进入正式数据传输阶段,第一个数据报文段的序号为ISN+1,后续报文段的序号则遵循 "前一个报文的序号 + 前一个报文的数据(不含 TCP首部,也就是TCP中的数据)" 的规则递增。

例如,若发送端某一TCP报文的序号为 5,数据净荷长度为 12 字节,那么下一个发送的报文段,其序号应设置为 5+12=17。不过序号增长到一定程度就会回绕回起点。序号的作用就是保证数据的按序到达以及去重。

确认序号表示 "接收端期望收到的下一个字节的序号",同时也确认 "序号小于该值的所有字节都已正确接收"。确认序号和序号一样,增长到一定程度就会回绕回起点。

若收到带应用层数据的报文,确认序号 = 收到的报文序号 + 对应数据字节数;

若收到无应用层数据的报文且是 SYN=1 或 FIN=1 的控制报文,确认序号 = 收到的报文序号 + 1;

注:纯 ACK 报文段(无 SYN、无 FIN、无应用层数据)不消耗序号,不会触发确认序号的递增。

首部长度表示TCP首部的总长度(单位是4字节),TCP首部的总长度是指TCP协议除去数据部分的总长度。所以TCP头部最大长度是15 * 4 = 60字节。因此选项的最大长度为40个字节。

保留六位是指预留的6位字段,目前未使用,默认全为 0。

再然后的就是六个控制位,每个位为1表示启用对应功能,各个功能如下:

URG(紧急):表示报文中包含紧急数据,此时 "紧急指针" 字段有效。

ACK(确认):表示 "确认序号" 字段有效,用于确认已收到的数据(TCP通信中,除了初始的三次握手的第一个SYN报文,后续报文的ACK位几乎都是 1)。

SYN(同步):用于建立连接。

FIN(终止):用于关闭连接,表示发送端已无数据要发送,请求断开连接。

PSH(推送):核心作用是强制接收端跳过默认的 "延迟批量交付" 策略,立即将当前缓冲区中的数据交给应用层,以此满足数据传输的实时性需求。

RST(复位):表示 "重置连接",当TCP连接出现异常时,用RST强制关闭连接。

窗口大小表示接收端当前的接收缓冲区剩余容量,实际窗口大小 = 窗口大小 * 2 ^ 扩大因子,单位是字节,这个是用来进行流量控制的(扩大因子是通过选项设置的,默认为0)。

检验和是用于检测TCP报文在传输过程中是否出错。

紧急指针仅当 URG 位为1时有效,紧急指针的值是一个偏移量,和序号值相加并减1表示紧急数据最后一个字节的序号。

选项占0到40个字节,TCP固定首部只有20字节,复杂功能都靠选项字段实现。

3.2、确认应答机制

TCP保证可靠性的一个重要的机制就是确认应答机制,收到了确认应答就表示最近发送的消息对方是收到了的。本质是接收端向发送端反馈数据的接收状态。简单来说就是发送端发送了一个消息,接收端要发送一个应答。

如下图所示:

注:客户端和服务器端在基于TCP通信时,无论是确认应答还是其他信息,都是一个完整的TCP报文。

3.3、捎带应答

很多情况下,客户端和服务器在应用层是"一发一收"的,意味着客户端给服务器发送一个消息,服务器也会给客户端回一个消息,那么这个时候ACK应答就可以搭顺风车,和服务器回应的信息一起发送给客户端。

如下图所示:

3.4、超时重传机制

主机A发送数据给主机B,可能因为网络拥堵等原因,数据无法到达主机B;如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发。但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了,此时主机B是收到了主机A的消息的,但是主机A仍然会进行重发,因此主机B可能会收到很多重复数据,那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉,这时候我们可以利用前面提到的序号,就可以很容易做到去重的效果。

那么,超时的时间如何确定?最理想的情况下,找到一个最小的时间,保证 "确认应答一定能在这个时间内返回",但是这个时间的长短,会随着网络环境的不同,是有差异的,如果超时时间设的太长,会影响整体的重传效率,如果超时时间设的太短,有可能会频繁发送重复的包;TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间,超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍,如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传,如果仍然得不到应答,等待 4*500ms 进行重传,依次类推,以指数形式递增,累计到一定的重传次数,TCP认为网络或者对端主机出现异常, 强制关闭连接。

3.5、连接管理机制

正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接,如下图所示:

客户端状态转化:

CLOSED -> SYN_SENT:客户端调用connect,发送连接报文。

SYN_SENT -> ESTABLISHED:connect调用成功,则进入ESTABLISHED状态,开始读写数据。

ESTABLISHED -> FIN_WAIT_1:客户端主动调用close时,向服务器发送结束报文,同时进入 FIN_WAIT_1。

FIN_WAIT_1 -> FIN_WAIT_2:客户端收到服务器对结束报文的确认,则进入FIN_WAIT_2,开始等待服务器的结束报文。

FIN_WAIT_2 -> TIME_WAIT:客户端收到服务器发来的结束报文,进入TIME_WAIT,并发出应答。

TIME_WAIT -> CLOSED:客户端要等待一个2MSL的时间,才会进入CLOSED状态。

服务端状态转化:

CLOSED -> LISTEN:服务器端调用listen后进入LISTEN状态,等待客户端连接。

LISTEN -> SYN_RECV:一旦监听到连接请求,就将该连接放入内核等待队列中,并向客户端发送确认报文。

SYN_RECV -> ESTABLISHED:服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据了。

ESTABLISHED -> CLOSE_WAIT:当客户端主动关闭连接,服务器会收到结束报文,服务器返回确认报文并进入CLOSE_WAIT。

CLOSE_WAIT -> LAST_ACK:进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据),当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)。

LAST_ACK -> CLOSED:服务器收到了对FIN的ACK,彻底关闭连接。

注:CLOSED是一个假想的起始点,不是真实状态,其他的都是真实的连接的状态。三次握手是可以验证双方的网络通路是否是通畅的;在三次握手中,只要把第三个报文发出去,客户端也就认为连接建立好了,服务端要接收到第三个报文才会认为连接建立成功;TCP是允许连接建立失败的。操作系统中会有很多的连接,连接也是要先描述再组织的,操作系统维护连接也是有成本的。另外就是在四次挥手中的中间两次挥手之所以没捎带应答是因为大部分情况下,这两个是时间上是错开的,仅仅只有少数时候,中间的两次挥手才会捎带应答。

3.5.1、accept

connect系统调用仅仅只是发起TCP的三次握手,关于三次握手是由双方的操作系统自动完成的;accept调用不参与三次握手,它只是把已经建立好的连接拿过来,如果连接没建立好,那它就会阻塞住。

3.5.2、listen

listen的第二个参数backlog加1表示底层已经建立好的连接的队列的最大长度,对于这些建立的连接,accept可以拿走它们。这是因为,Linux内核协议栈为一个TCP连接管理使用两个队列:

1、全连接队列:用来管理处于ESTABLISHED状态的连接,但是应用层还没有调用accept取走的请求,随时等着上层accept获取。全连接队列的长度会受到listen第二个参数的影响,全连接队列满了的时候,就无法继续让连接状态进入ESTABLISHED状态了,这个队列的长度一般是listen的第二个参数+1。

2、半连接队列:未完全建立的连接也是需要进行管理的,半连接队列就是管理处于SYN_RECV状态的连接。服务端不会长时间维护SYN_RECV状态,也就是半连接的节点不会长时间维护,定期会进行释放(当全连接队列满时,服务端无法将SYN_RECV状态的连接变为ESTABLISHED,就会出现客户端显示连接为ESTABLISHED状态,但服务端仍处于SYN_RECV状态的不一致现象,不过这个现象具体的体现也会受到内核里面一些参数的影响)。

为什么listen的第二个参数不能太大,又不能太小?原因就是太大可能会消耗过多的资源,太小又可能会导致无法及时的获取连接。

3.5.3、TIME_WAIT

主动断开连接的一方,在四次挥手完成后,要进入TIME_WAIT状态,等待若干时长之后,会自动释放。因为处在TIME_WAIT状态时连接还没有彻底断开,因此IP和port还正在被使用,所以服务器端的服务挂掉后,是没法立即重启的,会出现下面的现象,例如:

我们可以使用下面的调用来解决这个问题,这个接口是用来设置套接字属性的,如下:

cpp 复制代码
#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname, const void optval[.optlen], socklen_t optlen);

其中,sockfd是要设置的套接字描述符;level指选项所在的协议层级;optname指要设置的具体选项名;optval选项的值的指针;optlen是指optval数据的长度。

常用的方式如下:进行复用,如果服务器历史上已启动正常的服务,那再启动该服务依然会启动失败;如果是因为TIME_WAIT状态,那么可以启动。

cpp 复制代码
// 允许端口复用(TIME_WAIT 状态下也能绑定)
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

注:客户端是不会受这样的影响的,因为客户端每次启动端口都不固定。

TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL的时间后才能回到CLOSED状态,我们使用Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Ubuntu上可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看MSL的值。如下图所示:

上图中的60也就是1分钟。MSL是TCP报文的最大生存时间,因此TIME_WAIT持续存在2MSL的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的),同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失,那么客户端可以再重发一个FIN,这时虽然服务器端的进程不在了,但是TCP连接还在,仍然可以重发最后的ACK)。总结一下为什么要有TIME_WAIT这个等待时间?原因是为了让通信双方历史数据得以消散,此外,可以让断开连接也就是四次挥手具有更好的容错性。

3.6、流量控制

接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满(极端情形),这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应,因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制

接收端将自己可以接收的缓冲区大小放入TCP首部中的 "窗口大小" 字段,通过ACK通知发送端, 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端,发送端接受到这个窗口之后,就会减慢自己的发送速度,如果接收端缓冲区满了,就会将窗口置为0,这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端,另外接收端的接收缓冲区有空闲时,也会向发送端发送一个窗口更新通知。

如下图所示:

注:三次握手期间双方已经协商了双方的接收能力;确切的说就是三次握手的前两次握手就已经协商了双方的接收能力,第三次握手的时候报文就可以携带数据了,前两次握手是不允许携带数据的。另外三次握手不仅仅只协商了双方的接收能力,还协商了序号。

3.7、滑动窗口

刚才说了确认应答机制,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据,这样做有一个比较大的缺点,就是性能较差,尤其是数据往返的时间较长的时候。既然这样一发一收的方式性能较低,那么我们就发送多条数据,就可以大大的提高性能。

如下图所示:

注:可以通过序号保证数据的按序到达,因为可以通过序号进行排序。

对于已经发送出去的数据,但是还没接收到应答,操作系统是要先保存起来的,因为可能需要超时重传,对于已经发送的,还没有收到应答的多个报文,会被保存在哪里呢?实际上是没有必要单独保存一份的,因为发送缓冲区中本来就有一份。可以对发送缓冲区进行区域划分,如下图所示:

滑动窗口是缓冲区的一部分;滑动窗口的大小不能超过对方接收缓冲区剩余空间的大小;

区域划分的本质是通过指针或者下标来进行区域划分的,窗口滑动本质就是指针右移;窗口不会向左移动,只会向右移动,滑动窗口的大小是动态变化的,甚至可能变为0;滑动窗口向右移动分为以下的情形,一个是右指针不变,左指针向右移动,滑动窗口整体变小,另一个是左右指针都向右移动,此时滑动窗口可能变大,也可能变小,也可能不变。

因为有滑动窗口区域的存在,才可以一次向对方发送大量的TCP报文。滑动窗口会在发送缓冲区中越界吗?TCP中采用了类似于环状的算法,也就是基于数组的环,因此是不会存在越界的。

滑动窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。流量控制就是通过滑动窗口的大小来实现的。

那么如果出现了丢包,如何进行重传? 这里分两种情况讨论,如下:

情况一:数据包已经抵达,ACK被丢了。

这种情况下,部分ACK丢了并不要紧,因为可以通过后续的其他ACK中的确认序号进行确认。

情况二:数据包就直接丢了。

如果发送端主机连续三次及以上收到了同样一个应答,就会将对应的数据进行重新发送,这种机制被称为 "高速重发控制",也叫 "快重传"。如下图所示:

已经有了快速重传,为啥还要有超时重传?一个是快速重传是有条件的,要求三次及以上同样的确认应答,另外快速重传是用来提高效率的,超时重传是用来兜底的。

3.8、延迟应答

如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小。比如:假设接收端缓冲区为1M,一次收到了500K的数据,如果立刻应答,返回的窗口就是500K,但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了,在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来,如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M,窗口越大,传输就越快。延迟应答指接收端收到数据后,不立即发送ACK确认报文,而是等待一小段时间(通常是 200ms 以内,可通过内核参数调整),再决定是否发送 ACK。

延迟应答的策略如下:

有数量限制:每隔N个包就应答一次;

时间限制:超过最大延迟时间就应答一次;

具体的数量和超时时间,操作系统的不同也有差异,一般N取2,超时时间取200ms。

如下图所示:

注:写TCP服务器时,比较推荐的做法就是每次都尽快的使用操作系统的调用接口将数据从内核中拿上来。

3.9、拥塞控制

TCP 协议的设计,既兼顾了通信双方主机的接收与处理能力,也充分考虑了中间网络信道的传输状态。数据传输出现异常,根源可能有两个:一是对端主机,二是网络信道本身。滑动窗口是 TCP 实现高效可靠传输的核心手段,但如果通信初期就向网络发送大量数据,很容易加剧网络负担,毕竟网络中存在海量主机,当前的网络状态可能本就处于拥堵边缘,贸然发送大流量数据,无异于雪上加霜。

拥塞控制如下图所示:

慢启动阶段:TCP引入慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。

此处引入一个概念称为拥塞窗口,开始拥塞窗口大小为1;开始每次收到一个ACK应答,拥塞窗口加1。拥塞窗口的增长速度,是指数级别的,"慢启动" 只是指初始时慢,但是增长速度非常快。

每次发送数据包的时候,TCP会将拥塞窗口大小、接收端主机反馈的窗口大小以及有效发送数据大小做比较,取较小的值作为实际的滑动窗口的大小。简单来说就是滑动窗口的大小等于对方主机窗口大小和拥塞窗口大小以及有效数据大小当中的较小值;滑动窗口的大小不仅仅考虑了对方主机接收能力的大小,还考虑了网络信道本身的接收能力。

拥塞避免阶段 :为了避免拥塞窗口无限制指数膨胀,TCP 引入慢启动阈值来约束增长节奏,当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长,进入拥塞避免阶段。在TCP启动初期,慢启动阈值设为一个较大的默认值;每当发生网络拥塞时,慢启动阈值会被调整为当前拥塞窗口大小的一半。

当出现丢包时,TCP 会根据丢包严重程度判定网络状态,并执行不同的应对策略:

少量丢包:触发快速重传,一般判定为偶发乱序或轻微拥塞,不会重置拥塞窗口;

大量丢包:触发超时重传,判定为网络拥塞。此时 TCP 会将慢启动阈值调整为当前拥塞窗口的一半,同时将拥塞窗口重置为1,重新进入慢启动阶段。

注:在网络拥塞时并不是只有一台主机在进行拥塞控制,而是所有识别网络拥塞的主机都会进行拥塞控制。拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。

3.10、TCP的特点

3.10.1、面向字节流

创建一个TCP的socket,同时在内核中创建一个发送缓冲区 和一个接收缓冲区

进行写入时,数据会先写入发送缓冲区中,然后再进行发送,如果发送的字节数太长,会被拆分成多个TCP的数据包发出,如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机再发送出去。

接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可以调用接口从接收缓冲区拿数据。

另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据,这个概念叫做全双工

由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:

写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节。

读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read读100个字节,也可以一次read读一个字节,重复100次。

2.10.2、粘包问题

首先要明确,粘包问题中的 "包",是指的应用层的数据包。在TCP的协议头中,没有如同UDP一样的 "报文长度" 这样的字段,但是有一个序号这样的字段。站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。站在应用层的角度,看到的只是一串连续的字节数据,那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包。

那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界。

对于定长情形:保证每次都按固定大小读取即可。

对于变长的情形:可以使用特殊字符进行分隔,也可以使用自描述字段加定长的报头。例如可以在报头的位置,约定一个报文长度的字段,从而就知道了包的结束位置;还可以在包和包之间使用明确的分隔符(应用层协议,是程序员自己来定,只要保证分隔符不和正文冲突即可)。

2.10.3、TCP异常情况

进程终止(包括异常和正常终止):进程终止会释放文件描述符,仍然可以发送FIN,连接正常断开。

机器重启或关机:重启或者关机都先要杀死所有的进程,和进程终止的情况相同,连接正常断开。

机器掉电或网线断开:另一端认为连接还在,一旦另一端有写入操作,另一端发现连接已经不在了,就会进行连接关闭;即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放。

注:另外应用层的某些协议,也有一些这样的检测机制, 例如HTTP长连接中,也会定期检测对方的状态。

3.10.4、基于TCP的应用层协议

1、HTTP

2、HTTPS

3、SSH

4、Telnet

5、FTP

6、SMTP

当然,也包括自己写TCP程序时自定义的应用层协议。

3.11、用UDP实现可靠传输

参考TCP的可靠性机制,在应用层实现类似的逻辑。

例如:

1、引入序列号,保证数据顺序。

2、引入确认应答,确保对端收到了数据。

3、引入超时重传,如果隔一段时间没有应答,就重发数据。

4、等等。

4、文件与网络套接字

Linux中一切皆文件的思想,网络套接字也是一种文件。如下图所示:

注:封装和解包操作,不需要繁琐的拷贝,仅仅只是指针的移动而已。

另外,再介绍一个命令,叫scp命令,作用是进行远程拷贝。

相关推荐
hmywillstronger2 小时前
【Rhino】【Python】对包含特定关键词的文字的MTEXT对象添加指定内容
linux·服务器·python
ICT董老师2 小时前
通过OpenSSL 生成自签名证书
linux·运维·服务器·https·ssl
詹某某34112 小时前
什么是 IP SSL 证书?该如何申请
服务器·https·ssl
skywalk81632 小时前
cbsd的clonos/control-pane web管理页面一直闪烁和网页打开显示500error 的问题解决(500error问题未解决)
服务器·前端·freebsd·cbsd
2501_945837432 小时前
火山引擎边缘云服务器,AI+5G协同赋能实时场景
服务器
weixin_436525072 小时前
若依多租户版 - modules中创建子模块
java·服务器·前端
m0_737302582 小时前
火山引擎安全增强型云服务器,筑牢AI时代数据屏障
网络·人工智能
板面华仔2 小时前
Linux基础(下)——工作中常用命令总结
linux·运维·服务器
RisunJan2 小时前
Linux命令-let(执行算术运算)
linux·服务器