1. 程式人生 > >網路程式設計—套接字基礎 & 基本TCP套接字程式設計-基本套接字函式

網路程式設計—套接字基礎 & 基本TCP套接字程式設計-基本套接字函式

套接字基礎

一個通用套接字地址結構sockaddr:

struct sockaddr
{
    unsigned short sa_family; //套接字的協議簇地址型別,AF_XX
    char        sa_data[14];//儲存具體的協議地址
};

填充特定協議地址時使用sockaddr_in

struct sockaddr_in
{
    unsigned short  sin_len;         //IPv4地址長度
    short int   sin_family;   //指代協議簇,TCP套接字程式設計為AF_INET
    unsigned short
sin_port; //儲存埠號(使用網路位元組順序),資料型別是一個16位的無符號整數 struct in_addr sin_addr; //儲存IP地址,是一個in_addr結構體 unsigned char sin_zero[8]; //為了讓sockaddr與sockaddr_in兩個資料結構保持大小相同而保留的空位元組 };

作為bind()、connect()、sendto()、recvfrom()等函式的引數時需要使用sockaddr,這時要通過指標強制轉換的方式轉為struct sockaddr*指標。

IPv4地址結構示例

struct sockaddr_in mysock;
mysock.sin_family = AF_INET; //TCP地址結構 mysock.sin_port = htons(3333); //位元組順序轉換函式 mysock.sin_addr.s_addr = inet_addr("166.111.160.10"); //設定IP地址 //如果mysock.sin_addr.s_addr = INADDR_ANY,則不指定IP地址(用於server程式) bzero(&(mysock.sin_zero),8); //設定sin_zero為8位保留位元組

IPV6套接字地址結構sockaddr_in6

#DEFINE SIN6_LEN
struct
sockaddr_in6 { unsigned short sin6_len; //IPv6地址長度,是一個無符號的8位整數,表示128位的IPv6地址 short int sin6_family; //地址型別為AF_INET6 unsigned short sin6_port; //儲存埠號,使用網路位元組順序 unsigned short int sin6_flowinfo; //低24位是流量標號,然後是4位優先順序標誌,剩下4位保留 struct in6_addr sin6_addr; //IPv6地址,網路位元組順序 }; struct in6_addr { unsigned long s6_addr; //網路位元組順序的IPv6地址 };

IP地址轉換函式

  • Inet_aton():將字串形式的IP地址轉換成二進位制形式的IP地址,成功返回1,否則返回0,轉換後的IP地址儲存在引數inp中。
  • inet_ntoa():將32位二進位制形式的IP地址轉換為數字點形式的IP地址,結果在函式返回值中返回。

下面四個函式分別用於長整型和短整型數在網路位元組序和主機位元組序之間進行轉換,其中s指short,l指long,h指host,n指network。

#include <netinet/in.h>
unsigned long htonl(unsigned long host_long);
unsigned short htons(unsigned short host_short);
unsigned long ntohl(unsigned long net_long);
unsigned short ntohs(unsigned short net_short);

套接字的工作原理
BSD套接字——INET套接字——TCP協議
程序在利用套接字進行通訊時,採用客戶-伺服器模型。伺服器首先建立一個套接字,並將某個名稱繫結到該套接字上,套接字的名稱依賴於套接字的底層地址族,但通常是伺服器的本地地址。
對於INET套接字來說,伺服器的地址由兩部分組成:伺服器的IP地址和伺服器的埠地址。已註冊的標準埠可檢視/etc/services 檔案。
將地址繫結到套接字之後,伺服器就可以監聽請求連線該繫結地址的傳入連線。
連線請求由客戶生成,它首先建立一個套接字,並指定伺服器的目標地址以請求建立連線。
傳入的連線請求通過不同的協議層到達伺服器的監聽套接字。
伺服器接收到傳入請求後,如果能夠接受該請求,伺服器必須建立一個新的套接字來接受該請求並建立通訊連線(用於監聽的套接字不能用來建立通訊連線),這時,伺服器和客戶就可以利用建立好的通訊連線傳輸資料。

  1. 建立套接字——2. 在INET BSD套接字上繫結(bind)地址——3. 在INET BSD套接字上建立連線 (connect)——4. 監聽(listen) INET BSD 套接字——5. 接受連線請求(accept)
    接受連線請求(accept):
    接受操作在監聽套接字上進行,從監聽 socket 中克隆一個新的 socket 資料結構。其過程如下:
    接受操作首先傳遞到支援協議層,即INET中,以便接受任何傳入的連線請求。接受操作可以是阻塞的或是非阻塞的。非阻塞時,若沒有可接受的傳入連線,則接受操作將失敗,而新建立的socket資料結構被拋棄。阻塞時,執行阻塞操作的網路應用程式將新增到等待佇列中並保持掛起直到接收到一個TCP連線請求為止。
    當連線請求到達之後,包含連線請求的sk_buff被丟棄,而由TCP建立的新sock資料結構返回到INET套接字層,在這裡,sock資料結構和先前建立的新socket資料結構建立連結。而新socket的檔案描述符(fd)被返回到網路應用程式,此後,應用程式就可以利用該檔案描述符在新建立的INET BSD套接字上進行套接字操作。

套接字為使用者提供的系統呼叫
系統呼叫 說明
Accept 接收套接字上連線請求
Bind 在套接字繫結地址資訊
Connet 連線兩個套接字
Getpeername 獲取已連線端套接字的地址
Getsockname 獲取套接字的地址
Getsockopt 獲取套接字上的設定選項
Listen 監聽套接字連線
Recv 從已連線套接字上接收訊息
Recvfrom 從套接字上接收訊息
Send 向已連線的套接字傳送訊息
Sendto 向套接字傳送訊息
Setdomainname 設定系統的域名
Sethostid 設定唯一的主機識別符號
Sethostname 設定系統的主機名稱
Setsockopt 修改套接字選項
Shutdown 關閉套接字
Socket 建立套接字通訊的端點
Socketcall 套接字呼叫多路複用轉換器
Socketpair 建立兩個連線套接字

gethostbyname():主機名轉換為IP地址
gethostbyaddr():IP地址轉換成主機名
getservbyname():根據給定名字查詢相應服務,返回服務的埠號
getservbyport():給定埠號和可選協議查詢相應服務
gethostbyname():主機名轉換為IP地址
gethostbyaddr():IP地址轉換成主機名
getservbyname():根據給定名字查詢相應服務,返回服務的埠號
getservbyport():給定埠號和可選協議查詢相應服務
位元組處理函式
套接字地址是多位元組資料,不是以空字元結尾的,Linux提供兩組函式來處理多位元組資料。一組函式以b開頭,適合BSD系統相容的函式;另一組函式以mem開頭,是ANSI C提供的函式。

#include<string.h>
void bzero(void *s,int n);
函式bzero將引數s指定的內容的前n個位元組設定為0,通常用它來將套接字地址清零。
void bcopy(const void *src,void *dest,int n);
函式bcopy從引數src指定的記憶體區域拷貝指定數目的位元組內容到引數dest指定的記憶體區域
void bcmp(const void *s1,const void *s2,int n);
函式bcmp比較引數s1指定的記憶體區域和引數s2指定的記憶體區域的前n個位元組內容,相同則返回0,否則返回非0
void *memset(void *s,int c,size_t n);
將引數s指定的記憶體區域的前n個位元組設定為引數c的內容
void *memcpy(void *dest,void *src,size_t n);
類似於bcopy,但bcopy能處理引數src和引數dest所指定的區域有重疊的情況,而memcpy不能
void memcmp(const void *s1, const void *s2,size_t n);
比較引數s1和引數s2指定區域的前n個位元組內容,相同則返回0,否則返回非0

基本TCP套接字程式設計-基本套接字函式

1.建立套接字

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,  int type,  int protocol);
返回:若成功返回一個正整數(套接字描述符),否則返回-1

套接字的域名(domain),代表套接字協議族
套接字的型別(types),最常用的值是SOCK_STREAM、SOCK_DGRAM和SOCK_RAW
使用的協議(protocol),一般情況下該引數為0,表示由系統在當前設定的domain下,自動選擇適合的協議型別

sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd<0){
    fprintf(stderr,”socket error:%s\n”,strerror(errno);
    exit(1);
}

Linux系統中建立一個套接字的操作主要是:
在核心中建立一個套接字資料結構,然後返回一個套接字描述符。這個套接字資料結構包含連線的各種資訊。例如:對方地址,TCP狀態,以及傳送和接收快取等。
TCP協議根據這個套接字資料結構來控制這條連線。

實際上,套接字對於使用者程式而言就是特殊的已開啟的檔案。核心中為套接字定義了一種特殊的檔案型別,形成一種特殊的檔案系統sockfs。
所謂建立一個套接字,就是在sockfs檔案系統中建立一個特殊檔案,或者說一個節點,並建立起為實現套接字功能所需的一整套資料結構。
所以,函式sock_create()首先是建立一個socket資料結構,然後將其“對映”到一個已開啟的檔案中,進行socket結構和sock結構的分配和初始化。

  1. 流式套接字連線發起
#include <sys/types.h>
#include <sys/socket.h>
int connect (int sockfd, struct sockaddr_in *serveraddr, int serveraddrlen);
返回:成功返回0,失敗返回-1,並設定errno為以下任一種錯誤。

ETIMEDOUT:若客戶端TCP在發出首個SYN分段後沒有收到任何應答,則大約在呼叫connect()函式75秒後將返回該錯誤;
ECONNREFUSED:當伺服器程序並未啟動,此時客戶端TCP將向客戶端應用返回一個RST錯誤 。
EHOSTUNREACH或ENETUNREACH:若客戶端TCP發出的SYN分段收到了途經的中間路由器的“目標不可達”ICMP報文,則客戶端TCP會重發SYN分段直到超過75秒,此時客戶端TCP向客戶端應用返回該錯誤
按照TCP狀態轉換圖,connect函式導致當前套接字從CLOSED狀態轉移到SYN_SENT狀態。
若成功則再轉移到ESTABLISHED狀態。
若失敗則該套接字不再可用,必須關閉,不能對這樣的套接字再呼叫connect。
在呼叫connect前,客戶機需要指定伺服器程序的套接字地址。
客戶機一般不指定自己的套接字地址,系統會自動從1024至5000的埠範圍內為它選擇一個未用的埠號,然後以這個埠號和本機的IP地址填充套接字地址。

建立一個TCP連線的操作一般如下:
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERVER_PORT);
if(inet_aton(argv[1], &servaddr.sin_addr)<0){
fprintf(stderr,“inet_aton error\n”;
exit(1);
}
if(connect(sockfd, (struct sockaddr *) &servaddr,sizeof(servaddr))<0){
fprintf(stderr,“connect error:%s\n”,strerror(errno));
exit(1);
}


  1. 繫結套接字
  • int bind (int sockfd, struct sockaddr_in *localaddr, int localaddrlen);
    返回:成功返回0,否則返回-1,並設定errno,錯誤型別為EADDRINUSR。
    伺服器和客戶機都可以呼叫函式bind來繫結套接字地址,但是一般都是伺服器呼叫bind來繫結自己的公認埠號。繫結操作一般如下:

bzero(&my_addr,sizeof(my_addr));
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(PORT);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { 
    fprintf(stderr,“bind to port %d error!\n”,PORT);
    exit(1); 
}

  1. 流式套接字連線監聽
  • int listen (int sockfd, int backlog ); 返回:成功返回0,否則返回-1。
    socket建立的主動套接字,可以用它來進行主動連線(呼叫connect),但是不能接收連線請求。而伺服器的套接字必須能都接收客戶機的請求。listen將一個尚未連線的主動套接字轉換成一個被動套接字:
    告訴TCP協議,這個套接字可以接收連線請求;
    執行listen後,TCP狀態由CLOSED狀態轉換成LISTEN狀態。
    TCP將到達的連線請求排隊,backlog指定這個佇列的最大長度。
  • 流式套接字連線接收
  • int accept (int sockfd, struct sockaddr_in clientaddr, int clientaddrlen);
    返回:成功返回連線套接字描述符>0,否則返回-1。
    accept從監聽套接字的完成連線佇列中接收一個已經建立的TCP連線。TCP協議建立一個新的連線套接字來標識這個要接收的連線,並將其描述符返回給應用程式。
    監聽套接字
    連線套接字
  • 關閉套接字描述符

#include <unistd.h>
int close(int sockfd);
int shutdown (int sockfd, int how);

close將這個套接字描述符標記為關閉狀態(不同於CLOSED),然後立即返回程序。
管理套接字後後程序將不能訪問這個套接字。但是TCP協議繼續使用這個套接字,將尚未傳送的資料傳遞到對方,然後傳送FIN資料段,執行關閉操作,一直等到TCP連線完全關閉後,TCP才刪除這個套接字。
7. 讀流式套接字

#include <unistd.h>
int read(int sockfd,  const void *buf,  int buflen);
返回:實際所讀取的資料位元組長度,0表示讀到檔案尾,-1表示錯誤

每個套接字都有兩個緩衝區:接收緩衝區和傳送緩衝區
如果可讀資料大於len指定值,則返回指定長度的值
如果可讀資料量小於len,read不等待所有資料都到達,而是立即返回緩衝區所有資料,此時返回值小於len。①
當無資料可讀時,read將阻塞,等待資料到達。
當程式從一個套接字讀資料時,有以下幾種情況:
套接字接受緩衝區中接收到資料(返回>0)
接收到FIN資料段(返回=0)
接收到RST資料段(返回=-1,ECONNRESET)(當TCP協議接收到RST資料段,表示連接出現了某種錯誤,函式read將以錯誤返回,錯誤型別為ECONNERESET。)
程序阻塞過程中接收到訊號(返回=-1,EINTR)②
在上面所述的兩種情況下並不表示讀操作發生錯誤。所以用下面的readn函式繼續讀操作。
8. 寫流式套接字

#include <unistd.h>
int write(int sockfd,  const void *buf,  int buflen);
返回:實際所寫的資料位元組長度,-1表示錯誤

當程式從一個套接字寫資料時,有以下幾種情況:
傳送緩衝區有足夠的空間(返回>0)
接收到RST資料段(返回=-1,EPIPE)
程序阻塞過程中接收到訊號(返回-1,EINTR)
為了處理EINTR錯誤,繼續寫操作,通常用下面的writen實現。