My setup and environment:
- Win10
- TCP Client: C++ with
asio
library (no boost), running in background thread. - TCP Server: Python3 with
SocketServer
module, running in background thread. - All use blocking I/O at the moment
Requriments:
- Client sends string commands on user interaction to server occasionally.
- Server receives commands and do stuff.
Problem:
- Client may hang at
read()
. - Server may hang at
recv()
after Client hangs.
The "net" result is that Server always gets my initial "Hello" handshaking, but its response string ACK
was never received by Client. Looks like some sort of read/write coordination between Client and Server is required.
From questions like https://stackoverflow.com/a/1480246/987846 and Does the TCPServer + BaseRequestHandler in Python's SocketServer close the socket after each call to handle()? I learned that two problems may be involved
- Python TCP server always closes the connection on receiving so that the handler is not called continuously.
- TCP connection needs a keep-alive mechanism before it's closed due to a timeout.
I wonder how to fix this and what would be the best strategy for my requirements
- Design a periodic "sanity-check" ping-pong data transmission between Server and Client.
- Resort to non-blocking I/O, which I'm unfamiliar with.
Or is it simply a bug on my part?
Server code:
import socketserver
import sys
import threading
_dostuff = True
class CmdHandler(socketserver.StreamRequestHandler):
def handle(self):
while True:
data = self.request.recv(1024)
s = data.decode('utf-8')
if s == 'Hello':
print('HANDSHAKE: ACK', flush=True)
self.request.send('ACK\x00'.encode())
if s == 'Stop':
print('Cmd: Mute', flush=True)
with threading.Lock():
_dostuff = False
if s == 'Start':
with threading.Lock():
_dostuff = True
return
if __name__ == '__main__':
import socket
import time
# Command server
address = ('localhost', 1234) # let the kernel assign a port
cmd_server = socketserver.TCPServer(address, CmdHandler)
cmd_ip, cmd_port = cmd_server.server_address # what port was assigned?
t1 = threading.Thread(target=cmd_server.serve_forever)
t1.setDaemon(True) # don't hang on exit
t1.start()
while True:
time.sleep(1)
client code (partial)
virtual bool Connect() override {
bool isInitialized = false;
try {
asio::io_context io_context;
asio::ip::tcp::resolver resolver(io_context);
asio::ip::tcp::resolver::query query("127.0.0.1", "1234");
asio::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
asio::ip::tcp::socket socket(io_context);
asio::connect(socket, endpoint_iterator);
while (true) {
std::array<char, 128> buf;
asio::error_code error;
// Handshaking
// - on connection, say hello to cmd-server; wait for ACK
if ( ! isInitialized ) {
debug("CmdClient {}: handshaking ...", m_id.c_str());
std::string handshake("Hello");
asio::write(socket, asio::buffer(handshake.c_str(), handshake.length()));
if (error == asio::error::eof)
continue; // Connection closed cleanly by peer; keep trying.
else if (error)
throw asio::system_error(error); // Some other error.
// ***PROBLEM: THIS MAY BLOCK FOREVER***
size_t len = asio::read(socket, asio::buffer(buf), error);
// ***PROBLEM END***
if (len <= 0) {
debug("CmdClient {}: No response", m_id.c_str());
}
std::string received = std::string(buf.data());
if (received == std::string("ACK")) {
debug("CmdClient {}: handshaking ... SUCCESS!", m_id.c_str());
isInitialized = true;
Notify("ACK");
}
else {
debug("CmdClient {}: Received: {}", m_id.c_str(), received.c_str());
}
continue;
}
SendCommand(socket);
}
}
catch (std::exception& e) {
std::cerr << e.what() << std::endl;
isInitialized = false;
}
return true;
}
void SendCommand(asio::ip::tcp::socket& socket) {
std::string cmd("");
switch (m_cmd) {
case NoOp:
break;
case Stop:
cmd = "Stop";
break;
case Start:
cmd = "Start";
break;
default:
break;
}
if (cmd.size() > 0) {
debug("CmdClient {}: Send command: {}", m_id.c_str(), cmd.c_str());
size_t len = asio::write(socket, asio::buffer(cmd.c_str(), cmd.length()));
debug("CmdClient {}: {} bytes written.", m_id.c_str(), len);
m_cmd = NoOp; // Avoid resend in next frame;
}
}
If I remove the while-loop on the server side, so that it looks like
class CmdHandler(socketserver.StreamRequestHandler):
# timeout = 5
def handle(self):
data = self.request.recv(1024)
s = data.decode('utf-8')
if s == 'Hello':
self.request.send('ACK\x00'.encode())
if s == 'Stop':
with threading.Lock():
_dostuff = False
if s == 'Start':
with threading.Lock():
_dostuff = True
return
then
- Server's
ACK
is received by Client. - But the subsequent messages sent by Client won't be received by Server.