SOUI新组件SIpcObject介绍

SIpcObject是一个基于Windows消息及共享内存的一个IPC(跨进程函数调用)的组件。

GITHUB上有很多IPC模块,我这里又造了一个轮子,不一定比现有的IPC更好,不过我觉得已经足够简单了。

老规矩,先看一下IPC模块的路径: 

 

再看一下IPC模块的接口:

  1 #pragma once
  2 
  3 #include <unknown/obj-ref-i.h>
  4 
  5 #define UM_CALL_FUN (WM_USER+1000)
  6 
  7 namespace SOUI
  8 {
  9     enum {
 10         FUN_ID_CONNECT = 100,
 11         FUN_ID_DISCONNECT,
 12         FUN_ID_START,
 13     };
 14 
 15     struct IShareBuffer {
 16         virtual void StartRead() = 0;
 17         virtual void StartWrite() = 0;
 18         virtual int Write(const void * data, UINT nLen) = 0;
 19         virtual int Read(void * buf, UINT nLen) = 0;
 20     };
 21 
 22 
 23     class SParamStream
 24     {
 25     public:
 26         SParamStream(IShareBuffer *pBuf, bool bOutStream) :m_pBuffer(pBuf)
 27         {
 28             m_pBuffer->StartRead();
 29             if (bOutStream) m_pBuffer->StartWrite();
 30         }
 31 
 32         IShareBuffer * GetBuffer() {
 33             return m_pBuffer;
 34         }
 35 
 36         template<typename T>
 37         SParamStream & operator<<(const T & data)
 38         {
 39             Write((const void*)&data, sizeof(data));
 40             return *this;
 41         }
 42 
 43 
 44         template<typename T>
 45         SParamStream & operator >> (T &data)
 46         {
 47             Read((void*)&data, sizeof(data));
 48             return *this;
 49         }
 50 
 51     public:
 52         int Write(const void * data, int nLen)
 53         {
 54             return m_pBuffer->Write(data, nLen);
 55         }
 56         int Read(void * buf, int nLen) 
 57         {
 58             return m_pBuffer->Read(buf, nLen);
 59         }
 60 
 61     protected:
 62         IShareBuffer * m_pBuffer;
 63     };
 64 
 65     struct  IFunParams
 66     {
 67         virtual UINT GetID() = 0;
 68         virtual void ToStream4Input(SParamStream &  ps) = 0;
 69         virtual void ToStream4Output(SParamStream &  ps) = 0;
 70         virtual void FromStream4Input(SParamStream &  ps) = 0;
 71         virtual void FromStream4Output(SParamStream &  ps) = 0;
 72     };
 73 
 74     struct IIpcConnection;
 75     struct IIpcHandle : IObjRef
 76     {
 77         virtual void SetIpcConnection(IIpcConnection *pConn) = 0;
 78 
 79         virtual IIpcConnection * GetIpcConnection() const = 0;
 80 
 81         virtual LRESULT OnMessage(ULONG_PTR idLocal, UINT uMsg, WPARAM wp, LPARAM lp, BOOL &bHandled) = 0;
 82 
 83         virtual HRESULT ConnectTo(ULONG_PTR idLocal, ULONG_PTR idRemote) = 0;
 84 
 85         virtual HRESULT Disconnect() = 0;
 86 
 87         virtual bool CallFun(IFunParams * pParam) const = 0;
 88 
 89         virtual ULONG_PTR GetLocalId() const = 0;
 90 
 91         virtual ULONG_PTR GetRemoteId() const = 0;
 92 
 93         virtual IShareBuffer * GetSendBuffer()  = 0;
 94 
 95         virtual IShareBuffer * GetRecvBuffer()  = 0;
 96 
 97         virtual BOOL InitShareBuf(ULONG_PTR idLocal, ULONG_PTR idRemote, UINT nBufSize, void* pSa) = 0;
 98     };
 99 
100     struct IIpcConnection : IObjRef
101     {
102         virtual IIpcHandle * GetIpcHandle() = 0;
103         virtual bool HandleFun(UINT uFunID, SParamStream & ps) = 0;
104         virtual void BuildShareBufferName(ULONG_PTR idLocal, ULONG_PTR idRemote, TCHAR szBuf[MAX_PATH]) const = 0;
105     };
106 
107     struct IIpcSvrCallback 
108     {
109         virtual void OnNewConnection(IIpcHandle * pIpcHandle, IIpcConnection ** ppConn) = 0;
110         virtual int GetBufSize() const = 0;
111         virtual void * GetSecurityAttr() const = 0;
112         virtual void ReleaseSecurityAttr(void* psa) const = 0;
113     };
114 
115     struct IIpcServer : IObjRef
116     {
117         virtual HRESULT Init(ULONG_PTR idSvr, IIpcSvrCallback * pCallback) =0;
118         virtual void CheckConnectivity() =0;
119         virtual LRESULT OnMessage(ULONG_PTR idLocal, UINT uMsg, WPARAM wp, LPARAM lp,BOOL &bHandled) =0;
120     };
121 
122     struct IIpcFactory : IObjRef
123     {
124         virtual HRESULT CreateIpcServer(IIpcServer ** ppServer) =0;
125         virtual HRESULT CreateIpcHandle(IIpcHandle ** ppHandle) =0;
126     };
127 
128 
129 }

和所有SOUI的组件一样,可以通过SOUI::IPC::SCreateInstance来创建IPC组件的IIpcFactory接口。

有了这个接口就可以用来创建IIpcServer和IIpcHandle这两个对象了。

IIpcServer是在IPC的服务端运行的接口,IIpcHandle是用来在服务端和客户端通讯的接口,在服务端,IIpcHandle由IIpcServer在客户端发起连接请求时自动创建,在客户端则直接使用IIpcFactory创建。

IIpcHandle是由SIpcObject实现的,在应用层中只需要直接使用。

应用层为了实现客户端与服务器的通讯还需要定义好协议。

SIpcObject的协议就是一个继承自IFunParam接口的定义的调用方法ID及方法参数。

下面看一下启程输入法使用IpcObject的协议定义。

  1 #pragma once
  2 #include <string>
  3 #include <sstream>
  4 #include "sinstar-i.h"
  5 #include "TextService-i.h"
  6 #include <interface/SIpcObj-i.h>
  7 #include <helper/sipcparamhelper.hpp>
  8 
  9 #define SINSTAR3_SERVER_HWND _T("sinstar3_server_wnd_{85B55CBC-7D48-4860-BA88-0BE4B073A94F}")
 10 #define SINSTAR3_SHARE_BUF_NAME_FMT _T("sistart3_share_buffer_8085395F-E2FA-4F96-8BD0-FE5D7412CD22_%08x_2_%08x")
 11 
 12 
 13 //////////////////////////////////////////////////////////////////
 14 namespace SOUI{
 15 
 16 template<>
 17 inline SParamStream & SParamStream::operator<<(const std::string & str)
 18 {
 19     int nSize = (int)str.size();
 20     GetBuffer()->Write((const BYTE*)&nSize, sizeof(int));
 21     GetBuffer()->Write((const BYTE*)str.c_str(), nSize);
 22     return *this;
 23 }
 24 template<>
 25 inline SParamStream & SParamStream::operator >> (std::string & str)
 26 {
 27     int nSize = 0;
 28     GetBuffer()->Read((BYTE*)&nSize, sizeof(int));
 29     char *pBuf = new char[nSize];
 30     GetBuffer()->Read((BYTE*)pBuf, nSize);
 31     str = std::string(pBuf, nSize);
 32     delete[]pBuf;
 33     return *this;
 34 }
 35 
 36 ////////////////////////////////////////////////////////////////////////
 37 template<>
 38 inline SParamStream & SParamStream::operator<<(const std::wstring & str)
 39 {
 40     int nSize = (int)str.size();
 41     GetBuffer()->Write((const BYTE*)&nSize, sizeof(int));
 42     GetBuffer()->Write((const BYTE*)str.c_str(), nSize*sizeof(wchar_t));
 43     return *this;
 44 }
 45 template<>
 46 inline SParamStream & SParamStream::operator >> (std::wstring & str)
 47 {
 48     int nSize = 0;
 49     GetBuffer()->Read((BYTE*)&nSize, sizeof(int));
 50     wchar_t *pBuf = new wchar_t[nSize];
 51     GetBuffer()->Read((BYTE*)pBuf, nSize*sizeof(wchar_t));
 52     str = std::wstring(pBuf, nSize);
 53     delete[]pBuf;
 54     return *this;
 55 }
 56 
 57 //////////////////////////////////////////////////////////////////////
 58 template<>
 59 inline SParamStream & SParamStream::operator<<(const POINT & pt)
 60 {
 61     GetBuffer()->Write((const BYTE*)&pt.x, sizeof(int));
 62     GetBuffer()->Write((const BYTE*)&pt.y, sizeof(int));
 63     return *this;
 64 }
 65 template<>
 66 inline SParamStream & SParamStream::operator >> (POINT & pt)
 67 {
 68     int tmp = 0;
 69     GetBuffer()->Read((BYTE*)&tmp, sizeof(int));
 70     pt.x = tmp;
 71     GetBuffer()->Read((BYTE*)&tmp, sizeof(int));
 72     pt.y = tmp;
 73     return *this;
 74 }
 75 
 76 }
 77 
 78 struct FunParams_Base : SOUI::IFunParams
 79 {
 80     virtual void ToStream4Input(SOUI::SParamStream &  ps) {}
 81     virtual void ToStream4Output(SOUI::SParamStream &  ps) {}
 82     virtual void FromStream4Input(SOUI::SParamStream &  ps) {}
 83     virtual void FromStream4Output(SOUI::SParamStream &  ps) {}
 84 };
 85 
 86 
 87 enum {
 88     ISinstar_Create = SOUI::FUN_ID_START,
 89     ISinstar_Destroy,
 90     ISinstar_OnImeSelect,
 91     ISinstar_OnCompositionStarted,
 92     ISinstar_OnCompositionChanged,
 93     ISinstar_OnCompositionTerminated,
 94     ISinstar_OnSetCaretPosition,
 95     ISinstar_OnSetFocusSegmentPosition,
 96     ISinstar_ProcessKeyStoke,
 97     ISinstar_TranslateKey,
 98     ISinstar_OnSetFocus,
 99     ISinstar_GetCompositionSegments,
100     ISinstar_GetCompositionSegmentEnd,
101     ISinstar_GetCompositionSegmentAttr,
102     ISinstar_OnOpenStatusChanged,
103     ISinstar_OnConversionModeChanged,
104     ISinstar_ShowHelp,
105     ISinstar_GetDefInputMode,
106 
107     ITextService_InputStringW = ISinstar_GetDefInputMode + 100,
108     ITextService_IsCompositing,
109     ITextService_StartComposition,
110     ITextService_ReplaceSelCompositionW,
111     ITextService_UpdateResultAndCompositionStringW,
112     ITextService_EndComposition,
113     ITextService_GetImeContext,
114     ITextService_ReleaseImeContext,
115     ITextService_SetConversionMode,
116     ITextService_GetConversionMode,
117     ITextService_SetOpenStatus,
118     ITextService_GetOpenStatus,
119     ITextService_GetActiveWnd,
120 };
121 
122 
123 struct Param_Create : FunParams_Base
124 {
125     bool   bDpiAware;
126     std::string strHostPath;
127     DWORD  dwVer;
128     FUNID(ISinstar_Create)
129         PARAMS3(Input, bDpiAware,strHostPath,dwVer)
130 };
131 
132 struct Param_Destroy : FunParams_Base
133 {
134     FUNID(ISinstar_Destroy)
135 };
136 
137 struct Param_OnImeSelect : FunParams_Base
138 {
139     BOOL bSelect;
140     FUNID(ISinstar_OnImeSelect)
141     PARAMS1(Input, bSelect)
142 };
143 
144 struct Param_OnCompositionStarted : FunParams_Base
145 {
146     FUNID(ISinstar_OnCompositionStarted)
147 };
148 
149 
150 struct Param_OnCompositionTerminated : FunParams_Base
151 {
152     bool bClearCtx;
153     FUNID(ISinstar_OnCompositionTerminated)
154     PARAMS1(Input, bClearCtx)
155 };
156 
157 struct Param_OnCompositionChanged : FunParams_Base
158 {
159     FUNID(ISinstar_OnCompositionChanged)
160 };
161 
162 struct Param_OnSetCaretPosition : FunParams_Base
163 {
164     POINT pt;
165     int nHei;
166     FUNID(ISinstar_OnSetCaretPosition)
167     PARAMS2(Input, pt,nHei)
168 };
169 
170 struct Param_OnSetFocusSegmentPosition : FunParams_Base
171 {
172     POINT pt; int nHei;
173     FUNID(ISinstar_OnSetFocusSegmentPosition)
174     PARAMS2(Input, pt, nHei)
175 };
176 
177 struct Param_ProcessKeyStoke : FunParams_Base {
178     UINT64 lpImeContext; UINT vkCode; DWORD lParam; BOOL bKeyDown; 
179     BYTE byKeyState[256];
180     BOOL bEaten;
181     FUNID(ISinstar_ProcessKeyStoke)
182     PARAMS5(Input, lpImeContext, vkCode, lParam, bKeyDown, byKeyState)
183     PARAMS1(Output,bEaten)
184 };
185 
186 struct Param_TranslateKey : FunParams_Base
187 {
188     UINT64 lpImeContext; UINT vkCode; UINT uScanCode; BOOL bKeyDown; 
189     BYTE byKeyState[256];
190     BOOL bEaten;
191     FUNID(ISinstar_TranslateKey)
192     PARAMS5(Input, lpImeContext, vkCode, uScanCode, bKeyDown, byKeyState)
193     PARAMS1(Output, bEaten)
194 };
195 
196 struct Param_OnSetFocus : FunParams_Base
197 {
198     BOOL bFocus;
199     FUNID(ISinstar_OnSetFocus)
200         PARAMS1(Input, bFocus)
201 };
202 
203 struct Param_GetCompositionSegments : FunParams_Base
204 {
205     int nSegs;
206     FUNID(ISinstar_GetCompositionSegments)
207         PARAMS1(Output, nSegs)
208 };
209 
210 struct Param_GetCompositionSegmentEnd : FunParams_Base
211 {
212     int iSeg;
213     int iEnd;
214     FUNID(ISinstar_GetCompositionSegmentEnd)
215         PARAMS1(Input,iSeg)
216         PARAMS1(Output,iEnd)
217 };
218 
219 struct Param_GetCompositionSegmentAttr : FunParams_Base
220 {
221     int iSeg;
222     int nAttr;
223     FUNID(ISinstar_GetCompositionSegmentAttr)
224         PARAMS1(Input, iSeg)
225         PARAMS1(Output, nAttr)
226 };
227 
228 struct Param_OnOpenStatusChanged : FunParams_Base
229 {
230     BOOL bOpen;
231     FUNID(ISinstar_OnOpenStatusChanged)
232         PARAMS1(Input, bOpen)
233 };
234 
235 struct Param_OnConversionModeChanged : FunParams_Base
236 {
237     EInputMethod uMode;
238     FUNID(ISinstar_OnConversionModeChanged)
239         PARAMS1(Input, uMode)
240 };
241 
242 struct Param_ShowHelp : FunParams_Base
243 {
244     FUNID(ISinstar_ShowHelp)
245 };
246 
247 struct Param_GetDefInputMode : FunParams_Base
248 {
249     EInputMethod uMode;
250     FUNID(ISinstar_GetDefInputMode)
251         PARAMS1(Output,uMode)
252 };
253 
254 
255 ////////////////////////////////////////////////////////////////////////////
256 struct Param_InputStringW : FunParams_Base
257 {
258     std::wstring buf;
259     BOOL bRet;
260     FUNID(ITextService_InputStringW)
261         PARAMS1(Input,buf)
262         PARAMS1(Output,bRet)
263 };
264 
265 struct Param_IsCompositing : FunParams_Base
266 {
267     BOOL bRet;
268     FUNID(ITextService_IsCompositing)
269         PARAMS1(Output,bRet)
270 };
271 
272 struct Param_StartComposition : FunParams_Base
273 {
274     UINT64 lpImeContext;
275     FUNID(ITextService_StartComposition)
276         PARAMS1(Input,lpImeContext)
277 };
278 
279 struct Param_ReplaceSelCompositionW : FunParams_Base
280 {
281     UINT64 lpImeContext; int nLeft; int nRight; std::wstring buf;
282     FUNID(ITextService_ReplaceSelCompositionW)
283         PARAMS4(Input,lpImeContext,nLeft,nRight,buf)
284 };
285 
286 struct Param_UpdateResultAndCompositionStringW : FunParams_Base
287 {
288     UINT64 lpImeContext; std::wstring resultStr; std::wstring compStr;
289     FUNID(ITextService_UpdateResultAndCompositionStringW)
290         PARAMS3(Input, lpImeContext, resultStr, compStr)
291 };
292 
293 struct Param_EndComposition : FunParams_Base
294 {
295     UINT64 lpImeContext;
296     FUNID(ITextService_EndComposition)
297         PARAMS1(Input,lpImeContext)
298 };
299 
300 struct Param_GetImeContext : FunParams_Base
301 {
302     UINT64 lpImeContext;
303     FUNID(ITextService_GetImeContext)
304         PARAMS1(Output,lpImeContext)
305 };
306 
307 struct Param_ReleaseImeContext : FunParams_Base
308 {
309     UINT64 lpImeContext;
310     BOOL bRet;
311     FUNID(ITextService_ReleaseImeContext)
312         PARAMS1(Input, lpImeContext)
313         PARAMS1(Output,bRet)
314 };
315 
316 struct Param_SetConversionMode : FunParams_Base
317 {
318     EInputMethod mode;
319     FUNID(ITextService_SetConversionMode)
320         PARAMS1(Input,mode)
321 };
322 
323 struct Param_GetConversionMode : FunParams_Base
324 {
325     EInputMethod mode;
326     FUNID(ITextService_GetConversionMode)
327         PARAMS1(Output, mode)
328 };
329 
330 struct Param_SetOpenStatus : FunParams_Base
331 {
332     UINT64 lpImeContext;
333     BOOL bOpen;
334     BOOL bRet;
335     FUNID(ITextService_SetOpenStatus)
336         PARAMS2(Input,lpImeContext,bOpen)
337         PARAMS1(Output,bRet)
338 };
339 
340 struct Param_GetOpenStatus : FunParams_Base
341 {
342     UINT64 lpImeContext;
343     BOOL bOpen;
344     FUNID(ITextService_GetOpenStatus)
345         PARAMS1(Input, lpImeContext)
346         PARAMS1(Output, bOpen)
347 };
348 
349 struct Param_GetActiveWnd : FunParams_Base
350 {
351     DWORD hActive;
352     FUNID(ITextService_GetActiveWnd)
353         PARAMS1(Output, hActive)
354 }


首先我们通过一组枚举值定义所有调用的函数ID。

然后实现一个继承自IFunParams的对象FunParams_Base,以实现接口中的缺省方法。

然后从FunParams_Base继承出每一个IPC调用需要的参数。

我们以256行的Param_InputStringW为例来说明如何定义方法参数。

struct Param_InputStringW : FunParams_Base
{
    std::wstring buf;
    BOOL bRet;
    FUNID(ITextService_InputStringW)
        PARAMS1(Input,buf)
        PARAMS1(Output,bRet)
};

这个IPC调用输入是一个wstring字符串,输出是一个BOOL类型返回值。

首先在对象中定义这两个成员变量。

定义好后通过宏FUNID来指定这个方法的函数调用ID。

再通过宏PARAM1(Input,buf)来指定这个方法的输入参数buf, 注意宏的第一个参数"input"。

第三步通过宏PARAM1(output,bRet)来定义这个方法的输出变量为bRet. PARAMX目前实现的X范围为1-5, 分别对应1-5个参数,如果在一次调用中有更多参数,可以参考PARAMX的实现多写几个宏就好了。

实际上这些宏就是为了组合IFunParams的几个虚方法。

这个对象在进行IPC调用的时候,先在请求端借助SParamStream对象序列化到共享内存中,SParamStream重载了输入"<<"及输出">>"操作符,默认操作是直接拷贝变量内存,这对于基本变量类型是适用的,但是对于string,wstring等对象就不适用了,对于那些不能通过简单的内存拷贝来传递的对象,我们需要像协议开头那样为这些类型的序列化做模板特化。对于比如POINT这样的对象也是可以直接通过内存拷贝就可以实现序列化的,因此这里对POINT的特化其实是多余的(最新的代码已经删除)。

协议定义好后,我们来看看如何进行IPC调用及响应IPC调用。

 1 class CClientConnection : public SOUI::TObjRefImpl<SOUI::IIpcConnection>
 2 {
 3 public:
 4     CClientConnection(ITextService * pTxtService);
 5 
 6 public:
 7     // 通过 IIpcConnection 继承
 8     virtual SOUI::IIpcHandle * GetIpcHandle() override;
 9     virtual void BuildShareBufferName(ULONG_PTR idLocal, ULONG_PTR idRemote, TCHAR szName[MAX_PATH]) const override;
10     bool CallFun(SOUI::IFunParams *params) const;
11 protected:
12     void OnInputStringW( Param_InputStringW &param);
13     void OnIsCompositing( Param_IsCompositing &param);
14     void OnStartComposition( Param_StartComposition &param);
15     void OnReplaceSelCompositionW( Param_ReplaceSelCompositionW &param);
16     void OnUpdateResultAndCompositionStringW( Param_UpdateResultAndCompositionStringW &param);
17     void OnEndComposition( Param_EndComposition &param);
18     void OnGetImeContext( Param_GetImeContext &param);
19     void OnReleaseImeContext( Param_ReleaseImeContext &param);
20     void OnSetConversionMode( Param_SetConversionMode &param);
21     void OnGetConversionMode( Param_GetConversionMode &param);
22     void OnSetOpenStatus( Param_SetOpenStatus &param);
23     void OnGetOpenStatus( Param_GetOpenStatus &param);
24     void OnGetActiveWnd( Param_GetActiveWnd &param);
25 
26     FUN_BEGIN
27         FUN_HANDLER(Param_InputStringW, OnInputStringW)
28         FUN_HANDLER(Param_IsCompositing, OnIsCompositing)
29         FUN_HANDLER(Param_StartComposition, OnStartComposition)
30         FUN_HANDLER(Param_ReplaceSelCompositionW, OnReplaceSelCompositionW)
31         FUN_HANDLER(Param_UpdateResultAndCompositionStringW, OnUpdateResultAndCompositionStringW)
32         FUN_HANDLER(Param_EndComposition, OnEndComposition)
33         FUN_HANDLER(Param_GetImeContext, OnGetImeContext)
34         FUN_HANDLER(Param_ReleaseImeContext, OnReleaseImeContext)
35         FUN_HANDLER(Param_SetConversionMode, OnSetConversionMode)
36         FUN_HANDLER(Param_GetConversionMode, OnGetConversionMode)
37         FUN_HANDLER(Param_SetOpenStatus, OnSetOpenStatus)
38         FUN_HANDLER(Param_GetOpenStatus, OnGetOpenStatus)
39         FUN_HANDLER(Param_GetActiveWnd, OnGetActiveWnd)
40     FUN_END
41 
42 private:
43     ITextService * m_pTxtService;
44     SOUI::CAutoRefPtr<SOUI::IIpcHandle> m_ipcHandle;
45 };
1 bool CClientConnection::CallFun(SOUI::IFunParams *params) const
2 {
3     SASSERT(m_ipcHandle);
4     return m_ipcHandle->CallFun(params);
5 }
 1 void CSinstarProxy::ProcessKeyStoke(UINT64 imeContext, UINT vkCode, LPARAM lParam, BOOL bKeyDown, BYTE byKeyState[256], BOOL * pbEaten)
 2 {
 3     Param_ProcessKeyStoke param;
 4     param.lpImeContext = imeContext;
 5     param.vkCode = vkCode;
 6     param.lParam = (DWORD)lParam;
 7     param.bKeyDown = bKeyDown;
 8     memcpy(param.byKeyState, byKeyState, 256);
 9     param.bEaten = false;
10     m_conn.CallFun(&param);
11     *pbEaten = param.bEaten;
12 }
CSinstarProxy对象有一个CClientConnection对象:m_conn,它需要调用服务器的方法ProcessKeyStoke,我们需要把对应的函数参数包装到对象:Param_ProcessKeyStoke中,调用m_conn.CallFun(&param),再从参数中获取返回值。

 在CClientConnection对象中有一组FUN_BEGIN,FUN_END包装的处理函数映射表,分别用来处理服务端对客户端的函数调用。

如此,一个客户端服务器双向调用的IPC就完成了。

这个IPC核心就是用参数对象来包装参数列表并经过序列化,反序列化来实现跨进程函数调用,并通过实现一些宏简化开发,美化代码结构,目前在我的启程输入法3.0中工作很好。

启程输入法3.0 GIT仓库: https://gitee.com/setoutsoft/sinstar3

 

启程软件 2019-02-03

posted @ 2019-02-03 14:49  启程软件  阅读(1554)  评论(0编辑  收藏  举报