0

I have a server application that uses IOCP. I want to know what is the proper way to close a SOCKET.

If I simply call closesocket() (for a SOCKET with a handle of for example 12345), and this SOCKET has pending IO operations (for example: a pending WSARecv() request), then the following scenario could happen:

  • I call closesocket() which will destroy the SOCKET.

  • I accept another SOCKET with the same handle of 12345.

  • I deque the pending WSARecv() completion packet for the SOCKET with the handle of 12345. Now I would assume that this completion packet is for the current SOCKET with the handle of 12345, but in fact it is for the SOCKET that was previously closed (this is he main problem with this approach).

So this is obviously a bad approach.

The second approach that seems correct is the following:

  • I associate a struct instance with each SOCKET. The struct has the following members: an int called number_of_pending_IO_operations, and a boolean called Is_SOCKET_being_closed.

  • When I issue an IO operation for the SOCKET (for example: a WSASend() request), I increment number_of_pending_IO_operations by 1, and when I deque a completion packet for the SOCKET, I decrement number_of_pending_IO_operations by 1.

  • Now, when I want to close the SOCKET, I don't simply call closesocket(), but rather I call CancelIOEx() to cancel all pending IO operations for the SOCKET, and I also set Is_SOCKET_being_closed to true.

  • When I am about to issue another IO operation (for example: a WSASend() request), I would check the value of Is_SOCKET_being_closed, and if it is true, I would not issue the IO operation.

  • Now I simply wait for all of the completion packets to be dequeued, and when number_of_pending_IO_operations reaches 0 and Is_SOCKET_being_closed is set to true, I call closesocket().

Of course I would have race conditions, so I would use critical sections.

Is the second approach a correct way to close a SOCKET, or is there a better way?

Steve
  • 691
  • 5
  • 18
  • "The second approach" - yes, now you on correct way. however you not need critical sections here, all can be done with interlocked operations. `InterlockedIncrement`/`InterlockedDecrement` for `number_of_pending_IO_operations`. about `Is_SOCKET_being_closed` not good enough. really here need run-down protection (for socket handle). in user mode no api for this, but very simply implement it by self. `if (AcquireRundownProtection()) { WSASend(); ReleaseRundownProtection(); }`when want closesocket - initiate rundown (but not wait for it here) and when it complete close handle. – RbMm Feb 27 '17 at 16:08
  • about [Run-Down Protection](https://msdn.microsoft.com/en-us/library/windows/hardware/jj569382(v=vs.85).aspx) – RbMm Feb 27 '17 at 16:09
  • 2
    Note that the `OVERLAPPED` structure you pass to I/O operations can be the first member of a larger structure containing context information, e.g., a pointer to the structure associated with the socket. That saves you looking up the socket handle in a table or similar, which in turns means there's no compelling reason not to close the socket if it is convenient to do so. – Harry Johnston Feb 27 '17 at 21:01
  • RbMm good to see fellow driver devs here:-). Looks like you have some work experience with minifilters. I actually did implement rundown protection in userland in the same codebase and was considering to use it for exactly this purpose (in addition to some others) -- and decided against it after some measurements. Rundown protection seems to be better fitted for the case where the references are longer lived, and where they don't spawn (or handling failure to AddRef() is easier), and when you have a clear owner therefore you guarantee that there is only one call to BeginRundown. – Sergei Vorobiev Mar 07 '17 at 05:20
  • Harry Johnson, I don't see how this approach alone guarantees correctness in case when closesocket() is called while another thread enters WSARecv/WSASend(). As for containing OVERLAPPED inside another structure, this is indeed a very useful technique. – Sergei Vorobiev Mar 07 '17 at 05:50
  • @SergeiVorobiev, if your program is multithreaded you still have to make sure other threads aren't actively using the socket handle before you can safely close it. It's just that that wasn't the problem the OP asked about, and the OP seemed aware enough of race conditions that I didn't think it necessary to mention it. – Harry Johnston Mar 07 '17 at 21:49

1 Answers1

1

I had a similar issue in my client-server application. I think I have no races, no rundown protection, and no critical sections. There are some tradeoffs though.

  • only one WSARecv() at a time. Multiple buffers -- maybe, but only one WSARecv(). WSARecv completes (possibly, inline), packet pops from completion port, I quickly check what I got and issue another WSARecv()
  • I have a flag (actually, a counter that can only go up) for Is_SOCKET_being_closed. Counter because I may simultaneously decide to kill the socket in two different places.
  • after setting Is_SOCKET_being_closed, I call shutdown() followed by CancelIoEx() which will break the previously issued WSARecv().
  • there is a race between CancelIoEx and completion of WSARecv() -- I eliminate it with double checking Is_SOCKET_being_closed immediately before issuing WSARecv(). Even if race occurs, WSARecv() is doomed to fail because shutdown().
  • the structure is refcounted, where one ref is held by the receive state machine, and one ref is held for every legal owner of the ptr (as in, potential sender). If you have a legal ref, you can make a copy which you can hand off to someone else (it differs here from rundown references as their AddRef() may fail). I will only closesocket() when that ref drains to zero, because I cannot guarantee that there is no thread that has passed all the checks and about to issue a WSARecv/WSASend.
Sergei Vorobiev
  • 364
  • 2
  • 8
  • I'm not sure whether having only a single pending WSARecv() per socket is going to impact performance or not - I'd guess not, since any data that arrives while the IOCP is queued will just be buffered by the TCP driver. If you wanted to have multiple receives pending, you could always reference count them. But I think the main reason not to do that is that if two such IOCPs were received by different threads, how would you know what order they belonged in? :-) – Harry Johnston Mar 07 '17 at 21:59
  • Harry Johnson -- consider the case where you decide to reuse your TCP code for UDP. As for refcounting receives, sure, very likely it would just work, but I have not fully thought it through. – Sergei Vorobiev Mar 07 '17 at 22:37
  • TCP and UDP are different enough that you need to expect to revise your code. – Harry Johnston Mar 07 '17 at 22:54
  • you are probably right, I don't have experience with UDP. – Sergei Vorobiev Mar 08 '17 at 00:31