第 19.1.4 節

TCP communication

0瀏覽次數0訪問次數--跳出率--平均停留

TCP is a connection-oriented, reliable, ordered byte stream protocol. In robots, TCP is commonly used for: host computer debugging tools, remote control, log upload, parameter service, LAN device communication, etc.

Note: TCP is a 'byte stream', not a 'message packet'. Therefore, you must face:

粘包、半包、协议分帧

This section still starts with writing the ordinary main(), followed by the class-encapsulated version. Asynchronous callbacks uniformly use std::bind.


Example 1: Writing a synchronous TCP Echo Server in ordinary main()

program objective

Write a minimal TCP server:

  1. Monitor 0.0.0.0:9000;
  2. Waiting for a client connection;
  3. Read a piece of data sent by the client;
  4. 原样发回去;
  5. Program ended.

Full code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <iostream>
#include <string>

using boost::asio::ip::tcp;

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io;
    boost::system::error_code ec;

    // acceptor 负责监听 TCP 端口并接收客户端连接。
    tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 9000));

    std::cout << "server:监听 0.0.0.0:9000,等待客户端连接" << std::endl;

    tcp::socket socket(io);
    // 同步 accept 会阻塞,直到有客户端连接进来。
    acceptor.accept(socket, ec);
    if (ec)
    {
        std::cout << "server:accept 失败:" << ec.message() << std::endl;
        return 1;
    }

    std::cout << "server:客户端已连接" << std::endl;

    std::array<char, 1024> data;
    std::size_t n = socket.read_some(boost::asio::buffer(data), ec);
    if (ec)
    {
        std::cout << "server:读取失败:" << ec.message() << std::endl;
        return 1;
    }

    std::string msg(data.data(), n);
    std::cout << "server:收到 " << n << " 字节:" << msg;

    boost::asio::write(socket, boost::asio::buffer(data, n), ec);
    if (ec)
    {
        std::cout << "server:回写失败:" << ec.message() << std::endl;
        return 1;
    }

    std::cout << "server:已原样回写,程序结束" << std::endl;

    return 0;
}

运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。

Compile and run

Terminal 1: Run the server.

g++ demo1_tcp_sync_server.cpp -o demo1_tcp_sync_server -std=c++17 -lboost_system -pthread
./demo1_tcp_sync_server

Terminal 2: Connect to the server using nc.

echo "hello tcp" | nc 127.0.0.1 9000

Execution output and chronological order

Immediately after the server starts, output:

server:监听 0.0.0.0:9000,等待客户端连接

The server is now blocking at:

acceptor.accept(socket, ec);

After the client connects and sends data, the server outputs:

server:客户端已连接
server:收到 10 字节:hello tcp
server:已原样回写,程序结束

The client terminal will receive an echo:

hello tcp

Points to note for this example

A synchronous TCP server will block:

  1. accept() blocking wait for connection;
  2. read_some() blocking wait for data;
  3. write() blocking until write completes.

This is suitable for understanding the flow, but not suitable for the main thread of complex robot programs.

Key Function Explanation

tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 9000));

Purpose: Create a TCP listener that listens on port 9000 on all local IPv4 addresses.

acceptor.accept(socket, ec);

Function: Synchronously wait for client connection.

Return value: void.

After successful connection, socket represents the connection to this client.

socket.read_some(buffer, ec);

Synchronously reads some currently readable bytes.

Return value: the number of bytes read.

Note: It does not guarantee reading a complete message at once.


Example 2: Writing a synchronous TCP Client in a normal main()

program objective

Write a TCP client that connects to 127.0.0.1:9000, sends a line of data, and then reads the server echo.

Full code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <iostream>
#include <string>

using boost::asio::ip::tcp;

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io;
    boost::system::error_code ec;

    tcp::socket socket(io);
    tcp::endpoint server_endpoint(boost::asio::ip::make_address("127.0.0.1"), 9000);

    std::cout << "client:准备连接 127.0.0.1:9000" << std::endl;

    // 发起连接,成功后 socket 才能收发数据。
    socket.connect(server_endpoint, ec);
    if (ec)
    {
        std::cout << "client:连接失败:" << ec.message() << std::endl;
        return 1;
    }

    std::cout << "client:连接成功" << std::endl;

    std::string msg = "hello tcp\n";
    boost::asio::write(socket, boost::asio::buffer(msg), ec);
    if (ec)
    {
        std::cout << "client:发送失败:" << ec.message() << std::endl;
        return 1;
    }

    std::cout << "client:发送完成,等待回显" << std::endl;

    std::array<char, 1024> data;
    // 读取收到的数据,返回值表示本次实际读到的字节数。
    std::size_t n = socket.read_some(boost::asio::buffer(data), ec);
    if (ec)
    {
        std::cout << "client:读取失败:" << ec.message() << std::endl;
        return 1;
    }

    std::string reply(data.data(), n);
    std::cout << "client:收到回显:" << reply;

    return 0;
}

运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。

Compile and run

Terminal 1: First, run the server of Example 1.

./demo1_tcp_sync_server

Terminal 2: Run the client.

g++ demo2_tcp_sync_client.cpp -o demo2_tcp_sync_client -std=c++17 -lboost_system -pthread
./demo2_tcp_sync_client

Execution output and chronological order

Client output:

client:准备连接 127.0.0.1:9000
client:连接成功
client:发送完成,等待回显
client:收到回显:hello tcp

Server output:

server:监听 0.0.0.0:9000,等待客户端连接
server:客户端已连接
server:收到 10 字节:hello tcp
server:已原样回写,程序结束

Points to note for this example

The client must run after the server has started listening, otherwise the connection will fail:

Connection refused

socket.connect() Description

socket.connect(server_endpoint, ec);

Purpose: Synchronously connect to the server.

After successful connection, you can then read and write using the same socket.


Example 3: Writing an Asynchronous TCP Echo Session in a Class

program objective

Write an asynchronous TCP Echo Server:

  1. Accept client connections asynchronously;
  2. Each connection creates one EchoSession;
  3. Read client data asynchronously;
  4. Asynchronous write-back to the client;
  5. 已准备好继续读取下一段数据,请提供具体内容。

Full code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <functional>
#include <iostream>
#include <memory>

using boost::asio::ip::tcp;

class EchoSession : public std::enable_shared_from_this<EchoSession>
{
public:
    explicit EchoSession(boost::asio::io_context& io)
        : socket_(io)
    {
    }

    tcp::socket& socket()
    {
        return socket_;
    }

    void start()
    {
        std::cout << "session:开始读取客户端数据" << std::endl;
        do_read();
    }

private:
    void do_read()
    {
        socket_.async_read_some(boost::asio::buffer(data_),
                                std::bind(&EchoSession::on_read,
                                          shared_from_this(),
                                          std::placeholders::_1,
                                          std::placeholders::_2));
    }

    void on_read(const boost::system::error_code& ec, std::size_t length)
    {
        if (ec)
        {
            std::cout << "session:读取结束或失败:" << ec.message() << std::endl;
            return;
        }

        std::cout << "session:收到 " << length << " 字节,准备回写" << std::endl;

        boost::asio::async_write(socket_,
                                 boost::asio::buffer(data_, length),
                                 std::bind(&EchoSession::on_write,
                                           shared_from_this(),
                                           std::placeholders::_1,
                                           std::placeholders::_2));
    }

    void on_write(const boost::system::error_code& ec, std::size_t length)
    {
        if (ec)
        {
            std::cout << "session:写失败:" << ec.message() << std::endl;
            return;
        }

        std::cout << "session:回写完成,字节数 = " << length << std::endl;

        do_read();
    }

private:
    tcp::socket socket_;
    std::array<char, 1024> data_;
};

class EchoServer
{
public:
    EchoServer(boost::asio::io_context& io, unsigned short port)
        : io_(io),
          acceptor_(io, tcp::endpoint(tcp::v4(), port))
    {
        std::cout << "server:监听端口 " << port << std::endl;
        do_accept();
    }

private:
    void do_accept()
    {
        std::shared_ptr<EchoSession> session = std::make_shared<EchoSession>(io_);

        acceptor_.async_accept(session->socket(),
                               std::bind(&EchoServer::on_accept,
                                         this,
                                         session,
                                         std::placeholders::_1));
    }

    void on_accept(std::shared_ptr<EchoSession> session,
                   const boost::system::error_code& ec)
    {
        if (ec)
        {
            std::cout << "server:accept 失败:" << ec.message() << std::endl;
        }
        else
        {
            std::cout << "server:有新客户端连接" << std::endl;
            session->start();
        }

        do_accept();
    }

private:
    boost::asio::io_context& io_;
    // acceptor 负责监听 TCP 端口并接收客户端连接。
    tcp::acceptor acceptor_;
};

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io;
    EchoServer server(io, 9000);

    std::cout << "main:调用 io.run(),服务端开始运行" << std::endl;

    // 启动事件循环,前面注册的异步任务会在这里被调度执行。
    io.run();

    std::cout << "main:io.run() 返回" << std::endl;

    return 0;
}

运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。

Compile and run

Terminal 1: Run the asynchronous server.

g++ demo3_async_tcp_echo_server.cpp -o demo3_async_tcp_echo_server -std=c++17 -lboost_system -pthread
./demo3_async_tcp_echo_server

Terminal 2: Connection Test.

nc 127.0.0.1 9000

Then input:

hello
robot

Execution output and chronological order

Server startup output:

server:监听端口 9000
main:调用 io.run(),服务端开始运行

After the client connects:

server:有新客户端连接
session:开始读取客户端数据

After the client inputs hello and presses Enter, the server outputs:

session:收到 6 字节,准备回写
session:回写完成,字节数 = 6

The client will see an echo:

hello

After continuing to input robot and pressing Enter, the server outputs:

session:收到 6 字节,准备回写
session:回写完成,字节数 = 6

Points to note for this example

This program will not exit automatically because the server continuously calls:

do_accept();
do_read();

So io.run() will keep running.

shared_from_this() Description

The most common issue with asynchronous sessions is object lifecycle.

If the callback has not yet been executed and the EchoSession object has already been released, it will crash.

So here, use:

std::enable_shared_from_this<EchoSession>
shared_from_this()

Have the asynchronous callback hold EchoSession's shared_ptr to ensure the object is still alive when the callback executes.

async_read_some() Description

socket_.async_read_some(buffer, handler);

Asynchronously reads some currently arrived bytes.

Callback parameter:

const boost::system::error_code& ec
std::size_t length

Note: It does not guarantee reading a complete message.


Example 4: Writing an Asynchronous TCP Client in a Class

program objective

Write an asynchronous TCP client:

  1. Connect to server asynchronously;
  2. After successful connection, send a line of data;
  3. Read server echo
  4. Program ended.

Full code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <functional>
#include <iostream>
#include <memory>
#include <string>

using boost::asio::ip::tcp;

class TcpClient
{
public:
    TcpClient(boost::asio::io_context& io,
              const std::string& host,
              unsigned short port)
        : socket_(io),
          endpoint_(boost::asio::ip::make_address(host), port),
          msg_("hello async tcp\n")
    {
    }

    void start()
    {
        std::cout << "client:开始异步连接" << std::endl;

        socket_.async_connect(endpoint_,
                              std::bind(&TcpClient::on_connect,
                                        this,
                                        std::placeholders::_1));
    }

private:
    void on_connect(const boost::system::error_code& ec)
    {
        if (ec)
        {
            std::cout << "client:连接失败:" << ec.message() << std::endl;
            return;
        }

        std::cout << "client:连接成功,开始异步发送" << std::endl;

        boost::asio::async_write(socket_,
                                 boost::asio::buffer(msg_),
                                 std::bind(&TcpClient::on_write,
                                           this,
                                           std::placeholders::_1,
                                           std::placeholders::_2));
    }

    void on_write(const boost::system::error_code& ec, std::size_t length)
    {
        if (ec)
        {
            std::cout << "client:发送失败:" << ec.message() << std::endl;
            return;
        }

        std::cout << "client:发送完成,字节数 = " << length << ",等待回显" << std::endl;

        // 读取收到的数据,返回值表示本次实际读到的字节数。
        socket_.async_read_some(boost::asio::buffer(data_),
                                std::bind(&TcpClient::on_read,
                                          this,
                                          std::placeholders::_1,
                                          std::placeholders::_2));
    }

    void on_read(const boost::system::error_code& ec, std::size_t length)
    {
        if (ec)
        {
            std::cout << "client:读取失败:" << ec.message() << std::endl;
            return;
        }

        std::string reply(data_.data(), length);
        std::cout << "client:收到回显:" << reply;
    }

private:
    tcp::socket socket_;
    tcp::endpoint endpoint_;
    std::string msg_;
    std::array<char, 1024> data_;
};

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io;

    TcpClient client(io, "127.0.0.1", 9000);
    client.start();

    std::cout << "main:调用 io.run()" << std::endl;

    // 启动事件循环,前面注册的异步任务会在这里被调度执行。
    io.run();

    std::cout << "main:io.run() 返回" << std::endl;

    return 0;
}

运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。

Compile and run

Terminal 1: First run the asynchronous server of Example 3.

./demo3_async_tcp_echo_server

Terminal 2: Run the client.

g++ demo4_async_tcp_client.cpp -o demo4_async_tcp_client -std=c++17 -lboost_system -pthread
./demo4_async_tcp_client

Execution output and chronological order

Client output:

client:开始异步连接
main:调用 io.run()
client:连接成功,开始异步发送
client:发送完成,字节数 = 16,等待回显
client:收到回显:hello async tcp
main:io.run() 返回

Server output like:

server:有新客户端连接
session:开始读取客户端数据
session:收到 16 字节,准备回写
session:回写完成,字节数 = 16
session:读取结束或失败:End of file

Points to note for this example

msg_ and data_ are both member variables, to ensure that memory remains valid during asynchronous writes and asynchronous reads.

Do not write such dangerous code inside on_connect():

std::string msg = "hello\n";
boost::asio::async_write(socket_, boost::asio::buffer(msg), handler);

Because after on_connect() returns, msg is destroyed.


TCP Sticky and Half Packet Reminder

TCP is a byte stream. You send three times:

A
B
C

The receiving end may read all at once:

ABC

It might also be read in two parts:

A
BC

Even a message is split open:

AB
C

So real projects need to design protocols, for example:

  1. Separated by newline characters: cmd_vel 0.1 0.0\n;
  2. Fixed-length packet;
  3. Header + Length + payload + CRC;
  4. protobuf / flatbuffers and other serialization protocols.

Section Summary

  1. Synchronous TCP is suitable for understanding the flow, but it blocks.
  2. Asynchronous TCP requires io.run() driver.
  3. The server side is usually of the async_accept() + Session class.
  4. Each TCP connection corresponds to a socket.
  5. shared_from_this() is often used to ensure the session lifecycle.
  6. TCP has no message boundaries; real-world projects must design a framing protocol.
音乐页