Serveur
Dans cette partie nous verrons a construction du serveur, ce dernier est répartit en plusieurs modules le premier étant celui des ressources Input/Output c'est à dire concernant les abstractions liés aux notifications système. Un deuxième module connexe au premier fait la liaison entre les entrée de notification systeme et les connections TCP permettant à ces dernières d'être entièrement asynchrones. Un dernier est lié au marché avec la liste des titres et les modules d'éxécution d'ordre.
Structure des éléments de l'application (dossiers et fichiers non pertinents omis):
.
├── build.rs -> script de compilation, génère le code protobuf
├── Cargo.toml -> fichier de spécification du projet
├── src
│ ├── lib.rs
│ ├── ...
│ ├── net
│ │ ├── mod.rs
│ │ ├── codec.rs
│ │ └── tcp.rs
│ ├── market
│ │ ├── securities.rs
│ │ ├── core.rs
│ │ ├── mod.rs
│ │ ├── pool.rs
│ │ └── router.rs
│ ├── io
│ │ ├── mod.rs
│ │ ├── registration.rs
│ │ └── driver
│ │ ├── context.rs
│ │ └── mod.rs
│ ├── server.rs
│ └── bin -> binaires éxécutables
├── proto
│ └── aqmm.proto -> protocol en protobuf
└── ...
Exécuteur et I/O
La structure du serveur (dans le fichier src/server.rs
) est composé d'un "driver" pour les
ressources I/O liés aux connections arrivant. Le but du serveur est d'écouter les connections
arrivantes et pour chaque connection d'éxécuter un client qui prend la forme d'un future. A chaque
client est associé une connection ainsi qu'une session. Afin de determiner quels clients ont une
action de prête le serveur utilise la librairie mio
qui permet d'associer
des ressources I/O et d'emettre des notifications sur les connections ayant du travail. Les
évènements sont ensuite transferé à des entrées contenant les std::task::Waker
des streams de
message des clients ces messages sont ensuite décodés et une réponse est envoyé suviant le protocole
décrit dans la partie précédente.
Réseau
Le module src/net
contient tous les éléments nécéssaire au maintient et à l'utilisation des
connections.
Stream Tcp Asynchrone
Le driver de ressources I/O permet de savoir quand une connection est en mesure d'être écrite/lu. De
cette manière le driver peut reveiller les Waker
associés. Il manque donc deux trait comme
std::future::Future
mais pour les actions de lecture et d'ecriture d'un flux de donnée. Le
language expose les traits synchrone std::io::Read
et std::io::Write
. La libraire futures
définit de la même facon futures::io::AsyncRead
et futures::io::AsyncWrite
. Ainsi avec le driver
I/O nous pouvons construire une abstraction par dessus un TcpStream synchrone qui implémente
AsyncRead + AsyncWrite
Codec
Les codecs sont des structures construites sur les traits tokio_util::codec::Encoder
et
tokio_util::codec::Decoder
trouvé dans la librairie tokio_util
.
Il permettent de lier l'utilisation de zone mémoire tampon avec l'encodage et le décodage de
messages. Le module expose un codec client, qui encode des requêtes et décode des réponses ainsi
qu'un codec serveur qui encode les réponses et décode les requètes.
L'interet d'exposer ces codec est de pouvoir utiliser la structure tokio_util::codec::Framed
exposé par la même libraire. La structure permet de créer un stream et un "sink" depuis un codec
(Encoder + Decoder
) et une structure pouvant être lu/écrite de manière asynchrone
(AsyncRead + AsyncWrite
). Le stream renvois ainsi les objets décodés et le "sink" permet d'envoyer
des objets qui seront encodés puis envoyés sur la connection.
Marché
Le marché est composé de plusieurs éléments. La structure principale qui sert de pointeur vers un état partagé qui comprend une liste des coeurs du marché et un accès au routeur d'ordres. Tous les coeurs ont également un pointeur vers un une structure partagé contenant le compte de chaque titres ainsi qu'un accès au multicast.
Le but de cette structure est de servire d'interface partagé entre tous les clients pour envoyer des ordres en contrepartie d'un future sur le résultat de cet ordre.
Routeur et coeurs du marché
Le router d'ordre est un structure faisant part du marché son but est d'envoyer les ordres sur les
différents coeurs. Les stratégies de routage des ordres peuvent être plus ou moins complexe. En
effet, le but des coeurs est d'éxécuter des ordres cependant on pourrait se retrouver dans une
situation ou un coeur recoit tous les ordres tandis que les autres ne font rien gachant ainsi du
temps de processeur. Par exemple, on pourrait imaginer une stratégie ou les ordres sont triés et en
fonction des titres visés sont envoyé sur un coeurs différent. Dans le cadre de ce projet le router
est plutot basique et agit comme une roue distribuant tour à tour à chaque coeur. Les points de
sortis des router implémente égalment le trait futures::stream::Stream
de manière à ce que les
coeurs ne soient actifs que lorsque des ordres arrivent (et ainsi les notifiants).
Le coeur du marché est lancé sur un processus différent, il s'agit d'une structure asynchrone qui attend les ordres du router. Une fois l'ordre reçu il tente attend de pouvoir avoir un accès exclusifs (via un Mutex) aux quantités actuelles de l'animateur de marché. Une fois les quantités acquises il calcule le score pour les anciennes et le score pour les nouvelles après avoir éxécuté tous les ordres determinant le prix de la transaction. Une fois le verrous des quantités levé il tente de partager les nouvelles quantités, cette action échouera si les quantités ont été partagé trop récemment.
Titres
Le fichier src/market/securities.rs
permet d'éditer les titres avant la compilation et contient
également des fonctions y facilitant l'accès. Le fichier définit également un autre type
Securities
dont la description est du texte statique car il est, pour le moment, impossible de
créer statiquement (comprendre lors de la compilation) un pointeur de texte String
requis par
prost pour les entrées type string en protobuf.
const SECURITIES: &'static [Security] = &[
Security {
security_id: 0,
description: "first security",
},
Security {
security_id: 1,
description: "second security",
},
Security {
security_id: 2,
description: "third security",
},
];