wwwroot
file1.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试</title>
</head>
<body>
<h1>hello file1</h1>
<h1>hello file1</h1>
<h1>hello file1</h1>
<h1>hello file1</h1>
<h1>hello file1</h1>
<h1>hello file1</h1>
<h1>hello file1</h1>
<a href="/">返回首页</a>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<!-- 一张网页可能会有很多要素资源入图片、音频,每一个资源都要依次发起http request,比如说一个主体网页有10张图片资源,这就需要发起11次请求 -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- <form action="/a/b/c.exe" method="GET"> -->
<!-- <form action="/a/b/c.exe"是表明表单要提交给谁,表单做为一个整体当我们点击提交按钮将会由浏览器解释这一系列动作并用GET的方法构建http请求交给/a/b/c.exe > -->
<!-- <form action="/a/b/c.exe" method="GET"> -->
<form action="/a/b/c.exe" method="get">
姓名: <input type="text" name="myname" value=""><br/>
密码: <input type="password" name="mypasswd"><br/>
<input type="submit" value="提交"><br/>
</form>
<h1>this is a test</h1>
<h1>this is a test</h1>
<h1>this is a test</h1>
<h1>this is a test</h1>
<h1>this is a test</h1>
<h1>this is a test</h1>
<h1>this is a test</h1>
<img src="/image/2.jpg" alt="这是一张石榴花图片"><br/>
<!-- 向当前网页插入一张图片资源,浏览器扫描下来发现还需要图片,便会发起第二次http请求,alt是获取图片失败了就会显示该文字 -->
<!-- 所谓的跳转,本质其实就是让html中特定的标签被浏览器解释,重新发起http请求 -->
<a href="https://www.baidu.com">百度</a> <!-- 跳转到百度 -->
<a href="/file1.html">file1</a> <!-- 跳转到web根目录下的file1.html -->
<a href="/file2.html">file2</a>
</body>
</html>
Err.h
#pragma once
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
SETSID_ERR,
OPEN_ERR
};
Sock.h
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include "Log.hpp"
#include "Err.hpp"
static const int gbacklog = 32;
static const int defaultfd = -1;
class Sock
{
public:
Sock() : _sock(defaultfd)
{
}
void Socket()
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
logMessage(Fatal, "socket error, code: %d, errstring: %s", errno, strerror(errno));
exit(SOCKET_ERR);
}
}
void Bind(const uint16_t &port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));
exit(BIND_ERR);
}
}
void Listen()
{
if (listen(_sock, gbacklog) < 0)
{
logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));
exit(LISTEN_ERR);
}
}
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int sock = accept(_sock, (struct sockaddr *)&temp, &len);
if (sock < 0)
{
logMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));
}
else
{
*clientip = inet_ntoa(temp.sin_addr);
*clientport = ntohs(temp.sin_port);
}
return sock;
}
int Connect(const std::string &serverip, const uint16_t &serverport)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
return connect(_sock, (struct sockaddr *)&server, sizeof(server));
}
int Fd()
{
return _sock;
}
void Close()
{
if (_sock != defaultfd)
close(_sock);
}
~Sock()
{
}
private:
int _sock;
};
Util.h
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sstream>
#include "Log.hpp"
class Util
{
public:
// 坑:一般网页文件,都是文本的,文本文件不会出现\0但是二进制文件是可能会出现\0的,所以我们不能按照字符串去读取。
// 但是如果是图片,视频,音频-> 二进制的
static bool ReadFile(const std::string &path, std::string *fileContent)//读取路径path下的文件,并将内容放到fukeContent字符串中
{
// 1. 不能按照字符串去读取,获取文件本身的大小。我们按照二进制进行读取,可以放到字符串当中,string可以放置'\0',但是字符串的空间应该控制好。
struct stat st;//这个对象里面由去多和文件相关到字段
int n = stat(path.c_str(), &st);//第二个参数是输出型参数,通过stat函数对该st对象填充相应的内容。
if(n<0)return false;
int size = st.st_size;
// 2. 调整string的空间
fileContent->resize(size);//以字符串的形式存储该二进制文件,但是要求空间足够。
// 3. 读取
int fd = open(path.c_str(), O_RDONLY);//按照只读方式读取
if(fd < 0) return false;
read(fd, (char*)fileContent->c_str(), size);//fileContent做为字符串里面可能包含了\0的内容,但是没问题都可以hold住。
close(fd);
logMessage(Info, "read file %s done", path.c_str());
return true;
}
static std::string ReadOneLine(std::string &message, const std::string &sep)//从字符串message中以sep为分隔符读取到第一行,并将该行内容重message中删除再将该行内容返回
{
auto pos = message.find(sep);
if(pos == std::string::npos) return "";
std::string s = message.substr(0, pos);
message.erase(0, pos+sep.size());//移除第一行的内容
return s;
}
// GET /favicon.ico HTTP/1.1
// GET /a/b/c/d/e/f/g/h/i/g/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z HTTP/1.1
static bool ParseRequestLine(const std::string &line, std::string *method, std::string *url, std::string *httpVersion)
{
std::stringstream ss(line);//stringstream流可以实现直接的按照空格进行划分
ss >> *method >> *url >> *httpVersion;
return true;
}
};
HttpServer.h
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include "Sock.hpp"
static const uint16_t defaultport = 8888;
class HttpServer;
using func_t = std::function<std::string(std::string &)>;
class ThreadData
{
public:
ThreadData(int sock, const std::string &ip, const uint16_t &port, HttpServer *tsvrp)
: _sock(sock), _ip(ip), _port(port), _tsvrp(tsvrp)
{
}
~ThreadData() {}
public:
int _sock;
std::string _ip;
uint16_t _port;
HttpServer *_tsvrp;
};
class HttpServer
{
public:
HttpServer(func_t f, int port = defaultport) :func(f), port_(port)
{
}
void InitServer()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
}
void HandlerHttpRequest(int sock)
{
char buffer[4096];
std::string request;
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 我们认为我们一次读完
if (s > 0)
{
buffer[s] = 0;
request = buffer;
std::string response = func(request);
send(sock, response.c_str(), response.size(), 0);
}
else
{
logMessage(Info, "client quit...");
}
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->_tsvrp->HandlerHttpRequest(td->_sock);
close(td->_sock);
delete td;
return nullptr;
}
void Start()
{
for (;;)
{
std::string clientip;
uint16_t clientport;
int sock = listensock_.Accept(&clientip, &clientport);
if (sock < 0)
continue;
pthread_t tid;
ThreadData *td = new ThreadData(sock, clientip, clientport, this);
pthread_create(&tid, nullptr, threadRoutine, td);
}
}
~HttpServer()
{
}
private:
int port_;
Sock listensock_;
func_t func;
};
Main.cc
#include "HttpServer.hpp"
#include "Err.hpp"
#include "Util.hpp"
#include <unordered_map>
#include <memory>
#include <vector>
const std::string SEP = "\r\n";
const std::string page_404 = "./wwwroot/err_404.html";
// 一般一个webserver,不做特殊说明,如果用户之间默认访问‘/’, 我们绝对不能把把整站给对方
// 需要添加默认首页!!而且,不能让用户访问wwwroot里面的任何一个目录本身,也可以给每一个目录都带上一个默认首页!
const std::string defaultHomePage = "index.html";//默认首页
const std::string webRoot = "./wwwroot"; // web根目录
class HttpRequest
{
public:
HttpRequest() : path_(webRoot)
{
}
~HttpRequest() {}
void Print()
{
logMessage(Debug, "method: %s, url: %s, version: %s",
method_.c_str(), url_.c_str(), httpVersion_.c_str());
// for(const auto & line : body_)
// logMessage(Debug, "-%s", line.c_str());
logMessage(Debug, "path: %s", path_.c_str());
logMessage(Debug, "suffix_: %s", suffix_.c_str());
}
public:
std::string method_;//请求行的方法
std::string url_;//请求行的资源路径
std::string httpVersion_;//请求行的http版本
std::vector<std::string> body_;
std::string path_;//结合web根目录的路径
std::string suffix_;//我们要访问资源的后缀
};
HttpRequest Deserialize(std::string &message)//将我们读到的请求(一段没有分割过的字符串内容)反序列化成为一个结构化的字段HttpRequest
{
HttpRequest req;
std::string line = Util::ReadOneLine(message, SEP);
Util::ParseRequestLine(line, &req.method_, &req.url_, &req.httpVersion_);//将请求行line三个字段的内容提取出来
while (!message.empty())
{
line = Util::ReadOneLine(message, SEP);
req.body_.push_back(line);//将message除了请求行的内容全部以sep做为分割分别仿作vector容器body_当中
}
req.path_ += req.url_; // path_是根目录,"wwwroot/a/b/c.html", "./wwwroot/",所以我么url中的/不一定是系统根目录,一定是web的根目录。
if (req.path_[req.path_.size() - 1] == '/')//这就说明如果请求就是一个/那就访问默认首页,而不是让它访问整个整站
req.path_ += defaultHomePage;
auto pos = req.path_.rfind(".");//
if (pos == std::string::npos)
req.suffix_ = ".html";
else
req.suffix_ = req.path_.substr(pos);
return req;
}
std::string GetContentType(const std::string &suffix)
{
std::string = "Content-Type: ";
if (suffix == ".html" || suffix == ".htm")
content_type + "text/html";
else if (suffix == ".css")
content_type += "text/css";
else if (suffix == ".js")
content_type += "application/x-javascript";
else if (suffix == ".png")
content_type += "image/png";
else if (suffix == ".jpg")
content_type += "image/jpeg";
else
{
}
return content_type + SEP;
}
class Session // redis -> Session
{
public:
std::string name;
std::string passwd;
uint64_t loginTime;
// 用户的其他信息
int status;
int fd;
int sesisonid;
};
//std::unordered_map<int, Session*> sessions;//服务器用来保存每一个session id和session对象
// bool Login(std::string &message)//读取到一个完整的报文,注意这个函数的代码是一个粗糙的,伪代码理解用的。
// {
// // post-> 正文包含账号密码
// std::string name;//通过报文中的正文也就是有效荷载部分取到用户名和密码
// std::string passwd;
// if(check(name, passwd))
// {
// Session* session = new Session(name, passwd...);//用户名和密码正确就生成一个session对象
// int random = rand();
// sessions.insert(std::pair<int, Session*>(random, session));
// }
// http response;//构建响应
// Set-Cookie: sessionid=random;//在响应当中设置Set-Cookie字段
// }
std::string HandlerHttp(std::string &message)
{
// 1. 读取请求
// 确信,request一定是一个完整的http请求报文
// 给别人返回的是一个http response
std::cout << "----------------------------------------" << std::endl;
std::cout << message << std::endl;
// 资源,图片(.png, .jpg...),网页(.html, .htm),视频(),音频(..)->都是文件!都要有自己的后缀!
// 2. 反序列化和分析请求
// HttpRequest req = Deserialize(message);
// req.Print();
// // 3. 使用请求
// std::string body;//这个body就是有效荷载,不能硬编码到HandlerHttp代码当中
// // bool ret = Util::ReadFile(req.path_, &body);
// // /a/b/c.html
// std::string response;//响应的荷载部分可以是普通字符串,浏览器可以对普通字符串做解释,但是我们通常的有载荷部分是以网页的形式给用户呈现的。
// if (true == Util::ReadFile(req.path_, &body))//读取文件成功,按要求进行返回
// {
// response = "HTTP/1.0 500 OK" + SEP;//响应的状态行
// response += "Content-Length: " + std::to_string(body.size()) + SEP;//该字段是描述荷载的长度
// response += GetContentType(req.suffix_); //我们响应的有效荷载可以是各种各样的i资源,比如音视频、html、css、js、图片等等。而该字段是用来描述荷载是什么类型的,比如网页、音频。GetContentType是通过要访问的资源后缀来构建
// response += SEP;
// response += body;
// }
// else//读取文件失败,就404返回
// {
// response = "HTTP/1.0 404 Not Found" + SEP;
// Util::ReadFile(page_404, &body);//page_404是一个路径的宏定义
// response += "Content-Length: " + std::to_string(body.size()) + SEP;
// response += GetContentType(".html");
// response += SEP;
// response += body;
// }
// 4. 重定向测试
// std::string response;
// response = "HTTP/1.0 301 Moved Permanently" + SEP;
// response += "Location: https://www.baidu.com/" + SEP;//当浏览器识别到响应是302,location里面有新地址,浏览器就再次发起依次请求。
// response += SEP;
// 5. cookie && session实验
// std::string response;
// response = "HTTP/1.0 200 OK" + SEP;
// response += "Set-Cookie: sessionid=1234abcd" + SEP; //当客户端第一次向服务器发送请求的时候,response中的session id会被浏览器存储到cookie当中,第二次在访问该服务器时,客户端向服务器发送的request报文当中就会在报头包含cookie字段。
// response += SEP;
// if()
// request -> sessionid;//下次客户端发过来的请求里面就会携带session id
// sessions[sessionid]->status;//服务器通过session id来确认用户身份保证其vip的行为权力
// TODO
return response;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<HttpServer> tsvr(new HttpServer(HandlerHttp, port)); // TODO
tsvr->InitServer();
tsvr->Start();
return 0;
}