Skip to content

Socket considered connected when Select is interrupted by shutdown #34872

Description

@drieseng

When a (blocking) Socket.Select(...) is interrupted by a Socket.Shutdown(SocketShutdown.Both) from another thread, in some cases Socket.Connected continues to return true for some time on Linux.

Even though Socket compatibility with .NET Framework has greatly improved in .NET 5.0 Preview 2 (due to dotnet/corefx#38804?), the result on Linux is still not always the same as we get on Windows.

On Windows (both .NET Core and .NET Framework), Socket.Connected immediately return false if Socket.Select(...) is interrupted because the socket is shut down.

To reproduce, compile and run the following code while specifying a number of iterations to run (I use 300 as value):

using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

class SocketSelect
{
    private static int LastId = 0;

    static void Main(string[] args)
    {
        if (args.Length != 1)
        {
            Console.Error.WriteLine("Please specify the number of iterations.");
            Console.Error.WriteLine("For example:");
            Console.Error.WriteLine("dotnet SocketSelect.dll 300");
            return;
        }

        if (!int.TryParse(args[0], out var iterationCount))
        {
            Console.Error.WriteLine("Please specify a valid number of iterations.");
            Console.Error.WriteLine("For example:");
            Console.Error.WriteLine("dotnet SocketSelect.dll 300");
            return;
        }

        var endPoint = new IPEndPoint(IPAddress.Loopback, 8080);

        var serverTask = Task.Factory.StartNew((ep) =>
        {
            var listener = new TcpListener((IPEndPoint)ep);
            listener.Start();

            while (true)
            {
                var s = listener.AcceptSocket();

                while (true)
                {
                    var bytesReceived = s.Receive(new byte[1]);
                    if (bytesReceived == 0)
                    {
                        s.Shutdown(SocketShutdown.Both);
                        break;
                    }
                    s.Send(new byte[1]);
                }

                s.Close();
            }
        }, endPoint);

        Thread.Sleep(500);

        var threadId = Interlocked.Increment(ref LastId);

        for (var i = 0; i < iterationCount; i++)
        {
            var client = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
            client.Connect(endPoint);

            var allDataReceived = new ManualResetEvent(false);

            var listenerTask = Task.Factory.StartNew(s =>
            {
                var clientState = (ClientState)s;

                var socket = clientState.Socket;
                int dataReceivedCounter = 0;

                while (socket.Connected)
                {
                    try
                    {

                        var readSockets = new System.Collections.Generic.List<Socket> { socket };
                        Socket.Select(readSockets, null, null, -1);

                        if (dataReceivedCounter == 10)
                        {
                            // when we've received 10 "payloads", the socket should be shutdown
                            if (!socket.Connected)
                            {
                                break;
                            }

                            Console.WriteLine($"[{clientState.Id}] Connected | Data received counter = {dataReceivedCounter})");
                        }
                        else if (!socket.Connected)
                        {
                                break;
                        }
                    } catch (Exception ex)
                    {
                        Console.WriteLine($"[{clientState.Id}] Exception on Select ({dataReceivedCounter}): " + ex);
                        break;
                    }

                    try
                    {
                        var bytesReceived = socket.Receive(new byte[1]);
                        dataReceivedCounter++;

                        if (dataReceivedCounter == 10)
                        {
                            allDataReceived.Set();
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"[{clientState.Id}] Exception on Receive ({dataReceivedCounter}): " + ex);
                    }
                }
            }, new ClientState(client, threadId), TaskCreationOptions.LongRunning);

            for (var z = 0; z < 10; z++)
            {
                client.Send(new byte[1]);
            }

            allDataReceived.WaitOne();

            // Allow time for the message loop to enter the blocking Socket.Select(...) call
            Thread.Sleep(100);

            client.Shutdown(SocketShutdown.Both);

            if (!listenerTask.Wait(5000))
            {
                Console.WriteLine($"[{threadId}] Socket did not break out of Select call within 5 seconds.");
            }

            client.Dispose();

        }
    }

    private class ClientState
    {
        public ClientState(Socket socket, int id)
        {
            Socket = socket;
            Id = id;
        }

        public Socket Socket { get; }
        public int Id { get; }
    }
}

Expected result:
The application completes without any output produced.

Actual result (on Linux):
The following is output several times:

[1] Connected | Data received counter = 10)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions