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,会统计\0
class 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 MYSELF
std::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;
#else
std::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 MYSELF
std::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;
#else
std::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;
else
resp._result = req._x / req._y;
break;
case '%':
if (0 == req._y)
resp._code = 2;
else
resp._result = req._x % req._y;
break;
default:
resp._code = 3;
break;
}
return resp;
}
void 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, SIGCHID
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
//2.不要让自己成为组长
if(fork() > 0)
{
exit(0);//父进程退出,剩下子进程其实是孤儿进程
}
//3.调用setsid
setsid();
//4.标准输入,标准输出,标准错误的重定向,守护进程不能直接向显示器打印消息
int devnull = open("/dev/null", O_RDONLY | O_WRONLY);
if(devnull > 0)
{
dup2(0, devnull);
dup2(1, devnull);
dup2(2, devnull);
close(devnull);
}
}
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;
else
resp._result = req._x / req._y;
break;
case '%':
if (0 == req._y)
resp._code = 2;
else
resp._result = req._x % req._y;
break;
default:
resp._code = 3;
break;
}
return resp;
}
void 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 CalServer
CalClient:CalClient.cc
g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
CalServer:CalServer.cc
g++ -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,会统计\0
class 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 MYSELF
std::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;
#else
Json::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类型实质也是int
return 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 MYSELF
std::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;
#else
Json::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 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./http.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
if(level== DEBUG) return;
#endif
// va_list ap;
// va_start(ap, format);
// while()
// int x = va_arg(ap, int);
// va_end(ap); //ap=nullptr
char 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\n
static 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 test
char 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 test
char 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";
}
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;
}
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请求后,就将连接关掉,每次都要建立连接获取图片等资源;