权限管理系统系列之WCF通信

目录

权限管理系统系列之序言     

        首先说下题外话,有些园友看了前一篇【权限管理系统系列之序言】博客加了QQ群(186841119),看了我写的权限管理系统的相关文档(主要是介绍已经开发的功能),给出了一些建议,感觉非常好,希望后续有更多的园友能再接再厉给出更多的指导意见,在平常的开发中个人会结合你们的建议做出适当修改和完善,促进共同学习和进步。关于源码共享的问题,可能会过段时间公布,不会现在公开源码,个人还在不断完善中,等完成差不多后会公开源码。

        客户端与服务器的通信在一个程序中会占住关键的作用,处理起来可能会有很多方式,比如说Remoting、Socket、WebServices、WCF等等都可以实现。本人这几种基本上都用过,Socket可能比较少些,一些聊天室的程序就会使用Socket,通过字节的形式接收数据;WebServices会WinCE开发中使用到,数据传输进行压缩,这样操作数据就比较方便,实时操作数据库;Remoting主要用在MIS系统的客户端与服务端通信,个人也说不出那种好;WCF也是我最近一两年才接触到的,公司现在使用的就是WCF通信的,个人感觉用WCF比较方便和简单,实用起来使用三个函数(一个函数是检测客户端与服务器端的心跳,一个是用于登录的、一个是公共的接口,基本上所有的客户端和服务端的通信都是用這个函数),這个函数可以搞定所有的客户端访问服务端的方法,所有的SQL在服务端执行,便于维护和日常的分工,不过在平常的开发中也不会分客户端和服务端的开发,基本上也是一个一个模块进行分工的。

WCF的配置(包括客户端和服务端)

客户端的配置文件:

 1 <?xml version="1.0"?>
 2 <configuration>
 3   <system.serviceModel>
 4     <bindings>
 5       <netTcpBinding>
 6         <binding name="TcpBinding_AppService" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:10:00" transactionFlow="false" transferMode="Buffered" transactionProtocol="OleTransactions" hostNameComparisonMode="StrongWildcard" listenBacklog="10" maxBufferPoolSize="10485760" maxBufferSize="10485760" maxConnections="10" maxReceivedMessageSize="10485760">
 7           <readerQuotas maxDepth="32" maxStringContentLength="10485760" maxArrayLength="10485760" maxBytesPerRead="10485760" maxNameTableCharCount="10485760"/>
 8           <reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false"/>
 9           <security mode="None">
10             <transport clientCredentialType="Windows" protectionLevel="EncryptAndSign"/>
11             <message clientCredentialType="Windows"/>
12           </security>
13         </binding>
14 
15         <binding name="TcpBinding_MessageService" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:10:00" transactionFlow="false" transferMode="Buffered" transactionProtocol="OleTransactions" hostNameComparisonMode="StrongWildcard" listenBacklog="10" maxBufferPoolSize="10485760" maxBufferSize="10485760" maxConnections="10" maxReceivedMessageSize="10485760">
16           <readerQuotas maxDepth="32" maxStringContentLength="10485760" maxArrayLength="10485760" maxBytesPerRead="10485760" maxNameTableCharCount="10485760"/>
17           <reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false"/>
18           <security mode="None">
19             <transport clientCredentialType="Windows" protectionLevel="EncryptAndSign"/>
20             <message clientCredentialType="Windows"/>
21           </security>
22         </binding>
23 
24       </netTcpBinding>
25     </bindings>
26     <client>
27       <endpoint address="net.tcp://localhost:9090/AppService" binding="netTcpBinding" bindingConfiguration="TcpBinding_AppService" contract="IAppService" name="TcpBinding_AppService"/>
28 
29       <endpoint address="net.tcp://localhost:7070/MessageService" binding="netTcpBinding" bindingConfiguration="TcpBinding_MessageService" contract="IMessageService" name="TcpBinding_MessageService"/>
30     </client>
31   </system.serviceModel>
32 </configuration>
View Code

服务端的配置文件:

 

 1 <?xml version="1.0"?>
 2 <configuration>
 3   <system.serviceModel>
 4     <services>
 5       <service behaviorConfiguration="Service.Behavior" name="Server.AppService">
 6         <endpoint address="AppService" binding="netTcpBinding" bindingConfiguration="AppServiceBinding" name="TcpBinding_AppService" contract="Server.IAppService" />
 7         <endpoint address="AppService/mex" binding="mexTcpBinding" contract="IMetadataExchange" />
 8         <host>
 9           <baseAddresses>
10             <add baseAddress="net.tcp://localhost:9090" />
11           </baseAddresses>
12         </host>
13       </service>
14       <service behaviorConfiguration="Service.Behavior" name="Server.MessageService">
15         <endpoint address="MessageService" binding="netTcpBinding" bindingConfiguration="MessageServiceBinding" name="TcpBinding_MessageService" contract="Server.IMessageService" />
16         <endpoint address="MessageService/mex" binding="mexTcpBinding" contract="IMetadataExchange" />
17         <host>
18           <baseAddresses>
19             <add baseAddress="net.tcp://localhost:7070" />
20           </baseAddresses>
21         </host>
22       </service>
23     </services>
24     <bindings>
25       <netTcpBinding>
26         <binding name="AppServiceBinding" maxBufferSize="10485760" maxReceivedMessageSize="10485760">
27           <readerQuotas maxDepth="32" maxStringContentLength="10485760"
28             maxArrayLength="10485760" maxBytesPerRead="10485760" maxNameTableCharCount="10485760" />
29           <reliableSession ordered="true" inactivityTimeout="00:10:00"
30             enabled="false" />
31           <security mode="None" />
32         </binding>
33         <binding name="MessageServiceBinding" maxBufferSize="10485760" maxReceivedMessageSize="10485760">
34           <readerQuotas maxDepth="32" maxStringContentLength="10485760"
35             maxArrayLength="10485760" maxBytesPerRead="10485760" maxNameTableCharCount="10485760" />
36           <reliableSession ordered="true" inactivityTimeout="00:10:00"
37             enabled="false" />
38           <security mode="None" />
39         </binding>
40       </netTcpBinding>
41     </bindings>
42     <behaviors>
43       <serviceBehaviors>
44         <behavior name="Service.Behavior">
45           <serviceMetadata />
46           <serviceDebug includeExceptionDetailInFaults="true" />
47           <!--会话最大数量(并发会话)-->
48           <serviceThrottling maxConcurrentSessions="100" />
49           <!--数据序列最大量-->
50           <dataContractSerializer maxItemsInObjectGraph="10485760" />
51         </behavior>
52         <behavior name="mexConfig">
53           <serviceDebug includeExceptionDetailInFaults="True" />
54           <serviceMetadata />
55         </behavior>
56       </serviceBehaviors>
57     </behaviors>
58   </system.serviceModel>
59 </configuration>
View Code

 

以上为客户端与服务端的配置文件,有两个配置,一个为基本通信所用,一个为双工通信所用。

 

介绍完配置文件后再介绍实现函数:

 

 1 [ServiceContract(Name = "IAppService", SessionMode = SessionMode.Allowed, Namespace = "http://tempuri.org/")]
 2     public interface IAppService
 3     {
 4         //心跳
 5         [OperationContract]
 6         string HeartBeat(string echo);
 7 
 8         //登录
 9         [OperationContract]
10         bool Login(string UserName, string Password);
11 
12         //统一的应用业务请求调用,用于会话控制和调用转发
13         [OperationContract]
14         Result AppCall(Request request);
15     }
16 
17     #region * 推送消息
18     [ServiceContract(CallbackContract = typeof(IPushClient))]
19     public interface IMessageService
20     {
21         [OperationContract]
22         void RegisterClient();
23     }
24 
25     public interface IPushClient
26     {
27         [OperationContract(IsOneWay = true)]
28         void SendMessage(string message);
29     }
30     #endregion

 

      分别有三个函数,一个是维持服务端与客户端的心跳,一个为登陆所用,一个为所有函数的接口,基本上所有的通信都是通过这个函数进行调用。最下面的为双工通信的定义,定义成回调函数的形式。

以上函数实现逻辑:

  1  //每个会话一个实例,同一个会话下的多线程并发
  2     [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)]
  3     public class AppService : IAppService
  4     {
  5         //记录是否登录
  6         bool IsLogined = false;
  7         Context context = new Context();
  8 
  9         private static readonly string _username = "Server";
 10         private static readonly string _password = "123456";
 11 
 12         //心跳请求,维持链路正常
 13         public string HeartBeat(string echo)
 14         {
 15             Log.Log.Debug("收到请求:" + echo);
 16             return "Re:" + echo;
 17         }
 18 
 19         //用于登录
 20         public bool Login(string UserName, string Password)
 21         {
 22             Log.Log.Debug("收到请求:" + UserName + ";" + Password);
 23             if (_username.Equals(Encrypt.DecryptDES(UserName, Const.EncryptKey)) && _password.Equals(Encrypt.DecryptDES(Password, Const.EncryptKey)))
 24             {
 25                 IsLogined = true;
 26                 context.UserName = UserName;
 27                 return IsLogined;
 28             }
 29             else
 30             {
 31                 IsLogined = false;
 32                 context.UserName = "";
 33                 return IsLogined;
 34             }
 35         }
 36 
 37         //统一的业务请求调用代理
 38         delegate Result ActionDelegate(string reqdata);
 39         //统一的应用业务请求调用,用于会话控制和调用转发
 40         public Result AppCall(Request request)
 41         {
 42             if (!string.IsNullOrEmpty(request.data))
 43             {
 44                 if (request.data.Contains(JSON.CompressionFlag))
 45                 {
 46                     Log.Log.Debug("收到请求:" + Compression.DecompressString(request.data.Replace(JSON.CompressionFlag, JSON.ReplaceFlag)));
 47                 }
 48                 else
 49                 {
 50                     Log.Log.Debug("收到请求:" + request.data);
 51                 }
 52             }
 53             if (!IsLogined)
 54             {
 55                 Result result = new Result() { success = false, errors = "用户未登录,请登录后再提交请求!" };
 56                 Log.Log.Info("Server.AppService.AppCall(Request request):" + JSON.Object2Json(result, false));
 57                 return result;
 58             }
 59 
 60             if (request.action == null || request.method == null)
 61             {
 62                 Result result = new Result() { success = false, errors = "请求数据格式不正确{request.action==null || request.method==null},请检查!" };
 63                 Log.Log.Error("Server.AppService.AppCall(Request request)出错:" + JSON.Object2Json(result, false));
 64                 return result;
 65 
 66             }
 67             try
 68             {
 69                 Type type = Type.GetType("Server.Action." + request.action);
 70                 object action = Activator.CreateInstance(type, new object[] { this.context });
 71                 ActionDelegate doAction = (ActionDelegate)Delegate.CreateDelegate(typeof(ActionDelegate), action, request.method);
 72                 Result result = doAction(request.data);
 73                 return result;
 74             }
 75             catch (Exception e)
 76             {
 77                 Log.Log.Error("Server.AppService.AppCall(Request request)异常:" + e.ToString());
 78                 return new Result() { success = false, errors = "执行业务请求错误!" };
 79             }
 80         }
 81     }
 82 
 83     #region * 推送消息
 84     [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
 85     public class MessageService : IMessageService, IDisposable
 86     {
 87         public static List<IPushClient> ClientCallbackList { get; set; }
 88         public MessageService()
 89         {
 90             ClientCallbackList = new List<IPushClient>();
 91         }
 92 
 93         public void RegisterClient()
 94         {
 95             var client = OperationContext.Current.GetCallbackChannel<IPushClient>();
 96             var id = OperationContext.Current.SessionId;
 97             //Console.WriteLine("{0}registered.", id);
 98             Log.Log.Info(string.Format("{0}registered.", id));
 99             OperationContext.Current.Channel.Closing+=new EventHandler(Channel_Closing);
100             ClientCallbackList.Add(client);
101         }
102 
103         private void Channel_Closing(object sender, EventArgs e)
104         {
105             lock (ClientCallbackList)
106             {
107                 ClientCallbackList.Remove((IPushClient)sender);
108             }
109         }
110 
111         public void Dispose()
112         {
113             ClientCallbackList.Clear();
114         }
115     }
116     #endregion
117 }
View Code

 

       通信接口通过以上实体,success为函数执行状态,msg为函数返回的信息,data为返回的数据(格式为json格式的),errors为返回的错误信息,sql为函数执行的sql,返回给前台界面。

WCF服务生成客户端的配置文件步骤:
a.打开vs命令行,用cd进入到exe文件目录。
b.svcutil .exe
c.svcutil *.wsdl *.xsd
完成以上即可。

客户调用CS文件:

  1 //------------------------------------------------------------------------------
  2 // <auto-generated>
  3 //     此代码由工具生成。
  4 //     运行时版本:4.0.30319.18444
  5 //
  6 //     对此文件的更改可能会导致不正确的行为,并且如果
  7 //     重新生成代码,这些更改将会丢失。
  8 // </auto-generated>
  9 //------------------------------------------------------------------------------
 10 
 11 namespace Server.Domain
 12 {
 13     using System.Runtime.Serialization;
 14 
 15 
 16     [System.Diagnostics.DebuggerStepThroughAttribute()]
 17     [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")]
 18     [System.Runtime.Serialization.DataContractAttribute(Name = "Request", Namespace = "http://schemas.datacontract.org/2004/07/Server.Domain")]
 19     public partial class Request : object, System.Runtime.Serialization.IExtensibleDataObject
 20     {
 21 
 22         private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
 23 
 24         private string actionField;
 25 
 26         private string dataField;
 27 
 28         private string methodField;
 29 
 30         public System.Runtime.Serialization.ExtensionDataObject ExtensionData
 31         {
 32             get
 33             {
 34                 return this.extensionDataField;
 35             }
 36             set
 37             {
 38                 this.extensionDataField = value;
 39             }
 40         }
 41 
 42         [System.Runtime.Serialization.DataMemberAttribute()]
 43         public string action
 44         {
 45             get
 46             {
 47                 return this.actionField;
 48             }
 49             set
 50             {
 51                 this.actionField = value;
 52             }
 53         }
 54 
 55         [System.Runtime.Serialization.DataMemberAttribute()]
 56         public string data
 57         {
 58             get
 59             {
 60                 return this.dataField;
 61             }
 62             set
 63             {
 64                 this.dataField = value;
 65             }
 66         }
 67 
 68         [System.Runtime.Serialization.DataMemberAttribute()]
 69         public string method
 70         {
 71             get
 72             {
 73                 return this.methodField;
 74             }
 75             set
 76             {
 77                 this.methodField = value;
 78             }
 79         }
 80     }
 81 
 82     [System.Diagnostics.DebuggerStepThroughAttribute()]
 83     [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")]
 84     [System.Runtime.Serialization.DataContractAttribute(Name = "Result", Namespace = "http://schemas.datacontract.org/2004/07/Server.Domain")]
 85     public partial class Result : object, System.Runtime.Serialization.IExtensibleDataObject
 86     {
 87 
 88         private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
 89 
 90         private string dataField;
 91 
 92         private string errorsField;
 93 
 94         private string msgField;
 95 
 96         private string sqlField;
 97 
 98         private bool successField;
 99 
100         public System.Runtime.Serialization.ExtensionDataObject ExtensionData
101         {
102             get
103             {
104                 return this.extensionDataField;
105             }
106             set
107             {
108                 this.extensionDataField = value;
109             }
110         }
111 
112         [System.Runtime.Serialization.DataMemberAttribute()]
113         public string data
114         {
115             get
116             {
117                 return this.dataField;
118             }
119             set
120             {
121                 this.dataField = value;
122             }
123         }
124 
125         [System.Runtime.Serialization.DataMemberAttribute()]
126         public string errors
127         {
128             get
129             {
130                 return this.errorsField;
131             }
132             set
133             {
134                 this.errorsField = value;
135             }
136         }
137 
138         [System.Runtime.Serialization.DataMemberAttribute()]
139         public string msg
140         {
141             get
142             {
143                 return this.msgField;
144             }
145             set
146             {
147                 this.msgField = value;
148             }
149         }
150 
151         [System.Runtime.Serialization.DataMemberAttribute()]
152         public string sql
153         {
154             get
155             {
156                 return this.sqlField;
157             }
158             set
159             {
160                 this.sqlField = value;
161             }
162         }
163 
164         [System.Runtime.Serialization.DataMemberAttribute()]
165         public bool success
166         {
167             get
168             {
169                 return this.successField;
170             }
171             set
172             {
173                 this.successField = value;
174             }
175         }
176     }
177 }
178 
179 
180 [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
181 [System.ServiceModel.ServiceContractAttribute(ConfigurationName = "IAppService")]
182 public interface IAppService
183 {
184 
185     [System.ServiceModel.OperationContractAttribute(Action = "http://tempuri.org/IAppService/HeartBeat", ReplyAction = "http://tempuri.org/IAppService/HeartBeatResponse")]
186     string HeartBeat(string echo);
187 
188     [System.ServiceModel.OperationContractAttribute(Action = "http://tempuri.org/IAppService/Login", ReplyAction = "http://tempuri.org/IAppService/LoginResponse")]
189     bool Login(string UserName, string Password);
190 
191     [System.ServiceModel.OperationContractAttribute(Action = "http://tempuri.org/IAppService/AppCall", ReplyAction = "http://tempuri.org/IAppService/AppCallResponse")]
192     Server.Domain.Result AppCall(Server.Domain.Request request);
193 }
194 
195 [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
196 public interface IAppServiceChannel : IAppService, System.ServiceModel.IClientChannel
197 {
198 }
199 
200 [System.Diagnostics.DebuggerStepThroughAttribute()]
201 [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
202 public partial class AppServiceClient : System.ServiceModel.ClientBase<IAppService>, IAppService
203 {
204 
205     public AppServiceClient()
206     {
207     }
208 
209     public AppServiceClient(string endpointConfigurationName) :
210         base(endpointConfigurationName)
211     {
212     }
213 
214     public AppServiceClient(string endpointConfigurationName, string remoteAddress) :
215         base(endpointConfigurationName, remoteAddress)
216     {
217     }
218 
219     public AppServiceClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) :
220         base(endpointConfigurationName, remoteAddress)
221     {
222     }
223 
224     public AppServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
225         base(binding, remoteAddress)
226     {
227     }
228 
229     public string HeartBeat(string echo)
230     {
231         return base.Channel.HeartBeat(echo);
232     }
233 
234     public bool Login(string UserName, string Password)
235     {
236         return base.Channel.Login(UserName, Password);
237     }
238 
239     public Server.Domain.Result AppCall(Server.Domain.Request request)
240     {
241         return base.Channel.AppCall(request);
242     }
243 }
244 
245 [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
246 [System.ServiceModel.ServiceContractAttribute(ConfigurationName = "IMessageService", CallbackContract = typeof(IMessageServiceCallback))]
247 public interface IMessageService
248 {
249 
250     [System.ServiceModel.OperationContractAttribute(Action = "http://tempuri.org/IMessageService/RegisterClient", ReplyAction = "http://tempuri.org/IMessageService/RegisterClientResponse")]
251     void RegisterClient();
252 }
253 
254 [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
255 public interface IMessageServiceCallback
256 {
257 
258     [System.ServiceModel.OperationContractAttribute(IsOneWay = true, Action = "http://tempuri.org/IMessageService/SendMessage")]
259     void SendMessage(string message);
260 }
261 
262 [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
263 public interface IMessageServiceChannel : IMessageService, System.ServiceModel.IClientChannel
264 {
265 }
266 
267 [System.Diagnostics.DebuggerStepThroughAttribute()]
268 [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
269 public partial class MessageServiceClient : System.ServiceModel.DuplexClientBase<IMessageService>, IMessageService
270 {
271 
272     public MessageServiceClient(System.ServiceModel.InstanceContext callbackInstance) :
273         base(callbackInstance)
274     {
275     }
276 
277     public MessageServiceClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName) :
278         base(callbackInstance, endpointConfigurationName)
279     {
280     }
281 
282     public MessageServiceClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress) :
283         base(callbackInstance, endpointConfigurationName, remoteAddress)
284     {
285     }
286 
287     public MessageServiceClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) :
288         base(callbackInstance, endpointConfigurationName, remoteAddress)
289     {
290     }
291 
292     public MessageServiceClient(System.ServiceModel.InstanceContext callbackInstance, System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
293         base(callbackInstance, binding, remoteAddress)
294     {
295     }
296 
297     public void RegisterClient()
298     {
299         base.Channel.RegisterClient();
300     }
301 }
View Code
 1        public static AppServiceClient AppService;
 2         public static ServiceHearBeat HeartBeat;
 3         /// <summary>
 4         /// 连接是否正常
 5         /// </summary>
 6         public static bool IsAlive
 7         {
 8             get { return HeartBeat.IsAlive; }
 9         }
10         /// <summary>
11         /// 是否已经弹出过提示
12         /// </summary>
13         public static bool HasNotice
14         {
15             get { return HeartBeat.HasNotice; }
16             set { HeartBeat.HasNotice = value; }
17         }
18 
19 
20          //连接到应用服务
21           if (!ConnectToAppServer())
22           {
23                 Comm.MessageBox.Info("连接服务端失败,请检查服务端是否已经启动。");
24                  return;
25            }
26 
27 //连接到应用服务
28         public static bool ConnectToAppServer()
29         {
30             try
31             {
32                 //如果服务存在,则尝试关闭链接
33                 if (AppService != null)
34                 {
35                     try
36                     {
37                         AppService.Close();
38                     }
39                     catch (Exception)
40                     {
41                     }
42                 }
43                 //创建新的服务对象,并进行连接和登录
44                 AppService = new AppServiceClient();
45                 AppService.Open();
46                 if (!AppClient.AppService.Login(Encrypt.EncryptDES("Server", Const.EncryptKey), Encrypt.EncryptDES("123456", Const.EncryptKey)))
47                 {
48                     return false;
49                 }
50                 return true;
51             }
52             catch (EndpointNotFoundException)
53             {
54                 Log.Error("连接服务端失败,服务端IP端口配置错误或者是服务端尚未启动");
55             }
56             catch (SocketException)
57             {
58                 Log.Error("连接服务端失败,服务端IP端口配置错误或者是服务端尚未启动");
59             }
60             catch (Exception e)
61             {
62                 Log.Error("连接服务端出错:" + e.ToString());
63             }
64             return false;
65 
66         }
View Code

以上即可完成对客户端连接服务端了,基本上完成以后步骤可以说完成了WCF的通信,实现了客户端连接服务端。

服务端打开的效果:

客户端打开的效果:

如对权限管理系统有兴趣可加QQ群:186841119,可参与相关话题讨论。相互学习交流,共同进步。

posted @ 2014-11-28 20:09  气宇轩昂_2017  阅读(2294)  评论(0编辑  收藏  举报