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

@ -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()