个人主页:Eternity._
⏩收录专栏⏪:Linux “ 登神长阶 ”
期待您的关注

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


前言:应用层协议,作为网络通信架构中的最高层,直接与用户应用交互,负责数据的格式化和传输控制。通过自定义应用层协议,开发者可以实现特定业务需求,提高数据传输效率,增强系统的安全性和可扩展性。

本文旨在为广大Linux开发者提供一份关于自定义应用层协议的全面学习指南。我们将从协议设计的基本原理出发,深入探讨Linux系统下的网络通信机制,详细解析自定义协议的实现步骤和调试技巧,并分享一些实际案例和最佳实践。

1. 封装 Socket


首先,在自定义应用层协议前,我们先来封装一下 Socket 来简化网络通信的复杂性,并且我们可以提供更强的可维护性和可扩展性,以便更容易管理和使用网络连接的编程技术。在套接字编程TCP中,我们固定的步骤是:创建,连接,监听。我们可以先确定基类Socket

基类:Socket

const int backlog = 3;

class Socket
{
public:
	~Socket()
    {}
	// 创建
    virtual void CreateSocketOrDie() = 0;
    // 连接
    virtual void BindSocketOrDie(uint16_t port) = 0;
    // 监听
    virtual void ListenSocketOrDie(int backlog) = 0;
    // accept
    virtual Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) = 0;
    // connect服务
    virtual bool ConnectServer(std::string &serverip, uint16_t serverport) = 0;
    // 获取文件描述符
    virtual int GetSocket() = 0;
    // 创建文件描述符
    virtual void SetSocket(int sockfd) = 0;
    // 关闭文件描述符
    virtual void CloseSocket() = 0;

    // 接收,发送信息
    virtual bool Recv(std::string *buffer, int size) = 0;
    virtual void Send(std::string &send_str) = 0;

public:
    void BuildListenSocketMethod(uint16_t port, int backlog)
    {
        CreateSocketOrDie();
    	BindSocketOrDie(port);
        ListenSocketOrDie(backlog);
    }

    bool BuildConnectSocketMethod(std::string &serverip, uint16_t serverport)
    {
    	CreateSocketOrDie();
        return ConnectServer(serverip, serverport);
    }

    void BuildNOrmalSocketMethod(int sockfd)
    {
    	SetSocket(sockfd);
    }
};

子类:TcpSocket

const static int defaultsockfd = -1; // 定义一个缺省值
#define Convert(addrptr) ((struct sockaddr *)(addrptr)) // 定义宏用于强制类型转换
// 错误码
enum
{
	SocketError = 1,
	BindError,
	ListenError,   
};

class TcpSocket : public Socket
{
public:
	// 构造与析构 
	TcpSocket(int sockfd = defaultsockfd)
		: _sockfd(sockfd)
    {}
	~TcpSocket()
	{} 
	// 调用套接字接口进行封装
	// socket
    void CreateSocketOrDie() override
    {
    	_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    	if (_sockfd < 0) exit(SocketError);
    }
    // bind
    void BindSocketOrDie(uint16_t port) override
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(port);

        int n = ::bind(_sockfd, Convert(&local), sizeof(local));
        if (n < 0) exit(BindError);
    }
    // listen
	void ListenSocketOrDie(int backlog) override
    {
    	int n = listen(_sockfd, backlog);
        if (n < 0) exit(ListenError);
    }
    // accept
    Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) override
    {
    	struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newsockfd = ::accept(_sockfd, Convert(&peer), &len);
        if (newsockfd < 0) return nullptr;
        *peerport = ntohs(peer.sin_port);
        *peerip = inet_ntoa(peer.sin_addr);
            
        Socket *s = new TcpSocket(newsockfd);
        return s;
	}
	// connect
	bool ConnectServer(std::string &serverip, uint16_t serverport) override
	{
    	struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(serverport);
        server.sin_addr.s_addr = inet_addr(serverip.c_str());

        int n = ::connect(_sockfd, Convert(&server), sizeof(server));
        if (n == 0) return true;
         else return false;
	}
    int GetSocket() override
    {
    	return _sockfd;
    }
    void SetSocket(int sockfd) override
    {
    	_sockfd = sockfd;
    }
    void CloseSocket() override
    {
        if (_sockfd > defaultsockfd)
                ::close(_sockfd);
    }
    // recv
    bool Recv(std::string *buffer, int size) override
    {
    	char inbuffer[size];
        ssize_t n = recv(_sockfd, inbuffer, size-1, 0);
        if(n > 0)
        {
        	inuffer[n] = 0;
            *buffer += inbuffer;
            return true;
        }
        else if(n < 0) return false;

        else return false;
	}
	// send
	void Send(std::string &send_str) override
	{
    	send(_sockfd, send_str.c_str(), send_str.size(), 0);
	}

private:
	int _sockfd;
};

在封装Socket都是我们之前学习常见的方式,我们熟悉之后,就可以很轻松的的使用了。我们可以用前面的知识,建立一个服务端,用户端进行连接访问

在这里插入图片描述

2. 自定义应用层协议


应用层


Linux网络应用层是网络通信架构中的关键组成部分,它直接与用户应用交互,并负责数据的格式化和传输控制。

在应用层,数据通常以结构化的形式存在,在传输之前,这些数据需要被 序列化 成字符串形式,以便在网络中传输。接收方在收到数据后,再进行 反序列化 操作,将数据还原为原始的结构化形式。

⭐自定义协议 — TCP


  1. 协议设计:根据业务需求设计协议的数据格式、传输方式和控制信息。确保协议具有足够的灵活性和可扩展性。
  2. 协议实现:使用编程语言(如C、C++、Python等)实现协议的数据封装、解析和传输功能。通常需要使用套接字编程接口来与底层网络通信机制进行交互。
  3. 协议结构
  • 协议头:通常包含版本号和协议长度等信息。我们这里简单实现一下,所以用的是协议长度
  • 协议体:包含实际传输的数据。
  1. 序列化和反序列化
  • 序列化:将计算机语言中的内存对象转换为网络字节流的过程。
  • 反序列化:将网络字节流转换为计算机语言中的内存对象的过程。

在这里插入图片描述
我们今天自定义应用层协议是针对网络版计算器而言的,所以我们依据 “先描述,再组织”,先构建两个结构体来描述输入(Request)输出(Response) 以及 工厂(Factory) 来快速构建输入输出

输入(Request):

const std::string ProtSep = " ";
const std::string LineBreakSep = "\n";

class Request
{
public:
	Request() : _data_x(0), _data_y(0), _oper(0)
    {}
    Request(int x, int y, char op) : _data_x(x), _data_y(y), _oper(op)
    {}
    // 结构化数据 -> 字符串
    bool Serialize(std::string *out)
    {
    	*out = std::to_string(_data_x) + ProtSep + _oper + ProtSep + std::to_string(_data_y);
    	return true;
    }
    // 字符串 -> 结构化数据
	bool Deserialize(std::string &in) // "x op y"
    {
    	auto left = in.find(ProtSep);
        if (left == std::string::npos)
        	return false;

        auto right = in.rfind(ProtSep);
        if (right == std::string::npos)
        	return false;

        _data_x = std::stoi(in.substr(0, left));
        _data_y = std::stoi(in.substr(right + ProtSep.size()));
        std::string oper = in.substr(left + ProtSep.size(), right - (left + ProtSep.size()));
        
        if (oper.size() != 1) return false;

        _oper = oper[0];
    	return true;
    }
    int GetX() { return _data_x; }
    int GetY() { return _data_y; }
	char GetOper() { return _oper; }

private:
	// _data_x _oper _data_y
    // 报文的自描述字段
    // "len\nx op y\n" : \n不属于报文的一部分, 约定
    int _data_x; // 第一个参数
    int _data_y; // 第二个参数
    char _oper;  // + - * / %
};

在这里插入图片描述
输出(Response):



class Response
{
public:
    Response() : _result(0), _code(0)
    {}

	Response(int result, int code)
    	: _result(result), _code(code)
    {}

    // 结构化数据 -> 字符串
    bool Serialize(std::string *out)
    {
    	*out = std::to_string(_result) + ProtSep + std::to_string(_code);
        return true;
    }
	bool Deserialize(std::string &in) // "_result _code"
    {
     	auto pos = in.find(ProtSep);
        if (pos == std::string::npos)
        	return false;

        _result = std::stoi(in.substr(0, pos));
        _code = std::stoi(in.substr(pos + ProtSep.size()));

    	return true;
    }
    void SetResult(int result) { _result = result; }
    void SetCode(int code) { _code = code; }

    int GetResule() { return _result; }
	int GetCode() { return _code; }

private:
	int _result; // 运算结果
    int _code;   // 运算状态
};

工厂(Factory):

class Factory
{
public:
	std::shared_ptr<Request> BuildRequest()
	{
		std::shared_ptr<Request> req = std::make_shared<Request>();
		return req;
	}
	std::shared_ptr<Request> BuildRequest(int x, int y, char op)
	{
		std::shared_ptr<Request> req = std::make_shared<Request>(x, y, op);
		return req;
	}

	std::shared_ptr<Response> BuildResponse()
    {
		std::shared_ptr<Response> resp = std::make_shared<Response>();
		return resp;
	}

	std::shared_ptr<Response> BuildResponse(int result, int code)
	{
    	std::shared_ptr<Response> resp = std::make_shared<Response>(result, code);
    	return resp;
	}
};

在解决完协议主要的步骤后,我们需要解决协议结构,报头 + 报文,因此我们需要让它们变成我们想要的样子,在Protocol.hpp中封装两个解决协议结构的函数

给报文加一个内容长度的报头:

// "len\nx op y\n" : \n不属于报文的一部分, 约定
// 给报文加一个内容长度的报头
std::string Encode(const std::string &message)
{
	std::string len = std::to_string(message.size());
	std::string package = len + LineBreakSep + message + LineBreakSep;
	return package;
}

无法保证package就是一个完整独立的报文:
在这里插入图片描述

bool Decode(std::string &package, std::string *message)
{
	// 判断报文的完整性
    auto pos = package.find(LineBreakSep);
    if (pos == std::string::npos)
    	return false;

    std::string lens = package.substr(0, pos);
    int messagelen = std::stoi(lens);
    int total = lens.size() + messagelen + 2 * LineBreakSep.size();
    if (package.size() < total)
    	return false;
    // 至少package中有一个完整的报文
    *message = package.substr(pos + LineBreakSep.size(), messagelen);
    package.erase(0, total);
    return true;
}

3. 网络版计算器


Calculate:

enum{
    Success = 0,
    DivZeroErr,
    ModZeroErr,
    UnKnowOper,
};

class Calculate
{
public:
    Calculate()
    {}

    std::shared_ptr<Protocol::Response> Cal(std::shared_ptr<Protocol::Request> req)
    {
        std::shared_ptr<Protocol::Response> resp = factory.BuildResponse();
        resp->SetCode(Success);
        switch(req->GetOper())
        {
        case '+':
            resp->SetResult(req->GetX() + req->GetY());
            break;
        case '-':
            resp->SetResult(req->GetX() - req->GetY());
            break;
        case '*':
            resp->SetResult(req->GetX() * req->GetY());
            break;
        case '/':
        {
            if(req->GetY() == 0)
            {
                resp->SetCode(DivZeroErr);
            }
            else
            {
                resp->SetResult(req->GetX() / req->GetY());
            }
        }
            break;
        case '%':
        {
            if(req->GetY() == 0)
            {
                resp->SetCode(ModZeroErr);
            }
            else
            {
                resp->SetResult(req->GetX() % req->GetY());
            }
        }
        break;
        default:
            resp->SetCode(UnKnowOper);
            break;
        }

        return resp;
    }

    ~Calculate()
    {}
private:
    Protocol::Factory factory;
};

在构建好以上的代码时,我们只需要在服务器与客户端中展开使用即可,这里就不过多展开,唯一值得注意的就是我们在使用线程时的回调函数,以及我们对客户请求的处理
在这里插入图片描述

在这里插入图片描述

TcpServerMain.cc
TcpServer.hpp
TcpClientMain.cc

自定义应用层协议代码仓库

网络版计算器

4. 总结


回顾整个学习过程,我们不难发现,自定义应用层协议是一项复杂而细致的工作。它要求我们具备全面的知识体系,从底层网络通信到高层应用逻辑,每一个环节都需要我们投入大量的时间和精力去学习和实践。然而,正是这份努力和坚持,让我们在解决问题的过程中不断成长,收获了宝贵的知识和经验。

在此,我们衷心希望本文能够为你提供一份有价值的参考和启示,帮助你在Linux下自定义应用层协议的道路上走得更远、更稳。

在这里插入图片描述

希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!

在这里插入图片描述

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部