windows服务入门详解

17 篇文章 1 订阅

关于windows服务请看该博文:https://blog.csdn.net/qq_37059136/article/details/114069779

看了上面链接的文章,肯定对本地服务有了清醒的认识,本文就关于如何写一个本地服务并启动关闭进行示例

首先了解本地服务也是一个可执行程序,上面有说程序入口地址一般是main函数,就用最简单的win32控制台程序来写一个服务程序

创建win32控制台程序

项目名字叫http_test219(这个名字默认是你项目编译后可执行文件的名字)

创建cpp文件

我这里创建的是 微服务.cpp

下面附上cpp代码

/*
    代码网址:https://blog.csdn.net/shine_journey/article/details/52485961
	解析网址:http://blog.chinaunix.net/uid-7667983-id-2046535.html
	该文件演示了如何创建一个windows微服务
	由于windows服务不需要界面,所以大部分程序为win32控制台应用程序
	主函数要做的主要工作就是完成①②两步
	①初始化SERVICE_TABLE_ENTRY 分派表结构体
	②调用StartServiceCtrlDispatcher()把调用进程的主线程转换为控制分派器。该分派器启动一个新线程,该线程运行分派表中对应于你的服务的ServiceMain()函数
	ServiceMain函数为void WINAPI ServiceMain(int argc, char** argv)格式的函数,函数名字可以任意定义。它的作用就是:将你需要执行的任务放到该函数中循环执行即可。
	ServiceMain()就是服务程序的工作函数。
	在ServiceMain执行你的任务前,需要给SERVICE_TABLE_ENTRY 分派表结构体进行赋值,注意由于此时服务还没有开始执行你的任务所以我们将服务的状态设置为SERVICE_START_PENDING,即正在初始化
	
	下面演示一个服务,每隔5秒打印一条日志到桌面日志文件中

	注:一个服务程序可以使用CreateService函数将服务的信息添加到SCM的数据库

*/

#include <stdio.h>
#include <iostream>
#include <windows.h>
#include <tchar.h>

using namespace std;

#define SLEEP_TIME 5*1000   //间隔时间
#define FILE_PATH "C:\\Users\\Administrator\\Desktop\\log.txt"  //绝对路径的桌面日志文件

bool b_run = true;
SERVICE_STATUS servicestatus;   //SERVICE_STATUS结构中包含了表示当前服务状态的信息
SERVICE_STATUS_HANDLE hstatus;  //服务状态句柄

int write2Log(char*str);   
void WINAPI ServiceMain(int argc, char** argv);  //服务程序的工作函数
void WINAPI CtrlHandler(DWORD request);

int InitService(); //初始化服务状态

int Write2Log(char* str)
{

	FILE* pfile;
	fopen_s(&pfile, FILE_PATH, "a+");
	if (pfile == NULL)
	{
		return -1;
	}
	fprintf_s(pfile, "%s\n", str);
	fclose(pfile);
	return 0;
}

void WINAPI ServiceMain(int argc, char** argv)  //服务运行的主函数,服务需要提供的功能在此函数中表示
{
	/*
	ServiceMain函数通常忽略传递给它的两个参数,因为服务一般不怎么传递参数。设置一个服务最好的方法就是设置注册表,一般服务在
	HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Service\ServiceName\Parameters子键下存放自己的设置,
	这里的ServiceName是服务的名字。
	事实上,可能要写一个客户应用程序去进行服务的背景设置,这个客户应用程序将这些信息存在注册表中,以便服务读取。
	当一个外部应用程序已经改变了某个正在运行中的服务的设置数据的时候,这个服务能够用RegNotifyChangeKeyValue函数去接受一个通知,这样就允许服务快速的重新设置自己。
	发出启动服务请求之后,如果在一定时间之内无法完成服务的初始化,SCM会认为服务的启动已经失败了,这个时间的长度在Win NT 4.0中是80秒,Win2000中不详…
	基于上面的理由,ServiceMain要迅速完成自身工作,首先是必不可少的两项工作,第一项是调用RegisterServiceCtrlHandler函数去通知SCM它的CtrlHandler回调函数的地址:
	SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(
	LPCTSTR lpServiceName, //服务的名字
	LPHANDLER_FUNCTION lpHandlerProc //CtrlHandler函数地址
	)
	第一个参数指明你正在建立的CtrlHandler是为哪一个服务所用,第二个参数是CtrlHandler函数的地址。lpServiceName必须和在SERVICE_TABLE_ENTRY里面被初始化的服务的名字相匹配。
	RegisterServiceCtrlHandler返回一个SERVICE_STATUS_HANDLE,这是一个32位的句柄。SCM用它来唯一确定这个服务。当这个服务需要把它当时的状态报告给SCM的时候,就必须把这个句柄传给需要它的Win32函数。注意:这个句柄和其他大多数的句柄不同,你无需关闭它。
	SCM要求ServiceMain函数的线程在一秒钟内调用RegisterServiceCtrlHandler函数,否则SCM会认为服务已经失败。但在这种情况下,SCM不会终止服务,不过在NT 4中将无法启动这个服务,同时会返回一个不正确的错误信息,这一点在Windows 2000中得到了修正。
	在RegisterServiceCtrlHandler函数返回后,ServiceMain线程要立即告诉SCM服务正在继续初始化。具体的方法是通过调用SetServiceStatus函数传递SERVICE_STATUS数据结构
	BOOL SetServiceStatus(
	SERVICE_STATUS_HANDLE hService, //服务的句柄
	SERVICE_STATUS lpServiceStatus //SERVICE_STATUS结构的地址
	)
	这个函数要求传递给它指明服务的句柄(刚刚通过调用RegisterServiceCtrlHandler得到),和一个初始化的SERVICE_STATUS结构的地址:
	typedef struct _SERVICE_STATUS
	{
	DWORD dwServiceType;
	DWORD dwCurrentState;
	DWORD dwControlsAccepted;
	DWORD dwWin32ExitCode;
	DWORD dwServiceSpecificExitCode;
	DWORD dwCheckPoint;
	DWORD dwWaitHint;
	} SERVICE_STATUS, *LPSERVICE_STATUS;
	SERVICE_STATUS结构含有七个成员,它们反映服务的现行状态。所有这些成员必须在这个结构被传递到SetServiceStatus之前正确的设置。
	成员dwServiceType指明服务可执行文件的类型。如果你的可执行文件中只有一个单独的服务,就把这个成员设置成SERVICE_WIN32_OWN_PROCESS;如果拥有多个服务的话,就设置成SERVICE_WIN32_SHARE_PROCESS。除了这两个标志之外,如果你的服务需要和桌面发生交互(当然不推荐这样做),就要用“OR”运算符附加上SERVICE_INTERACTIVE_PROCESS。这个成员的值在你的服务的生存期内绝对不应该改变。
	成员dwCurrentState是这个结构中最重要的成员,它将告诉SCM你的服务的现行状态。为了报告服务仍在初始化,应该把这个成员设置成SERVICE_START_PENDING。在以后具体讲述CtrlHandler函数的时候具体解释其它可能的值。
	成员dwControlsAccepted指明服务愿意接受什么样的控制通知。如果你允许一个SCP去暂停/继续服务,就把它设成SERVICE_ACCEPT_PAUSE_CONTINUE。很多服务不支持暂停或继续,就必须自己决定在服务中它是否可用。如果你允许一个SCP去停止服务,就要设置它为SERVICE_ACCEPT_STOP。如果服务要在操作系统关闭的时候得到通知,设置它为SERVICE_ACCEPT_SHUTDOWN可以收到预期的结果。这些标志可以用“OR”运算符组合。
    成员dwWin32ExitCode和dwServiceSpecificExitCode是允许服务报告错误的关键,如果希望服务去报告一个Win32错误代码(预定义在WinError.h中),它就设置dwWin32ExitCode为需要的代码。一个服务也可以报告它本身特有的、没有映射到一个预定义的Win32错误代码中的错误。为了这一点,要把dwWin32ExitCode设置为ERROR_SERVICE_SPECIFIC_ERROR,然后还要设置成员dwServiceSpecificExitCode为服务特有的错误代码。当服务运行正常,没有错误可以报告的时候,就设置成员dwWin32ExitCode为NO_ERROR。
    最后的两个成员dwCheckPoint和dwWaitHint是一个服务用来报告它当前的事件进展情况的。当成员dwCurrentState被设置成SERVICE_START_PENDING的时候,应该把dwCheckPoint设成0,dwWaitHint设成一个经过多次尝试后确定比较合适的数,这样服务才能高效运行。一旦服务被完全初始化,就应该重新初始化SERVICE_STATUS结构的成员,更改dwCurrentState为SERVICE_RUNNING,然后把dwCheckPoint和dwWaitHint都改为0。
    dwCheckPoint成员的存在对用户是有益的,它允许一个服务报告它处于进程的哪一步。每一次调用SetServiceStatus时,可以增加它到一个能指明服务已经执行到哪一步的数字,它可以帮助用户决定多长时间报告一次服务的进展情况。如果决定要报告服务的初始化进程的每一步,就应该设置dwWaitHint为你认为到达下一步所需的毫秒数,而不是服务完成它的进程所需的毫秒数。
	在服务的所有初始化都完成之后,服务调用SetServiceStatus指明SERVICE_RUNNING,在那一刻服务已经开始运行。通常一个服务是把自己放在一个循环之中来运行的。在循环的内部这个服务进程悬挂自己,等待指明它下一步是应该暂停、继续或停止之类的网络请求或通知。当一个请求到达的时候,服务线程激活并处理这个请求,然后再循环回去等待下一个请求/通知。
	如果一个服务由于一个通知而激活,它会先处理这个通知,除非这个服务得到的是停止或关闭的通知。如果真的是停止或关闭的通知,服务线程将退出循环,执行必要的清除操作,然后从这个线程返回。当ServiceMain线程返回并中止时,引起在StartServiceCtrlDispatcher内睡眠的线程激活,并像在前面解释过的那样,减少它运行的服务的计数。
	*/
	servicestatus.dwServiceType = SERVICE_WIN32;
	servicestatus.dwCurrentState = SERVICE_START_PENDING;
	servicestatus.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;//在本例中只接受系统关机和停止服务两种控制命令
	servicestatus.dwWin32ExitCode = 0;
	servicestatus.dwServiceSpecificExitCode = 0;
	servicestatus.dwCheckPoint = 0;
	servicestatus.dwWaitHint = 0;

	//注册服务状态控制函数 注册服务状态控制函数后,会返回一个操作状态服务函数的句柄,注册成功之后,其实服务就已经开始运行了
	hstatus = ::RegisterServiceCtrlHandler(L"http_test219", CtrlHandler);
	if (hstatus == 0)
	{
		Write2Log("RegisterServiceCtrlHandler failed");
		return;
	}

	Write2Log("RegisterServiceCtrlHandler success");

	
	//修改服务工作状态
	servicestatus.dwCurrentState = SERVICE_RUNNING;
	//向SCM 报告运行状态
	SetServiceStatus(hstatus, &servicestatus);

	//下面就开始任务循环了
	//状态报告给SCM管理程序后,我们的服务就可以正常进行了,用一个while(bRunning)循环,不断处理各种情况,提供服务
	b_run = true;

	//这里演示打印内存
	MEMORYSTATUS memstatus;
	char str[100];
	memset(str, '\0', 100);

	while (b_run)
	{
		GlobalMemoryStatus(&memstatus);
		int availmb = memstatus.dwAvailPhys / 1024 / 1024;
		sprintf_s(str, 100, "available memory is %dMB", availmb);
		Write2Log(str);
		Sleep(SLEEP_TIME);
	}
	Write2Log("service stopped");
}

void WINAPI CtrlHandler(DWORD request)
{
	/*
	现在我们还剩下一个函数可以在细节上讨论,那就是服务的CtrlHandler函数。
	当调用RegisterServiceCtrlHandler函数时,SCM得到并保存这个回调函数的地址。一个SCP调一个告诉SCM如何去控制服务的Win32函数,现在已经有10个预定义的控制请求:
	SERVICE_CONTROL_STOP	Requests the service to stop. The hService handle must have SERVICE_STOP access.
	SERVICE_CONTROL_PAUSE	Requests the service to pause. The hService handle must have SERVICE_PAUSE_CONTINUE access.
	SERVICE_CONTROL_CONTINUE	Requests the paused service to resume. The hService handle must have SERVICE_PAUSE_CONTINUE access.
	SERVICE_CONTROL_INTERROGATE	Requests the service to update immediately its current status information to the service control manager. The hService handle must have SERVICE_INTERROGATE access.
	SERVICE_CONTROL_SHUTDOWN	Requests the service to perform cleanup tasks, because the system is shutting down. For more information, see Remarks.
	SERVICE_CONTROL_PARAMCHANGE	Windows 2000: Requests the service to reread its startup parameters. The hService handle must have SERVICE_PAUSE_CONTINUE access.
	SERVICE_CONTROL_NETBINDCHANGE	Windows 2000: Requests the service to update its network binding. The hService handle must have SERVICE_PAUSE_CONTINUE access.
	SERVICE_CONTROL_NETBINDREMOVE	Windows 2000: Notifies a network service that a component for binding has been removed. The service should reread its binding information and unbind from the removed component.
	SERVICE_CONTROL_NETBINDENABLE	Windows 2000: Notifies a network service that a disabled binding has been enabled. The service should reread its binding information and add the new binding.
	SERVICE_CONTROL_NETBINDDISABLE	Windows 2000: Notifies a network service that one of its bindings has been disabled. The service should reread its binding information and remove the binding.
	上表中标有Windows 2000字样的就是2000中新添加的控制代码。除了这些代码之外,服务也可以接受用户定义的,范围在128-255之间的代码。
	当CtrlHandler函数收到一个SERVICE_CONTROL_STOP、SERVICE_CONTROL_PAUSE、 SERVICE_CONTROL_CONTINUE控制代码的时候,SetServiceStatus必须被调用去确认这个代码,并指定你认为服务处理这个状态变化所需要的时间。
	
	例如:你的服务收到了停止请求,首先要把SERVICE_STATUS结构的dwCurrentState成员设置成SERVICE_STOP_PENDING,这样可以使SCM确定你已经收到了控制代码。当一个服务的暂停或停止操作正在执行的时候,必须指定你认为这种操作所需要的时间:这是因为一个服务也许不能立即改变它的状态,它可能必须等待一个网络请求被完成或者数据被刷新到一个驱动器上。指定时间的方法就像我上一章说的那样,用成员dwCheckPoint和dwWaitHint来指明它完成状态改变所需要的时间。如果需要,可以用增加dwCheckPoint成员的值和设置dwWaitHint成员的值去指明你期待的服务到达下一步的时间的方式周期性的报告进展情况。
	当整个启动的过程完成之后,要再一次调用SetServiceStatus。这时就要把SERVICE_STATUS结构的dwCurrentState成员设置成SERVICE_STOPPED,当报告状态代码的同时,一定要把成员dwCheckPoint和dwWaitHint设置为0,因为服务已经完成了它的状态变化。暂停或继续服务的时候方法也一样。
	当CtrlHandler函数收到一个SERVICE_CONTROL_INTERROGATE控制代码的时候,服务将简单的将dwCurrentState成员设置成服务当前的状态,同时,把成员dwCheckPoint和dwWaitHint设置为0,然后再调用SetServiceStatus就可以了。
    在操作系统关闭的时候,CtrlHandler函数收到一个SERVICE_CONTROL_SHUTDOWN控制代码。服务根本无须回应这个代码,因为系统即将关闭。它将执行保存数据所需要的最小行动集,这是为了确定机器能及时关闭。缺省时系统只给很少的时间去关闭所有的服务,MSDN里面说大概是20秒的时间,不过那可能是Windows NT 4的设置,在我的Windows 2000 Server里这个时间是10秒,你可以手动的修改这个数值,它被记录在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control子键里面的WaitToKillServiceTimeout,单位是毫秒。
	
	当CtrlHandler函数收到任何用户定义的代码时,它应该执行期望的用户自定义行动。除非用户自定义的行动要强制服务去暂停、继续或停止,否则不调SetServiceStatus函数。如果用户定义的行动强迫服务的状态发生变化,SetServiceStatus将被调用去设置dwCurrentState、dwCheckPoint和dwWaitHint,具体控制代码和前面说的一样。
	如果你的CtrlHandler函数需要很长的时间执行操作的话,千万要注意:假如CtrlHandler函数在30秒内没有返回的话,SCM将返回一个错误,这不是我们所期望的。所以如果出现上述情况,最好的办法是再建立一个线程,让它去继续执行操作,以便使得CtrlHandler函数能够迅速的返回。例如,当收到一个SERVICE_CONTROL_STOP请求的时候,就像上面说的一样,服务可能正在等待一个网络请求被完成或者数据被刷新到一个驱动器上,而这些操作所需要的时间是你不能估计的,那么就要建立一个新的线程等待操作完成后执行停止命令,CtrlHandler函数在返回之前仍然要报告SERVICE_STOP_PENDING状态,当新的线程执行完操作之后,再由它将服务的状态设置成SERVICE_STOPPED。如果当前操作的时间可以估计的到就不要这样做,仍然使用前面交待的方法处理。
	*/
	switch (request)
	{
	case SERVICE_CONTROL_STOP:
		b_run = false;
		servicestatus.dwCurrentState = SERVICE_STOPPED;
		break;

	case SERVICE_CONTROL_SHUTDOWN:
		b_run = false;
		servicestatus.dwCurrentState = SERVICE_STOPPED;
		break;
	default:
		break;
	}

	SetServiceStatus(hstatus, &servicestatus);
}



int _tmain(int argc, _TCHAR* argv[])
{
	/*
	windows服务被设计用于在后台运行的应用程序以及实现没有用户交互的任务
	一个程序可能包含若干个服务,每个服务都必须列于专门的分派表中,这个表中的每一项都是SERVICE_TABLE_ENTRY结构
	SERVICE_TABLE_ENTRY service_table_entry[] =
	{
	{ serviceName1 , serviceMain1 },
	{ serviceName2 , serviceMain2 },
	{ NULL, NULL },
	};
	第一个成员代表第一个服务的名字,第二个成员是第一个服务ServiceMain回调函数的地址,
	上面拥有两个服务,所以有三个SERVICE_TABLE_ENTRY元素,前两个用于服务,最后的NULL指明数组的结束。
	*/
	//①初始化SERVICE_TABLE_ENTRY 分派表结构体
	SERVICE_TABLE_ENTRY entrytable[2];
	entrytable[0].lpServiceName = L"http_test219";
	entrytable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;   //服务的入口程序
	entrytable[1].lpServiceName = NULL;
	entrytable[1].lpServiceProc = NULL;

	/*
	SERVICE_TABLE_ENTRY这个数组的地址被传递到StartServiceCtrlDispatcher函数:
	BOOL StartServiceCtrlDispatcher(
	LPSERVICE_TABLE_ENTRY lpServiceStartTable
	)
	这个Win32函数表明可执行文件的进程怎样通知SCM包含在这个进程中的服务
	StartServiceCtrlDispatcher为每一个传递到它的数组中的非空元素产生一个新的线程,每一个进程开始执行由数组元素中的lpServiceStartTable指明的ServiceMain函数
	SCM启动一个服务程序之后,它会等待该程序的主线程去调StartServiceCtrlDispatcher。如果那个函数在两分钟内没有被调用,SCM将会认为这个服务有问题,并调用TerminateProcess去杀死这个进程。这就要求你的主线程要尽可能快的调用StartServiceCtrlDispatcher。
	StartServiceCtrlDispatcher函数则并不立即返回,相反它会驻留在一个循环内。当在该循环内时,StartServiceCtrlDispatcher悬挂起自己,等待下面两个事件中的一个发生。
	第一,如果SCM要去送一个控制通知给运行在这个进程内一个服务的时候,这个线程就会激活。当控制通知到达后,线程激活并调用相应服务的CtrlHandler函数。CtrlHandler函数处理这个服务控制通知,并返回到StartServiceCtrlDispatcher。StartServiceCtrlDispatcher循环回去后再一次悬挂自己。
	第二,如果服务线程中的一个服务中止,这个线程也将激活。在这种情况下,该进程将运行在它里面的服务数减一。如果服务数为零,StartServiceCtrlDispatcher就会返回到入口点函数,以便能够执行任何与进程有关的清除工作并结束进程。如果还有服务在运行,哪怕只是一个服务,StartServiceCtrlDispatcher也会继续循环下去,继续等待其它的控制通知或者剩下的服务线程中止。
	*/
	//②调用StartServiceCtrlDispatcher()把调用进程的主线程转换为控制分派器。该分派器启动一个新线程,该线程运行分派表中对应于你的服务的ServiceMain()函数
	StartServiceCtrlDispatcher(entrytable);
	OutputDebugStringA("程序结束!");

	return 0;
}

编译程序

在debug目录下生成了可执行文件

执行这个服务

开始->运行->cmd,必须右键—>以管理员身份运行,此时进入dos命令提示符界面

输入如下命令

 sc create http_test219 binPath= "D:\visual studio 2013\Projects\http_test219\Debug\http_test219.exe"

因为我已经启用服务了,在此创建服务会报错,先删除服务  sc delete http_test219

这样就服务就创建好了

查看已经创建的服务

开始->附件->运行->输入:services.msc (win10直接在搜索里面搜索应用services.msc)

进入系统的服务界面,可看到新创建的http_test219服务

 

 

可以看到已经创建的服务,不过该服务还没有启动

启动服务

sc start http_test219

这时候桌面生成文件

说明服务已经开始运行

关闭服务

sc stop http_test219

 

这时候服务停止了,在日志里面做标记

然后再启动服务

删除服务

删除服务之前要先停止服务

sc stop http_test219

sc delete http_test219

这时候去services.msc中就看不到http_test219服务了

如果自己写了一个服务用cmd去一条一条的写会很繁琐而且不够智能,这时候就要用到批处理

启动服务.bat(bat文件跟服务程序放在同级目录下)

@echo.服务启动......
@echo off
::设置路径及文件名
@set halfpath=%~dp0
@set filename=http_test219.exe
::字符串拼接
@set "filenamefullpath=%halfpath%%filename%"
@sc create http_test219 binPath= "%filenamefullpath%"
@net start http_test219
@sc config http_test219 start= AUTO
@echo off
@echo.启动完毕!
pause

停止服务.bat

sc stop http_test219
TASKKILL /F /IM http_test219.exe
sc delete http_test219
pause

关于bat文件语法问题,可以看该博文:https://blog.csdn.net/qq_37059136/article/details/113862226

以上。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值