您现在的位置是:首页 >技术杂谈 >Linux/C++:基于TCP协议实现网络版本计算器(自定义应用层协议)网站首页技术杂谈

Linux/C++:基于TCP协议实现网络版本计算器(自定义应用层协议)

聪明且普信 2023-06-06 00:00:02
简介Linux/C++:基于TCP协议实现网络版本计算器(自定义应用层协议)

目录

Sock.hpp

TcpServer.hpp

Protocol.hpp

CalServer.cc

CalClient.cc

分析


因为,TCP面向字节流,所以TCP有粘包问题,故我们需要应用层协议来区分每一个数据包。防止读取到半个,一个半数据包的情况。

Sock.hpp

#pragma once

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

// 对于一些TCP相关调用的封装
class Sock
{
private:
    const static int gback_log = 20;
public:
    int Socket()
    {
        // 1. 创建套接字,成功返回对应套接字,失败直接进程exit
        int listen_sock = socket(AF_INET, SOCK_STREAM, 0); // 网络套接字, 面向字节流(tcp)
        if (listen_sock < 0)
        {
            logMessage(FATAL, "create listen socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create listen socket success: %d", listen_sock); // 1111Log
        return listen_sock;
    }
    void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        // 2. bind,注意云服务器不能绑定公网IP,不允许。
        // 成功bind则成功bind,失败进程exit(bind不在循环语句内,故失败直接进程退出。)
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = inet_addr(ip.c_str());
        if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }
    }
    void Listen(int sock)
    {
        // 3. listen监听: 因为TCP是面向连接的,在我们正式通信之前,需要先建立连接
        // listen: 将套接字状态设置为监听状态。服务器要一直处于等待状态,这样客户端才能随时随地发起连接。
        // 成功则成功,失败则exit
        if (listen(sock, gback_log) < 0) // gback_log后面讲,全连接队列的长度。我猜测就是这个服务器同一时刻允许连接的客户端的数量最大值?也不太对呀,这么少么?
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "listen success");
    }
    // 一般经验
    // const std::string &: 输入型参数
    // std::string *: 输出型参数
    // std::string &: 输入输出型参数
    int Accept(int sock, uint16_t *port, std::string *ip)
    {
        // accept失败进程不退出,返回-1
        // 成功则返回对应的通信套接字
        struct sockaddr_in client;
        socklen_t len = sizeof client;
        // 其实accept是获取已经建立好的TCP连接。建立好的连接在一个内核队列中存放,最大数量的第二个参数+1
        int service_sock = accept(sock, (struct sockaddr *)&client, &len); // 返回一个用于与客户端进行网络IO的套接字,不同于listen_sock
        // On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket.  On error, -1 is returned, and errno is set appropriately.
        if (service_sock < 0)
        {
            logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
            return -1;  // accept失败不直接exit,而是返回-1。因为在循环语句内部。
        }
        if (port)
            *port = ntohs(client.sin_port);
        if (ip)
            *ip = inet_ntoa(client.sin_addr);
        logMessage(NORMAL, "link(accept) success, service socket: %d | %s:%d", service_sock,
                   (*ip).c_str(), *port);
        return service_sock;
    }
    int Connect(int sock, const std::string &ip, const uint16_t &port)
    {
        // 惯例写一下:失败返回-1,成功则客户端与服务端连接成功,返回0
        struct sockaddr_in server;
        memset(&server, 0, sizeof server);
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr(ip.c_str());
        server.sin_port = htons(port);
        if (connect(sock, (struct sockaddr *)&server, sizeof server) < 0)
        {
            return -1;
        }
        return 0;
    }
public:
    Sock() = default;
    ~Sock() = default;
};

TcpServer.hpp

#ifndef _TCP_SERVER_HPP_
#define _TCP_SERVER_HPP_

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

// 说实话,这个TcpServer类实现的非常棒,真的很棒,网络和服务进行了解耦。
// 使用者直接BindServer, 然后start即可
namespace ns_tcpserver
{
    using func_t = std::function<void(int socket)>; // 服务器提供的服务方法类型void(int),可变

    class TcpServer;
    class ThreadData
    {
    public:
        ThreadData(int sock, TcpServer *server)
            : _sock(sock), _server(server)
        {}
        ~ThreadData() {}
    public:
        int _sock;
        TcpServer *_server; // 因为静态成员函数呀
    };

    class TcpServer
    {
        // 不关心bind的ip和port,因为用不到啊,保留一个listen_sock用于accept就够了。
    private:
        int _listen_sock;
        Sock _sock;
        std::vector<func_t> _funcs; // 服务器提供的服务
    private:
        static void *threadRoutine(void *args)
        {
            pthread_detach(pthread_self());    // 线程分离(避免类似于僵尸进程状态)
            ThreadData *td = (ThreadData *)args;
            td->_server->excute(td->_sock); // 提供服务
            close(td->_sock);   // 保证四次挥手正常结束
            delete td;
            return nullptr;
        }
    public:
        TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
        {
            // 创建监听套接字,bind,listen
            _listen_sock = _sock.Socket();
            _sock.Bind(_listen_sock, port, ip);
            _sock.Listen(_listen_sock);
        }
        void start()
        {
            for (;;)
            {
                // 开始accept,然后执行任务
                std::string ip;
                uint16_t port;     // 这两个东西,也并没有传给线程。
                int sock = _sock.Accept(_listen_sock, &port, &ip); // 后面是输出型参数
                if (sock == -1)
                    continue; // 本次accept失败,循环再次accept。目前来看几乎不会

                // 连接客户端成功,ip port已有。但是这里没用...
                pthread_t tid;
                ThreadData *td = new ThreadData(sock, this);
                pthread_create(&tid, nullptr, threadRoutine, (void *)td); // 新线程去提供service,主线程继续accept
            }
        }
        void bindService(func_t service)   // 暴露出去的接口,用于设置该服务器的服务方法
        {
            _funcs.push_back(service);
        }
        void excute(int sock)
        {
            for (auto &func : _funcs)
            {
                func(sock);
            }
        }
        ~TcpServer()
        {
            if (_listen_sock >= 0)
                close(_listen_sock);
        }
    };
}
#endif

Protocol.hpp

#ifndef _PROTOCOL_HPP_
#define _PROTOCOL_HPP_

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <jsoncpp/json/json.h>

// important and new
namespace ns_protocol
{

// #define MYSELF 1 // 自己实现序列化反序列化还是使用json库

#define SPACE " "
#define SPACE_LENGTH strlen(SPACE)
#define SEP "
"
#define SEP_LENGTH strlen(SEP)

    // 请求和回复,都需要序列化和反序列化的成员函数
    // 序列化和反序列化双方都不同。但是添加报头和去报头是相同的,"Length
xxxxx
";
    // 客户端生成请求,序列化之后发送给服务端
    class Request
    {
    public:
        Request() = default;
        Request(int x, int y, char op)
            : _x(x), _y(y), _op(op)
        {
        }
        ~Request() {}

    public:
        int _x;
        int _y;
        char _op;

    public:
        std::string serialize()
        {
            // 序列化为"_x _op _y"  (注意,序列化和添加报头是分开的,反序列化和去掉报头是分开的
#ifdef MYSELF
            std::string s = std::to_string(_x);
            s += SPACE;
            s += _op;
            s += SPACE;
            s += std::to_string(_y);
            return s;
#else
            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 &s)
        {
#ifdef MYSELF
            // "_x _op _y"
            std::size_t left = s.find(SPACE);
            if (left == std::string::npos)
                return false;
            std::size_t right = s.rfind(SPACE);
            if (right == left)
                return false;
            _x = atoi(s.substr(0, left).c_str());
            _op = s[left + SPACE_LENGTH];
            _y = atoi(s.substr(right + SPACE_LENGTH).c_str());
#else
            Json::Value root;
            Json::Reader reader;
            reader.parse(s, root);
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _op = root["op"].asInt();
#endif
            return true;
        }
    };
    // 服务端收到请求,反序列化,业务处理生成response,序列化后发送给客户端
    class Response
    {
    public:
        Response(int result = 0, int code = 0)
            : _result(result), _code(code)
        {
        }
        ~Response() {}

    public:
        std::string serialize()
        {
            // 序列化为"_code _result"  (注意,序列化和添加报头是分开的,反序列化和去掉报头是分开的
#ifdef MYSELF
            std::string s = std::to_string(_code);
            s += SPACE;
            s += std::to_string(_result);
            return s;
#else
            Json::Value root;
            root["code"] = _code;
            root["result"] = _result;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }
        bool deserialize(const std::string &s)
        {
#ifdef MYSELF
            // "_code _result"
            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_LENGTH).c_str());
#else
            Json::Value root;
            Json::Reader reader;
            reader.parse(s, root);
            _result = root["result"].asInt();
            _code = root["code"].asInt();
#endif
            return true;
        }
    public:
        int _result;
        int _code; // 状态码, 防止除零,模零,和其他错误(比如非法运算符运算符)。code == 0时,result有效。
    };
    // 进行去报头,报文完整则去报头,并返回有效载荷,不完整则代表失败返回空字符串。
    std::string deCode(std::string &s) // 输入型输出型参数
    {
        // "Length
x op y
"   成功返回有效载荷,失败返回空串
        std::size_t left = s.find(SEP);
        if (left == std::string::npos)
            return "";
        std::size_t right = s.rfind(SEP);
        if (right == left)
            return "";
        int length = atoi(s.substr(0, left).c_str());
        if (length > s.size() - left - 2 * SEP_LENGTH)
            return ""; // 有效载荷长度不足,不是一个完整报文,其实经过上面两次的if判断已经够了可能。
        // 是一个完整报文,进行提取
        std::string ret;
        s.erase(0, left + SEP_LENGTH);
        ret = s.substr(0, length);
        s.erase(0, length + SEP_LENGTH);
        return ret;
    }
    std::string enCode(const std::string &s)
    {
        // "Length
1+1
"
        std::string retStr = std::to_string(s.size());
        retStr += SEP;
        retStr += s;
        retStr += SEP;
        return retStr;
    }
    // 我真的很想用引用,但是好像传统规则是输出型参数用指针...
    // 其实这个Recv就是一个单纯的读数据的函数,将接收缓冲区数据读到应用层缓冲区中,也就是*s中。存储的是对端发来的应用层报文。
    bool Recv(int sock, std::string *s)
    {
        // 仅仅读取数据到*s中
        char buff[1024];
        ssize_t sz = recv(sock, buff, sizeof buff, 0);
        if (sz > 0)
        {
            buff[sz] = '';
            *s += buff;    
            return true;
        }
        else if (sz == 0)
        {
            std::cout << "peer quit" << std::endl;
            return false;
        }
        else
        {
            std::cout << "recv error" << std::endl;
            return false;
        }
    }
    bool Send(int sock, const std::string &s)
    {
        ssize_t sz = send(sock, s.c_str(), s.size(), 0);
        if (sz > 0)
        {
            return true;
        }
        else
        {
            std::cout << "send error!" << std::endl;
            return false;
        }
    }
}

#endif

CalServer.cc

#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>

using namespace ns_tcpserver;
using namespace ns_protocol;

Response calculatorHelp(const Request &req)
{
    // "1+1"???
    Response resp;
    int x = req._x;
    int y = req._y;
    switch (req._op)
    {
    case '+':
        resp._result = x + y;
        break;
    case '-':
        resp._result = x - y;
        break;
    case '*':
        resp._result = x * y;
        break;
    case '/':
        if (y == 0)
            resp._code = 1;
        else
            resp._result = x / y;
        break;
    case '%':
        if (y == 0)
            resp._code = 2;
        else
            resp._result = x % y;
        break;
    default:
        resp._code = 3;
        break;
    }
    return resp;
}

void calculator(int sock)
{
    std::string s;
    for (;;)
    {
        if (Recv(sock, &s) <= 0) // 输出型参数
            break;  // 大概率对端退出,则服务结束。一般不会读取失败recv error
        std::string package = deCode(s);
        if (package.empty())
            continue; // 不是一个完整报文,继续读取(因为TCP面向字节流!!!)
        // 读取到一个完整报文,且已经去了应用层报头,有效载荷在package中。如"1 + 2"
        Request req;
        req.deserialize(package);
        Response resp = calculatorHelp(req);
        std::string backStr = resp.serialize();
        backStr = enCode(backStr);
        if (!Send(sock, backStr)) // 发送失败就退出
            break;
    }
}

// ./cal_server port
int main(int argc, char **argv)
{
    // std::cout << "test remake" << std::endl;  // success
    if (argc != 2)
    {
        std::cout << "
Usage: " << argv[0] << " port
"
                  << std::endl;
        exit(1);
    }
    std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
    server->bindService(calculator); // 给服务器设置服务方法,将网络服务和业务逻辑进行解耦
    server->start();                 // 服务器开始进行accept,连接一个client之后就提供上方bind的服务
    return 0;
}

CalClient.cc

#include "Protocol.hpp"
#include "Sock.hpp"
#include <memory>

using namespace ns_protocol;

// ./client serverIp serverPort
int main(int argc, char **argv)
{
    if (argc != 3)
    {
        std::cout << "
Usage: " << argv[0] << " serverIp serverPort
"
                  << std::endl;
        exit(1);
    }
    Sock sock;
    int sockfd = sock.Socket();
    // 客户端不需要显式bind, 老生常谈了。
    if (sock.Connect(sockfd, argv[1], atoi(argv[2])) == -1)
    {
        std::cout << "connect error" << std::endl;
        exit(3);
    }
    std::string backStr;  // 
    bool quit = false;
    while (!quit)
    {
        Request req;
        std::cout << "Please enter# ";
        std::cin >> req._x >> req._op >> req._y;
        std::string reqStr = req.serialize();
        reqStr = enCode(reqStr); // 添加应用层报头,此处添加报头(制定协议)是为了解决TCP粘包问题,因为TCP是面向字节流的。
        if (!Send(sockfd, reqStr))
            break;
        while (true)
        {
            if (!Recv(sockfd, &backStr))
            {
                quit = true;
                break;
            }
            std::string package = deCode(backStr);
            if(package.empty())
                continue;    // 这次不是一个完整的应用层报文,继续读取
            // 读取到一个完整的应用层报文,且已经去报头,获取有效载荷成功,在package中。(这个有效载荷是server发来的,计算结果)
            Response resp;
            resp.deserialize(package);
            switch (resp._code)
            {
            case 1:
                std::cout << "除零错误" << std::endl;
                break;
            case 2:
                std::cout << "模零错误" << std::endl;
                break;
            case 3:
                std::cout << "其他错误" << std::endl;
                break;
            default:
                std::cout << req._x << " " << req._op << " " << req._y << " = " << resp._result << std::endl;
                break;
            }
            break;  // 退出防止TCP粘包问题的循环。
        }
        // 进行下一次获取用户输入,进行计算。
    }
    close(sockfd);
    return 0;
}

分析

可以分为两个模块:网络通信模块,应用层模块(包括应用层协议,以及应用层计算器逻辑)。

网络模块中,Sock.hpp就是一个简单的对于系统调用的封装,TcpServer.hpp的设计很优雅,内部有一个std::vector<func_t> _funcs;即这个server提供的服务。对外提供一个BindServer的方法,可以指定这个服务器提供的服务。Start,为服务器开始方法,先accept获取与客户端建立好的连接,然后创建新线程给客户端提供服务,服务就是BindServer绑定的方法,类型为void(int)。

应用层协议:一个Request,一个Response。分别是客户端的请求(x,y,运算符)和服务端的响应(计算结果)。这两个类,都有序列化和反序列化的方法,便于网络传输。还有一个Encode添加报头和Decode去报头的方法,这个其实就是应用层协议的报头,大体格式为 Length xxxx 。目的就是解决TCP面向字节流所引起的粘包问题。
Recv内部就是一个recv调用,将读取的网络数据添加到一个输出型参数string*指向string的结尾。因为TCP粘包问题,所以可能读取的不是一个完整报文(半个?),故,在Decode方法内部,也就是去报头时,会检测此时是否有至少一个完整应用层报文。若有,则去报头,获取有效载荷。若没有则返回一个空串。上层可以通过判断是否为空串。判断是否读到了一个完整应用层报文,若没有,则再次Recv,直到读到一个完整应用层报文为止。所以,server和client在读取网络数据并去报头时,都是在while循环内部进行的。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。