From 093ab188f9dce19c78aa27912ef2881b4e1a759a Mon Sep 17 00:00:00 2001 From: wangguibao Date: Thu, 5 Sep 2019 16:31:59 +0800 Subject: [PATCH] Demo: HTTP interface via Python and PHP --- demo-client/CMakeLists.txt | 12 ++ demo-client/php/echo.php | 25 +++++ demo-client/php/text_classification.php | 101 +++++++++++++++++ demo-client/python/echo.py | 15 +++ demo-client/python/text_classification.py | 58 ++++++++++ doc/HTTP_INTERFACE.md | 131 ++++++++++++++++++++++ 6 files changed, 342 insertions(+) create mode 100644 demo-client/php/echo.php create mode 100644 demo-client/php/text_classification.php create mode 100644 demo-client/python/echo.py create mode 100644 demo-client/python/text_classification.py create mode 100644 doc/HTTP_INTERFACE.md diff --git a/demo-client/CMakeLists.txt b/demo-client/CMakeLists.txt index 72847a89..897b6278 100644 --- a/demo-client/CMakeLists.txt +++ b/demo-client/CMakeLists.txt @@ -77,6 +77,10 @@ install(TARGETS echo RUNTIME DESTINATION ${PADDLE_SERVING_INSTALL_DIR}/demo/client/echo/bin) install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/conf DESTINATION ${PADDLE_SERVING_INSTALL_DIR}/demo/client/echo/) +install(FILES ${CMAKE_CURRENT_LIST_DIR}/python/echo.py + DESTINATION ${PADDLE_SERVING_INSTALL_DIR}/demo/client/echo/python) +install(FILES ${CMAKE_CURRENT_LIST_DIR}/php/echo.php + DESTINATION ${PADDLE_SERVING_INSTALL_DIR}/demo/client/echo/php) install(TARGETS echo_kvdb RUNTIME DESTINATION ${PADDLE_SERVING_INSTALL_DIR}/demo/client/echo_kvdb/bin) @@ -108,6 +112,14 @@ install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/conf DESTINATION ${PADDLE_SERVING_INSTALL_DIR}/demo/client/text_classification/) install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/data/text_classification DESTINATION ${PADDLE_SERVING_INSTALL_DIR}/demo/client/text_classification/data) +install(FILES ${CMAKE_CURRENT_LIST_DIR}/python/text_classification.py + DESTINATION + ${PADDLE_SERVING_INSTALL_DIR}/demo/client/text_classification/python) +install(FILES ${CMAKE_CURRENT_LIST_DIR}/php/text_classification.php + DESTINATION + ${PADDLE_SERVING_INSTALL_DIR}/demo/client/text_classification/php) + + install(TARGETS ctr_prediction RUNTIME DESTINATION diff --git a/demo-client/php/echo.php b/demo-client/php/echo.php new file mode 100644 index 00000000..1892a715 --- /dev/null +++ b/demo-client/php/echo.php @@ -0,0 +1,25 @@ +value ,key1 => value1 ,.... +echo http_post('http://127.0.0.1:8010/BuiltinTestEchoService/inference', array("a" => 1, "b" => 0.5)); +?> diff --git a/demo-client/php/text_classification.php b/demo-client/php/text_classification.php new file mode 100644 index 00000000..804e1d85 --- /dev/null +++ b/demo-client/php/text_classification.php @@ -0,0 +1,101 @@ + $sample); + $labels[] = $label; + + unset($x); + unset($buffer); + unset($ids); + unset($sample); + unset($label); + } + + if (!feof($handle)) { + echo "Unexpected fgets() fail"; + return -1; + } + fclose($handle); +} + +function &http_connect($url) { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + // true是获取文本,不直接输出 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + // 强制curl不使用100-continue + curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:')); + // set header + curl_setopt($ch, + CURLOPT_HTTPHEADER, + array( + 'Content-Type: application/json' + ) + ); + + return $ch; +} + +function http_post(&$ch, $data) { + // array to json string + $data_string = json_encode($data); + + // post data 封装 + curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); + + // set header + curl_setopt($ch, + CURLOPT_HTTPHEADER, + array( + 'Content-Length: ' . strlen($data_string) + ) + ); + + // 执行 + $result = curl_exec($ch); + return $result; +} + +if ($argc != 2) { + echo "Usage: php text_classification.php DATA_SET_FILE\n"; + return -1; +} + +ini_set('memory_limit', '-1'); + +$samples = array(); +$labels = array(); +read_data($argv[1], $samples, $labels); +echo count($samples) . "\n"; + +// key value 数组,如果多,后面用逗号分开key =>value ,key1 => value1 ,.... + +$ch = &http_connect('http://127.0.0.1:8010/TextClassificationService/inference'); + +$count = 0; +for ($i = 0; $i < count($samples) - BATCH_SIZE; $i += BATCH_SIZE) { + $instances = array_slice($samples, $i, BATCH_SIZE); + echo http_post($ch, array("instances" => $instances)) . "\n"; +} + +curl_close($ch); + +?> diff --git a/demo-client/python/echo.py b/demo-client/python/echo.py new file mode 100644 index 00000000..cc14a4c4 --- /dev/null +++ b/demo-client/python/echo.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +import json +import urllib2 + +data = {"a": 1, "b": 0.5} +request_json = json.dumps(data) + +req = urllib2.Request("http://127.0.0.1:8010/BuiltinTestEchoService/inference") +req.add_header('Content-Type', + 'application/json') # 如果server版本在r31987后,不需要设置这个。 +try: + response = urllib2.urlopen(req, request_json, 1) + print response.read() +except urllib2.HTTPError as e: + print e.reason diff --git a/demo-client/python/text_classification.py b/demo-client/python/text_classification.py new file mode 100644 index 00000000..91c13cdb --- /dev/null +++ b/demo-client/python/text_classification.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +import json +import httplib +import sys +import os + +BATCH_SIZE = 10 + + +def data_reader(data_file, samples, labels): + if not os.path.exists(data_file): + print "Path %s not exist" % data_file + return -1 + + with open(data_file, "r") as f: + for line in f: + line = line.replace('(', ' ') + line = line.replace(')', ' ') + line = line.replace('[', ' ') + line = line.replace(']', ' ') + ids = line.split(',') + ids = [int(x) for x in ids] + label = ids[-1] + ids = ids[0:-1] + samples.append(ids) + labels.append(label) + + +if __name__ == "__main__": + """ main + """ + if len(sys.argv) != 2: + print "Usage: python text_classification.py DATA_FILE" + sys.exit(-1) + + samples = [] + labels = [] + ret = data_reader(sys.argv[1], samples, labels) + + conn = httplib.HTTPConnection("127.0.0.1", 8010) + # conn.putheader('Content-Type', 'application/json') # 如果server版本在r31987后,不需要设置这个。 + + for i in range(0, len(samples) - BATCH_SIZE, BATCH_SIZE): + batch = samples[i:i + BATCH_SIZE] + ids = [] + for x in batch: + ids.append({"ids": x}) + ids = {"instances": ids} + + request_json = json.dumps(ids) + + try: + conn.request('POST', "/TextClassificationService/inference", + request_json, {"Content-Type": "application/json"}) + response = conn.getresponse() + print response.read() + except httplib.HTTPException as e: + print e.reason diff --git a/doc/HTTP_INTERFACE.md b/doc/HTTP_INTERFACE.md new file mode 100644 index 00000000..64bbd70f --- /dev/null +++ b/doc/HTTP_INTERFACE.md @@ -0,0 +1,131 @@ +# HTTP Inferface + +Paddle Serving服务均可以通过HTTP接口访问,客户端只需按照Service定义的Request消息格式构造json字符串即可。客户端构造HTTP请求,将json格式数据以POST请求发给serving端,serving端**自动**按Service定义的Protobuf消息格式,将json数据转换成protobuf消息。 + +本文档介绍以python和PHP语言访问Serving的HTTP服务接口的用法。 + +## 1. 访问地址 + +访问Serving节点的HTTP服务与C++服务使用同一个端口(例如8010),访问URL规则为: + +``` +http://127.0.0.1:8010/ServiceName/inference +http://127.0.0.1:8010/ServiceName/debug +``` + +其中ServiceName应该与Serving的配置文件`conf/services.prototxt`中配置的一致,假如有如下2个service: + +```protobuf +services { + name: "BuiltinTestEchoService" + workflows: "workflow3" +} + +services { + name: "TextClassificationService" + workflows: "workflow6" +} +``` + +则访问上述2个Serving服务的HTTP URL分别为: + +``` +http://127.0.0.1:8010/BuiltinTestEchoService/inference +http://127.0.0.1:8010/BuiltinTestEchoService/debug + +http://127.0.0.1:8010/TextClassificationService/inference +http://127.0.0.1:8010/TextClassificationService/debug +``` + +## 2. Python访问HTTP Serving + +Python语言访问HTTP Serving,关键在于构造json格式的请求数据,可以通过以下步骤完成: + +1) 按照Service定义的Request消息格式构造python object +2) `json.dump()` / `json.dumps()` 等函数将python object转换成json格式字符串 + +以TextClassificationService为例,关键代码如下: + +```python +# Connect to server +conn = httplib.HTTPConnection("127.0.0.1", 8010) + +# samples是一个list,其中每个元素是一个ids字典: +# samples[0] = [190, 1, 70, 382, 914, 5146, 190...] +for i in range(0, len(samples) - BATCH_SIZE, BATCH_SIZE): + # 构建批量预测数据 + batch = samples[i: i + BATCH_SIZE] + ids = [] + for x in batch: + ids.append({"ids" : x}) + ids = {"instances": ids} + + # python object转成json + request_json = json.dumps(ids) + + # 请求HTTP服务,打印response + try: + conn.request('POST', "/TextClassificationService/inference", request_json, {"Content-Type": "application/json"}) + response = conn.getresponse() + print response.read() + except httplib.HTTPException as e: + print e.reason +``` + +完整示例请参考[text_classification.py](demo-client/python/text_classification.py) + +## 3. PHP访问HTTP Serving + +PHP语言构造json格式字符串的步骤如下: + +1) 按照Service定义的Request消息格式,构造PHP array +2) `json_encode()`函数将PHP array转换成json字符串 + +以TextCLassificationService为例,关键代码如下: + +```PHP +function http_post(&$ch, $data) { + // array to json string + $data_string = json_encode($data); + + // post data 封装 + curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); + + // set header + curl_setopt($ch, + CURLOPT_HTTPHEADER, + array( + 'Content-Length: ' . strlen($data_string) + ) + ); + + // 执行 + $result = curl_exec($ch); + return $result; +} + +$ch = &http_connect('http://127.0.0.1:8010/TextClassificationService/inference'); + +$count = 0; + +# $samples是一个2层array,其中每个元素是一个如下array: +# $samples[0] = array( +# "ids" => array( +# [0] => int(190), +# [1] => int(1), +# [2] => int(70), +# [3] => int(382), +# [4] => int(914), +# [5] => int(5146), +# [6] => int(190)...) +# ) + +for ($i = 0; $i < count($samples) - BATCH_SIZE; $i += BATCH_SIZE) { + $instances = array_slice($samples, $i, BATCH_SIZE); + echo http_post($ch, array("instances" => $instances)) . "\n"; +} + +curl_close($ch); +``` + +完整代码请参考[text_classification.php](demo-client/php/text_classification.php) -- GitLab