一、项目的背景
1. 什么是Boost库
Boost库是C++的准标准库, 它提供了很多C++没有的功能,可以称之为是C++的后备力量。早期的开发者多为C++标准委员会的成员,一些Boost库也被纳入了C++11中(如:哈希、智能指针);这里大家可以去百度百科上搜索,一看便知。
2. 什么是搜索引擎
对于搜索引擎,相信大家一定不陌生,如:百度、360、搜狗等,都是我们常用的搜索引擎。但是你想自己实现出一个和百度、360、搜狗一模一样哪怕是类似的搜索引擎,是非常非常困难的。基本上搜索引擎根据我们所给的关键字,搜出来的结果展示都是以网页标题、网页内容摘要和跳转的网址组成的,但是它可能还有相应的照片、视频、广告,这些我们在设计Boost库的搜索引擎项目的时候不考虑,它们属于扩展内容。
二、搜索引擎的原理
原理图分析:
我们要实现出boost库的站内搜索引擎,红色虚线框内就是我们要实现的内容,总的分为客户端和服务器,详细分析如下:
1、我们从客户端想要获取到大学生的相关信息(呈现在网页上的样子就是:网页的标题+摘要+网址),首先我们构建的服务器就要有对应的数据存在,这些数据从何而来,我们可以进行全网的一个爬虫,将数据爬到我们的服务器的磁盘上,但是我们这个项目是不涉及任何爬虫程序的,我们可以直接将boost库对应版本的数据直接解压到我们对应文件里。
2、现在数据已经被我们放到了磁盘中了,接下来客户端要访问服务器,那么服务器首先要运行起来,服务器一旦运行起来,它首先要做的工作就是对磁盘中的这些数据进行去标签和数据清洗的动作,因为我们从boost库拿的数据其实就是对应文档html网页,但是我们需要的只是每个网页的标题+网页内容摘要+跳转的网址,所以才有了去标签和数据清洗(只拿我们想要的)。这样就可以直接跳过网址跳转到boost库相应文档的位置。
3、服务器完成了去标签和数据清洗之后,就需要对这些清洗后的数据建立索引(方便客户端快速查找);
4、当服务器所以的工作都完成之后,客户端就发起http请求,通过GET方法,上传搜索关键,服务器收到了会进行解析,通过客户端发来的关键字去检索已经构建好的索引,找到了相关的html后,就会将逐个的将每个网页的标题、摘要和网址拼接起来,构建出一个新的网页,响应给客户端;至此,客户就看到了相应的内容,点击网址就可以跳转到boost库相应的文档位置。
三、搜索引擎技术栈和项目环境
Boost库搜索引擎项目所涉及的技术栈和项目环境如下:
技术栈:
C/C++/C++11、STL、boost库、Jsoncpp、cppjieba、cpp-httplib、epoll
html5、css、js、jQuery、Ajax
项目环境:
Centos 7 云服务器或Ubuntu服务器
vim/gcc/g++/Makefile
vs2019 or vscode
四、正排索引 VS 倒排索引 —— 搜索引擎的具体原理
1. 正排索引(forword index)
正排索引:就是从文档ID找到文档内容(文档内的关键字)
正排索引是创建倒排索引的基础,有了正排索引之后,如何构建倒排索引呢?
我们要对目标文档进行分词,以上面的文档1/2为例,我们来进行分词演示:
文档1:雷军、买、四斤、小米、四斤小米
文档2:雷军、发布、小米、手机、小米手机
进行分词之后,就能够方便的建立倒排索引和查找。
我们可以看到,在文档1/2中,其中的 “了” 子被我们省去了,这是因为像:了,呢,吗 ,a,the等都是属于停止词,一般我们在分词的时候可以不考虑。那么什么是停止词呢?
停止词: 它是搜索引擎分词的一项技术,停止词就是没有意义的词。如:在一篇文章中,你可以发现有很多类似于了,呢,吗 ,a,the等(中文或英文中)都是停止词,因为频繁出现,如果我们在进行分词操作的时候,如果把这些停止词也算上,不仅会建立索引麻烦,而且会增加精确搜索的难度。
2. 倒排索引(inverted index)
刚刚我们说正排索引是创建倒排索引的基础,首先是要对文档进行分词操作;
倒排索引:就是根据文档内容的分词,整理不重复的各个关键字,对应联系到文档ID的方案
模拟一次查找的过程:
用户输入:小米 ---> 去倒排索引中查找关键字“小米” ---> 提取出文档ID【1,2】---> 去正排索引中,根据文档ID【1,2】找到文档内容 ---> 通过 [ 标题 + 内容摘要 + 网址 ] 的形式,构建响应结果 ---> 返回给用户小米相关的网页信息。
五、编写数据去标签和数据清洗模块
1. 数据准备
boost官网:Boost C++ Libraries
在编写Parser模块的之前,我们先将数据准备好,去boost官网下载最新版本的库,解压到Linux下。我们需要的就是boost_1_84_0/doc/html目录下的html,我们进入到boost库下的doc/html目录里面除了html为后缀的文件外,还有一些目录,但是我们只需要html文件,所以我们只拿html文件进行数据清洗。对数据清洗之后,拿到的全都是html文件,此时还需要对html文件进行去标签处理。我们的目标是把每个文档都去标签,然后写入到同一个文件中,每个文档内容不需要任何\n,文档和文档之间用 \3 进行区分。类似:XXXXXXXXXXXXXXX\3YYYYYYYYYYYYYYYYYY\3ZZZZZZZZZZZZZZZZZZZZ\3
采用下面的方案:
写入文件中,一定要考虑下一次在读取的时候,也要方便操作!
类似:title\3content\3url \n title\3content\3url \n title\3content\3url \n ...
方便我们getline(ifsream, line),直接获取文档的全部内容:title\3content\3ur
2. 编写数据清洗模块
基本框架主要完成的工作如下:将data/input/所有后缀为html的文件筛选出来,然后对筛选好的html文件进行解析(去标签),拆分出标题、内容、网址,最后将去标签后的所有html文件的标题、内容、网址按照 \3 作为分割符,每个文件再按照 \n 进行区分,写入到data/raw_html/raw.txt下。
主要实现:枚举文件、解析html文件、保存html文件。要实现是需要使用boost库中的方法,我们需要安装一下boost的库。
#include "Util.hpp"
// 文件信息结构体
typedef struct DocInfo
{
string title;
string content;
string url;
} DocInfo_t;
class FileParse
{
public:
// 枚举文件 使用boost库中的方法
static bool EnumFile(const string &src, vector<string> *files_list)
{
// path是跨平台的路径对象
boost::filesystem::path root_path(src);
// boost::filesystem命名空间下的全局函数
if (!boost::filesystem::exists(src))
{
cerr << src << " not exists " << endl;
return false;
}
// recursive_directory_iterator(path)就是递归迭代器的起点
// 无参数的recursive_directory_iterator()就是递归迭代器的终点
boost::filesystem::recursive_directory_iterator end;
for (boost::filesystem::recursive_directory_iterator it(root_path); it != end; it++)
{
// boost::filesystem命名空间下的全局函数
if (!boost::filesystem::is_regular_file(*it))
{
continue;
}
// it迭代器的path()方法提取路径 extension()获取路径的后缀
if (it->path().extension() != ".html")
{
continue;
}
// cout << "debug: " << it->path().string() << endl;
//*it = const boost::filesystem::path
files_list->push_back(it->path().string());
}
return true;
}
static bool ParseHtml(const vector<string> &file_list, vector<DocInfo_t> *results)
{
for (const string &fileName : file_list)
{
// cout << fileName << endl;
string result;
if (!FileUtil::readFile(fileName, &result))
{
continue;
}
DocInfo_t doc;
if (!FileUtil::parseTitle(result, &doc.title))
{
continue;
}
if (!FileUtil::parseContent(result, &doc.content))
{
continue;
}
if (!FileUtil::parseURL(fileName, &doc.url))
{
continue;
}
results->push_back(std::move(doc));
}
return true;
}
static bool SaveHtml(const vector<DocInfo_t> &results, const string &output)
{
ofstream ofs(output, ios::out | ios::binary);
if (!ofs.is_open())
{
cerr << "open " << output << " failed " << endl;
return false;
}
for (auto &e : results)
{
string out;
out = e.title;
out += SEP;
out += e.content;
out += SEP;
out += e.url;
out += '\n';
ofs.write(out.c_str(), out.size());
}
ofs.close();
return true;
}
};
#pragma once
#include <iostream>
#include <fstream>
#include <cassert>
#include <boost/filesystem.hpp>
#include <boost/algorithm/string.hpp>
#include <string>
#include <vector>
#include "cppjieba/Jieba.hpp"
using namespace std;
#define SEP '\3'
class FileUtil
{
public:
static bool readFile(const string &file_path, string *out)
{
ifstream ifs(file_path, ios::in);
if (!ifs.is_open())
{
cerr << "open file " << file_path << " error " << endl;
return false;
}
string line;
// istream& while的判断对象是cin,即当前是否存在有效的输入流
while (getline(ifs, line))
{
*out += line;
}
ifs.close();
return true;
}
static bool parseTitle(const string &file, string *title)
{
size_t begin = file.find("<title>");
if (begin == string::npos)
{
return false;
}
size_t end = file.find("</title>");
if (end == string::npos)
{
return false;
}
begin += (sizeof("<title>") - 1);
if (begin > end)
{
return false;
}
*title = file.substr(begin, end - begin);
return true;
}
static bool parseContent(const string &file, string *content)
{
enum status
{
LABLE,
CONTENT
};
enum status s = LABLE;
for (char c : file)
{
switch (s)
{
case LABLE:
if (c == '>')
s = CONTENT;
break;
case CONTENT:
if (c == '<')
{
s = LABLE;
}
else
{
if (c == '\n')
{
c = ' ';
}
else
{
*content += c;
}
}
break;
default:
break;
}
}
return true;
}
static bool parseURL(const string &file, string *url)
{
string urlHead = "https://www.boost.org/doc/libs/1_84_0";
string urlTail = file.substr(sizeof("./data/input") - 1);
*url = urlHead + urlTail;
return true;
}
static void infoSplit(const string &target, vector<string> *out, const string &sep)
{
// 第一个参数:将分割完的字符串放在哪儿
// 第二个参数:要分割的字符串
// 第三个参数:用什么分隔符来分割,可以一个或多个
// 第四个参数:切分时是否进行压缩,默认不压缩(保留空格)
boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);
}
};
#include "Parser.hpp"
const string src_path = "./data/input";
const string output = "./data/raw_html/raw.txt";
int main()
{
vector<string> files_list;
if (!FileParse::EnumFile(src_path, &files_list))
{
cerr << "enum file name error" << endl;
return 1;
}
vector<DocInfo_t> filesContent;
if (!FileParse::ParseHtml(files_list, &filesContent))
{
cerr << "parse html error" << endl;
return 2;
}
if (!FileParse::SaveHtml(filesContent, output))
{
cerr << "save file error" << endl;
return 3;
}
return 0;
}
六、编写建立索引的模块
在构建索引模块时,我们要构建出正排索引和倒排索引,正排索引是构建倒排索引的基础;通过给到的关键字,去倒排索引里查找出文档ID,再根据文档ID,找到对应的文档内容。
#pragma once
#include <unordered_map>
#include "Util.hpp"
struct DocInfoNode
{
string title;
string content;
string url;
uint64_t docId;
};
struct InvertNode
{
uint64_t docId;
string word;
int weight;
};
using InvertList = vector<InvertNode>;
class Index
{
private:
Index() {}
Index(const Index &index) = delete;
Index &operator=(const Index &index) = delete;
static mutex mtx;
static Index *instance;
public:
~Index() {}
public:
static Index *getInstance()
{
if (instance == nullptr)
{
mtx.lock();
if (instance == nullptr)
{
instance = new Index();
cout << "创建单例成功" << endl;
}
mtx.unlock();
}
return instance;
}
// 根据docId找到正排索引对应的内容
DocInfoNode *getForwardIndex(uint64_t id)
{
if (id > forwardIndex.size())
{
cerr << "id out of range,error !" << endl;
return nullptr;
}
return &forwardIndex[id];
}
// 根据关键字找到倒排索引对应的拉链
InvertList *getInvertIndex(const string &input)
{
auto it = invertIndex.find(input);
if (it == invertIndex.end())
{
cerr << "no invertList,error !" << endl;
return nullptr;
}
return &it->second;
}
bool buildIndex(const string &path)
{
ifstream ifs(path, ios::in | ios::binary);
if (!ifs.is_open())
{
cerr << "path " << path << " error " << endl;
return false;
}
int count = 0;
string buffer;
while (getline(ifs, buffer))
{
DocInfoNode *doc = buildForwardIndex(buffer);
if (doc == nullptr)
{
cerr << "build " << buffer << " error " << endl;
continue;
}
buildInvertIndex(*doc);
count++;
if (count % 100 == 0)
{
cout << "already create " << count << " success " << endl;
}
}
return true;
}
DocInfoNode *buildForwardIndex(const string &info)
{
vector<string> result;
const std::string sep = "\3";
FileUtil::infoSplit(info, &result, sep);
if (result.size() != 3)
{
return nullptr;
}
DocInfoNode doc;
doc.title = result[0];
doc.content = result[1];
doc.url = result[2];
// 先保存id,再进行插入 对应的id就是forwardIndex中的下标
doc.docId = forwardIndex.size();
forwardIndex.push_back(move(doc));
return &forwardIndex.back(); // 返回forwardIndex中的最后一个
}
bool buildInvertIndex(const DocInfoNode &info)
{
struct wordCnt
{
int titleCnt;
int contentCnt;
wordCnt() : titleCnt(0), contentCnt(0) {}
};
unordered_map<string, wordCnt> wordMap;
vector<string> titleWord;
JiebaUtil::cutString(info.title, &titleWord);
for (auto e : titleWord)
{
boost::to_lower(e);
wordMap[e].titleCnt++;
}
vector<string> contentWord;
JiebaUtil::cutString(info.content, &contentWord);
for (auto e : contentWord)
{
boost::to_lower(e);
wordMap[e].contentCnt++;
}
#define X 10;
#define Y 1;
for (auto e : wordMap)
{
InvertNode node;
node.word = e.first;
node.weight = e.second.titleCnt * X + e.second.contentCnt * Y;
node.docId = info.docId; // InvertNode的id就是DocInfoNode的id
invertIndex[e.first].push_back(move(node));
}
return true;
}
private:
vector<DocInfoNode> forwardIndex;
unordered_map<string, InvertList> invertIndex;
};
Index *Index::instance = nullptr;
mutex Index::mtx;
const char *const DICT_PATH = "./dict/jieba.dict.utf8";
const char *const HMM_PATH = "./dict/hmm_model.utf8";
const char *const USER_DICT_PATH = "./dict/user.dict.utf8";
const char *const IDF_PATH = "./dict/idf.utf8";
const char *const STOP_WORD_PATH = "./dict/stop_words.utf8";
class JiebaUtil
{
private:
static cppjieba::Jieba jieba;
public:
static void cutString(const string &src, vector<string> *out)
{
jieba.CutForSearch(src, *out);
}
};
cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
七、编写搜索引擎模块
我们已经完成了数据清洗、去标签和索引相关的工作,接下来就是要编写服务器所提供的服务,我们试想一下,服务器要做哪些工作:首先,我们的数据事先已经经过了数据清洗和去标签的,服务器运行起来之后,应该要先去构建索引,然后通过服务,索引我们在Searcher模块中实现两个函数,分别为InitSearcher()和Search(),代码如下:
#include "Index.hpp"
#include <ctype.h>
#include <stdio.h>
#include <jsoncpp/json/json.h>
struct InvertDeduplicationNode
{
uint64_t docId;
int weight;
vector<string> words;
InvertDeduplicationNode() : docId(0), weight(0) {}
};
class Searcher
{
public:
void initSearchaer(const string &input)
{
index = Index::getInstance();
index->buildIndex(input);
}
void search(const string &query, string *out_json)
{
vector<string> queryWord;
JiebaUtil::cutString(query, &queryWord);
unordered_map<uint64_t, InvertDeduplicationNode> dedupMap;
for (auto e : queryWord)
{
boost::to_lower(e);
InvertList *invertList = index->getInvertIndex(e);
if (invertList == nullptr)
{
continue;
}
for (const auto &elem : *invertList)
{
auto &dedupNode = dedupMap[elem.docId];
dedupNode.docId = elem.docId;
dedupNode.weight += elem.weight;
dedupNode.words.push_back(elem.word);
}
}
vector<InvertDeduplicationNode> InvertDedupList;
for (auto &e : dedupMap)
{
InvertDedupList.push_back(move(e.second));
}
sort(InvertDedupList.begin(), InvertDedupList.end(), [](const InvertDeduplicationNode &i1, const InvertDeduplicationNode &i2)
{ return i1.weight > i2.weight; });
Json::Value root;
for (const auto &e : InvertDedupList)
{
DocInfoNode *doc = index->getForwardIndex(e.docId);
if (doc == nullptr)
{
continue;
}
Json::Value item;
item["title"] = doc->title;
item["content"] = contentSplit(doc->content, e.words[0]);
item["url"] = doc->url;
item["id"] = (int)e.docId;
item["weight"] = e.weight;
root.append(item);
}
// Json::FastWriter fw;
Json::StyledWriter sw;
*out_json = sw.write(root);
}
string contentSplit(const string &content, const string &word)
{
auto it = std::search(content.begin(), content.end(), word.begin(), word.end(), [](int e1, int e2)
{ return tolower(e1) == tolower(e2); });
if (it == content.end())
{
return "None1";
}
const int prevStep = 50;
const int nextStep = 100;
int pos = std::distance(content.begin(), it);
int start = 0;
if (start < pos - prevStep)
{
start = pos - prevStep;
}
int end = content.size() - 1;
if (end > pos + nextStep)
{
end = pos + nextStep;
}
if (start >= end)
{
return "None2";
}
string out = content.substr(start, end - start);
out += "...";
return out;
}
private:
Index *index;
};
八、编写httpServer模块
#include "cpp-httplib/httplib.h"
#include "Search.hpp"
const string path = "data/raw_html/raw.txt";
const string root_path = "./wwwroot";
int main()
{
httplib::Server ser;
Searcher search;
search.initSearchaer(path);
ser.set_base_dir(root_path.c_str());
ser.Get("/s", [&search](const httplib::Request req, httplib::Response resp)
{
if (!req.has_param("word"))
{
resp.set_content("必须要有搜索关键字!", "text/plain; charset=utf-8");
return ;
}
string query = req.get_param_value("word");
cout << "用户在搜索:" << query << endl;
string jsonString;
search.search(query,&jsonString);
resp.set_content(jsonString,"application/json"); });
cout << "服务器成功启动" << endl;
ser.listen("0.0.0.0", 8081);
return 0;
}
九、编写前端模块
<!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">
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<title>boost 搜索引擎</title>
<style>
/*去掉网页中的所有内外边距,可以了解html的盒子模型*/
* {
margin: 0;
/* 设置外边距 */
padding: 0;
/* 设置内边距 */
}
/* 将我们的body内的内容100%和html的呈现吻合 */
html,
body {
height: 100%;
}
/* 以点开头的称为类选择器.container */
.container {
/* 设置div的宽度 */
width: 800px;
/* 通过设置外边距达到居中对其的目的 */
margin: 0px auto;
/* 设置外边距的上边距,保持元素和网页的上部距离 */
margin-top: 15px;
}
/* 复合选择器,选中container下的search */
.container .search {
/* 宽度与父标签保持一致 */
width: 100%;
/* 高度设置52px */
height: 50px;
}
/* 选中input标签,直接设置标签的属性,先要选中,标签选择器 */
/* input在进行高度设置的时候,没有考虑边框的问题 */
.container .search input {
/* 设置left浮动 */
float: left;
width: 600px;
height: 50px;
/* 设置边框属性,依次是边框的宽度、样式、颜色 */
border: 2px solid #CCC;
/* 去掉input输入框的右边框 */
border-right: none;
/* 设置内内边距,默认文字不要和左侧边框紧挨着 */
padding-left: 10px;
/* 设置input内部的字体的颜色和样式 */
color: #CCC;
color: #CCC;
font-size: 17px;
}
.container .search button {
/* 设置left浮动 */
float: left;
width: 150px;
height: 54px;
/* 设置button的背景颜色 #4e6ef2*/
background-color: #4e6ef2;
color: #FFF;
/* 设置字体的大小 */
font-size: 19px;
font-family: Georgia, 'Times New Roman', Times, serif 'Times New Roman', Times, serif;
}
.container .result {
width: 100%;
}
.container .result .item {
margin-top: 15px;
}
.container .result .item a {
/* 设置为块级元素,单独占一行 */
display: block;
text-decoration: none;
/* 设置a标签中的文字字体大小 */
font-size: 22px;
/* 设置字体的颜色 */
color: #4e6ef2;
}
.container .result .item a:hover {
/* 设置鼠标放在a之上的动态效果 */
text-decoration: underline;
}
.container .result .item p {
margin-top: 5px;
font-size: 16px;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
}
.container .result .item i {
/* 设置为块级元素,单独占一行 */
display: block;
/* 取消斜体风格 */
font-style: normal;
color: green;
}
</style>
</head>
<body>
<div class="container">
<div class="search">
<input type="text" value="输入搜索关键字...">
<button onclick="Search()">搜索一下</button>
</div>
<div class="result">
<!-- <div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://hao.360.com/?hj=llq7a</i>
</div>
<div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://hao.360.com/?hj=llq7a</i>
</div>
<div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://hao.360.com/?hj=llq7a</i>
</div>
<div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://hao.360.com/?hj=llq7a</i>
</div>
<div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://hao.360.com/?hj=llq7a</i>
</div>
<div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://hao.360.com/?hj=llq7a</i>
</div> -->
</div>
</div>
<script>
function Search() {
// 是浏览器的一个弹出窗
// 1.提取数据,$可以理解为就是JQuery的别称
let query = $(".container .search input").val();
console.log("query = " + query); //console是浏览器对话框,可以用来进行查看js数据
// 2.发起http请求,ajax属于一个和后端进行数据交互的函数
$.ajax({
type: "GET",
url: "/s?word=" + query,
success: function (data) {
console.log(data);
BuildHtml(data);
}
});
}
function BuildHtml(data) {
// 获取html中的result标签
let result_lable = $(".container .result");
// 清空历史搜索结果
result_lable.empty();
for (let elem of data) {
console.log(elem.title);
console.log(elem.url);
let a_lable = $("<a>", {
text: elem.title,
href: elem.url,
// 跳转到新的页面
target: "_blank"
});
let p_lable = $("<p>", {
text: elem.desc
});
let i_lable = $("<p>", {
text: elem.url
});
let div_lable = $("<div>", {
class: "item"
});
a_lable.appendTo(div_lable);
p_lable.appendTo(div_lable);
i_lable.appendTo(div_lable);
div_lable.appendTo(result_lable);
}
}
</script>
</body>
</html>
十、编写Socket套接字和Epoll模型模块
#pragma once
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
class Sock
{
public:
static const int g_num = 20;
static int Socket()
{
int listenSocket = socket(AF_INET, SOCK_STREAM, 0);
if (listenSocket < 0)
{
exit(1);
}
int opt = 1;
setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
return listenSocket;
}
static void Bind(int sock, uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(sock, (sockaddr *)&local, sizeof(local)) < 0)
{
exit(2);
}
}
static void Listen(int sock)
{
if (listen(sock, g_num) < 0)
{
exit(3);
}
}
static int Accept(int sock, string *clientIp, uint16_t *clientPort)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(sock, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
return -1;
}
if (clientIp)
*clientIp = inet_ntoa(peer.sin_addr);
if (clientPort)
*clientPort = ntohs(peer.sin_port);
return serviceSock;
}
};
#pragma once
#include <iostream>
#include <sys/epoll.h>
using namespace std;
class Epoll
{
public:
static const int gsize = 64;
static int creatEpoll()
{
int epfd = epoll_create(gsize);
if (epfd < 0)
{
LogMessage(FATAL, "epoll_create: %d : %s", errno, strerror(errno));
exit(4);
}
return epfd;
}
static bool addEvent(int epfd, int sock, uint32_t event)
{
struct epoll_event ev;
ev.data.fd = sock;
ev.events = event;
int n = epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
return n == 0;
}
static bool delEvent(int epfd, int sock)
{
int n = epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
return n == 0;
}
static void modEvent(int epfd, int sock, uint32_t event)
{
struct epoll_event ev;
ev.data.fd = sock;
ev.events = event;
int n = epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &ev);
assert(n == 0);
}
static int loopOnce(int epfd, struct epoll_event revs[], int num)
{
int n = epoll_wait(epfd, revs, num, -1); // 非阻塞
if (n == -1)
{
LogMessage(FATAL, "epoll_wait : %d : %s", errno, strerror(errno));
}
return n;
}
};
#pragma once
#include <iostream>
#include <vector>
#include <string>
using namespace std;
#define SPE 'X'
#define SPE_LEN sizeof(SPE) //'X'为字符
// XXX
void PackageSplit(string &inbuffer, vector<string> *result)
{
while (true)
{
size_t pos = inbuffer.find(SPE);
if (pos == string::npos)
break;
result->push_back(inbuffer.substr(0, pos));
inbuffer.erase(0, pos + SPE_LEN);
}
}
#pragma once
#include <cassert>
#include <cstdarg>
#include <cstring>
#include <ctime>
#include <cerrno>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define DEBUG 0
#define NOTICE 1
#define WARNING 2
#define FATAL 3
const char *log_level[] = {"DEBUG", "NOTICE", "WARNING", "FATAL"};
void LogMessage(int level, const char *format, ...)
{
assert(level >= DEBUG);
assert(level <= FATAL);
char *name = getenv("USER");
char buff[1024];
va_list v;
va_start(v, format);
vsnprintf(buff, sizeof(buff) - 1, format, v);
va_end(v);
FILE *out = (level == FATAL) ? stderr : stdout;
fprintf(out, "%s | %u | %s | %s\n",
log_level[level],
(unsigned int)time(nullptr),
name == nullptr ? "unknow" : name,
buff);
fflush(out);
}
十一、编写TcpServer模块(高性能IO的精华设计)
#pragma once
#include <iostream>
#include <functional>
#include <vector>
#include <unordered_map>
#include <sys/epoll.h>
#include "Sock.hpp"
#include "Log.hpp"
#include "Epoller.hpp"
#include "Util.hpp"
#include "Procotol.hpp"
#include "Search.hpp"
using namespace std;
class Connection;
class TcpServer;
using func_t = function<int(Connection *)>;
using callback_t = function<int(Connection *, string &)>;
class Connection
{
public:
Connection(int sock, TcpServer *r) : sock_(sock), R_(r) {}
void SetRecver(func_t recver) { recver_ = recver; }
void SetSender(func_t sender) { sender_ = sender; }
void SetExcepter(func_t excepter) { excepter_ = excepter; }
~Connection() {}
public:
int sock_;
TcpServer *R_;
string inbuffer_;
string outbuffer_;
func_t recver_;
func_t sender_;
func_t excepter_;
};
class TcpServer
{
public:
TcpServer(callback_t cb, int port = 8080) : cb_(cb)
{
revs = new struct epoll_event[revs_num];
listensock_ = Sock::Socket();
Util::SetNonBlock(listensock_);
Sock::Bind(listensock_, port);
Sock::Listen(listensock_);
epfd_ = Epoll::creatEpoll();
ser.initSearchaer("data/raw_html/raw.txt");
addConnection(listensock_, EPOLLIN | EPOLLET, std::bind(&TcpServer::accepter, this, placeholders::_1), nullptr, nullptr);
}
void addConnection(int sock, uint32_t event, func_t recver, func_t sender, func_t excepter)
{
if (event & EPOLLET)
{
Util::SetNonBlock(sock);
}
Epoll::addEvent(epfd_, sock, event);
Connection *conn = new Connection(sock, this);
conn->SetRecver(recver);
conn->SetSender(sender);
conn->SetExcepter(excepter);
connections.insert({sock, conn});
LogMessage(DEBUG, "添加新链接到connections成功: %d", sock);
}
int accepter(Connection *conn)
{
// listensock也是ET工作模式,一个连接到来就有对应的事件就绪,那么如果是一批连接呢?
while (true)
{
string clientip;
uint16_t clientport = 0;
int serviceSock = Sock::Accept(conn->sock_, &clientip, &clientport);
if (serviceSock < 0)
{
if (errno == EINTR) // 因信号中断而导致IO未读取完
{
continue;
}
else if (errno == EAGAIN || errno == EWOULDBLOCK) // 本轮读取完成
{
break;
}
else
{
LogMessage(WARNING, "accept error");
return -1;
}
}
LogMessage(DEBUG, "get a new link: %d", serviceSock);
// 注意:默认只设置了让epoll关心读事件,没有关心写事件
// 为什么没有关注写事件:因为最开始的时候,写空间一定是就绪的!
// 运行中可能会存在条件不满足 -- 写空间被写满了
addConnection(serviceSock, EPOLLIN | EPOLLET,
std::bind(&TcpServer::TcpRecver, this, placeholders::_1),
std::bind(&TcpServer::TcpSender, this, placeholders::_1),
std::bind(&TcpServer::TcpExcepter, this, placeholders::_1));
}
return 0;
}
int TcpRecver(Connection *conn)
{
while (true)
{
char buffer[1024];
ssize_t s = recv(conn->sock_, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
conn->inbuffer_ += buffer;
}
else if (s == 0)
{
LogMessage(DEBUG, "client quit");
conn->excepter_(conn);
break;
}
else
{
if (errno == EINTR) // 是否是因为信号中断而导致IO未读完
{
continue;
}
else if (errno == EAGAIN || errno == EWOULDBLOCK) // 本轮缓冲区读完
{
break;
}
else
{
// 读取时出错了
LogMessage(DEBUG, "recv error: %d : %s", errno, strerror(errno));
conn->excepter_(conn);
break;
}
}
}
// splitXThreadXvector
vector<string> result_;
PackageSplit(conn->inbuffer_, &result_);
for (auto &e : result_)
{
// split
// Thread
// vector
cb_(conn, e);
}
return 0;
}
int TcpSender(Connection *conn)
{
while (true)
{
ssize_t n = send(conn->sock_, conn->outbuffer_.c_str(), conn->outbuffer_.size(), 0);
if (n > 0)
{
conn->outbuffer_.erase(0, n); // 将已发送的数据删除
}
else
{
if (errno == EINTR)
{
continue;
}
else if (errno == EAGAIN || errno == EWOULDBLOCK)
{
break;
}
else
{
conn->excepter_(conn);
LogMessage(DEBUG, "send error: %d : %s", errno, strerror(errno));
break;
}
}
}
return 0;
}
int TcpExcepter(Connection *conn)
{
if (!IsExist(conn->sock_))
{
return -1;
}
Epoll::delEvent(epfd_, conn->sock_);
LogMessage(DEBUG, "remove epoll event!");
close(conn->sock_);
LogMessage(DEBUG, "close fd: %d", conn->sock_);
delete connections[conn->sock_];
LogMessage(DEBUG, "delete connection object done");
connections.erase(conn->sock_);
LogMessage(DEBUG, "erase connection from connections");
return 0;
}
bool IsExist(int sock)
{
auto it = connections.find(sock);
if (it == connections.end())
{
return false;
}
else
{
return true;
}
}
void EnableReadWrite(int sock, bool readable, bool writeable)
{
uint32_t event = 0;
event |= (readable ? EPOLLIN : 0);
event |= (writeable ? EPOLLOUT : 0);
Epoll::modEvent(epfd_, sock, event);
}
void Dispatcher()
{
int n = Epoll::loopOnce(epfd_, revs, revs_num);
for (int i = 0; i < n; i++)
{
int sock = revs[i].data.fd;
uint32_t revent = revs[i].events;
// 不能被epoll出错而阻塞
if (revent & EPOLLHUP)
revent |= (EPOLLIN | EPOLLOUT);
if (revent & EPOLLERR)
revent |= (EPOLLIN | EPOLLOUT);
if (revent & EPOLLIN)
{
if (IsExist(sock) && connections[sock]->recver_)
{
connections[sock]->recver_(connections[sock]);
}
}
if (revent & EPOLLOUT)
{
if (IsExist(sock) && connections[sock]->sender_)
{
connections[sock]->sender_(connections[sock]);
}
}
}
}
void run()
{
while (true)
{
Dispatcher();
}
}
Searcher& getSearcher(){
return ser;
}
~TcpServer()
{
if (listensock_ != -1)
{
close(listensock_);
}
if (epfd_ != -1)
{
close(epfd_);
}
delete[] revs;
}
private:
static const int revs_num = 64;
int listensock_;
int epfd_;
unordered_map<int, Connection *> connections;
struct epoll_event *revs;
callback_t cb_;
Searcher ser;
};
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include "Log.hpp"
using namespace std;
class Util
{
public:
static void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
LogMessage(FATAL, "fcntl: %d : %s", errno, strerror(errno));
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
};
#include <memory>
#include "TcpServer.hpp"
#include "Procotol.hpp"
using namespace std;
static void Usage(string process)
{
cerr << "\n Usage : " << process << " port\n"
<< endl;
}
int StartHandler(Connection *conn, string &message, Searcher &service)
{
string jsonStr;
service.search(message, &jsonStr);
conn->outbuffer_ += jsonStr;
conn->sender_(conn);
if (conn->outbuffer_.empty())
{
conn->R_->EnableReadWrite(conn->sock_, true, false);
}
else
{
conn->R_->EnableReadWrite(conn->sock_, true, true);
}
return 0;
}
int HandlerRequest(Connection *conn, string &message)
{
return StartHandler(conn, message,conn->R_->getSearcher());
}
int main(int args, char *argv[])
{
if (args != 2)
{
Usage(argv[0]);
exit(0);
}
unique_ptr<TcpServer> svr(new TcpServer(HandlerRequest, atoi(argv[1])));
svr->run();
return 0;
}
十二、项目总结
关于项目总结,主要是针对项目的扩展
1. 建立整站搜索
我们搜索的内容是在boost库下的doc目录下的html文档,你可以将这个库建立搜索,也可以将所有的版本,但是成本是很高的,对单个版本的整站搜索还是可以完成的,取决于你服务器的配置。
2. 设计一个在线更新的方案,信号,爬虫,完成整个服务器的设计
我们在获取数据源的时候,是我们手动下载的,你可以学习一下爬虫,写个简单的爬虫程序。采用信号的方式去定期的爬取。
3. 不使用组件,而是自己设计一下对应的各种方案
我们在编写http_server的时候,是使用的组件,你可以自己设计一个简单的;
4. 在我们的搜索引擎中,添加竞价排名
我们在给用户反馈是,提供的是json串,显示到网页上,有title、content和url;就可以在构建json串时,你加上你的博客链接(将博客权重变高了,就能够显示在第一个)
5. 热次统计,智能显示搜索关键词(字典树,优先级队列)
6. 设置登陆注册,引入对mysql的使用