Máy chủ TCP không chặn Python

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ền

Bâ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[]
0

Hã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í

  • 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,
    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[]
    
    8
  • 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[]
    
    9,
    $ python client.py "client N"
    
    0
  • $ python client.py "client N"
    
    1,
    $ python client.py "client N"
    
    2

đ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
    $ python client.py "client N"
    
    3;
  • 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ủ PID

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[]
9

Hệ đ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

Chủ Đề