歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> UNIX套接字編程

UNIX套接字編程

日期:2017/3/1 11:45:09   编辑:關於Linux

在UNIX中,創建套接字時和文件打開一樣,在描述符表中取回一個int類型的索引號。套接字和文件是共享描述符表,因此他們的索引號不能重復,一個進程能同時創建最大的套接字數和文件數是相同的。

下面介紹開發中使用的函數。

1.套接字的創建、關閉

創建套接字時使用的函數是 socket(),其原型如下(注:函數參數詳細介紹就不多贅述了)
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);

2.Blocking和Nonblocking

在套接字編程中,有“Blocking mode socket”和“Nonblocking mode socket”倆種模式,這倆種套接字模式的區別就是:當用創建的套接字進行處理時,比如調用特定的套接字函數進行數據發送或者接收時,等還是不等改函數的結果。 在以Blocking方式創建的套接字中,保存有對方主機傳送的數據時。我們把該數據取到程序變量所使用的函數recv為例,若有10個字節數據可取,則使用recv函數時,取完10個字節後,返回recv函數。但如果沒有recv函數可取的數據,則recv函數一直等待可取數據,不返回。 與此相反,若以Nonblocking方式創建套接字,則調用recv函數時,若有可取的數據,則取到程序的緩沖器中,若沒有可取的數據,則返回一個錯誤信息。實際中,利用socket創建的套接字是Blocking模式,若想轉換成Noblocking模式,需要進行下面的處理:
/*
設置套接字為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函數。

3.與其他主機進行連接

在OSI協議層的分組交換網的網絡協議中,采用“兩次握手”方式建立連接。在這種方式下,接受連接請求的主機發出同意連接應答,就可以實現連接,即關於建立連接的報文雙方只傳一次就可以了。 在TCP/IP中,建立連接的方式不是“兩次握手”,而是“三次握手”。在“兩次握手”方式中,建立連接的雙方只需要進行倆次數據交換就可以建立連接,但“三次握手”中,需要進行三次數據交換,才可以建立連接。 與其他主機建立連接的函數是connect(),connect的函數原型如下:
int connect(int s, const struct sockaddr *name, int namelen);
當connect函數的調用成功時,即與對方主機連接成功時,函數返回值為0,其他情況返回-1,若需要查看詳細錯誤原因,可以利用errno函數檢查。
下面是connect函數的應用示例。
/*
利用指定的參數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;
}

4.等待連接

客戶端使用socket創建套接字,再利用connect與其他主機建立連接。而作為接受請求的主機服務器,在使用socket創建套接字後,需要綁定創建的套接字和客戶端將請求連接的地址,負責套接字和地址綁定的函數是bind()函數,bind函數的原型如下:
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的處理方法可能不同,有可能只允許一個客戶連接,也可能一個用戶都不允許連接。

5.接受連接

當客戶端發起連接請求並在backlog queue中等待時,就要處理接受連接的請求,並且獲取與客戶端交換數據的套接字,完成該功能的函數就是accept()。accept()函數的原型如下:
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。

6.數據傳送

send

send()是通過套接字給對方主機發送數據的函數,該函數的原型如下所示:
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;
}
(切記隨手保存啊、、、、這一段寫了倆次,哭了大哭大哭

recv

recv是當對方主機通過send函數傳輸的數據保存到系統recv的隊列時,從系統緩沖器中取出數據並存儲到程序變量的函數,recv函數的原型如下所示。
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數據有序的存儲在這一緩沖器中,然後,以被存儲的數據為基准,進行分析和使用。采用這樣的方式,才可以進行完整的數據分析,也能防止數據的丟失。

write

由於在UNIX操作系統中,可以像文件描述符的處理方式一樣處理套接字,所以在套接字的處理中,也可以使用write函數。在套接字處理中,write函數的與本質上與send函數的用途相同。write函數的原型如下:
int write(int sock, void* buf, size_t len);
其參數與send函數的參數意義一致。

read

send函數和recv函數的關系與write函數和read函數的關系相同,即有相同的功能。read函數的原型如下:
int read(int sock, void* buf, size_t len);

sendto、recvfrom

sendto和recvfrom函數可以稱為UDP函數。與TCP函數不同,UDP函數是沒有必要再創建套接字後調用connect函數,在服務器中也沒必要調用accept函數,直接可以進行下一步處理的函數。如果把sendto是函數connect和send函數的合並,recvfrom當做accept和recv函數的合並,及容易理解這倆個函數了。這倆個函數的原型如下:
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數據)。
Copyright © Linux教程網 All Rights Reserved