アプリケーション層プロトコルの実装をシミュレートする

アプリケーション層プロトコルの実装をシミュレートする

アプリケーション層

画像-20230829191153866

アプリケーション層は、OSI モデルの 7 番目の層です。アプリケーション層はアプリケーション プログラムと直接インターフェイスし、共通のネットワーク アプリケーション サービスを提供します。アプリケーション層はプレゼンテーション層に対してリクエストも行います。アプリケーション層はオープン システムの最上位層であり、アプリケーション プロセスにサービスを直接提供します。その機能は、複数のシステムアプリケーションプロセスが相互に通信できるようにしながら、業務処理に必要な一連のサービスを完了することです。実際の問題を解決し、日常のニーズを満たすためにプログラマーによって作成されたネットワーク プログラムはすべてアプリケーション層にあります。

もう一度契約について話しましょう

トランスポート層はアプリケーション プロセスにエンドツーエンドの通信サービスを提供しますが、異なるネットワーク アプリケーションのアプリケーション プロセス間では異なる通信ルールが必要となるため、トランスポート層プロトコルに加えてアプリケーション層プロトコルも存在します。契約としては、特定のガイドラインに従う必要があります。対応するアプリケーション層プロトコルは通常、以下に準拠する必要があります。

  1. アプリケーション プロセスによって交換されるメッセージの種類 (要求メッセージや応答メッセージなど)。
  2. メッセージ内の各フィールドとその詳細な説明など、さまざまなメッセージ タイプの構文。
  3. フィールドのセマンティクス、つまりフィールドに含まれる情報の意味。
  4. プロセスがメッセージをいつどのように送信するか、およびメッセージに応答するためのルール

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

公式定義: シリアル化とは、オブジェクトの状態情報を保存または送信できる形式に変換するプロセスです。シリアル化中、オブジェクトは現在の状態を一時ストレージまたは永続ストレージに書き込みます。後で、オブジェクトの状態をストアから読み取るか逆シリアル化することによって、オブジェクトを再作成できます。

シリアル化には次の 2 つの目的があります。

  • オブジェクトのバイト シーケンスをハードディスク (通常はファイル (シリアル化されたオブジェクト)) に永続的に保存します。
  • ネットワーク上でオブジェクトを送信する一連のバイト (ネットワーク転送オブジェクト)

実際には、データがメモリに保存されてメモリ リソースが消費されるのを防ぐために、データを永続化します。さらに、シリアル化により、ネットワークでの転送と配布も容易になります。

画像-20230829223851219

たとえば、WeChatで相手にメッセージを送信する場合、実際にアバター、ニックネーム、メッセージ内容、送信時刻などの構造を構築し、その構造をシリアル化する、つまり構造が形成されます。バイトストリームメッセージに変換してネットワーク経由で送信 相手にメッセージを送信すると、相手はメッセージをデシリアライズして構造体に変換し、アバター、ニックネーム、メッセージ内容、送信時刻に再構築します、など。

シリアル化により、エンディアンやメモリ アライメントに起因するネットワーク伝送構造におけるデータ エラーなどの問題も解決されます。

オンライン計算機

単純なプロトコルを指定することで、サーバー バージョンの計算機が実装されるようになりました。クライアントで計算される 2 つの数値と演算子を送信する必要があります。その後、サーバーが計算を実行し、最後に結果をクライアントに返します。

対応するネットワーク計算機契約:

  • クライアントは「1+1」の形式で文字列を送信します。
  • この文字列のオペランドは両方とも整数です
  • 2 つのオペランドの間には演算子が 1 つだけあり、オペランドと演算子の間にスペースを入れることはできません。

対応するネットワークのシリアル化と逆シリアル化:

  • 対話する必要がある情報を表す構造を定義する
  • データ送信時には規則に従って構造体が文字列に変換され、データ受信時には同じ規則に従って文字列が構造体に変換されます。このプロセスはシリアル化と逆シリアル化と呼ばれます

TCP プロトコルでは、受信者が完全なメッセージを確実に受信できるようにするにはどうすればよいでしょうか?

画像-20230829231938344

  • 私たちが呼び出す送信関数または受信関数は、本質的にはコピー関数です。

  • アプリケーション層によって呼び出される送信関数または受信関数は、ネットワークからデータを直接読み取ったり、送信したりしません。たとえば、クライアントがデータを送信するためにアプリケーション層の送信関数を呼び出すと、アプリケーション層の送信バッファからトランスポート層の送信バッファにデータがコピーされます。次に、トランスポート層がいつデータをネットワークに送信するかを独自に決定し、サーバーのトランスポート層がネットワーク経由でデータを受信バッファに読み取り、アプリケーション層の受信バッファにコピーします。つまり、TCPプロトコルは伝送制御プロトコルです

  • TCP プロトコルの両側には送信バッファと受信バッファがあるため、データの読み取りと送信は互いに干渉せず、同時に双方向にデータを送信できます。したがって、TCP プロトコルは全二重です。通信プロトコル。

  • TCP プロトコルは全二重通信プロトコルであるため、クライアントがデータを送信する速度がサーバーがデータを読み取る速度よりもはるかに速いため、サーバーの受信バッファーに大量のメッセージが蓄積されます。直線的につながっているので、各メッセージを完全に読み取るにはどうすればよいでしょうか? **指定されたプロトコルを使用し、プロトコルで指定された方法で読み取ります。**プロトコルのカスタマイズ方法には次のものが含まれます。

    • 固定長: メッセージの長さを指定します。
    • ブレーク記号: メッセージ間にブレーク記号があることを指定します。
    • 自己記述型メソッド: カスタムプロトコル

このオンライン バージョンの電卓に対応するプロトコルは次のとおりです。

画像-20230829235118910

  • UDP プロトコルはデータグラムの形式でデータを送受信するため、データは送信プロセス中に完成するため、シーケンスや逆シリアル化を通じてメッセージ境界を定義したり、データに特別なコンテンツを追加したりする必要はありませんが、TCP プロトコルはデータを送信します。データをバイト ストリームの形式で受信するため、メッセージを識別して保護するには、関連するプロトコル手段を使用する必要があります。

カスタムプロトコル

プロトコル.hpp

#pragma once
#include<iostream>
#include<string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
using namespace std;

#define SEP " "
#define SEP_LEN strlen(SEP)//strlen统计'\0'之前的字符个数,而sizeof统计的是所占内存的空间大小,使用sizeof会越界出问题
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP)

enum {
    
    
    NONE=0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERR
};
//"x op y"->"text_len"\r\n"x op y"\r\n---給内容加上报头
std::string enLength(const std::string& text)//协议定制
{
    
    
    std::string send_str=to_string(text.size());
    send_str+=LINE_SEP;
    send_str+=text;
    send_str+=LINE_SEP;

    return send_str;
}
//"text_len"\r\n"x op y"\r\n -> "x op y"---去掉报头,取出里面的内容
bool deLength(const std::string& str,string* ret)//协议定制
{
    
    
    auto it=str.find(LINE_SEP);//找到报头
    if(it==std::string::npos) return false;//如果没找到则直接返回

    int len=stoi(str.substr(0,it));//取出字符串的长度
    *ret=str.substr(it+LINE_SEP_LEN,len);//取出数据
    return true;
}

class Request
{
    
    
public:
Request():_x(0),_y(0),_op(0){
    
    }
Request(int x,int y,int op):_x(x),_y(y),_op(op){
    
    }

bool Serialize(std::string* out)//序列化,将传入的x op y转化为字符串"x op y"
{
    
    
    *out="";
    *out+=to_string(_x);
    *out+=SEP;
    *out+=to_string(_op);
    *out+=SEP;
    *out+=to_string(_y);

    return true;
}

bool Deserialize( const string& origin)//反序列化,将传过来的字符串拆出来传参給_x _op _y
{
    
    //"_xSEP_opSEP_y"-> _x,_op,_y

    auto leftit=origin.find(SEP);
    cout<<"Deserialize找到了leftSEP: "<<leftit<<endl;
    auto rightit=origin.rfind(SEP);
    cout<<"Deserialize找到了rightSEP: "<<rightit<<endl;
    if(leftit==string::npos|| rightit==string::npos) return false;

    if(leftit==rightit) return false;
    int opsize=rightit-leftit-1;
    cout<<"opsize: "<<opsize<<endl;
//1 43 1--leftit=1,rightit=4,opsize=rightit-leftit-1=4-1-1=2;
//1 3 1--leftit=1,right=3,opsize=rightit-leftit-1=3-1-1=1
   // if(rightit-(leftit+SEP_LEN)!=1) return false;
    if(rightit-(leftit+SEP_LEN)!=opsize) return false;
    //+号ASCII码是43,从char转int被解析成43即stringlen为两位,这里的运算rightit-(leftit+SEP_LEN)!=1就出问题
//4-(1+1)==2;3-(1+1)=1
    std::string origin_x=origin.substr(0,leftit);
    std::string origin_y=origin.substr(rightit+SEP_LEN);
    if(origin_x.empty()) return false;

    if(origin_y.empty()) return false;

    cout<<"origin_x: "<<origin_x<<" origin_y: "<<origin_y<<endl;
    _x=stoi(origin_x);
    int opf=stoi(origin.substr(leftit,rightit));
    _op=opf;
    cout<<"opf: "<<opf<<"_op: "<<_op<<endl;
    _y=stoi(origin_y);

    return true;

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

class Response
{
    
    
public:
Response():_exitcode(0),_result(0){
    
    }
Response(int exitcode,int result):_exitcode(exitcode),_result(result){
    
    }
bool Serialize(string*out)//序列化
{
    
    //_exitcode _result ->"_exitcodeSEP_result"
*out="";
*out+=to_string(_exitcode);
*out+=SEP;
*out+=to_string(_result);

return true;
}

bool Deserialize(const string& in)//反序列化
{
    
    //_exitcodeSEP_result"->_exitcode _result

auto pos=in.find(SEP);
if(pos==string::npos) return false;

string excstr=in.substr(0,pos);
string resstr=in.substr(pos+SEP_LEN);
if(excstr.empty()||resstr.empty()) return false;

_exitcode=stoi(excstr);
_result=stoi(resstr);

return true;

}

public:
int _exitcode;//退出码
int _result;//结果
};

//"text_len"\r\n"x op y"\r\n
bool recvPackage(int sock,string& inbuffer,string*out)
{
    
    
char buffer[1024];

while(true)
{
    
    
ssize_t  s=recv(sock,buffer,sizeof(buffer)-1,0);
if(s>0)
{
    
    
    buffer[s]=0;
    inbuffer+=buffer;
    auto pos=inbuffer.find(LINE_SEP);
    if(pos==string::npos)continue;//没找到报头和有效载荷之间的分隔符---如果字节流式的报文没读全就继续读
    string text_len=inbuffer.substr(0,pos);//报头是有效载荷的长度
    int len=stoi(text_len);
    
    int totallen=text_len.size()+LINE_SEP_LEN*2+len;//整个报文的长度
    if(inbuffer.size()<totallen) 
    {
    
    
        cout<<"输入的消息不完整,请继续输入.continue..."<<endl;
        continue;//报文没读完继续读
    }
    
    cout<<"处理前的inbuffer: \n"<<inbuffer<<endl;

    *out=inbuffer.substr(0,totallen);
    inbuffer.erase(0,totallen);

    cout<<"处理后的inbuffer: \n"<<inbuffer<<endl;

    break;
}
else return false;
}

return true;

}

導入:

  • Request クラス内:
  1. 2 つの外部オペランドと演算子が Request オブジェクトを構築するために渡され、Serialize シリアル化関数が呼び出されて文字列 "_xSEP_opSEP_y" が構築されます。ここで、SEP はスペース、_x は左側のオペランド、_op は演算子、_y は右のオペランド。
  2. 外部パラメータ文字列「_xSEP_opSEP_y」が入力され、Deserialize が呼び出され、この文字列を介して埋め込みパラメータ _x、_op、_y を逆シリアル化および構築します。演算子 _op が渡されると、シンボルが対応する ASCII コードに変換されることに注意してください。たとえば、「+」は「43」に変換されますが、このとき、後で「+」に戻せるように、文字列中の「43」を取り出して整数の43に変換する必要があります。 。
  • Response クラス内:
  1. 外部文字列「_exitcodeSEP_result」が渡され、Deserialize 逆シリアル化関数が呼び出されて、埋め込みパラメータ _exitcode および _result が構築されます。ここで、exitcode は終了コードです。計算結果が正しい場合、終了コードは NONE になります。計算中にゼロ除算エラーが発生し、終了コードは DIV_ZERO でした。計算中にゼロ商エラーが発生しました。終了コードは MOD_ZERO です。パラメータが渡されるとオペレータ エラーが発生し、終了コードは OP_ERR になります。_result は計算結果です。
  2. Serialize シリアル化関数を呼び出して、埋め込みパラメーター _exitcode および _result を使用して文字列「_exitcodeSEP_result」を構築します。
  • enLength プロトコル カスタマイズ関数は、受信文字列に「ヘッダー」を追加し、文字列「xSEPopSEPy」を渡します。「ヘッダー」を追加した後の文字列は「text_len」\r\n「xSEPopSEPy」\r\n です。「 xSEPopSEPy」はペイロード、text_len はペイロードの長さ、SEP はスペースです。

  • deLength プロトコルのカスタム関数。この機能は、受信文字列 str から「ヘッダー」を削除し、ペイロードを ret 経由で渡すことです。受信文字列は「text_len」\r\n「xSEPopSEPy」\r\n です。「ヘッダー」を削除した後、文字列「xSEPopSEPy」を取り出します。ここで、SEP はスペースです

  • recvPackage 関数は、データ パケットを受信するためにサーバーによって呼び出される関数です。サーバーは、クライアントが送信したメッセージを受信します。メッセージが定義されたプロトコル形式に準拠していない場合、または未読のメッセージが完全でない場合は、完全なメッセージが読み取られるまで読み取りがブロックされます。次に、メッセージは出力パラメーター out を通じて送信されます。

カルサーバー.cc


#include"calserver.hpp"
#include"log.hpp"
#include<iostream>
#include<stdlib.h>
#include<memory>
using namespace Server;
using namespace std;

static void Usage(string proc)
{
    
    
    cout<<"\nUsage:\n\t"<<proc<<" local_port\n\n"<<endl;
}
//req是一个已经处理的完整的对象
bool cal(const Request& req,Response& rep)
//根据req填充rep 
{
    
    
switch(req._op)
{
    
    
    case '+':
    rep._result=req._x+req._y;
    break;
    case '-':
        rep._result=req._x-req._y;
    break;    
    case '*':
        rep._result=req._x*req._y;
    break;    
    case '/':
    {
    
    
        if(req._y==0) rep._exitcode=DIV_ZERO;
        else
        rep._result=req._x/req._y;
    }   
    break;    
    case '%':
    {
    
    
        if(req._y==0) rep._exitcode=MOD_ZERO;
        else
        rep._result=req._x%req._y;
    }
    break;
    default:
    rep._exitcode=OP_ERR;
    break;
}
return true;

}

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

uint16_t port=atoi(argv[1]);//将字符串转化为整数

unique_ptr<calserver> ts(new calserver(port));
ts->initserver();
ts->start(cal);


return 0;
}
  • cal は、Request オブジェクトと Response オブジェクトを渡す計算関数です。Request オブジェクトのパラメータには 2 つのオペランド _ x、_ y と演算子 _ op があり、計算結果は Response オブジェクトのパラメータ _result に配置され、終了コードは _exitcode に配置されます。

カルサーバー.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include<functional>
#include"log.hpp"
#include"protocol.hpp"
#define NUM 1024

using namespace std;
static const int gbacklog = 5;
namespace Server
{
    
    
    enum
    {
    
    
        USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
    };

typedef function<bool(const Request&req,Response& res)> func_t;

void handlerentry(int sock,func_t func)
{
    
    
     string inbuffer;
    while(true)
    {
    
    
    //1. 获取客户端发送来的数据报,确定数据报是带报头的数据报
    //"text_len"\r\n"x op y"\r\n 
   string req_text,req_str;
    if(!recvPackage(sock,inbuffer,&req_text)) return;
    cout<<"带报头的请求(数据报): "<<req_text<<endl;
    //2.对数据报进行反序列化
    //"text_len"\r\n"x op y"\r\n -> "x op y"---去掉报头,取出里面的内容
    if(!deLength(req_text,&req_str)) return;
    cout<<"去掉报头的请求(数据报):"<<req_str<<endl;
    //走到这里再往下就卡主了,只打印到上面那条日志后面都没打印到!!!
    //3.获得一个结构化的请求对象
    Request req;
   if(!req.Deserialize(req_str)) return;//如果反序列化失败直接返回
    //4.对对象进行操作---进行服务器业务
    //4.1.获得一个结构化响应
     Response rep;
    func(req,rep);
    //5.对对象进行序列化
    //_exitcode _result ->"_exitcodeSEP_result"
    string rep_str;
    rep.Serialize(&rep_str);
    cout<<"计算完成后的响应: "<<rep_str<<endl;
    //6.給有效载荷加上报头
     //"exitcode result" -> "content_len"\r\n"exitcode result"\r\n
    string rep_text=enLength(rep_str);
    cout<<"加上报头的完整响应(报文): "<<rep_text<<endl;

    //7.把报文发送回給客户端
    send(sock,rep_text.c_str(),rep_text.size(),0);
    
    }
    
}
typedef function<bool(const Request&req,Response& res)> func_t;
class calserver
{
    
    
 
public:
calserver(const uint16_t& port):_port(port),_listensock(-1){
    
    }


void initserver()
{
    
    
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
    
    
    logMessage(FATAL,"create listensocket error");
    exit(SOCK_ERR);
}
 logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
    
    
    logMessage(FATAL,"bind error");
    exit(BIND_ERR);
}
 logMessage(NORMAL,"bind success");
//3.将套接字设置为监听模式
if(listen(_listensock,gbacklog)<0)
{
    
    
    logMessage(FATAL,"listen error");
    exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}
void start(func_t fun)
{
    
    
    while(true)
    {
    
    
        struct sockaddr_in cli;
        socklen_t len=sizeof(cli);
        bzero(&cli,len);

        int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
    
    
            logMessage(FATAL,"accept client error");
            continue;
        }
        logMessage(NORMAL,"accept client success");

        cout<<"accept sock: "<<sock<<endl;

        //多进程版---
        //一个客户端占用一个文件描述符,原因在于孙子进程执行IO任务需要占用独立的文件描述符,而文件描述符是继承父进程的,而每次客户端进来都要占用新的文件描述符
        //因此若接收多个客户端不退出的话文件描述符会越来越少。
         pid_t id=fork();//创建子进程
         if(id==0)//子进程进入
         {
    
    
             close(_listensock);//子进程不需要用于监听因此关闭该文件描述符
             handlerentry(sock,fun);
             close(sock);
             exit(0);
        }
        //父进程
        close(sock);
        pid_t ret=waitpid(id,nullptr,0);
        if(ret<0)
        {
    
    
            cout << "waitsuccess: " << ret << endl;
        }
    }
}
~calserver(){
    
    }
private:
int _listensock;//用于监听服务器的sock文件描述符
uint16_t _port;//端口号
};

}
  • handlerentry 関数は、サーバーによって呼び出される送受信関数です。受信パラメータは、通信に使用されるファイル記述子 sock と、呼び出される計算関数 cal (cal は calserver.cc ファイル内にあります) です。サーバーは handlerentry 関数を呼び出し、クライアントによって送信されたメッセージを "text_len"\r\n"x op y"\r\n の形式で受信します。サーバーは deLength 関数を呼び出して "ヘッダー" を削除します。変換された文字列は "x op y" の形式になります。Request オブジェクトの逆シリアル化関数 Deserialize を呼び出し、文字列 "x op y" を通じて Request オブジェクトの _ x、_ op、_ y パラメータを入力します。 ; func 関数または cal 関数を呼び出して、Request オブジェクトのパラメーターを使用して計算する 結果を出力し、Response オブジェクトを構築する; Response オブジェクトのシリアル化関数を呼び出して、パラメーター _exitcode _result を文字列 "_exitcodeSEP_result" に変換する; enLength 関数を呼び出すヘッダーを追加すると、変換された文字列は "content_len"\r\n"exitcode result"\r\n の形式になります。次に、send 関数を通じて文字列をクライアントに送信します。
  • サーバーはマルチプロセス バージョンであることに注意してください。これは、サーバーが複数のクライアントと並行して通信できることを意味します。

calclient.cc

#include<iostream>
#include<string>
#include<memory>
#include"calclient.hpp"
using namespace std;
using namespace client;
static void Usage(string proc)
{
    
    
    cout<<"\nUsage :\n\t"<<proc<<" serverip serverport\n"<<endl;
}
int main(int argc, char* argv[])
{
    
    
    if(argc!=3)
    {
    
    
        Usage(argv[0]);
        exit(1);
    }

string serverip=argv[1];
uint16_t serverport=atoi(argv[2]);

unique_ptr<calclient> tc(new calclient(serverip,serverport));

tc->initclient();
tc->start();

    return 0;
}

calclient.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include"protocol.hpp"
using namespace std;
#define NUM 1024
namespace client
{
    
    

    class calclient
{
    
    

public:
calclient(const string& ip,const uint16_t& port)
:_sock(-1)
,_port(port)
,_ip(ip)
{
    
    }

void initclient()
{
    
    
//1.创建sockfd
_sock=socket(AF_INET,SOCK_STREAM,0);
if(_sock<0)
{
    
    
   cerr<<"socket create error"<<endl;
   exit(2);
}
//2.绑定 ip port,不显示绑定,OS自动绑定
}

void start()
{
    
    
struct sockaddr_in ser;
bzero(&ser,sizeof(ser));
socklen_t len=sizeof(ser);
ser.sin_family=AF_INET;
ser.sin_port=htons(_port);
ser.sin_addr.s_addr=inet_addr(_ip.c_str());
if(connect(_sock,(struct sockaddr *)&ser,len)!=0)
{
    
    
    cerr<<"connect error"<<endl;
}else
{
    
    
    string line;
    string inbuffer;
    while(true)
    {
    
    
        cout<<"mycal>>: ";//输入"xopy"
        getline(cin,line);
        Request req=ParseLine(line);//用"xopy"取出x op y构造Request对象
        string context;
        req.Serialize(&context);//序列化,用x op y构造字符串"xSEPopSEPy"
        string send_str=enLength(context);//定制协议---"x op y"->"text_len"\r\n"x op y"\r\n---給内容加上报头
        cout<<"calclient send str: "<<send_str<<endl;
        send(_sock,send_str.c_str(),send_str.size(),0);//客户端把报文发送給服务器

        string package;
        if(!recvPackage(_sock,inbuffer,&package)) continue;//服务器处理完数据,客户端接收服务器发送来的报文
        //  "content_len"\r\n"exitcode result"\r\n
        string reser_len;
        if(!deLength(package,&reser_len)) continue;//去报头
         //  "content_len"\r\n"exitcode result"\r\n -> "exitcode result"
        Response rep;
        rep.Deserialize(reser_len);//反序列化://_exitcodeSEP_result"->_exitcode _result

        cout<<"_exitcode: "<<rep._exitcode<<endl;
        cout<<"_result: "<<rep._result<<endl;
    }
}
}

~calclient()
{
    
    
    if(_sock>=0) close(_sock);
}

Request ParseLine(const string& line)
{
    
    //"xopy"->取出来到x op y 上
int i=0;
int status=0;
int num=line.size();
string left,right;
char op;

while(i<num)
{
    
    
switch(status)
{
    
    
    case 0:
    {
    
    
        if(!isdigit(line[i]))
    {
    
    
        op=line[i];//取出运算符**
        status=1;
    }else
    left.push_back(line[i++]);//取出左操作数
    }
    break;
    case 1:
    i++;
    status=2;
    break;

    case 2:
    right.push_back(line[i++]);
    break;
}
}
cout<<"left: "<<stoi(left)<<" op: "<<op<<" right: "<<stoi(right)<<endl;
return Request(stoi(left),stoi(right),op);//返回Request对象

}

private:
int _sock;
uint16_t _port;
string _ip;

};
}

  • ParseLine 関数は、「1+1」の形式の文字列を受け取り、その文字列を解析した後に Request オブジェクトを構築します。
  • クライアントは start 関数を呼び出し、コマンド ラインから送信された "1+1" 形式の文字列を受信して​​から、ParseLine 関数を呼び出して Request オブジェクトを構築します。Request オブジェクトのシリアル化関数 Serialize を呼び出して、文字列「xSEPopSEPy」を構築します。プロトコル カスタマイズ関数 enLength を呼び出して文字列に「ヘッダー」を追加すると、変換された文字列は "text_len"\r\n"x op y"\r\n になります。send 関数を呼び出して文字列をサーバーに送信します。サーバーの計算が完了したら、結果を送り返し、recvPackage 関数を呼び出してサーバーから送り返された文字列を受け取ります。文字列の形状は "content_len" のようなものです\r\n"exitcode result"\r\n; deLength 関数を呼び出しますヘッダーを削除するには、変換された文字列の形状は "exitcode result" のようになります。Response オブジェクトの逆シリアル化関数 Deserialize を呼び出して、文字列からパラメーター _exitcode _result を取得し、それを出力します。

メイクファイル

.PHONY:all
all:calclient calserver

calclient:calclient.cc
	g++ -o $@ $^ -std=c++11

calserver:calserver.cc
	g++ -o $@ $^ -std=c++11 

.PHONY:clean
clean:
	rm -rf calserver calclient

画像-20230830121135167

Json を使用したシリアル化と逆シリアル化

JSON (JavaScript Object Notation) は、読み書きしやすい構造化データをテキストで表す軽量のデータ交換形式です。JSON は、キーと値のペア (キーと値のペアのコレクション) と値の順序付きリストという 2 つの構造で構成されます。ここではキーと値のペアとして使用されます。

jsonライブラリのインストール

次のコマンドを入力してインストールします

sudo yum install -y jsoncpp-devel

インストール後、ls を通じてクエリできます。

画像-20230830151558485

メイクファイル

cc=g++
LD=-DMYPRO
.PHONY:all
all:calclient calserver

calclient:calclient.cc
	$(cc) -o $@ $^ -std=c++11 -ljsoncpp #${LD}

calserver:calserver.cc
	$(cc) -o $@ $^ -std=c++11 -ljsoncpp #${LD}

.PHONY:clean
clean:
	rm -rf calserver calclient
  • Json シリアル化を使用しない場合は、2 行目をコメントアウトします。LD=-DMYPRO

プロトコル.hpp

#pragma once
#include<iostream>
#include<string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <jsoncpp/json/json.h>
using namespace std;

#define SEP " "
#define SEP_LEN strlen(SEP)//strlen统计'\0'之前的字符个数,而sizeof统计的是所占内存的空间大小,使用sizeof会越界出问题
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP)

enum {
    
    
    NONE=0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERR
};
//"x op y"->"text_len"\r\n"x op y"\r\n---給内容加上报头
std::string enLength(const std::string& text)//协议定制
{
    
    
    std::string send_str=to_string(text.size());
    send_str+=LINE_SEP;
    send_str+=text;
    send_str+=LINE_SEP;

    return send_str;
}
//"text_len"\r\n"x op y"\r\n -> "x op y"---去掉报头,取出里面的内容
bool deLength(const std::string& str,string* ret)//协议定制
{
    
    
    auto it=str.find(LINE_SEP);//找到报头
    if(it==std::string::npos) return false;//如果没找到则直接返回

    int len=stoi(str.substr(0,it));//取出字符串的长度
    *ret=str.substr(it+LINE_SEP_LEN,len);//取出数据
    return true;
}

class Request
{
    
    
public:
Request():_x(0),_y(0),_op(0){
    
    }
Request(int x,int y,int op):_x(x),_y(y),_op(op){
    
    }

bool Serialize(std::string* out)//序列化,将传入的x op y转化为字符串"x op y"
{
    
    
#ifdef MYPRO
    *out="";
    *out+=to_string(_x);
    *out+=SEP;
    *out+=to_string(_op);
    *out+=SEP;
    *out+=to_string(_y);
#else
Json::Value root;//json的对象是键值对[key,value]
root["first"]=_x;//int类型被设置进json的键值对时自动转换为string类型
root["second"]=_y;
root["oper"]=_op;

Json::FastWriter writer;

*out=writer.write(root);//调用接口序列化返回值为字符串

#endif

    return true;
}

bool Deserialize( const string& origin)//反序列化,将传过来的字符串拆出来传参給_x _op _y
{
    
    //"_xSEP_opSEP_y"-> _x,_op,_y

#ifdef MYPRO
    auto leftit=origin.find(SEP);
    cout<<"Deserialize找到了leftSEP: "<<leftit<<endl;
    auto rightit=origin.rfind(SEP);
    cout<<"Deserialize找到了rightSEP: "<<rightit<<endl;
    if(leftit==string::npos|| rightit==string::npos) return false;

    if(leftit==rightit) return false;
    int opsize=rightit-leftit-1;
    cout<<"opsize: "<<opsize<<endl;
//1 43 1--leftit=1,rightit=4,opsize=rightit-leftit-1=4-1-1=2;
//1 3 1--leftit=1,right=3,opsize=rightit-leftit-1=3-1-1=1
   // if(rightit-(leftit+SEP_LEN)!=1) return false;
    if(rightit-(leftit+SEP_LEN)!=opsize) return false;
    //+号ASCII码是43,从char转int被解析成43即stringlen为两位,这里的运算rightit-(leftit+SEP_LEN)!=1就出问题
//4-(1+1)==2;3-(1+1)=1
    std::string origin_x=origin.substr(0,leftit);
    std::string origin_y=origin.substr(rightit+SEP_LEN);
    if(origin_x.empty()) return false;

    if(origin_y.empty()) return false;

    cout<<"origin_x: "<<origin_x<<" origin_y: "<<origin_y<<endl;
    _x=stoi(origin_x);
    int opf=stoi(origin.substr(leftit,rightit));
    _op=opf;
    cout<<"opf: "<<opf<<"_op: "<<_op<<endl;
    _y=stoi(origin_y);
#else
Json::Value root;
Json::Reader reader;
reader.parse(origin,root);//反序列化,将字符串中的协议字符串填进对象对应的元素中

_x=root["first"].asInt();
_y=root["second"].asInt();
_op=root["oper"].asInt();
#endif
    return true;

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

class Response
{
    
    
public:
Response():_exitcode(0),_result(0){
    
    }
Response(int exitcode,int result):_exitcode(exitcode),_result(result){
    
    }
bool Serialize(string*out)//序列化
{
    
    //_exitcode _result ->"_exitcodeSEP_result"
#ifdef MYPRO
*out="";
*out+=to_string(_exitcode);
*out+=SEP;
*out+=to_string(_result);
#else
Json::Value root;
root["exitcode"]=_exitcode;
root["result"]=_result;

Json::FastWriter writer;
*out= writer.write(root);
#endif

return true;
}

bool Deserialize(const string& in)//反序列化
{
    
    //_exitcodeSEP_result"->_exitcode _result

#ifdef MYPRO
auto pos=in.find(SEP);
if(pos==string::npos) return false;

string excstr=in.substr(0,pos);
string resstr=in.substr(pos+SEP_LEN);
if(excstr.empty()||resstr.empty()) return false;

_exitcode=stoi(excstr);
_result=stoi(resstr);

#else
Json::Value root;
Json::Reader reader;
reader.parse(in,root);
_exitcode=root["exitcode"].asInt();
_result=root["result"].asInt();
#endif
return true;

}

public:
int _exitcode;//退出码
int _result;//结果
};

//"text_len"\r\n"x op y"\r\n
bool recvPackage(int sock,string& inbuffer,string*out)
{
    
    
char buffer[1024];

while(true)
{
    
    
ssize_t  s=recv(sock,buffer,sizeof(buffer)-1,0);
if(s>0)
{
    
    
    buffer[s]=0;
    inbuffer+=buffer;
    auto pos=inbuffer.find(LINE_SEP);
    if(pos==string::npos)continue;//没找到报头和有效载荷之间的分隔符---如果字节流式的报文没读全就继续读
    string text_len=inbuffer.substr(0,pos);//报头是有效载荷的长度
    int len=stoi(text_len);
    
    int totallen=text_len.size()+LINE_SEP_LEN*2+len;//整个报文的长度
    if(inbuffer.size()<totallen) 
    {
    
    
        cout<<"输入的消息不完整,请继续输入.continue..."<<endl;
        continue;//报文没读完继续读
    }
    
    cout<<"处理前的inbuffer: \n"<<inbuffer<<endl;

    *out=inbuffer.substr(0,totallen);
    inbuffer.erase(0,totallen);

    cout<<"处理后的inbuffer: \n"<<inbuffer<<endl;

    break;
}
else return false;
}

return true;

}
  • Jsonシリアル化はRequestクラスとResponseクラスで使用されます。

条件付きコンパイル

  • #ifdef ディレクティブは、プリプロセスで次の識別子 (DEBUG) が定義されている場合、つまり DEBUG が true の場合、#ifdef と #else の間のコードがすべて実行され、#else 以降のコードは実行されないことを説明します。DEBUFG が未定義の場合、つまり DEBUG が false の場合、#else と #endif の間のコードが実行されます。#endif は、条件付きコンパイル ディレクティブを終了するために使用されます。#ifdef と #endif は一緒に使用されます。

フォーマット

#ifdef DEBUG
//......
#else
//......
#endif
//Request类内的Json序列化片段-Serialize
Json::Value root;//json的对象是键值对[key,value]
root["first"]=_x;//int类型被设置进json的键值对时自动转换为string类型
root["second"]=_y;
root["oper"]=_op;

Json::FastWriter writer;

*out=writer.write(root);//调用接口序列化返回值为字符串
  • キー「first」と対応する値 _x を持つ Json Value オブジェクトを作成し、オペランドと演算子を Value オブジェクトに設定して、シリアル化のために Json FastWriter オブジェクトを通じて write を呼び出します。
Request类内的Json反序列化片段-Deserialize
Json::Value root;
Json::Reader reader;
reader.parse(origin,root);//反序列化,将字符串中的协议字符串填进对象对应的元素中

_x=root["first"].asInt();
_y=root["second"].asInt();
_op=root["oper"].asInt();
  • Json Value オブジェクトを作成してから、Json Reader オブジェクトを作成し、Reader オブジェクトの解析を呼び出して、プロトコルを運ぶ文字列を Value オブジェクトの対応する要素に入力します。次に、キーと値のペアを通じて要素を削除します。

画像-20230830150840323

おすすめ

転載: blog.csdn.net/m0_71841506/article/details/132583387