当前位置:主页 > c/c++教程 > Windows C++程序

Windows下sentry接入C/C++程序的详细过程

发布:2023-03-02 20:00:02 59


给网友们整理相关的编程文章,网友乔元凯根据主题投稿了本篇教程内容,涉及到Windows、C++程序、Windows、sentry接入C++程序、Windows C++程序相关内容,已被743网友关注,相关难点技巧可以阅读下方的电子资料。

Windows C++程序

sentry简介

首先,如果你是为工作而做,公司一定有自己的sentry账号,去找他们申请权限吧;如果是学习或自己用,先去这里申请一个账号sentry.io。记得,绑定的邮箱很关键,之后的所有崩溃、消息、异常sentry都会发送邮件给你

sentry的作用

为大家通俗的总结:针对开发中的项目,即使我们做足了一切的排查,可仍然有出现crash(崩溃)的可能,这是我们必须承认的;或者,有些时候我们使用try catch,对于某些catch我们很希望知道它的触发率是什么。因此,sentry的本质就是做这样的工作:为我们的工程上一层保险,让我们开发人员能随时知道自己维护的项目是怎样的状态

举个例子:让大家感受一下

例如:这里的项目,我们可以清楚的看到,崩溃触发率等很多我们关心的内容

下方的图片,我们甚至可以清楚的定位到,本次crash发生在线程29428,SentryTest.exe中main函数的71行

这里只举了两个最最基本的例子,但已经能看到sentry的强大。

sentry-native是什么

sentry作为一个开源的软件,发展至今,已经非常成熟。它支持的平台众多,甚至于针对不同的工作者(后台、前端、客户端)都有相应的内容。而我们本次讨论的是针对C/C++的版本
sentry官方GitHub链接

基于C/C++的native版,SDK编译

有一点我在这里吐槽一下:sentry的官方只给了源码和编译方法,为什么不送佛送到西,给咱们把SDK编译好!!!(如果你熟悉vckpg,其实vckpg可以直接集成sentry)

sentry-native C/C++版本源码

1.截至到我写这篇博客,更新到0.5版本,使用可以将其对于的zip下载即可

2.开始编译工作
我只能说,这里的编译应该是他们的疏忽,官方给的流程中有一些问题,我将整个流程进行汇总,并将有问题的地方在后面标注(希望在你使用的时候,这些问题已经被修复)

  • sentry的SDK构建借助的是CMake,在你下载的工程目录下,打开cmd

如果你的开发环境是 macOS ,流程如下

# Configure the CMake build into the `build` directory with crashpad (the default
# backend on macOS, thus optional to specify). Specifying `RelWithDebInfo` as the 
# `CMAKE_BUILD_TYPE` is also optional because it is the default in sentry-native
# for all generators supporting it.
cmake -B build -D SENTRY_BACKEND=crashpad -D CMAKE_BUILD_TYPE=RelWithDebInfo
# build the project
cmake --build build --parallel
# install the resulting artifacts into a specific prefix
cmake --install build --prefix install
# which will result in the following (on macOS):
exa --tree install --level 2
install
├── bin
│  └── crashpad_handler
├── include
│  └── sentry.h
└── lib
   ├── cmake
   ├── libsentry.dylib
   └── libsentry.dylib.dSYM

如果你是在Windows下,流程如下

# The msbuild generator ignores the CMAKE_BUILD_TYPE because it contains all 
# build-types. Here we leave out the backend specification and rely on CMake
# selecting crashpad as Windows' default backend.
cmake -B build 
# The actual build step then requires we specify which build-type we want 
# to apply via the `--config` parameter. Please be aware that in msbuild 
# projects, the `--parallel` option has no effect. 
cmake --build build --config RelWithDebInfo
# install the resulting artifacts (again requiring build-type!)
cmake --install build --prefix install --config RelWithDebInfo
# which will result in the following output (ignoring non-essential lines):
tree /f install
├───bin
│       crashpad_handler.exe
│       crashpad_handler.pdb
│       sentry.dll
│       sentry.pdb
│
├───include
│       sentry.h
│
└───lib
    │   sentry.lib

接下来,进行问题的解决(如果没有问题,忽略这些内容):

0.5版本整个工程编译都会报警告:提示修改文件为Unicode。这是因为这些文件是他们在Linux下做的,所以文件都是UTF-8无签名,而Windows下需要UTF-8带签名。但文件这么多,如果一个个在visual stdio下改不现实,因此,打开CmakeList,将警告忽略,添加一句代码:add_definitions(-w)

一定要注意

一定要注意,上图的例子是我在Windows下做所以写在这里,如果你是Mac下,就要写在外面

工程里面给的example部分,竟然有语法错误。。。这我真没想到。解决办法:自己打开visual stdio解决一下,或者另一个方式,忽略即可,只要正常编译出来了dll、lib、exe即可。

或者打开build目录,cmake成功后,会生成解决方案,用visual stdio打开,这里一定要将编译方法选为下图这个

接着,对每一个项目(有问题的RUN_TASK略过)进行生成

如果你要编译x86版本,除了在vs下将所以相关x64的设置都改成x86或win32,也许还会遇到这个错误:模块对于 SAFESEH 映像是不安全的错误。解决方法是,对于出问题的模块,属性->链接器->命令行->添加:/SAFESEH:NO

暂时就这么多错误,如果你还有其它的错误,百度搜一搜,或问问大佬,别放弃,想想我当时也很难的!!!

sentry-SDK接入本地工程

接入我们自己的工程

接下来,我们将编译好的SDK接入工程,注意:我所有的操作都是以Windows下例子,如果你是后端,去看对应的官方文档吧 sentry入口
ps:SDK,就是我们编译出来的sentry.dll、sentry.lib、crashpad_handler.exe,sentry.h通俗来讲就是可以直接集成到我们工程里的内容。

接下来的操作,熟悉代码编译原理和程序运行原理的朋友一定觉得我啰嗦

1.熟悉代码编译原理和程序运行原理的朋友一定不陌生,一个接入的SDK想要成功编译,需要将头文件引入

2.lib库是头文件中那些函数的具体实现,你需要在你的工程中将其链接进来,可以使用 #pragma comment(lib, “sentry.lib”)手动链接,亦可以在链接器中添加(一定要链接成功,否则编译的时候会报错)

3.sentry.dll是编译出来的程序正常运行需要的动态库,你需要将它和编译出来的内容放到同级目录下,否则编译出来的exe是无法正常运行的。

4.crashpad_handler.exe,这是sentry执行崩溃操作的exe,也就是说,我们调用的接口,最终都是它来执行操作。因此,想让sentry正常工作,需要将它和编译产物放到一起(和sentry.dll一样)。

关于配置

一些必要信息的初始化

记住一句话,sentry的初始化越早越好,始终记得,你的目的是让sentry监控你的项目状态。

这里先上一段代码吧

#include 

int main(void) {
  sentry_options_t *options = sentry_options_new();
  sentry_options_set_dsn(options, "https://examplePublicKey@o0.ingest.sentry.io/0");
  sentry_options_set_release(options, "my-project-name@2.3.12");
  sentry_options_set_debug(options, 1);
  sentry_options_set_environment(options, "production");
  sentry_options_set_database_path(options, ".native");
  sentry_init(options);

  /* ... */
}

上面的代码完成了基本的初始化,也就是说sentry已经可以正常工作了,接下来细说

1.初始化用到的对象
sentry_options_t *options = sentry_options_new();这段代码中options就是后面需要用的对象,或许你可以把它的定义放到类内成员函数处dsn
2.DSN告诉 SDK 将事件发送到哪里。如果未提供此值,SDK 将尝试从SENTRY_DSN环境变量中读取它。如果该变量也不存在,SDK 将不会发送任何事件。
关于dsn的查看如下图,这个dsn是独有的,也是固定的,因此代码中可以直接写死,即调用sentry_options_set_dsn接口,第一个参数就是初始化的对象,第二个参数即dsn地址,是一个字符串

3.Release版本
通常你的产品有多个不同版本,有些时候你可能还需要对它们进行区分,如下图,你可以清晰的看到每个版本的表现

使用sentry_options_set_release(参数1,参数2);参数1为初始化对象,参数2为具体版本(一个字符串)

4.触发环境
有些时候,你的产品可能面向多种用户,例如:product、develop、test。设置环境去进行区分
sentry_options_set_environment(参数1,参数2);参数1为初始化对象,参数2为环境(一个字符串)

5.日志目录
sentry的运行是会产生目录的,如果不设置,就会以当前路径为默认目录,在一些面向用户的产品中,这是肯定不允许的,也许你需要将它设置为temp临时目录下去
sentry_options_set_database_path(参数1,参数2);参数1为初始化对象,参数2为目录路径(字符串,绝对路径和相对路径均可)

自定义配置

有些时候,你可能需要一些其它信息来辅助你排查错误,或者是处理后续的事宜,这里列举几个,可以选择性参考

添加一个自定义content字段

例如上图,这个character字段就是我自己添加的。
下面的函数是我自己封装的函数,第一个参数为自定义的内容,例如:character,第二个参数为一个map,输入你希望的信息,例如:uuid:19,name:Mighty Fighter,都是调用了sentry SDK接口。在需要添加信息的地方使用即可

void addContext(const char* contextName, std::unordered_map textContent)
{
    if (contextName == nullptr || textContent.empty()) {
        return;
    }
    sentry_value_t character = sentry_value_new_object();

    for (auto it = textContent.begin(); it != textContent.end(); it++) {
        sentry_value_set_by_key(character, it->first, sentry_value_new_string(it->second));
    }

    sentry_set_context(contextName, character);
}

丰富本次触发的用户信息
下面的函数是我自定义的,为本次在触发添加用户信息,这里添加了触发的ip地址和uuid,当然你也可以自定义其它内容。注意:sentry可以自动拿到ip地址,格式如下面的代码:sentry_value_new_string(“{{auto}}”)

void addUser(const char* ipAddress, const char* uuid) 
{
    if (uuid == nullptr) {
        return;
    }
    sentry_value_t user = sentry_value_new_object();
    if (ipAddress == nullptr) {
        sentry_value_set_by_key(user, "ip_address", sentry_value_new_string("{{auto}}"));
    } else {
        sentry_value_set_by_key(user, "ip_address", sentry_value_new_string(ipAddress));
    }

    sentry_value_set_by_key(user, "uuid", sentry_value_new_string(uuid));
    LOG_INFO << "[SentryReporter::addUser]: add User: uuid: " << uuid;

    sentry_set_user(user);
}

利用sentry监视崩溃

在完成所有的初始化工作后,sentry就可以为你的工程进行监控了,在出现crash时,即可向你的客户端发送报告。

崩溃回调

sentry提供了崩溃回调函数,什么作用呢?在发送crash时,sentry会执行它的所有工作,拦截+上发,但在这之前,如果你想做一些其它的事情,也是可以的,看如下代码:

sentry_value_t SentryReporter::onCrashCallback(
    const sentry_ucontext_t* uctx, // Crashed user space context
    sentry_value_t event,
    void* closure                  // User data that provide during configuration
)
{
    const std::wstring uuid = APP->getMainWindow()->getUUID();
    addUser(nullptr, Utils::StringUtils::ws2s(uuid).c_str());

    //if you want to give up this crash,Add a judgment method and make the following if true
    if (GIVE_UP_CRASH) {
        sentry_value_decref(event);
        return sentry_value_new_null();
    }

    return event;
}

//----------------初始化时添加-------------------------
sentry_options_set_on_crash(optioning, onCrashCallback, NULL);

代码中,实现了一个函数onCrashCallback,并在sentry初始化时进行绑定,这样做之后,就可以达到上面说的效果。

你可以在崩溃回调函数中做一些操作,例如:添加一些附加信息,又或者调用接口放弃上传此次崩溃
一定要注意,sentry没有做线程安全处理,因此这里的崩溃回调函数一定要轻量级,不要做过重的计算操作

手动发送异常、消息

sentry的另一种用法就是我们手动上发事件,举个例子:代码中try catch,我希望得知某些catch的触发率,就可以在对应的catch处进行一个手动的数据上发。
看如下代码:

void capturingErrorsWithStacktrace(const char* errorMessage) 
{
    sentry_value_t event = sentry_value_new_event();

    sentry_value_t exc = sentry_value_new_exception("Exception", errorMessage);
    sentry_value_set_stacktrace(exc, NULL, 0);
    sentry_event_add_exception(event, exc);
    LOG_INFO << "[SentryReporter::capturingErrorsWithStacktrace]:sentry sent error,message: " << errorMessage;

    sentry_capture_event(event);
}

void capturingMessages(sentry_level_t level, const char* logger, const char* message) 
{
    if (logger == nullptr || message == nullptr) {
        return;
    }

    sentry_value_t event = sentry_value_new_message_event(level, logger, message);
    LOG_INFO << "[SentryReporter::capturingMessages]:sentry  sent message,level: " 
        << level << "logger: " << logger << "message: " << message;

    sentry_capture_event(event);
}

上面的两个函数,一个可以手动发送错误、一个可以手动发送一个自定义消息,这里就不讲述了,自己看代码理解一下。

消息回调

与crash回调一样,手动发送的消息也可以有回调函数,这里直接给出实现,原理请看上文:

/*
* This function sends an error or message manually to make a callback
* You can do some operations, such as adding information
*/
sentry_value_t SentryReporter::beforeSendCallback(sentry_value_t event, void* hint, void* closure)
{
    //do something before send
    return event;
}


//---------------------设置发送消息回调--------------------------
sentry_options_set_before_send(m_pOptions.get(), beforeSendCallback, NULL);

上传PDB,直接定位崩溃发生的位置

sentry的另一个强大之处,在于当发生崩溃时,它不仅可以及时捕捉并上报,还可以配合正确的PDB(Windows下的调试文件),精确的定位到发生crash的exe或dll,甚至可以定位到具体哪一行代码

  • 适用于 iOS、iPadOS、tvOS、watchOS 和 macOS 的dSYM 文件
  • 适用于 Linux 和 Android (NDK) 的ELF 符号
  • 适用于 Windows 的PDB 文件
  • 适用于所有平台的Breakpad 符号
  • WebAssembly 的WASM 文件
  • 适用于 Java 和 Android 的ProGuard 映射

崩溃发生在线程29428,SentryTest.exe中main函数71行


崩溃发生在线程29428,SentryTest.exe中main函数71行

流程如下:

1.最直接的方式是借助第三方的工具sentry-cli.exe,下载链接,下载下来的内容仅仅一个sentry-cli.exe,在cmd下利用该exe进行操作即可

2.首先,你可能需要先对身份进行配置,cmd下该命令,其中如果你是使用的自己项目组或公司的sentry,需要将https://myserver.invalid/换为自己项目的url,这里放置的url是sentry io的官方环境

sentry-cli --url https://myserver.invalid/ login

3.上传调试文件,这里以Windows下的pdb为例子

#首先对调试文件进行检查,是否格式正确
sentry-cli difutil check SentryTest.pdb

如果出现了类似以下内容,即为格式正确

Debug Info File Check
  Type: elf debug companion
  Contained debug identifiers:
    > 924e148f-3bb7-06a0-74c1-36f42f08b40e (x86_64)
  Contained debug information:
    > symtab, debug
  Usable: yes

接下来,将该文件上传,其中 < org >要换你的项目所在组织;< project > 要换位你的项目名称;后面的files则为具体文件,支持绝对路径和相对路径,且支持上传多个文件

sentry-cli upload-dif -o  -p  files...

出现类似如下内容,则上传成功

> Found 2 debug information files
> Prepared debug information files for upload
> Uploaded 2 missing debug information files
> File processing complete:

  PENDING 1ddb3423-950a-3646-b17b-d4360e6acfc9 (MyApp; x86_64 executable)
  PENDING 1ddb3423-950a-3646-b17b-d4360e6acfc9 (MyApp; x86_64 debug companion)

又或者,你可以去sentry项目处查看:

博主自己封装的sentry类

注意:这里的代码是不能直接用的,因为涉及到了一些信息的提取,需要根据自身项目进行适配,在对sentry有一定了解后,再来看

SentryReporter.h

#pragma once
#include "sentry.h"
#include "stdafx.h"
#include 
#include 
#include 

#ifdef ANDROID_SIX
const std::string SENTRY_DSN = "https://b2a9969e03944fae81a12cf84afa6bd2@sentry.netease.com/81";
#else
const std::string SENTRY_DSN = "https://ca88798ea2404243a3dc5a5e2ebdb670@sentry.netease.com/82";
#endif // ANDROID_SIX

const std::string DATABASE_NAME = ".sentry-native";
const std::string ENVIRONMENT = "production";

const bool GIVE_UP_CRASH = false;

struct SentryInitInfo
{
    std::string dsn;
    std::string databasePath;
    std::string releaseVersion;
    std::string environment;
    std::string nemuDownloadLog;
    float sampleRate;//Range must be within :0-1
    int maxBreadcrumbs;//default:100
    bool attachStacktrace;//default:false
};

class SentryReporter {

public:
    SentryReporter();
    ~SentryReporter();
    void setup();
    static void addContext(const char* contextName, std::unordered_map textContent);
    static void addUser(const char* ipAddress, const char* uuid);//if ipAddress = nullptr,get from system
    static void capturingErrorsWithStacktrace(const char* errorMessage);
    static void capturingMessages(sentry_level_t level, const char* logger, const char* message);

private:
    void fetchInitInfomation();
    static sentry_value_t onCrashCallback(const sentry_ucontext_t* uctx, sentry_value_t event, void* closure);
    static sentry_value_t beforeSendCallback(sentry_value_t event, void* hint, void* closure);

private:
    shared_ptr m_pOptions;
    SentryInitInfo m_sentryInitInfo;
};

SentryReporter.cpp

#include "SentryReport.h"
#include "utils.h"
#include "VersionNo.h"
#include "MainWindow.h"

sentry_value_t SentryReporter::onCrashCallback(
    const sentry_ucontext_t* uctx, // Crashed user space context
    sentry_value_t event,
    void* closure                  // User data that provide during configuration
)
{
    const std::wstring uuid = APP->getMainWindow()->getUUID();
    addUser(nullptr, Utils::StringUtils::ws2s(uuid).c_str());

    //if you want to give up this crash,Add a judgment method and make the following if true
    if (GIVE_UP_CRASH) {
        sentry_value_decref(event);
        return sentry_value_new_null();
    }

    return event;
}

/*
* This function sends an error or message manually to make a callback
* You can do some operations, such as adding information
*/
sentry_value_t SentryReporter::beforeSendCallback(sentry_value_t event, void* hint, void* closure)
{
    //do something before send
    return event;
}

SentryReporter::SentryReporter()
    :m_pOptions(sentry_options_new())
{
    fetchInitInfomation();
}

SentryReporter::~SentryReporter()
{
    sentry_close();
    m_pOptions = nullptr;
}

void SentryReporter::fetchInitInfomation()
{
    m_sentryInitInfo.dsn = SENTRY_DSN;

    std::wstring databasePath = Utils::pathCombine(DCommon::DSystemUtils::getTempPath(), Utils::StringUtils::s2ws(DATABASE_NAME));
    m_sentryInitInfo.databasePath = Utils::StringUtils::ws2s(databasePath);

    m_sentryInitInfo.releaseVersion = Utils::StringUtils::ws2s(STRVERSION);
    m_sentryInitInfo.environment = ENVIRONMENT;

    std::wstring logFilePath = Utils::getLogFile();
    m_sentryInitInfo.nemuDownloadLog = Utils::StringUtils::ws2s(logFilePath);

    sentry_options_set_auto_session_tracking(m_pOptions.get(), false);
    sentry_options_set_symbolize_stacktraces(m_pOptions.get(), true);

    //use default value
    m_sentryInitInfo.sampleRate = 1;
    m_sentryInitInfo.maxBreadcrumbs = 100;
    m_sentryInitInfo.attachStacktrace = false;
}

void SentryReporter::setup()
{
    sentry_options_set_dsn(m_pOptions.get(), m_sentryInitInfo.dsn.c_str());
    sentry_options_set_database_path(m_pOptions.get(), m_sentryInitInfo.databasePath.c_str());
    sentry_options_set_release(m_pOptions.get(), m_sentryInitInfo.releaseVersion.c_str());
    sentry_options_set_environment(m_pOptions.get(), m_sentryInitInfo.environment.c_str());
    sentry_options_set_traces_sample_rate(m_pOptions.get(), m_sentryInitInfo.sampleRate);
    sentry_options_add_attachment(m_pOptions.get(), m_sentryInitInfo.nemuDownloadLog.c_str());

    sentry_options_set_on_crash(m_pOptions.get(), onCrashCallback, NULL);
    sentry_options_set_before_send(m_pOptions.get(), beforeSendCallback, NULL);

    int ret = sentry_init(m_pOptions.get());
    if (ret != 0) {
        LOG_INFO << "[SentryReporter::setup]: sentry init error";
    }
}

void SentryReporter::addContext(const char* contextName, std::unordered_map textContent)
{
    if (contextName == nullptr || textContent.empty()) {
        LOG_INFO << "[SentryReporter::addContext]: contextName==nullptr or textContent==nullptr";
        return;
    }
    sentry_value_t character = sentry_value_new_object();

    for (auto it = textContent.begin(); it != textContent.end(); it++) {
        sentry_value_set_by_key(character, it->first, sentry_value_new_string(it->second));
    }

    sentry_set_context(contextName, character);
}

void SentryReporter::addUser(const char* ipAddress, const char* uuid) 
{
    if (uuid == nullptr) {
        LOG_INFO << "[SentryReporter::addUser]: uuid==nullptr";
        return;
    }
    sentry_value_t user = sentry_value_new_object();
    if (ipAddress == nullptr) {
        sentry_value_set_by_key(user, "ip_address", sentry_value_new_string("{{auto}}"));
    } else {
        sentry_value_set_by_key(user, "ip_address", sentry_value_new_string(ipAddress));
    }

    sentry_value_set_by_key(user, "uuid", sentry_value_new_string(uuid));
    LOG_INFO << "[SentryReporter::addUser]: add User: uuid: " << uuid;

    sentry_set_user(user);
}

void SentryReporter::capturingErrorsWithStacktrace(const char* errorMessage) 
{
    sentry_value_t event = sentry_value_new_event();

    sentry_value_t exc = sentry_value_new_exception("Exception", errorMessage);
    sentry_value_set_stacktrace(exc, NULL, 0);
    sentry_event_add_exception(event, exc);
    LOG_INFO << "[SentryReporter::capturingErrorsWithStacktrace]:sentry sent error,message: " << errorMessage;

    sentry_capture_event(event);
}

void SentryReporter::capturingMessages(sentry_level_t level, const char* logger, const char* message) 
{
    if (logger == nullptr || message == nullptr) {
        return;
    }

    sentry_value_t event = sentry_value_new_message_event(level, logger, message);
    LOG_INFO << "[SentryReporter::capturingMessages]:sentry  sent message,level: " 
        << level << "logger: " << logger << "message: " << message;

    sentry_capture_event(event);
}

到此这篇关于Windows——sentry接入C/C++程序的文章就介绍到这了,更多相关Windows C++程序内容请搜索码农之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持码农之家!


参考资料

相关文章

  • windows7配置Nginx+php+mysql教程

    发布:2019-06-20

    这篇文章主要介绍了windows7配置Nginx+php+mysql的详细教程 的相关资料,需要的朋友可以参考下


  • Python控制windows系统音量实现实例

    发布:2023-03-10

    这篇文章主要介绍了Python控制windows系统音量实现实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧


  • Windows安装Anaconda并且配置国内镜像的详细教程

    发布:2023-03-10

    我们在学习 Python 的时候需要不同的 Python 版本,关系到电脑环境变量配置换来换去很是麻烦,所以这个时候我们需要一个虚拟的 Python 环境变量,这篇文章主要介绍了Windows安装Anaconda并且配置国内镜像教程,需要的朋友可以参考下


  • python在windows调用svn-pysvn的实现

    发布:2023-04-04

    本文主要介绍了python在windows调用svn-pysvn的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧


  • 利用Python实现Windows下的鼠标键盘模拟的实例代码

    利用Python实现Windows下的鼠标键盘模拟的实例代码

    发布:2022-06-29

    为网友们分享了关于Python的教程,本篇文章主要介绍了利用Python实现Windows下的鼠标键盘模拟的实例代码,具有一定的参考价值,有兴趣的可以了解一下


  • C语言实现经典windows游戏扫雷的示例代码

    发布:2023-03-04

    今天我们会用C语言实现一个经典的windows小游戏:扫雷。扫雷是一款单机小游戏,每次通关最高难度的关卡都会开心好一阵。现在学会了C语言,总算可以自己实现扫雷了。话不多说,咱们开始吧


  • Java代码如何判断linux系统windows系统

    发布:2023-03-04

    这篇文章主要介绍了Java代码如何判断linux系统windows系统问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教


  • Windows Server 2008 R2和2012中PHP连接MySQL过慢的原因

    发布:2019-12-04

    这篇文章主要介绍了Windows Server 2008 R2和2012中PHP连接MySQL过慢的解决方法,同时对Windows 7和8的本地开发环境也有效,需要的朋友可以参考下


网友讨论