學習筆記之SOCKET網絡編程

1.什麼是SOCKET (SOCKET也就是所謂的套接字)
簡單的說它是一個文件.它是使用標準Unix文件描述符和其它程序通訊的方式,Unix中的一切就是文件,程序在執行任何形式的 I/O 的時候,程序是在讀或者寫一個文件描述符。一個文件描述符只是一個和打開的文件相關聯的整數,這個文件可能是一個網絡連接,FIFO,管道,終端,磁盤上的文件或者什麼其它的東西.
這裏的SOCKET就是一個特定的文件描術符,(稱之爲Internet 套接字)用於描述IP地址和端口,是一個通信鏈的句柄。應用程序通常通過它向網絡發出請求或者應答網絡請求.

Internet 套接字分爲多種形式,常見的有stream sockets流格式和datagram sockets數據報格式.等.但經常用的就是這兩種.數據報套接節有時候也叫無連接套接字.
流式套接字.
流格式的套接字就是有連接的套接字.它是可以雙向通訊的數據流.如果你向流格式的套接字按順序輸出"1,2",那麼它們將按順序"1,2"到達另一邊.
比如常見的telnet程序就是使用的流式套接字.還有WWW瀏覽器使用的 HTTP協議也使用它們來下載頁面,實際上,當你通過端口80 telnet 到一個 WWW 站點,然後輸入 "GET pagename" 的時候,你也可以得到HTML頁面的內容
流式套接字使用了一種叫做"TCP"的協議來控制你的數據按順序到達並不會有錯.
我們常說的"TCP/IP"其實是兩個東西,它表示"TCP協議"和"Internet 協議"
Internet 協議只是處理Internet 路由而已"
數據報套接字.
數據報套接字也叫無連接套節字.假設你發送一個數據報,它可能會到達,它也可能次數顛倒了.如果它到達,那麼在這個包的內部是無錯誤的.對於數據報套接字,你只需要建立一個包.並構造一個有目標信息的IP頭.然後把這個包發送出去.所以說它是無連接的.
數據報套接字使用的是用戶數據協議也叫做"UDP"協議來按據數據的傳輸.當然它們也是用"Internet 協議"來處理Internet 路由的.
使用數據報套接字的程序有tftp, bootp等等.當然在這裏你也許會問"如果數據包在傳送過程中丟失,程序將如何正常工作?" 每個程序在UDP上有自己的協議.協議每發出一個數據包,收到者必鬚髮回一個包來告訴發送者"我收到了".如果一定時間內發送者沒有收到應答,它將重新發送.直到收到應答爲止.

2.關於網絡分層模型.
首先談談數據是如何在網絡中以包的方式來傳送的.假設現有一臺機器上的TFTP程序和另一臺機器上的程序通訊.我們知道TFTP程序是使用數據報套接字的.原始數據最先是被第一層協議(這裏也就是TFTP)在它的報頭包裝好,然後整個數據(包括TFTP頭)被另一層協議(在這裏就是UDP)包裝.然後再下一個(IP協議)包裝這樣一直下去直到物理硬件層(比如以太網卡)當另外一臺機器收到數據包.硬件層先剝去以太網頭,內核剝去IP和UDP頭,最後由TFTP程序剝去TFTP頭.就得到了原始數據.
這一過程對應一個網絡分層模型.這種模型在描述網絡系統上相對於其它模型有很多優點.例如:你可以寫一個套接字程序而不用關心數據的物理傳輸.因爲底層的程序會爲你處理它們.

網絡分層模型:

應用層 (Application)
表示層 (Presentation)
會話層 (Session)
傳輸層(Transport)
網絡層(Network)
數據鏈路層(Data Link)
物理層(Physical)

如果把它對應到 Unix,結果是:

應用層(Application Layer) (telnet, ftp,等等)
傳輸層(Host-to-Host Transport Layer) (TCP, UDP)
Internet層(Internet Layer) (IP和路由)
網絡訪問層 (Network Access Layer) (網絡層,數據鏈路層和物理層)

看了上面的內容就知道建立一個簡單的數據包其實是一個非常複雜的工作.但是對於用戶而言我們只需要簡單的調用send()或sendto()就可以了.內核將爲你建立傳輸層和Internet層,硬件完成網絡訪問層.

3.SOCKET編程中幾個重要的數據結構.
首先是sockaddr它的定義:
struct sockaddr
{
   unsigned short sa_family;  /* 地址家族, AF_xxx */
   char sa_data[14];          /*14字節協議地址*/
};
sa_family 能夠是各種各樣的類型,但是在這篇文章中都是 "AF_INET".
sa_data包含套接字中的目標地址和端口信息.

爲了處理sockaddr程序員創造一個並列的結構sockaddr_in後面的in表示"Internet"它的定義如下:
struct sockaddr_in
{
   short int sin_family;        /* 通信類型 */
   unsigned short int sin_port; /* 端口 */
   struct in_addr sin_addr;     /* Internet 地址 (也就是像"192.168.0.1"這樣的IP地址)*/
   unsigned char sin_zero[8];   /* 使其與sockaddr結構的長度相同*/
};

用sockaddr_in結構可以輕鬆的處理套接字地址的基本元素.注意:sin_zero的作用是爲了與結構體sockaddr保持相同的長度.在使用的時候應用memset()類型的函數來將它全部置0.正是有了它.你纔可以在使用sockaddr的地方仍然使用sockaddr_in結構代替.只需要做簡單的轉換即可.
同時注意sockaddr_in中的sin_family和sockaddr中的sa_family一致.
最後sin_port和sin_addr必須是網絡字節順序 (Network Byte Order)

4.關於字節順序.
事實上有兩種字節排列順序:A.重要的字節在前.B.不重要的字節在前面.
而前一種字節排列順序就叫網絡字節順序.
當我們說某一數據必須按照網絡字節順序.那麼你就需要調用函數(例如:htons())來將它從本機字節順序轉換成網絡字節順序.我們能夠轉換兩種類型"short"和"long"對於unsigned也實用
假如:你想將short從本機字節順序轉換成網絡字節順序.用"h"表示本機(host),接着是"to"然後用"n"表示網絡(network)最後用"s"表示short類型,這樣就可以得到這個函數htons()...該類型的函數有:
    htons()  "Host to Network Short" //將short類型由本機字節順序轉換成網絡字節順序
  htonl()  "Host to Network Long"  //將long類型由本機字節順序轉換成網絡字節順序
  ntohs()  "Network to Host Short" //將short類型由網絡字節順序轉換成本機字節順序
  ntohl()  "Network to Host Long"  //將long類型由網絡字節順序轉換成本機字節順序

記住:這裏是Unix的世界.在你將數據放到網絡上的時候,請確信它們是按網絡字節順序排列的.

爲什麼在sockaddr_in結構中sin_addr和sin_port需要轉換成網絡字節順序.而sin_family不需要呢?
答案是:sin_addr和sin_port分別封裝在包的IP層和UDP層.因此它必須按網絡字節順序.但是sin_family只是被內核用來判斷數據結構中包含什麼類型的地址.它並沒有被放到網絡上進行傳送.所以它可以是本機字節順序.

5.IP地址及其處理方法
假設你現在已經創建了一個sockaddr_in結構體myaddr 現在你要將IP地址"192.168.1.110"存貯到結構體中.你只需要調用函數inet_addr()將IP地址從點數格式轉換成無符號長整型.例:
myaddr.sin_addr.s_addr = inet_addr("192.168.1.110");
需要注意的是:函數inet_addr()返回的已經是網絡字節順序.所以你無需再調用htonl()函數來進行轉換.
還需要注意一點:函數inet_addr()在錯誤時返回值爲-1.剛好等於IP地址"255.255.255.255"這可是個廣播地址.所以在使用inet_addr()函數時一定要進行錯誤檢查.

函數inet_ntoa()則剛好相反,它可以將一個結構體in_addr輸出成點數格式的IP地址.
注意:inet_ntoa()函數將結構體in_addr作爲一個參數.而不是長整型.

6.socket()函數
功能:建立一個套接字
定義:int socket(int domain, int type, int protocol);
參數domain指定通信發生的區域,windows中只支持"AF_INET"
參數type描述要建立的套接字的類型.如SOCK_STREAM類型或SOCK_DGRAM類型
參數protocol說明該套接字使用的特定協議.如果調用者不希望特別指定使用的協議,則置爲0,使用默認的連接模式

函數socket()將會根據指定的三個參數建立一個套接字.並將相應的資源分配給它.同時返回一個整型套接字描述符.
注意:如果該函數調用失敗返回值爲-1.

7.bind()函數
功能:將套節字和本地地址關聯在一起
定義:int bind(SOCKET s,struct sockaddr *name, int namelen);
參數s是由socket()函數返回的並且未作連接的套接字描述符
參數name是賦給套接字s的本地地址結構的指針.(也就是sockaddr_in結構)
參數namelen表明了本地地址結構的長度.

函數調用成功返回0,有錯誤則返回-1.

在進行綁定本地地址時有幾個注意事項:
A.將sockaddr_in結構的sin_port置爲0時,告訴系統隨機選擇一個沒有使用的端口.
B.將sockaddr_in結構的sin_addr.saddr置爲INADDR_ANY,告訴它自動填上它所運行的機器的IP地址
C.如果需要手動指定端口.一定記住不要採用小於1024的端口,因爲所有小於1024的端口都被系統保留.而我們可以選擇的端口則從1024到65535
D.如果你使用connect()來和遠程機器通訊,你不需要關心你的本地端口,你只需要簡單的調用connect()就可以了,它會檢查套接字是否綁字端口,如果沒有,它會自己綁定一個沒有使用的端口.

8.connect()函數
定義:int connect(SOCKET s,struct sockaddr *name, int namelen);
參數s是欲建立連接的本地套接字描述符.
參數name是將要連接的目標地址結構sockaddr的指針.
參數namelen指明目標地址結構的長度

函數在錯誤時返回-1.
注意:在使用connect()函數來和遠程機器建立連接時可以不需要調用bind()函數,因爲我在乎本地端口號.我只關心我要連接到哪裏(也就是目標),並且連接後目標會自動獲取我們的IP地址,以及我們所使用的端口等信息.

9.listen()和accept()函數.
假如你不希望與遠程的一個地址相連,那麼你就需要等待接入請求.並且用各種方法處理它們.這個過程就是聽及listen()然後接受accept()

listen()用於監聽連接.它地調用需在accept()之前
定義:int listen(SOCKET s, int backlog);
參數s標識一個本地已建立,尚未連接的套接字描述符.
參數backlog表示請求連接隊列的最大長度,大多數系統允許最大數目是20.
成功返回0,錯誤則返回-1.

accept()函數
系統調用accept()的情況就像有一臺遠程的機器通過你在監聽listen()的端口連接connect()到你的機器.這個連接將被加入到等待接受的隊列中.然後你調用accept()告訴它你有空閒的連接.這時系統將返回一個新的套接字描術符.這樣你就有兩個套接字了.原來的一個還在監聽那個端口是否有新的連接到來.新的這個套接字則準備發送send()或者接收recv()數據了.
定義:SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
參數s爲本地套接字描述符,也就是正在用於監聽listen()的那個套接字描術符.
參數addr是一個struct sockaddr結構指針.它用來存貯客戶的地址.也就是要求連接的機器的地址信息.
參數addrlen保存客戶方套接字地址的長度(注意它是一個int型指針)
函數調用成功將會返回一個套接字(表示接收到的套接字).如果失敗則返回-1.

10.send()和recv()函數
不管是服務端程序或是客戶端程序都將使用這兩個函數從TCP連接的另一端發送或接收數據.

send()函數
定義:int send(int s, void *msg, int len, int flags);
參數s是你相發送數據的套接字描述符.這裏可能是socket()返回的,也可能是accept()返回的.
參數msg是你想要發送的數據的指針.
參數len是將要發送的數據的長度.
參數flags通常設爲0.

如果將要發送的數據的長度len大於套接字s的發送緩衝區的長度.該函數將返回-1.
如果len小於或等於s的發送緩衝區長度,那麼send()先檢果協議是否正在發送s的發送緩衝區中的數據.如果是就等待協議把數據發送完.如果協議還沒有開始發送s的發送緩衝區中的數據或者s的發送緩衝區中沒有數據,那麼send()就比較發送緩衝區和剩餘空間和len,如果len大於s的發送緩衝區剩餘空間send()就會一直等待協議把s的發送緩衝區中的數據發送完.如果len小於發送緩衝區的剩餘空間send()就把msg所指的數據拷貝到s的發送緩衝區剩餘空間裏.

注意:並不是send()把數據傳送到連接的另一端.而是具體的協議來完成傳送的.send()只是把數據拷貝到套接字的發送緩衝區剩餘空間裏而以.
如果send()拷貝數據成功.就返回實際拷貝數據的字節數.
另外send()把數據拷貝到套接字的發送緩衝區中就返回了.此時.這些數據並不一定馬上被傳送到連接的另一端.

注意:在Unix系統下.如果send()在等待協議傳送數據時網絡斷開的話,調用send的進程會接收到一個SIGPIPE信號,進程對該信號的默認處理是進程終止.

recv()函數
定義:int recv(int s, void *buf, int len, int flags);
參數s爲接收端的套接字描述符.
參數buf指明一個緩衝區.該緩衝區用來存放recv()接收到的數據.
參數len指明緩衝區的長度.
參數flags通常設爲0.

成功將返回實際寫入緩衝區的數據的長度.失敗返回-1.
同樣的.recv()函數只是負責把數據從s的接收緩衝區中拷貝到buf緩衝區中.真正的接收數據的工作是由協議來完成的.所以協議接收到的數據有可能大於buf緩衝區的長度.在這種情況下要調用多次recv()函數才能把接收緩衝區中的數據拷貝完.

另外:除了send()函數外其它的socket函數比如recv()都要先等待套接字的發送緩衝中的數據被協議傳送完畢才能執行.


今天就到此吧!!!

自:http://blog.sina.com.cn/s/blog_56ea069101000bx5.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章