***********************前提:大通废话可以跳过*************************
Dump文件是C++程序发生异常时,保存程序运行状态的信息的二进制文件。在大型项目开发过程中,尤其是游戏项目,dump文件是调试异常程序的重要资源,其中包含的堆栈调用信息数据,是会极大地方便开发人员来排查错误并进行修正。
公司项目一开始在C++工程中添加了dump生成代码,以及开发了一个内部使用的dump解析网址。日常使用场景是:玩游戏,程序崩溃产生dump,打开解析网址,选择对应的游戏版本号、分支以及dump文件,提交(!nice!)然后将解析出的网址发给开发人员,这样开发人员就可以来排错改bug了。
BUT!!!实际上很多策划美术同学进入游戏产生了崩溃,只要不影响他们干自己的活,这个dump就静静地待在他们的文件夹里,不为人所知,那么其包含的珍贵堆栈信息也就随时间消散了。。。
针对这个问题,行政上的督促事半功倍,自动化的工具和流程才值得依赖。sososo博主针对公司项目情况,将该流程由手动提交优化成自动提交,致力于拯救那些消散在时间长河里的dump文件😎
***********************进入正题*************************
1、客户端采用socket方式连接服务器 c#实现
public ClientConnect()
{
}
// 初始化
public bool Init_Socket()
{
_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
return Connect();
}
// 连接服务器
private bool Connect()
{
if (_socket != null)
{
try
{
IPAddress remoteHostIP = IPAddress.Parse(_url);
_socket.Connect(new IPEndPoint(remoteHostIP, _port));
Console.WriteLine($"_socket.Connected:{_socket.Connected}");
_ready = true;
}
catch (Exception ex)
{
Console.WriteLine("服务器连接失败:" + ex.ToString());
Console.ReadKey();
}
}
return _ready;
}
// 关闭连接
public void Disconnect()
{
Console.WriteLine("准备关闭_socket套接字...");
if (_socket.Connected)
_socket.Shutdown(SocketShutdown.Both);
_socket?.Close();//_socket不为空则Close()=_socket.Close()
}
2、传输多个二进制及字符串类型的数据
注意:数据传输流的结束以及数据之间的分割
// 传输数据
public void Trans(string file, string rev, int vers, string auther, string txt)
{
byte[] versBytes = BitConverter.GetBytes(vers);
byte[] revBytes = BitConverter.GetBytes(int.Parse(rev));
byte[] autBytes = Encoding.UTF8.GetBytes(auther);
byte[] txtBytes = Encoding.UTF8.GetBytes(txt);
byte[] fileData = File.ReadAllBytes(file);
// 数据分割标志
byte[] _1rdMaker = Encoding.UTF8.GetBytes("DOB");
byte[] _2rdMaker = Encoding.UTF8.GetBytes("EOF");
byte[] _3rdMaker = Encoding.UTF8.GetBytes("END");
int totalLength = revBytes.Length + versBytes.Length + autBytes.Length +
txtBytes.Length + _1rdMaker.Length + _2rdMaker.Length + fileData.Length;
// 创建合并后的字节数组
byte[] combinedBytes = new byte[totalLength];
// 使用BlockCopy合并所有的字节数组
int offset = 0;
Buffer.BlockCopy(versBytes, 0, combinedBytes, offset, versBytes.Length == 1 ? 1 : 1);
offset += versBytes.Length == 1 ? 1 : 1;
Buffer.BlockCopy(revBytes, 0, combinedBytes, versBytes.Length == 1 ? 1 : 1, revBytes.Length == 4 ? 4 : 4);
offset += revBytes.Length == 4 ? 4 : 4;
Buffer.BlockCopy(fileData, 0, combinedBytes, offset, fileData.Length);
offset += fileData.Length;
Buffer.BlockCopy(_1rdMaker, 0, combinedBytes, offset, _1rdMaker.Length);
offset += _1rdMaker.Length;
Buffer.BlockCopy(autBytes, 0, combinedBytes, offset, autBytes.Length);
offset += autBytes.Length;
Buffer.BlockCopy(_2rdMaker, 0, combinedBytes, offset, _2rdMaker.Length);
offset += _2rdMaker.Length;
Buffer.BlockCopy(txtBytes, 0, combinedBytes, offset, txtBytes.Length);
if (_socket.Connected)
{
try
{
_socket.Send(combinedBytes, combinedBytes.Length, SocketFlags.None);
// 告知服务器文件流已传输完毕
_socket.Send(_3rdMaker);
Console.WriteLine("文件上传成功");
_finish = true;
}
catch (SocketException ex)
{
Console.WriteLine("文件传输失败:" + ex.Message);
_finish = true;
}
}
else
{
Console.WriteLine("服务器未连接...请尝试先开启服务器连接");
_finish = true;
return;
}
}
3、服务器采用socket方式接收响应 nodejs实现
const csharpServer = net.createServer((socket) => {
socket.name = socket.remoteAddress + ":" + socket.remotePort;
console.log(new Date().toLocaleString(),`客户端已连接:${socket.name}`);
// 连接后的业务逻辑代码
socket.on('data', (data) => {
console.log(new Date().toLocaleString(),'接收到数据');
// 接收数据后的业务逻辑代码
});
socket.on('close', () => {
console.log(new Date().toLocaleString(),`客户端已断开:${socket.name}`);
// 关闭连接后的业务逻辑代码
});
socket.on('error', (err) => {
console.error(new Date().toLocaleString(),'Socket error:', err.message);
// 连接出错后的业务逻辑代码
});
});
const clientPort = /*端口号*/;
csharpServer.listen(clientPort, () => {
console.log(`CSharp服务器已启动,正在监听端口 ${clientPort}`);
});
4、数据接收后的处理
该部分分为两步:1、数据分割 2、数据解析
1、数据分割
这个比较简单,按照之前设置的分隔符分割即可
let receivedData = Buffer.from([]);
const _1rdMarker = Buffer.from('DOB');
const _2rdMarker = Buffer.from('EOF');
const _3rdMarker = Buffer.from('END');
socket.on('data', (data) => {
console.log(new Date().toLocaleString(),'接收到数据');
receivedData = Buffer.concat([receivedData, data]);
if (isCompleteFile(receivedData)) {
// 接收到结束标志后 清空数据缓存区
processReceivedData(socket, receivedData);
receivedData = Buffer.alloc(0);
}
});
function isCompleteFile(data) {
return data.includes('END');
}
function processReceivedData(socket, receivedData) {
const _1rdIndex = receivedData.lastIndexOf(_1rdMarker);
const _2rdIndex = receivedData.lastIndexOf(_2rdMarker);
const _3rdIndex = receivedData.lastIndexOf(_3rdMarker);
let vers = receivedData[0];//类型
let rev = receivedData.readInt32LE(1);//版本
let fileData = receivedData.slice(5, _1rdIndex);//dump文件
let user = receivedData.slice(_1rdIndex + 3, _2rdIndex);
let txt = receivedData.slice(_2rdIndex + 3, _3rdIndex);
// 存储dump
const filePath = "你的路径";
fs.writeFileSync(filePath, fileData, { flag: 'a+' }, (err) => {
if (err) {
console.error('写文件出错:', err);
} else {
console.log('文件已保存为', filePath);
}
});
// 处理dump文件
let result = RunWinDbg(filePath, vers, rev);
2、数据解析
这个比较麻烦。建议去了解一下windbg。一般都是使用windbg工具解析dump,这个需要对应的工程符号表。所以在写业务逻辑代码前,要和开发人员确定好工程符号表的存放路径。主要是流程上的确认。
const command = `${windbgPath}`; // 你的windbg.exe路径
const args = [
'-z',
dumpPath, // dump文件存放路径
'-c',
'.ecxr;kp;',
'-loga',
outPath, // 解析结果存放路径
'-c',
'q'
];
const options = {
timeout: time,
cwd: releasePath // 你的符号表路径
};
// 记录此次dump的版本信息
fs.writeFileSync(outPath, svr_type[vers - 1] + "svn版本:" + rev + '\r\n');
var url = "服务器ip与端口";
// 不同版本工程对应不同版本符号表 所以注意svn更到相应的版本
executeWindowsCommand('svn', ['cleanup', releasePath], {})
executeWindowsCommand('svn', ['revert','-R', releasePath], {})
executeWindowsCommand('svn', ['up', '-r', rev, releasePath], {})
// 同步windbg
if (exctorCmdSync(command, args,options)) {
return "http://" + `${url}` + "/" + filename + ".txt";
}
return "false"
5、结果的发送
这部分很简单,是有现成的模板。主要看你是想发送到哪。贴主是发送到企业微信群聊。
async function sendToWechatGroup(param1, param2, param3) {
// 拼接要发送的内容
const content = "你自己设定";
const webhookUrl = '企业微信群机器人webhook';
// 出错情况的业务逻辑
// 完美情况
try {
const response = await axios.post(webhookUrl, {
msgtype: 'text',
text: {
content: content,
},
});
// 发送异常处理
if (response.status === 200 && response.data.errcode === 0) {
console.log(new Date().toLocaleString(),'消息发送成功');
} else {
console.error(new Date().toLocaleString(),'消息发送失败');
}
} catch (error) {
console.error(new Date().toLocaleString(),'发送请求出错:', error);
}
}
6、效果展示
*******************************一些碎碎念**********************************
贴主一开始接手时,是将解析链接发送到群聊。但实际生产中,由机器人同一发送的消息,找不到提交人,这就导致开发人员在排查时都不知道问谁,提交了的同学也不知道是不是自己提交的。于是乎采用了“svn auth”来获取当前同学的姓名。
!!!但竟然有同学没有安装svn命令行工具,导致客户端连接时啪唧一下就断了,于是乎贴主在服务器端采用“socket.name = socket.remoteAddress + ":" + socket.remotePort;”获取当前同学的电脑ip,找运维大哥定位是哪位小可爱,催着ta安装svn命令行。
终于当同学们都准备好前置工具可以无痛自动提交dump后!!!又有聪明的小可爱提出顺便加上崩溃时的游戏场景描述。
好好好有什么需求我都满足放马过来吧!!!最后我在客户端工程新建了Winform项目,增加一个文本描述框记录文本信息。
一通操作下来,竟然有了从0到1的产品设计的赶脚。最后看着群聊里机器人发送的一个又一个dump信息,有种修修的欣慰感,但很快转瞬即逝,毕竟这属于幸灾乐祸了hhhhhhhhhhhhhhh
ps::该帖意在记录与分享自己工作中解决的问题,以及一些疯癫的碎碎念。如果可能帮助到相似情景的广大同胞们,那就真是太棒了!!!