很抱歉這一篇文章中仍然要討論 C 語言 socket 函數相關的知識,其實準確來講這些是任意一種開發語言直接調用系統 api 時都要掌握的知識點.這些知識是必定要再詳細解釋一次的.
1.A版本函數和W版本函數的問題
好比其中說到的 LoadLibrary 函數的 A 版本和 W 版本的問題,這實際上是說 windows 函數中有兩個 LoadLibrary 函數,分別是 LoadLibraryA 和 LoadLibraryW,這是因有最初的 C 字符串對應的都是 ANSI 字符串,後來有了 UNICODE 字符串, A 版函數的字符串參數的對應的就是 ANSI 字符串, W 版本其實對應的就是 UNICODE 字符串. 實際上 dll 的導出函數中並無 LoadLibrary 這個函數,而是否是用 LoadLibraryA 就得用 LoadLibraryW,之因此大家能在開發環境中用 LoadLibrary 那是被宏定義從新定義了的結果.對於絕大多數涉及到字符串操做的 windows api 函數,都有 A 版和 W 版,至於如今如日中天的 utf8 字符串,那是沒有的,由於 utf 更先進也更晚"出生",大多數狀況下 UNICODE 已經足夠用了,因此是要將 utf8 轉換成 UNICODE 後再調用 W 版本的 api 函數. 也許之後會有 utf8 版本的也不必定...
這在 vc 環境下編譯時是有可能引發錯誤的,因此你們要注意一下.linux
2.很是重要的字節對齊問題
由於咱們上一編示例中用的是 C 語言來說解原始的系統 socket api 函數來訪問網絡,這裏面其實隱藏了一個很是重要的網絡編程的細節.那就是字節對齊! 回顧一下上一篇文章中的代碼,大量的使用了 socket 相關的結構體,看上去它們和普通的結構體並無什麼區別,可是假如咱們是用 delphi 來作示例的話,會 delphi 語法的同窗必定會發現它們的結構體聲明中多了一個 packed 聲明.例如:ios
THostEnt = packed record h_name: PChar; h_aliases: ^PChar; h_addrtype: Smallint; h_length: Smallint; case integer of 0: (h_addr_list: ^PChar); 1: (h_addr: ^PInAddr); end;
我又要很慚愧地說,我又是工做近一年後才知道它是什麼意思,以及是有多重要的.簡單地來講這個標誌就是要告訴編譯器,這個結構體不要進行字節對齊,聲明瞭多個字節的數據就給多少個字節 -- 這是什麼意思? 初學者必定會莫名其妙,咱們來看下 windows 中的一個真實結構體聲明:
程序員
struct tagRGBTRIPLE { BYTE rgbtBlue; BYTE rgbtGreen; BYTE rgbtRed; }
你以爲編譯器會給這個結構體分配幾個字節的內存? 你會說3個呀? 沒錯,這很容易用 sizeof 函數來證實. 好吧,你本身聲明一個編程
struct tagRGBTRIPLE_my { BYTE rgbtBlue; BYTE rgbtGreen; BYTE rgbtRed; }
你以爲還會是3個字節嗎? 我根本不用試,就能夠告訴你多半是 4 個字節,你打開 vc 試了一下,果真是 4 個,你會問爲何我本身寫的結構體是是 4 個字節? 更細心的讀者還會再問一個問題:爲何說多半是 4 個難道做者本身也不能肯定?個人答案是,我確實不能肯定是佔用多少個字節,由於這受不少因素的影響,通常來講編譯器會把結構體進行 4 字節或者 8 字節進行對齊,因此通常來講自定義的那個結構體會是 4 字節. 而系統自帶的那個 tagRGBTRIPLE 會是三字節,那是由於 vc 在它代碼上下加了字節對齊要用多少個的編譯指令,它的完整聲明應該是:
windows
#pragma pack(push,1) typedef struct tagRGBTRIPLE { BYTE rgbtBlue; BYTE rgbtGreen; BYTE rgbtRed; } RGBTRIPLE,*LPRGBTRIPLE; #pragma pack(pop)
你們不信的話能夠直接打開頭文件看看.
說到用 pragma pack 指令來控制結構體,我以爲這真的是一個可怕的語法,若是 pragma pack 指令沒有成對出現的話那真是一場大災難,因此 delphi 纔會直接修飾到結構體中,雖然 C 語言讀者會不高興不過這真的是一種更先進的作法.而 gcc 確實要考慮得更周全一些: gcc 能夠在結構體上直接用 __attribute__ 指令來完成這個任務.
至於結構體爲何要進行字節對齊(以及其餘更詳細的信息),你們自行百度吧,咱們這裏主要是說它對網絡編程的影響.
曾經我在一家公司開會討論問題時,一位主力的手機開發人員談起這個結構體字節數對不上的問題時,另外一個同事很不屑的直接說,這是結構體字節對齊問題,你連這個都不知道還寫什麼網絡程序... 確實若是這個問題都不搞清楚的話你們仍是先不要寫網絡程序的好. 這麼嚴重! 你會說. 是的,就有這麼嚴重,由於咱們前面的示例都是發送收取的字符串,而字符串這裏都是用的單字節的字節流,因此你們沒感受到字節對齊的影響.可是當你要發送一個結構體給另一個程序的時候問題就會出現了.服務器通常都是寫好了的,假設它要求一個三字節的結構體,好了,你定義了一個上面那種 tagRGBTRIPLE_my 的結構體填充好數據後發送出去,第一個結構體也許會正常收到了,第二個結構體呢 ... 若是您仔細看了前面的內容必定會知道數據是對不上的! 由於你這時候實際上發送了4個字節出去.
由於 smtp 等協議的特色,這個問題一直都不會影響咱們後面的示例,因此只能請你們自行牢記字節對齊的影響了.不要在未來讓別人也說你一個開發網絡程序的人連字節對齊都不知道...api
(圖1:vc6中的結構體對齊設置)數組
3. inet_addr 仍是 gethostbyname
這個問題就要有點 socket 經驗的同窗纔看得懂了,不過也沒有關係,你們先記着,之後再回頭來看.
在上次的代碼中實際上有兩個鏈接網絡的函數: ConnectHost 和 ConnectIP, ConnectHost 是實際用到的函數,而 ConnectIP 則是大多數教程中經常使用的 inet_addr 函數的簡單封裝而已. inet_addr 能夠將一個字符串轉換成 connect 函數須要的地址結構,在我剛學網絡編程的時候很長一段時間內分不清 inet_addr 和 gethostbyname 有什麼區別 -- 反正測試能鏈接成功. 這主要是之前的環境上網不易,不少時候都是局域網內鏈接一個 ip 就寫測試程序了,二者的做用又有點近似 -- 都是將字符串變成 connect 的參數嘛. inet_addr 放到互聯網上其實也能夠,可是假如你們真的拿來鏈接示例中的 newbt.net 的話就會發現鏈接是失敗的. 固然了大多數同窗多是知道的,那是由於 inet_addr 只能轉換 ip 地址,而 gethostbyname 能夠轉換域名. 對於我這樣喜歡刨根問底的人來講 gethostbyname 是 socket 函數族中最特別的函數:你們看看前面的例子就能夠知道 socket 函數都是完成很是底層的工做的,象 inet_addr 不過是將地址的字符串表示變成二進制而已,而 gethostbyname 能直接轉換域名實在是神奇,由於這裏面實際上還包括了訪問 dns 的過程,而訪問 dns 其實是要用另一個 udp 的 socket 過程來完成的,若是明白了整個 socket 過程的話必定會是很是奇怪的,爲何 socket 函數開恩把這麼複雜的過程變成了直接一個函數來完成? 這估計是由於域名的轉換沒有直接訪問 dns 就好了那麼簡單,還要根據不一樣的狀況有可能會訪問操做系統的 host 文件,還要考慮用什麼樣的 dns 服務器地址 ... 一系列的問題若是都靠程序員去實現的話顯然太過複雜,因而 socket 函數的最初設計者只好把域名轉換成 ip 二進制的過程放到一個函數中算了.
另外聽說 linux 如今有新的函數來代替 gethostbyname 了,也許在不久的未來你們就可能要用到那個新函數.瀏覽器
再另外,其實 gethostbyname 取到的是一串 ip 地址的數組,教程中通常都是直接取第一個了事,緣由也要之後再說了.
4.真的能鏈接了嗎 -- 引出爲何手機不能直接使用原始 socket服務器
這裏還要和你們說一個問題,獲得地址後用 connet 就能鏈接上網絡了.可是這裏有一個前提,那就是你的電腦得先鏈接上網絡,這個 connect 函數的做用其實不是 "鏈接上網絡" 而是 "讓個人程序鏈接上網絡",若是電腦自己就沒能鏈接上那是不行的. 網友必定氣壞了,你這不廢話嗎? 固然不是廢話,早期的電腦就算電腦鏈接上了全部的網絡設備和線路後還要有一個過程才能上網:那就是撥號.固然如今的撥號過程都是路由來完成了,在 pc 上固然不會有程序須要去本身完成撥號功能的,但科技發展來到手機時代後,撥號就是必需要完成的了.由於手機要控制流量是不能可一直在線的,程序必須在須要時先進行撥號操做,而使用原始的 socket 是不會產生撥號動做的! 早期的 windows ce 時代我寫過不少 ppc 手機程序,有個公司就是寫不了括號過程,每次要鏈接網絡時都調用一下瀏覽器訪問一個網址,讓瀏覽器先幫它撥號成功 ... 因此如今的 ios 是封裝了 socket 函數的,全部的 ios 編程書都會介紹你使用那個封裝後的 socket 而告誡你說不要使用原始的 socket ,道理就是這樣. 安卓固然也是相似. 另外手機不推薦使用原始 socket 的緣由還有線程和ipv6以及 socket 阻塞等等 ...
這些都來不及說了,咱們之後再詳細展開吧.下篇文章仍是讓咱們先回到 smtp/pop3 等通信協議上來吧.網絡
--------------------------------------------------
版權聲明:
本系列文章已受權百家號 "clq的程序員學前班" .
有些網友轉發了本系列的一些文章,很感謝你們,這也是對文章內容的承認,不過仍是但願你們在條件容許的狀況下儘量的放上文章在博客園的原始首發網址 ... 再次感謝.