Repository: chendotjs/lotos Branch: master Commit: 3eb36cc3723a Files: 47 Total size: 134.0 KB Directory structure: gitextract_ry1mqk4z/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── doc/ │ ├── BENCHMARK.md │ ├── CONCURRENCY_MODEL.md │ ├── DEBUG.md │ ├── DEBUG_LOG.md │ ├── DESIGN.md │ ├── PURPOSE.md │ └── dot/ │ └── dict_structure.dot ├── src/ │ ├── CMakeLists.txt │ ├── Makefile │ ├── buffer.c │ ├── buffer.h │ ├── connection.c │ ├── connection.h │ ├── dict.c │ ├── dict.h │ ├── http_parser.c │ ├── http_parser.h │ ├── lotos_epoll.c │ ├── lotos_epoll.h │ ├── main.c │ ├── mem_pool.c │ ├── mem_pool.h │ ├── misc.c │ ├── misc.h │ ├── request.c │ ├── request.h │ ├── response.c │ ├── response.h │ ├── server.c │ ├── server.h │ ├── ssstr.c │ ├── ssstr.h │ └── test/ │ ├── Makefile │ ├── buffer_test.c │ ├── dict_test.c │ ├── heap_test.c │ ├── mem_pool_test.c │ ├── minctest.h │ ├── parse_test.c │ ├── slow_client.c │ └── ssstr_test.c └── www/ ├── error.html └── index.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Prerequisites *.d # Object files *.o *.ko *.obj *.elf # Linker output *.ilk *.map *.exp # Precompiled Headers *.gch *.pch # Libraries *.lib *.a *.la *.lo # Shared objects (inc. Windows DLLs) *.dll *.so *.so.* *.dylib # Executables *.exe *.out *.app *.i*86 *.x86_64 *.hex lotos # Debug files *.dSYM/ *.su *.idb *.pdb # Kernel Module Compile Results *.mod* *.cmd .tmp_versions/ modules.order Module.symvers Mkfile.old dkms.conf #own files minctest/* ================================================ FILE: .travis.yml ================================================ language: c dist: trusty before_install: - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test - sudo apt-get update -qq - sudo apt-get install -qq g++-6 - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-6 90 compiler: - clang - gcc script: - cd src/ && make && make test ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Chen YaQi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Lotos WebServer [![Build Status](https://travis-ci.org/chendotjs/lotos.svg?branch=master)](https://travis-ci.org/chendotjs/lotos) **Lotos is a tiny but high-performance HTTP WebServer following the Reactor model, using non-blocking IO and IO multiplexing(epoll ET) to handle concurrency. Lotos is written in pure c and well tested. Several HTTP headers (Connection, Content-Length, etc.) is supported and more will be added in the future.** ``` ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- C 19 367 275 2428 C/C++ Header 12 120 131 514 make 2 17 0 42 CMake 1 7 0 18 ------------------------------------------------------------------------------- SUM: 34 511 406 3002 ------------------------------------------------------------------------------- ``` ## Documents 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 ------------------------ | ---------------------------------- | ----------------------- | ---------------------- | -------------------------- | -------------------------- [项目目的](./doc/PURPOSE.md) | [并发模型](./doc/CONCURRENCY_MODEL.md) | [设计实现](./doc/DESIGN.md) | [测试调试](./doc/DEBUG.md) | [性能测试](./doc/BENCHMARK.md) | [调试记录](./doc/DEBUG_LOG.md) ## Environment - gcc >= 5.4 or clang >= 3.5 (gcc4.9 is not supported) - Linux only, kernel version >= 3.9 ## Usage ### Build ``` $ git clone https://github.com/chendotjs/lotos.git $ cd lotos/src/ $ make && make test ``` ### Run Usage: lotos -r html_root_dir [-p port] [-t timeout] [-w worker_num] [-d (debug mode)] ``` $ ./lotos -r ../www -t 60 -w 4 -p 8888 ``` then you can visit . ## Feature - EPOLL Edge Trigger mode, more efficient. - Nonblocking IO. - Multiprocessing, port reuse. - TCP connections managed by min-heap data structure. - HTTP persistent connection support. Close TCP connection when connection expires. - Parse HTTP requests using FSM. - Handle errors and exceptions. - Memory pool is optional. ## Test Unit tests are based on [minctest](https://github.com/codeplea/minctest). It is simple, lightweight, and flexible. Moreover, I contributed some codes to it. ## Benchmark Please refer to [BENCHMARK.md](./doc/BENCHMARK.md). ## Reference [nginx](https://github.com/nginx/nginx) [node.js http parser](https://github.com/nodejs/http-parser) [Tkeed](https://github.com/linw7/TKeed) ================================================ FILE: doc/BENCHMARK.md ================================================ # Benchmark 常见的压力测试工具有ab,[wrk](https://github.com/wg/wrk)。HTTP/1.1的长连接已经很普及,wrk默认支持长连接,ab需要加上`-k`选项, 否则ab的压力测试会默认采用HTTP/1.0,即每一个请求建立一个TCP连接。 ## 测试环境 - 测试环境为本地,配置4核心`Intel(R) Core(TM) i5-2320 CPU @ 3.00GHz` - 测试工具为wrk和ab,测试时间为60s,worker数设置为4 - nginx的worker_processes配置为4 - 标准1KB静态页面测试 ## 测试内容 ### 1.使用wrk测试长连接Req/Sec Concurrency | lotos | nginx ----------- | ------ | ------ 10 | 21.94k | 15.42k 100 | 28.21k | 16.95k 1000 | 26.01k | 16.62k ### 2.使用ab测试长连接Req/Sec Concurrency | lotos | nginx ----------- | ------- | ------ 10 | 129.11k | 81.89k 100 | 136.20k | 77.56k 1000 | 97.56k | 53.69k ### 3.使用ab测试短连接Req/Sec Concurrency | lotos | nginx ----------- | ------ | ------ 10 | 25.96k | 25.92k 100 | 26.52k | 26.63k 1000 | 22.53k | 23.29k ### 4.使用ab短连接测试内存池 Concurrency | USE_MEM_POOL | NO_USE_MEM_POOL ----------- | ------------ | --------------- 10 | 25.78k | 25.75k 100 | 26.43k | 26.39k 1000 | 23.17k | 23.24k ## 测试总结 我不知道ab的结果为什么比wrk大很多,可能是两者的计算方式不同吧,但起码Lotos确实表现比nginx要好(毕竟功能简单,实现得也很简单)。 并发的瓶颈不在于内存的malloc和free,现代的内存分配器性能已经很强,但是如果可以用内存池在初始化阶段就分配一大片内存,对于短连接的测试,性能应该还是有些许提升的。 从ab短连接和长连接的测试结果来看,连接的建立和关闭才是真的瓶颈所在, TCP的三次握手和四次挥手比较耗费时间。 如果采用内存池,效率基本没有提升。事实上glibc的malloc已经够快了。 ================================================ FILE: doc/CONCURRENCY_MODEL.md ================================================ # 并发模型 ## 0x01 介绍 常见的并发模型主要有Fork-Exec模型和event driven模型。 Lotos采用的就是多进程Reactor模型,属于event driven模型。 传统的网络服务器的构建中,IO模式会按照Blocking/Non-Blocking、Synchronous/Asynchronous这两个标准进行分类,其中Blocking与Synchronous基本上一个意思,而NIO与Async的区别在于NIO强调的是Polling(即用户进程需要时常询问IO操作是否就绪),而Async强调的是Notification(kernel将IO数据拷贝入用户空间给用户进程使用)。 将同步与否、阻塞与否组合一下,可以得到3种主要的IO模式: - **同步阻塞**:在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。 - **同步非阻塞**:在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。 - **异步非阻塞**:在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。 前些年在IO并发领域有个很著名的[C10K](http://www.kegel.com/c10k.html)问题,即有10000个客户端需要连上一个服务器并保持TCP连接,客户端会不定时的发送请求给服务器,服务器收到请求后需及时处理并返回结果。 ## 0x02 Unix下5种IO模式 ### 1.阻塞 I/O(blocking IO) 在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样: ![](https://lukangping.gitbooks.io/java-nio/content/resources/blocking_io.jpg) 当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。 所以,blocking IO的特点就是在IO执行的两个阶段都被block了。 ### 2.非阻塞 I/O(nonblocking IO) linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子: ![](https://sfault-image.b0.upaiyun.com/961/916/961916360-570a10b5a0ea9_articlex) 当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。 所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。 ### 3.I/O 多路复用( IO multiplexing) IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。 ![](https://sfault-image.b0.upaiyun.com/304/440/3044406194-570a10b9efcb2_articlex) 当用户进程调用了select,那么整个进程会被block,而同时,kernel会"监视"所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。 所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。 这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。 所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。) 在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。 ### 4.信号驱动式IO ![](https://sfault-image.b0.upaiyun.com/221/712/2217129294-570a10c14d83b_articlex) ### 5.异步 I/O(asynchronous IO) inux下的asynchronous IO其实用得很少。先看一下它的流程: ![](https://sfault-image.b0.upaiyun.com/401/185/4011854437-570a10c3b6e2c_articlex) 用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。 ## 0x03 Fork-Exec模型 Fork-Exec模型多采用同步阻塞IO,对每一个客户端的socket连接,都需要一个线程来处理,而且在此期间这个线程一直被占用,直到socket关闭。通常由一个独立的Acceptor线程负责监听客户端的连接,接收到客户端连接之后为客户端连接创建一个新的线程处理请求消息,处理完成之后,返回应答消息给客户端,线程销毁,这就是典型的一请求一应答模型。该架构最大的问题就是不具备弹性伸缩能力,当并发访问量增加后,服务端的线程个数和并发访问数成线性正比。创建线程多了,数据频繁拷贝(I/O,内核数据拷贝到用户进程空间、阻塞),进程/线程上下文切换消耗大,从而导致操作系统崩溃。 面对这种问题,相应的改进方式,可以设计一个线程池,复用线程资源,减少线程新建、销毁的开销。但仍然是一种浪费资源的方式。 面对即时聊天(IM)程序这种连接时间长、载体消息短的应用场景,一台服务器可能要撑起几十万的连接量(C100k问题),Fork-Exec模型是很难应对的。如果业务逻辑中,线程需要进行时间较长的IO操作(例如跨机房访问接口),则线程大部分时间都在等待IO的返回,白白浪费了大量CPU时间片。 ## 0x04 Reactor模型 什么是Reactor? 换个名词"non-blocking IO + IO multiplexing",意思就显而易见了。Reactor模式用非阻塞IO + IO复用函数来处理并发,程序的基本结构是一个事件循环,以事件驱动和事件回调的方式实现业务逻辑。 Lotos就是采用NIO + epoll的方式处理并发。Lotos使用epoll作为同步事件多路分解器(Synchronous Event Demultiplexer),等待IO事件的发生。当可读或者可写事件发生于某个文件描述符时,事件分解器会去调用之前注册的相应的事件处理器。 对应到Lotos的代码中,[main.c](../src/mainl.c)中`request_handle`和`response_handle`就是对读、写操作的相应handler。 ## 0x05 Proactor模型 与Reactor模式对应的就是Proactor模式。Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而 Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备。Proactor的实现依赖操作系统对异步的支持,目前实现了纯异步操作的操作系统少,实现优秀的如windows IOCP。由于Unix/Linux系统对纯异步的支持有限,应用事件驱动的主流还是通过select/epoll来实现。 ## 参考 ================================================ FILE: doc/DEBUG.md ================================================ # 测试调试 ## 调试工具 - nc - curl - wrk - gdb - valgrind - perf ## 测试 对每一个数据结构都写了简单的单元测试。我不敢说测试很充分,但是主要的功能点还是测试到了在开发过程中多次大块修改代码结构,测试能跑过就可以比较安心,有单元测试对于重构代码帮助很大! 在代码设计中,考虑到测试的便捷性,需要把每个模块独立起来,尽量减少模块之间的耦合。对于利用recv这样的系统调用获得输入,从而进行测试的过程,会比较麻烦。我在[parse_test.c](../src/test/parse_test.c)使用`buffer_cat`函数模拟recv得到的数据,从而进行parser的测试。在这里,`buffer_t`和parser是较为紧密的耦合关系,不过总体而言,相比较另写客户端提供数据,这种方式还是便捷得多。 [设计实现](./DESIGN.md)里面说过,NIO决定了每一个IO操作的状态包含三种:OK, ERROR, AGAIN。健壮的服务器应该能处理这种慢请求,因此测试代码也实现了一个[慢速client](../src/test/slow_client.c),每隔30ms发送一个字节给服务器,测试服务器能否正确解析。client里面已经内置了一些请求体。 ================================================ FILE: doc/DEBUG_LOG.md ================================================ # 问题1 ## 问题描述 使用`wrk`进行压力测试,发现`Avg Req/Sec`只有10^2数量级,显然不符合预期。请求返回的`Avg Latency` 也在40ms左右,在单机测试的环境下,这也是很长的响应时间了。 下面是wrk的测试报告: ``` $ wrk -t5 -c10 -d10s http://localhost:8888/ -vvv wrk [epoll] Copyright (C) 2012 Will Glozer wrk [epoll] Copyright (C) 2012 Will Glozer wrk [epoll] Copyright (C) 2012 Will Glozer Running 10s test @ http://localhost:8888/ 5 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 43.72ms 3.16ms 49.97ms 99.21% Req/Sec 45.68 6.97 60.00 88.00% 2284 requests in 10.01s, 333.74KB read Requests/sec: 228.12 Transfer/sec: 33.33KB ``` ## Debug记录 一开始猜测是在某个函数上开销比较大,但是函数太多,具体到某个函数又不容易定位。于是想起可以用`on-cpu 火焰图`分析函数执行耗时。 针对是否使用http keep-alive选项,对`lotos`使用两种模式的压力测试。 - `ab`版本 ab版本不加keep-alive,纯http1.0模式,得到![149bcb68/ab-perf-kernel.svg](flamegraph/149bcb68/ab-perf-kernel.svg) - `wrk`版本 采用http/1.1协议,支持keep-alive,得到![149bcb68/wrk-perf-kernel.svg](flamegraph/149bcb68/wrk-perf-kernel.svg) ab版本的火焰图很符合预期,每次都会建立、断开tcp连接,请求、回复的handler占有的比重也很正常,总之是个很漂亮的火焰图😉。 相比ab版本,发现wrk版本在epoll_wait上等待较多,cpu的采样点也少得可怜,可以大胆猜测应该是在某处IO上阻塞了。 我给`lotos`的主事件循环中加入了一些调试信息,采用`curl`命令发起keep-alive请求,命令如下 ``` curl localhost:8000 localhost:8000 -vvv ``` 命令的返回如下: ``` $ curl localhost:8888 -o /dev/null localhost:8888 -o /dev/null -vvv * Rebuilt URL to: localhost:8888/ % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying ::1... * TCP_NODELAY set * connect to ::1 port 8888 failed: 連線被拒絕 * Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8888 (#0) > GET / HTTP/1.1 > Host: localhost:8888 > User-Agent: curl/7.57.0 > Accept: */* > < HTTP/1.1 200 OK < Date: Tue, 30 Jan 2018 07:08:59 GMT < Server: lotos/0.1 < Content-Type: text/html < Content-Length: 6 < Connection: keep-alive < { [6 bytes data] 100 6 100 6 0 0 6 0 0:00:01 --:--:-- 0:00:01 666 * Connection #0 to host localhost left intact * Rebuilt URL to: localhost:8888/ * Found bundle for host localhost: 0x55fc045f4460 [can pipeline] * Re-using existing connection! (#0) with host localhost * Connected to localhost (127.0.0.1) port 8888 (#0) > GET / HTTP/1.1 > Host: localhost:8888 > User-Agent: curl/7.57.0 > Accept: */* > < HTTP/1.1 200 OK < Date: Tue, 30 Jan 2018 07:08:59 GMT < Server: lotos/0.1 < Content-Type: text/html < Content-Length: 6 < Connection: keep-alive < { [6 bytes data] 100 6 100 6 0 0 6 0 0:00:01 --:--:-- 0:00:01 6 * Connection #0 to host localhost left intact ``` 输出中的`Re-using existing connection! (#0) with host localhost`确实表明了curl确实重用了这条tcp连接。 对应的`lotos`的调试信息: ``` epoll: 0 epoll: 0 epoll: 1 [2018-01-30 10:12:25] fd: 6 127.0.0.1:43556 [2018-01-30 10:12:25] malloc 0x56418c766c70 1 [2018-01-30 10:12:25] ---------------accept epoll: 1 0x56418c766c70 in 6 [2018-01-30 10:12:25] 0---------------in 108 us [2018-01-30 10:12:25] 0---------------out 0 us epoll: 1 [2018-01-30 10:12:25] 0---------------in 0 us 0x56418c766c70 out 6 send 143 bytes send 0 bytes [2018-01-30 10:12:25] 0---------------out 130 us epoll: 1 0x56418c766c70 in 6 [2018-01-30 10:12:25] 0---------------in 51 us [2018-01-30 10:12:25] 0---------------out 0 us epoll: 1 [2018-01-30 10:12:25] 0---------------in 0 us 0x56418c766c70 out 6 send 143 bytes send 0 bytes [2018-01-30 10:12:25] 0---------------out 87 us epoll: 0 epoll: 0 epoll: 1 0x56418c766c70 in 6 [2018-01-30 10:12:25] -1---------------in 27 us [2018-01-30 10:12:25] -1---------------out 0 us [2018-01-30 10:12:25] prune 0x56418c766c70 1 epoll: 0 epoll: 0 epoll: 0 epoll: 0 ``` 问题出现了, ``` 0x56418c766c70 out 6 send 143 bytes send 0 bytes [2018-01-30 10:12:25] 0---------------out 87 us epoll: 0 epoll: 0 epoll: 1 0x56418c766c70 in 6 [2018-01-30 10:12:25] -1---------------in 27 us ``` 在最后一次给客户端发送完之后,`epoll_wait`出现了两次等待超时!!!很神奇!!!压力测试的工具竟然会延迟给我返回数据? 或者,是我的程序发送数据存在延迟??? 可是我已经设置了禁用Nagle算法!!!(大四的网络程序设计课吃过瘪,很有印象)。 掏出神器strace执行`strace -tt ./lotos -r /tmp -t 10 -w 4 -d`,发现确实存在两次`epoll_wait`的超时,正好是40ms的阻塞。 ``` 00:39:34.964433 sendfile(7, 8, NULL, 6) = 6 00:39:34.964535 sendfile(7, 8, NULL, 6) = 0 00:39:34.964616 close(8) = 0 00:39:34.964692 epoll_ctl(5, EPOLL_CTL_MOD, 7, {EPOLLET, {u32=717054400, u64=94206529724864}}) = 0 00:39:34.964776 epoll_ctl(5, EPOLL_CTL_MOD, 7, {EPOLLIN|EPOLLET, {u32=717054400, u64=94206529724864}}) = 0 00:39:34.964858 epoll_pwait(5, [], 10240, 20, NULL, 8) = 0 00:39:34.985093 epoll_pwait(5, [], 10240, 20, NULL, 8) = 0 00:39:35.005413 epoll_pwait(5, [{EPOLLIN, {u32=717054400, u64=94206529724864}}], 10240, 20, NULL, 8) = 1 00:39:35.006764 recvfrom(7, "GET / HTTP/1.1\r\nHost: 192.168.1."..., 8192, 0, NULL, NULL) = 44 00:39:35.006875 recvfrom(7, 0x7ffe23f19810, 8192, 0, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable) 00:39:35.006929 openat(3, "./", O_RDONLY) = 6 00:39:35.007000 fstat(6, {st_mode=S_IFDIR|S_ISVTX|0777, st_size=840, ...}) = 0 00:39:35.007104 openat(6, "index.html", O_RDONLY) = 8 ``` 掏出wireshark抓包看也是一切正常。囧了。google搜索也没给出答案(关键词不对:sweat: 后来我发现不论`epoll_wait`的超时值设为多少,`Avg Latency` 总是在40ms左右。于是把`40ms`作为关键词google之,发现还真是Nagle算法的问题。估计是我代码写错了吧,去排查一下,发现果真对文件描述符0做了设置TCP_NODELAY的操作!!!我真的是写bug的程序员... 修改了代码之后,问题瞬间就不存在了。 重新生成火焰图, - `ab`版本 ab版本不加keep-alive,纯http1.0模式,得到![8717b1a3/ab-perf-kernel.svg](flamegraph/8717b1a3/ab-perf-kernel.svg) - `wrk`版本 采用http/1.1协议,支持keep-alive,得到![8717b1a3/wrk-perf-kernel.svg](flamegraph/8717b1a3/wrk-perf-kernel.svg) 关于Nagle算法,我觉得这篇文章已经讲得很清晰[《神秘的40毫秒延迟与 TCP_NODELAY》](http://jerrypeng.me/2013/08/mythical-40ms-delay-and-tcp-nodelay/)。 lotos的设计上确实也是http headers和body分开发送,所以headers被立即发送("send data immediately"),body则被放在缓冲区里面("enqueue data in the buffer until an acknowledge is received"),直到对面40ms的超时ACK来临,才会把body发送出去。 好吧,虽然我知道很多服务器都设置TCP_NODELAY,包括nginx,以前也只是经验性的设置一下该选项,直到今天踩了坑,才对这玩意有更深的理解。调试过程也是一个学习成长的过程! # 问题2 ## 问题描述 使用wrk压力测试时候,程序莫名退出,也没有打印出任何错误信息。 ## Debug记录 掏出gdb,在gdb中运行,然后使用wrk压力测试。 ``` Program received signal SIGPIPE, Broken pipe. 0x00007ffff7b1751d in send () from /usr/lib/libc.so.6 ``` 恍然大悟,原来是没有处理SIGPIPE。[`man 7 pipe`](https://linux.die.net/man/7/pipe)中写的很清楚。 > If all file descriptors referring to the write end of a pipe have been closed, then an attempt to read(2) from the pipe will see end-of-file (read(2) will return 0). If all file descriptors referring to the read end of a pipe have been closed, then a write(2) will cause a SIGPIPE signal to be generated for the calling process. If the calling process is ignoring this signal, then write(2) fails with the error EPIPE. An application that uses pipe(2) and fork(2) should use suitable close(2) calls to close unnecessary duplicate file descriptors; this ensures that end-of-file and SIGPIPE/EPIPE are delivered when appropriate. 当对端关闭了连接,并且本端忽略了SIGPIPE信号,那么`write`系统调用会失败并且设置errno为EPIPE。 ================================================ FILE: doc/DESIGN.md ================================================ # 设计与实现 ## 0x01 重要的结构体 1.配置信息结构(server.h) ```c typedef struct { uint16_t port; /* listen port */ bool debug; /* debug mode */ int timeout; /* connection expired time */ uint32_t worker; /* worker num */ char *rootdir; /* html root directory */ int rootdir_fd; /* fildes of rootdir */ } config_t; ``` 2.请求/响应缓冲结构(buffer.h) ```c typedef struct { int len; /* used space length in buf */ int free; /* free space length in buf */ char buf[]; /* store data */ } buffer_t; ``` 采用柔性数组,可以动态增长,围绕该结构设计了一系列操作函数。 3.简单静态字符串结构(ssstr.h) ```c typedef struct { char *str; int len; } ssstr_t; ``` 对于字符串字面常量以及`buffer_t`结构的子串,都可以用该结构描述而无需开启新的缓冲区存储。有效节省了空间。 4.HTTP连接结构(connection.h) ```c struct connection { int fd; /* connection fildes */ struct epoll_event event; /* epoll event */ struct sockaddr_in saddr; /* IP socket address */ time_t active_time; /* connection accpet time */ int heap_idx; /* idx at lotos_connections */ request_t req; /* request */ }; typedef struct connection connection_t; ``` 5.请求信息结构(request.h) ```c struct request { struct connection *c; /* belonged connection */ buffer_t *ib; /* request buffer */ buffer_t *ob; /* response buffer */ parse_archive par; /* parse_archive */ int resource_fd; /* resource fildes */ int resource_size; /* resource size */ int status_code; /* response status code */ int (*req_handler)(struct request *); /* request handler for rl, hd, bd */ int (*res_handler)(struct request *); /* response handler for hd bd */ }; typedef struct request request_t; ``` 6.HTTP请求解析结构(http_parser.h) ```c typedef struct { /* parsed request line result */ http_method method; http_version version; ssstr_t request_url_string; req_url url; /* parsed header lines result */ bool keep_alive; /* connection keep alive */ int content_length; /* request body content_length */ int transfer_encoding; /* affect body recv strategy */ request_headers_t req_headers; int num_headers; ssstr_t header[2]; /* store header every time `parse_header_line` */ /* preserve buffer_t state, so when recv new data, we can keep parsing */ char *next_parse_pos; /* parser position in buffer_t */ int state; /* parser state */ /* private members, do not modify !!! */ char *method_begin; char *url_begin; char *header_line_begin; char *header_colon_pos; char *header_val_begin; char *header_val_end; size_t body_received; int buffer_sent; bool isCRLF_LINE; bool response_done; bool err_req; } parse_archive; ``` 7.错误页面结构(response.h) ```c typedef struct { int err_page_fd; /* fildes of err page */ const char *raw_err_page; /* raw data of err page file */ size_t raw_page_size; /* size of err page file */ buffer_t *rendered_err_page; /* buffer contains err msg */ size_t rendered_page_size; /* size of err page file */ } err_page_t; ``` ## 0x02 数据结构 ### 1.最小堆(connection.c) connection.c实现了一个最小二叉堆, 依据每个connection的active_time比较大小。因为二叉堆是一个完全二叉树的形态,为了简化编程,可以使用数组来存储堆结点。假设堆顶的position为0,按照层次遍历(BFS)的顺序编号,那么position为`i`的结点,左孩子的position为`2*i+1`, 右孩子的position为`2*i+2`。有了这层关系,可以通过position很快定位到孩子或者父结点的位置。 在这样的基础上,实现了以下操作: - heap_bubble_up - heap_bubble_down - heap_insert ### 2.HashMap(dict.c) 实现了一个简单的HashMap。将其结构画出来,应该也是很一目了然的。 ![](./dot/dict_structure.png) 有一个小细节需要注意,通常我们需要把hash函数算出来的hash值映射回一个HashMap数组的对应位置,使其可以被加入索引。虽然最简单直接的想法是通过取模运算(%),但是%运算比较低效,在大规模的查询/插入操作时很费CPU时间。换一个方式,我们可以规定的HashMap数组的长度为2的幂(如16,32,64...),这样数组的范围就是[0, 2^n-1],映射回HashMap数组的对应方法可以是 `index = Hash(key) & (Length - 1)`。这样`Length - 1`的二进制低位就全是1,如此可以均匀地把key映射到数组中。Lotos的实现中,HashMap的数组长度定为256,可以通过修改`DICT_MASK_SIZE`宏来改变数组长度。 在Lotos中,HashMap的使用场景数据量比较小,就没有考虑负载因子、rehash等因素,仅仅实现了最简单的功能。 - dict_init - dict_put - dict_get - dict_free ## 0x03 NIO配合epoll 所有关于epoll的问题几乎都可以在[`man 7 epoll`](https://linux.die.net/man/7/epoll)中找到。manual写的很详细了,也给了服务器处理事件循环的样例代码,大部分采用epoll的服务器结构无外乎如此。 ```c /* Set up listening socket, 'listen_sock' (socket(), bind(), listen()) */ epollfd = epoll_create(10); if (epollfd == -1) { perror("epoll_create"); exit(EXIT_FAILURE); } ev.events = EPOLLIN; ev.data.fd = listen_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE); } for (;;) { nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_pwait"); exit(EXIT_FAILURE); } for (n = 0; n < nfds; ++n) { if (events[n].data.fd == listen_sock) { conn_sock = accept(listen_sock, (struct sockaddr *) &local, &addrlen); if (conn_sock == -1) { perror("accept"); exit(EXIT_FAILURE); } setnonblocking(conn_sock); ev.events = EPOLLIN | EPOLLET; ev.data.fd = conn_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) { perror("epoll_ctl: conn_sock"); exit(EXIT_FAILURE); } } else { do_use_fd(events[n].data.fd); } } } ``` epoll中很重要的一个结构体类型是`struct epoll_event`, 它包含了一个`epoll_data_t`类型的联合对象`data`。 ```c typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; ``` 样例代码中`ev.data.fd = conn_sock;`,直接使用`fd`成员存储新建立连接的文件描述符,这样做简洁明了,但是需要额外的代码去描述一个连接的状态。在Lotos中使用了`connection_t`类型来描述一个连接的属性和状态,所以Lotos使用了`ptr`成员来保存`connection_t`实例的地址。 ## 0x04 长连接与超时关闭 相比于HTTP/1.0,在服务器端发送完数据后关闭文件描述符即可,HTTP/1.1支持长连接,这就需要考虑连接的超时关闭问题,否则大量的非活动连接会消耗尽系统资源。 Lotos将所有连接注册进一个最小堆,active_time表示该连接上次活动的Epoch时间,active_time越小,表明该连接上次活动时间越早,越有可能超时。当连接建立或者有IO操作时,active_time会被更新,并且在堆中的位置会做相应调整。在每次的事件循环中,都会检查一下堆顶的连接是否超时(connection_prune函数),若超时则关闭连接、移出最小堆。对连接的操作中假若出现了错误,需要关闭连接,最简单的办法是将其active_time设为很小的一个值(比如0),然后等待connection_prune函数将其移除。 ## 0x05 HTTP请求解析 对于HTTP请求体的解析,可以采用有穷状态机(FSM)逐个字母匹配,也可以采用简单的字符串匹配方式。 由于Lotos采用了NIO,不一定可以一次得到完整的请求体(这点在[测试调试](./DEBUG.md)部分也有体现),所以保存连接请求的解析状态是必不可少的工作,否则每次请求体到来之后从头解析就显得愚钝了。状态机恰好可以可以满足这种需求,写起来也不是特别复杂,[RFC2016](https://www.w3.org/Protocols/rfc2616/rfc2616.html)已经给出了BNF范式,照着BNF范式逐字匹配即可,遇到对不上的请求体返回错误即可。Lotos目前实现了Request Line、Header和部分Body的解析,解析代码都在[http_parser.c](../src/http_parser.c)中,解析的结果保存在`parse_archive`类型的结构体中,`request_t`类型有一个`parse_archive`类型的成员`par`,用来记录每个请求解析的状态以及结果。 ## 0x06 状态管理 NIO决定了每一个IO操作的状态包含三种:OK, ERROR, AGAIN。Lotos中有两个函数`int request_recv(request_t *r)`和`int response_send(request_t *r)`用于接受和发送数据。在这里三个状态对应的语义应该是: 对于`request_recv`: - OK: 读到EOF,对端正常关闭连接,无需再读 - ERROR: 错误,需要进入错误处理环节,如断开连接 - AGAIN: 还有数据等待读取,等待下次再读 对于`response_send`: - OK: 全部数据已经发送(并不代表对端收到),无需再发 - ERROR: 错误,需要进入错误处理环节,如断开连接 - AGAIN: 还有数据等待发送,等待下次再发 请求体的状态判定不仅和IO操作的状态相关,也与HTTP协议解析是耦合的。比如对端发出`GE`,在HTTP请求解析模块里,这一部分是合法的请求,但并不完整,我们很大程度上相信这将会是一个HTTP GET请求,所以我们需要再次recv获得更多请求体才能确定。如果接下又收到`T / HTTP/1.0`,那么认为该次请求的Request Line是OK的,否则就是ERROR。所以需要赋予请求的每个状态更明确的语义。 - OK: 请求是合法的 - ERROR: 错误,需要进入错误处理环节,如断开连接 - AGAIN: 请求体目前是合法的,但不完整,需要再读 ## 0x07 错误处理 作为一个长时间跑在后台的程序而言,需要足够健壮,需要对错误处理做足功夫。调试时就遇到[使用wrk压力测试时,程序退出没有任何错误的假象](./DEBUG_LOG.md),原因是对于SIGPIPE没有做正确的处理。中给出了触发网络编程中两个常见错误`Connection reset by peer` 和`Broken pipe`的示例代码。在编写代码时候需要对每个syscall做错误检查,否则调试时定位bug则会困难许多。保证内存没有泄露也是很重要的一点,用valgrind测试是最简单的方式。 ================================================ FILE: doc/PURPOSE.md ================================================ # 项目目的 Lotos是个人的业余网络编程项目,也是自己将理论付诸于实践的一个过程。初学c的socket编程是在本科三年级,从那时起就梦想写一个HTTP Server,虽然之后陆陆续续也写过几个和网络编程有关的小项目,比如网络扫描器和[Tinyhttpd](https://github.com/EZLippi/Tinyhttpd),但也仅限于几百行的规模。 2017年花了约两个月断断续续读完了[《Unix/Linux编程实践教程》](https://book.douban.com/subject/1219329/),可以说这是一本相见恨晚的书,既适合入门,也适合重新梳理知识体系,我觉得自己有能力重新开一个HTTP Server的坑了。 我对Lotos的期望是“能用”,这就要求能够正确处理syscall error、signal以及很多极端情况下的用户请求。更进一步,在能用的基础上争取做到“高性能”,高兴的是,在[Benchmark](./BENCHMARK.md)中,Lotos的性能还是很不错的。 Lotos的开发几乎占用了我一个多月的所有业余时间,私下里认为代码还算工整规范,现在再把开发调试的思考和总结记录成文档,方便与大家交流学习。 #### 开发规范 开发流程比较完整,确定需求 -> 服务器模型选型 -> 数据结构选择 -> 单元测试 -> 开发 -> 集成测试 -> 性能测试。核心的数据结构都配套有单元测试。 #### 数据结构 通过对场景需求和将来扩展性的考量,需要设计合理的、高效的数据结构。Lotos中用了最小堆来淘汰超时过期的HTTP连接,以及使用了HashMap这种KV结构加速对诸如`Mime Type`和 `HTTP Header Handler`的查找。 #### 语言特性 项目中涉及C语言中很多特性,比如条件编译,嵌套宏定义、柔性数组、静态全局变量、函数指针、位运算以及利用内存空间全局区等。 #### 抽象能力 在用户空间进行c编程,由于没有天然的面向对象支持,需要程序员自己封装各种结构体。系统编程中,IO操作基本上都是对文件描述符fd进行操作,在本项目中,一个文件描述符对应着一个TCP连接,把fd封装进`connection_t`作为一个成员变量是一个自然而然的抽象过程。 #### 协议理解 开发HTTP服务器从宏观上来说会对网络协议TCP及其各个状态理解更深,会对HTTP协议主要字段的功能理解更深,会对网络I/O模型认识更深。 ================================================ FILE: doc/dot/dict_structure.dot ================================================ digraph structs { rankdir=LR node [shape=record]; struct1 [label=" dict_t| table| size_mask| used"]; struct2 [label=" dict_node_t| 0| 1| 2| ...| 254| 255"]; struct3 [label="NULL"] struct4 [label="NULL"] struct5 [label="NULL"] struct6 [label="NULL"] struct7[label=" dict_node_t| { k | v | next }"] struct8[label=" dict_node_t| { k | v | next }"] struct9 [label="NULL"] struct1:f1 -> struct2:f0; struct2:f1 -> struct3; struct2:f2 -> struct4; struct2:f5 -> struct5; struct2:f6 -> struct6; struct2:f3 -> struct7; struct7:next -> struct8; struct8:next -> struct9; } ================================================ FILE: src/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 2.6 FATAL_ERROR) project(lotos) if (NOT CMAKE_BUILD_TYPE) message(STATUS "No build type defined; defaulting to 'Debug'") set(CMAKE_BUILD_TYPE "Release" CACHE STRING "The type of build. Possible values are: Debug, Release, RelWithDebInfo and MinSizeRel.") endif() message(STATUS "Host is: ${CMAKE_HOST_SYSTEM}. Build target is: ${CMAKE_SYSTEM}") set(SRC buffer.c connection.c dict.c http_parser.c lotos_epoll.c main.c misc.c request.c response.c server.c ssstr.c mem_pool.c ) message(STATUS "OS type: ${CMAKE_SYSTEM_NAME}") if (NOT ${CMAKE_SYSTEM_NAME} MATCHES "Linux") message( FATAL_ERROR "Only Linux is supported, CMake will exit." ) endif() add_executable(lotos ${SRC}) add_definitions(-DNDEBUG) set_property(TARGET lotos PROPERTY C_STANDARD 99) ================================================ FILE: src/Makefile ================================================ CFLAGS=-std=c99 -Wall -O3 -DNDEBUG -DUSE_MEM_POOL=1 OPTFLAGS= OBJS=misc.o ssstr.o dict.o lotos_epoll.o buffer.o request.o response.o \ connection.o http_parser.o server.o mem_pool.o main.o lotos : $(OBJS) $(CC) $(CFLAGS) $^ -o $@ $(OPTFLAGS) test : make -C ./test make test -C ./test format : find . -iname '*.[ch]' -exec clang-format -i -style="{ColumnLimit: 80}" {} + clean : rm -f *.o lotos .PHONY : test clean format ================================================ FILE: src/buffer.c ================================================ #include "buffer.h" #include "misc.h" #include #include #include buffer_t *buffer_init() { return buffer_new(BUFSIZ); } buffer_t *buffer_new(size_t initlen) { buffer_t *pb = malloc(sizeof(buffer_t) + initlen + 1); // reserve space for \0 assert(pb != NULL); if (pb == NULL) { return NULL; } pb->len = 0; pb->free = initlen; pb->buf[pb->len] = '\0'; return pb; } void buffer_free(buffer_t *pb) { if (pb != NULL) { free(pb); } } inline void buffer_clear(buffer_t *pb) { pb->free += pb->len; pb->len = 0; } /** * newbuf = buffer_cat(oldbuf, "abc", 3); * After the call, oldbuf is no longer valid, and must be substituted with * newbuf * * @param pb [description] * @param buf [description] * @param nbyte [description] */ buffer_t *buffer_cat(buffer_t *pb, const char *buf, size_t nbyte) { buffer_t *npb = NULL; if (nbyte <= buffer_avail(pb)) { // no need to realloc memcpy(pb->buf + pb->len, buf, nbyte); pb->len += nbyte; pb->free -= nbyte; pb->buf[pb->len] = '\0'; return pb; } /* realloc */ size_t cur_len = buffer_len(pb); size_t new_len = cur_len + nbyte; /* realloc strategy */ if (new_len < BUFFER_LIMIT) new_len *= 2; else new_len += BUFFER_LIMIT; npb = realloc(pb, sizeof(buffer_t) + new_len + 1); if (npb == NULL) return NULL; memcpy(npb->buf + npb->len, buf, nbyte); npb->len += nbyte; npb->free = new_len - npb->len; npb->buf[npb->len] = '\0'; return npb; } buffer_t *buffer_cat_cstr(buffer_t *pb, const char *cstr) { return buffer_cat(pb, cstr, strlen(cstr)); } void buffer_print(buffer_t *pb) { char *p = pb->buf; for (; p != pb->buf + pb->len; p++) { printf("%c", *p); fflush(stdout); } printf("\n"); } ================================================ FILE: src/buffer.h ================================================ #ifndef _BUFFER_H__ #define _BUFFER_H__ #include "misc.h" #include /* usually 2M, a request buffer may increase refering to this */ #define BUFFER_LIMIT (BUFSIZ * 250) typedef struct { int len; /* used space length in buf */ int free; /* free space length in buf */ char buf[]; /* store data */ } buffer_t; extern buffer_t *buffer_init(); extern buffer_t *buffer_new(size_t initlen); extern void buffer_free(buffer_t *pb); extern void buffer_clear(buffer_t *pb); extern buffer_t *buffer_cat(buffer_t *pb, const char *buf, size_t nbyte); extern buffer_t *buffer_cat_cstr(buffer_t *pb, const char *cstr); extern void buffer_print(buffer_t *pb); static inline size_t buffer_len(const buffer_t *pb) { return pb->len; } static inline size_t buffer_avail(const buffer_t *pb) { return pb->free; } static inline buffer_t *buffer_buffer(const char *buf) { return (buffer_t *)(buf - (sizeof(buffer_t))); } static inline char *buffer_end(const buffer_t *pb) { return ((char *)(pb->buf) + pb->len); } #endif ================================================ FILE: src/connection.c ================================================ #include "connection.h" #include "lotos_epoll.h" #include "misc.h" #include "server.h" #include #include #include #include #include #include #include /************************** heap operation start ****************************/ /* lotos_connections is seen as a binary min heap */ connection_t *lotos_connections[MAX_CONNECTION] = {0}; static int heap_size = 0; #define LCHILD(x) (((x) << 1) + 1) #define RCHILD(x) (LCHILD(x) + 1) #define PARENT(x) ((x - 1) >> 1) #define INHEAP(n, x) (((-1) < (x)) && ((x) < (n))) inline static void c_swap(int x, int y) { assert(x >= 0 && x < heap_size && y >= 0 && y < heap_size); connection_t *tmp = lotos_connections[x]; lotos_connections[x] = lotos_connections[y]; lotos_connections[y] = tmp; // update heap_idx lotos_connections[x]->heap_idx = x; lotos_connections[y]->heap_idx = y; } /* used for inserting */ static void heap_bubble_up(int idx) { while (PARENT(idx) >= 0) { int fidx = PARENT(idx); // fidx is father of idx; connection_t *c = lotos_connections[idx]; connection_t *fc = lotos_connections[fidx]; if (c->active_time >= fc->active_time) break; c_swap(idx, fidx); idx = fidx; } } /* used for extracting or active_time update larger */ static void heap_bubble_down(int idx) { while (TRUE) { int proper_child; int lchild = INHEAP(heap_size, LCHILD(idx)) ? LCHILD(idx) : (heap_size + 1); int rchild = INHEAP(heap_size, RCHILD(idx)) ? RCHILD(idx) : (heap_size + 1); if (lchild > heap_size && rchild > heap_size) { // no children break; } else if (INHEAP(heap_size, lchild) && INHEAP(heap_size, rchild)) { proper_child = lotos_connections[lchild]->active_time < lotos_connections[rchild]->active_time ? lchild : rchild; } else if (lchild > heap_size) { proper_child = rchild; } else { proper_child = lchild; } // idx is the smaller than children if (lotos_connections[idx]->active_time <= lotos_connections[proper_child]->active_time) break; assert(INHEAP(heap_size, proper_child)); c_swap(idx, proper_child); idx = proper_child; } } static int heap_insert(connection_t *c) { if (heap_size >= MAX_CONNECTION) { return ERROR; } lotos_connections[heap_size++] = c; c->heap_idx = heap_size - 1; heap_bubble_up(heap_size - 1); return 0; } static void heap_print() { connection_t *c; int i; printf("----------------heap---------------\n"); for (i = 0; i < heap_size; i++) { c = lotos_connections[i]; printf("[%2d] %p fd: %2d heap_idx: %2d active_time: %lu\n", i, c, c->fd, c->heap_idx, c->active_time); } printf("----------------heap---------------\n"); } /************************** heap operation end ******************************/ /* ref `man 7 tcp`. disable Nagle Algorithm, make send(2) flush */ static inline void connection_set_nodelay(connection_t *c) { static int enable = 1; setsockopt(c->fd, IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(enable)); } connection_t *connection_init() { // Too many malloc would be slow, but mem pool seems not popular right now. #if USE_MEM_POOL connection_t *c = pool_alloc(&connection_pool); #else connection_t *c = malloc(sizeof(connection_t)); #endif // init request if (c) { if (request_init(&c->req, c) == ERROR) { #if USE_MEM_POOL pool_free(&connection_pool, c); #else free(c); #endif c = NULL; } } return c; } connection_t *connection_accept(int fd, struct sockaddr_in *paddr) { connection_t *c = connection_init(); assert(c != NULL); if (c == NULL) { // malloc fail connection_close(c); return NULL; } /* fill in connection_t */ c->fd = fd; if (paddr) c->saddr = *paddr; c->active_time = time(NULL); set_fd_nonblocking(c->fd); connection_set_nodelay(c); if (connection_register(c) == ERROR) { connection_close(c); return NULL; } if (lotos_epoll_add(epoll_fd, c, EPOLLIN | EPOLLET, &c->event) == ERROR) { connection_close(c); return NULL; } #ifndef NDEBUG char ip_addr[32]; uint16_t port; get_internet_address(ip_addr, 32, &port, &c->saddr); lotos_log(LOG_INFO, "fd: %2d %s:%u\n", fd, ip_addr, port); lotos_log(LOG_INFO, "malloc %p %d\n", c, heap_size); (void)heap_print; /* Unused. Silent compiler warning. */ #endif return c; } int connection_register(connection_t *c) { if (heap_size >= MAX_CONNECTION) { return ERROR; } return heap_insert(c); } void connection_unregister(connection_t *c) { assert(heap_size >= 1); lotos_connections[c->heap_idx] = lotos_connections[heap_size - 1]; lotos_connections[c->heap_idx]->heap_idx = c->heap_idx; heap_size--; heap_bubble_down(c->heap_idx); } static inline void connection_free(connection_t *c) { if (c) { buffer_free(c->req.ib); c->req.ib = NULL; buffer_free(c->req.ob); c->req.ob = NULL; #if USE_MEM_POOL pool_free(&connection_pool, c); #else free(c); #endif } } /* close connection, free memory */ int connection_close(connection_t *c) { if (c == NULL) return OK; /* * explicitly delete fd from epoll_set, see `man 7 epoll` Q6 * Q6 Will closing a file descriptor cause it to be removed from all epoll * sets automatically? */ lotos_epoll_del(epoll_fd, c, 0, NULL); close(c->fd); connection_unregister(c); connection_free(c); return OK; } void connection_prune() { while (heap_size > 0) { connection_t *c = lotos_connections[0]; if (time(NULL) - c->active_time >= server_config.timeout) { #ifndef NDEBUG // heap_print(); lotos_log(LOG_INFO, "prune %p %d\n", c, heap_size); #endif connection_close(c); } else break; } } inline bool connecion_is_expired(connection_t *c) { return (time(NULL) - c->active_time > server_config.timeout); } void connecion_set_reactivated(connection_t *c) { c->active_time = time(NULL); heap_bubble_down(c->heap_idx); } void connecion_set_expired(connection_t *c) { c->active_time = 0; // very old time heap_bubble_up(c->heap_idx); } int set_fd_nonblocking(int fd) { int flag = fcntl(fd, F_GETFL, 0); ABORT_ON(flag == ERROR, "fcntl: F_GETFL"); flag |= O_NONBLOCK; ABORT_ON(fcntl(fd, F_SETFL, flag) == ERROR, "fcntl: FSETFL"); return 0; } ================================================ FILE: src/connection.h ================================================ #ifndef _CONNECTION_H__ #define _CONNECTION_H__ #include "buffer.h" #include "misc.h" #include "request.h" #include #include #include #include #define MAX_CONNECTION (10240) struct connection { int fd; /* connection fildes */ struct epoll_event event; /* epoll event */ struct sockaddr_in saddr; /* IP socket address */ time_t active_time; /* connection accpet time */ int heap_idx; /* idx at lotos_connections */ request_t req; /* request */ }; typedef struct connection connection_t; extern connection_t *connection_accept(int fd, struct sockaddr_in *paddr); extern connection_t *connection_init(); extern int connection_register(connection_t *c); extern void connection_unregister(connection_t *c); extern int connection_close(connection_t *c); extern void connection_prune(); extern bool connecion_is_expired(connection_t *c); extern void connecion_set_reactivated(connection_t *c); extern void connecion_set_expired(connection_t *c); extern int set_fd_nonblocking(int fd); #endif ================================================ FILE: src/dict.c ================================================ #include "dict.h" #include "misc.h" #include "ssstr.h" #include #include #include // https://www.byvoid.com/zhs/blog/string-hash-compare static unsigned int ssstr_hash_sdbm(const ssstr_t *key) { const char *str = key->str; unsigned int hash = 0; while (str != (key->str + key->len)) { // equivalent to: hash = 65599*hash + (*str++); hash = (*str++) + (hash << 6) + (hash << 16) - hash; } return (hash & 0x7FFFFFFF); } #define LOTOS_HASH(key) (ssstr_hash_sdbm(key) & DICT_MASK_SIZE) void dict_put(dict_t *dict, const ssstr_t *key, void *val) { unsigned int hash = LOTOS_HASH(key); dict_node_t *p = dict->table[hash]; dict_node_t *q = malloc(sizeof(dict_node_t)); // p == NULL or p != NULL, the same dict_node_init(q, (ssstr_t *)key, val, p); dict->table[hash] = q; dict->used++; } /* cause we can put NULL as a valid value, so found_flag is necessary */ void *dict_get(dict_t *dict, const ssstr_t *key, bool *found_flag) { if (found_flag != NULL) *found_flag = FALSE; unsigned int hash = LOTOS_HASH(key); dict_node_t *p = dict->table[hash]; while (p != NULL) { if (ssstr_cmp(&p->k, key) == 0) { if (found_flag != NULL) *found_flag = TRUE; return p->v; } p = p->next; } return NULL; } void dict_free(dict_t *d) { if (d == NULL) return; int i; for (i = 0; i < sizeof(d->table) / sizeof(dict_node_t *); i++) { dict_node_t *p = d->table[i]; while (p != NULL) { dict_node_t *q = p->next; free(p); d->used--; p = q; } } assert(d->used == 0); } ================================================ FILE: src/dict.h ================================================ #ifndef _DICT_H__ #define _DICT_H__ /** * dict used with a small amount of data, no need to rehash */ #include "misc.h" #include "ssstr.h" #include #define DICT_MASK_SIZE (0xFF) typedef struct dict_node { ssstr_t k; void *v; struct dict_node *next; } dict_node_t; typedef struct { dict_node_t *table[DICT_MASK_SIZE + 1]; unsigned int size_mask; unsigned int used; } dict_t; static inline void dict_init(dict_t *d) { memset(d, 0, sizeof(dict_t)); d->size_mask = DICT_MASK_SIZE; } static inline void dict_node_init(dict_node_t *node, ssstr_t *s, void *v, dict_node_t *next) { node->k = *s; node->v = v; node->next = next; } extern void dict_put(dict_t *dict, const ssstr_t *key, void *val); extern void *dict_get(dict_t *dict, const ssstr_t *key, bool *found_flag); extern void dict_free(dict_t *d); #endif ================================================ FILE: src/http_parser.c ================================================ #include "http_parser.h" #include "buffer.h" #include "misc.h" #include #include #define STR2_EQ(p, q) ((p)[0] == (q)[0] && (p)[1] == (q)[1]) #define STR3_EQ(p, q) (STR2_EQ(p, q) && (p)[2] == (q)[2]) #define STR4_EQ(p, q) (STR2_EQ(p, q) && STR2_EQ(p + 2, q + 2)) #define STR5_EQ(p, q) (STR2_EQ(p, q) && STR3_EQ(p + 2, q + 2)) #define STR6_EQ(p, q) (STR3_EQ(p, q) && STR3_EQ(p + 3, q + 3)) #define STR7_EQ(p, q) (STR3_EQ(p, q) && STR4_EQ(p + 3, q + 3)) #define HEADER_SET(header, str_beg, str_end) \ do { \ assert(str_beg <= str_end); \ (header)->str = str_beg; \ (header)->len = (str_end) - (str_beg); \ } while (0) static int parse_method(char *begin, char *end); static int parse_url(char *begin, char *end, parse_archive *ar); /* parse request line */ /** * @return * OK: request line OK * AGAIN: parse to the end of buffer, but no complete request line * INVALID_REQUEST request not valid */ int parse_request_line(buffer_t *b, parse_archive *ar) { char ch; char *p; for (p = ar->next_parse_pos; p < buffer_end(b); p++) { ch = *p; switch (ar->state) { case S_RL_BEGIN: switch (ch) { case 'a' ... 'z': case 'A' ... 'Z': /* save current pos, which is METHOD beginning */ ar->method_begin = p; ar->state = S_RL_METHOD; break; default: return INVALID_REQUEST; } // end S_RL_BEGIN case S_RL_METHOD: switch (ch) { case 'a' ... 'z': case 'A' ... 'Z': break; case ' ': { ar->method = parse_method(ar->method_begin, p); if (ar->method == HTTP_INVALID) return INVALID_REQUEST; ar->state = S_RL_SP_BEFORE_URL; break; default: return INVALID_REQUEST; } } // end S_RL_METHOD break; case S_RL_SP_BEFORE_URL: switch (ch) { case ' ': case '\t': /* ease parser, '\t' is also considered valid */ break; case '\r': case '\n': return INVALID_REQUEST; default: ar->state = S_RL_URL; ar->url_begin = p; } break; case S_RL_URL: switch (ch) { case ' ': case '\t': // assume url part has been received completely ar->state = S_RL_SP_BEFORE_VERSION; int url_status = parse_url(ar->url_begin, p, ar); if (url_status) return url_status; break; case '\r': case '\n': return INVALID_REQUEST; default: break; } // end S_RL_URL break; case S_RL_SP_BEFORE_VERSION: switch (ch) { case ' ': case '\t': break; case 'H': case 'h': ar->state = S_RL_VERSION_H; break; default: return INVALID_REQUEST; } // end S_RL_SP_BEFORE_RL_VERSION break; case S_RL_VERSION_H: switch (ch) { case 'T': case 't': ar->state = S_RL_VERSION_HT; break; default: return INVALID_REQUEST; } // end S_RL_VERSION_H break; case S_RL_VERSION_HT: switch (ch) { case 'T': case 't': ar->state = S_RL_VERSION_HTT; break; default: return INVALID_REQUEST; } // end S_RL_VERSION_HT break; case S_RL_VERSION_HTT: switch (ch) { case 'P': case 'p': ar->state = S_RL_VERSION_HTTP; break; default: return INVALID_REQUEST; } // end S_RL_VERSION_HTT break; case S_RL_VERSION_HTTP: switch (ch) { case '/': ar->state = S_RL_VERSION_HTTP_SLASH; break; default: return INVALID_REQUEST; } // end S_RL_VERSION_HTTP break; case S_RL_VERSION_HTTP_SLASH: switch (ch) { case '0' ... '9': ar->version.http_major = ar->version.http_major * 10 + ch - '0'; ar->state = S_RL_VERSION_MAJOR; break; default: return INVALID_REQUEST; } // end S_RL_VERSION_HTTP_SLASH break; case S_RL_VERSION_MAJOR: switch (ch) { case '0' ... '9': ar->version.http_major = ar->version.http_major * 10 + ch - '0'; if (ar->version.http_major > 1) return INVALID_REQUEST; break; case '.': ar->state = S_RL_VERSION_DOT; break; default: return INVALID_REQUEST; } // end S_RL_VERSION_MAJOR break; case S_RL_VERSION_DOT: switch (ch) { case '0' ... '9': ar->version.http_minor = ar->version.http_minor * 10 + ch - '0'; ar->state = S_RL_VERSION_MINOR; break; default: return INVALID_REQUEST; } // end S_RL_VERSION_DOT break; case S_RL_VERSION_MINOR: switch (ch) { case '0' ... '9': ar->version.http_minor = ar->version.http_minor * 10 + ch - '0'; if (ar->version.http_minor > 1) return INVALID_REQUEST; break; case '\r': ar->state = S_RL_CR_AFTER_VERSION; break; default: return INVALID_REQUEST; } // end S_RL_VERSION_MINOR break; case S_RL_CR_AFTER_VERSION: switch (ch) { case '\n': ar->state = S_RL_LF_AFTER_VERSION; /* parse request line done*/ goto done; default: return INVALID_REQUEST; } // end S_RL_CR_AFTER_VERSION break; } // end switch(state) } // end for ar->next_parse_pos = buffer_end(b); return AGAIN; done:; ar->next_parse_pos = p + 1; ar->state = S_HD_BEGIN; return OK; } /* parse header line */ /** * @return * OK: one header line has been parsed * AGAIN: parse to the end of buffer, but no complete header * INVALID_REQUEST request not valid * CRLF_LINE: `\r\n`, which means all headers have been parsed * */ int parse_header_line(buffer_t *b, parse_archive *ar) { char ch, *p; // NOTE: isCRLF_LINE must be an attribute of ar, cannot be a local variable. // see the change in fix commit. // bool isCRLF_LINE = TRUE; for (p = ar->next_parse_pos; p < buffer_end(b); p++) { ch = *p; switch (ar->state) { case S_HD_BEGIN: switch (ch) { case 'A' ... 'Z': case 'a' ... 'z': case '0' ... '9': case '-': ar->state = S_HD_NAME; ar->header_line_begin = p; ar->isCRLF_LINE = FALSE; break; case '\r': ar->state = S_HD_CR_AFTER_VAL; ar->isCRLF_LINE = TRUE; break; case ' ': case '\t': break; default: return INVALID_REQUEST; } break; case S_HD_NAME: switch (ch) { case 'A' ... 'Z': case 'a' ... 'z': case '0' ... '9': case '-': break; case ':': ar->state = S_HD_COLON; ar->header_colon_pos = p; break; default: return INVALID_REQUEST; } break; case S_HD_COLON: switch (ch) { case ' ': case '\t': ar->state = S_HD_SP_BEFORE_VAL; break; case '\r': case '\n': return INVALID_REQUEST; default: ar->state = S_HD_VAL; ar->header_val_begin = p; break; } break; case S_HD_SP_BEFORE_VAL: switch (ch) { case ' ': case '\t': break; case '\r': case '\n': return INVALID_REQUEST; default: ar->state = S_HD_VAL; ar->header_val_begin = p; break; } break; case S_HD_VAL: switch (ch) { case '\r': ar->header_val_end = p; ar->state = S_HD_CR_AFTER_VAL; break; case '\n': ar->state = S_HD_LF_AFTER_VAL; break; default: break; } break; case S_HD_CR_AFTER_VAL: switch (ch) { case '\n': ar->state = S_HD_LF_AFTER_VAL; goto done; default: return INVALID_REQUEST; } break; } // end switch state } // end for ar->next_parse_pos = buffer_end(b); return AGAIN; done:; ar->next_parse_pos = p + 1; ar->state = S_HD_BEGIN; ar->num_headers++; /* put header name and val into header[2] */ HEADER_SET(&ar->header[0], ar->header_line_begin, ar->header_colon_pos); HEADER_SET(&ar->header[1], ar->header_val_begin, ar->header_val_end); return ar->isCRLF_LINE ? CRLF_LINE : OK; } static int parse_method(char *begin, char *end) { int len = end - begin; switch (len) { case 3: if (STR3_EQ(begin, "GET")) { return HTTP_GET; } else if (STR3_EQ(begin, "PUT")) { return HTTP_PUT; } else { return HTTP_INVALID; } break; case 4: if (STR4_EQ(begin, "POST")) { return HTTP_POST; } else if (STR4_EQ(begin, "HEAD")) { return HTTP_HEAD; } else { return HTTP_INVALID; } break; case 6: if (STR6_EQ(begin, "DELETE")) { return HTTP_DELETE; } else { return HTTP_INVALID; } break; default: return HTTP_INVALID; } return HTTP_INVALID; } /** * Some Samples: * * /abc/def/ * /unp.pdf * /unp.pdf/ `dir` * /abc.def/set?name=chen&val=newbie * /video/life.of.pi.BlueRay.rmvb * /video/life.of.pi.BlueRay `dir` */ /* simple parse url */ static int parse_url(char *begin, char *end, parse_archive *ar) { ar->request_url_string.str = begin; ar->request_url_string.len = end - begin; assert(ar->request_url_string.len >= 0); int curr_state = S_URL_BEGIN; char ch; char *p = begin; for (; p != end + 1; p++) { ch = *p; switch (curr_state) { case S_URL_BEGIN: switch (ch) { case '/': curr_state = S_URL_ABS_PATH; break; default: return ERROR; } break; case S_URL_ABS_PATH: switch (ch) { case ' ': ar->url.abs_path.str = begin; ar->url.abs_path.len = p - begin; ar->url.query_string.str = p; ar->url.query_string.len = 0; curr_state = S_URL_END; break; case '?': ar->url.abs_path.str = begin; ar->url.abs_path.len = p - begin; begin = p + 1; curr_state = S_URL_QUERY; break; default: break; } break; case S_URL_QUERY: switch (ch) { case ' ': ar->url.query_string.str = begin; ar->url.query_string.len = p - begin; curr_state = S_URL_END; default: break; } break; case S_URL_END: goto parse_extension; } // end switch(curr_state) } // end for parse_extension:; // directory extension will be corrected in `request_handle_request_line` char *abs_path_end = ar->url.abs_path.str + ar->url.abs_path.len; for (p = abs_path_end; p != ar->url.abs_path.str; p--) { if (*p == '.') { ar->url.mime_extension.str = p + 1; ar->url.mime_extension.len = abs_path_end - p - 1; break; } else if (*p == '/') break; } return OK; } int parse_header_body_identity(buffer_t *b, parse_archive *ar) { if (ar->content_length <= 0) return OK; // not that complicated, using `next_parse_pos` to indicate where to parse size_t received = buffer_end(b) - ar->next_parse_pos; ar->body_received += received; #ifndef NDEBUG printf("%s %d\n", __FUNCTION__, __LINE__); printf("%s %lu\n", ar->next_parse_pos, received); #endif ar->next_parse_pos = buffer_end(b); if (ar->body_received >= ar->content_length) { // full data recv return OK; } return AGAIN; // will conitinue to recv until full data recv or conn timeout } ================================================ FILE: src/http_parser.h ================================================ #ifndef _PARSER_H__ #define _PARSER_H__ #include "buffer.h" #include "misc.h" #include "ssstr.h" #include /* RFC2616 */ /* Ref: https://www.w3.org/Protocols/rfc2616/rfc2616.html */ /* This is a simple implementation */ /** * * Request = Request-Line ; Section 5.1 (( general-header ; Section 4.5 | request-header ; Section 5.3 | entity-header ) CRLF) ; Section 7.1 CRLF [ message-body ] ; Section 4.3 */ /** * Request-Line = Method SP Request-URI SP HTTP-Version CRLF */ /** * Ref: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.2 * http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]] */ /** * request-header = Accept ; Section 14.1 | Accept-Charset ; Section 14.2 | Accept-Encoding ; Section 14.3 | Accept-Language ; Section 14.4 | Authorization ; Section 14.8 | Expect ; Section 14.20 | From ; Section 14.22 | Host ; Section 14.23 | If-Match ; Section 14.24 | If-Modified-Since ; Section 14.25 | If-None-Match ; Section 14.26 | If-Range ; Section 14.27 | If-Unmodified-Since ; Section 14.28 | Max-Forwards ; Section 14.31 | Proxy-Authorization ; Section 14.34 | Range ; Section 14.35 | Referer ; Section 14.36 | TE ; Section 14.39 | User-Agent ; Section 14.43 */ #define INVALID_REQUEST (-1) #define CRLF_LINE (2) #define MAX_ELEMENT_SIZE (2048) /* basic http method */ typedef enum { HTTP_DELETE, HTTP_GET, HTTP_HEAD, HTTP_POST, HTTP_PUT, HTTP_INVALID, } http_method; /* HTTP protocol version */ typedef struct { unsigned short http_major; unsigned short http_minor; } http_version; /* Request URL */ typedef struct { ssstr_t abs_path; ssstr_t query_string; ssstr_t mime_extension; } req_url; /* parser state used in fsm */ typedef enum { /* request line states */ S_RL_BEGIN = 0, S_RL_METHOD, S_RL_SP_BEFORE_URL, S_RL_URL, S_RL_SP_BEFORE_VERSION, S_RL_VERSION_H, S_RL_VERSION_HT, S_RL_VERSION_HTT, S_RL_VERSION_HTTP, S_RL_VERSION_HTTP_SLASH, S_RL_VERSION_MAJOR, S_RL_VERSION_DOT, S_RL_VERSION_MINOR, S_RL_CR_AFTER_VERSION, S_RL_LF_AFTER_VERSION, /* header states */ S_HD_BEGIN, S_HD_NAME, S_HD_COLON, S_HD_SP_BEFORE_VAL, S_HD_VAL, S_HD_CR_AFTER_VAL, S_HD_LF_AFTER_VAL, /* url states */ S_URL_BEGIN, S_URL_ABS_PATH, S_URL_QUERY, S_URL_END, } parser_state; // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding typedef enum { TE_IDENTITY = 0, TE_CHUNKED, TE_COMPRESS, TE_DEFLATE, TE_GZIP, } transfer_encoding_t; /* some of the request headers we may parse */ typedef struct { ssstr_t cache_control; ssstr_t connection; ssstr_t date; ssstr_t transfer_encoding; ssstr_t accept; ssstr_t accept_charset; ssstr_t accept_encoding; ssstr_t accept_language; ssstr_t cookie; ssstr_t host; ssstr_t if_modified_since; ssstr_t if_unmodified_since; ssstr_t max_forwards; ssstr_t range; ssstr_t referer; ssstr_t user_agent; ssstr_t content_length; } request_headers_t; typedef struct { /* parsed request line result */ http_method method; http_version version; ssstr_t request_url_string; req_url url; /* parsed header lines result */ bool keep_alive; /* connection keep alive */ int content_length; /* request body content_length */ int transfer_encoding; /* affect body recv strategy */ request_headers_t req_headers; int num_headers; ssstr_t header[2]; /* store header every time `parse_header_line` */ /* preserve buffer_t state, so when recv new data, we can keep parsing */ char *next_parse_pos; /* parser position in buffer_t */ int state; /* parser state */ /* private members, do not modify !!! */ char *method_begin; char *url_begin; char *header_line_begin; char *header_colon_pos; char *header_val_begin; char *header_val_end; size_t body_received; int buffer_sent; bool isCRLF_LINE; bool response_done; bool err_req; } parse_archive; static inline void parse_archive_init(parse_archive *ar, buffer_t *b) { memset(ar, 0, sizeof(parse_archive)); ar->next_parse_pos = b->buf; ar->isCRLF_LINE = TRUE; ar->content_length = -1; // no Content-Length header } extern int parse_request_line(buffer_t *b, parse_archive *ar); extern int parse_header_line(buffer_t *b, parse_archive *ar); extern int parse_header_body_identity(buffer_t *b, parse_archive *ar); #endif ================================================ FILE: src/lotos_epoll.c ================================================ #include "lotos_epoll.h" #include "connection.h" #include "misc.h" #include struct epoll_event lotos_events[MAX_EVENTS]; int lotos_epoll_create(int flags) { return epoll_create1(flags); } int lotos_epoll_add(int epoll_fd, connection_t *c, uint32_t events, struct epoll_event *pev) { FILL_EPOLL_EVENT(pev, c, events); return epoll_ctl(epoll_fd, EPOLL_CTL_ADD, c->fd, pev); } int lotos_epoll_mod(int epoll_fd, connection_t *c, uint32_t events, struct epoll_event *pev) { FILL_EPOLL_EVENT(pev, c, events); return epoll_ctl(epoll_fd, EPOLL_CTL_MOD, c->fd, pev); } int lotos_epoll_del(int epoll_fd, connection_t *c, uint32_t events, struct epoll_event *pev) { (void)pev; /* Unused. Silent compiler warning. */ return epoll_ctl(epoll_fd, EPOLL_CTL_DEL, c->fd, NULL); } inline int lotos_epoll_wait(int epoll_fd, struct epoll_event *events, int max_events, int timeout) { return epoll_wait(epoll_fd, events, max_events, timeout); } ================================================ FILE: src/lotos_epoll.h ================================================ #ifndef _LOTOS_EPOLL_H__ #define _LOTOS_EPOLL_H__ #include "connection.h" #include "misc.h" #include #define MAX_EVENTS (10240) #define FILL_EPOLL_EVENT(pev, pconn, e_events) \ do { \ struct epoll_event *ev = pev; \ ev->data.ptr = pconn; \ ev->events = e_events; \ } while (0) #define CONN_IS_IN(c) ((c)->event.events & EPOLLIN) #define CONN_IS_OUT(c) ((c)->event.events & EPOLLOUT) extern struct epoll_event lotos_events[MAX_EVENTS]; // global extern int lotos_epoll_create(int flags); extern int lotos_epoll_add(int epoll_fd, connection_t *restrict c, uint32_t events, struct epoll_event *pev); extern int lotos_epoll_mod(int epoll_fd, connection_t *restrict c, uint32_t events, struct epoll_event *pev); extern int lotos_epoll_del(int epoll_fd, connection_t *restrict c, uint32_t events, struct epoll_event *pev); extern int lotos_epoll_wait(int epoll_fd, struct epoll_event *events, int max_events, int timeout); static inline int connection_enable_in(int epoll_fd, connection_t *c) { if (CONN_IS_IN(c)) return OK; c->event.events |= EPOLLIN; return epoll_ctl(epoll_fd, EPOLL_CTL_MOD, c->fd, &c->event); } static inline int connection_disable_in(int epoll_fd, connection_t *c) { if (!CONN_IS_IN(c)) return OK; c->event.events &= ~EPOLLIN; return epoll_ctl(epoll_fd, EPOLL_CTL_MOD, c->fd, &c->event); } static inline int connection_enable_out(int epoll_fd, connection_t *c) { if (CONN_IS_OUT(c)) return OK; c->event.events |= EPOLLOUT; return epoll_ctl(epoll_fd, EPOLL_CTL_MOD, c->fd, &c->event); } static inline int connection_disable_out(int epoll_fd, connection_t *c) { if (!CONN_IS_OUT(c)) return OK; c->event.events &= ~EPOLLOUT; return epoll_ctl(epoll_fd, EPOLL_CTL_MOD, c->fd, &c->event); } #endif ================================================ FILE: src/main.c ================================================ #include "lotos_epoll.h" #include "misc.h" #include "server.h" #include #include #include #include #include #include #include #include static void usage(const char *executable) { printf("Usage: %s -r html_root_dir [-p port] " "[-t timeout] [-w worker_num] [-d (debug mode)]\n", executable); } int main(int argc, char *argv[]) { if (argc < 2 || config_parse(argc, argv) != OK) { usage(argv[0]); exit(ERROR); } if (server_config.debug) { goto work; } int nworker = 0; while (TRUE) { if (nworker >= server_config.worker) { int status; waitpid(-1, &status, 0); // wait all children if (WIFEXITED(status)) raise(SIGINT); lotos_log(LOG_ERR, "a worker exit, please restart..."); raise(SIGINT); } pid_t pid = fork(); ABORT_ON(pid == -1, "fork"); if (pid == 0) { // child break; // child ends up in loop and directly goto `work` } nworker++; } work:; int nfds; int i; server_setup(server_config.port); while (TRUE) { /** * nfds is number of file descriptors ready for the requested I/O or zero * if timeout */ nfds = lotos_epoll_wait(epoll_fd, lotos_events, MAX_EVENTS, 20); if (nfds == ERROR) { // if not caused by signal, cannot recover ERR_ON(errno != EINTR, "lotos_epoll_wait"); } for (i = 0; i < nfds; i++) { struct epoll_event *curr_event = lotos_events + i; int fd = *((int *)(curr_event->data.ptr)); if (fd == listen_fd) { // accept connection server_accept(listen_fd); } else { // handle connection connection_t *c = curr_event->data.ptr; int status; assert(c != NULL); if (connecion_is_expired(c)) continue; if (curr_event->events & EPOLLIN) { // recv status = request_handle(c); if (status == ERROR) connecion_set_expired(c); else connecion_set_reactivated(c); } if (curr_event->events & EPOLLOUT) { // send status = response_handle(c); if (status == ERROR) connecion_set_expired(c); else connecion_set_reactivated(c); } } // else } // for loop /* prune expired connections */ connection_prune(); } // while close(epoll_fd); server_shutdown(); return OK; } ================================================ FILE: src/mem_pool.c ================================================ #include "mem_pool.h" #include "misc.h" #include #include static inline void *last_block(mem_pool_t *pool) { return ((char *)pool->blocks) + (pool->block_num - 1) * pool->block_size; } int pool_create(mem_pool_t *pool, size_t nmemb, size_t size) { memset(pool, 0, sizeof(mem_pool_t)); pool->block_num = nmemb; pool->block_size = size > sizeof(void *) ? size : sizeof(void *); pool->blocks = calloc(pool->block_num, pool->block_size); if (pool->blocks == NULL) { lotos_log(LOG_ERR, "memory pool creating failed...exit"); exit(1); } pool->next = pool->blocks; /** * similar to what we do in ACM, pseudo linkedlist * * * suppose block_size = 30 and block_num = 4, the `blocks` memory: * 0 30 60 90 120 * ——————————————————————————————————————————— * |30 |60 |90 |NULL | * ——————————————————————————————————————————— * */ char *char_ptr = pool->blocks; void **ptr_ptr; for (; char_ptr < (char *)last_block(pool);) { ptr_ptr = (void **)char_ptr; *ptr_ptr = (char_ptr += pool->block_size); } ptr_ptr = (void **)char_ptr; *ptr_ptr = NULL; return OK; } void pool_destroy(mem_pool_t *pool) { if (pool->blocks) { free(pool->blocks); pool->blocks = NULL; } } void *pool_alloc(mem_pool_t *pool) { if (pool->block_allocated >= pool->block_num) return NULL; void *cur = pool->next; void **ptr_ptr = cur; if (ptr_ptr) { pool->next = *ptr_ptr; pool->block_allocated++; } return cur; } void pool_free(mem_pool_t *pool, void *ptr) { if (ptr == NULL) return; void **ptr_ptr = ptr; *ptr_ptr = pool->next; pool->next = ptr; pool->block_allocated--; } ================================================ FILE: src/mem_pool.h ================================================ #ifndef _MEM_POOL_H__ #define _MEM_POOL_H__ #include typedef struct { size_t block_num; /* num of blocks expected to alloc */ size_t block_size; /* size of each block */ size_t block_allocated; /* num of allocated blocks */ void *next; /* next block to be allocated */ void *blocks; /* store data */ } mem_pool_t; int pool_create(mem_pool_t *pool, size_t nmemb, size_t size); void pool_destroy(mem_pool_t *pool); void *pool_alloc(mem_pool_t *pool); void pool_free(mem_pool_t *pool, void *ptr); #endif ================================================ FILE: src/misc.c ================================================ #include "misc.h" #include #include #include void lotos_log(int priority, const char *format, ...) { FILE *fp; const char *color_prefix = NULL; switch (priority) { case LOG_ERR: fp = stderr; color_prefix = "\033[1;31m"; break; case LOG_INFO: fp = stdout; color_prefix = "\033[1;32m"; break; default: fp = stdout; } time_t t = time(NULL); struct tm tm = *localtime(&t); fprintf(fp, "[%4d-%02d-%02d %02d:%02d:%02d] ", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); color_prefix == NULL ? 0 : fprintf(fp, "%s", color_prefix); va_list args; va_start(args, format); vfprintf(fp, format, args); fprintf(fp, "\033[0m\n"); fflush(fp); va_end(args); } ================================================ FILE: src/misc.h ================================================ #ifndef _MISC_H__ #define _MISC_H__ #include #include typedef enum { TRUE = 1, FALSE = 0 } bool; #define ERROR (-1) #define OK (0) #define AGAIN (1) #define CRLF "\r\n" #define ABORT_ON(cond, msg) \ do { \ if (cond) { \ fprintf(stderr, "%s: %d: ", __FILE__, __LINE__); \ perror(msg); \ abort(); \ } \ } while (0) #define ERR_ON(cond, msg) \ do { \ if (cond) { \ fprintf(stderr, "%s: %d: ", __FILE__, __LINE__); \ perror(msg); \ } \ } while (0) enum { LOG_ERR, LOG_INFO }; extern void lotos_log(int priority, const char *format, ...); #endif ================================================ FILE: src/request.c ================================================ #define _GNU_SOURCE #include "buffer.h" #include "connection.h" #include "dict.h" #include "http_parser.h" #include "lotos_epoll.h" #include "misc.h" #include "response.h" #include "server.h" #include "ssstr.h" #include #include #include #include #include #include #include #include #include #include #include static int request_handle_request_line(request_t *r); static int request_handle_headers(request_t *r); static int request_handle_body(request_t *r); static int response_handle_send_buffer(struct request *r); static int response_handle_send_file(struct request *r); static int response_assemble_buffer(struct request *r); static int response_assemble_err_buffer(struct request *r, int status_code); typedef int (*header_handle_method)(request_t *, size_t); /* handlers for specific http headers */ static int request_handle_hd_base(request_t *r, size_t offset); static int request_handle_hd_connection(request_t *r, size_t offset); static int request_handle_hd_content_length(request_t *r, size_t offset); static int request_handle_hd_transfer_encoding(request_t *r, size_t offset); typedef struct { ssstr_t hd; header_handle_method func; size_t offset; } header_func; #define XX(hd, hd_mn, func) \ { SSSTR(hd), func, offsetof(request_headers_t, hd_mn) } static header_func hf_list[] = { XX("accept", accept, request_handle_hd_base), XX("accept-charset", accept_charset, request_handle_hd_base), XX("accept-encoding", accept_encoding, request_handle_hd_base), XX("accept-language", accept_language, request_handle_hd_base), XX("cache-control", cache_control, request_handle_hd_base), XX("content-length", content_length, request_handle_hd_content_length), XX("connection", connection, request_handle_hd_connection), XX("cookie", cookie, request_handle_hd_base), XX("date", date, request_handle_hd_base), XX("host", host, request_handle_hd_base), XX("if-modified-since", if_modified_since, request_handle_hd_base), XX("if-unmodified-since", if_unmodified_since, request_handle_hd_base), XX("max-forwards", max_forwards, request_handle_hd_base), XX("range", range, request_handle_hd_base), XX("referer", referer, request_handle_hd_base), XX("transfer-encoding", transfer_encoding, request_handle_hd_transfer_encoding), XX("user-agent", user_agent, request_handle_hd_base), }; #undef XX dict_t header_handler_dict; void header_handler_dict_init() { dict_init(&header_handler_dict); size_t nsize = sizeof(hf_list) / sizeof(hf_list[0]); int i; for (i = 0; i < nsize; i++) { dict_put(&header_handler_dict, &hf_list[i].hd, (void *)&hf_list[i]); } } void header_handler_dict_free() { dict_free(&header_handler_dict); } int request_init(request_t *r, connection_t *c) { assert(r != NULL); memset(r, 0, sizeof(request_t)); r->c = c; r->ib = buffer_init(); r->ob = buffer_init(); if (r->ib == NULL || r->ob == NULL) return ERROR; parse_archive_init(&r->par, r->ib); r->resource_fd = -1; r->status_code = 200; r->req_handler = request_handle_request_line; r->res_handler = response_handle_send_buffer; return OK; } /* when connection keep-alive, reuse request_t in connection_t */ int request_reset(request_t *r) { buffer_t *ib = r->ib; buffer_t *ob = r->ob; connection_t *c = r->c; memset(r, 0, sizeof(request_t)); r->ib = ib; r->ob = ob; r->c = c; buffer_clear(ib); buffer_clear(ob); parse_archive_init(&r->par, r->ib); r->resource_fd = -1; r->status_code = 200; r->req_handler = request_handle_request_line; r->res_handler = response_handle_send_buffer; return OK; } static int request_recv(request_t *r) { char buf[BUFSIZ]; int len; while (TRUE) { len = recv(r->c->fd, buf, sizeof(buf), 0); // https://stackoverflow.com/questions/2416944/can-read-function-on-a-connected-socket-return-zero-bytes if (len == 0) { /* if client close, will read EOF */ return OK; } if (len == ERROR) { if (errno != EAGAIN) { lotos_log(LOG_ERR, "recv: %s", strerror(errno)); return ERROR; } else return AGAIN; /* does not have data now */ } buffer_cat(r->ib, buf, len); /* append new data to buffer */ } return AGAIN; } int request_handle(connection_t *c) { request_t *r = &c->req; int status = request_recv(r); if (status == ERROR || status == OK) /* error or client close */ return ERROR; /** * parse request main process: * * do { * status = parse_request_line() * status = parse_header() * *status = parse_body() * } while (status != error && !parse_done) * * if status == AGAIN, then exit `request_handle` and try to recv in the next * loop. * if status == ERROR, then exit `request_handle` and expire this connection * */ do { status = r->req_handler(r); } while (r->req_handler != NULL && status == OK); return status; } static int request_handle_request_line(request_t *r) { int status; status = parse_request_line(r->ib, &r->par); if (status == AGAIN) // not a complete request line return AGAIN; if (status != OK) { // INVALID_REQUEST // send error response to client return response_assemble_err_buffer(r, 400); } // status = OK now parse_archive *ar = &(r->par); /* check http version */ if (ar->version.http_major > 1 || ar->version.http_minor > 1) { // send 505 error response to client return response_assemble_err_buffer(r, 505); } ar->keep_alive = (ar->version.http_major == 1 && ar->version.http_minor == 1); // make `relative_path` a c-style string, really ugly.... char *p = ar->url.abs_path.str; while (*p != ' ' && *p != '?') p++; *p = '\0'; /* check abs_path */ const char *relative_path = NULL; relative_path = ar->url.abs_path.len == 1 && ar->url.abs_path.str[0] == '/' ? "./" : ar->url.abs_path.str + 1; int fd = openat(server_config.rootdir_fd, relative_path, O_RDONLY); if (fd == ERROR) { // send 404 error response to client return response_assemble_err_buffer(r, 404); } struct stat st; fstat(fd, &st); if (S_ISDIR(st.st_mode)) { // substitute dir to index.html // fd is a dir fildes int html_fd = openat(fd, "index.html", O_RDONLY); close(fd); if (fd == ERROR) { // send 404 error response to client return response_assemble_err_buffer(r, 404); } fd = html_fd; fstat(fd, &st); ssstr_set(&ar->url.mime_extension, "html"); } r->resource_fd = fd; r->resource_size = st.st_size; r->req_handler = request_handle_headers; return OK; } static int request_handle_headers(request_t *r) { int status; buffer_t *b = r->ib; parse_archive *ar = &r->par; while (TRUE) { status = parse_header_line(b, ar); switch (status) { /* not a complete header */ case AGAIN: return AGAIN; /* header invalid */ case INVALID_REQUEST: // send error response to client return response_assemble_err_buffer(r, 400); /* all headers completed */ case CRLF_LINE: goto header_done; /* a header completed */ case OK: ssstr_tolower(&r->par.header[0]); // handle header individually header_func *hf = dict_get(&header_handler_dict, &r->par.header[0], NULL); if (hf == NULL) break; header_handle_method func = hf->func; size_t offset = hf->offset; if (func != NULL) { status = func(r, offset); if (status != OK) return OK; } break; } } header_done:; r->req_handler = request_handle_body; return OK; } static int request_handle_body(request_t *r) { int status; buffer_t *b = r->ib; parse_archive *ar = &r->par; switch (r->par.transfer_encoding) { case TE_IDENTITY: status = parse_header_body_identity(b, ar); break; default: status = ERROR; break; } switch (status) { case AGAIN: return AGAIN; case OK: connection_disable_in(epoll_fd, r->c); connection_enable_out(epoll_fd, r->c); r->req_handler = NULL; // body parse done !!! no more handlers response_assemble_buffer(r); return OK; default: // send error response to client return response_assemble_err_buffer(r, 501); } return OK; } /* save header value into the proper position of parse_archive.req_headers */ int request_handle_hd_base(request_t *r, size_t offset) { parse_archive *ar = &r->par; ssstr_t *header = (ssstr_t *)(((char *)(&ar->req_headers)) + offset); *header = ar->header[1]; return OK; } int request_handle_hd_connection(request_t *r, size_t offset) { request_handle_hd_base(r, offset); ssstr_t *connection = &(r->par.req_headers.connection); if (ssstr_caseequal(connection, "keep-alive")) { r->par.keep_alive = TRUE; } else if (ssstr_caseequal(connection, "close")) { r->par.keep_alive = FALSE; } else { // send error response to client return response_assemble_err_buffer(r, 400); } return OK; } int request_handle_hd_content_length(request_t *r, size_t offset) { request_handle_hd_base(r, offset); ssstr_t *content_length = &(r->par.req_headers.content_length); int len = atoi(content_length->str); if (len <= 0) { // send error response to client return response_assemble_err_buffer(r, 400); } r->par.content_length = len; return OK; } // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding // https://imququ.com/post/content-encoding-header-in-http.html int request_handle_hd_transfer_encoding(request_t *r, size_t offset) { request_handle_hd_base(r, offset); ssstr_t *transfer_encoding = &(r->par.req_headers.transfer_encoding); if (ssstr_caseequal(transfer_encoding, "chunked")) { // may implement, send error response to client r->par.transfer_encoding = TE_CHUNKED; return response_assemble_err_buffer(r, 501); } else if (ssstr_caseequal(transfer_encoding, "compress")) { // send error response to client r->par.transfer_encoding = TE_COMPRESS; return response_assemble_err_buffer(r, 501); } else if (ssstr_caseequal(transfer_encoding, "deflate")) { // send error response to client r->par.transfer_encoding = TE_DEFLATE; return response_assemble_err_buffer(r, 501); } else if (ssstr_caseequal(transfer_encoding, "gzip")) { // send error response to client r->par.transfer_encoding = TE_GZIP; return response_assemble_err_buffer(r, 501); } else if (ssstr_caseequal(transfer_encoding, "identity")) { r->par.transfer_encoding = TE_IDENTITY; return response_assemble_err_buffer(r, 501); } else { // send error response to client return response_assemble_err_buffer(r, 400); } return OK; } /** * Return: * OK: all data sent * AGAIN: haven't sent all data * ERROR: error */ static int response_send(request_t *r) { int len = 0; buffer_t *b = r->ob; char *buf_beg; while (TRUE) { buf_beg = b->buf + r->par.buffer_sent; len = send(r->c->fd, buf_beg, buffer_end(b) - buf_beg, 0); if (len == 0) { buffer_clear(b); r->par.buffer_sent = 0; return OK; } else if (len < 0) { if (errno == EAGAIN) return AGAIN; lotos_log(LOG_ERR, "send: %s", strerror(errno)); return ERROR; } r->par.buffer_sent += len; } return OK; } int response_handle(struct connection *c) { request_t *r = &c->req; int status; do { status = r->res_handler(r); } while (status == OK && r->par.response_done != TRUE); if (r->par.response_done) { // response done if (r->par.keep_alive) { request_reset(r); connection_disable_out(epoll_fd, c); connection_enable_in(epoll_fd, c); } else return ERROR; // make connection expired } return status; } int response_handle_send_buffer(struct request *r) { int status; status = response_send(r); if (status != OK) { return status; } else { if (r->resource_fd != -1) { r->res_handler = response_handle_send_file; return OK; } r->par.response_done = TRUE; connection_disable_out(epoll_fd, r->c); return OK; } return OK; } int response_handle_send_file(struct request *r) { int len; connection_t *c = r->c; while (TRUE) { // zero copy, make it faster len = sendfile(c->fd, r->resource_fd, NULL, r->resource_size); if (len == 0) { r->par.response_done = TRUE; close(r->resource_fd); return OK; } else if (len < 0) { if (errno == EAGAIN) { return AGAIN; } lotos_log(LOG_ERR, "sendfile: %s", strerror(errno)); return ERROR; } } return OK; } int response_assemble_buffer(struct request *r) { response_append_status_line(r); response_append_date(r); response_append_server(r); response_append_content_type(r); response_append_content_length(r); response_append_connection(r); response_append_crlf(r); return OK; } int response_assemble_err_buffer(struct request *r, int status_code) { r->req_handler = NULL; r->par.err_req = TRUE; r->status_code = status_code; response_append_status_line(r); response_append_date(r); response_append_server(r); response_append_content_type(r); response_append_content_length(r); r->par.keep_alive = FALSE; response_append_connection(r); response_append_crlf(r); // add err page html r->ob = buffer_cat_cstr(r->ob, err_page_render_buf()); connection_disable_in(epoll_fd, r->c); connection_enable_out(epoll_fd, r->c); r->par.response_done = TRUE; return OK; } ================================================ FILE: src/request.h ================================================ #ifndef _REQUEST_H__ #define _REQUEST_H__ #include "buffer.h" #include "connection.h" #include "http_parser.h" #include "misc.h" struct request { struct connection *c; /* belonged connection */ buffer_t *ib; /* request buffer */ buffer_t *ob; /* response buffer */ parse_archive par; /* parse_archive */ int resource_fd; /* resource fildes */ int resource_size; /* resource size */ int status_code; /* response status code */ int (*req_handler)(struct request *); /* request handler for rl, hd, bd */ int (*res_handler)(struct request *); /* response handler for hd bd */ }; typedef struct request request_t; extern int request_init(request_t *r, struct connection *c); extern int request_reset(request_t *r); extern int request_handle(struct connection *c); extern int response_handle(struct connection *c); void header_handler_dict_init(); void header_handler_dict_free(); #endif ================================================ FILE: src/response.c ================================================ #define _GNU_SOURCE #include "response.h" #include "buffer.h" #include "connection.h" #include "dict.h" #include "misc.h" #include "request.h" #include "server.h" #include "ssstr.h" #include #include #include #include #include #include #define ERR_HEADER_MAX_LEN (BUFSIZ) static const char *status_table[512]; static ssstr_t mime_list[][2] = { {SSSTR("word"), SSSTR("application/msword")}, {SSSTR("pdf"), SSSTR("application/pdf")}, {SSSTR("zip"), SSSTR("application/zip")}, {SSSTR("js"), SSSTR("application/javascript")}, {SSSTR("gif"), SSSTR("image/gif")}, {SSSTR("jpeg"), SSSTR("image/jpeg")}, {SSSTR("jpg"), SSSTR("image/jpeg")}, {SSSTR("png"), SSSTR("image/png")}, {SSSTR("css"), SSSTR("text/css")}, {SSSTR("html"), SSSTR("text/html")}, {SSSTR("htm"), SSSTR("text/html")}, {SSSTR("txt"), SSSTR("text/plain")}, {SSSTR("xml"), SSSTR("text/xml")}, {SSSTR("svg"), SSSTR("image/svg+xml")}, {SSSTR("mp4"), SSSTR("video/mp4")}, }; static dict_t mime_dict; void mime_dict_init() { size_t nsize = sizeof(mime_list) / sizeof(mime_list[0]); int i; dict_init(&mime_dict); for (i = 0; i < nsize; i++) { dict_put(&mime_dict, &mime_list[i][0], &mime_list[i][1]); } } void mime_dict_free() { dict_free(&mime_dict); } void status_table_init() { memset(status_table, 0, sizeof(status_table)); #define XX(num, name, string) status_table[num] = #num " " #string; HTTP_STATUS_MAP(XX); #undef XX } static err_page_t err_page; int err_page_init() { err_page_t *ep = &err_page; // open error.html ep->err_page_fd = openat(server_config.rootdir_fd, "error.html", O_RDONLY); ABORT_ON(ep->err_page_fd == ERROR, "openat"); struct stat st; fstat(ep->err_page_fd, &st); ep->raw_page_size = st.st_size; ep->rendered_err_page = buffer_init(ep->raw_page_size + ERR_HEADER_MAX_LEN); ABORT_ON(ep->rendered_err_page == NULL, "buffer_init"); // mmap file to memory ep->raw_err_page = mmap(NULL, ep->raw_page_size, PROT_READ, MAP_SHARED, ep->err_page_fd, 0); ABORT_ON(ep->raw_err_page == NULL, "mmap"); return OK; } void err_page_free() { err_page_t *ep = &err_page; buffer_free(ep->rendered_err_page); // munmap munmap((void *)ep->raw_err_page, ep->raw_page_size); close(ep->err_page_fd); } inline char *err_page_render_buf() { return err_page.rendered_err_page->buf; } void response_append_status_line(struct request *r) { buffer_t *b = r->ob; if (r->par.version.http_minor == 1) { r->ob = buffer_cat_cstr(b, "HTTP/1.1 "); } else { r->ob = buffer_cat_cstr(b, "HTTP/1.0 "); } // status const char *status_str = status_table[r->status_code]; if (status_str != NULL) r->ob = buffer_cat_cstr(b, status_str); r->ob = buffer_cat_cstr(b, CRLF); } void response_append_date(struct request *r) { buffer_t *b = r->ob; time_t now = time(NULL); struct tm *tm = gmtime(&now); size_t len = strftime(b->buf + buffer_len(b), b->free, "Date: %a, %d %b %Y %H:%M:%S GMT" CRLF, tm); b->len += len; b->free -= len; } void response_append_server(struct request *r) { buffer_t *b = r->ob; r->ob = buffer_cat_cstr(b, "Server: "); r->ob = buffer_cat_cstr(b, SERVER_NAME CRLF); } void response_append_content_type(struct request *r) { buffer_t *b = r->ob; parse_archive *ar = &r->par; ssstr_t content_type; if (ar->err_req) { content_type = SSSTR("text/html"); goto done; } ssstr_t *v = dict_get(&mime_dict, &ar->url.mime_extension, NULL); if (v != NULL) { content_type = *v; } else { content_type = SSSTR("text/html"); } done:; r->ob = buffer_cat_cstr(b, "Content-Type: "); r->ob = buffer_cat_cstr(b, content_type.str); r->ob = buffer_cat_cstr(b, CRLF); } void response_append_content_length(struct request *r) { buffer_t *b = r->ob; char cl[128]; int len; buffer_t *rendered_err_page = err_page.rendered_err_page; // modify content_length when sending err page if (r->par.err_req) { len = snprintf(rendered_err_page->buf, rendered_err_page->free + rendered_err_page->len, err_page.raw_err_page, status_table[r->status_code]); err_page.rendered_page_size = len; } else { len = r->resource_size; } sprintf(cl, "Content-Length: %d" CRLF, len); r->ob = buffer_cat_cstr(b, cl); } void response_append_connection(struct request *r) { buffer_t *b = r->ob; ssstr_t connection; if (r->par.keep_alive) { connection = SSSTR("Connection: keep-alive"); } else { connection = SSSTR("Connection: close"); } r->ob = buffer_cat_cstr(b, connection.str); r->ob = buffer_cat_cstr(b, CRLF); } void response_append_crlf(struct request *r) { buffer_t *b = r->ob; r->ob = buffer_cat_cstr(b, CRLF); } ================================================ FILE: src/response.h ================================================ #ifndef _RESPONSE_H__ #define _RESPONSE_H__ #include "buffer.h" #include "connection.h" #include "request.h" #define SERVER_NAME "lotos/0.1" // https://github.com/nodejs/http-parser/blob/b11de0f5c65bcc1b906f85f4df58883b0c133e7b/http_parser.h#L233 /* status code */ #define HTTP_STATUS_MAP(XX) \ XX(100, CONTINUE, Continue) \ XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \ XX(102, PROCESSING, Processing) \ XX(200, OK, OK) \ XX(201, CREATED, Created) \ XX(202, ACCEPTED, Accepted) \ XX(203, NON_AUTHORITATIVE_INFORMATION, Non - Authoritative Information) \ XX(204, NO_CONTENT, No Content) \ XX(205, RESET_CONTENT, Reset Content) \ XX(206, PARTIAL_CONTENT, Partial Content) \ XX(207, MULTI_STATUS, Multi - Status) \ XX(208, ALREADY_REPORTED, Already Reported) \ XX(226, IM_USED, IM Used) \ XX(300, MULTIPLE_CHOICES, Multiple Choices) \ XX(301, MOVED_PERMANENTLY, Moved Permanently) \ XX(302, FOUND, Found) \ XX(303, SEE_OTHER, See Other) \ XX(304, NOT_MODIFIED, Not Modified) \ XX(305, USE_PROXY, Use Proxy) \ XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \ XX(308, PERMANENT_REDIRECT, Permanent Redirect) \ XX(400, BAD_REQUEST, Bad Request) \ XX(401, UNAUTHORIZED, Unauthorized) \ XX(402, PAYMENT_REQUIRED, Payment Required) \ XX(403, FORBIDDEN, Forbidden) \ XX(404, NOT_FOUND, Not Found) \ XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \ XX(406, NOT_ACCEPTABLE, Not Acceptable) \ XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \ XX(408, REQUEST_TIMEOUT, Request Timeout) \ XX(409, CONFLICT, Conflict) \ XX(410, GONE, Gone) \ XX(411, LENGTH_REQUIRED, Length Required) \ XX(412, PRECONDITION_FAILED, Precondition Failed) \ XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \ XX(414, URI_TOO_LONG, URI Too Long) \ XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \ XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \ XX(417, EXPECTATION_FAILED, Expectation Failed) \ XX(421, MISDIRECTED_REQUEST, Misdirected Request) \ XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \ XX(423, LOCKED, Locked) \ XX(424, FAILED_DEPENDENCY, Failed Dependency) \ XX(426, UPGRADE_REQUIRED, Upgrade Required) \ XX(428, PRECONDITION_REQUIRED, Precondition Required) \ XX(429, TOO_MANY_REQUESTS, Too Many Requests) \ XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \ XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \ XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \ XX(501, NOT_IMPLEMENTED, Not Implemented) \ XX(502, BAD_GATEWAY, Bad Gateway) \ XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \ XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \ XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \ XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \ XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \ XX(508, LOOP_DETECTED, Loop Detected) \ XX(510, NOT_EXTENDED, Not Extended) \ XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) typedef enum { #define XX(num, name, string) HTTP_STATUS_##name = num, HTTP_STATUS_MAP(XX) #undef XX } http_status; extern void mime_dict_init(); extern void mime_dict_free(); extern void status_table_init(); typedef struct { int err_page_fd; /* fildes of err page */ const char *raw_err_page; /* raw data of err page file */ size_t raw_page_size; /* size of err page file */ buffer_t *rendered_err_page; /* buffer contains err msg */ size_t rendered_page_size; /* size of err page file */ } err_page_t; extern int err_page_init(); extern void err_page_free(); extern char *err_page_render_buf(); extern void response_append_status_line(struct request *r); extern void response_append_date(struct request *r); extern void response_append_server(struct request *r); extern void response_append_content_type(struct request *r); extern void response_append_content_length(struct request *r); extern void response_append_connection(struct request *r); extern void response_append_crlf(struct request *r); #endif ================================================ FILE: src/server.c ================================================ #define _GNU_SOURCE #include "connection.h" #include "lotos_epoll.h" #include "misc.h" #include "response.h" #include "server.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include config_t server_config = { .port = 8888, .debug = FALSE, .timeout = 60, .worker = 4, .rootdir = NULL, .rootdir_fd = -1, }; int epoll_fd = -1; int listen_fd = -1; #if USE_MEM_POOL mem_pool_t connection_pool; #endif static void sigint_handler(int signum); static int make_server_socket(uint16_t port, int backlog); static int add_listen_fd(); int config_parse(int argc, char *argv[]) { int c; while ((c = getopt(argc, argv, "p:dt:w:r:")) != -1) { switch (c) { case 'p': server_config.port = atoi(optarg); break; case 'd': server_config.debug = TRUE; break; case 't': server_config.timeout = atoi(optarg); break; case 'w': server_config.worker = atoi(optarg); if (server_config.worker > sysconf(_SC_NPROCESSORS_ONLN)) { fprintf(stderr, "Config ERROR: worker num greater than cpu available cores.\n"); return ERROR; } break; case 'r': server_config.rootdir = optarg; break; default: return ERROR; } } DIR *dirp = NULL; if (server_config.rootdir != NULL && (dirp = opendir(server_config.rootdir)) != NULL) { closedir(dirp); server_config.rootdir_fd = open(server_config.rootdir, O_RDONLY); ERR_ON(server_config.rootdir_fd == ERROR, server_config.rootdir); return OK; } else { perror(server_config.rootdir); return ERROR; } } static void sigint_handler(int signum) { if (signum == SIGINT) { mime_dict_free(); header_handler_dict_free(); err_page_free(); #if USE_MEM_POOL pool_destroy(&connection_pool); #endif lotos_log(LOG_INFO, "lotos(PID: %u) exit...", getpid()); kill(-getpid(), SIGINT); exit(0); } } int server_setup(uint16_t port) { signal(SIGINT, sigint_handler); signal(SIGPIPE, SIG_IGN); // client close, server write will recv sigpipe mime_dict_init(); header_handler_dict_init(); status_table_init(); err_page_init(); #if USE_MEM_POOL pool_create(&connection_pool, 1024, sizeof(connection_t)); #endif listen_fd = make_server_socket(port, 1024); ABORT_ON(listen_fd == ERROR, "make_server_socket"); epoll_fd = lotos_epoll_create(0); ABORT_ON(epoll_fd == ERROR, "lotos_epoll_create"); ABORT_ON(add_listen_fd() == ERROR, "add_listen_fd"); return OK; } int server_shutdown() { return close(listen_fd); } int server_accept(int listen_fd) { int conn_fd; static struct sockaddr_in saddr; socklen_t saddrlen = sizeof(struct sockaddr_in); while ((conn_fd = accept(listen_fd, &saddr, &saddrlen)) != ERROR) { connection_accept(conn_fd, &saddr); } return 0; } static int make_server_socket(uint16_t port, int backlog) { int listen_fd; struct sockaddr_in saddr; listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd == ERROR) return ERROR; int enable = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); if (server_config.worker > 1) { // since linux 3.9 setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(enable)); } memset((void *)&saddr, 0, sizeof(saddr)); saddr.sin_family = AF_INET; saddr.sin_port = htons(port); saddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listen_fd, (struct sockaddr *)&saddr, sizeof(saddr)) != OK) return ERROR; if (listen(listen_fd, backlog) != OK) return ERROR; return listen_fd; } static int add_listen_fd() { set_fd_nonblocking(listen_fd); struct epoll_event ev; ev.data.ptr = &listen_fd; ev.events = EPOLLIN; return epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev); } int get_internet_address(char *host, int len, uint16_t *pport, struct sockaddr_in *paddr) { strncpy(host, inet_ntoa(paddr->sin_addr), len); *pport = ntohs(paddr->sin_port); return 0; } ================================================ FILE: src/server.h ================================================ #ifndef _SERVER_H__ #define _SERVER_H__ #include "mem_pool.h" #include "misc.h" #include #include #include typedef struct { uint16_t port; /* listen port */ bool debug; /* debug mode */ int timeout; /* connection expired time */ uint32_t worker; /* worker num */ char *rootdir; /* html root directory */ int rootdir_fd; /* fildes of rootdir */ } config_t; extern config_t server_config; extern int epoll_fd; /* epoll fd */ extern int listen_fd; /* server listen fd */ extern int config_parse(int argc, char *argv[]); /* parse command line options */ extern int server_setup(uint16_t port); /* bind and listen */ extern int server_shutdown(); /* server shutdown */ extern int server_accept(int listen_fd); /* accpet all connections */ extern int get_internet_address(char *host, int len, uint16_t *pport, struct sockaddr_in *paddr); /* get ip/port info */ #if USE_MEM_POOL extern mem_pool_t connection_pool; #endif #endif ================================================ FILE: src/ssstr.c ================================================ #include "ssstr.h" #include "misc.h" #include #include #include void ssstr_print(const ssstr_t *s) { if (s == NULL || s->str == NULL) return; int i; for (i = 0; i < s->len; i++) { printf("%c", s->str[i]); fflush(stdout); } printf("\n"); } void ssstr_tolower(ssstr_t *s) { int i; for (i = 0; i < s->len; i++) { s->str[i] = tolower(s->str[i]); } } int ssstr_cmp(const ssstr_t *l, const ssstr_t *r) { if (l == r || (l->str == r->str && l->len == r->len)) return 0; if (l->str == NULL) return -1; if (r->str == NULL) return 1; int llen = l->len; int rlen = r->len; int minlen = llen > rlen ? rlen : llen; int i; for (i = 0; i < minlen; i++) { if (l->str[i] < r->str[i]) return -1; else if (l->str[i] > r->str[i]) return 1; } return (rlen == llen) ? 0 : ((llen < rlen) ? -1 : 1); } bool ssstr_equal(const ssstr_t *s, const char *cstr) { ssstr_t s2; ssstr_set(&s2, cstr); return ssstr_cmp(s, &s2) == 0 ? TRUE : FALSE; } ================================================ FILE: src/ssstr.h ================================================ /** * simple static c-style string, used with buffer_t or constant c string, * no need to copy memory, just save time */ #ifndef _SSSTR_H__ #define _SSSTR_H__ #include "misc.h" #include #include typedef struct { char *str; int len; } ssstr_t; /** * const char *p = "hello"; * SSSTR("hello") ✅ * SSSTR(p)❌ */ #define SSSTR(cstr) \ (ssstr_t) { cstr, sizeof(cstr) - 1 } static inline void ssstr_init(ssstr_t *s) { s->str = NULL; s->len = 0; } static inline void ssstr_set(ssstr_t *s, const char *cstr) { s->str = (char *)cstr; s->len = strlen(cstr); } extern void ssstr_print(const ssstr_t *s); extern void ssstr_tolower(ssstr_t *s); extern int ssstr_cmp(const ssstr_t *l, const ssstr_t *r); extern bool ssstr_equal(const ssstr_t *s, const char *cstr); static inline bool ssstr_caseequal(ssstr_t *s, const char *cstr) { return strncasecmp(s->str, cstr, strlen(cstr)) == 0 ? TRUE : FALSE; } #endif ================================================ FILE: src/test/Makefile ================================================ CFLAGS=-g -std=c99 -Wall OPTFLAGS= OBJS=slow_client heap_test buffer_test ssstr_test dict_test parse_test\ mem_pool_test all : $(OBJS) slow_client : slow_client.c ../misc.c $(CC) $(CFLAGS) $^ -o $@ heap_test : heap_test.c $(CC) $(CFLAGS) $^ -o $@ buffer_test : buffer_test.c ../buffer.c $(CC) $(CFLAGS) $^ -o $@ ssstr_test : ssstr_test.c ../ssstr.c $(CC) $(CFLAGS) $^ -o $@ dict_test : dict_test.c ../dict.c ../ssstr.c $(CC) $(CFLAGS) $^ -o $@ parse_test : parse_test.c ../http_parser.c ../buffer.c ../ssstr.c $(CC) $(CFLAGS) $^ -o $@ mem_pool_test : mem_pool_test.c ../mem_pool.c ../misc.c $(CC) $(CFLAGS) $^ -o $@ test : ./heap_test ./buffer_test ./ssstr_test ./dict_test ./parse_test ./mem_pool_test clean : rm -f $(OBJS) ================================================ FILE: src/test/buffer_test.c ================================================ #include "../buffer.h" #include "minctest.h" #include #include void test1() { buffer_t *buffer = buffer_new(10); lok(buffer->len == 0 && buffer->free == 10); buffer = buffer_cat(buffer, "abc", 3); lok(buffer->len == 3 && buffer->free == 7); lsequal(buffer->buf, "abc"); buffer_free(buffer); } void test2() { buffer_t *buffer = buffer_new(10); lok(buffer->len == 0 && buffer->free == 10); buffer = buffer_cat(buffer, "aaaaabbbbb", 10); lok(buffer->len == 10 && buffer->free == 0); lsequal(buffer->buf, "aaaaabbbbb"); buffer_free(buffer); } void test3() { buffer_t *buffer = buffer_new(10); lok(buffer->len == 0 && buffer->free == 10); buffer = buffer_cat(buffer, "", 0); lok(buffer->len == 0 && buffer->free == 10); lsequal(buffer->buf, ""); buffer_free(buffer); } void test4() { buffer_t *buffer = buffer_new(10); lok(buffer->len == 0 && buffer->free == 10); buffer = buffer_cat(buffer, "abc", 3); lok(buffer->len == 3 && buffer->free == 7); buffer = buffer_cat(buffer, "defgh", 5); lok(buffer->len == 8 && buffer->free == 2); lsequal(buffer->buf, "abcdefgh"); buffer_free(buffer); } void test5() { buffer_t *buffer = buffer_new(10); lok(buffer->len == 0 && buffer->free == 10); buffer = buffer_cat(buffer, "abc", 3); lok(buffer->len == 3 && buffer->free == 7); buffer = buffer_cat(buffer, "defghijk", 8); lok(buffer->len == 11 && buffer->free == 11); lsequal(buffer->buf, "abcdefghijk"); buffer = buffer_cat_cstr(buffer, "cstr"); lok(buffer->len == 15 && buffer->free == 7); lsequal(buffer->buf, "abcdefghijkcstr"); buffer_free(buffer); } void test6() { buffer_t *buffer = buffer_init(); lok(buffer->len == 0 && buffer->free == BUFSIZ); static char buf[BUFFER_LIMIT]; memset(buf, 'x', sizeof(buf)); buffer = buffer_cat(buffer, "abc", 3); lok(buffer->len == 3 && buffer->free == BUFSIZ - 3); buffer = buffer_cat(buffer, buf, sizeof(buf)); lequal(buffer->len, BUFFER_LIMIT + 3); lequal(buffer->free, BUFFER_LIMIT); buffer_free(buffer); } void test7() { buffer_t *buffer = buffer_new(10); lok(buffer->len == 0 && buffer->free == 10); lok(buffer->buf == buffer_end(buffer)); buffer = buffer_cat(buffer, "abc", 3); lok(buffer->len == 3 && buffer->free == 7); lsequal(buffer->buf, "abc"); lequal('c', *(buffer_end(buffer) - 1)); lequal('b', *(buffer_end(buffer) - 2)); buffer_free(buffer); } int main(int argc, char const *argv[]) { lrun("test1", test1); lrun("test2", test2); lrun("test3", test3); lrun("test4", test4); lrun("test5", test5); lrun("test6", test6); lrun("test7", test7); lresults(); printf("\n\n"); return lfails != 0; } ================================================ FILE: src/test/dict_test.c ================================================ #include "../dict.h" #include "../ssstr.h" #include "minctest.h" #include #include // https://github.com/nodejs/http-parser/blob/b11de0f5c65bcc1b906f85f4df58883b0c133e7b/http_parser.h#L233 #define HTTP_STATUS_MAP(XX) \ XX(100, CONTINUE, Continue) \ XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \ XX(102, PROCESSING, Processing) \ XX(200, OK, OK) \ XX(201, CREATED, Created) \ XX(202, ACCEPTED, Accepted) \ XX(203, NON_AUTHORITATIVE_INFORMATION, Non - Authoritative Information) \ XX(204, NO_CONTENT, No Content) \ XX(205, RESET_CONTENT, Reset Content) \ XX(206, PARTIAL_CONTENT, Partial Content) \ XX(207, MULTI_STATUS, Multi - Status) \ XX(208, ALREADY_REPORTED, Already Reported) \ XX(226, IM_USED, IM Used) \ XX(300, MULTIPLE_CHOICES, Multiple Choices) \ XX(301, MOVED_PERMANENTLY, Moved Permanently) \ XX(302, FOUND, Found) \ XX(303, SEE_OTHER, See Other) \ XX(304, NOT_MODIFIED, Not Modified) \ XX(305, USE_PROXY, Use Proxy) \ XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \ XX(308, PERMANENT_REDIRECT, Permanent Redirect) \ XX(400, BAD_REQUEST, Bad Request) \ XX(401, UNAUTHORIZED, Unauthorized) \ XX(402, PAYMENT_REQUIRED, Payment Required) \ XX(403, FORBIDDEN, Forbidden) \ XX(404, NOT_FOUND, Not Found) \ XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \ XX(406, NOT_ACCEPTABLE, Not Acceptable) \ XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \ XX(408, REQUEST_TIMEOUT, Request Timeout) \ XX(409, CONFLICT, Conflict) \ XX(410, GONE, Gone) \ XX(411, LENGTH_REQUIRED, Length Required) \ XX(412, PRECONDITION_FAILED, Precondition Failed) \ XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \ XX(414, URI_TOO_LONG, URI Too Long) \ XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \ XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \ XX(417, EXPECTATION_FAILED, Expectation Failed) \ XX(421, MISDIRECTED_REQUEST, Misdirected Request) \ XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \ XX(423, LOCKED, Locked) \ XX(424, FAILED_DEPENDENCY, Failed Dependency) \ XX(426, UPGRADE_REQUIRED, Upgrade Required) \ XX(428, PRECONDITION_REQUIRED, Precondition Required) \ XX(429, TOO_MANY_REQUESTS, Too Many Requests) \ XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \ XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \ XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \ XX(501, NOT_IMPLEMENTED, Not Implemented) \ XX(502, BAD_GATEWAY, Bad Gateway) \ XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \ XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \ XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \ XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \ XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \ XX(508, LOOP_DETECTED, Loop Detected) \ XX(510, NOT_EXTENDED, Not Extended) \ XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) #define XX(num, name, string) num, int HTTP_STATUS_CODE[] = {HTTP_STATUS_MAP(XX)}; #undef XX #define XX(num, name, string) #string, const char *HTTP_STATUS_STRING[] = {HTTP_STATUS_MAP(XX)}; #undef XX size_t nsize = sizeof(HTTP_STATUS_CODE) / sizeof(int); dict_t dict; void make_dict() { dict_init(&dict); int i; for (i = 0; i < nsize; i++) { ssstr_t key; ssstr_set(&key, HTTP_STATUS_STRING[i]); dict_put(&dict, &key, (void *)(HTTP_STATUS_CODE + i)); } } void test1() { lequal(0, dict.size_mask & (dict.size_mask + 1)); lok(dict.used == nsize); } void test2() { ssstr_t key = SSSTR("Network Authentication Required"); bool found; int *val = dict_get(&dict, &key, &found); lequal(found, TRUE); lequal(511, *val); } void test3() { ssstr_t key = SSSTR("Permanent Redirect"); bool found; int *val = dict_get(&dict, &key, &found); lequal(found, TRUE); lequal(308, *val); } /** * dict->table[244] -> "Unauthorized" -> "Bad Request" -> 0x0 */ void test4() { ssstr_t key = SSSTR("Bad Request"); bool found; int *val = dict_get(&dict, &key, &found); lequal(found, TRUE); lequal(400, *val); } void test5() { ssstr_t key = SSSTR("Internal Server Error"); bool found; int *val = dict_get(&dict, &key, &found); lequal(found, TRUE); lequal(500, *val); } void test6() { ssstr_t key = SSSTR("Intrnal Server Error"); bool found; int *val = dict_get(&dict, &key, &found); lequal(found, FALSE); lok(NULL == val); } void test7() { ssstr_t key = SSSTR(""); bool found; int *val = dict_get(&dict, &key, &found); lequal(found, FALSE); lok(NULL == val); } void test8() { ssstr_t key = SSSTR("take it easy, chendotjs"); bool found; int *val = dict_get(&dict, &key, &found); lequal(found, FALSE); lok(NULL == val); } int main(int argc, char const *argv[]) { make_dict(); lrun("test1", test1); lrun("test2", test2); lrun("test3", test3); lrun("test4", test4); lrun("test5", test5); lrun("test6", test6); lrun("test7", test7); lrun("test8", test8); lresults(); dict_free(&dict); printf("\n\n"); return lfails != 0; } ================================================ FILE: src/test/heap_test.c ================================================ #include "../connection.h" #include "../misc.h" #include "minctest.h" #include #include /* lotos_connections is seen as a binary min heap */ connection_t *lotos_connections[MAX_CONNECTION] = {0}; static int heap_size = 0; #define LCHILD(x) (((x) << 1) + 1) #define RCHILD(x) (LCHILD(x) + 1) #define PARENT(x) ((x - 1) >> 1) #define INHEAP(n, x) (((-1) < (x)) && ((x) < (n))) inline static void c_swap(int x, int y) { lok(x >= 0 && x < heap_size && y >= 0 && y < heap_size); connection_t *tmp = lotos_connections[x]; lotos_connections[x] = lotos_connections[y]; lotos_connections[y] = tmp; // update heap_idx lotos_connections[x]->heap_idx = x; lotos_connections[y]->heap_idx = y; } /* used for inserting */ static void heap_bubble_up(int idx) { while (PARENT(idx) >= 0) { int fidx = PARENT(idx); // fidx is father of idx; connection_t *c = lotos_connections[idx]; connection_t *fc = lotos_connections[fidx]; if (c->active_time >= fc->active_time) break; c_swap(idx, fidx); idx = fidx; } } /* used for extracting or active_time update larger */ static void heap_bubble_down(int idx) { while (TRUE) { int proper_child; int lchild = INHEAP(heap_size, LCHILD(idx)) ? LCHILD(idx) : (heap_size + 1); int rchild = INHEAP(heap_size, RCHILD(idx)) ? RCHILD(idx) : (heap_size + 1); if (lchild > heap_size && rchild > heap_size) { // no children break; } else if (INHEAP(heap_size, lchild) && INHEAP(heap_size, rchild)) { proper_child = lotos_connections[lchild]->active_time < lotos_connections[rchild]->active_time ? lchild : rchild; } else if (lchild > heap_size) { proper_child = rchild; } else { proper_child = lchild; } // idx is the smaller than children if (lotos_connections[idx]->active_time <= lotos_connections[proper_child]->active_time) break; lok(INHEAP(heap_size, proper_child)); c_swap(idx, proper_child); idx = proper_child; } } static int heap_insert(connection_t *c) { if (heap_size >= MAX_CONNECTION) { return ERROR; } lotos_connections[heap_size++] = c; c->heap_idx = heap_size - 1; heap_bubble_up(heap_size - 1); return 0; } /****************************** TEST CASES **********************************/ static int test_bubble_up = 0; void TEST_BUBBLE_UP(int input[], int ans[], size_t n) { // init memset(lotos_connections, 0, sizeof(lotos_connections)); heap_size = 0; int *arr = input; connection_t *conn_arr = malloc(sizeof(connection_t) * n); ERR_ON(conn_arr == NULL, "malloc"); for (int i = 0; i < n; i++) { connection_t *c = conn_arr + i; c->active_time = arr[i]; heap_insert(c); } // TEST printf("%s %d:\n", "TEST_BUBBLE_UP CASE", test_bubble_up++); lok(heap_size == n); for (int i = 0; i < n; i++) { printf("{%d %lu} ", i, lotos_connections[i]->active_time); lok(ans[i] == lotos_connections[i]->active_time); } printf("\n"); free(conn_arr); } static int test_bubble_down = 0; void TEST_BUBBLE_DOWN(int input[], int ans[], size_t n, int pos, int nval) { // init memset(lotos_connections, 0, sizeof(lotos_connections)); heap_size = 0; int *arr = input; connection_t *conn_arr = malloc(sizeof(connection_t) * n); ERR_ON(conn_arr == NULL, "malloc"); for (int i = 0; i < n; i++) { connection_t *c = conn_arr + i; c->active_time = arr[i]; heap_insert(c); } // update value lotos_connections[pos]->active_time = nval; heap_bubble_down(pos); // TEST printf("%s %d:\n", "TEST_BUBBLE_DOWN CASE", test_bubble_down++); lok(heap_size == n); for (int i = 0; i < n; i++) { printf("{%d %lu} ", i, lotos_connections[i]->active_time); lok(ans[i] == lotos_connections[i]->active_time); } printf("\n"); free(conn_arr); } int main(int argc, char const *argv[]) { { int arr[] = {4, 1, 5, 9, 6, 0}; int ans[] = {0, 4, 1, 9, 6, 5}; TEST_BUBBLE_UP(arr, ans, sizeof(arr) / sizeof(int)); } { int arr[] = {5, 8, 12, 19, 28, 20, 15, 22, 3}; int ans[] = {3, 5, 12, 8, 28, 20, 15, 22, 19}; TEST_BUBBLE_UP(arr, ans, sizeof(arr) / sizeof(int)); } { int arr[] = {9, 12, 17, 30, 50, 20, 60, 65, 4, 19}; int ans[] = {4, 9, 17, 12, 19, 20, 60, 65, 30, 50}; TEST_BUBBLE_UP(arr, ans, sizeof(arr) / sizeof(int)); } printf("\n\n"); { int arr[] = {4, 1, 5, 9, 6, 0}; int ans[] = {1, 4, 5, 9, 6, 100}; TEST_BUBBLE_DOWN(arr, ans, sizeof(arr) / sizeof(int), 0, 100); } { int arr[] = {4, 1, 5, 9, 6, 0}; int ans[] = {0, 6, 1, 9, 100, 5}; TEST_BUBBLE_DOWN(arr, ans, sizeof(arr) / sizeof(int), 1, 100); } { int arr[] = {4, 1, 5, 9, 6, 0}; int ans[] = {0, 6, 1, 9, 8, 5}; TEST_BUBBLE_DOWN(arr, ans, sizeof(arr) / sizeof(int), 1, 8); } { int arr[] = {4, 1, 5, 9, 6, 0}; int ans[] = {0, 5, 1, 9, 6, 5}; TEST_BUBBLE_DOWN(arr, ans, sizeof(arr) / sizeof(int), 1, 5); } { int arr[] = {4, 1, 5, 9, 6, 0}; int ans[] = {0, 4, 1, 9, 7, 5}; TEST_BUBBLE_DOWN(arr, ans, sizeof(arr) / sizeof(int), 4, 7); } { int arr[] = {4, 1, 5, 9, 6, 0}; int ans[] = {0, 4, 1, 9, 6, 10}; TEST_BUBBLE_DOWN(arr, ans, sizeof(arr) / sizeof(int), 5, 10); } { int arr[] = {4, 1, 5, 9, 6, 0}; int ans[] = {0, 4, 3, 9, 6, 5}; TEST_BUBBLE_DOWN(arr, ans, sizeof(arr) / sizeof(int), 2, 3); } { int arr[] = {4, 1, 5, 9, 6, 0}; int ans[] = {0, 4, 5, 9, 6, 10}; TEST_BUBBLE_DOWN(arr, ans, sizeof(arr) / sizeof(int), 2, 10); } { int arr[] = {1}; int ans[] = {10}; TEST_BUBBLE_DOWN(arr, ans, sizeof(arr) / sizeof(int), 0, 10); } lresults(); printf("\n\n"); return lfails != 0; } ================================================ FILE: src/test/mem_pool_test.c ================================================ #include "../mem_pool.h" #include "minctest.h" #include #include void test1() { mem_pool_t pool; pool_create(&pool, 4, 20); void **q = pool.blocks; lok(*q == pool.blocks + 20); q = pool.blocks + 20; lok(*q == pool.blocks + 40); q = pool.blocks + 60; lok(*q == 0); pool_destroy(&pool); } void test2() { mem_pool_t pool; pool_create(&pool, 4, 20); lok(pool.block_num == 4); lok(pool.block_size == 20); lok(pool.block_allocated == 0); void *p; p = pool_alloc(&pool); lok(p == pool.blocks); lok(pool.block_allocated == 1); p = pool_alloc(&pool); lok(p == pool.blocks + 20); p = pool_alloc(&pool); lok(p == pool.blocks + 40); p = pool_alloc(&pool); lok(p == pool.blocks + 60); lok(pool.block_allocated == 4); p = pool_alloc(&pool); lok(p == NULL); lok(pool.block_allocated == 4); p = pool_alloc(&pool); lok(p == NULL); lok(pool.block_allocated == 4); pool_destroy(&pool); } void test3() { mem_pool_t pool; pool_create(&pool, 4, 20); lok(pool.block_num == 4); lok(pool.block_size == 20); lok(pool.block_allocated == 0); void *p; p = pool_alloc(&pool); lok(p == pool.blocks); lok(pool.block_allocated == 1); p = pool_alloc(&pool); lok(p == pool.blocks + 20); p = pool_alloc(&pool); lok(p == pool.blocks + 40); pool_free(&pool, p); lok(pool.block_allocated == 2); p = pool_alloc(&pool); lok(p == pool.blocks + 40); lok(pool.block_allocated == 3); p = pool_alloc(&pool); lok(p == pool.blocks + 60); lok(pool.block_allocated == 4); p = pool_alloc(&pool); lok(p == NULL); lok(pool.block_allocated == 4); p = pool_alloc(&pool); lok(p == NULL); lok(pool.block_allocated == 4); pool_destroy(&pool); } int main(int argc, char const *argv[]) { lrun("test1", test1); lrun("test2", test2); lrun("test3", test3); lresults(); printf("\n\n"); return lfails != 0; } ================================================ FILE: src/test/minctest.h ================================================ /* * * MINCTEST - Minimal C Test Library - 0.2.0 * * Copyright (c) 2014-2017 Lewis Van Winkle * * http://CodePlea.com * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgement in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. * */ /* * MINCTEST - Minimal testing library for C * * * Example: * * void test1() { * lok('a' == 'a'); * } * * void test2() { * lequal(5, 6); * lfequal(5.5, 5.6); * } * * int main() { * lrun("test1", test1); * lrun("test2", test2); * lresults(); * return lfails != 0; * } * * * * Hints: * All functions/variables start with the letter 'l'. * */ #ifndef __MINCTEST_H__ #define __MINCTEST_H__ #include #include #include #include /* How far apart can floats be before we consider them unequal. */ #ifndef LTEST_FLOAT_TOLERANCE #define LTEST_FLOAT_TOLERANCE 0.001 #endif /* Track the number of passes, fails. */ /* NB this is made for all tests to be in one file. */ static int ltests = 0; static int lfails = 0; /* Display the test results. */ #define lresults() do {\ if (lfails == 0) {\ printf("ALL TESTS PASSED (%d/%d)\n", ltests, ltests);\ } else {\ printf("SOME TESTS FAILED (%d/%d)\n", ltests-lfails, ltests);\ }\ } while (0) /* Run a test. Name can be any string to print out, test is the function name to call. */ #define lrun(name, test) do {\ const int ts = ltests;\ const int fs = lfails;\ const clock_t start = clock();\ printf("\t%-14s", name);\ test();\ printf("pass:%2d fail:%2d %4dms\n",\ (ltests-ts)-(lfails-fs), lfails-fs,\ (int)((clock() - start) * 1000 / CLOCKS_PER_SEC));\ } while (0) /* Assert a true statement. */ #define lok(test) do {\ ++ltests;\ if (!(test)) {\ ++lfails;\ printf("%s:%d error \n", __FILE__, __LINE__);\ }} while (0) /* Prototype to assert equal. */ #define lequal_base(equality, a, b, format) do {\ ++ltests;\ if (!(equality)) {\ ++lfails;\ printf("%s:%d ("format " != " format")\n", __FILE__, __LINE__, (a), (b));\ }} while (0) /* Assert two integers are equal. */ #define lequal(a, b)\ lequal_base((a) == (b), a, b, "%d") /* Assert two floats are equal (Within LTEST_FLOAT_TOLERANCE). */ #define lfequal(a, b)\ lequal_base(fabs((double)(a)-(double)(b)) <= LTEST_FLOAT_TOLERANCE\ && fabs((double)(a)-(double)(b)) == fabs((double)(a)-(double)(b)), (double)(a), (double)(b), "%f") /* Assert two strings are equal. */ #define lsequal(a, b)\ lequal_base(strcmp(a, b) == 0, a, b, "%s") #endif /*__MINCTEST_H__*/ ================================================ FILE: src/test/parse_test.c ================================================ #include "../buffer.h" #include "../http_parser.h" #include "../misc.h" #include "../ssstr.h" #include "minctest.h" #include #include void test_method1() { buffer_t *buffer = buffer_init(); parse_archive ar; parse_archive_init(&ar, buffer); buffer_cat(buffer, "POST / HTTP/1.0", 10); parse_request_line(buffer, &ar); lok(ar.method_begin == buffer->buf); lok(ar.next_parse_pos == buffer_end(buffer)); lequal(HTTP_POST, ar.method); lok(ssstr_equal(&ar.request_url_string, "/")); lok(ssstr_equal(&ar.url.abs_path, "/")); lok(ssstr_equal(&ar.url.query_string, "")); } /* so parse_request_line can be called many times when recv new data */ void test_method2() { buffer_t *buffer = buffer_init(); parse_archive ar; parse_archive_init(&ar, buffer); int status = -1; buffer_cat(buffer, "GE", 2); status = parse_request_line(buffer, &ar); lequal(AGAIN, status); lok(ar.method_begin == buffer->buf); lok(ar.next_parse_pos == buffer_end(buffer)); char next_buf[] = "T /s?wd=hello%20world"; buffer_cat(buffer, next_buf, strlen(next_buf)); status = parse_request_line(buffer, &ar); lequal(HTTP_GET, ar.method); lequal(AGAIN, status); buffer_cat(buffer, " ", 1); status = parse_request_line(buffer, &ar); lok(ssstr_equal(&ar.url.abs_path, "/s")); lok(ssstr_equal(&ar.url.query_string, "wd=hello%20world")); } /* valid request line */ void test_method3() { buffer_t *buffer = buffer_init(); parse_archive ar; parse_archive_init(&ar, buffer); int status = -1; char req_line[] = "GET /api/set/?wd=123abc HTTP/1.1\r\nHost:localhost:8888"; buffer_cat(buffer, req_line, strlen(req_line)); status = parse_request_line(buffer, &ar); lequal(HTTP_GET, ar.method); lequal(ar.version.http_major, 1); lequal(ar.version.http_minor, 1); lok(ssstr_equal(&ar.request_url_string, "/api/set/?wd=123abc")); lok(ssstr_equal(&ar.url.abs_path, "/api/set/")); lok(ssstr_equal(&ar.url.query_string, "wd=123abc")); lequal(OK, status); lok(ar.next_parse_pos[0] = 'H'); lok(ar.next_parse_pos[1] = 'o'); } /* invalid request line */ void test_method4() { buffer_t *buffer = buffer_init(); parse_archive ar; parse_archive_init(&ar, buffer); int status = -1; char req_line[] = "POST /api/set/?wd=123abc HTTP/01.10\r\nHost:localhost:8888"; buffer_cat(buffer, req_line, strlen(req_line)); status = parse_request_line(buffer, &ar); lequal(HTTP_POST, ar.method); lequal(ar.version.http_major, 1); lequal(ar.version.http_minor, 10); lok(ssstr_equal(&ar.request_url_string, "/api/set/?wd=123abc")); lequal(ERROR, status); } /* curl GET */ void test_method5() { buffer_t *buffer = buffer_init(); parse_archive ar; parse_archive_init(&ar, buffer); ar.state = S_HD_BEGIN; int status = -1; char req_line[] = "User-Agent: curl/7.18.0 (i486-pc-linux-gnu) " "libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1\r\n" "Host: 0.0.0.0=5000\r\n" "Accept: */*\r\n" "\r\n"; buffer_cat(buffer, req_line, strlen(req_line)); status = parse_header_line(buffer, &ar); lok(ssstr_equal(&ar.header[0], "User-Agent")); lok(ssstr_equal(&ar.header[1], "curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 " "OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1")); lequal(OK, status); status = parse_header_line(buffer, &ar); lok(ssstr_equal(&ar.header[0], "Host")); lok(ssstr_equal(&ar.header[1], "0.0.0.0=5000")); lequal(OK, status); status = parse_header_line(buffer, &ar); lok(ssstr_equal(&ar.header[0], "Accept")); lok(ssstr_equal(&ar.header[1], "*/*")); lequal(OK, status); status = parse_header_line(buffer, &ar); lequal(CRLF_LINE, status); } /* firefox GET */ void test_method6() { buffer_t *buffer = buffer_init(); parse_archive ar; parse_archive_init(&ar, buffer); int status = -1; char req_line[] = "GET /favicon.ico HTTP/1.1\r\n" "Host: 0.0.0.0=5000\r\n" "User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) " "Gecko/2008061015 Firefox/3.0\r\n" "Accept: " "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" "Accept-Language: en-us,en;q=0.5\r\n" "Accept-Encoding: gzip,deflate\r\n" "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" "Keep-Alive: 300\r\n" "Connection: keep-alive\r\n" "\r\n"; buffer_cat(buffer, req_line, strlen(req_line)); status = parse_request_line(buffer, &ar); lequal(HTTP_GET, ar.method); lequal(ar.version.http_major, 1); lequal(ar.version.http_minor, 1); lok(ssstr_equal(&ar.request_url_string, "/favicon.ico")); lok(ssstr_equal(&ar.url.abs_path, "/favicon.ico")); lok(ssstr_equal(&ar.url.query_string, "")); lok(ssstr_equal(&ar.url.mime_extension, "ico")); lequal(OK, status); status = parse_header_line(buffer, &ar); lok(ssstr_equal(&ar.header[0], "Host")); lok(ssstr_equal(&ar.header[1], "0.0.0.0=5000")); lequal(OK, status); status = parse_header_line(buffer, &ar); lok(ssstr_equal(&ar.header[0], "User-Agent")); lok(ssstr_equal(&ar.header[1], "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) " "Gecko/2008061015 Firefox/3.0")); lequal(OK, status); status = parse_header_line(buffer, &ar); lok(ssstr_equal(&ar.header[0], "Accept")); lok(ssstr_equal( &ar.header[1], "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")); lequal(OK, status); status = parse_header_line(buffer, &ar); lok(ssstr_equal(&ar.header[0], "Accept-Language")); lok(ssstr_equal(&ar.header[1], "en-us,en;q=0.5")); lequal(OK, status); status = parse_header_line(buffer, &ar); lok(ssstr_equal(&ar.header[0], "Accept-Encoding")); lok(ssstr_equal(&ar.header[1], "gzip,deflate")); lequal(OK, status); status = parse_header_line(buffer, &ar); lok(ssstr_equal(&ar.header[0], "Accept-Charset")); lok(ssstr_equal(&ar.header[1], "ISO-8859-1,utf-8;q=0.7,*;q=0.7")); lequal(OK, status); status = parse_header_line(buffer, &ar); lok(ssstr_equal(&ar.header[0], "Keep-Alive")); lok(ssstr_equal(&ar.header[1], "300")); lequal(OK, status); status = parse_header_line(buffer, &ar); lok(ssstr_equal(&ar.header[0], "Connection")); lok(ssstr_equal(&ar.header[1], "keep-alive")); lequal(OK, status); status = parse_header_line(buffer, &ar); lequal(CRLF_LINE, status); } int main(int argc, char const *argv[]) { lrun("test_method1", test_method1); lrun("test_method2", test_method2); lrun("test_method3", test_method3); lrun("test_method4", test_method4); lrun("test_method5", test_method5); lrun("test_method6", test_method6); lresults(); printf("\n\n"); return lfails != 0; } ================================================ FILE: src/test/slow_client.c ================================================ /** * ./slow_client 8888 [0-9] */ #define _GNU_SOURCE #include "../misc.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include const char *requests[] = { #define SLOW_CLIENT_GET 0 "GET / HTTP/1.1\r\n" "Host:127.0.0.1:8888\r\n" "User-Agent:SLOW_CLIENT\r\n" "\r\n", #define CURL_GET 1 "GET / HTTP/1.1\r\n" "User-Agent: curl/7.18.0 (i486-pc-linux-gnu) " "libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 " "libidn/1.1\r\n" "Host: 0.0.0.0=5000\r\n" "Accept: */*\r\n" "\r\n", #define FIREFOX_GET 2 "GET /favicon.ico HTTP/1.1\r\n" "Host: 0.0.0.0=5000\r\n" "User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) " "Gecko/2008061015 Firefox/3.0\r\n" "Accept: " "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" "Accept-Language: en-us,en;q=0.5\r\n" "Accept-Encoding: gzip,deflate\r\n" "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" "Keep-Alive: 300\r\n" "Connection: keep-alive\r\n" "\r\n", #define POST_IDENTITY_BODY_WORLD 3 "POST /post_identity_body_world?q=search#hey HTTP/1.1\r\n" "Accept: */*\r\n" "Transfer-Encoding: identity\r\n" "Content-Length: 5\r\n" "\r\n" "World", #define GET_FUNKY_CONTENT_LENGTH 4 "GET / HTTP/1.0\r\n" "conTENT-Length: 5\r\n" "\r\n" "HELLO", #define POST_CHUNKED_ALL_YOUR_BASE 5 "POST / HTTP/1.1\r\n" "Transfer-Encoding: chunked\r\n" "\r\n" "1e\r\nall your base are belong to us\r\n" "0\r\n" "\r\n", }; int connect_to_server(uint16_t port) { static const char *host = "127.0.0.1"; int fd = socket(AF_INET, SOCK_STREAM, 0); ABORT_ON(fd == -1, "socket"); struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); int status = inet_pton(AF_INET, host, &addr.sin_addr); ABORT_ON(status <= 0, "inet_pton"); status = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); ABORT_ON(status != 0, "connect"); return fd; } int main(int argc, char *argv[]) { int fd, opt; if (argc < 3) { printf("Usage: %s port [0-5]\n", argv[0]); return 1; } signal(SIGPIPE, SIG_IGN); fd = connect_to_server(atoi(argv[1])); opt = atoi(argv[2]); if (opt >= sizeof(requests) / sizeof(requests[0])) return 1; /* send begin */ for (size_t i = 0; i < strlen(requests[opt]); i++) { int ch = requests[opt][i]; if (-1 == send(fd, &ch, 1, 0)) { perror("send"); // probably broken pipe break; } printf("%c", ch); fflush(stdout); usleep(30 * 1000); } /* send end */ /* recv begin */ printf("\n\n***************Recv**************\n\n"); while (1) { int ch; int len = recv(fd, &ch, 1, 0); if (len != 1) { break; } printf("%c", ch); fflush(stdout); } /* recv end */ close(fd); return 0; } ================================================ FILE: src/test/ssstr_test.c ================================================ #include "../ssstr.h" #include "minctest.h" #include #include void test1() { ssstr_t s = SSSTR("hello"); lequal(5, s.len); lsequal("hello", s.str); } void test2() { ssstr_t s1 = SSSTR("hello"); ssstr_t s2 = SSSTR("hello"); lequal(0, ssstr_cmp(&s1, &s2)); s1 = SSSTR(""); s2 = SSSTR(""); lequal(0, ssstr_cmp(&s1, &s2)); } void test3() { char str1[] = "hello"; char str2[] = "hello"; ssstr_t s1 = SSSTR(str1); ssstr_t s2 = SSSTR(str2); lequal(0, ssstr_cmp(&s1, &s2)); } void test4() { char str1[] = "hello"; char str2[] = "hullo"; ssstr_t s1 = SSSTR(str1); ssstr_t s2 = SSSTR(str2); lequal(-1, ssstr_cmp(&s1, &s2)); } void test5() { char str1[] = "hello_world"; char str2[] = "hello"; ssstr_t s1 = SSSTR(str1); ssstr_t s2 = SSSTR(str2); lequal(1, ssstr_cmp(&s1, &s2)); lequal(11, s1.len); lequal(5, s2.len); } void test6() { char str1[] = "hello_world"; char str2[] = "hullo"; ssstr_t s1 = SSSTR(str1); ssstr_t s2 = SSSTR(str2); lequal(-1, ssstr_cmp(&s1, &s2)); } void test7() { char str1[] = ""; char str2[] = ""; ssstr_t s1 = SSSTR(str1); ssstr_t s2 = SSSTR(str2); lok(s1.str != s2.str); lequal(0, ssstr_cmp(&s1, &s2)); } void test8() { char str1[] = "hello"; char str2[] = "hello"; ssstr_t s1 = SSSTR(str1); lok(ssstr_equal(&s1, str2)); lok(ssstr_equal(&s1, "hello")); lok(!ssstr_equal(&s1, "hello_world")); lok(!ssstr_equal(&s1, "hullo")); } void test9() { char str1[] = "HellO"; char str2[] = "heLLo-World"; ssstr_t s1 = SSSTR(str1); ssstr_t s2 = SSSTR(str2); ssstr_tolower(&s1); ssstr_tolower(&s2); lok(ssstr_equal(&s1, "hello")); lok(ssstr_equal(&s2, "hello-world")); } void test10() { char str1[] = "HellO"; char str2[] = "heLLo-World"; ssstr_t s1 = SSSTR(str1); ssstr_t s2 = SSSTR(str2); lok(ssstr_caseequal(&s1, "hello")); lok(ssstr_caseequal(&s2, "hello-world")); } int main(int argc, char const *argv[]) { lrun("test1", test1); lrun("test2", test2); lrun("test3", test3); lrun("test4", test4); lrun("test5", test5); lrun("test6", test6); lrun("test7", test7); lrun("test8", test8); lrun("test9", test9); lrun("test10", test10); lresults(); printf("\n\n"); return lfails != 0; } ================================================ FILE: www/error.html ================================================ Error

%s

Sorry, the page you are looking for is currently unavailable.
Please try again later.

If you are the system administrator of this resource then you should check the error log for details.

Faithfully yours, lotos.

================================================ FILE: www/index.html ================================================ Welcome to lotos!

Welcome to lotos!

If you see this page, the lotos web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to https://github.com/chendotjs/lotos.

Thank you for using lotos.