Конспект по использованию UNIX Socket DGRAM (2019 г)


UNIX Socket with DGRAM

/*
 Статья старая, но подробная: https://rsdn.org/article/unix/sockets.xml
 Статья про IP-сокеты, но подробная: https://www.softlab.ntua.gr/facilities/documentation/unix/unix-socket-faq/unix-socket-faq.html
 Когда окончательно запутаетесь в sockaddr_*: https://beej.us/guide/bgnet/html/multi/sockaddr_inman.html
 Тоже полезно: https://beej.us/guide/bgnet/html/multi/index.html
*/

Механника UNIX-сокетов довольно простая, но не без заморочек

Гадство в том, что в man 7 unix это всё описано слегка мутно, там не говорится как нужно делать, а только о том,
что делает каждая команда в отдельности и в общих чертах.

Если про STREAM-режим (с коннектами, с гарантированной последовательностью и доставкой) есть хоть какие (и не мало)
примеры и вопросы/ответы в инете, про SEQPACKET-режим (с коннектами, гарантированной последовательностью и доставкой и датаграммами)
пример есть прям в мануале, то по DGRAM-режиму примеров маловато.

А суть там, в целом, очень простая.

UNIX-сокеты - те же самые TCP/UDP по логике использования, но с тем отличием, что вместо IP-шника и номера порта
здесь в этом же смысле используются просто строковые имена. Мелкое отличие только в том, что если не вызывать
bind на IP-трафике, какой-то адрес у сокета всё равно будет, разве что порт будет случайным, а адрес будет включать
в себя какой-то из адресов (или все сразу) локальной системы. В случае UNIX-сокетов без bind имени нет, и, таким
образом, тебе ничего нельзя прислать (хотя сам ты можешь спамить).

/Есть даже ненулевая вероятность, что если обе стороны слушают адрес 0.0.0.0, то механизмы в ядре системы, обслуживающие
всё это дело, либо мало чем отличаются друг от друга либо это вообще на 99% один и тот же код. Хотя попробовал под линухом - он
0.0.0.0 транслирует в 127.0.0.1/

Причем есть два варианта: в linux можно использовать скрытые имена ("\0name\0") - это непортируемо, но не
использует файловую систему и автоматом считается, что такое имя может быть использовано сразу обеими сторонами
(или я протупил ?). И второй вариант: имя - это имя специального файла на обычной файловой системе (где нибудь в /tmp или /var/run).

Это имя создаётся вызовом bind сразу после вызова connect, и, также как и в случае удалённых соединений (ip/port),
свои имена должны иметь обе стороны (хотя, я не исключаю, что флаг REUSE на сокете позволяет вывернуться и с одним именем.
Только фиг пойми, как тогда быть с двусторонней связью, т.е. не получится ли так, что отправитель сможет тут же прочитать
свои же отправления, типа как в простом pipe ?).

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

После закрытия соединения специальный файл смысла уже не имеет и его можно удалить.
Для простоты удаление делается также и перед его созданием (на случай, если прога упала и не удалила после себя файл).
Такие файлы имеет смысл создавать во временных файловых системах. Хранить их вроде бы можно и на постоянных,
но bind, при попытке зацепиться на уже существующий файл, скажет "in use". Так что создать файл, раздать на него
права и юзать потом всю оставшуюся жизнь не получится.

Вся механника работы с DGRAM, таким образом, сводится к следующему:

сторона1					сторона2
socket						socket	- оба создают сокеты
bind						bind	- оба прилепляют к своим сокетам какие нибудь адекватные имена (но не совпадающие!)

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

Варианта есть два: можно общаться функциями типа read/write, но тогда сперва нужно сделать connect своей стороны на противоположную,
указав в connect уже таки имя противоположной стороны. Либо можно общаться функциями recvfrom/sendto - указывая имена уже им.

Фишка в том, что bind с именем файла нужен только для получения сообщений, а для отправки, в общем-то, в нём нужды нет.
Но такой случай не очень интересен практически. Хотя, почему бы и нет?

С практической точки зрения, для двухстороннего обмена, после bind, можно действовать так:

сервер						клиент
while (1) {					connect(fd, "serv_name");
  recvfrom(data_in, addr);			write(fd, data_out);
  process(data_in, &data_out);			read(fd, data_in);
  if (addr) sendto(data_out, addr);
}

Т.е. сервер просто отсылает ответ тому,		А клиент будет отсылать запрос серверу.
кто его прислал.

Если у вас не нет логического сервера, а, например, есть две симметричные стороны (две копии игрушки, например или чата):

сторона 1					сторона 2
while (1) {					while (1) {
  if (select(fd) > 0) read(fd, data_in);	  if (select(fd) > 0) read(fd, data_in);
  sendto(fd, data_out, "side2_addr");		  sendto(fd, data_out, "side2_addr");
}						}

Датаграммы в такой схеме удобны тем, что они позволяют чётко разделить начала/концы блоков данных, которыми вы обмениваетесь.
Нужно только помнить, что если вы отправляете, например, строку, то strlen вернёт её размер без учёта финального \0,
и если вы его не отправляете, то на приёмной стороне вы его должны добавить:

r = read(fd, buf, sizeof(buf));
buf[r] = '\0';

Следует отметить, что если в connect указать неверное имя файла (каталог, regular-file, ...) то вернётся внезапная ошибка Connection refused.
Ну а отстутствие файла возвращает более логичное сообщение: No such file or directory.

Если вам не вполне удобно использовать sendto (например, вам хочется использовать dprintf), вы можете временно подключить
сокет к нужному адресату, а затем вновь перевести его в режим прослушивания с любых адресов:

  int r;
  r = connect(us, (struct sockaddr *) &src_addr, addrlen); if (r < 0) perror("connect 1"); // чтобы следующий вывод шел тому, кто спросил
  dprintf(us, "testXXtest, fd=%d\n", us);

  struct sockaddr sa_null;
  sa_null.sa_family = AF_UNSPEC;
  r = connect(us, &sa_null, sizeof(sa_null)); if (r < 0) perror("connect 2"); // Снова слушаем всех подряд

Этот трюк доступен в Linux, но недоступен во FreeBSD (в 11.0, во всяком случае).

Все представленные ниже проги собираются под linux и под freebsd, но под FreeBSD не работает трюк с
unspec-connect (хотя проге это особенно не мешает).

Простой "клиент"

#include <stdio.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include <string.h> int main() { char sn_ser[] = "/tmp/aagge-serv"; char sn_cli[] = "/tmp/aagge-clie"; /* SOCK_DGRAM: server: bind(свой путь); client: bind(свой путь) connect(путь сервера); SOCK_SEQPACKET: server: bind, listen, accept; client: connect; */ int us = socket(AF_UNIX, SOCK_DGRAM, 0); if (us < 0) perror("socket"); struct sockaddr_un sa; memset(&sa, 0, sizeof(sa)); sa.sun_family = AF_UNIX; strncpy(sa.sun_path, sn_cli, sizeof(sa.sun_path)); unlink(sn_cli); if (bind(us, (struct sockaddr *) &sa, sizeof(sa)) < 0) perror("bind"); // if (listen(us, 20) < 0) perror("listen"); memset(&sa, 0, sizeof(sa)); sa.sun_family = AF_UNIX; strncpy(sa.sun_path, sn_ser, sizeof(sa.sun_path)); if (connect(us, (struct sockaddr *) &sa, sizeof(sa)) < 0) perror("connect"); // if (accept(us, NULL, NULL...) int r; printf("t1\n"); write(us, "testous1", 8); if (r < 0) perror("write"); printf("t2\n"); write(us, "testous2", 8); if (r < 0) perror("write"); printf("t3\n"); char inb[128]; r = read(us, inb, sizeof(inb)); inb[r] = '\0'; if (r < 0) perror("read"); else printf("%d: %s\n", r, inb); r = read(us, inb, sizeof(inb)); inb[r] = '\0'; if (r < 0) perror("read"); else printf("%d: %s\n", r, inb); close(us); // unlink(sn); // fgets(inb, sizeof(inb), f); // puts(inb); }

Простой "сервер" (отвечает "клиенту")

#define _WITH_DPRINTF #include <stdio.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include <string.h> int main() { char sn[] = "/tmp/aagge-serv"; /* SOCK_DGRAM: server: bind; client: connect; SOCK_SEQPACKET: server: bind, listen, accept; client: connect; */ int us = socket(AF_UNIX, SOCK_DGRAM, 0); if (us < 0) perror("socket"); struct sockaddr_un sa; memset(&sa, 0, sizeof(sa)); sa.sun_family = AF_UNIX; strncpy(sa.sun_path, sn , sizeof(sa.sun_path)); unlink(sn); if (bind(us, (struct sockaddr *) &sa, sizeof(sa)) < 0) perror("bind"); // if (listen(us, 20) < 0) perror("listen"); // if (connect(us, (struct sockaddr *) &sa, sizeof(sa)) < 0) perror("connect"); // if (accept(us, NULL, NULL...) for (int x = 1; x < 100; x++) { int r; char inb[128]; struct sockaddr_un src_addr; socklen_t addrlen = sizeof(src_addr); printf("1:family = %d, addrlen = %d\n", src_addr.sun_family, addrlen); r = recvfrom(us, inb, sizeof(inb), 0, (struct sockaddr *) &src_addr, &addrlen); inb[r] = '\0'; if (r < 0) perror("read"); else printf("%d: %s\n", r, inb); printf("2:family = %d, addrlen = %d, name = %s\n", src_addr.sun_family, addrlen, src_addr.sun_path); printf("t1\n"); // Можно так: r = connect(us, (struct sockaddr *) &src_addr, addrlen); if (r < 0) perror("connect 1"); // чтобы следующий вывод шел тому, кто спросил dprintf(us, "testXXtest, fd=%d\n", us); struct sockaddr sa_null; sa_null.sa_family = AF_UNSPEC; // Этот трюк проходит только в linux ! r = connect(us, &sa_null, sizeof(sa_null)); if (r < 0) perror("connect 2"); // Снова слушаем всех подряд // Можно попроще: // r = sendto(us, "testous1", 8, 0, (struct sockaddr *) &src_addr, addrlen); if (r < 0) perror("sendto"); printf("t2\n"); sleep(1); } close(us); unlink(sn); // fgets(inb, sizeof(inb), f); // puts(inb); }

Простой симметричный чат на датаграм-сокетах

// Простой чат по DGRAM-сокетам // В комстроке нужно указать свой адрес и адрес своего собеседника // Пустая строка - выход из обеих копий программы #include <stdio.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include <string.h> int main(int argc, char * argv[]) { if (argc < 3) { printf("Usage: %s self_addr remote_addr\n" "example: %s /tmp/my /tmp/your\n", argv[0], argv[0]); return 1; } int us = socket(AF_UNIX, SOCK_DGRAM, 0); if (us < 0) { perror("socket"); return 1; } struct sockaddr_un sa; memset(&sa, 0, sizeof(sa)); sa.sun_family = AF_UNIX; strncpy(sa.sun_path, argv[1], sizeof(sa.sun_path)); unlink(argv[1]); if (bind(us, (struct sockaddr *) &sa, sizeof(sa)) < 0) { perror("bind"); return 1; } printf("Ready...\n"); while (1) { fd_set set; struct timeval tv; int nfds = 0; FD_SET(0, &set); FD_SET(us, &set); tv.tv_sec = 60; tv.tv_usec = 0; int ret = select(us+1, &set, NULL, NULL, &tv); if (ret < 0) { perror("select"); return 1; } if (ret == 0) break; if (FD_ISSET(0, &set)) { char inb[128]; fgets(inb, sizeof(inb), stdin); char * n = rindex(inb, '\n'); if (n) *n = '\0'; // удаляем \n struct sockaddr_un sa; bzero(&sa, sizeof(sa)); sa.sun_family = AF_UNIX; strncpy(sa.sun_path, argv[2], sizeof(sa)); int r = sendto(us, inb, strlen(inb)+1, 0, (struct sockaddr *) &sa, sizeof(sa)); if (r < 0) { perror("sendto"); return 1; } if (strlen(inb) == 0) break; }; if (FD_ISSET(us, &set)) { char inb[128]; int r = read(us, inb, sizeof(inb)); if (r < 0) { perror("read"); return 1; } if (strlen(inb) == 0) break; puts(inb); } }; close(us); unlink(argv[1]); printf("Bye !\n"); }

Владимир