显示标签为“学习笔记”的博文。显示所有博文
显示标签为“学习笔记”的博文。显示所有博文

2007年8月30日

不够厚道的Microsoft

仍然在做IPV6的对应工作......

说起来,我原来一直都是对.NET Framework抱有好感的,对MSDN的各种说明也是比较信任的。然而,当我深入研究了System.Net命名空间下的Dns.GetHostAddress(string hostNameOrAddres)方法后,我的心情只能用一个词来形容,那就是"失望"。

这个方法是微软在.NET Framework V2.0中新加入的方法。作用是解析用户输入的域名/地址,生成一个或多个抽象的IPAddress对象。

在该方法的说明文档中(不知道为什么,关于该方法,中文或日文的MSDN中没有任何说明),对于返回值的定义存在如下描述:

If hostNameOrAddress is an IP address, this address is returned without querying the DNS server.


然而,实际情况是:当操作系统没有启用IPV6时,如果用户输入一个IPV6的地址(比如"fe3b::234:4b35"时)。Dns.GetHostAddress方法根本不会解析出一个IPAddress对象,而是直接抛出了一个SocketException,其对应的Winsock Error Code为11001(WSAHOST_NOT_FOUND).看来在系统未启用IPV6的情况下,这个API是把用户输入的IPV6地址当作普通域名来处理了。

然而匪夷所思的事情还有呢。当我把系统的IPV6启用,但停用IPV4时,尝试输入一个IPV4地址。本来按照思维定势,以为它也会抛一个SocketException。然而此时它却没有抛异常,而是返回给我一个长度为0的IPAddress数组,真是让我哭笑不得。

根据我的实际测试结果,对Dns.GetHostAddress(string)方法的返回值做了如下总结:






















输入的域名/地址形式 只启用了IPV4 只启用了IPV6 启用了IPV4+IPV6
IPV4型地址 长度为1的IPAddress数组 长度为0的IPAddress数组 长度为1的IPAddress数组
IPV6型地址 抛出SocketException 长度为1的IPAddress数组 长度为1的IPAddress数组
普通域名 长度为n的IPAddress数组(*) 长度为n的IPAddress数组(*) 长度为n的IPAddress数组(*)

(*: 具体的数组长度,需要根据DNS服务器的实际绑定,或者HOST文件中的定义来决定)


由此可见,我觉得微软对于这个方法的设计是存在比较严重的败笔的。首先,该API的实际动作在IPV4环境和IPV6环境表现不一致;另外,关于输入IPV6型IP地址会导致SocketException的情况,在MSDN关于该方法的说明文档中并没有提及,唯一关于输入IP地址的叙述,与该方法的实际效果其实是南辕北辙的。

其实,设计函数也是门学问。至少,应该友好一点吧~

2007年8月21日

获取主机所有网络接口的IPV6地址

最近在做一项关于IPV6的对应工作。因为工作平台是.NET Framework,所以也参照了不少关于这方面的文档。上午看到了一篇帖介绍了关于如何获取当前主机上所有网络接口(如网卡)的IPV6单播地址的方法,感觉还不错,所以晚上便把它摘录了下来。

using System.Collection;
using System.Net;
using System.Net.NetworkInformation;
static IPAddress[] GetAllAddressV6()
{
    IPAddress [] ret = null;
    ArrayList alRet = new ArrayList();
    NetworkInterface[] adapters =
        NetworkInterface.GetAllNetworkInterfaces();
    foreach(NetworkInterface adapter in adapters)
    {
        IpInterfaceProperties adapterProperty =
            adapter.GetIPProperties();
        UnicastIPAddressInformationCollection allAddress =
            adapterProperty.UnicastAddresses;
        foreach(UnicastIPAddressInformation addr in
            allAddress)
        {
            if (addr.Address.AddressFamily ==
                AddressFamily.InterNetworkV6)
            {
                alRet.Add(addr.Address);
            }
        }
    }
    ret = alRet.ToArray(typeof(IPAddress)) as IPAddress[];
    return ret;
}



原帖请参见 这里

注:当上述方法返回的数组长度为零时,就说明当前主机根本没有被分配合适的IPV6地址。

2007年6月26日

关于C#中控制线程的一点笔记

.NET为多线程的操作提供了很多类和方法,但是翻遍了MSDN,却没有一个令当前线程挂起直到指定线程集全部终止的方法。于是经过在Google Group上和网友讨论,找到了一个比较实用的方法,并将其写成一个函数。

函数目的:控制当前线程等待直到指定线程集合全部终止。

void WaitTilOthersEnd(Thread [] arr)
{
  bool bOtherAlive = true;
  while(bOtherAlive )
  {
    bOtherAlive = false;
    for (index = 0; index < arr.Length; index++)
    {
      bChildThAlive = arr[index].IsAlive || bChildThAlive;
    }
    Thread.Sleep(0);
  }
}


这样一来,只要调用了该函数。那么当前函数在指定线程集的所有线程终止之前是不会继续下面的动作的。虽然是个很简单的方法,不过我在实践中经常用到它,感觉还算有用,所以在这里做个笔记。也希望今后还能从性能的角度对它做进一步的修改。

2006年3月25日

关于值/引用传递的一点深入理解

关于值传递和引用传递,第一次在考卷之外认识到它的重要性是在去年八月做类分析器的演习项目中,在那之后仔细翻阅了钱能的那本C++教材,觉得它讲得不够彻底,于是又去翻阅了谭浩强的那本著名的C语言教材,基本上能够从本质上认清了值/引用传递,前两天在翻阅Deitel父子的<C# for Experienced Programmers>时再一次巩固了对值/引用传递的认识,遂作此文。

函数(方法)传值,从本质上来说其实是只有一种机制——将实参在栈中生成一个备份,称作"形参"。然后在栈中展开函数,对形参进行操作。函数中所有对形参的更改都不会保留到实参中。比如下面这个常见的例子:

void swap(int, int);

void main()
{
int a=3;
int b=5;
swap(a, b);

cout << "a=" << a << "; b=" << b <<endl;
}

void swap(int var1, int var2)

{
int temp = var1;
var1 = var2;
var2 = temp;
}

在上面的例子中,本意是想通过调用swap函数来更改a,b的值,结果因为函数中只对形参进行操作,当函数调用结束,栈被清空,存储在内存数据段的a,b值没有受到任何影响。

这时,如果对上 面的swap函数进行如下调整:

void swap(int *p1, int *p2)
{
int temp = (*p1);
(*p1) = (*p2);
(*p2) = temp;
}

对应的函数调用也应该改为 swap(&a, &b);再次输出结果就会看出变量a,b的值的确被对调过了。

BUT WHY?

原因很简单,关键就在于更改后的函数签名中的参数已经不再是单纯的int型,而是int型指针——说白了,就是两个内存地址。于是,当swap函数被调用时。变量a和b的地址会各被复制一份用作函数的形参。而在上例的函数体中,进行交换的则是两个地址中所存储的数值。而这一点是相当重要的,因为保存有两个地址的形参仍然会在函数调用结束后从栈中清空,但由于两个地址是数据段的地址,而在形参被清空之前,两个数据段地址实际保存的值已经被交换了(就是变量a,b的地址所保存的值被交换),所以在函数调用结束后可以看出a和b的值被交换过了

由于很多资料对引用传递的机制 表述很含糊,就会读者造成这么一种错觉——如果传给参数的是指针,那么在函数体内操纵的就是实际的指针。因此,下面一种错误就出来了。

void swap(int *p1, int *p2)
{
int *temp = p1;
p1 = p2;
p2 = temp;
}

调用函数的方法依旧为swap(&a, &b);作者本以为函数体内操纵的就是a,b的实际地址,所以预料输出结果应该是正确的。但实际上,它在swap函数内操作的仍旧为a, b地址在栈中的备份。

现在来看看小wing在去年八月所犯下的一个错误:我想要写一个函数来生成一个单链表的头节点,所以写了如下方法(之前已经定义了节点 Node——是一个结构体):

int GetFirstNode(Node *pHead)
{
if (pHead)
{
return 0;
}
else
{
pHead=(Node*)malloc(sizeof (Node));
// Assign the value to the members of Node
... ...
return 1;
}
}

当我调用该函数时,我首先让头节点指针pHead为NULL,之后通过int flag = GetFirstNode(pHead)的方式来调用函数。满以为调用结束后,pHead会指向新生成的节点,结果一跟踪却 发现pHead仍旧为 NULL。

当然,现在来看这段代码很容易就能发现问题的所在(这是一段比较危险的代码):由于pHead一开始为空,调用函数时,pHead的形参自然也是为空,之后的代码中却改变了 pHead的值(是个地址)。但是调用结束后,形参被销毁,pHead没有变化。事实上,这段代码中,pHead 没有正确指向预期的地址倒是小时,关键问题是在函数体中调用了malloc在堆上申请了空间后却没有任何指针可以控制它,更不用说去释放这段内存空间。这便造成了烦人的内存泄漏。

实际上,这段代 码可以进行如下修正就可以解决这个问题:

int GetFirstNode(Node **pHeadPoint)
{
if ((*pHead))
{
return 0;
}
else
{
(*pHead)=(Node*)malloc(sizeof (Node));
// Assign the value to the members of Node
... ...
return 1;
}
}

实际调用时,假设一开始声明仍旧是Node * pHead = NULL;那么就可以利用 GetFirstNode(&pHead)来让pHead指向新生成的节点。因为指向pHead的指针(即双重指针)在函数体中没有发生变化。

SO...

到现在,我们就可以得出这样一个结论——
值传递 和引用传递在本质上没有区别,都是利用函数的形参。关键在于,如果在函数体中想要改变一个外部变量(可能是个基本数据类型变量,也有可能是一个指针变量)的值,并且希望这种改变能够在函数调用完成之后仍旧被保持。那么,就请将该外部变量的地址作为函数的参数传进去。

另外,由于C#的托管代码中没有指针和取址符&amp;amp;所以C#提供了两个特有的关键字ref和out,前者更像C/C++中的取址符 &,而后者则更能应付在调用函数前没有经过初始化的变量。而C#中需要记住的另一个原则就是,任何class类型的变量其本质都是一个指针——即变量所保存的值其实是一个指向该class实例的地址。