学习视频链接
目录
学习目标:实现一个简单的 web 服务器 myhttpd 能够给浏览器提供服务,供用户借助浏览器访问服务器主机中的文件
启动服务器
访问路径
访问文件
输入了错误的地址,访问不到需要的文件,就会展示错误页面
HTTP,超文本传输协议 (HyperText Transfer Protocol)。互联网应用最为广泛的一种网络应用层协议。它可以减少网络传输,使浏览器更加高效。通常 HTTP 消息包括客户机向服务器的请求消息和服务器向客户机的响应消息
浏览器 -> 发给 -> 服务器。主旨内容包含 4 部分:
请求行:说明请求类型,要访问的资源,以及使用的 http 版本
请求头:说明服务器要使用的附加信息
空行:必须!即使没有请求数据
请求数据:也叫主体,可以添加任意的其他数据
服务器 -> 发给 -> 浏览器。主旨内容包含 4 部分:
状态行:包括 http 协议版本号,状态码,状态信息
消息报头:说明客户端要使用的一些附加信息
空行:必须!
响应正文:服务器返回给客户端的文本信息
- #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; 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
- #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 能访问客户端的 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
-
- // 只处理读事件, 其他事件默认不处理
- 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 汉字字符编码和解码
-
相关阅读:
spring boot破解xjar.go加密后的jar包
Godot 4.0 加载为占位符(InstancePlaceholder)的用法和特点
【FISCO-BCOS】十七、角色的权限控制
docker 启动 centos/golang 容器 及参数说明
中国信息通信设备行业发展趋势及投资风险研究报告
【Obsidian样式】修改文件夹名称和文件名称前的图标
Eclipse项目导入笔记大全&踩坑大全
AI智能文案写作工具,迅速生成高质量的文案
【Vue.js生命周期】什么是生命周期?声明周期详解
分享一个java+springboot+vue校园电动车租赁系统(源码、调试、开题、lw)
-
原文地址:https://blog.csdn.net/HuanBianCheng27/article/details/127814345