1. 程式人生 > >tcp客服端伺服器模型

tcp客服端伺服器模型

本文講述了TCP套接字程式設計模組,包括伺服器端的建立套接字、繫結、監聽、接受、讀/寫、終止連線,客戶端的建立套接字、連線、讀/寫、終止連線。先給出例項,進而結合程式碼分析。


PS:本文權當複習套接字程式設計的讀書筆記。


一、TCP套接字程式設計模型

    同一臺計算機上執行的程序可以利用管道、訊息佇列、訊號量、共享記憶體等進行相互通訊,不同計算機上執行的程序可以通過套接字網路IPC介面進行相互通訊。套接字程式設計基本步驟如下圖所示:

圖 TCP套接字程式設計模型[1]

二、原始碼

    

本例項旨在實現簡單的echo服務,客戶端傳送資料給服務端,在服務端打印出來並且回發給客戶端,並在客戶端顯示。

TCP_socket_programming_example原始檔 TCP_socket_programming_example.rar   

2.1 TCP服務端

  1. //filename:TCPserver.c
  2. #include <stdio.h>
  3. #include <errno.h>
  4. #include <sys/socket.
    h>
  5. #include <netinet/in.h>

  6. #define BACKLOG 10
  7. #define BUFFER_SIZE 1024

  8. int main(int argc, char *argv[])
  9. {
  10.   if(!= argc)
  11.   {
  12.     printf("Usage:%s portnumber\n", argv[
    0]);
  13.     return - 1;
  14.   }

  15.   /***1.create a socket***/
  16.   int fd_server = socket(AF_INET, SOCK_STREAM, 0); //TCP
  17.   if( - 1 == fd_server)
  18.   {
  19.     printf("%s\n", strerror(errno));
  20.     return - 1;
  21.   }

  22.   /***2.bind the socket***/
  23.   int listen_port = atoi(argv[1]);
  24.   struct sockaddr_in addr_server;
  25.   //memset(&addr_server, 0, sizeof(addr_server));
  26.   addr_server.sin_family = AF_INET;
  27.   addr_server.sin_port = htons(listen_port);
  28.   addr_server.sin_addr.s_addr = htonl(INADDR_ANY);

  29.   if(bind(fd_server, (struct sockaddr*) &addr_server, sizeof(addr_server)) == - 1)
  30.   {
  31.     printf("%s\n", strerror(errno));
  32.     return - 1;
  33.   }

  34.   /***3.listen the socket***/
  35.   if(listen(fd_server, BACKLOG) == - 1)
  36.   {
  37.     printf("%s\n", strerror(errno));
  38.     return - 1;
  39.   }

  40.   /***4.accept the requirement of some client***/
  41.   struct sockaddr_in addr_client;
  42.   int len_addr_client = sizeof(addr_client);
  43.   int fd_client = accept(fd_server, (struct sockaddr*) &addr_client, &len_addr_client);
  44.   if( - 1 == fd_client)
  45.   {
  46.     printf("%s\n", strerror(errno));
  47.     return - 1;
  48.   }

  49.   /****5.serve the client******/
  50.   char buf[BUFFER_SIZE];
  51.   int size;
  52.   while(1)
  53.   {
  54.     /***read from client***/
  55.     size = recv(fd_client, buf, sizeof(buf), 0);
  56.     buf[size] = '\0';
  57.     printf("%s\n", buf);

  58.     /***write to client***/
  59.     size = send(fd_client, buf, strlen(buf), 0);
  60.   }

  61.   /****6.close the socket******/
  62.   close(fd_server);
  63.   close(fd_client);
  64. }

2.2 TCP客戶端

點選(此處)摺疊或開啟

  1. //filename:TCPclient.c
  2. #include <stdio.h>
  3. #include <errno.h>
  4. #include <sys/socket.h>
  5. #include <netinet/in.h>

  6. #define BUFFER_SIZE 1024

  7. int main(int argc, char *argv[])
  8. {
  9.   if(!= argc)
  10.   {
  11.     printf("Usage:%s hostname portnumber\n", argv[0]);
  12.     return - 1;
  13.   }

  14.   /***1.create a socket***/
  15.   int fd_client = socket(AF_INET, SOCK_STREAM, 0); //TCP
  16.   if( - 1 == fd_client)
  17.   {
  18.     printf("%s\n", strerror(errno));
  19.     return - 1;
  20.   }
  21.   /***2.connect to the server***/
  22.   int portnumber = atoi(argv[2]);
  23.   struct sockaddr_in addr_server;
  24.   addr_server.sin_family = AF_INET;
  25.   addr_server.sin_port = htons(portnumber);
  26.   if(== inet_pton(AF_INET, argv[1], (void*) &addr_server.sin_addr.s_addr))
  27.   {
  28.     printf("Invalid address.\n");
  29.     return - 1;
  30.   }

  31.   if(connect(fd_client, (struct sockaddr*) &addr_server, sizeof(addr_server)) == - 1)
  32.   {
  33.     printf("%s\n", strerror(errno));
  34.     return - 1;
  35.   }


  36.   /****3.get the server******/
  37.   char buf[BUFFER_SIZE];
  38.   int size;
  39.   while(1)
  40.   {
  41.     /***write to server***/
  42.     scanf("%s", buf);
  43.     size = send(fd_client, buf, strlen(buf), 0);

  44.     /***read from server***/
  45.     size = recv(fd_client, buf, BUFFER_SIZE, 0);

  46.     buf[size] = '\0';
  47.     printf("%s\n", buf);

  48.   }
  49.   /****4.close the socket******/
  50.   close(fd_client);
  51. }

2.3 測試結果

$ ./TCPserver 2000

$ ./TCPclient 127.0.0.1 2000

三、原始碼分析

3.1 建立套接字

  1. int socket(int domain, int type, int protocol);//成功返回套接字描述符.出錯返回-1

    這一步事實上是確定通訊特徵,各個域domain有自己的格式表示地址,以AF_開頭(address family);type確定套接字型別,如資料報、位元組流;協議protocol對同一個域和套接字型別支援的多個協議進行選擇,通常為0,即按給定的域和套接字型別選擇預設的協議。典型的TCP、UDP如下:

  1. TCP:(AF_INET, SOCK_DGRAM, 0)

  2. UDP:(AF_INET, SOCK_STREAM, 0)

注:

    儘管套接字本質是檔案描述符,但不是所有用於檔案操作的函式都能用於套接字操作,比如lseek,套接字不支援檔案偏移量。

3.2 繫結

  1. int bind(int sockfd, const struct sockaddr *addr, socklen_t len);//成功返回0.出錯返回-1

    bind函式用於將地址繫結到一個套接字。伺服器需要給一個接收客戶端請求套接字繫結一個眾所周知的地址,而客戶端可以讓系統選一個預設地址繫結(無須繫結)。

(1) 套接字地址sockaddr_in

在IPv4因特網域AF_INET中,套接字地址用結構sockaddr_in表示,如下:

  1. struct sockaddr_in
  2. {
  3.   sa_family_t sin_family; //unsigned short 地址族
  4.   in_port_t sin_sport; //uint16_t
  5.   struct in_addr sin_addr; //IPv4
  6. };

  7. struct in_addr
  8. {
  9.   in_addr_t s_addr; //uint32_t
  10. };

注:

    初始化sockaddr_in結構體時,因為sin_port和sin_addr被封裝在網路傳輸,所以埠號和地址必須用網路位元組序;而sin_family只是被核心用來決定資料結構包含什麼型別的地址,沒有傳送到到網路,應該是本機位元組順序。處理器與網路位元組序之間轉換函式為htonl、htons、ntohl、ntohs(h指host主機,n指network網路,l指long32位,s指short16位)。

    理論上,埠號可以是0~65535,但1~1023已由IANA管理,繫結時埠號不少於1024[2]。

    此處的地址s_addr是二進位制地址格式,如果引數是點分十進位制字串表示,則需通過函式inet_ntop(將網路位元組序的二進位制地址轉換成點分十進位制字串表示)、inet_pton進行相互轉換。其轉換過程如下:

  1. 127.0.0.1 --> 7F.0.0.1 --> 100007F=16777343(網路位元組序為大端)

    如果地址s_addr為ANADDR_ANY,套接字端點可以被繫結到所有系統網路介面,即可以收到這個系統所安裝的所有網絡卡的資料包。

(2) 通用地址格式sockaddr

    地址格式與特定的通訊域有關(如AF_INET、AF_INET6),為使不同地址格式地址能夠傳入套接字函式,地址被強制轉換成通用的地址結構sockaddr,如下(以Linux為例):

  1. struct sockaddr
  2. {
  3.   unsigned short sa_family; /* address family, AF_xxx */
  4.   char sa_data[14]; /* 14 bytes of protocol address */
  5. };

3.3 監聽listen

  1. int listen(int sockfd, int backlog);//成功返回0,出錯返回-1

    一旦伺服器呼叫listen,套接字就能接收連線請求。backlog用於表示該程序所要入隊的連線請求數量,實際值由系統決定,但上限由SOMAXCONN指定。一旦佇列滿,系統會拒絕多餘連線請求。

3.4 接受連線請求accept

  1. //成功返回套接字描述符,出錯返回-1
  2. int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);

    使用accept獲得連線請求並建立連線,新的套接字描述符連線到呼叫connect的客戶端。傳給accept的原始套接字(sockfd)沒有關聯到這個連線,而是接收保持可用狀態並接受其他請求連線,這樣做是為了使新的套接字描述符和原始套接字具有相同的地址族domain和套接字型別type。

    如果伺服器呼叫accept並且當前沒有連線請求,伺服器會阻塞直到一個請求到來。如果不關心客戶端標識,可以將引數addr和len設為NULL。

注:

    關鍵字restrict是C99新引入的,所有修改該指標所指向內容的操作全部都是基於(base on)該指標的,即不存在其它進行修改操作的途徑;從而幫助編譯器進行更好的程式碼優化,生成更有效率的彙編程式碼[4]。

3.5 建立連線connect

  1. //成功返回0,出錯返回-1
  2. int connect(int sockfd, const struct sockaddr *addr, socklen_t len);

    addr是想與之通訊的伺服器地址,如果sockfd沒有繫結到一個地址,connect會給呼叫者繫結一個預設的地址。成功連線需要以下條件:要連線的機器開啟且正在執行,伺服器繫結到一個想與之連線的地址,伺服器的等待連線佇列有足夠的空間。

3.6 讀取資料

  1. ssize_t read(int fd, void *buf, size_t nbytes); //成功返回讀到的位元組數,已到檔案末尾返回0,出錯返回-1

  2. ssize_t recv(int sockfd, const void *buf, size_t nbytes, int flags); //成功返回位元組計數的訊息長度,無可用訊息或對方已經按序結束返回0,出錯返回-1

  3. ssize_t recvfrom(int sockfd,void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socketlen_t *restruct addrlen); //成功返回位元組計數的訊息長度,無可用訊息或對方已經按序結束返回0,出錯返回-1

  4. ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);//成功返回位元組計數的訊息長度,無可用訊息或對方已經按序結束返回0,出錯返回-1

    可以使用read通過套接字通訊,但read只能交換資料,若想指定選項、從多個客戶端接收資料包,則需選擇套接字函式recv(指定標誌控制接收資料的方式)、recvfrom(得到資料傳送者的源地址)、resvmsg(將接收到資料送入多個緩衝區或接收輔助資料)。

3.7 寫入資料

  1. ssize_t write(int fd, void *buf, size_t count); //成功返回寫入位元組數,出錯返回-1

  2. ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags); //成功返回傳送的位元組數,出錯返回-1

  3. ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, 
  4.               const struct sockaddr *destaddrsocklen_t destlen); //成功返回傳送的位元組數,出錯返回-1

  5. ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)//成功返回傳送的位元組數,出錯返回-1

注:send與sendto的flags含義相同,sendmsg的flags與前兩者不同

    可以使用write通過套接字通訊,但write只能交換資料,若想指定選項、傳送帶外資料,則需選擇套接字函式send(指定標誌改變處理傳輸資料的方式)、sendto(允許無連線的套接字上指定一個目標地址)、sendmsg(指定多重緩衝傳輸資料)。

3.8 終止連線

  1. int close(int fd); //成功返回0,出錯返回-1

  2. int shutdown(int sockfd, int how);//成功返回0,出錯返回-1

    關閉套接字close只有在最後一個活動引用被關閉後才釋放網路端點,而shutdown提供更精細的控制,套接字通訊是雙向的,可以用shutdown禁止套接字上的輸入/輸出,即how為SHUT_RD、SHUT_WR、SHUT_RDWR。除此之外,shutdown允許使一個套接字處於不活動狀態(不管引用它的檔案描述符數目多少),便於複製一個套接字(如dup)。