diff --git a/src/main/scala/io/stepfunc/dnp3c/protocol/channel/TcpClientChannelFactory.scala b/src/main/scala/io/stepfunc/dnp3c/protocol/channel/TcpClientChannelFactory.scala index 1f629f8..a3826fa 100644 --- a/src/main/scala/io/stepfunc/dnp3c/protocol/channel/TcpClientChannelFactory.scala +++ b/src/main/scala/io/stepfunc/dnp3c/protocol/channel/TcpClientChannelFactory.scala @@ -46,10 +46,14 @@ object TcpClientChannelFactory { } // connects the socket; a failure to use its local address, e.g. one whose port is still held by a connection to the - // same remote address and port that closed moments ago, is LocalBindException + // same remote address and port that closed moments ago, is LocalBindException. Nagle's algorithm is off, as in DNP3 + // stacks, so a small write goes out at once rather than waiting for the previous one to be acknowledged, which would + // shift the timing the procedures check def connect(socket: SocketChannel, remote: SocketAddress, local: Option[InetSocketAddress], timeoutMs: Int): Unit = - try socket.socket().connect(remote, timeoutMs) - catch { + try { + socket.setOption(StandardSocketOptions.TCP_NODELAY, java.lang.Boolean.TRUE) + socket.socket().connect(remote, timeoutMs) + } catch { case ex: java.net.BindException => throw new LocalBindException(local.getOrElse(new InetSocketAddress(0)), ex) } diff --git a/src/test/scala/io/stepfunc/dnp3c/protocol/channel/TcpClientChannelFactoryTest.scala b/src/test/scala/io/stepfunc/dnp3c/protocol/channel/TcpClientChannelFactoryTest.scala new file mode 100644 index 0000000..58346c3 --- /dev/null +++ b/src/test/scala/io/stepfunc/dnp3c/protocol/channel/TcpClientChannelFactoryTest.scala @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright Step Function I/O LLC + +package io.stepfunc.dnp3c.protocol.channel + +import java.net.{ InetAddress, InetSocketAddress, ServerSocket, StandardSocketOptions } +import java.nio.channels.SocketChannel + +import scala.util.Using + +import org.junit.runner.RunWith +import org.scalatestplus.junit.JUnitRunner +import org.scalatest.matchers.should.Matchers +import org.scalatest.funsuite.AnyFunSuite + +@RunWith(classOf[JUnitRunner]) +class TcpClientChannelFactoryTest extends AnyFunSuite with Matchers { + + test("a connection has Nagle's algorithm off, so each write goes out at once") { + Using.resource(new ServerSocket(0, 1, InetAddress.getLoopbackAddress)) { server => + Using.resource(SocketChannel.open()) { socket => + TcpClientChannelFactory.connect(socket, server.getLocalSocketAddress, None, 1000) + socket.getOption(StandardSocketOptions.TCP_NODELAY) should equal(true) + } + } + } +}