openrvdas.logger.writers.tcp_writer
No module-level documentation available.
1#!/usr/bin/env python3 2 3import logging 4import socket 5 6from typing import Union 7 8from logger.writers.writer import Writer # noqa: E402 9 10 11class TCPWriter(Writer): 12 """Write TCP packtes to network.""" 13 def __init__(self, destination, port, 14 num_retry=2, warning_limit=5, eol='', 15 reuseaddr=False, reuseport=False, **kwargs): 16 """ 17 Write records to a TCP network socket. 18 19 ``` 20 destination The destination to send TCP packets to. Can be resolvable hostname 21 or valid IP address. 22 23 port Port to which packets should be sent 24 25 num_retry Number of times to retry if write fails. 26 27 warning_limit Number of times the writer gives up on writing a message 28 (without any intervening successes) before it gives up complaining 29 about failures. 30 31 eol If specified, an end of line string to append to record 32 before sending 33 34 reuseaddr Specifies wether to set SO_REUSEADDR on the created socket. If 35 you don't know you need this, don't enable it. 36 37 reuseport Specifies wether to set SO_REUSEPORT on the created socket. If 38 you don't know you need this, don't enable it. 39 40 encoding - 'utf-8' by default. If empty or None, do not attempt any 41 decoding and return raw bytes. Other possible encodings are 42 listed in online documentation here: 43 https://docs.python.org/3/library/codecs.html#standard-encodings 44 45 encoding_errors - 'ignore' by default. Other error strategies are 46 'strict', 'replace', and 'backslashreplace', described here: 47 https://docs.python.org/3/howto/unicode.html#encodings 48 49 ``` 50 """ 51 super().__init__(**kwargs) # processes 'quiet', encodings and type hints 52 53 self.num_retry = num_retry 54 self.warning_limit = warning_limit 55 self.num_warnings = 0 56 57 # 'eol' comes in as a (probably escaped) string. We need to 58 # unescape it, which means converting to bytes and back. 59 if eol is not None and self.encoding: 60 eol = self._unescape_str(eol) 61 self.eol = eol 62 63 # do name resolution once in the constructor 64 # 65 # NOTE: This means the hostname must be valid when we start, otherwise 66 # the config_check code will puke. That's fine. The alternative 67 # is we let name resolution happen while we're running, but then 68 # each failed lookup is going to block our write() routine for a 69 # few seconds - not good. 70 # 71 # NOTE: This also catches specifying impropperly formatted IP 72 # addresses. The only way through gethostbyname() w/out throwing 73 # an exception is to provide a valid hostname or IP address. 74 # Propperly formatted IPs just get returned. 75 # 76 self.destination = socket.gethostbyname(destination) 77 78 # make sure port gets stored as an int, even if passed in as a string 79 self.port = int(port) 80 81 self.reuseaddr = reuseaddr 82 self.reuseport = reuseport 83 84 # socket gets initialized on-demand in write() 85 # 86 # NOTE: Since connect() can actually fail w/ a TCP socket, we don't try 87 # that here. Let's just do safe things. 88 # 89 self.socket = None 90 91 ############################ 92 def __del__(self): 93 if self.socket: 94 logging.debug('__del__: closing socket') 95 self._close_socket(self.socket) 96 97 ############################ 98 def _open_socket(self): 99 """Do socket prep so we're ready to write(). Returns socket object or None on 100 failure. 101 """ 102 this_socket = socket.socket(family=socket.AF_INET, 103 type=socket.SOCK_STREAM, 104 proto=socket.IPPROTO_TCP) 105 if self.reuseaddr: 106 this_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) 107 if self.reuseport: 108 try: # Raspbian doesn't recognize SO_REUSEPORT 109 this_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, True) 110 except AttributeError: 111 logging.warning('Unable to set socket REUSEPORT; may be unsupported') 112 113 # Try connecting 114 try: 115 this_socket.connect((self.destination, self.port)) 116 except OSError as e: 117 if self.num_warnings < self.warning_limit: 118 logging.error('Unable to connect to %s:%d: %s', self.destination, self.port, e) 119 self.num_warnings += 1 120 if self.num_warnings == self.warning_limit: 121 logging.error('TCPWriter._open_socket() - muting errors') 122 return None 123 124 # success, reset warning counter 125 if self.num_warnings == self.warning_limit: 126 logging.info('TCPWriter._open_socket() successfully connected after a series of ' 127 'failures; restting warnings.') 128 self.num_warnings = 0 129 return this_socket 130 131 ############################ 132 def _close_socket(self, s): 133 try: 134 s.shutdown(socket.SHUT_RDWR) 135 s.close() 136 except OSError: 137 logging.debug('Unable to close socket') 138 139 ############################ 140 def write(self, record: Union[str, bytes]): 141 """Write the record to the network.""" 142 143 # See if it's something we can process, and if not, try digesting 144 if not self.can_process_record(record): # inherited from BaseModule() 145 self.digest_record(record) # inherited from BaseModule() 146 return 147 148 # Append eol if configured 149 if self.eol: 150 record += self.eol 151 152 # NOTE: Unlike UDP socket, which really only can detect failure during 153 # send() (and even then only very poorly), a TCP connect() can 154 # fail, so we need to track attempts to connect in order to honor 155 # `num_retry` on a disconnected socket. 156 # 157 # We also need to tear down and start over with a fresh connect() 158 # if send() fails. 159 # 160 num_tries = 0 161 bytes_sent = 0 162 rec_len = len(record) 163 while num_tries <= self.num_retry and bytes_sent < rec_len: 164 num_tries += 1 165 # attempt to connect socket if needed, up to `num_tries` times 166 if not self.socket: 167 self.socket = self._open_socket() 168 if not self.socket: 169 # no need for further error messages, _open_socket() will have 170 # already complained sufficiently 171 continue 172 173 # attempt to verify we're really connected 174 # 175 # NOTE: This is because the only way to detect a closed TCP socket 176 # is via recv(), which is awkward when we're trying to 177 # guarantee a write was successful. 178 # 179 # https://stackoverflow.com/questions/49457631/c-server-socket-closed-but-client-socket-still-able-send-two-more-packages 180 # 181 # "send() just puts the data in the kernel socket buffer, 182 # it doesn't wait for the data to be transmitted or the 183 # server to acknowledge receipt of it. 184 # 185 # You don't get SIGPIPE until the data is transmitted and 186 # the server rejects it by sending a RST segment. 187 # 188 # It works this way because each direction of a TCP 189 # connection is treated independently. When the server 190 # closes the socket, it sends a FIN segment. This just 191 # tells the client that the server is done sending data, it 192 # doesn't mean that the server cannot receive data. There's 193 # nothing in the TCP protocol that allows the server to 194 # inform the client of this. So the only way to find out 195 # that it's not accepting any more data is when the client 196 # gets that RST response. 197 # 198 # Informing the client that they shouldn't send anything 199 # more is usually done in the application protocol, since 200 # it's not available in TCP." 201 # 202 # So it might take a couple "successful" send() calls before 203 # the socket layer notices nobody's home on the other end. 204 # 205 # Fine then, let's do a 1 byte recv() w/ flags arranged so we 206 # don't really read off the socket forcing it to raise 207 # BlockingIOError if the socket is still connected. Not 208 # perfect, because the remote side could still close after 209 # this call but before our send() below, but it's something. 210 # 211 try: 212 # If the socket is still connected, this will raise 213 # BlockingIOError. If it's not, it returns 0 bytes. 214 msg = self.socket.recv(1, socket.MSG_DONTWAIT | socket.MSG_PEEK) # noqa F841 215 logging.error('TCPWriter: connection closed') 216 self.num_warnings += 1 217 self._close_socket(self.socket) 218 self.socket = None 219 continue 220 except BlockingIOError: 221 pass 222 223 # we're connected, try sending 224 try: 225 bytes_sent = self.socket.send(self._encode_str(record)) 226 except OSError as e: 227 # send failed, we need to disconnect and start over 228 # 229 # NOTE: In order to get this far, we have to have successfully 230 # connected, which means we JUST reset self.num_warnings 231 # 232 logging.error('TCPWriter: send() error: %s:%d: %s', 233 self.destination, self.port, str(e)) 234 self.num_warnings += 1 235 self._close_socket(self.socket) 236 self.socket = None 237 continue 238 239 # check to see if we really wrote it all 240 if bytes_sent < rec_len and not self.quiet: 241 logging.warning('TCPWriter: send() did not send the whole record: ' 242 'bytes_sent=%d, rec_len=%d', bytes_sent, rec_len) 243 244 logging.debug('TCPWriter.write() wrote %d/%d bytes after %d tries', 245 bytes_sent, rec_len, num_tries)
class
TCPWriter(logger.writers.writer.Writer):
12class TCPWriter(Writer): 13 """Write TCP packtes to network.""" 14 def __init__(self, destination, port, 15 num_retry=2, warning_limit=5, eol='', 16 reuseaddr=False, reuseport=False, **kwargs): 17 """ 18 Write records to a TCP network socket. 19 20 ``` 21 destination The destination to send TCP packets to. Can be resolvable hostname 22 or valid IP address. 23 24 port Port to which packets should be sent 25 26 num_retry Number of times to retry if write fails. 27 28 warning_limit Number of times the writer gives up on writing a message 29 (without any intervening successes) before it gives up complaining 30 about failures. 31 32 eol If specified, an end of line string to append to record 33 before sending 34 35 reuseaddr Specifies wether to set SO_REUSEADDR on the created socket. If 36 you don't know you need this, don't enable it. 37 38 reuseport Specifies wether to set SO_REUSEPORT on the created socket. If 39 you don't know you need this, don't enable it. 40 41 encoding - 'utf-8' by default. If empty or None, do not attempt any 42 decoding and return raw bytes. Other possible encodings are 43 listed in online documentation here: 44 https://docs.python.org/3/library/codecs.html#standard-encodings 45 46 encoding_errors - 'ignore' by default. Other error strategies are 47 'strict', 'replace', and 'backslashreplace', described here: 48 https://docs.python.org/3/howto/unicode.html#encodings 49 50 ``` 51 """ 52 super().__init__(**kwargs) # processes 'quiet', encodings and type hints 53 54 self.num_retry = num_retry 55 self.warning_limit = warning_limit 56 self.num_warnings = 0 57 58 # 'eol' comes in as a (probably escaped) string. We need to 59 # unescape it, which means converting to bytes and back. 60 if eol is not None and self.encoding: 61 eol = self._unescape_str(eol) 62 self.eol = eol 63 64 # do name resolution once in the constructor 65 # 66 # NOTE: This means the hostname must be valid when we start, otherwise 67 # the config_check code will puke. That's fine. The alternative 68 # is we let name resolution happen while we're running, but then 69 # each failed lookup is going to block our write() routine for a 70 # few seconds - not good. 71 # 72 # NOTE: This also catches specifying impropperly formatted IP 73 # addresses. The only way through gethostbyname() w/out throwing 74 # an exception is to provide a valid hostname or IP address. 75 # Propperly formatted IPs just get returned. 76 # 77 self.destination = socket.gethostbyname(destination) 78 79 # make sure port gets stored as an int, even if passed in as a string 80 self.port = int(port) 81 82 self.reuseaddr = reuseaddr 83 self.reuseport = reuseport 84 85 # socket gets initialized on-demand in write() 86 # 87 # NOTE: Since connect() can actually fail w/ a TCP socket, we don't try 88 # that here. Let's just do safe things. 89 # 90 self.socket = None 91 92 ############################ 93 def __del__(self): 94 if self.socket: 95 logging.debug('__del__: closing socket') 96 self._close_socket(self.socket) 97 98 ############################ 99 def _open_socket(self): 100 """Do socket prep so we're ready to write(). Returns socket object or None on 101 failure. 102 """ 103 this_socket = socket.socket(family=socket.AF_INET, 104 type=socket.SOCK_STREAM, 105 proto=socket.IPPROTO_TCP) 106 if self.reuseaddr: 107 this_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) 108 if self.reuseport: 109 try: # Raspbian doesn't recognize SO_REUSEPORT 110 this_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, True) 111 except AttributeError: 112 logging.warning('Unable to set socket REUSEPORT; may be unsupported') 113 114 # Try connecting 115 try: 116 this_socket.connect((self.destination, self.port)) 117 except OSError as e: 118 if self.num_warnings < self.warning_limit: 119 logging.error('Unable to connect to %s:%d: %s', self.destination, self.port, e) 120 self.num_warnings += 1 121 if self.num_warnings == self.warning_limit: 122 logging.error('TCPWriter._open_socket() - muting errors') 123 return None 124 125 # success, reset warning counter 126 if self.num_warnings == self.warning_limit: 127 logging.info('TCPWriter._open_socket() successfully connected after a series of ' 128 'failures; restting warnings.') 129 self.num_warnings = 0 130 return this_socket 131 132 ############################ 133 def _close_socket(self, s): 134 try: 135 s.shutdown(socket.SHUT_RDWR) 136 s.close() 137 except OSError: 138 logging.debug('Unable to close socket') 139 140 ############################ 141 def write(self, record: Union[str, bytes]): 142 """Write the record to the network.""" 143 144 # See if it's something we can process, and if not, try digesting 145 if not self.can_process_record(record): # inherited from BaseModule() 146 self.digest_record(record) # inherited from BaseModule() 147 return 148 149 # Append eol if configured 150 if self.eol: 151 record += self.eol 152 153 # NOTE: Unlike UDP socket, which really only can detect failure during 154 # send() (and even then only very poorly), a TCP connect() can 155 # fail, so we need to track attempts to connect in order to honor 156 # `num_retry` on a disconnected socket. 157 # 158 # We also need to tear down and start over with a fresh connect() 159 # if send() fails. 160 # 161 num_tries = 0 162 bytes_sent = 0 163 rec_len = len(record) 164 while num_tries <= self.num_retry and bytes_sent < rec_len: 165 num_tries += 1 166 # attempt to connect socket if needed, up to `num_tries` times 167 if not self.socket: 168 self.socket = self._open_socket() 169 if not self.socket: 170 # no need for further error messages, _open_socket() will have 171 # already complained sufficiently 172 continue 173 174 # attempt to verify we're really connected 175 # 176 # NOTE: This is because the only way to detect a closed TCP socket 177 # is via recv(), which is awkward when we're trying to 178 # guarantee a write was successful. 179 # 180 # https://stackoverflow.com/questions/49457631/c-server-socket-closed-but-client-socket-still-able-send-two-more-packages 181 # 182 # "send() just puts the data in the kernel socket buffer, 183 # it doesn't wait for the data to be transmitted or the 184 # server to acknowledge receipt of it. 185 # 186 # You don't get SIGPIPE until the data is transmitted and 187 # the server rejects it by sending a RST segment. 188 # 189 # It works this way because each direction of a TCP 190 # connection is treated independently. When the server 191 # closes the socket, it sends a FIN segment. This just 192 # tells the client that the server is done sending data, it 193 # doesn't mean that the server cannot receive data. There's 194 # nothing in the TCP protocol that allows the server to 195 # inform the client of this. So the only way to find out 196 # that it's not accepting any more data is when the client 197 # gets that RST response. 198 # 199 # Informing the client that they shouldn't send anything 200 # more is usually done in the application protocol, since 201 # it's not available in TCP." 202 # 203 # So it might take a couple "successful" send() calls before 204 # the socket layer notices nobody's home on the other end. 205 # 206 # Fine then, let's do a 1 byte recv() w/ flags arranged so we 207 # don't really read off the socket forcing it to raise 208 # BlockingIOError if the socket is still connected. Not 209 # perfect, because the remote side could still close after 210 # this call but before our send() below, but it's something. 211 # 212 try: 213 # If the socket is still connected, this will raise 214 # BlockingIOError. If it's not, it returns 0 bytes. 215 msg = self.socket.recv(1, socket.MSG_DONTWAIT | socket.MSG_PEEK) # noqa F841 216 logging.error('TCPWriter: connection closed') 217 self.num_warnings += 1 218 self._close_socket(self.socket) 219 self.socket = None 220 continue 221 except BlockingIOError: 222 pass 223 224 # we're connected, try sending 225 try: 226 bytes_sent = self.socket.send(self._encode_str(record)) 227 except OSError as e: 228 # send failed, we need to disconnect and start over 229 # 230 # NOTE: In order to get this far, we have to have successfully 231 # connected, which means we JUST reset self.num_warnings 232 # 233 logging.error('TCPWriter: send() error: %s:%d: %s', 234 self.destination, self.port, str(e)) 235 self.num_warnings += 1 236 self._close_socket(self.socket) 237 self.socket = None 238 continue 239 240 # check to see if we really wrote it all 241 if bytes_sent < rec_len and not self.quiet: 242 logging.warning('TCPWriter: send() did not send the whole record: ' 243 'bytes_sent=%d, rec_len=%d', bytes_sent, rec_len) 244 245 logging.debug('TCPWriter.write() wrote %d/%d bytes after %d tries', 246 bytes_sent, rec_len, num_tries)
Write TCP packtes to network.
TCPWriter( destination, port, num_retry=2, warning_limit=5, eol='', reuseaddr=False, reuseport=False, **kwargs)
14 def __init__(self, destination, port, 15 num_retry=2, warning_limit=5, eol='', 16 reuseaddr=False, reuseport=False, **kwargs): 17 """ 18 Write records to a TCP network socket. 19 20 ``` 21 destination The destination to send TCP packets to. Can be resolvable hostname 22 or valid IP address. 23 24 port Port to which packets should be sent 25 26 num_retry Number of times to retry if write fails. 27 28 warning_limit Number of times the writer gives up on writing a message 29 (without any intervening successes) before it gives up complaining 30 about failures. 31 32 eol If specified, an end of line string to append to record 33 before sending 34 35 reuseaddr Specifies wether to set SO_REUSEADDR on the created socket. If 36 you don't know you need this, don't enable it. 37 38 reuseport Specifies wether to set SO_REUSEPORT on the created socket. If 39 you don't know you need this, don't enable it. 40 41 encoding - 'utf-8' by default. If empty or None, do not attempt any 42 decoding and return raw bytes. Other possible encodings are 43 listed in online documentation here: 44 https://docs.python.org/3/library/codecs.html#standard-encodings 45 46 encoding_errors - 'ignore' by default. Other error strategies are 47 'strict', 'replace', and 'backslashreplace', described here: 48 https://docs.python.org/3/howto/unicode.html#encodings 49 50 ``` 51 """ 52 super().__init__(**kwargs) # processes 'quiet', encodings and type hints 53 54 self.num_retry = num_retry 55 self.warning_limit = warning_limit 56 self.num_warnings = 0 57 58 # 'eol' comes in as a (probably escaped) string. We need to 59 # unescape it, which means converting to bytes and back. 60 if eol is not None and self.encoding: 61 eol = self._unescape_str(eol) 62 self.eol = eol 63 64 # do name resolution once in the constructor 65 # 66 # NOTE: This means the hostname must be valid when we start, otherwise 67 # the config_check code will puke. That's fine. The alternative 68 # is we let name resolution happen while we're running, but then 69 # each failed lookup is going to block our write() routine for a 70 # few seconds - not good. 71 # 72 # NOTE: This also catches specifying impropperly formatted IP 73 # addresses. The only way through gethostbyname() w/out throwing 74 # an exception is to provide a valid hostname or IP address. 75 # Propperly formatted IPs just get returned. 76 # 77 self.destination = socket.gethostbyname(destination) 78 79 # make sure port gets stored as an int, even if passed in as a string 80 self.port = int(port) 81 82 self.reuseaddr = reuseaddr 83 self.reuseport = reuseport 84 85 # socket gets initialized on-demand in write() 86 # 87 # NOTE: Since connect() can actually fail w/ a TCP socket, we don't try 88 # that here. Let's just do safe things. 89 # 90 self.socket = None
Write records to a TCP network socket.
destination The destination to send TCP packets to. Can be resolvable hostname
or valid IP address.
port Port to which packets should be sent
num_retry Number of times to retry if write fails.
warning_limit Number of times the writer gives up on writing a message
(without any intervening successes) before it gives up complaining
about failures.
eol If specified, an end of line string to append to record
before sending
reuseaddr Specifies wether to set SO_REUSEADDR on the created socket. If
you don't know you need this, don't enable it.
reuseport Specifies wether to set SO_REUSEPORT on the created socket. If
you don't know you need this, don't enable it.
encoding - 'utf-8' by default. If empty or None, do not attempt any
decoding and return raw bytes. Other possible encodings are
listed in online documentation here:
https://docs.python.org/3/library/codecs.html#standard-encodings
encoding_errors - 'ignore' by default. Other error strategies are
'strict', 'replace', and 'backslashreplace', described here:
https://docs.python.org/3/howto/unicode.html#encodings
def
write(self, record: Union[str, bytes]):
141 def write(self, record: Union[str, bytes]): 142 """Write the record to the network.""" 143 144 # See if it's something we can process, and if not, try digesting 145 if not self.can_process_record(record): # inherited from BaseModule() 146 self.digest_record(record) # inherited from BaseModule() 147 return 148 149 # Append eol if configured 150 if self.eol: 151 record += self.eol 152 153 # NOTE: Unlike UDP socket, which really only can detect failure during 154 # send() (and even then only very poorly), a TCP connect() can 155 # fail, so we need to track attempts to connect in order to honor 156 # `num_retry` on a disconnected socket. 157 # 158 # We also need to tear down and start over with a fresh connect() 159 # if send() fails. 160 # 161 num_tries = 0 162 bytes_sent = 0 163 rec_len = len(record) 164 while num_tries <= self.num_retry and bytes_sent < rec_len: 165 num_tries += 1 166 # attempt to connect socket if needed, up to `num_tries` times 167 if not self.socket: 168 self.socket = self._open_socket() 169 if not self.socket: 170 # no need for further error messages, _open_socket() will have 171 # already complained sufficiently 172 continue 173 174 # attempt to verify we're really connected 175 # 176 # NOTE: This is because the only way to detect a closed TCP socket 177 # is via recv(), which is awkward when we're trying to 178 # guarantee a write was successful. 179 # 180 # https://stackoverflow.com/questions/49457631/c-server-socket-closed-but-client-socket-still-able-send-two-more-packages 181 # 182 # "send() just puts the data in the kernel socket buffer, 183 # it doesn't wait for the data to be transmitted or the 184 # server to acknowledge receipt of it. 185 # 186 # You don't get SIGPIPE until the data is transmitted and 187 # the server rejects it by sending a RST segment. 188 # 189 # It works this way because each direction of a TCP 190 # connection is treated independently. When the server 191 # closes the socket, it sends a FIN segment. This just 192 # tells the client that the server is done sending data, it 193 # doesn't mean that the server cannot receive data. There's 194 # nothing in the TCP protocol that allows the server to 195 # inform the client of this. So the only way to find out 196 # that it's not accepting any more data is when the client 197 # gets that RST response. 198 # 199 # Informing the client that they shouldn't send anything 200 # more is usually done in the application protocol, since 201 # it's not available in TCP." 202 # 203 # So it might take a couple "successful" send() calls before 204 # the socket layer notices nobody's home on the other end. 205 # 206 # Fine then, let's do a 1 byte recv() w/ flags arranged so we 207 # don't really read off the socket forcing it to raise 208 # BlockingIOError if the socket is still connected. Not 209 # perfect, because the remote side could still close after 210 # this call but before our send() below, but it's something. 211 # 212 try: 213 # If the socket is still connected, this will raise 214 # BlockingIOError. If it's not, it returns 0 bytes. 215 msg = self.socket.recv(1, socket.MSG_DONTWAIT | socket.MSG_PEEK) # noqa F841 216 logging.error('TCPWriter: connection closed') 217 self.num_warnings += 1 218 self._close_socket(self.socket) 219 self.socket = None 220 continue 221 except BlockingIOError: 222 pass 223 224 # we're connected, try sending 225 try: 226 bytes_sent = self.socket.send(self._encode_str(record)) 227 except OSError as e: 228 # send failed, we need to disconnect and start over 229 # 230 # NOTE: In order to get this far, we have to have successfully 231 # connected, which means we JUST reset self.num_warnings 232 # 233 logging.error('TCPWriter: send() error: %s:%d: %s', 234 self.destination, self.port, str(e)) 235 self.num_warnings += 1 236 self._close_socket(self.socket) 237 self.socket = None 238 continue 239 240 # check to see if we really wrote it all 241 if bytes_sent < rec_len and not self.quiet: 242 logging.warning('TCPWriter: send() did not send the whole record: ' 243 'bytes_sent=%d, rec_len=%d', bytes_sent, rec_len) 244 245 logging.debug('TCPWriter.write() wrote %d/%d bytes after %d tries', 246 bytes_sent, rec_len, num_tries)
Write the record to the network.