• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            興海北路

            ---男兒仗劍自橫行
            <2008年7月>
            293012345
            6789101112
            13141516171819
            20212223242526
            272829303112
            3456789

            統計

            • 隨筆 - 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 隨意門 閱讀(1378) | 評論 (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 隨意門 閱讀(631) | 評論 (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 隨意門 閱讀(293) | 評論 (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在軟件建模方面的不足?,F在XMI已經變成了各種軟件建模工具的通用存儲方式,可以將不同建模工具建立的模型互相轉換。

            總的來說,XML是一種基于文本的、格式化的數據存儲技術,它包括一系列的數據解決方案,它們是輕量級的、易于學習的、平臺無關的數據解決方案。弄清楚了這個概念再去學習XML,也許更有幫助。

            posted @ 2008-04-01 17:06 隨意門 閱讀(141) | 評論 (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 隨意門 閱讀(1121) | 評論 (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"http://看到沒有,這就是我們增加的頭文件

            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。使用最為方便??蛻舳舜a如下: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 隨意門 閱讀(239) | 評論 (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)中只允許有一個線程,這樣多線程就意味著多進程?,F在,多線程技術已經被許多操作系統所支持,包括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)?!?br>  函數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 隨意門 閱讀(233) | 評論 (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 隨意門 閱讀(246) | 評論 (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 隨意門 閱讀(601) | 評論 (0)編輯 收藏
            僅列出標題
            共9頁: 1 2 3 4 5 6 7 8 9 
            久久这里只有精品18| 国产精自产拍久久久久久蜜| 久久精品国产99国产精偷| 色婷婷久久综合中文久久一本| av无码久久久久不卡免费网站| 亚洲午夜无码久久久久小说| 国产精品欧美亚洲韩国日本久久| 国产麻豆精品久久一二三| 日韩精品久久久肉伦网站| 久久久亚洲AV波多野结衣| 久久这里的只有是精品23| 久久婷婷五月综合成人D啪| 久久久WWW免费人成精品| 久久国产美女免费观看精品| 国产精品成人99久久久久91gav| 人人狠狠综合久久亚洲婷婷| 97久久综合精品久久久综合| 97久久精品国产精品青草| 久久99国产精品久久| 国产2021久久精品| 久久亚洲2019中文字幕| 最新久久免费视频| 久久99热这里只有精品国产| 日产精品久久久久久久| 国产精品久久久久影院色| 久久九九亚洲精品| 久久精品国产精品亜洲毛片| 久久精品视屏| 久久精品青青草原伊人| 久久精品天天中文字幕人妻| 欧美精品一本久久男人的天堂| 久久e热在这里只有国产中文精品99| 久久精品综合一区二区三区| 久久综合亚洲色HEZYO社区| 无码人妻精品一区二区三区久久 | 国产亚洲精品久久久久秋霞 | 久久国产高清字幕中文| 91亚洲国产成人久久精品网址 | 一个色综合久久| 久久国产精品99精品国产| 99热精品久久只有精品|