用手機(jī)寫代碼:基于 Serverless 的在線編程能力探索
隨著計(jì)算機(jī)科學(xué)與技術(shù)的發(fā)展,越來(lái)越多的人開始接觸編程,也有越來(lái)越多的在線編程平臺(tái)誕生。以 Python 語(yǔ)言的在線編程平臺(tái)為例,大致可以分為兩類:
一類是 OJ 類型的,即在線評(píng)測(cè)的編程平臺(tái),這類的平臺(tái)特點(diǎn)是阻塞類型的執(zhí)行,即用戶需要一次性將代碼和標(biāo)準(zhǔn)輸入內(nèi)容提交,當(dāng)程序執(zhí)行完成會(huì)一次性將結(jié)果返回;
另一類則是學(xué)習(xí)、工具類的在線編程平臺(tái),例如 Anycodes 在線編程等網(wǎng)站,這一類平臺(tái)的特點(diǎn)是非阻塞類型的執(zhí)行,即用戶可以實(shí)時(shí)看到代碼執(zhí)行的結(jié)果,以及可以實(shí)時(shí)內(nèi)容進(jìn)行內(nèi)容的輸入。
Anycodes
但是,無(wú)論是那種類型的在線編程平臺(tái),其背后的核心模塊( “代碼執(zhí)行器”或“判題機(jī)”)都是極具有研究?jī)r(jià)值,一方面,這類網(wǎng)站通常情況下都需要比要嚴(yán)格的“安全機(jī)制”,例如程序會(huì)不會(huì)有惡意代碼,出現(xiàn)死循環(huán)、破壞計(jì)算機(jī)系統(tǒng)等,程序是否需要隔離運(yùn)行,運(yùn)行時(shí)是否會(huì)獲取到其他人提交的代碼等;
另一方面,這類平臺(tái)通常情況下都會(huì)對(duì)資源消耗比較大,尤其是比賽來(lái)臨時(shí),更是需要突然間對(duì)相關(guān)機(jī)器進(jìn)行擴(kuò)容,必要時(shí)需要大規(guī)模集群來(lái)進(jìn)行應(yīng)對(duì)。同時(shí)這類網(wǎng)站通常情況下也都有一個(gè)比較大的特點(diǎn),那就是觸發(fā)式,即每個(gè)代碼執(zhí)行前后實(shí)際上并沒(méi)有非常緊密的前后文關(guān)系等。
隨著 Serverless 架構(gòu)的不斷發(fā)展,很多人發(fā)現(xiàn) Serverless 架構(gòu)的請(qǐng)求級(jí)隔離和極致彈性等特性可以解決傳統(tǒng)在線編程平臺(tái)所遇到的安全問(wèn)題和資源消耗問(wèn)題,Serverless 架構(gòu)的按量付費(fèi)模式,可以在保證在線編程功能性能的前提下,進(jìn)一步降低成本。所以,通過(guò) Serverless 架構(gòu)實(shí)現(xiàn)在線編程功能的開發(fā)就逐漸的被更多人所關(guān)注和研究。本文將會(huì)以阿里云函數(shù)計(jì)算為例,通過(guò) Serverless 架構(gòu)實(shí)現(xiàn)一個(gè) Python 語(yǔ)言的在線編程功能,并對(duì)該功能進(jìn)一步的優(yōu)化,使其更加貼近本地本地代碼執(zhí)行體驗(yàn)。
在線編程功能開發(fā)
在線執(zhí)行代碼 用戶可以輸入內(nèi)容 可以返回結(jié)果(標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯(cuò)誤等)

subprocess.PIPE:一個(gè)可以被用于 Popen 的stdin 、stdout 和 stderr 3 個(gè)參數(shù)的特殊值,表示需要?jiǎng)?chuàng)建一個(gè)新的管道;
subprocess.STDOUT:一個(gè)可以被用于 Popen 的 stderr 參數(shù)的輸出值,表示子程序的標(biāo)準(zhǔn)錯(cuò)誤匯合到標(biāo)準(zhǔn)輸出;
#?-*-?coding:?utf-8?-*-import?subprocesschild = subprocess.Popen("python %s" % (fileName),stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,shell=True)output = child.communicate(input=input_data.encode("utf-8"))print(output)
#?-*-?coding:?utf-8?-*-import?randomrandomStr?=?lambda?num=5:?"".join(random.sample('abcdefghijklmnopqrstuvwxyz',?num))path?=?"/tmp/%s"%?randomStr(5)
#?-*-?coding:?utf-8?-*-import jsonimport uuidimport randomimport?subprocess# 隨機(jī)字符串randomStr?=?lambda?num=5:?"".join(random.sample('abcdefghijklmnopqrstuvwxyz',?num))# Responseclass Response:def __init__(self, start_response, response, errorCode=None):self.start = start_responseresponseBody = {'Error': {"Code": errorCode, "Message": response},} if errorCode else {'Response': response}# 默認(rèn)增加uuid,便于后期定位responseBody['ResponseId'] = str(uuid.uuid1())self.response = json.dumps(responseBody)def __iter__(self):status = '200'response_headers = [('Content-type', 'application/json; charset=UTF-8')]self.start(status, response_headers)yield self.response.encode("utf-8")def WriteCode(code, fileName):try:with open(fileName, "w") as f:f.write(code)return Trueexcept Exception as e:print(e)return Falsedef RunCode(fileName, input_data=""):child = subprocess.Popen("python %s" % (fileName),stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,shell=True)output = child.communicate(input=input_data.encode("utf-8"))return output[0].decode("utf-8")def handler(environ, start_response):try:request_body_size = int(environ.get('CONTENT_LENGTH', 0))except (ValueError):request_body_size = 0requestBody = json.loads(environ['wsgi.input'].read(request_body_size).decode("utf-8"))code = requestBody.get("code", None)inputData = requestBody.get("input", "")fileName = "/tmp/" + randomStr(5)????responseData?=?RunCode(fileName,?inputData)?if?code?and?WriteCode(code,?fileName)?else?"Error"return Response(start_response, {"result": responseData})
print('HELLO WORLD')
我們通過(guò)響應(yīng)結(jié)果,可以看到,系統(tǒng)是可以正常輸出我們的預(yù)期結(jié)果:“HELLO WORLD” 至此我們完成了標(biāo)準(zhǔn)輸出功能的測(cè)試,接下來(lái)我們對(duì)標(biāo)準(zhǔn)錯(cuò)誤等功能進(jìn)行測(cè)試,此時(shí)我們將剛剛的輸出代碼進(jìn)行破壞:
print('HELLO WORLD)
結(jié)果中,我們可以看到 Python 的報(bào)錯(cuò)信息,是符合我們的預(yù)期的,至此完成了在線編程功能的標(biāo)準(zhǔn)錯(cuò)誤功能的測(cè)試,接下來(lái),我們進(jìn)行標(biāo)準(zhǔn)輸入功能的測(cè)試,由于我們使用的 subprocess.Popen() 方法,是一種阻塞方法,所以此時(shí)我們需要將代碼和標(biāo)準(zhǔn)輸入內(nèi)容一同放到服務(wù)端。測(cè)試的代碼為:
tempInput = input('please input: ')print('Output: ', tempInput)
當(dāng)我們使用同樣的方法,發(fā)起請(qǐng)求之后,我們可以看到:

系統(tǒng)是正常輸出預(yù)期的結(jié)果。至此我們完成了一個(gè)非常簡(jiǎn)單的在線編程服務(wù)的接口。該接口目前只是初級(jí)版本,僅用于學(xué)習(xí)使用,其具有極大的優(yōu)化空間:
超時(shí)時(shí)間的處理 代碼執(zhí)行完成,可以進(jìn)行清理
更貼近“本地”的代碼執(zhí)行器
import timeprint("hello world")time.sleep(10)tempInput = input("please: ")print("Input data: ", tempInput)
當(dāng)我們?cè)诒镜氐膱?zhí)行這段 Python 代碼時(shí),整體的用戶側(cè)的實(shí)際表現(xiàn)是:
系統(tǒng)輸出 hello world 系統(tǒng)等待 10 秒 系統(tǒng)提醒我們 please,我們此時(shí)可以輸入一個(gè)字符串 系統(tǒng)輸出 Input data 以及我們剛剛輸入的字符串
代碼與我們要輸入內(nèi)容一同傳給系統(tǒng) 系統(tǒng)等待 10 秒 輸出 hello world、please,以及最后輸 Input data 和我們輸入的內(nèi)容

業(yè)務(wù)邏輯函數(shù):該函數(shù)的主要操作是業(yè)務(wù)邏輯,包括創(chuàng)建代碼執(zhí)行的任務(wù)(通過(guò)對(duì)象存儲(chǔ)觸發(fā)器進(jìn)行異步函數(shù)執(zhí)行),以及獲取函數(shù)輸出結(jié)果以及對(duì)任務(wù)函數(shù)的標(biāo)準(zhǔn)輸入進(jìn)行相關(guān)操作等; 執(zhí)行器函數(shù):該函數(shù)的主要作用是執(zhí)行用戶的函數(shù)代碼,這部分是通過(guò)對(duì)象存儲(chǔ)觸發(fā),通過(guò)下載代碼、執(zhí)行代碼、獲取輸入、輸出結(jié)果等;代碼獲取從代碼存儲(chǔ)桶,輸出結(jié)果和獲取輸入從業(yè)務(wù)存儲(chǔ)桶; 代碼存儲(chǔ)桶:該存儲(chǔ)桶的作用是存儲(chǔ)代碼,當(dāng)用戶發(fā)起運(yùn)行代碼的請(qǐng)求, 業(yè)務(wù)邏輯函數(shù)收到用戶代碼后,會(huì)將代碼存儲(chǔ)到該存儲(chǔ)桶,再由該存儲(chǔ)桶處罰異步任務(wù); 業(yè)務(wù)存儲(chǔ)桶:該存儲(chǔ)桶的作用是中間量的輸出,主要包括輸出內(nèi)容的緩存、輸入內(nèi)容的緩存;該部分?jǐn)?shù)據(jù)可以通過(guò)對(duì)象存儲(chǔ)的本身特性進(jìn)行生命周期的制定;
獲取用戶的代碼信息,生成代碼執(zhí)行 ID,并將代碼存到對(duì)象存儲(chǔ),異步觸發(fā)在線編程函數(shù)的執(zhí)行,返回生成代碼執(zhí)行 ID; 獲取用戶的輸入信息和代碼執(zhí)行 ID,并將內(nèi)容存儲(chǔ)到對(duì)應(yīng)的對(duì)象存儲(chǔ)中; 獲取代碼的輸出結(jié)果,根據(jù)用戶指定的代碼執(zhí)行 ID,將執(zhí)行結(jié)果從對(duì)象存儲(chǔ)中讀取出來(lái),并返回給用戶;

# -*- coding: utf-8 -*-import osimport oss2import jsonimport uuidimport random# 基本配置信息AccessKey = {"id": os.environ.get('AccessKeyId'),"secret": os.environ.get('AccessKeySecret')}OSSCodeConf = {'endPoint': os.environ.get('OSSConfEndPoint'),'bucketName': os.environ.get('OSSConfBucketCodeName'),'objectSignUrlTimeOut': int(os.environ.get('OSSConfObjectSignUrlTimeOut'))}OSSTargetConf = {'endPoint': os.environ.get('OSSConfEndPoint'),'bucketName': os.environ.get('OSSConfBucketTargetName'),'objectSignUrlTimeOut': int(os.environ.get('OSSConfObjectSignUrlTimeOut'))}# 獲取獲取/上傳文件到OSS的臨時(shí)地址auth = oss2.Auth(AccessKey['id'], AccessKey['secret'])codeBucket = oss2.Bucket(auth, OSSCodeConf['endPoint'], OSSCodeConf['bucketName'])targetBucket = oss2.Bucket(auth, OSSTargetConf['endPoint'], OSSTargetConf['bucketName'])# 隨機(jī)字符串randomStr = lambda num=5: "".join(random.sample('abcdefghijklmnopqrstuvwxyz', num))# Responseclass Response:def __init__(self, start_response, response, errorCode=None):self.start = start_responseresponseBody = {'Error': {"Code": errorCode, "Message": response},} if errorCode else {'Response': response}# 默認(rèn)增加uuid,便于后期定位responseBody['ResponseId'] = str(uuid.uuid1())self.response = json.dumps(responseBody)def __iter__(self):status = '200'response_headers = [('Content-type', 'application/json; charset=UTF-8')]self.start(status, response_headers)yield self.response.encode("utf-8")def handler(environ, start_response):try:request_body_size = int(environ.get('CONTENT_LENGTH', 0))except (ValueError):request_body_size = 0requestBody = json.loads(environ['wsgi.input'].read(request_body_size).decode("utf-8"))reqType = requestBody.get("type", None)if reqType == "run":# 運(yùn)行代碼code = requestBody.get("code", None)runId = randomStr(10)codeBucket.put_object(runId, code.encode("utf-8"))responseData = runIdelif reqType == "input":# 輸入內(nèi)容inputData = requestBody.get("input", None)runId = requestBody.get("id", None)targetBucket.put_object(runId + "-input", inputData.encode("utf-8"))responseData = 'ok'elif reqType == "output":# 獲取結(jié)果runId = requestBody.get("id", None)targetBucket.get_object_to_file(runId + "-output", '/tmp/' + runId)with open('/tmp/' + runId) as f:responseData = f.read()else:responseData = "Error"return Response(start_response, {"result": responseData})
從存儲(chǔ)桶獲取代碼,并通過(guò) pexpect.spawn() 進(jìn)行代碼執(zhí)行; 通過(guò) pexpect.spawn().read_nonblocking() 非阻塞的獲取間斷性的執(zhí)行結(jié)果,并寫入到對(duì)象存儲(chǔ); 通過(guò) pexpect.spawn().sendline() 進(jìn)行內(nèi)容輸入;

import osimport reimport oss2import jsonimport timeimport pexpectAccessKey = {"id": os.environ.get('AccessKeyId'),"secret": os.environ.get('AccessKeySecret')}OSSCodeConf = {'endPoint': os.environ.get('OSSConfEndPoint'),'bucketName': os.environ.get('OSSConfBucketCodeName'),'objectSignUrlTimeOut': int(os.environ.get('OSSConfObjectSignUrlTimeOut'))}OSSTargetConf = {'endPoint': os.environ.get('OSSConfEndPoint'),'bucketName': os.environ.get('OSSConfBucketTargetName'),'objectSignUrlTimeOut': int(os.environ.get('OSSConfObjectSignUrlTimeOut'))}auth = oss2.Auth(AccessKey['id'], AccessKey['secret'])codeBucket = oss2.Bucket(auth, OSSCodeConf['endPoint'], OSSCodeConf['bucketName'])targetBucket = oss2.Bucket(auth, OSSTargetConf['endPoint'], OSSTargetConf['bucketName'])def handler(event, context):event = json.loads(event.decode("utf-8"))for eveEvent in event["events"]:code = eveEvent["oss"]["object"]["key"]localFileName = "/tmp/" + event["events"][0]["oss"]["object"]["eTag"]codeBucket.get_object_to_file(code, localFileName)foo = pexpect.spawn('python %s' % localFileName)outputData = ""startTime = time.time()try:timeout = int(re.findall("timeout(.*?)s", code)[0])except:timeout = 60while (time.time() - startTime) / 1000 <= timeout:try:tempOutput = foo.read_nonblocking(size=999999, timeout=0.01)tempOutput = tempOutput.decode("utf-8", "ignore")if len(str(tempOutput)) > 0:outputData = outputData + tempOutputtargetBucket.put_object(code + "-output", outputData.encode("utf-8"))except Exception as e:print("Error: ", e)if str(e) == "Timeout exceeded.":try:targetBucket.get_object_to_file(code + "-input", localFileName + "-input")targetBucket.delete_object(code + "-input")with open(localFileName + "-input") as f:inputData = f.read()if inputData:foo.sendline(inputData)except:passelif "End Of File (EOF)" in str(e):targetBucket.put_object(code + "-output", outputData.encode("utf-8"))return True# 程序拋出異常else:outputData = outputData + "\n\nException: %s" % str(e)targetBucket.put_object(code + "-output", outputData.encode("utf-8"))return False
import timeprint('hello world')time.sleep(10)tempInput = input('please: ')print('Input data: ', tempInput)


time.sleep(10)
tempInput = input('please: ')

總結(jié)
HTTP 觸發(fā)器的基本使用方法;對(duì)象存儲(chǔ)觸發(fā)器的基本使用方; 函數(shù)計(jì)算組件、對(duì)象存儲(chǔ)組件的基本使用方法,組件間依賴的實(shí)現(xiàn)方法;

社區(qū)官網(wǎng)

Serverless Devs
http://www.serverless-devs.com/https://github.com/Serverless-Devs/Serverless-Devshttps://serverlessdevs.resume.net.cn/zh-cn/desktop/index.htmlhttp://serverlessdk.oss.devsapp.net/docs/tutorial-dk/intro/react://serverlessdevs.resume.net.cn/zhcn/cli/index.htmlhttps://serverlesshub.resume.net.cn/#/hubs/special-view
?點(diǎn)擊原文,即可跳轉(zhuǎn)?Serbverless Devs!