C语言实现简单的WebServer服务器
基于TCP的套接字通信
这是一个单线程流程,服务器创建用于监听的套接字,绑定本地的ip和端口,listen函数去监听绑定的端口。
如果有客户端进行连接,服务器端就可以和发起连接的客户端建立连接,连接建立成功会生成一个用于通信的套接字。用于监听的套接字和用于通信的套接字是不一样的。监听的套接字用于建立连接,通信的套接字用于数据交互。用于数据交互的read和write都是阻塞函数,在单线程下面,一个服务器想和多客户端进行通信,肯定是做不到的,因为accept,read,write都是阻塞的。
为了使服务器可以正常的与多个客户端建立连接,并进行数据交互,需要用到多线程,多线程中的主线程负责建立连接(调用accept),子线程负责数据通信。
多线程切换有一定的开销,因此引入非阻塞 I/O。非阻塞 I/O 不会将进程挂起,调用时会立即返回成功或错误,因此可以在一个线程里轮询多个文件描述符是否就绪。但是这种做法缺点是,每次发起系统调用,只能检查一个文件描述符是否就绪。当文件描述符很多时,系统调用的成本很高。
IO多路复用,也就是select,poll,epoll,可以通过一次系统调用,检查多个文件描述符的状态,相比于非阻塞 I/O,在文件描述符较多的场景下,避免了频繁的用户态和内核态的切换,减少了系统调用的开销。在IO多路复用中,阻塞是由内核实现的,自己编写的代码可以少许多不必要的阻塞。
在单线程下,只用IO多路复用,没办法同时处理两件事情,为了提高效率,一般采用多线程+IO多路复用的方法。
单线程服务器流程
自己编写的代码充当服务器,浏览器作为客户端的角色进行固定地址的访问。
1. 在终端输入 启动程序 端口 和 程序主目录。
2. 启动监听套接字initListenFd(unsigned short port):
a) 创建监听fd,采用IPv4,TCP协议
b) 设置端口复用:如果程序服务器是主动断开连接的一方,会有一个2msl的等待时长,为了确认客户端已经收到我断开确认ack
c) 绑定IP和端口
d) 设置监听
e) 返回监听套接字lfd
3. 启动服务器程序epollRun(int lfd):
a) 创建epoll 树的根节点
b) lfd上树:上树用的是epoll_ctl函数
c) while true不停地检测是否有事件到来,根据epoll_wait返回的数组中的fd文件描述符,判断事件是连接请求,还是数据通信请求:
i. 如果是连接请求,调用acceptClient(lfd, epfd),建立新的连接。
ii. 如果是数据通信请求,调用recvHttpRequest(cfd, epfd),以http协议的方式传递消息。
acceptClient(lfd, epfd):
1. 建立连接,调用accept函数。
2. 设置非阻塞模式,非阻塞说的是文件描述符,默认得到的cfd是阻塞的,用fcntl修改文件描述符的属性。
3. cfd添加到epoll中,设置边沿触发方式。
recvHttpRequest(cfd, epfd):
1. 读取客户端发送过来的http请求头:
2. 判断数据是否被接收完毕:
a) if (len == -1 && errno == EAGAIN):证明有数据
解析请求行:parseRequestLine(const char* line, int cfd):
i. sscanf拆分字符串,得到请求方法和请求路径,仅处理get请求:
ii. 对请求路径中的中文进行处理,否则会乱码
iii. 判断文件路径是指向目录,还是文件,或者文件不存在:
1. 若文件路径不存在,发送404.html
2. 若文件路径是目录,则发送html的头部,然后发送格式化的目录列表,也是符合html格式。
3. 若文件路径指向具体文件,则分析文件类型,然后发送具体文件。
由于通信的文件描述符是非阻塞的,用sendfile发送文件的时候要处理返回值,
不断根据偏移量去发送文件,直到文件发送完毕,否则大文件的传输会出现问题。
cfd去读取发送数据内存的时候,是非阻塞的,读数据块速度很快,读到文件末尾偏移量之后,再进行while循环读取的时候
ret返回值为-1,errno == EAGAIN代表没有数据,可以再次进行尝试。
off_t offset = 0;
int size = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
while (offset < size) // 如果偏移量小于size,则表示文件没有发送完,继续发送
{
// 通信的文件描述符是非阻塞的
int ret = sendfile(cfd, fd, &offset, size - offset);
printf("ret value: %d\n", ret);
if (ret == -1 && errno == EAGAIN) // EAGAIN的意思是没有数据,可以再次进行尝试
{
printf("没数据...\n");
}
}
close(fd);
b) 否则说明客户端断开了连接,要及时的将cfd下树,并且关闭对应文件描述符
多线程服务流程
单线程的服务器模型中,主程序会不断阻塞的进行连接和数据通信两项工作,两项工作和主线程不独立,当连接请求比较多的时候,效率相对较低。
多线程的处理方法:在建立连接 acceptClient(lfd, epfd) 和 数据通信模块 recvHttpRequest(cfd, epfd)两部分,都开辟新的线程去做,让子线程去处理动作。
注意要在项目的输入,库依赖项中输入pthread,否则linux链接的时候找不到。
main.c代码:
#include <stdio.h>
#include"Server.h"
#include<unistd.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
if (argc < 3) {
printf("./a.out port path\n");
return -1;
}
unsigned short port = atoi(argv[1]);
// 切换服务器的工作目录
chdir(argv[2]);
// 初始化监听的套接字
int lfd = initListenFd(port);
// 启动服务器程序
epollRun(lfd);
return 0;
}
Server.h代码:
#pragma once
// 初始化监听的文件描述符
int initListenFd(unsigned short port);
// 启动epoll
int epollRun(int lfd);
// 和客户端建立连接
// int acceptClient(int lfd, int epfd);
void* acceptClient(void* arg);
// 接收http请求
// int recvHttpRequest(int cfd, int epfd);
void recvHttpRequest(void* arg);
// 解析请求行
int parseRequestLine(const char* line, int cfd);
// 发送文件
int sendFile(const char* fileName, int cfd);
// 发送响应头(状态行和响应头)
/**
* cfd 通信文件描述符
* status 状态码
* descr 状态描述
* type 描述数据格式
* length 数据库长度 若为-1,则告诉浏览器去计算长度
*/
int sendHeadMsg(int cfd, int status, const char* descr, const char* type, int length);
// 获取文件类型,已经有,不用再写
const char* getFileType(const char* name);
// 发送目录
int sendDir(const char* dirName, int cfd);
// 将数字从十六进制转换成十进制
int hexToDec(char c);
// 解码,解决中文乱码问题,from传入参数,to传出参数
void decodeMsg(char* to, char* from);
Server.c代码:
#include "Server.h"
#include <arpa/inet.h>
#include