兴海北路

---男儿仗剑自横行
<2008年3月>
2425262728291
2345678
9101112131415
16171819202122
23242526272829
303112345

统计

  • 随笔 - 85
  • 文章 - 0
  • 评论 - 17
  • 引用 - 0

常用链接

留言簿(6)

随笔分类

随笔档案

收藏夹

全是知识啊

搜索

  •  

最新评论

阅读排行榜

评论排行榜

c++ 获取当前时间

#include <iostream>

#include <time.h>

using namespace std;

int main()

{

time_t ltime;
char tmpbuf[128];

//方法1:分别获取当前时间,日期
/* Display operating system-style date and time. */
    _strtime( tmpbuf );
    printf( "OS time:\\t\\t\\t\\t%s\\n", tmpbuf ); //打印当前时间
    _strdate( tmpbuf );
    printf( "OS date:\\t\\t\\t\\t%s\\n", tmpbuf ); //打印当前日期

//方法二:获取当前时间日期
time(&ltime); //获取从1970至今经过的秒数

cout << ctime(&ltime) << endl; //折算成当前时间日期

return 0;

}

=========================================
VC中基于 Windows 的精确定时

http://www.vckbase.com/document/viewdoc/?id=1301

-------------------------------------------------------------------------------------------------------------

//获取程序运行时间   
          long   t1=GetTickCount();//程序段开始前取得系统运行时间(ms) ;
          Sleep(500);   
          long   t2=GetTickCount();();//程序段结束后取得系统运行时间(ms)   
          str.Format("time:%dms",t2-t1);//前后之差即   程序运行时间   
          AfxMessageBox(str);  


posted @ 2008-04-08 16:00 随意门 阅读(1373) | 评论 (0)编辑 收藏
VC使用C API 连接操作MySQL数据库

一切尽在代码中,代码中也太多了if/else,可以对它进行更好的函数及至类的封装,规范的处理好异常。

  1. #include <windows.h>
  2. //需要在VC的Options设置一个include路径指向'%mysql_home%/inlude'目录   
  3. #include <mysql.h>
  4. //设置一个lib路径指向'%mysql_home%/lib/opt'目录 (mysql5.0是个目录)   
  5. #pragma comment(lib,"libmysql.lib")    
  6. #define host_name "localhost"  //数据库服务器   
  7. #define db_name "test"         //数据库名   
  8. #define user_name "root"       //用户名   
  9. #define password ""            //密码   
  10. int  main( int  argc, char  * argv[]) {   
  11.     
  12.   char  szSqlText[500] ;   
  13.     
  14.  MYSQL *conn;   
  15.  MYSQL_RES *rs;   
  16.  MYSQL_ROW row;    //注意它的声明 typedef char **MYSQL_ROW,字符串数组   
  17.   BOOL  bCreate = FALSE;   
  18.     
  19.  conn = mysql_init(NULL);   
  20.   if (conn == NULL)   
  21.  {   
  22.   fprintf(stderr, "mysql_init() failed (probably out of memory)\n" );   
  23.   exit(1);   
  24.  }   
  25.     
  26.   if  (mysql_real_connect(conn,host_name,user_name,password,   
  27.   db_name,MYSQL_PORT,NULL,0) == NULL)   
  28.  {   
  29.    //在MYSQL初始化之后的操作如果有错误,可以用mysql_errno(MYSQL*)和   
  30.    //mysql_errer(MYSQL*) 分别获得出错代号和描述   
  31.   fprintf(stderr, "mysql_real_connect() failed:\nError %u (%s)\n" ,   
  32.    mysql_errno(conn),mysql_error(conn));   
  33.   exit(1);   
  34.  }   
  35.     
  36.  printf( "connect to db successful.\n" );   
  37.   if  (bCreate) {   
  38.    //第一次运行创建一个表mytable   
  39.   sprintf(szSqlText, "create table mytable(time datetime,s1 char(6),s2 char(11),s3 int,s4 int)" );   
  40.    if  (mysql_query(conn,szSqlText)) {   
  41.    printf( "Can't create table.\n" );   
  42.    mysql_close(conn);   
  43.     return  0;   
  44.   }   
  45.  }   
  46.  sprintf(szSqlText, "insert into mytable values('2000-3-10 21:01:30','Test','MySQLTest',2000,3)" );   
  47.   if  (mysql_query(conn,szSqlText)) {   
  48.   printf( "Insert values error:\nError %u (%s)\n" ,   
  49.    mysql_errno(conn),mysql_error(conn));   
  50.   mysql_close(conn);   
  51.    return  0;   
  52.  }   
  53.   else {   
  54.    //insert/delete/update 语句可用mysql-affected_rows()得到受作用的行   
  55.   printf( "INSERT statement succeeded: %lu rows affected\n" ,   
  56.    (unsigned  long )mysql_affected_rows(conn));   
  57.  }   
  58.     
  59.   //查询数据   
  60.  sprintf(szSqlText, "select * from mytable" );   
  61.     
  62.   //执行成功则返回零   
  63.   if  (mysql_query(conn,szSqlText) != 0) {   
  64.   mysql_close(conn);   
  65.    return  0;   
  66.  }   
  67.   else  {   
  68.    //立即从服务器返回所有行,存储到本地,产生结果集,失败则返回NULL   
  69.   rs = mysql_store_result(conn);   
  70.      
  71.    //结果集是保留在服务器上,fetch_row时才逐行从服务器上取   
  72.    //rs = mysql_use_result(conn);   
  73.    //Get query result.   
  74.    int  count = ( int )mysql_num_rows(rs);   
  75.   printf( "Query: %s.\n%ld records found.\n" ,szSqlText,count);   
  76.    //MYSQL_ROW是一个指向数值数组的指针,row[0],row[1]...row[列数-1]   
  77.    //所有的数据类型都以字符串返回,即使是数字型,要进行串转换   
  78.    //NULL指针代表数据库字段的NULL,应经常检查列是否为NULL   
  79.    while ((row = mysql_fetch_row(rs)) != NULL){   //返回NULL,则说明不再有行   
  80.     for (unsigned  int  i=0; i<mysql_num_fields(rs);i++){   
  81.      if (i>0)   
  82.     fputc('\t',stdout);   
  83.     printf( "%s" ,row[i]!=NULL ? row[i]: "NULL" );   
  84.    }   
  85.    fputc('\n',stdout);   
  86.   }   
  87.    //使用完后,释放结果集占用内存   
  88.   mysql_free_result(rs);   
  89.  }   
  90.  mysql_close(conn);   
  91.   return  0;   
  92. }   
  93. //最后,注意查询语句中字符串的转义 select a from t where a=''1',是要出错的   
  94. //空字符转换为'\0',这里的0 是可打印的ASCII 码0,而不是空。   
  95. //反斜线、单引号和双引号分别转换为‘\\’、‘\'’ 和‘\"’   
  96. //你也可以用mysql_escape_string(to_str,from_str,from_len)转换sql语句   
输出结果是:
connect to db successful.
INSERT statement succeeded: 1 rows affected
Query: select * from mytable.
8 records found.
2000-03-10 21:01:30     Test    MySQLTest       2000    3
2000-03-10 21:01:30     Test    MySQLTest       2000    3
2000-03-10 21:01:30     Test    MySQLTest       2000    3
2000-03-10 21:01:30     Test    MySQLTest       2000    3
2000-03-10 21:01:30     Test    MySQLTest       2000    3
Press any key to continue

posted @ 2008-04-08 14:50 随意门 阅读(626) | 评论 (0)编辑 收藏
XSL:转换从哪里开始?

前言

爱好XML的人最终会试着将XML转换为HTML,或者转换为其他类型的文档,DOM/SAX显然不是专门为转换设计的,CSS对于转换也是力有不逮,所以XML的爱好者们几乎无一例外的要遭遇XSL,但是XSL似乎有非常多的用法,对于XML仅仅只是表示格式化的数据而言,XSL显得复杂且毫无头绪。

例如《跟我学XSL》和《XSL基础入门》这样的教程会带给你XSL的一些概念和例子,但是对于XSL的运行环境、平台特性和本质,似乎都语焉不详,你最终学会的仅仅是在XMLSPY或者IE中打开你的XML看看它转换后的效果罢了。一有人提到脚本语言或者JAVA中调用XSL你就头大了,甚至你不清楚XSL和XSLT究竟有什么区别。迷失在网络中的人们喜欢不停的用google搜索你想要的中文资料,但是其实有那个时间,干脆去那种技术的官方网站上好好看看吧。http://www.w3.org/Style/XSL/是XSL技术的W3C的官方网站,在网页正文的第一行它就解释和XSL和XSLT的区别。原文如下:

XSL is a family of recommendations for defining XML document transformation and presentation. It consists of three parts:

XSL Transformations (XSLT)

a language for transforming XML

the XML Path Language (XPath)

an expression language used by XSLT to access or refer to parts of an XML document. (XPath is also used by the XML Linking specification)

XSL Formatting Objects (XSL-FO)

an XML vocabulary for specifying formatting semantics

XSL是一组定义XML文档的转换和显示特征的推荐标准,它包括三个部分:XSL转换(XSLT)是一种为了转换XML而定义的语言;XML路径语言(XPath)是一种表达式语言,它被XSLT用来访问或者提交一个XML文档的某些部分(XPath也同时被XML Linking标准使用);XSL格式化对象(XSL-FO)是一个XML词汇表用来定义XML的格式化语义。

从何开始

一般人学习XSL都是从XMLSPY等工具开始运行他的一个XSL例子,当然用文本编辑器编辑XML何XSL文件,用IE去打开XML也是一个好主意。因为XMLSPY和IE都有嵌入式的XSL解析器,例如IE的XSL解析器是MSXML,这样不用显式的调用XSL进行转换过程,只需要在XML文档的头部加上一句<?xml:stylesheet type="text/xsl" href="xxx.xsl"?>就可以让嵌入的XSL解析器自动的进行转换了。例如下面这个著名的例子,它包括cd_catalog.xml和cd_catalog.xsl文件,内容如下:

xml文件:

<?xml version="1.0" encoding="GB2312"?>

<?xml:stylesheet type="text/xsl" href="cd_catalog.xsl"?>

<CATALOG>

   <CD>

       <TITLE>Empire Burlesque</TITLE>

       <ARTIST>Bob Dylan</ARTIST>

       <COUNTRY>USA</COUNTRY>

       <COMPANY>Columbia</COMPANY>

       <PRICE>10.90</PRICE>

       <YEAR>1985</YEAR>

   </CD>

   <CD>

       <TITLE>喀什噶尔胡杨</TITLE>

       <ARTIST>刀郎</ARTIST>

       <COUNTRY>China</COUNTRY>

       <COMPANY>先之唱片</COMPANY>

       <PRICE>20.60</PRICE>

       <YEAR>2004</YEAR>

   </CD>

   <CD>

       <TITLE>敦煌(特别版)</TITLE>

       <ARTIST>女子十二乐坊</ARTIST>

       <COUNTRY>China</COUNTRY>

       <COMPANY>百代唱片</COMPANY>

       <PRICE>25.60</PRICE>

       <YEAR>2005</YEAR>

   </CD>

</CATALOG>

 

xsl文件:

<?xml version="1.0" encoding="GB2312"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">

   <xsl:template match="/">

       <html>

          <body>

             <table border="2" bgcolor="yellow">

                 <tr>

                    <th>Title</th>

                    <th>Artist</th>

                 </tr>

                 <xsl:for-each select="CATALOG/CD">

                    <tr>

                        <td>

                           <xsl:value-of select="TITLE"/>

                       </td>

                       <td>

                           <xsl:value-of select="ARTIST"/>

                       </td>

                    </tr>

                 </xsl:for-each>

             </table>

          </body>

       </html>

   </xsl:template>

</xsl:stylesheet>

将它们保存在同一目录下然后用IE5以上版本的IE直接打开xml文件,则会看到转换后的效果。当然用XMLSPY中自带的浏览器也可。

JScript显式调用XSL解析器

上面的运行方法显然是“贪天之功”,利用了IE和XMLSPY自带的XSL解析器,是让一只看不见的手运行了转换过程。那么,也可以用Jscript语言显式的调用XSL解析器,让没有嵌入解析器的浏览器也可以运行XSL,当然,此浏览器必须支持Jscript脚本语言。我们还是使用上面的例子,不过将cd_catalog.xml中的<?xml:stylesheet type="text/xsl" href="cd_catalog.xsl"?>这一行去掉,同时新建一个cd_catalog.html文档,内容如下:

   <html>

    <body>

    <script language="javascript">

    // Load XML

    var xml = new ActiveXObject("Microsoft.XMLDOM")

    xml.async = false

    xml.load("cd_catalog.xml")

    // Load the XSL

    var xsl = new ActiveXObject("Microsoft.XMLDOM")

    xsl.async = false

    xsl.load("cd_catalog.xsl")

    // Transform

    document.write(xml.transformNode(xsl))

    </script>

    </body>

</html>

将此html文档在支持Jscript的浏览器中打开,即可看到如前一段执行的结果。当然不仅仅是Jscript,其他的脚本语言如VBScript等等也可以,不过Jscript是XSL默认的脚本语言。

脚本扩充的XSL,令人疑惑的xsl:eval标记

    xsl:eval标记并不是一个标准的xsl标记,它属于http://www.w3.org/TR/WD-xsl这个名字空间,这个名字空间最终被微软采用,于是xsl:eval也被微软用来调用Jscript脚本,以此来扩充XSL的功能。而标准的XSL1.0版本的名字空间是http://www.w3.org/1999/XSL/Transform,它并不包含xsl:eval标记,这是很容易理解的,XSL应该属于一个平台无关的技术,如果它的某个标记要依赖微软公司的产品,那显然是自掘坟墓。关于平台无关的讨论,将在本文的最后展开。

    xsl:eval标记的含义是计算其中脚本语言的表达式,并作为文本输出。下面的例子中计算了cd_catalog.xml中各种CD的总价格,修改上面的cd_catalog.xsl并另存为cd_catalog2.xsl文件如下:

<?xml version="1.0" encoding="GB2312"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/TR/WD-xsl">

   <xsl:template match="/">

       <html>

          <body>

             <table border="2" bgcolor="yellow">

                 <tr>

                    <th>Title</th>

                    <th>Artist</th>

                 </tr>

                 <xsl:for-each select="CATALOG/CD">

                    <tr>

                       <td>

                           <xsl:value-of select="TITLE"/>

                       </td>

                       <td>

                           <xsl:value-of select="ARTIST"/>

                       </td>

                    </tr>

                 </xsl:for-each>

                 <tr>

                    <td>合计</td>

                    <td>

                       <xsl:eval>total("PRICE")</xsl:eval>

                    </td>

                    <xsl:script>

                       function total(q){

                            temp=0;

                            mark='/CATALOG/CD/'+q;

                            v=selectNodes(mark);

                            for(t=v.nextNode();t;t=v.nextNode()){

                                 temp+=Number(t.text);

                            }

                            return temp;

                      }

                    </xsl:script>

                 </tr>

             </table>

          </body>

       </html>

   </xsl:template>

</xsl:stylesheet>

在IE中打开cd_catalog.xml文件(注意修改xsl为cd_catalog2.xsl)即可看到结果,注意这个xsl文件的这一行<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/TR/WD-xsl">,写错了名字空间xsl:eval标记就会报错。

浏览器无关的XSL解决方案,服务端的XSL

不管如何折腾,要将XML通过XSL转换为HTML必须要求本地主机上有一个XSL解析器,不管是浏览器内嵌的,还是可以通过脚本语言调用。那么,更好的解决方案当然是从服务器端直接发送HTML回来,这样无论什么浏览器都可以看到转换的结果了。ASP提供了这个功能,这是可想而知的,不过我对ASP不熟,这段略过,有兴趣的可以找本ASP的XML教材看看。

应用程序中的XSL,语言相关的XSL

众所周知,Java是对XML技术支持得最好的语言,Java上面的xml包非常多,其中支持XSL转换的包最著名的有Saxon和xalan。Saxon包可以在http://saxon.sourceforge.net/上面下载。将Saxon包解压缩到C:\saxon6_5_3,6.5.3版本提供了对XSL1.0最稳定的支持。然后在Classpath中加入C:\saxon6_5_3\saxon.jar;C:\saxon6_5_3\saxon-jdom.jar。

Saxon提供命令行式的XSL转换和API。其中命令行式的转换如下,将目录移动到存放xml(去掉xml的指定xsl的那一行)和xsl的目录,然后输入下面的命令:

java com.icl.saxon.StyleSheet cd_catalog.xml cd_catalog.xsl

就可以看到输出在屏幕上的结果,但是这样看起来不方便,所以输入如下命令:

java com.icl.saxon.StyleSheet cd_catalog.xml cd_catalog.xsl>a.html

然后将生成的a.html在浏览器中打开,可以清晰的看到结果。

下面是在Java程序中调用Saxon包,进行XSL转换的例子,文件名为XslExam.java:

import java.io.File;

import javax.xml.transform.Result;

import javax.xml.transform.Source;

import javax.xml.transform.Templates;

import javax.xml.transform.Transformer;

import javax.xml.transform.TransformerConfigurationException;

import javax.xml.transform.TransformerException;

import javax.xml.transform.sax.SAXSource;

import javax.xml.transform.stream.StreamResult;

import com.icl.saxon.ExtendedInputSource;

import com.icl.saxon.TransformerFactoryImpl;

 

public class XSLExam {

      public static void main(String[] args) {

        String sourceFileName = "cd_catalog.xml";

        String styleFileName = "cd_catalog.xsl";

        String outputFileName = "result.html";

        File sourceFile = null;

        File styleFile = null;

        File outputFile = null;

       

        TransformerFactoryImpl factory = new TransformerFactoryImpl();

       

        Source sourceInput = null;

        sourceFile = new File(sourceFileName);

        ExtendedInputSource eis = new ExtendedInputSource(sourceFile);

        sourceInput = new SAXSource(factory.getSourceParser(), eis);

        eis.setEstimatedLength((int)sourceFile.length());

       

        Source styleSource ;

        File sheetFile = new File(styleFileName);

        eis = new ExtendedInputSource(sheetFile);

        styleSource = new SAXSource(factory.getStyleParser(), eis);

       

        outputFile=new File(outputFileName);

       

             try {

                     Templates sheet = factory.newTemplates(styleSource);

                     Transformer instance = sheet.newTransformer();

               Result result = new StreamResult(outputFile);

               instance.transform(sourceInput, result);

              

              } catch (TransformerConfigurationException e) {

                     e.printStackTrace();

              }catch (TransformerException err) {

                     err.printStackTrace();

        }

       }

}

这个例子程序将cd_catalog.xml文件使用cd_catalog.xsl转换为result.html。在Eclipse3.01中调试通过(Saxon没有简单的xsl示例程序,我也是将com.icl.saxon.StyleSheet类拔光了才得到这个稍微简单的例子,如果需要更详细的用法,参考com.icl.saxon.StyleSheet类)。

数据是独立的,处理是平台相关的

    总结前面的内容,可以看出XSL转换可以从这几个地方开始:

Ø         IE,XMLSPY:嵌入的解析器,例如MSXML3;

Ø         JScript,显式调用XSL解析器;

Ø         用JScript扩充XSL功能,半吊子的XSL;

Ø         浏览器无关的XSL解决方案,服务器端的XSL,ASP显式调用XSL;

Ø         语言相关的XSL,Java的XSL包Saxon,xalan。

可以看出来,XSL无论如何,都是要平台相关的,第一种方法依赖嵌入浏览器的XSL解析器;第二、三种方法依赖操作系统安装的XSL解析器;第四种方法依赖服务器端安装的XSL解析器;最后的方法依赖JAVA语言提供的XSL API。其中微软还不顾W3C的反对,自定义了XSL的脚本扩充功能,功能倒是强大了,可惜脱离了Windows就玩不转了。JAVA号称平台无关,可是JAVA本身就是一个平台,要是有人的机器没有JRE又怎么办呢?丢弃XSL?

不过事物总是有因果的,其实XML作为数据的存储载体,可以做到完全的平台无关,但是XSL作为一个可执行的语言,一定要依赖某种已存在的运行环境的,就如同数据库中的表格和SQL语言一样。SQL号称适用于任何关系数据库,但是实际上还是需要一个环境来run的。那么XSL是否破坏了XML的平台无关性呢?我认为没有,因为XSL本身是一个XML文档,XML文档可以平台无关的保存和传输,至于使用何种方法来调用它则是另外考虑的问题。再者,XSL的源和目标都是平台无关的文档(例如XML和HTML),而它自己的调用方式则是可替换的,这点也减轻了XSL的负罪感吧。

以上的讨论都是基于XSL1.0标准的,目前XSL2.0标准尚在讨论中,不过初稿已经发布了,而Saxon8.0以上的版本号称已经支持了XSL2.0。让我们拭目以待XSL2.0带给我们的惊喜。

参考文献

W3C站点:http://www.w3.org/Style/XSL/

XSL主题:http://www-900.ibm.com/developerWorks/cn/xml/theme/x-xsl.shtml

中文译文站点:http://www.opendl.com/

XSLT是什么类型的语言,SAXON的作者谈XSLhttp://www-900.ibm.com/developerWorks/cn/xml/x-xslt/index.shtml

posted @ 2008-04-01 17:07 随意门 阅读(290) | 评论 (0)编辑 收藏
XML的本质讨论

(这里的XML不仅仅指XML脚本语言,还包括XML的一系列技术,包括DTD,XSLT,XML SCHEMA,XPATH,DOM,SAX等等)

XML的本质是什么?这个问题对于很多XML的初学者来说都不容易回答。因为XML涉及的方面太多,有人是为了写出更漂亮的网页才从HTML进一步学到XML;有人是为了学JAVA才来了解XML;有人是从数据库到XML;有人是从UML到XML;当然还有一些人是从SOAP或者其他网络协议而了解到XML。那么到底如何解释XML的本质呢?

我认为XML的本质是数据,XML文档实际上是对数据的格式化存储,而XML的一系列技术都是围绕着数据来发展的。例如DTD、Schema是对数据格式的定义和检验;XSLT是对数据的转换;DOM、SAX是对数据的提取和操作。

既然XML只是数据,而且是用文本形式存储的数据,那么为什么不更简单的用普通文本来存储数据呢?早期的一些程序员确实是这么做的,但是这么做的缺点是对于每一组数据,都需要专用的数据格式定义、检验、转换和操作的程序。如果使用XML来存储数据,由于XML的一系列技术已经对以上的问题提供了工具,我们只需要使用那些技术即可快捷的达到自己的目的。有人可能会说,使用数据库不是更方便么?它也提供了以上的功能。确实如此,但是并不是每个地方都适用数据库的,如果说数据库是大而全的数据解决方案的话,XML可以用“举重若轻,大象无形”来形容,这一点后面再讨论。

XML的本质决定了它在网页制作方面比HTML更具有优越性。传统的网页包括HTML+CSS,在这种模式中,数据和数据的显示特性都包含在HTML中,CSS只是对显示特性的一种补充;而XML网页包括XML+XML Schema+XSL,其中XML存储数据,XML Schema定义了数据的存储格式,XSL定义了数据的显示特性(其实它定义了如何将XML转换为HTML,实际上就是定义了数据的显示特性)。使用XML制作的网页将数据、数据格式和显示特性清晰的分为三个部分,在添加或者修改网页的时候可以单独的修改每个部分,从而得到更好的维护性和更高的制作效率。当然动态网页可以由Database+脚本语言(JSP、ASP、PHP)+HTML+CSS组成;同样基于XML的动态网页可以由Database+中间程序(提取数据库内容形成XML文档)+XML+XML Schema+XSL组成。基于XML的解决方案同样保持了层次清晰的优点。

对于XML和数据库的比较,我的上一篇文章中有过讨论()。从本质上来说,XML和数据的本质差不多,都是围绕着数据来提供一系列的解决方案,但是它们之间存在几个显著的不同:1.XML是轻量级的数据解决方案,容易学习,可以用文本编辑器进行编辑,一般的浏览器都支持XSLT,适用于数据量小的各种环境;2.XML是完全平台无关的,不需要依赖于特定的操作系统、浏览器或者编程语言,而数据库不是完全平台无关的;3.XML是基于文本的,适合于网络传输,你不能指望每个EJB的配置文档都用数据库来表示吧;4.XML和数据库是可以互相结合和转换的。

对数据的不同理解可以将XML应用到不同的方面。你可以这样理解:XML是数据库中的数据;Schema是数据库的表;XSL是显示数据的程序;也可以这么理解:XML是网页素材;Schema是素材的数据结构;XSL是素材的显示特性。同样,在软件建模方面,也可以用XML来替代UML。这是基于这么一种理解:Schema代表类图,它如同UML一样存储了类的结构特性;XML代表对象,它存储了类的实例化对象的属性数据;而XSL是对类图的转换,即MDA(Model Driven Architecture,模型驱动架构)中的提到的模型转换。在UML中没有模型转换技术,但是一些UML工具提供了代码生成的功能(例如RationalRose),这中功能可以理解为模型转换的一个特例。因此有人提出了用XSLT做代码生成的建议,事实上这种代码生成技术已经比较成熟。由于XML Schema并不是天生就用来刻划类图的,所以它在类的继承等方面存在一些不足之处,为了修正这些不足,OMG(Object Manage Group,对象管理组)组织提出了XMI(XML Metadata Interchange,XML元数据交换)标准,用来补充XML Schema在软件建模方面的不足。现在XMI已经变成了各种软件建模工具的通用存储方式,可以将不同建模工具建立的模型互相转换。

总的来说,XML是一种基于文本的、格式化的数据存储技术,它包括一系列的数据解决方案,它们是轻量级的、易于学习的、平台无关的数据解决方案。弄清楚了这个概念再去学习XML,也许更有帮助。

posted @ 2008-04-01 17:06 随意门 阅读(138) | 评论 (0)编辑 收藏
Socket编程指南及示例程序

例子代码就在我的博客中,包括六个UDP和TCP发送接受的cpp文件,一个基于MFC的局域网聊天小工具工程,和此小工具的所有运行时库、资源和执行程序。代码的压缩包位置是http://www.blogjava.net/Files/wxb_nudt/socket_src.rar

1         前言

在一些常用的编程技术中,Socket网络编程可以说是最简单的一种。而且Socket编程需要的基础知识很少,适合初学者学习网络编程。目前支持网络传输的技术、语言和工具繁多,但是大部分都是基于Socket开发的,虽说这些“高级”的网络技术屏蔽了大部分底层实现,号称能极大程度的简化开发,而事实上如果你没有一点Socket基础,要理解和应用这些技术还是很困难的,而且会让你成为“半瓢水”。

深有感触的是当年我学习CORBA的时候,由于当时各方面的基础薄弱,整整啃了半年书,最终还是一头雾水。如果现在让我带一个人学CORBA,我一定会安排好顺序:首先弄清C++语法;然后是VC编译环境或者nmake的用法;接下来学习一些网络基础知识;然后是Socket编程;这些大概要花费3、4个月。有了这些基础学习CORBA一周即可弄懂,两个月就可以基于CORBA进行开发了。

好了,说了半天其实中心思想就一个,Socket很简单,很好学!如果你会C++或者JAVA,又懂一点点网络基础如TCP和UDP的机制,那么你看完本文就可以熟练进行Socket开发了。

2         Socket简介(全文摘抄)

(本节内容全部抄自网络,不保证正确性,有兴趣的可以看看!)

80年代初,美国政府的高级研究工程机构(ARPA)给加利福尼亚大学Berkeley分校提供了资金,让他们在UNIX操作系统下实现TCP/IP协议。在这个项目中,研究人员为TCP/IP网络通信开发了一个API(应用程序接口)。这个API称为Socket接口(套接字)。今天,SOCKET接口是TCP/IP网络最为通用的API,也是在INTERNET上进行应用开发最为通用的API。

90年代初,由Microsoft联合了其他几家公司共同制定了一套WINDOWS下的网络编程接口,即WindowsSockets规范。它是BerkeleySockets的重要扩充,主要是增加了一些异步函数,并增加了符合Windows消息驱动特性的网络事件异步选择机制。WINDOWSSOCKETS规范是一套开放的、支持多种协议的Windows下的网络编程接口。从1991年的1.0版到1995年的2.0.8版,经过不断完善并在Intel、Microsoft、Sun、SGI、Informix、Novell等公司的全力支持下,已成为Windows网络编程的事实上的标准。目前,在实际应用中的WINDOWSSOKCETS规范主要有1.1版和2.0版。两者的最重要区别是1.1版只支持TCP/IP协议,而2.0版可以支持多协议。2.0版有良好的向后兼容性,任何使用1.1版的源代码,二进制文件,应用程序都可以不加修改地在2.0规范下使用。

SOCKET实际在计算机中提供了一个通信端口,可以通过这个端口与任何一个具有SOCKET接口的计算机通信。应用程序在网络上传输,接收的信息都通过这个SOCKET接口来实现。在应用开发中就像使用文件句柄一样,可以对SOCKET句柄进行读,写操作。

3         再说两句

网上很多文章对于Socket的来龙去脉有如教科书一般的精准。但是涉及具体编程技术就往往被VC等集成开发环境所毒害了,把Windows SDK、MFC、Socket、多线程、DLL以及编译链接等等技术搅合在一起煮成一锅夹生饭。

既然要学习Socket,就应该用最简单直白的方式把Socket的几个使用要点讲出来。我认为程序员最关心的有以下几点,按照优先级排列如下:

1.         Socket的机制是什么?

2.         用C/C++写Socket需要什么头文件、库文件、DLL,它们可以由谁提供,安装后一般处于系统的哪个文件夹内?

3.         编写Socket程序需要的编程基础是什么?

4.         Socket库内最重要的几个函数和数据类型是什么?

5.         两个最简单的例子程序;

6.         一个贴近应用的稍微复杂的Socket应用程序。

我将一一讲述这些要点,并给出从简到繁,从朴素到花哨的所有源代码以及编译链接的命令。

4         Socket的机制是什么?

我们可以简单的把Socket理解为一个可以连通网络上不同计算机程序之间的管道,把一堆数据从管道的A端扔进去,则会从管道的B端(也许同时还可以从C、D、E、F……端冒出来)。管道的端口由两个因素来唯一确认,即机器的IP地址和程序所使用的端口号。IP地址的含义所有人都知道,所谓端口号就是程序员指定的一个数字,许多著名的木马程序成天在网络上扫描不同的端口号就是为了获取一个可以连通的端口从而进行破坏。比较著名的端口号有http的80端口和ftp的21端口(我记错了么?)。当然,建议大家自己写程序不要使用太小的端口号,它们一般被系统占用了,也不要使用一些著名的端口,一般来说使用1000~5000之内的端口比较好。

Socket可以支持数据的发送和接收,它会定义一种称为套接字的变量,发送数据时首先创建套接字,然后使用该套接字的sendto等方法对准某个IP/端口进行数据发送;接收端也首先创建套接字,然后将该套接字绑定到一个IP/端口上,所有发向此端口的数据会被该套接字的recv等函数读出。如同读出文件中的数据一样。

5         所需的头文件、库文件和DLL

对于目前使用最广泛的Windows Socket2.0版本,所需的一些文件如下(以安装了VC6为例说明其物理位置):

l         头文件winsock2.h,通常处于C:"Program Files"Microsoft Visual Studio"VC98"INCLUDE;查看该头文件可知其中又包含了windows.h和pshpack4.h头文件,因此在windows中的一些常用API都可以使用;

l         库文件Ws2_32.lib,通常处于C:"Program Files"Microsoft Visual Studio"VC98"Lib;

l         DLL文件Ws2_32.dll,通常处于C:"WINDOWS"system32,这个是可以猜到的。

6         编写Socket程序需要的编程基础

在开始编写Socket程序之前,需要以下编程基础:

l         C++语法;

l         一点点windows SDK的基础,了解一些SDK的数据类型与API的调用方式;

l         一点点编译、链接和执行的技术;知道cl和link的最常用用法即可。

7         UDP

用最通俗的话讲,所谓UDP,就是发送出去就不管的一种网络协议。因此UDP编程的发送端只管发送就可以了,不用检查网络连接状态。下面用例子来说明怎样编写UDP,并会详细解释每个API和数据类型。

7.1 UDP广播发送程序

下面是一个用UDP发送广播报文的例子。

#include <winsock2.h>

#include <iostream.h>

void main()

{

    SOCKET sock;   //socket套接字

    char szMsg[] = "this is a UDP test package";//被发送的字段

    //1.启动SOCKET库,版本为2.0

    WORD wVersionRequested;

    WSADATA wsaData;

    int err;  

    wVersionRequested = MAKEWORD( 2, 0 ); 

    err = WSAStartup( wVersionRequested, &wsaData );

    if ( 0 != err ) //检查Socket初始化是否成功

    {

       cout<<"Socket2.0初始化失败,Exit!";

       return;

    }

    //检查Socket库的版本是否为2.0

    if (LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 0 )

    {

       WSACleanup( );

       return;

    }

    //2.创建socket,

    sock = socket(

       AF_INET,           //internetwork: UDP, TCP, etc

       SOCK_DGRAM,        //SOCK_DGRAM说明是UDP类型

       0                  //protocol

       );

    if (INVALID_SOCKET == sock ) {

       cout<<"Socket 创建失败,Exit!";

       return;

    }

    //3.设置该套接字为广播类型,

    bool opt = true;

    setsockopt(sock, SOL_SOCKET, SO_BROADCAST, reinterpret_cast<char FAR *>(&opt), sizeof(opt));

    //4.设置发往的地址

    sockaddr_in addrto;            //发往的地址 

    memset(&addrto,0,sizeof(addrto));

    addrto.sin_family = AF_INET;               //地址类型为internetwork

    addrto.sin_addr.s_addr = INADDR_BROADCAST; //设置ip为广播地址

    addrto.sin_port = htons(7861);             //端口号为7861

    int nlen=sizeof(addrto);

    unsigned int uIndex = 1;

    while(true)

    {

       Sleep(1000); //程序休眠一秒

       //向广播地址发送消息

       if( sendto(sock, szMsg, strlen(szMsg), 0, (sockaddr*)&addrto,nlen)

           == SOCKET_ERROR )

           cout<<WSAGetLastError()<<endl;

       else

           cout<<uIndex++<<":an UDP package is sended."<<endl;

    }

    if (!closesocket(sock)) //关闭套接字

    {

       WSAGetLastError();

       return;

    }

    if (!WSACleanup())       //关闭Socket库

    {

       WSAGetLastError();

       return;

    }  

}

编译命令:

CL /c UDP_Send_Broadcast.cpp

链接命令(注意如果找不到该库,则要在后面的/LIBPATH参数后加上库的路径):

link UDP_Send_Broadcast.obj ws2_32.lib

执行命令:

D:"Code"成品代码"Socket"socket_src>UDP_Send_Broadcast.exe

1:an UDP package is sended.

2:an UDP package is sended.

3:an UDP package is sended.

4:an UDP package is sended.

^C

下面一一解释代码中出现的数据类型与API函数。有耐心的可以仔细看看,没耐心的依葫芦画瓢也可以写程序了。

7.2 SOCKET类型

SOCKET是socket套接字类型,在WINSOCK2.H中有如下定义:

typedef unsigned int    u_int;

typedef u_int           SOCKET;

可知套接字实际上就是一个无符号整型,它将被Socket环境管理和使用。套接字将被创建、设置、用来发送和接收数据,最后会被关闭。

7.3 WORD类型、MAKEWORD、LOBYTE和HIBYTE宏

WORD类型是一个16位的无符号整型,在WTYPES.H中被定义为:

typedef unsigned short WORD;

其目的是提供两个字节的存储,在Socket中这两个字节可以表示主版本号和副版本号。使用MAKEWORD宏可以给一个WORD类型赋值。例如要表示主版本号2,副版本号0,可以使用以下代码:

WORD wVersionRequested;

wVersionRequested = MAKEWORD( 2, 0 ); 

注意低位内存存储主版本号2,高位内存存储副版本号0,其值为0x0002。使用宏LOBYTE可以读取WORD的低位字节,HIBYTE可以读取高位字节。

7.4 WSADATA类型和LPWSADATA类型

WSADATA类型是一个结构,描述了Socket库的一些相关信息,其结构定义如下:

typedef struct WSAData {

        WORD                    wVersion;

        WORD                    wHighVersion;

        char                    szDescription[WSADESCRIPTION_LEN+1];

        char                    szSystemStatus[WSASYS_STATUS_LEN+1];

        unsigned short          iMaxSockets;

        unsigned short          iMaxUdpDg;

        char FAR *              lpVendorInfo;

} WSADATA;

typedef WSADATA FAR *LPWSADATA;

值得注意的就是wVersion字段,存储了Socket的版本类型。LPWSADATA是WSADATA的指针类型。它们不用程序员手动填写,而是通过Socket的初始化函数WSAStartup读取出来。

7.5 WSAStartup函数

WSAStartup函数被用来初始化Socket环境,它的定义如下:

int PASCAL FAR WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData);

其返回值为整型,调用方式为PASCAL(即标准类型,PASCAL等于__stdcall),参数有两个,第一个参数为WORD类型,指明了Socket的版本号,第二个参数为WSADATA类型的指针。

若返回值为0,则初始化成功,若不为0则失败。

7.6 WSACleanup函数

这是Socket环境的退出函数。返回值为0表示成功,SOCKET_ERROR表示失败。

7.7 socket函数

socket的创建函数,其定义为:

SOCKET PASCAL FAR socket (int af, int type, int protocol);

第一个参数为int af,代表网络地址族,目前只有一种取值是有效的,即AF_INET,代表internet地址族;

第二个参数为int type,代表网络协议类型,SOCK_DGRAM代表UDP协议,SOCK_STREAM代表TCP协议;

第三个参数为int protocol,指定网络地址族的特殊协议,目前无用,赋值0即可。

返回值为SOCKET,若返回INVALID_SOCKET则失败。

7.8 setsockopt函数

这个函数用来设置Socket的属性,若不能正确设置socket属性,则数据的发送和接收会失败。定义如下:

int PASCAL FAR setsockopt (SOCKET s, int level, int optname,

                           const char FAR * optval, int optlen);

其返回值为int类型,0代表成功,SOCKET_ERROR代表有错误发生。

第一个参数SOCKET s,代表要设置的套接字;

第二个参数int level,代表要设置的属性所处的层次,层次包含以下取值:SOL_SOCKET代表套接字层次;IPPROTO_TCP代表TCP协议层次,IPPROTO_IP代表IP协议层次(后面两个我都没有用过);

第三个参数int optname,代表设置参数的名称,SO_BROADCAST代表允许发送广播数据的属性,其它属性可参考MSDN;

第四个参数const char FAR * optval,代表指向存储参数数值的指针,注意这里可能要使用reinterpret_cast类型转换;

第五个参数int optlen,代表存储参数数值变量的长度。

7.9 sockaddr_in、in_addr类型,inet_addr、inet_ntoa函数

sockaddr_in定义了socket发送和接收数据包的地址,定义:

struct sockaddr_in {

        short   sin_family;

        u_short sin_port;

        struct in_addr sin_addr;

        char    sin_zero[8];

};

其中in_addr的定义如下:

struct in_addr {

        union {

                struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;

                struct { u_short s_w1,s_w2; } S_un_w;

                u_long S_addr;

        } S_un;

首先阐述in_addr的含义,很显然它是一个存储ip地址的联合体(忘记union含义的请看c++书),有三种表达方式:

第一种用四个字节来表示IP地址的四个数字;

第二种用两个双字节来表示IP地址;

第三种用一个长整型来表示IP地址。

给in_addr赋值的一种最简单方法是使用inet_addr函数,它可以把一个代表IP地址的字符串赋值转换为in_addr类型,如

addrto.sin_addr.s_addr=inet_addr("192.168.0.2");

本例子中由于是广播地址,所以没有使用这个函数。其反函数是inet_ntoa,可以把一个in_addr类型转换为一个字符串。

sockaddr_in的含义比in_addr的含义要广泛,其各个字段的含义和取值如下:

第一个字段short   sin_family,代表网络地址族,如前所述,只能取值AF_INET;

第二个字段u_short sin_port,代表IP地址端口,由程序员指定;

第三个字段struct in_addr sin_addr,代表IP地址;

第四个字段char    sin_zero[8],很搞笑,是为了保证sockaddr_in与SOCKADDR类型的长度相等而填充进来的字段。

以下代表指明了广播地址,端口号为7861的一个地址:

    sockaddr_in addrto;            //发往的地址 

    memset(&addrto,0,sizeof(addrto));

    addrto.sin_family = AF_INET;               //地址类型为internetwork

    addrto.sin_addr.s_addr = INADDR_BROADCAST; //设置ip为广播地址

    addrto.sin_port = htons(7861);             //端口号为7861

7.10           sockaddr类型

sockaddr类型是用来表示Socket地址的类型,同上面的sockaddr_in类型相比,sockaddr的适用范围更广,因为sockaddr_in只适用于TCP/IP地址。Sockaddr的定义如下:

struct sockaddr {

 u_short    sa_family;

 char       sa_data[14];

};  

可知sockaddr有16个字节,而sockaddr_in也有16个字节,所以sockaddr_in是可以强制类型转换为sockaddr的。事实上也往往使用这种方法。

7.11           Sleep函数

线程挂起函数,表示线程挂起一段时间。Sleep(1000)表示挂起一秒。定义于WINBASE.H头文件中。WINBASE.H又被包含于WINDOWS.H中,然后WINDOWS.H被WINSOCK2.H包含。所以在本例中使用Sleep函数不需要包含其它头文件。

7.12           sendto函数

在Socket中有两套发送和接收函数,一是sendto和recvfrom;二是send和recv。前一套在函数参数中要指明地址;而后一套需要先将套接字和一个地址绑定,然后直接发送和接收,不需绑定地址。sendto的定义如下:

int PASCAL FAR sendto (SOCKET s, const char FAR * buf, int len, int flags, const struct sockaddr FAR *to, int tolen);

第一个参数就是套接字;

第二个参数是要传送的数据指针;

第三个参数是要传送的数据长度(字节数);

第四个参数是传送方式的标识,如果不需要特殊要求则可以设置为0,其它值请参考MSDN;

第五个参数是目标地址,注意这里使用的是sockaddr的指针;

第六个参数是地址的长度;

返回值为整型,如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR。

7.13           WSAGetLastError函数

该函数用来在Socket相关API失败后读取错误码,根据这些错误码可以对照查出错误原因。

7.14           closesocket

关闭套接字,其参数为SOCKET类型。成功返回0,失败返回SOCKET_ERROR。

7.15           小结

总结以上内容,写一个UDP发送程序的步骤如下:

1.         用WSAStartup函数初始化Socket环境;

2.         用socket函数创建一个套接字;

3.         用setsockopt函数设置套接字的属性,例如设置为广播类型;很多时候该步骤可以省略;

4.         创建一个sockaddr_in,并指定其IP地址和端口号;

5.         用sendto函数向指定地址发送数据,这里的目标地址就是广播地址;注意这里不需要绑定,即使绑定了,其地址也会被sendto中的参数覆盖;若使用send函数则会出错,因为send是面向连接的,而UDP是非连接的,只能使用sendto发送数据;

6.         用closesocket函数关闭套接字;

7.         用WSACleanup函数关闭Socket环境。

那么,与之类似,一个UDP接收程序的步骤如下,注意接收方一定要bind套接字:

1.         用WSAStartup函数初始化Socket环境;

2.         用socket函数创建一个套接字;

3.         用setsockopt函数设置套接字的属性,例如设置为广播类型;

4.         创建一个sockaddr_in,并指定其IP地址和端口号;

5.         用bind函数将套接字与接收的地址绑定起来,然后调用recvfrom函数或者recv接收数据; 注意这里一定要绑定,因为接收报文的套接字必须在网络上有一个绑定的名称才能保证正确接收数据;

6.         用closesocket函数关闭套接字;

7.         用WSACleanup函数关闭Socket环境。

广播接收程序见源程序代码UDP_Recv_Broadcast.cpp。编译、链接、执行与UDP_Send_Broadcast类似。

7.16           UDP点对点发送接收程序

广播发送和接收使用并不广泛,一般来说指定发送和接收的IP比较常用。点对点方式的UDP发送和接收与上面的例子非常类似,不同的就是需要指定一个具体的IP地址。并且不需要调用setsockopt设置socket的广播属性。

其具体源代码见UDP_Send_P2P.cpp和UDP_Recv_P2P.cpp。

注意在使用这两个程序时要设为自己所需的IP。

8         TCP

TCP与UDP最大的不同之处在于TCP是一个面向连接的协议,在进行数据收发之前TCP必须进行连接,并且在收发的时候必须保持该连接。

发送方的步骤如下(省略了Socket环境的初始化、关闭等内容):

1.         用socket函数创建一个套接字sock;

2.         用bind将sock绑定到本地地址;

3.         用listen侦听sock套接字;

4.         用accept函数接收客户方的连接,返回客户方套接字clientSocket;

5.         在客户方套接字clientSocket上使用send发送数据;

6.         用closesocket函数关闭套接字sock和clientSocket;

而接收方的步骤如下:

1.         用socket函数创建一个套接字sock;

2.         创建一个指向服务方的远程地址;

3.         用connect将sock连接到服务方,使用远程地址;

4.         在套接字上使用recv接收数据;

5.         用closesocket函数关闭套接字sock;

值得注意的是,在服务方有两个地址,一个是本地地址myaddr,另一个是目标地址addrto。本地地址myaddr用来和本地套接字sock绑定,目标地址被sock用来accept客户方套接字clientSocket。这样sock和clientSocket连接成功,这两个地址也连接上了。在服务方使用clientSocket发送数据,则会从本地地址传送到目标地址。

在客户方只有一个地址,即来源地址addrfrom。这个地址被用来connect远程的服务方套接字,connect成功则本地套接字与远程的来源地址连接了,因此可以使用该套接字接收远程数据。其实这时客户方套接字已经被隐性的绑定了本地地址,所以不需要显式调用bind函数,即使调用也不会影像结果。

具体源代码见TCP_Send.cpp和TCP_Recv.cpp。注意将源代码中的IP地址修改为符合自己需要的IP。为了减少代码复杂性,没有使用读取本机IP的代码,后续例子程序中含有此功能代码。

8.1 bind函数

bind函数用来将一个套接字绑定到一个IP地址。一般只在服务方(即数据发送方)调用,很多函数会隐式的调用bind函数。

8.2 listen函数

从服务方监听客户方的连接。同一个套接字可以多次监听。

8.3 connect和accept函数

connect是客户方连接服务方的函数,而accept是服务方同意客户方连接的函数。这两个配套函数分别在各自的程序中被成功调用后就可以收发数据了。

8.4 send和recv函数

send和recv是用来发送和接收数据的两个重要函数。send只能在已经连接的状态下使用,而recv可以面向连接和非连接的状态下使用。

send的定义如下:

int WSAAPI send(

    SOCKET s,

    const char FAR * buf,

    int len,

    int flags

    );

其参数的含义和sendto中的前四个参数一样。而recv的定义如下:

int WSAAPI recv(

    SOCKET s,

    char FAR * buf,

    int len,

    int flags

    );

其参数含义与send中的参数含义一样。

9         一个局域网聊天工具的编写

掌握了以上关于socket的基本用法,编写一个局域网聊天程序也就变得非常简单,如同设计一个普通的对话框程序一样。

9.1 功能设计

功能设计如下:

1.         要能够指定聊天对象的IP和端口(端口可以内部确定);

2.         要能够发送消息给指定聊天对象;

3.         要能够接收聊天对象的消息;

4.         接收消息时要播放声音;

5.         接收消息时如果当前对话框不是最前端,要闪动图标;

6.         要有托盘图标,可以将对话框收入托盘;

9.2 功能实现

将内部端口设为3456,提供一个IP地址控件来设置聊天对象的IP。该控件必须能够读取IP地址并赋值给内部变量。将地址转换为in_addr类型。

发送消息需要使用一个套接字。

接收消息也需要使用一个套接字,由于发送消息也使用了一个套接字,为了在同一个进程中同时发送和接收消息,需要使用多线程技术,将发送消息的线程设为主线程;而接收消息的线程设为子线程,子线程只负责接收UDP消息,在收到消息后显示到主界面中。

接收消息时播放声音这个功能在子线程中完成,使用sndPlaySound函数,并提供一个wav文件即可。

闪动图标这个最白痴的功能需要使用一个Timer,在主对话框类中添加一个OnTimer函数,定时检查当前窗口状态变量是否为假,若为假就每次设置另一个图标。若当前窗口显示到最顶端,则设置为默认图标。

托盘图标功能用网上下载的CtrayIcon类轻松搞定。需要提供一个自定义消息,一个弹出菜单资源。

9.3 所需资源

头文件:winsock2.h,Mmsystem.h

库文件:ws2_32.lib,winmm.lib

dll:Ws2_32.dll,winmm.dll

wav文件:recv.wav

图标:一个主程序图标IDI_MAIN、四个变化图标IDI_ICON1~4;

菜单:一个给托盘用的弹出菜单IDR_TRAYICON;

说明,Mmsystem.h和winmm.lib、winmm.dll是为了那个播放声音的功能。

9.4 托盘功能

托盘属于界面功能,是变更很少的需求,因此首先完成。

1.         引入TRAYICON.H和TRAYICON.cpp两个类;

2.         在CLANTalkDlg类中加入一个CTrayIconm_trayIcon;属性;

3.         在CLANTalkDlg的构造函数中初始化m_trayIcon,m_trayIcon(IDR_TRAYICON);

4.         添加一个自定义消息WM_MY_TRAY_NOTIFICATION,即在三个地方添加消息定义、消息响应函数、消息映射;

5.         在InitDialog方法中调用托盘初始化的两个函数      m_trayIcon.SetNotificationWnd(this, WM_MY_TRAY_NOTIFICATION);    m_trayIcon.SetIcon(IDI_MAIN);

6.         重写OnClose方法,添加弹出菜单的OnAppSuspend和OnAppOpen以及OnAppAbout方法;

7.         重写对话框的OnCancel方法。

9.5 动态图标

动态图标也是界面相关功能,首先完成。

1.         添加四个HICON变量m_hIcon1,m_hIcon2,m_hIcon3,m_hIcon4;

2.         在构造函数中初始化这四个变量m_hIcon1 = AfxGetApp()->LoadIcon(IDI_ICON1);

3.         在InitDialog中设置调用SetTimer(1,300,NULL);设置一个timer,id为1,间隔为300微秒;

4.         添加一个布尔属性m_bDynamicIcon,指示目前是否需要动态图标,并给出一个设置函数SetDynamicIcon;

5.         添加一个OnTimer函数,让每次timer调用时根据m_bDynamicIcon的值修改图标;

两个地方是用来设置动态图标的,一个是当程序收到消息并且程序不在桌面顶端时,这时设置为动态图标,在后面的消息接收线程中处理;二是当程序显示到桌面顶端时,设置为非动态;

重载OnActivate方法可以完成第二个时刻的要求。当窗口状态为WA_ACTIVE或者WA_CLICKACTIVE时SetDynamicIcon(false),否则设置SetDynamicIcon(true);

9.6 发送UDP报文功能

发送UDP报文只需在主线程中完成,需要以下步骤:

1.         初始化Socket环境,这可以在CLANTalkApp的InitInstance中完成,同理关闭Socket环境在ExitInstance中完成;我们可以使用前面的方法,也可以直接调用MFC中的AfxSocketInit函数,这个函数可以确保在程序结束时自动关闭Socket环境;

2.         创建socket,考虑到报错信息需要弹出对话框,因此不在CLANTalkDlg的构造函数中创建,而是在InitDialog中构建;发送报文的socket为m_sendSock;

3.         设置目的地址功能,需要一个地址赋值函数setAddress(char* szAddr);可以将一个字符串地址赋值给sockaddr_in形式的地址;在CLANTalkDlg中增加一个sockaddr_in m_addrto;属性;

4.         读取文本框中的文字,用sendto发送到对象地址;

5.         清空文本框,在记录框中添加聊天记录。

这时可以使用前面的UDP简单接收程序来辅助测试,因为此时还未完成报文接收功能。

9.7 接收UDP报文功能

接收UDP报文要考虑几个问题,第一个是要创建一个子线程,在子线程中接收报文;第二是接收报文和发送报文要有互斥机制,以免冲突;第三是接收到报文要播放声音;第四是接收报文且当前窗口不在桌面顶端要调用动态图标功能。

按照以上需求设计步骤如下:

1.         创建接收套接字m_recvSock,

2.         利用gethostname和gethostbyname等函数获取本机IP,并将套接字bind到该地址;

3.         添加一个CwinThread* m_pRecvThread属性,并在InitDialog中调用AfxBeginThread创建子线程;

4.         编写子线程运行函数void RecvProcess(LPVOID pParam),这时一个全局函数,为了方便调用CLANTalkDlg类中的各种变量与方法,将CLANTalkDlg类的指针作为参数传入子线程函数,并将RecvProcess设置为CLANTalkDlg类的友元。

5.         子线程函数中完成以下功能:利用recv接收报文;保存聊天记录;判断当前窗口是否在前台,并修改动态图标属性;播放声音。

6.         用来记录聊天信息的ClistBox的Sort属性要去掉,否则记录会按内容排序,很不好看。在RC编辑器中去掉这个属性即可。

7.         最后要注意,在主线程退出时要保证子线程退出,但此时子线程还阻塞在recv方法上,因此主线程向自己发送一条消息消除阻塞,同时改变子线程退出标志保证子线程可以退出。

9.8 设置聊天对象IP

点击“确认对象”按钮时,检测IP地址控件,如果IP地址有效,则将IP地址读入内部属性。这个IP地址作为发送信息的目标地址。

这个设置只能设置发送消息的对象,所有人都可以向本机发送信息,只要他的端口是正确的。

9.9 编译链接和运行

下载压缩包后可以打开VC工程编译链接,若直接运行则可以点击LANTalkExeFile目录中的可执行文件,这个目标包含了运行所需要的所有dll和资源文件。

当然,如果需要可以用InstallShield做一个安装程序,不过看来是没有必要的。

9.10           小结

这个聊天程序很简单,但是基本上具有了一个框架,可以有最简单的聊天功能。要在此基础上进行扩展几乎已经没有什么技术问题了。

10    使用好的Socket包可以简化开发过程

本文中所有的技术尽量采用最原始的方式来使用。例如多线程使用的是AfxBeginThread,套接字使用了最原始的套接字,并在很多地方直接使用了SDK函数,而尽量避免了MFC等代码框架,这是为了方便他人掌握技术的最基本内涵。

其实在具体的编程中,当然是怎么方便怎么来,Socket和多线程以及界面等功能都有大量方便可用的代码库,复用这些代码库会比自己动手写方便很多。但是,掌握了基本原理再使用这些库,事半功倍

posted @ 2008-04-01 17:05 随意门 阅读(1113) | 评论 (0)编辑 收藏
DLL编写教程

引自http://www.blogjava.net/wxb_nudt/archive/2007/09/11/144371.html

DLL编写教程

半年不能上网,最近网络终于通了,终于可以更新博客了,写点什么呢?决定最近写一个编程技术系列,其内容是一些通用的编程技术。例如DLL,COM,Socket,多线程等等。这些技术的特点就是使用广泛,但是误解很多;网上教程很多,但是几乎没有什么优质良品。我以近几个月来的编程经验发现,很有必要好好的总结一下这些编程技术了。一来对自己是总结提高,二来可以方便光顾我博客的朋友。

好了,废话少说,言归正传。第一篇就是《DLL编写教程》,为什么起这么土的名字呢?为什么不叫《轻轻松松写DLL》或者《DLL一日通》呢?或者更nb的《深入简出DLL》呢?呵呵,常常上网搜索资料的弟兄自然知道。

本文对通用的DLL技术做了一个总结,并提供了源代码打包下载,下载地址为:

http://www.blogjava.net/Files/wxb_nudt/DLL_SRC.rar

DLL的优点

简单的说,dll有以下几个优点:

1)      节省内存。同一个软件模块,若是以源代码的形式重用,则会被编译到不同的可执行程序中,同时运行这些exe时这些模块的二进制码会被重复加载到内存中。如果使用dll,则只在内存中加载一次,所有使用该dll的进程会共享此块内存(当然,像dll中的全局变量这种东西是会被每个进程复制一份的)。

2)      不需编译的软件系统升级,若一个软件系统使用了dll,则该dll被改变(函数名不变)时,系统升级只需要更换此dll即可,不需要重新编译整个系统。事实上,很多软件都是以这种方式升级的。例如我们经常玩的星际、魔兽等游戏也是这样进行版本升级的。

3)      Dll库可以供多种编程语言使用,例如用c编写的dll可以在vb中调用。这一点上DLL还做得很不够,因此在dll的基础上发明了COM技术,更好的解决了一系列问题。

最简单的dll

开始写dll之前,你需要一个c/c++编译器和链接器,并关闭你的IDE。是的,把你的VC和C++ BUILDER之类的东东都关掉,并打开你以往只用来记电话的记事本程序。不这样做的话,你可能一辈子也不明白dll的真谛。我使用了VC自带的cl编译器和link链接器,它们一般都在vc的bin目录下。(若你没有在安装vc的时候选择注册环境变量,那么就立刻将它们的路径加入path吧)如果你还是因为离开了IDE而害怕到哭泣的话,你可以关闭这个页面并继续去看《VC++技术内幕》之类无聊的书了。

最简单的dll并不比c的helloworld难,只要一个DllMain函数即可,包含objbase.h头文件(支持COM技术的一个头文件)。若你觉得这个头文件名字难记,那么用windows.H也可以。源代码如下:dll_nolib.cpp

#include <objbase.h>

#include <iostream.h>

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

    HANDLE g_hModule;

    switch(dwReason)

    {

    case DLL_PROCESS_ATTACH:

       cout<<"Dll is attached!"<<endl;

       g_hModule = (HINSTANCE)hModule;

       break;

    case DLL_PROCESS_DETACH:

       cout<<"Dll is detached!"<<endl;

       g_hModule=NULL;

       break;

    }

    return true;

}

其中DllMain是每个dll的入口函数,如同c的main函数一样。DllMain带有三个参数,hModule表示本dll的实例句柄(听不懂就不理它,写过windows程序的自然懂),dwReason表示dll当前所处的状态,例如DLL_PROCESS_ATTACH表示dll刚刚被加载到一个进程中,DLL_PROCESS_DETACH表示dll刚刚从一个进程中卸载。当然还有表示加载到线程中和从线程中卸载的状态,这里省略。最后一个参数是一个保留参数(目前和dll的一些状态相关,但是很少使用)。

从上面的程序可以看出,当dll被加载到一个进程中时,dll打印"Dll is attached!"语句;当dll从进程中卸载时,打印"Dll is detached!"语句。

编译dll需要以下两条命令:

cl /c dll_nolib.cpp

这条命令会将cpp编译为obj文件,若不使用/c参数则cl还会试图继续将obj链接为exe,但是这里是一个dll,没有main函数,因此会报错。不要紧,继续使用链接命令。

Link /dll dll_nolib.obj

这条命令会生成dll_nolib.dll。

注意,因为编译命令比较简单,所以本文不讨论nmake,有兴趣的可以使用nmake,或者写个bat批处理来编译链接dll。

加载DLL(显式调用)

使用dll大体上有两种方式,显式调用和隐式调用。这里首先介绍显式调用。编写一个客户端程序:dll_nolib_client.cpp

#include <windows.h>

#include <iostream.h>

int main(void)

{

    //加载我们的dll

    HINSTANCE hinst=::LoadLibrary("dll_nolib.dll"); 

    if (NULL != hinst)

    {

       cout<<"dll loaded!"<<endl;

    }

    return 0;

}

注意,调用dll使用LoadLibrary函数,它的参数就是dll的路径和名称,返回值是dll的句柄。 使用如下命令编译链接客户端:

Cl dll_nolib_client.cpp

并执行dll_nolib_client.exe,得到如下结果:

Dll is attached!

dll loaded!

Dll is detached!

以上结果表明dll已经被客户端加载过。但是这样仅仅能够将dll加载到内存,不能找到dll中的函数。

使用dumpbin命令查看DLL中的函数

Dumpbin命令可以查看一个dll中的输出函数符号名,键入如下命令:

Dumpbin –exports dll_nolib.dll

通过查看,发现dll_nolib.dll并没有输出任何函数。

如何在dll中定义输出函数

总体来说有两种方法,一种是添加一个def定义文件,在此文件中定义dll中要输出的函数;第二种是在源代码中待输出的函数前加上__declspec(dllexport)关键字。

Def文件

首先写一个带有输出函数的dll,源代码如下:dll_def.cpp

#include <objbase.h>

#include <iostream.h>

void FuncInDll (void)

{

    cout<<"FuncInDll is called!"<<endl;

}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

    HANDLE g_hModule;

    switch(dwReason)

    {

    case DLL_PROCESS_ATTACH:

       g_hModule = (HINSTANCE)hModule;

       break;

    case DLL_PROCESS_DETACH:

        g_hModule=NULL;

        break;

    }

    return TRUE;

}

这个dll的def文件如下:dll_def.def

;

; dll_def module-definition file

;

LIBRARY         dll_def.dll

DESCRIPTION     '(c)2007-2009 Wang Xuebin'

EXPORTS

                FuncInDll @1 PRIVATE

你会发现def的语法很简单,首先是LIBRARY关键字,指定dll的名字;然后一个可选的关键字DESCRIPTION,后面写上版权等信息(不写也可以);最后是EXPORTS关键字,后面写上dll中所有要输出的函数名或变量名,然后接上@以及依次编号的数字(从1到N),最后接上修饰符。

用如下命令编译链接带有def文件的dll:

Cl /c dll_def.cpp

Link /dll dll_def.obj /def:dll_def.def

再调用dumpbin查看生成的dll_def.dll:

Dumpbin –exports dll_def.dll

得到如下结果:

Dump of file dll_def.dll

File Type: DLL

 Section contains the following exports for dll_def.dll

           0 characteristics

    46E4EE98 time date stamp Mon Sep 10 15:13:28 2007

        0.00 version

           1 ordinal base

           1 number of functions

           1 number of names

    ordinal hint RVA      name

          1    0 00001000 FuncInDll

 Summary

        2000 .data

        1000 .rdata

        1000 .reloc

        6000 .text

观察这一行

          1    0 00001000 FuncInDll

会发现该dll输出了函数FuncInDll。

显式调用DLL中的函数

写一个dll_def.dll的客户端程序:dll_def_client.cpp

#include <windows.h>

#include <iostream.h>

int main(void)

{

    //定义一个函数指针

    typedef void (* DLLWITHLIB )(void); 

    //定义一个函数指针变量

    DLLWITHLIB pfFuncInDll = NULL; 

    //加载我们的dll

    HINSTANCE hinst=::LoadLibrary("dll_def.dll"); 

    if (NULL != hinst)

    {

       cout<<"dll loaded!"<<endl;

    }

    //找到dll的FuncInDll函数

    pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, "FuncInDll"); 

    //调用dll里的函数

    if (NULL != pfFuncInDll)

    {

       (*pfFuncInDll)();  

    }

    return 0;

}

有两个地方值得注意,第一是函数指针的定义和使用,不懂的随便找本c++书看看;第二是GetProcAddress的使用,这个API是用来查找dll中的函数地址的,第一个参数是DLL的句柄,即LoadLibrary返回的句柄,第二个参数是dll中的函数名称,即dumpbin中输出的函数名(注意,这里的函数名称指的是编译后的函数名,不一定等于dll源代码中的函数名)。

编译链接这个客户端程序,并执行会得到:

dll loaded!

FuncInDll is called!

这表明客户端成功调用了dll中的函数FuncInDll。

__declspec(dllexport)

为每个dll写def显得很繁杂,目前def使用已经比较少了,更多的是使用__declspec(dllexport)在源代码中定义dll的输出函数。

Dll写法同上,去掉def文件,并在每个要输出的函数前面加上声明__declspec(dllexport),例如:

__declspec(dllexport) void FuncInDll (void)

这里提供一个dll源程序dll_withlib.cpp,然后编译链接。链接时不需要指定/DEF:参数,直接加/DLL参数即可,

Cl /c dll_withlib.cpp

Link /dll dll_withlib.obj

然后使用dumpbin命令查看,得到:

1    0 00001000 ?FuncInDll@@YAXXZ

可知编译后的函数名为?FuncInDll@@YAXXZ,而并不是FuncInDll,这是因为c++编译器基于函数重载的考虑,会更改函数名,这样使用显式调用的时候,也必须使用这个更改后的函数名,这显然给客户带来麻烦。为了避免这种现象,可以使用extern “C”指令来命令c++编译器以c编译器的方式来命名该函数。修改后的函数声明为:

extern "C" __declspec(dllexport) void FuncInDll (void)

dumpbin命令结果:

1    0 00001000 FuncInDll

这样,显式调用时只需查找函数名为FuncInDll的函数即可成功。

extern “C”

使用extern “C”关键字实际上相当于一个编译器的开关,它可以将c++语言的函数编译为c语言的函数名称。即保持编译后的函数符号名等于源代码中的函数名称。

隐式调用DLL

显式调用显得非常复杂,每次都要LoadLibrary,并且每个函数都必须使用GetProcAddress来得到函数指针,这对于大量使用dll函数的客户是一种困扰。而隐式调用能够像使用c函数库一样使用dll中的函数,非常方便快捷。

下面是一个隐式调用的例子:dll包含两个文件dll_withlibAndH.cpp和dll_withlibAndH.h。

代码如下:dll_withlibAndH.h

extern "C" __declspec(dllexport) void FuncInDll (void);

dll_withlibAndH.cpp

#include <objbase.h>

#include <iostream.h>

#include "dll_withLibAndH.h"//看到没有,这就是我们增加的头文件

extern "C" __declspec(dllexport) void FuncInDll (void)

{

    cout<<"FuncInDll is called!"<<endl;

}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

    HANDLE g_hModule;

    switch(dwReason)

    {

    case DLL_PROCESS_ATTACH:

       g_hModule = (HINSTANCE)hModule;

       break;

    case DLL_PROCESS_DETACH:

        g_hModule=NULL;

        break;

    }

    return TRUE;

}

编译链接命令:

Cl /c dll_withlibAndH.cpp

Link /dll dll_withlibAndH.obj

在进行隐式调用的时候需要在客户端引入头文件,并在链接时指明dll对应的lib文件(dll只要有函数输出,则链接的时候会产生一个与dll同名的lib文件)位置和名称。然后如同调用api函数库中的函数一样调用dll中的函数,不需要显式的LoadLibrary和GetProcAddress。使用最为方便。客户端代码如下:dll_withlibAndH_client.cpp

#include "dll_withLibAndH.h"

//注意路径,加载 dll的另一种方法是 Project | setting | link 设置里

#pragma comment(lib,"dll_withLibAndH.lib")

int main(void)

{

    FuncInDll();//只要这样我们就可以调用dll里的函数了

    return 0;

}

__declspec(dllexport)和__declspec(dllimport)配对使用

上面一种隐式调用的方法很不错,但是在调用DLL中的对象和重载函数时会出现问题。因为使用extern “C”修饰了输出函数,因此重载函数肯定是会出问题的,因为它们都将被编译为同一个输出符号串(c语言是不支持重载的)。

事实上不使用extern “C”是可行的,这时函数会被编译为c++符号串,例如(?FuncInDll@@YAXH@Z、 ?FuncInDll@@YAXXZ),当客户端也是c++时,也能正确的隐式调用。

这时要考虑一个情况:若DLL1.CPP是源,DLL2.CPP使用了DLL1中的函数,但同时DLL2也是一个DLL,也要输出一些函数供Client.CPP使用。那么在DLL2中如何声明所有的函数,其中包含了从DLL1中引入的函数,还包括自己要输出的函数。这个时候就需要同时使用__declspec(dllexport)和__declspec(dllimport)了。前者用来修饰本dll中的输出函数,后者用来修饰从其它dll中引入的函数。

所有的源代码包括DLL1.H,DLL1.CPP,DLL2.H,DLL2.CPP,Client.cpp。源代码可以在下载的包中找到。你可以编译链接并运行试试。

值得关注的是DLL1和DLL2中都使用的一个编码方法,见DLL2.H

#ifdef DLL_DLL2_EXPORTS

#define DLL_DLL2_API __declspec(dllexport)

#else

#define DLL_DLL2_API __declspec(dllimport)

#endif

DLL_DLL2_API void FuncInDll2(void);

DLL_DLL2_API void FuncInDll2(int);

在头文件中以这种方式定义宏DLL_DLL2_EXPORTS和DLL_DLL2_API,可以确保DLL端的函数用__declspec(dllexport)修饰,而客户端的函数用__declspec(dllimport)修饰。当然,记得在编译dll时加上参数/D “DLL_DLL2_EXPORTS”,或者干脆就在dll的cpp文件第一行加上#define DLL_DLL2_EXPORTS。

VC生成的代码也是这样的!事实证明,我是抄袭它的,hoho!

DLL中的全局变量和对象

解决了重载函数的问题,那么dll中的全局变量和对象都不是问题了,只是有一点语法需要注意。如源代码所示:dll_object.h

#ifdef DLL_OBJECT_EXPORTS

#define DLL_OBJECT_API __declspec(dllexport)

#else

#define DLL_OBJECT_API __declspec(dllimport)

#endif

DLL_OBJECT_API void FuncInDll(void);

extern DLL_OBJECT_API int g_nDll;

class DLL_OBJECT_API CDll_Object {

public:

    CDll_Object(void);

    show(void);

    // TODO: add your methods here.

};

Cpp文件dll_object.cpp如下:

#define DLL_OBJECT_EXPORTS

#include <objbase.h>

#include <iostream.h>

#include "dll_object.h"

DLL_OBJECT_API void FuncInDll(void)

{

    cout<<"FuncInDll is called!"<<endl;

}

DLL_OBJECT_API int g_nDll = 9;

CDll_Object::CDll_Object()

{

    cout<<"ctor of CDll_Object"<<endl;

}

CDll_Object::show()

{

    cout<<"function show in class CDll_Object"<<endl;

}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

    HANDLE g_hModule;

    switch(dwReason)

    {

    case DLL_PROCESS_ATTACH:

       g_hModule = (HINSTANCE)hModule;

       break;

    case DLL_PROCESS_DETACH:

        g_hModule=NULL;

        break;

    }

    return TRUE;

}

编译链接完后Dumpbin一下,可以看到输出了5个符号:

1    0 00001040 ??0CDll_Object@@QAE@XZ

 2    1 00001000 ??4CDll_Object@@QAEAAV0@ABV0@@Z

 3    2 00001020 ?FuncInDll@@YAXXZ

 4    3 00008040 ?g_nDll@@3HA

 5    4 00001069 ?show@CDll_Object@@QAEHXZ

它们分别代表类CDll_Object,类的构造函数,FuncInDll函数,全局变量g_nDll和类的成员函数show。下面是客户端代码:dll_object_client.cpp

#include "dll_object.h"

#include <iostream.h>

//注意路径,加载 dll的另一种方法是 Project | setting | link 设置里

#pragma comment(lib,"dll_object.lib")

int main(void)

{

    cout<<"call dll"<<endl;

    cout<<"call function in dll"<<endl;

    FuncInDll();//只要这样我们就可以调用dll里的函数了

    cout<<"global var in dll g_nDll ="<<g_nDll<<endl;

    cout<<"call member function of class CDll_Object in dll"<<endl;

    CDll_Object obj;

    obj.show();

    return 0;

}

运行这个客户端可以看到:

call dll

call function in dll

FuncInDll is called!

global var in dll g_nDll =9

call member function of class CDll_Object in dll

ctor of CDll_Object

function show in class CDll_Object

可知,在客户端成功的访问了dll中的全局变量,并创建了dll中定义的C++对象,还调用了该对象的成员函数。

中间的小结

牢记一点,说到底,DLL是对应C语言的动态链接技术,在输出C函数和变量时显得方便快捷;而在输出C++类、函数时需要通过各种手段,而且也并没有完美的解决方案,除非客户端也是c++。

记住,只有COM是对应C++语言的技术。

下面开始对各各问题一一小结。

显式调用和隐式调用

何时使用显式调用?何时使用隐式调用?我认为,只有一个时候使用显式调用是合理的,就是当客户端不是C/C++的时候。这时是无法隐式调用的。例如用VB调用C++写的dll。(VB我不会,所以没有例子)

Def和__declspec(dllexport)

其实def的功能相当于extern “C” __declspec(dllexport),所以它也仅能处理C函数,而不能处理重载函数。而__declspec(dllexport)和__declspec(dllimport)配合使用能够适应任何情况,因此__declspec(dllexport)是更为先进的方法。所以,目前普遍的看法是不使用def文件,我也同意这个看法。

从其它语言调用DLL

从其它编程语言中调用DLL,有两个最大的问题,第一个就是函数符号的问题,前面已经多次提过了。这里有个两难选择,若使用extern “C”,则函数名称保持不变,调用较方便,但是不支持函数重载等一系列c++功能;若不使用extern “C”,则调用前要查看编译后的符号,非常不方便。

第二个问题就是函数调用压栈顺序的问题,即__cdecl和__stdcall的问题。__cdecl是常规的C/C++调用约定,这种调用约定下,函数调用后栈的清理工作是由调用者完成的。__stdcall是标准的调用约定,即这些函数将在返回到调用者之前将参数从栈中删除。

这两个问题DLL都不能很好的解决,只能说凑合着用。但是在COM中,都得到了完美的解决。所以,要在Windows平台实现语言无关性,还是只有使用COM中间件。

总而言之,除非客户端也使用C++,否则dll是不便于支持函数重载、类等c++特性的。DLL对c函数的支持很好,我想这也是为什么windows的函数库使用C加dll实现的理由之一。

在VC中编写DLL

在VC中创建、编译、链接dll是非常方便的,点击fileàNewàProjectàWin32 Dynamic-Link Library,输入dll名称dll_InVC然后点击确定。然后选择A DLL that export some symbols,点击Finish。即可得到一个完整的DLL。

仔细观察其源代码,是不是有很多地方似曾相识啊,哈哈!



posted @ 2008-04-01 17:03 随意门 阅读(237) | 评论 (0)编辑 收藏
C++的XML编程经验――LIBXML2库使用指南

     摘要: 写这篇文章的原因有如下几点:1)C++标准库中没有操作XML的方法,用C++操作XML文件必须熟悉一种函数库,LIBXML2是其中一种很优秀的XML库,而且它同时支持多种编程语言;2)LIBXML2库的Tutorial写得不太好,尤其是编码转换的部分,不适用于中文编码的转换;3)网上的大多数关于Libxml2的介绍仅仅是翻译了自带的资料,没有详细介绍如何在windows平台下进行编程,更很少提到如...  阅读全文

posted @ 2008-04-01 17:01 随意门| 编辑 收藏
Linux下的多线程编程

Linux下的多线程编程


作者:姚继锋 2001-08-11 09:05:00 来自:http://www.china-pub.com

1 引言
  线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多线程技术已经被许多操作系统所支持,包括Windows/NT,当然,也包括Linux。
  为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。
  使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来桓鼋痰目笤际且桓鱿叱炭?0倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
  使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
  除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
  1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
  2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
  下面我们先来尝试编写一个简单的多线程程序。

2 简单的多线程编程
  Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明。下面我们展示一个最简单的多线程程序example1.c。

/* example.c*/
#include <stdio.h>
#include <pthread.h>
void thread(void)
{
int i;
for(i=0;i<3;i++)
printf("This is a pthread.\n");
}

int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,(void *) thread,NULL);
if(ret!=0){
printf ("Create pthread error!\n");
exit (1);
}
for(i=0;i<3;i++)
printf("This is the main process.\n");
pthread_join(id,NULL);
return (0);
}

我们编译此程序:
gcc example1.c -lpthread -o example1
运行example1,我们得到如下结果:
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
再次运行,我们可能得到如下结果:
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.

  前后两次结果不一样,这是两个线程争夺CPU资源的结果。上面的示例中,我们使用到了两个函数,  pthread_create和pthread_join,并声明了一个pthread_t型的变量。
  pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义:
  typedef unsigned long int pthread_t;
  它是一个线程的标识符。函数pthread_create用来创建一个线程,它的原型为:
  extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
  void *(*__start_routine) (void *), void *__arg));
  第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里,我们的函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。对线程属性的设定和修改我们将在下一节阐述。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。
  函数pthread_join用来等待一个线程的结束。函数原型为:
  extern int pthread_join __P ((pthread_t __th, void **__thread_return));
  第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是象我们上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。它的函数原型为:
  extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
  唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。
  在这一节里,我们编写了一个最简单的线程,并掌握了最常用的三个函数pthread_create,pthread_join和pthread_exit。下面,我们来了解线程的一些常用属性以及如何设置这些属性。

3 修改线程的属性
  在上一节的例子里,我们用pthread_create函数创建了一个线程,在这个线程中,我们使用了默认参数,即将该函数的第二个参数设为NULL。的确,对大多数程序来说,使用默认属性就够了,但我们还是有必要来了解一下线程的有关属性。
  属性结构为pthread_attr_t,它同样在头文件/usr/include/pthread.h中定义,喜欢追根问底的人可以自己去查看。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
  关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。
  设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;

/*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

pthread_create(&tid, &attr, (void *) my_function, NULL);

  线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
  另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。
#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;

pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, &param);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, &param);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
  
4 线程的数据处理
  和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也给多线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。在进程中共享的变量必须用关键字volatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX参数)改变它们的使用方式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。下面,我们就逐步介绍处理线程数据时的有关知识。

4.1 线程数据
  在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
  和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。
  创建键的函数原型为:
  extern int pthread_key_create __P ((pthread_key_t *__key,
  void (*__destr_function) (void *)));
  第一个参数为指向一个键值的指针,第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。这个函数常和函数pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,为了让这个键只被创建一次。函数pthread_once声明一个初始化函数,第一次调用pthread_once时它执行这个函数,以后的调用将被它忽略。

  在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数createWindow,这个函数定义一个图形窗口(数据类型为Fl_Window *,这是图形界面开发工具FLTK中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。
/* 声明一个键*/
pthread_key_t myWinKey;
/* 函数 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
/* 调用函数createMyKey,创建键*/
pthread_once ( & once, createMyKey) ;
/*win指向一个新建立的窗口*/
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
/* 对此窗口作一些可能的设置工作,如大小、位置、名称等*/
setWindow(win);
/* 将窗口指针值绑定在键myWinKey上*/
pthread_setpecific ( myWinKey, win);
}

/* 函数 createMyKey,创建一个键,并指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(&myWinKey, freeWinKey);
}

/* 函数 freeWinKey,释放空间*/
void freeWinKey ( Fl_Window * win){
delete win;
}

  这样,在不同的线程中调用函数createMyWin,都可以得到在线程内部均可见的窗口变量,这个变量通过函数pthread_getspecific得到。在上面的例子中,我们已经使用了函数pthread_setspecific来将线程数据和一个键绑定在一起。这两个函数的原型如下:
  extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
  extern void *pthread_getspecific __P ((pthread_key_t __key));
  这两个函数的参数意义和使用方法是显而易见的。要注意的是,用pthread_setspecific为一个键指定新的线程数据时,必须自己释放原有的线程数据以回收空间。这个过程函数pthread_key_delete用来删除一个键,这个键占用的内存将被释放,但同样要注意的是,它只释放键占用的内存,并不释放该键关联的线程数据所占用的内存资源,而且它也不会触发函数pthread_key_create中定义的destructor函数。线程数据的释放必须在释放键之前完成。

4.2 互斥锁
  互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。
  我们先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。

void reader_function ( void );
void writer_function ( void );

char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void ){
pthread_t reader;
/* 定义延迟时间*/
delay.tv_sec = 2;
delay.tv_nec = 0;
/* 用默认属性初始化一个互斥锁对象*/
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
writer_function( );
}

void writer_function (void){
while(1){
/* 锁定互斥锁*/
pthread_mutex_lock (&mutex);
if (buffer_has_item==0){
buffer=make_new_item( );
buffer_has_item=1;
}
/* 打开互斥锁*/
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}

void reader_function(void){
while(1){
pthread_mutex_lock(&mutex);
if(buffer_has_item==1){
consume_item(buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
  这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。函数pthread_mutex_init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用函数pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数pthread_mutexattr_settype用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上所、解锁机制,一般情况下,选用最后一个默认属性。
  pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。在上面的例子中,我们使用了pthread_delay_np函数,让线程睡眠一段时间,就是为了防止一个线程始终占据此函数。
  上面的例子非常简单,就不再介绍了,需要提出的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使用函数pthread_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点。

4.3 条件变量
  前一节中我们讲述了如何使用互斥锁来实现线程间数据的共享和通信,互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步。
  条件变量的结构为pthread_cond_t,函数pthread_cond_init()被用来初始化一个条件变量。它的原型为:
  extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
  其中cond是一个指向结构pthread_cond_t的指针,cond_attr是一个指向结构pthread_condattr_t的指针。结构pthread_condattr_t是条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量的函数为pthread_cond_ destroy(pthread_cond_t cond)。 
  函数pthread_cond_wait()使线程阻塞在一个条件变量上。它的函数原型为:
  extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
  pthread_mutex_t *__mutex));
  线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。
  另一个用来阻塞线程的函数是pthread_cond_timedwait(),它的原型为:
  extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
  pthread_mutex_t *__mutex, __const struct timespec *__abstime));
  它比函数pthread_cond_wait()多了一个时间参数,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。
  函数pthread_cond_signal()的原型为:
  extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
  它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait函数之间被发出,从而造成无限制的等待。下面是使用函数pthread_cond_wait()和函数pthread_cond_signal()的一个简单的例子。

pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count () {
pthread_mutex_lock (&count_lock);
while(count==0)
pthread_cond_wait( &count_nonzero, &count_lock);
count=count -1;
pthread_mutex_unlock (&count_lock);
}

increment_count(){
pthread_mutex_lock(&count_lock);
if(count==0)
pthread_cond_signal(&count_nonzero);
count=count+1;
pthread_mutex_unlock(&count_lock);
}
  count值为0
时,decrement函数在pthread_cond_wait处被阻塞,并打开互斥锁count_lock。此时,当调用到函数increment_count时,pthread_cond_signal()函数改变条件变量,告知decrement_count()停止阻塞。读者可以试着让两个线程分别运行这两个函数,看看会出现什么样的结果。
  函数pthread_cond_broadcast(pthread_cond_t *cond)用来唤醒所有被阻塞在条件变量cond上的线程。这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。

4.4 信号量
  信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。下面我们逐个介绍和信号量有关的一些函数,它们都在头文件/usr/include/semaphore.h中定义。
  信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
  extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
  sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
  函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。
  函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
  函数sem_destroy(sem_t *sem)用来释放信号量sem。
  下面我们来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。
/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 从文件1.dat读取数据,每读一次,信号量加一*/
void ReadData1(void){
FILE *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*从文件2.dat读取数据*/
void ReadData2(void){
FILE *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/
void HandleData1(void){
while(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}

void HandleData2(void){
while(1){
sem_wait(&sem);
printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}
int main(void){
pthread_t t1,t2,t3,t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* 防止程序过早退出,让它在此无限期等待*/
pthread_join(t1,NULL);
}

  在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem。 我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我们运行sem,得到如下的结果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11

  从中我们可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。

5 小结
  多线程编程是一个很有意思也很有用的技术,使用多线程技术的网络蚂蚁是目前最常用的下载工具之一,使用多线程技术的grep比单线程的grep要快上几倍,类似的例子还有很多。希望大家能用多线程技术写出高效实用的好程序来。

posted @ 2008-03-26 15:11 随意门 阅读(230) | 评论 (0)编辑 收藏
Linux下非常命令学习

转自http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_62.html

刚学linux的时候,有些东西不大熟悉,非常恼火
为了脱离这话总困境,把自己遇到并解决的一些常用命令行操作集中写到这里

1,如何删除非空目录?

用rmdir吗?不是,而是

#rm [your directory] -rf

意思是强制删除该目录,以及该目录下所有文件,试试,肯定奏效,呵呵
不过不要随便用,毫无提示就会删除掉的

而rmdir只能删除空目录哦
另外,如果不强制删除,只用
#rm [your directory] -r

2,压缩-解压缩命令大全

tar.gz这个比较常见
解压:tar zxvf FileName.tar.gz
压缩:tar zcvf FileName.tar.gz DirName

还在为面对一大堆的压缩文件无法解压缩而烦恼吗?
这里有比较全面的信息哦
http://www.chinaitlab.com/www/techspecial/tar/

3,如何用命令行创建和删除文件名开头为"-"的文件?

让我们来创建一个这样的文件“-test”
#touch -test
touch:日期格式 "est" 无效
#touch -- -test
#rm -test
rm:无效选项 --t
请尝试执行"rm --help"来获取更多帮助
#rm -- test
呵呵,是不是发现,只有加了"--"才可以正常操作阿

4,如果,我在键入ls命令以后只想显示文件的部分信息,我该怎么办呢?
也许你会查帮助ls --help
可是那么多的组合确实是让人烦恼
不过先在不用烦恼拉
因为我们有gawk

看看这个:ls -l | gawk '{printf $9}'
看看输出什么出来拉
是不是只有文件名拉
要是我还要别的呢,那就在printf后面再加一个$x(x为1到9之间的字符哦)

呵呵,其实gawk是一个脚本语言哦,功能非常强大,有兴趣看看相关的参考书去拉

5,有个好东西,可以对linux服务进行相关的操作

chkconf

6,用rpm命令安装和卸载软件

RPM共有10种基本的模式:它们是安装、查询、验证、删除等。

安装模式:     rpm –i [安装选项] <软件包>
查询模式:     rpm –q [查询选项]
验证模式:     rpm –V 或 –verify [验证选项]
删除模式:     rpm –e <软件包>

7,tee命令

这个命令的强大指处在于它会从标准输入设备读取数据,将其内容输出到标准输出设备,同时保存成文件。
例如,我们想把一个文件inputfile的内容即输出到终端上也保存成outputfile1,outputfile2,那么我们就可以这么来弄:

Quote:

cat inputfile | tee outputfile1 outputfile2



参考资料:
http://jkwx007.blogchina.com/2514993.html
http://jordi.blogbus.com/logs/2004/10/452282.html
http://bbs.3671041.com/dispbbs.asp?boardid=9&id=747&star=1&page=1
http://www.knowsky.com/print.asp?id=18403

posted @ 2008-03-18 14:27 随意门 阅读(242) | 评论 (0)编辑 收藏
《windows网络编程技术》之 Winsock基础 - [技术补钙]

转自http://xiekeli.blogbus.com/logs/4019775.html

前段时间根据客服的反映,老翁的前置机程序存在不工作的情况,初步表现为GPRS登录失败,我查看了报文(强烈要求老板发奖金,有什么问题我总 是冲锋在前)发现基本出现在网络频繁断开的情况后(网络每隔10分钟被断开一次,socket错误10053,什么原因还不得而知)。忘了说了,前置机是 通过TCP连接到省局的GPRS代理服务器(是由小赖开发的)然后和现场的终端进行通信。前置机程序中是通过delphi的clientsocket进行 连接的。一下子还真不知道是什么原因。对于socket这块我绝对不是专家,知其然,不知其所以然。于是我决定先从清理基本概念开始:
鸟瞰TCP/IP体系结构 
首先从TCP/IP体系结构开始(这也是不少公司面试时的必备良题啊),相信下图已经表达得非常清除了
其次是winsocket与tcp/ip(其实,不止TCP/IP协议族,这里只讨论TCP/IP) 
TCP/IP协议核心与应用程序关系图。

最后是常用协议特性:
 关于定址
Winsock中,通过SOCKADDR_IN结构来描述IP地址和服务端口:
struct sockaddr_in
{
      short                             sin_family;
      u_short                         sin_port;
      struct in_addr               sin_addr;
      char                              sin_zero[8];
};
哦,我只关心IP协议,所以sin_family = AF_INET;
关于端口要注意哦,0-1023为固定服务保留的(别打他们的注意了);1024-49151供普通用户的普通用户进程使用;49152-65535是动态和私有端口。
几个特殊地址:
  INADDR_ANY:允许服务器应用监听主机上每个网络接口上的客户机活动;
  INADDR_BROADCAST用于在一个IP网络中发送广播UDP数据报。
字节排序:
从主机字节顺序---> 网络字节顺序
返回四字节,用于IP地址
u_long htonl(u_long hostlong)
int WSAHtonl(
        SOCKET s,
        u_long hostlong,
        u_long FAR * lpnetlong
);
返回两字节,用于端口号
u_short htons(u_short hostshort);
int WSAHtons(
      SOCKET s,
      u_short hostshort,
      u_short FAR * lpnetshort
); 
对应的反向函数:
u_long ntohl(u_long netong)
int WSANtohl(
        SOCKETs,
        u_long netong,
        u_long FAR * lphostlong
);
u_short htons(u_short netshort);
int WSANtons(
      SOCKET s,
      u_short netshort,
      u_short FAR * lphostshort
); 
进入winsocket
 下面开始整理winsocket 的一些细节:
 所有的winsocket应用其实都是调用winsock dll 中的方法,所以通过WSAstartup加载是第一步。否则就会出错:WSANOTINITIALISED(10093)。
下面先来看看面向连接的协议:
从服务器端来看:
1.bind,将套接字和一个已知的地址进行绑定。
 
这样就创建了一个流套接字,这个步骤最常见的错误是WSAEADDRINUSE (10048) ,表示另外一个进程已经和本地IP和端口进行了绑定,或者那个IP地址和端口号处于TIME_WAIT状态。
2.Listen,将套接字置于监听状态。
  
  int listen(
        SOCKET s,
       int backlog
    )
backlog参数指定了正在等待连接的最大队列长度,如果实际访问的客户端大于该最大长度就会出错:WSAECONNREFUSED (10061)。事实上该backlog本身也是由基层协议提供者决定的。在这个阶段还有一种常见的错误就是WSAEINVAL (10022),即没有绑定就进行监听了。
3.accept和WSAAccept
SOCKET accept(
SOCKET s,
struct sockaddr FAR *addr,
int FAR* addrlen,
调用accept可为待决连接队列中的第一个连接请求提供服务。(在服务器端接收连接前,所有的客户端连接请求是放在一个“待决”队列中的。)
accept会返回一个新的套接字描述符,它对应于已经接受的那个客户机连接。对于
该客户机后续的所有操作,都应使用这个新套接字。至于原来那个监听套接字,它仍然用于
接受其他客户机连接,而且仍处于监听模式。
SOCKET WSAAccept(
SOCKET s,
struct sockaddr FAR *addr,
LPINT addrlen,
LPCONDITIONPROC lpfncondition,
DWORD dwCallBackData
对于客户端相对要简单得多,主要由以下几步:
1) 用socket或WSASocket创建一个套接字。
2) 解析服务器名(以基层协议为准)。
3) 用connect或WSAConnect初始化一个连接。
在connect过程常发生的错误有:WSAECONNREFUSED (10061)连接的计算机没有监听指定端口的进程;WSAETIMEDOUT (10060)这种情况一般发生在试图连接的计算机不能用时(亦可能因为到主机之间的路由上出现硬件故障或主机目前不在网上)。
连接之后就是数据传输了,就是发送和接收了:
int send(
    SOCKET s,
    const char FAR * buf,
    int len,
    int flags)
返回发送的字节数,如果出错常见的错误是:WSAECONNABORTED (10053) 这一错误一般发生在虚拟回路由于超时或协议有错而中断的时候。远程主机上的应用通过执行强行关闭或意外中断操作重新设置虚拟虚路时,或远程主机重新启动时,发生的则是WSAECONNRESET(10054)错误。。最后一个常见错误是WSAETIMEOUT(10060),它发生在连接由于网络故障或远程连接系统异常死机而引起的连接中断时。
int recv(
    SOCKET s,
    const char FAR * buf,
    int len,
    int flags)
无连接协议
    首先从接收端(类似于有连接方式中的服务端,但不是服务端)看,首先也是通过socket或WSAsocket创建套接字。再通过bind进行绑定。下面跳过Listen和Accept步骤,直接等待接收就可以了。
接收函数:
int recvfrom(
    SOCKET s,
    char FAR * buf,
    int len,
    int flags,
    struct SockAddr FAR *from,
    int FAR * fromlen
)
发送:建立SCOKET后调用sendto或WSASendTo
int sendto(
    SOCKET s,
    char FAR * buf,
    int len,
    int flags,
    struct SockAddr FAR * to,
    int FAR * tolen
)

posted @ 2008-03-18 11:16 随意门 阅读(596) | 评论 (0)编辑 收藏
仅列出标题
共9页: 1 2 3 4 5 6 7 8 9