Commit ebf19fb1 authored by Fumitoshi Ukai's avatar Fumitoshi Ukai Committed by Andrew Gerrand

websocket: Implements new version of WebSocket protocol.

This introduces new APIs.
- DialConfig can open client connection using Config, so user can specify protocol version, tls.Config if necessary.

- Message can be used to send/receive text/binary data in a frame.
- JSON can be used to send/receive JSON data in a frame.

R=golang-dev, adg, rsc, m, tarmigan, raul.san, yohcop
CC=golang-dev
https://golang.org/cl/4635084
parent 7a309512
......@@ -5,5 +5,7 @@ GOFILES=\
client.go\
server.go\
websocket.go\
hixie.go\
hybi.go\
include ../../Make.pkg
This diff is collapsed.
This diff is collapsed.
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bufio"
"bytes"
"fmt"
"http"
"os"
"strings"
"testing"
"url"
)
// Test the getChallengeResponse function with values from section
// 5.1 of the specification steps 18, 26, and 43 from
// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
func TestHixie76Challenge(t *testing.T) {
var part1 uint32 = 777007543
var part2 uint32 = 114997259
key3 := []byte{0x47, 0x30, 0x22, 0x2D, 0x5A, 0x3F, 0x47, 0x58}
expected := []byte("0st3Rl&q-2ZU^weu")
response, err := getChallengeResponse(part1, part2, key3)
if err != nil {
t.Errorf("getChallengeResponse: returned error %v", err)
return
}
if !bytes.Equal(expected, response) {
t.Errorf("getChallengeResponse: expected %q got %q", expected, response)
}
}
func TestHixie76ClientHandshake(t *testing.T) {
b := bytes.NewBuffer([]byte{})
bw := bufio.NewWriter(b)
br := bufio.NewReader(strings.NewReader(`HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: http://example.com
Sec-WebSocket-Location: ws://example.com/demo
Sec-WebSocket-Protocol: sample
8jKS'y:G*Co,Wxa-`))
var err os.Error
config := new(Config)
config.Location, err = url.ParseRequest("ws://example.com/demo")
if err != nil {
t.Fatal("location url", err)
}
config.Origin, err = url.ParseRequest("http://example.com")
if err != nil {
t.Fatal("origin url", err)
}
config.Protocol = append(config.Protocol, "sample")
config.Version = ProtocolVersionHixie76
config.handshakeData = map[string]string{
"key1": "4 @1 46546xW%0l 1 5",
"number1": "829309203",
"key2": "12998 5 Y3 1 .P00",
"number2": "259970620",
"key3": "^n:ds[4U",
}
err = hixie76ClientHandshake(config, br, bw)
if err != nil {
t.Errorf("handshake failed: %v", err)
}
req, err := http.ReadRequest(bufio.NewReader(b))
if err != nil {
t.Errorf("read request: %v", err)
}
if req.Method != "GET" {
t.Errorf("request method expected GET, but got %q", req.Method)
}
if req.RawURL != "/demo" {
t.Errorf("request path expected /demo, but got %q", req.RawURL)
}
if req.Proto != "HTTP/1.1" {
t.Errorf("request proto expected HTTP/1.1, but got %q", req.Proto)
}
if req.Host != "example.com" {
t.Errorf("request Host expected example.com, but got %v", req.Host)
}
var expectedHeader = map[string]string{
"Connection": "Upgrade",
"Upgrade": "WebSocket",
"Origin": "http://example.com",
"Sec-Websocket-Key1": config.handshakeData["key1"],
"Sec-Websocket-Key2": config.handshakeData["key2"],
"Sec-WebSocket-Protocol": config.Protocol[0],
}
for k, v := range expectedHeader {
if req.Header.Get(k) != v {
t.Errorf(fmt.Sprintf("%s expected %q but got %q", k, v, req.Header.Get(k)))
}
}
}
func TestHixie76ServerHandshake(t *testing.T) {
config := new(Config)
handshaker := &hixie76ServerHandshaker{Config: config}
br := bufio.NewReader(strings.NewReader(`GET /demo HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
Sec-WebSocket-Protocol: sample
Upgrade: WebSocket
Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
Origin: http://example.com
^n:ds[4U`))
req, err := http.ReadRequest(br)
if err != nil {
t.Fatal("request", err)
}
code, err := handshaker.ReadHandshake(br, req)
if err != nil {
t.Errorf("handshake failed: %v", err)
}
if code != http.StatusSwitchingProtocols {
t.Errorf("status expected %q but got %q", http.StatusSwitchingProtocols, code)
}
b := bytes.NewBuffer([]byte{})
bw := bufio.NewWriter(b)
err = handshaker.AcceptHandshake(bw)
if err != nil {
t.Errorf("handshake response failed: %v", err)
}
expectedResponse := strings.Join([]string{
"HTTP/1.1 101 WebSocket Protocol Handshake",
"Upgrade: WebSocket",
"Connection: Upgrade",
"Sec-WebSocket-Origin: http://example.com",
"Sec-WebSocket-Location: ws://example.com/demo",
"Sec-WebSocket-Protocol: sample",
"", ""}, "\r\n") + "8jKS'y:G*Co,Wxa-"
if b.String() != expectedResponse {
t.Errorf("handshake expected %q but got %q", expectedResponse, b.String())
}
}
func TestHixie76SkipLengthFrame(t *testing.T) {
b := []byte{'\x80', '\x01', 'x', 0, 'h', 'e', 'l', 'l', 'o', '\xff'}
buf := bytes.NewBuffer(b)
br := bufio.NewReader(buf)
bw := bufio.NewWriter(buf)
config := newConfig(t, "/")
ws := newHixieConn(config, bufio.NewReadWriter(br, bw), nil, nil)
msg := make([]byte, 5)
n, err := ws.Read(msg)
if err != nil {
t.Errorf("Read: %v", err)
}
if !bytes.Equal(b[4:9], msg[0:n]) {
t.Errorf("Read: expected %q got %q", b[4:9], msg[0:n])
}
}
func TestHixie76SkipNoUTF8Frame(t *testing.T) {
b := []byte{'\x01', 'n', '\xff', 0, 'h', 'e', 'l', 'l', 'o', '\xff'}
buf := bytes.NewBuffer(b)
br := bufio.NewReader(buf)
bw := bufio.NewWriter(buf)
config := newConfig(t, "/")
ws := newHixieConn(config, bufio.NewReadWriter(br, bw), nil, nil)
msg := make([]byte, 5)
n, err := ws.Read(msg)
if err != nil {
t.Errorf("Read: %v", err)
}
if !bytes.Equal(b[4:9], msg[0:n]) {
t.Errorf("Read: expected %q got %q", b[4:9], msg[0:n])
}
}
func TestHixie76ClosingFrame(t *testing.T) {
b := []byte{0, 'h', 'e', 'l', 'l', 'o', '\xff'}
buf := bytes.NewBuffer(b)
br := bufio.NewReader(buf)
bw := bufio.NewWriter(buf)
config := newConfig(t, "/")
ws := newHixieConn(config, bufio.NewReadWriter(br, bw), nil, nil)
msg := make([]byte, 5)
n, err := ws.Read(msg)
if err != nil {
t.Errorf("read: %v", err)
}
if !bytes.Equal(b[1:6], msg[0:n]) {
t.Errorf("Read: expected %q got %q", b[1:6], msg[0:n])
}
n, err = ws.Read(msg)
if err != os.EOF {
t.Errorf("read: %v", err)
}
}
This diff is collapsed.
This diff is collapsed.
......@@ -5,11 +5,41 @@
package websocket
import (
"bufio"
"fmt"
"http"
"io"
"strings"
"os"
)
func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request) (conn *Conn, err os.Error) {
config := new(Config)
var hs serverHandshaker = &hybiServerHandshaker{Config: config}
code, err := hs.ReadHandshake(buf.Reader, req)
if err != nil {
hs = &hixie76ServerHandshaker{Config: config}
code, err = hs.ReadHandshake(buf.Reader, req)
}
if err != nil {
hs = &hixie75ServerHandshaker{Config: config}
code, err = hs.ReadHandshake(buf.Reader, req)
}
if err != nil {
fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
buf.WriteString("\r\n")
buf.WriteString(err.String())
return
}
config.Protocol = nil
err = hs.AcceptHandshake(buf.Writer)
if err != nil {
return
}
conn = hs.NewServerConn(buf, rwc, req)
return
}
/*
Handler is an interface to a WebSocket.
......@@ -23,7 +53,7 @@ A trivial example server:
"websocket"
)
// Echo the data received on the Web Socket.
// Echo the data received on the WebSocket.
func EchoServer(ws *websocket.Conn) {
io.Copy(ws, ws);
}
......@@ -38,26 +68,8 @@ A trivial example server:
*/
type Handler func(*Conn)
/*
Gets key number from Sec-WebSocket-Key<n>: field as described
in 5.2 Sending the server's opening handshake, 4.
*/
func getKeyNumber(s string) (r uint32) {
// 4. Let /key-number_n/ be the digits (characters in the range
// U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_1/,
// interpreted as a base ten integer, ignoring all other characters
// in /key_n/.
r = 0
for i := 0; i < len(s); i++ {
if s[i] >= '0' && s[i] <= '9' {
r = r*10 + uint32(s[i]) - '0'
}
}
return
}
// ServeHTTP implements the http.Handler interface for a Web Socket
func (f Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
rwc, buf, err := w.(http.Hijacker).Hijack()
if err != nil {
panic("Hijack failed: " + err.String())
......@@ -67,153 +79,12 @@ func (f Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// the client did not send a handshake that matches with protocol
// specification.
defer rwc.Close()
if req.Method != "GET" {
return
}
// HTTP version can be safely ignored.
if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" ||
strings.ToLower(req.Header.Get("Connection")) != "upgrade" {
return
}
// TODO(ukai): check Host
origin := req.Header.Get("Origin")
if origin == "" {
return
}
key1 := req.Header.Get("Sec-Websocket-Key1")
if key1 == "" {
return
}
key2 := req.Header.Get("Sec-Websocket-Key2")
if key2 == "" {
return
}
key3 := make([]byte, 8)
if _, err := io.ReadFull(buf, key3); err != nil {
return
}
var location string
if req.TLS != nil {
location = "wss://" + req.Host + req.URL.RawPath
} else {
location = "ws://" + req.Host + req.URL.RawPath
}
// Step 4. get key number in Sec-WebSocket-Key<n> fields.
keyNumber1 := getKeyNumber(key1)
keyNumber2 := getKeyNumber(key2)
// Step 5. get number of spaces in Sec-WebSocket-Key<n> fields.
space1 := uint32(strings.Count(key1, " "))
space2 := uint32(strings.Count(key2, " "))
if space1 == 0 || space2 == 0 {
return
}
// Step 6. key number must be an integral multiple of spaces.
if keyNumber1%space1 != 0 || keyNumber2%space2 != 0 {
return
}
// Step 7. let part be key number divided by spaces.
part1 := keyNumber1 / space1
part2 := keyNumber2 / space2
// Step 8. let challenge be concatenation of part1, part2 and key3.
// Step 9. get MD5 fingerprint of challenge.
response, err := getChallengeResponse(part1, part2, key3)
if err != nil {
return
}
// Step 10. send response status line.
buf.WriteString("HTTP/1.1 101 WebSocket Protocol Handshake\r\n")
// Step 11. send response headers.
buf.WriteString("Upgrade: WebSocket\r\n")
buf.WriteString("Connection: Upgrade\r\n")
buf.WriteString("Sec-WebSocket-Location: " + location + "\r\n")
buf.WriteString("Sec-WebSocket-Origin: " + origin + "\r\n")
protocol := strings.TrimSpace(req.Header.Get("Sec-Websocket-Protocol"))
if protocol != "" {
buf.WriteString("Sec-WebSocket-Protocol: " + protocol + "\r\n")
}
// Step 12. send CRLF.
buf.WriteString("\r\n")
// Step 13. send response data.
buf.Write(response)
if err := buf.Flush(); err != nil {
return
}
ws := newConn(origin, location, protocol, buf, rwc)
ws.Request = req
f(ws)
}
/*
Draft75Handler is an interface to a WebSocket based on the
(soon obsolete) draft-hixie-thewebsocketprotocol-75.
*/
type Draft75Handler func(*Conn)
// ServeHTTP implements the http.Handler interface for a Web Socket.
func (f Draft75Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if req.Method != "GET" || req.Proto != "HTTP/1.1" {
w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, "Unexpected request")
return
}
if req.Header.Get("Upgrade") != "WebSocket" {
w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, "missing Upgrade: WebSocket header")
return
}
if req.Header.Get("Connection") != "Upgrade" {
w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, "missing Connection: Upgrade header")
return
}
origin := strings.TrimSpace(req.Header.Get("Origin"))
if origin == "" {
w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, "missing Origin header")
return
}
rwc, buf, err := w.(http.Hijacker).Hijack()
conn, err := newServerConn(rwc, buf, req)
if err != nil {
panic("Hijack failed: " + err.String())
return
}
defer rwc.Close()
var location string
if req.TLS != nil {
location = "wss://" + req.Host + req.URL.RawPath
} else {
location = "ws://" + req.Host + req.URL.RawPath
}
// TODO(ukai): verify origin,location,protocol.
buf.WriteString("HTTP/1.1 101 Web Socket Protocol Handshake\r\n")
buf.WriteString("Upgrade: WebSocket\r\n")
buf.WriteString("Connection: Upgrade\r\n")
buf.WriteString("WebSocket-Origin: " + origin + "\r\n")
buf.WriteString("WebSocket-Location: " + location + "\r\n")
protocol := strings.TrimSpace(req.Header.Get("Websocket-Protocol"))
// canonical header key of WebSocket-Protocol.
if protocol != "" {
buf.WriteString("WebSocket-Protocol: " + protocol + "\r\n")
}
buf.WriteString("\r\n")
if err := buf.Flush(); err != nil {
return
if conn == nil {
panic("unepxected nil conn")
}
ws := newConn(origin, location, protocol, buf, rwc)
f(ws)
h(conn)
}
This diff is collapsed.
......@@ -5,7 +5,6 @@
package websocket
import (
"bufio"
"bytes"
"fmt"
"http"
......@@ -13,6 +12,7 @@ import (
"io"
"log"
"net"
"strings"
"sync"
"testing"
"url"
......@@ -23,31 +23,38 @@ var once sync.Once
func echoServer(ws *Conn) { io.Copy(ws, ws) }
type Count struct {
S string
N int
}
func countServer(ws *Conn) {
for {
var count Count
err := JSON.Receive(ws, &count)
if err != nil {
return
}
count.N++
count.S = strings.Repeat(count.S, count.N)
err = JSON.Send(ws, count)
if err != nil {
return
}
}
}
func startServer() {
http.Handle("/echo", Handler(echoServer))
http.Handle("/echoDraft75", Draft75Handler(echoServer))
http.Handle("/count", Handler(countServer))
server := httptest.NewServer(nil)
serverAddr = server.Listener.Addr().String()
log.Print("Test WebSocket server listening on ", serverAddr)
}
// Test the getChallengeResponse function with values from section
// 5.1 of the specification steps 18, 26, and 43 from
// http://www.whatwg.org/specs/web-socket-protocol/
func TestChallenge(t *testing.T) {
var part1 uint32 = 777007543
var part2 uint32 = 114997259
key3 := []byte{0x47, 0x30, 0x22, 0x2D, 0x5A, 0x3F, 0x47, 0x58}
expected := []byte("0st3Rl&q-2ZU^weu")
response, err := getChallengeResponse(part1, part2, key3)
if err != nil {
t.Errorf("getChallengeResponse: returned error %v", err)
return
}
if !bytes.Equal(expected, response) {
t.Errorf("getChallengeResponse: expected %q got %q", expected, response)
}
func newConfig(t *testing.T, path string) *Config {
config, _ := NewConfig(fmt.Sprintf("ws://%s%s", serverAddr, path), "http://localhost")
return config
}
func TestEcho(t *testing.T) {
......@@ -58,19 +65,18 @@ func TestEcho(t *testing.T) {
if err != nil {
t.Fatal("dialing", err)
}
ws, err := newClient("/echo", "localhost", "http://localhost",
"ws://localhost/echo", "", client, handshake)
conn, err := NewClient(newConfig(t, "/echo"), client)
if err != nil {
t.Errorf("WebSocket handshake error: %v", err)
return
}
msg := []byte("hello, world\n")
if _, err := ws.Write(msg); err != nil {
if _, err := conn.Write(msg); err != nil {
t.Errorf("Write: %v", err)
}
var actual_msg = make([]byte, 512)
n, err := ws.Read(actual_msg)
n, err := conn.Read(actual_msg)
if err != nil {
t.Errorf("Read: %v", err)
}
......@@ -78,10 +84,10 @@ func TestEcho(t *testing.T) {
if !bytes.Equal(msg, actual_msg) {
t.Errorf("Echo: expected %q got %q", msg, actual_msg)
}
ws.Close()
conn.Close()
}
func TestEchoDraft75(t *testing.T) {
func TestCount(t *testing.T) {
once.Do(startServer)
// websocket.Dial()
......@@ -89,27 +95,39 @@ func TestEchoDraft75(t *testing.T) {
if err != nil {
t.Fatal("dialing", err)
}
ws, err := newClient("/echoDraft75", "localhost", "http://localhost",
"ws://localhost/echoDraft75", "", client, draft75handshake)
conn, err := NewClient(newConfig(t, "/count"), client)
if err != nil {
t.Errorf("WebSocket handshake: %v", err)
t.Errorf("WebSocket handshake error: %v", err)
return
}
msg := []byte("hello, world\n")
if _, err := ws.Write(msg); err != nil {
t.Errorf("Write: error %v", err)
var count Count
count.S = "hello"
if err := JSON.Send(conn, count); err != nil {
t.Errorf("Write: %v", err)
}
var actual_msg = make([]byte, 512)
n, err := ws.Read(actual_msg)
if err != nil {
t.Errorf("Read: error %v", err)
if err := JSON.Receive(conn, &count); err != nil {
t.Errorf("Read: %v", err)
}
actual_msg = actual_msg[0:n]
if !bytes.Equal(msg, actual_msg) {
t.Errorf("Echo: expected %q got %q", msg, actual_msg)
if count.N != 1 {
t.Errorf("count: expected 1 got %q", 1, count.N)
}
ws.Close()
if count.S != "hello" {
t.Errorf("count: expected %q got %q", "hello", count.S)
}
if err := JSON.Send(conn, count); err != nil {
t.Errorf("Write: %v", err)
}
if err := JSON.Receive(conn, &count); err != nil {
t.Errorf("Read: %v", err)
}
if count.N != 2 {
t.Errorf("count: expected 1 got %q", 2, count.N)
}
if count.S != "hellohello" {
t.Errorf("count: expected %q got %q", "hellohello", count.S)
}
conn.Close()
}
func TestWithQuery(t *testing.T) {
......@@ -120,8 +138,13 @@ func TestWithQuery(t *testing.T) {
t.Fatal("dialing", err)
}
ws, err := newClient("/echo?q=v", "localhost", "http://localhost",
"ws://localhost/echo?q=v", "", client, handshake)
config := newConfig(t, "/echo")
config.Location, err = url.ParseRequest(fmt.Sprintf("ws://%s/echo?q=v", serverAddr))
if err != nil {
t.Fatal("location url", err)
}
ws, err := NewClient(config, client)
if err != nil {
t.Errorf("WebSocket handshake: %v", err)
return
......@@ -137,8 +160,10 @@ func TestWithProtocol(t *testing.T) {
t.Fatal("dialing", err)
}
ws, err := newClient("/echo", "localhost", "http://localhost",
"ws://localhost/echo", "test", client, handshake)
config := newConfig(t, "/echo")
config.Protocol = append(config.Protocol, "test")
ws, err := NewClient(config, client)
if err != nil {
t.Errorf("WebSocket handshake: %v", err)
return
......@@ -167,27 +192,15 @@ func TestHTTP(t *testing.T) {
}
}
func TestHTTPDraft75(t *testing.T) {
once.Do(startServer)
r, err := http.Get(fmt.Sprintf("http://%s/echoDraft75", serverAddr))
if err != nil {
t.Errorf("Get: error %#v", err)
return
}
if r.StatusCode != http.StatusBadRequest {
t.Errorf("Get: got status %d", r.StatusCode)
}
}
func TestTrailingSpaces(t *testing.T) {
// http://code.google.com/p/go/issues/detail?id=955
// The last runs of this create keys with trailing spaces that should not be
// generated by the client.
once.Do(startServer)
config := newConfig(t, "/echo")
for i := 0; i < 30; i++ {
// body
ws, err := Dial(fmt.Sprintf("ws://%s/echo", serverAddr), "", "http://localhost/")
ws, err := DialConfig(config)
if err != nil {
t.Error("Dial failed:", err.String())
break
......@@ -206,19 +219,18 @@ func TestSmallBuffer(t *testing.T) {
if err != nil {
t.Fatal("dialing", err)
}
ws, err := newClient("/echo", "localhost", "http://localhost",
"ws://localhost/echo", "", client, handshake)
conn, err := NewClient(newConfig(t, "/echo"), client)
if err != nil {
t.Errorf("WebSocket handshake error: %v", err)
return
}
msg := []byte("hello, world\n")
if _, err := ws.Write(msg); err != nil {
if _, err := conn.Write(msg); err != nil {
t.Errorf("Write: %v", err)
}
var small_msg = make([]byte, 8)
n, err := ws.Read(small_msg)
n, err := conn.Read(small_msg)
if err != nil {
t.Errorf("Read: %v", err)
}
......@@ -226,7 +238,7 @@ func TestSmallBuffer(t *testing.T) {
t.Errorf("Echo: expected %q got %q", msg[:len(small_msg)], small_msg)
}
var second_msg = make([]byte, len(msg))
n, err = ws.Read(second_msg)
n, err = conn.Read(second_msg)
if err != nil {
t.Errorf("Read: %v", err)
}
......@@ -234,38 +246,5 @@ func TestSmallBuffer(t *testing.T) {
if !bytes.Equal(msg[len(small_msg):], second_msg) {
t.Errorf("Echo: expected %q got %q", msg[len(small_msg):], second_msg)
}
ws.Close()
}
func testSkipLengthFrame(t *testing.T) {
b := []byte{'\x80', '\x01', 'x', 0, 'h', 'e', 'l', 'l', 'o', '\xff'}
buf := bytes.NewBuffer(b)
br := bufio.NewReader(buf)
bw := bufio.NewWriter(buf)
ws := newConn("http://127.0.0.1/", "ws://127.0.0.1/", "", bufio.NewReadWriter(br, bw), nil)
msg := make([]byte, 5)
n, err := ws.Read(msg)
if err != nil {
t.Errorf("Read: %v", err)
}
if !bytes.Equal(b[4:8], msg[0:n]) {
t.Errorf("Read: expected %q got %q", msg[4:8], msg[0:n])
}
}
func testSkipNoUTF8Frame(t *testing.T) {
b := []byte{'\x01', 'n', '\xff', 0, 'h', 'e', 'l', 'l', 'o', '\xff'}
buf := bytes.NewBuffer(b)
br := bufio.NewReader(buf)
bw := bufio.NewWriter(buf)
ws := newConn("http://127.0.0.1/", "ws://127.0.0.1/", "", bufio.NewReadWriter(br, bw), nil)
msg := make([]byte, 5)
n, err := ws.Read(msg)
if err != nil {
t.Errorf("Read: %v", err)
}
if !bytes.Equal(b[4:8], msg[0:n]) {
t.Errorf("Read: expected %q got %q", msg[4:8], msg[0:n])
}
conn.Close()
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment