1. 程式人生 > >Linux 下Socket程式設計基礎

Linux 下Socket程式設計基礎

作者: 東北大學秦皇島分校軟體中心技術研發部 敬茂華

1、 引言
Linux的興起可以說是Internet創造的一個奇蹟。Linux作為一個完全開放其原始碼的免費的自由軟體,相容了各種UNIX標準(如POSIX、UNIX System V 和 BSD UNIX 等)的多使用者、多工的具有複雜核心的作業系統。在中國,隨著Internet的普及,一批主要以高等院校的學生和ISP的技術人員組成的Linux愛好者隊伍已經蓬勃成長起來。越來越多的程式設計愛好者也逐漸酷愛上這個優秀的自由軟體。本文介紹了Linux下Socket的基本概念和函式呼叫。

2、 什麼是Socket
Socket(套接字)是通過標準的UNIX檔案描述符和其它程式通訊的一個方法。每一個套接字都用一個半相關描述:{協議,本地地址、本地埠}來表示;一個完整的套接字則用一個相關描述:{協議,本地地址、本地埠、遠端地址、遠端埠},每一個套接字都有一個本地的由作業系統分配的唯一的套接字號。

3、 Socket的三種類型
(1) 流式Socket(SOCK_STREAM)
流式套接字提供可靠的、面向連線的通訊流;它使用TCP協議,從而保證了資料傳輸的正確性和順序的。
(2) 資料報Socket(SOCK_DGRAM)
資料報套接字定義了一種無連線的服務,資料通過相互獨立的報文進行傳輸,是無序的,並且不保證可靠、無差錯。它使用資料報協議UDP
(3) 原始Socket
原始套接字允許對底層協議如IP或ICMP直接訪問,它功能強大但使用較為不便,主要用於一些協議的開發。

4、 利用套接字傳送資料
1、 對於流式套接字用系統呼叫send()來發送資料。
2、 對於資料報套接字,則需要自己先加一個資訊頭,然後呼叫sendto()函式把資料傳送出去。

5、 Linux中Socket的資料結構
(1) struct sockaddr { //用於儲存套接字地址
unsigned short sa_family;//地址型別
char sa_data[14]; //14位元組的協議地址
};
(2) struct sockaddr_in{ //in 代表internet
short int sin_family; //internet協議族
unsigned short int sin_port;//埠號,必須是網路位元組順序
struct in_addr sin_addr;//internet地址,必須是網路位元組順序
unsigned char sin_zero;//添0(和struct sockaddr一樣大小
};
(3) struct in_addr{
unsigned long s_addr;
};

6、 網路位元組順序及其轉換函式
(1) 網路位元組順序
每一臺機器內部對變數的位元組儲存順序不同,而網路傳輸的資料是一定要統一順序的。所以對內部位元組表示順序與網路位元組順序不同的機器,一定要對資料進行轉換,從程式的可移植性要求來講,就算本機的內部位元組表示順序與網路位元組順序相同也應該在傳輸資料以前先呼叫資料轉換函式,以便程式移植到其它機器上後能正確執行。真正轉換還是不轉換是由系統函式自己來決定的。
(2) 有關的轉換函式
* unsigned short int htons(unsigned short int hostshort):
主機位元組順序轉換成網路位元組順序,對無符號短型進行操作4bytes
* unsigned long int htonl(unsigned long int hostlong):
主機位元組順序轉換成網路位元組順序,對無符號長型進行操作8bytes
* unsigned short int ntohs(unsigned short int netshort):
網路位元組順序轉換成主機位元組順序,對無符號短型進行操作4bytes
* unsigned long int ntohl(unsigned long int netlong):
網路位元組順序轉換成主機位元組順序,對無符號長型進行操作8bytes
注:以上函式原型定義在netinet/in.h裡

7、 IP地址轉換
有三個函式將數字點形式表示的字串IP地址與32位網路位元組順序的二進位制形式的IP地址進行轉換
(1) unsigned long int inet_addr(const char * cp):該函式把一個用數字和點表示的IP地址的字串轉換成一個無符號長整型,如:struct sockaddr_in ina
ina.sin_addr.s_addr=inet_addr("202.206.17.101")
該函式成功時:返回轉換結果;失敗時返回常量INADDR_NONE,該常量=-1,二進位制的無符號整數-1相當於255.255.255.255,這是一個廣播地址,所以在程式中呼叫iner_addr()時,一定要人為地對呼叫失敗進行處理。由於該函式不能處理廣播地址,所以在程式中應該使用函式inet_aton()。
(2)int inet_aton(const char * cp,struct in_addr * inp):此函式將字串形式的IP地址轉換成二進位制形式的IP地址;成功時返回1,否則返回0,轉換後的IP地址儲存在引數inp中。
(3) char * inet_ntoa(struct in-addr in):將32位二進位制形式的IP地址轉換為數字點形式的IP地址,結果在函式返回值中返回,返回的是一個指向字串的指標。

8、 位元組處理函式
Socket地址是多位元組資料,不是以空字元結尾的,這和C語言中的字串是不同的。Linux提供了兩組函式來處理多位元組資料,一組以b(byte)開頭,是和BSD系統相容的函式,另一組以mem(記憶體)開頭,是ANSI C提供的函式。
以b開頭的函式有:
(1) void bzero(void * s,int n):將引數s指定的記憶體的前n個位元組設定為0,通常它用來將套接字地址清0。
(2) void bcopy(const void * src,void * dest,int n):從引數src指定的記憶體區域拷貝指定數目的位元組內容到引數dest指定的記憶體區域。
(3) int bcmp(const void * s1,const void * s2,int n):比較引數s1指定的記憶體區域和引數s2指定的記憶體區域的前n個位元組內容,如果相同則返回0,否則返回非0。
注:以上函式的原型定義在strings.h中。
以mem開頭的函式有:
(1) void * memset(void * s,int c,size_t n):將引數s指定的記憶體區域的前n個位元組設定為引數c的內容。
(2) void * memcpy(void * dest,const void * src,size_t n):功能同bcopy(),區別:函式bcopy()能處理引數src和引數dest所指定的區域有重疊的情況,memcpy()則不能。
(4) int memcmp(const void * s1,const void * s2,size_t n):比較引數s1和引數s2指定區域的前n個位元組內容,如果相同則返回0,否則返回非0。
注:以上函式的原型定義在string.h中。

9、 基本套接字函式
(1) socket()
#include< sys/types.h> 
#include< sys/socket.h> 
int socket(int domain,int type,int protocol)
引數domain指定要建立的套接字的協議族,可以是如下值:
AF_UNIX //UNIX域協議族,本機的程序間通訊時使用
AF_INET //Internet協議族(TCP/IP)
AF_ISO //ISO協議族
引數type指定套接字型別,可以是如下值:
SOCK_STREAM //流套接字,面向連線的和可靠的通訊型別
SOCK_DGRAM //資料報套接字,非面向連線的和不可靠的通訊型別
SOCK_RAW //原始套接字,只對Internet協議有效,可以用來直接訪問IP協議
引數protocol通常設定成0,表示使用預設協議,如Internet協議族的流套接字使用TCP協議,而資料報套接字使用UDP協議。當套接字是原始套接字型別時,需要指定引數protocol,因為原始套接字對多種協議有效,如ICMP和IGMP等。
Linux系統中建立一個套接字的操作主要是:在核心中建立一個套接字資料結構,然後返回一個套接字描述符標識這個套接字資料結構。這個套接字資料結構包含連線的各種資訊,如對方地址、TCP狀態以及傳送和接收緩衝區等等,TCP協議根據這個套接字資料結構的內容來控制這條連線。
(2) 函式connect()
#include< sys/types.h> 
#include< sys/socket.h> 
int connect(int sockfd,struct sockaddr * servaddr,int addrlen)
引數sockfd是函式socket返回的套接字描述符;引數servaddr指定遠端伺服器的套接字地址,包括伺服器的IP地址和埠號;引數addrlen指定這個套接字地址的長度。成功時返回0,否則返回-1,並設定全域性變數為以下任何一種錯誤型別:ETIMEOUT、ECONNREFUSED、EHOSTUNREACH或ENETUNREACH。
在呼叫函式connect之前,客戶機需要指定伺服器程序的套接字地址。客戶機一般不需要指定自己的套接字地址(IP地址和埠號),系統會自動從1024至5000的埠號範圍內為它選擇一個未用的埠號,然後以這個埠號和本機的IP地址填充這個套接字地址。
客戶機呼叫函式connect來主動建立連線。這個函式將啟動TCP協議的3次握手過程。在建立連線之後或發生錯誤時函式返回。連線過程可能出現的錯誤情況有:
(1) 如果客戶機TCP協議沒有接收到對它的SYN資料段的確認,函式以錯誤返回,錯誤型別為ETIMEOUT。通常TCP協議在傳送SYN資料段失敗之後,會多次傳送SYN資料段,在所有的傳送都高中失敗之後,函式以錯誤返回。
注:SYN(synchronize)位:請求連線。TCP用這種資料段向對方TCP協議請求建立連線。在這個資料段中,TCP協議將它選擇的初始序列號通知對方,並且與對方協議協商最大資料段大小。SYN資料段的序列號為初始序列號,這個SYN資料段能夠被確認。當協議接收到對這個資料段的確認之後,建立TCP連線。
(2) 如果遠端TCP協議返回一個RST資料段,函式立即以錯誤返回,錯誤型別為ECONNREFUSED。當遠端機器在SYN資料段指定的目的埠號處沒有服務程序在等待連線時,遠端機器的TCP協議將傳送一個RST資料段,向客戶機報告這個錯誤。客戶機的TCP協議在接收到RST資料段後不再繼續傳送SYN資料段,函式立即以錯誤返回。
注:RST(reset)位:表示請求重置連線。當TCP協議接收到一個不能處理的資料段時,向對方TCP協議傳送這種資料段,表示這個資料段所標識的連接出現了某種錯誤,請求TCP協議將這個連線清除。有3種情況可能導致TCP協議傳送RST資料段:(1)SYN資料段指定的目的埠處沒有接收程序在等待;(2)TCP協議想放棄一個已經存在的連線;(3)TCP接收到一個數據段,但是這個資料段所標識的連線不存在。接收到RST資料段的TCP協議立即將這條連線非正常地斷開,並嚮應用程式報告錯誤。
(3) 如果客戶機的SYN資料段導致某個路由器產生“目的地不可到達”型別的ICMP訊息,函式以錯誤返回,錯誤型別為EHOSTUNREACH或ENETUNREACH。通常TCP協議在接收到這個ICMP訊息之後,記錄這個訊息,然後繼續幾次傳送SYN資料段,在所有的傳送都告失敗之後,TCP協議檢查這個ICMP訊息,函式以錯誤返回。
注:ICMP:Internet 訊息控制協議。Internet的執行主要是由Internet的路由器來控制,路由器完成IP資料包的傳送和接收,如果傳送資料包時發生錯誤,路由器使用ICMP協議來報告這些錯誤。ICMP資料包是封裝在IP資料包的資料部分中進行傳輸的,其格式如下:
型別
碼 
校驗和
資料
0 8 16 24 31
型別:指出ICMP資料包的型別。
程式碼:提供ICMP資料包的進一步資訊。
校驗和:提供了對整個ICMP資料包內容的校驗和。
ICMP資料包主要有以下型別:
(1) 目的地不可到達:A、目的主機未執行;B、目的地址不存在;C、路由表中沒有目的地址對應的條目,因而路由器無法找到去往目的主機的路由。
(2) 超時:路由器將接收到的IP資料包的生存時間(TTL)域減1,如果這個域的值變為0,路由器丟棄這個IP資料包,並且傳送這種ICMP訊息。
(3) 引數出錯:當IP資料包中有無效域時傳送。
(4) 重定向:將一條新的路徑通知主機。
(5) ECHO請求、ECHO回答:這兩條訊息用語測試目的主機是否可以到達。請求者向目的主機發送ECHO請求ICMP資料包,目的主機在接收到這個ICMP資料包之後,返回ECHO回答ICMP資料包。
(6) 時戳請求、時戳回答:ICMP協議使用這兩種訊息從其他機器處獲得其時鐘的當前時間。

呼叫函式connect的過程中,當客戶機TCP協議傳送了SYN資料段的確認之後,TCP狀態由CLOSED狀態轉為SYN_SENT狀態,在接收到對SYN資料段的確認之後,TCP狀態轉換成ESTABLISHED狀態,函式成功返回。如果呼叫函式connect失敗,應該用close關閉這個套接字描述符,不能再次使用這個套接字描述符來呼叫函式connect。

注:TCP協議狀態轉換圖:

被動OPEN CLOSE 主動OPEN
(建立TCB) (刪除TCB) (建立TCB,
傳送SYN)
接收SYN SEND
(傳送SYN,ACK) (傳送SYN)

接收SYN的ACK(無動作) 
接收SYN的ACK 接收SYN,ACK 
(無動作) (傳送ACK)
CLOSE
(傳送FIN) CLOSE 接收FIN
(傳送FIN) (傳送FIN)

接收FIN
接收FIN的ACK(無動作) (傳送ACK) CLOSE(傳送FIN)


接收FIN 接收FIN的ACK 接收FIN的ACK
(傳送ACK) (無動作) (無動作)

2MSL超時(刪除TCB) 
(3) 函式bind()
函式bind將本地地址與套接字繫結在一起,其定義如下:
#include< sys/types.h> 
#include< sys/socket.h> 
int bind(int sockfd,struct sockaddr * myaddr,int addrlen);
引數sockfd是函式sockt返回的套接字描述符;引數myaddr是本地地址;引數addrlen是套接字地址結構的長度。執行成功時返回0,否則,返回-1,並設定全域性變數errno為錯誤型別EADDRINUSER。
伺服器和客戶機都可以呼叫函式bind來繫結套接字地址,但一般是伺服器呼叫函式bind來繫結自己的公認埠號。繫結操作一般有如下幾種組合方式:
表1
程式型別
IP地址
埠號
說明
伺服器
INADDR_ANY
非零值
指定伺服器的公認埠號
伺服器
本地IP地址
非零值
指定伺服器的IP地址和公認埠號
客戶機
INADDR_ANY
非零值
指定客戶機的連線埠號
客戶機
本地IP地址
非零值
指定客戶機的IP地址連線埠號
客戶機
本地IP地址

指定客戶機的IP地址
分別說明如下:
(1) 伺服器指定套接字地址的公認埠號,不指定IP地址:即伺服器呼叫bind時,設定套接字的IP地址為特殊的INADDE-ANY,表示它願意接收來自任何網路裝置介面的客戶機連線。這是伺服器最常用的繫結方式。
(2) 伺服器指定套接字地址的公認埠號和IP地址:伺服器呼叫bind時,如果設定套接字的IP地址為某個本地IP地址,這表示這臺機器只接收來自對應於這個IP地址的特定網路裝置介面的客戶機連線。當伺服器有多塊網絡卡時,可以用這種方式來限制伺服器的接收範圍。
(3) 客戶機指定套接字地址的連線埠號:一般情況下,客戶機呼叫connect函式時不用指定自己的套接字地址的埠號。系統會自動為它選擇一個未用的埠號,並且用本地的IP地址來填充套接字地址中的相應項。但有時客戶機需要使用一個特定的埠號(比如保留埠號),而系統不會未客戶機自動分配一個保留埠號,所以需要呼叫函式bind來和一個未用的保留埠號繫結。
(4) 指定客戶機的IP地址和連線埠號:表示客戶機使用指定的網路裝置介面和埠號進行通訊。
(5) 指定客戶機的IP地址:表示客戶機使用指定的網路裝置介面和埠號進行通訊,系統自動為客戶機選一個未用的埠號。一般只有在主機有多個網路裝置介面時使用。
我們一般不在客戶機上使用固定的客戶機埠號,除非是必須使用的情況。在客戶機上使用固定的埠號有以下不利:
(1) 伺服器執行主動關閉操作:伺服器最後進入TIME_WAIT狀態。當客戶機再次與這個伺服器進行連線時,仍使用相同的客戶機埠號,於是這個連線與前次連線的套接字對完全一樣,但是一呢、為前次連線處於TIME_WAIT狀態,並未消失,所以這次連線請求被拒絕,函connect以錯誤返回,錯誤型別為ECONNREFUSED
(2) 客戶機執行主動關閉操作:客戶機最後進入TIME_WAIT狀態。當馬上再次執行這個客戶機程式時,客戶機將繼續與這個固定客戶機埠號繫結,但因為前次連線處於TIME_WAIT狀態,並未消失,系統會發現這個埠號仍被佔用,所以這次繫結操作失敗,函式bind以錯誤返回,錯誤型別為EADDRINUSE。
(4) 函式listen()
函式listen將一個套接字轉換為徵聽套接字,定義如下;
#include< sys/socket,h> 
int listen(int sockfd,int backlog)
引數sockfd指定要轉換的套接字描述符;引數backlog設定請求佇列的最大長度;執行成功時返回0, 否則返回-1。函式listen功能有兩個:
(1) 將一個尚未連線的主動套接字(函式socket建立的可以用來進行主動連線但不能接受連線請求的套接字)轉換成一個被動連線套接字。執行listen之後,伺服器的TCP狀態由CLOSED轉為LISTEN狀態。
(2) TCP協議將到達的連線請求佇列,函式listen的第二個引數指定這個佇列的最大長度。
注:引數backlog的作用:
TCP協議為每一個徵聽套接字維護兩個佇列:
(1) 未完成連線佇列:每個尚未完成3次握手操作的TCP連線在這個佇列中佔有一項。TCP希望儀在接收到一個客戶機SYN資料段之後,在這個佇列中建立一個新條目,然後傳送對客戶機SYN資料段的確認和自己的SYN資料段(ACK+SYN資料段),等待客戶機對自己的SYN資料段的確認。此時,套接字處於SYN_RCVD狀態。這個條目將儲存在這個佇列中,直到客戶機返回對SYN資料段的確認或者連線超時。
(2) 完成連線佇列:每個已經完成3次握手操作,但尚未被應用程式接收(呼叫函式accept)的TCP連線在這個佇列中佔有一項。當一個在未完成連線佇列中的連線接收到對SYN資料段的確認之後,完成3次握手操作,TCP協議將它從未完成連線佇列移到完成連線佇列中。此時,套接字處於ESTABLISHED狀態。這個條目將儲存在這個佇列中,直到應用程式呼叫函式accept來接收它。
引數backlog指定某個徵聽套接字的完成連線佇列的最大長度,表示這個套接字能夠接收的最大數目的未接收連線。如果當一個客戶機的SYN資料段到達時,徵聽套接字的完成佇列已經滿了,那麼TCP協議將忽略這個SYN資料段。對於不能接收的SYN資料段,TCP協議不傳送RST資料段,
(5) 函式accept()
函式accept從徵聽套接字的完成佇列中接收一個已經建立起來的TCP連線。如果完成連線佇列為空,那麼這個程序睡眠。
#include< sys/socket.h> 
int accept(int sockfd,struct sockaddr * addr,int * addrlen)
引數sockfd指定徵聽套接字描述符;引數addr為指向一個Internet套接字地址結構的指標;引數addrlen為指向一個整型變數的指標。執行成功時,返回3個結果:函式返回值為一個新的套接字描述符,標識這個接收的連線;引數addr指向的結構變數中儲存客戶機地址;引數addrlen指向的整型變數中儲存客戶機地址的長度。失敗時返回-1。
徵聽套接字專為接收客戶機連線請求,完成3次握手操作而用的,所以TCP協議不能使用徵聽套接字描述符來標識這個連線,於是TCP協議建立一個新的套接字來標識這個要接收的連線,並將它的描述符發揮給應用程式。現在有兩個套接字,一個是呼叫函式accept時使用的徵聽套接字,另一個是函式accept返回的連線套接字(connected socket)。一個伺服器通常只需建立一個徵聽套接字,在伺服器程序的整個活動期間,用它來接收所有客戶機的連線請求,在伺服器程序終止前關閉這個徵聽套接字;對於沒一個接收的(accepted)連線,TCP協議都建立一個新的連線套接字來標識這個連線,伺服器使用這個連線套接字與客戶機進行通訊操作,當伺服器處理完這個客戶機請求時,關閉這個連線套接字。
當函式accept阻塞等待已經建立的連線時,如果程序捕獲到訊號,函式將以錯誤返回,錯誤型別為EINTR。對於這種錯誤,一般重新呼叫函式accept來接收連線。
(6) 函式close()
函式close關閉一個套接字描述符。定義如下:
#include< unistd.h> 
int close(int sockfd);
執行成功時返回0,否則返回-1。與操作檔案描述符的close一樣,函式close將套接字描述符的引用計數器減1,如果描述符的引用計數大於0,則表示還有程序引用這個描述符,函式close正常返回;如果為0,則啟動清除套接字描述符的操作,函式close立即正常返回。
呼叫close之後,程序將不再能夠訪問這個套接字,但TCP協議將繼續使用這個套接字,將尚未傳送的資料傳遞到對方,然後傳送FIN資料段,執行關閉操作,一直等到這個TCP連線完全關閉之後,TCP協議才刪除該套接字。
(7) 函式read()和write()
用於從套接字讀寫資料。定義如下:
int read(int fd,char * buf,int len)
int write(int fd,char * buf,int len)
函式執行成功時,返回讀或寫的資料量的大小,失敗時返回-1。
每個TCP套接字都有兩個緩衝區:套接字傳送緩衝區、套接字接收緩衝區,分別處理髮送和接收任務。從網路讀、寫資料的操作是由TCP協議在核心中完成的:TCP協議將從網路上接收到的資料儲存在相應套接字的接收緩衝區中,等待使用者呼叫函式將它們從接收緩衝區拷貝到使用者緩衝區;使用者將要傳送的資料拷貝到相應套接字的傳送緩衝區中,然後由TCP協議按照一定的演算法處理這些資料。
讀寫連線套接字的操作與讀寫檔案的操作類似,也可以使用函式read和write。函式read完成將資料從套接字接收緩衝區拷貝到使用者緩衝區:當套接字接收緩衝區有資料可讀時,1:可讀資料量大於函式read指定值,返回函式引數len指定的資料量;2:了度資料量小於函式read指定值,函式read不等待請求的所有資料都到達,而是立即返回實際讀到的資料量;當無資料可讀時,函式read將阻塞不返回,等待資料到達。
當TCP協議接收到FIN資料段,相當於給讀操作一個檔案結束符,此時read函式返回0,並且以後所有在這個套接字上的讀操作均返回0,這和普通檔案中遇到檔案結束符是一樣的。
當TCP協議接收到RST資料段,表示連接出現了某種錯誤,函式read將以錯誤返回,錯誤型別為ECONNERESET。並且以後所有在這個套接字上的讀操作均返回錯誤。錯誤返回時返回值小於0。
函式write完成將資料從使用者緩衝區拷貝到套接字傳送緩衝區的任務:到套接字傳送緩衝區有足夠拷貝所有使用者資料的空間時,函式write將資料拷貝到這個緩衝區中,並返回老輩的數量大小,如果可用空間小於write引數len指定的大小時,函式write將阻塞不返回,等待緩衝區有足夠的空間。
當TCP協議接收到RST資料段(當對方已經關閉了這條連線之後,繼續向這個套接字傳送資料將導致對方TCP協議返回RST資料段),TCP協議接收到RST資料段時,函式write將以錯誤返回,錯誤型別為EINTR。以後可以繼續在這個套接字上寫資料。
(8) 函式getsockname()和getpeername()
函式getsockname返回套接字的本地地址;函式getpeername返回套接字對應的遠端地址。

10、 結束語
網路程式設計全靠套接字接收和傳送資訊。上文主要講述了Linux 下Socket的基本概念、Sockets API以及Socket所涉及到的TCP常識

posted on 2006-04-20 17:40

楊粼波 閱讀(802) 評論(0)  編輯 收藏 引用 所屬分類: 網路程式設計