C语言控制台程序编写Windows服务(一)

服务的概念:
Microsoft Windows 服务(过去称为 NT 服务)允许用户创建可在其自身的 Windows 会话中长时间运行的可执行应用程序。 这些服务可在计算机启动时自动启动,可以暂停和重启,并且不显示任何用户界面。
注:所以一个正常运行的服务程序是不应该执行完就退出的。应该在所有进程中的所有服务均终止时才返回退出。

目标:
编写一个服务,能够正常添加到Windows服务列表并能随系统启动,可正常手动启停。暂时不需要通过服务实现任何实际的功能。

组成部分:
使用C语言通过WindowsAPI编写一个可用的服务至少要包括以下几部分内容:
1. 服务程序的入口点
2. 服务程序的主体ServiceMain函数
3. 服务程序的控制处理功能函数
以上三者缺一不可,否则将无法正常启动。

具体步骤:

1.创建服务
该部分可选,如果忽略创建服务部分可使用手动方式添加服务。
2.编写入口点
第一个重要组成部分,必不可少。

		SERVICE_TABLE_ENTRY	service_table[2];				//为可以在调用过程中运行的服务指定ServiceMain函数
		service_table[0].lpServiceName = L"TestService";			//此服务在进程中运行的名称
		service_table[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;	//指向ServiceMain函数的指针
		service_table[1].lpServiceName = NULL;					//表中最后一个条目的成员必须具有NULL值才能指定表的末尾
		service_table[1].lpServiceProc = NULL;

		result = StartServiceCtrlDispatcherW(service_table);	//将服务进程的主线程连接到服务控制管理器,该服务控制管理器使该线程成为调用过程的服务控制调度程序线程
		if (result == 0)
		{
			printf("StartServiceCtrlDispatcherW error: %d\n", GetLastError());

			break;
		}

3.编写ServiceMain函数
第二个重要组成部分,必不可少。

VOID WINAPI ServiceMain(DWORD argc, LPWSTR* argv)
{
	service_status_handle = RegisterServiceCtrlHandlerW(L"TestService", (LPHANDLER_FUNCTION)ServiceControlHandler);  //参数2为函数指针,该指针指向的函数定义了如何处理服务控制管理器发来的控制请求
        //填充SERVICE_STATUS结构体,该结构体为全局变量
	service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	service_status.dwCurrentState = SERVICE_RUNNING;            //如果有需要初始化的内容,此处可以先填SERVICE_START_PENDING,待初始化完成后将值改为SERVICE_RUNNING
	service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
	service_status.dwWin32ExitCode = 0;
	service_status.dwServiceSpecificExitCode = 0;
	service_status.dwCheckPoint = 0;
	service_status.dwWaitHint = 0;

	SetServiceStatus(service_status_handle, &service_status);   //该函数用以更新服务控制管理器的的状态信息

	return;

4.编写控制处理程序功能(即前面代码参数2对应的函数)
第三个重要组成部分,必不可少。该函数用于接收服务控制管理器(SCM)发来的指令,然后根据指令让服务做出相应的状态更新。如下面代码,若不增加对停止服务指令的状态更新,则服务无法正常停止。

VOID WINAPI ServiceControlHandler(DWORD parameter)                 //该函数的参数即服务控制管理器传入的控制参数
{
	service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	service_status.dwCurrentState = SERVICE_RUNNING;
	service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
	service_status.dwWin32ExitCode = 0;
	service_status.dwServiceSpecificExitCode = 0;
	service_status.dwCheckPoint = 0;
	service_status.dwWaitHint = 0;

	if (parameter == SERVICE_CONTROL_STOP)                     //根据参数内容更新对应的服务状态
	{
		service_status.dwCurrentState = SERVICE_STOPPED;
	}

	SetServiceStatus(service_status_handle, &service_status);

	return;
}

包含以上内容就完成了作为一个最基本的Windows服务。

以下为该示例完整代码:

#include <stdio.h>
#include <Windows.h>

SERVICE_STATUS           service_status;
SERVICE_STATUS_HANDLE    service_status_handle;

BOOL InstallService()
{
	BOOL		result = FALSE;
	SC_HANDLE	SCM_handle = NULL;
	SC_HANDLE	service_handle = NULL;
	DWORD		return_value = 0;
	TCHAR		Path[MAX_PATH];
	HANDLE		file_handle = NULL;
	char		error[64];
	DWORD		bytes_written = 0;
	DWORD		last_error = 0;

	SecureZeroMemory(error, sizeof(error));

	do
	{
		return_value = GetModuleFileNameW(NULL, Path, MAX_PATH);
		if (return_value == 0)
		{
			printf("GetModuleFileName error: %d\n", GetLastError());

			break;
		}

		SCM_handle = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
		if (SCM_handle == NULL)
		{
			printf("OpenSCManagerW error: %d\n", GetLastError());

			break;
		}

		service_handle = OpenServiceW(SCM_handle, L"TestService", SC_MANAGER_ALL_ACCESS);
		if (service_handle != NULL)
		{
			printf("Test Service Created\n");
		}
		else
		{
			service_handle = CreateServiceW(
				SCM_handle,
				L"TestService",
				L"TestService",
				SC_MANAGER_ALL_ACCESS,
				SERVICE_WIN32_OWN_PROCESS,
				SERVICE_AUTO_START,
				SERVICE_ERROR_IGNORE,
				Path,
				NULL,
				NULL,
				NULL,
				NULL,
				NULL
			);
			if (service_handle == NULL)
			{
				printf("CreateServiceW error: %d\n", GetLastError());

				break;
			}
		}
		result = TRUE;

	} while (0);

	if (SCM_handle != NULL)
	{
		CloseServiceHandle(SCM_handle);
	}
	if (service_handle != NULL)
	{
		CloseServiceHandle(service_handle);
	}

	return result;
}

VOID WINAPI ServiceControlHandler(DWORD parameter)
{
	service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	service_status.dwCurrentState = SERVICE_RUNNING;
	service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
	service_status.dwWin32ExitCode = 0;
	service_status.dwServiceSpecificExitCode = 0;
	service_status.dwCheckPoint = 0;
	service_status.dwWaitHint = 0;

	if (parameter == SERVICE_CONTROL_STOP)
	{
		service_status.dwCurrentState = SERVICE_STOPPED;
	}

	SetServiceStatus(service_status_handle, &service_status);

	return;
}

VOID WINAPI ServiceMain(DWORD argc, LPWSTR* argv)
{
	service_status_handle = RegisterServiceCtrlHandlerW(L"TestService", (LPHANDLER_FUNCTION)ServiceControlHandler);

	service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	service_status.dwCurrentState = SERVICE_RUNNING;
	service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
	service_status.dwWin32ExitCode = 0;
	service_status.dwServiceSpecificExitCode = 0;
	service_status.dwCheckPoint = 0;
	service_status.dwWaitHint = 0;

	SetServiceStatus(service_status_handle, &service_status);

	return;
}

int main(int argc, char* argv[])
{
	BOOL		result = 0;
	HANDLE		file_handle = NULL;
	DWORD		last_error = 0;

	do
	{
		result = InstallService();
		if (result == 0)
		{
			printf("InstallService faild.\n");
			break;
		}

		SERVICE_TABLE_ENTRY	service_table[2];
		service_table[0].lpServiceName = L"TestService";
		service_table[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;
		service_table[1].lpServiceName = NULL;
		service_table[1].lpServiceProc = NULL;

		result = StartServiceCtrlDispatcherW(service_table);
		if (result == 0)
		{
			printf("StartServiceCtrlDispatcherW error: %d\n", GetLastError());

			break;
		}
	} while (0);

	return 0;
}

ANSI与Unicode

  1. C语言中用char数据类型来表示一个8位ANSI字符。
  2. MicroSoft的C/C++编译器定义了一个内建的数据类型wchar_t,它表示一个16位的Unicode(UTF-16)字符。
  3. 自Windows NT起,Windows的所有版本都完全用Unicode来构建。也就是说,所有核心函数(创建窗口、显示文本、进行字符串处理等等)都需要Unicode字符串。
  4. 调用Windows函数时,如果向它传入一个ANSI字符串,那么函数首先会把字符串转换为Unicode,再把结果传给操作系统。这会让系统产生时间和内存上的开销。
  5. Windows API中的一些函数(如WinExee和OpenFile)存在的唯一目的就是为了兼容16位Windows程序,因为其只支持ANSI字符串。所以在开发新程序是应避免使用这些函数。在使用WinExee和OpenFile调用的地方,应该用CreateProcess和CreateFile函数调用代替。
  6. 在C运行库中,strlen就是一个能返回ANSI字符串长度的函数。与之对应的是wcslen,这个C运行库函数能返回Unicode字符串的长度。

枚举服务状态EnumServicesStatusExW

在指定的服务控制管理器数据库中枚举服务。

函数原型:
BOOL
WINAPI
EnumServicesStatusExW(
_In_ SC_HANDLE hSCManager,
参数1:服务控制管理器数据库的句柄。该句柄由OpenSCManager函数返回 ,并且必须具有SC_MANAGER_ENUMERATE_SERVICE访问权限。

_In_ SC_ENUM_TYPE InfoLevel,
参数2:要返回的服务属性。使用SC_ENUM_PROCESS_INFO检索数据库中每个服务的名称和服务状态信息。所述lpServices参数是一个指向接收数组的缓冲器 ENUM_SERVICE_STATUS_PROCESS结构。缓冲区必须足够大以容纳结构及其成员指向的字符串。

_In_ DWORD dwServiceType,
参数3:
要枚举的服务类型。此参数可以是以下值中的一个或多个。

表1
价值 意义
SERVICE_DRIVER
0x0000000B
类型为SERVICE_KERNEL_DRIVERSERVICE_FILE_SYSTEM_DRIVER的服务
SERVICE_FILE_SYSTEM_DRIVER
0x00000002
文件系统驱动程序服务。
SERVICE_KERNEL_DRIVER
0x00000001
驱动服务。
SERVICE_WIN32
0x00000030
类型为SERVICE_WIN32_OWN_PROCESSSERVICE_WIN32_SHARE_PROCESS的服务
SERVICE_WIN32_OWN_PROCESS
0x00000010
在自己的进程中运行的服务。
SERVICE_WIN32_SHARE_PROCESS
0x00000020
与一个或多个其他服务共享一个流程的服务。有关更多信息,请参见服务程序

_In_ DWORD dwServiceState,
参数4:
要枚举的服务状态。此参数可以是以下值之一。

表2
价值 意义
SERVICE_ACTIVE
0x00000001
枚举处于以下状态的服务:SERVICE_START_PENDINGSERVICE_STOP_PENDINGSERVICE_RUNNINGSERVICE_CONTINUE_PENDINGSERVICE_PAUSE_PENDINGSERVICE_PAUSED
SERVICE_INACTIVE
0x00000002
枚举处于SERVICE_STOPPED状态的服务。
SERVICE_STATE_ALL
0x00000003
合并SERVICE_ACTIVESERVICE_INACTIVE状态。

_Out_writes_bytes_opt_(cbBufSize) LPBYTE lpServices,
参数5:指向接收状态信息的缓冲区的指针。该数据的格式取决于InfoLevel参数的值。该数组的最大大小为256K字节。若要确定所需的大小,请为此参数指定NULL,为cbBufSize参数指定0 。该函数将失败,并且GetLastError将返回ERROR_MORE_DATA。该pcbBytesNeeded参数将接收到所需的大小。

_In_ DWORD cbBufSize,
参数6:lpServices参数指向的缓冲区大小,以字节为单位。

_Out_ LPDWORD pcbBytesNeeded,
参数7:如果缓冲区太小,则指向变量的指针,该变量接收返回剩余服务条目所需的字节数。

_Out_ LPDWORD lpServicesReturned,
参数8:指向一个变量的指针,该变量接收返回的服务条目数。

_Inout_opt_ LPDWORD lpResumeHandle,
参数9:指向变量的指针,该变量在输入时指定枚举的起点。您必须在第一次调用EnumServicesStatusEx函数时将此值设置为零 。在输出中,如果函数成功,则该值为零。但是,如果函数返回零,并且 GetLastError函数返回ERROR_MORE_DATA,则此值指示调用EnumServicesStatusEx函数以检索其他数据时要读取的下一个服务条目 。

_In_opt_ LPCWSTR pszGroupName
参数10:加载顺序组名称。如果此参数是字符串,则枚举的唯一服务是属于具有由字符串指定的名称的组的服务。如果此参数为空字符串,则仅枚举不属于任何组的服务。如果此参数为NULL,则将忽略组成员身份,并枚举所有服务。
);

示例:

#include <stdio.h>
#include <Windows.h>
#include <locale.h>

int main(int argc, char* argv[])
{
    HANDLE							handle_service_manager = 0;
    int								return_value = 0;
    unsigned char*					service_status_process = NULL;
    DWORD							buffer_size = 0;
    DWORD							bytes_needed = 0;
    DWORD							services_returned = 0;
    ENUM_SERVICE_STATUS_PROCESS*	enmu_service_status_process;

    setlocale(LC_ALL, "chs");

    do
    {
        handle_service_manager = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
        if (handle_service_manager == NULL)
        {
            printf("OpenSCManager Error:%d\n", GetLastError());

            break;
        }

        return_value = EnumServicesStatusExW(
            handle_service_manager,		//服务控制管理器数据库的句柄。
            SC_ENUM_PROCESS_INFO,		//要返回的服务属性。
            SERVICE_WIN32,				//要枚举的服务类型。
            SERVICE_STATE_ALL,			//要枚举的服务状态。
            NULL,						//指向接收状态信息的缓冲区的指针。该数据的格式取决于InfoLevel参数的值。
            buffer_size,				//lpServices参数指向的缓冲区大小,以字节为单位。
            &bytes_needed,				//如果缓冲区太小,则指向变量的指针,该变量接收返回剩余服务条目所需的字节数。
            &services_returned,			//指向一个变量的指针,该变量接收返回的服务条目数。
            0,							//指向变量的指针,该变量在输入时指定枚举的起点。您必须在第一次调用EnumServicesStatusEx函数时将此值设置为零。
            NULL						//加载顺序组名称。
        );
        if (return_value == 0 && GetLastError() != ERROR_MORE_DATA)
        {
            printf("get bytes needed faild\n");

            break;
        }
        printf("bytes_needed:%d\n", bytes_needed);
        buffer_size = bytes_needed;
        service_status_process = LocalAlloc(LPTR, buffer_size);
        if (service_status_process == NULL)
        {
            break;
        }
        SecureZeroMemory(service_status_process, buffer_size);

        return_value = EnumServicesStatusExW(
            handle_service_manager,		//服务控制管理器数据库的句柄。
            SC_ENUM_PROCESS_INFO,		//要返回的服务属性。
            SERVICE_WIN32,				//要枚举的服务类型。
            SERVICE_STATE_ALL,			//要枚举的服务状态。
            service_status_process,		//指向接收状态信息的缓冲区的指针。该数据的格式取决于InfoLevel参数的值。
            buffer_size,				//lpServices参数指向的缓冲区大小,以字节为单位。
            &bytes_needed,				//如果缓冲区太小,则指向变量的指针,该变量接收返回剩余服务条目所需的字节数。
            &services_returned,			//指向一个变量的指针,该变量接收返回的服务条目数。
            0,							//指向变量的指针,该变量在输入时指定枚举的起点。您必须在第一次调用EnumServicesStatusEx函数时将此值设置为零。
            NULL						//加载顺序组名称。
        );
        if (return_value == 0)
        {
            printf("EnumServicesStatusExW2 faild:%d\n", GetLastError());

            break;
        }

        printf("services returned:%d\n\n", services_returned);

        enmu_service_status_process = (ENUM_SERVICE_STATUS_PROCESS*)service_status_process;
        for (size_t i = 0; i < services_returned; i++)
        {
            printf("ServiceName:%ws\n", enmu_service_status_process[i].lpServiceName);
            printf("DisplayName:%ws\n", enmu_service_status_process[i].lpDisplayName);
            printf("ProcessId:%d\n", enmu_service_status_process[i].ServiceStatusProcess.dwProcessId);
            printf("CurrentState:%d\n\n", enmu_service_status_process[i].ServiceStatusProcess.dwCurrentState);
        }

    } while (0);

    return 0;
}

注:遍历多个相同的结构体时可以把结构体看做数组,以数组下标的方式表示。

https://docs.microsoft.com/zh-cn/windows/win32/api/winsvc/nf-winsvc-enumservicesstatusexw

打开服务控制管理器OpenSCManagerW

在指定的计算机上建立与服务控制管理器的连接,并打开指定的服务控制管理器数据库。

函数原型:
SC_HANDLE OpenSCManagerW(
LPCWSTR lpMachineName,
LPCWSTR lpDatabaseName,
DWORD dwDesiredAccess
);

参数1:目标计算机的名称。如果指针为NULL或指向空字符串,则该函数将连接到本地计算机上的服务控制管理器。

参数2:服务控制管理器数据库的名称。此参数应设置为SERVICES_ACTIVE_DATABASE。如果为NULL,则默认情况下将打开SERVICES_ACTIVE_DATABASE数据库。

参数3:访问服务控制管理器。

服务控制管理器的访问权限

以下是SCM的特定访问权限。

服务控制管理器的访问权限
访问权 描述
SC_MANAGER_ALL_ACCESS(0xF003F) 除了此表中的所有访问权限之外,还包括STANDARD_RIGHTS_REQUIRED
SC_MANAGER_CREATE_SERVICE(0x0002) 调用CreateService函数来创建服务对象并将其添加到数据库中是必需的。
SC_MANAGER_CONNECT(0x0001) 连接到服务控制管理器所需。
SC_MANAGER_ENUMERATE_SERVICE(0x0004) 调用EnumServicesStatusEnumServicesStatusEx函数以列出数据库中的服务是必需的。创建或删除任何服务时
,调用NotifyServiceStatusChange函数以接收通知是必需的。
SC_MANAGER_LOCK(0x0008) 调用LockServiceDatabase函数以获取数据库锁是必需的。
SC_MANAGER_MODIFY_BOOT_CONFIG(0x0020) 调用NotifyBootConfigStatus函数所需。
SC_MANAGER_QUERY_LOCK_STATUS(0x0010) 调用QueryServiceLockStatus函数以检索数据库的锁状态信息所必需。

以下是SCM的常规访问权限

服务控制管理器的访问权限
访问权 描述
GENERIC_READ STANDARD_RIGHTS_READ
SC_MANAGER_ENUMERATE_SERVICE
SC_MANAGER_QUERY_LOCK_STATUS
GENERIC_WRITE STANDARD_RIGHTS_WRITE
SC_MANAGER_CREATE_SERVICE
SC_MANAGER_MODIFY_BOOT_CONFIG
GENERIC_EXECUTE STANDARD_RIGHTS_EXECUTE
SC_MANAGER_CONNECT
SC_MANAGER_LOCK
GENERIC_ALL SC_MANAGER_ALL_ACCESS

注意:权限要采取用多少申请多少的原则。

https://docs.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-openscmanagerw

创建进程函数CreateProcess

用来创建一个新进程和它的主线程。

函数原型:

BOOL CreateProcessA(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);

参数1:指向一个NULL结尾的、用来指定可执行模块的字符串。
这个字符串可以是可执行模块的绝对路径,也可以是相对路径,在后一种情况下,函数使用当前驱动器和目录建立可执行模块的路径。
这个参数可以被设为NULL,在这种情况下,可执行模块的名字必须处于 lpCommandLine 参数最前面并由空格符与后面的字符分开。

参数2:指向一个以NULL结尾的字符串,该字符串指定要执行的命令行。
这个参数可以为空,那么函数将使用lpApplicationName参数指定的字符串当做要运行的程序的命令行。
如果lpApplicationName和lpCommandLine参数都不为空,那么lpApplicationName参数指定将要被运行的模块,lpCommandLine参数指定将被运行的模块的命令行。新运行的进程可以使用GetCommandLine函数获得整个命令行。C语言程序可以使用argc和argv参数。

参数3:指向SECURITY_ATTRIBUTES结构体的指针,该结构体确定子进程是否可以继承返回到新进程对象的句柄。如果lpProcessAttributes为NULL,则不能继承该句柄。

参数4:指向SECURITY_ATTRIBUTES结构体的指针,该结构体确定子进程是否可以继承返回到新线程对象的句柄。如果lpThreadAttributes为NULL,则不能继承该句柄。

参数5:如果此参数为TRUE,则新进程将继承调用进程中的每个可继承句柄。如果参数为FALSE,则不会继承句柄。请注意,继承的句柄与原始句柄具有相同的值和访问权限。

参数6:控制优先级类别和流程创建的标志。
⑴值:CREATE_DEFAULT_ERROR_MODE
含义:新的进程不继承调用进程的错误模式。CreateProcess函数赋予新进程当前的默认错误模式作为替代。应用程序可以调用SetErrorMode函数设置当前的默认错误模式。
这个标志对于那些运行在没有硬件错误环境下的多线程外壳程序是十分有用的。
对于CreateProcess函数,默认的行为是为新进程继承调用者的错误模式。设置这个标志以改变默认的处理方式。
⑵值:CREATE_NEW_CONSOLE
含义:新的进程将使用一个新的控制台,而不是继承父进程的控制台。这个标志不能与DETACHED_PROCESS标志一起使用。
⑶值:CREATE_NEW_PROCESS_GROUP
含义:新进程将是一个进程树的根进程。进程树中的全部进程都是根进程的子进程。新进程树的用户标识符与这个进程的标识符是相同的,由lpProcessInformation参数返回。进程树经常使用GenerateConsoleCtrlEvent函数允许发送CTRL+C或CTRL+BREAK信号到一组控制台进程。
⑷值:CREATE_SEPARATE_WOW_VDM
如果被设置,新进程将会在一个私有的虚拟DOS机(VDM)中运行。另外,默认情况下所有的16位Windows应用程序都会在同一个共享的VDM中以线程的方式运行。单独运行一个16位程序的优点是一个应用程序的崩溃只会结束这一个VDM的运行;其他那些在不同VDM中运行的程序会继续正常的运行。同样的,在不同VDM中运行的16位Windows应用程序拥有不同的输入队列,这意味着如果一个程序暂时失去响应,在独立的VDM中的应用程序能够继续获得输入。
⑸值:CREATE_SHARED_WOW_VDM
如果WIN.INI中的Windows段的DefaultSeparateVDM选项被设置为真,这个标识使得CreateProcess函数越过这个选项并在共享的虚拟DOS机中运行新进程。
⑹值:CREATE_SUSPENDED
含义:新进程的主线程会以暂停的状态被创建,直到调用ResumeThread函数被调用时才运行。
⑺值:CREATE_UNICODE_ENVIRONMENT
含义:如果被设置,由lpEnvironment参数指定的环境块使用Unicode字符,如果为空,环境块使用ANSI字符。
⑻值:DEBUG_PROCESS
含义:如果这个标志被设置,调用进程将被当做一个调试程序,并且新进程会被当做被调试的进程。系统把被调试程序发生的所有调试事件通知给调试器。
如果你使用这个标志创建进程,只有调用进程(调用CreateProcess函数的进程)可以调用WaitForDebugEvent函数。
⑼值:DEBUG_ONLY_THIS_PROCESS
含义:如果此标志没有被设置且调用进程正在被调试,新进程将成为调试调用进程的调试器的另一个调试对象。如果调用进程没有被调试,有关调试的行为就不会产生。
⑽值:DETACHED_PROCESS
含义:对于控制台进程,新进程没有访问父进程控制台的权限。新进程可以通过AllocConsole函数自己创建一个新的控制台。这个标志不可以与CREATE_NEW_CONSOLE标志一起使用。
〔11〕值:CREATE_NO_WINDOW
含义:系统不为新进程创建CUI窗口,使用该标志可以创建不含窗口的CUI程序。

参数7:指向新进程的环境块的指针。如果此参数为NULL,则新进程将使用调用进程的环境。

参数8:指向一个以NULL结尾的字符串,这个字符串用来指定子进程的工作路径。这个字符串必须是一个包含驱动器名的绝对路径。如果这个参数为空,新进程将使用与调用进程相同的驱动器和目录。

参数9:指向STARTUPINFO或STARTUPINFOEX结构的指针 。

参数10:指向PROCESS_INFORMATION结构的指针,该结构体接收有关新进程的标识信息。该参数为输出参数。

使用匿名管道实现远程cmd

DWORD WINAPI DoublePipe(LPVOID p)
{
    BOOL		DataSend;
    int			SendDataLen = 0;
    char		cmdline[1024];
    char		PipeBuffer[1024];
    HANDLE		hRead2;
    HANDLE		hWrite2;
    SECURITY_ATTRIBUTES sa2;		//这个结构为很多函数创建对象时提供安全性设置。
    STARTUPINFO		si;		//用于指定新进程的主窗口特性的一个结构。
    PROCESS_INFORMATION	pi;		//在创建进程时相关的数据结构之一,该结构返回有关新进程及其主线程的信息。
    BOOL		Result;
    DWORD		bytesRead;
    int			ReadLen = 0;	//已读数据长度

    struct Socket_Recv* Information = p;

    do
    {
        SecureZeroMemory(cmdline, sizeof(cmdline));
        SecureZeroMemory(&si, sizeof(si));
        SecureZeroMemory(PipeBuffer, sizeof(PipeBuffer));

        sa2.nLength = sizeof(SECURITY_ATTRIBUTES);
        sa2.lpSecurityDescriptor = NULL;
        sa2.bInheritHandle = TRUE;
        CreatePipe(&hRead2, &hWrite2, &sa2, 0);

        si.cb = sizeof(STARTUPINFO);
        GetStartupInfo(&si);            //WindowsAPI函数。该函数取得进程在启动时被指定的 STARTUPINFO 结构。
        si.hStdError = hWrite2;
        si.hStdOutput = hWrite2;
        si.hStdInput = Information->hRecvData;
        si.wShowWindow = SW_HIDE;
        si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;

        GetSystemDirectory(cmdline, MAX_PATH + 1); //取得Windows系统目录(System目录)的完整路径名。
        strcat_s(cmdline, sizeof(cmdline), "\\cmd.exe");

        Result = CreateProcess(NULL, cmdline, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
        if (Result == 0)
        {
            printf("Error code is %u", GetLastError());
            break;
        }

        while (1)
        {
            Result = PeekNamedPipe(hRead2, PipeBuffer, sizeof(PipeBuffer) - 1, &bytesRead, NULL, NULL);
            if (Result == FALSE)
            {
                printf("PeekNamePipe Faild.");
                break;
            }
            if (bytesRead > 0)
            {
                Result = ReadFile(hRead2, PipeBuffer, sizeof(PipeBuffer) - 1, &bytesRead, NULL);
                if (Result == FALSE)
                {
                    printf("ReadFile Faild.");
                    break;
                }
                PipeBuffer[bytesRead] = 0;
                printf("%s", PipeBuffer);

                SendDataLen = strlen(PipeBuffer) + 1;
                DataSend = SendPacket(Information->socket, PipeBuffer, SendDataLen);
                if (DataSend == FALSE)
                {
                    printf("Send Pipe Information faild !\n");
                    printf("The connection is closed !\n");
                    break;
                }
            }
        }
    } while (0);

    CloseHandle(hWrite2);
    CloseHandle(hRead2);
    CloseHandle(Information->hRecvData);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    closesocket(Information->socket);
    return 0;

}


DWORD WINAPI RecvData(LPVOID p)
{
    int					DataRecv;
    int					RecvLen = 1024;
    int					IDRecvLen = 16;
    HANDLE				hNamePipe;
    DWORD				bytesRead;
    BOOL				Result;
    DWORD				ReturnValue;
    HANDLE				hRead1;
    HANDLE				hWrite1;
    SECURITY_ATTRIBUTES sa1;
    struct Socket_Recv Information;

    Information.socket = (SOCKET)p;

    do
    {
        sa1.nLength = sizeof(SECURITY_ATTRIBUTES);
        sa1.lpSecurityDescriptor = NULL;
        sa1.bInheritHandle = TRUE;
        CreatePipe(&hRead1, &hWrite1, &sa1, 0);
        Information.hRecvData = hRead1;

        CreateThread(NULL, 0, DoublePipe, &Information, 0, NULL);

        while (1)
        {
            RecvLen = sizeof(Information.recvbuffer) - 1;
            DataRecv = RecvPacket(Information.socket, Information.recvbuffer, &RecvLen);
            if (DataRecv == FALSE)
            {
                printf("Data receive faild !\n");
                break;
            }
            printf("%s\n", Information.recvbuffer);
            Information.recvbuffer[RecvLen] = 0;
            if (Information.recvbuffer[0] == '/')
            {
                if (Information.recvbuffer[1] == 'p' &&
                    Information.recvbuffer[2] == 'r' &&
                    Information.recvbuffer[3] == 'o' &&
                    Information.recvbuffer[4] == 'c' &&
                    Information.recvbuffer[5] == '\0')
                {
                    CreateThread(NULL, 0, ListProcess, (LPVOID)Information.socket, 0, NULL);
                }
                else if (
                    Information.recvbuffer[1] == 'k' &&
                    Information.recvbuffer[2] == 'i' &&
                    Information.recvbuffer[3] == 'l' &&
                    Information.recvbuffer[4] == 'l' &&
                    Information.recvbuffer[5] == ' ')
                {
                    strncpy_s(Information.ProcessID, sizeof(Information.ProcessID), Information.recvbuffer + 6, sizeof(Information.ProcessID) - 1);
                    printf("%s\n", Information.ProcessID);
                    ReturnValue = StrToIntA((PCSTR)Information.ProcessID);
                    Result = KillProc(ReturnValue);
                    if (Result == FALSE)
                    {
                        printf("Kill Process Faild.\n");
                    }
                }
                else if (
                    Information.recvbuffer[1] == 'd' &&
                    Information.recvbuffer[2] == 'r' &&
                    Information.recvbuffer[3] == 'i' &&
                    Information.recvbuffer[4] == 'v' &&
                    Information.recvbuffer[5] == 'e' &&
                    Information.recvbuffer[6] == 'r' &&
                    Information.recvbuffer[7] == '\0')
                {
                    CreateThread(NULL, 0, DriverList, (LPVOID)Information.socket, 0, NULL);
                }
                else if (
                    Information.recvbuffer[1] == 'f' &&
                    Information.recvbuffer[2] == 'f' &&
                    Information.recvbuffer[3] == ' ')
                {
                    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)FindFile, &Information, 0, NULL);
                }
                else if (
                    Information.recvbuffer[1] == 'd' &&
                    Information.recvbuffer[2] == 'l' &&
                    Information.recvbuffer[3] == ' ')
                {
                    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)FileDownload, &Information, 0, NULL);
                }
            }
            else
            {
                strcat_s(Information.recvbuffer, sizeof(Information.recvbuffer), "\r\n");
                printf("%s\n", Information.recvbuffer);

                WriteFile(hWrite1, Information.recvbuffer, strlen(Information.recvbuffer), &bytesRead, NULL);
            }
        }
    } while (0);

    closesocket(Information.socket);
    printf("The connection is closed !\n");
    WSACleanup();
    return 0;
}

使用两个匿名管道(pipe)实现双向通信。

pipe1连接接收数据的缓冲区和cmd进程的hStdInput;

pipe2连接cmd进程的hStdError、hStdOutput和发送数据的缓冲区,以便让服务端获得回显。

创建匿名管道CreatePipe

函数原型:

BOOL CreatePipe(
PHANDLE hReadPipe,
PHANDLE hWritePipe,
LPSECURITY_ATTRIBUTES lpPipeAttributes,
DWORD nSize
);

参数1:指向变量的指针,该变量接收管道的读句柄。

参数2:指向变量的指针,该变量接收管道的写句柄。

参数3:指向SECURITY_ATTRIBUTES结构的指针,该 结构确定子进程是否可以继承返回的句柄。如果lpPipeAttributes为NULL,则不能继承该句柄。
该结构的lpSecurityDescriptor成员为新管道指定安全描述符。如果lpPipeAttributes为NULL,则管道将获取默认的安全描述符。管道的默认安全描述符中的ACL来自创建者的主令牌或模拟令牌。

参数4:管道缓冲区的大小,以字节为单位。大小只是一个建议;系统使用该值来计算适当的缓冲机制。如果此参数为零,则系统使用默认缓冲区大小。

管道的读句柄和写句柄:
从管道里读数据通过读句柄(hReadPipe),往管道里写数据通过写句柄(hWritePipe)。
所以将管道中的数据写入文件里应该通过读句柄
将文件中的数据写入管道应该通过写句柄

创建线程函数CreateThread

使用WindowsAPI中的创建线程函数

函数原型:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpsa,
  DWORD cbStack,
  LPTHREAD_START_ROUTINE lpStartAddr,
  LPVOID lpvThreadParam,
  DWORD fdwCreate,
  LPDWORD lpIDThread
);

参数1:一般填NULL,选择默认

参数2:设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小。任何情况下,Windows根据需要动态延长堆栈的大小。

参数3:指向线程函数的指针

参数4:向线程函数传递的参数,是一个指向结构的指针,不需传递参数时,为NULL

参数5:选择0或CREATE_SUSPENDED。0表示立即激活,CREATE_SUSPENDED表示该线程在挂起状态下创建,直到调用ResumeThread函数才运行

参数6:指向接收线程标识符的32位变量的长指针。如果此参数为NULL,则不返回线程标识符

返回值:若设置了参数6,则成功时返回新线程的句柄。

Keep_Alive(TCP协议)

当连接已经建立,如不采取任何措施,则无法确认对端是没有发送数据还是已经掉线,所以需要主动确认对端是否存活。如果对端已经掉线还持续占用连接,影响是非常大的。

Keep_Alive可以设定时间间隔主动发送心跳包,如果长期无回应则释放连接。

BOOL Keep_Alive(SOCKET sock)
{
    int					SetSockRet = 0;
    int					WSActl = 0;
    BOOL				KeepALiveSet = TRUE;
    DWORD				IBRet;

    struct tcp_keepalive alive_in;     
    struct tcp_keepalive alive_out;
    alive_in.onoff = 1;                //0为开启,1为关闭
    alive_in.keepalivetime = 30000;    //多长时间(ms)没有数据传输就开始发送心跳包
    alive_in.keepaliveinterval = 2000; //每隔多长时间(ms)send一个心跳包,发5次(系统值)

    SetSockRet = setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (char*)&KeepALiveSet, sizeof(KeepALiveSet));                 //设置KEEPALIVE,默认为2小时没有数据传输开始检测
    if (SetSockRet == SOCKET_ERROR)
    {
        printf("setsockopt failed\n");
        return FALSE;
    }
    WSActl = WSAIoctl(
        sock,               //参数1:Socket
        SIO_KEEPALIVE_VALS, //参数2:要执行的操作的控制代码
        &alive_in,          //参数3:指向输入缓冲区的指针
        sizeof(alive_in),   //参数4:输入缓冲区的大小(以字节为单位)
        &alive_out,         //参数5:指向输出缓冲区的指针
        sizeof(alive_out),  //参数6:输出缓冲区的大小(以字节为单位)
        &IBRet,             //参数7:指向实际输出字节数的指针
        NULL, 
        NULL
    );
    if (WSActl == SOCKET_ERROR)
    {
        printf("WSAIoctl failed\n");
        return FALSE;
    }
    return TRUE;
}

 

 

C语言Socket基本用法

要是用Socket实现基本的通信,主要分以下几个步骤。

1.初始化

函数原型:int WSAStartup( WORD wVersionRequired, LPWSADATA lpWSAData );

参数1:版本选择

参数2:设置一个指向WSADATA数据类型的指针

返回值为0时,表示该函数执行成功。

Startup = WSAStartup(MAKEWORD(2, 2), &wsadata);
if (Startup != 0)
{
    printf("Failed to WSAStartup !\n");
    break;
}

2.申请Socket

函数原型:int socket( int af, int type, int protocol);

参数1:选择地址族(如:AF_INET为ipv4,AF_INET6为ipv6)

参数2:选择套接字类型

参数3:选择协议

返回值:返回套接字描述符

SrvSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == SrvSocket)
{
    printf("Socket invoke failed !\n");
    break;
}

3.设置Socket地址及端口

需声明一个SOCKADDR_IN类型的结构体,具体示例如下

SOCKADDR_IN			LocalAddr;

LocalAddr.sin_family = AF_INET;		//地址族,如AF_INET,带标TCP/IP地址族
LocalAddr.sin_addr.S_un.S_addr = 0;	//存储IP地址
LocalAddr.sin_port = htons(26822);	//存储端口号(htons将整型变量从主机字节顺序转变成网络字节顺序)

memset(LocalAddr.sin_zero, 0x0, sizeof(LocalAddr.sin_zero));
//sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
//用来将sockaddr_in结构填充到与struct sockaddr同样的长度,可以用memset()函数将其置为零。

4.绑定IP地址

函数原型:int bind( SOCKET s, const sockaddr *addr, int namelen );

参数1:套接字描述符

参数2:指向要分配给绑定套接字的本地地址的sockaddr结构的指针

参数3:名称参数所指向的值的长度(以字节为单位)

SrvBind = bind(SrvSocket, (SOCKADDR*)&LocalAddr, sizeof(SOCKADDR_IN));
if (0 != SrvBind)
{
    printf("Failed to bind !\n");
    printf("Bind Error Code:%u", WSAGetLastError());
    break;
}

5.监听Socket

函数原型:int listen( SOCKET s, int backlog );

参数1:套接字描述符

参数2:挂起连接队列的最大长度。如果将backlog设置为SOMAXCONN,则负责套接字s的基础服务提供商会将backlog设置为最大合理值。没有标准条款来获取实际积压值。

SrvListen = listen(SrvSocket, 5);

if (0 != SrvListen)
{
    printf("Failed to listen !\n");
    break;
}

6.接受链接

函数原型:SOCKET WSAAPI accept( SOCKET s, sockaddr *addr, int *addrlen );

参数1:标示一个已处于监听状态下的套接字

参数2:通信层已知的指向接收连接实体地址的缓冲区的可选指针。addr参数的确切格式由创建sockaddr结构的套接字时建立的地址族确定 。

参数3:指向整数的可选指针,该整数包含addr参数指向的结构的长度。

AddrLen = sizeof(Clientaddr);
AcceptSocket = accept(SrvSocket, (SOCKADDR*)&Clientaddr, &AddrLen);
if (INVALID_SOCKET == AcceptSocket)
{
    printf("Accept error !\n");
    break;
}