Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ZAOCH_KR_2l.doc
Скачиваний:
6
Добавлен:
04.12.2018
Размер:
164.35 Кб
Скачать

САМОСТОЯТЕЛЬНАЯ РАБОТА НА ТЕМУ ПРОГРАММИРОВАНИЕ

СЕТЕВОГО ВЗАИМОДЕЙСТВИЯ КОМПЬЮТЕРОВ С ИСПОЛЬЗОВАНИЕМ ИНТЕРФЕЙСА СОКЕТОВ В СРЕДЕ ОС Linux Fedora12.

Задание1:

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

-В качестве протокола сетевого уровня использовать IP, а в качестве протокола транспортного уровня использовать TCP.

-При этом необходимо реализовать возможность обращения клиента к серверу как по IP адресу, так и по имени компьютера.

-Реализовать работу сокета (по желанию – либо на клиенте, либо на сервере ) в блокирующем и неблокирующем режимах.

Задание2:

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

-В качестве протокола сетевого уровня использовать IP, а в качестве протокола транспортного уровня использовать UDP.

-При этом необходимо реализовать возможность широковещательной рассылки файлов всем компьютерам локальной сети.

Замечания:

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

-При компиляции программ из нескольких файлов использовать утилиту make и создать соответствующий Makefile.

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

Отчет:

Отчет состоит из двух частей – записки и практической части.

Записка должна содержать следующие основные пункты:

  1. Диаграммы системных вызовов при взаимодействии клиента и сервера с использованием протоколов транспортного уровня TCP и UDP.

  2. Схемы алгоритмов программ клиента и сервера.

  3. Исходные тексты программ клиента и сервера.

  4. Текст makefile для компиляции программ клиента и сервера.

В практической части студент должен продемонстрировать:

  1. Создание из исходных текстов исполнимых файлов клиента и сервера при помощи созданного файла makefile и утилиты make.

  2. Продемонстрировать работу клиента и сервера соответствии с заданием.

Кто по списку группы имеет нечётный номер выполняет Задание1, остальные выполняют Задание2.

Литература:

  1. Ш.Уолтон. Создание сетевых приложений в среде Linux. Руководство разработчика.-Москва,Санкт-Петербург,Киев 2001.

  2. А.Робачевский. Операционная система UNIX. – Москва, Санкт-Петербург,Киев 1997.

  3. Make.info.’The GNU Make Manual’, Version 3.79. April 2000

Приложение1.

Основы компиляции программ в ос Linux.

Компилятор превращает код программы на "человеческом" языке в объектный код понятный компьютеру. Компиляторов под Linux существует много, практически для каждого распространенного языка. Большинство самых востребованных компиляторов входит в набор GNU Compiler Collection, известных под названием GCC.

Изначально аббревиатура GCC имела смысл GNU C Compiler, но в апреле 1999 года сообщество GNU решило взять на себя более сложную миссию и начать создание компиляторов для новых языков с новыми методами оптимизации, поддержкой новых платформ, улучшенных runtime-библиотек и других изменений. Поэтому сегодня коллекция содержит в себе компиляторы для языков C, C++, Objective C, Chill, Fortran, Ada и Java, как библиотеки для этих языков (libstdc++, libgcj, ...).

Компиляция программ производится командой:

gcc <имя_файла>

После этого, если процесс компиляции пройдет успешно, то вы получите загружаемый файл a.out, запустить который можно командой:

./a.out

Для примера давайте напишем маленькую простейшую программку:

#include <stdio.h>

int main(){

printf("[http://linux.firststeps.ru]\n");

printf("Our first program for Linux.\n");

return 0;

};

И запустим ее:

Любой компилятор по умолчанию снабжает объектный файл отладочной информацией. Компилятор gcc также снабжает файл такой информацией и на результат вы можете посмотреть сами. При компиляции проекта из предыдущего шага у нас появился файл a.out размером 11817 байт (возможно у вас он может быть другого размера).

Вся эта отладочная информация предназначается для отладки программы отладчиком GNU Debugger. Запустить его вы можете командой:

gdb a.out

При этом запустится отладчик и наша скомпилированная программа. Об отладке мы с вами поговорим отдельно и более подробно.

Компилятор gcc может создавать отладочную информацию в различных объемах и форматах, контролировать которые можно специальными ключами. Посмотреть их подробное описание можно командой man gcc:

Debugging Options

-a -dletters -fpretend-float -g -glevel -gcoff

-gxcoff -gxcoff+ -gdwarf -gdwarf+ -gstabs -gstabs+

-ggdb -p -pg -save-temps -print-file-name=library

-print-libgcc-file-name -print-prog-name=program

Ключ -g создает отладочню информацию в родном для операционной системы виде, он выбирает между несколькими форматами: stabs, COFF, XCOFF или DWARF. На многих системах данный ключ позволяет использовать специальную информацию, которую умеет использовать только отладчик gdb. Другие ключи позволяют более тонко контролировать процесс встраивания отладочной информации.

Ключ -ggdb включает в исполняемый файл отладочную информацию в родном для ОС виде и дополняет ее специализированной информацией для отладчика gdb.

Ключ -gstabs создает отладочную информацию в формате stabs без дополнительных расширений gdb. Данный формат используется отладчиком DBX на большинстве BSD систем. Ключ -gstabs+ дополняет отладочную информацию расширенниями понятными отладчику gdb.

Ключ -gcoff создает отладочную информацию в формате COFF, которая используется отладчиком SDB на большинстве систем System V до версии System V R4.

Ключ -gxcoff снабжает файл информацией в формате XCOFF, который используется отладчиком DBX на системах IBM RS/6000. Использование -gxcoff+ влкючает использование дополнительной информации для gdb.

Ключ -gdwarf добавляет инфомацию в формате DWARF приняотм в системе System V Release 4. Соответственно ключ -gdwarf+ прибавляет возможностей отладчику gdb.

Добавление к этим ключам в конце цифры позволяет увеличить или уменьшить уровень отладки, т.е. управлять размером требуемой отладочной информации. Например ключ:

gcc -g3 ...

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

Отладочная информация может значительно увеличить объем вашего файла (в три-четыре раза). Для создания программ "релизов" существует отдельная программа, позволяющая удалить отладочную информацию из запускаемого файла. Называется эта программа strip. Для того, чтобы полностью очистить файл от отладочной информации, требуется вызвать ее с ключом -s:

strip -s a.out

Обычно простые программы состоят из одного исходного файла. Дело обстоит несколько сложнее, если эта программа становится большой. При работе с такой программой может возникнуть несколько достаточно серьезных проблем:

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

  • Если над программой работает много человек, то практически невозможно отследить сделанные изменения.

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

Это далеко не все пробемы, которые могут возникнуть при наличии программы "монстра". Поэтому при разработке программ рекомендуется их разбивать на куски, которые функционально ограничены и закончены. В этом значительно помогает сам язык C++, предоставляя свой богатый синтаксис.

Для того, чтобы вынести функцию или переменную в отдельный файл надо перед ней поставить зарезервированное слово extern. Давайте для примера создадим программу из нескольких файлов. Сначала создадим главную программу, в которой будут две внешние процедуры. Назовем этот файл main.c:

#include <stdio.h>

// описываем функцию f1() как внешнюю

extern int f1();

// описываем функцию f2() как внешнюю

extern int f2();

int main()

{

int n1, n2;

n1 = f1();

n2 = f2();

printf("f1() = %d\n",n1);

printf("f2() = %d\n",n2);

return 0;

}

Теперь создаем два файла, каждый из которых будет содержать полное определение внешней функции из главной программы. Файлы назовем f1.c и f2.c:

// файл f1.c

int f1()

{

return 2;

}

// файл f2.c

int f2()

{

return 10;

}

После этого процесс компиляции программы с помощью gcc будет выглядеть несколько иначе от описанного при компиляции одного файла. Компилировать можно все файлы одновременно одной командой, перечисляя составные файлы через пробел после ключа -c:

gcc -c main.c f1.c f2.c

Или каждый файл в отдельности:

gcc -c f1.c

gcc -c f2.c

gcc -c main.c

В результате работы компилятора мы получим три отдельных объектных файла:

main.o

f1.o

f2.o

Чтобы их собрать в один файл с помощью gcc надо использовать ключ -o, при этом линкер соберет все файлы в один:

gcc main.o f1.o f2.o -o rezult

В результате вызова полученной программы rezult командой:

./rezult

На экране появится результат работы:

dron:~# ./rezult

f1() = 2

f2() = 10

dron:~#

Теперь, если мы изменим какую-то из процедур, например f1():

int f1()

{

return 25;

}

То компилировать заново все файлы не придется, а понадобится лишь скомпилировать измененный файл и собрать результирующий файл из кусков:

dron:~# gcc -c f1.c

dron:~# gcc main.o f1.o f2.o -o rezult2

dron:~# ./rezult2

f1() = 25

f2() = 10

dron:~#

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

Если каждая функция будет содержаться в отдельном файле, то таких файлов может оказаться десятки или даже сотни. Управлять таким количеством файлов очень сложно. Для этого был придуман механизм создания библиотек объектных файлов.

Библиотека объектных файлов - это файл содержащий несколько объектных файлов, которые будут использоваться вместе в стадии линковки программы. Нормальная библиотека содержит символьный индекс, который состоит из названий функций и переменных и т.д., которые содержатся в библиотеке. Это позволяет ускорить процесс линковки программы, так как поиск функций и переменных в объектных файлах библиотеки происходит намного быстрее, чем поиск в наборе указанных объектных файлов. Поэтому использование библиотеки позволяет компактно хранить все требуемые объектные файлы в одном месте, и при этом значительно повысить скорость компиляции.

Объектные библиотеки по способу использования разделяются на два вида:

  • Статические библиотеки

  • Динамические библиотеки

Статическая библиотека - это коллекция объектных файлов, которые присоединяются к программе во время линковки программы. Таким образом статические библиотеки используются только при создании программы. Потом в работе самой программы они не принимают участие, в отличие от динамических библиотек.

Динамическая библиотека - это созданная специальным образом библиотека, которая присоединяется к результирующей программе в два этапа. Первый этап, это естественно этап компиляции. На этом этапе линковщик встраивает в программу описания требуемых функций и переменных, которые присутствуют в библиотеке. Сами объектные файлы из библиотеки не присоединяются к программе. Присоединение этих объектных файлов (кодов функций) осуществляет системный динамический загрузчик во время запуска программы. Загрузчик проверяет все библиотеки прилинкованные к программе на наличие требуемых объектных файлов, затем загружает их в память и присоединяет их в копии запущенной программы, находящейся в памяти.

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

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

Для начала стоит сказать, что объектный файл создаваемый нашим проверенным способом вовсе не подходит для динамических библиотек. Связано это с тем, что все объектные файлы создаваемые обычным образом не имеют представления о том в какие адреса памяти будет загружена использующая их программа. Несколько различных программ могут использовать одну библиотеку, и каждая из них располагается в различном адресном пространстве. Поэтому требуется, чтобы переходы в функциях библиотеки (операции goto на ассемблере) использовали не абсолютную адресацию, а относительную. То есть генерируемый компилятором код должен быть независимым от адресов, такая технология получила название PIC - Position Independent Code. В компиляторе gcc данная возможность включается ключом -fPIC.

Теперь компилирование наших файлов будет иметь вид:

dron:~# gcc -fPIC -c f1.c

dron:~# gcc -fPIC -c f2.c

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

dron:~# gcc -shared -o libfsdyn.so f1.o f2.o

В результате получим динамическую библиотеку libfsdyn.so: Теперь, чтобы компилировать результирующий файл с использованием динамической библиотеки нам надо собрать файл командой:

dron:~# gcc -с main.с

dron:~# gcc main.o -L. -lfsdyn -o rezultdyn

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

Если Вы сейчас попробуете запустить файл rezultdyn, то получите ошибку:

dron:~# ./rezultdyn

./rezultdyn: error in loading shared libraries: libfsdyn.so: cannot open

shared object file: No such file or directory

dron:~#

Это сообщение выдает загрузчик динамических библиотек (динамический линковщик - dynamic linker), который в нашем случае не может обнаружить библиотеку libfsdyn.so. Для настройки динамического линковщика существует ряд программ.

Первая программа называется ldd. Она выдает на экран список динамических библиотек используемых в программе и их местоположение. В качестве параметра ей сообщается название обследуемой программы. Давайте попробуем использовать ее для нашей программы rezultdyn:

dron:~# ldd rezultdyn

libfsdyn.so => not found

libc.so.6 => /lib/libc.so.6 (0x40016000)

/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

dron:~#

Как видите все правильно. Программа использует три библиотеки:

  • libc.so.6 - стандартную библиотеку функций языка C++.

  • ld-linux.so.2 - библиотеку динамической линковки программ ELF формата.

  • libfsdyn.so - нашу динамическую библиотеку функций.

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

Для того, чтобы добавить нашу директорию с библиотекой в список известных директорий надо подредактировать файл /etc/ld.so.conf. Например, пусть этот файл состоит из таких строк:

dron:~# cat /etc/ld.so.conf

/usr/X11R6/lib

/usr/i386-slackware-linux/lib

/usr/i386-slackware-linux-gnulibc1/lib

/usr/i386-slackware-linux-gnuaout/lib

dron:~#

Во всех этих директории хранятся всеми используемые библиотеки. В этом списке нет лишь одной директории - /lib, которая сама по себе не нуждается в описании, так как она является главной. Получается, что наша библиотека станет "заметной", если поместить ее в один их этих каталогов, либо отдельно описать в отдельном каталоге. Давайте для теста опишем, добавим строку в конец файла ld.so.conf:

/root

Для примера, этот файл находится в домашнем каталога пользователя root, у Вас он может быть в другом месте. Теперь после этого динамический линковщик будет знать где можно найти наш файл, но после изменения конфигурационного файла ld.so.conf необходимо, чтобы система перечитала настройки заново. Это делает программа ldconfig. Пробуем запустить нашу программу:

dron:~# ldconfig

dron:~# ./rezultdyn

f1() = 25

f2() = 10

dron:~#

Как видите все заработало :) Если теперь Вы удалите добавленную нами строку и снова запустите ldconfig, то данные о расположении нашей библиотеки исчезнут и будет появляться таже самая ошибка.

Но описанный метод влияет на всю систему в целом и требует доступа администратора системы, т.е. root. А если Вы простой пользователь без сверх возможностей ?!

Для такого случая есть другое решение. Это использование специальной переменной среды LD_LIBRARY_PATH, в которой перечисляются все каталоги содержащие пользовательские динамические библиотеки. Для того, чтобы установить эту переменную в командной среде bash надо набрать всего несколько команд. Для начала посмотрим есть ли у нас такая переменная среды:

dron:~# echo $LD_LIBRARY_PATH

Если в ответ выводится пустая строка, то эт означает, что такой переменной среды нет. Устанавливается она следующим образом:

dron:~# LD_LIBRARY_PATH=/root

dron:~# export LD_LIBRARY_PATH

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

dron:~# LD_LIBRARY_PATH=/root:${LD_LIBRARY_PATH}

dron:~# export LD_LIBRARY_PATH

Если Вы обнулите эту переменную, то снова библиотека перестанет работать:

dron:~# LD_LIBRARY_PATH=""

dron:~# export LD_LIBRARY_PATH

dron:~# ./rezultdyn

./rezultdyn: error in loading shared libraries: libfsdyn.so: cannot open

shared object file: No such file or directory

dron:~#

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

Почти всем сложным программам для работы требуется входные параметры (опции, аргументы), от значения которых строится последовательность работы алгоритма заложенного в программе или используются различные источники данных.

Передача параметров в программу на C/C++ осуществляется через массив функции main(). Он называется argv (от arguments values - значения аргументов), но в принципе его можно назвать и по другому. Количество этих параметров передается через переменную argc (от arguments counter - счетчик аргументов).

Программа, для работы которой требуется набор входных параметров задается при помощи специального определения функции main():

int main(int argc, char *argv[]{

};

int main(int argc, char **argv){

};

В качестве напишем программку, которая выводит значения переданных параметров:

// программа test.c

#include <stdio.h>

int main(int argc, char *argv[]){

int i=0;

for (i=0;i<argc;i++){

printf("Argument %d: %s\n",i,argv[i]);

};

};

Сохраняем в файл test.c и компилируем:

dron:~# gcc test.c -o test

После этого попробуем запустить программу:

dron:~# ./test

Argument 0: ./test

Передадим несколько параметров:

dron:~# ./test qwe sdf fgh hjk kl 123 --help

Argument 0: ./test

Argument 1: qwe

Argument 2: sdf

Argument 3: fgh

Argument 4: hjk

Argument 5: kl

Argument 6: 123

Argument 7: --help

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

Рассмотрим теперь, как работать с переданными параметрами. В системе Linux существует два вида параметров: короткие и длинные. Короткие параметры начинаются с одного дефиса и имеют длину в один символ, их просто и быстро набирать в командной строке. Длинные параметры начинаются с двух дефисов и могут иметь длинное имя, которое целесообразно использовать в скриптах (чтобы потом можно было вспомнить, что и как происходит). Кроме этого любой параметр может иметь значение, а может и не иметь. Ниже приведен пример несколько параметров:

-h - короткий параметр

--help - длинный параметр

-s 10 - параметры со значениями

--size 10

--size=10

Существует несколько специальных функций предназначенных для разбора списка переданных параметров:

  • int getopt(...) - Обрабатывает короткие параметры

  • int getopt_long(...) - Обрабатывает короткие и длинные параметры

  • int getopt_long_only(...) - Обрабатывает параметры только как длинные

Разберемся с работой первой функции - getopt(...). Ее определение выглядит следующим образом:

#include <unistd.h>

int getopt(int argc, char * const argv[],

const char *optstring);

extern char *optarg;

extern int optind, opterr, optopt;

Эта функция последовательно перебирает переданные параметры в программу. Для работы в функцию передается количество параметров argc, массив параметров argv[] и специальная строка optstring, в которой перечисляются названия коротких параметров и признаки того, что параметры должны иметь значение. Например, если программа должна воспринимать три параметра a, b, F , то такая строка бы выглядела как "abF". Если параметр должен иметь значение, то после буквы параметра ставится двоеточие, например параметр F и d имеют значения, а параметры e, a и b не имеют, тогда эта строка могла бы выглядеть как "eF:ad:b". Если параметр может иметь (т.е. может и не иметь) значение, то тогда ставится два знака двоеточия, например "a::" (это специальное расширение GNU). Если optstring содержит "W:", то тогда параметр -W opt переданный в программу, будет восприниматься как длинный параметр --opt. Это связано с тем, что параметр W зарезервирован в POSIX.2 для расширения возможностей.

Для перебора параметров функцию getopt() надо вызывать в цикле. В качестве результата возвращется буква названия параметра, если же параметры кончились, то функция возвращает -1. Индекс текущего параметра хранится в optind, а значение параметра помещается в optarg (указатель просто указывает на элемент массива argv[]). Если функция находит параметр не перечисленный в списке, то выводится сообщение об ошибке в stderr и код ошибки сохраняется в opterr, при этом в качестве значения возврящается "?". Вывод ошибки можно запретить, если установить opterr в 0.

#include <stdio.h>

#include <unistd.h>

int main(int argc, char *argv[]){

int rez=0;

// opterr=0;

while ( (rez = getopt(argc,argv,"ab:C::d")) != -1){

switch (rez){

case 'a': printf("found argument \"a\".\n"); break;

case 'b': printf("found argument \"b = %s\".\n",optarg); break;

case 'C': printf("found argument \"C = %s\".\n",optarg); break;

case 'd': printf("found argument \"d\"\n"); break;

case '?': printf("Error found !\n");break;

};

};

};

Попробуем скомпилировать данную программку и запустить:

dron:~# gcc test.c -o test

dron:~# ./test -a -b -d -C

found argument "a".

found argument "b = -d".

found argument "C = (null)".

dron:~# ./test -a -b -C -d

found argument "a".

found argument "b = -C".

found argument "d"

dron:~# ./test -a -b1 -C -d

found argument "a".

found argument "b = 1".

found argument "C = (null)".

found argument "d"

dron:~# ./test -b1 -b2 -b 15

found argument "b = 1".

found argument "b = 2".

found argument "b = 15".

Рассмотрим, как функция getopt вылавливает ошибки. Попробуем задать параметр, которого нет в списке:

dron:~# ./test -h -a

./test: invalid option -- h

Error found !

found argument "a".

Функция вывела сообщение об ошибке в stderr. Давайте выключим вывод сообщений, для этого надо где-то в программе перед вызовом функции вставить opterr=0;. Компилируем и запускаем:

dron:~# ./test -h -a

Error found !

found argument "a".

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

Приложение2.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]