网络基础 -- 传输层协议(UDP与TCP/三次握手与四次挥手/可靠传输)

 

目录

传输层

端口号

UDP协议(User Datagram Protocol, 用户数据报协议)

UDP报文格式

UDP的特点

协议实现(原理) / 特性对于上层应用层代码编写的影响 (我们用UDP协议时该注意什么?)

TCP协议(Transmission Control Protocol, 传输控制协议)

TCP报文格式

TCP三次握手与四次挥手

扫描二维码关注公众号,回复: 10812512 查看本文章

close()shutdown()的区别及使用场景

三次挥手和四次挥手中的一些问题

TCP特点

面向字节流

可靠传输


传输层

传输层负责应用程序之间的数据传输, 也就是负责数据能够从发送端传输到接收端 .

一个发送的数据在网络中, 用IP地址来确定这条数据来自哪台主机(源IP地址), 要发往哪台主机(目的IP地址). 但当一台主机接收到一条网络数据时, 主机里有不止一个应用程序在运行着, 谁来接收这条网络数据呢 ? 所以还需要一个信息来标识, 这条数据是哪个应用程序的, 这个标识就是端口号. 

源IP地址和目的IP地址是在传输层下层的网络层IP协议中封装在头部的. 而端口号封装在传输层协议的头部.

这是为什么呢? 因为传输层的作用就是负责应用程序之间的数据传输, 在网络上中数据时如何传输是下层网络层该操心的事, 当数据到达对端主机时, 哪个应用程序接收才是传输层关心的事, 所以IP地址封装在网络层, 端口号封装在传输层.


端口号

在传输层的上层应用层中, 用一些常用协议搭建的服务器都有一些固定端口号, 如下:

  • http 服务器 : 80端口
  • https 服务器 : 443端口
  • ssh 服务器 : 22端口
  • ftp 服务器 : 21端口
  • telnet 服务器 : 23端口

在Linux中, 在 /etc/services 中保存着这些知名端口,  我们自己写的程序要避开这些知名端口

端口号的划分范围

  • 0 ~ 1023 : 知名端口号, 上面所说的广泛使用的应用层协议, 它们的端口号是固定的.
  • 1024 ~ 65535 : 操作系统动态分配的端口号, 客户端程序的一般不主动绑定固定端口号的原因就是就是由操作系统动态分配, 避免端口号冲突.

问题

1. 一个进程是否可以bind 多个端口号? (bind指bind()接口, 用于给socket套接字绑定自己的地址信息(包括源IP地址和源端口号))

 答 : 一个进程可以bind多个端口

2.一个端口号是否可以被多个进程 bind?

答  : 一个端口号只能被一个进程bind

注 : 在Linux中, 这里的一个进程可以理解为一个PCB, 因为Linux中的线程是轻量级进程, 所以线程也有相同特性, 所也就能说是一个PCB可以bind多个端口号, 一个端口号只能被一个PCB所bind.

相关命令

netstat : 用于查看Linux中网络状态

  • n 拒绝显示别名,能显示数字的全部转化成数字
  • l 仅列出有在 Listen (监听) 的服务状态
  • p 显示建立相关链接的程序名
  • t (tcp)仅显示tcp相关选项
  • u (udp)仅显示udp相关选项
  • a (all)显示所有选项,默认不显示LISTEN相关

pidof : 用于查找指定名称的进程的进程id

语法 : pidof 进程名


UDP协议(User Datagram Protocol, 用户数据报协议)

UDP报文格式

图片来源于网络
  • 16位源端口号 :  标识本机的哪个应用程序发送的数据
  • 16位目的端口号 : 标识要给对端主机哪个应用程序发送数据
  • 16位UDP长度 : 标识整个UDP报文(首部 + 数据)的最大长度
  • 16位校验和 : 在TCP协议, 网络层的IP协议等协议的报文头部中都要, 作用是检验所传输的报文数据是否出错, 出错则丢弃.
    检测原理 : 发送数据时, 将校验和设置为0, 然后从所要发送数据的第一个字节开始, 每个字节进行取反相加, 溢出时, 溢出部分再从低位进行相加, 最终得到的数字填充到检验和中. 在接收数据时, 直接拿出校验和计算, 用和发送时同样的. 如果最后结果为0, 则表示发送的数据与接收的数据一致, 否则数据不一致, 直接丢弃.

UDP的特点

  • 无链接 : 直到对端的IP和端口号就能直接进行传输, 无需建立连接
  • 不可靠传输 : 没有确认应答机制, 没有重传机制, 没有进行包序管理, 流量控制等机制, 相对于TCP来说非常不可靠
  • 面向数据报 : 收发数据报文时整条一次收发完成, 不够灵活

协议实现(原理) / 特性对于上层应用层代码编写的影响 (我们用UDP协议时该注意什么?)

1. 由于UDP报头中只用了16位2字节来标识整个UDP报文的大小, 所以, UDP报文的大小是有限制的, 16位最大能表示的数是65535, 即UDP报文最大是65535个字节, 即64K, 但报文中头部还占了8字节, 所以, UDP所能发送的数据最大是64k - 8. 但由于传输层协议在下层网络层中还要进行封装, 而网络层的IP协议中, 整个IP报文最大也只能是63K, 而IP协议报文头部最小也要20字节, 这样一来, UDP报文中数据最大也只能是64k - 20 - 8, 如下图:

                                                    

注意 : 值得注意的是, 这里限制UDP报文大小的根本原因不是UDP长度标识是16位的数据, 如果是这样, 那要想数据再长些给更多位不就好了. 其实UDP报文不能太大的根本原因是UDP协议是面向数据报传输, 也就是在调用sendto()接口发送数据时, socket会将数据直接交给内核封装UDP报头然后一次性发送出去 (注意 : UDP是没有真正意义上的发送缓冲区的), 如果这个数据很大, 那么发送失败的几率就很大, 所以, UDP报文数据不能太大的原因就是因为其面向数据报传输, 如果数据很大, 发送失败的几率就很大

刚说到, UDP没有真正意义上的缓冲区, 什么意思呢? 因为UDP是不可靠传输, 其发送失败也不会保存应用程序的数据拷贝(比如说, 应用程序产生了一个临时的数据需要发送, 之后就会销毁这个数据, 此时UDP发送失败了, 那么这条数据就没有了, 因为这个数据是在应用程序中被销毁, 而UDP也没有将其先放在发送缓冲区中).

2. 如果发送的数据大于64K-20-8, 则不能一次性发送, 需要用户在上层应用层手动进行分包操作(将一个大数据截断为多个小数据,分别发送). 由于UDP协议是不可靠传输, 发送的数据并不能有序到达, 还可能会丢失, 所以需要我们程序员手动在应用层进行包序管理.

3. 在使用UDP协议时, 由于收发都是整个报文一次性收发, 所以用户的接收缓冲区要定义的足够大, 否则recvfrom()接收缓冲区满了之后之后的数据就会接被丢弃.


TCP协议(Transmission Control Protocol, 传输控制协议)

TCP报文格式

图片来源于网络
  • 16位源端口号 :  标识本机的哪个应用程序发送的数据
  • 16位目的端口号 : 标识要给对端主机哪个应用程序发送数据
  • 32位序号(seq) : 用来标记数据段的顺序,TCP把连接中发送的所有数据字节都编上一个序号,第一个字节的编号由本地随机产生. 给字节编上序号后,就给每一个报文段指派一个序号. 序列号seq就是这个报文段中的第一个字节的数据编号.
  • 32位确认序号(ack) : 期待收到对方下一个报文段的第一个数据字节的序号. 序列号表示报文段携带数据的第一个字节的编号. 而确认号指的是期望接收到下一个字节的编号;因此当前报文段最后一个字节的编号+1即为确认号. 确认号的字段只在ACK标志为1时才有效.
  • 4位数据偏移(或称为首部长度) :  用于表示TCP报文首部的长度. 单位是字节, 一个TCP报文前20个字节是必有的, 后40个字节根据情况可能有可能没有. 如果TCP报文首部是20个字节, 则首部长度为 20/4 = 5 .
  • 6位保留位 : 目前必须是0, 因为设计之初还没想好用来干嘛, 为将来定义新用途保留的.
  • 6位标志位 : 6个标志位各占一位
        • URG -- 紧急指针标志位 : URG=1时, 紧急指针字段有效.URG=0时, 无效
        • ACK -- 确认回复标志位 :  ACK=1时, 确认号字段才有效. ACK=0时, 无效
        • PSH -- 提示立即接收位 :  PSH=1时, 表示接收方应立即将数据交给应用层处理
        • RST -- 重置连接位 : RST=1时, 请求重建一个当前连接, 用来复位产生错误/混乱的连接, 也用来拒绝错误和非法的数据包
        • SYN -- 连接建立请求位 : 连接建立时用于同步序号. 当SYN=1, ACK=0时表示, 这是一个连接请求报文段(客户端向服务端请求). 若(服务端)同意连接, 则在响应报文段中使得SYN=1, ACK=1. 因此, SYN=1表示这是一个连接请求, 或连接接受报文。SYN这个标志位只有在TCP建产连接(三次握手)时才会被置1, 握手完成后SYN标志位被置0.
        • FIN  --  断开连接请求位 : 用来释放一个连接. FIN=1表示:此报文段的发送方的数据已经发送完毕, 并要求释放连接
  • 16位窗口大小 : TCP流量控制由连接的每一端通过声明的窗口大小来提供
  • 16位校验和 : 和其他协议中的校验和作用一样, 用于收到的数据段(首部+数据)是否出错.
  • 16位紧急指针 : URG = 1时, 该字段有效, 有效时指向数据中优先部分的最后一个字节, 通知接收方紧急数据的长度.
  • 最长40位选项 : 长度为0 ~ 40字节, 必须以4字节为单位变化, 必要时可以填充0. 通常包含: 最长报文大小(MaximumSegment Size,MSS)、窗口扩大选项、时间戳选项、选择性确认(Selective ACKnowlegement,SACK)等.

TCP三次握手与四次挥手

三次握手建立连接

第一次握手 : 

     client  : 客户端向服务端发送连接请求SYN包(发送连接请求)(SYN=1, 同时选择一个初始序号seq=x)后, 客户端进入SYN-SENT状态, 等待服务器确认回复.

     server :当服务端还没有接收到客户端的连接请求时, 服务端处于LISTEN状态.

第二次握手 : 

     server : 当服务端收到客户端的连接请求时(收到syn包), 为新的连接请求创建新的通信socket, 此时服务端必须确认客户端的SYN请求(回复确认序号ack = x + 1), 确认序号有效ACK=1, 因为连接是双向的, 所以服务端也向客户端发送连接请求SYN包(SYN = 1, 为自己选择一个初始序号seq = y), 即服务端向客户端发送 ACK+SYN 包, 服务端进入SYN_RCVD状态. 当第二次握手完成, 还没进行第三次握手时, 此时TCP连接的状态称之为半连接状态.

第三次握手 : 

      client : 当客户端收到服务端回复的SYN+ACK包时, 确认建立连接(客户端这边已经没什么问题了, 可以通信了), 并回复给服务端确认信息ACK包(seq = x + 1, ack = y+1), 客户端的进入ESTABLISHED状态, 完成连接

      server : 当服务端收到客户端发送的ACK包后, 确认客户端连接就绪, 可以开始通行, 进入ESTABLISHED状态 


 四次挥手断开连接

图片来源于网络

第一次挥手 :

      client : 当客户端确定不再需要发送数据时, 调用 close(sockfd) / shutdown(sockfd, SHUT_WR) (两者的区别以及用法下面说). 客户端会向服务端发送FIN包(FIN=1, seq = u)(u就是客户端之前收到的数据的最后一个字节的序号+1), 客户端进入FIN_WAIT1状态.  (注意 : TCP协议规定, FIN报文段就算没有数据, 也需要消耗一个序号)

      server : 当服务端未收到客户端发送的FIN包时, 一直处于ESTABLISHED状态

第二次挥手 :

      server : 当服务端收到客户端发来的FIN包后, 知道客户端不会再发送数据了, 也就不需要接受, 先调用close(sockfd) / shutdown(sockfd, SHUT_RD), 再确认回复客户端, 即(ACK=1, ack = u+1),  并且带上自己的序列号seq=v, 此时服务端的进入了CLOSE_WAIT状态. TCP服务端就通知高层的应用进程, 客户端不会再向服务端发送数据了, 此时TCP通信的连接状态就称为半关闭状态,即客户端已经没有数据要发送了, 但是服务器若发送数据, 客户端依然要接受. 这个状态还要持续一段时间, 也就是整个CLOSE_WAIT状态持续的时间.

     client : 当客户端收到服务器的确认请求(ACK包)后, 此时, 客户端就进入FIN_WAIT2状态, 等待服务器发送FIN (在这之前还需要接收服务器发送的最后的数据)

第三次挥手 : 

     server : 服务端将最后的数据发送完毕后, 再不需要发送数据了, 就调用shutdown(sockfd, SHUT_WR) , 再向客户端发送FIN包, 由于在半关闭状态, 服务器很可能又向客户端发送了一些数据, 假定此时的序列号为seq=w,即FIN包(ACK=1, seq=w, ack=u+1). 此时, 服务端就进入了LAST_ACK(最后确认)状态, 等待客户端的确认.

第四次挥手 : 

    client : 当客户端收到服务端的连接释放请求(FIN包)时, 必须发出确认, ACK=1,ack=w+1, 而自己的序列号是seq=u+1, 此时,客户端就进入了TIME_WAIT状态. 注意 : 此时TCP连接还没有释放, 必须经过2倍的MSL(最长报文段寿命)的时间后, 当客户端释放连接后, 才进入CLOSED状态.

    server : 服务端只要收到了客户端发出的确认(ACK包), 立即进入CLOSED状态. 同样, 释放TCB连接后, 就结束了这次的TCP连接. 可以看到, 服务端结束TCP连接的时间要比客户端早一些.

注意 : 需要注意的是, 四次挥手可以是由客户端首先发送FIN包触发, 也可以由服务端首先发送FIN包触发.


close()shutdown()的区别及使用场景

由于TCP是全双工双的, 所以连接的断开也需要单独将两个通道拆除, 四次挥手所做的事情就是拆除两条通道释放资源.

那么已经完成三次握手的TCP连接, 什么时候会触发四次握手呢 ? 从逻辑上, 是通信结束, 不需要这个TCP连接时, 从具体操作上, 就涉及到两个系统调用接口close()和shutdown()

对于close(), 当调用close(sockdfd)只会造成套接字文件描述符的引用计数减一, 当引用计数为0时, 调用方主动发送FIN包, 触发四次挥手. 例如在多进程服务器中, 父子进程共享socket文件描述符, 当父进程或者某个子进程调用close(sockfd) 时, socket文件描述的引用计数减一, 直到父进程和所有的子进程都调用close(sockfd)后(引用计数为0), 才会发生四次挥手, 释放资源.

对于shutdown(sockfd, how), 是拆分四次挥手过程, 在设置how参数为SHUT_WR或者SHUT_RDWR时, 会立即发送FIN, 触发四次挥手. 无论socket文件描述符引用计数是多少,  只要任一进程调用该接口都会破坏所有进程的连接, 任一进程在该描述符上读取数据都会收到EOF结束符, recv()返回0, 写数据时会收到SIGPIPE信号. 因为其并不释放资源, 所以最后还需要调用close().

对于FIN包, shutdown()和close()都能发送FIN包, 对于发送FIN包, 是想要告诉对方, 我不再向你发送数据了

close()和shutdown()的使用场景 : 多进程服务器, 就不能用shutdown()只能用close(), 客户端可以用shutdown(), 也可以用close(). 多进程服务器端, 每监听到一个连接就会创建一个子进程, shutdown会关闭服务器监听工作, 也会关闭其他正在与不同客户端通信的进程. 客户端可以使用shutdown(), 因为客户端和服务器端一般只有一条连接,可以使用shutdown(). 当客户端与服务器端有多条连接且是在同一个进程中, 最好用close(), 以免影响到其他连接通信.

int shutdown(int sockfd, int how) 

头文件 :  sys/socket.h

功能 : 关闭socket的全部或部分全双工连接, 只关闭其读写功能, 并不释放资源

参数 : sockfd : socket的操作句柄, 即文件描述符
          how : 有三种选择 : SHUT_RD : 值为0, 关闭读端
                                         SHUT_WR : 值为1, 关闭写端
                                         SHUT_RDWR : 值为2, 关闭读端和写端

由于TCP是全双工通信, 所以会存在半连接状态. 即当主动端关闭写端代表不再发送数据, 被动端关闭读端代表不再接收数据(但还是能对收到的数据进行确认回复).

返回值 : 正确执行返回0, 错误返回-1, 并设置errno


int close(int fd)

头文件 : unistd.h

参数 : fd : 文件描述符

返回值 : 正确执行返回0, 错误返回-1, 并设置errno


三次挥手和四次挥手中的一些问题

1. 为什么不是两次/四次握手而是三次握手?

    答 : 两次不安全, 四次没必要;  三次握手完成两个重要功能, 一是确保通信双方都准备好了(双方各自都知道对方准备好了), 二是双方就初始序列号进行协商(这个序列号在握手过程中被发送和确认).

两次不安全 : 假设一个客户端像一个服务端发送连接请求报文段(SYN包), 服务器端收到SYN包并回复确认应答报文段(ACK+SYN), 如果是两次握手那么此时连接已建立, 可以开始通信(发送数据报文段).  如果有以下可能, 就会造成不安全的情况. 如 :  服务端的ACK包在传输过程中丢失, 那么客户端就不知道服务端是否已经准备好, 不知道服务端建议用什么样的序号用于客户端和服务端之间的传输, 也不知道服务器是否同意自己发送的初始序列号, 甚至怀疑服务端是否收到自己发送的的SYN包
                                                       
在这种情况下, 客户端既不会向服务端发送数据报文段, 也不会接收服务端发送来的数据报文段(就算服务端发送了, 客户端还以为没建立连接, 当然不会接收), 服务端发给客户端的数据报文段没有得到客户端的确认应答, 服务端就开始只等待接收客户端的确认应答报文段了, 而等到服务器怀疑自己发出的数据报文段没有被客户端收到时, 就会重复发送同样的数据报文段(超时重传机制, 下面说), 就形成了死锁.     或者当客户端像服务端发出SYN包之后就关闭了(断网断电...), 服务端会出现相同的问题. 

                       

四次没必要 : 由于握手要确保通信双方都准备好了(双方各自都知道对方准备好了), 不仅仅客户端要向服务端发送连接请求SYN包, 服务端确认应答回复给客户端ACK包. 还要服务端向客户端发送连接请求SYN包, 客户端在确认应答回复给服务端ACK包, 这样双方都就放心了, 但中间服务端给客户端先回复ACK包, 再次向客户端发送请求SYN包, 这就有点憨了, 何不一起置1, 一次发送呢? 所以四次可以, 但没必要.

2. 为什么不是三次挥手而是四次挥手?

疑问 : 建立连接时需要三次握手, 那断开连接反着来不就好了, 不也只需要三次吗?

答 : 前面说到, FIN包的发送并不表示发送方不发送数据也不接收数据了, 而是告诉对方我不再给你发送数据报文段了, 但不代表发送发不再接收数据报文段了, 所以当客户端向服务端发送FIN包后, 服务端需要确认回复ACK包, 但不能像三次握手一样, ACK和FIN同时置1回复, 因为客户端(虽然不能发送数据报文段了, 但)还可能要接收服务端发送的的数据报文段, 所以服务端要先给客户端回复ACK包, 再向客户端发送FIN包, 在服务端发送ACK包和FIN包之间服务端还可能会向客户端发送数据报文段. 所以在客户端回复ACK包之后, 确定自己不再向客户端送数据报文段了时, 才会向客户端发送数据报文段.

3. 为什么握手三次, 挥手四次?

答 : 也就是前两个问题, 因为当服务端端收到客户端的SYN连接请求报文(SYN包)后, 可以直接发送SYN+ACK报文. 其中ACK报文是用来应答的, SYN报文是用来请求连接的. 但是关闭连接时, 当服务端收到FIN报文时, 很可能并不会立即关闭SOCKET, 所以只能先回复一个ACK报文, 告诉客户端, "你发的FIN报文我收到了". 只有等到服务端所有的报文都发送完了, 才能向客户端发送FIN报文, 因此不能一起发送. 所以需要四次挥手.

4. 三次握手失败了, 服务端如何处理?

    1. 如果是客户端发送的SYN包没有到达服务端, 服务器根本不知道有这个请求, 因此服务器没什么要处理的

    2. 客户端发送了SYN后服务端收到并进行了SYN+ACK报文的响应, 但没有收到客户端应答的ACK包. 服务端等待最后一个ACK超时后, 则会给客户端发送RST重置连接报文, 要求对方重新发起连接请求, 释放为本次连接请求创建的socket, 而并非重传SYN+ACK

5. 为什么FIN包最先发送方发送了ACK包后, 并没有直接进入CLOSED状态释放资源, 而是进入TIME_WAIT状态呢?
   (为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSED状态?) (TIME_WAIT的作用?)

 答 : 讲道理, FIN包先发送方在ACK包发送完了之后, 就可以直接进入CLOSED状态, 释放资源了, 但我们的网络不是一定可靠的, 有可能最后一个ACK包会丢失, 假设没有TIME_WAIT状态, 客户端在发送最后一个ACK包之后直接进如CLOSED状态释放资源, 客户端关闭. 而这个ACK包丢失在网络中, 服务端因为没有收到客户端ACK包, 向客户端超时重传了FIN包, 在服务端向已经关闭的客户端重传FIN包之前将客户端再重启. 如果重启后的客户端地址信息与之前关闭的客户端相同, 有两种情况 :
  1. 刚启动的客户端就会收到一个服务端的FIN包. 就会对接下来客户端与服务端的连接造成影响.  
  2. 重启的客户端向服务端请求新连接发送FIN包, 服务端正在四次挥手, 处于LAST_ACK状态. 突然客户端来个SYN包, 服务端就会认为状态错误, 向客户端发送RST连接重置报文(RST包), 要求客户端重新建立连接. (分手分到一半, 对方说发个消息说做我女(男)朋友好吗, 懵不懵?, 卑微的我就心里想 : "我就当你脑子抽风了". 然后告诉对方, 我们重新来过吧)

所以, 在FIN包先发送方在ACK包发送完了之后, 进入TIME_WAIT状态进过两个MSL时间后再进入CLOSED状态, 就是为在这段时间内处理服务端可能重传的SYN包, 为了本次连接的所有数据都被处理或消失于网络之中, 不会对后续新连接造成影响.
MSL : 报文最大生存周期, 任何报文在网络上存在的最长时间, 超过这个时间报文将被丢弃(一个报文在网络中总不能一直传一直传, 可能造成占用大量带宽等问题, MSL就是限制报文在网络中一直传输的一种方式)

总结 : TIME_WAIT的作用 : 1. 可靠地实现TCP全双工连接的终止  2. 让"老"的数据在网络中被丢弃

5. 服务器上出现了大量的CLOSE_WAIT状态是什么原因?

CLOSE_WAIT的危害在于, 在一个端口上打开的文件描述符超过一定数量, (在linux上默认是1024, 可修改), 新来的socket连接就无法建立了.

答 : CLOSE_WAIT是被动方收到FIN包进行回复后还没向主动方发送FIN时的状态, 等待调用close(sockfd) / shutdown(sockfd, SHUT_WR).  服务器中出现大量的CLOSE_WAIT很大可能是对于大量的套接字连接断开之后, 没有调用clsoe()关闭, 释放资源, 是代码不够健壮造成的

6. 服务器出现大量的TIME_WAIT是什么原因?

答 : TIME_WAIT是主动关闭方, 在发送最后一个ACK包之后的状态, 意味着服务器上大量主动的关闭了连接, 通常出现在爬虫服务器上. 解决方法如下 :
   1. 开启地址复用 : 如果服务器重启时需要对端口号以及socket地址进行复用,从而避免了TIME_WAIT状态(接口 : setsockopt() )

   2. 设置MSL时间, 设置的更短一些.

7. SYN 泛洪(flood)攻击

攻击方的客户端只发送SYN包发送给服务器,然后对服务器发回来的SYN+ACK什么也不做, 直接忽略掉, 也就是不发送ACK包给服务器. 当有大量的SYN flood 攻击时, 半连接队列就会被占满, 会导致正常的客户端连接无法连接上服务器

SYN flood攻击的方式其实也分两种, 第一种, 攻击方的客户端一直向服务端发送SYN包, 对于服务器回应的SYN+ACK什么也不做. 也就是不给服务端回复ACK包.  第二种,攻击方的客户端发送SYN包时, 将源IP改为一个虚假的IP, 然后服务器将SYN+ACK发送到虚假的IP, 这样当然永远也得不到ACK的回应.

8. 如果已经建立了连接,但是客户端突然出现故障了怎么办?

答 : TCP还设有一个保活机制, 其中有一个保活计时器.  显然, 客户端如果出现故障,服务器不能一直等下去,白白浪费资源. 服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为7200s, 即2小时, 若两小时还没有收到客户端的任何数据,服务器就会每隔75秒发送一个探测报文段, 若一连发送9个探测报文, 客户端仍然没反应,服务器就认为客户端出了故障, 断开连接,上层(应用层)体现, recv()返回0, send()触发异常


TCP特点

  • 有链接 : 需要三次握手建立连接, 断开连接要四次挥手
  • 面向字节流 : 可靠的, 有序的, 双向的, 基于连接的字节流传输方式
  • 可靠传输 : 保证数据能够有序, 可靠的到达对端

有连接上面已经写完, 接下来再看面向字节流和可靠传输 .


面向字节流

可靠的, 有序的, 双向的, 基于连接的字节流传输方式

字节流传输 : 发送的数据都会放到发送缓冲区中进行缓冲, 字节流传输不关心缓冲区有多少数据, 会根据自己实际情况选择一次发送的数据大小, 然后从缓冲区中取出合适大小的数据进行封装.

面向字节流传输 : 传输灵活, 并不限制上层发送的数据大小以及接收的数据大小. (字节流就像生活中的水流一样)

字节流的弊端 : 粘包

粘包就是多条数据粘连在一起被TCP作为一条数据进行发送或交付给上层(接收), 粘包可能发生在接收端或发送端.

本质原因 : TCP对上层数据边界并不敏感(因为其不关心上层是什么数据, 有多少数据, 只管从缓冲区中取出合适大小的数据进行发送(给网络层)或交付(给应用层)).  当发送端发送的数据较小, 需要等多个数据填满缓冲区才发送出去, 就会造成粘包, 或者接收方不及时接收缓冲区的包, 造成多个包接收在缓冲区中

不是所有的粘包现象都需要处理, 若传输的数据为不带结构的连续流数据(如文件传输), 则不必把粘连的包分开(分包). 但在实际工程应用中, 传输的数据一般为带结构的数据,这时就需要做分包处理.

解决方案: 既然TCP在传输层并不不对数据进行边界管理, 那么就需要在应用层我们程序员自己进行边界管理, 

  • 1. 使用特殊字符进行间隔, (在HTTP协议中的典型应用, 用\r\n\r\n来标识头部结尾) .(缺点 : 数据中如果有特殊字符, 就需要进行转义)
  • 2. 定长数据. (缺点 : 小数据(没有规定长度长), 就只能补全, 会造成资源浪费)
  • 3. 将数据长度与数据一起发送

注意 : UDP不会产生粘包现象, 因为其传输方式是面向数据报的, 并且其头部中有数据长度


可靠传输

写在另一篇中: 戳链接( ̄︶ ̄)↗ : https://blog.csdn.net/qq_41071068/article/details/105474889

发布了232 篇原创文章 · 获赞 720 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/qq_41071068/article/details/105434602