diff --git a/v/config.yaml b/v/config.yaml index ca11d8a6228..7d75d5c9649 100644 --- a/v/config.yaml +++ b/v/config.yaml @@ -1,10 +1,10 @@ language: - version: "weekly.2024.49" + version: "latest" files: - "**/*.v" - server bootstrap: - - cd /opt/vlang && git fetch --all --tags && git checkout tags/weekly.2024.49 && make && v -version && cd /app + - cd /opt/vlang && git fetch --all --tags && git checkout master && make && v -version && cd /app build_flags: - -prod -cc gcc diff --git a/v/v/.editorconfig b/v/v/.editorconfig new file mode 100644 index 00000000000..01072caf100 --- /dev/null +++ b/v/v/.editorconfig @@ -0,0 +1,8 @@ +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.v] +indent_style = tab diff --git a/v/v/.gitattributes b/v/v/.gitattributes new file mode 100644 index 00000000000..9a98968cecf --- /dev/null +++ b/v/v/.gitattributes @@ -0,0 +1,8 @@ +* text=auto eol=lf +*.bat eol=crlf + +*.v linguist-language=V +*.vv linguist-language=V +*.vsh linguist-language=V +v.mod linguist-language=V +.vdocignore linguist-language=ignore diff --git a/v/v/.gitignore b/v/v/.gitignore new file mode 100644 index 00000000000..068c5251f25 --- /dev/null +++ b/v/v/.gitignore @@ -0,0 +1,24 @@ +# Binaries for programs and plugins +main +a +*.exe +*.exe~ +*.so +*.dylib +*.dll + +# Ignore binary output folders +bin/ + +# Ignore common editor/system specific metadata +.DS_Store +.idea/ +.vscode/ +*.iml + +# ENV +.env + +# vweb and database +*.db +*.js diff --git a/v/v/README.md b/v/v/README.md new file mode 100644 index 00000000000..f0921102d9e --- /dev/null +++ b/v/v/README.md @@ -0,0 +1,91 @@ +# Vanilla + +Vanilla is a raw V server. + +## Description + +This project is a simple server written in the V programming language. It aims to provide a minimalistic and efficient server implementation. + +## Features + +- Lightweight and fast +- Minimal dependencies +- Easy to understand and extend + +## Installation + +To install Vanilla, you need to have the V compiler installed. You can download it from the [official V website](https://vlang.io). + +## Usage + +To run the server, use the following command: + +```sh +v -prod crun . +``` + +This will start the server, and you can access it at `http://localhost:3000`. + +## Code Overview + +### Main Server + +The main server logic is implemented in [src/main.v](v/vanilla/src/main.v). The server is initialized and started in the `main` function: + +```v +module main + +const port = 3000 + +fn main() { + mut server := Server{ + router: setup_router() + } + + server.server_socket = create_server_socket(port) + if server.server_socket < 0 { + return + } + server.epoll_fd = C.epoll_create1(0) + if server.epoll_fd < 0 { + C.perror('epoll_create1 failed'.str) + C.close(server.server_socket) + return + } + + server.lock_flag.lock() + if add_fd_to_epoll(server.epoll_fd, server.server_socket, u32(C.EPOLLIN)) == -1 { + C.close(server.server_socket) + C.close(server.epoll_fd) + + server.lock_flag.unlock() + return + } + + server.lock_flag.unlock() + + server.lock_flag.init() + for i := 0; i < 16; i++ { + server.threads[i] = spawn worker_thread(&server) + } + println('listening on http://localhost:${port}/') + event_loop(&server) +} +``` + +## Test + +### CURL + +```sh +curl -X GET --verbose http://localhost:3000/ && +curl -X POST --verbose http://localhost:3000/user && +curl -X GET --verbose http://localhost:3000/user/1 + +``` + +### WRK + +```sh +wrk --connection 512 --threads 16 --duration 10s http://localhost:3000 +``` diff --git a/v/v/config.yaml b/v/v/config.yaml new file mode 100644 index 00000000000..5dbc9b4c5d0 --- /dev/null +++ b/v/v/config.yaml @@ -0,0 +1,2 @@ +framework: + github: vlang/v diff --git a/v/v/src/controllers.v b/v/v/src/controllers.v new file mode 100644 index 00000000000..7a1cbcea1ac --- /dev/null +++ b/v/v/src/controllers.v @@ -0,0 +1,42 @@ +module main + +import strings + +const http_ok_response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes() + +const http_created_response = 'HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes() + +fn home_controller(params []string) ![]u8 { + return http_ok_response +} + +fn get_users_controller(params []string) ![]u8 { + return http_ok_response +} + +@[direct_array_access; manualfree] +fn get_user_controller(params []string) ![]u8 { + if params.len == 0 { + return tiny_bad_request_response + } + id := params[0] + response_body := id + + mut sb := strings.new_builder(200) + sb.write_string('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: ') + sb.write_string(response_body.len.str()) + sb.write_string('\r\nConnection: keep-alive\r\n\r\n') + sb.write_string(response_body) + + defer { + unsafe { + response_body.free() + params.free() + } + } + return sb +} + +fn create_user_controller(params []string) ![]u8 { + return http_created_response +} diff --git a/v/v/src/main.v b/v/v/src/main.v new file mode 100644 index 00000000000..c6b5fc73739 --- /dev/null +++ b/v/v/src/main.v @@ -0,0 +1,37 @@ +module main + +const port = 3000 +const max_thread_pool_size = 8 + +fn main() { + mut server := Server{} + + server.server_socket = create_server_socket(port) + if server.server_socket < 0 { + return + } + server.epoll_fd = C.epoll_create1(0) + if server.epoll_fd < 0 { + C.perror('epoll_create1 failed'.str) + C.close(server.server_socket) + return + } + + server.lock_flag.lock() + if add_fd_to_epoll(server.epoll_fd, server.server_socket, u32(C.EPOLLIN)) == -1 { + C.close(server.server_socket) + C.close(server.epoll_fd) + + server.lock_flag.unlock() + return + } + + server.lock_flag.unlock() + + server.lock_flag.init() + for i := 0; i < max_thread_pool_size; i++ { + server.threads[i] = spawn worker_thread(&server) + } + println('listening on http://localhost:${port}/') + event_loop(&server) +} diff --git a/v/v/src/request_parser.v b/v/v/src/request_parser.v new file mode 100644 index 00000000000..e8fe13bd7da --- /dev/null +++ b/v/v/src/request_parser.v @@ -0,0 +1,70 @@ +module main + +struct Slice { + start int + len int +} + +struct HttpRequest { +mut: + buffer []u8 + method Slice + path Slice + version Slice +} + +fn parse_request_line(mut req HttpRequest) ! { + mut i := 0 + // Parse HTTP method + for i < req.buffer.len && req.buffer[i] != ` ` { + i++ + } + req.method = Slice{ + start: 0 + len: i + } + i++ + + // Parse path + mut path_start := i + for i < req.buffer.len && req.buffer[i] != ` ` { + i++ + } + req.path = Slice{ + start: path_start + len: i - path_start + } + i++ + + // Parse HTTP version + mut version_start := i + for i < req.buffer.len && req.buffer[i] != `\r` { + i++ + } + req.version = Slice{ + start: version_start + len: i - version_start + } + + // Move to the end of the request line + if i + 1 < req.buffer.len && req.buffer[i] == `\r` && req.buffer[i + 1] == `\n` { + i += 2 + } else { + return error('Invalid HTTP request line') + } +} + +fn decode_http_request(buffer []u8) !HttpRequest { + mut req := HttpRequest{ + buffer: buffer + } + + parse_request_line(mut req)! + + return req +} + +// Helper function to convert Slice to string for debugging +fn slice_to_string(buffer []u8, s Slice) string { + return buffer[s.start..s.start + s.len].bytestr() +} diff --git a/v/v/src/router.v b/v/v/src/router.v new file mode 100644 index 00000000000..acf2e09de33 --- /dev/null +++ b/v/v/src/router.v @@ -0,0 +1,23 @@ +module main + +// handle_request finds and executes the handler for a given route. +// It takes an HttpRequest object as an argument and returns the response as a byte array. +fn handle_request(req HttpRequest) ![]u8 { + method := unsafe { tos(&req.buffer[req.method.start], req.method.len) } + path := unsafe { tos(&req.buffer[req.path.start], req.path.len) } + + if method == 'GET' { + if path == '/' { + return home_controller([]) + } else if path.starts_with('/user/') { + id := path[6..] + return get_user_controller([id]) + } + } else if method == 'POST' { + if path == '/user' { + return create_user_controller([]) + } + } + + return tiny_bad_request_response +} diff --git a/v/v/src/server.c.v b/v/v/src/server.c.v new file mode 100644 index 00000000000..e7459d88895 --- /dev/null +++ b/v/v/src/server.c.v @@ -0,0 +1,301 @@ +// This module implements a basic HTTP server using epoll for handling multiple client connections efficiently. +// The server is designed to be non-blocking and uses multiple threads to handle incoming requests concurrently. +// +// Performance Considerations: +// - Non-blocking I/O: The server uses non-blocking sockets to ensure that it can handle multiple connections without being blocked by any single connection. +// - Epoll: The use of epoll allows the server to efficiently monitor multiple file descriptors to see if I/O is possible on any of them. +// - Threading: The server spawns multiple threads to handle client requests, which can improve performance on multi-core systems. +// - Memory Management: Care is taken to allocate and free memory appropriately to avoid memory leaks and ensure efficient memory usage. +// - Error Handling: The server includes error handling to manage and log errors without crashing, ensuring robustness and reliability. +// - SO_REUSEPORT: The server sets the SO_REUSEPORT socket option to allow multiple sockets on the same host and port, which can improve performance in certain scenarios. +// - Connection Handling: The server efficiently handles client connections, including accepting new connections, reading requests, and sending responses. +// - Mutex Locking: The server uses mutex locks to manage access to shared resources, ensuring thread safety while minimizing contention. +module main + +import sync + +const tiny_bad_request_response = 'HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes() + +#include +#include +#include +#include +#include +#include + +fn C.socket(__domain int, __type int, __protocol int) int + +fn C.bind(sockfd int, addr &Addr, addrlen u32) int + +fn C.send(__fd int, __buf voidptr, __n usize, __flags int) int + +fn C.recv(__fd int, __buf voidptr, __n usize, __flags int) int + +fn C.setsockopt(__fd int, __level int, __optname int, __optval voidptr, __optlen u32) int + +fn C.listen(__fd int, __n int) int + +struct In_addr { + s_addr int +} + +enum In_port_t as u16 { + ipproto_hopopts = 0 + ipproto_routing = 43 + ipproto_fragment = 44 + ipproto_icmpv6 = 58 + ipproto_none = 59 + ipproto_dstopts = 60 + ipproto_mh = 135 +} + +struct Sockaddr_in { + sin_family u16 + sin_port u16 + sin_addr In_addr + sin_zero [8]u8 +} + +fn C.htons(__hostshort u16) u16 + +@[typedef] +union C.epoll_data { +mut: + ptr voidptr + fd int + u32 u32 + u64 u64 +} + +pub struct C.epoll_event { + events u32 + data C.epoll_data +} + +fn C.epoll_create1(__flags int) int + +fn C.epoll_ctl(__epfd int, __op int, __fd int, __event &C.epoll_event) int + +fn C.epoll_wait(__epfd int, __events &C.epoll_event, __maxevents int, __timeout int) int + +struct Server { +mut: + server_socket int + epoll_fd int + lock_flag sync.Mutex + has_clients int + threads [max_thread_pool_size]thread +} + +fn C.fcntl(fd int, cmd int, arg int) int +fn C.perror(s &u8) voidptr +fn C.close(fd int) int +fn C.accept(sockfd int, address &C.sockaddr_in, addrlen &u32) int + +const sock_stream = C.SOCK_STREAM +const sock_nonblock = C.SOCK_NONBLOCK + +const max_unix_path = 108 + +const max_connection_size = 1024 + +@[_pack: '1'] +pub struct Ip6 { + port u16 + flow_info u32 + addr [16]u8 + scope_id u32 +} + +@[_pack: '1'] +pub struct Ip { + port u16 + addr [4]u8 + sin_pad [8]u8 +} + +pub struct Unix { + path [max_unix_path]u8 +} + +union AddrData { + Unix + Ip + Ip6 +} + +@[_pack: '1'] +pub struct Addr { +pub: + f u16 + addr AddrData +} + +fn set_blocking(fd int, blocking bool) { + flags := C.fcntl(fd, C.F_GETFL, 0) + if flags == -1 { + return + } + if blocking { + C.fcntl(fd, C.F_SETFL, flags & ~C.O_NONBLOCK) + } else { + C.fcntl(fd, C.F_SETFL, flags | C.O_NONBLOCK) + } +} + +fn create_server_socket(port int) int { + server_fd := C.socket(2, sock_stream | sock_nonblock, 0) + if server_fd < 0 { + eprintln(@LOCATION) + C.perror('Socket creation failed'.str) + return -1 + } + + // Enable SO_REUSEPORT + opt := 1 + if C.setsockopt(server_fd, C.SOL_SOCKET, C.SO_REUSEPORT, &opt, sizeof(opt)) < 0 { + eprintln(@LOCATION) + C.perror('setsockopt SO_REUSEPORT failed'.str) + C.close(server_fd) + return -1 + } + + server_addr := Sockaddr_in{ + sin_family: 2 // ip + sin_port: C.htons(port) + sin_addr: In_addr{C.INADDR_ANY} + sin_zero: [8]u8{} + } + + if C.bind(server_fd, voidptr(&server_addr), sizeof(server_addr)) < 0 { + eprintln(@LOCATION) + C.perror('Bind failed'.str) + C.close(server_fd) + return -1 + } + if C.listen(server_fd, max_connection_size) < 0 { + eprintln(@LOCATION) + C.perror('Listen failed'.str) + C.close(server_fd) + return -1 + } + return server_fd +} + +// Function to add a file descriptor to the epoll instance +fn add_fd_to_epoll(epoll_fd int, fd int, events u32) int { + mut ev := C.epoll_event{ + events: events + } + ev.data.fd = fd + if C.epoll_ctl(epoll_fd, C.EPOLL_CTL_ADD, fd, &ev) == -1 { + eprintln(@LOCATION) + C.perror('epoll_ctl'.str) + return -1 + } + return 0 +} + +// Function to remove a file descriptor from the epoll instance +fn remove_fd_from_epoll(epoll_fd int, fd int) { + C.epoll_ctl(epoll_fd, C.EPOLL_CTL_DEL, fd, C.NULL) +} + +fn handle_accept(server &Server) { + for { + client_fd := C.accept(server.server_socket, C.NULL, C.NULL) + if client_fd < 0 { + // Check for EAGAIN or EWOULDBLOCK, usually represented by errno 11. + if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK { + break // No more incoming connections; exit loop. + } + + eprintln(@LOCATION) + C.perror('Accept failed'.str) + return + } + + // Set the client socket to non-blocking mode if accepted successfully + set_blocking(client_fd, false) + unsafe { + server.lock_flag.lock() + if add_fd_to_epoll(server.epoll_fd, client_fd, u32(C.EPOLLIN | C.EPOLLET)) == -1 { + C.close(client_fd) + } + server.lock_flag.unlock() + } + } +} + +fn handle_client_closure(server &Server, client_fd int) { + unsafe { + server.lock_flag.lock() + remove_fd_from_epoll(client_fd, client_fd) + server.lock_flag.unlock() + } +} + +@[manualfree] +fn process_events(server &Server) { + events := [max_connection_size]C.epoll_event{} + num_events := C.epoll_wait(server.epoll_fd, &events[0], max_connection_size, -1) + for i := 0; i < num_events; i++ { + if events[i].events & u32((C.EPOLLHUP | C.EPOLLERR)) != 0 { + handle_client_closure(server, unsafe { events[i].data.fd }) + continue + } + if events[i].events & u32(C.EPOLLIN) != 0 { + request_buffer := [140]u8{} + bytes_read := C.recv(unsafe { events[i].data.fd }, &request_buffer[0], 140 - 1, + 0) + if bytes_read > 0 { + mut readed_request_buffer := []u8{cap: bytes_read} + + unsafe { + readed_request_buffer.push_many(&request_buffer[0], bytes_read) + } + + decoded_http_request := decode_http_request(readed_request_buffer) or { + eprintln('Error decoding request ${err}') + C.send(unsafe { events[i].data.fd }, tiny_bad_request_response.data, + tiny_bad_request_response.len, 0) + handle_client_closure(server, unsafe { events[i].data.fd }) + continue + } + + // This lock is a workaround for avoiding race condition in router.params + // This slows down the server, but it's a temporary solution + (*server).lock_flag.lock() + response_buffer := handle_request(decoded_http_request) or { + eprintln('Error handling request ${err}') + C.send(unsafe { events[i].data.fd }, tiny_bad_request_response.data, + tiny_bad_request_response.len, 0) + handle_client_closure(server, unsafe { events[i].data.fd }) + (*server).lock_flag.unlock() + continue + } + (*server).lock_flag.unlock() + + C.send(unsafe { events[i].data.fd }, response_buffer.data, response_buffer.len, + 0) + handle_client_closure(server, unsafe { events[i].data.fd }) + } else if bytes_read == 0 + || (bytes_read < 0 && C.errno != C.EAGAIN && C.errno != C.EWOULDBLOCK) { + handle_client_closure(server, unsafe { events[i].data.fd }) + } + } + } +} + +fn worker_thread(server &Server) { + for { + process_events(server) + } + return +} + +fn event_loop(server &Server) { + for { + handle_accept(server) + } +} diff --git a/v/v/v.mod b/v/v/v.mod new file mode 100644 index 00000000000..78299892878 --- /dev/null +++ b/v/v/v.mod @@ -0,0 +1,7 @@ +Module { + name: 'vanilla' + description: 'A raw V server' + version: '0.0.1' + license: 'MIT' + dependencies: [] +}