Linux_网络_传输层协议 TCP/UDP(端口,报头,协议格式,通信细节,三次握手与四次挥手,TCP协议机制,连接管理机制)
紧跟网络应用层协议http等 应用层上层协议将自己的数据发送给下层传输层协议。文章目录端口端口数据经过网络传输到主机后,系统根据端口号确认数据给那个应用程序。端口存在与传输层,使用端口与应用层关系。在传输层的协议报头中可以看到端口号标记了一个主机上进行通信的不同程序在TCP/IP协议中使用源IP,源端口号,目的IP,目的端口号来标识一个通信(netstat -n)。...
紧跟网络应用层协议
http等 应用层上层协议将自己的数据发送给下层传输层协议。
文章目录
端口
数据经过网络传输到主机后,系统根据端口号确认数据给那个应用程序。
端口存在与传输层,使用端口与应用层关系。在传输层的协议报头中可以看到
端口号标记了一个主机上进行通信的不同程序
在TCP/IP协议中使用源IP,源端口号,目的IP,目的端口号来标识一个通信(netstat -n)。
端口号的范围
-
0-1023:知名端口号 (http、ssh等)这些端口号都是固定的
-
1024-65535:操作系统动态分配的端口号,客户端程序的端口也在这个范围。
注意:
- 一个进程可以绑定多个端口
- 一个端口只能绑定一个进程
- Socket是一层系统调用,存在应用层与传输层之间。
- 传输层及其往下都在系统内核中
㈠ UPD协议(用户数据报协议)
UDP在向上交付数据时,只将有效载荷数据交给上层,报头信息不上交
1. UDP协议格式
其中:
- 源端口号与目的端口号是在上层调用sendto等函数有struct sockaddr*字样,调用函数陷入内核后初始化的。
- 16位UDP长度:整个UDP(报头+正文)的长度
- 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通信特点与缓冲区
-
无连接:只需要对端的ip+端口就可以直接通信,无需要建立连接。
-
不可靠:没有确认应答机制,没有重传机制,也不向应用层提供任何错误信息。
-
面向数据报:不能灵活的控制读写数据的次数于数量。
eg:写端sendto发送100个字节,接收端recvfrom必须一次接受100个字节。 -
UDP具有接受缓冲区:当UDP到达对端时,如果对端来不及处理UDP。在UDP中存在一段缓冲区(接受缓冲区)来暂时储存没处理的UDP协议。如果缓冲区的数据满了,再来的UDP数据会被直接丢弃。但是这个缓冲区不能保证收到的UDP消息顺序与发送时的消息一致
-
内核向UDP提供发送缓冲区:UDP没有真正意义上的发送缓冲区,当调用sendto函数时直接将UDP数据交到内核。内核将数据经过网络层传输。
-
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代码中实现的
有两种情况:
- 丢包
当接受端丢包时,发送端没有收到应答,发送端一段时间后进行重传
- 接受端发送的确认应答报文丢包
发送端没有收到应答,发送端还要重发一次报文。
注意:接受方不用担心数据重复。
因为TCP报头中有32位序号,不仅可以保证数据按需到达,还可以根据序号去重。
Linux下TCP为了保证效率,每次重传的时间都是500ms的整数倍。达到一定重传次数时强制关闭连接。
eg:
第一次重传等待500ms,第二次重传等待2×500ms,依次类推,最后如果还没有响应断开连接
3. TCP连接管理机制与状态
为什么建立连接是三次握手?
建立连接三次握手与对应状态图:
SYN与SYN+ACK:确认了客户端可以向服务器发送接受数据。
SYN+ACK与ACK:确认了服务器可以向客户但接受发送数据。
所以
-
通过三次通信就可以保证TCP通信的全双工。所以三次握手为最好
-
如果是偶数次建立连接,最后的异常一定挂在服务器上,由服务器来维护。而服务器与客户端数量是1:N。会占用服务器太多资源
为什么断开连接是四次挥手
首先,断开连接不需要再确认双方是否能通信,因为前一直在通信。
断开连接图与对应状态
所以
FIN与ACK:确保了服务器知道了客户端断开连接的请求,客户端断开了连接。
同理因为TCP是全双工的,所以还需要服务器断开连接
FIN与ACK:确保了客户端知道了服务器断开连接请求,服务器断开连接。
所以断开连接是四次挥手
注意:
- 如果只有客户端关闭套接字,那么服务器会处于CLOSE_WAIT状态,会导致服务器连接可用资源变少。
- 服务器向客户端发送FIN后,客户端接受到并响应。此时客户端不是处于CLOSE状态,而是TIME_WAIT。因为最后一次ACK可能会丢包,如果客户端直接进入CLOSE状态关闭连接,服务器丢包重发时客户端就收不到了。
- 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管理
更多推荐
所有评论(0)