Как правильно подключиться к сокету
У меня есть следующий метод, который соединяется с конечной точкой, когда моя программа запускает
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
должна использоваться, - это то, о чем я упоминал выше. Он просто не ожидает, что сможет установить соединение сразу. И, следовательно, единственный вариант - сделать асинхронный вызов, и пока вы ждете, пока соединение установится через несколько минут, ожидайте и планируйте его сбой. По существу, скорее всего, это не идеальный вариант.
-
Если долгое ожидание и неопределенность неприемлемы, то другой вариант - как-то согласовать другой порт между клиентом и сервером. (Например, в теории вы можете использовать UDP, который без установления соединения, для согласования нового TCP-порта, на который вы восстановите соединение). Связь с использованием UDP, в теории, конечно, сама по себе не гарантируется дизайном. Но он должен работать большую часть времени (сегодня сетевое взаимодействие в типичной организации не так плохое/ненадежное). Субъективный сценарию/мнению, возможно, лучше, чем вариант 1, но больше работы и меньшая, но конечная вероятность не работать.
-
Как было предложено в одном из комментариев, именно там применяются протоколы прикладного уровня, такие как 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();`
}
Это работает для меня. Дайте мне знать, если это не сработает для вас.