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

使用 Boost.Asio 进行 C++ 套接字编程:TCP 服务器和客户端

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (30投票s)

2018年10月22日

CPOL

10分钟阅读

viewsIcon

250665

downloadIcon

3121

本文将帮助您开始使用 C++ 进行套接字编程。我们将使用 C++ 中的 boost.asio 库构建一个 TCP 服务器和客户端。

引言

套接字编程对于程序员来说并不是什么新概念。自互联网问世以来,它就改变了人们对互联网应用程序的看法。这就是网络编程模型的出现。套接字(socket)是这种网络编程最基本的技术。暂且不谈技术本身,在大多数情况下,它仅仅是一个工作的客户端-服务器模型。服务器负责提供客户端请求的信息或所需的服务。以下类比将帮助您理解该模型。

但是信息是如何传输的呢?这涉及到传输层的网络服务,通常称为 TCP/IP(传输控制协议/互联网协议)。例如,当您打开浏览器并搜索某些内容时,您仅仅是向服务器请求信息,通常是通过 HTTP(别忘了 HTTP 本身只是一个使用 TCP/IP 服务的应用程序)。那么套接字在哪里呢?让我回到我说的套接字是“基础”的那句话,它们为这些协议提供了编程接口。总的来说,套接字为两个进程或程序在网络上通信提供了一种方式。套接字提供了足够的灵活性和透明性,同时几乎没有通信开销。

来源

为什么是 C++?正如我之前提到的,套接字只是提供了一个网络编程接口,与实现所用的编程语言无关。C++ 在这方面可能是最佳选择,因为它带来了速度和效率。有些人可能不同意我的观点,因为该语言可能隐含的复杂性,包括但不限于手动内存管理、模板语法、库不兼容、编译器等。但我认为不同。C++ 允许您深入了解底层实际发生的情况,尽管对计算机网络的良好概念和知识是毋庸置疑的。本文将帮助您使用 boost 库在 C++ 中轻松入门套接字编程。但在深入代码之前,让我们再澄清几点。

套接字编程到底是什么?

让我们谈谈套接字到底是什么,以及它在通信中扮演的角色。

套接字仅仅是一个双向通信链路的一个端点。它代表了两个实体在网络上尝试通信的单个连接,这两个实体通常是服务器和客户端。也可以设置两个以上的实体进行通信,但需要使用多个套接字。

这种基于套接字的通信是通过网络进行的;一个端点可以是您的计算机,另一个端点可以是世界另一端(再次以浏览为例)或同一台机器(本地主机)。现在的问题来了:服务器如何知道客户端正在请求连接,以及请求的是哪个服务?这都是 IP 地址端口号的游戏。每台计算机都有一个特定的 IP 地址用于标识它。(如果您访问一个网站,那么该名称最终会被解析为 IP 地址。)服务则通过端口号来区分。

现在总结一下:当客户端请求服务器提供服务时,它会打开一个套接字,并通过指定其 IP 地址和端口号(以便服务器知道要提供哪个服务)将请求发送给服务器。服务器将接受连接请求,并传输数据或提供任何其他请求的服务。请求得到满足后,连接将关闭。观察下图中的工作流程。

为什么选择 Boost.Asio?

编写可移植且易于维护的网络代码长期以来一直是一个问题。C++ 通过引入 boost.asio 来解决这个问题。它是一个跨平台的 C++ 库,用于网络和低级 I/O 编程,为开发人员提供了一种使用现代 C++ 方法的一致的异步模型。以下是它提供的一些功能列表:

  • 跨平台网络代码(代码可在 Windows、Linux 等上运行)
  • IPv4 和 IPv6 支持
  • 异步事件支持
  • 计时器支持
  • iostream 兼容性

还有更多。您可以在此处获取该库的完整概述。

我们不会深入研究网络编程,而是开发一个简单的客户端-服务器模型,看看它是如何工作的。所以,废话不多说,让我们开始吧。

环境设置

我目前使用的是 Linux (18.04 LTS),所以将涵盖其环境设置。

要开始,我们只需要以下工具:

  • boost.asio
  • C++ 编译器(最好是 g++)
  • 文本编辑器

在 Linux 上获取 asio 的最简单方法是执行以下命令:

$ sudo apt-get install libboost-all-dev  

如果您使用的是其他平台,或者上面的方法不适合您,请按照此处的文档在您的系统上获取 asio。

下一步是确保您的编译器上已安装 C++ 编译器。我使用的是 g++。您可以在 Linux 中使用以下命令获取:

$ sudo apt-get install g++

一旦您有了编译器,您就可以继续了。我没有特定的文本编辑器偏好。您可以选择您喜欢的任何一款。

现在我们已经准备好一切,可以开始为我们的 TCP 服务器-客户端模型编写代码了。

TCP 服务器

正如我们在文章前面提到的,服务器会指定一个地址供客户端发出请求。服务器监听新连接并作出相应响应。现在,以下是我们的服务器开发步骤:

当然,在做其他事情之前,我们需要导入我们的库。所以,我们开始:

#include <iostream>
#include <boost/asio.hpp>

using namespace boost::asio;
using ip::tcp;
using std::string;
using std::cout;
using std::endl;

using namespace std 被认为是一种不好的做法,因为它会在全局范围内导入各种名称,并可能导致歧义。由于我们只需要 std 命名空间中的三个名称,最好单独导入它们,或者您随意。

我们希望服务器能够接收来自客户端的消息,然后进行响应。为此,我们需要两个用于读和写的功能。

string read_(tcp::socket & socket) {
       boost::asio::streambuf buf;
       boost::asio::read_until( socket, buf, "\n" );
       string data = boost::asio::buffer_cast<const char*>(buf.data());
       return data;
}
void send_(tcp::socket & socket, const string& message) {
       const string msg = message + "\n";
       boost::asio::write( socket, boost::asio::buffer(message) );
}

让我们稍微分解一下。在这里,我们使用 tcp socket 进行通信。boost::asio 中的 read_untilwrite 函数已用于执行所需的功能。boost::asio::buffer 创建一个用于通信数据的缓冲区。

现在我们有了函数,让我们启动服务器:

int main() {
      boost::asio::io_service io_service;
//listen for new connection
      tcp::acceptor acceptor_(io_service, tcp::endpoint(tcp::v4(), 1234 ));
//socket creation 
      tcp::socket socket_(io_service);
//waiting for connection
      acceptor_.accept(socket_);
//read operation
      string message = read_(socket_);
      cout << message << endl;
//write operation
      send_(socket_, "Hello From Server!");
      cout << "Servent sent Hello message to Client!" << endl;
   return 0;
}

使用 asio 的程序需要一个 io_service 对象。tcp::acceptor 用于监听客户端请求的连接。我们将两个参数传递给函数:一个是之前声明的同一个 io_service 对象,另一个是连接的端点,初始化为 ipv4,端口为 1234。接下来,服务器将创建一个套接字并等待与客户端的连接。一旦建立连接,我们的读写操作将被执行,然后连接将被关闭。

TCP 客户端

我们也需要通信的另一端,即请求服务器的客户端。基本结构与我们为服务器所做的相同。

#include <iostream>
#include <boost/asio.hpp>

using namespace boost::asio;
using ip::tcp;
using std::string;
using std::cout;
using std::endl;

这些是与服务器相同的导入。没什么新的。

int main() {
     boost::asio::io_service io_service;
//socket creation
     tcp::socket socket(io_service);
//connection
     socket.connect( tcp::endpoint( boost::asio::ip::address::from_string("127.0.0.1"), 1234 ));
// request/message from client
     const string msg = "Hello from Client!\n";
     boost::system::error_code error;
     boost::asio::write( socket, boost::asio::buffer(msg), error );
     if( !error ) {
        cout << "Client sent hello message!" << endl;
     }
     else {
        cout << "send failed: " << error.message() << endl;
     }
 // getting response from server
    boost::asio::streambuf receive_buffer;
    boost::asio::read(socket, receive_buffer, boost::asio::transfer_all(), error);
    if( error && error != boost::asio::error::eof ) {
        cout << "receive failed: " << error.message() << endl;
    }
    else {
        const char* data = boost::asio::buffer_cast<const char*>(receive_buffer.data());
        cout << data << endl;
    }
    return 0;
}

我们再次从创建 io_service 对象和创建套接字开始。我们需要使用 localhost(IP 127.0.0.1)连接到服务器,并指定与服务器相同的端口以成功建立连接。连接建立后,我们将使用 boost::asio::write 发送一条 hello 消息给服务器。如果消息传输成功,服务器将发送回响应。为此,我们有 boost::asio::read 函数来读取响应。现在让我们运行我们的程序看看效果。

通过执行以下命令来编译并运行服务器:

 $ g++ server.cpp -o server –lboost_system
 $ ./server

切换到另一个终端窗口来运行客户端。

$ g++ client.cpp -o client –lboost_system
$ ./client   

从上面的输出观察工作流程。客户端发送了它的请求,说“hello”给服务器,之后服务器响应“hello”。数据传输完成后,连接关闭。

TCP 异步服务器

上面的程序解释了我们简单的同步 TCP 服务器和客户端,我们在其中按顺序执行操作,即先从套接字读取,然后写入。每个操作都是阻塞的,这意味着读取操作必须先完成,然后我们才能进行写入操作。但是,如果有一个以上的客户端试图连接到服务器怎么办?如果我们不希望在读写套接字时中断主程序,就需要一个多线程的 TCP 客户端-服务器来处理这种情况。另一个选择是拥有一个异步服务器。我们可以启动操作,但不知道它何时结束,我们也不需要提前知道,因为它不是阻塞的。其他操作可以并行执行。可以把同步想象成对讲机,一次只能一个人说话;而异步更像普通手机。现在我们已经了解了基础知识,让我们深入了解并尝试创建一个异步服务器。

//importing libraries
#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/enable_shared_from_this.hpp>

using namespace boost::asio;
using ip::tcp;
using std::cout;
using std::endl;

我们有两个新的导入:bindenable_shared_from_this。前者将用于将任何参数绑定到特定值并将输入参数路由到任意位置。后者用于获取一个有效的 shared_ptr 实例。

让我们定义一个类来处理连接,如下所示:

class con_handler : public boost::enable_shared_from_this<con_handler>
{
private:
  tcp::socket sock;
  std::string message="Hello From Server!";
  enum { max_length = 1024 };
  char data[max_length];

public:
  typedef boost::shared_ptr<con_handler> pointer;
  con_handler(boost::asio::io_service& io_service): sock(io_service){}
// creating the pointer
  static pointer create(boost::asio::io_service& io_service)
  {
    return pointer(new con_handler(io_service));
  }
//socket creation
  tcp::socket& socket()
  {
    return sock;
  }

  void start()
  {
    sock.async_read_some(
        boost::asio::buffer(data, max_length),
        boost::bind(&con_handler::handle_read,
                    shared_from_this(),
                    boost::asio::placeholders::error,
                    boost::asio::placeholders::bytes_transferred));
  
    sock.async_write_some(
        boost::asio::buffer(message, max_length),
        boost::bind(&con_handler::handle_write,
                  shared_from_this(),
                  boost::asio::placeholders::error,
                  boost::asio::placeholders::bytes_transferred));
  }

  void handle_read(const boost::system::error_code& err, size_t bytes_transferred)
  {
    if (!err) {
         cout << data << endl;
    } else {
         std::cerr << "error: " << err.message() << std::endl;
         sock.close();
    }
  }
  void handle_write(const boost::system::error_code& err, size_t bytes_transferred)
  {
    if (!err) {
       cout << "Server sent Hello message!"<< endl;
    } else {
       std::cerr << "error: " << err.message() << endl;
       sock.close();
    }
  }
};

shared_ptrenabled_shared_from_this 用于在任何引用它的操作中保持我们的对象处于活动状态。然后,我们以与同步服务器相同的方式创建了套接字。现在是时候指定我们要通过该套接字执行的功能了。我们使用 async_read_someasync_write_some 函数来实现与我们之前开发的服务器相同的功能,但现在是异步的。boost::bind 用于绑定参数并将它们路由到 handle_read/writehandle_read/write 现在将负责任何进一步的操作。如果您查看 handle_read/write 的定义,它具有与 boot::bind 的最后两个参数相同的参数,并在数据成功传输或未成功传输到客户端/服务器之间执行某些操作。

class Server 
{
private:
   tcp::acceptor acceptor_;
   void start_accept()
   {
    // socket
     con_handler::pointer connection = con_handler::create(acceptor_.get_io_service());

    // asynchronous accept operation and wait for a new connection.
     acceptor_.async_accept(connection->socket(),
        boost::bind(&Server::handle_accept, this, connection,
        boost::asio::placeholders::error));
  }
public:
//constructor for accepting connection from client
  Server(boost::asio::io_service& io_service): acceptor_(io_service, tcp::endpoint(tcp::v4(), 1234))
  {
     start_accept();
  }
  void handle_accept(con_handler::pointer connection, const boost::system::error_code& err)
  {
    if (!err) {
      connection->start();
    }
    start_accept();
  }
};

我们的 server 类将创建一个套接字并开始 accept 操作以异步等待连接。我们还为 server 定义了一个构造函数,以便在指定端口上开始监听连接。如果没有发生错误,将建立与客户端的连接。这是我们的主函数。

int main(int argc, char *argv[])
{
  try
    {
    boost::asio::io_service io_service;  
    Server server(io_service);
    io_service.run();
    }
  catch(std::exception& e)
    {
    std::cerr << e.what() << endl;
    }
  return 0;
}

同样,我们需要 io_service 对象以及 Server 类的实例。run() 函数将阻塞直到所有工作完成,直到 io_service 被停止。

是时候放出我们的怪物了。使用以下命令编译并运行上述代码:

$ g++ async_server.cpp -o async_server –lboost_system
$ ./async_server

再次运行您的客户端。

$ ./client

请注意,客户端在交换数据后关闭了连接,但服务器仍在运行。可以建立新的连接,否则服务器将一直运行直到被明确要求停止。如果我们想停止它,我们可以这样做:

boost::optional<boost::asio::io_service::work> work =    boost::in_place(boost::ref(io_service));
work = boost::none;

这将告诉 run() 函数所有工作已完成,并且不再阻塞。

结语

本文并非旨在向您展示最佳实践或让您成为网络编程专家,而是专注于让您轻松入门 boost.asio 的套接字编程。它是一个非常方便的库,因此如果您对高端网络编程感兴趣,我鼓励您深入研究并多加尝试。此外,服务器和客户端的源代码都已附加。随意进行一些更改,如果您有好的想法,请告诉我。

© . All rights reserved.