Клиент <-> Сервер (Python)

Суббота, Февраль 5, 2011 г.
Приветствую всех кто читает эти строки ;) давно я ничего нового не писал. Возможно кто то подумал, что блог заброшен, но этот вовсе не так. Я здесь, продолжаю писать, пусть и не так часто как другие. Начну с небольшого лирического отступления. Новый год начался с приятной неожиданности — апдейта google pr. Приятной потому что принёс моему блогу единичку. А неожиданности — я не проводил никаких дополнительных работ по раскрутке или оптимизации, просто писал. Хороший стимул продолжать дальше :).

Сегодня я хочу коснуться темы взаимодействия двух разных, а зачастую и значительно удалённых друг от друга приложений. Под взаимодействием мы будем понимать передачу каких либо данных. Примером послужит простой echo сервер на python. Задача такого сервера ожидать подключение от клиента, принимать переданные данные и отправлять их обратно.
Вот сервер:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket

host = "localhost"
port = 44444

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))
s.listen(5)
sock, addr = s.accept()
while True:
    buf = sock.recv(1024)
    if buf == "exit":
        sock.send("bye")
        break
    elif buf:
        sock.send(buf)
sock.close()


Один мой знакомый упрекнул меня в том, что я уделяю слишком мало внимания простым деталям, тем самым отпугивая «совсем новичков» кучей необъяснённого кода, который как мне кажется и так должен быть понятен. Частично я с ним согласился. Я постараюсь более подробно описывать ключевые моменты кода, но в тоже время знание основ я оставляю на совести читателя. Согласитесь, глупо браться писать и читать не выучив азбуки.
Вернёмся к серверу. Подключаем модуль для работы с сокетами. Он содержит весь необходимый нам функционал. Далее мы определим хост на котором сервер будет ждать соединение и порт который он будет слушать. 9 строка — создаём сокет для Ipv4. Это далеко не единственный вариант. Для расширения кругозора советую заглянуть сюда. В 10 строке мы устанавливаем опцию повторного использования порта, чтобы не ждать пока он освободится после останова сервера. Далее биндим (ассоциируем) сокет с хостом и портом. Указываем в 12 строке количество ожидающих обработки запросов. Функция accept() переводит приложение в ожидание подключения клиента. При успешном коннекте accept возвратит кортеж (пару) из объекта соединения и адреса клиента. Полученный объект мы и будем использовать для взаимодействия с клиентом. Запускаем вечный цикл, в котором читаем из объекта отправленные данные блоками указанной в 15 строке величины (в данном случае 1024). Проверяем полученные данные, если клиент прислал слово exit, отправляем ему bye и выходим из цикла закрывая соединение. Если же принятые данные не exit, то отправляем их обратно.
А вот и клиент к нему:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket

host = "localhost"
port = 44444

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
while True:
    buf = raw_input(">>")
    s.send(buf)
    result = s.recv(1024)
    print result
    if buf == "exit":
        break
s.close()



В начале всё как и у сервера. Создаём сокет, биндим к адресу и порту сервера. Далее в цикле организуем что то вроде чата с сервером :). В 12 строке читаем введённые с клавиатуры данные. Отправляем их серверу, получаем ответ и выводим на консоль. Далее, если мы отправили серверу команду exit, выходим из клиента или же переходим к следующей итерации цикла, возвращаясь к приёму данных из клавиатуры. Запустите на разных терминалах клиент с сервером и попробуйте поиграться, это забавно :).
Сервер получился самый простой. После закрытия соединения клиентом сервер сам закрывается. Далее попробуем его немного усложнить так, чтобы он мог обрабатывать теоретически неограниченное количество подключений одновременно. Используем для этого многопоточность из модуля threading. Многопоточный сервер:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket
import threading

host = "localhost"
port = 44444

class Connect(threading.Thread):
    def __init__(self, sock, addr):
        self.sock = sock
        self.addr = addr
        threading.Thread.__init__(self)
    def run (self):
        while True:
            buf = self.sock.recv(1024)
            if buf == "exit":
                self.sock.send("bye")
                break
            elif buf:
                self.sock.send(buf)
        self.sock.close()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))
s.listen(5)
while True:
    sock, addr = s.accept()
    Connect(sock, addr).start()



Что нового?..
Мы подключили модуль threading. В 10 строке создаём класс Connect потомок threading.Thread в котором описываем взаимодействие с клиентом переопределив родительский метод run(). Именно этот метод создаёт отдельный поток и выполняет в нём своё содержимое. Далее после создания и бинда сокета запускаем цикл в котором ожидаем подключение. При соединении запускаем обработку отдельным потоком, то есть переходим к ожиданию следующего подключения не зависимо от состояния предыдущего.
Вот собственно и всё. Как это можно использовать? Да как угодно :) написать например собственный чат, или файловый сервер. Через сокеты между клиентом и сервером можно передавать практически любую информацию, как текст так и байтовый поток. Воспользовавшись модулем pickle можно консервировать и отправлять любые структуры данных: объекты, списки, и т. д.
Богатое поле для деятельности ;).
Теги: Python
 
   
Комментарии (10)
Andrey popp
06.02.2011 в 22:57
На socket.send надо всегда проверять сколько байт было послано, тоже самое для socket.recv.

Удачи!
demoriz
07.02.2011 в 15:55
А зачем?
Andrey popp
07.02.2011 в 17:35
Потому что при send может быть послано не всё. А при recv аргументом передаётся размер буффера, это значит, что будет принято количество байт не больше этого значения, но не конкретное количество. Вообщем-то, вместо send можно использовать sendall, тогда проверок при отправке данных можно избежать. И да - всё это применимо не только к python, можно посмотреть документацию к libc, хотя думаю и на http://docs.python.org/library/socket.html это сказано.
demoriz
08.06.2011 в 15:12
Спасибо, учту.
Присоединяюсь к Andrey popp - всегда нужно проверять, сколько данных реально отправлено и реально принято. Ну или писать не на чистых сокетах, а использовать модуль, в котором все проверки уже реализованы.

Еще советую провентилировать вопрос "А не придет ли процессу SIGPIPE, если клиент не вовремя закроет сокет?". В C/C++ и Perl SIGPIPE 100% приходит. И по умолчанию этот сигнал убивает приложение, так что нужен свой обработчик. Как в Пайтоне - не знаю.

Еще в сях всегда остро стоият вопросы "а кто вызовет деструктор этого объекта, а будет ли освобождена память там-то", но в вашем случае, похоже, этой проблемы нет. На досуге советую подумать над корректным завершении программы (с ожиданием завершения каждого потока и закрытием каждого сокета) в случае команды администратора остановить сервер. Такой командой может быть создание какого-нибудь файла.
Kein
30.06.2011 в 14:28
Ух ты, спасибо за нормальную статью. А то все какое-то не очень попадается, а я только Python учить начал)
Начинающий
05.07.2011 в 20:58
Спасибо за статью, все подробно описано! Пишу мессенджер, чтобы изучить питон, параллельно читаю мануалы) Как раз упёрся в проблему многопоточности сервера. Еще раз спасибо!
HasK
01.10.2011 в 11:50
Разбирался с тем, что написано тут, но всё же получалось не очень удачно при разрыве соединения клиентом. Затем нашёл лучшее решение - asyncore - стандартный модуль питона. Подробнее в документации: http://docs.python.org/library/asyncore.html
greg
17.02.2012 в 8:15
проходил мимо и заметил:
if buf == "exit":
    sock.send("bye")
        break
    elif buf:
        self.sock.send(buf)


может все таки
self.sock.send("bye")
demoriz
21.02.2012 в 10:54
Да действительно, нелепая очепятка :)
Спасибо.

PS. И ведь до сих пор никто так и не заметил :)))
Оставить комментарий   Нажмите, чтобы отменить ответ.
Доступен html впределах разумного. Для цитирования используйте <blockquote></blockquote>, для отрисовки программного кода [code][/code].
Для всяких хакеров и прочих: комментарии проходят санитизацию, всё лишнее будет вырезано. Так что не тратьте своё драгоценное время.
Имя (обязательно)
E-Mail (Не будет опубликован , обязательно)
Сайт (необязательно)