Начало работы с программированием сокетов в C # - Лучшие практики - PullRequest
33 голосов
/ 22 июля 2009

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

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

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 - Закрытие сервера

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

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

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

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

Ответы [ 3 ]

26 голосов
/ 22 июля 2009

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

13 голосов
/ 22 июля 2009

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 socket:

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 сказал :-) Единственный надежный механизм обнаружения - это пинг или отправка контрольных сообщений, когда соединение не используется.

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

2 голосов
/ 22 июля 2009

Рассмотрите возможность использования асинхронных сокетов. Вы можете найти больше информации по этому вопросу в

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...