Работать в пятницу после обеда первого апреля не хочется — вдруг ещё техника выкинет какую-нибудь шутку. Потому решил о чем-либо написать.
Не так давно на просторах хабра в одной статье огульно охаяли сразу Unix-сокеты, mysql, php и redis. Говорить обо всём в одной статье не будем, остановимся на сокетах и немного на redis.
Итак вопрос: что быстрее Unix- или TCP-сокеты?
Вопрос, который не стоит и выеденного яйца, однако, постоянно муссируемый и писать не стал бы если б не опрос в той самой статье, согласно которому едва-ли не половина респондентов считает, что лучше/надёжнее/стабильнее использовать TCP-сокеты.
Тем, кто и так выбирает AF_UNIX, можно дальше не читать.

Начнём с краткой выжимки из теории.
Сокет — один из интерфейсов межпроцессного взаимодействия, позволяющий разрабатывать клиент-серверные системы для локального или сетевого использования. Так как мы рассматриваем в сравнении (с одной стороны) Unix-сокеты, то в дальнейшем будем говорить об IPC в пределах одной машины.
В отличии от именованных каналов, при использовании сокетов прослеживается отличие между клиентом и сервером. Механизм сокетов позволяет создавать сервер к которому подключается множество клиентов.

Как реализуется взаимодействие со стороны сервера:
— системный вызов socket создаёт сокет, но этот сокет не может использоваться совместно с другими процессами;
— сокет именуется. Для локальных сокетов домена AF_UNIX(AF_LOCAL) адрес будет задан именем файла. Сетевые сокеты AF_INET именуются в соответствии с их ip/портом;
— системный вызов listen(int socket, int backlog) формирует очередь входящих подключений. Второй параметр backlog определяет длину этой очереди;
— эти подключения сервер принимает с помощью вызова accept, который создаёт новый сокет, отличающийся от именованного сокета. Этот новый сокет применяется только для взаимодействия с данным конкретным клиентом.

С точки зрения клиента подключение происходит несколько проще:
— вызывается socket;
— и connect, используя в качестве адреса именованный сокет сервера.

Остановимся внимательнее на вызове int socket(int domain, int type, int protocol) второй параметр которого определяет тип обмена данными используемого с этим сокетом. В нашем сравнении мы будем рассматривать его возможное значение SOCK_STREAM, являющееся надежным, упорядоченным двунаправленным потоком байтов. То есть в рассмотрении участвуют сокеты вида
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
и
sockfd = socket(AF_INET, SOCK_STREAM, 0);

Структура сокета в домене AF_UNIX проста:
struct sockaddr_un {
        unsigned char   sun_len;        /* sockaddr len including null */
        sa_family_t     sun_family;     /* AF_UNIX */
        char    sun_path[104];          /* path name (gag) */
};

В домене AF_INET несколько сложнее:
struct sockaddr_in {
        uint8_t sin_len;
        sa_family_t     sin_family;
        in_port_t       sin_port;
        struct  in_addr sin_addr;
        char    sin_zero[8];
};

и на её заполнение мы понесём дополнительные расходы. В частности, это могут быть расходы на ресолвинг (gethostbyname) и/или выяснение того с какой стороны разбивать яйца (htons).

Также сокеты в домене AF_INET, несмотря на обращение к localhost, «не знают» того, что они работают на локальной системе. ��ем самым они не прилагают никаких усилий, чтобы обойти механизмы сетевого стека для увеличения производительности. Таким образом мы «оплачиваем» усилия на переключения контекста, ACK, TCP управление потоком, маршрутизацию, разбиение больших пакетов и т.п. То есть это «полноценная TCP работа» несмотря на то, что мы работаем на локальном интерфейсе.

В свою очередь сокеты AF_UNIX «осознают», что они работают внутри одной системы. Они избегают усилий на установку ip-заголовков, роутинг, расчёт контрольных сумм и т.д. Кроме того, раз в домене AF_UNIX используется файловая система в качестве адресного пространства, мы получаем бонус в виде возможности использования прав доступа к файлам и управления доступа к ним. Тем самым мы можем без существенных усилий ограничивать процессам доступ к сокетам и, опять же, не несём затрат на этапы обсепечения безопасности.

Проверим теорию на практике.
Мне лень писать серверную часть, потому воспользуюсь тем же redis-server. Его функционал отлично для этого подходит и заодно проверим справедливы ли были обвинения в его адрес. Клиентские части набросаем свои. Будем выполнять простейшую команду INCR со сложностью O(1).
Создание сокетов намеренно помещаем внутри циклов.
TCP-клиент:
AF_INET
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/socket.h>

#include <netdb.h>
#include <netinet/in.h>

#include <string.h>

int main(int argc, char *argv[]) {
   int sockfd, portno, n;
   struct sockaddr_in serv_addr;
   struct hostent *server;
   
   char buffer[256];
   
   if (argc < 4) {
      fprintf(stderr,"usage %s hostname port count_req\n", argv[0]);
      exit(0);
   }
	
   portno = atoi(argv[2]);
   
   int i=0;
   int ci = atoi(argv[3]);
   for(i; i < ci; i++)
   {
      
      sockfd = socket(AF_INET, SOCK_STREAM, 0);
      
      if (sockfd < 0) {
         perror("ERROR opening socket");
         exit(1);
      }
           
      server = gethostbyname(argv[1]);
      
      if (server == NULL) {
         fprintf(stderr,"ERROR, no such host\n");
         exit(0);
      }
      
      bzero((char *) &serv_addr, sizeof(serv_addr));
      serv_addr.sin_family = AF_INET;
      bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length);
      serv_addr.sin_port = htons(portno);
      
      if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
         perror("ERROR connecting");
         exit(1);
      }
      
      char str[] = "*2\r\n$4\r\nincr\r\n$3\r\nfoo\r\n";
      int len = sizeof(str);
      bzero(buffer, len);
      memcpy ( buffer, str, len );

      n = write(sockfd, buffer, strlen(buffer));
      
      if (n < 0) {
         perror("ERROR writing to socket");
         exit(1);
      }
      
      bzero(buffer,256);
      n = read(sockfd, buffer, 255);
      
      if (n < 0) {
         perror("ERROR reading from socket");
         exit(1);
      }
      
      printf("%s\n",buffer);
      close(sockfd);
   }
   return 0;
}


UNIX-клиент:
AF_UNIX
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/socket.h>

#include <sys/un.h>

#include <string.h>

int main(int argc, char *argv[]) {
   int sockfd, portno, n;
   struct sockaddr_un serv_addr;
   struct hostent *server;
   
   char buffer[256];
   
   if (argc < 1) {
      fprintf(stderr,"usage %s count_req\n", argv[0]);
      exit(0);
   }
	
   int i=0;
   int ci = atoi(argv[1]);
   for(i; i < ci; i++)
   {
      
      sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
      
      if (sockfd < 0) {
         perror("ERROR opening socket");
         exit(1);
      }
      
      bzero((char *) &serv_addr, sizeof(serv_addr));
      serv_addr.sun_family = AF_UNIX;
      strcpy(serv_addr.sun_path, "/tmp/redis.sock");
      
      if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
         perror("ERROR connecting");
         exit(1);
      }
      
      char str[] = "*2\r\n$4\r\nincr\r\n$3\r\nfoo\r\n";
      int len = sizeof(str);
      bzero(buffer, len);
      memcpy ( buffer, str, len );
      
      n = write(sockfd, buffer, strlen(buffer));
      
      if (n < 0) {
         perror("ERROR writing to socket");
         exit(1);
      }
      
      bzero(buffer,256);
      n = read(sockfd, buffer, 255);
      
      if (n < 0) {
         perror("ERROR reading from socket");
         exit(1);
      }
      
      printf("%s\n",buffer);
      close(sockfd);
   }
   return 0;
}


Тестируем с одним клиентом:
# redis-cli set foo 0 ; time ./redistcp 127.0.0.1 6379 1000000 > /dev/null ; redis-cli get foo
OK
2.108u 21.991s 1:13.75 32.6%    9+158k 0+0io 0pf+0w
"1000000"

# redis-cli set foo 0 ; time ./redisunix 1000000 > /dev/null ; redis-cli get foo
OK
0.688u 9.806s 0:36.90 28.4%     4+151k 0+0io 0pf+0w
"1000000"


И теперь для двадцати паралелльных клиентов отправляющих 500000 запросов каждый.
для TCP: 6:12.86
# redis-cli info Commandstats
cmdstat_set:calls=1,usec=5,usec_per_call=5.00
cmdstat_incr:calls=10000000,usec=24684314,usec_per_call=2.47

для UNIX: 4:11.23
# redis-cli info Commandstats
cmdstat_set:calls=1,usec=8,usec_per_call=8.00
cmdstat_incr:calls=10000000,usec=22258069,usec_per_call=2.23


Тем самым, в целом, аргументами в пользу TCP-сокетов может служить лишь мобильность применения и возможность простого масштабирования. Но если же вам требуется работа в пределах одной машины, то выбор, безусловно, в пользу UNIX-сокетов. Поэтому выбор между TCP- и UNIX-сокетами — это, в первую очередь, выбор между переносимостью и производительностью.

На сим предлагаю любить Unix-сокеты, а вопросы тупоконечностей оставить жителям Лилипутии и Блефуску.