Skip to content

Wiki

TCP communication

About 12 min read

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

Note: TCP is a "byte stream", not a "message packet". So from now on, we must face:

粘包、半包、协议分帧

In this section, we will still first write the ordinary main(), and then write the class-encapsulated version. Asynchronous callbacks uniformly use std::bind.


1.Example 1: Ordinary main() write a synchronous TCP Echo Server in it

1.1.Program Objectives

Write a minimal TCP server:

  1. listen on 0.0.0.0:9000;
  2. wait for a client connection;
  3. read a segment of data sent by the client;
  4. send it back as is;
  5. the program ends.

1.2.Complete code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <print>
#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::println("server:监听 0.0.0.0:9000,等待客户端连接");

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

    std::println("server:客户端已连接");

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

    std::string msg(data.data(), n);
    std::print("server:收到 {} 字节:{}", n, msg);

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

    std::println("server:已原样回写,程序结束");

    return 0;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

1.3.Compile and run

Terminal 1: Run the server.

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

Terminal 2: Use nc to connect to the server.

echo "hello tcp" | nc 127.0.0.1 9000

1.4.Run output and chronological order

Output immediately after the server starts:

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

At this point, the server side blocks 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 the echo:

hello tcp

1.5.Points to note in this example

A synchronous TCP server will block:

  1. accept() Block waiting for connection;
  2. read_some() Block waiting for data;
  3. write() Block waiting for write completion.

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

1.6.Key Function Description

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

Function: Creates a TCP listener that listens on port 9000 on all IPv4 addresses on the local machine.

1.6.2.acceptor.accept(socket, ec);

Function: synchronously wait for a client connection.

Return value:void.

After a successful connection,socket indicates the connection to this client.

1.6.3.socket.read_some(buffer, ec);

Function: Synchronously reads some bytes that can currently be read.

Return value: the number of bytes read.

Note: It does not guarantee reading a complete message in one go.


2.Example 2: Ordinary main() Writing a Synchronous TCP Client in

2.1.Program Objectives

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

2.2.Complete code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <print>
#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::println("client:准备连接 127.0.0.1:9000");

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

    std::println("client:连接成功");

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

    std::println("client:发送完成,等待回显");

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

    std::string reply(data.data(), n);
    std::print("client:收到回显:{}", reply);

    return 0;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

2.3.Compile and run

Terminal 1: First run the server side 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++23 -lboost_system -pthread
./demo2_tcp_sync_client

2.4.Run 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:已原样回写,程序结束

2.5.Points to note in this example

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

Connection refused

2.6.socket.connect() Description

socket.connect(server_endpoint, ec);

Function: Synchronously connect to the server.

After a successful connection, subsequent reads and writes can be performed through the same socket.


3.Example 3: Writing an asynchronous TCP Echo Session in a class

3.1.Program Objectives

Write an asynchronous TCP Echo Server:

  1. Asynchronously accept client connections;
  2. Create one per connection EchoSession;
  3. Asynchronously read client data;
  4. Asynchronously write back to the client;
  5. Continue reading the next segment of data.

3.2.Complete code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <functional>
#include <print>
#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::println("session:开始读取客户端数据");
        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::println("session:读取结束或失败:{}", ec.message());
            return;
        }

        std::println("session:收到 {} 字节,准备回写", length);

        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::println("session:写失败:{}", ec.message());
            return;
        }

        std::println("session:回写完成,字节数 = {}", length);

        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::println("server:监听端口 {}", port);
        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::println("server:accept 失败:{}", ec.message());
        }
        else
        {
            std::println("server:有新客户端连接");
            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::println("main:调用 io.run(),服务端开始运行");

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

    std::println("main:io.run() 返回");

    return 0;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

3.3.Compile and run

Terminal 1: Run the asynchronous server.

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

Terminal 2: Connection test.

nc 127.0.0.1 9000

Then enter:

hello
robot

3.4.Run output and chronological order

After the server starts, output:

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

After the client connects:

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

Client input hello After pressing Enter, the server outputs:

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

The client will see the echo:

hello

Continue typing robot After pressing Enter, the server outputs:

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

3.5.Points to note in this example

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

do_accept();
do_read();

So io.run() will keep running.

3.6.shared_from_this() Description

The most error-prone aspect of asynchronous sessions is object lifecycle.

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

So here, use:

std::enable_shared_from_this<EchoSession>
shared_from_this()

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

3.7.async_read_some() Description

socket_.async_read_some(buffer, handler);

Function: asynchronously reads some bytes that have currently arrived.

Callback parameter:

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

Note: It does not guarantee reading a complete message.


4.Example 4: Writing an asynchronous TCP Client in a class

4.1.Program Objectives

Write an asynchronous TCP client:

  1. Asynchronously connect to the server;
  2. After a successful connection, send one line of data;
  3. Read the server echo;
  4. The program ends.

4.2.Complete code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <functional>
#include <print>
#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::println("client:开始异步连接");

        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::println("client:连接失败:{}", ec.message());
            return;
        }

        std::println("client:连接成功,开始异步发送");

        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::println("client:发送失败:{}", ec.message());
            return;
        }

        std::println("client:发送完成,字节数 = {},等待回显", length);

        // 读取收到的数据,返回值表示本次实际读到的字节数。
        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::println("client:读取失败:{}", ec.message());
            return;
        }

        std::string reply(data_.data(), length);
        std::print("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::println("main:调用 io.run()");

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

    std::println("main:io.run() 返回");

    return 0;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

4.3.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++23 -lboost_system -pthread
./demo4_async_tcp_client

4.4.Run output and chronological order

Client output:

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

The server outputs something like:

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

4.5.Points to note in this example

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

Do not write this kind of dangerous code in on_connect():

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

Because on_connect() after returning, msg is destroyed.


5.TCP Sticky Packets and Half Packets Notice

TCP is a byte stream. You send three times:

A
B
C

The receiver may read at once:

ABC

It may also be read in two parts:

A
BC

Even a single message gets split apart:

AB
C

Therefore, real projects must design protocols, for example:

  1. Newline-separated:cmd_vel 0.1 0.0\n;
  2. Fixed-length packet;
  3. Header + Length + Payload + CRC;
  4. Serialization protocols such as protobuf / flatbuffers.

6.Summary of this section

  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 async_accept() + Session classes.
  4. Each TCP connection corresponds to one socket.
  5. shared_from_this() is commonly used to guarantee the session lifecycle.
  6. TCP has no message boundaries, so real projects must design a framing protocol.