使用 Boost.Asio 向客户端发送 2 个文件时出现问题,错误:读取:文件结尾 [asio.misc:2]

问题描述 投票:0回答:1

我想发送两个文件给客户端,第一个文件是img.jpg,第二个文件是message.txt

第一个文件 img.jpg 被正确接收,但文件 message.txt 被接收,大小为零

客户端输出是: 图片.jpg: 49152 消息.txt:4096

read: End of file [asio.misc:2]

这是我的客户端和服务器代码:

服务器.cpp:

#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <boost/asio.hpp>

int main(int argc, char* argv[])
{
    try
    {
        boost::asio::io_context io;
        std::cout << "Server Start\n";
        boost::asio::ip::tcp::acceptor acc(io,
            boost::asio::ip::tcp::endpoint(
                boost::asio::ip::tcp::v4(), 6666));

        for (;;) {
            boost::asio::ip::tcp::socket sock(io);
            acc.accept(sock);

            std::vector<std::string> names{ "img.jpg" , "message.txt"};
            std::vector<int> sizes{ 49152 , 4096 };

            for (int i = 0; i < 2; ++i) {
            
                //Send Header
                boost::asio::streambuf reply;
                std::ostream header(&reply);
                header << names[i] << " ";
                header << std::to_string(sizes[i]) << " ";
                header << "\r\n";
                boost::asio::write(sock, reply);

                //Send Bytes
                std::ifstream input(names[i], std::ifstream::binary);
                std::vector<char> vec(sizes[i]);
                input.read(&vec[i], sizes[i]);
                boost::asio::write(sock, boost::asio::buffer(vec, sizes[i]));
            }

            sock.close();
        }
        acc.close();
    }
    catch (std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

客户端.cpp:

#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <boost/asio.hpp>

int main(int argc, char* argv[])
{
    try
    {

        boost::asio::io_context io;
        boost::asio::ip::tcp::resolver resolv(io);
        boost::asio::ip::tcp::resolver::query q("127.0.0.1", "6666");
        boost::asio::ip::tcp::resolver::iterator ep = resolv.resolve(q);
        boost::asio::ip::tcp::socket sock(io);
        boost::asio::connect(sock, ep);

        //Get Files
        for (int i = 0; i < 2; ++i) {

            //Read Header
            boost::asio::streambuf reply;
            boost::asio::read_until(sock, reply, "\r\n");
            std::istream header(&reply);
        
            std::string fileName;
            int fileSize;
            header >> fileName;
            header >> fileSize;
            std::cout << fileName << ": " << fileSize << '\n';
            //Read File Data
            std::ofstream output(fileName, std::ofstream::binary | std::ofstream::app);

            std::vector<char> vec(fileSize);
            boost::asio::read(sock, boost::asio::buffer(vec, vec.size()));

            output.write(&vec[0], vec.size());

            output.close();
        }

        sock.close();

    }
    catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}
boost boost-asio tcpclient tcpserver asio
1个回答
0
投票

有很多问题。

  • &vec[i]
    是服务器端的一个错误:

     std::vector<char> vec(sizes[i]);
     input.read(&vec[i], sizes[i]);
    

    如果

    i>0
    (除了第一次运行之外总是如此),您将解决
    vec
    出界,因为尺寸不是
    sizes[i]+i

  • 你的标题格式很草率:带有空格的文件名会导致UB

  • os << to_string(n)
    应该只是
    os << n

  • 服务器使用客户端...完全忽略的分隔符

    "\r\n"
    。所有文件至少以未被消耗的
    \r\n
    开头,更糟糕的是,文件的最后两个字符将被读取为下一个文件头的一部分。这充其量会失败,但可能会导致 UB

  • 事实上,它总是会导致UB,因为客户端完全缺乏错误处理

  • 我现在注意到,您几乎通过对标题(

    streambuf header;
    )和内容(直接进入
    vec
    )使用单独的缓冲区来解决这个问题。然而,
    read_until
    记录了它可能会读到超过分隔符的情况。因此,您应该写入
    streambuf
    中的所有剩余数据,并从仍要读取的数量中减去长度。

    简而言之,建议每个流使用单独的、精确大小的缓冲区 OR 一个

    DynamicBuffer
    (如
    streambuf
    )。

  • 同样的问题是客户端在循环的每次迭代中使用新的缓冲区:

     for (int i = 0; i < 2; ++i) {
         // Read Header
         asio::streambuf reply;
    

    它至少应该在循环之外,以便接收到的任何多余数据都将在下一次迭代中正确使用

  • 您通常希望处理读取的部分成功(即接受与 EOF 条件一起接收的数据)。这里它不应该影响正确性,因为你精确地限制了正文读取到预期的大小,但这仍然是一个好习惯

  • 您在

    asio::buffer(vec, vec.size())
    中指定了冗余缓冲区大小,这只会引发错误。让它们远离以获得相同的行为,而不会有获得错误尺寸的风险:
    asio::buffer(vec)
    (例如,它将避免前面提到的 UB)

演示缓冲区问题

具有中途修复的组合服务器/客户端:https://coliru.stacked-crooked.com/a/03bee101ff6e8a7a

客户端增加了很多错误处理

asio::streambuf buf;
for (;;) {
    asio::read_until(sock, buf, "\r\n");

    std::string name;
    int         size;
    if (std::istream header(&buf); (header >> std::quoted(name) >> size).ignore(1024, '\n')) {
        std::cout << name << ": " << size << '\n';

        std::vector<char> vec(size);
        boost::system::error_code ec;
        auto n = read(sock, asio::buffer(vec), ec);

        if (n != vec.size()) {
            std::cerr << "Read completed: " << ec.message() << std::endl;
            std::cerr << "Incomplete data (" << n << " of " << vec.size() << ")" << std::endl;
            std::cerr << "Streambuf still had " << buf.size() << " bytes (total: " << (n + buf.size()) << ")" << std::endl;
            break;
        }
        std::ofstream(name, std::ios::binary /*| std::ios::app*/).write(&vec[0], n);
    } else {
        std::cerr << "Error receiving header, header invalid?" << std::endl;
        break;
    }
}

这使我们能够通过

streambuf
读数超出分隔符来演示问题:

main.cpp: 2712
main.cpp: 2712
Read completed: End of file
Incomplete data (2217 of 2712)
Streambuf still had 495 bytes (total: 2712)

或者我本地的测试:)

test.gif: 400557
message.txt: 4096
Incomplete data (3604 of 4096)
Streambuf still had 492 (total: 4096)

笨拙/幼稚的修复方法可能看起来像这样:

if (std::istream header(&buf); (header >> std::quoted(name) >> size).ignore(1024, '\n')) {
    std::cout << name << ": " << size << '\n';

    std::cerr << "Streambuf still had " << buf.size() << " bytes" << std::endl;
    size -= buf.size();
    std::ofstream ofs(name, std::ios::binary /*| std::ios::app*/);
    ofs << &buf;

    std::cerr << "Adjusted size to read: " << size << std::endl;

    std::vector<char> vec(size);
    boost::system::error_code ec;
    auto n = read(sock, asio::buffer(vec), ec);

    if (n != vec.size()) {
        std::cerr << "Read completed: " << ec.message() << std::endl;
        std::cerr << "Incomplete data (" << n << " of " << vec.size() << ")" << std::endl;
        break;
    }
    ofs.write(&vec[0], n);
} else {
    std::cerr << "Error receiving header, header invalid?" << std::endl;
    break;
}

虽然它可能看起来工作正常:

它只会引发小文件的新问题,其中整个后续文件被“意外”用作当前文件的内容。相反,只需 SayWhatYouMean(TM):

if ((std::istream(&buf) >> name >> size).ignore(1024, '\n')) {
    std::cout << name << ": " << size << '\n';

    read(sock, buf, asio::transfer_exactly(size), ec);

    if (buf.size() < size) {
        std::cerr << "Incomplete data" << std::endl;
        break;
    }
    std::ofstream(output_dir / name, std::ios::binary /*| std::ios::app*/)
        .write(buffer_cast<char const*>(buf.data()), size);

    buf.consume(size);
} else {

完整修复

还从命令行获取文件列表,而不是硬编码文件/大小,并写入输出目录以确保安全:

住在Coliru

#include <boost/asio.hpp>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <ranges>
namespace asio = boost::asio;
using asio::ip::tcp;
using std::ranges::contains;
using std::filesystem::path;
constexpr uint16_t PORT = 6666;

int main(int argc, char* argv[]) try {
    asio::io_context io;

    std::vector<std::string_view> const
        args(argv + 1, argv + argc), 
        opts{"--client", "-c", "--server", "-s"};

    bool const server = contains(args, "--server") || contains(args, "-s");
    bool const client = contains(args, "--client") || contains(args, "-c");

    if (server) {
        std::cout << "Server Start" << std::endl;

        for (tcp::acceptor acc(io, {{}, PORT});;) {
            tcp::socket sock = acc.accept();

            for (path name : args) {
                if (contains(opts, name))
                    continue;
                auto size = file_size(name);

                // Send Header
                asio::streambuf buf;
                std::ostream(&buf) << name << " " << size << "\r\n";
                write(sock, buf);

                // Send bytes
                std::vector<char> vec(size);
                std::ifstream(name, std::ios::binary).read(vec.data(), size);
                write(sock, asio::buffer(vec));
            }
        }
    }

    if (client) {
        path output_dir = "./output/";
        create_directories(output_dir);

        tcp::socket sock(io);
        // connect(sock, tcp::resolver(io).resolve("127.0.0.1", std::to_string(PORT)));
        sock.connect({{}, PORT});

        asio::streambuf buf;

        for (boost::system::error_code ec;;) {
            read_until(sock, buf, "\r\n", ec);

            path   name;
            size_t size;
            if ((std::istream(&buf) >> name >> size).ignore(1024, '\n')) {
                std::cout << name << ": " << size << '\n';

                read(sock, buf, asio::transfer_exactly(size), ec);

                if (buf.size() < size) {
                    std::cerr << "Incomplete data" << std::endl;
                    break;
                }
                std::ofstream(output_dir / name, std::ios::binary /*| std::ios::app*/)
                    .write(buffer_cast<char const*>(buf.data()), size);

                buf.consume(size);
            } else {
                std::cerr << "Error receiving header, header invalid?" << std::endl;
                break;
            }
        }
    }
} catch (std::exception const& e) {
    std::cerr << e.what() << std::endl;
    return 1;
}

通过现场测试

for a in {1..10}; do dd if=/dev/urandom bs=1 count=10 of=small-$a.txt; done 2>/dev/null
g++ -std=c++2b -O2 -Wall -pedantic -pthread main.cpp
./a.out --server *.* &
sleep 1; ./a.out --client
kill %1
md5sum {.,output}/*.* | sort

印刷:

Server Start
"a.out": 187496
"main.cpp": 2546
"small-1.txt": 10
"small-10.txt": 10
"small-2.txt": 10
"small-3.txt": 10
"small-4.txt": 10
"small-5.txt": 10
"small-6.txt": 10
"small-7.txt": 10
"small-8.txt": 10
"small-9.txt": 10
Error receiving header, header invalid?
024c40ee2e93ee2e6338567336094ba2  ./small-8.txt
024c40ee2e93ee2e6338567336094ba2  output/small-8.txt
164f873a00178eca1354b1a4a398bf0f  ./small-10.txt
164f873a00178eca1354b1a4a398bf0f  output/small-10.txt
2ff416d02ca7ea8db2b5cb489a63852d  ./small-6.txt
2ff416d02ca7ea8db2b5cb489a63852d  output/small-6.txt
4559b8844afe7d5090948e97a8cef8d8  ./small-7.txt
4559b8844afe7d5090948e97a8cef8d8  output/small-7.txt
6fd6eac47427bfda3fc5456afed99602  ./small-4.txt
6fd6eac47427bfda3fc5456afed99602  output/small-4.txt
76fa51d5d6f06b9c8483f5539cd5611b  ./a.out
76fa51d5d6f06b9c8483f5539cd5611b  output/a.out
8a114a62f0ad5e087d7b338eeebcadf1  ./small-1.txt
8a114a62f0ad5e087d7b338eeebcadf1  output/small-1.txt
b4f11b6ed8870d431c5ec579d12991c0  ./small-5.txt
b4f11b6ed8870d431c5ec579d12991c0  output/small-5.txt
e1f0f06f1226ff7c82f942684d22e100  ./small-3.txt
e1f0f06f1226ff7c82f942684d22e100  output/small-3.txt
ec3fabd7edd0870bcfa5bbcfc7f2c7ec  ./small-2.txt
ec3fabd7edd0870bcfa5bbcfc7f2c7ec  output/small-2.txt
f80c5bbe46af5f46e4d4bcb2b939bf38  ./main.cpp
f80c5bbe46af5f46e4d4bcb2b939bf38  output/main.cpp
ff225cbc0f536f8af6946261c4a6b3ec  ./small-9.txt
ff225cbc0f536f8af6946261c4a6b3ec  output/small-9.txt

奖金

不要进行基于文本的 IO,而是考虑发送二进制文件大小和名称信息。请参阅示例:https://stackoverflow.com/search?tab=newest&q=user%3a85371%20endian%20file&searchOn=3


¹ 因为 TCP 数据包传输的工作原理;这就是所有图书馆的行为方式

© www.soinside.com 2019 - 2024. All rights reserved.