.NET Interop на примере работы с сокетами

У нас есть множество технологий. Одни неимоверно быстры, другие неимоверно удобны. Одни позволяют летать со скоростью света, другие позволяют разрабатывать со скоростью света. Споры насчёт того, какой же подход лучше, утихают редко. Сейчас я покажу, как можно скрестить ежа с ужом. У нас есть .NET, которым можно быстро делать и есть Native, который может быстро делать.

В образовательных целях мы будем скрещивать эти два направления. У статьи есть ещё одна цель. В её основе лежит написанная мною и Arwyl'ом программа под названием DuSter. Эта программа представляет собой сервер-пустышку, который позволяет тестировать сетевые программы. Сервер очень прост в использовании, достаточно гибко настраивается, поддерживает файлы описания протоколов, которые позволяют более-менее автоматизировать тестирования работы любых протоколов. Я занимался разработкой сетевого уровня, мой друг — бизнес-логикой и парсингом протоколов. Получилось что-то неимоверно хорошо вылизанное и приятное. Мы гордимся своей программой, и хотим предоставить её сорцы миру, для некоммерческого использования.

Существует CLR — Common Language Runtime, среда, которая позволяет выполнять программы, написанные на языках, поддерживающих CLI (Common Language Infrastructure). Всё это дело + компиляторы и библиотеки образует .NET Framework, одну из самых распространённых сред разработки в мире. Я не буду рассказывать о том, как работают программы, написанные на .NET, поскольку эта тема достойна ещё пары статей. Скажу лишь основную вещь, необходимую для нашей статьи. Машинный код .NET и машинный код Native (Не-.NET) приложений это не одно и то же. Соответственно, выходит интересная штука: мы можем взять одно Native приложение, написанное на языке Assembler, и взять другое Native приложение, написанное я языке Pascal, и скрестить их вместе. Это достаточно просто. Нам давали такое задание в универе. Я, следуя своей любопытной натуре, решил выпендриться. Я решил скрещивать Assembler и C#. Я думал, что всё будет просто, я возьму, да и впишу в C# код ассемблера. Как же я ошибался. Естественно, узнав про то, что такое MSIL, я понял, что затея была не лучшая, но сдаваться не хотелось. Я долго искал выход из этой ситуации — и нашёл: P\Invoke через DllImport.

И так, мы имеем — программу на языке .NET, которая работает с использованием среды исполнения .NET. Задача, сделать так, чтобы среда исполнения дёргала внешние библиотеки. Что же, ещё немного усложним задачу — пусть программа позволит работать с сокетами на основе Windows Socket 2.0.

Когда у нас в институте было сетевое программирование, нас заставляли писать с использованием WS2, но мы, как заядлые шарписты воротили нос от этой библиотеки, так как в сравнении с библиотекой System.Net.Socket WS2 — это жалкая пародия на код.
Мы долго искали компромиссы с нашим преподом, и в итоге пришли к следующему: Нам позволяют использовать .NET при условии, что WS2 мы будем дёргать через DllImport.



Приступим, и сразу перейдём к разбору кода:

[DllImport("ws2_32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern Int32 accept(Int32 socketHandle, ref SocketAddres socketAddress, ref Int32 addressLength);

[DllImport("ws2_32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern Int32 bind(Int32 socketHandle, ref SocketAddres socketAddress, Int32 addressLength);

[DllImport("ws2_32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern Int32 listen(Int32 socket, Int32 queue);

[DllImport("ws2_32.dll", SetLastError = true)]
public static extern Int32 WSAStartup(Int16 wVersionRequested, ref WSADATA lpWSAData);

[DllImport("ws2_32.dll", SetLastError = true)]
public static extern String inet_ntoa(Int32 inadr);

[DllImport("ws2_32.dll", SetLastError = true)]
public static extern Int32 inet_addr(String addr);

[DllImport("ws2_32.dll", SetLastError = true)]
public static extern Int32 WSACleanup();

[DllImport("ws2_32.dll", SetLastError = true)]
public static extern Int32 WSAGetLastError();

[DllImport("ws2_32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
public static extern Int32 gethostbyname(String name);

[DllImport("ws2_32.dll", SetLastError = true)]
public static extern Int32 socket(Int32 af, Int32 type, Int32 protocol);

[DllImport("ws2_32.dll", SetLastError = true)]
public static extern Int32 closesocket(Int32 socket);


* This source code was highlighted with Source Code Highlighter.



Это код на C#, он делает простую и очевидную вещь. Обращаясь к стандартной для Windows библиотеке ws2_32.dll он импортирует указатели на вышепреведённые методы в .NET. То есть, с виду получается следующее — я позволяю своей программе использовать Native методы.

Более подробно все особенности DllImport освящены пользователем в этой статье

Основные методы уже есть в программе — осталось привести их в порядок.

Что меня всегда раздражало в библиотеке WS2 — так это способы возвращения ошибок, и чтения информации. Я очень опасливо отношусь к методам, которые возвращают значение прочитанных байт и -1 в случае ошибки. Тем более, мне не нравится после получения результата -1 делать GetLastError, чтобы понять, в чём была ошибка. Механизм раскрутки стэка исключений, который присутствует в .NET намного больше удовлетворяет моим эстетическим требованиям.


Поэтому — следующий наш шаг был таков: Привести механизм работы с сокетами на уровень .NET приложений. Что для этого надо?

Для начала — соберём все константы, которые есть в Native приложениях в enum'ы, чтобы они не болтались где не надо.

enum ADDRESS_FAMILIES : short
  {
    /// <summary>
    /// Unspecified [value = 0].
    /// </summary>
    AF_UNSPEC = 0,
    /// <summary>
    /// Local to host (pipes, portals) [value = 1].
    /// </summary>
    AF_UNIX = 1,
    ...


* This source code was highlighted with Source Code Highlighter.



Далее — методы, экспортированные нами из WS2 работают с переменными, которых не существует в среде .NET. Поэтому, необходимо было немного поизвращаться с технологией маршалинга, чтобы свести концы с концами:

[StructLayout(LayoutKind.Sequential)]
public struct SocketAddres
{
  public Int16 sin_family;
  public UInt16 sin_port;
  public Int32 sin_addr;
  
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8)]
  public String sin_zero;
}


* This source code was highlighted with Source Code Highlighter.



Эта структура позволяет вам оперировать адресом удалённого хоста.

В результате — мы имеем полный импорт библиотеки WS2 в .NET. Это круто, но мы сочли это недостаточным. Потому что пользоваться этой библиотекой жутко неудобно. Поэтому, имея под рукой WS2 методы, мы начали разрабатывать класс NSocket. Первым шагом, было создание наипростейшего класса исключений. Потому что работа с сетью проблемами полнится, и сообщать об этих проблемах необходимо разработчику. А в .NET самый лучший способ сообщить об ошибке — это кинуть Exception.

/// <summary>
/// Класс обработки исключения сокета
/// </summary>
public class NSocketException : System.Net.Sockets.SocketException

* This source code was highlighted with Source Code Highlighter.



Этот класс является простой обёрткой, которая позволяет сообщать об ошибках. Отлично, ошибки у нас уже есть, теперь надо сделать то, что бы их кидало. Для этих целей написано ещё 2 класса. NSocket и NNet. Эти классы производят основную работу с сетью. Если класс NNet больше заточен для работы с сетью, то NSocket представляет собой объектно-ориентированное представление сокета (то, чего больше всего не хватает мне в WS2).

/// <summary>
/// Принять входящее соединения на данном сокете.
/// </summary>
/// <param name="bindedSocket">Привязанный сокет, по которому надо получать соединение</param>
/// <returns>Указатель на подключившейся сокет</returns>
public static NSocket Accept(NSocket bindedSocket)
{
  WS2_NET.SocketAddres n = new WS2_NET.SocketAddres();
  Int32 toref = Marshal.SizeOf(n);
  NSocket s = new NSocket(WS2_NET.accept((Int32)bindedSocket, ref n, ref toref));
  s.Connected = true;
  return s;
}


* This source code was highlighted with Source Code Highlighter.



Вот один из методов класса Nnet, который позволяет принять входящее соединение.

/// <summary>
/// Содзание нового сокета.
/// Сокет будет автоматически создан для IPv4
/// </summary>
/// <param name="type">Тип сокета</param>
/// <param name="proto">Протокол сокета</param>
public NSocket(NSocketType type, NProtocol proto)
{
  if (this.Disposed)
  throw new InvalidOperationException("Component is disposed");
  socket = WS2_NET.socket(2, (Int32)type, (Int32)proto); // 2 = AIF_INET
  if (this.HasError)
    throw new NSocketException(WS2_NET.WSAGetLastError());
  this.Closed = false;
  this.Protocol = proto;
  this.SocketType = type;
}


* This source code was highlighted with Source Code Highlighter.



А это — конструктор класса NSocket, который инициализирует новый экземпляр сокета.

Что мы имеем на выходе?
Да, признаться честно — я не очень-то хорошо тогда разбирался в .NET, поэтому в реализации классов есть кое-какие огрехи, но в общем — мы создали с нуля работу с сокетами за 1 неделю не очень напряжённой работы. Следует учесть, что здесь мы проследили эволюционный путь от Native WS2 к .NET Objects. Действительно, задача является несколько невостребованной, потому что в .NET существуют не только отличные классы работы с сокетами, но и классы, реализующие серверы и клиенты популярных протоколов. Я уже не говорю о такой штуке, как WCF — одного из столпов .NET 3.0, который позволяет связывать программы по сети, не требуя знания о сокетах. Но! Для целей этих гигантов, которые избавляют сетевых программистов от проблем, трудятся забытые всеми WS2. Изучить их было бы неплохо, чисто для понимания.

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

В итоге, работа с сокетами превратилась в песню:

try
{
  NetModule.NNet.StartWS();
  if (CurrExemp.SocketProtocol == NetModule.NProtocol.Tcp)
    CurrExemp.Socket = new NetModule.NSocket(NetModule.NSocketType.Stream, CurrExemp.SocketProtocol);
  else
    CurrExemp.Socket = new NetModule.NSocket(NetModule.NSocketType.Datagram, CurrExemp.SocketProtocol);
  CurrExemp.Socket.Bind(CurrExemp.Port);
  if (CurrExemp.SocketProtocol == NetModule.NProtocol.Tcp)
    CurrExemp.Socket.Listen();
}
catch
{
  if (CurrExemp.ClientSocket != null && CurrExemp.ClientSocket.Connected)
    CurrExemp.ClientSocket.Close();
  if (CurrExemp.Socket != null && CurrExemp.Socket.Binded)
    CurrExemp.Socket.Close();
  if (NetModule.NNet.Started)
    NetModule.NNet.StopWS();
  MessageBox.Show("Can't create socket on specified port!");
  return;
}


* This source code was highlighted with Source Code Highlighter.



Данный код показывает, как просто запустить новый сервер, для протоколов TCP или UDP. Нет необходимости проводить множество проверок, которые нужны для WS2.

Используя эту информацию этой статьи, вы можете начать создавать свои приложения, которые, например, будут использовать библиотеку OpenGL. Хотя, нет, не стоит, такая библиотека уже существует.

Ну, можно попытаться ускорить ваше приложение, используя вычисления на очень сложном и быстром Assembler'е. Могу даже сказать, как это просто сделать. MASM32 Тут вы найдёте восхитительный пакет для работы на асме под Windows. Он позволяет вам экспортировать ваш код на ассемблере в виде стандартных библиотек, которые вы можете подключить в свои .NET приложения.

Ещё можно написать программу для взаимодействия с COM портами или USB интерфейсами, с ядром на С/С++ а лицом на C#. Я думаю, многие согласятся, что программировать интерфейсы на C# намного удобнее, чем на чистом С.

А, ну и самое главное: Все исходные коды работы с сокетами, бизнес-логики XML и сам сервер для тестирования сетевых приложений я пока разместил здесь

Ну вот, это и есть мой рассказ о том, как можно скрестить ежа с ужом.

Автор материала: Nurked



Опубликовал admin
12 Авг, Четверг 2010г.



Программирование для чайников.