[Linux网络】基本网络命令socket编写TCP应用层实现简易计算器

news2024/9/22 5:35:18

W...Y的主页 😊

代码仓库分享💕 


前言:我们在上篇博客中学习了使用socket套接字完成了UDP的网络编程,今天我们继续使用套接字完成TCP的学习。

首先我们先来了解一些网络指令,让大家可以在实现网络编程后查看一些与网络有关的系统信息。

目录

网络命令

Ping 命令

netstat

pidof

Socket 编程 TCP

TCP socket API 详解

V1 - Echo Server

应用层

再谈 "协议“

网络版计算器

序列化 和 反序列化

重新理解 read、write、recv、send 和 tcp 为什么支持全双工

开始实现

Socket 封装 

关于流式数据的处理 

Jsoncpp

Json::Value

定制协议


网络命令

Ping 命令

ping命令主要是检测网络连通性的命令,当我们登录xshell时连接不上服务器,回头发现是网络的问题,我们就可以先使用ping命令进行检测。

ping + 想要访问服务器的URL即可查询。

 我们使用ping命令后都是死循环一直进行检测的,所以想要检测一次就可以使用 -c次数的选项执行后就可以检测对应的次数。

netstat

netstat 是一个用来查看网络状态的重要工具.
语法:netstat [选项]
功能:查看网络状态

常用选项:
• n 拒绝显示别名,能显示数字的全部转化成数字
• l 仅列出有在 Listen (监听) 的服務状态
• p 显示建立相关链接的程序名
• t (tcp)仅显示 tcp 相关选项 
• u (udp)仅显示 udp 相关选项
• a (all)显示所有选项,默认不显示 LISTEN 相关

 当我们执行UDP/TCP服务时,我们可以使用平ps ajx配合管道grep查看对应的信息,但是看不到相关网络信息,所以我们可以使用netstat指令加上选项就看也查看了。

pidof

在查看服务器的进程 id 时非常方便.
语法:pidof [进程名]
功能:通过进程名, 查看进程 id

当我们多开进程时使用ps -ajx就会查出很多进程pid,所以我们使用pidof + 进程名可以准确查出进程pid方便给进程发送信号。

Socket 编程 TCP

TCP socket API 详解

下面介绍程序中用到的 socket API,这些函数都在 sys/socket.h 中。

socket():

socket()打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描述符;
应用程序可以像读写文件一样用 read/write 在网络上收发数据;
如果 socket()调用出错则返回-1;
对于 IPv4, family 参数指定为 AF_INET;
对于 TCP 协议,type 参数指定为 SOCK_STREAM, 表示面向流的传输协议
protocol 参数的介绍从略,指定为 0 即可。 

bind():
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用 bind 绑定一个固定的网络地址和端口号; 
bind()成功返回 0,失败返回-1。
bind()的作用是将参数 sockfd 和 myaddr 绑定在一起, 使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号;
前面讲过,struct sockaddr *是一个通用指针类型,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen指定结构体的长度; 

我们的程序中对 myaddr 参数是这样初始化的:1.将整个结构体清零;(也可以使用memset函数)
2. 设置地址类型为 AF_INET;
3. 网络地址为 INADDR_ANY, 这个宏表示本地的任意 IP 地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址, 这样设置可以在所有的 IP 地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址;
4. 端口号为 SERV_PORT, 我们定义为 9999; 

上面就与UDP的socket大相径庭。 

 listen():

listen()声明 sockfd 处于监听状态, 并且最多允许有 backlog 个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是 5), 具体细节我们后面解释;
listen()成功返回 0,失败返回-1;

accept(): 

三次握手完成后, 服务器调用 accept()接受连接;
如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
addr 是一个传出参数,accept()返回时传出客户端的地址和端口号;
如果给 addr 参数传 NULL,表示不关心客户端的地址;
addrlen 参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区 addr 的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);

我们的服务器程序结构是这样的:

 重点是我们accept函数的返回值,返回值是一个int类型,代表是接受客户端的sockfd,以后使用的是accept函数返回的值进行接发送消息。就如同饭店拉客人一样,饭店门口有一个人是专门拉客人的,而他的任务只是拉客人没有服务客人,当将客人拉入饭店后就会又出来一个人服务你,给你点菜等等服务。而服务你的人就如同accept的返回值,拉客的人就是我们使用socket时返回的sockfd。

connect

客户端需要调用 connect()连接服务器;
connect 和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址, 而connect 的参数是对方的地址;
connect()成功返回 0,出错返回-1;

TCP的socket apl已经讲完现在我们进入时事件来写代码。

Echo Server

TcpServer.hpp 

#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>

#include "InetAddr.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR
};

const static int defaultsockfd = -1;
const static int gbacklog = 16;

class TcpServer;

class ThreadData
{
public:
    ThreadData(int fd, InetAddr addr, TcpServer *s):sockfd(fd), clientaddr(addr), self(s)
    {}
public:
    int sockfd;
    InetAddr clientaddr;
    TcpServer *self;
};

using task_t = std::function<void()>;

class TcpServer
{
public:
    TcpServer(int port) : _port(port), _listensock(defaultsockfd), _isrunning(false)
    {
    }
    void InitServer()
    {
        // 1. 创建流式套接字
        _listensock = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            LOG(FATAL, "socket error");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success, sockfd is : %d\n", _listensock);

        // 2. 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 = INADDR_ANY;

        int n = ::bind(_listensock, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind error");
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "bind success, sockfd is : %d\n", _listensock);

        // 3. tcp是面向连接的,所以通信之前,必须先建立连接。服务器是被连接的
        //    tcpserver 启动,未来首先要一直等待客户的连接到来
        n = listen(_listensock, gbacklog);
        if (n < 0)
        {
            LOG(FATAL, "listen error");
            exit(LISTEN_ERROR);
        }
        LOG(DEBUG, "listen success, sockfd is : %d\n", _listensock);
    }
    void Service(int sockfd, InetAddr client)
    {
        LOG(DEBUG, "get a new link, info %s:%d, fd : %d\n", client.Ip().c_str(), client.Port(), sockfd);

        std::string clientaddr = "[" + client.Ip() + ":" + std::to_string(client.Port()) + "]# ";
        while (true)
        {
            char inbuffer[1024];
            ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::cout << clientaddr << inbuffer << std::endl;

                std::string echo_string = "[server echo]# ";
                echo_string += inbuffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                // client 退出&&关闭连接了
                LOG(INFO, "%s quit\n", clientaddr.c_str());
                break;
            }
            else
            {
                LOG(ERROR, "read error\n", clientaddr.c_str());
                break;
            }
	    sleep(5);
	    break;
        }
	std::cout << "server开始退出" << std::endl;
	shutdown(sockfd, SHUT_RD);
	std::cout << "shut _ rd " << std::endl;
	sleep(10);
	//shutdown(sockfd, SHUT_WR);
	//std::cout << "shut _ wr " << std::endl;
        //::close(sockfd); // 文件描述符泄漏
    }
    static void *HandlerSock(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->self->Service(td->sockfd, td->clientaddr);
        delete td;
        return nullptr;
    }
    void Loop()
    {
        _isrunning = true;
        // 4. 不能直接接受数据,先获取连接
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = ::accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                continue;
            }
            // Version 0 : 一次只能处理一个请求 --- 不可能
            // Service(sockfd, InetAddr(peer));

            // Version 1: 采用多进程
            // pid_t id = fork();
            // if (id == 0)
            // {
            //     // child : 关心sockfd, 不关心listensock
            //     ::close(_listensock); // 建议
            //     if(fork() > 0) exit(0); 
            //     Service(sockfd, InetAddr(peer)); //孙子进程 -- 孤儿进程 --- 系统领养
            //     exit(0);
            // }

            // // father: 关心listensock,不关心sockfd
            // ::close(sockfd);
            // waitpid(id, nullptr, 0);

            // version 2: 采用多线程
            pthread_t t;
            ThreadData *td = new ThreadData(sockfd, InetAddr(peer), this);
            pthread_create(&t, nullptr, HandlerSock, td); //将线程分离

            // vesion 3: 采用线程池
            // task_t t = std::bind(&TcpServer::Service, this, sockfd, InetAddr(peer));
            // ThreadPool<task_t>::GetInstance()->Enqueue(t);
        }
        _isrunning = false;
    }
    ~TcpServer()
    {
        if (_listensock > defaultsockfd)
            ::close(_listensock);
    }

private:
    uint16_t _port;
    int _listensock;
    bool _isrunning;
};

在UDP中,服务器与客户端进行通信时只需要一个sockfd就可以进行通信,使用单进程/线程都可以进行,所以网络中的消息可能是任何一个用户发的。而TCP中双方要进行通信必须建立连接,而建立连接时每一个进程都有自己accept的sockfd,所以这里就得引入多线程/多进程来解决问题。

我们也可以使用线程池来解决,但是线程池中只有有限个线程,这里并不适合做这个事情,线程池适合将事情进行任务后结束接受下一个任务,而在这里可能会进行长时间的服务,所以如果用户不退出,可能只会有几个用户可以进行网络通信,这里不建议使用线程池。 

main.cc

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

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " local_port\n" << std::endl;
}

// ./tcpserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    EnableScreen();
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
    tsvr->InitServer();
    tsvr->Loop();

    return 0;
}

TcpClientMain.cc 

#include <iostream>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
              << std::endl;
}

// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // tcp client 要bind,不要显示的bind.
    struct sockaddr_in server;
    // 构建目标主机的socket信息
    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, (struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(3);
    }

    while(true)
    {
        std::cout << "Please Enter# ";
        std::string outstring;
        std::getline(std::cin, outstring);

        ssize_t s = send(sockfd, outstring.c_str(), outstring.size(), 0); //write
        if(s > 0)
        {
            char inbuffer[1024];
            ssize_t m = recv(sockfd, inbuffer, sizeof(inbuffer)-1, 0);
            if(m > 0)
            {
                inbuffer[m] = 0;
                std::cout << inbuffer<< std::endl;
            }
            else
            {
                break;
            }
        }
        else
        {
            break;
        }
    }
    shutdown(sockfd, SHUT_WR);
    //::close(sockfd);
    return 0;
}

上述中都与UDP相同,我们也可以在对象中加入回调函数,让其有对应的任务。

但是UDP与TCP不同的点就是UDP面向数据报,而TCP面向字节流,所以在TCP这里就会有一个问题读到的内容一定是完整的吗?

就比如abcdefg这一串字符串,当客户端一直发送abcdefg,服务器读字符串时就可能出现不完整,或者多读的场景,那我们应该怎么去做才能读到一个完整的字符串呢?这就要我们在应用层去实现。

应用层

我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层.

再谈 "协议“

协议是一种 "约定". socket api 的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些 "结构化的数据" 怎么办呢?

其实,协议就是双方约定好的结构化的数据

网络版计算器

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过
去, 然后由服务器进行计算, 最后再把结果返回给客户端.

约定方案一:
• 客户端发送一个形如"1+1"的字符串;
• 这个字符串中有两个操作数, 都是整形;
• 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
• 数字和运算符之间没有空格;
约定方案二:
• 定义结构体来表示我们需要交互的信息;
• 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按
照相同的规则把字符串转化回结构体;
• 这个过程叫做 "序列化" 和 "反序列化"

这样就可以解决TCP的字节流粘包问题。

序列化 和 反序列化

无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据,在另一端能够正确的进行解析, 就是 ok 的. 这种约定, 就是 应用层协议但是,为了让我们深刻理解协议,我们打算自定义实现一下协议的过程。
我们采用方案 2,我们也要体现协议定制的细节
我们要引入序列化和反序列化,只不过我们课堂直接采用现成的方案 -- jsoncpp库
我们要对 socket 进行字节流的读取处理 

重新理解 read、write、recv、send 和 tcp 为什么支持全双工

 

由上面这张图我们可以看出,操作系统会提供两个缓冲区,所以支持全双工。但是往缓冲区拿资源和放资源都是由函数来实现的。这些数据不是直接到网络中去,而是先被拷贝到缓冲区去,所以这些read、write、send、recv全都是拷贝函数。

所以什么时候发送到网络,怎么发、发错怎么办全是由TCP协议传输的。这里就会由我们之前所说粘包问题的本质。这就要我们上层协议自行判断数据是否完整。

总结:
在任何一台主机上,TCP 连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工。
这就是为什么一个 tcp sockfd 读写都是它的原因
实际数据什么时候发,发多少,出错了怎么办,由 TCP 控制,所以 TCP 叫做传输控制协议

 所以我们之前写的tcp的发送和接收都是由问题的,所以我们必须在应用层有很好方法来判断是否是完整一个字节流才行!

所以我们现在要写的计算器会写出具体做法,但是做法有很多,大家可以自己去想一个非常好的算法来识别。

开始实现

我们现在实现的代码可以将socket进行封装

Socket 封装 

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/types.h>
#include <memory>
#include "InetAddr.hpp"
#include "Log.hpp"

namespace socket_ns
{
    class Socket;
    const static int gbacklog = 8;
    using socket_sptr = std::shared_ptr<Socket>;

    enum
    {
        SOCKET_ERROR = 1,
        BIND_ERROR,
        LISTEN_ERROR,
        USAGE_ERROR
    };

    class Socket
    {
    public:
        virtual void CreateSocketOrDie() = 0;
        virtual void BindSocketOrDie(InetAddr &addr) = 0;
        virtual void ListenSocketOrDie() = 0;
        virtual socket_sptr Accepter(InetAddr *addr) = 0;
        virtual bool Connetcor(InetAddr &addr) = 0;
        virtual int SockFd() = 0;
        virtual int Recv(std::string *out) = 0;
        virtual int Send(const std::string &in) = 0;

    public:
    public:
        void BuildListenSocket(InetAddr &addr)
        {
            CreateSocketOrDie();
            BindSocketOrDie(addr);
            ListenSocketOrDie();
        }
        bool BuildClientSocket(InetAddr &addr)
        {
            CreateSocketOrDie();
            return Connetcor(addr);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket(int fd = -1)
            : _sockfd(fd)
        {
        }
        void CreateSocketOrDie() override
        {
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(FATAL, "socket error");
                exit(SOCKET_ERROR);
            }
            LOG(DEBUG, "socket create success, sockfd is : %d\n", _sockfd);
        }
        void BindSocketOrDie(InetAddr &addr) override
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(addr.Port());
            local.sin_addr.s_addr = inet_addr(addr.Ip().c_str());

            int n = bind(_sockfd, (sockaddr *)&local, sizeof(local));
            if (n < 0)
            {
                LOG(FATAL, "bind error");
                exit(BIND_ERROR);
            }
            LOG(DEBUG, "bind success, sockfd is : %d\n", _sockfd);
        }
        void ListenSocketOrDie() override
        {
            int n = listen(_sockfd, gbacklog);
            if (n < 0)
            {
                LOG(FATAL, "listen error");
                exit(LISTEN_ERROR);
            }
            LOG(DEBUG, "listen success, sockfd is : %d\n", _sockfd);
        }
        socket_sptr Accepter(InetAddr *addr) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_sockfd, (sockaddr*)&peer, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                return nullptr;
            }
            *addr = peer;
            socket_sptr sock = std::make_shared<TcpSocket>(sockfd);
            return sock;
        }
        bool Connetcor(InetAddr &addr) override
        {
             // tcp client 要bind,不要显示的bind.
            struct sockaddr_in server;
            // 构建目标主机的socket信息
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(addr.Port());
            server.sin_addr.s_addr = inet_addr(addr.Ip().c_str());

            int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                std::cerr << "connect error" << std::endl;
                return false;
            }
            return true;
        }
        int Recv(std::string *out) override
        {
            char inbuffer[1024];
            ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
            if (n > 0)
            {
                inbuffer[n] = 0;
                *out += inbuffer; // ??? +=
            }
            return n;
        }
        int Send(const std::string &in) override
        {
            int n = ::send(_sockfd, in.c_str(), in.size(), 0);
            return n;
        }
        int SockFd() override
        {
            return _sockfd;
        }

    private:
        int _sockfd;
    };
}

因为TCP与UDP的socket部分是相同的,所以我们可以使用继承多态的方式来创建socket对象,分别让对应的TCP或UDP来继承我们的socket类。

在TCP中我们进行accept会接收许多客户端,创建许多sockfd,所以我们的类成员就可以是其中_sockfd。

关于流式数据的处理 

你如何保证你每次读取就能读完请求缓冲区的所有内容?
你怎么保证读取完毕或者读取没有完毕的时候,读到的就是一个完整的请求呢?
处理 TCP 缓冲区中的数据,一定要保证正确处理请求

所以,完整的处理过程应该是:

 

Jsoncpp

Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字
符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,广泛用于各
种需要处理 JSON 数据的 C++ 项目中。

特性
1. 简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
2. 高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
3. 全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。
4. 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。
当使用 Jsoncpp 库进行 JSON 的序列化和反序列化时,确实存在不同的做法和工具类可供选择。安装

ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

序列化
序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件
中。Jsoncpp 提供了多种方式进行序列化:
1. 使用 Json::Value 的 toStyledString 方法:
优点:将 Json::Value 对象直接转换为格式化的 JSON 字符串。
示例:

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
std::string s = root.toStyledString();
std::cout << s << std::endl;
return 0;
}
$ ./test.exe
{
"name" : "joe",
"sex" : "男"
}

 使用 Json::StreamWriter

优点:提供了更多的定制选项,如缩进、换行符等。
示例:

#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::StreamWriterBuilder wbuilder; // StreamWriter 的
工厂
std::unique_ptr<Json::StreamWriter>
writer(wbuilder.newStreamWriter());
std::stringstream ss;
writer->write(root, &ss);
std::cout << ss.str() << std::endl;
return 0;
}
$ ./test.exe
{
"name" : "joe",
"sex" : "男"
}

使用 Json::FastWriter:
优点:比 StyledWriter 更快,因为它不添加额外的空格和换行符。 
 

#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
$ ./test.exe
{"name":"joe","sex":"男"}
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
// Json::FastWriter writer;
Json::StyledWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
$ ./test.exe
{
"name" : "joe",
"sex" : "男"
}

 在下述代码中我们使用的是第三种方法。

反序列化
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp 提供
了以下方法进行反序列化:

使用 Json::Reader:
优点:提供详细的错误信息和位置,方便调试。 

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main() {
// JSON 字符串
std::string json_string = "{\"name\":\"张三\",
\"age\":30, \"city\":\"北京\"}";
// 解析 JSON 字符串
Json::Reader reader;
Json::Value root;
// 从字符串中读取 JSON 数据
bool parsingSuccessful = reader.parse(json_string,
root);
if (!parsingSuccessful) {
// 解析失败,输出错误信息
std::cout << "Failed to parse JSON: " <<
reader.getFormattedErrorMessages() << std::endl;
return 1;
}
// 访问 JSON 数据
std::string name = root["name"].asString();
int age = root["age"].asInt();
std::string city = root["city"].asString();
// 输出结果
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "City: " << city << std::endl;
return 0;
}
$ ./test.exe
Name: 张三
Age: 30
City: 北京

使用 Json::CharReader 的派生类(不推荐了,上面的足够了):
在某些情况下,你可能需要更精细地控制解析过程,可以直接使用
Json::CharReader 的派生类。
但通常情况下,使用 Json::parseFromStream 或 Json::Reader 的 parse
方法就足够了。

总结
• toStyledString、StreamWriter 和 FastWriter 提供了不同的序列化选项,
你可以根据具体需求选择使用。
• Json::Reader 和 parseFromStream 函数是 Jsoncpp 中主要的反序列化工具,
它们提供了强大的错误处理机制。
• 在进行序列化和反序列化时,请确保处理所有可能的错误情况,并验证输入和输
出的有效性。 

Json::Value


Json::Value 是 Jsoncpp 库中的一个重要类,用于表示和操作 JSON 数据结构。以下是一些常用的 Json::Value 操作列表:
1. 构造函数
• Json::Value():默认构造函数,创建一个空的 Json::Value 对象。
• Json::Value(ValueType type, bool allocated = false):根据给定的ValueType(如 nullValue, intValue, stringValue 等)创建一个 Json::Value 对象。
2. 访问元素
• Json::Value& operator[](const char* key):通过键(字符串)访问对象中的元素。如果键不在,则创建一个新的元素。
• Json::Value& operator[](const std::string& key):同上,但使用std::string 类型的键。
• Json::Value& operator[](ArrayIndex index):通过索引访问数组中的元素。如果索引超出范围,则创建一个新的元素。
• Json::Value& at(const char* key):通过键访问对象中的元素,如果键不存在则抛出异常。
• Json::Value& at(const std::string& key):同上,但使用 std::string类型的键。
3. 类型检查
• bool isNull():检查值是否为 null。
• bool isBool():检查值是否为布尔类型。
• bool isInt():检查值是否为整数类型。
• bool isInt64():检查值是否为 64 位整数类型。
• bool isUInt():检查值是否为无符号整数类型。
• bool isUInt64():检查值是否为 64 位无符号整数类型。
• bool isIntegral():检查值是否为整数或可转换为整数的浮点数。
• bool isDouble():检查值是否为双精度浮点数。
• bool isNumeric():检查值是否为数字(整数或浮点数)。
• bool isString():检查值是否为字符串。
• bool isArray():检查值是否为数组。
• bool isObject():检查值是否为对象(即键值对的集合)。
4. 赋值和类型转换
• Json::Value& operator=(bool value):将布尔值赋给 Json::Value 对象。
• Json::Value& operator=(int value):将整数赋给 Json::Value 对象。
• Json::Value& operator=(unsigned int value):将无符号整数赋给Json::Value 对象。
• Json::Value& operator=(Int64 value):将 64 位整数赋给 Json::Value对象。
• Json::Value& operator=(UInt64 value):将 64 位无符号整数赋给Json::Value 对象。
• Json::Value& operator=(double value):将双精度浮点数赋给Json::Value 对象。
• Json::Value& operator=(const char* value):将 C 字符串赋给Json::Value 对象。
• Json::Value& operator=(const std::string& value):将 std::string赋给 Json::Value 对象。
• bool asBool():将值转换为布尔类型(如果可能)。
• int asInt():将值转换为整数类型(如果可能)。
• Int64 asInt64():将值转换为 64 位整数类型(如果可能)。
• unsigned int asUInt():将值转换为无符号整数类型(如果可能)。
• UInt64 asUInt64():将值转换为 64 位无符号整数类型(如果可能)。
• double asDouble():将值转换为双精度浮点数类型(如果可能)。
• std::string asString():将值转换为字符串类型(如果可能)。
5. 数组和对象操作
• size_t size():返回数组或对象中的元素数量。
• bool empty():检查数组或对象是否为空。
• void resize(ArrayIndex newSize):调整数组的大小。
• void clear():删除数组或对象中的所有元素。
• void append(const Json::Value& value):在数组末尾添加一个新元素。
• Json::Value& operator[](const char* key, const Json::Value&defaultValue = Json::nullValue):在对象中插入或访问一个元素,如果键不存在则使用默认值。
• Json::Value& operator[](const std::string& key, constJson::Value& defaultValue = Json::nullValue):同上,但使用 std::string类型的 

定制协议

 定制基本的结构化字段,这个就是协议 

#pragma once

#include <iostream>
#include <string>
#include<memory>
#include <jsoncpp/json/json.h>
#include<unistd.h>
namespace protocol_ns
{
    const std::string SEP = "\r\n";

    // 我们把tcp中读到的报文,可能读到半个,也可能读到1个半等, TCP 粘报问题
    // 解决TCP的粘报问题
    std::string Encode(const std::string &json_str)
    {
        int json_str_len = json_str.size();
        std::string proto_str = std::to_string(json_str_len);
        proto_str += SEP;
        proto_str += json_str;
        proto_str += SEP;
        return proto_str;
    }
    // "len"\r\n"{
    // "len"\r\n"{             }"
    // "len"\r\n"{             }"\r\n;
    // "len"\r\n"{             }"\r\n"len";
    // "len"\r\n"{             }"\r\n"len"\r\n"{             }";
    // "len"\r\n"{             }"\r\n"len"\r\n"{             }"\r\n
    std::string Decode(std::string &inbuffer)
    {
        auto pos = inbuffer.find(SEP);
        if (pos == std::string::npos)
            return std::string();
        std::string len_str = inbuffer.substr(0, pos);
        if (len_str.empty())
            return std::string();
        int packlen = std::stoi(len_str);

        int total = packlen + len_str.size() + 2 * SEP.size();
        if (inbuffer.size() < total)
            return std::string();

        std::string package = inbuffer.substr(pos + SEP.size(), packlen);
        inbuffer.erase(0, total);
        return package;
    }
    // 我们的协议的样子:
    // 报文 = 报头+有效载荷
    // "有效载荷的长度"\r\n"有效载荷"\r\n
    // "len"\r\n"_x _op _y"\r\n  -> len: 有效载荷的长度,约定\r\n是分隔符,不参与统计

    // 就是结构化数据 能像网络中直接发送吗?不能,也不建议
    // 其他问题?class Reqeust req(10, 20, "*"); req->字节? 直接发送req对象,class Reqeust req
    class Request
    {
    public:
        Request()
        {
        }
        Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
        {
        }
        bool Serialize(std::string *out)
        {
            // 转换成为字符串
            Json::Value root;
            root["x"] = _x;
            root["y"] = _y;
            root["oper"] = _oper;

            Json::FastWriter writer;
            // Json::StyledWriter writer;
            *out = writer.write(root);
            return true;
        }
        bool Deserialize(const std::string &in) // 你怎么知道你读到的in 就是完整的一个请求呢?
        {
            Json::Value root;
            Json::Reader reader;
            bool res = reader.parse(in, root);
            if (!res)
                return false;

            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _oper = root["oper"].asInt();
            return true;
        }

    public:
        int _x;
        int _y;
        char _oper; // "+-*/%" _x _oper _y
    }; // --- "字符串"

    class Response
    {
    public:
        Response()
        {
        }
        Response(int result, int code) : _result(result), _code(code)
        {
        }
        bool Serialize(std::string *out)
        {
            // 转换成为字符串
            Json::Value root;
            root["result"] = _result;
            root["code"] = _code;

            Json::FastWriter writer;
            // Json::StyledWriter writer;
            *out = writer.write(root);
            return true;
        }
        bool Deserialize(const std::string &in)
        {
            Json::Value root;
            Json::Reader reader;
            bool res = reader.parse(in, root);
            if (!res)
                return false;

            _result = root["result"].asInt();
            _code = root["code"].asInt();
            return true;
        }

    public:
        int _result; // 结果
        int _code;   // 0:success 1: 除0 2: 非法操作 3. 4. 5
    }; // --- "字符串"

    class Factory
    {
    public:
        Factory()
        {
            srand(time(nullptr) ^ getpid());
            opers = "+/*/%^&|";
        }
        std::shared_ptr<Request> BuildRequest()
        {
            int x = rand() % 10 + 1;
            usleep(x * 10);
            int y = rand() % 5; // [01,2,3,4]
            usleep(y * x * 5);
            char oper = opers[rand() % opers.size()];
            std::shared_ptr<Request> req = std::make_shared<Request>(x, y, oper);
            return req;
        }
        std::shared_ptr<Response> BuildResponse()
        {
            return std::make_shared<Response>();
        }
        ~Factory()
        {
        }

    private:
        std::string opers;
    };
}

 最后我们实现一个仿函数来模拟计算器的计算即可:

#pragma once
#include <iostream>
#include "Protocol.hpp"

using namespace protocol_ns;

class Calculate
{
public:
    Calculate()
    {
    }
    Response Excute(const Request &req)
    {
        Response resp(0, 0);

        switch (req._oper)
        {
        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 (req._y == 0)
            {
                resp._code = 1;
            }
            else
            {
                resp._result = req._x / req._y;
            }
        }
        break;
        case '%':
        {
            if (req._y == 0)
            {
                resp._code = 2;
            }
            else
            {
                resp._result = req._x % req._y;
            }
        }
        break;
        default:
            resp._code = 3;
            break;
        }
        return resp;
    }
    ~Calculate()
    {
    }

private:
};

servermain.cc

#include <iostream>
#include <functional>
#include <memory>
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "CalCulate.hpp"

using namespace protocol_ns;

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " local_port\n"
              << std::endl;
}

using callback_t = std::function<Response(const Request &req)>;

class Service
{
public:
    Service(callback_t cb) : _cb(cb)
    {
    }
    void ServiceHelper(socket_sptr sockptr, InetAddr client)
    {
        int sockfd = sockptr->SockFd();
        LOG(DEBUG, "get a new link, info %s:%d, fd : %d\n", client.Ip().c_str(), client.Port(), sockfd);
        std::string clientaddr = "[" + client.Ip() + ":" + std::to_string(client.Port()) + "] ";
        std::string inbuffer;

        while (true)
        {
            sleep(5);

            Request req;
            // 你怎么保证一个大的字符串里面有没有完整的请求
            // 1.
            int n = sockptr->Recv(&inbuffer); // 你怎么保证你读到的是一个完整的request. 有效载荷的长度
            if (n < 0)
            {
                LOG(DEBUG, "client %s quit\n", clientaddr.c_str());
                break;
            }
           
            // 2. 分析数据,确认完整报文
            std::string package;
            while (true)
            {
                sleep(1);
                std::cout << "inbuffer: " << inbuffer << std::endl;
                package = Decode(inbuffer);
                if (package.empty())
                    break;
                std::cout << "------------------------begin---------------" << std::endl;
                std::cout << "resq string:\n"
                          << package << std::endl;
                // 我敢保证,你一定读到了一个完整的json串.
                // 3.反序列化
                req.Deserialize(package);
                // 4. 业务处理
                Response resp = _cb(req);

                // 5. 对应答做序列化
                std::string send_str;
                resp.Serialize(&send_str);

                std::cout << "resp Serialize:" << std::endl;
                std::cout << send_str << std::endl;
                // 6. 添加长度报头
                send_str = Encode(send_str);
                std::cout << "resp Encode:" << std::endl;
                std::cout << send_str << std::endl;

                // "len"\r\n"{             }"\r\n"
                sockptr->Send(send_str); // 本次不对发送做处理, EPOLL
            }
        }
    }

private:
    callback_t _cb;
};

// ./tcpserver port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    uint16_t port = std::stoi(argv[1]);

    daemon(0, 0);
    
    EnableFile();
    Calculate cal;
    Service calservice(std::bind(&Calculate::Excute, &cal, std::placeholders::_1));
    io_service_t service = std::bind(&Service::ServiceHelper, &calservice, std::placeholders::_1, std::placeholders::_2);
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, service);

    tsvr->Loop();
    return 0;
}

 我们只需要将代码中的函数进行组合绑定即可。而在main函数中我们使用daemon(0,0)只是将其变成守护进程,后面我会给大家讲解。

而在main函数中,我们就可以感受到ISO定义网络的七层的最后三层会话层、表示层、应用层,深刻理解下面这个图的含义:

 Calculate cal;创建了一个计算器的方法争对特殊的数据(协议),所以代表应用层。

std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, service);
使用了智能指针来创建一个TCP服务,只管理TCP通信这不就是会话层吗!!!

Service calservice(std::bind(&Calculate::Excute, &cal, std::placeholders::_1));创建Service对象就是创建数据,给数据创建标准和序列化反序列化这不就是表示层吗!!!

所以将代码进行大规模解耦就是为了看清这三层的作用,而这三层在TCP/IP协议中被归纳为1层,可以说明这三层全部都是由我们的程序员在操控。OS不会知道我们是怎么想的,所以需要我们自己去完成!


关于完整的代码,博主的代码库中有,自行选取!!!感谢大家观看。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2057764.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

勇闯机器学习(第五关--中文文本特征提取)

以下内容皆为原创&#xff0c;制作实属不易&#xff0c;请点点关注和赞赞❥(^_^) 第一关&#xff1a;机器学习概念和流程http://t.csdnimg.cn/IuHh4第二关&#xff1a;数据集的使用http://t.csdnimg.cn/2jsdi第三关&#xff1a;特征工程-字典特征提取http://t.csdnimg.cn/ZpMt…

[数据集][目标检测]铁轨缺陷检测数据集VOC+YOLO格式4020张4类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;4020 标注数量(xml文件个数)&#xff1a;4020 标注数量(txt文件个数)&#xff1a;4020 标注…

高性能web服务器2——Nginx概述

Nginx 概述 Nginx 是一款广泛使用的高性能 Web 和反向代理服务器&#xff0c;以其出色的并发处理能力和低内存消耗而闻名。自 2004 年首次发布以来&#xff0c;Nginx 已经成为许多企业和组织的首选 Web 服务器和负载均衡解决方案。本文将对 Nginx 进行一个全面的概述&#xff…

探索Qotom Q51251OPS迷你电脑:功能与广泛应用

Qotom Q51251 OPS&#xff08;开放可插拔规范&#xff09;迷你电脑是一款设计紧凑且功能强大的设备&#xff0c;旨在满足不同领域的多样化需求。基于英特尔Core i5-12450H Alder Lake H处理器&#xff0c;这款设备不仅具备出色的计算性能&#xff0c;还提供了丰富的连接选项&am…

电路板中的MARK点

什么是mark点&#xff0c;什么情况下有mark点 Mark点的种类 局部mark点&#xff1a;针对那些 引脚数量众多 引脚间距非常紧凑的元器件 比如说QFT封装 BGA封装 MARK点的作用 不论是 拼版还是全局mark 一般都会放上2-3个点 第三个mark点一般用于比较大的电路板或者是比较大的拼…

接口加密解决方案,Python的各种加密实现!

01、前言 在现代软件开发中&#xff0c;接口测试已经成为了不可或缺的一部分。随着互联网的普及&#xff0c;越来越多的应用程序都采用了接口作为数据传输的方式。接口测试的目的是确保接口的正确性、稳定性和安全性&#xff0c;从而保障系统的正常运行。 在接口测试中&…

TXT 记录解析怎么做?

在当今数字化的时代&#xff0c;网络技术的应用越来越广泛&#xff0c;而域名系统&#xff08;DNS&#xff09;则是网络通信中至关重要的一部分。TXT 记录作为 DNS 中的一种记录类型&#xff0c;有着特定的用途和解析方法。 那么&#xff0c;TXT 记录解析究竟该怎么做呢&#…

学习node.js 七 http 模块

目录 http模块 创建http服务器 反向代理 代码实现 邮件服务 案例实现&#xff1a; 动静分离 代码实现 http模块 “http” 模块是 Node.js 中用于创建和处理 HTTP 服务器和客户端的核心模块。它使得构建基于 HTTP 协议的应用程序变得更加简单和灵活。 创建 Web 服务…

高性能企业WEB服务器

一&#xff1a;nginx的编译安装&#xff1a; 1.1官方源下载地址&#xff1a; https://nginx.org/en/download.html 1.1.1下载编译所需要的软件&#xff1a; [rootnginx nginx-1.24.0]# dnf install gcc pcre-devel zlib-devel openssl-devel -y 编译&#xff1a; 验证版本及编…

DHU 二维数组 阵列

输出范例给的不工整 思路及代码 写的不优雅 分为三部分 枚举输出 n 1-11 规律求出 n > 12 的矩阵 输出需要的矩阵&#xff0c;把每一行存成一个字符串&#xff0c;方便左右对齐 #include<iostream> #include<cmath> #include<iomanip> #include&l…

C++ 设计模式(4. 建造者模式)

建造者模式&#xff08;也被成为生成器模式&#xff09;&#xff0c;是一种创建型设计模式&#xff0c;软件开发过程中有的时候需要创建很复杂的对象&#xff0c;而建造者模式的主要思想是将对象的构建过程分为多个步骤&#xff0c;并为每个步骤定义一个抽象的接口。具体的构建…

计算机毕业设计 公寓出租系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

消防认证-火灾显示盘GB 17429-2011

一、消防认证 消防认证是指消防产品符合国家相关技术要求和标准&#xff0c;且通过了国家认证认可监督管理委员会审批&#xff0c;获得消防认证资质的认证机构颁发的证书&#xff0c;消防产品具有完好的防火功能&#xff0c;是住房和城乡建设领域验收的重要指标。 二、认证依据…

软考高项—项目采购管理总结

采购管理包括规划采购管理、实施采购和控制采购3个过程。 管理领域管理过程定义作用频率项目采购管理规划采购管理记录项目采购决策、明确采购方法&#xff0c;及识别潜在卖方的过程。确定是否从项目外部获取货物和服务&#xff0c;如果是&#xff0c;则还要确定将在什么时间、…

运维学习————nginx2-配置详解及负载均衡

目录 一、配置文件详解 1.1、结构 1.2、重要配置解释 1.3、详细配置 全局配置 Events HTTP 服务器配置 server虚拟主机配置 location URL匹配配置 1.4、完整配置 二、负载均衡 2.1、概念 2.2、集群规划及实现 2.3、具体实现 2.3.1、克隆 2.3.2、修改tomcat1配…

【python】Python中的日志模块logging使用技巧与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

软件设计师全套备考系列文章5 -- 数据结构的基本概念与算法

软考-- 软件设计师&#xff08;5&#xff09;-- 数据结构的基本概念与算法 文章目录 软考-- 软件设计师&#xff08;5&#xff09;-- 数据结构的基本概念与算法前言一、基本概念二、算法 前言 考试时间&#xff1a;每年5月、11月&#xff0c;软件设计师每年都会开考。 考试条件…

【Linux修行路】文件系统之缓冲区

目录 ⛳️推荐 一、先看现象 二、用户缓冲区的引入 三、用户缓冲区的刷新策略 四、为什么要有用户缓冲区 五、现象解释 ⛳️推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站 一、…

BUG——imx6u开发_结构体导致的死机问题(未解决)

简介&#xff1a; 最近在做imx6u的linux下裸机驱动开发&#xff0c;由于是学习的初级阶段&#xff0c;既没有现成的IDE可以使用&#xff0c;也没有GDB等在线调试工具&#xff0c;只能把代码烧写在SD卡上再反复插拔&#xff0c;仅靠卑微的亮灯来判断程序死在哪一步。 至于没有使…