网络通信之应用层协议--Linux

Posted 皮皮蜥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络通信之应用层协议--Linux相关的知识,希望对你有一定的参考价值。

文章目录

关于应用层协议的理解

在之前的一篇关于套接字实现网络通信一文中,进行通信的内容多是字符串,内容形式可谓是很简单了。在传输的过程中只需要控制数据的传输大小,字符串就会通过套接字进行传送接收。但是结合实际生活来看,通信的场景却不只是这么简单的内容,比如很有可能是一个结构体,里面有着各种不同数据类型的变量。这里我们就希望用一种统一的视角来看待各不相同的数据结构体(C/C++)或者是对象(C++),无论是多么复杂的数据结构,我们总可以将我们所需要的数据进行排列并转化成字符串,并将这种转换方式规定为一种约定好的协议,使得大家都认同这种处理方式,以便于从字符串中准确读定位并读取所需要的信息。

简而言之,应用层协议是一种数据转换成字符串的转换方式,使得程序员可以按需传送相当复杂的数据结构。并且,协议可以由程序员自己规定,只要有人认同并使用该程序员的协议,那么就可以实现正常的网络通信了。经过网络发展的几十年,一些比较成熟的协议被几乎所有的程序员所认同,例如著名的http和https,都已经写好并投入使用好多年,作为网络协议的初始学习对象是再合适不过的,但在学习http与https相关协议之前,我们不妨试着自己写一个简单的协议,具体感受一下协议的定制过程,加深对协议的了解。

应用层协议的制定

接下来我就以网络简单版的计算器为示例,制定协议。

理论部分

第一步,在制定协议之前,得先清楚计算器的主要构成:操作数A、操作符、操作数B、计算结果、计算状态码(标识计算是否出现错误)其中操作数A、操作符、操作数B都是客户端的请求数据,而计算结果、计算状态码则是服务端的响应数据,因此我们需要两个结构体来进行数据的传送。

第二步,规定转换的字符串的风格,一般情况下该字符串我们成为报文,由报头和有效载荷两部分构成,如:

如果具体到算式,比如1+1,则有下面的客户端请求的报文形式:

其中报头5是有效载荷的长度(单位是字节),1 + 1是有效载荷(字符之间用空格分隔)。报头和有效载荷之间用\\r\\n分隔开,有效载荷后的\\r\\n是为了表明一个报文的结束。

有了以上的逻辑,我们接下来考虑的就是该如何将协议运用到代码之中去了。

我们提到了结构体转化成字符串的这一操作,规范一点的叫法就是**序列化。而将字符串再转化成结构体的操作,称之为反序列化。其次还有添加报头与分隔符和删去报头与分隔符的操作,称之为分装解包**。

值得注意的是,对于请求和响应两种结构体,他们的成员变量不同,因此各自的序列化和反序列化的函数还是要有所不同,但是对于已经序列化后生成的字符串,封装和解包的操作是一致的。

代码部分

自定义的协议头文件:

//Protocl.hpp
// 协议定制
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
using namespace std;
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define CRLF "\\r\\n"
#define CRLF_LEN strlen(CRLF)
#define OPS "+-*/%"

// 封装函数
// 增加序列化数据的长度大小的字符串到数据头,并添加\\r\\n(截断一段完整的信息)
// 例如:5\\r\\n1 + 1\\r\\n
string encode(string &in, size_t len)

    string tmp = to_string(len);
    tmp += CRLF;
    tmp += in;
    tmp += CRLF;
    return tmp;


// 解包函数
// 删去序列化数据头部表示长度的字符串,并删去\\r\\n(使信息暴露出来)
// 计算一段数据的长度,将其赋值给len
// 返回解包后的字符串(有效载荷)
string decode(string &in, size_t &len)

    len = 0;
    string package = "";//有效载荷
    size_t crlfOne = in.find(CRLF);//报头结尾
    size_t crlfTwo = in.rfind(CRLF);//有效载荷结尾
    if (crlfOne == string::npos || crlfTwo == string::npos)
    
        return package;
    
    string lenStr = in.substr(0, crlfOne);//报头字符串
    size_t tmpLen = atoi(lenStr.c_str());//报头所表示的数字
    // 检测是否有完整的有效载荷
    if (tmpLen != (in.size() - 2 * CRLF_LEN - lenStr.size()))//报头数字应该与有效载荷的长度相等
    
        return package;
    
    len = tmpLen;
    // 获得删去\\r\\n和头部数字的数据
    package = in.substr(crlfOne + CRLF_LEN, len);

    // 读取过的报文要从in中删去,避免in存在多个报文
    in.erase(0, crlfOne + 2 * CRLF_LEN + len);
    // 返回有效载荷
    return package;


class Request

public:
    Request()
    
    
    ~Request()
    
    
    // 序列化   结构化的数据-->字符串(即有效载荷)
    // 风格:数字|空格|运算符|空格|数字 例:1 + 1
    void serialize(string *out) // 输出型参数out
    
        string xstr = to_string(x_);
        string ystr = to_string(y_);
        *out = xstr;
        *out += SPACE;
        *out += op_;
        *out += SPACE;
        *out += ystr;
    
    // 反序列化  将传进来的字符串(即有效载荷)转换成结构化数据
    bool deserialize(string &in)
    
        size_t spaceOne = in.find(SPACE);
        size_t spaceTwo = in.rfind(SPACE);
        if (spaceOne == string::npos || spaceTwo == string::npos)
        
            return false;
        
        string number1 = in.substr(0, spaceOne);
        string number2 = in.substr(spaceTwo + SPACE_LEN);
        string op = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
        x_ = atoi(number1.c_str());
        y_ = atoi(number2.c_str());
        op_ = op[0];
        return true;
    
    void debug()//调试,显示结构体变量
    
        cout << "#################################" << endl;
        cout << "x_: " << x_ << endl;
        cout << "op_: " << op_ << endl;
        cout << "y_: " << y_ << endl;
        cout << "#################################" << endl;
    

public:
    int x_;
    int y_;
    char op_;
;
class Response

public:
    Response() : calCode_(0), result_(0)
    
    
    ~Response()
    
    
    // 序列化  结构化的数据-->字符串(即有效载荷)
    // 风格: 计算状态码|空格|计算结果 例:0 2
    void serialize(string *out)
    
        string code = to_string(calCode_);
        string result = to_string(result_);
        *out = code;
        *out += SPACE;
        *out += result;
    
    // 反序列化  将传进来的字符串(即有效载荷)转换成结构化数据
    bool deserialize(string &in)
    
        size_t spaceIndex = in.find(SPACE);
        if (spaceIndex == string::npos)
        
            return false;
        
        string code = in.substr(0, spaceIndex);
        string result = in.substr(spaceIndex + SPACE_LEN);
        calCode_ = atoi(code.c_str());
        result_ = atoi(result.c_str());
        return true;
    
    void debug()//调试,显示结构体变量
    
        cout << "#################################" << endl;
        cout << "calCode_: " << calCode_ << endl;
        cout << "result_: " << result_ << endl;
        cout << "#################################" << endl;
    

public:
    int calCode_; // 计算状态码,0为正常计算,-1为除零错误,-2为模零错误
    int result_;
;

上面其实就是协议的定制内容了,具体的使用我们可以在代码中讲解:

客户端相关代码(代码部分细节省略,后面整体的代码中会补全):

// 根据输入的字符串填充 请求结构体
bool makeRequest(const std::string &str, Request *req)

    // 123+1
    char strtmp[1024];
    snprintf(strtmp, sizeof strtmp, "%s", str.c_str());//将in中的数据拷贝到strtmp中
    char *left = strtok(strtmp, OPS);//根据操作符,找第一个操作数
    if (!left)
        return false;//找不到返回false
    char *right = strtok(nullptr, OPS);//找第二个操作数
    if (!right)
        return false;//找不到返回false
    char mid = str[strlen(left)];//操作符

    req->x_ = atoi(left);//字符串转化成数字
    req->y_ = atoi(right);
    req->op_ = mid;
    return true;


//下面是main函数中有关协议使用的相关代码
string message;
cout<<"please enter# ";
getline(cin,message);//获取运算表达式
Request req;
makeRequest(message, &req);//形成请求
std::string package;
req.serialize(&package); //请求序列化
package = encode(package, package.size());//封装
ssize_t s = write(sock, package.c_str(), package.size());//发送给服务端封装好的字符串(即报文)
if (s > 0) //发送成功,读取响应

    char buff[1024];
    size_t s = read(sock, buff, sizeof(buff)-1);//从服务端读取报文
    if(s > 0) buff[s] = 0;
    std::string echoPackage = buff;
    Response resp;
    size_t len = 0;
    std::string tmp = decode(echoPackage, len); //对报文进行解包,拿到有效载荷
    if(len > 0)
    
        echoPackage = tmp;
        resp.deserialize(echoPackage);//对有效载荷进行反序化
        printf("[calCode: %d] %d\\n", resp.calCode_, resp.result_);//输出计算结果
    

服务端相关代码(代码部分细节省略,后面整体的代码中会补全):

//根据请求生成响应
static Response calculator(const Request &req)

    Response resp;
    //根据请求的操作符进行操作数之间的运算,并标记计算状态码
    switch (req.op_)
    
    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.calCode_ = -1;
            else resp.result_ = req.x_ / req.y_;
        
    break;
    case '%':
        
            if (req.y_ == 0) resp.calCode_ = -2; 
            else resp.result_ = req.x_ % req.y_;
        
    break;
    default:
        resp.calCode_ = -3;
        break;
    
    return resp;
 
//下面是服务端关于协议的使用
string inbuffer;
char buff[BUFFER_SIZE];
ssize_t s = read(sock, buff, sizeof(buff) - 1);//读取请求报文
buff[s]=0;
inbuffer+=buff;//报文填充到buffer容器中
Request req;
size_t packageLen=0;
string package=decode(inbuffer,packageLen);//解包,拿到有效载荷
if(req.deserialize(package))//反序列化,填充请求结构体req

    Response resp=calculator(req);//根据请求,进行计算,生成响应
    string respPackage;
    resp.serialize(&respPackage);//将响应序列化,生成有效载荷
    respPackage=encode(respPackage,respPackage.size());//对有效载荷进行封装,形成报文
    resp.debug();
    write(sock,respPackage.c_str(),respPackage.size());//将响应报文发送给客户端

完整代码以及测试

我们采用守护进程的方式启动服务端,使用TCP传输协议,以单例线程池的方式处理计算任务,并且生成服务端的日志文件(相较于之前的日志函数,更加完善)。详细参考之前写过的一篇文章[网络通信]中TCP的通信模式,这里依旧会给出完整代码。

守护进程头文件:

//daemonize.hpp
#pragma once
#include <cstdio>
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void daemonize()

    int fd = 0;
    //忽略一些信号
    signal(SIGPIPE,SIG_IGN);
    
    //创建进程后直接结束父进程
    if(fork()>0)
    
        exit(0);
    
    //调用setsid()函数,使得子进程成为一组进程的组长
    setsid();
    
    //打开特殊文件“/dev/null”,相当于回收站,一切输入的数据都会被忽略
    if((fd=open("/dev/null",O_RDWR))!=-1)
    
        //三次重定向使得所有的输出都指向回收文件
        dup2(fd,0);
        dup2(fd,1);
        dup2(fd,2);
        if(fd>2) close(fd);//关闭特殊文件描述符,避免文件描述符泄露
    

上面这个函数调用完之后就会使得进程成为后台进程。

单例线程池

//ThreadPool.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
#include <queue>
#include <string>
using namespace std;
#define NUM 10
template <class T>
class ThreadPool

private:
    ThreadPool(const int &threadNum = NUM) : threadNum_(threadNum), isStart_(false)
    
        assert(threadNum_ > 0);
        //初始化锁和条件变量
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    
    ThreadPool(const ThreadPool<T> &) = delete;     // 删除拷贝构造
    void operator=(const ThreadPool<T> &) = delete; // 删除赋值构造
public:
    ~ThreadPool()
    
        //销毁锁和条件变量
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    
    int threadNum()//获取线程个数
    
        return threadNum_;
    
    static ThreadPool<T> *getInstance() // 申请类的对象
    
        pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
        if (instance == nullptr)
        
            pthread_mutex_lock(&mutex);
            if (instance == nullptr)
            
                instance = new ThreadPool<T>();
            
            pthread_mutex_unlock(&mutex);
        
        return instance;
    
    static void *threadpool(void *arg) //线程的回调函数
    
        pthread_detach(pthread_self());//线程分离
        ThreadPool<T> *tp 

Linux之网络基础?

网络基础

技术图片


网络模型两种基本类型:协议模型和参考模型
TCP/IP 模型描述了 TCP/IP 协议簇中每个协议层实现的功能,因此属于协议模型。
开放式系统互联 (OSI) 模型是最广为人知的网际网络参考模型,用于数据网络设计、操作规范和故障排除。
技术图片
技术图片


一段数据在任意协议层的表示形式称为协议数据单元 (PDU)

数据 - 一般术语,泛指应用层使用的 PDU
数据段 - 传输层 PDU
数据包 - 网络层 PDU
- 网络接入层 PDU
比特(位) - 通过介质实际传输数据时使用的 PDU
技术图片


OSI 参考模型各层功能

应用层:负责为应用程序提供网络服务
表示层:处理数据格式、数据加密等
会话层:建立、维护和管理会话
传输层:建立、维护虚电路,进行差错校验、提供端到端的可靠传输和流量控制
网络层:决定传输报文的最佳路由,其关键问题是确定数据包从源端到目的端如何选择路由
数据链路层:提供介质访问,链路控制等
物理层:涉及在通信信道(Channel)上传输的原始比特流,它定义了传输数据所需要的机械、电气功能及规程等特性。


IP地址分类

A类:1.0.0.0-126.0.0.0
B类:128.1.0.0-191.254.0.0
C类:192.0.1.0-223.255.254.0
D类:224.0.0.0-239.255.255.254(IP 地址通常作为组播地址)
E类:240.0.0.0-255.255.255.255(保留研究所用)


交换机只隔离冲突域,路由器既隔离广播域又隔离冲突域,集线器都不隔离

以上是关于网络通信之应用层协议--Linux的主要内容,如果未能解决你的问题,请参考以下文章

网络LinuxLinux网络编程-TCP,UDP套接字编程及代码示范

Linux学习资料整理

linuxlinux centos 6 日志服务rsyslogd日志服务

网络基础之网络协议

网络通信之应用层协议--Linux

计算机网络——应用层之远程终端协议(TELNET)简单网络管理协议应用进程跨越网络的通信