Конспект по использованию 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");
}
Владимир