前言
愛(ài)好XML的人最終會(huì)試著將XML轉(zhuǎn)換為HTML,或者轉(zhuǎn)換為其他類型的文檔,DOM/SAX顯然不是專門(mén)為轉(zhuǎn)換設(shè)計(jì)的,CSS對(duì)于轉(zhuǎn)換也是力有不逮,所以XML的愛(ài)好者們幾乎無(wú)一例外的要遭遇XSL,但是XSL似乎有非常多的用法,對(duì)于XML僅僅只是表示格式化的數(shù)據(jù)而言,XSL顯得復(fù)雜且毫無(wú)頭緒。
例如《跟我學(xué)XSL》和《XSL基礎(chǔ)入門(mén)》這樣的教程會(huì)帶給你XSL的一些概念和例子,但是對(duì)于XSL的運(yùn)行環(huán)境、平臺(tái)特性和本質(zhì),似乎都語(yǔ)焉不詳,你最終學(xué)會(huì)的僅僅是在XMLSPY或者IE中打開(kāi)你的XML看看它轉(zhuǎn)換后的效果罷了。一有人提到腳本語(yǔ)言或者JAVA中調(diào)用XSL你就頭大了,甚至你不清楚XSL和XSLT究竟有什么區(qū)別。迷失在網(wǎng)絡(luò)中的人們喜歡不停的用google搜索你想要的中文資料,但是其實(shí)有那個(gè)時(shí)間,干脆去那種技術(shù)的官方網(wǎng)站上好好看看吧。http://www.w3.org/Style/XSL/是XSL技術(shù)的W3C的官方網(wǎng)站,在網(wǎng)頁(yè)正文的第一行它就解釋和XSL和XSLT的區(qū)別。原文如下:
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文檔的轉(zhuǎn)換和顯示特征的推薦標(biāo)準(zhǔn),它包括三個(gè)部分:XSL轉(zhuǎn)換(XSLT)是一種為了轉(zhuǎn)換XML而定義的語(yǔ)言;XML路徑語(yǔ)言(XPath)是一種表達(dá)式語(yǔ)言,它被XSLT用來(lái)訪問(wèn)或者提交一個(gè)XML文檔的某些部分(XPath也同時(shí)被XML Linking標(biāo)準(zhǔn)使用);XSL格式化對(duì)象(XSL-FO)是一個(gè)XML詞匯表用來(lái)定義XML的格式化語(yǔ)義。
從何開(kāi)始
一般人學(xué)習(xí)XSL都是從XMLSPY等工具開(kāi)始運(yùn)行他的一個(gè)XSL例子,當(dāng)然用文本編輯器編輯XML何XSL文件,用IE去打開(kāi)XML也是一個(gè)好主意。因?yàn)閄MLSPY和IE都有嵌入式的XSL解析器,例如IE的XSL解析器是MSXML,這樣不用顯式的調(diào)用XSL進(jìn)行轉(zhuǎn)換過(guò)程,只需要在XML文檔的頭部加上一句<?xml:stylesheet type="text/xsl" href="xxx.xsl"?>就可以讓嵌入的XSL解析器自動(dòng)的進(jìn)行轉(zhuǎn)換了。例如下面這個(gè)著名的例子,它包括cd_catalog.xml和cd_catalog.xsl文件,內(nèi)容如下:
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>女子十二樂(lè)坊</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直接打開(kāi)xml文件,則會(huì)看到轉(zhuǎn)換后的效果。當(dāng)然用XMLSPY中自帶的瀏覽器也可。
用JScript顯式調(diào)用XSL解析器
上面的運(yùn)行方法顯然是“貪天之功”,利用了IE和XMLSPY自帶的XSL解析器,是讓一只看不見(jiàn)的手運(yùn)行了轉(zhuǎn)換過(guò)程。那么,也可以用Jscript語(yǔ)言顯式的調(diào)用XSL解析器,讓沒(méi)有嵌入解析器的瀏覽器也可以運(yùn)行XSL,當(dāng)然,此瀏覽器必須支持Jscript腳本語(yǔ)言。我們還是使用上面的例子,不過(guò)將cd_catalog.xml中的<?xml:stylesheet type="text/xsl" href="cd_catalog.xsl"?>這一行去掉,同時(shí)新建一個(gè)cd_catalog.html文檔,內(nèi)容如下:
<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的瀏覽器中打開(kāi),即可看到如前一段執(zhí)行的結(jié)果。當(dāng)然不僅僅是Jscript,其他的腳本語(yǔ)言如VBScript等等也可以,不過(guò)Jscript是XSL默認(rèn)的腳本語(yǔ)言。
腳本擴(kuò)充的XSL,令人疑惑的xsl:eval標(biāo)記
xsl:eval標(biāo)記并不是一個(gè)標(biāo)準(zhǔn)的xsl標(biāo)記,它屬于http://www.w3.org/TR/WD-xsl這個(gè)名字空間,這個(gè)名字空間最終被微軟采用,于是xsl:eval也被微軟用來(lái)調(diào)用Jscript腳本,以此來(lái)擴(kuò)充XSL的功能。而標(biāo)準(zhǔn)的XSL1.0版本的名字空間是http://www.w3.org/1999/XSL/Transform,它并不包含xsl:eval標(biāo)記,這是很容易理解的,XSL應(yīng)該屬于一個(gè)平臺(tái)無(wú)關(guān)的技術(shù),如果它的某個(gè)標(biāo)記要依賴微軟公司的產(chǎn)品,那顯然是自掘墳?zāi)埂jP(guān)于平臺(tái)無(wú)關(guān)的討論,將在本文的最后展開(kāi)。
xsl:eval標(biāo)記的含義是計(jì)算其中腳本語(yǔ)言的表達(dá)式,并作為文本輸出。下面的例子中計(jì)算了cd_catalog.xml中各種CD的總價(jià)格,修改上面的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>合計(jì)</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中打開(kāi)cd_catalog.xml文件(注意修改xsl為cd_catalog2.xsl)即可看到結(jié)果,注意這個(gè)xsl文件的這一行<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/TR/WD-xsl">,寫(xiě)錯(cuò)了名字空間xsl:eval標(biāo)記就會(huì)報(bào)錯(cuò)。
瀏覽器無(wú)關(guān)的XSL解決方案,服務(wù)端的XSL
不管如何折騰,要將XML通過(guò)XSL轉(zhuǎn)換為HTML必須要求本地主機(jī)上有一個(gè)XSL解析器,不管是瀏覽器內(nèi)嵌的,還是可以通過(guò)腳本語(yǔ)言調(diào)用。那么,更好的解決方案當(dāng)然是從服務(wù)器端直接發(fā)送HTML回來(lái),這樣無(wú)論什么瀏覽器都可以看到轉(zhuǎn)換的結(jié)果了。ASP提供了這個(gè)功能,這是可想而知的,不過(guò)我對(duì)ASP不熟,這段略過(guò),有興趣的可以找本ASP的XML教材看看。
應(yīng)用程序中的XSL,語(yǔ)言相關(guān)的XSL
眾所周知,Java是對(duì)XML技術(shù)支持得最好的語(yǔ)言,Java上面的xml包非常多,其中支持XSL轉(zhuǎn)換的包最著名的有Saxon和xalan。Saxon包可以在http://saxon.sourceforge.net/上面下載。將Saxon包解壓縮到C:\saxon6_5_3,6.5.3版本提供了對(duì)XSL1.0最穩(wěn)定的支持。然后在Classpath中加入C:\saxon6_5_3\saxon.jar;C:\saxon6_5_3\saxon-jdom.jar。
Saxon提供命令行式的XSL轉(zhuǎn)換和API。其中命令行式的轉(zhuǎn)換如下,將目錄移動(dòng)到存放xml(去掉xml的指定xsl的那一行)和xsl的目錄,然后輸入下面的命令:
java com.icl.saxon.StyleSheet cd_catalog.xml cd_catalog.xsl
就可以看到輸出在屏幕上的結(jié)果,但是這樣看起來(lái)不方便,所以輸入如下命令:
java com.icl.saxon.StyleSheet cd_catalog.xml cd_catalog.xsl>a.html
然后將生成的a.html在瀏覽器中打開(kāi),可以清晰的看到結(jié)果。
下面是在Java程序中調(diào)用Saxon包,進(jìn)行XSL轉(zhuǎn)換的例子,文件名為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();
}
}
}
這個(gè)例子程序?qū)d_catalog.xml文件使用cd_catalog.xsl轉(zhuǎn)換為result.html。在Eclipse3.01中調(diào)試通過(guò)(Saxon沒(méi)有簡(jiǎn)單的xsl示例程序,我也是將com.icl.saxon.StyleSheet類拔光了才得到這個(gè)稍微簡(jiǎn)單的例子,如果需要更詳細(xì)的用法,參考com.icl.saxon.StyleSheet類)。
數(shù)據(jù)是獨(dú)立的,處理是平臺(tái)相關(guān)的
總結(jié)前面的內(nèi)容,可以看出XSL轉(zhuǎn)換可以從這幾個(gè)地方開(kāi)始:
Ø IE,XMLSPY:嵌入的解析器,例如MSXML3;
Ø JScript,顯式調(diào)用XSL解析器;
Ø 用JScript擴(kuò)充XSL功能,半吊子的XSL;
Ø 瀏覽器無(wú)關(guān)的XSL解決方案,服務(wù)器端的XSL,ASP顯式調(diào)用XSL;
Ø 語(yǔ)言相關(guān)的XSL,Java的XSL包Saxon,xalan。
可以看出來(lái),XSL無(wú)論如何,都是要平臺(tái)相關(guān)的,第一種方法依賴嵌入瀏覽器的XSL解析器;第二、三種方法依賴操作系統(tǒng)安裝的XSL解析器;第四種方法依賴服務(wù)器端安裝的XSL解析器;最后的方法依賴JAVA語(yǔ)言提供的XSL API。其中微軟還不顧W3C的反對(duì),自定義了XSL的腳本擴(kuò)充功能,功能倒是強(qiáng)大了,可惜脫離了Windows就玩不轉(zhuǎn)了。JAVA號(hào)稱平臺(tái)無(wú)關(guān),可是JAVA本身就是一個(gè)平臺(tái),要是有人的機(jī)器沒(méi)有JRE又怎么辦呢?丟棄XSL?
不過(guò)事物總是有因果的,其實(shí)XML作為數(shù)據(jù)的存儲(chǔ)載體,可以做到完全的平臺(tái)無(wú)關(guān),但是XSL作為一個(gè)可執(zhí)行的語(yǔ)言,一定要依賴某種已存在的運(yùn)行環(huán)境的,就如同數(shù)據(jù)庫(kù)中的表格和SQL語(yǔ)言一樣。SQL號(hào)稱適用于任何關(guān)系數(shù)據(jù)庫(kù),但是實(shí)際上還是需要一個(gè)環(huán)境來(lái)run的。那么XSL是否破壞了XML的平臺(tái)無(wú)關(guān)性呢?我認(rèn)為沒(méi)有,因?yàn)閄SL本身是一個(gè)XML文檔,XML文檔可以平臺(tái)無(wú)關(guān)的保存和傳輸,至于使用何種方法來(lái)調(diào)用它則是另外考慮的問(wèn)題。再者,XSL的源和目標(biāo)都是平臺(tái)無(wú)關(guān)的文檔(例如XML和HTML),而它自己的調(diào)用方式則是可替換的,這點(diǎn)也減輕了XSL的負(fù)罪感吧。
以上的討論都是基于XSL1.0標(biāo)準(zhǔn)的,目前XSL2.0標(biāo)準(zhǔn)尚在討論中,不過(guò)初稿已經(jīng)發(fā)布了,而Saxon8.0以上的版本號(hào)稱已經(jīng)支持了XSL2.0。讓我們拭目以待XSL2.0帶給我們的驚喜。
參考文獻(xiàn)
W3C站點(diǎn):http://www.w3.org/Style/XSL/
XSL主題:http://www-900.ibm.com/developerWorks/cn/xml/theme/x-xsl.shtml
中文譯文站點(diǎn):http://www.opendl.com/
XSLT是什么類型的語(yǔ)言,SAXON的作者談XSL:http://www-900.ibm.com/developerWorks/cn/xml/x-xslt/index.shtml
例子代碼就在我的博客中,包括六個(gè)UDP和TCP發(fā)送接受的cpp文件,一個(gè)基于MFC的局域網(wǎng)聊天小工具工程,和此小工具的所有運(yùn)行時(shí)庫(kù)、資源和執(zhí)行程序。代碼的壓縮包位置是http://www.blogjava.net/Files/wxb_nudt/socket_src.rar。
1 前言
在一些常用的編程技術(shù)中,Socket網(wǎng)絡(luò)編程可以說(shuō)是最簡(jiǎn)單的一種。而且Socket編程需要的基礎(chǔ)知識(shí)很少,適合初學(xué)者學(xué)習(xí)網(wǎng)絡(luò)編程。目前支持網(wǎng)絡(luò)傳輸?shù)募夹g(shù)、語(yǔ)言和工具繁多,但是大部分都是基于Socket開(kāi)發(fā)的,雖說(shuō)這些“高級(jí)”的網(wǎng)絡(luò)技術(shù)屏蔽了大部分底層實(shí)現(xiàn),號(hào)稱能極大程度的簡(jiǎn)化開(kāi)發(fā),而事實(shí)上如果你沒(méi)有一點(diǎn)Socket基礎(chǔ),要理解和應(yīng)用這些技術(shù)還是很困難的,而且會(huì)讓你成為“半瓢水”。
深有感觸的是當(dāng)年我學(xué)習(xí)CORBA的時(shí)候,由于當(dāng)時(shí)各方面的基礎(chǔ)薄弱,整整啃了半年書(shū),最終還是一頭霧水。如果現(xiàn)在讓我?guī)б粋€(gè)人學(xué)CORBA,我一定會(huì)安排好順序:首先弄清C++語(yǔ)法;然后是VC編譯環(huán)境或者nmake的用法;接下來(lái)學(xué)習(xí)一些網(wǎng)絡(luò)基礎(chǔ)知識(shí);然后是Socket編程;這些大概要花費(fèi)3、4個(gè)月。有了這些基礎(chǔ)學(xué)習(xí)CORBA一周即可弄懂,兩個(gè)月就可以基于CORBA進(jìn)行開(kāi)發(fā)了。
好了,說(shuō)了半天其實(shí)中心思想就一個(gè),Socket很簡(jiǎn)單,很好學(xué)!如果你會(huì)C++或者JAVA,又懂一點(diǎn)點(diǎn)網(wǎng)絡(luò)基礎(chǔ)如TCP和UDP的機(jī)制,那么你看完本文就可以熟練進(jìn)行Socket開(kāi)發(fā)了。
2 Socket簡(jiǎn)介(全文摘抄)
(本節(jié)內(nèi)容全部抄自網(wǎng)絡(luò),不保證正確性,有興趣的可以看看!)
80年代初,美國(guó)政府的高級(jí)研究工程機(jī)構(gòu)(ARPA)給加利福尼亞大學(xué)Berkeley分校提供了資金,讓他們?cè)赨NIX操作系統(tǒng)下實(shí)現(xiàn)TCP/IP協(xié)議。在這個(gè)項(xiàng)目中,研究人員為T(mén)CP/IP網(wǎng)絡(luò)通信開(kāi)發(fā)了一個(gè)API(應(yīng)用程序接口)。這個(gè)API稱為Socket接口(套接字)。今天,SOCKET接口是TCP/IP網(wǎng)絡(luò)最為通用的API,也是在INTERNET上進(jìn)行應(yīng)用開(kāi)發(fā)最為通用的API。
90年代初,由Microsoft聯(lián)合了其他幾家公司共同制定了一套WINDOWS下的網(wǎng)絡(luò)編程接口,即WindowsSockets規(guī)范。它是BerkeleySockets的重要擴(kuò)充,主要是增加了一些異步函數(shù),并增加了符合Windows消息驅(qū)動(dòng)特性的網(wǎng)絡(luò)事件異步選擇機(jī)制。WINDOWSSOCKETS規(guī)范是一套開(kāi)放的、支持多種協(xié)議的Windows下的網(wǎng)絡(luò)編程接口。從1991年的1.0版到1995年的2.0.8版,經(jīng)過(guò)不斷完善并在Intel、Microsoft、Sun、SGI、Informix、Novell等公司的全力支持下,已成為Windows網(wǎng)絡(luò)編程的事實(shí)上的標(biāo)準(zhǔn)。目前,在實(shí)際應(yīng)用中的WINDOWSSOKCETS規(guī)范主要有1.1版和2.0版。兩者的最重要區(qū)別是1.1版只支持TCP/IP協(xié)議,而2.0版可以支持多協(xié)議。2.0版有良好的向后兼容性,任何使用1.1版的源代碼,二進(jìn)制文件,應(yīng)用程序都可以不加修改地在2.0規(guī)范下使用。
SOCKET實(shí)際在計(jì)算機(jī)中提供了一個(gè)通信端口,可以通過(guò)這個(gè)端口與任何一個(gè)具有SOCKET接口的計(jì)算機(jī)通信。應(yīng)用程序在網(wǎng)絡(luò)上傳輸,接收的信息都通過(guò)這個(gè)SOCKET接口來(lái)實(shí)現(xiàn)。在應(yīng)用開(kāi)發(fā)中就像使用文件句柄一樣,可以對(duì)SOCKET句柄進(jìn)行讀,寫(xiě)操作。
3 再說(shuō)兩句
網(wǎng)上很多文章對(duì)于Socket的來(lái)龍去脈有如教科書(shū)一般的精準(zhǔn)。但是涉及具體編程技術(shù)就往往被VC等集成開(kāi)發(fā)環(huán)境所毒害了,把Windows SDK、MFC、Socket、多線程、DLL以及編譯鏈接等等技術(shù)攪合在一起煮成一鍋夾生飯。
既然要學(xué)習(xí)Socket,就應(yīng)該用最簡(jiǎn)單直白的方式把Socket的幾個(gè)使用要點(diǎn)講出來(lái)。我認(rèn)為程序員最關(guān)心的有以下幾點(diǎn),按照優(yōu)先級(jí)排列如下:
1. Socket的機(jī)制是什么?
2. 用C/C++寫(xiě)Socket需要什么頭文件、庫(kù)文件、DLL,它們可以由誰(shuí)提供,安裝后一般處于系統(tǒng)的哪個(gè)文件夾內(nèi)?
3. 編寫(xiě)Socket程序需要的編程基礎(chǔ)是什么?
4. Socket庫(kù)內(nèi)最重要的幾個(gè)函數(shù)和數(shù)據(jù)類型是什么?
5. 兩個(gè)最簡(jiǎn)單的例子程序;
6. 一個(gè)貼近應(yīng)用的稍微復(fù)雜的Socket應(yīng)用程序。
我將一一講述這些要點(diǎn),并給出從簡(jiǎn)到繁,從樸素到花哨的所有源代碼以及編譯鏈接的命令。
4 Socket的機(jī)制是什么?
我們可以簡(jiǎn)單的把Socket理解為一個(gè)可以連通網(wǎng)絡(luò)上不同計(jì)算機(jī)程序之間的管道,把一堆數(shù)據(jù)從管道的A端扔進(jìn)去,則會(huì)從管道的B端(也許同時(shí)還可以從C、D、E、F……端冒出來(lái))。管道的端口由兩個(gè)因素來(lái)唯一確認(rèn),即機(jī)器的IP地址和程序所使用的端口號(hào)。IP地址的含義所有人都知道,所謂端口號(hào)就是程序員指定的一個(gè)數(shù)字,許多著名的木馬程序成天在網(wǎng)絡(luò)上掃描不同的端口號(hào)就是為了獲取一個(gè)可以連通的端口從而進(jìn)行破壞。比較著名的端口號(hào)有http的80端口和ftp的21端口(我記錯(cuò)了么?)。當(dāng)然,建議大家自己寫(xiě)程序不要使用太小的端口號(hào),它們一般被系統(tǒng)占用了,也不要使用一些著名的端口,一般來(lái)說(shuō)使用1000~5000之內(nèi)的端口比較好。
Socket可以支持?jǐn)?shù)據(jù)的發(fā)送和接收,它會(huì)定義一種稱為套接字的變量,發(fā)送數(shù)據(jù)時(shí)首先創(chuàng)建套接字,然后使用該套接字的sendto等方法對(duì)準(zhǔn)某個(gè)IP/端口進(jìn)行數(shù)據(jù)發(fā)送;接收端也首先創(chuàng)建套接字,然后將該套接字綁定到一個(gè)IP/端口上,所有發(fā)向此端口的數(shù)據(jù)會(huì)被該套接字的recv等函數(shù)讀出。如同讀出文件中的數(shù)據(jù)一樣。
5 所需的頭文件、庫(kù)文件和DLL
對(duì)于目前使用最廣泛的Windows Socket2.0版本,所需的一些文件如下(以安裝了VC6為例說(shuō)明其物理位置):
l 頭文件winsock2.h,通常處于C:"Program Files"Microsoft Visual Studio"VC98"INCLUDE;查看該頭文件可知其中又包含了windows.h和pshpack4.h頭文件,因此在windows中的一些常用API都可以使用;
l 庫(kù)文件Ws2_32.lib,通常處于C:"Program Files"Microsoft Visual Studio"VC98"Lib;
l DLL文件Ws2_32.dll,通常處于C:"WINDOWS"system32,這個(gè)是可以猜到的。
6 編寫(xiě)Socket程序需要的編程基礎(chǔ)
在開(kāi)始編寫(xiě)Socket程序之前,需要以下編程基礎(chǔ):
l C++語(yǔ)法;
l 一點(diǎn)點(diǎn)windows SDK的基礎(chǔ),了解一些SDK的數(shù)據(jù)類型與API的調(diào)用方式;
l 一點(diǎn)點(diǎn)編譯、鏈接和執(zhí)行的技術(shù);知道cl和link的最常用用法即可。
7 UDP
用最通俗的話講,所謂UDP,就是發(fā)送出去就不管的一種網(wǎng)絡(luò)協(xié)議。因此UDP編程的發(fā)送端只管發(fā)送就可以了,不用檢查網(wǎng)絡(luò)連接狀態(tài)。下面用例子來(lái)說(shuō)明怎樣編寫(xiě)UDP,并會(huì)詳細(xì)解釋每個(gè)API和數(shù)據(jù)類型。
7.1 UDP廣播發(fā)送程序
下面是一個(gè)用UDP發(fā)送廣播報(bào)文的例子。
#include <winsock2.h>
#include <iostream.h>
void main()
{
SOCKET sock; //socket套接字
char szMsg[] = "this is a UDP test package";//被發(fā)送的字段
//1.啟動(dòng)SOCKET庫(kù),版本為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庫(kù)的版本是否為2.0
if (LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 0 )
{
WSACleanup( );
return;
}
//2.創(chuàng)建socket,
sock = socket(
AF_INET, //internetwork: UDP, TCP, etc
SOCK_DGRAM, //SOCK_DGRAM說(shuō)明是UDP類型
0 //protocol
);
if (INVALID_SOCKET == sock ) {
cout<<"Socket 創(chuàng)建失敗,Exit!";
return;
}
//3.設(shè)置該套接字為廣播類型,
bool opt = true;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, reinterpret_cast<char FAR *>(&opt), sizeof(opt));
//4.設(shè)置發(fā)往的地址
sockaddr_in addrto; //發(fā)往的地址
memset(&addrto,0,sizeof(addrto));
addrto.sin_family = AF_INET; //地址類型為internetwork
addrto.sin_addr.s_addr = INADDR_BROADCAST; //設(shè)置ip為廣播地址
addrto.sin_port = htons(7861); //端口號(hào)為7861
int nlen=sizeof(addrto);
unsigned int uIndex = 1;
while(true)
{
Sleep(1000); //程序休眠一秒
//向廣播地址發(fā)送消息
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)) //關(guān)閉套接字
{
WSAGetLastError();
return;
}
if (!WSACleanup()) //關(guān)閉Socket庫(kù)
{
WSAGetLastError();
return;
}
}
編譯命令:
CL /c UDP_Send_Broadcast.cpp
鏈接命令(注意如果找不到該庫(kù),則要在后面的/LIBPATH參數(shù)后加上庫(kù)的路徑):
link UDP_Send_Broadcast.obj ws2_32.lib
執(zhí)行命令:
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
下面一一解釋代碼中出現(xiàn)的數(shù)據(jù)類型與API函數(shù)。有耐心的可以仔細(xì)看看,沒(méi)耐心的依葫蘆畫(huà)瓢也可以寫(xiě)程序了。
7.2 SOCKET類型
SOCKET是socket套接字類型,在WINSOCK2.H中有如下定義:
typedef unsigned int u_int;
typedef u_int SOCKET;
可知套接字實(shí)際上就是一個(gè)無(wú)符號(hào)整型,它將被Socket環(huán)境管理和使用。套接字將被創(chuàng)建、設(shè)置、用來(lái)發(fā)送和接收數(shù)據(jù),最后會(huì)被關(guān)閉。
7.3 WORD類型、MAKEWORD、LOBYTE和HIBYTE宏
WORD類型是一個(gè)16位的無(wú)符號(hào)整型,在WTYPES.H中被定義為:
typedef unsigned short WORD;
其目的是提供兩個(gè)字節(jié)的存儲(chǔ),在Socket中這兩個(gè)字節(jié)可以表示主版本號(hào)和副版本號(hào)。使用MAKEWORD宏可以給一個(gè)WORD類型賦值。例如要表示主版本號(hào)2,副版本號(hào)0,可以使用以下代碼:
WORD wVersionRequested;
wVersionRequested = MAKEWORD( 2, 0 );
注意低位內(nèi)存存儲(chǔ)主版本號(hào)2,高位內(nèi)存存儲(chǔ)副版本號(hào)0,其值為0x0002。使用宏LOBYTE可以讀取WORD的低位字節(jié),HIBYTE可以讀取高位字節(jié)。
7.4 WSADATA類型和LPWSADATA類型
WSADATA類型是一個(gè)結(jié)構(gòu),描述了Socket庫(kù)的一些相關(guān)信息,其結(jié)構(gòu)定義如下:
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字段,存儲(chǔ)了Socket的版本類型。LPWSADATA是WSADATA的指針類型。它們不用程序員手動(dòng)填寫(xiě),而是通過(guò)Socket的初始化函數(shù)WSAStartup讀取出來(lái)。
7.5 WSAStartup函數(shù)
WSAStartup函數(shù)被用來(lái)初始化Socket環(huán)境,它的定義如下:
int PASCAL FAR WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData);
其返回值為整型,調(diào)用方式為PASCAL(即標(biāo)準(zhǔn)類型,PASCAL等于__stdcall),參數(shù)有兩個(gè),第一個(gè)參數(shù)為WORD類型,指明了Socket的版本號(hào),第二個(gè)參數(shù)為WSADATA類型的指針。
若返回值為0,則初始化成功,若不為0則失敗。
7.6 WSACleanup函數(shù)
這是Socket環(huán)境的退出函數(shù)。返回值為0表示成功,SOCKET_ERROR表示失敗。
7.7 socket函數(shù)
socket的創(chuàng)建函數(shù),其定義為:
SOCKET PASCAL FAR socket (int af, int type, int protocol);
第一個(gè)參數(shù)為int af,代表網(wǎng)絡(luò)地址族,目前只有一種取值是有效的,即AF_INET,代表internet地址族;
第二個(gè)參數(shù)為int type,代表網(wǎng)絡(luò)協(xié)議類型,SOCK_DGRAM代表UDP協(xié)議,SOCK_STREAM代表TCP協(xié)議;
第三個(gè)參數(shù)為int protocol,指定網(wǎng)絡(luò)地址族的特殊協(xié)議,目前無(wú)用,賦值0即可。
返回值為SOCKET,若返回INVALID_SOCKET則失敗。
7.8 setsockopt函數(shù)
這個(gè)函數(shù)用來(lái)設(shè)置Socket的屬性,若不能正確設(shè)置socket屬性,則數(shù)據(jù)的發(fā)送和接收會(huì)失敗。定義如下:
int PASCAL FAR setsockopt (SOCKET s, int level, int optname,
const char FAR * optval, int optlen);
其返回值為int類型,0代表成功,SOCKET_ERROR代表有錯(cuò)誤發(fā)生。
第一個(gè)參數(shù)SOCKET s,代表要設(shè)置的套接字;
第二個(gè)參數(shù)int level,代表要設(shè)置的屬性所處的層次,層次包含以下取值:SOL_SOCKET代表套接字層次;IPPROTO_TCP代表TCP協(xié)議層次,IPPROTO_IP代表IP協(xié)議層次(后面兩個(gè)我都沒(méi)有用過(guò));
第三個(gè)參數(shù)int optname,代表設(shè)置參數(shù)的名稱,SO_BROADCAST代表允許發(fā)送廣播數(shù)據(jù)的屬性,其它屬性可參考MSDN;
第四個(gè)參數(shù)const char FAR * optval,代表指向存儲(chǔ)參數(shù)數(shù)值的指針,注意這里可能要使用reinterpret_cast類型轉(zhuǎn)換;
第五個(gè)參數(shù)int optlen,代表存儲(chǔ)參數(shù)數(shù)值變量的長(zhǎng)度。
7.9 sockaddr_in、in_addr類型,inet_addr、inet_ntoa函數(shù)
sockaddr_in定義了socket發(fā)送和接收數(shù)據(jù)包的地址,定義:
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的含義,很顯然它是一個(gè)存儲(chǔ)ip地址的聯(lián)合體(忘記union含義的請(qǐng)看c++書(shū)),有三種表達(dá)方式:
第一種用四個(gè)字節(jié)來(lái)表示IP地址的四個(gè)數(shù)字;
第二種用兩個(gè)雙字節(jié)來(lái)表示IP地址;
第三種用一個(gè)長(zhǎng)整型來(lái)表示IP地址。
給in_addr賦值的一種最簡(jiǎn)單方法是使用inet_addr函數(shù),它可以把一個(gè)代表IP地址的字符串賦值轉(zhuǎn)換為in_addr類型,如
addrto.sin_addr.s_addr=inet_addr("192.168.0.2");
本例子中由于是廣播地址,所以沒(méi)有使用這個(gè)函數(shù)。其反函數(shù)是inet_ntoa,可以把一個(gè)in_addr類型轉(zhuǎn)換為一個(gè)字符串。
sockaddr_in的含義比in_addr的含義要廣泛,其各個(gè)字段的含義和取值如下:
第一個(gè)字段short sin_family,代表網(wǎng)絡(luò)地址族,如前所述,只能取值A(chǔ)F_INET;
第二個(gè)字段u_short sin_port,代表IP地址端口,由程序員指定;
第三個(gè)字段struct in_addr sin_addr,代表IP地址;
第四個(gè)字段char sin_zero[8],很搞笑,是為了保證sockaddr_in與SOCKADDR類型的長(zhǎng)度相等而填充進(jìn)來(lái)的字段。
以下代表指明了廣播地址,端口號(hào)為7861的一個(gè)地址:
sockaddr_in addrto; //發(fā)往的地址
memset(&addrto,0,sizeof(addrto));
addrto.sin_family = AF_INET; //地址類型為internetwork
addrto.sin_addr.s_addr = INADDR_BROADCAST; //設(shè)置ip為廣播地址
addrto.sin_port = htons(7861); //端口號(hào)為7861
7.10 sockaddr類型
sockaddr類型是用來(lái)表示Socket地址的類型,同上面的sockaddr_in類型相比,sockaddr的適用范圍更廣,因?yàn)閟ockaddr_in只適用于TCP/IP地址。Sockaddr的定義如下:
struct sockaddr {
u_short sa_family;
char sa_data[14];
};
可知sockaddr有16個(gè)字節(jié),而sockaddr_in也有16個(gè)字節(jié),所以sockaddr_in是可以強(qiáng)制類型轉(zhuǎn)換為sockaddr的。事實(shí)上也往往使用這種方法。
7.11 Sleep函數(shù)
線程掛起函數(shù),表示線程掛起一段時(shí)間。Sleep(1000)表示掛起一秒。定義于WINBASE.H頭文件中。WINBASE.H又被包含于WINDOWS.H中,然后WINDOWS.H被WINSOCK2.H包含。所以在本例中使用Sleep函數(shù)不需要包含其它頭文件。
7.12 sendto函數(shù)
在Socket中有兩套發(fā)送和接收函數(shù),一是sendto和recvfrom;二是send和recv。前一套在函數(shù)參數(shù)中要指明地址;而后一套需要先將套接字和一個(gè)地址綁定,然后直接發(fā)送和接收,不需綁定地址。sendto的定義如下:
int PASCAL FAR sendto (SOCKET s, const char FAR * buf, int len, int flags, const struct sockaddr FAR *to, int tolen);
第一個(gè)參數(shù)就是套接字;
第二個(gè)參數(shù)是要傳送的數(shù)據(jù)指針;
第三個(gè)參數(shù)是要傳送的數(shù)據(jù)長(zhǎng)度(字節(jié)數(shù));
第四個(gè)參數(shù)是傳送方式的標(biāo)識(shí),如果不需要特殊要求則可以設(shè)置為0,其它值請(qǐng)參考MSDN;
第五個(gè)參數(shù)是目標(biāo)地址,注意這里使用的是sockaddr的指針;
第六個(gè)參數(shù)是地址的長(zhǎng)度;
返回值為整型,如果成功,則返回發(fā)送的字節(jié)數(shù),失敗則返回SOCKET_ERROR。
7.13 WSAGetLastError函數(shù)
該函數(shù)用來(lái)在Socket相關(guān)API失敗后讀取錯(cuò)誤碼,根據(jù)這些錯(cuò)誤碼可以對(duì)照查出錯(cuò)誤原因。
7.14 closesocket
關(guān)閉套接字,其參數(shù)為SOCKET類型。成功返回0,失敗返回SOCKET_ERROR。
7.15 小結(jié)
總結(jié)以上內(nèi)容,寫(xiě)一個(gè)UDP發(fā)送程序的步驟如下:
1. 用WSAStartup函數(shù)初始化Socket環(huán)境;
2. 用socket函數(shù)創(chuàng)建一個(gè)套接字;
3. 用setsockopt函數(shù)設(shè)置套接字的屬性,例如設(shè)置為廣播類型;很多時(shí)候該步驟可以省略;
4. 創(chuàng)建一個(gè)sockaddr_in,并指定其IP地址和端口號(hào);
5. 用sendto函數(shù)向指定地址發(fā)送數(shù)據(jù),這里的目標(biāo)地址就是廣播地址;注意這里不需要綁定,即使綁定了,其地址也會(huì)被sendto中的參數(shù)覆蓋;若使用send函數(shù)則會(huì)出錯(cuò),因?yàn)閟end是面向連接的,而UDP是非連接的,只能使用sendto發(fā)送數(shù)據(jù);
6. 用closesocket函數(shù)關(guān)閉套接字;
7. 用WSACleanup函數(shù)關(guān)閉Socket環(huán)境。
那么,與之類似,一個(gè)UDP接收程序的步驟如下,注意接收方一定要bind套接字:
1. 用WSAStartup函數(shù)初始化Socket環(huán)境;
2. 用socket函數(shù)創(chuàng)建一個(gè)套接字;
3. 用setsockopt函數(shù)設(shè)置套接字的屬性,例如設(shè)置為廣播類型;
4. 創(chuàng)建一個(gè)sockaddr_in,并指定其IP地址和端口號(hào);
5. 用bind函數(shù)將套接字與接收的地址綁定起來(lái),然后調(diào)用recvfrom函數(shù)或者recv接收數(shù)據(jù); 注意這里一定要綁定,因?yàn)榻邮請(qǐng)?bào)文的套接字必須在網(wǎng)絡(luò)上有一個(gè)綁定的名稱才能保證正確接收數(shù)據(jù);
6. 用closesocket函數(shù)關(guān)閉套接字;
7. 用WSACleanup函數(shù)關(guān)閉Socket環(huán)境。
廣播接收程序見(jiàn)源程序代碼UDP_Recv_Broadcast.cpp。編譯、鏈接、執(zhí)行與UDP_Send_Broadcast類似。
7.16 UDP點(diǎn)對(duì)點(diǎn)發(fā)送接收程序
廣播發(fā)送和接收使用并不廣泛,一般來(lái)說(shuō)指定發(fā)送和接收的IP比較常用。點(diǎn)對(duì)點(diǎn)方式的UDP發(fā)送和接收與上面的例子非常類似,不同的就是需要指定一個(gè)具體的IP地址。并且不需要調(diào)用setsockopt設(shè)置socket的廣播屬性。
其具體源代碼見(jiàn)UDP_Send_P2P.cpp和UDP_Recv_P2P.cpp。
注意在使用這兩個(gè)程序時(shí)要設(shè)為自己所需的IP。
8 TCP
TCP與UDP最大的不同之處在于TCP是一個(gè)面向連接的協(xié)議,在進(jìn)行數(shù)據(jù)收發(fā)之前TCP必須進(jìn)行連接,并且在收發(fā)的時(shí)候必須保持該連接。
發(fā)送方的步驟如下(省略了Socket環(huán)境的初始化、關(guān)閉等內(nèi)容):
1. 用socket函數(shù)創(chuàng)建一個(gè)套接字sock;
2. 用bind將sock綁定到本地地址;
3. 用listen偵聽(tīng)sock套接字;
4. 用accept函數(shù)接收客戶方的連接,返回客戶方套接字clientSocket;
5. 在客戶方套接字clientSocket上使用send發(fā)送數(shù)據(jù);
6. 用closesocket函數(shù)關(guān)閉套接字sock和clientSocket;
而接收方的步驟如下:
1. 用socket函數(shù)創(chuàng)建一個(gè)套接字sock;
2. 創(chuàng)建一個(gè)指向服務(wù)方的遠(yuǎn)程地址;
3. 用connect將sock連接到服務(wù)方,使用遠(yuǎn)程地址;
4. 在套接字上使用recv接收數(shù)據(jù);
5. 用closesocket函數(shù)關(guān)閉套接字sock;
值得注意的是,在服務(wù)方有兩個(gè)地址,一個(gè)是本地地址myaddr,另一個(gè)是目標(biāo)地址addrto。本地地址myaddr用來(lái)和本地套接字sock綁定,目標(biāo)地址被sock用來(lái)accept客戶方套接字clientSocket。這樣sock和clientSocket連接成功,這兩個(gè)地址也連接上了。在服務(wù)方使用clientSocket發(fā)送數(shù)據(jù),則會(huì)從本地地址傳送到目標(biāo)地址。
在客戶方只有一個(gè)地址,即來(lái)源地址addrfrom。這個(gè)地址被用來(lái)connect遠(yuǎn)程的服務(wù)方套接字,connect成功則本地套接字與遠(yuǎn)程的來(lái)源地址連接了,因此可以使用該套接字接收遠(yuǎn)程數(shù)據(jù)。其實(shí)這時(shí)客戶方套接字已經(jīng)被隱性的綁定了本地地址,所以不需要顯式調(diào)用bind函數(shù),即使調(diào)用也不會(huì)影像結(jié)果。
具體源代碼見(jiàn)TCP_Send.cpp和TCP_Recv.cpp。注意將源代碼中的IP地址修改為符合自己需要的IP。為了減少代碼復(fù)雜性,沒(méi)有使用讀取本機(jī)IP的代碼,后續(xù)例子程序中含有此功能代碼。
8.1 bind函數(shù)
bind函數(shù)用來(lái)將一個(gè)套接字綁定到一個(gè)IP地址。一般只在服務(wù)方(即數(shù)據(jù)發(fā)送方)調(diào)用,很多函數(shù)會(huì)隱式的調(diào)用bind函數(shù)。
8.2 listen函數(shù)
從服務(wù)方監(jiān)聽(tīng)客戶方的連接。同一個(gè)套接字可以多次監(jiān)聽(tīng)。
8.3 connect和accept函數(shù)
connect是客戶方連接服務(wù)方的函數(shù),而accept是服務(wù)方同意客戶方連接的函數(shù)。這兩個(gè)配套函數(shù)分別在各自的程序中被成功調(diào)用后就可以收發(fā)數(shù)據(jù)了。
8.4 send和recv函數(shù)
send和recv是用來(lái)發(fā)送和接收數(shù)據(jù)的兩個(gè)重要函數(shù)。send只能在已經(jīng)連接的狀態(tài)下使用,而recv可以面向連接和非連接的狀態(tài)下使用。
send的定義如下:
int WSAAPI send(
SOCKET s,
const char FAR * buf,
int len,
int flags
);
其參數(shù)的含義和sendto中的前四個(gè)參數(shù)一樣。而recv的定義如下:
int WSAAPI recv(
SOCKET s,
char FAR * buf,
int len,
int flags
);
其參數(shù)含義與send中的參數(shù)含義一樣。
9 一個(gè)局域網(wǎng)聊天工具的編寫(xiě)
掌握了以上關(guān)于socket的基本用法,編寫(xiě)一個(gè)局域網(wǎng)聊天程序也就變得非常簡(jiǎn)單,如同設(shè)計(jì)一個(gè)普通的對(duì)話框程序一樣。
9.1 功能設(shè)計(jì)
功能設(shè)計(jì)如下:
1. 要能夠指定聊天對(duì)象的IP和端口(端口可以內(nèi)部確定);
2. 要能夠發(fā)送消息給指定聊天對(duì)象;
3. 要能夠接收聊天對(duì)象的消息;
4. 接收消息時(shí)要播放聲音;
5. 接收消息時(shí)如果當(dāng)前對(duì)話框不是最前端,要閃動(dòng)圖標(biāo);
6. 要有托盤(pán)圖標(biāo),可以將對(duì)話框收入托盤(pán);
9.2 功能實(shí)現(xiàn)
將內(nèi)部端口設(shè)為3456,提供一個(gè)IP地址控件來(lái)設(shè)置聊天對(duì)象的IP。該控件必須能夠讀取IP地址并賦值給內(nèi)部變量。將地址轉(zhuǎn)換為in_addr類型。
發(fā)送消息需要使用一個(gè)套接字。
接收消息也需要使用一個(gè)套接字,由于發(fā)送消息也使用了一個(gè)套接字,為了在同一個(gè)進(jìn)程中同時(shí)發(fā)送和接收消息,需要使用多線程技術(shù),將發(fā)送消息的線程設(shè)為主線程;而接收消息的線程設(shè)為子線程,子線程只負(fù)責(zé)接收UDP消息,在收到消息后顯示到主界面中。
接收消息時(shí)播放聲音這個(gè)功能在子線程中完成,使用sndPlaySound函數(shù),并提供一個(gè)wav文件即可。
閃動(dòng)圖標(biāo)這個(gè)最白癡的功能需要使用一個(gè)Timer,在主對(duì)話框類中添加一個(gè)OnTimer函數(shù),定時(shí)檢查當(dāng)前窗口狀態(tài)變量是否為假,若為假就每次設(shè)置另一個(gè)圖標(biāo)。若當(dāng)前窗口顯示到最頂端,則設(shè)置為默認(rèn)圖標(biāo)。
托盤(pán)圖標(biāo)功能用網(wǎng)上下載的CtrayIcon類輕松搞定。需要提供一個(gè)自定義消息,一個(gè)彈出菜單資源。
9.3 所需資源
頭文件:winsock2.h,Mmsystem.h
庫(kù)文件:ws2_32.lib,winmm.lib
dll:Ws2_32.dll,winmm.dll
wav文件:recv.wav
圖標(biāo):一個(gè)主程序圖標(biāo)IDI_MAIN、四個(gè)變化圖標(biāo)IDI_ICON1~4;
菜單:一個(gè)給托盤(pán)用的彈出菜單IDR_TRAYICON;
說(shuō)明,Mmsystem.h和winmm.lib、winmm.dll是為了那個(gè)播放聲音的功能。
9.4 托盤(pán)功能
托盤(pán)屬于界面功能,是變更很少的需求,因此首先完成。
1. 引入TRAYICON.H和TRAYICON.cpp兩個(gè)類;
2. 在CLANTalkDlg類中加入一個(gè)CTrayIconm_trayIcon;屬性;
3. 在CLANTalkDlg的構(gòu)造函數(shù)中初始化m_trayIcon,m_trayIcon(IDR_TRAYICON);
4. 添加一個(gè)自定義消息WM_MY_TRAY_NOTIFICATION,即在三個(gè)地方添加消息定義、消息響應(yīng)函數(shù)、消息映射;
5. 在InitDialog方法中調(diào)用托盤(pán)初始化的兩個(gè)函數(shù) m_trayIcon.SetNotificationWnd(this, WM_MY_TRAY_NOTIFICATION); m_trayIcon.SetIcon(IDI_MAIN);
6. 重寫(xiě)OnClose方法,添加彈出菜單的OnAppSuspend和OnAppOpen以及OnAppAbout方法;
7. 重寫(xiě)對(duì)話框的OnCancel方法。
9.5 動(dòng)態(tài)圖標(biāo)
動(dòng)態(tài)圖標(biāo)也是界面相關(guān)功能,首先完成。
1. 添加四個(gè)HICON變量m_hIcon1,m_hIcon2,m_hIcon3,m_hIcon4;
2. 在構(gòu)造函數(shù)中初始化這四個(gè)變量m_hIcon1 = AfxGetApp()->LoadIcon(IDI_ICON1);
3. 在InitDialog中設(shè)置調(diào)用SetTimer(1,300,NULL);設(shè)置一個(gè)timer,id為1,間隔為300微秒;
4. 添加一個(gè)布爾屬性m_bDynamicIcon,指示目前是否需要?jiǎng)討B(tài)圖標(biāo),并給出一個(gè)設(shè)置函數(shù)SetDynamicIcon;
5. 添加一個(gè)OnTimer函數(shù),讓每次timer調(diào)用時(shí)根據(jù)m_bDynamicIcon的值修改圖標(biāo);
兩個(gè)地方是用來(lái)設(shè)置動(dòng)態(tài)圖標(biāo)的,一個(gè)是當(dāng)程序收到消息并且程序不在桌面頂端時(shí),這時(shí)設(shè)置為動(dòng)態(tài)圖標(biāo),在后面的消息接收線程中處理;二是當(dāng)程序顯示到桌面頂端時(shí),設(shè)置為非動(dòng)態(tài);
重載OnActivate方法可以完成第二個(gè)時(shí)刻的要求。當(dāng)窗口狀態(tài)為WA_ACTIVE或者WA_CLICKACTIVE時(shí)SetDynamicIcon(false),否則設(shè)置SetDynamicIcon(true);
9.6 發(fā)送UDP報(bào)文功能
發(fā)送UDP報(bào)文只需在主線程中完成,需要以下步驟:
1. 初始化Socket環(huán)境,這可以在CLANTalkApp的InitInstance中完成,同理關(guān)閉Socket環(huán)境在ExitInstance中完成;我們可以使用前面的方法,也可以直接調(diào)用MFC中的AfxSocketInit函數(shù),這個(gè)函數(shù)可以確保在程序結(jié)束時(shí)自動(dòng)關(guān)閉Socket環(huán)境;
2. 創(chuàng)建socket,考慮到報(bào)錯(cuò)信息需要彈出對(duì)話框,因此不在CLANTalkDlg的構(gòu)造函數(shù)中創(chuàng)建,而是在InitDialog中構(gòu)建;發(fā)送報(bào)文的socket為m_sendSock;
3. 設(shè)置目的地址功能,需要一個(gè)地址賦值函數(shù)setAddress(char* szAddr);可以將一個(gè)字符串地址賦值給sockaddr_in形式的地址;在CLANTalkDlg中增加一個(gè)sockaddr_in m_addrto;屬性;
4. 讀取文本框中的文字,用sendto發(fā)送到對(duì)象地址;
5. 清空文本框,在記錄框中添加聊天記錄。
這時(shí)可以使用前面的UDP簡(jiǎn)單接收程序來(lái)輔助測(cè)試,因?yàn)榇藭r(shí)還未完成報(bào)文接收功能。
9.7 接收UDP報(bào)文功能
接收UDP報(bào)文要考慮幾個(gè)問(wèn)題,第一個(gè)是要?jiǎng)?chuàng)建一個(gè)子線程,在子線程中接收?qǐng)?bào)文;第二是接收?qǐng)?bào)文和發(fā)送報(bào)文要有互斥機(jī)制,以免沖突;第三是接收到報(bào)文要播放聲音;第四是接收?qǐng)?bào)文且當(dāng)前窗口不在桌面頂端要調(diào)用動(dòng)態(tài)圖標(biāo)功能。
按照以上需求設(shè)計(jì)步驟如下:
1. 創(chuàng)建接收套接字m_recvSock,
2. 利用gethostname和gethostbyname等函數(shù)獲取本機(jī)IP,并將套接字bind到該地址;
3. 添加一個(gè)CwinThread* m_pRecvThread屬性,并在InitDialog中調(diào)用AfxBeginThread創(chuàng)建子線程;
4. 編寫(xiě)子線程運(yùn)行函數(shù)void RecvProcess(LPVOID pParam),這時(shí)一個(gè)全局函數(shù),為了方便調(diào)用CLANTalkDlg類中的各種變量與方法,將CLANTalkDlg類的指針作為參數(shù)傳入子線程函數(shù),并將RecvProcess設(shè)置為CLANTalkDlg類的友元。
5. 子線程函數(shù)中完成以下功能:利用recv接收?qǐng)?bào)文;保存聊天記錄;判斷當(dāng)前窗口是否在前臺(tái),并修改動(dòng)態(tài)圖標(biāo)屬性;播放聲音。
6. 用來(lái)記錄聊天信息的ClistBox的Sort屬性要去掉,否則記錄會(huì)按內(nèi)容排序,很不好看。在RC編輯器中去掉這個(gè)屬性即可。
7. 最后要注意,在主線程退出時(shí)要保證子線程退出,但此時(shí)子線程還阻塞在recv方法上,因此主線程向自己發(fā)送一條消息消除阻塞,同時(shí)改變子線程退出標(biāo)志保證子線程可以退出。
9.8 設(shè)置聊天對(duì)象IP
點(diǎn)擊“確認(rèn)對(duì)象”按鈕時(shí),檢測(cè)IP地址控件,如果IP地址有效,則將IP地址讀入內(nèi)部屬性。這個(gè)IP地址作為發(fā)送信息的目標(biāo)地址。
這個(gè)設(shè)置只能設(shè)置發(fā)送消息的對(duì)象,所有人都可以向本機(jī)發(fā)送信息,只要他的端口是正確的。
9.9 編譯鏈接和運(yùn)行
下載壓縮包后可以打開(kāi)VC工程編譯鏈接,若直接運(yùn)行則可以點(diǎn)擊LANTalkExeFile目錄中的可執(zhí)行文件,這個(gè)目標(biāo)包含了運(yùn)行所需要的所有dll和資源文件。
當(dāng)然,如果需要可以用InstallShield做一個(gè)安裝程序,不過(guò)看來(lái)是沒(méi)有必要的。
9.10 小結(jié)
這個(gè)聊天程序很簡(jiǎn)單,但是基本上具有了一個(gè)框架,可以有最簡(jiǎn)單的聊天功能。要在此基礎(chǔ)上進(jìn)行擴(kuò)展幾乎已經(jīng)沒(méi)有什么技術(shù)問(wèn)題了。
10 使用好的Socket包可以簡(jiǎn)化開(kāi)發(fā)過(guò)程
本文中所有的技術(shù)盡量采用最原始的方式來(lái)使用。例如多線程使用的是AfxBeginThread,套接字使用了最原始的套接字,并在很多地方直接使用了SDK函數(shù),而盡量避免了MFC等代碼框架,這是為了方便他人掌握技術(shù)的最基本內(nèi)涵。
其實(shí)在具體的編程中,當(dāng)然是怎么方便怎么來(lái),Socket和多線程以及界面等功能都有大量方便可用的代碼庫(kù),復(fù)用這些代碼庫(kù)會(huì)比自己動(dòng)手寫(xiě)方便很多。但是,掌握了基本原理再使用這些庫(kù),事半功倍
引自http://www.blogjava.net/wxb_nudt/archive/2007/09/11/144371.html
DLL編寫(xiě)教程
半年不能上網(wǎng),最近網(wǎng)絡(luò)終于通了,終于可以更新博客了,寫(xiě)點(diǎn)什么呢?決定最近寫(xiě)一個(gè)編程技術(shù)系列,其內(nèi)容是一些通用的編程技術(shù)。例如DLL,COM,Socket,多線程等等。這些技術(shù)的特點(diǎn)就是使用廣泛,但是誤解很多;網(wǎng)上教程很多,但是幾乎沒(méi)有什么優(yōu)質(zhì)良品。我以近幾個(gè)月來(lái)的編程經(jīng)驗(yàn)發(fā)現(xiàn),很有必要好好的總結(jié)一下這些編程技術(shù)了。一來(lái)對(duì)自己是總結(jié)提高,二來(lái)可以方便光顧我博客的朋友。
好了,廢話少說(shuō),言歸正傳。第一篇就是《DLL編寫(xiě)教程》,為什么起這么土的名字呢?為什么不叫《輕輕松松寫(xiě)DLL》或者《DLL一日通》呢?或者更nb的《深入簡(jiǎn)出DLL》呢?呵呵,常常上網(wǎng)搜索資料的弟兄自然知道。
本文對(duì)通用的DLL技術(shù)做了一個(gè)總結(jié),并提供了源代碼打包下載,下載地址為:
http://www.blogjava.net/Files/wxb_nudt/DLL_SRC.rar
DLL的優(yōu)點(diǎn)
簡(jiǎn)單的說(shuō),dll有以下幾個(gè)優(yōu)點(diǎn):
1) 節(jié)省內(nèi)存。同一個(gè)軟件模塊,若是以源代碼的形式重用,則會(huì)被編譯到不同的可執(zhí)行程序中,同時(shí)運(yùn)行這些exe時(shí)這些模塊的二進(jìn)制碼會(huì)被重復(fù)加載到內(nèi)存中。如果使用dll,則只在內(nèi)存中加載一次,所有使用該dll的進(jìn)程會(huì)共享此塊內(nèi)存(當(dāng)然,像dll中的全局變量這種東西是會(huì)被每個(gè)進(jìn)程復(fù)制一份的)。
2) 不需編譯的軟件系統(tǒng)升級(jí),若一個(gè)軟件系統(tǒng)使用了dll,則該dll被改變(函數(shù)名不變)時(shí),系統(tǒng)升級(jí)只需要更換此dll即可,不需要重新編譯整個(gè)系統(tǒng)。事實(shí)上,很多軟件都是以這種方式升級(jí)的。例如我們經(jīng)常玩的星際、魔獸等游戲也是這樣進(jìn)行版本升級(jí)的。
3) Dll庫(kù)可以供多種編程語(yǔ)言使用,例如用c編寫(xiě)的dll可以在vb中調(diào)用。這一點(diǎn)上DLL還做得很不夠,因此在dll的基礎(chǔ)上發(fā)明了COM技術(shù),更好的解決了一系列問(wèn)題。
最簡(jiǎn)單的dll
開(kāi)始寫(xiě)dll之前,你需要一個(gè)c/c++編譯器和鏈接器,并關(guān)閉你的IDE。是的,把你的VC和C++ BUILDER之類的東東都關(guān)掉,并打開(kāi)你以往只用來(lái)記電話的記事本程序。不這樣做的話,你可能一輩子也不明白dll的真諦。我使用了VC自帶的cl編譯器和link鏈接器,它們一般都在vc的bin目錄下。(若你沒(méi)有在安裝vc的時(shí)候選擇注冊(cè)環(huán)境變量,那么就立刻將它們的路徑加入path吧)如果你還是因?yàn)殡x開(kāi)了IDE而害怕到哭泣的話,你可以關(guān)閉這個(gè)頁(yè)面并繼續(xù)去看《VC++技術(shù)內(nèi)幕》之類無(wú)聊的書(shū)了。
最簡(jiǎn)單的dll并不比c的helloworld難,只要一個(gè)DllMain函數(shù)即可,包含objbase.h頭文件(支持COM技術(shù)的一個(gè)頭文件)。若你覺(jué)得這個(gè)頭文件名字難記,那么用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是每個(gè)dll的入口函數(shù),如同c的main函數(shù)一樣。DllMain帶有三個(gè)參數(shù),hModule表示本dll的實(shí)例句柄(聽(tīng)不懂就不理它,寫(xiě)過(guò)windows程序的自然懂),dwReason表示dll當(dāng)前所處的狀態(tài),例如DLL_PROCESS_ATTACH表示dll剛剛被加載到一個(gè)進(jìn)程中,DLL_PROCESS_DETACH表示dll剛剛從一個(gè)進(jìn)程中卸載。當(dāng)然還有表示加載到線程中和從線程中卸載的狀態(tài),這里省略。最后一個(gè)參數(shù)是一個(gè)保留參數(shù)(目前和dll的一些狀態(tài)相關(guān),但是很少使用)。
從上面的程序可以看出,當(dāng)dll被加載到一個(gè)進(jìn)程中時(shí),dll打印"Dll is attached!"語(yǔ)句;當(dāng)dll從進(jìn)程中卸載時(shí),打印"Dll is detached!"語(yǔ)句。
編譯dll需要以下兩條命令:
這條命令會(huì)將cpp編譯為obj文件,若不使用/c參數(shù)則cl還會(huì)試圖繼續(xù)將obj鏈接為exe,但是這里是一個(gè)dll,沒(méi)有main函數(shù),因此會(huì)報(bào)錯(cuò)。不要緊,繼續(xù)使用鏈接命令。
這條命令會(huì)生成dll_nolib.dll。
注意,因?yàn)榫幾g命令比較簡(jiǎn)單,所以本文不討論nmake,有興趣的可以使用nmake,或者寫(xiě)個(gè)bat批處理來(lái)編譯鏈接dll。
加載DLL(顯式調(diào)用)
使用dll大體上有兩種方式,顯式調(diào)用和隱式調(diào)用。這里首先介紹顯式調(diào)用。編寫(xiě)一個(gè)客戶端程序: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;
}
注意,調(diào)用dll使用LoadLibrary函數(shù),它的參數(shù)就是dll的路徑和名稱,返回值是dll的句柄。 使用如下命令編譯鏈接客戶端:
并執(zhí)行dll_nolib_client.exe,得到如下結(jié)果:
Dll is attached!
dll loaded!
Dll is detached!
以上結(jié)果表明dll已經(jīng)被客戶端加載過(guò)。但是這樣僅僅能夠?qū)ll加載到內(nèi)存,不能找到dll中的函數(shù)。
使用dumpbin命令查看DLL中的函數(shù)
Dumpbin命令可以查看一個(gè)dll中的輸出函數(shù)符號(hào)名,鍵入如下命令:
Dumpbin –exports dll_nolib.dll
通過(guò)查看,發(fā)現(xiàn)dll_nolib.dll并沒(méi)有輸出任何函數(shù)。
如何在dll中定義輸出函數(shù)
總體來(lái)說(shuō)有兩種方法,一種是添加一個(gè)def定義文件,在此文件中定義dll中要輸出的函數(shù);第二種是在源代碼中待輸出的函數(shù)前加上__declspec(dllexport)關(guān)鍵字。
Def文件
首先寫(xiě)一個(gè)帶有輸出函數(shù)的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;
}
這個(gè)dll的def文件如下:dll_def.def
;
; dll_def module-definition file
;
LIBRARY dll_def.dll
DESCRIPTION '(c)2007-2009 Wang Xuebin'
EXPORTS
FuncInDll @1 PRIVATE
你會(huì)發(fā)現(xiàn)def的語(yǔ)法很簡(jiǎn)單,首先是LIBRARY關(guān)鍵字,指定dll的名字;然后一個(gè)可選的關(guān)鍵字DESCRIPTION,后面寫(xiě)上版權(quán)等信息(不寫(xiě)也可以);最后是EXPORTS關(guān)鍵字,后面寫(xiě)上dll中所有要輸出的函數(shù)名或變量名,然后接上@以及依次編號(hào)的數(shù)字(從1到N),最后接上修飾符。
用如下命令編譯鏈接帶有def文件的dll:
Cl /c dll_def.cpp
Link /dll dll_def.obj /def:dll_def.def
再調(diào)用dumpbin查看生成的dll_def.dll:
Dumpbin –exports dll_def.dll
得到如下結(jié)果:
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
觀察這一行
會(huì)發(fā)現(xiàn)該dll輸出了函數(shù)FuncInDll。
顯式調(diào)用DLL中的函數(shù)
寫(xiě)一個(gè)dll_def.dll的客戶端程序:dll_def_client.cpp
#include <windows.h>
#include <iostream.h>
int main(void)
{
//定義一個(gè)函數(shù)指針
typedef void (* DLLWITHLIB )(void);
//定義一個(gè)函數(shù)指針變量
DLLWITHLIB pfFuncInDll = NULL;
//加載我們的dll
HINSTANCE hinst=::LoadLibrary("dll_def.dll");
if (NULL != hinst)
{
cout<<"dll loaded!"<<endl;
}
//找到dll的FuncInDll函數(shù)
pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, "FuncInDll");
//調(diào)用dll里的函數(shù)
if (NULL != pfFuncInDll)
{
(*pfFuncInDll)();
}
return 0;
}
有兩個(gè)地方值得注意,第一是函數(shù)指針的定義和使用,不懂的隨便找本c++書(shū)看看;第二是GetProcAddress的使用,這個(gè)API是用來(lái)查找dll中的函數(shù)地址的,第一個(gè)參數(shù)是DLL的句柄,即LoadLibrary返回的句柄,第二個(gè)參數(shù)是dll中的函數(shù)名稱,即dumpbin中輸出的函數(shù)名(注意,這里的函數(shù)名稱指的是編譯后的函數(shù)名,不一定等于dll源代碼中的函數(shù)名)。
編譯鏈接這個(gè)客戶端程序,并執(zhí)行會(huì)得到:
dll loaded!
FuncInDll is called!
這表明客戶端成功調(diào)用了dll中的函數(shù)FuncInDll。
__declspec(dllexport)
為每個(gè)dll寫(xiě)def顯得很繁雜,目前def使用已經(jīng)比較少了,更多的是使用__declspec(dllexport)在源代碼中定義dll的輸出函數(shù)。
Dll寫(xiě)法同上,去掉def文件,并在每個(gè)要輸出的函數(shù)前面加上聲明__declspec(dllexport),例如:
__declspec(dllexport) void FuncInDll (void)
這里提供一個(gè)dll源程序dll_withlib.cpp,然后編譯鏈接。鏈接時(shí)不需要指定/DEF:參數(shù),直接加/DLL參數(shù)即可,
Cl /c dll_withlib.cpp
Link /dll dll_withlib.obj
然后使用dumpbin命令查看,得到:
1 0 00001000 ?FuncInDll@@YAXXZ
可知編譯后的函數(shù)名為?FuncInDll@@YAXXZ,而并不是FuncInDll,這是因?yàn)閏++編譯器基于函數(shù)重載的考慮,會(huì)更改函數(shù)名,這樣使用顯式調(diào)用的時(shí)候,也必須使用這個(gè)更改后的函數(shù)名,這顯然給客戶帶來(lái)麻煩。為了避免這種現(xiàn)象,可以使用extern “C”指令來(lái)命令c++編譯器以c編譯器的方式來(lái)命名該函數(shù)。修改后的函數(shù)聲明為:
extern "C" __declspec(dllexport) void FuncInDll (void)
dumpbin命令結(jié)果:
這樣,顯式調(diào)用時(shí)只需查找函數(shù)名為FuncInDll的函數(shù)即可成功。
extern “C”
使用extern “C”關(guān)鍵字實(shí)際上相當(dāng)于一個(gè)編譯器的開(kāi)關(guān),它可以將c++語(yǔ)言的函數(shù)編譯為c語(yǔ)言的函數(shù)名稱。即保持編譯后的函數(shù)符號(hào)名等于源代碼中的函數(shù)名稱。
隱式調(diào)用DLL
顯式調(diào)用顯得非常復(fù)雜,每次都要LoadLibrary,并且每個(gè)函數(shù)都必須使用GetProcAddress來(lái)得到函數(shù)指針,這對(duì)于大量使用dll函數(shù)的客戶是一種困擾。而隱式調(diào)用能夠像使用c函數(shù)庫(kù)一樣使用dll中的函數(shù),非常方便快捷。
下面是一個(gè)隱式調(diào)用的例子:dll包含兩個(gè)文件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://看到?jīng)]有,這就是我們?cè)黾拥念^文件
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
在進(jìn)行隱式調(diào)用的時(shí)候需要在客戶端引入頭文件,并在鏈接時(shí)指明dll對(duì)應(yīng)的lib文件(dll只要有函數(shù)輸出,則鏈接的時(shí)候會(huì)產(chǎn)生一個(gè)與dll同名的lib文件)位置和名稱。然后如同調(diào)用api函數(shù)庫(kù)中的函數(shù)一樣調(diào)用dll中的函數(shù),不需要顯式的LoadLibrary和GetProcAddress。使用最為方便。客戶端代碼如下:dll_withlibAndH_client.cpp
#include "dll_withLibAndH.h"
//注意路徑,加載 dll的另一種方法是 Project | setting | link 設(shè)置里
#pragma comment(lib,"dll_withLibAndH.lib")
int main(void)
{
FuncInDll();//只要這樣我們就可以調(diào)用dll里的函數(shù)了
return 0;
}
__declspec(dllexport)和__declspec(dllimport)配對(duì)使用
上面一種隱式調(diào)用的方法很不錯(cuò),但是在調(diào)用DLL中的對(duì)象和重載函數(shù)時(shí)會(huì)出現(xiàn)問(wèn)題。因?yàn)槭褂胑xtern “C”修飾了輸出函數(shù),因此重載函數(shù)肯定是會(huì)出問(wèn)題的,因?yàn)樗鼈兌紝⒈痪幾g為同一個(gè)輸出符號(hào)串(c語(yǔ)言是不支持重載的)。
事實(shí)上不使用extern “C”是可行的,這時(shí)函數(shù)會(huì)被編譯為c++符號(hào)串,例如(?FuncInDll@@YAXH@Z、 ?FuncInDll@@YAXXZ),當(dāng)客戶端也是c++時(shí),也能正確的隱式調(diào)用。
這時(shí)要考慮一個(gè)情況:若DLL1.CPP是源,DLL2.CPP使用了DLL1中的函數(shù),但同時(shí)DLL2也是一個(gè)DLL,也要輸出一些函數(shù)供Client.CPP使用。那么在DLL2中如何聲明所有的函數(shù),其中包含了從DLL1中引入的函數(shù),還包括自己要輸出的函數(shù)。這個(gè)時(shí)候就需要同時(shí)使用__declspec(dllexport)和__declspec(dllimport)了。前者用來(lái)修飾本dll中的輸出函數(shù),后者用來(lái)修飾從其它dll中引入的函數(shù)。
所有的源代碼包括DLL1.H,DLL1.CPP,DLL2.H,DLL2.CPP,Client.cpp。源代碼可以在下載的包中找到。你可以編譯鏈接并運(yùn)行試試。
值得關(guān)注的是DLL1和DLL2中都使用的一個(gè)編碼方法,見(jiàn)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端的函數(shù)用__declspec(dllexport)修飾,而客戶端的函數(shù)用__declspec(dllimport)修飾。當(dāng)然,記得在編譯dll時(shí)加上參數(shù)/D “DLL_DLL2_EXPORTS”,或者干脆就在dll的cpp文件第一行加上#define DLL_DLL2_EXPORTS。
VC生成的代碼也是這樣的!事實(shí)證明,我是抄襲它的,hoho!
DLL中的全局變量和對(duì)象
解決了重載函數(shù)的問(wèn)題,那么dll中的全局變量和對(duì)象都不是問(wèn)題了,只是有一點(diǎn)語(yǔ)法需要注意。如源代碼所示: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個(gè)符號(hào):
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,類的構(gòu)造函數(shù),F(xiàn)uncInDll函數(shù),全局變量g_nDll和類的成員函數(shù)show。下面是客戶端代碼:dll_object_client.cpp
#include "dll_object.h"
#include <iostream.h>
//注意路徑,加載 dll的另一種方法是 Project | setting | link 設(shè)置里
#pragma comment(lib,"dll_object.lib")
int main(void)
{
cout<<"call dll"<<endl;
cout<<"call function in dll"<<endl;
FuncInDll();//只要這樣我們就可以調(diào)用dll里的函數(shù)了
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;
}
運(yùn)行這個(gè)客戶端可以看到:
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
可知,在客戶端成功的訪問(wèn)了dll中的全局變量,并創(chuàng)建了dll中定義的C++對(duì)象,還調(diào)用了該對(duì)象的成員函數(shù)。
中間的小結(jié)
牢記一點(diǎn),說(shuō)到底,DLL是對(duì)應(yīng)C語(yǔ)言的動(dòng)態(tài)鏈接技術(shù),在輸出C函數(shù)和變量時(shí)顯得方便快捷;而在輸出C++類、函數(shù)時(shí)需要通過(guò)各種手段,而且也并沒(méi)有完美的解決方案,除非客戶端也是c++。
記住,只有COM是對(duì)應(yīng)C++語(yǔ)言的技術(shù)。
下面開(kāi)始對(duì)各各問(wèn)題一一小結(jié)。
顯式調(diào)用和隱式調(diào)用
何時(shí)使用顯式調(diào)用?何時(shí)使用隱式調(diào)用?我認(rèn)為,只有一個(gè)時(shí)候使用顯式調(diào)用是合理的,就是當(dāng)客戶端不是C/C++的時(shí)候。這時(shí)是無(wú)法隱式調(diào)用的。例如用VB調(diào)用C++寫(xiě)的dll。(VB我不會(huì),所以沒(méi)有例子)
Def和__declspec(dllexport)
其實(shí)def的功能相當(dāng)于extern “C” __declspec(dllexport),所以它也僅能處理C函數(shù),而不能處理重載函數(shù)。而__declspec(dllexport)和__declspec(dllimport)配合使用能夠適應(yīng)任何情況,因此__declspec(dllexport)是更為先進(jìn)的方法。所以,目前普遍的看法是不使用def文件,我也同意這個(gè)看法。
從其它語(yǔ)言調(diào)用DLL
從其它編程語(yǔ)言中調(diào)用DLL,有兩個(gè)最大的問(wèn)題,第一個(gè)就是函數(shù)符號(hào)的問(wèn)題,前面已經(jīng)多次提過(guò)了。這里有個(gè)兩難選擇,若使用extern “C”,則函數(shù)名稱保持不變,調(diào)用較方便,但是不支持函數(shù)重載等一系列c++功能;若不使用extern “C”,則調(diào)用前要查看編譯后的符號(hào),非常不方便。
第二個(gè)問(wèn)題就是函數(shù)調(diào)用壓棧順序的問(wèn)題,即__cdecl和__stdcall的問(wèn)題。__cdecl是常規(guī)的C/C++調(diào)用約定,這種調(diào)用約定下,函數(shù)調(diào)用后棧的清理工作是由調(diào)用者完成的。__stdcall是標(biāo)準(zhǔn)的調(diào)用約定,即這些函數(shù)將在返回到調(diào)用者之前將參數(shù)從棧中刪除。
這兩個(gè)問(wèn)題DLL都不能很好的解決,只能說(shuō)湊合著用。但是在COM中,都得到了完美的解決。所以,要在Windows平臺(tái)實(shí)現(xiàn)語(yǔ)言無(wú)關(guān)性,還是只有使用COM中間件。
總而言之,除非客戶端也使用C++,否則dll是不便于支持函數(shù)重載、類等c++特性的。DLL對(duì)c函數(shù)的支持很好,我想這也是為什么windows的函數(shù)庫(kù)使用C加dll實(shí)現(xiàn)的理由之一。
在VC中編寫(xiě)DLL
在VC中創(chuàng)建、編譯、鏈接dll是非常方便的,點(diǎn)擊fileàNewàProjectàWin32 Dynamic-Link Library,輸入dll名稱dll_InVC然后點(diǎn)擊確定。然后選擇A DLL that export some symbols,點(diǎn)擊Finish。即可得到一個(gè)完整的DLL。
仔細(xì)觀察其源代碼,是不是有很多地方似曾相識(shí)啊,哈哈!