Flask下载动态生成文件加一个简单的进度条

前一段码代码的时候碰见了这么一个情况,用户需要点击一个button然后下载一个比较大的动态生成的文件。flask里有一个send_file方法,可以把一个文件返回给客户端。可以先把动态生成的内容写到文件中,然后再用发送静态文件的方法,用send_file方法把这个文件返回,然后再把这个文件删除。不过这样的话就多了磁盘IO这一段时间,感觉很多此一举。

然后看了看send_file方法的代码,实际上是调用了一个FileWrapper的类,返回一个iterator,然后迭代返回块(默认的大小是4KB),和普通的Streaming的方法也没什么不一样的,只是包装了一下。所以直接用生成器构造Response即可。代码如下,关键的地方就只有添加一个Content-Length头,客户端每隔一段时间就可以计算一下当前的下载进度。

from flask import Flask
from flask import Response
from flask import render_template
from flask import make_response
from gevent.pywsgi import WSGIServer
app = Flask(__name__)
app.template_folder = "."

@app.route("/generate-on-fly")
def gen_on_fly():
def gen():
for i in xrange(200000):
yield "l"+"o"*100+"g\n"
resp = Response(gen())
resp.headers["Content-Length"] = resp.calculate_content_length()
return resp

@app.route("/")
def root():
resp = make_response(render_template("./root.html"))
resp.headers["Cache-Control"] = "no-store"
return resp

if __name__ == "__main__":
server = WSGIServer(('', 5000), app)
server.serve_forever()

根目录会返回一个html页面,页面上只有一个进度条和一个按钮。点击按钮会向/generate-on-fly发送请求,然后进度条开始滚动。root.html的内容如下。

<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script>
function updateProgress(evt)
{
var percentComplete = (evt.loaded / evt.total)*100;
$('#progressbar').attr("aria-valuenow", percentComplete);
$('#progressbar').attr("style", "width: "+percentComplete+"%;");
}
function sendreq(evt)
{
var req = new XMLHttpRequest();
req.onprogress = updateProgress;
req.open('GET', 'http://localhost:5000/generate-on-fly', true);
req.send();
}
</script>
</head>
<body>
<div class="row" style="margin: 30px">
<div class="col-lg-6">
<div class="progress">
<div id="progressbar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0"
aria-valuemax="100"
style="width: 0%;">
</div>
</div>
</div>
<button type="button" class="btn" onclick="sendreq()">Download</button>
</div>
</body>
</html>

过程大致就是每隔一段时间,就会进入req的onprogress绑定的回调,回调里面计算一下当前下载的百分比,然后更新一下进度条的状态,也不是很复杂。

算法学习笔记(5):Dijkstra

在刷Hackerrank的时候碰到了这道题,其本身倒是十分普通的一道题,照本宣科的把Dijkstra实现一遍就可以做出来了。在讨论区和别人讨论发现的时间复杂度也是会被Accept的。

如果使用数组储存所有unvisited点的距离,在找当前最近点的时候,会需要遍历整个数组,而这是的时间复杂度。如果使用一个最小优先级队列,那么只需要将队首的元素取出,然后min-heapify一下,保持最小堆的性质,而这是的时间复杂度。能够显著减少时间消耗。

但是如果使用最小堆的话,降低堆中元素的优先级的操作就很麻烦了。首先如果想要找到堆中的某个元素就需要的时间复杂度,而修改了这个元素的优先级之后,想要维持最小堆的性质又要的时间复杂度。所以整体而言就变得和没有使用最小堆一样了。

为了解决这个问题有一个弥补的方法。就是在一个散列表中存储优先级队列中对应点的元素的handle。

如果想要降低某个点的优先级,那么可以通过查询散列表,获得最小堆中对应元素的handle,然后直接标志该点已经被删除(但是不实际从堆中删除),然后在储存堆的数组的最后新添加一个储存新优先级和该点的元素。标志删除的操作因为没有破坏最小堆的性质,只是 的时间复杂度,而最小堆中插入一个元素是 的时间复杂度。最后就实现了 的降低优先级的操作。

如果使用python实现的话,python的官方文档中简单介绍了使用heapq实现最小优先级队列的方法。

不过残念的是,add_task()方法的第一行就出现了个遍历dict,而这是的,所以推荐额外再使用一个set来维护entry_finder.keys(),这样查找和删除操作就都是 了。

另外一个有意思的事情是,heappush()原地的,所以pq会在原地修改。而pq中保存的是[distance, vertex]的list,而list是mutable的。所以在remove_vertex()把entry[-1]也就是vertex标志城REMOVED了之后,pq中的对应entry会自动修改。

最后挂上自己的代码。

# Enter your code here. Read input from STDIN. Print output to STDOUT
from heapq import *
 
def add_vertex(vertex, distance=0):
    if vertex in entry_set:
        remove_vertex(vertex)
    entry = [distance, vertex]
    entry_finder[vertex] = entry
    entry_set.add(vertex)
    heappush(pq, entry)
 
def remove_vertex(vertex):
    entry = entry_finder.pop(vertex)
    entry[-1] = REMOVED
    entry_set.remove(vertex)
 
def pop_vertex():
    while pq:
        priority, vertex = heappop(pq)
        if vertex is not REMOVED:
            del entry_finder[vertex]
            return priority, vertex
 
T = int(raw_input())
for T_ in range(T):
    pq = []
    entry_finder = {}
    REMOVED = "REMOVED"
 
    N, M = [int(i) for i in raw_input().split(" ")]
    connection = [[] for conn_ in range(N)]
    for M_ in range(M):
        x, y, r = [int(i)-1 for i in raw_input().split(" ")]
        connection[x].append([y, r+1])
        connection[y].append([x, r+1])
    S = int(raw_input())-1
     
    entry_set = set()
    unvisited = set()
    distance = {}
 
    for node in range(M):
        unvisited.add(node)
        if node!=S:
            distance[node] = float("Inf")
            add_vertex(vertex = node, distance = float("Inf"))
        else:
            distance[node] = 0
            add_vertex(vertex = node, distance = 0)
 
    while len(unvisited)!=0:
        current_distance, current_node = pop_vertex()
        if current_distance==float("Inf"):
            break
        for node, edge in connection[current_node]:
            if node in unvisited:
                new_distance = current_distance + edge
                if new_distance < distance[node]:
                    distance[node] = new_distance
                    add_vertex(node, new_distance)
                distance[node] = new_distance if new_distance < distance[node] else distance[node]
        unvisited.remove(current_node)
 
    for node in range(N):
        if node!=S:
            if distance[node]==float("Inf"):
                print -1,
            else:
                print distance[node],
    print

How to Install Pyeemd on Windows in 3 Steps

1. Prepare Compiling Environment

Download MSYS2 of your windows version from here.

Follow the instructions on the homepage to update the system. Run commands listed below.

$ update-core
$ pacman -Suy

If you have trouble connecting to the official repository of MSYS2, modify the configuration files of pacman, which are located at /etc/pacman.d/.

For those living in China, I recommend a MSYS2 mirror hosted by LUG@USTC. Just follow the instructions on their wiki.

We need to install the mingw64 toolchain (suppose we use 64-bit windows) and some tools like tar and make. The mingw64 toolchain contains compiler and other compiling-related tools. Just run the command below.

$ pacman -S mingw-w64-x86_64-toolchain tar make

After the installation, close the MSYS2 window and run mingw64_shell.bat located at MSYS2 root directory. You may notice the word in purple before tilde changes from MSYS to
MINGW64. According the introduction of MSYS2, the MINGW64 shell is more natively oriented. Meanwhile, and the MSYS2 shell provides some linux features.

The mingw subsystems provide native Windows programs and are the main focus of the project. These programs are built to co-operate well with other Windows programs, independently of the other subsystems.

The msys2 subsystem provides an emulated mostly-POSIX-compliant environment for building software, package management, and shell scripting. These programs live in a virtual single-root filesystem (the root is the MSYS2 installation directory). Some effort is made to have the programs work well with native Windows programs, but it's not seamless.

2. Compile GSL and Libeemd

Libeemd depends on GNU Scientific Library (GSL). Run following command in MINGW64 shell to download GSL source code.

$ curl -o gsl-latest.tar.gz http://mirror.clarkson.edu/gnu/gsl/gsl-latest.tar.gz
$ tar zxvf gsl-latest.tar.gz

Change directory to the GSL directory and compile it. (Suppose we have GSL 2.1 here.)

$ cd gsl-2.1
$ ./configure
$ make
$ make install

GSL is installed to /usr/local/ without assigning --prefix flag. So we need to modify PKG_CONFIG_PATH variable before compiling libeemd so compiler may find GSL libraries.

$ export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig

Then we clone libeemd source code from bitbucket and compile it. A makefile is included so we don't need to run a configure script.

$ git clone https://bitbucket.org/luukko/libeemd.git
$ cd libeemd
$ make

After compiling, you may use file command to check out file type of libeemd.so. It's a dll file! It sounds weird but I will continue with this convention. Otherwise you may modify Makefile and remake by yourself.

$ file libeemd.so
libeemd.so: PE32+ executable (DLL) (console) x86-64, for MS Windows

3. Install pyeemd module to Anaconda2

Anaconda is a great platform based on python for data analytic. It contains lots of modules and make it much simpler to do calculation.

But there're no EMD related modules included in Anaconda by default. And I can't find any one in pypi or by conda search command. (There's one named emd in pypi actually. But there's no document and I don't think it reliable.) That's why I write this blog.

If you tried googling about libeemd and windows before reading this blog, you may have found this issue. Owner of libeemd stated that using libeemd on windows may be unpleasant.

Please also note that installing libeemd on Windows is currently quite unsupported – I haven't tested it, and I have no way to test it.

This unpleasantness may be caused by compiling, which we have already fixed, and dll loading. Library searching is different in linux and windows. I know little about os. So it may be appropriate to quote from tldp.org, rather than gibberishing by myself.

The interface used by Linux is essentially the same as that used in Solaris, which I'll call the ''dlopen()'' API. However, this same interface is not supported by all platforms; HP-UX uses the different shl_load() mechanism, and Windows platforms use DLLs with a completely different interface.

It means, instead of appointing library search work to os, that we need to load dll files libeemd.so depending on on our own. Luckily finding dll dependency and loading them in python is not hard.

You may use objdump to find dll dependency as shown below.

$ objdump -p libeemd.so | grep "\.dll"
        DLL Name: KERNEL32.dll
        DLL Name: msvcrt.dll
        DLL Name: libgomp-1.dll
        DLL Name: libgsl-19.dll

There's a handy tool named dependencywalker to find dll dependency as well. After opening the dll file, namely libeemd.so here, dependencies will be shown in the top-left column.

dll

We have known that libeemd.so depends on kernel32.dll, msvcrt.dll, libgomp-1.dll and libgsl-19.dll. Kernel32.dll and msvcrt.dll are located in C:/windows/system32 and we don't need to worry about them. We have to worry about libgomp-1.dll's and libgsl-19.dll's dependencies. (pretty messy, right?)

Do basically same to them as to libeemd.so. And we get this dependency tree. (You don't get it directly. I draw this manually.)

libeemd.so
|-KERNEL32.dll
|-msvcrt.dll
|-libgomp-1.dll
  |-KERNEL32.dll
  |-msvcrt.dll
  |-libwinpthread-1.dll
    |-KERNEL32.dll
    |-msvcrt.dll
  |-user32.dll
  |-libgcc_s_seh-1.dll
    |-KERNEL32.dll
    |-msvcrt.dll
    |-libwinpthread-1.dll
|-libgsl-19.dll
  |-KERNEL32.dll
  |-msvcrt.dll
  |-user32.dll
  |-libgslcblas-0.dll
    |-KERNEL32.dll
    |-msvcrt.dll

This tree ends in kernel32.dll, msvcrt.dll and user32.dll. They are all located in C:/windows/system32, which is in system searching path. Other dlls are located in MSYS2 directory. You can find them in MINGW64 shell by find / -name command.

After all these dlls being found, we may begin installing pyeemd to Anaconda.

Change directory to libeemd/pyeemd directory and install pyeemd module to Anaconda2/Lib/site-packages directory.

Make sure you're using Anaconda2's python when running the setup.py script. You can use absolute path in MINGW64 shell like /c/Users/juiceyang/Anaconda/python . (You should replace with your own username here.)

$ /c/Users/juiceyang/Anaconda2/python setup.py install

Change directory to Anaconda2/Lib/site-packages/pyeemd, you may find they are already there. But once you try to run pyeemd.py, a windows error 126 prompt out. That's because python can't find dlls libeemd.so depending on. We may fix this by insert some lines into pyeemd.py. It looks like this after inserting.

# Load libeemd.so
# ---------new lines start from here
ctypes.WinDLL("kernel32.dll")
ctypes.WinDLL("msvcrt.dll")
ctypes.WinDLL("user32.dll")
ctypes.WinDLL("C:\\msys64\\mingw64\\bin\\libwinpthread-1.dll")
ctypes.WinDLL("C:\\msys64\\mingw64\\bin\\libgcc_s_seh-1.dll")
ctypes.WinDLL("C:\\msys64\\mingw64\\bin\\libgomp-1.dll")
ctypes.WinDLL("C:\\msys64\\home\\YOUR_USERNAME\\gsl-2.1\\cblas\\.libs\\libgslcblas-0.dll")
ctypes.WinDLL("C:\\msys64\\home\\YOUR_USERNAME\\gsl-2.1\\.libs\\libgsl-19.dll")
# ctypes.WinDLL("C:\\msys64\\home\\YOUR_USERNAME\\libeemd\\libeemd.so") --> This line is commented out. Load libeemd.so in next 3 lines.
# ---------new lines end
_LIBDIR = os.path.dirname(os.path.realpath(__file__))
_LIBFILE = os.path.join(_LIBDIR, "libeemd.so")
_libeemd = ctypes.CDLL(_LIBFILE)

ctypes.WinDLL() function loads dlls. First three dlls are in system32 folder so we need no absolute path. Other dlls is located by absolute path.

Then run following snippet in Jupyter notebook to check out whether it's working!

%matplotlib inline
from pyeemd import ceemdan
from pyeemd.utils import plot_imfs
import matplotlib.pyplot as plt
import numpy as np

y = np.sin(2*np.pi*np.linspace(0,1,1000))+np.sin(20*np.pi*np.linspace(0,1,1000))
plt.plot(y)
plt.show()
imfs = ceemdan(y, S_number=4, num_siftings=50)
plot_imfs(imfs)
plt.show()

Here's my output. Have fun using pyeemd!

notebook

MongoDB上手笔记4——在很多数据中进行查询

上一次把简单的Restful API搭建起来了,接下来让我们往数据库里面填充一些数据。

俗话说的好,一天一个苹果,考不上博士。假设小明每天吃一个苹果,为了记录截止到某一天小明一共吃了多少个苹果,我在db logs的collection applesIAte中,记录了截止到某一天小明一共吃了多少苹果。

假设小明命中注定不会读博,所以小明骨骼惊奇的生下来就不吃奶,而是出生第一天就吃苹果,每天以苹果为生(好像一个并吃不饱的样子,请不要在意这些细节)。然后每天为了建设祖国而奋斗,一直活到了100岁,最后在100岁生日的当晚因为看到了祖国的建设成果过于兴奋(以及吃了太多的苹果)而含笑九泉。

按照这以上假设,我写了个脚本,记录了无私而强韧的小明的不平凡的一生中吃了的苹果数量。这段脚本中用了个insert_many()的方法,在一次性写入很多条document的时候比较快。

from flask_pymongo import MongoClient
mongo = MongoClient(host="localhost", port=27999)
db = mongo.logs
auth_res = db.authenticate("readwriter", "123456")
apples = [{
    "day": day,
    "apple": day
} for day in range(1, 100*365+1)]
db.applesIAte.insert_many(apples)

然后我们看一看小明这光辉的一生。

> db.applesIAte.stats()
{
        "ns" : "logs.applesIAte",
        "count" : 36500,
        "size" : 1752064,
        "avgObjSize" : 48,
        "numExtents" : 5,
        "storageSize" : 2793472,
        "lastExtentSize" : 2097152,
        "paddingFactor" : 1,
        "paddingFactorNote" : "paddingFactor is unused and unmaintained in 3.0. It remains hard coded to 1.0 for compatibility only.",
        "userFlags" : 1,
        "capped" : false,
        "nindexes" : 1,
        "totalIndexSize" : 1193696,
        "indexSizes" : {
                "_id_" : 1193696
        },
        "ok" : 1
}

恩,小明活了36500天(原谅我忘了闰年这事),真是了不起!

在回顾小明这光辉的一生(中吃了多少苹果)之前,让我们先做一些小手脚。这样我们才能看看我们能以多快的速度回顾(查询)了小明的一生(丫可是活了100年口阿!)

> db.setProfilingLevel(2)
{ "was" : 0, "slowms" : 100, "ok" : 1 }

上面这句话的意思是,我们所有的回顾小明一生的行为(查询)的相关信息都会被记录下来。

接下来我们看看小明在10000天的时候一共吃了多少苹果。(此处也可以用findOne({day: 10000}),不过并不影响结论)

> db.applesIAte.find({day: 10000})
{ "_id" : ObjectId("56a9c39cabf8322aa0828dec"), "day" : 10000, "apple" : 10000 }

显然他吃了10000个,awesome!那让我们看看我们花了多长时间发现他吃了这么多苹果。

> db.system.profile.find()
{
    "op":"query",
    "ns":"logs.applesIAte",
    "query":{
        "day":10000
    },
    "ntoreturn":0,
    "ntoskip":0,
    "nscanned":0,
    "nscannedObjects":36500,
    "keyUpdates":0,
    "writeConflicts":0,
    "numYield":285,
    "locks":{
        "Global":{
            "acquireCount":{
                "r":NumberLong(572)
            }
        },
        "MMAPV1Journal":{
            "acquireCount":{
                "r":NumberLong(286)
            }
        },
        "Database":{
            "acquireCount":{
                "r":NumberLong(286)
            }
        },
        "Collection":{
            "acquireCount":{
                "R":NumberLong(286)
            }
        }
    },
    "nreturned":1,
    "responseLength":62,
    "millis":20,
    "execStats":{
        "stage":"COLLSCAN",
        "filter":{
            "day":{
                "$eq":10000
            }
        },
        "nReturned":1,
        "executionTimeMillisEstimate":10,
        "works":36502,
        "advanced":1,
        "needTime":36500,
        "needFetch":0,
        "saveState":285,
        "restoreState":285,
        "isEOF":1,
        "invalidates":0,
        "direction":"forward",
        "docsExamined":36500
    },
    "ts":    ISODate("2016-01-28T07:53:57.857    Z"),
    "client":"127.0.0.1",
    "allUsers":[
        {
            "user":"boss",
            "db":"admin"
        },
        {
            "user":"readwriter",
            "db":"logs"
        }
    ],
    "user":"readwriter@logs"
}

上面这一大段基本上都没啥用,我们只需要两条——"millis":20,"docsExamined":36500。也就是查询一共花了20ms,查看了36500条document才找到了这条记录。(好慢!)

如果小明不是一个普通的人,是一个脱离了低级趣味的神仙,他活了一万年,甚至十万年前他就和猛犸象谈笑风生了(可他依旧不想读博)。那么我们的记录会越来越多,查询速度会越来越慢。

而我们为days加上索引可以有效的解决这个问题。因为day是单调增长的,所以参数是{day: 1}。

> db.applesIAte.createIndex({day: 1})
{
        "createdCollectionAutomatically" : false,
        "numIndexesBefore" : 1,
        "numIndexesAfter" : 2,
        "ok" : 1
}
> db.applesIAte.stats()
{
        "ns" : "logs.applesIAte",
        "count" : 36500,
        "size" : 1752064,
        "avgObjSize" : 48,
        "numExtents" : 5,
        "storageSize" : 2793472,
        "lastExtentSize" : 2097152,
        "paddingFactor" : 1,
        "paddingFactorNote" : "paddingFactor is unused and unmaintained in 3.0. It remains hard coded to 1.0 for compatibility only.",
        "userFlags" : 1,
        "capped" : false,
        "nindexes" : 2,
        "totalIndexSize" : 2117584,
        "indexSizes" : {
                "_id_" : 1193696,
                "day_1" : 923888
        },
        "ok" : 1
}

从stats()返回的结果来看,day的索引确实是加上了。那么结果如何呢?

{
    "op":"query",
    "ns":"logs.applesIAte",
    "query":{
        "day":20000
    },
    "ntoreturn":0,
    "ntoskip":0,
    "nscanned":1,
    "nscannedObjects":1,
    "keyUpdates":0,
    "writeConflicts":0,
    "numYield":0,
    "locks":{
        "Global":{
            "acquireCount":{
                "r":NumberLong(2)
            }
        },
        "MMAPV1Journal":{
            "acquireCount":{
                "r":NumberLong(1)
            }
        },
        "Database":{
            "acquireCount":{
                "r":NumberLong(1)
            }
        },
        "Collection":{
            "acquireCount":{
                "R":NumberLong(1)
            }
        }
    },
    "nreturned":1,
    "responseLength":62,
    "millis":0,
    "execStats":{
        "stage":"FETCH",
        "nReturned":1,
        "executionTimeMillisEstimate":0,
        "works":2,
        "advanced":1,
        "needTime":0,
        "needFetch":0,
        "saveState":0,
        "restoreState":0,
        "isEOF":1,
        "invalidates":0,
        "docsExamined":1,
        "alreadyHasObj":0,
        "inputStage":{
            "stage":"IXSCAN",
            "nReturned":1,
            "executionTimeMillisEstimate":0,
            "works":2,
            "advanced":1,
            "needTime":0,
            "needFetch":0,
            "saveState":0,
            "restoreState":0,
            "isEOF":1,
            "invalidates":0,
            "keyPattern":{
                "day":1
            },
            "indexName":"day_1",
            "isMultiKey":false,
            "direction":"forward",
            "indexBounds":{
                "day":[
                    "[20000.0, 20000.0]"
                ]
            },
            "keysExamined":1,
            "dupsTested":0,
            "dupsDropped":0,
            "seenInvalidated":0,
            "matchTested":0
        }
    },
    "ts":    ISODate("2016-01-28T08:05:46.904    Z"),
    "client":"127.0.0.1",
    "allUsers":[
        {
            "user":"boss",
            "db":"admin"
        },
        {
            "user":"readwriter",
            "db":"logs"
        }
    ],
    "user":"readwriter@logs"
}

结果发现"docsExamined":1,"millis":0。我们只查询了一条数据就直接找到了到20000天的时候小明吃了多少苹果,花的时间更是小到近似于0了!所以说当小明成仙之后,我们必须对day加上索引才能知道他为了不读博吃了多少苹果XD。

后记:最近看了一本书,作者比鸟哥还谐,整本书写的很像《春物》这种轻小说的风格。于是尝试着『学习』了一下这种风格,试过之后感觉有些浮夸啊XD

MongoDB上手笔记3——写个简单的Restful API读写数据库

上一篇中设置好了db logs的两个用户,并尝试往collection gen1中写入了一条简单的document。使用上一篇中的同样的方法,又写入了新的两条document,现在gen1中有了三条document。

> db
logs
> db.gen1.find()
{ "_id" : ObjectId("56a0daabcf03a917cb662aef"), "time" : 1, "value" : 1 }
{ "_id" : ObjectId("56a0dab3cf03a917cb662af0"), "time" : 2, "value" : 2 }
{ "_id" : ObjectId("56a0dabacf03a917cb662af1"), "time" : 3, "value" : 3 }

在命令行中写js语句来操作mongoDB自然是可行的,可是有很多情况需要使用别的语言来读取数据库。js可能缺少某些别人造好的轮子,比如我想用python来做科学计算,数据存在mongoDB里面,我就需要用python的驱动来操作数据库。

提供这个功能的轮子已经有很多了,mongoEngine和pymongo之类的。但是代码变多之后,感觉维护变得比较麻烦。代码中往往操作数据库和进行计算的部分混合在一起,看着条理不是很清晰。

所以想是不是可以将两部分分开来做,中间用个什么东西连接起来。后来知道了很Fancy(大概?)的Restful API,于是想着可以把各个部分分开,中间用http连接起来。

于是用flask和做了个十分简单的Restful API,功能就只有读取db logs的collection gen1中的最新一条数据以及向其中写数据。

api的验证部分使用了最简单的basic http authentication。request header中的auth部分就只是使用了base64加密而已(都不能算得上加密吧),十分简陋,完全没有安全性。每次请求都会创建一个MongoClient,简单测试了一下,目前没看出来有什么性能影响。

# 创建MongoClient
mongo = MongoClient(host="localhost", port=27999)
# 使用创建的对象验证身份
mongo["logs"].authenticate(request.authorization.username, request.authorization.password)

以下就是所有的代码了,一共只有50行。

simple_restful.py

from flask import request
from flask_restful import Resource
from bson.json_util import loads, dumps
from flask_pymongo import MongoClient
from pymongo.errors import OperationFailure
from flask import Flask
from flask_restful import Api


class Data(Resource):
    def get(self):
        mongo = MongoClient(host="localhost", port=27999)
        try:
            mongo["logs"].authenticate(request.authorization.username, request.authorization.password)
            last_log = mongo["logs"]["gen1"].find().sort("_id", -1)[0]
            return {
                       "err": "False",
                       "message": "Successfully get data",
                       "result": {"data": dumps(last_log)}
            }
        except OperationFailure, err:
            return {
                "err": "True",
                "message": "Failed getting data",
                "result": err.details
            }

    def post(self):
        mongo = MongoClient(host="localhost", port=27999)
        try:
            mongo["logs"].authenticate(request.authorization.username, request.authorization.password)
            data = request.data
            l = loads(data)
            result = mongo["logs"]["gen1"].insert_one(l["data"])
            return {
                "err": "False",
                "message": "Successfully post data",
                "result": {"data": dumps(l["data"]), "ack": str(result.acknowledged)}
            }
        except OperationFailure, err:
            return {
                "err": "True",
                "message": "Failed post data",
                "result": err.details
            }

app = Flask(__name__)
api = Api(app)
api.add_resource(Data, '/data')
app.run(debug=True)

使用curl简单测试一下吧。

>curl -v -H "Content-Type: application/json" --user readwriter:123456 http://localhost:5000/data
* Adding handle: conn: 0x140cf10
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x140cf10) send_pipe: 1, recv_pipe: 0
* About to connect() to localhost port 5000 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
* Server auth using Basic with user 'readwriter'
> GET /data HTTP/1.1
> Authorization: Basic cmVhZHdyaXRlcjoxMjM0NTY=
> User-Agent: curl/7.33.0
> Host: localhost:5000
> Accept: */*
> Content-Type: application/json
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Type: application/json
< Content-Length: 189
< Server: Werkzeug/0.11.3 Python/2.7.10
< Date: Thu, 21 Jan 2016 13:59:24 GMT
<
{
    "err": "False",
    "message": "Successfully get data",
    "result": {
        "data": "{\"_id\": {\"$oid\": \"56a0dabacf03a917cb662af1\"}, \"value\":
3.0, \"time\": 3.0}"
    }
}
* Closing connection 0

确实读出来了最新的一条document。再写入一条document试试。

C:\Users\Juiceyang\Desktop>curl -v -H "Content-Type: application/json" --user re
adwriter:123456 http://localhost:5000/data -X POST -d "{\"data\":{\"time\":4,\"v
alue\":4}}"
* Adding handle: conn: 0x85cf10
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x85cf10) send_pipe: 1, recv_pipe: 0
* About to connect() to localhost port 5000 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
* Server auth using Basic with user 'readwriter'
> POST /data HTTP/1.1
> Authorization: Basic cmVhZHdyaXRlcjoxMjM0NTY=
> User-Agent: curl/7.33.0
> Host: localhost:5000
> Accept: */*
> Content-Type: application/json
> Content-Length: 29
>
* upload completely sent off: 29 out of 29 bytes
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Type: application/json
< Content-Length: 210
< Server: Werkzeug/0.11.3 Python/2.7.10
< Date: Thu, 21 Jan 2016 14:02:58 GMT
<
{
    "err": "False",
    "message": "Successfully post data",
    "result": {
        "ack": "True",
        "data": "{\"_id\": {\"$oid\": \"56a0e512612bf10dcce366d7\"}, \"value\":
4, \"time\": 4}"
    }
}
* Closing connection 0

结果来看也确实写入成功了。

MongoDB上手笔记2——新建用户并设置权限

上一篇说到了把MongoDB安装完毕,并设置了开机启动服务。接下来记录下怎么创建用户,并通过MongoDB的权限管理功能进行管理。

mongodb的用户权限管理的方式是分组的,不同的用户组有着不同的权限,对应的就是可以执行不同的命令。

形象地说就是警察用户组的只有抓捕的权力,医生用户组的只有开处方的权力等等。MongoDB的用户组的设置也秉承了这样的方法,在保证能正常使用的前提下,只给特定用户最少的权力。

首先,先启动mongod,此处使用配置文件的启动方法。

mongod --config E:/MongoDB/mongod.conf

然后连接mongodb。

mongo 127.0.0.1:27999
show dbs
use admin

这个时候可以看到成功的连接了进去,只有一个local的数据库。然后切换到admin数据库,准备创建用户。

MongoDB内置了许多类型的用户类型,大致分为了几类:

  • 单个数据库用户(Database User Roles)
  • 单个数据库管理员(Database Administration Roles)
  • 集群管理员(Cluster Administration Roles)
  • 备份恢复管理员(Backup and Restoration Roles)
  • 全部数据库的管理员(All-Database Roles)
  • 超级用户(Superuser Roles)

不同的用户类别下都有若干个用户类型,分别对应的这不同的权力。比如现在有一个数据库里记录着不同时间段的测量数据,我们想把特定时间段的测量数据读出来,然后在脚本里输出成csv文件,我们就需要一个read用户就够了,而不需要一个root用户(对应的就是上面说的『最少权力』的原则)。

回到正题,简单的举个例子,我们需要一个read用户用来读测量数据用来显示输出,一个readWrite用户用来写数据进入数据库,还有一个权力最大的用户来管理用户和数据库。然我就像建立一个公司一样,我们先任命一个boss,然后再由boss找员工(当然如果这个公司只有一个人的话,boss又要当老板,又要当苦力,那么在roles里面填多个role就可以了)。

db.createUser({user: "boss",pwd: "123456",roles:[{ role: "userAdminAnyDatabase", db: "admin" }]})

我们创建了一个叫boss的用户,他的角色是userAdminAnyDatabase,。从名字上也能看出来,userAdminAnyDatabase类型的用户可以管理所有数据库下的用户。(如果想在创建这个用户之前就开启权限管理,则需要在配置文件中开启localhost exception)

完成了用户管理员的创建之后就可以重新启动启用了权限管理的MongoDB了(此处在管理员权限的cmd中安装了mongoDB的服务)。

mongod --config E:/MongoDB/mongod.conf --auth --install

如果这个时候直接登陆的话,会发现有的命令是无法执行的,因为没有这个权限。比如最简单的显示所有的数据库都无法执行。

C:\Users\Juiceyang>mongo 127.0.0.1:27999/admin
MongoDB shell version: 3.0.6
connecting to: 127.0.0.1:27999/admin
> db
admin
> show dbs
2015-12-13T20:29:44.926+0800 E QUERY    Error: listDatabases failed:{
        "ok" : 0,
        "errmsg" : "not authorized on admin to execute command { listDatabases:
1.0 }",
        "code" : 13
}
    at Error (<anonymous>)
    at Mongo.getDBs (src/mongo/shell/mongo.js:47:15)
    at shellHelper.show (src/mongo/shell/utils.js:630:33)
    at shellHelper (src/mongo/shell/utils.js:524:36)
    at (shellhelp2):1:1 at src/mongo/shell/mongo.js:47
> ^C
bye

而登录了之后,就获得了部分权限,比如可以看都有哪些数据库了。

C:\Users\Juiceyang>mongo 127.0.0.1:27999/admin -u boss -p 123456
MongoDB shell version: 3.0.6
connecting to: 127.0.0.1:27999/admin
> show dbs
admin  0.078GB
local  0.078GB

之后就可以开始创建其他的用户了,创建的方法和创建boss的方法一样。假设我们需要对logs数据库进行读写。

use logs
db.createUser({user: "reader",pwd: "123456",roles:[{ role: "read", db: "logs" }]})
db.createUser({user: "readwriter",pwd: "123456",roles:[{ role: "readWrite", db: "logs" }]})

之后尝试用reader用户往gen1集合写一次数据。

> db.gen1.insert({"time":1, "value":1});
WriteResult({
        "writeError" : {
                "code" : 13,
                "errmsg" : "not authorized on logs to execute command { insert:
\"gen1\", documents: [ { _id: ObjectId('566d691840d302a5c4e9cfc6'), time: 1.0, v
alue: 1.0 } ], ordered: true }"
        }
})

发现无法写入,原因是没有权限执行insert。

之后换readwriter尝试一次。

> db.auth("readwriter","123456")
1
> db.gen1.insert({"time":1, "value":1});
WriteResult({ "nInserted" : 1 })

发现成功写入了。

再换回reader,读一次数据。

> db.auth("reader","123456")
1
> db.gen1.find()
{ "_id" : ObjectId("566d696a40d302a5c4e9cfc7"), "time" : 1, "value" : 1 }

发现能够成功读数据。

MongoDB上手笔记1——在windows desktop上安装并设置MongoDB服务

最近一个活儿要从设备那里每秒取一些数据回来,出于查询简单的想法,想把数据存在数据库里。之前不知道是因为错觉还是什么,一直觉得node.js非常的酉告(cooooool好像只能这样翻译了?),所以花了几天的时间学了学mongodb。大致整理成了如下这个类似于quick start之类的东西。

之所以注明是在windows desktop上安装,是因为这个活儿里,数据从设备到远程的PC中间数据通过GPRS传到第三方的服务器上,再通过第三方提供的软件从服务器上将数据取回,通过一个虚拟串口将数据读取出来。而第三方提供的软件只能运行在windows上,又只有盗版windows desktop,反正也只是做做样子只能凑活用下了。

1.简单启动MongoDB

从mongodb网站上下载完安装文件之后自行安装完毕。在此不表。

进入mongodb的安装路径中的放exe的文件夹下,可以看到里面有两个exe文件,分别叫mongo和mongod。

mongod就是启动数据库所需要的可执行文件。如果没有理解错误的话,mongod的名字应该是指mongo daemon process的意思,他会运行在后台,默默的完成数据库中的操作。

mongodb在安装的时候并不会把bin文件夹放到环境变量下,所以首先要将bin文件夹对应的路径放到系统环境变量PATH里。这样在cmd中输入mongo和mongod时,它才会知道这两个东西在哪里。

运行数据库,自然需要对数据库进行一系列的配置。在cmd中运行mongod的时候可以完成一些简单的配置,比如数据库存储数据的路径、日志文件的路径、端口的设置等等。例如如下的配置方式会在D:/dbs/data下存储数据文件,将日志文件存储在D:/dbs/log.txt中。

mongod --dbpath d:/dbs/data --logpath d:/dbs/log.txt

2.配置文件启动MongoDB

不过因为在cmd中输入一大长串字符串十分的不方便;而且会在桌面上留下一个cmd大黑框,很不美观。所以还是推荐使用配置文件进行数据库的配置。(当然如果不觉得cmd大黑框无所谓的话,桌面上放个bat文件也可以)

先建立一个mongodb的配置文件,假设放在了D:/dbs/mongo.conf这个路径下。随便用个什么编辑器打开mongo.conf。数据库配置文件使用的是YAML格式,简单使用的话,YAML和JSON没什么不一样的。不过需要注意的一个地方就是YAML不支持tab缩进,所以需要使用空格。以下是我的配置文件,简单配置的话大概有这么几个参数。

systemLog:
    destination: file
    path: "e:/MongoDB/log.txt"
    logAppend: true
storage:
    dbPath: "e:/MongoDB/data"
    journal:
        enabled: true
net:
    bindIp: 127.0.0.1
    port: 27999

其中,各个选项的意思如下。

systemLog.destination的值可以为file或是syslog,因为之后我们会将mongod设置为一个系统服(也就是不会出现cmd大黑框,并可以设置为开机启动)。所以设置为syslog没有意义,此处选择file。

当systemLog.destination设置为file的时候,systemLog.path也必须被确定(也就是上面对应的--logpath的参数)此参数设置了日志文件的路径。

systemLog.logAppend设置为true之后,日志文件就会每次都在现有的log.txt之后续写。默认的是false,则每次mongod重启之后都会同文件夹下备份上次的日志文件,同时新建一个日志文件。

storage.dbPath对应的就是之前--dbpath之后的参数,设置数据文件的存储目录。

storage.journal.enabled对应是否启用mongodb的日志机制。简单的可以理解为启用之后,mongodb崩溃之后可以快速恢复,即可以满足ACID中的一致性。

net.bindIp这个参数是设置绑定ip用的,默认使用本地地址。

net.port这个参数是设置绑定的端口的,默认使用是27017,此处改了改,改成了27999。

假设配置文件mongo.conf放在了d:/dbs/mongo.conf路径下。在这之后,就可以win+r打开cmd,然后输入如下命令启动mongodb。

mongod --config d:/dbs/mongo.conf

3.安装MongoDB服务

按照上面的做法,还是会出现一个cmd大黑框,而且每次都要输入一遍这个命令,还是很麻烦。

这个问题可以通过设置为MongoDB服务来解决(推荐在设置完用户权限之后在安装服务)。

首先以管理员权限打开cmd,然后输入如下命令。

mongod --config d:/dbs/mongo.conf --install

之后就可以在计算机管理->服务和应用程序->服务中看到MongoDB服务了。也可以修改是否要开机自动启动。这样就不会出现cmd的大黑框了。

如果需要修改配置文件,需要先删除现有的MongoDB服务,命令如下。

mongod --remove

之后修改配置文件,并重新安装MongoDB服务即可。

ipv6环境下openvpn的搭建

小学期如期结束,即将搬家到市里的校区,学校的网管也早早下班,ipv4的网络不再计费。趁着这几天赋闲在宿舍,搭了个用ipv6连接的openvpn。

在这里不得不赞叹下DigitalOcean的文档做的真是好,从头到尾什么都写了。虽然是ipv4环境下的,不过仍然非常有借鉴价值。搭建过程中google了下,有一篇很流行的中文指南是在CentOS下的,Ubuntu的好像不是很多。所以在这里把DigitalOcean上的那片指南翻译过来。

VPN是个什么干什么用的就不细说了。在这里只翻译下主要的步骤。

原文链接:How To Set Up an OpenVPN Server on Ubuntu 14.04

本文基本翻译自原文,另有部分修改以方便阅读。

前提

本文中使用的系统是Ubuntu 14.04,操作过程中全程使用了root账号。当然在openvpn配置完毕之后,使用一个拥有sudo权限的普通账户进行管理是一个不错的点子。


1. 安装并设置OpenVPN的服务器环境

以下步骤将完成服务器端的设置。

1.1 OpenVPN和EasyRSA的安装

首先更新Ubuntu的Repository列表,并安装OpenVPN和EasyRSA。

#apt-get update
#apt-get install openvpn easy-rsa

1.2 创建OpenVPN的配置文件server.conf

DigitalOcean的Droplet初始化之后在/usr/share/doc/openvpn/example/这个路径下有示例配置,所以将服务器端的示例配置文件拷贝到openvpn的安装路径下。

#gunzip -c /usr/share/doc/openvpn/examples/sample-config-files/server.conf.gz &gt; /etc/openvpn/server.conf

1.3 编辑server.conf,配置OpenVPN

然后编辑解压出来的server.conf文件,可以使用自己喜欢的编辑器,DigitalOcean的指南里推荐了vim。不过nano比较简单,我还是选择了nano。

#nano /etc/openvpn/server.conf

server.conf中有几处需要改的地方,推荐用CTRL+W搜索能够比较快的找到。

如果需要修改OpenVPN的端口号的话可以修改port之后的数字。默认的端口号是1194.

# Which TCP/UDP port should OpenVPN listen on?
# If you want to run multiple OpenVPN instances
# on the same machine, use a different port
# number for each one.  You will need to
# open up this port on your firewall.
port 1194

如果使用的是ipv6进行连接的话(比如教育网),需要将下面所示的udp修改为udp6。如果使用ipv4进行连接的话,则不用修改此处。

# TCP or UDP server?
;proto tcp
proto udp

ipv6链接的配置修改之后如下所示。

# TCP or UDP server?
;proto tcp
proto udp6

之后再找到如下所示的地方。

# Diffie hellman parameters.
# Generate your own with:
#   openssl dhparam -out dh1024.pem 1024
# Substitute 2048 for 1024 if you are using
# 2048 bit keys.
dh dh1024.pem

将上文中的dh1024.pem修改为dh2048.pem,此处修改的为RSA的密钥长度,自然是长度越长加密程度越高。修改之后的如下所示。(井号之后的内容都是注释,就不再写第二遍了。)

dh dh2048.pem

之后仍旧在server.conf中找到以下这处。

# If enabled, this directive will configure
# all clients to redirect their default
# network gateway through the VPN, causing
# all IP traffic such as web browsing and
# and DNS lookups to go through the VPN
# (The OpenVPN server machine may need to NAT
# or bridge the TUN/TAP interface to the internet
# in order for this to work properly).
;push "redirect-gateway def1 bypass-dhcp"

去掉push前的分号,修改之后如下所示。之后OpenVPN的服务端才会使所有的网络流量通过VPN(如同注释所说的一样)。

push "redirect-gateway def1 bypass-dhcp"

之后仍旧在server.conf中找到以下这处。

# Certain Windows-specific network settings
# can be pushed to clients, such as DNS
# or WINS server addresses.  CAVEAT:
# http://openvpn.net/faq.html#dhcpcaveats
# The addresses below refer to the public
# DNS servers provided by opendns.com.
;push "dhcp-option DNS 208.67.222.222"
;push "dhcp-option DNS 208.67.220.220"

去掉两个push之前的两个分号,修改为以下所示。

push "dhcp-option DNS 208.67.222.222"
push "dhcp-option DNS 208.67.220.220"

修改之后OpenVPN才会通过设置的DNS进行解析。此处使用的208.67.222.222和208.67.220.220为opendns的地址,也可以使用google的8.8.8.8之类的地址。

之后修改server.conf,找到下方所示的地方。

# You can uncomment this out on
# non-Windows systems.
;user nobody
;group nogroup

去掉usergroup之前的分号,最后结果如下。

user nobody
group nogroup

此处如果不修改的话OpenVPN会以root用户的权限运行。出于安全的考虑,会将其权限限制在用户nobody和用户组nogroup的等级上,而其没有特权,所以能保证安全。

之后找到如下所示的地方。

# Select a cryptographic cipher.
# This config item must be copied to
# the client config file as well.
;cipher BF-CBC        # Blowfish (default)
;cipher AES-128-CBC   # AES
;cipher DES-EDE3-CBC  # Triple-DES

去掉第二个cipher之前的分号,选择使用AES进行加密,修改之后我们之后还会在客户端的配置文件中修改加密方式使其对应。如果不需要加密的话此处也可以不修改。

修改之后的结果为

;cipher BF-CBC        # Blowfish (default)
cipher AES-128-CBC   # AES
;cipher DES-EDE3-CBC  # Triple-DES

修改完此处之后,server.conf已经修改完毕。如果使用的是nano编辑的话,可以通过CTRL+x退出,记得保存。

1.4 设置包转发

在终端中输入

#echo 1 &gt; /proc/sys/net/ipv4/ip_forward

这样服务器才会将客户端的网络流量转发到互联网中,否则流量只会停止在服务器这一点,无法到达用户指定的地址。

然而按照上面所示的方法,在重启之后包转发的设置会被重置为不进行转发。为了重启之后也能正常工作,编辑如下文件。(我还是使用了nano,毕竟简单。)终端中输入

#nano /etc/sysctl.conf

照旧找到以下所示一处。

# Uncomment the next line to enable packet forwarding for IPv4
#net.ipv4.ip_forward=1

net.ipv4.ip_forward前面的井号去掉,取消注释。修改之后如下所示。

# Uncomment the next line to enable packet forwarding for IPv4
net.ipv4.ip_forward=1

然而CTRL+x退出,同样要记得保存。

1.5 打开Uncomplicated Firewall(ufw)防火墙
ufw是Ubuntu 14.04自带的一个防火墙。和它的名字一样,配置这个ufw并不是很复杂。当然如果觉得不需要防火墙保护则可以直接跳过这部分。

在终端输入如下的话,可以完成ipv4的配置。首先让ufw允许ssh,否则无法ssh登录了;之后因为之前在server.conf中使用了1194端口和udp协议,所以需要允许1194端口中的udp流量。此处需要注明的是,如果使用的ipv6进行连接的话,可能需要将udp修改为udp6。而且如果同时还跑着其他的程序占用其他端口,同样需要设置对应的端口。比如我的vps中还运行着shadowsocks,一开始开了OpenVPN之后shadowsocks就不能用了,想了一会才发现是ufw的锅。所以干脆我就不用ufw了...

#ufw allow ssh
#ufw allow 1194/udp

之后修改/etc/default/ufw文件修改ufw的转发策略。终端中输入

#nano /etc/default/ufw

找到DEFAULT_FORWARD_POLICY="DROP",这里的DROP必须修改为ACCEPT。修改之后如下所示。

DEFAULT_FORWARD_POLICY="ACCEPT"

之后要为ufw添加额外的规则。终端中输入

#nano /etc/ufw/before.rules

修改before.rules文件,在文件中中间添加一部分。修改完之后样子如下(我已经看不懂在干啥了...总之照做之后能用...)

#
# rules.before
#
# Rules that should be run before the ufw command line added rules. Custom
# rules should be added to one of these chains:
#   ufw-before-input
#   ufw-before-output
#   ufw-before-forward
#

# START OPENVPN RULES
# NAT table rules
*nat
:POSTROUTING ACCEPT [0:0] 
# Allow traffic from OpenVPN client to eth0
-A POSTROUTING -s 10.8.0.0/8 -o eth0 -j MASQUERADE
COMMIT
# END OPENVPN RULES

# Don't delete these required lines, otherwise there will be errors
*filter

设置完ufw之后,我们可以开启ufw了。终端中输入来开启ufw。

#ufw enable

之后会提示

Command may disrupt existing ssh connections. Proceed with operation (y|n)?

输入y并回车,此时ufw已启动。

如果需要查看ufw的规则,终端中输入

#ufw status

如果输出结果类似如下,则配置完成。(视之前的规则设置结果会有不同。)

Status: active

To                         Action      From
--                         ------      ----
22                         ALLOW       Anywhere
1194/udp                   ALLOW       Anywhere
22 (v6)                    ALLOW       Anywhere (v6)
1194/udp (v6)              ALLOW       Anywhere (v6)

如果觉得ufw好烦没必要,也可以关闭掉ufw,终端中输入

#ufw disable

即可关闭ufw。

2. 创建证书认证和服务器端的证书和密钥

OpenVPN使用证书来加密流量。

2.1 设置并创建证书认证

这一步中将设置证书认证,并创建OpenVPN的证书和密钥。OpenVPN可以使用双向的基于证书的认证方法,也就是说在服务端和客户端建立互信关系之前,服务端和客户端需要互相验证证书。这一步中我们将要使用EasyRSA中的脚本来生成证书。

首先将EasyRSA的脚本拷贝到OpenVPN的目录下。终端中输入

#cp -r /usr/share/easy-rsa/ /etc/openvpn

之后在其下创建一个存放密钥的目录keys。终端中输入

#mkdir /etc/openvpn/easy-rsa/keys

EasyRSA有一个变量文件vars,我们可以编辑它使证书能排除掉我们希望的用户以外的人。这些变量信息会被拷贝到证书和密钥中,之后还会在识别密钥的过程中起作用。终端中输入以下内容来编辑变量文件vars

#nano /etc/openvpn/easy-rsa/vars

然后在vars文件中,以下所示的部分需要按照你的要求进行修改。

export KEY_COUNTRY="US"
export KEY_PROVINCE="TX"
export KEY_CITY="Dallas"
export KEY_ORG="My Company Name"
export KEY_EMAIL="sammy@example.com"
export KEY_OU="MYOrganizationalUnit"

之后仍然在vars这个文件中,找到并修改如下一行,修改之后如下所示。

export KEY_NAME="server"

出于简单的考虑,此处将使用『server』作为密钥的名字。如果需要使用不同的名字,还需要修改OpenVPN的配置文件中对于server.keyserver.crt的引用。

之后需要生成Diffie-Hellman参数,这个操作会需要一段时间。终端中输入

#openssl dhparam -out /etc/openvpn/dh2048.pem 2048

之后切换到工作目录,也就是之前拷贝的EasyRSA的脚本所在目录。

#cd /etc/openvpn/easy-rsa

之后进行PKI(Public Key Infrastructure)的初始化,终端中输入以下命令,需要注意两个点之间有个空格。

#. ./vars

上面那个命令会输出以下内容。不过因为我们还没在keys目录下生成任何东西,所以并不需要担心这条警告。

NOTE: If you run ./clean-all, I will be doing a rm -rf on /etc/openvpn/easy-rsa/keys

之后我们会清理当前的工作目录,防止以前的旧密钥或是示例密钥遗留下来。终端中输入

#./clean-all

之后最后一个命令将会建立证书认证,其会使用一个互动式的OpenSSL的命令(说白了就是你问我答...)。弹出的提示将会提示你确认之前vars文件中输入的那些信息(国家、省份、组织之类的)。

#./build-ca

简单一些的话,一路留空回车即可。如果需要修改哪一条的话,在这里修改也可以。

2.2 生成服务端的证书和密钥

前面说道OpenVPN的证书验证是双向的,服务端和客户端都要有证书和密钥。这一步将会生成服务端的证书和密钥。

现在仍旧在/etc/openvpn/easy-rsa这个目录下,终端中输入以下命令来创建服务端的密钥。需要注意的是,这句命令中的参数『server』需要和之前2.1部分中vars文件中export KEY_NAME后的名字相同。

#./build-key-server server

然后会输出类似于2.1部分最后一步./build-ca之后的提示。照例可以一路回车过去。不过这次会多出两个提示,如下所示。

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

两个提示都需要留空,所以直接回车跳过即可。

之后会又有两个询问,分别输入y再回车确认即可。

Sign the certificate? [y/n]
1 out of 1 certificate requests certified, commit? [y/n]

照着做完之后,会出现如下提示。

Write out database with 1 new entries
Data Base Updated

2.3 移动一下服务端的证书和密钥

按照之前的server.conf的设置,我们需要将证书和密钥移动到/etc/openvpn这个目录下。终端中输入

#cp /etc/openvpn/easy-rsa/keys/{server.crt,server.key,ca.crt} /etc/openvpn

这个时候OpenVPN的服务端已经可以运行了,终端中输入以下内容来启动OpenVPN的服务端并检查下运行状态。

#service openvpn start
#service openvpn status

上面第二条是查询状态的命令,其会输出如下内容。

VPN 'server' is running

3. 生成客户端的证书和密钥

到此为止,我们已经安装并配置了OpenVPN服务端,建立了证书认证,并建立了服务端的证书和密钥。在接下来,我们将使用服务端的证书认证来创建每一个客户端的证书和密钥。这些证书和密钥之后将被安装在客户端所在的设备上(比如笔记本和手机等)。

3.1 创建密钥和证书

比较理想的做法是,为每一个连接到VPN的客户端准备一个单独的证书和密钥。另外最好在创建一个通用的证书和密钥供所有的客户端使用。

需要注意的是,OpenVPN默认不允许不同的客户端通过同一个证书进行连接。(在server.confduplicate-cn部分可以查看设置)

为每个客户端创建密钥和证书,你需要为每个客户端执行一遍本部分中的命令,不过需要将本部分中的client1的名字改成另外的名字,比如client2、phone2之类的名字。由于使用了不同的证书,所以每个客户端之后都可以从客户端单独的切断连接。本部分剩余部分将使用client1作为示例的客户端名称。

现在首先为client1客户端创建一个密钥,此时你应当仍旧处于/etc/openvpn/easy-rsa这个路径下。终端中输入

#./build-key client1

像上面的一样,你会被提示要求确认vars文件中你输入的国家、省份、组织之类的一些信息,一路留空回车过去即可。同时也会像2.2部分中一样被要求多进行两相确认,提示如下。

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

同2.2部分中一样,上面两个分别留空并回车确认。

之后也会同之前一样有两个要求确认的地方,如下所示。

Sign the certificate? [y/n]
1 out of 1 certificate requests certified, commit? [y/n]

同样输入y并回车两次分别确认。

如果密钥成功创建了的话,终端中会有以下输出。

Write out database with 1 new entries
Data Base Updated

类似于服务端的配置文件server.conf,客户端同样在路径/usr/share/doc/openvpn/examples下有实例配置文件。我们会将其作为模板,以备下载到客户端之后进行修改。拷贝过程中我们将会把实例配置文件client.conf的扩展名conf修改为ovpn,因为客户端软件需要的文件是ovpn扩展名的。在终端中执行以下命令。

cp /usr/share/doc/openvpn/examples/sample-config-files/client.conf /etc/openvpn/easy-rsa/keys/client.ovpn

到此一个客户端的证书和密钥就配置并创建完毕了。如果需要多组密钥和证书,所需要做的就是重复本部分。当然就像前面提到的,还需要将client1的名字修改为其他的名字。

3.2 将证书和密钥拷贝到客户端设备中

这部分DigitalOcean的指南中介绍了很多方法。不过我觉得准备配置OpenVPN的人中,如果是Linux用户,那这点事情肯定难不住他;如果是windows用户,推荐使用WinSCP,特别傻瓜,全是窗口操作,没啥好说的;没用过mac,所以我也不知道该咋办(= =||)。

对于每个客户端我们需要从服务端拷贝一份该客户端对应的证书、密钥和客户端示例配置文件。

继续以上文中的client1作为例子,我们的客户端client1的证书和密钥在服务器的以下路径中。

  • /etc/openvpn/easy-rsa/keys/client1.crt
  • /etc/openvpn/easy-rsa/keys/client1.key

另外对于所有的客户端,证书认证文件和客户端的示例配置文件在如下所示的路径中。需要注意证书认证文件在openvpn的根目录下,跟剩下三个不在一个路径下面。

  • /etc/openvpn/easy-rsa/keys/client.ovpn
  • /etc/openvpn/ca.crt

本部分完成之后,你的客户端设备中将有以下四个文件。

  • client1.crt
  • client1.key
  • client.ovpn
  • ca.crt

4. 创建一个OpenVPN的客户端档案

管理客户端文件有许多方法,然而最简单的一种方法就是把所有的文件都放到一个档案文件中。我们只需要修改配置文件模板client.ovpn,使其包含服务端的证书认证、服务端的证书和密钥。修改完毕之后,所需要导入的配置文件就只有client.ovpn这一个文件了。

接下来我们将在客户端设备上修改之前下载的四个文件。之前的客户端配置文件模板client.ovpn需要被复制并重命名,叫什么名字随意。名字并不需要和客户端设备相关,客户端软件将会将这个文件的名字当作VPN连接的名字。所以client.ovpn的命名应当相同与你想要其出现在你操作系统中的名字,比如work就重命名为work.ovpn等。

在这篇指南中,我们将会想要VPN的名字为DigitalOcean,所以client.ovpn将会被复制并重命名为DigitalOcean.ovpn,之后在编辑器中打开DigitalOcean.ovpn。

首先,如果使用的是ipv6连接vpn的话,类似于server.conf中的内容,DigitalOcean中的协议同样需要被修改。找到如下的一段内容

# Are we connecting to a TCP or
# UDP server?  Use the same setting as
# on the server.
;proto tcp
proto udp

proto udp修改为proto udp6。如果使用ipv4连接的话,则不需要修改这一段。修改之后的效果为

;proto tcp
proto udp6

之后找到如下一段

# The hostname/IP and port of the server.
# You can have multiple remote entries
# to load balance between the servers.
remote my-server-1 1194

其中将my-server-1修改为你的服务器的地址。如果使用的是ipv4连接VPN,则将my-server-1修改为服务器的ipv4地址;同样如果使用ipv6连接VPN则将my-server-1修改为ipv6地址(切记看清楚不要设置成网关的,说多了都是泪)

之后找到下面一段,去掉usergroup前面的分号,作用和第1部分中说的一样。客户端如果运行在Windows系统中,那么可以忽略这一步。修改之前为

# Downgrade privileges after initialization (non-Windows only)
;user nobody
;group nogroup

修改之后为

# Downgrade privileges after initialization (non-Windows only)
user nobody
group nogroup

之后找到如下一段

# SSL/TLS parms.
# . . .
ca ca.crt
cert client.crt
key client.key

ca、cert、key前面加上井号将其注释,这样我们之后将证书认证、证书和密钥输入到ovpn文件中才会生效。修改之后的效果如下

# SSL/TLS parms.
# . . .
#ca ca.crt
#cert client.crt
#key client.key

之前如果在server.conf中设置了加密方式中(比如我们在第1部分中做的那样),那么在DigitalOcean.ovpn中找到如下一段。

# Select a cryptographic cipher.
# If the cipher option is used on the server
# then you must also specify it here.
;cipher

由于我们之前选择了AES-128-CBC的加密方法,所以我们此处的设置也与server.conf中相同。修改之后的效果如下。

# Select a cryptographic cipher.
# If the cipher option is used on the server
# then you must also specify it here.
cipher AES-128-CBC

最后在DigitalOcean.ovpn文件的末尾添加以下XML结构的内容。

<ca>
(在此处插入ca.crt的内容)
</ca>
<cert>
(在此处插入client1.crt的内容)
</cert>
<key>
(在此处插入client1.key的内容)
</key>


分别用编辑器打开这三个文件,并插入即可。需要注意的是client1.crt的内容和另外两个有些不同,不过不用管,直接加入进去就好了。修改之后的效果如下。


<ca>
-----BEGIN CERTIFICATE-----
. . .
-----END CERTIFICATE-----
</ca>

<cert>
Certificate:
. . .
-----END CERTIFICATE-----
. . .
-----END CERTIFICATE-----
</cert>

<key>
-----BEGIN PRIVATE KEY-----
. . .
-----END PRIVATE KEY-----
</key>

至此为止,我们的客户端配置文件已经创建并设置完毕。

5. 安装客户端的配置文件

我目前只在windows中设置过openvpn。所以此处只简单叙述下windows中的方法。毕竟之前的才是重头戏,后面的都已经好解决了。

首先将我们的DigitalOcean.ovpn的配置文件拷贝到OpenVPN的安装根目录下的config文件夹下。

然后管理员权限启动OpenVPN GUI。(一定要以管理员权限运行)

然后在系统托盘图标上右键,并点击connect即可。

至此OpenVPN的服务端和客户端的配置已经完全完成,享受私密网络吧!

GMM的EM算法实现

GMM的全名是Gaussian Mixture Model,即高斯混合模型,这个名字挺直观。

如果给了一组数据点,假设这些数据点符合iid,那么很容就能根据数据拟合出一个最佳的高斯分布。

但是如果给了一堆数据点,而他们却不符合iid,只能做到互相独立,并不能做到同分布。相反的,数据点们有可能来自不同的高斯分布,这也就是『高斯混合』模型名字的意思。这样如何能拟合出最佳的高斯分布们呢?

EM算法提供了一种方法,这种方法的思路有些像牛拉法。先是确定一个初值,然后看看当前值距离目标还有多远,然后逐渐的缩小差距(不断修正membership weight)。

具体原理见这里

这篇小短文大概记录下这次作业的内容。作业题目是生成了2个二维高斯分布混合的数据点,共2000个,然后用这两千个点拟合出2个二维高斯分布的参数。

clear;
%产生2个二维正态数据
MU1    = [1 2];
SIGMA1 = [1 0; 0 0.5];
MU2    = [-1 -1];
SIGMA2 = [1 0; 0 1];
X      = [mvnrnd(MU1, SIGMA1, 1000);mvnrnd(MU2, SIGMA2, 1000)];
scatter(X(:,1),X(:,2),10,'.');

%initial values
h_mu1 = X(randi(2000),:);
h_sigma1 = cov(X);
h_mu2 = X(randi(2000),:);
h_sigma2 = cov(X);
h_alpha1 = 0.5;
h_alpha2 = 0.5;

%simple iteration
for iter=1:100
    %E step
    w1 = mvnpdf(X,h_mu1,h_sigma1)*h_alpha1./...
    (mvnpdf(X,h_mu1,h_sigma1)*h_alpha1 + mvnpdf(X,h_mu2,h_sigma2)*h_alpha2);
    w2 = mvnpdf(X,h_mu2,h_sigma2)*h_alpha2./...
    (mvnpdf(X,h_mu1,h_sigma1)*h_alpha1 + mvnpdf(X,h_mu2,h_sigma2)*h_alpha2);

    %M step
    h_alpha1 = sum(w1)/2000;
    h_alpha2 = sum(w2)/2000;
    
    h_mu1 = sum([w1.*X(:,1) w1.*X(:,2)])/sum(w1);
    h_mu2 = sum([w2.*X(:,2) w2.*X(:,2)])/sum(w2);

    M1 = X-ones(2000,1)*h_mu1;
    h_sigma1 = ([sum(w1.*M1(:,1).^2) 0;0 sum(w1.*M1(:,2).^2)]+...
        sum(w1.*(M1(:,1).*M1(:,2)))*[0 1;1 0])/sum(w1);
    M2 = X-ones(2000,1)*h_mu2;
    h_sigma2 = ([sum(w2.*M2(:,1).^2) 0;0 sum(w2.*M2(:,2).^2)]+...
        sum(w2.*(M2(:,1).*M2(:,2)))*[0 1;1 0])/sum(w2);
end

figure;
[x,y] = meshgrid(linspace(-5,5,75));
p1 = mvnpdf([x(:),y(:)],h_mu1,h_sigma1);
p2 = mvnpdf([x(:),y(:)],h_mu2,h_sigma2);
surf(x,y,reshape(p1,75,75));hold on
surf(x,y,reshape(p2,75,75));

options = statset('Display','final');
obj = gmdistribution.fit(X,2,'Options',options);
figure,h = ezmesh(@(x,y)pdf(obj,[x,y]),[-8 6], [-8 6]);

绝大多数时候都没有什么问题,不过由于初始点是从X里随便选的一个数据点,极偶然会跑偏。

以下分别是数据点的分布、自行拟合出的结果、利用matlab自带拟合函数拟合出的结果。

originalgmm

根据给定的均值、协方差矩阵画出的GMM的图

xscatter

数据点的散点图,保存之后可能有点糊

fitbymyself

自己写的脚本拟合出的GMM的图

fitbymatlabtoolbox

matlab提供的拟合函数拟合出的图

迭代100次之后,高斯分布1的均值分别偏差0.2776%、4.4462%,协方差分别偏差了2.4458%、5.0146%;高斯分布2的均值偏差了2.6981%、1.2911%,协方差偏差了3.8498%、1.5517%。精度凑凑活活把...

对比之下,matlab自带的拟合函数拟合出来的高斯分布1的均值偏差2.2415%、2.2415%,协方差偏差为4.02%、1.32%;高斯分布2的均值偏差了0.4165%、0.4165%,协方差偏差为2.39%、4.62%。精度也没高到哪里去...不过均值偏差一样让我很是在意...

然而看图1、图3和图4的话,明显还是自己写的代码拟合效果更好一些啊,有些不科学。

算法学习笔记(4):计数排序

在宿舍呆着无聊接着在hackerrank上刷水题。这道题由于排序的依据是一定范围内的整数,所以用计数排序效率应当不错。

说到计数排序,实际上思路也很常见。最常见的一个例子就是比赛或者是考试的时候要进行排名。

比如某班有3名同学考了100分,2名同学考了99分,5名同学考了98分。那么很明显,比99分高的同学一共有3名,所以考99分的同学只能是第4名了。

计数排序大概就是做了个数数每个元素前面有多少元素这个事情。先统计最高分多少分,然后依次统计各个分数的学生个数,然后以此叠加就可以统计出前面有多少个学生了。

而且由于不同分数段的学生是按照原顺序放置到新数组中的,所以在原数组中的顺序并不会被打乱,即这个算法是稳定的。这也是为什么这道题可以用计数排序解,因为并不会打乱文本的顺序。

顺便一提,这道题中的最大值max()的复杂度应该是,所以总运行时间应该是。因为计数排序并不涉及到不同元素之间的比较,即不是比较排序,所以的下界并不适用于计数排序。

最后附上这道题的一个无脑解。

# Enter your code here. Read input from STDIN. Print output to STDOUT
n = int(raw_input())
ar = []
idx = []
out = [None]*n
for _ in range(n):
    ar.append(raw_input().split())
    ar[-1][0] = int(ar[-1][0])
    idx.append(ar[-1][0])
    if _<n/2:
        ar[-1][1] = "-"
countlist = [0]*(max(idx)+1)
for item in ar:
    countlist[item[0]]+=1
for i in range(len(countlist)-1):
    countlist[i+1]+=countlist[i]
for item in reversed(ar):
    out[countlist[item[0]]-1]=item
    countlist[item[0]]-=1
for item in out:
    print item[1],