Разработка клиент-серверных приложений в .Net

advertisement
Вычислительные системы,
сети и
телекоммуникации
Часть 1
2013
Учебно-методические материалы для лабораторных занятий
по курсу
«Вычислительные системы, сети и
телекоммуникации»
Содержание
СОДЕРЖАНИЕ .................................................................................................... 2
ПРЕДИСЛОВИЕ .................................................................................................. 4
РАЗРАБОТКА КЛИЕНТ-СЕРВЕРНЫХ ПРИЛОЖЕНИЙ В .NET............. 6
РАБОТА С ПОТОКАМИ ДАННЫХ ............................................................................ 6
Синхронный ввод/вывод .................................................................................. 6
Асинхронный ввод/вывод ................................................................................ 7
Основные классы для работы с потоками ................................................... 7
Члены класса Stream........................................................................................ 8
Работа с объектом FileStream ...................................................................... 9
Работа с объектом MemoryStream ............................................................... 9
Класс CryptoStream........................................................................................ 10
Reader-ы и Writer-ы ....................................................................................... 10
Наиболее важные члены базового класса TextWriter ................................ 11
Наиболее важные члены класса TextReader ............................................... 11
Работа с двоичными данными (классы BinaryReader и BinaryWriter) .... 12
Наиболее важные члены класса BinaryWriter ............................................ 12
Наиболее важные члены класса BinaryReader ........................................... 13
Пример ............................................................................................................ 13
Задание для самостоятельной работы ...................................................... 14
СЕРИАЛИЗАЦИЯ .................................................................................................. 15
Сериализация и десериализация в формат XML ........................................ 15
Сериализация с помощью объектов форматирования ............................ 18
Лирическое отступление. Графы для отношений объектов ................... 19
Выбираем объект Formatter ........................................................................ 20
Сериализация в двоичном формате ............................................................ 21
Сериализация и десериализация в формате SOAP .................................... 21
Задание для самостоятельной работы ...................................................... 22
СЕТЕВОЕ ПРОГРАММИРОВАНИЕ В .NET ............................................................. 23
Сокеты ........................................................................................................... 23
Порты............................................................................................................. 23
Классы для работы с сокетами в .NET ...................................................... 24
Класс System.Net.Sockets.Socket .................................................................... 25
Создание сервера ........................................................................................... 26
Создание клиента .......................................................................................... 27
2
Вычислительные системы, сети и телекоммуникации
Классы TcpListener и TcpClient .................................................................... 27
Класс TcpClient .............................................................................................. 28
Задание для самостоятельной работы ...................................................... 29
МНОГОПОТОЧНОСТЬ........................................................................................... 30
Пространство имен System.Threading ........................................................ 31
Работа с классом Thread.............................................................................. 32
Обычные члены класса Thread ..................................................................... 32
Пример ............................................................................................................ 33
Многопотоковый сервер ............................................................................... 36
Что изменится на сервере ........................................................................... 37
Что изменится на клиенте .......................................................................... 38
Задание для самостоятельной работы ...................................................... 38
Потоки на клиентской стороне клиент-серверных приложений ........... 39
Проблемы с доступом между потоками ................................................... 40
БАЗЫ ДАННЫХ В .NET ........................................................................................ 42
Управляемый провайдер OLE DB ................................................................ 42
Наиболее важные типы пространства имен System.Data.OleDb ........... 42
Установление соединения при помощи типа OleDbConnection .............. 43
Наиболее часто используемые провайдеры OLE DB: ............................... 43
Члены класса OleDbConnection .................................................................... 44
Построение команды SQL............................................................................ 44
Члены класса OleDbCommand ...................................................................... 45
Работа с OleDbDataReader .......................................................................... 46
Подключение к базе данных Access ............................................................. 46
НЕКОТОРЫЕ ПРОБЛЕМЫ И СПОСОБЫ ИХ РЕШЕНИЯ............................................. 47
ЛИТЕРАТУРА ..................................................................................................... 49
3
Предисловие
Данное учебное пособие разрабатывается
для поддержки компьютерных лабораторных
занятий и самостоятельной работы студентов по
курсу «Вычислительные системы, сети и
телекоммуникации».
В рамках данного курса каждый студент разрабатывает отдельный
проект, представляющий собой клиент-серверное приложение.
Результат работы оценивается в диапазоне от 0 до 50 баллов.
Обязательные требования к проекту (25 баллов):
 наличие серверного модуля в виде консольного или оконного
приложения (10 баллов);
 наличие клиентского модуля в виде оконного приложения (10
баллов);
 совместное функционирование двух вышеуказанных модулей,
заключающееся в обмене сообщениями между ними (5 баллов).
Обратите внимание, что для допуска к зачету выполнения
обязательных требований недостаточно, следует добавить к проекту
какие-либо (можно все) функциональные возможности из
дополнительного списка.
Дополнительные требования к проекту:
 применение сериализации и десериализации для обмена
информацией между клиентом и сервером (5 баллов);
 разработка специального класса (или нескольких классов) для
обмена информацией между клиентом и сервером (5 баллов);
 применение многопотоковости на сервере (5 баллов) и/или на
клиенте (5 баллов);
 использование базы данных в серверном приложении (5 баллов).
Бонус:
 применение технологий симметричного и/или асимметричного
шифрования и/или цифровой подписи при передаче данных от
клиента к серверу и/или от сервера клиенту (10 баллов).
Примерные темы проектов:
1. чат (система обмена текстовыми сообщениями) с авторизацией
участников и, возможно, шифрованием всех сообщений;
4
Вычислительные системы, сети и телекоммуникации
2. система для идентификации сотрудников по личным
идентификаторам;
3. система для поиска информации о товаре по штрих-коду;
4. платежный терминал для проведения операций с банковским
счётом;
5. корпоративная система обмена сообщениями, в которой не
требуется секретности, но обязательно обеспечить целостность
сообщений с помощью технологии цифровой подписи.
Следует отметить, что в клиентском приложении предполагается
только ввод данных пользователем через текстовые поля и другие
элементы интерфейса – флажки, радиокнопки, списки и т.п.
Считывание штрих-кодов, отпечатков пальцев и других нестандартных
данных требует специальных периферийных устройств и не
рассматривается в нашем курсе .
Предполагается, что читатель:
 успешно изучил язык C# как основу технологии .Net в
рамках курса «Объектно-ориентированное программирование» и имеет опыт разработки консольных и оконных
.Net-приложений;
 успешно изучил курс «Базы данных» и имеет
представление о проектировании баз данных, языке SQL и
СУБД SQL server.
Наше пособие разбито на две части. Первая часть «Разработка
клиент-серверных приложений в .Net» содержит всю необходимую
информацию для разработки основы клиент-серверного приложения, а
вторая часть «Криптографические возможности» содержит описания
технологий и примеры применения криптографических классов в .Net.
5
Разработка клиент-серверных приложений в .Net
Работа с потоками данных
Поскольку для обмена информацией в
клиент-серверных приложениях, разработанных на платформе .Net, широко применяются
потоки, вспомним, что это такое.
Поток
(stream)
это
абстрактное
представление
последовательного устройства, для которого сохранение и считывание
данных выполняется побайтно. Базовым устройством для потока
может быть, например, файл, принтер или сетевой сокет. Через эту
абстракцию можно из одного и того же процесса обращаться к разным
устройствам, и аналогичный программный код может использоваться
для чтения данных из файлового и сетевого входных потоков. Тем
самым программисту не требуется беспокоиться о реальном
физическом механизме такого устройства.
Синхронный ввод/вывод
По умолчанию все операции с потоками выполняются
синхронно, и это простейший способ для операций ввода/вывода.
Недостаток синхронного ввода/вывода состоит в том, что обработка
блокируется до завершения операции ввода/вывода и лишь затем
приложению разрешается продолжить обработку.
Синхронный ввод/вывод бывает полезен при небольших
размерах файлов, но для больших файлов из-за блокирования
выполнения текущего потока производительность приложения может
оказаться слишком низкой.
Синхронный ввод/вывод не подходит для выполнения операций
в сети, где слабо влияние на время, необходимое для завершения
операции. Следовательно, синхронный ввод/вывод был бы неудачным
выбором для передачи больших потоков через сеть с низкой
пропускной способностью или скоростью.
Вводя многопоточную обработку (threading) в синхронные
методы, можно имитировать асинхронный ввод/вывод.
6
Вычислительные системы, сети и телекоммуникации
Асинхронный ввод/вывод
При асинхронном вводе/выводе до завершения операции
ввода/вывода могут выполняться другие задачи. Когда операция
ввода/вывода завершается, операционная система уведомляет об этом
вызывающую программу.
Следовательно, для асинхронного ввода/вывода требуется
отдельный механизм уведомления.
Этот метод полезен, когда одновременно с передачей больших
объемов данных из потока приложению требуется продолжать
выполнение других задач или работать с медленными устройствами,
чья скорость могла бы в противном случае замедлить работу
приложения.
При асинхронном вводе/выводе для каждого запроса
ввода/вывода создается отдельный поток выполнения, и это может
привести к повышению накладных расходов для операционной
системы.
Основные классы для работы с потоками
Большинство классов, связанных с вводом-выводом информации,
находятся в пространстве имен System.IO.
Все классы, производные от базового класса Stream,
предназначены для работы с блоками двоичных данных.
7
Члены класса Stream
CanRead
CanSeek
CanWrite
Определяют, будет ли данный поток поддерживать
чтение, поиск и (или) запись
Close()
Закрывает текущий поток и освобождает связанные с
ним ресурсы (сокеты, указатели на файлы и т. п.)
Flush()
Записывает данные из буфера в связанный с потоком
источник данных и очищает буфер. Если для данного
потока буфер не используется, то этот метод ничего
не делает
Length
Возвращает длину потока в байтах
Position
Определяет указатель на местонахождение (позицию)
в текущем потоке
Read()
ReadByte()
Считывают последовательность байтов (или
единственный байт) в текущем потоке и перемещают
указатель в потоке на количество считанных байтов
Seek()
Устанавливает указатель на местонахождение
(позицию) в текущем потоке
SetLength()
Устанавливает длину текущего потока
Write()
WriteByte()
Записывают последовательность байтов (или
единственный байт) в текущий поток и перемещают
указатель в потоке на количество записанных байтов
8
Вычислительные системы, сети и телекоммуникации
Работа с объектом FileStream
Класс FileStream предназначен для работы с внешними
носителями – информация пишется и читается на/с винчестера,
дискеты или флеш-накопителя. Рассмотрим пример записи/чтения
целых чисел. После создания файла можете открыть его в текстовом
редакторе и посмотреть, что получилось. Объясните результат.
// Создаем файл в текущем каталоге
FileStream myFStream = new FileStream("test.dat",
FileMode.OpenOrCreate, FileAccess.ReadWrite);
// Записываем байты в файл *.dat
for (int i = 0; i < 256; i++)
{ myFStream.WriteByte((byte)i); }
// Переставляем внутренний указатель на начало
myFStream.Position = 0;
// Считываем байты из файла *.dat
for (int i = 0; i < 256; i++)
{ Console.Write(myFStream.ReadByte()); }
myFStream.Close();
Работа с объектом MemoryStream
Основой для потоков может служить не только внешнее
устройство, но и оперативная память. Разумеется, для долгосрочного
хранения информации такой подход не годится.
Обратите внимание, как в этом примере происходит
преобразование строки в массив байтов и обратно.
// Создаем пустой поток в памяти
MemoryStream ms = new MemoryStream();
byte[] memData = Encoding.ASCII.GetBytes
("This will go in Memory!");
// Записываем данные в память
ms.Write(memData, 0, memData.Length);
// Устанавливаем указатель на начало
ms.Position=0;
byte[] inData = new byte[100];
// Читаем из памяти и выводим на экран
ms.Read(inData, 0, 100);
Console.WriteLine(Encoding.ASCII.GetString(inData));
// А также переписываем содержимое памяти в файл
Stream strm =new FileStream("Memoutput.txt",
FileMode.OpenOrCreate, FileAccess.Write);
ms.WriteTo(strm);
9
Класс CryptoStream
В некоторых случаях защита данных при передаче и хранении
является очень важным требованием.
Для защиты данных, как правило, применяется их шифрование
секретным либо публичным ключом. В зависимости от алгоритма для
расшифровки может использоваться тот же секретный ключ, что и для
шифрования (при симметричном шифровании), или другой ключ (при
асимметричном шифровании).
Платформа
.NET
предоставляет
класс
CryptoStream,
связывающий потоки с криптографическими преобразованиями. Хотя
CryptoStream не принадлежит на самом деле пространству имен
System.IO, он все же порожден от класса Stream.
Класс CryptoStream может использоваться для выполнения
криптографических операций на объекте Stream.
В
нашем
распоряжении
имеются
самые
разные
криптографические преобразования - можно использовать любой
провайдер
службы
криптографии,
реализующий
интерфейс
ICryptoTransform.
Reader-ы и Writer-ы
Классы StreamReader и StreamWriter пригодятся нам в тех
ситуациях, когда необходимо считать или записать символьные
данные (данные в формате string). По умолчанию оба этих типа
работают с кодировкой Unicode. Классы StringReader и StringWriter
выполняют те же функции, но запись и чтение ведётся в символьную
строку (в оперативной памяти).
10
Вычислительные системы, сети и телекоммуникации
Наиболее важные члены базового класса TextWriter
Close()
Flush()
NewLine
Write()
WriteLine()
Закрывает соответствующий объект Writer и
освобождает связанные с ним ресурсы. Если в
процессе записи используется буфер, он будет
автоматически очищен
Очищает все буферы для текущего объекта Writer и
записывает накопленные в них данные в место
постоянного их хранения, но при этом сам объект
Writer не закрывается
Используется для определения последовательности
символов, означающих начало новой строки. По
умолчанию используется последовательность “возврат
каретки” — “перевод строки” (\r\n)
Записывает новый отрезок текста в поток без
применения последовательности начала новой строки
Записывает новую строку в поток (с применением
последовательности начала новой строки)
Наиболее важные члены класса TextReader
Возвращает следующий символ, не изменяя позицию
указателя в файле
Считывает данные из потока на входе
Read()
ReadBlock() Считывает указанное пользователем количество
символов, начиная с определенной позиции, и
записывает считанные данные в буфер
ReadLine() Считывает строку данных из текущего потока и
возвращает ее как значение типа string. Пустая строка
(null string) означает конец файла (EOF)
ReadToEnd() Считывает все символы, начиная с текущей позиции и
до конца потока, и возвращает считанные данные как
единое значение типа string
Peek()
В следующем примере мы записываем в файл строки, введенные
с консоли, а затем читаем содержимое файла и выводим на экран.
11
Обратите внимание, что открытие файла происходит с помощью
статического метода класса File.
string mythought;
// создаем файл StreamWriter и заполняем его умными мыслями c
консоли:
StreamWriter sw=File.CreateText("Thoughts.txt");
while ((mythought = Console.ReadLine()) != "End")
{
sw.WriteLine(mythought);
}
sw.Close();
// выводим информацию из файла на консоль при помощи
// StreamReader
Console.WriteLine("Here are your thoughts:\n");
StreamReader sr = File.OpenText("Thoughts.txt");
string input = null;
while ((input = sr.ReadLine()) !=null)
{
Console.WriteLine(input);
}
sr.Close();
Работа с двоичными данными (классы
BinaryReader и BinaryWriter)
Эти классы позволяют считывать и записывать определенные
двоичные типы данных в поток. Класс BinaryWriter определяет
многократно перегруженный метод Write() для помещения в поток
объектов самых разных типов данных.
Наиболее важные члены класса BinaryWriter
BaseStream
12
Представляет поток, с которым работает объект
Вычислительные системы, сети и телекоммуникации
BinaryWriter
Close()
Закрывает поток
Flush()
Очищает буфер
Seek()
Устанавливает позицию в текущем потоке
Write()
Записывает значение в текущий поток
Наиболее важные члены класса BinaryReader
BaseStream
Представляет поток, с которым работает объект
BinaryReader
Close()
Закрывает объект BinaryReader
PeekChar()
Возвращает следующий символ без перемещения
внутреннего указателя в потоке
Read()
Считывает поток байтов или
символов
и
сохраняет в массиве (передаваемом как входящий
параметр)
ReadXXXX()
Считывает данные определенного типа из потока
(например, ReadBoolean(), ReadByte(), Readlnt32()
и т. д.)
Пример
Console.WriteLine("Creating a file and writing binary
data...");
FileStream myFStream = new FileStream("temp.dat",
FileMode.OpenOrCreate,
FileAccess.ReadWrite);
// Записываем двоичные данные
BinaryWriter binWrit = new BinaryWriter(myFStream);
binWrit.Write("Hello as binary info...");
int myInt = 99;
float myFloat = 9984.82343F;
bool myBool = false;
char[] myCharArray = {'H', 'e', 'l', 'l', 'o'};
binWrit.Write(myInt);
binWrit.Write(myFloat);
binWrit.Write(myBool);
binWrit.Write(myCharArray);
13
// Устанавливаем внутренний указатель на начало
binWrit.BaseStream.Position = 0;
// Считываем двоичную информацию как поток байтов
Console.WriteLine("Reading binary data...");
BinaryReader binRead = new BinaryReader(myFStream);
int temp = 0;
while(binRead.PeekChar()!=-1)
{
Console.Write(binRead.ReadByte());
temp = temp + 1;
if(temp == 5)
{
// Добавляем пустую строку через каждые 5 байтов
temp = 0;
Console.WriteLine();
}
}
// Все закрываем
binWrit.Close();
binRead.Close();
myFStream.Close();
Console.ReadKey();
Задание для самостоятельной работы
Пусть текстовый файл содержит пары «логин-пароль». Напишите
программу, которая по заданному логину находит и печатает пароль
или сообщение о том, что логин не найден.
14
Вычислительные системы, сети и телекоммуникации
Сериализация
Для передачи информации между сервером
и клиентом очень удобно применять механизм
сериализации.
Сериализацией
(serialization)
обычно
называют процесс преобразования объекта в
линейную последовательность байтов. Обратное
действие, при котором из потока байтов объект восстанавливается в
исходном виде, называется десериализацией (deserialization).
Сериализация имеет следующие «плюсы»:
 Доступность. Объект можно сохранить в файле и
обращаться к нему в любое время.
 Время жизни. Сохранение состояния объекта в файл
продлевает ему жизнь. Обычно объекты просто хранятся в
оперативной памяти компьютера и автоматически уничтожаются по
окончании работы программы.
 Использование в сетевых приложениях. Объекты
сложной формы преобразуются в формат, который легко передается
через сеть.
 Надежность. Сохраненный объект можно воссоздать в его
первоначальной форме.
Сериализация и десериализация в формат XML
Для сериализации объектов часто используется формат XML.
Этот формат удобен как для автоматической передачи информации
между программами, так и для чтения человеком. Сериализованные
объекты в виде текста можно переслать по сети или сохранить на диск
для дальнейшего использования.
Класс XmlSerializer содержится в пространстве имен
System.Xml.Serialization.
Для того чтобы объект некоторого класса можно было
сериализовать с использованием класса XmlSerializer, нужно
выполнить следующие требования:
15

Класс, подлежащий сериализации, должен содержать
используемый по умолчанию открытый конструктор без
параметров. Это условие возникло потому, что при восстановлении
объекта в процессе десериализации сначала объект создается
конструктором по умолчанию, а затем из входного потока данных
читаются открытые свойства. Если конструктор по умолчанию
отсутствует, .NET Framework не будет знать, как создать объект.

При сериализации сохраняются только открытые
свойства, поддерживающие операции get и set, и открытые члены
данных. Это объясняется тем, что процесс сериализации не может
обращаться к закрытым и доступным только для чтения элементам
данных.
Предположим, у нас есть класс Message.
public class Message
{
public string from;
public string to;
public string text;
public int priority;
public Message() {}
public Message (string txt_from, string txt_to,
string txt_text, int num_priority)
{
from = txt_from;
to = txt_to;
text = txt_text;
priority=num_priority;
}
}
Создадим объект этого класса и сериализуем его.
. . .
Message mes1 = new Message
("Юстас", "Алекс", "Вторник. Новостей нет.", 1);
// создаем поток для записи
StreamWriter writer = new StreamWriter("Message.xml");
// создаем сериализатор
XmlSerializer serializer = new XmlSerializer(typeof(Message));
// и сериализуем объект
serializer.Serialize(writer, mes1);
writer.Close();
. . .
Получим файл Message.xml со следующим содержимым:
<?xml version="1.0" encoding="utf-8"?>
16
Вычислительные системы, сети и телекоммуникации
<Message xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<from>Юстас</from>
<to>Алекс</to>
<text>Вторник. Новостей нет.</text>
<priority>1</priority>
</Message>
Теперь выполним обратное действие:
. . .
// создаем поток для чтения
FileStream reader= new FileStream("Message.xml", FileMode.Open,
FileAccess.Read);
// создаем десериализатор
XmlSerializer deserializer = new
XmlSerializer(typeof(Message));
// и десериализуем объект
Message mes2 = (Message)deserializer.Deserialize(reader);
Console.Write("Сообщение от: "+mes2.from);
Console.WriteLine(" к: " + mes2.to);
Console.WriteLine("текст: " + mes2.text);
Console.WriteLine("приоритет: " + mes2.priority);
. . .
Усложним структуру класса Message:
public class Message
{
public Person from;
public Person to;
public String text;
public int priority;
public Message() {}
public Message(Person person_from, Person person_to,
string txt_text, int num_priority)
{
from = person_from;
to = person_to;
text = txt_text;
priority=num_priority;
}
}
public class Person
{
public string name;
public string position;
public Person() { }
public Person(string txt_name, string txt_position)
{
name = txt_name;
position = txt_position;
}
17
}
Сериализованный объект будет выглядеть примерно следующим
образом:
<?xml version="1.0" encoding="utf-8"?>
<Message xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<from>
<name>Юстас</name>
<position>шпион</position>
</from>
<to>
<name>Алекс</name>
<position>разведчик</position>
</to>
<text>Вторник. Новостей нет.</text>
<priority>1</priority>
</Message>
Сериализация с помощью объектов форматирования
Для сериализации объектов сложной структуры (например,
содержащих вложенные объекты, в том числе и других классов)
удобно применять так называемые объекты форматирования, или
форматтеры. При использовании этой технологии каждый класс,
который будет участвовать в сериализации, должен обладать
атрибутом [Serializable]. Те элементы объекта, которые сериализовать
не нужно, следует пометить атрибутом [NonSerialized].
// Классы Message и Person могут быть сериализованы
[Serializable]
public class Person
{
public string name;
public string position;
public Person(string txt_name, string txt_position)
{
name = txt_name;
position = txt_position;
}
}
[Serializable]
public class Message
{
public Person from;
18
Вычислительные системы, сети и телекоммуникации
public Person to;
public String text;
public int priority;
[NonSerialized]
public string color;
public Message(Person person_from, Person person_to,
string txt_text, int num_priority, string txt_color)
{
from = person_from;
to = person_to;
text = txt_text;
priority=num_priority;
color = txt_color;
}
}
Лирическое отступление. Графы для отношений объектов
Службы сериализации в .NET — это довольно сложные
программные модули. С их помощью выполняются многие
неочевидные действия: когда объект сериализуется в поток,
информация обо всех других объектах, на которые он так или иначе
ссылается, также должна сериализоваться. Например, когда
сериализуется объект производного класса, ссылки на другие классы,
которые есть в базовых классах для этого производного класса, также
должны отслеживаться и сохраняться.
19
Набор взаимосвязанных объектов, сериализованных в поток
байтов, называется графом объектов (object graph). Графы позволяют
фиксировать отношения объектов друг к другу, и они, вообще говоря,
не соответствуют классическим моделям отношений классов в
объектно-ориентированном программировании.
Внутри объектного графа каждому из объектов присваивается
уникальный номер, который используется только для служебных
целей в графе и которому совершенно не обязательно должно что-то
соответствовать в реальном мире. Далее записывается информация о
соответствии имени класса этому номеру, информация обо всех
отношениях этого класса с другими классами и отношениях других
классов между собой.
Выбираем объект Formatter
Пространство имен System.Runtime.Serialization.Formatters
включает в себя еще два пространства имен - *.Binary и *.Soap,
каждому из которых соответствует один из двух классов Formatter.
 Класс ВinаryFormatter сериализует объектный граф в
компактном потоке двоичного формата,
 а класс SoapFormatter представляет граф как текстовое
сообщение протокола SOAP (Simple Object Access Protocol — это
простой протокол доступа к объектам) в формате XML.
Класс BinaryFormatter находится в базовой библиотеке
mscorlib.dll, поэтому единственное, что нам потребуется для
сериализации при помощи такого объекта — указать использование
этого пространства имен:
// Для сериализации объектов в двоичном формате
using System.Runtime.Serialization.Formatters.Binary;
Класс SoapFormatter находится в отдельной сборке, поэтому для
сохранения объекта в формате SOAP нам потребуется добавить ссылку
на сборку System.Runtime.Serializaton.Formatters.Soap.dll (в пункте
Add Reference текущего проекта), а затем использовать аналогичную
команду:
// Для сериализации объектов в формате SOAP
using System.Runtime.Serialization.Formatters.Soap;
20
Вычислительные системы, сети и телекоммуникации
Сериализация в двоичном формате
using System.Runtime.Serialization.Formatters.Binary;
. . .
Message mes1 = new Message(
new Person("Юстас","шпион"),
new Person("Алекс","разведчик"),
"Вторник. Новостей нет.", 1, "red");
// Создаем поток для записи
FileStream myStream = File.Create("Message.dat");
// Создаем форматтер
BinaryFormatter myBinaryFormat = new BinaryFormatter();
// Помещаем объектный граф в поток в двоичном формате
myBinaryFormat.Serialize(myStream, mes1);
// Закрываем поток
myStream.Close();
Сериализация и десериализация в формате SOAP
using System.Runtime.Serialization.Formatters.Soap;
. . .
Message mes1 = new Message(
new Person("Юстас","шпион"),
new Person("Алекс","разведчик"),
"Вторник. Новостей нет.", 1, "red");
// Создаем поток для записи
FileStream myStream = File.Create("Message2.xml");
// Создаем форматтер
SoapFormatter myXMLFormat = new SoapFormatter();
// Помещаем объектный граф в поток в SOAP формате
myXMLFormat.Serialize(myStream, mes1);
// Закрываем поток
myStream.Close();
. . .
// Восстанавливаем объект из файла SOAP
myStream = File.OpenRead("Message2.xml");
Message mes2 = (Message)myXMLFormat.Deserialize(myStream);
Console.Write("Сообщение от: " + mes2.from.name +","
+mes2.from.position);
Console.WriteLine(" к: " + mes2.to.name + "," +
mes2.to.position);
Console.WriteLine("текст: " + mes2.text);
Console.WriteLine("приоритет: " + mes2.priority);
myStream.Close();
. . .
В результате получим такой файл:
21
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchemainstance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0"
SOAPENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:Message id="ref-1"
xmlns:a1="http://schemas.microsoft.com/clr/nsassem/ConsoleAppli
cation3/ConsoleApplication3%2C%20Version%3D1.0.0.0%2C%20Culture
%3Dneutral%2C%20PublicKeyToken%3Dnull">
<from href="#ref-3"/>
<to href="#ref-4"/>
<text id="ref-5">Вторник. Новостей нет.</text>
<priority>1</priority>
</a1:Message>
<a1:Person id="ref-3"
xmlns:a1="http://schemas.microsoft.com/clr/nsassem/ConsoleAppli
cation3/ConsoleApplication3%2C%20Version%3D1.0.0.0%2C%20Culture
%3Dneutral%2C%20PublicKeyToken%3Dnull">
<name id="ref-6">Юстас</name>
<position id="ref-7">шпион</position>
</a1:Person>
<a1:Person id="ref-4"
xmlns:a1="http://schemas.microsoft.com/clr/nsassem/ConsoleAppli
cation3/ConsoleApplication3%2C%20Version%3D1.0.0.0%2C%20Culture
%3Dneutral%2C%20PublicKeyToken%3Dnull">
<name id="ref-8">Алекс</name>
<position id="ref-9">разведчик</position>
</a1:Person>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Задание для самостоятельной работы
Напишите программу, которая сериализует и десериализует
произвольный объект в файл. Используйте обычный формат XML и
формат SOAP. Просмотрите полученные XML-файлы в любом
текстовом редакторе или браузере.
22
Вычислительные системы, сети и телекоммуникации
Сетевое программирование в .NET
Наконец, мы приступаем к раcсмотрению
базовых технологий для разработки клиентсерверных приложений.
Сокеты
Сокет - это один конец двустороннего канала связи между
двумя программами, работающими в сети. Соединяя вместе два
сокета, можно передавать данные между разными процессами
(локальными или удаленными). Реализация сокетов обеспечивает
инкапсуляцию протоколов сетевого и транспортного уровней.
Существуют два основных типа сокетов - потоковые сокеты и
дейтаграммные.
Потоковый сокет – это сокет с установленным соединением,
состоящий из потока байтов, который может быть двунаправленным,
т.е. через эту конечную точку приложение может и передавать, и
получать данные. Потоковый сокет гарантирует исправление ошибок,
обрабатывает доставку и сохраняет последовательность данных. На
него можно положиться в доставке упорядоченных, недублированных
данных.
Потоки базируются на явных соединениях: сокет А запрашивает
соединение с сокетом В, а сокет В либо соглашается с запросом на
установление соединения, либо отвергает его.
Порты
Понятие порта определено, чтобы разрешить задачу
одновременного взаимодействия с несколькими приложениями. По
существу, с его помощью расширяется понятие IP-адреса. Компьютер,
на котором в одно и то же время выполняется несколько приложений,
получая пакет из сети, может идентифицировать целевой процесс,
пользуясь уникальным номером порта, определенным при
установлении соединения.
Сокет состоит из IP-адреса машины и номера порта,
используемого приложением ТСР. Поскольку IP-адрес уникален в
Интернете, а номера портов уникальны на отдельной машине, номера
сокетов также уникальны во всем Интернете.
23
Эта характеристика позволяет процессу общаться через сеть с
другим процессом исключительно на основании номера сокета.
За определенными службами номера портов зарезервированы (80
- Apache, 3306 - mySQL, 8080 и т.п.)
Классы для работы с сокетами в .NET
MulticastOption
Устанавливает
значение
IP
aдpeca
для
присоединения к IР-rруппе или для выхода из нее.
TcpClient
строится на классе Socket, чтобы обеспечить ТСР
обслуживание на более высоком уровне. TcpClient
предоставляет несколько методов для отправки и
получения данных через сеть.
TcpListener
также построен на низкоуровневом классе Socket.
Ero основное назначение - серверные приложения.
Он ожидает входящие запросы на соединения от
клиентов и уведомляет приложение о любых
соединениях.
реализует базовый класс потока, из котоpoгo
данные отправляются и в котором они
получаются. Это абстракция высокого уровня,
представляющая соединение с каналом связи
TCP/IP.
NetworkStream
UdpClient
UDP - это протокол, не организующий
соединение, следовательно, для реализации UDРобслуживания
в
.NEТ
требуется
друrая
функциональность. Класс UdpClient предназначен
для реализации UDР обслуживания.
SocketException
это исключение порождается, когда в сокете
возникает ошибка.
Socket
обеспечивает
базовую
приложения сокета.
24
функциональность
Вычислительные системы, сети и телекоммуникации
Класс System.Net.Sockets.Socket
Свойство
Описание
AddressFamlly
Возвращает семейство адресов сокета - значение
из перечисления Socket.AddressFamily.
Available
Возвращает объем доступных для чтения данных.
Blocking
Возвращает или устанавливает значение,
показывающее, находится ли сокет в
блокирующем режиме.
Connected
Возвращает значение, информирующее, соединен
ли сокет с удаленным хостом.
LocalEndPoint
Возвращает локальную конечную точку
ProtocolType
Возвращает тип протокола сокета.
RemoteEndPoint
Возвращает удаленную конечную точку сокета.
SocketType
Возвращает тип сокета
Метод
Описание
Accept()
Создает новый сокет для обработки входящего
запроса на соединение
Bind()
Связывает сокет с локальной конечной точкой для
ожидания входящих запросов на соединение
Close()
Закрывает сокет
25
Connect()
Устанавливает соединение с удаленным хостом
Listen()
Помещает сокет в режим прослушивания
Receive()
Получает данные от соединенного сокета
Poll()
Определяет статус сокета
Send()
Отправляет данные соединенному сокету
ShutDown()
Запрещает операции отправки и получения
данных на сокете
Создание сервера
Не забудем подключить System.Net.Sockets и System.Net !
Для корректной пересылки русских букв или символов других
национальных кодировок при преобразовании строки в массив байтов
(и обратно) удобно использовать класс Encoding.UTF8.
// создаем конечную точку подключения
IPHostEntry ipHost = Dns.GetHostEntry("localhost");
IPAddress ipAddr = ipHost.AddressList[0];
// В новых версиях Framework следует использовать
// IPAddress ipAddr = ipHost.AddressList[1];
IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 11000);
// создаем сокет
Socket sListener = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
// и связываем его с точкой подключения
sListener.Bind(ipEndPoint);
// запускаем прослушивание
sListener.Listen(10);
while (true)
{
Console.WriteLine("Start new client");
Socket handler = sListener.Accept();
// подключился клиент
string data = null;
byte[] bytes = new byte[1024];
int bytesRec = handler.Receive(bytes);
// принимаем от него сообщение
data = Encoding.UTF8.GetString(bytes, 0, bytesRec);
26
Вычислительные системы, сети и телекоммуникации
Console.WriteLine(data);
string theReply = "Спасибо!";
byte[] msg = Encoding.UTF8.GetBytes(theReply);
handler.Send(msg);
// и отправляем ответ
handler.Shutdown(SocketShutdown.Both);
// завершаем сеанс
handler.Close();
}
Создание клиента
byte[] bytes = new byte[1024];
// таким же образом создаем конечную точку подключения
IPHostEntry ipHost = Dns.GetHostEntry("127.0.0.1");
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 11000);
// создаем сокет
Socket sender = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
// устанавливаем соединение
sender.Connect(ipEndPoint);
// отправляем сообщение
string message = "Привет!";
int bytesSend =
sender.Send(Encoding.UTF8.GetBytes(message));
// получаем ответ от сервера
int bytesRec = sender.Receive(bytes);
Console.WriteLine("Server:" +
Encoding.UTF8.GetString(bytes, 0, bytesRec));
// завершаем работу
sender.Shutdown(SocketShutdown.Both);
sender.Close();
Console.ReadKey();
Не забудьте в приложениях обработать исключения! Подсказка: чтобы
определить, где и какие исключения следует обработать, создавайте
нарочно ошибочные ситуации. Например, можно запустить клиент без
сервера и посмотреть, что получится.
Классы TcpListener и TcpClient
В отличие от класса Socket, в котором для отправки и получения
данных применяется побайтовый подход, классы TcpClient и
TcpListener придерживаются потоковой модели.
27
В этих классах все взаимодействие между клиентом и сервером
базируется на потоке с использованием класса NetworkStream. Однако
при необходимости можно работать и с байтами.
// Создать объект класса TcpListener и запустить
// его на прослушивание порта 65125
IPHostEntry ipHost = Dns.GetHostEntry("127.0.0.1");
IPAddress ipAddr = ipHost.AddressList[0];
TcpListener tcpListener = new TcpListener(ipAddr, 65125);
tcpListener.Start();
Console.WriteLine("Start of listening");
while (true)
// Ожидание запроса приемника
{
// По запросу приемника установить соединение и вернуть
// новый объект класса сокета, который используется для
// установки связи с приемником; тем временем tcpListener
// продолжает прослушивание
Socket clientSocket = tcpListener.AcceptSocket();
// теперь можно создать сетевой поток данных и
// объект для работы с ним – например, бинарный поток
// вывода
NetworkStream netStream = new NetworkStream(clientSocket);
BinaryWriter binWriter = new BinaryWriter(netStream);
// . . . обмен данными . . .
// в конце работы с клиентом не забудем закрыть сокет:
clientSocket.Close();
}
Класс TcpClient
// создать объект TcpClient для работы с сервером
TcpClient serverSocket;
serverSocket = new TcpClient("localhost", 65125);
// создать сетевой поток данных и объект для чтения из
// него
NetworkStream netStream = serverSocket.GetStream();
BinaryReader binReader = new BinaryReader(netStream);
// . . . обмен данными . . .
serverSocket.Close();
Не забывайте в приложениях обработать исключения! Подсказка:
чтобы определить, где и какие исключения следует обработать,
28
Вычислительные системы, сети и телекоммуникации
создавайте нарочно ошибочные ситуации. Например, можно запустить
клиент без сервера и посмотреть, что получится.
Задание для самостоятельной работы
Создайте простые приложения клиент и сервер на основе классов
TcpListener и TcpClient.
Пусть, например, клиент отправляет сообщение, сервер его
принимает и в ответ тоже отправляет сообщение, и на этом сеанс
клиента завершается.
Разработанное приложение представляет собой шаблон, «скелет»
для вашего проекта.
29
Многопоточность
В клиент-серверных приложениях особое
внимание уделяется понятию многопоточности.
Каждому традиционному приложению
Win32 соответствует один (обычно) или
несколько процессов (process).
Процесс — это единица, которая
характеризуется собственным набором внешних ресурсов и
выделенной приложению областью оперативной памяти.
Для каждого файла ЕХЕ операционная система создает
отдельную изолированную область в оперативной памяти, которой
процесс пользуется в течение всего своего жизненного цикла.
Каждому процессу соответствует один (по крайней мере) или
несколько потоков.
Поток (thread) можно представить как специфический путь
выполнения внутри процесса Win32 (thread в буквальном переводе с
английского означает «нить»).
Первый поток, создаваемый в процессе, называется первичным
процессом (primary thread).
В любом процессе существует, по крайней мере, один поток,
который играет роль точки входа для приложения. В традиционных
графических приложениях Windows такой точкой входа является
метод WinMain (), а в консольных приложениях — метод main().
Основная цель, для которой создаются многопоточные
приложения (вместо использования единственного потока), —
повышение производительности и сокращение времени отклика
приложения.
Многопоточные приложения на однопроцессорном компьютере
создают иллюзию одновременного выполнения сразу нескольких дел.
Управление потоками производится на уровне операционной системы.
Компьютеры с единственным центральным процессором в
действительности не могут одновременно обрабатывать более одного
потока.
Иллюзия многозадачности достигается тем, что каждому потоку
выдаются (в соответствии с его приоритетом) специальные кванты
времени (time-slice).
При исчерпании потоком выделенного ему кванта времени
ресурсы центрального процессора передаются другим потокам, а
первый поток вновь ждет своей очереди.
30
Вычислительные системы, сети и телекоммуникации
Для того чтобы поток мог продолжить выполнение с того места,
на котором его работа была остановлена, он обеспечивается
возможностью записи в локальную память потока (Thread Local
Storage) и отдельным стеком вызовов.
Примечание. К сожалению, в русском языке и для понятия
Stream, и для понятия Thread используется один и тот же термин –
поток. Постарайтесь не путать эти два понятия. Обычно из контекста
бывает ясно, о чем именно идет речь.
Пространство имен System.Threading
Класс
Interlocked
Назначение
Для синхронизированного доступа к общим данным
Обеспечивает синхронизацию потоковых объектов
при помощи блокировок и управления ожиданием
Примитив синхронизации, используемый для
Mutex
синхронизации разных процессов
Представляет поток, работающий в среде выполнения
Thread
.NET. При помощи этого типа можно порождать в
текущем домене приложения новые потоки
Используется для управления набором
ThreadPool
взаимосвязанных потоков
Определяет делегат, который будет вызван в
Timer
указанное время. Операция ожидания выполняется
потоком в пуле потоков
Представляет во время выполнения все объекты
WaitHandle
синхронизации (которые позволяют многократное
ожидание)
Представляет делегат со ссылкой на метод, который
ThreadStart
должен быть выполнен перед запуском потока
TimerCallback Делегат для объектов Timer
Monitor
WaitCallback
Делегат, который представляет метод обратного
вызова для рабочих элементов ThreadPool
31
Работа с классом Thread
Самый простой тип в пространстве имен System.Threading —
это класс Thread. Этот класс в .NET — не более чем объектная
оболочка вокруг некоторого этапа выполнения программы внутри
домена приложения.
Статические
переменные
методы
Назначение
и
CurrentThread
GetData() SetData()
GetDomain()
GetDomainlD()
Sleep()
Это свойство «только для чтения» возвращает
ссылку на поток, выполняемый в настоящее
время
Возвращает/устанавливает значение для
указанного слота в текущем потоке
Возвращает ссылку на домен приложения
(идентификатор домена приложения), в
рамках которого работает указанный поток
Приостанавливает
выполнение текущего
потока на указанное пользователем время
Обычные члены класса Thread
Переменные и методы
Назначение
IsAlive
Это свойство возвращает «true» или «false»
в зависимости от того, запущен поток или
нет
Свойство предназначено для получения или
установки значения, которое показывает,
является ли этот поток фоновым
Свойство для установки дружественного
текстового имени потока
Позволяет получить/установить приоритет
потока (используются значения из
перечисления ThreadPriority)
Возвращает информацию о состоянии
IsBackground
Name
Priority
ThreadState
32
Вычислительные системы, сети и телекоммуникации
Interrupt()
Join()
Resume()
Start()
Suspend()
потока (используются значения из
перечисления ThreadState)
Прерывает работу текущего потока
Ждет появления другого потока (или
указанный промежуток времени) и
завершается
Продолжает работу после приостановки
работы потока
Начинает выполнение потока,
определенного делегатом ThreadStart
Приостанавливает выполнение потока.
Пример
Создается класс Car (автомобиль). Метод Pusk данного класса
содержит цикл из 100 итераций, на каждой из которых скорость
автомобиля изменяется на случайную величину и печатается
информация о состоянии автомобиля.
using System.Threading;
...
public class Car
{
string name;
int rate;
Random r = new Random();
public Car(string name, int rate)
{
this.name = name;
this.rate = rate;
}
public override string ToString()
{
return ("
" + this.name +
", скорость=" + this.rate);
}
public void Pusk()
{
for (int i = 0; i < 100; i++)
{
rate += r.Next(30) - 15;
33
if (rate < 0) rate = 0;
Console.WriteLine(this);
}
}
}
Сначала запускаем пример без использования потоков. Создаем
объект First и запускаем метод Pusk для него, затем то же самое
производим с объектом Second. Все действия выполняются в
единственном, основном потоке – сначала для первого объекта, потом
для второго.
public class Road
{
static void Main(string[] args)
{
Car first = new Car("Камаз-бетономешалка", 100);
first.Pusk();
Car second = new Car("Ока-кабриолет", 50);
second.Pusk();
Console.ReadLine();
}
}
Теперь изменим класс Road: создадим два независимых потока.
Обратите внимание, что поток представляет собой только оболочку
поверх некоторого выполняемого метода:
public class Road
{
34
Вычислительные системы, сети и телекоммуникации
static void Main(string[] args)
{
Car first = new Car("Камаз-бетономешалка", 100);
Thread firstThread = new Thread(
new ThreadStart(first.Pusk));
firstThread.Start();
Car second = new Car("Ока-кабриолет", 50);
Thread secondThread = new Thread(
new ThreadStart(second.Pusk));
secondThread.Start();
}
}
В этом примере (поскольку компьютер быстрый), для каждого
автомобиля в течение интервала времени, выделенного для данного
потока, успевает выполниться сразу несколько итераций.
Теперь заставим потоки «засыпать» на полсекунды. Для этого
изменим метод Pusk. Благодаря паузам информация об автомобиле
поступает «в режиме реального времени»
public void Pusk()
{
for (int i = 0; i < 100; i++)
{
rate += r.Next(30) - 15;
if (rate < 0) rate = 0;
Console.WriteLine(this);
Thread.Sleep(500);
}
35
}
Многопотоковый сервер
Часто серверное приложение вынуждено работать сразу с
несколькими клиентами (пример – чат-сервер).
Тогда для каждого клиента следует создавать отдельный поток.
Удобно разработать специальный класс для этих целей (назовем
его Handler).
Рассмотрим пример сервера-«эха», который только и умеет получать сообщение и тут же отправлять его обратно. Следующий
класс является составной частью серверного проекта:
36
Вычислительные системы, сети и телекоммуникации
class Handler
{
Socket clientSocket; // сокет для связи с клиентом
NetworkStream netStream; // сетевой поток
BinaryWriter binWriter; // поток для записи
BinaryReader binReader; // поток для чтения
public Handler(Socket clientSocket)
{
this.clientSocket = clientSocket;
}
// Это метод для приема сообщений от клиента. Именно на
// него мы и будем «навешивать» отдельный поток в классе
// сервера
public void Reader()
{
// создаем потоки для чтения-записи
netStream = new NetworkStream(clientSocket);
binWriter = new BinaryWriter(netStream);
binReader = new BinaryReader(netStream);
// цикл приема сообщений от клиента
while (true)
{
// здесь возникает пауза, пока клиент
// не пришлет очередное сообщение
string message = binReader.ReadString();
// полученное сообщение выводим на экран
Console.WriteLine("Received:" + message);
// и отправляем обратно в качестве «эха»
binWriter.Write("Received:" + message);
// при получении сообщения о выходе клиента
if (message.Equals("quit"))
{
// разрываем с ним соединение
Console.WriteLine("Client disconnected");
binWriter.Close();
netStream.Close();
break;
}
}
}
Что изменится на сервере
while (true)
{
// Ожидание запроса приемника
37
Socket clientSocket = tcpListener.AcceptSocket();
if (clientSocket.Connected)
{
// создаем объект обработчика для данного клиента
Handler hand = new Handler(clientSocket);
// создаем поток – оболочку на метод Reader
Thread mythread = new Thread(
new ThreadStart(hand.Reader));
// и запускаем поток
mythread.Start();
}
}
Обратите внимание, что всё общение с клиентом теперь перенесено в
класс Handler.
Что изменится на клиенте
// Цикл передачи-приема сообщений выглядит следующим образом:
// клиент отправляет сообщение и ждет ответа, затем процесс
// повторяется
string message;
while (true)
{
message = Console.ReadLine();
binWriter.Write(message);
Console.WriteLine(binReader.ReadString());
// если мы отправили сообщение о выходе,
// закрываем сокет и завершаем работу
if (message.Equals("quit"))
{
serverSocket.Close();
break;
}
}
В примерах не обработаны исключения. Обработайте их
самостоятельно!
Задание для самостоятельной работы
Создайте серверное и клиентское приложения по примеру.
Подумайте, как можно рассылать сообщения всем клиентам (как
это обычно и бывает в чате).
38
Вычислительные системы, сети и телекоммуникации
Потоки на клиентской стороне клиент-серверных приложений
Как мы уже видели, на серверной стороне возникает
необходимость создавать множество потоков при обмена данными со
множеством клиентов (отдельному клиенту - отдельный поток).
Это касается, в основном, приема сообщений от клиентов, так
как мы не можем точно предугадать, в какой момент времени от
клиента придет очередное сообщение.
Но та же самая проблема может возникать и на клиентской
стороне! Происходит это в приложениях, где сервер может присылать
клиенту сообщения в произвольные моменты времени, как, например,
чат-сервер. Поскольку обычно мы тоже не можем точно предугадать,
когда от сервера придет очередное сообщение, приходится
организовывать специальный поток для приема сообщений. Иначе
пауза ожидания сообщений «заморозит» все элементы формы.
Вариант решения проблемы может быть таким: создаем
отдельный метод с бесконечным циклом, в котором читается
очередное сообщение от сервера и на этот метод «навешиваем» поток.
// переменные объекта
TcpClient serverSocket; // сокет
Thread potok = null;
// поток для приема сообщений с сервера
NetworkStream netStream; // сетевой поток
BinaryReader binReader; // поток чтения
BinaryWriter binWriter; // поток записи
39
...
// в обработчике кнопки connect
// после подключения к серверу организуем
// поток-оболочку для метода read:
potok = new Thread(new ThreadStart(this.read));
potok.Start();
...
// Метод для приема сообщений с сервера. Он позволяет серверу
// присылать сообщения в любой момент
void read()
{
string msg=null;
do
{
// прочитать ответ
try
{
msg = binReader.ReadString();
// записать сообщение в текстовую область
result.AppendText("Received: " + msg + "\n");
}
catch (IOException) { } // ну, вдруг ошибка какая-нибудь
} while ((msg.ToLower()).CompareTo("exit") != 0);
}
Проблемы с доступом между потоками
Строка
result.AppendText("Received: " + msg + "\n");
при выполнении вызовет исключение:
40
Вычислительные системы, сети и телекоммуникации
Exception System.InvalidOperationException was thrown in
debuggee:
Cross-thread operation not valid: Control ‘result'
accessed from a thread other than the thread it was created on.
В чем же проблема? Дело в том, что мы пытаемся выполнить
перекрестную операцию между потоками: получить доступ к элементу
управления result, который был создан в основном потоке.
Здесь нам на помощь придут делегаты. Вспомним, что делегат
представляет собой нечто похожее на указатель на функцию в С++.
Для элемента управления result выполним следующие действия:
result.Invoke(new MethodInvoker(delegate {
result.AppendText("Received: " + msg + "\n"); }));
Теперь проблемы с перекрестным доступом между потоками
решены.
41
Базы данных в .NET
Как вы уже знаете, для работы с базами
данных в .Net есть много разнообразных
возможностей. Коротко напомним одну из самых
старых и универсальных технологий для .Net –
OLE DB.
Управляемый провайдер OLE DB
Провайдер OLE DB
реализуется при помощи типов,
определенных в пространстве имен System.Data.OleDb.
Провайдер OleDb позволяет нам обращаться к данным,
расположенным в любом хранилище, к которому можно подключиться
по протоколу OLE DB. Таким образом, аналогично классическому
ADO, вы можете использовать в ADO.NET управляемый провайдер
OLE DB для доступа, например, к базам данных SQL Server, MS
Access и Oracle.
Однако поскольку при этом типы из пространства имен
System.Data.OleDb должны взаимодействовать с обычным (не .NET)
кодом драйверов OLE DB, то при таком обращении будет
производиться множество преобразований вызовов .NET в вызовы
СОМ, что в некоторых ситуациях может привести к падению
производительности.
Наиболее важные типы пространства имен System.Data.OleDb
Тип
OleDbCommand
OleDbConnection
OleDbDataAdapter
OleDbDataReader
42
Описание
Представляет запрос SQL, производимый
к источнику данных
Представляет открытое соединение с
источником данных
Представляет соединение с базой данных
и набор команд, используемых для
заполнения объекта DataSet, а также
обновления исходной базы данных после
внесения изменений в DataSet
Обеспечивает метод считывания потока
данных из источника в одном
Вычислительные системы, сети и телекоммуникации
направлении (вперед)
OleDbErrorCollection представляет набор
ошибок и предупреждений возвращаемых
источником данных
Сами эти ошибки и предупреждения
OleDbError
представлены объектами OleDbError.
При возникновении ошибки может быть
OleDbException
сгенерировано исключение,
представленное объектом OleDbException
Используются для передачи параметров
OleDbParameter
OleDbParameterCollection процедуре, хранимой на источнике
данных. Параметры представлены
объектами
OleDbParameter, весь набор — объектом
OleDbParameterCollection
OleDbErrorCollection
Установление соединения при помощи типа OleDbConnection
// Первый шаг: устанавливаем соединение
OleDbConnection cn = new OleDbConnection();
cn.ConnectionString = "Provider=SQLOLEDB.1;" +
"Integrated Security=SSPI;" +
"Persist Security Info=False;" +
"Initial Catalog=Kontora;" +
"Data Source=HOME-1E51FF62F3;";
cn.Open();
// Выполняем различные операции
cn.Close();
Data Source — это имя компьютера, с которым мы устанавливаем
соединение.
Initial Catalog — имя базы данных, к которой мы подключаемся
(в нашем случае Kontora).
Provider — это имя провайдера OLE DB, который будет
использован для обращения к источнику данных.
Наиболее часто используемые провайдеры OLE DB:
43
Microsoft.JET.OLEDB.4.0
Этот провайдер OLE DB
используется для подключения к
базам данных JET 9, то есть
Access)
MSDAORA
Для подключения к базам данных
Oracle
Для подключения к базам данных
MS SQL Server
SQLOLEDB
Члены класса OleDbConnection
BeginTransaction()
CommitTransacton()
RollbackTransaction()
Используются для того, чтобы
программным образом начать транзакцию,
завершить ее или отменить
Close()
Закрывает соединение с источником
данных (наиболее рекомендуемый способ)
Позволяет настроить строку подключения
при установлении соединения или
получить ее содержание
Позволяет получить или установить время
тайм-аута при
установке
соединения
Позволяет получить или установить
название текущей базы данных во время
подключения
Позволяет получить или установить имя
источника данных
Открывает соединение с базой данных,
используя текущие настройки свойств
соединения
Позволяет получить или установить имя
провайдера
Позволяет получить информацию о
текущем состоянии соединения
ConnectionString
ConnectonTimeout
Database
DataSource
Ореn()
Provider
State
Построение команды SQL
44
Вычислительные системы, сети и телекоммуникации
Объектно-ориентированным представлением запроса на языке
SQL в ADO.NET является класс OleDbCommand. Сам текст команды
определяется через свойство OleDbCommand.CommandText.
// Второй шаг: создаем команду SQL
// Первый вариант выполнения SQL-запроса
string strSQL1 = "Select * from k_firm":
OleDbCommand myCommand = new OleDbCommand(strSQL1, cn);
// Второй вариант выполнения SQL-запроса
string strSQL2 = "Select * from k_firm";
OleDbCommand myCommand2 = new OleDbCommand():
myCommand2.Connection = cn;
myConmand2.CommandText = strSQL2;
Члены класса OleDbCommand
Cancel()
CommandText
CommandTimeout
CommandType
Connection
ExecuteReader()
Parameters
Prepare()
Прекращает выполнение команды
Позволяет получить или задать текст запроса на
языке
SQL (возможно, с особенностями
диалекта, определяемыми типом источника
данных), который будет передан источнику
данных
Позволяет получить время тайм-аута при
выполнении команды. По умолчанию это время
равно 30 секундам
Позволяет получить или установить значение,
определяющее, как именно будет
интерпретирован текст запроса, заданный через
свойство.
Позволяет получить ссылку на объект
OleDbConnection, для которого используется
данный объект OleDbCommand
Возвращает объект OleDbDataReader
Возвращает коллекцию параметров
OleDbParameterCollection
Готовит команду к выполнению (например, она
будет откомпилирована) на источнике данных
45
Работа с OleDbDataReader
Этот класс представляет однонаправленный (только вперед),
доступный только для чтения поток данных, который за один раз
возвращает одну строку в ответ на запрос SQL.
// Третий шаг: получаем объект OleDbDataReader при помощи
// метода ExecuteReader()
OleDbDataReader myDataReader;
myDataReader = myCommand.ExecuteReader();
// Четвертый шаг: проходим циклом по всем возвращаемым данным
while (myDataReader.Read())
{
Console.WriteLine("Фирма: " +
myDataReader["firm_name"].ToString());
}
myDataReader.Close();
Подключение к базе данных Access
OleDbConnection cn = new OleDbConnection();
cn.ConnectionString =
"Provider=Microsoft.JET.OLEDB.4.0.;" +
@"Data Source=D:\book.mdb;";
Примечание: При работе в Visual Studio с 64-битовыми
операционными системами для подключения к этому провайдеру
требуется в свойствах проекта установить 32-битовую платформу
(x86).
46
Вычислительные системы, сети и телекоммуникации
Некоторые проблемы и способы их решения
Проблема: предположим, мы разработали
серверное приложение и клиентское приложение
и хотим разработать специальный класс
«Сообщение» для обмена информацией между
ними. Возникает вопрос: куда, в какой проект
помещать этот специальный класс?
Решение: Самым лучшим решением будет разработка отдельного
проекта, содержащего класс (или несколько классов) сообщений. Этот
проект нужно создавать в виде библиотеки классов, а затем ссылку на
получившийся DLL-файл следует включить в оба основных проекта –
серверный и клиентский.
Проблема: при использовании сериализации
посредством
форматтеров
в
сетевых
приложениях возникает вопрос: с каким типом
потока можно производить сериализацию, т.е.,
какой потоковый тип можно использовать в
качестве первого параметра в методах Serialize и
Deserialize?
Решение:
В
сетевых
приложениях
можно
использовать
непосредственно тип NetworkStream и для чтения, и для записи.
Проблема:
при
работе
с
оконными
приложениями, если создаются дополнительные
потоки (threads), может возникать исключение
InvalidOperationException:
Cross-thread operation not valid: Control
'someControle' accessed from a thread other than
the thread it was created on.
Решение: Дело в том, что мы пытаемся выполнить перекрестную
операцию между потоками: в данном примере мы пытаемся получить
доступ к элементу управления 'someControle', который был создан в
47
другом потоке. То действие, которое вызвало это исключение, следует
обрабатывать с помощью делегата:
someControle.Invoke(new MethodInvoker (
delegate { /* сюда помещаем наше действие */ }));
48
Вычислительные системы, сети и телекоммуникации
Литература
1. Троелсен Э. Язык программирования C# 2010 и платформа
.NET 4.0. Москва, Издательский дом "Вильямс", 2011 (5-е
издание).
2. Эндрю Кровчик, Винод Кумар, Номан Лагари, Аджит Мунгале,
Кристиан Нагел, Тим Паркер, Шриниваса Шивакумар, .Net.
Сетевое программирование для профессионалов. Москва,
Лори, 2005.
3. П. Торстейнсон, Г. А .Ганеш, Криптография и безопасность в
технологии .NET. Москва, Бином, 2007.
49
Download