上一章:网络编程套接字_吕世雄的技术博客_51CTO博客

一、认识协议

TCP是面向字节流,write发了五次,对端可能read一次就收完了,也可能要多次才收完。对端读多少次,和发了多少次无关。此时如果读到的数据是多次发送的数据,就需要由我们自己来讲这些数据分开。那我如何知道要怎么分开这些数据呢?它们之间由有边界吗?想要解决,就需要协议。

协议就是大家认同的一种“约定”。


想象给朋友发送消息的常见,:

应用层协议_TCP

通过上面这张图,我们初步认识了序列化与反序列化。

可是这和协议有啥关系呢?如果把昵称、时间、消息内容封装为一个结构体,发送方与接收方都使用这个结构体来处理数据。这个结构体,就可以说是协议,这是发送放与接收方的约定嘛,协议,就是约定。

应用层协议_协议_02

但是,这种结构体的类型来制定应用层协议并不推荐,因为接收方和发送方的操作系统可能存在差异,结构体在不同的运行环境下可能存在差异,跨平台性太差。所以应用层协议一般不这么定义。

所以我们不能把这些结构体类型直接通过网络发送给对端,而是要通过序列化把结构体数据变为一个字符串,再通过网络发送给对端,对端收到后,把字符串转化成结构体数据。

  • 序列化:将协议对应的结构体数据转化为“字符串”(字节流)。
  • 反序列化:将字节流转化为结构体数据。

为什么要进行序列化?最重要的就是方便网络发送

为什么要进行反序列化呢?为了上层业务能更方便的提取有效数据

二、实现网络版计算器

接下来,我们要写一个网络版计算器,并且在其中实现自定义协议。

1、v1版本

这个版本直接用结构体来进行网络传输。

(1)自定义协议

//Protocol.hpp
#pragma once
#include <iostream>
#include <memory>
class Request//请求
{
public:
    Request()
    {}
    Request(int x,int y,int oper)
        :_x(x),_y(y),_oper(oper)
    {}
    void Debug()
    {
        std::cout<<_x<<_oper<<_y<<std::endl;
    }
    void Inc()
    {
        ++_x;
        ++_y;
    }
private:
    int _x;//第一个参数
    int _y;//第二个参数
    char _oper;//运算符,+-*/
};
class Response//回应
{
public:
    Response()
    {}
    Response(int result,bool code)
        :_result(result),_code(code)
    {}
private:
    int _result;//运算结果
    bool _code;//运算状态
};
//工厂模式
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 oper)
    {
        std::shared_ptr<Request> req=std::make_shared<Request>(x,y,oper);
        return req;
    }
    std::shared_ptr<Response> BuildResponse()
    {
        std::shared_ptr<Response> res=std::make_shared<Response>();
        return res;
    }
    std::shared_ptr<Response> BuildResponse(int result,bool code)
    {
        std::shared_ptr<Response> res=std::make_shared<Response>(result,code);
        return res;
    }
};

既然是协议,那么server和client都要能使用Request和Response。那么以后写的serevr和client必然都需要包含Protocol.hpp这个头文件。

(2)实现Socket.hpp

就是把套接字操作封装起来。

//Socket.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#include <iostream>
#include <string>

namespace Net_Work
{
    enum
    {
        CreateErr = 1,
        BindErr,
        ListenErr,
    };

    class Socket
    {
    public:
        Socket(){};
        virtual ~Socket() {}
        virtual void CreateSocketOrDie() = 0;
        virtual void BindSocketOrDie(uint16_t port) = 0;
        virtual void ListenSocketOrDie(int backlog) = 0;
        virtual Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) = 0;
        virtual bool ConnectServer(std::string serverip, uint16_t serverport) = 0;
        virtual int GetSockfd() = 0;
        virtual void SetSockfd(int sockfd) = 0;
        virtual void CloseSockfd() = 0;
        virtual bool Recv(std::string *out, int size) = 0;
        virtual bool Send(const std::string &in) = 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)
        {
            SetSockfd(sockfd);
        }
    };

    class Tcp_Socket : public Socket
    {
    public:
        Tcp_Socket(int sockfd = -1)
            : _sockfd(sockfd){};
        void CreateSocketOrDie() override // 创建套接字
        {
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0) // 创建失败
            {
                exit(CreateErr);
            }
        }
        void BindSocketOrDie(uint16_t port) override // 绑定套接字
        {
            // 解决绑定失败问题
            int opt = 1;
            setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

            // 2.绑定
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = INADDR_ANY;
            int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
            if (n < 0)
            {
                exit(BindErr);
            }
        }
        void ListenSocketOrDie(int backlog) override // 设置套接字为监听状态
        {
            int m = listen(_sockfd, backlog);
            if (m < 0)
            {
                exit(ListenErr);
            }
        }
        Tcp_Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) override // 获取连接
        {
            sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int n = accept(_sockfd, (struct sockaddr *)&peer, &len);
            if (n < 0)
            {
                return nullptr;
            }
            *peerip = inet_ntoa(peer.sin_addr);
            *peerport = ntohs(peer.sin_port);
            Tcp_Socket *ret = new Tcp_Socket(n);
            return ret;
        }
        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);
            inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);
            int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n == 0)
                return true;
            else
                return false;
        }
        int GetSockfd() override//获取Sockfd
        {
            return _sockfd;
        }
        void SetSockfd(int sockfd) override//设置Sockfd
        {
            _sockfd = sockfd;
        }
        void CloseSockfd() override//关闭Sockfd
        {
            if (_sockfd > -1)
                close(_sockfd);
        }
        bool Recv(std::string *out, int size) override//读取
        {
            char buffer[size];
            ssize_t n = recv(_sockfd, buffer, size - 1, 0);
            if (n <= 0)
            {
                return false;
            }
            buffer[n] = '\0';
            *out += buffer;
            return true;
        }
        bool Send(const std::string &in) override//写入
        {
            ssize_t n = send(_sockfd, in.c_str(), in.size(), 0);
            if (n < 0)
                return false;
            else
                return true;
        }

        ~Tcp_Socket() {}

    private:
        int _sockfd;
    };
}

(4)完整代码

protocol_v1 · 吕世雄/Linux_test - 码云 - 开源中国 (gitee.com)

(5)测试

应用层协议_协议_03

经过我们的测试,server和client在直接传输结构体时没有出现任何问题啊。这是因为此时测试的server和client都是在同一台机器上,运行环境都是相同的,如果在不同的运行环境上,是可能会出问题的。

2、v2版本

TCP建立连接后,server可以给client发消息,client也可以给server发消息,二者都是有发送缓冲区与接收缓冲区的。当我们把数据从client调用send/write发送给server时,并不是直接就发送过去,而是拷贝到发送缓冲区,此时send/write的工作就结束了。也就是说,其实我们调用的send/write并么有把数据发送到网络中,只是把数据从应用层拷贝到了内核的缓冲区中。同理,recv/read也只是执行拷贝工作。如果发送缓冲区满了,send/write就会出错,接收缓冲区满了,recv/read就会出错。

TCP之所以是全双工的,就是因为发送缓冲区和接收缓冲区是分离的,互不影响。

应用层协议_TCP_04

缓冲区中的数据,什么时候发?发多少?出错了怎么办?完全由内核决定,具体一点就是完全由TCP协议决定。

TCP实际通信时,其实是双方操作系统之间进行通信。

假设此时client想要向server发送一个helloworld,当send/write把helloworld拷贝到发送缓冲区后,发现对方的接受缓冲区只剩下五个字节的空间了,于是就只能发hello过去。

应用层协议_TCP_05

所以TCP下发送方发了多少字节,接收方不一定就要接收这么多字节。UDP则不一样,如果发,那就一定发完整的报文,否则就干脆不发了。

在假设client要发送两条报文,一条是helloworld,一条是good,这两条消息都会被拷拷贝到发送缓冲区,再发出去。当server收到了helloworldgood时,他该如何知道这是两条报文,还是一条报文,或者三条报文呢?

应用层协议_协议_06

UDP就不会有这个问题,因为它没有发送缓冲区。

如何确定收到的报文是否是完整的呢?如何分离多条报文呢?可以通过协议,确定报文的边界,如果读到边界了,也就能确认读完了一条报文。同时也能区分开多条报文(解决粘包问题)。


重新回到网络版计算器:

(1)自定义协议

相比于v1版本,现在我们要解决两个问题:1.结构化数据的序列化与反序列化问题。2.用户区分报文边界的问题。

先解决第一个问题,现在把第一个参数x,第二个参数y,操作符op序列化为 “x op y” ,用空格分开参数与操作符。

接下来解决第二个问题,op的长度一定是1个字节,但是x和y是变长的。为了方便定义报文边界,于是在前面再加一个len,代表有效载荷的长度, "len""x op y" 。这个len就相当于报文的报头,可以通先读到len,确定接下来的有效载荷要读多少个字节能读完。这个len就是报文的自描述字段。

但是。如何保证读到了一个完整的len呢?有点鸡生蛋蛋生鸡的问题。在len和有效载荷中间加一个\n, "len"\n"x op y" 。读到\n了,就一定读到了一个完整的len。

诶?问题又来了,既然\n能保证读到一个完整的len,那为什么不用 "x op y"\n 呢,这样也能保证读到完整的报文了。在写网络版计算器中,确实不要len也没问题,因为现在我们的有效载荷里没有\n。但是以后我们的报文的有效载荷如果是文本呢?图片呢?视频呢?之所以要加入len是为了保证通用性,我能保证len里面一定不包含\n,但是不能保证有效载荷里一定没有\n呀。如果报文的有效载荷里本就含有\n,那就会影响到我们判断报文边界,此时就需要用len来确实报文长度了。 

我们一个报文的格式应该是"len\nx op y"。

收到多个报文时就是"len\nx op y""len\nx op y""len\nx op y""len\nx op y"。也没问题,也能区分报文边界。但是,为了方便打印收到的报文,于是决定在一个报文的末尾再加一个\n。\n是不会纳入有效载荷长度的。

所以,一个报文的最终格式应该是"len\nx op y\n"

//Protocol.hpp
#pragma once
#include <iostream>
#include <memory>
#include <string>
namespace ProtocolNS
{
    const std::string ProtSep = " ";
    const std::string MessageSep = "\n";

    //message可能是 "x op y"或者"resulet code"
    std::string Encode(const std::string &message) // 封装
    {
        std::string len = std::to_string(message.size());//len
        std::string package = len + MessageSep + message + MessageSep;
        //拼接 "len\nmessage\n"
        return package;
    }
    //我无法保证package是一个完整的报文,它可能是如下的多种的情况:
    //"l"
    //"le"
    //"len"
    //"len\n"
    //"len\nx"
    //"len\nx "
    //"len\nx op"
    //"len\nx op "
    //"len\nx op y"
    //"len\nx op y\n"
    //"len\nx op y\n""len\n"
    bool Decode(std::string &package, std::string *message) // 解包
    {
        int pos = package.find(MessageSep);//寻找第一个\n
        if (pos == std::string::npos)
            return false;//没找着,那就不用解包了,连一个完整的len都没有
        std::string lens = package.substr(0, pos);//获取len
        int messagelen = std::stoi(lens);//有效载荷的长度
        int total = messagelen + lens.size() + 2 * MessageSep.size(); //一整条报文的长度
        if (package.size() < total)//没有一条完整的报文
            return false;
        *message = package.substr(pos + 1, messagelen);//从报文中提取有效载荷可能是"x op y"或者"resulet code"
        package.erase(0, total);//删掉已经处理完的报文
        return true;
    }

    class Request // 请求
    {
    public:
        Request()
            : _x(0), _y(0), _oper('+')
        {}
        Request(int x, int y, int oper)
            : _x(x), _y(y), _oper(oper)
        {}
        void Debug()
        {
            std::cout << _x << _oper << _y << std::endl;
        }
        void Inc()
        {
            ++_x;
            ++_y;
        }
        int Getx()//获取第一个参数
        {
            return _x;
        }
        int Gety()//获取第二个参数
        {
            return _y;
        }
        char Getoper()//获取运算符
        {
            return _oper;
        }
        //"x op y"
        bool Serialize(std::string *out) // 序列化
        {
            //把结构体数据转化为字符串
            *out = std::to_string(_x) + ProtSep + _oper + ProtSep + std::to_string(_y);
            return true;
        }
        //"x op y"
        bool Deserialize(const std::string &in) // 反序列化
        {
            //把字符串转化为结构体数据
            int left = in.find(ProtSep, 0);
            if (left == std::string::npos)
                return false;
            int right = in.find(ProtSep, left + 1);
            if (right == std::string::npos)
                return false;
            _x = std::stoi(in.substr(0, left));
            _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;
        }

    private:
        int _x;//第一个参数
        int _y;//第二个参数
        char _oper;//操作符,+-*/
    };
    class Response // 回应
    {
    public:
        Response()
            : _result(0), _code(true)
        {}
        Response(int result, bool code)
            : _result(result), _code(code)
        {}
        void Setresult(int result)//设置运算结果
        {
            _result = result;
        }
        void SetCode(bool code)//设置运算状态
        {
            _code = code;
        }
        //"result code"
        bool Serialize(std::string *out) //序列化
        {
            //把结构体数据转化为字符串
            *out = std::to_string(_result) + ProtSep + std::to_string(_code);
            return true;
        }
        //"result code"
        bool Deserialize(const std::string &in) // 反序列化
        {
            //把字符串转化为结构体数据
            int pos = in.find(ProtSep, 0);
            if (pos == std::string::npos)
                return false;
            _result = std::stoi(in.substr(0, pos));
            _code = std::stoi(in.substr(pos + ProtSep.size()));
            return true;
        }

    private:
        int _result;//运算结果
        bool _code;//运算状态
    };
    //工厂模式
    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 oper)
        {
            std::shared_ptr<Request> req = std::make_shared<Request>(x, y, oper);
            return req;
        }
        std::shared_ptr<Response> BuildResponse()
        {
            std::shared_ptr<Response> res = std::make_shared<Response>();
            return res;
        }
        std::shared_ptr<Response> BuildResponse(int result, bool code)
        {
            std::shared_ptr<Response> res = std::make_shared<Response>(result, code);
            return res;
        }
    };
}

(2)业务处理

收到报文,并从中提取有效载荷生成Request,此时就需要对Request处理了,出完完毕后,生成Response,再把Response发回去。

#include "Protocol.hpp"
#include "Socket.hpp"
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>


int main(int argc,char* args[])
{
    if(argc!=3)
    {
        std::cout<<"请按照格式使用:./server ip port"<<std::endl;
        return 1;
    }
    std::string serverip=args[1];
    uint16_t serverport=atoi(args[2]);

    Net_Work::Tcp_Socket socket;
    auto n=socket.BuildConnectSocketMethod(serverip,serverport);
    if(!n) 
    {
        std::cout<<"连接失败"<<std::endl;
        return 1;
    }

    ProtocolNS::Factory f;
    std::shared_ptr<ProtocolNS::Request> req=f.BuildRequest(10,10,'+');
    while(true)
    {
        //1.序列化
        std::string message;
        auto r=req->Serialize(&message);
        if(!r)
            std::cout<<"序列化失败"<<std::endl;
        std::cout<<"序列化成功,message--->"<<message<<std::endl;
        //2.封装
        std::string package=ProtocolNS::Encode(message);
        std::cout<<"封装成功,package--->"<<package<<std::endl;
        
        //3.发送
        ssize_t n=send(socket.GetSockfd(),package.c_str(),sizeof(*req),0);
        if(n<0)
        {
            std::cout<<"发送失败,errno="<<errno<<std::endl;
            continue;
        }
        req->Inc();
        socket.CloseSocket();
        sleep(1);
    }
    std::cout<<"client 退出"<<std::endl;
    return 0;
}

(3)完整代码

protocol_v2 · 吕世雄/Linux_test - 码云 - 开源中国 (gitee.com)

(4)测试

应用层协议_协议_07

3、v3版本

相比v2版本的自定义序列反序列化,v3版本将引入更成熟的序列反序列化方案——json。

用条件编译让使用者可以选择使用自定义序列反序列化方案或者json方案。相比v2版本的代码,变化集中在Protocol.hpp中。

(1)完整代码


(2)测试

应用层协议_协议_08

下一章:http协议_吕世雄的技术博客_51CTO博客