运输层详解

网络层为主机之间提供逻辑通信,而运输层为应用进程之间提供端到端的逻辑通信
用户数据报协议udp
一.
1.udp是无连接的
2.udp使用尽最大努力交付
3.udp是面向报文的
4.udp没有拥塞控制
5.udp支持一对一、一对多、多对一和多对多的交互通信
6.udp的首部开销小
二.
udp的首部格式
源端口、目的端口、长度、校验和
传输控制协议tcp
1.tcp是面向连接的运输层协议
2.每一条tcp连接只能有两个端点
3.tcp是可靠交付的服务
4.tcp提供全双工通信
5.面向字节流
tcp的连接
每一条tcp连接唯一的被通信两端的两个端点(即两个套接字)所确定
套接字socket = ip地址:端口号
可靠传输的工作原理
超时重传机制
tcp首部格式
源端口和目的端口、序号、确认号、数据偏移、保留、紧急urg、确认ack(在建立连接后所有传送的报文段都必须把ack置1)、推送psh、复位rst、同步syn(在建立连接时用来同步序号。当syn=1而ack=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使syn=1和ack=1。因此,syn置为1就表示这是一个连接请求或连接接受报文)、终止fin、窗口、校验和、紧急指针、选项、
滑动窗口
TCP发送一个数据,如果需要收到确认应答,才会发送下一个数据。这样的话就会有个缺点:效率比较低
★ 这就好像我们面对面在聊天,你说完一句,我应答之后,你才能说下一句。那么,如果我在忙其他事情,没有能够及时回复你呢?你说完一句后,要等到我忙完回复你,你才说下句,这显然不现实,效率太低。
为了解决这个问题,TCP 引入了窗口,它是操作系统开辟的一个缓存空间。窗口大小值表示无需等待确认应答,而可以继续发送数据的最大值。

TCP 头部有个字段叫 win,也即那个16 位的窗口大小,它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度,从而达到流量控制的目的。

★ 通俗点讲,就是接受方每次收到数据包,在发送确认报文的时候,同时告诉发送方,自己的缓存区还有多少空余空间,缓冲区的空余空间,我们就称之为接受窗口大小。这就是 win。
TCP 滑动窗口分为两种: 发送窗口和接收窗口。发送端的滑动窗口包含四大部分,如下:
已发送且已收到 ACK 确认
已发送但未收到 ACK 确认
未发送但可以发送
未发送也不可以发送
在这里插入图片描述

虚线矩形框,就是发送窗口。
SND.WND: 表示发送窗口的大小, 上图虚线框的格子数是 14 个,即发送窗口大小是 14。
SND.NXT:下一个发送的位置,它指向未发送但可以发送的第一个字节的序列号。
SND.UNA: 一个绝对指针,它指向的是已发送但未确认的第一个字节的序列号。
接收方的滑动窗口包含三大部分,如下:
已成功接收并确认
未收到数据但可以接收
未收到数据并不可以接收的数据

在这里插入图片描述

虚线矩形框,就是接收窗口。
REV.WND: 表示接收窗口的大小, 上图虚线框的格子就是 9 个。
REV.NXT: 下一个接收的位置,它指向未收到但可以接收的第一个字节的序列号。
流量控制
TCP 三次握手,发送端和接收端进入到 ESTABLISHED 状态,它们即可以愉快地传输数据啦。

但是发送端不能疯狂地向接收端发送数据,因为接收端接收不过来的话,接收方只能把处理不过来的数据存在缓存区里。如果缓存区都满了,发送方还在疯狂发送数据的话,接收方只能把收到的数据包丢掉,这就浪费了网络资源啦。

★ TCP 提供一种机制可以让发送端根据接收端的实际接收能力控制发送的数据量,这就是流量控制。

TCP 通过滑动窗口来控制流量,我们看下流量控制的简要流程吧:

首先双方三次握手,初始化各自的窗口大小,均为 400 个字节。
在这里插入图片描述

TCP 的流量控制
假如当前发送方给接收方发送了 200 个字节,那么,发送方的
SND.NXT会右移 200 个字节,也就是说当前的可用窗口减少了 200 个字节。
接受方收到后,放到缓冲队列里面,REV.WND =400-200=200 字节,所以 win=200 字节返回给发送方。接收方会在 ACK 的报文首部带上缩小后的滑动窗口 200 字节
发送方又发送 200 字节过来,200 字节到达,继续放到缓冲队列。不过这时候,由于大量负载的原因,接受方处理不了这么多字节,只能处理 100 字节,剩余的 100 字节继续放到缓冲队列。这时候,REV.WND = 400-200-100=100 字节,即 win=100 返回发送方。
发送方继续干活,发送 100 字节过来,这时候,接受窗口 win 变为 0。
发送方停止发送,开启一个定时任务,每隔一段时间,就去询问接受方,直到 win 大于 0,才继续开始发送。
拥塞控制
慢开始,拥塞避免,快重传,快恢复
在这里插入图片描述
在这里插入图片描述

tcp的连接建立
在这里插入图片描述
第一次,首部中的同步位syn=1,同时选择一个初始序号seq=x,syn=1的报文段不能携带数据,但要消耗一个序号。这时,tcp客户进程进入syn-sent同步已发送状态。
第二次,syn和ack都为1,确认号ack=x+1,同时也为自己选择一个初始序号seq=y。这个报文也不能携带数据,但同样要消耗掉一个序号。这时tcp服务器进程进入syn-rcvd(同步收到)状态。
第三次,ack为1,确认号ack=y+1,而自己的序号seq=x+1这时tcp连接已建立
为什么客户端最后还要发送一次确认呢?
为了防止已失效的连接请求报文段突然又传送到了B,因而产生错误
tcp的连接释放
数据传输结束后,通信的双方都可释放连接
在这里插入图片描述

第一次,客户端把连接释放报文首部的终止控制位fin置1,其序号seq=u,它等于前面已传送过去的数据的最后一个字节的序号加1。这时A进入fin-wait-1(终止等待1)状态,等待服务端的确认。fin报文段即使不携带数据,它也消耗掉一个序号。
第二次,服务端收到连接释放报文段后即发出确认,确认号是ack=u+1,而这个报文自己的序号是v,等于服务端前面已传送过的数据的最后一个字节的序号加1。然后服务端进入close-wait(关闭等待)状态。客户端收到来自服务端的确认后,就进入fin-wait-2(终止等待2)状态,等待服务端发出的连接释放报文段。这时的tcp连接处于半关闭状态。
第三次,服务端发出连接释放报文必须使fin=1。现假定服务端的序号为w(在半关闭连接状态B可能又发送了一些数据)。B还必须重复上次已发送过的确认号ack=u+1。这时B就进入last-ack(最后确认)状态,等待A的确认。
第四次,客户端在收到服务端的连接释放报文段后,必须对此发出确认。把ack置为1,确认号ack=w+1,而自己的序号是seq=u+1
为什么客户端在time-wait状态必须等待2msl的时间呢?

  1. 为了保证客户端发送的最后一个 ACK 报文段能够到达服务端。 这个 ACK 报文段有可能丢失,因而使处在LAST-ACK状态的服务端就收不到对已发送的FIN + ACK报文段的确认。服务端会超时重传这个 FIN+ACK 报文段,而客户端就能在 2MSL 时间内(超时 + 1MSL 传输)收到这个重传的 FIN+ACK 报文段。接着客户端重传一次确认,重新启动 2MSL 计时器。最后,客户端和服务器都正常进入到CLOSED状态。(简述:防止ack报文丢失,Server再次发送Fin报文,一来一回最长时间就是2MSL)
  2. 防止已失效的连接请求报文段出现在本连接中。客户端在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个连接中不会出现这种旧的连接请求报文段。

socket
对于一次TCP连接而言,建立连接主要通过socket、bind、listen、accept这些API,整个流程如下图所示:

在这里插入图片描述
socket(创建一个用于通信的节点)
int socket(int domain, int type, int protocol);
参数

domain:指定通信的域,选择通信的协议族,比较常见的有:AF_INET(代表ipv4)、AF_INET6(代表ipv6)等等,这个参数指定了ip地址的格式。

type:指定通信的方式,传递数据的方式,比较常见的有:SOCK_STREAM、SOCK_DGRAM。

protocol:指定通信的协议,常见的有IPPROTO_TCP、IPPROTO_UDP、IPPRO_SCTP。

返回值:int

函数的返回值是文件描述符,该文件描述符是一个正整数,唯一标识服务端与某客户端的连接,服务端和客户端可以通过此连接进行通信。出错情况下,会返回-1,并设置errno,可以通过errno获得出错信息。

bind(将生成的文件描述符绑定到需要监听的端口)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数

sockfd:通过socket函数返回的文件描述符

addr:指定需要监听的地址,地址包含了ip和端口,该结构体定义如下:

struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
实际上,在调用bind函数时传递的参数定义是struct sockaddr_in,定义如下:

struct sockaddr_in {
short int sin_family; /* 协议族,在socket编程中基本为AF_INET /
unsigned short int sin_port; /
端口号r /
struct in_addr sin_addr; /
IP地址 /
unsigned char sin_zero[8]; /
空字节,为了让sockaddr和sockaddr_in有相同的字节大小 */
};
sockaddr_in结构体清楚定义了ip地址和端口,而socketaddr结构体则是将ip地址和端口号捆绑在一起保存在data了,因此使用sockaddr_in进行初始化可以分开存储ip地址和结构体,也让代码看起来更加清晰,之后可以通过类型转换传递到bind函数。

addrlen:第二个参数的大小。

返回值:int

成功返回0,出错情况下,会返回-1,并设置errno,可以通过errno获得出错信息。

listen(服务端侧使用,开始监听已经建立完连接的socket)
int listen(int sockfd, int backlog)
参数

sockfd:指向socket的文件描述符

backlog:指定请求数量缓冲区长度,如果请求数量超过backlog的大小,客户端会收到报错信息。

返回值:int

成功返回0,出错情况下,会返回-1,并设置errno,可以通过errno获得出错信息。

connect(客户端请求连接服务端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t socklen);
参数含义与bind相同。

返回值:int

成功返回0,出错情况下,会返回-1,并设置errno,可以通过errno获得出错信息。

accept(接收请求的到来)
int accept(int sockfd, struct sockaddr *addr, socklen_t *socklen);

accept参数的含义与bind类似,有一个不一样的是socklen,是一个引用传递方式的参数,调用方需要默认的长度,即参数addr的长度,函数调用成功后,会将真实的大小写到socklen。

close(关闭打开的文件描述符)
int close(int fd);
参数

fd:文件描述符。

linux I/O API
建立连接后,就需要开始传递数据进行通信,比较常用的I/O api有read、write、recv、send。

read(从文件描述符读取count字节的数据,并保存到buf)
ssize_t read(int fd, void *buf, size_t count);
参数 fd:已经打开的文件描述符

buf:保存读取数据的指针

count:要读取的数据字节大小,不能为0

返回值 int

如果成功,返回已经读取的字节大小,0表示到达文件结尾,-1表示错误。

write(写入在buf中count个字节的数据到打开的文件描述符fd中)
ssize_t write(int fd, const void *buf, size_t count);
参数 fd:已经打开的文件描述符

buf:保存数据的指针

count:要写入的数据字节大小

返回值:如果成功,返回写入的字节大小,返回的值比count小不算一种错误,如果出错,返回-1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值