«Code Monkey like Fritos
Code Monkey like Tab and Mountain Dew
Code Monkey very simple man
With big warm fuzzy secret heart:
Code Monkey like you
Code Monkey like you»
— Jonathan Coulton — Code Monkey
Я думаю, многим знакома эта шикарная песня Jonathan Coulton'а, и эта жизненная ситуация, когда «Rob say Code Monkey very diligent», но «his output stink» и «his code not 'functional' or 'elegant'».
Язык Си, подаривший нам столько полезного софта, потихоньку был вытеснен из десктопа и энтерпрайза такими высокоуровневыми гигантами как Java и C# и занял нишу системного программирования. И все бы хорошо, но системщики — очень
Сегодня мы поговорим о некоторых полезных практиках, которые я вынес из глубин системного программирования на Си. Поехали.
Пункты будут располагаться от самых фундаментальных и очевидных (ориентированных на новичков в языке Си) до самых специфичных, но полезных. Если чувствуете, что вы это знаете — листайте дальше.
Практика I: Соблюдайте единый Code Style и фундаментальные принципы «хорошего тона»
Функция принимает в качестве аргумента переменную INPUT, парсит её в массив IncomingValues и возвращает result_to_return? Отставить быдлокод!
То, что в первую очередь выдает новичка — несоблюдение единого стиля написания кода в рамках конкретного приложения. Следом идет игнорирование правил «хорошего тона».
Вот несколько самых распространенных рекомендаций к оформлению кода на Си:
- Названия макросов и макрофункций пишутся капсом, слова в названиях отделяются друг от друга нижним подчеркиванием.
#define MAX_ARRAY_SIZE 32 #define INCORRECT_VALUE -1 #define IPC_FIND_NODE(x) ipc_find_node(config.x)
- Названия переменных записываются в нижнем регистре, а слова в названиях отделяются нижним подчеркиванием
int my_int_variable = 0; char *hello_str = "hello_habrahabr"; pid_t current_pid = fork();
Вообще, этот пункт спорный. Мне доводилось видеть проекты, где имена переменных и функций пишутся в camelCase и PascalCase соответственно.
UPD: Спасибо пользователю fogree за то, что он обнаружил косяк с перепутанным PascalCase и camelCase.
- Названия библиотечных функций общего пользования пишутся одним словом, иногда сокращенным, но это слово передает суть функции — что она должна делать.
Кстати, рекомендую взять за привычку писать код так, чтобы одна функция делала только одну вещь. Именно это должно отразиться в названии.
То же можно сказать и про переменные — никаких a, b, c — в названии должен быть отражен смысл (итераторы не в счет). Самодокументируемый код — очень хорошая практика.
- Специализированные функции (которые вызываются в пределах работы внутри какого-то специфичного контекста) лучше называть так, чтобы было определенно ясно, что делает эта функция.
Как правило, можно выбрать между стилем написания названия: PascalCase и under_score, тут уже зависит от вас.
/* пример функции общего пользования */ static void dgtprint(char *str) { int i; for (i = 0; i < strlen(str); i++) { if (isdigit(str[i])) printf("%c", str[i]); else print("_"); } } /* пример функции для работы со специфичным контекстом */ /* PascalCase */ void EnableAllVlans(struct vlan_cfg *vp) { int i; for (i = 0; i < VLAN_COUNT; i++) { EnableVlanByProto(vp.vlan[i]); } } /* under_score */ void enable_all_vlans(struct vlan_cfg *vp) { int i; for (i = 0; i < VLAN_COUNT; i++) { enable_vlan_by_proto(vp.vlan[i]); } }
- i, j, k — стандартные названия для итераторов цикла
int array[MAX_ARRAY_SIZE] = arrinit(); register int i, j, k; for (i = 0; i < MAX_ARRAY_SIZE; i++) for (j = 0; j < MAX_ARRAY_SIZE; j++) for (k = MAX_ARRAY_SIZE; k >= 0; k--) dosmthng(i, j, k, array[i]);
- Соблюдайте однородность переноса скобок
if (condition) { dosmthng(); } else { dont_do_something(); } /* Не делайте так */ if (condition) { dosmthng(); } else { dont_do_something(); } /* Гораздо правильнее будет следовать одному правилу переноса скобок, как тут */ if (condition) { dosmthng(); } else { dont_do_something(); } /* Или как тут */ /* Ну, или как тут, но это уже совсем экзотика */ if (condition) { dosmthng(); } else { dont_do_something(); }
- Объявляйте переменные в начале функции. Если это глобальные переменные, то в начале файла.
По возможности инициализируйте переменные при объявлении. Численные с помощью нуля, указатели — NULL:
int counter = 0, start_position = 0, unknown_position = 0; struct dhcp_header * dhcp = NULL, * dhcp_temp = NULL; char input_string[32] = { 0 };
Ну оставили мы переменные неинициализированными, и что?
А то. Если смотреть их (до инициализации) в отладке (в том же gdb), там будет лежать мусор. Это нередко сбивает с толку (особенно, если мусор «похож на правду»). Про указатели я вообще молчу.
- Пишите комментарии с умом.
Не надо комментировать каждую строчку кода — если вы пишите самодокументируемый код, большая часть его будет простой для понимания.
Оптимальное решение — писать описания функций, если из аргументов и названия сложно понять весь её функционал. Для переменных — правила те же, в пояснении нуждаются только какие-то нелинейный вещи, где одного названия мало.
На самом деле, в вопросе документации у вас есть полная свобода действий — надо лишь следить, чтобы комментариев было не много, но достаточно, чтобы человек, видящий ваш код в первый раз, не задавал вопросов.
/* Возвращает 1, если параметры, связанные с модемным * соединением изменились, и 0, если нет. */ static int CheckModemConnection() { int i = 0; /* Проверка сети отдельно - меняется чаще всего */ if (CHECK_CFG_STR(Network.LanIpAddress) || CHECK_CFG_STR(Network.LanNetmask)) return 1; for(i = 0; i < MAX_MODEM_IDX; i++) { if (CHECK_CFG_INT(Modems.Modem[i].Proto) || CHECK_CFG_INT(Modems.Modem[i].MTU) || CHECK_CFG_STR(Modems.Modem[i].Username) || CHECK_CFG_STR(Modems.Modem[i].Password) || CHECK_CFG_STR(Modems.Modem[i].Number) || CHECK_CFG_STR(Modems.Modem[i].AdditionalParams) || CHECK_CFG_STR(Modems.Modem[i].PIN) || CHECK_CFG_STR(Modems.Modem[i].MRU) || CHECK_CFG_STR(Modems.Modem[i].PppoeIdle) || CHECK_CFG_STR(Modems.Modem[i].USBPort) || CHECK_CFG_STR(Reservation.Prefer) || CHECK_CFG_STR(Modems.Modem[i].PppoeConnectType) || CHECK_CFG_INT(Modems.Mode) || CHECK_CFG_INT(Aggregation.usb1) || CHECK_CFG_INT(Aggregation.usb2)) return 1; } return 0; }
Если вы постоянно работаете с трекерами (вроде RedMine), то при внесении правок в код можно указать номер задачи, в рамках которой эти правки были внесены. Если у кого-то при просмотре кода возникнет вопрос а-ля «Зачем тут этот функционал?», ему не придется далеко ходить. В нашей компании еще пишут фамилию программиста, чтобы если что знать, к кому идти с расспросами.
/* Muraviyov: #66770 */
P.S. Для тех кто устраивается на работу: так же не следует забывать, что в каждой компании, как правило, используется свой Code Style, и ему нужно следовать. В противном сулучае можно получить как минимум укоризненные взгляды товарищей-разрабов или втык от начальства.
Практика II: Оптимизируйте структуру вашего проекта
Если у вас в проекте несколько файлов — имеет смысл хорошо подумать над структурой проекта.
Каждый проект уникален, но, тем не менее, существует ряд рекомендаций, которые помогут удобно структурировать проект:
- Называйте файлы так, чтобы всем было ясно, какой файл за что отвечает.
Не следует называть файлы file1.c, mySUPER_COOL_header.h и т.д.
main.c — для файла с точкой входа, graph_const.h — для заголовочника с графическими константами будет в самый раз.
- Храните заголовочники в директории include.
Рассмотрим пример:
- project/
- common.c
- common.h
- main.c
- network.h
- networking.c
- networking_v6.c
- packet.c
- packet.h
- Makefile
Если он точно не знает, какой файл ему нужен? Можно сберечь много нервов, если сделать так:
- project/
- include/
- common.h
- network.h
- packet.h
- common.c
- main.c
- networking.c
- networking_v6.c
- packet.c
- Makefile
- include/
Не забываем, что путь к директории include следует указать в параметрах сборки. Вот примерчик для простого Makefile (include в той же директории, что и Makefile):
@$(CC) $(OBJS) -o networkd -L$(ROMFS)/lib -linteraction -Wall -lpthread -I ./include
- project/
- Логически группируйте .c файлы в папки.
Если у вас игра, в которой есть файлы, отвечающие за движок/звук/графику — будет удобно раскидать их по папкам. Звук, графику и движок — отдельно друг от друга.
- Дополнение к предыдущему пункту — файлы сборки круто размещать в каждой из таких отдельных директорий, и просто вызывать их из файла сборки в корневой директории. В таком случае Makefile в корневой директории будет выглядеть примерно так:
.PHONY clean build build: cd sound/ && make clean && make cd graphics/ && make clean && make cd engine/ && make clean && make sound: cd sound/ && make clean && make graphics: cd graphics/ && make clean && make engine: cd engine/ && make clean && make clean: cd sound/ && make clean cd engine/ && make clean cd greaphics/ && make clean
Практика III: Используйте враппер-функции для обработки возвращаемых значений
Враппер-функция (функция-обертка) в языке Си используется как функция со встроенной обработкой возвращаемого значения. Как правило, в случае ошибки в работе функции, возвращаемое значение вам об этом скажет, а глобальная переменная errno примет в себя код ошибки.
Если вы пишите в системе (а сейчас большинство программ на си — именно системные программы), то нет ничего хуже, чем «немое» падение программы. По-хорошему, она должна красиво завершиться, напоследок сказав, что именно пошло не по плану.
Но обрабатывать значение от каждой функции в коде — такое себе решение. Тут же упадет читаемость, и объем (+ избыточность) кода увеличится в пару раз.
Тут и помогают врапперы. Рассмотрим первый пример — безопасный код без врапперов:
int sock_one = 0, sock_two = 0, sock_three = 0;
/* операция сравнения имеет больший приоритет, чем операция присваивания,
* поэтому присваивание выполняется в скобках
*/
if ((socket_one = socket(AF_INET , SOCK_STREAM , 0)) <= 0) {
perror("socket one");
exit(EXIT_ERROR_CODE);
}
if ((socket_two = socket(AF_INET , SOCK_DGRAM , 0)) <= 0) {
perror("socket two");
exit(EXIT_ERROR_CODE);
}
if ((socket_three = socket(PF_INET , SOCK_RAW , 0)) <= 0) {
perror("socket three");
exit(EXIT_ERROR_CODE);
}
Ну, такое себе, не правда ли? Теперь попробуем с обертками.
/* Где-то в коде... */
int Socket(int domain, int type, int proto) {
int desk = socket(domain, type, proto);
if (desk <= 0) {
perror("socket");
exit(EXIT_ERROR_CODE);
}
return desk;
}
/* ......... n строчек спустя - наш предыдущий пример ......... */
int socket_one = 0, socket_two = 0, soket_three = 0;
socket_one = Socket(AF_INET , SOCK_STREAM , 0);
socket_two = Socket(AF_INET , SOCK_DGRAM , 0);
socket_three = Socket(PF_INET , SOCK_RAW , 0);
Как видите, код по-прежнему безопасен (не будет «немого» падения), но теперь его функциональная часть гораздо компактнее.
Я называю обертки именем самих функций, но с большой буквы. Каждый сам волен выбрать, как их оформлять.
В использовании оберток есть небольшой минус, который, если захотеть, можно решить костылем. А что это за минус — можете предположить в комментариях :)
Практика IV: Используйте keywords как профи
Хорошее знание keywords никогда не будет лишним. Да, и без них ваш код будет работать, не спорю. Но когда речь зайдет об экономии места, быстродействии и оптимизации — это именно то, чего вам будет не хватать.
К тому же, мало кто может похвастаться хорошим знанием ключевых слов, поэтому их повседневное использование может быть шансом блеснуть знаниями перед коллегами. Однако, не надо бездумно пихать кейворды всюду, куда только можно. Вот вам несколько фич:
- register — дает компилятору указание по возможности хранить переменную в регистрах процессора, а не в оперативной памяти. Использование модификатора register при объявлении переменной-итератора цикла с небольшим телом может повысить скорость работы всего цикла в несколько раз.
register byte i = 0; for (i; i < 256; i++) check_value(i);
- restrict — при объявлении указателя дает компилятору гарантию (вы, как программист, гарантируете), что ни один указатель не будет указывать на область памяти, на которую указывает целевой указатель. Профит этого модификатора в том, что компилятору не придется проверять, не указывает ли какой-то еще указатель на целевой блок памяти. Если у вас внутри функции несколько указателей одного типа — возможно, он вам пригодится.
void updatePtrs(size_t *restrict ptrA, size_t *restrict ptrB, size_t *restrict val);
- volatile — указывает компилятору, что переменная может быть изменена неявным для него образом. Даже если компилятор пометит код, зависимый от волатильной переменной, как dead code (код, который никогда не будет выполнен), он не будет выброшен, и в рантайме выполнится в полном объеме.
int var = 1; if (!var) /* Эти 2 строчки будут отброшены компилятором */ dosmthng(); volatile int var = 1; if (!var) /* А вот эти - нет */ dosmthng();
И это только вершина айсберга. Различных модификаторов и ключевых слов — куча.
Практика V: Не доверяйте себе. Доверяйте valgrind.
Если у вас в программе есть работа со строками, динамическое выделение памяти и все, где замешаны указатели, то не будет лишним проверить себя.
Valgrind — программа, которая создана для того, чтобы помочь программисту выявить утечки памяти и ошибки контекста. Не буду вдаваться в подробности, скажу лишь, что даже в небольших программах он нередко находит косяки, которые совсем не очевидны для большинства программистов, но, тем не менее, в эксплуатации могут повлечь за собой большие проблемы. За всем не уследишь.
+ у нее есть и другой полезный функционал.
Более подробно о нем можно узнать тут.
Практика VI: Помогайте тем, кто хочет улучшить ваш софт
Пример будет взят из исходников busybox 1.21. Для тех кто не знает, что такое busybox, можете посмотреть эту вики-статью.
UPD: до этого здесь был пример «плохого» кода из busybox. Спасибо пользователю themiron за то, что показал, что этот код был понят мною неправильно — это были лишь тонкости реализации, причем реализации очень хорошей. В качестве извинения за свою «клевету» на busybox, здесь будет пример хорошего кода.
Причем, все так же из busybox.
Код busybox очень эллегантен, пусть и совсем не прост. Всем, кто хочет взглянуть на язык си под другим углом — рекомендую ознакомиться с исходниками.
Теперь обобщения по этому пункту на примерах из busybox. Все примеры взяты из udhcpc — крохотного DHCP клиента:
- Оставляй комментарии, там где они нужны.
Протокол DHCP имеет полную документацию в RFC, там описаны все возможные поля dhcp-пакета. Но, тем не менее, ребята озаботились и полностью задокументировали даже поля структуры. Эта структура — первое, на что посмотрит программист, расширяющий функционал программы (DHCP-клиент <-> DHCP-пакет).
(файл networking/udhcp/common.h)
struct dhcp_packet { uint8_t op; /* BOOTREQUEST or BOOTREPLY */ uint8_t htype; /* hardware address type. 1 = 10mb ethernet */ uint8_t hlen; /* hardware address length */ uint8_t hops; /* used by relay agents only */ uint32_t xid; /* unique id */ uint16_t secs; /* elapsed since client began acquisition/renewal */ uint16_t flags; /* only one flag so far: */ #define BROADCAST_FLAG 0x8000 /* "I need broadcast replies" */ uint32_t ciaddr; /* client IP (if client is in BOUND, RENEW or REBINDING state) */ uint32_t yiaddr; /* 'your' (client) IP address */ /* IP address of next server to use in bootstrap, returned in DHCPOFFER, DHCPACK by server */ uint32_t siaddr_nip; uint32_t gateway_nip; /* relay agent IP address */ uint8_t chaddr[16]; /* link-layer client hardware address (MAC) */ uint8_t sname[64]; /* server host name (ASCIZ) */ uint8_t file[128]; /* boot file name (ASCIZ) */ uint32_t cookie; /* fixed first four option bytes (99,130,83,99 dec) */ uint8_t options[DHCP_OPTIONS_BUFSIZE + CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS]; } PACKED;
- Держи однотипные и зависимые друг от друга вещи поблизости.
Листинг выше частично вошел сюда, т.к. подходит для еще одного примера.
Посмотрите: описание упакованных структур идет перед перечислением, отражающим размеры этих структур.
(файл networking/udhcp/common.h)
struct dhcp_packet { uint8_t op; /* BOOTREQUEST or BOOTREPLY */ uint8_t htype; /* hardware address type. 1 = 10mb ethernet */ uint8_t hlen; /* hardware address length */ uint8_t hops; /* used by relay agents only */ uint32_t xid; /* unique id */ uint16_t secs; /* elapsed since client began acquisition/renewal */ uint16_t flags; /* only one flag so far: */ #define BROADCAST_FLAG 0x8000 /* "I need broadcast replies" */ uint32_t ciaddr; /* client IP (if client is in BOUND, RENEW or REBINDING state) */ uint32_t yiaddr; /* 'your' (client) IP address */ /* IP address of next server to use in bootstrap, returned in DHCPOFFER, DHCPACK by server */ uint32_t siaddr_nip; uint32_t gateway_nip; /* relay agent IP address */ uint8_t chaddr[16]; /* link-layer client hardware address (MAC) */ uint8_t sname[64]; /* server host name (ASCIZ) */ uint8_t file[128]; /* boot file name (ASCIZ) */ uint32_t cookie; /* fixed first four option bytes (99,130,83,99 dec) */ uint8_t options[DHCP_OPTIONS_BUFSIZE + CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS]; } PACKED; #define DHCP_PKT_SNAME_LEN 64 #define DHCP_PKT_FILE_LEN 128 #define DHCP_PKT_SNAME_LEN_STR "64" #define DHCP_PKT_FILE_LEN_STR "128" struct ip_udp_dhcp_packet { struct iphdr ip; struct udphdr udp; struct dhcp_packet data; } PACKED; struct udp_dhcp_packet { struct udphdr udp; struct dhcp_packet data; } PACKED; enum { IP_UDP_DHCP_SIZE = sizeof(struct ip_udp_dhcp_packet) - CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS, UDP_DHCP_SIZE = sizeof(struct udp_dhcp_packet) - CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS, DHCP_SIZE = sizeof(struct dhcp_packet) - CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS, };
Если в этом же файле мы спустимся чуть пониже, то увидим, что объявления функций работы с опциями так же находятся в одном месте:
(файл networking/udhcp/common.h)
unsigned FAST_FUNC udhcp_option_idx(const char *name); uint8_t *udhcp_get_option(struct dhcp_packet *packet, int code) FAST_FUNC; int udhcp_end_option(uint8_t *optionptr) FAST_FUNC; void udhcp_add_binary_option(struct dhcp_packet *packet, uint8_t *addopt) FAST_FUNC; void udhcp_add_simple_option(struct dhcp_packet *packet, uint8_t code, uint32_t data) FAST_FUNC; #if ENABLE_FEATURE_UDHCP_RFC3397 char *dname_dec(const uint8_t *cstr, int clen, const char *pre) FAST_FUNC; uint8_t *dname_enc(const uint8_t *cstr, int clen, const char *src, int *retlen) FAST_FUNC; #endif struct option_set *udhcp_find_option(struct option_set *opt_list, uint8_t code) FAST_FUNC;
- Не убирай безвозвратно неиспользуемый по умолчанию функционал
В udhcpc есть огромное количество опций, которые по умолчанию не используются. Каждая опция соответствует макросу, который закрывает ее номер.
Если бы они были раскомментированы — то это были бы макросы, которые не всплывают нигде в коде. Человек, который спросил бы «а зачем все эти опции?» искал бы ответ очень долго. И не нашел бы.
Как итог — у нас получилось подобие интерфейса, где комментарием закрыты те опции, методы для которых еще не реализованы.
(файл networking/udhcp/common.h)
#define DHCP_PADDING 0x00 #define DHCP_SUBNET 0x01 //#define DHCP_TIME_OFFSET 0x02 /* (localtime - UTC_time) in seconds. signed */ //#define DHCP_ROUTER 0x03 //#define DHCP_TIME_SERVER 0x04 /* RFC 868 time server (32-bit, 0 = 1.1.1900) */ //#define DHCP_NAME_SERVER 0x05 /* IEN 116 _really_ ancient kind of NS */ //#define DHCP_DNS_SERVER 0x06 //#define DHCP_LOG_SERVER 0x07 /* port 704 UDP log (not syslog) //#define DHCP_COOKIE_SERVER 0x08 /* "quote of the day" server */ //#define DHCP_LPR_SERVER 0x09 #define DHCP_HOST_NAME 0x0c /* either client informs server or server gives name to client */ //#define DHCP_BOOT_SIZE 0x0d //#define DHCP_DOMAIN_NAME 0x0f /* server gives domain suffix */ //#define DHCP_SWAP_SERVER 0x10 //#define DHCP_ROOT_PATH 0x11 //#define DHCP_IP_TTL 0x17 //#define DHCP_MTU 0x1a //#define DHCP_BROADCAST 0x1c //#define DHCP_ROUTES 0x21 //#define DHCP_NIS_DOMAIN 0x28 //#define DHCP_NIS_SERVER 0x29 //#define DHCP_NTP_SERVER 0x2a //#define DHCP_WINS_SERVER 0x2c #define DHCP_REQUESTED_IP 0x32 /* sent by client if specific IP is wanted */ #define DHCP_LEASE_TIME 0x33 #define DHCP_OPTION_OVERLOAD 0x34 #define DHCP_MESSAGE_TYPE 0x35 #define DHCP_SERVER_ID 0x36 /* by default server's IP */ #define DHCP_PARAM_REQ 0x37 /* list of options client wants */ //#define DHCP_ERR_MESSAGE 0x38 /* error message when sending NAK etc */ #define DHCP_MAX_SIZE 0x39 #define DHCP_VENDOR 0x3c /* client's vendor (a string) */ #define DHCP_CLIENT_ID 0x3d /* by default client's MAC addr, but may be arbitrarily long */ //#define DHCP_TFTP_SERVER_NAME 0x42 /* same as 'sname' field */ //#define DHCP_BOOT_FILE 0x43 /* same as 'file' field */ //#define DHCP_USER_CLASS 0x4d /* RFC 3004. set of LASCII strings. "I am a printer" etc */ #define DHCP_FQDN 0x51 /* client asks to update DNS to map its FQDN to its new IP */ //#define DHCP_DOMAIN_SEARCH 0x77 /* RFC 3397. set of ASCIZ string, DNS-style compressed */ //#define DHCP_SIP_SERVERS 0x78 /* RFC 3361. flag byte, then: 0: domain names, 1: IP addrs */ //#define DHCP_STATIC_ROUTES 0x79 /* RFC 3442. (mask,ip,router) tuples */ #define DHCP_VLAN_ID 0x84 /* 802.1P VLAN ID */ #define DHCP_VLAN_PRIORITY 0x85 /* 802.1Q VLAN priority */ //#define DHCP_MS_STATIC_ROUTES 0xf9 /* Microsoft's pre-RFC 3442 code for 0x79? */ //#define DHCP_WPAD 0xfc /* MSIE's Web Proxy Autodiscovery Protocol */ #define DHCP_END 0xff
- Инкапсулируй подобные функции в зависимости от предназначения.
Довольно сложный для восприятия аспект, требующий пояснения. В двух словах: если у вас есть функция, которая внутри проекта вызывается с n комбинациями различных параметров (где n — небольшое число), причем каждая комбинация вызывается по нескольку раз, имеет смысл сделать для каждой комбинации отдельную функцию, вызывающую внутри себя целевую функцию, но уже с нужными параметрами. Например. У нас есть функция, отправляющая пакеты на определенный порт и адрес:
(файл networking/udhcp/packet.c)
/* Construct a ip/udp header for a packet, send packet */ int FAST_FUNC udhcp_send_raw_packet(struct dhcp_packet *dhcp_pkt, uint32_t source_nip, int source_port, uint32_t dest_nip, int dest_port, const uint8_t *dest_arp, int ifindex) { struct sockaddr_ll dest_sll; struct ip_udp_dhcp_packet packet; unsigned padding; int fd; int result = -1; const char *msg; fd = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP)); if (fd < 0) { msg = "socket(%s)"; goto ret_msg; } memset(&dest_sll, 0, sizeof(dest_sll)); memset(&packet, 0, offsetof(struct ip_udp_dhcp_packet, data)); packet.data = *dhcp_pkt; /* struct copy */ dest_sll.sll_family = AF_PACKET; dest_sll.sll_protocol = htons(ETH_P_IP); dest_sll.sll_ifindex = ifindex; dest_sll.sll_halen = 6; memcpy(dest_sll.sll_addr, dest_arp, 6); if (bind(fd, (struct sockaddr *)&dest_sll, sizeof(dest_sll)) < 0) { msg = "bind(%s)"; goto ret_close; } /* We were sending full-sized DHCP packets (zero padded), * but some badly configured servers were seen dropping them. * Apparently they drop all DHCP packets >576 *ethernet* octets big, * whereas they may only drop packets >576 *IP* octets big * (which for typical Ethernet II means 590 octets: 6+6+2 + 576). * * In order to work with those buggy servers, * we truncate packets after end option byte. */ padding = DHCP_OPTIONS_BUFSIZE - 1 - udhcp_end_option(packet.data.options); packet.ip.protocol = IPPROTO_UDP; packet.ip.saddr = source_nip; packet.ip.daddr = dest_nip; packet.udp.source = htons(source_port); packet.udp.dest = htons(dest_port); /* size, excluding IP header: */ packet.udp.len = htons(UDP_DHCP_SIZE - padding); /* for UDP checksumming, ip.len is set to UDP packet len */ packet.ip.tot_len = packet.udp.len; packet.udp.check = inet_cksum((uint16_t *)&packet, IP_UDP_DHCP_SIZE - padding); /* but for sending, it is set to IP packet len */ packet.ip.tot_len = htons(IP_UDP_DHCP_SIZE - padding); packet.ip.ihl = sizeof(packet.ip) >> 2; packet.ip.version = IPVERSION; packet.ip.ttl = IPDEFTTL; packet.ip.check = inet_cksum((uint16_t *)&packet.ip, sizeof(packet.ip)); udhcp_dump_packet(dhcp_pkt); result = sendto(fd, &packet, IP_UDP_DHCP_SIZE - padding, /*flags:*/ 0, (struct sockaddr *) &dest_sll, sizeof(dest_sll)); msg = "sendto"; ret_close: close(fd); if (result < 0) { ret_msg: bb_perror_msg(msg, "PACKET"); } return result; }
Но dhcp не всегда нуждается в отправке пакета на один IP адрес. В основном используется широковещательная (BROADCAST) рассылка.
Но широковещательная отправка пакета — всего лишь отправка пакета по адресу, зарезервированному под broadcast. Собственно, для того, чтоб отправить широковещательный запрос, достаточно использовать описанную выше функцию, но в качестве адреса указать тот, что зарезервирован про бродкаст. Отсюда получаем функцию:
(файл networking/udhcp/dhcpc.c)
static int raw_bcast_from_client_config_ifindex(struct dhcp_packet *packet) { return udhcp_send_raw_packet(packet, /*src*/ INADDR_ANY, CLIENT_PORT, /*dst*/ INADDR_BROADCAST, SERVER_PORT, MAC_BCAST_ADDR, client_config.ifindex); }
Профит этого в том, что везде, где мы будем встречать эту функцию, можно будет по названию понять, что она делает. Если бы мы использовали функцию udhcp_send_raw_packet, то нам бы осталось только гадать по параметрам.
Заключение
Пиши код так, чтобы те, кто будет его сопровождать любили тебя, а не ненавидели. Сложная гибкая реализация гораздо лучше простого костыля.
Описывай интерфейсы доступа, комментируй проблемные моменты. Не делай констант, от изменения которых придется переписывать весь код. Не допускай утечек памяти. Следи за безопасностью и отказоустойчивостью кода.
Пиши на Си как джентльмен.
Удачи, Хабр!