在UNIX中,創建套接字時和文件打開一樣,在描述符表中取回一個int類型的索引號。套接字和文件是共享描述符表,因此他們的索引號不能重復,一個進程能同時創建最大的套接字數和文件數是相同的。
下面介紹開發中使用的函數。
int socket(int af, int type, int protocol);socket函數的返回值是新創建的套接字的int型索引。如果套接字創建失敗,則socket函數的返回值為 -1,具體錯誤原因可以通過errno函數查找。 下面是socket() 函數的示例。
/* 套接字的創建函數,創建成功返回套接字索引,創建失敗時返回-1 */ int CreateSocket() { int newSocket; newSocket = socket(AF_INET, SOCK_STREAM, 0); if (newSocket < 0) return -1; return newSocket; }當想終止套接字的連接或者套接字的使用結束時,我們需要返還套接字的資源。與文件一樣,使用結束的套接字返回是用函數close() 完成。close() 的原型如下:
int close(int d);close的唯一參數是要結束的套接字的索引。這裡有一個問題需要考慮的是,如果還存在要傳送給對方主機的數據或者還沒有處理對方送來的數據,則默認處理方式是close函數等這些未處理的數據梳理完後,返還套接字的資源。 除close函數外,shutdown也能關閉套接字。一般情況下使用close函數居多,但shutdown函數提供更多的選項。shutdown函數的原型如下:
int shutdown(int socket, int direction);
/* 設置套接字為Nonblocking模式 */ void NonBlock(int sock) { int flags = fcntl(sock, F_GETFL, 0); flags |= O_NONBLOCK; if ( fcntl(sock, F_SETFL, flags) < 0 ) exit(1); }fcntl函數是提供獲取或修改套接字等文件描述符的屬性功能的UNIX函數。
int connect(int s, const struct sockaddr *name, int namelen);當connect函數的調用成功時,即與對方主機連接成功時,函數返回值為0,其他情況返回-1,若需要查看詳細錯誤原因,可以利用errno函數檢查。
/* 利用指定的參數int sock,與localhost(127.0.0.1)的8081端口建立連接 成功返回1,失敗返回0 */ int ConnectToServer(int sock) { struct sockaddr_in addr_in; addr_in.sin_family = AF_INET; addr_in.sin_addr.s_addr = inet_addr("127.0.0.1"); addr_in.sin_port = htons(8081); if (connect(sock, (struct sockaddr*)&addr_in, sizeof(addr_in) < 0) return 0; return 1; }除了使用ip與主機建立連接外,還可以使用DNS進行連接。用域名時,不能用把ip地址轉換成二進制形式的inet_addr函數來獲取對方主機目的地址。為了把域名轉換成二進制形式的地址,需要使用如下所示的gethostbyname函數。
/* 傳送到char* adrr參數的Internet域名地址,轉換成long類型地址形式 */ unsigned long GetAddrBydomian(char* addr) { struct hostent *ph; struct in_addr in; ph = gethostname(addr); if (ph == NULL) return NULL; memcpy((char**)&(in), ph->h_addr, ph->h_length); return in.s_addr; }上述函數中,使用gethostname函數把域名轉換成hostent結構體返回,在hostent結構體中獲取二進制形式的地址。一般引用hostent結構體定義的變量,使用成員h_addr,該成員指向h_addr_list第一個指針變量的地址。下面是利用GetAddrBydomian函數的ConnectToServer的應用示例。
/* 既可以使用ip地址,也可以使用域名的conncet函數 成功返回1,失敗返回0 */ int ConnectToServer(int sock,char* address,int port,int isDomain) { struct sockaddr_in addr_in; addr_in.sin_family = AF_INET; addr_in.sin_port = htons(port); if (isDomain) addr_in.sin_addr.s_addr = GetAddrBydomian(address); else addr_in.sin_addr.s_addr = inet_addr(address); if (connect(sock, (struct sockaddr*)&addr_in, sizeof(addr_in) < 0) return 0; return 1; }
int bind(int sock, const struct sockaddr* addr, socklen_t addrlen);如果bind函數返回0,則說明綁定成功;若返回值是-1,則可能因為各種原因而綁定失敗,一般如果其他地方沒什麼問題,失敗的原因很可能是指定的地址與其他套接字綁定了。下面是bind函數的應用示例:
/* 綁定指定的int sock套接字和指定的int port端口號 */ int BindServerSock(int sock, int port) { struct sockaddr_in sa; sa.sin_family = AF_INET; sa.sin_port = htons(port); sa.sin_addr.s_addr = INADDR_ANY; if (bind(sock, (struct sockaddr*)&sa, sizeof(sa)) < 0) return 0; return 1; }需要注意的是結構體sockaddr_in的sin_addr變量使用的是INADDR_ANY值,INADDR_ANY不是指定特定的值,而是任意值,以便欲連接到特定端口的任何Internet地址都能與對應的套接字進行連接。 使用bind函數成功將套接字與地址綁定後,需要設置套接字為等待客戶端連接請求的狀態,完成這一功能的函數就是listen()函數,listen函數的原型為:
int listen(int sock, int backlog);在該函數中需要特別留意的是參數backlog。當新的客戶使用connect請求連接時,在連接請求被處理之前,將在backlog queue中等待,而backlog用於確定backlog queue的大小。假設該值設為10,則當未處理的連接用戶大於10時,將產生backlog queue的溢出,此後請求連接的用戶將出現錯誤,給系統網絡帶來嚴重問題。該值最好根據最大同時連接的客戶端數量來指定backlog的值。backlog的值會受到系統最大值的限定,若大於系統最大值,則系統自動調整為系統最大值,所以不用擔心太大。但是backlog的值不能為0,根據操作系統的不同,設置為0的處理方法可能不同,有可能只允許一個客戶連接,也可能一個用戶都不允許連接。
int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);accept返回值是新連接請求被處理結束後服務端與客戶端進行通信的套接字的編號。當accept返回-1時,表示沒有正常結束新的連接請求。一般accept有問題,很可能是整個網絡功能有問題或者超出可創建套接字的最大數。詳細的錯誤原因可以通過error獲得。下面是accept的應用示例:
/* 接收新的客戶端連接 */ int AcceptNewConnect(int sock) { int newSock; struct sockaddr_in per; socklen_t perSize; perSize = sizeof(per); newSock = accept(sock, (struct sockaddr*)&per,&perSize); if (newSock < 0) return -1; return newSock; }該函數的參數int sock是指定通過bind->listen函數處理,等待新連接的套接字,而返回值是與新連接進行數據交換的套接字編號,該函數失敗時返回-1。
size_t send(int sock, const char* msg, size_t len, int flags);send函數的返回值是通過send函數發送數據的長度,當使用Nonblocking套接字時,有可能只發送比len指定長度小的數據。了解TCP/IP的數據傳送形態就很容易理解出現這種情況的原因。在TCP/IP中,通過send函數發送的數據並不是直接發送到對方主機,而是先存儲到緩沖器(嚴格來講,可看做是通過TCP/IP4層協議的過程)後傳送。如果系統的緩沖器沒有剩余空間,或因其他原因,當調用send函數時不能保存msg變量的內容,則Blocking套接字等到處理結束,而Nonblocking套接字完成可能的處理後(或者沒有完成處理本身)返回錯誤代碼。如果在不了解這樣的特性的情況下編寫套接字應用程序,則當系統處於客戶爆滿或者系統網絡處於超負荷等原因不能正常的數據交換時,就會碰到很多莫名其妙的bug。雖然在目前的高配置硬件環境下,這樣的現象很少見,但最好還是考慮進去。如果send返回小於0的值,意味著發送數據的套接字出了問題,需要通過error函數檢測錯誤代碼。下面是send的應用示例:
/* 通過send向對方主機發送數據 */ int SendData(int sock, const char* buf, int size) { int sendSize; sendSize = send(sock, buf, size, 0); if (sendSize == 0) return -1; //當出現nonblocking模式的錯誤時 #ifdef EAGAIN if (error == EAGAIN) return 0; #endif // EAGAIN #ifdef EWOULDBLOCK if (ERROR == EWOULDBLOCK) return 0; #endif // EWOULDBLOCK //如果傳送的數據和實際需要傳送的數據不一致,則發送錯誤 if ((size - sendSize) != 0) return -1; return sendSize; }(切記隨手保存啊、、、、這一段寫了倆次,哭了)
int recv(int s, void* buf, size_t len, int flags);recv返回值是從系統recv queue中取出並存儲在buf變量的數據字節數,當返回0時,意味著對方主機正常斷開了連接。當recv函數返回值小於0時,一位置相關套接字有錯誤。當使用Nonblocking套接字時,如果error的值為EWOULDBLOCK或者EAGAIN,則意味著並不是套接字本身有錯,而是雖然調用了recv函數,但由於系統的recv queue空,沒有可取出的數據,因此,直接返回recv函數。通過recv函數的第三個參數len傳遞的從系統recv queue取出並存儲在buf變量的數據長度總是最大值。最大值的意思是,當實際系統的recv queue中有10字節數據時,即使設置len參數為1024,recv函數只取出10個字節數據,並返回值是10。與此相反,當實際系統的recv queue中有1024字節數據時,如果設置len參數為10,則只取出10個字節數據存儲在buf變量中,而1014個字節數據仍然留在系統recv queue中。下面是recv函數的應用示例:
/* 數據的接收 */ int RecvData(int sock, char* recv_buf, int size) { int recvSize; recvSize = recv(sock, recv_buf, size, 0); if (recvSize > 0) return recvSize; if (recvSize == 0) return -1; //如果recvSize小於0 #ifdef EINTR if (ERROR == EINTR) return 0; #endif // EINTR #ifdef EAGAIN if (error == EAGAIN) return 0; #endif // EAGAIN #ifdef EWOULDBLOCK if (ERROR == EWOULDBLOCK) return 0; #endif // EWOULDBLOCK return -1; }在Nonblocking模式的套接字中,使用recv函數時,應該注意如下內容:
send(sock, "abcd", 4, 0);在使用send發送“abcd”4個字節的字符串到對方主機時,如果想的簡單,則會認為在接收數據的主機方,調用一次recv函數,就獲取4個字節的“abcd”字符串。但實際上,recv並不是這樣工作的,這就給編程帶來麻煩。明明是傳送了4個字節的字符串,但作為調用recv函數的主機是無法知道以什麼順序傳輸了多少個字節的數據的。作為接收的一方,可以調用一次recv函數獲取4個字節的字符串;也可以調用倆次recv函數獲取字符串。因此,作為調用recv函數的接收方,很難知道接收的數據時以什麼形式傳輸過來的。為了屏蔽這樣的特性,一般的做法是在網絡應用程序內部設置能保存recv的數據的隊列或其他數據結構的緩沖器,並把recv數據有序的存儲在這一緩沖器中,然後,以被存儲的數據為基准,進行分析和使用。采用這樣的方式,才可以進行完整的數據分析,也能防止數據的丟失。
int write(int sock, void* buf, size_t len);其參數與send函數的參數意義一致。
int read(int sock, void* buf, size_t len);
int sendto(int s, const char * msg, int len, int flags, const struct sockaddr FAR *to, socket_t tolen); int recvfrom(int s, void *buf, int len, int flags, struct sockaddr *from, socket_t *fromlen);在sendto和recvfrom倆個函數參數中,除第5個和第6個參數之外,其他參數的意義與send、recv函數的參數相同。但是需要注意的地方是,與面向連接的TCP不同,UDP調用各個函數時數據傳輸是獨立處理的。sendto不經過系統send queue,直接進行傳輸處理的。當通過sendto函數按照A,B,C,D順序傳輸數據報,並且利用recvfrom函數接收數據時,有可能按發送順序接收數據,也有可能與發送順序無關的接收數據(在利用send,recv函數的TCP套接字網絡應用程序中,不管怎麼樣,當通過一個套接字傳輸數據時,接收方案send函數的發送順序recv數據)。