这里写目录标题
- 1. 内存问题
- 1.1. 内存泄漏
- 1.1.1. 内存泄漏案例检查方法
- 1.1.2. 主线程提前退出导致【控】
- 1.1.3. PostThreadMessage失败导致的内存泄漏**【控】**
- 1.1.4. SendMessage 时关闭客户端【控】
- 1.1.5. 线程机制导致【**控】**
- 1.1.6. exit(0)导致【控】
- 1.2. 内存冲突
- 1.2.1. 内存非法【控】
- 1.2.2. 访问内存冲突2【控】
- 2. 编译阶段问题
- LINK2005、1169重定义
- link1120和LNK2019:**无法解析的外部符号**
- C4996警告
- C4005宏重定义
- C2664字符集
- LNK2001 LNK1120
- 3. 文件、库等
- 3.1. 程序的架构不一致【控】
- 4. 网络问题
- 4.1. **服务端关闭了 socket,但客户端未closesocket,导致新连接请求被系统拒绝**。 **10056**
- 4.2. 10054 远程主机强迫关闭了一个现有的连接。
- 4.3. 连接失败:10060 客户端主动断开连接,远程服务端出现bug
- 5. 调试技巧
1. 内存问题
1.1. 内存泄漏
1.1.1. 内存泄漏案例检查方法
一个很牛的方法:
-
搜索所有的new,然后在其new后打印一下new出来的内存大小,如果有delete,不确定有没有执行delete,就在delete后打印TRACE(“xxx has been deleteed”);所有都这样搞,再去执行一遍程序,就能发现哪个没delete;发现控制类的Instance没delete,就打断点看看cHelper的构造和析构有没有执行;
-
打断点发现都没执行;查找所有引用发现m_helper只声明根本就没实现;然后实现下,实现后就可以执行,内存泄漏的问题解决,
-
vId使用Visual Leak Detector:
- 可以得到内存泄漏点的调用堆栈,如果可以的话,还可以得到其所在文件及行号;
- 可以得到泄露内存的完整数据;
- 可以设置内存泄露报告的级别;
- 它是一个已经打包的lib,使用时无须编译它的源代码。而对于使用者自己的代码,也只需要做很小的改动;
- 他的源代码使用GNU许可发布,并有详尽的文档及注释。对于想深入了解堆内存管理的读者,是一个不错的选
1.1.2. 主线程提前退出导致【控】
atlTraceGeneral - m_hwnd = 00000000 —说明窗口句柄未初始化
**debug输出:**Detected memory leaks!Dumping objects ->{187} normal block at 0x0000025DAB749D20, 90 bytes long. Data:/ 61 00 74 00 6C 00 54 00 72 00 61 00 63 00 65 00
- 程序崩溃和内存转储(Core Dump) 程序运行时,Visual Studio 会在调试模式下自动检测内存泄漏并输出相关信息。输出的内存转储数据包括了被泄漏的内存块的地址、大小以及该内存区域的内容。
- Dumping objects -> 开始输出(dump)内存中被分配的对象的详细信息。它通常会显示当前所有未释放的内存块(即潜在的内存泄漏)以及它们的大小、地址等详细信息。
- {Object dump complete.} 一行`表示内存泄漏的对象信息已经完全输出完毕。
- 表示一个正常的内存块(不是数组或堆栈),它位于地址
0x0000025DAB749D20
,占用 90 字节 - 这些数据
61 00 74 00 6C 00 54 00 72 00 61 00 63 00 65 00
是 Unicode 编码的字节表示。每对字节代表一个字符,其中每个字符占 2 字节。 转换成可读的字符就是:a t l T r a c e
。
内存泄漏信息表示在程序运行中分配了内存,但未正确释放。常见原因:
- 对象未被销毁,例如对话框对象未调用
DestroyWindow
。 - 动态分配的资源未释放。
问题原因:
_beginthread
并不提供线程同步功能,它只是启动一个线程并管理资源 ,导致主线程开启这个线程后一会主线程执行完结束了,导致程序退出,导致线程强制终止,导致创建的内存还没来得及释放。
解决
- sleep主动阻塞主线程
- 用thread+join √
1.1.3. PostThreadMessage失败导致的内存泄漏**【控】**
虽然在SendPack那个线程收到这个new的包复制后就立马delete,但是万一发送失败,就会产生内存泄漏,所以在这地方也要作出处理,那失败的包怎么处理???
1.1.4. SendMessage 时关闭客户端【控】
服务端在远程启动,关闭客户端后发现很多鼠标和图像的内存泄漏,因为客户端SendPack收到数据后,SendMessage到dlg是new的数据发过去的,如果此时客户端关闭了,这些new的数据无法释放,本来处理逻辑是在dlg收到数据后复制一下(栈),然后吧堆区数据给释放;这种情况怎么办???
1.1.5. 线程机制导致【控】
线程函数,不管是主线程子线程,对cpu的使用权,记录一个使用权,main结束是回到系统函数,真正的启动函数点事mainCRTStartip,调用到main了;
当使用 endthread
来结束线程时,它会直接终止线程,并且不允许线程中的局部变量正常析构(坑的点)。这可能导致内存泄漏或无法释放的资源,因为局部变量的析构函数不会被调用。 就会导致内存泄漏,现在把这个线程函数的主题放入到另一个函数threadmain里,这样函数结束就会析构变量,所以一般线程函数里在搞一个函数;线程入口函数(threadQueueEntry)和线程功能函数(threadmain)分开;
解决思路:
- 先把所有的new搜一遍,对应的delete是否能对应上。
- 确保delete是否执行到?打印日志一般,一般到这问题就出现了
- 现在发现delete对没问题,唯一的可能是list, 当你往
std::list<T>
里添加元素时,每个节点通常都是 在堆区分配的,而std::list
本身的管理结构体(如begin
指针、end
指针等)可能位于栈上或堆上,取决于std::list
的 定义方式。 - 试着调用list.clear,仍然泄露
- 最后百度搜索_endthread的问题,解决方案在上面
1.1.6. exit(0)导致【控】
内存泄漏往往很难定位触发点,此次情况,new delete对应ok;加日志,所有可能内存泄漏地方加日志分析;
之前这里有个exit(0);函数直接终止,不会 调用当前作用域中的局部变量的析构函数(包括栈上的对象) ,图中创建的队列也只会调用构造,不对调用其析构,就导致内存泄漏;和那个endthread是一样道理;
去掉这个exit后,内存泄漏没了,但是出现了崩溃的bug;
1.2. 内存冲突
1.2.1. 内存非法【控】
在使用 memcpy
之前,需要确保以下几点:
- 确保
**strData**
的大小足够: 调用resize(nSize)
后,strData
的大小应该足够容纳nSize
字节。 - 确保
**pData**
是有效的指针: 在调用memcpy
前,检查pData
是否为空指针。 - 确保
**nSize**
合法: 确保nSize
在合理范围内,既不能为负值,也不能超过pData
实际拥有的内存大小。
发现:
pData
的值为 0x00000009
,这是一个非常不合理的指针地址(通常有效的指针应该是内存中有效的区域,如堆或栈上的地址),并且调试器提示 读取字符串时出错。这说明问题的根本原因是 pData
是一个非法指针。
通过调用堆栈,查看调用这个函数的地方:
发现data是9,传入的参数,把一个非指针( 整数值
9
)转为指针,(BYTE*)data
并不是指向有效内存区域的指针,而是一个直接指向地址 0x00000009
的指针,这显然是非法的。
经验: 所以以后看到0x00000009
这样的非法指针,很有可能是把一个数据强转为指针然后传了。
经验2:报错系统库函数里的变量内存访问冲突,发现其内存是0x000005b8,这显然是个不合法内存;
这种情况就看调用堆栈,观察自己的代码哪里开始出错;发现在控制类的InitController里的this指针是空指针;
合法内存地址通常看起来是较大的十六进制值
常见的非法地址包括:
0x00000000
:空指针。0x000005b8
:属于操作系统保护的低地址区域。0xdeadbeef
或0xcccccccc
:调试环境中常用的未初始化指针标记。- 非法越界地址,例如访问数组时超出范围。
1.2.2. 访问内存冲突2【控】
四个参数里,其他三个是上面定义的局部变量,肯定没问题,那问题只能处在成员变量m_hCompletionPort,调用堆栈向前分析:
发现thiz指针非法;那就是线程函数传递进来的参数有问题;
传错了,应该传的this指针,才能去调用其成员变量;问题根源是直接ctrl c过来的,现在把这些东西都封装在类里面,线程函数传的参数也应该改变!
2. 编译阶段问题
-
遇到编译问题不要慌,一个一个解决
-
解决的方法:
-
调整头文件的顺序(宏冲突)(
windows.h
的宏污染(如min
/max
)或 C++ 标准库冲突)22222222222222 -
对应的库文件的引用(
LNK2019
无法解析的外部符号错误) -
#pragma comment(lib, “rpcrt4.lib”) // 显式链接库
-
4996 的警告一般可以通过警告本身的提示,添加宏来解决
-
#define _CRT_SECURE_NO_WARNINGS // 禁用安全警告
-
头文件引用错误(若无法跳转到定义,说明路径错误或文件缺失)
调试问题总结:
1 socket 返回-1 而参数没有问题,那么有可能是 WSAStartup 未调用
2 很多时候,问题是叠加在一起的,只要不断的解决,总能搞定
3 没有测试的程序,是不靠谱的,无论谁写的
4 大胆假设,小心求证
LINK2005、1169重定义
其实就是在头文件声明和定义了函数,在多个.cpp文件#include这个头文件就会报错,出个定义,违反了ODR原则。
解决办法
- 函数声明与定义分离,声明在头文件,定义在 .cpp 文件
- 将函数声明为
**inline**
, 如果你需要在头文件中定义函数(例如,函数实现非常简单),可以将其声明为inline
,让编译器将其视为内联函数,不会重复定义: 注意:inline
适用于小型、简单的函数。如果函数较大或复杂,建议不要内联。 - 将函数声明为 全局static,使其作用域限制在当前编译单元(每个 .cpp 文件生成独立副本)
- 定义为一个类的静态函数
link1120和LNK2019:无法解析的外部符号
1、无法解析的外部符号,基本上都是声明了函数,但是没有定义这个函数
LNK2019
无法解析的外部符号 _main,函数 “int __cdecl invoke_main(void)” (invoke_main@@YAHXZ) 中引用了该符号
-
这通常意味着链接器找不到程序的入口点
main
函数 -
未定义main函数
-
展开 链接器 > 系统,查看 子系统 选项:
-
如果您正在写控制台程序,确保子系统设置为 Console (/SUBSYSTEM:CONSOLE)。
-
如果是 Windows 应用程序,确保子系统设置为 Windows (/SUBSYSTEM:WINDOWS)。
-
根据项目类型,实现相应的入口函数:
-
对于控制台程序,定义
main
函数。 -
对于 Windows 程序,定义
WinMain
函数
发现是入口点函数写成了main,但是客户端mfc项目是以winmain为起点的
2、 RTSPServer.obj : error LNK2019: 无法解析的外部符号 __imp**__UuidCreate@4,函数 “public: __thiscall RTSPSession::RTSPSession(class ESocket const &)**” (??0RTSPSession@@QAE@ABVESocket@@@Z) 中引用了该符号 :
- 通常是由于 未正确链接 RPC 运行时库 或 函数声明与实现不匹配 导致的
UuidCreate
是 Windows RPC 运行时库中的函数,其声明位于rpcdce.h
或rpc.h
,实现位于rpcrt4.lib
。若未正确链接该库,或未包含头文件,会引发此错误- 解决:
#pragma comment(lib, "rpcrt4.lib") // 添加此行到引用 UuidCreate 的源文件或头文件中
或在项目属性中配置链接库:
- 右键项目 → 属性 → 链接器 → 输入 → 附加依赖项 → 添加
rpcrt4.lib
2 6。 - 确保
rpcrt4.lib
的路径正确(如C:\Program Files (x86)\Windows Kits\10\Lib\...
)
缺网络库:#pragma comment(lib, “ws2_32.lib”)
3、
- 报错:
- 此时头文件目录,静态库都配到 vs 项目里,dll 也配到环境变量
- 发现这些库是 release 版本,vs 使用 debug 模式,改为 release 后,报错变化为下:
C4996警告
C4996 是 Visual Studio 编译器的一个警告代码,用于指示一些被 Microsoft 标记为不安全或已过时的函数。例如scanf等sprintf
,inet_addr
函数已被视为过时。推荐使用 inet_pton()
项目里面禁用4996警告三种方法:
禁用SDL检查Security Development Lifecycle Checks)可以解决;不推荐长期禁用 SDL 检查:SDL 检查的目的是帮助开发者编写更安全的代码。
- 项目属性里编译器预处理那定义,在这里加上去个 _WINSOCK_DEPRECATED_NO_WARNINGS; 来禁用与过时 API 相关的警告
- 文件内部定义#pragma warning(disable:4996)
C4005宏重定义
- 头文件顺序问题:
windows.h
文件在 Windows 平台中定义了大量的宏和类型,这些宏可能与套接字头文件中的定义冲突。- 例如,
windows.h
可能会定义min
和max
宏,与其他头文件中的定义冲突。 - 解决:将套接字的头文件(如
winsock2.h
或sys/socket.h
)放在windows.h
前面
C2664字符集
解决方法:vs字符集改为多字符集即可
LNK2001 LNK1120
- LNK1120:共9个未解析的外部符号(函数/变量)
- LNK2001:具体列出缺失的FFmpeg API:
问题类型 | 具体表现 | 典型场景 |
---|---|---|
库文件未链接 | 缺少avformat.lib 等静态库 | 项目配置遗漏 |
函数声明不匹配 | 头文件与库版本不一致 | 升级FFmpeg后未更新头文件 |
编译架构冲突 | x86库用于x64项目 | 平台配置错误 |
当C++代码调用C语言库(如FFmpeg)时,必须使用此语法包裹C语言的头文件。
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
}
- C++ 语言支持函数重载等特性,编译器在编译时会对函数名进行 “名字改编(Name Mangling)”,将函数名和参数类型等信息混合生成内部使用的符号名,以此区分同名但参数不同的函数。而 C 语言不支持函数重载,没有名字改编机制 ,函数名就是实际链接时使用的符号名。
- FFmpeg 库(
libavcodec
和libavformat
是其相关库)是用 C 语言编写的,为了能在 C++ 代码中(.cpp文件里)正确调用 FFmpeg 库中的 C 函数,需要告诉 C++ 编译器,这部分代码要按照 C 语言的规则进行编译,避免名字改编带来的链接错误,extern "C"
就是起这个作用。
3. 文件、库等
3.1. 程序的架构不一致【控】
一个典型的 Windows 错误,错误代码为 0xc000007b
,提示程序无法正常启动 :
- 确保程序的架构(32 位或 64 位)与所有依赖的 DLL 的架构一致。
发现正是这个问题导致!
4. 网络问题
recv
返回 -1 时的错误码
-
**WSAECONNRESET**
(10054): -
服务端主动关闭了连接,导致客户端读取数据失败。
-
或者,服务端异常退出,连接被 TCP 重置。
-
**WSAETIMEDOUT**
(10060): -
接收超时,客户端等待服务端的数据时超时。
-
**WSAENOTCONN**
(10057): -
套接字未连接,可能是客户端在连接断开后尝试接收数据。
-
**WSAEINTR**
: -
接收操作被中断。
4.1. 服务端关闭了 socket,但客户端未closesocket,导致新连接请求被系统拒绝。 10056
10056 在一个已经连接的套接字上做了一个连接请求
1、发现客户端也closesocket之后再connect就能连上了,这是为啥????
-
**closesocket**
释放了客户端的本地端口 -
当客户端调用
closesocket()
时,本地的套接字资源被立即释放,包括绑定的本地端口。 -
此时,客户端可以立即创建一个新的套接字,并发起
connect
操作。 -
因为新的套接字使用了新的资源,与之前的旧套接字无关,因此不会有冲突。
-
服务端的端口可以接受新的连接
-
服务端虽然进入了
TIME_WAIT
状态,但TIME_WAIT
并不影响服务端套接字接受新的连接请求。 -
TIME_WAIT
只会限制服务端重用之前的连接,而不会阻止服务端监听套接字接受新的连接。 -
新的套接字使用了新的连接三元组
-
每个 TCP 连接由三元组
(客户端 IP, 客户端端口, 服务端 IP:服务端端口)
唯一标识。 -
当客户端创建新的套接字时,系统会为其分配一个新的客户端端口(如果没有指定),使新的连接三元组和旧的连接三元组完全独立,从而不会冲突。
-
网络资源释放及时
-
客户端调用
closesocket()
时,系统通常会立即释放该套接字的本地端口资源,而无需等待TIME_WAIT
。
2、如果客户端等待 2 分钟(或者操作系统配置的 TIME_WAIT
时间)让服务端的 TIME_WAIT
状态过去后, 客户端在 不调用 **closesocket()**
的情况下,直接使用同一个套接字句柄 (socket
) 再次调用 connect
,这种行为是 非法的,并且会导致 **connect**
调用失败,报错 **WSAEISCONN**
或 **WSAENOTSOCK**
,具体原因如下:
-
**socket**
的状态已经是已连接 -
在 TCP 协议中,当客户端完成一次
connect
后,该套接字已经绑定到特定的连接三元组(本地IP, 本地端口, 服务端IP:服务端端口)
。 -
套接字资源仍然有效,但它已经与之前的连接绑定。
-
如果此时服务端已经关闭连接,而客户端没有关闭套接字,却尝试再次调用
connect
: -
操作系统会检查套接字的状态,发现它仍然处于连接绑定状态,而不是一个空闲的套接字。
-
此时调用
connect
会报错**WSAEISCONN**
(表示该套接字已连接)。 -
服务端的
**TIME_WAIT**
不影响这个行为 -
服务端的
TIME_WAIT
状态与客户端无关。客户端的行为取决于其套接字资源的状态。 -
如果客户端的套接字仍然有效,但已经绑定到旧的三元组,则不能直接使用该套接字重新连接。
-
客户端的操作系统规则
-
操作系统对 TCP 套接字的状态有严格的定义,一个已连接的套接字不能被用于发起新的连接。
-
TCP 套接字的生命周期如下:
**socket()**
:创建套接字,分配资源。**connect()**
:与服务端建立连接,绑定三元组。**closesocket()**
:关闭套接字,释放绑定的三元组。
- 如果没有调用
closesocket()
,操作系统仍然认为该套接字与之前的连接绑定,即使服务端关闭连接,客户端也无法重新使用该套接字。
4.2. 10054 远程主机强迫关闭了一个现有的连接。
本机联调进行testConnection通信没问题,但是服务端放在虚拟机上,就报这个报错!
已知错误码10054
-
**WSAECONNRESET**
(10054): -
服务端主动关闭了连接,导致客户端读取数据失败。
-
或者,服务端异常退出,连接被 TCP 重置。
难绷,客户端第一次connect服务端返回成功,send也没报错,recv报错了,但是发现ping不通虚拟机,那为啥客户端第一次connect会返回0呢;ping不同是因为主机换wifi了,网段变了,但之前虚拟机设置了静态ip,桥接模式下虚拟机和宿主机网段一样才能ping通,
改了ip后,CS网络通信正常了!
可能回答:
虚拟机的 IP 静态配置没有影响初始连接
- 虽然虚拟机设置了静态 IP,但当宿主机切换 Wi-Fi 导致网段改变时,虚拟机和宿主机可能仍然在某种程度上保持通信能力。
- 原因:在某些桥接模式下,虚拟机和宿主机可能会尝试使用旧的 ARP 缓存或其他机制进行通信,允许一部分数据包通过,尤其是初始的 TCP 握手。
- 但后续的数据包可能因网段不一致而丢失。
为什么 **send**
没报错?
- TCP 的发送缓冲区行为 当客户端调用
send
时,数据会先被放入操作系统的发送缓冲区,立即返回成功。 - 如果 TCP 的状态仍然是
ESTABLISHED
,操作系统不会立即检查数据是否成功到达服务端。 - 即使
send
成功,数据可能无法到达服务端,因为网段不一致或路由配置错误导致数据包被丢弃。 - 当客户端调用
recv
时,操作系统检测到连接被服务端关闭,返回错误码10054
。
为什么 recv
报错?
- 由于虚拟机的 IP 地址错误,服务端可能接收不到客户端发送的数据,误认为客户端超时,主动关闭了连接。
- 服务端关闭连接后,客户端的
recv
会报错10054
,表示连接被重置。
4.3. 连接失败:10060 客户端主动断开连接,远程服务端出现bug
1 由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败
为
LISTENING
状态仅表示服务端已经调用了 listen
并正在监听指定端口。
如果套接字是默认的阻塞模式,调用 recv
时:
- 如果接收缓冲区中有数据,
recv
会立即返回已接收的数据字节数。 - 如果缓冲区中没有数据,
recv
会阻塞当前线程,直到:
- 数据可用。
- 连接被对端关闭(返回
**0**
)。或者设置接受0长度那也返回0; 当服务端的recv
函数返回0
时,表示 对端(客户端)已经优雅地关闭了连接,即发送了一个 FIN 包。对于这种情况:recv
返回0
是一次性的。 如果继续调用recv
,通常会返回**-1**
,并设置错误码(如EINVAL
或类似错误),提示该套接字已失效。 - 发生错误(返回
-1
)。
问题:本机联调客户端和服务器没问题,服务端放远程后一直启动,客户端第一次启动也没问题,但第二次启动发现服务端在dealcommand里死循环,无法再次进入accept函数里导致客户端connect时候报错10060;
**那为啥死循环?
**:发现多次客户端测试网络通信,服务端accept返回的m_client都是552???总不能每次都复用552吧;
客户端这边每次发送新的命令,使用的socket会变,只是偶尔复用原来的socket;
死循环是因为客户端关闭后,应该要return后重新accept新的客户端,但代码逻辑还一直在recv recv循环,逻辑问题;
为什么 **recv**
会一直频繁返回 **0**
?
当 recv
返回 0
时,说明对端(客户端)已经优雅地关闭了连接(发送了 FIN 包)。但在 **recv**
返回 **0**
之后,套接字仍然是合法的(并没有直接closesocket,才会发fin)还可以收数据,因此你可以继续对这个套接字调用 recv
,而它仍然可能返回 0
。
- 阻塞行为在这种情况下不适用,因为套接字已经进入了一种特殊状态(“对端关闭但本端未关闭”),所以
recv
不会再阻塞。 - 如果服务端继续调用
recv
,TCP 协议层会立即返回0
,表示对端已经关闭发送方向。
为什么没有返回 **-1**
?
**-1**
是错误的标志,而不是连接关闭的标志:
- 当发生网络错误或套接字非法时 ,
recv
返回-1
。 - 对于对端优雅关闭的情况(发送了 FIN 包),
recv
不会返回-1
,因为这是正常的 TCP 关闭行为,不是错误。 - 阻塞行为在这种情况下不适用,因为套接字已经进入了一种特殊状态(“对端关闭但本端未关闭”),所以
recv
不会再阻塞。
解决:
直接return,客户端都断开了还while个屁,需要return然后服务端再次进入accept阻塞等待新的客户端连接;
但是为啥本机调试时候没出现这个问题呢:
本机调试,客户端关闭时候,服务端肯定也是一起关闭的,死循环虽有但是关闭后看不到了罢了;
5. 调试技巧
- send()之后,如果想知道send多少数据,直接在寄存器eax里查看就行/
InitSocket执行时候出问题了,可以右键这个函数:设置下一条语句,直接进去走一遍检查
- 断点中加条件,例如在循环里执行到某次时候断点,或者遍历数据集时候的某个数据打断点
或者这个循环命中次数
- 调用堆栈窗口的重要功能是:可以找到当前函数的调用函数,以及依次往前的每一级调用函数。