网络通信之应用层协议--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套接字编程及代码示范