首先,服务程序的架构是服务器/客服端这样的结构呈现;服务程序就是承担服务器的功能,另外还必须有客户端程序与它配合使用,否则仅有服务程序就失去了意义。
我们可以利用VC创建Win32项目中的控制台程序,也可以创建MFC项目程序,这里我们采用第一种。
一、服务端介绍
1、 服务程序的工作原理
(1)、服务程序的安装
服务程序的运行环境是SCM(Service Control Manager),因此,要运行服务程序,首先要将它加入SCM。安装服务程序,简单地说,就是将服务程序加入“开始-控制面板-管理工具-服务”中的“服务”对话框。由于SCM并不直接支持服务程序的安装,只能通过自己的服务程序来安装。
每个服务程序在SCM中有一个显示名称以区别于其它服务程序,这个名称就是SERVICE.h文件中的SZSERVICEDISPLAYNAME宏变量,它为"My SDK Simple Service",它要通过CreateService()来实现。
与服务程序安装相关的主要API函数是CreateService(),这部分请参考本项目中的CmdInstallService()函数。
(2)、服务程序的启动
服务程序可能加入SCM就开始运行,也可能加入后并未运行,需要手动运行,这取决于CreateService()的参数。
手动启动服务程序的方法:单击“开始-控制面板-管理工具-服务”项,在“服务”对话框中,在所需的服务程序的快捷菜单中,选择“启动”即可。
与服务程序启动相关的主要API函数是StartServiceCtrlDispatcher(), 该函数的一大特点是只能在SCM启动,直接运行会报1063错误。不过,我们的项目编译成功后,会调用该函数,就总会在系统日志文件中写入这样的错误。别紧张,错误是暂时的,只要我们将服务程序安装进SCM,然后启动它就可以了。
2、 服务程序的相关文件
(1)、服务程序的躯壳部分
服务程序有自身的特点,就是要在SCM中操作,要实现这个功能,就会有相关的代码来保障,这就是服务程序的躯壳。
这部分代码的相关文件位于SERVICE.h/Service.cpp。
(2)、服务程序的实现部分
服务程序不是花瓶,供人观看的,它是要做事的,为客户端提供功能的。这部分代码的相关文件位于SIMPLE.cpp文件中的ServiceStart()函数,客户端就是与这部分打交道。
服务器与客服端通信的方式较多,这里采用管道来实现的。
3、 服务程序的注意事项
要关闭360安全卫士和360杀毒软件,否则注册表不能修改,自己开发的服务程序也不能启动,还不能将文件放人启动菜单。
4、 服务程序的使用方法
服务程序的使用方法,不是鼠标单击或者双击打开,一般是以命令的方式执行, 即在操作系统桌面左下角的命令框中运行。
如,我的执行文件位于G:\Temp\ Service\Release\Service.exe,我要安装服务,可以在命令框中输入如下内容再回车:
G:\Temp\ Service\Release\Service –install
或者G:\Temp\ Service\Release\ Service/install
其它操作如调试、移除的用法,如法炮制。
二、客户端介绍
使用管道技术与服务器通信。
本例的关键代码如下:
//一、服务端
//1、SERVICE.h文件
//执行文件的名称,用于安装、调试本软件
#define SZAPPNAME "Service"
//服务的内部名称
#define SZSERVICENAME "MySDKSimpleService"
//显示服务的名称
#define SZSERVICEDISPLAYNAME "My SDK Simple Service"
//服务依赖项列表 - "dep1\0dep2\0\0"
#define SZDEPENDENCIES ""
//以下函数定义于Simple.CPP文件,用以实现服务的实际功能
VOID ServiceStart(DWORD dwArgc, LPTSTR *lpszArgv);
VOID ServiceStop();
//设置服务的当前状态,并将其报告给服务控制管理器
BOOL ReportStatusToSCMgr(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint);
//将错误消息添加到系统日志
void AddToMessageLog(LPTSTR lpszMsg);
//2、Service.cpp文件
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <tchar.h>
#include "service.h"
SERVICE_STATUS ssStatus;
SERVICE_STATUS_HANDLE sshStatusHandle;
DWORD dwErr = 0;
BOOL bDebug = FALSE;
TCHAR szErr[256];
VOID WINAPI service_ctrl(DWORD dwCtrlCode);
VOID WINAPI service_main(DWORD dwArgc, LPTSTR *lpszArgv);
VOID CmdInstallService();
VOID CmdRemoveService();
VOID CmdDebugService(int argc, char **argv);
BOOL WINAPI ControlHandler ( DWORD dwCtrlType );
LPTSTR GetLastErrorText( LPTSTR lpszBuf, DWORD dwSize );
int _tmain(int argc, _TCHAR* argv[])
{
SERVICE_TABLE_ENTRY dispatchTable[] =
{
{TEXT(SZSERVICENAME), (LPSERVICE_MAIN_FUNCTION)service_main },
{ NULL, NULL }
};
if ((argc > 1) && ((*argv[1] == '-') || (*argv[1] == '/')))
{
if ( _stricmp( "install", argv[1]+1 ) == 0 )
{
CmdInstallService();
}
else if ( _stricmp( "remove", argv[1]+1 ) == 0 )
{
CmdRemoveService();
}
else if ( _stricmp( "debug", argv[1]+1 ) == 0 )
{
bDebug = TRUE;
CmdDebugService(argc, argv);
}
else
{
goto dispatch;
}
exit(0);
}
dispatch:
printf( "%s -install to install the service\n", SZAPPNAME );
printf( "%s -remove to remove the service\n", SZAPPNAME );
printf( "%s -debug <params> to run as a console app for debugging\n", SZAPPNAME );
printf( "\nStartServiceCtrlDispatcher being called.\n" );
printf( "This may take several seconds. Please wait.\n" );
if (!StartServiceCtrlDispatcher(dispatchTable))
AddToMessageLog(TEXT("StartServiceCtrlDispatcher failed."));
return 1;
}
//service_main函数为服务程序的入口,它与ServiceMain函数的格式一致。
void WINAPI service_main(DWORD dwArgc, LPTSTR *lpszArgv)
{
//注册我们的服务控制处理器
sshStatusHandle = RegisterServiceCtrlHandler(TEXT(SZSERVICENAME), service_ctrl);
if (!sshStatusHandle)
goto cleanup;
//示例中,没有更改SERVICE_STATUS的成员
ssStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
ssStatus.dwServiceSpecificExitCode = 0;
//将状态信息报告给服务控制管理器。
if (!ReportStatusToSCMgr(
SERVICE_START_PENDING, //服务状态
NO_ERROR, //退出代码
3000)) //等待提示
goto cleanup;
//以下函数定义于Simple.CPP文件,用以实现服务的实际功能;
//其它的都是服务的架构相关的代码
ServiceStart(dwArgc, lpszArgv);
cleanup:
//将状态报告给服务控制管理器
if (sshStatusHandle)
(VOID)ReportStatusToSCMgr(
SERVICE_STOPPED,
dwErr,
0);
}
//当这个服务调用ControlService()时,这个函数会被SCM调用。
//ControlService()于CmdRemoveService()中被调用。
//即调用CmdRemoveService()时,service_ctrl()被SCM调用。
//参数为请求的控件类型。
VOID WINAPI service_ctrl(DWORD dwCtrlCode)
{
switch(dwCtrlCode)
{
//调用定义于Simple.CPP文件中的ServiceStop(),以停止服务。
//这可能会导致1053 -服务没有回应…错误。
case SERVICE_CONTROL_STOP:
ReportStatusToSCMgr(SERVICE_STOP_PENDING, NO_ERROR, 0);
ServiceStop();
return;
//更新服务状态。
case SERVICE_CONTROL_INTERROGATE:
break;
//无效的控制代码。
default:
break;
}
ReportStatusToSCMgr(ssStatus.dwCurrentState, NO_ERROR, 0);
}
//本函数用于将状态报告给服务控制管理器;
//参数分别表示:服务状态、退出代码、等待提示。
BOOL ReportStatusToSCMgr(DWORD dwCurrentState,
DWORD dwWin32ExitCode,
DWORD dwWaitHint)
{
static DWORD dwCheckPoint = 1;
BOOL fResult = TRUE;
if (!bDebug) //当不调试时,我们才能向服务控制管理器报告。
{
if (dwCurrentState == SERVICE_START_PENDING)
ssStatus.dwControlsAccepted = 0;
else
ssStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
ssStatus.dwCurrentState = dwCurrentState;
ssStatus.dwWin32ExitCode = dwWin32ExitCode;
ssStatus.dwWaitHint = dwWaitHint;
if ((dwCurrentState == SERVICE_RUNNING) ||
(dwCurrentState == SERVICE_STOPPED))
ssStatus.dwCheckPoint = 0;
else
ssStatus.dwCheckPoint = dwCheckPoint++;
//真正实现将状态报告给服务控制管理器。
if (!(fResult = SetServiceStatus(sshStatusHandle, &ssStatus)))
{
AddToMessageLog(TEXT("SetServiceStatus函数"));
}
}
return fResult;
}
//本函数用于将错误消息发送到系统日志文件。
VOID AddToMessageLog(LPTSTR lpszMsg)
{
TCHAR szMsg[256];
HANDLE hEventSource;
LPTSTR lpszStrings[2];
if (!bDebug)
{
dwErr = GetLastError();
//使用事件日志记录错误。
hEventSource = RegisterEventSource(NULL, TEXT(SZSERVICENAME));
_stprintf(szMsg, TEXT("%s 错误: %d"), TEXT(SZSERVICENAME), dwErr);
lpszStrings[0] = szMsg;
lpszStrings[1] = lpszMsg;
if (hEventSource != NULL)
{
ReportEvent(hEventSource, //事件源句柄
EVENTLOG_ERROR_TYPE, //事件类型
0, //事件类别
0, //事件标识符
NULL, //当前用户的SID
2, //在lpszStrings中的字符串
0, //没有字节的原始数据
(LPCSTR*)lpszStrings, //错误字符串数组
NULL); //没有原始数据
(VOID) DeregisterEventSource(hEventSource);
}
}
}
//本函数用于安装服务程序
//执行该命令的方法:在DOS下,切换到Service.exe所在路径,输入Service -install或者Service/install后回车。
//此时printf()函数起作用,而AfxMessageBox()不起作用。
void CmdInstallService()
{
SC_HANDLE schService;
SC_HANDLE schSCManager;
TCHAR szPath[512];
if (GetModuleFileName(NULL, szPath, 512) == 0)
{
_tprintf(TEXT("不能安装%s - %s\n"),
TEXT(SZSERVICEDISPLAYNAME),
GetLastErrorText(szErr, 256));
return;
}
schSCManager = OpenSCManager(
NULL, //参数为NULL,则连接到本机的服务控制管理器。
NULL, //参数为NULL,则使用默认数据库。
SC_MANAGER_ALL_ACCESS //允许使用所有的权限。
);
if (schSCManager)
{
//创建服务对象并将其添加到指定的服务控制管理器数据库中
schService = CreateService(
schSCManager, //由OpenSCManager函数返回的句柄。
TEXT(SZSERVICENAME), //要安装的服务的名称。
TEXT(SZSERVICEDISPLAYNAME), //用户界面程序用来标识服务的显示名称。
SERVICE_ALL_ACCESS, //服务的访问权限,这里为允许使用所有的权限。
SERVICE_WIN32_OWN_PROCESS, //服务类型,这里为在自己的进程中运行服务。
SERVICE_DEMAND_START, //服务启动选项,这里为调用StartService函数时,由服务控制管理器启动的服务。
SERVICE_ERROR_NORMAL, //服务启动失败的错误值,这里为将错误记录在事件日志中,但继续启动操作。
szPath, //服务文件的完全限定路径。
NULL, //服务所属组的名称,这里为不属于某个组。
NULL, //接收上一个参数中指定的组中唯一的标记值,这里为不更改现有标记。
TEXT(SZDEPENDENCIES), //服务依赖项。
NULL, //服务运行时使用的帐户的名称,这里为 LocalSystem帐户。
NULL); //服务运行时使用的密码,这里为不需要密码。
if (schService)
{
_tprintf(TEXT("%s 安装\n"), TEXT(SZSERVICEDISPLAYNAME));
CloseServiceHandle(schService);
}
else
{
_tprintf(TEXT("创建服务失败 - %s\n"), GetLastErrorText(szErr, 256));
}
CloseServiceHandle(schSCManager);
}
else
{
_tprintf(TEXT("打开管理器失败 - %s\n"), GetLastErrorText(szErr,256));
}
}
//本函数用于移除服务程序
//执行该命令的方法:在DOS下,切换到Service.exe所在路径,输入Service -remove或者Service/remove后回车。
void CmdRemoveService()
{
SC_HANDLE schService;
SC_HANDLE schSCManager;
schSCManager = OpenSCManager(
NULL, //参数为NULL,则连接到本机的服务控制管理器。
NULL, //参数为NULL,则使用默认数据库。
SC_MANAGER_ALL_ACCESS //允许使用所有的权限。
);
if (schSCManager)
{
schService = OpenService(schSCManager, TEXT(SZSERVICENAME), SERVICE_ALL_ACCESS);
if (schService)
{
//向服务发送控制代码,这里为停止服务。
if (ControlService(schService, SERVICE_CONTROL_STOP, &ssStatus))
{//函数操作成功
_tprintf(TEXT("停止过程中 %s。"), TEXT(SZSERVICEDISPLAYNAME));
Sleep(1000);
//根据指定的信息级别检索指定服务的当前状态。
while(QueryServiceStatus(schService, &ssStatus))
{
if (ssStatus.dwCurrentState == SERVICE_STOP_PENDING)
{
_tprintf(TEXT("."));
Sleep(1000);
}
else
break;
}
if (ssStatus.dwCurrentState == SERVICE_STOPPED)
{
_tprintf(TEXT("\n%s 已经停止。\n"), TEXT(SZSERVICEDISPLAYNAME));
}
else
{
_tprintf(TEXT("\n%s 停止失败。\n"), TEXT(SZSERVICEDISPLAYNAME));
}
}
//移除服务
if(DeleteService(schService))
{
_tprintf(TEXT("%s 移除。\n"), TEXT(SZSERVICEDISPLAYNAME) );
}
else
{
_tprintf(TEXT("移除服务失败。 - %s\n"), GetLastErrorText(szErr,256));
}
CloseServiceHandle(schService);
}
else
{
_tprintf(TEXT("打开服务失败 - %s\n"), GetLastErrorText(szErr,256));
}
CloseServiceHandle(schSCManager);
}
else
{
_tprintf(TEXT("打开管理器失败 - %s\n"), GetLastErrorText(szErr,256));
}
}
//本函数用于运行服务程序
//执行该命令的方法:在DOS下,切换到Service.exe所在路径,输入Service -debug或者Service/debug后回车。
//本函数同安装函数相比,可以测试客户端与服务端的通信,但不会添加到SCM。
void CmdDebugService(int argc, char ** argv)
{
DWORD dwArgc;
LPTSTR *lpszArgv;
#ifdef UNICODE
lpszArgv = CommandLineToArgvW(GetCommandLineW(), &(dwArgc));
#else
dwArgc = (DWORD) argc;
lpszArgv = argv;
#endif
_tprintf(TEXT("调试过程中 %s.\n"), TEXT(SZSERVICEDISPLAYNAME));
SetConsoleCtrlHandler(ControlHandler, TRUE);
ServiceStart(dwArgc, lpszArgv);
}
BOOL WINAPI ControlHandler(DWORD dwCtrlType)
{
switch(dwCtrlType)
{
//使用Ctrl+C或Ctrl+Break来模拟。
case CTRL_BREAK_EVENT:
//在调试模式下使用SERVICE_CONTROL_STOP,以停止服务。
case CTRL_C_EVENT:
_tprintf(TEXT("停止过程中 %s。\n"), TEXT(SZSERVICEDISPLAYNAME));
ServiceStop();
return TRUE;
break;
}
return FALSE;
}
LPTSTR GetLastErrorText(LPTSTR lpszBuf, DWORD dwSize)
{
DWORD dwRet;
LPTSTR lpszTemp = NULL;
dwRet = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |FORMAT_MESSAGE_ARGUMENT_ARRAY,
NULL,
GetLastError(),
LANG_NEUTRAL,
(LPTSTR)&lpszTemp,
0,
NULL);
if (!dwRet || ((long)dwSize < (long)dwRet+14 ))
lpszBuf[0] = TEXT('\0');
else
{
lpszTemp[lstrlen(lpszTemp)-2] = TEXT('\0');
_stprintf(lpszBuf, TEXT("%s (0x%x)"), lpszTemp, GetLastError());
}
if (lpszTemp)
LocalFree((HLOCAL) lpszTemp);
return lpszBuf;
}
//3、SIMPLE.cpp文件
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <tchar.h>
#include "service.h"
//服务应该结束时,该事件有信号
HANDLE hServerStopEvent = NULL;
//服务的实际代码
//dwArgc - 命令行参数的数量
//lpszArgv - 命令行参数数组
//默认的行为是打开一个命名管道,\\.\pipe\simple,并从它读取。
//它修改数据并将其写回管道。
//当hServerStopEvent被通知时,服务停止。
VOID ServiceStart (DWORD dwArgc, LPTSTR *lpszArgv)
{
HANDLE hPipe = INVALID_HANDLE_VALUE;
HANDLE hEvents[2] = {NULL, NULL};
OVERLAPPED os;
PSECURITY_DESCRIPTOR pSD = NULL;
SECURITY_ATTRIBUTES sa;
TCHAR szIn[80];
TCHAR szOut[80];
LPTSTR lpszPipeName = TEXT("\\\\.\\pipe\\simple");
BOOL bRet;
DWORD cbRead;
DWORD cbWritten;
DWORD dwWait;
UINT ndx;
//
// 开始服务初始化
//
//将状态信息报告给服务控制管理器。
if (!ReportStatusToSCMgr(
SERVICE_START_PENDING, //服务状态
NO_ERROR, //退出代码
3000)) //等待提示
goto cleanup;
//创建事件对象。控制处理器函数发出信号
//当它接收到"stop"控制代码时发生此事件。
hServerStopEvent = CreateEvent(
NULL, //没有安全属性
TRUE, //手工重置的事件
FALSE, //无初始信号
NULL); //无名称
if (hServerStopEvent == NULL)
goto cleanup;
hEvents[0] = hServerStopEvent;
if (!ReportStatusToSCMgr(
SERVICE_START_PENDING,
NO_ERROR,
3000))
goto cleanup;
hEvents[1] = CreateEvent(
NULL,
TRUE,
FALSE,
NULL);
if (hEvents[1] == NULL)
goto cleanup;
if (!ReportStatusToSCMgr(
SERVICE_START_PENDING,
NO_ERROR,
3000))
goto cleanup;
//创建一个安全描述符,允许任何人写入管道…
pSD = (PSECURITY_DESCRIPTOR) malloc( SECURITY_DESCRIPTOR_MIN_LENGTH );
if (pSD == NULL)
goto cleanup;
if (!InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION))
goto cleanup;
//添加一个空disc. ACL到安全描述符。
if (!SetSecurityDescriptorDacl(pSD, TRUE, (PACL) NULL, FALSE))
goto cleanup;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = pSD;
sa.bInheritHandle = TRUE;
//将状态信息报告给服务控制管理器。
if (!ReportStatusToSCMgr(
SERVICE_START_PENDING, //服务状态
NO_ERROR, //退出代码
3000)) //等待提示
goto cleanup;
//允许用户tp定义管道名称
for (ndx = 1; ndx < dwArgc-1; ndx++)
{
if (((*(lpszArgv[ndx]) == TEXT('-')) ||
(*(lpszArgv[ndx]) == TEXT('/'))) &&
_tcsicmp(TEXT("pipe"), lpszArgv[ndx]+1) == 0)
{
lpszPipeName = lpszArgv[++ndx];
}
}
//打开我们的命名管道...
hPipe = CreateNamedPipe(
lpszPipeName, //管道的名字
FILE_FLAG_OVERLAPPED |
PIPE_ACCESS_DUPLEX, //管道打开模式
PIPE_TYPE_MESSAGE |
PIPE_READMODE_MESSAGE |
PIPE_WAIT, //管道类型
1, //实例数量
0, //输出的大小,0则根据需要分配
0, //输入的大小
1000, //默认的超时值
&sa); //安全属性
if (hPipe == INVALID_HANDLE_VALUE)
{
AddToMessageLog(TEXT("无法创建命名管道"));
goto cleanup;
}
if (!ReportStatusToSCMgr(
SERVICE_RUNNING,
NO_ERROR,
0))
goto cleanup;
//
//结束服务初始化
//
//服务现在正在运行,执行工作直到关机。
while (1)
{
memset( &os, 0, sizeof(OVERLAPPED) );
os.hEvent = hEvents[1];
ResetEvent( hEvents[1] );
//等待连接……
ConnectNamedPipe(hPipe, &os);
if (GetLastError() == ERROR_IO_PENDING)
{
dwWait = WaitForMultipleObjects( 2, hEvents, FALSE, INFINITE );
if (dwWait != WAIT_OBJECT_0+1) //没有重叠I/O事件-错误发生,
break; //或服务器停止信号。
}
memset(&os, 0, sizeof(OVERLAPPED));
os.hEvent = hEvents[1];
ResetEvent( hEvents[1] );
//抓取管子里的东西…
bRet = ReadFile(
hPipe, //要读取的文件
szIn, //输入缓冲器地址
sizeof(szIn), //要读取的字节数
&cbRead, //实际读取的字节数
&os); //重叠I/O
if (!bRet && (GetLastError() == ERROR_IO_PENDING))
{
dwWait = WaitForMultipleObjects( 2, hEvents, FALSE, INFINITE );
if (dwWait != WAIT_OBJECT_0+1)
break;
}
_stprintf(szOut, TEXT("Hello! [%s]"), szIn);
memset( &os, 0, sizeof(OVERLAPPED) );
os.hEvent = hEvents[1];
ResetEvent( hEvents[1] );
//写东西进管子…
bRet = WriteFile(
hPipe, //要写的文件
szOut, //输出缓冲器地址
sizeof(szOut), //要写的字节数
&cbWritten, //实际写的字节数
&os); //重叠I/O
if (!bRet && ( GetLastError() == ERROR_IO_PENDING))
{
dwWait = WaitForMultipleObjects( 2, hEvents, FALSE, INFINITE );
if (dwWait != WAIT_OBJECT_0+1)
break;
}
//抛弃连接
DisconnectNamedPipe(hPipe);
}
cleanup:
if (hPipe != INVALID_HANDLE_VALUE)
CloseHandle(hPipe);
if (hServerStopEvent)
CloseHandle(hServerStopEvent);
if (hEvents[1]) // 重叠i/o事件
CloseHandle(hEvents[1]);
if (pSD)
free(pSD);
}
//停止该服务。
//如果ServiceStop过程将要执行时间超过3秒,
//它应该生成一个线程来执行停止代码,并返回。
//否则,ServiceControlManager认为服务已经停止响应。
VOID ServiceStop()
{
if (hServerStopEvent)
SetEvent(hServerStopEvent);
}
//二、客户端
//发送并接收数据
//服务端接收到数据后,将数据加上“Hello!”前缀后,
//且使用[]括起来,送回客户端。
void CClientDlg::OnClickedButton1()
{
char inbuf[80];
char outbuf[80];
DWORD bytesRead;
BOOL ret;
LPSTR lpszPipeName = "\\\\.\\pipe\\simple";
LPSTR lpszString;
CString strSend;
((CEdit*)GetDlgItem(IDC_EDIT1))->GetWindowText(strSend);
strcpy(outbuf, strSend);
//连接到服务端的管道服务器
ret = CallNamedPipeA(lpszPipeName,
outbuf, sizeof(outbuf),
inbuf, sizeof(inbuf),
&bytesRead, NMPWAIT_WAIT_FOREVER);
CString strTemp;
if (ret)
{
strTemp.Format("%s\n", inbuf);
m_listBox.AddString(strTemp); //将接收数据插入列表框。
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通