Let’e me give an example:
-
client connect to server, and send 1MB data to server every 1 second.
-
server side accept a connection, and then sleep 20 second, without recv msg from client.So the
tcp send buffer
in the client side will be full.
Code in client side:
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#define exit_if(r, ...)
if (r) {
printf(__VA_ARGS__);
printf("%s:%d error no: %d error msg %sn", __FILE__, __LINE__, errno, strerror(errno));
exit(1);
}
void setNonBlock(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
exit_if(flags < 0, "fcntl failed");
int r = fcntl(fd, F_SETFL, flags | O_NONBLOCK);
exit_if(r < 0, "fcntl failed");
}
void test_full_sock_buf_1(){
short port = 8000;
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
int fd = socket(AF_INET, SOCK_STREAM, 0);
exit_if(fd<0, "create socket error");
int ret = connect(fd, (struct sockaddr *) &addr, sizeof(struct sockaddr));
exit_if(ret<0, "connect to server error");
setNonBlock(fd);
printf("connect to server success");
const int LEN = 1024 * 1000;
char msg[LEN]; // 1MB data
memset(msg, 'a', LEN);
for (int i = 0; i < 1000; ++i) {
int len = send(fd, msg, LEN, 0);
printf("send: %d, erron: %d, %s n", len, errno, strerror(errno));
sleep(1);
}
}
int main(){
test_full_sock_buf_1();
return 0;
}
Code in server side:
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#define exit_if(r, ...)
if (r) {
printf(__VA_ARGS__);
printf("%s:%d error no: %d error msg %sn", __FILE__, __LINE__, errno, strerror(errno));
exit(1);
}
void test_full_sock_buf_1(){
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
exit_if(listenfd<0, "create socket error");
short port = 8000;
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
int r = ::bind(listenfd, (struct sockaddr *) &addr, sizeof(struct sockaddr));
exit_if(r<0, "bind socket error");
r = listen(listenfd, 100);
exit_if(r<0, "listen socket error");
struct sockaddr_in raddr;
socklen_t rsz = sizeof(raddr);
int cfd = accept(listenfd, (struct sockaddr *) &raddr, &rsz);
exit_if(cfd<0, "accept socket error");
sockaddr_in peer;
socklen_t alen = sizeof(peer);
getpeername(cfd, (sockaddr *) &peer, &alen);
printf("accept a connection from %s:%dn", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
printf("but now I will sleep 15 second, then exit");
sleep(15);
}
Start server side, then start client side.
server side may output:
accept a connection from 127.0.0.1:35764
but now I will sleep 15 second, then exit
Process finished with exit code 0
client side may output:
connect to server successsend: 1024000, erron: 0, Success
send: 1024000, erron: 0, Success
send: 1024000, erron: 0, Success
send: 552190, erron: 0, Success
send: -1, erron: 11, Resource temporarily unavailable
send: -1, erron: 11, Resource temporarily unavailable
send: -1, erron: 11, Resource temporarily unavailable
send: -1, erron: 11, Resource temporarily unavailable
send: -1, erron: 11, Resource temporarily unavailable
send: -1, erron: 11, Resource temporarily unavailable
send: -1, erron: 11, Resource temporarily unavailable
send: -1, erron: 11, Resource temporarily unavailable
send: -1, erron: 11, Resource temporarily unavailable
send: -1, erron: 11, Resource temporarily unavailable
send: -1, erron: 11, Resource temporarily unavailable
send: -1, erron: 104, Connection reset by peer
send: -1, erron: 32, Broken pipe
send: -1, erron: 32, Broken pipe
send: -1, erron: 32, Broken pipe
send: -1, erron: 32, Broken pipe
send: -1, erron: 32, Broken pipe
You can see, as the server side doesn’t recv the data from client, so when the client side tcp buffer
get full, but you still send data, so you may get Resource temporarily unavailable
error.
- Index
- » Networking
- » Resource temporarily unavailable Error In Socket
#1 2010-03-02 12:08 PM
- kavinsivakumar
- Member
- Registered: 2010-03-02
- Posts: 7
Re: Resource temporarily unavailable Error In Socket
Hi,
Now I am programming to communicate with some network printer through TCP Socket program.By sending command «33E 1r» to printer,causes, check the port for error normally.
In my case i used following code
bytesSent = send( sockfd, "33E 1r",sizeof("33E 1r"), 0);
bytesRecv = recv( sockfd,recvbuf, 1024, 0 );
Here command goes to printer proper manner.But when i try to receive the recv gets blocking can not able to read anything its hanging there.
Then i used Non Blocking Socket with select() method in this case i am getting «Resource temporarily unavailable».
Please help me to figure out this error
Thanks in Advance
Regards
Siva
#2 2010-03-02 01:21 PM
- i3839
- Oddministrator
- From: Amsterdam
- Registered: 2003-06-07
- Posts: 2,239
Re: Resource temporarily unavailable Error In Socket
When you do a read on a socket and there’s nothing to read then this happens:
1) If you are using blocking sockets, the read call will block until data arrives.
2) If using non-blocking sockets, the read won’t block, but return with an error
EAGAIN/EWOULDBLOCK, which means there’s nothing to read.
So your real problem is the printer not sending you any data at all. The «error»
is not a real error, but a way to tell you what you already knew: That there is
(yet) nothing to read.
If I had to guess the printer either expects you to send something else or more,
or it behaves as printers do and just stopped working for no apparent reason.
#3 2010-03-02 01:36 PM
- kavinsivakumar
- Member
- Registered: 2010-03-02
- Posts: 7
Re: Resource temporarily unavailable Error In Socket
Thanks to reply to me,
But in printer port have some error.I comes to know through the LCD display of Printer.Even same thing i applied in USB Printer it gives the error report.In network printer i cant able to read even port has error.
#4 2010-03-02 02:59 PM
- i3839
- Oddministrator
- From: Amsterdam
- Registered: 2003-06-07
- Posts: 2,239
Re: Resource temporarily unavailable Error In Socket
Are you trying to print out a «1»? If not, try removing the space.
I’d make sure the command you’re sending is correct, and to reset the
printer before sending it new stuff. Also, what reply do you expect?
And I’d try to adding a n after the r, or instead of it.
#5 2010-03-03 04:45 AM
- kavinsivakumar
- Member
- Registered: 2010-03-02
- Posts: 7
Re: Resource temporarily unavailable Error In Socket
Hi,
The Command «33E 1r» is the printer specific command.When i send this one it will check the printer port for error in case error is there it will return the error number else it will return the Printer make and model string.
As of now even in blocking and non blocking i cant able to receive anything from printer i want to resolve the problem please help me.
Thanking You
Siva
#6 2010-03-03 10:55 AM
- i3839
- Oddministrator
- From: Amsterdam
- Registered: 2003-06-07
- Posts: 2,239
Re: Resource temporarily unavailable Error In Socket
Are you really sure it’s exactly ’33E 1r’? Not n, or withut space or binary 1
instead of ASCII ‘1’?
#7 2010-03-03 01:57 PM
- kavinsivakumar
- Member
- Registered: 2010-03-02
- Posts: 7
Re: Resource temporarily unavailable Error In Socket
Hi,
I Know very well the command is «33E 1r».We only developed the firmware to the printer.
Regards
Siva
#8 2010-03-03 02:43 PM
- i3839
- Oddministrator
- From: Amsterdam
- Registered: 2003-06-07
- Posts: 2,239
Re: Resource temporarily unavailable Error In Socket
You could try to look at what’s being send with tcpdump or Wireshark,
maybe that sheds some light on the matter. I think it’s a miscommunication
between the printer and this software, or there’s a bug in the printer
firmware, or the network part stopped working after the error.
If you trust the printer to be okay then you’d need to post more of your code
if you want us to have any chance to find the bug. Your original question is
answered though: You get that behaviour because the printer doesn’t send
anything. It’s possible, but highly unlikely, it’s because you didn’t send the full
command: Check bytesSent.
What happens when you send that command just after the printer started up?
#9 2010-03-03 04:47 PM
- RobSeace
- Administrator
- From: Boston, MA
- Registered: 2002-06-12
- Posts: 3,839
- Website
Re: Resource temporarily unavailable Error In Socket
bytesSent = send( sockfd, "33E 1r",sizeof("33E 1r"), 0);
Do you really want to use sizeof() there? That will result in you sending the null
character, as well… Is that maybe freaking the printer out, because it doesn’t expect
to see embedded nulls in its data stream? What happens if you use strlen() instead?
#10 2010-03-04 05:19 AM
- kavinsivakumar
- Member
- Registered: 2010-03-02
- Posts: 7
Re: Resource temporarily unavailable Error In Socket
Hi,
Thank you to reply to me,
Here i used strlen() even though i am getting same error.In My case error less condition if i am sending «33E 1r» it returns the Device Model and make.But if again i am sending the printer shows command error.
In this case i cant able to receive the error code to that error from printer.
Regards
Siva
#11 2010-03-04 02:11 PM
- RobSeace
- Administrator
- From: Boston, MA
- Registered: 2002-06-12
- Posts: 3,839
- Website
Re: Resource temporarily unavailable Error In Socket
*shrug* Sounds to me like a problem with the printer, not your code, then… But,
we can’t really be sure without seeing all the real code in actual use… (Though, even
then, we might not be able to say much about it without access to the same type of
printer you’re talking to, either… Unless there’s some obvious and simple bug,
anyway…)
- Index
- » Networking
- » Resource temporarily unavailable Error In Socket
Сокеты и API сокетов используются для отправки сообщений по сети. Они предоставляют форму inter-process communication (IPC). Сеть может представлять собой логическую локальную сеть с компьютером или физически подключенную к внешней сети с собственными подключениями к другим сетям. Очевидным примером является Интернет, к которому вы подключаетесь через своего интернет-провайдера.
Этот учебник имеет три разных итерации построения сокет-сервера и клиента с Python:
-
Мы начнем учебник с рассмотрения простого сокет-сервера и клиента.
-
После того, как вы увидели API и то, как все работает в этом первоначальном примере, мы рассмотрим улучшенную версию, которая обрабатывает несколько соединений одновременно.
-
Наконец, мы приступим к созданию примера сервера и клиента, который функционирует как полноценное приложение для сокетов, со своим собственным настраиваемым заголовком и содержимым.
К концу этого урока вы поймете, как использовать основные функции и методы в модуле Python socket для написания собственных клиент-серверных приложений. , Это включает в себя показ того, как использовать пользовательский класс для отправки сообщений и данных между конечными точками, которые вы можете использовать и использовать для своих собственных приложений.
Примеры в этом руководстве используют Python 3.6. Вы можете найти source код на GitHub.
Сеть и розетки — это большие предметы. О них написаны буквальные тома. Если вы плохо знакомы с сокетами или сетями, это совершенно нормально, если вы чувствуете себя перегруженными всеми терминами и частями. Я знаю, что сделал!
Не расстраивайтесь, хотя. Я написал этот урок для вас. Как и в случае с Python, мы можем учиться понемногу. Используйте функцию закладок браузера и возвращайтесь, когда будете готовы к следующему разделу.
Давайте начнем!
Фон
Розетки имеют долгую историю. Их использование originated с ARPANET в 1971 году и позднее стало API в операционной системе Berkeley Software Distribution (BSD), выпущенной в 1983 году, под названием https://en.wikipedia .org/wiki/Berkeley_sockets [Беркли сокеты].
Когда в 1990-х годах появился Интернет со Всемирной паутиной, то же самое произошло и с сетевым программированием. Веб-серверы и браузеры были не единственными приложениями, использующими преимущества новых подключенных сетей и использующих сокеты. Клиент-серверные приложения всех типов и размеров получили широкое распространение.
Сегодня, несмотря на то, что базовые протоколы, используемые API сокетов, развивались годами, и мы видели новые, низкоуровневый API остался прежним.
Наиболее распространенным типом приложений сокетов являются клиент-серверные приложения, где одна сторона выступает в качестве сервера и ожидает подключения от клиентов. Это тип приложения, о котором я расскажу в этом руководстве. Более конкретно, мы рассмотрим API сокетов для сокетов Internet, иногда называемых сокетами Беркли или BSD. Также есть Unix сокеты домена, которые можно использовать только для связи между процессами на одном хосте.
Обзор Socket API
Https://docs.python.org/3/library/socket.html модуль[socket Python] предоставляет интерфейс для API сокетов Berkeley. Это модуль, который мы будем использовать и обсудить в этом руководстве.
Основные функции и методы API сокетов в этом модуле:
-
+ Раструб () +
-
+ Связывания () +
-
+ Слушать () +
-
+ Принимаю () +
-
+ Connect () +
-
+ Connect_ex () +
-
+ Send () +
-
+ RECV () +
-
+ Закрытие () +
Python предоставляет удобный и непротиворечивый API, который отображается непосредственно на эти системные вызовы, их C-аналоги. Мы рассмотрим, как они используются вместе, в следующем разделе.
Как часть своей стандартной библиотеки, Python также имеет классы, которые облегчают использование этих низкоуровневых функций сокетов. Хотя это и не рассматривается в этом руководстве, см. Https://docs.python.org/3/library/socketserver.html[socketserver module], среду для сетевых серверов. Есть также много доступных модулей, которые реализуют высокоуровневые интернет-протоколы, такие как HTTP и SMTP. Для обзора см. Https://docs.python.org/3/library/internet.html[Internet Протоколы и поддержка].
TCP-сокеты
Как вы вскоре увидите, мы создадим объект сокета с помощью + socket.socket () +
и определим тип сокета как + socket.SOCK_STREAM +
. Когда вы делаете это, используется протокол по умолчанию Transmission Control Protocol (TCP). Это хороший вариант по умолчанию и, вероятно, то, что вы хотите.
Почему вы должны использовать TCP? Протокол управления передачей (TCP):
-
Надежен: отброшенные в сети пакеты обнаруживаются и повторно передаются отправителем.
-
Имеет порядок доставки данных: данные читаются вашим приложением в том порядке, в котором они были написаны отправителем.
Напротив, User Datagram Protocol (UDP) сокеты, созданные с помощью + socket.SOCK_DGRAM +
, не являются надежными, и данные, считываемые получателем, могут быть вне приказ от отправителя пишет.
Почему это важно? Сети — это система доставки с максимальными усилиями. Нет никакой гарантии, что ваши данные достигнут пункта назначения или что вы получите то, что было отправлено вам.
Сетевые устройства (например, маршрутизаторы и коммутаторы) имеют ограниченную доступную полосу пропускания и собственные системные ограничения. Они имеют процессоры, память, шины и интерфейсные буферы пакетов, как наши клиенты и серверы. TCP освобождает вас от необходимости беспокоиться о потере packet, поступлении данных не по порядку и многих других вещах, которые неизменно происходят, когда вы общаетесь по сети.
На диаграмме ниже давайте рассмотрим последовательность вызовов API сокетов и поток данных для TCP:
Левый столбец представляет сервер. На правой стороне находится клиент.
Начиная с верхнего левого столбца, обратите внимание на API-вызовы, которые сервер делает для настройки «слушающего» сокета:
-
+ Раструб () +
-
+ Связывания () +
-
+ Слушать () +
-
+ Принимаю () +
Разъем для прослушивания делает именно так, как он звучит. Он слушает соединения от клиентов. Когда клиент подключается, сервер вызывает + accept () +
, чтобы принять или завершить соединение.
Клиент вызывает + connect () +
, чтобы установить соединение с сервером и инициировать трехстороннее рукопожатие. Этап рукопожатия важен, поскольку он гарантирует, что каждая сторона соединения доступна в сети, другими словами, что клиент может достичь сервера и наоборот. Может случиться так, что только один хост, клиент или сервер может связаться с другим.
В середине находится раздел «туда-обратно», где данные обмениваются между клиентом и сервером с помощью вызовов + send () +
и + recv () +
.
Внизу клиент и сервер + close () +
их соответствующие сокеты.
Эхо-клиент и сервер
Теперь, когда вы увидели обзор API сокетов и взаимодействия клиента и сервера, давайте создадим наш первый клиент и сервер. Начнем с простой реализации. Сервер будет просто отображать все, что он получает обратно клиенту.
Эхо-сервер
Вот сервер, + echo-server.py +
:
#!/usr/bin/env python3
import socket
HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 65432 # Port to listen on (non-privileged ports are > 1023)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
*Примечание:* Не беспокойтесь о понимании всего вышесказанного прямо сейчас. В этих нескольких строках кода много чего происходит. Это только отправная точка, чтобы вы могли увидеть базовый сервер в действии.
В конце этого руководства есть ссылка: #reference [reference section], которая содержит больше информации и ссылки на дополнительные ресурсы. Я буду ссылаться на эти и другие ресурсы на протяжении всего урока.
Давайте пройдемся по каждому вызову API и посмотрим, что происходит.
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
pass # Use the socket object without calling s.close().
Аргументы, передаваемые в https://docs.python.org/3/library/socket.html#socket.socket [+ socket () +
], указывают ссылку: # socket-address-family [семейство адресов] и сокет тип. + AF_INET +
— это семейство интернет-адресов для IPv4. + SOCK_STREAM +
— это тип сокета для ссылки: # tcp-sockets [TCP], протокол, который будет использоваться для передачи наших сообщений в сети.
+ bind () +
используется для связывания сокета с определенным сетевым интерфейсом и номером порта:
HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 65432 # Port to listen on (non-privileged ports are > 1023)
# ...
s.bind((HOST, PORT))
Значения, передаваемые в + bind () +
, зависят от ссылки: # socket-address-family [address family] сокета. В этом примере мы используем + socket.AF_INET +
(IPv4). Так что ожидается 2 кортежа: + (хост, порт) +
.
+ host +
может быть именем хоста, IP-адресом или пустой строкой. Если используется IP-адрес, + host +
должен быть строкой адреса в формате IPv4. IP-адрес + 127.0.0.1 +
является стандартным IPv4-адресом для интерфейса loopback, поэтому только процессы на хосте смогут подключаться к серверу. Если вы передадите пустую строку, сервер будет принимать соединения на всех доступных интерфейсах IPv4.
+ port +
должно быть целым числом из + 1 +
-` + 65535 + (
+ 0 + зарезервировано). Это TCP port номер для приема соединений от клиентов. Некоторые системы могут требовать привилегий суперпользователя, если порт <
+ 1024 +`.
Вот примечание об использовании имен хостов с + bind () +
:
_
«Если вы используете имя хоста в части хоста адреса сокета IPv4/v6, программа может показывать недетерминированное поведение, так как Python использует первый адрес, возвращенный из разрешения DNS. Адрес сокета будет по-разному преобразован в фактический адрес IPv4/v6, в зависимости от результатов разрешения DNS и/или конфигурации хоста. Для детерминированного поведения используйте числовой адрес в части хоста ». (Source)
_
Я расскажу об этом позже в ссылке: # using-hostnames [Using Hostnames], но об этом стоит упомянуть здесь. А пока, просто поймите, что при использовании имени хоста вы можете увидеть разные результаты в зависимости от того, что возвращается из процесса разрешения имени.
Это может быть что угодно. При первом запуске приложения это может быть адрес + 10.1.2.3 +
. В следующий раз это будет другой адрес, + 192.168.0.1 +
. В третий раз это может быть + 172.16.7.8 +
и так далее.
Продолжая пример с сервером, + listen () +
позволяет серверу + accept () +
соединений. Это делает его «слушающим» сокетом:
s.listen()
conn, addr = s.accept()
+ listen () +
имеет параметр + backlog +
. Он указывает количество неприемлемых подключений, которые система разрешит перед отказом в новых подключениях. Начиная с Python 3.5, это необязательно. Если не указано, выбирается значение по умолчанию + backlog +
.
Если ваш сервер получает много запросов на соединение одновременно, увеличение значения + backlog +
может помочь, установив максимальную длину очереди для ожидающих соединений. Максимальное значение зависит от системы. Например, в Linux см. Https://serverfault.com/questions/518862/will-increasing-net-core-somaxconn-make-a-difference/519152 [+/proc/sys/net/core/somaxconn +
].
+ accept () +
link: # blocking-вызывает [блокирует] и ждет входящего соединения. Когда клиент подключается, он возвращает новый объект сокета, представляющий соединение, и кортеж, содержащий адрес клиента. Кортеж будет содержать + (хост, порт) +
для соединений IPv4 или + (хост, порт, flowinfo, scopeid) +
для IPv6. См. Ссылку: # socket-address-family [Семейства адресов сокетов] в справочном разделе для получения подробной информации о значениях кортежей.
Одна вещь, которую необходимо понять, это то, что теперь у нас есть новый объект сокета из + accept () +
. Это важно, так как это сокет, который вы будете использовать для связи с клиентом. Он отличается от сокета прослушивания, который сервер использует для приема новых соединений:
conn, addr = s.accept()
with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
После получения объекта клиентского сокета + conn +
из + accept () +
бесконечный цикл + while +
используется для циклического перемещения по ссылке: # blocking-call [blocking messages] to + conn.recv () + `. Это читает любые данные, которые клиент отправляет, и возвращает их обратно, используя `+ conn.sendall () +
.
Если + conn.recv () +
возвращает пустой https://docs.python.org/3/library/stdtypes.html#bytes-objects [+ bytes +
] объект, + b '' +
, затем клиент закрыл соединение и цикл прерывается. Оператор + with +
используется с + conn +
для автоматического закрытия сокета в конце блока.
Эхо-клиент
Теперь давайте посмотрим на клиента, + echo-client.py +
:
#!/usr/bin/env python3
import socket
HOST = '127.0.0.1' # The server's hostname or IP address
PORT = 65432 # The port used by the server
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall(b'Hello, world')
data = s.recv(1024)
print('Received', repr(data))
По сравнению с сервером клиент довольно прост. Он создает объект сокета, подключается к серверу и вызывает + s.sendall () +
для отправки своего сообщения. Наконец, он вызывает + s.recv () +
, чтобы прочитать ответ сервера, а затем распечатать его.
Запуск клиента и сервера Echo
Давайте запустим клиент и сервер, чтобы посмотреть, как они себя ведут, и проверить, что происходит.
*Примечание:* Если у вас возникли проблемы с запуском примеров или собственного кода из командной строки, прочитайте https://dbader.org/blog/how-to-make-command-line-commands-with-python [Как создавать свои собственные команды командной строки с использованием Python?] Если вы работаете в Windows, проверьте https://docs.python.org/3.6/faq/windows.html[Python Windows FAQ].
Откройте терминал или командную строку, перейдите в каталог, содержащий ваши сценарии, и запустите сервер:
Ваш терминал будет зависать. Это потому, что сервер является ссылкой: # blocking-Call [заблокирован] (приостановлено) в вызове:
Он ждет подключения клиента. Теперь откройте другое окно терминала или командную строку и запустите клиент:
$ ./echo-client.py
Received b'Hello, world'
В окне сервера вы должны увидеть:
$ ./echo-server.py
Connected by ('127.0.0.1', 64623)
В приведенном выше выводе сервер напечатал кортеж + addr +
, возвращенный из + s.accept () +
. Это IP-адрес клиента и номер порта TCP. Номер порта + 64623 +
, скорее всего, будет другим, когда вы запустите его на своем компьютере.
Просмотр состояния сокета
Чтобы увидеть текущее состояние сокетов на вашем хосте, используйте + netstat +
. Он доступен по умолчанию в macOS, Linux и Windows.
Вот вывод netstat из macOS после запуска сервера:
$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 127.0.0.1.65432 *. *LISTEN
Обратите внимание, что + Local Address +
равно + 127.0.0.1.65432 +
. Если бы + echo-server.py +
использовал + HOST = '' +
вместо + HOST = '127.0.0.1' +
, netstat показал бы это:
$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 * .65432 *. *LISTEN
+ Local Address +
равно +* . 65432 +
, что означает, что все доступные хост-интерфейсы, которые поддерживают семейство адресов, будут использоваться для приема входящих соединений. В этом примере при вызове + socket () +
было использовано + socket.AF_INET +
(IPv4). Вы можете увидеть это в столбце + Proto +
: + tcp4 +
.
Я обрезал вывод выше, чтобы показать только эхо-сервер. Скорее всего, вы увидите гораздо больше результатов, в зависимости от того, на какой системе вы его используете. Обратите внимание на столбцы + Proto +
, + Local Address +
и + (state) +
. В последнем примере выше netstat показывает, что эхо-сервер использует TCP-сокет IPv4 (+ tcp4 +
), на порту 65432 на всех интерфейсах (+ *. 65432 +
), и он находится в состоянии прослушивания (`+ LISTEN + `).
Другой способ увидеть это вместе с дополнительной полезной информацией — использовать + lsof +
(список открытых файлов). Он доступен по умолчанию в macOS и может быть установлен в Linux с помощью диспетчера пакетов, если это еще не сделано:
$ lsof -i -n
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Python 67982 nathan 3u IPv4 0xecf272 0t0 TCP *:65432 (LISTEN)
+ lsof +
дает вам + COMMAND +
, + PID +
(идентификатор процесса) и + USER +
(идентификатор пользователя) открытых интернет-сокетов при использовании с опцией + -i +
. Выше процесс эхо-сервера.
+ netstat +
и + lsof +
имеют много доступных опций и различаются в зависимости от ОС, на которой вы их используете. Проверьте страницу + man +
или документацию для обоих. Им определенно стоит потратить немного времени и узнать поближе. Вы будете вознаграждены. В macOS и Linux используйте + man netstat +
и + man lsof +
. Для Windows используйте + netstat/? +
.
Вот типичная ошибка, которую вы увидите, когда будет предпринята попытка подключения к порту без прослушивающего сокета:
$ ./echo-client.py
Traceback (most recent call last):
File "./echo-client.py", line 9, in <module>
s.connect((HOST, PORT))
ConnectionRefusedError: [Errno 61] Connection refused
Либо указан неверный номер порта, либо сервер не работает. А может, на пути к файлу есть брандмауэр, блокирующий соединение, о котором легко забыть. Вы также можете увидеть ошибку + Время ожидания истекло +
. Добавьте правило брандмауэра, которое позволяет клиенту подключаться к порту TCP!
Список ссылок: #errors [errors] в справочном разделе.
Отсутствие взаимопонимания
Давайте подробнее рассмотрим, как клиент и сервер взаимодействуют друг с другом:
- При использовании интерфейса loopback (адрес IPv4
+ 127.0.0.1 +
или адрес IPv6 `+ -
1 `) данные никогда не покидают хост или не касаются внешняя сеть. На приведенной выше схеме интерфейс обратной связи находится внутри хоста. Это представляет внутреннюю природу интерфейса обратной связи и то, что соединения и данные, которые передают его, являются локальными для хоста. Вот почему вы также услышите интерфейс обратной связи и IP-адрес ` 127.0.0.1 ` или ` :: 1 +`, называемый «localhost».
Приложения используют интерфейс обратной связи для связи с другими процессами, запущенными на хосте, а также для обеспечения безопасности и изоляции от внешней сети. Поскольку он внутренний и доступен только изнутри хоста, он не раскрывается.
Это можно увидеть в действии, если у вас есть сервер приложений, который использует собственную частную базу данных. Если это не база данных, используемая другими серверами, возможно, она настроена на прослушивание соединений только через интерфейс обратной связи. В этом случае другие хосты в сети не могут подключиться к нему.
- Когда вы используете в своих приложениях IP-адрес, отличный от
+ 127.0.0.1 +
или `+ -
1 +`, он, вероятно, связан с интерфейсом Ethernet, который подключен к внешней сети. Это ваши ворота к другим хостам за пределами вашего «локального» королевства:
Будь осторожен там. Это неприятный, жестокий мир. Обязательно прочитайте ссылку на раздел: # using-hostnames [Using Hostnames], прежде чем выходить из безопасных границ «localhost». Есть примечание о безопасности, которое применяется, даже если вы не используете имена хостов и используете только IP-адреса.
Обработка нескольких соединений
Эхо-сервер определенно имеет свои ограничения. Самое большое, что он обслуживает только одного клиента и затем выходит. Клиент echo также имеет это ограничение, но есть дополнительная проблема. Когда клиент делает следующий вызов, возможно, что + s.recv () +
вернет только один байт, + b’H '+
из + b’Hello, world' +
:
Аргумент + bufsize +
для + 1024 +
, использованный выше, является максимальным объемом данных, которые должны быть получены одновременно. Это не означает, что + recv () +
вернет + 1024 +
байт.
+ send () +
также ведет себя так. + send () +
возвращает количество отправленных байтов, которое может быть меньше размера передаваемых данных. Вы несете ответственность за проверку этого и вызываете + send () +
столько раз, сколько необходимо для отправки всех данных:
_
«Приложения отвечают за проверку того, что все данные были отправлены; если были переданы только некоторые данные, приложение должно попытаться доставить оставшиеся данные ». (Source)
_
Мы избежали необходимости делать это с помощью + sendall () +
:
_
«В отличие от send (), этот метод продолжает отправлять данные из байтов до тех пор, пока не будут отправлены все данные или пока не произойдет ошибка. Никто не возвращается в случае успеха ». (Source)
_
На данный момент у нас есть две проблемы:
-
Как мы обрабатываем несколько соединений одновременно?
-
Нам нужно вызывать
+ send () +
и+ recv () +
, пока все данные не будут отправлены или получены.
Что мы делаем? Существует множество подходов к concurrency. В последнее время популярным подходом является использование Asynchronous I/O. + asyncio +
была введена в стандартную библиотеку в Python 3.4. Традиционный выбор — использовать threads.
Проблема с параллелизмом состоит в том, что трудно понять правильно. Есть много тонкостей, которые нужно учитывать и оберегать. Все, что требуется для того, чтобы один из них проявил себя, и ваше приложение может внезапно выйти из строя не очень тонкими способами.
Я не говорю это, чтобы отпугнуть вас от обучения и использования параллельного программирования. Если ваше приложение нуждается в масштабировании, это необходимо, если вы хотите использовать более одного процессора или одно ядро. Тем не менее, для этого урока мы будем использовать нечто более традиционное, чем потоки, и о котором легче рассуждать. Мы собираемся использовать дедушку системных вызовов: https://docs.python.org/3/library/selectors.html#selectors.BaseSelector.select [+ select () +
].
+ select () +
позволяет проверять завершение ввода/вывода в нескольких сокетах. Таким образом, вы можете вызвать + select () +
, чтобы увидеть, какие сокеты имеют ввод/вывод, готовый для чтения и/или записи. Но это Python, так что это еще не все. Мы собираемся использовать модуль selectors в стандартной библиотеке, чтобы использовать наиболее эффективную реализацию независимо от операционной системы, в которой мы работаем на:
_
«Этот модуль обеспечивает высокоуровневое и эффективное мультиплексирование ввода/вывода, построенное на примитивах выбранного модуля. Вместо этого пользователям рекомендуется использовать этот модуль, если они не хотят точно контролировать используемые примитивы уровня ОС ». (Source)
_
Несмотря на то, что с помощью + select () +
мы не можем работать одновременно, в зависимости от вашей рабочей нагрузки, этот подход все еще может быть достаточно быстрым. Это зависит от того, что ваше приложение должно делать, когда оно обслуживает запрос, и от количества клиентов, которые ему необходимо поддерживать.
https://docs.python.org/3/library/asyncio.html [+ asyncio +
] использует однопотоковую совместную многозадачность и цикл обработки событий для управления задачами. С помощью + select () +
мы напишем нашу собственную версию цикла событий, хотя и более просто и синхронно. При использовании нескольких потоков, даже если у вас есть параллелизм, в настоящее время мы должны использовать GIL с CPython и PyPy . Это эффективно ограничивает объем работы, которую мы можем выполнять параллельно.
Я говорю все это, чтобы объяснить, что использование + select () +
может быть идеальным выбором. Не думайте, что вам нужно использовать + asyncio +
, потоки или последнюю асинхронную библиотеку. Как правило, в сетевом приложении ваше приложение связано с вводом/выводом: оно может ожидать в локальной сети, конечных точках на другой стороне сети, на диске и т. Д.
Если вы получаете запросы от клиентов, которые инициируют работу с привязкой к процессору, посмотрите на модуль concurrent.futures. Он содержит класс ProcessPoolExecutor, который использует пул процессов для асинхронного выполнения вызовов.
В следующем разделе мы рассмотрим примеры сервера и клиента, которые решают эти проблемы. Они используют + select () +
для одновременной обработки нескольких соединений и вызывают + send () +
и + recv () +
столько раз, сколько необходимо.
Многоканальный клиент и сервер
В следующих двух разделах мы создадим сервер и клиент, который будет обрабатывать несколько соединений, используя объект + selector +
, созданный из модуля selectors.
Multi-Connection Server
Во-первых, давайте посмотрим на сервер множественных соединений + multiconn-server.py +
. Вот первая часть, которая устанавливает сокет прослушивания:
import selectors
sel = selectors.DefaultSelector()
# ...
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print('listening on', (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)
Самое большое различие между этим сервером и сервером эха — это вызов + lsock.setblocking (False) +
для настройки сокета в неблокирующем режиме. Вызовы, сделанные на этот сокет, больше не будут связывать: # blocking-Call [block] Когда он используется с + sel.select () +
, как вы увидите ниже, мы можем ожидать события на одном или нескольких сокетах, а затем читать и записывать данные, когда они будут готовы.
+ sel.register () +
регистрирует сокет, который будет отслеживаться с помощью + sel.select () +
для интересующих вас событий. Для сокета прослушивания мы хотим прочитать события: + selectors.EVENT_READ +
.
+ data +
используется для хранения любых произвольных данных вместе с сокетом. Возвращается, когда возвращается + select () +
. Мы будем использовать + data +
для отслеживания того, что было отправлено и получено в сокете.
Далее идет цикл событий:
import selectors
sel = selectors.DefaultSelector()
# ...
while True:
events = sel.select(timeout=None)
for key, mask in events:
if key.data is None:
accept_wrapper(key.fileobj)
else:
service_connection(key, mask)
https://docs.python.org/3/library/selectors.html#selectors.BaseSelector.select [+ sel.select (timeout = None) +
] ссылка: # blocking-call [blocks] до появления сокетов готов к вводу/выводу. Он возвращает список (ключ, события) кортежей, по одному для каждого сокета. + key +
является SelectorKey + namedtuple +
, который содержит атрибут + fileobj +
. + key.fileobj +
— объект сокета, а + mask +
— маска события готовых операций.
Если + key.data +
равно + None +
, то мы знаем, что это из сокета прослушивания, и нам нужно + accept () +
соединение. Мы вызовем нашу собственную функцию-оболочку + accept () +
, чтобы получить новый объект сокета и зарегистрировать его с помощью селектора. Мы посмотрим на это через мгновение.
Если + key.data +
не равно + None +
, то мы знаем, что это клиентский сокет, который уже принят, и нам нужно его обслужить. Затем вызывается + service_connection () +
и передается + key +
и + mask +
, которые содержат все, что нам нужно для работы с сокетом.
Давайте посмотрим, что делает наша функция + accept_wrapper () +
:
def accept_wrapper(sock):
conn, addr = sock.accept() # Should be ready to read
print('accepted connection from', addr)
conn.setblocking(False)
data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')
events = selectors.EVENT_READ | selectors.EVENT_WRITE
sel.register(conn, events, data=data)
Поскольку прослушивающий сокет был зарегистрирован для события + selectors.EVENT_READ +
, он должен быть готов к чтению. Мы вызываем + sock.accept () +
, а затем сразу же вызываем + conn.setblocking (False) +
, чтобы перевести сокет в неблокирующий режим.
Помните, что это главная цель в этой версии сервера, так как мы не хотим, чтобы он связывал: # blocking-Call [block]. Если он блокируется, то весь сервер останавливается, пока не вернется. Это означает, что другие сокеты остаются в ожидании. Это ужасное состояние зависания, при котором вы не хотите, чтобы ваш сервер находился в режиме ожидания.
Затем мы создаем объект для хранения данных, которые мы хотим включить вместе с сокетом, используя класс + types.SimpleNamespace +
. Поскольку мы хотим знать, когда клиентское соединение готово для чтения и записи, оба эти события устанавливаются с использованием следующего:
events = selectors.EVENT_READ | selectors.EVENT_WRITE
Затем маска + events +
, сокет и объекты данных передаются в + sel.register () +
.
Теперь давайте посмотрим на + service_connection () +
, чтобы увидеть, как обрабатывается клиентское соединение, когда оно готово:
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024) # Should be ready to read
if recv_data:
data.outb += recv_data
else:
print('closing connection to', data.addr)
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if data.outb:
print('echoing', repr(data.outb), 'to', data.addr)
sent = sock.send(data.outb) # Should be ready to write
data.outb = data.outb[sent:]
Это сердце простого сервера мультисвязи. + key +
— это + namedtuple +
, возвращаемый из + select () +
, который содержит объект сокета (+ fileobj +
) и объект данных. + mask +
содержит события, которые готовы.
Если сокет готов к чтению, то + mask & selectors.EVENT_READ +
равно true и вызывается + sock.recv () +
. Любые прочитанные данные добавляются в + data.outb +
, поэтому их можно отправить позже.
Обратите внимание на блок + else: +
, если данные не получены:
if recv_data:
data.outb += recv_data
else:
print('closing connection to', data.addr)
sel.unregister(sock)
sock.close()
Это означает, что клиент закрыл свой сокет, так что сервер тоже должен. Но не забудьте сначала вызвать + sel.unregister () +
, чтобы он больше не отслеживался + select () +
.
Когда сокет готов к записи, что всегда должно быть в случае работоспособного сокета, любые полученные данные, хранящиеся в + data.outb +
, передаются клиенту с помощью + sock.send () +
. Отправленные байты затем удаляются из буфера отправки:
data.outb = data.outb[sent:]
Multi-Connection Client
Теперь давайте посмотрим на клиент множественного подключения, + multiconn-client.py +
. Он очень похож на сервер, но вместо прослушивания соединений он начинается с инициализации соединений с помощью + start_connections () +
:
messages = [b'Message 1 from client.', b'Message 2 from client.']
def start_connections(host, port, num_conns):
server_addr = (host, port)
for i in range(0, num_conns):
connid = i + 1
print('starting connection', connid, 'to', server_addr)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
sock.connect_ex(server_addr)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
data = types.SimpleNamespace(connid=connid,
msg_total=sum(len(m) for m in messages),
recv_total=0,
messages=list(messages),
outb=b'')
sel.register(sock, events, data=data)
+ num_conns +
читается из командной строки, которая является числом соединений, которые нужно создать с сервером. Как и сервер, каждый сокет установлен в неблокирующий режим.
+ connect_ex () +
используется вместо + connect () +
, поскольку + connect () +
немедленно вызовет исключение + BlockingIOError +
. + connect_ex () +
изначально возвращает индикатор ошибки + errno.EINPROGRESS +
вместо того, чтобы вызывать исключение во время соединения. Как только соединение завершено, сокет готов к чтению и записи и возвращается как таковой + select () +
.
После настройки сокета данные, которые мы хотим сохранить в сокете, создаются с использованием класса + types.SimpleNamespace +
. Сообщения, которые клиент отправит на сервер, копируются с использованием + list (messages) +
, поскольку каждое соединение вызывает + socket.send () +
и изменяет список. Все необходимое для отслеживания того, что клиент должен отправить, отправил и получил, а общее количество байтов в сообщениях хранится в объекте + data +
.
Давайте посмотрим на + service_connection () +
. Он в основном такой же, как сервер:
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024) # Should be ready to read
if recv_data:
print('received', repr(recv_data), 'from connection', data.connid)
data.recv_total += len(recv_data)
if not recv_data or data.recv_total == data.msg_total:
print('closing connection', data.connid)
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if not data.outb and data.messages:
data.outb = data.messages.pop(0)
if data.outb:
print('sending', repr(data.outb), 'to connection', data.connid)
sent = sock.send(data.outb) # Should be ready to write
data.outb = data.outb[sent:]
Есть одно важное отличие. Он отслеживает количество байтов, полученных от сервера, чтобы он мог закрыть свою сторону соединения. Когда сервер обнаруживает это, он также закрывает свою сторону соединения.
Обратите внимание, что при этом сервер зависит от хорошего поведения клиента: сервер ожидает, что клиент закроет свою сторону соединения после завершения отправки сообщений. Если клиент не закрывается, сервер оставит соединение открытым. В реальном приложении вы можете принять меры против этого на своем сервере и предотвратить накопление клиентских подключений, если они не отправляют запрос через определенное время.
Запуск клиента и сервера Multi-Connection
Теперь давайте запустим + multiconn-server.py +
и + multiconn-client.py +
. Они оба используют аргументы командной строки. Вы можете запустить их без аргументов, чтобы увидеть варианты.
Для сервера передайте номера + host +
и + port +
:
$ ./multiconn-server.py
usage: ./multiconn-server.py <host> <port>
Для клиента также передайте количество соединений, которые нужно создать, на сервер, + num_connections +
:
$ ./multiconn-client.py
usage: ./multiconn-client.py <host> <port> <num_connections>
Ниже приведены выходные данные сервера при прослушивании интерфейса обратной связи через порт 65432:
$ ./multiconn-server.py 127.0.0.1 65432
listening on ('127.0.0.1', 65432)
accepted connection from ('127.0.0.1', 61354)
accepted connection from ('127.0.0.1', 61355)
echoing b'Message 1 from client.Message 2 from client.' to ('127.0.0.1', 61354)
echoing b'Message 1 from client.Message 2 from client.' to ('127.0.0.1', 61355)
closing connection to ('127.0.0.1', 61354)
closing connection to ('127.0.0.1', 61355)
Ниже приведен вывод клиента, когда он создает два соединения с сервером выше:
$ ./multiconn-client.py 127.0.0.1 65432 2
starting connection 1 to ('127.0.0.1', 65432)
starting connection 2 to ('127.0.0.1', 65432)
sending b'Message 1 from client.' to connection 1
sending b'Message 2 from client.' to connection 1
sending b'Message 1 from client.' to connection 2
sending b'Message 2 from client.' to connection 2
received b'Message 1 from client.Message 2 from client.' from connection 1
closing connection 1
received b'Message 1 from client.Message 2 from client.' from connection 2
closing connection 2
Клиент приложения и сервер
Пример клиента и сервера с несколькими подключениями, безусловно, является улучшением по сравнению с тем, с чего мы начали. Однако давайте сделаем еще один шаг и рассмотрим недостатки предыдущего примера «multiconn» в окончательной реализации: клиент приложения и сервер.
Мы хотим, чтобы клиент и сервер обрабатывали ошибки соответствующим образом, чтобы другие соединения не были затронуты. Очевидно, что наш клиент или сервер не должны рухнуть в ярости, если исключение не поймано. Это то, что мы не обсуждали до сих пор. Я намеренно пропустил обработку ошибок для краткости и ясности в примерах.
Теперь, когда вы знакомы с базовым API, неблокирующими сокетами и + select () +
, мы можем добавить некоторую обработку ошибок и обсудить «слона в комнате», который я скрыл от вас за этим большой занавес там. Да, я говорю о пользовательском классе, который я упоминал еще во введении. Я знал, что ты не забудешь.
Во-первых, давайте исправим ошибки:
_
«Все ошибки вызывают исключения. Нормальные исключения для недопустимых типов аргументов и условий нехватки памяти могут быть вызваны; начиная с Python 3.3, ошибки, связанные с семантикой сокетов или адресов, приводят к + OSError +
или одному из его подклассов. ” (Source)
_
Нам нужно отловить + OSError +
. Еще одна вещь, которую я не упомянул в связи с ошибками, это тайм-ауты. Вы увидите их во многих местах в документации. Тайм-ауты случаются и являются «нормальной» ошибкой. Хосты и маршрутизаторы перезагружаются, порты коммутатора выходят из строя, кабели выходят из строя, кабели отключаются, вы называете это. Вы должны быть готовы к этим и другим ошибкам и обрабатывать их в своем коде.
Что насчет «слона в комнате»? Как подсказывает тип сокета + socket.SOCK_STREAM +
, при использовании TCP вы читаете из непрерывного потока байтов. Это похоже на чтение из файла на диске, но вместо этого вы читаете байты из сети.
Когда байты поступают в ваш сокет, включаются сетевые буферы. Как только вы их прочитаете, их нужно где-то сохранить. Вызов + recv () +
снова считывает следующий поток байтов, доступных из сокета.
Это означает, что вы будете читать из сокета порциями. Вам нужно вызвать + recv () +
и сохранить данные в буфере, пока вы не прочитаете достаточно байтов, чтобы получить полное сообщение, которое имеет смысл для вашего приложения.
Вы сами должны определить и отслеживать границы сообщений. Что касается сокета TCP, то он просто отправляет и получает необработанные байты в и из сети. Он ничего не знает о том, что означают эти необработанные байты.
Это подводит нас к определению протокола прикладного уровня. Что такое протокол прикладного уровня? Проще говоря, ваше приложение будет отправлять и получать сообщения. Эти сообщения являются протоколом вашего приложения.
Другими словами, длина и формат, который вы выбираете для этих сообщений, определяют семантику и поведение вашего приложения. Это напрямую связано с тем, что я объяснил в предыдущем параграфе относительно чтения байтов из сокета. Когда вы читаете байты с помощью + recv () +
, вам нужно следить за тем, сколько байтов было прочитано, и выяснить, где находятся границы сообщения.
Как это сделать? Одним из способов является отправка сообщений фиксированной длины. Если они всегда одинакового размера, то это легко. Когда вы прочитали это количество байтов в буфер, вы знаете, что у вас есть одно полное сообщение.
Тем не менее, использование сообщений фиксированной длины неэффективно для небольших сообщений, где вам нужно использовать заполнение для их заполнения. Кроме того, у вас все еще остается проблема с тем, что делать с данными, которые не вписываются в одно сообщение.
В этом уроке мы будем использовать общий подход. Подход, который используется многими протоколами, включая HTTP. Мы добавим в префикс сообщения заголовок, который включает в себя длину содержимого, а также любые другие необходимые нам поля. Делая это, нам нужно только идти в ногу с заголовком. После того, как мы прочитали заголовок, мы можем обработать его, чтобы определить длину содержимого сообщения, а затем прочитать это количество байтов, чтобы использовать его.
Мы реализуем это, создав собственный класс, который может отправлять и получать сообщения, содержащие текстовые или двоичные данные. Вы можете улучшить и расширить его для своих собственных приложений. Самое главное, что вы сможете увидеть пример того, как это делается.
Мне нужно упомянуть кое-что о сокетах и байтах, которые могут повлиять на вас. Как мы говорили ранее, при отправке и получении данных через сокеты вы отправляете и получаете необработанные байты.
Если вы получаете данные и хотите использовать их в контексте, в котором они интерпретируются как несколько байтов, например, 4-байтовое целое число, вам необходимо учитывать, что они могут быть в формате, который не является родным для ЦП вашей машины. Клиент или сервер на другом конце может иметь ЦП, который использует порядок байтов, отличный от вашего собственного. Если это так, перед использованием его необходимо преобразовать его в собственный порядок байтов вашего хоста.
Этот порядок байтов называется процессором endianness. Смотрите ссылку: # byte-endianness [Byte Endianness] в справочном разделе для деталей. Мы избежим этой проблемы, воспользовавшись Unicode для нашего заголовка сообщения и используя кодировку UTF-8. Поскольку UTF-8 использует 8-битное кодирование, проблем с порядком байтов не возникает.
Вы можете найти объяснение в документации Python Encodings and Unicode. Обратите внимание, что это относится только к текстовому заголовку. Мы будем использовать явный тип и кодировку, определенные в заголовке для содержимого, которое отправляется, полезной нагрузки сообщения. Это позволит нам передавать любые данные, которые нам нужны (текстовые или двоичные), в любом формате.
Вы можете легко определить порядок байтов вашей машины, используя + sys.byteorder +
. Например, на моем ноутбуке Intel это происходит:
$ python3 -c 'import sys; print(repr(sys.byteorder))'
'little'
Если я запускаю это на виртуальной машине с emulations процессором с прямым порядком байтов (PowerPC), то это происходит:
$ python3 -c 'import sys; print(repr(sys.byteorder))'
'big'
В этом примере приложения наш протокол прикладного уровня определяет заголовок как текст Unicode с кодировкой UTF-8. Для реального содержимого сообщения, полезной нагрузки сообщения, вам все равно придется поменять порядок байтов вручную, если это необходимо.
Это будет зависеть от вашего приложения и от того, нужно ли ему обрабатывать многобайтовые двоичные данные с компьютера с другим порядком байтов. Вы можете помочь вашему клиенту или серверу реализовать двоичную поддержку, добавив дополнительные заголовки и используя их для передачи параметров, аналогично HTTP.
Не волнуйтесь, если это еще не имеет смысла. В следующем разделе вы увидите, как все это работает и соответствует друг другу.
Заголовок протокола приложения
Давайте полностью определим заголовок протокола. Заголовок протокола:
-
Текст переменной длины
-
Юникод с кодировкой UTF-8
-
Словарь Python, сериализованный с использованием JSON
Требуемые заголовки или подзаголовки в словаре заголовка протокола следующие:
Name | Description |
---|---|
|
The byte order of the machine (uses |
|
The length of the content in bytes. |
|
The type of content in the payload, for example, |
|
The encoding used by the content, for example, |
Эти заголовки информируют получателя о содержании в полезной нагрузке сообщения. Это позволяет отправлять произвольные данные, предоставляя достаточно информации, чтобы получатель мог правильно декодировать и интерпретировать содержимое. Поскольку заголовки находятся в словаре, легко добавить дополнительные заголовки, вставляя пары ключ/значение по мере необходимости.
Отправка сообщения приложения
Есть еще небольшая проблема. У нас есть заголовок переменной длины, который хорош и гибок, но как узнать длину заголовка при чтении его с помощью + recv () +
?
Когда мы ранее говорили об использовании + recv () +
и границ сообщения, я упоминал, что заголовки фиксированной длины могут быть неэффективными. Это правда, но мы собираемся использовать небольшой 2-байтовый заголовок фиксированной длины для префикса заголовка JSON, который содержит его длину.
Вы можете думать об этом как о гибридном подходе к отправке сообщений. По сути, мы запускаем процесс получения сообщения, отправляя сначала длину заголовка. Это позволяет нашему получателю легко деконструировать сообщение.
Чтобы лучше понять формат сообщения, давайте рассмотрим сообщение целиком:
Сообщение начинается с заголовка фиксированной длины в 2 байта, который является целым числом в сетевом порядке байтов. Это длина следующего заголовка, JSON-заголовка переменной длины. После того, как мы прочитали 2 байта с помощью + recv () +
, мы знаем, что можем обработать 2 байта как целое число и затем прочитать это число байтов перед декодированием заголовка JSON UTF-8.
Ссылка: # application-protocol-header [JSON header] содержит словарь дополнительных заголовков. Одним из них является + content-length +
, которое представляет собой количество байтов содержимого сообщения (не включая заголовок JSON). Как только мы вызвали + recv () +
и прочитали + content-length +
байтов, мы достигли границы сообщения и прочитали все сообщение.
Класс сообщения приложения
Наконец, выигрыш! Давайте посмотрим на класс + Message +
и посмотрим, как он используется с + select () +
, когда в сокете происходят события чтения и записи.
Для этого примера приложения мне нужно было придумать, какие типы сообщений будут использовать клиент и сервер. На данный момент мы намного больше, чем просто клиенты и серверы игрушечного эха.
Для простоты и демонстрации того, как все будет работать в реальном приложении, я создал протокол приложения, который реализует базовую функцию поиска. Клиент отправляет запрос поиска, а сервер выполняет поиск совпадения. Если запрос, отправленный клиентом, не распознается как поиск, сервер предполагает, что это двоичный запрос, и возвращает двоичный ответ.
Прочитав следующие разделы, выполнив примеры и поэкспериментировав с кодом, вы увидите, как все работает. Затем вы можете использовать класс + Message +
в качестве отправной точки и изменить его для своего собственного использования.
Мы на самом деле не так уж далеки от «многоконного» примера клиента и сервера. Код цикла событий остается тем же в + app-client.py +
и + app-server.py +
. Что я сделал, так это переместил код сообщения в класс с именем + Message +
и добавил методы для поддержки чтения, записи и обработки заголовков и содержимого. Это отличный пример использования класса.
Как мы уже говорили ранее, и вы увидите ниже, работа с сокетами подразумевает сохранение состояния. Используя класс, мы сохраняем все состояние, данные и код в единое целое. Экземпляр класса создается для каждого сокета на клиенте и сервере, когда соединение установлено или принято.
Класс в основном одинаков как для клиента, так и для сервера для методов-оболочек и утилит. Они начинаются с подчеркивания, например + Message._json_encode () +
. Эти методы упрощают работу с классом. Они помогают другим методам, позволяя им оставаться короче и поддерживают принцип DRY.
Серверный класс + Message +
работает по сути так же, как и клиентский, и наоборот. Разница в том, что клиент инициирует соединение и отправляет сообщение с запросом, после чего обрабатывает ответное сообщение сервера. И наоборот, сервер ожидает подключения, обрабатывает сообщение запроса клиента и затем отправляет ответное сообщение.
Это выглядит так:
Step | Endpoint | Action/Message Content |
---|---|---|
1 |
Client |
Sends a |
2 |
Server |
Receives and processes client request |
3 |
Server |
Sends a |
4 |
Client |
Receives and processes server response |
Вот расположение файла и кода:
Application | File | Code |
---|---|---|
Server |
|
The server’s main script |
Server |
|
The server’s |
Client |
|
The client’s main script |
Client |
|
The client’s |
Точка ввода сообщения
Я хотел бы обсудить, как работает класс «+ Message +», упомянув сначала об одном аспекте его дизайна, который не был сразу очевиден для меня. Только после рефакторинга, по крайней мере, пять раз я пришел к тому, чем он является в настоящее время Why? Управляющий государством.
После того, как объект + Message +
создан, он связывается с сокетом, который отслеживается для событий с помощью + selector.register () +
:
message = libserver.Message(sel, conn, addr)
sel.register(conn, selectors.EVENT_READ, data=message)
*Примечание:* Некоторые примеры кода в этом разделе взяты из основного сценария сервера и класса `+ Message +`, но этот раздел и обсуждение в равной степени относятся и к клиенту. Я покажу и объясню версию клиента, когда она будет отличаться.
Когда события готовы к сокету, они возвращаются с помощью + selector.select () +
. Затем мы можем получить ссылку обратно на объект сообщения, используя атрибут + data +
объекта + key +
, и вызвать метод в + Message +
:
while True:
events = sel.select(timeout=None)
for key, mask in events:
# ...
message = key.data
message.process_events(mask)
Посмотрев на цикл событий выше, вы увидите, что + sel.select () +
находится на месте водителя. Это блокировка, ожидание наверху цикла событий. Он отвечает за пробуждение, когда события чтения и записи готовы для обработки в сокете. Это означает, что косвенно он также отвечает за вызов метода + process_events () +
. Это то, что я имею в виду, когда говорю, что метод + process_events () +
является точкой входа.
Давайте посмотрим, что делает метод + process_events () +
:
def process_events(self, mask):
if mask & selectors.EVENT_READ:
self.read()
if mask & selectors.EVENT_WRITE:
self.write()
Это хорошо: + process_events () +
просто. Он может делать только две вещи: вызывать + read () +
и + write () +
.
Это возвращает нас к управлению государством. После нескольких рефакторингов я решил, что если другой метод зависит от переменных состояния, имеющих определенное значение, то они будут вызываться только из + read () +
и + write () +
. Это сохраняет логику настолько простой, насколько это возможно, так как события поступают в сокет для обработки.
Это может показаться очевидным, но первые несколько итераций класса представляли собой смесь некоторых методов, которые проверяли текущее состояние и, в зависимости от их значения, вызывали другие методы для обработки данных вне + read () +
или `+ write ( ) + `. В конце концов, это оказалось слишком сложным, чтобы справиться и не отставать.
Вам определенно следует изменить класс в соответствии с вашими потребностями, чтобы он работал лучше для вас, но я бы рекомендовал вам сохранять проверки состояния и вызовы методов, которые зависят от этого состояния, для + read () +
и ` + write () + `методы, если это возможно.
Давайте посмотрим на + read () +
. Это версия сервера, но версия клиента такая же. Он просто использует другое имя метода, + process_response () +
вместо + process_request () +
:
def read(self):
self._read()
if self._jsonheader_len is None:
self.process_protoheader()
if self._jsonheader_len is not None:
if self.jsonheader is None:
self.process_jsonheader()
if self.jsonheader:
if self.request is None:
self.process_request()
Метод + _read () +
вызывается первым. Он вызывает + socket.recv () +
для чтения данных из сокета и сохранения их в приемном буфере.
Помните, что когда вызывается + socket.recv () +
, все данные, составляющие полное сообщение, возможно, еще не поступили. + socket.recv () +
может потребоваться вызвать снова. Вот почему существуют проверки состояния для каждой части сообщения перед вызовом соответствующего метода для его обработки.
Перед тем, как метод обрабатывает свою часть сообщения, он сначала проверяет, было ли прочитано достаточно байтов в приемный буфер. Если они есть, он обрабатывает свои соответствующие байты, удаляет их из буфера и записывает свои выходные данные в переменную, которая используется на следующем этапе обработки. Поскольку в сообщении есть три компонента, есть три проверки состояния и вызовы метода + process +
:
Message Component | Method | Output |
---|---|---|
Fixed-length header |
|
|
JSON header |
|
|
Content |
|
|
Далее давайте посмотрим на + write () +
. Это версия сервера:
def write(self):
if self.request:
if not self.response_created:
self.create_response()
self._write()
+ write () +
сначала проверяет наличие + request +
. Если он существует и ответ не был создан, вызывается + create_response () +
. + create_response () +
устанавливает переменную состояния + response_created +
и записывает ответ в буфер отправки.
Метод + _write () +
вызывает + socket.send () +
, если в буфере отправки есть данные.
Помните, что когда вызывается + socket.send () +
, все данные в буфере отправки могут не помещаться в очередь для передачи. Сетевые буферы для сокета могут быть заполнены, и может потребоваться повторный вызов + socket.send () +
. Вот почему существуют государственные проверки. + create_response () +
следует вызывать только один раз, но ожидается, что + _write () +
нужно будет вызывать несколько раз.
Клиентская версия + write () +
похожа:
def write(self):
if not self._request_queued:
self.queue_request()
self._write()
if self._request_queued:
if not self._send_buffer:
# Set selector to listen for read events, we're done writing.
self._set_selector_events_mask('r')
Так как клиент инициирует соединение с сервером и сначала отправляет запрос, проверяется переменная состояния + _request_queued +
. Если запрос не был поставлен в очередь, он вызывает + queue_request () +
. + queue_request () +
создает запрос и записывает его в буфер отправки. Он также устанавливает переменную состояния + _request_queued +
, поэтому она вызывается только один раз.
Как и сервер, + _write () +
вызывает + socket.send () +
, если в буфере отправки есть данные.
Заметная разница в версии клиента + write () +
— это последняя проверка, чтобы увидеть, был ли запрос поставлен в очередь. Это будет объяснено более подробно в ссылке раздела: # client-main-script [Client Main Script], но причина этого состоит в том, чтобы сказать + selector.select () +
, чтобы прекратить мониторинг сокета на наличие событий записи. Если запрос был поставлен в очередь, а буфер отправки пуст, тогда мы закончили запись и нас интересуют только события чтения. Нет причин получать уведомление о том, что сокет доступен для записи.
Я завершу этот раздел, оставив вас с одной мыслью. Основной целью этого раздела было объяснить, что + selector.select () +
вызывает класс + Message +
через метод + process_events () +
, и описать, как управляется состояние.
Это важно, потому что + process_events () +
будет вызываться много раз в течение жизни соединения. Поэтому убедитесь, что любые методы, которые должны вызываться только один раз, либо сами проверяют переменную состояния, либо переменная состояния, установленная методом, проверяется вызывающей стороной.
Главный скрипт сервера
В основном скрипте сервера + app-server.py +
аргументы считываются из командной строки, которые определяют интерфейс и порт для прослушивания:
$ ./app-server.py
usage: ./app-server.py <host> <port>
Например, чтобы прослушивать интерфейс обратной связи на порту + 65432 +
, введите:
$ ./app-server.py 127.0.0.1 65432
listening on ('127.0.0.1', 65432)
Используйте пустую строку для + <host> +
для прослушивания всех интерфейсов.
После создания сокета вызывается + socket.setsockopt () +
с опцией + socket.SO_REUSEADDR +
:
# Avoid bind() exception: OSError: [Errno 48] Address already in use
lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Например, если сервер активно закрыл соединение, оно будет оставаться в состоянии «+ TIME_WAIT » в течение двух или более минут, в зависимости от операционной системы. Если вы попытаетесь запустить сервер еще раз до истечения состояния ` TIME_WAIT `, вы получите исключение ` OSError ` для ` Адрес уже используется +`. Это гарантия того, что любые задержанные пакеты в сети не будут доставлены не тому приложению.
Цикл обработки событий фиксирует любые ошибки, чтобы сервер мог оставаться в рабочем состоянии и продолжать работу:
while True:
events = sel.select(timeout=None)
for key, mask in events:
if key.data is None:
accept_wrapper(key.fileobj)
else:
message = key.data
try:
message.process_events(mask)
except Exception:
print('main: error: exception for',
f'{message.addr}:n{traceback.format_exc()}')
message.close()
Когда клиентское соединение принято, создается объект + Message +
:
def accept_wrapper(sock):
conn, addr = sock.accept() # Should be ready to read
print('accepted connection from', addr)
conn.setblocking(False)
message = libserver.Message(sel, conn, addr)
sel.register(conn, selectors.EVENT_READ, data=message)
Объект + Message +
связан с сокетом в вызове + sel.register () +
и изначально настроен на мониторинг только для событий чтения. Как только запрос будет прочитан, мы изменим его, чтобы прослушивать только события записи.
Преимущество такого подхода на сервере заключается в том, что в большинстве случаев, когда сокет исправен и нет проблем с сетью, он всегда будет доступен для записи.
Если бы мы сказали + sel.register () +
также контролировать + EVENT_WRITE +
, цикл обработки событий немедленно активировался бы и уведомил нас, что это так. Однако в этот момент нет причин просыпаться и вызывать + send () +
для сокета. Ответа нет, поскольку запрос еще не обработан. Это будет потреблять и тратить ценные циклы процессора.
Класс сообщения сервера
В ссылке на раздел: # message-entry-point [Точка входа в сообщение] мы рассмотрели, как объект + Message +
был вызван в действие, когда события сокета были готовы через + process_events () +
. Теперь давайте посмотрим, что происходит, когда данные считываются в сокет и компонент или часть сообщения готова для обработки сервером.
Класс сообщений сервера находится в + libserver.py +
. Вы можете найти source код на GitHub.
Методы появляются в классе в том порядке, в котором происходит обработка сообщения.
Когда сервер прочитал как минимум 2 байта, заголовок фиксированной длины может быть обработан:
def process_protoheader(self):
hdrlen = 2
if len(self._recv_buffer) >= hdrlen:
self._jsonheader_len = struct.unpack('>H',
self._recv_buffer[:hdrlen])[0]
self._recv_buffer = self._recv_buffer[hdrlen:]
Заголовок фиксированной длины представляет собой 2-байтовое целое число в сетевом порядке (с прямым порядком байтов), которое содержит длину заголовка JSON. https://docs.python.org/3/library/struct.html [struct.unpack ()] используется для чтения значения, его декодирования и сохранения в + self._jsonheader_len +
. После обработки фрагмента сообщения, за которое он отвечает, + process_protoheader () +
удаляет его из буфера приема.
Как и заголовок фиксированной длины, когда в буфере приема достаточно данных для заголовка JSON, он также может быть обработан:
def process_jsonheader(self):
hdrlen = self._jsonheader_len
if len(self._recv_buffer) >= hdrlen:
self.jsonheader = self._json_decode(self._recv_buffer[:hdrlen],
'utf-8')
self._recv_buffer = self._recv_buffer[hdrlen:]
for reqhdr in ('byteorder', 'content-length', 'content-type',
'content-encoding'):
if reqhdr not in self.jsonheader:
raise ValueError(f'Missing required header "{reqhdr}".')
Метод + self._json_decode () +
вызывается для декодирования и десериализации заголовка JSON в словарь. Поскольку заголовок JSON определен как Unicode с кодировкой UTF-8, в вызове жестко кодируется + utf-8 +
. Результат сохраняется в + self.jsonheader +
. После обработки фрагмента сообщения, за которое он отвечает, + process_jsonheader () +
удаляет его из буфера приема.
Далее идет фактическое содержание или полезная нагрузка сообщения. Это описывается заголовком JSON в + self.jsonheader +
. Когда байты + content-length +
доступны в приемном буфере, запрос может быть обработан:
def process_request(self):
content_len = self.jsonheader['content-length']
if not len(self._recv_buffer) >= content_len:
return
data = self._recv_buffer[:content_len]
self._recv_buffer = self._recv_buffer[content_len:]
if self.jsonheader['content-type'] == 'text/json':
encoding = self.jsonheader['content-encoding']
self.request = self._json_decode(data, encoding)
print('received request', repr(self.request), 'from', self.addr)
else:
# Binary or unknown content-type
self.request = data
print(f'received {self.jsonheader["content-type"]} request from',
self.addr)
# Set selector to listen for write events, we're done reading.
self._set_selector_events_mask('w')
После сохранения содержимого сообщения в переменную + data +
, + process_request () +
удаляет его из буфера приема. Затем, если тип контента JSON, он декодирует и десериализует его. Если это не так, для этого примера приложения предполагается, что это двоичный запрос, и просто печатается тип содержимого.
Последнее, что делает + process_request () +
, это модифицирует селектор для мониторинга только событий записи. В основном сценарии сервера + app-server.py +
сокет изначально настроен на мониторинг только событий чтения. Теперь, когда запрос был полностью обработан, мы больше не заинтересованы в чтении.
Теперь можно создать ответ и записать его в сокет. Когда сокет доступен для записи, + create_response () +
вызывается из + write () +
:
def create_response(self):
if self.jsonheader['content-type'] == 'text/json':
response = self._create_response_json_content()
else:
# Binary or unknown content-type
response = self._create_response_binary_content()
message = self._create_message(**response)
self.response_created = True
self._send_buffer += message
Ответ создается путем вызова других методов, в зависимости от типа содержимого. В этом примере приложения простой поиск по словарю выполняется для запросов JSON, когда + action == 'search' +
. Вы можете определить другие методы для своих собственных приложений, которые вызываются здесь.
После создания ответного сообщения устанавливается переменная состояния + self.response_created +
, поэтому + write () +
больше не вызывает + create_response () +
. Наконец, ответ добавляется в буфер отправки. Это видно и отправлено через + _write () +
.
Один хитрый момент, который нужно выяснить, — как закрыть соединение после написания ответа. Я поместил вызов + close () +
в методе + _write () +
:
def _write(self):
if self._send_buffer:
print('sending', repr(self._send_buffer), 'to', self.addr)
try:
# Should be ready to write
sent = self.sock.send(self._send_buffer)
except BlockingIOError:
# Resource temporarily unavailable (errno EWOULDBLOCK)
pass
else:
self._send_buffer = self._send_buffer[sent:]
# Close when the buffer is drained. The response has been sent.
if sent and not self._send_buffer:
self.close()
Хотя это несколько «скрыто», я думаю, что это приемлемый компромисс, учитывая, что класс «+ Message +» обрабатывает только одно сообщение на соединение. После того как ответ написан, серверу ничего не остается сделать. Он завершил свою работу.
Основной скрипт клиента
В основном скрипте клиента + app-client.py +
аргументы считываются из командной строки и используются для создания запросов и запуска соединений с сервером:
$ ./app-client.py
usage: ./app-client.py <host> <port> <action> <value>
Вот пример:
$ ./app-client.py 127.0.0.1 65432 search needle
После создания словаря, представляющего запрос из аргументов командной строки, хост, порт и словарь запроса передаются в + start_connection () +
:
def start_connection(host, port, request):
addr = (host, port)
print('starting connection to', addr)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
sock.connect_ex(addr)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
message = libclient.Message(sel, sock, addr, request)
sel.register(sock, events, data=message)
Для соединения с сервером создается сокет, а также объект + Message +
с использованием словаря + request +
.
Как и сервер, объект + Message +
связан с сокетом при вызове + sel.register () +
. Однако для клиента сокет изначально настроен на мониторинг событий чтения и записи. Как только запрос будет написан, мы изменим его, чтобы прослушивать только события чтения.
Такой подход дает нам то же преимущество, что и сервер: не тратить время процессора. После того, как запрос был отправлен, мы больше не интересуемся записью событий, поэтому нет причин просыпаться и обрабатывать их.
Класс сообщения клиента
В ссылке на раздел: # message-entry-point [Точка входа в сообщение] мы рассмотрели, как объект сообщения вызывался в действии, когда события сокета были готовы через + process_events () +
. Теперь давайте посмотрим, что происходит после того, как данные прочитаны и записаны в сокет, и сообщение готово для обработки клиентом.
Класс сообщений клиента находится в + libclient.py +
. Вы можете найти source код на GitHub.
Методы появляются в классе в том порядке, в котором происходит обработка сообщения.
Первая задача для клиента — поставить запрос в очередь:
def queue_request(self):
content = self.request['content']
content_type = self.request['type']
content_encoding = self.request['encoding']
if content_type == 'text/json':
req = {
'content_bytes': self._json_encode(content, content_encoding),
'content_type': content_type,
'content_encoding': content_encoding
}
else:
req = {
'content_bytes': content,
'content_type': content_type,
'content_encoding': content_encoding
}
message = self._create_message(**req)
self._send_buffer += message
self._request_queued = True
Словари, используемые для создания запроса, в зависимости от того, что было передано в командной строке, находятся в основном скрипте клиента, + app-client.py +
. Словарь запроса передается классу в качестве аргумента при создании объекта + Message +
.
Сообщение запроса создается и добавляется в буфер отправки, который затем просматривается и отправляется с помощью + _write () +
. Переменная состояния + self._request_queued +
установлена так, что + queue_request () +
больше не вызывается.
После того, как запрос был отправлен, клиент ожидает ответа от сервера.
Способы чтения и обработки сообщения на клиенте такие же, как на сервере. Когда данные ответа считываются из сокета, вызываются методы заголовка + process +
: + process_protoheader () +
и + process_jsonheader () +
.
Разница заключается в именовании окончательных методов + process +
и в том, что они обрабатывают ответ, а не создают его: + process_response () +
, + _process_response_json_content () +
и `+ _process_response_binary_content ( ) + `.
И последнее, но не менее важное, это последний вызов + process_response () +
:
def process_response(self):
# ...
# Close when response has been processed
self.close()
Заключение класса сообщений
Я завершу обсуждение класса «+ Message +», упомянув пару вещей, которые важно заметить с помощью нескольких поддерживающих методов.
Любые исключения, вызванные классом, перехватываются главным скриптом в его предложении + кроме +
:
try:
message.process_events(mask)
except Exception:
print('main: error: exception for',
f'{message.addr}:n{traceback.format_exc()}')
message.close()
Обратите внимание на последнюю строку: + message.close () +
.
Это действительно важная линия по нескольким причинам! Он не только гарантирует, что сокет закрыт, но + message.close () +
также удаляет сокет из списка + select () +
. Это значительно упрощает код в классе и уменьшает сложность. Если есть исключение или мы явно вызываем его сами, мы знаем, что + close () +
позаботится об очистке.
Методы + Message._read () +
и + Message._write () +
также содержат что-то интересное:
def _read(self):
try:
# Should be ready to read
data = self.sock.recv(4096)
except BlockingIOError:
# Resource temporarily unavailable (errno EWOULDBLOCK)
pass
else:
if data:
self._recv_buffer += data
else:
raise RuntimeError('Peer closed.')
Обратите внимание на строку + кроме +
: + кроме BlockingIOError: +
.
У + _write () +
тоже есть. Эти строки важны, потому что они ловят временную ошибку и пропускают ее, используя + pass +
. Временная ошибка возникает, когда сокет связывает: # blocking-Call [block], например, если он ожидает в сети или на другом конце соединения (его одноранговый узел).
Перехватывая и пропуская исключение с помощью + pass +
, + select () +
в конечном итоге снова вызовет нас, и мы получим еще один шанс прочитать или записать данные.
Запуск приложения-клиента и сервера
После всей этой тяжелой работы давайте повеселимся и начнем поиски!
В этих примерах я запускаю сервер, чтобы он прослушивал все интерфейсы, передавая пустую строку для аргумента + host +
. Это позволит мне запустить клиент и подключиться с виртуальной машины, которая находится в другой сети. Он эмулирует машину PowerPC с прямым порядком байтов.
Во-первых, давайте запустим сервер:
$ ./app-server.py '' 65432
listening on ('', 65432)
Теперь давайте запустим клиент и введем поиск. Давайте посмотрим, сможем ли мы найти его:
$ ./app-client.py 10.0.1.1 65432 search morpheus
starting connection to ('10.0.1.1', 65432)
sending b'x00d{"byteorder": "big", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 41}{"action": "search", "value": "morpheus"}' to ('10.0.1.1', 65432)
received response {'result': 'Follow the white rabbit. ????'} from ('10.0.1.1', 65432)
got result: Follow the white rabbit. ????
closing connection to ('10.0.1.1', 65432)
В моем терминале запущена оболочка с текстовой кодировкой Unicode (UTF-8), поэтому вышеприведенный вывод хорошо печатается с помощью emojis.
Давайте посмотрим, сможем ли мы найти щенков:
$ ./app-client.py 10.0.1.1 65432 search ????
starting connection to ('10.0.1.1', 65432)
sending b'x00d{"byteorder": "big", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 37}{"action": "search", "value": "xf0x9fx90xb6"}' to ('10.0.1.1', 65432)
received response {'result': '???? Playing ball! ????'} from ('10.0.1.1', 65432)
got result: ???? Playing ball! ????
closing connection to ('10.0.1.1', 65432)
Обратите внимание на строку байтов, отправленную по сети для запроса, в строке + send +
. Проще увидеть, если вы ищите байты, напечатанные в шестнадцатеричном формате, которые представляют смайлики щенка: + xf0 x9f x90 xb6 +
. Я смог enter emoji для поиска, так как мой терминал использует Unicode с кодировкой UTF-8.
Это показывает, что мы отправляем необработанные байты по сети, и они должны быть декодированы получателем для правильной интерпретации. Вот почему мы пошли на все проблемы, чтобы создать заголовок, который содержит тип содержимого и кодировку.
Вот выходные данные сервера из обоих клиентских подключений выше:
accepted connection from ('10.0.2.2', 55340)
received request {'action': 'search', 'value': 'morpheus'} from ('10.0.2.2', 55340)
sending b'x00g{"byteorder": "little", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 43}{"result": "Follow the white rabbit. xf0x9fx90xb0"}' to ('10.0.2.2', 55340)
closing connection to ('10.0.2.2', 55340)
accepted connection from ('10.0.2.2', 55338)
received request {'action': 'search', 'value': '????'} from ('10.0.2.2', 55338)
sending b'x00g{"byteorder": "little", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 37}{"result": "xf0x9fx90xbe Playing ball! xf0x9fx8fx90"}' to ('10.0.2.2', 55338)
closing connection to ('10.0.2.2', 55338)
Посмотрите на строку + send +
, чтобы увидеть байты, которые были записаны в сокет клиента. Это ответное сообщение сервера.
Вы также можете проверить отправку двоичных запросов на сервер, если аргумент + action +
отличается от + search +
:
$ ./app-client.py 10.0.1.1 65432 binary ????
starting connection to ('10.0.1.1', 65432)
sending b'x00|{"byteorder": "big", "content-type": "binary/custom-client-binary-type", "content-encoding": "binary", "content-length": 10}binaryxf0x9fx98x83' to ('10.0.1.1', 65432)
received binary/custom-server-binary-type response from ('10.0.1.1', 65432)
got response: b'First 10 bytes of request: binaryxf0x9fx98x83'
closing connection to ('10.0.1.1', 65432)
Поскольку запрос + content-type +
не является + text/json +
, сервер обрабатывает его как пользовательский двоичный тип и не выполняет JSON-декодирование. Он просто печатает + content-type +
и возвращает первые 10 байтов клиенту:
$ ./app-server.py '' 65432
listening on ('', 65432)
accepted connection from ('10.0.2.2', 55320)
received binary/custom-client-binary-type request from ('10.0.2.2', 55320)
sending b'x00x7f{"byteorder": "little", "content-type": "binary/custom-server-binary-type", "content-encoding": "binary", "content-length": 37}First 10 bytes of request: binaryxf0x9fx98x83' to ('10.0.2.2', 55320)
closing connection to ('10.0.2.2', 55320)
Поиск проблемы
Неизбежно, что-то не сработает, и вам будет интересно, что делать. Не волнуйся, это случается со всеми нами. Надеемся, что с помощью этого руководства, вашего отладчика и любимой поисковой системы вы сможете снова начать работу с частью исходного кода.
Если нет, то вашей первой остановкой должна стать документация Python socket module. Убедитесь, что вы прочитали всю документацию для каждой функции или метода, который вы вызываете. Кроме того, прочитайте ссылку: #reference [Ссылка] раздел для идей. В частности, проверьте ссылку: #errors [Errors] раздел.
Иногда дело не только в исходном коде. Исходный код может быть правильным, и это просто другой хост, клиент или сервер. Или это может быть сеть, например, маршрутизатор, брандмауэр или какое-то другое сетевое устройство, играющее «человек посередине».
Для этих типов проблем необходимы дополнительные инструменты. Ниже приведены несколько инструментов и утилит, которые могут помочь или, по крайней мере, предоставить некоторые подсказки.
ping
+ ping +
проверит, если хост жив и подключен к сети, отправив эхо-запрос ICMP. Он напрямую связывается со стеком протоколов TCP/IP операционной системы, поэтому он работает независимо от любого приложения, работающего на хосте.
Ниже приведен пример запуска ping на macOS:
$ ping -c 3 127.0.0.1
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.058 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.165 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.164 ms
--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.058/0.129/0.165/0.050 ms
Обратите внимание на статистику в конце вывода. Это может быть полезно, когда вы пытаетесь обнаружить периодически возникающие проблемы с подключением. Например, есть ли потеря пакетов? Сколько существует задержка (см. Время туда-обратно)?
Если между вами и другим хостом установлен брандмауэр, эхо-запрос эхо-запроса может быть запрещен. Некоторые администраторы брандмауэра реализуют политики, обеспечивающие это. Идея в том, что они не хотят, чтобы их хосты были доступны для обнаружения. Если это так, и вы добавили правила брандмауэра, чтобы узлы могли обмениваться данными, убедитесь, что правила также позволяют ICMP проходить между ними.
ICMP — это протокол, используемый + ping +
, но также протокол TCP и другие протоколы более низкого уровня, используемые для передачи сообщений об ошибках. Если вы испытываете странное поведение или медленные соединения, это может быть причиной.
Сообщения ICMP идентифицируются по типу и коду. Чтобы дать вам представление о важной информации, которую они несут, вот некоторые из них:
ICMP Type | ICMP Code | Description |
---|---|---|
8 |
0 |
Echo request |
0 |
0 |
Echo reply |
3 |
0 |
Destination network unreachable |
3 |
1 |
Destination host unreachable |
3 |
2 |
Destination protocol unreachable |
3 |
3 |
Destination port unreachable |
3 |
4 |
Fragmentation required, and DF flag set |
11 |
0 |
TTL expired in transit |
См. Статью Path[Path MTU Discovery для получения информации о фрагментации и сообщениях ICMP. Это пример чего-то, что может вызвать странное поведение, о котором я упоминал ранее.
NetStat
В ссылке на раздел: # view-socket-state [Просмотр состояния сокета] мы рассмотрели, как + netstat +
можно использовать для отображения информации о сокетах и их текущем состоянии. Эта утилита доступна в macOS, Linux и Windows.
Я не упоминал столбцы + Recv-Q +
и + Send-Q +
в выходных данных примера. В этих столбцах будет показано количество байтов, которые хранятся в сетевых буферах, которые находятся в очереди для передачи или получения, но по какой-то причине не были прочитаны или записаны удаленным или локальным приложением.
Другими словами, байты ожидают в сетевых буферах в очередях операционной системы. Одной из причин может быть то, что приложение связано с процессором или не может вызвать + socket.recv () +
или + socket.send () +
и обработать байты. Или могут быть проблемы с сетью, влияющие на связь, такие как перегрузка или сбой сетевого оборудования или кабелей.
Чтобы продемонстрировать это и увидеть, сколько данных я могу отправить до появления ошибки, я написал тестовый клиент, который подключается к тестовому серверу и многократно вызывает + socket.send () +
. Тестовый сервер никогда не вызывает + socket.recv () +
. Он просто принимает соединение. Это приводит к заполнению сетевых буферов на сервере, что в конечном итоге приводит к ошибке на клиенте.
Сначала я запустил сервер:
$ ./app-server-test.py 127.0.0.1 65432
listening on ('127.0.0.1', 65432)
Затем я запустил клиент. Давайте посмотрим, в чем ошибка:
$ ./app-client-test.py 127.0.0.1 65432 binary test
error: socket.send() blocking io exception for ('127.0.0.1', 65432):
BlockingIOError(35, 'Resource temporarily unavailable')
Вот вывод + netstat +
, в то время как клиент и сервер все еще работали, с клиентом, выводящим сообщение об ошибке выше несколько раз:
$ netstat -an | grep 65432
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 408300 0 127.0.0.1.65432 127.0.0.1.53225 ESTABLISHED
tcp4 0 269868 127.0.0.1.53225 127.0.0.1.65432 ESTABLISHED
tcp4 0 0 127.0.0.1.65432 *.* LISTEN
Первая запись — это сервер (+ Local Address +
имеет порт 65432):
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 408300 0 127.0.0.1.65432 127.0.0.1.53225 ESTABLISHED
Обратите внимание на + Recv-Q +
: + 408300 +
.
Вторая запись — это клиент (+ Foreign Address +
имеет порт 65432):
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 269868 127.0.0.1.53225 127.0.0.1.65432 ESTABLISHED
Обратите внимание на + Send-Q +
: + 269868 +
.
Конечно, клиент пытался записать байты, но сервер их не читал. Это привело к тому, что очередь сетевого буфера сервера заполнялась на стороне приема, а очередь сетевого буфера клиента заполнялась на стороне отправки.
Windows
Если вы работаете с Windows, есть набор утилит, которые вы обязательно должны проверить, если вы еще этого не сделали: Windows Sysinternals.
Одним из них является + TCPView.exe +
. TCPView является графическим + netstat +
для Windows. В дополнение к адресам, номерам портов и состоянию сокета, он покажет вам промежуточные итоги для количества пакетов и байтов, отправленных и полученных. Как и утилита Unix + lsof +
, вы также получаете имя и идентификатор процесса. Проверьте меню для других вариантов отображения.
Wireshark
Иногда вам нужно посмотреть, что происходит на проводе. Забудьте о том, что говорит журнал приложения или каково значение, которое возвращается из библиотечного вызова. Вы хотите увидеть, что на самом деле отправляется или принимается в сети. Так же, как отладчики, когда вам нужно это увидеть, нет замены.
Wireshark — это анализатор сетевых протоколов и приложение для захвата трафика, которое работает в MacOS, Linux и Windows и других. Существует версия графического интерфейса с именем + wireshark +
, а также терминальная текстовая версия с именем + tshark +
.
Выполнение захвата трафика — отличный способ наблюдать за тем, как приложение ведет себя в сети, и собирать данные о том, что оно отправляет и получает, как часто и как много. Вы также сможете увидеть, когда клиент или сервер закрывает или прерывает соединение или перестает отвечать на запросы. Эта информация может быть чрезвычайно полезна при устранении неполадок.
В Интернете есть много хороших учебных пособий и других ресурсов, которые познакомят вас с основами использования Wireshark и TShark.
Вот пример захвата трафика с использованием Wireshark на интерфейсе обратной связи:
Вот тот же пример, показанный выше с использованием + tshark +
:
$ tshark -i lo0 'tcp port 65432'
Capturing on 'Loopback'
1 0.000000 127.0.0.1 → 127.0.0.1 TCP 68 53942 → 65432 [SYN] Seq=0 Win=65535 Len=0 MSS=16344 WS=32 TSval=940533635 TSecr=0 SACK_PERM=1
2 0.000057 127.0.0.1 → 127.0.0.1 TCP 68 65432 → 53942 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=16344 WS=32 TSval=940533635 TSecr=940533635 SACK_PERM=1
3 0.000068 127.0.0.1 → 127.0.0.1 TCP 56 53942 → 65432 [ACK] Seq=1 Ack=1 Win=408288 Len=0 TSval=940533635 TSecr=940533635
4 0.000075 127.0.0.1 → 127.0.0.1 TCP 56 [TCP Window Update] 65432 → 53942 [ACK] Seq=1 Ack=1 Win=408288 Len=0 TSval=940533635 TSecr=940533635
5 0.000216 127.0.0.1 → 127.0.0.1 TCP 202 53942 → 65432 [PSH, ACK] Seq=1 Ack=1 Win=408288 Len=146 TSval=940533635 TSecr=940533635
6 0.000234 127.0.0.1 → 127.0.0.1 TCP 56 65432 → 53942 [ACK] Seq=1 Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
7 0.000627 127.0.0.1 → 127.0.0.1 TCP 204 65432 → 53942 [PSH, ACK] Seq=1 Ack=147 Win=408128 Len=148 TSval=940533635 TSecr=940533635
8 0.000649 127.0.0.1 → 127.0.0.1 TCP 56 53942 → 65432 [ACK] Seq=147 Ack=149 Win=408128 Len=0 TSval=940533635 TSecr=940533635
9 0.000668 127.0.0.1 → 127.0.0.1 TCP 56 65432 → 53942 [FIN, ACK] Seq=149 Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
10 0.000682 127.0.0.1 → 127.0.0.1 TCP 56 53942 → 65432 [ACK] Seq=147 Ack=150 Win=408128 Len=0 TSval=940533635 TSecr=940533635
11 0.000687 127.0.0.1 → 127.0.0.1 TCP 56 [TCP Dup ACK 6#1] 65432 → 53942 [ACK] Seq=150 Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
12 0.000848 127.0.0.1 → 127.0.0.1 TCP 56 53942 → 65432 [FIN, ACK] Seq=147 Ack=150 Win=408128 Len=0 TSval=940533635 TSecr=940533635
13 0.001004 127.0.0.1 → 127.0.0.1 TCP 56 65432 → 53942 [ACK] Seq=150 Ack=148 Win=408128 Len=0 TSval=940533635 TSecr=940533635
^C13 packets captured
Ссылка
Этот раздел служит общей ссылкой с дополнительной информацией и ссылками на внешние ресурсы.
Документация Python
-
Https://docs.python.org/3/library/socket.html[socket модуль Python]
-
Https://docs.python.org/3/howto/sockets.html#socket-howto[Socket Programming HOWTO Python’s]
ошибки
Следующее из документации модуля + socket +
в Python:
_
«Все ошибки вызывают исключения. Нормальные исключения для недопустимых типов аргументов и условий нехватки памяти могут быть вызваны; начиная с Python 3.3, ошибки, связанные с семантикой сокетов или адресов, приводят к + OSError +
или одному из его подклассов. ” (Source)
_
Вот некоторые распространенные ошибки, с которыми вы, вероятно, столкнетесь при работе с сокетами:
Exception | errno Constant |
Description |
---|---|---|
BlockingIOError |
EWOULDBLOCK |
Resource temporarily unavailable. For example, in non-blocking mode, when calling |
OSError |
EADDRINUSE |
Address already in use. Make sure there’s not another process running that’s using the same port number and your server is setting the socket option |
ConnectionResetError |
ECONNRESET |
Connection reset by peer. The remote process crashed or did not close its socket properly (unclean shutdown). Or there’s a firewall or other device in the network path that’s missing rules or misbehaving. |
TimeoutError |
ETIMEDOUT |
Operation timed out. No response from peer. |
ConnectionRefusedError |
ECONNREFUSED |
Connection refused. No application listening on specified port. |
Семейство адресов с сокетами
+ socket.AF_INET +
и + socket.AF_INET6 +
представляют адреса и семейства протоколов, используемые для первого аргумента + socket.socket () +
. API, которые используют адрес, ожидают, что он будет в определенном формате, в зависимости от того, был ли сокет создан с помощью + socket.AF_INET +
или + socket.AF_INET6 +
.
Address Family | Protocol | Address Tuple | Description |
---|---|---|---|
|
IPv4 |
|
|
|
IPv6 |
|
|
Обратите внимание на приведенную ниже выдержку из документации модуля сокетов Python относительно значения + host +
для кортежа адресов:
_
«Для адресов IPv4 вместо адреса хоста принимаются две специальные формы: пустая строка представляет` + INADDR_ANY + , а строка
+ ‘<broadcast>’ + представляет
+ INADDR_BROADCAST + `. Такое поведение несовместимо с IPv6, поэтому вы можете избежать этого, если намереваетесь поддерживать IPv6 с вашими программами Python ». (Source)
_
Я использовал сокеты IPv4 в этом руководстве, но если ваша сеть поддерживает это, попробуйте протестировать и использовать IPv6, если это возможно. Один из способов легко это сделать — использовать функцию https://docs.python.org/3/library/socket.html#socket.getaddrinfo [socket.getaddrinfo ()]. Он переводит аргументы + host +
и + port +
в последовательность из 5 кортежей, которая содержит все необходимые аргументы для создания сокета, подключенного к этому сервису. + socket.getaddrinfo () +
будет понимать и интерпретировать переданные IPv6-адреса и имена хостов, которые преобразуются в IPv6-адреса, в дополнение к IPv4.
В следующем примере информация об адресе для TCP-соединения возвращается в + example.org +
через порт + 80 +
>>>
>>> socket.getaddrinfo("example.org", 80, proto=socket.IPPROTO_TCP)
[(<AddressFamily.AF_INET6: 10>, <SocketType.SOCK_STREAM: 1>,
6, '', ('2606:2800:220:1:248:1893:25c8:1946', 80, 0, 0)),
(<AddressFamily.AF_INET: 2>, <SocketType.SOCK_STREAM: 1>,
6, '', ('93.184.216.34', 80))]
Результаты могут отличаться в вашей системе, если IPv6 не включен. Возвращенные выше значения можно использовать, передав их в + socket.socket () +
и + socket.connect () +
. Пример клиента и сервера приведен в разделе Example документации по модулю сокетов Python.
Использование имен хостов
Для контекста этот раздел в основном применяется к использованию имен хостов с + bind () +
и + connect () +
или + connect_ex () +
), когда вы собираетесь использовать интерфейс обратной петли «localhost». Тем не менее, он применяется всякий раз, когда вы используете имя хоста, и ожидается, что оно разрешится по определенному адресу и будет иметь особое значение для вашего приложения, которое влияет на его поведение или предположения. Это отличается от типичного сценария, когда клиент использует имя хоста для подключения к серверу, который разрешен DNS, например, www.example.com.
Следующее из документации модуля + socket +
в Python:
_
«Если вы используете имя хоста в части хоста адреса сокета IPv4/v6, программа может показывать недетерминированное поведение, так как Python использует первый адрес, возвращенный из разрешения DNS. Адрес сокета будет по-разному преобразован в фактический адрес IPv4/v6, в зависимости от результатов разрешения DNS и/или конфигурации хоста. Для детерминированного поведения используйте числовой адрес в части хоста ». (Source)
_
- Стандартное соглашение для имени «https://en.wikipedia.org/wiki/Localhost[localhost]» предусматривает разрешение на «+ 127.0.0.1 » или «
-
1 +», интерфейс обратной петли. Это, скорее всего, будет иметь место для вас в вашей системе, но, возможно, нет. Это зависит от того, как ваша система настроена для разрешения имен. Как и во всем, что касается ИТ, всегда есть исключения, и нет никаких гарантий, что использование имени «localhost» будет подключаться к интерфейсу обратной связи.
Например, в Linux см. + Man nsswitch.conf +
, файл конфигурации переключателя службы имен. Еще одно место для проверки в macOS и Linux — файл +/etc/hosts +
. В Windows смотрите + C: Windows System32 drivers etc hosts +
. Файл + hosts +
содержит статическую таблицу имен для сопоставления адресов в простом текстовом формате. DNS — еще одна часть головоломки.
Интересно, что на момент написания этой статьи (июнь 2018 года) есть проект RFC Let ‘localhost’ be localhost , в котором обсуждаются условные обозначения, предположения и безопасность, связанные с использованием названия «localhost».
Важно понимать, что когда вы используете имена хостов в своем приложении, возвращаемый адрес (адреса) может быть буквально любым. Не делайте предположений относительно имени, если у вас есть приложение, чувствительное к безопасности. В зависимости от вашего приложения и среды, это может или не может беспокоить вас.
*Примечание:* Меры предосторожности и лучшие практики по-прежнему применяются, даже если ваше приложение не «чувствительно к безопасности». Если ваше приложение обращается к сети, оно должно быть защищено и поддержано. Это означает, как минимум:
-
Системные обновления программного обеспечения и исправления безопасности применяются регулярно, включая Python. Вы используете сторонние библиотеки? Если это так, убедитесь, что они также проверены и обновлены.
-
Если возможно, используйте выделенный или основанный на хосте брандмауэр, чтобы ограничить подключения только к доверенным системам.
-
Какие DNS-серверы настроены? Вы доверяете им и их администраторам?
-
Перед тем, как вызывать другой код, обрабатывающий эти данные, убедитесь, что данные запроса максимально проверены и проверены. Используйте (нечеткие) тесты для этого и регулярно запускайте их.
Независимо от того, используете ли вы имена хостов, если ваше приложение должно поддерживать безопасные соединения (шифрование и аутентификация), вы, вероятно, захотите изучить использование TLS , Это отдельная тема, выходящая за рамки данного руководства. Для начала ознакомьтесь с документацией по Python ssl module. Это тот же протокол, который используется вашим веб-браузером для безопасного подключения к веб-сайтам.
С учетом интерфейсов, IP-адресов и разрешения имен существует много переменных. Что вы должны сделать? Вот несколько рекомендаций, которые вы можете использовать, если у вас нет процесса рассмотрения сетевых приложений:
Application | Usage | Recommendation |
---|---|---|
Server |
loopback interface |
Use an IP address, for example, |
Server |
ethernet interface |
Use an IP address, for example, |
Client |
loopback interface |
Use an IP address, for example, |
Client |
ethernet interface |
Use an IP address for consistency and non-reliance on name resolution. For the typical case, use a hostname. See the security note above. |
Для клиентов или серверов, если вам нужно аутентифицировать хост, к которому вы подключаетесь, изучите использование TLS.
Блокировка звонков
Функция сокета или метод, который временно приостанавливает ваше приложение, является блокирующим вызовом. Например, + accept () +
, + connect () +
, + send () +
и + recv () +
“block”). Они не возвращаются сразу. Блокирующие вызовы должны ждать завершения системных вызовов (I/O), прежде чем они смогут вернуть значение. Таким образом, вы, вызывающий абонент, блокируетесь до тех пор, пока они не завершат работу или не произойдет таймаут или другая ошибка.
Блокирующие вызовы сокетов могут быть установлены в неблокирующий режим, поэтому они немедленно возвращаются. Если вы сделаете это, вам потребуется как минимум рефакторинг или редизайн вашего приложения для обработки операции сокета, когда она будет готова.
Поскольку вызов немедленно возвращается, данные могут быть не готовы. Вызываемый ожидает в сети и не успел завершить свою работу. Если это так, текущим статусом является + errno +
значение + socket.EWOULDBLOCK +
. Неблокирующий режим поддерживается с помощью https://docs.python.org/3/library/socket.html#socket.socket.setblocking [setblocking ()].
По умолчанию сокеты всегда создаются в режиме блокировки. См. Https://docs.python.org/3/library/socket.html#notes-on-socket-timeouts[Notes на время ожидания сокета] для описания трех режимов.
Закрытие соединения
Интересно отметить, что для TCP или клиента вполне законно закрывать свою сторону соединения, в то время как другая сторона остается открытой. Это называется «полуоткрытым» соединением. Решение приложения является желательным или нет. В общем, это не так. В этом состоянии сторона, которая закрыла свой конец соединения, больше не может отправлять данные. Они могут только получить это.
Я не рекомендую использовать этот подход, но в качестве примера HTTP использует заголовок с именем «Соединение», который используется для стандартизации того, как приложения должны закрывать или сохранять открытые соединения. Для получения дополнительной информации см. Https://tools.ietf.org/html/rfc7230#section-6.3[section 6.3 в RFC 7230, Протокол передачи гипертекста (HTTP/1.1): синтаксис сообщения и маршрутизация].
При разработке и написании вашего приложения и его протокола уровня приложений хорошей идеей будет пойти дальше и выяснить, как вы ожидаете, что соединения будут закрыты. Иногда это очевидно и просто, или это то, что может потребовать некоторого первоначального прототипирования и тестирования. Это зависит от приложения и способа обработки цикла сообщений с ожидаемыми данными. Просто убедитесь, что розетки всегда закрыты своевременно после завершения работы.
Байтовый порядок
См. Https://en.wikipedia.org/wiki/Endianness[Wikipedia статью о порядке байтов], чтобы узнать, как разные процессоры хранят порядок байтов в памяти. При интерпретации отдельных байтов это не проблема. Однако при обработке нескольких байтов, которые считываются и обрабатываются как одно значение, например 4-байтовое целое число, порядок следования байтов должен быть обратным, если вы общаетесь с машиной, которая использует другой порядок байтов.
Порядок байтов также важен для текстовых строк, которые представлены в виде многобайтовых последовательностей, таких как Юникод. Если вы не всегда используете «true», строгий ASCII и управляете реализациями клиента и сервера, вам, вероятно, лучше использовать Unicode с кодировкой, подобной UTF-8 или тот, который поддерживает метку заказа byte (BOM).
Важно четко определить кодировку, используемую в протоколе уровня приложения. Вы можете сделать это, указав, что весь текст имеет формат UTF-8 или используя заголовок «content-encoding», который задает кодировку. Это препятствует тому, чтобы ваше приложение обнаружило кодировку, которую вы должны избегать, если это возможно.
Это становится проблематичным, когда имеются данные, которые хранятся в файлах или базе данных, и нет доступных метаданных, определяющих их кодировку. Когда данные передаются в другую конечную точку, она должна попытаться определить кодировку. Для обсуждения см. Https://en.wikipedia.org/wiki/Unicode[Wikipedia статью Unicode], в которой содержится ссылка на RFC 3629: UTF-8, формат преобразования ISO 10646:
_
«Однако RFC 3629, стандарт UTF-8, рекомендует запрещать метки порядка байтов в протоколах, использующих UTF-8, но обсуждает случаи, когда это может быть невозможно. Кроме того, большое ограничение на возможные шаблоны в UTF-8 (например, не может быть никаких одиночных байтов с установленным старшим битом) означает, что должна быть возможность отличить UTF-8 от других кодировок символов, не полагаясь на BOM ». (Source)
_
Вывод из этого заключается в том, чтобы всегда хранить кодировку, используемую для данных, обрабатываемых вашим приложением, если она может варьироваться. Другими словами, попытайтесь как-то сохранить кодировку в виде метаданных, если это не всегда UTF-8 или какая-либо другая кодировка с спецификацией. Затем вы можете отправить эту кодировку в заголовке вместе с данными, чтобы сообщить получателю, что это такое.
Порядок байтов, используемый в TCP/IP, называется big-endian и называется сетевым порядком. Сетевой порядок используется для представления целых чисел на нижних уровнях стека протоколов, таких как IP-адреса и номера портов. Модуль сокетов Python включает в себя функции, которые преобразуют целые числа в сетевой и из байтовых порядков:
Function | Description |
---|---|
|
Convert 32-bit positive integers from network to host byte order. On machines where the host byte order is the same as network byte order, this is a no-op; otherwise, it performs a 4-byte swap operation. |
|
Convert 16-bit positive integers from network to host byte order. On machines where the host byte order is the same as network byte order, this is a no-op; otherwise, it performs a 2-byte swap operation. |
|
Convert 32-bit positive integers from host to network byte order. On machines where the host byte order is the same as network byte order, this is a no-op; otherwise, it performs a 4-byte swap operation. |
|
Convert 16-bit positive integers from host to network byte order. On machines where the host byte order is the same as network byte order, this is a no-op; otherwise, it performs a 2-byte swap operation. |
Вы также можете использовать struct module для упаковки и распаковки двоичных данных, используя строки формата:
import struct
network_byteorder_int = struct.pack('>H', 256)
python_int = struct.unpack('>H', network_byteorder_int)[0]
Заключение
Мы рассмотрели много вопросов в этом уроке. Сеть и розетки — это большие предметы. Если вы плохо знакомы с сетью или сокетами, не расстраивайтесь от всех терминов и сокращений.
Есть много вещей, с которыми нужно ознакомиться, чтобы понять, как все работает вместе. Однако, как и в Python, он станет более понятным, когда вы узнаете отдельные части и будете проводить с ними больше времени.
Мы рассмотрели низкоуровневый API сокетов в модуле Python + socket +
и увидели, как его можно использовать для создания клиент-серверных приложений. Мы также создали наш собственный класс и использовали его в качестве протокола прикладного уровня для обмена сообщениями и данными между конечными точками. Вы можете использовать этот класс и использовать его для изучения и помощи в создании и упрощении создания собственных приложений для сокетов.
Поздравляем с завершением! Теперь вы уже на пути к использованию сокетов в своих приложениях.
Я надеюсь, что это руководство дало вам информацию, примеры и вдохновение, необходимые для того, чтобы начать вас в процессе разработки сокетов.
This blog post was originally posted on JetBrains .NET blog.
Rider consists of several processes that send messages to each other via sockets. To ensure the reliability of the whole application, it’s important to properly handle all the socket errors. In our codebase, we had the following code which was adopted from Mono Debugger Libs and helps us communicate with debugger processes:
protected virtual bool ShouldRetryConnection (Exception ex, int attemptNumber)
{
var sx = ex as SocketException;
if (sx != null) {
if (sx.ErrorCode == 10061) //connection refused
return true;
}
return false;
}
In the case of a failed connection because of a “ConnectionRefused” error, we are retrying the connection attempt. It works fine with .NET Framework and Mono. However, once we migrated to .NET Core, this method no longer correctly detects the “connection refused” situation on Linux and macOS. If we open the SocketException
documentation, we will learn that this class has three different properties with error codes:
SocketError SocketErrorCode
: Gets the error code that is associated with this exception.int ErrorCode
: Gets the error code that is associated with this exception.int NativeErrorCode
: Gets the Win32 error code associated with this exception.
What’s the difference between these properties? Should we expect different values on different runtimes or different operating systems? Which one should we use in production? Why do we have problems with ShouldRetryConnection
on .NET Core? Let’s figure it all out!
Digging into the problem
Let’s start with the following program, which prints error code property values for SocketError.ConnectionRefused
:
var se = new SocketException((int) SocketError.ConnectionRefused);
Console.WriteLine((int)se.SocketErrorCode);
Console.WriteLine(se.ErrorCode);
Console.WriteLine(se.NativeErrorCode);
If we run it on Windows, we will get the same value on .NET Framework, Mono, and .NET Core:
SocketErrorCode | ErrorCode | NativeErrorCode | |
.NET Framework | 10061 | 10061 | 10061 |
Mono | 10061 | 10061 | 10061 |
.NET Core | 10061 | 10061 | 10061 |
10061 corresponds to the code of the connection refused socket error code in Windows (also known as WSAECONNREFUSED
).
Now let’s run the same program on Linux:
SocketErrorCode | ErrorCode | NativeErrorCode | |
Mono | 10061 | 10061 | 10061 |
.NET Core | 10061 | 111 | 111 |
As you can see, Mono returns Windows-compatible error codes. The situation with .NET Core is different: it returns a Windows-compatible value for SocketErrorCode (10061) and a Linux-like value for ErrorCode
and NativeErrorCode
(111).
Finally, let’s check macOS:
SocketErrorCode | ErrorCode | NativeErrorCode | |
Mono | 10061 | 10061 | 10061 |
.NET Core | 10061 | 61 | 61 |
Here, Mono is completely Windows-compatible again, but .NET Core returns 61 for ErrorCode
and NativeErrorCode
.
In the IBM Knowledge Center, we can find a few more values for the connection refused error code from the Unix world (also known as ECONNREFUSED
):
- AIX: 79
- HP-UX: 239
- Solaris: 146
For a better understanding of what’s going on, let’s check out the source code of all the properties.
SocketErrorCode
SocketException.SocketErrorCode
returns a value from the SocketError
enum. The numerical values of the enum elements are the same on all the runtimes (see its implementation in .NET Framework, .NET Core 3.1.3, and Mono 6.8.0.105):
public enum SocketError
{
SocketError = -1, // 0xFFFFFFFF
Success = 0,
OperationAborted = 995, // 0x000003E3
IOPending = 997, // 0x000003E5
Interrupted = 10004, // 0x00002714
AccessDenied = 10013, // 0x0000271D
Fault = 10014, // 0x0000271E
InvalidArgument = 10022, // 0x00002726
TooManyOpenSockets = 10024, // 0x00002728
WouldBlock = 10035, // 0x00002733
InProgress = 10036, // 0x00002734
AlreadyInProgress = 10037, // 0x00002735
NotSocket = 10038, // 0x00002736
DestinationAddressRequired = 10039, // 0x00002737
MessageSize = 10040, // 0x00002738
ProtocolType = 10041, // 0x00002739
ProtocolOption = 10042, // 0x0000273A
ProtocolNotSupported = 10043, // 0x0000273B
SocketNotSupported = 10044, // 0x0000273C
OperationNotSupported = 10045, // 0x0000273D
ProtocolFamilyNotSupported = 10046, // 0x0000273E
AddressFamilyNotSupported = 10047, // 0x0000273F
AddressAlreadyInUse = 10048, // 0x00002740
AddressNotAvailable = 10049, // 0x00002741
NetworkDown = 10050, // 0x00002742
NetworkUnreachable = 10051, // 0x00002743
NetworkReset = 10052, // 0x00002744
ConnectionAborted = 10053, // 0x00002745
ConnectionReset = 10054, // 0x00002746
NoBufferSpaceAvailable = 10055, // 0x00002747
IsConnected = 10056, // 0x00002748
NotConnected = 10057, // 0x00002749
Shutdown = 10058, // 0x0000274A
TimedOut = 10060, // 0x0000274C
ConnectionRefused = 10061, // 0x0000274D
HostDown = 10064, // 0x00002750
HostUnreachable = 10065, // 0x00002751
ProcessLimit = 10067, // 0x00002753
SystemNotReady = 10091, // 0x0000276B
VersionNotSupported = 10092, // 0x0000276C
NotInitialized = 10093, // 0x0000276D
Disconnecting = 10101, // 0x00002775
TypeNotFound = 10109, // 0x0000277D
HostNotFound = 11001, // 0x00002AF9
TryAgain = 11002, // 0x00002AFA
NoRecovery = 11003, // 0x00002AFB
NoData = 11004, // 0x00002AFC
}
These values correspond to the Windows Sockets Error Codes.
NativeErrorCode
In .NET Framework and Mono, SocketErrorCode
and NativeErrorCode
always have the same values:
public SocketError SocketErrorCode {
//
// the base class returns the HResult with this property
// we need the Win32 Error Code, hence the override.
//
get {
return (SocketError)NativeErrorCode;
}
}
In .NET Core, the native code is calculated in the constructor (see SocketException.cs#L20):
public SocketException(int errorCode) : this((SocketError)errorCode)
// ...
internal SocketException(SocketError socketError) : base(GetNativeErrorForSocketError(socketError))
The Windows implementation of GetNativeErrorForSocketError
is trivial (see SocketException.Windows.cs):
private static int GetNativeErrorForSocketError(SocketError error)
{
// SocketError values map directly to Win32 error codes
return (int)error;
}
The Unix implementation is more complicated (see SocketException.Unix.cs):
private static int GetNativeErrorForSocketError(SocketError error)
{
int nativeErr = (int)error;
if (error != SocketError.SocketError)
{
Interop.Error interopErr;
// If an interop error was not found, then don't invoke Info().RawErrno as that will fail with assert.
if (SocketErrorPal.TryGetNativeErrorForSocketError(error, out interopErr))
{
nativeErr = interopErr.Info().RawErrno;
}
}
return nativeErr;
}
TryGetNativeErrorForSocketError
should convert SocketError
to the native Unix error code.
Unfortunately, there exists no unequivocal mapping between Windows and Unix error codes. As such, the .NET team decided to create a Dictionary
that maps error codes in the best possible way (see SocketErrorPal.Unix.cs):
private const int NativeErrorToSocketErrorCount = 42;
private const int SocketErrorToNativeErrorCount = 40;
// No Interop.Errors are included for the following SocketErrors, as there's no good mapping:
// - SocketError.NoRecovery
// - SocketError.NotInitialized
// - SocketError.ProcessLimit
// - SocketError.SocketError
// - SocketError.SystemNotReady
// - SocketError.TypeNotFound
// - SocketError.VersionNotSupported
private static readonly Dictionary<Interop.Error, SocketError> s_nativeErrorToSocketError = new Dictionary<Interop.Error, SocketError>(NativeErrorToSocketErrorCount)
{
{ Interop.Error.EACCES, SocketError.AccessDenied },
{ Interop.Error.EADDRINUSE, SocketError.AddressAlreadyInUse },
{ Interop.Error.EADDRNOTAVAIL, SocketError.AddressNotAvailable },
{ Interop.Error.EAFNOSUPPORT, SocketError.AddressFamilyNotSupported },
{ Interop.Error.EAGAIN, SocketError.WouldBlock },
{ Interop.Error.EALREADY, SocketError.AlreadyInProgress },
{ Interop.Error.EBADF, SocketError.OperationAborted },
{ Interop.Error.ECANCELED, SocketError.OperationAborted },
{ Interop.Error.ECONNABORTED, SocketError.ConnectionAborted },
{ Interop.Error.ECONNREFUSED, SocketError.ConnectionRefused },
{ Interop.Error.ECONNRESET, SocketError.ConnectionReset },
{ Interop.Error.EDESTADDRREQ, SocketError.DestinationAddressRequired },
{ Interop.Error.EFAULT, SocketError.Fault },
{ Interop.Error.EHOSTDOWN, SocketError.HostDown },
{ Interop.Error.ENXIO, SocketError.HostNotFound }, // not perfect, but closest match available
{ Interop.Error.EHOSTUNREACH, SocketError.HostUnreachable },
{ Interop.Error.EINPROGRESS, SocketError.InProgress },
{ Interop.Error.EINTR, SocketError.Interrupted },
{ Interop.Error.EINVAL, SocketError.InvalidArgument },
{ Interop.Error.EISCONN, SocketError.IsConnected },
{ Interop.Error.EMFILE, SocketError.TooManyOpenSockets },
{ Interop.Error.EMSGSIZE, SocketError.MessageSize },
{ Interop.Error.ENETDOWN, SocketError.NetworkDown },
{ Interop.Error.ENETRESET, SocketError.NetworkReset },
{ Interop.Error.ENETUNREACH, SocketError.NetworkUnreachable },
{ Interop.Error.ENFILE, SocketError.TooManyOpenSockets },
{ Interop.Error.ENOBUFS, SocketError.NoBufferSpaceAvailable },
{ Interop.Error.ENODATA, SocketError.NoData },
{ Interop.Error.ENOENT, SocketError.AddressNotAvailable },
{ Interop.Error.ENOPROTOOPT, SocketError.ProtocolOption },
{ Interop.Error.ENOTCONN, SocketError.NotConnected },
{ Interop.Error.ENOTSOCK, SocketError.NotSocket },
{ Interop.Error.ENOTSUP, SocketError.OperationNotSupported },
{ Interop.Error.EPERM, SocketError.AccessDenied },
{ Interop.Error.EPIPE, SocketError.Shutdown },
{ Interop.Error.EPFNOSUPPORT, SocketError.ProtocolFamilyNotSupported },
{ Interop.Error.EPROTONOSUPPORT, SocketError.ProtocolNotSupported },
{ Interop.Error.EPROTOTYPE, SocketError.ProtocolType },
{ Interop.Error.ESOCKTNOSUPPORT, SocketError.SocketNotSupported },
{ Interop.Error.ESHUTDOWN, SocketError.Disconnecting },
{ Interop.Error.SUCCESS, SocketError.Success },
{ Interop.Error.ETIMEDOUT, SocketError.TimedOut },
};
private static readonly Dictionary<SocketError, Interop.Error> s_socketErrorToNativeError = new Dictionary<SocketError, Interop.Error>(SocketErrorToNativeErrorCount)
{
// This is *mostly* an inverse mapping of s_nativeErrorToSocketError. However, some options have multiple mappings and thus
// can't be inverted directly. Other options don't have a mapping from native to SocketError, but when presented with a SocketError,
// we want to provide the closest relevant Error possible, e.g. EINPROGRESS maps to SocketError.InProgress, and vice versa, but
// SocketError.IOPending also maps closest to EINPROGRESS. As such, roundtripping won't necessarily provide the original value 100% of the time,
// but it's the best we can do given the mismatch between Interop.Error and SocketError.
{ SocketError.AccessDenied, Interop.Error.EACCES}, // could also have been EPERM
{ SocketError.AddressAlreadyInUse, Interop.Error.EADDRINUSE },
{ SocketError.AddressNotAvailable, Interop.Error.EADDRNOTAVAIL },
{ SocketError.AddressFamilyNotSupported, Interop.Error.EAFNOSUPPORT },
{ SocketError.AlreadyInProgress, Interop.Error.EALREADY },
{ SocketError.ConnectionAborted, Interop.Error.ECONNABORTED },
{ SocketError.ConnectionRefused, Interop.Error.ECONNREFUSED },
{ SocketError.ConnectionReset, Interop.Error.ECONNRESET },
{ SocketError.DestinationAddressRequired, Interop.Error.EDESTADDRREQ },
{ SocketError.Disconnecting, Interop.Error.ESHUTDOWN },
{ SocketError.Fault, Interop.Error.EFAULT },
{ SocketError.HostDown, Interop.Error.EHOSTDOWN },
{ SocketError.HostNotFound, Interop.Error.EHOSTNOTFOUND },
{ SocketError.HostUnreachable, Interop.Error.EHOSTUNREACH },
{ SocketError.InProgress, Interop.Error.EINPROGRESS },
{ SocketError.Interrupted, Interop.Error.EINTR },
{ SocketError.InvalidArgument, Interop.Error.EINVAL },
{ SocketError.IOPending, Interop.Error.EINPROGRESS },
{ SocketError.IsConnected, Interop.Error.EISCONN },
{ SocketError.MessageSize, Interop.Error.EMSGSIZE },
{ SocketError.NetworkDown, Interop.Error.ENETDOWN },
{ SocketError.NetworkReset, Interop.Error.ENETRESET },
{ SocketError.NetworkUnreachable, Interop.Error.ENETUNREACH },
{ SocketError.NoBufferSpaceAvailable, Interop.Error.ENOBUFS },
{ SocketError.NoData, Interop.Error.ENODATA },
{ SocketError.NotConnected, Interop.Error.ENOTCONN },
{ SocketError.NotSocket, Interop.Error.ENOTSOCK },
{ SocketError.OperationAborted, Interop.Error.ECANCELED },
{ SocketError.OperationNotSupported, Interop.Error.ENOTSUP },
{ SocketError.ProtocolFamilyNotSupported, Interop.Error.EPFNOSUPPORT },
{ SocketError.ProtocolNotSupported, Interop.Error.EPROTONOSUPPORT },
{ SocketError.ProtocolOption, Interop.Error.ENOPROTOOPT },
{ SocketError.ProtocolType, Interop.Error.EPROTOTYPE },
{ SocketError.Shutdown, Interop.Error.EPIPE },
{ SocketError.SocketNotSupported, Interop.Error.ESOCKTNOSUPPORT },
{ SocketError.Success, Interop.Error.SUCCESS },
{ SocketError.TimedOut, Interop.Error.ETIMEDOUT },
{ SocketError.TooManyOpenSockets, Interop.Error.ENFILE }, // could also have been EMFILE
{ SocketError.TryAgain, Interop.Error.EAGAIN }, // not a perfect mapping, but better than nothing
{ SocketError.WouldBlock, Interop.Error.EAGAIN },
};
internal static bool TryGetNativeErrorForSocketError(SocketError error, out Interop.Error errno)
{
return s_socketErrorToNativeError.TryGetValue(error, out errno);
}
Once we have an instance of Interop.Error
, we call interopErr.Info().RawErrno
. The implementation of RawErrno can be found in Interop.Errors.cs:
internal int RawErrno
{
get { return _rawErrno == -1 ? (_rawErrno = Interop.Sys.ConvertErrorPalToPlatform(_error)) : _rawErrno; }
}
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_ConvertErrorPalToPlatform")]
internal static extern int ConvertErrorPalToPlatform(Error error);
Here we are jumping to the native function SystemNative_ConvertErrorPalToPlatform that maps Error to the native integer code that is defined in errno.h. You can get all the values using the errno util. Here is a typical output on Linux:
$ errno -ls
EPERM 1 Operation not permitted
ENOENT 2 No such file or directory
ESRCH 3 No such process
EINTR 4 Interrupted system call
EIO 5 Input/output error
ENXIO 6 No such device or address
E2BIG 7 Argument list too long
ENOEXEC 8 Exec format error
EBADF 9 Bad file descriptor
ECHILD 10 No child processes
EAGAIN 11 Resource temporarily unavailable
ENOMEM 12 Cannot allocate memory
EACCES 13 Permission denied
EFAULT 14 Bad address
ENOTBLK 15 Block device required
EBUSY 16 Device or resource busy
EEXIST 17 File exists
EXDEV 18 Invalid cross-device link
ENODEV 19 No such device
ENOTDIR 20 Not a directory
EISDIR 21 Is a directory
EINVAL 22 Invalid argument
ENFILE 23 Too many open files in system
EMFILE 24 Too many open files
ENOTTY 25 Inappropriate ioctl for device
ETXTBSY 26 Text file busy
EFBIG 27 File too large
ENOSPC 28 No space left on device
ESPIPE 29 Illegal seek
EROFS 30 Read-only file system
EMLINK 31 Too many links
EPIPE 32 Broken pipe
EDOM 33 Numerical argument out of domain
ERANGE 34 Numerical result out of range
EDEADLK 35 Resource deadlock avoided
ENAMETOOLONG 36 File name too long
ENOLCK 37 No locks available
ENOSYS 38 Function not implemented
ENOTEMPTY 39 Directory not empty
ELOOP 40 Too many levels of symbolic links
EWOULDBLOCK 11 Resource temporarily unavailable
ENOMSG 42 No message of desired type
EIDRM 43 Identifier removed
ECHRNG 44 Channel number out of range
EL2NSYNC 45 Level 2 not synchronized
EL3HLT 46 Level 3 halted
EL3RST 47 Level 3 reset
ELNRNG 48 Link number out of range
EUNATCH 49 Protocol driver not attached
ENOCSI 50 No CSI structure available
EL2HLT 51 Level 2 halted
EBADE 52 Invalid exchange
EBADR 53 Invalid request descriptor
EXFULL 54 Exchange full
ENOANO 55 No anode
EBADRQC 56 Invalid request code
EBADSLT 57 Invalid slot
EDEADLOCK 35 Resource deadlock avoided
EBFONT 59 Bad font file format
ENOSTR 60 Device not a stream
ENODATA 61 No data available
ETIME 62 Timer expired
ENOSR 63 Out of streams resources
ENONET 64 Machine is not on the network
ENOPKG 65 Package not installed
EREMOTE 66 Object is remote
ENOLINK 67 Link has been severed
EADV 68 Advertise error
ESRMNT 69 Srmount error
ECOMM 70 Communication error on send
EPROTO 71 Protocol error
EMULTIHOP 72 Multihop attempted
EDOTDOT 73 RFS specific error
EBADMSG 74 Bad message
EOVERFLOW 75 Value too large for defined data type
ENOTUNIQ 76 Name not unique on network
EBADFD 77 File descriptor in bad state
EREMCHG 78 Remote address changed
ELIBACC 79 Can not access a needed shared library
ELIBBAD 80 Accessing a corrupted shared library
ELIBSCN 81 .lib section in a.out corrupted
ELIBMAX 82 Attempting to link in too many shared libraries
ELIBEXEC 83 Cannot exec a shared library directly
EILSEQ 84 Invalid or incomplete multibyte or wide character
ERESTART 85 Interrupted system call should be restarted
ESTRPIPE 86 Streams pipe error
EUSERS 87 Too many users
ENOTSOCK 88 Socket operation on non-socket
EDESTADDRREQ 89 Destination address required
EMSGSIZE 90 Message too long
EPROTOTYPE 91 Protocol wrong type for socket
ENOPROTOOPT 92 Protocol not available
EPROTONOSUPPORT 93 Protocol not supported
ESOCKTNOSUPPORT 94 Socket type not supported
EOPNOTSUPP 95 Operation not supported
EPFNOSUPPORT 96 Protocol family not supported
EAFNOSUPPORT 97 Address family not supported by protocol
EADDRINUSE 98 Address already in use
EADDRNOTAVAIL 99 Cannot assign requested address
ENETDOWN 100 Network is down
ENETUNREACH 101 Network is unreachable
ENETRESET 102 Network dropped connection on reset
ECONNABORTED 103 Software caused connection abort
ECONNRESET 104 Connection reset by peer
ENOBUFS 105 No buffer space available
EISCONN 106 Transport endpoint is already connected
ENOTCONN 107 Transport endpoint is not connected
ESHUTDOWN 108 Cannot send after transport endpoint shutdown
ETOOMANYREFS 109 Too many references: cannot splice
ETIMEDOUT 110 Connection timed out
ECONNREFUSED 111 Connection refused
EHOSTDOWN 112 Host is down
EHOSTUNREACH 113 No route to host
EALREADY 114 Operation already in progress
EINPROGRESS 115 Operation now in progress
ESTALE 116 Stale file handle
EUCLEAN 117 Structure needs cleaning
ENOTNAM 118 Not a XENIX named type file
ENAVAIL 119 No XENIX semaphores available
EISNAM 120 Is a named type file
EREMOTEIO 121 Remote I/O error
EDQUOT 122 Disk quota exceeded
ENOMEDIUM 123 No medium found
EMEDIUMTYPE 124 Wrong medium type
ECANCELED 125 Operation canceled
ENOKEY 126 Required key not available
EKEYEXPIRED 127 Key has expired
EKEYREVOKED 128 Key has been revoked
EKEYREJECTED 129 Key was rejected by service
EOWNERDEAD 130 Owner died
ENOTRECOVERABLE 131 State not recoverable
ERFKILL 132 Operation not possible due to RF-kill
EHWPOISON 133 Memory page has hardware error
ENOTSUP 95 Operation not supported
Note that errno
may be not available by default in your Linux distro. For example, on Debian, you should call sudo apt-get install moreutils
to get this utility.
Here is a typical output on macOS:
$ errno -ls
EPERM 1 Operation not permitted
ENOENT 2 No such file or directory
ESRCH 3 No such process
EINTR 4 Interrupted system call
EIO 5 Input/output error
ENXIO 6 Device not configured
E2BIG 7 Argument list too long
ENOEXEC 8 Exec format error
EBADF 9 Bad file descriptor
ECHILD 10 No child processes
EDEADLK 11 Resource deadlock avoided
ENOMEM 12 Cannot allocate memory
EACCES 13 Permission denied
EFAULT 14 Bad address
ENOTBLK 15 Block device required
EBUSY 16 Resource busy
EEXIST 17 File exists
EXDEV 18 Cross-device link
ENODEV 19 Operation not supported by device
ENOTDIR 20 Not a directory
EISDIR 21 Is a directory
EINVAL 22 Invalid argument
ENFILE 23 Too many open files in system
EMFILE 24 Too many open files
ENOTTY 25 Inappropriate ioctl for device
ETXTBSY 26 Text file busy
EFBIG 27 File too large
ENOSPC 28 No space left on device
ESPIPE 29 Illegal seek
EROFS 30 Read-only file system
EMLINK 31 Too many links
EPIPE 32 Broken pipe
EDOM 33 Numerical argument out of domain
ERANGE 34 Result too large
EAGAIN 35 Resource temporarily unavailable
EWOULDBLOCK 35 Resource temporarily unavailable
EINPROGRESS 36 Operation now in progress
EALREADY 37 Operation already in progress
ENOTSOCK 38 Socket operation on non-socket
EDESTADDRREQ 39 Destination address required
EMSGSIZE 40 Message too long
EPROTOTYPE 41 Protocol wrong type for socket
ENOPROTOOPT 42 Protocol not available
EPROTONOSUPPORT 43 Protocol not supported
ESOCKTNOSUPPORT 44 Socket type not supported
ENOTSUP 45 Operation not supported
EPFNOSUPPORT 46 Protocol family not supported
EAFNOSUPPORT 47 Address family not supported by protocol family
EADDRINUSE 48 Address already in use
EADDRNOTAVAIL 49 Can`t assign requested address
ENETDOWN 50 Network is down
ENETUNREACH 51 Network is unreachable
ENETRESET 52 Network dropped connection on reset
ECONNABORTED 53 Software caused connection abort
ECONNRESET 54 Connection reset by peer
ENOBUFS 55 No buffer space available
EISCONN 56 Socket is already connected
ENOTCONN 57 Socket is not connected
ESHUTDOWN 58 Can`t send after socket shutdown
ETOOMANYREFS 59 Too many references: can`t splice
ETIMEDOUT 60 Operation timed out
ECONNREFUSED 61 Connection refused
ELOOP 62 Too many levels of symbolic links
ENAMETOOLONG 63 File name too long
EHOSTDOWN 64 Host is down
EHOSTUNREACH 65 No route to host
ENOTEMPTY 66 Directory not empty
EPROCLIM 67 Too many processes
EUSERS 68 Too many users
EDQUOT 69 Disc quota exceeded
ESTALE 70 Stale NFS file handle
EREMOTE 71 Too many levels of remote in path
EBADRPC 72 RPC struct is bad
ERPCMISMATCH 73 RPC version wrong
EPROGUNAVAIL 74 RPC prog. not avail
EPROGMISMATCH 75 Program version wrong
EPROCUNAVAIL 76 Bad procedure for program
ENOLCK 77 No locks available
ENOSYS 78 Function not implemented
EFTYPE 79 Inappropriate file type or format
EAUTH 80 Authentication error
ENEEDAUTH 81 Need authenticator
EPWROFF 82 Device power is off
EDEVERR 83 Device error
EOVERFLOW 84 Value too large to be stored in data type
EBADEXEC 85 Bad executable (or shared library)
EBADARCH 86 Bad CPU type in executable
ESHLIBVERS 87 Shared library version mismatch
EBADMACHO 88 Malformed Mach-o file
ECANCELED 89 Operation canceled
EIDRM 90 Identifier removed
ENOMSG 91 No message of desired type
EILSEQ 92 Illegal byte sequence
ENOATTR 93 Attribute not found
EBADMSG 94 Bad message
EMULTIHOP 95 EMULTIHOP (Reserved)
ENODATA 96 No message available on STREAM
ENOLINK 97 ENOLINK (Reserved)
ENOSR 98 No STREAM resources
ENOSTR 99 Not a STREAM
EPROTO 100 Protocol error
ETIME 101 STREAM ioctl timeout
EOPNOTSUPP 102 Operation not supported on socket
ENOPOLICY 103 Policy not found
ENOTRECOVERABLE 104 State not recoverable
EOWNERDEAD 105 Previous owner died
EQFULL 106 Interface output queue is full
ELAST 106 Interface output queue is full
Hooray! We’ve finished our fascinating journey into the internals of socket error codes. Now you know where .NET is getting the native error code for each SocketException
from!
ErrorCode
The ErrorCode
property is the most boring one, as it always returns NativeErrorCode
.
.NET Framework, Mono 6.8.0.105:
public override int ErrorCode {
//
// the base class returns the HResult with this property
// we need the Win32 Error Code, hence the override.
//
get {
return NativeErrorCode;
}
}
In .NET Core 3.1.3:
public override int ErrorCode => base.NativeErrorCode;
Writing cross-platform socket error handling
Circling back to the original method we started this post with, we rewrote ShouldRetryConnection as follows:
protected virtual bool ShouldRetryConnection(Exception ex)
{
if (ex is SocketException sx)
return sx.SocketErrorCode == SocketError.ConnectionRefused;
return false;
}
There was a lot of work involved in tracking down the error code to check against, but in the end, our code is much more readable now. Adding to that, this method is now also completely cross-platform, and works correctly on any runtime.
Overview of the native error codes
In some situations, you may want to have a table with native error codes on different operating systems. We can get these values with the following code snippet:
var allErrors = Enum.GetValues(typeof(SocketError)).Cast<SocketError>().ToList();
var maxNameWidth = allErrors.Select(x => x.ToString().Length).Max();
foreach (var socketError in allErrors)
{
var name = socketError.ToString().PadRight(maxNameWidth);
var code = new SocketException((int) socketError).NativeErrorCode.ToString().PadLeft(7);
Console.WriteLine("| {name} | {code} |");
}
We executed this program on Windows, Linux, and macOS. Here are the aggregated results:
SocketError | Windows | Linux | macOS |
Success | 0 | 0 | 0 |
OperationAborted | 995 | 125 | 89 |
IOPending | 997 | 115 | 36 |
Interrupted | 10004 | 4 | 4 |
AccessDenied | 10013 | 13 | 13 |
Fault | 10014 | 14 | 14 |
InvalidArgument | 10022 | 22 | 22 |
TooManyOpenSockets | 10024 | 23 | 23 |
WouldBlock | 10035 | 11 | 35 |
InProgress | 10036 | 115 | 36 |
AlreadyInProgress | 10037 | 114 | 37 |
NotSocket | 10038 | 88 | 38 |
DestinationAddressRequired | 10039 | 89 | 39 |
MessageSize | 10040 | 90 | 40 |
ProtocolType | 10041 | 91 | 41 |
ProtocolOption | 10042 | 92 | 42 |
ProtocolNotSupported | 10043 | 93 | 43 |
SocketNotSupported | 10044 | 94 | 44 |
OperationNotSupported | 10045 | 95 | 45 |
ProtocolFamilyNotSupported | 10046 | 96 | 46 |
AddressFamilyNotSupported | 10047 | 97 | 47 |
AddressAlreadyInUse | 10048 | 98 | 48 |
AddressNotAvailable | 10049 | 99 | 49 |
NetworkDown | 10050 | 100 | 50 |
NetworkUnreachable | 10051 | 101 | 51 |
NetworkReset | 10052 | 102 | 52 |
ConnectionAborted | 10053 | 103 | 53 |
ConnectionReset | 10054 | 104 | 54 |
NoBufferSpaceAvailable | 10055 | 105 | 55 |
IsConnected | 10056 | 106 | 56 |
NotConnected | 10057 | 107 | 57 |
Shutdown | 10058 | 32 | 32 |
TimedOut | 10060 | 110 | 60 |
ConnectionRefused | 10061 | 111 | 61 |
HostDown | 10064 | 112 | 64 |
HostUnreachable | 10065 | 113 | 65 |
ProcessLimit | 10067 | 10067 | 10067 |
SystemNotReady | 10091 | 10091 | 10091 |
VersionNotSupported | 10092 | 10092 | 10092 |
NotInitialized | 10093 | 10093 | 10093 |
Disconnecting | 10101 | 108 | 58 |
TypeNotFound | 10109 | 10109 | 10109 |
HostNotFound | 11001 | -131073 | -131073 |
TryAgain | 11002 | 11 | 35 |
NoRecovery | 11003 | 11003 | 11003 |
NoData | 11004 | 61 | 96 |
SocketError | -1 | -1 | -1 |
This table may be useful if you work with native socket error codes.
Summary
From this investigation, we’ve learned the following:
SocketException.SocketErrorCode
returns a value from theSocketError
enum. The numerical values of the enum elements always correspond to the Windows socket error codes.SocketException.ErrorCode
always returnsSocketException.NativeErrorCode
.SocketException.NativeErrorCode
on .NET Framework and Mono always corresponds to the Windows error codes (even if you are using Mono on Unix). On .NET Core,SocketException.NativeErrorCode
equals the corresponding native error code from the current operating system.
A few practical recommendations:
- If you want to write portable code, always use
SocketException.SocketErrorCode
and compare it with the values ofSocketError
. Never use raw numerical error codes. - If you want to get the native error code on .NET Core (e.g., for passing to another native program), use
SocketException.NativeErrorCode
. Remember that different Unix-based operating systems (e.g., Linux, macOS, Solaris) have different native code sets. You can get the exact values of the native error codes by using the errno command.
References
- Microsoft Docs: Windows Sockets Error Codes
- IBM Knowledge Center: TCP/IP error codes
- MariaDB: Operating System Error Codes
- gnu.org: Error Codes
- Stackoverflow: Identical Error Codes
Dev Team blog
How Socket Error Codes Depend on Runtime and Operating System
This post is the first part of a blog post series that covers different technical challenges that we had to resolve during the migration of the Rider backend process from Mono to .NET Core. By sharing our experiences, we hope to help out those who are in the same boat.
There’s too much to share in one post, so we will make this into a series of posts. In this series:
- How Socket Error Codes Depend on Runtime and Operating System
- How Sorting Order Depends on Runtime and Operating System
- How ListSeparator Depends on Runtime and Operating System
Let’s dive in!
Sockets and error codes
Rider consists of several processes that send messages to each other via sockets. To ensure the reliability of the whole application, it’s important to properly handle all the socket errors. In our codebase, we had the following code which was adopted from Mono Debugger Libs and helps us communicate with debugger processes:
protected virtual bool ShouldRetryConnection (Exception ex, int attemptNumber) { var sx = ex as SocketException; if (sx != null) { if (sx.ErrorCode == 10061) //connection refused return true; } return false; }
In the case of a failed connection because of a “ConnectionRefused” error, we are retrying the connection attempt. It works fine with .NET Framework and Mono. However, once we migrated to .NET Core, this method no longer correctly detects the “connection refused” situation on Linux and macOS. If we open the SocketException
documentation, we will learn that this class has three different properties with error codes:
SocketError SocketErrorCode
: Gets the error code that is associated with this exception.int ErrorCode
: Gets the error code that is associated with this exception.int NativeErrorCode
: Gets the Win32 error code associated with this exception.
What’s the difference between these properties? Should we expect different values on different runtimes or different operating systems? Which one should we use in production? Why do we have problems with ShouldRetryConnection
on .NET Core? Let’s figure it all out!
Digging into the problem
Let’s start with the following program, which prints error code property values for SocketError.ConnectionRefused
:
var se = new SocketException((int) SocketError.ConnectionRefused); Console.WriteLine((int)se.SocketErrorCode); Console.WriteLine(se.ErrorCode); Console.WriteLine(se.NativeErrorCode);
If we run it on Windows, we will get the same value on .NET Framework, Mono, and .NET Core:
SocketErrorCode | ErrorCode | NativeErrorCode | |
.NET Framework | 10061 | 10061 | 10061 |
Mono | 10061 | 10061 | 10061 |
.NET Core | 10061 | 10061 | 10061 |
10061 corresponds to the code of the connection refused socket error code in Windows (also known as WSAECONNREFUSED
).
Now let’s run the same program on Linux:
SocketErrorCode | ErrorCode | NativeErrorCode | |
Mono | 10061 | 10061 | 10061 |
.NET Core | 10061 | 111 | 111 |
As you can see, Mono returns Windows-compatible error codes. The situation with .NET Core is different: it returns a Windows-compatible value for SocketErrorCode (10061) and a Linux-like value for ErrorCode
and NativeErrorCode
(111).
Finally, let’s check macOS:
SocketErrorCode | ErrorCode | NativeErrorCode | |
Mono | 10061 | 10061 | 10061 |
.NET Core | 10061 | 61 | 61 |
Here, Mono is completely Windows-compatible again, but .NET Core returns 61 for ErrorCode
and NativeErrorCode
.
In the IBM Knowledge Center, we can find a few more values for the connection refused error code from the Unix world (also known as ECONNREFUSED
):
- AIX: 79
- HP-UX: 239
- Solaris: 146
For a better understanding of what’s going on, let’s check out the source code of all the properties.
SocketErrorCode
SocketException.SocketErrorCode
returns a value from the SocketError
enum. The numerical values of the enum elements are the same on all the runtimes (see its implementation in .NET Framework, .NET Core 3.1.3, and Mono 6.8.0.105):
public enum SocketError { SocketError = -1, // 0xFFFFFFFF Success = 0, OperationAborted = 995, // 0x000003E3 IOPending = 997, // 0x000003E5 Interrupted = 10004, // 0x00002714 AccessDenied = 10013, // 0x0000271D Fault = 10014, // 0x0000271E InvalidArgument = 10022, // 0x00002726 TooManyOpenSockets = 10024, // 0x00002728 WouldBlock = 10035, // 0x00002733 InProgress = 10036, // 0x00002734 AlreadyInProgress = 10037, // 0x00002735 NotSocket = 10038, // 0x00002736 DestinationAddressRequired = 10039, // 0x00002737 MessageSize = 10040, // 0x00002738 ProtocolType = 10041, // 0x00002739 ProtocolOption = 10042, // 0x0000273A ProtocolNotSupported = 10043, // 0x0000273B SocketNotSupported = 10044, // 0x0000273C OperationNotSupported = 10045, // 0x0000273D ProtocolFamilyNotSupported = 10046, // 0x0000273E AddressFamilyNotSupported = 10047, // 0x0000273F AddressAlreadyInUse = 10048, // 0x00002740 AddressNotAvailable = 10049, // 0x00002741 NetworkDown = 10050, // 0x00002742 NetworkUnreachable = 10051, // 0x00002743 NetworkReset = 10052, // 0x00002744 ConnectionAborted = 10053, // 0x00002745 ConnectionReset = 10054, // 0x00002746 NoBufferSpaceAvailable = 10055, // 0x00002747 IsConnected = 10056, // 0x00002748 NotConnected = 10057, // 0x00002749 Shutdown = 10058, // 0x0000274A TimedOut = 10060, // 0x0000274C ConnectionRefused = 10061, // 0x0000274D HostDown = 10064, // 0x00002750 HostUnreachable = 10065, // 0x00002751 ProcessLimit = 10067, // 0x00002753 SystemNotReady = 10091, // 0x0000276B VersionNotSupported = 10092, // 0x0000276C NotInitialized = 10093, // 0x0000276D Disconnecting = 10101, // 0x00002775 TypeNotFound = 10109, // 0x0000277D HostNotFound = 11001, // 0x00002AF9 TryAgain = 11002, // 0x00002AFA NoRecovery = 11003, // 0x00002AFB NoData = 11004, // 0x00002AFC }
These values correspond to the Windows Sockets Error Codes.
NativeErrorCode
In .NET Framework and Mono, SocketErrorCode
and NativeErrorCode
always have the same values:
public SocketError SocketErrorCode { // // the base class returns the HResult with this property // we need the Win32 Error Code, hence the override. // get { return (SocketError)NativeErrorCode; } }
In .NET Core, the native code is calculated in the constructor (see SocketException.cs#L20):
public SocketException(int errorCode) : this((SocketError)errorCode) // ... internal SocketException(SocketError socketError) : base(GetNativeErrorForSocketError(socketError))
The Windows implementation of GetNativeErrorForSocketError
is trivial (see SocketException.Windows.cs):
private static int GetNativeErrorForSocketError(SocketError error) { // SocketError values map directly to Win32 error codes return (int)error; }
The Unix implementation is more complicated (see SocketException.Unix.cs):
private static int GetNativeErrorForSocketError(SocketError error) { int nativeErr = (int)error; if (error != SocketError.SocketError) { Interop.Error interopErr; // If an interop error was not found, then don't invoke Info().RawErrno as that will fail with assert. if (SocketErrorPal.TryGetNativeErrorForSocketError(error, out interopErr)) { nativeErr = interopErr.Info().RawErrno; } } return nativeErr; }
TryGetNativeErrorForSocketError
should convert SocketError
to the native Unix error code.
Unfortunately, there exists no unequivocal mapping between Windows and Unix error codes. As such, the .NET team decided to create a Dictionary
that maps error codes in the best possible way (see SocketErrorPal.Unix.cs):
private const int NativeErrorToSocketErrorCount = 42; private const int SocketErrorToNativeErrorCount = 40; // No Interop.Errors are included for the following SocketErrors, as there's no good mapping: // - SocketError.NoRecovery // - SocketError.NotInitialized // - SocketError.ProcessLimit // - SocketError.SocketError // - SocketError.SystemNotReady // - SocketError.TypeNotFound // - SocketError.VersionNotSupported private static readonly Dictionary<Interop.Error, SocketError> s_nativeErrorToSocketError = new Dictionary<Interop.Error, SocketError>(NativeErrorToSocketErrorCount) { { Interop.Error.EACCES, SocketError.AccessDenied }, { Interop.Error.EADDRINUSE, SocketError.AddressAlreadyInUse }, { Interop.Error.EADDRNOTAVAIL, SocketError.AddressNotAvailable }, { Interop.Error.EAFNOSUPPORT, SocketError.AddressFamilyNotSupported }, { Interop.Error.EAGAIN, SocketError.WouldBlock }, { Interop.Error.EALREADY, SocketError.AlreadyInProgress }, { Interop.Error.EBADF, SocketError.OperationAborted }, { Interop.Error.ECANCELED, SocketError.OperationAborted }, { Interop.Error.ECONNABORTED, SocketError.ConnectionAborted }, { Interop.Error.ECONNREFUSED, SocketError.ConnectionRefused }, { Interop.Error.ECONNRESET, SocketError.ConnectionReset }, { Interop.Error.EDESTADDRREQ, SocketError.DestinationAddressRequired }, { Interop.Error.EFAULT, SocketError.Fault }, { Interop.Error.EHOSTDOWN, SocketError.HostDown }, { Interop.Error.ENXIO, SocketError.HostNotFound }, // not perfect, but closest match available { Interop.Error.EHOSTUNREACH, SocketError.HostUnreachable }, { Interop.Error.EINPROGRESS, SocketError.InProgress }, { Interop.Error.EINTR, SocketError.Interrupted }, { Interop.Error.EINVAL, SocketError.InvalidArgument }, { Interop.Error.EISCONN, SocketError.IsConnected }, { Interop.Error.EMFILE, SocketError.TooManyOpenSockets }, { Interop.Error.EMSGSIZE, SocketError.MessageSize }, { Interop.Error.ENETDOWN, SocketError.NetworkDown }, { Interop.Error.ENETRESET, SocketError.NetworkReset }, { Interop.Error.ENETUNREACH, SocketError.NetworkUnreachable }, { Interop.Error.ENFILE, SocketError.TooManyOpenSockets }, { Interop.Error.ENOBUFS, SocketError.NoBufferSpaceAvailable }, { Interop.Error.ENODATA, SocketError.NoData }, { Interop.Error.ENOENT, SocketError.AddressNotAvailable }, { Interop.Error.ENOPROTOOPT, SocketError.ProtocolOption }, { Interop.Error.ENOTCONN, SocketError.NotConnected }, { Interop.Error.ENOTSOCK, SocketError.NotSocket }, { Interop.Error.ENOTSUP, SocketError.OperationNotSupported }, { Interop.Error.EPERM, SocketError.AccessDenied }, { Interop.Error.EPIPE, SocketError.Shutdown }, { Interop.Error.EPFNOSUPPORT, SocketError.ProtocolFamilyNotSupported }, { Interop.Error.EPROTONOSUPPORT, SocketError.ProtocolNotSupported }, { Interop.Error.EPROTOTYPE, SocketError.ProtocolType }, { Interop.Error.ESOCKTNOSUPPORT, SocketError.SocketNotSupported }, { Interop.Error.ESHUTDOWN, SocketError.Disconnecting }, { Interop.Error.SUCCESS, SocketError.Success }, { Interop.Error.ETIMEDOUT, SocketError.TimedOut }, }; private static readonly Dictionary<SocketError, Interop.Error> s_socketErrorToNativeError = new Dictionary<SocketError, Interop.Error>(SocketErrorToNativeErrorCount) { // This is *mostly* an inverse mapping of s_nativeErrorToSocketError. However, some options have multiple mappings and thus // can't be inverted directly. Other options don't have a mapping from native to SocketError, but when presented with a SocketError, // we want to provide the closest relevant Error possible, e.g. EINPROGRESS maps to SocketError.InProgress, and vice versa, but // SocketError.IOPending also maps closest to EINPROGRESS. As such, roundtripping won't necessarily provide the original value 100% of the time, // but it's the best we can do given the mismatch between Interop.Error and SocketError. { SocketError.AccessDenied, Interop.Error.EACCES}, // could also have been EPERM { SocketError.AddressAlreadyInUse, Interop.Error.EADDRINUSE }, { SocketError.AddressNotAvailable, Interop.Error.EADDRNOTAVAIL }, { SocketError.AddressFamilyNotSupported, Interop.Error.EAFNOSUPPORT }, { SocketError.AlreadyInProgress, Interop.Error.EALREADY }, { SocketError.ConnectionAborted, Interop.Error.ECONNABORTED }, { SocketError.ConnectionRefused, Interop.Error.ECONNREFUSED }, { SocketError.ConnectionReset, Interop.Error.ECONNRESET }, { SocketError.DestinationAddressRequired, Interop.Error.EDESTADDRREQ }, { SocketError.Disconnecting, Interop.Error.ESHUTDOWN }, { SocketError.Fault, Interop.Error.EFAULT }, { SocketError.HostDown, Interop.Error.EHOSTDOWN }, { SocketError.HostNotFound, Interop.Error.EHOSTNOTFOUND }, { SocketError.HostUnreachable, Interop.Error.EHOSTUNREACH }, { SocketError.InProgress, Interop.Error.EINPROGRESS }, { SocketError.Interrupted, Interop.Error.EINTR }, { SocketError.InvalidArgument, Interop.Error.EINVAL }, { SocketError.IOPending, Interop.Error.EINPROGRESS }, { SocketError.IsConnected, Interop.Error.EISCONN }, { SocketError.MessageSize, Interop.Error.EMSGSIZE }, { SocketError.NetworkDown, Interop.Error.ENETDOWN }, { SocketError.NetworkReset, Interop.Error.ENETRESET }, { SocketError.NetworkUnreachable, Interop.Error.ENETUNREACH }, { SocketError.NoBufferSpaceAvailable, Interop.Error.ENOBUFS }, { SocketError.NoData, Interop.Error.ENODATA }, { SocketError.NotConnected, Interop.Error.ENOTCONN }, { SocketError.NotSocket, Interop.Error.ENOTSOCK }, { SocketError.OperationAborted, Interop.Error.ECANCELED }, { SocketError.OperationNotSupported, Interop.Error.ENOTSUP }, { SocketError.ProtocolFamilyNotSupported, Interop.Error.EPFNOSUPPORT }, { SocketError.ProtocolNotSupported, Interop.Error.EPROTONOSUPPORT }, { SocketError.ProtocolOption, Interop.Error.ENOPROTOOPT }, { SocketError.ProtocolType, Interop.Error.EPROTOTYPE }, { SocketError.Shutdown, Interop.Error.EPIPE }, { SocketError.SocketNotSupported, Interop.Error.ESOCKTNOSUPPORT }, { SocketError.Success, Interop.Error.SUCCESS }, { SocketError.TimedOut, Interop.Error.ETIMEDOUT }, { SocketError.TooManyOpenSockets, Interop.Error.ENFILE }, // could also have been EMFILE { SocketError.TryAgain, Interop.Error.EAGAIN }, // not a perfect mapping, but better than nothing { SocketError.WouldBlock, Interop.Error.EAGAIN }, }; internal static bool TryGetNativeErrorForSocketError(SocketError error, out Interop.Error errno) { return s_socketErrorToNativeError.TryGetValue(error, out errno); }
Once we have an instance of Interop.Error
, we call interopErr.Info().RawErrno
. The implementation of RawErrno can be found in Interop.Errors.cs:
internal int RawErrno { get { return _rawErrno == -1 ? (_rawErrno = Interop.Sys.ConvertErrorPalToPlatform(_error)) : _rawErrno; } } [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_ConvertErrorPalToPlatform")] internal static extern int ConvertErrorPalToPlatform(Error error);
Here we are jumping to the native function SystemNative_ConvertErrorPalToPlatform that maps Error to the native integer code that is defined in errno.h. You can get all the values using the errno util. Here is a typical output on Linux:
$ errno -ls EPERM 1 Operation not permitted ENOENT 2 No such file or directory ESRCH 3 No such process EINTR 4 Interrupted system call EIO 5 Input/output error ENXIO 6 No such device or address E2BIG 7 Argument list too long ENOEXEC 8 Exec format error EBADF 9 Bad file descriptor ECHILD 10 No child processes EAGAIN 11 Resource temporarily unavailable ENOMEM 12 Cannot allocate memory EACCES 13 Permission denied EFAULT 14 Bad address ENOTBLK 15 Block device required EBUSY 16 Device or resource busy EEXIST 17 File exists EXDEV 18 Invalid cross-device link ENODEV 19 No such device ENOTDIR 20 Not a directory EISDIR 21 Is a directory EINVAL 22 Invalid argument ENFILE 23 Too many open files in system EMFILE 24 Too many open files ENOTTY 25 Inappropriate ioctl for device ETXTBSY 26 Text file busy EFBIG 27 File too large ENOSPC 28 No space left on device ESPIPE 29 Illegal seek EROFS 30 Read-only file system EMLINK 31 Too many links EPIPE 32 Broken pipe EDOM 33 Numerical argument out of domain ERANGE 34 Numerical result out of range EDEADLK 35 Resource deadlock avoided ENAMETOOLONG 36 File name too long ENOLCK 37 No locks available ENOSYS 38 Function not implemented ENOTEMPTY 39 Directory not empty ELOOP 40 Too many levels of symbolic links EWOULDBLOCK 11 Resource temporarily unavailable ENOMSG 42 No message of desired type EIDRM 43 Identifier removed ECHRNG 44 Channel number out of range EL2NSYNC 45 Level 2 not synchronized EL3HLT 46 Level 3 halted EL3RST 47 Level 3 reset ELNRNG 48 Link number out of range EUNATCH 49 Protocol driver not attached ENOCSI 50 No CSI structure available EL2HLT 51 Level 2 halted EBADE 52 Invalid exchange EBADR 53 Invalid request descriptor EXFULL 54 Exchange full ENOANO 55 No anode EBADRQC 56 Invalid request code EBADSLT 57 Invalid slot EDEADLOCK 35 Resource deadlock avoided EBFONT 59 Bad font file format ENOSTR 60 Device not a stream ENODATA 61 No data available ETIME 62 Timer expired ENOSR 63 Out of streams resources ENONET 64 Machine is not on the network ENOPKG 65 Package not installed EREMOTE 66 Object is remote ENOLINK 67 Link has been severed EADV 68 Advertise error ESRMNT 69 Srmount error ECOMM 70 Communication error on send EPROTO 71 Protocol error EMULTIHOP 72 Multihop attempted EDOTDOT 73 RFS specific error EBADMSG 74 Bad message EOVERFLOW 75 Value too large for defined data type ENOTUNIQ 76 Name not unique on network EBADFD 77 File descriptor in bad state EREMCHG 78 Remote address changed ELIBACC 79 Can not access a needed shared library ELIBBAD 80 Accessing a corrupted shared library ELIBSCN 81 .lib section in a.out corrupted ELIBMAX 82 Attempting to link in too many shared libraries ELIBEXEC 83 Cannot exec a shared library directly EILSEQ 84 Invalid or incomplete multibyte or wide character ERESTART 85 Interrupted system call should be restarted ESTRPIPE 86 Streams pipe error EUSERS 87 Too many users ENOTSOCK 88 Socket operation on non-socket EDESTADDRREQ 89 Destination address required EMSGSIZE 90 Message too long EPROTOTYPE 91 Protocol wrong type for socket ENOPROTOOPT 92 Protocol not available EPROTONOSUPPORT 93 Protocol not supported ESOCKTNOSUPPORT 94 Socket type not supported EOPNOTSUPP 95 Operation not supported EPFNOSUPPORT 96 Protocol family not supported EAFNOSUPPORT 97 Address family not supported by protocol EADDRINUSE 98 Address already in use EADDRNOTAVAIL 99 Cannot assign requested address ENETDOWN 100 Network is down ENETUNREACH 101 Network is unreachable ENETRESET 102 Network dropped connection on reset ECONNABORTED 103 Software caused connection abort ECONNRESET 104 Connection reset by peer ENOBUFS 105 No buffer space available EISCONN 106 Transport endpoint is already connected ENOTCONN 107 Transport endpoint is not connected ESHUTDOWN 108 Cannot send after transport endpoint shutdown ETOOMANYREFS 109 Too many references: cannot splice ETIMEDOUT 110 Connection timed out ECONNREFUSED 111 Connection refused EHOSTDOWN 112 Host is down EHOSTUNREACH 113 No route to host EALREADY 114 Operation already in progress EINPROGRESS 115 Operation now in progress ESTALE 116 Stale file handle EUCLEAN 117 Structure needs cleaning ENOTNAM 118 Not a XENIX named type file ENAVAIL 119 No XENIX semaphores available EISNAM 120 Is a named type file EREMOTEIO 121 Remote I/O error EDQUOT 122 Disk quota exceeded ENOMEDIUM 123 No medium found EMEDIUMTYPE 124 Wrong medium type ECANCELED 125 Operation canceled ENOKEY 126 Required key not available EKEYEXPIRED 127 Key has expired EKEYREVOKED 128 Key has been revoked EKEYREJECTED 129 Key was rejected by service EOWNERDEAD 130 Owner died ENOTRECOVERABLE 131 State not recoverable ERFKILL 132 Operation not possible due to RF-kill EHWPOISON 133 Memory page has hardware error ENOTSUP 95 Operation not supported
Note that errno
may be not available by default in your Linux distro. For example, on Debian, you should call sudo apt-get install moreutils
to get this utility.
Here is a typical output on macOS:
$ errno -ls EPERM 1 Operation not permitted ENOENT 2 No such file or directory ESRCH 3 No such process EINTR 4 Interrupted system call EIO 5 Input/output error ENXIO 6 Device not configured E2BIG 7 Argument list too long ENOEXEC 8 Exec format error EBADF 9 Bad file descriptor ECHILD 10 No child processes EDEADLK 11 Resource deadlock avoided ENOMEM 12 Cannot allocate memory EACCES 13 Permission denied EFAULT 14 Bad address ENOTBLK 15 Block device required EBUSY 16 Resource busy EEXIST 17 File exists EXDEV 18 Cross-device link ENODEV 19 Operation not supported by device ENOTDIR 20 Not a directory EISDIR 21 Is a directory EINVAL 22 Invalid argument ENFILE 23 Too many open files in system EMFILE 24 Too many open files ENOTTY 25 Inappropriate ioctl for device ETXTBSY 26 Text file busy EFBIG 27 File too large ENOSPC 28 No space left on device ESPIPE 29 Illegal seek EROFS 30 Read-only file system EMLINK 31 Too many links EPIPE 32 Broken pipe EDOM 33 Numerical argument out of domain ERANGE 34 Result too large EAGAIN 35 Resource temporarily unavailable EWOULDBLOCK 35 Resource temporarily unavailable EINPROGRESS 36 Operation now in progress EALREADY 37 Operation already in progress ENOTSOCK 38 Socket operation on non-socket EDESTADDRREQ 39 Destination address required EMSGSIZE 40 Message too long EPROTOTYPE 41 Protocol wrong type for socket ENOPROTOOPT 42 Protocol not available EPROTONOSUPPORT 43 Protocol not supported ESOCKTNOSUPPORT 44 Socket type not supported ENOTSUP 45 Operation not supported EPFNOSUPPORT 46 Protocol family not supported EAFNOSUPPORT 47 Address family not supported by protocol family EADDRINUSE 48 Address already in use EADDRNOTAVAIL 49 Can`t assign requested address ENETDOWN 50 Network is down ENETUNREACH 51 Network is unreachable ENETRESET 52 Network dropped connection on reset ECONNABORTED 53 Software caused connection abort ECONNRESET 54 Connection reset by peer ENOBUFS 55 No buffer space available EISCONN 56 Socket is already connected ENOTCONN 57 Socket is not connected ESHUTDOWN 58 Can`t send after socket shutdown ETOOMANYREFS 59 Too many references: can`t splice ETIMEDOUT 60 Operation timed out ECONNREFUSED 61 Connection refused ELOOP 62 Too many levels of symbolic links ENAMETOOLONG 63 File name too long EHOSTDOWN 64 Host is down EHOSTUNREACH 65 No route to host ENOTEMPTY 66 Directory not empty EPROCLIM 67 Too many processes EUSERS 68 Too many users EDQUOT 69 Disc quota exceeded ESTALE 70 Stale NFS file handle EREMOTE 71 Too many levels of remote in path EBADRPC 72 RPC struct is bad ERPCMISMATCH 73 RPC version wrong EPROGUNAVAIL 74 RPC prog. not avail EPROGMISMATCH 75 Program version wrong EPROCUNAVAIL 76 Bad procedure for program ENOLCK 77 No locks available ENOSYS 78 Function not implemented EFTYPE 79 Inappropriate file type or format EAUTH 80 Authentication error ENEEDAUTH 81 Need authenticator EPWROFF 82 Device power is off EDEVERR 83 Device error EOVERFLOW 84 Value too large to be stored in data type EBADEXEC 85 Bad executable (or shared library) EBADARCH 86 Bad CPU type in executable ESHLIBVERS 87 Shared library version mismatch EBADMACHO 88 Malformed Mach-o file ECANCELED 89 Operation canceled EIDRM 90 Identifier removed ENOMSG 91 No message of desired type EILSEQ 92 Illegal byte sequence ENOATTR 93 Attribute not found EBADMSG 94 Bad message EMULTIHOP 95 EMULTIHOP (Reserved) ENODATA 96 No message available on STREAM ENOLINK 97 ENOLINK (Reserved) ENOSR 98 No STREAM resources ENOSTR 99 Not a STREAM EPROTO 100 Protocol error ETIME 101 STREAM ioctl timeout EOPNOTSUPP 102 Operation not supported on socket ENOPOLICY 103 Policy not found ENOTRECOVERABLE 104 State not recoverable EOWNERDEAD 105 Previous owner died EQFULL 106 Interface output queue is full ELAST 106 Interface output queue is full
Hooray! We’ve finished our fascinating journey into the internals of socket error codes. Now you know where .NET is getting the native error code for each SocketException
from!
ErrorCode
The ErrorCode
property is the most boring one, as it always returns NativeErrorCode
.
.NET Framework, Mono 6.8.0.105:
public override int ErrorCode { // // the base class returns the HResult with this property // we need the Win32 Error Code, hence the override. // get { return NativeErrorCode; } }
In .NET Core 3.1.3:
public override int ErrorCode => base.NativeErrorCode;
Writing cross-platform socket error handling
Circling back to the original method we started this post with, we rewrote ShouldRetryConnection as follows:
protected virtual bool ShouldRetryConnection(Exception ex) { if (ex is SocketException sx) return sx.SocketErrorCode == SocketError.ConnectionRefused; return false; }
There was a lot of work involved in tracking down the error code to check against, but in the end, our code is much more readable now. Adding to that, this method is now also completely cross-platform, and works correctly on any runtime.
Overview of the native error codes
In some situations, you may want to have a table with native error codes on different operating systems. We can get these values with the following code snippet:
var allErrors = Enum.GetValues(typeof(SocketError)).Cast<SocketError>().ToList(); var maxNameWidth = allErrors.Select(x => x.ToString().Length).Max(); foreach (var socketError in allErrors) { var name = socketError.ToString().PadRight(maxNameWidth); var code = new SocketException((int) socketError).NativeErrorCode.ToString().PadLeft(7); Console.WriteLine($TEXT$quot;| {name} | {code} |"); }
We executed this program on Windows, Linux, and macOS. Here are the aggregated results:
SocketError | Windows | Linux | macOS |
Success | 0 | 0 | 0 |
OperationAborted | 995 | 125 | 89 |
IOPending | 997 | 115 | 36 |
Interrupted | 10004 | 4 | 4 |
AccessDenied | 10013 | 13 | 13 |
Fault | 10014 | 14 | 14 |
InvalidArgument | 10022 | 22 | 22 |
TooManyOpenSockets | 10024 | 23 | 23 |
WouldBlock | 10035 | 11 | 35 |
InProgress | 10036 | 115 | 36 |
AlreadyInProgress | 10037 | 114 | 37 |
NotSocket | 10038 | 88 | 38 |
DestinationAddressRequired | 10039 | 89 | 39 |
MessageSize | 10040 | 90 | 40 |
ProtocolType | 10041 | 91 | 41 |
ProtocolOption | 10042 | 92 | 42 |
ProtocolNotSupported | 10043 | 93 | 43 |
SocketNotSupported | 10044 | 94 | 44 |
OperationNotSupported | 10045 | 95 | 45 |
ProtocolFamilyNotSupported | 10046 | 96 | 46 |
AddressFamilyNotSupported | 10047 | 97 | 47 |
AddressAlreadyInUse | 10048 | 98 | 48 |
AddressNotAvailable | 10049 | 99 | 49 |
NetworkDown | 10050 | 100 | 50 |
NetworkUnreachable | 10051 | 101 | 51 |
NetworkReset | 10052 | 102 | 52 |
ConnectionAborted | 10053 | 103 | 53 |
ConnectionReset | 10054 | 104 | 54 |
NoBufferSpaceAvailable | 10055 | 105 | 55 |
IsConnected | 10056 | 106 | 56 |
NotConnected | 10057 | 107 | 57 |
Shutdown | 10058 | 32 | 32 |
TimedOut | 10060 | 110 | 60 |
ConnectionRefused | 10061 | 111 | 61 |
HostDown | 10064 | 112 | 64 |
HostUnreachable | 10065 | 113 | 65 |
ProcessLimit | 10067 | 10067 | 10067 |
SystemNotReady | 10091 | 10091 | 10091 |
VersionNotSupported | 10092 | 10092 | 10092 |
NotInitialized | 10093 | 10093 | 10093 |
Disconnecting | 10101 | 108 | 58 |
TypeNotFound | 10109 | 10109 | 10109 |
HostNotFound | 11001 | -131073 | -131073 |
TryAgain | 11002 | 11 | 35 |
NoRecovery | 11003 | 11003 | 11003 |
NoData | 11004 | 61 | 96 |
SocketError | -1 | -1 | -1 |
This table may be useful if you work with native socket error codes.
Summary
From this investigation, we’ve learned the following:
SocketException.SocketErrorCode
returns a value from theSocketError
enum. The numerical values of the enum elements always correspond to the Windows socket error codes.SocketException.ErrorCode
always returnsSocketException.NativeErrorCode
.SocketException.NativeErrorCode
on .NET Framework and Mono always corresponds to the Windows error codes (even if you are using Mono on Unix). On .NET Core,SocketException.NativeErrorCode
equals the corresponding native error code from the current operating system.
A few practical recommendations:
- If you want to write portable code, always use
SocketException.SocketErrorCode
and compare it with the values ofSocketError
. Never use raw numerical error codes. - If you want to get the native error code on .NET Core (e.g., for passing to another native program), use
SocketException.NativeErrorCode
. Remember that different Unix-based operating systems (e.g., Linux, macOS, Solaris) have different native code sets. You can get the exact values of the native error codes by using the errno command.
References
- Microsoft Docs: Windows Sockets Error Codes
- IBM Knowledge Center: TCP/IP error codes
- MariaDB: Operating System Error Codes
- gnu.org: Error Codes
- Stackoverflow: Identical Error Codes
Subscribe to Blog updates
Discover more
The docs say:
10035: WSAEWOULDBLOCK.
Resource temporarily unavailable.
This error is returned from operations on nonblocking sockets that cannot be completed immediately, for example recv when no data is queued to be read from the socket. It is a nonfatal error, and the operation should be retried later. It is normal for WSAEWOULDBLOCK to be reported as the result from calling connect on a nonblocking SOCK_STREAM socket, since some time must elapse for the connection to be established.
WTF???
wtf indeed
This error isn’t an error at all. Pay attention to the last phrase:
10035: WSAEWOULDBLOCK.
Resource temporarily unavailable.
This error is returned from operations on nonblocking sockets that cannot be completed immediately, for example recv when no data is queued to be read from the socket. It is a nonfatal error, and the operation should be retried later. It is normal for WSAEWOULDBLOCK to be reported as the result from calling connect on a nonblocking SOCK_STREAM socket, since some time must elapse for the connection to be established.
In a program, like that crummy msdn example, I wrote:
// Connect to server. if ( connect( g.s, (SOCKADDR*) &clientService, sizeof(clientService) ) == SOCKET_ERROR) { int err = WSAGetLastError(); printf( "Failed to connect: Error code: %d.n", err ); WSACleanup(); return; }
But what would happen EVERY TIME is 10035: WSAEWOULDBLOCK.
WHY??? I puzzled over this again and again. WHAT IS WSAEWOULDBLOCK??
codegear has:
Abstract: Whenever I try to run my socket program, I get the error WSAEWOULDBLOCK.
Question
Why do I get a WSAEWOULDBLOCK error when I run my program.
Answer
This means that you are setting up your program as a non-blocking sockets program, however the computer is telling you that it would have to create a blocked connection to the socket.
Of all the bullshit…
THAT DOESN’T ANSWER MY QUESTION!!
So, I tried putting it in a loop to see if the state would change:
// Connect to server. while ( connect( g.s, (SOCKADDR*) &clientService, sizeof(clientService) ) == SOCKET_ERROR) { int err = WSAGetLastError(); printf( "Failed to connect: Error code: %d.n", err ); //WSACleanup(); //return; }
INTERESTINGLY, this is what happens:
10035
10056
10056
10056
10056
10056
10056
10056
.
.
.
Where 10056 is:
10056: WSAEISCONN
Socket is already connected.
A connect request was made on an already-connected socket. Some implementations also return this error if sendto is called on a connected SOCK_DGRAM socket (for SOCK_STREAM sockets, the to parameter in sendto is ignored) although other implementations treat this as a legal occurrence.
WHAT???? WOOHOO . . . ? ITS CONNECTED!!! BUT WHY??
Ah. I get it. Read that red italicized text again:
10035: WSAEWOULDBLOCK.
Resource temporarily unavailable.
This error is returned from operations on nonblocking sockets that cannot be completed immediately, for example recv when no data is queued to be read from the socket. It is a nonfatal error, and the operation should be retried later. It is normal for WSAEWOULDBLOCK to be reported as the result from calling connect on a nonblocking SOCK_STREAM socket, since some time must elapse for the connection to be established.
So it would SEEM that since 10035 WSAEWOULDBLOCK is a non-fatal error, you should IGNORE IT TRY AND USE THE SOCKET ANYWAY.
If you want to use an if statement like the crummy MSDN example (which is the reason this took so long to get past!!)
// Connect to server. while ( connect( g.s, (SOCKADDR*) &clientService, sizeof(clientService) ) == SOCKET_ERROR) { int err = WSAGetLastError(); printf( "Failed to connect: Error code: %d.n", err ); printf( errCodes[ err ] ); //WSACleanup(); //return; // these errors are non-fatal: 10056 means already connected // and 10035 means its trying really hard to connect // and you should just give it a moment. if( err == 10056 || err == 10035 ) break; }