$Id$
The UNIX socket API has been standardized for a decade. But the standards are vague on a lot of critical behavior. These tests exist to remove that ambiguity.
A summary of my test results is below. To know what happens on your exact systems, run the tests yourself!
| test | Linux | OS X | ||
|---|---|---|---|---|
| Non-blocking connects | ||||
| Behaviors of various tests while performing a non-blocking connect. I've somewhat arbitrarily declared some to have passed and failed. The important thing is the aggregate: can I develop a consistent procedure that will determine the state of a connection across platforms? | ||||
| connect after connected | 0 | EISCONN | ||
| connect after refused | ECONNREFUSED | EINVAL | ||
| connect while connecting | ENOTCONN | ENOTCONN | ||
| getsockopt(SO_ERROR) after connected | 0 | 0 | ||
| getsockopt(SO_ERROR) after refused | ECONNREFUSED | ECONNREFUSED | ||
| getsockopt(SO_ERROR) while connecting | 0 | 0 | ||
| getpeername after connected | 0 | 0 | ||
| getpeername after refused | ENOTCONN | ENOTCONN | ||
| getpeername while connecting | ENOTCONN | ENOTCONN | ||
| read(0) while connecting | EWOULDBLOCK | 0 | ||
| read(0) after connected (no data) | EWOULDBLOCK | 0 | ||
| read(0) after connected (data) | EWOULDBLOCK | 0 | ||
| read(0) after refused | ECONNREFUSED | ECONNREFUSED | ||
| read(1) while connecting | EWOULDBLOCK | EWOULDBLOCK | ||
| read(1) after connected (no data) | EWOULDBLOCK | EWOULDBLOCK | ||
| read(1) after connected (data) | 1 | 1 | ||
| read(1) after refused | ECONNREFUSED | ECONNREFUSED | ||
| write(0) while connecting | EWOULDBLOCK | ENOTCONN | ||
| write(0) after connected | 0 | 0 | ||
| write(0) after refused | ECONNREFUSED | EPIPE | ||
| write(1) while connecting | EWOULDBLOCK | ENOTCONN | ||
| write(1) after connected | 1 | 1 | ||
| write(1) after refused | ECONNREFUSED | EPIPE | ||
| Closing sockets | ||||
| Corner cases dealing with FIN vs RST | ||||
| Default close type | background | background | ||
| read(1) from background-closed peer | 0 | 0 | ||
| write(1) to closed peer | 1 | 1 | ||
| shutdown(SHUT_WR) after peer close | 0 | 0 | ||
| … getsockopt(SO_ERROR) | ??? | ??? | ||
| shutdown(SHUT_WR) after peer reset | ENOTCONN | EINVAL | ||
| … getsockopt(SO_ERROR) | ENOTCONN | ECONNRESET | ||
| shutdown(SHUT_WR) after peer close, local write | ENOTCONN | EINVAL | ||
| … getsockopt(SO_ERROR) | EPIPE | ECONNRESET | ||
| read(1) after peer close, local write | 0 | ECONNRESET | ||
| read(1) after peer reset | ECONNRESET | ECONNRESET | ||
| write(1) after peer reset | ECONNRESET | EPIPE | ||
| getsockopt(SO_ERROR) after EPIPE | (no EPIPE) | ECONNRESET | ||
| Background close with unread data | RST | FIN | ||
| 1-second lingering close with no ACK received | 0 (after 1 sec) | 0 (after 0.01 sec)[1] | ||
| 1-second lingering close on non-blocking socket[2] | 0 (after 1 sec) | 0 (immediately) | ||
[1] - OS X blocking linger uses 1/100th of the requested value, due to a missing factor of HZ. It also stores the value in a short, limiting linger to (2^15-1)/HZ sec ~= 5.5 minutes.
[2] - close is the only IO syscall with no EWOULDBLOCK return.
We need a procedure for making a non-blocking connect. It should be tolerant of spurious readiness notifications from epoll(2) and the like. Thus, it needs a way to distinguish the connecting state, as well as the connected and failed states. Ideally, it should have no side effects (data read or written) and be the same on all platforms.
Armed with the information above, we can produce such a procedure:
(Note that by the result of getsockopt(SO_ERROR), I mean the resulting optval. The direct return value should always be 0.
To correctly handle all closing cases, a portable program should: