手把手写C++服务器(11):手撕网络带宽测试工具TTCP

Posted 沉迷单车的追风少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手写C++服务器(11):手撕网络带宽测试工具TTCP相关的知识,希望对你有一定的参考价值。

前言:TTCP诞生于1984年,是Unix很流行的网络带宽测试工具,并从中衍生出了著名的网络测试工具Ipref。TTCP手撕的源代码量比较小,是网络编程入门的最佳练习demo。本篇文章参考陈硕的TTCP,进行学习分析。

目录

1、program_options控制命令行输入

2、代码框架

3、带宽计算原理

4、内存优化

5、讲解视频

6、源代码

参考


1、program_options控制命令行输入

具体可以看我之前总结的博客:https://xduwq.blog.csdn.net/article/details/117486239

TTCP里提供了发送模式、接收模式、端口设置、长度设置等,需要在命令行指定。

2、代码框架

主函数里对客户端、服务端两种模式进行判断,调用传输、接收两种模式。整体代码由三个部分组成:传输端、接收端和命令行参数解析。客户端每次读取完成之后构造响应,再发送回去。

// 获取命令行选项
bool parseCommandLine(int argc, char* argv[], Options* opt)

// 客户端代码
void transmit(const Options& opt)

// 服务端代码
void receive(const Options& opt)

int main(int argc, char* argv[])

定义了两个重要的数据结构:SessionMessage和PayloadMessage。

// 先发送数据包的个数和长度,告知服务端
struct SessionMessage
{
  int32_t number;
  int32_t length;
} __attribute__ ((__packed__));//紧凑排列的方式取消字节对齐

// 建立连接后实际发送的数据包
struct PayloadMessage
{
  int32_t length;
  //防止超长,用最后一个数组表示长度,此长度运行时决定
  //分配多少内存,数组就有多长
  char data[0];
};

3、带宽计算原理

模仿TCP三次握手的流程,客户端先发送一个数据包,服务端对包进行确认,并且在确认之前客户端不发送下一个包。可以设置发包的数量和每个包的大小,程序计算传输所有包所用的时间,然后计算出带宽。

4、内存优化

因为ttcp以精简为己任,所以在内存上做了一些优化。需要知道柔型数组技术、字节对齐技术等,具体在前面的博文中总结出了:https://xduwq.blog.csdn.net/article/details/118362300

5、讲解视频

TTCP源代码分析

6、源代码

C++版本,关键地方都写了注释。

#include "Acceptor.h"
#include "InetAddress.h"
#include "TcpStream.h"

#include <iostream>

#include <boost/program_options.hpp>

#include <sys/time.h>

struct Options
{
  uint16_t port;
  int length;
  int number;
  bool transmit, receive, nodelay;
  std::string host;
  Options()
    : port(0), length(0), number(0),
      transmit(false), receive(false), nodelay(false)
  {
  }
};

// 先发送数据包的个数和长度,告知服务端
struct SessionMessage
{
  int32_t number;
  int32_t length;
} __attribute__ ((__packed__));//紧凑排列的方式取消字节对齐

// 建立连接后实际发送的数据包
struct PayloadMessage
{
  int32_t length;
  //防止超长,用最后一个数组表示长度,此长度运行时决定
  //分配多少内存,数组就有多长
  char data[0];
};

double now()
{
  struct timeval tv = { 0, 0 };
  gettimeofday(&tv, NULL);//sys/time.h当中,精确获取当前时间
  return tv.tv_sec + tv.tv_usec / 1000000.0;
}

// 获取命令行选项
bool parseCommandLine(int argc, char* argv[], Options* opt)
{
  // 使用boost,命令行选项功能
  namespace po = boost::program_options;

  po::options_description desc("Allowed options");
  desc.add_options()
      ("help,h", "Help")
      ("port,p", po::value<uint16_t>(&opt->port)->default_value(5001), "TCP port")
      ("length,l", po::value<int>(&opt->length)->default_value(65536), "Buffer length")
      ("number,n", po::value<int>(&opt->number)->default_value(8192), "Number of buffers")
      ("trans,t",  po::value<std::string>(&opt->host), "Transmit")
      ("recv,r", "Receive")
      ("nodelay,D", "set TCP_NODELAY")
      ;

  po::variables_map vm;
  try {
    po::store(po::parse_command_line(argc, argv, desc), vm);
  } catch (...) {
    std::cout << "input undecleare options!" <<std::endl;
  }
  po::notify(vm);

  opt->transmit = vm.count("trans");
  opt->receive = vm.count("recv");
  opt->nodelay = vm.count("nodelay");
  if (vm.count("help"))
  {
    std::cout << desc << std::endl;
    return false;
  }

  if (opt->transmit == opt->receive)
  {
    printf("either -t or -r must be specified.\\n");
    return false;
  }

  printf("port = %d\\n", opt->port);
  if (opt->transmit)
  {
    printf("buffer length = %d\\n", opt->length);
    printf("number of buffers = %d\\n", opt->number);
  }
  else
  {
    printf("accepting...\\n");
  }
  return true;
}

// 客户端代码
void transmit(const Options& opt)
{
  InetAddress addr(opt.port);
  // 连接地址失败
  if (!InetAddress::resolve(opt.host.c_str(), &addr))
  {
    printf("Unable to resolve %s\\n", opt.host.c_str());
    return;
  }

  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;
  }

  if (opt.nodelay)
  {
    stream->setTcpNoDelay(true);
  }
  printf("connected\\n");
  double start = now();
  struct SessionMessage sessionMessage = { 0, 0 };
  sessionMessage.number = htonl(opt.number);
  sessionMessage.length = htonl(opt.length);
  if (stream->sendAll(&sessionMessage, sizeof(sessionMessage)) != sizeof(sessionMessage))
  {
    perror("write SessionMessage");
    return;
  }

  const int total_len = sizeof(int32_t) + opt.length;
  PayloadMessage* payload = static_cast<PayloadMessage*>(::malloc(total_len));
  std::unique_ptr<PayloadMessage, void (*)(void*)> freeIt(payload, ::free);
  assert(payload);
  payload->length = htonl(opt.length);
  for (int i = 0; i < opt.length; ++i)
  {
    payload->data[i] = "0123456789ABCDEF"[i % 16];
  }
  // 计算时间
  double total_mb = 1.0 * opt.length * opt.number / 1024 / 1024;
  printf("%.3f MiB in total\\n", total_mb);

  for (int i = 0; i < opt.number; ++i)
  {
    int nw = stream->sendAll(payload, total_len);
    assert(nw == total_len);

    int ack = 0;
    int nr = stream->receiveAll(&ack, sizeof(ack));
    assert(nr == sizeof(ack));
    ack = ntohl(ack);
    assert(ack == opt.length);
  }

  double elapsed = now() - start;
  printf("%.3f seconds\\n%.3f MiB/s\\n", elapsed, total_mb / elapsed);
}

// 服务端代码
void receive(const Options& opt)
{
  Acceptor acceptor(InetAddress(opt.port));
  TcpStreamPtr stream(acceptor.accept());
  if (!stream)
  {
    return;
  }
  struct SessionMessage sessionMessage = { 0, 0 };
  if (stream->receiveAll(&sessionMessage, sizeof(sessionMessage)) != sizeof(sessionMessage))
  {
    perror("read SessionMessage");
    return;
  }

  sessionMessage.number = ntohl(sessionMessage.number);
  sessionMessage.length = ntohl(sessionMessage.length);
  printf("receive buffer length = %d\\nreceive number of buffers = %d\\n",
         sessionMessage.length, sessionMessage.number);
  double total_mb = 1.0 * sessionMessage.number * sessionMessage.length / 1024 / 1024;
  printf("%.3f MiB in total\\n", total_mb);

  const int total_len = sizeof(int32_t) + sessionMessage.length;
  PayloadMessage* payload = static_cast<PayloadMessage*>(::malloc(total_len));
  std::unique_ptr<PayloadMessage, void (*)(void*)> freeIt(payload, ::free);
  assert(payload);

  double start = now();
  for (int i = 0; i < sessionMessage.number; ++i)
  {
    payload->length = 0;
    //验证两个消息长度是否相等
    if (stream->receiveAll(&payload->length, sizeof(payload->length)) != sizeof(payload->length))
    {
      perror("read length");
      return;
    }
    payload->length = ntohl(payload->length);
    // 不相等直接崩溃退出
    assert(payload->length == sessionMessage.length);
    if (stream->receiveAll(payload->data, payload->length) != payload->length)
    {
      perror("read payload data");
      return;
    }
    // 读取完成之后构造响应,再发送回去
    int32_t ack = htonl(payload->length);
    if (stream->sendAll(&ack, sizeof(ack)) != sizeof(ack))
    {
      perror("write ack");
      return;
    }
  }
  double elapsed = now() - start;
  printf("%.3f seconds\\n%.3f MiB/s\\n", elapsed, total_mb / elapsed);
}

int main(int argc, char* argv[])
{
  Options options;
  if (parseCommandLine(argc, argv, &options))
  {
    if (options.transmit)
    {
      transmit(options);
    }
    else if (options.receive)
    {
      receive(options);
    }
    else
    {
      assert(0);
    }
  }
}

参考

以上是关于手把手写C++服务器(11):手撕网络带宽测试工具TTCP的主要内容,如果未能解决你的问题,请参考以下文章

手把手写C++服务器(30):手撕代码——基于TCP/IP的抛弃服务discard

手把手写C++服务器(29):手撕echo回射服务器代码

手把手写C++服务器(28):手撕CGI通用网关接口服务器代码

手把手写C++服务器(35):手撕代码——高并发高QPS技术基石之非阻塞send万字长文

手把手写C++服务器(36):手撕代码——高并发高QPS技术基石之非阻塞recv万字长文

手把手写C++服务器:Linux四大必备网络分析工具