Рубрики
Без рубрики

Клиент и сервер TCP / IP

Автор оригинала: Doug Hellmann.

Сокеты можно настроить для работы в качестве сервера и прослушивания входящих сообщений или для подключения к другим приложениям в качестве клиента . После подключения обоих концов сокета TCP/IP обмен данными становится двунаправленным.

Эхо-сервер

Эта программа-пример, основанная на той, что приведена в документации стандартной библиотеки, принимает входящие сообщения и возвращает их отправителю. Он начинается с создания сокета TCP/IP, затем используется bind () для связывания сокета с адресом сервера. В этом случае это адрес localhost , относящийся к текущему серверу, а номер порта – 10000.

socket_echo_server.py

import socket
import sys

# Create a TCP/IP socket
sock  socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to the port
server_address  ('localhost', 10000)
print('starting up on {} port {}'.format(*server_address))
sock.bind(server_address)

# Listen for incoming connections
sock.listen(1)

while True:
    # Wait for a connection
    print('waiting for a connection')
    connection, client_address  sock.accept()
    try:
        print('connection from', client_address)

        # Receive the data in small chunks and retransmit it
        while True:
            data  connection.recv(16)
            print('received {!r}'.format(data))
            if data:
                print('sending data back to the client')
                connection.sendall(data)
            else:
                print('no data from', client_address)
                break

    finally:
        # Clean up the connection
        connection.close()

Вызов listen () переводит сокет в режим сервера, а accept () ожидает входящего соединения. Целочисленный аргумент – это количество подключений, которые система должна поставить в очередь в фоновом режиме, прежде чем отклонять новых клиентов. В этом примере предполагается, что одновременно будет работать только одно соединение.

accept () возвращает открытое соединение между сервером и клиентом вместе с адресом клиента. На самом деле соединение представляет собой другой сокет на другом порту (назначенный ядром). Данные считываются из соединения с помощью recv () и передаются с помощью sendall () .

Когда связь с клиентом завершена, необходимо очистить соединение с помощью close () . В этом примере используется блок try: finally , чтобы гарантировать, что close () всегда вызывается, даже в случае ошибки.

Эхо-клиент

Клиентская программа настраивает свой сокет иначе, чем сервер. Вместо привязки к порту и прослушивания он использует connect () для присоединения сокета непосредственно к удаленному адресу.

socket_echo_client.py

import socket
import sys

# Create a TCP/IP socket
sock  socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Connect the socket to the port where the server is listening
server_address  ('localhost', 10000)
print('connecting to {} port {}'.format(*server_address))
sock.connect(server_address)

try:

    # Send data
    message  b'This is the message.  It will be repeated.'
    print('sending {!r}'.format(message))
    sock.sendall(message)

    # Look for the response
    amount_received  0
    amount_expected  len(message)

    while amount_received < amount_expected:
        data  sock.recv(16)
        amount_received  len(data)
        print('received {!r}'.format(data))

finally:
    print('closing socket')
    sock.close()

После установления соединения данные могут быть отправлены через socket с помощью sendall () и получены с помощью recv () , как и на сервере. Когда все сообщение отправлено и получена копия, сокет закрывается, чтобы освободить порт.

Клиент и сервер вместе

Клиент и сервер должны запускаться в отдельных окнах терминала, чтобы они могли взаимодействовать друг с другом. Выходные данные сервера показывают входящее соединение и данные, а также ответ, отправленный обратно клиенту.

$ python3 socket_echo_server.py
starting up on localhost port 10000
waiting for a connection
connection from ('127.0.0.1', 65141)
received b'This is the mess'
sending data back to the client
received b'age.  It will be'
sending data back to the client
received b' repeated.'
sending data back to the client
received b''
no data from ('127.0.0.1', 65141)
waiting for a connection

Выходные данные клиента показывают исходящее сообщение и ответ сервера.

$ python3 socket_echo_client.py
connecting to localhost port 10000
sending b'This is the message.  It will be repeated.'
received b'This is the mess'
received b'age.  It will be'
received b' repeated.'
closing socket

Простые клиентские подключения

Клиенты TCP/IP могут сэкономить несколько шагов, используя удобную функцию create_connection () для подключения к серверу. Функция принимает один аргумент, двухзначный кортеж, содержащий адрес сервера, и получает лучший адрес для использования для соединения.

socket_echo_client_easy.py

import socket
import sys


def get_constants(prefix):
    """Create a dictionary mapping socket module
    constants to their names.
    """
    return {
        getattr(socket, n): n
        for n in dir(socket)
        if n.startswith(prefix)
    }


families  get_constants('AF_')
types  get_constants('SOCK_')
protocols  get_constants('IPPROTO_')

# Create a TCP/IP socket
sock  socket.create_connection(('localhost', 10000))

print('Family  :', families[sock.family])
print('Type    :', types[sock.type])
print('Protocol:', protocols[sock.proto])
print()

try:

    # Send data
    message  b'This is the message.  It will be repeated.'
    print('sending {!r}'.format(message))
    sock.sendall(message)

    amount_received  0
    amount_expected  len(message)

    while amount_received < amount_expected:
        data  sock.recv(16)
        amount_received  len(data)
        print('received {!r}'.format(data))

finally:
    print('closing socket')
    sock.close()

create_connection () использует getaddrinfo () для поиска параметров соединения-кандидата и возвращает socket , открытый с первой конфигурацией, которая создает успешное соединение. Атрибуты family , type и proto можно проверить, чтобы определить тип возвращаемого socket .

$ python3 socket_echo_client_easy.py
Family  : AF_INET
Type    : SOCK_STREAM
Protocol: IPPROTO_TCP

sending b'This is the message.  It will be repeated.'
received b'This is the mess'
received b'age.  It will be'
received b' repeated.'
closing socket

Выбор адреса для прослушивания

Важно привязать сервер к правильному адресу, чтобы клиенты могли общаться с ним. Во всех предыдущих примерах в качестве IP-адреса использовался 'localhost' , что ограничивает подключения клиентов, работающих на одном сервере. Используйте общедоступный адрес сервера, например значение, возвращаемое функцией gethostname () , чтобы разрешить другим хостам подключаться. Этот пример изменяет эхо-сервер для прослушивания адреса, указанного в аргументе командной строки.

socket_echo_server_explicit.py

import socket
import sys

# Create a TCP/IP socket
sock  socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to the address given on the command line
server_name  sys.argv[1]
server_address  (server_name, 10000)
print('starting up on {} port {}'.format(*server_address))
sock.bind(server_address)
sock.listen(1)

while True:
    print('waiting for a connection')
    connection, client_address  sock.accept()
    try:
        print('client connected:', client_address)
        while True:
            data  connection.recv(16)
            print('received {!r}'.format(data))
            if data:
                connection.sendall(data)
            else:
                break
    finally:
        connection.close()

Аналогичная модификация клиентской программы необходима до тестирования сервера.

socket_echo_client_explicit.py

import socket
import sys

# Create a TCP/IP socket
sock  socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Connect the socket to the port on the server
# given by the caller
server_address  (sys.argv[1], 10000)
print('connecting to {} port {}'.format(*server_address))
sock.connect(server_address)

try:

    message  b'This is the message.  It will be repeated.'
    print('sending {!r}'.format(message))
    sock.sendall(message)

    amount_received  0
    amount_expected  len(message)
    while amount_received < amount_expected:
        data  sock.recv(16)
        amount_received  len(data)
        print('received {!r}'.format(data))

finally:
    sock.close()

После запуска сервера с аргументом hubert команда netstat показывает, что сервер прослушивает адрес указанного хоста.

$ host hubert.hellfly.net

hubert.hellfly.net has address 10.9.0.6

$ netstat -an | grep 10000

Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
...
tcp4       0      0  10.9.0.6.10000         *.*                    LISTEN
...

Запуск клиента на другом хосте с передачей hubert.hellfly.net в качестве хоста, на котором работает сервер, дает:

$ hostname

apu

$ python3 ./socket_echo_client_explicit.py hubert.hellfly.net
connecting to hubert.hellfly.net port 10000
sending b'This is the message.  It will be repeated.'
received b'This is the mess'
received b'age.  It will be'
received b' repeated.'

И вывод сервера:

$ python3 socket_echo_server_explicit.py hubert.hellfly.net
starting up on hubert.hellfly.net port 10000
waiting for a connection
client connected: ('10.9.0.10', 33139)
received b''
waiting for a connection
client connected: ('10.9.0.10', 33140)
received b'This is the mess'
received b'age.  It will be'
received b' repeated.'
received b''
waiting for a connection

Многие серверы имеют более одного сетевого интерфейса и, следовательно, более одного IP-адреса. Вместо того, чтобы запускать отдельные копии службы, привязанной к каждому IP-адресу, используйте специальный адрес INADDR_ANY для одновременного прослушивания всех адресов. Хотя socket определяет константу для INADDR_ANY , это целочисленное значение, и его необходимо преобразовать в адрес строки с точечной нотацией, прежде чем его можно будет передать в bind () . В качестве ярлыка используйте « 0.0.0.0 » или пустую строку ( '' ) вместо выполнения преобразования.

socket_echo_server_any.py

import socket
import sys

# Create a TCP/IP socket
sock  socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to the address given on the command line
server_address  ('', 10000)
sock.bind(server_address)
print('starting up on {} port {}'.format(*sock.getsockname()))
sock.listen(1)

while True:
    print('waiting for a connection')
    connection, client_address  sock.accept()
    try:
        print('client connected:', client_address)
        while True:
            data  connection.recv(16)
            print('received {!r}'.format(data))
            if data:
                connection.sendall(data)
            else:
                break
    finally:
        connection.close()

Чтобы увидеть фактический адрес, используемый сокетом, вызовите его метод getsockname () . После запуска службы запуск netstat снова показывает, что она прослушивает входящие соединения по любому адресу.

$ netstat -an

Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address    Foreign Address  (state)
...
tcp4       0      0  *.10000          *.*              LISTEN
...