Сокеты в Python 3: TCP, клиент, сервер

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

«Со́кеты (англ. socket — разъём) — название программного интерфейса для обеспечения обмена данными между процессами. Процессы при таком обмене могут исполняться как на одной ЭВМ, так и на различных ЭВМ, связанных между собой сетью. Сокет — абстрактный объект, представляющий конечную точку соединения.» © Википедия

Суть работы: на одном компьютере программа открывает сокет, слушает какой-то порт (в случае с TCP и UDP), другая программа на другом (или том же) компьютере, указав IP и этот самый порт, подключается к слушающей порт программе, и дальше они обмениваются какими надо данными, после чего закрывают соединение.

Для работы с сокетами нам нужно импортировать соответствующий модуль.
import socket
Теперь нужно создать сам сокет.
sock = socket.socket()

Теперь у нас есть сокет в переменной sock, и мы можем работать с ним дальше.

Сервер TCP


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

Данные в TCP — это поток байтов. Разделять его на отдельные сообщения придётся самой программе.

Сокет для TCP-соединения создаётся как обычно. Для создания сервера нужно связать сокет с одним или всеми из имеющихся у компьютера хостов (IP-адресов) и каким-либо свободным портом. Если не указать хост или указать "0.0.0.0", сокет будет прослушивать все хосты. Если указать "127.0.0.1", то подключиться можно будет только с этого же компьютера.

Для привязки используется функция bind сокета, которая принимает массив, содержащий два элемента: хост и порт.
sock.bind( ("", 14900) )

Теперь можно заняться прослушкой, это можно сделать с помощью функции listen. Она принимает в качестве аргумента максимальное число соединений.
sock.listen(10)

Теперь принимаем соединения с помощью функции accept. Она ждёт появление входящего соединения и возвращает связанный с ним сокет и адрес подключившегося. Адрес — массив, состоящий из IP-адреса и порта.
conn, addr = sock.accept()

В объекте conn теперь у нас сокет, через который мы можем обмениваться данными с клиентом, в addr[0] — IP-адрес подключившегося клиента. Чтобы получить следующего клиента, нужно вызвать функцию accept ещё раз, при этом необязательно закрывать соединение с предыдущим клиентом: можно держать столько подключенных клиентов, сколько было указано в listen.

Для чтения данных используется функция recv, которой первым параметром нужно передать количество получаемых байт данных. Если столько байт, сколько указано, не пришло, а какие-то данные уже появились, она всё равно возвращает всё, что имеется, поэтому надо контролировать размер полученных данных.
data = conn.recv(16384)

Тип возвращаемых данных — bytes. У этого типа есть почти все методы, что и у строк, но для того, чтобы использовать из него текстовые данные с другими строками (складывать, например, или искать строку в данных, или печатать), придётся декодировать данные (или их часть, если вы обработали байты и выделили строку) и использовать уже полученную строку. (Здесь и далее используется кодировка utf-8, если вы вдруг по какой-то глупости используете другую кодировку — указывайте свою.)
udata = data.decode("utf-8")
print("Data: " + udata)

Если вы попытаетесь использовать байты вместо строк, вы получите ошибку:
>>> print("Data: "+data)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't convert 'bytes' object to str implicitly

Для отправки данных в сокет используется функция send. Принимает она тоже bytes, поэтому для отправки строки вам придётся её закодировать.
conn.send(b"Hello!\n")
conn.send(b"Your data: " + udata.encode("utf-8"))


Вот так с помощью функций recv и send и осуществляется весь обмен данными в TCP-соединении.

После всего и клиенту, и серверу необходимо закрыть сокет с помощью функции close.
conn.close()
Теперь этот сокет использовать нельзя.

В случае, если другая сторона сторона закроет сокет, функция recv вернёт пустой объект bytes.

Если данных приходится ждать слишком долго, можно перед использованием функции recv задать (однократно) таймаут с помощью функции settimeout.
conn, addr = sock.accept() # старая строка получения сокета
conn.settimeout(60) # установка таймаута
data = conn.recv(16384) # получение данных, про это рассказано выше

Теперь, если за 60 секунд не придут никакие данные, функция recv вернёт пустой объект bytes, как и при закрытом соединении.
if not data:
print("No data")
conn.close()


Для примера и закрепления всего прочитанного привожу простенький HTTP-сервер, возвращающий текущие дату и время.

Запустите его и наберите в браузере адрес http://localhost:8080/time.html

Показать код
#!/usr/bin/env python3

import time
import socket

def send_answer(conn, status="200 OK", typ="text/plain; charset=utf-8", data=""):
data = data.encode("utf-8")
conn.send(b"HTTP/1.1 " + status.encode("utf-8") + b"\r\n")
conn.send(b"Server: simplehttp\r\n")
conn.send(b"Connection: close\r\n")
conn.send(b"Content-Type: " + typ.encode("utf-8") + b"\r\n")
conn.send(b"Content-Length: " + bytes(len(data)) + b"\r\n")
conn.send(b"\r\n")# после пустой строки в HTTP начинаются данные
conn.send(data)

def parse(conn, addr):# обработка соединения в отдельной функции
data = b""

while not b"\r\n" in data: # ждём первую строку
tmp = conn.recv(1024)
if not tmp: # сокет закрыли, пустой объект
break
else:
data += tmp

if not data: # данные не пришли
return # не обрабатываем

udata = data.decode("utf-8")

# берём только первую строку
udata = udata.split("\r\n", 1)[0]
# разбиваем по пробелам нашу строку
method, address, protocol = udata.split(" ", 2)

if method != "GET" or address != "/time.html":
send_answer(conn, "404 Not Found", data="Не найдено")
return

answer = """<!DOCTYPE html>"""
answer += """<html><head><title>Время</title></head><body><h1>"""
answer += time.strftime("%H:%M:%S %d.%m.%Y")
answer += """</h1></body></html>"""

send_answer(conn, typ="text/html; charset=utf-8", data=answer)


sock = socket.socket()
sock.bind( ("", 8080) )
sock.listen(5)

try:
while 1: # работаем постоянно
conn, addr = sock.accept()
print("New connection from " + addr[0])
try:
parse(conn, addr)
except:
send_answer(conn, "500 Internal Server Error", data="Ошибка")
finally:
# так при любой ошибке
# сокет закроем корректно
conn.close()
finally: sock.close()
# так при возникновении любой ошибки сокет
# всегда закроется корректно и будет всё хорошо


TCP-клиент


HTTP-сервер с браузером это, конечно, хорошо, но вы же тут все хотите онлайн-игры делать ;D Поэтому придётся научиться делать программу клиентом. Сокет создаётся точно так же:
conn = socket.socket()

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

conn.connect( ("127.0.0.1", 14900) )

А дальше всё как обычно: для установки таймаута используется settimeout, для обмена данными send и recv, для закрытия close.
conn.send(b"Hello! \n")
data = b""
tmp = conn.recv(1024)
while tmp:
data += tmp
tmp = conn.recv(1024)
print( data.decode("utf-8") )
conn.close()


Работающий пример

Те, кто собрался делать крутые онлайн-игры в Blender Game Engine, столкнутся с тем, что функции accept и recv ждут соединения и данных, и в результате игра виснет на время ожидания, что плохо. Блокировку с recv поможет снять функция setblocking(0). Тогда в случае отсутствия данных функция не будет ждать, а выкинет исключение socket.error, которое можно будет поймать в блоке try-except, после чего спокойно завершить скрипт, не вешая игру.
# при открытии соединения:
conn = socket.socket()
conn.connect( ("yandex.ru", 80) )
conn.setblocking(0)

# в скрипте, читающем данные:
try: data = conn.recv(1024)
except socket.error: # данных нет
pass # тут ставим код выхода
else: # данные есть
print(data)
# если в блоке except вы выходите,
# ставить else и отступ не нужно
# скрипт, читающий данные, запускаем на каждом кадре


Аналогично с функцией accept.
# при создании сервера:
conn = socket.socket()
conn.bind( ("", 8989) )
conn.listen(100)
conn.setblocking(0)

# в скрипте, который получает клиентов:
try: client, addr = conn.accept()
except socket.error: # данных нет
pass # тут ставим код выхода
else: # данные есть
client.setblocking(0) # снимаем блокировку и тут тоже
parse(client, addr)
# если в блоке except вы выходите,
# ставить else и отступ не нужно
# скрипт, получающий клиентов, запускаем на каждом кадре


Но, конечно, для сервера обрабатывать, например, под сотню клиентов, у каждого просить recv и прочее это издевательство, и тут лучше использовать потоки или какие-нибудь гринлеты с gevent, а полученные данные хранить в очереди (Queue), которая будет читаться основным потоком, или быть гуру и заюзать Twisted, и всё такое прочее мудрёное, но брать на себя ответственность объяснять такую страшную и тяжелую вещь я не хочу, так как сам в этом ещё не силён; лучше вот почитайте хороший цикл статей про многопоточность в питоне.

Для обработки текстовых данных ознакомьтесь с функцями для работы со строками, с помощью них уже можно будет составить простой текстовый протокол и, закодироав координаты кубика с помощью str, соединив через join, отправив закодированное по сокету и разбив присланное на другом компьютере, двигать кубик по сети. (Но лучше, конечно, какой-нибудь pickle использовать, а для серьёзной игры свой бинарный протокол, чтобы трафик впустую не тратился.)
anonymous 28 августа 2013 г. 23:03
Спасибо, повторю сегодня же.

Подскажите есть ли примеры работы с mysql. И если можно также клиент сервер.
andreymal ответил anonymous 28 августа 2013 г. 23:05
Ну сервер однозначно не напишешь (mysql это и есть сервер), а про клиент лучше у гугла спросить
anonymous 31 августа 2013 г. 12:24
сервер на большинстве компов из сети видно не будет!!!!
andreymal ответил anonymous 31 августа 2013 г. 12:25
Будет.
anonymous ответил andreymal 19 сентября 2013 г. 00:32
неа не видно!
andreymal ответил anonymous 19 сентября 2013 г. 00:32
У тебя плохой провайдер или ненастроенный роутер.
anonymous ответил andreymal 19 сентября 2013 г. 01:17
у большинства пользователей 90% будет эта проблема!
andreymal ответил anonymous 19 сентября 2013 г. 09:34
Это однозначно их проблемы, не так ли?
anonymous ответил andreymal 20 сентября 2013 г. 17:02
ну ты даешь, если я делаю игру а она у 90% работать не будет то зачем мне этот урок читать? Тесты проводил и оно так и есть, в смысле из 10 чел. у 6 такая схема не работала!
andreymal ответил anonymous 20 сентября 2013 г. 17:05
Для таких вещей вообще-то заводят нормальные сервера.
andreymal ответил anonymous 20 сентября 2013 г. 17:07
Ах да, так, как тут написано, работают АБСОЛЮТНО ВСЕ интернет-приложения, включая те же апач и тим фортресс 2. Питоновые функции - это лишь обёртка над сишной библиотекой с сокетами Беркли.
anonymous 20 сентября 2013 г. 17:38
Суть остается та же, мы запускаем на одной машине сервер а на другой клиент (обе машины клиентские ) так как за нормальный сервер нужно положить копеечку то желательно както сэкономить ведь игра у нас бесплатная, платные игры делают команды а денег на команду у нас нет! Вот и выходит что этот урок раскрывает не все что нужно раскрыть!!!!
andreymal ответил anonymous 20 сентября 2013 г. 17:40
В уроке описано только то, как открыть сокет и как через него обмениваться данными. Настройка роутера, получение внешнего IP-адреса у провайдера, отключение брандмауэра, узнавание собственного IP-адреса, ну или настройка hamachi/pppoe не входят в рамки того, что описывает урок, и входить никак не может, потому что интернеты у людей слишком разные и каждый надо настраивать по-своему.
anonymous 20 сентября 2013 г. 17:50
Ладно я понял что читать то что уже знаю трата времени, зато теперь те кто не знал как работать с сокетами научатся, но будут знать что без внешнего сервера у них работать не будет и выход здесь либо платить либо прыгать с одного бесплатного хостинга на другой этак раз в месяц!
andreymal ответил anonymous 20 сентября 2013 г. 17:50
Типа того, да.
anonymous 23 октября 2013 г. 21:03
А каким образом можно реализовать отправку нетекстовых файлов?
andreymal ответил anonymous 23 октября 2013 г. 21:07
Текстовые (с технической точки зрения в питоне) данные отправить через сокет вообще никак нельзя. В посте отправляются бинарные данные.
andreymal ответил anonymous 23 октября 2013 г. 21:08
Небуквы отправляются обычным способом:
sock.send(b"\x00\xff\x30\x47\xb8\x00\x00\x00\x0f\x0a")
anonymous 23 октября 2013 г. 21:22
Ну, допустим, как отправить jpg-изображение?
andreymal ответил anonymous 23 октября 2013 г. 21:22
sock.send( open("file.jpg", "rb").read() )
anonymous 23 октября 2013 г. 21:25
Спасибо. А принять как?)
andreymal ответил anonymous 23 октября 2013 г. 21:27
Как вариант:
fp = open("file.jpg", "wb")
while 1:
data = sock.read(4096)
if not data: break
fp.write(data)
fp.close()

Но, разумеется, адекватный нормально работающий вариант уже от конкретного приложения зависит, это лишь пример
anonymous 26 октября 2013 г. 15:21
А как сделать так, чтобы сервер не вырубался при отключении клиента?
andreymal ответил anonymous 26 октября 2013 г. 15:22
смотри пример простенького HTTP-сервера, там цикл с while 1 и sock.accept
anonymous 27 октября 2013 г. 14:48
Ну, допустим, у нас есть серв:

import socket

s = socket.socket()
s.bind(('', 9090))
s.listen(1)
conn, addr = s.accept()
print('Connected by', addr)
while 1:
data = conn.recv(1024)
if not data: break
conn.sendall(data)
conn.close()

Ккк сделать так,чтобы при закрытии клиента сервер не выдавал ошибку, и продолжал работать?
andreymal ответил anonymous 27 октября 2013 г. 15:18
Чё ещё за ошибку?
anonymous 30 декабря 2013 г. 16:17
Ну и дебильные вопросы... (Как можно так тупить?! Человек не понимает как открыть доступ к своему локальному лэптопу:)
А статья хорошая (хоть и написана по-простому).
Автору - спасибо!
anonymous 13 июня 2014 г. 20:59
Не понимаю, в первом примере выдаёт: "SyntaxError: Non-ASCII character '\xd0' in file simple_serv.py on line 13, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
"
Хотя data = data.encode("utf-8") есть.
anonymous 13 июня 2014 г. 21:11
Разобрался! Geany по-умолчанию использовал 2-ой питон, а не 3-ий (проблему решил созданием кастомной команды). Но на этом трудности не закончились - скрипт все равно отказывался работать, только с другой ошибкой (не буду вдаватсья в подробности, но она тоже относилась к кодировке). У самого файла поставил кодировку UTF-8 - без измениний. И только с использованием Unicode Bom всё наконец-таки заработало как положено. Спасибо за туториал!
andreymal ответил anonymous 13 июня 2014 г. 21:22
По-хорошему файл должен начинаться с этих двух строчек без всяких BOM:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
anonymous 1 июня 2015 г. 15:29
А е прымер игры?
andreymal ответил anonymous 1 июня 2015 г. 15:30
Есть не моего авторства где-то в интернетах
Dave 2 июня 2015 г. 22:16
А как управлять обьектами из клиента?
doxxboloxx 8 июля 2015 г. 21:35
Крутая статья, спасибо. Все разжевано
nardist01@mail.ru 29 июля 2015 г. 13:03
можете привести пример где бы k. строка на сервере работала, то есть мы могли бы ввести команду (get_host) , и сервер вернул бы хост аддрес, ну или чтото ещо, ну или тика кик клиента по порту, типа (kick 8214) и юзер с этим портом был бы отключен, мне не удовалось сделать работающий сервер с паботающей k. строкой, или одно или другое, так как ответ (строку) нужно ждать, а сервер постоянно в цикле. если ли решение ? За ранее благодарю
anonymous 6 января 2016 г. 20:23
Спасибо! Очень понятно все разжевано!
anonymous 12 января 2016 г. 19:04
Спасибо, написав простой чат
anonymous 17 января 2016 г. 12:07
Спасибо, Андрей!

-- a1batross
anonymous 19 января 2016 г. 23:12
новичок. как открыть и сервер и клиент? надо на виртуальной машине или можно с одной ос как-то?
andreymal ответил anonymous 19 января 2016 г. 23:14
Просто взять и запустить питоном оба скрипта любым удобным способом. Например, в двух разных окнах терминала (в винде оно вроде называлось cmd/командная строка), как у меня на скриншоте
anonymous 20 января 2016 г. 17:06
а что за Blender Game Engine
anonymous ответил anonymous 20 января 2016 г. 17:07
незнаю
anonymous ответил anonymous 20 января 2016 г. 17:07
я тоже
anonymous 2 февраля 2016 г. 11:29
Сервер буде видний если открить порт
anonymous 28 февраля 2016 г. 23:14
Заранее извините за, наверное глупый вопрос, но все же:
Одна программа на пк1(сервер), на пк2 вторая (клиент), вот я на пк1 ставлю прослушиваться все Хосты ("") и порт например 9090, на пк2 подключаюсь к ПК 1 используя порт 9090 и ip пк1 который выдает яндекс, по запросу (мой ip), возникают ошибки .. Что делаю нитак?
andreymal ответил anonymous 28 февраля 2016 г. 23:15
Очевидно, зависит от того, какие именно ошибки возникают. Тем не менее если вдруг компьютеры находятся в одной локальной сети, то надо брать не тот IP-адрес, который выдаёт яндекс, а тот, который прописан в настройках подключения в локальной сети.
anonymous ответил andreymal 28 февраля 2016 г. 23:26
Большое спасибо, а какой порт можно указать что бы программа работала на любом компьютере? Опять же извиняюсь за наверняка глупый вопрос...
andreymal ответил anonymous 28 февраля 2016 г. 23:27
Порт — любой от 1024 до 65535
anonymous 9 апреля 2016 г. 16:16
#сервер ретлянслятор, может понадобиться
import threading
import socket

conn = socket.socket()
conn.bind(("",25570))
conn.listen(2)
print("Ожидаем... порт 25570")
conn1, addr1 = conn.accept()
print("Подключен 1 клиент IP: " + addr1[0])
conn2, addr2 = conn.accept()
print("Подключен 2 клиент IP: " + addr2[0])

def p1():
global conn1;
while 1:
data1 = conn1.recv(100)
if not data1:
conn1.close()
print("Отключен 1 клиент")
conn1, addr1 = conn.accept()
print("Переодключен 1 клиент IP: " + addr1[0])
else:
conn2.send(data1)
pass

def p2():
global conn2;
while 1:
data2 = conn2.recv(100)
if not data2:
conn2.close()
print("Отключен 2 клиент")
conn2, addr2 = conn.accept()
print("Переподключен 2 клиент IP: " + addr2[0])
else:
conn1.send(data2)
pass


pp1 = threading.Thread(name='pp1', target=p1)
pp2 = threading.Thread(name='pp2', target=p2)
pp1.start()
pp2.start()
anonymous 9 апреля 2016 г. 16:16
#сервер ретлянслятор, может понадобиться
import threading
import socket

conn = socket.socket()
conn.bind(("",25570))
conn.listen(2)
print("Ожидаем... порт 25570")
conn1, addr1 = conn.accept()
print("Подключен 1 клиент IP: " + addr1[0])
conn2, addr2 = conn.accept()
print("Подключен 2 клиент IP: " + addr2[0])

def p1():
global conn1;
while 1:
data1 = conn1.recv(100)
if not data1:
conn1.close()
print("Отключен 1 клиент")
conn1, addr1 = conn.accept()
print("Переодключен 1 клиент IP: " + addr1[0])
else:
conn2.send(data1)
pass

def p2():
global conn2;
while 1:
data2 = conn2.recv(100)
if not data2:
conn2.close()
print("Отключен 2 клиент")
conn2, addr2 = conn.accept()
print("Переподключен 2 клиент IP: " + addr2[0])
else:
conn1.send(data2)
pass


pp1 = threading.Thread(name='pp1', target=p1)
pp2 = threading.Thread(name='pp2', target=p2)
pp1.start()
pp2.start()
anonymous 19 июля 2016 г. 18:31
спасибо за статью
anonymous 20 июля 2016 г. 11:27
Классно, давно искал, пишу игру на python используя pygame библиотеку. Вот: https://vk.com/zendes25 хочу сделать сетевую игру. Только я не разобрался с хостами...
Вообщем вот я в вк: https://vk.com/mbagazov
anonymous 9 августа 2016 г. 20:30
извените! а как при запрсе от клиента выплюнуть ему в ответ файл, не локальый, а из сети (http://example.com/bigfile.zip)
anonymous 19 октября 2016 г. 21:55
Доброе время суток. Попробовал запустить Time сервер. Сделал все как тут описано. В итоге получаю "New connection from: 127.0.0.1" и все... Больше ничего не происходит. Подскажите, в чем может быть проблема?
anonymous 25 октября 2016 г. 16:44
запустил сервер-нормально. Запустил клиента-выдал ошибку. В чем может быть причина?
Traceback (most recent call last):
File "E:\python\tcpClientAndreyMal.py", line 4, in <module>
conn.connect( ("127.0.0.1", 14900) )
ConnectionRefusedError: [WinError 10061] Подключение не установлено, т.к. конечный компьютер отверг запрос на подключение
anonymous 25 октября 2016 г. 17:07
Извините-после перезагрузки все заработало-но что это могло быть?
anonymous 25 октября 2016 г. 18:56
А теперь и после перезагрузки снова та же ошибка. В чем тут дело?
anonymous 4 декабря 2016 г. 10:19
Здравствуйте скажите пожалуйста в чём проблема, хочу отправлять клиенту переменные но у меня работает почему?
Скрипт сервера

import socket #Импортируем модуль

s = socket.socket() #Создаём сокет, с помощью этой
#переменной мы будем им
#управлять

host = '0.0.0.0' #Заводим хост (IP-адрес)

port = 21 #Заводим порт

s.bind((host, port)) #Привязываем

s.listen(1) #Прослушиваем, указываем максимальное
#кол-во соединений

looking = True #Присваиваем переменной истинное значение

while looking ==True: #Выполнять пока значение перемнной истинно

print("Looking...") #Выводим в консоль надпись

c, addr = s.accept() #Принимаем входящее соединение,
#возвращаем входящимй сокет и адрес
#Адрес состоит из IP-адреса и порта.

print("Got a connection: ", addr) #Выводим надпись в консоль

try: #Делает если не встретил ошибки

print("Sent!")#Выводит в консоль надпись
data = str("Hello")
c.sendto(data, (host, port)) #Отправляет закодированное
#сообщение

except: #Если try встретил ошибку и она совпадает с этой, то
#будут выполнятся данные команды

print("can't send...") #Выводит в консоль надпись

looking = False #Присваивает переменной отрицательное значение

while looking == False: #Делать пока значение переменной
#отрицательное

c.close() #Завершить соединение входящего сокета

break #Остановить цикл

s.close() #Закрываем соединение с серверным сокетом


скрипт клиента
import socket #Импортируем модуль

s = socket.socket() #Создаём сокет, с помощью этой
#переменной мы будем им
#управлять

host = '127.0.0.1' #Заводим хост (IP-адрес)

port = 21 #Заводим порт

s.connect((host, port)) #Подключаемся к серверу

data, addr = s.recvfrom(1024) #Читает сообщение определённого размера
#Возвращает полученные данные и адрес

print("data not decoded:", data) #Выводит полученные данные кодированные

print("data decoded", data.decode()) #Выводит данные раскодированные

print("socket ended: ") #Выводит надпись

s.close() #Завершает сокет
anonymous 25 июля 2017 г. 13:33
Попробуй другой порт, например 7777
Комментировать
Вы anonymous