我有服务器和客户端。我正在使用 winsock2。客户端发送 4 个字节:
char *ack = new char[4];
sprintf( ack, "%d", counter );
sendto( clientSocket, ack, 4, 0, ( struct sockaddr* )&remote, sizeof( remote ) );
服务器接收这 4 个字节:
char* acks = new char[4];
if( ( bytes = recvfrom( serverSocket, acks, 4, 0, ( struct sockaddr* )&remote, &remote_size ) ) == SOCKET_ERROR ) {
cout << "socket error = " << WSAGetLastError() << endl;
break;
}
if( bytes > 0 ) {
sscanf( acks, "%d", &i );
}
我收到此错误,但我不知道如何解决它:
>Critical error detected c0000374
>
>server.exe has triggered a breakpoint.
我知道指针和内存分配有问题。但我的 c++ 技能是基本的。
字符串格式溢出
最紧迫的问题是你正在使用sprintf和sscanf。避免使用 sprintf 和 sscanf - 它们很容易意外创建您在此处看到的错误类型,即缓冲区溢出(在您的客户端和服务器上)。
考虑一下当你的"计数器"值为 1729 时,你的客户端会发生什么。您的代码将运行
sprintf(ack, "%d", 1729);
1729 的 C 样式字符串表示形式是五个字节长 - 字符值 '1'
、'7'
、'2'
、'9'
和 ' '
各一个字节。但是您的 ack 缓冲区只有 4 个字节长!现在,您已将最后一个零字节写入您从未分配的某个内存块中。在 C/C++ 中,这是未定义的行为,这意味着您的程序可能会崩溃,或者可能不会崩溃,如果它没有崩溃,它可能会在以后出现微妙的错误,或者它可能运行良好,或者它可能大部分时间都工作,除了它在星期二中断。
这不是一个好地方。
您可能想知道,"如果这如此糟糕,为什么sprintf
不返回错误或我用太小的缓冲区调用它的东西? 答案1是sprintf
无法进行检查,因为它没有给你任何方法来告诉它ack
实际上有多大。当你这里的代码调用sprintf
时,你知道 ack 有 4 个字节长(因为你刚刚创建了它),但 sprintf 看到的只是一个指向某个内存的指针,在某个地方 - 你没有告诉它长度,所以它只需要盲目地希望你给它的内存块足够大。
盲目地希望是编写软件的一种非常糟糕的方式。
您可以在这里考虑一些替代方案。
- 如果您实际上只是尝试通过网络发送 int,则根本不需要字符串化 int - 只需将
reinterpret_cast<char*>(&counter)
作为缓冲区传递给sendto 2,并以 sizeof(counter) 作为相应的缓冲区长度,以本机格式发送它。在另一端的 recvfrom 中使用类似的结构。请注意,如果您的发送方和接收方具有不同的整数基础表示形式(例如,如果他们使用不同的字节序),这将中断,但由于您在这里谈论的是 Winsock,我假设您假设两端都是合理的最新版本的 Windows,这不会成为问题。 - 如果您确实需要先字符串化内容,请使用大小识别字符串转换函数,例如 boost::format(它是隐式的大小识别,因为它处理 std::string 而不是原始 char* 缓冲区)或 _snprintf_s/_snscanf_s(显式接受缓冲区长度参数,但特定于Microsoft)。
从访问冲突接收
但是,sscanf/sprintf 中的溢出并不一定能解释这一点:
我只想补充一点,错误发生在 sscanf 行中。如果我在该行发表评论,则错误发生在 recvfrom 行中。
对此的一种可能的解释可能是没有为远程地址提供足够的空间,尽管只要您的remote_size
正确反映了您的remote
,我希望这会导致recvfrom
返回错误3,而不是崩溃。另一种可能性是传递错误的内存/句柄(例如,如果您已将 new
运算符设置为不会在失败时抛出,或者如果您的套接字初始化失败并且您没有救助)。如果不看到初始化所有变量的代码,就不可能准确说出,理想情况下,您在这种情况下遇到的实际错误。
1 尽管 sprintf 无法捕获此错误,但静态分析工具(如 Visual Studio 2012/2013 中包含的工具)非常能够捕获此特定错误。如果您通过默认的 Visual Studio 2012 代码分析器运行发布的代码,它将抱怨:
错误 C4996:"sprintf":此函数或变量可能不安全
2 有些人更喜欢static_cast<char*>(static_cast<void*>(&counter))
而不是reinterpret_cast<char*>(&counter)
。两者都有效,本质上是一种编码约定选择。
3 例如,如果您将remote
初始化为SOCKADDR_IN
而不是SOCKADDR_STORAGE
,如果您碰巧从 IPv6 地址接收,您可能会遇到此类错误。这个答案通过了一些相关的血腥细节。