WCF - REST服务
WCF REST服务
一个基于REST的WEB服务操作请求只需要体现两点 一是资源的唯一标识 二是操作类型 资源的唯一标识通过URI来完成 而操作类型通过HTTP方法(GET/HEAD POST PUT DELETE)来表示 而如果服务采用基于SOAP 那么操作是通过<Action>来表示的 REST从资源的角度来观察整个网络 整个网络的资源由URI确定 而客户端的应用只需要通过URI来获取资源的表征 获得这些表征致使这些应用程序转变了其状态 随着不断获取资源的表征 客户端应用不断地在转变着其状态 这就是所谓的表征状态转移(Representational State Transfer)其实这里面的概念说直白些 就是通过URI的渠道 我们能得到我们想要的资源 资源的方式可以是XML、JSON或者是HTML 取决于读者是机器还是人 是消费web服务的客户软件还是web浏览器 当然也可以是任何其他的方式 REST通俗点来讲究是一种使用针对资源进行增删改查操作、使用HTTP协议来通讯的服务端-客户端交互的通信风格 如果两个通信的程序都是相同的架构(比如.Net Application) 则使用WCF SOAP会非常方便 但如果客户有使用浏览器调用服务、js调用服务 那么服务最好还是设计成Rest风格的 也就是说现阶段其实Rest的开放性、通用性都要优于SOAP
REST服务的体现
一个REST服务需要体现以下几点
URI
用于标识某一互联网资源名称的字符串 如一个提供获取所有员工列表的资源 可以定义为这样的URI来标识
http://www.cnblogs.com/CRMService/Employees
消息格式
请求消息或回复的消息格式可以是JSON XML YAML HTML 等
请求方法
资源所接受的HTTP请求方式 有PUT(增)DELETE(删)POST(改)GET(查)
一个简单的REST服务
下面来一个实例演示 实现一个基本的REST服务 创建一个Service.Interface的项目 添加对System.ServiceModel、System.ServiceModel.Web、System.Runtime.Serialization的引用 添加一个Employee类 如
using System.ServiceModel;
using System.Runtime.Serialization; namespace Service.Interface
{
[DataContract(Namespace = "http://www.cnblogs.com/")]
public class Employee
{
[DataMember]
public string ID { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public string Department { get; set; }
[DataMember]
public string Grade { get; set; } public override string ToString()
{
return string.Format("ID:{0.-5}姓名:{1,-5}级别:{2,-4}部门:{3}",ID,Name,Grade,Department);
}
}
}
再添加一个服务契约接口
using System.ServiceModel;
using System.ServiceModel.Web; namespace Service.Interface
{
[ServiceContract(Namespace = "http://www.cnblogs.com/")]
public interface IEmployees
{
[WebGet(UriTemplate="all")]
IEnumerable<Employee> GetEmployees(); [WebGet(UriTemplate="{id}")]
Employee GetByID(string id); [WebInvoke(UriTemplate="/",Method="POST")]
void AddEmployee(Employee employee); [WebInvoke(UriTemplate="{id}",Method="DELETE")]
void DeleteEmployee(string id); [WebInvoke(UriTemplate="/",Method="PUT")]
void UpdateEmployee(Employee employee);
}
}
创建Service控制台项目 添加对Service.Interface和System.ServiceModel.Web的引用 添加一个EmployeeService类实现IEmployees契约接口
using Service.Interface;
using System.ServiceModel.Web; namespace Service
{
public class EmployeeService:IEmployees
{
private static List<Employee> employees = new List<Employee>
{
new Employee{ ID="", Name="sam" , Grade="G6", Department="开发部"},
new Employee{ ID="", Name="korn" , Grade="G7", Department="人事部"}
}; //获取所有员工
public IEnumerable<Employee> GetEmployees()
{
return employees;
} //根据id查询某位员工
public Employee GetByID(string id)
{
var employee=employees.Where(n => n.ID == id).SingleOrDefault();
if (employee == null)
{
WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.NotFound;
}
return employee;
} //添加新员工
public void AddEmployee(Employee employee)
{
var TestEmployee = employees.Where(n => n.ID == employee.ID).SingleOrDefault();
if (TestEmployee == null)
{
employees.Add(employee);
}
else
{
WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.Conflict;
}
} //删除员工
public void DeleteEmployee(string id)
{
var employee=this.GetByID(id);
if (employee != null)
{
employees.Remove(employee);
}
} //更新员工
public void UpdateEmployee(Employee employee)
{
this.DeleteEmployee(employee.ID);
employees.Add(employee);
}
}
}
现在将EmployeeService服务寄宿在控制台进程中 先配置终结点 终结点绑定使用的是webHttpBinding 如下
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="Service.EmployeeService">
<endpoint address="http://127.0.0.1:8888/employees" binding="webHttpBinding" contract="Service.Interface.IEmployees"/>
</service>
</services>
</system.serviceModel>
</configuration>
寄宿使用WebServiceHost
using System.ServiceModel.Web; namespace Service
{
class Program
{
static void Main(string[] args)
{
using (WebServiceHost host = new WebServiceHost(typeof(EmployeeService)))
{
host.Open();
Console.Read();
}
}
}
}
因为服务时基于web的 服务中有两个服务操作的请求方式被设置为GET 它们是GetEmployees方法和GetByID方法 所以我们可以通过浏览器键入服务的终结点地址来调用服务 打开浏览器 输入终结点地址Get的终结点别名
http://127.0.0.1:8888/employees/all
我们调用了GetEmployees服务操作 服务端以xml返回了结果
接下来测试GetByID操作 结果如下
http://127.0.0.1:8888/employees/001
WebGet特性和WebInvoke特性
ns:System.ServiceModel.Web
WebGet特性应用在契约操作上 表示此操作接受的请求类型为HTTP GET请求 而WebInvoke特性应用在契约操作上 表示此操作接受的请求类型为HTTP的其它三种请求(除GET)请求 两个特性实现了IOperationBehavior接口 REST服务是基于WEB的 所以契约操作可以不应用OperationContract特性 只需根据需要选择使用WebGet或者WebInvoke特性 除了WebInvoke的Method属性 两个特性都具有如下属性
RequestFormat属性
设置请求消息的格式 值为WebMessageFormat枚举 可能的值如Json、Xml 默认值Xml
ResponseFormat属性
设置回复消息的格式 值为WebMessageFormat枚举 可能的值如Json、Xml 默认值Xml
UriTemplate属性
此属性用于设置当前操作与服务终结点需要组成的操作地址 UriTemplate是一个URI模板 它有两种类型的Segment参数 一种是静态参数 另一种是动态参数 看例子
[WebGet(UriTemplate="{id}")]
Employee GetByID(string id);
使用中括号括起来的参数 我们成为动态Segment参数 此例子中就是设了一个动态的Segment参数 假设服务的终结点地址为http://127.0.0.1:8888/employees 那么为契约操作应用WebGet或WebInvoke的UriTemplate为{id} 则客户端调用该操作时地址的格式类似这样 http://127.0.0.1:8888/employees/001 不使用中括号括起来的参数 称为静态Segment参数 静态的参数在请求的URI中必须提供一样的值 如果把例子改为
[WebGet(UriTemplate="sam")]
[Description("根据id查询某位员工")]
Employee GetByID(string id);
则客户端调用该操作时地址的格式必须是这样 http://127.0.0.1:8888/employees/sam 静态参数和动态参数可以组合使用 所以可以如下定义UriTemplate
[WebGet(UriTemplate = "employee'sID-{id}")]
[Description("根据id查询某位员工")]
Employee GetByID(string id);
对应的操作地址为:http://127.0.0.1:8888/employees/employee'sID-001 还可以为动态参数提供默认值 只需这样做{id=001}
BodyStyle属性
设置请求消息或回复消息的消息主体的风格 值为WebMessageBodyStyle枚举 可能的值如下
Bare
消息主体部分仅仅包含序列化后的内容 它BodyStyle的默认值
Wrapped
消息主体部分不但包含序列化后的内容 还会在内容外部设置一个封套 封套名称就是当前操作的名称+result
WrappedRequest
封套请求消息 但不封套回复消息
WrappedResponse
封套回复消息 但不封套请求消息
继续使用前面的简单的REST服务的例子 现在做一些测试来看看设置了WebGet或WebInvoke的BodyStyle属性后具有怎样的效果
[ServiceContract(Namespace = "http://www.cnblogs.com/")]
public interface IEmployees
{
[WebGet(UriTemplate="all",ResponseFormat=WebMessageFormat.Json,BodyStyle=WebMessageBodyStyle.Bare)]
IEnumerable<Employee> GetEmployees(); //其它操作略……
}
在浏览器键入终结点地址来调用该操作
http://127.0.0.1:8888/employees/all
服务返回的结果如下 我们将回复消息的格式设为了JSON 并指定了回复消息主体的风格为Bare 即只返回序列化后的内容 无封套
[
{"Department":"开发部","Grade":"G6","ID":"","Name":"sam"},
{"Department":"人事部","Grade":"G7","ID":"","Name":"korn"}
]
再看将消息主体风格设为Wrapped后的效果
[WebGet(UriTemplate="all",ResponseFormat=WebMessageFormat.Json,BodyStyle=WebMessageBodyStyle.Wrapped)]
IEnumerable<Employee> GetEmployees();
结果为
{
"GetEmployeesResult":
[
{"Department":"开发部","Grade":"G6","ID":"","Name":"sam"},
{"Department":"人事部","Grade":"G7","ID":"","Name":"korn"}
]
}
Description特性
ns:System.ComponentModel
此特性表示契约操作的描述信息 可以在契约操作上应用此特性 应用后通过开启REST服务帮助页面 那么调用服务的客户端就可以打开服务的帮助页面查看关于服务的所有操作的描述信息 如
using System.ComponentModel;
public interface IEmployees
{
[WebGet(UriTemplate="all",ResponseFormat=WebMessageFormat.Json,BodyStyle=WebMessageBodyStyle.Wrapped)]
[Description("获取所有员工")]
IEnumerable<Employee> GetEmployees();
7 }
WebHttpBehavior
此类表示REST服务的终结点行为之一 它的属性如下
helpEnabled属性
WCF4.0为REST服务提供了帮助页面的功能 此功能默认是关闭的 可以通过设置服务端的终结点行为WebHttp的helpEnabled为true来开启此功能
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior>
<webHttp helpEnabled="true"/>
</behavior>
</endpointBehaviors>
</behaviors>
<services>
<service name="Service.EmployeeService">
<endpoint address="http://127.0.0.1:8888/employees" binding="webHttpBinding" contract="Service.Interface.IEmployees"/>
</service>
</services>
</system.serviceModel>
</configuration>
我们将帮助页面开启并结合应用在契约操作上的Description特性 开启服务 在浏览器地址栏输入http://127.0.0.1:8888/employees/Help 打开的帮助页面如下所示
defaultBodyStyle属性
表示如果契约操作没有通过应用WebGet或WebInvoke的BodyStyle属性的值来指定某个操作在消息主体中的显示风格 那么就会使用此属性设置的默认风格 值为Bare、Wrapped、WrappedRequest、WrappedResponse
automaticFormatSelectionEnabled属性
布尔值 契约操作是否使用默认的消息格式
如果契约操作通过应用WebGet或WebInvoke的ResponseFormat属性指定了某个操作的回复消息的消息格式 则使用该格式
如果HTTP消息报头具有Accept报头 则自动根据该报头来决定回复消息的格式
如果HTTP消息报头具有Content-Type报头 则自动根据该报头来决定回复消息的格式
如果设置了WebHttp的defaultOutgoingResponseFormat属性 则决定回复消息使用该属性指定的格式
以上都没有显示设置 则使用默认的Xml消息格式
AspNetCacheProfile特性
ns:System.ServiceModel.Web
此特性表示输出缓存 ASP.NET输出缓存机制允许我们针对整个WEB页面或页面的某个部分最终呈现的HTML进行缓存 对于后续针对相同资源的请求 只需要直接将缓存的HTML呈现给客户端而无需再次使用服务端程序对请求进行处理 以达到提高服务端计算性能 节约带宽的目的 下面来演示如何在WCF REST服务上使用缓存机制
要建立的几个项目结构如图
Client控制台项目
Service类库项目
Service.Interface类库项目
WebService网站项目 (右击解决方案- 新建网站 - WCF服务 - 选择网站存储路径 并命名为WebService)
在Service.Interface中创建一个用于获取时间的服务契约
using System.ServiceModel;
using System.ServiceModel.Web; namespace Service.Interface
{
[ServiceContract(Namespace = "http://www.cnblogs.com/")]
public interface ITime
{
[WebGet(UriTemplate="/Time")]
[AspNetCacheProfile("default")]
DateTime GetTime();
}
}
契约操作GetTime方法应用了AspNetCacheProfile特性 表示寄宿在ASP.NET网站中的服务操作将使用在Web.config的outputCacheProfiles配置节中指定的缓存策略 接着在Service项目中实现契约接口ITime
using Service.Interface;
using System.ServiceModel.Activation; namespace Service
{
[AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed)]//兼容模式
public class TimeService:ITime
{
public DateTime GetTime()
{
return DateTime.Now;
}
}
}
AspNetCompatibilityRequirements特性ASP.NET兼容模式 属性RequirementsMode的值为Allowed 表示此服务操作将以ASP.NET兼容模式来运行此服务 现在我们来将WCF服务寄宿在IIS上 首先在WebService中删除App_Code中的文件 接着将svc文件命名为Service.svc 打开它 输入以下指令
<%@ ServiceHost Language="C#" Debug="true" Service="Service.TimeService" Factory="System.ServiceModel.Activation.WebServiceHostFactory" %>
然后打开Web.config 作如下配置
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="default" duration="" varyByParam="none"/>
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web> <system.serviceModel>
<services>
<service name="Service.TimeService">
<endpoint binding="webHttpBinding" contract="Service.Interface.ITime"/>
</service>
</services>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
</system.serviceModel>
caching/outputCacheSettings/outputCacheProfiles配置节添加了一个缓存策略 名为default 对应了契约操作GetTime所应用的特性[AspNetCacheProfile("default")] 该策略对操作的结果缓存了60秒 varyByParam属性表示忽略请求的URI中的查询字符
完成后打开IIS信息服务管理器 添加一个新网站 命名为myWeb 物理路径选择WebService 项目根目录 点击连接为 路径凭据选择特定用户 点击设置 输入登录计算机的用户名和密码后点击确定 IP设为127.0.0.1 端口默认80即可
最后启动该网站即可 这样WCF就寄宿在了IIS中 接下来在浏览器中输入http://127.0.0.1/Service.svc/Time 结果如下
<dateTime xmlns="http://schemas.microsoft.com/2003/10/Serialization/">2013-09-09T16:44:35.9183382+08:00</dateTime>
刷新该地址几次 得到的将是同样的结果 时间并无改变 正是因为该操作使用了缓存的缘故 我们也可以通过Client项目在控制台程序中调用该服务操作来测试缓存 控制台需要配置终结点 配置如下所示
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="webBehavior">
<webHttp/>
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint binding="webHttpBinding" address="http://127.0.0.1/Service.svc" name="timeservice" contract="Service.Interface.ITime" behaviorConfiguration="webBehavior"/>
</client>
</system.serviceModel>
因为创建的服务是REST的 所以终结点使用了webHttp行为
using System.ServiceModel;
using Service.Interface;
using System.Threading; using (ChannelFactory<ITime> factory = new ChannelFactory<ITime>("timeservice"))
{
var proxy = factory.CreateChannel();
//每隔2秒调用一次服务 可以看到10秒内重复调用服务操作的结果是一样的
for (var i = ; i < ; i++)
{
Console.WriteLine(proxy.GetTime());
Thread.Sleep();
}
Console.Read();
}
结果如下
缓存的条件获取
缓存存在这样的问题 客户端每次请求得到的都是缓存 那么当服务端数据做了修改 客户端却不能得到最新的数据 实际上HTTP对条件获取提供了原生的支持 内部的具体实现有两种 第一种是当服务端第一次接收到某个请求后 除了将回复消息返回给客户端 同时还会为HTTP回复消息添加一个ETag报头 它会将客户端请求的数据的哈希值添加到ETag报头中与回复消息一并发往客户端 客户端接受到回复消息 当再次请求时 客户端会为HTTP请求添加一个If-None-Match报头 此报头的值就是ETag报头的值 服务端再次接收到该请求并取出If-None-Match报头的值 将其与服务端数据的哈希值进行匹配 如果完全相等 则不再处理这个请求 而是将回复消息的值设为304(未做改变)然后返回 这样客户端请求的数据就会从缓存中读取 如果服务端取出的If-None-Match报头的值与数据的哈希值不等 则会将新数据的哈希值放进ETag报头同时将结果存进回复消息主体一并返回给客户端 第二种是基于Last Modified Time(最近修改时间)来实现的 服务端会记录下最近一次对某个操作的修改时间 将此时间作为ETag报头添加到HTTP回复消息中 客户端再次请求相同的操作时 它会将上次请求后服务端回复的ETag的时间添加到If-Modifiled-Since报头中 服务端接收到该请求 取出If-Modifiled-Since中的时间与服务端针对某个操作的最近一次修改时间进行对比 如果相等 则将回复消息的值设为304(未做改变)然后返回 如果不相等 则会将新的时间添加到ETag报头中同时将新的数据返回给客户端 可以将前面的例子做一个修改 来测试条件获取 先打开浏览器访问这个地址 http://127.0.0.1/Service.svc/Time 结果如下
1 <dateTime xmlns="http://schemas.microsoft.com/2003/10/Serialization/">2013-09-09T16:44:35.9183382+08:00</dateTime>
接着修改契约操作 将GetTime操作的返回值改为string类型
[ServiceContract(Namespace = "http://www.cnblogs.com/")]
public interface ITime
{
[WebGet(UriTemplate="/Time")]
[AspNetCacheProfile("default")]
string GetTime();
}
修改服务操作TimeService 将返回的时间格式化为中文日期
[AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed)]//兼容模式
public class TimeService:ITime
{
public string GetTime()
{
return DateTime.Now.ToString("yyyy年MM月dd HH时mm分ss秒");
}
}
生成解决方案一次 再次刷新浏览器 可以看到 当服务端数据改变后 缓存是最新的数据了
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">2013年09月09 18时47分11秒</string>
WebOperationContext
此类与基于SOAP消息的OperationContext有类似的作用 表示操作的上下文 使用该对象可以设置出栈消息的相关信息和接收入栈消息的信息 设置后 信息会自动附加到消息上 它通过Current静态属性返回一个WebOperationContext实例对象 实例属性如下
IncomingRequest属性
返回一个表示入栈请求消息的上下文对象
IncomingResponse属性
返回一个表示入栈回复消息的上下文对象
OutgoingRequest属性
返回一个表示出栈请求消息的上下文对象
OutgoingResponse属性
返回一个表示出栈回复消息的上下文对象
WCF - REST服务的更多相关文章
- 如何:加载分页结果(WCF 数据服务)
WCF 数据服务 允许数据服务限制单个响应源中返回的实体数.在此情况下,源中的最后一项包含指向下一页数据的链接.通过调用执行 DataServiceQuery 时返回的 QueryOperationR ...
- 微软开源 WCF 分布式服务框架,并入 .NET 基金会项目
微软北京时间2015.5.20 在其 .NET Foundation GitHub 开源项目页中开放了 WCF 分布式服务框架的代码.WCF突然之间成为一个热门话题,在各大网站上都有不同的报道:dot ...
- 一个通过JSONP跨域调用WCF REST服务的例子(以jQuery为例)
JSONP(JSON with Padding)可以看成是JSON的一种“使用模式”,用以解决“跨域访问”的问题,这篇简单的文章给出一个简单的例子用于模拟如何通过jQuery以JSONP的访问调用一个 ...
- WCF服务与WCF数据服务的区别
问: Hi, I am newbie to wcf programming and a little bit confused between WCF Service and WCF Data Se ...
- WCF 数据服务 4.5
.NET Framework 4.5 其他版本 WCF 数据服务(以前称为"ADO.NET Data Services")是 .NET Framework 的一个组件.可以使用此组 ...
- IIS上发布WCF发布服务,访问不到
1 环境是IIS7,发布WCF发布服务,访问不到. 一种原因站点自动生成“程序应用池”和站点的Framwork版本不一致. 解决的办法:新建一个“程序应用池”,然后站点指向这个新建的“程序应用池”
- 使用多种客户端消费WCF RestFul服务(四)——Jquery篇
Jquery篇 互联网开发中少不了各类前端开发框架,其中JQUERY就是最流行之一,本篇我们就采用JQUERY来消费WCF RestFul服务,其中用到JSON基础知识,如果有想了解的朋友,请访问:& ...
- 【WCF--初入江湖】06 WCF契约服务行为和异常处理
06 WCF契约服务行为和异常处理 一.WCF契约服务行为 [1] 服务行为可以修改和控制WCF服务的运行特性. 在实现了WCF服务契约后,可以修改服务的很多执行特性. 这些行为(或者特性)是通过配置 ...
- WCF:为 SharePoint 2010 Business Connectivity Services 构建 WCF Web 服务(第 1 部分,共 4 部分)
转:http://msdn.microsoft.com/zh-cn/library/gg318615.aspx 摘要:通过此系列文章(共四部分)了解如何在 Microsoft SharePoint F ...
- WCF SOA服务应用
WCF是微软官方推出的一个基于服务的整合框架,它整合了以前的Web Service.MSMQ.Remoting等通信技术,通过灵活的配置,让服务编程更加容易.可扩展.这篇文章主要目的就是带领大家从开发 ...
随机推荐
- python is == 的区别
要点: is 判断是否是同一个对象.是通过id来判断的 == 是通过值来判断的 为了提高内存利用率对一些简单的对象,如一些数值较小的int对象,python采用重用对象内存的方法 例如指向 ...
- jQuery获取屏幕的宽度
Javascript: 网页可见区域宽: document.body.clientWidth网页可见区域高: document.body.clientHeight网页可见区域宽: document.b ...
- uva 1453 - Squares
旋转卡壳算法: 直接在这个上面粘的模板 主要用途:用于求凸包的直径.宽度,两个不相交凸包间的最大距离和最小距离··· 这题就是求凸包的直径 #include <cstdio> #inclu ...
- 从 IT 中断中学到的最佳监控实践
每个运维监控工具,一般要追踪数十万个内部性能指标.学会对哪些事件进行告警以及监控确实需要花费想当长的一段时间.因为,并非所有的指标等级都是一致.因此我们需要摸索出一套简单的方法,便于管理所有指标,而且 ...
- Ansj分词双数组Trie树实现与arrays.dic词典格式
http://www.hankcs.com/nlp/ansj-word-pairs-array-tire-tree-achieved-with-arrays-dic-dictionary-format ...
- 转:Nginx 日志文件切割
http://www.cnblogs.com/benio/archive/2010/10/13/1849935.html 偶然发现access.log有21G大,所以将其切割. Nginx 是一个非常 ...
- android中handler中 obtainmessge与New message区别
obtainmessage()是从消息池中拿来一个msg 不需要另开辟空间new new需要重新申请,效率低,obtianmessage可以循环利用: //use Handler.obtainMess ...
- 深入详解SQL中的Null
深入详解SQL中的Null NULL 在计算机和编程世界中表示的是未知,不确定.虽然中文翻译为 “空”, 但此空(null)非彼空(empty). Null表示的是一种未知状态,未来状态,比如小明兜里 ...
- Flash加载网页内容
import flash.net.URLLoader; var m_loader:URLLoader = new URLLoader(); m_loader.addEventListener(Even ...
- android学习之BUG——The connection to adb is down, and a severe error has occured.
开始--运行--cmd 进入命令提示符 输入netstat -ano 即可看到所有连接的PID 之后在任务管理器中找到这个PID所对应的程序如果任务管理器中没有PID这一项,可以在任务管理器中选&qu ...