学习视频链接
02-web大练习的概述_bilibili_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1iJ411S7UA/?p=132&spm_id_from=pageDriver&vd_source=0471cde1c644648fafd07b54e303c905
目录
一、项目展示
二、HTTP 协议基础
2.1 HTTP协议基础。
2.2 请求消息(Request)
2.3 响应消息 (Response)
三、简单的代码
3.1 代码
3.2 后续需要增加的内容
3.3 获取需要的数据
3.4 错误处理函数
3.5 正则表达式
3.6 判断文件是否存在
3.7 应答回复客户端
3.8 文件类型区分
3.9 细节处理
3.10 文件夹处理
3.11 汉字字符编码和解码
学习目标:实现一个简单的 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 汉字字符编码和解码