Как правильно подключиться к сокету

У меня есть следующий метод, который соединяется с конечной точкой, когда моя программа запускает

ChannelSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
var remoteIpAddress = IPAddress.Parse(ChannelIp);
ChannelEndPoint = new IPEndPoint(remoteIpAddress, ChannelPort);
ChannelSocket.Connect(ChannelEndPoint);

У меня также есть таймер, который запускается каждые 60 секунд для вызова CheckConnectivity, который пытается отправить произвольный массив байтов в конечную точку, чтобы убедиться, что соединение все еще живое, и если сбой отправки, он попытается снова подключиться.

public bool CheckConnectivity(bool isReconnect)
{
    if (ChannelSocket != null)
    {
        var blockingState = ChannelSocket.Blocking;
        try
        {
            var tmp = new byte[] { 0 };
            ChannelSocket.Blocking = false;
            ChannelSocket.Send(tmp);
        }
        catch (SocketException e)
        {
            try
            {
                ReconnectChannel();
            }
            catch (Exception ex)
            {
                return false;
            }
        }
    }
    else
    {
        ConnectivityLog.Warn(string.Format("{0}:{1} is null!", ChannelIp, ChannelPort));
        return false;
    }

    return true;
} 

private void ReconnectChannel()
{
    try
    {
        ChannelSocket.Shutdown(SocketShutdown.Both);
        ChannelSocket.Disconnect(true);
        ChannelSocket.Close();
    }
    catch (Exception ex)
    {
        ConnectivityLog.Error(ex);
    }

    ChannelSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    var remoteIpAddress = IPAddress.Parse(ChannelIp);
    ChannelEndPoint = new IPEndPoint(remoteIpAddress, ChannelPort);
    ChannelSocket.Connect(ChannelEndPoint);
    Thread.Sleep(1000);

    if (ChannelSocket.Connected)
    {
        ConnectivityLog.Info(string.Format("{0}:{1} is reconnected!", ChannelIp, ChannelPort));
    }
    else
    {
        ConnectivityLog.Warn(string.Format("{0}:{1} failed to reconnect!", ChannelIp, ChannelPort));
    }
}

Итак, как бы я тестировал выше, физически отключить кабель LAN от моего устройства ethernet, позволяя моему коду пытаться снова подключиться (что не получается) и повторно подключить кабель LAN.

Однако даже после повторного подключения кабеля LAN (способного к ping) ChannelSocket.Connect(ChannelEndPoint) в моем методе Reconnect всегда вызывает эту ошибку

No connection could be made because the target machine actively refused it 192.168.168.160:4001

Если бы я перезапустил все свое приложение, он будет успешно подключен. Как я могу настроить метод повторного подключения таким образом, что мне не нужно перезапускать приложение для повторного подключения к моему устройству Ethernet?

Ответ 1

Если приложение закрывает порт TCP/IP, протокол указывает, что порт остается в состоянии TIME_WAIT для определенной продолжительности (по умолчанию 240 секунд на машине Windows). См. Следующие ссылки -

http://en.wikipedia.org/wiki/Transmission_Control_Protocol

http://support.microsoft.com/kb/137984

http://www.pctools.com/guides/registry/detail/878/

Что это означает для вашего сценария - это то, что вы не можете ожидать закрытия (добровольно или неохотно) и повторно открыть порт за короткий промежуток времени (даже несколько секунд). Несмотря на некоторые настройки реестра, которые вы найдете в Интернете, порт будет недоступен для любого приложения в окнах в течение как минимум 30 секунд. (Опять же, по умолчанию - 240 секунд)

Ваши параметры - здесь ограничены...

"Если сокет был ранее отключен, вы не сможете использовать этот метод (Connect) для восстановления соединения. Используйте один из методов асинхронного BeginConnect для повторного подключения. Это ограничение основного поставщика".

Причина, по которой документация предполагает, что BeginConnect должна использоваться, - это то, о чем я упоминал выше. Он просто не ожидает, что сможет установить соединение сразу. И, следовательно, единственный вариант - сделать асинхронный вызов, и пока вы ждете, пока соединение установится через несколько минут, ожидайте и планируйте его сбой. По существу, скорее всего, это не идеальный вариант.

  1. Если долгое ожидание и неопределенность неприемлемы, то другой вариант - как-то согласовать другой порт между клиентом и сервером. (Например, в теории вы можете использовать UDP, который без установления соединения, для согласования нового TCP-порта, на который вы восстановите соединение). Связь с использованием UDP, в теории, конечно, сама по себе не гарантируется дизайном. Но он должен работать большую часть времени (сегодня сетевое взаимодействие в типичной организации не так плохое/ненадежное). Субъективный сценарию/мнению, возможно, лучше, чем вариант 1, но больше работы и меньшая, но конечная вероятность не работать.

  2. Как было предложено в одном из комментариев, именно там применяются протоколы прикладного уровня, такие как http и http-сервисы. Если это возможно, используйте их вместо низкоуровневых сокетов. Если это приемлемо, это лучший вариант для перехода.

(PS - FYI - для HTTP есть много специальной обработки, встроенной в ОС, включая окна - например, имеется выделенный драйвер Http.sys, специально для работы с несколькими приложениями, пытающимися прослушивать на том же порту 80 и т.д. Подробности здесь - тема для другого времени.. точка есть, есть много добра и тяжелой работы, сделанной для вас, когда дело доходит до HTTP)

Ответ 2

Возможно, вам стоит перейти на более высокий класс абстракции, который лучше справляется со всеми этими изящными мелочами?

Я собираюсь использовать для этих сетевых подключений TcpListener и TcpClient. Использование этих классов довольно просто:

Клиентская сторона:

public void GetInformationAsync(IPAddress ipAddress)
{
    _Log.Info("Start retrieving informations from address " + ipAddress + ".");
    var tcpClient = new TcpClient();
    tcpClient.BeginConnect(ipAddress, _PortNumber, OnTcpClientConnected, tcpClient);
}

private void OnTcpClientConnected(IAsyncResult asyncResult)
{
    try
    {
        using (var tcpClient = (TcpClient)asyncResult.AsyncState)
        {
            tcpClient.EndConnect(asyncResult);
            var ipAddress = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address;
            var stream = tcpClient.GetStream();
            stream.ReadTimeout = 5000;
            _Log.Debug("Connection established to " + ipAddress + ".");

            var formatter = new BinaryFormatter();
            var information = (MyInformation)formatter.Deserialize(stream);

            _Log.Info("Successfully retrieved information from address " + ipAddress + ".");
            InformationAvailable.FireEvent(this, new InformationEventArgs(information));
        }
    }
    catch (Exception ex)
    {
        _Log.Error("Error in retrieving informations.", ex);
        return;
    }
}

Серверная сторона:

public void Start()
{
    ThrowIfDisposed();

    if (_TcpServer != null;)
        _TcpServer.Stop();

    _TcpServer = new TcpListener(IPAddress.Any, _PortNumber);
    _TcpServer.Start();

    _TcpServer.BeginAcceptTcpClient(OnClientConnected, _TcpServer);
    _Log.Info("Start listening for incoming connections on " + _TcpServer.LocalEndpoint + ".");
}

private void OnClientConnected(IAsyncResult asyncResult)
{
    var tcpServer = (TcpListener)asyncResult.AsyncState;
    IPAddress address = IPAddress.None;

    try
    {
        if (tcpServer.Server != null
            && tcpServer.Server.IsBound)
            tcpServer.BeginAcceptTcpClient(OnClientConnected, tcpServer);

        using (var client = tcpServer.EndAcceptTcpClient(asyncResult))
        {
            address = ((IPEndPoint)client.Client.RemoteEndPoint).Address;
            _Log.Debug("Client connected from address " + address + ".");

            var formatter = new BinaryFormatter();
            var informations = new MyInformation()
            {
                // Initialize properties with desired values.
            };

            var stream = client.GetStream();
            formatter.Serialize(stream, description);

            _Log.Debug("Sucessfully serialized information into network stream.");
        }
    }
    catch (ObjectDisposedException)
    {
        // This normally happens, when the server will be stopped
        // and their exists no other reliable way to check this state
        // before calling EndAcceptTcpClient().
    }
    catch (Exception ex)
    {
        _Log.Error(String.Format("Cannot send instance information to {0}.", address), ex);
    }
}

Этот код работает и не создает проблем с потерянным соединением на стороне клиента. Если у вас потерянное соединение на стороне сервера, вам нужно восстановить слушателя, но это другая история.

Ответ 3

В ReconnectChannel просто удалите объект ChannelSocket.

try
    {
     `//ChannelSocket.Shutdown(SocketShutdown.Both);
        //ChannelSocket.Disconnect(true);
        //ChannelSocket.Close();
        ChannelSocket.Dispose();`   
    }

Это работает для меня. Дайте мне знать, если это не сработает для вас.