半冷半暖秋天

2011-01-26

【翻译】两种高性能I/O设计模式(Reactor/Proactor)的比较

Filed under: C/C++,编程相关 — 标签:, , — sunu @ 13:16:45

这是05年的老文章,网上应该有人早就翻译过了,我翻译它仅仅为了学习Reactor/Proactor两种TCP服务器设计模式,顺便作翻译练习。

标题: 两种高性能I/O设计模式的比较
作者: Alexander Libman 、Vladimir Gilbourd
原文: http://www.artima.com/articles/io_design_patterns.html
时间: November 25, 2005
译者: 潘孙友 2010-01-26 于深圳

综述

这篇文章探讨并比较两种用于TCP服务器的高性能设计模式. 除了介绍现有的解决方案, 还提出了一种更具伸缩性,只需要维护一份代码并且跨平台的解决方案(含代码示例), 以及其在不同平台上的微调. 此文还比较了java,c#,c++对各自现有以及提到的解决方案的实现性能.

系统I/O 可分为阻塞型, 非阻塞同步型以及非阻塞异步型[1, 2]. 阻塞型I/O意味着控制权只到调用操作结束了才会回到调用者手里.
结果调用者被阻塞了, 这段时间了做不了任何其它事情. 更郁闷的是,在等待IO结果的时间里,调用者所在线程此时无法腾出手来去响应其它的请求,这真是太浪费资源了。拿read()操作来说吧, 调用此函数的代码会一直僵在此处直至它所读的socket缓存中有数据到来.

相比之下,非阻塞同步是会立即返回控制权给调用者的。调用者不需要等等,它从调用的函数获取两种结果:要么此次调用成功进行了;要么系统返回错误标识告诉调用者当前资源不可用,你再等等或者再试度看吧。比如read()操作, 如果当前socket无数据可读,则立即返回EWOULBLOCK/EAGAIN,告诉调用read()者”数据还没准备好,你稍后再试”.

在非阻塞异步调用中,稍有不同。调用函数在立即返回时,还告诉调用者,这次请求已经开始了。系统会使用另外的资源或者线程来完成这次调用操作,并在完成的时候知会调用者(比如通过回调函数)。拿Windows的ReadFile()或者POSIX的aio_read()来说,调用它之后,函数立即返回,操作系统在后台同时开始读操作。

在以上三种IO形式中,非阻塞异步是性能最高、伸缩性最好的。

这篇文章探讨不同的I/O利用机制并提供一种跨平台的设计模式(解决方案). 希望此文可以给于TCP高性能服务器开发者一些帮助,选择最佳的设计方案。下面我们会比较 Java, c#, C++各自对探讨方案的实现以及性能. 我们在文章的后面就不再提及阻塞式的方案了,因为阻塞式I/O实在是缺少可伸缩性,性能也达不到高性能服务器的要求。

两种IO多路复用方案:Reactor and Proactor

一般情况下,I/O 复用机制需要事件分享器(event
demultiplexor [1, 3]).
事件分享器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁的什么东西送了, 快来拿吧。开发人员在开始的时候需要在分享器那里注册感兴趣的事件,并提供相应的处理者(event handlers),或者是回调函数; 事件分享器在适当的时候会将请求的事件分发给这些handler或者回调函数.

涉及到事件分享器的两种模式称为:Reactor and Proactor [1]. Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的. 在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离者就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。

而在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称有overlapped的技术),事件分离者等IOCompletion事件完成[1]. 这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。

举另外个例子来更好地理解Reactor与Proactor两种模式的区别。这里我们只关注read操作,因为write操作也是差不多的。下面是Reactor的做法:

  • 某个事件处理者宣称它对某个socket上的读事件很感兴趣;
  • 事件分离者等着这个事件的发生;
  • 当事件发生了,事件分离器被唤醒,这负责通知先前那个事件处理者;
  • 事件处理者收到消息,于是去那个socket上读数据了. 如果需要,它再次宣称对这个socket上的读事件感兴趣,一直重复上面的步骤;

下面再来看看真正意义的异步模式Proactor是如何做的:

  • 事件处理者直接投递发一个写操作(当然,操作系统必须支持这个异步操作). 这个时候,事件处理者根本不关心读事件,它只管发这么个请求,它魂牵梦萦的是这个写操作的完成事件。这个处理者很拽,发个命令就不管具体的事情了,只等着别人(系统)帮他搞定的时候给他回个话。
  • 事件分离者等着这个读事件的完成(比较下与Reactor的不同);
  • 当事件分离者默默等待完成事情到来的同时,操作系统已经在一边开始干活了,它从目标读取数据,放入用户提供的缓存区中,最后通知事件分离者,这个事情我搞完了;
  • 事件分享者通知之前的事件处理者: 你吩咐的事情搞定了;
  • 事件处理者这时会发现想要读的数据已经乖乖地放在他提供的缓存区中,想怎么处理都行了。如果有需要,事件处理者还像之前一样发起另外一个写操作,和上面的几个步骤一样。

现行做法

开源C++开发框架 ACE[1, 3](Douglas Schmidt, et al.开发) 提供了大量平台独立的底层并发支持类(线程、互斥量等). 同时在更高一层它也提供了独立的几组C++类,用于实现Reactor及Proactor模式。 尽管它们都是平台独立的单元,但他们都提供了不同的接口.

ACE Proactor在MS-Windows上无论是性能还在健壮性都更胜一筹,这主要是由于Windows提供了一系列高效的底层异步API.
[4, 5].

(这段可能过时了点吧) 不幸的是,并不是所有操作系统都为底层异步提供健壮的支持。举例来说, 许多Unix系统就有麻烦.因此, ACE Reactor可能是Unix系统上更合适的解决方案. 正因为系统底层的支持力度不一,为了在各系统上有更好的性能,开发者不得不维护独立的好几份代码: 为Windows准备的ACE Proactor以及为Unix系列提供的ACE Reactor.

就像我们提到过的,真正的异步模式需要操作系统级别的支持。由于事件处理者及操作系统交互的差异,为Reactor和Proactor设计一种通用统一的外部接口是非常困难的。这也是设计通行开发框架的难点所在。

更好的解决方案

在文章这一段时,我们将尝试提供一种融合了Proactor和Reactor两种模式的解决方案. 为了演示这个方案,我们将Reactor稍做调整,模拟成异步的Proactor模型(主要是在事件分离器里完成本该事件处理者做的实际读写工作,我们称这种方法为”模拟异步“)。 下面的示例可以看看read操作是如何完成的:

  • 事件处理者宣称对读事件感兴趣,并提供了用于存储结果的缓存区、读数据长度等参数;
  • 调试者等待(比如通过select());
  • 当有事件到来(即可读),调试者被唤醒, 调试者去执行非阻塞的读操作(前面事件处理者已经给了足够的信息了)。读完后,它去通知事件处理者。
  • 事件处理者这时被知会读操作已完成,它拥有完整的原先想要获取的数据了.

我们看到,通过为分离者(也就上面的调试者)添加一些功能,可以让Reactor模式转换为Proactor模式。所有这些被执行的操作,其实是和Reactor模型应用时完全一致的。我们只是把工作打散分配给不同的角色去完成而已。这样并不会有额外的开销,也不会有性能上的的损失,我们可以再仔细看看下面的两个过程,他们实际上完成了一样的事情:

标准的经典的 Reactor模式:

  • 步骤 1) 等待事件 (Reactor 的工作)
  • 步骤 2) 发”已经可读”事件发给事先注册的事件处理者或者回调 ( Reactor 要做的)
  • 步骤 3) 读数据 (用户代码要做的)
  • 步骤 4) 处理数据 (用户代码要做的)

模拟的Proactor模式:

  • 步骤 1) 等待事件 (Proactor 的工作)
  • 步骤 2) 读数据(看,这里变成成了让 Proactor 做这个事情)
  • 步骤 3) 把数据已经准备好的消息给用户处理函数,即事件处理者(Proactor 要做的)
  • 步骤 4) 处理数据 (用户代码要做的)

在没有底层异步I/O API支持的操作系统,这种方法可以帮我们隐藏掉socket接口的差异(无论是性能还是其它), 提供一个完全可用的统一“异步接口”。这样我们就可以开发真正平台独立的通用接口了。

TProactor

我们提出的TProactor方案已经由TerabitP/L [6]公司实现了. 它有两种实现: C++的和Java的.C++版本使用了ACE平台独立的底层元件,最终在所有操作系统上提供了统一的异步接口。

TProactor中最重要的组件要数Engine和WaitStrategy了. Engine用于维护异步操作的生命周期;而WaitStrategy用于管理并发策略. WaitStrategy和Engine一般是成对出现的, 两者间提供了良好的匹配接口.

Engines和等待策略被设计成高度可组合的(完整的实现列表请参照附录1)。TProactor是高度可配置的方案,通过使用异步内核API和同步Unix API(select(), poll(), /dev/poll (Solaris 5.8+), port_get (Solaris 5.10),RealTime (RT) signals (Linux 2.4+), epoll (Linux 2.6), k-queue (FreeBSD) ),它内部实现了三种引擎(POSIX AIO, SUN AIO and Emulated AIO)并隐藏了六类等待策略。TProactor实现了和标准的 ACE Proactor一样的接口。这样一来,为不同平台提供通用统一的只有一份代码的跨平台解决方案成为可能。

Engines和WaitStrategies可以像乐高积木一样自由地组合,开发者可以在运行时通过配置参数来选择合适的内部机制(引擎和等待策略)。可以根据需求设定配置,比如连接数,系统伸缩性,以及运行的操作系统等。如果系统支持相应的异步底层API,开发人员可以选择真正的异步策略,否则用户也可以选择使用模拟出来的异步模式。所有这一切策略上的实现细节都不太需要关注,我们看到的是一个可用的异步模型。

举例来说,对于运行在Sun Solaris上的HTTP服务器,如果需要支持大量的连接数,/dev/poll或者port_get()之类的引擎是比较合适的选择;如果需要高吞吐量,那使用基本select()的引擎会更好。由于不同选择策略内在算法的问题,像这样的弹性选择是标准ACE Reactor/Proactor模式所无法提供的(见附录2)。

在性能方面,我们的测试显示,模拟异步模式并未造成任何开销,没有变慢,反倒是性能有所提升。根据我们的测试结果,TProactor相较标签的ACE Reactor在Unix/Linux系统上有大约10-35%性能提升,而在Windows上差不多(测试了吞吐量及响应时间)。

性能比较 (JAVA / C++ / C#).

除了C++,我们也在Java中实现了TProactor. JDK1.4中, Java仅提供了同步方法, 像C中的select() [7, 8]. Java TProactor基于Java的非阻塞功能(java.nio包),类似于C++的TProactor使用了select()引擎.

图1、2显示了以 bits/sec为单位的传输速度以及相应的连接数。这些图比较了以下三种方式实现的echo服务器:标准ACE Reactor实现(基于RedHat Linux9.0)、TProactor C++/Java实现(Microsoft Windows平台及RedHat v9.0), 以及C#实现。测试的时候,三种服务器使用相同的客户端疯狂地连接,不间断地发送固定大小的数据包。

这几组测试是在相同的硬件上做的,在不同硬件上做的相对结果对比也是类似。

图 1. Windows XP/P4 2.6GHz HyperThreading/512 MB RAM.
图 2. Linux RedHat 2.4.20-smp/P4 2.6GHz HyperThreading/512 MB RAM.

用户代码示例

下面是TProactor Java实现的echo服务器代码框架。总的来说,开发者只需要实现两个接口:一是OpRead,提供存放读结果的缓存;二是OpWrite,提供存储待写数据的缓存区。同时,开发者需要通过回调onReadComplated()和onWriteCompleted()实现协议相关的业务代码。这些回调会在合适的时候被调用.

class EchoServerProtocol implements AsynchHandler
{

  AsynchChannel achannel = null;

  EchoServerProtocol( Demultiplexor m,  SelectableChannel channel )
  throws Exception
  {
    this.achannel = new AsynchChannel( m, this, channel );
  }

  public void start() throws Exception
  {
    // called after construction
    System.out.println( Thread.currentThread().getName() +
	": EchoServer protocol started" );
    achannel.read( buffer);
  }

  public void onReadCompleted( OpRead opRead ) throws Exception
  {
    if ( opRead.getError() != null )
    {
      // handle error, do clean-up if needed
      System.out.println( "EchoServer::readCompleted: " +
      opRead.getError().toString());
      achannel.close();
      return;
    }

    if ( opRead.getBytesCompleted () <= 0)
    {
      System.out.println("EchoServer::readCompleted: Peer closed "
	   + opRead.getBytesCompleted();
      achannel.close();
      return;
    }

    ByteBuffer buffer = opRead.getBuffer();

    achannel.write(buffer);
  }

  public void onWriteCompleted(OpWrite opWrite)
  throws Exception
  {
    // logically similar to onReadCompleted
    ...
  }
}

结束语

TProactor为多个平台提供了一个通用、弹性、可配置的高性能通讯组件,所有那些在附录2中提到的问题都被很好地隐藏在内部实现中了。

从上面的图中我们可以看出C++仍旧是编写高性能服务器最佳选择,虽然Java已紧随其后。然而因为Java本身实现上的问题,其在Windows上表现不佳(这已经应该成为历史了吧)。

需要注意的是,以上针对Java的测试,都是以裸数据的形式测试的,未涉及到数据的处理(影响性能)。

纵观AIO在Linux上的快速发展[9], 我们可以预计Linux内核API将会提供大量更加强健的异步API, 如此一来以后基于此而实现的新的Engine/等待策略将能轻松地解决能用性方面的问题,并且这也能让标准ACE Proactor接口受益。

附录 I

TProactor中实现的Engines 和 等待策略

引擎类型 等待策略 操作系统
POSIX_AIO (true async)

aio_read()/aio_write()
aio_suspend()
Waiting for RT signal
Callback function
POSIX complained UNIX (not robust)
POSIX (not robust)
SGI IRIX, LINUX (not robust)
SUN_AIO (true async)

aio_read()/aio_write()
aio_wait() SUN (not robust)
Emulated Async

Non-blocking read()/write()
select()

poll()

/dev/poll

Linux RT signals

Kqueue
generic POSIX

Mostly all POSIX implementations

SUN

Linux

FreeBSD

附录 II

所有同步等待策略可划分为两组:

  • edge-triggered (e.g. Linux实时信号) – signal readiness only when socket became ready
    (changes state);
  • level-triggered (e.g. select(), poll(), /dev/poll) – readiness at any time.

让我们看看这两组的一些普遍的逻辑问题:

  • edge-triggered group: after executing I/O operation, the demultiplexing loop can lose the state of socket readiness. Example: the “read” handler did not read whole chunk of data, so the socket remains still ready for read. But the demultiplexor loop will not receive next notification.
  • level-triggered group: when demultiplexor loop detects readiness, it starts the write/read user defined handler. But before the start, it should remove socket descriptior from theset of monitored descriptors. Otherwise, the same event can be dispatched twice.
  • Obviously, solving these problems adds extra complexities to development. All these problems were resolved internally within TProactor and the developer should not worry about those details, while in the synch approach one needs to apply extra effort to resolve them.

资源

[1] Douglas C. Schmidt, Stephen D. Huston “C++ Network Programming.” 2002, Addison-Wesley
ISBN 0-201-60464-7

[2] W. Richard Stevens “UNIX Network Programming” vol. 1 and 2, 1999, Prentice Hill, ISBN 0-13-
490012-X

[3] Douglas C. Schmidt, Michael Stal, Hans Rohnert, Frank Buschmann “Pattern-Oriented Software
Architecture: Patterns for Concurrent and Networked Objects, Volume 2″ Wiley & Sons, NY
2000

[4] INFO: Socket Overlapped I/O Versus Blocking/Non-blocking Mode. Q181611. Microsoft
Knowledge Base Articles.

[5] Microsoft MSDN. I/O Completion Ports.

http://msdn.microsoft.com/library/default.asp?url=/library/en-

us/fileio/fs/i_o_completion_ports.asp

[6] TProactor (ACE compatible Proactor).

www.terabit.com.au

[7] JavaDoc java.nio.channels

http://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/package-summary.html

[8] JavaDoc Java.nio.channels.spi Class SelectorProvider

http://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/spi/SelectorProvider.html

[9] Linux AIO development

http://lse.sourceforge.net/io/aio.html, and

http://archive.linuxsymposium.org/ols2003/Proceedings/All-Reprints/Reprint-Pulavarty-OLS2003.pdf

更多

Ian Barile “I/O Multiplexing & Scalable Socket Servers”, 2004 February, DDJ

Further reading on event handling

http://www.cs.wustl.edu/~schmidt/ACE-papers.html

The Adaptive Communication Environment

http://www.cs.wustl.edu/~schmidt/ACE.html

Terabit Solutions

http://terabit.com.au/solutions.php

关于作者

Alex Libman has been programming for 15 years. During the past 5 years his main area of interest is pattern-oriented multiplatform networked programming using C++ and Java. He is big fan and contributor of ACE.

Vlad Gilbourd works as a computer consultant, but wishes to spend more time listening jazz :) As a hobby,he started and runs www.corporatenews.com.au website.

2010-12-31

字符集和字符编码(及答复spring)

Filed under: C/C++ — 标签:, , , , — sunu @ 11:43:48

@spring 给你留言了,但似乎被当成垃圾评论,你在后台查一下。

这个主题已经被N多人讨论过了,这里仅仅是个人总结,不是教程。

字符集和字符编码

潘孙友 2010-12-31 于遵义

目录
一、字符集
二、字符编码
三、Windows平台
  3.1 Codepage代码页
  3.2 编码转换(API)
  3.3 编码转换(CRT)
四、Linux/unix平台
  4.1 iconv
  4.2 ICU

一、字符集

字符集是一个集合,描述并定义了这个集合中可以出现哪些字符,常见的字符有GB2312、GBK、GB18030、UNICODE等。字符集仅仅是一种规范,一种约定,我们也可以定义自己的字符集。

举例来说,银行IT系统为了字段合法性校验的方便,常内部定义一些小字符集,比如X字符集,N字符集。

x-字符集由以下86个字符组成
a b c d e f g h I j k l m n o p q r s t u v w x y z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
0 1 2 3 4 5 6 7 8 9
. , - _ ( ) / = ’+ : ? ! ” % & * < > ; @ #
(cr)(lf) (space)

不同的字符集之间很可能是有交集的,并且包含越多字符的字符集越通用。中国的BG2312,GBK,GB18030是在不同时期,逐步扩展而来的,所以GB18030是前者的超集。现今最大的字符集是UNICODE,几乎包含了世界上所有语言的字符。

二、字符编码

先有字符集才有字符编码,编码是字符集的具体表示方式,一个字符集可以有多种编码方式,只要这种方式可以涵盖字符集中的所有字符。

比如UNICODE字符集的具体编码方式有很多种,utf-8/utf-16/utf-32;而gb2312就只有常见的一种编码方式GB2312(所以有时候人们常将编码与字符集弄混)。可能有人会说,utf-8中包含了gb2312的所有字符,那不就是以utf-8编码表示了gb2312字符集么,事实是这样,但问题人家utf-8编码方式又不是为你gb2312定制的,人家只是顺手把你给表示了。

不同的编码方式,根据其特性应用在不同场合。以UNICODE字符集的编码方式为例,其中utf-8以尽可能少的存储空间存放字符,自动纠错性能好(由于编码的特殊性,其一个字符出错只会影响其后几个字节,而utf16/32等等长字节的就连错一大片了),适合传输及存储;utf-16/32以等字节表示所有字符,在程序内部更容易处理,一般用于系统内部字符格式。

三、Windows平台

3.1 Codepage代码页

Windows平台有所谓的代码页,用于表示字符编码方式,至少支持哪些,要看你的系统里安装了哪些。通过GetACP我们可以获取当前系统的编码方式。

WINBASEAPI UINT WINAPI GetACP(void);

通过以下小段代码,可以获举系统支持的编码方式。

	for(int i=0; i<=65001; i++)
	{
		CPINFOEXA cpinfo;
		if (IsValidCodePage(i)){
			if (GetCPInfoExA(i, 0, &cpinfo))
				printf("%d=[%s]\n", cpinfo.CodePage, cpinfo.CodePageName);
		}
	}

详细的列表见:
http://msdn.microsoft.com/en-us/library/dd317756(v=VS.85).aspx

3.2 编码转换(API)

Windows为我们提供了两个函数用于字符编码的转换。

WINBASEAPI
int
WINAPI
MultiByteToWideChar(
    __in UINT     CodePage,
    __in DWORD    dwFlags,
    __in_bcount(cbMultiByte) LPCSTR   lpMultiByteStr,
    __in int      cbMultiByte,
    __out_ecount_opt(cchWideChar) __transfer(lpMultiByteStr) LPWSTR  lpWideCharStr,
    __in int      cchWideChar);

WINBASEAPI
int
WINAPI
WideCharToMultiByte(
    __in UINT     CodePage,
    __in DWORD    dwFlags,
    __in_ecount(cchWideChar) LPCWSTR  lpWideCharStr,
    __in int      cchWideChar,
    __out_bcount_opt(cbMultiByte) __transfer(lpWideCharStr) LPSTR   lpMultiByteStr,
    __in int      cbMultiByte,
    __in_opt LPCSTR   lpDefaultChar,
    __out_opt LPBOOL  lpUsedDefaultChar);

Windows内部应该是完全使用Unicode编码的,系统提供的W后缀的API都接受wchar_t(unsigned short)的字符串,而A系列的API实则是先内部转换为Unicode编码再调用W系统函数。

在windows上做不同编码间的转换,一般是先将原始编码转换为Unicode编码,再将Unicode编码的字符串转换为目标编码。以gbk到utf-8为例:

std::string ctk_gbk2utf8(const char*s)
{
	s = s?s:"";
	std::wstring unicodestr;
	std::string utf8str;
	//gbk转换成utf16
	int n = MultiByteToWideChar(936, 0, s, -1, NULL, 0);

	unicodestr.resize(n);
	MultiByteToWideChar(936, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length());

	//从utf16转utf8
	n = WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 );
	utf8str.resize(n);
	WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, (char*)utf8str.c_str(), (int)utf8str.length(), 0, 0 );

	return utf8str;
}

std::string ctk_utf82gbk(const char*s)
{
	s = s?s:"";
	std::wstring unicodestr;
	std::string gbkstr;
	//utf8转换成utf16
	int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);

	unicodestr.resize(n);
	MultiByteToWideChar(CP_UTF8, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length());

	//从utf16转gbk
	n = WideCharToMultiByte(936, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 );
	gbkstr.resize(n);
	WideCharToMultiByte(936, 0, unicodestr.c_str(), -1, (char*)gbkstr.c_str(), (int)gbkstr.length(), 0, 0 );

	return gbkstr;
}

测试代码:

	const char*gbk  = "这是中文. hello world!";
	printf("gbk=%s\n", gbk);

	string utf8str = ctk_gbk2utf8(gbk);
	printf("utf8str=%s\n", utf8str.c_str());

	string gbkstr = ctk_utf82gbk(utf8str.c_str());
	printf("gbkstr=%s\n", gbkstr.c_str());

如果要处理像日本或者韩国语言编码,只要换一个那个936为相应的代码就可以了。上面的CP_UTF8可以换为65501。一般情况下,使用系统API转换编码就差不多了。

3.3 编码转换(CRT)

另外我们可以通过c/c++标准库中的mbstocws, cwstombs函数来转码,我们可通过两组函数来测试,一组为Windows API实际的,一组为
mbstocws, cwstombs实现,效果是一样的。

std::string ctk_gbk2utf8(const char*s)
{
	s = s?s:"";
	std::wstring unicodestr;
	std::string utf8str;
	int n = MultiByteToWideChar(936, 0, s, -1, NULL, 0);
	unicodestr.resize(n);
	MultiByteToWideChar(936, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length());
	n = WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 );
	utf8str.resize(n);
	WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, (char*)utf8str.c_str(), (int)utf8str.length(), 0, 0 );
	return utf8str;
}

std::string ctk_gbk2big5(const char*s)
{
	s = s?s:"";
	std::wstring unicodestr;
	std::string dststr;
	int n = MultiByteToWideChar(936, 0, s, -1, NULL, 0);
	unicodestr.resize(n);
	MultiByteToWideChar(936, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length());
	n = WideCharToMultiByte(950, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 );
	dststr.resize(n);
	WideCharToMultiByte(950, 0, unicodestr.c_str(), -1, (char*)dststr.c_str(), (int)dststr.length(), 0, 0 );
	return dststr;
}
std::string ctk_gbk2big5_crt(const char*s)
{
	s = s?s:"";
	std::string srcstr = s;
	std::string curLocale = setlocale(LC_ALL, NULL);
	setlocale(LC_ALL, ".936");
	size_t newSize = srcstr.length() + 1;
	wstring unicodestr;
	unicodestr.resize(newSize);
	wmemset((wchar_t*)unicodestr.c_str(), 0, newSize);
	mbstowcs((wchar_t*)unicodestr.c_str(), srcstr.c_str(), newSize);
	string newstr;
	newSize = newSize*2 + 1;
	setlocale(LC_ALL, ".950");
	newstr.resize(newSize);
	memset((char*)newstr.c_str(), 0, newSize);
	wcstombs((char*)newstr.c_str(), unicodestr.c_str(), newSize);
	setlocale(LC_ALL, curLocale.c_str());
	return newstr;
}

std::string ctk_big52gbk_crt(const char*s)
{
	s = s?s:"";
	std::string srcstr = s;
	std::string curLocale = setlocale(LC_ALL, NULL);
	setlocale(LC_ALL, ".950");

	size_t newSize = srcstr.length() + 1;
	wstring unicodestr;
	unicodestr.resize(newSize);
	wmemset((wchar_t*)unicodestr.c_str(), 0, newSize);
	mbstowcs((wchar_t*)unicodestr.c_str(), srcstr.c_str(), newSize);
	string newstr;
	newSize = newSize*2 + 1;
	setlocale(LC_ALL, ".936");
	newstr.resize(newSize);
	memset((char*)newstr.c_str(), 0, newSize);
	wcstombs((char*)newstr.c_str(), unicodestr.c_str(), newSize);
	setlocale(LC_ALL, curLocale.c_str());
	return newstr;
}

四、Linux/unix平台

4.1 iconv

这些平台上,字符编码的转换一般使用iconv,可能是独立的iconv库也可能是glibc自带的版本。
在linux/unix上字符编码的问题比windows更容易碰到,所以经常有人喊搭了个ftp传上来的文件名乱码什么的(顺便提一句,包括IE7在内的之前的IE版本直接访问utf8编码的ftp时会有乱码,通过网络抓包工具会发现是IE发送的utf-8字符串是部分错误的)。

暂时只谈程序中的编码问题,先不管locale之类的(虽然也相关)。

自己“封装”的转码函数。

char *ctk_iconv(const char *fromStr, const int fromLen, char**toStr,  const char *fromCode, const char *toCode)
{
	char *buffer;
	iconv_t cd;
	const char *inbuf = NULL;
	size_t inbytesleft = 0;
	char *outbuf = NULL;
	size_t outbytesleft = 0;
	int errorCode = 0;
	int bufferSize=0;
	size_t ret = 0;
	int done = 0;

	if (fromStr==NULL || fromStr[0]=='\0' || fromLen <=0 ) return NULL;
	if (fromCode==NULL || fromCode[0]=='\0' ) return NULL;
	if (toCode==NULL || toCode[0]=='\0' ) return NULL;

	memset(&cd, 0x00, sizeof(iconv_t));
	inbuf = fromStr;
	inbytesleft = fromLen;

	errorCode = 0;
	bufferSize = fromLen*4+1;
	buffer = (char*)malloc(sizeof(char)*bufferSize);
	memset(buffer, 0x00, bufferSize);

	outbuf = buffer;
	outbytesleft = bufferSize;

	if ( (iconv_t)-1  == ( cd = iconv_open(toCode, fromCode) ) ) {
		return NULL;
	}	

	while ( inbytesleft >0 && done !=1 ) {
		ret = iconv(cd, (char**)&inbuf, &inbytesleft, &outbuf, &outbytesleft);
		if ( (size_t)-1  == ret ) {
			errorCode = errno;
			switch(errorCode)
			{
			case EILSEQ:
			{
				if((outbuf<buffer+bufferSize)&&(outbuf>=buffer))
				{
					memcpy(outbuf, inbuf, 1);
					outbuf += 1;
					outbytesleft -= 1;
					inbuf += 1;
					inbytesleft -= 1;
					if ( inbytesleft <= 0 ) break;
				}
			}
			break;
			case EINVAL:
			{
				done = 1;
			}
			break;
			case E2BIG:
			{
				done = 1;
				break;
			}
			break;
			default:
				done = 1;
			}
		}
	}
	if ( NULL != toStr)
		*toStr = buffer;
	iconv_close(cd);
	return buffer;
}

std::string ctk_iconv_gbk2utf8(const char*s)
{
	s = s ? s:"";
	char *utf8str = NULL;
	ctk_iconv(s, strlen(s), &utf8str,  "gbk", "utf-8");
	std::string result("");
	if (utf8str!=NULL)
	{
		result = utf8str;
		free(utf8str);
	}
	return result;
}

std::string ctk_iconv_utf82gbk(const char*s)
{
	s = s ? s:"";
	char *gbkstr = NULL;
	ctk_iconv(s, strlen(s), &gbkstr, "utf-8", "gbk");
	std::string result("");
	if (gbkstr!=NULL)
	{
		result = gbkstr;
		free(gbkstr);
	}
	return result;
}

Iconv在windows上也非常好用。

4.2 ICU

IBM出品的ICU也是编码转换的好手,到处有它的身影,php6就使用它来做内码。由于没有亲身使用经历,就不多说了。

http://www-01.ibm.com/software/globalization/icu/index.html

2010-12-27

gSOAP学习笔记

Filed under: C/C++,文档及资源 — 标签:, , , , — sunu @ 14:58:53

gSOAP学习笔记

潘孙友 2010-12-27 于遵义

目录
一、基本概念
  1.1 关于SOAP
  1.2 关于gSOAP
  1.3 gSOAP编译器(命令行工具)
    1.3.1 wsdl2h
    1.3.2 socapcpp2
二、gSOAP开发:Web Service服务端
三、gSOAP开发:Web Service客户端
四、参考资料

一、基本概念

1.1 关于SOAP

SOAP(Simple Object Access Protocol),即简单对象访问协议,是在分布式的环境中交换数据的简单协议,以XML作为数据传送语言。
SOAP有两种工作模式,一种是RPC(Remote Procedure Call),另一种是Message-Oriented。MO可以利用XML来交换结构更复杂的数据。RPC模式的SOAP可以理解为这样一个开发协议:SOAP=RPC+HTTP+XML,具有以下特点:

  • 采用HTTP作为通信协议,采用客户/服务模式;
  • RPC作为统一的远程方法调用途径;
  • 传送的数据使用XML格式。

看一个简单的请求及回复SOAP数据(真实数据):

POST /wpsoap/ HTTP/1.1
Host: 127.0.0.1:10240
User-Agent: gSOAP/2.7
Content-Type: text/xml; charset=utf-8; action=""
Content-Length: 480
Connection: close
SOAPAction: ""

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ns1="http://www.example.org/wpsoap/" xmlns:ns2="urn:nszfpt"><SOAP-ENV:Body><ns2:login><req><username>admin</username><password>3.14159</password></req></ns2:login></SOAP-ENV:Body></SOAP-ENV:Envelope>
HTTP/1.1 200 OK
Server: gSOAP/2.7
Content-Type: text/xml; charset=utf-8; action=""
Content-Length: 555
Connection: close

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wpsoap="urn:nszfpt"><SOAP-ENV:Body SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><wpsoap:tagRspLogin><rsp><retCode>0</retCode><retMessage>login ok!</retMessage></rsp><session>01234567890</session></wpsoap:tagRspLogin></SOAP-ENV:Body></SOAP-ENV:Envelope>

这东西非常的复杂,我仅仅记录一下使用到的部分。

1.2 关于gSOAP

引用:http://blog.csdn.net/darkone/archive/2006/12/14/1442525.aspx
gSOAP编译工具提供了一个SOAP/XML 关于C/C++ 语言的实现,
从而让C/C++语言开发web服务或客户端程序的工作变得轻松
了很多。绝大多数的C++web服务工具包提供一组API函数类库
来处理特定的SOAP数据结构,这样就使得用户必须改变程序
结构来适应相关的类库。与之相反,gSOAP利用编译器技术提
供了一组透明化的SOAP API,并将与开发无关的SOAP实现细节
相关的内容对用户隐藏起来。gSOAP的编译器能够自动的将用
户定义的本地化的C或C++数据类型转变为符合XML语法的数据
结构,反之亦然。这样,只用一组简单的API就将用户从SOAP
细节实现工作中解脱了出来,可以专注与应用程序逻辑的实
现工作了。gSOAP编译器可以集成C/C++和Fortran代码(通过
一个Fortran到C的接口),嵌入式系统,其他SOAP程序提供
的实时软件的资源和信息;可以跨越多个操作系统,语言环
境以及在防火墙后的不同组织。
	gSOAP使编写web服务的工作最小化了。gSOAP编译器生成
SOAP的代码来序列化或反序列化C/C++的数据结构。gSOAP包
含一个WSDL生成器,用它来为你的web服务生成web服务的解
释。gSOAP的解释器及导入器可以使用户不需要分析web服务
的细节就可以实现一个客户端或服务端程序。

照我理解,gSOAP可以为我们生成soap服务器端+客户端代码的框架,我们只需实现具体的接口函数即可。而生成代码的工具就是上面文中提到的“gSOAP编译器”。

1.3 gSOAP编译器(命令行工具)

1.3.1 wsdl2h

此工具用来从WSDL文件生成c/c++头文件。

wsdl2h -o 头文件名 WSDL文件名或URL
常用的其它参数:
-o 文件名,指定输出头文件
-n 名空间前缀 代替默认的ns
-c 产生纯C代码,否则是C++代码
-s 不要使用STL代码
-t 文件名,指定type map文件,默认为typemap.dat
-e 禁止为enum成员加上名空间前缀

1.3.2 socapcpp2

此工具用来从头文件,生成SOAP服务器及客户端代码,还包括WSDL、测试用XML数据。

soapcpp2 头文件
常用选项
-C 仅生成客户端代码
-S 仅生成服务器端代码
-L 不要产生soapClientLib.c和soapServerLib.c文件
-c 产生纯C代码,否则是C++代码(与头文件有关)
-I 指定import路径(见上文)
-x 不要产生XML示例文件
-i 生成C++包装,客户端为xxxxProxy.h(.cpp),服务器端为xxxxService.h(.cpp)。

二、gSOAP开发:Web Service服务端

开发服务器程序,需使用gSOAP生成服务器端代码框架。我们有两种做法:

  1. 编写WSDL,使用wsdl2h生成头文件,再soapcpp2生成框架代码;
  2. 编写头文件,使用soapcpp2生成框架代码;

这两种方式,结果是一样的,最终都有产生头文件,并生成代码。不同在于,在项目的开发中需要维护的文件不同,前者是需要维护WSDL文件,后者维护头文件。

我个人觉得第二种方式更好用,不仅仅是少了个步骤,而是WSDL的语法太难写了,有点XSD的味道。而头文件的编写,更接近于程序员的思考方式,比如定义消息结构,定义接口名称等。

gSOAP是非常智能的,它利用C/C++的注释来获取信息,所以在手工编写的头文件中,注释是用用处的,常以// gsoap 名字空间 …开头。做为学习,我准备为php blog程序wordpress写一个web service接口,名字叫wpsoap。

我开始写头文件(wpSoap.h)了,出于学习目的,我仅实现了两个接口:一是用户登陆;一是日志发布。

/**
 * @file wpsoap.h
 * @brief 为wordpress2.7提供web service接口
 *
 *  "//gsoap"开头行,请勿删除.
 *
 *  1. 通过此文件生成WSDL 及 服务端代码
 *
 *    >mkdir -p srvSrcFromH
 *    >cd srvSrcFromH
 *    >soapcpp2 -L -S "wpsoap.h" -I /path/to/gsoap-2.8/gsoap/import/
 *
 *  2. 通过WSDL生成客户端代码
 *
 *    >mkdir -p clientSrcFromWSDL
 *    >cd clientSrcFromWSDL
 *    >wsdl2h.exe  -o wpsoap.h ../srvSrcFromH/wpsoap.wsdl -I /path/to/gsoap-2.8/gsoap/import/
 *    >soapcpp2 -L -C wpsoap.h -I /path/to/gsoap-2.8/gsoap/import/
 *
 * @author pansunyou@gmail.com
 * @version 1.0
 * @date 2010-12-27
*/

//gsoap wpsoap service name: wpsoap
//gsoap wpsoap service namespace: http://www.example.org/wpsoap/
//gsoap wpsoap service location: http://192.168.0.187:10240/wpsoap/
//gsoap wpsoap service encoding: encoded
//gsoap wpsoap schema namespace: urn:nszfpt

#import "stlvector.h"

//通用回复
class wpsoap__tagCommResponse
{
       int                  retCode                 ;      //回复码
       std::string retMessage           ;      //回复消息
};

//[请求]用户登陆
class wpsoap__tagReqLogin
{
    std::string username              ;      //用户名
    std::string password        ;      //密码名文
};

//[答复]用户登陆
class wpsoap__tagRspLogin
{
    wpsoap__tagCommResponse rsp    ;      //通用回复
       std::string session                 ;      //会话标识
};

//[接口]登陆接口
int wpsoap__login(wpsoap__tagReqLogin req, wpsoap__tagRspLogin& rsp);

//[请求]发布日志
class wpsoap__tagReqPost
{
    std::string title         ;      //标题
    std::string body              ;      //正文
};

//[答复]发布日志
class wpsoap__tagRspPost
{
    wpsoap__tagCommResponse rsp    ;      //通用回复
};

//[接口]发布日志接口
int wpsoap__post(wpsoap__tagReqPost req, wpsoap__tagRspPost& rsp);

在接口中,我使用到了自定义的消息结构wp_soap_tag*,这里的wpsoap__前缀是必须的,这样soapcpp2才能为我们生成正确的代码。

之后,我使用soapcpp2生成服务端代码框架:

@echo off
@set path=%cd%\..\..\contrib\gsoap-2.8\gsoap\bin\win32\;%path%

mkdir srvSrcFromH 2>nul
cd srvSrcFromH
soapcpp2.exe -L -S ..\res\wpSoap.h -I ..\..\..\contrib\gsoap-2.8\gsoap\import\
pause

要编译出服务程序,有这些代码还不够,还需要自己写两个文件,一个用来写main函数,一个用来写wpsoap的接口函数(当然可以放在一个文件里)。最终我的服务器程序有以下文件:(另外,还需要gsoap目录下的stdsoap2.cpp,因为我把它编译为静态库了,所以这里没列出来。)

D:\wpSoapServer
|   makeSrc.bat
|   wpsoapimpl.cpp                //这里实现了soapStub.h给出的接口
|   wpsoapsrv.cpp                  //这里是main函数开始的地方
+---res
|       wpSoap.h
\---srvSrcFromH
        soapC.cpp
        soapH.h
        soapServer.cpp
        soapStub.h
        soapwpsoapObject.h
        wpsoap.login.req.xml
        wpsoap.login.res.xml
        wpsoap.nsmap
        wpsoap.post.req.xml
        wpsoap.post.res.xml
        wpsoap.wsdl
        wpsoap.xsd

每次我修改了res/wpSoap.h后,我就运行一下makeSrc.bat,自动重新生成srvSrcFromH目录里的所有东西,并且这个目录里的所有代码是不需要手工维护的(除非有特殊需要)。

在服务器代码中,我仅实现了以下两个函数(wpsoapimpl.cpp):

int wpsoap__login(struct soap*, wpsoap__tagReqLogin req, wpsoap__tagRspLogin &rsp);
int wpsoap__post(struct soap*, wpsoap__tagReqPost req, wpsoap__tagRspPost &rsp);

wpsoapsrv.cpp里的代码仅仅是调用gSOAP产生的代码来建立socket服务器,基本不需维护。gSOAP是线程安全的,可以将请求分配到线程池内实现高效服务,但我仅为了走通gSOAP的使用流程,没有这样使用。

具体做法可以参考:http://www.cs.fsu.edu/~engelen/soapdoc2.html

三、gSOAP开发:Web Service客户端

客户端代码本来也是可以通过为服务端编写的头文件生成的,但是为了真实一点,假设我无法获取服务器开发时使用的头文件,仅仅有个公开的WSDL文件,就是上面产生的srvSrcFromH /wpsoap.wsdl。

我用这个脚本来生成客户端框架代码:

@echo off
@set path=%cd%\..\..\contrib\gsoap-2.8\gsoap\bin\win32\;%path%

mkdir clientSrcFromWSDL 2>nul
cd clientSrcFromWSDL
wsdl2h.exe -o wpsoap.h ..\..\wpSoapServer\srvSrcFromH\wpsoap.wsdl
soapcpp2.exe -L -C wpsoap.h -I ..\..\..\contrib\gsoap-2.8\gsoap\import\
pause

加上我测试用的代码wpsoapclient.cpp,以及gosap目录里的stdsoap2.cpp,我有了如下文件:

D:\wpSoapClient
|   makeSrc.bat
|   wpsoapclient.cpp
\---clientSrcFromWSDL
        soapC.cpp
        soapClient.cpp
        soapH.h
        soapStub.h
        soapwpsoapProxy.h
        wpsoap.h
        wpsoap.login.req.xml
        wpsoap.login.res.xml
        wpsoap.nsmap
        wpsoap.post.req.xml
        wpsoap.post.res.xml

客户端代码非常少(仅仅是实现,容错之类的都未考虑):

/**
 * @file wpsoapclient.cpp
 * @brief 访问wpsoap服务
 *
 * 调用wpsoap的客户端示例代码
 *
 * @author pansunyou@gmail.com
 * @version 1.0
 * @date 2010-12-27
*/

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
#include <cstdlib>
#include <string>
#include "clientSrcFromWSDL/soapStub.h"
#include "clientSrcFromWSDL/soapwpsoapProxy.h"
#include "clientSrcFromWSDL/wpsoap.nsmap"

using namespace std;

int main(int argc, char*argv[])
{
  wpsoap wpsoapClient;
  if (argc==2)
    wpsoapClient.endpoint = argv[1];

  //1. 登陆
  string username = "admin";
  string password = "3.14159";
  int r = 0;

  ns2__tagReqLogin req;
  req.username = username;
  req.password = password;
  _ns2__login ns2__login;
  ns2__login.req = &req;
  _ns2__tagRspLogin rsp;
  r = wpsoapClient.__ns1__login(&ns2__login, &rsp);
  if (r!=0)
  {
    fprintf(stderr, "调用soap接口失败!\n");
    return -1;
  }

  if (0!=rsp.rsp->retCode)
  {
    printf("登陆失败 retCode=%d, retMessage=%s\n",
		rsp.rsp->retCode,
		rsp.rsp->retMessage.c_str());
    return -1;
  }
  printf("登陆成功! [session=%s]\n",
	rsp.session.c_str());

  ns2__tagReqPost reqPost;
  reqPost.body = "post article by wpsoap!";
  reqPost.title = "hello, wpsoap!";

  _ns2__post ns2__post;
  ns2__post.req = &reqPost;
  _ns2__tagRspPost ns2__tagRspPost;
  r = wpsoapClient.__ns1__post(&ns2__post,
	&ns2__tagRspPost);
  if (r!=0)
  {
    fprintf(stderr, "调用soap接口失败!\n");
    return -1;
  }

  if (0!=rsp.rsp->retCode)
  {
    printf("发布日志失败 retCode=%d,
		retMessage=%s\n",
		rsp.rsp->retCode,
		rsp.rsp->retMessage.c_str());
    return -1;
  }
  printf("日志发布成功! [retMessage=%s]\n",
  rsp.rsp->retMessage.c_str());

  return 0;
}

四、参考资料

http://gsoap2.sourceforge.net/
http://www.cppprog.com/2009/0723/138.html

http://hi.baidu.com/winnyang/blog/item/d5fd4f3df38f35cd9e3d625b.html

http://www.cs.fsu.edu/~engelen/soap.html
http://tangentsoft.net/mysql++/

2010-12-13

让服务器程序优雅地退出

Filed under: C/C++ — 标签:, , — sunu @ 18:39:28

假设我们的服务器程序提供服务是在主线程外的其它线程(或者子进程)中,主线程仅做些非常简单的事情,它创建服务后就进入休眠状态,不想通过循环sleep忙等,不想被用户打扰(比如被Ctrl+C草率地干掉),又觉得可以让用户友好地不慌不尽快地关闭(还是通过Ctrl+C或者其它信号,但关闭前要做些资源收回、状态保存之类的收尾工作)。

Ctrl+C或者其它信号强制杀掉进程,服务器程序肯定来不及收尾。

当然,这些需求是可以通过让程序成为daemon来实现,但是那样主线程(实际是主进程)就不存在了。

linux下可以这样做:

pthread_mutex_t condition_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  condition_cond  = PTHREAD_COND_INITIALIZER;

void sigHandle(int sig)
{
  signal(SIGINT, SIG_IGN);
  printf("要退出咯!\n");

  //比如在这里让服务器不慌不忙地停止工作

  //可以让main函数继续了
  pthread_mutex_lock( &condition_mutex );
  pthread_cond_signal( &condition_cond );
  pthread_mutex_unlock( &condition_mutex );
}

int main(int argc, char**argv)
{
  //拦截信号,当然不止SIGINT一程
  //可通过kill -l查看
  signal(SIGINT, sigHandle); 

  //其它代码, 比如在其它线程中创建服务器...

  //停在这里了
  pthread_mutex_lock( &condition_mutex );
  pthread_cond_wait( &condition_cond, &condition_mutex );
  pthread_mutex_unlock( &condition_mutex );

  return 0;
}

windows下可以这样做:

HANDLE hEvent;
void sigHandle(int sig)
{
  signal(SIGINT, SIG_IGN);
  SetEvent(hEvent);
}

int main(int argc, char*argv[])
{
  signal(SIGINT, sigHandle);

  hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
  ResetEvent(hEvent);
  int r = WaitForSingleObject(hEvent, INFINITE);
}

2010-12-12

[zz]从”吵架”贴中加深对C++的理解

Filed under: C/C++ — 标签:, — sunu @ 15:20:49

无意中看到这篇文章:http://www.cppblog.com/converse/archive/2010/07/06/119427.html
以下文字,出自上文评论中名maxime的牛人,此处仅做为学习之用,未经牛人允许,勿怪。

 C++是一门在工业实践中成长起来的语言,工业界发明这些东西是因为需要,学院派却总跟不上进度,教材几十年一变。要用C++,就要做好准备,否则,你干嘛不用Java或者C#。
1. 关于所谓“频繁的构造/析构开销大”
 你首先要清楚“构造”和“析构”中编译器到底为你做了什么。
 1.)分配对象空间:如果是在堆中分配对象,那么会有一个代价很大的堆分配(new,在2.7G的CPU上单线程new性能是5M次/秒);如果在堆栈上分配,内存分配代价几乎为零。
 2)调用构造函数和析构函数.这有两个开销,一个是调用本身的开销,一个是函数体内部代码的开销,很明显,前者才C++带来的额外开销。我可以告诉你的是,如果是内联,这个开销为0,如果不是内联,这个开销在2.7G的CPU上单线程性能是1200M次/秒,作为类比,2.7G的CPU上单线程可以做400M次32位整型变量写入操作,也就是这个开销比写一个整型变量还小。
 现在,看看你说的情况,局部对象的构造和析构,每次的代价比写一个32位整型的变量还小得多,相比每次日志输出至少十几个字节的内存拷贝,这点开销完全可以忽略不计,除非打算每秒中打算做1M次的日志,它带来的代价不占用1%的CPU而已,不过事实是,每秒钟写不了1M次的文件IO。
 最后从设计的角度考虑这个问题,你的系统打算每秒中写多少次日志,应该心理有数吧,从这个意义上,从设计的角度,上面我写的那些分析毫无必要,只是为了加深对C++的理解,事实是,即便“频繁的构造/析构开销大”很大,它们仍然不是系统的真正瓶颈,没必要过早优化。如果它们真成了瓶颈,你应该做的事情是,调整成合理的日志策略。

2.所谓“比如log << "hello " << "world",是无法判断到底在输出"hello"还是"world"的时候上面的参数输入已经结束了”
 其实,这个问题,流的设计者早已考虑到了,std::endl就是用来干这件事情的。事实上,自定义的流操控符,还可以干很多事情比如:

std::cout << v1 << mylock(v2) << v2 << myunlock(v2); 

 上面的mylock,myunlock就是自定义的操作符,用来给v2加锁解锁,而不输出任何字符。它到底能做什么,取决于你的想象力。我总爱把C++比作机械行业的钳工,他们比不上机器的速度,但没他们不行,很多事情机器做不了。使用正确的工具做正确的事情,如果你感觉不对,先想想选对工具没,而不是抱怨工具很烂。
 额外,说明一点,有人告诉你sprintf存在写错的可能性,所以,你可以说,如果别人忘了写上他的endl怎么办?
 我来告诉你吧,写错了其实没什么大不了的,问题关键是,写错了会带来什么危害。sprintf写错了,可能带来的是内存溢出覆盖,这才是我们恐惧他的原因,一个内存溢出带来的危害我就不说了。 反之,少写了一个endl,最多就是两行日志重叠,或者一个日志输出时间晚了一会儿。如果你真看到这个情况,把endl加上去就行了。
 不知道现在是否能理解了,不要害怕bug,不要害怕写错,要怕会让你掉进深渊的bug。我得承认,这是C/C++的弱点,java/C#相对好很多。
 C++最害怕的,就是指针操作,内存覆盖可以毁掉整个程序的运行基础,却不容易找到错误的代码。但这也是C++的优点,C++为什么要用流替换C的sprintf,就是要减少内存覆盖错误的机会。当然,C++中仍然有这种错误的机会,因为抛弃了指针,C++和Java就没区别了。如果说C是做操作系统的,java是做应用的,C++就是做系统和应用结合部的,只有理解了这点,你才能用好C++,而不是抱怨,它既没C简单,也没java安全。
 事实是,C++就是这么个怪胎,比Java更快,比C更安全更有开发效率。

3. 关于“要使用这门语言写出正确的程序来,需要了解底下多少的细节呢?!”
 首先答案是,不需要知道细节,只需要知道“规范”。C++真正的问题不是太复杂,而是在实践中缺乏规范,尤其在中国的软件作坊里面。就像你会开汽车一样的,你没比要知道汽车发动机原理,同样能把汽车开好。因为你遵守了开汽车的规范,比如启动的时候,慢加油门。
 很多人的问题在于,在思想上,忽视了规范,到头来却怪东西太复杂。
 其次是了解细节,可以工作更深入。再说了,就算复杂,C++能有多复杂,一个C++语言里面能有多少东西呢?相比一个Java库,这点东西真算不了什么。很多人掌握不好,是因为没有正正经经的机会去学,去练。这点像数学,学的时候比较枯燥,不管怎么说,这点东西就叫复杂,那只能说,做的应用系统太简单。

4. 关于“假如需要考虑多线程的话,那么一次输入有多个函数函数中被调用”
 要在多线程进行IO操作,肯定是要用锁的,就算你不直接用,系统API的流API,比如Win32的WriteFile,也是要用的。
 所以,答案很简单,用锁。问题不在于有几次函数调用,而在于能否让这几次函数调用位于同一个锁当中。
 传统上,一个sprinf,你可以加一次锁,就够了。 而现在呢,分成了好几次调用,那么就在这几次调用之间和之后加锁就行了,在本例中,也就是那个被认为过于调用繁琐的临时对象了,在它的构造函数加锁,在它的析构函数中解锁,就能保证输出的原子性。如果这样还不满意,还可以考虑流操控符加锁,不过有点危险。
 不过呢,说道最后,如果你明白,那个看似效率低下的临时对象其实对整行的输出做了缓存,所以在glog中,临时对象中是没必要用锁的,因为临时对象中保存的字串是不会被多线程打断的,它能够保证所有的“<<”调用在输出上的原子性。最后析构函数中,真正进行输出时,在下层的实际输出位置,实际上是有锁。

5. 最后谈一下,C++流的真正缺点?
 从安全性的角度讲,C++流相对sprintf是一次飞跃。从实际项目来看,C++程序员的代码产出和维护量,通常会数倍甚至几十倍于C程序员,这表面了在某些问题域上,C++比更有开发效率。
 但由此带来的问题是,在代码量少的时候,C程序员可以花时间慢慢检查代码,保证sprintf没问题。而C++程序员再这样做效率就太低了。所以才会有了C++流的方案,C++流设计者正是从实践中品尝到了sprintf的苦果。
 事实是,C++语法形式,从实用性角度,的确很蹩脚。而且性能只有sprintf的1/3.不过实际环境下,性能通常不是问题,流输出很少会是一个应用系统真正的瓶颈。
 蹩脚的语法,是个问题,尤其当你需要做格式控制的时候,代码可能非常长。这个问题,我的看法是,写的时候可能多花点时间,不过以后维护起来就轻松了。毕竟,我宁愿选择安全性,花三天时间去找一个缓冲区溢出是不会宁人愉悦的。当你认为语法问题很重要时,通常暗示代码管理上有问题。我通常认为代码的书写只占20%的时间,80%时间是在维护代码。维护效率远比书写效率重要。
 在C++领域,新发明似乎是没有止境的,有一个新的,利用重载“()”操作符的格式化库出现了,具体我本人没有用过,看起来还不错,据说在性能上优于sprintf,在安全性上不输于C++流,在格式上类似sprintf。由于缺乏大规模应用,实际情况如何,还不好说。
 就我本人而言,我认为C++流的效率和格式问题,并非致命问题,所以也就不急着使用更先进的东西了,短期内我C++流仍是最好的格式化输出工具。除非,项目主要业务逻辑就是格式化字符串,那也许我会选择sprintf或者其他的东西。
 最后,我感觉楼主,似乎想在一个输出语句中,输出很长很长的,可能跨越多次物理输出的内容。
 这样做,首先代码不易理解,不易修改维护。 根据本人的实际经验来看,日志输出最好还是按实际物理行为单位比较好,所以glog没有支持所谓endl特性。
 楼主可能真正担心的是另一个问题,在多线程程环境下,想要连续输出的几行文本,会被其他线程打断,以致阅读性变差。对此,我建议,如果不希望被打断,使用glog那就需要八几行输出写在一个glog句子,作为一次原子输出就行了。但是,如果楼主对这样的原子输出,还要求再被分成多次物理输出,那这是为什么呢?有这个必要吗?既然打算连续输出几行,且在一个语句之中,整个语句时间是非常快的,对观察者而言,一次原子输出是由一次物理输出还是多次物理输出构成,没有任何实际意义。

Powered by WordPress