65.9K
CodeProject 正在变化。 阅读更多。
Home

从想法到实现:在 C++14 项目中播下第一颗种子。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (15投票s)

2016年12月28日

CPOL

17分钟阅读

viewsIcon

21156

downloadIcon

190

引言

很少有程序员有幸亲眼见证一个项目的诞生。比如,有人提出了一个绝妙的主意,要在网络上传输视频。然后,一个设计就形成了,C++源代码开始出现在空白屏幕上。这将是梦想成真,因为多年以后,你将成为其他程序员引用的专家。然而,我们大多数人只能在已经存在多年的基础源代码上工作。更糟糕的是,有些源代码是从其他程序员那里“祖父”传给我们的。在这种情况下,有时你别无选择,只能接受别人的编码风格,并在过程中沾染他们的坏习惯。同化意味着你只是不想让自己的好代码像田野里的草一样突兀。但是,今天,我们就来假装是公司委托启动编码项目的那些人。

我们的目标是创建一个应用程序,在一个机器的屏幕上播放视频,而另一台连接的机器能够实时复制该视频。从外表来看,我们需要两个进程——一个发送和播放视频帧,另一个接收和播放这些帧。这两个进程需要通过网络协议进行通信——一个可靠的协议。由于我们打算通过网络发送视频帧,接收的顺序至关重要。你可以想象,如果一些帧在网络上传输时丢失,视频质量会大打折扣。TCP 是一种面向连接的协议,这意味着在建立可靠连接之前,数据传输不会开始。在该协议中,所有数据包都会按发送顺序到达目的地,因为它们遵循相同的路由。如果无法维持路由,可能会建立另一条路由,目的地节点将要求在处理新数据包之前重传所有丢失的数据包。因此,TCP 正是我们这个应用程序所需要的——视频将从发生路由技术困难之前停止的地方继续播放。

我们知道,脚本和编程语言是开发应用程序的工具。选择合适的工具对于圆满完成任务至关重要。你不能用一个小扳手砍倒一棵大树;另一方面,用锯子也无法拧松一个紧密的螺栓。然而,如果可以选择,尽量远离 Visual Studio 语言(C#、Visual Basic、Visual C++),除非你不在乎被束缚在 Windows 平台。在流行的脚本和编程语言中,现在选择缩小到 Java、Python 或 C++。Java 解释器和 JVM 可能会阻碍视频帧的快速渲染,而使用 Python,我们可能无法精细地控制程序的执行。例如,如果我们对 Python 字典容器的内存性能不满意,那就完了。当然,你可以通过查看源代码来改进 Python 的任何功能,这引导我们做出最终选择。显而易见的选择是 C++,因为它非常接近底层。我必须告诉你,如果你觉得 C++ 是正确的选择,那么选择 C++ 没有错。

现在,让我们来看看显示视频的可用方法。如果你曾经在 YouTube 上看过视频,那么你应该确信,你的应用程序也有传输和显示视频的方法。这叫做视频流。关于这个主题的快速谷歌搜索会得到大量关于 OpenCV 的结果。这是一个开源库,包含图像处理算法。它有 C++、C 和 Python 的接口。该库包含播放和处理图像的功能,以及由图像扩展而来的视频。下载并安装 OpenCV。现在看起来我们已经万事俱备了。

创建进程

和你们许多人一样,我通过一个显示“Hello world!”的小程序接触到编程。我必须说,当时我并没有为此感到丝毫的赞赏。对我来说,我一直能够在屏幕上显示我在文本编辑器中输入的任何内容。我被教导以某种方式进行编译和链接,这就是我创建可执行文件并能够在不深入思考其工作原理的情况下显示那个愚蠢的“Hello world!”消息的方式。那时我认为没有什么比回显消息更愚蠢的了。时间快进到现在的我。现在,当我发现很难使用调试器时,我会在调试时打印大量的消息。显示输出通常是检查事情是否正常工作的一种可接受的方式。在这里,我们也遵循这个模式。

创建两个名为 VideoSender.cpp 和 VideoReceiver.cpp 的文本文件,并输入以下文本。

// filename - videoSender.cpp 

#include <iostream> 

#include "process_metrics.h" 

int senderMainEntry() 
{ 
   std::cout << "Video sender ..." << std::endl; 
   return 0; 
} 

int main() 
{ 
   return senderMainEntry(); 
}
// filename - videoReceiver.cpp 

#include <iostream> 

#include "process_metrics.h" 

int receiverMainEntry() 
{ 
   std::cout << "Video receiver ..." << std::endl; 
   return 0; 
} 

int main() 
{ 
   return receiverMainEntry(); 
}

这两个文件中的代码基本是标准的——目前还没有太多内容。C++不允许我们更改入口函数的名称,使其不同于 `main()`,但我发现过去为每个进程拥有一个独特命名的入口函数很有用。在一个包含多个进程的大项目中,当使用 IDE 的调用层次结构时,知道调用层次结构起源于哪个进程会很有帮助。因此,`main()` 函数在这里的目的是调用进程的入口函数。到此为止,我们就已经创建了这两个进程。

暂时将注意力转向别处。接下来,将在两个进程中包含一个基本的度量工具。创建一个名为 process_metrics.h 的新文件,并包含此文本。

// filename - process_metrics.h 

#include <stdio.h> 
#include <time.h> 

#define LOCAL_TIME(x) \ 
   time_t t = time(NULL); \ 
   struct tm * locTime = localtime(&t); \ 
   char x[9]; \ 
   strftime(x, 9, "%T", locTime); \ 
   while(0) 

clock_t pTicks; 

__attribute__((constructor)) void begin () 
{ 
   LOCAL_TIME(sLocTime); 
   printf("begin execution: %s\n", sLocTime); 
   pTicks = clock(); 
} 

__attribute__((destructor)) void end () 
{ 
   pTicks = clock() - pTicks; 
   printf("Number of clock ticks: %ld\n", pTicks); 
   LOCAL_TIME(sLocTime); 
   printf("end execution: %s\n", sLocTime); 
}

有些人可能会觉得这段代码特别奇怪。特别是,`__attribute__((constructor)) void begin()` 和 `__attribute__((destructor)) void end()` 的定义可能看起来不像正常的 C++。而且,奇怪的是,正如你在 VideoSender.cpp 和 VideoReceiver.cpp 中看到的那样,我没有添加 `begin()` 和 `end()` 的调用,因为我完全期望它们会自动运行。好吧,实际上我正在将 `begin()` 添加到全局构造函数列表 (CTOR) 中,并将 `end()` 添加到全局析构函数列表 (DTOR) 中。全局对象必须像 C++ 中的任何其他对象一样拥有构造函数和析构函数,因此,CTOR 和 DTOR 列表中的所有函数都会自动运行。就用户而言,进程从 `main()` 开始,在 `exit(0)` 终止,但 CTOR 中的每个函数都在 `main()` 之前调用,而 DTOR 中的所有函数都在 `exit()` 之后调用。其余的代码用于显示进程的开始和结束时间以及它花费的时钟周期数。

熟悉 OpenCV

在互联网时代,你能做的最糟糕的事情就是重新发明轮子(嗯,除了你能在互联网上做的所有糟糕的事情之外)。很有可能你不是第一个解决你正在尝试解决的问题的人;更优秀的人已经为你编写了代码,并以免费许可证的形式提供了源代码。你可以直接使用他们的代码,或者阅读它来学习。谁知道呢,在这个过程中,你可能会成为那个将其推向新高度的人,并为那个开源项目做出贡献。

我对 OpenCV 知之甚少,我会将其描述为一组共享库,其中包含图像处理函数。使用 OpenCV,你可以从视频中读取帧,进行处理,然后显示。但是,OpenCV 需要编解码器来打开视频文件。我下载了一个包含大多数常见编解码器的软件包,并在我的 Ubuntu VM 上安装了它。

> sudo apt-get install -y ubuntu-restricted-extras

接下来,我下载、编译和链接了 OpenCV(编译和链接就是我所说的“构建”)。我不得不经过几次构建选项的迭代,并调试 OpenCV 以尝试使其工作,直到我找到一篇由 Manuel Ignacio López Quintero 撰写的文章,详细介绍了构建步骤。这是产生结果的 cmake 选项列表的最终版本。

> mkdir build
>
> cd build
>
> cmake -DWITH_IPP=ON -DWITH_OPENGL=ON -DWITH_GTK=ON -DWITH_VTK=ON <code class="western">-DCMAKE_INSTALL_PREFIX=./build ..
>
> make install

OpenCV 将不会安装在标准的 `/usr/bin` 和 `/usr/lib` 目录中,而是安装在 `./build` 中(参见 `CMAKE_INSTALL_PREFIX`)。由于我们以非标准方式安装 OpenCV,请不要忘记运行

> sudo ldconfig

现在是时候配置项目了。但是,在配置构建之前,您必须确保已安装所有必需的依赖项。例如,由于您需要将视频显示在窗口中,请下载并安装 GTK 和 VTK 的最新开发库。

> sudo apt-get install -y libgtk-3-dev libvtk6-dev

在谈论 GTK 和 GUI 时,OpenCV 也可以配置为使用 Qt——另一个 GUI 开发库。在处理 GUI 编程时,并行性是一头必须驯服的野兽;OpenCV 可以与 TBB 配置以处理多线程,但除此之外,OpenCV 使用 pThreads。

因此,总而言之,我能够通过修改 VideoSender.cpp 来运行视频文件。

// filename - videoSender.cpp

#include <iostream>

#include "process_metrics.h"

#include <opencv2/opencv.hpp>

int senderMainEntry (const char* videoFile)
{
   std::cout << "Video sender ..." << std::endl;

   cv::VideoCapture capturedVideo;
   capturedVideo.open(videoFile);
   if (!capturedVideo.isOpened())
   {
      std::cerr << "Unable to open video: " << videoFile << std::endl;
      return -1;
   }

   cv::Mat dispFrame;
   cv::namedWindow("Video Send", cv::WINDOW_AUTOSIZE);

   while (true)
   {
      capturedVideo >> dispFrame;
      if (dispFrame.empty()) // end of file
      {
         break;
      }
      else
      {
         imshow("Video Send", dispFrame);
      }
      if (cv::waitKey(30) == 0) // wait for key press event
      {
         puts("Key press");
         break;
      }
   }

   return 0;
}

int main(int argv, char** argc)
{
   return senderMainEntry(argc[1]);
}

正如你所见,VideoSender.cpp 中的代码以一种能够逐帧读取视频文件并显示它的方式进行了修改。

连接和信令进程

应用程序级别的通信发生在进程之间。NIC 在网络中具有唯一的 IP 地址,被许多进程可以访问的端口共享。端口由端口号标识,通信的进程必须知道彼此的端口号。操作系统通过称为套接字的东西向进程提供端口和 IP 地址的访问权限。在这里,你会发现一个名为 `SocketHandler` 的类,它抽象了套接字的使用。

// filename - SocketHandler.h

#ifndef SOCKETHANDLER_H
#define SOCKETHANDLER_H

#include <functional>
#include <string>
#include <sys/types.h>
#include <netinet/in.h>
#include <unistd.h>

#define PORT_ONE 51001
#define PORT_TWO 51002

class SocketHandler
{
   enum InitializationStatus
   {
      Uninitialized,
      Initialized
   };
   const std::size_t PACKET_SIZE = 21845;
   InitializationStatus init_status;
   int                  socket_fd; // socket file descriptor
   int                  connected_socket_fd;
   int                  port_no;
   struct sockaddr_in   connect_to_addr;
   bool                 is_client_connected = false;

public:
   SocketHandler (int socket_type, int port);
   ~SocketHandler ();

   int InitializeAsServer ();
   void ConnectToServer (std::string ip_address);
   void ConnectToClient ();
   int GetFileDescriptor ();
   ssize_t Send (const void * data, std::size_t len);
   ssize_t Receive (void * buffer, std::size_t len = 1);
   ssize_t SendLarge (const void * data, std::size_t len);
   ssize_t ReceiveLarge (void * buffer, std::size_t len);
   ssize_t SendAndWait (const void * data, std::size_t len);
   ssize_t ReceiveAndAcknowledge (void * buffer, std::size_t len, std::function <void(void *)> PerformBeforeAcknowledging);
   void CloseSocket ();
};

inline int SocketHandler::GetFileDescriptor ()
{
   return is_client_connected ? connected_socket_fd : socket_fd;
}

inline ssize_t SocketHandler::Send (const void * data, std::size_t len)
{
   return send(GetFileDescriptor(), data, len, 0);
}

inline ssize_t SocketHandler::Receive (void * buffer, std::size_t len)
{
   return recv(GetFileDescriptor(), buffer, len, 0);
}

inline void SocketHandler::CloseSocket ()
{
   close(GetFileDescriptor());
}
#endif
// filename - SocketHandler.cpp

#include <sys/poll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>

#include <stdio.h>
#include <strings.h>

#include "SocketHandler.h"

SocketHandler::SocketHandler (int socket_type, int port):
   init_status(Uninitialized),
   port_no(port)
{
   socket_fd = socket(AF_INET, socket_type, 0);
   if (socket_fd < 0)
   {
      perror("Unable to create socket");
   }
}

SocketHandler::~SocketHandler ()
{
   CloseSocket();
}

int SocketHandler::InitializeAsServer ()
{
   if (init_status != Uninitialized)
   {
      perror("Socket already initialized");
      return -1;
   }

   // Declare and clean the address object
   struct sockaddr_in server_addr;
   bzero((char *) &server_addr, sizeof(server_addr));

   // Fill in address and port number information
   server_addr.sin_family = AF_INET;
   server_addr.sin_addr.s_addr = INADDR_ANY; // any ip address of the host
   server_addr.sin_port = htons(port_no);

   auto bind_status = bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
   if (bind_status < 0)
   {
      perror("Socket binding not successful");
   }
   return bind_status;
}

void SocketHandler::ConnectToClient ()
{
   is_client_connected = true;
   if (InitializeAsServer() >= 0)
   {
      puts("Connecting to client");
      listen(socket_fd, 1); // wait for only one connection
      // clean address object and store connection address
      auto size_of_addr = sizeof(connect_to_addr);
      bzero((char *) &connect_to_addr, size_of_addr);
      connected_socket_fd = accept(socket_fd, (struct sockaddr *) &connect_to_addr, (socklen_t *) &size_of_addr);
      close(socket_fd);
      printf("Connection established. Previous socket(%d) replaced by socket(%d)\n", socket_fd, connected_socket_fd);
   }
}

void SocketHandler::ConnectToServer (std::string ip_address)
{
   printf("Connecting to server on port %d. Socket(%d)\n", port_no, socket_fd);
   if (init_status != Uninitialized)
   {
      fputs("Socket already initialized\n", stderr);
   }

   auto client = inet_addr(ip_address.c_str());

   // Declare and clean the address object
   struct sockaddr_in client_addr;
   bzero((char *) &client_addr, sizeof(client_addr));

   // Fill in address and port number information
   client_addr.sin_family = AF_INET;
   client_addr.sin_addr.s_addr = client;
   client_addr.sin_port = htons(port_no);

   if (connect(socket_fd, (struct sockaddr *) &client_addr, sizeof(client_addr)) < 0)
   {
      perror("Cannot connect to server");
   }
}

ssize_t SocketHandler::SendLarge (const void * data, std::size_t len)
{
   auto dataOffset = 0;
   while (len > 0)
   {
      auto sentSize = Send(static_cast<const char *>(data) + dataOffset, PACKET_SIZE < len ? PACKET_SIZE : len);
      if (sentSize < 0)
      {
         perror("Message not sent by SocketHandler::SendLarge");
         return -1;
      }
      len -= sentSize;
      dataOffset += sentSize;
   }
   return dataOffset;
}

ssize_t SocketHandler::ReceiveLarge (void * buffer, std::size_t len)
{
   auto dataOffset = 0;
   while (len > 0)
   {
      auto receiveSize = Receive(static_cast<char *>(buffer) + dataOffset, PACKET_SIZE < len ? PACKET_SIZE : len);
      if (receiveSize < 0)
      {
         perror("Message not received by SocketHandler::ReceiveLarge");
         return -1;
      }
      len -= receiveSize;
      dataOffset += receiveSize;
   }
   return dataOffset;
}

ssize_t SocketHandler::SendAndWait (const void * data, std::size_t len)
{
   auto retStatus = Send(data, len);
   char recAck;
   Receive(&recAck);
   return retStatus;
}

ssize_t SocketHandler::ReceiveAndAcknowledge (void * buffer, std::size_t len, std::function <void(void *)> PerformBeforeAcknowledging)
{
   struct sockaddr_storage read_from;
   socklen_t read_from_len = sizeof(read_from);
   struct pollfd ds[1];
   ds[0].fd = GetFileDescriptor();
   ds[0].events = POLLIN;
   poll(ds, 1, -1);
   auto retStatus = recvfrom(GetFileDescriptor(), buffer, len, 0, (struct sockaddr *)&read_from, &read_from_len);

   // received data, now perform pre-acknowledgment actionns
   PerformBeforeAcknowledging(buffer);

   auto recAck = 'A';
   sendto(GetFileDescriptor(), &recAck, sizeof(recAck), 0, (struct sockaddr *)&read_from, read_from_len);
   return retStatus;
}

此类可用于数据包(UDP)和流(TCP)通信协议;用户只需在构造函数中指定套接字类型。网络通信通常发生在客户端和服务器之间。`SocketHandler::InitializeAsServer` 方法用于设置 UDP 或 TCP 服务器,但由于 TCP 通信是面向连接的,服务器还必须使用 `SocketHandler::ConnectToClient` 方法来接受来自客户端的 TCP 连接请求。此方法对 UDP 通信无效。在客户端方面,UDP 或 TCP 客户端套接字在初始化期间必须调用 `SocketHandler::ConnectToServer`。数据发送和接收通过其余的方法完成,这些方法的名称都表明了其用途。当我们在主驱动程序中调用它们时,其用法将变得更加明显。

别忘了——我们的目标是通过网络流式传输视频(发送帧)。数字艺术家们知道视频帧的特点是它们很大。由于系统对 `send()` 和 `recv()` 的容量有限制,我们必须想办法发送和接收非常大的字节大小。`SocketHandler::SendLarge` 和 `SocketHandler::ReceiveLarge` 方法正是为此目的而设计的。这些方法通过将一个大包分解成 `PACKET_SIZE=21845` 的小块来工作。这个大小是我目前的设置中最优的,并且是通过在 Wireshark 中观察得出的实验结果,对于这个大小,在 TCP 通信中发送的包和收到的确认之间存在一对一的对应关系。有一个普遍的误解是,就像 UDP 一样,TCP 需要“接收”对应于每一个“发送”,形成一对一的关系。实际上,TCP 是一种流协议,因此接收方可以在任何方便的时间收集其数据。例如,我们可以有两个发送和一个接收。`SocketHandler::SendLarge` 和 `SocketHandler::ReceiveLarge` 方法反映了这一事实。

我们可以协调不同进程的一种方式是通过消息传递并等待响应或确认。`SocketHandler::SendAndWait` 方法会将一条消息发送到另一端,并立即进入等待状态。它会在收到确认后退出。另一方面,`SocketHandler::ReceiveAndAcknowledge` 方法会收到发送的消息,并向消息的发送者发出确认。但前提是它会执行某些操作!为了执行这些操作,可以将一个函数或任何其他可调用对象传递给 `SocketHandler::ReceiveAndAcknowledge` 方法的 `PerformBeforeAcknowledging` 参数。

另一种向进程发送清晰消息的方式是使用信号。操作系统能够向进程传递许多信号。例如,按下 Ctrl+C 允许操作系统向活动终端中的进程发送 SIGINT(中断)信号,SIGALRM 是由系统调用 `alarm()` 触发的。但与当前上下文更相关的信号是 SIGPIPE 和 SIGIO。当 TCP 连接断开并且继续尝试通过该连接进行通信时,会产生 SIGPIPE;而 SIGIO 是操作系统生成的信号,用于指示 IO 设备(通过其文件描述符)已准备好传输或接收数据。但首先,让我们学习如何为已知信号注册信号处理程序。

// filename - SignalHandling.h

#ifndef SIGNALHANDLING_H
#define SIGNALHANDLING_H

#include <csignal>
#include <stdio.h>
#include <sys/fcntl.h>
#include <unistd.h>

struct SignalHandling
{
   void operator() (int signal, void performSigAction (int s), int fd = -1)
   {
      struct sigaction sigParam;
      if (sigfillset(&sigParam.sa_mask) < 0)
      {
         perror("SignalHandling::PrepareSIGIO cannot block other signals");
      }
      sigParam.sa_flags = 0;
      sigParam.sa_handler = performSigAction;
      if (sigaction(signal, &sigParam, NULL) < 0)
      {
         perror("SignalHandling::PrepareSIGIO unable to change default behavior after signal delivery");
      }

      if (signal == SIGIO)
      {
         const auto CURRENT_PROCESS = getpid();
         if (fcntl(fd, F_SETOWN, CURRENT_PROCESS) < 0)
         {
            perror("SignalHandling::PrepareSIGIO unable to set ownership of descriptor's SIGIO to current process");
         }
         if (fcntl(fd, F_SETFL, O_ASYNC) < 0)
         {
            perror("SignalHandling::PrepareSIGIO unable to set file descriptor for asynchronous delivery of signals");
         }
      } 
   }
};

SignalHandling RegisterHandlerForSignal;
#endif

类型 `SignalHandling` 重载了 `operator()`:该类型的对象被称为可调用对象。对象 `RegisterHandlerForSignal` 被声明为 `SignalHandling` 类型。该对象使用系统函数 `sigaction()` 为特定信号注册一个处理程序。如果没有提供用户定义的处理程序,该信号将被完全忽略,或者将执行默认操作。这里还需要另一个系统调用 `sigfillset()` 来屏蔽或阻止在当前信号处理程序运行时所有其他信号。我们还可以看到,调用 `RegisterHandlerForSignal()` 会对 SIGIO 进行特殊处理。对于 SIGIO,我们正在进行特殊的 `fcntl()` 调用,将信号的所有权设置为当前进程,并将文件描述符的 IO 操作设为异步。类似于常规函数调用,在同步操作中,调用者等待操作返回。这与异步操作相反,在异步操作中,调用者会继续运行,直到被调用者准备好返回。将信号的所有权设置为当前进程,会让内核知道将信号传递给当前进程,并且由于我们正在干扰 SIGIO 到 `send()` 或 `recv()` 的正常流程,异步操作是我们避免阻塞 IO 操作的关键。

一个建议——我怎么强调描述性错误消息的重要性都不为过,这可以节省调试花费的时间。

项目结构与组织

没有考虑编译和链接,你就不能称一个 C++ 项目为完整。Linux 发行版带有一个名为 `make` 的程序,它有助于设置目标文件(编译目标,如可执行文件和对象文件)和依赖项。`make` 程序会识别依赖项中的更改,并相应地重新运行目标构建。可以创建一个 makefile 来保存命令以供重用。

# filename - makefile

all : SocketHandler.o videoSendBin videoReceiveBin

SocketHandler.o : SocketHandler.h SocketHandler.cpp
	g++ -Wall -g -std=c++14 -c SocketHandler.cpp

export LIBRARY_PATH=../../opencv-3.1.0/build/lib/:../../opencv-3.1.0/3rdparty/ippicv/unpack/ippicv_lnx/lib/intel64/
export PKG_CONFIG_PATH=../../opencv-3.1.0/build/unix-install/
INC=-I../../opencv-3.1.0/include \
-I../../opencv-3.1.0/modules/core/include \
-I../../opencv-3.1.0/modules/imgproc/include \
-I../../opencv-3.1.0/modules/photo/include \
-I../../opencv-3.1.0/modules/video/include \
-I../../opencv-3.1.0/modules/features2d/include \
-I../../opencv-3.1.0/modules/flann/include \
-I../../opencv-3.1.0/modules/objdetect/include \
-I../../opencv-3.1.0/modules/calib3d/include \
-I../../opencv-3.1.0/modules/imgcodecs/include \
-I../../opencv-3.1.0/modules/videoio/include \
-I../../opencv-3.1.0/modules/highgui/include \
-I../../opencv-3.1.0/modules/ml/include

videoSendBin : SocketHandler.o videoSender.cpp SignalHandling.h SharedStructs.h process_metrics.h
	g++ -Wall -g -std=c++14 -o videoSendBin videoSender.cpp SocketHandler.o `pkg-config --libs opencv` $(INC)

videoReceiveBin : SocketHandler.o videoReceiver.cpp SignalHandling.h SharedStructs.h process_metrics.h
	g++ -Wall -g -std=c++14 -pthread -o videoReceiveBin videoReceiver.cpp SocketHandler.o `pkg-config --libs opencv` $(INC)

clean :
	-rm videoSendBin videoReceiveBin SocketHandler.o

这个 makefile 中有五个目标:`clean`、`all`、`videoSendBin`、`videoReceiveBin` 和 `SocketHandler.o`。`clean` 目标的规则简单地删除了生成的二进制文件,并且该目标没有依赖项。当你想要重新构建所有二进制文件时,就会运行这个目标。在有权访问 makefile 的终端提示符中,运行

> make clean

目标 `all` 构建所有二进制文件。它的依赖项是 `videoSendBin`、`videoReceiveBin` 和 `SocketHandler.o`。在命令行中,输入

> make

目标 `videoSendBin`、`videoReceiveBin` 和 `SocketHandler.o` 的制作过程类似。它们都有源代码作为依赖项,并调用 `g++`(编译器和链接器程序)以及编译器和链接器选项。但是,`videoSendBin` 和 `videoReceiveBin` 是最终链接的产品,与 `SocketHandler.o`(将要链接到可执行文件中的对象文件)不同。

可执行文件 `videoSendBin` 和 `videoReceiveBin` 是链接到 OpenCV 的。但是,OpenCV 安装在一个非标准的前缀中,命令行环境必须使用环境变量进行修改。修改后的环境变量 `LIBRARY_PATH` 使 OpenCV 共享库可用于我们的构建,而 `PKG_CONFIG_PATH` 通过调用 ``pkg-config --libs opencv` 来提供有关该包的各种信息,例如查找共享库的位置。与 `g++` 一起使用的其他选项是 `-I`(包含头文件)、`-Wall`(不忽略任何构建警告)、`-g`(使二进制文件可 GDB 调试)、`-std=c++14`(使用 C++14 标准)、`-pthread`(启用多线程)和 `-o`(命名输出二进制文件)。

这张图展示了项目的结构。你已经遇到了其中的许多文件,但你也可以看到几个新文件。视频文件 bird.avi 和 nasa_arctic_sea_ice_sets_new_record_winter_low.mp4 是我用于测试和实验的文件。由于选择将 OpenCV 安装在非标准前缀中,我们必须修改另一个环境变量 `LD_LIBRARY_PATH`,以方便运行时链接 OpenCV 共享库。GDB 文件用于在 GDB 中启动可执行文件。

#!/bin/bash

# filename - videoSend

export LD_LIBRARY_PATH=../../opencv-3.1.0/build/lib/

./videoSendBin $1
#!/bin/bash

# filename – videoSend-gdb

export LD_LIBRARY_PATH=../../opencv-3.1.0/build/lib/

gdb ./videoSendBin $1
#!/bin/bash

# filename – videoReceive

export LD_LIBRARY_PATH=../../opencv-3.1.0/build/lib/

./videoReceiveBin
#!/bin/bash

# filename – videoReceive-gdb

export LD_LIBRARY_PATH=../../opencv-3.1.0/build/lib/

gdb ./videoReceiveBin

发送和接收视频帧

数据通信是通过协议实现的;发送方和接收方必须就协议达成一致。协议决定了数据如何格式化以便接收方能够正确解释。发送进程发送帧后,在接收端,通过 TCP 连接发送的帧需要被接收、重新组合和显示。但是,要重新组装代表视频帧的矩阵表示的内存块,我们需要元数据,例如行和列的大小。我们将通过 UDP 发送诸如矩阵表示的维度之类的数据。发送和接收进程共享 `SharedStructs` 来传递帧元数据。

// filename - SharedStructs.h

#ifndef SHAREDSTRUCTS_H
#define SHAREDSTRUCTS_H

struct MatMetaData
{
   int rows;
   int cols;
   int type;
};
#endif

TCP 或 UDP 接收函数可能是阻塞的,这意味着它们将永远等待下一个数据包的到来。因此,UDP 和 TCP 服务器不能单线程运行;我们必须开发一个多线程解决方案。一个阻塞不应该阻止另一个执行。这是接收器。

// filename - videoReceiver.cpp

#include "SharedStructs.h"
#include "SignalHandling.h"
#include "SocketHandler.h"

#include <chrono>
#include <condition_variable>
#include <functional>
#include <future>
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>

#include <opencv2/opencv.hpp>

#include "process_metrics.h"

std::mutex tcpReadyLocker;
std::condition_variable tcpReadyEvent;
auto tcpReadyNonSpurious = false;

std::mutex udpReadyLocker;
std::condition_variable udpReadyEvent;
auto udpReadyNonSpurious = false;

// shared resources
std::promise<MatMetaData> * frameMeta = nullptr;
std::function<void ()> ConnectionReset;

void ReceiverMainEntry()
{
   std::cout << "Video receiver ..." << std::endl;

   while (true)
   {
      // TCP server in the main thread
      SocketHandler tcpSocket(SOCK_STREAM, PORT_ONE);
      auto AsyncConnect = [&tcpSocket]{ tcpSocket.ConnectToClient(); return &tcpSocket; };
      auto futureConnection = std::async(std::launch::async, AsyncConnect);

      do
      {
          std::this_thread::sleep_for(std::chrono::milliseconds(200));
      }while (!frameMeta); // while frameMeta is null
      auto frameMetaData = frameMeta->get_future().get();
      frameMeta = nullptr;

      auto dispFrame = cv::Mat{frameMetaData.rows, frameMetaData.cols, frameMetaData.type};
      auto frameSize = dispFrame.total() * dispFrame.elemSize();
      auto framePixels = std::make_unique<uchar[]>(frameSize);
      dispFrame.data = framePixels.get();

      {
         std::lock_guard<std::mutex> lk(tcpReadyLocker);
           tcpReadyNonSpurious = true;
      }
      tcpReadyEvent.notify_one();

      futureConnection.get(); // ready to get the connection now

      // you can close current socket when it is time to reset the connection
      ConnectionReset = [&tcpSocket]{ tcpSocket.CloseSocket(); };

      cv::namedWindow("Video Receive", cv::WINDOW_AUTOSIZE);

      while (true)
      {
         if (tcpSocket.ReceiveLarge(dispFrame.data, frameSize) >= 0)
         {
            imshow("Video Receive", dispFrame);
         }
         else
         {
            // if connection was terminated, give TCP time to die out
            std::this_thread::sleep_for(std::chrono::seconds(5));
            break; // exit current loop
         }

         if (cv::waitKey(30) == 0) // wait for key press event
         {
            puts("Key press");
            break;
         }
      }
   }
}

void MetaReceiverEntry()
{
   // UDP server in the background thread
   SocketHandler udpSocket(SOCK_DGRAM, PORT_TWO);
   udpSocket.InitializeAsServer();

   auto action = [] (int)
                 {
                    static auto passOnce = true;
                    if (passOnce)
                    {
                       passOnce = false;
                       return;
                    }
                    {
                       std::lock_guard<std::mutex> lk(udpReadyLocker);
                         udpReadyNonSpurious = true;
                    }
                    udpReadyEvent.notify_one();
                 };
   RegisterHandlerForSignal(SIGIO, action, udpSocket.GetFileDescriptor());

   MatMetaData frameInfo;
   while (true)
   {
      std::promise<MatMetaData> p; // a std::promise called p
      frameMeta = &p;
      auto PerformToAcknowledge = [] (void * frameData)
      {
         frameMeta->set_value(*static_cast<MatMetaData *>(frameData));

         std::unique_lock<std::mutex> lk(tcpReadyLocker);
         tcpReadyEvent.wait(lk, []
                                {
                                   auto returnValue = tcpReadyNonSpurious;
                                   tcpReadyNonSpurious = false;
                                   return returnValue; 
                                });
      };

      udpSocket.ReceiveAndAcknowledge(&frameInfo, sizeof(frameInfo), PerformToAcknowledge);

      // wait for another UDP communication from the clinet,
      // then reset the TCP connection
      std::unique_lock<std::mutex> lk(udpReadyLocker);
      udpReadyEvent.wait(lk, []
                             {
                                auto returnValue = udpReadyNonSpurious;
                                udpReadyNonSpurious = false;
                                return returnValue; 
                             });
      ConnectionReset();

   }
}

int main()
{
   std::thread bgthread(MetaReceiverEntry);
   bgthread.detach();
   ReceiverMainEntry();
   return 0;
}

函数 `MetaReceiverEntry()` 在新线程中启动并运行 UDP 服务器。通过 `bgthread.detach()` 结构,线程的控制立即交给操作系统在后台运行它。UDP 服务器随时准备接收来自发送方的元数据;它将更新主线程的接收信息并向发送方发送确认。它通过将 `std::promise` 传递给主线程中的 `std::future` 来传递元数据。如果你想知道为什么 `std::promise` 被声明为指针,那是因为你不能重置和重用 `std::promise`;你必须始终重新声明一个 `std::promise` 对象。回到确认点,在确认之前,我们将确保 TCP 连接已成功建立,并且动态内存已在主线程中分配,方法是运行 lambda 函数 `PerformToAcknowledge()`;在这里,我们等待 `tcpReadyEvent`。确认后,接收方将进入等待下一个连接请求(来自另一个发送方)的模式。它等待 SIGIO 处理程序的事件被触发。lambda 函数 `action()` 是 SIGIO 信号处理程序,它唤醒 `udpReadyEvent`。此时,当前存在的 TCP 连接将被 `ConnectionReset()` 调用断开,然后进程将重新开始。

一个小问题是如何干净地退出应用程序。网络套接字和堆内存等资源必须被释放并返回给操作系统。发送方一旦完成发送视频帧就可以退出。但是,接收方由于可以跨越多个视频发送,因此必须与发送方协调。TCP 服务器由 `ReceiverMainEntry()` 启动并在主线程中运行。从 UDP 服务器接收元数据后,将创建一个名为 `dispFrame` 的 OpenCV 矩阵对象。要显示的像素存储在由智能指针 `framePixels` 管理的动态内存中。智能指针的美妙之处在于,您不必担心释放管理的内存,因为它们在超出范围时会自动为您完成。并且,为了在适当的时候关闭 TCP 连接,`ConnectionReset()` 被声明为 `std::function`,分配一个可调用对象,并传递给 UDP 服务器(记住 UDP 服务器知道何时使用它)。TCP 连接最初是异步建立的,就像通过 AsyncConnect 对象一样,因为我们知道等待客户端可能会阻塞。

接下来,我们最终看看发送方发生了什么。

// filename - videoSender.cpp

#include "SharedStructs.h"
#include "SignalHandling.h"
#include "SocketHandler.h"

#include <iostream>

#include <opencv2/opencv.hpp>

#include "process_metrics.h"

int SenderMainEntry (const char* videoFile)
{
   std::cout << "Video sender ..." << std::endl;

   cv::VideoCapture capturedVideo;
   capturedVideo.open(videoFile);
   if (!capturedVideo.isOpened())
   {
      std::cerr << "Unable to open video: " << videoFile << std::endl;
      return -1;
   }

   cv::namedWindow("Video Send", cv::WINDOW_AUTOSIZE);

   SocketHandler udpSocket(SOCK_DGRAM, PORT_TWO);
   udpSocket.ConnectToServer("127.0.0.1");

   SocketHandler tcpSocket(SOCK_STREAM, PORT_ONE);

   auto action = [] (int) { fputs("Exiting in response to SIGPIPE\n", stderr); exit(1); };
   RegisterHandlerForSignal(SIGPIPE, action);

   cv::Mat dispFrame;
   std::size_t frameSize;
   bool passOnce = false;
   while (true)
   {
      capturedVideo >> dispFrame;
      if (!passOnce)
      {
         passOnce = true;
         MatMetaData frameMetaData{dispFrame.rows, dispFrame.cols, dispFrame.type()};
         udpSocket.SendAndWait(&frameMetaData, sizeof(frameMetaData));
         tcpSocket.ConnectToServer("127.0.0.1");
         frameSize = dispFrame.total() * dispFrame.elemSize();
      }

      if (dispFrame.empty()) // end of file
      {
         break;
      }
      else
      {
         tcpSocket.SendLarge(dispFrame.data, frameSize);
         imshow("Video Send", dispFrame);
      }
      if (cv::waitKey(30) == 0) // wait for key press event
      {
         puts("Key press");
         break;
      }
   }

   return 0;
}

int main(int argv, char** argc)
{
   return SenderMainEntry(argc[1]);
}

最后,要看到视频在发送方和接收方运行,请先在命令行中输入启动接收方

> ./videoReceive

接下来,在另一个终端输入

> videoSend bird.avi

想法到实现:在 C++14 项目上播下第一颗种子 - CodeProject - 代码之家
© . All rights reserved.