第2章 包装外观(Wrapper Facade):用于在类中封装函数的结构型模式

2.1 介绍   本论文描述包装外观模式。该模式的意图是通过面向对象( OO)类接口来封装低级函数和数据结构。常见的包装外观模式的例子是像 MFC、 ACE和 AWT这样的类库,它们封装本地的 OS C API,比如 socket、 pthreads或 GUI函数。 `_'I 9,.a  
  直接对本地 OS C API进行编程会使网络应用繁琐、不健壮、不可移植,且难以维护,因为应用开发者需要了解许多低级、易错的细节。本论文阐释包装外观模式怎样使这些类型的应用变得更为简洁、健壮、可移植和可维护。 ,wlh0;,  
  本论文被组织如下: 2.2详细描述使用西门子格式 [1]的包装外观模式, 2.3给出结束语。 H{P"$zj`l  
|!)3[<.  
2.2 包装外观模式 9)QvJ87e@7  
b&!7(Q[ sT  
2.2.1 意图 Rd5r~iT  
  在更为简洁、健壮、可移植和可维护的较高级面向对象类接口中封装低级函数和数据结构。 94F9f^ L  
xxm1Nog6  
2.2.2 例子 lfKrd3KS_  
  为阐释包装外观模式,考虑图 2-1中所示的分布式日志服务的服务器。客户应用使用日志服务来记录关于它们在分布式环境中的执行状态的信息。这些状态信息通常包括错误通知、调试跟踪和性能诊断。日志记录被发送到中央日志服务器,由它将记录写到各种输出设备,比如网络管理控制台、打印机或数据库。


                       

 

图 2-1 分布式日志服务

图 2-1 所示的日志服务器处理客户发送的连接请求和日志记录。日志记录和连接请求可以并发地在多个 socket 句柄 上到达。各个句柄标识在 OS 中管理的网络通信资源。 Tp`by 1s  
  客户使用像 TCP[2] 这样的面向连接协议与日志服务器通信。因而,当客户想要记录日志数据时,它必须首先向日志服务器发送连接请求。服务器使用句柄工厂 ( handle factory )来接受连接请求,句柄工厂在客户知道的网络地址上进行侦听。当连接请求到达时, OS 句柄工厂接受客户的连接,并创建表示该客户的连接端点的 socket 句柄。该句柄被返回给日志服务器,后者在这个句柄和其他句柄上等待客户日志请求到达。一旦客户被连接,它们可以发送日志记录给服务器。服务器通过已连接的 socket 句柄来接收这些记录,处理记录,并将它们写到它们的输出设备。 r 8,6qP[  
  开发并发处理多个客户的日志服务器的常见方法是使用低级 C 语言函数和数据结构来完成线程、同步及网络通信等操作。例如,图 2-2 演示怎样将 Solaris 线程 [3] 和 socket[4] 网络编程 API 用于开发多线程日志服务器。 x1$tS#lS 
                            
图 2-2 多线程日志服务器

在此设计中,日志服务器的句柄工厂在它的主线程中接受客户网络连接。它随即派生一个新线程,在单独的连接中运行 logging_handler 函数、以处理来自每个客户的日志记录。下面的两个 C 函数演示怎样使用 socket 、互斥体和线程的本地 Solaris OS API 来实现此日志服务器设计。 a$FELlMv  
Z| +/Wl-h  
// At filescope. lB L;aTzo  
// Keep trackof number of logging requests. 7k:}9M~  
static intrequest_count; 8Hs>+Udl  
// Lock toprotect request_count. HlY4%M5q/  
static mutex_tlock; RIM"MR9qe=  
// Forwarddeclaration. >/$qF  
static void*logging_handler (void *); X1i6CEa<  
// Port numberto listen on for requests. %n9}P , ?  
static c*****tint logging_port = 10000; 7Z"mVh}  
  SA{A E9y  
// Main driverfunction for the multi-threaded ALwkX"AN  
// loggingserver. Some error handling has been xue-5 '  
// omitted tosave space in the example. ZqkP# ]+Y'  
int main (intargc, char *argv[]) F1)Q#ThF/  
{ * "ER8/  
structsockaddr_in sock_addr; @k,u xe-  
  yK0Q,  
// HandleUNIX/Win32 portability differences. `t:�7&$>T  
#if defined(_WINSOCKAPI_) _ts0@Z_:  
SOCKETacceptor; C%"h1zWE:  
#else brNe13d3~"  
int acceptor; djPr 4Nog  
#endif /*_WINSOCKAPI_ */ )Bl% {C  
  vP3Fb;  
// Create alocal endpoint of communication. Zo;@StN3}T  
acceptor =socket (PF_INET, SOCK_STREAM, 0); ,<|EoravH  
  ; MCv  
// Set up theaddress to become a server. -d %bc?  
memset(reinterpret_cast <void *> (&sock_addr), 0, sizeof sock_addr); o]MQ)/ r  
sock_addr.sin_family= AF_INET; ]BP/KCjAI<  
sock_addr.sin_port= ht***** (logging_port); c"H4/,F  
sock_addr.sin_addr.s_addr= htonl (INADDR_ANY); 9 o-T#~i  
  _xAdvr' W  
// Associateaddress with endpoint. X+P& up06  
bind (acceptor, sryujb.,  
reinterpret_cast <structsockaddr *> v=i[s  
(&sock_addr), (~}yt�.7K  
sizeof sock_addr); =35EG{W(  
  ;r- /h1iA'  
// Makeendpoint listen for connecti*****. }N2T/U  
listen(acceptor, 5); f&8&UL>e`  
  *ta|,  
// Main serverevent loop. x nWapG  
for (;;) WsJ3zZc  
{ 0}PW?t76  
thread_t t_id; vay_QxB5  
  }v0oFY$u`H  
// Handle UNIX/Win32 portabilitydifferences. &UbNp8h  
#if defined(_WINSOCKAPI_) 1-r1hZ-  
SOCKET h; G}BO!Z6  
#else ~6kEpa  
int h; GLnj& Ve  
#endif /*_WINSOCKAPI_ */ lsY5QE:Qrp  
  9i yNR!  
// Block waiting for clients toconnect. HR/yJt  
int h = accept (acceptor, 0, 0); f@[q# }6  
  vs7Hg)F  
// Spawn a new thread that runs the `cy_@Z5A  
// <logging_handler> entrypoint and 8g(%6 ET  
// processes client logging recordson $khWu>b  
// socket handle <h>. y''`73U"  
thr_create (0, 0, nI|jUD+y  
logging_handler, 'F5&f9A  
reinterpret_cast <void *>(h), %^2LTK(P  
THR_DETACHED, #If}P $!  
&t_id); qg'RD]a>�R  
} O<Ht-TN&  
  i0v;mc �  
/* NOTREACHED*/ +]Po!bN@@  
return 0; x: `oqbd  
} Xb?P'nD  
? Z .p.v  
logging_handler 函数运行在单独的线程控制中,也就是,每个客户一个线程。它在各个连接上接收并处理日志记录,如下所示: (Z,v)TOXjV  
,gUSW  
// Entry pointthat processes logging records for SBzJQt@Hs  
// one clientconnection. Ha[Bf*  
void*logging_handler (void *arg) .Quu_S_ vH  
{ !)?n n3  
// HandleUNIX/Win32 portability differences. K;k_MA310  
#if defined(_WINSOCKAPI_) :p;!/4)u  
SOCKET h =reinterpret_cast <SOCKET> (arg); Rf^$?D&^  
#else FWq+'Gk SV  
int h =reinterpret_cast <int> (arg); %nSm 32/t3  
#endif /*_WINSOCKAPI_ */ @mSdksB /L  
for (;;) fIN F;TK  
{ 0BMKwZg  
UINT_32 len; // Ensure a 32-bitquantity. lG I1LUo  
char log_record[LOG_RECORD_MAX]; ({v$!AAv  
  TD'Rv�Tpl  
// The first <recv> reads thelength "9 '~6b  
// (stored as a 32-bit integer) of $5yH(Z[[  
// adjacent logging record. Thiscode ?3D|{  
// does not handle"short-<recv>s". q_bE?j{  
ssize_t n = recv cfBq/2I  
(h, i ?&t@"'  
reinterpret_cast <char *>(&len), 4nK/gXz19  
sizeof len, 0); ##+|zka!U  
  ROlef;/A  
// Bail out if we don’t get theexpected len. HY0q!.qog  
if (n <= sizeof len) break; S96H`kedZo  
len = ntohl (len); // Convertbyte-ordering. @}eEV[Lli  
if (len > LOG_RECORD_MAX) break; AFE6@/'  
  }{T9`^V:h  
// The second <recv> thenreads <len> sU!�h^N$  
// bytes to obtain the actualrecord. $0$'co"  
// This code handles"short-<recv>s". t5t,(^�;f  
for (size_t nread = 0; #hG0{_d7  
nread < len; %IrR+f+H  
nread += n) 4<}!+X7m  
{ (3 cJ8o>&  
n = recv (h, log_record + nread,len - nread, 0); [Jv0^"]  
// Bail out if an error occurs. ^j]"!:h  
if (n <= 0) return 0; p7et>;WRx  
} Hdvtgss!  
  Dl=9<:6FW  
mutex_lock (&lock); TL&�`Ywy  
  >QyJRMY  
// Execute following two statementsin a X$*MxMNs  
// critical section to avoid raceconditi***** O[}{$NXw  
// and scrambled output,respectively. kN7 JZ12  
++request_count; // Count # ofrequests received. ] V|hDU=t  
  0s9-`nHen|  
if (write (1, log_record, len) ==-1) d}D%%noIu  
// Unexpected error occurred, bailout. 1oKF-";u(  
break; G47(LE"2b  
  ~6{iQZa1Y  
mutex_unlock (&lock); rU6F$I=  
} L hp  
  HQp�/0NC]  
close (h); Ibf~gr(j  
return 0; ,LE�15},  
} a E|'%72g  
6=cfr; BH2  
注意全部的线程、同步及网络代码是怎样使用 Solaris 操作系统所提供的低级 C 函数和数据结构来编写的。 cI Sugk~  
&}'FC7}  
2.2.3 上下文 :g%hT$,]3b  
  访问由低级函数和数据结构所提供服务的应用。 epicY  
Yg/g9$'  
2.2.4 问题 }Q2v~ eD  
  网络应用常常使用 2.2.2 中所演示的低级函数和数据结构来编写。尽管这是一种惯用方法,由于不能解决以下问题,它会给应用开发者造成许多问题: n~"qbtp}  
mXY�G^}  
繁琐、不健壮的程序: 直接对低级函数和数据结构编程的应用开发者必须反复地重写大量冗长乏味的软件逻辑。一般而言,编写和维护起来很乏味的代码常常含有微妙而有害的错误。 Vu`5/QDq  
例如,在 2.2.2 的 main 函数中创建和初始化接受器 socket 的代码是容易出错的,比如没有对 sock_addr 清零,或没有对 logging_port 号使用 ht*****[5] 。 mutex_lock 和 mutex_unlock 也容易被误用。例如,如果 write 调用返回 -1 , logging_handler 代码就会不释放互斥锁而跳出循环。同样地,如果嵌套的 for 循环在遇到错误时返回, socket 句柄 h 就不会被关闭。 W [*G o  
]OSq}ul  
缺乏可移植性: 使用低级函数和数据结构编写的软件常常不能在不同的 OS 平台和编译器间移植。而且,它们甚至常常不能在同一 OS 或编译器的不同版本间移植。不可移植性源于隐藏在基于低级 API 的函数和数据结构中的信息匮乏。 Y/x>wNW  
例如,在 2.2.2 中的日志服务器实现已经硬编码了对若干不可移植的本地 OS 线程和网络编程 C API 的依赖。特别地,对 thr_create 、 mutex_lock 和 mutex_unlock 的使用不能移植到非 Solaris OS 平台上。同样地,特定的 socket 特性,比如使用 int 表示 socket 句柄,不能移植到像 Win32 的 WinSock 这样的非 Unix 平台上; WinSock 将 socket 句柄表示为指针。 a(J@]X>'  
f+ cN'jH E  
高维护开销: C 和 C++ 开发者通常通过使用 #ifdef 在他们的应用源码中显式地增加条件编译指令来获得可移植性。但是,使用条件编译来处理平台特有的变种在各方面 都增加了应用源码的物理设计 复杂性 [6] 。开发者难以维护和扩展这样的软件,因为平台特有的实现细节分散在应用源文件的各个地方。 _<E.?K$gbU  
例如,处理 socket 数据类型的 Win32 和 UNIX 可移植性(也就是, SOCKET vs. int )的 #ifdef 妨碍了代码的可读性。对像这样的低级 C API 进行编程的开发者必须十分熟悉许多 OS 平台特性,才能编写和维护该代码。 Ucx"//"  
由于有这些缺点,通过对低级函数和数据结构直接进行编程来开发应用常常并非是有效的设计选择。 hW!2C6  
$7QGi|W*k  
2.2.5 解决方案 pG6?"*Fz;  
  确保应用不直接访问低级函数和数据结构的一种有效途径是使用包装外观 模式。对于每一组相关的函数和数据结构,创建一或多个包装外观类,在包装外观接口所提供的更为简洁、健壮、可移植和可维护的方法中封装低级函数和数据结构。 4U8N7  
J$W4AT  
2.2.6 结构 )3 I~6ar  
  包装外观模式的参与者的结构在图 2-1 中的 UML 类图中演示:

                                              
图 2-1 包装外观模式的参与者的结构

包装外观模式中的关键参与者包括: /.aKxj5  
y6dQ4Whv&  
函数( Function ): 函数 是现有的低级函数和数据结构,它们提供内聚的( cohesive )服务。 pO/vD~C>  
!kG�|BJ$j  
包装外观( WrapperFa?ade ): 包装外观 是封装函数和与其相关联的数据结构的一个或一组类。包装外观提供的方法将客户调用转发给一或多个低级函数。 zcC:b4  
yH(3 m#  
2.2.7 动力特性 VbDk44X.W  
  图 2-2 演示包装外观模式中的各种协作:
                                            
图 2-2 包装外观模式中的协作

如下所述,这些协作是十分简单明了的: : ?}mu1  
;PB_@Zg  
1. 客户调用( Client invocation ): 客户通过包装外观的实例来调用方法。 pw&k0?K#  
2. 转发( Forwarding ): 包装外观方法将请求转发给它封装的一或多个底层函数,并传递函数所需的任何内部数据结构。 S&'?L0  
=:Yrb2gP_/  
2.2.8 实现 6g/hQ/+Z}  
  这一部分解释通过包装外观模式实现组件和应用所涉及的步骤。我们将阐释这些包装外观是怎样克服繁琐、不健壮的程序、缺乏可移植性,以及高维护开销等问题的;这些问题折磨着使用低级函数和数据结构的解决方案。 [7Q%c!e$�*  
  这里介绍的例子基于 2.2.2 描述的日志服务器,图 2-3 演示此例中的结构和参与者。这一部分中的例子应用了来自 ACE 构架 [7] 的可复用组件。 ACE 提供一组丰富的可复用 C++ 包装和构架组件,以跨越广泛的 OS 平台完成常见的通信软件任务。 2cO6'?b 
                          
图 2-3 多线程日志服务器
可采取下面的步骤来实现包装外观模式: t 9t '9  
lX-i�<0`  
1. 确定现有函数间的内聚的抽象和关系: 像 Win32 、 POSIX 或 X Windows 这样被实现为独立的函数和数据结构的传统 API 提供许多内聚的抽象,比如用于网络编程、同步和线程,以及 GUI 管理的机制。但是,由于在像 C 这样的低级语言中缺乏数据抽象支持,开发者常常并不能马上明了这些现有的函数和数据结构是怎样互相关联的。因此,应用包装外观的第一步就是确定现有 API 中的较低级函数之间的内聚的抽象和关系。换句话说,我们通过将现有的低级 API 函数和数据结构聚合进一或多个类中来定义一种“对象模型”。 Vo6+|�ztk|  
在我们的日志例子中,我们从仔细检查我们原来的日志服务器实现开始。该实现使用了许多低级函数,由它们实际提供若干内聚的服务,比如同步和网络通信。例如, mutex_lock 和 mutex_unlock 函数与互斥体同步抽象相关联。同样地, socket 、 bind 、 listen 和 accept 函数扮演了网络编程抽象的多种角色。 +?W4ac1  
2. 将内聚的函数组聚合进包装外观类和方法中: 该步骤可划分为以下子步骤: gkX7,J-0  
在此步骤中,我们为每组相关于特定抽象的函数和数据结构定义一或多个包装外观类。 Y]*&/Ex"/  
@ze2'56F }  
A. 创建内聚的类: 我们从为每组相关于特定抽象的函数和数据结构定义一或多个包装外观类开始。用于创建内聚的类的若干常用标准包括: QF npp/K  
?E_;[(Mcr  
  •  
    • 确定在底层函数中什么是通用的 什么是可变的 ,并把函数分组进类中,从而将变化隔离在统一的接口后面。
  • 将具有高内聚性 ( cohesion )的函数合并进独立的类中,同时使类之间不必要的耦合 最小化。
BsA'r+ho?H  
一般而言,如果原来的 API 含有广泛的相关函数,就有可能必须创建若干包装外观类来适当地对事务进行分理。 :<s`)  
Y+o/?|q-E  
B. 将多个独立函数合并进类方法中: 除了将现有函数分组进类中,在每个包装类中将多个独立函数组合进数目更少的方法中常常也是有益的。例如,为确保一组低级函数以适当的顺序被调用,可能必须要采用此设计。 qo$ls/[X  
  Kj�"X!-�  
C. 选择间接层次: 大多数包装外观类简单地将它们的方法调用直接转发给底层的低级函数。如果包装外观方法是内联的,与直接调用低级函数相比,可能并没有额外的间接层次。为增强可扩展性,还可以通过动态分派包装外观方法实现来增加另外的间接层次。在这种情况下,包装外观类扮演桥接( Bridge )模式 [8] 中的抽象 ( Abstraction )角色。 qp)Wt6 k?  
  9R2"(.U  
D. 确定在哪里处理平台特有的变种: 使平台特有的应用代码最少化是使用包装外观模式的重要好处。因而,尽管包装外观类方法的实现 在不同的 OS 平台上可以不同,它们应该提供统一的、平台无关的接口 Bu&9J(J1  
处理平台特有变种的一种策略是在包装外观类方法实现中使用 #ifdef 。在联合使用 #ifdef 和自动配置工具(比如 GNU autoconf )时,可以通过单一的源码树创建统一的、不依赖于平台的包装外观。另一种可选策略是将不同的包装外观类实现分解进分离的目录中(例如,每个平台有一个目录),并配置语言处理工具,以在编译时将适当的包装外观类包含进应用中。 tHo|8c~ [  
选择特定的策略在很大程度上取决于包装外观方法实现变动的频度。例如,如果它们频繁变动,为每个平台正确地更新 #ifdef 可能是单调乏味的。同样地,所有依赖于该文件的文件可能都需要重编译,即使变动仅仅对一个平台来说是必需的。 /gkhSLq  
>MKj~Ud  
 在我们的日志例子中,我们将为互斥体、 socket 和线程定义包装外观类,以演示每一子步骤是怎样被实施的。如下所示: dWQB1Y*N  
:s&dn%5N"  
  • 互斥体包装外观: 我们首先定义 Thread_Mutex 抽象,在统一和可移植的类接口中封装 Solaris 互斥体函数:
  QxA( *1  
classThread_Mutex y�3o3�G  
{ c|`$ h  
public: VH~YwO!x  
Thread_Mutex(void) iF Mf[qBg  
{ $Hj;i/zD  
mutex_init (&mutex_, 0, 0); JB].ht  
} s3nO"~tM  
  `Fo/RZOW  
?Thread_Mutex(void) !H) -  
{ p/RT*?<  
mutex_destroy (&mutex_); [` qdpzUp&  
} s2NBYDi$?  
  rD4 umWi  
int acquire(void) [:#K_EI5%  
{ }zf!mlk  
return mutex_lock (&mutex_); ~Ck�OiWC0  
} OR!W3 @  
  Xpn/TD<_I  
int release(void) %b<W]HwA  
{ ~=iH*AQR  
return mutex_unlock (&mutex_); Ikf[K%NKn  
} Iq4B%xo6G  
  :&TM0O  
private: [8IO0lul+  
//Solaris-specific Mutex mechanism. qf/W,SM  
mutex_t mutex_; sAqy(oy#M  
  @WS77d~S  
// = Disallowcopying and assignment. } v:YSG  
Thread_Mutex(c*****t Thread_Mutex &); 4jC)"tch  
void operator=(c*****t Thread_Mutex &); 'xj5R=V  
} ; 6{]F#ig=  
ll4CF}k  
  通过定义 Thread_Mutex 类接口,并随之编写使用它、而不是低级本地 OS CAPI 的应用,我们可以很容易地将我们的包装外观移植到其他平台。例如,下面的 Thread_Mutex 实现在 Win32 上工作: cI/[)5&  
`}#rc DK  
classThread_Mutex Fy Ih/  
{ EUuSN| a  
public: ;Go^)bN ;  
Thread_Mutex(void) iLuC_.'u=  
{ [QgP6f]=  
InitializeCriticalSection(&mutex_); |*NZ^6`@  
} Fb]+h)on  
  bc'IoD/  
?Thread_Mutex(void) )8W! |  
{ s<F*kLib  
DeleteCriticalSection(&mutex_); zFExYYd  
} `/>kN%  
  } jJKE  
int acquire(void) Tt)z[^)%  
{ W^L^7  
EnterCriticalSection (&mutex_);return 0; r9_ ON|  
} T /}U{9ELL  
  tjx8UgSi  
int release(void) xjo`u:BH  
{ ?lsK?>uU  
LeaveCriticalSection (&mutex_);return 0; }b(h D|e  
} ZDFq=)0C  
  /Pg)7Zn  
private: '2 r  
//Win32-specific Mutex mechanism. jDY B*Y^F  
CRITICAL_SECTIONmutex_; hI86WP9*  
  Hloe7+5UD  
// = Disallowcopying and assignment. rS BI'op  
Thread_Mutex(c*****t Thread_Mutex &); _R ii19k  
void operator=(c*****t Thread_Mutex &); _ Lh0  
} ; cpa" ,8  
03fOm  
  如早先所描述的,我们可以通过在 Thread_Mutex 方法实现中使用 #ifdef 以及自动配置工具(比如 GUN autoconf )来支持多个 OS 平台,以使用单一源码树提供统一的、平台无关的互斥体抽象。相反,我们也可以将不同的 Thread_Mutex 实现分解进分离的目录中,并指示我们的语言处理工具在编译时将适当的版本包含进我们的应用中。 |:R/j0t  
  除了改善可移植性,我们的 Thread_Mutex 包装外观还提供比直接编程低级 Solaris 函数和 mutex_t 数据结构更不容易出错的互斥体接口。例如,我们可以使用 C++ private 访问控制指示符来禁止互斥体的拷贝和赋值;这样的使用是错误的,但却不会被不那么强类型化的 C 编程 API 所阻止。 })�-V,/  
);.$ `0  
  • socket 包装外观: socket API 比 Solaris 互斥体 API 要大得多,也有表现力得多 [5] 。因此,我们必须定义一组相关的包装外观类来封装 socket 。我们将从定义下面的处理 UNIX/Win32 可移植性差异的 typedef 开始:
  hH1Q:}a  
#if !defined(_WINSOCKAPI_) ]%6%rq%9C  
typedef intSOCKET; m0bxVV^DK!  
#defineINVALID_HANDLE_VALUE -1 qIZ+%ZOu  
#endif /*_WINSOCKAPI_ */ 2X2Ax~d@  
s9wzN6re  
接下来,我们将定义 INET_Addr 类,封装 Internet 域地址结构: cn} CI  
,"`20.Lv  
class INET_Addr ` 7iA?;  
{ b /UXO$_~-  
public: Dr.eos4 ~  
INET_Addr(u_short port, long addr) ejV`W7U  
{ MM32/}Y6  
// Set up the address to become aserver. m-O *t$6  
memset (reinterpret_cast <void*> (&addr_), 0, sizeof addr_); =cl#aS}e8  
addr_.sin_family = AF_INET; ;&j'`t P  
addr_.sin_port = ht***** (port); i/IpS@/{-v  
addr_.sin_addr.s_addr = htonl(addr); AJ/Hw>>$?m  
} y05!-G:Y/  
  q<E7q Y+  
u_shortget_port (void) c*****t U=D;CjAh  
{ 9mDdX  
return addr_.sin_port; !6|_`l>G,  
} {O _X/y~  
  )2).kL>  
longget_ip_addr (void) c*****t 3/v tx9D  
{ `S((F|Ty=;  
return addr_.sin_addr.s_addr; IA0 vSF:  
} &pI/VIx ?  
  Kc]cJ`P4.  
sockaddr *addr(void) c*****t k`>qb8,  
{ Ia](CN*;6  
return reinterpret_cast<sockaddr *>(&addr_); GThGV"  
} =Jl/^u%H(x  
  :c]y/lQmV  
size_t size(void) c*****t bP$e1I3`  
{ 1<@lM8&.kO  
return sizeof (addr_); O8hx}dOjA  
} umP n w  
// ... FsUH/Y y  
  %tkqWK:  
private: KD#zsL)3  
sockaddr_inaddr_; wa[J/lW  
} ; hlyh8=Z6o  
L ' _%zO  
  注意 INET_Addr 构造器是怎样通过将 sockaddr_in 域清零,并确保端口和 IP 地址被转换为网络字节序,消除若干常见的 socket 编程错误的。 5Zc  
  下一个包装外观类, SOCK_Stream ,对应用可在已连接 socket 句柄上调用的 I/O 操作(比如 recv 和 send )进行封装: Y]R=z*i%  
2 O(k@M5E?  
classSOCK_Stream >*/ |tL  
{ ce4rhtkV  
public: Pi[]k]XA/  
// =C*****tructors. fc:87ZR{K  
// Defaultc*****tructor. )e[q% %ks  
SOCK_Stream(void) i{:?Iw 'ay  
: handle_ (INVALID_HANDLE_VALUE) {} cJT_Qfxx  
  hBZh0x y  
// Initializefrom an existing HANDLE. F9w2+z.  
SOCK_Stream(SOCKET h): handle_ (h) {} mMZ=9 ?m  
  ]�%7m+-h@  
//Automatically close the handle on destruction. U2_;  
?SOCK_Stream (void){ close (handle_); } VKXB)-'L  
  K:4G(?w  
void set_handle(SOCKET h) { handle_ = h; } RionKiN  
SOCKETget_handle (void) c*****t { return handle_; } ENYc.$r  
  1:h(8%H@"  
// = I/Ooperati*****. 50S*_4R  
int recv (char*buf, size_t len, int flags = 0); _ Axw$oYS  
int send (c*****tchar *buf, size_t len, int flags = 0); ojWf]$^y}  
// ... q<g!bW%  
  $H,9GIivD  
private: } F*=+n  
// Handle forexchanging socket data. CwEb ?  
SOCKET handle_; '~6l 6wi  
} ; )]}68}9  
Wu?[1L:x  
注意此类是怎样确保 socket 句柄在 SOCK_Stream 对象出作用域时被自动关闭的。 %eu_Pr�6X  
  SOCK_Stream 对象由连接工厂 SOCK_Acceptor 创建,后者封装被动的 连接建立逻辑 [9] 。 SOCK_Acceptor 构造器初始化被动模式接受器 socket ,以在 sock_addr 地址上进行侦听。同样地, accept 工厂方法通过新接受的连接来初始化 SOCK_Stream ,如下所示: Iak06E  
KC/W6|NtGj  
classSOCK_Acceptor % +$!ctn  
{ xiQd[[(sM  
public: p_sqw~)^%  
SOCK_Acceptor(c*****t INET_Addr &sock_addr) Ac,bf 8C  
{ DV bY  
// Create a local endpoint ofcommunication. x A ZRl  
handle_ = socket (PF_INET,SOCK_STREAM, 0); 6}mSA@4&  
  ^6^A/] v  
// Associate address with endpoint. %t-}dC&  
bind (handle_, sock_addr.addr (), sock_addr.size()); ^W ,x  
  |fWR[/NU  
// Make endpoint listen forconnecti*****. 1b"3 ]?  
listen (handle_, 5); cy_zEJjbD  
} ; I#t#%!InH  
  .`N&,& H  
// Accept aconnection and initialize K}Pi"Le@W  
// the<stream>. yCye3z.  
int accept(SOCK_Stream &stream) E@ !~q�  
{ u!VY6y7p  
stream.set_handle (accept (handle_,0, 0)); Z|Xv_Xo|4  
if (stream.get_handle () ==INVALID_HANDLE_VALUE) X=b]Whuv  
return -1; RjQdlr6*  
else return 0; [kg*BaG:  
} J|I&{  
  /wo'XF3:  
private: s av  
// Sockethandle factory. cY/"{o"C  
SOCKET handle_; P&] PJt5  
} ; }>u<,  
c1Ta!p{%  
注意 SOCK_Acceptor 的构造器是怎样确保低级的 socket 、 bind 和 listen 函数总是以正确的次序被调用的。 o.H(&ex|  
  完整的 socket 包装外观集还包括 SOCK_Connector ,封装主动的 连接建立逻辑 [9] 。 d#G H4+C  
.9;wJ9Bw[  
  • 线程外观: 在不同的 OS 平台上有许多线程 API 可用,包括 Solaris 线程、 POSIX Pthreads 和 Win32 线程。这些 API 显示出微妙的语法和语义差异,例如, Solaris 和 POSIX 线程可以“分离”( detached )模式被派生,而 Win32 线程则不行。但是,可以提供 Thread_Manager 包装外观,在统一的 API 中封装这些差异。如下所示:
  ['n;e:*  
classThread_Manager g{06d~Y  
{ -{XXU�)Z  
public: X>y6-%@  
int spawn (void*(*entry_point) (void *), Z}'"c9oB  
void *arg, x,SzZ)l-9  
long flags, GT"gB$Mh  
long stack_size = 0, }PDNW  
void *stack_pointer = 0, c,I|O' &k  
thread_t *t_id = 0) K+_$ WT_  
{ hd}"%9p  
thread_t t; 5#U*vGVT  
if (t_id == 0) Eo }mSd  
t_id = &t; 7Q9zEd" d  
  -Fj:^q:@u  
return thr_create (stack_size, isP4*g&%x  
stack_pointer, T;%ceLD  
entry_point, _ADK8a6%)  
arg, #Mz N7  
flags, b}[W[J}`  
t_id); Px)/`'D  
} W%=b|6E  
// ... huau(s0um  
} ; @edi6b1W  
IRZ?'Im  
Thread_Manager 还提供联接( join )和取消线程的方法。 &r;4$7  
*zy0,{bl  
1. 确定错误处理机制: 低级的 C 函数 API 通常使用返回值和整型代码(比如 errno )来将错误通知给它们的调用者。但是,此技术是容易出错的,因为调用者可能会忘记检查它们的函数调用的返回状态。 &zF1&J58z  
更为优雅的报告错误的方式是使用异常处理。许多编程语言,比如 C++ 和 Java ,使用异常处理来作为错误报告机制。它也被某些操作系统所使用,比如 Win32 。 ;TK:D=p4  
使用异常处理作为包装外观类的错误处理机制有若干好处: 5UwaBPj4  
l/yFx  
  • 它是可扩展的: 现代编程语言允许通过对现有接口和使用干扰极少的特性来扩展异常处理策略和机制。例如, C++ 和 Java 使用继承来定义异常类的层次。 它使错误处理与正常处理得以干净地去耦合: 例如,错误处理信息不会显式地传递给操作。而且,应用不会因为没有检查函数返回值而偶然地忽略异常。 它可以是类型安全的: 在像 C++ 和 Java 这样的语言中,异常以一种强类型化的方式被扔出和捕捉,以增强错误处理代码的组织和正确性。相对于显式地检查线程专有的错误值,编译器会确保对于每种类型的异常,将执行正确的处理器。
vTD`Ja#h  
但是,为包装外观类使用异常处理也有若干缺点: ~T')s-,l,:  
Ve&(izI h  
  • 它不是通用的: 不是所有语言都提供异常处理。例如,某些 C++ 编译器没有实现异常。同样地,当 OS 提供异常服务时,它们必须被语言扩展所支持,从而降低了代码的可移植性。 它使多种语言的使用变得复杂化: 因为语言以不同的方式实现异常,或根本不实现异常,如果以不同语言编写的组件扔出异常,可能很难把它们集成在一起。相反,使用整型值或结构来报告错误信息提供了更为通用的解决方案。 它使资源管理变得复杂化: 如果在 C++ 或 Java 代码块中有多个退出路径,资源管理可能会变得复杂化 [10] 。因而,如果语言或编程环境不支持垃圾收集,必须注意确保在有异常扔出时删除动态分配的对象。 它有着潜在的时间和 / 或空间低效的可能性: 即使没有异常扔出,异常处理的糟糕实现也会带来时间和 / 或空间的过度开销 [10] 。对于必须具有高效和低内存占用特性的嵌入式系统来说,这样的开销可能会特别地成问题。
XY5I5H_U  
  对于封装内核级设备驱动程序或低级的本地 OS API (它们必须被移植到许多平台上)的包装外观来说,异常处理的缺点也是特别成问题的。对于这些类型的包装外观,更为可移植、高效和线程安全的处理错误的方式是定义错误处理器抽象,显式地维护关于操作的成功或失败的信息。使用线程专有存储( Thread-Specific Storage )模式 [11] 是被广泛用于这些系统级包装外观的解决方案。 iaq0/d.[7  
kg$<^:uX  
1. 定义相关助手类(可选): 一旦低级函数和数据结构被封装在内聚的包装外观类中,常常有可能创建其他助手类来进一步简化应用开发。通常要在包装外观模式已被应用于将低级函数和与其关联的数据聚合进类中之后,这些助手类的效用才变得明显起来。 =S-'*F  
例如,在我们的日志例子中,我们可以有效地利用下面的实现 C++ ScopedLocking 习语的 Guard 类;该习语确保 Thread_Mutex 被适当地释放,不管程序的控制流是怎样退出作用域的。 {O6f1LuH  
Xv'M/T}6C+  
template<class LOCK> /HDRr*KO  
class Guard )#r]x1[Kn  
{ oSt-w{ !  
public: =@m|g )  
Guard (LOCK&lock): lock_ (lock) -/j}le6;c  
{ X;�T(?,,  
lock_.acquire (); ]7 ROCJ;  
} )L`0VTw'M  
  xX  
?Guard (void) s} ,p>8  
{ Hq^s U%  
lock_.release (); pHY~_^B4&  
} 8p7Uvn+m*  
  r}9qK%C G.  
private: S 1|[}nYP  
// Hold thelock by reference to avoid &@A(8(%  
// the use ofthe copy c*****tructor... JcZs/ fl9  
LOCK&lock_; O�GrVy=rd  
} )oa6;=go  
8(D>ws$  
Guard 类应用了 [12] 中描述的 C++ 习语,藉此,在一定作用域中“构造器获取资源而析构器释放它们”。如下所示: &D uvy#J  
~d7!)c`z  
// ... /A _g  
{ waW2$9O  
// C*****tructorof <mon> automatically vj^vzFb�K  
// acquires the<mutex> lock. $ W(m  
Guard<Thread_Mutex>mon (mutex); '6fMF#X4F  
  +%8c8]2  
// ...operati***** that must be serialized ... mTW0_!.  
  S?*v p=  
// Destructorof <mon> automatically *8fnxWR  
// releases the<mutex> lock. Txfu%'2)e  
} v$w!hYsQ  
// ... @X"p"3V  
qn6Y(@<[  
  因为我们使用了像 Thread_Mutex 包装外观这样的 ,我们可以很容易地替换不同类型的锁定机制,与此同时仍然复用 Guard 的自动锁定 / 解锁协议。例如,我们可以用 Process_Mutex 类来取代 Thread_Mutex 类,如下所示: 1-1x,U7w  
s6rdQI]  
// Acquire aprocess-wide mutex. E:f0NV3"1  
Guard<Process_Mutex>mon (mutex); Lc�f =)GL  
<jQ?l% /  
  如果使用 C 函数和数据结构、而不是 C++ 类,获得这种程度的“可插性”( pluggability )要困难得多。 +L!-JrYHS4  
qXkc~{W_  
2.2.9 例子解答 .:*V CDOM  
  下面的代码演示日志服务器的 main 函数,它已使用 2.2.8 描述的互斥体、 socket 和线程的包装外观重写。 ^vLHs=<  
f:G�Zb?Wyd  
// At filescope. xpKD 'O=T  
// Keep trackof number of logging requests. :O{`!&[>L  
static intrequest_count; +n�%uIv  
  =GTltFqI 1  
// Managethreads in this process. DF-`nD  
staticThread_Manager thr_mgr; (n05MwKu/  
  V]t ucs  
// Lock to protectrequest_count. t>.�mB@se|  
staticThread_Mutex lock; ZWQrG'$?o8  
  Wjn1W;m&g  
// Forwarddeclaration. //nR=Dy{  
static void*logging_handler (void *); 5�e~/o}]  
  $J>GCY  
// Port numberto listen on for requests. O6NgI2[O  
static c*****tint logging_port = 10000; cH?j@-pY  
  +VkhM;'"C  
// Main driverfunction for the multi-threaded t8DyS FT  
// loggingserver. Some error handling has been eEePK~%c  
// omitted tosave space in the example. x+x 6F  
int main (intargc, char *argv[]) 9E4H`[EQ  
{ 3`S|I_$(T"  
// Internetaddress of server. ?2zVW Z  
INET_Addr addr(port); uo;aC$US  
  CpNnywDRwU  
// Passive-modeacceptor object. Veo:G{  
SOCK_Acceptorserver (addr); _fx0-S*$  
  OvqCuX  
SOCK_Streamnew_stream; l2QO/O I9m  
  ||a 5)D  
// Wait for aconnection from a client. D's'LspQ  
for (;;) e6f:@ O?  
{ wDswK "T  
// Accept a connection from aclient. GO{o # }  
server.accept (new_stream); 2/h}6DGx2  
  <`,pyvR Kv  
// Get the underlying handle. H=Rqr  
SOCKET h = new_stream.get_handle(); J0CEZ  
  b�fy `UZr  
// Spawn off athread-per-connection. }6ObQa43  
thr_mgr.spawn (logging_handler, y`O !,kW  
reinterpret_cast <void *>(h), g"v�g {Q  
THR_DETACHED); :+,>0%  
} ^Y�z.,!B[  
} M:x?I_JG8  
]vj4E"2;  
logging_handler 函数运行在单独的线程控制中,也就是,每个相连客户有一个线程。它在各个连接上接收并处理日志记录,如下所示: y57]q#k  
i( P/=B  
// Entry pointthat processes logging records for kXimJL_<g  
// one clientconnection. "Y0:Y?Vz"  
void*logging_handler (void *arg) td!WgL,m�  
{ "8MG[$Y  
SOCKET h =reinterpret_cast <SOCKET> (arg); (qd�$wv^h  
  Lt ; !q b.  
// Create a<SOCK_Stream> object from SOCKET <h>. G{$(t/>8  
SOCK_Streamstream (h); x�xxM  
for (;;) fFP>$  
{ 5sJi- ^�  
UINT_32 len; // Ensure a 32-bitquantity. RDU,yTHq  
char log_record[LOG_RECORD_MAX]; +�E8/g  
  '5xf?0@s.  
// The first <recv_n> readsthe length W+k`^A|@  
// (stored as a 32-bit integer) of <hlH@[7!  
// adjacent logging record. Thiscode P|}~=2J  
// handles"short-<recv>s". 86Q/G.h7  
ssize_t n = stream.recv_n bg.f';C  
(reinterpret_cast <char *>(&len), r,}U -S.w  
sizeof len); P(?i>F7s  
  ;0*T7l  
// Bail out if we’re shutdown or s@C KZ`  
// errors occur unexpectedly. !(/OT  
if (n <= 0) break; 4k' 2FkDA  
len = ntohl (len); // Convertbyte-ordering. }'HJV��B_  
if (len > LOG_RECORD_MAX) break; WK7=z3mu  
  :x e/7�-  
// The second <recv_n> thenreads <len> fsc^8  
// bytes to obtain the actualrecord. 7{=<_  
// This code handles"short-<recv>s". F@Bh>Vb  
n = stream.recv_n (log_record,len); Y9F78 =Q�  
  v=Ep  
// Bail out if we’re shutdown or EoLF7j<W  
// errors occur unexpectedly. 0:-i  
if (n <= 0) break; 3UeG>5R  
  Bvx%|:R  
{ a&YD4DQ05  
// C*****tructor of Guardautomatically 2^ 'X  
// acquires the lock. :b+C<Bp64r  
Guard<Thread_Mutex> mon(lock); c_b^t09  
  75vd ]45as  
// Execute following two statementsin a �f:L%th  
// critical section to avoid raceconditi***** Nx4_Oc^hY  
// and scrambled output,respectively. ?! dp0<  
++request_count; // Count # ofrequests "Yw-1h`fR  
  =!3G�,qV  
if (write (STDOUT, log_record, len)== -1) T1m097  
break; DmAMr=p  
  PQAN�,d  
// Destructor of Guardautomatically ^!�^8]u<Q  
// releases the lock, regardless of w2y{3O"p=  
// how we exit this block! $5nOi�aQL  
} jTk !wm=  
} .(Q3M0.D  
  :pQZ) bF  
// Destructorof <stream> automatically OI�B~ W  
// closes down<h>. w C]yE/P1  
return 0; H@2JL.(k�  
} M$A"<5  
~$w-I/Q!  
注意上面的代码是怎样解决 2.2.2 所示代码的各种问题的。例如, SOCK_Stream 和 Guard 的析构器会分别关闭 socket 句柄和释放 Thread_Mutex ,而不管代码块是怎样退出的。同样地,此代码要容易移植和维护得多,因为它没有使用平台特有的 API 。 Bn*D<<{T  
2.2.10 已知应用 sJtz{'  
  本论文中的例子聚焦于并发网络编程。但是,包装外观模式已被应用到其他的许多领域,比如 GUI 构架和数据库类库。下面是包装外观模式的一些广为人知的应用: &O5%6Sv3d  
mJ0nyjX^  
Microsoft Foundation Class ( MFC ): MFC 提供一组封装大多数低级 C Win32 API 的包装外观,主要集中于提供实现 Microsoft 文档 / 模板体系结构的 GUI 组件。 2J�rr;"r  
2;.7c+r0  
ACE 构架: 2.2.8 描述的互斥体、线程和 socket 的包装外观分别基于 ACE 构架中的组件 [7] : ACE_Thread_Mutex 、 ACE_Thread_Manager 和 ACE_SOCK* 类。 csT_!sII  
nab:y(]$/  
Rogue Wave 类库: Rogue Wave 的 Net.h++ 和 Threads.h++ 类库在许多 OS 平台上实现了 socket 、线程和同步机制的包装外观。 (.7_`T6QG  
/^SL Zhe  
ObjectSpace System<Toolkit> 该工具包也提供了 socket 、线程和同步机制的包装外观。 P"W$ZX  
D:'|poH  
Java 虚拟机和 Java 基础类库: Java 虚拟机( JVM )和各种 Java 基础类库,比如 AWT 和 Swing ,提供了一组封装大多数低级的本地 OS 系统调用和 GUI API 的包装外观。 ^ #e:q  
T=: &W 3  
2.2.11 效果 z.oDH<1  
  包装外观模式提供以下好处: 3WS`,}  
h>$,97EU  
更为简洁和健壮的编程接口: 包装外观模式在一组更为简洁的 OO 类方法中封装许多低级函数。这减少了使用低级函数和数据结构开发应用的枯燥性,从而降低了发生编程错误的潜在可能性。 ULMG"."IH  
)ruC_)  
改善应用可移植性和可维护性: 包装外观类的实现可用以使应用开发者与低级函数和数据结构的不可移植的方面屏蔽开来。而且,通过用基于逻辑设计 实体(比如基类、子类,以及它们的关系)的应用配置策略取代基于物理设计 实体(比如文件和 #ifdef )的策略 [6] ,包装外观模式改善了软件结构。一般而言,根据应用的逻辑设计、而不是物理设计来理解和维护它们要更为容易一些。 j2deb`GD�  
-vwkvNn8  
改善应用的模块性、可复用性和可配置性: 通过使用像继承和参数化类型这样的 OO 语言特性,包装外观模式创建的可复用类组件可以一种整体方式被“插入”其他组件,或从中“拔出”。相反,不求助于粗粒度的 OS 工具,比如链接器或文件系统,替换成组的函数要难得多。 Y'S�xehx  
A{/7HV�5  
包装外观模式有以下缺点: W&}YM b  
=|�S8.|r+  
额外的间接性( Indirection ): 与直接使用低级的函数和数据结构相比,包装外观模式可能带来额外的间接。但是,支持内联的语言,比如 C++ ,可以无需显著的开销而实现该模式,因为编译器可以内联用于实现包装外观的方法调用。 `23&vGk}  
2.2.12 参见 *aS|4M-  
  包装外观模式与外观模式是类似的 [8] 。外观模式的意图是简化子系统的接口。包装外观模式的意图则更为具体:它提供简洁、健壮、可移植和可维护的类接口,封装低级的函数和数据结构,比如本地 OS 互斥体、 socket 、线程和 GUI C 语言 API 。一般而言,外观将复杂的类关系隐藏在更简单的 API 后面,而包装外观将复杂的函数和数据结构关系隐藏在更丰富的类 API 后面。 4?g~GI3  
  如果动态分派被用于实现包装外观方法,包装外观模式可使用桥接模式 [8] 来实现;包装外观方法在桥接模式中扮演抽象 ( Abstraction )角色。 b* qkox;j  
  n'7�3DApW  
2.3 结束语 &1893#V  
  本论文描述包装外观模式,并给出了详细的例子演示怎样使用它。在本论文中描述的 ACE 包装外观组件的实现可在 ACE[7] 软件发布中自由获取( URL : http://www.cs.wustl.edu/~schmidt/ACE.html )。该发布含有在圣路易斯华盛顿大学开发的完整的 C++ 源码、文档和测试例子驱动程序。目前 ACE 正在用于许多公司(像 Bellcore 、波音、 DEC 、爱立信、柯达、朗讯、摩托罗拉、 SAIC 和西门子)的通信软件项目中。 << LmO-92  
感谢 f( �hK>H  
  感谢 Hans Rohnert 、 Regine Meunier 、 Michael Stal 、 Christa Schwanninger 、 Frank Buschmann 和 Brad Appleton ,他们的大量意见极大地改善了包装外观模式描述的形式和内容。 rw8O<No4.o  
gWqmK/.U.0  
参考文献 9z/q_ 0&i  
[1] F.Buschmann, R. Meunier, H. Rohnert, P. Sommerlad, and M. Stal, Pattern-OrientedSoftware Architecture - A System of Pattern s. Wiley and S*****, 1996. *pw:oTO  
[2]W.R.Stevens, UNIX Network Programming, First Editio n. Englewood Cliffs,NJ: Prentice Hall, 1990. a+)Yk8%KY  
[3] J.Eykholt, S. Kleiman, S. Barton, R. Faulkner, A. Shivalin-giah, M. Smith, D.Stein, J. Voll, M. Weeks, and D. Williams, “Beyond Multiprocessing...Multithreading the SunOS Ker-nel,” in Proceedings of the Summer USENIXConferenc e,(San Antonio, Texas), June 1992. 03J,NXs  
[4]W.R.Stevens, UNIX Network Programming, Second Editio n. Englewood Cliffs,NJ: Prentice Hall, 1997. (' /S~  
[5] D. C.Schmidt, “IPC SAP: An Object-Oriented Interface to Interprocess Communicati*****ervices,” C++ Repor t,vol.4, November/December 1992. Run)E*sf  
[6] J.Lakos, Large-scale Software Development with C+ +. Reading, MA:Addison-Wesley, 1995. /#20`;~F)  
[7] D. C.Schmidt, “ACE: an Object-Oriented Framework for Developing DistributedApplicati*****,” in Proceedings of the 6th USENIX C++ TechnicalConferenc e, (Cambridge, Mas-sachusetts), USENIX Association, April 1994. 6#7f^uIK  
[8] E.Gamma, R. Helm, R. Johnson, and J. Vlissides, Design Pat-terns: Elements ofReusable Object-Oriented Softwar e. Read-ing, MA: Addison-Wesley, 1995. )9##mUt'}  
[9] D. C.Schmidt, “Acceptor and Connector: Design Patterns for InitializingCommunication Services,” in Pattern Languages of Program Design (R.Martin, F. Buschmann, and D. Riehle, eds.), Reading, MA: Addison-Wesley, 1997. c~ l$_A  
[10] H.Mueller, “Patterns for Handling Exception Handling Suc-cessfully,” C++ Repor t,vol. 8, Jan. 1996. #l2wF> 0  
[11] D. C.Schmidt, T. Harrison, and N. Pryce, “Thread-Specific Storage – An ObjectBehavioral Pattern for Accessing per-Thread State Efficiently,” C++ Repor t,vol.9,Novem-ber/December1997. UY*Hc  
[12]Bjarne Stroustrup, The C++ Programming Language, 3rd Editio n.Addison-Wesley, 1998. n 0 _:!]k^  
s!nFc{  
:RzcK>Gub=  
This file is decompiled by an unregistered version of ChmDecompiler. PkF B.  
Regsitered version does not show this message. v+znKpE  
You can download ChmDecompiler at :     [url]http://www.zipghost.com/ [/url] 

Ng,#d`Br 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值