【发布时间】:2020-01-12 21:00:22
【问题描述】:
我的设置和环境:
- Win10
- TCP 客户端:带有
asio库的 C++(无增强),在后台线程中运行。 - TCP 服务器:带有
SocketServer模块的 Python3,在后台线程中运行。 - 目前都使用阻塞 I/O
要求:
- 客户端偶尔向服务器发送用户交互的字符串命令。
- 服务器接收命令并执行操作。
问题:
- 客户端可能会挂在
read()。 - 在客户端挂起后,服务器可能会在
recv()处挂起。
“净”结果是服务器总是得到我最初的“Hello”握手,但它的响应字符串ACK 从未被客户端收到。看起来需要在客户端和服务器之间进行某种读/写协调。
从https://stackoverflow.com/a/1480246/987846 和Does the TCPServer + BaseRequestHandler in Python's SocketServer close the socket after each call to handle()? 之类的问题中,我了解到可能涉及两个问题
- Python TCP 服务器总是在接收时关闭连接,这样处理程序就不会被连续调用。
- TCP 连接在因超时而关闭之前需要保持活动状态。
我想知道如何解决这个问题,什么是满足我要求的最佳策略
- 在服务器和客户端之间设计一个周期性的“健全性检查”乒乓数据传输。
- 求助于我不熟悉的非阻塞 I/O。
或者这只是我的一个错误?
服务器代码:
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)
客户端代码(部分)
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;
}
}
如果我删除服务器端的while循环,它看起来像
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
然后
- 客户端收到服务器的
ACK。 - 但是Client发送的后续消息不会被Server接收到。
【问题讨论】:
-
我通过专门使用非阻塞 I/O 来避免这些问题;使用非阻塞 I/O,您永远不会因为阻塞调用而失去对线程的控制。 (这确实意味着您必须实现状态机代码来处理部分读取和写入,但这都是可行的)
-
这是您代码中的错误。您应该在两端使用读取超时。但是,如果每次您的客户端应该收到流结束指示时服务器真的关闭连接。
-
@JeremyFriesner 您的非阻塞 I/O 技术到底是什么?是asio的异步API还是原始套接字
select? -
@user207421 如果我使用超时,是否意味着套接字将在超时后关闭,然后客户端必须在每次通信时重新连接到服务器套接字?
-
不,如果发生超时,它不会关闭,它只会抛出任何涉及的异常,或者返回任何涉及的错误代码。读取超时对连接来说不是致命的。
标签: python c++ sockets networking