Текст книги "Программирование мобильных устройств на платформе .NET Compact Framework"
Автор книги: Иво Салмре
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 53 (всего у книги 69 страниц)
Как и в случае кода для настольных компьютеров и серверов, целесообразно работать на самом высоком из допустимых уровней абстракции.
Например, если в процессе работы с Internet-протоколами у вас есть возможность работать на уровне Web-служб, используя запросы/ответы SOAP, то именно так вам и следует поступить; встроенные абстракции позволят вам сэкономить массу времени, превращая ваши Web-запросы в простые вызовы методов. Если для повышения производительности приложения или в интересах его пользовательской адаптации вам необходимо спуститься вниз на один уровень, то запросы и ответы HTTP или HTTPS обеспечат вам довольно высокий уровень абстракции и, как правило, дружественность по отношению к брандмауэрам. Если окажется, что запросы/ответы HTTP не в состоянии удовлетворить ваши потребности, у вас есть возможность использовать средства коммуникации на уровне сокетов и создаваемые поверх сокетов потоки.
Работа на уровне абстракции сокетов усложняет код, поскольку для взаимодействия с серверами вам придется спроектировать собственные коммуникационные протоколы, а не использовать простые и надежно тестированные механизмы запросов/ответов HTTP. Если ваше приложение должно связываться с сервером, который требует взаимодействия на уровне сокетов, то рекомендуется рассмотреть возможность создания прокси-компонента на стороне сервера, который взаимодействовал бы с интерфейсом сокета и, в свою очередь, предоставлял вашему приложению интерфейс HTTP или Web-служб. Поскольку взаимодействие сервера с сервером обычно характеризуется более высокой надежностью по сравнению с взаимодействием между устройством и сервером, то описанная мера позволяет значительно упростить код на стороне устройства и повысить надежность вашего мобильного приложения.
Работать на коммуникационных уровнях ниже уровня сокетов, а также непосредственно использовать стек протоколов TCP/IP имеет смысл лишь в крайних случаях. Если вашему приложению необходимо организовать информационный обмен на этом уровне, то для работы с протоколами вам, вероятнее всего, придется написать большой объем собственного кода на языке С. Дополнительная сложность кода и необходимость его тщательного тестирования почти никогда не стоят того выигрыша, который от этого получит приложение. Высокоуровневые протоколы надежно тестируются, и для того чтобы обеспечить ту же степень надежности при использовании собственных коммуникационных протоколов, вам надо будет проделать невероятно большой объем работы. То же самое справедливо и в отношении протоколов, отличных от протокола TCP/IP; возможно, протокол TCP/IP и не является самым идеальным коммуникационным механизмом для всех задач, но обеспечить своими силами тот же уровень тестирования и надежности кода, что и для этих стеков протоколов, очень трудно. Если только вы не намереваетесь предложить совершенно новый коммерческий протокол, проектирование и тестирование которого потребуют от вас огромных усилий, то нет никакого резона заново изобретать колесо, пытаясь получить идеальный протокол, который будет использоваться исключительно для ваших собственных нужд. Использовать 80% вполне пригодного "колеса", которое уже существует и прошло многолетнее тестирование, намного лучше, чем пытаться создать новое "колесо" самому. Прежде чем приниматься за изобретение нового коммуникационного протокола или переходить на использование более низкого уровня в стеке коммуникационных протоколов, вы должны удостовериться в том, что существующие высокоуровневые коммуникационные протоколы не в состоянии удовлетворить запросам вашего мобильного приложения.
Всегда исходите из того, что связь может быть нарушена в любой моментПри написании коммуникационного кода ключевую роль должно играть обеспечение его отказоустойчивости. Традиционные коммуникационные технологии часто описываются в виде многоуровневого стека, который начинается физическим уровнем, далее содержит канальные уровни и уровни протоколов и заканчивается уровнем приложения. Большинство этих уровней аналогичным образом работают и на мобильных устройствах. Как правило, для каждого уровня предусмотрены встроенные средства повышения отказоустойчивости, вызываемые при обнаружении ошибок или возникновении незначительных сбоев в процессе связи. В большинстве случаев вам не следует беспокоиться о специфике нижних коммуникационных уровней; ситуация здесь в значительной степени та же, что и при написании кода для настольных компьютеров и серверов. Единственное различие состоит в том, что в мобильных сетях нарушения связи случаются чаще, чем в кабельных или стационарных беспроводных сетях.
При написании отказоустойчивого коммуникационного кода очень важно тщательно следить за тем, как освобождаются ресурсы в случае возникновения каких-либо нарушений нормального режима работы. Организация связи представляет собой сложный процесс, включающий многоступенчатое установление множества соединений и распределение системных ресурсов. В случае возникновения сбоев в процессе связи важно вовремя освободить системные и иные ресурсы, удерживаемые вашим приложением, а если вы используете .NET Compact Framework, то обязательно осуществите профилактический вызов метода Dispose() для освобождения тех ресурсов, которые поддерживают данный метод.
Вызов метода Dispose() имеет очень большое значение, поскольку это приводит к немедленному освобождению соответствующих системных ресурсов еще до того, как сборщик мусора закроет ненужные дескрипторы и освободит заблокированные ресурсы. В данной ситуации вам может очень пригодиться ключевое слово С# using (например, using(myObject) {...ваш код...}), поскольку его использование гарантирует вызов метода Dispose() не только в случае успешного выполнения определяемого им блока кода, но и при возникновении исключений в этом блоке. Важно отметить, что некоторые классы .NET, такие как System.Net.Sockets.Socket, не имеют общедоступного метода Dispose(), однако для них предусмотрен метод Close(), который и следует вызывать для освобождения ресурсов, удерживаемых объектом. Вы должны внимательно изучить имеющуюся документацию по всем коммуникационным объектам, которые используете, чтобы иметь полную уверенность в том, что все правила и процедуры, используемые для восстановления ресурсов этих объектов, вам понятны.
Если при обработке сбойных ситуаций вы непреднамеренно оставите ресурсы открытыми, то тем самым создадите предпосылки для сбоя при последующей попытке установления соединения. Если необходимые операции по закрытию ресурсов не выполнены, то разрыв сетевого соединения в результате сбоя, вероятнее всего, приведет к ситуации, в которой последующие попытки установления соединения окажутся неудачными, поскольку необходимый локальный ресурс перед этим был оставлен открытым в состоянии исключительного доступа и поэтому не может быть повторно открыт.
Последующие попытки восстановления связи окажутся неудачными даже после восстановления соединения с физической сетью. В этом отношении мы имеем дело с той же ситуацией, какая возникает и при написании кода для настольных компьютеров и серверов, если не считать того, что нерегулярные сетевые сбои чаще всего происходят тогда, когда устройство является одновременно и мобильным, и беспроводным. Таким образом, обработке сбойных ситуаций необходимо уделять больше внимания, поскольку такие ситуации возникают чаще.
Коммуникационные классы могут предоставлять и другие функции, используемые для освобождения ресурсов, вызов которых может оказаться необходимым для корректного выхода из сбойной ситуации.
Если канал связи необходимо закрывать вручную, обеспечьте вызов соответствующего метода Close() или Dispose(), поместив этот вызов в интерфейсную оболочку, предназначенную для обработки ошибок в случае возникновения сбоев. Сбои в работе вашего приложения могут быть обусловлены не только разрывом соединения, но и другими причинами, например, несоответствием результатов синтаксического анализа ответа сервера тому, что ожидается вашим код. В зависимости от вида приложения и используемых сетевых служб может оказаться важным соблюдение определенной процедуры завершения связи. Так, если ваше приложение связывается с пользовательской службой через сокеты, и при этом используется понятие входа и выхода из системы, то при входе приложения в непредвиденные состояния очень важно завершить сеанс связи корректным образом. Может оказаться так, что вместо простого вызова метода Close() ваше приложение сначала должно будет завершить сеанс связи, послав на сервер команду выхода из системы и вызвав метод Shutdown() для сокета, и только после этого вызвать метод Close(). Очень важно хорошо понимать особенности установления и разрыва соединений с теми службами, которые используются вашим приложением.
Возбуждение и перехват исключений, которые могут приводить к образованию висячих соединений, и проблемы производительности
Под "исключениями" понимается следующее: это исключительные обстоятельства, которые должны обрабатываться средой выполнения и кодом приложения. Возбуждение (или, другими словами, генерация) исключения запускает в среде выполнения сложный процесс, сопровождающийся развертыванием стека для поиска обработчиков исключений, уничтожением локальных переменных и другими операциями, связанными с большими накладными расходами. С точки зрения производительности этот процесс, как правило, требует больших вычислительных ресурсов по сравнению подходом, в котором отказываются от возбуждения исключений и вместо этого заблаговременно выявляют условия, которые могут приводить к возникновению ошибок. Как и в случае других аспектов производительности, окончательным критерием всегда должны служить результаты количественных экспериментов. Кроме того, поскольку возбуждение исключений нарушает обычный порядок выполнения операций в вашем приложении, может оказаться так, что важные операции, связанные с освобождением коммуникационных ресурсов, не будут выполнены. В результате этого часть коммуникационных ресурсов может остаться в "зависшем" состоянии, что может стать причиной дополнительных проблем в процессе дальнейшего выполнения приложения.
В самой идее использования обработки исключений в приложении нет ничего плохого, однако важно понимать, при каких обстоятельствах этого делать не следует. Никакие исключения не должны возбуждаться и обрабатываться в процессе нормального выполнения приложения. Нецелесообразно использовать в приложении код, генерирующий и обрабатывающий исключения, лишь для того, чтобы контролировать возникновение самых обычных ситуаций в ходе выполнения приложения, поскольку это приведет к неэффективным тратам вычислительных ресурсов и усложнению программы. При малейшей возможности старайтесь контролировать состояние информационного обмена с помощью обычного кода, а не посредством механизма возбуждения и обработки исключений. Если обработчики исключений запускаются при нормальном выполнении приложения, то первое, что вы должны сделать – это проанализировать код и посмотреть, нельзя ли организовать дополнительные логические проверки, при помощи которых можно было бы предотвратить возбуждение этих исключений.
Пример сравнения возможностей механизма исключений и обычного алгоритмического кода можно найти в главе 7
В листинге 15.1 представлен простой пример файлового ввода-вывода, иллюстрирующий некоторые различия между сбойными ситуациями, возникающими при обращении к локальным и удаленным файлам. Хотя вы и можете поместить этот код в форму мобильного приложения и запустить его на выполнение, вряд ли стоит это делать; этот листинг приведен в основном лишь для того, чтобы вы его изучили. Как видно из приведенных в коде комментариев, в случае доступа к серверу вероятность сбоя повышается не только для операции открытия файла, но и для файловых операций чтения/записи, что связано с возможностью потери доступа к сети. Вероятность сбоя повышается с увеличением количества коммуникационных уровней, отделяющих ваше устройство от данных, к которым оно пытается обратиться. Если используется беспроводная сеть, то сигнал может быть утерян вследствие перемещения устройства. Если доступ к данным осуществляется посредством виртуальной частной сети через брандмауэр, то сбой на данном сервере может наступить в любой момент. Если соединение проходит через сеть мобильной связи, то сбой может наступить по вине мобильной сети. Несмотря на то что каждый из этих уровней сам по себе может быть надежным, их объединение повышает вероятность сбоя. Поэтому при попытках переноса кода из систем с локальным доступом к файлам в системы, использующие доступ к удаленным серверам, следует быть очень внимательным. В коде, ориентированном на локальный доступ к файлам, могут быть заложены предположения, которые при доступе к удаленным источникам станут причиной сбоев. Чтобы этого не произошло, рекомендуется применять следующие меры предосторожности:
1. Помещайте код доступа к удаленным серверам в блоки try/catch. Любая операция, для выполнения которой требуется удаленное соединение, может закончиться сбоем. Все блоки кода, которые осуществляют доступ к ресурсам, расположенным вне устройства, необходимо помещать в блоки try/catch, которые будут обрабатывать сбойные ситуации.
2. При информационном обмене с удаленными источниками данных старайтесь укладываться в короткие сеансы связи и по окончании работы сразу же закрывайте соединение. Чем дольше вы держите открытыми удаленный сокет, файл, соединение с базой данных или иной удаленный ресурс, тем больше вероятность сбоя. Поэтому очень важно аккуратно инкапсулировать весь коммуникационный код, который открывает соединение, выполняет необходимую работу и закрывает соединение, прежде чем будет продолжено выполнение другой работы. Весьма неразумно оставлять в подвешенном состоянии открытые соединения с сетевыми ресурсами.
Листинг 15.1. Простой код файлового ввода-вывода, иллюстрирующий различия между локальной и удаленной передачей данных
private void button1_Click(object sender, System.EventArgs e) {
//Запись в локальный файл
WriteFile("\testfile.txt");
//Замените имя сервера (MyFileServer) именем своего сервера
//и удалите символы комментария.
//Запись в файл, находящийся в общей сетевой папке (MyFileShare) на сервере:
//"\MyFileServerMyFileShare"
//WriteFile("\\MyFileServer\MyFileShare\testfile.txt");
System.Windows.Forms.MessageBox.Show("Success");
}
private void button2_Click(object sender, System.EventArgs e) {
//Чтение из локального файла
int numberLinesInFile;
numberLinesInFile = CountNumberLinesInFile("\testfile.txt");
//Отобразить количество считанных строк
System.Windows.Forms.MessageBox.Show(
"Successfully read file " + numberLinesInFile.ToString() + " Lines.");
//Замените имя сервера (MyFileServer) именем своего сервера
//и удалите символы комментария.
/* //Записать файл из общей сетевой папки:
//"\MyFileServerMyFileShare"
numberLinesInFile = CountNumberLinesInFile(\\MyFileServer\MyFileShare\testfile.txt);
//Отобразить количество считанных строк
System.Windows.Forms.MessageBox.Show(
"Successfully read file " + numberLinesInFile.ToString() + " Lines.");
*/
}
private void WriteFile(string filename) {
//–
//СОЗДАНИЕ ФАЙЛА:
//
//Для локальных файлов:
// Сбой может возникнуть, если:
// 1. Файл уже существует, и мы не можем осуществить запись поверх него.
// 2. Отсутствует свободное место в файловой системе.
//
//Для файлов на сервере:
// Сбой может возникнуть по вышеперечисленным причинам, а также
// из-за проблем подключения к серверу через сеть,
// проблем безопасности и так далее
//–
System.IO.StreamWriter myStreamWriter = System.IO.File.CreateText(filename);
//–
//ЗАПИСЬ В ФАЙЛ:
//Для локальных файлов:
// Если мы успешно открыли файл, значит, сможем осуществить
// в него запись, лишь бы не исчерпали доступное
// дисковое пространство.
//Для файлов на сервере:
// Сбой может возникнуть по вышеперечисленным причинам, а также
// из-за разрыва нашего сетевого соединения с сервером
//–
myStreamWriter.WriteLine("Hello!");
myStreamWriter.WriteLine("MyTextFile!");
myStreamWriter.WriteLine("GoodBye!");
//–
//ЗАКРЫТЬ ФАЙЛ:
//Аналогично предыдущему, вероятность сбоя при выполнении
//этой операции повышается при доступе к файлу, находящемуся
//в общей сетевой папке
//–
myStreamWriter.Close();
}
private int CountNumberLinesInFile(string filename) {
int numberLinesInput = 0;
//–
//ОТКРЫТЬ ФАЙЛ:
//Для локальных файлов:
// В случае доступа к локальным файлам устройства можно генерировать
// исключение, если доступ к файлу оказывается невозможным
//Для файлов на сервере:
// В дополнение ко всем вышеперечисленным причинам возможных сбоев
// при открытии локального файла сбои также возможны
// из-за проблем доступа к сети, принятой на сервере
// политики безопасности и так далее
//–
System.IO.StreamReader myStreamReader = System.IO.File.OpenText(filename);
string inputLine;
//Организовать построчное чтение файла
do {
//–
//ВВЕСТИ СТРОКУ:
//Для локальных файлов:
// Если файл был успешно открыт, этот вызов
// никогда не завершится сбоем.
//
//Для файлов на сервере:
// Проблемы доступа к беспроводным сетям, с которыми мы можем
// столкнуться при попытке доступа к файлу, повышают вероятность
// сбоя для этого кода и генерации соответствующего исключения
//–
inputLine = myStreamReader.ReadLine();
//Если не было возвращено значение 'null', то мы
//не достигли конца файла. Увеличить значение счетчика строк.
if (inputLine != null) {
numberLinesInput++;
}
//Продолжать выполнение, пока в файле есть данные для чтения
} while(inputLine != null);
//–
//ЗАКРЫТЬ ФАЙЛ:
//Аналогично предыдущему, вероятность сбоя при выполнении
//этой операции повышается при доступе к файлу, находящемуся
//в общей сетевой папке
//–
myStreamReader.Close();
//Возвратить количество строк в файле
return numberLinesInput;
}
Настоятельно рекомендуется тестировать всевозможные условия возникновения ошибок в процессе передачи данных, намеренно вводя нарушения связи и наблюдая за тем, как ведет себя приложение в этих условиях. Не менее важно тестировать, насколько удачными оказываются попытки возобновления доступа к сети после того, как ваше приложение устранило последствия предшествующего сбоя.
Имитация сбоев связи при помощи кода на стороне клиента
В листинге 15.2 представлен механизм, позволяющий тестировать устойчивость работы мобильного приложения после устранения последствий сбоев при связи. Листинг содержит условно компилируемый код, который можно активизировать, поместив в начале файла с исходным кодом директиву #define DEBUG_SIMULATE_FAILURES.
Приведенная ниже функция writeDataToSocket() вызывается в процессе нормальной передачи данных. Для тестирования реакции приложения на сбои при обмене данными можно установить переменную g_failureCode = SimulatedFailures.failInNextWriteSocketCode в любой момент в процессе выполнения приложения. Когда впоследствии будет вызываться коммуникационный код, он возбудит исключение при первом вызове, но не при последующих. Это позволяет имитировать при тестировании ситуации, в которых сетевое соединение внезапно обрывается, вызывая сбой при передаче данных, но затем восстанавливается. Подобное использование условной компиляции тестирующего кода является не совсем элегантным, но зато эффективным способом мониторинга выполнения кода при имитации реальных условий возникновения сбоев.
Коммуникационный код, который вы пишете, следует тщательно проектировать и внимательно проверять, но одного этого еще недостаточно; код необходимо тестировать в условиях намеренно введенных сбоев. Ничто не в состоянии заменить тестирование в реальных условиях, однако самостоятельно инициировать с этой целью варианты сбоя для всех возможных случаев очень трудно. Единственная реальная альтернатива состоит в том, чтобы попытаться сымитировать и исследовать ход выполнения программы для каждой разновидности возможных сбоев. С учетом того, что при возникновении сбоев код может вести себя самым неожиданным образом, единственный способ проверить его отказоустойчивость – это вызвать возникновение ошибок в контролируемых условиях и убедиться в том, что ваше приложение в состоянии устранить последствия этих ошибок и продолжить дальнейшее выполнение.
Листинг 15.2. Имитация сбоев связи с целью тестирования приложения
//–
//Глобальная переменная, которую мы хотим использовать для указания
//необходимости генерации исключений в процессе передачи данных
//–
#if DEBUG_SIMULATE_FAILURES
//Переменная для хранения информация о следующем сбое
static SimulatedFailures g_failureCode = SimulatedFailures.noFailurePending;
//Список сбоев, которые мы хотим имитировать
public enum SimulatedFailures {
noFailurePending, //Отсутствуют текущие сбои, подлежащие обработке
//Имитируемые сбои:
failInNextWriteSocketCode,
failInNextWebServiceCall,
failInNextFileIODuringFileOpen,
failInNextFileIODuringFileRead
//и так далее
}
#endif //DEBUG_SIMULATE_FAILURES
//–
//Функция, которую мы используем для передачи данных...
//–
private void writeDataToSocket(System.Net.Sockets.Socket mySocket, byte[] dataToSend) {
//–
//Этот код следует компилировать лишь при тестировании сетевых сбоев
//–
#if DEBUG_SIMULATE_FAILURES
//Если это сбой, который мы хотим тестировать, генерировать исключение
if (g_failureCode == SimulatedFailures.failInNextWriteSocketCode) {
//Сбросить этот сбой, чтобы он не возник
//при следующем вызове этой функции
g_failureCode = SimulatedFailures.noFailurePending;
throw new Exception("Test communications failure: " + g_failureCode.ToString());
}
#endif
//Передать данные обычным образом...
mySocket.Send(dataToSend);
} //конец функции
Имитация сбоев связи при помощи кода на стороне сервера
Подобно тому, как для имитации сбойных ситуаций можно использовать тестирующий код на устройстве, целесообразно ужесточить условия тестирования приложения в отношении передачи данных путем имитации сбоев и задержек с помощью кода на стороне сервера. Используя мониторинг выполнения кода на сервере, можно вынудить сервер прервать обработку запроса или сформировать "зависание" на неопределенное время в процессе отправки ответа. В этих случаях ваше мобильное приложение, несмотря на сбои, должно предоставить конечному пользователю интерфейс, сохраняющий способность к отклику, и обеспечить возможность восстановления работы приложения. Выполнение тестирование путем имитации сбоев, генерируемых на стороне клиента, а также инициация сбоев и задержек при помощи кода на стороне сервера позволяют проверить выполнение этих требований. Например, в случае вызова Web-службы это легко сделать, передав вместе с запросом дополнительный параметр, указывающий на то, какой тип сбоя требуется тестировать. Значение параметра тестирования по умолчанию может указывать на режим обычного выполнения, тогда как другие значения могут указывать на необходимость генерации сбоя или формирования длительной задержки перед отправкой ответа. Клиент, вызывающий Web-службу, для которой предусмотрен мониторинг выполнения кода, может далее сделать запрос, который сгенерирует одну из этих сбойных ситуаций, предоставляя возможность тестировать ответ.







