跳到正文

Wiki

TCP 通訊

約 5 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

TCP 是面向連線、可靠、有序的位元組流協議。
在機器人裡,TCP 常用於:上位機除錯工具、遠端控制、日誌上傳、引數服務、區域網裝置通訊等。

注意:TCP 是“位元組流”,不是“訊息包”。所以以後必須面對:

粘包、半包、协议分帧

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


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

1.1.程式目標

寫一個最小 TCP 服務端:

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

1.2.完整程式碼

#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.3.編譯執行

終端 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

1.4.執行輸出與時間順序

服務端啟動後立刻輸出:

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

此時服務端阻塞在:

acceptor.accept(socket, ec);

客戶端連線併發送資料後,服務端輸出:

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

客戶端終端會收到回顯:

hello tcp

1.5.本示例需要注意的點

同步 TCP 服務端會阻塞:

  1. accept() 阻塞等待連線;
  2. read_some() 阻塞等待資料;
  3. write() 阻塞等待寫完成。

這適合理解流程,但不適合複雜機器人程式主執行緒。

1.6.關鍵函式說明

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

作用:建立 TCP 監聽器,監聽本機所有 IPv4 地址的 9000 埠。

1.6.2.acceptor.accept(socket, ec);

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

返回值:void

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

1.6.3.socket.read_some(buffer, ec);

作用:同步讀取當前能讀到的一些位元組。

返回值:讀取到的位元組數。

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


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

2.1.程式目標

寫一個 TCP 客戶端,連線 127.0.0.1:9000,傳送一行資料,再讀取服務端回顯。

2.2.完整程式碼

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

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

2.3.編譯執行

終端 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

2.4.執行輸出與時間順序

客戶端輸出:

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

服務端輸出:

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

2.5.本示例需要注意的點

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

Connection refused

2.6.socket.connect() 說明

socket.connect(server_endpoint, ec);

作用:同步連線伺服器。

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


3.示例 3:類裡寫非同步 TCP Echo Session

3.1.程式目標

寫一個非同步 TCP Echo Server:

  1. 非同步接受客戶端連線;
  2. 每個連線建立一個 EchoSession
  3. 非同步讀取客戶端資料;
  4. 非同步寫回客戶端;
  5. 繼續讀取下一段資料。

3.2.完整程式碼

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

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

3.3.編譯執行

終端 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

3.4.執行輸出與時間順序

服務端啟動後輸出:

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

客戶端連線後:

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

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

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

客戶端會看到回顯:

hello

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

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

3.5.本示例需要注意的點

這個程式不會自動退出,因為服務端持續呼叫:

do_accept();
do_read();

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

3.6.shared_from_this() 說明

非同步 session 最容易出問題的是物件生命週期。

如果回撥還沒執行,EchoSession 物件已經被釋放,就會崩潰。

所以這裡用:

std::enable_shared_from_this<EchoSession>
shared_from_this()

讓非同步回撥持有 EchoSessionshared_ptr,保證回撥執行時物件還活著。

3.7.async_read_some() 說明

socket_.async_read_some(buffer, handler);

作用:非同步讀取當前到達的一些位元組。

回撥引數:

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

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


4.示例 4:類裡寫非同步 TCP Client

4.1.程式目標

寫一個非同步 TCP 客戶端:

  1. 非同步連線伺服器;
  2. 連線成功後傳送一行資料;
  3. 讀取服務端回顯;
  4. 程式結束。

4.2.完整程式碼

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

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

4.3.編譯執行

終端 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

4.4.執行輸出與時間順序

客戶端輸出:

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

服務端輸出類似:

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

4.5.本示例需要注意的點

msg_data_ 都是成員變數,是為了保證非同步寫和非同步讀期間記憶體一直有效。

不要在 on_connect() 裡寫這種危險程式碼:

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

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


5.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 等序列化協議。

6.本節總結

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