搜索引擎最重要的目的,嗯,不出意料就是搜索。你传给它一个请求,然后它依照相关性返回你一串匹配的结果。我们可以根据自己的内容创造各种请求结构,试验各种不同的分析器,搜索引擎都会努力尝试提供最好的结果。

不过,一个现代的全文搜索引擎可以做的比这个更多。因为它的核心是基于一个为了高效查询匹配文档而高度优化过的数据结构——倒排索引。它也可以为我们的数据完成复杂的聚合运算,在这里我们叫它facets。(不好翻译,后文对这个单词都保留英文)

facets通常的目的是提供给用户某个方面的导航或者搜索。 当你在网上商店搜索“相机”,你可以选择不同的制造商,价格范围或者特定功能来定制条件,这应该就是点一下链接的事情,而不是通过修改一长串查询语法。
Facet搜索为数不多的几个可以把强大的请求能力开放给最终用户的办法之一,详见Moritz Stefaner的试验“Elastic Lists”,或许你会有更多灵感。

但是,除了链接和复选框,其实我们还能做的更多。比如利用这些数据画图,而这就是我们在这篇文章中要讲的。

实时仪表板
在几乎所有的分析、监控和数据挖掘服务中,或早或晚的你都会碰到这样的需求:“我们要一个仪表板!”。因为大家都爱仪表板,可能因为真的有用,可能单纯因为它漂亮~这时候,我们不用写任何OLAP实现,用facets就可以完成一个很漂亮很给力的分析引擎。

下面的截图就是从一个社交媒体监控应用上获取的。这个应用不单用ES来搜索和挖掘数据,还通过交互式仪表板提供数据聚合功能。
当用户深入数据,添加一个关键字,使用一个自定义查询,所有的图都会实时更新,这就是facet聚合的工作方式。仪表板上不是数据定期计算好的的静态快照,而是一个用于数据探索的真正的交互式工具。

在本文中,我们将会学习到怎样从ES中获取数据,然后怎么创建这些图表。

关系聚合(terms facet)的饼图
第一个图,我们用ES中比较简单的termsfacet来做。这个facet会返回一个字段中最常见的词汇和它的计数值。

首先我们先插入一些数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
curl -X DELETE "http://localhost:9200/dashboard"
curl -X POST "http://localhost:9200/dashboard/article" -d '
{ "title" : "One",
"tags" : ["ruby", "java", "search"]}
'
curl -X POST "http://localhost:9200/dashboard/article" -d '
{ "title" : "Two",
"tags" : ["java", "search"] }
'
curl -X POST "http://localhost:9200/dashboard/article" -d '
{ "title" : "Three",
"tags" : ["erlang", "search"] }
'
curl -X POST "http://localhost:9200/dashboard/article" -d '
{ "title" : "Four",
"tags" : ["search"] }
'
curl -X POST "http://localhost:9200/dashboard/_refresh"

你们都看到了,我们存储了一些文章的标签,每个文章可以多个标签,数据以JSON格式发送,这也是ES的文档格式。

现在,要知道文档的十大标签,我们只需要简单的请求:

1
2
3
4
5
6
7
8
curl -X POST "http://localhost:9200/dashboard/_search?pretty=true" -d '
{
"query" : { "match_all" : {} },
"facets" : {
"tags" : { "terms" : {"field" : "tags", "size" : 10} }
}
}
'

你看到了,我接受所有文档,然后定义一个terms facet叫做“tags”。这个请求会返回如下样子的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"took" : 2,
// ... snip ...
"hits" : {
"total" : 4,
// ... snip ...
},
"facets" : {
"tags" : {
"_type" : "terms",
"missing" : 1,
"terms" : [
{ "term" : "search", "count" : 4 },
{ "term" : "java", "count" : 2 },
{ "term" : "ruby", "count" : 1 },
{ "term" : "erlang", "count" : 1 }
]
}
}
}

JSON中facets部分是我们关心的,特别是facets.tags.terms数组。它告诉我们有四篇文章打了search标签,两篇java标签,等等…….(当然,我们或许应该给请求添加一个size参数跳过前面的结果)

这种比例类型的数据最合适的可视化方案就是饼图,或者它的变体:油炸圈饼图。
我们将使用Protovis一个JavaScript的数据可视化工具集。Protovis是100%开源的,你可以想象它是数据可视化方面的RoR。和其他类似工具形成鲜明对比的是,它没有附带一组图标类型来供你“选择”。而是定义了一组原语和一个灵活的DSL,这样你可以非常简单的创建自定义的可视化。创建饼图就非常简单。

因为ES返回的是JSON数据,我们可以通过Ajax调用加载它。不要忘记你可以clone或者下载实例的全部源代码。

首先需要一个HTML文件来容纳图标然后从ES里加载数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html>
<head>
<title>ElasticSearch Terms Facet Donut Chart</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Load JS libraries -->
<script src="jquery-1.5.1.min.js"></script>
<script src="protovis-r3.2.js"></script>
<script src="donut.js"></script>
<script>
$( function() { load_data(); });
var load_data = function() {
$.ajax({ url: 'http://localhost:9200/dashboard/article/_search?pretty=true'
, type: 'POST'
, data : JSON.stringify({
"query" : { "match_all" : {} },
"facets" : {
"tags" : {
"terms" : {
"field" : "tags",
"size" : "10"
}
}
}
})
, dataType : 'json'
, processData: false
, success: function(json, statusText, xhr) {
return display_chart(json);
}
, error: function(xhr, message, error) {
console.error("Error while loading data from ElasticSearch", message);
throw(error);
}
});
var display_chart = function(json) {
Donut().data(json.facets.tags.terms).draw();
};
};
</script>
</head>
<body>
<!-- Placeholder for the chart -->
<div id="chart"></div>
</body>
</html>

文档加载后,我们通过Ajax收到和之前curl测试中一样的facet。在jQuery的Ajaxcallback里我们通过封装的display_chart()把返回的JSON传给Donut()函数.

Donut()函数及注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
var Donut = function(dom_id) {
if ('undefined' == typeof dom_id) { // Set the default DOM element ID to bind
dom_id = 'chart';
}
var data = function(json) { // Set the data for the chart
this.data = json;
return this;
};
var draw = function() {
var entries = this.data.sort( function(a, b) { // Sort the data by term names, so the
return a.term < b.term ? -1 : 1; // color scheme for wedges is preserved
}), // with any order
values = pv.map(entries, function(e) { // Create an array holding just the counts
return e.count;
});
// console.log('Drawing', entries, values);
var w = 200, // Dimensions and color scheme for the chart
h = 200,
colors = pv.Colors.category10().range();
var vis = new pv.Panel() // Create the basis panel
.width(w)
.height(h)
.margin(0, 0, 0, 0);
vis.add(pv.Wedge) // Create the "wedges" of the chart
.def("active", -1) // Auxiliary variable to hold mouse over state
.data( pv.normalize(values) ) // Pass the normalized data to Protovis
.left(w/3) // Set-up chart position and dimension
.top(w/3)
.outerRadius(w/3)
.innerRadius(15) // Create a "donut hole" in the center
.angle( function(d) { // Compute the "width" of the wedge
return d * 2 * Math.PI;
})
.strokeStyle("#fff") // Add white stroke
.event("mouseover", function() { // On "mouse over", set the "wedge" as active
this.active(this.index);
this.cursor('pointer');
return this.root.render();
})
.event("mouseout", function() { // On "mouse out", clear the active state
this.active(-1);
return this.root.render();
})
.event("mousedown", function(d) { // On "mouse down", perform action,
var term = entries[this.index].term; // such as filtering the results...
return (alert("Filter the results by '"+term+"'"));
})
.anchor("right").add(pv.Dot) // Add the left part of he "inline" label,
// displayed inside the donut "hole"
.visible( function() { // The label is visible when its wedge is active
return this.parent.children[0]
.active() == this.index;
})
.fillStyle("#222")
.lineWidth(0)
.radius(14)
.anchor("center").add(pv.Bar) // Add the middle part of the label
.fillStyle("#222")
.width(function(d) { // Compute width:
return (d*100).toFixed(1) // add pixels for percents
.toString().length*4 +
10 + // add pixels for glyphs (%, etc)
entries[this.index] // add pixels for letters (very rough)
.term.length*9;
})
.height(28)
.top((w/3)-14)
.anchor("right").add(pv.Dot) // Add the right part of the label
.fillStyle("#222")
.lineWidth(0)
.radius(14)
.parent.children[2].anchor("left") // Add the text to label
.add(pv.Label)
.left((w/3)-7)
.text(function(d) { // Combine the text for label
return (d*100).toFixed(1) + "%" +
' ' + entries[this.index].term +
' (' + values[this.index] + ')';
})
.textStyle("#fff")
.root.canvas(dom_id) // Bind the chart to DOM element
.render(); // And render it.
};
return { // Create the public API
data : data,
draw : draw
};
};

现在你们看到了,一个简单的JSON数据转换,我们就可以创建出丰富的有吸引力的关于我们文章标签分布的可视化图标。完整的例子在这里。

当你使用完全不同的请求,比如显示某个特定作者的文章,或者特定日期内发表的文章,整个可视化都照样正常工作,代码是可以重用的。

日期直方图(date histogram facets)时间线
Protovis让创建另一种常见的可视化类型也非常容易:时间线。任何类型的数据,只要和特定日期相关的,比如文章发表,事件发生,目标达成,都可以被可视化成时间线。
好了,让我们往索引里存一些带有发表日期的文章吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
curl -X DELETE "http://localhost:9200/dashboard"
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "1", "published" : "2011-01-01" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "2", "published" : "2011-01-02" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "3", "published" : "2011-01-02" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "4", "published" : "2011-01-03" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "5", "published" : "2011-01-04" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "6", "published" : "2011-01-04" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "7", "published" : "2011-01-04" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "8", "published" : "2011-01-04" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "9", "published" : "2011-01-10" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "10", "published" : "2011-01-12" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "11", "published" : "2011-01-13" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "12", "published" : "2011-01-14" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "13", "published" : "2011-01-14" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "14", "published" : "2011-01-15" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "15", "published" : "2011-01-20" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "16", "published" : "2011-01-20" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "17", "published" : "2011-01-21" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "18", "published" : "2011-01-22" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "19", "published" : "2011-01-23" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "20", "published" : "2011-01-24" }'
curl -X POST "http://localhost:9200/dashboard/_refresh"

我们用ES的date histogram facet来获取文章发表的频率。

1
2
3
4
5
6
7
8
9
10
11
12
13
curl -X POST "http://localhost:9200/dashboard/_search?pretty=true" -d '
{
"query" : { "match_all" : {} },
"facets" : {
"published_on" : {
"date_histogram" : {
"field" : "published",
"interval" : "day"
}
}
}
}
'

注意我们是怎么设置间隔为天的。这个很容易就可以替换成周,月 ,或者年。

请求会返回像下面这样的JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"took" : 2,
// ... snip ...
"hits" : {
"total" : 4,
// ... snip ...
},
"facets" : {
"published" : {
"_type" : "histogram",
"entries" : [
{ "time" : 1293840000000, "count" : 1 },
{ "time" : 1293926400000, "count" : 2 }
// ... snip ...
]
}
}
}

我们要注意的是facets.published.entries数组,和上面的例子一样。同样需要一个HTML页来容纳图标和加载数据。机制既然一样,代码就直接看这里吧。

既然已经有了JSON数据,用protovis创建时间线就很简单了,用一个自定义的area chart即可。

完整带注释的Timeline()函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
var Timeline = function(dom_id) {
if ('undefined' == typeof dom_id) { // Set the default DOM element ID to bind
dom_id = 'chart';
}
var data = function(json) { // Set the data for the chart
this.data = json;
return this;
};
var draw = function() {
var entries = this.data; // Set-up the data
entries.push({ // Add the last "blank" entry for proper
count : entries[entries.length-1].count // timeline ending
});
// console.log('Drawing, ', entries);
var w = 600, // Set-up dimensions and scales for the chart
h = 100,
max = pv.max(entries, function(d) {return d.count;}),
x = pv.Scale.linear(0, entries.length-1).range(0, w),
y = pv.Scale.linear(0, max).range(0, h);
var vis = new pv.Panel() // Create the basis panel
.width(w)
.height(h)
.bottom(20)
.left(20)
.right(40)
.top(40);
vis.add(pv.Label) // Add the chart legend at top left
.top(-20)
.text(function() {
var first = new Date(entries[0].time);
var last = new Date(entries[entries.length-2].time);
return "Articles published between " +
[ first.getDate(),
first.getMonth() + 1,
first.getFullYear()
].join("/") +
" and " +
[ last.getDate(),
last.getMonth() + 1,
last.getFullYear()
].join("/");
})
.textStyle("#B1B1B1")
vis.add(pv.Rule) // Add the X-ticks
.data(entries)
.visible(function(d) {return d.time;})
.left(function() { return x(this.index); })
.bottom(-15)
.height(15)
.strokeStyle("#33A3E1")
.anchor("right").add(pv.Label) // Add the tick label (DD/MM)
.text(function(d) {
var date = new Date(d.time);
return [
date.getDate(),
date.getMonth() + 1
].join('/');
})
.textStyle("#2C90C8")
.textMargin("5")
vis.add(pv.Rule) // Add the Y-ticks
.data(y.ticks(max)) // Compute tick levels based on the "max" value
.bottom(y)
.strokeStyle("#eee")
.anchor("left").add(pv.Label)
.text(y.tickFormat)
.textStyle("#c0c0c0")
vis.add(pv.Panel) // Add container panel for the chart
.add(pv.Area) // Add the area segments for each entry
.def("active", -1) // Auxiliary variable to hold mouse state
.data(entries) // Pass the data to Protovis
.bottom(0)
.left(function(d) {return x(this.index);}) // Compute x-axis based on scale
.height(function(d) {return y(d.count);}) // Compute y-axis based on scale
.interpolate('cardinal') // Make the chart curve smooth
.segmented(true) // Divide into "segments" (for interactivity)
.fillStyle("#79D0F3")
.event("mouseover", function() { // On "mouse over", set segment as active
this.active(this.index);
return this.root.render();
})
.event("mouseout", function() { // On "mouse out", clear the active state
this.active(-1);
return this.root.render();
})
.event("mousedown", function(d) { // On "mouse down", perform action,
var time = entries[this.index].time; // eg filtering the results...
return (alert("Timestamp: '"+time+"'"));
})
.anchor("top").add(pv.Line) // Add thick stroke to the chart
.lineWidth(3)
.strokeStyle('#33A3E1')
.anchor("top").add(pv.Dot) // Add the circle "label" displaying
// the count for this day
.visible( function() { // The label is only visible when
return this.parent.children[0] // its segment is active
.active() == this.index;
})
.left(function(d) { return x(this.index); })
.bottom(function(d) { return y(d.count); })
.fillStyle("#33A3E1")
.lineWidth(0)
.radius(14)
.anchor("center").add(pv.Label) // Add text to the label
.text(function(d) {return d.count;})
.textStyle("#E7EFF4")
.root.canvas(dom_id) // Bind the chart to DOM element
.render(); // And render it.
};
return { // Create the public API
data : data,
draw : draw
};
};

完整示例代码在这里。不过先去下载protovis提供的关于area的原始文档,然后观察当你修改interpolate(‘cardinal’)成interpolate(‘step-after’)后发生了什么。对于多个facet,画叠加的区域图,添加交互性,然后完全定制可视化应该都不是什么问题了。

重要的是注意,这个图表完全是根据你传递给ES的请求做出的响应,使得你有可能做到简单立刻的完成某项指标的可视化需求。比如“显示这个作者在这个主题上最近三个月的出版频率”。只需要提交这样的请求就够了:

1
author:John AND topic:Search AND published:[2011-03-01 TO 2011-05-31]

总结
当你需要为复杂的自定义查询做一个丰富的交互式的数据可视化时,使用ES的facets应该是最容易的办法之一,你只需要传递ES的JSON响应给Protovis这样的工具就好了。

通过模仿本文中的方法和代码,你可以在几小时内给你的数据跑通一个示例。

REFERENECE : http://www.chepoo.com/elasticsearch-protovis.html | IT技术精华网

 

用ElasticSearch和Protovis实现数据可视化的更多相关文章

  1. Nagios 快速实现数据可视化的几种方式

    Nagios 是一款强大的开源监控软件,但他本身不能绘图,只能查看当前数据,不能看历史数据以及趋势,也正因此,想要更舒适的使用就要搭配绘图软件,现在可搭配的绘图软件有很多,例如 pnp4nagios, ...

  2. 数据可视化的开源方案: Superset vs Redash vs Metabase (一)

    人是视觉动物,要用数据把一个故事讲活,图表是必不可少的.如果你经常看到做数据分析同事,在SQL客户端里执行完查询,把结果复制/粘贴到Excel里再做成图表,那说明你的公司缺少一个可靠的数据可视化平台. ...

  3. 4款开源免费的数据可视化JavaScript库

    概述:交互式数据可视化在很大程度上取决于JavaScript库的任务能力.在这篇文章中,我们将看看四个JavaScript库:D3,InfoVis,Processing.js,和Recline.js. ...

  4. 13个可实现超棒数据可视化效果的Javascript框架

    随着商业及其相关需求的发展,数据成为越来越重要的元素之一,为了更加直观和明显的展示商业潜在的趋势和内在的特性,我们需要使用图表和图形的方式来直观动态的展示数据内在秘密,在今天的这篇文章中我们推荐12款 ...

  5. Webservice WCF WebApi 前端数据可视化 前端数据可视化 C# asp.net PhoneGap html5 C# Where 网站分布式开发简介 EntityFramework Core依赖注入上下文方式不同造成内存泄漏了解一下? SQL Server之深入理解STUFF 你必须知道的EntityFramework 6.x和EntityFramework Cor

    Webservice WCF WebApi   注明:改编加组合 在.net平台下,有大量的技术让你创建一个HTTP服务,像Web Service,WCF,现在又出了Web API.在.net平台下, ...

  6. Kibana数据可视化

    Kibana数据可视化 1,3.1使用logstash导入数据的问题 会出现错误提示: [location] is defined as an object in mapping [doc] but ...

  7. Webstorm+Webpack+echarts构建个性化定制的数据可视化图表&&两个echarts详细教程(柱状图,南丁格尔图)

    Webstorm+Webpack+echarts   ECharts 特性介绍 ECharts,一个纯 Javascript 的图表库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(I ...

  8. flask+sqlite3+echarts2+ajax数据可视化报错:UnicodeDecodeError: 'utf8' codec can't decode byte解决方法

    flask+sqlite3+echarts2+ajax数据可视化报错: UnicodeDecodeError: 'utf8' codec can't decode byte 解决方法: 将 py文件和 ...

  9. 手把手教你用FineBI做数据可视化

    前些日子公司引进了帆软商业智能FineBI,在接受了简单的培训后,发现这款商业智能软件用作可视分析只用一个词形容的话,那就是“轻盈灵动”!界面简洁.操作流畅,几个步骤就可以创建分析,获得想要的效果.此 ...

随机推荐

  1. Medusa引擎开源了

    首先贴出 Github地址 然后博客地址 相比于市面上其他的著名游戏引擎,例如Unity,cocos2dx,Unreal,Medusa游戏引擎目前还非常的简陋,目前实现的功能还集中在2D部分,3D的虽 ...

  2. 【原】js实现复制到剪贴板功能,兼容所有浏览器

    两天前听了一个H5的分享,会议上有一句话,非常有感触:不是你不能,而是你对自己的要求太低.很简单的一句话,相信很多事情不是大家做不到,真的是对自己的要求太低,如果对自己要求多一点,那么你取得的进步可能 ...

  3. ZBrush中怎样对遮罩进行反选

    通过对ZBrush的学习,我们知道了如何手动创建遮罩,手动创建遮罩相对来说是最简单有效的方法,在某些特定的使用场合会起到事半功倍的效果.创建遮罩我们可以结合Ctrl键在物体保持编辑的状态下来执行,您可 ...

  4. jmeter的http sample使用

    1.jmeter最简单的http sample 使用 : 最简单的get请求, 输入服务器名称或者ip, 如果有路径则添加路径 ,带参数的添加具体的参数及values值 parameters 和 bo ...

  5. u3d_Shader_effects笔记2 自定义surfaceDiffuseLight

    1.前面的心情 今晚7点半睡着后,9点半左右被吵醒.醒来后非常失落,感觉人生到底在追求什么,我又在追求什么.昨晚梦到妈妈了.最近不时会想到爷爷的去世.人世的险恶,良心的缺失.不过一切总要向前看,至少我 ...

  6. [No000013]在Office中关闭自动拼写检查和自动语法检查

    大家知道有时候语法检查很麻烦,搞得文档里都是红线和绿线.解决办法就是关闭自动拼写检查.现在我们来介绍怎么关闭office包括Word .Outlook .PowerPoint .OneNote .Pu ...

  7. mysql表名忽略大小写问题记录

    问题描述:一开发同事在linux下调一个程序老是报错说找不到表,但是登陆mysql,show tables查看明明是已经创建了这张表的!!如下: mysql> show tables; +--- ...

  8. webstrom软件使用

    很多人都发现 http://idea.lanyus.com/ 不能激活了 很多帖子说的 http://15.idea.lanyus.com/ 之类都用不了了 选择 License server (20 ...

  9. NFine的后台源码

    Chloe官网及基于NFine的后台源码毫无保留开放   扯淡 经过不少日夜的赶工,Chloe 的官网于上周正式上线.上篇博客中LZ说过要将官网以及后台源码都会开放出来,为了尽快兑现我说过的话,趁周末 ...

  10. iptables/Netfilter 学习

    开始学iptables,因为它是和路由器技术紧密结合在一起的. iptables的命令看起来眼花缭乱,随便找两个: iptables -A FORWARD -p tcp -s -d -j ACCEPT ...