【Linux网络编程】网络基础二

一、应用层

程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层.

1.1、再谈协议

协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些
“结构化的数据” 怎么办呢?

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.

约定方案一:

客户端发送一个形如"1+1"的字符串; 这个字符串中有两个操作数, 都是整形; 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
数字和运算符之间没有空格;


约定方案二:

定义结构体来表示我们需要交互的信息; 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转
化回结构体; 这个过程叫做 “序列化" 和 "反序列化

// proto.h 定义通信的结构体
typedef struct Request {
int a;
int b;
} Request;

typedef struct Response {
int sum;
} Response;

// client.c 客户端核心代码
Request request;
Response response;
scanf("%d,%d", &request.a, &request.b);
write(fd, request, sizeof(Request));
read(fd, response, sizeof(Response));

// server.c 服务端核心代码
Request request;
read(client_fd, &request, sizeof(request));
Response response;
response.sum = request.a + request.b;
write(client_fd, &response, sizeof(response));

无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解
析, 就是ok的. 这种约定, 就是 应用层协议

1.2、HTTP协议

二、传输层

2.1、再谈端口号

端口号(port)标识了一个主机上进行通信的不同的应用程序。

在这里插入图片描述
在TCP/IP协议中, 用 “源IP”, “源端口号”, “目的IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个通信(可以通过netstat -n查看,具体还需要加别的参数);

在这里插入图片描述

  • 客户端A上面两个浏览器请求,客户端B上面一个浏览器请求,这三个请求同时请求访问服务器上的80端口号应用程序
  • 五元组由此建立,根据"源IP", “源端口号”, “目的IP”, “目的端口号”, "协议号"形成报头,封装在数据内容的首部进行传输,通过五元组识别通信。

端口号范围划分

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

  • 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的

认识知名端口号

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

  • ssh服务器, 使用22端口
  • ftp服务器, 使用21端口
  • telnet服务器, 使用23端口
  • http服务器, 使用80端口
  • https服务器, 使用443
cat /etc/services

或者

vim /etc/services

执行上面的任一命令都可以查看知名的端口号,

我们自己写一个程序使用端口号时, 要避开这些知名端口号

2.2.1、两个问题

  1. 一个进程是否可以bind多个端口号?
  2. 一个端口号是否可以被多个进程bind?
  • 问题一:可以,端口号保证的是唯一性,一个程序需要同时监听不同的端口,在web端很常见,每种协议的端口号各不相同,一个程序的执行可能需要很多的协议来保证通信传输。
  • 问题二:不可以,端口号必须唯一的被一个进程bind,保证唯一性,当一个进程成功bind了一个端口号后,其他进程就不能再使用相同的端口号。如果另一个进程试图使用已被占用的端口号,那么操作系统会返回一个错误并提示端口号已经被占用。

2.2.2、两个常用命令

netstat

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

语法: netstat [选项]
功能:查看网络状态
常用选项

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

pidof

在查看服务器的进程id时非常方便

语法: pidof [进程名]
功能:通过进程名, 查看进程id

在这里插入图片描述

2.2、UDP协议

2.2.1、udp协议端格式

在这里插入图片描述

  • 前8个字节各部分16位的原因是Linux操作系统用的是16位的,所以应用层也传16位 uint16_t 的数据
  • 16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的最大长度;
  • 如果校验和出错, 就会直接丢弃;

在这里插入图片描述
在这里插入图片描述

2.2.2、udp的特点

UDP传输的过程类似于寄信

  • 无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
  • 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
  • 面向数据报: 不能够灵活的控制读写数据的次数和数量;
  1. 所谓无连接就是没有tcp那样的监听,建立连接等等一系列通信前摇,上来知道了ip和端口号直接就发
  2. 所谓的不可靠就是,我把数据交给你了之后我就不管了,应用层传入的数据,交给传输层,然后网络。。。不考虑数据的丢失,不考虑怎么解决丢包,怎么传输
  3. 所谓的面向数据报,就是应用层发给udp一个报文,udp原样发送,你给我传多少,我给下一层传多少,不会拆分,不会合并,发给udp100字节,udp直接读取100字节,而不是分10次读取10字节,丢包就是没有数据。

2.2.3、udp的缓冲区

  • UDP没有真正意义上的 发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;

  • UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃

  • tcp的server端和client端都有发送缓冲区和接收缓冲区,数据的发送本质是拷贝数据,把数据拷贝给缓冲区,就可以继续工作,数据的发送交给缓冲区了,数据的接受也从缓冲区中拷贝过来,用生产者消费者模型的思想完成IO操作的解耦,使数据传输更为高效。
  • 而udp是有多少发多少,不需要发送缓冲区,只有接收缓冲区,由于数据的发送不可靠,自然缓冲区中的顺序也就不可靠,当然也有丢包问题的存在,至于缓冲区满的时候就直接丢弃。

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

  • 半双工:发送方和接收方之间只能一个发送一个接受
  • 全双工:发送方和接收方能够同时发送和接受,也就是a同时发送给b,也接收b的数据,b发给a数据,也同时接收a的数据,这就叫做全双工

UDP使用注意事项

我们注意到, UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部).

然而64K在当今的互联网环境下, 是一个非常小的数字.

如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装;

2.2.4、基于udp的应用层协议

  • NFS: 网络文件系统
  • TFTP: 简单文件传输协议
  • DHCP: 动态主机配置协议
  • BOOTP: 启动协议(用于无盘设备启动)
  • DNS: 域名解析协议

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

2.3、TCP协议

TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名, 要对数据的传输进行一个详细的控制;

2.3.1、认识TCP协议的报头 — 字段

在这里插入图片描述

  • 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
  • 32位序号/32位确认号: 后面详细讲;
  • 4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60
  • 6位标志位:
    • URG: 紧急指针是否有效
    • ACK: 确认号是否有效
    • PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
    • RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
    • SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
    • FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
  • 16位窗口大小: 后面再说
  • 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.
  • 16位紧急指针: 标识哪部分数据是紧急数据;
  • 40字节头部选项: 暂时忽略;
2.3.1.1、如何封装解包,如何分用

解包

  1. tcp协议标准长度是20字节,先读取20字节
  2. 把提取到的报头转化为结构化的数据,立马提取标准报头中的4位首部长度

怎么理解4位首部长度

这里的 4位首部长度是具有自描述属性的字段

tcp报头的总长度 4位 0000-1111 [0,15];
tcp报文的总长度=4位首部长度*4字节
【0,60】,【20,60】
(选项中有数据不止20字节,默认标准是20字节)

如果报文就是20字节,4位首部长度填写的就是0101

  1. 通过4位首部长度可以计算得出剩余报头的大小 x4-20=0 orx4-20=n;
  2. 只要把tcp报头处理完毕,剩下的就是有效载荷

问题:tcp报头里面为什么没有有效载荷的长度?

在 TCP 协议中,数据是按照连续的字节流方式进行传输的,没有明确划分出报文头和报文体的概念。(简单解答,不止这么简单)

封装和解包的过程是逆向的,不过多解释

如何分用

通过目的端口号,就可以找到应用层的进程,把数据交给进程。

理解:收到报文后,如何找到曾经bind特定port的进程?网络协议栈和文件什么关系?

系统内部有很多情况需要快速定位一个进程

实际上系统内部维护了一个以端口号port位key值的哈希表

请看下图描述过程:

在这里插入图片描述

Linux一切皆文件,网络协议栈也不例外

2.3.1.2、如何理解TCP的报头

在这里插入图片描述

2.3.1.3、TCP的可靠性(确认应答)&&提高传送效率
  1. 为什么网络传输的时候,会存在不可靠问题?
  2. 不可靠问题常见的有哪些不可靠场景?
  3. tcp的可靠性怎么保证?切入点是什么?

解答:

  1. 本质的原因是因为距离太长,干扰因素大大增加,通信中间会有损耗,还会有磁场干扰等等
  2. 丢包,乱序,校验错误,重复 。。。
  3. tcp通信中只存在相对的可靠性,一个报文只有在收到了应答之后,才能保证报文的可靠性。

在这里插入图片描述

2.3.1.4、序号和确认序号

为什么会同时存在序号和确认序号呢

所谓的序号和确认序号本质是为了保证数据传输的一致性。

A:吃了吗
B:吃了,你呢
A:我也吃过了,咱们一起逛街把
B:好的,几点去? 。。。

在服务器和客户端之间也存在这类似上面的通信交流,但是机器无法直接确认B发给A的响应是响应的那一句话,所以此时就需要序号来标记发送的数据段,同时响应的时候会对相应的确认序号做处理

1. tcp数据段要有序号来表示数据段本身
2. tcp数据段要根据确认序号来给对方相应收到了多少准确的、连续的报文
3.tcp的数据传输是全双工的,这就是为什么会同时存在序号和确认序号

当客户端发送一段数据的时候,会填上对应的序号,服务器做出响应,填写序号和对应的确认序号
服务器发送数据的时候,跟客户端一样,响应的时候会有自己的序号,方便对方填写确认序号

序号和确认序号的填写

当发送数据段的时候会填入序号,连续发送了10 11 12 13 14 这些数据段,
当服务器收到这些信息后,会向响应中填写确认序号 15  
代表15前面的数据段我都准确无误且连续的收到了,下次发送从15开始发。
但是
当发送了 10 11 12 13 14的时候,服务器只收到了10 11  13 14
此时服务器发送的确认序号就是12,代表12前面连续的收到了,下次发送继续从12开始发

上面的序号填写的理解只是理性理解,并不是实际中数据传输序号的方式

2.3.1.5、窗口大小

什么是窗口大小呢?

简单来说,发送方和接收方需要一种方式来知道对方的缓冲区大小,方便对自己的发送和接收速度做调整,而这个方式就是窗口大小

  1. 发送方不能发送的太快,也不能发送的太慢,所以就需要通过窗口大小来知道对方缓冲区剩余空间的大小。
  2. 我需要知道对方的剩余接收缓冲区空间大小,对方也要知道我的大小,发送数据段的时候会填写自己的接受缓冲区大小,填入16位窗口大小 让对方知道
  3. 我不论是发送方还是接收方,构建报文就是为了发送给对方,通过互相知道对方窗口大小(接收缓冲区大小),来对流量进行控制。
2.3.1.6、六个标记位

tcp报文也是有类型的!!

服务器在工作的过程中会遇到各种各样的报文,有可能至少响应,有可能是既有数据也有响应,也可能是其他的,不同的类型就具有不同的标记位

在这里插入图片描述

SYN建立TCP连接

FIN断开TCP连接

ACK确认应答标志位,收到数据后,会发送ACK包给对方,表示已收到

PSH催促接收方,让上层尽快取走数据,腾出空间接收数据

URG数据内部有需要被尽快读取的数据(插队),这个数据在哪里由紧急指针指向,通过紧急指针偏移量找到,只有一个字节是紧急数据

报文是有序号的,报文传送到对方的接收缓冲区,是以队列的方式进行排队的,当然可能有特殊情况下需要特殊紧急处理某些数据,所以有了插队的情况

RSTreset,复位标志位,如果服务器出现异常,丢失了连接信息,那么设置该标记为1,发送给客户端,让客户端重新发起连接

2.3.2、确认应答(ack)机制

在这里插入图片描述
TCP将每个字节的数据都进行了编号. 即为序列号

在这里插入图片描述

每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发

2.3.3、超时重传机制

在这里插入图片描述

  • 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
  • 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;

但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了;

在这里插入图片描述
因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.

这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果.

思考点: 发送端发出去的数据并不会马上被清理,而是会在发送缓冲区继续保留,知道收到接收方给的应答后,缓冲区中的发送数据才会被覆盖写入,保留的原因就是有可能丢包需要重新传。

那么, 如果超时的时间如何确定?

  • 最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”.
  • 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
  • 如果超时时间设的太长, 会影响整体的重传效率;
  • 如果超时时间设的太短, 有可能会频繁发送重复的包;

TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.

  • Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时
  • 时间都是500ms的整数倍.
  • 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
  • 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
  • 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.

2.3.4、连接管理机制

在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接

在这里插入图片描述
服务端状态转化:

  • [CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
  • [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文.
  • [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了.
  • [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT;
  • [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态,
    等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
  • [LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.

客户端状态转化:

  • [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, 并发出LAST_ACK;
  • [TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态.

在这里插入图片描述

  • 较粗的虚线表示服务端的状态变化情况;
  • 较粗的实线表示客户端的状态变化情况;
  • CLOSED是一个假想的起始点, 不是真实状态;
  • 10
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

翻转的乌龟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值