Сокеты Python: мощный инструмент для создания эффективных сетевых приложений

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Программисты и разработчики, интересующиеся сетевым программированием на Python
  • Студенты и начинающие разработчики, желающие углубить свои знания в работе с сокетами
  • Профессионалы в области ИТ, ищущие решения для оптимизации и создания сетевых приложений

    Сокеты в Python — это мощный инструмент для создания сетевых приложений, от простых чат-ботов до сложных распределенных систем. Когда мой клиент попросил разработать систему мониторинга для десятков серверов с минимальными накладными расходами, я сразу понял: без низкоуровневого сокет-программирования не обойтись. Готовые фреймворки добавляли ненужную сложность и потребляли лишние ресурсы. Погрузившись в документацию модуля socket, я создал легковесное решение, которое работает безотказно уже третий год. В этом руководстве я поделюсь техниками, которые помогут вам овладеть сокетами в Python и решить задачи любой сложности. 🐍🔌

Хотите быстро освоить работу с сокетами и другими инструментами сетевого взаимодействия? Курс Обучение Python-разработке от Skypro погружает в практические аспекты веб-программирования, включая низкоуровневую работу с сетевыми протоколами. Вы научитесь создавать эффективные сетевые приложения под руководством опытных разработчиков. Реальные проекты в портфолио уже через 6 месяцев обучения!

Основы сетевых сокетов в Python: концепция и протоколы

Сокеты представляют собой программные абстракции, обеспечивающие конечные точки для коммуникации между процессами. Они выступают фундаментом для всех сетевых взаимодействий — от HTTP-запросов до потоковой передачи видео. Python предоставляет удобный интерфейс для работы с сокетами через встроенный модуль socket.

Прежде чем погрузиться в код, важно понять основные типы сокетов, доступные в Python:

Тип сокета Характеристики Применение
TCP (SOCK_STREAM) Потоковый, надежный, упорядоченный Веб-сервисы, передача файлов, критичные данные
UDP (SOCK_DGRAM) Датаграммный, без гарантии доставки Потоковое видео, игры, DNS-запросы
UNIX (AF_UNIX) Локальное межпроцессное взаимодействие Коммуникация между процессами на одной машине
RAW (SOCK_RAW) Низкоуровневый доступ к протоколам Сетевая диагностика, специализированные протоколы

Основной принцип работы с сокетами заключается в следующем процессе:

  1. Создание сокета определенного типа
  2. Привязка сокета к адресу (для серверной части)
  3. Прослушивание и принятие соединений (сервер) или установление соединения (клиент)
  4. Обмен данными
  5. Закрытие соединения

Каждый сокет в Python характеризуется семейством адресов (AFINET для IPv4, AFINET6 для IPv6) и типом (SOCKSTREAM для TCP, SOCKDGRAM для UDP). Важно понимать, что Python абстрагирует низкоуровневые детали, предоставляя удобный API, но знание фундаментальных принципов критично для эффективной разработки.

Чтобы импортировать модуль сокетов и создать базовый сокет:

Python
Скопировать код
import socket

# Создание TCP сокета для IPv4
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Создание UDP сокета для IPv4
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

Выбор протокола зависит от требований вашего приложения. TCP обеспечивает надежность передачи данных, но с большей задержкой и накладными расходами. UDP быстрее и эффективнее, но не гарантирует доставку или порядок пакетов. 📊

Пошаговый план для смены профессии

Создание и настройка TCP-сокетов на сервере и клиенте

TCP-сокеты формируют основу большинства интернет-приложений благодаря гарантированной доставке данных. Рассмотрим процесс создания TCP-сервера и клиента с пошаговыми примерами.

Антон Величко, ведущий Python-разработчик

В 2021 году мне поручили оптимизировать систему мониторинга в нашем дата-центре. Использование высокоуровневых решений вроде Django или Flask создавало лишнюю нагрузку на серверы. Я решил вернуться к основам и реализовать легковесный сервер на чистых сокетах.

Первый прототип выглядел примерно так же, как серверный код из этого руководства. Самым сложным было правильно организовать многопоточную обработку соединений — сервер должен был одновременно принимать данные с сотен агентов. Я потратил неделю на отладку race conditions и утечек ресурсов, пока не пришел к надежной архитектуре с пулом потоков.

Результат превзошел ожидания: система стала потреблять на 70% меньше ресурсов при той же функциональности. Директор по ИТ был настолько доволен, что выделил дополнительный бюджет на развитие моего решения. Это еще раз доказало мне: иногда стоит отказаться от громоздких фреймворков в пользу фундаментальных инструментов языка.

Создание TCP-сервера включает несколько ключевых шагов:

Python
Скопировать код
import socket

# Создаем TCP/IP сокет
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Привязываем сокет к адресу и порту
server_address = ('localhost', 8000)
server_socket.bind(server_address)

# Начинаем прослушивать соединения (очередь до 5 подключений)
server_socket.listen(5)
print(f"Сервер запущен на {server_address[0]}:{server_address[1]}")

while True:
# Ожидаем соединения
client_socket, client_address = server_socket.accept()
print(f"Подключение с {client_address}")

try:
# Получаем данные от клиента
data = client_socket.recv(1024)
print(f"Получено: {data.decode('utf-8')}")

# Отправляем ответ
client_socket.sendall("Сообщение получено".encode('utf-8'))
finally:
# Закрываем соединение
client_socket.close()

Клиентская часть TCP-соединения реализуется следующим образом:

Python
Скопировать код
import socket

# Создаем сокет
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Подключаемся к серверу
server_address = ('localhost', 8000)
client_socket.connect(server_address)

try:
# Отправляем данные
message = "Привет, сервер!"
client_socket.sendall(message.encode('utf-8'))

# Получаем ответ
data = client_socket.recv(1024)
print(f"Получено: {data.decode('utf-8')}")

finally:
# Закрываем соединение
client_socket.close()

При разработке TCP-приложений важно учитывать следующие аспекты:

  • Таймауты соединений — используйте socket.settimeout(seconds) для предотвращения бесконечных блокировок
  • Буферизация — метод recv() может вернуть меньше данных, чем запрошено
  • Завершение соединения — всегда закрывайте сокеты с помощью close() или используйте контекстный менеджер
  • Адресация — используйте IP-адрес '0.0.0.0' для прослушивания на всех интерфейсах сервера

Опция SO_REUSEADDR позволяет избежать блокировки порта после перезапуска сервера:

Python
Скопировать код
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

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

Реализация UDP-сокетов для быстрой передачи данных

UDP (User Datagram Protocol) предоставляет механизм для отправки сообщений с минимальными накладными расходами на установление соединения. Этот протокол идеален для приложений, где скорость важнее надежности: онлайн-игры, потоковое аудио/видео и системы мониторинга в реальном времени.

В отличие от TCP, UDP-сокеты не поддерживают концепцию "соединения". Вместо этого они просто отправляют и принимают пакеты (датаграммы), не гарантируя их доставку или порядок получения.

Рассмотрим реализацию UDP-сервера:

Python
Скопировать код
import socket

# Создаем UDP сокет
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Привязываем к адресу и порту
server_address = ('localhost', 9000)
server_socket.bind(server_address)

print(f"UDP сервер запущен на {server_address[0]}:{server_address[1]}")

while True:
# Получаем данные и адрес отправителя
data, client_address = server_socket.recvfrom(1024)
print(f"Получено {len(data)} байт от {client_address}")
print(f"Данные: {data.decode('utf-8')}")

# Отправляем ответ
response = f"Получено {len(data)} байт"
server_socket.sendto(response.encode('utf-8'), client_address)

А вот пример UDP-клиента:

Python
Скопировать код
import socket

# Создаем UDP сокет
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Устанавливаем таймаут
client_socket.settimeout(1.0)

# Адрес сервера
server_address = ('localhost', 9000)
message = "Это тестовое UDP сообщение"

try:
# Отправляем данные
bytes_sent = client_socket.sendto(message.encode('utf-8'), server_address)
print(f"Отправлено {bytes_sent} байт")

# Получаем ответ
data, server = client_socket.recvfrom(1024)
print(f"Получено от сервера: {data.decode('utf-8')}")

except socket.timeout:
print("Тайм-аут при ожидании ответа")

finally:
client_socket.close()

Сравним производительность и особенности TCP и UDP для различных сценариев использования:

Характеристика TCP UDP
Установка соединения Требует трехстороннего рукопожатия Не требуется
Гарантия доставки Гарантирована (с повторными попытками) Не гарантирована
Порядок пакетов Сохраняется Не гарантируется
Проверка ошибок Полная, с повторной отправкой Базовая, без повторной отправки
Накладные расходы Высокие Низкие
Идеально для Веб, передача файлов, базы данных Потоковое медиа, игры, IoT-устройства

При работе с UDP важно учитывать следующие особенности:

  • Максимальный размер UDP-датаграммы теоретически составляет 65,507 байт, но на практике следует ограничивать размер пакетов до 1500 байт, чтобы избежать фрагментации
  • Отсутствие встроенных механизмов повторной отправки означает, что вам может потребоваться реализовать собственные протоколы подтверждения для критичных данных
  • UDP-сокеты могут получать датаграммы от любого отправителя, поэтому всегда проверяйте источник данных
  • Для обеспечения надежности в UDP-приложениях можно реализовать простые механизмы подтверждения и повторной отправки на уровне приложения

UDP идеально подходит для сценариев, где приоритетна минимальная задержка, а потеря отдельных пакетов не критична. 🚀

Обработка ошибок и многопоточность в сокет-программировании

Надежное сетевое приложение должно корректно обрабатывать разнообразные исключения и эффективно управлять множественными соединениями. Проблемы с сетью, разрывы соединений и непредсказуемое поведение клиентов могут вызывать ошибки, которые без должной обработки приведут к аварийному завершению программы.

Елена Соколова, системный архитектор

Работая над системой обмена сообщениями для крупного финансового учреждения, я столкнулась с критической проблемой: под нагрузкой наш TCP-сервер падал после нескольких часов работы. Расследование показало, что мы не обрабатывали должным образом ситуации, когда клиент неожиданно обрывал соединение.

Сначала я реализовала простой однопоточный сервер, который блокировался на каждом соединении. При тестировании с десятком клиентов всё работало прекрасно. Но в производственной среде с сотнями одновременных подключений сервер не справлялся — клиенты ждали своей очереди, таймауты истекали, а необработанные исключения накапливались.

Переход на многопоточную архитектуру с тщательной обработкой исключений кардинально изменил ситуацию. Мы внедрили пул потоков с ограниченным размером, добавили таймауты для всех сетевых операций и корректно обрабатывали все возможные ошибки. Наше решение выдержало тест с симуляцией 10,000 одновременных подключений и теперь безотказно работает в течение месяцев без перезапуска.

Этот опыт научил меня: в сетевом программировании нельзя пренебрегать обработкой краевых случаев и масштабированием. То, что работает на вашей машине, может мгновенно развалиться под реальной нагрузкой.

Основные типы исключений, возникающих при работе с сокетами в Python:

  • socket.error — базовый класс ошибок для всех связанных с сокетами исключений
  • socket.timeout — возникает, когда операция с сокетом превышает установленный таймаут
  • ConnectionRefusedError — сервер отклонил соединение
  • ConnectionResetError — соединение было неожиданно закрыто удаленной стороной
  • BrokenPipeError — попытка записи в закрытое соединение
  • OSError — общие ошибки операционной системы, связанные с I/O

Пример обработки основных исключений в клиентском коде:

Python
Скопировать код
import socket
import time

def connect_with_retry(host, port, max_attempts=3):
for attempt in range(max_attempts):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5) # 5 секунд таймаут
sock.connect((host, port))
return sock
except ConnectionRefusedError:
print(f"Попытка {attempt+1}/{max_attempts}: Соединение отказано")
except socket.timeout:
print(f"Попытка {attempt+1}/{max_attempts}: Тайм-аут соединения")
except OSError as e:
print(f"Попытка {attempt+1}/{max_attempts}: Ошибка {e}")

if attempt < max_attempts – 1:
time.sleep(2) # Пауза перед повторной попыткой

raise ConnectionError("Не удалось установить соединение после нескольких попыток")

try:
client_socket = connect_with_retry('localhost', 8000)
# Работа с сокетом...
client_socket.close()
except ConnectionError as e:
print(f"Критическая ошибка: {e}")

Для обработки нескольких клиентов одновременно существует несколько подходов:

  1. Многопоточность — создание отдельного потока для каждого клиента
  2. Мультипроцессорная обработка — использование отдельных процессов
  3. Асинхронный ввод-вывод — неблокирующие операции с селекторами (select, poll)
  4. Высокоуровневые асинхронные библиотеки — asyncio, Twisted, Tornado

Рассмотрим пример многопоточного TCP-сервера с обработкой исключений:

Python
Скопировать код
import socket
import threading

def handle_client(client_socket, address):
print(f"Новое соединение от {address}")
try:
while True:
try:
data = client_socket.recv(1024)
if not data:
print(f"Клиент {address} закрыл соединение")
break

print(f"Получено от {address}: {data.decode('utf-8')}")
client_socket.sendall("Получено".encode('utf-8'))

except ConnectionResetError:
print(f"Соединение с {address} было сброшено")
break
except socket.timeout:
print(f"Тайм-аут при чтении от {address}")
continue

except Exception as e:
print(f"Ошибка при обработке клиента {address}: {e}")
finally:
client_socket.close()
print(f"Соединение с {address} закрыто")

def start_server(host, port):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

try:
server_socket.bind((host, port))
server_socket.listen(5)
print(f"Сервер запущен на {host}:{port}")

while True:
try:
client_socket, address = server_socket.accept()
client_socket.settimeout(60) # 60 секунд таймаут на операции с клиентом

# Создаем новый поток для обработки клиента
client_thread = threading.Thread(
target=handle_client,
args=(client_socket, address)
)
client_thread.daemon = True # Фоновый поток
client_thread.start()

except Exception as e:
print(f"Ошибка при принятии соединения: {e}")

except KeyboardInterrupt:
print("Сервер останавливается...")
finally:
server_socket.close()

if __name__ == "__main__":
start_server('localhost', 8000)

Для более эффективного управления ресурсами в многопоточных серверах рекомендуется использовать пул потоков:

Python
Скопировать код
from concurrent.futures import ThreadPoolExecutor

# В функции start_server
with ThreadPoolExecutor(max_workers=20) as executor:
while True:
client_socket, address = server_socket.accept()
client_socket.settimeout(60)
executor.submit(handle_client, client_socket, address)

При работе с множеством соединений также важно:

  • Устанавливать разумные таймауты для всех сетевых операций
  • Ограничивать максимальное количество одновременных подключений
  • Использовать неблокирующие I/O для высоконагруженных серверов
  • Применять механизм отложенного повторного подключения с экспоненциальной задержкой
  • Добавлять мониторинг и логирование активных соединений

Правильная обработка ошибок и эффективное управление соединениями — краеугольные камни надежного сетевого приложения. 🛡️

Практические проекты: от чата до файлового обмена

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

1. Простой чат-сервер

Минималистичный многопользовательский чат на TCP-сокетах:

Python
Скопировать код
import socket
import threading
import select

class ChatServer:
def __init__(self, host='localhost', port=9090):
self.host = host
self.port = port
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(5)

self.clients = []
self.nicknames = {}

print(f"Чат-сервер запущен на {self.host}:{self.port}")

def broadcast(self, message, sender_socket=None):
for client in self.clients:
if client != sender_socket:
try:
client.send(message)
except:
# Удаляем клиента при проблеме с отправкой
self.clients.remove(client)

def handle_client(self, client_socket, address):
try:
# Запрашиваем ник пользователя
client_socket.send("NICK?".encode('utf-8'))
nickname = client_socket.recv(1024).decode('utf-8')
self.nicknames[client_socket] = nickname

# Уведомляем о новом пользователе
self.clients.append(client_socket)
self.broadcast(f"{nickname} присоединился к чату!".encode('utf-8'))

while True:
try:
message = client_socket.recv(1024)
if message:
self.broadcast(f"{nickname}: {message.decode('utf-8')}".encode('utf-8'), client_socket)
else:
# Пустое сообщение означает, что клиент отключился
raise Exception("Клиент отключился")
except:
# Удаляем и оповещаем об отключении клиента
self.clients.remove(client_socket)
nickname = self.nicknames.pop(client_socket, "Неизвестный")
self.broadcast(f"{nickname} покинул чат!".encode('utf-8'))
client_socket.close()
break
except Exception as e:
print(f"Ошибка: {e}")

def run(self):
try:
while True:
client_socket, address = self.server_socket.accept()
thread = threading.Thread(
target=self.handle_client,
args=(client_socket, address)
)
thread.daemon = True
thread.start()
except KeyboardInterrupt:
print("Сервер остановлен")
finally:
self.server_socket.close()

if __name__ == "__main__":
server = ChatServer()
server.run()

Соответствующий клиентский код:

Python
Скопировать код
import socket
import threading
import tkinter as tk
from tkinter import scrolledtext, Entry, Button

class ChatClient:
def __init__(self, host='localhost', port=9090):
self.host = host
self.port = port
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.nickname = ""

# Создаем GUI
self.root = tk.Tk()
self.root.title("Python Чат")
self.root.geometry("400x500")

# Область чата
self.chat_area = scrolledtext.ScrolledText(self.root, wrap=tk.WORD, state='disabled')
self.chat_area.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)

# Ввод сообщения
self.msg_frame = tk.Frame(self.root)
self.msg_frame.pack(padx=10, pady=5, fill=tk.X)

self.msg_entry = Entry(self.msg_frame)
self.msg_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)

self.send_button = Button(self.msg_frame, text="Отправить", command=self.send_message)
self.send_button.pack(side=tk.RIGHT)

self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

def connect(self):
try:
self.sock.connect((self.host, self.port))

# Запрос и отправка никнейма
server_request = self.sock.recv(1024).decode('utf-8')
if server_request == "NICK?":
self.nickname = input("Введите ваш ник: ")
self.sock.send(self.nickname.encode('utf-8'))

# Запускаем поток для приема сообщений
receive_thread = threading.Thread(target=self.receive_messages)
receive_thread.daemon = True
receive_thread.start()

self.root.mainloop()

except Exception as e:
print(f"Ошибка подключения: {e}")
self.root.destroy()

def receive_messages(self):
while True:
try:
message = self.sock.recv(1024).decode('utf-8')
self.update_chat(message)
except:
print("Ошибка при получении сообщения")
self.sock.close()
break

def update_chat(self, message):
self.chat_area.config(state='normal')
self.chat_area.insert(tk.END, message + "\n")
self.chat_area.config(state='disabled')
self.chat_area.yview(tk.END)

def send_message(self):
message = self.msg_entry.get()
if message:
try:
self.sock.send(message.encode('utf-8'))
self.msg_entry.delete(0, tk.END)
except:
self.update_chat("Сообщение не отправлено")

def on_closing(self):
try:
self.sock.close()
except:
pass
self.root.destroy()

if __name__ == "__main__":
client = ChatClient()
client.connect()

2. Простой файловый сервер

Этот проект демонстрирует, как использовать сокеты для передачи файлов:

Python
Скопировать код
# Серверная часть
import socket
import os
import threading

class FileServer:
def __init__(self, host='0.0.0.0', port=9999, file_dir='./shared_files'):
self.host = host
self.port = port
self.file_dir = file_dir
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# Создаем директорию, если не существует
if not os.path.exists(file_dir):
os.makedirs(file_dir)

def handle_client(self, client_socket, address):
try:
# Получаем команду: LIST, GET или PUT
command = client_socket.recv(1024).decode('utf-8')

if command.startswith("LIST"):
# Отправляем список файлов
files = os.listdir(self.file_dir)
client_socket.send((",".join(files) or "Нет доступных файлов").encode('utf-8'))

elif command.startswith("GET"):
# Отправляем файл клиенту
_, filename = command.split("|")
file_path = os.path.join(self.file_dir, filename)

if os.path.exists(file_path):
# Отправляем размер файла
file_size = os.path.getsize(file_path)
client_socket.send(f"SIZE|{file_size}".encode('utf-8'))
client_socket.recv(1024) # Подтверждение от клиента

# Отправляем файл
with open(file_path, 'rb') as f:
data = f.read(4096)
while data:
client_socket.send(data)
data = f.read(4096)
else:
client_socket.send("ERROR|Файл не найден".encode('utf-8'))

elif command.startswith("PUT"):
# Получаем файл от клиента
_, filename, size = command.split("|")
file_size = int(size)

# Подтверждаем готовность к приему
client_socket.send("READY".encode('utf-8'))

# Получаем и сохраняем файл
file_path = os.path.join(self.file_dir, filename)
received = 0

with open(file_path, 'wb') as f:
while received < file_size:
data = client_socket.recv(min(4096, file_size – received))
if not data:
break
f.write(data)
received += len(data)

client_socket.send("SUCCESS".encode('utf-8'))

except Exception as e:
print(f"Ошибка при обработке запроса от {address}: {e}")
finally:
client_socket.close()

def start(self):
try:
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(5)
print(f"Файловый сервер запущен на {self.host}:{self.port}")

while True:
client_socket, address = self.server_socket.accept()
print(f"Подключение с {address}")

client_thread = threading.Thread(
target=self.handle_client,
args=(client_socket, address)
)
client_thread.daemon = True
client_thread.start()

except KeyboardInterrupt:
print("Сервер остановлен")
except Exception as e:
print(f"Ошибка сервера: {e}")
finally:
self.server_socket.close()

if __name__ == "__main__":
server = FileServer()
server.start()

Другие идеи для проектов с использованием сокетов:

  • HTTP-сервер — создание простого веб-сервера, обрабатывающего GET/POST запросы
  • Сетевая игра — многопользовательская игра с синхронизацией состояний
  • Система мониторинга — сбор метрик с удаленных машин и их визуализация
  • Распределенное вычисление — разделение вычислительных задач между несколькими машинами
  • Прокси-сервер — перенаправление и потенциальная модификация сетевого трафика

Возможности сокет-программирования ограничены лишь вашим воображением. Начните с простых проектов и постепенно переходите к более сложным, углубляя понимание сетевых протоколов и архитектурных паттернов. 💻🌐

Сокеты в Python — это фундаментальный строительный блок для любого сетевого приложения. Освоив принципы их работы, вы получаете контроль над сетевым взаимодействием, недоступный при использовании высокоуровневых библиотек. Ключевое преимущество низкоуровневого программирования — возможность точной настройки под ваши потребности: от оптимизации производительности до реализации собственных протоколов. При разработке сетевых приложений всегда балансируйте между производительностью, надежностью и простотой поддержки. Помните: хорошо спроектированная система с правильной обработкой ошибок будет работать годами без сбоев, а плохо спроектированная развалится под первой серьезной нагрузкой.

Загрузка...