Unix Domain Socket: использование дейтаграммы между одним серверным процессом и несколькими клиентскими процессами - PullRequest
33 голосов
/ 24 июля 2010

Я хотел бы установить соединение 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, сообщая об ошибке 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);

Здесь связывание не выполняется («Адрес уже используется»). Итак, мне нужно установить некоторые параметры сокетов, или это вообще неправильный подход?

Заранее спасибо за любые комментарии / решения!

Ответы [ 7 ]

38 голосов
/ 21 апреля 2012

Есть хитрость в использовании сокетов датаграмм unix. В отличие от потоковых сокетов (домен tcp или unix), сокетам дейтаграмм необходимы конечные точки, определенные как для сервера, так и для клиента. Когда устанавливается соединение в потоковых сокетах, операционная система неявно создает конечную точку для клиента. Соответствует ли это эфемерному порту TCP / UDP или временному индоду для домена 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");
}

Это относится практически ко всем системным вызовам Си.

Лучшим примером для этого является "Сетевое программирование Unix" Стивена. В 3-м издании, раздел 15.4, на страницах 415-419 приведены некоторые примеры и перечислены многие предостережения.

Кстати, со ссылкой на

Полагаю, это потому, что ни один процесс получения не прослушивает этот локальный сокет, верно?

Я думаю, что вы правы насчет ошибки ENOTCONN от write() на сервере. Сокет UDP обычно не будет жаловаться, потому что у него нет возможности узнать, слушает ли клиентский процесс. Однако сокеты датаграмм домена unix отличаются. Фактически, write() будет фактически блокироваться, если приемный буфер клиента заполнен, а не отбрасывать пакет. Это делает сокеты дейтаграмм в домене Unix намного лучше, чем UDP для IPC, потому что UDP, безусловно, будет отбрасывать пакеты при загрузке даже на локальном хосте. С другой стороны, это означает, что вы должны быть осторожны с быстрыми писателями и медленными читателями.

7 голосов
/ 25 июля 2010

Непосредственной причиной вашей ошибки является то, что write() не знает, куда вы хотите отправить данные в . bind() устанавливает имя вашей стороны сокета - т.е. откуда поступают данные от . Чтобы установить целевую сторону сокета, вы можете использовать connect(); или вы можете использовать sendto() вместо write().

Другая ошибка («Адрес уже используется») заключается в том, что только один процесс может bind() получить адрес.

Вам нужно будет изменить свой подход, чтобы учесть это. Ваш сервер должен будет прослушивать общеизвестный адрес, установленный с помощью bind(). Ваши клиенты должны будут отправить сообщение на сервер по этому адресу, чтобы зарегистрировать свою заинтересованность в получении дейтаграмм. Сервер получит сообщения о регистрации от клиентов, используя recvfrom(), и запишет адрес, используемый каждым клиентом. Когда он хочет отправить сообщение, ему придется перебрать всех известных ему клиентов, используя sendto() для отправки сообщения каждому по очереди.

В качестве альтернативы вы можете использовать локальную IP-рассылку вместо доменных сокетов UNIX (доменные сокеты UNIX не поддерживают многоадресную рассылку).

2 голосов
/ 29 марта 2013

Если вопрос предназначен для вещания (насколько я понимаю), то в соответствии с unix (4) - семейство протоколов домена UNIX , широковещание недоступно для доменных сокетов UNIX:

Семейство протоколов Unix Ns -domain не поддерживает широковещательная адресация или любая форма подстановочного соответствия на входящие сообщения. Все адреса являются абсолютными или относительные пути других Unix Ns -доменовых сокетов.

Может быть многоадресная рассылка, но я чувствую, что она недоступна в POSIX, хотя Linux поддерживает многоадресную рассылку сокетов UNIX .

Также см .: Представляем многоадресные сокеты Unix .

0 голосов
/ 25 мая 2016

Это произойдет из-за сервер или клиент умирают перед тем, как отсоединить / удалить для связывания файла bind (). любой клиент / сервер, использующий этот путь привязки, попробуйте снова запустить сервер.

решения: если вы хотите снова связать, просто проверьте, что файл уже связан, затем отсоедините этот файл. Как сделать шаг: сначала проверьте доступ к этому файлу с помощью access (2); если да, то отмените связь (2). поместите этот код перед вызовом bind (), позиция независима.

 if(!access(filename.c_str()))
    unlink(filename.c_str());

для дополнительной информации читайте unix (7)

0 голосов
/ 02 сентября 2010

Вы можете устранить ошибку связывания с помощью следующего кода:

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()
}
0 голосов
/ 24 июля 2010

Не проще ли использовать разделяемую память или именованные каналы?Сокет - это соединение между двумя процессами (на одном или другом компьютере).Это не метод массовой коммуникации.

Если вы хотите дать что-то нескольким клиентам, вы создаете сервер, который ожидает подключения, а затем все клиенты могут подключиться, и он дает им информацию.Вы можете принимать параллельные соединения, делая программу многопоточной или разветвляя процессы.Сервер устанавливает несколько соединений на основе сокетов с несколькими клиентами, а не с одним сокетом, к которому подключаются несколько клиентов.

0 голосов
/ 24 июля 2010

Вы должны смотреть на IP многоадресную рассылку вместо Unix-домена. В настоящее время вы просто пытаетесь написать в никуда. И если вы подключитесь к одному клиенту, вы будете писать только этому клиенту.

Этот материал не работает так, как вы, кажется, думаете.

...