clq

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

 很抱歉这一篇文章中仍然要讨论 C 语言 socket 函数相关的知识,其实准确来说这些是任意一种开发语言直接调用系统 api 时都要掌握的知识点.这些知识是一定要再详细解释一次的.

1.A版本函数和W版本函数的问题

    比如其中说到的 LoadLibrary 函数的 A 版本和 W 版本的问题,这其实是说 windows 函数中有两个 LoadLibrary 函数,分别是 LoadLibraryA 和 LoadLibraryW,这是因有最初的 C 字符串对应的都是 ANSI 字符串,后来有了 UNICODE 字符串, A 版函数的字符串参数的对应的就是 ANSI 字符串, W 版本其实对应的就是 UNICODE 字符串. 实际上 dll 的导出函数中并没有 LoadLibrary 这个函数,而是不是用 LoadLibraryA 就得用 LoadLibraryW,之所以你们能在开发环境中用 LoadLibrary 那是被宏定义重新定义了的结果.对于绝大多数涉及到字符串操作的 windows api 函数,都有 A 版和 W 版,至于现在如日中天的 utf8 字符串,那是没有的,因为 utf 更先进也更晚"出生",大多数情况下 UNICODE 已经足够用了,所以是要将 utf8 转换成 UNICODE 后再调用 W 版本的 api 函数. 也许以后会有 utf8 版本的也不一定...
    这在 vc 环境下编译时是有可能引起错误的,所以大家要注意一下.


2.非常重要的字节对齐问题

    因为我们上一编示例中用的是 C 语言来讲解原始的系统 socket api 函数来访问网络,这里面其实隐藏了一个非常重要的网络编程的细节.那就是字节对齐! 回顾一下上一篇文章中的代码,大量的使用了 socket 相关的结构体,看上去它们和普通的结构体并没有什么区别,但是假如我们是用 delphi 来做示例的话,会 delphi 语法的同学一定会发现它们的结构体声明中多了一个 packed 声明.例如:

THostEnt = packed record
h_name: PChar;
h_aliases: ^PChar;
h_addrtype: Smallint;
h_length: Smallint;
case integer of
0: (h_addr_list: ^PChar);
1: (h_addr: ^PInAddr);
end;


我又要很惭愧地说,我又是工作近一年后才知道它是什么意思,以及是有多重要的.简单地来说这个标志就是要告诉编译器,这个结构体不要进行字节对齐,声明了多个字节的数据就给多少个字节 -- 这是什么意思? 初学者一定会莫名其妙,我们来看下 windows 中的一个真实结构体声明:

struct tagRGBTRIPLE {
BYTE rgbtBlue;
BYTE rgbtGreen;
BYTE rgbtRed;
}


你觉得编译器会给这个结构体分配几个字节的内存? 你会说3个呀? 没错,这很容易用 sizeof 函数来证明. 好吧,你自己声明一个

struct tagRGBTRIPLE_my {
BYTE rgbtBlue;
BYTE rgbtGreen;
BYTE rgbtRed;
}


你觉得还会是3个字节吗? 我根本不用试,就可以告诉你多半是 4 个字节,你打开 vc 试了一下,果然是 4 个,你会问为什么我自己写的结构体是是 4 个字节? 更细心的读者还会再问一个问题:为什么说多半是 4 个难道作者自己也不能确定?我的答案是,我确实不能确定是占用多少个字节,因为这受很多因素的影响,一般来说编译器会把结构体进行 4 字节或者 8 字节进行对齐,所以一般来说自定义的那个结构体会是 4 字节. 而系统自带的那个 tagRGBTRIPLE 会是三字节,那是因为 vc 在它代码上下加了字节对齐要用多少个的编译指令,它的完整声明应该是:

#pragma pack(push,1)
typedef struct tagRGBTRIPLE {
BYTE rgbtBlue;
BYTE rgbtGreen;
BYTE rgbtRed;
} RGBTRIPLE,*LPRGBTRIPLE;
#pragma pack(pop)


大家不信的话可以直接打开头文件看看.

说到用 pragma pack 指令来控制结构体,我觉得这真的是一个可怕的语法,如果 pragma pack 指令没有成对出现的话那真是一场大灾难,所以 delphi 才会直接修饰到结构体中,虽然 C 语言读者会不高兴不过这真的是一种更先进的做法.而 gcc 确实要考虑得更周全一些: gcc 可以在结构体上直接用 __attribute__ 指令来完成这个任务.

至于结构体为什么要进行字节对齐(以及其他更详细的信息),大家自行百度吧,我们这里主要是说它对网络编程的影响.

曾经我在一家公司开会讨论问题时,一位主力的手机开发人员谈起这个结构体字节数对不上的问题时,另一个同事很不屑的直接说,这是结构体字节对齐问题,你连这个都不知道还写什么网络程序... 确实如果这个问题都不搞清楚的话大家还是先不要写网络程序的好. 这么严重! 你会说. 是的,就有这么严重,因为我们前面的示例都是发送收取的字符串,而字符串这里都是用的单字节的字节流,所以大家没感觉到字节对齐的影响.但是当你要发送一个结构体给另外一个程序的时候问题就会出现了.服务器一般都是写好了的,假设它要求一个三字节的结构体,好了,你定义了一个上面那种 tagRGBTRIPLE_my 的结构体填充好数据后发送出去,第一个结构体也许会正常收到了,第二个结构体呢 ... 如果您仔细看了前面的内容一定会知道数据是对不上的! 因为你这时候实际上发送了4个字节出去.

因为 smtp 等协议的特点,这个问题一直都不会影响我们后面的示例,所以只能请大家自行牢记字节对齐的影响了.不要在将来让别人也说你一个开发网络程序的人连字节对齐都不知道...

(图1:vc6中的结构体对齐设置)

 3. inet_addr 还是 gethostbyname

    这个问题就要有点 socket 经验的同学才看得懂了,不过也没有关系,大家先记着,以后再回头来看.
    在上次的代码中实际上有两个连接网络的函数: ConnectHost 和 ConnectIP, ConnectHost 是实际用到的函数,而 ConnectIP 则是大多数教程中常用的 inet_addr 函数的简单封装而已. inet_addr 可以将一个字符串转换成 connect 函数需要的地址结构,在我刚学网络编程的时候很长一段时间内分不清 inet_addr 和 gethostbyname 有什么区别 -- 反正测试能连接成功. 这主要是以前的环境上网不易,很多时候都是局域网内连接一个 ip 就写测试程序了,两者的作用又有点近似 -- 都是将字符串变成 connect 的参数嘛. inet_addr 放到互联网上其实也可以,但是假如大家真的拿来连接示例中的 newbt.net 的话就会发现连接是失败的. 当然了大多数同学可能是知道的,那是因为 inet_addr 只能转换 ip 地址,而 gethostbyname 可以转换域名. 对于我这样喜欢刨根问底的人来说 gethostbyname 是 socket 函数族中最特别的函数:大家看看前面的例子就可以知道 socket 函数都是完成非常底层的工作的,象 inet_addr 不过是将地址的字符串表示变成二进制而已,而 gethostbyname 能直接转换域名实在是神奇,因为这里面实际上还包括了访问 dns 的过程,而访问 dns 实际上是要用另外一个 udp 的 socket 过程来完成的,如果明白了整个 socket 过程的话一定会是非常奇怪的,为什么 socket 函数开恩把这么复杂的过程变成了直接一个函数来完成? 这估计是因为域名的转换没有直接访问 dns 就行了那么简单,还要根据不同的情况有可能会访问操作系统的 host 文件,还要考虑用什么样的 dns 服务器地址 ... 一系列的问题如果都靠程序员去实现的话显然太过复杂,于是 socket 函数的最初设计者只好把域名转换成 ip 二进制的过程放到一个函数中算了.

另外据说 linux 现在有新的函数来代替 gethostbyname 了,也许在不久的将来大家就可能要用到那个新函数.

再另外,其实 gethostbyname 取到的是一串 ip 地址的数组,教程中一般都是直接取第一个了事,原因也要以后再说了.

4.真的能连接了吗 -- 引出为什么手机不能直接使用原始 socket


    这里还要和大家说一个问题,得到地址后用 connet 就能连接上网络了.但是这里有一个前提,那就是你的电脑得先连接上网络,这个 connect 函数的作用其实不是 "连接上网络" 而是 "让我的程序连接上网络",如果电脑本身就没能连接上那是不行的. 网友一定气坏了,你这不废话吗? 当然不是废话,早期的电脑就算电脑连接上了所有的网络设备和线路后还要有一个过程才能上网:那就是拨号.当然现在的拨号过程都是路由来完成了,在 pc 上当然不会有程序需要去自己完成拨号功能的,但科技发展来到手机时代后,拨号就是必须要完成的了.因为手机要控制流量是不能可一直在线的,程序必须在需要时先进行拨号操作,而使用原始的 socket 是不会产生拨号动作的! 早期的 windows ce 时代我写过很多 ppc 手机程序,有个公司就是写不了括号过程,每次要连接网络时都调用一下浏览器访问一个网址,让浏览器先帮它拨号成功 ... 所以现在的 ios 是封装了 socket 函数的,所有的 ios 编程书都会介绍你使用那个封装后的 socket 而告诫你说不要使用原始的 socket ,道理就是这样. 安卓当然也是类似. 另外手机不推荐使用原始 socket 的原因还有线程和ipv6以及 socket 阻塞等等 ...

    这些都来不及说了,我们以后再详细展开吧.下篇文章还是让我们先回到 smtp/pop3 等通讯协议上来吧.

--------------------------------------------------

版权声明:
本系列文章已授权百家号 "clq的程序员学前班" .

有些网友转发了本系列的一些文章,很感谢大家,这也是对文章内容的认可,不过还是希望大家在条件允许的情况下尽可能的放上文章在博客园的原始首发网址 ... 再次感谢.

 

posted on 2018-01-29 11:35  clq  阅读(823)  评论(0编辑  收藏  举报