常见的应用层协议
HTTP/HTTPS称为超文本传输协议,用于传输浏览器使用的普通文本、超文本、音频和视频等数据。
HTTP协议
认识URL
url也就是网址,主要是由协议方案名,登录信息,服务器地址,服务器端口号,带层次的路径,查询字符串以及片段标识符组成。
当然现在有些信息在浏览器中被隐藏起来,并且会对一些字符类似于+,?,/等字符进行转义,如下图所示,c++的加号被转义成%2B,转义规则一般是将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY 格式。
HTTP协议格式
http是怎么样分离报头跟有效载荷的,主要是通过空行来决定的,http的协议格式主要如下所示,分为请求端以及响应端;
请求端:
由请求行,请求报头,空行以及正文部分组成,其中请求行中包含了[方法]+[url]+[版本],其中方法主要是包括GET方法(获取资源)以及POST方法(传输实体主题)。请求报头也称为header,主要包括请求的属性,类似于长度,主机地址等等,在接下来一个空行分隔报头和正文内容,正文内容也叫做body,body允许为空字符串,如果body存在,则header中会有一个Content-Length属性标识Body的长度。
响应端:
由响应行,响应,空行以及body组成,响应行:[版本号]+[状态码]+[状态码解释]组成;响应(Header): 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束;响应正文(Body): 空行后面的内容都是Body,Body允许为空字符串。如果Body存在, 则在Header中会有一个 Content-Length属性来标识Body的长度;如果服务器返回了一个html页面, 那么html页面内容就是在 body中。
相应端的状态码
类别 | 原因短语 | |
1xx | informational(信息状态码) | 正在处理请求 |
2xx | Success(成功状态码) | 请求处理完毕 |
3xx | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4xx | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5xx | Server Error(服务端错误状态码) | 服务器处理请求出错 |
最常见的状态码,比如 200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向), 504(Bad Gateway)。
上诉的请求和响应的细节:
1、请求和响应如何保证应用层完整读取完毕了?
首先c++保证了可以完整的读取一行,使用while(getline(request,line))就可以读到所有的请求行+请求报头全部读完直到空行,根据报头中的content-Length属性,就可以确定正文的长度,通过解析出来的内容长度,根据内容长度读取正文即可。
2、请求和响应怎么做到序列化和反序列化的?
hhtp自己实现的,第一行+请求/响应报头,只需要按照\r\n将字符串1->n既可以,正文不用做。
当然对于直接调用http协议的话,就不需要做序列化和反序列化。
示例1:网络计算器,自己实现的序列化以及反序列化
协议以及打印错误信息的的代码,这里请求和响应必须同时带有serialize和diserialize的功能,进行序列化和反序列化,然后需要计算正文的长度。这里的协议采用两种方式,一种是普通的字符串方式,一种是JSON的方式
protocol.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
#define SEP " "
#define SEP_LEN strlen(SEP) // sizeof 计算长度的时候会加上/n 而strlen 只计算字符的个数
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP)
enum
{
PERICOUS = 0,
DIV_ZERO,
MOD_ZERO,
INPUT_ERROR
};
// 增加字长去区分正文和标记
// 发送字节流的格式是 “content_len\r\ncontent\r\n”
std::string enLength(const std::string &str)
{
std::string res_text = std::to_string(str.size());
res_text+=LINE_SEP;
res_text+=str;
res_text+=LINE_SEP;
return res_text;
}
//去掉前面的“content_len\r\ncontent\r\n” -->content_len,将内容存储在req_str中
bool deLength(std::string &str, std::string *req_str)
{
auto pos = str.find(LINE_SEP);
if(pos ==std::string::npos) return false;
std::string content_len = str.substr(0,pos);
ssize_t len = std::stoi(content_len);
//这里求出来的是内容的长度
*req_str=str.substr(pos+LINE_SEP_LEN,len);
return true;
}
//没有人规定网络通信的时候,规定一种协议
//怎么让系统知道我们使用的是哪一种协议 呢
// x + y ------->res
// 因为TCP是全双工的,所以对于string的数据需要把他转化成 x op y的形式
class Request
{
public:
Request()
: _x(0), _op('+'), _y(0)
{
}
Request(int x, char op, int y)
: _x(x), _op(op), _y(y)
{
}
// 1. 序列化---》1.自己写 2. 用现成的
// "x + y" 需要转换成这样的形式,然后转成字节流传送给服务端 --序列化
bool serialize(std::string *expression) // 输入输出型参数
{
#ifdef MYSELF
*expression = std::to_string(_x);
*expression += SEP;
*expression += _op;
*expression += SEP;
*expression += std::to_string(_y);
#else
Json::Value root;
root["first"] = _x;
root["op"] = _op;
root["second"]=_y;
Json::FastWriter writer;
*expression = writer.write(root);
#endif
return true;
}
// "x + y /t/n" 的string 类型的对象,需要将他拆成 _x _op _y的格式 --反序列化
bool deserialize(const std::string &str)
{
#ifdef MYSELF
size_t left = str.find(SEP);
size_t right = str.rfind(SEP);
if (left == std::string::npos || right == std::string::npos)
return false;
if (left == right)
return false;
if (right - (left + SEP_LEN) != 1)
return false;
std::string x_string = str.substr(0,left);
std::string y_string = str.substr(right+SEP_LEN);
if(x_string.empty() || y_string.empty()) return false;
_x = std::stoi(x_string);
_y = std::stoi(y_string);
_op = str[left + SEP_LEN];
#else
Json::Value root;
Json::Reader reader;
reader.parse(str,root);
_x=root["first"].asInt();
_y=root["second"].asInt();
_op=root["op"].asInt();
#endif
return true;
}
~Request() {}
public:
int _x;
char _op;
int _y;
};
//
class Response
{
public:
Response()
: _exitcode(0), _exitrest(0)
{
}
Response(const int &exitrest, const int &exitcode)
: _exitcode(exitcode), _exitrest(exitrest)
{
}
// 把response 的exitcode 和exitresult 包装成字符串传出去,用于网络层的传输
//"exitcode exitresult"
bool serialize(std::string *expression)
{
#ifdef MYSELF
*expression += std::to_string(_exitcode);
*expression += SEP;
*expression += std::to_string(_exitrest);
#else
Json::Value root;
root["_exitrest"] = _exitrest;
root["_exitcode"] = _exitcode;
Json::FastWriter writer;
*expression = writer.write(root);
#endif
return true;
}
//把"exitcode exitresult"的字符串进行分割
bool deserialize(std::string &str)
{
#ifdef MYSELF
auto pos = str.find(SEP);
if(pos == std::string::npos) return false;
std::string ec_string =str.substr(0,pos);
std::string res_string =str.substr(pos+SEP_LEN);
if(ec_string.empty() || res_string.empty()) return false;
_exitcode = std::stoi(ec_string);
_exitrest = std::stoi(res_string);
#else
Json::Value root;
Json::Reader reader;
reader.parse(str,root);
_exitrest=root["_exitrest"].asInt();
_exitcode=root["_exitcode"].asInt();
#endif
return true;
}
~Response() {}
public:
int _exitrest; //计算的结果
int _exitcode; // 0 表示计算结果正确 1 表示除法除数为0 或者%运算有0
};
//content_len\r\ncontent\r\n content_len\r\ncontent\r\ncontent_len\r\ncontent\r\ncontent_len\r\ncontent\r\n
bool recvRequest(const int sock,std::string &inbuffer,std::string *req_text)
{
char buffer[1024];
while(true)
{
ssize_t num = recv(sock,buffer,sizeof(buffer)-1,0);
if(num>0)
{
buffer[num]=0; //给尾部置0
inbuffer+=buffer;
std::cout << "读取到的inbuffer: " << inbuffer << std::endl;
auto pos = inbuffer.find(LINE_SEP);
if(pos == std::string::npos) continue;
//就是读到了第一个/r/n
//首先计算出正文的长度
std::string len_str = inbuffer.substr(0,pos);
int len_content = std::stoi(len_str);
int total_len = len_str.size()+2*LINE_SEP_LEN+len_content;
std::cout << "处理前#inbuffer: " << inbuffer << std::endl;
if(inbuffer.size()<total_len)
{
std::cout<<"输入的消息未遵守协议,正在等待后续内容"<<std::endl;
continue;
}
//至少由一个完整的报文 //已经将正文内容读取到了text_content中
*req_text= inbuffer.substr(0,total_len);
//抹去已经读取到的一个完整的报文
std::cout<<"从inbuffer中读取到的数据"<<*req_text<<std::endl;
inbuffer.erase(0,total_len);
std::cout<<"处理后#inbuffer:"<<inbuffer<<std::endl;
break;
}
else
{
return false;
}
}
return true;
}
客户端代码:客户端需要进行tcp链接,首先需要socket通信,tcp的客户端需要bind,但是不用显示的bind,os这里尤其是client port要让OS自定随机指定!不需要进行listen和accept操作,需要向服务端发起connect连接,在connect之后,需要将自己的需求发送给服务端send操作,在服务端计算完成之后,进行recv操作。
calClient.hpp
#pragma once
#include <iostream>
#include <cstring>
#include "data.hpp"
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
namespace client
{
using namespace std;
enum
{
USAGE_ERROR = 0,
SOCKET_ERROR,
BIND_ERROR
};
class calclient
{
public:
calclient(const string &ip, const uint16_t port)
: _sockfd(-1), _serverip(ip), _serverport(port)
{
}
void initClient()
{
// 1.跟udp一样 首先进行socket设置
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
cout << "SOCKET error" << endl;
exit(SOCKET_ERROR);
}
cout << "SOCKET success" << endl;
// 2. tcp的客户端要不要bind?要的! 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!
// 3. 要不要listen?不要!
// 4. 要不要accept? 不要!
// 5. 要什么呢??要发起链接
}
void run()
{
// 这里需要connect链接
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_serverport);
peer.sin_addr.s_addr = inet_addr(_serverip.c_str());
if (connect(_sockfd, (struct sockaddr *)&peer, sizeof(peer)) != 0)
{
cerr << "socket connect error" << endl;
}
else
{
string message;
string inbuffer;
while(true)
{
cout << "mycal <<<<<";
getline(cin, message); //1+1 -->用来初始化req的格式
Request req = ParseLine(message);
std::string content;
req.serialize(&content);
cout<<"序列化之后的content: "<<content<<endl;
std::string send_message = enLength(content);
cout<<"加上分割符的send_message: "<<send_message<<endl;
send(_sockfd,send_message.c_str(),send_message.size(),0);
// 说明数据发送过去了
std::string package,text;
if(!recvRequest(_sockfd,inbuffer,&package))
continue;
cout<<"package= "<<package<<endl;
if(!deLength(package,&text))
{
continue;
}
//exit_code exit_result
Response resp;
resp.deserialize(text);
std::cout<<"exitcode: "<<resp._exitcode<<"_exitrest: "<<resp._exitrest<<endl;
}
}
}
~calclient() {}
//
Request ParseLine(const std::string &line)
{
// 建议版本的状态机!
//"1+1" "123*456" "12/0"
int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
int i = 0;
int cnt = line.size();
std::string left, right;
char op;
while (i < cnt)
{
switch (status)
{
case 0:
{
if(!isdigit(line[i]))
{
op = line[i];
status = 1;
}
else left.push_back(line[i++]);
}
break;
case 1:
i++;
status = 2;
break;
case 2:
right.push_back(line[i++]);
break;
}
}
std::cout << std::stoi(left)<<" " << std::stoi(right) << " " << op << std::endl;
return Request(std::stoi(left),op, std::stoi(right));
}
private:
int _sockfd;
string _serverip;
uint16_t _serverport;
char _op[5] ={'+','-','*','/','%'};
};
}
calClient.cc:主要是启动服务端
#include "calClient.hpp"
#include <memory>
using namespace client;
static void Usage(char* proc)
{
cout<<"/n/t"<<proc<<" local port error"<<endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
//对输入的参数进行转化
uint16_t port = atoi(argv[2]);
//创建指针
unique_ptr<calclient> tclt(new calclient(argv[1],port));
//开始初始化
tclt->initClient();
tclt->run();
return 0;
}
服务端的代码:
首先需要创建套接字,并且绑定固定的端口,在进行监听操作,这里就是和udp不同的地方需要监听的,然后进行accept获取新连接,获取新连接之后,返回对应的文件描述符,通过文件描述符与对应客户端进行链接,对于实际情况中,可能多个客户端与服务端相连,这是就需要采取多线程或者多进程的解决方案,尽量不要在服务端的内部完成业务函数,尽量写在外面,完成解耦,一贯原则:低耦合,高内聚。
calServer.hpp
#pragma once
#include "logMessage.hpp"
#include "data.hpp"
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <functional>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pthread.h>
namespace server
{
using namespace std;
enum
{
USAGE_ERROR = 0,
SOCKET_ERROR,
BIND_ERROR,
LISTEN_ERROR
};
#define NUM 1024
static int backlog = 5;
static uint16_t severport = 8080;
//using func_t = function<bool()>;
typedef function<bool(const Request& resq,Response& resp)> func_t; //这里resp是个输出型参数
//保证解耦
void calHandle(int sock,func_t func)
{
std::string inbuffer;
while(true)
{
//1.读取
//1.1 保证读到的消息是 【一个】 完整的请求
std::string req_text,req_str;
//1.2 保证req_text里面肯定是一个完整的请求 "content_len"\r\n"x op y"\r\n
if(!recvRequest(sock,inbuffer,&req_text))
return;
std::cout << "带报头的请求:" << req_text << std::endl;
if(!deLength(req_text, &req_str))
return; //
std::cout << "去掉报头的正文:" << req_str << std::endl;
//2. 反序列化
//2.1 得到一个结构化的请求对象
Request resq;
if(! resq.deserialize(req_str))
return ;
//3. 计算机处理req.x req.op req.y --业务逻辑
//3.1 得到一个结构化的响应
Response resp;
func(resq,resp); //resq的处理结果,全部放入到了re
//4.对响应Response 进行序列化
//4.1 得到一个字符串
std::string resp_str;
resp.serialize(&resp_str);
std::cout << "计算完成, 序列化响应: " << resp_str << std::endl;
// 5.发送响应
//5.1 首先需要构建一个完整的报文
std::string send_string = enLength(resp_str);
std::cout<<"构成完整的响应 :"<<send_string<<std::endl;
// send()
send(sock,send_string.c_str(),send_string.size(),0);
}
}
class calserver
{
public:
calserver(const uint16_t &port = severport)
: _listenSockfd(-1), _port(port)
{
}
void initServer()
{
// 1.首先创建套接字
_listenSockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSockfd == -1)
{
debugLog(FATAL, "socket error");
exit(SOCKET_ERROR);
}
debugLog(NORMAL, "socket success %d",_listenSockfd);
// 2.bind操作
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 主机转网络这儿需要进行一次转换
local.sin_addr.s_addr = INADDR_ANY; // 不需要绑定特定的端口号,所有的底层网络收到的消息都往上传
if (bind(_listenSockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
debugLog(FATAL, "bind error");
exit(BIND_ERROR);
}
debugLog(NORMAL, "bind success");
// 这里tcp与udp最大的不同之处在于,tcp是需要建立链接的,也就是需要监听的
int sockfd = listen(_listenSockfd, backlog); // 这里先把第二个参数设置为5
// 3.这里开始进行监听操作
if (sockfd < 0) // 建立链接失败
{
debugLog(FATAL, "listen error");
exit(LISTEN_ERROR);
}
debugLog(NORMAL, "listen sucess %d",sockfd);
}
void runServer(func_t func)
{
for (;;)
{
// 4.sever 获取新链接 accept函数
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 这里accept返回的sock也是一个文件描述符,通过这个与对应客户端进行链接
int sock = accept(_listenSockfd, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
debugLog(ERROR, "accept sock error");
continue;
}
debugLog(NORMAL, "accept a new link");
//version2使用多进程
pid_t id = fork();
if(id==0)
{
//child子进程
//对于子进程而言,需要父进程进行回收,父进程回收的话有两种方式
//阻塞等待和非阻塞等待,但是执行任务时死循环,如果阻塞等待跟串行运行没有区别
//所以选择再次fork()出一个孙子进程,再把子进程回收,然孙子进程去OS操作系统下
close(_listenSockfd); //对于每个子进程而言,都有父进程的资源,所以可以选择把listensockfd关闭
if(fork()>0) exit(-1);
calHandle(sock,func);
close(sock);
exit(0); //执行完成自动交给父进程退出
}
//father
waitpid(id,nullptr,0);
}
}
~calserver()
{
}
private:
int _listenSockfd;
uint16_t _port;
};
}
上诉程序的多进程处可以进行如下的修改
// version1 单进程主进程
// 5. 这里就是一个sock,未来通信我们就用这个sock,面向字节流的,后续全部都是文件操作
// seveceIO(sock,clientport);
// close(sock);
version3 多线程模式
// pthread_t pdt;
// threadData *td = new threadData(this, sock,clientport);
// pthread_create(&pdt, nullptr, startRoutine, td);
// // 这里也不需要阻塞式等待
// //pthread_join();
// //version4 线程池的方式
// ThreadPool<CalTask>::getInstace()->run();
// CalTask t(sock,seveceIO);
// ThreadPool<CalTask>::getInstace()->push(t);
calServer.cc代码:主要是业务逻辑代码以及服务端初始代码
#include "calServer.hpp"
#include "data.hpp"
#include <memory>
using namespace server;
static void Usage(char *proc)
{
cout << "/n/t" << proc << " local port error" << endl;
}
bool calfunc(const Request &resq, Response &resp)
{
resp._exitcode = PERICOUS;
switch (resq._op)
{
case '+':
{
resp._exitrest = resq._x + resq._y;
break;
}
case '-':
{
resp._exitrest = resq._x - resq._y;
break;
}
case '*':
{
resp._exitrest = resq._x * resq._y;
break;
}
case '/':
{
if (resq._y == 0)
resp._exitcode = DIV_ZERO;
else
resp._exitrest = resq._x / resq._y;
break;
}
case '%':
{
if (resq._y == 0)
resp._exitcode = MOD_ZERO;
else
resp._exitrest = resq._x % resq._y;
break;
}
default:
resp._exitcode = INPUT_ERROR;
break;
}
return true;
}
// 输入的形式为 ./tcpServer 8080的格式
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(-1);
}
unique_ptr<calserver> tser(new calserver(atoi(argv[1]))); // 智能指针
tser->initServer(); // 初始化操作
tser->runServer(calfunc); // 启动服务端
return 0;
}
打印日志的代码:其中引入了可变参数列表:可变参数列表,参数的压栈顺序从右向左的,形参实例化也是从右向左的。需要找到可变参数列表的前一个参数,作为分隔符进行分割。
#pragma once
#include <iostream>
#include <string>
#include <cstdarg>
#include <cstdio>
#include <time.h>
#include <unistd.h>
#define DEBUG 0
#define NORMAL 1 // 正常
#define WARNING 2
#define ERROR 3 // 普通错误,可以正常运行
#define FATAL 4 // 严重错误,不能正常运行
char *tostring_level(const int level)
{
switch (level)
{
case DEBUG:
return (char *)"DEBUG";
case NORMAL:
return (char *)"NORMAL";
case WARNING:
return (char *)"WARNING";
case ERROR:
return (char *)"ERROR";
case FATAL:
return (char *)"FATAL";
default:
return nullptr;
}
}
// 可变参数列表
// void debugLog(DEBUG,"hello %f %d %c",3.14,10,'C')
void debugLog(const int level, const char *format, ...) // 可变参数列表,参数的压栈顺序从右向左的,形参实例化也是从右向左的
{
#define NUM 1024
char logprofix[NUM]; // 得到[日志等级][2023-7-23][pid]等信息
char logcontent[NUM]; // 得到后面的可变参数列表信息
snprintf(logprofix, sizeof(logprofix), "[%s]-[%ld]-[%d]", tostring_level(level), (long int)time(nullptr), getpid());
va_list arg; //首先声明一个可变参数列表
va_start(arg, format); //这里的va_start会以format为分割线,将...里面的参数分到arg中
vsnprintf(logcontent, sizeof(logcontent), format, arg);
std::cout<<logprofix<<logcontent<<std::endl;
}
可变参数列表
首先声明一个函数,这个省略号就是可变参数列表,需要访问这个参数,首先需要了解一下偷吻中包含的va_list和三个宏va_start,va_arg和va_end。
void debugLog(const int level, const char *format, ...)
首先需要声明一个va_list的变量,声明方式如下
va_list arg;
紧接者初始化这个变量,通过调用va_start来初始化这个变量,他的第一次参数是va_list变量,第二个参数是省略号前面的最后一个有名字的有效参数,因为可变参数列表,参数的压栈顺序从右向左的,形参实例化也是从右向左的,通过最后一个有效参数就可以拿到可变参数的起始压栈位置,从而对参数进行初始化
vsnprintf(logcontent, sizeof(logcontent), format, arg);
这里的vsnprintf()原型如下,就可以按照指定的格式将可变参数列表里面的参数写入到logcontent变量中。
其中第三个参数是参数列表中参数的类型。
当然如果想要一个参数一个参数的进行访问,可以使用va_arg()函数,va_arg可以返回va_list型参数当前指向的参数的值,并让va_list型参数指向可变参数列表中的下一个参数。
va_arg(arg,format);
最后,在函数末尾,还要用到va_end来“结束”这个va_list型变量,参数就是这个va_list型变量的名字。调用方式:
va_end(arg);
守护进程
什么是守护进程,就是当我们的服务端运行在云服务器上的时候,关闭链接窗口,依然可以在外部继续访问的进程叫做守护进程。
像xshell链接远程服务器的时候,会形成会话,会话中可以有多个后台进程,但是只能有一个前台进程,最常见的前台进程就是bash,可以通过jobs查看当前的任务数量以及任务编号,可以通过ctrl+z的方式将一个前台进程变成后台进程(bg+任务编号,也可以在运行任务的时候&+运行的任务也是放到后台执行),在输入fg+任务编号之后,由后台进程变成前台进程。这里面还涉及到一个一个进程组的概念:进程组:一个或多个进程的集合,进程组由进程组ID标识,进程组长的进程ID和进程组ID一致,并且进程组ID不会由于进程组长的退出而受到影响。
如何让一个进程守护进程化
1.让调用进程忽略掉异常信号;2、让自己不是任务组的组长;3、守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件;4、可选:执行路径(cwd)是否发生变化。
// 可以用系统提供的守护进程函数
// 也可以自己写
// 调用函数时setsid()
#pragma once
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEV "/dev/null"
void daemonSelf(const char* curPath = nullptr)
{
// 守护进程化的流程
// 1.让调用进程忽略异常的信号
signal(SIGPIPE, SIG_IGN);
// 2.如何让自己不是组长,setsid
if (fork() > 0)
exit(0); // 将父进程退出,子进程跟父进程的进程地址,页表一样
// 由子进程来当组长
int n = setsid();
assert(n!=-1);
// 3. 守护进程是脱离终端的 关闭或者重定向以前进程默认打开的文件,把默认打开的012文件描述符重定向到 /dev/null
int fp = open(DEV,O_RDWR);
if(fp!=-1)
{
dup2(fp,0);
dup2(fp,1);
dup2(fp,2);
close(fp);
}
else
{
close(0);
close(1);
close(2);
}
// 4.可选:进程执行路径发生改变,主要是进程自带属性cwd 当前工作路径
// cwd 属性可以在/proc/pid 文件夹中查看
if(curPath) chdir(curPath);
}
示例2:HTTP请求网站
对于http请求网站只需要构建服务器的代码即可。
Protocol.hpp协议的代码,主要是包括了协议的一些
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "Util.hpp"
const std::string sep = "\r\n";
const std::string default_root = "./wwwroot"; // web根目录
const std::string home_page = "index.html"; //
class Request
{
public:
Request() {}
~Request() {}
void parse()
{
// 1. 从outbuffer里面获取第一行的,分割符是""\r\n"
std::string line = Util::getOneLine(inbuffer, sep); // 静态方法可以这样调用
if (line.empty())
return;
// 2.从请求字段种提取三个字段
std::stringstream ss(line);
ss >> method >> url >> httpversion; // 这里会自动以空格为分割符,流输入到这三个方法中
// 2.1 a/b/c.py?name=zhangsan&pwd=12345
// 通过问号可以把左右两部分分开
// 如果是POST方法,本来就是分离的!
// 左边是PATH,右边是参数parm
// 3. 添加web默认路径
path = default_root; // ./wwwroot
path += url; // ./wwwroot/a/b/c.html
if (path[path.size() - 1] == '/')
path += home_page;
// 4. 获取path对应的资源后缀
// ./wwwroot/index.html
auto pos = path.rfind(".");
if (pos == std::string::npos)
suffix = nullptr;
else
suffix = path.substr(pos); //.html .jpg
// 5. 获取文件的大小
// 可以通过stat函数获取
struct stat st_stat;
stat(path.c_str(),&st_stat);
size = st_stat.st_size;
}
public:
std::string inbuffer;
std::string method;
std::string url;
std::string httpversion;
std::string path;
std::string suffix;
int size;
std::string parm;
};
class Response
{
public:
std::string outbuffer; // 用来接受输出
};
Util.hpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
class Util
{
public:
// XXXX XXX XXX\r\nYYYYY
static std::string getOneLine(std::string &buffer, const std::string &sep)
{
auto pos = buffer.find(sep);
if(pos == std::string::npos) return "";
std::string sub = buffer.substr(0, pos);
buffer.erase(0, sub.size()+sep.size());
return sub;
}
static bool ReadFile(const std::string path, char *body,int size) //文本文件读取到body中
{
std::ifstream in(path);
if(!in.is_open()) return false; //统一没找到就是404
in.read(body,size);
//对于文本文件是可以的,但是音频是二进制文件
// std::string line;
// while(getline(in,line))
// {
// *body+=line;
// }
in.close();
return true;
}
};
httpServer.cc
#include "httpServer.hpp"
#include <memory>
using namespace server;
void USAGE(std::string proc)
{
cout << "USAGE\r\n"
<< proc << "Usage error\r\n"
<< endl;
}
std::string suffixtoDes(std::string suffix)
{
// 通过url的后缀给与不同的content-type
string respheader = "Content-Type: ";
if (suffix == ".html")
respheader += "text/html";
else if (suffix == ".jpg")
respheader += "application/x-jpg";
respheader += "\r\n";
return respheader;
}
// 1. 服务器和网通分离,html
// 2. url-》 / :
// 3. 需要正确的给客户端返回资源类型,首先需要自己知道!所有的资源都有后缀
bool Get(const Request &resq, Response &resp)
{
// if(req.path == "test.py")
// {
// //建立进程间通信,pipe
// //fork创建子进程,execl("/bin/python", test.py)
// // 父进程,将req.parm 通过管道写给某些后端语言,py,java,php等语言
// }
// if(req.path == "/search")
// {
// // req.parm
// // 使用我们自己写的C++的方法,提供服务
// }
// for test
cout << "-------------------------http start---------------------" << endl;
cout << resq.inbuffer << endl;
cout << "method: " << resq.method << endl;
cout << "url: " << resq.url << endl;
cout << "httpversion: " << resq.httpversion << endl;
cout << "path: " << resq.path << endl;
cout << "suffix: " << resq.suffix << endl;
cout << "size: " << resq.size << endl;
cout << "-------------------------http end---------------------" << endl;
string respline = "HTTP/1.1 200 OK\r\n";
string respheader = suffixtoDes(resq.suffix); //"Content-Type: text/html\r\n";,这里需要通过不同的后缀名选择不同的content-type的类型
// 把长度也加上
if (resq.size >= 0)
{
// respheader += "Content-Length: ";
// respheader += std::to_string(resq.size);
// respheader += "\r\n";
}
respheader += "Set-Cookie: name=12345678abcdg;Max-Age=120\r\n";
// 往后每次http请求,都会自动携带曾经设置的所有cookie,帮服务器进行鉴权行为 ---http会话保持
string respblank = "\r\n";
// std::string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>王应登,我宣你啊,baby</p></body></html>";
// 通过读取的方式的方式来获取网页
string body;
body.resize(resq.size + 1);
if (!Util::ReadFile(resq.path, (char *)body.c_str(), resq.size)) // 如果读不到,就是资源不存在
{
const std::string html_404 = "./wwwroot/404.html";
Util::ReadFile(html_404, (char *)body.c_str(), resq.size);
}
respheader += "Content-Length: ";
respheader += std::to_string(body.size());
respheader += "\r\n";
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
cout << "-------------------------http response start---------------------" << endl;
cout << resp.outbuffer << endl;
cout << "-------------------------http response end---------------------" << endl;
resp.outbuffer += body;
return true;
}
// ./httpServer 8080
int main(int argc, char *argv[])
{
if (argc != 2)
{
USAGE(argv[0]);
exit(0);
}
uint16_t port = atoi(argv[1]);
unique_ptr<httpServer> httpsvr(new httpServer(Get, port));
// httpsvr->registerCb("/", Get); // 功能路由!
// httpsvr->registerCb("/search", Search);
// httpsrv->registerCb("/test.py", Other);
httpsvr->initServer();
httpsvr->runServer();
return 0;
}
httpServer.hpp
#include "httpServer.hpp"
#include <memory>
using namespace server;
void USAGE(std::string proc)
{
cout << "USAGE\r\n"
<< proc << "Usage error\r\n"
<< endl;
}
std::string suffixtoDes(std::string suffix)
{
// 通过url的后缀给与不同的content-type
string respheader = "Content-Type: ";
if (suffix == ".html")
respheader += "text/html";
else if (suffix == ".jpg")
respheader += "application/x-jpg";
respheader += "\r\n";
return respheader;
}
// 1. 服务器和网通分离,html
// 2. url-》 / :
// 3. 需要正确的给客户端返回资源类型,首先需要自己知道!所有的资源都有后缀
bool Get(const Request &resq, Response &resp)
{
// if(req.path == "test.py")
// {
// //建立进程间通信,pipe
// //fork创建子进程,execl("/bin/python", test.py)
// // 父进程,将req.parm 通过管道写给某些后端语言,py,java,php等语言
// }
// if(req.path == "/search")
// {
// // req.parm
// // 使用我们自己写的C++的方法,提供服务
// }
// for test
cout << "-------------------------http start---------------------" << endl;
cout << resq.inbuffer << endl;
cout << "method: " << resq.method << endl;
cout << "url: " << resq.url << endl;
cout << "httpversion: " << resq.httpversion << endl;
cout << "path: " << resq.path << endl;
cout << "suffix: " << resq.suffix << endl;
cout << "size: " << resq.size << endl;
cout << "-------------------------http end---------------------" << endl;
string respline = "HTTP/1.1 200 OK\r\n";
string respheader = suffixtoDes(resq.suffix); //"Content-Type: text/html\r\n";,这里需要通过不同的后缀名选择不同的content-type的类型
// 把长度也加上
if (resq.size >= 0)
{
// respheader += "Content-Length: ";
// respheader += std::to_string(resq.size);
// respheader += "\r\n";
}
respheader += "Set-Cookie: name=12345678abcdg;Max-Age=120\r\n";
// 往后每次http请求,都会自动携带曾经设置的所有cookie,帮服务器进行鉴权行为 ---http会话保持
string respblank = "\r\n";
// std::string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>王应登,我宣你啊,baby</p></body></html>";
// 通过读取的方式的方式来获取网页
string body;
body.resize(resq.size + 1);
if (!Util::ReadFile(resq.path, (char *)body.c_str(), resq.size)) // 如果读不到,就是资源不存在
{
const std::string html_404 = "./wwwroot/404.html";
Util::ReadFile(html_404, (char *)body.c_str(), resq.size);
}
respheader += "Content-Length: ";
respheader += std::to_string(body.size());
respheader += "\r\n";
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
cout << "-------------------------http response start---------------------" << endl;
cout << resp.outbuffer << endl;
cout << "-------------------------http response end---------------------" << endl;
resp.outbuffer += body;
return true;
}
// ./httpServer 8080
int main(int argc, char *argv[])
{
if (argc != 2)
{
USAGE(argv[0]);
exit(0);
}
uint16_t port = atoi(argv[1]);
unique_ptr<httpServer> httpsvr(new httpServer(Get, port));
// httpsvr->registerCb("/", Get); // 功能路由!
// httpsvr->registerCb("/search", Search);
// httpsrv->registerCb("/test.py", Other);
httpsvr->initServer();
httpsvr->runServer();
return 0;
}