網路程式設計之理論篇

Liberuman發表於2018-01-10

網路通訊作為網際網路的技術支援,已被廣泛應用在軟體開發中,無論是Web,服務端,客戶端還是桌面應用,都是必須掌握的一門技術。

什麼是網路程式設計?

在軟體開發層面實現遠端資料交換的程式設計技術。
複製程式碼

網路程式設計的主要場景

  • 基於Http/Https的web,移動端的開發;
  • 基於TCP/UDP的IM,桌面應用的開發;
  • 自定義協議的開發。

要熟悉網路程式設計,首先需要學習網路協議的相關知識。

網路協議

什麼網路協議呢?網路協議是為網路中進行資料交換定義的規則,以實現按此規範進行傳輸資料,就可在整個網際網路中進行資料交換的目標。所以說網路協議是網路通訊的基礎。

網路協議中其中最著名就是TCP/IP協議族。TCP/IP協議族通常被認為是一個四層的協議系統。從上到下依次為:

  • 應用層:包括所有和應用程式協同工作,利用基礎網路交換應用程式專用的資料協議,如Telnet, FTP, Http等;
  • 運輸層:負責為應用層提供端到端的通訊支援,包含TCP和UDP協議;
  • 網路層:負責資料包在網路中的傳輸,包含IP, ICMP和IGMP協議;
  • 鏈路層:用以處理與物理裝置的互動細節,對應於作業系統中的裝置驅動程式;

從TCP/IP協議族各層次的職責來看,網路資料的傳遞是從上到下依次傳遞。以Http協議為為例,具體的資料傳輸過程如圖所示:

資料傳輸過程

當然,這只是資料的流向,實際上網路資料在傳輸過程中需要封裝與分解,具體的過程如圖所示(圖片來源於百度圖片):

網路程式設計之理論篇

簡單來說,資料從本機上傳到網路之前,會在TCP/IP協議族的每一層新增協議首部,到達目標主機後,目標主機再進行分解,從而獲取需要的資料。

在眾多的網路協議中,使用最廣泛的應該就是TCP協議(傳輸控制協議)了,它是一種面向連線,可靠的,基於位元組流的傳輸層協議。其執行過程分為三個階段:建立連線、交換資料、斷開連線。

TCP連線的建立與終止

在使用TCP協議交換資料前,需先建立一條連線,在不需要傳送資料的時候,需要斷開連線,以釋放資源。

建立連線

TCP協議建立連線時需要三次握手,其過程可使用以下場景來描述:

面試官:說一下TCP建立連線時三次握手的過程。
小明: 三次握手?
面試官: 嗯。
小明:握手完成了。
面試官:what?
複製程式碼

沒錯,從小明開始問三次握手到說握手完成就是三次握手的過程,具體的過程如下:

  1. 客戶端發向服務端傳送一個SYN段,以告知客戶端要連線服務端的指定埠;
  2. 服務端收到客戶端的報文後,返回一個對客戶端報文進行確認的ack段(SYN+1)和一個表示服務端報文的SYN段;
  3. 客戶端收到服務端傳送的報文後,對服務端傳送一個ack(服務端的SYN+1),至此,客戶端和服務端就建立了一條連線。

為什麼需要3次握手而不是2次呢?

因為客戶端收到服務端的應答後,知道連線已建立成功,但是服務端並不知道自己傳送的確認報文客戶端是否收到,
所以需要客戶端對服務端的報文進行確認。
複製程式碼

服務端一定能收到客戶端傳送的確認報文麼?

不一定,如果收不到,那麼連線就不會建立,所以,3次握手只是理論上確保建立連線的次數。那能否通過4次握手呢?
不行,再握下去就是雞生蛋,蛋生雞的問題了。
複製程式碼

斷開連線

TCP斷開連線時需要四次握手,為什麼需要4次呢?這是由於TCP半關閉的性質造成的。所謂半關閉,就是可以傳送資料,卻不能接收資料或只能接收資料,不能傳送資料。

四次握手的過程:(由於主動斷開連線可傳送在客戶端,也可傳送在服務端,所以下面以A,B來區分兩端)

  1. A端向B端傳送一個FIN, 告知A端即將斷開連線;
  2. B端收到A端傳送的FIN後傳送一個ack對其進行確認;
  3. 隨後B端向再A端傳送一個FIN,以告知B端也即將斷開連線;
  4. A端時候到B端的FIN報文後,對其傳送一個ack以示確認。

B端在對A端進行確認的時候為什麼不同時發一個FIN呢? 可以同時發。分開發是為了考慮B端在對A端確認後,可能還會給A繼續傳送資料的情況。

TCP的狀態變遷過程

下面以一張圖來描述TCP的狀態變遷過程(圖片來自百度圖片)

網路程式設計之理論篇

圖片中的所有狀態對應於TCP的建立和連線過程,下面簡單介紹一下這幾種狀態:

LISTEN: 服務端狀態,表示服務端正在等待客戶端的連線請求,處於監聽狀態;

SYN收到:服務端狀態,服務端已收到客戶端的連線請求,並對客戶端的請求傳送了ACK確認(第二次握手完成);

SYN_SENT: 客戶端狀態,客戶端傳送SYN或資料後的狀態(第一次握手完成);

ESTABLISHED: 客戶端對服務端的SYN進行確認後處於ESTABLISHED,服務端收到客戶端傳送的ACK後也會處於
             ESTABLISHED狀態(三次握手完成後的狀態);

FIN_WAIT_1: 主動關閉的一端的狀態,傳送FIN後的狀態(斷開連線時的第一次握手完成);

FIN_WAIT_2: 主動關閉的一端的狀態,收到另一端的ACK確認後的狀態(斷開連線時的第二次握手完成);

CLOSING:主動關閉的一端的狀態,收到另一端的FIN,並對其進行確認後的狀態(客戶端和服務端同時關閉的情況);

TIME_WAIT: 主動關閉的一端的狀態,,收到另一端的FIN或(FIN和ACK)後,對其進行確認後的狀態
          (斷開連線時最後一次握手完成);

CLOSE_WAIT: 被動關閉的一端的狀態,收到另一端傳送的FIN並對其進行確認後的狀態(斷開連線時第二次握手完成);

LAST_ACK: 被動關閉的一端的狀態,傳送FIN後的狀態(斷開連線時第三次握手完成);

CLOSED:連線徹底斷開;
複製程式碼

TCP/IP協議族誕生之後,各個平臺(Window, Unix)就按照此協議規範在系統層面為開發網路程式提供了統一的介面——Socket。通過這個面向傳輸層協議的系統介面,我們可通過TCP/UDP協議快速實現網路資料的交換,同時也可用來實現應用層協議,如HTTP, SSL等。

Socket(套接字)

Socket是作業系統為上層應用實現網路資料交換提供的介面,我們可通過以下場景來理解:

當你給別人打電話的時候首先要確認打給誰,其次確認打哪個號碼,通過這兩個條件就可準確的聯絡到對方。那麼在網路中傳輸資料也是同樣的道理,在網路中定位主機是通過IP來實現的,一個IP代表了一臺主機,但是每臺主機有很多個埠號,所以要準確地與某個應用進行資料交換,除了IP地址外,還需要一個埠號。有了這兩個條件,就可通過Socket實現資料交換。由此可見,Socket其實就相當於一部手機,兩部手機之間建立一條通路即可實現通話。

Socket程式設計的步驟

客戶端

  1. 建立scoket;
  2. 連線伺服器;
  3. 傳送、接收資料;
  4. 關閉socket連線

程式碼實現(Linux C程式設計):

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h> 

int main(int argc, char *argv[])
{
    int sockfd = 0, n = 0;
    char recvBuff[1024];
    struct sockaddr_in serv_addr; 

    if(argc != 2)
    {
        printf("\n Usage: %s <ip of server> \n",argv[0]);
        return 1;
    } 

    memset(recvBuff, '0',sizeof(recvBuff));
    // 建立socket
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("\n Error : Could not create socket \n");
        return 1;
    } 

    // 設定IP和埠
    memset(&serv_addr, '0', sizeof(serv_addr)); 
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(5000); 

    if(inet_pton(AF_INET, argv[1], &serv_addr.sin_addr)<=0)
    {
        printf("\n inet_pton error occured\n");
        return 1;
    } 
    // 連線到指定的IP和埠 -> 連線成功後即三次握手完成
    if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
       printf("\n Error : Connect Failed \n");
       return 1;
    } 
    // 讀資料
    while ( (n = read(sockfd, recvBuff, sizeof(recvBuff)-1)) > 0)
    {
        recvBuff[n] = 0;
        if(fputs(recvBuff, stdout) == EOF)
        {
            printf("\n Error : Fputs error\n");
        }
    } 

    if(n < 0)
    {
        printf("\n Read error \n");
    } 
    close(scokfd);
    
    return 0;
}
複製程式碼

服務端

  1. 建立socket;
  2. 繫結IP和埠;
  3. 監聽客戶端的連線;
  4. 接收客戶端的連線;
  5. 傳送、接收資料
  6. 關閉socket連線;

程式碼實現:(Linux C程式設計)

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <time.h> 

int main(int argc, char *argv[])
{
    int listenfd = 0, connfd = 0;
    struct sockaddr_in serv_addr; 

    char sendBuff[1025];
    time_t ticks; 
    
    // 建立socket
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    memset(&serv_addr, '0', sizeof(serv_addr));
    memset(sendBuff, '0', sizeof(sendBuff)); 
    
    // 繫結IP地址和埠
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(5000); 
    bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); 
    
    // 監聽客戶埠的連線請求 -> 對應於狀態圖中的LISTEN狀態
    listen(listenfd, 10); 
    while(1)
    {
        // 接收客戶端的請求 -> 與客戶端三次握手完成
        connfd = accept(listenfd, (struct sockaddr*)NULL, NULL); 
        ticks = time(NULL);
        snprintf(sendBuff, sizeof(sendBuff), "%.24s\r\n", ctime(&ticks));
        
        // 向客戶端傳送資料
        write(connfd, sendBuff, strlen(sendBuff)); 

        // 關閉socket
        close(connfd);
        sleep(1);
     }
}
複製程式碼

程式碼出處 https://www.thegeekstuff.com/2011/12/c-socket-programming/

通過以上程式碼,我們對socket有了一個簡單的認識,同時也瞭解了資料交換的基本流程。後面會對基於TCP協議的HTTP協議進行一個詳細的介紹。

參考資料

TCP/IP協議族

傳輸控制協議

《TCP/IP詳解 卷一》

相關文章