Linux网络编程11——简单的web服务器
创始人
2024-01-28 14:53:31

学习视频链接

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   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   #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; ievents & 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.1if (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.1if (len == 0) {printf("服务器检测到客户端关闭\n");disconnect(cfd, epfd);}else {// 现在要按照空格分割得到 /hello.cchar 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.1if (len == 0) {printf("服务器检测到客户端关闭\n");disconnect(cfd, epfd);}else {// 现在要按照空格分割得到 /hello.cchar 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   
#include   
#include   
#include   
#include   
#include   
#include   
#include 
#include 
#include   
#include   #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 能访问客户端的 socketint 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.1if (len == 0) {printf("服务器检测到客户端关闭\n");disconnect(cfd, epfd);}else {// 现在要按照空格分割得到 /hello.cchar 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; ievents & 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 汉字字符编码和解码 

 

上一篇:描写关羽的成语

下一篇:排序(详解)

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
不用电脑怎么设置苹果铃声 ip... 需要一个app,加上苹果自带的app即可1、手机下载爱思极速版。因为之前换手机搬移数据时店员帮忙整理...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
才高八斗的主人公 三顾茅庐的主... 古代君王在建立自己的政治体系之时,大多会采取一些礼贤下士的手段,对于一些有卓越才能的人放低姿态。因为...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...