分享
 
 
 

Win32位程序设计初步之服务

王朝vc·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

每个操作系统都需要有在后台执行任务的方法,无论是谁正在使用这部机器,这些任务都可以继续运行,后台任务可以处理各种重要的服务,包括系统的或者用户的。例如,一个信使服务可以监控网络,并且在接收到另一台机子的信息时,可以显示一个对话框。一个发送和接收传真的应用需要在启动的时候运行,并且不断地监控负责传真的modem,看有没有传真进来。一个家庭的或者办公室的安全程序,用来控制一件检测设备时,它需要不时地查询传感器,并且在适当的时候响应它。所有这些任务都需要CPU时间来执行它们,不过由于它们需要的CPU时间很少,因此可以放在后台而不影响用户使用系统。

在MS-DOS中,后台的任务是通过TSR(Terminate and Stay Resident)程序来处理的。这些程序经由autoexec.bat文件开始。在UNIX中,后台任务是通过Daemons来处理的。在每次启动UNIX的过程中,你都可以看到操作系统启动一些任务,例如定时的程序(Cron)和Finger的daemons,然后才可以让首个用户登录。在Windows NT中,后台的任务被称为服务。服务可在每次NT启动的时候运行,并且不管是谁登陆,都会一直运行下去。

Windows NT的服务都是通过一般的可执行程序实现的,不同的是,它遵循内部的一个特定协议来设计,以便它们能够与服务控制管理器(SCM,Service Control Manager)进行正确的交互。在这篇文章中,你将学习到如何在Windows NT中创建和安装简单的Win32服务。一旦你懂得了这个简单的服务,你要建立自己的服务也不难了,因为所有的服务,不论是如何地复杂,都必须包含有同样基本的SCM接口代码。只要符合SCM的要求,其实为服务设计的可执行文件和一般的程序并没有多少的区别。

无论是对于编程者或者系统管理员,了解NT的服务如何工作都是很重要的。编程者就不必说了,因为他们要创建自己的服务,而对于系统管理员,也是同样重要的。因为后台的任务可以是很危险的。MS-DOS和Macintosh系统都是一个病毒的温床,因为它们在安全性方面先天不足,它们都可以允许任何人或者程序在任何时间创建后台的任务。Windows NT和UNIX系统是较安全的,因为只有系统管理员才可以为系统增加后台的任务,不过,如果系统管理员加入了一个破坏性的后台程序,就它就可以为所欲为了。因此系统管理员要了解Windows NT服务的技巧和权限设置,就可以避免加入有潜在危险的后台任务。

基本的概念

服务有两种不同的形式。驱动器服务使用驱动器协议,让NT可以与特定的硬件进行通信。另一个是Win32服务,通过一般的Win32 API来实现后台任务。这篇文章的重点是谈Win32服务,因为它们更为常见,而且创建起来也很容易。任何的NT编程者通过使用一般的NT SDK(或者Visual C++),并且可以用管理员的身份访问一台NT机器,都可以实现和安装自己的Win32服务。如果你想创建一些在Windows NT启动时就运行的程序,并且要求它会在系统中一直运行,你就要使用Win32的服务。

在NT中,服务通过控制面板进行管理。在控制面板中,你会发现有一个服务的图标,打开它你会看到所有Win32服务的清单。在那里你可以开始、停止、暂停和继续某个服务。你按下其中的启动按钮后,就会出现一个对话框,你可以修改启动操作以及服务使用的默认帐号。一个服务可以在系统启动的时候自动运行,也可以被完全禁止。或者设置为手动执行。在手动的时候,用户还可以设置启动的参数。要对服务中的项目作修改的话,你需要以一个管理员或者超级用户的身份登录。

Windows NT自带有一些预装的任务,用来处理诸如网络信使服务的操作或者使用“at”命令定时执行的操作,以及分布的RPC命名。在你创建自己的服务时,你必须执行一个独立的安装步骤,以将服务的信息插入到服务管理工具的列表中,这些信息包括有新服务的名字、执行文件的名字和启动的类型等,都会写入到注册表中,这样在机器下次启动的时候,SCM就会得到新服务的相关信息。

创建一个新的服务

执行服务的程序也是一个EXE文件,不过它必须符合某些特定的规范,以便可以与SCM进行正确的交互。微软很细致地设计了函数调用的流程,你必须遵循这些流程,否则你的服务就不能工作。具体的规定如下所列。你可以在Win32编程者的参考指南中找到以下涉及的函数,这些资料在SDK的Win32在线帮助或者Visual C++都有:

.服务的代码必须要有一个一般的main或者WinMain函数。这个函数应该会马上调用StartServiceCrtlDispatcher函数。通过调用这个函数,你可以让SCM得到ServiceMain函数的指针,这样在SCM要启动该服务时,就可以调用它

.在SCM要启动服务的时候,就会调用ServiceMain函数。例如,如果管理员在服务管理器中按下启动的按钮,SCM就会在一个独立的线程中执行ServiceMain函数。ServiceMain应该调用RegisterServiceCtrlHandler函数,这样可以注册一个Handler函数,以便SCM对服务进行控制。Handler函数的名字可以是任意的,不过它会在Handler下的文档中列出来。RegisterServiceCtrlHandler函数会返回一个句柄,在服务需要发送状态信息给SCM时,可以通过该句柄进行。

.ServiceMain函数也必须启动做该服务实际工作的线程。在服务停止前,ServiceMain函数是不应该有返回的。当它返回的时候,服务已经停止了。

.Handler函数包含了一个switch语句,用来分析由SCM传送过来的请求。默认的情况,SCM可以发送以下任何的的控制常数:

SERVICE_CONTROL_STOP - 要服务停止

SERVICE_CONTROL_PAUSE - 要服务暂停

SERVICE_CONTROL_CONTINUE - 要服务继续

SERVICE_CONTROL_INTERROGATE - 要服务马上报告它的状态

SERVICE_CONTROL_SHUTDOWN - 告诉服务即将关机

也可以创建自定义的常数(值在128到255之间),并且通过SCM发送给服务。

如果你创建的EXE包括有以上提到的main、ServiceMain和Handler函数,以及执行服务自身任务的线程函数,那么你的服务[/url][url=http://dev.21tx.com/java/j2me/code/]程序设计就完成了。以下的图总结了这些不同的函数和SCM之间的交互:

****************图一***********************

在本文的最后还有几段程序的列表,其中列表一为我们展示了一个可能是最简单的服务。该服务只发出"嘟"的响声。默认的状态下,它每两秒响一次。你可以通过启动的参数来修改发声的间隔。这个服务挺完整,它可以正确响应SCM传来的每个控制信号。因此,这个程序可作为你创建自己服务的一个很好的模板。

main函数通过调用StartServiceCtrlDispatcher来注册ServiceMain函数。注册的操作使用了一个SERVICE_TABLE_ENTRY结构的数组。在这个例子中,该程序只包含了一个服务,因此在表中只会有一个项目。不过,对于一个EXE文件,可以创建几个任务,这样在表中就会有几项,以识别不同的ServiceMain函数。在调用StartServiceCtrlDispatcher之前,可在main函数中放入初始化的代码,不过这些代码必须在少于30秒内完成,否则,SCM会认为某些地方出错而终止服务。

在自动或者手动启动服务时,将会调用ServiceMain函数。ServiceMain函数将包含有以下的步骤:

1.它马上调用RegisterServiceCtrlHandler来注册Handler函数,作为SCM控制该服务的Handler函数

2.然后它将调用SendStatusToSCM函数,将当前的进程通报给SCM。第四个参数是一个“click count”值,在程序每次更新状态时,它的值就会增加。SCM和其它的程序可以根据click count的值来知道初始化期间进行的处理。最后的参数是"wait hint",是用来告诉SCM在click count下次更新前,它需要等待的时间(以毫秒计算)。

3.ServiceMain接着会创建一个事情,该事件在函数的底部使用,可让它一直运行,直到SCM发出一个STOP的请求。

4.接着,ServiceMain检查启动的参数。参数可在用户手动启动服务时,通过服务管理工具中的启动参数传送过来。这些参数以一个argv形式的数组进入ServiceMain函数。

5.如果你的服务需要处理其它初始化的任务,它们应该放在这一步,在调用InitService之前。

6.ServiceMain函数接着调用InitService函数,这将启动线程并做服务的真正工作。如果该调用成功,SverviceMain将会通知SCM服务已经成功启动。

7.ServiceMain将调用WaitForSingleObject,用来等待terminateEvent事件对象被设置。这个对象通过Handler函数设置,一旦它被设置,ServiceMain将调用终止的函数来做清除的工作,然后就返回并停止服务。

你从上面可以看到,在这个函数中并没有多少灵活的地方。除了第5步外,你必须按步执行以上提到的任务,否则服务将不可以正确启动。

终止函数清除所有打开的句柄,并且发送一个状态的信息给SCM,告诉它服务现已停止。

在SCM要暂停、继续、询问或者停止服务时,它就会调用Handler函数。要停止服务,Handler设置terminateEvent,这样做会导致ServiceMain(它会以一个独立线程的形式执行)终止并且返回。一旦ServiceMain返回,服务就停止了。

SendStatusToSCM 函数负责发送服务的当前状态给SCM。

在需要启动服务线程时,ServiceMain就会调用InitService函数。该函数调用CreateThread来为服务创建一个新的线程。

ServiceThread函数包含有该服务真正要做的工作。在这个例子中,该线程包含有一个无限的循环,以一个预定义的时间间隔发出响声。在你创建自己的服务时,你可以在该线程中放入任何的代码,调用Win32函数或者你自己的函数。

安装和移除服务

为了使用上面提到的发响声服务,你必须安装它。安装可让SCM知道这个服务,并且让SCM将它加入到控制面板中的服务列表中。表单二的代码展示了如何安装一个服务。

表单2先通过OpenSCManager函数打开一个到SCM的连接。在OpenSCManager的调用中,你必须指定你要做的东西,以便SCM可以验证这个行为。如果你登录的帐号没有足够的权限,该调用将返回NULL。

CreateService的调用是真正用来装入新服务的。它使用OpenSCManager返回至SCM的指针、名字、标签和命令行中的EXE文件,还有一些标准的参数值。使用SERVICE_WIN32_OWN_PROCESS表明该服务的EXE文件只包含有一个服务,而SERVICE_DEMAND_START则表明该服务是手动而不是自动启动的。使用命令行安装程序的一个典型格式如下:

install BeepService "Beeper" c:\winnt\beep.exe

第一个参数是SCM内部使用的服务名字。这个名字也用在以后移除服务。第二个参数是一个标识符,即服务管理中显示的名字。第三个参数指出该服务的执行文件的路径。在你安装服务后,可在控制面板的服务管理中启动它。如果有错,可在Win32 API的在线文档中查出错误代码的含义。

要移除服务,你要按列表3的步骤进行。它首先打开一个到SCM的连接,然后使用OpenService函数打开一个与服务的连接。列表3中接着查询服务来看它是否现已停止了。如果不是的话,就会停止它。DeleteService 用来从控制面板中移除服务,移除操作的典型格式如下:

remove BeepService

需要的话,你也可以在马上重新安装服务。

结论

服务对于Windows NT是很重要的一部分,因为它可让你对操作系统进行扩展。使用列表1的代码作为一个模板,你将会发现要建立一个自己的新服务是非常简单的。

列表1

实现可能是最简单NT服务的代码

//***************************************************************

// From the book "Win32 System Services: The Heart of Windows NT"

// by Marshall Brain

// Published by Prentice Hall

file://

// This code implements the simplest possible service.

// It beeps every 2 seconds, or at a user specified interval.

file://***************************************************************

// beepserv.cpp

#include

#include

#include

#include

#define DEFAULT_BEEP_DELAY 2000

// Global variables

// The name of the service

char *SERVICE_NAME = "BeepService";

// Event used to hold ServiceMain from completing

HANDLE terminateEvent = NULL;

// Handle used to communicate status info with

// the SCM. Created by RegisterServiceCtrlHandler

SERVICE_STATUS_HANDLE serviceStatusHandle;

// The beep interval in ms.

int beepDelay = DEFAULT_BEEP_DELAY;

// Flags holding current state of service

BOOL pauseService = FALSE;

BOOL runningService = FALSE;

// Thread for the actual work

HANDLE threadHandle = 0;

void ErrorHandler(char *s, DWORD err)

{

cout << s << endl;

cout << "Error number: " << err << endl;

ExitProcess(err);

}

// This function consolidates the activities of

// updating the service status with

// SetServiceStatus

BOOL SendStatusToSCM (DWORD dwCurrentState,

DWORD dwWin32ExitCode,

DWORD dwServiceSpecificExitCode,

DWORD dwCheckPoint,

DWORD dwWaitHint)

{

BOOL success;

SERVICE_STATUS serviceStatus;

// Fill in all of the SERVICE_STATUS fields

serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;

serviceStatus.dwCurrentState = dwCurrentState;

// If in the process of doing something, then accept

// no control events, else accept anything

if (dwCurrentState == SERVICE_START_PENDING)

serviceStatus.dwControlsAccepted = 0;

else

serviceStatus.dwControlsAccepted =

SERVICE_ACCEPT_STOP |

SERVICE_ACCEPT_PAUSE_CONTINUE |

SERVICE_ACCEPT_SHUTDOWN;

// if a specific exit code is defined, set up

// the win32 exit code properly

if (dwServiceSpecificExitCode == 0)

serviceStatus.dwWin32ExitCode = dwWin32ExitCode;

else

serviceStatus.dwWin32ExitCode =

ERROR_SERVICE_SPECIFIC_ERROR;

serviceStatus.dwServiceSpecificExitCode =

dwServiceSpecificExitCode;

serviceStatus.dwCheckPoint = dwCheckPoint;

serviceStatus.dwWaitHint = dwWaitHint;

// Pass the status record to the SCM

success = SetServiceStatus (serviceStatusHandle,

&serviceStatus);

return success;

}

DWORD ServiceThread(LPDWORD param)

{

while (1)

{

Beep(200,200);

Sleep(beepDelay);

}

return 0;

}

// Initializes the service by starting its thread

BOOL InitService()

{

DWORD id;

// Start the service's thread

threadHandle = CreateThread(0, 0,

(LPTHREAD_START_ROUTINE) ServiceThread,

0, 0, &id);

if (threadHandle==0)

return FALSE;

else

{

runningService = TRUE;

return TRUE;

}

}

// Dispatches events received from the SCM

VOID Handler (DWORD controlCode)

{

DWORD currentState = 0;

BOOL success;

switch(controlCode)

{

// There is no START option because

// ServiceMain gets called on a start

// Stop the service

case SERVICE_CONTROL_STOP:

// Tell the SCM what's happening

success = SendStatusToSCM(SERVICE_STOP_PENDING,

NO_ERROR, 0, 1, 5000);

runningService=FALSE;

// Set the event that is holding ServiceMain

// so that ServiceMain can return

SetEvent(terminateEvent);

return;

// Pause the service

case SERVICE_CONTROL_PAUSE:

if (runningService && !pauseService)

{

// Tell the SCM what's happening

success = SendStatusToSCM(

SERVICE_PAUSE_PENDING,

NO_ERROR, 0, 1, 1000);

pauseService = TRUE;

SuspendThread(threadHandle);

currentState = SERVICE_PAUSED;

}

break;

// Resume from a pause

case SERVICE_CONTROL_CONTINUE:

if (runningService && pauseService)

{

// Tell the SCM what's happening

success = SendStatusToSCM(

SERVICE_CONTINUE_PENDING,

NO_ERROR, 0, 1, 1000);

pauseService=FALSE;

ResumeThread(threadHandle);

currentState = SERVICE_RUNNING;

}

break;

// Update current status

case SERVICE_CONTROL_INTERROGATE:

// it will fall to bottom and send status

break;

// Do nothing in a shutdown. Could do cleanup

// here but it must be very quick.

case SERVICE_CONTROL_SHUTDOWN:

return;

default:

break;

}

SendStatusToSCM(currentState, NO_ERROR, 0, 0, 0);

}

// Handle an error from ServiceMain by cleaning up

// and telling SCM that the service didn't start.

VOID terminate(DWORD error)

{

// if terminateEvent has been created, close it.

if (terminateEvent) CloseHandle(terminateEvent);

// Send a message to the scm to tell about stopage

if (serviceStatusHandle)

SendStatusToSCM(SERVICE_STOPPED, error,

0, 0, 0);

// If the thread has started, kill it off

if (threadHandle) CloseHandle(threadHandle);

// Do not need to close serviceStatusHandle

}

// ServiceMain is called when the SCM wants to

// start the service. When it returns, the service

// has stopped. It therefore waits on an event

// just before the end of the function, and

// that event gets set when it is time to stop.

// It also returns on any error because the

// service cannot start if there is an eror.

VOID ServiceMain(DWORD argc, LPTSTR *argv)

{

BOOL success;

// immediately call Registration function

serviceStatusHandle =

RegisterServiceCtrlHandler(

SERVICE_NAME, (LPHANDLER_FUNCTION)Handler);

if (!serviceStatusHandle) {terminate(GetLastError()); return;}

// Notify SCM of progress

success = SendStatusToSCM(SERVICE_START_PENDING,

NO_ERROR, 0, 1, 5000);

if (!success) {terminate(GetLastError()); return;}

// create the termination event

terminateEvent = CreateEvent (0, TRUE, FALSE, 0);

if (!terminateEvent) {terminate(GetLastError()); return;}

// Notify SCM of progress

success = SendStatusToSCM(SERVICE_START_PENDING,

NO_ERROR, 0, 2, 1000);

if (!success) {terminate(GetLastError()); return;}

// Check for startup params

if (argc == 2)

{

int temp = atoi(argv[1]);

if (temp < 1000)

beepDelay = DEFAULT_BEEP_DELAY;

else

beepDelay = temp;

}

// Notify SCM of progress

success = SendStatusToSCM(SERVICE_START_PENDING,

NO_ERROR, 0, 3, 5000);

if (!success) {terminate(GetLastError()); return;}

// Start the service itself

success = InitService();

if (!success) {terminate(GetLastError()); return;}

// The service is now running.

// Notify SCM of progress

success = SendStatusToSCM(SERVICE_RUNNING,

NO_ERROR, 0, 0, 0);

if (!success) {terminate(GetLastError()); return;}

// Wait for stop signal, and then terminate

WaitForSingleObject (terminateEvent, INFINITE);

terminate(0);

}

VOID main(VOID)

{

SERVICE_TABLE_ENTRY serviceTable[] =

{

{ SERVICE_NAME,

(LPSERVICE_MAIN_FUNCTION) ServiceMain},

{ NULL, NULL }

};

BOOL success;

// Register with the SCM

success =

StartServiceCtrlDispatcher(serviceTable);

if (!success)

ErrorHandler("In StartServiceCtrlDispatcher",

GetLastError());

}

列表2

安装NT服务的代码

file://***************************************************************

// From the book "Win32 System Services: The Heart of Windows NT"

// by Marshall Brain

// Published by Prentice Hall

file://

// This code installs a service.

file://***************************************************************

// install.cpp

#include

#include

void ErrorHandler(char *s, DWORD err)

{

cout << s << endl;

cout << "Error number: " << err << endl;

ExitProcess(err);

}

void main(int argc, char *argv[])

{

SC_HANDLE newService, scm;

if (argc != 4)

{

cout << "Usage:\n";

cout << " install service_name service_label executable\n";

cout << " service_name is the name used internally by the SCM\n";

cout << " service_label is the name that appears in the Services applet\n";

cout << " (for multiple words, put them in double quotes)\n";

cout << " executable is the full path to the EXE\n\n";

return;

}

// open a connection to the SCM

scm = OpenSCManager(0, 0, SC_MANAGER_CREATE_SERVICE);

if (!scm) ErrorHandler("In OpenScManager",

GetLastError());

// Install the new service

newService = CreateService(

scm, argv[1], // eg "beep_srv"

argv[2], // eg "Beep Service"

SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,

SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,

argv[3], // eg "c:\winnt\xxx.exe"

0, 0, 0, 0, 0);

if (!newService) ErrorHandler("In CreateService",

GetLastError());

else cout << "Service installed\n";

// clean up

CloseServiceHandle(newService);

CloseServiceHandle(scm);

}

列表3

移除NT服务的代码

file://***************************************************************

// From the book "Win32 System Services: The Heart of Windows NT"

// by Marshall Brain

// Published by Prentice Hall

file://

// This code removes a service from the Services applet in the

// Control Panel.

file://***************************************************************

// remove.cpp

#include

#include

void ErrorHandler(char *s, DWORD err)

{

cout << s << endl;

cout << "Error number: " << err << endl;

ExitProcess(err);

}

void main(int argc, char *argv[])

{

SC_HANDLE service, scm;

BOOL success;

SERVICE_STATUS status;

if (argc != 2)

{

cout << "Usage:\n remove service_name\n";

return;

}

// Open a connection to the SCM

scm = OpenSCManager(0, 0, SC_MANAGER_CREATE_SERVICE);

if (!scm) ErrorHandler("In OpenScManager",

GetLastError());

// Get the service's handle

service = OpenService(scm, argv[1],

SERVICE_ALL_ACCESS | DELETE);

if (!service) ErrorHandler("In OpenService",

GetLastError());

// Stop the service if necessary

success = QueryServiceStatus(service, &status);

if (!success) ErrorHandler("In QueryServiceStatus",

GetLastError());

if (status.dwCurrentState != SERVICE_STOPPED)

{

cout << "Stopping service...\n";

success = ControlService(service,

SERVICE_CONTROL_STOP, &status);

if (!success) ErrorHandler("In ControlService",

GetLastError());

}

// Remove the service

success = DeleteService(service);

if (success) cout << "Service removed\n";

else ErrorHandler("In DeleteService",

GetLastError());

// Clean up

CloseServiceHandle(service);

CloseServiceHandle(scm);

}

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有