Mục Lục
- Tổng quan về giao tiếp mạng
- Địa chỉ IP
- Câu lệnh Linux (ping, ifconfig)
- Cổng (Port)
- Giới thiệu về socket
- Chương trình mạng UDP - Gửi và nhận dữ liệu
- Chuyển đổi mã hóa trong Python 3
- Vấn đề liên kết cổng UDP (quan trọng)
- Lập trình giao tiếp mạng (phiên bản đơn giản)
- Case study: Phòng chat UDP
Tổng quan về giao tiếp mạng
1. Mạng là gì?
- Mạng là một công cụ giúp kết nối hai hoặc nhiều bên với nhau
- Không có mạng, thế giới "đơn máy" sẽ rất cô đơn
Ví dụ: Game đơn máy (không thể chơi cùng bạn bè ở xa)
2. Mục đích sử dụng mạng
Mục đích là để kết nối nhiều bên và cho phép giao tiếp, tức là truyền dữ liệu từ một bên sang bên khác.
Các chương trình chúng ta đã học trước đây đều là chương trình đơn máy, tức là không thể giao tiếp với các chương trình trên máy tính khác. Để các phần mềm trên các máy tính khác nhau có thể truyền dữ liệu cho nhau, chúng ta cần đến chức năng của mạng.
Tóm tắt nhỏ
- Sử dụng mạng có thể kết nối nhiều bên lại với nhau và cho phép truyền dữ liệu
- Lập trình mạng là cho phép các phần mềm trên các máy tính khác nhau có thể truyền dữ liệu cho nhau, tức là giao tiếp giữa các tiến trình
Địa chỉ IP (quan trọng)
1. Địa chỉ là gì?
Địa chỉ được dùng để đánh dấu vị trí.
2. Vai trò của địa chỉ IP
Địa chỉ IP: Dùng để đánh dấu một máy tính trong mạng, ví dụ 192.168.1.1; trong mạng cục bộ là duy nhất.
3. Phân loại địa chỉ IP (nắm bắt)
Mỗi địa chỉ IP bao gồm hai phần: địa chỉ mạng và địa chỉ máy chủ.
3.1 Địa chỉ IP loại A
Một địa chỉ IP loại A bao gồm 1 byte địa chỉ mạng và 3 byte địa chỉ máy chủ, bit cao nhất của địa chỉ mạng phải là "0", phạm vi địa chỉ từ 1.0.0.1-126.255.255.254.
3.2 Địa chỉ IP loại B
Một địa chỉ IP loại B bao gồm 2 byte địa chỉ mạng và 2 byte địa chỉ máy chủ, bit cao nhất của địa chỉ mạng phải là "10", phạm vi địa chỉ từ 128.1.0.1-191.255.255.254.
3.3 Địa chỉ IP loại C
Một địa chỉ IP loại C bao gồm 3 byte địa chỉ mạng và 1 byte địa chỉ máy chủ, bit cao nhất của địa chỉ mạng phải là "110", phạm vi địa chỉ từ 192.0.1.1-223.255.255.254.
3.4 Địa chỉ D dùng cho đa điểm phát sóng
Địa chỉ IP loại D bắt đầu bằng "1110", đây là một địa chỉ được dành riêng. Nó không chỉ đến một mạng cụ thể, hiện tại loại địa chỉ này được dùng cho đa điểm phát sóng (Multicast). Địa chỉ đa điểm phát sóng dùng để tìm kiếm một nhóm máy tính cùng lúc, phạm vi địa chỉ từ 224.0.0.1-239.255.255.254.
3.5 Địa chỉ IP loại E
Bắt đầu bằng "1111", được dành cho việc sử dụng trong tương lai.
3.6 IP riêng
Trong số nhiều địa chỉ mạng IP, quốc tế quy định một phần địa chỉ IP được dùng cho mạng cục bộ của chúng ta, tức là thuộc về IP riêng, không sử dụng trong mạng công cộng, phạm vi của chúng là:
10.0.0.0~10.255.255.255 172.16.0.0~172.31.255.255 192.168.0.0~192.168.255.255
3.7 Lưu ý
Địa chỉ IP 127.0.0.1~127.255.255.255 dùng cho kiểm tra vòng lặp, ví dụ: 127.0.0.1 có thể đại diện cho địa chỉ IP của máy bản địa, dùng `http://127.0.0.1` có thể kiểm tra máy chủ web được cấu hình trên máy này.
Câu lệnh Linux (ping, ifconfig)
Xem hoặc cấu hình thông tin card mạng: ifconfig
Nếu chúng ta chỉ gõ: ifconfig, nó sẽ hiển thị thông tin của tất cả các card mạng:
Kiểm tra tính kết nối của máy chủ từ xa: ping
Thông thường dùng ping để kiểm tra mạng có bình thường không.
Cổng (Port)
1. Cổng là gì?
Cổng giống như một cánh cửa của một ngôi nhà, là con đường bắt buộc để vào ra ngôi nhà đó. Nếu một chương trình cần nhận và gửi dữ liệu mạng, thì cần có những cổng như vậy.
Trong hệ thống Linux, có thể có tới 65536 (lũy thừa 2 của 16) cổng!
Vì có nhiều như vậy, hệ điều hành để quản lý thống nhất đã tiến hành đánh số, đó chính là `số cổng`.
2. Số cổng
Cổng được đánh dấu bằng số cổng, số cổng chỉ có số nguyên, phạm vi từ 0 đến 65535.
Lưu ý: Số lượng cổng của các hệ thống *nix khác nhau có thể khác nhau, còn có thể sửa đổi thủ công.
3. Cổng được phân bổ như thế nào?
Số cổng không được sử dụng tùy tiện, mà được phân bổ theo quy định nhất định. Phân loại cổng có tiêu chuẩn tốt几种, chúng ta không đi vào chi tiết ở đây, chỉ giới thiệu về cổng nổi tiếng và cổng động.
3.1 Cổng nổi tiếng (Well Known Ports)
Cổng nổi tiếng là những số cổng được biết đến rộng rãi, phạm vi từ 0 đến 1023.
80 cổng được phân bổ cho dịch vụ HTTP 21 cổng được phân bổ cho dịch vụ FTP
Có thể hiểu rằng, một số chức năng phổ biến sử dụng số cổng là ước tính, giống như số điện thoại 110, 10086, 10010.
Thông thường, nếu một chương trình cần sử dụng cổng nổi tiếng thì cần có quyền root.
3.2 Cổng động (Dynamic Ports)
Phạm vi của cổng động là từ 1024 đến 65535.
Được gọi là cổng động vì nó thường không được phân bổ cố định cho một dịch vụ cụ thể, mà được phân bổ động.
Phân bổ động là khi một hệ thống chương trình hoặc chương trình ứng dụng cần giao tiếp mạng, nó yêu cầu máy chủ một cổng, máy chủ sẽ phân bổ một cổng từ các số cổng có sẵn để nó sử dụng. Khi chương trình này đóng, đồng thời cũng giải phóng số cổng đã chiếm dụng.
3.3 Làm thế nào để xem cổng?
- Dùng "netstat -an" để xem trạng thái cổng
- lsof -i [tcp/udp]:2425
4. Tóm tắt nhỏ
Cổng có tác dụng gì? Chúng ta biết rằng, một máy chủ có địa chỉ IP có thể cung cấp nhiều dịch vụ, chẳng hạn như HTTP (dịch vụ web), FTP (truyền tệp), SMTP (email) v.v., những dịch vụ này hoàn toàn có thể được thực hiện thông qua một địa chỉ IP. Vậy máy chủ phân biệt các dịch vụ mạng khác nhau như thế nào? Rõ ràng không thể chỉ dựa vào địa chỉ IP, vì mối quan hệ giữa địa chỉ IP và dịch vụ mạng là một-một-nhiều. Thực tế, nó phân biệt các dịch vụ khác nhau thông qua "địa chỉ IP + số cổng". Cần lưu ý rằng cổng không phải là một-một. Ví dụ, khi máy tính của bạn làm máy khách truy cập một máy chủ WWW, máy chủ WWW sử dụng cổng "80" để giao tiếp với máy tính của bạn, nhưng máy tính của bạn có thể sử dụng cổng "3457" như vậy.
Giới thiệu về socket
1. Các tiến trình trên các máy tính khác nhau giao tiếp như thế nào?
Vấn đề cần giải quyết trước tiên là làm thế nào để đánh dấu duy nhất một tiến trình, nếu không thì giao tiếp không thể thực hiện!
Trên một máy tính, có thể dùng số tiến trình (PID) để đánh dấu duy nhất một tiến trình, nhưng trong mạng điều này không khả thi.
Thực tế, họp thức TCP/IP đã giúp chúng ta giải quyết vấn đề này, địa chỉ mạng "địa chỉ IP" có thể đánh dấu duy nhất máy tính trong mạng, còn tầng truyền tải "giao thức + cổng" có thể đánh dấu duy nhất tiến trình ứng dụng (tiến trình) trong máy chủ.
Đây là cách sử dụng địa chỉ IP, giao thức, cổng để đánh dấu các tiến trình mạng, giao tiếp giữa các tiến trình mạng có thể sử dụng dấu hiệu này để tương tác với các tiến trình khác.
Lưu ý:
- Đây là
tiến trình: Chỉ chương trình đang chạy và tài nguyên được sử dụng khi chạy gọi chung là tiến trình (sẽ giải thích chi tiết khi nói về lập trình đa nhiệm). - Đây là
giao tiếp giữa các tiến trình: Chia sẻ dữ liệu giữa các chương trình đang chạy. - Sẽ nói chi tiết về kiến thức như tầng mạng sau này, đừng vội.
2. Socket là gì?
Socket (viết tắt là cắm ổ) là một cách giao tiếp giữa các tiến trình, khác với các cách giao tiếp giữa các tiến trình khác là: Nó có thể thực hiện giao tiếp giữa các tiến trình trên các máy tính khác nhau, hầu hết các dịch vụ trên mạng của chúng ta đều hoàn thành giao tiếp dựa trên Socket.
Ví dụ: Mỗi ngày chúng ta duyệt web, chat QQ, gửi email v.v.
3. Tạo socket (quan trọng)
Trong Python, sử dụng hàm socket của mô-đun socket có thể hoàn thành:
import socket socket.socket(AddressFamily, Type)
Giải thích:
Hàm socket.socket tạo một socket, hàm này có hai tham số:
- Address Family: Có thể chọn AF_INET (dùng cho giao tiếp tiến trình Internet) hoặc AF_UNIX (dùng cho giao tiếp tiến trình trên cùng một máy), trong công việc thực tế thường dùng AF_INET.
- Type: Loại socket, có thể là SOCK_STREAM (socket luồng, chủ yếu dùng cho giao thức TCP) hoặc SOCK_DGRAM (socket gói dữ liệu, chủ yếu dùng cho giao thức UDP).
Tạo một socket tcp (socket tcp)
import socket # Tạo socket tcp mang_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # ...Đây là chức năng sử dụng socket (bỏ qua)... # Khi không dùng nữa, đóng socket mang_socket.close()
Tạo một socket udp (socket udp)
import socket # Tạo socket udp mang_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # ...Đây là chức năng sử dụng socket (bỏ qua)... # Khi không dùng nữa, đóng socket mang_socket.close()
Giải thích
Quy trình sử dụng socket tương tự như quy trình sử dụng tệp:
- Tạo socket
- Sử dụng socket để nhận/gửi dữ liệu
- Đóng socket
Chương trình mạng UDP - Gửi và nhận dữ liệu
1. Chương trình mạng UDP - Gửi dữ liệu
Tạo một chương trình mạng dựa trên udp rất đơn giản, các bước cụ thể như sau:
- Tạo socket của máy khách
- Gửi/nhận dữ liệu
- Đóng socket
Mã như sau:
#coding=utf-8
from socket import *
# 1. Tạo socket udp
ket_noi_udp = socket(AF_INET, SOCK_DGRAM)
# 2. Chuẩn bị địa chỉ của người nhận
# '192.168.1.103' biểu thị địa chỉ ip đích
# 8080 biểu thị cổng đích
dia_chi_nhan = ('192.168.1.103', 8080) # Lưu ý là bộ, ip là chuỗi, cổng là số
# 3. Nhập dữ liệu từ bàn phím
du_lieu_gui = input("Vui lòng nhập dữ liệu cần gửi:")
# 4. Gửi dữ liệu đến chương trình cụ thể trên máy tính cụ thể
ket_noi_udp.sendto(du_lieu_gui.encode('utf-8'), dia_chi_nhan)
# 5. Đóng socket
ket_noi_udp.close()
2. Chương trình mạng UDP - Gửi và nhận dữ liệu
#coding=utf-8
from socket import *
# 1. Tạo socket udp
ket_noi_udp = socket(AF_INET, SOCK_DGRAM)
# 2. Chuẩn bị địa chỉ của người nhận
dia_chi_nhan = ('192.168.236.129', 8080)
# 3. Nhập dữ liệu từ bàn phím
du_lieu_gui = input("Vui lòng nhập dữ liệu cần gửi:")
# 4. Gửi dữ liệu đến máy tính cụ thể
ket_noi_udp.sendto(du_lieu_gui.encode('utf-8'), dia_chi_nhan)
# 5. Chờ nhận dữ liệu gửi từ bên kia
du_lieu_nhan = ket_noi_udp.recvfrom(1024) # 1024 biểu thị số byte tối đa nhận được lần này
# 6. Hiển thị dữ liệu gửi từ bên kia
# Dữ liệu nhận được du_lieu_nhan là một bộ
# Phần tử thứ nhất là dữ liệu gửi từ bên kia
# Phần tử thứ hai là ip và cổng của bên kia
print(du_lieu_nhan[0].decode('gbk'))
print(du_lieu_nhan[1])
# 7. Đóng socket
ket_noi_udp.close()
Chuyển đổi mã hóa trong Python 3
str->bytes:encode mã hóa bytes->str:decode giải mã
Chuỗi được mã hóa thành mã byte, mã byte được giải mã thành chuỗi.
>>> text = 'Tôi là văn bản' >>> text 'Tôi là văn bản' >>> print(text) Tôi là văn bản >>> bytesText = text.encode() >>> bytesText b'T\xf4i l\xe0 v膬n b蓅n' >>> print(bytesText) b'T\xf4i l\xe0 v膬n b蓅n' >>> type(text) <class 'str'> >>> type(bytesText) <class 'bytes'> >>> textDecode = bytesText.decode() >>> textDecode 'Tôi là văn bản' >>> print(textDecode) Tôi là văn bản
Trong đó decode() và encode() có thể chấp nhận tham số, khai báo lần lượt là:
bytes.decode(encoding="utf-8", errors="strict") str.encode(encoding="utf-8", errors="strict")
Trong đó biểu thị trong quá trình giải mã mã hóa sử dụng mã hóa (ở đây "phương án mã hóa" là danh từ), errors biểu thị phương án xử lý lỗi.
Chi tiết có thể tham khảo tài liệu chính thức:
- https://docs.python.org/3/library/stdtypes.html?highlight=decode#str.encode
- https://docs.python.org/3/library/stdtypes.html?highlight=decode#bytes.decode
Liên kết thông tin UDP
1. Vấn đề cổng trong chương trình mạng UDP
- Số cổng sẽ thay đổi
Chạy lại nhiều lần script, trong "trợ lý gỡ lỗi mạng", hiện tượng thấy như sau:
Giải thích:
- Mỗi lần chạy lại chương trình mạng, số trong vòng tròn đỏ trong hình trên không giống nhau là do số này biểu thị chương trình mạng này, khi chạy lại, nếu không xác định chính xác dùng cái nào, hệ thống mặc định sẽ phân bổ ngẫu nhiên
- Nhớ một điểm: Trong quá trình chạy chương trình mạng này, nó sẽ duy nhất biểu thị chương trình này, vì vậy nếu các chương trình mạng trên máy tính khác muốn gửi dữ liệu đến chương trình này, thì cần gửi đến số (tức cổng) biểu thị chương trình này
2. Liên kết thông tin UDP
1. Liên kết thông tin
Thông thường, trên một máy tính chạy rất nhiều chương trình mạng, để không chiếm dụng cùng một số cổng với các chương trình mạng khác, thường trong lập trình, cổng udp thường không liên kết
Nhưng nếu cần làm thành một chương trình phía máy chủ thì cần liên kết, nghĩ xem lại tại sao?
Nếu số điện thoại báo cảnh sát thay đổi mỗi ngày, chắc chắn thế giới sẽ hỗn loạn, vì vậy các chương trình dịch vụ thường cần một số cổng cố định, đó chính là liên kết cổng.
2. Ví dụ liên kết
#coding=utf-8
from socket import *
# 1. Tạo socket
ket_noi_udp = socket(AF_INET, SOCK_DGRAM)
# 2. Liên kết thông tin cục bộ, nếu một chương trình mạng không liên kết, hệ thống sẽ phân bổ ngẫu nhiên
dia_chi_cuc_bo = ('', 7788) # Địa chỉ ip và số cổng, ip thường không cần viết, biểu thị bất kỳ ip nào của máy
ket_noi_udp.bind(dia_chi_cuc_bo)
# 3. Chờ nhận dữ liệu gửi từ bên kia
du_lieu_nhan = ket_noi_udp.recvfrom(1024) # 1024 biểu thị số byte tối đa nhận được lần này
# 4. Hiển thị dữ liệu nhận được
print(du_lieu_nhan[0].decode('gbk'))
# 5. Đóng socket
ket_noi_udp.close()
3. Kết quả chạy
4. Tóm tắt
- Một chương trình mạng udp, có thể không liên kết, lúc này hệ điều hành sẽ phân bổ ngẫu nhiên một cổng, nếu chạy lại chương trình này cổng có thể thay đổi
- Một chương trình mạng udp, cũng có thể liên kết thông tin (địa chỉ ip, số cổng), nếu liên kết thành công, hệ điều hành dùng số cổng này để phân biệt dữ liệu mạng nhận được có phải của tiến trình này không
Quy trình giao tiếp mạng (phiên bản đơn giản)
Giải thích
Trong quá trình giao tiếp mạng,之所以 cần ip, port v.v., chính là để có thể phân chia nhiệm vụ cho một quá trình giao tiếp phức tạp, từ đó đảm bảo dữ liệu truyền đi chính xác không sai
Ứng dụng: Trình chat UDP
Giải thích
Trong một máy tính viết 1 chương trình, có 2 chức năng 1. Nhận dữ liệu từ bàn phím và gửi cho bên kia 2. Nhận dữ liệu và hiển thị Và chức năng lựa chọn gọi 2 chức năng trên
Yêu cầu
Thực hiện chương trình trên
Mã tham khảo
import socket
def gui_thong_diep(socket_mang):
"""Nhận dữ liệu từ bàn phím và gửi cho bên kia"""
# 1. Nhập dữ liệu từ bàn phím
thong_diep = input("\nVui lòng nhập dữ liệu cần gửi:")
# 2. Nhập địa chỉ ip của bên kia
ip_dich = input("\nVui lòng nhập địa chỉ ip của bên kia:")
# 3. Nhập port của bên kia
port_dich = int(input("\nVui lòng nhập port của bên kia:"))
# 4. Gửi dữ liệu
socket_mang.sendto(thong_diep.encode("utf-8"), (ip_dich, port_dich))
def nhan_thong_diep(socket_mang):
"""Nhận dữ liệu và hiển thị"""
# 1. Nhận dữ liệu
du_lieu_nhan = socket_mang.recvfrom(1024)
# 2. Giải mã
ip_nhan = du_lieu_nhan[1]
du_lieu_nhan = du_lieu_nhan[0].decode("utf-8")
# 3. Hiển thị dữ liệu nhận được
print(">>>%s:%s" % (str(ip_nhan), du_lieu_nhan))
def chinh():
# 1. Tạo socket
socket_mang = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2. Liên kết thông tin cục bộ
socket_mang.bind(("", 7890))
while True:
# 3. Chọn chức năng
print("="*30)
print("1:Gửi tin nhắn")
print("2:Nhận tin nhắn")
print("="*30)
so_chuc_nang = input("Vui lòng nhập số thứ tự chức năng cần thực hiện:")
# 4. Gọi hàm tương ứng theo lựa chọn
if so_chuc_nang == "1":
gui_thong_diep(socket_mang)
elif so_chuc_nang == "2":
nhan_thong_diep(socket_mang)
else:
print("Nhập sai, vui lòng nhập lại...")
if __name__ == "__main__":
chinh()
Nghĩ nghĩ
- Chương trình trên nếu chọn chức năng nhận dữ liệu, và lúc này không có dữ liệu, chương trình sẽ bị kẹt ở đây, vậy làm thế nào để chương trình này có thể nhận và gửi dữ liệu cùng lúc? Đừng vội, học xong kiến thức đa nhiệm sau này sẽ giải quyết O(∩_∩)O...