flask 返回json_Flask模拟NRF

1.说明

1)本文主要介绍:

  • 如何使用Python的Flask模块来模拟NRF。

  • 如何使用浏览器和Python模拟发起5G网元的注册、更新、注销、发现等流程。

2)用到的素材包括:

  • Ubuntu虚机(Server):版本任意,用来模拟NRF,所有Flask脚本都跑在上面。

  • Windows电脑:要有浏览器和Python3,用来模拟Client访问NRF。

3)连接拓扑如图:

134ba45663f625b06e2c99ae560909e7.png

4)声明:代码中出现的IMSI,FQDN等信息仅为案例参考,请勿套用实际工作。

5)本文所有材料已打包:

https://share.weiyun.com/vkPFVU8q

2.Server安装Flask模块

1)首先安装Flask:

pip install flask

3)创建Flask的工作目录:

mkdir -p mypro/templates    # Flask默认用来存网页模板mkdir -p mypro/static    # Flask默认用来存静态数据mkdir -p mypro/5gc    # 用来存模拟的5GC网元数据cd mypro     # 跳转到Flask的工作目录

3.第一个Flask脚本:Hello world!

1)先体验一下Flask,在Ubuntu虚机中写脚本:vi app.py

# _*_ coding: utf-8 _*_# 设置utf-8编码支持中文,否则中文注释运行脚本可能会报错# 导入flask模块的Flask包from flask import Flask# 实例化flask对象app = Flask(__name__)# flask内置装饰器,能让对URL'/'的访问交给hello_world函数处理@app.route('/') # 这里的字符串'/'叫URL规则def hello_world():    # 返回字符串Hello World    return 'Hello, World!'

2)运行Flask:

# 写环境变量,方便测试cat << EOF >> ~/.bashrc# 默认运行的脚本名称export FLASK_APP=app.py# 打开开发者开关,这样修改app后不用重启脚本即可自动重载配置export FLASK_ENV=development# 打开调试功能,这样网页可以直线显示报错,加快定位export FLASK_DEBUG=1EOFsource ~/.bashrc# 正式运行,这里是用nohup放到后台运行# 默认监听地址127.0.0.1,可用--host自定义# 默认监听端口5000,可用--port自定义nohup flask run --host 192.168.70.138 > /var/log/flask.log 2>&1 &

3)电脑浏览器访问虚机:

http://192.168.70.138:5000

f4a6c4dae385b748cc888153af25f7ad.png运行成功。

4.Flask的工作过程

1)用户在浏览器输入URL,访问某个资源。

2)Flask接收用户请求并分析请求的URL。

3)为这个URL找到对应的处理函数。

4)执行函数并生成响应,返回给浏览器。

5)浏览器接收并解析响应,将信息显示在页面中。

这是标准的CS(Client/Server)模式。

5.NRF的工作过程

1)5G网络中,NRF主要提两种服务:

  • NF管理:注册、更新、注销、状态订阅,状态通知,取消状态订阅。

  • NF发现:提供NF发现服务。

9cc4e133ff064300605775735e80c281.png

(图 3GPP TS 23.502-5.2.7.1)

2)NRF的工作过程也是CS模式,区别是:

  • 5G网元使用HTTP2,Flask默认HTTP。

  • 5G网元请求完收到数据交给后端处理,无浏览器。

  • 5G网元使用REST API风格定义资源,和URL有区别。

3)虽然有以上3个明显的区别,但我们仍然可以利用Flask的能力来模拟网元的交互过程,加深对5G网元交互的理解。

4)另外,实际当中,除了23.502中列出的NRF支持的操作外,29.510提到NRF还支持:

  • 获取实例集合,query是可选参数,用作查询条件。

  • 获取某个实例的信息。

902bf816a86123854c1d43e0a2072d64.png

7351c5436024fdc4915c47820e4bf536.png

bebe47d644ab82a2d0f61ec0caf5c1d3.png

(图 3GPP TS 29.510 5.2.2)

6.获取NRF上的实例集合

1)首先简单看一下5G网元的API结构:(3GPP TS 29.501 4.4.1)

格式:{apiRoot}/<apiName>/<apiVersion>/<apiSpecificResourceUriPart>案例:http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances
  • apiRoot:http://192.168.70.138:5000,协议地址端口

  • apiName:nnrf-nfm

  • apiVersion:v1

  • apiSpecificResourceUriPart:nf-instances

2)接下来让我们直接上手模拟查询NRF上的实例集合。给app.py添加如下内容:vi app.py

# 导入flask内嵌json模块,用于处理json数据和文件from flask import Flask, json# 封装一个加载json文件的函数,供其他函数调用。5G网元大部分数据都是通过JSON传递。def load_file(file_name):    with open('5gc/%s.json' % file_name) as f:        data = json.loads(f.read())    return data# NRF的获取实例集合的API@app.route('/nnrf-nfm/v1/nf-instances')def list_instance():    # 这是一个提前写好的数据文件,里面存了一些实例样本    return load_file('ins') # 只传名称即可

3)把材料包中的5gc.zip解压到5gc.zip目录下:

节点实例一般以uuid命令,为方便区分,我把uuid改成了节点名称,如amf05.json。

c1a9d6e807c23cfeb1aadd4048eb85db.png

4)访问路径:

http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances

8edb3c19eea7183c90b68e6254e7293c.png

查到了10个节点的API

4)抓包看返回的数据是JSON:(实际从浏览器也能看出来)

ad7fb8fde6ef6cf6212f25c19a20bf6d.png

7.按条件查询实例

1)前文说到nf-instances后面可以加筛选条件,参数如何构建呢?先来看一个URL示例:

格式:scheme://netloc/path;params?query#fragment案例:http://www.baidu.com/index.html;user?id=5#comment
  • scheme:// :http://,协议

  • netloc:www.baidu.com,域名

  • path:/index.html,资源路径

  • params:user,参数

  • query:id=5,查询条件,多个查询条件用&割开

  • fragment:comment,锚点,用#与前面内容隔开

2)对应5G按条件查询就是:

http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances?num=5&type=amf

这里num和type就是参数,&是多个参数的分隔符。

以上参数是我自定义的,实际网元并没有使用num和type作为查询条件。

3)用Flask实现如下,修改app.py:vi app.py

# 导入flask中的request包,它会自动封装客户的请求报文from flask import Flask, json, request@app.route('/nnrf-nfm/v1/nf-instances')def list_instance():    # args是request封装的客户端请求的查询参数字典    num = request.args.get('num')    # 如果查询参数num是5,返回ins5    if num == '5':        # ins5.json里只放了5个实例        return load_file('ins5')    else:        return load_file('ins')

4)电脑访问如下URL:

http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances?num=5

e14b67d00a7c143a1c315fe6d9994b4c.png

5)感兴趣的可以直接:return request.args

def list_instance():        return request.args
http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances?num=5&type=amf&date=093

e0c0fe9abcbd7a6f8a71645dedeb8726.png

8.查询某实例详细信息

1)通过查询集合只能得到实例API,并不能得到实例的详细信息,想查看实例的详细信息,需要单独发请求,如前面5个实例的API。

http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/0c765084-9cc5-49c6-9876-ae2f5fa2a604http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/72a57feb-92b7-469d-92a2-babae9f8a7a3http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/f1bb352f-3cd4-4843-9125-f590d6ad8c7bhttp://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/de9f9fd9-ff3f-4255-a635-51f2e11c92fdhttp://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/13a1de33-ec45-4cd6-a842-ce5bb3cba3d9

观察发现这些API只有instances后面的内容不同(每节点有自己的UUID),是否有办法简单汇总成一个URL,然后交给一个函数处理?答案是可以的,现在请出URL规则中的变量。

2)在app.py中添加内容:vi app.py

# 为了方便区分文件,写一个uuid前缀映射成文件名的字典# 如果不考虑区分,代码会简单更简单uton = {    "f1bb352f-3cd4-4843-9125-f590d6ad8c7b": "amf03"      "60b08736-f384-46bd-b990-63b1a4f2a61c": "amf04"      "72a57feb-92b7-469d-92a2-babae9f8a7a3": "amf05"      "0c765084-9cc5-49c6-9876-ae2f5fa2a604": "ausf04"      "13a1de33-ec45-4cd6-a842-ce5bb3cba3d9": "nssf04"      "de9f9fd9-ff3f-4255-a635-51f2e11c92fd": "pcf02"      "fefd85ba-d52f-41e9-b3f3-100920000001": "smf03"      "5061c517-8ede-4c3a-a465-b151bd2e8e49": "smf04"      "123e4567-e89b-42d3-4456-426655440004": "udm04"      "bb2a33fd-5b16-4b86-9d14-249f12f45b93": "udr04"}# 写一个新URL规则,填入,即可在函数中使用@app.route('/v1/nf-instances/')def get_instance(uuid):    if uuid in uton.keys():        return load_file(uton[uuid])    else:        # 如果uuid不在字典中,说明uuid错误,return后面可以自定义错误代码        # 不写默认200        return {'Message': 'Wrong instance id'}, 400

需要注意,以下2个URL规则是不同的,前者没有下一层的path'/',后者有,访问后一个不会被前一个匹配,多了一个'/'就不同。

  • /v1/nf-instances

  • /v1/nf-instances/

3)各节点对应的instance id可以用以下命令查看:

grep -i nfInstanceId *

ba6d090039ac36c78990acc2793eefc9.png

4)尝试查询节点信息:

http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/0c765084-9cc5-49c6-9876-ae2f5fa2a604

d347b441e48d75ab0addec7018c12318.png

9.模拟节点注册

1)前文只是看已注册的节点,现在我们来模拟注册节点到NRF,这步需要Windows使用python爬虫提交json数据。

2)先看下3GPP的图:(图 3GPP TS 29.510 5.2.2.2)

519b0305d6015dd2461d868995807123.png

节点注册用PUT方法,API最后是节uuid,成功NRF需要返回201。

3)因为注册和查看实例信息的URL规则相同,所以需要修改之前的查看实例函数:vi app.py

# 封装一个将数据保存为json文件的函数def save_file(file_name, data):    with open('5gc/%s.json' % file_name, 'w') as f:        f.write(json.dumps(data, indent=2, ensure_ascii=False))    return 0# 限制请求的方法@app.route('/nnrf-nfm/v1/nf-instances/', methods=['PUT', 'GET'])def get_instance(uuid):    if request.method == 'GET':        if uuid in uton.keys():            return load_file(uton[uuid])        else:            return {'Message': 'Wrong instance id'}, 400    # 如果请求的方法是PUT,使用request内置的获取json方法获取其数据    elif request.method == 'PUT':        j_data = request.get_json()        save_file(uuid, j_data)        # 添加节点和文件名的映射字典中        uton[uuid] = uuid        return {'Message': 'Created.'}, 201    else:        return {'Message': 'Wrong method of request'}, 400

4)在自己的电脑里写个爬虫:simu_reg.py

from urllib import requestimport jsonurl = 'http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/11111111-aaaa-bbbb-cccc-dddddddddddd'headers = {'user-agent': 'AMF', 'content-type': 'application/json'}# 准备提交的数据,简单起见,我只写了一点amf相关信息data = {    "amfInfo": "amf06",    "fqdn": "amf06.amf.5gc.mnc007.mcc460.3gppnetwork.org",    "ipv4Addresses": "192.102.1.8",    "nfInstanceId": "11111111-aaaa-bbbb-cccc-dddddddddddd",    "nfStatus": "REGISTERED",    "nfType": "AMF"}j_data = json.dumps(data)b_data = bytes(j_data, encoding='utf-8')req = request.Request(url, data=b_data, headers=headers, method='PUT')r = request.urlopen(req)print(r.read().decode('utf-8'))

5)先用浏览器查询一下,提示错误的instance id。

a45416ea8f80aa13a972098f6d695e5f.png

6)运行爬虫:成功注册

fa941e24fc8f96a77d7903dac217b0e1.png

18404e86dc33a739b1044c5771ba04f1.png

7)再次用浏览器查询能够查询到,说明注册成功

ce90a2655f3d5a66c96fe5bf23f61bf4.png

10.模拟节点更新

1)先看看3GPP中的更新流程:(图 TS 29.510 5.2.2.3.1)

798bb8d3df5c7ab81b7502851aa71b96.png

6a33f0108d3cd2023c52111208bdc1a1.png

规范中更新有2种方式:

  • PUT:完全替换更新

  • PATCH:部分替换更新

另外从API上看它和注册,是相同的,所以还是要修改的查询实例函数。

2)修改之前的查看实例函数:vi app.py

# 扩充PATCH方法@app.route('/nnrf-nfm/v1/nf-instances/', methods=['PUT', 'GET', 'PATCH'])def get_instance(uuid):    # GET都是查询    if request.method == 'GET':        if uuid in uton.keys():            return load_file(uton[uuid])        else:            return {'Message': 'Wrong instance id'}, 400    # PUT有可能是更新,也可能是注册,所以需要判断一下在字典中有没有    elif request.method == 'PUT':        j_data = request.get_json()        save_file(uuid, j_data)        if uuid not in uton.keys():            uton[uuid] = uuid            return {'Message': 'Created'}        else:            return {'Message': 'Updated'}    # PATCH是部分更新    elif request.method == 'PATCH':        i_data = request.get_json()        if uuid in uton.keys():            # 加载历史数据,更换相应值            o_data = load_file(uton[uuid])            for item in i_data.keys():                o_data[item] = i_data[item]            save_file(uton[uuid], o_data)            return {'Message': 'Patched'}        else:            return {'Message': 'Your instance ID does not exist'}, 400    else:        return {'Message': 'Wrong request method'}, 400

3)windows电脑写2个脚本:simu_update.py

# update是整体更新,过程实际和注册相同,只是返回值是200,不是201。from urllib import requestimport jsonurl = 'http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/11111111-aaaa-bbbb-cccc-dddddddddddd'headers = {'user-agent': 'AMF', 'content-type': 'application/json'}# 这里替换了nfstatus为暂停,相当于AMF通知NRF,AMF下线了。data = {    "amfInfo": "amf06",    "fqdn": "amf06.amf.5gc.mnc007.mcc460.3gppnetwork.org",    "ipv4Addresses": "192.102.1.8",    "nfInstanceId": "11111111-aaaa-bbbb-cccc-dddddddddddd",    "nfStatus": "SUSPEND",    "nfType": "AMF"}j_data = json.dumps(data)b_data = bytes(j_data, encoding='utf-8')req = request.Request(url, data=b_data, headers=headers, method='PUT')r = request.urlopen(req)print(r.read().decode('utf-8'))

simu_patch.py

from urllib import requestimport jsonurl = 'http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/11111111-aaaa-bbbb-cccc-dddddddddddd'headers = {'user-agent': 'AMF', 'content-type': 'application/json'}# 这里将状态再改回到REGISTEREDdata = {"nfStatus": "REGISTERED"}j_data = json.dumps(data)b_data = bytes(j_data, encoding='utf-8')req = request.Request(url, data=b_data, headers=headers, method='PATCH')r = request.urlopen(req)print(r.read().decode('utf-8'))

4)修改app.py后flask自动重加载,uton字典重置,刚刚新注册的节点需要重新注册,之后才能验证更新。先运行注册爬虫:simu_reg.py

fa941e24fc8f96a77d7903dac217b0e1.png

5)运行:simu_update.py

3096b9ec6a97a1629bbdcfd95e0dacec.png

6)再运行:simu_patch.py

dee24a053962e3bff9dfac48e57bcd1c.png

7)检查文件变动效果:

e506f17836b962a950e34d2c745a7567.png

11.模拟节点注销

1)图3GPP TS 29.510 5.2.2.4

86ff55fa889e63dfd3d39aeedec42b4f.png

节点注销用DELETE,API和查看实例等一样,所以又要改一下查看实例函数,成功回复代码204。

2)修改app.py如下:vi app.py

# 添加delete方法@app.route('/nnrf-nfm/v1/nf-instances/', methods=['PUT', 'GET', 'PATCH', 'DELETE'])def get_instance(uuid):    if request.method == 'GET':        if uuid in uton.keys():            return load_file(uton[uuid])        else:            return {'Message': 'Wrong instance id'}, 400    elif request.method == 'PUT':        j_data = request.get_json()        save_file(uuid, j_data)        if uuid not in uton.keys():            uton[uuid] = uuid            return {'Message': 'Created'}        else:            return {'Message': 'Updated'}    elif request.method == 'PATCH':        i_data = request.get_json()        if uuid in uton.keys():            o_data = load_file(uton[uuid])            for item in i_data.keys():                o_data[item] = i_data[item]            save_file(uton[uuid], o_data)            return {'Message': 'Patched'}        else:            return {'Message': 'Your instance ID does not exist'}, 400    # 当时删除的时候,删除字典的映射。实际文件还保留。    elif request.method == 'DELETE':        if uuid in uton.keys():            del uton[uuid]            # 回复204            return '', 204        else:            return {'Message': 'Your instance ID does not exist'}, 400    else:        return {'Message': 'Wrong request method'}, 400

3)运行效果

同样是需要先注册节点,再注销节点,不然会报错。

a63405513d46dcee205aa5e560337d17.png

4)抓包看成功返回204:

54cf596cc22c30f4815e0d1b587734b7.png

12.模拟节点发现

1)5G网元注册到了NRF上之后,其他节点想要使用,需要向NRF发起服务查询(发现),因此节点想真正提供服务,发现也是重要的环节。

2)先看图 3GPP TS 29.510 5.3.2.2.2-1

da2a5da1d59f7b0d8207dbecdcebb480.png

使用GET,回复200,查询节点集合相似。

3)对于节点发现,3GPP TS 23.502 5.2.7.3.2描述必须有3个基本查询参数:

  • target-nf-type:目标节点类型

  • service-names:目前节点能提供的服务名称

  • requester-nf-type:查询者的节点类型

针对节点有如下可选参数:图摘自 51学通信公众号《5GC中的网元发现与选择》

046f8dc4951f8c19e55d3434f55b8e1c.png

综上,flask使用request.args对象就基本可以满足需求了。

4)添加discovery_node函数:vi app.py(注意发现节点的API名称是nnrf-disc,3GPP TS 29.510 5.1-1)

# 导入随机模块import random# 写一个网元类型和网元的字典tton = {    'AMF': ['amf03', 'amf04', 'amf05'],    # 'SMF': ['smf03', 'smf04'],    'AUSF': 'ausf04',    'UDM': 'udm04',    'PCF': 'pcf02',    'UDR': 'udr04',    'NSSF': 'nssf04'}# 节点发现URL的URL规则@app.route('/nnrf-disc/v1/nf-instances')def discovery_node():    # 获取请求中的参数target-nf-type给tt变量    tt = request.args.get('target-nf-type')    # 获取请求中的requester-nf-instance-fqdn给fqdn,这是个可选参数    fqdn = request.args.get('requester-nf-instance-fqdn')    # 如果查询AMF,随机返回一个AMF,    if tt == 'AMF':        node = random.choice(tton['AMF'])        return load_file(node)    # 如果查询SMF,根据映射关系返回    elif tt == 'SMF':        if fqdn.startswith('amf03'):            return load_file('smf03')        elif fqdn.startswith('amf05'):            return load_file('smf04')    # 如果是其他节点,根据字典返回    elif tt in tton.keys():        return load_file(tton[tt])    # 如果tt类型不对,返回目标节点类型错误    else:        return {'Message': "Wrong target nf type."}, 400

5)用浏览器当Client(作为SMF),查询2次AMF,返回了不同的AMF:

http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AMF&requester-nf-type=SMF

05f91ab2d98fbe88cfec5f64f83c3173.png

cac8f1a066f444619a7bd52612ab850f.png

6)查询SMF:(注意加了fqdn)

http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=SMF&requester-nf-type=AMF&requester-nf-instance-fqdn=amf03

213eb65c0a5744fb834d42667cc6959a.png

7)查询AUSF:

http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AUSF&requester-nf-type=AMF

a3dec49d98bbdde471ecd1cec98742ca.png

8)给一个错误的目标网元类型:

http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AAA&requester-nf-type=SMF

2d63cc0ee0b5b9fc5cfc9a7afde68dcc.png

9)需要说明的是,本文档只是演示交互过程,实际节点查询,会考虑多个参数,下面是一个真实SMF查询AMF的URL:

http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AMF&requester-nf-type=SMF&requester-nf-instance-fqdn=smf04.er.pc.smf.5gc.mnc008.mcc460.3gppnetwork.org&guami=%7B%22plmnId%22%3A%7B%22mcc%22%3A%20%22460%22%2C%20%22mnc%22%3A%20%2208%22%7D%2c%20%22amfId%22%3A%20%22$ID_X%22%7D"

以上是URL编码之后的内容,解码后内容为:

http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AMF&requester-nf-type=SMF&requester-nf-instance-fqdn=smf04.er.pc.smf.5gc.mnc008.mcc460.3gppnetwork.org&guami={"plmnId":{"mcc": "460", "mnc": "08"}, "amfId": "7"}"

可以看到,不仅有3个基本参数,还有其他参数,只有参数正确,NRF才能返回正确的结果。

10)另外windows用爬虫发现节点:

from urllib import requesturl = 'http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AMF&requester-nf-type=SMF'headers = {'user-agent': 'python'}req = request.Request(url, headers=headers)r = request.urlopen(req)print(r.read().decode('utf-8'))

13.总结

Flask上手简单,但需要一些Python基础。

修改配置后未永久保存的数据会丢失,可考虑使用pickle永久存储。

模拟交互需要注意HTTP 方法的逻辑,必要时画逻辑图。

实际Flask还可以模拟UDM/AUSF,不过方法类似,本文就不做展示了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值