net: make net.socket.write, net.socket.read_line more robust

pull/2804/head
Delyan Angelov 2019-11-18 23:13:14 +02:00 committed by Alexander Medvednikov
parent 1ccd1979a4
commit c73f34cc5f
5 changed files with 103 additions and 62 deletions

View File

@ -9,3 +9,7 @@ module net
fn error_code() int {
return C.errno
}
pub const (
MSG_NOSIGNAL = 0x4000
)

View File

@ -32,3 +32,6 @@ fn error_code() int {
return C.WSAGetLastError()
}
pub const (
MSG_NOSIGNAL = 0
)

View File

@ -188,7 +188,7 @@ pub fn dial(address string, port int) ?Socket {
// send string data to socket
pub fn (s Socket) send(buf byteptr, len int) ?int {
res := int( C.send(s.sockfd, buf, len, 0) )
res := int( C.send(s.sockfd, buf, len, MSG_NOSIGNAL) )
if res < 0 {
return error('net.send: failed with $res')
}
@ -241,52 +241,59 @@ pub fn (s Socket) close() ?int {
return 0
}
const (
MAX_READ = 400
pub const (
CRLF = '\r\n'
MAX_READ = 400
MSG_PEEK = 0x02
)
pub fn (s Socket) write(str string) {
line := '$str\r\n'
C.send(s.sockfd, line.str, line.len, 0)
// write - write a string with CRLF after it over the socket s
pub fn (s Socket) write(str string) ?int {
line := '$str$CRLF'
res := int( C.send(s.sockfd, line.str, line.len, MSG_NOSIGNAL) )
if res < 0 { return error('net.write: failed with $res') }
return res
}
// read_line - retrieves a line from the socket s (i.e. a string ended with \n)
pub fn (s Socket) read_line() string {
mut res := ''
for {
$if debug {
println('.')
}
mut buf := malloc(MAX_READ)
n := int(C.recv(s.sockfd, buf, MAX_READ-1, 0))
$if debug {
println('numbytes=$n')
mut buf := [MAX_READ]byte // where C.recv will store the network data
mut res := '' // The final result, including the ending \n.
for {
mut line := '' // The current line. Can be a partial without \n in it.
n := int(C.recv(s.sockfd, buf, MAX_READ-1, MSG_PEEK))
if n == -1 { return res }
if n == 0 { return res }
buf[n] = `\0`
mut eol_idx := -1
for i := 0; i < n; i++ {
if int(buf[i]) == `\n` {
eol_idx = i
// Ensure that tos_clone(buf) later,
// will return *only* the first line (including \n),
// and ignore the rest
buf[i+1] = `\0`
break
}
if n == -1 {
$if debug {
println('recv failed')
}
// TODO
return ''
}
if n == 0 {
break
}
// println('resp len=$numbytes')
buf[n] = `\0`
// C.printf('!!buf= "%s" n=%d\n', buf,n)
line := string(buf)
res += line
// Reached a newline. That's an end of an IRC message
// TODO dont need ends_with check ?
if line.ends_with('\n') || n < MAX_READ - 1 {
// println('NL')
break
}
if line.ends_with('\r\n') {
// println('RNL')
break
}
}
return res
}
line = tos_clone(buf)
if eol_idx > 0 {
// At this point, we are sure that recv returned valid data,
// that contains *at least* one line.
// Ensure that the block till the first \n (including it)
// is removed from the socket's receive queue, so that it does
// not get read again.
C.recv(s.sockfd, buf, eol_idx+1, 0)
res += line
break
}
// recv returned a buffer without \n in it .
C.recv(s.sockfd, buf, n, 0)
res += line
res += CRLF
break
}
return res
}
pub fn (s Socket) get_port() int {

View File

@ -1,21 +1,23 @@
import net
fn test_socket() {
server := net.listen(0) or {
panic(err)
}
fn setup() (net.Socket, net.Socket, net.Socket) {
server := net.listen(0) or { panic(err) }
server_port := server.get_port()
client := net.dial('127.0.0.1', server_port) or {
panic(err)
}
socket := server.accept() or {
panic(err)
}
client := net.dial('127.0.0.1', server_port) or { panic(err) }
socket := server.accept() or { panic(err) }
return server, client, socket
}
fn cleanup(server &net.Socket, client &net.Socket, socket &net.Socket) {
server.close() or {}
client.close() or {}
socket.close() or {}
}
fn test_socket() {
server, client, socket := setup()
message := 'Hello World'
socket.send(message.str, message.len) or {
assert false
}
socket.send(message.str, message.len) or { assert false }
$if debug { println('message send: $message') }
$if debug { println('send socket: $socket.sockfd') }
@ -23,10 +25,33 @@ fn test_socket() {
received := tos(bytes, blen)
$if debug { println('message received: $received') }
$if debug { println('client: $client.sockfd') }
assert message == received
server.close() or {}
client.close() or {}
socket.close() or {}
cleanup(server, client, socket)
}
fn test_socket_write() {
server, client, socket := setup()
message1 := 'a message 1'
socket.write(message1) or { assert false }
line1 := client.read_line()
assert line1 != message1
assert line1.trim_space() == message1
cleanup(server, client, socket)
}
fn test_socket_write_fail_without_panic() {
server, client, socket := setup()
message2 := 'a message 2'
// ensure that socket.write (i.e. done on the server side)
// continues to work, even when the client side has been disconnected
// this test is important for a stable long standing server
client.close() or {}
for i:=0; i<3; i++{
socket.write(message2) or {
println('write to a socket without a recipient should produce an option fail: $err | $message2')
assert true
}
}
cleanup(server, client, socket)
}

View File

@ -42,5 +42,7 @@ pub fn (b mut Builder) str() string {
}
pub fn (b mut Builder) free() {
//free(b.buf.data)
unsafe{ free(b.buf.data) }
b.buf = make(0, 1, 1)
b.len = 0
}