TCP 通信
TCP 是面向連接、可靠、有序的字節流協議。
在機器人裡,TCP 常用於:上位機調試工具、遠程控制、日誌上傳、參數服務、局域網設備通信等。
注意:TCP 是“字節流”,不是“消息包”。所以以後必須面對:
粘包、半包、协议分帧
本節依舊先寫普通 main(),再寫類封裝版本。異步回調統一用 std::bind。
示例 1:普通 main() 裡寫同步 TCP Echo Server
程序目標
寫一個最小 TCP 服務端:
- 監聽
0.0.0.0: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;
// 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 服務端會阻塞:
accept()阻塞等待連接;read_some()阻塞等待數據;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:
- 異步接受客戶端連接;
- 每個連接創建一個
EchoSession; - 異步讀取客戶端數據;
- 異步寫回客戶端;
- 繼續讀取下一段數據。
完整代碼
#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()
讓異步回調持有 EchoSession 的 shared_ptr,保證回調執行時對象還活著。
async_read_some() 說明
socket_.async_read_some(buffer, handler);
作用:異步讀取當前到達的一些字節。
回調參數:
const boost::system::error_code& ec
std::size_t length
注意:它不保證讀到完整一條消息。
示例 4:類裡寫異步 TCP Client
程序目標
寫一個異步 TCP 客戶端:
- 異步連接服務器;
- 連接成功後發送一行數據;
- 讀取服務端回顯;
- 程序結束。
完整代碼
#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
所以真實項目必須設計協議,例如:
- 換行符分隔:
cmd_vel 0.1 0.0\n; - 固定長度包;
- 包頭 + 長度 + payload + CRC;
- protobuf / flatbuffers 等序列化協議。
本節總結
- 同步 TCP 適合理解流程,但會阻塞。
- 異步 TCP 需要
io.run()驅動。 - 服務端通常是
async_accept()+Session類。 - 每個 TCP 連接對應一個 socket。
shared_from_this()常用於保證 session 生命週期。- TCP 沒有消息邊界,真實項目必須設計分幀協議。