一、应用层通信概念
1.协议
协议本质是收发端双方约定好格式的数据,常见协议是用结构体或者类的方式来表达,结构化的数据是为了方便被应用层解读,这个结构体中可能包括发送者ip和端口号以及主机名,还有通信信息,应用层可以用结构体区分并使用信息;使用结构体直接传递,但是可能会因为系统的不同,对结构体的定义方式不同,造成丢包.
结构体传送是指将结构体作为一个整体进行传输。结构体是一种自定义的数据类型,可以包含多个不同类型的数据成员,这些成员可以是整型、浮点型、字符型等。在结构体传送中,整个结构体的数据被作为一个单元进行传输,保持了结构体内部数据成员之间的关联关系。
字节流传送是指将数据分解为字节序列进行传输。在字节流传送中,数据被拆分为字节,并按照一定的顺序进行传输。这种传输方式不考虑数据之间的关联关系,只关注数据的字节表示。
结构体转字节流叫做序列化,反之为反序列化,一般网络通信应用层都会进行序列化和反序列化操作.
2.信息接收
网络通信其本质是信息拷贝行为,发送就是从本地用户缓冲区拷贝到网络缓冲区,网络缓冲区根据自己的发送策略拷贝到对端网络缓冲区,对端网络缓冲区将信息拷贝到用户缓冲区.
所以网络缓冲区跟用户缓冲区不同步,所以每次从网络缓冲区读取到的完整的数据量可能是不同的.所以每一个完成的信息都要包含报头,用报头来确定往后读取信息载体的字节数.
二、网络计算器实战应用
记录错误代码
error.hpp
#pragma once
#include<iostream>
using namespace std;
enum
{
SOCK_ERR=1,
BIND_ERR,
RECV_ERR,
SENDTO_ERR,
LISTEN_ERR,
READ_ERR,
SETSID_ERR,
OPEN_ERR,
CONNECT_ERR,
};
sock自定义类封装了网络套接字编程的基本接口,用于客户端和服务端网络通信框架的基本搭建.将套接字编程接口的返回值检查,日志打印,基本供调用进行了封装,所有接口基本围绕网络套接字sockfd,所以只有一个成员变量sockfd,方便后续使用.
sock.hpp
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<cstring>
#include<errno.h>
#include"log.hpp"
#include"error.hpp"
#define BACKLOG_DEFAULT 32
namespace sock_space
{
class sock
{
public:
sock():_fd(-1)
{}
//获取套接字
void Socket()
{
_fd=socket(AF_INET,SOCK_STREAM,0);
if(_fd<0)
logMessage(Fatal,"socket error,code:%d,%s",errno,strerror(errno));
}
//绑定
void Bind(const uint16_t& port)
{
struct sockaddr_in client;
memset(&client,0,sizeof(client));
client.sin_addr.s_addr=htons(INADDR_ANY);
client.sin_family=AF_INET;
client.sin_port=htons(port);
if(bind(_fd,(struct sockaddr*)&client,sizeof(client))==-1)
{
logMessage(Fatal,"bind error,code:%d,%s",errno,strerror(errno));
exit(BIND_ERR);
}
}
//监听
void Listen()
{
int n=listen(_fd,BACKLOG_DEFAULT);
if(n==-1)
{
logMessage(Fatal,"listen error,code:%d,%s",errno,strerror(errno));
exit(LISTEN_ERR);
}
}
//获取链接
int Accept(std::string& client_ip,uint16_t& client_port)
{
//获取io套接字
struct sockaddr_in client;
socklen_t len=sizeof(client);
int sockfd=accept(_fd,(struct sockaddr*)&client,&len);
if(sockfd==-1)
{
logMessage(Warn,"accept fail,relink......");
}
//输出客户端ip和port
client_ip=inet_ntoa(client.sin_addr);
client_port=ntohs(client.sin_port);
return sockfd;
}
//链接
int Connect(const string& server_ip,const uint16_t& server_port)
{
//设置服务器地址与端口结构体
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_addr.s_addr=inet_addr(server_ip.c_str());
server.sin_port=htons(server_port);
//向服务器申请链接
int n=connect(_fd,(struct sockaddr*)&server,sizeof(server));
if(n==-1)
{
logMessage(Error,"connect fail,relink......");
}
return n;
}
int get_sockfd(){return _fd;}
~sock()
{
close(_fd);
}
private:
int _fd;
};
}
用于格式化日志打印,每条日志都增加了进程ID,系统时间,和日志等级信息,可将日志打印至特定文件.
log.hpp
#pragma once
#include<iostream>
#include<string>
#include<time.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdarg.h>
#define filename "tcpserver.log"
using namespace std;
enum
{
/* DEBUG:用于调试目的,提供详细的程序执行信息。
INFO:提供程序执行的重要信息,用于跟踪程序的进程。
WARN:表示潜在的问题,可能导致程序错误或异常。
ERROR:表示程序中的错误,但不会导致程序终止。
FATAL:表示严重的错误,可能导致程序终止。 */
Debug,
Info,
Warn,
Error,
Fatal,
};
//将日志等级转换为字符串
static string get_leve_string(int leve)
{
switch(leve)
{
case Debug:return "Debug";
case Info:return "Info";
case Warn:return "Warn";
case Error:return "Error";
case Fatal:return "Fatal";
default: "Unkown";
}
}
//获取系统当前时间
static string get_time()
{
//获取时间戳
time_t cur=time(nullptr);
//转换日期结构
struct tm* tmp=localtime(&cur);
char buffer[1024];
//格式化日期到buffer
snprintf(buffer,sizeof(buffer),"%d-%d-%d_%d:%d:%d",
tmp->tm_year+1900,tmp->tm_mon+1,tmp->tm_mday,tmp->tm_hour,tmp->tm_min,tmp->tm_sec);
return buffer;
}
//输出格式化日志信息
static void logMessage(int leve,const char* format,...)
{
char logLeft[2048];
char logRight[2048];
//格式化时间,pid,日志等级
string leve_string=get_leve_string(leve);
string time_string=get_time();
snprintf(logLeft,sizeof(logLeft),"[%s][%s][%d]",leve_string.c_str(),time_string.c_str(),getpid());
//获取并格式化可变参数
va_list p;
va_start(p,format);
vsnprintf(logRight,sizeof(logRight),format,p);
va_end(p);
printf("%s,%s\n",logLeft,logRight);
//输出参数到文件
/* FILE* fp=fopen(filename,"a");
if(fp==nullptr)return ;
fprintf(fp,"%s %s\n",logLeft,logRight);
fclose(fp); */
}
用于将服务器进程设置为守护进程.
daemon.hpp
#pragma once
#include "log.hpp"
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
namespace daemon_space
{
void daemon()
{
// 处理异常信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 使进程不为组长
if (fork() > 0)
exit(0);
// 创建会话
pid_t sid = setsid();
if ((int)sid == -1)
{
logMessage(Fatal, "setsid error,error code:%d,%s", errno, strerror(errno));
}
/* // 处理标准输入输出
int fd = open("/dev/null", O_RDWR);
if (fd == -1)
{
logMessage(Error, "open error,error code:%d,%s", errno, strerror(errno));
}
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd); */
}
}
用于制定协议和协议的序列和反序列化相互转化,以及信息获取,报头的去除和添加.
protocol.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <jsoncpp/json/json.h>
#include "util.hpp"
namespace protocol_space
{
// 分隔符
#define SEPARATOR "\r\n"
#define SEP_LEN strlen(SEPARATOR)
// 接收数据包
int RecvPackage(int fd, string &package)
{
// 接收所有数据包
char buffer[2048];
ssize_t n = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
}
else if (n == 0)
{
logMessage(Fatal, "client quit");
}
else
{
logMessage(Fatal, "recv error,error code:%d,%s", errno, strerror(errno));
exit(RECV_ERR);
}
// 查找第一个分隔符
string str = buffer;
auto pos = str.find(SEPARATOR);
if (pos == std::string::npos)
return 0;
// 获取有效载荷长度
std::string slen = str.substr(0, pos);
// 确定完整数据包长度
int len = std::atoi(slen.c_str()) + slen.size() + SEP_LEN * 2;
// 判断str的长度是否达到完整数据包长度
if (str.size() < len)
return 0;
// 截取完整数据包到str中去
package = str.substr(0, len);
// 删除str中已经被复制截取的部分
str.erase(0, len);
return atoi(slen.c_str());
}
// 去除报头
void RemoveHeader(string &str, int len)
{
str = str.substr(str.size() - SEP_LEN - len, len);
}
// 添加报头
void AddHeader(string &str)
{
std::string strlen = std::to_string(str.size());
str = strlen + SEPARATOR + str + SEPARATOR;
}
// 需求
class request
{
public:
request() : _x(-1), _y(-1), _op(' ')
{
}
request(int x, int y, char op) : _x(x), _y(y), _op(op)
{
}
// 反序列化
void deserializetion(const std::string &str)
{
#ifdef MYSELF
// 分割
std::vector<std::string> strv;
util_space::split(str, " ", strv);
for (auto e : strv)
{
std::cout << e << " ";
}
std::cout << std::endl;
// 转换
_x = atoi(strv[0].c_str());
_op = strv[1][0];
_y = atoi(strv[2].c_str());
#else
// 从将序列化信息分割填充在root结构体中
Json::Value root;
Json::Reader read;
read.parse(str, root);
// 从root中提取结构化信息
_x = root["x"].asInt();
_op = root["op"].asInt();
_y = root["y"].asInt();
}
#endif
// 序列化
void serializetion(std::string & str)
{
#ifdef MYSELF
str = std::to_string(_x) + ' ' + _op + ' ' + std::to_string(_y);
#else
// 将信息填充在root结构中
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["op"] = _op;
// 用root结构体将信息序列化到str中
// Json::StyledWriter writer;
Json::FastWriter writer;
str = writer.write(root);
#endif
}
~request()
{
}
public:
int _x;
int _y;
char _op;
};
// 回应
class response
{
public:
response() : _result(-1), _code(-1)
{
}
response(int result, int code) : _result(result), _code(code)
{
}
// 反序列化
void deserializetion(const std::string &str)
{
#ifdef MYSELF
// 分割
std::vector<std::string> strv;
util_space::split(str, " ", strv);
// 转换
_result = atoi(strv[0].c_str());
_code = atoi(strv[1].c_str());
#else
// 从将序列化信息分割填充在root结构体中
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
// 从root中提取结构化信息
_result = root["result"].asInt();
_code = root["code"].asInt();
#endif
}
// 序列化
void serializetion(std::string &str)
{
#ifdef MYSELF
str = std::to_string(_result) + ' ' + std::to_string(_code);
#else
// 将信息填充在root结构中
Json::Value root;
root["result"] = _result;
root["code"] = _code;
// 用root结构体将信息序列化到str中
// Json::StyledWriter writer;
Json::FastWriter writer;
str = writer.write(root);
#endif
}
~response()
{
}
public:
int _result;
int _code;
};
}
经常被复用的函数
util.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <ctype.h>
namespace util_space
{
void split(const std::string &str, const char *sep, std::vector<std::string> &strv)
{
int start = 0;
while (start < str.size())
{
// 找分隔符位置
auto pos = str.find(sep, start);
if (pos == std::string::npos)
break;
// 分割字符串到strv
std::string result = str.substr(start, pos - start);
strv.push_back(result);
// 更新查找sep的位置
start = pos + strlen(sep);
}
// 最后一段字符可能不包含分隔符
if (start < str.size())
{
strv.push_back(str.substr(start, str.size() - start));
}
}
}
创建网络套接字,绑定ip地址和端口号,设置套接字为监听属性,监听并获取链接,创建多线程,使子线程处理网络IO,主线程反复监听链接;
IO:将网络缓冲区中的信息拷贝至用户缓冲区—>提取完整数据报—>去除报头—>反序列化—>信息处理—>将结果序列化—>添加报头—>将用户缓冲区的信息拷贝至网络缓冲区.
tcp_server.hpp
#include "sock.hpp"
#include "log.hpp"
#include "protocol.hpp"
#include <string>
#include <functional>
#define PORT_DEFAULT 8888
#define set
namespace server_space
{
// 信息处理函数类型
using func_t = function<void(const protocol_space::request &, protocol_space::response &)>;
// 线程参数包类
class tcp_server;
class thread_data
{
public:
thread_data(int sockfd, const string &client_ip, const uint16_t &client_port, tcp_server *ts)
: _sockfd(sockfd), _client_ip(client_ip), _client_port(client_port), _ts(ts)
{
}
~thread_data()
{
}
public:
int _sockfd;
string _client_ip;
uint16_t _client_port;
tcp_server *_ts;
};
// 服务器
class tcp_server
{
public:
tcp_server(func_t f, const uint16_t &port = PORT_DEFAULT) : _f(f), _port(port)
{
}
void server_init()
{
// 获取套接字
_sock.Socket();
// 绑定ip和port
_sock.Bind(_port);
// 监听
_sock.Listen();
logMessage(Info, "server init success");
}
void start()
{
while (true)
{
// 获取链接
std::string client_ip;
uint16_t client_port;
int sockfd = _sock.Accept(client_ip, client_port);
if (sockfd == -1)
continue;
logMessage(Info, "%s>%d链接成功", client_ip.c_str(), client_port);
// 服务
pthread_t td;
// 创建线程参数类对象
thread_data *tdata = new thread_data(sockfd, client_ip, client_port, this);
// 运行线程
pthread_create(&td, nullptr, Routine, tdata);
}
}
static void *Routine(void *args)
{
// 去关联
pthread_detach(pthread_self());
thread_data *tdata = static_cast<thread_data *>(args);
// 调用信息处理函数
tdata->_ts->IOservice(tdata->_sockfd, tdata->_client_ip, tdata->_client_port);
// 释放线程参数类对象
delete tdata;
return nullptr;
}
void IOservice(int fd, const string &client_ip, const uint16_t client_port)
{
while (true)
{
// 接收完整数据包
std::string recv_str;
int n = protocol_space::RecvPackage(fd, recv_str);
if (n == -1)
break;
else if (n == 0)
return;
else
{
logMessage(Info, "package recv success # %s", recv_str.c_str());
// 去除报头
protocol_space::RemoveHeader(recv_str, n);
logMessage(Info, "remove header success # %s", recv_str.c_str());
// 反序列化
protocol_space::request req;
req.deserializetion(recv_str);
logMessage(Info, "deseralizetion success # ");
std::cout << "{" << std::endl;
std::cout << " x=" << req._x << std::endl;
std::cout << " op=" << req._op << std::endl;
std::cout << " y=" << req._y << std::endl;
std::cout << "}" << std::endl;
// 信息处理
protocol_space::response resp;
_f(req, resp);
logMessage(Info, "info handle over # ");
std::cout << "{" << std::endl;
std::cout << "result= " << resp._result << std::endl;
std::cout << "code= " << resp._code << std::endl;
std::cout << "}" << std::endl;
// 序列化
std::string send_str;
resp.serializetion(send_str);
logMessage(Info,"serializetion success # %s",send_str.c_str());
// 添加报头
protocol_space::AddHeader(send_str);
logMessage(Info,"add header success # %s",send_str.c_str());
// 发送信息
int n = send(fd, send_str.c_str(), send_str.size(), 0);
if (n == -1)
{
logMessage(Fatal, "send error,code:%d,%s", errno, strerror(errno));
}
logMessage(Info,"send success # %s",send_str.c_str());
}
}
}
~tcp_server()
{
}
private:
uint16_t _port;
sock_space::sock _sock;
func_t _f;
};
}
tcp_server.cc
#include "tcp_server.hpp"
#include "daemon.hpp"
#include "protocol.hpp"
#include <memory>
#include <cstdio>
// 信息处理
void handle(const protocol_space::request &req, protocol_space::response &resp)
{
// 计算
switch (req._op)
{
case '+':
resp._result = req._x + req._y;
resp._code = 0;
break;
case '-':
resp._result = req._x - req._y;
resp._code = 0;
break;
case '*':
resp._result = req._x * req._y;
resp._code = 0;
break;
case '/':
{
if (req._y == 0)
{
resp._result = 0;
resp._code = -1;
}
else
{
resp._result = req._x / req._y;
resp._code=0;
}
}
break;
case '%':
{
if (req._y == 0)
{
resp._result = 0;
resp._code = -1;
}
else
{
resp._result = req._x % req._y;
resp._code=0;
}
}
break;
default:
{
resp._result=0;
resp._code=-1;
}
break;
}
}
int main(int argc, char *argv[])
{
// 获取port
uint16_t port = atoi(argv[1]);
// 运行server
std::unique_ptr<server_space::tcp_server> ts(new server_space::tcp_server(handle, port));
ts->server_init();
daemon_space::daemon();
ts->start();
}
客户端:创建网络套接字—>接收用户输入—>格式化—>序列化—>添加报头—>将用户缓冲区信息拷贝至网络缓冲区
将网络缓冲区拷贝至用户缓冲区—>提取完成报文—>去除报头—>反序列化—>输出结果
tcp_client.cc
#include "sock.hpp"
#include "protocol.hpp"
#include <iostream>
#include <cstdlib>
enum Stat
{
LEFT,
MIDDLE,
RIGHT,
};
void FormatStr(protocol_space::request &req, std::string str)
{
Stat s = LEFT;
std::string x, y, op;
for (int i = 0; i < str.size();)
{
switch (s)
{
case LEFT:
{
if (isdigit(str[i]))
{
x.push_back(str[i++]);
}
else
{
s = MIDDLE;
}
}
break;
case MIDDLE:
{
op = str[i++];
s = RIGHT;
}
break;
default:
{
y.push_back(str[i++]);
}
break;
}
}
req._x = atoi(x.c_str());
req._y = atoi(y.c_str());
req._op = op[0];
}
int main(int argc, char *argv[])
{
// 创建套接字
sock_space::sock _sock;
_sock.Socket();
logMessage(Info,"create socket success");
// 链接服务器
int N = 5;
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
while (_sock.Connect(serverip, serverport) == -1)
{
--N;
if(N<=0)
{
logMessage(Fatal,"connect fail exit");
exit(CONNECT_ERR);
}
}
logMessage(Info,"connect success");
while (true)
{
// 获取用户输入
cout<<"输入操作 ";
std::string str;
getline(std::cin, str);
protocol_space::request req;
FormatStr(req, str);
std::cout << "client send#"<< req._x << req._op << req._y << std::endl;
// 序列化
std::string sendstr;
req.serializetion(sendstr);
// 加报头
protocol_space::AddHeader(sendstr);
// 发送数据
if (send(_sock.get_sockfd(), sendstr.c_str(), sendstr.size(), 0) < 0)
{
std::cout << "send error,code"<< errno << strerror(errno) << std::endl;
exit(SENDTO_ERR);
}
// 接收数据
std::string recvstr;
int n = protocol_space::RecvPackage(_sock.get_sockfd(), recvstr);
if (n == -1)
break;
else if (n == 0)
continue;
else
{
// 去报头
protocol_space::RemoveHeader(recvstr, n);
// 反序列化
protocol_space::response resp;
resp.deserializetion(recvstr);
// 输出信息
std::cout << "server recv# " <<"result=" <<resp._result << " " <<"code="<< resp._code << std::endl;
}
}
}
三、http协议
1.基本认识
http属于应用层协议的一种,而且是已经被定义好的.http使用了URL(Uniform Resource Locator)统一资源定位器,是计算机Web网络相关的术语,就是网页地址的意思.来访问资源.,诸如视频,图片,文字,音频等统称为资源.协议和端口号是有映射关系的,什么样的协议就对应什么端口号;域名(www.baidu.com)就是IP地址;诸如"/?#"等这些符号在http协议报文中是会被encode编码成16进制的数字,每两位为一个单位,一个单位前面加一个’%';响应回来的时候会decode解码回来.
2.宏观理解http
http请求的格式:
http响应的格式:
因为服务器或者客户端一直在更新,但是有的客户端没有及时更新,但是服务器更新了,这就需要版本信息来确认协议方案,所以首行的协议版本号其实是服务器和客户端的版本号.
3.网站内部跳转
网站内部的跳转是增加了链接,格式文件1,其本质是再次向服务器发起资源请求,资源就是要链接的文件,返回首页其本质就是想服务器发起url为"/"的资源访问请求.
4.请求方法
最常用的请求方法是GET和POST,它们都是向服务器发起资源请求,区别是使用GET方式发起传参请求时,参数是放在url中的,参数不私密;POST方法在发起传参请求时,参数是放在body主体中的,这一点可以在网页的人机交互中可以提现,比如账号登录,这需要用到网页的表单功能.
表单的参数action接收的是要提交参数到的目标资源文件名,method参数是请求方法;
type传参类型,参数是kv键值对,k是name,v是vlue参数.value可以缺省传参也可以用户输入,传参类型为submit为提交,将参数提交都按action.
5.状态码
5.1重定向
重定向状态码分为两大类,永久和临时;
301永久移动,308永久重定向会更改浏览器的地址信息,设置状态码以后,每次访问老的地址,浏览器都会将地址更改为重定向或者是移动后的地址后访问,url显示的也是新地址.;重定向代码删除以重新运行服务器依旧访问的是新地址的网站 .
302临时移动,307临时重定向,不会更改浏览器的地址信息,访问时会将地址更改为新地址,但是url显示的还是老地址.
永久重定向的应用场景,一个网站不管是人在访问,也有可能是爬虫程序在访问,比如不友好的黑客,再比如搜索引擎,搜索引擎之所以能将用户随意的搜索需求正确的响应,是因为它一直在爬取全网的网站信息,铺捉到就存储在服务器本地,并建立各种各样的映射关系.当用户搜索网站的时候,将用户信息与库的信息比对进行反馈.当搜索引擎铺捉到一个网站,这个网站过了一段时间被永久重定向到了新的网站,搜索引擎再次爬取到一个新的网站就会将老的网站永久重定向到新的网站.
5.2错误码
4开头的状态码是客户端错误,服务器的资源是有限的,所以客户端的访问不是随意地,如果访问的服务器不能提供的服务,那就是客户端非法访问.5开头的是服务器错误,当一些函数调用错误,比如open,read或者write调用错误.但是这些错误,一般程序员也不会真实的反馈给客户,因为这关乎到浏览器的商业发展,服务器有问题,会给人不好的印象.一般做搜索引擎的网站都做浏览器,用来汇聚流量,有了流量就能赚钱,所以浏览器之间是有竞争关系的,它们对于状态码的支持大部分是不兼容的.
6.常见报头
7.http会话保持功能
如果一个网站需要登录后才能访问,那么同一个网站的不同网页之间每跳转一次就需要一次认证,我们平时访问网站没有看到这个现象是因为,以腾旭视频网站为例,第一次访问输入登录信息,发送给服务器,服务器会将信息记录在session会话对象中,响应的时候将对象中的信息反馈给浏览器的cookie缓存文件中,cookie文件记录用户登录信息,下次访问时浏览器自动那cookie中的信息在后台与服务器进行认证.
如果只是这样的话,电脑被病毒入侵,cookie信息被盗取,不法分子会拿到用户登录信息,对用户造成的威胁很大;升级版的方案是,服务器拿到用户信息记录在session对象中,生成一个对象唯一码并以kv关系存储在数据库中,将这个ID反馈给cookie,cookie用ID来认证登录,服务器拿到ID,与数据库中的ID对比认证.但是这样还是会被人拿到ID信息,不过服务器会在认证前检测ID发送的主机所在地,看是否是异地登录,如果是异地登录,就销毁这个ID并让该用户重新输入用户信息来登录.
8.模拟http协议服务器编程
程序一共有6大模块,错误码,日志,协议,公共工具,server类,主程序.
error.hpp
#pragma once
#include<iostream>
using namespace std;
enum
{
SOCK_ERR=1,
BIND_ERR,
RECV_ERR,
SENDTO_ERR,
LISTEN_ERR,
READ_ERR,
SETSID_ERR,
OPEN_ERR,
};
log.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <sys/types.h>
#include <unistd.h>
#include <sstream>
#include <time.h>
#include<cstdio>
namespace log_space
{
enum
{
DEBUG,
INFO,
ERROR,
FATAL,
};
std::string GetLevelStr(int level)
{
std::string str;
switch (level)
{
case DEBUG:
str = "DEBUG";
break;
case INFO:
str = "INFO";
break;
case ERROR:
str = "ERROR";
break;
case FATAL:
str = "FATAL";
break;
default:
str = "UNKNOW";
break;
}
return str;
}
std::string GetTimeStr()
{
time_t _time = time(nullptr);
struct tm *pt = localtime(&_time);
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%d-%d-%d_%d:%d:%d", pt->tm_year + 1900, pt->tm_mon + 1,
pt->tm_mday, pt->tm_hour, pt->tm_min, pt->tm_sec);
std::string str = buffer;
return str;
}
void LogMessage(int level, const char *Format, ...)
{
// 级别,时间,pid
char left[2048];
std::string level_str = GetLevelStr(level);
std::string time_str = GetTimeStr();
pid_t id = getpid();
snprintf(left, sizeof(left), "[%s][%s][%d]", level_str.c_str(), time_str.c_str(), id);
char right[2048];
va_list p;
va_start(p, Format);
vsnprintf(right, sizeof(right), Format, p);
va_end(p);
printf("%s: %s\n",left,right);
}
}
主要完成http协议的序列和反序列化,分离和添加 首行:请求方法,资源定位符,协议版本;报头:有效载荷长度,资源文件类型等.
重点:在url的处理上,犹豫浏览器会首先自动请求/favicon.ico文件,所以要判断文件是否为空或者为/favicon.ico文件,如果是则视为访问网页首页,否则访问url即可.
在分离path时,path必须初始化为web根目录,因为url是从/开始访问的,这代表将整个网站的资源全部给用户,这不合理,所以path的前面都要加webroot根目录,控制访问的区域.
使用stringstream类可以简化代码以空格分割字符串.
protocol.hpp
#pragma once
#include <iostream>
#include <string>
#include <sstream>
#include "log.hpp"
#include"util.hpp"
#define WEBROOT "wwwroot"
class Protocol
{
public:
Protocol() : _path(WEBROOT)
{
}
// 反序列化
void Deserialize(const std::string &str)
{
//分割首行
std::stringstream ss(str);
ss >> _method >> _url >> _httpversion;
log_space::LogMessage(log_space::INFO,"split frist line success: ");
std::cout<<"{"<<std::endl;
std::cout<<"method: "<<_method<<std::endl;
std::cout<<"url: "<<_url<<std::endl;
std::cout<<"httpversion: "<<_httpversion<<std::endl;
std::cout<<"}"<<std::endl;
//路径接收
if(strcmp(_url.c_str(),"/")==0||strcmp(_url.c_str(),"/favicon.ico")==0)
{
_path+="/index.html";
}
else
{
_path+=_url;
}
log_space::LogMessage(log_space::INFO,"get path success: %s",_path.c_str());
//提取文件后缀
auto pos = _path.rfind(".");
if (pos != std::string::npos)
{
_suffix = _path.substr(pos, _path.size() - pos);
}
log_space::LogMessage(log_space::INFO,"get suffix success: %s",_suffix.c_str());
}
~Protocol()
{
}
public:
// 方法
std::string _method;
// 资源定位符
std::string _url;
// 协议版本
std::string _httpversion;
// 路径
std::string _path;
// 文件后缀
std::string _suffix;
};
有三个工具函数,提取首行协议函数,读取body资源文件函数,获取协议类型函数.
util.hpp
#pragma once
#include "log.hpp"
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <cerrno>
#include <cstring>
const std::string SEP = "\r\n";
namespace util_space
{
// 提取协议首行
std::string ReadFristLine(std::string &str)
{
// 查找第一个分隔符SEP
auto pos = str.find(SEP);
if (pos == std::string::npos)
{
log_space::LogMessage(log_space::INFO, "not find %s", SEP.c_str());
return "";
}
// 截取第一行到result
std::string result = str.substr(0, pos + SEP.size());
log_space::LogMessage(log_space::INFO, "substr frist line success: %s", result.c_str());
// 从协议中删除第一行
str.erase(0, pos + SEP.size());
return result;
}
// 读文件
bool ReadFile(const std::string &path,std::string& body)
{
// 打开文件
int fd = open(path.c_str(), O_RDONLY);
if (fd < 0)
{
log_space::LogMessage(log_space::ERROR, "open %s error,code:%d,%s",path.c_str(), errno, strerror(errno));
return false;
}
// 获取文件大小
struct stat st;
if (stat(path.c_str(), &st) == -1)
{
log_space::LogMessage(log_space::ERROR, "stat fail,code:%d,%s", errno, strerror(errno));
return false;
}
int size = st.st_size;
log_space::LogMessage(log_space::INFO, "get file size: %d", size);
// 开空间
body.resize(size);
// 读取文件到ret
ssize_t n = read(fd, (char *)body.c_str(), body.size());
if (n == 0)
{
log_space::LogMessage(log_space::INFO, "client quie");
return false;
}
else if(n<0)
{
log_space::LogMessage(log_space::FATAL, "read error,code:%d,%s", errno, strerror(errno));
close(fd);
return false;
}
// 关闭文件
close(fd);
return true;
}
// 获取协议编码
std::string GetProtocolCode(const std::string &suffix)
{
if (suffix == ".html")
return "text/html";
else if (suffix == ".jpg")
return "application/x-jpg";
else if (suffix == ".jpeg")
return "image/jpeg";
else if (suffix == ".img")
return "application/x-img";
else if (suffix == ".mp4")
return "video/mpeg4";
else
return " ";
}
}
网页编写,内嵌一个视频资源
index.html
<!DOCTYPE html>
<html>
<!--头-->
<head>
<meta charset="UTF-8" />
<!--device设备,scale缩放-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hello haoyi</title>
</head>
<!--主体-->
<body>
<!--表单-->
<form action="index2.html" method="post">
<!--传参-->
user name:<br>
<input type="text" name="user name"><br>
password:<br>
<input type="password" name="password" value=""> <br>
<!--提交-->
<input type="submit" value="submit">
</form>
<h1>豪毅</h1>
<!--视频-->
<video width="320" height="240" controls="controls">
<source src="2.mp4" type="video/mp4" />
</video>
<br>
<!--网页跳转-->
<a href="index1.html">文件1</a>
<a href="index2.html">文件2</a>
<br>
</body>
</html>
当客户端请求的资源在服务器中没有时,就给客户端返回一个404错误码信息,提示信息包含在404.html文件中返回给客户端.
404.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404 Not Found</title>
<style>
body {
text-align: center;
padding: 150px;
}
h1 {
font-size: 50px;
}
body {
font-size: 20px;
}
a {
color: #008080;
text-decoration: none;
}
a:hover {
color: #005F5F;
text-decoration: underline;
}
</style>
</head>
<body>
<div>
<h1>404</h1>
<p>页面未找到<br></p>
<p>
您请求的页面可能已经被删除、更名或者您输入的网址有误。<br>
请尝试使用以下链接或者自行搜索:<br><br>
<a href="https://www.baidu.com">百度一下></a>
</p>
</div>
</body>
</html>
sock.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <errno.h>
#include "log.hpp"
#include "error.hpp"
#define BACKLOG_DEFAULT 32
namespace sock_space
{
class sock
{
public:
sock() : _fd(-1)
{
}
// 获取套接字
void Socket()
{
_fd = socket(AF_INET, SOCK_STREAM, 0);
if (_fd < 0)
{
log_space::LogMessage(log_space::FATAL, "socket error,code:%d,%s", errno, strerror(errno));
exit(SOCK_ERR);
}
log_space::LogMessage(log_space::INFO,"socket success");
}
// 绑定
void Bind(const uint16_t &port)
{
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
client.sin_addr.s_addr = htons(INADDR_ANY);
client.sin_family = AF_INET;
client.sin_port = htons(port);
if (bind(_fd, (struct sockaddr *)&client, sizeof(client)) == -1)
{
log_space::LogMessage(log_space::FATAL, "bind error,code:%d,%s", errno, strerror(errno));
exit(BIND_ERR);
}
log_space::LogMessage(log_space::INFO,"bind success");
}
// 监听
void Listen()
{
int n = listen(_fd, BACKLOG_DEFAULT);
if (n == -1)
{
log_space::LogMessage(log_space::FATAL, "listen error,code:%d,%s", errno, strerror(errno));
exit(LISTEN_ERR);
}
log_space::LogMessage(log_space::INFO,"listen success");
}
// 获取链接
int Accept(std::string &client_ip, uint16_t &client_port)
{
// 获取io套接字
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(_fd, (struct sockaddr *)&client, &len);
if (sockfd == -1)
{
log_space::LogMessage(log_space::ERROR, "accept fail,relink......");
}
log_space::LogMessage(log_space::INFO,"accept success");
// 输出客户端ip和port
client_ip = inet_ntoa(client.sin_addr);
client_port = ntohs(client.sin_port);
return sockfd;
}
// 链接
int Connect(const string &server_ip, const uint16_t &server_port)
{
// 设置服务器地址与端口结构体
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
server.sin_port = htons(server_port);
// 向服务器申请链接
int n = connect(_fd, (struct sockaddr *)&server, sizeof(server));
if (n == -1)
{
log_space::LogMessage(log_space::ERROR, "connect fail,relink......");
}
log_space::LogMessage(log_space::INFO,"connect success");
return n;
}
int get_sockfd() { return _fd; }
~sock()
{
close(_fd);
}
private:
int _fd;
};
}
http_server.hpp
#pragma once
#include <iostream>
#include <functional>
#include <pthread.h>
#include "sock.hpp"
namespace http_server_space
{
using func = std::function<std::string(std::string &)>;
// 线程参数包
class HttpServer;
class ThreadData
{
public:
ThreadData(int sock_fd, HttpServer *hs)
: _sock_fd(sock_fd), _hs(hs)
{
}
public:
int _sock_fd;
HttpServer *_hs;
};
class HttpServer
{
public:
HttpServer(uint16_t port, func HandleMethod) : _port(port), _HandleMethod(HandleMethod)
{
}
//初始化
void ServerInit()
{
// 创建套接字
_sock.Socket();
// 绑定ip和端口
_sock.Bind(_port);
// 设置套接字为监听
_sock.Listen();
}
//运行
void ServerStart()
{
while (true)
{
// 获取连接
string client_ip;
uint16_t client_port;
int sock_fd = _sock.Accept(client_ip, client_port);
if (sock_fd == -1)
continue;
log_space::LogMessage(log_space::INFO,"sock file descriaptor create success: %d",sock_fd);
// 多线程通信
pthread_t td;
ThreadData *tdata = new ThreadData(sock_fd, this);
pthread_create(&td, nullptr, Routine, tdata);
log_space::LogMessage(log_space::INFO,"create thread success");
}
}
//例程
static void *Routine(void *args)
{
//去关联
pthread_detach(pthread_self());
//接收线程参数
ThreadData *tdata = static_cast<ThreadData *>(args);
//通信
tdata->_hs->IOsevice(tdata->_sock_fd);
//关闭文件
close(tdata->_sock_fd);
//释放参数包空间资源
delete tdata;
return nullptr;
}
//通信
void IOsevice(int sock_fd)
{
// 接收信息
char buffer[4096];
int n = recv(sock_fd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
}
else if(n<0)
{
log_space::LogMessage(log_space::ERROR, "recv error,code:%d,%s",errno,strerror(errno));
}
else
{
log_space::LogMessage(log_space::INFO,"client quit");
}
// 处理信息
std::string recv_str = buffer;
std::string send_str = _HandleMethod(recv_str);
// 发送信息
send(sock_fd, (char*)send_str.c_str(), send_str.size(), 0);
return;
}
~HttpServer()
{
}
private:
func _HandleMethod;
uint16_t _port;
sock_space::sock _sock;
};
}
在处理协议报的时候要处理几个特殊情况;
情况1:客户端请求的资源不存在时,直接将404网页资源返回给客户端,协议首行的状态码为404 状态信息为not fount.
情况2:当需要临时将地址绑定到新的网站的时候可以将状态码设置为302,状态信信息 Moved Temporarily(临时移动)或者是307,状态信息 temporary redirect(临时重定向);永久性的可以将状态码设置为301,状态信息Moved Permanently(永久移动),或者是308,状态信息 Permanent Redirect(永久重定向); 同时增加转移对象网站的地址报头,Location: https://www.baidu.com/.
情况3:如果需要向浏览器的cookie文件写入信息,可以增加报头Set-Cookie: sessionid=“要写入缓存文件的信息”;
main.cc
#include <memory>
#include "http_server.hpp"
#include "protocol.hpp"
#include "util.hpp"
std::string HandleMethod(std::string &str)
{
std::cout << "-------------------------------------------------------------" << std::endl;
std::cout << str;
// 提取第一行
std::string OneLine = util_space::ReadFristLine(str);
// 反序列化
Protocol p;
p.Deserialize(OneLine);
//序列化
std::string body, send_str;
if (util_space::ReadFile(p._path, body)) // 读取用户申请的资源文件
{
// 协议版本,状态码,状态信息
send_str = "HTTP/1.0 200 OK" + SEP;
// 报头
send_str += "Content-length: " + std::to_string(body.size()) + SEP;
std::string suf = util_space::GetProtocolCode(p._suffix);
log_space::LogMessage(log_space::INFO, "get file type success: %s", suf.c_str());
send_str += "Content-Type: " + suf + SEP;
send_str += "Set-Cookie: sessionid=1234abcd" + SEP;
// 空行
send_str += SEP;
// 主体
send_str += body;
}
else //响应404网页资源给客户端
{
// 读取用户申请的资源文件
util_space::ReadFile("wwwroot/404.html", body);
// 协议版本,状态码,状态信息
send_str = "HTTP/1.0 404 Not Found" + SEP;
// 报头
send_str += "Content-length: " + std::to_string(body.size()) + SEP;
send_str = send_str + "Content-Type: " + "text/html" + SEP;
// 空行
send_str += SEP;
// 主体
send_str += body;
}
/* //重定向测试
send_str="HTTP/1.0 301 Moved Permanently"+SEP;
send_str+="Location: 43.139.221.111"+SEP;
send_str+=SEP; */
return send_str;
}
int main(int argc, char *argv[])
{
uint16_t port = atoi(argv[1]);
std::unique_ptr<http_server_space::HttpServer> hs(new http_server_space::HttpServer(port, HandleMethod));
hs->ServerInit();
hs->ServerStart();
return 0;
}
四、https协议
https就是在http的基础上又加了一个软件层SSL/TLS,这个软件层负责服务器与客户端的握手协商和加密解密.
1.加密概念
加密就是把 明⽂ (要传输的信息)进⾏⼀系列变换, ⽣成 密⽂ .
解密就是把 密⽂ 再进⾏⼀系列变换, 还原成 明⽂ .
在这个加密和解密的过程中, 往往需要⼀个或者多个中间数据, 辅助进⾏这个过程, 这样的数据称为 密钥 (正确发⾳ yue 四声, 不过⼤家平时都读作 yao 四声)
2.加密的作用
http协议是通过明文的方式发送数据的,如果有人从客户端和服务器中间劫持数据,那么数据就是以明文的方式呈现的,别人就可以篡改数据,或者利用数据非法盈利.这就是中间人攻击;比如运行商,所以的网络通信都是要经过运营商的,运营商也就可以劫持数据.或者是利用WiFi热点捕获数据;加密后的数据即便是被别人拿到也无法直接使用.
3.加密方式
对称加密
采⽤单钥密码系统的加密⽅法,同⼀个密钥可以同时⽤作信息的加密和解密,这种加密⽅法称为对
称加密,也称为单密钥加密,特征:加密和解密所⽤的密钥是相同的
• 常⻅对称加密算法(了解):DES、3DES、AES、TDEA、Blowfish、RC2等
• 特点:算法公开、计算量⼩、加密速度快、加密效率⾼
⾮对称加密
• 需要两个密钥来进⾏加密和解密,这两个密钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)。
• 常⻅⾮对称加密算法(了解):RSA,DSA,ECDSA
• 特点:算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,⽽使得加密解密速度没有对称加密解密的速度快。非对称加密要⽤到两个密钥, ⼀个叫做 “公钥”, ⼀个叫做 “私钥”.公钥和私钥是配对的. 最⼤的缺点就是运算速度⾮常慢,⽐对称加密要慢很多.
用法
• 通过公钥对明⽂加密, 变成密⽂
• 通过私钥对密⽂解密, 变成明⽂
也可以反着⽤
• 通过私钥对明⽂加密, 变成密⽂
• 通过公钥对密⽂解密, 变成明⽂
4.数据摘要/指纹
其基本原理是利⽤单向散列函数(Hash函数)对信息进⾏运算,⽣成⼀串固定⻓度
的数字摘要。数字指纹并不是⼀种加密机制,但可以⽤来判断数据有没有被窜改。
• 摘要常⻅算法:有MD5、SHA1、SHA256、SHA512等,算法把⽆限的映射成有限,因此可能会有
碰撞(两个不同的信息,算出的摘要相同,但是概率⾮常低)
• 摘要特征:和加密算法的区别是,摘要严格意义不是加密,因为没有解密,只不过从摘要很难反推
原信息,通常⽤来进⾏数据对⽐
5.数据签名
加密后的数据摘要叫做数据签名
6.https存在漏洞的加密方案
**对称加密: **只有一个密钥,想要加密客户端和服务器就必须请求密钥,但是密钥只能以明文发送,密钥同样会被中间人攻击,所以这种方案不可行.
**非对称加密: **有两个密钥,一个公钥,一个私钥,公钥是公开的,私有只有通信双方的一方有.
公钥有可能被中间人截获,所以只能保证用公钥加密的一方的安全,用私钥加密的一方.所以双方都要有自己的公钥和私钥.但是因为非对称加密解密的非常复杂,所以效率很低.也存在安全问题.
**非对称+对称: **
客户端获取服务器的公钥,用服务器的公钥给自己的密钥C加密后发送给服务器,以后双方就是使用密钥C进行对称加密通信.这样就解决了双非对称加密解密效率不高的问题.
但是这样做还是会被中间人攻击,中间人有自己的公钥M和私钥M`,在服务器和客户端没有建立连接之前就介入客户端和服务器之间准备窃取数据,首先收到服务器明文发送公钥S,然后将自己的公钥M发送给客户端,客户端无法验证密钥是不是真正的响应方发送的,就直接用M加密发送密钥C给中间人,中间人用S加密发送C给服务器.以后的通信,中间人都可以将窃取到的信息用密钥C加密解密.所以也是不安全的.
7.证书
非对称+对称加密方式不安全,究其主要原因是客户端无法验证自己收到的公钥是否是自己请求的服务器发送的密钥,证书可以很好的解决这个问题.证书
CA认证
服务端在使⽤HTTPS前,需要向CA机构申领⼀份数字证书,数字证书⾥含有证书申请者信息、公钥信息等。服务器把证书传输给浏览器,浏览器从证书⾥获取公钥就⾏了,证书就如⾝份证,证明服务端公钥的权威性
证书申请
首先要生成一个密钥对儿,linux可以通过指令ssh-keygen -t ed25519生成
公司拿着法人身份信息,网站域名信息,公钥等信息本地OpenSSL、KeyManager或者在线https://myssl.com/csr_create.html生成证书,后续向CA机构认证,CA机构有唯一的密钥对儿,CA将认证通过后会将证书通过公开的hash算法将证书映射成数据摘要,然后用私钥加密数据摘要形成数字签名,加在证书中颁发给服务端.
证书认证
客户端与服务器通过证书认证,浏览器在出厂的时候就有了CA机构密钥对的公钥了,服务器发给浏览器证书后,浏览器用CA的公钥解密证书中的数字签名,然后通过与CA相同的hash算法,将证书映射为数字摘要,将两者进行比对,如果相同则认证成功,否则失败.
8.最终加密方案
五、网络工具
1.netstat -nlu/tp命令
可以查看网络服务器的运行状态
2.telnet命令
可以在没有客户端的情况下测试服务器的链接
3. 响应工具postman
一款服务器响应测试工具
4.wget命令
获取网络资源命令
文章来源:https://www.toymoban.com/news/detail-612529.html
5.fiddle抓包工具
相当于在浏览器中启动fiddle,浏览器发送的请求都要经过fiddle,由fiddle转发给服务器,服务器的响应也要先经过fiddle,有fiddle响应给浏览器.
文章来源地址https://www.toymoban.com/news/detail-612529.html
到了这里,关于[linux--->应用层网络通信协议]的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!