├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── README.adoc ├── rust-toolchain └── src └── bin ├── client.rs └── server.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - staging 4 | - master 5 | - trying 6 | 7 | language: rust 8 | rust: nightly 9 | 10 | script: 11 | - cargo check 12 | 13 | script: 14 | - gem install asciidoctor 15 | - asciidoctor README.adoc -o _html/index.html 16 | 17 | deploy: 18 | provider: pages 19 | skip-cleanup: true 20 | github-token: $DOCS_TOKEN 21 | keep-history: true 22 | local-dir: _html 23 | on: 24 | branch: master 25 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "a-chat" 5 | version = "0.1.0" 6 | dependencies = [ 7 | "async-std 0.99.4 (registry+https://github.com/rust-lang/crates.io-index)", 8 | "futures-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 9 | ] 10 | 11 | [[package]] 12 | name = "async-std" 13 | version = "0.99.4" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | dependencies = [ 16 | "async-task 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 17 | "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 18 | "crossbeam-channel 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", 19 | "futures-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 20 | "futures-timer 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 21 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 22 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 23 | "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 24 | "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", 25 | "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", 26 | "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", 27 | "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", 28 | "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 29 | ] 30 | 31 | [[package]] 32 | name = "async-task" 33 | version = "1.0.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | dependencies = [ 36 | "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", 37 | ] 38 | 39 | [[package]] 40 | name = "bitflags" 41 | version = "1.1.0" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | 44 | [[package]] 45 | name = "c2-chacha" 46 | version = "0.2.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | dependencies = [ 49 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 50 | "ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", 51 | ] 52 | 53 | [[package]] 54 | name = "cfg-if" 55 | version = "0.1.9" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | 58 | [[package]] 59 | name = "crossbeam-channel" 60 | version = "0.3.9" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | dependencies = [ 63 | "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", 64 | ] 65 | 66 | [[package]] 67 | name = "crossbeam-utils" 68 | version = "0.6.6" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | dependencies = [ 71 | "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 72 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 73 | ] 74 | 75 | [[package]] 76 | name = "fuchsia-zircon" 77 | version = "0.3.3" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | dependencies = [ 80 | "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 81 | "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 82 | ] 83 | 84 | [[package]] 85 | name = "fuchsia-zircon-sys" 86 | version = "0.3.3" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | 89 | [[package]] 90 | name = "futures-channel-preview" 91 | version = "0.3.0-alpha.18" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | dependencies = [ 94 | "futures-core-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 95 | "futures-sink-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 96 | ] 97 | 98 | [[package]] 99 | name = "futures-core-preview" 100 | version = "0.3.0-alpha.18" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | 103 | [[package]] 104 | name = "futures-executor-preview" 105 | version = "0.3.0-alpha.18" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | dependencies = [ 108 | "futures-core-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 109 | "futures-util-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 110 | "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", 111 | ] 112 | 113 | [[package]] 114 | name = "futures-io-preview" 115 | version = "0.3.0-alpha.18" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | 118 | [[package]] 119 | name = "futures-join-macro-preview" 120 | version = "0.3.0-alpha.18" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | dependencies = [ 123 | "proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", 124 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 125 | "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", 126 | "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", 127 | ] 128 | 129 | [[package]] 130 | name = "futures-preview" 131 | version = "0.3.0-alpha.18" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | dependencies = [ 134 | "futures-channel-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 135 | "futures-core-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 136 | "futures-executor-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 137 | "futures-io-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 138 | "futures-sink-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 139 | "futures-util-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 140 | ] 141 | 142 | [[package]] 143 | name = "futures-select-macro-preview" 144 | version = "0.3.0-alpha.18" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | dependencies = [ 147 | "proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", 148 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 149 | "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", 150 | "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", 151 | ] 152 | 153 | [[package]] 154 | name = "futures-sink-preview" 155 | version = "0.3.0-alpha.18" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | dependencies = [ 158 | "futures-core-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 159 | ] 160 | 161 | [[package]] 162 | name = "futures-timer" 163 | version = "0.3.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | dependencies = [ 166 | "futures-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 167 | "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", 168 | ] 169 | 170 | [[package]] 171 | name = "futures-util-preview" 172 | version = "0.3.0-alpha.18" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | dependencies = [ 175 | "futures-channel-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 176 | "futures-core-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 177 | "futures-io-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 178 | "futures-join-macro-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 179 | "futures-select-macro-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 180 | "futures-sink-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)", 181 | "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 182 | "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", 183 | "proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", 184 | "proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 185 | "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 186 | "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 187 | ] 188 | 189 | [[package]] 190 | name = "getrandom" 191 | version = "0.1.10" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | dependencies = [ 194 | "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 195 | "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", 196 | "wasi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 197 | ] 198 | 199 | [[package]] 200 | name = "iovec" 201 | version = "0.1.2" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | dependencies = [ 204 | "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", 205 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 206 | ] 207 | 208 | [[package]] 209 | name = "kernel32-sys" 210 | version = "0.2.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | dependencies = [ 213 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 214 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 215 | ] 216 | 217 | [[package]] 218 | name = "lazy_static" 219 | version = "1.3.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | 222 | [[package]] 223 | name = "libc" 224 | version = "0.2.62" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | 227 | [[package]] 228 | name = "log" 229 | version = "0.4.8" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | dependencies = [ 232 | "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 233 | ] 234 | 235 | [[package]] 236 | name = "memchr" 237 | version = "2.2.1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | 240 | [[package]] 241 | name = "mio" 242 | version = "0.6.19" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | dependencies = [ 245 | "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 246 | "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 247 | "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 248 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 249 | "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", 250 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 251 | "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 252 | "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 253 | "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 254 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 255 | ] 256 | 257 | [[package]] 258 | name = "mio-uds" 259 | version = "0.6.7" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | dependencies = [ 262 | "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 263 | "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", 264 | "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", 265 | ] 266 | 267 | [[package]] 268 | name = "miow" 269 | version = "0.2.1" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | dependencies = [ 272 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 273 | "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 274 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 275 | "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 276 | ] 277 | 278 | [[package]] 279 | name = "net2" 280 | version = "0.2.33" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | dependencies = [ 283 | "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 284 | "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", 285 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 286 | ] 287 | 288 | [[package]] 289 | name = "num_cpus" 290 | version = "1.10.1" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | dependencies = [ 293 | "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", 294 | ] 295 | 296 | [[package]] 297 | name = "pin-utils" 298 | version = "0.1.0-alpha.4" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | 301 | [[package]] 302 | name = "ppv-lite86" 303 | version = "0.2.5" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | 306 | [[package]] 307 | name = "proc-macro-hack" 308 | version = "0.5.9" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | dependencies = [ 311 | "proc-macro2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 312 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 313 | "syn 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 314 | ] 315 | 316 | [[package]] 317 | name = "proc-macro-nested" 318 | version = "0.1.3" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | 321 | [[package]] 322 | name = "proc-macro2" 323 | version = "0.4.30" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | dependencies = [ 326 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 327 | ] 328 | 329 | [[package]] 330 | name = "proc-macro2" 331 | version = "1.0.1" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | dependencies = [ 334 | "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 335 | ] 336 | 337 | [[package]] 338 | name = "quote" 339 | version = "0.6.13" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | dependencies = [ 342 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 343 | ] 344 | 345 | [[package]] 346 | name = "quote" 347 | version = "1.0.2" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | dependencies = [ 350 | "proc-macro2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 351 | ] 352 | 353 | [[package]] 354 | name = "rand" 355 | version = "0.7.0" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | dependencies = [ 358 | "getrandom 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 359 | "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", 360 | "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 361 | "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 362 | "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 363 | ] 364 | 365 | [[package]] 366 | name = "rand_chacha" 367 | version = "0.2.1" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | dependencies = [ 370 | "c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 371 | "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 372 | ] 373 | 374 | [[package]] 375 | name = "rand_core" 376 | version = "0.5.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | dependencies = [ 379 | "getrandom 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 380 | ] 381 | 382 | [[package]] 383 | name = "rand_hc" 384 | version = "0.2.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | dependencies = [ 387 | "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 388 | ] 389 | 390 | [[package]] 391 | name = "slab" 392 | version = "0.4.2" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | 395 | [[package]] 396 | name = "syn" 397 | version = "0.15.44" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | dependencies = [ 400 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 401 | "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", 402 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 403 | ] 404 | 405 | [[package]] 406 | name = "syn" 407 | version = "1.0.3" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | dependencies = [ 410 | "proc-macro2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 411 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 412 | "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 413 | ] 414 | 415 | [[package]] 416 | name = "unicode-xid" 417 | version = "0.1.0" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | 420 | [[package]] 421 | name = "unicode-xid" 422 | version = "0.2.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | 425 | [[package]] 426 | name = "wasi" 427 | version = "0.5.0" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | 430 | [[package]] 431 | name = "winapi" 432 | version = "0.2.8" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | 435 | [[package]] 436 | name = "winapi" 437 | version = "0.3.7" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | dependencies = [ 440 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 441 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 442 | ] 443 | 444 | [[package]] 445 | name = "winapi-build" 446 | version = "0.1.1" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | 449 | [[package]] 450 | name = "winapi-i686-pc-windows-gnu" 451 | version = "0.4.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | 454 | [[package]] 455 | name = "winapi-x86_64-pc-windows-gnu" 456 | version = "0.4.0" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | 459 | [[package]] 460 | name = "ws2_32-sys" 461 | version = "0.2.1" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | dependencies = [ 464 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 465 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 466 | ] 467 | 468 | [metadata] 469 | "checksum async-std 0.99.4 (registry+https://github.com/rust-lang/crates.io-index)" = "95dbe66a9f8c59a70277214f98d39f25fe1f36f20f6e8412a8b33af0272a2c79" 470 | "checksum async-task 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de6bd58f7b9cc49032559422595c81cbfcf04db2f2133592f70af19e258a1ced" 471 | "checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" 472 | "checksum c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101" 473 | "checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" 474 | "checksum crossbeam-channel 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c8ec7fcd21571dc78f96cc96243cab8d8f035247c3efd16c687be154c3fa9efa" 475 | "checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" 476 | "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 477 | "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 478 | "checksum futures-channel-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)" = "f477fd0292c4a4ae77044454e7f2b413207942ad405f759bb0b4698b7ace5b12" 479 | "checksum futures-core-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)" = "4a2f26f774b81b3847dcda0c81bd4b6313acfb4f69e5a0390c7cb12c058953e9" 480 | "checksum futures-executor-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)" = "80705612926df8a1bc05f0057e77460e29318801f988bf7d803a734cf54e7528" 481 | "checksum futures-io-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)" = "ee7de0c1c9ed23f9457b0437fec7663ce64d9cc3c906597e714e529377b5ddd1" 482 | "checksum futures-join-macro-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)" = "1b151e04c412159cfe4ac5cd0d0bc037addda57f48c4d46d00152cfdae7e52d9" 483 | "checksum futures-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)" = "efa8f90c4fb2328e381f8adfd4255b4a2b696f77d1c63a3dee6700b564c4e4b5" 484 | "checksum futures-select-macro-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)" = "767dbbb9accba815dc1f327b20cbe932e42ef11668fe35764ed52f74c66a54c3" 485 | "checksum futures-sink-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)" = "e9b65a2481863d1b78e094a07e9c0eed458cc7dc6e72b22b7138b8a67d924859" 486 | "checksum futures-timer 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8f9eb554aa23143abc64ec4d0016f038caf53bb7cbc3d91490835c54edc96550" 487 | "checksum futures-util-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)" = "7df53daff1e98cc024bf2720f3ceb0414d96fbb0a94f3cad3a5c3bf3be1d261c" 488 | "checksum getrandom 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "6171a6cc63fbabbe27c2b5ee268e8b7fe5dc1eb0dd2dfad537c1dfed6f69117e" 489 | "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" 490 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 491 | "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" 492 | "checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba" 493 | "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 494 | "checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" 495 | "checksum mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)" = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23" 496 | "checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" 497 | "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" 498 | "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" 499 | "checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273" 500 | "checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" 501 | "checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b" 502 | "checksum proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e688f31d92ffd7c1ddc57a1b4e6d773c0f2a14ee437a4b0a4f5a69c80eb221c8" 503 | "checksum proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e" 504 | "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" 505 | "checksum proc-macro2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4c5c2380ae88876faae57698be9e9775e3544decad214599c3a6266cca6ac802" 506 | "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" 507 | "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" 508 | "checksum rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d47eab0e83d9693d40f825f86948aa16eff6750ead4bdffc4ab95b8b3a7f052c" 509 | "checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" 510 | "checksum rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "615e683324e75af5d43d8f7a39ffe3ee4a9dc42c5c701167a71dc59c3a493aca" 511 | "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 512 | "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 513 | "checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" 514 | "checksum syn 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "158521e6f544e7e3dcfc370ac180794aa38cb34a1b1e07609376d4adcf429b93" 515 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 516 | "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 517 | "checksum wasi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fd5442abcac6525a045cc8c795aedb60da7a2e5e89c7bf18a0d5357849bb23c7" 518 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 519 | "checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" 520 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 521 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 522 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 523 | "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" 524 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "a-chat" 3 | version = "0.1.0" 4 | authors = ["Aleksey Kladov "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | futures-preview = { version = "0.3.0-alpha.17", features = [ "async-await", "nightly" ] } 11 | async-std = "0.99" 12 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = a-chat tutorial 2 | :icons: font 3 | 4 | :source-language: rust 5 | 6 | CAUTION: the tutorial is moved to https://github.com/async-rs/async-std/blob/master/examples/a-chat. 7 | 8 | In this tutorial, we will implement an asynchronous chat on top of async-std. 9 | 10 | https://htmlpreview.github.io/?https://raw.githubusercontent.com/async-rs/a-chat/gh-pages/index.html[HTML version] 11 | 12 | == Specification 13 | 14 | The chat uses a simple text protocol over TCP. 15 | The protocol consists of utf-8 messages, separated by `\n`. 16 | 17 | The client connects to the server and sends login as a first line. 18 | After that, the client can send messages to other clients using the following syntax: 19 | 20 | [source] 21 | ---- 22 | login1, login2, ... login2: message 23 | ---- 24 | 25 | Each of the specified clients than receives a `from login: message` message. 26 | 27 | A possible session might look like this 28 | 29 | [cols="2",frame=none,grid=none] 30 | |=== 31 | a| 32 | .alice 33 | ---- 34 | > alice 35 | > bob: hello 36 | 37 | 38 | < from bob: hi! 39 | ---- 40 | 41 | a| 42 | .bob 43 | ---- 44 | > bob 45 | 46 | < from alice: hello 47 | > alice, bob: hi! 48 | < from bob: hi! 49 | ---- 50 | 51 | |=== 52 | 53 | The main challenge for the chat server is keeping track of many concurrent connections. 54 | The main challenge for the chat client is managing concurrent outgoing messages, incoming messages and user's typing. 55 | 56 | == Getting Started 57 | 58 | Let's create a new Cargo project: 59 | 60 | [source] 61 | ---- 62 | $ cargo new a-chat 63 | $ cd a-chat 64 | ---- 65 | 66 | Then add the following to your `Cargo.toml`: 67 | 68 | [source] 69 | ---- 70 | futures-preview = { version = "0.3.0-alpha.18", features = [ "async-await", "nightly" ] } 71 | async-std = "0.99" 72 | ---- 73 | 74 | At the moment `async-std` requires nightly, so let's add a rustup override for convenience: 75 | 76 | [source] 77 | ---- 78 | $ rustup override add nightly 79 | $ rustc --version 80 | rustc 1.38.0-nightly (c4715198b 2019-08-05) 81 | ---- 82 | 83 | == Accept Loop 84 | 85 | Let's implement the scaffold of the server: a loop that binds a TCP socket to an address and starts accepting connections. 86 | 87 | 88 | First of all, let's add required import boilerplate: 89 | 90 | [source,rust] 91 | ---- 92 | use std::net::ToSocketAddrs; <1> 93 | 94 | use async_std::{ 95 | prelude::*, <2> 96 | task, <3> 97 | net::TcpListener, <4> 98 | }; 99 | 100 | type Result = std::result::Result>; <5> 101 | ---- 102 | 103 | <1> `async_std` uses `std` types where appropriate. 104 | We'll need `ToSocketAddrs` to specify address to listen on. 105 | <2> `prelude` re-exports some traits required to work with futures and streams 106 | <3> The `task` module roughtly corresponds to `std::thread` module, but tasks are much lighter weight. 107 | A single thread can run many tasks. 108 | <4> For the socket type, we use `TcpListener` from `async_std`, which is just like `std::net::TcpListener`, but is non-blocking and uses `async` API. 109 | <5> We will skip implementing comprehensive error handling in this example. 110 | To propagate the errors, we will use a boxed error trait object. 111 | + 112 | NOTE: Do you know that there's `From<&'_ str> for Box` implementation in 113 | stdlib, which allows you to use strings with `?` operator? 114 | 115 | 116 | Now we can write the server's accept loop: 117 | 118 | [source,rust] 119 | ---- 120 | async fn server(addr: impl ToSocketAddrs) -> Result<()> { <1> 121 | let listener = TcpListener::bind(addr).await?; <2> 122 | let mut incoming = listener.incoming(); 123 | while let Some(stream) = incoming.next().await { <3> 124 | // TODO 125 | } 126 | Ok(()) 127 | } 128 | ---- 129 | 130 | <1> We mark `server` function as `async`, which allows us to use `.await` syntax inside. 131 | <2> `TcpListener::bind` call returns a future, which we `.await` to extract the `Result`, and then `?` to get a `TcpListener`. 132 | Note how `.await` and `?` work nicely together. 133 | This is exactly how `std::net::TcpListener` works, but with `.await` added. 134 | Mirroring API of `std` is an explicit design goal of `async_std`. 135 | <3> Here, we would like to iterate incoming sockets, just how one would do in `std`: 136 | + 137 | [source,rust] 138 | ---- 139 | let listener: std::net::TcpListener = unimplemented!(); 140 | for stream in listener.incoming() { 141 | 142 | } 143 | ---- 144 | + 145 | Unfortunately this doesn't quite work with `async` yet, because there's no support for `async` for-loops in the language yet. 146 | For this reason we have to implement the loop manually, by using `while let Some(item) = iter.next().await` pattern. 147 | 148 | Finally, let's add main: 149 | 150 | [source,rust] 151 | ---- 152 | fn main() -> Result<()> { 153 | let fut = server("127.0.0.1:8080"); 154 | task::block_on(fut) 155 | } 156 | ---- 157 | 158 | The crucial thing to realise that is in Rust, unlike other languages, calling an async function does **not** run any code. 159 | Async functions only construct futures, which are inert state machines. 160 | To start stepping through the future state-machine in an async function, you should use `.await`. 161 | In a non-async function, a way to execute a future is to handle it to the executor. 162 | In this case, we use `task::block_on` to execute future on the current thread and block until it's done. 163 | 164 | == Receiving messages 165 | 166 | Let's implement the receiving part of the protocol. 167 | We need to: 168 | 169 | . split incoming `TcpStream` on `\n` and decode bytes as utf-8 170 | . interpret the first line as a login 171 | . parse the rest of the lines as a `login: message` 172 | 173 | 174 | [source] 175 | ---- 176 | use async_std::net::TcpStream; 177 | 178 | async fn server(addr: impl ToSocketAddrs) -> Result<()> { 179 | let listener = TcpListener::bind(addr).await?; 180 | let mut incoming = listener.incoming(); 181 | while let Some(stream) = incoming.next().await { 182 | let stream = stream?; 183 | println!("Accepting from: {}", stream.peer_addr()?); 184 | let _handle = task::spawn(client(stream)); <1> 185 | } 186 | Ok(()) 187 | } 188 | 189 | async fn client(stream: TcpStream) -> Result<()> { 190 | let reader = BufReader::new(&stream); <2> 191 | let mut lines = reader.lines(); 192 | 193 | let name = match lines.next().await { <3> 194 | None => Err("peer disconnected immediately")?, 195 | Some(line) => line?, 196 | }; 197 | println!("name = {}", name); 198 | 199 | while let Some(line) = lines.next().await { <4> 200 | let line = line?; 201 | let (dest, msg) = match line.find(':') { <5> 202 | None => continue, 203 | Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), 204 | }; 205 | let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); 206 | let msg: String = msg.trim().to_string(); 207 | } 208 | Ok(()) 209 | } 210 | ---- 211 | 212 | <1> We use `task::spawn` function to spawn an independent task for working with each client. 213 | That is, after accepting the client the `server` loop immediately starts waiting for the next one. 214 | This is the core benefit of event-driven architecture: we serve many number of clients concurrently, without spending many hardware threads. 215 | 216 | <2> Luckily, the "split byte stream into lines" functionality is already implemented. 217 | `.lines()` call returns a stream of ``String``'s. 218 | TODO: show how one would implement `lines` by hand? 219 | 220 | <3> We get the first line -- login 221 | 222 | <4> And, once again, we implement a manual async for loop. 223 | 224 | <5> Finally, we parse each line into a list of destination logins and the message itself. 225 | 226 | == Managing Errors 227 | 228 | One serious problem in the above solution is that, while we correctly propagate errors in the `client`, we just drop the error on the floor afterwards! 229 | That is, `task::spawn` does not return error immediately (it can't, it needs to run the future to completion first), only after it is joined. 230 | We can "fix" it by waiting for the task to be joined, like this: 231 | 232 | [source,rust] 233 | ---- 234 | let handle = task::spawn(client(stream)); <1> 235 | handle.await? 236 | ---- 237 | 238 | The `.await` waits until the client finishes, and `?` propagates the result. 239 | 240 | There are two problems with this solution however! 241 | _First_, because we immediately await the client, we can only handle one client at time, and that completely defeats the purpose of async! 242 | _Second_, if a client encounters an IO error, the whole server immediately exits. 243 | That is, a flaky internet connection of one peer brings down the whole chat room! 244 | 245 | A correct way to handle client errors in this case is log them, and continue serving other clients. 246 | So let's use a helper function for this: 247 | 248 | [source,rust] 249 | ---- 250 | fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> 251 | where 252 | F: Future> + Send + 'static, 253 | { 254 | task::spawn(async move { 255 | if let Err(e) = fut.await { 256 | eprintln!("{}", e) 257 | } 258 | }) 259 | } 260 | ---- 261 | 262 | == Sending Messages 263 | 264 | Now it's time to implement the other half -- sending messages. 265 | A most obvious way to implement sending is to give each `client` access to the write half of `TcpStream` of each other clients. 266 | That way, a client can directly `.write_all` a message to recipients. 267 | However, this would be wrong: if Alice sends `bob: foo`, and Charley sends `bob: bar`, Bob might actually receive `fobaor`. 268 | Sending a message over a socket might require several syscalls, so two concurrent ``.write_all``'s might interfere with each other! 269 | 270 | As a rule of thumb, only a single task should write to each `TcpStream`. 271 | So let's create a `client_writer` task which receives messages over a channel and writes them to the socket. 272 | This task would be the point of serialization of messages. 273 | if Alice and Charley send two messages to Bob at the same time, Bob will see the messages in the same order as they arrive in the channel. 274 | 275 | [source,rust] 276 | ---- 277 | use futures::channel::mpsc; <1> 278 | use futures::SinkExt; 279 | 280 | type Sender = mpsc::UnboundedSender; <2> 281 | type Receiver = mpsc::UnboundedReceiver; 282 | 283 | async fn client_writer( 284 | mut messages: Receiver, 285 | stream: Arc, <3> 286 | ) -> Result<()> { 287 | let mut stream = &*stream; 288 | while let Some(msg) = messages.next().await { 289 | stream.write_all(msg.as_bytes()).await?; 290 | } 291 | Ok(()) 292 | } 293 | ---- 294 | 295 | <1> We will use channels from the `futures` crate. 296 | <2> For simplicity, we will use `unbounded` channels, and won't be discussing backpressure in this tutorial. 297 | <3> As `client` and `client_writer` share the same `TcpStream`, we need to put it into an `Arc`. 298 | Note that because `client` only reads from and `client_writer` only writes to the stream, so we don't get a race here. 299 | 300 | 301 | == Connecting Readers and Writers 302 | 303 | So how we make sure that messages read in `client` flow into the relevant `client_writer`? 304 | We should somehow maintain an `peers: HashMap>` map which allows a client to find destination channels. 305 | However, this map would be a bit of shared mutable state, so we'll have to wrap an `RwLock` over it and answer tough questions of what should happen if the client joins at the same moment as it receives a message. 306 | 307 | One trick to make reasoning about state simpler comes from the actor model. 308 | We can create a dedicated broker tasks which owns the `peers` map and communicates with other tasks by channels. 309 | By hiding `peers` inside such "actor" task, we remove the need for mutxes and also make serialization point explicit. 310 | The order of events "Bob sends message to Alice" and "Alice joins" is determined by the order of the corresponding events in the broker's event queue. 311 | 312 | [source,rust] 313 | ---- 314 | #[derive(Debug)] 315 | enum Event { <1> 316 | NewPeer { 317 | name: String, 318 | stream: Arc, 319 | }, 320 | Message { 321 | from: String, 322 | to: Vec, 323 | msg: String, 324 | }, 325 | } 326 | 327 | async fn broker(mut events: Receiver) -> Result<()> { 328 | let mut peers: HashMap> = HashMap::new(); <2> 329 | 330 | while let Some(event) = events.next().await { 331 | match event { 332 | Event::Message { from, to, msg } => { <3> 333 | for addr in to { 334 | if let Some(peer) = peers.get_mut(&addr) { 335 | peer.send(format!("from {}: {}\n", from, msg)).await? 336 | } 337 | } 338 | } 339 | Event::NewPeer { name, stream } => { 340 | match peers.entry(name) { 341 | Entry::Occupied(..) => (), 342 | Entry::Vacant(entry) => { 343 | let (client_sender, client_receiver) = mpsc::unbounded(); 344 | entry.insert(client_sender); <4> 345 | spawn_and_log_error(client_writer(client_receiver, stream)); <5> 346 | } 347 | } 348 | } 349 | } 350 | } 351 | Ok(()) 352 | } 353 | ---- 354 | 355 | <1> Broker should handle two types of events: a message or an arrival of a new peer. 356 | <2> Internal state of the broker is a `HashMap`. 357 | Note how we don't need a `Mutex` here and can confidently say, at each iteration of the broker's loop, what is the current set of peers 358 | <3> To handle a message we send it over a channel to each destination 359 | <4> To handle new peer, we first register it in the peer's map ... 360 | <5> ... and then spawn a dedicated task to actually write the messages to the socket. 361 | 362 | == All Together 363 | 364 | At this point, we only need to start broker to get a fully-functioning (in the happy case!) chat: 365 | 366 | [source,rust] 367 | ---- 368 | use std::{ 369 | net::ToSocketAddrs, 370 | sync::Arc, 371 | collections::hash_map::{HashMap, Entry}, 372 | }; 373 | 374 | use futures::{ 375 | channel::mpsc, 376 | SinkExt, 377 | }; 378 | 379 | use async_std::{ 380 | io::BufReader, 381 | prelude::*, 382 | task, 383 | net::{TcpListener, TcpStream}, 384 | }; 385 | 386 | type Result = std::result::Result>; 387 | type Sender = mpsc::UnboundedSender; 388 | type Receiver = mpsc::UnboundedReceiver; 389 | 390 | 391 | fn main() -> Result<()> { 392 | task::block_on(server("127.0.0.1:8080")) 393 | } 394 | 395 | async fn server(addr: impl ToSocketAddrs) -> Result<()> { 396 | let listener = TcpListener::bind(addr).await?; 397 | 398 | let (broker_sender, broker_receiver) = mpsc::unbounded(); <1> 399 | let _broker_handle = task::spawn(broker(broker_receiver)); 400 | let mut incoming = listener.incoming(); 401 | while let Some(stream) = incoming.next().await { 402 | let stream = stream?; 403 | println!("Accepting from: {}", stream.peer_addr()?); 404 | spawn_and_log_error(client(broker_sender.clone(), stream)); 405 | } 406 | Ok(()) 407 | } 408 | 409 | async fn client(mut broker: Sender, stream: TcpStream) -> Result<()> { 410 | let stream = Arc::new(stream); <2> 411 | let reader = BufReader::new(&*stream); 412 | let mut lines = reader.lines(); 413 | 414 | let name = match lines.next().await { 415 | None => Err("peer disconnected immediately")?, 416 | Some(line) => line?, 417 | }; 418 | broker.send(Event::NewPeer { name: name.clone(), stream: Arc::clone(&stream) }).await <3> 419 | .unwrap(); 420 | 421 | while let Some(line) = lines.next().await { 422 | let line = line?; 423 | let (dest, msg) = match line.find(':') { 424 | None => continue, 425 | Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), 426 | }; 427 | let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); 428 | let msg: String = msg.trim().to_string(); 429 | 430 | broker.send(Event::Message { <4> 431 | from: name.clone(), 432 | to: dest, 433 | msg, 434 | }).await.unwrap(); 435 | } 436 | Ok(()) 437 | } 438 | 439 | async fn client_writer( 440 | mut messages: Receiver, 441 | stream: Arc, 442 | ) -> Result<()> { 443 | let mut stream = &*stream; 444 | while let Some(msg) = messages.next().await { 445 | stream.write_all(msg.as_bytes()).await?; 446 | } 447 | Ok(()) 448 | } 449 | 450 | #[derive(Debug)] 451 | enum Event { 452 | NewPeer { 453 | name: String, 454 | stream: Arc, 455 | }, 456 | Message { 457 | from: String, 458 | to: Vec, 459 | msg: String, 460 | }, 461 | } 462 | 463 | async fn broker(mut events: Receiver) -> Result<()> { 464 | let mut peers: HashMap> = HashMap::new(); 465 | 466 | while let Some(event) = events.next().await { 467 | match event { 468 | Event::Message { from, to, msg } => { 469 | for addr in to { 470 | if let Some(peer) = peers.get_mut(&addr) { 471 | peer.send(format!("from {}: {}\n", from, msg)).await? 472 | } 473 | } 474 | } 475 | Event::NewPeer { name, stream} => { 476 | match peers.entry(name) { 477 | Entry::Occupied(..) => (), 478 | Entry::Vacant(entry) => { 479 | let (client_sender, client_receiver) = mpsc::unbounded(); 480 | entry.insert(client_sender); <4> 481 | spawn_and_log_error(client_writer(client_receiver, stream)); <5> 482 | } 483 | } 484 | } 485 | } 486 | } 487 | Ok(()) 488 | } 489 | ---- 490 | 491 | <1> Inside the `server`, we create broker's channel and `task`. 492 | <2> Inside `client`, we need to wrap `TcpStream` into an `Arc`, to be able to share it with the `client_writer`. 493 | <3> On login, we notify the broker. 494 | Note that we `.unwrap` on send: broker should outlive all the clients and if that's not the case the broker probably panicked, so we can escalate the panic as well. 495 | <4> Similarly, we forward parsed messages to the broker, assuming that it is alive. 496 | 497 | == Clean Shutdown 498 | 499 | On of the problems of the current implementation is that it doesn't handle graceful shutdown. 500 | If we break from the accept loop for some reason, all in-flight tasks are just dropped on the floor. 501 | A more correct shutdown sequence would be: 502 | 503 | . Stop accepting new clients 504 | . Deliver all pending messages 505 | . Exit the process 506 | 507 | A clean shutdown in a channel based architecture is easy, although it can appear a magic trick at first. 508 | In Rust, receiver side of a channel is closed as soon as all senders are dropped. 509 | That is, as soon as producers exit and drop their senders, the rest of the system shutdowns naturally. 510 | In `async_std` this translates to two rules: 511 | 512 | . Make sure that channels form an acyclic graph. 513 | . Take care to wait, in the correct order, until intermediate layers of the system process pending messages. 514 | 515 | In `a-chat`, we already have an unidirectional flow of messages: `reader -> broker -> writer`. 516 | However, we never wait for broker and writers, which might cause some messages to get dropped. 517 | Let's add waiting to the server: 518 | 519 | 520 | [source,rust] 521 | ---- 522 | async fn server(addr: impl ToSocketAddrs) -> Result<()> { 523 | let listener = TcpListener::bind(addr).await?; 524 | 525 | let (broker_sender, broker_receiver) = mpsc::unbounded(); 526 | let broker = task::spawn(broker(broker_receiver)); 527 | let mut incoming = listener.incoming(); 528 | while let Some(stream) = incoming.next().await { 529 | let stream = stream?; 530 | println!("Accepting from: {}", stream.peer_addr()?); 531 | spawn_and_log_error(client(broker_sender.clone(), stream)); 532 | } 533 | drop(broker_sender); <1> 534 | broker.await?; <5> 535 | Ok(()) 536 | } 537 | ---- 538 | 539 | And to the broker: 540 | 541 | [source,rust] 542 | ---- 543 | async fn broker(mut events: Receiver) -> Result<()> { 544 | let mut writers = Vec::new(); 545 | let mut peers: HashMap> = HashMap::new(); 546 | 547 | while let Some(event) = events.next().await { <2> 548 | match event { 549 | Event::Message { from, to, msg } => { 550 | for addr in to { 551 | if let Some(peer) = peers.get_mut(&addr) { 552 | peer.send(format!("from {}: {}\n", from, msg)).await? 553 | } 554 | } 555 | } 556 | Event::NewPeer { name, stream} => { 557 | match peers.entry(name) { 558 | Entry::Occupied(..) => (), 559 | Entry::Vacant(entry) => { 560 | let (client_sender, client_receiver) = mpsc::unbounded(); 561 | entry.insert(client_sender); 562 | let handle = spawn_and_log_error(client_writer(client_receiver, stream)); 563 | writers.push(handle); <4> 564 | } 565 | } 566 | } 567 | } 568 | } 569 | drop(peers); <3> 570 | for writer in writers { <4> 571 | writer.await?; 572 | } 573 | Ok(()) 574 | } 575 | ---- 576 | 577 | Notice what happens with all of the channels once we exit the accept loop: 578 | 579 | <1> First, we drop the main broker's sender. 580 | That way when the readers are done, there's no sender for the broker's channel, and the chanel closes. 581 | <2> Next, the broker exits `while let Some(event) = events.next().await` loop. 582 | <3> It's crucial that, at this stage, we drop the `peers` map. 583 | This drops writer's senders. 584 | <4> Now we can join all of the writers. 585 | <5> Finally, we join the broker, which also guarantees that all the writes have terminated. 586 | 587 | == Handling Disconnections 588 | 589 | Currently, we only ever _add_ new peers to the map. 590 | This is clearly wrong: if a peer closes connection to the chat, we should not try to send any more messages to it. 591 | 592 | One subtlety with handling disconnection is that we can detect it either in the reader's task, or in the writer's task. 593 | The most obvious solution here is to just remove the peer from the `peers` map in both cases, but this would be wrong. 594 | If _both_ read and write fail, we'll remove the peer twice, but it can be the case that the peer reconnected between the two failures! 595 | To fix this, we will only remove the peer when the write side finishes. 596 | If the read side finishes we will notify the write side that it should stop as well. 597 | That is, we need to add an ability to signal shutdown for the writer task. 598 | 599 | One way to approach this is a `shutdown: Receiver<()>` channel. 600 | There's a more minimal solution however, which makes a clever use of RAII. 601 | Closing a channel is a synchronization event, so we don't need to send a shutdown message, we can just drop the sender. 602 | This way, we statically guarantee that we issue shutdown exactly once, even if we early return via `?` or panic. 603 | 604 | First, let's add shutdown channel to the `client`: 605 | 606 | [source,rust] 607 | ---- 608 | #[derive(Debug)] 609 | enum Void {} <1> 610 | 611 | #[derive(Debug)] 612 | enum Event { 613 | NewPeer { 614 | name: String, 615 | stream: Arc, 616 | shutdown: Receiver, <2> 617 | }, 618 | Message { 619 | from: String, 620 | to: Vec, 621 | msg: String, 622 | }, 623 | } 624 | 625 | async fn client(mut broker: Sender, stream: TcpStream) -> Result<()> { 626 | // ... 627 | 628 | let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded::(); <3> 629 | broker.send(Event::NewPeer { 630 | name: name.clone(), 631 | stream: Arc::clone(&stream), 632 | shutdown: shutdown_receiver, 633 | }).await.unwrap(); 634 | 635 | // ... 636 | } 637 | ---- 638 | 639 | <1> To enforce that no messages are send along the shutdown channel, we use an uninhabited type. 640 | <2> We pass the shutdown channel to the writer task 641 | <3> In the reader, we create an `_shutdown_sender` whose only purpose is to get dropped. 642 | 643 | In the `client_writer`, we now need to chose between shutdown and message channels. 644 | We use `select` macro for this purpose: 645 | 646 | [source,rust] 647 | ---- 648 | use futures::select; 649 | 650 | async fn client_writer( 651 | messages: &mut Receiver, 652 | stream: Arc, 653 | mut shutdown: Receiver, <1> 654 | ) -> Result<()> { 655 | let mut stream = &*stream; 656 | loop { <2> 657 | select! { 658 | msg = messages.next() => match msg { 659 | Some(msg) => stream.write_all(msg.as_bytes()).await?, 660 | None => break, 661 | }, 662 | void = shutdown.next() => match void { 663 | Some(void) => match void {}, <3> 664 | None => break, 665 | } 666 | } 667 | } 668 | Ok(()) 669 | } 670 | ---- 671 | 672 | <1> We add shutdown channel as an argument. 673 | <2> Because of `select`, we can't use a `while let` loop, so we desugar it further into a `loop`. 674 | <3> In the shutdown case we use `match void {}` as a statically-checked `unreachable!()`. 675 | 676 | 677 | Another problem is that between the moment we detect disconnection in `client_writer` and the moment when we actually remove the peer from the `peers` map, new messages might be pushed into the peer's channel. 678 | To not lose these messages completely, we'll return the messages channel back to broker. 679 | This also allows us to establish a useful invariant that the message channel strictly outlives the peer in the `peers` map, and make the broker itself infailable. 680 | 681 | The final code looks like this: 682 | 683 | [source,rust] 684 | ---- 685 | 686 | 687 | use std::{ 688 | net::ToSocketAddrs, 689 | sync::Arc, 690 | collections::hash_map::{HashMap, Entry}, 691 | }; 692 | 693 | use futures::{ 694 | channel::mpsc, 695 | SinkExt, 696 | select, 697 | }; 698 | 699 | use async_std::{ 700 | io::BufReader, 701 | prelude::*, 702 | task, 703 | net::{TcpListener, TcpStream}, 704 | }; 705 | 706 | type Result = std::result::Result>; 707 | type Sender = mpsc::UnboundedSender; 708 | type Receiver = mpsc::UnboundedReceiver; 709 | 710 | #[derive(Debug)] 711 | enum Void {} 712 | 713 | fn main() -> Result<()> { 714 | task::block_on(server("127.0.0.1:8080")) 715 | } 716 | 717 | async fn server(addr: impl ToSocketAddrs) -> Result<()> { 718 | let listener = TcpListener::bind(addr).await?; 719 | 720 | let (broker_sender, broker_receiver) = mpsc::unbounded(); 721 | let broker = task::spawn(broker(broker_receiver)); 722 | let mut incoming = listener.incoming(); 723 | while let Some(stream) = incoming.next().await { 724 | let stream = stream?; 725 | println!("Accepting from: {}", stream.peer_addr()?); 726 | spawn_and_log_error(client(broker_sender.clone(), stream)); 727 | } 728 | drop(broker_sender); 729 | broker.await; 730 | Ok(()) 731 | } 732 | 733 | async fn client(mut broker: Sender, stream: TcpStream) -> Result<()> { 734 | let stream = Arc::new(stream); 735 | let reader = BufReader::new(&*stream); 736 | let mut lines = reader.lines(); 737 | 738 | let name = match lines.next().await { 739 | None => Err("peer disconnected immediately")?, 740 | Some(line) => line?, 741 | }; 742 | let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded::(); 743 | broker.send(Event::NewPeer { 744 | name: name.clone(), 745 | stream: Arc::clone(&stream), 746 | shutdown: shutdown_receiver, 747 | }).await.unwrap(); 748 | 749 | while let Some(line) = lines.next().await { 750 | let line = line?; 751 | let (dest, msg) = match line.find(':') { 752 | None => continue, 753 | Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), 754 | }; 755 | let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); 756 | let msg: String = msg.trim().to_string(); 757 | 758 | broker.send(Event::Message { 759 | from: name.clone(), 760 | to: dest, 761 | msg, 762 | }).await.unwrap(); 763 | } 764 | 765 | Ok(()) 766 | } 767 | 768 | async fn client_writer( 769 | messages: &mut Receiver, 770 | stream: Arc, 771 | mut shutdown: Receiver, 772 | ) -> Result<()> { 773 | let mut stream = &*stream; 774 | loop { 775 | select! { 776 | msg = messages.next() => match msg { 777 | Some(msg) => stream.write_all(msg.as_bytes()).await?, 778 | None => break, 779 | }, 780 | void = shutdown.next() => match void { 781 | Some(void) => match void {}, 782 | None => break, 783 | } 784 | } 785 | } 786 | Ok(()) 787 | } 788 | 789 | #[derive(Debug)] 790 | enum Event { 791 | NewPeer { 792 | name: String, 793 | stream: Arc, 794 | shutdown: Receiver, 795 | }, 796 | Message { 797 | from: String, 798 | to: Vec, 799 | msg: String, 800 | }, 801 | } 802 | 803 | async fn broker(mut events: Receiver) { 804 | let (disconnect_sender, mut disconnect_receiver) = <1> 805 | mpsc::unbounded::<(String, Receiver)>(); 806 | let mut peers: HashMap> = HashMap::new(); 807 | 808 | loop { 809 | let event = select! { 810 | event = events.next() => match event { 811 | None => break, <2> 812 | Some(event) => event, 813 | }, 814 | disconnect = disconnect_receiver.next() => { 815 | let (name, _pending_messages) = disconnect.unwrap(); <3> 816 | assert!(peers.remove(&name).is_some()); 817 | continue; 818 | }, 819 | }; 820 | match event { 821 | Event::Message { from, to, msg } => { 822 | for addr in to { 823 | if let Some(peer) = peers.get_mut(&addr) { 824 | peer.send(format!("from {}: {}\n", from, msg)).await 825 | .unwrap() <6> 826 | } 827 | } 828 | } 829 | Event::NewPeer { name, stream, shutdown } => { 830 | match peers.entry(name.clone()) { 831 | Entry::Occupied(..) => (), 832 | Entry::Vacant(entry) => { 833 | let (client_sender, mut client_receiver) = mpsc::unbounded(); 834 | entry.insert(client_sender); 835 | let mut disconnect_sender = disconnect_sender.clone(); 836 | spawn_and_log_error(async move { 837 | let res = client_writer(&mut client_receiver, stream, shutdown).await; 838 | disconnect_sender.send((name, client_receiver)).await <4> 839 | .unwrap(); 840 | res 841 | }); 842 | } 843 | } 844 | } 845 | } 846 | } 847 | drop(peers); <5> 848 | drop(disconnect_sender); <6> 849 | while let Some((_name, _pending_messages)) = disconnect_receiver.next().await { 850 | } 851 | } 852 | 853 | fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> 854 | where 855 | F: Future> + Send + 'static, 856 | { 857 | task::spawn(async move { 858 | if let Err(e) = fut.await { 859 | eprintln!("{}", e) 860 | } 861 | }) 862 | } 863 | ---- 864 | 865 | <1> In the broker, we create a channel to reap disconnected peers and their undelivered messages. 866 | <2> The broker's main loop exits when the input events channel is exhausted (that is, when all readers exit). 867 | <3> Because broker itself holds a `disconnect_sender`, we know that the disconnections channel can't be fully drained in the main loop. 868 | <4> We send peer's name and pending messages to the disconnections channel in both the happy and the not-so-happy path. 869 | Again, we can safely unwrap because broker outlives writers. 870 | <5> We drop `peers` map to close writers' messages channel and shut down the writers for sure. 871 | It is not strictly necessary in the current setup, where the broker waits for readers' shutdown anyway. 872 | However, if we add a server-initiated shutdown (for example, kbd:[ctrl+c] handling), this will be a way for the broker to shutdown the writers. 873 | <6> Finally, we close and drain the disconnections channel. 874 | 875 | == Implementing a client 876 | 877 | Let's now implement the client for the chat. 878 | Because the protocol is line-based, the implementation is pretty straightforward: 879 | 880 | * Lines read from stdin should be send over the socket. 881 | * Lines read from the socket should be echoed to stdout. 882 | 883 | Unlike the server, the client needs only limited concurrency, as it interacts with only a single user. 884 | For this reason, async doesn't bring a lot of performance benefits in this case. 885 | 886 | However, async is still useful for managing concurrency! 887 | Specifically, the client should _simultaneously_ read from stdin and from the socket. 888 | Programming this with threads is cumbersome, especially when implementing clean shutdown. 889 | With async, we can just use the `select!` macro. 890 | 891 | [source,rust] 892 | ---- 893 | 894 | 895 | use std::net::ToSocketAddrs; 896 | 897 | use futures::select; 898 | 899 | use async_std::{ 900 | prelude::*, 901 | net::TcpStream, 902 | task, 903 | io::{stdin, BufReader}, 904 | }; 905 | 906 | type Result = std::result::Result>; 907 | 908 | 909 | fn main() -> Result<()> { 910 | task::block_on(try_main("127.0.0.1:8080")) 911 | } 912 | 913 | async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { 914 | let stream = TcpStream::connect(addr).await?; 915 | let (reader, mut writer) = (&stream, &stream); <1> 916 | let reader = BufReader::new(reader); 917 | let mut lines_from_server = futures::StreamExt::fuse(reader.lines()); <2> 918 | 919 | let stdin = BufReader::new(stdin()); 920 | let mut lines_from_stdin = futures::StreamExt::fuse(stdin.lines()); <2> 921 | loop { 922 | select! { <3> 923 | line = lines_from_server.next() => match line { 924 | Some(line) => { 925 | let line = line?; 926 | println!("{}", line); 927 | }, 928 | None => break, 929 | }, 930 | line = lines_from_stdin.next() => match line { 931 | Some(line) => { 932 | let line = line?; 933 | writer.write_all(line.as_bytes()).await?; 934 | writer.write_all(b"\n").await?; 935 | } 936 | None => break, 937 | } 938 | } 939 | } 940 | Ok(()) 941 | } 942 | ---- 943 | 944 | <1> Here we split `TcpStream` into read and write halfs: there's `impl AsyncRead for &'_ TcpStream`, just like the one in std. 945 | <2> We crate a stream of lines for both the socket and stdin. 946 | <3> In the main select loop, we print the lines we receive from server and send the lines we read from the console. 947 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly-2019-08-21 2 | -------------------------------------------------------------------------------- /src/bin/client.rs: -------------------------------------------------------------------------------- 1 | use std::net::ToSocketAddrs; 2 | 3 | use futures::select; 4 | use futures::FutureExt; 5 | 6 | use async_std::{ 7 | prelude::*, 8 | net::TcpStream, 9 | task, 10 | io::{stdin, BufReader}, 11 | }; 12 | 13 | type Result = std::result::Result>; 14 | 15 | 16 | fn main() -> Result<()> { 17 | task::block_on(try_main("127.0.0.1:8080")) 18 | } 19 | 20 | async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { 21 | let stream = TcpStream::connect(addr).await?; 22 | let (reader, mut writer) = (&stream, &stream); 23 | let reader = BufReader::new(reader); 24 | let mut lines_from_server = futures::StreamExt::fuse(reader.lines()); 25 | 26 | let stdin = BufReader::new(stdin()); 27 | let mut lines_from_stdin = futures::StreamExt::fuse(stdin.lines()); 28 | loop { 29 | select! { 30 | line = lines_from_server.next().fuse() => match line { 31 | Some(line) => { 32 | let line = line?; 33 | println!("{}", line); 34 | }, 35 | None => break, 36 | }, 37 | line = lines_from_stdin.next().fuse() => match line { 38 | Some(line) => { 39 | let line = line?; 40 | writer.write_all(line.as_bytes()).await?; 41 | writer.write_all(b"\n").await?; 42 | } 43 | None => break, 44 | } 45 | } 46 | } 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /src/bin/server.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::ToSocketAddrs, 3 | sync::Arc, 4 | collections::hash_map::{HashMap, Entry}, 5 | }; 6 | 7 | use futures::{ 8 | channel::mpsc, 9 | SinkExt, 10 | FutureExt, 11 | select, 12 | }; 13 | 14 | use async_std::{ 15 | io::BufReader, 16 | prelude::*, 17 | task, 18 | net::{TcpListener, TcpStream}, 19 | }; 20 | 21 | type Result = std::result::Result>; 22 | type Sender = mpsc::UnboundedSender; 23 | type Receiver = mpsc::UnboundedReceiver; 24 | 25 | #[derive(Debug)] 26 | enum Void {} 27 | 28 | fn main() -> Result<()> { 29 | task::block_on(accept_loop("127.0.0.1:8080")) 30 | } 31 | 32 | async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { 33 | let listener = TcpListener::bind(addr).await?; 34 | 35 | let (broker_sender, broker_receiver) = mpsc::unbounded(); 36 | let broker = task::spawn(broker_loop(broker_receiver)); 37 | let mut incoming = listener.incoming(); 38 | while let Some(stream) = incoming.next().await { 39 | let stream = stream?; 40 | println!("Accepting from: {}", stream.peer_addr()?); 41 | spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); 42 | } 43 | drop(broker_sender); 44 | broker.await; 45 | Ok(()) 46 | } 47 | 48 | async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result<()> { 49 | let stream = Arc::new(stream); 50 | let reader = BufReader::new(&*stream); 51 | let mut lines = reader.lines(); 52 | 53 | let name = match lines.next().await { 54 | None => Err("peer disconnected immediately")?, 55 | Some(line) => line?, 56 | }; 57 | let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded::(); 58 | broker.send(Event::NewPeer { 59 | name: name.clone(), 60 | stream: Arc::clone(&stream), 61 | shutdown: shutdown_receiver, 62 | }).await.unwrap(); 63 | 64 | while let Some(line) = lines.next().await { 65 | let line = line?; 66 | let (dest, msg) = match line.find(':') { 67 | None => continue, 68 | Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), 69 | }; 70 | let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); 71 | let msg: String = msg.trim().to_string(); 72 | 73 | broker.send(Event::Message { 74 | from: name.clone(), 75 | to: dest, 76 | msg, 77 | }).await.unwrap(); 78 | } 79 | 80 | Ok(()) 81 | } 82 | 83 | async fn connection_writer_loop( 84 | messages: &mut Receiver, 85 | stream: Arc, 86 | mut shutdown: Receiver, 87 | ) -> Result<()> { 88 | let mut stream = &*stream; 89 | loop { 90 | select! { 91 | msg = messages.next().fuse() => match msg { 92 | Some(msg) => stream.write_all(msg.as_bytes()).await?, 93 | None => break, 94 | }, 95 | void = shutdown.next().fuse() => match void { 96 | Some(void) => match void {}, 97 | None => break, 98 | } 99 | } 100 | } 101 | Ok(()) 102 | } 103 | 104 | #[derive(Debug)] 105 | enum Event { 106 | NewPeer { 107 | name: String, 108 | stream: Arc, 109 | shutdown: Receiver, 110 | }, 111 | Message { 112 | from: String, 113 | to: Vec, 114 | msg: String, 115 | }, 116 | } 117 | 118 | async fn broker_loop(mut events: Receiver) { 119 | let (disconnect_sender, mut disconnect_receiver) = 120 | mpsc::unbounded::<(String, Receiver)>(); 121 | let mut peers: HashMap> = HashMap::new(); 122 | 123 | loop { 124 | let event = select! { 125 | event = events.next().fuse() => match event { 126 | None => break, 127 | Some(event) => event, 128 | }, 129 | disconnect = disconnect_receiver.next().fuse() => { 130 | let (name, _pending_messages) = disconnect.unwrap(); 131 | assert!(peers.remove(&name).is_some()); 132 | continue; 133 | }, 134 | }; 135 | match event { 136 | Event::Message { from, to, msg } => { 137 | for addr in to { 138 | if let Some(peer) = peers.get_mut(&addr) { 139 | peer.send(format!("from {}: {}\n", from, msg)).await 140 | .unwrap() 141 | } 142 | } 143 | } 144 | Event::NewPeer { name, stream, shutdown } => { 145 | match peers.entry(name.clone()) { 146 | Entry::Occupied(..) => (), 147 | Entry::Vacant(entry) => { 148 | let (client_sender, mut client_receiver) = mpsc::unbounded(); 149 | entry.insert(client_sender); 150 | let mut disconnect_sender = disconnect_sender.clone(); 151 | spawn_and_log_error(async move { 152 | let res = connection_writer_loop(&mut client_receiver, stream, shutdown).await; 153 | disconnect_sender.send((name, client_receiver)).await 154 | .unwrap(); 155 | res 156 | }); 157 | } 158 | } 159 | } 160 | } 161 | } 162 | drop(peers); 163 | drop(disconnect_sender); 164 | while let Some((_name, _pending_messages)) = disconnect_receiver.next().await { 165 | } 166 | } 167 | 168 | fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> 169 | where 170 | F: Future> + Send + 'static, 171 | { 172 | task::spawn(async move { 173 | if let Err(e) = fut.await { 174 | eprintln!("{}", e) 175 | } 176 | }) 177 | } 178 | --------------------------------------------------------------------------------