Trong bài đăng này, chúng ta sẽ nói về kết nối mạng nhưng bạn có thể dễ dàng ánh xạ nó tới các hoạt động đầu vào/đầu ra [I/O] khác, ví dụ: thay đổi ổ cắm thành bộ mô tả tệp. Ngoài ra, phần giải thích này không tập trung vào bất kỳ ngôn ngữ lập trình cụ thể nào mặc dù các ví dụ sẽ được đưa ra bằng Python [tôi có thể nói gì đây – Tôi yêu Python. ]
Bằng cách này hay cách khác, khi bạn có câu hỏi về việc chặn hoặc không chặn cuộc gọi, điều đó thường có nghĩa là xử lý I/O. Trường hợp phổ biến nhất trong thời đại thông tin, vi dịch vụ và chức năng lambda của chúng ta sẽ là xử lý yêu cầu. Chúng tôi có thể tưởng tượng ngay rằng bạn, bạn đọc thân mến, là người dùng của một trang web, trong khi trình duyệt của bạn [hoặc ứng dụng mà bạn đang đọc những dòng này] là một ứng dụng khách. Ở đâu đó trong sâu thẳm Amazon, có một máy chủ xử lý các yêu cầu gửi đến của bạn để tạo ra những dòng giống như bạn đang đọc
Để bắt đầu tương tác trong giao tiếp máy khách-máy chủ như vậy, trước tiên máy khách và máy chủ phải thiết lập kết nối với nhau. Chúng ta sẽ không đi sâu vào mô hình 7 lớp và ngăn xếp giao thức liên quan đến tương tác này, vì tôi nghĩ tất cả đều có thể dễ dàng tìm thấy trên Internet. Điều chúng ta cần hiểu là ở cả hai phía [máy khách và máy chủ] đều có các điểm kết nối đặc biệt được gọi là ổ cắm. Cả máy khách và máy chủ phải được liên kết với ổ cắm của nhau và lắng nghe chúng để hiểu những gì người kia nói ở phía đối diện của dây
Trong giao tiếp của chúng tôi, máy chủ đang thực hiện một việc gì đó - xử lý yêu cầu, chuyển đổi đánh dấu thành HTML hoặc xem hình ảnh ở đâu, nó thực hiện một số loại xử lý
Nếu bạn nhìn vào tỷ lệ giữa tốc độ CPU và tốc độ mạng, sự khác biệt là một vài bậc độ lớn. Nó chỉ ra rằng nếu ứng dụng của chúng tôi sử dụng I/O hầu hết thời gian, thì trong hầu hết các trường hợp, bộ xử lý sẽ không làm gì cả. Loại ứng dụng này được gọi là I/O-bound. Đối với các ứng dụng yêu cầu hiệu suất cao, đó là một nút cổ chai và đó là những gì chúng ta sẽ nói về tiếp theo
Có hai cách để tổ chức I/O [tôi sẽ đưa ra các ví dụ dựa trên Linux]. chặn và không chặn
Ngoài ra, có hai loại hoạt động I/O. đồng bộ và không đồng bộ
Tất cả cùng nhau, chúng đại diện cho các mô hình I/O có thể
Mỗi mô hình I/O này có các mẫu sử dụng thuận lợi cho các ứng dụng cụ thể. Ở đây tôi sẽ chứng minh sự khác biệt giữa hai cách tổ chức I/O
Chặn I/O
Với I/O chặn, khi máy khách đưa ra yêu cầu kết nối với máy chủ, ổ cắm xử lý kết nối đó và luồng tương ứng đọc từ nó sẽ bị chặn cho đến khi một số dữ liệu đọc xuất hiện. Dữ liệu này được đặt trong bộ đệm mạng cho đến khi tất cả được đọc và sẵn sàng để xử lý. Cho đến khi hoạt động hoàn tất, máy chủ không thể làm gì khác ngoài việc chờ đợi
Kết luận đơn giản nhất từ điều này là chúng ta không thể phục vụ nhiều hơn một kết nối trong một luồng. Theo mặc định, ổ cắm TCP hoạt động ở chế độ chặn
Một ví dụ đơn giản trên Python, máy khách
import socket
import sys
import time
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
while True:
sock.connect[[host, port]]
while True:
data = str.encode[sys.argv[1]]
sock.send[data]
time.sleep[0.5]
if __name__ == "__main__":
assert len[sys.argv] > 1, "Please provide message"
main[]
Ở đây chúng tôi gửi một tin nhắn với khoảng thời gian 50ms đến máy chủ trong vòng lặp vô tận. Hãy tưởng tượng rằng giao tiếp máy khách-máy chủ này bao gồm việc tải xuống một tệp lớn - phải mất một thời gian để hoàn thành
Và máy chủ
import socket
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
# bind the socket to the port
sock.bind[[host, port]]
# listen for incoming connections
sock.listen[5]
print["Server started..."]
while True:
conn, addr = sock.accept[] # accepting the incoming connection, blocking
print['Connected by ' + str[addr]]
while True:
data = conn.recv[1024] # receving data, blocking
if not data:
break
print[data]
if __name__ == "__main__":
main[]
Tôi đang chạy cái này trong các cửa sổ đầu cuối riêng biệt với một số máy khách như
$ python client.py "client N"
Và máy chủ như
$ python server.py
Ở đây chúng tôi chỉ lắng nghe ổ cắm và chấp nhận các kết nối đến. Sau đó, chúng tôi cố gắng nhận dữ liệu từ kết nối này
Trong đoạn mã trên, máy chủ về cơ bản sẽ bị chặn bởi một kết nối máy khách. Nếu chúng tôi chạy một ứng dụng khách khác với một thông báo khác, bạn sẽ không thấy nó. Tôi thực sự khuyên bạn nên chơi với ví dụ này để hiểu chuyện gì đang xảy ra
Chuyện gì đang xảy ra ở đây?
Phương thức
$ python client.py "client N"
0 sẽ cố gắng gửi tất cả dữ liệu đến máy chủ trong khi bộ đệm ghi trên máy chủ sẽ tiếp tục nhận dữ liệu. Khi lệnh gọi hệ thống để đọc được gọi, ứng dụng sẽ bị chặn và ngữ cảnh được chuyển sang kernel. Hạt nhân bắt đầu đọc - dữ liệu được chuyển đến bộ đệm không gian người dùng. Khi bộ đệm trống, kernel sẽ đánh thức lại tiến trình để nhận phần dữ liệu tiếp theo sẽ được truyềnBây giờ để xử lý hai máy khách với phương pháp này, chúng tôi cần có một số luồng, tôi. e. để phân bổ một luồng mới cho mỗi kết nối máy khách. Chúng tôi sẽ sớm quay lại vấn đề đó
I/O không chặn
Tuy nhiên, cũng có một tùy chọn thứ hai — non-blocking I/O. Sự khác biệt rõ ràng ngay từ cái tên của nó — thay vì chặn, bất kỳ thao tác nào cũng được thực hiện ngay lập tức. Non-blocking I/O có nghĩa là yêu cầu ngay lập tức được xếp hàng đợi và chức năng được trả về. I/O thực tế sau đó được xử lý sau đó
Bằng cách đặt ổ cắm ở chế độ không chặn, bạn có thể thẩm vấn nó một cách hiệu quả. Nếu bạn cố đọc từ ổ cắm không chặn và không có dữ liệu, nó sẽ trả về mã lỗi [
$ python client.py "client N"
1 hoặc $ python client.py "client N"
2]Trên thực tế, loại bỏ phiếu này là một ý tưởng tồi. Nếu bạn chạy chương trình của mình trong một chu kỳ thăm dò dữ liệu liên tục từ ổ cắm, nó sẽ tiêu tốn nhiều thời gian của CPU. Điều này có thể cực kỳ kém hiệu quả vì trong nhiều trường hợp, ứng dụng phải bận đợi cho đến khi có dữ liệu hoặc cố gắng thực hiện công việc khác trong khi lệnh được thực hiện trong kernel. Một cách hay hơn để kiểm tra xem dữ liệu có thể đọc được hay không là sử dụng
import socket
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
# bind the socket to the port
sock.bind[[host, port]]
# listen for incoming connections
sock.listen[5]
print["Server started..."]
while True:
conn, addr = sock.accept[] # accepting the incoming connection, blocking
print['Connected by ' + str[addr]]
while True:
data = conn.recv[1024] # receving data, blocking
if not data:
break
print[data]
if __name__ == "__main__":
main[]
0Hãy để chúng tôi quay lại ví dụ của chúng tôi với những thay đổi trên máy chủ
import select
import socket
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
sock.setblocking[0]
# bind the socket to the port
sock.bind[[host, port]]
# listen for incoming connections
sock.listen[5]
print["Server started..."]
# sockets from which we expect to read
inputs = [sock]
outputs = []
while inputs:
# wait for at least one of the sockets to be ready for processing
readable, writable, exceptional = select.select[inputs, outputs, inputs]
for s in readable:
if s is sock:
conn, addr = s.accept[]
inputs.append[conn]
else:
data = s.recv[1024]
if data:
print[data]
else:
inputs.remove[s]
s.close[]
if __name__ == "__main__":
main[]
Bây giờ nếu chúng tôi chạy mã này với> 1 máy khách, bạn sẽ thấy rằng máy chủ không bị chặn bởi một máy khách nào và nó xử lý mọi thứ có thể được phát hiện bởi các thông báo được hiển thị. Một lần nữa, tôi khuyên bạn nên tự mình thử ví dụ này
Những gì đang xảy ra ở đây?
Ở đây máy chủ không đợi tất cả dữ liệu được ghi vào bộ đệm. Khi chúng tôi tạo một ổ cắm không bị chặn bằng cách gọi
import socket
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
# bind the socket to the port
sock.bind[[host, port]]
# listen for incoming connections
sock.listen[5]
print["Server started..."]
while True:
conn, addr = sock.accept[] # accepting the incoming connection, blocking
print['Connected by ' + str[addr]]
while True:
data = conn.recv[1024] # receving data, blocking
if not data:
break
print[data]
if __name__ == "__main__":
main[]
1, nó sẽ không bao giờ đợi thao tác hoàn tất. Vì vậy, khi chúng ta gọi phương thức import socket
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
# bind the socket to the port
sock.bind[[host, port]]
# listen for incoming connections
sock.listen[5]
print["Server started..."]
while True:
conn, addr = sock.accept[] # accepting the incoming connection, blocking
print['Connected by ' + str[addr]]
while True:
data = conn.recv[1024] # receving data, blocking
if not data:
break
print[data]
if __name__ == "__main__":
main[]
2, nó sẽ quay trở lại luồng chính. Sự khác biệt cơ học chính là import socket
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
# bind the socket to the port
sock.bind[[host, port]]
# listen for incoming connections
sock.listen[5]
print["Server started..."]
while True:
conn, addr = sock.accept[] # accepting the incoming connection, blocking
print['Connected by ' + str[addr]]
while True:
data = conn.recv[1024] # receving data, blocking
if not data:
break
print[data]
if __name__ == "__main__":
main[]
3, import socket
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
# bind the socket to the port
sock.bind[[host, port]]
# listen for incoming connections
sock.listen[5]
print["Server started..."]
while True:
conn, addr = sock.accept[] # accepting the incoming connection, blocking
print['Connected by ' + str[addr]]
while True:
data = conn.recv[1024] # receving data, blocking
if not data:
break
print[data]
if __name__ == "__main__":
main[]
2, import socket
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
# bind the socket to the port
sock.bind[[host, port]]
# listen for incoming connections
sock.listen[5]
print["Server started..."]
while True:
conn, addr = sock.accept[] # accepting the incoming connection, blocking
print['Connected by ' + str[addr]]
while True:
data = conn.recv[1024] # receving data, blocking
if not data:
break
print[data]
if __name__ == "__main__":
main[]
5 và import socket
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
# bind the socket to the port
sock.bind[[host, port]]
# listen for incoming connections
sock.listen[5]
print["Server started..."]
while True:
conn, addr = sock.accept[] # accepting the incoming connection, blocking
print['Connected by ' + str[addr]]
while True:
data = conn.recv[1024] # receving data, blocking
if not data:
break
print[data]
if __name__ == "__main__":
main[]
6 có thể quay lại mà không cần làm gì cảVới cách tiếp cận này, chúng ta có thể thực hiện đồng thời nhiều thao tác I/O với các ổ cắm khác nhau từ cùng một luồng. Nhưng vì chúng tôi không biết liệu một ổ cắm đã sẵn sàng cho thao tác I/O hay chưa, chúng tôi sẽ phải hỏi từng ổ cắm với cùng một câu hỏi và về cơ bản quay trong một vòng lặp vô hạn [cách tiếp cận không chặn nhưng vẫn đồng bộ này được gọi là I
Để thoát khỏi vòng lặp không hiệu quả này, chúng ta cần cơ chế sẵn sàng bỏ phiếu. Trong cơ chế này, chúng tôi có thể thẩm vấn mức độ sẵn sàng của tất cả các ổ cắm và chúng sẽ cho chúng tôi biết cái nào đã sẵn sàng cho thao tác I/O mới và cái nào chưa sẵn sàng nếu không được hỏi rõ ràng. Khi bất kỳ ổ cắm nào sẵn sàng, chúng tôi sẽ thực hiện các thao tác trong hàng đợi và sau đó có thể quay lại trạng thái chặn, chờ ổ cắm sẵn sàng cho hoạt động I/O tiếp theo
Có một số cơ chế sẵn sàng bỏ phiếu, chúng khác nhau về hiệu suất và chi tiết, nhưng thông thường, các chi tiết được ẩn "dưới mui xe" và chúng tôi không nhìn thấy được
Từ khóa để tìm kiếm
thông báo
- Kích hoạt cấp độ [trạng thái]
- Kích hoạt cạnh [trạng thái đã thay đổi]
cơ khí
0,import socket def main[] -> None: host = socket.gethostname[] port = 12345 # create a TCP/IP socket with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock: # bind the socket to the port sock.bind[[host, port]] # listen for incoming connections sock.listen[5] print["Server started..."] while True: conn, addr = sock.accept[] # accepting the incoming connection, blocking print['Connected by ' + str[addr]] while True: data = conn.recv[1024] # receving data, blocking if not data: break print[data] if __name__ == "__main__": main[]
8import socket def main[] -> None: host = socket.gethostname[] port = 12345 # create a TCP/IP socket with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock: # bind the socket to the port sock.bind[[host, port]] # listen for incoming connections sock.listen[5] print["Server started..."] while True: conn, addr = sock.accept[] # accepting the incoming connection, blocking print['Connected by ' + str[addr]] while True: data = conn.recv[1024] # receving data, blocking if not data: break print[data] if __name__ == "__main__": main[]
9,import socket def main[] -> None: host = socket.gethostname[] port = 12345 # create a TCP/IP socket with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock: # bind the socket to the port sock.bind[[host, port]] # listen for incoming connections sock.listen[5] print["Server started..."] while True: conn, addr = sock.accept[] # accepting the incoming connection, blocking print['Connected by ' + str[addr]] while True: data = conn.recv[1024] # receving data, blocking if not data: break print[data] if __name__ == "__main__": main[]
0$ python client.py "client N"
1,$ python client.py "client N"
2$ python client.py "client N"
đa nhiệm
Do đó, mục tiêu của chúng tôi là quản lý nhiều khách hàng cùng một lúc. Làm cách nào chúng tôi có thể đảm bảo nhiều yêu cầu được xử lý cùng một lúc?
Có một số tùy chọn
Quy trình riêng biệt
Cách tiếp cận đầu tiên và đơn giản nhất trong lịch sử là xử lý từng yêu cầu trong một quy trình riêng biệt. Cách tiếp cận này là thỏa đáng vì chúng tôi có thể sử dụng cùng một API I/O chặn. Nếu một quy trình đột nhiên bị lỗi, nó sẽ chỉ ảnh hưởng đến các hoạt động được xử lý trong quy trình cụ thể đó chứ không ảnh hưởng đến bất kỳ quy trình nào khác
Điểm trừ là giao tiếp phức tạp. Về mặt hình thức, hầu như không có điểm chung nào giữa các quy trình và bất kỳ giao tiếp không tầm thường nào giữa các quy trình mà chúng tôi muốn tổ chức đều cần có thêm nỗ lực để đồng bộ hóa quyền truy cập, v.v. Ngoài ra, tại bất kỳ thời điểm nào, có thể có một số quy trình chỉ chờ yêu cầu của khách hàng và điều này chỉ gây lãng phí tài nguyên
Hãy để chúng tôi xem làm thế nào điều này hoạt động trong thực tế. Ngay khi quy trình đầu tiên [quy trình chính/quy trình chính] bắt đầu, nó sẽ tạo ra một số bộ quy trình dưới dạng worker. Mỗi người trong số họ có thể nhận các yêu cầu trên cùng một ổ cắm và đợi các máy khách đến. Ngay khi một kết nối đến xuất hiện, một trong các quy trình xử lý nó - nhận kết nối này, xử lý nó từ đầu đến cuối, đóng ổ cắm và sau đó sẵn sàng trở lại cho yêu cầu tiếp theo. Có thể có các biến thể - quy trình có thể được tạo cho mỗi kết nối đến hoặc tất cả chúng có thể được bắt đầu trước, v.v. Điều này có thể ảnh hưởng đến hiệu suất, nhưng nó không quá quan trọng đối với chúng tôi bây giờ
Ví dụ về các hệ thống như vậy
- Apache
3;$ python client.py "client N"
- FastCGI cho những người thường chạy PHP nhất;
- Phusion Pasbah dành cho những người viết trên Ruby on Rails;
- PostgreSQL
chủ đề
Một cách tiếp cận khác là sử dụng các luồng Hệ điều hành [OS]. Trong một quy trình, chúng tôi có thể tạo một số chủ đề. Chặn I/O cũng có thể được sử dụng vì chỉ một luồng sẽ bị chặn
Ví dụ
import socket
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
# bind the socket to the port
sock.bind[[host, port]]
# listen for incoming connections
sock.listen[5]
print["Server started..."]
while True:
conn, addr = sock.accept[] # accepting the incoming connection, blocking
print['Connected by ' + str[addr]]
while True:
data = conn.recv[1024] # receving data, blocking
if not data:
break
print[data]
if __name__ == "__main__":
main[]
7Để kiểm tra số lượng luồng trên quy trình máy chủ, bạn có thể sử dụng lệnh linux
$ python client.py "client N"
4 với quy trình máy chủ PIDimport socket
def main[] -> None:
host = socket.gethostname[]
port = 12345
# create a TCP/IP socket
with socket.socket[socket.AF_INET, socket.SOCK_STREAM] as sock:
# bind the socket to the port
sock.bind[[host, port]]
# listen for incoming connections
sock.listen[5]
print["Server started..."]
while True:
conn, addr = sock.accept[] # accepting the incoming connection, blocking
print['Connected by ' + str[addr]]
while True:
data = conn.recv[1024] # receving data, blocking
if not data:
break
print[data]
if __name__ == "__main__":
main[]
9Hệ điều hành tự quản lý các luồng và có khả năng phân phối chúng giữa các lõi CPU có sẵn. Các luồng nhẹ hơn các quy trình. Về bản chất, điều đó có nghĩa là chúng ta có thể tạo ra nhiều luồng hơn các quy trình trên cùng một hệ thống. Chúng tôi khó có thể chạy 10.000 quy trình, nhưng 10.000 luồng có thể dễ dàng. Không phải là nó sẽ hiệu quả
Mặt khác, không có sự cô lập, tôi. e. nếu có bất kỳ sự cố nào, nó có thể khiến không chỉ một luồng cụ thể bị hỏng mà toàn bộ quá trình bị hỏng. Và khó khăn lớn nhất là bộ nhớ của quá trình mà các luồng hoạt động được chia sẻ bởi các luồng. Chúng tôi có một tài nguyên được chia sẻ - bộ nhớ và điều đó có nghĩa là cần phải đồng bộ hóa quyền truy cập vào nó. Mặc dù vấn đề đồng bộ hóa quyền truy cập vào bộ nhớ dùng chung là trường hợp đơn giản nhất, nhưng chẳng hạn, có thể có một kết nối đến cơ sở dữ liệu hoặc một nhóm kết nối đến cơ sở dữ liệu, điều này phổ biến đối với tất cả các luồng bên trong ứng dụng xử lý các kết nối đến. Rất khó để đồng bộ hóa quyền truy cập vào tài nguyên của bên thứ 3