Socket编程的几个陷阱

翻译自http://www.ibm.com/developerworks/library/l-sockpit/

一、忽略返回值

第一个陷阱很明显,但却是新手最容易犯错的地方。如果你忽略了函数的返回值,就可能注意不到某些致命或非致命错误的发生。这些错误不断出现,让出错代码的定位变得十分困难。

不要无视这些返回值,捕捉并检查每个返回值。

Listing 1. Ignoring API function status return

1
2
3
4
5
6
7
8
9
10
int status, sock, mode; /* Create a new stream (TCP) socket */
sock = socket( AF_INET, SOCK_STREAM, 0 );
...
status = send( sock, buffer, buflen, MSG_DONTWAIT );
if (status == -1) {/* send failed */
printf( "send failed: %s\n";, strerror(errno) );
} else {
/* send succeeded -- or did it? */
}

程序清单1定义了一个函数用来创建一个套接字并发送数据。函数中的返回值被捕捉并测试过,但是这个例子忽略了非阻塞(通过MSG_DONTWAIT标志启用)发送的一个特点。

send() 调用可能的返回值有三个:

  1. 若数据成功进入缓冲区队列,返回0.
  2. 若出错,返回-1(并且错误码会被存入全局变量errno)
  3. 若数据没有完全进入缓冲区队列,返回值则是已发送的字节的长度。

由于是非阻塞方式,send() 调用可能在所有数据发送完、部分发送或者尚未发送的情况下返回。一旦忽略这个返回值会导致数据的丢失。

二、Socket闭包

UNIX一个有趣的特性就是: 你可以把一切都视为文件。普通文件、目录、管道、设备以及套接字都被当作文件。这是一次伟大的抽象,意味着同一套 API 可以应用于广泛的设备类型上。

以 read() 调用为例,read() 调用从文件中读取一定数量的字节。read() 返回读取的字节数,当读取出错时返回-1,当读取到文件末尾则返回0。

如果你从文件中读取到末尾(由某次返回的0指示),就能关闭文件。同样的做法也适用于 Socket 中,但是其语义有所不同。如果读取某个套接字并得到一个0返回值,这代表远端的套接字已经被 close() 调用所关闭。同样,0 值代表在文件读取中,没有更多的数据可通过这个描述符读取。

三、发送结构化数据

套接字是传输非结构化的二进制字节流或者 ASCII 字节流数据(比如基于HTTP协议的HTML页面和基于SMTP的email)的理想工具。然而当你试图通过套接字传送二进制数据,事情就变得尤为困难。

假设你想通过套接字发送一个整型的数字,试想你能否确定接收方将会以同样的方式解释这个整数么?相同架构的机器上运行的应用程序能依赖通用的平台标准来解释这个类型,但是如果事情发生在发送方为大端IBM PowerPC而接收方是一台32位小端的Intel x86机器的情况下呢?字节的顺序的不同会导致这个数值被解释成错误的值。

翻转与否

字节序决定了内存中储存字节的顺序,大端意味着从尾部开始,而小端正相反。

大端架构(例如PowerPC)往往要比小端(如Intel Pentium系列)更有优势。因为在计算机网络中,字节是以大端顺序为标准的。这意味着大端架构的机器能够更自然地通过TCP/IP协议栈处理网络数据。而小端架构的机器则需要字节翻转,因此,运行网络应用时,大端机器比小端机器有那么一点微弱的性能优势。

那如果发送的是一个C的结构体呢?你同样会陷入困境,因为并非所有的编译器在对结构体成员进行对齐(Align)时遵循同样的标准。结构体也会被装入一些仅仅用于占位的字节。

幸运的是,有一些解决方案能够确保双端彼此的数据一致地被解释。上古时代的RPC(Remote Procedure Call)工具提供了一种被称为XDR(External Data Representation)的方法,XDR定义了在进行异构网络程序开发中的数据的标准呈现。如今,一对新的协议提供了相似的兼容—-XML/RPC(The Extensible-Markup-Language/Remote Procedure Call protocol)。它基于HTTP协议,使用XML格式进行通讯。数据和元数据被编码为XML,并组织为ASCII字符串,实现了值与物理载体的分离。XML-RPC之后的SOAP并拓展了其思路,提供了更优秀的特性和功能。