Linux网络 - json,网络计算服务器与客户端改进

news2024/11/29 0:30:10

文章目录

  • 前言
  • 一、json
    • 1.引入库
    • 2. 使用步骤
    • 2.Calculator.hpp
    • 3.Task.hpp
    • 4.serverCal.hpp
  • 新客户端


前言

本章内容主要对上一章的网络计算器客户端和服务器进行一些Bug修正与功能改进。 并学习如何使用json库和daemon函数。


一、json

在我们自己的电脑上一些软件的文件夹中,我们经常能看到.json后缀的文件,那么这种文件是用来干什么的呢?

这就要说到我们上节课所讲的序列化和反序列化了,相信如果大家自己如果尝试写了一遍之后,会发现序列化和反序列化还是比较难写的。

而市面上,是存在这么一个库被广泛引用来做序列化和反序列化,他就是json库。

json库是一个第三方库,所以我们的Linux服务器一般是不自带的,需要下载安装。

sudo yum install -y jsoncpp-devel

安装后,它的头文件位于

ls /usr/include/jsoncpp/json

在这里插入图片描述

因为是第三方库,所以它也需要链接动态库。

ls /lib64/libjsoncpp.so

在这里插入图片描述

1.引入库

代码如下(示例):

#include <jsoncpp/json/json.h>

g++编译

Lib=-ljsoncpp
serverCal:main.cc
	g++ -o $@ $^ -std=c++11 -lpthread $(Lib)

clientCal:clientCal.cc
	g++ -o $@ $^ -std=c++11 $(Lib)

2. 使用步骤

#pragma once
#include <iostream>
#include <string>
#include "log.hpp"
#include <jsoncpp/json/json.h>

extern Log lg;
const char blank_space_sep = ' ';
const char protocol_sep = '\n';

enum Code
{
    Div_Zero_Err = 1,
    Mod_Zeor_Err,
    Operatorr_Err,
    Float_Mod_Err
};

enum Type
{
    Type_Int = 1,
    Type_Double = 2
};

// 多态版本

bool CheckType(std::string &in_str, int* type)
{
    Json::Value root;
    Json::Reader reader;
    bool suc = reader.parse(in_str, root);
    if (!suc)
    {
        lg(Warning, "Deserialize Failed...");
        return false;
    }
    *type = root["type"].asInt();
    return true;
}

class Request
{
public:
    Request() {}
    Request(char op)
        : _op(op) {}

    virtual bool serialize() {}

    virtual bool deserialize() {}

public:
    char _op;
};

class IntRequest : public Request
{
public:
    IntRequest() {}
    IntRequest(int x, int y, char op)
        : _x(x), _y(y), Request(op) {}

    virtual bool serialize(std::string *out_str)
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["op"] = _op;
        root["type"] = 1;
        Json::FastWriter writer;
        *out_str = writer.write(root);
        return true;
    }

    virtual bool deserialize(std::string &in_str)
    {
        Json::Value root;
        Json::Reader reader;
        bool suc = reader.parse(in_str, root);
        if (!suc)
        {
            lg(Warning, "Request Deserialize Failed...");
            return false;
        }
        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _op = root["op"].asInt();
        Json::FastWriter writer;
        in_str.erase(0, writer.write(root).size());
        std::cout << "已经删除已解析报头..." << std::endl;
        return true;
    }

public:
    int _x;
    int _y;
};

class DoubleRequest : public Request
{
public:
    DoubleRequest() {}
    DoubleRequest(double x, double y, char op)
        : _x(x), _y(y), Request(op) {}

    virtual bool serialize(std::string *out_str)
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["op"] = _op;
        root["type"] = 2;
        Json::FastWriter writer;
        *out_str = writer.write(root);
        return true;
    }

    virtual bool deserialize(std::string &in_str)
    {
        Json::Value root;
        Json::Reader reader;
        bool suc = reader.parse(in_str, root);
        if (!suc)
        {
            lg(Warning, "Request Deserialize Failed...");
            return false;
        }
        _x = root["x"].asDouble();
        _y = root["y"].asDouble();
        _op = root["op"].asInt();
        Json::FastWriter writer;
        in_str.erase(0, writer.write(root).size());
        return true;
    }

public:
    double _x;
    double _y;
};

class Respond
{
public:
    Respond() {}
    Respond(int code)
        : _code(code) {}

    virtual bool serialize() {}

    virtual bool deserialize() {}

public:
    int _code = -1;
};

class IntRespond : public Respond
{
public:
    IntRespond() {}
    IntRespond(int result, int code)
        : _result(result), Respond(code) {}

    virtual bool serialize(std::string *out_str)
    {

        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;
        root["type"] = 1;
        Json::FastWriter writer;
        *out_str = writer.write(root);
        return true;
    }

    virtual bool deserialize(const std::string &in_str)
    {
        Json::Value root;
        Json::Reader reader;
        bool suc = reader.parse(in_str, root);
        if (!suc)
        {
            lg(Warning, "Respond Deserialize Failed...");
            return false;
        }
        _result = root["result"].asInt();
        _code = root["code"].asInt();
        return true;
    }

public:
    int _result;
};

class DoubleRespond : public Respond
{
public:
    DoubleRespond() {}
    DoubleRespond(double result, int code)
        : _result(result), Respond(code) {}

    virtual bool serialize(std::string *out_str)
    {

        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;
        root["type"] = 2;
        Json::FastWriter writer;
        *out_str = writer.write(root);
        return true;
    }

    virtual bool deserialize(const std::string &in_str)
    {
        Json::Value root;
        Json::Reader reader;
        bool suc = reader.parse(in_str, root);
        if (!suc)
        {
            lg(Warning, "Respond Deserialize Failed...");
            return false;
        }
        _result = root["result"].asDouble();
        _code = root["code"].asInt();
        return true;
    }

public:
    double _result;
};


2.Calculator.hpp

#pragma once
#include "protocol.hpp"

class Calculator
{
public:
    Calculator() {}
    IntRespond calculate(const IntRequest &rq)
    {
        IntRespond rs;
        switch (rq._op)
        {
        case '+':
            rs._result = rq._x + rq._y;
            break;
        case '-':
            rs._result = rq._x - rq._y;
            break;
        case '*':
            rs._result = rq._x * rq._y;
            break;
        case '/':
            if (rq._y == 0)
            {
                lg(Warning, "Found Div Zero Error...");
                rs._code = Div_Zero_Err;
                return rs;
            }
            rs._result = rq._x / rq._y;
            break;
        case '%':
            if (rq._y == 0)
            {
                lg(Warning, "Found Mod Zero Error...");
                rs._code = Mod_Zeor_Err;
                return rs;
            }
            rs._result = rq._x % rq._y;
            break;

        default:
            lg(Warning, "Found Operator Error...");
            rs._code = Operatorr_Err;
            return rs;
        }
        rs._code = 0;
        return rs;
    }

    DoubleRespond calculate(const DoubleRequest &rq)
    {
        DoubleRespond rs;
        switch (rq._op)
        {
        case '+':
            rs._result = rq._x + rq._y;
            break;
        case '-':
            rs._result = rq._x - rq._y;
            break;
        case '*':
            rs._result = rq._x * rq._y;
            break;
        case '/':
            if (rq._y == 0)
            {
                lg(Warning, "Found Div Zero Error...");
                rs._code = Div_Zero_Err;
                return rs;
            }
            rs._result = rq._x / rq._y;
            break;
        case '%':
            lg(Warning, "Float Mod Error...");
            rs._code = Float_Mod_Err;
            return rs;

        default:
            lg(Warning, "Found Operator Error...");
            rs._code = Operatorr_Err;
            return rs;
        }
        rs._code = 0;
        return rs;
    }
};

3.Task.hpp

#pragma once
#include "Socket.hpp"
#include "protocol.hpp"
#include "Calculator.hpp"
class Task
{
public:
    Task(int socket_fd)
        : _socket_fd(socket_fd)
    {
    }
    
    void IntHandle(std::string &message)
    {
        IntRequest rq;
        Calculator cal;
        // 因为可能message里面已经存在了多个报文,所以就需要一次性多次处理
        if (!rq.deserialize(message))
        {
            // 反序列化失败说明里面的数据可能出现数据丢失等情况,出现这种情况说明我们的报文数据不再可信,最直接的办法就是丢弃全部报文!
            message = "";
            return;
        }
        IntRespond rs = cal.calculate(rq);
        std::string res;
        rs.serialize(&res);
        printf("%d %c %d = %d\n", rq._x, rq._op, rq._y, rs._result);
        write(_socket_fd, res.c_str(), res.size());
    }

    void DoubleHandle(std::string &message)
    {
        DoubleRequest rq;
        Calculator cal;
        // 因为可能message里面已经存在了多个报文,所以就需要一次性多次处理
        if (!rq.deserialize(message))
        {
            // 反序列化失败说明里面的数据可能出现数据丢失等情况,出现这种情况说明我们的报文数据不再可信,最直接的办法就是丢弃全部报文!
            message = "";
            return;
        }
        DoubleRespond rs = cal.calculate(rq);
        std::string res;
        rs.serialize(&res);
        printf("%lf %c %lf = %lf\n", rq._x, rq._op, rq._y, rs._result);
        write(_socket_fd, res.c_str(), res.size());
    }

    void run()
    {
        char in_buffer[1024];
        std::string message = "";
        while (true)
        {
            memset(in_buffer, 0, sizeof in_buffer);
            int n = read(_socket_fd, (void *)in_buffer, sizeof in_buffer - 1);
            if (n == 0)
            {
                lg(Warning, "Connection closed by foreign host, socketfd[%d] closed...", _socket_fd);
                break;
            }
            else if (n < 0)
            {
                lg(Warning, "Read Error, socketfd[%d]...", _socket_fd);
                break;
            }
            in_buffer[n] = 0;
            message = in_buffer;
            std::cout << "报文大小: " << message.size() << " ,报文内容: " << message << std::endl;

            // 判断发来的数据类型

            while (!message.empty())
            {
                int type;
                if (!CheckType(message, &type))
                {
                    //报文内容出现问题
                    message = "";
                    break;
                }
                if (type == 1)
                {
                    IntHandle(message);
                }
                else if (type == 2)
                {
                    DoubleHandle(message);
                }
                else{
                    lg(Warning, "Type Error, type: %d ...", type);
                }
            }
        }
    }

    void operator()()
    {
        run();
        close(_socket_fd);
    }

    ~Task()
    {
    }

private:
    int _socket_fd;
};

4.serverCal.hpp

#pragma once

#include "Socket.hpp"
#include "protocol.hpp"
#include "threadPool.hpp"
#include "Task.hpp"
class ServerCal
{
public:
    ServerCal()
    {
    }

    void Init(const int sinfamily, const std::string &ip, const uint16_t port)
    {
        _listensock.Init();
        _listensock.Bind(sinfamily, ip, port);
        _listensock.Listen();
    }

    void Run()
    {
        daemon(0, 0);  //仅此这里添加了一个守护线程功能
        ThreadPool<Task> *tp = ThreadPool<Task>::GetInstance();
        tp->Start();
        struct sockaddr_in client;

        while (true)
        {
            memset(&client, 0, sizeof client);
            socklen_t len;
            int socketfd = _listensock.Accept(&client, &len);
            if (socketfd < 0)
                continue;
            tp->Push(socketfd);

        }
    }

private:
    Socket _listensock;
};

这里我添加了守护线程的功能,使用的是系统库自带的函数。
在这里插入图片描述
nochdir如果被设为0,则更改工作路径为“/”根目录,否则则什么也不敢。
noclose如果被设为0,则将标准输入输出错误重定向到/dev/null文件中,/dev/null文件我们上章是讲过的。


其他的文件我没有变动,需要的可以在我上一个文章复制粘贴或者到我的gitee自行拷贝。

新客户端

#include "Socket.hpp"
#include "protocol.hpp"

#define VALID_OP 5
const char valid_operator[VALID_OP] = {'+', '-', '*', '/', '%'};

static int localfd = -1;

void Usage(const char *mes)
{
    std::cout << "Usage: " << mes << " ip[xxx.xxx.xxx.xxx] port[8080-9000]" << std::endl;
}

bool __CheckNumber(const std::string &str)
{
    for (const char c : str)
    {
        if ((!isdigit(c)) && (c != '.'))
        {
            return false;
        }
    }
    return true;
}

bool __CheckOp(const std::string &op)
{
    if (op.size() != 1)
    {
        return false;
    }

    for (int i = 0; i < VALID_OP; i++)
    {
        if (op.find(valid_operator[i]) != std::string::npos)
            break;
        if (i == 4)
        {
            return false;
        }
    }
    return true;
}

bool CheckSafe(const std::string &x, const std::string &op, const std::string &y, int *type)
{

    if (!__CheckOp(op))
    {
        std::cout << "Helper: 使用了除 + - * / % 以外的运算符" << std::endl;
        return false;
    }

    if (!__CheckNumber(x) || !__CheckNumber(y))
    {
        std::cout << "Helper: 请输入正确的数字" << std::endl;
        return false;
    }

    if ((x.find('.') != std::string::npos) || (y.find('.') != std::string::npos))
    {
        // 说明这是浮点数运算
        *type = 2;
        return true;
    }
    *type = 1;
    return true;
}

void IntHandle(const std::string &x, const std::string &op, const std::string &y, std::string &message)
{
    IntRequest rq;
    IntRespond rs;

    rq._x = std::stoi(x);
    rq._y = std::stoi(y);
    rq._op = op[0];

    rq.serialize(&message);
    write(localfd, message.c_str(), message.size());

    // 开始等待结果
    char buffer[1024];
    int n = read(localfd, buffer, sizeof buffer - 1);
    if (n == 0)
    {
        lg(Warning, "Connection closed by foreign host, socketfd[%d] closed...", localfd);
        exit(1);
    }
    else if (n < 0)
    {
        lg(Warning, "Read Error, socketfd[%d]...", localfd);
        exit(2);
    }
    buffer[n] = 0;
    std::string res = buffer;
    std::cout << res << std::endl;
    rs.deserialize(res);
    if (rs._code != 0)
    {
        switch (rs._code)
        {
        case 1:
            std::cout << "出现除0错误" << std::endl;
            break;
        case 2:
            std::cout << "出现模0错误" << std::endl;
            break;
        case 3:
            std::cout << "使用了除 + - * / % 以外的运算符" << std::endl;
            break;
        default:
            std::cout << "发生未知错误" << std::endl;
            break;
        }
        return;
    }
    printf("%d %c %d = %d\n", rq._x, rq._op, rq._y, rs._result);
}

void DoubleHandle(const std::string &x, const std::string &op, const std::string &y, std::string &message)
{
    DoubleRequest rq;
    DoubleRespond rs;

    rq._x = std::stod(x);
    rq._y = std::stod(y);
    rq._op = op[0];

    rq.serialize(&message);
    write(localfd, message.c_str(), message.size());

    // 开始等待结果
    char buffer[1024];
    int n = read(localfd, buffer, sizeof buffer - 1);
    if (n == 0)
    {
        lg(Warning, "Connection closed by foreign host, socketfd[%d] closed...", localfd);
        exit(1);
    }
    else if (n < 0)
    {
        lg(Warning, "Read Error, socketfd[%d]...", localfd);
        exit(2);
    }
    buffer[n] = 0;
    std::string res = buffer;
    std::cout << res << std::endl;
    rs.deserialize(res);
    if (rs._code != 0)
    {
        switch (rs._code)
        {
        case 1:
            std::cout << "出现除0错误" << std::endl;
            break;
        case 2:
            std::cout << "出现模0错误" << std::endl;
            break;
        case 3:
            std::cout << "使用了除 + - * / % 以外的运算符" << std::endl;
            break;
        default:
            std::cout << "发生未知错误" << std::endl;
            break;
        }
        return;
    }
    printf("%lf %c %lf = %lf\n", rq._x, rq._op, rq._y, rs._result);
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage("./clientCal");
    }
    Socket local;
    local.Init();
    int n = local.Connect(argv[1], argv[2]);
    if (n < 0)
    {
        return 1;
    }

    localfd = local.Getfd();

    std::cout << "            简易计算器, 目前仅支持\" + - * / %\"运算符 " << std::endl;
    std::cout << "            数字和运算符请用空格或回车隔开" << std::endl;
    std::string x, op, y;
    int type;
    std::string message;
    while (true)
    {
        std::cout << "请输入您的算式@ ";
        std::cin >> x >> op >> y;
        if (!CheckSafe(x, op, y, &type))
        {
            continue;
        }
        std::cout << type << std::endl;
        if (type == 1)
        {
            IntHandle(x,op,y,message);
        }
        else if(type ==2)
        {
            DoubleHandle(x,op,y,message);
        }
        else{
            lg(Warning, "Type Error, type: %d ...", type);
            exit(3);
        }
    }
    return 0;
}

添加了许多输入的安全检查,不检查引发的问题太多了!

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

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

相关文章

Unity编辑器扩展,快捷键的使用

代码部分 编辑器界面 使用方法&#xff1a; 使用方法和如图1一样&#xff0c;只需要在Menuitem的路径后面加上标识符号就行。 "#"对应的是shift "&"对应的是Alt "%"对应的是ctrl 比如我图中的是&#xff0c;%#s对应的是CtrlShifts&…

聚醚醚酮(Polyether Ether Ketone)PEEK在粘接使用时使用UV胶水的优势有哪些?要注意哪些事项?

聚醚醚酮&#xff08;Polyether Ether Ketone&#xff09;PEEK在粘接使用时使用UV胶水的优势有哪些&#xff1f;要注意哪些事项&#xff1f; 使用UV胶水在聚醚醚酮&#xff08;Polyether Ether Ketone&#xff0c;PEEK&#xff09;上进行粘接可能具有一些优势&#xff0c;但同时…

11-数组与指针深入理解——题型理解

11-数组与指针深入理解——题型理解 文章目录 11-数组与指针深入理解——题型理解一、理解题1二、理解题二三、理解题三四、理解题四五、理解题五六、理解题六 一、理解题1 #include <stdio.h>int main(void) {int (*p)[5] NULL; // 定义一个指向 拥有5个整型数据的数组…

Linux---sudo命令

文章目录 目录 文章目录 一.sudo命令简介 二.sudo 命令的特点 三.sudo 相关文件 四.sudo 命令授权配置 一.sudo命令简介 sudo 命令全称“SuperUser Do”&#xff0c;是Linux系统中的一个命令能够使普通用户以超级用户身份去执行某些命令。 二.sudo 命令的特点 sudo能够授权…

翻译《The Old New Thing》- How do I obtain the computer manufacturer’s name?

How do I obtain the computer manufacturers name? - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20081218-00/?p19783 Raymond Chen 2008年08月08日 如何获取计算机制造商的名字&#xff1f; 一位客户想要一种方法来确定计算机制造商的…

vuInhub靶场实战系列--prime:2

免责声明 本文档仅供学习和研究使用,请勿使用文中的技术源码用于非法用途,任何人造成的任何负面影响,与本人无关。 目录 免责声明前言一、环境配置1.1 靶场信息1.2 靶场配置 二、信息收集2.1 主机发现2.1.1 netdiscover2.1.2 nmap主机扫描2.1.3 arp-scan主机扫描 2.2 端口扫描…

Flutter中同步与异步

一&#xff0c;同步/异步的理解 1&#xff0c;await&#xff1a;同步机制 同步操作会阻止其他操作执行&#xff0c;直到完成为止。同步就好比打电话一样&#xff0c;打电话时都是一个人在说另一个人听&#xff0c;一个人在说的时候另一个人等待&#xff0c;等另一个人说完后再…

OCP-042之:Oracle结构体系

1. Oracle结构体系 1.1 概述 1.1.1 版本 版本后缀所代表的含义 i:代表基于Internet架构的数据库,如9i g:代表基于grid(网格)的数据库,如11g grid的目的:降低成本,提高服务质量,简化管理 Storage Grid:ASM(automatic storage management),继承了LVM技术,Oracl…

Git【版本控制和Git的安装介绍】

01 【版本控制和Git的安装介绍】 工程设计领域中&#xff0c;使用“版本控制”管理工程蓝图的设计过程。在 IT 开发中也可以使用版本控制思想管理代码的版本迭代。 1.目的 协同修改&#xff1a;支持在服务器对同一个文件多人协同地修改&#xff1b; 数据备份&#xff1a;同时…

java中的异常-异常处理(try、catch、finally、throw、throws)+自定义异常

一、概述 1、java程序员在编写程序时提前编写好对异常的处理程序&#xff0c;在程序发生异常时就可以执行预先设定好的处理程序&#xff0c;处理程序执行完之后&#xff0c;可以继续向后执行后面的程序 2、异常处理程序是在程序执行出现异常时才执行的 二、5个关键字 1、tr…

11 gpio 与 pinctrl 子系统

1、GPIO 硬件结构 GPIO 是通用输入/输出端口的简称。GPIO 的引脚与外部硬件设备连接,可实现与外部通讯、控制外部硬件或者采集外部硬件数据的功能。 八种工作模式 GPIO_Mode_AIN 模拟输入 GPIO_Mode_IN_FLOATING 浮空输入 GPIO_Mode_IPD 下拉输入 GPIO_Mode_IPU 上拉输入GP…

Hadoop笔记

1.hadoop环境搭建&#xff0c;linux命令&#xff08;vi);2.分布式的基本概念&#xff0c;cap理论&#xff08;遵循此原则开发分布式数据库&#xff09;&#xff0c;hdfs,mapreduce&#xff1b;3.3.1&#xff1b;3.2重点&#xff1b;4.map&#xff0c;reduce过程&#xff0c;优缺…

DBeaver无法连接Clickhouse,连接失败

DBeaver默认下载的是0.2.6版本的驱动&#xff0c;但是一直连接失败&#xff1a; 报错提示 解决办法 点击上图中的Open Driver Configuration点击库 - 重置为默认状态在弹出的窗口中修改驱动版本号为0.2.4或者其他版本&#xff08;我没有试用过其他版本&#xff09;&#xff0…

c++【入门】求圆环的面积

限制 时间限制 : 1 秒 内存限制 : 128 MB 题目 如下图所示的圆环铁片&#xff0c;中间是空心的&#xff0c;已知圆环外圆的半径是r1厘米&#xff08;如&#xff1a;10cm&#xff09;&#xff0c;内圆半径是r2厘米&#xff08;如&#xff1a;6cm&#xff09;&#xff0c;请编…

stm32最小系统焊接调试总结

stm32最小系统打板后,接下来开始焊接元器件,焊接元器件可以参考立创EDA焊接辅助工具。 图1 焊接辅助助手 焊接准备工具有,焊台,放大镜,元器件,镊子,焊锡膏,锡丝及万用表等。调节焊台温度到350-400摄氏度。焊接顺序是先焊接USB typec接口,5V电源,ldo,ch340,stm32芯片…

标准发布实施 | 《村镇污水处理一体化集成装备技术规范》

根据《中华人民共和国标准化法》以及国家标准化管理委员会、民政部联合制定的《团体标准管理规定》&#xff0c;依据全国团体标准信息平台和《中华环保联合会团体标准管理办法&#xff08;试行&#xff09;》&#xff0c;全国团体标准《村镇污水处理一体化集成装备技术指南》&a…

32.768k晶振FC-135R在智能手表手环中的作用

随着智能设备的普及&#xff0c;智能手表和手环已经成为人们日常生活中不可或缺的科技产品。晶振在智能手表手环中的作用是通过传感器给智能手环连接提供信号频率&#xff0c;是很重要的核心部位&#xff0c;这些设备的核心在于其精准的时钟管理和低功耗特性&#xff0c;32.768…

Polar Web【中等】xxe

Polar Web【中等】xxe Contents Polar Web【中等】xxe思路&探索EXP运行&总结 思路&探索 如题目所示&#xff0c;此题考查XXE漏洞&#xff0c;具体细节需要逐步深挖 打开站点&#xff0c;提示了flag所在的文件&#xff0c;点击按钮&#xff0c;可见php的配置信息&am…

【CS.DB】深度解析:ClickHouse与Elasticsearch在大数据分析中的应用与优化

文章目录 《深入对比&#xff1a;在大数据分析中的 ClickHouse和Elasticsearch》 1 介绍 2 深入非关系型数据库的世界2.1 非关系型数据库的种类2.2 列存储数据库&#xff08;如ClickHouse&#xff09;2.3 搜索引擎&#xff08;如Elasticsearch&#xff09;2.4 核心优势的归纳 3…

[word] word文字间隙怎么调整? #媒体#职场发展

word文字间隙怎么调整&#xff1f; 在文档中的数据包含英文、数字、中文等&#xff0c;会有间隙&#xff0c;有时候误以为是空格&#xff0c;但是根本删除不了&#xff0c;其实这是默认的间隙&#xff0c;是可以调整的&#xff0c;下面教大家word文字间隙怎么调整的操作&#…