国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

認識協(xié)議【網(wǎng)絡基礎】

這篇具有很好參考價值的文章主要介紹了認識協(xié)議【網(wǎng)絡基礎】。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

什么是協(xié)議

在網(wǎng)絡通信中,協(xié)議(Protocol)是指計算機或設備之間進行通信的一系列規(guī)則的集合。

不管是網(wǎng)絡還是生活中,協(xié)議是一種事先約定好的規(guī)則,通信的參與方按照同一份規(guī)則進行通信,如連接方式,如何識別等等。只有事先約定好了規(guī)則,才能保證后續(xù)通信時的效率和一定的安全性。

協(xié)議規(guī)定了通信實體之間所交換的消息的格式、意義、順序以及針對收到信息或發(fā)生的事件所采取的動作。協(xié)議可以分為不同的層次,每一層負責不同的通信功能。常見的協(xié)議有IP、TCP、HTTP、POP3、SMTP等,它們屬于TCP/IP協(xié)議族,也就是基于TCP和IP這兩個最初的協(xié)議之上的不同的通信協(xié)議的大集合。

結(jié)構(gòu)化數(shù)據(jù)

在網(wǎng)絡通信時,數(shù)據(jù)可以分為結(jié)構(gòu)化數(shù)據(jù)和非結(jié)構(gòu)化數(shù)據(jù)。

處理數(shù)據(jù)的主體是機器,數(shù)據(jù)的本質(zhì)是二進制序列,因此數(shù)據(jù)在網(wǎng)絡中傳輸也是以二進制序列的形式傳輸?shù)摹?/p>

值得注意的是:

  • 二進制序列和二進制數(shù)據(jù)流是不同的概念。二進制序列是指一串由0和1組成的數(shù)字,它可以表示任何類型的數(shù)據(jù),例如文本、圖像、音頻等。二進制數(shù)據(jù)流是指一種數(shù)據(jù)傳輸方式,它是指以二進制序列為單位的連續(xù)數(shù)據(jù)流,例如從網(wǎng)絡或文件中讀取或?qū)懭霐?shù)據(jù)。

  • 二進制序列流可以認為是字符流,也就是字符串,只要它們遵循一定的編碼規(guī)則,例如ASCII、UTF-8等。不同的編碼規(guī)則會影響二進制序列和字符之間的對應關(guān)系。

也就是說,數(shù)據(jù)在網(wǎng)絡中傳輸時,都是以二進制序列的形式傳輸?shù)?,無論是結(jié)構(gòu)化數(shù)據(jù)還是非結(jié)構(gòu)化數(shù)據(jù)。只是它們的組織方式不同,結(jié)構(gòu)化數(shù)據(jù)有固定的格式和模式,非結(jié)構(gòu)化數(shù)據(jù)沒有預定義的格式和模式。因此,處理和分析這兩種類型的數(shù)據(jù)需要不同的工具和方法。

結(jié)構(gòu)化數(shù)據(jù)

結(jié)構(gòu)化數(shù)據(jù)是指按照一定的規(guī)則、格式和順序組織的數(shù)據(jù),通常以表格、樹形結(jié)構(gòu)、圖形等形式呈現(xiàn),可以用數(shù)據(jù)庫二維邏輯表來表現(xiàn)的數(shù)據(jù)。例如,在一個數(shù)據(jù)庫中,數(shù)據(jù)以表格的形式存儲,每個表格都有固定的字段和數(shù)據(jù)類型,數(shù)據(jù)的格式和順序都是固定的。在網(wǎng)絡通信中,結(jié)構(gòu)化數(shù)據(jù)通常以XML、JSON、Protocol Buffers等格式進行傳輸。

簡單理解結(jié)構(gòu)化數(shù)據(jù):例如我們的身份證ID,雖然看起來是一串數(shù)字,但是不同的位數(shù)表示了不同的含義。這和下面的序列化和反序列化息息相關(guān)。

非結(jié)構(gòu)化數(shù)據(jù)

非結(jié)構(gòu)化數(shù)據(jù)是指沒有固定格式和順序的數(shù)據(jù),通常以文本、圖像、音頻、視頻等形式呈現(xiàn),不方便用數(shù)據(jù)庫二維邏輯表來表現(xiàn)的數(shù)據(jù)。例如,在一個文本文件中,數(shù)據(jù)沒有固定的格式和順序,而是由一些字符和換行符組成。在網(wǎng)絡通信中,非結(jié)構(gòu)化數(shù)據(jù)通常以二進制數(shù)據(jù)流的形式進行傳輸,例如在FTP、HTTP等協(xié)議中,文件就是以二進制數(shù)據(jù)流的形式傳輸?shù)摹?/p>

相比于非結(jié)構(gòu)化數(shù)據(jù),結(jié)構(gòu)化數(shù)據(jù)更容易被處理和解析,因為它們有固定的格式和順序,可以通過解析規(guī)則來進行處理。而非結(jié)構(gòu)化數(shù)據(jù)則需要更強的語義理解和處理能力,因為它們沒有固定的格式和順序,需要通過文本分析、圖像處理、音頻識別等技術(shù)來進行處理。在網(wǎng)絡通信中,結(jié)構(gòu)化數(shù)據(jù)通常用于傳輸少量的、格式固定的數(shù)據(jù),而非結(jié)構(gòu)化數(shù)據(jù)則用于傳輸大量的、沒有固定格式和順序的數(shù)據(jù),例如圖像、音頻、視頻等。

半結(jié)構(gòu)化數(shù)據(jù)

半結(jié)構(gòu)化數(shù)據(jù)是指非關(guān)系模型的、有基本固定結(jié)構(gòu)模式的數(shù)據(jù),例如日志文件、XML文檔、JSON文檔等。在此暫不考慮。

結(jié)構(gòu)化數(shù)據(jù)的傳輸

在網(wǎng)絡中傳輸結(jié)構(gòu)化數(shù)據(jù)時,通常會將數(shù)據(jù)轉(zhuǎn)換為字符串的形式進行傳輸,這樣可以方便在網(wǎng)絡上傳輸和解析。具體地說,結(jié)構(gòu)化數(shù)據(jù)在網(wǎng)絡中傳輸時,有以下兩種情況:

  1. 字符串形式傳輸:一種常見的方法是將結(jié)構(gòu)化數(shù)據(jù)轉(zhuǎn)換為字符串格式(也就是說結(jié)構(gòu)化的數(shù)據(jù)不能拆分地發(fā)送),例如XML、JSON等格式,然后通過網(wǎng)絡協(xié)議(如HTTP、TCP、UDP等)進行傳輸。在接收端,可以通過解析字符串,將其還原為結(jié)構(gòu)化數(shù)據(jù)。

    如果數(shù)據(jù)本身已經(jīng)是字符串格式,那么在網(wǎng)絡中傳輸時可以直接將其作為消息體進行傳輸,無需進行額外的轉(zhuǎn)換。例如,在使用HTTP協(xié)議進行通信時,可以將字符串格式的數(shù)據(jù)直接放在HTTP請求或響應的消息體中進行傳輸。

  2. 二進制形式傳輸:另一種方法是將結(jié)構(gòu)化數(shù)據(jù)直接編碼為二進制數(shù)據(jù),然后通過網(wǎng)絡協(xié)議進行傳輸。這種方法可以減少數(shù)據(jù)傳輸?shù)拇笮。岣邆鬏斝?。在接收端,可以將接收到的二進制數(shù)據(jù)解碼為結(jié)構(gòu)化數(shù)據(jù)。

需要注意的是:

  • [重要]結(jié)構(gòu)化數(shù)據(jù)在網(wǎng)絡中傳輸時,通常是作為一個整體進行傳輸?shù)?,而不是拆分成多個部分進行分別發(fā)送的。這是因為結(jié)構(gòu)化數(shù)據(jù)通常具有一定的層次結(jié)構(gòu),其中包含了多個元素或字段,這些元素或字段之間存在著一定的關(guān)系和依賴關(guān)系。如果將結(jié)構(gòu)化數(shù)據(jù)拆分成多個部分進行分別發(fā)送,可能會導致數(shù)據(jù)不完整或順序不正確,從而影響數(shù)據(jù)的正確性和解析。

    可以認為若干數(shù)據(jù)本身是一個結(jié)構(gòu)體或類的屬性(在示例中也是這么做的)。

    例如,假設要傳輸一個XML格式的文檔,其中包含多個標簽、元素和屬性。如果將文檔拆分成多個部分進行分別發(fā)送,可能會導致某些標簽、元素或?qū)傩员环珠_發(fā)送,從而無法正確解析文檔。此外,如果拆分后的數(shù)據(jù)包過小,還會導致網(wǎng)絡傳輸效率低下,增加網(wǎng)絡傳輸?shù)拈_銷。

    需要注意的是,盡管可以使用特殊的協(xié)議或技術(shù)將結(jié)構(gòu)化數(shù)據(jù)拆分成多個部分進行傳輸,但這種方式仍然可能會增加數(shù)據(jù)傳輸?shù)膹碗s度和開銷,因此只有在必要的情況下才應該使用。如果數(shù)據(jù)本身不需要拆分,那么應該將其作為一個整體進行傳輸,以確保數(shù)據(jù)的正確性和傳輸效率。

  • 如果字符串中包含一些特殊字符(例如空格、換行符、制表符、單引號、雙引號等),則需要對其進行轉(zhuǎn)義,以避免在傳輸過程中出現(xiàn)解析錯誤。常見的轉(zhuǎn)義方式包括使用轉(zhuǎn)義字符(如\n、\t、'、"等)或?qū)⒆址M行Base64編碼等。在接收端,需要根據(jù)具體的轉(zhuǎn)義方式進行解析和還原字符串數(shù)據(jù)。

  • 雖然字符串格式和二進制格式是兩種常見的數(shù)據(jù)傳輸方式,但在實際應用中也有其他的數(shù)據(jù)傳輸方式。例如,一些協(xié)議(如HTTP)支持直接傳輸HTML、CSS等格式的文本數(shù)據(jù),UDP協(xié)議可以支持直接傳輸音視頻流等二進制數(shù)據(jù)。因此,在選擇數(shù)據(jù)傳輸方式時,需要根據(jù)具體的應用場景和要求進行選擇。

  • 此外,如果字符串數(shù)據(jù)需要進行壓縮,可以使用壓縮算法(如Gzip、Deflate等)將其壓縮后再進行傳輸,以減少數(shù)據(jù)傳輸?shù)拇笮 T诮邮斩?,需要將接收到的壓縮數(shù)據(jù)進行解壓縮,還原為原始的字符串數(shù)據(jù)。

例如稍后要實現(xiàn)的網(wǎng)絡版計算器,操作符和其兩側(cè)的操作數(shù)就是結(jié)構(gòu)化的數(shù)據(jù),它不應該被分散地發(fā)送,因為對于一次運算它們是必要的。由于這是一個跨網(wǎng)絡的數(shù)據(jù)傳輸,因此對于客戶端向服務端發(fā)送計算請求時,應該將操作符和操作數(shù)打包在一起,作為一個整體發(fā)送給服務端處理;這樣就能保證服務端能夠接收到完整的數(shù)據(jù)。這便是“結(jié)構(gòu)化”的意義。

序列化和反序列化

序列化和反序列化是一種在網(wǎng)絡傳輸中處理對象的方法,它們是一對相反的操作。

  • 序列化是把(結(jié)構(gòu)化的)對象轉(zhuǎn)換為可以傳輸?shù)亩M制流(二進制序列)的過程。
  • 反序列化是把二進制流(序列)轉(zhuǎn)換為(結(jié)構(gòu)化的)對象的過程。

進行序列化和反序列化的原因有兩個:

  • 實現(xiàn)數(shù)據(jù)的持久化,通過序列化可以把數(shù)據(jù)永久地保存到硬盤上(通常存放在文件里);
  • 利用序列化實現(xiàn)遠程通信,即在網(wǎng)絡上傳送對象的字節(jié)序列。

網(wǎng)絡版計算器概述

制定協(xié)議

協(xié)議是通信方事先約定好的規(guī)則,由于通信的內(nèi)容有所不同,對于若干個綁定在一起的數(shù)據(jù),通過網(wǎng)絡傳輸會提高風險,因此使用一個類或結(jié)構(gòu)體保存它,然后將它打包通過網(wǎng)絡傳輸。

值得注意的是,網(wǎng)絡只是數(shù)據(jù)傳輸?shù)耐ǖ溃瑪?shù)據(jù)處理的主體是計算機,在計算機眼里,數(shù)據(jù)是由01組成的二進制序列。

為什么不直接傳輸結(jié)構(gòu)化數(shù)據(jù),而首先要轉(zhuǎn)換為二進制序列?

  • 一是為了保證數(shù)據(jù)的可移植性,不同的平臺或語言可能有不同的數(shù)據(jù)表示方式,而二進制數(shù)據(jù)是一種通用的格式,可以在不同的環(huán)境中進行交換;

    例如不同系統(tǒng)和不同平臺看待結(jié)構(gòu)體的視角不同、大小端也可能不同。

  • 二是為了提高數(shù)據(jù)的傳輸效率,結(jié)構(gòu)化的數(shù)據(jù)通常包含很多元數(shù)據(jù)和冗余信息,而二進制數(shù)據(jù)是一種緊湊的格式,可以減少數(shù)據(jù)的大小和帶寬消耗;

  • 三是為了保證數(shù)據(jù)的安全性,結(jié)構(gòu)化的數(shù)據(jù)容易被人為篡改或破解,而二進制數(shù)據(jù)是一種難以直接閱讀和修改的格式,可以增加數(shù)據(jù)的保密性。

客戶端和服務端是相對的,一般將請求計算資源的一端稱之為客戶端,將響應請求返回結(jié)果的一端稱之為服務端。向網(wǎng)絡中發(fā)送數(shù)據(jù)的主體可能是客戶端(發(fā)送請求)也可能是服務端(響應請求),這需要將數(shù)據(jù)轉(zhuǎn)化為二進制序列,也就是序列化;同樣地,從網(wǎng)絡中接受數(shù)據(jù)的主體可能是客戶端(接收服務端的處理結(jié)果)也可能是服務端(處理客戶端的請求),需要將二進制序列形式的數(shù)據(jù)轉(zhuǎn)換為服務器機器能處理的結(jié)構(gòu)化的數(shù)據(jù),才能進行處理

也就是說,二進制序列是通用的,它能被各種機器識別,而不被它們之間的設計差異而有所區(qū)別。因此服務端或客戶端機器將從網(wǎng)絡中獲取的二進制序列轉(zhuǎn)換為結(jié)構(gòu)化的數(shù)據(jù)后,這個結(jié)構(gòu)化數(shù)據(jù)(的二進制組成)不一定和原主機進行序列化之前的結(jié)構(gòu)化數(shù)據(jù)完全相同,這取決于機器和軟件的實現(xiàn)。

服務端大多數(shù)機器是Linux,客戶端的操作系統(tǒng)可能是各種各樣的。

通過字符串傳輸

很容易想到的一種協(xié)議是:客戶端發(fā)送形如操作數(shù)1 運算符 操作數(shù)2這樣的字符串給服務端,然后服務端解析這個字符串,取出操作數(shù)和運算符,將運算以后的結(jié)果返回。但是這樣的操作實在太麻煩了。而且服務端響應請求都要執(zhí)行這樣繁瑣的操作,大大降低效率。

結(jié)構(gòu)化數(shù)據(jù)+序列化與反序列化

將操作數(shù)和運算符打包為一個結(jié)構(gòu)化數(shù)據(jù),放在一個類或結(jié)構(gòu)體中,客戶端將屬性填充好以后對其進行序列化操作,使之能通過網(wǎng)絡傳輸給對端服務器。當服務器接受到二進制形式的結(jié)構(gòu)化數(shù)據(jù)后,對其進行反序列化,轉(zhuǎn)換為客戶端主機能處理的結(jié)構(gòu)化數(shù)據(jù),直接取出結(jié)構(gòu)體或類中的屬性,而不需要花費過多資源解析。

簡單地說,序列化后的數(shù)據(jù)是方便機器處理,反序列化后的數(shù)據(jù)是方便用戶層查看。

實現(xiàn)計算器

網(wǎng)絡相關(guān)接口

將Linux中的網(wǎng)絡操作例如創(chuàng)建套接字、監(jiān)聽、獲取連接、連接等操作封裝為函數(shù),然后放在類Sock中。

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Log.hpp"

class Sock
{
private:
    const static int _backlog = 20;

public:
    Sock() {}
    ~Sock() {}
    // 1. 創(chuàng)建套接字
    int Socket()
    {
        int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (listen_sockfd < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(DEBUG, "%s: %d", "create socket success, sockfd", listen_sockfd);
        return listen_sockfd;
    }
    // 2. 綁定
    void Bind(int listen_sockfd, uint16_t port, std::string ip = "0.0.0.0")
    {

        // 2.1 填充屬性
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;    // 網(wǎng)絡傳輸
        local.sin_port = htons(port); // 本地->網(wǎng)絡
        local.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());

        // 2.2 綁定
        if (bind(listen_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind():errno:%d:%s", errno, strerror(errno));
            exit(3);
        }
    }
    void Listen(int listen_sockfd)
    {
        // 3. 監(jiān)聽
        if (listen(listen_sockfd, _backlog) < 0)
        {
            logMessage(FATAL, "listen()errno:%d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "initialize tdp server...%s", strerror(errno));
    }
    int Accept(int listen_sockfd, std::string *ip, uint16_t *port)
    {
        struct sockaddr_in client;
        memset(&client, 0, sizeof(client));
        socklen_t len = sizeof(client);
        int service_sockfd = accept(listen_sockfd, (struct sockaddr *)&client, &len);
        // 獲取連接失敗
        if (service_sockfd < 0)
        {
            logMessage(ERROR, "accept()errno:%d:%s", errno, strerror(errno));
            return -1;
        }
        if (port)
            *port = ntohs(client.sin_port);
        if (ip)
            *ip = inet_ntoa(client.sin_addr);
        return service_sockfd;
    }
    // 獲取連接成功
    // 通信準備 (網(wǎng)絡->主機)
    bool Connect(int service_sockfd, const std::string &server_ip, const uint16_t &server_port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());

        if (connect(service_sockfd, (struct sockaddr *)&server, sizeof(server)) == 0)
            return true;
        else
            return false;
    }
};

框架

服務端

目前的服務端并未使用線程池,只是每獲取一個客戶端請求后,創(chuàng)建一個線程執(zhí)行線程函數(shù)。

  • 下面的邏輯將會在命名空間ns_tcpserver中定義,表示網(wǎng)絡流通信(Network Stream )。

  • 定義一個類ThreadData,保存網(wǎng)絡通信獲取的套接字文件描述符,為了能夠在靜態(tài)的線程函數(shù)中直接調(diào)用TcpServer類中的接口,用一個成員保存指向TcpServer對象的地址。

  • 定義一個TcpServer類,其中包含監(jiān)聽套接字文件描述符,以及剛才實現(xiàn)的Sock對象,以便直接通過這個對象執(zhí)行網(wǎng)絡相關(guān)操作。還有一個數(shù)組_func保存不同的線程函數(shù)。

    • 構(gòu)造函數(shù):獲取監(jiān)聽套接字文件描述符、綁定和監(jiān)聽。
    • 析構(gòu)函數(shù):關(guān)閉監(jiān)聽套接字文件描述符。
    • Bind():綁定一個服務,即將數(shù)組_func中的一個函數(shù)和內(nèi)核綁定起來。
    • Excute():執(zhí)行任務。
    • Start():通過Sock對象中的Accept()獲取連接,然后創(chuàng)建一個線程執(zhí)行任務。
// CalServer.hpp
#pragma once

#include <pthread.h>
#include <functional>
#include <vector>
#include "Sock.hpp"

namespace ns_tcpserver
{
    using func_t = std::function<void(int)>;

    class TcpServer; // 聲明TcpServer類,以供ThreadData定義成員
    
    class ThreadData
    {
    public:
        ThreadData(int sockfd, TcpServer *server)
            : _sockfd(sockfd), _server(server)
        {
        }
        ~ThreadData() {}
    public:
        int _sockfd;
        TcpServer *_server;
    };

    class TcpServer
    {
    private:
        static void *ThreadRoutine(void *args)
        {
            pthread_detach(pthread_self());
            ThreadData *td = static_cast<ThreadData *>(args);
            td->_server->Excute(td->_sockfd);
            close(td->_sockfd);
            return nullptr;
        }

    public:
        TcpServer(const uint16_t &port, const std::string &ip = "")
        {
            _listen_sockfd = _sock.Socket();
            _sock.Bind(_listen_sockfd, port, ip);
            _sock.Listen(_listen_sockfd);
        }
        ~TcpServer()
        {
            if (_listen_sockfd >= 0)
                close(_listen_sockfd);
        }
        void BindService(func_t func)
        {
            _func.push_back(func);
        }
        void Excute(int sockfd)
        {
            for (auto &f : _func)
                f(sockfd);
        }
        void Start()
        {
            while (1)
            {
                std::string client_ip;
                uint16_t client_port;
                int sockfd = _sock.Accept(_listen_sockfd, &client_ip, &client_port);
                if (sockfd == -1)
                    continue;
                logMessage(NORMAL, "%s: %d", "link success, sockfd: %d", sockfd);
                pthread_t tid;
                ThreadData *td = new ThreadData(sockfd, this);
                pthread_create(&tid, nullptr, ThreadRoutine, td);
            }
        }

    private:
        int _listen_sockfd;
        Sock _sock;
        std::vector<func_t> _func;
    };
}

值得注意的是:

  • ThreadData的成員_server類型是TcpServer *,但是后者還沒有實現(xiàn),所以要在ThreadData之前使用class TcpServer;聲明TcpServer類才能編譯通過。
  • using func_t = std::function<void(int)>;是一種常用的給函數(shù)對象起別名的方法。
// CalServer.cc
#include <iostream>
#include <memory>
#include "CalServer.hpp"

using namespace ns_tcpserver;

void Usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " [PORT]\n" << std::endl;
}

void debug(int sock)
{
    std::cout << "test" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::unique_ptr<TcpServer> server_ptr(new TcpServer(atoi(argv[1])));
    server_ptr->BindService(debug);
    server_ptr->Start();
}

目前的代碼能編譯通過,后續(xù)只要在這個基礎上修改即可。

客戶端

由于TCP面向連接,客戶端無需手動綁定,實現(xiàn)起來比服務端更簡單。

// CalClient.hpp
#include "Sock.hpp"

namespace ns_tcpclient
{
    class TcpClient
    {
    public:
        TcpClient(const uint16_t &server_port, const std::string &server_ip = "") 
        {
            _sockfd = _sock.Socket();
            if (!_sock.Connect(_sockfd, server_ip, server_port))
            {
                logMessage(FATAL, "Sock.Connect():errno:%d:%s", errno, strerror(errno));
                exit(2);
            }
        }
        ~TcpClient() 
        {
            if (_sockfd >= 0) 
                close(_sockfd);
        }
        void Start()
        {
            bool quit = false;
            while (!quit)
            {
                // 獲取請求
                // 發(fā)送請求
                // ...
            }
        }
    private:
        int _sockfd;
        Sock _sock;
    };
}
  • 成員屬性:_sockfd保存套接字文件描述符;Sock類型的_sock對象,以便客戶端調(diào)用網(wǎng)絡相關(guān)接口。
  • 構(gòu)造函數(shù):獲取用戶在命令行傳入的IP和PORT,然后通過_sock對象調(diào)用Socket()接口創(chuàng)建套接字,同時保存文件描述符。
  • 析構(gòu)函數(shù):關(guān)閉文件描述符。
  • Start():客戶端發(fā)起請求的邏輯,將在后續(xù)實現(xiàn)。
// CalClient.cc
#include <iostream>
#include <memory>
#include "CalClient.hpp"

using namespace ns_tcpclient;

void Usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " [IP] [PORT]\n" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    std::unique_ptr<TcpClient> client_ptr(new TcpClient(port, ip));
    client_ptr->Start();
}

制定協(xié)議

指定協(xié)議相關(guān)的邏輯將在Protocol.hpp中實現(xiàn)。

計算器的例子中,通信雙方是客戶端和服務端,它們都需要按照一套相同的規(guī)則通信,因此單獨將這一套相同的規(guī)則包裝。

客戶端和服務端都需要處理請求(request)和響應(response),因此它們都需要對數(shù)據(jù)進行序列化和反序列化。

  • 請求:
    • 序列化(Serialize):[客戶端生產(chǎn)請求]將結(jié)構(gòu)化的數(shù)據(jù)轉(zhuǎn)換為二進制序列,例如將形如1 + 1這兩個操作數(shù)和1個操作符以及2個空格作為一個字符串通過網(wǎng)絡傳輸給對端主機。
    • 反序列化(Deserialized):[服務端處理請求]將從網(wǎng)絡中獲取的序列化的二進制序列(字符串)按照規(guī)則,提取出運算需要的兩個操作數(shù)和一個操作符。
  • 響應:
    • 序列化(Serialize):[服務端生產(chǎn)響應]將得到的結(jié)果和錯誤碼轉(zhuǎn)換為二進制序列的字符串,通過網(wǎng)絡傳輸給請求的發(fā)出者。
    • 反序列化(Deserialized):[客戶端處理響應]將從網(wǎng)絡中獲取的結(jié)果和錯誤碼提取出來,然后根據(jù)錯誤碼和錯誤原因的映射情況處理。(錯誤碼一般和不同的錯誤原因有映射關(guān)系)如果沒有出現(xiàn)異常,客戶端則直接輸出結(jié)果。

實際上,為保證效率和穩(wěn)定性,一般會采用成熟的方案,而不會自己訂制協(xié)議。在此只是為了演示。

請求

序列化:

將形如1 + 1這樣的操作序列化為一個形如"1 + 1"這樣的字符串。其中包含2兩個空格,2個數(shù)字和1個操作符。為了方便后續(xù)操作,將空格和它的長度用宏定義。

序列化就是將這些操作數(shù)和操作符以及空格拼接成一個字符串。

反序列化:

和序列化的過程相反,從字符串中拆分,然后再轉(zhuǎn)換為操作數(shù)和操作符。

// Protocol.hpp
namespace ns_protocol
{
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
    // 請求
    class Request
    {
    public:
        // 序列化
        //  1  +  1 -> "1 + 1"
        // _x  + _y
        std::string Serialize()
        {
            std::string str;
            str += std::to_string(_x);
            str += SPACE;
            str += _op;
            str += SPACE;
            str += std::to_string(_y);
            return str;
        }
        // 反序列化
        // 1  +  1 <- "1 + 1"
        bool Deserialize(const std::string &str)
        {
            std::size_t left = str.find(SPACE);
            if (left == std::string::npos)
                return false;
            std::size_t right = str.rfind(SPACE);
            if (right == std::string::npos)
                return false;
            _x = atoi(str.substr(0, left).c_str());
            _y = atoi(str.substr(right + SPACE_LEN).c_str());
            if (left + SPACE_LEN > str.size())
                return false;
            else
                _op = str[left + SPACE_LEN];
            return true;
        }

    public:
        Request() {}
        Request(int x, int y, char op)
            : _x(x), _y(y), _op(op)
        {
        }
        ~Request() {}

    public:
        int _x;
        int _y;
        char _op;
    };
}

同樣地,它將在命名空間ns_protocol中被定義。

響應

響應的對象是請求,即處理請求。那么它應該包含兩個操作數(shù)和一個運算符,用_result保存結(jié)果。

除此之外,運算本身也是有一定前提的,因此它可能會產(chǎn)生錯誤,例如分母不能為零,模數(shù)不能為零。因此使用一個_code錯誤碼來標識錯誤類型,通常情況下,若干錯誤碼對應著不同的錯誤類型,當出現(xiàn)錯誤時從表中獲取錯誤類型并打印出來,以供調(diào)試和告知。這里的錯誤類型比較少,就直接用了。

// Protocol.hpp
namespace ns_protocol
{
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)    
	// 響應
    class Response
    {
    public:
        // 序列化
        std::string Serialize()
        {
            std::string str;
            str += std::to_string(_code);
            str += SPACE;
            str += std::to_string(_result);
            return str;
        }
        // 反序列化
        bool Deserialize(const std::string &str)
        {
            std::size_t pos = str.find(SPACE);
            if (pos == std::string::npos)
                return false;
            _code = atoi(str.substr(0, pos).c_str());
            _result = atoi(str.substr(pos + SPACE_LEN).c_str());
            return true;
        }

    public:
        Response() {}
        Response(int result, int code, int x, int y, char op)
            : _result(result), _code(code), _x(x), _y(y), _op(op)
        {
        }
        ~Response() {}

    public:
        int _result; // 結(jié)果
        int _code;   // 錯誤碼

        int _x;
        int _y;
        char _op;
    };

    // 發(fā)送數(shù)據(jù)
    void Send(int sockfd, const std::string &s)
    {
        if (send(sockfd, s.c_str(), s.size(), 0) < 0)
        {
            logMessage(FATAL, "send error, %d:%s", errno, strerror(errno));
            exit(5);
        }
        logMessage(DEBUG, "send %s", strerror(errno));
    }
    // 接收數(shù)據(jù)
    std::string Recv(int sockfd)
    {
        char inputBuffer[1024];
        if (recv(sockfd, inputBuffer, sizeof(inputBuffer), 0) < 0)
        {
            logMessage(FATAL, "recv error, %d:%s", errno, strerror(errno));
            exit(6);
        }
        logMessage(DEBUG, "recv %s", strerror(errno));
        return inputBuffer;
    }
}

發(fā)送和接收數(shù)據(jù)

服務端處理獲取請求,需要從網(wǎng)絡中獲?。环斩颂幚碚埱笠院?,可能需要將結(jié)果回傳給客戶端,因此需要將數(shù)據(jù)發(fā)送到網(wǎng)絡中??蛻舳送?。

發(fā)送和接收數(shù)據(jù)的邏輯是和網(wǎng)絡相關(guān)的,它可以放在Sock.hpp中,但由于服務端中處理請求的邏輯可能并不是被類包裝的,也就沒有Sock類的成員,因此也就無法直接通過成員調(diào)用被封裝在對象中的發(fā)送和接收數(shù)據(jù)的邏輯。

因此將發(fā)送和接收數(shù)據(jù)的邏輯放在了Protocol.hpp中,不被ns_protocol命名空間限制。

// 發(fā)送數(shù)據(jù)
void Send(int sockfd, const std::string &s)
{
    if (send(sockfd, s.c_str(), s.size(), 0) < 0)
    {
        logMessage(FATAL, "send error, %d:%s", errno, strerror(errno));
        exit(5);
    }
    logMessage(DEBUG, "send %s", strerror(errno));
}
// 接收數(shù)據(jù)
std::string Recv(int sockfd)
{
    char inputBuffer[1024];
    if (recv(sockfd, inputBuffer, sizeof(inputBuffer), 0) < 0)
    {
        logMessage(FATAL, "recv error, %d:%s", errno, strerror(errno));
        exit(6);
    }
    logMessage(DEBUG, "recv %s", strerror(errno));
    return inputBuffer;
}

實際上真正在執(zhí)行接收和發(fā)送數(shù)據(jù)的操作只有幾行,其他部分都是差錯處理和打日志的操作。

需要注意響應Response和請求Request中序列化和反序列化的參數(shù)及返回值。

計算邏輯

服務端首先從網(wǎng)絡中獲被客戶端序列化的字符串,然后定義一個Request類型的對象req,使用它來將其反序列化。

接著定義一個Response類型的對象resp,獲取處理請求后的結(jié)果,序列化以后發(fā)送到網(wǎng)絡中。

計算的邏輯很簡單,就是通過對象中_op的類型來進行不同的計算。需要注意的是calculatorHelper的參數(shù)是Request類型對象,返回值是Response類型對象。

static Response calculatorHelper(const Request &req)
{
    Response resp(0, 0, req._x, req._y, req._op);
    switch (req._op)
    {
    case '+':
        resp._result = req._x + req._y;
        break;
    case '-':
        resp._result = req._x - req._y;
        break;
    case '*':
        resp._result = req._x * req._y;
        break;
    case '/':
        if (req._y == 0) resp._code = 1;
        else resp._result = req._x / req._y;
        break;
    case '%':
        if (req._y == 0) resp._code = 2;
        else resp._result = req._x % req._y;
        break;
    default:
        resp._code = 3;
        break;
    }
    return resp;
}
void calculator(int sockfd)
{
    while (1)
    {
        std::string str = Recv(sockfd);
        Request req;
        req.Deserialize(str);                   // 網(wǎng)絡->反序列化
        Response resp = calculatorHelper(req);  // 處理請求
        std::string respStr = resp.Serialize(); // 序列化->網(wǎng)絡
        Send(sockfd, respStr);                  // 回傳數(shù)據(jù)
    }
}

測試

回顧之前定義的BindService()函數(shù),它的作用是將函數(shù)的地址push_back到數(shù)組中,以供線程調(diào)用。

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::unique_ptr<TcpServer> server_ptr(new TcpServer(atoi(argv[1])));
    server_ptr->BindService(calculator);
    server_ptr->Start();
}

從標準輸入獲取結(jié)構(gòu)化數(shù)據(jù):

// CalClient.hpp
namespace ns_tcpclient
{
    class TcpClient
    {		
		void Start()
        {
            bool quit = false;
            while (!quit)
            {
                int x, y;
                char op;
                std::cin >> x >> op >> y;
                Request req(x, y, op);
                // ...
            }
        }
    };
}

在這里,calculator就是被push_back的函數(shù)。

簡單測試幾個功能,注意到當除數(shù)為0時,錯誤碼是剛剛設置的1。

存在的問題

  1. 當前的邏輯當客戶端斷開連接時,服務端會直接退出。
  2. 服務端獲取序列化的字符串不應該為空,否則后續(xù)的Send()函數(shù)會出現(xiàn)問題。例如常見的問題是一端在寫入時,另一端直接關(guān)閉了。

解決辦法是增加差錯處理邏輯:

  • 忽略SIGPIPE信號
  • 當服務端沒有獲取到數(shù)據(jù)時,就直接break退出。
// Protocol.hpp
// 接收數(shù)據(jù)
bool Recv(int sockfd, std::string *out)
{
    char inBuffer[1024];
    ssize_t s = recv(sockfd, inBuffer, sizeof(inBuffer) - 1, 0);
    if (s > 0)
    {
        inBuffer[s] = 0;
        *out += inBuffer;
    }
    else if (s == 0)
    {
        logMessage(FATAL, "client quit %d:%s", errno, strerror(errno));
        return false;
    }
    else
    {
        logMessage(FATAL, "recv %d:%s", errno, strerror(errno));
        return false;
    }
    logMessage(DEBUG, "recv %s", strerror(errno));
    return true;
}

Recv()增加一個輸出型參數(shù),返回值改為bool類型。

// Calserver.cc
void calculator(int sockfd)
{
    while (1)
    {
        std::string str;
        bool ret = Recv(sockfd, &str);
        if (ret)
        {
            Request req;
            req.Deserialize(str);                   // 網(wǎng)絡->反序列化
            Response resp = calculatorHelper(req);  // 處理請求
            std::string respStr = resp.Serialize(); // 序列化->網(wǎng)絡
            Send(sockfd, respStr);                  // 回傳數(shù)據(jù)
        }
        else break;
    }
}
// CalClient.hpp
void Start()
{
    bool quit = false;
    while (!quit)
    {
        Request req;
        std::cout << "請輸入>>>";
        std::cin >> req._x >> req._op >> req._y;
        std::string s = req.Serialize(); // 序列化->網(wǎng)絡
        Send(_sockfd, s);
        std::string buffer;
        while (1)
        {
            bool ret = Recv(_sockfd, &buffer);
            if (!ret)
            {
                quit = true;
                break;
            }
            Response resp;
            resp.Deserialize(buffer); // 反序列化->主機
            std::string err;
            switch (resp._code)
            {
                case 1:
                    err = "除0錯誤";
                    break;
                case 2:
                    err = "模0錯誤";
                    break;
                case 3:
                    err = "非法操作";
                    break;
                default:
                    std::cout << "code: " << resp._code << std::endl;
                    std::cout << "result: " << resp._result << std::endl;
                    break;
            }
            if (!err.empty()) std::cerr << err << std::endl;
            break;
        }
    }
}

現(xiàn)在就從代碼邏輯上解決了服務端在讀取時,如果讀取失敗就直接退出。但是沒有解決一方正在寫入時對方把連接關(guān)閉的問題。一般經(jīng)驗:服務端在編寫時,要有較為嚴謹?shù)呐袛噙壿?,一般服務器都要忽略SIGPIPE信號,防止非法寫入的問題。

TCP服務端

TCP是面向字節(jié)流的,從實現(xiàn)上可以簡單地理解為使用字符串。不同于UDP,前者面向數(shù)據(jù)包,相當于發(fā)快遞,是客戶端發(fā)一次,服務端接收一次(調(diào)用一次recvfrom函數(shù)),因此讀取的是完整的Request對象。在TCP協(xié)議的服務端中,可能一個服務端會同時等待多個Request請求,然后一次性讀取多個Request對象,問題在TCP服務端獲取Request時,如何保證數(shù)據(jù)傳輸?shù)耐暾?/mark>。

TCP和UDP是「傳輸控制」協(xié)議。在TCP協(xié)議中,客戶端和服務端調(diào)用函數(shù)發(fā)送或接收數(shù)據(jù)時,只是將數(shù)據(jù)拷貝到緩沖區(qū),并未真正交給函數(shù)處理。 這是因為TCP是流式傳輸,沒有邊界,需要根據(jù)窗口大小和網(wǎng)絡狀況來確定何時發(fā)送數(shù)據(jù)。

所以不能認為調(diào)用send()和recvfrom()等函數(shù)是將數(shù)據(jù)發(fā)送/接收到網(wǎng)絡或?qū)Ψ街鳈C中。

一般來說,當緩沖區(qū)滿了或者超時了,TCP就會發(fā)送數(shù)據(jù)。當收到對方的確認信息或者重置信息,TCP就會接收數(shù)據(jù)。

結(jié)論:

  1. IO函數(shù)的本質(zhì)都是拷貝函數(shù),因為一般IO接口都會有一個用戶自定義的緩沖區(qū)作為參數(shù)。
  2. 數(shù)據(jù)何時發(fā)送、每次發(fā)送多少數(shù)據(jù),以及差錯處理等細節(jié),都不是應用層應該關(guān)心的,這取決于「傳輸控制」協(xié)議即TCP協(xié)議決定。
  3. 由于緩沖區(qū)的存在,發(fā)送數(shù)據(jù)的次數(shù)和接收次數(shù)沒有任何關(guān)系。(例如管道中寫端快讀端慢,兩端讀寫的次數(shù)不一定是相同的)

雖然如此,但是具有這些性質(zhì)并不能保證讀取的完整性。

保證報文的完整性

Protocol.hpp中的Recv()函數(shù)的返回值不再是一個字符串,而是以一個輸出型參數(shù)作為返回值,只返回bool類型以表示成功與否。這么做可以讓這個函數(shù)值負責接收數(shù)據(jù),CalServer.cc中的計算器函數(shù)calculator()函數(shù)使用一個指針類型的變量作為輸出型參數(shù)獲取處理以后的數(shù)據(jù)。如何在此函數(shù)中解析和處理數(shù)據(jù)。

像之前Recv()的邏輯,無法保證讀到inbuffer緩沖區(qū)中的數(shù)據(jù)是一個完整的請求。

因此需要在原有的基礎上對協(xié)議進一步定制。

定制協(xié)議(劃分報文)

在之前寫過的TCP socket中,TCP的客戶端-服務端(c-s)都無法保證報文的完整性,因此在協(xié)議中增加一個成員length,表示報文的長度。因此,報文的信息就包含了:報文長度+報文內(nèi)容。

形如"length\r\nx_ op_ y_\r\n"中的'\r\n'(宏SEP)是為了將length屬性字段和其他屬性字段劃分,是通過特殊字符區(qū)分屬性的方法。因為有效信息的長度本身是變化的,因此這個屬性length的值也可能會變,因此要有一個方法,使得機器無論如何都能得到length字段。

長度是一個整數(shù),內(nèi)部不會出現(xiàn)特殊符號,那么以length分隔的字段就是數(shù)據(jù)本身了,即正文。那么length即使協(xié)議報頭,后面的內(nèi)容就是有效載荷。

為什么使用\r\n劃分?

  • \r\n的可讀性比較好,如果不加的話也可以。

注意:length不能包含\r\n(2字節(jié))

這樣就通過多個標記字符'\r\n'(當做一個整體)將報文劃分為2個區(qū)域:有效數(shù)據(jù)的長度+有效數(shù)據(jù)。這樣當主機從網(wǎng)絡中獲取到報文時,通過這個標記字符就能取出它們,并驗證長度是否正確,這樣就能保證報文的完整性。為了方便使用,用宏定義它們:

#define SEP "\r\n"
#define SEP_LEN strlen(SEP)

注意不能用sizeof計算它的長度,否則會包含最后的’\0’。

編碼

編碼實際上就是將一個完整的報文用一個長度字段的報頭包裝起來,使得有效載荷是完整的。

實際上就是返回一個字符串,這個字符串的格式是這樣的:"length\r\nx_ op_ y_\r\n",編碼的格式就是簡單的字符串拼接,中間的有效數(shù)據(jù)(有效載荷)序列化時處理好了,現(xiàn)在要拼接的只有一個長度length和兩個'\r\n'

// 編碼
std::string Encode(std::string &s)
{
    std::string package = std::to_string(s.size());
    package += SEP;
    package += s;
    package += SEP;
    return package;
}

為什么在后面也要加一個\r\n呢?

因為可能一次發(fā)送的報文不止一個,例如:

length\r\nx_ op_ y_\r\nlength\r\nx_ op_ y_\r\n

這樣就能和其他報文區(qū)分了。

解碼

解碼的過程和編碼相反,是將字符串拆分的過程。對應的,是去除長度報頭,提取被包裝的有效載荷的過程。

Recv()只接受數(shù)據(jù),Decode()的作用就是根據(jù)定制的協(xié)議,提取數(shù)據(jù)。步驟:

  1. 用find()提取length字段,如果它不存在則返回空串。
  2. 驗證正文長度是否與length相等。
  3. 截取有效內(nèi)容,去除無效內(nèi)容。只要只有一個完整的報文,就可以提取。
// 解碼
// "length\r\nx_ op_ y_\r\n"
std::string Decode(std::string &buffer)
{
    // 查找length字段
    std::size_t pos = buffer.find(SEP);
    if (pos == std::string::npos)
        return "";
    // 驗證length和有效數(shù)據(jù)長度是否相等
    int size = atoi(buffer.substr(0, pos).c_str());
    int surplus = buffer.size() - pos - 2 * SEP_LEN;
    if (surplus >= size)  // 至少有一個完整的報文,提取
    {
        buffer.erase(0, pos + SEP_LEN);
        std::string s = buffer.substr(0, size);
        buffer.erase(0, size + SEP_LEN);
        return s;
    }
    else
        return "";
}

修改

calculator()TcpServer::Start()中增加添加和取出報頭的邏輯。

// CalClient.hpp
void Start()
{
    bool quit = false;
    std::string buffer;
    while (!quit)
    {
        // 生產(chǎn)請求
        Request req;
        std::cout << "請輸入>>> "; // 從標準輸入獲取數(shù)據(jù)
        std::cin >> req._x >> req._op >> req._y;
        // 序列化->網(wǎng)絡
        std::string s = req.Serialize();
        // 添加長度報頭
        s = Encode(s);
        // 發(fā)送數(shù)據(jù)到網(wǎng)絡中(服務器)
        Send(_sockfd, s);
        // 讀取
        while (1)
        {
            bool ret = Recv(_sockfd, &buffer);
            if (!ret)
            {
                quit = true;
                break;
            }
            std::string package = Decode(buffer); // 協(xié)議解析(提取有效載荷)
            if (package.empty())                  // 得到的字符串為空,進行下一次讀取
                continue;                         // 保證了讀取報文的安全性
            Response resp;
            resp.Deserialize(package); // 反序列化->主機(注意是有效載荷)
            std::string err;
            switch (resp._code) // 打印錯誤信息
            {
                case 1:
                    err = "除0錯誤";
                    break;
                case 2:
                    err = "模0錯誤";
                    break;
                case 3:
                    err = "非法操作";
                    break;
                default:
                    std::cout << "result: " << resp._result << std::endl;
                    break;
            }
            if (!err.empty())
                std::cerr << err << std::endl;
            break;
        }
    }
}
// CalServer.cc
void calculator(int sockfd)
{
    std::string inbuffer;
    while (1)
    {

        bool ret = Recv(sockfd, &inbuffer); // 從網(wǎng)絡中讀取請求
        if (!ret)
            break;

        std::string package = Decode(inbuffer); // 協(xié)議解析(提取有效載荷)
        if (package.empty())                    // 得到的字符串為空,進行下一次讀取
            continue;                           // 保證了讀取報文的安全性
        logMessage(NORMAL, "inbuffer: %s", package.c_str());
        Request req;
        req.Deserialize(package);               // 網(wǎng)絡->反序列化(注意要使用有效載荷)
        Response resp = calculatorHelper(req);  // 處理請求
        std::string respStr = resp.Serialize(); // 序列化->網(wǎng)絡
        respStr = Encode(respStr);              // 添加長度報頭
        Send(sockfd, respStr);                  // 回傳數(shù)據(jù)
    }
}

測試

簡單測試一下:

守護進程版本

守護進程(Daemon)是一種在后臺運行的特殊進程,它不屬于任何終端,也不受用戶的交互控制,通常用來執(zhí)行一些系統(tǒng)服務或應用程序。

像上面包括之前博文實現(xiàn)的UDP和TCP服務器,都是在前臺運行的進程(即運行起來以后光標一直在閃動,因為需要用無限循環(huán)使邏輯不斷運行)。關(guān)于守護進程:

  1. 每當一個用戶登錄計算機時,系統(tǒng)會自動創(chuàng)建一個新的shell會話,通常是bash、zsh、fish等。那么它(這個窗口)就是一個前臺進程(如果你試著kill掉某個bash進程,你的窗口就會被關(guān)閉)。每一個shell會話,只允許存在一個前臺進程,而可以用若干個后臺進程。用戶在這個窗口中執(zhí)行的各種命令的父進程都是shell會話進程(如bash)。
  2. 使用PID標識進程ID,使用PPID標識父進程ID,使用PGID標識進程組ID。
  3. 可以使用|管道同時啟動多個進程,這些進程是兄弟關(guān)系,它們的父進程是會話進程(例如bash),它作為前臺進程和用戶進行交互。這些兄弟進程可以使用匿名管道進行通信。
  4. 被同時啟動的進程總體被稱為進程組,通常第一個進程被作為這個進程的組長進程。這個進程組提供的服務就稱之為會話。

每當用戶登錄計算機,OS會為用戶創(chuàng)建一個新的會話,以提供相應服務;如果用戶退出登錄(如注銷),理論上OS會釋放所用對應的資源:

  • 前臺服務:(有可能)退出(取決于OS)。
  • 后臺服務:后臺服務不屬于任何終端,這種服務一般被期望用于長期服務的進程,那么它會自成一個會話,不被shell會話關(guān)閉而受影響,稱之為“守護進程”。

如何讓進程(組)自成一個會話?

可以使用setsid命令。它不僅會創(chuàng)建一個新的進程組,還會在一個新的會話中啟動命令。例如:

setsid COMMAND

這會在一個新的會話和進程組中運行COMMAND??梢允褂?code>ps -o pid,sid,pgid,comm命令來查看進程的會話ID(SID)和進程組ID(PGID)。

setsid()要成功被執(zhí)行,有什么前提?

  • 調(diào)用進程不能是一個進程組的組長進程。

如果調(diào)用進程是一個進程組的組長進程,setsid()會返回-1,并設置errno為EPERM(表示操作不被允許)。這是為了防止一個進程組的組長進程將自己放到一個新的會話中,而其他進程仍然留在原來的會話中,這樣會破壞會話和進程組的兩級層次結(jié)構(gòu)。為了確保setsid()能夠成功,可以先調(diào)用fork(2)并讓父進程退出,而子進程(一定不是一個進程組的組長進程)調(diào)用setsid()。

實現(xiàn)

下面將在進程中調(diào)用該函數(shù),讓它自己成為一個守護進程:

#pragma once

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void MyDaemon()
{
    // 1. 忽略信號
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);

    // 2. 不要讓自己成為組長
    if (fork() > 0)
        exit(0);
    // 3. 調(diào)用setsid
    setsid();
    // 4. 將標準輸入,標準輸出和標準錯誤的重定向到該路徑
    // 使得守護進程不能直接向顯示器打印消息
    int devnull = open("/dev/null", O_RDONLY | O_WRONLY);
    if (devnull > 0)
    {
        dup2(0, devnull);
        dup2(1, devnull);
        dup2(2, devnull);
        close(devnull);
    }
}

CalServer.cc中調(diào)用它:

int main(int argc, char *argv[])
{
    // ...
    MyDaemon();
    
    std::unique_ptr<TcpServer> server_ptr(new TcpServer(atoi(argv[1])));
    server_ptr->BindService(calculator);
    server_ptr->Start();
}

守護進程為什么要將標準輸入,標準輸出和標準錯誤的重定向?

  • 守護進程通常不需要和用戶交互,所以關(guān)閉標準輸入可以防止它們被意外地阻塞在等待用戶輸入的地方。
  • 守護進程可能會從父進程繼承一些打開的文件描述符,這些文件描述符可能是不需要的或者占用了系統(tǒng)資源。關(guān)閉所有不必要的文件描述符,包括標準輸入,標準輸出和標準錯誤,可以釋放這些資源,并避免對這些文件的誤操作。
  • 守護進程可能會產(chǎn)生一些輸出或者錯誤信息,如果不重定向到合適的地方,這些信息可能會丟失或者干擾其他程序。重定向標準輸出和標準錯誤到一個日志文件或者/dev/null,可以方便地記錄或者忽略這些信息。

總的來說,不能向顯示器打印消息的原因是它已經(jīng)是獨立的進程(組),和當前的會話(終端)已經(jīng)沒有關(guān)系了。一旦打印消息,就會被暫停或者被終止。

簡單測試一下:

可以看到,這個進程自成會話,表現(xiàn)是PPID=1,PID和PGID相同,TTY=?。守護進程是孤兒進程的一種,守護進程自成會話。這樣就能讓服務端在后臺運行,關(guān)閉終端窗口也不影響服務,這樣就能無時無刻的為客戶端提供服務了。而且后臺進程不會影響當前終端窗口的其他任務的執(zhí)行,因為含有無限循環(huán)的前臺進程會阻塞I/O,它會一直占用CPU資源,導致其他進程無法得到調(diào)度。

在同一shell會話中也能啟動客戶端進程測試:

json版本

json介紹

JavaScript Object Notation,是一種輕量級的數(shù)據(jù)交換格式,使用人類可讀的文本來存儲和傳輸由屬性值對和數(shù)組(或其他可序列化的值)組成的數(shù)據(jù)對象。JSON是一種語言無關(guān)的數(shù)據(jù)格式,它源自于JavaScript,但是許多現(xiàn)代編程語言都包含了生成和解析JSON格式數(shù)據(jù)的代碼。JSON文件使用.json作為擴展名,但不是強制的。

JSON有以下特點:

  • 數(shù)據(jù)以鍵值對的形式表示,類似于JavaScript對象的屬性。
  • 數(shù)據(jù)由逗號分隔,花括號保存對象,方括號保存數(shù)組。
  • JSON是“自描述”的,易于理解和使用。
  • JSON可以用于存儲和交換各種類型的數(shù)據(jù),如數(shù)字、布爾值、字符串、日期、對象、數(shù)組等。

JSON有以下用途:

  • JSON通常用于從服務器向網(wǎng)頁發(fā)送數(shù)據(jù)。

  • JSON可以用于在不同的編程語言之間進行數(shù)據(jù)交換。

  • JSON可以用于存儲配置文件、日志文件、用戶偏好等信息。

  • JSON可以用于構(gòu)建復雜的數(shù)據(jù)結(jié)構(gòu),如樹、圖、地圖等。

  • JSON數(shù)據(jù)格式比較簡單,易于讀寫,格式都是壓縮的,占用帶寬小。

  • JSON易于解析,客戶端JavaScript可以簡單地通過eval()函數(shù)進行JSON數(shù)據(jù)的讀取。

  • JSON格式能夠直接為服務器端代碼使用,大大簡化了服務器端和客戶端的代碼開發(fā)量,但是完成的任務不變,且易于維護。

優(yōu)點

相比于手動對字符串encode和手動decode,后者有以下缺點:

  • 需要編寫額外的代碼,增加了開發(fā)和維護的成本和復雜度。
  • 容易出錯,比如忘記轉(zhuǎn)義特殊字符,或者解析時沒有考慮到邊界情況。
  • 對字符串encode和手動decode可能導致數(shù)據(jù)類型的丟失或不一致,比如數(shù)字、布爾值、日期等在字符串中無法區(qū)分。
  • 對字符串encode和手動decode可能導致數(shù)據(jù)結(jié)構(gòu)的不清晰或不規(guī)范,比如數(shù)組、對象、嵌套等在字符串中無法直觀地表示。

實際上,之前手動寫的協(xié)議是一個很粗略的協(xié)議,實際上用戶只要輸入兩個操作數(shù)和一個操作符,中間的空格理論上可以是任意個,諸如此類的問題還有很多。因此實際應用中會使用成熟的協(xié)議。

實現(xiàn)

C++沒有內(nèi)置的json庫,但是有很多第三方的json庫可以使用,如RapidJSON, JsonCpp, sonic-cpp等。安裝JsonCpp:

sudo yum install jsoncpp-devel 

驗證安裝是否成功:

在編譯選項中要增加-ljsoncpp鏈接該第三方庫。

用法

注意頭文件的包含。

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main()
{
    std::string a = "hello";
    std::string b = "world";
    char c = '!';
    std::string d = "用變量賦值";

    Json::Value root;
    root["a"] = a;
    root["b"] = b;
    root["c"] = c;
    root["d"] = d;

    Json::Value son;
    son["aa"] = 233;
    son["str"] = "hi world(直接插入)";

    root["son(嵌套)"] = son;

    Json::StyledWriter swriter; // 格式化輸出(適合人類閱讀)
    Json::FastWriter fwriter;   // 無格式輸出(適合機器讀取)

    std::string sstr1 = swriter.write(root);
    std::string fstr1 = fwriter.write(root);
    std::cout << "格式化輸出:" << std :: endl << sstr1;
    std::cout << "無格式輸出:" << std :: endl << fstr1;

    std::string sstr2 = swriter.write(son);
    std::string fstr2 = fwriter.write(son);
    std::cout << "格式化輸出:" << std :: endl << sstr2;
    std::cout << "無格式輸出:" << std :: endl << fstr2;

}
  • 初始化鍵值對主要用兩種辦法,一是先初始化各種類型的變量,然后賦值給鍵(key);二是直接用值(value)賦值給鍵(key)。后者更方便。

  • root["son"] = son表示將son對象嵌套進root對象中。

  • 其次轉(zhuǎn)換為字符串有兩種格式,一是格式化,比較美觀,適合人類閱讀,方便調(diào)試;而是無格式,比較緊湊,能節(jié)省空間,提高傳輸效率。

例如:

實現(xiàn)

下面修改RequestResponse中序列化和反序列化的邏輯(Protocol.hpp):

// Request::Serialize
std::string Serialize()
{
    Json::Value root;
    root["x"] = _x;
    root["y"] = _y;
    root["op"] = _op;
    Json::FastWriter fwriter;
    return fwriter.write(root);
}
// Request::Deserialize
bool Deserialize(const std::string &str)
{
    Json::Value root;
    Json::Reader reader;
    reader.parse(str, root);
    _x = root["x"].asInt();
    _y = root["y"].asInt();
    _op = root["op"].asInt();
    return true;
}
// Response::Serialize
std::string Serialize()
{
    Json::Value root;
    root["code"] = _code;
    root["result"] = _result;
    root["xx"] = _x;
    root["yy"] = _y;
    root["zz"] = _op;
    Json::FastWriter fwriter;
    return fwriter.write(root);
}
// Response::Deserialize
bool Deserialize(const std::string &str)
{
    Json::Value root;
    Json::Reader reader;
    reader.parse(str, root);
    _code = root["code"].asInt();
    _result = root["result"].asInt();
    _x = root["xx"].asInt();
    _y = root["yy"].asInt();
    _op = root["zz"].asInt();
    return true;
}

這樣就能保證每次獲取的數(shù)據(jù)是一個完整的Json字節(jié)流。

簡單測試一下(為了方便測試,暫時不作為守護進程):

使用成熟的協(xié)議,能很方便地擴充或修改協(xié)議,可以在數(shù)據(jù)中包含x、y和op,那么就不用在函數(shù)內(nèi)部使用臨時字符串保存數(shù)據(jù)了,而Json數(shù)據(jù)本身就攜帶了這些信息。

例如日志記錄的Json字符串:

[NORMAL] [1685870721] inbuffer: {"op":42,"x":55,"y":2}

源代碼文章來源地址http://www.zghlxwxcb.cn/news/detail-471253.html

到了這里,關(guān)于認識協(xié)議【網(wǎng)絡基礎】的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權(quán),不承擔相關(guān)法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務器費用

相關(guān)文章

  • 紅藍攻防基礎-認識紅藍紫,初步學習網(wǎng)絡安全屬于那個隊?

    紅藍攻防基礎-認識紅藍紫,初步學習網(wǎng)絡安全屬于那個隊?

    紅隊,也叫藍軍是指網(wǎng)絡實戰(zhàn)攻防演練中的攻擊一方,以發(fā)現(xiàn)系統(tǒng)薄弱環(huán)節(jié)、提升系統(tǒng)安全性為目標,一般會針對目標單位的從業(yè)人員以及目標系統(tǒng)所在網(wǎng)絡內(nèi)的軟件、硬件設備執(zhí)行多角度、全方位、對抗性的混合式模擬攻擊,通過技術(shù)手段實現(xiàn)系統(tǒng)提權(quán)、控制業(yè)務、獲取數(shù)

    2024年02月13日
    瀏覽(33)
  • 【計算機網(wǎng)絡】網(wǎng)絡基礎--協(xié)議/網(wǎng)絡協(xié)議/網(wǎng)絡傳輸流程/地址管理

    【計算機網(wǎng)絡】網(wǎng)絡基礎--協(xié)議/網(wǎng)絡協(xié)議/網(wǎng)絡傳輸流程/地址管理

    網(wǎng)絡的發(fā)展分為一下幾個階段: 獨立模式: 計算機之間相互獨立: 此時計算機之間是相互獨立的,每個人在執(zhí)行任務的時候是獨立的,需要等待前一個將任務完成之后,自己才能進行執(zhí)行任務,是串行執(zhí)行的,效率很低。 網(wǎng)絡互聯(lián): 多臺計算機連接在一起, 完成數(shù)據(jù)共享:

    2024年02月03日
    瀏覽(31)
  • 網(wǎng)絡基礎與網(wǎng)絡協(xié)議

    網(wǎng)絡基礎與網(wǎng)絡協(xié)議

    抽象語言——電腦(加工)——二進制——抽象語言 應用層: 跟人進行交互(人機交互)——我們給他輸入抽象語言——編碼——后臺程序 表示層: 將“編碼”轉(zhuǎn)化為電腦可以識別的二進制 介質(zhì)訪問控制層(MAC): MAC地址是網(wǎng)卡決定的,是固定的。 物理層: 人類最早的

    2024年02月22日
    瀏覽(23)
  • 網(wǎng)絡基礎知識:了解網(wǎng)絡協(xié)議的組成和常見的網(wǎng)絡協(xié)議

    網(wǎng)絡基礎知識,了解網(wǎng)絡協(xié)議的組成和常見的網(wǎng)絡協(xié)議 1、協(xié)議及協(xié)議棧的基本概念 1.1、什么是協(xié)議 協(xié)議是網(wǎng)絡中計算機或設備之間進行通信的一系列規(guī)則的集合。常用協(xié)議有IP、TCP、HTTP、POP3、SMTP等。 1.2、什么是協(xié)議棧 在網(wǎng)絡中,為了完成通信,必須使用多層上的多種協(xié)

    2024年02月07日
    瀏覽(31)
  • 網(wǎng)絡基礎:通信原理及網(wǎng)絡協(xié)議

    網(wǎng)絡基礎:通信原理及網(wǎng)絡協(xié)議

    集線器:一個口收到的信號原封不動地轉(zhuǎn)發(fā)給其他所有口,其他口上的設備自己決定是否接收信號。有點類似廣播,但必廣播更純粹。由于hub只是單純地轉(zhuǎn)發(fā),所以工作在物理層(OSI第一層) 類似于廣播模式,純硬件 網(wǎng)橋:工作在數(shù)據(jù)鏈路層(OSI第二層)。以太網(wǎng)中,數(shù)據(jù)

    2024年02月21日
    瀏覽(25)
  • 計算機網(wǎng)絡基礎--網(wǎng)絡層協(xié)議分析實驗

    計算機網(wǎng)絡基礎--網(wǎng)絡層協(xié)議分析實驗

    一、實驗目的 1、掌握網(wǎng)絡數(shù)據(jù)包嗅探器Wireshark的使用; 2、理解IP協(xié)議,掌握IP分組格式和IP分片; 3、理解ICMP協(xié)議。 二、實驗內(nèi)容 (主要包括實驗設計、實驗環(huán)境、實驗步驟、測試數(shù)據(jù)和實驗結(jié)果) 1、通過使用ping命令,截獲報文,分析IP數(shù)據(jù)報的格式和IP分片; 2、通過使

    2024年02月04日
    瀏覽(30)
  • 【傳輸層】網(wǎng)絡基礎 -- UDP協(xié)議 | TCP協(xié)議

    【傳輸層】網(wǎng)絡基礎 -- UDP協(xié)議 | TCP協(xié)議

    端口號(Port)標識了一個主機上進行通信的不同的應用程序 在TCP/IP協(xié)議中,用 “源IP”, “源端口號”, “目的IP”, “目的端口號”, “協(xié)議號” 這樣一個五元組來標識一個通信(可以通過 netstat -n 查看) 0 - 1023:知名端口號,HTTP,F(xiàn)TP,SSH等這些廣為使用的應用層協(xié)議,他

    2024年02月09日
    瀏覽(54)
  • Linux 網(wǎng)絡基礎(1)基礎知識、IP地址、端口、協(xié)議、網(wǎng)絡字節(jié)序

    Linux 網(wǎng)絡基礎(1)基礎知識、IP地址、端口、協(xié)議、網(wǎng)絡字節(jié)序

    網(wǎng)絡發(fā)展背景: 網(wǎng)絡的劃分:局域網(wǎng)(覆蓋范圍在1000m以內(nèi))、城域網(wǎng)(覆蓋范圍在20km以內(nèi))、廣域網(wǎng)(更大范圍) 組網(wǎng)方式:以太網(wǎng)、令牌環(huán)網(wǎng).... 日常名詞:互聯(lián)網(wǎng),因特網(wǎng)----說的是一個網(wǎng)絡,就是國際化的廣域網(wǎng) 網(wǎng)卡:實現(xiàn)數(shù)字信號與電信號之間的轉(zhuǎn)換 中繼器:信號

    2024年02月05日
    瀏覽(37)
  • 網(wǎng)絡基礎 二 OSI七層模型與網(wǎng)絡協(xié)議

    網(wǎng)絡基礎 二 OSI七層模型與網(wǎng)絡協(xié)議

    OSI/RM------開放式系統(tǒng)互聯(lián)參考模型 數(shù)據(jù)鏈路層:介質(zhì)訪問控制層MAC+邏輯鏈路控制層LLC 邏輯鏈路控制層LLC:對數(shù)據(jù)驚醒校驗,只保障數(shù)據(jù)完整性;同時增加FCS(校驗核),校驗數(shù)據(jù)完整性。 應用層:抽象語言----編碼 表示層:編碼---二進制 網(wǎng)絡層:IP 互聯(lián)網(wǎng)協(xié)議 ? 數(shù)據(jù)鏈路

    2024年02月19日
    瀏覽(22)
  • 信息網(wǎng)絡協(xié)議基礎_緒論

    信息網(wǎng)絡協(xié)議基礎_緒論

    電話交換網(wǎng) 數(shù)據(jù)報交換 虛電路交換 如何理解間隙性? 斷斷續(xù)續(xù)的連接:間隙性指的是網(wǎng)絡連接的斷斷續(xù)續(xù),即網(wǎng)絡中的節(jié)點不是始終相互連接,而是時有時無。 等待傳輸機會:在DTN中,數(shù)據(jù)可能需要在網(wǎng)絡節(jié)點上等待一段時間,直到下一個傳輸機會出現(xiàn)。例如,一個在深

    2024年02月19日
    瀏覽(16)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包