python 网关服务器 python做服务器接口
接到个小活,要实现一个局域网内的小服务器,需求如下:
1.实现静态文件下发功能,主要是apk安装包和html及其相关文件;
2.实现数据接口,用于存储和查询网内其他设备发来的数据;
3.实际使用者电脑能力一般,环境搭建和操作越简单越好。
最近在学Python,正好来练练手。话不多说,开搞!
开发环境:Windows7 + Python3.5.2
第一步,当然是要把服务器跑起来啦!
查了下原来超级简单,只要用命令行进入想作为服务器目录的目录,并运行如下命令:
python -m SimpleHTTPServer 8000
结果报了错:No module named SimpleHTTPServer,什么情况?
查了下这个错误,原来SimpleHTTPServer是Python 2.X的模块,3.X中需要用http.server,于是修改如下:
python -m http.server 8080
运行后提示:
再打开浏览器,访问http://127.0.0.1:8000/
果然看到了目录的文件列表。
在服务器目录里随便丢了个index.html,浏览器访问http://127.0.0.1:8000/index.html:
成功!
第二步,用Python代码跑服务器
命令行一句话的事,用代码当然也很简单:
import http.server as hs httpAddress = ('', 8000) httpd = hs.HTTPServer(httpAddress, RequestHandler) httpd.serve_forever()
其中httpAddress为(url, port)的格式,url为空表示指向本机默认地址127.0.0.1,8000是端口号,改成其他未被占用的也可以。
那么RequestHandler是什么呢?
第三步,实现RequestHandler
RequestHandler其实是对http.server.BaseHTTPRequestHandler这个基础类的继承,即对http请求的响应。但它本身不包含任何方法,需要自行实现。
项目中没有用到post请求,因此只需实现do_GET方法:
import os import http.server as hs class RequestHandler(hs.BaseHTTPRequestHandler): def do_GET(self): self.full_path = os.getcwd() + self.path print(self.path) print(self.full_path) pass httpAddress = ('', 8000) httpd = hs.HTTPServer(httpAddress, RequestHandler) httpd.serve_forever()
其中self.path为请求的相对路径,os.getcwd()可以获取当前的工作目录。两者结合,便得到了请求目标在本机文件系统中的绝对路径。
运行程序,浏览器访问http://127.0.0.1:8000/index.html,果然看到了print的输出:
成功!
第四步,区分请求类型
获取访问地址后,当然是根据后缀名区分请求类型并各自处理咯。
结合需求和逻辑,服务器需要依照先后顺序判断并处理以下几类:
1.请求的文件不存在时,抛回错误页面并提示错误原因;
2.请求指向某个数据接口时,执行指定的python脚本并返回结果;
3.请求指向某个文件时,返回该文件;
4.以上情况皆不符合,抛回错误页面并提示错误原因。
项目中对这些请求分别建立包含test和act两个方法的类并一次调用,类似于:
cases = [case1(), case2(), case3(), case4()] for case in cases: if case.test(self): case.act(self) break
接下来就是实现每种请求的处理啦!以下几个是重点:
第五步,请求指定文件
前面已经拿到了请求文件的绝对地址,通过文件IO方法即可判断目标文件是否存在。
若文件存在,则读取文件内容并写入请求文件对象(通过self.wfile获取),对终端来说即是返回了该文件。
html文件里通常会包含css、js、图片、音频等各种引用,但实际访问时浏览器会自动对其所有引用文件单独发起get请求,所以服务器这边无需特殊对待,当做数个独立的文件请求来处理就行。
但是要注意,如果地址是类似url?param1=value1¶m2=value2的形式,判断和返回文件内容时需要先把后面的参数部分截掉。代码如下:
def handle_file(self, full_path): try: #处理过程中需先截掉参数,否则会判断为文件不存在 full_path = full_path.split("?") full_path = full_path[0] #判断文件类型以设置mimetype mimetype = "" if self.path.endswith(".html"): mimetype='text/html' if self.path.endswith(".jpg"): mimetype='image/jpg' if self.path.endswith(".png"): mimetype='image/png' if self.path.endswith(".gif"): mimetype='image/gif' if self.path.endswith(".js"): mimetype='application/javascript' if self.path.endswith(".css"): mimetype='text/css' if self.path.endswith(".apk"): mimetype='application/vnd.android.package-archive' self.send_response(200) self.send_header('Content-type', mimetype) self.end_headers() with open(full_path, 'rb') as f: self.wfile.write(f.read()) except IOError as msg: msg = "'{0}' cannot be read: {1}".format(self.path, msg) self.handle_error(msg)
假如用到了其他类型文件,从网上找到对应的mimetype加进去就好。
往文件夹里丢了张小图片,试着访问下:
再试试访问下包含这个文件的页面:
成功!
第六步,请求数据接口
数据接口的实现思路也很简单:
1.服务器上准备好所需的python脚本,对应不同数据接口的功能
2.终端直接以url+参数的形式访问数据接口(即对应的Python脚本);
3.服务器判断出这种情况后,执行该Python脚本并传入参数;
4.Python脚本被执行,根据参数对目标文件(或数据库)进行增删改查等操作;
5.Python脚本的执行结果以文本方式传回,作为本次数据请求的回调。
import argparse try: parser = argparse.ArgumentParser(description='manual to this script') parser.add_argument('--id', type = int, default = -1) parser.add_argument('--name', type = str, default = None) args = parser.parse_args() if < 0: print('{"result":"False","msg":"invalid id"}') else: id = name = name = name.encode(encoding = "utf-8") print('{"result":"True","id":"%d","name":"%s"}' %(id, name)) except BaseException as e: print('{"result":"False","msg":"%s"}' %str(e))
其中,脚本调用是通过subprocess.check_output()方法实现的。该方法会创建一个子进程以命令行方式执行指定的指令或脚本,父进程会等待其执行完毕并返回输出结果。
为了与传统接口形式统一,项目中的http请求参数是以?param1=value1¶m2=value2的形式传递的;但调用Python脚本时,参数需要以--param1=value1{空格}--param2=value2……的形式传递,因此需要对其进行转换。
实现以上功能的代码如下:
def run_cgi(self, fullpath): #运行脚本并得到格式化的输出 #参数以url?param1=1¶m2=2……的方式传递 path = fullpath.split("?") if len(path) > 1: params = path[1] params = params.split("&") shell = [] shell.append("python") shell.append(path[0]) for param in params: shell.append("--" + param) data = subprocess.check_output(shell) else: data = subprocess.check_output(["python", path[0]]) self.send_content(page = str(data, encoding = 'utf-8'))
最后一行send_content方法的功能是返回纯文本作为请求结果,其实就是一个mimetype为text/html的文件输出:
def send_content(self, page, status = 200): self.send_response(status) self.send_header("Content-type", 'text/html') self.end_headers() self.wfile.write(bytes(page, encoding = 'utf-8'))
附上示例脚本:
import argparse try: parser = argparse.ArgumentParser(description='manual to this script') parser.add_argument('--id', type = int, default = -1) parser.add_argument('--name', type = str, default = None) args = parser.parse_args() if < 0: print('{"result":"False","msg":"invalid id"}') else: id = name = name = name.encode(encoding = "utf-8") print('{"result":"True","id":"%d","name":"%s"}' %(id, name)) except BaseException as e: print('{"result":"False","msg":"%s"}' %str(e))
可见:
1. 参数使用argparse库中的方法即可获取;
2. 脚本中通过print输出的内容即为脚本的返回内容;
3. 返回结果的中文内容建议进行urf-8转码并在终端转回,否则传输过程中容易遇到各种奇奇怪怪的问题。
打开浏览器,访问该脚本并传入参数:
成功!
第七步,异常处理
经过上面几个步骤,服务功能基本搭得差不多了。剩下的就是处理下异常情况,即生成一个包含错误内容的html页面:
Error_Page = """ <html> <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> <body> <h1>Error accessing {path}</h1> <p>{msg}</p> </body> </html> """
访问个空地址试一试:
成功!
至此,全部功能完成。鞠躬。