Linux環境下的網路編程

來源:互聯網
上載者:User
Linux環境下的網路編程

xuyb 
                Email:bai_xy@21cn.com 
     本文介紹了在Linux環境下的socket編程常用函數用法及socket編程的一般規則和客戶/伺服器模型的編程應注意的事項和常遇問題的解決方案,並舉了具體代  碼執行個體。要理解本文所談的技術問題需要讀者具有一定C語言的編程經驗和TCP/IP方面的基本知識。要實習本文的樣本,需要Linux下的gcc編譯平台支援。  
     Socket定義  
     網路的Socket資料轉送是一種特殊的I/O,Socket也是一種檔案描述符。Socket也具有一個類似於開啟檔案的函數調用—Socket(),該函數返回一個整型的Socket描述符,隨後的串連建立、資料轉送等操作都是通過該Socket實現的。常用 的Socket類型有兩種:流式Socket—SOCK_STREAM和資料報式Socket—SOCK_DGRAM。流式是一種連線導向的Socket,針對於連線導向的TCP服務應用;資料報式Socket是一種不需連線的Socket,對應於不需連線的UDP服務應用。  
    Socket編程相關資料類型定義  
    電腦資料存放區有兩種位元組優先順序:高位位元組優先和低位位元組優先。Intenet上資料以高位位元組優先順序在網路上傳輸,所以對於在內部是以低位位元組優先方式儲存資料的機器,在Internet上傳輸資料時就需要進行轉換。  
   我們要討論的第一個結構類型是:struct sockaddr,該類型是用來儲存socket資訊的:  
     struct sockaddr {  
      unsigned short sa_family; /* 地址族, AF_xxx */  
      char sa_data[14]; /* 14 位元組的協議地址 */ };  
     sa_family一般為AF_INET;sa_data則包含該socket的IP地址和連接埠號碼。  
     另外還有一種結構類型:  
     struct sockaddr_in {  
      short int sin_family; /* 地址族 */  
      unsigned short int sin_port; /* 連接埠號碼 */  
      struct in_addr sin_addr; /* IP地址 */  
      unsigned char sin_zero[8]; /* 填充0 以保持與struct sockaddr同樣大 
   小 */  
     };  
     這個結構使用更為方便。sin_zero(它用來將sockaddr_in結構填充到與struct sockaddr同樣的長度)應該用bzero()或memset()函數將其置為零。指向sockaddr_in 的指標和指向sockaddr的指標可以相互轉換,這意味著如果一個函數所需參數類型是sockaddr時,你可以在函數調用的時候將一個指向sockaddr_in的指標轉換為指向sockaddr的指標;或者相反。sin_family通常被賦AF_INET;in_port和sin_addr應該轉換成為網路位元組優先順序;而sin_addr則不需要轉換。  
 我們下面討論幾個位元組順序轉換函式:  
  htons()--"Host to Network Short" ; htonl()--"Host to Network long" 
  ntohs()--"Network to Host Short" ; ntohl()--"Network to Host Long" 
  在這裡, h表示"host" ,n表示"network",s 表示"short",l表示 "long" 
   。  
   開啟socket 描述符、建立綁定並建立串連  
   socket函數原型為:  
  int socket(int domain, int type, int protocol);  
  domain參數指定socket的類型:SOCK_STREAM 或SOCK_DGRAM;protocol通常賦值“0”。Socket()調用返回一個整型socket描述符,你可以在後面的調用使用它。一旦通過socket調用返回一個socket描述符,你應該將該socket與你本機上的一個連接埠相關聯(往往當你在設計伺服器端程式時需要調用該函數。隨後你就可以在該連接埠監聽服務要求;而用戶端一般無須調用該函數)。 Bind函數原型為 :  
  int bind(int sockfd,struct sockaddr *my_addr, int addrlen);  
  Sockfd是一個socket描述符,my_addr是一個指向包含有本機IP地址及連接埠號碼等資訊的sockaddr類型的指標;addrlen常被設定為sizeof(struct sockaddr)。  
 最後,對於bind 函數要說明的一點是,你可以用下面的賦值實現自動獲得本機IP地址和隨機擷取一個沒有被佔用的連接埠號碼:  
     my_addr.sin_port = 0; /* 系統隨機播放一個未被使用的連接埠號碼 */  
     my_addr.sin_addr.s_addr = INADDR_ANY; /* 填入本機IP地址 */  
  通過將my_addr.sin_port置為0,函數會自動為你選擇一個未佔用的連接埠來使用。同樣,通過將my_addr.sin_addr.s_addr置為INADDR_ANY,系統會自動填入本機IP地址。Bind()函數在成功被調用時返回0;遇到錯誤時返回“-1”並將errno置為相應的錯誤號碼。另外要注意的是,當調用函數時,一般不要將連接埠號碼置為小於1024的值,因為1~1024是保留連接埠號碼,你可以使用大於1024中任何一個沒有被佔用的連接埠號碼。  
  Connect()函數用來與遠端伺服器建立一個TCP串連,其函數原型為:  
  int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);  
 Sockfd是目的伺服器的sockt描述符;serv_addr是包含目的機IP地址和連接埠號碼的指標。遇到錯誤時返回-1,並且errno中包含相應的錯誤碼。進行用戶端程式設計無須調用bind(),因為這種情況下只需知道目的機器的IP地址,而客戶通過哪個連接埠與伺服器建立串連並不需要關心,核心會自動選擇一個未被佔用的連接埠供用戶端來使用。  
  Listen()——監聽是否有服務要求  
  在伺服器端程式中,當socket與某一連接埠捆綁以後,就需要監聽該連接埠,以便對到達的服務要求加以處理。  
  int listen(int sockfd, int backlog);  
 Sockfd是Socket系統調用返回的socket 描述符;backlog指定在請求隊列中允許的最大請求數,進入的串連請求將在隊列中等待accept()它們(參考下文)。cklog對隊列中等待服務的請求的數目進行了限制,大多數系統預設值為20。 
  當listen遇到錯誤時返回-1,errno被置為相應的錯誤碼。  
 故伺服器端程式通常按下列順序進行函數調用:  
 socket(); bind(); listen(); /* accept() goes here */  
  accept()——串連連接埠的服務要求。  
  當某個用戶端試圖與伺服器監聽的連接埠串連時,該串連請求將排隊等待伺服器accept()它。通過調用accept()函數為其建立一個串連,accept()函數將返回一個新的socket描述符,來供這個新串連來使用。而伺服器可以繼續在以前的那個socket上監聽,同時可以在新的socket描述符上進行資料send()(發送)和recv()(接收)操作:  
  int accept(int sockfd, void *addr, int *addrlen);  
  sockfd是被監聽的socket描述符,addr通常是一個指向sockaddr_in變數的指標,該變數用來存放提出串連請求服務的主機的資訊(某台主機從某個連接埠發出該請求);addrten通常為一個指向值為sizeof(struct sockaddr_in)的整型指標變數。錯誤發生時返回一個-1並且設定相應的errno值。  
  Send()和recv()——資料轉送  
  這兩個函數是用於連線導向的socket上進行資料轉送。  
  Send()函數原型為:  
  int send(int sockfd, const void *msg, int len, int flags);  
  Sockfd是你想用來傳輸資料的socket描述符,msg是一個指向要發送資料的指標。  
  Len是以位元組為單位的資料的長度。flags一般情況下置為0(關於該參數的用法可參照man手冊)。  
  char *msg = "Beej was here!"; int len, bytes_sent; ... ...  
  len = strlen(msg); bytes_sent = send(sockfd, msg,len,0); ... ...  
  Send()函數返回實際上發送出的位元組數,可能會少於你希望發送的資料。所以需要對send()的傳回值進行測量。當send()傳回值與len不匹配時,應該對這種情況進行處理。  
  recv()函數原型為:  
  int recv(int sockfd,void *buf,int len,unsigned int flags);  
  Sockfd是接受資料的socket描述符;buf 是存放接收資料的緩衝區;len是緩衝的長度。Flags也被置為0。Recv()返回實際上接收的位元組數,或當出現錯誤時,返回-1共置相應的errno值。  
  Sendto()和recvfrom()——利用資料報方式進行資料轉送  
  在不需連線的資料報socket方式下,由於本地socket並沒有與遠端機器建立串連,所以在發送資料時應指明目的地址,sendto()函數原型為:  
  int sendto(int sockfd, const void *msg,int len,unsigned int flags, const struct sockaddr *to, int tolen);  
  該函數比send()函數多了兩個參數,to表示目地機的IP地址和連接埠號碼資訊,而tolen常常被賦值為sizeof (struct sockaddr)。Sendto 函數也返回實際發送的資料位元組長度或在出現發送錯誤時返回-1。  
  Recvfrom()函數原型為:  
  int recvfrom(int sockfd,void *buf,int len,unsigned int lags,struct sockaddr *from,int *fromlen);  
  from是一個struct sockaddr類型的變數,該變數儲存源機的IP地址及連接埠號碼。fromlen常置為sizeof (struct sockaddr)。當recvfrom()返回時,fromlen包含實際存入from中的資料位元組數。Recvfrom()函數返回接收到的位元組數或當出現錯誤時返回-1,共置相應的errno。  
  應注意的一點是,當你對於資料報socket調用了connect()函數時,你也可以利用send()和recv()進行資料轉送,但該socket仍然是資料報socket,並且利用傳輸層的UDP服務。但在發送或接收資料報時,核心會自動為之加上目地和源地址資訊。  
  Close()和shutdown()——結束資料轉送  
  當所有的資料操作結束以後,你可以調用close()函數來釋放該socket,從而 
   停止在該socket上的任何資料操作:close(sockfd);  
   你也可以調用shutdown()函數來關閉該socket。該函數允許你只停止在某個方向上的資料轉送,而一個方向上的資料轉送繼續進行。如你可以關閉某socket的寫操作而允許繼續在該socket上接受資料,直至讀入所有資料。  
  int shutdown(int sockfd,int how);  
  Sockfd的含義是顯而易見的,而參數 how可以設為下列值:  
  ·0-------不允許繼續接收資料  
  ·1-------不允許繼續發送資料  
  ·2-------不允許繼續發送和接收資料,均為允許則調用close ()  
  shutdown在操作成功時返回0,在出現錯誤時返回-1(共置相應errno)。  

  DNS——網域名稱服務 (DNS)相關函數  
  由於IP地址難以記憶和讀寫,所以為了讀寫記憶方便,人們常常用網域名稱來表示主機,這就需要進行網域名稱和IP地址的轉換。函數gethostbyname()就是完成這種轉換的,函數原型為:  
     struct hostent *gethostbyname(const char *name);  
     函數返回一種名為hosten的結構類型,它的定義如下:  
     struct hostent {  
      char *h_name; /* 主機的官方網域名稱 */  
      char **h_aliases; /* 一個以NULL結尾的主機別名數組 */  
      int h_addrtype; /* 返回的地址類型,在Internet環境下為AF-INET */  

      int h_length; /*地址的位元組長度 */  
      char **h_addr_list; /* 一個以0結尾的數組,包含該主機的所有地址*/  

     };  
     #define h_addr h_addr_list[0] /*在h-addr-list中的第一個地址*/  
     當 gethostname()調用成功時,返回指向struct hosten的指標,當調用失敗時返回-1。當調用gethostbyname時,你不能使用perror()函數來輸出錯誤資訊,而應該使用herror()函數來輸出。  
   連線導向的客戶/伺服器代碼執行個體  
  這個伺服器通過一個串連向客戶發送字串"Hello,world!"。只要在伺服器上運行該伺服器軟體,在用戶端運行客戶軟體,用戶端就會收到該字串。 
  該伺服器軟體代碼見程式1:  
     #include stdio.h  
     #include stdlib.h  
     #include errno.h  
     #include string.h  
     #include sys/types.h  
     #include netinet/in.h  
     #include sys/socket.h  
     #include sys/wait.h  
     #define MYPORT 3490 /*伺服器監聽連接埠號碼 */  
     #define BACKLOG 10 /* 最大同時串連請求數 */  
     main()  
     {  
     intsock fd,new_fd; /* 監聽socket: sock_fd,資料轉送socket:new_fd*/  
     struct sockaddr_in my_addr; /* 本機地址資訊 */  
     struct sockaddr_in their_addr; /* 客戶地址資訊 */  
     int sin_size;  
     if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { /*錯誤偵測  */  
     perror("socket"); exit(1); }  
     my_addr.sin_family=AF_INET;  
     my_addr.sin_port=htons(MYPORT);  
     my_addr.sin_addr.s_addr = INADDR_ANY;  
     bzero(&(my_addr.sin_zero),8);  
     if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))   
      == -1) {/*錯誤偵測*/  
     perror("bind"); exit(1); }  
     if (listen(sockfd, BACKLOG) == -1) {/*錯誤偵測*/  
     perror("listen"); exit(1); }  
     while(1) { /* main accept() loop */  
     sin_size = sizeof(struct sockaddr_in);  
     if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,   
     &sin_size)) == -1) {  
     perror("accept"); continue; }  
     printf("server: got connection from %s",inet_ntoa(their_addr.sin_addr));
     if (!fork()) { /* 子進程程式碼片段 */  
     if (send(new_fd, "Hello, world!", 14, 0) == -1)  
     perror("send"); close(new_fd); exit(0); }  
     close(new_fd); /* 父進程不再需要該socket */  
     waitpid(-1,NULL,WNOHANG) > 0 /*等待子進程結束,清除子進程所佔用資源*/  
     }  
     }  
     (程式1)  
     伺服器首先建立一個Socket,然後將該Socket與本地地址/連接埠號碼捆綁,成功之後就在相應的socket上監聽,當accpet捕捉到一個串連服務要求時,就產生一個新的socket,並通過這個新的socket向用戶端發送字串"Hello,world!",然後關閉該socket。  
  fork()函數產生一個子進程來處理資料轉送部分,fork()語句對於子進程返回的值為0。所以包含fork函數的if語句是子進程代碼部分,它與if語句後面的父進程代碼部分是並發執行的。  
  用戶端軟體代碼部分見程式2:  
     #includestdio.h  
     #include stdlib.h  
     #include errno.h  
     #include string.h  
     #include netdb.h  
     #include sys/types.h  
     #include netinet/in.h  
     #include sys/socket.h  
     #define PORT 3490  
     #define MAXDATASIZE 100 /*每次最大資料轉送量 */  
     int main(int argc, char *argv[])  
     {  
     int sockfd, numbytes;  
     char buf[MAXDATASIZE];  
     struct hostent *he;  
     struct sockaddr_in their_addr;  
     if (argc != 2) {  
     fprintf(stderr,"usage: client hostname"); exit(1); }  
     if((he=gethostbyname(argv[1]))==NULL) {  
     herror("gethostbyname"); exit(1); }  
     if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {  
     perror("socket"); exit(1); }  
     their_addr.sin_family=AF_INET;  
     their_addr.sin_port=htons(PORT);  
     their_addr.sin_addr = *((struct in_addr *)he->h_addr);  
     bzero(&(their_addr.sin_zero),8);  
     if (connect(sockfd, (struct sockaddr *)&their_addr,   
      sizeof(struct sockaddr)) == -1) {/*錯誤偵測*/  
     perror("connect"); exit(1); }  
     if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {  
     perror("recv"); exit(1); }  
     buf[numbytes] = '';  
     printf("Received: %s",buf);  
     close(sockfd);  
     return 0;  
     }  
     (程式2)  
  用戶端代碼相對來說要簡單一些,首先通過伺服器網域名稱獲得其IP地址,然後建立一個socket,調用connect函數與伺服器建立串連,串連成功之後接收從伺服器發送過來的資料,最後關閉socket,結束程式。  
 不需連線的客戶/伺服器程式的在原理上和串連的客戶/伺服器是一樣的,兩者的區別在於不需連線的客戶/伺服器中的客戶一般不需要建立串連,而且在發送接收資料時,需要指定遠端機的地址。  
  關於阻塞(blocking)的概念和select()函數當伺服器運行到accept語句時,而沒有客戶串連服務要求到來,那麼會發生什麼情況?這時伺服器就會停止在accept語句上等待串連服務要求的到來;同樣,當程式運行到接收資料語句時,如果沒有資料可以讀取,則程式同樣會停止在接收語句上。這種情況稱為blocking。但如果你希望伺服器僅僅注意檢查是否有客戶在等待串連,有就接受串連;否則就繼續做其他事情,則可以通過將Socke設定為非阻塞方式來實現:非阻塞socket在沒有客戶在等待時就使accept調用立即返回。  
     #include unistd.h  
     #include fcntl.h  
     . . . . ; sockfd = socket(AF_INET,SOCK_STREAM,0);  
     fcntl(sockfd,F_SETFL,O_NONBLOCK); . . . . .  
  通過設定socket為非阻塞方式,可以實現“輪詢”若干Socket。當企圖從一個沒有資料等待處理的非阻塞Socket讀入資料時,函數將立即返回,並且傳回值置為-1,並且errno置為EWOULDBLOCK。但是這種“輪詢”會使CPU處於忙等待方式,從而降低效能。考慮到這種情況,假設你希望伺服器監聽串連服務要求的同時從已經建立的串連讀取資料,你也許會想到用一個accept語句和多個recv()語句,但是由於accept及recv都是會阻塞的,所以這個想法顯然不會成功。  
  調用非阻塞的socket會大大地浪費系統資源。而調用select()會有效地解決這個問題,它允許你把進程本身掛起來,而同時使系統核心監聽所要求的一組檔案描述符的任何活動,只要確認在任何被監控的檔案描述符上出現活動,select()調用將返回指示該檔案描述符已準備好的資訊,從而實現了為進程選出隨機的變化,而不必由進程本身對輸入進行測試而浪費CPU開銷。Select函數原型為:  
 int select(int numfds,fd_set *readfds,fd_set *writefds,fd_set *exeptfds,struct timeval *timeout);  
  其中readfds、writefds、exceptfds分別是被select()監視的讀、寫和異常處理的檔案描述符集合。如果你希望確定是否可以從標準輸入和某個socket描述符讀取資料,你只需要將標準輸入的檔案描述符0和相應的sockdtfd加入到readfds集合中;numfds的值是需要檢查的號碼最高的檔案描述符加1,這個例子中numfds的值應為sockfd+1;當select返回時,readfds將被修改,指示某個檔案描述符已經準備被讀取,你可以通過FD_ISSSET()來測試。為了實現fd_set中對應的檔案描述符的設定、複位和測試,它提供了一組宏:  
     FD_ZERO(fd_set *set)----清除一個檔案描述符集;  
     FD_SET(int fd,fd_set *set)----將一個檔案描述符加入檔案描述符集中; 
     
     FD_CLR(int fd,fd_set *set)----將一個檔案描述符從檔案描述符集中清除 
   ;  
     FD_ISSET(int fd,fd_set *set)----試判斷是否檔案描述符被置位。  
     Timeout參數是一個指向struct timeval類型的指標,它可以使select()在等待timeout長時間後沒有檔案描述符準備好即返回。struct timeval資料結構為: 
     
     struct timeval {  
      int tv_sec; /* seconds */  
      int tv_usec; /* microseconds */  
     };  
     我們通過程式3來說明:  
     #include sys/time.h  
     #include sys/types.h  
     #include unistd.h  
     #define STDIN 0 /*標準輸入檔案描述符*/  
     main()  
     {  
      struct timeval tv;  
      fd_set readfds;  
      tv.tv_sec = 2;  
      tv.tv_usec = 500000;  
      FD_ZERO(&readfds);  
      FD_SET(STDIN,&readfds);  
      /* 這裡不關心寫檔案和異常處理檔案描述符集合 */  
      select(STDIN+1, &readfds, NULL, NULL, &tv);  
      if (FD_ISSET(STDIN, &readfds)) printf("A key was pressed!");  

      else printf("Timed out.");  
     }  
     (程式3)  
     select()在被監視連接埠等待2.5秒鐘以後,就從select返回

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.