哦耶!美团二面过了!
大家好,我是小林。
今天美团开展了24届秋招了,这次来分析一篇春招美团java后端实习二面面经。
问的问题主要是 mysql+redis+数据结构+java+linux命令+网络这些,考察的知识点范围还是比较广的,但是问题都不会太难。
mysql
mysql 有哪些索引,分别说一下?
可以按照四个角度来分类索引。
按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。 按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。 按「字段个数」分类:单列索引、联合索引。
为了让大家理解 B+Tree 索引的存储和查询的过程,接下来我通过一个简单例子,说明一下 B+Tree 索引在存储数据中的具体实现。
先创建一张商品表,id 为主键,如下:
CREATE TABLE `product` (
`id` int(11) NOT NULL,
`product_no` varchar(20) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`price` decimal(10, 2) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
商品表里,有这些行数据:
这些行数据,存储在 B+Tree 索引时是长什么样子的?
B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。
主键索引的 B+Tree 如图所示(图中叶子节点之间我画了单向链表,但是实际上是双向链表,原图我找不到了,修改不了,偷个懒我不重画了,大家脑补成双向链表就行):
通过主键查询商品数据的过程
比如,我们执行了下面这条查询语句:
select * from product where id= 5;
这条语句使用了主键索引查询 id 号为 5 的商品。查询过程是这样的,B+Tree 会自顶向下逐层进行查找:
将 5 与根节点的索引数据 (1,10,20) 比较,5 在 1 和 10 之间,所以根据 B+Tree的搜索逻辑,找到第二层的索引数据 (1,4,7); 在第二层的索引数据 (1,4,7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4,5,6); 在叶子节点的索引数据(4,5,6)中进行查找,然后我们找到了索引值为 5 的行数据。
数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。
B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O,所以B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。
通过二级索引查询商品数据的过程
主键索引的 B+Tree 和二级索引的 B+Tree 区别如下:
主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里; 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。
我这里将前面的商品表中的 product_no (商品编码)字段设置为二级索引,那么二级索引的 B+Tree 如下图(图中叶子节点之间我画了单向链表,但是实际上是双向链表,原图我找不到了,修改不了,偷个懒我不重画了,大家脑补成双向链表就行)。
其中非叶子的 key 值是 product_no(图中橙色部分),叶子节点存储的数据是主键值(图中绿色部分)。
如果我用 product_no 二级索引查询商品,如下查询语句:
select * from product where product_no = '0002';
会先检二级索引中的 B+Tree 的索引值(商品编码,product_no),找到对应的叶子节点,然后获取主键值,然后再通过主键索引中的 B+Tree 树查询到对应的叶子节点,然后获取整行数据。这个过程叫「回表」,也就是说要查两个 B+Tree 才能查到数据。如下图(图中叶子节点之间我画了单向链表,但是实际上是双向链表,原图我找不到了,修改不了,偷个懒我不重画了,大家脑补成双向链表就行):
不过,当查询的数据是能在二级索引的 B+Tree 的叶子节点里查询到,这时就不用再查主键索引查,比如下面这条查询语句:
select id from product where product_no = '0002';
这种在二级索引的 B+Tree 就能查询到结果的过程就叫作「覆盖索引」,也就是只需要查一个 B+Tree 就能找到数据。
联合索引
通过将多个字段组合成一个索引,该索引就被称为联合索引。
比如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name)
,创建联合索引的方式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
联合索引(product_no, name)
的 B+Tree 示意图如下(图中叶子节点之间我画了单向链表,但是实际上是双向链表,原图我找不到了,修改不了,偷个懒我不重画了,大家脑补成双向链表就行)。
可以看到,联合索引的非叶子节点用两个字段的值作为 B+Tree 的 key 值。当在联合索引查询数据时,先按 product_no 字段比较,在 product_no 相同的情况下再按 name 字段比较。
也就是说,联合索引查询的 B+Tree 是先按 product_no 进行排序,然后再 product_no 相同的情况再按 name 字段排序。
因此,使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。
比如,如果创建了一个 (a, b, c)
联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:
where a=1; where a=1 and b=2 and c=3; where a=1 and b=2;
需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。
但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:
where b=2; where c=3; where b=2 and c=3;
上面这些查询条件之所以会失效,是因为(a, b, c)
联合索引,是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。所以,b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。
我这里举联合索引(a,b)的例子,该联合索引的 B+ Tree 如下(图中叶子节点之间我画了单向链表,但是实际上是双向链表,原图我找不到了,修改不了,偷个懒我不重画了,大家脑补成双向链表就行)。
可以看到,a 是全局有序的(1, 2, 2, 3, 4, 5, 6, 7 ,8),而 b 是全局是无序的(12,7,8,2,3,8,10,5,2)。因此,直接执行where b = 2
这种查询条件没有办法利用联合索引的,利用索引的前提是索引里的 key 是有序的。
只有在 a 相同的情况才,b 才是有序的,比如 a 等于 2 的时候,b 的值为(7,8),这时就是有序的,这个有序状态是局部的,因此,执行where a = 2 and b = 7
是 a 和 b 字段能用到联合索引的,也就是联合索引生效了。
MVCC的隔离机制介绍一下?
我们需要了解两个知识:
Read View 中四个字段作用; 聚簇索引记录中两个跟事务有关的隐藏列;
那 Read View 到底是个什么东西?
Read View 有四个重要的字段:
m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。 min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。 max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1; creator_trx_id :指的是创建该 Read View 的事务的事务 id。
知道了 Read View 的字段,我们还需要了解聚簇索引记录中的两个隐藏列。
假设在账户余额表插入一条小林余额为 100 万的记录,然后我把这两个隐藏列也画出来,该记录的整个示意图如下:
对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:
trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里; roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
如果记录的 trx_id 值小于 Read View 中的 min_trx_id
值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id
值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。如果记录的 trx_id 值在 Read View 的 min_trx_id和max_trx_id之间,需要判断 trx_id 是否在 m_ids 列表中: 如果记录的 trx_id 在 m_ids
列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。如果记录的 trx_id 不在 m_ids
列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
事务原子性是怎么实现的?
MySQL的事务原子性主要通过Undo Log(撤销日志)来实现的。
当进行一次事务操作时,MySQL会首先在Undo Log中记录下事务操作前的数据状态。如果事务成功执行并提交,Undo Log中的记录就可以被删除。但如果在事务执行过程中出现错误,或者用户执行了ROLLBACK操作,MySQL就会利用Undo Log中的信息将数据恢复到事务开始前的状态,从而实现事务的原子性。
这就意味着,事务要么全部执行成功,要么如果部分执行失败,那么已经执行的部分也会被撤销,保证数据的一致性。
事务的隔离性怎么实现的?
MySQL的事务隔离性主要通过锁机制和多版本并发控制(MVCC)来实现。
锁机制:包括行锁和表锁。行锁可以精确到数据库表中的某一行,而表锁则会锁定整个数据表。当一个事务在操作某个数据项时,会对其加锁,阻止其他事务对同一数据项的并发操作,从而实现隔离性。
多版本并发控制(MVCC):这是InnoDB存储引擎特有的一种机制,它可以在不加锁的情况下创建数据在某一时间点的快照。在读取数据时,MVCC会返回该时间点的数据版本,即使该数据后来被其他事务修改。这样,每个事务都有自己的数据视图,彼此之间不会互相影响,实现了隔离性。
此外,MySQL还提供了四种隔离级别(读未提交、读已提交、可重复读、串行化),可以根据需要选择不同的隔离级别,以在并发性和数据一致性之间取得平衡。
事务一致性怎么实现的?
MySQL实现事务一致性主要依赖于其InnoDB存储引擎的ACID属性,其中C代表一致性(Consistency)。具体来说,以下是MySQL如何实现事务一致性的一些方式:
使用锁机制:InnoDB存储引擎支持行级锁和表级锁,通过锁机制来控制并发事务的访问冲突,确保每个事务都在一致性的状态下执行。
使用MVCC:InnoDB存储引擎通过MVCC来实现读已提交和可重复读两个隔离级别,保证了事务的一致性视图,即在事务开始时生成一个快照,事务在执行过程中看到的数据都是这个快照中的数据。
使用Undo日志:InnoDB存储引擎在修改数据前,会先将原始数据保存在Undo日志中,如果事务失败或者需要回滚,就可以利用Undo日志将数据恢复到原始状态,从而保证了数据的一致性。
使用Redo日志:Redo日志用于保证事务的持久性,但也间接保证了一致性。因为在系统崩溃恢复时,可以通过Redo日志来重做已提交的事务,保证这些事务的修改能够持久保存。
以上四点结合起来,就能保证MySQL事务的一致性。
数据库的三大范式介绍一下?可以反范式吗?
数据库的三大范式是数据库设计的基本原则,主要包括:
第一范式(1NF):数据表中的每一列都是不可分割的最小单元,也就是属性值是原子性的。
第二范式(2NF):在第一范式的基础上,要求数据表中的每一列都与主键相关,也就是说非主键列必须完全依赖于主键,不能只依赖主键的一部分(针对联合主键)。
第三范式(3NF):在第二范式的基础上,要求一个数据表中不包含已在其他表中已包含的非主键信息,也就是说,非主键列必须直接依赖于主键,不能存在传递依赖。
关于反范式,是的,数据库设计可以反范式。反范式设计是为了优化数据库性能,通过增加冗余数据或者组合数据,减少复杂的数据查询,提高数据读取性能。
redis
Redis数据库和缓存一致性怎么保证?
对于读数据,我会选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache。
对于写数据,我会选择更新 db 后,再删除缓存。
针对删除缓存异常的情况,我还会对 key 设置过期时间兜底,只要过期时间一到,过期的 key 就会被删除了。
除此之外,还有两种方式应对删除缓存失败的情况。
消息队列方案
我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。
如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
举个例子,来说明重试机制的过程。
订阅 MySQL binlog,再操作缓存
「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。
Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
下图是 Canal 的工作原理:
所以,如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。
java
说一下HashMap和Hashtable、ConcurrentMap的区别
HashMap线程不安全,效率高一点,可以存储null的key和value,null的key只能有一个,null的value可以有多个。默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。底层数据结构为数组+链表,插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。 HashTable线程安全,效率低一点,其内部方法基本都经过synchronized修饰,不可以有null的key和value。默认初始容量为11,每次扩容变为原来的2n+1。创建时给定了初始容量,会直接用给定的大小。底层数据结构为数组+链表。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。 ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它可以在多线程环境下并发地进行读写操作,而不需要像传统的HashTable那样在读写时加锁。ConcurrentHashMap的实现原理主要基于分段锁和CAS操作。它将整个哈希表分成了多Segment(段),每个Segment都类似于一个小的HashMap,它拥有自己的数组和一个独立的锁。在ConcurrentHashMap中,读操作不需要锁,可以直接对Segment进行读取,而写操作则只需要锁定对应的Segment,而不是整个哈希表,这样可以大大提高并发性能。
为什么HashMap的容量一定是2的次方?
在HashMap中,元素的存储位置是根据键的哈希值来确定的。当需要存储一个键值对时,HashMap会根据键的哈希值计算出一个索引位置,然后将该键值对存储在该索引位置上。
选择容量为2的次方,主要是为了利用位运算来代替取模运算,以提高计算效率。在HashMap内部,计算索引位置时,使用的是(n - 1) & hash
的位运算方式,其中n为HashMap的容量,hash为键的哈希值。由于容量为2的次方,所以(n - 1)
的二进制表示形式全是1,这样就可以通过位运算取哈希值的低位,避免了昂贵的取模运算。
此外,选择容量为2的次方还可以减少哈希冲突的概率。当容量为2的次方时,哈希值的低位在计算索引位置时会更加均匀地分布在HashMap的桶中,减少了哈希冲突的可能性,提高了HashMap的性能。
因此,容量选择为2的次方是为了提高HashMap的计算效率和减少哈希冲突的概率。
ThreadLocal的原理和适用场景知道吗?
Thread类有两个变量:threadLocals和inheritableThreadLocals
这两个变量默认为null,只有当该线程调用了ThreadLocal类的get/set方法时才会创建他们,而调用ThreadLocal的get/set实际上是调用ThreadLocalMap的get/set
ThreadLocalMap可理解成给ThreadLocal定制化的HashMap
最终的变量放在了线程的ThreadLocalMap中,而不是ThreadLocal中,ThreadLocal只是对其进行封装,向其传递变量值。
用一个场景分析ThreadLocal的get/set流程:
首先在所有线程外部创建一个共享的ThreadLocal对象,记为TL1。在一个线程中调用TL1.get()时,首先获取到当前线程对象,记为t,然后判断t.threadLocals是否为null,如果为null,就在t中创建一个新的ThreadLocalMap对象赋值给t.threadLocals,并将<TL1, null>插入其中,最后get方法返回null;如果不为null,则尝试获取threadLocals中TL1所在的键值对,如果该键值对为null,则向threadLocals中通过set方法插入<TL1, null>,最后返回null,如果键值对不为null,则返回键值对中的值。
调用set方法时,流程和get基本一致,只是从读变成了写。
这样就可以实现不同线程访问同一个ThreadLocal能拿到各自向其中存放的值。
数据结构
红黑树说一下,跳表说一下?
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它在插入和删除操作后能够通过旋转和重新着色来保持树的平衡。红黑树的特点如下:
每个节点都有一个颜色,红色或黑色。 根节点是黑色的。 每个叶子节点(NIL节点)都是黑色的。 如果一个节点是红色的,则它的两个子节点都是黑色的。 从根节点到叶子节点或空子节点的每条路径上,黑色节点的数量是相同的。
红黑树通过这些特性来保持树的平衡,确保最长路径不超过最短路径的两倍,从而保证了在最坏情况下的搜索、插入和删除操作的时间复杂度都为O(logN)。
跳表(Skip List)是一种基于链表的数据结构,它通过添加多层索引来加速搜索操作。
跳表的特点如下:
跳表中的数据是有序的。 跳表中的每个节点都包含一个指向下一层和右侧节点的指针。
跳表通过多层索引的方式来加速搜索操作。最底层是一个普通的有序链表,而上面的每一层都是前一层的子集,每个节点在上一层都有一个指针指向它在下一层的对应节点。这样,在搜索时可以通过跳过一些节点,直接进入目标区域,从而减少搜索的时间复杂度。
跳表的平均搜索、插入和删除操作的时间复杂度都为O(logN),与红黑树相比,跳表的实现更加简单,但空间复杂度稍高。跳表常用于需要高效搜索和插入操作的场景,如数据库、缓存等。
你知道什么地方用了红黑树和跳表吗?
epoll 用了红黑树来保存监听的 socket redis 用了跳表来实现 zset
linux
怎么看端口被哪个进程监听了?
netstat -napt | grep 443
top命令有哪些参数,说一下
top命令是一个用于实时监控系统资源和进程的命令,它可以显示当前运行的进程、CPU使用情况、内存使用情况等信息。以下是一些常用的top命令参数:
-d <秒数>:指定top命令刷新的时间间隔,默认为3秒。 -n <次数>:指定top命令执行的次数后自动退出。 -p <进程ID>:指定要监控的进程ID。 -u <用户名>:只显示指定用户名的进程。 -s <排序字段>:按指定字段对进程进行排序,常见的字段有cpu(CPU使用率)、mem(内存使用率)等。 -H:显示进程的层次关系。 -i:只显示运行中的进程,不显示僵尸进程。
怎么显示线程?
在Linux中,可以使用以下命令来显示线程:
top命令:在top命令的默认显示中,可以看到每个进程的线程数(Threads列)。例如,执行top命令后,按下"Shift + H"键可以切换到线程视图,显示每个进程的线程信息。 ps命令:通过ps命令结合选项来显示线程。例如,使用"ps -eLf"命令可以显示系统中所有线程的详细信息。
怎么查看日志?
tail -f test.log
网络
如何实现一个可靠UDP?
可以通过以下方法实现一个可靠的UDP:
应用层协议设计:在应用层上设计一个自定义的协议,通过在UDP数据包中添加序列号、校验和、确认应答等字段来实现可靠性。发送方发送数据时,需要等待接收方的确认应答,如果没有收到确认应答或者收到了错误的确认应答,就进行重传。
超时重传:发送方在发送数据后设置一个超时时间,如果在超时时间内没有收到确认应答,就进行重传。接收方在接收到数据后发送确认应答,如果发送方没有收到确认应答,就进行重传。
数据校验:在发送方和接收方都进行数据校验,例如使用校验和算法(如CRC)来检测数据是否被篡改。如果校验失败,就进行重传。
应答机制:发送方发送数据后,接收方需要发送确认应答来告知发送方数据已经接收成功。如果发送方没有收到确认应答,就进行重传。
流量控制和拥塞控制:在发送方和接收方之间进行流量控制和拥塞控制,以防止数据包的丢失和网络拥塞。
ping命令是什么协议?
icmp 协议
同个子网下的主机 A 和 主机 B,主机 A 执行ping
主机 B 后,我们来看看其间发送了什么?
ping 命令执行的时候,源主机首先会构建一个 ICMP 回送请求消息数据包。
ICMP 数据包内包含多个字段,最重要的是两个:
第一个是类型,对于回送请求消息而言该字段为 8
;另外一个是序号,主要用于区分连续 ping 的时候发出的多个数据包。
每发出一个请求数据包,序号会自动加 1
。为了能够计算往返时间 RTT
,它会在报文的数据部分插入发送时间。
然后,由 ICMP 协议将这个数据包连同地址 192.168.1.2 一起交给 IP 层。IP 层将以 192.168.1.2 作为目的地址,本机 IP 地址作为源地址,协议字段设置为 1
表示是 ICMP
协议,再加上一些其他控制信息,构建一个 IP
数据包。
接下来,需要加入 MAC
头。如果在本地 ARP 映射表中查找出 IP 地址 192.168.1.2 所对应的 MAC 地址,则可以直接使用;如果没有,则需要发送 ARP
协议查询 MAC 地址,获得 MAC 地址后,由数据链路层构建一个数据帧,目的地址是 IP 层传过来的 MAC 地址,源地址则是本机的 MAC 地址;还要附加上一些控制信息,依据以太网的介质访问规则,将它们传送出去。
主机 B
收到这个数据帧后,先检查它的目的 MAC 地址,并和本机的 MAC 地址对比,如符合,则接收,否则就丢弃。
接收后检查该数据帧,将 IP 数据包从帧中提取出来,交给本机的 IP 层。同样,IP 层检查后,将有用的信息提取后交给 ICMP 协议。
主机 B
会构建一个 ICMP 回送响应消息数据包,回送响应数据包的类型字段为 0
,序号为接收到的请求数据包中的序号,然后再发送出去给主机 A。
在规定的时候间内,源主机如果没有接到 ICMP 的应答包,则说明目标主机不可达;如果接收到了 ICMP 回送响应消息,则说明目标主机可达。
此时,源主机会检查,用当前时刻减去该数据包最初从源主机上发出的时刻,就是 ICMP 数据包的时间延迟。
针对上面发送的事情,总结成了如下图:
当然这只是最简单的,同一个局域网里面的情况。如果跨网段的话,还会涉及网关的转发、路由器的转发等等。
但是对于 ICMP 的头来讲,是没什么影响的。会影响的是根据目标 IP 地址,选择路由的下一跳,还有每经过一个路由器到达一个新的局域网,需要换 MAC 头里面的 MAC 地址。
说了这么多,可以看出 ping 这个程序是使用了 ICMP 里面的 ECHO REQUEST(类型为 8 ) 和 ECHO REPLY (类型为 0)。
了解哪些网络错误码吗?
我了解一些常见的网络错误码,以下是其中一些常见的错误码及其含义:
400 Bad Request:请求无效或不完整。 401 Unauthorized:未经授权,需要身份验证。 403 Forbidden:服务器拒绝请求,没有访问权限。 404 Not Found:请求的资源不存在。 500 Internal Server Error:服务器内部错误。 502 Bad Gateway:网关错误,作为代理或网关的服务器从上游服务器接收到无效的响应。 503 Service Unavailable:服务不可用,服务器暂时过载或维护中。 504 Gateway Timeout:网关超时,作为代理或网关的服务器在等待上游服务器响应时超时。 505 HTTP Version Not Supported:不支持的HTTP协议版本。
算法
算法:字符串相乘