作者: 华丞臧.
专栏:【网络】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注
)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 LeetCode刷题网站
在上一篇文章中我们见识了序列化和反序列化,其是处于应用层的,是进行网络通信双方进行一种约定;在网络发展至今,已经有了一些非常成熟的场景,并且有大佬自定义的一些协议,写的很好因此成为了应用层特定协议的标准,
虽然我们说,应用层协议是我们程序猿自己定的。但实际上,已经有大佬们定义了一些现成的,又非常好用的应用层协议,供我们直接参考使用,http(超文本传输协议)就是其中之一,底层主流采用的是tcp协议,http是无连接的。
服务器地址是域名,域名就是字符串类型的字段,域名在我们访问网网站的时候必须被转换称为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协议版本 |
---|---|---|
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 fd
logMessage(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;
}
服务器收到请求,提供服务向对应的套接字文件中写入一个“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.0
size_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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"> //
<title>华丞臧(runoob.com)</title> //网页名称
</head>
<body> //正文
<h1>我的第一个标题</h1> //标题
<p>我的第一个段落。</p>
</body>
</html>
在浏览器中访问服务器可以看到网页显示如下图所示:
Liunx上收到的请求如下图,左为服务器收到的请求,右为客户端收到的响应。
用户的网络行为无非有两种:
在浏览器上,网页通常会让用户注册一个账号,用来标识该用户,用户的存放在远端的资源也可以使用该账号标识,在html中这种端口称为表单。
在http中get会以明文方式将用户对应的参数信息拼接到url中。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>华丞臧(runoob.com)</title>
</head>
<body>
<h1>hello world</h1>
<p>我的第一个段落。</p>
<form action="html_form_action.php" method="get">
Username: <input type="text" name="user"><br>
Password: <input type="password" name="passwd"><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
在http中post会以明文方式将用户对应的参数信息拼接到正文中。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>华丞臧(runoob.com)</title>
</head>
<body>
<h1>hello world</h1>
<p>我的第一个段落。</p>
<form action="html_form_action.php" method="post">
Username: <input type="text" name="user"><br> //用户名,
标识换行
Password: <input type="password" name="passwd"><br> //密码
<input type="submit" value="Submit">
</form>
</body>
</html>
表单形式如下图所示:
在表单中填入用户名和密码后,可以看到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,大公司有充足的动力和能力来保护自己不受攻击,并且还会有各种措施来防护。
补充: