Linux知识点 – 网络基础(二)-- 应用层
文章目录
- Linux知识点 -- 网络基础(二)-- 应用层
- 一、使用协议来实现一个网络版的计算器
- 1.自定义协议
- 2.守护进程
- 3.使用json来完成序列化
- 二、HTTP协议
- 1.概念
- 2.HTTP协议请求和响应的报文格式
- 3.使用HTTP协议进行网络通信
- 4.HTTP协议的方法
- 5.HTTP协议的状态码
- 6.HTTP协议的报头
- 7.connetion选项
一、使用协议来实现一个网络版的计算器
1.自定义协议
定义结构体来表示我们需要交互的信息;
发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
这个过程叫做"序列化"和"反序列化”;
Sock.hpp
将套接字封装成对象,其中包含套接字的创建与连接成员函数
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"class Sock
{
private:const static int gbacklog = 20;public:Sock() {}int Socket(){int listensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "create socket success, listensock: %d", listensock);return listensock;}void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0"){struct sockaddr_in local;memset(&local, 0, sizeof local);local.sin_family = AF_INET;local.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &local.sin_addr);if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0){logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));exit(3);}}void Listen(int sock){if (listen(sock, gbacklog) < 0){logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));exit(4);}logMessage(NORMAL, "init server success");}// 一般经验:// const string& 输入型参数// string* 输出型参数// string& 输入输出型参数int Accept(int listensock, std::string *ip, uint16_t *port){struct sockaddr_in src;socklen_t len = sizeof src;int servicesock = accept(listensock, (struct sockaddr *)&src, &len);if (servicesock < 0){logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));return -1;}if(port){*port = ntohs(src.sin_port);}if(ip){*ip = inet_ntoa(src.sin_addr);return servicesock;}}bool Connect(int sock, const std::string& server_ip, const uint16_t& server_port){struct sockaddr_in server;memset(&server, 0, sizeof server);server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());if(connect(sock, (struct sockaddr*)&server, sizeof server) == 0){return true;}else{return false;}}~Sock() {}
};
TcpServer.hpp
封装TCP服务接口的类;
注意:类内回调函数由于参数有this指针,无法正-常回调,因此需要设置成static成员,再通过参数传进this指针,来访问类内非静态成员;
#pragma once#include "Sock.hpp"
#include <vector>
#include <functional>
#include <pthread.h>namespace ns_tcpserver
{using func_t = std::function<void(int)>;class TcpServer;class ThreadData // 传入回调函数的参数{public:ThreadData(int sock, TcpServer *server): _sock(sock), _server(server){}~ThreadData() {}public:int _sock;TcpServer *_server; // 里面有TcpServer对象的指针,由于回调函数是静态成员函数,无法访问非静态成员// 这里的TcpServer对象指针是用来在回调函数中访问非静态成员的};class TcpServer{private://如果是类内成员函数,参数中是有this指针的,多线程回调会出问题//因此需设置成静态成员,才可以回调static void* ThreadRoutine(void* args){pthread_detach(pthread_self());//线程分离ThreadData* td = static_cast<ThreadData*>(args);//类型转换td->_server->Excute(td->_sock);//通过对象this指针调用成员函数close(td->_sock);return nullptr;}public:TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0"){// 创建套接字,绑定并监听_listensock = _sock.Socket();_sock.Bind(_listensock, port, ip);_sock.Listen(_listensock);}// 将服务请求放入函数队列void BindService(func_t func){_func.push_back(func);}// 执行服务void Excute(int sock){for (auto &f : _func){f(sock);}}void Start(){for (;;){std::string cli_ip;uint16_t cli_port;int sock = _sock.Accept(_listensock, &cli_ip, &cli_port);if (sock == -1){continue;}logMessage(NORMAL, "create new link succsee, sock: %d", sock);// 多线程处理请求pthread_t tid;ThreadData *td = new ThreadData(sock, this);pthread_create(&tid, nullptr, ThreadRoutine, td);}}~TcpServer(){if (_listensock >= 0){close(_listensock);}}private:int _listensock;Sock _sock;std::vector<func_t> _func; // 回调函数列表};}
Protocol.hpp
定制协议:
分别有计算请求的序列化和计算结果的序列化;
- TCP协议的读写接口(read和write)都是将数据拷贝到缓冲区或者从缓冲区拷贝出来,并不是直接发送到对方主机;发送给对方主机是由TCP传输控制协议决定的
- 由于TCP是面向字节流的协议,因此,发送和接受的次数,每次发送多少字符,都不受控制(UDP协议每次发送和接受的都是完整的报文),有可能每次接收到的不一定是完整的报文,也有可能一次读取了多个报文,所以需要自己定制协议解包代码;在读取时不能简单地receive,而需要对读取的数据进行解析;
- 自主定制的协议使用"length\r\nx_ op_ y_\r\n"协议,前面加上数据长度;
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include "Sock.hpp"namespace ns_protocol
{
#define MYSELF 1
#define SPACE " "
#define SPACE_LEN strlen(SPACE)#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是sizeof,会统计\0class Request // 计算请求序列{public:Request() {}Request(int x, int y, char op): _x(x), _y(y), _op(op){}~Request() {}std::string Serialize() // 序列化{
#ifdef MYSELF// 使用自定义序列化方案// 将请求传换成string:_x _op _y的形式std::string str;str = std::to_string(_x);str += SPACE;str += _op;str += SPACE;str += std::to_string(_y);return str;
#else// 使用现成方案std::cout << "to do" << std::endl;
#endif}bool Deserialized(const std::string &str) // 反序列化{
#ifdef MYSELFstd::size_t left = str.find(SPACE);if (left == std::string::npos){return false;}std::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;#elsestd::cout << "to do" << std::endl;
#endif}public:int _x;int _y;char _op; // + - * / %};class Response // 计算结果响应序列{public:Response() {}Response(int result, int code): _result(result), _code(code){}~Response() {}std::string Serialize() // 序列化:_code _result{
#ifdef MYSELF// 使用自定义序列化方案// 将请求传换成string:_x _op _y的形式std::string str;str = std::to_string(_code);str += SPACE;str += std::to_string(_result);return str;
#else// 使用现成方案std::cout << "to do" << std::endl;
#endif}bool Deserialized(const std::string &str) // 反序列化{
#ifdef MYSELFstd::size_t pos = str.find(SPACE);if (pos == std::string::npos){return false;}_code = atoi(str.substr(0, pos).c_str());_code = atoi(str.substr(pos + SPACE_LEN).c_str());return true;#elsestd::cout << "to do" << std::endl;
#endif}public:int _result; // 计算结果int _code; // 计算结果的状态码:运算是否成功};// 临时方案// 期望返回的是一个完整地报文bool Recv(int sock, std::string* out){//TCP是面向字节流的,无法保证独到的inbuffer是一个完整地请求//因此需要解析协议,查看数据是否完整char buffer[1024];ssize_t s = recv(sock, buffer, sizeof buffer - 1, 0);if (s > 0){buffer[s] = 0;*out += buffer;}else if(s == 0){//客户端退出return false;}else{//读取错误return false;}return true;}void Send(int sock, const std::string str){send(sock, str.c_str(), str.size(), 0);}//添加报文// "XXXXXX"// "123\r\nXXXXXX\r\n"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 "";//如果没找到分隔符,返回空串}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 "";//没有完整地报文,继续接收}}
}
CalServer.cc
计算服务
- 服务器运行时,对端如果直接关闭,我们收到的就是空的信息,send的也是已经关闭的文件描述符,就可能导致服务器关闭;
方案一:对SIGPIPE信号忽略,这样即使正在发送信息时对方关闭,也可以保证服务器不退出;
方案二:接收到信息时,需要判断信息的完整性,读取是否成功 - 一般经验:在server编写的时候,要有较为严谨的判断逻辑;
一般服务器都是要忽略SIGPIPE信号的,防止在运行过程中出现非法写入的问题;
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>
#include <signal.h>using namespace ns_protocol;
using namespace ns_tcpserver;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);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;elseresp._result = req._x / req._y;break;case '%':if (0 == req._y)resp._code = 2;elseresp._result = req._x % req._y;break;default:resp._code = 3;break;}return resp;
}void calculator(int sock)
{std::string inbuffer;//每次读取到的缓冲区while (true){//1.读取成功bool res = Recv(sock, &inbuffer); // 读到了一个请求if(!res){break;}//2.协议解析,保证得到一个完整的报文std::string package = Decode(inbuffer);if(package.empty()){continue; //如果读到的报文不完整,继续读取}logMessage(NORMAL, "%s", package.c_str());//3.保证该报文是一个完整的报文Request req;//4.反序列化,字节流->结构化req.Deserialized(package); // 反序列化//5.业务逻辑Response resp = calculatorHelper(req);//6.序列化std::string respString = resp.Serialize();//对计算结果进行序列化//7.添加长度信息,形成一个完整的报文respString = Encode(respString);//8.发送Send(sock, respString);//将结果序列发回给客户端}
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}signal(SIGPIPE, SIG_IGN);std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));server->BindService(calculator);server->Start();return 0;
}
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();// std::string temp = s;// 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._result << " [success]" << std::endl;break;}if(!err.empty()) std::cerr << err << std::endl;// sleep(1);break;//完整读取一个报文就退出}}close(sockfd);return 0;
}
运行结果:
2.守护进程
-
(1)前台进程:和终端关联的进程;在终端下能读取输入并作出反应(如bash);
(2)任何xshell登陆,只允许一个前台进程和多个后台进程;
(3)进程除了有自己的pid, ppid, 还有一个组ID;
(4)在命令行中,同时用管道启动多个进程,多个进程是兄弟关系,父进程都是bash ->可以用匿名管道来进行通信;
(5)而同时被创建的多个进程可以成为一个进程组的概念,组长一般是第一个进程;
(6)任何一次登陆,登陆的用户,需要有多个进程(组),来给这个用户提供服务的(bash),用户自己可以启动很多进程,或者进程组。我们把给用户提供服务的进程,或者用户自己启动的所有的进程或者服务,整体都是要属于一个叫做会话的机制中的。
(7)当用户退出登陆的时候,整个会话中的进程组都会结束;
想让一个进程不再属于用户的会话,而是自成一个会话,这个进程称为守护进程;
(8)如何将进程变为守护进程->setsid()接口;
(9)setsid要成功被调用,必须保证当前进程不是进程组的组长,可以通过fork创建的子进程实现;
(10)守护进程不能直接向显示器打印消息,一旦打印,会被暂停,终止; -
如何在Linux正确的写一个让进程守护进程化的代码:
写一个函数,让进程调用这个函数,自动变成守护进程; -
/dev/null文件
可以理解为一个文件黑洞,可以向里面打印数据,也可以从里面读取,但都不会有实际的数据输入输出;
因此可以将标准输入,标准输出,标准错误重定向到devnull文件中;
Daemon.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void MyDaemon()
{//1.忽略信号,SIPPIPE, SIGCHIDsignal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);//2.不要让自己成为组长if(fork() > 0){exit(0);//父进程退出,剩下子进程其实是孤儿进程}//3.调用setsidsetsid();//4.标准输入,标准输出,标准错误的重定向,守护进程不能直接向显示器打印消息int devnull = open("/dev/null", O_RDONLY | O_WRONLY);if(devnull > 0){dup2(0, devnull);dup2(1, devnull);dup2(2, devnull);close(devnull);}
}
CalServer.cc
在服务器进程中调用守护进程函数,让服务器进程成为守护进程;
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>
#include <signal.h>
#include "Daemon.hpp"using namespace ns_protocol;
using namespace ns_tcpserver;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);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;elseresp._result = req._x / req._y;break;case '%':if (0 == req._y)resp._code = 2;elseresp._result = req._x % req._y;break;default:resp._code = 3;break;}return resp;
}void calculator(int sock)
{std::string inbuffer;//每次读取到的缓冲区while (true){//1.读取成功bool res = Recv(sock, &inbuffer); // 读到了一个请求if(!res){break;}//2.协议解析,保证得到一个完整的报文std::string package = Decode(inbuffer);if(package.empty()){continue; //如果读到的报文不完整,继续读取}logMessage(NORMAL, "%s", package.c_str());//3.保证该报文是一个完整的报文Request req;//4.反序列化,字节流->结构化req.Deserialized(package); // 反序列化//5.业务逻辑Response resp = calculatorHelper(req);//6.序列化std::string respString = resp.Serialize();//对计算结果进行序列化//7.添加长度信息,形成一个完整的报文respString = Encode(respString);//8.发送Send(sock, respString);//将结果序列发回给客户端}
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}signal(SIGPIPE, SIG_IGN);MyDaemon();//让该进程成为守护进程,自成一个会话std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));server->BindService(calculator);server->Start();return 0;
}
运行结果:
注:
守护进程实际上是孤儿进程,但是没有被系统领养,而是自成会话;
这样下来,服务器进程成为了守护进程,自成一个会话,即使用户退出登录,该进程也不会退出;
3.使用json来完成序列化
json:网络通信的格式
- 在Linux上安装json:
- json实际上是一个结构化数据格式,里面是很多的kv结构:
- json库的使用:
StyleWriter对象,两个kv对象之间有换行符;
StyleWriter对象的write函数会将root中的kv内容直接转换为对应的string;
运行结果:
FastWriter对象,中间没有换行符
运行结果:
json里面是可以套json的
使用json协议完成序列化和反序列化:
由于使用的是非cpp官方库,因此需要添加编译选项:
makefile
.PHONY:all
all:CalClient CalServerCalClient:CalClient.ccg++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
CalServer:CalServer.ccg++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp.PHONY:clean
clean:rm -f CalClient CalServer
Protocol.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include "Sock.hpp"
#include <jsoncpp/json/json.h>namespace ns_protocol
{
//#define MYSELF 1
#define SPACE " "
#define SPACE_LEN strlen(SPACE)#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是sizeof,会统计\0class Request // 计算请求序列{public:Request() {}Request(int x, int y, char op): _x(x), _y(y), _op(op){}~Request() {}std::string Serialize() // 序列化{
#ifdef MYSELF// 使用自定义序列化方案// 将请求传换成string:_x _op _y的形式std::string str;str = std::to_string(_x);str += SPACE;str += _op;str += SPACE;str += std::to_string(_y);return str;
#else// 使用现成方案Json::Value root;root["x"] = _x;root["y"] = _y;root["op"] = _op;Json::FastWriter writer;return writer.write(root);
#endif}bool Deserialized(const std::string &str) // 反序列化{
#ifdef MYSELFstd::size_t left = str.find(SPACE);if (left == std::string::npos){return false;}std::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;#elseJson::Value root;Json::Reader reader;reader.parse(str, root);//parse函数能够将序列化的json字符串直接读取到Value对象中_x = root["x"].asInt();_x = root["y"].asInt();_x = root["op"].asInt();//char类型实质也是intreturn true;
#endif}public:int _x;int _y;char _op; // + - * / %};class Response // 计算结果响应序列{public:Response() {}Response(int result, int code): _result(result), _code(code){}~Response() {}std::string Serialize() // 序列化:_code _result{
#ifdef MYSELF// 使用自定义序列化方案// 将请求传换成string:_x _op _y的形式std::string str;str = std::to_string(_code);str += SPACE;str += std::to_string(_result);return str;
#else// 使用现成方案Json::Value root;root["code"] = _code;root["result"] = _result;Json::FastWriter writer;return writer.write(root);
#endif}bool Deserialized(const std::string &str) // 反序列化{
#ifdef MYSELFstd::size_t pos = str.find(SPACE);if (pos == std::string::npos){return false;}_code = atoi(str.substr(0, pos).c_str());_result = atoi(str.substr(pos + SPACE_LEN).c_str());return true;#elseJson::Value root;Json::Reader reader;reader.parse(str, root);_code = root["code"].asInt();_result = root["result"].asInt();return true;
#endif}public:int _result; // 计算结果int _code; // 计算结果的状态码:运算是否成功};// 临时方案// 期望返回的是一个完整地报文bool Recv(int sock, std::string* out){//TCP是面向字节流的,无法保证独到的inbuffer是一个完整地请求//因此需要解析协议,查看数据是否完整char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;*out += buffer;}else if(s == 0){//客户端退出return false;}else{//读取错误return false;}return true;}void Send(int sock, const std::string str){int n = send(sock, str.c_str(), str.size(), 0);if(n < 0){std::cout << "send error" << std::endl;}}//添加报头// "XXXXXX"// "123\r\nXXXXXX\r\n"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 "";//如果没找到分隔符,返回空串}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 "";//没有完整地报文,继续接收}}}
运行结果:
二、HTTP协议
1.概念
-
应用层:就是程序员基于socket接口之上编写的具体逻辑,有很多和文本处理相关的工作;http协议一定会有大量的文本分析和处理;
-
URL:我们平时说的网址,其结构如下;
其中,服务器地址IP就是域名,用来标识唯一的主机;冒号后面是端口号,标识特定主机上的特定进程;
端口号后面是带层次的文件路径,其中第一个文件夹叫做web根目录;文件路径标识客户想访问的资源路径;
URL:union resource local统一资源定位符,代表本次访问请求的资源位置,定位互联网中唯一的一份资源;
在用户访问网络资源时,先通过url找到服务器上的特定文件资源,在进行读取或写入; -
如果用户想在url中包含url本身作为特殊字符使用的字符时,浏览器会自动对该字符进行编码,在服务端收到后,需要转回特殊字符;
2.HTTP协议请求和响应的报文格式
单纯在报文角度,http可以是基于行的文本协议;
-
请求报文:
请求行:方法 URL 协议版本
http的方法为:
请求报头Header:多行kv结构,都是属性;
空行:用来区分报头和有效载荷;
请求正文(可以没有); -
响应报文:
状态行:协议版本 状态码 状态码描述;
响应报头;
空行;
响应正文;
3.使用HTTP协议进行网络通信
Log.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"
};#define LOGFILE "./http.log"// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOWif(level== DEBUG) return;
#endif// va_list ap;// va_start(ap, format);// while()// int x = va_arg(ap, int);// va_end(ap); //ap=nullptrchar stdBuffer[1024]; //标准部分time_t timestamp = time(nullptr);// struct tm *localtime = localtime(×tamp);snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);char logBuffer[1024]; //自定义部分va_list args;va_start(args, format);// vprintf(format, args);vsnprintf(logBuffer, sizeof logBuffer, format, args);va_end(args);FILE *fp = fopen(LOGFILE, "a");// printf("%s%s\n", stdBuffer, logBuffer);fprintf(fp, "%s%s\n", stdBuffer, logBuffer);fclose(fp);
}
Sock.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"class Sock
{
private:const static int gbacklog = 20;public:Sock() {}int Socket(){int listensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "create socket success, listensock: %d", listensock);return listensock;}void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0"){struct sockaddr_in local;memset(&local, 0, sizeof local);local.sin_family = AF_INET;local.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &local.sin_addr);if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));exit(3);}}void Listen(int sock){if (listen(sock, gbacklog) < 0){logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));exit(4);}logMessage(NORMAL, "init server success");}// 一般经验// const std::string &: 输入型参数// std::string *: 输出型参数// std::string &: 输入输出型参数int Accept(int listensock, std::string *ip, uint16_t *port){struct sockaddr_in src;socklen_t len = sizeof(src);int servicesock = accept(listensock, (struct sockaddr *)&src, &len);if (servicesock < 0){logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));return -1;}if(port) *port = ntohs(src.sin_port);if(ip) *ip = inet_ntoa(src.sin_addr);return servicesock;}bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;else return false;}~Sock() {}
};
Usage.hpp
#pragma once
#include <iostream>
#include <string>
void Usage(std::string proc)
{std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
Util.hpp
工具类,分割字符串
#pragma once#include <iostream>
#include <vector>class Util
{
public:// aaaa\r\nbbbbb\r\nccc\r\n\r\nstatic void cutString(std::string s, const std::string &sep, std::vector<std::string> *out){std::size_t start = 0;while (start < s.size()){auto pos = s.find(sep, start);if (pos == std::string::npos) break;std::string sub = s.substr(start, pos - start);// std::cout << "----" << sub << std::endl;out->push_back(sub);start += sub.size();start += sep.size();}if(start < s.size()) out->push_back(s.substr(start));}
};
HttpServer.hpp
#pragma once#include <iostream>
#include <signal.h>
#include <functional>
#include "Sock.hpp"class HttpServer
{
public:using func_t = std::function<void(int)>;
private:int listensock_;uint16_t port_;Sock sock;func_t func_;
public:HttpServer(const uint16_t &port, func_t func): port_(port),func_(func){listensock_ = sock.Socket();sock.Bind(listensock_, port_);sock.Listen(listensock_);}void Start(){signal(SIGCHLD, SIG_IGN);for( ; ; ){std::string clientIp;uint16_t clientPort = 0;int sockfd = sock.Accept(listensock_, &clientIp, &clientPort);if(sockfd < 0) continue;if(fork() == 0){close(listensock_);func_(sockfd);close(sockfd);exit(0);}close(sockfd);}}~HttpServer(){if(listensock_ >= 0) close(listensock_);}
};
HttpServer.cc
这里是主要的对http协议进行解析的代码,逐行解析,提取首行url,访问目标资源;
#include <iostream>
#include <memory>
#include <cassert>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "HttpServer.hpp"
#include "Usage.hpp"
#include "Util.hpp"
// 一般http都要有自己的web根目录
#define ROOT "./wwwroot" // ./wwwroot/index.html
// 如果客户端只请求了一个/,我们返回默认首页
#define HOMEPAGE "index.html"
void HandlerHttpRequest(int sockfd)
{// 1. 读取请求 for testchar buffer[10240];ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;// std::cout << buffer << "--------------------\n" << std::endl;}std::vector<std::string> vline; // 取出http请求的每一行Util::cutString(buffer, "\n", &vline);std::vector<std::string> vblock; // 取出第一行的每一个子串Util::cutString(vline[0], " ", &vblock);std::string file = vblock[1]; // 请求的资源std::string target = ROOT;if(file == "/") file = "/index.html";target += file; //请求的资源从web根目录下开始,如果不指定web根目录,就会访问Linux根目录std::cout << target << std::endl;std::string content;std::ifstream in(target); // 打开文件if(in.is_open()){std::string line;while(std::getline(in, line)){content += line;}in.close();}std::string HttpResponse;if(content.empty()) HttpResponse = "HTTP/1.1 404 NotFound\r\n";else HttpResponse = "HTTP/1.1 200 OK\r\n";HttpResponse += "\r\n";HttpResponse += content;// std::cout << "####start################" << std::endl;// for(auto &iter : vblock)// {// std::cout << "---" << iter << "\n" << std::endl;// }// std::cout << "#####end###############" << std::endl;// 2. 试着构建一个http的响应send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));httpserver->Start();return 0;
}
在目录下创建web根目录wwwroot,里面创建首页index.html;
index.html
在vscode下装插件,!table会出现网页模板;
<!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>lmx</title>
</head>
<body><h3>这个一个Linux课程</h3><p>我是一个Linux的学习者,我正在进行http的测试工作!!</p><p>我是一个Linux的学习者,我正在进行http的测试工作!!</p><p>我是一个Linux的学习者,我正在进行http的测试工作!!</p><p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
</body>
</html>
运行结果:
4.HTTP协议的方法
其中最常用的是GET和POST方法;
-
用户数据提交到服务器的流程:
用户发起申请,形成表单,指明提交方法,表单中的数据,会被转成http request的一部分,之后收集用户数据,并把用户数据推送给服务器; -
GET方法可以将数据从服务器端拿到客户端,也可以将客户端的数据提交到服务器;
使用GET方法提交请求
web目录结构:
index.html
使用GET方法将进行提交
**input是按钮,其中的action是点击按钮后访问的文件,method是方法,这里是GET;
下面的Username和Password是kv结构输入框,type是内容,name是标签;
**
运行结果:
使用浏览器访问建立好的网页,这是一个可以登陆的界面;
输入好用户名和密码后,点击登录;
跳转到如上界面,登陆的时候其实就是把用户信息提交给服务器;
在上面的网址栏可以看到自己输入的用户名和密码,?后面是参数,前面是提交的地址,就是将参数提交到目标文件中;
服务器收到的请求:
这是因为get方法通过url传参,并将参数回显到url中; -
POST方法用于将客户端的数据提交到服务器;
使用POST方法提交请求
insex.html
运行结果:
点击登录:
服务器收到的请求:
POST是不会通过URL传参的,它通过正文传参;
总结
- GET方法通过URL传参,回显输入的私密信息,不够私密;
- POST方法通过正文传参,不会回显私密信息,私密性有保证;
- 私密性不是安全性;
- 登录和注册一般常用的是POST方法;
内容较大也建议使用POST方法,因为POST方法里面有正文长度,方便整段读取;
5.HTTP协议的状态码
-
最常见的状态码:
200(OK),404(Not Found), 403(Forbidden), 302(Redirect,重定向),504(Bad Gateway); -
重定向:当网页进行请求时,需要跳转到其他网页;
301:永久移动,直接重定向到另一个网也,不会返回原来的网页,影响用户后续的请求策略;
302:临时移动,临时重定向到另一个网页,比如登陆界面,处理好后再返回原始网页,不影响用户后续的请求策略;
307:临时重定向; -
重定向过程
客户端向服务器发起http请求 -> 服务器返回30X重定向状态码,并携带新的网页地址信息 -> 客户端浏览器拿到新的地址后,自动向新的地址发起请求;
重定向实验
HttpServer.cc
如果读取的文件不存在,返回的状态码为301,会进行重定向操作;
其中Location属性就是重定向后的目标文件地址;
#include <iostream>
#include <memory>
#include <cassert>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "HttpServer.hpp"
#include "Usage.hpp"
#include "Util.hpp"
// 一般http都要有自己的web根目录
#define ROOT "./wwwroot" // ./wwwroot/index.html
// 如果客户端只请求了一个/,我们返回默认首页
#define HOMEPAGE "index.html"
void HandlerHttpRequest(int sockfd)
{// 1. 读取请求 for testchar buffer[10240];ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;std::cout << buffer << "\n--------------------\n"<< std::endl;}std::vector<std::string> vline; // 取出http请求的每一行Util::cutString(buffer, "\n", &vline);std::vector<std::string> vblock; // 取出第一行的每一个子串Util::cutString(vline[0], " ", &vblock);std::string file = vblock[1]; // 请求的资源std::string target = ROOT;if (file == "/")file = "/index.html";target += file; // 请求的资源从web根目录下开始,如果不指定web根目录,就会访问Linux根目录std::cout << target << std::endl;std::string content; // 文件中的内容std::ifstream in(target); // 打开文件if (in.is_open()){std::string line;while (std::getline(in, line)){content += line;}in.close();}std::string HttpResponse;if (content.empty()){HttpResponse = "HTTP/1.1 301 NotFound\r\n";HttpResponse += "Location: http://47.115.213.66:8080/a/b/404.html\r\n";}elseHttpResponse = "HTTP/1.1 200 OK\r\n";HttpResponse += "\r\n";HttpResponse += content;// std::cout << "####start################" << std::endl;// for(auto &iter : vblock)// {// std::cout << "---" << iter << "\n" << std::endl;// }// std::cout << "#####end###############" << std::endl;// 2. 试着构建一个http的响应send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));httpserver->Start();return 0;
}
index.html
客户端点击登陆后,会跳转到/a/b/notexit.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>lmx</title>
</head><body><h3>Hello Guests!</h3><form name="input" action="/a/b/notexit.html" method="POST">Username: <input type="text" name="user"> <br/>Password: <input type="password" name="pwd"> <br/><input type="submit" value="登陆"></form>
</body></html>
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><h2>你访问的页面不存在</h2>
</body>
</html>
运行结果:
客户端访问网页HOME地址:
点击登陆后,访问/a/b/notexit.html这个地址的文件,但是这个文件是不存在的,文件读取返回结果为空,状态码为301,触发重定向;
重定向到了a/b/404.html这个文件;
6.HTTP协议的报头
Content-Type:数据类型(text/html等);
Content-Length:Body(正文)的长度;
Host:客户端告知服务器所请求的资源是在哪个主机的哪个端口上;
User-Agent:声明用户的操作系统和浏览器版本信息;
referer:当前页面是从哪个页面跳转过来的;
location:搭配3xx状态码使用,告诉客户端接下来要去哪里访问;
Cookie:用于在客户端存储少量信息.通常用于实现会话(session)的功能;
- Content-Type、Content-Length
添加内容类型及正文长度报头;
- Cookie会话管理
http的特征:
a.简单快速;
b.无连接,指http不维护连接,连接是由TCP维护的;
c.无状态,http不会记录用户曾经请求的网页,不会对用户的行为做记录;
http协议是无状态的,但是我们平常在浏览器进行访问网页时,一般网站是会记录下我们的状态的,这是因为http协议为了支持常规用户的会话管理,支持两个报头属性Cookie(请求)、Set-Cookie(响应);
用户登录后,曾经输入的用户名和密码等信息会保存为一个文件,在今后每次的http请求中,每次都会携带这个文件中的账户密码内容,这个文件就是cookie文件;
cookie文件的创建与使用流程:
当用户访问网站后,在网站上输入用户密码信息,之后服务器会将用户信息返回给客户端,客户端的浏览器会将用户信息保存,形成cookie文件,之后用户每次访问该网站,都会将cookie文件再次上传到服务器,进行用户星系比对,不用每次都重新输入信息了;
但是cookie文件中是将用户信息明文保存的,如果被黑客注入木马病毒,是能够盗取用户的私密信息;
现在的新cookie方案:在网站认证用户信息后,服务端会形成一个用户唯一ID,session id,并返回给用户端,保存到cookie文件中;这样每次用户访问网站,上传的cookie文件都是用户在网站形成的唯一session id,就算被盗取,也不会暴露用户的私密信息;
验证cookie
7.connetion选项
keep-alive:长连接,网页该有的资源通过一个连接全部拿到;
close:短连接,处理完一个http请求后,就将连接关掉,每次都要建立连接获取图片等资源;