第 19.1.4 節

TCP 通信

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

TCP 是面向連接、可靠、有序的字節流協議。
在機器人裏,TCP 常用於:上位機調試工具、遠程控制、日誌上傳、參數服務、局域網設備通信等。

注意:TCP 是“字節流”,不是“消息包”。所以以後必須面對:

粘包、半包、协议分帧

本節依舊先寫普通 main(),再寫類封裝版本。異步回調統一用 std::bind


示例 1:普通 main() 裏寫同步 TCP Echo Server

程序目標

寫一個最小 TCP 服務端:

  1. 監聽 0.0.0.0:9000
  2. 等待一個客戶端連接;
  3. 讀取客戶端發來的一段數據;
  4. 原樣發回去;
  5. 程序結束。

完整代碼

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

運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。

編譯運行

終端 1:運行服務端。

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

終端 2:用 nc 連接服務端。

echo "hello tcp" | nc 127.0.0.1 9000

運行輸出與時間順序

服務端啓動後立刻輸出:

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

此時服務端阻塞在:

acceptor.accept(socket, ec);

客戶端連接併發送數據後,服務端輸出:

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

客戶端終端會收到回顯:

hello tcp

本示例需要注意的點

同步 TCP 服務端會阻塞:

  1. accept() 阻塞等待連接;
  2. read_some() 阻塞等待數據;
  3. write() 阻塞等待寫完成。

這適合理解流程,但不適合複雜機器人程序主線程。

關鍵函數說明

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

作用:創建 TCP 監聽器,監聽本機所有 IPv4 地址的 9000 端口。

acceptor.accept(socket, ec);

作用:同步等待客戶端連接。

返回值:void

連接成功後,socket 表示和這個客戶端的連接。

socket.read_some(buffer, ec);

作用:同步讀取當前能讀到的一些字節。

返回值:讀取到的字節數。

注意:它不保證一次讀到完整一條消息。


示例 2:普通 main() 裏寫同步 TCP Client

程序目標

寫一個 TCP 客戶端,連接 127.0.0.1:9000,發送一行數據,再讀取服務端回顯。

完整代碼

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

運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。

編譯運行

終端 1:先運行示例 1 的服務端。

./demo1_tcp_sync_server

終端 2:運行客戶端。

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

運行輸出與時間順序

客戶端輸出:

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

服務端輸出:

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

本示例需要注意的點

客戶端必須在服務端已經監聽後再運行,否則會連接失敗:

Connection refused

socket.connect() 說明

socket.connect(server_endpoint, ec);

作用:同步連接服務器。

連接成功後,後續可以通過同一個 socket 進行讀寫。


示例 3:類裏寫異步 TCP Echo Session

程序目標

寫一個異步 TCP Echo Server:

  1. 異步接受客戶端連接;
  2. 每個連接創建一個 EchoSession
  3. 異步讀取客戶端數據;
  4. 異步寫回客戶端;
  5. 繼續讀取下一段數據。

完整代碼

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

運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。

編譯運行

終端 1:運行異步服務端。

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

終端 2:連接測試。

nc 127.0.0.1 9000

然後輸入:

hello
robot

運行輸出與時間順序

服務端啓動後輸出:

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

客戶端連接後:

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

客戶端輸入 hello 回車後,服務端輸出:

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

客戶端會看到回顯:

hello

繼續輸入 robot 回車後,服務端輸出:

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

本示例需要注意的點

這個程序不會自動退出,因爲服務端持續調用:

do_accept();
do_read();

所以 io.run() 會一直運行。

shared_from_this() 說明

異步 session 最容易出問題的是對象生命週期。

如果回調還沒執行,EchoSession 對象已經被釋放,就會崩潰。

所以這裏用:

std::enable_shared_from_this<EchoSession>
shared_from_this()

讓異步回調持有 EchoSessionshared_ptr,保證回調執行時對象還活着。

async_read_some() 說明

socket_.async_read_some(buffer, handler);

作用:異步讀取當前到達的一些字節。

回調參數:

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

注意:它不保證讀到完整一條消息。


示例 4:類裏寫異步 TCP Client

程序目標

寫一個異步 TCP 客戶端:

  1. 異步連接服務器;
  2. 連接成功後發送一行數據;
  3. 讀取服務端回顯;
  4. 程序結束。

完整代碼

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

運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。

編譯運行

終端 1:先運行示例 3 的異步服務端。

./demo3_async_tcp_echo_server

終端 2:運行客戶端。

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

運行輸出與時間順序

客戶端輸出:

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

服務端輸出類似:

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

本示例需要注意的點

msg_data_ 都是成員變量,是爲了保證異步寫和異步讀期間內存一直有效。

不要在 on_connect() 裏寫這種危險代碼:

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

因爲 on_connect() 返回後,msg 就銷燬了。


TCP 粘包和半包提醒

TCP 是字節流。你發送三次:

A
B
C

接收端可能一次讀到:

ABC

也可能分兩次讀到:

A
BC

甚至一條消息被拆開:

AB
C

所以真實項目必須設計協議,例如:

  1. 換行符分隔:cmd_vel 0.1 0.0\n
  2. 固定長度包;
  3. 包頭 + 長度 + payload + CRC;
  4. protobuf / flatbuffers 等序列化協議。

本節總結

  1. 同步 TCP 適合理解流程,但會阻塞。
  2. 異步 TCP 需要 io.run() 驅動。
  3. 服務端通常是 async_accept() + Session 類。
  4. 每個 TCP 連接對應一個 socket。
  5. shared_from_this() 常用於保證 session 生命週期。
  6. TCP 沒有消息邊界,真實項目必須設計分幀協議。
音乐页