Чтение данных из syscall.RawConn golang
Есть tcp сервер. Мне нужно получить данные из syscall.RawConn. Я пытаюсь это сделать таким образом:
for {
for _, conn := range tcpConns {
rawConn, err := conn.SyscallConn()
if err != nil {
fmt.Printf("get raw conn error: %s\n", err)
}
rawConn.Read(readConn)
}
}
func readConn(fd uintptr) bool {
f := os.NewFile(fd, "file")
b, err := ioutil.ReadAll(f)
if err != nil {
fmt.Printf("read file error: %s\n", err)
return false
} else {
fmt.Printf("New data: %s\n", string(b))
return true
}
}
при выполнении, в терминале я наблюдаю:
read file error: read file: resource temporarily unavailable
т.е. эту ошибку возвращает ioutil.ReadAll(f). Далее функция readConn возвращает false и выполнение программы блочится. Т.е. код не идёт дальше rawConn.Read(readConn), хотя моя функция была выполнена и вернула результат. В документации говорится что в случае, когда readConn возвращает false будет предпринято ещё несколько попыток прочитать данные, когда это станет возможно. Но ничего не происходит и не понятно что значит "когда это станет возможно".
Я пробовал возвращать true в любом случае в функции readConn но тогда выполнение блочится на втором круге бесконечного цикла на строке ioutil.ReadAll(f). Что я должен сделать чтобы получить данные?
Я знаю что можно не получать syscall.RawConn и читать из net.TCPConn, но мне не нравится что там нужно создавать по горутине для каждого соединения и оно жрёт много памяти, поэтому я хочу сделать 1 бесконечный цикл который будет читать данные из всех соединений без горутин
Ответы (1 шт):
Go создаёт сокеты в неблокирующем режиме. Если данных нет, то системный вызов read возвращает ошибку EAGAIN, которую Go снабжает тестовым сообщением resource temporarily unavailable
Если эту ошибку проигнорировать, то rawConn работает.
NB. Если вы пользуетесь RawConn.Read, то вам не нужно вызывать ReadAll -- RawConn.Read блокируется, то есть будет вызывать вашу функцию readConn до тех пор, пока функция не вернёт true.
Read invokes f on the underlying connection's file descriptor or handle; f is expected to try to read from the file descriptor. If f returns true, Read returns. Otherwise Read blocks waiting for the connection to be ready for reading and tries again repeatedly. The file descriptor is guaranteed to remain valid while f executes but not after f returns.
Пример использования RawConn.Read. Программа читает из файлового дескриптора сокета системным вызовом syscall.Read.
package main
import (
"fmt"
"net"
"os"
"syscall"
)
func main() {
addr, err := net.ResolveTCPAddr("tcp", "localhost:54321")
if err != nil {
fmt.Println("Address resolution failed:", err.Error())
os.Exit(1)
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
fmt.Println("Error listening:", err.Error())
os.Exit(1)
}
// Close the listener when the application closes.
defer l.Close()
fmt.Println("Listening on ", addr)
conn, err := l.AcceptTCP()
if err != nil {
fmt.Println("Accept failed: ", err)
}
defer conn.Close()
rawConn, err := conn.SyscallConn()
if err != nil {
fmt.Printf("get raw conn error: %s\n", err)
}
rawConn.Read(readConnWithSyscall)
}
func readConnWithSyscall(fd uintptr) (done bool) {
fmt.Println("readConn: file descriptor:", fd)
byteBuf := make([]byte, 1024)
n, err := syscall.Read(int(fd), byteBuf)
if err != nil {
if err == syscall.EAGAIN {
println("readconn: EAGAIN")
// Try once more
return false
} else {
println("read: error ", err)
return true
}
}
if n > 0 {
println("readconn: read", n, "bytes: ", string(byteBuf[:n]))
// More left to read
return false
} else {
// read returned 0 == EOF
println("readconn: finished")
return true
}
}
Тестовый запуск echo "Test" | nc -N -w 1 -v localhost 54321
Серверная программа вывела:
Listening on 127.0.0.1:54321
readConn: file descriptor: 7
readconn: EAGAIN
readConn: file descriptor: 7
readconn: read 5 bytes: Test
readConn: file descriptor: 7
readconn: finished
UPDATE
Асинхронный читатель из сокета. Этот вариант не блокирует RawConn.Read.
package main
import (
"fmt"
"net"
"os"
"syscall"
)
type asyncReader struct {
data []byte
done bool
}
func (r *asyncReader) Read(fd uintptr) (done bool) {
byteBuf := make([]byte, 1024)
done = true
n, err := syscall.Read(int(fd), byteBuf)
if err != nil {
if err == syscall.EAGAIN {
println("asyncReader: EAGAIN")
} else {
println("asyncReader: error ", err)
r.done = true
}
} else if n > 0 {
println("asyncReader: read", n, "bytes: ", string(byteBuf[:n]))
r.data = append(r.data, byteBuf[:n]...)
} else {
// read returned 0 == EOF
println("asyncReader: finished")
r.done = true
}
return
}
func main() {
addr, err := net.ResolveTCPAddr("tcp", "localhost:54321")
if err != nil {
fmt.Println("Address resolution failed:", err.Error())
os.Exit(1)
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
fmt.Println("Error listening:", err.Error())
os.Exit(1)
}
// Close the listener when the application closes.
defer l.Close()
fmt.Println("Listening on ", addr)
conn, err := l.AcceptTCP()
if err != nil {
fmt.Println("Accept failed: ", err)
}
defer conn.Close()
rawConn, err := conn.SyscallConn()
if err != nil {
fmt.Printf("get raw conn error: %s\n", err)
}
// rawConn.Read(readConnWithSyscall)
r := &asyncReader{done: false, data: make([]byte, 0)}
for !r.done {
rawConn.Read(r.Read)
println("main: data size:", len(r.data))
}
println("main: received:", string(r.data))
}
Пример - клиент посылает одну строку, потом ждёт секунду и посылает ещё строку:
{ echo "Test 1"; sleep 1; echo "Test 2"; } | nc -N -w 1 -v localhost 54321
Сервер:
Listening on 127.0.0.1:54321
asyncReader: EAGAIN
main: data size: 0
asyncReader: EAGAIN
main: data size: 0
asyncReader: EAGAIN
main: data size: 0
asyncReader: EAGAIN
main: data size: 0
asyncReader: EAGAIN
main: data size: 0
asyncReader: read 7 bytes: Test 1
main: data size: 7
asyncReader: EAGAIN
main: data size: 7
.
.
.
asyncReader: read 7 bytes: Test 2
main: data size: 14
.
.
.
asyncReader: finished
main: data size: 14
main: received: Test 1
Test 2
В троеточиях спрятались 90 тысяч (!) вызовов асинхронного читателя.
Основной недостаток - этот вариант грузит CPU на 100%, так как постоянно дёргает сокет.
Для по-настоящему асинхронной обработки нескольких сокетов без избыточных запросов нужно пользоваться syscall.Select или syscall.EpollWait