Skip to content

Commit 5c1c46f

Browse files
authored
Configure socketcand TCP socket to reduce latency (#1683)
* 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
1 parent 38c4dc4 commit 5c1c46f

File tree

3 files changed

+55
-1
lines changed

3 files changed

+55
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Features
3939
* PCAN: Optimize send performance (#1640)
4040
* PCAN: Support version string of older PCAN basic API (#1644)
4141
* Kvaser: add parameter exclusive and `override_exclusive` (#1660)
42+
* socketcand: Add parameter `tcp_tune` to reduce latency (#1683)
4243

4344
### Miscellaneous
4445
* Distinguish Text/Binary-IO for Reader/Writer classes. (#1585)

can/interfaces/socketcand/socketcand.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
http://www.domologic.de
99
"""
1010
import logging
11+
import os
1112
import select
1213
import socket
1314
import time
@@ -75,10 +76,42 @@ def connect_to_server(s, host, port):
7576

7677

7778
class SocketCanDaemonBus(can.BusABC):
78-
def __init__(self, channel, host, port, can_filters=None, **kwargs):
79+
def __init__(self, channel, host, port, tcp_tune=False, can_filters=None, **kwargs):
80+
"""Connects to a CAN bus served by socketcand.
81+
82+
It will attempt to connect to the server for up to 10s, after which a
83+
TimeoutError exception will be thrown.
84+
85+
If the handshake with the socketcand server fails, a CanError exception
86+
is thrown.
87+
88+
:param channel:
89+
The can interface name served by socketcand.
90+
An example channel would be 'vcan0' or 'can0'.
91+
:param host:
92+
The host address of the socketcand server.
93+
:param port:
94+
The port of the socketcand server.
95+
:param tcp_tune:
96+
This tunes the TCP socket for low latency (TCP_NODELAY, and
97+
TCP_QUICKACK).
98+
This option is not available under windows.
99+
:param can_filters:
100+
See :meth:`can.BusABC.set_filters`.
101+
"""
79102
self.__host = host
80103
self.__port = port
104+
105+
self.__tcp_tune = tcp_tune
81106
self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
107+
108+
if self.__tcp_tune:
109+
if os.name == "nt":
110+
self.__tcp_tune = False
111+
log.warning("'tcp_tune' not available in Windows. Setting to False")
112+
else:
113+
self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
114+
82115
self.__message_buffer = deque()
83116
self.__receive_buffer = "" # i know string is not the most efficient here
84117
self.channel = channel
@@ -120,6 +153,8 @@ def _recv_internal(self, timeout):
120153
ascii_msg = self.__socket.recv(1024).decode(
121154
"ascii"
122155
) # may contain multiple messages
156+
if self.__tcp_tune:
157+
self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
123158
self.__receive_buffer += ascii_msg
124159
log.debug(f"Received Ascii Message: {ascii_msg}")
125160
buffer_view = self.__receive_buffer
@@ -173,16 +208,26 @@ def _recv_internal(self, timeout):
173208
def _tcp_send(self, msg: str):
174209
log.debug(f"Sending TCP Message: '{msg}'")
175210
self.__socket.sendall(msg.encode("ascii"))
211+
if self.__tcp_tune:
212+
self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
176213

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

182221
def send(self, msg, timeout=None):
222+
"""Transmit a message to the CAN bus.
223+
224+
:param msg: A message object.
225+
:param timeout: Ignored
226+
"""
183227
ascii_msg = convert_can_message_to_ascii_message(msg)
184228
self._tcp_send(ascii_msg)
185229

186230
def shutdown(self):
231+
"""Stops all active periodic tasks and closes the socket."""
187232
super().shutdown()
188233
self.__socket.close()

doc/interfaces/socketcand.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ The output may look like this::
3737
Timestamp: 1637791111.609763 ID: 0000031d X Rx DLC: 8 16 27 d8 3d fe d8 31 24
3838
Timestamp: 1637791111.634630 ID: 00000587 X Rx DLC: 8 4e 06 85 23 6f 81 2b 65
3939

40+
Bus
41+
---
42+
43+
.. autoclass:: can.interfaces.socketcand.SocketCanDaemonBus
44+
:show-inheritance:
45+
:member-order: bysource
46+
:members:
47+
4048
Socketcand Quickstart
4149
---------------------
4250

0 commit comments

Comments
 (0)