【Linux进阶之路】网络 —— “?“ (下)

文章目录

  • 前言
  • 一、概念铺垫
    • 1.TCP
    • 2.全双工
  • 二、网络版本计算器
    • 1. 原理简要
    • 2. 实现框架&&代码
      • 2.1 封装socket
      • 2.2 客户端与服务端
      • 2.3 封装与解包
      • 2.4 请求与响应
      • 2.5 对数据进行处理
      • 2.6 主程序逻辑
    • 3.Json的简单使用
  • 总结
  • 尾序

前言

 在上文我们学习使用套接字的相关接口进行了编程,因此对网络编程有了一定的认识,可是我们之前只是以字符串的形式简单的收发信息,如果我们要发送和接受的信息更加复杂,比如:客户端发送一个结构体,服务端要如何接收这个结构体呢? 如果说还要对结构体的数据进行处理并返回呢?下面就让我们带着这些疑问开始今天的学习吧!

  • 说明:
  1. 每台计算机的结构体的对齐方式可能会有所不同,因此不能直接发送结构体。
  2. 因此要将结构体里的数据要以特定的形式,即协议的方式发送和接收。
  3. 对数据处理后,还要以协议的方式发送给客户端,从而客户端收到并进行对应的处理。

一、概念铺垫

TCP_12">1.TCP

  • 众所周知,TCP是可靠的传输控制协议,一般是通过三次握手和四次挥手来保证数据的传输是可靠的。
  • 说明:下面只是简单的理解,后面博主详细讲解的。
  • 三次握手 :
    在这里插入图片描述
  • 三次交互,建立连接。
  • 四次挥手:

在这里插入图片描述

  • 断开连接,就是要断的干净,避免之后一方进行死缠烂打。

2.全双工

  • 所谓的全双工,就是服务端和客户端都是可以收消息和发消息的,例如UDP和TCP协议都是全双工的。
  1. UDP

在这里插入图片描述

  1. TCP
    在这里插入图片描述
  • 理解传输控制协议:
    1. 对于UDP来说,在传输层对于发消息不做控制,但是对于收消息如何处理,则全权交由UDP决定。
    2. 对于TCP来说,用户只负责将消息发送到发送和接收缓存区,但对于消息如何处理,则全权由TCP决定。
    • 说明:处理一般涉及什么时候传,传多少,传错了怎么办等等。
  • 从UDP与TCP相比较,TCP多了一个发送缓冲区,这在一定程度上可以体现TCP的可靠性。

二、网络版本计算器

1. 原理简要

  1. 因为我们做的是网络版本的计算器,数据格式设定为[ 数据(空格)方法(空格)数据(换行符)]即可,而且在网络中我们一般是以字符串的形式进行发送的,因此我们还要将整形数据转换为字符串,便于之后的解析。
  2. 数据的封装,为了能将一个完整的数据解析出来,因此我们应该在数据的前面封装数据的长度,当截取数据时,我们按照长度截取即可检查是否可获取到一个完整的数据,并且长度应与数据分开,便于获取,这里我们用换行符作为分割符即可。这里实现了数据的封装也就间接的实现了对数据解包。
  • 举一个体现自定义协议的例子,比如 [1 + 1]封装为 [5\n][1 + 1\n],数据按上面的封装,而服务器读取时,假如只读取到了[5\n 1 +],通过读取5这个字符串,转换为int,可以验证读取的报文是否是完整的报文,那么数据不是无法进行解包的,会直接返回。
  1. 因为客户端和服务端都要遵循这种规则,即自定义协议是一种约定,因此双方都要遵守的,因此不存在数据被污染的情况,即网络中传输的数据都是符合要求的。
  2. 因此客户端传输的数据可以被服务端正确的提取,提取之后,我们要进行解析和处理数据,并将处理后的数据以:【结果 返回码】,返回码用于检查数据是否计算可靠,比如1 除 0 无法进行计算,设返回码为1表示除0错误。并以上述同样的方式进行封装,将封装之后的结果,返回给用户进行解析,并处理。

2. 实现框架&&代码

  1. 实现服务器和封装socket套接字。
  2. 对请求和响应分别进行序列化和反序列化。
  3. 对序列化的数据进行封装与解包。
  4. 服务器对解析的数据进行处理和返回。
  • 代码框架:
    在这里插入图片描述

2.1 封装socket

 在之前我们实现代码时,主要目的是为了熟悉系统调用接口,熟练使用之后这里我们可以将Socket进行封装(包含客户端与服务端的常用的接口),方便我们之后进行调用:

#pragma once

#include<iostream>
#include<string>

#include<cstring>
#include<strings.h>
#include<unistd.h>

//网络相关的头文件。
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

//小组件
#include"Log.hpp"

using std::string;

enum FAIL
{
    CREAT = 1,
    SIP_TO_NIP,
    BIND,
    LISTEN,
    ACCEPT,
    CONNECT,
};
uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
class Sock
{
public:
    Sock(uint16_t port = defaultport,string ip = defaultip)
    :_port(port),_ip(ip)
    {}
    ~Sock()
    {
        if(_sockfd > 0)
        {
            close(_sockfd);
        }
    }
    //创建套接字
    void Socket()
    {
        _sockfd = socket(AF_INET,SOCK_STREAM,0);
        if(_sockfd < 0)
        {
            lg(CRIT,"socket create fail,reason is\
             %s,errno is %d",strerror(errno),errno);
            exit(CREAT);
        }
        lg(INFORE,"sockfd is %d,create success!",_sockfd);
    }
    //获取套接字
    int GetSocket()
    {
        return _sockfd; 
    }
    //绑定
    void Bind()
    {
        sockaddr_in server;
        memset(&server,0,sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_port);
        if(inet_pton(AF_INET,_ip.c_str(),&server.sin_addr) != 1)
        {
            lg(CRIT,"string_ip to inet_ip fail,reason is %s\
            ,errno is %d",strerror(errno),errno);
            exit(SIP_TO_NIP);
        }
        if(bind(_sockfd,(sockaddr*)&server,sizeof(server)) == -1)
        {
            lg(CRIT,"bind fail,reason is %s,errno \
            is %d",strerror(errno),errno);
            exit(BIND);
        }
        lg(INFORE,"bind success!");
    }
    //监听
    void Listen()
    {
        if(listen(_sockfd,_backlog) == -1)
        {
            lg(CRIT,"bind fail,reason is %s,errno is\
             %d",strerror(errno),errno);
            exit(LISTEN);
        }
        lg(INFORE,"lisen success!");
    }
    //接收连接
    int Accept(sockaddr_in* client,socklen_t* len)
    {
        int fd = accept(_sockfd,(sockaddr*)client,len);
        if(fd < 0)
        {
            lg(CRIT,"accept fail,reason is %s,\
            errno is %d",strerror(errno),errno);
            exit(ACCEPT);

        }
        uint16_t port = ntohs(client->sin_port);
        char ip[64] = {0};
        inet_ntop(AF_INET,&(client->sin_addr),ip,sizeof(ip) - 1);
        lg(INFORE,"accept success,get a new link,ip is\
         %s, port is %d",ip,port);
        return fd;
    }
    //连接
    void Connect(sockaddr_in* server)
    {
        memset(server,0,sizeof(sockaddr_in));
        server->sin_family = AF_INET;
        server->sin_port = htons(_port);
        if(inet_pton(AF_INET,_ip.c_str(),\
        &(server->sin_addr)) == -1)
        {
            lg(WARNNING,"inet_pton fail,reason is %s\
            ,errno is %d",strerror(errno),errno);
            return;
        }
        int res = connect(_sockfd,\
        (sockaddr*)server,sizeof(sockaddr_in));
        if(res == -1)
        {
            lg(CRIT,"connect fail,reason is %s,\
            errno is %d",strerror(errno),errno);
            exit(CONNECT);
            return;
        }
        lg(INFORE,"connect success!");
    }
    //从指定的套接字文件描述符里面读取数据。
    string Read(int fd)
    {
        char buffer[128] = {0};
        ssize_t n = read(fd,buffer,sizeof(buffer) - 1);
        if(n < 0)
        {
            lg(CRIT,"read fail,reason is %s,\
            errno is %d",strerror(errno),errno);
            sleep(1);
            return "";
        }
        else if(n == 0)
        {
            lg(INFORE,"read nothing!");
            sleep(1);
            return "";
        }
        buffer[n] = '\0';
        return buffer;
    }
	//向指定的套接字文件描述符里面写数据。
    int Write(int fd,const string& str)
    {
        ssize_t n = write(fd,str.c_str(),str.size());
        if(n < 0)
        {
            lg(CRIT,"write fail,reason is %s,errno \
            is %d",strerror(errno),errno);
            sleep(1);
            return n;
        }
        else if(n == 0)
        {
            lg(INFORE,"write nothing!");
            sleep(1);
            return n;
        }
        return n;  
    }
    void Close(int fd)
    {
        close(fd);
    }
private:
    int _sockfd;
    uint16_t _port;
    string _ip;
    int _backlog = 5;//?
};
  • 以后我们直接用这个小组件即可,不用再手搓系统调用的接口了。

2.2 客户端与服务端

 这里我们使用上面封装的socket接口,实现的服务端与客户端。

  • 服务端
#pragma once 
#include<iostream>
#include<pthread.h>
#include<functional>
#include"../Tools/Socket.hpp"
#include"../Tools/Log.hpp"
using cal_t = function<string(string&)>;
class TcpServer;

struct ThreadData
{
    ThreadData(int fd,TcpServer* tp)
    :_fd(fd),_tp(tp)
    {}
    int _fd;
    TcpServer* _tp;
};
class TcpServer
{
public:
    TcpServer(uint16_t port = 8080,cal_t cal = nullptr)
    :_socket(port),_cal(cal)
    {}
    ~TcpServer()
    {}
    void Init()
    {
        _socket.Socket();
        _socket.Bind();
        _socket.Listen();
    }
    static void* Rouetine(void* args)
    {
        //分离线程
        pthread_detach(pthread_self());
        auto thread_ptr = static_cast<ThreadData*>(args);
        TcpServer* tp = thread_ptr->_tp;
        int fd = thread_ptr->_fd;
        tp->Server(fd);
        return nullptr;
    }
    void Run()
    {
        for(;;)
        {
            sockaddr_in client;
            socklen_t len = sizeof(client);
            int fd = _socket.Accept(&client,&len);
            pthread_t tid;
            pthread_create(&tid,nullptr,Rouetine,\
            new ThreadData(fd,this));
        }
    }
    void Server(int fd)
    {
        string mes;
        for(;;)
        {
            sleep(10);
            //收消息
            string str = _socket.Read(fd);
            //啥也没读到
            if(str == "") break;
            mes += str;
            
            //处理消息
            string ans;
            string echo_mes;

            //一次处理一批
            while((echo_mes = _cal(mes)) != "")
            {
                
                ans += echo_mes;
            }
            //没有读取到整段的报文或者报文为空。
            int res = _socket.Write(fd,ans);
            if(res <= 0) break;
        }
        _socket.Close(fd);
    }
private:
    Sock _socket;
    cal_t _cal; //这里的cal函数是对接收的消息的处理方法。
};
  • 根据上面的信息,我们可以大致了解服务器的基本框架:
  1. 创建套接字,绑定套接字,监听套接字。
  2. 接收外面的请求,建立连接,接收信息。
  3. 调用处理信息的接口,返回处理之后的信息。

  • 因此: 我们可以让服务器与处理信息的逻辑进行解耦,并且使用封装之后的套接字是很方便的。
  • 客户端:
#pragma once
#include<iostream>
#include<string>

#include"../Tools/Log.hpp"
#include"../Tools/protocol.hpp"
#include"../Tools/Socket.hpp"

using std::string;
string default_ip = "59.110.171.164";
uint16_t default_port = 8080;
class TcpClient
{
public:
    TcpClient(string ip = default_ip,uint16_t port = default_port)
    :_sock(port,ip)
    {}
    void Init()
    {
    }
    void Run()
    {
        string res;
        for(;;)
        {
            _sock.Socket();
            sockaddr_in server;
            _sock.Connect(&server); 
            int fd = _sock.GetSocket();

            while(true)
            {
                cout << "Please Enter@";

                int x,y;
                char oper;
                cin >> x >> oper >> y;
                Request req(x,y,oper);

                string str = req.Serialize();

                //为了更好的体现自定义协议,这里我们多次进行写入。
                _sock.Write(fd,str);
                _sock.Write(fd,str);
                _sock.Write(fd,str);
                _sock.Write(fd,str);
                _sock.Write(fd,str);

                sleep(10);
                //一次读一批
                res += _sock.Read(fd);

                Response resq;
                //一次处理一批:
                while(resq.Deserialize(res));
            }
            _sock.Close(fd);
        }
    }
private:
    Sock _sock;
};

  • 说明:这里我们让客户端一次发一批消息,处理一批消息,服务端一次处理一批消息,发一批消息,这样更加能够体现自定义协议的功能。

2.3 封装与解包

//.....

char space = ' ';
char newline = '\n';
//解包
string Decode(string& str)
{
    int pos = str.find(newline);
    if(pos == string::npos) return "";
    int len = stoi(str.substr(0,pos));
    
    int totalsize = pos + len + 2;
    //如果总的报文的长度大于读取的字符串的长度,说明没有一个完整的报文。
    if(totalsize > str.size())
    {
        return "";
    }
    //将有效载荷截取出来
    string actual_load = str.substr(pos + 1,len);
    //将完整的报文丢弃,便于下一次进行读取。
    str.erase(0,totalsize);

    return actual_load; 
}
//编码
string InCode(const string& str)
{
	//一个完整的报文:有效载荷的长度 + 换行符 + 有效载荷 + 换行。
    string text = to_string(str.size()) + newline + str + newline;
    return text;
}
  1. 封装数据,我们将在报头处封装有效载荷的长度,并以换行符作为分割符。
  2. 解析数据,首先要找到有效载荷的长度,并检验是否存在一个完整的报文。

2.4 请求与响应

struct Request
{
    Request(int x, int y, char oper)
        :_x(x), _y(y), _oper(oper)
    {}
    Request()
    {}
    bool Deserialize(string& str)
    {
        cout << "+++++++++++++++++++++++++++++" << endl;
        //首先把字符串的报头和有效载荷进行分离
        string content = Decode(str);
        if(content == "") return false;
        //解析字符串:字符 + 空格 + 字符
        int left = content.find(space);
        int right = content.rfind(space);
        if (left + 1 != right - 1)
        {
            //说明是无效的字符
            return false;
        }
        _x = stoi(content.substr(0, left));
        _y = stoi(content.substr(right + 1));
        _oper = content[left + 1];
        cout << "解析的字符串:"<< _x << _oper << _y << endl; 
        cout << "待读取的字符串:" << endl << str << endl;
        cout << "-------------------------------" << endl;
        return true;
    }
    string Serialize()
    {
        string package;
        //首先对结构体进行编码
        //编码格式:字符 + 空格 + 操作符 + 空格 + 字符
        package = to_string(_x) + space + _oper + space\
         + to_string(_y);	
        //对报文再进行封装
        package = InCode(package);
        return package;
    }
    int _x = 0;
    int _y = 0;
    char _oper = '0';
    //给出一个缺省值,避免编译器告警。
};

struct Response
{
    Response(int res, int code)
        :_res(res), _code(code)
    {}
    Response()
    {}
    bool Deserialize(string& str)
    {
        string content = Decode(str);
        if (content == "") return false;
        int pos = content.find(space);
        _res = stoi(content.substr(0,pos));
        _code = stoi(content.substr(pos + 1));
        //for debug:
        cout << "+++++++++++++++++++++++++++++++" << endl;
         cout <<"转换结果:"<< _res << " " << _code << endl;
        cout << "待读取的字符串" << endl << str << endl;
        cout << "-------------------------------" << endl;
        return true;
    }
    string Serialize()
    {
        string package = to_string(_res) + space \
        + to_string(_code);
        package = InCode(package);
        return package;
    }
    int _res = 0;
    int _code = 0;
    //同理。
};
  1. Request,是客户端对服务器发送的请求,要客户端进行序列化,服务端进行反序列化,并进行解析。
  2. Response,是服务端对客户端发送的响应,要服务端进行序列化,客户端进行反序列化,并进行解析。

2.5 对数据进行处理

#include<iostream>
#include"../Tools/Log.hpp"
#include"../Tools/protocol.hpp"

enum CAL 
{
    DIV_ZERO = 1,
    MOD_ZERO,
};
struct CalHelper
{
	string Cal(string& str)
	{
	    Request req;
	    if(req.Deserialize(str) == false) return "";
	    int x = req._x;
	    int y = req._y;
	    char op = req._oper;
	    int res = 0, code = 0;
	    switch(op)
	    {
	        case '+':
	                res = x + y;
	            break;
	        case '-':
	                res = x - y;
	            break;
	        case '*': 
	                res = x * y;
	            break;
	        case '/':
	                if(!y)
	                {
	                    code = DIV_ZERO;
	                    break;
	                }
	                res = x / y;
	            break;
	        case '%':
	                if(!y)
	                {
	                    code = MOD_ZERO;
	                    break;
	                }
	                res = x % y;
	            break;
	        default:
	            break;
	    }
	    return Response(res,code).Serialize();
	}
};

  • 这是服务器对客户端请求的处理,包含请求的反序列化和对数据的处理,以及结果的序列化。

2.6 主程序逻辑

  • client.cc
#include<iostream>
#include<memory>
#include"clientcal.hpp"
using std::unique_ptr;
void Usage(char* pragma_name)
{
    cout << endl << "Usage: " << pragma_name << \
    "+ ip + port[8000-8888]" << endl << endl; 
}
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    string ip = argv[1];
    uint16_t port = stoi(argv[2]);
    unique_ptr<TcpClient> cp(new TcpClient(ip,port));
    cp->Init();
    cp->Run();
    return 0;
}
  • server.cc
#include<iostream>
#include<memory>
#include<functional>
#include"server.hpp"
#include"servercal.hpp"
using std::unique_ptr;

void Usage(char* pragma_name)
{
    cout << endl << "Usage: " << pragma_name \
    << " + port[8000-8888]" << endl << endl; 
}
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    uint16_t port = stoi(argv[1]);
    CalHelper cal;
    unique_ptr<TcpServer> tp(new TcpServer(port,\
    bind(&CalHelper::Cal,&cal,placeholders::_1)));
    //bind是C++的一个接口,用于封装函数,便于使用。
    //因为cal是库里面的,因此要指定作用域,并传this指针,
    //绑定参数,进而封装出指定类型的函数。
    tp->Init();
    tp->Run();
    return 0;
}
  • bind的使用:跳转详见目录
  • 运行结果:
    在这里插入图片描述
  • 这里我们传数据,接收数据,处理数据都是一批一批的进行的,因此可以看见待处理的字符串。

3.Json的简单使用

  • 在上面实现的过程中,唯一比较难设计的就是序列化与反序列化的过程,上面我们为了进一步的理解,所以自己设计,但是市面上有一些简单好用的序列化与反序列化工具,下面我们介绍一种。

网络中,序列化与反序列化有现成的工具,比如json 和 protobuf这两个工具,下面我们简单介绍Json的使用。

  1. 安装Json库
sudo yum install -y jsoncpp-devel
  • 说明: 普通用户需要输入root密码并且要添加到系统的信任白名单中,所以这里建议直接su命令切到root用户直接安装。
  1. 简单使用
  • test.cc
#include<iostream>
#include<string>
#include<jsoncpp/json/json.h>
using namespace std;
int main()
{
  Json::Value root;
  Json::StyledWriter writer;
  //Json::FastWriter writer;
  //StyleWriter打印起来比较有风格。
  //FastWrier打印比较紧凑,比较省空间。
  root["x"] = 1;
  root["y"] = 2;
  root["oper"] = '+';

  string res = writer.write(root);
  

  //序列化之后的结果:
  cout << "序列化之后的结果:" << endl;
  cout << res << endl;

  
  Json::Value des;
  Json::Reader r;
  r.parse(res,des);
  int x = des["x"].asInt();
  int y = des["y"].asInt();
  char oper = des["oper"].asInt();
  

  //反序列化的结果:
  cout << "反序列化的结果为:" << endl;
  cout << x << " " << oper << " " << y << endl;
  return 0;
}
  1. 编译运行查看结果
g++  test.cc -std=c++11 -ljsoncpp

在这里插入图片描述

总结

  1. 铺垫TCP三次握手,四次挥手的概念,以及理解全双工。
  2. 实现了自定义协议(封装报头) + 序列化与反序列化网络版本的计算器。
  3. 介绍了Json工具的基本使用。

 了解自定义协议之后,我们将在下篇认识现成的应用层协议之Http。

尾序

  • 我是舜华,期待与你的下一次相遇!

http://www.niftyadmin.cn/n/5414865.html

相关文章

SORA 探秘

感谢过年的时候的openai 给全世界带来了一份大礼SORA&#xff0c;一直以来的业界都很关心&#xff0c;怎么讲图片的生成能力推广到视频上&#xff0c;openai 就给出了这个问题的答案 Sora: Creating video from text​openai.com/sora 从给出的视频效果来看&#xff0c;达到了…

走向审计4.0:内部审计数字化转型的路径与方法【文末送书-34】

文章目录 走向审计4.0&#xff1a;内部审计数字化转型的路径与方法一、内部审计的发展阶段二、内部审计的逻辑架构三、内部审计数字化转型面临的问题四、内部审计数字化转型的框架方法五、内部审计的数字化转型能力体系六、内部审计的数字化转型路径七、内部审计的数字化系统平…

mongo和redis的数据备份和还原

redis 安装 Redis安装和基本使用&#xff08;windows版&#xff09; - 知乎 window环境下Redis7服务器的安装和运行_redis7 windows-CSDN博客 备份数据 Redis SAVE 命令用于创建当前数据库的备份。 该命令将在 redis 安装目录中创建dump.rdb文件 查询路径 CONFIG GET dir…

SpringMVC 执行流程、组件解析、DispatcherServlet初始化过程 调用组件处理请求

SpringMVC执行流程 用户发送请求至前端控制器DispatcherServlet。DispatcherServlet收到请求调用HandlerMapping处理器映射器。处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找)&#xff0c;生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServl…

liunx 进程地址空间

程序地址空间 地址空间的整体进程地址空间进程空间的写时拷贝进程控制块 地址空间的整体 接下来我们用代码验证存储位置。 #include<iostream> #include<stdlib.h> using namespace std;int g_nval; int g_val10;int main() {cout<<"code add:"&l…

祝全天下的女孩们节日快乐,想要更快乐请使用闪侠惠递!

她们是社会的中坚力量 是家庭的温暖港湾 无论在家庭、职场 还是在生活的每一个角落 她们都以独特的魅力 影响着世界 使之变得更加美好 在三八妇女节这个特殊的日子里&#xff0c;让我们向所有的女性致以最崇高的敬意和祝福。让我们感谢她们为社会、家庭和人类所做出的贡献…

高校水电预付费系统有什么作用?

Hey小伙伴们&#xff0c;今天来聊聊咱们大学里的一个超级实用的设施——水电预付费系统&#xff01;你是不是还在为每个月的水电账单烦恼呢&#xff1f;别担心&#xff0c;这个系统就是你的生活小帮手&#xff01; 首先&#xff0c;想象一下这 样的场景&#xff1a;开学初&…

[力扣 Hot100]Day46 二叉树展开为链表

题目描述 给你二叉树的根结点 root &#xff0c;请你将它展开为一个单链表&#xff1a; 展开后的单链表应该同样使用 TreeNode &#xff0c;其中 right 子指针指向链表中下一个结点&#xff0c;而左子指针始终为 null 。 展开后的单链表应该与二叉树 先序遍历 顺序相同。 出…