本文实现的效果:输入服务名、方法名和参数,输出格式化后的请求结果
对dubbo和telnet有所了解的可以直接移步github
Dubbo和telnet
简介
Dubbo是阿里巴巴开源的一款RPC(Remote Procedure Call,远程过程调用)框架,用于实现分布式服务的跨服务调用,具有远程通讯、动态配置、地址路由等功能。
Dubbo基于dubbo协议,dubbo协议是TCP协议之上的协议,采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。
Telnet协议是TCP/IP协议族中的一员,是Internet远程登录服务的标准协议和主要方式。Telnet是常用的远程控制Web服务器的方法,具体介绍可以查看百度百科:Telnet_百度百科
Dubbo 2.0.5版本以上开始支持telnet命令调用,本文正是基于telnet命令来实现对Dubbo接口的调用。
使用telnet命令调用dubbo服务
编写代码之前,先简单了解一下如何使用telnet命令调用dubbo服务。
首先打开cmd,使用telnet命令连接服务器(如果提示指令不存在的话,在设置-启用或关闭Windows功能里勾选“Telnet客户端“并保存)
telnet 172.16.51.4 20885
其中172.16.51.4
是dubbo服务所在的服务器地址,20885
是dubbo服务指定的端口。
连接后按回车,出现dubbo>输入提示符表示已经连上了dubbo服务
telnet命令
- 输入
ls
并回车,列出所有可用的服务 - 输入
ls 服务名
,列出服务下所有的方法 - 输入
ls -l 服务名
,列出服务下的所有方法和参数 - 输入
invoke 服务名.方法名(调用参数)
,调用对应服务的dubbo接口 - 其它Telnet命令参考
telnet调用示例
调用UserInfoRpcService服务的getUserState方法,参数是一个Long类型数值
如下图,接口成功响应并返回了一个json字符串(默认不是utf-8编码,所以中文会乱码)
综上所述,使用telnet命令的方式调用dubbo接口最少需要三步:
1、查找服务的ip和端口(通常是在dubbo-admin或zookeeper查找)
2、telnet连接服务器
3、invoke命令调用接口
如果一次两次还好,如果用的多了,就会感觉很不方便:
1、每次都要查找对应服务的ip端口并手动连接
2、连接一段时间不操作,就会自动断开连接
3、invoke命令纯手输,输错了不能用光标移动到错误处修改,只能删除重新输入
4、响应不支持中文,且没有格式化,需要复制出来手动格式化才具有易读性
使用Python实现
既然手动输入telnet命令很不方便,那么使用代码实现可以做到哪些改进呢?
重复几次命令操作之后就会发现,除了调用时需要填写的服务名、方法名和参数之外,其余如查找ip端口、连接服务器、格式化响应都是重复性很高的操作,完全可以简化。
所以我们用Python实现的目标就是:只输入服务名
、方法名
、参数
并执行,就得到一个易读的返回结果。
通过服务名自动查找ip端口
首先人工查找服务的ip端口的步骤是可以省略的。
系统通过dubbo提供服务,需要将服务注册到指定的注册中心(通常是使用Zookeeper作为Dubbo的注册中心),并暴露其服务器ip和端口,既然如此,就可以通过注册中心查找对应服务的注册信息,来自动获取其ip和端口了。
在Python中,可以通过kazoo库来连接Zookeeper,代码如下:
from urllib.parse import unquote
from kazoo.client import KazooClient
# zookeeper的ip和端口
zk = {
'host': '172.16.253.21',
'port': 2181
}
class Zookeeper:
client = None
service_dict = {}
class ServiceNotAvailableError(ValueError):
pass
def __init__(self, timeout=100):
# 连接zookeeper
self.client = KazooClient('%s:%s' % (zk['host'], zk['port']), timeout=timeout)
self.client.start()
# 查找所有注册的dubbo服务
service_list = self.client.get_children('dubbo')
for service in service_list:
name = str(service).split('.')[-1] # 去掉包名,剩下的服务名作为key
self.service_dict[name] = service # 此处如果有重名的服务,会覆盖
def get_service_address(self, service):
"""获取指定服务的注册地址信息"""
if '.' not in service:
# 如果传入的服务名不带包名,就从service_dict找到完整服务名
service = self.service_dict[service]
uri = 'dubbo/%s' % service
if not self.client.exists(uri):
raise ServiceNotAvailableError('服务"%s"不存在' % service)
elif not self.client.exists('%s/providers' % uri):
raise ServiceNotAvailableError('服务"%s"没有提供者' % service)
else:
providers = self.client.get_children('%s/providers' % uri)
addrs = []
for provider in providers:
addr = str(unquote(provider)).split('/')[2]
addrs.append((str(addr).split(':')[0], str(addr).split(':')[1], str(addr)))
return addrs
def close(self):
self.client.stop()
self.client.close()
通过实例化Zookeeper对象,并调用get_service_address方法,就可以获得指定服务的ip地址和端口了。
实现telnet命令调用并格式化返回值
完成服务的ip端口查找后,开始进行接口调用,并将接口响应格式化成易读的形式。
在Python中,使用telnetlib库来完成telnet命令操作,代码如下:
import json
import telnetlib
... # 此处省略zk的代码
class DubboTester(telnetlib.Telnet):
class Args:
def __init__(self, service, method, params, host=None, port=0, index=0):
self.service = service
self.method = method
self.params = params
self.host = host
self.port = port
self.index = index
prompt = 'dubbo>'
coding = 'utf-8'
zk = Zookeeper()
args = None
def __init__(self, args: Args or dict):
"""
实例化DubboTester,这一步会连接到指定服务的服务器
:param args: 可以传Args对象实例,也可以传字典数据,字典最少要包含service、method、params,
params必须是list类型,list中的元素就是方法所需的参数
"""
# dict解析成Args对象
if isinstance(args, dict):
args = self.__init_args_from_dict(args) if isinstance(args, dict) else args
address_list = self.zk.get_service_address(args.service)
if len(address_list) > 1:
# 对于多节点服务,默认连接第一个节点,可用index指定
print('——' * 43)
print('|%s服务有多个地址,使用index参数指定请求地址,默认index=0:|' % str(args.service).center(30, ' '))
print('-' * 86)
for i, address in enumerate(address_list):
print('| %d ==> %s:%s |' % (i, address[0], str(address[1]).ljust(80 - len(address[2]), ' ')))
print('——' * 43)
args.host = address_list[args.index][0]
args.port = address_list[args.index][1]
print('当前连接地址: %s:%s' % (args.host, args.port))
self.args = args
super(DubboTester, self).__init__(host=args.host, port=args.port)
self.write(b'\n')
@staticmethod
def __init_args_from_dict(d):
service = d.get('service')
method = d.get('method')
params = d.get('params', [])
host = d.get('host')
port = d.get('port')
index = d.get('index', 0)
if port is not None and not isinstance(port, int):
raise TypeError('port必须是数值类型')
elif params is not None and not isinstance(params, list):
raise TypeError('params必须是list类型')
return DubboTester.Args(service, method, params, port, index)
@staticmethod
def __parse_args(args):
"""将参数解析成tenlet命令行调用的字符串格式"""
if isinstance(args, str) or isinstance(args, dict):
args = json.dumps(args)
elif isinstance(args, list):
tmp = ''
for arg in args:
tmp += json.dumps(arg) + ','
args = tmp[:-1]
return args
def command(self, flag, str_=""):
data = self.read_until(flag.encode())
self.write(str_.encode() + b'\n')
return data
def invoke(self):
arg = self.__parse_args(self.args.params)
command_str = "invoke {0}.{1}({2})".format(self.args.service, self.args.method, arg)
print(self.prompt, command_str)
self.command(self.prompt, command_str)
data = self.command(self.prompt, "")
# [:-6] 截取掉返回结果末尾的'dubbo>'
data = data.decode(self.coding, errors='ignore')[:-6].strip()
# 截取掉elapsed及之后的部分
if 'elapsed' in data:
data = data[:data.index('elapsed')].strip()
# 双换行符替换为单换行符
data = data.replace('\r\n', '\n')
return data
def close(self):
if self.zk:
self.zk.close()
def run(case: dict):
try:
tester = DubboTester(case)
result = tester.invoke()
try:
# 解析结果,结果缩进,支持中文
result = json.dumps(json.loads(result), ensure_ascii=False, sort_keys=True, indent=4)
except json.JSONDecodeError as e:
print(e)
print('请求结果:\n%s ' % result)
tester.close()
except TimeoutError:
print('连接超时!')
在run函数中,首先实例化一个DubboTester对象并调用invoke方法,执行telnet命令,得到返回的结果后,再尝试json解析并格式化,最后得到一个易读的返回结果。
执行
通过上述代码的实现,现在只需要传入一个包含service、method、params字段的字典类型数据,就可以调用对应的dubbo接口了:
if __name__ == '__main__':
# 输入服务名、方法名和参数
case = {
'service': 'UserInfoRpcService',
'method': 'getUserState',
'params': [600001]
}
# 执行
run(case)
执行结果如下:
完整代码:
#!/usr/bin/python3
import json
import telnetlib
from urllib.parse import unquote
from kazoo.client import KazooClient
test_env = {
'zk': '{ip}:{端口}'
}
env = test_env
class Zookeeper:
address = None
timeout = 100
client = None
service_dict = {}
def __init__(self, address=None, timeout=100):
self.address = address if address else env['zk']
self.timeout = timeout
self.client = KazooClient(hosts=self.address, timeout=timeout)
self.client.start()
service_list = self.client.get_children('dubbo')
for service in service_list:
name = str(service).split('.')[-1]
self.service_dict[name] = service
def get_service_address(self, service):
if '.' not in service:
service = self.service_dict[service]
uri = 'dubbo/%s' % service
if not self.client.exists(uri):
raise ValueError('服务%s不存在' % service)
elif not self.client.exists(uri + '/providers'):
raise ValueError('服务%s没有提供者' % service)
else:
providers = self.client.get_children(uri + '/providers')
addrs = []
for provider in providers:
addr = str(unquote(provider)).split('/')[2]
addrs.append((str(addr).split(':')[0], str(addr).split(':')[1], str(addr)))
return addrs
def close(self):
self.client.stop()
self.client.close()
class DubboTester(telnetlib.Telnet):
class Args:
host: str
port: int
service: str
index: int
method: str
param: list
def __init__(self, host=None, port=0, service=None, index=None, method=None, param=None):
self.host = host
self.port = port
self.service = service
self.index = index
self.method = method
self.param = param
prompt = 'dubbo>'
coding = 'utf-8'
zk = None
args: Args
def __init__(self, args: Args or dict or str):
# json字符串解析成dict
args = json.loads(args) if isinstance(args, str) else args
# dict解析成Args对象
args = self.__init_args_from_dict(args) if isinstance(args, dict) else args
if args.host and args.port:
print('当前模式:直连')
elif args.service is not None and args.service != '':
print('当前模式:zookeeper')
self.zk = Zookeeper()
address_list = self.zk.get_service_address(args.service)
if len(address_list) > 1 and args.index is None:
index = 0
print('——' * 43)
print('|%s服务有多个地址,使用index参数指定请求地址,默认index=0:|' % str(args.service).center(30, ' '))
print('-' * 86)
for i, address in enumerate(address_list):
print('| %d ==> %s:%s |' % (i, address[0], str(address[1]).ljust(80 - len(address[2]), ' ')))
print('——' * 43)
else:
index = 0 if args.index is None else args.index
print('当前使用地址:%s:%s' % (address_list[index][0], address_list[index][1]))
args.host = address_list[index][0]
args.port = address_list[index][1]
else:
raise KeyError('参数错误')
self.args = args
super(DubboTester, self).__init__(host=args.host, port=args.port)
self.write(b'\r\n')
def command(self, str_=""):
data = self.read_until(self.prompt.encode())
self.write(str_.encode() + b"\n")
return data
@staticmethod
def __init_args_from_dict(d: dict):
host = d.get('host')
port = d.get('port')
service = d.get('service')
index = d.get('index')
method = d.get('method')
param = d.get('param')
if port is not None and not isinstance(port, int):
raise TypeError('port参数类型错误,需要int类型,传入了%s类型' % type(port))
if param is not None and not isinstance(param, list):
raise TypeError('param参数类型错误,需要list类型,传入了%s类型' % type(param))
port = port if port else 0
return DubboTester.Args(host, port, service, index, method, param)
@staticmethod
def __parse_args(args):
if isinstance(args, str) or isinstance(args, dict):
args = json.dumps(args)
elif isinstance(args, list):
tmp = ''
for param in args:
tmp += json.dumps(param) + ','
args = tmp[:-1]
return args
def invoke(self, parse=True):
"""
调用dubbo接口
:param parse: 是否解析参数,如果为false,则直接使用self.args.param来请求接口,如果为True,则先将参数解析为json形式再请求
:return: 返回请求结果字符串
"""
if self.args.method is None:
raise KeyError('缺少method参数')
elif self.args.param is None:
raise KeyError('缺少param参数')
elif not isinstance(self.args.param, list):
raise TypeError('param参数必须是list类型')
arg = self.__parse_args(self.args.param) if parse else self.args.param
command_str = "invoke {0}.{1}({2})".format(self.args.service, self.args.method, arg)
# print('\033[33m' + self.prompt, command_str)
print(self.prompt, command_str)
self.command(command_str)
data = self.command('')
# [:-6]去掉返回结果末尾的'dubbo>'
data = data.decode(self.coding, errors='ignore')[:-6].strip()
# 去掉elapsed(如果返回结果中包含elapsed,会返回不完整的结果)
if 'elapsed' in data:
data = data[:data.index('elapsed')].strip()
# 双换行符替换为单换行符
data = data.replace('\r\n', '\n')
return data
def do(self, arg):
command_str = arg
self.command(command_str)
data = self.command('')
return data.decode(self.coding, errors='ignore').split('\r\n')[:-1]
def close(self):
if self.get_socket():
super(DubboTester, self).close()
if self.zk:
self.zk.close()
def get_service_address(self):
return self.host, self.port
def get_method_list(self, service_name, detail=False):
if detail:
method_list = self.do('ls -l %s' % service_name)
else:
method_list = self.do('ls %s' % service_name)
return [method for method in method_list]
def get_method_info(self, method='default'):
method = self.args.method if method == 'default' else method
if method is None or method not in self.get_method_list(self.args.service):
raise KeyError('%s不是%s的方法' % (method, self.args.service))
for item in self.get_method_list(self.args.service, True):
if method == str(item).split(' ')[1].split('(')[0]:
return item
def print_method_list(tester, package=None):
if package:
title = '* 服务%s的方法列表:' % package.split('.')[-1]
result = tester.get_method_list(package, True)
else:
title = '* %s:%s 的服务列表:' % tester.get_service_address()
result = [service for service in tester.do('ls')]
print('=' * 80)
print(title)
print('-' * 80)
for item in result:
print('*', item)
print('=' * 80)
def run(case: dict, parse=True, invoke=True, print_service=False, print_method=False, print_req_json=False):
try:
# 不调用接口时,默认打印服务或接口信息
if not invoke:
print_service = print_service if case['service'] else False
print_method = not print_service
tester = DubboTester(case)
print_method_list(tester) if print_service else None
print_method_list(tester, case['service']) if print_method else None
if print_req_json:
print(json.dumps('%s.%s::%s' % (case['service'], case['method'], json.dumps(case['param']))))
if invoke:
result = tester.invoke(parse)
try:
# 解析结果,结果缩进、支持中文
result = json.dumps(json.loads(result), ensure_ascii=False, sort_keys=True, indent=4)
# print('\033[32m')
except json.JSONDecodeError:
# print('\033[31m')
pass
if str(result).startswith('No such method') and case['method'] in tester.get_method_list(case['service']):
result = '方法参数错误:%s\n你的传参:%s' % (tester.get_method_info(), [type(param) for param in case['param']])
print('请求结果:\n%s' % result)
tester.close()
except TimeoutError:
print('连接超时,请检查ip和端口!')
except (KeyError, ValueError, TypeError) as e:
print(e)