一、问题的提出
因为web和原有系统互通的需要,而原系统已经有基于tcp的长连接API。由于web客户端是并发性的访问,而老系统的API是C2C的对等长连接通信,不允许多个客户端同时连接一个服务器。因此要在原有C系统与web间搭建起互通的适配层,将web的通信协议与内部系统间对接起。
二、解决方案
早先曾考虑用JNI的方式把api包装下,但JNI在处理字符串的时候,经常会有字符集转换的问题。再则,JNI搞定了java,那php、RoR等又得一个个的单独实现,比较费时,因此改用语言无关的方案。
基于这个需求,想来python最合适干这个,原因有三:
1、python标准库里的协议集最全面,随手可得不说,还有标准的doc和一些立即可以run的example,这对于为web提供服务来说,最容易不过;
2、python对c的集成也是非常容易的
3、python代码的简洁性是有目共睹的,包括简洁的语法、大量唾手可得的标志模块,如日志系统、线程等等
选定python,就着手开始下一步,选定与web的协议。考虑到web client不能也定死到python,应该允许使用php、java等任意web语言使用本模块的可能性。因此可选的方案必须要符合语言无关的特点(当然也是平台无关的),可选的方案有:webservice、xmlrpc、REST等。因为ws过于复杂而REST还需要自己定义参数格式,最后选择了xmlrpc这种既简洁又具有参数类型的协议(xmlrpc在参数的前面添加类别定义信息,一般的xmlrpc协议栈都会根据该定义将数据转换到与语言对应的类型上来)
至于原有CAPI的包装,已有非常多的文章,可以google下,笔者常用的是Boost.Python,非常简单,差不多是公式性代码,这里不再藉述。
三、具体实现
接下来就开始实现这个xmlrpcserver,python里就有一个SimpleXMLRPCServer的,在python的帮助里还有示例代码:
服务器侧:
from SimpleXMLRPCServer import SimpleXMLRPCServer # Create server server = SimpleXMLRPCServer(("localhost", 8000)) server.register_introspection_functions() # Register pow() function; this will use the value of # pow.__name__ as the name, which is just 'pow'. server.register_function(pow) # Register a function under a different name def adder_function(x,y): return x + y server.register_function(adder_function, 'add') # Register an instance; all the methods of the instance are # published as XML-RPC methods (in this case, just 'div'). class MyFuncs: def div(self, x, y): return x // y server.register_instance(MyFuncs()) # Run the server's main loop server.serve_forever()
客户端侧:import xmlrpclib s = xmlrpclib.Server('http://localhost:8000') print s.pow(2,3) # Returns 2**3 = 8 print s.add(2,3) # Returns 5 print s.div(5,2) # Returns 5//2 = 2 # Print list of available methods print s.system.listMethods()
windows下开两个pyshell,先运行服务器侧的代码,然后输入客户端的代码,马上就能看到结果。
一切进展顺利,然后新问题出现了。
三、性能优化与改进
这个适配层很通用,意味着可能被用来做为多个web与多个后台系统间的统一中间层,而这个SimpleServer里没有看到任何并发的概念,因此立即修改了server的一个函数,增加了time.sleep。果然,如我所料,全部调用都是串行的。这在web与后台系统的集成中是会经常发生的。
因此需要解决两个问题:
1、如何把两个请求独立开来处理,不能因为某个调用阻塞了其他所有的请求
2、web有超时的概念,这个xmlrpc该如何处理
先来看第一个问题,google了资料。有人说要改python库的代码,有人说用twisted。
虽然鄙人早先使用VC的时候,曾有过修改MFC代码的时候,但深知这种修改所带来的麻烦。在VC的工程里如果修改了MFC代码,几个人合作的项目要让别人也编译过是多么的麻烦不说,就算是自己过段时间重装了windows后再来修改几个月前的项目,也会遇到不少麻烦,因此不可取。twisted到其网站上查找xmlrpc,得到的答案是这个东西对xmlrpc的支持很勉强。因此还决定采用C++里的常用模式,copy出pyhton里的SimpleXMLRPCServer来,自己修改后重新命名成自己的。其实因为python强大的标准库,修改起来很简单。只需要:
from SimpleXMLRPCServer import SimpleXMLRPCDispatcher, SimpleXMLRPCRequestHandler from SocketServer import ThreadingTCPServer try: import fcntl except ImportError: fcntl = None class MyXMLRPCServer(ThreadingTCPServer, SimpleXMLRPCDispatcher): """Simple XML-RPC server. Simple XML-RPC server that allows functions and a single instance to be installed to handle requests. The default implementation attempts to dispatch XML-RPC calls to the functions or instance installed in the server. Override the _dispatch method inhereted from SimpleXMLRPCDispatcher to change this behavior. """ allow_reuse_address = True def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, logRequests=True, allow_none=False, encoding=None): self.logRequests = logRequests SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding) ThreadingTCPServer.__init__(self, addr, requestHandler) # [Bug #1222790] If possible, set close-on-exec flag; if a # method spawns a subprocess, the subprocess shouldn't have # the listening socket open. if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'): flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD) flags |= fcntl.FD_CLOEXEC fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
然后把原有代码里的SimpleXMLRPCServer换成MyXMLRPCServer就行了。查看ThreadingTCPServer的代码,可以看到其核心实际上是基类的ThreadingMixIn里的process_request方法,在每个request到来时,都是很简单的启动一个python里的threading:
def process_request(self, request, client_address): """Start a new thread to process the request.""" import threading t = threading.Thread(target = self.process_request_thread, args = (request, client_address)) if self.daemon_threads: t.setDaemon (1) t.start()
再明显不过,因此多线程问题得到解决。如果实际系统在运行的时候,发现这种“不受控”的开启线程过于“并发”,当然可以自己实现一个ThreadingMixIn,仿照Python库里的方式足组合一把。
遗留问题:
1、API调用的超时问题该如何解决
2、 Java客户端代码