|
| 1 | +open Httpun |
| 2 | +open Httpun_eio |
| 3 | +open Eio.Std |
| 4 | + |
| 5 | +(* Heavily inspired by the original httpaf implementation in this repo with eio additions from: |
| 6 | + * https://github.com/ocaml-multicore/eio/blob/8f7f82d2c12076af8e9b8b365c58ebadaa963b8c/examples/net/server.ml |
| 7 | + * https://github.com/ocaml-multicore/eio/blob/8f7f82d2c12076af8e9b8b365c58ebadaa963b8c/examples/net/main.ml |
| 8 | + * https://github.com/anmonteiro/httpun/blob/37fdcd8fd09dc851acbf224c4ec1cb8681942f04/examples/lib/httpun_examples.ml#L115-L125 |
| 9 | + * https://github.com/anmonteiro/httpun/blob/37fdcd8fd09dc851acbf224c4ec1cb8681942f04/examples/eio/eio_connect_server.ml |
| 10 | + *) |
| 11 | + |
| 12 | +(* Dates *) |
| 13 | + |
| 14 | +let get_date () = Unix.(gettimeofday () |> gmtime) |
| 15 | + |
| 16 | +let dow = function |
| 17 | + | 0 -> "Sun" |
| 18 | + | 1 -> "Mon" |
| 19 | + | 2 -> "Tue" |
| 20 | + | 3 -> "Wed" |
| 21 | + | 4 -> "Thu" |
| 22 | + | 5 -> "Fri" |
| 23 | + | _ -> "Sat" |
| 24 | + |
| 25 | +let month = function |
| 26 | + | 0 -> "Jan" |
| 27 | + | 1 -> "Feb" |
| 28 | + | 2 -> "Mar" |
| 29 | + | 3 -> "Apr" |
| 30 | + | 4 -> "May" |
| 31 | + | 5 -> "Jun" |
| 32 | + | 6 -> "Jul" |
| 33 | + | 7 -> "Aug" |
| 34 | + | 8 -> "Sep" |
| 35 | + | 9 -> "Oct" |
| 36 | + | 10 -> "Nov" |
| 37 | + | _ -> "Dec" |
| 38 | + |
| 39 | +let date () = |
| 40 | + let d = get_date () in |
| 41 | + (* Wed, 17 Apr 2013 12:00:00 GMT *) |
| 42 | + Format.sprintf "%s, %02d %s %4d %02d:%02d:%02d GMT" (dow d.tm_wday) d.tm_mday |
| 43 | + (month d.tm_mon) (1900 + d.tm_year) d.tm_hour d.tm_min d.tm_sec |
| 44 | + |
| 45 | +let memo_date = ref @@ date () |
| 46 | + |
| 47 | +let refresh_date () = |
| 48 | + let f _ = |
| 49 | + memo_date := date (); |
| 50 | + ignore @@ Unix.alarm 1 |
| 51 | + in |
| 52 | + (ignore @@ Sys.(signal sigalrm (Signal_handle f))); |
| 53 | + f () |
| 54 | + |
| 55 | +(* HTTP *) |
| 56 | + |
| 57 | +let request_handler (_ : Eio.Net.Sockaddr.stream) { Gluten.reqd; _ } = |
| 58 | + let req = Reqd.request reqd in |
| 59 | + match req.target with |
| 60 | + | "/json" -> |
| 61 | + let obj = `Assoc [ ("message", `String "Hello, World!") ] in |
| 62 | + let payload = Yojson.to_string obj in |
| 63 | + let headers = |
| 64 | + Headers.of_rev_list |
| 65 | + [ |
| 66 | + ("content-length", string_of_int @@ String.length payload); |
| 67 | + ("content-type", "application/json"); |
| 68 | + ("server", "httpun"); |
| 69 | + ("date", !memo_date); |
| 70 | + ] |
| 71 | + in |
| 72 | + let rsp = Response.create ~headers `OK in |
| 73 | + Reqd.respond_with_string reqd rsp payload |
| 74 | + | "/plaintext" -> |
| 75 | + let payload = "Hello, World!" in |
| 76 | + let headers = |
| 77 | + Headers.of_rev_list |
| 78 | + [ |
| 79 | + ("content-length", string_of_int @@ String.length payload); |
| 80 | + ("content-type", "text/plain"); |
| 81 | + ("server", "httpun"); |
| 82 | + ("date", !memo_date); |
| 83 | + ] |
| 84 | + in |
| 85 | + let rsp = Response.create ~headers `OK in |
| 86 | + Reqd.respond_with_string reqd rsp payload |
| 87 | + | _ -> |
| 88 | + let moo = "m00." in |
| 89 | + let headers = |
| 90 | + Headers.of_list |
| 91 | + [ ("content-length", string_of_int @@ String.length moo) ] |
| 92 | + in |
| 93 | + let rsp = Response.create ~headers `OK in |
| 94 | + Reqd.respond_with_string reqd rsp moo |
| 95 | + |
| 96 | +let error_handler (_ : Eio.Net.Sockaddr.stream) ?request:_ error start_response |
| 97 | + = |
| 98 | + let response_body = start_response Headers.empty in |
| 99 | + (match error with |
| 100 | + | `Exn exn -> |
| 101 | + Body.Writer.write_string response_body (Printexc.to_string exn); |
| 102 | + Body.Writer.write_string response_body "\n" |
| 103 | + | #Status.standard as error -> |
| 104 | + Body.Writer.write_string response_body |
| 105 | + (Status.default_reason_phrase error)); |
| 106 | + Body.Writer.close response_body |
| 107 | + |
| 108 | +let () = |
| 109 | + let domain_count = Stdlib.Domain.recommended_domain_count () in |
| 110 | + Printf.eprintf "Detected %d cores\n" domain_count; |
| 111 | + let ulimit_n = |
| 112 | + Unix.open_process_in "ulimit -n" |> input_line |> int_of_string |
| 113 | + in |
| 114 | + Printf.eprintf "Detected %d max open files\n" ulimit_n; |
| 115 | + let somaxconn = |
| 116 | + Stdlib.open_in "/proc/sys/net/core/somaxconn" |
| 117 | + |> Stdlib.input_line |> Stdlib.int_of_string |
| 118 | + in |
| 119 | + Printf.eprintf "Detected %d somaxconn\n" somaxconn; |
| 120 | + refresh_date (); |
| 121 | + let backlog = Stdlib.min ulimit_n somaxconn in |
| 122 | + |
| 123 | + Eio_main.run @@ fun env -> |
| 124 | + Switch.run @@ fun sw -> |
| 125 | + (* https://github.com/ocaml-multicore/eio/tree/main?tab=readme-ov-file#executor-pool *) |
| 126 | + let dm = Eio.Stdenv.domain_mgr env in |
| 127 | + |
| 128 | + let addr = `Tcp (Eio.Net.Ipaddr.V4.any, 8080) in |
| 129 | + let listening_socket = |
| 130 | + Eio.Net.listen ~sw env#net addr ~backlog ~reuse_addr:true |
| 131 | + in |
| 132 | + |
| 133 | + let connection_handler flow addr = |
| 134 | + Server.create_connection_handler ~request_handler ~error_handler ~sw addr |
| 135 | + flow |
| 136 | + in |
| 137 | + let run listening_socket = |
| 138 | + Eio.Net.run_server ~additional_domains:(dm, domain_count) listening_socket |
| 139 | + ~max_connections:backlog connection_handler |
| 140 | + ~on_error:(traceln "Error handling connection: %a" Fmt.exn) |
| 141 | + in |
| 142 | + run listening_socket |
0 commit comments