Подтвердить что ты не робот

Начало работы с программированием сокетов на С# - Лучшие практики

Я видел много ресурсов здесь, на SO о Sockets. Я считаю, что ни один из них не охватывал детали, которые я хотел знать. В моем приложении сервер выполняет всю обработку и отправляет периодические обновления клиентам.

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

1 - Связывание и прослушивание сокета

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

IPHostEntry localHost = Dns.GetHostEntry(Dns.GetHostName());
IPEndPoint endPoint = new IPEndPoint(localHost.AddressList[0], 4444);

serverSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, 
                     ProtocolType.Tcp);
serverSocket.Bind(endPoint);
serverSocket.Listen(10);

2 - Получение данных

Я использовал массив байтов размером 255. Поэтому, когда я получаю данные размером более 255 байт, мне нужно вызвать метод получения, пока я не получу полные данные, не так ли? Как только я получу полные данные, мне нужно добавить все полученные байты, чтобы получить полное сообщение. Это верно? Или есть лучший подход?

3 - Отправка данных и указание длины данных

Поскольку в TCP нет способа найти длину получаемого сообщения, я планирую добавить длину сообщения. Это будет первый байт пакета. Поэтому клиентские системы знают, сколько данных доступно для чтения.

Любой другой лучший подход?

4 - Закрытие клиента

Когда клиент закрыт, он отправит сообщение серверу с указанием закрытия. Сервер удалит данные клиента из его списка клиентов. Ниже приведен код, используемый на стороне клиента для разъединения сокета (часть сообщений не показана).

client.Shutdown(SocketShutdown.Both);
client.Close();

Любые предложения или проблемы?

5 - Закрытие сервера

Сервер отправляет сообщение всем клиентам, указывающим на выключение. Каждый клиент будет отключать сокет, когда он получит это сообщение. Клиенты отправят закрытое сообщение на сервер и закроют. Когда сервер получает сообщение от всех клиентов, он отключает сокет и прекращает прослушивание. Вызовите Dispose на каждом клиентском сокете, чтобы освободить ресурсы. Это правильный подход?

6 - Неизвестные отключения клиентов

Иногда клиент может отключиться, не сообщая серверу. Мой план заключается в следующем: когда сервер отправляет сообщения всем клиентам, проверьте состояние сокета. Если он не подключен, удалите этого клиента из списка клиентов и закройте сокет для этого клиента.

Любая помощь будет замечательной!

4b9b3361

Ответ 1

Поскольку это "начало", мой ответ будет придерживаться простой реализации, а не очень масштабируемой. Лучше сначала почувствовать себя комфортно с простым подходом, прежде чем сделать вещи более сложными.

1 - Связывание и прослушивание
Мне кажется, что ваш код мне подходит, лично я использую:

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444));

Вместо того, чтобы идти по маршруту DNS, но я не думаю, что есть реальная проблема в любом случае.

1.5 - Принятие клиентских подключений
Просто упомянем об этом для полноты... Я предполагаю, что вы это делаете, иначе вы не дойдете до шага 2.

2 - Получение данных
Я бы сделал буфер немного длиннее 255 байт, если только вы не можете ожидать, что все сообщения вашего сервера будут содержать не более 255 байт. Я думаю, вам нужен буфер, который, вероятно, будет больше размера пакета TCP, поэтому вы можете избежать выполнения нескольких чтений для получения одного блока данных.

Я бы сказал, что выбор 1500 байт должен быть хорошим или даже 2048 для хорошего круглого номера.

В качестве альтернативы, возможно, вы можете избежать использования byte[] для хранения фрагментов данных и вместо этого сверните свой клиентский сокет на стороне сервера в NetworkStream, завернутый в BinaryReader, чтобы вы могли прочитать компоненты своего message direclty из сокета, не беспокоясь о размерах буфера.

3 - Отправка данных и определение длины данных
Ваш подход будет работать очень хорошо, но, очевидно, требует, чтобы было легко рассчитать длину пакета, прежде чем вы начнете его отправлять.

В качестве альтернативы, если ваш формат сообщения (порядок его компонентов) сконструирован таким образом, чтобы в любой момент клиент смог определить, должно ли быть больше данных, следующих (например, код 0x01 означает, что следующий будет int и строка, код 0x02 означает, что следующий будет 16 байтов и т.д. и т.д.). В сочетании с подходом NetworkStream на стороне клиента это может быть очень эффективным подходом.

Чтобы быть в безопасности, вы можете добавить подтверждение полученных компонентов, чтобы убедиться, что вы обрабатываете только правильные значения. Например, если вы получили указание на строку длиной 1 ТБ, возможно, у вас было повреждение пакетов, и может быть безопаснее закрыть соединение и заставить клиента повторно подключиться и "начать". Этот подход дает вам очень хорошее поведение при любых неожиданных сбоях.

4/5 - Закрытие клиента и сервера
Лично я бы выбрал только Close без дальнейших сообщений; когда соединение закрыто, вы получите исключение при любом блокировании чтения/записи на другом конце соединения, которое вам придется обслуживать.

Так как вы должны обслуживать "неизвестные отключения" в любом случае, чтобы получить надежное решение, сделать отсоединение более сложным, как правило, бессмысленно.

6 - Неизвестные отключения
Я бы не стал доверять даже статусу сокета... возможно, что соединение может умереть где-то по пути между клиентом/сервером, не замечая клиента или сервера.

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

В результате единственным надежным способом обнаружения всех неожиданных подключений является реализация механизма "ping", где в идеале клиент и сервер будут периодически отправлять сообщение на другой конец, что приводит только к ответному сообщению указывая, что был получен "ping".

Чтобы оптимизировать ненужные пинги, вам может понадобиться механизм "тайм-аута", который отправляет только пинг, когда другой трафик не был получен с другого конца в течение установленного промежутка времени (например, если последний сообщение с сервера больше, чем x секунд, клиент отправляет пинг, чтобы убедиться, что соединение не умерло без уведомления).

Более продвинутые
Если вам нужна высокая масштабируемость, вам придется искать асинхронные методы для всех операций сокета (Accept/Send/Receive). Это варианты "Begin/End", но они намного сложнее в использовании.

Я рекомендую не пытаться, пока у вас не будет простой версии и не будет работать.

Также обратите внимание, что если вы не планируете масштабировать больше, чем несколько десятков клиентов, это на самом деле не будет проблемой. Асинхронные методы действительно необходимы только в том случае, если вы намереваетесь масштабировать тысячи или сотни тысяч подключенных клиентов, в то время как ваш сервер не умирает напрямую.

Я, вероятно, забыл целую кучу других важных предложений, но этого должно быть достаточно, чтобы вы получили достаточно надежную и надежную реализацию, чтобы начать с

Ответ 2

1 - Связывание и прослушивание сокета

Выглядит хорошо. Ваш код будет связывать сокет только с одним IP-адресом. Если вы просто хотите прослушивать любой IP-адрес/сетевой интерфейс, используйте IPAddress.Any:

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444));

Чтобы быть в будущем доказательством, вы можете поддержать IPv6. Чтобы прослушать любой IPv6-адрес, используйте IPAddress.IPv6Any вместо IPAddress.Any.

Обратите внимание, что вы не можете прослушивать любой IPv4 и любой IPv6-адрес одновременно, за исключением случаев, когда вы используете Dual-Stack Socket. Для этого вам необходимо отключить опцию сокета IPV6_V6ONLY:

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, 0);

Чтобы включить Teredo с вашим сокетом, вам нужно установить опцию сокета PROTECTION_LEVEL_UNRESTRICTED:

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)23, 10);

2 - Получение данных

Я бы рекомендовал использовать NetworkStream, который переносит сокет в Stream вместо того, чтобы читать куски вручную.

Чтение фиксированного количества байтов немного неудобно:

using (var stream = new NetworkStream(serverSocket)) {
   var buffer = new byte[MaxMessageLength];
   while (true) {
      int type = stream.ReadByte();
      if (type == BYE) break;
      int length = stream.ReadByte();
      int offset = 0;
      do
         offset += stream.Read(buffer, offset, length - offset);
      while (offset < length);
      ProcessMessage(type, buffer, 0, length);
   }
}

Где NetworkStream действительно сияет то, что вы можете использовать его, как и любой другой Stream. Если важна безопасность, просто оберните NetworkStream в SslStream для аутентификации сервера и (необязательно) клиентов с X.509 сертификаты. Сжатие работает одинаково.

var sslStream = new SslStream(stream, false);
sslStream.AuthenticateAsServer(serverCertificate, false, SslProtocols.Tls, true);
// receive/send data SSL secured

3 - Отправка данных и указание длины данных

Ваш подход должен работать, хотя вы, вероятно, не захотите идти по пути, чтобы изобретать колесо и разработать для этого новый протокол. Посмотрите BEEP или, может быть, даже что-то простое, например protobuf.

В зависимости от ваших целей, возможно, стоит подумать о выборе абстракции над сокетами, такими как WCF или какой-либо другой механизм RPC.

4/5/6 - Закрытие и неизвестные отключения

Что jerryjvl сказал:-) Единственный надежный механизм обнаружения - это пинг или отправка keep-alives, когда соединение простаивает.

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