您的位置:首页 > 职场人生

Winsock程序员经典问答(中译)之第2部分:Winsock入门级问答

2011-12-06 23:39 316 查看
[b][b]2.1 我该从哪套sockets API入手?
[/b][/b]
从程序员的角度来说,Winsock有两个大版本:2.0和1.1,这正是您需要传给WSAStartup()的参数。您的选择带来的影响是,要么声明#include <winsock2.h>并链接到ws2_32.dll,要么声明#include <winsock.h>并链接到wsock32.dll。您也可以选择在您的程序中声明其它小版本号,然而我却推荐您只用1.1或2.0以使兼容性最佳,除非您确实知道需要使用其它小版本中一些不同的东西。

Winsock的设计是对旧技术的一系列扩充,而不是全新的东西。Winsock 2.0基本上是Winsock 1.1的严格子集,而Winsock 1.1基本上也是BSD Sockets的严格子集。(这两次派生都有一些小地方被弃用或改动。后文中我会讨论这个问题。)

旧的API仍然非常有用。如果您的程序不需要后来API版本的任何新增内容,那么把您的程序限制在Winsock 1.1或可移植的BSD Sockets是最完美的做法。

最新版本的Windows仍然提供Winsock 1.1的dll。Winsock 1与Winsock 2相比有一些限制,但是极少有程序只需要Winsock 2的独有特性的。

同样的, BSD Sockets尽管历史悠久,但仍然包含很多可用的函数。现在Linux、Unix、OS X系统上运行的大多数网络程序仍然仅仅采用了旧的纯BSD Sockets API。这样看的话,甚至Winsock 1.1都可以算是现代、新型、强大的API了。如果您的程序不需要任何Windows专有扩展特性,或者必须可移植到其它操作系统上,那就可以尽管使用BSD Sockets API子集。虽然有些细节上的东西会让你在Windows上重新编译BSD Sockets程序时遇到困难,但是一般很好克服。

我还不知道在新操作系统上选择旧版API会有什么坏处,而且我也不知道在Winsock 2程序构建时使用旧版API子集会有什么坏处。这样做固然会降低可移植性,然而,既然目前的Winsock 2有选项支持14年前的操作系统,这基本上也不算个事。

本文档中的示例程序都是以Winsock 2代码进行构建的,以跟上潮流。(再强调一遍,14年!)也就是说,您只需通过修改本条目开始提到的那些配置,就可以轻松使您的程序在Winsock 1.1下成功构建。

[b]2.2 网上有示例程序吗?[/b]
有。有几个程序列在第5部分(资源),还有一些在第6部分(示例)。如果您是个Winsock菜鸟,也许您会特别关注6.1节的那几个例子。

[b]2.3 调用WSAStartup之前需要初始化WSAData结构体吗?[/b]
不需要,因为WSAStartup()为您填充了这个数据结构。

[b]2.4 编译Winsock程序的时候报链接错误,怎么回事?[/b]
最有可能的是您没有设置与正确的Winsock导入库的链接。如果您使用的是Winsock 2,就应该链接ws2_32.lib;Winsock 1.1链接的是wsock32.lib。

[b]2.5 我写的Winsock程序可以和Unix socket程序通信吗?[/b]
绝对没问题!这是个常见的问题,有此疑问的原因是混淆了协议和API的概念。彼此通信的程序不一定要使用相同的API,只要他们使用相同的传输和网络协议即可。
在处理跨平台网络之前,请先阅读后面的一篇文章《有效使用TCP》。此文讨论了几个妨碍跨平台程序通信的问题,如结构体成员对齐、数据表示等。

[b]2.6 在各种我偏爱的语言里能使用Winsock吗?[/b]
也许可以,但是通常Winsock只是直接用于低级语言如C及其近亲语言,原因如下。

原因1:一些语言缺少可直接调用Winsock API的语言特性。

一款编程语言需要具备如下特性才能访问Winsock:
1. 指针:能够通过地址访问指定的一片内存;
2. 位运算符:能够修改一个字节的指定位;
3. 结构体或记录:能够将一块内存定义为简单数据元素的集合,如两个字符后跟一个16位整型数据,也允许对数据内存布局的一些控制措施。

大多数高级语言缺少其中一个以上的特性。您使用的语言可能有某种扩展机制,提供了一个中介,可以让您编写C或类似语言的代码,但这些常常是不必要的。

原因2:能在Windows平台使用的大多数现代编程语言都提供了网络支持,作为语言本身或其运行环境的一部分。典型代表是跨平台语言,如Java、Perl、Python、Tcl、Ruby、Lua等等。甚至更专的编程语言,如R、Mathematica、Erlang通常也包含网络支持。您没有什么理由直接使用Winsock,除非您使用的语言没有内置网络支持,或者您需要探究该语言的网络支持没有暴露的某种Winsock专有机制。使用某语言本身的网络API,通常能带来更精短、可移植性更高的代码。

原因3:旧的编程语言通常没有把网络特性作为语言的一部分,但是提供了扩展机制,网络特性在此基础上可以后续添加。这方面的一个很好的例子是Visual Basic的旧版本和现在在市场上已停滞不前的ActiveX网络控件。除了这些外接组件的不同,这条原因的论据与原因2一致。

基于这样那样的原因,本文档优先面向C和C++。可能Winsock对C家族的其它近亲成员(如Objective C、D)同样适用,但是我不用这些语言,所以我不能保证代码的兼容性。

如果您使用的语言允许直接访问Winsock API,您可以把文档中的C++代码转换成基于您的语言的等效代码。尽管如此,您最好还是在网上其它地方找找基于您的语言的示例代码。您也许能找到准确满足您需要的代码,如果找不到,至少您也要对该语言中的网络API有一定基础,这样您转换代码的工作会变得更容易。

2.7 有可用的Winsock程序调试工具吗?
主要的调试工具有两种:嗅探器和填充器。

嗅探器

所有关于嗅探器介绍、局限及其变通方案的讨论都转移到了一篇单独的文章《封包嗅探器技术内幕》。

嗅探器最主要的优势是通过它能看到网络通信中的每一位数据,一直到Winsock层下的网络硬件协议层(如以太网协议层)。还有一个优势是,一个好的嗅探器是非常强大且可配置的。例如,某些嗅探器允许你编写“协议插件”来解析任意协议,比如你开发的自定义协议。

到目前为止最流行的封包嗅探器是开源、跨平台的Wireshark套件。(其前身叫做Ethereal。)Firefox自毁商业前程的过程缓慢而不均衡,然而却是不可避免的,同样的,Wireshark推出了一切,就是没有推出其最好的商业版本。它并不完美,但对于日常的封包嗅探已经很好用了,我几乎只使用这一款工具。

幸存的主流商业嗅探器有WildPacket公司的OmniPeek、Network Instruments公司的Observer以及NetScout公司的Sniffer(这款产品的第一版的名字如今成为了标志性术语“嗅探器”)。购买这些产品都要花费1000美元,而且会陷入数以千计的增强版本和可选插件中。我有许多年不用这些东西了,所以我也不能说出它们到底哪里物有所值。(应该是物有所值的,否则它们不可能面对一个足够好的免费软件还能生存这么久。)

我只用另外一款嗅探器tcpdump。此软件的主要优点是几乎现有的每一款Linux、Unix、OS X都会默认安装。当你需要调试什么东西的时候,你常常能就近找到这么一台机器。我几乎从不用它来分析抓到的数据包,因为它的显示格式难以读懂,而且经常不能透露足够的信息。反而我会用它的-w参数把抓到的数据包写入文件,然后压缩之并传回到我的PC,然后用Wireshark加载并分析。不在现场的情况下需要处理问题时,我经常这么干,通过拨号或者ssh进入现场的一台机器。需要注意的是你可能需要加一个-s参数,要不然程序只能抓包头数据。

如果条件允许,你也可以使用Wireshark的tshark命令行辅助工具取而代之。改程序的输出格式更易读,并且默认是抓取整包数据的。

填充器

另一种主要的调试工具是填充器(亦称间谍软件)。填充器通过几种不同的方法监视一个应用程序对Winsock API的使用情况。比起嗅探器来,填充器能带你站在更高的层次上观察程序的网络交互情况。

视角的不同使得填充器更容易展示出你的程序要干什么。而抓到的数据包有时候离题太远,并不会立即有助于你。

(反之亦然:知道了API的调用顺序并不能使你明白网络上传输的数据的所有细节。嗅探器可以让你明白。)

有些场合填充器能做嗅探器所不能之事。有些类型的点对点连接(如调制解调器上运行的PPP)本质上是能阻止嗅探器的,然而如果它们对Winsock开放,填充器就能看到它们的运行情况。大多数嗅探器在本机运行需要管理员权限,而有些类型的填充器则不需要。

主流的填充器有Systems软件科技公司的TracePlus/Winsock(160美元)和WinTECH公司的SocktSpy(60美元)。

工具种种

还有各种各样其它的网络调试工具,对程序员很有用。

每一位网络程序员都需要一个端口扫描器,可以用来探测一台主机上哪些端口开放,哪些端口关闭或被防火墙阻止,甚至可以根据远程主机对扫描数据包的应答得知其上运行的操作系统。Atelier网络公司的安全端口扫描器(亦称AWSPS,价格34美元~120美元不等,取决于功能选项)是这类工具中的一款,有漂亮的图形界面。另一款同类的开源产品是Nmap,功能更强但界面逊色。

另一类有用的工具能告诉你在指定的主机上哪个程序在监听哪些端口。毕竟仅仅知道有程序在监听TCP端口8472,不如知道谁在监听该端口有用。AWSPS也能做这件事。也一款免费但不开源的工具,SysInternals公司的TCPView。还有一款是来自安全公司Foundstone的Vision。在Windows XP及更高版本的系统下,你键入命令行netstat -ba即可得到这类信息的原始形式。其中,-b参数是Windows特有的,如果想在Linux、BSD、OS X系统下得到同样的信息,好好研究一下烦人的lsof命令吧。(想得到上面那个例题的答案,键入命令lsof -i:8472。)

一般性建议

没有一款工具能够满足你的全部需求。你可以用端口扫描器检查打开的端口,启动嗅探器抓取该端口上的数据包,将你的程序定位到该端口抓取数据,然后回来通过填充器运行该程序,找出为什么程序调用API后得到了一些异常的网络数据包(通过嗅探器抓到的)。

我的建议是你要从Wireshar、Nmap和TCPView开始,然后研究一些对应的商业演示版软件,看看是否添加了你想要的功能。我发现填充器用处较少,但是这也许是长期经验所致;我认为绝大多数时候自己去解释某个程序的特定抓包行为也不是什么得不偿失的事。

你也许会觉得《TCP/IP调试》这篇问答性质的文章对TCP程序的不完全自动化调试方法是有用的。

不起作用的方法

有一些调试工具被认为是不起作用或不可靠的。第一个就是socket参数选项SO_DEBUG,在微软系统的堆栈下完全不起作用。另一个是Winsock DLL调试插件dt_dll.dll,是不可靠的。Bob Quinn写了一篇相关的文章,然而不幸的是其所在站点被另一家公司收购了,这篇文章至今也没有再露面。

2.8 我怎样通过Winsock错误码获取可读的错误信息?

这个问题本身就有问题:它的前提是每种场合都有一个“良好的”标准化错误信息。然而现实是,很多时候你得知道程序的环境场景,你才能把一个错误码转换为有意义的错误信息。例如,WSAEFAULT依据不同环境可以表达为下列意义:

无效指针传入

传入的缓冲区太小

不支持该版本API

既然Winsock告诉你了每个函数返回的最有可能的错误码,你就应该利用此信息去构造智能错误处理函数。

尽管如此,一个API调用有时也会返回意外值,因而返回一个加密的错误消息总比没有强。对于此等"default"分支的处理,所有当代版本的Windows(NT4除外)都有一个FormatMessage()接口,输入Winsock错误码,就能返回标准化信息。

在此我必须强调:花点时间构造基于程序场景的有意义的错误消息,比追求那些最好情况下也不见得管用的东西要好得多。当你的程序遇到一个不知该怎么处理的错误时再使用标准化消息。

2.9 Winsock一直返回WSAEWOULDBLOCK的错误。我的程序有什么问题?

这没什么大不了的。使用非阻塞异步socket的程序中出现WSAEWOULDBLOCK是非常正常的事情。这是Winsock用自己的方式告诉程序:“我不能马上做事,因为我被阻塞了。”

接下来的问题是:你怎么知道什么时候再试一把才是安全的?在异步socket模式,调用send()函数失败后,Winsock将会在可以安全写入的时候发送一个FD_WRITE消息;调用recv()函数失败后,Winsock将会在有更多数据到达时发送一个FD_READ消息。同样的,非阻塞式socket程序使用select()函数,可以写入时writefds将被设置,有数据可读时readfds将被设置。

别指望Winsock能以你预期的方式处理阻塞。无论何时你使用了某种形式的非阻塞式socket,你都要随时做好处理WSAEWOULDBLOCK的准备。这就是一种保护式编程方式,就像检查空指针一样。

2.10 不搭建网络的情况下如何测试我的Winsock程序?

有一个特殊的地址称作回路或本机地址,即127.0.0.1。此地址使运行于同一台机器上的两个程序能够通信。通常,服务器程序监听着来自所有可用接口的连接,而客户端程序可以连接主机地址。(基本的客户端和服务器程序代码请参考示例程序一节。)

如果你的开发机上有互联网或局域网连接,你就已经万事具备了。

对于无网络的机器,你必须建立一个空网络。在Windows的现代版本上,你可进入网络控制面板,通知系统添加一个网络接口,然后选择微软回路设备。这样你就不需要任何其它的网络支持就可以工作了。

需要提醒的是:通过回路接口的行为与真实网络不同,正因为单机环境比局域网或广域网上要简单得多。你应该尽量在真实网络上测试程序,尽管你只在单机上做基础的开发工作。

2.11 怎样正确关闭TCP socket?

TCP是一个双向协议。想想,它就像两人之间的谈话。某人说完就走的行为是很粗鲁的。正确的社会习俗要求说完的人要听听另一个人要说什么才可以走开。当一方的话开始引导谈话的结束:“嗯,真好,但我要走了。”,他说完会礼貌地等着另一方说完并同意结束谈话(“好,再见。”)。

Winsock的TCP连接设计也以同样方式工作:

一方结束发送数据,就调用shutdown()函数会通知栈,参数how设置为SD_SEND。

双方继续调用recv()函数。结束发送的一方必须等待,因为连接的另一端可能还没有发送完毕。仍在发送的一方必须继续调用recv()函数,因为在另一端调用shutdown(SD_SEND)后,此处的recv()会返回0。如果你用的是异步socket或事件对象,这时返回的就是FD_CLOSE。这时程序若调用select(),将会返回readfds集合中的一个带标志的socket,告诉你该调用recv()了,之后返回0。注意两端都要对recv()的返回值-1(表示错误)做处理。

只有在确认通信结束后,双方才可以调用closesocket(),要么是因为发送停止并且之后调用recv()返回了0,要么是发生了错误。

违背这一完善的关闭协议会导致数据丢失。

非阻塞式或异步式socket是问题更复杂化。可以将结束发送/接收的逻辑融入你的日常I/O循环,也可以暂时将socket设置为阻塞模式,然后按上述方式执行最后一次I/O。你的程序架构和需求决定了正确的选择应该是什么。

2.12 有可能在关闭连接时发生异常吗?

当然有可能,但这么做是有害的。

最简单的方法是在任意调用closesocket()之后,调用shutdown(),并将参数how设置为SD_BOTH(双向)。还有一种方法是在调用closesocket()之前调用setsockopt(),并将SO_LINGER标志设为0。这会导致TCP连接的重置(即发出的TCP包设置了RST标志,强制远程栈立即关闭连接)。

在极少数情况下,使用这种“连接突然死亡法”是合理的。你必须对TCP的工作方式有非常深刻的了解,才能对什么时候使用这一技术做出正确的决策。一般情况下,若本地或远程的程序崩溃,需要使用连接突然死亡法进行处理。我强烈建议您尽力修改崩溃的程序,这样就不必求助于这种饱受质疑的技术了。

2.13 我怎样检测到TCP连接被关闭?

《I/O策略面面观》一文中讨论的所有I/O策略都有某种方法表示连接被关闭。

首先,要牢记TCP是一个全双工协议。意思是你可以在一端不完全关闭连接,同时另一端继续发送数据。一个实例就是以前的HTTP 1.0网络协议。浏览器向网络服务器发送一个短请求,然后关闭它这一端的连接;然后网络服务器通过它那一端的连接发回请求的数据,并关闭发送端,从而结束了一个TCP会话。(HTTP 1.1更为复杂,但基本的东西终归如是。)

一般的TCP程序只关闭发送一端,对于远程程序而言是接收一端。所以,一般你想检测的远程程序是否关闭了发送一端,即你不能在接收任何数据了。

对于异步socket而言,当连接关闭中断时,Winsock向你发送FD_CLOSE消息。事件对象也是类似:系统通过FD_CLOSE通知触发事件对象。

对于阻塞和非阻塞式socket,你可能会对socket循环调用recv()。当远程程序关闭连接时,recv()返回0。正如你所料,如果使用select(),当连接断开时,read_fds参数的SOCKET描述符会被设置。正常情况下都是调用recv(),看返回值是否为0。

从上述讨论中,你可能也猜到了,接收端关闭连接也是有可能的。这是如果远程程序视图向你发送数据,数据栈会忽略这些数据,并向远程程序发送TCP RST。

关于对异常断开的处理,请参看下文。

2.14 我怎样检测异常的网络断开?

上个问题探讨的是检测协议连接的正常断开,然而如果你想检测诸如未插网线或工作站崩溃之类的其他问题,该怎么办呢?这种情况下,连接出了问题无法通知到远程程序。我感觉这正是特色之处,因为崩溃的程序在引起注意之前可能就被修复了,所以为什么一定要连接被重建呢?

如果你的工作场景要求你必须能够检测所有的网络错误,那么你有两种选择:

选择一:给协议赋予命令/响应结构,主机发送命令,并等待另一主机的响应,表示命令已收到或已执行。如果响应未到达,就认为连接已断开,至少是出了错误。

选择二:在协议中添加echo功能,主机(通常是客户端)周期性向另一主机发送“你还在线吗?”之类的数据包,另一主机必须及时应答。如果发送echo的主机未收到回答,或者接收的主机在规定的周期内没有收到echo请求,程序即可断定连接出了问题,或者远程主机挂了。

如果选择了echo方案,请避免使用ICMP的“ping”机制。如果你使用ping,那么你必须使通信两端都发送ping命令,因为微软的协议栈不会让你看到另一端的echo请求,只能看到echo应答。ping的另一个问题是,它不在你的协议中,因此如果硬件连接没有断开,它是不能检测出TCP连接失败的。最后一个问题,ICMP是不可靠的协议:使用一个不可靠的协议来增强另一个协议可靠性的担保,是一个明智的选择吗?

如果你的程序只在Windows 2000之后的平台使用,你可以考虑TCP持活方案。这种方式定时通知协议栈通过连接发送一个数据包,有没有真实数据没关系。如果主机在线,将发回一个同样的应答包。如果TCP连接失效(例如主机重启),远程主机将发送一个重置包,关闭本地主机的连接。如果远程主机挂掉,本地主机的TCP协议栈将因等待超时而关闭连接。持活机制的最主要问题是持活数据包完全是浪费资源,因为它们不携带有用数据。至少命令/应答方案还可以使每个数据包携带有意义的数据。

需要注意的是,不同类型的网络处理物理连接断开的方式也不同。例如,以太网建立的是非链路层连接,如果你拔掉网线,远程主机是无法告诉本地主机物理上不能通信的。相比之下,PPP连接断开的话,在链路层是可以检测到的,并向上发送到Winsock层,从而你的程序也能够检测的到。

2.15 怎样修改Winsock函数的超时时间?

有些阻塞式的Winsock函数(如connect())内部嵌入了一个超时时间。其背后的原理是只有协议栈拥有全部必要的信息来设置合适的超时时间。然而,有些人认为协议栈使用的值(可达1分钟以上)对于应用程序而言太长了。

你可以通过setsockopt()函数的参数选项SO_SNDTIMEO和SO_RCVTIMEO来调整send()和recv()的超时时间。

对于其它Winsock函数的最佳解决方案是不要完全使用阻塞式socket。所有非阻塞式socket都提供了自定义超时时间的方法。

使用select()的非阻塞式socket:select()函数的第五个参数是超时值。

异步socket:使用Windows API函数SetTimer()。

事件对象:WSAWaitForMultipleEvents()有一个超时参数

可等待计时器:调用CreateWaitableTimers()创建可等待计时器,然后把你的socket传给类似WSAEventSelect()这样的函数,如果计时器时间已到而没有socket被触发,阻塞的函数就会强制返回。

注意:使用异步socket和非阻塞式socket时,你完全可以避免使用超时机制。即使Winsock在忙的时候,你的程序也会继续工作。因此,你可以把取消耗时较长的操作的选择交给用户,或者让Winsock自然等到超时,而非在你的代码里使用此功能。

2.16 什么是窥探?有何坏处?

窥探即提前看到TCP流中的数据:调用recv()时使用MSG_PEEK标志,将从栈缓冲区返回字节数据,但是并不将其从缓冲区移除。(你也可以做另一种形式的窥探:通过调用ioctlsocket()时设置FIONREAD选项)

探根本是不必要的,因为你可以将数据读到自己的缓冲区并就地处理。这种方式是不错的,因为窥探经常带来问题。问题的确很大,在缺陷表和微软的知识库占有一席之地是实至名归的(请看文章KB192599,本文专门讲了窥探操作导致的Winsock协议栈的问题)。

2.17 什么是带外数据(MSG_OOB)?有何坏处?

带外(OOB)数据好比另一个数据通道。这个机制的本意是传输大部分数据使用常规的TCP数据流,传输“加急”信息使用带外数据流。telnet协议传输中断按键Ctrl+C就是使用的带外数据,因而中断的发生不用等待远程程序处理完常规的TCP数据。调用send()时设置MSG_OOB标志就可以发送带外数据,接收带外数据则可在调用recv()时设置MSG_OOB。也可以在调用setsockopt()时设置SO_OOBINLINE标志来获取带外数据。

带外数据是一个有用的概念,但不幸的是,在协议栈层对带外数据的处理方式有两种互相矛盾的解释:早先对带外数据的描述见TCP协议规约(RFC 793),后来此规约被“主机需求”规约(RFC 1122)所取代,但是还有很多机型使用RFC 793规定的带外数据的实现方式。为什么RFC 793与RFC 1122之争会带来问题?对于这一残酷事实,Winsock 2.2.2规约的3.5.2小节给出了解释。

同时OOB也不是一个功能完整的数据通道,它的确很受限制,即使在两种对OOB概念保持一致的机器之间亦是如此。

因此,绝不要使用OOB,除非要实现的是像telnet这样的遗留协议(确实需要它)。简单来说,你可以使用两个数据连接--一个用于正常数据,另一个用于加急数据--以得到可靠的仿OOB行为。

2.18 MSG_PEEK和MSG_OOB都有坏处,那我该给send()和recv()的标志参数传什么?

给send()和recv()的标志参数传0是相当有效的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: