紧跟网络应用层协议
http等 应用层上层协议将自己的数据发送给下层传输层协议。

端口

数据经过网络传输到主机后,系统根据端口号确认数据给那个应用程序。

端口存在与传输层,使用端口与应用层关系。在传输层的协议报头中可以看到

端口号标记了一个主机上进行通信的不同程序

在TCP/IP协议中使用源IP,源端口号,目的IP,目的端口号来标识一个通信(netstat -n)。

端口号的范围

  • 0-1023:知名端口号 (http、ssh等)这些端口号都是固定的

  • 1024-65535:操作系统动态分配的端口号,客户端程序的端口也在这个范围。

注意:

  • 一个进程可以绑定多个端口
  • 一个端口只能绑定一个进程
  • Socket是一层系统调用,存在应用层与传输层之间。
  • 传输层及其往下都在系统内核中

㈠ UPD协议(用户数据报协议)

UDP在向上交付数据时,只将有效载荷数据交给上层,报头信息不上交

1. UDP协议格式

在这里插入图片描述
其中:

  1. 源端口号与目的端口号是在上层调用sendto等函数有struct sockaddr*字样,调用函数陷入内核后初始化的。
  2. 16位UDP长度:整个UDP(报头+正文)的长度
  3. 16位UDP校验和:发送时填充校验,接受时二次校验。如果两次校验结果相同,代表这次通信没有错误。反之错误直接丢弃

注意:

UDP实现报头与有效载荷分离:

  • UDP报头的长度是8字节(确定的),是定长报头。其中底层读取8个字节后,剩下的就是报有效载荷,这样实现分离。

UDP将有效载荷传递给上层的协议:

  • UDP的上层是应用层,UDP中有目的端口号,根据目的端口号找到特定的进程。找到特定的进程后上层协议直接与UDP交互。

UDP 报头:

  • 在Linux中UDP的报头是用结构体的位断实现的。

eg:UDP报头类型

struct udp_hander {
	unsigned int src_port : 16;
	unsigned int dst_port : 16;
	unsigned int src_len : 16;
	unsigned int src_chk : 16;
};

传输层通过端口找到对应进程:

  • 内核通过哈希映射的方式来管理进程pid和端口号。UDP上的端口号通过映射的方式找到进程pid,根据进程pid找到这个进程

2. UDP通信特点与缓冲区

  1. 无连接:只需要对端的ip+端口就可以直接通信,无需要建立连接。

  2. 不可靠:没有确认应答机制,没有重传机制,也不向应用层提供任何错误信息。

  3. 面向数据报:不能灵活的控制读写数据的次数于数量。
    eg:写端sendto发送100个字节,接收端recvfrom必须一次接受100个字节。

  4. UDP具有接受缓冲区:当UDP到达对端时,如果对端来不及处理UDP。在UDP中存在一段缓冲区(接受缓冲区)来暂时储存没处理的UDP协议。如果缓冲区的数据满了,再来的UDP数据会被直接丢弃。但是这个缓冲区不能保证收到的UDP消息顺序与发送时的消息一致

  5. 内核向UDP提供发送缓冲区:UDP没有真正意义上的发送缓冲区,当调用sendto函数时直接将UDP数据交到内核。内核将数据经过网络层传输。

  6. UDP通信是全双工的:UDP的套接字(socket)既可以读取对端,也可以向对端写入(全双工)

注意: 在UDP报头中16位最大长度,说明UDP报文最大为64K。如果要传输的数据大于64K时,需要在应用层手动分包,多次发送,并在对端进行组合。

㈡ TCP协议(传输控制协议)

TCP相比于UDP来讲保证了可靠性,但可靠意味着要做更多的工作。所以TCP相比于UDP要复杂,保证可靠的成本比较高。

1. TCP协议格式

在这里插入图片描述

可以看到TCP协议报头的大小为20字节。与UDP报头相同,TCP报头也是通过结构体的位断来实现的。

数据从应用层拷贝到内核,内核加上TCP报头后交给下层传输到对端。

注意:

报头与有效载荷分离:

  • TCP报文中前20个字节中一定含有4字节的数据偏移(4位首部长度)。

    这里的4位首部长度不是以字节为单位的而是以4字节为单位的

    所以TCP报头最大为15×4字节=60字节(20字节+选项),因为选项也在报头中。

  • 通过报头前20字节的4位首部长度的值,将这个值×4就是报头的大小,通过这种方式实现报头与有效载荷的分离

TCP将有效载荷交给上层协议:

  • TCP中存在目的端口号,根据目的端口号,通过哈希找到对应进程PID(与UDP的方法一致)。

2. TCP可靠性的体现(确认应答机制,序号与确认序号,超时重传机制)

确认应答机制:

对于信息传输来讲可靠性体现在:

当信息A传输到对端,对端有响应B传回。就证明信息A传输时可靠的。

TCP为了保证通信可靠性。一般来讲,发送的数据都会有响应。这种策略在TCP中称为确认应答方式

在这里插入图片描述
序号与确认序号保证数据传输的连续有序:

TCP将每个字节的数据都进行了编号如下图
在这里插入图片描述
32位序号:发送报文的末端序列号

32位确认序号:发送报文的末端序列号+1,告知对方发送报文的末端序列号前的数据已经收到,下次发送从发送报文的末端序列号+1处开始发送
在这里插入图片描述
这两个序号联合起来确保通信的连续有序。32序号表示的是本机信息,32位确认序号是对端信息的确认,(TCP是全双工)缺一不可

超时重传机制下边介绍


TCP接收到报文时报文顺序可能会打乱
eg:
发送ABC接受时为BAC
但TCP在接受报文时会根据报文号进行排序,所以最后在缓冲区的TCP报文最后是跟发送时的顺序是一致的。


设1号TCP报文A(1-1001),2号报文B(1002-2002),3号报文C(2003-3003)
eg:
客户端发送报文A后服务端已经收到,但在发送报文B时丢包。报文C服务端接受到

服务器此时已经收到A报文,又收到报文C。根据A报文大小推算32位确认序号是1002,和C报文的起始序号对不上,说明丢包。服务器响应TCP报文中的确认序号任然是1002,这样客户端会将报文B重新再传一次。保证了数据的连续性。


3. TCP报文成员分析

在这里插入图片描述
前几个字段前文都介绍过,不在赘述。

6位保留字段:

保留指的是这些字段还未使用,未来可能会使用。

16位窗口大小:(流量控制)

TCP传输数据时,为了保证数据的可靠性,TCP在内部存在发送和接受缓冲区。

在TCP缓冲区中进行报文重排(数据有序性),上层来不及读取的报文暂时保存在接受缓冲区中等等。
eg:
在这里插入图片描述
类似生产者消费者模型,上述是客户端向服务器发送信息的过程。

可能客户端网络条件好,发送数据的速度特别快,而服务器拿取数据比较慢。此时接受缓冲区的数据会堆积。
如果没有任何保护措施,接受缓冲区满了后剩余的报文会被丢弃,浪费网络资源。

16位窗口大小就是其中的保护措施。

16位窗口中填写的是本机的接受缓冲区中剩余空间的大小。在接受到对端的报文时,根据确认应答机制将缓冲区中剩余空间的大小通过响应告知对端,当接近为0时对端就会停止发送报文,避免了上述问题。

16位校验和:
接受方在收到报文的时候会检验校验和,错误时将报文丢弃,对端重传。


服务器可能会接受许多报文,这些报文的目的可能不同,TCP报文的目的就是保留字段后6个
标志位来确定的。TCP根据不同的报文有不同的处理逻辑。

⑴SYN

TCP是面向链接的,在通信前客户端与服务器需要发送建立链接报文。此外客户端与服务器断开链接时还需要发送断开连接报文。
SYN位设置为1,称为建立链接报文。

⑵ACK

ACK位被设置为1时,称为确认报文。
一般而言ACK位是经常被设置的(第一次建立链接时ACK位为0,其余时刻大部分为1)

①三次握手建立连接

eg:
TCP建立连接的过程:(三次握手)
在这里插入图片描述

上述过程属于通信细节,在双方系统中,上层不需关心。

套接字中:
connect函数是指发起三次握手请求。
accept函数获取已经建立好的链接。三次握手细节在系统中进行

⑶FIN

FIN标志位设置为1,称为断开连接报文

②四次挥手断开连接

eg:
TCP断开连接过程(四次挥手)

TCP是全双工的。如果客户端与服务器都断开连接,两端都要向对端申请断开连接
在这里插入图片描述

③建立连接与断开连接

通常情况下:客户端与服务器之间的数量比为1:n,系统中通常有多个连接要处理。

Linux操作系统用结构体描述连接属性,通过链表的方法管理多条连接。

建立连接:
系统创建连接结构体,填写连接属性(缓存区位置等)。将这个结构体插入到链表中管理

断开连接:
将连接结构体从链表中删除,释放结构体空间资源。


在这里插入图片描述

⑷URG与16位紧急指针

根据上文可知,TCP发送消息,对端接受顺序是按顺序的。

同样通常情况下:数据在接受端读取时也是按顺序读取的。

但如果后面的报文比较重要,需要优先被上层读取的话就需要URG与紧急指针。

URG标志位设为1,代表这个报文需要考虑16位紧急指针,否则会自动忽略。
16位紧急指针:代表紧急数据在报文中的偏移量。TCP协议中,只能有一个字节大小的紧急数据

eg:
在这里插入图片描述
其中flags有选项
在这里插入图片描述
代表带外数据。通过flags选项可以发送紧急指针。

⑸PSH

根据上文:
TCP通信时存在接受缓冲区,在向对端发送信息时窗口大小携带本端的接受缓冲区的大小。

当上层取接受缓冲区时特别慢时,接受缓冲区长时间处于满状态。对端发送端长时间等待。

此时发送端发送报文,并将PSH设置为1,代表要求接收端尽快将缓冲区的内容交给上层。

缓冲区中存在字段recv_low_water。当缓冲区数据超过recv_low_water时再通知上层读取。PSH设置为1代表取消recv_low_water限制,即便没有超过也通知上层读取

⑹RST

建立连接是三次握手,但在建立连接时可能会失败。因为三次握手最后一次应答不能确认是否对端收到。

在这里插入图片描述
此时客户端认为连接已经建立。但服务器没有收到ACK,所以认为连接没有建立。

此后客户端向服务器发送数据时,服务器认为没有建立连接,但却接受到信息。此时服务器向客户端发送RST报文,让客户端重新建立连接

在这里插入图片描述
(此时如果客户端是浏览器会报错“连接已重置“):
在这里插入图片描述

㈢ TCP协议机制

1. 确认应答机制

上文TCP可靠性哪里已经介绍,不在赘述

2. 超时重传机制

超时重传机制是在TCP代码中实现的

有两种情况:

  1. 丢包

当接受端丢包时,发送端没有收到应答,发送端一段时间后进行重传

  1. 接受端发送的确认应答报文丢包

发送端没有收到应答,发送端还要重发一次报文。
注意:接受方不用担心数据重复。
因为TCP报头中有32位序号,不仅可以保证数据按需到达,还可以根据序号去重。

Linux下TCP为了保证效率,每次重传的时间都是500ms的整数倍。达到一定重传次数时强制关闭连接。
eg:
第一次重传等待500ms,第二次重传等待2×500ms,依次类推,最后如果还没有响应断开连接

3. TCP连接管理机制与状态

为什么建立连接是三次握手?

建立连接三次握手与对应状态图:
在这里插入图片描述

SYN与SYN+ACK:确认了客户端可以向服务器发送接受数据。

SYN+ACK与ACK:确认了服务器可以向客户但接受发送数据。

所以

  1. 通过三次通信就可以保证TCP通信的全双工。所以三次握手为最好

  2. 如果是偶数次建立连接,最后的异常一定挂在服务器上,由服务器来维护。而服务器与客户端数量是1:N。会占用服务器太多资源

为什么断开连接是四次挥手

首先,断开连接不需要再确认双方是否能通信,因为前一直在通信。

断开连接图与对应状态
在这里插入图片描述

所以
FIN与ACK:确保了服务器知道了客户端断开连接的请求,客户端断开了连接。

同理因为TCP是全双工的,所以还需要服务器断开连接
FIN与ACK:确保了客户端知道了服务器断开连接请求,服务器断开连接。

所以断开连接是四次挥手

注意:

  1. 如果只有客户端关闭套接字,那么服务器会处于CLOSE_WAIT状态,会导致服务器连接可用资源变少。
  2. 服务器向客户端发送FIN后,客户端接受到并响应。此时客户端不是处于CLOSE状态,而是TIME_WAIT。因为最后一次ACK可能会丢包,如果客户端直接进入CLOSE状态关闭连接,服务器丢包重发时客户端就收不到了。
  3. TIME_WAIT状态:一段时间后关闭连接进入CLOAE状态,TIME_WAIT等待时间是2×单次通讯最大时间

观察四次挥手CLOSE_WAIT 与TIME_WAIT状态 以及连续重启服务器bind错误

本地测试
注意:这段套接字代码并没有关闭文件描述符,我们需要手动关闭(先关闭客户端,再关闭服务器)观察四次挥手时状态变化

#include<pthread.h>
#include<iostream>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<cstring>

void*FuncTion(void*args);

int main(){
  int listSock=socket(AF_INET,SOCK_STREAM,0);
  if(listSock<0){
    std::cerr<<"socket error"<<std::endl;
    return 1;
  }
  sockaddr_in local;
  memset(&local,0,sizeof(local));
  local.sin_family=AF_INET;
#define PORT 8081
  local.sin_port=htons(PORT);
  local.sin_addr.s_addr=INADDR_ANY;
  if(bind(listSock,(struct sockaddr*)&local,sizeof(local))<0){
    std::cerr<<"bind error"<<std::endl;
    return 2;
  }
#define LISTNUM 5
  if(listen(listSock,LISTNUM)<0){
    std::cout<<"listen error"<<std::endl;
    return 3;
  }

  struct sockaddr_in peer;
  while(true){
    memset(&peer,0,sizeof(peer));
    socklen_t len=sizeof(peer);
    int sock=accept(listSock,(struct sockaddr*)&peer,&len);
    if(sock<0){
      std::cout<<"accept error"<<std::endl;
      continue;
    }
    int* AdSock=new int(sock);
    pthread_t tid;
    pthread_create(&tid,nullptr,FuncTion,(void*)AdSock);
  }

  return 0;
}

void*FuncTion(void*args){
  pthread_detach(pthread_self());
  int sock=*(int*)args;
  delete (int*)args;
  std::cout<<"Get a Link Sock= "<< sock<<std::endl;
  return nullptr;
}

在这里插入图片描述
在这里插入图片描述
当还在TIME_WAIT状态时重启服务器时会绑定失败,因为连接没有被完全释放。

解决方法:
在这里插入图片描述
Linux下

int opt=1;
setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

根据上述可得:
客户端与服务器进程关闭后,TCP连接还是存在,所以系统进程管理与TCP连接管理相对独立。TCP连接由TCP管理

Logo

更多推荐