一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

Python一鍵轉(zhuǎn)Jar包,Java調(diào)用Python新姿勢!

 書山之客 2020-05-30

轉(zhuǎn)自:編程技術(shù)宇宙

粉絲朋友們,不知道大家看故事看膩了沒(要是沒膩可一定留言告訴我^_^),今天這篇文章?lián)Q換口味,正經(jīng)的來寫寫技術(shù)文。言歸正傳,咱們開始吧!

今天的這篇文章,聊一個軒轅君之前工作中遇到的需求:如何在Java中調(diào)用Python代碼?

要不要先Mark一下,說不定將來哪天就用上了呢?

本文結(jié)構(gòu):

- 需求背景
- 進(jìn)擊的 Python
- Java 和 Python
- 給 Python 加速
- 尋找方向
- Jython?
- Python->Native 代碼
- 整體思路
- 實際動手
- 自動化
- 關(guān)鍵問題
- import 的問題
- Python GIL 問題
- 測試效果
- 總結(jié)

需求背景

進(jìn)擊的 Python

隨著人工智能的興起,Python 這門曾經(jīng)小眾的編程語言可謂是煥發(fā)了第二春。

以 tensorflow、pytorch 等為主的機(jī)器學(xué)習(xí)/深度學(xué)習(xí)的開發(fā)框架大行其道,助推了 python 這門曾經(jīng)以爬蟲見長(python 粉別生氣)的編程語言在 TIOBE 編程語言排行榜上一路披荊斬棘,坐上前三甲的寶座,僅次于 Java 和 C,將 C 、JavaScript、PHP、C#等一眾勁敵斬落馬下。

當(dāng)然,軒轅君向來是不提倡編程語言之間的競爭對比,每一門語言都有自己的優(yōu)勢和劣勢,有自己應(yīng)用的領(lǐng)域。另一方面,TIOBE 統(tǒng)計的數(shù)據(jù)也不能代表國內(nèi)的實際情況,上面的例子只是側(cè)面反映了 Python 這門語言如今的流行程度。

Java 還是 Python

說回咱們的需求上來,如今在不少的企業(yè)中,同時存在 Python 研發(fā)團(tuán)隊和 Java 研發(fā)團(tuán)隊,Python 團(tuán)隊負(fù)責(zé)人工智能算法開發(fā),而 Java 團(tuán)隊負(fù)責(zé)算法工程化,將算法能力通過工程化包裝提供接口給更上層的應(yīng)用使用。

可能大家要問了,為什么不直接用 Java 做 AI 開發(fā)呢?要弄兩個團(tuán)隊。其實,現(xiàn)在包括 TensorFlow 在內(nèi)的框架都逐漸開始支持 Java 平臺,用 Java 做 AI 開發(fā)也不是不行(其實已經(jīng)有不少團(tuán)隊在這樣做了),但限于歷史原因,做 AI 開發(fā)的人本就不多,而這一些人絕大部分都是 Python 技術(shù)棧入坑,Python 的 AI 開發(fā)生態(tài)已經(jīng)建設(shè)的相對完善,所以造成了在很多公司中算法團(tuán)隊和工程化團(tuán)隊不得不使用不同的語言。

現(xiàn)在該拋出本文的重要問題:Java 工程化團(tuán)隊如何調(diào)用 Python 的算法能力?

答案基本上只有一個:Python 通過 Django/Flask 等框架啟動一個 Web 服務(wù),Java 中通過 Restful API 與之進(jìn)行交互

上面的方式的確可以解決問題,但隨之而來的就是性能問題。尤其是在用戶量上升后,大量并發(fā)接口訪問下,通過網(wǎng)絡(luò)訪問和 Python 的代碼執(zhí)行速度將成為拖累整個項目的瓶頸。

當(dāng)然,不差錢的公司可以用硬件堆出性能,一個不行,那就多部署幾個 Python Web 服務(wù)。

那除此之外,有沒有更實惠的解決方案呢?這就是這篇文章要討論的問題。

給 Python 加速

尋找方向

上面的性能瓶頸中,拖累執(zhí)行速度的原因主要有兩個:

  • 通過網(wǎng)絡(luò)訪問,不如直接調(diào)用內(nèi)部模塊快
  • Python 是解釋執(zhí)行,快不起來

眾所周知,Python 是一門解釋型腳本語言,一般來說,在執(zhí)行速度上:

解釋型語言 < 中間字節(jié)碼語言 < 本地編譯型語言

自然而然,我們要努力的方向也就有兩個:

  • 能否不通過網(wǎng)絡(luò)訪問,直接本地調(diào)用
  • Python 不要解釋執(zhí)行

結(jié)合上面的兩個點,我們的目標(biāo)也清晰起來:

將 Python 代碼轉(zhuǎn)換成 Java 可以直接本地調(diào)用的模塊

對于 Java 來說,能夠本地調(diào)用的有兩種:

  • Java 代碼包
  • Native 代碼模塊

其實我們通常所說的 Python 指的是 CPython,也就是由 C 語言開發(fā)的解釋器來解釋執(zhí)行。而除此之外,除了 C 語言,不少其他編程語言也能夠按照 Python 的語言規(guī)范開發(fā)出虛擬機(jī)來解釋執(zhí)行 Python 腳本:

  • CPython: C 語言編寫的解釋器
  • Jython: Java 編寫的解釋器
  • IronPython: .NET 平臺的解釋器
  • PyPy: Python 自己編寫的解釋器(雞生蛋,蛋生雞)

Jython?

如果能夠在 JVM 中直接執(zhí)行 Python 腳本,與 Java 業(yè)務(wù)代碼的交互自然是最簡單不過。但隨后的調(diào)研發(fā)現(xiàn),這條路很快就被堵死了:

  • 不支持 Python3.0 以上的語法
  • python 源碼中若引用的第三方庫包含 C 語言擴(kuò)展,將無法提供支持,如 numpy 等

這條路行不通,那還有一條:把 Python 代碼轉(zhuǎn)換成 Native 代碼塊,Java 通過 JNI 的接口形式調(diào)用。

Python -> Native 代碼

整體思路

先將 Python 源代碼轉(zhuǎn)換成 C 代碼,之后用 GCC 編譯 C 代碼為二進(jìn)制模塊 so/dll,接著進(jìn)行一次 Java Native 接口封裝,使用 Jar 打包命令轉(zhuǎn)換成 Jar 包,然后 Java 便可以直接調(diào)用。

流程并不復(fù)雜,但要完整實現(xiàn)這個目標(biāo),有一個關(guān)鍵問題需要解決:

Python 代碼如何轉(zhuǎn)換成 C 代碼?

終于要輪到本文的主角登場了,將要用到的一個核心工具叫:Cython

請注意,這里的Cython和前面提到的CPython不是一回事。CPython 狹義上是指 C 語言編寫的 Python 解釋器,是 Windows、Linux 下我們默認(rèn)的 Python 腳本解釋器。

而 Cython 是 Python 的一個第三方庫,你可以通過pip install Cython進(jìn)行安裝。

官方介紹 Cython 是一個 Python 語言規(guī)范的超集,它可以將 Python C 混合編碼的.pyx 腳本轉(zhuǎn)換為 C 代碼,主要用于優(yōu)化 Python 腳本性能或 Python 調(diào)用 C 函數(shù)庫。

聽上去有點復(fù)雜,也有點繞,不過沒關(guān)系,get 一個核心點即可:Cython 能夠把 Python 腳本轉(zhuǎn)換成 C 代碼

來看一個實驗:

# FileName: test.py
def TestFunction():
print('this is print from python script')

將上述代碼通過 Cython 轉(zhuǎn)化,生成 test.c,長這個樣子:代碼非常長,而且不易讀,這里僅截圖示意。

實際動手

1.準(zhǔn)備 Python 源代碼

# FileName: Test.py
# 示例代碼:將輸入的字符串轉(zhuǎn)變?yōu)榇髮?/span>
def logic(param):
print('this is a logic function')
print('param is [%s]' % param)
return param.upper()

# 接口函數(shù),導(dǎo)出給Java Native的接口
def JNI_API_TestFunction(param):
print('enter JNI_API_test_function')
result = logic(param)
print('leave JNI_API_test_function')
return result

注意1:這里在 python 源碼中使用一種約定:以JNI_API_為前綴開頭的函數(shù)表示為Python代碼模塊要導(dǎo)出對外調(diào)用的接口函數(shù),這樣做的目的是為了讓我們的 Python 一鍵轉(zhuǎn) Jar 包系統(tǒng)能自動化識別提取哪些接口作為導(dǎo)出函數(shù)。

注意2:這一類接口函數(shù)的輸入是一個 python 的 str 類型字符串,輸出亦然,如此可便于移植以往通過JSON形式作為參數(shù)的 RESTful 接口。使用JSON的好處是可以對參數(shù)進(jìn)行封裝,支持多種復(fù)雜的參數(shù)形式,而不用重載出不同的接口函數(shù)對外調(diào)用。

注意3:還有一點需要說明的是,在接口函數(shù)前綴JNI_API_的后面,函數(shù)命名不能以 python 慣有的下劃線命名法,而要使用駝峰命名法,注意這不是建議,而是要求,原因后續(xù)會提到。

2.準(zhǔn)備一個 main.c 文件

這個文件的作用是對 Cython 轉(zhuǎn)換生成的代碼進(jìn)行一次封裝,封裝成 Java JNI 接口形式的風(fēng)格,以備下一步 Java 的使用。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
#include <Python.h>
#include <stdio.h>

#ifndef _Included_main
#define _Included_main
#ifdef __cplusplus
extern 'C' {
#endif

#if PY_MAJOR_VERSION < 3
# define MODINIT(name) init ## name
#else
# define MODINIT(name) PyInit_ ## name
#endif
PyMODINIT_FUNC MODINIT(Test)(void);

JNIEXPORT void JNICALL Java_Test_initModule
(JNIEnv *env, jobject obj)
{
PyImport_AppendInittab('Test', MODINIT(Test));
Py_Initialize();

PyRun_SimpleString('import os');
PyRun_SimpleString('__name__ = \'__main__\'');
PyRun_SimpleString('import sys');
PyRun_SimpleString('sys.path.append('./')');

PyObject* m = PyInit_Test_Test();
if (!PyModule_Check(m)) {
PyModuleDef *mdef = (PyModuleDef *) m;
PyObject *modname = PyUnicode_FromString('__main__');
m = NULL;
if (modname) {
m = PyModule_NewObject(modname);
Py_DECREF(modname);
if (m) PyModule_ExecDef(m, mdef);
}
}
PyEval_InitThreads();
}


JNIEXPORT void JNICALL Java_Test_uninitModule
(JNIEnv *env, jobject obj)
{
Py_Finalize();
}

JNIEXPORT jstring JNICALL Java_Test_testFunction
(JNIEnv *env, jobject obj, jstring string)
{
const char* param = (char*)(*env)->GetStringUTFChars(env, string, NULL);
static PyObject *s_pmodule = NULL;
static PyObject *s_pfunc = NULL;
if (!s_pmodule || !s_pfunc) {
s_pmodule = PyImport_ImportModule('Test');
s_pfunc = PyObject_GetAttrString(s_pmodule, 'JNI_API_testFunction');
}
PyObject *pyRet = PyObject_CallFunction(s_pfunc, 's', param);
(*env)->ReleaseStringUTFChars(env, string, param);
if (pyRet) {
jstring retJstring = (*env)->NewStringUTF(env, PyUnicode_AsUTF8(pyRet));
Py_DECREF(pyRet);
return retJstring;
} else {
PyErr_Print();
return (*env)->NewStringUTF(env, 'error');
}
}
#ifdef __cplusplus
}
#endif
#endif

這個文件中一共有3個函數(shù):

  • Java_Test_initModule: python初始化工作
  • Java_Test_uninitModule: python反初始化工作
  • Java_Test_testFunction:真正的業(yè)務(wù)接口,封裝了對原來Python中定義對JNI_API_testFuncion函數(shù)的調(diào)用,同時要負(fù)責(zé)JNI層面的參數(shù)jstring類型的轉(zhuǎn)換。

根據(jù) JNI 接口規(guī)范,native 層面的 C 函數(shù)命名需要符合如下的形式:

// QualifiedClassName: 全類名
// MethodName: JNI接口函數(shù)名
void
JNICALL
Java_QualifiedClassName_MethodName(JNIEnv*, jobject)
;

所以在main.c文件中對定義需要向上面這樣命名,這也是為什么前面強(qiáng)調(diào)python接口函數(shù)命名不能用下劃線,這會導(dǎo)致JNI接口找不到對應(yīng)的native函數(shù)。

3.使用 Cython 工具編譯生成動態(tài)庫

補(bǔ)充做一個小小的準(zhǔn)備工作:把Python源碼文件的后綴從.py改成.pyx

python源代碼Test.pyx和main.c文件都準(zhǔn)備就緒,接下來便是Cython登場的時候了,它將會將所有pyx的文件自動轉(zhuǎn)換成.c文件,并結(jié)合我們自己的main.c文件,內(nèi)部調(diào)用gcc生成一個動態(tài)二進(jìn)制庫文件。

Cython 的工作需要準(zhǔn)備一個 setup.py 文件,配置好轉(zhuǎn)換的編譯信息,包括輸入文件、輸出文件、編譯參數(shù)、包含目錄、鏈接目錄,如下所示:

from distutils.core import setup
from Cython.Build import cythonize
from distutils.extension import Extension

sourcefiles = ['Test.pyx', 'main.c']

extensions = [Extension('libTest', sourcefiles,
include_dirs=['/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/include',
'/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/include/darwin/',
'/Library/Frameworks/Python.framework/Versions/3.6/include/python3.6m'],
library_dirs=['/Library/Frameworks/Python.framework/Versions/3.6/lib/'],
libraries=['python3.6m'])]

setup(ext_modules=cythonize(extensions, language_level = 3))

注意:這里涉及Python二進(jìn)制代碼的編譯,需要鏈接Python的庫

注意:這里涉及JNI相關(guān)數(shù)據(jù)結(jié)構(gòu)定義,需要包含Java JNI目錄

setup.py文件準(zhǔn)備就緒后,便執(zhí)行如下命令,啟動轉(zhuǎn)換+編譯工作:

python3.6 setup.py build_ext --inplace

生成我們需要的動態(tài)庫文件:libTest.so

4.準(zhǔn)備Java JNI調(diào)用的接口文件

Java業(yè)務(wù)代碼使用需要定義一個接口,如下所示:

// FileName: Test.java
public class Test {
public native void initModule();
public native void uninitModule();
public native String testFunction(String param);

到這一步,其實已經(jīng)實現(xiàn)了在Java中調(diào)用的目的了,注意調(diào)用業(yè)務(wù)接口之前,需要先調(diào)用initModule進(jìn)行native層面的Python初始化工作。


import Test;
public class Demo {
public void main(String[] args) {
System.load('libTest.so');
Test tester = new Test();
tester.initModule();
String result = tester.testFunction('this is called from java');
tester.uninitModule();

System.out.println(result);
}
}

輸出:

enter JNI_API_test_function
this is a logic function
param is [this is called from java]
leave JNI_API_test_function
THIS IS CALLED FROM JAVA!

成功實現(xiàn)了在Java中調(diào)用Python代碼!

5.封裝為 Jar 包

做到上面這樣還不能滿足,為了更好的使用體驗,我們再往前一步,封裝成為Jar包。

首先原來的JNI接口文件需要再擴(kuò)充一下,加入一個靜態(tài)方法loadLibrary,自動實現(xiàn)so文件的釋放和加載。

// FileName: Test.java
public class Test {
public native void initModule();
public native void uninitModule();
public native String testFunction(String param);
public synchronized static void loadLibrary() throws IOException {
// 實現(xiàn)略...

}

接著將上面的接口文件轉(zhuǎn)換成java class文件:

javac Test.java

最后,準(zhǔn)備將class文件和so文件放置于Test目錄下,打包:

jar -cvf Test.jar ./Test

自動化

上面5個步驟如果每次都要手動來做著實是麻煩!好在,我們可以編寫Python腳本將這個過程完全的自動化,真正做到Python一鍵轉(zhuǎn)換Jar包

限于篇幅原因,這里僅僅提一下自動化過程的關(guān)鍵:

  • 自動掃描提取python源代碼中需要導(dǎo)出的接口函數(shù)
  • main.c、setup.py和JNI接口java文件都需要自動化生成(可以定義模板+參數(shù)形式快速構(gòu)建),需要處理好各模塊名、函數(shù)名對應(yīng)關(guān)系

關(guān)鍵問題

1.import 問題

上面演示的案例只是一個單獨(dú)的 py 文件,而實際工作中,我們的項目通常是具有多個 py 文件,并且這些文件通常是構(gòu)成了復(fù)雜的目錄層級,互相之間各種 import 關(guān)系,錯綜復(fù)雜。

Cython 這個工具有一個最大的坑在于:經(jīng)過其處理的文件代碼中會丟失代碼文件的目錄層級信息,如下圖所示,C.py 轉(zhuǎn)換后的代碼和 m/C.py 生成的代碼沒有任何區(qū)別。

這就帶來一個非常大的問題:A.py 或 B.py 代碼中如果有引用 m 目錄下的 C.py 模塊,目錄信息的丟失將導(dǎo)致二者在執(zhí)行 import m.C 時報錯,找不到對應(yīng)的模塊!

幸運(yùn)的是,經(jīng)過實驗表明,在上面的圖中,如果 A、B、C 三個模塊處于同一級目錄下時,import 能夠正確執(zhí)行。

軒轅君曾經(jīng)嘗試閱讀 Cython 的源代碼,并進(jìn)行修改,將目錄信息進(jìn)行保留,使得生成后的 C 代碼仍然能夠正常 import,但限于時間倉促,對 Python 解釋器機(jī)理了解不足,在一番嘗試之后選擇了放棄。

在這個問題上卡了很久,最終選擇了一個笨辦法:將樹形的代碼層級目錄展開成為平坦的目錄結(jié)構(gòu),就上圖中的例子而言,展開后的目錄結(jié)構(gòu)變成了

A.py
B.py
m_C.py

單是這樣還不夠,還需要對 A、B 中引用到 C 的地方全部進(jìn)行修正為對 m_C 的引用。

這看起來很簡單,但實際情況遠(yuǎn)比這復(fù)雜,在 Python 中,import 可不只有 import 這么簡單,有各種各樣復(fù)雜的形式:

import package
import module
import package.module
import module.class / function
import package.module.class / function
import package.*
import module.*
from module import *
from module import module
from package import *
from package import module
from package.module import class / function
...

除此之外,在代碼中還可能存在直接通過模塊進(jìn)行引用的寫法。

展開成為平坦結(jié)構(gòu)的代價就是要處理上面所有的情況!軒轅君無奈之下只有出此下策,如果各位大佬有更好的解決方案還望不吝賜教。

2.Python GIL 問題

Python 轉(zhuǎn)換后的 jar 包開始用于實際生產(chǎn)中了,但隨后發(fā)現(xiàn)了一個問題:

每當(dāng) Java 并發(fā)數(shù)一上去之后,JVM 總是不定時出現(xiàn) Crash

隨后分析崩潰信息發(fā)現(xiàn),崩潰的地方正是在 Native 代碼中的 Python 轉(zhuǎn)換后的代碼中。

  • 難道是 Cython 的 bug?
  • 轉(zhuǎn)換后的代碼有坑?
  • 還是說上面的 import 修正工作有問題?

崩潰的烏云籠罩在頭上許久,冷靜下來思考:為什么測試的時候正常沒有發(fā)現(xiàn)問題,上線之后才會崩潰?

再次翻看崩潰日志,發(fā)現(xiàn)在 native 代碼中,發(fā)生異常的地方總是在 malloc 分配內(nèi)存的地方,難不成內(nèi)存被破壞了?又發(fā)現(xiàn)測試的時候只是完成了功能性測試,并沒有進(jìn)行并發(fā)壓力測試,而發(fā)生崩潰的場景總是在多并發(fā)環(huán)境中。多線程訪問 JNI 接口,那 Native 代碼將在多個線程上下文中執(zhí)行。

猛地一個警覺:99%跟 Python 的 GIL 鎖有關(guān)系!

眾所周知,限于歷史原因,Python 誕生于上世紀(jì)九十年代,彼時多線程的概念還遠(yuǎn)遠(yuǎn)沒有像今天這樣深入人心過,Python 作為這個時代的產(chǎn)物一誕生就是一個單線程的產(chǎn)品。

雖然 Python 也有多線程庫,允許創(chuàng)建多個線程,但由于 C 語言版本的解釋器在內(nèi)存管理上并非線程安全,所以在解釋器內(nèi)部有一個非常重要的鎖在制約著 Python 的多線程,所以所謂多線程實際上也只是大家輪流來占坑。

原來 GIL 是由解釋器在進(jìn)行調(diào)度管理,如今被轉(zhuǎn)成了 C 代碼后,誰來負(fù)責(zé)管理多線程的安全呢?

由于 Python 提供了一套供 C 語言調(diào)用的接口,允許在 C 程序中執(zhí)行 Python 腳本,于是翻看這套 API 的文檔,看看能否找到答案。

幸運(yùn)的是,還真被我找到了:

獲取 GIL 鎖:

釋放 GIL 鎖:

在 JNI 調(diào)用入口需要獲得 GIL 鎖,接口退出時需要釋放 GIL 鎖。

加入 GIL 鎖的控制后,煩人的 Crash 問題終于得以解決!

測試效果

準(zhǔn)備兩份一模一樣的 py 文件,同樣的一個算法函數(shù),一個通過 Flask Web 接口訪問,(Web 服務(wù)部署于本地 127.0.0.1,盡可能減少網(wǎng)絡(luò)延時),另一個通過上述過程轉(zhuǎn)換成 Jar 包。

在 Java 服務(wù)中,分別調(diào)用兩個接口 100 次,整個測試工作進(jìn)行 10 次,統(tǒng)計執(zhí)行耗時:

上述測試中,為進(jìn)一步區(qū)分網(wǎng)絡(luò)帶來的延遲和代碼執(zhí)行本身的延遲,在算法函數(shù)的入口和出口做了計時,在 Java 執(zhí)行接口調(diào)用前和獲得結(jié)果的地方也做了計時,這樣可以計算出算法執(zhí)行本身的時間在整個接口調(diào)用過程中的占比。

  • 從結(jié)果可以看出,通過 Web API 執(zhí)行的接口訪問,算法本身執(zhí)行的時間只占到了 30% ,大部分的時間用在了網(wǎng)絡(luò)開銷(數(shù)據(jù)包的收發(fā)、Flask 框架的調(diào)度處理等等)。

  • 而通過 JNI 接口本地調(diào)用,算法的執(zhí)行時間占到了整個接口執(zhí)行時間的 80%以上,而 Java JNI 的接口轉(zhuǎn)換過程只占用 10% 的時間,有效提升了效率,減少額外時間的浪費(fèi)。

  • 除此之外,單看算法本身的執(zhí)行部分,同一份代碼,轉(zhuǎn)換成 Native 代碼后的執(zhí)行時間在 300~500μs,而 CPython 解釋執(zhí)行的時間則在 2000~4000μs,同樣也是相差懸殊。

總結(jié)

本文提供了一種 Java 調(diào)用 Python 代碼的新思路,僅供參考,其成熟度和穩(wěn)定性還有待商榷,通過 HTTP Restful 接口訪問仍然是跨語言對接的首選。

至于文中的方法,感興趣的朋友歡迎留言交流。

PS:限于筆者水平有限,文中如有錯誤,歡迎各位不吝賜教,以免誤導(dǎo)讀者,多謝。

感謝閱讀

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    日本美国三级黄色aa| 午夜小视频成人免费看| 亚洲熟女诱惑一区二区| 久久精品国产亚洲熟女| 在线观看免费午夜福利| 五月情婷婷综合激情综合狠狠| 日韩国产精品激情一区| 欧美成人久久久免费播放| 欧美日韩国产黑人一区| 亚洲精品国产第一区二区多人| 老外那个很粗大做起来很爽| 日韩精品中文字幕在线视频| 亚洲一区二区三区有码| 丰满少妇被猛烈撞击在线视频| 99久久人妻中文字幕| 91亚洲国产成人久久精品麻豆| 人妻乱近亲奸中文字幕| 日本精品视频一二三区| 国产91麻豆精品成人区| 欧美日韩视频中文字幕| 国产av一区二区三区久久不卡| 开心五月激情综合婷婷色| 欧美黑人在线精品极品| 欧美国产精品区一区二区三区| 亚洲中文字幕综合网在线| 国产高清在线不卡一区| 国产亚洲欧美一区二区| 中文字幕日韩无套内射| 婷婷激情四射在线观看视频| 亚洲最新av在线观看| 国产av一二三区在线观看| 美女激情免费在线观看| 成人精品日韩专区在线观看| 欧美精品一区二区水蜜桃| 国内真实露脸偷拍视频| 亚洲av日韩一区二区三区四区| 欧美国产日韩变态另类在线看| 亚洲a级一区二区不卡| 91欧美日韩精品在线| 男女午夜视频在线观看免费| 欧美色婷婷综合狠狠爱|