独自のアプリケーション層プロトコルをカスタマイズするにはどうすればよいですか? |バイトストリーム指向|バイトストリームはスティッキーパケットの問題をどう解決するのか?バイトのストリームをデータグラムに分割するにはどうすればよいでしょうか?

序文

那么这里博主先安利一些干货满满的专栏了!

首先是博主的高质量博客的汇总,这个专栏里面的博客,都是博主最最用心写的一部分,干货满满,希望对大家有帮助。

高质量干货博客汇总https://blog.csdn.net/yu_cblog/category_12379430.html?spm=1001.2014.3001.5482

プロジェクトのGITHUBアドレス

Web 電卓 - シリアル化と逆シリアル化 - プロトコルのカスタマイズicon-default.png?t=N6B9https://github.com/Yufccode/BitCode/tree/main/Linux/%E4%BB%A3%E7%A0%81/0226%E7%BD% 91%E7% BB%9C%E8%AE%A1%E7%AE%97%E5%99%A8-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C %E5%8F %8D%E5%BA%8F%E5%88%97%E5%8C%96-%E5%8D%8F%E8%AE%AE%E5%AE%9A%E5%88%B6

バイトストリームとデータグラムとは何ですか

コンピュータ ネットワーク理論では、バイト ストリームとは何ですか、データグラムとは何ですか。TCP や UDP との関係は何ですか。また、バイト ストリームとデータグラムの関係は何ですか?

バイト ストリームは、連続した無制限のデータ ストリームです。データは、明確なグループ化や境界のない連続したバイトのシーケンスとして扱われます。バイト ストリームでは、データは送信された順序で送信され、受信側は同じ順序でデータを受信します。バイト ストリームは、信頼性が高く、順序付けられたエラー検出および修正ベースのデータ転送を提供する接続指向のトランスポートです。TCP(Transmission Control Protocol)は、バイトストリーム伝送を使用するプロトコルです。

データグラムは、データを送信するための個別の制限された方法です。データを固定サイズのパケットに分割し、それぞれに独自のヘッダー情報 (通常、送信元アドレス、宛先アドレス、シーケンス番号などが含まれる) を付けて、独立して送信します。各データ パケットはネットワーク上を独立して移動し、場合によっては受信者まで異なるパスをたどります。データグラムの送信は通常コネクションレスであり、信頼性や順序性の保証はありません。UDP (User Datagram Protocol) は、データグラム伝送を使用するプロトコルです。

TCP および UDP との関係について:

  • TCP は、バイト ストリーム送信を使用して、TCP 接続を通じて信頼性が高く、整然とした接続指向のデータ送信を提供します。TCP はシーケンス番号と確認応答メカニズムを使用して、データの信頼性と順序を保証します。
  • UDP はデータグラム伝送方式を使用して、コネクションレスで信頼性の低いデータ伝送を提供します。UDP は、リアルタイム パフォーマンスに対する高い要件があるものの、データの信頼性と順序に対する要件が低いアプリケーション シナリオに適しています。

バイト ストリームとデータグラムの間に直接の関係はなく、これらは異なるトランスポートです。TCP はバイト ストリーム伝送を使用しますが、UDP はデータグラム伝送を使用します。これら 2 つの送信方法は、さまざまなネットワーク アプリケーション シナリオに適しており、具体的な選択はアプリケーションの要件と設計によって異なります。

粘着袋問題

スティッキー パケットの問題はネットワーク通信でよく見られる現象で、受信側がバイト ストリームを元のデータグラムに正確に分割できず、データ解析エラーが発生することを意味します。粘着性パッケージの問題を解決するには、次の方法を採用できます。

  • 固定長セグメンテーションを使用すると、各データグラムの長さが固定され、受信側は固定長に従ってデータを抽出します。
  • 特定の文字セグメンテーションを使用して、特殊文字または文字シーケンスをデータグラム間の区切り文字として定義すると、受信側は区切り文字に従ってデータグラムを抽出します。
  • 長さフィールドの分割を使用すると、データグラム ヘッダーに長さを示すフィールドが追加され、受信側は長さフィールドを読み取り、対応する長さのバイトをデータグラムとして抽出します。

コンピューター ネットワーク アプリケーション層プロトコルを学習する原則は、ネットワーク通信メカニズムを理解し、カスタム プロトコルを実装することです。カスタム プロトコルの原則を研究すると、特殊文字に従ってバイト ストリームをデータグラムに分割し、逆シリアル化を通じて必要なメッセージを解析するのに役立ちます。これは、Web アプリケーションの開発、データ対話、エラー処理にとって重要です。

シリアル化と逆シリアル化

シリアル化は、オブジェクトの状態をバイト ストリームに変換して、メモリ、ファイル、ネットワーク上に保存したり、他のリモート システムに送信したりできるようにするプロセスです。シリアル化によりオブジェクトがバイト シーケンスに変換され、異なるプラットフォーム、システム、またはプログラミング言語間でのデータ交換と永続的なストレージが可能になります。

逆シリアル化 (逆シリアル化) は、バイト ストリームをオブジェクトの状態に変換して戻すプロセスです。つまり、シリアル化されたバイト ストリームからオブジェクトのプロパティとデータ構造を回復します。逆シリアル化は、バイト ストリームを元のオブジェクトの形式に変換して戻し、これらのオブジェクトを使用および操作できるようにします。

シリアル化と逆シリアル化は通常、分散システム、ネットワーク通信、永続ストレージなどのシナリオで使用されます。シリアル化を通じて、オブジェクトをバイト ストリームに変換して、ネットワーク経由で送信したり、ディスクに保存したりできます。その後、逆シリアル化を通じて、オブジェクトの状態の処理、操作、復元のためにバイト ストリームを元のオブジェクトに戻すことができます。

HTTP プロトコルを使用する場合、ブラウザを使用する場合、ブラウザのバックエンドとサーバーのバックエンドは、これらのことをうまく行うのに役立ちますが、今日はブロガーが独自のプロトコルをカスタマイズする方法を説明します。上記の原則を理解してください。

このプロジェクト: 独自のカスタム プロトコルを使用して電卓のネットワーク バージョンを実装します。

Githubアドレス

Web 電卓 - シリアル化と逆シリアル化 - プロトコルのカスタマイズicon-default.png?t=N6B9https://github.com/Yufccode/BitCode/tree/main/Linux/%E4%BB%A3%E7%A0%81/0226%E7%BD% 91%E7% BB%9C%E8%AE%A1%E7%AE%97%E5%99%A8-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C %E5%8F %8D%E5%BA%8F%E5%88%97%E5%8C%96-%E5%8D%8F%E8%AE%AE%E5%AE%9A%E5%88%B6

基本的なサーバーの準備

基礎となる層は TCP 接続を使用します。ブロガーによって実装されたコードでは、ブロガーは簡単にマルチスレッドを実装し、マルチスレッド バージョンを作成しました。ここでは、ブロガーは基礎となるサーバーのコードを表示しません。友人は直接読み取ることができます。 blogger's github 上記のもので十分です。

シリアル化と逆シリアル化のルールとメッセージ ルールを策定する

操作が 3 つのフィールドで構成されることを指定します。

x、y、op 

x は最初のオペランドを表します

y は 2 番目のオペランドを表します

opは演算子の略です

1+2 つまり、x=1、y=2、op='+'

規定によると、クライアントの入力形式は次のとおりです。

 シリアル化後は次のようになります。

リクエストメッセージのフォーマット

length\r\n__x __op __y\r\n

つまり、最初のフィールドはメッセージの長さで、その後に 2 つの特殊文字 \r\n、x、y、op がスペースで区切られ、最後に \r\n が追加されます。

これが私たちのメッセージです! 

次のように進めます。

ユーザーがクライアントに入力した後、クライアントはまず 3 つのフィールドを Request 構造体に保存し、次にシリアル化インターフェイスを呼び出してその構造体を Request 文字列にシリアル化します。これは、上で示したメッセージの形式です。

サーバーはリクエスト メッセージを受信すると、デシリアライズされたインターフェイスを呼び出して 3 つのフィールドとリクエスト構造を取得します。

次に、Request 構造のコンテンツを通じて、結果を計算した後、Respone 構造を形成し、Respone をシリアル化し、クライアントに送信します。クライアントは Respone を逆シリアル化して最終結果を取得します。

応答メッセージの形式

length\r\n__x __op __y\r\n

プロトコル.hpp



#ifndef __Yufc_Protocol_For_Cal
#define __Yufc_Protocol_For_Cal

#include <iostream>
#include <string>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"

/* 协议的本质 --> 约定! */

#define RECV_MAX_SIZE 1024

/* 请求协议是必须自带序列化功能的 一个结构体是很难发送的 需要序列化成string */
/* 上次0222定制的协议是不完善的!我们需要定制报文*/

// 协议
/* length\r\n__x __op __y\r\n*/
/* length这些就叫做协议报头 */

namespace yufc_ns_protocol
{
#define MYSELF true
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)
    class Request
    {
    public:
        std::string Serialize()
        {
#if MYSELF
            std::string str;
            str = std::to_string(__x);
            str += SPACE;
            str += __op; // BUG
            str += SPACE;
            str += std::to_string(__y);
            return str;
#else
            /* 使用别人的序列化方案 */
            #include <jsoncpp/json/json.h>
            Json::Value root;
            root["x"] = __x;
            root["y"] = __y;
            root["op"] = __op;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }
        bool Deserialize(const std::string &str)
        {
#if MYSELF
            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()); // 拿到__x的字符串之后直接 atoi 就行
            __y = atoi(str.substr(right + SPACE_LEN).c_str());
            if (left + SPACE_LEN > str.size())
                return false;
            __op = str[left + SPACE_LEN];
            return true;
#else
            #include <jsoncpp/json/json.h>
            Json::Value root;
            Json::Reader reader;
            reader.parse(str, root);
            __x = root["x"].asInt();
            __y = root["y"].anInt();
            __op = root["op"].asInt();
            return true;
#endif
        }

    public:
        Request() {}
        Request(int x, int y, char op) : __x(x), __y(y), __op(op) {}
        ~Request() {}

    public:
        int __x;
        int __y;
        char __op; // '+' ...
    };

    class Response
    {
    public:
        /* "code result" */
        std::string Serialize()
        {
#if MYSELF
            std::string s;
            s = std::to_string(__code);
            s += SPACE;
            s += std::to_string(__result);
            return s;
#else
            #include <jsoncpp/json/json.h>
            Json::Value root;
            root["code"] = __code;
            root["result"] = __result;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }
        bool Deserialize(const std::string &s)
        {
#if MYSELF
            // std::cerr << "s: " << s << std::endl;
            std::size_t pos = s.find(SPACE);
            if (pos == std::string::npos)
                return false;
            __code = atoi(s.substr(0, pos).c_str());
            __result = atoi(s.substr(pos + SPACE_LEN).c_str());
            return true;
#else   
            Json::Value root;
            Json::Reader reader;
            reader.parse(s, root);
            __code = root["code"].asInt();
            __result = root["result"].asInt();
            return true;
#endif
        }

    public:
        Response() {}
        Response(int result, int code) : __result(result), __code(code) {}
        ~Response() {}

    public:
        int __result; // 计算结果
        int __code;   // 计算结果的状态码
        /* 0表示计算结果可信任 */
    };

    // 临时方案
    bool Recv(int sock, std::string *out)
    {
        char buffer[RECV_MAX_SIZE];
        memset(buffer, 0, sizeof buffer);
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            *out += buffer;
        }
        else if (s == 0)
        {
            logMessage(NORMAL, "client quit");
            return false;
        }
        else
        {
            std::cerr << "recv error" << std::endl;
            return false;
        }
        return true;
    }

    // 临时方案
    void Send(int sock, const std::string sendStr)
    {
        send(sock, sendStr.c_str(), sendStr.size(), 0);
    }

    // "length\r\n__x __op __y\r\n"
    std::string Decode(std::string &buffer)
    {
        std::size_t pos = buffer.find(SEP);
        if (pos == std::string::npos) // 没有找到 返回空串
            return "";
        int size = atoi(buffer.substr(0, pos).c_str());
        int surplus = buffer.size() - pos - SEP_LEN - SEP_LEN; // 如果这个surplus大于报文长度
        // 则说明 这个报文长度是可以保证我们整体解析出一个合法字符串的
        if (surplus >= size)
        {
            // 至少具有一个合法报文,可以动手提取了
            buffer.erase(0, pos + SEP_LEN); // 此时"__x __op __y\r\n"
            std::string s = buffer.substr(0, size);
            buffer.erase(0, size + SEP_LEN);
            // 此时我们就把合法的部分截取出来了 就是s
            return s;
        }
        else
        {
            return "";
        }
    }

    std::string Encode(std::string &s)
    {
        // 添加报头
        std::string length = std::to_string(s.size());
        std::string new_package = length;
        new_package += SEP;
        new_package += s;
        new_package += SEP;
        return new_package;
    }

}

#endif

サーバープロセスをデーモンプロセスとして設定します

 

CalServer.cc

#include "Protocol.hpp"
#include "TcpServer.hpp"
#include <memory>
#include <signal.h>
#include "Daemon.hpp"

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

void debug(int sock)
{
    std::cout << "service for debug, sock: " << sock << std::endl;
}

static yufc_ns_protocol::Response calHelper(const yufc_ns_protocol::Request &req)
{
    yufc_ns_protocol::Response resp(0, 0);
    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 (0 == req.__y)
            resp.__code = 1;
        else
            resp.__result = req.__x / req.__y;
        break;
    case '%':
        if (0 == req.__y)
            resp.__code = 2;
        else
            resp.__result = req.__x % req.__y;
        break;
    default:
        resp.__code = 3;
        break;
    }
    return resp;
}

void Calculator(int sock)
{
    /* 此处我们就要去定制协议 */
    std::string inbuffer;
    while (true)
    {
        // 1. 读取数据
        bool res = yufc_ns_protocol::Recv(sock, &inbuffer); // 在这里我们读到了一个请求
        // 2. 协议解析 -- 需要保证得到一个完整的报文
        if (!res)
            break;                                                // 读取失败
        std::string package = yufc_ns_protocol::Decode(inbuffer); // 这里Decode保证返回的是一个完整的字节流,是正确的,是可以序列化反序列化的!
        // 如果这个package是空 表示Decode没有给我们返回完整报文
        if (package.empty())
            continue;
        // 3. 保证该报文是一个完整的报文
        yufc_ns_protocol::Request req;
        // 4. 反序列化
        req.Deserialize(package);
        // 5. 业务逻辑
        yufc_ns_protocol::Response resp = calHelper(req);
        // 6. 序列化
        std::string respString = resp.Serialize(); // 序列化

        // 7. 在发送之前,添加报头
        // "length\r\ncode result\r\n"
        respString = yufc_ns_protocol::Encode(respString);
        // 8. 发送(暂时这样写,高级IO的时候,我们再来谈发送的逻辑)
        yufc_ns_protocol::Send(sock, respString); // 发送
    }
}

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    signal(SIGPIPE, SIG_IGN);
    /*
    一般经验: server 在编写的时候都要有严谨的判断逻辑
    一般的服务器,都是要忽略SIGPIPE信号的!防止运行中出现非法写入的问题
    */
    std::unique_ptr<yufc_tcpServer::TcpServer> server(new yufc_tcpServer::TcpServer(atoi(argv[1])));
    server->BindService(Calculator);
    MyDaemon();
    server->Start();
    return 0;
}

動作現象

おすすめ

転載: blog.csdn.net/Yu_Cblog/article/details/131752021