🥁作者: 华丞臧.
📕专栏:【网络】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注
)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 👉 LeetCode刷题网站
在上一篇文章中我们见识了序列化和反序列化,其是处于应用层的,是进行网络通信双方进行一种约定;在网络发展至今,已经有了一些非常成熟的场景,并且有大佬自定义的一些协议,写的很好因此成为了应用层特定协议的标准,
虽然我们说,应用层协议是我们程序猿自己定的。但实际上,已经有大佬们定义了一些现成的,又非常好用的应用层协议,供我们直接参考使用,http(超文本传输协议)就是其中之一,底层主流采用的是tcp协议,http是无连接的。
平时我们所说的网址就是URL。
服务器地址是域名,域名就是字符串类型的字段,域名在我们访问网网站的时候必须被转换称为IP地址,访问网络服务必须具有网络号即端口号,网络通信的本质就是socket,IP+port。
一般在使用确定协议的时候,再url上面会缺省端口号,在进行网络通信时端口号会被(浏览器)自动添加上,所以浏览器访问指定url时,浏览器必须给我们自动添加port,浏览器就必须知道这些端口,所以特定的众所周知的服务端口号都是并且必须是确定的。因此,在前面学习端口号时,我们说用户自己使用的套接字不能在0~1023。
http --> 80
https --> 443
sshd --> 22
http是以网页的形式呈现的,比如说查阅文档、查看视频;所以http是获取网页资源的,视频、音频等也都是文件,而网页也是一个.html文件。
http是向特定服务器申请特定资源的,获取到本地进行展示或者某种使用的。
网页上的资源在该网页的网络服务器(软件)所在的服务器(硬件)上,而服务器都是Linux,资源文件存放在Linux服务器上;用户在网页上申请资源时,服务器需要根据路径找到该文件,所以URL中需要添加资源文件的路径。/
是Linux下的路径分隔符,但是需要注意这个/
不一定为根目录,因为软件层面可以做一些处理,比如在这个目录前添加一个路径。
像 / ? :等这样的字符,已经被url当做特殊意义理解了,因此这些字符不能随意出现。
比如, 某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义。在进行网络传输进行编码时,尤其是http中有些字段也会进行编码,服务端进行解码。编码是浏览器做的,解码是服务器做的。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。
“+” 被转义成了 “%2B”
urldecode就是urlencode的逆过程:
urlencode工具
网页的内容在开发者工具中可以看到,如下图:
http是基于行的协议。
方法 | 说明 | 支持的HTTP协议版本 |
---|---|---|
GET | 获取资源 | 1.0、1.1 |
POST | 传输实体主体 | 1.0、1.1 |
PUT | 传输文件 | 1.0、1.1 |
HEAD | 获取报文首部 | 1.0、1.1 |
DELETE | 删除文件 | 1.0、1.1 |
OPTIONS | 询问支持的方法 | 1.1 |
TRACE | 追踪路径 | 1.1 |
CONNECT | 要求用隧道协议链接代理 | 1.1 |
LINK | 建立和资源之间的联系 | 1.0 |
UNLINE | 断开连接关系 | 1.0 |
其中最常用的就是GET方法和POST方法.
类别 | 原因短语 | |
---|---|---|
1XX | Informational(信息状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
使用tcp协议的服务器端,在浏览器中可以连接该服务器,浏览器上默认使用http协议进行连接。在前几篇文章中已经编写了tcp套接字,这里只需要编写提供服务的代码即可。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"using namespace std;
volatile bool quitSer = false;void Usage(void *vgs)
{std::cout << "Usage:./tcpserver port ip" << std::endl;
}
//提供服务
void Serverhttp(int sock)
{char buffer[10240];ssize_t s = read(sock, buffer, sizeof buffer);cout << s << endl;if(s > 0){logMessage(DEBUG, "read success..");std::cout << buffer;}
}class server
{
public:server(int port, std::string ip = ""): sockfd_(-1), ip_(ip), port_(port){}~server(){}public:void init(){// 1. 创建套接字sockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (sockfd_ < 0){logMessage(FATAL, "socket:%s[%d]", strerror(errno), sockfd_);exit(SOCK_ERR);}logMessage(DEBUG, "socket success..");// 2.填充域struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_);ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));// 3. 绑定网络信息if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind:%s[%d]", strerror(errno), sockfd_);exit(BIND_ERR);}logMessage(DEBUG, "bind success...");// 4. 监听套接字if (listen(sockfd_, 5) < 0){logMessage(FATAL, "listen:%s[%d]", strerror(errno), sockfd_);exit(LISTEN_ERR);}logMessage(DEBUG, "listen success...");// 完成}void start(){char inbuffer_[1024]; // 用来接收客户端发来的消息// 提供服务while (true){quitSer = false;struct sockaddr_in peer;socklen_t len = sizeof(peer);// 5. 获取连接, accept 的返回值是一个新的socket fdlogMessage(DEBUG,"start1");int serviceSock = accept(sockfd_, (struct sockaddr *)&peer, &len);logMessage(DEBUG,"start2");if (serviceSock < 0){// 获取链接失败logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);continue;}logMessage(DEBUG, "accept success");pid_t id = fork();if(id == 0){pid_t tid = fork();if(tid == 0){uint16_t peerPort = htons(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);Serverhttp(serviceSock);exit(0);}exit(0);}close(serviceSock);int ret = waitpid(id, nullptr, 0);if(ret < 0){logMessage(WARINING, "wait child error:%d", errno);}logMessage(DEBUG, "wait success...");}}private:int sockfd_;uint16_t port_;std::string ip_;
};// ./tcpserver port ip
int main(int argc, char *argv[])
{if (argc < 2 || argc > 3){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3)ip = argv[2];std::cout << port << ":" << ip << std::endl; server tcpSer(port, ip);tcpSer.init();tcpSer.start();return 0;
}
服务端并没有提供服务,所以访问服务器时网页显示如下图:
在Linux服务端可以看到请求的相关属性,如下图:
服务器收到请求,提供服务向对应的套接字文件中写入一个“hello world”,注意响应的格式为首行+header+body,其中header和body之间需要空一行。
void Serverhttp(int sock)
{char buffer[10240];ssize_t s = read(sock, buffer, sizeof buffer);cout << s << endl;if(s > 0){logMessage(DEBUG, "read success..");std::cout << buffer;}//开始响应string response;response = "HTTP/1.0 200 OK\r\n"; //响应首行response += "\r\n"; //空行response += "hello world!!"; //正文send(sock, response.c_str(), response.size(), 0);
}
服务器收到的请求报文如下图:
浏览器收到的响应如下图:
使用telnet
可以查看服务器给用户的响应。
//安装telnet
sudo yum install -y telnet//使用格式
telnet ip port
//connected后
//出现escape character时使用ctrl+}
//输入请求
作为服务端,它并不关心html
的信息也不需要管html的格式是什么,服务器只负责网络请求并且把网页信息给用户推过去就行了,所以实际上在进行网站开发时,网页的资源都是通过html文件推送给用户的,请求的文件在请求行中,其第二个字段就是用户需要访问的文件,如下图:
其中/
并不是根目录,而是web根目录,可以设置成为根目录。
//获取资源路径
string getPath(string in)
{size_t pos = in.find(CRLF);string request = in.substr(0, pos);cout << "request:" << request << endl;// get path http/1.0size_t first = request.find(SPACE); //第一个空格cout << "first:" << first << endl;if(first == string:: npos) return nullptr;size_t second = request.rfind(SPACE); //第二个空格cout << "second:" << second << endl;if(second == string::npos) return nullptr;string path = request.substr(first + SPACE_LEN, second- (first + SPACE_LEN));if(path.size() == 1 && path[0] == '/') path += HOME_PAGE; //用户访问根目录,就返回主页cout << "getPath:" << path << endl;return path;
}
//获取资源
string readFile(string& recource)
{ifstream in(recource);if(!in.is_open()) return "404";string content;string line;while(getline(in, line)) content += line;return content;
}//提供服务
void Serverhttp(int sock)
{//std::cout << peerIp << ":" << peerPort << std::endl;char buffer[10240];ssize_t s = read(sock, buffer, sizeof buffer);cout << s << endl;if(s > 0){logMessage(DEBUG, "read success..");std::cout << buffer;}// 1. 获取路径string path = getPath(buffer);string recource = ROOT_PATH;recource += path;cout << recource << endl;// 获取对应文件内容string html = readFile(recource);// 开始响应string response;response = "HTTP/1.0 200 OK\r\n";response += "Content-Type:text/html\r\n";response += ("Content-Length:" + std::to_string(html.size()) + "\r\n");response += "\r\n";//response += "hello world!!
\r\n";response += html;send(sock, response.c_str(), response.size(), 0);
}
关于html的编写方法可以参考👉菜鸟教程
//index.html
//
华丞臧(runoob.com) //网页名称
//正文我的第一个标题
//标题我的第一个段落。
在浏览器中访问服务器可以看到网页显示如下图所示:
Liunx上收到的请求如下图,左为服务器收到的请求,右为客户端收到的响应。
用户的网络行为无非有两种:
在浏览器上,网页通常会让用户注册一个账号,用来标识该用户,用户的存放在远端的资源也可以使用该账号标识,在html中这种端口称为表单。
在http中get会以明文方式将用户对应的参数信息拼接到url中。
华丞臧(runoob.com)
hello world
我的第一个段落。
表单形式如下图所示:
在http中post会以明文方式将用户对应的参数信息拼接到正文中。
华丞臧(runoob.com)
hello world
我的第一个段落。
表单形式如下图所示:
在表单中填入用户名和密码后,可以看到url中并没有拼接用户参数,如下图:
使用progress telerik fiddler
可以查看用户发送的请求报文(Raw),可以看到用户参数被拼接到正文当中,如下图:
在服务端也可以收到用户发来的请求,如下图:
不难发现,不管是get方法还是post方法都是不安全的,都是以明文的方式传输;如果有第三方来攻击用户,无疑能非常简单的获取用户的数据;与get方法相比,post方法只是更为私密但还是明文传输,不过post方法会将参数添加到请求的正文中,因此当用户需要传输大资源的时候无疑使用post方法更加合适。
301:永久重定向。
void Serverhttp(int sock)
{char buffer[10240];ssize_t s = read(sock, buffer, sizeof buffer);cout << s << endl;if(s > 0){logMessage(DEBUG, "read success..");std::cout << buffer;}string response = "HTTP/1.1 301 Permanent redirect\r\n"; //永久重定向response +="Location: https://blog.csdn.net/qq_59456417?spm=1011.2415.3001.5343r\n";response += "\r\n";send(sock, response.c_str(), response.size(), 0);
}
访问服务器结果如下:
302:临时重定向。
void Serverhttp(int sock)
{char buffer[10240];ssize_t s = read(sock, buffer, sizeof buffer);cout << s << endl;if(s > 0){logMessage(DEBUG, "read success..");std::cout << buffer;}string response = "HTTP/1.0 302 Temporary redirect\r\n"; //临时重定向response +="Location: https://www.qq.com/r\n";response += "\r\n";send(sock, response.c_str(), response.size(), 0);
}
访问服务器结果如下:
http状态特点之一:无状态,用户各种资源的请求http协议不会记录,也就是说http不知道用户访问服务器次数;虽然http不会记录,但http可以借助其他的手段来记录用户的访问。
为什么需要记录呢?
因为用户需要会话保持,网站中某些资源是需要进行用户认证的,如果访问不被记录,就会导致每次访问资源都需要进行用户认证,比如在腾讯视频上每次访问vip影视都需要用户进行一次登录这显然是非常麻烦的的,所以服务器需要保持用户会话。
我们可以登录一下bilibili如下所示,点击网址左边的锁,可以看到有一个Cookie,Cookie是一种浏览器会话保持的策略。
在Cookie类表中可以找到以bilibili开头的:
当你把Cookie中关于bilibili的全部删除,下一次再进入bilibili时就需要重新登陆了。
Cookie内容:
void Serverhttp(int sock)
{char buffer[10240];ssize_t s = read(sock, buffer, sizeof buffer);cout << s << endl;if(s > 0){logMessage(DEBUG, "read success..");std::cout << buffer;}// 1. 获取路径string path = getPath(buffer);string recource = ROOT_PATH;recource += path;cout << recource << endl;string html = readFile(recource);// 开始响应string response;response = "HTTP/1.0 200 OK\r\n";response += "Content-Type:text/html\r\n";response += ("Content-Length:" + std::to_string(html.size()) + "\r\n");response += "Set-Cookie: this is my Cookie content\r\n";response += "\r\n";response += html;send(sock, response.c_str(), response.size(), 0);
}
Cookie就是浏览器给用户维护的一个文件,Cookie可以在磁盘上也可以在内存中,内存中的Cookie将浏览器关闭后Cookie就会释放,再次启动浏览器访问服务器时就需要重新登录。Cookie对用户的安全极大的影响,第三方如果拿到用户的Cookie,第三方就可以通过Cookie使用用户的身份登录服务器,更严重的第三方还是以通过Cookie拿到用户的用户名和密码。
Cookie策略存在极大的安全隐患,所以现在主流的方案不是Cookie,而是Cookie + session的方式。服务器将用户名和密码形成一个session文件,这个文件的文件名session_id在服务器上具有唯一性,服务器将session_id返回客户端,客户端将session_id写入本地的Cookie,后续访问都会通过session_id去访问服务器,此时用户的用户名和密码就是存放在服务器上,软件厂商来维护session和cookie,大公司有充足的动力和能力来保护自己不受攻击,并且还会有各种措施来防护。
补充:
上一篇:Python 分支结构