文章目录
再谈 “协议”
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层
协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?
模拟实现网络版计算器
例如, 我们需要实现一个服务器版的计算器. 我们需要客户端把要计算的两个数和符号发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。
实现程序前我们先做约定
- 客户端依次输入 数字1,数字2,操作符
- 操作符只能为 加 减 乘 除 取模
- 除零错误码为1,模零错误码为2,输入错误操作符错误码为3
我们所做的约定就可以理解为协议
协议一定和具体使用场景有关,一般比较简单,鲁棒性(健壮性),可扩展性不强。
互联网应用中,有很多场景是特别高频的,就有一些程序员前辈为我们做好了很多应用层协议,这些场景我们就不用自己设计协议了。我们作为学习者,需要了解别人的协议,适度的学别人的协议细节甚至自己编写一小部分!不排除有些场景,需要自己定义协议如游戏领域,通信领域…
协议文件 protocol.hpp
#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<strings.h>
namespace ns_protocol{
struct Request{
int x;
int y;
char op;
Request():x(0),y(0),op('+'){}
};
struct Response{
int code;// 状态码
int result;
Response():code(0),result(0){}
};
}
服务端文件 server.hpp
#pragma once
#include"protocol.hpp"
#include<pthread.h>
namespace ns_server{
class Server{
private:
uint16_t port;
int listen_sock;
public:
Server(uint16_t _port):port(_port),listen_sock(-1)
{}
void InitServer()
{
listen_sock = socket(AF_INET,SOCK_STREAM,0);
if(listen_sock<0){
std::cerr<<"socket error"<<std::endl;
exit(2);
}
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0){
std::cerr<<"bind error"<<std::endl;
exit(3);
}
if(listen(listen_sock,5)<0){
std::cerr<<"listen error"<<std::endl;
exit(4);
}
}
static void *calc(void *args)
{
int sock = *(int*)args;
delete (int*)args;
while(true){
ns_protocol::Request req;
ssize_t s=recv(sock,&req,sizeof(req),0);// read
if(s>0){
ns_protocol::Response resp;
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;
case '/':
if(0==req.y) resp.code = 1;// 除0
else resp.result = req.x / req.y;
break;
case '%':
if(0==req.y) resp.code = 2;// 模0
else resp.result = req.x % req.y;
break;
default:
resp.code = 3;
break;
}
send(sock,&resp,sizeof(resp),0);// write
}
else if(s==0){
std::cout<<"client quit,me too"<<std::endl;
break;
}
else{
std::cerr<<"client quit,me too"<<std::endl;
break;
}
}
close(sock);
return nullptr;
}
void Loop()
{
while(true){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
if(sock<0){
std::cerr<<"accept error"<<std::endl;
continue;
}
pthread_t tid;
int *p = new int(sock);
pthread_create(&tid,nullptr,calc,p);
}
}
~Server()
{
if(listen_sock>0){
close(listen_sock);
}
}
};
}
服务端文件 server.cc
#include"server.hpp"
void Usage(std::string proc)
{
std::cerr<<"Usage: "<<"\n\t"<<proc<<" port"<<std::endl;
}
int main(int argc,char *argv[])
{
if(argc!=2){
Usage(argv[0]);
exit(1);
}
uint16_t port=atoi(argv[1]);
ns_server::Server svr(port);
svr.InitServer();
svr.Loop();
return 0;
}
用户端 client.hpp
#pragma once
#include "protocol.hpp"
#include <string>
namespace ns_client{
class Client{
private:
std::string svr_ip;
uint16_t svr_port;
int sock;
public:
Client(const std::string &_ip,uint16_t _port):svr_ip(_ip),svr_port(_port),sock(-1)
{}
void InitClient(){
sock = socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
std::cerr<<"socket error"<<std::endl;
exit(2);
}
}
void Run()
{
struct sockaddr_in peer;
bzero(&peer,sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(svr_port);
peer.sin_addr.s_addr = inet_addr(svr_ip.c_str());
int ret = connect(sock,(struct sockaddr*)&peer,sizeof(peer));
if(ret<0){
std::cerr<<"connect error"<<std::endl;
exit(3);
}
while(true){
ns_protocol::Request req;
std::cout << "请输入你的第一个数据# ";
std::cin >> req.x;
std::cout << "请输入你的第二个数据# ";
std::cin >> req.y;
std::cout << "请输入要进行的计算<+-*/%># ";
std::cin >> req.op;
//req是一个结构化的数据,其实也需要进行序列化,暂时忽略
send(sock,&req,sizeof(req),0);
ns_protocol::Response resp;
ssize_t s = recv(sock,&resp,sizeof(resp),0);
if(s > 0){
//你读到的一定是一个曾经被server序列化的字符串,需要你进行反序列化,暂时忽略
std::cout << "code: " << resp.code <<std::endl;
std::cout << "result: " << resp.result <<std::endl;
}
}
}
~Client()
{
if(sock >= 0) close(sock);
}
};
}
客户端文件 client.cc
#include "client.hpp"
static void Usage(std::string proc)
{
std::cerr << "Usage: " << "\n\t" << proc << " svr_ip svr_port" << std::endl;
}
// ./client svr_ip svr_port
int main(int argc, char *argv[])
{
if( argc != 3 ){
Usage(argv[0]);
exit(1);
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
ns_client::Client cli(ip, port);
cli.InitClient();
cli.Run();
return 0;
}
运行结果
HTTP协议概念
- HTTP(HyperText Transfer Protocol,超⽂本传输协 )的协议。
- HTTP是⽆连接, ⽆状态, ⼯作在应⽤层的协议。
- ⽆连接理解为: http协议本身是没有维护连接信息的, http的数据会交给⽹络协议栈传输层的TCP协议, ⽽TCP是⾯向连接的。
- ⽆状态: HTTP 协议⾃身不对请求和响应之间的通信状态进⾏保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。
认识URL
平时我们俗称的 “网址” 其实就是说的 URL。
urlencode和urldecode
在网址中,像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现。如果某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
例如C++就会被转义成C%2B%2B,这里的"+" 被转义成了 “%2B”
urldecode就是urlencode的逆过程
HTTP协议格式
HTTP请求
HTTP 协议规定,请求从客户端发出,最后服务器端响应该请求并返回。换句话说,肯定是先从客户端开始建⽴通信的,服务器端在没有接收到请求之前不会发送响应。
- 首行: [方法] + [url] + [版本]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;
http的请求为什么会存在空行?
作为一个特殊符号来表示http请求报头的结束
因为每一层协议都必须解决两个问题:
1.将报头和有效载荷进行分离!(解包)特殊字符的方案
2.将自己的有效载荷交付给上层协议!(分用)
HTTP响应
- 首行: [版本号] + [状态码] + [状态码解释]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中
HTTP协议请求方法
其中最常用的就是GET方法和POST方法
GET vs POST
- GET和POST方法都不安全,因为默认都没有加密
- GET通常用来进行获取网页等资源,不过也可以提交参数:百度搜索
- POST通常用来提交数据
HTTP的状态码
HTTP常见Header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上
- User-Agent: 声明用户的操作系统和浏览器版本信息
- referer: 当前页面是从哪个页面跳转过来的
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
HTTP的特点
- HTTP本身是无连接的
虽然底层是基于TCP协议的,但是http要发起所谓的http request的时候,不会先在http层建立链接,而是直接通过底层的TCP进行传输.
- HTTP本身是无状态的
http不会记录自己的发起http请求的上下文,不会对历史请求有任何记忆能力!
这样会给用户造成非常的不方便:比如:当vip用户登录上某些vip类的视频网站的时候,点播某些vip视频,因为http是无状态的,那么也就意味着,新打开的网页,以及里面的vip网页,都要重新登录
我们可以给http添加一些其他的技术特点(cookie+ session),让http具有保存状态和会话能力!
cookie和session
当我们在网页中成功输入账户和密码后,浏览器会提示我们是否保存账户和密码。页面保存是将自己的私密信息保存到浏览器的默认文件中,这里的默认文件就是cookie
浏览器每次发起请求的时候,http请求都会自动给你携带你曾经写到临时文件(默认文件)中的内容,发送给服务器端!
浏览器使用cookie的时候
- 每一个http request都会携带cookie的信息,发给服务器!一定需要服务器也支持cookie技术(其中认证的环节,不是http完成的,程序员自己完成认证的)
- cookie"文件":有些浏览器配置的时候,cookie有内存级的,也有文件级别的!
内存级: 当浏览器不关闭时,访问都会直接登录。一旦关闭浏览器,再打开访问就需要输入密码。
文件级: 较长的一段时间内访问都会直接登录,无论是否关闭浏览器。
只用cookie有什么风险!
cookie里面保存的是用户的私密信息,而且有可能是基于文件的,万一我的电脑被注入木马等恶意程序,盗取浏览器的所有cookie信息!比如:拿到了你的访问某个网页的所有cookie信息!,不法分子拿着你的cookie拷贝到自己的浏览器cookie路径下,然后自己访问该网页,就是以你的身份访问了。
所以存在如下风险
1.个人私密信息泄漏的风险
2.盗用个人的身份从事一些违法的事情
那么如何解决上述问题呢?这里我们引入了session文件
session文件是通过复杂的算法将cookie文件转换成唯一的session文件。
session文件保存在用户端,用户的个人私密信息保存在服务端(比较安全),我们在访问时,会用session文件去在服务端找对应的个人信息进行访问。
cookie 和 session 本身都是有时间和空间限制的,比如说:上午在北京访问,下午在南京访问,session会失效,这时就要重新输入用户信息。
http VS https
- http请求无论是get或者post都是不安全的,只有经过加密的数据,才能保证在理论上是安全的!
SSL (Secure Sockets Layer,安全套接字层 )
TLS (Transport Layer Security,传输层安全)
这里所谓的加密解密的软件层,本质是在用户层!
ssl也是分层,分为两层第一层为封装加密解密 , 第一层为https握手。
- 加密分为对称加密(只有一个秘钥)和非对称加密(公钥和私钥),非对称加密中,一般公钥是公开的,私钥必须保存!对称加密效率高,非对称加密效率低(需要不断的加密解密操作)
只要加密了,就一定是安全的吗?
所有的密码学加密算法,都是和CPI的算力有关。如果想破解一个加密的数据,只要我们的算力足够强,就大概率可以破解!强大的算力是有成本的,所谓的安全本质:破解的成本>>破解的收益
- MD5信息摘要算法,一种被广泛使用的密码散列函数
MD5是将文本提取出一部分内容,进行不可逆算法,形成一串字符串,该字符串我们称之为摘要,接着再对摘要进行加密,形成一串复杂的字符,该字符被称为数据指纹
指纹技术: 凡是对原始文本做任何修改,在进行摘要形成指纹之后,得到的指纹数据,差别会非常大!
用户的密码信息是必须经过加密的!保存在对一个公司的mysql数据库中
用户输入密码的流程
加密
- 选择什么种类的加密算法?对称的,还是非对称的?
http刚开始通信的时候,绝对不能使用对称加密,第一次交换秘钥时会出现无法解密的问题。但我们可以使用一对(2个)公钥和私钥,做到安全通信,我们需要所有的client和server提前内置公钥和私钥。所以解决方法是前期ssl握手期间,使用非对称加密,来交换对称加密的秘钥,通信期间,采用对称加密来保证数据安全。
加密过程可以简单的总结为: 非对称加密解密算法(秘钥协商阶段)+对称加密解密算法(加密通信阶段)
秘钥协商阶段
这样做有没有什么问题呢?
可能有黑客截获服务端发来的公钥,将自己的公钥发送给客户端。客户端不考虑接收的公钥是否合法,直接通过公钥加密对称秘钥并发送回去。这样黑客就能以客户端的身份和用户端进行交流了。
如何确认对方发来的公钥是合法的且没有被篡改呢?
这就需要确认对方的主机Server是合法的(有权威机构对网站或者服务器进行合法认证:CA认证)
生成证书的过程
如何证明证书是合法的
client只要能够用CA的公钥成功解密,证书就是合法的!
如何证明证书没有被篡改过
通过对比原文摘要和指纹解密之后的携带的摘要是否相等,来判定是否被篡改过!
中间人是否可以只对证书明文内容进行修改
可以
为什么携带了指纹的证书,根本就不怕被中间人修改呢?
根本原因是,任何中间人,都没有曾经形成指纹的时候,CA的私钥!在client认证阶段,client只会只用CA的公钥来进行证书认证,不相信任何人的其他公钥信息!
结论:世界上的任何一份证书=数据+指纹,没有任何人可以修改因为大部分人,没有CA的私钥,无法二次重新生成指纹
为何我们要先认证证书呢?
不是目的,目的是为了相信server发过来的公钥是合法的!
client是如何得知CA的公钥呢?
操作系统or浏览器出厂的时候,内置了各种权威的根认证机构的各种证书(公钥)
对称加密解密算法(加密通信阶段)
简单的HTTP服务器
http_server.hpp文件
#include <iostream>
#include <string>
#include <fstream>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#define HTTP_VERSION "HTTP/1.0"
#define HOME_PAGE "wwwroot/index.html"
namespace ns_http{
class HttpServer{
private:
uint16_t port;
int listen_sock;
public:
HttpServer(uint16_t _port): port(_port), listen_sock(-1)
{}
void InitHttpServer()
{
listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0){
exit(2);
}
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
exit(3);
}
if(listen(listen_sock, 5) < 0){
exit(4);
}
}
static void *HandlerRequest(void *args)
{
pthread_detach(pthread_self());
int sock = *(int*)args;
delete (int*)args;
//debug
//get request
std::cout << "######################################## begin: " << sock << " #####" << std::endl;
char buffer[4096];
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if(s > 0){
std::cout << buffer;
}
std::cout << "######################################## end: " << sock << " #####" << std::endl;
std::string response;
std::string body;
std::ifstream in(HOME_PAGE, std::ios::in | std::ios::binary);
if(!in.is_open()){
}
else{//成功
std::string line;
while(std::getline(in, line)){
body+=line;
}
response = HTTP_VERSION;
response += " 200 OK\n";
response += "Content-Type: text/html\n";
response += ("Content-Length: " + std::to_string(body.size()) + "\n");
response += "\n"; //传说中的空行,用来区分我们的报头和有效载荷
response += body;
}
in.close();
send(sock, response.c_str(), response.size(), 0);
close(sock); //基于短链接
return nullptr;
}
void Loop()
{
while(true){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if(sock < 0){
continue;
}
pthread_t tid;
int *p = new int(sock);
pthread_create(&tid, nullptr, HandlerRequest, p);
}
}
~HttpServer(){
if(listen_sock >= 0) close(listen_sock);
}
};
}
http_server.hpp文件
#include "http_server.hpp"
#include <unistd.h>
#include <cstdlib>
static void Usage(std::string proc)
{
std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}
// ./http_server 8081
int main(int argc, char *argv[])
{
if(argc != 2){
Usage(argv[0]);
exit(1);
}
ns_http::HttpServer http_server(atoi(argv[1]));
http_server.InitHttpServer();
http_server.Loop();
return 0;
}
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>用来测试</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
运行结果