Skip to content

Commit f787e03

Browse files
authored
examples: add vanilla_http_server - a fast, multi-threaded, non-blocking, port and host reuse, thread-safe, epoll server (#23094)
1 parent 6623ac2 commit f787e03

File tree

11 files changed

+576
-0
lines changed

11 files changed

+576
-0
lines changed

cmd/tools/modules/testing/common.v

+1
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
242242
skip_files << 'examples/database/psql/customer.v'
243243
}
244244
$if windows {
245+
skip_files << 'examples/vanilla_http_server' // requires epoll
245246
skip_files << 'examples/1brc/solution/main.v' // requires mmap
246247
skip_files << 'examples/database/mysql.v'
247248
skip_files << 'examples/database/orm.v'

cmd/tools/vbuild-examples.v

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const efolders = [
1010
'examples/viewer',
1111
'examples/vweb_orm_jwt',
1212
'examples/vweb_fullstack',
13+
'examples/vanilla_http_server',
1314
]
1415

1516
pub fn normalised_vroot_path(path string) string {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[*]
2+
charset = utf-8
3+
end_of_line = lf
4+
insert_final_newline = true
5+
trim_trailing_whitespace = true
6+
7+
[*.v]
8+
indent_style = tab
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
* text=auto eol=lf
2+
*.bat eol=crlf
3+
4+
*.v linguist-language=V
5+
*.vv linguist-language=V
6+
*.vsh linguist-language=V
7+
v.mod linguist-language=V
8+
.vdocignore linguist-language=ignore
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Binaries for programs and plugins
2+
main
3+
*.exe
4+
*.exe~
5+
*.so
6+
*.dylib
7+
*.dll
8+
9+
# Ignore binary output folders
10+
bin/
11+
12+
# Ignore common editor/system specific metadata
13+
.DS_Store
14+
.idea/
15+
.vscode/
16+
*.iml
17+
18+
# ENV
19+
.env
20+
21+
# vweb and database
22+
*.db
23+
*.js
+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Vanilla
2+
3+
Vanilla is a raw V server.
4+
5+
## Description
6+
7+
This project is a simple server written in the V programming language.
8+
It aims to provide a minimalistic and efficient server implementation.
9+
10+
## Features
11+
12+
- Lightweight and fast
13+
- Minimal dependencies
14+
- Easy to understand and extend
15+
16+
## Installation
17+
18+
To install Vanilla, you need to have the V compiler installed.
19+
You can download it from the [official V website](https://vlang.io).
20+
21+
## Usage
22+
23+
To run the server, use the following command:
24+
25+
```sh
26+
v -prod crun .
27+
```
28+
29+
This will start the server, and you can access it at `http://localhost:3000`.
30+
31+
## Code Overview
32+
33+
### Main Server
34+
35+
The main server logic is implemented in [src/main.v](v/vanilla/src/main.v).
36+
The server is initialized and started in the `main` function:
37+
38+
```v ignore
39+
module main
40+
41+
const port = 3000
42+
43+
fn main() {
44+
mut server := Server{
45+
router: setup_router()
46+
}
47+
48+
server.socket_fd = create_server_socket(port)
49+
if server.socket_fd < 0 {
50+
return
51+
}
52+
server.epoll_fd = C.epoll_create1(0)
53+
if server.epoll_fd < 0 {
54+
C.perror('epoll_create1 failed'.str)
55+
C.close(server.socket_fd)
56+
return
57+
}
58+
59+
server.lock_flag.lock()
60+
if add_fd_to_epoll(server.epoll_fd, server.socket_fd, u32(C.EPOLLIN)) == -1 {
61+
C.close(server.socket_fd)
62+
C.close(server.epoll_fd)
63+
64+
server.lock_flag.unlock()
65+
return
66+
}
67+
68+
server.lock_flag.unlock()
69+
70+
server.lock_flag.init()
71+
for i := 0; i < 16; i++ {
72+
server.threads[i] = spawn process_events(&server)
73+
}
74+
println('listening on http://localhost:${port}/')
75+
event_loop(&server)
76+
}
77+
```
78+
79+
## Test
80+
81+
### CURL
82+
83+
```sh
84+
curl -X GET --verbose http://localhost:3000/ &&
85+
curl -X POST --verbose http://localhost:3000/user &&
86+
curl -X GET --verbose http://localhost:3000/user/1
87+
88+
```
89+
90+
### WRK
91+
92+
```sh
93+
wrk --connection 512 --threads 16 --duration 10s http://localhost:3000
94+
```
95+
96+
### Valgrind
97+
```sh
98+
# Race condition check
99+
v -prod -gc none .
100+
valgrind --tool=helgrind ./vanilla_http_server
101+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module main
2+
3+
import strings
4+
5+
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()
6+
7+
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()
8+
9+
fn home_controller(params []string) ![]u8 {
10+
return http_ok_response
11+
}
12+
13+
fn get_users_controller(params []string) ![]u8 {
14+
return http_ok_response
15+
}
16+
17+
@[direct_array_access; manualfree]
18+
fn get_user_controller(params []string) ![]u8 {
19+
if params.len == 0 {
20+
return tiny_bad_request_response
21+
}
22+
id := params[0]
23+
response_body := id
24+
25+
mut sb := strings.new_builder(200)
26+
sb.write_string('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: ')
27+
sb.write_string(response_body.len.str())
28+
sb.write_string('\r\nConnection: keep-alive\r\n\r\n')
29+
sb.write_string(response_body)
30+
31+
defer {
32+
unsafe {
33+
response_body.free()
34+
params.free()
35+
}
36+
}
37+
return sb
38+
}
39+
40+
fn create_user_controller(params []string) ![]u8 {
41+
return http_created_response
42+
}
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module main
2+
3+
// handle_request finds and executes the handler for a given route.
4+
// It takes an HttpRequest object as an argument and returns the response as a byte array.
5+
fn handle_request(req HttpRequest) ![]u8 {
6+
method := unsafe { tos(&req.buffer[req.method.start], req.method.len) }
7+
path := unsafe { tos(&req.buffer[req.path.start], req.path.len) }
8+
9+
if method == 'GET' {
10+
if path == '/' {
11+
return home_controller([])
12+
} else if path.starts_with('/user/') {
13+
id := path[6..]
14+
return get_user_controller([id])
15+
}
16+
} else if method == 'POST' {
17+
if path == '/user' {
18+
return create_user_controller([])
19+
}
20+
}
21+
22+
return tiny_bad_request_response
23+
}
24+
25+
fn main() {
26+
mut server := Server{
27+
request_handler: handle_request
28+
port: 3001
29+
}
30+
31+
server.run()
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
module main
2+
3+
struct Slice {
4+
start int
5+
len int
6+
}
7+
8+
struct HttpRequest {
9+
mut:
10+
buffer []u8
11+
method Slice
12+
path Slice
13+
version Slice
14+
}
15+
16+
@[direct_array_access]
17+
fn parse_request_line(mut req HttpRequest) ! {
18+
mut i := 0
19+
// Parse HTTP method
20+
for i < req.buffer.len && req.buffer[i] != ` ` {
21+
i++
22+
}
23+
req.method = Slice{
24+
start: 0
25+
len: i
26+
}
27+
i++
28+
29+
// Parse path
30+
mut path_start := i
31+
for i < req.buffer.len && req.buffer[i] != ` ` {
32+
i++
33+
}
34+
req.path = Slice{
35+
start: path_start
36+
len: i - path_start
37+
}
38+
i++
39+
40+
// Parse HTTP version
41+
mut version_start := i
42+
for i < req.buffer.len && req.buffer[i] != `\r` {
43+
i++
44+
}
45+
req.version = Slice{
46+
start: version_start
47+
len: i - version_start
48+
}
49+
50+
// Move to the end of the request line
51+
if i + 1 < req.buffer.len && req.buffer[i] == `\r` && req.buffer[i + 1] == `\n` {
52+
i += 2
53+
} else {
54+
return error('Invalid HTTP request line')
55+
}
56+
}
57+
58+
fn decode_http_request(buffer []u8) !HttpRequest {
59+
mut req := HttpRequest{
60+
buffer: buffer
61+
}
62+
63+
parse_request_line(mut req)!
64+
65+
return req
66+
}
67+
68+
// Helper function to convert Slice to string for debugging
69+
fn slice_to_string(buffer []u8, s Slice) string {
70+
return buffer[s.start..s.start + s.len].bytestr()
71+
}

0 commit comments

Comments
 (0)