再谈 “协议”
- 协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?
网络版计算器
-
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端
-
约定方案一:
- 客户端发送一个形如"1+1"的字符串;
- 这个字符串中有两个操作数, 都是整形;
- 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
- 数字和运算符之间没有空格;
- …
- 约定方案二:
- 定义结构体来表示我们需要交互的信息;
- 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
- 这个过程叫做 “序列化” 和 “反序列化”
保证可以完整的读取一个报文
方案一(自己写序列化)
makefile
cc=g++
LD=-DMYSELF
.PHONY:all
all:calserver calclient
calclient:calClient.cc
$(cc) -o $@ $^ -std=c++11
calserver:calServer.cc
$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f calclient calserver
log.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char * to_levelstr(int level)
{
switch(level)
{
case DEBUG : return "DEBUG";
case NORMAL: return "NORMAL";
case WARNING: return "WARNING";
case ERROR: return "ERROR";
case FATAL: return "FATAL";
default : return nullptr;
}
}
void logMessage(int level, const char *format, ...)
{
#define NUM 1024
char logprefix[NUM];
snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",
to_levelstr(level), (long int)time(nullptr), getpid());
char logcontent[NUM];
va_list arg;
va_start(arg, format);
vsnprintf(logcontent, sizeof(logcontent), format, arg);
std::cout << logprefix << logcontent << std::endl;
}
protocol.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#define SEP " "
// 不敢使用sizeof(),因为其包含\0这个字符,而strlen()不会
#define SEP_LEN strlen(SEP)
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()
enum
{
OK = 0,
DIV_ZERO,
MOD_ZERO,
OP_ERROR
};
// 将报头和报文的正文内容进行拼接,中间用\r\n进行分割
// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
std::string enLength(const std::string &text)
{
// 报头中存放的就是报文正文内容所占字节的大小
std::string send_string = std::to_string(text.size());
// 将报头和报文的正文部分进行拼接,它们之前只用分隔符进行分割
send_string += LINE_SEP;
send_string += text;
send_string += LINE_SEP;
// 将其返回
return send_string;
}
// 去掉完整报文的报头,通过输出参数std::string *text
// 返回报文的正文部分
// "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{
// "content_len"\r\n"exitcode result"\r\n
// package.find(LINE_SEP)拿到报头右侧的分隔符的位置
auto pos = package.find(LINE_SEP);
if (pos == std::string::npos)
return false;
// package.substr(0, pos)拿到报头字符串
std::string text_len_string = package.substr(0, pos);
// 将报头字符串转化为数字,其对应的就是报文正文的长度,即text_len
int text_len = std::stoi(text_len_string);
// package.substr(pos + LINE_SEP_LEN, text_len)拿到报文正文的内容
*text = package.substr(pos + LINE_SEP_LEN, text_len);
// 去掉报头成功,则返回真
return true;
}
// 当服务器或者客户端收到请求消息时,需要将网络中的消息先进行反序列化
// 再进行处理
class Request
{
public:
// 无参构造
Request() : x(0), y(0), op(0)
{
}
Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_)
{
}
// 序列化
// 1. 自己写序列化
// 2. 用现成的系统提供的序列化或者反序列化的接口
bool serialize(std::string *out)
{
// 将*out进行清空
*out = "";
// 结构化 -> "x op y";
// x是数字,使用接口to_string()将数字转化为字符串
// op本身就是char类型,因此不需要进行转化
std::string x_string = std::to_string(x);
std::string y_string = std::to_string(y);
// 将所有的字符串进行拼接,且字符之间使用SEP(空格)进行分割
*out = x_string;
*out += SEP;
*out += op;
*out += SEP;
*out += y_string;
return true;
}
// 反序列化
// "x op y";
// const std::string &in, in里面存放的去掉报头的报文正文
// 将其进行反序列化
bool deserialize(const std::string &in)
{
// "x op y" -> 结构化
// in.find(SEP),找到的是左操作数x右边的分隔符
auto left = in.find(SEP);
// in.rfind(SEP),找到的是右操作数y左边的分隔符
auto right = in.rfind(SEP);
if (left == std::string::npos || right == std::string::npos)
return false;
if (left == right)
return false;
// SEP_LEN 是分隔符SEP的长度
if (right - (left + SEP_LEN) != 1)
return false;
// in.substr(0, left),分割的字符串是左闭右开的区间
// [0, 2) [start, end) , start(起始位置),
// end - start(从起始位置分割end - start个字符串)
std::string x_string = in.substr(0, left); // 分割出来的做操作数
std::string y_string = in.substr(right + SEP_LEN); // 分割出来的右操作数
// 保证x_string不为空串
if (x_string.empty())
return false;
if (y_string.empty())
return false;
// 将左操作数和右操作数,从字符串转化为数字
x = std::stoi(x_string);
y = std::stoi(y_string);
// 操作符op的下标为left + SEP_LEN
op = in[left + SEP_LEN];
return true;
}
public:
// "x op y"
int x;
int y;
char op;
};
// 当服务器或者客户端处理完收到的请求的数据之后,
// 需要将处理后的结果构建成可以发送给网络的响应
class Response
{
public:
Response() : exitcode(0), result(0)
{}
Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_)
{}
// 序列化
bool serialize(std::string *out)
{
*out = "";
// 将退出码,和处理结果转化为字符串
std::string ec_string = std::to_string(exitcode);
std::string res_string = std::to_string(result);
// 将退出码,和处理结果转化的字符串进行拼接
*out = ec_string;
*out += SEP;
*out += res_string;
return true;
}
// 反序列化
bool deserialize(const std::string &in)
{
// "exitcode result"
auto mid = in.find(SEP);
if (mid == std::string::npos)
return false;
std::string ec_string = in.substr(0, mid);
std::string res_string = in.substr(mid + SEP_LEN);
// 保证退出码和处理结果不是空字符串
if (ec_string.empty() || res_string.empty())
return false;
// 将退出码和处理结果从字符串转化为数字
exitcode = std::stoi(ec_string);
result = std::stoi(res_string);
// 反序列化成功,则返回真
return true;
}
public:
// 0:计算成功,!0表示计算失败,具体是多少,定好标准
int exitcode;
int result; // 计算结果
};
// 从套接字对应的文件描述符sock中进行读取
// 如下是多个报文的字节流
// "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op
bool recvPackage(int sock, std::string &inbuffer, std::string *text)
{
// 定义应用层的缓冲区,来存放报文
char buffer[1024];
while (true)
{
// 从sock读取sizeof(buffer) - 1个字节并将其存放到buffer中
// 经过多次循环,就可以将sock中的内容全部读取
// 并返回读取内容的字节个数
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
// 给字符串的末尾添加结束符,即\0
buffer[n] = 0;
// 将读取的所有数据放入到string类型的inbuffer中
inbuffer += buffer;
// 分析处理
// "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n
// inbuffer.find(LINE_SEP)就是报头"content_len"右侧的分隔符的位置
auto pos = inbuffer.find(LINE_SEP);
if (pos == std::string::npos)
continue; // 如果没有找到,那么就返回继续读取,再重新找
// inbuffer.substr(0, pos),获取到报头
// 报头存放就是报文正文内容所占的字节数,即text_len_string
std::string text_len_string = inbuffer.substr(0, pos);
// 将报文正文内容所占的字节数,由字符串转为数字
int text_len = std::stoi(text_len_string);
// total_len 报文的总长度
// text_len_string.size()是报头的长度
// 2 * LINE_SEP_LEN + text_len 报文正文和两个分割符长度的总和
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
// text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size();
std::cout << "处理前#inbuffer: \n"<< inbuffer << std::endl;
// inbuffer.size()小于total_len,说明inbuffer没有一个完整的报文
// 那么就返回,继续从sock读取,直到inbuffer.size() > total_len
if (inbuffer.size() < total_len)
{
std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;
continue;
}
// 至少有一个完整的报文
// 分割出一个完整报文
*text = inbuffer.substr(0, total_len);
// 从inbuffer删除掉分割出的报文
inbuffer.erase(0, total_len);
std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;
break;
}
else
// 如果n小于等于0说明对方关闭了sock文件描述符对应的文件,因此返回错误
return false;
}
return true;
}
calServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <functional>
#include "log.hpp"
#include "protocol.hpp"
using namespace std;
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 默认缺省的端口号为8080
static const uint16_t gport = 8080;
static const int gbacklog = 5;
// const Request &req: 输入型
// Response &resp: 输出型
typedef std::function<bool(const Request &req, Response &resp)> func_t;
void handlerEntery(int sock, func_t func)
{
std::string inbuffer;
while (true)
{
// 1. 读取:"content_len"\r\n"x op y"\r\n
// 1.1 你怎么保证你读到的消息是 【一个】完整的请求
// req_text代表收到的完整请求报文(输出型参数)
// req_str代表收到的请求报文的正文(输出型参数)
std::string req_text, req_str;
// 1.2 我们保证,我们req_text里面一定是一个完整的请求:"content_len"\r\n"x op y"\r\n
if (!recvPackage(sock, inbuffer, &req_text))
return;
std::cout << "带报头的请求:\n"<< req_text << std::endl;
if (!deLength(req_text, &req_str))
return;
std::cout << "去掉报头的正文:\n"<< req_str << std::endl;
// 2. 对请求Request,反序列化
// 2.1 得到一个结构化的请求对象
// 这个结构化的请求对象为报文的正文部分,需要将其进行反序列化
// 我们才能够对正文中的数据做处理
Request req;
// req.deserialize(req_str),反序列化成功,则返回值为真
if (!req.deserialize(req_str))
return;
// 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑
// 3.1 得到一个结构化的响应
Response resp;
// resp就是将收到的请求做处理后的响应,发回给请求方
// req的处理结果,全部放入到了resp
func(req, resp);
// 4.对响应Response,进行序列化
// 4.1 得到了一个"字符串",也就是数据处理后得到的响应结果
// 需要将响应结果进行序列化,才能够发送给网络
std::string resp_str;
resp.serialize(&resp_str);
// 5. 然后再发送响应
// 5.1 构建成为一个完整的报文
std::string send_string = enLength(resp_str);
std::cout << "构建完成完整的响应\n"<< send_string << std::endl;
// 将其发送
send(sock, send_string.c_str(), send_string.size(), 0);
}
}
class calServer
{
public:
// tcp服务器的构造函数
calServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{
}
// 初始化服务器
void initServer()
{
// 1. 创建socket文件套接字对象
// tcp也是网络连接,因此第一个参数为AF_INET
// tcp是面向字节流的,因此第二个参数为SOCK_STREAM
// 确定好前两个参数,第三个参数默认为0就可以
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 套接字的文件描述符小于0,那么创建套接字失败
// 则我们通过日志,打印对应的错误信息
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
// 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;
// bind小于0,代表绑定失败
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
// 通过日志,打印绑定失败的错误信息
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
// tcp是建立连接的协议,因此客户端和服务器是要建立连接的
// 所谓的连接,也就是服务器实时知道客户端向其发出了请求
// tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
// 然后才可以向服务器发送消息
// 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
// 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
// 等待客户的电话,这样才可以实时与客户建立连接
// 3. 设置socket 为监听状态
// 参数_listensock,就是要将这个参数设置为监听状态
// gbacklog,后续再说其用处,目前暂时先设置为5
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
// // 通过日志,打印设置监听状态失败的错误信息
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
}
// 开始启动服务器
void start(func_t func)
{
// 4. server 获取新链接
// _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
// _listensock只负责监听用户请求,新的套接字sock只负责通信
// sock, 和client进行通信的fd(文件描述符)
for (;;)
{
// 参数
// struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
// socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
// 获取连接失败,并不是致命的错误,因此日志等级为ERROR
logMessage(ERROR, "accept error, next");
// 获取连接失败,则重复上面代码的运行,继续获取_listensock监听的下一个用户的连接
continue;
}
logMessage(NORMAL, "accept a new link success, get new sock: %d", sock);
// version 2 多进程版(2)
pid_t id = fork();
if (id == 0) // child
{
close(_listensock);
// if(fork()>0) exit(0);
handlerEntery(sock, func);
close(sock);
exit(0);
}
close(sock);
// father
pid_t ret = waitpid(id, nullptr, 0);
if (ret > 0)
{
logMessage(NORMAL, "wait child success");
}
}
}
~calServer()
{
}
private:
// 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
int _listensock;
uint16_t _port;
};
} // namespace server
calServer.cc
#include "calServer.hpp"
#include <memory>
using namespace server;
using namespace std;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
// req: 在进行任务处理时,此时req里面一定是处理好的报文的正文,可以直接进行数据处理
// resp: 根据req,进行业务处理,填充resp,不用管理任何读取和写入,序列化和反序列化等任何细节
bool cal(const Request &req, Response &resp)
{
// exitcode,和结果默认设置为0
resp.exitcode = OK;
resp.result = OK;
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)
// 发生除0错误,返回对应的错误码
resp.exitcode = DIV_ZERO;
else
resp.result = req.x / req.y;
}
break;
case '%':
{
if (req.y == 0)
// 发生模0错误,返回对应的错误码
resp.exitcode = MOD_ZERO;
else
resp.result = req.x % req.y;
}
break;
default:
// 运行到这里,说明操作符出错,则返回对应的错误码
resp.exitcode = OP_ERROR;
break;
}
return true;
}
// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<calServer> tsvr(new calServer(port));
tsvr->initServer();
tsvr->start(cal);
return 0;
}
calClient.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "protocol.hpp"
#define NUM 1024
class calClient
{
public:
// 客户端的构造函数
calClient(const std::string &serverip, const uint16_t &serverport)
: _sock(-1), _serverip(serverip), _serverport(serverport)
{
}
// 初始化客户端
void initClient()
{
// 1. 创建套接字socket
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
// 创建套接字失败,打印错误信息
std::cerr << "socket create error" << std::endl;
exit(2);
}
// 2. tcp的客户端要不要bind?要的!
// 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!
// 3. 要不要listen?不要!
// 4. 要不要accept? 不要!
// 5. 要什么呢? 要发起链接!
}
// 启动客户端
void start()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_serverport);
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
{
// 客户端向服务器发起链接失败,则打印错误信息
std::cerr << "socket connect error" << std::endl;
}
else
{
// 链接成功,客户端和服务端进行通信
std::string line;
std::string inbuffer;
while (true)
{
std::cout << "mycal>>> ";
// 将输入流的数据,放入到应用层的缓冲区line中
std::getline(std::cin, line); // 1+1
// ParseLine(line)拿到了输入数据构建的请求
Request req = ParseLine(line); // "1+1"
// 将数据序列化之后,通过输出参数std::string content返回
// 此时content也就是要发送的报文的正文
std::string content; // 输出型参数
req.serialize(&content);
// 给报文添加报头并返回
std::string send_string = enLength(content);
std::cout << "sendstring:\n"<< send_string << std::endl;
// 将序列化之后的报文发送
send(_sock, send_string.c_str(), send_string.size(), 0);
// 读取服务器计算数据后的返回结果
// std::string package,输出型参数,返回完整的报文
// std::string text,输出型参数,返回报文正文
std::string package, text;
// 从套接字的文件描述符sock中读取一个完整的报文
// "content_len"\r\n"exitcode result"\r\n
if (!recvPackage(_sock, inbuffer, &package))
continue;
// 分离报文报头和报文正文
if (!deLength(package, &text))
continue;
// 报文正文包含如下内容
// "exitcode result"
// 从网络中拿到的数据,因此需要先进行反序列化
Response resp;
resp.deserialize(text);
std::cout << "exitCode: " << resp.exitcode << std::endl;
std::cout << "result: " << resp.result << std::endl;
}
}
}
// 客户端的析构函数
~calClient()
{
// 关闭客户端套接字对应的文件描述符
if (_sock >= 0)
close(_sock);
}
Request ParseLine(const std::string &line)
{
// 建议版本的状态机!
//"1+1" "123*456" "12/0"
int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
int i = 0;
// line.size()是输入左操作数、右操作数、操作符和分隔符的总长度
int cnt = line.size();
std::string left, right;
char op;
while (i < cnt)
{
switch (status)
{
case 0:
{
// 状态码为0,表明当前位置是在操作符之前
// isdigit(line[i])判断line[i]是不是数字,是数字则返回真
if (!isdigit(line[i]))
{
// 此时line[i]一定是操作符
op = line[i];
// 1:碰到了操作符,因此将状态码改为1
status = 1;
}
else
// 此时line[i] 一定是左操作数
left.push_back(line[i++]);
}
break;
case 1:
// 状态码为1,说明line[i]一定是操作符,因此进行++
i++;
// ++ 之后line[i]一定为右操作数,因此将状态码变为2
status = 2;
break;
case 2:
// 此时line[i]一定为右操作数
right.push_back(line[i++]);
break;
}
}
std::cout << std::stoi(left) << " " << std::stoi(right) << " " << op << std::endl;
// 将拿到的数据,构建为一个请求并返回
return Request(std::stoi(left), std::stoi(right), op);
}
private:
int _sock;
std::string _serverip;
uint16_t _serverport;
};
calClient.cc
#include "calClient.hpp"
#include <memory>
using namespace std;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n";
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
unique_ptr<calClient> tcli(new calClient(serverip, serverport));
tcli->initClient();
tcli->start();
return 0;
}
演示结果
方案二(系统接口写序列化)
安装jsoncpp库
sudo yum install -y jsoncpp-devel
- 成功安装后就可以进行如下查看
json格式示例
// json 内部存储的是键对值,是key,value形式的
{
"name":"张三",
"age":18,
"身高":1.81,
"生日":"2002-01-02",
"学校":"XXXX",
"专业":["厨师","挖掘机"],
"单身":true,
"地址":null,
"好友":{...}
}
makefile
cc=g++
LD=-DMYSELF # 定义了MYSELF ,就使用自己写的序列化和反序列化,如果没有定义那么就用系统接口写的序列化和反序列化
.PHONY:all
all:calserver calclient
calclient:calClient.cc
$(cc) -o $@ $^ -std=c++11 -ljsoncpp #${LD}
calserver:calServer.cc
$(cc) -o $@ $^ -std=c++11 -ljsoncpp #${LD}
.PHONY:clean
clean:
rm -f calclient calserver
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 " "
// 不敢使用sizeof(),因为其包含\0这个字符,而strlen()不会
#define SEP_LEN strlen(SEP)
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()
enum
{
OK = 0,
DIV_ZERO,
MOD_ZERO,
OP_ERROR
};
// 将报头和报文的正文内容进行拼接,中间用\r\n进行分割
// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
std::string enLength(const std::string &text)
{
// 报头中存放的就是报文正文内容所占字节的大小
// 需要将其转化为字符串,才可以与正文内容进行拼接
std::string send_string = std::to_string(text.size());
// 将报头和报文的正文部分进行拼接,它们之前只用分隔符进行分割
send_string += LINE_SEP;
send_string += text;
send_string += LINE_SEP;
// 将其返回
return send_string;
}
// 去掉完整报文的报头,通过输出参数std::string *text
// 返回报文的正文部分
// "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{
// "content_len"\r\n"exitcode result"\r\n
// package.find(LINE_SEP)拿到报头右侧的分隔符的位置
auto pos = package.find(LINE_SEP);
if (pos == std::string::npos)
return false;
// package.substr(0, pos)拿到报头字符串
std::string text_len_string = package.substr(0, pos);
// 将报头字符串转化为数字,其对应的就是报文正文的长度,即text_len
int text_len = std::stoi(text_len_string);
// package.substr(pos + LINE_SEP_LEN, text_len)拿到报文正文的内容
*text = package.substr(pos + LINE_SEP_LEN, text_len);
// 去掉报头成功,则返回真
return true;
}
// 当服务器或者客户端收到请求消息时,需要将网络中的消息先进行反序列化
// 再进行处理
class Request
{
public:
// 无参构造
Request() : x(0), y(0), op(0)
{
}
Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_)
{
}
// 序列化
// 1. 自己写序列化
// 2. 用现成的系统提供的序列化或者反序列化的接口
bool serialize(std::string *out)
{
#ifdef MYSELF
// 将*out进行清空
*out = "";
// 结构化 -> "x op y";
// x是数字,使用接口to_string()将数字转化为字符串
// op本身就是char类型,因此不需要进行转化
std::string x_string = std::to_string(x);
std::string y_string = std::to_string(y);
// 将所有的字符串进行拼接,且字符之间使用SEP(空格)进行分割
*out = x_string;
*out += SEP;
*out += op;
*out += SEP;
*out += y_string;
#else
// 定义一个json对象
Json::Value root;
// x是数字,将x与first配对,存储到json中
// 那么josn会将x转化为字符串
root["first"] = x;
root["second"] = y;
root["oper"] = op;
// 两种方法都可以将root中的数据进行序列化
Json::FastWriter writer;
// Json::StyledWriter writer;
// 将root传入,writer.write(root)会自动将root中的内容进行序列化
// writer.write(root)返回的是一个string类型的对象
*out = writer.write(root);
#endif
return true;
}
// 反序列化
// "x op y";
// const std::string &in, in里面存放的去掉报头的报文正文
// 将其进行反序列化
bool deserialize(const std::string &in)
{
#ifdef MYSELF
// "x op y" -> 结构化
// in.find(SEP),找到的是左操作数x右边的分隔符
auto left = in.find(SEP);
// in.rfind(SEP),找到的是右操作数y左边的分隔符
auto right = in.rfind(SEP);
if (left == std::string::npos || right == std::string::npos)
return false;
if (left == right)
return false;
// SEP_LEN 是分隔符SEP的长度
if (right - (left + SEP_LEN) != 1)
return false;
// in.substr(0, left),分割的字符串是左闭右开的区间
// [0, 2) [start, end) , start(起始位置),
// end - start(从起始位置分割end - start个字符串)
std::string x_string = in.substr(0, left); // 分割出来的做操作数
std::string y_string = in.substr(right + SEP_LEN); // 分割出来的右操作数
// 保证x_string不为空串
if (x_string.empty())
return false;
if (y_string.empty())
return false;
// 将左操作数和右操作数,从字符串转化为数字
x = std::stoi(x_string);
y = std::stoi(y_string);
// 操作符op的下标为left + SEP_LEN
op = in[left + SEP_LEN];
#else
Json::Value root;
Json::Reader reader;
// 将in这个流中读取的数据放入到root中
reader.parse(in, root);
// 根据键对值,拿到相应的操作数和操作符
// asInt()的返回值是一个数字
x = root["first"].asInt();
y = root["second"].asInt();
// 整数赋值给char类型的op,会自动转化为一个字符
op = root["oper"].asInt();
#endif
return true;
}
public:
// "x op y"
int x;
int y;
char op;
};
// 当服务器或者客户端处理完收到的请求的数据之后,
// 需要将处理后的结果构建成可以发送给网络的响应
class Response
{
public:
Response() : exitcode(0), result(0)
{
}
Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_)
{
}
// 序列化
bool serialize(std::string *out)
{
#ifdef MYSELF
*out = "";
// 将退出码,和处理结果转化为字符串
std::string ec_string = std::to_string(exitcode);
std::string res_string = std::to_string(result);
// 将退出码,和处理结果转化的字符串进行拼接
*out = ec_string;
*out += SEP;
*out += res_string;
#else
Json::Value root;
root["exitcode"] = exitcode;
root["result"] = result;
Json::FastWriter writer;
*out = writer.write(root);
#endif
return true;
}
// 反序列化
bool deserialize(const std::string &in)
{
#ifdef MYSELF
// "exitcode result"
auto mid = in.find(SEP);
if (mid == std::string::npos)
return false;
std::string ec_string = in.substr(0, mid);
std::string res_string = in.substr(mid + SEP_LEN);
// 保证退出码和处理结果不是空字符串
if (ec_string.empty() || res_string.empty())
return false;
// 将退出码和处理结果从字符串转化为数字
exitcode = std::stoi(ec_string);
result = std::stoi(res_string);
#else
Json::Value root;
Json::Reader reader;
reader.parse(in, root);
exitcode = root["exitcode"].asInt();
result = root["result"].asInt();
#endif
// 反序列化成功,则返回真
return true;
}
public:
// 0:计算成功,!0表示计算失败,具体是多少,定好标准
int exitcode;
int result; // 计算结果
};
// 从套接字对应的文件描述符sock中进行读取
// 如下是多个报文的字节流
// "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op
bool recvPackage(int sock, std::string &inbuffer, std::string *text)
{
// 定义应用层的缓冲区,来存放报文
char buffer[1024];
while (true)
{
// 从sock读取sizeof(buffer) - 1个字节并将其存放到buffer中
// 经过多次循环,就可以将sock中的内容全部读取
// 并返回读取内容的字节个数
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
// 给字符串的末尾添加结束符,即\0
buffer[n] = 0;
// 将读取的所有数据放入到string类型的inbuffer中
inbuffer += buffer;
// 分析处理
// "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n
// inbuffer.find(LINE_SEP)就是报头"content_len"右侧的分隔符的位置
auto pos = inbuffer.find(LINE_SEP);
if (pos == std::string::npos)
continue; // 如果没有找到,那么就返回继续读取,再重新找
// inbuffer.substr(0, pos),获取到报头
// 报头存放就是报文正文内容所占的字节数,即text_len_string
std::string text_len_string = inbuffer.substr(0, pos);
// 将报文正文内容所占的字节数,由字符串转为数字
int text_len = std::stoi(text_len_string);
// total_len 报文的总长度
// text_len_string.size()是报头的长度
// 2 * LINE_SEP_LEN + text_len 报文正文和两个分割符长度的总和
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
// text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size();
std::cout << "处理前#inbuffer: \n"
<< inbuffer << std::endl;
// inbuffer.size()小于total_len,说明inbuffer没有一个完整的报文
// 那么就返回,继续从sock读取,直到inbuffer.size() > total_len
if (inbuffer.size() < total_len)
{
std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;
continue;
}
// 至少有一个完整的报文
// 分割出一个完整报文
*text = inbuffer.substr(0, total_len);
// 从inbuffer删除掉分割出的报文
inbuffer.erase(0, total_len);
std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;
break;
}
else
// 如果n小于等于0说明对方关闭了sock文件描述符对应的文件,因此返回错误
return false;
}
return true;
}
演示结果
HTTP协议
认识URL
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
http协议格式
通过代码来验证(基本框架)
什么是web
makefile
cc=g++
httpserver:HttpServer.cc
$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f httpserver
Protocol.hpp
#pragma once
#include <iostream>
#include <string>
class HttpRequest
{
public:
HttpRequest() {}
~HttpRequest() {}
public:
std::string inbuffer;
};
class HttpResponse
{
public:
std::string outbuffer;
};
HttpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "Protocol.hpp"
using namespace std;
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 默认缺省的端口号为8080
static const uint16_t gport = 8080;
static const int gbacklog = 5;
using func_t = std::function<bool(const HttpRequest &, HttpResponse &)>;
class HttpServer
{
public:
// 服务器的构造函数
HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port)
{
}
// 初始化服务器
void initServer()
{
// 1. 创建socket文件套接字对象
// tcp也是网络连接,因此第一个参数为AF_INET
// tcp是面向字节流的,因此第二个参数为SOCK_STREAM
// 确定好前两个参数,第三个参数默认为0就可以
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 套接字的文件描述符小于0,那么创建套接字失败
// 则我们通过日志,打印对应的错误信息
exit(SOCKET_ERR);
}
// 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;
// bind小于0,代表绑定失败
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
exit(BIND_ERR);
}
// tcp是建立连接的协议,因此客户端和服务器是要建立连接的
// 所谓的连接,也就是服务器实时知道客户端向其发出了请求
// tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
// 然后才可以向服务器发送消息
// 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
// 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
// 等待客户的电话,这样才可以实时与客户建立连接
// 3. 设置socket 为监听状态
// 参数_listensock,就是要将这个参数设置为监听状态
// gbacklog,后续再说其用处,目前暂时先设置为5
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
// // 通过日志,打印设置监听状态失败的错误信息
exit(LISTEN_ERR);
}
}
void HandlerHttp(int sock)
{
// 1. 读到完整的http请求
// 2. 反序列化
// 3. httprequst, httpresponse, _func(req, resp)
// 4. resp序列化
// 5. send
char buffer[4096];
// http的请求协议
HttpRequest req;
// http的响应协议
HttpResponse resp;
// 大概率我们直接就能读取到完整的http请求
size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer;
// req.parse();
_func(req, resp); // req -> resp
send(sock, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
}
}
// 开始启动服务器
void start()
{
// 4. server 获取新链接
// _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
// _listensock只负责监听用户请求,新的套接字sock只负责通信
// sock, 和client进行通信的fd(文件描述符)
for (;;)
{
// 参数
// struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
// socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
continue;
}
// version 2 多进程版(2)
pid_t id = fork();
if (id == 0) // child
{
close(_listensock);
if (fork() > 0)
exit(0);
HandlerHttp(sock);
close(sock);
exit(0);
}
close(sock);
// father
pid_t ret = waitpid(id, nullptr, 0);
}
}
~HttpServer()
{
}
private:
// 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
int _listensock;
uint16_t _port;
func_t _func;
};
} // namespace server
HttpServer.cc
#include "HttpServer.hpp"
#include <memory>
using namespace std;
using namespace server;
void Usage(std::string proc)
{
cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}
bool Get(const HttpRequest &req, HttpResponse &resp)
{
// for test
cout << "----------------------http start---------------------------" << endl;
cout << req.inbuffer << std::endl;
cout << "----------------------http end---------------------------" << endl;
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->initServer();
httpsvr->start();
return 0;
}
演示结果
-
将浏览器的网页作为客户端,Linux服务器作为服务端
-
浏览器将是如下场景(因为此时浏览器并没有向服务器请求任何的文件资源)
- 服务器是如下的场景
- 解释
验证代码2
telnet命令
在Linux中,可以使用telnet命令来演示telnet的功能。首先,确保你的Linux系统已经安装了telnet客户端。
打开终端(Terminal)。
输入以下命令来安装telnet客户端(如果尚未安装):
sudo yum install telnet
或者使用适合你的Linux发行版的包管理器来安装telnet。
输入以下命令来连接到远程主机:
telnet <远程主机IP地址> <端口号>
替换
<远程主机IP地址>
和<端口号>
为实际的远程主机IP地址和端口号。例如,要连接到IP地址为192.168.0.100的远程主机的23号端口,可以输入:telnet 192.168.0.100 23
如果连接成功,你将看到一个类似于以下的提示符:
Trying <远程主机IP地址>... Connected to <远程主机IP地址>. Escape character is '^]'.
可以按下Ctrl + ],然后输入http协议响应首行(【版本】【状态码】【状态码描述】)
请注意,telnet协议是明文传输数据的,因此不建议在不受信任的网络中使用telnet。在安全性要求较高的环境中,应使用更安全的协议,如SSH。
HttpServer.cc
#include "HttpServer.hpp"
#include <memory>
using namespace std;
using namespace server;
void Usage(std::string proc)
{
cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}
bool Get(const HttpRequest &req, HttpResponse &resp)
{
// for test
cout << "----------------------http start---------------------------" << endl;
cout << req.inbuffer << std::endl;
cout << "----------------------http end---------------------------" << endl;
// 响应首行,分别对应http版本, 状态码, 状态码描述
std::string respline = "HTTP/1.1 200 OK\r\n";
// 分割行
std::string respblank = "\r\n";
// "<html></html>" 代表开始和结尾
// "<head></head>" 代表标题的开始和结尾
// "<body></body>" 代表正文内容的开始和结尾
// "<title></title>" 代表标签,就是浏览器上方显示的这个网页的标签
// std::string body = "<html><head><title></head><body></body></html>"
// \"en\" : 使用 \ 将 " 进行转义
std::string body = "<html lang=\"en\"><head><title>for test</title><h1>hello word</h1></head><body><p>I dedicate my whole life to loving you</body></html>";
resp.outbuffer += respline;
resp.outbuffer += respblank;
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->initServer();
httpsvr->start();
return 0;
}
演示结果
演示结果
验证代码3
stringstream的用法
-
服务器和网页分离,html
-
url -> / : web根目录
创建目录wwwroot(客户端默认目录)
makefile
cc=g++
httpserver:HttpServer.cc
$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f httpserver
Util.hpp
#pragma once
#include <iostream>
#include <string>
class Util
{
public:
// XXXX XXX XXX\r\nYYYYY -->首行对应:XXXX XXX XXX
// 请求行的首行: 【方法】【url】【版本】\r\n
static std::string getOneLine(std::string &buffer, const std::string &sep)
{
// 查找首行的第一个分隔符(这个分割符时\r\n,定义的宏是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;
}
};
Protocol.hpp
#pragma once
#include <iostream>
#include <string>
#include <sstream>
#include "Util.hpp"
const std::string sep = "\r\n";
// 默认目录,就是web客户端的根目录
const std::string default_root = "./wwwroot";
// 当客户端没有请求资源时,那么就会将默认路径下主页资源(index.html)返回给客户端
// 默认路径下的主页资源就是我们平时打开浏览器的首页,我们刚打开浏览器时,还没有请求任何资源
const std::string home_page = "index.html";
class HttpRequest
{
public:
HttpRequest() {}
~HttpRequest() {}
void parse()
{
// 1. 从inbuffer中拿到第一行,分隔符\r\n
std::string line = Util::getOneLine(inbuffer, sep);
if (line.empty())
return;
// 2. 从请求行中提取三个字段
// std::cout << "line: " << line << std::endl;
std::stringstream ss(line);
// stringstream 默认是以空格来分割字符串的
// method url httpversion 对应【方法】【url】【版本】
ss >> method >> url >> httpversion;
// 3. 添加web默认路径
path = default_root; // 默认路径为:./wwwroot
// 当url为/a/b/c.html,那么就是请求./wwwroot/a/b/c.html路径下的文件资源
// 当客户端没有请求资源时,url就为 / 那么path即使./wwwroot/,
// 当path为./wwwroot/时,我们就将默认路径下的主页资源返回给客户端
path += url;
if (path[path.size() - 1] == '/')
path += home_page;
}
public:
std::string inbuffer;
std::string method;
std::string url;
std::string httpversion;
std::string path;
};
class HttpResponse
{
public:
std::string outbuffer;
};
HttpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "Protocol.hpp"
#include "Util.hpp"
using namespace std;
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 默认缺省的端口号为8080
static const uint16_t gport = 8080;
static const int gbacklog = 5;
using func_t = std::function<bool(const HttpRequest &, HttpResponse &)>;
class HttpServer
{
public:
// 服务器的构造函数
HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port)
{
}
// 初始化服务器
void initServer()
{
// 1. 创建socket文件套接字对象
// tcp也是网络连接,因此第一个参数为AF_INET
// tcp是面向字节流的,因此第二个参数为SOCK_STREAM
// 确定好前两个参数,第三个参数默认为0就可以
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 套接字的文件描述符小于0,那么创建套接字失败
// 则我们通过日志,打印对应的错误信息
exit(SOCKET_ERR);
}
// 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;
// bind小于0,代表绑定失败
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
exit(BIND_ERR);
}
// tcp是建立连接的协议,因此客户端和服务器是要建立连接的
// 所谓的连接,也就是服务器实时知道客户端向其发出了请求
// tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
// 然后才可以向服务器发送消息
// 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
// 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
// 等待客户的电话,这样才可以实时与客户建立连接
// 3. 设置socket 为监听状态
// 参数_listensock,就是要将这个参数设置为监听状态
// gbacklog,后续再说其用处,目前暂时先设置为5
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
// // 通过日志,打印设置监听状态失败的错误信息
exit(LISTEN_ERR);
}
}
void HandlerHttp(int sock)
{
// 1. 读到完整的http请求
// 2. 反序列化
// 3. httprequst, httpresponse, _func(req, resp)
// 4. resp序列化
// 5. send
char buffer[4096];
// 定义http的请求协议
HttpRequest req;
// 定义http的响应协议
HttpResponse resp;
// 大概率我们直接就能读取到完整的http请求
size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer;
req.parse();
_func(req, resp); // req -> resp
send(sock, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
}
}
// 开始启动服务器
void start()
{
// 4. server 获取新链接
// _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
// _listensock只负责监听用户请求,新的套接字sock只负责通信
// sock, 和client进行通信的fd(文件描述符)
for (;;)
{
// 参数
// struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
// socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
continue;
}
// version 2 多进程版(2)
pid_t id = fork();
if (id == 0) // child
{
close(_listensock);
if (fork() > 0)
exit(0);
HandlerHttp(sock);
close(sock);
exit(0);
}
close(sock);
// father
pid_t ret = waitpid(id, nullptr, 0);
}
}
~HttpServer()
{
}
private:
// 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
int _listensock;
uint16_t _port;
func_t _func;
};
} // namespace server
HttpServer.cc
#include "HttpServer.hpp"
#include <memory>
using namespace std;
using namespace server;
void Usage(std::string proc)
{
cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}
bool Get(const HttpRequest &req, HttpResponse &resp)
{
// for test
cout << "----------------------http start---------------------------" << endl;
cout << req.inbuffer << std::endl;
std::cout << "method: " << req.method << std::endl;
std::cout << "url: " << req.url << std::endl;
std::cout << "httpversion: " << req.httpversion << std::endl;
std::cout << "path: " << req.path << std::endl;
cout << "----------------------http end---------------------------" << endl;
// 响应首行,分别对应http版本, 状态码, 状态码描述
std::string respline = "HTTP/1.1 200 OK\r\n";
// 代表我的正文属于什么资源
std::string respheader = "Content-Type: text/html\r\n";
// 分割行
std::string respblank = "\r\n";
// "<html></html>" 代表开始和结尾
// "<head></head>" 代表标题的开始和结尾
// "<body></body>" 代表正文内容的开始和结尾
// "<title></title>" 代表标签,就是浏览器上方显示的这个网页的标签
// <meta charset="UTF-8"> 是编码方式
// std::string body = "<html><head><title></head><body></body></html>"
// \"en\" : 使用 \ 将 " 进行转义
std::string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello word</h1></head><body><p>you are right</body></html>";
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
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->initServer();
httpsvr->start();
return 0;
}
演示结果
验证代码4
ifstream的使用方法
ifstream的构造方法
stat()
// 头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
// 函数
int stat(const char *path, struct stat *buf);
// 参数
const char *path:文件路径
struct stat *buf:结构体(包含文件属性),其中包含所读取文件的大小,即off_t st_size;
struct stat
{
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
RETURN VALUE
On success,zero is returned.On error, -1 is returned, and errno is set appropriately.
创建wwwroot目录
test/a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>beautiful</title>
</head>
<body>
<h1>a网页</h1>
<img src="https://qwy.oss-cn-chengdu.aliyuncs.com/image_for_typora/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20230821185354.jpg" alt="天使">
<img src="https://qwy.oss-cn-chengdu.aliyuncs.com/image_for_typora/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20230821185356.jpg" alt="天使">
<a href="/"> 返回 </a>
</body>
</html>
test/b.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>elegant</title>
</head>
<body>
<h1>b网页</h1>
<img src="https://qwy.oss-cn-chengdu.aliyuncs.com/image_for_typora/image-20230924222102670.png" alt="天使">
<a href="/"> 返回 </a>
</body>
</html>
wwwroot/404.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>资源不存在</title>
</head>
<body>
<h1>你所访问的资源并不存在,404!</h1>
</body>
</html>
wwwroot/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XXX专属</title>
</head>
<body>
<h1>hello world</h1>
<!-- 这是html文件的注释符 -->
<!--<a href="/test/a.html">新闻</a>
表示跳转到当前目录下/test/a.html这个文件 -->
<a href="/test/a.html">a网页</a>
<a href="/test/b.html">b网页</a>
<!-- 加载图片成功,显示图片,加载图片失败则返回alt存储的字符
图片的位置时在当前目录下的image中-->
<!-- 这张图片由于图片过大,服务器配置太低,所以未能传输到客户端-->
<!-- <img src="/image/1.jpg" alt="无人机"> -->
<!-- 登录框 -->
<!-- "/a/b/c.py" 代表未来将会把登录框的数据将传递给这个文件 -->
<!-- "POST" 代表提取登录框数据的方法 -->
<form action="/a/b/c.py" method="POST">
姓名:<br>
<input type="text" name="xname">
<br>
密码:<br>
<input type="password" name="ypwd">
<br><br>
<input type="submit" value="登陆">
</form>
<!-- <br> 就是回车换行 -->
<!-- submit n.按钮 -->
<!--
<form action="/a/b/c.py" method="GET">
姓名:<br>
<input type="text" name="xname">
<br>
密码:<br>
<input type="password" name="ypwd">
<br><br>
<input type="submit" value="登陆">
</form>
-->
</body>
</html>
makefile
cc=g++
httpserver:HttpServer.cc
$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f httpserver
Util.hpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <fcntl.h>
class Util
{
public:
// XXXX XXX XXX\r\nYYYYY -->首行对应:XXXX XXX XXX
// 请求行的首行: 【方法】【url】【版本】\r\n
static std::string getOneLine(std::string &buffer, const std::string &sep)
{
// 查找首行的第一个分隔符(这个分割符时\r\n,定义的宏是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 resource, char *buffer, int size)
{
std::ifstream in(resource, std::ios::binary);
if(!in.is_open()) return false; // resource not found
// 需要用二进制的方式读取
in.read(buffer, size);
// getline(in, line)只能读取字符串
// std::string line;
// while(std::getline(in, line))
// {
// *out += line;
// }
in.close();
return true;
}
};
HttpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "Protocol.hpp"
#include "Util.hpp"
using namespace std;
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 默认缺省的端口号为8080
static const uint16_t gport = 8080;
static const int gbacklog = 5;
using func_t = std::function<bool(const HttpRequest &, HttpResponse &)>;
class HttpServer
{
public:
// 服务器的构造函数
HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port)
{
}
// 初始化服务器
void initServer()
{
// 1. 创建socket文件套接字对象
// tcp也是网络连接,因此第一个参数为AF_INET
// tcp是面向字节流的,因此第二个参数为SOCK_STREAM
// 确定好前两个参数,第三个参数默认为0就可以
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 套接字的文件描述符小于0,那么创建套接字失败
// 则我们通过日志,打印对应的错误信息
exit(SOCKET_ERR);
}
// 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;
// bind小于0,代表绑定失败
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
exit(BIND_ERR);
}
// tcp是建立连接的协议,因此客户端和服务器是要建立连接的
// 所谓的连接,也就是服务器实时知道客户端向其发出了请求
// tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
// 然后才可以向服务器发送消息
// 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
// 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
// 等待客户的电话,这样才可以实时与客户建立连接
// 3. 设置socket 为监听状态
// 参数_listensock,就是要将这个参数设置为监听状态
// gbacklog,后续再说其用处,目前暂时先设置为5
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
// // 通过日志,打印设置监听状态失败的错误信息
exit(LISTEN_ERR);
}
}
void HandlerHttp(int sock)
{
// 1. 读到完整的http请求
// 2. 反序列化
// 3. httprequst, httpresponse, _func(req, resp)
// 4. resp序列化
// 5. send
char buffer[4096];
// 定义http的请求协议
HttpRequest req;
// 定义http的响应协议
HttpResponse resp;
// 大概率我们直接就能读取到完整的http请求
size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer;
req.parse();
_func(req, resp); // req -> resp
send(sock, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
}
}
// 开始启动服务器
void start()
{
// 4. server 获取新链接
// _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
// _listensock只负责监听用户请求,新的套接字sock只负责通信
// sock, 和client进行通信的fd(文件描述符)
for (;;)
{
// 参数
// struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
// socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
continue;
}
// version 2 多进程版(2)
pid_t id = fork();
if (id == 0) // child
{
close(_listensock);
if (fork() > 0)
exit(0);
HandlerHttp(sock);
close(sock);
exit(0);
}
close(sock);
// father
pid_t ret = waitpid(id, nullptr, 0);
}
}
~HttpServer()
{}
private:
// 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
int _listensock;
uint16_t _port;
func_t _func;
};
} // namespace server
HttpServer.cc
#include "HttpServer.hpp"
#include <memory>
using namespace std;
using namespace server;
void Usage(std::string proc)
{
cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}
std::string suffixToDesc(const std::string suffix)
{
std::string ct = "Content-Type: ";
if (suffix == ".html")
ct += "text/html";
else if (suffix == ".jpg")
ct += "application/x-jpg";
// "Content-Length: " 后也需要有"\r\n",此处不可以省略
// 否则,服务器无法解析
ct += "\r\n";
return ct;
}
bool Get(const HttpRequest &req, HttpResponse &resp)
{
// for test
cout << "----------------------http start---------------------------" << endl;
cout << req.inbuffer << std::endl;
std::cout << "method: " << req.method << std::endl;
std::cout << "url: " << req.url << std::endl;
std::cout << "httpversion: " << req.httpversion << std::endl;
std::cout << "path: " << req.path << std::endl;
std::cout << "suffix: " << req.suffix << std::endl;
std::cout << "size: " << req.size << "字节" << std::endl;
cout << "----------------------http end---------------------------" << endl;
// 版本1
// 响应首行,分别对应http版本, 状态码, 状态码描述
// std::string respline = "HTTP/1.1 200 OK\r\n";
// location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问
// 版本2(location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问)
std::string respline = "HTTP/1.1 307 Temporary Redirect\r\n";
// 传输资源的类型
std::string respheader = suffixToDesc(req.suffix);
// 如果size大于0,说明正文是存在内容的
if (req.size > 0)
{
// Content-Length,正文的长度
respheader += "Content-Length: ";
respheader += std::to_string(req.size);
// "Content-Length: " 后也需要有"\r\n",此处不可以省略
// 否则,服务器无法解析
respheader += "\r\n";
}
// 搭配版本2的respline使用
// 都需要\r\n进行分割,以便客户端识别
respheader += "location: https://www.snut.edu.cn/\r\n ";
// 分割行(此处仅仅只是定义了,并没有将其合并给报头)
std::string respblank = "\r\n";
std::string body;
// req.size+1,c语言字符串有结束符\0
body.resize(req.size+1);
if (!Util::readFile(req.path, (char*)body.c_str(), req.size))
{
// 如果读取失败,那么就给客户端返回404错误码
// 这里假设读取html_404一定是可以成功的
Util::readFile(html_404,(char*)body.c_str(), req.size);
}
// 响应的首行内容
resp.outbuffer += respline;
// 响应的报头
resp.outbuffer += respheader;
// 观察服务器给客户端响应的内容(由于图片是二进制,所以就不查看了)
cout << "----------------------http response start---------------------------" << endl;
std::cout << resp.outbuffer << std::endl;
cout << "----------------------http response end---------------------------" << endl;
// 分割行,分割报头和正文内容
resp.outbuffer += respblank;
// 响应的正文内容
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->initServer();
httpsvr->start();
return 0;
}
GET方法与POST方法的区别
------http start---------------------------" << endl;
cout << req.inbuffer << std::endl;
std::cout << "method: " << req.method << std::endl;
std::cout << "url: " << req.url << std::endl;
std::cout << "httpversion: " << req.httpversion << std::endl;
std::cout << "path: " << req.path << std::endl;
std::cout << "suffix: " << req.suffix << std::endl;
std::cout << "size: " << req.size << “字节” << std::endl;
cout << “----------------------http end---------------------------” << endl;
// 版本1
// 响应首行,分别对应http版本, 状态码, 状态码描述
// std::string respline = "HTTP/1.1 200 OK\r\n";
// location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问
// 版本2(location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问)
std::string respline = "HTTP/1.1 307 Temporary Redirect\r\n";
// 传输资源的类型
std::string respheader = suffixToDesc(req.suffix);
// 如果size大于0,说明正文是存在内容的
if (req.size > 0)
{
// Content-Length,正文的长度
respheader += "Content-Length: ";
respheader += std::to_string(req.size);
// "Content-Length: " 后也需要有"\r\n",此处不可以省略
// 否则,服务器无法解析
respheader += "\r\n";
}
// 搭配版本2的respline使用
// 都需要\r\n进行分割,以便客户端识别
respheader += "location: https://www.snut.edu.cn/\r\n ";
// 分割行(此处仅仅只是定义了,并没有将其合并给报头)
std::string respblank = "\r\n";
std::string body;
// req.size+1,c语言字符串有结束符\0
body.resize(req.size+1);
if (!Util::readFile(req.path, (char*)body.c_str(), req.size))
{
// 如果读取失败,那么就给客户端返回404错误码
// 这里假设读取html_404一定是可以成功的
Util::readFile(html_404,(char*)body.c_str(), req.size);
}
// 响应的首行内容
resp.outbuffer += respline;
// 响应的报头
resp.outbuffer += respheader;
// 观察服务器给客户端响应的内容(由于图片是二进制,所以就不查看了)
cout << "----------------------http response start---------------------------" << endl;
std::cout << resp.outbuffer << std::endl;
cout << "----------------------http response end---------------------------" << endl;
// 分割行,分割报头和正文内容
resp.outbuffer += respblank;
// 响应的正文内容
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 httpsvr(new HttpServer(Get, port));
httpsvr->initServer();
httpsvr->start();
return 0;
}
## GET方法与POST方法的区别
[外链图片转存中...(img-blAqSqt1-1743662363073)]