一、应用层
程序员写的解决实际问题满足日常需求的网络层序实际上都是在应用层
再谈协议
之前写的UDP、TCP socket发送接收的都是字符串,但是要是发送和接收的是结构化的数据该怎么办?
之下来将实现一个网络版本的计算器,客户端发送操作数以及运算符,服务端拿到之后计算并且返回。有两种实现方案,第一种是我们自己设计字符串然后发送,第二种是将操作数、运算符或者结果定义为结构化的数据,发送时将结构体转为字符串,接收时将字符串转为结构体,这就是序列化和反序列化
无论使用哪种方案,只要保证两端能够正确的解析就是可以的,这种约定就是应用层协议 ,协议就是双方约定好的结构化数据


其实也可以直接传结构体(二进制数据)不转换,但是不建议,应为客户端和服务端内存可能没有对齐,另一个重要的点就是两端的语言可以不一样,结构体也就不一样,不兼容;可以传体现在OS内部,应为OS都是C语言写的
若是要进行网络协议的通信,在应用层,直接使用序列化和反序列化即可
重新理解read、write以及tcp为什么支持全双工通信

在tcp协议实现的内部实际上存在缓冲区,接收以及发送缓冲区;而我们使用的write以及read并不是直接从网络上获取以及写入数据,而是从缓冲区中操作的;而通信的本质是数据的拷贝,先拷贝到tcp发送缓冲区,再拷贝到网卡,再拷贝到网络,从而拷贝到对方接收缓冲区;
至于为什么tcp支持全双工,这是因为使用tcp协议通信的双方都有两个缓冲区,在发送的同时也可以接收,这也是一个sockfd既可以读又可以写的原因。但是要有同步机制,也就是说一对通信双方再tcp内部是四个生产消费者模型。发送缓冲区空了,os不读等待应用层写;接收缓冲区满了,os不写,等待应用层读。
至于缓冲区内部是如何发送的,因为tcp是面向字节流的,也就是说我们不知道每一次读取以及写入会有多少数据,不想udp一次发送就是一个数据报,所以tcp是控制传输协议,什么时候操作缓冲区(读写)是由os拿着tcp协议自动控制的
网络计算器代码实现
服务端
首先设计服务端
最顶层封装Socket.hpp ,其中有两个类,一个基类一个子类,基类是Sokcet,子类是TcpSocket,父类设计为纯虚类,其中大部分方法是纯虚方法,子类实现;
之后在上层实例化一个TcpSocket的类并且形成多态,直接调用BuildTcpSocketMethod就可以一步完成socket通信的初始化了(申请、绑定、监听);
Accept函数在接收到客户端获取连接之后通过返回值的形式直接形成多态,这样上层拿到的就是用用可以读写fd的sock,然后利用此sock通信
Recv和Send函数都是在连接获取之后上层可以调用和客户端通信的
TcpServer.hpp,这一层是服务器层,主要功能是获取连接,之后将读写sock以及Accept带出来的client交给上层协议层对数据进行处理
Protocol.hpp 这一层是协议层,要完成两个任务,序列化和反序列化(保证应用层能够拿到数据形成约定,可以互相识别)、保证数据可以被读取一整个报文(TCP是面向字节流的,所以无法自动控制读到多少字节,所以我们要实现一种约定,保证对方读取的时候可以读到一整个报文)
Encode和Decode是处理读取问题的具体约定,封装报头形成类似于10\r\n{json串}\r\n,这样在解析的时候知道每一整个报文的头尾、Decode是解析
GetRequest是传给底层的处理函数,目的是解析成为含有报头的json串之后序列化这个json串去掉报头,之后再交给上层去计算,最后将结果封装报头and序列化成为json串,发送给客户端
NetCal.hpp:真正计算数据的顶层
总体思路:服务端初始化,已经处于监听状态,并且知道协议层的回调函数,协议层也知道顶层计算数据的回调函数,客户端建立连接,服务端Accept获取连接,得到读写sock,并且将其传给上层协议层调用回调函数,此时服务端处在等待接收数据,客户端发送数据,协议层拿到数据进行自定义处理(解析、反序列化),然后交给上层NetCal计算,拿到结果,封装报头之后序列化、添加报头后传给客户端,done

实际上OSI七层协议定义的很完美,只是前三层个性化太强,也就是不同用户的需求不一样,所以不能向传输层和网络层一样设计到内核中固化功能,所以教材上才会说OSI七层模型不好实现
客户端
申请套接字;通过传入的服务端ip和端口建立tcp连接,成功之后开始从标准输入获取数据,然后通过协议构建请求进行发送,接收服务端处理之后的结果进行打印
总体代码
Socket.hpp
cpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include <memory>
#include "InetAddr.hpp"
#include "Log.hpp"
namespace SocketModule
{
const int dbacklog = 16;
using namespace LogModule;
class Socket
{
private:
public:
virtual ~Socket(){};
virtual void SocketorDie() = 0;
virtual void BindorDie(uint16_t port) = 0;
virtual void ListenorDie(int backlog) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr* client) = 0;
virtual void Close() = 0;
virtual int Recv(std::string *out) = 0;
virtual int Send(const std::string& msg) = 0;
virtual void Connect(std::string& server_ip, uint16_t server_port) = 0;
void BuildTcpSocketMethod(uint16_t port, int backlog = dbacklog)
{
SocketorDie();
BindorDie(port);
ListenorDie(backlog);
}
void BuildTcpClientSocketMethod()
{
SocketorDie();
}
};
const static int dsockfd = -1;
class TcpSocket : public Socket
{
private:
int _sockfd; // listensockfd, readwritefd
public:
TcpSocket():_sockfd(dsockfd)
{}
TcpSocket(int sockfd):_sockfd(sockfd)
{}
void SocketorDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd == -1)
{
logger(LogLevel::FATAL) << "socket error!";
exit(SOCKET_ERR);
}
logger(LogLevel::INFO) << "socket success, _listen_sockfd : " << _sockfd;
}
void BindorDie(uint16_t port) override
{
InetAddr peer(port);
int n = ::bind(_sockfd, peer.NetAddr(), peer.AddrLen());
if (n != 0)
{
logger(LogLevel::FATAL) << "bind error!";
exit(BIND_ERR);
}
logger(LogLevel::INFO) << "bind success, _listen_sockfd : " << _sockfd;
}
void ListenorDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n != 0)
{
logger(LogLevel::FATAL) << "listen error!";
exit(LISTEN_ERR);
}
logger(LogLevel::INFO) << "listen success";
}
std::shared_ptr<Socket> Accept(InetAddr* client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = ::accept(_sockfd, CONV(peer), &len);
if (fd < 0)
{
logger(LogLevel::WARNING) << "accept error";
return nullptr;
}
client->SetAddr(peer);
return std::make_shared<TcpSocket>(fd);
}
void Close()
{
if (_sockfd >= 0)
::close(_sockfd);
}
int Recv(std::string *out) override
{
char buffer[1024];
int n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
// 此时调用Recv,使用的一定是进行读写的fd,因为上层是Accept中fd构造的
if (n > 0)
{
buffer[n] = '\0';
*out += buffer; // += 的原因是一直无脑读取,上层会处理这里只需要读即可
}
return n;
}
int Send(const std::string& msg) override
{
return ::send(_sockfd, msg.c_str(), msg.size(), 0);
}
void Connect(std::string& server_ip, uint16_t server_port) override
{
InetAddr server(server_ip, server_port);
int n = ::connect(_sockfd, server.NetAddr(), server.AddrLen());
if (n < 0)
{
logger(LogLevel::WARNING) << "connect error";
exit(CONNECT_ERR);
}
}
};
}
Tcpserver.hpp
cpp
#pragma once
#include "Socket.hpp"
using namespace SocketModule;
using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr& client)>;
// 这一层用来获取连接
class TcpServer
{
private:
uint16_t _port;
std::unique_ptr<Socket> _listensockptr;
bool _isrunning;
ioservice_t _service;
public:
TcpServer(uint16_t port, ioservice_t service):
_port(port), _listensockptr(std::make_unique<TcpSocket>()),
_isrunning(false), _service(service)
{
_listensockptr->BuildTcpSocketMethod(_port);
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
InetAddr client;
auto sock = _listensockptr->Accept(&client);
if (sock == nullptr)
{
continue;
}
logger(LogLevel::INFO) << "accept success";
// 多线程处理
pid_t pid = fork();
if (pid < 0)
{
logger(LogLevel::FATAL) << "fork error";
exit(FORK_ERR);
}
else if (pid == 0)
{
// 子进程
_listensockptr->Close();
if (fork() > 0)
exit(OK);
// 孙子进程执行任务
_service(sock, client);
sock->Close();
exit(OK);
}
else
{
// 父进程
sock->Close();
::waitpid(pid, nullptr, 0);
}
}
_isrunning = false;
}
};
Protocol.hpp
cpp
#pragma once
#include "Socket.hpp"
#include <iostream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
#include <functional>
using namespace SocketModule;
// 基于TCP协议的通信必须要有两个功能
// 一是要有序列化和反序列化
// 二是保证读数据的时候,可以读到完整的一个报文
class Request
{
private:
int _x;
int _y;
char _oper;
public:
Request()
{}
Request(int x, int y, char oper)
:_x(x)
,_y(y)
,_oper(oper)
{}
~Request()
{}
int GetX() {return _x;};
int GetY() {return _y;};
char GetOper() {return _oper;};
std::string Serialize()
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::FastWriter writer;
std::string s = writer.write(root);
return s;
}
bool Deserialize(std::string &in)
{
Json::Reader reader;
Json::Value root;
bool ok = reader.parse(in, root);
if (ok)
{
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
}
return ok;
}
};
class Response
{
private:
int _result;
int _code; // 一种约定 0表示成功运算,1、2、3、4表示不同的运算异常
public:
Response()
{}
Response(int result, int code)
:_result(result)
,_code(code)
{}
~Response()
{}
void SetResult(int result) {_result = result;}
void SetCode(int code) {_code = code;}
std::string Serialize()
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
std::string s = writer.write(root);
return s;
}
bool Deserialize(std::string &in)
{
Json::Reader reader;
Json::Value root;
bool ok = reader.parse(in, root);
if (ok)
{
_result = root["result"].asInt();
_code = root["code"].asInt();
}
return ok;
}
void ShowResult()
{
std::cout << "result : " << _result << " [" << _code << "]" << std::endl;
}
};
const std::string sep = "\r\n";
using func_t = std::function<Response(Request&)>; // 数据的计算交给上层
class Protocol
{
private:
func_t _func;
public:
Protocol(func_t func) : _func(func)
{}
Protocol()
{}
~Protocol()
{}
// 封装应用层报文
std::string Encode(std::string &jsonstring)
{
std::string len = std::to_string(jsonstring.size());
return len + sep + jsonstring + sep;
}
// 解析读到的报文
// 必须要找到sep并且读到的整个报文的长度要大于等于
// 数字字符串长度加上有效数据长度加上两个sep长度才行
// 只有这样才可能解析出一个正确的有效数据
bool Decode(std::string &buffer, std::string* package)
{
int pos = buffer.find(sep);
if (pos == std::string::npos)
{
return false;
}
std::string numstr = buffer.substr(0, pos);
int num = std::stoi(numstr); // 有效载荷的大小
int targetlen = numstr.size() + num + sep.size() * 2;
if (buffer.size() < targetlen) // 说明缓冲区内的数据还不够一个有效载荷
{
return false;
}
// 提取有效载荷到package 并且从缓冲移除这部分数据
*package = buffer.substr(pos + sep.size(), num);
buffer.erase(0, targetlen);
return true;
}
// 这个方法就是底层TcpServer调用的上层方法,用来处理数据(解析&&序列化)
// 而计算的实现再回调给上层去处理
void GetRequest(std::shared_ptr<Socket> &sock, InetAddr& client)
{
// 首先获取发送来的数据
std::string buffer_queue;
while (true)
{
int n = sock->Recv(&buffer_queue);
if (n > 0)
{
// 开始解析
std::string jsonpackage;
int ok = Decode(buffer_queue, &jsonpackage);
if (!ok)
continue; //继续读然后解析
// 反序列化才可以操作数据
Request req;
ok = req.Deserialize(jsonpackage);
if (!ok)
continue;
// 此时将req传入上层去计算
Response resp = _func(req);
// 但是不能将结果直接发送,先序列化之后添加报头然后发送
std::string jsonstr = resp.Serialize();
std::string sendstr = Encode(jsonstr);
sock->Send(sendstr);
}
else if (n == 0) // 客户端退出->写端退出
{
logger(LogLevel::INFO) << client.StringAddr() << "Quit!";
break;
}
else // 读取失败
{
logger(LogLevel::WARNING) << client.StringAddr() << " resv error!";
break;
}
}
}
// 获取响应
bool GetResponse(std::shared_ptr<Socket> &client, std::string& resp_buffer, Response *resp)
{
while (true)
{
int n = client->Recv(&resp_buffer);
if (n > 0)
{
// 解析字符串
std::string json_package;
bool ret = Decode(resp_buffer, &json_package);
if (!ret)
continue;
// 反序列化
resp->Deserialize(json_package);
return true;
}
else if (n == 0)
{
logger(LogLevel::INFO) << "Server Quit!";
break;
}
else
{
logger(LogLevel::WARNING) << " resv error!";
break;
}
}
return false;
}
// 构建请求
std::string BuildRequestString(int x, int y, char oper)
{
// 首先序列化然后添加报头
Request req_json(x, y, oper);
std::string json_str = req_json.Serialize();
std::string req_str = Encode(json_str);
return req_str;
}
};
NetCal.hpp
cpp
#pragma once
#include <iostream>
#include "Protocol.hpp"
class Cal
{
public:
Cal(){}
~Cal(){}
Response Execute(Request& req)
{
Response resp(0, 0); // 第二个0表示正常运算
switch (req.GetOper())
{
case '+' :
resp.SetResult(req.GetX() + req.GetY());
break;
case '-' :
resp.SetResult(req.GetX() - req.GetY());
break;
case '*' :
resp.SetResult(req.GetX() * req.GetY());
break;
case '/' :
{
if (req.GetY() == 0)
{
resp.SetCode(1); // 表示除0错误
}
else
{
resp.SetResult(req.GetX() / req.GetY());
}
}
break;
case '%' :
{
if (req.GetY() == 0)
{
resp.SetCode(2); // 表示模0错误
}
else
{
resp.SetResult(req.GetX() % req.GetY());
}
}
break;
default:
resp.SetCode(3); // 表示非法操作
break;
}
return resp;
}
};
cpp
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "NetCal.hpp"
using namespace SocketModule;
// ./main port
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "Usage : " << argv[0] << " port" << std::endl;
exit(USAGE_ERR);
}
uint16_t port = std::stoi(argv[1]);
// 顶层
std::unique_ptr<Cal> cal = std::make_unique<Cal>();
// 协议层
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request& req)
->Response{return cal->Execute(req);}
);
//服务器层
std::unique_ptr<TcpServer> tcpsver = std::make_unique<TcpServer>(port,
[&protocol](std::shared_ptr<Socket> &sock, InetAddr& client){
protocol->GetRequest(sock, client);
});
tcpsver->Start();
return 0;
}
cpp
#include "Socket.hpp"
#include <string>
#include <iostream>
#include <memory>
#include "Protocol.hpp"
using namespace SocketModule;
void GetDataFromStdin(int* x, int* y, char* oper)
{
std::cout << "输入x:" << std::endl;
std::cin >> *x;
std::cout << "输入y:" << std::endl;
std::cin >> *y;
std::cout << "输入oper:" << std::endl;
std::cin >> *oper;
}
// ./tcpclient serverip serverport
int main(int argc, char* argv[])
{
if (argc != 3)
{
std::cout << "Usage: " << "tcpclient " << argv[1] << " " << argv[2] << std::endl;
exit(USAGE_ERR);
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();
// 申请套接字
client->BuildTcpClientSocketMethod();
// 建立连接
client->Connect(ip, port);
// 定义协议
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();
std::string resp_buffer;
while (true)
{
int x, y;
char oper;
GetDataFromStdin(&x, &y, &oper);
// 构建请求
std::string req_str = protocol->BuildRequestString(x, y, oper);
// 发送请求
client->Send(req_str);
// 获取响应
Response resp;
bool ret = protocol->GetResponse(client, resp_buffer, &resp);
if (!ret)
break;
resp.ShowResult();
}
client->Close();
}
测试结果


二、守护进程化
现在有一个问题,服务端在命令行启动是不可行的,因为服务端本质上不能收到终端开启关闭的影响,不能说终端关闭了,服务器就崩溃了吧,服务端应该是独立的,那么要如何实现呢?这就需要守护进程的引入。
前置知识
进程组 :
使用管道将三个sleep进程连接起来,加上&在后台运行,ps -axj查看相关信息,这三个进程的父进程id相同,这三个进程的关系是兄弟进程;PGID就是进程组id;进程组就是多个进程的集合,用来完成一个任务,进程组组长的pid就是进程组id;若是只有一个进程完成一个任务,那么这个进程就是一个独立的进程组,也就是里面只有一个进程,那么进程id就是进程组id。
session:
SID就是会话id,一个会话里面有多个进程组;当我们打开终端并且连接的时候,系统自动创建一个会话,并且会话里面默认有一个独立进程组,这个进程组里面的进程就是bash


进程组一定是属于某一个会话的
进程组和任务的区别就是一个硬币的正反面,在系统角度是进程组,在用户角度这些进程组是为了完成一项任务的,可以理解为就是一个任务
任务在前台后台的切换指令:
jobs:查看当前系统的后台任务
fg+任务号:将后台任务提到前台来
ctrl+c:终止前台进程,但是后台进程不会受到影响
ctrl+z;暂停前台任务,并且将此任务切换到后台,状态是暂停;bash又会切换回前台
bg+任务号:让暂停的后台进程运行起来,状态变为运行

现在有一组现象:先写一个proc.cc这个程序是一直打印hello server;在前台直接运行这个程序,运行过程中输入指令(ls、pwd等等)会发现这些指令失效了,但是将这个任务切换到后台运行之后,输入这些指令bash又会做出相应了,这是为什么?
首先看这个现象

现在回答这个问题:
在UNIX系统中,用户通过终端登陆系统之后回得到一个Shell进程,这个终端成为Shell进程的控制终端,控制终端是保存在PCB中的信息,fork会复制PCB中的信息,因此有Shell进程吸毒的其他进程的控制终端也是这个终端,默认情况下没有重定向,每个进程的标准输入、标准输出和标注错误都指向这个控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出也就是输出到显示器上。
这样就会出现一个问题:当多个进程启动并且需要从标准输入获取数据的时候就会出错,因为键盘只有一个,在某一时间也只能为一个进程来提供输入。
也就是标准输入的数据只能给一个指定的任务,这个任务就是前台任务。
那么得出结论:只有前台任务才可以获取标准输入的数据,后台进程不会对键盘的输入做出响应。在一个会话内部,进程组必须分为前台和后台进程组,并且前台任务组只能有一个,而后台进程组可以有多个。
回归正轨,守护进程(精灵进程)
有前置知识我们知道,我们登录终端之后创建的一个会话包含我们写的网络计算器服务端,那么当终端退出会话销毁之后这个服务端就会退出,而这个影响了服务器的运行,我们期望的是服务器的运行不受到影响。
所以有这样一个方法:将服务器所在的进程组单独提出来构建一个会话,并且这个会话只有一个进程组,也就是服务器所在进程组。如何实现呢,使用setsid 接口即可。但是这个接口有一个前提:调用这个接口的进程不能是一个进程组的组长,那么我们在具体实现的时候就需要创建子进程,然后让父进程直接退出,所以守护进程本质上是一个孤儿进程,因为它的父进程已经终止,它由1号进程领养
代码实现
Daemon的两个参数设置位0,0;第一个0表示将守护进程的工作目录设置为根目录,保证守护进程不会一直占用此目录,防止想要删除该目录的时候删除失败,第二个0表示将守护进程的重定向标准输入,标准输出,标准错误到/dev/null(这个null会吞掉),这个时候就看不到守护进程的运作了,想要知道守护进程是否启动成功,只能使用echo $?,输出位0,表示守护进程正常运转
cpp
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "NetCal.hpp"
#include "Daemon.hpp"
using namespace SocketModule;
// ./main port
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "Usage : " << argv[0] << " port" << std::endl;
exit(USAGE_ERR);
}
std::cout << "服务器启动,已经是一个守护进程了" << std::endl;
Daemon(0, 0);
uint16_t port = std::stoi(argv[1]);
// 顶层
std::unique_ptr<Cal> cal = std::make_unique<Cal>();
// 协议层
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request& req)
->Response{return cal->Execute(req);}
);
//服务器层
std::unique_ptr<TcpServer> tcpsver = std::make_unique<TcpServer>(port,
[&protocol](std::shared_ptr<Socket> &sock, InetAddr& client){
protocol->GetRequest(sock, client);
});
tcpsver->Start();
return 0;
}
Daemon.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
#include "Common.hpp"
using namespace LogModule;
const std::string dev = "/dev/null";
// 将服务进行守护进程化的服务
void Daemon(int nochdir, int noclose)
{
// 1. 忽略IO,子进程退出等相关的信号
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN); // SIG_DFL
// 2. 父进程直接结束
if (fork() > 0)
exit(0);
// 3. 只能是子进程,孤儿了,父进程就是1
setsid(); // 成为一个独立的会话
if(nochdir == 0) // 更改进程的工作路径???为什么??
chdir("/");
// 4. 依旧可能显示器,键盘,stdin,stdout,stderr关联的.
// 守护进程,不从键盘输入,也不需要向显示器打印
// 方法1:关闭0,1,2 -- 不推荐
// 方法2:打开/dev/null, 重定向标准输入,标准输出,标准错误到/dev/null
if (noclose == 0)
{
int fd = ::open(dev.c_str(), O_RDWR);
if (fd < 0)
{
logger(LogLevel::FATAL) << "open " << dev << " errno";
exit(OPEN_ERR);
}
else
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
}
运行结果,此时看不到守护进程的输出了


成为孤儿进程