From 47c0b1f7d4115e6f15e6776c1f91d28e7d96fe0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=80=D0=B5=D0=BD=D0=B1=D0=B5=D1=80=D0=B3=20?= =?UTF-8?q?=D0=9C=D0=B0=D1=80=D0=BA?= Date: Sun, 28 Jan 2018 03:20:50 +0500 Subject: [PATCH] bpo-32221: makeipaddr(): remove interface part + speedup (GH-4724) --- Doc/library/socket.rst | 15 +++++ Lib/test/test_socket.py | 44 ++++++++++++++ .../2017-12-06-10-10-10.bpo-32221.ideco_.rst | 4 ++ Modules/socketmodule.c | 59 ++++++++++--------- 4 files changed, 94 insertions(+), 28 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2017-12-06-10-10-10.bpo-32221.ideco_.rst diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 7edd4ba3604..777710f16f2 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -77,6 +77,11 @@ created. Socket addresses are represented as follows: backward compatibility. Note, however, omission of *scopeid* can cause problems in manipulating scoped IPv6 addresses. + .. versionchanged:: 3.7 + For multicast addresses (with *scopeid* meaningful) *address* may not contain + ``%scope`` (or ``zone id``) part. This information is superfluous and may + be safely omitted (recommended). + - :const:`AF_NETLINK` sockets are represented as pairs ``(pid, groups)``. - Linux-only support for TIPC is available using the :const:`AF_TIPC` @@ -630,6 +635,10 @@ The :mod:`socket` module also offers various network-related services: .. versionchanged:: 3.2 parameters can now be passed using keyword arguments. + .. versionchanged:: 3.7 + for IPv6 multicast addresses, string representing an address will not + contain ``%scope`` part. + .. function:: getfqdn([name]) Return a fully qualified domain name for *name*. If *name* is omitted or empty, @@ -688,6 +697,8 @@ The :mod:`socket` module also offers various network-related services: or numeric address representation in *host*. Similarly, *port* can contain a string port name or a numeric port number. + For IPv6 addresses, ``%scope`` is appended to the host part if *sockaddr* + contains meaningful *scopeid*. Usually this happens for multicast addresses. .. function:: getprotobyname(protocolname) @@ -1178,6 +1189,10 @@ to sockets. an exception, the method now retries the system call instead of raising an :exc:`InterruptedError` exception (see :pep:`475` for the rationale). + .. versionchanged:: 3.7 + For multicast IPv6 address, first item of *address* does not contain + ``%scope`` part anymore. In order to get full IPv6 address use + :func:`getnameinfo`. .. method:: socket.recvmsg(bufsize[, ancbufsize[, flags]]) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 275384c28fb..a971f921546 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -1586,6 +1586,50 @@ class GeneralModuleTests(unittest.TestCase): with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s: self.assertRaises(OverflowError, s.bind, (support.HOSTv6, 0, -10)) + @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test.') + def test_getaddrinfo_ipv6_basic(self): + ((*_, sockaddr),) = socket.getaddrinfo( + 'ff02::1de:c0:face:8D', # Note capital letter `D`. + 1234, socket.AF_INET6, + socket.SOCK_DGRAM, + socket.IPPROTO_UDP + ) + self.assertEqual(sockaddr, ('ff02::1de:c0:face:8d', 1234, 0, 0)) + + @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test.') + @unittest.skipUnless( + hasattr(socket, 'if_nameindex'), + 'IPv6 scope id by interface name is not supported' + ) + def test_getaddrinfo_ipv6_scopeid(self): + test_interface = 'lo' + ifindex = socket.if_nametoindex(test_interface) + ((*_, sockaddr),) = socket.getaddrinfo( + 'ff02::1de:c0:face:8D%' + test_interface, + 1234, socket.AF_INET6, + socket.SOCK_DGRAM, + socket.IPPROTO_UDP + ) + # Note missing interface name part in IPv6 address + self.assertEqual(sockaddr, ('ff02::1de:c0:face:8d', 1234, 0, ifindex)) + + @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test.') + def test_getaddrinfo_ipv6_scopeid_numeric(self): + ((*_, sockaddr),) = socket.getaddrinfo( + 'ff02::1de:c0:face:8D%42', + 1234, socket.AF_INET6, + socket.SOCK_DGRAM, + socket.IPPROTO_UDP + ) + # Note missing interface name part in IPv6 address + self.assertEqual(sockaddr, ('ff02::1de:c0:face:8d', 1234, 0, 42)) + + @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test.') + def test_getnameinfo_ipv6_scopeid(self): + sockaddr = ('ff02::1de:c0:face:8D', 1234, 0, 100500) # Note capital letter `D`. + nameinfo = socket.getnameinfo(sockaddr, socket.NI_NUMERICHOST | socket.NI_NUMERICSERV) + self.assertEqual(nameinfo, ('ff02::1de:c0:face:8d%100500', '1234')) + def test_str_for_enums(self): # Make sure that the AF_* and SOCK_* constants have enum-like string # reprs. diff --git a/Misc/NEWS.d/next/Library/2017-12-06-10-10-10.bpo-32221.ideco_.rst b/Misc/NEWS.d/next/Library/2017-12-06-10-10-10.bpo-32221.ideco_.rst new file mode 100644 index 00000000000..a88dcf48e02 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-12-06-10-10-10.bpo-32221.ideco_.rst @@ -0,0 +1,4 @@ +Various functions returning tuple containig IPv6 addresses now omit ``%scope`` +part since the same information is already encoded in *scopeid* tuple item. +Especially this speeds up :func:`socket.recvfrom` when it receives multicast +packet since useless resolving of network interface name is omitted. diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index 5fe2431bee8..30afe81d635 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -1094,25 +1094,33 @@ setipaddr(const char *name, struct sockaddr *addr_ret, size_t addr_ret_size, int } -/* Create a string object representing an IP address. - This is always a string of the form 'dd.dd.dd.dd' (with variable - size numbers). */ +/* Convert IPv4 sockaddr to a Python str. */ static PyObject * -makeipaddr(struct sockaddr *addr, int addrlen) +make_ipv4_addr(const struct sockaddr_in *addr) { - char buf[NI_MAXHOST]; - int error; - - error = getnameinfo(addr, addrlen, buf, sizeof(buf), NULL, 0, - NI_NUMERICHOST); - if (error) { - set_gaierror(error); + char buf[INET_ADDRSTRLEN]; + if (inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)) == NULL) { + PyErr_SetFromErrno(PyExc_OSError); return NULL; } return PyUnicode_FromString(buf); } +#ifdef ENABLE_IPV6 +/* Convert IPv6 sockaddr to a Python str. */ + +static PyObject * +make_ipv6_addr(const struct sockaddr_in6 *addr) +{ + char buf[INET6_ADDRSTRLEN]; + if (inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)) == NULL) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + return PyUnicode_FromString(buf); +} +#endif #ifdef USE_BLUETOOTH /* Convert a string representation of a Bluetooth address into a numeric @@ -1177,11 +1185,10 @@ makesockaddr(SOCKET_T sockfd, struct sockaddr *addr, size_t addrlen, int proto) case AF_INET: { - struct sockaddr_in *a; - PyObject *addrobj = makeipaddr(addr, sizeof(*a)); + const struct sockaddr_in *a = (const struct sockaddr_in *)addr; + PyObject *addrobj = make_ipv4_addr(a); PyObject *ret = NULL; if (addrobj) { - a = (struct sockaddr_in *)addr; ret = Py_BuildValue("Oi", addrobj, ntohs(a->sin_port)); Py_DECREF(addrobj); } @@ -1225,11 +1232,10 @@ makesockaddr(SOCKET_T sockfd, struct sockaddr *addr, size_t addrlen, int proto) #ifdef ENABLE_IPV6 case AF_INET6: { - struct sockaddr_in6 *a; - PyObject *addrobj = makeipaddr(addr, sizeof(*a)); + const struct sockaddr_in6 *a = (const struct sockaddr_in6 *)addr; + PyObject *addrobj = make_ipv6_addr(a); PyObject *ret = NULL; if (addrobj) { - a = (struct sockaddr_in6 *)addr; ret = Py_BuildValue("OiII", addrobj, ntohs(a->sin6_port), @@ -5036,14 +5042,14 @@ static PyObject * socket_gethostbyname(PyObject *self, PyObject *args) { char *name; - sock_addr_t addrbuf; + struct sockaddr_in addrbuf; PyObject *ret = NULL; if (!PyArg_ParseTuple(args, "et:gethostbyname", "idna", &name)) return NULL; - if (setipaddr(name, SAS2SA(&addrbuf), sizeof(addrbuf), AF_INET) < 0) + if (setipaddr(name, (struct sockaddr *)&addrbuf, sizeof(addrbuf), AF_INET) < 0) goto finally; - ret = makeipaddr(SAS2SA(&addrbuf), sizeof(struct sockaddr_in)); + ret = make_ipv4_addr(&addrbuf); finally: PyMem_Free(name); return ret; @@ -5145,7 +5151,7 @@ gethost_common(struct hostent *h, struct sockaddr *addr, size_t alen, int af) sin.sin_len = sizeof(sin); #endif memcpy(&sin.sin_addr, *pch, sizeof(sin.sin_addr)); - tmp = makeipaddr((struct sockaddr *)&sin, sizeof(sin)); + tmp = make_ipv4_addr(&sin); if (pch == h->h_addr_list && alen >= sizeof(sin)) memcpy((char *) addr, &sin, sizeof(sin)); @@ -5162,8 +5168,7 @@ gethost_common(struct hostent *h, struct sockaddr *addr, size_t alen, int af) sin6.sin6_len = sizeof(sin6); #endif memcpy(&sin6.sin6_addr, *pch, sizeof(sin6.sin6_addr)); - tmp = makeipaddr((struct sockaddr *)&sin6, - sizeof(sin6)); + tmp = make_ipv6_addr(&sin6); if (pch == h->h_addr_list && alen >= sizeof(sin6)) memcpy((char *) addr, &sin6, sizeof(sin6)); @@ -5934,14 +5939,11 @@ socket_inet_ntop(PyObject *self, PyObject *args) Py_buffer packed_ip; const char* retval; #ifdef ENABLE_IPV6 - char ip[Py_MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN) + 1]; + char ip[Py_MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)]; #else - char ip[INET_ADDRSTRLEN + 1]; + char ip[INET_ADDRSTRLEN]; #endif - /* Guarantee NUL-termination for PyUnicode_FromString() below */ - memset((void *) &ip[0], '\0', sizeof(ip)); - if (!PyArg_ParseTuple(args, "iy*:inet_ntop", &af, &packed_ip)) { return NULL; } @@ -5969,6 +5971,7 @@ socket_inet_ntop(PyObject *self, PyObject *args) return NULL; } + /* inet_ntop guarantee NUL-termination of resulting string. */ retval = inet_ntop(af, packed_ip.buf, ip, sizeof(ip)); PyBuffer_Release(&packed_ip); if (!retval) {