1. 程式人生 > >【Linux】Linux網路程式設計(含常見伺服器模型,上篇)

【Linux】Linux網路程式設計(含常見伺服器模型,上篇)

基本資料結構介紹

Linux系統是通過提供巢狀字(socket)來進行網路程式設計的。網路程式通過socket和其他幾個函式的呼叫,會返回一個通用的檔案描述符,使用者可以將這個描述符看成普通的檔案的描述符來操作,這就是Linux的裝置無關性的好處。使用者可以通過向描述符的讀寫操作實現網路之間的資料交流。

表示套介面的socket結構體

struct socket {
	socket_state		state;            //指明套介面的連線狀態
	short			type;
	unsigned long		flags;

	struct fasync_struct	*fasync_list;
	wait_queue_head_t	wait;

	struct file		*file;            //指向sockfs檔案系統中的相應檔案
	struct sock		*sk;            //任何協議族都有其特定的套介面特性,該域就指向特定協議族的套介面物件
	const struct proto_ops	*ops;            //指明可對套介面進行的各種操作
};

描述套介面通用地址的資料結構sockaddr結構體

由於歷史的緣故,在bind、connect等系統呼叫中,特定於協議的套介面地址結構指標都要強制轉換成該通用的套介面地址結構指標。結構形式如下:

struct sockaddr {
	sa_family_t	sa_family;	/* address family, AF_xxx	*/
	char		sa_data[14];	/* 14 bytes of protocol address	*/
};

描述因特網地址結構的資料結構sockaddr_in(這裡侷限於IP4)

struct sockaddr_in {
        sa_family_t		sin_family;	    /* 描述協議族		*/
        __be16		sin_port;	        /* 埠號			*/
        struct in_addr	sin_addr;	    /* 因特網地址		*/

        unsigned char		__pad[__SOCK_SIZE__ - sizeof(short int) -
                sizeof(unsigned short int) - sizeof(struct in_addr)];
};

基本網路函式介紹

表頭檔案

#include <sys/types.h>
#include <sys/socket.h>

socket()函式

int socket(int domain, int type, int protocol);

函式說明:socket()用來建立一個新的socket,也就是向系統註冊,通知系統建立一通訊埠。

  • 引數domain指定協議域,又稱為協議族。常用的協議族有:AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE等等。協議族決定了socket的地址型別,在通訊中必須採用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與埠號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址,完整的定義在usr/include/bits/socket.h內;
  • 引數type指定socket型別。常用的socket型別有:SOCK_STREAM(雙向連續且可信的資料流,即TCP)、SOCK_DGRAM(不連續不可信賴的資料包連線)、SOCK_PACKET(和網路驅動程式直接通訊)、SOCK_SEQPACKET(連續可信賴的資料包連線)等等;
  • 引數protocol指定協議。常用的協議有:IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議。

返回值:成功則返回socket描述符,失敗則返回-1。

bind()函式

int bind(int sockfd, struct sockaddr * my_addr, int addrlen);

函式說明:bind()函式把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和埠號組合賦給socket。

  • 引數sockfd:即socket描述字,它是通過socket()函式建立了,唯一標識一個socket;
  • 引數addr:一個指向sockaddr型別物件的指標,指向要繫結給sockfd的協議地址。這個地址結構根據地址建立socket時的地址協議族的不同而不同;
  • 引數addrlen:對應的是地址的長度。

返回值:成功則返回0,失敗則返回-1。

對於不同socket的sockaddr,定義了一個通用的資料結構:

struct sockaddr {
	sa_family_t	sa_family;	/* address family, AF_xxx	*/
	char		sa_data[14];	/* 14 bytes of protocol address	*/
};

其中,sa_family為呼叫socket()時的domain引數,即AF_xxxx值;sa_data最多使用14個字元長度。

sockaddr結構會因使用不同的socket domain而有不同的結構定義,例如AF_INET:

struct sockaddr_in {
        sa_family_t    sin_family; /* address family: AF_INET */
        in_port_t      sin_port;   /* port in network byte order */
        struct in_addr sin_addr;   /* internet address */
};

struct in_addr {
        uint32_t       s_addr;     /* address in network byte order */
};

例如AF_INET6:

struct sockaddr_in6 { 
        sa_family_t     sin6_family;   /* AF_INET6 */ 
        in_port_t       sin6_port;     /* port number */ 
        uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
        struct in6_addr sin6_addr;     /* IPv6 address */ 
        uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};

struct in6_addr { 
        unsigned char   s6_addr[16];   /* IPv6 address */ 
};

listen()函式

int listen(int sockfd, int backlog)

函式說明:sockfd引數為要監聽的socket描述字,backlog引數為相應socket可以排隊的最大連線個數。socket()函式建立的socket預設是一個主動型別的,listen()函式將socket變為被動型別的,等待客戶的連線請求。

返回值:成功則返回0,失敗則返回-1。

注意:listen()只適合SOCK_STREAM或SOCK_SEQPACKET的socket型別。如果socket為AF_INET,則引數backlog的最大值為128。backlog不能限制連線的個數,只能限制後備連線(連線請求佇列)的大小;一旦呼叫accept()函式處於請求佇列裡面的後備連線的數量就減一。

connect()函式

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函式說明:sockfd引數為客戶端的socket描述字,addr引數為伺服器的socket地址,addrlen引數為socket地址的長度。客戶端通過呼叫connect()函式來建立與TCP伺服器的連線。

返回值:成功則返回0,失敗返回-1,錯誤原因存於errno中。

accept()函式

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

函式說明:sockfd引數為伺服器的socket描述字,addr引數為指向struct sockaddr *的指標,用於返回客戶端的協議地址,第三個引數為協議地址的長度。

返回值:如果accpet()成功,那麼其返回值是由核心自動生成的一個全新的描述字,代表與返回客戶的TCP連線。

注意:accept()的第一個引數為伺服器的socket描述字,是伺服器開始呼叫socket()函式生成的,稱為監聽socket描述字;而accept()函式返回的是已連線的socket描述字。一個伺服器通常僅僅只建立一個監聽socket描述字,它在該伺服器的生命週期內一直存在。核心為每個由伺服器程序接受的客戶連線建立了一個已連線socket描述字,當伺服器完成了對某個客戶的服務,相應的已連線socket描述字就被關閉。

伺服器與客戶端的資訊函式

位元組轉換函式

區分一下網路位元組序與主機位元組序:

主機位元組序就是我們平常說的大端和小端模式:不同的CPU有不同的位元組序型別,這些位元組序是指整數在記憶體中儲存的順序,這個叫做主機序。引用標準的Big-Endian和Little-Endian的定義如下:

  • Little-Endian就是低位位元組排放在記憶體的低地址端,高位位元組排放在記憶體的高地址端;
  • Big-Endian就是高位位元組排放在記憶體的低地址端,低位位元組排放在記憶體的高地址端。

網路位元組序:4個位元組的32bit值以下面的次序傳輸:首先是0~7bit,其次8~15bit,然後16~23bit,最後是24~31bit。這種傳輸次序稱作大端位元組序。由於TCP/IP首部中所有的二進位制整數在網路中傳輸時都要求以這種次序,因此它又稱作網路位元組序。位元組序,顧名思義位元組的順序,就是大於一個位元組型別的資料在記憶體中的存放順序,一個位元組的資料沒有順序的問題了。

所以:在將一個地址繫結到socket的時候,請先將主機位元組序轉換成為網路位元組序,而不要假定主機位元組序跟網路位元組序一樣使用的是Big-Endian。

為了統一,在Linux下有專門的位元組轉換函式:

#include <netinet/in.h>

unsigned long int htonl(unsigned long int hostlong);        //將32位主機位元組序轉換成網路位元組序
unsigned short int htons(unsigned short int hostshort);        //將16位主機位元組序轉換成網路位元組序
unsigned long int ntohl(unsigned long int netlong);        //將32位網路位元組序轉換成主機位元組序
unsigned short int ntohs(unsigned short int netshort);        //將16位網路位元組序轉換成主機位元組序

在這四個轉換函式中,h代表host,n代表network,l代表long,s代表short。

IP和域名的轉換

在網路上,標誌一臺計算機可以用名字形式的網址,例如blog.csdn.net/qq_38410730,也可以使用地址的IP形式47.95.164.112,它是一個32位的整數,每個網路節點有一個IP地址,它唯一地確定一臺主機,但一臺主機可以有多個IP地址。

在網路中,通常組織執行多個名字伺服器來提供名字與IP地址之間的轉換,各種應用程式通過呼叫解析器庫中的函式來與域名服務系統通訊。常用的解析函式有:

struct hostent * gethostbyname(const char * hostname);            //名字地址轉換為數字地址
struct hostent * gethostbyaddr(const char * addr, int len, int type);            //數字地址轉換為名字地址

函式說明:第二個函式sddr引數為含有IP地址資訊的in_addr結構的指標(為了同時傳遞IPv4之外的其他資訊,設定為char*型別的);len引數為地址資訊的位元組數,IPv4為4,IPv6為16;type引數為地址族資訊,IPv4為AF_INET,IPv6為AF_INET6。

返回值:兩個函式失敗時返回NULL且設定h_errno錯誤變數,呼叫h_strerrno()可以得到詳細的錯誤資訊。

其中,struct hostent的定義為:

struct hostent
{
        char *h_name;         //正式主機名
        char **h_aliases;     //主機別名
        int h_addrtype;       //主機IP地址型別:IPV4-AF_INET
        int h_length;		  //主機IP地址位元組長度,對於IPv4是四位元組,即32位
        char **h_addr_list;	  //主機的IP地址列表
};
	
#define h_addr h_addr_list[0]   //儲存的是IP地址

例如:

#include <stdio.h>
#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main(int argc, char *argv[])
{
	char *ptr, **pptr;
	struct hostent *hptr;
	char str[32] = {0};
	ptr = argv[1];
	
	if((hptr = gethostbyname(ptr)) == NULL){
		printf("gethostbyname error: %s\n", ptr);
		return 0;
	}
	
	printf("official hostname:%s\n", hptr->h_name);   //主機規範名
	for(pptr = hptr->h_aliases; *pptr != NULL; pptr++)   //將主機別名打印出來
		printf("alias: %s\n", *pptr);
	
	switch(hptr->h_addrtype)  //根據地址型別,將地址打印出來
	{
		case AF_INET:
		case AF_INET6:
			pptr = hptr->h_addr_list;
		
			for(; *pptr != NULL; pptr++)   //將得到的所有地址打印出來
			{
				printf("address: %s\n", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));   //inet_ntop: 將網路位元組序的二進位制轉換為文字字串的格式
				printf("first address: %s\n", inet_ntop(hptr->h_addrtype, hptr->h_addr, str, sizeof(str)));
			}
			break;
		default:
			printf("unkown address type\n");
			break;
	}
	
	return 0;
}

字串的IP和32位的IP轉換

網路上用的IP地址都是數字加點構成的,而在struct in_addr結構中用的是32位的IP,把數字加點型別轉換成32位IP,可以使用下面兩個函式:

int inet_aton(const char * cp, struct in_addr * inp);        //將數字加點型別轉化成32位的IP,儲存在inp指標裡
char * inet_ntoa(struct in_addr in);        //將32位的IP轉換成數字加點型別

函式中的a表示ASCII,n代表network。

注意與下面兩個函式的區別:

in_addr_t inet_addr(const char *cp);        //將數字加點型別轉化成32位的IP
in_addr_t inet_network(const char *cp);        //將數字加點型別轉化成32位的IP

這兩個函式與inet_aton()的區別在於:

這兩個函式,當IP是255.255.255.255時,會認為這是個無效的IP地址,這是歷史遺留問題,其實在目前大部分的路由器上,這個255.255.255.255的IP都是有效的。而inet_aton()函式認為255.255.255.255是有效的,它不會冤枉這個看似特殊的IP地址。

服務資訊函式

在網路程式中,使用者有時需要知道埠IP和服務資訊,這個時候可以使用以下幾個函式:

int getsockname(int sockfd, struct sockaddr *localaddr, int *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, int *addrlen);

struct servent *getservbyname(const char *servname, const char *protoname);            //某一個協議下的某個服務
struct servent *getservbyport(int port, const char *protoname);

struct servent
{
        char *s_name;            //正式服務名
        char **s_aliases;            //別名列表
        int s_port;            //埠號
        char *s_proto;            //使用的協議
}

例子:

struct servent *sptr;

sptr = getservbyname("domain", "udp");        // DNS using UDP
sptr = getservbyname("ftp", "tcp");        //FTP using TCP

sptr = getservbyport(htons(53), "udp");        // DNS using UDP
sptr = getservbyport(htons(21), "tcp");        //FTP using TCP

完整的讀寫函式

一旦使用者建立了連線,下一步就是進行通訊了。在Linux下,把使用者前面建立的通道看作檔案描述符,這樣伺服器端和客戶端進行通訊時,只要往檔案描述符裡面讀寫東西就可以了,就像使用者往檔案讀寫一樣。

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);

函式說明:將資料寫入已開啟的檔案內,write()會把引數buf所指的記憶體寫入count個位元組到引數fd所指向的檔案內。如果順利,write()會返回實際寫入的位元組數,當有錯誤發生時,則返回-1;

從已開啟的檔案讀取資料,read()會把引數fd所指向的檔案傳送count個位元組到buf指標所指的記憶體中。返回值為實際讀取到的位元組數。

例子:

//客戶端向伺服器端寫
struct my_struct my_struct_client;
write(fd, (void *)&my_struct_client, sizeof(struct my_struct));

//伺服器端的讀
char buffer[sizeof(struct my_struct)];
struct *my_struct_server;
read(fd, (void *)buffer, sizeof(struct my_struct));
my_struct_server=(struct my_struct *)buffer;

需要注意的是:在網路上傳遞資料時,使用者一般都是把資料轉化為char型別的資料來傳遞。接收時也是一樣。同時,使用者沒有必要在網路上傳遞指標(因為傳遞指標時沒有任何意義的,使用者必須傳遞指標所指向的內容)。

這裡還要注意一下堵塞的問題:

在阻塞模式下, 對於TCP套接字(預設情況下),當使用 write() 傳送資料時:

  • 首先會檢查緩衝區,如果緩衝區的可用空間長度小於要傳送的資料,那麼 write() 會被阻塞(暫停執行),直到緩衝區中的資料被髮送到目標機器,騰出足夠的空間,才喚醒 write() 函式繼續寫入資料;
  • 如果TCP協議正在向網路傳送資料,那麼輸出緩衝區會被鎖定,不允許寫入,write() 也會被阻塞,直到資料傳送完畢緩衝區解鎖,write() 才會被喚醒;
  • 如果要寫入的資料大於緩衝區的最大長度,那麼將分批寫入;
  • 直到所有資料被寫入緩衝區 write() 才能返回。

當使用 read() 讀取資料時:

  • 首先會檢查緩衝區,如果緩衝區中有資料,那麼就讀取,否則函式會被阻塞,直到網路上有資料到來;
  • 如果要讀取的資料長度小於緩衝區中的資料長度,那麼就不能一次性將緩衝區中的所有資料讀出,剩餘資料將不斷積壓,直到有 read() 函式再次讀取;
  • 直到讀取到資料後 read() 函式才會返回,否則就一直被阻塞。

這就是TCP套接字的阻塞模式。所謂阻塞,就是上一步動作沒有完成,下一步動作將暫停,直到上一步動作完成後才能繼續,以保持同步性。

使用者資料報傳送

之前的主要是基於TCP協議的網路程式,下面就主要介紹一下基於UDP協議的網路程式。

表頭檔案

#include <sys/types.h>
#include <sys/socket.h>

recvfrom()函式

int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);

函式說明:經socket接收資料,recvfrom()用來接收遠端主機指定的socket傳來的資料,並把資料存到引數buf指向的記憶體空間,引數len為可接收資料的最大長度。引數flags一般設為0,引數from用來指定欲傳送的網路地址,引數fromlen為sockaddr的結構長度。

返回值:成功則返回接收到的字元數,失敗則返回-1。

sendto()函式

int sendto(int s, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);

函式說明:經socket傳送資料,sendto()用來將資料由指定的socket傳給對方主機。引數s為已建好連線的socket,如果利用UDP協議則不需經過連線操作。引數msg指向欲連線線的資料內容,引數flags一般設為0,引數to用來指定欲傳送的網路地址,引數tolen為sockaddr的結構長度。

返回值:成功則返回接收到的字元數,失敗則返回-1。