REST can be conveniently used for data communication between the server and the client. Regarding the usage of REST, there is information on how to create a client in various languages and frameworks. However, on the server side, there is information such as how to build with "JsonServer" or Apache + PHP as a mock that can be easily prepared, but it is a method of accepting inconvenience or taking time and effort. (It will be as far as I can find.)
This time, I tried to build a server that is in the middle of the above, which allows you to freely set services and easily build with one script. (I don't want it to be robust enough to be published on the WEB, but the purpose is to build my own Raspberry Pi service.)
It's a live resource code.
restserver.py
#!/usr/bin/env python3
import http.server
import json
import threading
import sys,os
import time
class RestHandler(http.server.BaseHTTPRequestHandler):
def do_OPTIONS(self):
#Supports preflight request
print( "options" )
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
self.send_header('Access-Control-Allow-Headers', '*')
self.end_headers()
def do_POST(self):
print( "post" )
local_path = self.path.strip("/").split("/")
#Get request
content_len = int(self.headers.get("content-length"))
body = json.loads(self.rfile.read(content_len).decode('utf-8'))
#Response processing
if( local_path[0] == "dat" ):
if(os.path.getsize('./dat.json')):
with open('./dat.json', 'r') as json_open:
json_load = json.load(json_open)
json_load.update(**body)
json_wraite = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False)
else:
json_wraite = json.dumps(body, sort_keys=False, indent=4, ensure_ascii=False)
with open('./dat.json', 'w') as json_open:
json_open.write(json_wraite)
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
self.send_header('Access-Control-Allow-Headers', '*')
self.end_headers()
return
else:
print( "no" )
print( self.path )
return
def do_GET(self):
print( "get" )
local_path = self.path.strip("/").split("/")
#Response processing
if( local_path[0] == "dat" ):
print( "dat" )
if(os.path.getsize('./dat.json')):
with open('./dat.json', 'r') as json_open:
json_load = json.load(json_open)
else:
json_load = {}
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
self.send_header('Access-Control-Allow-Headers', '*')
self.send_header('Content-type', 'application/json;charset=utf-8')
self.end_headers()
body_json = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False)
self.wfile.write(body_json.encode("utf-8"))
return
else:
print( "no" )
print( self.path )
return
def do_DELETE(self):
print( "delete" )
local_path = self.path.strip("/").split("/")
if( local_path[0] == "dat" ):
print( "dat" )
with open('./dat.json', 'w') as file_open:
pass
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
self.send_header('Access-Control-Allow-Headers', '*')
self.end_headers()
return
else:
print( "no" )
print( self.path )
return
def rest_server(port):
httpd_rest = http.server.ThreadingHTTPServer(("", port), RestHandler)
httpd_rest.serve_forever()
def main():
#Server startup
port_rest = 3333
try:
t1 = threading.Thread(target=rest_server, args=(port_rest,), daemon = True)
t1.start()
while True: time.sleep(1)
except (KeyboardInterrupt, SystemExit):
print("exit")
sys.exit()
if __name__ == "__main__":
main()
The HTTP server is set up on port 3333 by doing REST parsing with do_OPTIONS / do_POST / do_GET / do_DELETE of Python's BaseHTTPRequestHandler. The operation is simple, it updates (POST), gets the contents (GET), and initializes (DELETE) the "dat.json" file in the current directory.
REST usually accepts the following four types of requests.
It should be created according to this, but I decided not to prepare PUT because POST can be used as a substitute.
For "OPTIONS" that are not included in the 4 types, depending on the browser specifications, an OPTIONS request will be issued prior to the POST request. The "Access-Control-Allow-Methods" included in the header of the response to this OPTIONS request describes the methods that the server can handle, and if POST is included, the POST request is issued following the OPTIONS request. I will. This operation is called preflight, and it is a browser operation, so there are workarounds such as requesting from the server side, but if you prepare your own server, it is easier to support OPTIONS request, so make it compatible. I did.
I will explain each volume. do_OPTIONS
do_OPTIONS
def do_OPTIONS(self):
#Supports preflight request
print( "options" )
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
self.send_header('Access-Control-Allow-Headers', '*')
self.end_headers()
Create a response to the OPTIONS request described in the previous section. It simply returns a response.
do_POST
do_POST
def do_POST(self):
print( "post" )
local_path = self.path.strip("/").split("/")
#Get request
content_len = int(self.headers.get("content-length"))
body = json.loads(self.rfile.read(content_len).decode('utf-8'))
#Response processing
if( local_path[0] == "dat" ):
if(os.path.getsize('./dat.json')):
with open('./dat.json', 'r') as json_open:
json_load = json.load(json_open)
json_load.update(**body)
json_wraite = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False)
else:
json_wraite = json.dumps(body, sort_keys=False, indent=4, ensure_ascii=False)
with open('./dat.json', 'w') as json_open:
json_open.write(json_wraite)
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', '*')
self.end_headers()
return
else:
print( "no" )
print( self.path )
return
The POST process writes the JSON data received in the POST request to a file. We will follow the process in order from the top.
local_path = self.path.strip("/").split("/")
self.path contains the URL data of the request. Divide this by "/" and keep it in the array. For example, if the request is "servername / aaa / bbb / ccc /", the local_path will be as follows. local_path[0]='aaa' local_path[1]='bbb' local_path[2]='ccc'
content_len = int(self.headers.get("content-length"))
body = json.loads(self.rfile.read(content_len).decode('utf-8'))
It is a process to parse the json data received along with the request and store it in the dictionary type data.
#Response processing
if( local_path[0] == "dat" ):
if(os.path.getsize('./dat.json')):
with open('./dat.json', 'r') as json_open:
json_load = json.load(json_open)
json_load.update(**body)
json_wraite = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False)
else:
json_wraite = json.dumps(body, sort_keys=False, indent=4, ensure_ascii=False)
with open('./dat.json', 'w') as json_open:
json_open.write(json_wraite)
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', '*')
self.end_headers()
return
In the next stage, check local_path [0] and it will be the API definition when it is "dat". In the sample, only local_path [0] is used, but the API will be defined by checking local_path [1] and local_path [2] in sequence. The rest are simple file operations. The reason for checking the file size first is that if you load an empty file in json.load (), an error will occur, so if the file is empty, the received data will be written to the file as it is. (File operations around here are verbose and muddy.)
The part that creates the final response is the same as OPTIONS and does not consider error handling.
else:
print( "no" )
print( self.path )
return
This is when the URL is unexpected. Normally, you should return a 404 error, but since it is not a public service, it is boldly omitted.
do_GET
do_GET
def do_GET(self):
print( "get" )
local_path = self.path.strip("/").split("/")
#Response processing
if( local_path[0] == "dat" ):
print( "dat" )
if(os.path.getsize('./dat.json')):
with open('./dat.json', 'r') as json_open:
json_load = json.load(json_open)
else:
json_load = {}
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
self.send_header('Access-Control-Allow-Headers', '*')
self.send_header('Content-type', 'application/json;charset=utf-8')
self.end_headers()
body_json = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False)
self.wfile.write(body_json.encode("utf-8"))
return
else:
print( "no" )
print( self.path )
return
The basic part is the same as POST. It eliminates the JSON data acquisition from the request, reads the file, and puts the data in the response.
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
self.send_header('Access-Control-Allow-Headers', '*')
self.send_header('Content-type', 'application/json;charset=utf-8')
self.end_headers()
body_json = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False)
self.wfile.write(body_json.encode("utf-8"))
The difference from POST is that Content-type is added to the header and data is written to the body part.
do_DELETE
do_DELETE
def do_DELETE(self):
print( "delete" )
local_path = self.path.strip("/").split("/")
if( local_path[0] == "dat" ):
print( "dat" )
with open('./dat.json', 'w') as file_open:
pass
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
self.send_header('Access-Control-Allow-Headers', '*')
self.end_headers()
return
else:
print( "no" )
print( self.path )
return
DELETE simply opens the target file with the w attribute and closes it without doing anything to make it an empty file.
rest_server
rest_server
def rest_server(port):
httpd_rest = http.server.ThreadingHTTPServer(("", port), RestHandler)
httpd_rest.serve_forever()
It is a function that simply starts the server on the port specified by the argument.
main
main
def main():
#Server startup
port_rest = 3333
try:
t1 = threading.Thread(target=rest_server, args=(port_rest,), daemon = True)
t1.start()
while True: time.sleep(1)
except (KeyboardInterrupt, SystemExit):
print("exit")
sys.exit()
I am trying to start the server in a separate thread. In addition, the program ends when an end interrupt (Ctrl + C) is received from the keyboard.
When you start the script, the server will start on port 3333, so try executing the following command.
curl -X POST -H 'Content-Type:application/json' -d '{"key":"val"}' localhost:3333/dat
curl -X GET localhost:3333/dat
curl -X DELETE localhost:3333/dat
The command is localhost because it is from the same machine as the server. If you use another machine, try using the IP address of the machine that will be the server.
I was able to build a REST server for the time being. If you want to add an API, you can nest path analysis more and more. Also, since you can easily call another command, you can extend it so that it can also be used for system management.
Please try it.
Recommended Posts