Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

example: fast, multi-threaded, non-blocking, port and host reuse, thread-safe, epoll server #23094

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/tools/vbuild-examples.v
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const efolders = [
'examples/viewer',
'examples/vweb_orm_jwt',
'examples/vweb_fullstack',
'examples/vanilla_http_server',
]

pub fn normalised_vroot_path(path string) string {
Expand Down
8 changes: 8 additions & 0 deletions examples/vanilla_http_server/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.v]
indent_style = tab
8 changes: 8 additions & 0 deletions examples/vanilla_http_server/.gitattributes
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions examples/vanilla_http_server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Binaries for programs and plugins
main
*.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
94 changes: 94 additions & 0 deletions examples/vanilla_http_server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# 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 ignore
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
```
42 changes: 42 additions & 0 deletions examples/vanilla_http_server/src/controllers.v
Original file line number Diff line number Diff line change
@@ -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
}
61 changes: 61 additions & 0 deletions examples/vanilla_http_server/src/main.c.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module main

const port = 3000
const max_thread_pool_size = 8

// 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
}

fn main() {
mut server := Server{
request_handler: handle_request
}

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)
}
71 changes: 71 additions & 0 deletions examples/vanilla_http_server/src/request_parser.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
module main

struct Slice {
start int
len int
}

struct HttpRequest {
mut:
buffer []u8
method Slice
path Slice
version Slice
}

@[direct_array_access]
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()
}
Loading
Loading