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:
parent
ffd9d2e695
commit
a9572e6613
4 changed files with 80 additions and 38 deletions
|
|
@ -6,6 +6,7 @@ import "fmt"
|
||||||
const (
|
const (
|
||||||
errInit = "Server initialization failed"
|
errInit = "Server initialization failed"
|
||||||
errClosed = "Server closed"
|
errClosed = "Server closed"
|
||||||
|
errSend = "Failed to encode request"
|
||||||
errReceive = "Failed to decode response"
|
errReceive = "Failed to decode response"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package phpx
|
package phpx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/flate"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -102,10 +105,6 @@ func (server *Server) MarshalEval(ctx context.Context, value any, code string) e
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// input to be sent to the server
|
|
||||||
delim := findDelimiter(code)
|
|
||||||
input := delim + "\n" + code + "\n" + delim + "\n"
|
|
||||||
|
|
||||||
server.m.Lock()
|
server.m.Lock()
|
||||||
defer server.m.Unlock()
|
defer server.m.Unlock()
|
||||||
|
|
||||||
|
|
@ -114,9 +113,13 @@ func (server *Server) MarshalEval(ctx context.Context, value any, code string) e
|
||||||
return ServerError{Message: errClosed}
|
return ServerError{Message: errClosed}
|
||||||
}
|
}
|
||||||
|
|
||||||
// find a delimiter for the code, and then send
|
// encode a message to the server!
|
||||||
io.WriteString(server.in, input)
|
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) {
|
data, err, _ := contextx.Run2(ctx, func(start func()) (string, error) {
|
||||||
return nobufio.ReadLine(server.out)
|
return nobufio.ReadLine(server.out)
|
||||||
}, func() {
|
}, func() {
|
||||||
|
|
@ -126,9 +129,9 @@ func (server *Server) MarshalEval(ctx context.Context, value any, code string) e
|
||||||
return ServerError{Message: errReceive, Err: err}
|
return ServerError{Message: errReceive, Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// read whatever we received
|
// decode the response
|
||||||
var received [2]json.RawMessage
|
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}
|
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)
|
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
|
// Eval is like [MarshalEval], but returns the value as an any
|
||||||
func (server *Server) Eval(ctx context.Context, code string) (value any, err error) {
|
func (server *Server) Eval(ctx context.Context, code string) (value any, err error) {
|
||||||
err = server.MarshalEval(ctx, &value, code)
|
err = server.MarshalEval(ctx, &value, code)
|
||||||
|
|
@ -190,27 +246,6 @@ func (server *Server) Call(ctx context.Context, function string, args ...any) (v
|
||||||
return
|
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.
|
// Close closes this server and prevents any further code from being run.
|
||||||
func (server *Server) Close() error {
|
func (server *Server) Close() error {
|
||||||
server.prepare()
|
server.prepare()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,13 @@
|
||||||
// It should only contain comments at the beginning of each line, and only starting with '//'.
|
// It should only contain comments at the beginning of each line, and only starting with '//'.
|
||||||
// See server.go.
|
// 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!
|
// define a json_encode alias, this saves a single character!
|
||||||
// (we also reuse it in the error string)
|
// (we also reuse it in the error string)
|
||||||
$E = 'json_encode';
|
$E = 'json_encode';
|
||||||
|
|
@ -19,15 +26,11 @@ while(1) {
|
||||||
// stop outputting errors when executing
|
// stop outputting errors when executing
|
||||||
ob_start(null,0,PHP_OUTPUT_HANDLER_CLEANABLE);
|
ob_start(null,0,PHP_OUTPUT_HANDLER_CLEANABLE);
|
||||||
|
|
||||||
// read the next line to get an end-of-line marker
|
// read the next line (which will be code to execute)
|
||||||
$m = fgets(STDIN) or exit(0);
|
$b = fgets(STDIN) or exit(0);
|
||||||
|
|
||||||
// accumulate the buffer until the marker is reached
|
// read the next command to parse
|
||||||
// bail out if there is an unexpected end of input
|
$b = gzinflate(base64_decode($b));
|
||||||
for($b = $l = ""; $l !== $m;) {
|
|
||||||
$b .= $l;
|
|
||||||
$l = fgets(STDIN) or exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute the code, and json_encode it
|
// execute the code, and json_encode it
|
||||||
try {
|
try {
|
||||||
|
|
@ -43,5 +46,8 @@ while(1) {
|
||||||
|
|
||||||
// and write out the result
|
// and write out the result
|
||||||
ob_end_clean();
|
ob_end_clean();
|
||||||
|
|
||||||
|
// deflate and base64_encode
|
||||||
|
$j = base64_encode(gzdeflate($j));
|
||||||
fwrite(STDOUT,"$j\n");
|
fwrite(STDOUT,"$j\n");
|
||||||
}
|
}
|
||||||
|
|
@ -56,6 +56,6 @@ func (pathbuilder *Pathbuilder) Fetch(flags ingredient.FetcherFlags, info *statu
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info.Pathbuilders, _ = pathbuilder.GetAll(flags.Context, flags.Server)
|
info.Pathbuilders, err = pathbuilder.GetAll(flags.Context, flags.Server)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue