Socket API 是网络应用程序开发中实际应用的标准 API。虽然该 API 简单。可是开发新手可能会经历一些常见的问题。本文识别一些最常见的隐患并向您显示怎样避免它们。
隐患 1.忽略返回状态
第一个隐患非常明显,但它是开发新手最easy犯的一个错误。
假设您忽略函数的返回状态,当它们失败或部分成功的时候,您或许会迷失。
反过来。这可能传播错误。使定位问题的源头变得困难。
捕获并检查每个返回状态。而不是忽略它们。考虑清单 1 显示的样例,一个套接字 send 函数。
清单 1. 忽略 API 函数返回状态
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 探究一个函数片断,它完毕套接字 send 操作(通过套接字发送数据)。函数的错误状态被捕获并測试,但这个样例忽略了 send 在无堵塞模式(由 MSG_DONTWAIT 标志启用)下的一个特性。
send API 函数有三类可能的返回值:
· 假设数据成功地排到传输队列。则返回 0。
· 假设排队失败。则返回 -1(通过使用 errno 变量能够了解失败的原因)。
· 假设不是全部的字符都可以在函数调用时排队,则终于的返回值是发送的字符数。
因为 send 的 MSG_DONTWAIT 变量的无堵塞性质,函数调用在发送全然部的数据、一些数据或没有发送不论什么数据后返回。在这里忽略返回状态将导致不全然的发送和随后的数据丢失。
隐患 2.对等套接字闭包
UNIX 有趣的一面是您差点儿能够把不论什么东西看成是一个文件。文件本身、文件夹、管道、设备和套接字都被当作文件。
这是新颖的抽象,意味着一整套的 API 能够用在广泛的设备类型上。
考虑 read API 函数,它从文件读取一定数量的字节。
read 函数返回读取的字节数(最高为您指定的最大值);或者 -1,表示错误;或者 0,假设已经到达文件末尾。
假设在一个套接字上完毕一个 read 操作并得到一个为 0 的返回值,这表明远程套接字端的对等层调用了 close API 方法。该指示与文件读取同样 —— 没有多余的数据能够通过描写叙述符读取(參见 清单 2)。
清单 2.适当处理 read API 函数的返回值
int sock, status; sock = socket( AF_INET, SOCK_STREAM, 0 ); ... status = read( sock, buffer, buflen ); if (status > 0) { /* Data read from the socket */ } else if (status == -1) { /* Error, check errno, take action... */ } else if (status == 0) { /* Peer closed the socket, finish the close */ close( sock ); /* Further processing... */ }
相同,能够用 write API 函数来探測对等套接字的闭包。
在这样的情况下,接收 SIGPIPE 信号,或假设该信号堵塞,write 函数将返回 -1 并设置 errno 为 EPIPE。
隐患 3.地址使用错误(EADDRINUSE)
您能够使用 bind API 函数来绑定一个地址(一个接口和一个port)到一个套接字端点。能够在server设置中使用这个函数,以便限制可能有连接到来的接口。也能够在client设置中使用这个函数,以便限制应当供出去的连接所使用的接口。bind 最常见的使用方法是关联port号和server,并使用通配符地址(INADDR_ANY),它同意不论什么接口为到来的连接所使用。
bind 普遍遭遇的问题是试图绑定一个已经在使用的port。
该陷阱是或许没有活动的套接字存在,但仍然禁止绑定port(bind 返回EADDRINUSE)。它由 TCP 套接字状态 TIME_WAIT 引起。该状态在套接字关闭后约保留 2 到 4 分钟。
在 TIME_WAIT 状态退出之后。套接字被删除,该地址才干被又一次绑定而不出问题。
等待 TIME_WAIT 结束可能是令人恼火的一件事,特别是假设您正在开发一个套接字server,就须要停止server来做一些修改,然后重新启动。幸运的是,有方法能够避开 TIME_WAIT 状态。能够给套接字应用 SO_REUSEADDR 套接字选项,以便port能够立即重用。
考虑清单 3 的样例。在绑定地址之前,我以 SO_REUSEADDR 选项调用 setsockopt。
为了同意地址重用,我设置整型參数(on)为 1 (不然。能够设为 0 来禁止地址重用)。
清单 3.使用 SO_REUSEADDR 套接字选项避免地址使用错误
int sock, ret, on; struct sockaddr_in servaddr; /* Create a new stream (TCP) socket */ sock = socket( AF_INET, SOCK_STREAM, 0 ): /* Enable address reuse */on = 1; ret = setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) ); /* Allow connections to port 8080 from any available interface */ memset( &servaddr, 0, sizeof(servaddr) );servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl( INADDR_ANY ); servaddr.sin_port = htons( 45000 ); /* Bind to the address (interface/port) */ ret = bind( sock, (struct sockaddr *)&servaddr, sizeof(servaddr) );
在应用了 SO_REUSEADDR 选项之后,bind API 函数将同意地址的马上重用。
隐患 4.TCP 中的帧同步假定
TCP 不提供帧同步,这使得它对于面向字节流的协议是完美的。这是 TCP 与 UDP(User Datagram Protocol,用户数据报协议)的一个重要差别。UDP 是面向消息的协议,它保留发送者和接收者之间的消息边界。TCP 是一个面向流的协议,它假定正在通信的数据是无结构的,如图 1 所看到的。
图 1.UDP 的帧同步能力和缺乏帧同步的 TCP
图 1 的上部说明一个 UDP client和server。左边的对等层完毕两个套接字的写操作,每一个 100 字节。
协议栈的 UDP 层追踪写的数量,并确保当右边的接收者通过套接字获取数据时,它以相同数量的字节到达。换句话说。为读者保留了写者提供的消息边界。
如今,看图 1 的底部.它为 TCP 层演示了同样粒度的写操作。两个独立的写操作(每一个 100 字节)写入流套接字。
但在本例中,流套接字的读者得到的是 200 字节。协议栈的 TCP 层聚合了两次写操作。这样的聚合能够发生在 TCP/IP 协议栈的发送者或接收者中不论什么一方。
重要的是,要注意到聚合或许不会发生 —— TCP 仅仅保证数据的有序发送。
对大多数开发者来说,该陷阱会引起困惑。
您想要获得 TCP 的可靠性和 UDP 的帧同步。除非改用其它的传输协议,比方流传输控制协议(STCP),否则就要求应用层开发者来实现缓冲和分段功能。