C 语言是一门很基础的语言,程序员们对它推崇备至,虽然它是个人入门语言,但大学的 C 语言知道早已经还给了老师,C 的使用能够说是从头学起。javascript
以前一直在读书,看了《C Primer Plus》、《APUE》、《UNP》,第一本看完以后虽然对 C 的语法有了大概的了解,但是要说应用,还差得很远;后两本算是咬着牙翻完的,应用更不敢说,只是对概念有了基本的认识。php
咱们都知道,学一门语言,只看不写,很容易出现眼高手低,写代码无处下手的状况,因而终于在下班和周末挤出时间,准备写一个小项目。正好最近在看 nginx 服务器与 php sapi 相关的知识,因而考虑以 nginx 的思想,写一个相似的简化版 web 服务器。css
项目最终的成果不敢保证,像上次写的 PHP 框架,在原理通透,技术要点掌握以后只剩下功能完善和代码堆叠,也就没有继续下去的欲望了,因而太监了。。。 可是跟着学习和理解一遍必定会有很大收获,这点是能保证的。 另一直写同一系列的东西会让我有一种负担感,并且偏底层的东西也须要不少时间去学习,这一系列可能会间隔更新,欢迎关注。html
最后附上项目 GitHub 地址:请点我java
以 nginx 的思想来考虑本服务器架构,初步考虑以下图:python
固然 php 进程也能够替换为其余的脚本语言,能够更改源码中的 command 变量实现。nginx
服务器有一个 master 进程,其有多个子进程为 worker 进程,master 进程受理客户端的请求,而后分发给 worker 进程,worker 进程处理 http 头信息后将参数传递给 php 进程处理后,将结果返回到上层,再响应给客户端。git
也考虑过使用 php-fpm 的 worker 进程池方式,那样的话 php-fpm 进程也要仿写了,目前还不熟悉其内部构造,若是能够简单化,天然向其靠拢。目前对 PHP 的 SAPI 接口不熟,了解一下再考虑。程序员
当前状态的服务器还极其简单,总结下来有如下地方待优化:github
虽然简单,但服务器已经有基本的功能了:
它监听本地地址的 8080 端口,将接收到的 http 头中的 path 信息提出出来交给 php 进程,php 进程将参数信息处理后返回给服务器,服务器拼装 http 响应信息再将结果返回给客户端。
下面介绍各个功能的实现:
在介绍函数之间先用一张图来介绍一次 http 请求中客户端与服务器之间的交互:
如图:服务器建立要进行:
int socket(int domain, int type, int protocol);
int bind( int socket, const struct sockaddr *address, size_t address_len);
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr *address, size_t *address_len);
int recv(int s, void *buf, int len, unsigned int flags);
int send(int s, const void * msg, int len, unsigned int falgs);
socket 间的接收和发送信息在 C 中有几个系列:write() / read() 、send() / recv() 、sendto() / recvfrom()、 sendmsg() / recvmsg()
,能够自行选用。
另外函数参数释义和要点,都被我注释在代码中了,感兴趣的能够拉下来看一下,这些在网上也多有介绍,这里再也不赘述。
而后是 C 进程和 php 进程的交互,考虑到简单易用,目前在 C 进程中直接执行 php 脚本:
一开始使用 system() 函数: int system(const char *command);
system 函数会 fork 一个子进程,在子进程中以 cli 方式执行 php 脚本,并将错误码或返回值返回。因为其结果类型不可控,编译时会报一个 warning。并且它将结果返回给父进程时,还会在标准输出中打印结果,在服务器执行时会抛出异常。
因而找到了另外一个方法 popen, FILE * popen(const char * command, const char * type);
:
popen 一样会 fork 一个子进程来执行 command ,而后创建管道连到子进程的标准输出设备或标准输入设备,而后返回一个文件指针。随后进程即可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。
其 type 参数即是控制链接到子进程的标准输入仍是标准输出。咱们想要子进程的标准输出,因而传入 type参数为 字符 “r” (read)。同理,若是想写入子进程标准输入的话,能够传值 “w”(write)。
另外在接收缓冲区内容的时候也出现了一点小意外:因为使用的 fgets() 方法会以换行符\n
为一段的结尾,在接收 php 进程输出时遇到换行会结束,这里使用了一个中间字符串数组line
来接收每一行的信息,将每一行的信息拼装到结果中。
代码以下:
char * execPHP(char *args){ // 这里不能用变长数组,须要给command留下足够长的空间,以存储args参数,否则拼接参数时会栈溢出 char command[BUFF_SIZE] = "php /Users/mfhj-dz-001-441/CLionProjects/cproject/tinyServer/index.php "; FILE *fp; static char buff[BUFF_SIZE]; // 声明静态变量以返回变量指针地址 char line[BUFF_SIZE]; strcat(command, args); memset(buff, 0, BUFF_SIZE); // 静态变量会一直保留,这里初始化一下 if((fp = popen(command, "r")) == NULL){ strcpy(buff, "服务器内部错误"); }else{ // fgets会在获取到换行时中止,这里将每一行拼接起来 while (fgets(line, BUFF_SIZE, fp) != NULL){ strcat(buff, line); }; } return buff; }
socket 处于应用层和传输层之间的虚拟层,因为设置服务器 socket 协议类型为 TCP,那么 TCP 的握手挥手、数据读取等步骤对于咱们都是透明的。咱们拿到的数据即 HTTP 报文,关于 HTTP 报文结构和其字段解释的文章很是多,这里也再也不多提。
首先使用 C 的 strtok() 方法,获取到 HTTP 头的第一行,获取到其 http 方法和 path 信息,将这些信息处理后,再使用 sprintf() 方法拼合 HTTP 响应报文,主要替换了 响应内容长度和响应内容。
对 C 的用法还不太熟悉,没用指针、结构等华丽操做,光简单的实现就花了我很久。可能代码路子也会有点野,但愿有路过的大神能随手提点一二;
服务器相关的知识很深,每个优化点须要扎实的基础知识来巩固,可能我学到的也只是皮毛,文章不免有错漏处,若是发现,烦请指出。
若是您以为本文对您有帮助,能够点击下面的 推荐 支持一下我。博客一直在更新,欢迎 关注 。