c++ - trama - rarp y arp
Mejor estructura para la implementación de protocolos basados en solicitudes. (6)
Creo que este es un caso ideal para una implementación similar a REST. Otra forma también podría ser agrupar los métodos de manejador según la categoría / cualquier otro criterio para varias clases de trabajadores.
Estoy utilizando un protocolo, que es básicamente un protocolo de solicitud y respuesta sobre TCP, similar a otros protocolos basados en línea (SMTP, HTTP, etc.).
El protocolo tiene aproximadamente 130 métodos de solicitud diferentes (por ejemplo, inicio de sesión, adición de usuario, actualización de usuario, obtención de registro, información de archivo, información de archivos, ...). Todos estos métodos no se asignan tan bien a los métodos generales como se usan en HTTP (GET, POST, PUT, ...). Tales métodos amplios introducirían algunos giros inconsecuentes del significado real.
Pero los métodos de protocolo se pueden agrupar por tipo (por ejemplo, gestión de usuarios, gestión de archivos, gestión de sesiones, ...).
La implementación actual del lado del servidor utiliza una class Worker
con métodos ReadRequest()
(la solicitud de lecturas, que consiste en el método más la lista de parámetros), HandleRequest()
(vea a continuación) y WriteResponse()
(escribe el código de respuesta y los datos de respuesta reales).
HandleRequest()
llamará a una función para el método de solicitud real, utilizando un mapa hash del nombre del método para apuntar la función de miembro al controlador real.
El manejador real es una función miembro simple; hay uno por método de protocolo: cada uno valida sus parámetros de entrada, hace lo que tiene que hacer y establece el código de respuesta (éxito sí / no) y los datos de respuesta.
Código de ejemplo:
class Worker {
typedef bool (Worker::*CommandHandler)();
typedef std::map<UTF8String,CommandHandler> CommandHandlerMap;
// handlers will be initialized once
// e.g. m_CommandHandlers["login"] = &Worker::Handle_LOGIN;
static CommandHandlerMap m_CommandHandlers;
bool HandleRequest() {
CommandHandlerMap::const_iterator ihandler;
if( (ihandler=m_CommandHandlers.find(m_CurRequest.instruction)) != m_CommandHandler.end() ) {
// call actual handler
return (this->*(ihandler->second))();
}
// error case:
m_CurResponse.success = false;
m_CurResponse.info = "unknown or invalid instruction";
return true;
}
//...
bool Handle_LOGIN() {
const UTF8String username = m_CurRequest.parameters["username"];
const UTF8String password = m_CurRequest.parameters["password"];
// ....
if( success ) {
// initialize some state...
m_Session.Init(...);
m_LogHandle.Init(...);
m_AuthHandle.Init(...);
// set response data
m_CurResponse.success = true;
m_CurResponse.Write( "last_login", ... );
m_CurResponse.Write( "whatever", ... );
} else {
m_CurResponse.Write( "error", "failed, because ..." );
}
return true;
}
};
Asi que. El problema es que: mi clase de trabajador ahora tiene aproximadamente 130 "métodos de control de comandos". Y cada uno necesita acceso a:
- parámetros de solicitud
- objeto de respuesta (para escribir datos de respuesta)
- diferentes otros objetos locales de sesión (como un identificador de base de datos, un identificador para consultas de autorización / permiso, registro, manejadores de varios subsistemas del servidor, etc.)
¿Cuál es una buena estrategia para estructurar mejor esos métodos de manejo de comandos?
Una idea era tener una clase por controlador de comandos e inicializarla con referencias a la solicitud, objetos de respuesta, etc., pero la sobrecarga no es aceptable (en realidad, agregaría una indirección para cualquier acceso individual a todo lo que el controlador necesita : solicitud , respuesta, objetos de sesión, ...). Podría ser aceptable si proporcionara una ventaja real. Sin embargo, eso no suena muy razonable:
class HandlerBase {
protected:
Request &request;
Response &response;
Session &session;
DBHandle &db;
FooHandle &foo;
// ...
public:
HandlerBase( Request &req, Response &rsp, Session &s, ... )
: request(req), response(rsp), session(s), ...
{}
//...
virtual bool Handle() = 0;
};
class LoginHandler : public HandlerBase {
public:
LoginHandler( Request &req, Response &rsp, Session &s, ... )
: HandlerBase(req,rsp,s,..)
{}
//...
virtual bool Handle() {
// actual code for handling "login" request ...
}
};
De acuerdo, HandlerBase podría simplemente llevar una referencia (o puntero) al objeto de trabajo en sí (en lugar de refs para solicitar, respuesta, etc.). Pero eso también agregaría otro direccionamiento indirecto (esta sesión-> trabajador-> en lugar de esta sesión-). Esa indirección estaría bien, si comprara alguna ventaja después de todo.
Alguna información sobre la arquitectura en general.
El objeto de trabajo representa un solo hilo de trabajo para una conexión TCP real a algún cliente. Cada subproceso (por lo tanto, cada trabajador) necesita su propio identificador de base de datos, identificador de autorización, etc. Estos "manejadores" son objetos por subproceso que permiten el acceso a algún subsistema del servidor.
Toda esta arquitectura se basa en algún tipo de inyección de dependencia: por ejemplo, para crear un objeto de sesión, uno debe proporcionar un "identificador de base de datos" al constructor de la sesión. El objeto de sesión utiliza este identificador de base de datos para acceder a la base de datos. Nunca llamará al código global ni usará singletons. Por lo tanto, cada hilo puede ejecutarse sin ser molestado por sí mismo.
Pero el costo es que, en lugar de solo llamar a objetos singleton, el trabajador y sus manejadores de comandos deben acceder a cualquier dato u otro código del sistema a través de dichos manejadores específicos de hilos. Esos manejadores definen su contexto de ejecución.
Resumen y aclaración: Mi pregunta actual
Estoy buscando una alternativa elegante a la solución actual ("objeto de trabajo con una gran lista de métodos de manejo"): debería ser mantenible, tener poca sobrecarga y no debería requerir escribir demasiado código de pegamento. Además, DEBE seguir permitiendo que cada método controle aspectos muy diferentes de su ejecución (eso significa que si un método "super flurry foo" quiere fallar cada vez que está activada la luna llena, debe ser posible que esa implementación lo haga) . También significa que no quiero ningún tipo de abstracción de entidad (crear / leer / actualizar / eliminar tipo XFoo) en esta capa arquitectónica de mi código (existe en diferentes capas en mi código). Esta capa arquitectónica es puro protocolo, nada más.
Al final, seguramente será un compromiso, ¡pero estoy interesado en cualquier idea!
El bono AAA: una solución con implementaciones de protocolo intercambiables (en lugar de solo la class Worker
actual class Worker
, que es responsable de analizar las solicitudes y escribir las respuestas). Tal vez podría haber una class ProtocolSyntax
intercambiable class ProtocolSyntax
, que maneja esos detalles de sintaxis de protocolo, pero aún usa nuestros nuevos controladores de comando de estructura brillante.
El patrón de comando es su solución a ambos aspectos de este problema.
Úselo para implementar su controlador de protocolo con una interfaz de protocolo de IP generalizada (y / o una clase base abstracta) y diferentes implementaciones de controlador de protocolo con diferentes clases especializadas para cada protocolo.
Luego, implemente sus comandos de la misma manera con una interfaz ICommand y cada método de comando implementado en una clase separada. Ya casi has llegado con esto. Divide tus métodos existentes en nuevas clases especializadas.
Envuelva sus solicitudes y respuestas como objetos Mememento
En cuanto a la parte específica del transporte (TCP), ¿ ZMQ un vistazo a la biblioteca ZMQ que admite varios patrones de computación distribuidos a través de sockets / colas de mensajería? En mi humilde opinión, debe encontrar un patrón apropiado que satisfaga sus necesidades en su documento Guía .
Para la elección de la implementación de los mensajes de protocolo, yo personalmente preferiría los búferes de protocolo de Google, que funcionan muy bien con C ++, ahora los estamos utilizando para un par de proyectos.
Al menos se reducirá a las implementaciones de despachador y manejador para solicitudes específicas y sus parámetros + los parámetros de retorno necesarios. Las extensiones de mensajes de protobuf de Google permiten esto de una manera genérica.
EDITAR:
Para concretar un poco más, utilizando los mensajes de protobuf, la principal diferencia entre el modelo del despachador y el suyo será que no necesita realizar el análisis completo del mensaje antes del despacho, pero puede registrar manejadores que se dicen a sí mismos si pueden manejar un determinado Mensaje o no por las extensiones del mensaje. La clase (principal) de despachador no necesita saber acerca de las extensiones concretas para manejar, solo pregunta a las clases de manejadores registrados. Puede extender fácilmente este mecanismo para que ciertos sub-despachadores cubran jerarquías de categorías de mensajes más profundas.
Debido a que el compilador de protobuf ya puede ver su modelo de datos de mensajería por completo, no necesita ningún tipo de prueba de polimorfismo de clase dinámica para determinar el contenido del mensaje concreto. Su código de C ++ puede solicitar estáticamente posibles extensiones de un mensaje y no se compilará si no existe.
No sé cómo explicar esto de una mejor manera, o mostrar un ejemplo concreto de cómo mejorar su código existente con este enfoque. Me temo que ya ha realizado algunos esfuerzos en el código de des / serialización de sus formatos de mensaje, que podría haberse evitado usando mensajes de Google protobuf (¿o qué tipo de clases son Request
y Response
?).
La biblioteca ZMQ puede ayudar a implementar su contexto de Session
para enviar solicitudes a través de la infraestructura.
Ciertamente, no debe terminar en una única interfaz que maneje todo tipo de solicitudes posibles, sino en una serie de interfaces que se especializan en categorías de mensajes (puntos de extensión).
Si fuera yo, probablemente usaría una solución híbrida de los dos en tu pregunta.
Tenga una clase base de trabajador que pueda manejar múltiples comandos relacionados y que permita que su clase principal de "envío" pruebe los comandos compatibles. Para el pegamento, simplemente debe informar a la clase de despacho sobre cada clase de trabajador.
class HandlerBase
{
public:
HandlerBase(HandlerDispatch & dispatch) : m_dispatch(dispatch) {
PopulateCommands();
}
virtual ~HandlerBase();
bool CommandSupported(UTF8String & cmdName);
virtual bool HandleCommand(UTF8String & cmdName, Request & req, Response & res);
virtual void PopulateCommands();
protected:
CommandHandlerMap m_CommandHandlers;
HandlerDispatch & m_dispatch;
};
class AuthenticationHandler : public HandlerBase
{
public:
AuthenticationHandler(HandlerDispatch & dispatch) : HandlerBase(dispatch) {}
bool HandleCommand(UTF8String & cmdName, Request & req, Response & res) {
CommandHandlerMap::const_iterator ihandler;
if( (ihandler=m_CommandHandlers.find(req.instruction)) != m_CommandHandler.end() ) {
// call actual handler
return (this->*(ihandler->second))(req,res);
}
// error case:
res.success = false;
res.info = "unknown or invalid instruction";
return true;
}
void PopulateCommands() {
m_CommandHandlers["login"]=Handle_LOGIN;
m_CommandHandlers["logout"]=Handle_LOGOUT;
}
void Handle_LOGIN(Request & req, Response & res) {
Session & session = m_dispatch.GetSessionForRequest(req);
// ...
}
};
class HandlerDispatch
{
public:
HandlerDispatch();
virtual ~HandlerDispatch() {
// delete all handlers
}
void AddHandler(HandlerBase * pHandler);
bool HandleRequest() {
vector<HandlerBase *>::iterator i;
for ( i=m_handlers.begin() ; i < m_handlers.end(); i++ ) {
if ((*i)->CommandSupported(m_CurRequest.instruction)) {
return (*i)->HandleCommand(m_CurRequest.instruction,m_CurRequest,m_CurResponse);
}
}
// error case:
m_CurResponse.success = false;
m_CurResponse.info = "unknown or invalid instruction";
return true;
}
protected:
std::vector<HandlerBase*> m_handlers;
}
Y luego, para pegarlo todo, harías algo como esto:
// Init
m_handlerDispatch.AddHandler(new AuthenticationHandler(m_handlerDispatch));
Si los métodos de protocolo solo pueden agruparse por tipo pero los métodos del mismo grupo no tienen nada en común en su implementación, posiblemente lo único que puede hacer para mejorar la mantenibilidad es distribuir métodos entre diferentes archivos, un archivo para un grupo.
Pero es muy probable que los métodos del mismo grupo tengan algunas de las siguientes características comunes:
- Puede haber algunos campos de datos en la clase
Worker
que son utilizados por un solo grupo de métodos o por varios (pero no todos) grupos. Por ejemplo, sim_AuthHandle
puede ser usado por la administración de usuarios y los métodos de administración de sesiones. - Puede haber algunos grupos de parámetros de entrada, utilizados por cada método de algún grupo.
- Puede haber algunos datos comunes, escritos en la respuesta por cada método de algún grupo.
- Puede haber algunos métodos comunes, llamados por varios métodos de algún grupo.
Si algunos de estos hechos son ciertos, hay una buena razón para agrupar estas características en diferentes clases. No una clase por controlador de comandos, sino una clase por grupo de eventos. O, si hay características, comunes a varios grupos, una jerarquía de clases.
Puede ser conveniente agrupar instancias de todas estas clases de grupo en un solo lugar:
classe UserManagement: public IManagement {...};
classe FileManagement: public IManagement {...};
classe SessionManagement: public IManagement {...};
struct Handlers {
smartptr<IManagement> userManagement;
smartptr<IManagement> fileManagement;
smartptr<IManagement> sessionManagement;
...
Handlers():
userManagement(new UserManagement),
fileManagement(new FileManagement),
sessionManagement(new SessionManagement),
...
{}
};
En lugar de la new SomeClass
, se puede usar alguna plantilla como make_unique . O, si se necesitan "implementaciones de protocolo intercambiables", una de las posibilidades es usar fábricas en lugar de algunos (o todos) los new SomeClass
operadores new SomeClass
.
m_CommandHandlers.find()
debe dividirse en dos búsquedas de mapas: una: para encontrar el controlador adecuado en esta estructura, otra (en la implementación apropiada de IManagement
): para encontrar un indicador de función miembro para el controlador real.
Además de encontrar un puntero a función miembro, el método HandleRequest
de cualquier implementación de IManagement
puede extraer parámetros comunes para su grupo de eventos y pasarlos a los controladores de eventos (uno por uno si solo hay varios de ellos, o agrupados en una estructura si hay muchos ).
IManagement
implementación de IManagement
también puede contener el método WriteCommonResponce
para simplificar la escritura de campos de respuesta, comunes a todos los controladores de eventos.
Ya tienes la mayoría de las ideas correctas, así es como procederé.
Comencemos con su segunda pregunta: protocolos intercambiables. Si tiene objetos de solicitud y respuesta genéricos, puede tener una interfaz que lee solicitudes y escribe respuestas:
class Protocol {
virtual Request *readRequest() = 0;
virtual void writeResponse(Response *response) = 0;
}
y podría tener una implementación llamada HttpProtocol
por ejemplo.
En cuanto a sus controladores de comando, "una clase por controlador de comando" es el enfoque correcto:
class Command {
virtual void execute(Request *request, Response *response, Session *session) = 0;
}
Tenga en cuenta que enrolé todos los manejadores de sesión comunes (DB, Foo, etc.) en un solo objeto en lugar de pasar un montón de parámetros. También hacer estos parámetros de método en lugar de argumentos de constructor significa que solo necesita una instancia de cada comando.
A continuación, tendría un CommandFactory
que contiene el mapa de nombres de comandos para mandar objetos:
class CommandFactory {
std::map<UTF8String, Command *> handlers;
Command *getCommand(const UTF8String &name) {
return handlers[name];
}
}
Si has hecho todo esto, el Worker
vuelve extremadamente delgado y simplemente coordina todo:
class Worker {
Protocol *protocol;
CommandFactory *commandFactory;
Session *session;
void handleRequest() {
Request *request = protocol->readRequest();
Response response;
Command *command = commandFactory->getCommand(request->getCommandName());
command->execute(request, &response, session);
protocol->writeResponse(&response);
}
}