Hola Habr!
A veces, al desarrollar servicios de red e interfaces de usuario, uno tiene que lidiar con escenarios de interacción bastante complicados que contienen ramas y bucles. Tales escenarios no se ajustan a una máquina de estado simple: no es suficiente almacenar todos los datos en el objeto de sesión, también es recomendable rastrear la ruta del sistema para entrar en un estado u otro, y en algunos casos poder retroceder unos pocos pasos, repetir el diálogo en un bucle, etc. .d. Anteriormente, para este propósito, tenía que desarrollar sus propias estructuras de datos que imitaran la máquina de pila, o incluso utilizar lenguajes de script de terceros. Con el advenimiento de las capacidades asincrónicas en casi todos los lenguajes de programación, ha sido posible escribir scripts en el mismo lenguaje en el que se escribe el servicio. El script, con su pila y variables locales, es en realidad una sesión de usuario, es decir, almacena datos y la ruta. Por ejemplo, goroutine con bloqueo de lectura del canal resuelve fácilmente este problema, pero en primer lugar, el hilo verde
no está
libre , y en segundo lugar, escribimos en Rust, donde no hay hilos verdes, pero hay
generadores y
async / wait .
Por ejemplo, escribiremos un simple http-bot que muestre un formulario html en el navegador, haciéndole preguntas al usuario hasta que responda que se siente bien. El programa es el servidor http más simple de un solo subproceso; escribimos el script de bot en forma de un generador de óxido. Permítame recordarle que los
generadores de JavaScript permiten el intercambio de datos bidireccional, es decir, dentro del generador puede pasar la pregunta:
my_generator.next (my_question);y devuelve la respuesta de ella:
yield my_response;En Rust, la transferencia de valores dentro del generador aún no se ha implementado (la función
resume () no tiene parámetros, aunque hay
una discusión para solucionar esto), por lo que organizamos el intercambio de datos a través de una celda compartida, en la que se encuentra la estructura con los datos recibidos y enviados. El script de nuestro bot es creado por la función
create_scenario () , que devuelve una instancia del generador, esencialmente el cierre al que se mueve el parámetro, un puntero a la
celda de datos
udata . Para cada sesión de usuario, almacenamos nuestra propia celda con datos y nuestra propia instancia del generador, con su propio estado de la pila y los valores de las variables locales.
#[derive(Default, Clone)] struct UserData { sid: String, msg_in: String, msg_out: String, script: String, } type UserDataCell = Rc<RefCell<UserData>>; struct UserSession { udata: UserDataCell, scenario: Pin<Box<dyn Generator<Yield = (), Return = ()>>>, } type UserSessions = HashMap<String, UserSession>; fn create_scenario(udata: UserDataCell) -> impl Generator<Yield = (), Return = ()> { move || { let uname; let mut umood; udata.borrow_mut().msg_out = format!("Hi, what is you name ?"); yield (); uname = udata.borrow().msg_in.clone(); udata.borrow_mut().msg_out = format!("{}, how are you feeling ?", uname); yield (); 'not_ok: loop { umood = udata.borrow().msg_in.clone(); if umood.to_lowercase() == "ok" { break 'not_ok; } udata.borrow_mut().msg_out = format!("{}, think carefully, maybe you're ok ?", uname); yield (); umood = udata.borrow().msg_in.clone(); if umood.to_lowercase() == "ok" { break 'not_ok; } udata.borrow_mut().msg_out = format!("{}, millions of people are starving, maybe you're ok ?", uname); yield (); } udata.borrow_mut().msg_out = format!("{}, good bye !", uname); return (); } }
Cada paso de la secuencia de comandos consiste en acciones simples: obtener un enlace al contenido de la celda, guardar la entrada del usuario en variables locales, establecer el texto de respuesta y dar control al exterior, a través del
rendimiento . Como se puede ver en el código, nuestro generador devuelve una tupla vacía (), y todos los datos se transmiten a través de una celda común con un contador de referencia
Ref <Cell <... >> . Dentro del generador, debe asegurarse de que el préstamo de los contenidos de la celda
prestada () no cruce el punto de
rendimiento , de lo contrario será imposible actualizar los datos desde el exterior del generador; por lo tanto, desafortunadamente, no puede escribir una vez al comienzo del algoritmo
let udata_mut = udata.borrow_mut () , y tienes que pedir prestado un valor después de cada rendimiento.
Implementamos nuestro propio bucle de eventos (lectura desde el socket), y para cada solicitud entrante creamos una nueva sesión de usuario o encontramos la existente por sid, actualizando los datos en ella:
let mut udata: UserData = read_udata(&mut stream); let mut sid = udata.sid.clone(); let session; if sid == "" {
A continuación, transferimos el control dentro del generador correspondiente y actualizamos los datos actualizados nuevamente al socket. En el último paso, cuando se completa todo el script, eliminamos la sesión del hashmap y ocultamos el campo de entrada de la página html usando un script js.
udata = match session.scenario.as_mut().resume() { GeneratorState::Yielded(_) => session.udata.borrow().clone(), GeneratorState::Complete(_) => { let mut ud = sessions.remove(&sid).unwrap().udata.borrow().clone(); ud.script = format!("document.getElementById('form').style.display = 'none'"); ud } }; write_udata(&udata, &mut stream);
Código de trabajo completo aquí:
github.com/epishman/habr_samples/blob/master/chatbot/main.rsPido disculpas por el análisis http de la "granja colectiva", que ni siquiera admite la entrada cirílica, pero todo se hace utilizando herramientas de lenguaje estándar, sin marcos, bibliotecas y sms. Realmente no me gusta la clonación de cadenas, y el script en sí no parece bastante compacto debido al uso intensivo de
loan_mut () y
clone () . Probablemente, los rastamans experimentados podrán simplificar esto (por ejemplo, usando macros). Lo principal es que el problema se resuelve por medios mínimos, y espero que pronto recibamos un conjunto completo de herramientas asincrónicas en una versión estable.
PS
Para compilar, necesita una compilación nocturna:
rustup default nightly rustup update
Los camaradas del inglés Stack Overflow me ayudaron a lidiar con los generadores:
stackoverflow.com/questions/56460206/how-can-i-transfer-some-values-into-a-rust-generator-at-each-step