07b9183
Branches > main > 07b9183 > files / deps.edn
Decouple frames from mutable, writable buffers
Commit: 07b9183626cca706abdf710342b25c11005b3b6b
Parent: 2258a02
5 changed files:
src/net/bowmap/server/network/streams.clj view file
@@ -1,10 +1,9 @@ (ns net.bowmap.server.network.streams "Functions for reading and writing frames to Input and OutputStreams" (:require- [net.bowmap.server.protocol.frame :refer [frame]]+ [net.bowmap.server.protocol.frame :as frame] [net.bowmap.server.protocol.types :refer [max-varint-size]]) (:import- [io.netty.buffer ByteBuf] [java.io EOFException IOException@@ -46,7 +45,7 @@ (if (= r -1) (throw (EOFException. "EOF while reading frame")) (recur (+ offset r))))))- (frame arr)))+ (frame/frame arr))) (defn write-varint!! "Blocks and writes a varint to an OutputStream."@@ -63,9 +62,8 @@ "Blocks and writes a frame to an OutputStream. Throws if the frame cannot be written." [frame output-stream]- (let [len (ByteBuf/.readableBytes frame)- arr (byte-array len)]- (ByteBuf/.readBytes frame arr)- (write-varint!! output-stream len)- (OutputStream/.write output-stream arr)- (OutputStream/.flush output-stream)))+ (frame/write! frame+ (fn [array length]+ (write-varint!! output-stream length)+ (OutputStream/.write output-stream array 0 length)+ (OutputStream/.flush output-stream))))src/net/bowmap/server/protocol/buffer.clj view file
@@ -0,0 +1,24 @@+(ns net.bowmap.server.protocol.buffer+ (:require+ [clojure.spec.alpha :as s]+ [net.bowmap.server.protocol.frame :as frame])+ (:import+ [java.io ByteArrayOutputStream]))++(defprotocol WriteBuffer+ "A single-threaded transiently mutable growable buffer of bytes."+ (write-byte! [buf b] "Returns this buffer after adding one byte to the end.+ Byte may be signed or unsigned.")+ (->frame [buf] "Returns a frame from this buf.+ After this call, the buffer is not to be mutated."))++(s/def ::write-buffer #(satisfies? WriteBuffer %))++(defn write-buffer []+ (let [baos (ByteArrayOutputStream.)]+ (reify WriteBuffer+ (write-byte! [buf b]+ (^[int] ByteArrayOutputStream/.write baos (bit-and 0xFF b))+ buf)+ (->frame [_]+ (frame/frame (ByteArrayOutputStream/.toByteArray baos))))))src/net/bowmap/server/protocol/frame.clj view file
@@ -1,12 +1,33 @@ (ns net.bowmap.server.protocol.frame- "A framed byte buffer containing one serialized Minecraft protocol packet.- A frame contains the packet's numeric id and payload.- Frames are not length-prefixed.- See https://minecraft.wiki/w/Java_Edition_protocol/Packets for the format."+ (:require+ [clojure.spec.alpha :as s]) (:import- [io.netty.buffer Unpooled]))+ [io.netty.buffer ByteBuf Unpooled]))++(defprotocol Frame+ "A immutable sequence of bytes representing one framed, serialized Minecraft protocol packet.+ Frames contain the packet's numeric id and payload, but are not length-prefixed.+ See https://minecraft.wiki/w/Java_Edition_protocol/Packets for the packet format."+ (write! [frame f] "Calls 'f' with the array and length of the frame."))++(s/def ::frame #(satisfies? Frame %)) (defn frame- "Returns an empty frame, or a frame containing the bytes of an array."- ([] (Unpooled/buffer))- ([arr] (Unpooled/wrappedBuffer arr)))+ ([array] (frame array (alength array)))+ ([array length]+ ^{:type ::frame}+ (reify Frame+ (write! [_ f] (f array length)))))++(defmethod print-method ::frame [frame writer]+ (write! frame+ (fn [array length]+ (.write writer "#Frame ")+ (.write writer (pr-str (subvec (vec array) 0 length))))))++(defn ->ByteBuf+ "Returns a read-only ByteBuf containing the bytes of a frame."+ [frame]+ (write! frame+ (fn [array length]+ (ByteBuf/.asReadOnly (Unpooled/wrappedBuffer array 0 length)))))src/net/bowmap/server/protocol/packet_serialization.clj view file
@@ -1,7 +1,8 @@ (ns net.bowmap.server.protocol.packet-serialization (:require+ [net.bowmap.server.protocol.buffer :as buffer] [net.bowmap.server.protocol.clientbound-packets :as clientbound]- [net.bowmap.server.protocol.frame :refer [frame]]+ [net.bowmap.server.protocol.frame :as frame] [net.bowmap.server.protocol.packet :as packet] [net.bowmap.server.protocol.phase :as phase] [net.bowmap.server.protocol.readers :as readers]@@ -14,16 +15,18 @@ [{::phase/keys! [phase]} {::packet/keys! [id] :as packet}] (let [numeric-id (req! (clientbound/phase->id->numeric-id phase) id) writer (req! (clientbound/phase->id->writer phase) id)]- (-> (frame)- (writers/varint> numeric-id)- (writer packet))))+ (buffer/->frame+ (-> (buffer/write-buffer)+ (writers/varint> numeric-id)+ (writer packet))))) (defn decode-frame "Decodes a packet from a frame. Can throw if the frame does not contain a valid packet." [{:keys! [::phase/phase]} frame]- (let [numeric-id (readers/varint< frame)+ (let [buf (frame/->ByteBuf frame)+ numeric-id (readers/varint< buf) id (req! (serverbound/phase->numeric-id->id phase) numeric-id) reader (req! (serverbound/phase->id->reader phase) id)]- (assoc (reader frame)+ (assoc (reader buf) ::packet/id id)))src/net/bowmap/server/protocol/writers.clj view file
@@ -1,18 +1,19 @@ (ns net.bowmap.server.protocol.writers- "Functions for writing values into io.netty.ByteBufs.+ "Functions for writing values into write buffers. Functions look like type>, where 'type' is a Minecraft protocol type. See https://minecraft.wiki/w/Java_Edition_protocol/Packets#Data_types" (:require [clojure.data.json :as json] [clojure.spec.alpha :as s]+ [net.bowmap.server.protocol.buffer :as buffer] [net.bowmap.server.protocol.schema :as p]) (:import- [io.netty.buffer ByteBuf] [java.nio.charset StandardCharsets] [java.util UUID])) (s/def ::writer (s/fspec- :args (s/cat :buf #(instance? ByteBuf %) :value any?)+ :args (s/cat :buf ::buffer/write-buffer+ :value any?) :fn #(= (-> % :args :buf) (:ret %)))) @@ -25,6 +26,7 @@ (catch Throwable t (throw (ex-info "Error while writing table field" {:field field field (get m field)+ :writer writer :table m} t))))) @@ -51,14 +53,28 @@ (fn? x) x :else (throw (IllegalArgumentException. (str "Cannot coerce into writer: " (pr-str x)))))) -(def byte> ByteBuf/.writeByte)-(def ubyte> ByteBuf/.writeByte)-(def short> ByteBuf/.writeShort)-(def ushort> ByteBuf/.writeShort)-(def int> ByteBuf/.writeInt)-(def long> ByteBuf/.writeLong)-(def float> ByteBuf/.writeFloat)-(def double> ByteBuf/.writeDouble)+(def byte> buffer/write-byte!)++(defn- integer> [byte-count]+ (let [range (range (* 8 (dec byte-count)) -1 -8)]+ (fn [buf n]+ (reduce (fn [acc bits]+ (byte> acc (bit-shift-right n bits)))+ buf range))))++(defn- apply>+ "Returns a writer that writes (f x) via 'writer'."+ [f writer]+ (fn [buf x]+ (writer buf (f x))))++(def ubyte> byte>)+(def short> (integer> 2))+(def ushort> short>)+(def int> (integer> 4))+(def long> (integer> 8))+(def float> (apply> Float/floatToRawIntBits int>))+(def double> (apply> Double/doubleToRawLongBits long>)) (defn boolean> "Writes the byte 1 if 'x' is truthy, else the byte 0."@@ -77,44 +93,6 @@ (recur (byte> buf (bit-or b 0x80)) nxt))))) -(defn string>- "Writes a string as a varint byte length followed by its UTF-8 bytes.-- When passed 1 arg, returns a string writer that throws when the string- exceeds 'max-length' characters."- ([max-length]- (fn [buf s]- (if (> (count s) max-length)- (throw (ex-info "Cannot encode string, too long" {:max-length max-length :string s}))- (string> buf s))))- ([buf s]- {:pre [(string? s)]}- (doto buf- (varint> (count s))- (ByteBuf/.writeCharSequence s StandardCharsets/UTF_8))))--(defn json>- "Turns 'x' into JSON and writes it as a string."- [buf x]- (string> buf (json/write-str x)))--(defn status-response> [buf {::p/keys! [version-name protocol]- ::p/keys [motd max-player-count online-player-count]}]- (json> buf- (cond-> {:version {:name version-name- :protocol protocol}}- motd (assoc :description motd)-- (and max-player-count online-player-count)- (assoc :players {:max max-player-count- :online online-player-count}))))--(defn uuid> [buf uuid]- {:pre [(uuid? uuid)]}- (-> buf- (long> (UUID/.getMostSignificantBits uuid))- (long> (UUID/.getLeastSignificantBits uuid))))- (defn array> "Returns a writer that writes every value in a coll via 'writer'." [writer]@@ -133,8 +111,45 @@ (-> (varint> buf (count xs)) (array-writer xs))))) +(def bytes> (list> byte>))++(defn string>+ "Writes a string as a varint byte length followed by its UTF-8 bytes.++ When passed 1 arg, returns a string writer that throws when the string+ exceeds 'max-length' characters."+ ([max-length]+ (fn [buf s]+ (if (> (count s) max-length)+ (throw (ex-info "Cannot encode string, too long" {:max-length max-length :string s}))+ (string> buf s))))+ ([buf s]+ (bytes> buf (String/.getBytes s StandardCharsets/UTF_8))))++(def json>+ "Turns 'x' into JSON and writes it as a string."+ (apply> json/write-str string>))++(defn- ->status-response [{::p/keys! [version-name protocol]+ ::p/keys [motd max-player-count online-player-count]}]+ (cond-> {:version {:name version-name+ :protocol protocol}}+ motd (assoc :description motd)++ (and max-player-count online-player-count)+ (assoc :players {:max max-player-count+ :online online-player-count})))++(def status-response> (apply> ->status-response json>))++(defn uuid> [buf uuid]+ {:pre [(uuid? uuid)]}+ (-> buf+ (long> (UUID/.getMostSignificantBits uuid))+ (long> (UUID/.getLeastSignificantBits uuid))))+ (defn context-optional>- "Returns a writer that writes a value via 'writer' if the value is non-nil."+ "Returns a writer that only writes a value via 'writer' if the value is non-nil." [writer] (fn [buf x] (if (some? x)@@ -143,7 +158,7 @@ (defn optional> "Returns a writer that prefixes the value with a boolean indicating whether- it is present, and writes the value via 'writer' if it is non-nil."+ it is present, and only writes the value via 'writer' if the value is non-nil." [writer] (let [writer (context-optional> writer)] (fn [buf x]