Implement dial, support IPv6 in httpclient (#5763)

* Implement dial, support IPv6 in httpclient

Added ``dial`` procedure to networking modules: ``net``, ``asyncdispatch``,
``asyncnet``. It merges socket creation, address resolution, and connection
into single step. When using ``dial``, you don't have to worry about
IPv4 vs IPv6 problem.

Fixed addrInfo loop in connect to behave properly.
Previously it would stop on first non-immediate failure, instead of
continuing and trying the remaining addresses.

Fixed newAsyncNativeSocket to raise proper error if socket creation
fails.

Fixes: #3811

* Check domain during connect() only on non-Windows

This is how it was in the previous implementation of connect().

* Call 'osLastError' before 'close' in net.dial

* Record osLastError before freeAddrInfo in net.dial

* Add missing docs for 'dial' proc

* Optimize dial to create one FD per domain, add tests

And make async IPv6 servers work on Windows.

* Add IPv6 test to uri module

* Fix getAddrString error handling
This commit is contained in:
Ruslan Mustakov 2017-05-02 14:25:50 +07:00 • committed by Andreas Rumpf
commit ecf278c467
13 changed files with 559 additions and 334 deletions

View file

@ -66,7 +66,7 @@
##
{.deadCodeElim: on.}
import nativesockets, os, strutils, parseutils, times, sets
import nativesockets, os, strutils, parseutils, times, sets, options
export Port, `$`, `==`
export Domain, SockType, Protocol
@ -669,7 +669,7 @@ proc close*(socket: Socket) =
## Closes a socket.
try:
when defineSsl:
if socket.isSSL:
if socket.isSSL and socket.sslHandle != nil:
ErrClearError()
# As we are closing the underlying socket immediately afterwards,
# it is valid, under the TLS standard, to perform a unidirectional
@ -1477,6 +1477,63 @@ proc isIpAddress*(address_str: string): bool {.tags: [].} =
return false
return true
proc dial*(address: string, port: Port,
protocol = IPPROTO_TCP, buffered = true): Socket
{.tags: [ReadIOEffect, WriteIOEffect].} =
## Establishes connection to the specified ``address``:``port`` pair via the
## specified protocol. The procedure iterates through possible
## resolutions of the ``address`` until it succeeds, meaning that it
## seamlessly works with both IPv4 and IPv6.
## Returns Socket ready to send or receive data.
let sockType = protocol.toSockType()
let aiList = getAddrInfo(address, port, AF_UNSPEC, sockType, protocol)
var fdPerDomain: array[low(Domain).ord..high(Domain).ord, SocketHandle]
for i in low(fdPerDomain)..high(fdPerDomain):
fdPerDomain[i] = osInvalidSocket
template closeUnusedFds(domainToKeep = -1) {.dirty.} =
for i, fd in fdPerDomain:
if fd != osInvalidSocket and i != domainToKeep:
fd.close()
var success = false
var lastError: OSErrorCode
var it = aiList
var domain: Domain
var lastFd: SocketHandle
while it != nil:
let domainOpt = it.ai_family.toKnownDomain()
if domainOpt.isNone:
it = it.ai_next
continue
domain = domainOpt.unsafeGet()
lastFd = fdPerDomain[ord(domain)]
if lastFd == osInvalidSocket:
lastFd = newNativeSocket(domain, sockType, protocol)
if lastFd == osInvalidSocket:
# we always raise if socket creation failed, because it means a
# network system problem (e.g. not enough FDs), and not an unreachable
# address.
let err = osLastError()
freeAddrInfo(aiList)
closeUnusedFds()
raiseOSError(err)
fdPerDomain[ord(domain)] = lastFd
if connect(lastFd, it.ai_addr, it.ai_addrlen.SockLen) == 0'i32:
success = true
break
lastError = osLastError()
it = it.ai_next
freeAddrInfo(aiList)
closeUnusedFds(ord(domain))
if success:
result = newSocket(lastFd, domain, sockType, protocol)
elif lastError != 0.OSErrorCode:
raiseOSError(lastError)
else:
raise newException(IOError, "Couldn't resolve address: " & address)
proc connect*(socket: Socket, address: string,
port = Port(0)) {.tags: [ReadIOEffect].} =