title | institute | lang | geometry | mainfont | monofont | fontsize | colorlinks |
---|---|---|---|---|---|---|---|
Лабораторная работа № 2. Приемы программирования сетевых приложений |
кафедра Управления и информатики НИУ «МЭИ» |
ru-RU |
top=2cm, bottom=2cm, left=3cm, right=2cm |
PT Serif |
PT Mono |
12pt |
blue |
Требуется:
- повторить средства C++ для работы с массивами (буферами), приведением типов, битовыми флагами и масками, представлением данных в памяти;
- научиться компилировать и компоновать (собирать) программы, использующие сетевые API в Windows;
- научиться обнаруживать и расшифровывать ошибки, возникающие в API сокетов.
Предполагается, что вы знаете основы C++ и его средства низкоуровневого программирования.
Повторенные и полученные знания понадобятся как на последующих ЛР, так и при программировании сетевых приложений в целом.
Примечание. О языке программирования
В течение курса предлагается писать сетевые приложения на C++, и вот почему:
- язык позволяет напрямую обращаться с байтами, битами, указателями;
- сетевые приложения уровня сокетов чаще всего пишут на C и C++, поэтому для них доступно больше примеров и литературы;
- API сокетов стандартизировано и документировано для C.
Вообще говоря, допускается выполнять задания на любом другом языке, но только при соблюдении условий:
- Альтернативный язык и его средства согласованы с преподавателем.
- Должно использоваться именно API сокетов (как его предоставляет язык).
Например, для C# это класс
Socket
модуляSystem.Net.Sockets
, но неTcpClient
,TcpServer
,NetworkStream
того же модуля. - Программы должно быть возможно скомпилировать в лаборатории и выполнить с ними все задания (возможно, на собственном ноутбке и через Wi-Fi). Невозможность это сделать не может служить оправданием невыполнения работы.
- При работе в бригаде все участники должны использовать один язык. Не допускается ссылаться на незнание альтернативного языка на защитах.
(@) Создайте учебный проект lab02-recap
.
Запустив CodeBlocks из меню «Пуск», создайте в ней новый проект (File → New → Project…) типа Console application, выбрав язык C++. Каталог проекта не должен включать пробелов и кириллических символов (на Windows XP).
Каждый пункт этого задания кратко напоминает определенный аспект C++ и требует написать короткую функцию-демонстрацию. Таким образом, итоговая программа должна иметь такую структуру:
#include <cstdio>
// прочие #include…
void demo_something(); // объявление одной из функций-демонстраций
// аналогичные объявления…
int
main() {
demo_something(); // вызов демонстрации
// аналогичные вызовы…
}
// реализация демонстрации
void
demo_something() {
// вывод имени функции
puts(__func__);
// код пункта задания
// …
// отделение вывода пустой строкой для удобства
putchar('\n');
}
// прочие реализации…
Стандарт C++ не определяет точный размер встроенных типов данных.
Так, на практике переменная типа long
может занимать 4 или 8 байтов.
В сетевых протоколах же размеры полей строго фиксированы.
Заголовочный файл <cstdint>
определяет типы данных фиксированного размера:
uint8_t
— байт или октет в сетевых терминах (когда зарождался интернет, еще активно использовались машины с размером «байта» не 8 бит);uint16_t
— 16-битное «короткое» слово;uint32_t
— 32-битное «длинное» слово.
Размер любой переменной можно узнать оператором sizeof
, например:
printf("sizeof(int) = %d\n", sizeof(int));
(@) Напишите функцию demo_size()
и добавьте в программу ее вызов.
Функция должна демонстрировать работу с размерами типов:
* Объявить три переменных: октет `x`, «короткое» слово `y`
и «длинное» слово `z` (понадобится подключить `<cstdint>`).
* Напечатать размеры `x`, `y` и `z`.
Во многих местах API сокетов необходимо указывать некое подмножество возможных вариантов. Например, при приеме данных из сети можно, в частности:
- не ждать данных, а сразу получить ошибку, если их нет (
MSG_DONTWAIT
); - оставить данные доступными для следующего считывания (
MSG_PEEK
).
Допускается не задействовать никакую из этих возможностей, задействовать одну
или обе сразу. Чтобы указать нужный выбор (например, и то, и другое),
используется одно целое значение, каждый бит которого означает, включена ли
определенная опция во множество запрошенных. Упомянутый пример представлен так:
0b0100'0010
, где младшая единица значит MSG_PEEK
, старшая — MSG_DONTWAIT
.
Целое значение, в котором установлен один бит, отвечающий за элемент
множества, называется флагом. Например флаг MSG_PEEK
равен 2
(0b0000'0010
или 0x02
), а MSG_DONTWAIT
равен 0b0100'0000
или 0x40
.
Флаги комбинируются логическим ИЛИ, потому что оно оставляет в результате те биты, которые установлены хотя бы в одном операнде; то есть все биты, устанавливаемые флагами, окажутся в их комбинации:
int flags = MSG_PEEK | MSG_DONTWAIT;
Чтобы проверить наличие флага в комбинации, используется логическое И, потому что оно оставляет в результате только биты, установленные в обоих операндах; то есть, если во флаге установлен один бит, результат будет отличаться от нуля только в том случае, если бит присутствует и в комбинации:
if (flags & MSG_PEEK) {
// Действия в случае, когда MSG_PEEK установлен.
}
Побитовое отрицание флага, например, ~FLAG_GOOD
позволяет получить комбинацию,
где указанный флаг сброшен, а остальные установлены. Побитовое И такой
комбинации с любой другой сбрасывает этот флаг в результате.
Исключающее ИЛИ (XOR, ^
) комбинации и флага позволяет изменить состояние
флага на противоположное (используется редко).
(@) Напишите функцию demo_flags()
и добавьте в программу ее вызов.
Функция должна демонстрировать работу с флагами:
* Объявить набор констант-флагов:
```
enum Flag : uint32_t { // Флаги будут числами типа uint32_t:
FLAG_QUICK = 0x01, // - быстро
FLAG_GOOD = 0x02, // - качественно
FLAG_CHEAP = 0x04 // - недорого
};
```
* Объявить переменную, присвоив комбинацию любых двух флагов из трех:
```
int flags = FLAG_GOOD | …; // не обязательно FLAG_GOOD
```
* Проверить наличие в переменной-комбинации того флага, который включен
в нее. Вывести `<имя константы> present`, если флаг обнаружен
в комбинации; `<имя константы> absent`, если не обнаружен.
```
if (flags & FLAG_GOOD) {
puts("FLAG_GOOD present");
} else {
puts("FLAG_GOOD absent");
}
```
* Сделать то же самое для флага, который не включен в комбинацию.
Все ячейки памяти, то есть байты, имеют номер, называемый адресом. Указатель — это переменная, содержащая адрес памяти. Подчеркнем, что адрес имеет смысл только в пределах машины и во время выполнения программы.
При наличии переменной можно получить ее адрес оператором &
:
int variable = 42; // int — целое число
int* pointer = &variable; // int* — указатель на целое число
При наличии указателя можно обратиться к данным по адресу в нем
оператором разыменования (*
):
printf("variable = %d\n", *pointer); // 42
(*pointer) = 43; // variable = 43
Это часто используется, чтобы организовать выходные параметры функций. Одним из параметров является указатель, по которому функция записывает полезное значение. Вызывающий код объявляет переменную и передает в такую функцию адрес этой переменной. После вызова функции значение переменной оказывается измененным.
(@) Напишите функцию, которая записывает по адресу в quotient
результат
деления x
, на y
, а в remainder
— остаток от деления x
на y
и возвращает true
. Если y
равна 0
, функция сразу возвращает false
.
Прототип функции:
bool quot_rem(int x, int y, int* quotient, int* remainder);
**Указание.** Числа сравниваются `==`, остаток от деления берется `%`.
(@) Напишите функцию demo_pointers()
и добавьте в программу ее вызов.
Функция должна демонстрировать работу с указателями:
* Объявить целочисленные переменные `quotient` и `remainder`.
* Вызвать функцию `quot_rem()`, получая частное и остаток в объявленные
переменные и печатая их значения после вызова:
- с ненулевыми `x` и `y`;
* с любым `x` и нулевым `y`.
Есть специальное значение — нулевой указатель (NULL
, читается «нал»),
которое используется для индикации, что указатель не содержит адреса,
по которому можно обратиться. Часто в API сокетов встречаются места,
куда можно передать указатель, чтобы по нему записались полезные данные,
или NULL
, если эти данные не интересуют.
(@) Доработайте quot_rem()
: если quotient
или remainder
— нулевой,
не записывайте данных по адресу в нем. Добавьте в demo_pointers()
три новых вызова quot_rem()
, где нулевыми указателями является
quotient
, remainder
и обе сразу (а y
— не нуль).
Часто применяются указатели на неизменяемые данные, например:
const int* readonly_pointer = &variable;
Их можно разыменовать, чтобы прочитать значение, на которое они указывают, не нельзя изменить это значение через указатель. Как правило, в виде таких указателей передаются входные параметры функций. Указатель на неизменяемые данные нельзя использовать так, где нужен обычный указатель.
Особый вид указателей — нетипизированные: void*
и const void*
.
Они могут указывать на данные любого типа, соответственно, любой указатель
можно использовать там, где требуется нетипизированный (с учетом const
).
Однако их нельзя разыменовать: неизвестен тип данных, который при этом
получится. Как же использовать такие указатели? Технически любой указатель —
просто число, которое можно интерпретировать как адрес данных любого же типа.
Это называется приведением типов (type cast). Например, можно взять адрес
целого числа, привести его к указателю на байты и рассмотреть байты,
которые составляют целое число:
int number = 42;
auto bytes = reinterpret_cast<uint8_t*>(&number);
printf("%02x %02x %02x %02x\n", bytes[0], bytes[1], bytes[2], bytes[3]);
(Ключевое слово auto
сообщает компилятору вывести, то есть самому понять,
тип переменной, в данном случае, uint8_t*
. Само auto
— не тип.)
В реальном коде часто встречается приведение в стиле C: (uint8_t*)(&number)
.
Так как приведения типов слабо контролируются компилятором, а значит, чреваты
ошибками, громоздкая и заметная конструкция reinterpret_cast
предпочтительна.
Массив — последовательно расположенные в памяти элементы одного типа.
Размер статических массивов известен на этапе компиляции и не меняется.
Например, если программа готова принять из сети до 1,5 КБ данных, буфер
для них можно объявить так (необходимо подключить <array>
):
std::array<uint8_t, 1536> buffer;
Массивы std::array
лучше массивов C (uint8_t buffer[1536]
) дополнительными
средствами для работы с ними, например, количество элементов можно узнать
как buffer.size()
.
Не следует объявлять очень большие (свыше нескольких тысяч элементов) статические массивы: они расходуют ограниченную область памяти под локальные переменные, называемую стеком. Исчерпание стека приведет к краху программы.
N. B.: Размер массива (sizeof(buffer)
) — не то же самое, что количество
элементов в нем (buffer.size()
); они совпадают только для байтовых массивов.
Размер памяти под массив (в байтах) — произведение размера элемента на их
количество.
Размер динамических массивов определяется только при работе программы.
Например, часто сначала из сети принимается размер сообщения, затем выделяется
динамический массив этого размера, в который принимается само сообщение.
Для динамических массивов рекомендуются векторы из <vector>
:
std::vector<uint8_t> buffer;
// Между этипи строками каким-то образом выбирается размер size массива.
buffer.resize(size);
Векторы автоматически освобождают занятую в процессе работы память.
N. B. Размер самого вектора (sizeof(buffer)
) никак не связан с количеством
элементов в нем (buffer.size()
). Размер памяти, занимаемой вектором,
рассчитывается так же, как для массивов.
На элементы массивов можно создавать указатели:
uint8_t* partial = &buffer[1024];
К указателям можно применять индексацию, например, partial[0]
— то же самое,
что buffer[1024]
. Разумеется, при этом нельзя обращаться за пределы массива.
К сожалению, C++ не пресекает такие попытки.
N. B. Выход за пределы массив и порча тем посторонней памяти («расстрел памяти») — распространенная и очень опасная ошибка с непредсказуемыми последствиями. Сетевые приложения должны проверять корректность индексов.
Ключевые функции API сокетов для приема и передачи данных принимают, в числе прочих, два параметра: указатель на данные и количество байт, которые находятся по этому адресу — доступных для заполнения или готовых для отправки соответственно.
(@) Напишите функцию demo_arrays()
и добавьте в программу ее вызов.
Функция должна демонстрировать работу с массивами:
* В начале программы необходимо подключить библиотеки: `#include <array>`
* В функции завести статический массив: `std::array<int, 42> static_buffer`
* Напечатать:
- размер переменной-массива:
```
printf("sizeof(static_buffer) = %d\n", sizeof(static_buffer));
```
- размер его элемента с индексом 0:
```
printf("sizeof(static_buffer[0] = %d\n", sizeof(static_buffer[0]));
```
- количество элементов в массиве:
```
printf("static_buffer.size() = %u\n", static_buffer.size());
```
- размер массива в байтах:
```
printf("static_buffer takes %u bytes\n",
static_buffer.size() * sizeof(static_buffer[0]));
```
* Проделать то же самое, но для динамического массива `dynamic_buffer`.
(@) Напишите функцию для отображения указанного количества count
байт памяти,
расположенных по адресу address
. Перед выводом собственно байт функция
должна выводить отдельно значение count
(например: dumping 8 bytes
).
void hex_dump(const void* address, size_t count);
Реализовать ее можно так:
- напечатать значение
count
; - привести
address
к указателю на байты (const uint8_t*
): - в цикле по
i
от0
доcount
(не включая) выводитьi
-й байт. - перейти на следующую строку, чтобы отделить вывод байт от последующего.
Функция понадобится в последующих пунктах.
Структура — определяемый пользователем тип данных, содержащий несколько именованных членов-переменных (полей), которые хранятся вместе:
// Показания датчика.
struct Metric {
uint32_t time; // время съема, секунд с 00:00:00 01.01.1970 GMT
char sensor[10]; // имя датчика (до 9 символов и завершающий '\0')
float value; // значение показателя
};
Структуры объявляются как обычные переменные, а обращения к их полям делается через точку:
Metric metric;
metric.time = time(NULL);
strcpy(metric.sensor, "cpu0");
metric.value = 59.4f;
Если имеется указатель на структуру, к полям удобно обращаться оператором-стрелкой:
Metric* pointer = &metric;
pointer->time += 42; // (*pointer).time += 42;
Для оптимизации компилятор может применять выравнивание: вставлять между членами структуры неиспользуемое пространство, чтобы они оказались в памяти по адресам, кратным 4, 8 или 16.
(@) Напишите функцию demo_alignment()
и добавьте в программу ее вызов.
* Объявите структуру `Metric`. (Обычно типы не объявляют внутри функций,
но это не запрещается языком и в данном случае удобно.)
* Объявите переменную типа `Metric` и заполните ее, как показано.
Функция `time()` объявлена в `<ctime>`, `strcpy()` — в `<cstring>`.
* Выведите значения полей структуры:
```
printf("metric.time = %u\n", metric.time);
printf("metric.sensor = '%s'\n", metric.sensor);
printf("metric.value = %f\n", metric.value);
hex_dump(&metric, sizeof(metric));
```
* Выведите байты памяти, которую занимает переменная `metric`.
Выравнивание определяется компилятором. Если скомпилировать одну и ту же программу для разных машин разными компиляторами (или с разными настройками), выравнивание может измениться. И тогда, если отправить байты структуры в сеть, на другой машине нельзя интерпретировать эти байты как структуру, потому что одно и то же поле имеет разное смещение от начала структуры в зависимости от выравнивания.
(@) Убедитесь, что выравнивание может создавать описанную проблему,
создав аналогичные структуры и попытавшись совместить их представление
в памяти. (Изменения вносите в demo_alignment()
.)
* Окружите структуру `Metric` директивами компилятора, жестко задающими
выравнивание на 8 байт (для воспроизводимости эксперимента):
```
#pragma pack(push, 8)
struct { … };
#pragma pack(pop)
```
* Объявите ниже точно такую же структуру, но под именем `Metric2`
и с отключенным выравниванием, то есть с выравниванием на 1 байт.
* Интерпретируйте память, занимаемую `metric`, как байты:
```
uint8_t* bytes = reinterpret_cast<uint8_t*>(&metric);
```
* Затем интерпретируйте эти байты как `Metric2`:
```
Metric2* metric2 = reinterpret_cast<Metric2*>(bytes);
```
* Выведите значения полей `metric2` и байты памяти, которые `metric2`
занимает (при вызове `hex_dump()` помните, что `metric` была
структурой, а `metric2` — уже указатель на структуру).
В отчете дополнительно отметьте неиспользуемые байты выравнивания в metric
.
В сетевых протоколах обычно либо не допускается выравнивания, либо, наоборот, выравнивание есть и строго задано, то есть расположение данных в памяти (и в пакете) фиксировано.
При записи чисел принято писать старшие разряды в начале, например,
513 — сначала сотни (5), затем десятки (1), затем единицы (3).
В памяти минимально адресуемая единица не разряд, а байт, то есть 513
(в шестнадцатеричной системе 0x0201
) представлено как два байта:
0x02
и 0x01
. В памяти их можно расположить как 0x02
, 0x01
—
от старшего к младшему (big-endian) или как 0x01
, 0x02
— от младшего
к старшему (little-endian). Это выбирают разработчики процессора,
а программисту их выбор безразличен: пока работа ведется через переменные,
внутреннее представление всегда будет использоваться одно и то же.
При передаче данных по сети нет переменных — только байты.
Возникает неоднозначность, как трактовать целое число, пришедшее по сети
в виде нескольких байтов (например, 0x01
, 0x02
): как big-endian
(тогда это 0x0102
= 258) или как little-endian (тогда это 0x0201
= 513).
По сети могут передаваться данные между машинами, процессоры которых
используют разный порядок байт, поэтому нужна договоренность заранее.
Эта договоренность описывается как часть любого двоичного протокола.
Исторически сложилось, что большинство протоколов используют big-endian, поэтому его еще называют сетевым порядком байт (network byte-order). «Сетевой» — это термин, сама по себе передача по сети не влияет на порядок байт. Архитектура x86 (процессоры Intel и AMD) использует little-endian.
В API сокетов входят функции для преобразования порядка байт:
htons
, htonl
, ntohs
и ntohl
. Названия расшифровываются по такой схеме:
host to network [for] l
ong
— из порядка байт хоста
в сетевой порядок байт для длинного целого (4 байта); двухбайтовые целые
называются s
hort
. Функции объявлены в <winsock2.h>
.
(@) Расшифруйте названия оставшихся трех функций и занесите в отчет.
(@) Напишите функцию demo_byte_order()
и добавьте в программу ее вызов.
В функции нужно продемонстрировать работу с порядком байт
для 16- и 32-битного целого. Для 16-битного код приведен,
для 32-битного добавьте код самостоятельно.
* Завести две целочисленные переменные и дать им произвольные значения,
чтобы все байты были ненулевыми:
```
uint16_t host2 = 0x2018;
```
* Вывести значения переменных и байты, составляющие их (`%04x` — вывод
в шестнадцатеричном виде (`x`) четырех цифр с ведущими нулями):
```
printf("host2 = %04x\n", host2);
hex_dump(&host2, sizeof(host2));
```
* Преобразовать значения в сетевой порядок байт:
```
uint16_t net2 = htons(host2);
```
* Вывести преобразованные значения и их байты:
```
printf("net2 = %04x\n", net2);
hex_dump(&net2, sizeof(net2));
```
Предупреждение. У вас не получится собрать программу, использующую
htons()
и ей подобные функции. Данный пункт необходимо довести со состояния,
когда останутся две ошибки undefined reference
.
(@) Сохраните проект и продолжайте выполнять задания. По завершении следующего раздела вернитесь и доведите программу до работоспособного состояния. Переключение между проектами в CodeBlocks делается двойным щелчком по ним.
(@) Создайте новый проект — lab02-api
.
(@) Подключите к программе API сокетов, добавив вверху файла main.cpp
:
```
#include <winsock2.h>
```
(@) Внутри функции main()
инициализируйте API сокетов:
```
// Инициализировать API сокетов (ws2_32.dll).
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
// Код, работающий с сетью, должен размещаться здесь или вызываться отсюда.
// Завершить работу с API сокетов (ws2_32.dll).
WSACleanup();
```
(@) Попытайтесь собрать программу (клавиши Ctrl+F9, пункт меню Build → Build или кнопка на панели инструментов).
Скопируйте содержимое вкладки *Build Log* в отчет.
Программа компилируется, но не компонуется (не линкуется).
Это можно видеть во вкладке Build Log ниже исходного кода: первая команда
компиляции g++.exe … -c …\main.cpp -o …\main.o
прошла успешно, а команда
компоновки g++.exe -o …\lab02-api.exe …\main.o …
завершилась с ошибками.
Текст ошибок сообщает об unresolved reference
— неразрешенных ссылках,
то есть о вызовах функций, машинного кода которых нет ни в программе,
ни в стандартных библиотеках. Ошибки указывают на конкретные функции —
задействованные нами WSAStartup()
и WSACleanup()
.
(@) Добавьте недостающие библиотеки с API сокетов в компоновку.
Откройте диалог настроек сборки проекта (Project → Build options…).
В дереве слева выберите верхний узел lab02-api
: библиотека нужна
как в отладочной сборке (Debug
), так и в фильной (Release
).
На вкладке Linker settings кнопкой Add добавьте в список библиотек
(Link libraries) нужную — ws2_32
(без расширения).
Имя библиотеки программисты узнают из документации (пункт 4).
(@) Убедитесь, что теперь программа собирается.
В качестве примера API сокетов рассмотрим функции для диагностики ошибок. Они крайне важны потому, что к надежности сетевых программ предъявляются повышенные требования. Сетевые приложения часто работают долгое время без остановки, поэтому недопустимы утечки ресурсов (выделение без освобождения), в том числе при ошибках, а значит, ошибки должны аккуратно обрабатываться.
Практически любое действие, связанное с сетью, может привести к ошибке.
Как правило, функции в случае ошибки возвращают особое значение (часто -1
,
но не всегда) и выставляют глобальный (для потока) числовой код ошибки.
Его можно получить функцией WSAGetLastError()
в Windows или из переменной
errno
в *nix. Описания кодов сведены в таблицу (Windows)
или перечислены на man
-страницах (*nix).
Очень важно понимать, что глобальный код ошибки используется не только API сокетов, и успешные вызовы сбрасывают его в 0. Между вызовом API сокетов и проверкой кода ошибке не должно быть кода, обращающегося к системным API, которые могут код ошибки изменить. Например, вот неверный код:
auto channel = socket(0x12, 0x34, 0x56); // Некорректный API сокетов.
if (channel == INVALID_SOCKET) {
cerr << "Error code: " << WSAGetLastError() << '\n';
}
Действие cerr << "Error code: "
выводит данные на экран, то есть обращается
к системному API (для жоступа к экрану). Обычно это происходит успешно,
и глобальный код ошибки сбрасывается в 0
. Правильный код:
auto channel = socket(0x12, 0x34, 0x56);
if (channel == INVALID_SOCKET) {
int error = WSAGetLastError();
cerr << "Error code: " << error << '\n';
}
Здесь код ошибки запоминается сразу после проблемного вызова, поэтому дальнейший сброс его уже не имеет значения.
(@) Добавьте корректный вариант кода в программу. Запустите ее, чтобы получить код ошибки, и занесите его в отчет. По таблице определите, как называется константа для этого кода, каков его смысл, и добавьте это в отчет.
Сокет — это идентификатор устройства для связи по сети, подобно тому, как файловый дескриптор является идентификатором устройства (файла) на диске. Например, через переменную-сокет ведется работа с отдельным подключением.
Некоторые операции работают с несколькими сокетами (например, ожидание прибытия данных по одному из них), поэтому при ошибках по глобальному коду нельзя определить, с каким именно сокетом возникла проблема. В таких случаях есть возможность получить код последней ошибки конкретного сокета.
Следующий код создает сокет channel
:
auto channel = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
(@) Исправьте в программе вызов socket()
на правильный. Запустите программу,
опишите и обоснуйте в отчете результат ее выполнения.
Без полноценного сетевого взаимодействия затруднительно вызывать сбой сокета.
Просто получим код ошибки сокета в переменную error
(он будет нулевым,
так как ошибки нет):
int error = 0;
int length = sizeof(error);
getsockopt(channel, SOL_SOCKET, SO_ERROR, (char*)&error, &length);
printf("Socket error (code %d)!\n", error);
(@) Добавьте в программу код для получения ошибки сокета. Запустите программу и занесите в отчет результат ее выполнения.
(@) Функция getsockopt()
сама по себе может
завершиться с ошибкой (см. раздел Return value).
Добавьте в программу код, который обрабатывает ее.
(@) Изучите разделы Parameters и Remarks документации на getsockopt()
.
Опищите в отчете, почему функиця должна быть вызвана именно с приведенными
значениями параметров.
#. Почему для битовых флагов используются значения 0x01
, 0x02
, 0x04
и тому подобные (а не просто 1
, 2
, 3
, например)?
#. Запишите выражение для получения из комбинации флагов flags
такой же,
но со сброшенным флагом FLAG_GOOD
(не важно, был ли он уже сброшен).
#. Запишите выражение для получения из комбинации флагов flags
такой же,
но где флаг FLAG_GOOD
установлен, если он был сброшен, и сброшен,
если он был установлен.
#. Каков размер указателя, от чего это зависит и от чего не зависит (для платформы x86_64)?
#. Требуется ли учитывать и менять порядок байтов для указателей? Если да, когда и как; если нет, почему?
#. Можно ли через приведение типов получить доступ к данным явно некорректным образом, например, рассмотреть дробное число как текст? Ответ обоснуйте.
#. Имеется массив buffer
(std::array
или std::vector
). Запишите вызов
функции hex_dump()
, позволяющий напечатать байты памяти, которая занята
элементами buffer
, начиная с 512. Размер массива допускается узнавать,
используя только сам buffer
и операции над ним.
#. Может ли запись значений в массив по индексам, большим его размера, привести к негативным последствиям, и если да, к каким? (Исследуйте варианты в сети или предложите обоснуйте теоретически.)
#. Имеется связанный список, узлы которого — упакованные структуры с полем-указателем на следующий элемент и текстовыми даннными. Можно ли, и почему, передавать по сети узлы списка (байты памяти, которую они занимают), принимать их и использовать «как есть»?
#. Отправитель формирует в памяти упакованную структуру Metric
и отправляет блок памяти с ней по сети. Получатель считывает эти байты,
приводит указатель на них к типу Metric*
и использует как структуру.
Корректен ли такой протокол в общем случае (для любых двух платформ)?
Если да, чем это гарантируется; если нет, как исправить?
#. Нужно ли при передаче по сети полей-комбинаций флагов учитывать порядок байтов в машинном слове? Если да, в каких случаях; если нет, почему?
#. Требуется ли учитывать и менять порядок байтов для uint8_t
и float
?
Если да, каким образом; если нет, почему?
#. Ознакомьтесь с документацией на WSAStartup()
. Что означает первый параметр
функции? Как можно сформировать его без макроса MAKEWORD()
?
#. Как можно диагностировать ситуацию, когда до начала работы с API сокетов
не сделан вызов WSAStartup()
, и как проверять ошибки самой этой функции?
#. Что такое утечка ресурсов (приведите не менее трех примеров) и какие негативные последствия она может повлечь?