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 (
|
||||
errInit = "Server initialization failed"
|
||||
errClosed = "Server closed"
|
||||
errSend = "Failed to encode request"
|
||||
errReceive = "Failed to decode response"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue