学习视频链接
目录
学习目标:实现一个简单的 web 服务器 myhttpd 能够给浏览器提供服务,供用户借助浏览器访问服务器主机中的文件
一、项目展示
启动服务器
访问路径
访问文件
输入了错误的地址,访问不到需要的文件,就会展示错误页面
二、HTTP 协议基础
2.1 HTTP协议基础。
HTTP,超文本传输协议 (HyperText Transfer Protocol)。互联网应用最为广泛的一种网络应用层协议。它可以减少网络传输,使浏览器更加高效。通常 HTTP 消息包括客户机向服务器的请求消息和服务器向客户机的响应消息
2.2 请求消息(Request)
浏览器 -> 发给 -> 服务器。主旨内容包含 4 部分:
请求行:说明请求类型,要访问的资源,以及使用的 http 版本
请求头:说明服务器要使用的附加信息
空行:必须!即使没有请求数据
请求数据:也叫主体,可以添加任意的其他数据
2.3 响应消息 (Response)
服务器 -> 发给 -> 浏览器。主旨内容包含 4 部分:
状态行:包括 http 协议版本号,状态码,状态信息
消息报头:说明客户端要使用的一些附加信息
空行:必须!
响应正文:服务器返回给客户端的文本信息
三、简单的代码
3.1 代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#define MAXSIZE 2048
int init_listen_fd(int port, int epfd)
{
// 创建监听的套接字 lfd
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
perror("socket error");
exit(1);
}
// 创建服务器地址结构 IP+port
struct sockaddr_in srv_addr;
bzero(&srv_addr, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(port);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 给 lfd 绑定地址结构
int ret = bind(lfd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
if (ret == -1) {
perror("bind error");
exit(1);
}
// 设置监听上限
ret = listen(lfd, 128);
if (ret == -1) {
perror("listen error");
exit(1);
}
// lfd 添加到 epoll 树上
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1) {
perror("epoll_ctl add lfd error");
exit(1);
}
return lfd;
}
void do_accept(int lfd, int epfd)
{
struct sockaddr_in clt_addr;
socklen_t clt_addr_len = sizeof(clt_addr);
int cfd = accept(lfd, (struct sockaddr*)&clt_addr, &clt_addr_len);
if (cfd == -1) {
perror("accept error");
exit(1);
}
// 打印客户端IP+port
char client_ip[64] = {0};
printf("New Client IP: %s, Port: %d, cfd = %d\n",
inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
ntohs(clt_addr.sin_port), cfd);
// 设置 cfd 非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 将新节点cfd 挂到 epoll 监听树上
struct epoll_event ev;
ev.data.fd = cfd;
// 边沿非阻塞模式
ev.events = EPOLLIN | EPOLLET;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if (ret == -1) {
perror("epoll_ctl add cfd error");
exit(1);
}
}
void do_read(int cfd, int epfd)
{
// read cfd 小 -- 大 write 回
// 读取一行http协议, 拆分, 获取 get 文件名 协议号
}
void epoll_run(int port)
{
int i = 0;
struct epoll_event all_events[MAXSIZE];
// 创建一个epoll监听树根
int epfd = epoll_create(MAXSIZE);
if (epfd == -1) {
perror("epoll_create error");
exit(1);
}
// 创建lfd,并添加至监听树
int lfd = init_listen_fd(port, epfd);
while (1) {
// 监听节点对应事件
int ret = epoll_wait(epfd, all_events, MAXSIZE, -1);
if (ret == -1) {
perror("epoll_wait error");
exit(1);
}
for (i=0; i<ret; ++i) {
// 只处理读事件, 其他事件默认不处理
struct epoll_event *pev = &all_events[i];
// 不是读事件
if (!(pev->events & EPOLLIN)) {
continue;
}
if (pev->data.fd == lfd) { // 接受连接请求
do_accept(lfd, epfd);
} else { // 读数据
do_read(pev->data.fd, epfd);
}
}
}
}
int main(int argc, char *argv[])
{
// 命令行参数获取 端口 和 server提供的目录
if (argc < 3)
{
printf("./server port path\n");
}
// 获取用户输入的端口
int port = atoi(argv[1]);
// 改变进程工作目录
int ret = chdir(argv[2]);
if (ret != 0) {
perror("chdir error");
exit(1);
}
// 启动 epoll监听
epoll_run(port);
return 0;
}
3.2 后续需要增加的内容
3.3 获取需要的数据
nt get_line(int cfd, char *buf, int size)
{
int i = 0;
char c = '\0';
int n;
//每次读一个字节,判断合理就放入缓冲区中
while ((i < size - 1) && (c != '\n')) {
n = recv(cfd, &c, 1, 0);
if (n > 0) {
if (c == '\r') {
//MSG_PEEK 使得recv以拷贝的方式从缓冲区中读取数据(否则读取之后,缓冲区中的数据就没了)
//试探性的获取缓冲区中的数据量
n = recv(cfd, &c, 1, MSG_PEEK);
//缓冲区中有数据并且结尾是 \n ,则读取数据
if ((n > 0) && (c == '\n')) {
recv(cfd, &c, 1, 0);
}
else {
c = '\n';
}
}
buf[i] = c;
i++;
}
else {
c = '\n';
}
}
buf[i] = '\0';
//recv失败
if (n == -1) {
i = -1;
}
return i;
}
3.4 错误处理函数
// 断开连接
void disconnect(int cfd, int epfd)
{
int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
if(ret != 0) {
perror("epoll_ctl error");
exit(1);
}
close(cfd);
}
void do_read(int cfd, int epfd)
{
// 读取一行http协议, 拆分, 获取 get 文件名 协议号
char line[1024] = { 0 };
int len = get_line(cfd, line, sizeof(line)); // 确定读出 GET /hello.c HTTP/1.1
if (len == 0) {
printf("服务器检测到客户端关闭\n");
disconnect(cfd, epfd);
}
else {
// 现在要按照空格分割得到 /hello.c
}
}
3.5 正则表达式
void do_read(int cfd, int epfd)
{
// 读取一行http协议, 拆分, 获取 get 文件名 协议号
char line[1024] = { 0 };
int len = get_line(cfd, line, sizeof(line)); // 确定读出 GET /hello.c HTTP/1.1
if (len == 0) {
printf("服务器检测到客户端关闭\n");
disconnect(cfd, epfd);
}
else {
// 现在要按照空格分割得到 /hello.c
char method[16], path[256], protocol[16];
sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
printf("method=%s, path=%s, protocol=%s\n", method, path, protocol);
}
}
现在测试代码
正则表达式字符类
正则表达式数量限定
3.6 判断文件是否存在
// 处理http请求,判断文件是否存在,回发
void http_request(const char *file)
{
struct stat sbuf;
// 判断文件是否存在
int ret = stat(file, &sbuf);
if (ret != 0) {
// 回发浏览器 404 错误页面
perror("stat");
exit(1);
}
if(S_ISREG(sbuf.st_mode)) { // 是一个普通文件
printf("It's a file\n");
}
}
void do_read(int cfd, int epfd)
{
// 读取一行http协议, 拆分, 获取 get 文件名 协议号
char line[1024] = { 0 };
int len = get_line(cfd, line, sizeof(line)); // 确定读出 GET /hello.c HTTP/1.1
if (len == 0) {
printf("服务器检测到客户端关闭\n");
disconnect(cfd, epfd);
}
else {
// 现在要按照空格分割得到 /hello.c
char method[16], path[256], protocol[16];
sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
printf("method=%s, path=%s, protocol=%s\n", method, path, protocol);
// 丢弃缓冲区中后面的数据
while (1) {
char buf[1024] = { 0 };
len = get_line(cfd, buf, sizeof(buf));
printf("-- len = %d\n", len);
if (len == '\n') {
break;
}
if(len == -1) {
break;
}
}
if(strncasecmp(method, "GET", 3) == 0)
{
char *file = path + 1; // 取出客户端要访问的文件名
http_request(file);
}
}
}
3.7 应答回复客户端
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#define MAXSIZE 2048
int init_listen_fd(int port, int epfd)
{
// 创建监听的套接字 lfd
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
perror("socket error");
exit(1);
}
// 创建服务器地址结构 IP+port
struct sockaddr_in srv_addr;
bzero(&srv_addr, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(port);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 给 lfd 绑定地址结构
int ret = bind(lfd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
if (ret == -1) {
perror("bind error");
exit(1);
}
// 设置监听上限
ret = listen(lfd, 128);
if (ret == -1) {
perror("listen error");
exit(1);
}
// lfd 添加到 epoll 树上
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1) {
perror("epoll_ctl add lfd error");
exit(1);
}
return lfd;
}
void do_accept(int lfd, int epfd)
{
struct sockaddr_in clt_addr;
socklen_t clt_addr_len = sizeof(clt_addr);
int cfd = accept(lfd, (struct sockaddr*)&clt_addr, &clt_addr_len);
if (cfd == -1) {
perror("accept error");
exit(1);
}
// 打印客户端IP+port
char client_ip[64] = {0};
printf("New Client IP: %s, Port: %d, cfd = %d\n",
inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
ntohs(clt_addr.sin_port), cfd);
// 设置 cfd 非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 将新节点cfd 挂到 epoll 监听树上
struct epoll_event ev;
ev.data.fd = cfd;
// 边沿非阻塞模式
ev.events = EPOLLIN | EPOLLET;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if (ret == -1) {
perror("epoll_ctl add cfd error");
exit(1);
}
}
// 通过文件名获取文件的类型
const char *get_file_type(const char *name)
{
char *dot;
// 自右向左查找‘.’字符, 如不存在返回NULL
dot = strrchr(name, '.');
if (dot == NULL)
return "text/plain; charset=utf-8";
if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
return "text/html; charset=utf-8";
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
return "image/jpeg";
if (strcmp(dot, ".gif") == 0)
return "image/gif";
if (strcmp(dot, ".png") == 0)
return "image/png";
if (strcmp(dot, ".css") == 0)
return "text/css";
if (strcmp(dot, ".au") == 0)
return "audio/basic";
if (strcmp(dot, ".wav" ) == 0)
return "audio/wav";
if (strcmp(dot, ".avi") == 0)
return "video/x-msvideo";
if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
return "video/quicktime";
if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
return "video/mpeg";
if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
return "model/vrml";
if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
return "audio/midi";
if (strcmp(dot, ".mp3") == 0)
return "audio/mpeg";
if (strcmp(dot, ".ogg") == 0)
return "application/ogg";
if (strcmp(dot, ".pac") == 0)
return "application/x-ns-proxy-autoconfig";
return "text/plain; charset=utf-8";
}
int get_line(int cfd, char *buf, int size)
{
int i = 0;
char c = '\0';
int n;
//每次读一个字节,判断合理就放入缓冲区中
while ((i < size - 1) && (c != '\n')) {
n = recv(cfd, &c, 1, 0);
if (n > 0) {
if (c == '\r') {
//MSG_PEEK 使得recv以拷贝的方式从缓冲区中读取数据(否则读取之后,缓冲区中的数据就没了)
//试探性的获取缓冲区中的数据量
n = recv(cfd, &c, 1, MSG_PEEK);
//缓冲区中有数据并且结尾是 \n ,则读取数据
if ((n > 0) && (c == '\n')) {
recv(cfd, &c, 1, 0);
}
else {
c = '\n';
}
}
buf[i] = c;
i++;
}
else {
c = '\n';
}
}
buf[i] = '\0';
//recv失败
if (n == -1) {
i = -1;
}
return i;
}
// 断开连接
void disconnect(int cfd, int epfd)
{
int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
if(ret != 0) {
perror("epoll_ctl error");
exit(1);
}
close(cfd);
}
// 客户端的fd,错误号,错误描述,回发文件类型,文件长度
void send_respond(int cfd, int no, char *disp, char *type, int len)
{
char buf[1024] = { 0 };
sprintf (buf, "HTTP/1.1 %d %s\r\n", no, disp);
sprintf(buf + strlen(buf), "%s\r\n", type) ;
sprintf(buf + strlen(buf), "Content-Length:%d\r\n", len);
send(cfd, buf, strlen(buf), 0);
send(cfd, "\r\n", 2, 0);
}
// 发送服务器本地文件给浏览器
void send_file(int cfd, const char *file)
{
int n = 0;
char buf[1024];
// 打开的服务器本地文件 —— cfd 能访问客户端的 socket
int fd = open(file, O_RDONLY);
if (fd == -1) {
// 404 错误页面
perror("open error");
exit(1);
}
int ret;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
ret = send(cfd, buf, n, 0);
if (ret == -1) {
if (ret == -1) {
perror("send error");
exit(1);
}
}
}
close(fd);
}
// 处理http请求,判断文件是否存在,回发
void http_request(int cfd, const char *file)
{
struct stat sbuf;
// 判断文件是否存在
int ret = stat(file, &sbuf);
if (ret != 0) {
// 回发浏览器 404 错误页面
perror("stat");
//exit(1);
}
if(S_ISREG(sbuf.st_mode)) { // 是一个普通文件
// 回发 http 协议应答
// send_respond(cfd, 200, "OK", "Content-Type: text/plain; charset=iso-8859-1", sbuf.st_size);
char *type = get_file_type(file);
send_respond(cfd, 200, "OK", type, sbuf.st_size);
// 回发 给客户端请求数据内容
send_file(cfd, file);
}
}
void do_read(int cfd, int epfd)
{
// 读取一行http协议, 拆分, 获取 get 文件名 协议号
char line[1024] = { 0 };
int len = get_line(cfd, line, sizeof(line)); // 确定读出 GET /hello.c HTTP/1.1
if (len == 0) {
printf("服务器检测到客户端关闭\n");
disconnect(cfd, epfd);
}
else {
// 现在要按照空格分割得到 /hello.c
char method[16], path[256], protocol[16];
sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
printf("method=%s, path=%s, protocol=%s\n", method, path, protocol);
// 丢弃缓冲区中后面的数据
while (1) {
char buf[1024] = { 0 };
len = get_line(cfd, buf, sizeof(buf));
if (len == '\n') {
break;
}
if(len == -1) {
break;
}
}
if(strncasecmp(method, "GET", 3) == 0)
{
char *file = path + 1; // 取出客户端要访问的文件名
http_request(cfd, file);
}
}
}
void epoll_run(int port)
{
int i = 0;
struct epoll_event all_events[MAXSIZE];
// 创建一个epoll监听树根
int epfd = epoll_create(MAXSIZE);
if (epfd == -1) {
perror("epoll_create error");
exit(1);
}
// 创建lfd,并添加至监听树
int lfd = init_listen_fd(port, epfd);
while (1) {
// 监听节点对应事件
int ret = epoll_wait(epfd, all_events, MAXSIZE, -1);
if (ret == -1) {
perror("epoll_wait error");
exit(1);
}
for (i=0; i<ret; ++i) {
// 只处理读事件, 其他事件默认不处理
struct epoll_event *pev = &all_events[i];
// 不是读事件
if (!(pev->events & EPOLLIN)) {
continue;
}
if (pev->data.fd == lfd) { // 接受连接请求
do_accept(lfd, epfd);
} else { // 读数据
do_read(pev->data.fd, epfd);
}
}
}
}
int main(int argc, char *argv[])
{
// 命令行参数获取 端口 和 server提供的目录
if (argc < 3)
{
printf("./server port path\n");
}
// 获取用户输入的端口
int port = atoi(argv[1]);
// 改变进程工作目录
int ret = chdir(argv[2]);
if (ret != 0) {
perror("chdir error");
exit(1);
}
// 启动 epoll监听
epoll_run(port);
return 0;
}
单个文件请求成功
mp3 格式,他的头文件是这样的
3.8 文件类型区分
// 通过文件名获取文件的类型
const char *get_file_type(const char *name)
{
char *dot;
// 自右向左查找‘.’字符, 如不存在返回NULL
dot = strrchr(name, '.');
if (dot == NULL)
return "text/plain; charset=utf-8";
if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
return "text/html; charset=utf-8";
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
return "image/jpeg";
if (strcmp(dot, ".gif") == 0)
return "image/gif";
if (strcmp(dot, ".png") == 0)
return "image/png";
if (strcmp(dot, ".css") == 0)
return "text/css";
if (strcmp(dot, ".au") == 0)
return "audio/basic";
if (strcmp(dot, ".wav" ) == 0)
return "audio/wav";
if (strcmp(dot, ".avi") == 0)
return "video/x-msvideo";
if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
return "video/quicktime";
if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
return "video/mpeg";
if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
return "model/vrml";
if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
return "audio/midi";
if (strcmp(dot, ".mp3") == 0)
return "audio/mpeg";
if (strcmp(dot, ".ogg") == 0)
return "application/ogg";
if (strcmp(dot, ".pac") == 0)
return "application/x-ns-proxy-autoconfig";
return "text/plain; charset=utf-8";
}
// 处理http请求,判断文件是否存在,回发
void http_request(int cfd, const char *file)
{
struct stat sbuf;
// 判断文件是否存在
int ret = stat(file, &sbuf);
if (ret != 0) {
// 回发浏览器 404 错误页面
perror("stat");
//exit(1);
}
if(S_ISREG(sbuf.st_mode)) { // 是一个普通文件
// 回发 http 协议应答
// send_respond(cfd, 200, "OK", "Content-Type: text/plain; charset=iso-8859-1", sbuf.st_size);
char *type = get_file_type(file);
send_respond(cfd, 200, "OK", type, sbuf.st_size);
// 回发 给客户端请求数据内容
send_file(cfd, file);
}
}
3.9 细节处理
1、没有找到文件的页面
也可以自己在文件夹中写一个错误页面,然后没找到的发发送写过的错误页面
2、和客户端第二次请求 ico 文件处理
如上图,只需要在文件夹中放一个需要的 ico 文件,浏览器就能获取需要的网页图标
3、get 处理完成后删除服务器和客户端的连接
3.10 文件夹处理
拼接一个 html 页面
3.11 汉字字符编码和解码