18-TCP使用的注意事项

SIGPIPE

  • 如果程序向已关闭的管道写数据,返回SIGPIPE信号

    • SIGPIPE信号的默认行为是终止进程。对于命令行通道有好处

      • gunzip -c huge.log.gz | grep ERROR | head 。(如果管道末端的命令执行失败,那么整个管道的程序将会依次收到SIGPIPE。避免了无效的计算。)

  • 当客户端关闭socket时,而服务端仍然往socket写数据会发生什么?

    • client could be offensive or misbehaving, server should be defensive(不懂)

  • SIGPIPE在网络编程中的危害:

    • 当一个客户端关闭后,服务端向其发送数据,导致SIGPIPE信号发生,导致服务器终止运行,从而影响所有的客户端

  • 所以,在网络编程中通常忽略SIGPIPE(muduo中是通过全局对象构造来做到这一点)

    • 另外,需要注意的是,忽略SIGPIPE后,如果对方关闭了连接,我们的程序可能不会退出,因此,我们需要额外关注一些函数的返回值,例如我们向一个连接请求数据后将它输出到标准输出时,我们需要额外关注printf 函数的返回值,如果它的返回值是负值,则表示对端连接已关闭。那么我们的程序此刻应该退出。

Nagle算法、TCP_NODELAY

  • 影响请求-相应协议的延迟

  • write有可能会被延迟(将多个write的数据存在一起再发送)

  • 建议关闭Nagle算法,即设置TCP_NODELYA选项

 

Nagle算法:数据只有在写缓存中累积到一定量之后,才会被发送出去,这样明显提高了网络利用率(实际传输数据payload与协议头的比例大大提高)。但是这又不可避免地增加了延时;与TCP delayed ack这个特性结合,这个问题会更加显著,延时基本在40ms左右。当然这个问题只有在连续进行两次写操作的时候,才会暴露出来。

Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。

 

测试代码

服务端:recipes/tpc/bin at master · chenshuo/recipes (github.com)

 #include "Acceptor.h"
 #include "InetAddress.h"
 #include "TcpStream.h"
 
 #include <thread>
 #include <vector>
 
 #include <assert.h>
 #include <string.h>
 #include <sys/time.h>
 
 double now()
 {
   struct timeval tv = { 0, 0 };
   gettimeofday(&tv, NULL);
   return tv.tv_sec + tv.tv_usec / 1000000.0;
 }
 
 // an interative request-response server
 int main(int argc, char* argv[])
 {
   InetAddress listenAddr(3210);
   Acceptor acceptor(listenAddr);
   printf("Accepting... Ctrl-C to exit\n");
   int count = 0;
   // 命令行中有"-D" 代表开启TCP_NODELAY选项
   bool nodelay = argc > 1 && strcmp(argv[1], "-D") == 0;
   while (true)
  {
     TcpStreamPtr tcpStream = acceptor.accept();
     printf("accepted no. %d client\n", ++count);
     if (nodelay)
       tcpStream->setTcpNoDelay(true);
 
     while (true)
    {
       int len = 0;
       // 接收header,其实就是一个整数,指明payload大小  
       int nr = tcpStream->receiveAll(&len, sizeof len);
       if (nr <= 0)
         break;
       printf("%f received header %d bytes, len = %d\n", now(), nr, len);
       assert(nr == sizeof len);
   
       // 接收payload
       std::vector<char> payload(len);
       nr = tcpStream->receiveAll(payload.data(), len);
       printf("%f received payload %d bytes\n", now(), nr);
       assert(nr == len);
       
       // 将接收的数据大小发送回去  
       int nw = tcpStream->sendAll(&len, sizeof len);
       assert(nw == sizeof len);
    }
 
     printf("no. %d client ended.\n", count);
  }
 }

客户端:recipes/tpc/bin at master · chenshuo/recipes (github.com)

 #include "InetAddress.h"
 #include "TcpStream.h"
 
 #include <string.h>
 #include <sys/time.h>
 #include <unistd.h>
 
 double now()
 {
   struct timeval tv = { 0, 0 };
   gettimeofday(&tv, NULL);
   return tv.tv_sec + tv.tv_usec / 1000000.0;
 }
 
 int main(int argc, char* argv[])
 {
   if (argc < 3)
  {
     // 命令行参数
     /*
     -b:将两个request合并作一次发送(就是将header和payload一次发送),没有这个选项,就分两次发送
     -D:开启TCP_NODELAY选项
     -n:发送多少次数据,默认为1
     */  
     printf("Usage: %s [-b] [-D] [-n num] hostname message_length\n", argv[0]);
     printf(" -b Buffering request before sending.\n"
            " -D Set TCP_NODELAY.\n"
            " -n num Send num concurrent requests, default = 1.\n");
     return 0;
  }
 
   int opt = 0;
   bool buffering = false;
   bool tcpnodelay = false;
   int num = 1;
   while ( (opt = getopt(argc, argv, "bDn:")) != -1)
  {
     switch (opt)
    {
       case 'b':
         buffering = true;
         break;
       case 'D':
         tcpnodelay = true;
         break;
       case 'n':
         num = atoi(optarg);
         break;
       default:
         printf("Unknown option '%c'\n", opt);
         return 0;
    }
  }
 
   if (optind > argc - 2)
  {
     printf("Please specify hostname and message_length.\n");
     return 0;
  }
 
   const char* hostname = argv[optind];
   int len = atoi(argv[optind+1]);
 
   const uint16_t port = 3210;
   InetAddress addr;
   if (!InetAddress::resolve(hostname, port, &addr))
  {
     printf("Unable to resolve %s\n", argv[1]);
     return 0;
  }
 
   printf("connecting to %s\n", addr.toIpPort().c_str());
   TcpStreamPtr stream(TcpStream::connect(addr));
   if (!stream)
  {
     printf("Unable to connect %s\n", addr.toIpPort().c_str());
     perror("");
     return 0;
  }
 
   if (tcpnodelay)
  {
     stream->setTcpNoDelay(true);
     printf("connected, set TCP_NODELAY\n");
  }
   else
  {
     stream->setTcpNoDelay(false);
     printf("connected\n");
  }
 
   // 计时起点
   double start = now();
   for (int n = 0; n < num; ++n)
  {
     printf("Request no. %d, sending %d bytes\n", n, len);
     if (buffering) // 一次发送
    {
       std::vector<char> message(len + sizeof len, 'S');
       memcpy(message.data(), &len, sizeof len);
       int nw = stream->sendAll(message.data(), message.size());
       printf("%.6f sent %d bytes\n", now(), nw);
    }
     else// 两次发送
    {
       stream->sendAll(&len, sizeof len);
       printf("%.6f sent header\n", now());
       usleep(1000); // prevent kernel merging TCP segments,防止内核将两次发送数据端合并(那就和buffering一样了)
       std::string payload(len, 'S');
       int nw = stream->sendAll(payload.data(), payload.size());
       printf("%.6f sent %d bytes\n", now(), nw);
    }
  }
 
   printf("Sent all %d requests, receiving responses.\n", num);
   //
   for (int n = 0; n < num; ++n)
  {
     int ack = 0;
     int nr = stream->receiveAll(&ack, sizeof ack);
     printf("%.6f received %d bytes, ack = %d\n", now(), nr, ack);
  }
   // 计算延迟  
   printf("total %f seconds\n", now() - start);
 }
 

 

测试步骤

按视频中应该要两台主机,但是这里只用了一台

  • 在终端1启动服务端

image-20230213114515941

  • 在终端2启动tcpdump监听。-i lo指定监听网卡,-nn指定主机和端口都以数字形式显示

image-20230213114559989

  • 在终端3重复启动客户端,测试延迟

  • 第一启动,不带任何参数(不启用TCP_NODEALY和缓存),传1000字节的数据。延迟为0.001380s。由tcpdump截图可见,确实是分两次分别发送header和payload。(用了Nagle算法,且生效)

image-20230213114810250

image-20230213115217076

 

  • 第二次启动,带-b参数,即将header和payload一同发出。传1000字节的数据。延迟为0.000181s。由tcpdump截图可见,确实是一次发送header和payload。(用Nagle算法,但只发送了一个数据包,没有生效)

image-20230213120051760

image-20230213120337630

 

  • 第三次启动,带-D参数,即启用TCP_NODELAY选项。传1000字节的数据。延迟为0.002042s(反而更慢了,这里是因为是同一台主机运行客户端和服务端,当客户端和服务端运行在两台不同的主机上,且RTT较大,此时差距就会很大,比不启用TCP_NODELAY会快得多)。由tcpdump截图可见,确实是一次发送header和payload。(禁用Nagle算法)

image-20230213120701420

image-20230213121007460

 

总结:可以从两个发面降低延迟

  1. 用应用层buffer,将多个要连续发送的小数据包存起来一起发送

  2. 启用TCP_NODELAY

 

总结 TCP client/server

一般来说,在初始化TCP server 或者 client都应该做以下三步:

  • 开启SO_REUSEADDR选项

    • 当TCP服务器在关闭后立即启用时,这个选项很重要。(可以马上重启,否则要等待两分钟后才能重启)

    • 对于每个连接一个进程的模型也同样重要

  • 忽略SIGPIPE

  • 开启TCP_NODELAY

  •  
posted @ 2023-04-29 15:35  DavidJIAN  阅读(49)  评论(0)    收藏  举报