Skip to content

Commit

Permalink
Configure socketcand TCP socket to reduce latency (#1683)
Browse files Browse the repository at this point in the history
* Configure TCP socket to reduce latency

TCP_NODELAY disables Nagles algorithm. This improves latency (reduces),
but worsens overall throughput. For the purpose of bridging a CAN bus
over a network connection to socketcand (and given the relatively low
overall bandwidth of CAN), optimizing for latency is more important.

TCP_QUICKACK disables the default delayed ACK timer. This is ~40ms in
linux (not sure about windows). The thing is, TCP_QUICKACK is reset when
you send or receive on the socket, so it needs reenabling each time.
Also, TCP_QUICKACK doesn't seem to be available in windows.

Here's a comment by John Nagle himself that some may find useful:
https://news.ycombinator.com/item?id=10608356

"That still irks me. The real problem is not tinygram prevention. It's
ACK delays, and that stupid fixed timer. They both went into TCP around
the same time, but independently. I did tinygram prevention (the Nagle
algorithm) and Berkeley did delayed ACKs, both in the early 1980s. The
combination of the two is awful. Unfortunately by the time I found about
delayed ACKs, I had changed jobs, was out of networking, and doing a
product for Autodesk on non-networked PCs.  Delayed ACKs are a win only
in certain circumstances - mostly character echo for Telnet. (When
Berkeley installed delayed ACKs, they were doing a lot of Telnet from
terminal concentrators in student terminal rooms to host VAX machines
doing the work. For that particular situation, it made sense.) The
delayed ACK timer is scaled to expected human response time. A delayed
ACK is a bet that the other end will reply to what you just sent almost
immediately. Except for some RPC protocols, this is unlikely. So the ACK
delay mechanism loses the bet, over and over, delaying the ACK, waiting
for a packet on which the ACK can be piggybacked, not getting it, and
then sending the ACK, delayed. There's nothing in TCP to automatically
turn this off. However, Linux (and I think Windows) now have a
TCP_QUICKACK socket option. Turn that on unless you have a very unusual
application.

"Turning on TCP_NODELAY has similar effects, but can make throughput
worse for small writes. If you write a loop which sends just a few bytes
(worst case, one byte) to a socket with "write()", and the Nagle
algorithm is disabled with TCP_NODELAY, each write becomes one IP
packet. This increases traffic by a factor of 40, with IP and TCP
headers for each payload. Tinygram prevention won't let you send a
second packet if you have one in flight, unless you have enough data to
fill the maximum sized packet. It accumulates bytes for one round trip
time, then sends everything in the queue. That's almost always what you
want. If you have TCP_NODELAY set, you need to be much more aware of
buffering and flushing issues.

"None of this matters for bulk one-way transfers, which is most HTTP
today. (I've never looked at the impact of this on the SSL handshake,
where it might matter.)

"Short version: set TCP_QUICKACK. If you find a case where that makes
things worse, let me know.

John Nagle"

* Make tune TCP for low latency optional

* Move os check into __init__

* Add docstrings

* Add Bus class documentation to docs/interfaces

* Update changelog
  • Loading branch information
faisal-shah authored Oct 30, 2023
1 parent 38c4dc4 commit 5c1c46f
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Features
* PCAN: Optimize send performance (#1640)
* PCAN: Support version string of older PCAN basic API (#1644)
* Kvaser: add parameter exclusive and `override_exclusive` (#1660)
* socketcand: Add parameter `tcp_tune` to reduce latency (#1683)

### Miscellaneous
* Distinguish Text/Binary-IO for Reader/Writer classes. (#1585)
Expand Down
47 changes: 46 additions & 1 deletion can/interfaces/socketcand/socketcand.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
http://www.domologic.de
"""
import logging
import os
import select
import socket
import time
Expand Down Expand Up @@ -75,10 +76,42 @@ def connect_to_server(s, host, port):


class SocketCanDaemonBus(can.BusABC):
def __init__(self, channel, host, port, can_filters=None, **kwargs):
def __init__(self, channel, host, port, tcp_tune=False, can_filters=None, **kwargs):
"""Connects to a CAN bus served by socketcand.
It will attempt to connect to the server for up to 10s, after which a
TimeoutError exception will be thrown.
If the handshake with the socketcand server fails, a CanError exception
is thrown.
:param channel:
The can interface name served by socketcand.
An example channel would be 'vcan0' or 'can0'.
:param host:
The host address of the socketcand server.
:param port:
The port of the socketcand server.
:param tcp_tune:
This tunes the TCP socket for low latency (TCP_NODELAY, and
TCP_QUICKACK).
This option is not available under windows.
:param can_filters:
See :meth:`can.BusABC.set_filters`.
"""
self.__host = host
self.__port = port

self.__tcp_tune = tcp_tune
self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

if self.__tcp_tune:
if os.name == "nt":
self.__tcp_tune = False
log.warning("'tcp_tune' not available in Windows. Setting to False")
else:
self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

self.__message_buffer = deque()
self.__receive_buffer = "" # i know string is not the most efficient here
self.channel = channel
Expand Down Expand Up @@ -120,6 +153,8 @@ def _recv_internal(self, timeout):
ascii_msg = self.__socket.recv(1024).decode(
"ascii"
) # may contain multiple messages
if self.__tcp_tune:
self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
self.__receive_buffer += ascii_msg
log.debug(f"Received Ascii Message: {ascii_msg}")
buffer_view = self.__receive_buffer
Expand Down Expand Up @@ -173,16 +208,26 @@ def _recv_internal(self, timeout):
def _tcp_send(self, msg: str):
log.debug(f"Sending TCP Message: '{msg}'")
self.__socket.sendall(msg.encode("ascii"))
if self.__tcp_tune:
self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)

def _expect_msg(self, msg):
ascii_msg = self.__socket.recv(256).decode("ascii")
if self.__tcp_tune:
self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
if not ascii_msg == msg:
raise can.CanError(f"{msg} message expected!")

def send(self, msg, timeout=None):
"""Transmit a message to the CAN bus.
:param msg: A message object.
:param timeout: Ignored
"""
ascii_msg = convert_can_message_to_ascii_message(msg)
self._tcp_send(ascii_msg)

def shutdown(self):
"""Stops all active periodic tasks and closes the socket."""
super().shutdown()
self.__socket.close()
8 changes: 8 additions & 0 deletions doc/interfaces/socketcand.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ The output may look like this::
Timestamp: 1637791111.609763 ID: 0000031d X Rx DLC: 8 16 27 d8 3d fe d8 31 24
Timestamp: 1637791111.634630 ID: 00000587 X Rx DLC: 8 4e 06 85 23 6f 81 2b 65

Bus
---

.. autoclass:: can.interfaces.socketcand.SocketCanDaemonBus
:show-inheritance:
:member-order: bysource
:members:

Socketcand Quickstart
---------------------

Expand Down

0 comments on commit 5c1c46f

Please sign in to comment.