mirror of
https://github.com/vlang/v.git
synced 2023-08-10 21:13:21 +03:00
picoev, picohttparser: reimplement in V (#18506)
This commit is contained in:
@ -1,53 +1,31 @@
|
||||
// Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module picoev
|
||||
|
||||
import net
|
||||
import picohttpparser
|
||||
import time
|
||||
|
||||
#include <errno.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <signal.h>
|
||||
#flag -I @VEXEROOT/thirdparty/picoev
|
||||
#flag @VEXEROOT/thirdparty/picoev/picoev.o
|
||||
#include "src/picoev.h"
|
||||
pub const (
|
||||
max_fds = 1024
|
||||
max_queue = 4096
|
||||
|
||||
[typedef]
|
||||
struct C.picoev_loop {}
|
||||
|
||||
fn C.picoev_del(&C.picoev_loop, int) int
|
||||
|
||||
fn C.picoev_set_timeout(&C.picoev_loop, int, int)
|
||||
|
||||
// fn C.picoev_handler(loop &C.picoev_loop, fd int, revents int, cb_arg voidptr)
|
||||
// TODO: (sponge) update to C.picoev_handler with C type def update
|
||||
type PicoevHandler = fn (loop &C.picoev_loop, fd int, revents int, context voidptr)
|
||||
|
||||
fn C.picoev_add(&C.picoev_loop, int, int, int, &PicoevHandler, voidptr) int
|
||||
|
||||
fn C.picoev_init(int) int
|
||||
|
||||
fn C.picoev_create_loop(int) &C.picoev_loop
|
||||
|
||||
fn C.picoev_loop_once(&C.picoev_loop, int) int
|
||||
|
||||
fn C.picoev_destroy_loop(&C.picoev_loop) int
|
||||
|
||||
fn C.picoev_deinit() int
|
||||
|
||||
const (
|
||||
max_fds = 1024
|
||||
max_timeout = 10
|
||||
// events
|
||||
picoev_read = 1
|
||||
picoev_write = 2
|
||||
picoev_timeout = 4
|
||||
picoev_add = 0x40000000
|
||||
picoev_del = 0x20000000
|
||||
picoev_readwrite = 3 // 1 xor 2
|
||||
)
|
||||
|
||||
enum Event {
|
||||
read = C.PICOEV_READ
|
||||
write = C.PICOEV_WRITE
|
||||
timeout = C.PICOEV_TIMEOUT
|
||||
add = C.PICOEV_ADD
|
||||
del = C.PICOEV_DEL
|
||||
readwrite = C.PICOEV_READWRITE
|
||||
// Target is a data representation of everything that needs to be associated with a single
|
||||
// file descriptor (connection)
|
||||
pub struct Target {
|
||||
pub mut:
|
||||
fd int
|
||||
loop_id int = -1
|
||||
events u32
|
||||
cb fn (int, int, voidptr)
|
||||
// used internally by the kqueue implementation
|
||||
backend int
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
@ -62,137 +40,230 @@ pub:
|
||||
max_write int = 8192
|
||||
}
|
||||
|
||||
struct Picoev {
|
||||
loop &C.picoev_loop = unsafe { nil }
|
||||
cb fn (voidptr, picohttpparser.Request, mut picohttpparser.Response)
|
||||
err_cb fn (voidptr, picohttpparser.Request, mut picohttpparser.Response, IError)
|
||||
user_data voidptr
|
||||
[heap]
|
||||
pub struct Picoev {
|
||||
cb fn (voidptr, picohttpparser.Request, mut picohttpparser.Response)
|
||||
err_cb fn (voidptr, picohttpparser.Request, mut picohttpparser.Response, IError) = default_err_cb
|
||||
user_data voidptr = unsafe { nil }
|
||||
|
||||
timeout_secs int
|
||||
max_headers int
|
||||
max_read int
|
||||
max_write int
|
||||
max_headers int = 100
|
||||
max_read int = 4096
|
||||
max_write int = 8192
|
||||
mut:
|
||||
date &u8 = unsafe { nil }
|
||||
buf &u8 = unsafe { nil }
|
||||
idx [1024]int
|
||||
out &u8 = unsafe { nil }
|
||||
loop &LoopType = unsafe { nil }
|
||||
file_descriptors [max_fds]&Target
|
||||
timeouts map[int]i64
|
||||
num_loops int
|
||||
|
||||
buf &u8 = unsafe { nil }
|
||||
idx [1024]int
|
||||
out &u8 = unsafe { nil }
|
||||
|
||||
date string
|
||||
}
|
||||
|
||||
[inline]
|
||||
fn setup_sock(fd int) ! {
|
||||
flag := 1
|
||||
if C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_NODELAY, &flag, sizeof(int)) < 0 {
|
||||
return error('setup_sock.setup_sock failed')
|
||||
// init fills the `file_descriptors` array
|
||||
pub fn (mut pv Picoev) init() {
|
||||
assert picoev.max_fds > 0
|
||||
|
||||
pv.num_loops = 0
|
||||
|
||||
for i in 0 .. picoev.max_fds {
|
||||
pv.file_descriptors[i] = &Target{}
|
||||
}
|
||||
$if freebsd {
|
||||
if C.fcntl(fd, C.F_SETFL, C.SOCK_NONBLOCK) != 0 {
|
||||
return error('fcntl failed')
|
||||
}
|
||||
} $else {
|
||||
if C.fcntl(fd, C.F_SETFL, C.O_NONBLOCK) != 0 {
|
||||
return error('fcntl failed')
|
||||
}
|
||||
|
||||
// add adds a file descriptor to the loop
|
||||
[direct_array_access]
|
||||
pub fn (mut pv Picoev) add(fd int, events int, timeout int, cb voidptr) int {
|
||||
assert fd < picoev.max_fds
|
||||
|
||||
mut target := pv.file_descriptors[fd]
|
||||
target.fd = fd
|
||||
target.cb = cb
|
||||
target.loop_id = pv.loop.id
|
||||
target.events = 0
|
||||
|
||||
if pv.update_events(fd, events | picoev.picoev_add) != 0 {
|
||||
pv.del(fd)
|
||||
return -1
|
||||
}
|
||||
|
||||
// update timeout
|
||||
pv.set_timeout(fd, timeout)
|
||||
return 0
|
||||
}
|
||||
|
||||
// del removes a file descriptor from the loop
|
||||
[direct_array_access]
|
||||
fn (mut pv Picoev) del(fd int) int {
|
||||
assert fd < picoev.max_fds
|
||||
mut target := pv.file_descriptors[fd]
|
||||
|
||||
$if trace_fd ? {
|
||||
eprintln('delete ${fd}')
|
||||
}
|
||||
|
||||
if pv.update_events(fd, picoev.picoev_del) != 0 {
|
||||
target.loop_id = -1
|
||||
target.fd = 0
|
||||
return -1
|
||||
}
|
||||
|
||||
pv.set_timeout(fd, 0)
|
||||
target.loop_id = -1
|
||||
target.fd = 0
|
||||
return 0
|
||||
}
|
||||
|
||||
fn (mut pv Picoev) loop_once(max_wait int) int {
|
||||
pv.loop.now = get_time()
|
||||
|
||||
if pv.poll_once(max_wait) != 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
if max_wait != 0 {
|
||||
pv.loop.now = get_time()
|
||||
}
|
||||
|
||||
pv.handle_timeout()
|
||||
return 0
|
||||
}
|
||||
|
||||
// set_timeout sets the timeout in seconds for a file descriptor. If a timeout occurs
|
||||
// the file descriptors target callback is called with a timeout event
|
||||
[direct_array_access; inline]
|
||||
fn (mut pv Picoev) set_timeout(fd int, secs int) {
|
||||
assert fd < picoev.max_fds
|
||||
if secs != 0 {
|
||||
pv.timeouts[fd] = pv.loop.now + secs
|
||||
} else {
|
||||
pv.timeouts.delete(fd)
|
||||
}
|
||||
}
|
||||
|
||||
// handle_timeout loops over all file descriptors and removes them from the loop
|
||||
// if they are timed out. Also the file descriptors target callback is called with a
|
||||
// timeout event
|
||||
[direct_array_access; inline]
|
||||
fn (mut pv Picoev) handle_timeout() {
|
||||
for fd, timeout in pv.timeouts {
|
||||
if timeout <= pv.loop.now {
|
||||
target := pv.file_descriptors[fd]
|
||||
assert target.loop_id == pv.loop.id
|
||||
|
||||
pv.timeouts.delete(fd)
|
||||
unsafe { target.cb(fd, picoev.picoev_timeout, &pv) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[inline]
|
||||
fn close_conn(loop &C.picoev_loop, fd int) {
|
||||
C.picoev_del(voidptr(loop), fd)
|
||||
C.close(fd)
|
||||
}
|
||||
|
||||
[inline]
|
||||
fn req_read(fd int, b &u8, max_len int, idx int) int {
|
||||
unsafe {
|
||||
return C.read(fd, b + idx, max_len - idx)
|
||||
}
|
||||
}
|
||||
|
||||
fn rw_callback(loop &C.picoev_loop, fd int, events int, context voidptr) {
|
||||
mut p := unsafe { &Picoev(context) }
|
||||
defer {
|
||||
p.idx[fd] = 0
|
||||
}
|
||||
if (events & int(Event.timeout)) != 0 {
|
||||
close_conn(loop, fd)
|
||||
// accept_callback accepts a new connection from `listen_fd` and adds it to the loop
|
||||
fn accept_callback(listen_fd int, events int, cb_arg voidptr) {
|
||||
mut pv := unsafe { &Picoev(cb_arg) }
|
||||
newfd := accept(listen_fd)
|
||||
if newfd >= picoev.max_fds {
|
||||
// should never happen
|
||||
close_socket(newfd)
|
||||
return
|
||||
} else if (events & int(Event.read)) != 0 {
|
||||
C.picoev_set_timeout(voidptr(loop), fd, p.timeout_secs)
|
||||
}
|
||||
|
||||
// Request init
|
||||
mut buf := p.buf
|
||||
$if trace_fd ? {
|
||||
eprintln('accept ${newfd}')
|
||||
}
|
||||
|
||||
if newfd != -1 {
|
||||
setup_sock(newfd) or {
|
||||
eprintln('setup_sock failed, fd: ${newfd}, listen_fd: ${listen_fd}, err: ${err.code()}')
|
||||
pv.err_cb(pv.user_data, picohttpparser.Request{}, mut &picohttpparser.Response{},
|
||||
err)
|
||||
return
|
||||
}
|
||||
pv.add(newfd, picoev.picoev_read, pv.timeout_secs, raw_callback)
|
||||
}
|
||||
}
|
||||
|
||||
// close_conn closes the socket `fd` and removes it from the loop
|
||||
[inline]
|
||||
pub fn (mut pv Picoev) close_conn(fd int) {
|
||||
pv.del(fd)
|
||||
close_socket(fd)
|
||||
}
|
||||
|
||||
[direct_array_access]
|
||||
fn raw_callback(fd int, events int, context voidptr) {
|
||||
mut pv := unsafe { &Picoev(context) }
|
||||
defer {
|
||||
pv.idx[fd] = 0
|
||||
}
|
||||
|
||||
if events & picoev.picoev_timeout != 0 {
|
||||
$if trace_fd ? {
|
||||
eprintln('timeout ${fd}')
|
||||
}
|
||||
pv.close_conn(fd)
|
||||
return
|
||||
} else if events & picoev.picoev_read != 0 {
|
||||
pv.set_timeout(fd, pv.timeout_secs)
|
||||
|
||||
mut buf := pv.buf
|
||||
unsafe {
|
||||
buf += fd * p.max_read // pointer magic
|
||||
buf += fd * pv.max_read // pointer magic
|
||||
}
|
||||
mut req := picohttpparser.Request{}
|
||||
|
||||
// Response init
|
||||
mut out := p.out
|
||||
mut out := pv.out
|
||||
unsafe {
|
||||
out += fd * p.max_write // pointer magic
|
||||
out += fd * pv.max_write // pointer magic
|
||||
}
|
||||
mut res := picohttpparser.Response{
|
||||
fd: fd
|
||||
date: p.date
|
||||
buf_start: out
|
||||
buf: out
|
||||
date: pv.date.str
|
||||
}
|
||||
|
||||
for {
|
||||
// Request parsing loop
|
||||
r := req_read(fd, buf, p.max_read, p.idx[fd]) // Get data from socket
|
||||
r := req_read(fd, buf, pv.max_read, pv.idx[fd]) // Get data from socket
|
||||
if r == 0 {
|
||||
// connection closed by peer
|
||||
close_conn(loop, fd)
|
||||
pv.close_conn(fd)
|
||||
return
|
||||
} else if r == -1 {
|
||||
// error
|
||||
if C.errno == C.EAGAIN {
|
||||
// try again later
|
||||
return
|
||||
}
|
||||
if C.errno == C.EWOULDBLOCK {
|
||||
// try again later
|
||||
if fatal_socket_error(fd) == false {
|
||||
return
|
||||
}
|
||||
|
||||
// fatal error
|
||||
close_conn(loop, fd)
|
||||
pv.close_conn(fd)
|
||||
return
|
||||
}
|
||||
p.idx[fd] += r
|
||||
pv.idx[fd] += r
|
||||
|
||||
mut s := unsafe { tos(buf, p.idx[fd]) }
|
||||
pret := req.parse_request(s, p.max_headers) // Parse request via picohttpparser
|
||||
mut s := unsafe { tos(buf, pv.idx[fd]) }
|
||||
pret := req.parse_request(s) or {
|
||||
// Parse error
|
||||
pv.err_cb(pv.user_data, req, mut &res, err)
|
||||
return
|
||||
}
|
||||
if pret > 0 { // Success
|
||||
break
|
||||
} else if pret == -1 { // Parse error
|
||||
p.err_cb(p.user_data, req, mut &res, error('ParseError'))
|
||||
return
|
||||
}
|
||||
|
||||
assert pret == -2
|
||||
// request is incomplete, continue the loop
|
||||
if p.idx[fd] == sizeof(buf) {
|
||||
p.err_cb(p.user_data, req, mut &res, error('RequestIsTooLongError'))
|
||||
if pv.idx[fd] == sizeof(buf) {
|
||||
pv.err_cb(pv.user_data, req, mut &res, error('RequestIsTooLongError'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Callback (should call .end() itself)
|
||||
p.cb(p.user_data, req, mut &res)
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_callback(loop &C.picoev_loop, fd int, events int, cb_arg voidptr) {
|
||||
mut p := unsafe { &Picoev(cb_arg) }
|
||||
newfd := C.accept(fd, 0, 0)
|
||||
if newfd != -1 {
|
||||
setup_sock(newfd) or {
|
||||
p.err_cb(p.user_data, picohttpparser.Request{}, mut &picohttpparser.Response{},
|
||||
err)
|
||||
}
|
||||
C.picoev_add(voidptr(loop), newfd, int(Event.read), p.timeout_secs, rw_callback,
|
||||
cb_arg)
|
||||
pv.cb(pv.user_data, req, mut &res)
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,42 +272,12 @@ fn default_err_cb(data voidptr, req picohttpparser.Request, mut res picohttppars
|
||||
res.end()
|
||||
}
|
||||
|
||||
// new creates a `Picoev` struct and initializes the main loop
|
||||
pub fn new(config Config) &Picoev {
|
||||
fd := C.socket(net.AddrFamily.ip, net.SocketType.tcp, 0)
|
||||
assert fd != -1
|
||||
listen_fd := listen(config)
|
||||
|
||||
// Setting flags for socket
|
||||
flag := 1
|
||||
assert C.setsockopt(fd, C.SOL_SOCKET, C.SO_REUSEADDR, &flag, sizeof(int)) == 0
|
||||
$if linux {
|
||||
assert C.setsockopt(fd, C.SOL_SOCKET, C.SO_REUSEPORT, &flag, sizeof(int)) == 0
|
||||
assert C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_QUICKACK, &flag, sizeof(int)) == 0
|
||||
timeout := 10
|
||||
assert C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_DEFER_ACCEPT, &timeout, sizeof(int)) == 0
|
||||
queue_len := 4096
|
||||
assert C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_FASTOPEN, &queue_len, sizeof(int)) == 0
|
||||
}
|
||||
|
||||
// Setting addr
|
||||
mut addr := C.sockaddr_in{
|
||||
sin_family: u8(C.AF_INET)
|
||||
sin_port: C.htons(config.port)
|
||||
sin_addr: C.htonl(C.INADDR_ANY)
|
||||
}
|
||||
size := sizeof(C.sockaddr_in)
|
||||
bind_res := C.bind(fd, voidptr(unsafe { &net.Addr(&addr) }), size)
|
||||
assert bind_res == 0
|
||||
listen_res := C.listen(fd, C.SOMAXCONN)
|
||||
assert listen_res == 0
|
||||
setup_sock(fd) or {
|
||||
config.err_cb(config.user_data, picohttpparser.Request{}, mut &picohttpparser.Response{},
|
||||
err)
|
||||
}
|
||||
|
||||
C.picoev_init(picoev.max_fds)
|
||||
loop := C.picoev_create_loop(picoev.max_timeout)
|
||||
mut pv := &Picoev{
|
||||
loop: loop
|
||||
num_loops: 1
|
||||
cb: config.cb
|
||||
err_cb: config.err_cb
|
||||
user_data: config.user_data
|
||||
@ -244,25 +285,45 @@ pub fn new(config Config) &Picoev {
|
||||
max_headers: config.max_headers
|
||||
max_read: config.max_read
|
||||
max_write: config.max_write
|
||||
date: &u8(C.get_date())
|
||||
buf: unsafe { malloc_noscan(picoev.max_fds * config.max_read + 1) }
|
||||
out: unsafe { malloc_noscan(picoev.max_fds * config.max_write + 1) }
|
||||
}
|
||||
|
||||
C.picoev_add(voidptr(loop), fd, int(Event.read), 0, accept_callback, pv)
|
||||
spawn update_date(mut pv)
|
||||
// epoll for linux
|
||||
// kqueue for macos and bsd
|
||||
// select for windows and others
|
||||
$if linux {
|
||||
pv.loop = create_epoll_loop(0) or { panic(err) }
|
||||
} $else $if freebsd || macos {
|
||||
pv.loop = create_kqueue_loop(0) or { panic(err) }
|
||||
} $else {
|
||||
pv.loop = create_select_loop(0) or { panic(err) }
|
||||
}
|
||||
|
||||
pv.init()
|
||||
|
||||
pv.add(listen_fd, picoev.picoev_read, 0, accept_callback)
|
||||
return pv
|
||||
}
|
||||
|
||||
pub fn (p Picoev) serve() {
|
||||
// serve starts the Picoev server
|
||||
pub fn (mut pv Picoev) serve() {
|
||||
spawn update_date(mut pv)
|
||||
|
||||
for {
|
||||
C.picoev_loop_once(p.loop, 1)
|
||||
pv.loop_once(1)
|
||||
}
|
||||
}
|
||||
|
||||
fn update_date(mut p Picoev) {
|
||||
// update_date updates `date` on `pv` every second.
|
||||
fn update_date(mut pv Picoev) {
|
||||
for {
|
||||
p.date = &u8(C.get_date())
|
||||
C.usleep(1000000)
|
||||
// get GMT (UTC) time for the HTTP Date header
|
||||
gmt := time.utc()
|
||||
mut date := gmt.strftime('---, %d --- %Y %H:%M:%S GMT')
|
||||
date = date.replace_once('---', gmt.weekday_str())
|
||||
date = date.replace_once('---', gmt.smonth())
|
||||
pv.date = date
|
||||
time.sleep(time.second)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user