Unix Domain Socket: использование обмена датаграммой между одним серверным процессом и несколькими клиентскими процессами
Я хотел бы установить соединение IPC между несколькими процессами в Linux. Я никогда раньше не использовал сокеты UNIX, и поэтому я не знаю, правильно ли это относится к этой проблеме.
Один процесс получает данные (неформатированные, двоичные) и должен распространять эти данные через локальный сокет AF_UNIX, используя протокол датаграммы (то есть аналогичный UDP с AF_INET). Данные, отправленные из этого процесса в локальный сокет Unix, должны приниматься несколькими клиентами, которые прослушивают один и тот же сокет. Количество приемников может меняться.
Для этого используется следующий код для создания сокета и отправки ему данных (серверный процесс):
struct sockaddr_un ipcFile;
memset(&ipcFile, 0, sizeof(ipcFile));
ipcFile.sun_family = AF_UNIX;
strcpy(ipcFile.sun_path, filename.c_str());
int socket = socket(AF_UNIX, SOCK_DGRAM, 0);
bind(socket, (struct sockaddr *) &ipcFile, sizeof(ipcFile));
...
// buf contains the data, buflen contains the number of bytes
int bytes = write(socket, buf, buflen);
...
close(socket);
unlink(ipcFile.sun_path);
Эта запись возвращает -1 с сообщением errno ENOTCONN ( "Конечная точка транспорта не подключена" ). Я предполагаю, что это происходит потому, что в настоящее время ни один процесс приема не прослушивает этот локальный сокет, правильно?
Затем я попытался создать клиента, который подключается к этому сокету.
struct sockaddr_un ipcFile;
memset(&ipcFile, 0, sizeof(ipcFile));
ipcFile.sun_family = AF_UNIX;
strcpy(ipcFile.sun_path, filename.c_str());
int socket = socket(AF_UNIX, SOCK_DGRAM, 0);
bind(socket, (struct sockaddr *) &ipcFile, sizeof(ipcFile));
...
char buf[1024];
int bytes = read(socket, buf, sizeof(buf));
...
close(socket);
Здесь сбой не выполняется ( "Адрес уже используется" ). Итак, нужно ли устанавливать некоторые параметры сокетов, или это вообще неправильный подход?
Заранее благодарим за любые комментарии/решения!
Ответы
Ответ 1
Там есть трюк для использования сокетов UNIX datagram. В отличие от потоковых сокетов (домен tcp или unix), сокетам дейтаграмм нужны конечные точки, определенные как для сервера, так и для клиента. Когда устанавливается соединение в потоковых сокетах, конечная точка для клиента неявно создается операционной системой. Независимо от того, соответствует ли это эфемерному порту TCP/UDP или временному inode для домена unix, конечная точка для клиента создается для вас. Вот почему вам обычно не нужно вызывать вызов bind() для сокетов потока в клиенте.
Причина, по которой вы видите, что адрес уже используется, заключается в том, что вы говорите клиенту о привязке к тому же адресу, что и сервер. bind()
относится к утверждению внешнего тождества. Обычно два сокета не имеют одинакового имени.
С сокетами дейтаграмм, в частности сокетами UNIX домена дейтаграммы, клиент должен bind()
к своей конечной точке, а затем connect()
к конечной точке сервера. Вот ваш код клиента, слегка измененный, с некоторыми другими лакомствами, брошенными в:
char * server_filename = "/tmp/socket-server";
char * client_filename = "/tmp/socket-client";
struct sockaddr_un server_addr;
struct sockaddr_un client_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, server_filename, 104); // XXX: should be limited to about 104 characters, system dependent
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sun_family = AF_UNIX;
strncpy(client_addr.sun_path, client_filename, 104);
// get socket
int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
// bind client to client_filename
bind(sockfd, (struct sockaddr *) &client_addr, sizeof(client_addr));
// connect client to server_filename
connect(sockfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
...
char buf[1024];
int bytes = read(sockfd, buf, sizeof(buf));
...
close(sockfd);
В этот момент ваш сокет должен быть полностью настроен. Я думаю, что теоретически вы можете использовать read()
/write()
, но обычно я использую send()
/recv()
для сокетов датаграмм.
Обычно вам нужно проверить ошибку после каждого из этих вызовов и затем выпустить perror()
. Это очень поможет вам, когда все пойдет не так. В общем, используйте шаблон, подобный этому:
if ((sockfd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {
perror("socket failed");
}
Это касается практически любых системных вызовов C.
Лучшей ссылкой для этого является Стивен "Сетевое программирование Unix". В третьем издании, раздел 15.4, на страницах 415-419 приведены некоторые примеры и перечислены многие из предостережений.
Кстати, ссылаясь на
Я предполагаю, что это происходит потому, что в настоящий момент ни один процесс приема не прослушивает этот локальный сокет, правильно?
Я думаю, что вы правы в ошибке ENOTCONN от write()
на сервере. Разъем UDP обычно не будет жаловаться, потому что у него нет возможности узнать, прослушивает ли клиентский процесс. Однако сокеты datagram домена unix различны. Фактически, write()
фактически блокирует, если буфер приема клиента заполнен, а не удаляет пакет. Это делает сокеты datagram домена unix намного превосходящими UDP для IPC, потому что UDP, безусловно, будет отбрасывать пакеты при загрузке, даже на localhost. С другой стороны, это означает, что вы должны быть осторожны с быстрыми писателями и медленными читателями.
Ответ 2
Ближайшей причиной вашей ошибки является то, что write()
не знает, куда вы хотите отправить данные. bind()
устанавливает имя вашей стороны сокета - то есть. откуда поступают данные. Чтобы установить целевую сторону сокета, вы можете использовать connect()
; или вы можете использовать sendto()
вместо write()
.
Другая ошибка ( "Адрес уже используется" ) заключается в том, что только один процесс может bind()
на адрес.
Вам нужно будет изменить свой подход, чтобы принять это во внимание. Вашему серверу необходимо будет прослушивать по известному адресу, установленному с помощью bind()
. Вашим клиентам необходимо будет отправить сообщение на сервер по этому адресу, чтобы зарегистрировать свою заинтересованность в получении дейтаграмм. Сервер получит регистрационные сообщения от клиентов с помощью recvfrom()
и запишет адрес, используемый каждым клиентом. Когда он хочет отправить сообщение, ему придется перебирать все клиенты, о которых он знает, используя sendto()
для отправки сообщения каждому из них по очереди.
В качестве альтернативы вы можете использовать локальную многоадресную IP-сеть вместо сокетов домена UNIX (сокеты домена UNIX не поддерживают многоадресную рассылку).
Ответ 3
Если речь идет о вещании (как я понимаю), то согласно unix (4) - семейство протоколов UNIX, вещание недоступно в UNIX-сокетах UNIX:
Семейство протоколов Unix Ns -domain не поддерживает широковещательная адресация или любая форма подстановочного соответствия по входящим сообщениям. Все адреса являются абсолютными или относительные пути других сокетов Unix Ns-domain.
Может быть, многоадресная рассылка может быть вариантом, но я чувствую, что она недоступна с POSIX, хотя Linux поддерживает многоадресную рассылку доменов UNIX.
Также см.: Представление многоадресных сокетов Unix.
Ответ 4
Не было бы проще использовать разделяемую память или именованные каналы? Сокет - это соединение между двумя процессами (на той же или другой машине). Это не метод массовой коммуникации.
Если вы хотите что-то дать нескольким клиентам, вы создаете сервер, ожидающий подключения, а затем все клиенты могут подключиться и предоставить им информацию. Вы можете принимать одновременные соединения, делая программы многопоточными или процессами forking. Сервер устанавливает несколько соединений на основе сокетов с несколькими клиентами, вместо того, чтобы иметь один сокет, к которому подключаются несколько клиентов.
Ответ 5
Это произойдет из-за сервер или клиент умирают перед отключением/удалением для привязки файла bind(). любой клиент/сервер, использующий этот путь привязки, попробуйте снова запустить сервер.
решения:
когда вы хотите снова связать, просто проверьте, что файл уже ассоциирован, а затем отсоедините этот файл.
Как сделать шаг:
сначала проверить доступ к этому файлу путем доступа (2);
если да, то отсоедините (2) его.
поставьте этот мир кода перед вызовом bind(), позиция независима.
if(!access(filename.c_str()))
unlink(filename.c_str());
для дополнительной справки read unix (7)
Ответ 6
Вы должны заглянуть в многоадресную рассылку IP вместо домена Unix. В настоящее время вы просто пытаетесь писать в никуда. И если вы подключаетесь к одному клиенту, вы будете писать только этому клиенту.
Этот материал не работает так, как вы думаете.
Ответ 7
Вы можете решить ошибку привязки с помощью следующего кода:
int use = yesno;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char*)&use, sizeof(int));
С протоколом UDP вы должны вызывать connect()
, если вы хотите использовать write()
или send()
, иначе вы должны использовать sendto()
.
Для достижения ваших требований может понадобиться следующий псевдо-код:
sockfd = socket(AF_INET, SOCK_DGRAM, 0)
set RESUSEADDR with setsockopt
bind()
while (1) {
recvfrom()
sendto()
}