Blog

ICMP终点不可达的场景

Content #

小兵:报告主公,您让把粮草送到张将军那里,结果没有送到。

如果你是主公,你肯定会问,为啥送不到?具体的原因在代码中表示就是,网络不可达代码为 0,主机不可达代码为 1,协议不可达代码为 2,端口不可达代码为 3,需要进行分片但设置了不分片位代码为 4。

具体的场景就像这样:

  1. 网络不可达:主公,找不到地方呀?

  2. 主机不可达:主公,找到地方没这个人呀?

  3. 协议不可达:主公,找到地方,找到人,口号没对上,人家天王盖地虎,我说 12345!

  4. 端口不可达:主公,找到地方,找到人,对了口号,事儿没对上,我去送粮草,人家说他们在等救兵。

  5. 需要进行分片但设置了不分片位:主公,走到一半,山路狭窄,想换小车,但是您的将令,严禁换小车,就没办法送到了。

Viewpoints #

From #

第7讲 | ICMP与ping:投石问路的侦察兵

暂缓性迷惑

Content #

车厢门被推开,一小队犯人蜂拥而入。这些人穿着条纹囚服,头发剃得精光,看起来营养不错,说各种各样的欧洲语言,都带着这一环境中听起来十分怪异的幽默感。仿佛一个快要淹死的人抓住了救命稻草一样,天生乐观的我(乐观情绪经常主宰着我的情感,连最绝望时也是如此)常常想:这些囚徒看起来身体健康、情绪高昂,还时常笑哈哈的,说不定我也能获得他们这样好的待遇呢。

精神病学中有一种被称作“暂缓性迷惑”的状态。被宣布处决的人在行刑前的最后时刻会产生死刑可能暂缓执行的幻觉。我们也抱着这种希望,相信最后的结果不至于太糟。囚徒们胖乎乎、红润润的面庞就是对我们极大的鼓舞。其实,我们并不知道,几年来日复一日跑到车站接新囚徒的这些人是经过特别挑选的“精英”。他们负责接管新囚徒及其行李,这些行李中藏着稀有物品和原本严禁携带的珠宝。欧洲战争的最后几年,奥斯维辛一定算得上一个奇特的地方,不论在大仓库里还是党卫军手中,金、银和钻石等罕见珠宝随处可见。

From #

活出生命的意义

为CMyString类添加赋值运算符函数

Problem #

题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数。

class CMyString
{
public:
    CMyStringchar pData = NULL;
    CMyStringconst CMyString& str;
    CMyStringvoid;

private:
    char m_pData;
};

当面试官要求应聘者定义一个赋值运算符函数时,他会在检查应聘者写出的代码时关注如下几点:● 是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(即*this)。只有返回一个引用,才可以允许连续赋值。否则如果函数的返回值是void,应用该赋值运算符将不能做连续赋值。假设有3个CMyString的对象:str1、str2和str3,在程序中语句str1=str2=str3将不能通过编译。● 是否把传入的参数的类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次复制构造函数。把参数声明为引用可以避免这样的无谓消耗,能提高代码的效率。同时,我们在赋值运算符函数内不会改变传入的实例的状态,因此应该为传入的引用参数加上const关键字。● 是否释放实例自身已有的内存。如果我们忘记在分配新内存之前释放自身已有的空间,程序将出现内存泄露。● 是否判断传入的参数和当前的实例(*this)是不是同一个实例。如果是同一个,则不进行赋值操作,直接返回。如果事先不判断就进行赋值,那么在释放实例自身的内存的时候就会导致严重的问题:当*this和传入的参数是同一个实例时,那么一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了。

经典的解法,适用于初级程序员 #

当我们完整地考虑了上述4个方面之后,我们可以写出如下的代码:

CMyString& CMyString::operator =const CMyString &str
{
    ifthis == &str
        return this;

    delete []m_pData;
    m_pData = NULL;
    m_pData = new char[strlenstr.m_pData + 1];
    strcpym_pData, str.m_pData;

    return this;
}

考虑异常安全性的解法,高级程序员必备 #

在前面的函数中,我们在分配内存之前先用delete释放了实例m_pData的内存。如果此时内存不足导致new char抛出异常,m_pData将是一个空指针,这样非常容易导致程序崩溃。也就是说一旦在赋值运算符函数内部抛出一个异常, CMyString的实例不再保持有效的状态,这就违背了异常安全性(Exception Safety)原则。要想在赋值运算符函数中实现异常安全性,我们有两种方法。一个简单的办法是我们先用new分配新内容再用delete释放已有的内容。这样只在分配内容成功之后再释放原来的内容,也就是当分配内存失败时我们能确保 CMyString 的实例不会被修改。我们还有一个更好的办法是先创建一个临时实例,再交换临时实例和原来的实例。下面是这种思路的参考代码:

CMyString& CMyString::operator =const CMyString &str
{
    ifthis != &str
    {
        CMyString strTempstr;

        char pTemp = strTemp.m_pData;
        strTemp.m_pData = m_pData;
        m_pData = pTemp;
    }

    return this;
}

在这个函数中,我们先创建一个临时实例 strTemp,接着把strTemp.m_pData和实例自身的m_pData做交换。由于strTemp是一个局部变量,但程序运行到 if 的外面时也就出了该变量的作用域,就会自动调用strTemp 的析构函数,把 strTemp.m_pData 所指向的内存释放掉。由于strTemp.m_pData指向的内存就是实例之前m_pData的内存,这就相当于自动调用析构函数释放实例的内存。

...

STP的四个基本概念

Content #

在 STP 协议里面有很多概念,译名就非常拗口。

  1. Root Bridge,也就是根交换机。这个比较容易理解,可以比喻为“掌门”交换机,是某棵树的老大,是掌门,最大的大哥。

  2. Designated Bridges,有的翻译为指定交换机。这个比较难理解,可以想像成一个“小弟”,对于树来说,就是一棵树的树枝。所谓“指定”的意思是,我拜谁做大哥,其他交换机通过这个交换机到达根交换机,也就相当于拜他做了大哥。这里注意是树枝,不是叶子,因为叶子往往是主机。

  3. Bridge Protocol Data Units (BPDU) ,网桥协议数据单元。可以比喻为“相互比较实力”的协议。行走江湖,比的就是武功,拼的就是实力。当两个交换机碰见的时候,也就是相连的时候,就需要互相比一比内力了。BPDU 只有掌门能发,已经隶属于某个掌门的交换机只能传达掌门的指示。

  4. Priority Vector,优先级向量。可以比喻为实力 (值越小越牛)。实力是啥?就是一组 ID 数目,[Root Bridge ID, Root Path Cost, Bridge ID, and Port ID]。为什么这样设计呢?这是因为要看怎么来比实力。先看 Root Bridge ID。拿出老大的 ID 看看,发现掌门一样,那就是师兄弟;再比 Root Path Cost,也即我距离我的老大的距离,也就是拿和掌门关系比,看同一个门派内谁和老大关系铁;最后比 Bridge ID,比我自己的 ID,拿自己的本事比。

Viewpoints #

From #

第6讲 | 交换机与VLAN:办公室太复杂,我要回学校

环路问题

Content #

先看机器 1 访问机器 2 的过程。一开始,机器 1 并不知道机器 2 的 MAC 地址,所以它需要发起一个 ARP 的广播。广播到达机器 2,机器 2 会把 MAC 地址返回来,看起来没有这两个交换机什么事情。

但是这两个交换机还是都能够收到广播包的。交换机 A 一开始是不知道机器 2 在哪个局域网的,所以它会把广播消息放到局域网二,在局域网二广播的时候,交换机 B 右边这个网口也是能够收到广播消息的。交换机 B 会将这个广播信息发送到局域网一。局域网一的这个广播消息,又会到达交换机 A 左边的这个接口。交换机 A 这个时候还是不知道机器 2 在哪个局域网,于是将广播包又转发到局域网二。左转左转左转,好像是个圈哦。

可能有人会说,当两台交换机都能够逐渐学习到拓扑结构之后,是不是就可以了?

别想了,压根儿学不会的。机器 1 的广播包到达交换机 A 和交换机 B 的时候,本来两个交换机都学会了机器 1 是在局域网一的,但是当交换机 A 将包广播到局域网二之后,交换机 B 右边的网口收到了来自交换机 A 的广播包。根据学习机制,这彻底损坏了交换机 B 的三观,刚才机器 1 还在左边的网口呢,怎么又出现在右边的网口呢?哦,那肯定是机器 1 换位置了,于是就误会了,交换机 B 就学会了,机器 1 是从右边这个网口来的,把刚才学习的那一条清理掉。同理,交换机 A 右边的网口,也能收到交换机 B 转发过来的广播包,同样也误会了,于是也学会了,机器 1 从右边的网口来,不是从左边的网口来。

然而当广播包从左边的局域网一广播的时候,两个交换机再次刷新三观,原来机器 1 是在左边的,过一会儿,又发现不对,是在右边的,过一会,又发现不对,是在左边的。

...

查看发送队列与接收队列

Content #

查询套接字信息:

# -l 表示只显示监听套接字
# -t 表示只显示 TCP 套接字
# -n 表示显示数字地址和端口(而不是名字)
# -p 表示显示进程信息
$ ss -ltnp | head -n 3
State    Recv-Q    Send-Q        Local Address:Port        Peer Address:Port
LISTEN   0         128           127.0.0.53:53               0.0.0.0:*        users:(("systemd-resolve",pid=840,fd=13))
LISTEN   0         128                 0.0.0.0:22               0.0.0.0:*        users:(("sshd",pid=1459,fd=3))

其中,接收队列(Recv-Q)和发送队列(Send-Q)它们通常应该是 0。当你发现它们不是 0 时,说明有网络包的堆积发生。当然还要注意,在不同套接字状态下,它们的含义不同。

当套接字处于连接状态(Established)时,

  1. Recv-Q 表示套接字缓冲还没有被应用程序取走的字节数(即接收队列长度)。
  2. 而 Send-Q 表示还没有被远端主机确认的字节数(即发送队列长度)。

当套接字处于监听状态(Listening)时,

  1. Recv-Q 表示全连接队列的长度。
  2. 而 Send-Q 表示全连接队列的最大长度。

From #

34 | 关于 Linux 网络,你必须知道这些(下)

全连接队列与半连接队列

...

超时重传(Retransmission)

Content #

超时重试,也即对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。但是这个超时的时间如何评估呢?

这个时间不宜过短,时间必须大于往返时间 RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。

估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断地变化。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法(Adaptive Retransmission Algorithm)。

当某个包再次超时的时候,需要重传,TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

Viewpoints #

From #

第12讲 | TCP协议(下):西行必定多妖孽,恒心智慧消磨难

接收端缓存结构

Content #

第一部分:接受并且确认过的。第二部分:还没接收,但是马上就能接收的。第三部分:还没接收,也没法接收的。也即超过工作量的部分,实在做不完。

  1. MaxRcvBuffer:最大缓存的量;
  2. LastByteRead 之后是已经接收了,但是还没被应用层读取的;
  3. NextByteExpected 是第一部分和第二部分的分界线。

第二部分的窗口有多大呢?

NextByteExpected 和 LastByteRead 的差其实是还没被应用层读取的部分占用掉的 MaxRcvBuffer 的量,我们定义为 A。

AdvertisedWindow 其实是 MaxRcvBuffer 减去 A。

AdvertisedWindow=MaxRcvBuffer-((NextByteExpected-1)-LastByteRead)。

那第二部分和第三部分的分界线在哪里呢?NextByteExpected 加 AdvertisedWindow 就是第二部分和第三部分的分界线,其实也就是 LastByteRead 加上 MaxRcvBuffer。

其中第二部分里面,由于受到的包可能不是顺序的,会出现空档,只有和第一部分连续的,可以马上进行回复,中间空着的部分需要等待,哪怕后面的已经来了。

Viewpoints #

From #

第12讲 | TCP协议(下):西行必定多妖孽,恒心智慧消磨难

发送端缓存结构

Content #

发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分。

第一部分:发送了并且已经确认的。第二部分:发送了并且尚未确认的。第三部分:没有发送,但是已经等待发送的。第四部分:没有发送,并且暂时还不会发送的。

为什么要区分第三部分和第四部分呢?没交代的,一下子全交代了不就完了吗?

这是出于“流量控制,把握分寸”的需要。作为项目管理人员,你应该根据以往的工作情况和这个员工反馈的能力、抗压力等,先在心中估测一下,这个人一天能做多少工作。如果工作布置少了,就会不饱和;如果工作布置多了,他就会做不完;如果你使劲逼迫,人家可能就要辞职了。

到底一个员工能够同时处理多少事情呢?在 TCP 里,接收端会给发送端报一个窗口的大小,叫 Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分,就是已经交代了没做完的加上马上要交代的。超过这个窗口的,接收端做不过来,就不能发送了。

于是,发送端需要保持下面的数据结构。

  1. LastByteAcked:第一部分和第二部分的分界线
  2. LastByteSent:第二部分和第三部分的分界线
  3. LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线

Viewpoints #

From #

第12讲 | TCP协议(下):西行必定多妖孽,恒心智慧消磨难