天天看點

linux網絡程式設計

三元組(ip位址,協定,端口)就可以辨別網絡的程序

3.1,OSI七層模型和TCP/IP五層模型

OSI七層網絡模型由下至上為1至7層,分别為:

實體層(Physical layer),資料鍊路層(Data link layer),網絡層(Network layer),傳輸層(Transport layer),會話層(Session layer),表示層(Presentation layer),應用層(Application layer)。

1.1 應用層,很簡單,就是應用程式。這一層負責确定通信對象,并確定由足夠的資源用于通信,這些當然都是想要通信的應用程式幹的事情。 

1.2 表示層,負責資料的編碼、轉化,確定應用層的正常工作。這一層,是将我們看到的界面與二進制間互相轉化的地方,就是我們的語言與機器語言間的轉化。資料的壓縮、解壓,加密、解密都發生在這一層。這一層根據不同的應用目的将資料處理為不同的格式,表現出來就是我們看到的各種各樣的檔案擴充名。 

1.3 會話層,負責建立、維護、控制會話,區分不同的會話,以及提供單工(Simplex)、半雙工(Half duplex)、全雙工(Full duplex)三種通信模式的服務。我們平時所知的NFS,RPC,X Windows等都工作在這一層。 

1.4 傳輸層,負責分割、組合資料,實作端到端的邏輯連接配接。資料在上三層是整體的,到了這一層開始被分割,這一層分割後的資料被稱為段(Segment)。三次握手(Three-way handshake),面向連接配接(Connection-Oriented)或非面向連接配接(Connectionless-Oriented)的服務,流控(Flow control)等都發生在這一層。 

1.5 網絡層,負責管理網絡位址,定位裝置,決定路由。我們所熟知的IP位址和路由器就是工作在這一層。上層的資料段在這一層被分割,封裝後叫做包(Packet),包有兩種,一種叫做使用者資料包(Data packets),是上層傳下來的使用者資料;另一種叫路由更新包(Route update packets),是直接由路由器發出來的,用來和其他路由器進行路由資訊的交換。 

1.6 資料鍊路層,負責準備實體傳輸,CRC校驗,錯誤通知,網絡拓撲,流控等。我們所熟知的MAC位址和交換機都工作在這一層。上層傳下來的包在這一層被分割封裝後叫做幀(Frame)。 

1.7 實體層,就是實實在在的實體鍊路,負責将資料以比特流的方式發送、接收,就不多說了。 

3.2,TCP握手

3.2.1 三次握手建立連接配接

在TCP/IP協定中,TCP協定提供可靠的連接配接服務,采用三次握手建立一個連接配接。

1,用戶端向伺服器發送一個SYN J

2,伺服器向用戶端響應一個SYN K,并對SYN J進行确認ACK J+1

3,用戶端再想伺服器發一個确認ACK K+1

linux網絡程式設計

從圖中可以看出,當用戶端調用connect時,觸發了連接配接請求,向伺服器發送了SYN J包,這時connect進入阻塞狀态;伺服器監聽到連接配接請求,即收到SYN J包,調用accept函數接收請求向用戶端發送SYN K ,ACK J+1,這時accept進入阻塞狀态;用戶端收到伺服器的SYN K ,ACK J+1之後,這時connect傳回,并對SYN K進行确認;伺服器收到ACK K+1時,accept傳回,至此三次握手完畢,連接配接建立。

三次握手的目的是建立雙向的連接配接,第一次握手是用戶端向伺服器端送出請求 

第二次握手是伺服器端告訴用戶端,第一次握手是成功的,即可以從用戶端發送到用戶端, 

第三次握手是用戶端告訴伺服器端,第二次握手是成功的,即可以從用戶端到伺服器端 

這樣就保證了用戶端和伺服器端的雙向通信,

3.2.2 四次握手釋放連接配接

linux網絡程式設計

1,某個應用程序首先調用close主動關閉連接配接,這時TCP發送一個FIN M;

2,另一端接收到FIN M之後,執行被動關閉,對這個FIN進行确認。它的接收也作為檔案結束符傳遞給應用程序,因為FIN的接收意味着應用程序在相應的連接配接上再也接收不到額外資料;

3,一段時間之後,接收到檔案結束符的應用程序調用close關閉它的socket。這導緻它的TCP也發送一個FIN N;

4,接收到這個FIN的源發送端TCP對它進行确認。

3.3,socket程式設計

javascript:void(0)

先從伺服器端說起。伺服器端先初始化Socket,然後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待用戶端連接配接。在這時如果有個用戶端初始化一個Socket,然後連接配接伺服器(connect),如果連接配接成功,這時用戶端與伺服器端的連接配接就建立了。用戶端發送資料請求,伺服器端接收請求并處理請求,然後把回應資料發送給用戶端,用戶端讀取資料,最後關閉連接配接,一次互動結束。

3.3.1 AF_INET TCP傳輸最簡單版本

linux網絡程式設計

tcp_client.c

[cpp] view plain copy

  1. #include <unistd.h>  
  2. #include <stdio.h>  
  3. #include <stdlib.h>  
  4. #include <string.h>  
  5. #include <errno.h>  
  6. #include <sys/types.h>  
  7. #include <sys/socket.h>  
  8. #include <netinet/in.h>  
  9. #define PORT 6666  
  10. #define BUFF_SIZE 1024  
  11. #define MAXLEN 4096  
  12. main(int argc, char** argv)  
  13. {  
  14.     if(argc!=2){  
  15.         printf("usage: ./client <ipaddress>\n");  
  16.         exit(0);  
  17.     }  
  18.     char sendline[4096];  
  19.     //socket()建立socket  
  20.     int sockfd = socket(AF_INET,SOCK_STREAM,0);  
  21.     //要連接配接的伺服器位址  
  22.     struct sockaddr_in sliaddr;  
  23.     sliaddr.sin_family = AF_INET;  
  24.     sliaddr.sin_port = htons(PORT);  
  25.     inet_pton(AF_INET, argv[1], &sliaddr.sin_addr);  
  26.     //connect()發送請求(ip=argv[1],protocal=TCP,port=6666)  
  27.     connect(sockfd, (struct sockaddr*)&sliaddr, sizeof(sliaddr));  
  28.     //recv sth  
  29.     recv_len = recv(sockfd, buff, sizeof(buff), 0);  
  30.     buff[recv_len] = '\0';  
  31.     printf(" %s ", buff);  
  32.     //interactive  
  33.     while (1)  
  34.     {  
  35.         printf("Enter string to send: ");  
  36.         scanf("%s", buff);  
  37.         if (!strcmp(buff, "quit"))  
  38.             break;  
  39.         send_len = send(sockfd, buff, strlen(buff), 0);  
  40.         recv_len = recv(sockfd, buff, BUFF_SIZE, 0);  
  41.         buff[recv_len] = '\0';  
  42.         printf("    received: %s \n", buff);  
  43.     //close()關閉連接配接  
  44.     close(sockfd);  
  45. }  

tcp_server.c

  1. #define WAIT_QUEUE_LEN 5  
  2. #define WELCOME "Welcome to my server ^_^!\n"  
  3. main()  
  4.     int connfd;  
  5.     char buff[MAXLEN];  
  6.     int len;  
  7.     //socket() 建立socket,其中SOCK_STREAM表示tcp連接配接  
  8.     struct sockaddr_in servaddr;  
  9.     servaddr.sin_family = AF_INET;  
  10.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  11.     servaddr.sin_port = htons(PORT);  
  12.     //bind()綁定一個socket(ip=all,protocal=TCP,port=6666)  
  13.     bind(sockfd,(struct sockaddr *) &servaddr, sizeof(servaddr));  
  14.     //listen()監聽  
  15.     listen(sockfd, WAIT_QUEUE_LEN);  
  16.     //accept() & close()  
  17.     printf("======waiting for client's request======\n");  
  18.     while(1){  
  19.         c_addrlen = sizeof(struct sockaddr_in);  
  20.         connfd = accept(serverfd, (struct sockaddr *)&caddr, &c_addrlen);  
  21.         printf("client: ip=%s,port=%s\n", cliaddr.sin_addr.s_addr,cliaddr.sin_port);  
  22.         //send a welcome  
  23.         send(connfd, WELCOME, strlen(WELCOME), 0);  
  24.         //阻塞模式下recv==0表示用戶端已斷開連接配接  
  25.         while ((len = recv(connfd, buff, BUFF_SIZE, 0)) > 0)  
  26.         {  
  27.             buff[len] = '\0';  
  28.             printf("recv msg is : %s \n", buff);  
  29.             send(connfd, buff, len, 0);  
  30.         }  
  31.         close(connfd);  

阻塞與非阻塞recv傳回值沒有區分,都是

<0 出錯

=0 連接配接關閉

>0 接收到資料大小,

makefile

[plain] view plain copy

  1. .PHONY : main  
  2. main : server client  
  3. server : server.o  
  4.         gcc -g -o server server.o   
  5. client : client.o  
  6.         gcc -g -o client client.o   
  7. server.o : server.c  
  8.         gcc -g -c server.c  
  9. client.o : client.c  
  10.         gcc -g -c client.c  
  11. clean :   
  12.         rm -rf *.o  
  13. ser :  
  14.         ./server  
  15. cli :  
  16.         ./client  

3.3.2 加入傳回值檢查和IP位址

  1.     int sockfd;  
  2.     if((sockfd=socket(AF_INET,SOCK_STREAM,0)) ==-1){  
  3.         printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);  
  4.     struct sockaddr_in cliaddr;  
  5.     cliaddr.sin_family = AF_INET;  
  6.     cliaddr.sin_port = htons(6666);  
  7.     if(inet_pton(AF_INET, argv[1], &cliaddr.sin_addr)==-1){  
  8.         printf("inet_pton error for %s\n",argv[1]);  
  9.     if(connect(sockfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr))==-1){  
  10.         printf("connect error: %s(errno: %d)\n",strerror(errno),errno);  
  11.     printf("send msg to server: \n");  
  12.     fgets(sendline, 4096, stdin);  
  13.     //send()發送資料  
  14.     if(send(sockfd, sendline, strlen(sendline), 0)==-1){  
  15.         printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);  
  1.     int n;  
  2.     if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){  
  3.         printf("create socket error: %s(errno:%d)\n",strerror(errno),errno);  
  4.     servaddr.sin_port = htons(6666);  
  5.     if(bind(sockfd,(struct sockaddr *) &servaddr, sizeof(servaddr))==-1){  
  6.         printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);  
  7.     if(listen(sockfd,10)==-1){  
  8.         printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);  
  9.         if((connfd=accept(sockfd, (struct sockaddr *)NULL,NULL))==-1){  
  10.             printf("accept socket error: %s(errno: %d)",strerror(errno),errno);  
  11.             continue;  
  12.         struct sockaddr_in serv, guest;  
  13.         char serv_ip[20];  
  14.         char guest_ip[20];  
  15.         int serv_len = sizeof(serv);  
  16.         int guest_len = sizeof(guest);  
  17.         getsockname(connfd, (struct sockaddr *)&serv, &serv_len);  
  18.         getpeername(connfd, (struct sockaddr *)&guest, &guest_len);  
  19.         inet_ntop(AF_INET, &serv.sin_addr, serv_ip, sizeof(serv_ip));  
  20.         inet_ntop(AF_INET, &guest.sin_addr, guest_ip, sizeof(guest_ip));  
  21.         printf("host %s:%d guest %s:%d\n", serv_ip, ntohs(serv.sin_port), guest_ip, ntohs(guest.sin_port));  
  22.         n = recv(connfd, buff, MAXLEN,0);  
  23.         buff[n] = '\0';  
  24.         printf("recv msg from client: %s\n", buff);  

3.3.3 AF_INET UDP傳輸最簡單版本

linux網絡程式設計

udp_client.c

  1. /** 
  2. *   @file: udpclient.c 
  3. *   @brief: A simple Udp server 
  4. *   @author: ToakMa <[email protected]
  5. *   @date:  2014/10/09 
  6. */  
  7. #include <strings.h>  
  8. #include <arpa/inet.h>  
  9. #define PORT     9988  
  10. int main(int argc, char *argv[])  
  11.     struct sockaddr_in remote_addr;  
  12.     char buff[BUFF_SIZE];  
  13.     //1. create a socket  
  14.     sockfd = socket(AF_INET, SOCK_DGRAM, 0);  
  15.     if (-1 == sockfd)  
  16.         perror("udp client socket: ");  
  17.         return -1;  
  18.     //2. prepare ip and port  
  19.     memset(&remote_addr, 0, sizeof(remote_addr));  
  20.     remote_addr.sin_family = AF_INET;  
  21.     remote_addr.sin_port   = htons(PORT);  
  22.     remote_addr.sin_addr.s_addr = inet_addr(argv[1]);  
  23.     bzero(&(remote_addr.sin_zero), 8);  
  24.     //3. sendto  
  25.     strcpy(buff, "this a test\n");  
  26.     printf("sending : %s\n", buff);  
  27.     len = sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr *)&remote_addr, sizeof(remote_addr));  
  28.     if (len < 0)  
  29.         perror("udp client sendto :");  
  30.     //4. close  
  31.     return 0;  

udp_server.c

  1. *   @file: udpserver.c 
  2. #define PORT 9988  
  3.     int sin_len;  
  4.     struct sockaddr_in saddr;  
  5.     char buff[BUFF_SIZE];     
  6.     int res, len;  
  7.     //1. create socket  
  8.         perror("Udp server socket: ");  
  9.     printf("Udp server socket create succ!\n");  
  10.     //2. prepare IP and port  
  11.     memset(&saddr, 0, sizeof(saddr));  
  12.     saddr.sin_family = AF_INET;  
  13.     saddr.sin_port   = htons(PORT);  
  14.     saddr.sin_addr.s_addr = INADDR_ANY;  
  15.     bzero(saddr.sin_zero, 8);  
  16.     //3. bind  
  17.     res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));  
  18.     if (-1 == res)  
  19.         perror("udp server bind: ");  
  20.     //4. recvfrom  
  21.     printf("Wait for a packet ...\n");  
  22.     sin_len = sizeof(struct sockaddr_in);  
  23.     len = recvfrom(sockfd, buff, BUFF_SIZE, 0, (struct sockaddr *)&remote_addr, &sin_len);  
  24.     if (-1 == len)  
  25.         perror("udp server recvform: ");  
  26.     buff[len] = '\0';  
  27.     printf("Recived packet from %s, contents is: %s \n", \  
  28.         inet_ntoa(remote_addr.sin_addr), buff);  
  29.     //5. close  

3.3.4 AF_INET UNIX本地傳輸最簡單版本

unix_client.c

  1. #include <sys/un.h>  
  2. #include <errno.h>   
  3. int main()  
  4.     int result;  
  5.     char ch = 'A';  
  6.     //建立socket  
  7.     if((sockfd = socket(AF_UNIX,SOCK_STREAM,0) == -1){  
  8.     struct sockaddr_un address;  
  9.     address.sun_family = AF_UNIX;  
  10.     strcpy(address.sun_path,"server_socket");  
  11.     int len = sizeof(address);  
  12.     //connect()  
  13.     if((result = connect(socket,(struct sockaddr*)&address,len)==-1){  
  14.         printf("connect error: %s(errno: %d)\n",strerror(errno),errno);    
  15.         exit(0);    
  16.     //read & write  
  17.     write(sockfd,&ch,1);  
  18.     read(sockfd,&ch,1);  
  19.     printf("char from server = %c\n",ch);  
  20.     //close()  
  21.     exit(0);  

unix_server.c

  1.     int server_len,client_len;  
  2.     struct socket_un server_address;  
  3.     struct socket_un client_address;  
  4.     //删除以前套接字  
  5.     unlink("server_socket");  
  6.     //socket  
  7.     int server_sockfd = socket(AF_UNIX,SOCK_STREAM,0);  
  8.     server_address.sun_family = AF_UNIX;  
  9.     strcpy(server_address.sun_path,"server_socket");  
  10.     server_len = sizeof(server_address);  
  11.     //bind()  
  12.     bind(server_sockfd,(struct sockaddr*)&server_address,server_len);  
  13.     //listen()  
  14.     listen(server_sockfd,5);  
  15.         char ch;  
  16.         printf("server waiting\n");  
  17.         client_len = sizeof(client_address);  
  18.         client_sockfd = accept(server_sockfd,(struct sockaddr*)&client_address,&client_len);  
  19.         //read & write  
  20.         read(client_sockfd,&ch,1);  
  21.         ch++;  
  22.         write(client_sockfd,&ch,1);  
  23.         close(client_sockfd);  

3.4 網絡程式設計函數

Unix/Linux基本哲學之一就是“一切皆檔案”,都可以用“打開open –> 讀寫write/read –> 關閉close”

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

3.4.1 函數socket

建立套接字:socket函數對應于普通檔案的打開操作。普通檔案的打開操作傳回一個檔案描述字,而socket()用于建立一個socket描述符(socket descriptor),它唯一辨別一個socket。

  1. 函數原型:  
  2. int socket(int domain, int type, int protocol);  
  3. 參數說明:  
  4. domain:即協定域。常用的協定族有,AF_INET(ipv4)、AF_INET6(ipv6)、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。  
  5. type:指定socket類型。常用的socket類型有,SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。  
  6. protocol:指定協定。常用的協定有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等。  
  7. int:傳回值為-1表示出錯  

注意:并不是上面的type和protocol可以随意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol為0時,會自動選擇type類型對應的預設協定。

3.4.2 函數bind

命名套接字:bind()函數把一個位址族中的特定位址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6位址和端口号組合賦給socket。

  1. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  
  2. sockfd:是由socket()調用傳回的套接口檔案描述符。  
  3. addr:傳入資料結構sockaddr的指針,包括(IP,protocol,port)需要轉換為通用位址類型struct sockaddr*  
  4. addrlen:以設定成sizeof(struct sockaddr)  
  5. int:傳回值,-1表示出錯  
  1. ipv4位址結構【AF_INET】:  
  2. struct sockaddr_in {  
  3.     sa_family_t    sin_family; /* address family: AF_INET */  
  4.     in_port_t      sin_port;   /* port in network byte order */  
  5.     struct in_addr sin_addr;   /* internet address */  
  6. };  
  7. /* Internet address. */  
  8. struct in_addr {  
  9.     uint32_t       s_addr;     /* address in network byte order */  

ipv6位址結構【AF_INET6】:

  1. struct sockaddr_in6 {   
  2.     sa_family_t     sin6_family;   /* AF_INET6 */   
  3.     in_port_t       sin6_port;     /* port number */   
  4.     uint32_t        sin6_flowinfo; /* IPv6 flow information */   
  5.     struct in6_addr sin6_addr;     /* IPv6 address */   
  6.     uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */   
  7. struct in6_addr {   
  8.     unsigned char   s6_addr[16];   /* IPv6 address */   

Unix域位址結構【AF_UNIX】:

  1. #define UNIX_PATH_MAX    108  
  2. struct sockaddr_un {   
  3.     sa_family_t sun_family;               /* AF_UNIX */   
  4.     char        sun_path[UNIX_PATH_MAX];  /* pathname */ 使用strcpy(address.sun_path,"server_socket")   

通常伺服器在啟動的時候都會綁定一個衆所周知的位址(如ip位址+端口号),用于提供服務,客戶就可以通過它來接連伺服器;是以伺服器端在listen之前會調用bind()。

而用戶端就不用指定,有系統自動配置設定一個端口号和自身的ip位址組合,在connect()時由系統随機生成一個。由于用戶端不需要固定的端口号,是以不必調用bind(),用戶端的端口号由核心自動配置設定。注意,用戶端不是不允許調用bind(),隻是沒有必要調用

bind()固定一個端口号,伺服器也不是必須調用bind(),但如果伺服器不調用bind(),核心會自動給伺服器配置設定監聽端口,每次啟動伺服器時端口号都不一樣,用戶端要連接配接伺服器就會遇到麻煩。

對server端的addr的初始化如下所示:

  1. memset(&servaddr, 0, sizeof(servaddr));  
  2. servaddr.sin_family = AF_INET;  
  3. servaddr.sin_port = htons(5188);  
  4. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  

首先将整個結構體清零(也可以用bzero函數),然後設定位址類型為AF_INET,網絡位址為INADDR_ANY,這個宏表示本地的任意IP位址,因為伺服器可能有多個網卡,每個網卡也可能綁定多個IP位址,這樣設定可以在所有的IP位址上監聽,直到與某個用戶端建立了連接配接時才确定下來到底用哪個IP位址,端口号為5188。

3.4.3 函數listen

建立套接字隊列

  1. int listen(int sockfd, int backlog);  
  2. sockfd:即為要監聽的socket描述字  
  3. backlog:相應socket可以排隊的最大連接配接個數  

第二個參數是進入隊列中允許的連接配接的個數。進入的連接配接請求在使用系統調用accept()應答之前要在進入隊列中等待。這個值是隊列中最多可以擁有的請求的個數。大多數系統的預設設定為20。你可以設定為5或者10。當出錯時,listen()将會傳回-1值。

這個函數對于backlog的解釋《unix網絡程式設計》P85是說已完成連接配接隊列(ESTABLISHED)和未完成連接配接隊列(SYN_RCVD)之和(即等待連接配接數而非連接配接數)。伺服器調用的accept()傳回并接受這個連接配接,如果有大量的用戶端發起連接配接而伺服器來不及處理,尚未accept的用戶端就處于連接配接等待狀态,listen()聲明sockfd處于監聽狀态,并且最多允許有backlog個用戶端處于連接配接等待狀态,如果接收到更多的連接配接請求就忽略。在計算機早期由于網絡連接配接的處理速度很慢,幾個并發的請求就可能導緻系統處理不過來而引發錯誤,現在這個數字的意義已經不大了,現在軟硬體性能幾乎能保證這個隊列不可能滿。連接配接的狀态變為ESTABLISHED之後(實際是連接配接由從半連接配接隊列轉移到了完成握手的完成隊列)才表示可以被accpet()處理。

3.4.4 函數accept

接受連接配接

  1. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);  
  2. addr:傳出連接配接客戶的sockaddr指針,包括(IP,protocol,port)  
  3. addrlen:傳入指定客戶結構addr的長度,傳回時該值傳出為連接配接客戶位址addr的實際長度  

調用accept()之後,将會傳回一個全新的套接口檔案描述符來處理這個單個的連接配接。這樣,對于同一個連接配接來說,你就有了兩個檔案描述符。原先的一個檔案描述符正在監聽你指定的端口,新的檔案描述符可以用來調用send()和recv()。

可以通過對套接字檔案描述符設定O_NONBLOCK來改變其是否阻塞:

  1. int flags = fcntl(socket, F_GETFL,0);  
  2. fcntl(socket, F_SETFL, O_NONBLOCK| flags);  

3.4.5 函數connect

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

用戶端連接配接伺服器,addr為傳入參數,表示目的伺服器的(ip,protocal,port)。

3.4.6 函數read與write

read()/write()

recv()/send()

readv()/writev()

recvmsg()/sendmsg()

recvfrom()/sendto()

  1. ssize_t read(int fd, void *buf, size_t count);  
  2. ssize_t write(int fd, const void *buf, size_t count);  
  3. ssize_t send(int sockfd, const void *buf, size_t len, int flags);  
  4. ssize_t recv(int sockfd, void *buf, size_t len, int flags);  
  5. ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,  
  6.                const struct sockaddr *dest_addr, socklen_t addrlen);  
  7. ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,  
  8.                struct sockaddr *src_addr, socklen_t *addrlen);  
  9. ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);  
  10. ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);  

3.4.7 函數close

  1. int close(int fd);  

注意:close操作隻是使相應socket描述字的引用計數-1,隻有當引用計數為0的時候,才會觸發TCP用戶端向伺服器發送終止連接配接請求。

3.4.8 函數getsockname

函數傳回與套接口關聯的本地協定位址。

  1. int getsockname(int sockfd, struct sockaddr * localaddr, socken_t * addrlen);  

3.4.9 函數getpeername

函數傳回與套接口關聯的遠端協定位址。

  1. int getpeername(int sockfd,struct sockaddr* peeraddr,int* addrlen);  
  2. peeraddr:傳出資料結構sockaddr的指針,包括(IP,protocol,port)  
  3. addrlen:傳出結構大小的指針  

3.5 網絡位元組序與主機位元組序轉換

将主機位元組序轉換為網絡位元組序(避免大端小端問題)

#include <netinet/in.h>

3.5.1 函數htonl

  1. uint32_t htonl(uint32_t hostlong);  

3.5.2 函數htons

  1. uint16_t htons(uint16_t hostshort);  

3.5.3 函數ntohl

  1. uint32_t ntohl(uint32_t netlong);  

3.5.4 函數ntohs

  1. uint16_t ntohs(uint16_t netshort);  

3.6 IP位址與主機位元組序轉換

3.6.1 函數inet_pton

[将“點分十進制” -> “整數”]

  1. int inet_pton(int af, const char *src, void *dst);  
  2. af:位址族,AF_INET為ipv4位址,AF_INET6為ipv6位址  
  3. src:為ip位址(ipv4例如1.1.1.1)  
  4. dst:函數将該位址轉換為in_addr的結構體,并複制在*dst中  
  5. int:傳回值:如果函數出錯将傳回一個負值,并将errno設定為EAFNOSUPPORT,如果參數af指定的位址族和src格式不對,函數将傳回0。  

3.6.2 函數inet_ntop

[将“整數” -> “點分十進制”]

  1. const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);  

3.7 主機資料庫函數

3.7.1 函數gethostname

它傳回(本地)計算機的名字,存儲在hostname中,大小為size

  1. int gethostname(char*hostname,size_tsize);  
  2. int:傳回值gethostname将傳回0。如果失敗,它将傳回-1。   

3.7.2 函數gethostbyname

根據域名或者主機名擷取資訊

  1. struct hostent *gethostbyname(const char *name);  
  2. name:主機名,如"www.baidu.com"  
  3. hostend:傳回值。如果函數調用失敗,将傳回NULL。  

hostend的結構如下:

  1. struct hostent   
  2.   char  *h_name;            //表示的是主機的規範名,例如www.google.com的規範名其實是www.l.google.com  
  3.   char  **h_aliases;        //表示的是主機的别名  
  4.   int   h_addrtype;         //IP位址的類型  
  5.   int   h_length;           //IP位址的長度  
  6.   char  **h_addr_list;      //主機的ip位址,注意這是以網絡位元組順序儲存的一個值,用inet_ntop恢複  

示例代碼:

  1. #include <netdb.h>  
  2. int main(int argc, char **argv)  
  3.     char *host,**names;  
  4.     struct hostent *hostinfo;  
  5.     cha str[32];  
  6.     /* 取得指令後第一個參數,即要解析的域名或主機名 */  
  7.     if(argc==1){  
  8.         char myname[256];  
  9.         gethostname(myname,255);  
  10.         host=myname;  
  11.     else{  
  12.         host=argv[1];  
  13.     /* 調用gethostbyname()。調用結果都存在hostinfo中 */  
  14.     if( (hostinfo = gethostbyname(host) ) == NULL )  
  15.         printf("gethostbyname error for host:%s/n", host);  
  16.         exit(1); /* 如果調用gethostbyname發生錯誤,傳回1 */  
  17.     /* 将主機的規範名打出來 */  
  18.     printf("official hostname:%s/n",hostinfo->h_name);  
  19.     /* 主機可能有多個别名,将所有别名分别打出來 */  
  20.     for(names = hostinfo->h_aliases; *names != NULL; names++)  
  21.         printf(" alias:%s/n",*names);  
  22.     /* 根據位址類型,将位址打出來 */  
  23.     switch(hostinfo->h_addrtype)  
  24.     case AF_INET:  
  25.     case AF_INET6:  
  26.         names=hostinfo->h_addr_list;  
  27.         /* 将剛才得到的所有位址都打出來。其中調用了inet_ntop()函數 */  
  28.         for(;*names!=NULL;names++)  
  29.             printf(" address:%s/n", inet_ntop(hostinfo->h_addrtype, *names, str, sizeof(str)));  
  30.         break;  
  31.     default:  
  32.         printf("unknown address type/n");  
  33. }   

3.7.3 函數gethostbyaddr

根據ip位址(網絡位元組序)擷取資訊

  1. struct hostent *gethostbyaddr(const char *name,int len,int type)  
  2. name:ip位址,例如: inet_addr("192.168.4.111")  
  3. len:  
  4. type:  

3.7.4 函數getservbyname

用于根據給定的名字來查找相應的伺服器,傳回對應于給定服務名和協定名的相關服務資訊

  1. struct servernt *gerservbyname(const char *servname,const char *protoname)  
  2. servname:例如smtp  
  3. protoname:例如tcp  

servent結構如下:

  1. struct servent{  
  2.     char *s_name;    /*服務的正規名字*/  
  3.     char **s_aliases;/*别名清單*/  
  4.     int s_port;      /*服務的端口号*/  
  5.     char *s_proto;   /*服務的協定,如tcp或udp*/  

3.7.5 函數getservbyport

傳回與給定服務名對應的包含名字和服務号資訊的servent結構指針(注意參數port的值必須是網絡位元組序類型的,是以在使用的時候需要使用函數htons(port)來進行轉換)。

  1. struct servent *getservbyport(int port,const char *proroname);  
  2. 函數示例:  
  3. sptr=getservbyport(htons(53),"udp");  
  1. #擷取任意已知主機的日期和時間  
  2.     char *host;  
  3.     struct hostent * hostinfo;  
  4.     struct servent * servinfo;  
  5.         host="localhost";  
  6.     if ( (servinfo = getservbyname("daytime","tcp")) );  
  7.         printf("no daytime service/n");  
  8.         exit(1); /* 如果調用getservbyname發生錯誤,傳回1 */  
  9.     /* 将daytime的端口列印出來*/  
  10.     printf("daytime port is %d/n",ntohs(servinfo -> s_port));  
  11.     int sockfd = socket(AF_INET, SOCK_STREAM,0);  
  12.     struct sockaddr_in address;  
  13.     address.sin_family = AF_INET;  
  14.     address.sin_port = servinfo -> s_port;  
  15.     address.sin_addr= *(struct in_addr*)*hostinfo->h_addr_list;  
  16.     if((result = connect(socket,(struct sockaddr*)&address,len))==-1){  
  17.         exit(0);   
  18.     char buffer[128];  
  19.     result = read(sockfd,buffer,sizeof(buffer));  
  20.     buffer[result]='\0';  
  21.     printf("read %d bytes: %s",result,buffer);  

3.5 多客戶程式設計

3.5.1 阻塞與非阻塞

(1)阻塞block

    所謂阻塞方式block,顧名思義,就是程序或是線程執行到這些函數時必須等待某個事件的發生,如果事件沒有發生,程序或線程就被阻塞(程序Sleep),函數不能立即傳回。

    例如socket程式設計中connect、accept、recv、recvfrom這樣的阻塞程式。

    再如絕大多數的函數調用、語句執行,嚴格來說,他們都是以阻塞方式執行的。

(2)非阻塞non-block

    所謂非阻塞方式non-block,就是程序或線程執行此函數時不必非要等待事件的發生,一旦執行肯定傳回,以傳回值的不同來反映函數的執行情況,如果事件發生則與阻塞方式相同,若事件沒有發生則傳回一個代碼來告知事件未發生,而程序或線程繼續執行,是以效率較高。

   非阻塞I/O有一個缺點,如果所有裝置都一直沒有資料到達,調用者需要反複查詢做無用功,如果阻塞在那裡,作業系統可以排程别的程序執行,就不會做無用功了。?????

3.5.2 函數select

  1. int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);    
  2. nfds:為檔案描述符集合中的最大值+1,避免函數檢查fd_set的所有1024位  
  3. readfds:被監控是否可以讀   
  4. writefds:被監控是否可以寫   
  5. exceptfds:被監控是否發生異常   
  6. timeout:逾時時間,Linux傳回時會被修改成剩餘的時間。  
  7. int:傳回值,傳回狀态發生變化的描述符的總數,錯誤傳回-1,逾時傳回0。  

關于timeout:

  1. struct timeval結構如下:  
  2. struct timeval  
  3.        long tv_sec;  //seconds  
  4.        long tv_usec; //microseconds  

1,若timeout設定為t>0,表示等待固定時間,有一個fd位被置為1或者時間耗盡,函數均傳回。

2,若timeout設定為t=0,表示非阻塞,函數檢查完每個fd後立即傳回。

3,若timeout設定為t="NULL",表示阻塞,直到有一個fd位被置為1函數才傳回。

  1. 相關操作:    
  2. FD_ZERO(fd_set *set);         //fd_set很多系統實作為bit arrays,将所有的檔案描述符從fd_set中清空    
  3. FD_CLR(int fd, fd_set *set);  //從set中清除fd      
  4. FD_SET(int fd, fd_set *set);  //将fd添加到set中     
  5. FD_ISSET(int fd, fd_set *set);//判斷描述符fd是否在給定的描述符集set中,通常配合select函數使用,由于select函數成功傳回時會将未準備好的描述符位清零。通常我們使用FD_ISSET是為了檢查在select函數傳回後,某個描述符是否準備好,以便進行接下來的處理操作。       

常用代碼:

  1. fd_set  rdfds;  
  2. struct timeval tv;  
  3. tv.tv_sec = 1;  
  4. tv.tv_uses = 500;  
  5. int ret;  
  6. FD_ZERO(&rdfds);  
  7. FD_SET(socket, &rdfds);  
  8. ret = select (socket + 1, %rdfds, NULL, NULL, &tv);  
  9. if(ret < 0)   
  10.  perror ("select");  
  11. else if (ret == 0)   
  12.  printf("time out");  
  13. else {  
  14.        printf(“ret = %d/n”,ret);  
  15.        if(FD_ISSET(socket, &rdfds)){  
  16.        /* 讀取socket句柄裡的資料 */  
  17.        recv( );  
  18.        }  

3.5.3 函數pselect

  1. #include <sys/select.h>  
  2. int pselect(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds,fd_set *restrict exceptfds, const struct timespec *restrict tsptr,const sigset_t *restrict sigmask);  
  3. 參數說明  
  4. 傳回值:Returns: count of ready descriptors, 0 on timeout, -1 on error  

它與select的差別在于:

1,pselect使用timespec結構指定逾時值。timespec結構以秒和納秒表示時間,而非秒和微秒。

2,pselect的逾時值被聲明為const,這保證了調用pselect不會改變timespec結構。

3,pselect可使用一個可選擇的信号屏蔽字。在調用pselect時,以原子操作的方式安裝該信号屏蔽字,在傳回時恢複以前的信号屏蔽字。

3.5.4 函數poll

  1. #include <sys/poll.h>  
  2. int poll (struct pollfd *fds, unsigned int nfds, int timeout);  
  3. timeout:【值=INFTIM】表示永遠等待【值=0】表示立即傳回,不阻塞程序【值>0】等待指定數目的毫秒數。  

pollfd結構說明:

  1. struct pollfd {  
  2.      int fd; /* 檔案描述符 */  
  3.      short events; /* 等待的事件 */  
  4.      short revents; /* 發生的事件 */  
  5. poll函數可用的測試值:  
  6. 常量  說明  
  7. POLLIN  普通或優先級帶資料可讀  
  8. POLLRDNORM  普通資料可讀  
  9. POLLRDBAND  優先級帶資料可讀  
  10. POLLPRI 高優先級資料可讀  
  11. POLLOUT 普通資料可寫  
  12. POLLWRNORM  普通資料可寫  
  13. POLLWRBAND  優先級帶資料可寫  
  14. POLLERR 發生錯誤  
  15. POLLHUP 發生挂起  
  16. POLLNVAL    描述字不是一個打開的檔案  
  17. 注意:後三個隻能作為描述字的傳回結果存儲在revents中,而不能作為測試條件用于events中。  

3.5.5 函數epoll

一共有三個函數:

  1. int epoll_create(int size);  

建立一個epoll的句柄,size用來告訴核心這個監聽的數目一共有多大。這個參數不同于select()中的第一個參數,給出最大監聽的fd+1的值。需要注意的是,當建立好epoll句柄後,它就是會占用一個fd值,在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,是以在使用完epoll後,必須調用close()關閉,否則可能導緻fd被耗盡。

  1. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  

epoll的事件注冊函數,它不同與select()是在監聽事件時告訴核心要監聽什麼類型的事件,而是在這裡先注冊要監聽的事件類型。第一個參數是epoll_create()的傳回值,第二個參數表示動作,用三個宏來表示:

EPOLL_CTL_ADD:注冊新的fd到epfd中;

EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;

EPOLL_CTL_DEL:從epfd中删除一個fd;

第三個參數是需要監聽的fd,第四個參數是告訴核心需要監聽什麼事。

struct epoll_event結構如下:

  1. struct epoll_event {  
  2.   __uint32_t events;  /* Epoll events */  
  3.   epoll_data_t data;  /* User data variable */  

events可以是以下幾個宏的集合:

EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);

EPOLLOUT:表示對應的檔案描述符可以寫;

EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);

EPOLLERR:表示對應的檔案描述符發生錯誤;

EPOLLHUP:表示對應的檔案描述符被挂斷;

EPOLLET: 将EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水準觸發(Level Triggered)來說的。

EPOLLONESHOT:隻監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裡

  1. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);  

等待事件的産生,類似于select()調用。參數events用來從核心得到事件的集合,maxevents告之核心這個events有多大,這個 maxevents的值不能大于建立epoll_create()時的size,參數timeout是逾時時間(毫秒,0會立即傳回,-1将不确定,也有 說法說是永久阻塞)。該函數傳回需要處理的事件數目,如傳回0表示已逾時。

在前面講過的客戶/伺服器程式中,伺服器隻能處理一個用戶端的請求,如何同時服務多個用戶端呢?在未講到select/poll/epoll等進階IO之前,比較老土的辦法是使用fork來實作。網絡伺服器通常用fork來同時服務多個用戶端,父程序專門負責監聽端口,每次accept一個新的用戶端連接配接就fork出一個子程序專門服務這個用戶端。但是子程序退出時會産生僵屍程序,父程序要注意處理SIGCHLD信号和調用wait清理僵屍程序,最簡單的辦法就是直接忽略SIGCHLD信号。

繼續閱讀