[Linux] TCP协议介绍(1): TCP协议 数据格式、可靠性的控制、标记位... 简单介绍

news2024/11/25 2:19:54

上一篇文章, 针对UDP协议的格式、数据等内容做了一些简单的介绍. 并且提到, 在网络协议栈TCP/IP模型的传输层中, 有两个最具代表性的协议: UDPTCP

下面就简单介绍分析一下TCP协议


TCP协议, 完整的称呼其实叫: 传输控制协议(Transmission Control Protocol)

从名字就可以看出来, TCP协议 实际是可以对数据的传输进行详细控制的

TCP协议格式

使用TCP协议进行通信, 操作系统会对数据添加TCP的协议报头, 那么TCP协议的格式是这样的:

可以看到, TCP协议报头要比UDP协议报头复杂的多

在介绍UDP协议时提到过, TCP/IP协议栈的每一层在进行数据传输时, 都需要考虑三个内容: 封装、解包和分用

TCP协议报头中有两个熟悉的面孔: 16位源端口号 和 16位目的端口号, 这两个数据的作用就不用多做介绍了, 是为了解决 分用 的问题的

TCP协议的封装和解包

UDP协议的报头采用了8字节的固定长度, 所以可以很好的解决封装和解包的问题

而在TCP报文中, 我们可以看到在前20字节之外, 还有一个占有n字节的选项. 这n字节的部分是不固定的, 并且也属于TCP报头的内容

TCP报头没有选项时一共20字节, 这20字节的数据是必须的, 被称为标准长度

TCP协议在使用时, 报头的长度可能90%的情况都是标准长度(20字节). 即使选项不是必须的, 也不能忽略那n字节的选项长度

也就是说, TCP报头的长度最少为20字节, 但是并不固定 可能会更大.

既然TCP报头长度不固定, 那么怎么解决封装和解包的问题呢?

观察TCP报文格式可以发现, 在报头的第13字节(4位空间)处, 存储的数据表示的是 首部长度. 这个首部长度, 实际就表示TCP协议报头的长度

但是, 又有一个问题: TCP报头长度最少20字节, 但是报头中表示首部长度的数据只有4位, 最多也就能表示16个数据(0000~1111), 好像不太够用

4位空间:

0000000100100011
0100010101100111
1000100110101011
1100110111101111

实际上是够用的, 因为 4位 16个数据的单位并不是1字节, 而是4字节. 那么也就是说, 这四位数据最多可以表示 60字节, 即 TCP首部长度最大为60字节

但是, 不要忽略一个细节, TCP报头的标准长度为20字节, 也就是说最少为20字节. 那么, 实际上这 4位表示首部长度的数据 至少是0101(5)

既然TCP报头中存储有表示报头长度的数据, 那么就可以很好的解决封装和解包的问题

问题:

TCP报头并没有表示报文总长度的数据, 那么接收端如何接收到报文中所包含的所有数据呢

这个问题要等到介绍网络IP层才会有一个答案

TCP的可靠性

无论是UDP/TCP Socket的介绍, 还是上一篇文章中关于UDP协议的介绍, 文章中总提到: UDP协议是不可靠的, TCP协议是可靠的

那么, 究竟什么是不可靠? 什么是可靠?

什么是不可靠呢?

实际上我们已经提到过什么是不可靠的表现, 比如: 出现丢包、乱序、检验失败等情况, 并且不对这种情况做出处理. 这些情况, 在UDP协议中可能会经常出现. 不过, 既然使用了那么这些不可靠一定就不会对服务造成很大的影响

那么, 可靠就与不可靠相反了

UDP协议不会对丢包、检验失败等情况做出处理, 即使接收方没有正常收到数据, 接收方也不会有任何反应, 发送方更不会做出弥补. 我们说这是UDP协议更简单的一些代价

TCP协议则不同. 使用TCP协议通信时, 如果出现了丢包等接收方没有收到数据的情况, TCP协议会有一些处理, 比如: 重传、控制流量等

那么, TCP协议是如何维护数据传输的可靠性的呢?

TCP协议的确认应答机制

TCP协议会对接收方没有正常收到数据的情况做出弥补. 但是要实现这样的功能的第一个问题就是: 在这样的长距离通信中, 发送方(A)该如何确认发送的数据是丢了还是接收到了?

答案是, 接收方(B)的回应. 只要B收到数据之后, 给A发送一个回应, 那么A就可以确认数据已经发送到了

即, 只要发送方 收到了应答, 那么发送方就可以确认刚刚发送的消息一定已经被收到了

不过, B发送的回应, B如何确认A是否收到了呢?

答案是相同的, 如果 A收到B的回应之后, 再给B发送一个回应, 那么B就可以确认A收到了

就像这样:

你会发现, 如果想要做到每条消息都确定对方收到了, 是不可能的

因为 在这样的长距离通信中, 永远有一条最新的消息是不能被确认的、没有应答的

那么, 也就是说TCP协议也不是完全的可靠, 并且没有协议可以做到完全百分百的可靠

但是, TCP协议可以做到局部的可靠. 只要保证最新消息之前的消息都有了应答, 那么最新消息之前的数据就可以确定都已经接收到. 这就是TCP协议的可靠性

这样的机制, 被称为 TCP协议的确认应答机制(ACK)


不过, 这里又出现了其他问题: 使用TCP协议进行通信, 绝大多数情况下发送的报文是有很多个的. 那么, 发送方如何知道, 接收方应答的是哪一个报文呢?

这个问题是什么意思呢?

TCP协议为了维护可靠性, 是有确认应答机制的

在使用TCP进行通信时, 发送方可能一下发送很多报文, 接收方可能会一下子收到很多报文, 并且 接收方可能会针对接收到的每一个报文都单独做出应答

但是, 报文在网络中传输是充满不确定性的, 即使按照一定的顺序发送, 也不一定会按照顺序到达

所以, 发送方收到的 接收方的应答报文 很大可能是乱序 (接收方实际也是这样)

那么, 发送方如何确定对方的应答报文, 应答的是哪一个报文呢?

TCP协议报头中, 有两个字段: 32位序号 和 32位确认序号. 这两个字段, 可以解决上面这个问题

TCP协议在 发送数据 填充报头时, 会填充序号. 那么, 接收方接收到报文之后, 会根据报文的报头中填充的序号, 做出对应的应答. 即, 接收方 会在应答报文的报头中, 填充对应的确认序号

确认序号, 一般为接收到的报文序号+1, 表示 确认序号之前的所有序号的报文都已经接收到, 也同时表示接收方期望下次开始接收报文的序号

用图片可以很形象的表示出来:

同时, 接收端还可以通过接收到报文的序号对报文进行排序

这就是TCP协议报头中 32位序号 和 32位确认序号 这两个字段的作用

从这两个序号的作用可以看出来, 发送端报头中的序号 与 接收端报头中的确认序号 是配套使用的

那么, 也就意味着, 同一个报文的报头中, 序号与确认序号是相互独立、互不影响的. 这也是 TCP协议全双工 的一种体现, 因为同一个报文中的序号和确认序号是相互独立的, 所以同一个报文中可以同时填充序号和确认序号, 那么就表示这个报文在具有应答功能的同时, 还携带有数据进行发送

TCP协议规定了, 收到应答报文之后 发送方可以认为 确认序号之前的所有序号的报文都已经接收到

那么, 基于协议, 在实际实现时就可能会出现这样的情况:

|wide

这是种实现被称为 积累应答延迟应答, 可以有效提高通信效率

按照这样, 如果发送端发送了1~10序号的报文, 但是接收端只收到了1~68~10, 没有收到7

那么, 接收端应答报文中的确认序号 最高也只能填充7, 因为只有7之前序号的报文都收到了, 即使8~10也收到了, 也不会对其做出应答

TCP协议通信时, 报文的起始序号实际是随机的

并且, 后续的序号与 初始序号和报文数据本身 有关

序号协定的规则是什么呢?

首先, 起始序号是在建立连接时协定好的, 是随机的

并且, TCP协议会针对 报文数据的每一个字节进行编号

一个报文的序号, 就表示此 报文数据的第一个字节的编号

如果存在此次TCP通信的第一个报文:

|large

那么上图表示的这个报文中, 7214表示此次TCP通信的初始序号, 同样也表示此报文数据的第一个字节的编号,

那么第二个报文应为:

|wide

再之后的报文, 同样会按照相同的规则进行编号

TCP协议的缓冲区及流量控制

在介绍UDP协议的文章中提到过, 无论是UDP协议还是TCP协议. 在发送报文时, 都不会直接将数据发送到网络, 而是将数据放入内核针对协议实现的 发送缓冲区 中(UDP没有真正的发送缓冲区). 接收数据也是相同的, 操作系统会将报文放入 接收缓冲区

UDP协议 在内核中没有实现真正的发送缓冲区, 只有接收缓冲区

TCP协议 则在内核中真正实现了 发送缓冲区和接收缓冲区

那么, 两个主机在使用TCP协议进行通信, 使用write()/send()read()/recv()接口实现数据发送和接收所执行的操作, 简单理解可以看作:

|lwide

ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

ssize_t read(int fd, void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

这4个系统调用, 都需要指定一个buf

即, 使用TCP协议进行通信:

调用write()/send()发送数据 实际 是将数据 从进程指定的缓冲区中拷贝到了内核中TCP的发送缓冲区

调用read()/recv()读取数据 实际 是将数据 从内核中TCP的接收缓冲区拷贝到了进程指定的缓冲区中

也就是说, write()/send()实际并没有将数据发送走, 而只是将数据从用户拷贝到了内核数据中

实际上, 常用的I/O类函数, 本质上都是拷贝函数

即使使用write()向文件内写数据, 也只是将拷贝数据交给了操作系统, 并不是直接就写入了文件内

因为, TCP协议在内核中实现拥有发送缓冲区和接收缓冲区并且互不干扰, 所以TCP协议通信是全双工的

并且, 因为TCP协议发送数据, 是将数据拷贝到发送缓冲区, 然后由内核中的TCP协议自行决策(比如: 什么时候发、发多少、要不要进行一些调整…), 所以这个协议叫做 传输控制协议(Transmission Control Protocol)

TCP的流量控制

TCP协议在内核中是拥有发送缓冲区和接收缓冲区的, 那么 既然是缓冲区, 那就一定有一定的大小

并且, TCP协议通信是可靠的, 那么对发送出去的数据就不能不管不顾, 不能像UDP那样(如果接收缓冲区满了, 再发送过来的数据报就丢掉不管了)

那么, 如果TCP协议发送数据过快, 导致接受方的接收缓冲区满了, 怎么办? 继续快速的发送数据, 然后接收方来不及接收 直接丢包不管吗?

TCP协议并不会这样. TCP协议为了保障通信效率, 拥有自己的流量控制功能

TCP协议可以获取接收方当前接收数据的能力, 来调节发送方发送数据的速率

也就是说, 如果client需要向server发送数据, client可以根据server端的接收能力, 动态调控自己发送数据的速率

但是, client该如何知道server的接受能力呢? server的接收能力又如何表示呢?

缓冲区是有大小的, 那么, server接收能力 就可以通过 接收缓冲区的剩余空间大小来表示

那么, client该如何获取server的接收缓冲区剩余空间大小呢?

TCP协议报头中, 有一个字段是 16位窗口大小, 这个 窗口大小表示的就是接收缓冲区剩余空间的大小

如此一来, client接收到server的应答时, 就可以获取到server的窗口大小, 就可以调节自己发送速率, 进而实现流量控制

即, 发送方 可以通过 读取接收方的应答报头中填充的窗口大小, 来了解接收方接受数据的能力, 然后来调控自己发送数据的能力

问题:

既然在使用TCP协议正常通信时, 发送方可以通过接收方的应答报文中的窗口大小, 来获取接收方的接收能力

那么, 发送方在第一次发送数据的时候, 如何知晓接收方的窗口大小呢?

TCP报文类型 标记位

我们都知道, 在使用TCP协议进行通信的时候, 需要先"三次握手"建立连接, 然后才能实现正常的数据通信, 并且在通信结束的时候, 还需要"四次挥手"断开连接

为了方便TCP通信时需要做出一些特殊的处理, 实际上TCP报文是存在类型的, 针对不同类型的TCP报文

TCP协议会做出不同的处理和响应:

  1. 建立连接过程中发送的报文, TCP协议需要分辨出这个报文是建立连接用的, 然后会做出对应的处理与响应
  2. 正常通信过程中发送的报文, TCP协议需要分辨出这个报文是正常通信用的, 然后会做出对应的处理与响应
  3. 断开连接过程中发送的报文, TCP协议需要分辨出这个报文是断开连接用的, 然后会做出对应的处理与响应

TCP报文的类型, 则是通过TCP报头中的 6个标记位 来标识、分区的:

下面, 就来认识一下这6个标记位:

1. SYN

这个标记位全称是Synchronize Sequence Numbers, 叫做 同步标记位

是在"三次握手"建立连接时使用的, 使用时需要将此标志位设置为1

2. FIN

这个标记位全称是Finish, 叫做 结束标记位

是在"四次挥手"断开连接时使用的, 使用时需要将此标志位设置为1


SYNFIN这两个标志位的具体使用方法, 在具体介绍 TCP的"三次握手"和"四次挥手"时再进行介绍

3. ACK

这个标记位的名字, 在上面介绍TCP确认应答机制时见到过, 全称是Acknowledgement Number, 叫做 确认标记位

按照名字来说, 此标记位表示 该报文是对历史报文的确认, 应答报文应该设置此标记位为1. 但是, 实际上ACK标记位的使用 不仅仅只能作确认用

因为一般来说, 应答报文也是可以携带数据的, 而应答报文是需要设置ACK标记位的, 也就是说ACK标记位也允许在传输数据时设置

那么, TCP连接建立完成之后, 实际使用TCP协议进行通信时, 大部分的TCP报文都会将ACK标记位设置为1

4. PSH

这个标记位的全称是Push Function, 叫做 推送标记位

要理解这个标记位是干什么用的, 需要先介绍一些Linux操作系统I/O操作的特点

上面介绍过TCP协议是拥有接收缓冲区的, 而在TCP通信 调用read()是从TCP的接收缓冲区内拿数据到进程设置的缓冲区中

read()是一个阻塞式的接口, 当TCP接收缓冲区没有数据时, 调用read()的进程也好、线程也好 都会阻塞住, 直到TCP的接收缓冲区有数据了, read()才会继续执行读取数据. 这个过程中, read()只有主动调用 才会检测TCP接收缓冲区是否有数据, 然后才会阻塞或读取数据 的. 但是, 这样的阻塞式I/O并没有非常高效

所以, Linux实际提供的还有非阻塞式I/O接口(暂时不具体介绍). 也就是说, 应用层可以非阻塞式的从TCP接收缓冲区读取数据. 大概就是, 当TCP接收缓冲区没有数据的时候, 即使调用了非阻塞式接口, 进程或线程也不会阻塞住, 会结束执行. 而, 当TCP接收缓冲区中的数据大小达到一定阈值了(即让应用层读取数据的条件满足了), 内核会去通知进程或线程 可以读取数据了, 然后才会重新调用非阻塞式接口, 然后将数据读取到应用层. 也就是说, 这样的非阻塞式接口, 是不需要主动调用才能接收数据的(当然也可以主动调用), 它可以等待内核的通知, 然后再调用 实现读取数据

而设置PSH标志位, 就是 让内核通知应用层马上、尽快读取TCP接收缓冲区内的数据 的. 即使TCP接收缓冲区中的数据大小 还没有达到需要让内核通知应用层的阈值(即, 即使让应用层读取数据的条件并没有满足)

5. URG

这个标记位的全称是Urgent Pointer, 叫做 紧急指针标记位

我们知道, 报文在网络中路由时, 有些报文即使发送的早, 也不一定就会很早的到达接收方

那么也就是说, 在进行TCP通信时, 即使是按照序号的大小顺序发送的报文, 但是报文到达接收方的顺序也不一定是发送时的顺序. 即, 报文按顺序发送, 却乱序到达. 这是不可靠的一种体现, 而TCP协议是可靠的, 那么接收方就需要保证 接收到的数据是按照顺序的

所以, 接收方可以根据已经发送过来的报文的序号, 对报文进行排序并解包, 如果有数据没有到, 那就应答已经到了数据序号. 让后将排好顺序的报文数据再放到接收缓冲区中. 比如, 如果按照1 2 3 4 5 6 7 8发送数据, 数据却按照3 2 4 1 5 7 8 6的顺序到达了, 如果接收方当时只接受到了3 2 4 1 5 7 8, 还没接收到6, 那么就会对已经接收到的报文排序1 2 3 4 5 7 8, 发现6之前的报文都收到了, 那么接收方就会对1~5序号报文进行解包, 并应答确认序号6

这样, TCP可以实现报文数据按照发送顺序到达

但是, 这样会有另外的问题: 有些时候, 应用层需要处理的某些数据优先级比较高. 那么, 此时优先级高的数据如果还按照发送顺序进行接收, 报文到达的早还好, 如果报文到达的很晚, 好像高优先级就没有意义了

那么, 要解决这个问题, 就需要用到URG标记位了

当存在紧急数据需要发送时, TCP协议的发送方就会设置URG标志位, 接收方接收到报文读取到URG被设置为1时, 就会选择将紧急数据存入 外带缓冲区. 应用层可以直接从外带缓冲区读取紧急数据, 所以紧急数据也叫做外带数据

不过, 一个报文中的紧急数据的大小只能是1字节, 这与TCP报头的另一个字段有关: 16位紧急指针

TCP报头中的紧急指针字段, 实际上就是指紧急数据在本报文数据中的字节偏移量, 并且 只能保存一个偏移量, 也就是说, 一个TCP报文中只能标识1个紧急数据. 这也是为什么紧急数据只能是1字节


还有一个标记位没有介绍: RST

为了更好的理解RST标记位, 需要结合TCP"三次握手"的过程来解释

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1828681.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MobaXterm卡顿问题 解决方案

写在最前面,解决方案是:setting->X11->关闭Automatically start X server at MobaXterm start up 若有空,可以看一下下面的排障流程~ 现象描述 使用MobaXterm作为远程连接工具的时候,会出现很奇怪的卡顿问题。每隔几秒&…

Modbus转Profibus网关接变频器:实现工业自动化无缝连接

一、背景 在工业自动化领域,Modbus和Profibus是两种常见的通讯协议,而变频器作为控制电机转速的重要设备。为了实现不同设备之间的无缝连接和数据传输,现场大多数采用Modbus转Profibus网关(XD-MDPB100)来解决Modbus设…

linux 部署瑞数6实战(维普,药监局)sign第二部分

声明 本文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!wx …

从业余到专业:拼多多跨境选品师的成功之路

拼多多(Pinduoduo)作为中国颇具影响力的电商平台,其跨境电商领域近年来发展迅猛。作为跨境选品师,是否可以将其作为一项副业呢?以下将探讨这个问题,并提供一些实用建议。 1. 跨境电商市场概述 跨境电商市场正在迅速扩展,尤其是在…

UI学习--分栏控制器

UI学习 分栏控制器基础概念用法 分栏控制器高级高级属性 总结 分栏控制器基础 概念 分栏控制器可以理解为一个容器,可以容纳多个子视图控制器,并通过选项卡的方式进行切换。每个选项卡都与一个特定的视图控制器相关联,当用户点击不同的选项…

Elasticsearch 8.1官网文档梳理 - 十一、Ingest pipelines(管道)

Ingest pipelines 管道(Ingest pipelines)可让让数据在写入前进行常见的转换。例如可以利用管道删除文档(doc)的字段、或从文本中提取数据、丰富文档(doc)的字段等其他操作。 管道(Ingest pip…

2024年【广东省安全员A证第四批(主要负责人)】复审考试及广东省安全员A证第四批(主要负责人)复审模拟考试

题库来源:安全生产模拟考试一点通公众号小程序 广东省安全员A证第四批(主要负责人)复审考试参考答案及广东省安全员A证第四批(主要负责人)考试试题解析是安全生产模拟考试一点通题库老师及广东省安全员A证第四批&…

Bytebase 2.19.0 - 支持 DynamoDB

Bytebase 2.19.0 支持 DynamoDB 支持独立的 SQL 审核工单。 支持为工单事件配置 Slack 私信通知。 file 支持 PostgreSQL 的 DML 变更事前备份。 为 SQL Server 添加 SQL 审核规则:禁止冗余索引。 重大变更 创建多数据库工单时,不同数据库会共享同…

windows权限提升-WIN提权

Windows权限提升-WIN 全平台 Windows系统内置了许多本地用户组,这些用户组本身都已经被赋予一些权限(permissions),它们具有管理本地计算机或访问本地资源的权限。只要用户账户加入到这些本地组内,这回用户账户也将具备该组所拥有的权限。 普通权限 默…

二维数组与指针【C语言】

二维数组与指针 一维数组一维数组与指针二维数组二维数组与指针总结补充判断以下方式是否正确打印二维数组一维数组 int arr[] = {11, 22, 33, 44};arr:首地址(第一个元素的地址) 一维数组与指针 int arr[] = {11, 22, 33, 44};因为,arr表示的是首地址,等价于 int* p =…

C# TextBox模糊查询及输入提示

在程序中,我们经常会遇到文本框中不知道输入什么内容,这时我们可以在文本框中显示提示词提示用户;或者需要查询某个内容却记不清完整信息,通常可以通过文本框列出与输入词相匹配的信息,帮助用户快速索引信息。 文本框…

【区分】累次极限与二重极限

累次极限与二重极限不要混淆,区分好下面5个命题

学会python——读取大文本文件(python实例六)

目录 1、认识Python 2、环境与工具 2.1 python环境 2.2 Visual Studio Code编译 3、读取大文本文件 3.1 代码构思 3.2 代码示例 3.3 运行结果 4、总结 1、认识Python Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python 的设计具有很强…

计算机组成原理之定点乘法运算

文章目录 原码并行乘法与补码并行乘法原码算法运算规则存在的问题带符号的阵列乘法器习题原码阵列乘法器间接补码阵列乘法器直接补码阵列乘法器 补码与真值的转换 原码并行乘法与补码并行乘法 原码算法运算规则 存在的问题 理解流水式阵列乘法器(并行乘法器&#x…

本学期嵌入式期末考试的综合项目,我是这么出题的

时间过得真快,临近期末,又到了老师出卷的时候。作为《嵌入式开发及应用》这门课的主讲教师,今年给学生出的题目有一点点难度,最后的综合项目要求如下所示,各位学生朋友和教师同行可以评论一下难度如何,单片…

CMake的使用方法

1 CMakeLists.txt编写 cmake_minimum_required(VERSION 3.12)project(djl_plm)set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdc17 -g")add_executable(simple simple.cpp) add_executable(main main.cpp)include_directories(include) 相当于如下gcc命令&#xff1…

浅析MySQL-基础篇01

执行一条select语句,发生了什么? 学习SQL的时候,查询数据的时候简单的用到就是下面的这SQL语句: select * from tbl_1 where id 100; 有没有想过,MYSQL执行一条select查询语句,在MYSQL中期间发生了什么…

C/C++李峋同款跳动的爱心代码

一、写在前面 在编程的世界里,代码不仅仅是冷冰冰的命令,它也可以成为表达情感、传递浪漫的工具。今天,就让小编带着大家用C语言打造出李峋同款跳动的爱心吧! 首先,我们需要知道C作为一种高级编程语言,拥…

Linux常用操作大全(上)

Linux常用操作 文章目录 Linux常用操作一、各类小技巧**1.ctrl c 强制停止****2.ctrl d 退出或登出**3.历史命令搜索4.光标移动快捷键5.清屏6.复制Ctrlshiftc7.粘贴Ctrlshiftv 二、软件安装1.概念2.yum与apt 三、systemctl控制服务四、软链接ln五、日期时区1.date查看日期2.修…

高阶数据结构[2]图的初相识

图的初相识 1.前言 2.图的概念 3.图的相关术语 4.图的存储结构 4.1邻接矩阵 4.2邻接表 4.3两种存储方式的对比 5.图的存储实现 5.1邻接矩阵的实现 5.2邻接表的实现 6.总结 1.前言 本章将大家学习数据结构中的“图”。有学习过离散数学的同学对这一章节或许会比…