# Http 高并发服务器 (Reactor 模型) 01 simple

# 开发环境

# 系统

Visual studio 2022 + WSL 子系统,详细查看 微软教程 [1]

解决方案文件下->文件名->属性

  • 常规 -> 远程根目录 (可设置项目上传的目录)
  • C/C++ -> 语言 -> C语言标准 -> 选择 C11 (GNU Dialect) (-std=gnu11)
  • 链接器 -> 输入 -> 库依赖项 输入 pthread
//在linux安装相关库
sudo apt-get install g++ gdb make ninja-build rsync zip
//安装 openssh-server
sudo apt install openssh-server
//开始运行
sudo service ssh start
//设置开机自动启动
sudo systemctl enable ssh

# Linux 关闭进程

  • ctrl+c : 中断了进程,返回到终端界面 向进程 SIGINT 信号 杀死进程
  • ctrl+z : 暂停了进程,将进程暂停并挂 在后台 ,向进程 发送SIGSTOP 信号来 暂停进程
  • Ctrl-\ : 发送退出信号( SIGQUIT ); 默认情况下,这会导致进程终止并 转储核心
  • ctrl-s : 中断控制台输出
  • ctrl-q : 恢复控制台输出
  • ctrl-l : 清屏
# 列出所有进程详细信息
ps aux
# 实时监控系统进程
top
# 把暂停的第一个进程再放到前台
fd
# 将进程切换到后台运行
bg
# 杀死进程
kill -CONT <进程ID>

# HTTP 协议

http (超文本传输协议) 是 应用层协议 ,对通信的 数据进行封装

HTTP通信流程

http 请求 协议,http 响应 ,http 协议的 换行符 \r\n , 通过此判断,此行结束

请求报文&响应报文

# 客户端 (浏览器)

  • 通过浏览器地址栏给服务器发送请求,浏览器内部进行数据封装,根据 http协议 封装完毕, 数据发送服务器
  • 等待服务器回复
  • 收到服务器回复的数据,根据 http协议解析 数据,得到服务器回复的原始数据

# http 请求

  • 请求行
  • 请求头: get请求 ,静态请求, / 代表服务器给客户端提供的 资源目录 ,而非服务器根目录
  • 空行
  • 客户端向服务器提交的数据

# 接受 http 请求

void* recvHttpRequest(void* arg)
{
	struct FDInfo* info = (struct FDInfo*)arg;
	printf("recv message....\n");
	// 1,把用户所有数据先存到本地
	char buf[4096] = { 0 }; // 总
	char tmp[1024] = { 0 }; // 临时
	// 边缘模式,需要一次把所有数据全部读完
	int len = 0, total = 0; //len 每次接受的数据长度,total 为总长度
	// 将请求的数据全部存入缓存区当中
	while ((len = (int)recv(info->fd, tmp, sizeof tmp, 0)) > 0) // 接受客户端的请求数据
	{
		//printf("recv while total\n");
		if (total + len < sizeof(buf))
		{
			memcpy(buf + total, tmp, (size_t)len);  // 修改复制地址
		}
		total += len;
	}
	//printf("total = %d\n", total);
	// 判断数据是否被接受完毕
	if (len == -1 && errno == EAGAIN) //len 等于 - 1 并且错误为 EAGAIN
	{
		//printf("analyse get request!\n");
		// 解析请求行,对,先处理 get 请求
		char* pt = strstr(buf, "\r\n"); // 遇到这两个字符 \r\n 从左往右搜到之后,就结束了
		int reqLen = (int)(pt - buf); // 得到请求行的长度,结束的地址,减去起始的地址
		buf[reqLen] = '\0'; //buf [reqLen-1] 是上面的最后一个字符
		//printf("%s\n", buf);
		// GET /categories/ HTTP/1.1
		parseRequestLine(buf, info->fd); 
	}
	else if (len == 0)
	{
		printf("client closed!\n");
		// 客户端断开了连接,删除对应的文件描述符
		epoll_ctl(info->epfd, EPOLL_CTL_DEL, info->fd, NULL);
		close(info->fd);
	}
	else
	{
		//printf("recvHttpRequest error len = %d\n", len);
		perror("recv");
	}
	printf("recvMsg threadId : %ld\n", info->tid);
	free(info);
	return NULL;
}

请求报文
请求方法

# 解析请求头

int parseRequestLine(const char* line, int cfd)
{
	//GET /categories/ HTTP/1.1
	// 解析请求行, get /xxx/1.jpg http1.1  
	// 三部分:请求方式,请求资源,请求 http 协议的版本,数据之间有空格,
	//sscanf 对格式化的字符串进行拆分
	char method[12];  //get 或 post
	char path[1024];  // 存储请求的静态资源,文件路径
	sscanf(line, "%[^ ] %[^ ]", method, path); // 使用正则表达式记录空行来进行分割,取出请求方式,和请求路径
	printf("method: %s,path: %s\n", method, path);
	if (strcasecmp(method, "get") != 0)   //strcasecmp 比较时不区分大小写
	{
		// 非 get 请求不处理
		return -1;
	} 
	decodeMsg(path, path); // 避免中文的编码问题 将请求的路径转码 linux 会转成 utf8
	// 处理客户端请求的静态资源 (目录或文件)
	char* file = NULL;
	if (strcmp(path, "/") == 0) // 判断是不是根目录
	{
		file = "./";
	}
	else
	{
		file = path + 1;  // 指针 + 1 把开始的 / 去掉吧
	}
	// 判断 file 属性,是文件还是目录
	struct stat st;
	int ret = stat(file, &st); //file 文件属性,同时将信息传入 st 保存了文件的大小
	if (ret == -1)
	{
		// 文件不存在  -- 回复 404
		sendHeadMsg(cfd, 404, "Not Found", getFileType(".html"), -1);
		sendFile("404.html", cfd); // 发送 404 对应的 html 文件
		return 0;
	}
	// 判断文件类型
	if (S_ISDIR(st.st_mode)) // 如果时目录返回 1,不是返回 0
	{
		// 把这个目录中的内容发送给客户端
		sendHeadMsg(cfd, 200, "OK", getFileType(".html"), (int)st.st_size);
		sendDir(file, cfd);
	}
	else
	{
		// 把这个文件的内容发给客户端
		sendHeadMsg(cfd, 200, "OK", getFileType(file), (int)st.st_size);
		sendFile(file, cfd);
	}
	return 0;
}

# 服务器端

  • 接受数据,被 http协议封装 过的
  • 根据 http协议解析 数据,得到客户端请求的 原始数据
  • 处理客户端请求,得到 处理结果
  • 给客户端回复数据 (数据需要通过 http协议封装 ,然后发送给客户端)

# http 响应

服务器给客户端回复数据,称为 http响应 。http 响应消息也是一个数据块,若干行,换行 \r\n

  • 状态行
  • 消息报头 / 响应头
  • 空行
  • 回复给客户端的数据
    响应报文

# 响应代码

// 要回复 http 响应的数据块
int sendFile(const char* filename, int cfd)
{
	// 读一部分发一部分,发送数据底层使用 TCP 协议
	//1,打开文件
	int fd = open(filename, O_RDONLY);
	// 判断文件是否打开成功
	assert(fd > 0); // 如果大于 0 没有问题,若不大于 0,程序异常失败
#if 0
	// 读数据
	while (1)
	{
		char buf[1024];
		int len = read(fd, buf, sizeof(buf));
		if (len > 0)
		{
			send(cfd, buf, len, 0); // 发送数据给浏览器
			// 减慢发送节奏
			usleep(10); // 服务器休眠微妙
		}
		else if (len == 0)  // 文件已经读完了
		{
			break;
		}
		else
		{
			perror("read");
		}
	}
#else
	off_t offset = 0; //sendfile 的偏移量,判断是否将数据发送完毕
	int size = (int)lseek(fd, 0, SEEK_END); // 从 0 到某位的文件偏移量,即文件有多少个字节
	lseek(fd, 0, SEEK_SET); // 再把指针移动到首部
	// 系统函数,发送文件,linux 内核提供的 sendfile 也能减少拷贝次数
	// 通信文件描述符,打开文件描述符,fd 对应的文件偏移量一般为空,
	while (offset < size)
	{
		int ret = (int)sendfile(cfd, fd, &offset, (size_t)(size - offset));  // 单独单文件出现发送不全,offset 会自动修改当前读取位置
		printf("ret value: %d\n", ret);
		if (ret == -1 && errno == EAGAIN)
		{
			printf("not data ....");
			perror("sendfile");
		}
	}
#endif
	close(fd); // 关闭打开的文件
	return 0;
}

# http 状态码与处理

Http响应头)

if (strcasecmp(method, "get") != 0)   //strcasecmp 比较时不区分大小写

202

sendHeadMsg(cfd, 200, "OK", getFileType(".html"), (int)st.st_size);

404

sendHeadMsg(cfd, 404, "Not Found", getFileType(".html"), -1);
sendFile("404.html", cfd); // 发送 404 对应的 html 文件

# sscanf 函数

// 将参数 str 的字符串根据参数 format 字符串来转换并格式化数据,转换后的结果存在对应的参数内
sscanf(const char *str,const char *format,...);

# 具体功能

  • 根据格式从字符串中提取数据,如从字符串中取出 整数浮点数字符串
  • 指定长度 的字符串
  • 取到 指定字符为止 的字符串
  • 取仅包含 指定字符集的字符串
  • 取到 指定字符串位置大的字符串
  • 可以使用正则表达式进行字符串拆分。正则表达式:通过一个公式字符串,取匹配特定格式的字符串,判断这个字符串是否满足条件。

# 正则表达式规则

正则表达式使用的是贪心匹配方式,只要是匹配的字符没有结束,正则表达式就会一直匹配下去

[1-9] : 匹配一个字符,这个字符在 1-9 范围内就满足条件

[2-7] : 匹配一个字符,这个字符在 2-7 范围内就满足条件

[a-z] : 匹配一个字符,这个字符在 a-z 范围内就满足条件

[a,bc,d,e,f] : 匹配一个字符,这个字符是集合中 任意一个 就满足条件

[1-9,f-x] : 匹配一个字符,这个字符 是1-9 , 或者 f-x 集合中的任意一个就满足条件

[1:1] : ^ 代表否认,匹配一个字符,这个字符只要 不是1 就满足条件

[^ 2-8] : 匹配一个字符,这个字符只要 `不在 2-8`` 范围内就满足条件

[^ a-f] : 匹配一个字符,这个字符只要 不在a-f范围 内就满足条件

[^] : 匹配一个字符,这个字符只要 不是空格 就满足条件

const char *s = "http://www.baidu.com:1234";
char protocol[32] = {0};
char host[128] = {0};
char port[8] = {0};
//[^:] 从开始,到 ://  指针后移
sscanf(s,"%[^:]://%[^:]:%[1-9]",protocol,host,port);   //% 从字符串中去子字符串
sscanf("123456 abcdfg","%[^ ]",buf);
//buf 的值为 123456
sscanf("123456abcdefgBCDEF","%[1-9a-z]",buf);
//buf 的值:123456abcdefg
sscanf("123456abcdefgBCDEF","%[^A-Z]",buf);
//buf 的值: 123456abcdefg
char method[12];  //get 或 post
	char path[1024];  // 存储请求的静态资源,文件路径
	sscanf(line, "%[^ ] %[^ ]", method, path); // 使用正则表达式记录空行来进行分割,取出请求方式,和请求路径
	printf("method: %s,path: %s\n", method, path);

# 具体代码

# server.h

#pragma once
// 初始化监听的套接字
int initListenFd(unsigned short port);
// 启动 epoll
int epollRun(int lfd);
// 和客户端建立连接
void* acceptClient(void* arg);
// 接受 http 请求的消息
void* recvHttpRequest(void* arg);
// 解析请求行
int parseRequestLine(const char* line, int cfd); // 解析的数据,以及用于通信的文件描述符
// 发送文件
int sendFile(const char* filename, int cfd);
// 发送响应头 (状态行 + 响应头) 
// 通信文件描述符,状态码,状态描述,响应头,类型,长度
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);
// 将十六进制转为 10 进制
int hexToDec(char c);
// 中文字符解码
void decodeMsg(char* to, char* from);

# server.c

#include "server.h"
#include <arpa/inet.h>  //socket 需要
#include <sys/epoll.h>  //epoll 需要
#include <stdio.h>      //NULL 头文件
#include <unistd.h>
#include <fcntl.h>  //fcntl
#include <string.h>  //memcpy
#include <errno.h>  //errno
#include <strings.h>  //strcasecmp
#include <sys/stat.h>
#include <assert.h>  // 断言
#include <unistd.h>  //usleep
#include <sys/sendfile.h>  //sendfile
#include <dirent.h>
#include <stdlib.h>
#include <pthread.h>
#include <ctype.h>
// 传参结构体
struct FDInfo
{
	int fd;
	int epfd;
	pthread_t tid;
}FDInfo;
// 传入端口
int initListenFd(unsigned short port)
{
	//1,创建监听的 fd
	// AF_INET 基于 IPV4,0 流式协议中的 TCP 协议
	int lfd = socket(AF_INET, SOCK_STREAM, 0);
	if (lfd == -1)
	{
		perror("socket"); // 创建失败
		return -1;
	}
	//2,设置端口复用
	int opt = 1; //1 端口复用
	int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	if (ret == -1)
	{
		perror("setsockopt"); // 创建失败
		return -1;
	}
	//3,绑定端口
	struct sockaddr_in addr;
	addr.sin_family = AF_INET; //IPV4 协议
	addr.sin_port = htons(port); // 将端口转为网络字节序
	addr.sin_addr.s_addr = INADDR_ANY; // 本地所有 IP 地址
	ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
	if (ret == -1)
	{
		perror("bind"); // 失败
		return -1;
	}
	//4,设置监听  一次性可以和多少客户端连接,内核最大 128,若给很大,内核会改为 128
	ret = listen(lfd, 128);
	if (ret == -1)
	{
		perror("listen");
		return -1;
	}
	printf("Init successful....\n");
	// 返回 fd
	return lfd;
}
int epollRun(int lfd)
{
	//1,创建 epoll 实例
	int epfd = epoll_create(1); // 参数大于 0 即可,没有实际意义
	if (epfd == -1)
	{
		perror("epoll_Create");
		return -1;
	}
	// 2,lfd 上树
	struct epoll_event ev;
	ev.data.fd = lfd; // 指定监测的文件描述符
	ev.events = EPOLLIN; // 检测是否有新连接
	int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
	if (ret == -1)
	{
		perror("epoll_ctl");
		return -1;
	}
	//printf("epoll_stl up open...!\n");
	// 3, 检测,是一个持续的过程
	struct epoll_event evs[1024];
	int size = sizeof(evs) / sizeof(struct epoll_event); // 总长度 / 除以单个大小,得出个数
	while (1)
	{
		int num = epoll_wait(epfd, evs, size, -1); // 阻塞时间 - 1 没有事件触发,就一直阻塞
		//printf("epoll_wait open...!\n");
		// 取出
		for (int i = 0; i < num; ++i)
		{
			int fd = evs[i].data.fd;
			struct FDInfo* info = (struct FDInfo*)malloc(sizeof(struct FDInfo));
			info->fd = fd;
			info->epfd = epfd;
			// 判断是不是用于监听的
			if (fd == lfd)
			{
				// 建立新连接 accept 有连接就不会阻塞,建立连接,并将新的文件描述符添加到 epoll 当中
				//acceptClient(fd, epfd);
				pthread_create(&info->tid, NULL, acceptClient, info);
			}
			else
			{
				// 判断是接受接收端数据,数据为 http 请求的格式,
				//recvHttpRequest(fd, epfd);
				pthread_create(&info->tid, NULL, recvHttpRequest, info);
				//TODO 线程退出
			}
		}
	}
	return 0;
}
void* acceptClient(void* arg)
{
	struct FDInfo* info = (struct FDInfo*)arg;
	//printf("start connection ....\n");
	//1,建立连接
	int cfd = accept(info->fd, NULL, NULL);
	if (cfd == -1)
	{
		perror("accept");
		return NULL;
	}
	// 使用 epoll 的边沿模式
	//2, 设置非阻塞
	int flag = fcntl(cfd, F_GETFL);
	flag |= O_NONBLOCK;  // 按位或让其属性存在
	fcntl(cfd, F_SETFL, flag); // 添加到 cfd 当中,F_SETFL 设置文件状态
	//3,cfd 添加到 epoll 当中
	struct epoll_event ev;
	ev.data.fd = cfd; // 指定监测的文件描述符
	ev.events = EPOLLIN | EPOLLET; // 检测是否有读事件,边沿模式
	// 添加到 epoll 当中
	int ret = epoll_ctl(info->epfd, EPOLL_CTL_ADD, cfd, &ev);
	if (ret == -1)
	{
		perror("epoll_ctl");
		return NULL;
	}
	printf("recvMsg threadId : %ld\n", info->tid);
	free(info);
	return NULL;
}
void* recvHttpRequest(void* arg)
{
	struct FDInfo* info = (struct FDInfo*)arg;
	printf("recv message....\n");
	// 1,把用户所有数据先存到本地
	char buf[4096] = { 0 }; // 总
	char tmp[1024] = { 0 }; // 临时
	// 边缘模式,需要一次把所有数据全部读完
	int len = 0, total = 0; //len 每次接受的数据长度,total 为总长度
	// 将请求的数据全部存入缓存区当中
	while ((len = (int)recv(info->fd, tmp, sizeof tmp, 0)) > 0) // 接受客户端的请求数据
	{
		//printf("recv while total\n");
		if (total + len < sizeof(buf))
		{
			memcpy(buf + total, tmp, (size_t)len);  // 修改复制地址
		}
		total += len;
	}
	//printf("total = %d\n", total);
	// 判断数据是否被接受完毕
	if (len == -1 && errno == EAGAIN) //len 等于 - 1 并且错误为 EAGAIN
	{
		//printf("analyse get request!\n");
		// 解析请求行,对,先处理 get 请求
		char* pt = strstr(buf, "\r\n"); // 遇到这两个字符 \r\n 从左往右搜到之后,就结束了
		int reqLen = (int)(pt - buf); // 得到请求行的长度,结束的地址,减去起始的地址
		buf[reqLen] = '\0'; //buf [reqLen-1] 是上面的最后一个字符
		//printf("%s\n", buf);
		// GET /categories/ HTTP/1.1
		parseRequestLine(buf, info->fd); 
	}
	else if (len == 0)
	{
		printf("client closed!\n");
		// 客户端断开了连接,删除对应的文件描述符
		epoll_ctl(info->epfd, EPOLL_CTL_DEL, info->fd, NULL);
		close(info->fd);
	}
	else
	{
		//printf("recvHttpRequest error len = %d\n", len);
		perror("recv");
	}
	printf("recvMsg threadId : %ld\n", info->tid);
	free(info);
	return NULL;
}
int parseRequestLine(const char* line, int cfd)
{
	//GET /categories/ HTTP/1.1
	// 解析请求行, get /xxx/1.jpg http1.1  
	// 三部分:请求方式,请求资源,请求 http 协议的版本,数据之间有空格,
	//sscanf 对格式化的字符串进行拆分
	char method[12];  //get 或 post
	char path[1024];  // 存储请求的静态资源,文件路径
	sscanf(line, "%[^ ] %[^ ]", method, path); // 使用正则表达式记录空行来进行分割,取出请求方式,和请求路径
	printf("method: %s,path: %s\n", method, path);
	if (strcasecmp(method, "get") != 0)   //strcasecmp 比较时不区分大小写
	{
		// 非 get 请求不处理
		return -1;
	} 
	decodeMsg(path, path); // 避免中文的编码问题 将请求的路径转码 linux 会转成 utf8
	// 处理客户端请求的静态资源 (目录或文件)
	char* file = NULL;
	if (strcmp(path, "/") == 0) // 判断是不是根目录
	{
		file = "./";
	}
	else
	{
		file = path + 1;  // 指针 + 1 把开始的 / 去掉吧
	}
	// 判断 file 属性,是文件还是目录
	struct stat st;
	int ret = stat(file, &st); //file 文件属性,同时将信息传入 st 保存了文件的大小
	if (ret == -1)
	{
		// 文件不存在  -- 回复 404
		sendHeadMsg(cfd, 404, "Not Found", getFileType(".html"), -1);
		sendFile("404.html", cfd); // 发送 404 对应的 html 文件
		return 0;
	}
	// 判断文件类型
	if (S_ISDIR(st.st_mode)) // 如果时目录返回 1,不是返回 0
	{
		// 把这个目录中的内容发送给客户端
		sendHeadMsg(cfd, 200, "OK", getFileType(".html"), (int)st.st_size);
		sendDir(file, cfd);
	}
	else
	{
		// 把这个文件的内容发给客户端
		sendHeadMsg(cfd, 200, "OK", getFileType(file), (int)st.st_size);
		sendFile(file, cfd);
	}
	return 0;
}
// 要回复 http 响应的数据块
int sendFile(const char* filename, int cfd)
{
	// 读一部分发一部分,发送数据底层使用 TCP 协议
	//1,打开文件
	int fd = open(filename, O_RDONLY);
	// 判断文件是否打开成功
	assert(fd > 0); // 如果大于 0 没有问题,若不大于 0,程序异常失败
#if 0
	// 读数据
	while (1)
	{
		char buf[1024];
		int len = read(fd, buf, sizeof(buf));
		if (len > 0)
		{
			send(cfd, buf, len, 0); // 发送数据给浏览器
			// 减慢发送节奏
			usleep(10); // 服务器休眠微妙
		}
		else if (len == 0)  // 文件已经读完了
		{
			break;
		}
		else
		{
			perror("read");
		}
	}
#else
	off_t offset = 0; //sendfile 的偏移量,判断是否将数据发送完毕
	int size = (int)lseek(fd, 0, SEEK_END); // 从 0 到某位的文件偏移量,即文件有多少个字节
	lseek(fd, 0, SEEK_SET); // 再把指针移动到首部
	// 系统函数,发送文件,linux 内核提供的 sendfile 也能减少拷贝次数
	// 通信文件描述符,打开文件描述符,fd 对应的文件偏移量一般为空,
	while (offset < size)
	{
		int ret = (int)sendfile(cfd, fd, &offset, (size_t)(size - offset));  // 单独单文件出现发送不全,offset 会自动修改当前读取位置
		printf("ret value: %d\n", ret);
		if (ret == -1 && errno == EAGAIN)
		{
			printf("not data ....");
			perror("sendfile");
		}
	}
#endif
	close(fd); // 关闭打开的文件
	return 0;
}
// 发送文件之前先调用
int sendHeadMsg(int cfd, int status, const char* descr, const char* type, int length)
{
	// 状态行
	char buf[4096] = { 0 };
	sprintf(buf, "http/1.1 %d %s\r\n", status, descr); // 换行对应的 \r\n
	// 响应头
	sprintf(buf + strlen(buf), "content-type: %s\r\n", type); //buf + strlen (buf) 指针后移
	sprintf(buf + strlen(buf), "content-length: %d\r\n\r\n", length); // 最后一行是空行
	send(cfd, buf, strlen(buf), 0);
	return 0;
}
const char* getFileType(const char* name)
{
	//a.jpg,a.mp4,a.html
	// 从右向左查找 "." 字符,如不存在返回 NULL
	const char* dot = strrchr(name, '.'); //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";
}
/*
<html>
	<head>
		<title>test</title>
	</head>
<body>
	<table>
		<tr>
			<td></td>
			<td></td>
		</tr>
		<tr>
			<td></td>
			<td></td>
		</tr>
	</table>
</body>
</html>
*/
int sendDir(const char* dirName, int cfd)
{
	char buf[4096] = { 0 };
	sprintf(buf, "<html><head><title>%s</title></head><body><table>", dirName);
	struct dirent** namelist;
	int name = scandir(dirName, &namelist, NULL, alphasort); // 返回目录下有多少个文件
	for (int i = 0; i < name; ++i)
	{
		// 取出文件名 namelist 指向一个指针数组 struct dirent* tmp []
		char* name = namelist[i]->d_name;
		// 判断文件是不是目录
		struct stat st;
		char subPath[1024] = { 0 };
		sprintf(subPath, "%s/%s", dirName, name);
		// 得到文件属性
		stat(subPath, &st); //name 只是文件名称,需要拼接相对路径 
		if (S_ISDIR(st.st_mode))  // 是不是目录
		{
			sprintf(buf + strlen(buf),
				"<tr><td><a href=\"%s/\">%s</a></td><td>%ld</td></tr>",
				name, name, st.st_size);
		}
		else
		{
			// 文件 a 标签不需要加斜杠
			sprintf(buf + strlen(buf),
				"<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>",
				name, name, st.st_size);
		}
		send(cfd, buf, strlen(buf), 0);
		memset(buf, 0, sizeof(buf));
		free(namelist[i]);
	}
	// 把 html 结束的标签发送给字符串
	sprintf(buf, "</table></body></html>");
	send(cfd, buf, strlen(buf), 0);
	free(namelist);
	return 0;
}
// 将字符转换为整形数
int hexToDec(char c)
{
	if (c >= '0' && c <= '9')
		return c - '0';
	if (c >= 'a' && c <= 'f')
		return c - 'a' + 10;  //+10 进位
	if (c >= 'A' && c <= 'F')
		return c - 'A' + 10;
	return 0;
}
// 解码
//to 存储解码之后的数据,传出参数,from 被解码的数据,传入参数
void decodeMsg(char* to, char* from)
{
	for (; *from != '\0'; ++to, ++from) //
	{
		//isxdigit -> 判断字符是不是 16 进制格式,取值在 0-f
		// Linux%E5%86%85%E6%A0%B8.jpg
		if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2]))
		{
			// 将 16 进制的数 -> 十进制 将这个数值赋值给了字符 int -> char
			// B2 == 178
			// 将 3 个字符,变成了一个字符,这个字符就是原始数据
			*to = (char)(hexToDec(from[1]) * 16 + hexToDec(from[2]));  // 将两个数转为十进制
			// 跳过 from [1] 和 from [2] 因此在当前循环中已经处理过了
			from += 2; // 调到下一个 %
		}
		else
		{
			// 字符拷贝,赋值
			*to = *from;
		}
	}
	*to = '\0';
}

# main.c

#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include "server.h"
#include <stdio.h>
// 初始化监听的套接字
//argc 输入参数的个数
//argv [0] 可执行程序的名称 
//argv [1] 传入的第一个参数, port
//argv [2] 传入的第二个参数   path
int main(int argc, char* argv[])
{
    if (argc < 3)
    {
        printf("./a.out port path\n");
        return -1;
    }
    unsigned short port = (unsigned short)atoi(argv[1]);
    // 切换服务器的根目录,将根目录当前目录切换到其它目录
    chdir(argv[2]);
    // 初始化监听的套接字 0~65535 端口 尽量不 5000 以下,可能会被占用
    printf("绑定的端口:%d\n", port);
    int lfd = initListenFd(port);
    // 启动服务器程序 epoll
    epollRun(lfd);
    return 0;
}

# 资料

  • 图解 HTTP 上野宣 / 于均良 译
  • 爱编程的大丙《高并发 Reactor 服务器》
  • Github 地址: https://github.com/foryouos/cppserver-linux

  1. https://learn.microsoft.com/zh-cn/cpp/linux/?view=msvc-170 ↩︎ ↩︎