文章中有使用封装好的头文件,可以在下面连接处查询。
Linux相关博文中使用的头文件_Gosolo!的博客-CSDN博客
1. 应用层
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层
1.2 协议
我们在之前的套接字编程中使用的是基于字符串的通信,客户端connect()之后,通过getlne()来输入数据,从而通过send()发送给服务端。如果我们要传输一些"结构化的数据" 怎么办呢?
那就需要指定一些约定。这些约定构成了协议。
客户端发送一个形如"1 + 1"的字符串;
这个字符串中有两个操作数, 都是整形;
两个数字之间会有一个字符是运算符,...
定义结构体来表示我们需要交互的信息;
发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;...
2. 序列化与反序列化
2.1 序列化
以聊天软件为例,发送人、发送消息、发送时间明明是三个消息,但是通过一定手段合成一个报文统一的发给对方。
结构化的数据转为字符串数据。
2.2 反序列化
对方读到之后,将该报文再读回三个消息,分别装填到发送人、发送消息、发送时间。
字符串结构转为结构化的数据。
2.3 结构化数据和字节流式数据对比
为什么我们之前实现tcp,udp时就是传递的结构化的数据呢?
一是因为协议一旦定制完成就不会轻易更改,而这个协议可以支持网路通信使用结构化的数据。二是网络协议,有一些大小端问题,代码也有一些条件编译,会自动识别平台,设置好对齐的规则。内核级的协议定制解决了这些问题。
一般而言,结构化的数据是给上层应用来使用的。而字节流的数据更适用于网络传输,这样在上层业务和网络通信之间增加了一个软件层,使其解耦。
3.网络版计算器
要实现使用结构化的数据进行传输,首先需要有两个类,一个用于发送待计算的数字和运算方法,一个用于保存结果。假定输入格式为"x_ op_ y_",那还需要定义一个分隔符。
//封装到一个命名空间里 即没有命名冲突 到时候还可以展开 using namespace ns_protocol
namespace ns_protocol
{
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
一个用于返回结果
//请求
class Request
{
public:
Request()
{}
Request(int x,int y,char op)
:x_(x)
,y_(y)
,op_(op)
{}
~Request() {}
//序列化
//反序列化
public:
int x_;
int y_;
char op_;
};
//回复
class Response
{
public:
Response()
{}
Response(int result, int code, int x, int y, char op)
:result_(result)
,code_(code)
,x_(x)
,y_(y)
,op_(op)
{}
~Response() {}
//序列化
//反序列化
public:
int result_;//返回结果
int code_;//用于评判本次计算是否合法
int x_;
int y_;
char op_;
};
}
3.1 自定义协议
如果要想使用这个计算器,那么需要满足一些约定。仅规定输入格式为"_x _op _y"够用吗?
对于TCP而言,其是面向字节流的,IO读写操作,其实本质上是拷贝函数。
send || write 其实就是将我们自己定义的buffer中的内容拷贝到客户端的发送缓冲区中。
recv || read 其实就是将服务端的接收缓冲区中拷贝到我们自己定义的buffer里面。
那这些缓冲区什么时候发,发多少,出错了怎么办都是由TCP传输控制协议中决定的。所以发送的次数和接收的次数没有任何关系。
所以如果我们自主实现协议,就需要增加一些报文(特殊标识符)。来确定我们接收到的数据中,是否能组成一组“合法”的操作。
所以我们规定输入格式为 "length\r\nx_ op_ y_\r\n",至少读够一个这样的字符串才开始执行。
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)
3.1.1 序列化
std::string Requst::Serialize()
{
std::string str;
str=std::to_string(x_);
str+=SPACE;
str+=op_;
str+=SPACE;
str+=std::to_string(y_);
return str;
}
std::string Response::Serialize()
{
std::string s;
s = std::to_string(code_);
s += SPACE;
s += std::to_string(result_);
return s;
}
3.1.2 反序列化
bool Requst::Deserialized(const std::string &str)
{
size_t left=str.find(SPACE);
if(left==std::string::npos) return false;
size_t right=str.rfind(SPACE);
if(right==std::string::npos) return false;
//左右两个空格都找到了
x_=atoi(str.substr(0,left).c_str());
y_=atoi(str.substr(right+SPACE_LEN).c_str());
if (left + SPACE_LEN > str.size())
return false;
else
op_ = str[left + SPACE_LEN];
return true;
}
bool Respon::Deserialized(const std::string &s)
{
std::size_t pos = s.find(SPACE);
if (pos == std::string::npos)
return false;
code_ = atoi(s.substr(0, pos).c_str());
result_ = atoi(s.substr(pos + SPACE_LEN).c_str());
return true;
}
3.2 使用 json定制协议
安装json
sudo yum install jsoncpp-devel
json的使用
用{}的KV结构,支持数组
3.3 添加报文
namespace ns_protocol
{
//添加报文
std::string Encode(std::string &s)
{
std::string new_package = std::to_string(s.size());
new_package += SEP;
new_package += s;
new_package += SEP;
return new_package;
}
//删除报文
// "length\r\nx_ op_ y_\r\n..." //
std::string Decode(std::string &buffer)
{
std::size_t pos = buffer.find(SEP);
if(pos == std::string::npos) return "";
//拿到报文前面那个length
int size = atoi(buffer.substr(0, pos).c_str());
//拿到真实的有效载荷的长度
int surplus = buffer.size() - pos - 2*SEP_LEN;
if(surplus >= size)
{
//至少具有一个合法完整的报文, 可以动手提取了
buffer.erase(0, pos+SEP_LEN);
std::string s = buffer.substr(0, size);
//提取完成了 但是删干净 不影响下次进行判断
buffer.erase(0, size + SEP_LEN);
return s;
}
else
{
return "";
}
}
}
3.4 接受信息和发送信息
因为客户端和用户端都需要调用recv()和send(),而且这两端都需要包括协议文件,所以将这两个接口封装到这里。
namespace ns_protocol
{
bool Recv(int sock, std::string *out)
{
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0); // 9\r\n123+789\r\n
if (s > 0)
{
buffer[s] = 0;
*out += buffer;
}
else if (s == 0)
{
std::cout << "client quit" << std::endl;
return false;
}
else
{
std::cout << "recv error" << std::endl;
return false;
}
return true;
}
void Send(int sock, const std::string str)
{
std::cout << "send in" << std::endl;
int n = send(sock, str.c_str(), str.size(), 0);
if (n < 0)
std::cout << "send error" << std::endl;
}
}
4.客户端 服务端的代码逻辑
4.1 服务端 CalServer.cc
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>
#include <signal.h>
using namespace ns_tcpserver;//这个是TcpServer.hpp内的命名空间
using namespace ns_protocol;
static void Usage(const std::string &process)
{
std::cout << "\nUsage: " << process << " port\n"<< std::endl;
}
static Response calculatorHelper(const Request &req)
{
Response resp(0, 0, req.x_, req.y_, req.op_);
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 (0 == req.y_)
resp.code_ = 1;
else
resp.result_ = req.x_ / req.y_;
break;
case '%':
if (0 == req.y_)
resp.code_ = 2;
else
resp.result_ = req.x_ % req.y_;
break;
default:
resp.code_ = 3;
break;
}
return resp;
}
//返回值类型为void 参数类型为int 符合包装器
void calculator(int sock)
{
std::string inbuffer;
while(true)
{
//1.读取数据
bool res=Recv(sock,&inbuffer);
if(!res) break;
std::cout << "begin: inbuffer: " << inbuffer << std::endl;
//2.协议解析,得到一个完整的报文
std::string package=Decode(inbuffer);
if(package.empty())
{
//进行下一次读取
continue;
}
logMessage(NORMAL, "%s", package.c_str());
Request req;
//3.进行反序列化,将字符数据存进结构数据
req.Deserialized(package);
//4.对这些数据进行处理
Response resp = calculatorHelper(req);
//5. 再序列化回去
std::string respString= resp.Serialize();
//6.添加报文
respString = Encode(respString);
//7.发送数据
Send(sock, respString);
}
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
// 一般服务器,都是要忽略SIGPIPE信号的,防止在运行中出现非法写入而发生抛异常
signal(SIGPIPE, SIG_IGN);
std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
server->BindService(calculator);//屏蔽信号是为了这里
server->Start();
return 0;
}
4.2 服务端 CalClient.cc
#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"
using namespace ns_protocol;
static void Usage(const std::string &process)
{
std::cout << "\nUsage: " << process << " serverIp serverPort\n"
<< std::endl;
}
// ./client server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
Sock sock;
int sockfd = sock.Socket();
//连接
if (!sock.Connect(sockfd, server_ip, server_port))
{
std::cerr << "Connect error" << std::endl;
exit(2);
}
bool quit = false;
std::string buffer;
while (!quit)
{
// 1. 获取需求
Request req;
std::cout << "Please Enter # ";
std::cin >> req.x_ >> req.op_ >> req.y_;
// 2. 序列化
std::string s = req.Serialize();
// 3. 添加长度报头
s = Encode(s);
// 4. 发送给服务端
Send(sockfd, s);
// 5. 正常读取
while (true)
{
bool res = Recv(sockfd, &buffer);
if (!res)
{
quit = true;
break;
}
//去除报文
std::string package = Decode(buffer);
if (package.empty())
continue;
Response resp;
//反序列化
resp.Deserialized(package);
std::string err;
switch (resp.code_)
{
case 1:
err = "除0错误";
break;
case 2:
err = "模0错误";
break;
case 3:
err = "非法操作";
break;
default:
std::cout << resp.x_ << resp.op_ << resp.y_ << " = " << resp.result_ << " [success]" << std::endl;
break;
}
if(!err.empty()) std::cerr << err << std::endl;
break;
}
}
close(sockfd);
return 0;
}
4.3 运行截图
注意不能少打空格 有bug
5. 守护进程
我们之前写的程序,全部都是在前台进行的,即启动服务器必须输入
./CalServer 8080
如果我退出了XShell 这个进程就退出了。如果我们不想让它退出,就需要让它守护进程化。
一些概念:
- 前台进程:和终端关联的进程。
判断一个进程是不是前台进程:看能否处理你的输入。
- 任何xshell登录,只允许一个前台进程和多个后台进程。
- 进程除了有自己的pid,ppid,还有一个组id。
- 在命令行中,同时用管道启动多个进程,多个进程是兄弟进程,而同时被创建的多个进程可以成为一个进程组的概念,组长一般是第一个进程。
- 任何一次登录,登录的用户,需要有多个进程(组),来给这个用户提供服务。用户也可以自己启动很多进程(组)。我们把给用户提供服务的进程或者用户自己启动的所有的进程或者服务,整体归纳到一个叫做会话的机制中。
守护进程化其实就是让进程变成一个单独的会话。使退出bash也不会影响这个进程的存活。
如何将自己变成自成会话呢?
5.1 setsid
#include <unistd.h>
pid_t setsid(void);
让自己自成会话并变成这个进程组的组长。成功返回pid 失败返回-1
注:
- setsid要被成功调用,必须保证当前进程不是进程组的组长。所以需要配合fork()使用。
- 守护进程不能直接向显示器打印消息,一旦打印会被暂停、终止。
5.2 写一个函数 让调用的进程变成守护进程
目标:
1.忽略SIGPIPE,SIGCHLD。
2.不要让自己成为组长 ——fork()
3.调用setsid()
4.标准输入、标准输出、标准错误的重定向。——重定向到哪里呢? /dev/null
/dev/null 该文件是一个写入自动丢弃,读入的时候不阻塞但是什么都读不到。
#pragma once
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void MyDaemon()
{
// 1. 忽略信号,SIGPIPE,SIGCHLD
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2. 不要让自己成为组长
if (fork() > 0)
exit(0);
// 3. 调用setsid
setsid();
// 4. 标准输入,标准输出,标准错误的重定向,守护进程不能直接向显示器打印消息
int devnull = open("/dev/null", O_RDONLY | O_WRONLY);
if(devnull > 0)
{
dup2(0, devnull);
dup2(1, devnull);
dup2(2, devnull);
close(devnull);
}
}
使用