一开始就上手各种web框架并不是特别好,因为很多原理层面的东西都很少去了解了。最近在研究python的socket和多线程,于是写了个多线程的socket服务器,能调用相应的脚本来处理自定义的socket请求,客户端用的是php,当然,不一定用php,只要能socket编程即可。
首先,python服务器需要能完整接收从php发来的socket信息,并从中获取相应的调用信息,因为是纯字符交互,为了方便,将双方的输出转为json字符串。其次为了完整接收信息,还要定义一个字符传输是否完整的标准:在字符串开头定义整个字符串的长度。然后涉及到服务器里动态调用python脚本的问题(因为输入到服务器的参数都是字符串),此处使用__import__()函数将模块赋值给某一变量并使用exec()函数执行导出结果即可,调用完毕可能还需要释放调用的模块或脚本,使用del语句可达到此目的。
# -*- coding: utf-8 -*-
import socket
import threading
import json
import sys
HOST = '127.0.0.1' #绑定监听的ip,此时只有本机可以访问,为空或者0.0.0.0时外界可访问
PORT = 32123 #绑定监听的端口
RECVBUFFER = 1024 #socket接收字节流的缓冲大小,单位为字节
MAXTHREADS = 100 #开启监听的线程数
RELEASEPKG = True #是否在某线程执行完毕一次request后释放客户端请求加载的脚本
APPFOLDER = 'app' #普通脚本存放的位置,即在此文件夹写各种python脚本等待客户端调用
class Gear(threading.Thread):
def __init__(self, socket):
#将threading.Thread()作为超(父)类继承并初始化
threading.Thread.__init__(self)
self.socket = socket
def run(self):#将需要在子线程里执行的方法写进run()里重载threading.Thread()的run()方法即可
while True:
self.socket.listen(5)
clientsock, clientaddr = self.socket.accept()
print 'Got connection from', clientsock.getpeername()
print self.getName(), 'is handling this connection.'
datasize = clientsock.recv(11) #接收前11位形如29.00000000的字符串来得知所需接收数据的大小
try:
datasize = int(float(datasize))
except:
datasize = 0
data = ''
#当还有数据时,将数据接收并连接给data变量;python没有do...while语句,所以用while+break来控制此流程
while True:
recv = clientsock.recv(RECVBUFFER)
datasize -= RECVBUFFER
data += recv
if datasize <= 0:
break
try:
datamap = json.loads(data)
except:
datamap = {'entry': '', 'func': '', 'param': []}
#entry代表某个脚本,func代表需要调用的函数名,param是提供给此函数的参数组
entry = datamap['entry']
func = datamap['func']
param = datamap['param']
e = ''
try:
#将调用的脚本当成模块载入
src = __import__(APPFOLDER + '.' + entry, globals(), locals(), -1)
if len(param) == 0:
try:
exec("result = src." + func + "()")
except Exception as e:
result = []
else:
#将参数列表序列化为字符串
for i in range(0, len(param)):
param[i] = str(param[i])
paramstring = "','".join(param)
try:
#执行客户端需要调用的方法并获取结果
exec("result = src." + func + "('" + paramstring + "')")
except Exception as e:
result = []
if RELEASEPKG:
try:
#删除(释放)动态调用的模块
exec("del sys.modules['" + APPFOLDER + '.' + entry + "']")
except:
pass
except Exception as e:
result = []
send = json.dumps({'data': result, 'error': str(e)}) #error为捕获app文件夹下python脚本的错误信息
#计算输出字符串的长度一起发送给客户端
sendsize = len(send)
sendsize = str(sendsize) + '.'
if len(sendsize) > 11:
pass
while len(sendsize) < 11:
sendsize += '0'
send = sendsize + send
clientsock.send(send)
clientsock.close()
class Engine():
def __init__(self):
self.socket = None
def run(self):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
self.socket.bind((HOST, PORT))
except:
print 'PPY ERROR[PYTHON]: socket.bind() error'
raise SystemExit
i = 0
while i < MAXTHREADS:
#实例化子线程并让其开始执行等待客户端调度
gear = Gear(self.socket)
gear.start()
i += 1
if __name__ == '__main__':
engine = Engine()
engine.run()
#end
这样,一个小型的调用python脚本的多线程pythonsocket服务器就构造完毕了,如果输出是
HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nhello, world.
就成了一个HTTP服务器。当然也可以直接用浏览器访问这个socket服务器,并修改此脚本把浏览器的输入打印出来,就能对HTTP协议有一个底层上的大概了解了。
其实如此动态地调用python脚本和各种python框架差不多,是框架最初的雏形,在此基础上增加各种api来方便使用的话,就成了一个完整的框架了,当然,这也是为什么python脚本不能像php那样随便扔一个文件夹外部就能访问到,因为单个python脚本是”不具备”输出HTTP头信息的能力的,除非你不嫌麻烦和效率低下调用服务器的CGI并一个个文件地去写头信息。
如果想让脚本后台一直运行,使用
nohup python ***.py &
指令即可。
要停止脚本,查找其进程再终止即可:
ps aux | grep python
kill -s 15 12345 #12345为进程id
接下来的php客户端脚本就简单多了,只要发送参数并接收结果就完事了:
<?php
define('PPY_HOST', '127.0.0.1');
define('PPY_PORT', '32123');
define('PPY_RECVBUFFER', 1024); //socket接收数据时缓冲的大小,单位为字节
function exchange($entry='', $func='', $param=array()){ //依次是要访问的python脚本名,脚本里的函数名以及函数的参数
if (!isset($entry{0}) || !isset($func{0})){
die('PPY ERROR[PHP]: PARAM entry && func must be specified');
}
if (!is_array($param)){
die('PPY ERROR[PHP]: PARAM param must be an array');
}
$c = socket_create(AF_INET, SOCK_STREAM, 0);
if (socket_connect($c, PPY_HOST, PPY_PORT) === false){
die('PPY ERROR[PHP]: socket_connect() error');
};
$send = json_encode(array('entry' => $entry, 'func' => $func, 'param' => $param));
$sendsize = strlen($send);
$sendsize .= '.';
if (strlen($sendsize) > 11){
die('PPY ERROR[PHP]: Too much data.');
}
while (strlen($sendsize) < 11){
$sendsize .= '0';
}
$send = $sendsize . $send;
if (socket_write($c, $send) === false){
die('PPY ERROR[PHP]: socket_write() error');
}
if (($datasize = socket_read($c, 11)) === false){
die('PPY ERROR[PHP]: socket_read() error');
}
$data = '';
do {
if (($recv = socket_read($c, PPY_RECVBUFFER)) === false){
die('PPY ERROR[PHP]: socket_read() error');
}
$datasize -= PPY_RECVBUFFER;
$data .= $recv;
}
while ($datasize > 0);
socket_close($c);
return $data;
}
//测试:
print_r(exchange('h', 'hello'));
//end.
在python服务器脚本旁边的app文件夹下新建__init__.py和h.py脚本,其中__init__.py留空内容,h.py里面写入hello方法:
def hello():
return 'hello, world.'
此时目录结构看起来应该是这样的:
然后在浏览器里访问php脚本,就能看到hello world啦