404 NOT Found

在程序的世界里,有一大类问题都可以划分到404 NOT Found的类别里,一大票问题都可以归结于查表找不到...

构造函数

class Dispatch{
  Dispatch(int i){}
};
Dispatch a;// Compile Error

最早学C++的时候,总是被这个问题搞的莫名奇妙,原来C++编译器会给一个class偷偷添加默认构造函数
但是,如果用户自己定义了一个,则编译器不再提供默认构造函数。此时上述代码要通过的话需要我们
自己给Dispatch添加一个无参数构造函数,方可通过。

class Dispatch{
  Dispatch(){}
  Dispatch(int i){}
};
void DoSomething(){
  Dispatch a;// Compile Error
}

我们可以说

404 NOT Found:您调用的构造函数不存在
  • 添加explicit关键字的单参数构造函数,刻意让想要隐式转换并调用构造函数的时候,404 NOT Found...:
    see:what does the explicit keyword in c mean

  • 至于当时为什么我会在没有无参构造函数的时候写Dispatch a;这样的语句,应该是以为所有的变量都可以这样声明吧。可惜C++里这里的行为就是在栈上分配了内存,调用了无参构造函数。

  • 默认构造函数无参构造函数是两个概念。默认行为往往是坑,一旦使用方的需求和默认行为不匹配,而使用者又对默认行为不清楚,就会出现各种诡异的问题。
    see:Do the parentheses after the type name make a difference with new?

  • 比如说当使用STL的时候,class的拷贝构造函数、赋值操作符,小于比较操作符等都在模板算法里被使用,一个学习STL的新手一定会在不了解模板的duck type interface原理时被这种默认行为坑到。
    see:What is The Rule of Three?

  • C++03里构造函数不能调用别的构造函数,因为class还没构造完呢。。C++11添加了语法糖,支持了...

数组名和指针

C的数组和指针的区别在于,数组名只是数组起始地址的别名,编译器编译后就被替换成了数组的首地址,而一个指向数组的指针则是一个变量,是运行期行为。下面的代码:

char array_place[100] = "don't panic";
char* ptr_place = "don't panic";

int main()
{
    char a = array_place[7];
    char b = ptr_place[7];

    return 0;
}

编译后的汇编是

    char a = array_place[7];

0041137E  mov  al,byte ptr [_array_place+7 (417007h)]
00411383  mov  byte ptr [a],al

    char b = ptr_place[7];

00411386  mov  eax,dword ptr [_ptr_place (417064h)]
0041138B  mov  cl,byte ptr [eax+7]
0041138E  mov  byte ptr [b],cl

数组名示意图:

指向数组的指针示意图:

所以,而一个函数的数组参数

void foo(char arr_arg[], char* ptr_arg)
{
    char a = arr_arg[7];
    char b = ptr_arg[7];
}

编译后是:

char a = arr_arg[7];

00412DCE  mov  eax,dword ptr [arr_arg]
00412DD1  mov  cl,byte ptr [eax+7]
00412DD4  mov  byte ptr [a],cl

    char b = ptr_arg[7];

00412DD7  mov  eax,dword ptr [ptr_arg]
00412DDA  mov  cl,byte ptr [eax+7]
00412DDD  mov  byte ptr [b],cl

则是一模一样的,这是因为编译的时候,函数并没有被调用,所以编译器并不知道arr_arg的实际地址是什么,所以编译器就只能把它向指针一样处理。
这部分内容源自:are pointer and arrays equivalent in c?

我们可以说

404 NOT Found:函数编译时,数组的实际地址找不到,请看:https://lkml.org/lkml/2015/9/3/428,“because array arguments in C don't
actually exist”

野指针

一个class的指针成员变量,如果未被初始化,则是一个野指针,它不是NULL。
于是在运行的时候,它指向的内存(并不是有效的该类数据)被解码成这个类的数据,此时实际上是乱码的。
这样的乱码数据运行下去,就会有运行期的未定义行为

我们可以说

404 NOT Found:野指针,指针指向的数据并不是有效的对象数据,您要的数据不存在

又:函数里返回局部栈上变量的引用或者指针,函数调用完的时候,函数的Stack被ret掉,局部变量的引用或指针就指向了已经被ret了的内存,
在后续的stack变来边去的时候,那块内存的数据早就不是原来的了。

404 NOT Found:您指向的栈地址的数据早就不是当初那个东西了...

此处有人还长篇大论:Can a local variable's memory be accessed outside its scope?

未定义符号

C/C++ 编译的时候,出现未定义符号,原因可能是这个符号所在的头文件并没有被包含。
为了找到它,可能的行为包括

  1. 指定Include查找目录
  2. 添加必要的头文件

我们可以说

404 NOT Found:符号所定义的文件找不到

未解决的符号

C/C++链接的时候,出现未解决的符号,原因可能是这个符号的定义虽然在编译的时候找到了,但是链接的时候没发现它的实现。
为了找到它,可能的行为包括
0. 比如说函数在头文件里实现了,在.c或者.cpp里却没有实现,那么实现它

  1. 添加Lib查找的目录
  2. 添加需要连接的Lib文件

我们可以说

404 NOT Found:符号没有被实现或者找不到obj,或者找不到库(一堆obj的合集)

又:它的反面是同一个符号有多个实现的版本,比如多个同时链接了多个不同实现的C运行时。此时不是找不到,而是找到了多个不知道用哪个,解决也很简单,明确指定要用的是哪个。
又:还有一种是同时只有多个弱符号,没有一个强符号,则不知道用哪个。

这两种情况,可以说

404 NOT Found: 有多个版本,找不到一个最强的来用

模板的具现化

#define type_zero 0
#define type_one 1
#define type_two 2

template <int type>
struct trait;          //Declare

template <>
struct trait<type_zero>{ //A
  enum {value=0}; 
};

template <>
struct trait<type_one>{ //B
  enum {value=1};
};

template <>
struct trait<type_two>{ //B
  enum {value=2};
}

void DoSomething(){
  std::cout<<trait<2>::value<<std::endl;//(1) 
  std::cout<<trait<3>::value<<std::endl;//(2),Compile Error
}

对于(1):

  1. 编译器尝试使用A版本具现化,不匹配,错误A,先不出错,下一步;
  2. 编译器尝试使用B版本具现化,不匹配,错误B,先不出错,下一步;
  3. 编译器尝试使用C版本具现化,匹配,忽略错误A和错误B

对于(2):

  1. 编译器尝试使用A版本具现化,不匹配,错误A,先不出错,下一步;
  2. 编译器尝试使用B版本具现化,不匹配,错误B,先不出错,下一步;
  3. 编译器尝试使用C版本具现化,不匹配,错误C,抛出编译错误。

如果编译器在错误A的时候就直接编译错误,那就没什么好说了,但编译器会尝试找重载的模板尝试具现化,直到所有的尝试都失败时才认为是真的失败了。

我们可以说

404 NOT Found:找不到可以具现化的模板

在尝试具现化的过程中遇到失败的情况先不抛出的特性也被起了个名字:

SFINAE: "Substitution Failure Is Not An Error"

再来一个例子:

struct example
{
    template <typename T>
    static void pass_test(typename T::inner_type); // A

    template <typename T>
    static void pass_test(T); // B

    template <typename T>
    static void fail_test(typename T::inner_type); // C
};

int main()
{
    // enumerates all the possible functions to call: A and B
    // tries A, fails with error; error withheld to try others
    // tries B, works without error; previous error ignored
    example::pass_test(5);

    // enumerates all the possible functions to call: C
    // tries C, fails with error; error withheld to try others
    // no other functions to try, call failed: emit error
    example::fail_test(5);
}

see: Substitution Failure Is Not An Error

竞态条件

class Dispatch{

public:
  void AddObserver(Observerable* o){
      AutoSpinLock lock(m_lock);
      ...  
  }
  void RemoveObserver(Observerable* o){
      AutoSpinLock lock(m_lock);
      ...
  }
  void NotifyObservers(){
     AutoSpinLock lock(m_lock);
     Observers::iterator it=m_observers.begin();
     while(it!=m_observers.end()){
       Observer* o = *it;
       o.DoSomething();
       ++it;
     }
  }
private:
  typedef std::vector<Observerable*> Observers;   
  SpinLock m_lock;
  Observers m_observers;
}

发生的情况

  1. 线程A:NotifyObservers。
  2. 线程B:Observer要delete之前,调用Remove,成功,然后就析构自己。
  3. 此时A线程的DoSomething还在过程中,崩溃。

此时我们可以说

404 NOT Found:您要通知的对象已被析构...

解决的办法就是用引用计数智能指针+弱引用智能指针

class Dispatch{

public:
  void AddObserver(weak_ptr<Observerable> o){
      AutoSpinLock lock(m_lock);
      ...  
  }
  void RemoveObserver(weak_ptr<Observerable> o){
      AutoSpinLock lock(m_lock);
      ...
  }
  void NotifyObservers(){
     AutoSpinLock lock(m_lock);
     Observers::iterator it=m_observers.begin();
     while(it!=m_observers.end()){
       shared_ptr<Observer> obj(it->lock());// 如果存活,增持,避免被析构
       if(obj){
         o.DoSomething();
         ++it;
       }else{
         it = observers.erase(it);
       }
     }
  }
private:
  typedef std::vector<std::weak_ptr<Observerable>> Observers;   
  SpinLock m_lock;
  Observers m_observers;
}

see:Linux多线程服务端编程

如果不用智能指针,不带引用计数,那么,可以在NotifyObservers的时候,不立刻执行DoSomething,而是投递到目标线程去执行。假设Observer的工作线程是B,Dispatch在线程A,则NotifyObservers可以投递到线程B,在线程B才做真正的遍历触发,则保证Observer的Add、Remove、Find都在线程B,从而避免线程问题。

切换脚本语言

一个同学写了一段时间的lua代码

for i,v in pairs(nodes) do
  --//do something
end

有一天切换成到Python环境下写代码,同样遍历字典

for i,v in pairs(nodes):
  ## do something

404 NOT Found:paris是个什么鬼...

去掉

for i,v in nodes:
  ## do something

404 NOT Found:没有迭代器...

好吧:

for i,v in nodes.items():
  ## do something

声明的时候还没有定义

C的例子

typedef struct{
  ...
  list_node *next; // error
} list_node;

解决:

typedef struct list_node list_node;
struct list_node{
  ...
  list_node *next; 
};

ps,C和C++的tag

C++的例子

典型的C++ 前置声明用来做PIMPL (Private Implementation) 惯用法,避免循环include头文件

class A;
class B;
class C{
public:
  A* GetA();
  B* GetB();
}

Lua的例子

function fac()
  print(fac())--//error, 递归定义,此时fac还不存在
end

解决:

local fac
function fac()
  print(fac())
end

语法糖:

local function fac()
  print(fac())
end

scheme的例子

Y Combinator:http://mvanier.livejournal.com/2897.html
Y Combinator想要使用lambda搞出递归,最后办法就是把代码拷贝一份传递进去..

我们可以说

404 NOT Found:定义还没完成呢,想要用它的话,加个中介待定系数法吧

作为反例,switch-case的case里不能直接定义变量,因为case只是个可以goto的label,如果没有被goto到,这样的变量就只定义而没有初始化...,see:Why can't variables be declared in a switch statement?

版本

  • A:哥,测一个
  • B:好的,马上布下新服务,
  • 10秒过去..
  • A:哥,协议不对啊,好像你还是旧的协议
  • B:咦,怎么上传的还是旧的
404 NOT Found: 需要的协议版本不对,测试部署最好也做版本号区分

心跳

https://en.wikipedia.org/wiki/Heartbeat_(computing)
https://en.wikipedia.org/wiki/Keepalive

无论是Tcp还是Udp,连接中都会使用心跳包,每隔n秒发送一次心跳包给服务器,每隔m秒判断是否有回包。如果没有回包,则判断连接断开。在某些情况下,允许中间断开一段时间,这样会在稍后重试心跳。程序如下:

  1. 开始心跳,如果心跳失败,每个n秒重试,
  2. 连续重试m次如果失败,就会等待一个大的时间x秒,x秒后重新开始1

利用心跳,可以做到:

  • 在客户端,如果心跳失败,说明要么网络出问题,要么服务器出问题,则客户端可以连不上服务器,在P2P网络里则该客户端甚至无法与其他节点做打洞或者反连。
  • 在服务端,根据心跳可以判断在线节点的个数,如果出现大面积不在线,则要么网络出问题,要么客户端出现某种未知异常。
404 NOT Found: 心跳失败,既死亡

分支闭合

分支似乎不用说。编程里最基础的便是if else。然而我们流行一句话: 很多bug最后定位到的时候往往发现是一个很2的问题。
为什么会这样呢?根据我的经验,我觉得还是因为if else没做到处处闭合(封闭性)。编程语言并不强调有if就要有else,这是语法级别的。而且我们很多时候为了避免代码嵌套过深,采用卫语句的方式提前返回。当情况复杂时,就容易漏掉某个分支。
所以,编程还是回归到了最最基本的逻辑,在语义上要强调分支的闭合,有if就要有else,即使你不写出来。而且工程健壮的程序,就是在处理各种错误分支,好的程序对所有的错误分支都是有意识并且有处理的,缺一不可。所谓测试,核心其实就是对分支闭合的测试,这也是体现工程师逻辑是否严密的地方。

404 NOT Found: 这个分支你没处理

IP and Port

Ip和端口构成了一个EndPoint。

网络的世界里,两个EndPoint,A和B之间唯一定位彼此的是靠Ip和Port。可是,中间是各种墙(NAT,Router),只要任何一个地方把src和dest的Ip,Port封掉,它们之间就无法通信。网络上的可靠通信,基本原理是,三次握手打开连接,四次挥手关闭连接。放开关闭不说,单说握手。

1. A send syn to B
2. B send ack to A
3. A send ackack to B

三个步骤中的syn、ack、ackack任何一个包发送失败,都会导致该连接不能建立。而一般来说,如何把syn包从A发给B就是一个难题.

首先要对NAT分类:

see:wiki-NAT

  1. Full-cone NAT, also known as one-to-one NAT.

    • Once an internal address (iAddr:iPort) is mapped to an external address (eAddr:ePort), any packets from iAddr:iPort are sent through eAddr:ePort.
    • Any external host can send packets to iAddr:iPort by sending packets to eAddr:ePort.
  2. (Address)-restricted-cone NAT.

    • Once an internal address (iAddr:iPort) is mapped to an external address (eAddr:ePort), any packets from iAddr:iPort are sent through eAddr:ePort.
    • An external host (hAddr:any) can send packets to iAddr:iPort by sending packets to eAddr:ePort only if iAddr:iPort has previously sent a packet to hAddr:any. "Any" means the port number doesn't matter.
  3. Port-restricted cone NAT.

    • Like an address restricted cone NAT, but the restriction includes port numbers.
    • Once an internal address (iAddr:iPort) is mapped to an external address (eAddr:ePort), any packets from iAddr:iPort are sent through eAddr:ePort.
    • An external host (hAddr:hPort) can send packets to iAddr:iPort by sending packets to eAddr:ePort only if iAddr:iPort has previously sent a packet to hAddr:hPort.
  4. Symmetric NAT

    • Each request from the same internal IP address and port to a specific destination IP address and port is mapped to a unique external source IP address and port; if the same internal host sends a packet even with the same source address and port but to a different destination, a different mapping is used.
    • Only an external host that receives a packet from an internal host can send a packet back.

根据情况,1,2,3都可以利用规则把完成语义上的syn的动作,如果两端都是Symmetric NAT就没救了。在NAT后面的,要想连接,就得trick NAT的映射机制。

其次对A和B分类,其中,2和3是同一种类型:

  1. A和B都在外网
  2. A在外网,B在内网(NAT后面)
  3. A在内网(NAT后面),B在外网
  4. A和B都在内网(NAT后面)

根据情况,1可以直连,2和3可以反连,4可以通过打洞方式,完成syn动作。如果把1,2,3,4做merge,即实现了一个混合多路发syn的connector。

再次,对使用的协议做分类:

  1. 使用Tcp
  2. 使用Udp

根据情况,1可以做直连和反连,打洞相对困难,但也不是不可以做,用Tcp不用自己做错误处理、拥赛控制。2可以做直连、反连、打洞,但是用Udp需要自己重新实现各种错误处理、拥赛控制以实现可靠连接。

最后,对Ip和端口的恶劣场景做分类

  1. 频繁换IP,双网卡
  2. 某些端口被NAT和Router封杀

对于这两点,只能通过:

  1. 重新绑定Ip和端口
  2. 随机化端口,但如果用户有Upnp的情况下,随机的端口可能未必能用。

以上没有说明的一点是A和B在应用层是用什么互相标识的。举例常见的Id情况:直接用Ip,用域名,P2P网络里用PeerId。无论是直连、反连还是打洞、本质上都是在做Id到可达EndPoint之间的转换,在域名系统里,这就是DNS的职责,常见的网络污染就发生在DNS这个环节。

404 NOT Found: Ip和端口不对。
posted @   ffl  阅读(765)  评论(2编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示