前言
愛好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的作者談XSL:http://www-900.ibm.com/developerWorks/cn/xml/x-xslt/index.shtml
例子代碼就在我的博客中,包括六個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和多線程以及界面等功能都有大量方便可用的代碼庫,復用這些代碼庫會比自己動手寫方便很多。但是,掌握了基本原理再使用這些庫,事半功倍
引自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需要以下兩條命令:
這條命令會將cpp編譯為obj文件,若不使用/c參數則cl還會試圖繼續將obj鏈接為exe,但是這里是一個dll,沒有main函數,因此會報錯。不要緊,繼續使用鏈接命令。
這條命令會生成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的句柄。 使用如下命令編譯鏈接客戶端:
并執行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
觀察這一行
會發現該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命令結果:
這樣,顯式調用時只需查找函數名為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。
仔細觀察其源代碼,是不是有很多地方似曾相識啊,哈哈!