phpx/server: improve wire format

This commit updates the wire format of the phpx server. Instead of being
string-based, messages sent back and forth between go and php are now
base64-encoded DEFLATEd strings. This makes them a lot smaller and
faster to send.
This commit is contained in:
Tom 2023-05-01 15:50:21 +02:00
parent ffd9d2e695
commit a9572e6613
4 changed files with 80 additions and 38 deletions

View file

@ -6,6 +6,7 @@ import "fmt"
const (
errInit = "Server initialization failed"
errClosed = "Server closed"
errSend = "Failed to encode request"
errReceive = "Failed to decode response"
)

View file

@ -1,7 +1,10 @@
package phpx
import (
"bytes"
"compress/flate"
"context"
"encoding/base64"
"encoding/json"
"io"
"os"
@ -102,10 +105,6 @@ func (server *Server) MarshalEval(ctx context.Context, value any, code string) e
return err
}
// input to be sent to the server
delim := findDelimiter(code)
input := delim + "\n" + code + "\n" + delim + "\n"
server.m.Lock()
defer server.m.Unlock()
@ -114,9 +113,13 @@ func (server *Server) MarshalEval(ctx context.Context, value any, code string) e
return ServerError{Message: errClosed}
}
// find a delimiter for the code, and then send
io.WriteString(server.in, input)
// encode a message to the server!
if err := server.encode(server.in, code); err != nil {
server.cancel()
return ServerError{Message: errSend, Err: err}
}
// read the response
data, err, _ := contextx.Run2(ctx, func(start func()) (string, error) {
return nobufio.ReadLine(server.out)
}, func() {
@ -126,9 +129,9 @@ func (server *Server) MarshalEval(ctx context.Context, value any, code string) e
return ServerError{Message: errReceive, Err: err}
}
// read whatever we received
// decode the response
var received [2]json.RawMessage
if err := json.Unmarshal([]byte(data), &received); err != nil {
if err := server.decode(&received, []byte(data)); err != nil {
return ServerError{Message: errReceive, Err: err}
}
@ -147,6 +150,59 @@ func (server *Server) MarshalEval(ctx context.Context, value any, code string) e
return json.Unmarshal(received[0], value)
}
// Decode decodes a message received from the server.
// The message is assumed to be encoded by server.php.
//
// This function does the following:
// - decode base64 (opposite of php's "base64_encode")
// - inflate (opposite of php's "gzdeflate")
// - decode json (opposite of php's "json_encode")
func (*Server) decode(dest *[2]json.RawMessage, message []byte) error {
// decode base64
raw := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(message))
// unpack gzip
unpacker := flate.NewReader(raw)
defer unpacker.Close()
// and read the value
decoder := json.NewDecoder(unpacker)
return decoder.Decode(dest)
}
// Encode encodes and writes a message for the server into dest.
// The message is assumed to be received by server.php.
//
// This function does the following:
// - inflate (opposite of php's "gzdeflate")
// - encode base64 (opposite of php's "base64_decode")
func (*Server) encode(dest io.WriteCloser, code string) (err error) {
// write a final newline at the end!
defer func() {
if err != nil {
return
}
_, err = dest.Write([]byte("\n"))
}()
// base64 encode all the things!
encoder := base64.NewEncoder(base64.StdEncoding, dest)
defer encoder.Close()
// compress all the things!
compressor, err := flate.NewWriter(encoder, 9)
if err != nil {
return err
}
defer compressor.Close()
// do the write!
_, err = compressor.Write([]byte(code))
return
}
// Eval is like [MarshalEval], but returns the value as an any
func (server *Server) Eval(ctx context.Context, code string) (value any, err error) {
err = server.MarshalEval(ctx, &value, code)
@ -190,27 +246,6 @@ func (server *Server) Call(ctx context.Context, function string, args ...any) (v
return
}
const delimiterRune = 'F' // press to pay respect
// findDelimiter finds a delimiter that does not occur in the input string
func findDelimiter(input string) string {
// find the longest sequence of delimiter rune
var current, longest int
for _, r := range input {
if r == delimiterRune {
current++
} else {
current = 0
}
if current > longest {
longest = current
}
}
// and then return it multipled longer than that
return strings.Repeat(string(delimiterRune), longest+1)
}
// Close closes this server and prevents any further code from being run.
func (server *Server) Close() error {
server.prepare()

View file

@ -8,6 +8,13 @@
// It should only contain comments at the beginning of each line, and only starting with '//'.
// See server.go.
// This file runs an infinite REPL-like loop.
// It continously reads from standard input, decodes code to pass to eval(), and send the result back.
//
// Commands are read in DEFLATE base64 encoding (to prevent having to send too much over the wire).
// Results are written to STDOUT back in base64 DEFLATE encoding.
// The results are of the form [$result,$error] - $result being the actual object returned, and $error an error string.
// define a json_encode alias, this saves a single character!
// (we also reuse it in the error string)
$E = 'json_encode';
@ -19,15 +26,11 @@ while(1) {
// stop outputting errors when executing
ob_start(null,0,PHP_OUTPUT_HANDLER_CLEANABLE);
// read the next line to get an end-of-line marker
$m = fgets(STDIN) or exit(0);
// read the next line (which will be code to execute)
$b = fgets(STDIN) or exit(0);
// accumulate the buffer until the marker is reached
// bail out if there is an unexpected end of input
for($b = $l = ""; $l !== $m;) {
$b .= $l;
$l = fgets(STDIN) or exit(1);
}
// read the next command to parse
$b = gzinflate(base64_decode($b));
// execute the code, and json_encode it
try {
@ -43,5 +46,8 @@ while(1) {
// and write out the result
ob_end_clean();
// deflate and base64_encode
$j = base64_encode(gzdeflate($j));
fwrite(STDOUT,"$j\n");
}