Ответ 1
Интересно. Вы становитесь жертвой алгоритм Nagle вместе с Отложенные подтверждения TCP.
Алгоритм Nagle - это механизм, используемый в TCP для отсрочки передачи небольших сегментов до тех пор, пока не будет накоплено достаточное количество данных, что заставляет его строить и отправлять сегмент по сети. Из статьи в википедии:
Алгоритм Nagle работает, объединяя несколько небольших исходящих сообщений и отправки их сразу. В частности, до тех пор, пока представляет собой отправленный пакет, для которого отправитель не получил подтверждения, отправитель должен сохранять буферизацию своего вывода до тех пор, пока он не будет полностью пакетный вывод, так что вывод может быть отправлен сразу.
Однако TCP обычно использует что-то известное как TCP delayed подтверждения, которое представляет собой метод, который состоит из аккумулирования совокупности ответов ACK (поскольку TCP использует кумулятивный ACKS), чтобы уменьшить сетевой трафик.
В этой статье в Википедии упоминается следующее:
При использовании обоих алгоритмов приложения, выполняющие два последовательных записывает в TCP-соединение, а затем читает, что не будет выполняется до тех пор, пока данные второй записи не достигнут пункт назначения испытывает постоянную задержку до 500 миллисекунд, "Задержка ACK" .
(Акцент мой)
В вашем конкретном случае, поскольку сервер не отправляет больше данных перед чтением ответа, клиент вызывает задержку: если клиент дважды записывает, вторая запись будет отложена.
Если алгоритм Nagle используется стороной-отправителем, данные будут в очереди отправителя до получения ACK. Если отправитель не отправьте достаточно данных, чтобы заполнить максимальный размер сегмента (например, если он выполняет две небольшие записи, за которыми следует чтение блокировки), затем передача приостанавливается до тайм-аута задержки ACK.
Итак, когда клиент делает 2 вызова записи, это происходит:
- Клиент выдает первую запись.
- Сервер получает некоторые данные. Он не признает этого в надежде на то, что поступит больше данных (чтобы он мог объединить кучу ACK в одном ACK).
- Клиент выдает вторую запись. Предыдущая запись не была подтверждена, поэтому алгоритм Nagle отменяет передачу до тех пор, пока не поступит больше данных (пока не будет собрано достаточное количество данных для создания сегмента), или предыдущая запись ACKed.
- Сервер устал ждать и через 500 мс признает сегмент.
- Клиент, наконец, завершает вторую запись.
С записью 1 это происходит:
- Клиент выдает первую запись.
- Сервер получает некоторые данные. Он не признает этого в надежде на то, что поступит больше данных (чтобы он мог объединить кучу ACK в одном ACK).
- Сервер записывает в сокет.
ACK
является частью заголовка TCP, поэтому, если вы пишете, вы можете также признать предыдущий сегмент без каких-либо дополнительных затрат. Сделайте это. - Между тем, клиент написал один раз, так что он уже ожидал следующего чтения - не было второй записи, ожидающей сервер ACK.
Если вы хотите продолжать писать дважды на стороне клиента, вам нужно отключить алгоритм Nagle. Это решение, предложенное самим автором алгоритма:
Решение на уровне пользователя состоит в том, чтобы избежать Розетки. write-read-write-read - это нормально. write-write-write - это нормально. Но write-write-read - убийца. Итак, если вы можете, будьте осторожны записывает в TCP и отправляет их все сразу. Использование стандартного ввода-вывода UNIX упаковки и промывки перед тем, как каждое чтение обычно работает.
Как упоминалось в комментариях Дэвидом Шварцем, это может быть не самая лучшая идея по разным причинам, но она иллюстрирует суть и показывает, что это действительно вызывает задержка.
Чтобы отключить его, вам нужно установить опцию TCP_NODELAY
в сокетах с помощью setsockopt(2)
.
Это можно сделать в tcpConnectTo()
для клиента:
int tcpConnectTo(const char* server, const char* port)
{
struct sockaddr_in sa;
if(getsockaddr(server,port,(struct sockaddr*)&sa)<0) return -1;
int sock=tcpConnect(&sa); if(sock<0) return -1;
int val = 1;
if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) < 0)
perror("setsockopt(2) error");
return sock;
}
И в tcpAccept()
для сервера:
int tcpAccept(const char* port)
{
int listenSock, sock;
listenSock = tcpListenAny(port);
if((sock=accept(listenSock,0,0))<0) return fprintf(stderr,"Accept failed\n"),-1;
close(listenSock);
int val = 1;
if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) < 0)
perror("setsockopt(2) error");
return sock;
}
Интересно видеть огромную разницу в этом.
Если вы предпочитаете не вмешиваться в параметры сокета, достаточно убедиться, что клиент пишет один раз - и только один раз - перед следующим чтением. Вы все еще можете прочитать сервер дважды:
for(i=0;i<4000;++i)
{
if(amServer)
{ writeLoop(sock,buf,10);
//readLoop(sock,buf,20);
readLoop(sock,buf,10);
readLoop(sock,buf,10);
}else
{ readLoop(sock,buf,10);
writeLoop(sock,buf,20);
//writeLoop(sock,buf,10);
//writeLoop(sock,buf,10);
}
}