1. Giới thiệu
Trong lớp học lập trình này, bạn sẽ dùng gRPC-Python để tạo một ứng dụng khách và máy chủ tạo thành nền tảng của một ứng dụng lập bản đồ tuyến đường được viết bằng Python.
Khi kết thúc hướng dẫn này, bạn sẽ có một ứng dụng kết nối với một máy chủ từ xa bằng gRPC để lấy thông tin về các đối tượng trên tuyến đường của ứng dụng, tạo bản tóm tắt về tuyến đường của ứng dụng và trao đổi thông tin về tuyến đường (chẳng hạn như thông tin cập nhật về tình trạng giao thông) với máy chủ và các ứng dụng khác.
Dịch vụ này được xác định trong một tệp Protocol Buffers. Tệp này sẽ được dùng để tạo mã chuẩn cho ứng dụng và máy chủ để chúng có thể giao tiếp với nhau, giúp bạn tiết kiệm thời gian và công sức khi triển khai chức năng đó.
Mã được tạo này không chỉ xử lý sự phức tạp của việc giao tiếp giữa máy chủ và ứng dụng mà còn xử lý quá trình chuyển đổi dữ liệu thành chuỗi và chuyển đổi chuỗi thành dữ liệu.
Kiến thức bạn sẽ học được
- Cách sử dụng Protocol Buffers để xác định một API dịch vụ.
- Cách tạo máy khách và máy chủ dựa trên gRPC từ một định nghĩa Protocol Buffers bằng cách sử dụng tính năng tạo mã tự động.
- Hiểu rõ về giao tiếp truyền phát trực tiếp giữa máy khách và máy chủ bằng gRPC.
Lớp học lập trình này dành cho những nhà phát triển Python mới làm quen với gRPC hoặc muốn tìm hiểu lại về gRPC, hoặc bất kỳ ai khác quan tâm đến việc xây dựng các hệ thống phân tán. Bạn không cần có kinh nghiệm sử dụng gRPC.
2. Trước khi bắt đầu
Bạn cần có
- Python 3.9 trở lên. Bạn nên dùng Python 3.13. Để biết hướng dẫn cài đặt theo từng nền tảng, hãy xem phần Thiết lập và sử dụng Python. Ngoài ra, hãy cài đặt Python không phải là hệ thống bằng các công cụ như uv hoặc pyenv.
- pip để cài đặt các gói Python.
- venv để tạo môi trường ảo Python.
Các gói ensurepip
và venv
là một phần của Thư viện chuẩn Python và thường có sẵn theo mặc định.
Tuy nhiên, một số bản phân phối dựa trên Debian (bao gồm cả Ubuntu) chọn loại trừ các bản phân phối này khi phân phối lại Python. Để cài đặt các gói, hãy chạy:
sudo apt install python3-pip python3-venv
Lấy mã
Để đơn giản hoá quá trình học tập, lớp học lập trình này cung cấp một cấu trúc mã nguồn được tạo sẵn để giúp bạn bắt đầu. Các bước sau đây sẽ hướng dẫn bạn hoàn tất ứng dụng, bao gồm cả việc tạo mã gRPC bằng trình bổ trợ trình biên dịch grpc_tools.protoc
Protocol Buffer.
grpc-codelabs
Mã nguồn giàn giáo cho lớp học lập trình này có trong thư mục codelabs/grpc-python-streaming/start_here. Nếu bạn không muốn tự triển khai mã, thì mã nguồn hoàn chỉnh có trong thư mục completed
.
Trước tiên, hãy tạo thư mục làm việc cho lớp học lập trình rồi chuyển đến thư mục đó:
mkdir grpc-python-streaming && cd grpc-python-streaming
Tải và giải nén lớp học lập trình:
curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
| tar xvz --strip-components=4 \
grpc-codelabs-1/codelabs/grpc-python-streaming/start_here
Ngoài ra, bạn có thể tải tệp .zip chỉ chứa thư mục codelab xuống rồi giải nén theo cách thủ công.
3. Xác định thông báo và dịch vụ
Bước đầu tiên là xác định dịch vụ gRPC của ứng dụng, phương thức RPC và các loại thông báo yêu cầu và phản hồi bằng cách sử dụng Protocol Buffers. Dịch vụ của bạn sẽ cung cấp:
- Các phương thức RPC được gọi là
ListFeatures
,RecordRoute
vàRouteChat
mà máy chủ triển khai và ứng dụng gọi. - Các loại thông báo
Point
,Feature
,Rectangle
,RouteNote
vàRouteSummary
là các cấu trúc dữ liệu được trao đổi giữa máy khách và máy chủ khi gọi các phương thức RPC.
Tất cả các phương thức RPC này và các loại thông báo tương ứng sẽ được xác định trong tệp protos/route_guide.proto
của mã nguồn được cung cấp.
Vùng đệm giao thức thường được gọi là protobuf. Để biết thêm thông tin về thuật ngữ gRPC, hãy xem phần Các khái niệm, cấu trúc và vòng đời cốt lõi của gRPC.
Xác định các loại thông báo
Trong tệp protos/route_guide.proto
của mã nguồn, trước tiên hãy xác định kiểu thông báo Point
. Point
biểu thị một cặp toạ độ vĩ độ và kinh độ trên bản đồ. Trong lớp học lập trình này, hãy sử dụng số nguyên cho toạ độ:
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
Các số 1
và 2
là các số nhận dạng duy nhất cho từng trường trong cấu trúc message
.
Tiếp theo, hãy xác định loại thông báo Feature
. Feature
sử dụng trường string
cho tên hoặc địa chỉ bưu chính của một đối tượng tại một vị trí do Point
chỉ định:
message Feature {
// The name or address of the feature.
string name = 1;
// The point where the feature is located.
Point location = 2;
}
Để truyền trực tuyến nhiều điểm trong một khu vực đến một ứng dụng, bạn sẽ cần một thông báo Rectangle
đại diện cho hình chữ nhật vĩ độ – kinh độ, được biểu thị dưới dạng hai điểm đối diện theo đường chéo lo
và hi
:
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
Ngoài ra, một thông báo RouteNote
đại diện cho một thông báo được gửi tại một thời điểm nhất định:
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
Cuối cùng, bạn sẽ cần một thông báo RouteSummary
. Thông báo này được nhận để phản hồi một RPC RecordRoute
, được giải thích trong phần tiếp theo. Tệp này chứa số lượng điểm riêng lẻ nhận được, số lượng đối tượng được phát hiện và tổng khoảng cách đã đi được tính bằng tổng tích luỹ khoảng cách giữa mỗi điểm.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
Xác định các phương thức dịch vụ
Để xác định một dịch vụ, bạn chỉ định một dịch vụ có tên trong tệp .proto
. Tệp route_guide.proto
có cấu trúc service
tên là RouteGuide
, xác định một hoặc nhiều phương thức do dịch vụ của ứng dụng cung cấp.
Khi xác định các phương thức RPC
trong định nghĩa dịch vụ, bạn sẽ chỉ định các loại yêu cầu và phản hồi của các phương thức đó. Trong phần này của lớp học lập trình, hãy xác định:
ListFeatures
Lấy các đối tượng Feature
có trong Rectangle
đã cho. Kết quả được truyền trực tuyến thay vì trả về ngay lập tức vì hình chữ nhật có thể bao phủ một khu vực rộng lớn và chứa một số lượng lớn các đối tượng.
Đối với ứng dụng này, bạn sẽ sử dụng một RPC truyền phát trực tiếp phía máy chủ: ứng dụng gửi một yêu cầu đến máy chủ và nhận một luồng để đọc lại một chuỗi thông báo. Ứng dụng đọc từ luồng được trả về cho đến khi không còn thông báo nào nữa. Như bạn có thể thấy trong ví dụ của chúng tôi, bạn chỉ định một phương thức truyền trực tuyến phía máy chủ bằng cách đặt từ khoá truyền trực tuyến trước loại phản hồi.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
Chấp nhận một luồng Điểm trên tuyến đường đang được đi qua, trả về RouteSummary
khi quá trình đi qua hoàn tất.
RPC truyền trực tuyến phía máy khách là phù hợp trong trường hợp này: máy khách ghi một chuỗi thông báo và gửi chúng đến máy chủ, một lần nữa bằng cách sử dụng một luồng được cung cấp. Sau khi hoàn tất việc ghi các thông báo, ứng dụng sẽ đợi máy chủ đọc tất cả các thông báo đó và trả về phản hồi. Bạn chỉ định phương thức phát trực tuyến phía máy khách bằng cách đặt từ khoá stream trước loại yêu cầu.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
Chấp nhận luồng RouteNotes
được gửi trong khi một tuyến đường đang được đi qua, đồng thời nhận các RouteNotes
khác (ví dụ: từ những người dùng khác).
Đây chính xác là trường hợp sử dụng cho truyền phát trực tiếp hai chiều. Một RPC truyền trực tuyến hai chiều, trong đó cả hai bên đều gửi một chuỗi thông báo bằng cách sử dụng luồng đọc-ghi. Hai luồng này hoạt động độc lập, vì vậy, các ứng dụng và máy chủ có thể đọc và ghi theo bất kỳ thứ tự nào mà chúng muốn: ví dụ: máy chủ có thể đợi nhận tất cả tin nhắn của ứng dụng trước khi ghi phản hồi hoặc có thể lần lượt đọc một tin nhắn rồi ghi một tin nhắn, hoặc một số tổ hợp khác giữa đọc và ghi. Thứ tự của các thông báo trong mỗi luồng được giữ nguyên. Bạn chỉ định loại phương thức này bằng cách đặt từ khoá luồng trước cả yêu cầu và phản hồi.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
4. Tạo mã máy khách và máy chủ
Tiếp theo, hãy tạo mã gRPC chung cho cả máy khách và máy chủ từ tệp .proto
bằng trình biên dịch vùng đệm giao thức.
Đối với việc tạo mã gRPC Python, chúng tôi đã tạo grpcio-tools. Thư mục này bao gồm:
- Trình biên dịch protoc thông thường tạo mã Python từ các định nghĩa
message
. - Trình bổ trợ protobuf gRPC tạo mã Python (mã ứng dụng máy khách và máy chủ) từ các định nghĩa
service
.
Chúng ta sẽ cài đặt gói Python grpcio-tools
bằng pip. Hãy tạo một môi trường ảo Python (venv) mới để tách biệt các phần phụ thuộc của dự án với các gói hệ thống:
python3 -m venv --upgrade-deps .venv
Cách kích hoạt môi trường ảo trong trình bao bash/zsh:
source .venv/bin/activate
Đối với Windows và các shell không theo chuẩn, hãy xem bảng tại https://docs.python.org/3/library/venv.html#how-venvs-work.
Tiếp theo, hãy cài đặt grpcio-tools (thao tác này cũng cài đặt gói grpcio):
pip install grpcio-tools
Sử dụng lệnh sau để tạo mã khởi động Python:
python -m grpc_tools.protoc --proto_path=./protos \
--python_out=. --pyi_out=. --grpc_python_out=. \
./protos/route_guide.proto
Thao tác này sẽ tạo các tệp sau cho những giao diện mà chúng ta đã xác định trong route_guide.proto
:
route_guide_pb2.py
chứa mã tạo động các lớp được tạo từ các định nghĩamessage
.route_guide_pb2.pyi
là một "tệp stub" hoặc "tệp gợi ý loại" được tạo từ các định nghĩamessage
. Nó chỉ chứa các chữ ký không có quá trình triển khai. Các IDE có thể sử dụng tệp stub để cung cấp tính năng tự động hoàn thành và phát hiện lỗi hiệu quả hơn.route_guide_pb2_grpc.py
được tạo từ các định nghĩaservice
và chứa các lớp cũng như hàm dành riêng cho gRPC.
Mã dành riêng cho gRPC chứa:
RouteGuideStub
, có thể được máy khách gRPC sử dụng để gọi các RPC RouteGuide.RouteGuideServicer
, xác định giao diện cho các hoạt động triển khai dịch vụRouteGuide
.- Hàm
add_RouteGuideServicer_to_server
dùng để đăng kýRouteGuideServicer
với máy chủ gRPC.
5. Tạo máy chủ
Trước tiên, hãy xem cách bạn tạo một máy chủ RouteGuide
. Việc tạo và chạy một máy chủ RouteGuide
được chia thành 2 mục công việc:
- Triển khai giao diện trình chạy dịch vụ được tạo từ định nghĩa dịch vụ của chúng tôi bằng các hàm thực hiện "công việc" thực tế của dịch vụ.
- Chạy một máy chủ gRPC để lắng nghe các yêu cầu từ máy khách và truyền phản hồi.
Hãy xem route_guide_server.py
.
Triển khai RouteGuide
route_guide_server.py
có một lớp RouteGuideServicer
phân lớp lớp route_guide_pb2_grpc.RouteGuideServicer
đã tạo:
# RouteGuideServicer provides an implementation of the methods of the RouteGuide service.
class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):
RouteGuideServicer
triển khai tất cả các phương thức dịch vụ RouteGuide
.
RPC truyền phát trực tiếp phía máy chủ
ListFeatures
là một RPC truyền trực tuyến phản hồi, gửi nhiều Feature
đến máy khách:
def ListFeatures(self, request, context):
"""List all features contained within the given Rectangle."""
left = min(request.lo.longitude, request.hi.longitude)
right = max(request.lo.longitude, request.hi.longitude)
top = max(request.lo.latitude, request.hi.latitude)
bottom = min(request.lo.latitude, request.hi.latitude)
for feature in self.db:
lat, lng = feature.location.latitude, feature.location.longitude
if left <= lng <= right and bottom <= lat <= top:
yield feature
Trong đó, thông báo yêu cầu là một route_guide_pb2.Rectangle
mà trong đó ứng dụng muốn tìm Feature
. Thay vì trả về một phản hồi duy nhất, phương thức này sẽ tạo ra không hoặc nhiều phản hồi.
RPC truyền phát trực tiếp phía máy khách
Phương thức truyền trực tuyến yêu cầu RecordRoute
sử dụng một trình lặp gồm các giá trị yêu cầu và trả về một giá trị phản hồi duy nhất.
def RecordRoute(self, request_iterator, context):
"""Calculate statistics about the trip composed of Points."""
point_count = 0
feature_count = 0
distance = 0.0
prev_point = None
start_time = time.time()
for point in request_iterator:
point_count += 1
if get_feature(self.db, point):
feature_count += 1
if prev_point:
distance += get_distance(prev_point, point)
prev_point = point
elapsed_time = time.time() - start_time
return route_guide_pb2.RouteSummary(
point_count=point_count,
feature_count=feature_count,
distance=int(distance),
elapsed_time=int(elapsed_time),
)
RPC truyền trực tuyến hai chiều
Cuối cùng, hãy xem RPC truyền trực tuyến hai chiều RouteChat()
:
def RouteChat(self, request_iterator, context):
"""
Receive a stream of message/location pairs, and responds with
a stream of all previous messages for the given location.
"""
prev_notes = []
for new_note in request_iterator:
for prev_note in prev_notes:
if prev_note.location == new_note.location:
yield prev_note
prev_notes.append(new_note)
Ngữ nghĩa của phương thức này là sự kết hợp giữa ngữ nghĩa của phương thức truyền trực tuyến yêu cầu và phương thức truyền trực tuyến phản hồi. Thao tác này được truyền một đối tượng lặp lại của các giá trị yêu cầu và bản thân nó là một đối tượng lặp lại của các giá trị phản hồi.
Khởi động máy chủ
Sau khi bạn triển khai tất cả các phương thức RouteGuide
, bước tiếp theo là khởi động một máy chủ gRPC để máy khách có thể thực sự sử dụng dịch vụ của bạn:
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
RouteGuideServicer(),
server,
)
listen_addr = "localhost:50051"
server.add_insecure_port(listen_addr)
print(f"Starting server on {listen_addr}")
server.start()
server.wait_for_termination()
Phương thức start()
của máy chủ không chặn. Một luồng mới sẽ được khởi tạo để xử lý các yêu cầu. Luồng gọi server.start()
thường không có việc gì khác để làm trong thời gian chờ đợi. Trong trường hợp này, bạn có thể gọi server.wait_for_termination()
để chặn luồng gọi một cách rõ ràng cho đến khi máy chủ kết thúc.
6. Tạo ứng dụng
Hãy xem route_guide_client.py
.
Tạo một stub
Để gọi các phương thức dịch vụ, trước tiên, chúng ta cần tạo một stub.
Chúng ta tạo thực thể cho lớp RouteGuideStub
của mô-đun route_guide_pb2_grpc
, được tạo từ phương thức .proto.
trong run()
:
with grpc.insecure_channel("localhost:50051") as channel:
stub = route_guide_pb2_grpc.RouteGuideStub(channel)
Xin lưu ý rằng ở đây, channel
được dùng làm trình quản lý bối cảnh và sẽ tự động đóng khi trình thông dịch rời khỏi khối with
.
Gọi các phương thức dịch vụ
Đối với các phương thức RPC trả về một phản hồi ("response-unary"), gRPC Python hỗ trợ cả ngữ nghĩa luồng điều khiển đồng bộ (chặn) và không đồng bộ (không chặn). Đối với các phương thức RPC truyền trực tuyến phản hồi, các lệnh gọi sẽ trả về ngay một trình lặp của các giá trị phản hồi. Các lệnh gọi đến phương thức next()
của trình lặp đó sẽ chặn cho đến khi phản hồi được tạo từ trình lặp có sẵn.
RPC truyền phát trực tiếp phía máy chủ
Việc gọi ListFeatures
truyền trực tuyến phản hồi tương tự như khi làm việc với các loại chuỗi:
def guide_list_features(stub):
_lo = route_guide_pb2.Point(latitude=400000000, longitude=-750000000)
_hi = route_guide_pb2.Point(latitude=420000000, longitude=-730000000)
rectangle = route_guide_pb2.Rectangle(
lo=_lo,
hi=_hi,
)
print("Looking for features between 40, -75 and 42, -73")
features = stub.ListFeatures(rectangle)
for feature in features:
print(
f"Feature called '{feature.name}'"
f" at {format_point(feature.location)}"
)
RPC truyền phát trực tiếp phía máy khách
Việc gọi RecordRoute
truyền trực tuyến yêu cầu cũng tương tự như việc truyền một trình lặp đến một phương thức cục bộ. Giống như RPC đơn giản ở trên cũng trả về một phản hồi duy nhất, bạn có thể gọi RPC này một cách đồng bộ:
def guide_record_route(stub):
feature_list = route_guide_resources.read_route_guide_database()
route_iterator = generate_route(feature_list)
route_summary = stub.RecordRoute(route_iterator)
print(f"Finished trip with {route_summary.point_count} points")
print(f"Passed {route_summary.feature_count} features")
print(f"Traveled {route_summary.distance} meters")
print(f"It took {route_summary.elapsed_time} seconds")
RPC truyền trực tuyến hai chiều
Việc gọi RouteChat
truyền trực tuyến hai chiều có (như trường hợp ở phía dịch vụ) sự kết hợp giữa ngữ nghĩa truyền trực tuyến yêu cầu và truyền trực tuyến phản hồi.
Tạo các thông báo yêu cầu và gửi từng thông báo bằng yield
.
def generate_notes():
home = route_guide_pb2.Point(latitude=1, longitude=1)
work = route_guide_pb2.Point(latitude=2, longitude=2)
notes = [
make_route_note("Departing from home", home),
make_route_note("Arrived at work", work),
make_route_note("Having lunch at work", work),
make_route_note("Departing from work", work),
make_route_note("Arrived home", home),
]
for note in notes:
print(
f"Sending RouteNote for {format_point(note.location)}:"
f" {note.message}"
)
yield note
# Sleep to simulate moving from one point to another.
# Only for demonstrating the order of the messages.
time.sleep(0.1)
Nhận và xử lý các phản hồi của máy chủ:
def guide_route_chat(stub):
responses = stub.RouteChat(generate_notes())
for response in responses:
print(
"< Found previous note at"
f" {format_point(response.location)}: {response.message}"
)
Gọi các phương thức trợ giúp
Trong quá trình chạy, hãy thực thi các phương thức mà chúng ta vừa tạo và truyền cho các phương thức đó stub
.
print("-------------- ListFeatures --------------")
guide_list_features(stub)
print("-------------- RecordRoute --------------")
guide_record_route(stub)
print("-------------- RouteChat --------------")
guide_route_chat(stub)
7. Dùng thử
Chạy máy chủ:
python route_guide_server.py
Từ một thiết bị đầu cuối khác, hãy kích hoạt lại môi trường ảo (source .venv/bin/activate)
), sau đó chạy ứng dụng:
python route_guide_client.py
Hãy xem kết quả.
ListFeatures
Trước tiên, bạn sẽ thấy danh sách các tính năng. Mỗi đối tượng được truyền trực tuyến từ máy chủ (RPC truyền trực tuyến phía máy chủ) khi máy chủ phát hiện thấy các đối tượng đó nằm trong hình chữ nhật được yêu cầu:
-------------- ListFeatures -------------- Looking for features between 40, -75 and 42, -73 Feature called 'Patriots Path, Mendham, NJ 07945, USA' at (lat=407838351, lng=-746143763) Feature called '101 New Jersey 10, Whippany, NJ 07981, USA' at (lat=408122808, lng=-743999179) Feature called 'U.S. 6, Shohola, PA 18458, USA' at (lat=413628156, lng=-749015468) Feature called '5 Conners Road, Kingston, NY 12401, USA' at (lat=419999544, lng=-740371136) ...
RecordRoute
Thứ hai, RecordRoute
minh hoạ danh sách các điểm được truy cập ngẫu nhiên được truyền trực tuyến từ ứng dụng đến máy chủ (RPC truyền trực tuyến phía ứng dụng):
-------------- RecordRoute -------------- Visiting point (lat=410395868, lng=-744972325) Visiting point (lat=404310607, lng=-740282632) Visiting point (lat=403966326, lng=-748519297) Visiting point (lat=407586880, lng=-741670168) Visiting point (lat=406589790, lng=-743560121) Visiting point (lat=410322033, lng=-747871659) Visiting point (lat=415464475, lng=-747175374) Visiting point (lat=407586880, lng=-741670168) Visiting point (lat=402647019, lng=-747071791) Visiting point (lat=414638017, lng=-745957854)
Sau khi truyền phát xong tất cả các điểm đã truy cập, ứng dụng sẽ nhận được một phản hồi không truyền phát (một RPC đơn phương) từ máy chủ. Phản hồi này sẽ chứa thông tin tóm tắt về các phép tính được thực hiện trên toàn bộ tuyến đường của khách hàng.
Finished trip with 10 points Passed 10 features Traveled 654743 meters It took 0 seconds
RouteChat
Cuối cùng, đầu ra RouteChat
minh hoạ truyền trực tuyến hai chiều. Khi ứng dụng đang "truy cập" các điểm home
hoặc work
, ứng dụng sẽ ghi lại một ghi chú cho điểm đó bằng cách gửi RouteNote đến máy chủ. Khi một điểm đã được truy cập, máy chủ sẽ truyền trực tuyến tất cả các ghi chú trước đó cho điểm này.
-------------- RouteChat -------------- Sending RouteNote for (lat=1, lng=1): Departing from home Sending RouteNote for (lat=2, lng=2): Arrived at work Sending RouteNote for (lat=2, lng=2): Having lunch at work < Found previous note at (lat=2, lng=2): Arrived at work Sending RouteNote for (lat=2, lng=2): Departing from work < Found previous note at (lat=2, lng=2): Arrived at work < Found previous note at (lat=2, lng=2): Having lunch at work Sending RouteNote for (lat=1, lng=1): Arrived home < Found previous note at (lat=1, lng=1): Departing from home
8. Bước tiếp theo
- Tìm hiểu cách gRPC hoạt động trong phần Giới thiệu về gRPC và Các khái niệm cốt lõi
- Xem Hướng dẫn cơ bản
- Khám phá tài liệu tham khảo về Python API