2015新年伊始,校园网开始收费。明光村职业学院免费上网的岁月一去不返。我等广大群众感慨之际,突然发现流量用得如此之快。身边的朋友也开始渐渐对看视频心有余悸,对实时流量(此处仅指收费的v4校外流量)的关注也日渐升级。校园网流量计的想法便这样出现了。

p.s. 鉴于开发过程中,有队友的加入,所以下文按照时间顺序而非严格的逻辑顺序进行,望读者谅解。

背景话毕,如何分解我们的需求呢?要想让流量数据可视化,当然又必不可少的两点:a)数据 b)可视化。具体说来便是:

  • 怎么拿到流量数据
  • 怎么更友好的用图表的形式展示

这么一想,解决方案就出来了。

最初设计

考虑到用户对流量实时性了解的需要,决定用动态的折线图呈现最终效果。折线图能显示最近半小时内校园网流量的变化,每约30s采集一次数据。

爬虫程序

既然要收费,学校必定会给学生查看流量的渠道。netaccount.bupt.edu.cn需要离线后才能查看,上网注销窗是最好的选择,每次刷新即可显示当前上线时间,使用流量,剩余网费,很直观;最重要的是每次刷新都能得到实时数据。就爬它了!

写爬虫程序的语言可以有很多,从Java到python到php到js甚至windows shell都可以轻松完成这个简易的爬虫任务:爬取html页面,匹配流量字段,存储。

在爬之前,我们需要知道流量值藏在html页面代码的什么地方。打开开发者工具,代码一览无余。

从flow的大小来看,存储的单位是KB,没关系/1024/1024就行了。准备工作做完后,可以开始码代码了。python比较好上手,我们暂时选择python写爬虫。不说废话,直接上干货。

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
# -*- coding: utf-8 -*-
#---------------------------------------
#   程序:网关爬虫
#   版本:0.2
#   作者:xym
#   日期:2014-01-03
#   语言:Python 2.7
#   功能:输出当前已用流量
#---------------------------------------
#import urllib
import urllib2
import cookielib
import re
import time
import json
class GATE_Spider
    # 申明相关的属性 
    def __init__(self):   
        self.loginUrl = 'http://10.3.8.211'           # 网关url
        self.cookieJar = cookielib.CookieJar()        # 初始化一个CookieJar来处理Cookie的信息
        self.flow = []      #存储流量
        self.flow_json = []
        self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookieJar))
    def flow_init(self):
        for x in range(3):
            # 初始化链接并且获取cookie
            myRequest = urllib2.Request(url = self.loginUrl)# 发送查看请求
            result = self.opener.open(myRequest)            # 得到上网注销窗
            # 打印返回的内容 调试用
            # print result.read()
            self.deal_data(result.read().decode('gbk'))
            self.flow_json = json.dumps(self.flow,indent=2,separators=(',',':'))
            self.cal_data(self.flow);
            time.sleep(5)
    # 抠出流量并换算
    def deal_data(self,myPage): 
        myFlow = re.search(r';flow=\'\d+',myPage)
        myFlow = re.search(r'\d+',myFlow.group(0))     
        self.flow.append({'flow':float(myFlow.group(0).encode('gbk'))/1024,'time':time.time()})

    # 储存流量
    def cal_data(self,items):
        with open('./new.dat','w') as f:
            f.write(self.flow_json)

#调用 
mySpider = GATE_Spider() 
mySpider.flow_init()

思路很清晰,因为登录工作在程序运行前已经完成,cookie已经存在本地,每次刷新只是简单地GET请求而已。所以在初始化后,只需模拟GET请求,拿到回复后,匹配第一次出现的flow和fee即可,以标准的json格式存储在.dat文件里,每过30s循环一次。

p.s.有意思的是,上网注销窗那里的MB竟然是字符串拼接拼出来的,直接/1024就可以得到的结果却都两个大圈子才拼出来,不知目的在哪里。。。

好了,测试之后,文件里已经把流量,网费,时间都按顺序储存好了。怎么和前端数据可视化互动呢?最好的方式当然是选择js,用网页的形式呈现。不过,为了保证实时性,js需要不间断地向后台发起请求。整个流程应该是由js端驱动的。而我对python作网站后台并不熟悉,无奈之下,只能暂时放弃python爬虫这个主意,选择php爬虫。不过,思路上大同小异,没过多久,php的爬虫后台端就搞定了。

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
<?php
header("Content-type: text/json");
class Spider{

public $flow;
public $fee;
public $time

function __construct(){
$this->flow = 0;
$this->fee = 0;
$this->time = 0;
}
/*
get content from a certain URL
@param $url string
@return string
*/
function getURLcontent($url){
// 初始化
$data = "";
$curl = curl_init();
// 设置cURL 参数
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

// 运行cURL,请求网页
$data = curl_exec($curl);
$this->time = time() * 1000+3600*8000; //to fit UTC time
curl_close($curl);

// 显示获得的数据
//var_dump($data);
return $data;
}
/*
get flow and fee from certain message
@param $content string
@return boolean
*/
function simplifyURLcontent($content){
$flow = 0; $fee = 0;
preg_match('/flow=\'([\S\s]*)\';fsele=\d+;fee=([\S\s]*)\';xsele/', $content, $matches);

if(!$matches)
return false;
else{
//查找流量和费用字符串
$this->flow = intval($matches[1])/1024/1024;
$this->fee  = intval($matches[2]);
return true;
}
}
/*
return json to js
@return mixed
*/
function deliverFlow(){
$ret = array($this->time, $this->fee, $this->flow);
echo json_encode($ret);
}
/*
continous work flow
@return false
*/
function workFlow(){
$ret = $this->getURLcontent("http://10.3.8.211");
$isGet = $this->simplifyURLcontent($ret);
if($isGet) $this->deliverFlow();
return false;
}
}
$gate = new Spider();
$gate->workFlow();
?>

这样,js每次发送.ajax请求时,php就可以返回前端需要的数据了。

这么来看,后台爬虫端应该就这么多了。

数据可视化

好的,现在我们已经可以拿到所需的数据了。关键是怎么去呈现它。最好能有现成的工具来好好利用收集到的数据。前人怎么会没考虑到这点呢,js charts用SVG等方法渲染出来的图表足够用了。这里,我选择了Highcharts。Highcharts的文档很丰富,API文档也很充分,所以有一定web基础的人很容易上手。

Highcharts提供了很多种图表类型,考虑到实时性的需要,我们选择最基础的line类型。这里不再介绍Highcharts的基本信息,感兴趣的可以去它的官网查看。既然后台php已经把json标准化的数据拿过来了,就看Highcharts这边怎么用了。

由于这里各点数据是动态更新的。这里Highcharts的Chart对象里,series需要动态更新。这里写个简单的ajax,向后台index.php请求数据,根据上面的php程序,将得到一个[time,fee,flow]的json类型数据point,第一个自然是x轴数据,第二个是第二条y轴数据,第三个是第一条y轴数据。这里我们用series的addPoint方法把数据新增进去即可。由于图只显示近半小时数据,我们姑且视作50个点,因此这里还要判断点数是否超过50,然,则平移。因此addPoint的第二个第三个参数分别是TRUE和判断是否平移的Boolean值。在得到数据后,setTimeout来实现每约30s请求一次数据。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$.ajax({
url: 'index.php',
success: function(point) {
var series = chart.series[0],
series_f = chart.series[1],
shift = series.data.length > 50,
shift_f = series_f.data.length > 50; // shift if the series is longer than 50
// add the point
chart.series[0].addPoint([point[0],point[2]], true, shift);
chart.series[1].addPoint([point[0],point[1]], true, shift_f);
// call it again after one second
setTimeout(requestData, 20000);
},
cache: false
});

之后的就比较简单了。设置好前端表格的格式就可以看到炫酷的效果了。

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
$(document).ready(function() {
chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
defaultSeriesType: 'spline',
events: {
load: requestData
}
},
title: {
text: 'Campus Network Live Flow Data(ipv4 only)'
},
credits:{
enabled:false
},
xAxis: {
type: 'datetime',
tickPixelInterval: 40,
maxZoom: 50 * 1000
},
yAxis: [{
minPadding: 0.2,
maxPadding: 0.2,
title: {
text: 'Flow (GB)',
margin: 30
}
},{
opposite:true,
minPadding: 0.2,
maxPadding: 0.2,
title: {
text: 'Balance(yuan)',
margin: 30
}
}],
series: [{
name: 'FLow data',
data: []
},{
yAxis: 1,
name: 'Balance data',
` data: []
}]
});
});

嗯~ 看上去还不错。

p.s. 经测试,在后台取的时间不经处理会早8个小时,这是由于UTC和时区的原因,在后台额外加上整8个小时即可。

历史数据

在实时数据做完后,有朋友参与了进来。经过讨论,我们认为做一个历史性数据展示窗口也是必要的。毕竟,不是所有人都愿意开着网页等着数据更新。后台悄悄地采集数据会更友好。于是,下一步开始了。

有了前面的基础后,后面的开发要快许多。不过我们遇到了一个现实的问题。既然我们的小工具是面向不一定懂编程的用户的,那么php做后台就变得有些不现实。所以这里我们决定用windows的shell来写,并推广到不同操作系统。由于篇幅限制,我们将在下篇日志里介绍这种方法。这里仅对前段部分进行讲解。

思路还是类似的,后台采到的数据用csv格式存在.dat或诸如此类的文件里。Highcharts需要读入其中所有的数据,并存入到series的data域里,一次请求,一次性完成,这是不同于上个功能的。

依旧,在Highcharts的chart对象实体化前,得对变量赋值。重点当然在对csv格式的文件读取中。这里用jQuery的get方法,虽然js跨域访问陌生网站很困难,访问本地资源还是没问题的。读入数据后,以’\n’为分隔得到每个点,这里要初始化一个series对象,预存两条数据线的name,yAxis属性。然后.each函数里,split数据by ‘ , ‘, push到series的data域里即可(注意,这里最好对值parseFLoat处理一下较好)。最后将两组series push到chart的options里(注意,需要push两次,因为是两组数据)。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$.get('data.csv', function(data) {
// Split the lines
var lines = data.split('\n');
var series = [{
name: 'Flow Data',
data: []
},{
name: 'Balance Data',
yAxis: 1,
data: []
}];
// Iterate over the lines and add categories or series
$.each(lines, function(lineNo,line) {
var items = line.split(',');
series[0].data.push([parseFloat(items[0]),parseFloat(items[2])]);
series[1].data.push([parseFloat(items[0]),parseFloat(items[1])]);
});
options.series.push(series[0]);
options.series.push(series[1]);
// Create the chart
var chart = new Highcharts.Chart(options); 
});

配合上预先设定好的Highcharts其他属性,综合成一个options初始化一个Highcharts.chart对象即可完成任务。再稍微处理一下前端,将两个功能组合在一起,加上css样式和一个简单的收起js,得到了半成品.

整体代码js如下:

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
115
116
117
118
119
120
121
function requestData() {
$.ajax({
url: 'index.php',
success: function(point) {
var series = chart.series[0],
series_f = chart.series[1],
shift = series.data.length > 50,
shift_f = series_f.data.length > 50; // shift if the series is longer than 50
// add the point
chart.series[0].addPoint([point[0],point[2]], true, shift);
chart.series[1].addPoint([point[0],point[1]], true, shift_f);
// call it again after one second
setTimeout(requestData, 20000);
},
cache: false
});
}
var options = {
chart: {
renderTo: 'container_h',
defaultSeriesType: 'spline',
zoomType: 'x'
},
title: {
text: 'Campus Network History Flow Data(ipv4 only)'
},
credits:{
enabled:false
},
xAxis: {
type:'datetime',
minRange: 150000 // 150 seconds
},
yAxis: [{
minPadding: 0.2,
maxPadding: 0.2,
title: {
text: 'Flow (GB)',
margin: 30
}
},{
opposite:true,
minPadding: 0.2,
maxPadding: 0.2,
title: {
text: 'Balance (yuan)',
margin: 30
}
}],
series: []
};
$.get('data.csv', function(data) {
// Split the lines
var lines = data.split('\n');
var series = [{
name: 'Flow Data',
data: []
},{
name: 'Balance Data',
yAxis: 1,
data: []
}];
// Iterate over the lines and add categories or series
$.each(lines, function(lineNo,line) {
var items = line.split(',');
series[0].data.push([parseFloat(items[0]),parseFloat(items[2])]);
series[1].data.push([parseFloat(items[0]),parseFloat(items[1])]);
});
options.series.push(series[0]);
options.series.push(series[1]);
// Create the chart
var chart = new Highcharts.Chart(options);
});

$(document).ready(function() {
chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
defaultSeriesType: 'spline',
events: {
load: requestData
}
},
title: {
text: 'Campus Network Live Flow Data(ipv4 only)'
},
credits:{
enabled:false
},
xAxis: {
type: 'datetime',
tickPixelInterval: 40,
maxZoom: 50 * 1000
},
yAxis: [{
minPadding: 0.2,
maxPadding: 0.2,
title: {
text: 'Flow (GB)',
margin: 30
}
},{
opposite:true,
minPadding: 0.2,
maxPadding: 0.2,
title: {
text: 'Balance(yuan)',
margin: 30
}
}],
series: [{
name: 'FLow data',
data: []
},{
yAxis: 1,
name: 'Balance data',
data: []
}]
});
});

剩下的部分,我们在下篇文章里继续。下期见 ~

因个人兴趣,从暑期开始做起web后台开发工作。这数月过来,无数次提交,更改,撤销更改等动作,让我深感学习版本控制的必要性。在学长的推荐下,以Git-Book为基础开始学习git。Git-Book是本很好的教材,深入浅出,条理清晰。对比而言,很适合初学者使用。

git为版本控制而生,是DVCS的一种。早期的数据库形式被用户机——CVCS服务器形式所取代。这种模式下,中央宕机会造成成很大损失。考虑到这点,分布式版本控制系统(DVCS)出现,在DVCS下,各用户机亦保存有文件的各版本,即使服务器宕机,也可以通过各用户机恢复。git是DVCS的一种。

git起源于Linux的维护工作。诞生于2005年。建立在Linux经验的基础上,git有几个很明显的特征:追求速度,简单设计,非线性开发需求,完全分布式。因此,它能在简化操作的同时,简化开发者的工作量。

学习git之前,要先建立对git几个特点的认识。这将将极大程度上有利于之后的进展。

  • git记录文件的整体快照而非文件的局部改动。若文件改动,git将记载新文件;反之,git将只储存指向旧文件的指针。
  • git继承了DVCS的特点,操作的本地性极强。不需要联网也可以使用git来保存甚至提交自己所做的改动
  • git十分强调文件数据的完整性,用SHA-1算法得到文件校验得到一个Hash值表示文件。任意文件都有唯一的40bits十六进制字符串对应。
  • git的各种操作绝大多数都是向数据库中添加数据,这意味着几乎所有操作都可以恢复。不用担心覆盖掉正确版本。
  • git管理下的文件只有三种状态——修改,暂存,提交。这点很重要。git只对提交态的文件版本负责。
    在了解了git的基础知识后,就可以在你的主机上安装git了。由于Linux环境下对git的使用较频繁,且我自己也在Linux环境下使用git,下面对Linux做重点介绍。

Linux

源代码安装

先安装依赖包

1
2
3
4
//使用yum的系统
$ yum install curl-devel expat-devel gettext-devel \ openssl-devel zlib-devel
//使用apt-get的系统
$ apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \ libz-dev libssl-dev

之后下载最新源代码并编译安装

1
2
3
4
5
6
$ tar -zxf git-1.7.2.2.tar.gz 
$ cd git-1.7.2.2
$ make prefix=/usr/local all
$ sudo make prefix=/usr/local install
//将git仓库克隆到本地,方便日后更新
$ git clone git://git.kernel.org/pub/scm/git/git.git

傻瓜安装

1
2
3
//区别同上
$ yum install git-core
$ apt-get install git

Mac与Windows

Mac可以通过MacPorts或者下载git图形化工具完成git安装。Windows可以通过安装msysgit使用git命令。

在使用git前,可以通过git config更改个人用户名及电子邮箱地址。这样每次的提交会出现你个人的标识。

cbench用来测试OpenFlow控制器或是OpenFlow网络虚拟化平台实在是太合适了。它的原理是不断循环产生“新的”流(注意是新的),从而用大量的packet-in消息检验控制器或是平台的抗压能力。通过发送packet-in消息和查看flow-mod消息下发的统计消息,将性能指标呈现给用户。

cbench毕竟不是mininet,它的着眼点在调试。所以,虽然cbench也能虚拟出许多多台交换机和大批主机,但是开放的参数很少。不过,这对于测试已经足够了。

安装配置过程如下,官网上的过程可以照搬。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#第一步:安装依赖包 无脑apt-get即可
$ sudo apt-get install autoconf automake libtool libsnmp-dev libpcap-dev
#第二步:安装oflops 因为cbench是在oflops环境下的 不要装在openflow目录下 同目录下即可
$ git clone git://gitosis.stanford.edu/oflops.git
#进入目录 完成更新
$ cd oflops; git submodule init &amp;&amp; git submodule update
#第三步:安装openflow环境 接着进入分支
$ git clone git://gitosis.stanford.edu/openflow.git
$ cd openflow; git checkout -b release/1.0.0 remotes/origin/release/1.0.0
#第四步 安装libconfig包 对cbench编译配置
$ wget http://hyperrealm.com/libconfig/libconfig-1.4.9.tar.gz
$ tar -xvzf libconfig-1.4.9.tar.gz
$ cd libconfig-1.4.9
$ ./configure
$ sudo make &amp;&amp; sudo make install //容易出错权限不够 sudo 或者 sudo chmod 777 别忘了改回去
$ cd ../../netfpga-packet-generator-c-library/
$ sudo ./autogen.sh &amp;&amp; sudo ./configure &amp;&amp; sudo make //同理
$ cd ..
#第五步 可以开始安装cbench了
$ sh ./boot.sh ; ./configure --with-openflow-src-dir=&lt;absolute path to openflow branch&gt;; make
$ sudo make install //同理 安装好即可出现启动文件
$ cd cbench

以上的步骤中容易出两种情况的错误:

  • 权限不够,make报错 “xxxxxx Permission denied”,/usr/local/lib,include,share会出现
  • 依赖包补全,make报错“xxx: command not found”,笔者的错误是g++不识别,sudo apt-get install g++ 再次make就好了
    至此,cbench安装完成。可以使用cbench的各种参数完成你的仿真了。下面这行代码即可执行仿真命令(前提是本机上6633端口得有控制器),由于cbench有默认参数,所以这也是可以执行的。
    1
    2
    $ ./cbench
    $ ./cbench -h
    cbench的参数就都能看到了。参数的详细介绍可以见SDNAP

要想进行测试,还需要安装控制器。如POX或者Floodlight等。这比较简单,POX:

1
2
3
$ cd pox
# 启动
$ ./pox.py openflow.of_01 forwarding.l2_learning

Floodlight要稍复杂一点:

1
2
3
4
5
6
7
8
# 先安装操作环境,Floodlight是用Java写的所以要有标准JDK和构建工具ant Eclipse可选
$ sudo apt-get install build-essential default-jdk ant python-dev eclipse
$ git clone http://github.com/Floodlight/Floodlight.git //文件较大
$ cd Floodlight
$ checkout fl-last-passed-build
$ ant
# 启动
$ java -jar target/Flooglight.jar

根据个人经验,如果之前做个SDN的相关实验,有可能报错端口6633已被占用。这很可能是ovs-controller的开机启动导致的。

1
2
$ ps -aux | grep controller //杀干净吧
$ kill xxxx(进程号)

不过Floodlight的服务器可能还会启动不成功,不过并不影响实验。在Floodlight/src/main/resources/Floodlightdefault.properties中可以更改Floodlight的默认配置。

控制器也安好后,可以根据你的需要来启动测试了。cbench存储了范例,便是如下一行代码的结果。

1
./cbench -c localhost -p 6633 -m 10000 -l 10 -s 16 -M 1000 -t

-M 后的参数是不同MAC地址的主机数。根据个人体验,主机数较多点仿真出来的效果会比较好。

(处于隐私考虑,当事人名字均用化名代替)

“今天白天,我遇到一个人。让我在这个健忘的年代,回忆起了什么。

“在我初中阶段,有个女孩叫小伍,是我的同班同学。她发型蹩脚,其貌不扬,平日话也不多。每次同我讲话时,总会攥紧衣角。那时,我是成绩很好的那种。而她的成绩怎么努力也提高不了。她的问题总是很多,一有问题就会来找我。那时我年方十四,正是初恋的大好时光。小伍芳龄十五。可我却并不喜欢她。我喜欢隔壁班的小宛,却因为害羞不曾表白。这绝不是我现在的风格。转眼间,中考结束提交志愿,我们最后一次回到学校。小伍挤过人群,简单同我寒暄几句,将一张叠好的纸塞在我的手里。‘我猜你一定想要这个吧。’她没再说下去,微笑地又消失在人群里,留下纸上小宛的字体。这是小宛写给小伍的同学录。

“事后,我总觉得她还有什么没说,而那些没说的正是我一直的遗憾。这个遗憾,我从未向她谈起。

“后来,我去了市重点。她去了另一所学校。我以为自己一定再也见不到她了。在这里,我每天都能遇到许多陌生的人。我以为以我那可怜的记忆,一定渐渐就记不起她的名字了。就这样又过了两年。我再一次见到小伍是在网上,好友推荐里出现了她的名字。只是头像我一点都不认识。好奇地点进去,在惊讶中我愣了许久。大图里,她迎着光,长发垂肩,出落成一副亭亭玉立的模样。颤抖着,我羞赧地关掉了网页。深思熟虑下,还是点击了好友申请。到现在,我都能记起揣摩用词时,自己羞耻的样子。

“那时,有股莫名的伤感涌上心头。生活不会重演。我和小伍已不是数年前的关系。这么简单的道理,当时的我却不明白。行将陌路,覆水难收。

“就这样沉默着又过了半年,覆水突然有了重收的希望。她分手了,从每日的状态看得出来。不知为何,她把我选为倾诉的对象。也许是人在悲痛时总会犯傻吧。我陪她一聊一下午,一聊一晚上,一聊一夜。甚至,我们有几次约出来看球赛,对着空无一人的球场,嬉笑,怒骂。那段时光,我学业一落千丈,可是我很开心。她,我想,也很开心吧。在这开心里,我却有一丝的不安。因为我突然发现,除了这短暂的快乐,我没有什么东西可以留住她。

“果然,这快乐始于分手,终于爽约。又一次约去看球赛,空无一人的球场,嬉笑怒骂,只是只有我一个人。她消失了,消失得很彻底。连网上也找不到了。

“再后来,故事似乎开始快进。高考发挥有失水准,大学三年一晃即逝。好不容易,找到份工作糊口。终日西装革履,行走在高楼大厦间,单身。今天并不是双休日,我停下忙碌的脚步,在路边的甜点店要了个甜筒。准备歇会儿再走。这时,身边来了个也买甜筒的姑娘,红帽长裙,拖着拉箱。她转头的时候,我发现她是小伍。

“在那最后一个甜筒的时间里,我用尽浑身解数想让她注意到我。我做着鬼脸,做着各种奇怪的动作,在她身边。而小伍只是惬意地四顾。在这期间,她甚至望了我一眼。却只有礼貌的微笑。最后,她扬了扬手,走向远处的男子,没有一丝回头。就像,身边是个陌生人。

“在这里,我每天都能遇到许多陌生的人。若不是刻意,我也许已记不清她的名字了。在她离去的那一刻,我突然意识到,这离去似乎是必然的。因为我还有什么能留住她的理由呢?

–END–

远处天际射来温暖的光,闪得我睁不开眼。下午的宁静包围着我,追逐着我。

这一瞬,像鸟般飞翔。整个躯体,难言般轻松。整个躯体,飘在空中。

风吹向我下落的方向,呼啸着,不留情面。


猛地,我从梦里惊醒。刚才的情景原来只是个梦。不知为何,感觉如此真实。房间里寂静无声。斗室边的窗户外,有微弱的光。是个午后。

奇怪,身边怎么没人。

我洗了把脸,趿拉着拖鞋。推开一楼的门,外面是单元楼的小院,可是依然一个人都没看到。我一扭头,望着通向二楼的台阶。顿时,一股恐惧的感觉涌上来。

仿佛有人在悄悄说:“上来吧上来吧。”

我强忍住好奇,慢慢走向院外。院口的铁闸门半开着,依旧没有人。就在我迈步准备走出小院时,突然听到了人声。

“你醒了?”


猛地,我从梦里惊醒。什么?刚才又是个梦?睁开眼,母亲穿着围裙,正在床边笑着唤我。斗室边的窗户外,有微弱的光。是个午后。

我拿水抹了把脸,趿拉着拖鞋。推开一楼的门,外面是单元楼的小院,几个同单元的大妈正惬意着,仰卧在摇椅上闲聊。我仰起头,望向天空。高耸的单元楼压住我视野。

一扭头,又看见通向二楼的台阶。

那股恐惧感更强烈了。

一阵困意袭来,我无力地瘫在地上。

又是个梦吗?


我睁开眼。躺在那张熟悉的床。果然又是梦。房间里寂静无声。斗室边的窗户外,有微弱的光。是个午后。

奇怪,身边又没有人了。

推开卧室的门,墙上的时钟静止在13:41.厨房里,泡着水的电饭锅没有人管。几只洗干净的碗就放在池子边。一种诡异的吸引力愈发明显了。

看着通向二楼的台阶,脑海即将褪色的记忆,一点点聚合。无边的恐惧从身边包围。

再给我点时间,我就能弄清楚这一切了。

时间不多了。

眼前,院墙上的爬山虎一点点模糊。


我睁开眼,逃离那张熟悉的床。房间里寂静无声。斗室边的窗户外,有微弱的光。是个午后。墙上的时钟静止在13:41。

我套上拖鞋,朝母亲打声招呼,朝院外匆匆走去。

然而每一步,都比我想象的要艰难许多。通向二楼的台阶始终牵引着我,让我忍不住回头。

脑海里的记忆慢慢聚合,一阵阵剧痛袭来。

终于。我坐在院门口,无法动弹。


不知又过了多少次,有的时候,母亲在;有的时候又不知去了哪里。唯一不变的是墙上的挂钟,精准无比地指向13:41。

我用尽力量,企图远离台阶,走出院门。可是从没成功过。

而且,我隐约感觉,自己的力量一次不如一次。

脑海里,记忆一点点拼凑。消耗着我所剩无几的精力。


我爬下床,倚着墙,挤开卧室的门。墙上的时钟静止在13:41。我光脚走出门。终于倒在门口,正视着来自台阶的恐惧。

究竟为什么?!

模糊不清的意识里,一下子,回忆突然聚拢还原,一刹后又瞬间消湮。


我随着自己走上了台阶,一楼,二楼,三楼……


终于我意识到,这一切已无法挽回。

可是连流泪后悔的时间,都没有了。


我看着自己走上天台。看着自己纵身一跃。

远处天际射来温暖的光,闪得我睁不开眼。下午的宁静包围着我,追逐着我。

这一瞬,像鸟般飞翔。整个躯体,难言般轻松。整个躯体,飘在空中。

风吹向我下落的方向,呼啸着,不留情面。


–END–

0%