aiohttp_cors 和 aiohttp文件上传

aiohttp_cors 和 aiohttp文件上传

  • aiohttp_cors
    • 跨域共享
    • aiohttp_cors的使用方法
    • 其他使用方法
  • aiohttp的post和文件上传
    • 文件的下载和上传
    • 前端网页部分
    • 前端Javascript部分
    • 服务器端aiohttp部分
  • 完整程序代码
    • 说明
    • 前端完整代码
    • 服务端完整代码

aiohttp_cors

跨域共享

在浏览器中,如果使用与当前网页不是同协议(例如http)、同IP地址(或网址)、同端口的http资源,可能会出现跨域共享问题,浏览器控制台会报“No Access-Control-Allow-Origin header presence”错误,这是一种安全策略。有些例外情况跨域共享不会被禁止,详情可见:链接: http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html。
但我们的程序中,使用RestFUL网络服务,这些服务有时要用在不同网页中。为规避这一问题,就涉及到“跨域资源共享”(CORS)的问题。python的aiohttp是不具备这一功能(tornado内部具有此功能)的,为此,就必须使用与之配套的aiohttp_cors模块来补充这一功能。使用pip install aiohttp_cors命令,可以获取和安装这一模块,程序中使用import aiohttp_cors就可引入,作为对aiohttp CORS功能的补充。

aiohttp_cors的使用方法

aiohttp_cors的文档比较简单,使用方法在其文档中用例子来描述,在网页https://pypi.org/project/aiohttp_cors/0.7.0/中,其中没找到每个类和方法的详细说明。从编程的角度,我们关注的aiohttp_cors的关键有两点:

  • 它是aiohttp的附加机制,配合其运行,配合方式有两种,一种是先定义好所有aiohttp
    route,然后将cors功能加上,另一种是先定义cors功能,再逐个加入aiohttp route;
  • 它可以为aiohttp的每个route设置cors,也可以将一个cors用于所有route
    我们采用的是将一个cors用于全部route并且先定义cors的方式,所依据的是其官网的如下例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
cors = aiohttp_cors.setup(app, defaults={
    "*": aiohttp_cors.ResourceOptions(
            allow_credentials=True,
            expose_headers="*",
            allow_headers="*",
        )
})

# Add all resources to `CorsConfig`.
resource = cors.add(app.router.add_resource("/hello"))
cors.add(resource.add_route("GET", handler_get))
cors.add(resource.add_route("PUT", handler_put))
cors.add(resource.add_route("POST", handler_put))
cors.add(resource.add_route("DELETE", handler_delete))

据此,我们相应的程序段落如下:

1
2
3
4
5
6
7
8
9
10
cors = aiohttp_cors.setup(app, defaults={
    "*": aiohttp_cors.ResourceOptions(
        allow_credentials=True,
        expose_headers="*",
        allow_headers="*",
        allow_methods="*",
    )
})
resource = cors.add(app.router.add_resource("/post/file/"))
cors.add(resource.add_route("POST", post_file))

其他使用方法

官网中为route逐个设置cors的例子:

1
2
3
4
5
6
7
8
9
cors.add(
    app.router.add_route("GET", "/hello", handler), {
        "http://client.example.org": aiohttp_cors.ResourceOptions(
            allow_credentials=True,
            expose_headers=("X-Custom-Server-Header",),
            allow_headers=("X-Requested-With", "Content-Type"),
            max_age=3600,
        )
    })

还有先定义所有route,再施加cors的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Setup application routes.
app.router.add_route("GET", "/hello", handler_get)
app.router.add_route("PUT", "/hello", handler_put)
app.router.add_route("POST", "/hello", handler_put)
app.router.add_route("DELETE", "/hello", handler_delete)

# Configure default CORS settings.
cors = aiohttp_cors.setup(app, defaults={
    "*": aiohttp_cors.ResourceOptions(
            allow_credentials=True,
            expose_headers="*",
            allow_headers="*",
        )
})

# Configure CORS on all routes.
for route in list(app.router.routes()):
cors.add(route)

编程者可以根据自己不同的情况选用,例如,后一个例子对于已经有了aiohttp程序之后要增加cors功能的情况就十分方便。

aiohttp的post和文件上传

文件的下载和上传

文件下载只要一个超文本引用(href)就可解决问题,无论是HTML客户端还是服务器端,都不需要额外编程,例如下面的HTML程序可以下载服务器网页目录下share子目录中的buffe.txt和modbus_therm_hum_01.xlsx两个文件:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <a href="shared/buffer.txt" download="buffer.txt">文件:buffer.txt</a>
    <br/>
<a href="shared/modbus_therm_hum_01.xlsx" download="modbus_therm_hum_01.xlsx">文件:modbus_therm_hum_01.xlsx</a>
</body>
</html>

文件的上传却相对麻烦,不但需要网页客户端的(Javascript)程序,还需要服务器端的(jsp等)程序才能实现。由于我们使用aiohttp写的RestFul形式的Web Service,所以,将文件上传的服务也纳入其中。
当前,在文件上传方面,前台是JavaScript/CSS/HTML,后台是JSP、PHP等方式的参考资料较多,但前台是JavaScript/CSS/HTML,后台是aiohttp并且支持CORS的材料则相对较少。我们找了一些资料结合自己的情况,进行了测试和编程。本章介绍的是相关情况。解决了文件上传的问题,一般的POST编程也没有问题了。
完成文件上传一般以POST方式操作,需要以下组成部分:

  • 浏览器端的网页程序部分(form);
  • 浏览器端的文件处理程序部分(JavaScript);
  • 服务器端的文件处理程序部分(aiohttp)。

这些部分可以用不同的方式实现,括号中是我们使用的方式。无论哪种方式,网上都有很多例子,有些例子比较完整,有些例子不完整;有些例子可以正确工作,有些例子运行起来有问题。下面几节以一些有参考价值的例子为基础,具体描述一下它们的实现,以及在实现中发现和解决的问题。
HTML和JavaScript部分主要参考了以下两个网页中的内容:

  • 网页_1:https://www.ibm.com/developerworks/cn/web/1101_hanbf_fileupload/index.html
  • 网页_2:http://www.jq22.com/webqd18520

aiphttp部分主要参考了以下两个网页中的内容:

  • 网页_3:https://blog.csdn.net/u010080628/article/details/84975272
  • 网页_4:https://wizardforcel.gitbooks.io/aiohttp-chinese-documentation/content/aiohttp%E6%96%87%E6%A1%A3/ServerUsage.html#%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E7%AE%B1,见其中的“文件上传”一节

前端网页部分

多数网页部分的描述都比较相似,关键有二:

  • form具有enctype="multipart/form-data"属性;
  • form中有一个文件输入的元素。

例如网页_2中相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<form id="form1" enctype="multipart/form-data" method="post" action="Upload.aspx">
    <div class="row">
        <label for="fileToUpload">Select a File to Upload</label><br>
        <input type="file" name="fileToUpload" id="fileToUpload" onchange="fileSelected();">
    </div>
    <div id="fileName"></div>
    <div id="fileSize"></div>
    <div id="fileType"></div>
    <div class="row">
        <input type="button" onclick="uploadFile()" value="Upload">
    </div>
    <div id="progressNumber"></div>
</form>

其实,在文件上传的多数情况下,input 的 id 并不一定需要,如在网页_1中:

1
2
3
4
<form name="demoForm" id="demoForm" method="post" enctype="multipart/form-data" action="javascript: uploadAndSubmit();">
<p><center>[wp_ad_camp_4]</center></p><p>Upload File: <input type="file" name="file" /></p>
<p><input type="submit" value="Submit" /></p>
</form>

前端Javascript部分

网页_2中Javascript部分上载函数功能比较简单,经稍许修改即可正常工作:

1
2
3
4
5
6
7
8
9
10
11
function uploadFile() {
    var fd = new FormData();
    fd.append("fileToUpload", document.getElementById('fileToUpload').files[0]);
    var xhr = new XMLHttpRequest();
    xhr.upload.addEventListener("progress", uploadProgress, false);
    xhr.addEventListener("load", uploadComplete, false);
    xhr.addEventListener("error", uploadFailed, false);
    xhr.addEventListener("abort", uploadCanceled, false);
    xhr.open("POST", "http://localhost:8900/post/file/"); //修改成自己的接口
    xhr.send(fd);
}

事件处理使用单独定义的函数实现,后面我们会提供清单。这里面需要根据自己情况修改的有三处:

  • 第三行程序中的第一个"fileToUpload",到服务器端,对应为接收对象的键(key),服务器端要用该键来获取收到的内容(详见2.4节),这里保持了原有的命名;
  • 第三行程序中的第二个"fileToUpload",是对应网页input元素中的id,Javascript程序根据此id获取输入的文件对象,这里也保持了原有的命名;
  • xhr.open()一行中的链接:http://localhost:8900/post/file/对应服务器相应服务的url,这里根据后台aiohttp程序的设置进行了修改,如果后台是jsp或PHP程序,对应的则是相应的接收程序及其所需的参数,例如:upload.jsp?fileName=" + file.name

要使网页_1中的程序正常工作,修改相对较多一些,但经过修改,仍然是可以配合服务器端正常工作的。

服务器端aiohttp部分

解决了CORS问题之后,后台程序所费周折相对较少, 网页_3、网页_4两个参考资料的介绍也基本一致,经过调整,我们采用的处理函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async def post_file(request):
    try:
        reader = await request.multipart()
        file = await reader.next()
        filename = file.filename if file.filename else 'undefined'
        size = 0
        with open(filename, 'wb') as f:
            while True:
                chunk = await file.read_chunk()  # 默认是8192个字节。
                if not chunk:
                    break
                size += len(chunk)
                f.write(chunk)
        return web.Response(text="成功读取文件")
    except Exception as e:
        print(e)
        return web.Response(text="读取文件失败")

上述函数在aiohttp和aiohttp_cors中的注册代码的片段如下:

1
2
3
4
5
6
7
8
9
10
app = web.Application()
cors = aiohttp_cors.setup(app, defaults={
        "*": aiohttp_cors.ResourceOptions(
        allow_credentials=True,
        expose_headers="*",
        allow_headers="*",
    )
})
resource = cors.add(app.router.add_resource("/post/file/"))
cors.add(resource.add_route("POST", post_file))

上面的处理代码是采用块状传输的方式,如果是小文件,可以采用直接接收POST数据的方式:

1
2
3
4
5
6
7
8
9
async def post_file(request):
    try:
        data = await request.post()
        f = data['fileToUpload'].file
        content = f.read()
        print(content)
    except Exception as e:
        return web.Response(text="这是服务器返回的出错信息")
    return web.Response(text="这是服务器返回的正常信息")

这段程序仅用于测试,它并未将上载的文件内容写入服务器文件,只是将其打印出来,这也演示了一般的POST操作。下面的完整程序中,我们将这一段程序的函数体部分以注释的方式给出。

完整程序代码

说明

为方便读者测试,这里给出的完整的程序代码,它包括两个部分:

  • 前端代码:包括Javascript程序的完整HTML文件;
  • 服务端代码:用aiohttp和aiohttp_cors实现的完整python服务端程序。

这两个程序均不再需要依赖额外的程序代码就可以运行,但python环境中必须包含所有import语句中的模块。如果没有,可以用pip install下载。前后端程序只要改动服务器url,就可以在不同的环境下运行了。

前端完整代码

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
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>upload files</title>
   <script>

       function fileSelected() {
           var file = document.getElementById('fileToUpload').files[0];
           if (file) {
               var fileSize = 0;
               if (file.size > 1024 * 1024)
                   fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB';
               else
                   fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB';
               document.getElementById('fileName').innerHTML = 'Name: ' + file.name;
               document.getElementById('fileSize').innerHTML = 'Size: ' + fileSize;
               document.getElementById('fileType').innerHTML = 'Type: ' + file.type;
           }
       }

       function uploadFile() {
           var fd = new FormData();
           fd.append("fileToUpload", document.getElementById('fileToUpload').files[0]);
           var xhr = new XMLHttpRequest();
           xhr.upload.addEventListener("progress", uploadProgress, false);
           xhr.addEventListener("load", uploadComplete, false);
           xhr.addEventListener("error", uploadFailed, false);
           xhr.addEventListener("abort", uploadCanceled, false);
           xhr.open("POST", "http://localhost:8900/post/file/"); //修改成自己的接口
           xhr.send(fd);
       }

       function uploadProgress(evt) {
           if (evt.lengthComputable) {
               var percentComplete = Math.round(evt.loaded * 100 / evt.total);
               document.getElementById('progressNumber').innerHTML = percentComplete.toString() + '%';
           } else {
               document.getElementById('progressNumber').innerHTML = 'unable to compute';
           }
       }

       function uploadComplete(evt) {
           /* 服务器端返回响应时候触发event事件*/
           alert(evt.target.responseText);
       }

       function uploadFailed(evt) {
           alert("There was an error attempting to upload the file.");
       }

       function uploadCanceled(evt) {
           alert("The upload has been canceled by the user or the browser dropped the connection.");
       }

   </script>
</head>
<body>
<form id="form1" enctype="multipart/form-data" method="post" action="javascript: fileSelected();">
   <div class="row">
       <label for="fileToUpload">Select a File to Upload</label><br>
<!--        <input type="file" name="fileToUpload" id="fileToUpload" οnchange="fileSelected();">  -->
       <input type="file" name="fileToUpload" id="fileToUpload">
   </div>
   <div id="fileName"></div>
   <div id="fileSize"></div>
   <div id="fileType"></div>
   <div class="row">
       <input type="button" onclick="uploadFile()" value="Upload">
   </div>
   <div id="progressNumber"></div>
</form>
</body>
</html>

服务端完整代码

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
from aiohttp import web
import aiohttp_cors
import asyncio
import json
import os

'''
这段注释掉的程序就是2.4节中描述的小文件传输的实验程序(只含函数体)
   try:
       data = await request.post()
       f = data['fileToUpload'].file
       content = f.read()
       print(content)
   except Exception as e:
       return web.Response(text="这是服务器返回的出错信息")
   return web.Response(text="这是服务器返回的正常信息")

'''


async def post_file(request):
   try:
       reader = await request.multipart()
       file = await reader.next()
       filename = file.filename if file.filename else 'undefined'
       print('here: ', filename, os.getcwd())
       size = 0
       with open(filename, 'wb') as f:
           while True:
               chunk = await file.read_chunk()  # 默认是8192个字节。
               if not chunk:
                   break
               size += len(chunk)
               f.write(chunk)
       text = {'res': '上传成功'}
       return web.Response(text=json.dumps(text, ensure_ascii=False))
   except Exception as e:
       print(e)
       return web.Response(text="读取文件数据失败")

async def init():
   app = web.Application()

   cors = aiohttp_cors.setup(app, defaults={
       "*": aiohttp_cors.ResourceOptions(
           allow_credentials=True,
           expose_headers="*",
           allow_headers="*",
           # allow_methods="*",
       )
   })
   resource = cors.add(app.router.add_resource("/post/file/"))
   cors.add(resource.add_route("POST", post_file))
   runner = web.AppRunner(app)
   await runner.setup()
   site = web.TCPSite(runner, '0.0.0.0', 8900)
   await site.start()
print('------- Http server started at localhost:8900 --------')

# def main():
if __name__ == '__main__':
loop = asyncio.new_event_loop()    # get_event_loop()
loop.run_until_complete(init())
loop.run_forever()