LINUX (网络编程跨平台开发小项目)高并发低延迟服务器的简单开发
介绍:
此项目是利用visual stdio在windows系统下,搭建linux的unbuntu的环境进行开发,创建一个能使用浏览器通过输入服务器的IP地址和端口号,来访问服务器预设目录及其里的内容。要求服务器能够实现对http的get(静态资源的请求)请求的解析、包装和发送。在传输层主要利用tcp协议,可以更加简单方便一点。
具体实现:
服务器的高并发能力通过io多路转接来实现,这里具体采用epoll树。因此要构建一个epollRun()的函数,来具体管理epoll树上的各个节点。
1.创建监听文件描述符,并且将其设置到epoll树的根节点上
为了保证代码的可阅读性,创建监听文件的实现可以通过返回值是整型的函数来实现,通过函数创建监听文件并且将其的文件描述符返回,最后加到epoll树的根节点上去。
例如:
创建epoll树,并且调用设置监听文件的函数。
int epollRun(unsigned short port)
{
//创建epoll树
int epfd = epoll_create(10);
if (epfd == -1)
{
perror("epoll_create error!
");
exit(-1);
}
//创建监听文件描述符,将监听文件的文件描述符加入到epoll的根节点上去
int lfd = addListen(port, epfd);
设置监听文件的描述符并且将其加入到epoll的根节点上去。
**** 这里使用setsockopt()函数来设置端口复用。
并且使用形参来让服务器在调用主函数时来自己设置端口,提高服务器的灵活性。
int addListen(unsigned short port,int epfd)
{
//创建监听文件的文件描述符
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket error
");
exit(-1);
}
//设置端口复用
int opt = 1;
int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
if (ret == -1)
{
perror("setsocket error!
");
exit(-1);
}
//绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);//用户自己输入端口,灵活一点
addr.sin_addr.s_addr = INADDR_ANY;
ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind error
");
exit(-1);
}
//设置监听
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen error
");
exit(-1);
}
//将监听文件描述符加入到根节点上去
struct epoll_event ev;
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1)
{
perror("epoll_ctl error
");
exit(-1);
}
return lfd;
}
2.循环检测epoll树上的节点,来进行通信或者建立新连接
若是监听文件的读缓冲区有信息,就建立新的连接,并且创建新的通信文件描述符将其的读缓冲区加到epoll树的节点上去。这里依然使用函数来完成。
//循环检测监听文件和通信文件的文件描述符的读缓冲区
struct epoll_event evs[1024];
int size = sizeof(evs)/sizeof(evs[0]);
while (1)
{
int num = epoll_wait(epfd, evs, size, -1);
if (num == -1)
{
perror("epoll-wait error!
");
return -1;
}
for (int i = 0; i < num; i++)
{
//监听文件的读缓冲区有数据
if (evs[i].data.fd == lfd)
{
//与客户端进行链接,并且将通信文件的文件描述符加入到epoll的节点上,设置为边沿非阻模式
addClient(lfd, epfd);
}
在调用添加新的文件描述符时,将其设置为边沿非阻塞的模式,来提高通信的效率。
int addClient(int lfd, int epfd)
{
//与客户端进行连接,并且创建通信文件的文件描述符
int cfd = accept(lfd, NULL, NULL);
if (cfd == -1)
{
perror("accept error!
");
return -1;
}
//将通信文件的文件描述符设置为非阻塞模式
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
int ret = fcntl(cfd, F_SETFL, flag);
//将通信文件的文件描述符加入到epoll树上,并且设置为边沿模式
struct epoll_event ev;
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if (ret == -1)
{
perror("epoll_ctl error!
");
return -1;
}
return 0;
}
如果是通信文件的文件描述符的读缓冲区有信息,就开始进行通信。
通信的过程主要分为1.接受客户端的http请求 2.解析http的请求信息 3.提出客户端请求信息的内容 4.按照http的格式来发送内容
3.接收http的请求内容
因为当前服务器只考虑get(静态资源)的请求,所以其http请求的有效内容很小,只用预设4字节来保存请求内容即可。又因为其是边沿非阻塞的模式,所以读完内容只用判断错误提示满不满足EAGAIN即可。
真正在get类型的http请求中能使用到的信息,只有http请求的第一行内容,里面包括了请求方式、请求内容、http协议脚本,已经可以满足与服务器相应客户端请求,因此只要保存第一行内容即可。又因为http协议是通过/r/n来进行换行的,所以只要使用系统函数找到请求信息中的第一个/r/n的位置,就能得到我们所需要的请求信息。(这里使用strstr()函数)。
int recvHttp(int cfd,int epfd)
{
/* get请求类型的格式
* 第一行:请求行
* 第一部分:GET 第二部分:客户端要访问的资源的目录 第三部分:http协议的脚本
* 第二到N行:请求头
* 客户端的ip地址和端口号
* 剩下来全是空行
*/
//所以不用将全部的请求信息都保留下来,只需将请求信息的的第一行保存下来即可
//http协议是通过/r/n的方式进行换行的
char buf_r[1024];//每次接受1k的数据量
memset(buf_r, 0, sizeof(buf_r));
char buf[4096];//读出来的信息的缓冲区
memset(buf, 0, sizeof(buf));
int len = 0;//每次接受的数据量
int total = 0;//buf中已经缓存的数据量
while ((len = recv(cfd, buf_r, sizeof(buf_r), 0)) > 0)
{
if (total + len <= sizeof(buf))
{
memcpy(buf+total, buf_r, len);//第一个参数表示向右偏移total个字节数
total += len;
}
}
//文件的描述符是非阻塞的,所以这种情况是文件描述符的都缓冲区中的内容读完了
if (len == -1 && errno == EAGAIN)
{
//将请求行的内容拿出来
char* pt = strstr(buf, "
");
//两个指针相减就能得到起始位置和末尾位置之间的元素偏移量
int reqLine = pt - buf;
// /0 表示截断
buf[reqLine] = "/0";
//分析buf中的http请求
paraseLine(buf, cfd);
}
4.解析http的请求行
在第三步得到了http的请求行内容,这一步就要对得到的请求行内容来进行解析。
请求行的内容主要包括三部分,且中间是通过空格隔开,第一部分时请求方式(get/post),第二部分是请求的目录,第三部分是使用的http协议的版本。在这里主要用到的是前两部分,因此我们使用sccanf(),用正则表达式来将前两部分取出来使用。
我们首先要判断是不是get方式来请求的数据,不是的话就提示请求非法,是的话就接着对请求的目录来进行操作。因为在运行服务时我们指定了服务器的根目录是我们预设好的资源目录,所以客户端在请求时时将资源当作根目录来进行请求的,因此我们要将客户端请求的目录转换成服务器实际的绝对路径目录才可以操作。这里我们在主函数内设置一个传入参数,这样在服务器刚开始启动时,就可以使用mkdir()函数来把服务器运行在资源根目录的文件夹中。
例如: 客户端请求的目录是 : / ----->服务器要转换成: ./
客户端请求的目录是:/hello/world/... ----->服务器要转换成: hello/world/..(这里利用指针右移来完成)
int paraseLine(const char* requestLine,int cfd)
{
//请求行内容 GET /hello/world/ http/1.1
char method[6];
char fileName[1024];
//正则表达式
sscanf(requestLine, "[^ ] [^ ]", method, fileName);
//判断是不是用get 的方式来请求的
//不是get
if (memcmp(method, "GET", strlen(method))!= 0)
{
printf("客户端请求不合法
");
return -1;
}
//是get
else
{
//是以服务器的资源根目录作为总的根目录来说的
//将输入的路径名字转换成绝对或者相对路径
/*
可能有这几种方式
1./ ==> 服务器预设的资源根目录
2./hello/word/... ==> hello/word/...(因为工作路径已经在资源目录之下,所以其绝对路径只需将前面的根目录去掉即可)
*/
//确定文件的绝对路径
char* file = NULL ;
//1.
if (memcmp(fileName, "/", strlen(fileName)) == 0)
{
file = "./";
}
//2
else
{
file = fileName+ 1;//指针右移
}
5.发送文件内容
在发送文件内容之前,我们要进行简单的判断,该文件是不是目录文件,不是目录文件我们就称为普通文件。这里使用stat()函数来进行判断。
//1.
struct stat st;
int ret = stat(file, &st);
unsigned long length;
//函数调用失败,没有这个文件
if (ret == -1)
{
file = "/home/zx/text/20201103174810732538.webp";
stat(file, &st);
length = st.st_size;
//发送头信息
sendHead(cfd, 200, "OK", conductType(file), length);
//返回404的图片
sendMasage(cfd, file);
}
//成功,判断是普通文件还是目录
else
{
length = st.st_size;
//目录
if (S_ISDIR(st.st_mode))
{
//发送头信息,是以.html的(表格)格式来发送文件的
sendHead(cfd, 200, "OK", conductType(".html"), length);
//发送目录
sendDir(cfd, file);
}
//普通文件
else
{
//发送头信息
sendHead(cfd, 200, "OK", conductType(file), length);
//发送文件内容
sendMasage(cfd, file);
}
}
5.1普通文件
判断出来是普通文件,就要将文件的内容提取出来,按照http协议的格式发送给客户端。
先来说发送文件http协议的格式:
第一行:状态行 (http 协议版本,状态码,对状态码的描述)
第二行:响应头 (只要有content-type content-lenth内容即可)
第三行: 空行
第四行:文件内容
鉴于使用的是tcp传输协议,并且在传输目录和未找到相应目录时都要发送几乎类似的前三行信息,因此可以将发送前三行内容写成一个函数,在不同的地方直接调用即可。第一行内容固定的:http1.1 200 OK这个可以直接发送,第二行内容不同文件对应的content-type 和content- length是不同的,可以查到各个不同后缀名对应的content-type,因此将其可以写成一个函数,通过文件的后缀名来查找并返回出其对应的content-type(这里使用strrchr()函数来查找“.”后面的文件后缀名),最终得到contnnt-type。而content-length可以通过stat()函数的传出参数的元素来确定。
文件内容的获取先通过open()函数打开指定文件目录,再在while()循环中不断地读取文件的内容,在不断进行发送,在客户端会按照content-type的方式来进行表现。
发送文件头(前三行内容)
int sendHead(int cfd,int statnum,const char *state,const char *type,unsigned long length)
{
//状态行 (http 协议版本,状态码,对状态码的描述)
//响应头 (content-type content-lenth)
//因为使用的是tcp传输协议,所以不用一次全部都发
char buf_s[4096];
memset(buf_s, 0, sizeof(buf_s));
//状态行 (http 协议版本,状态码,对状态码的描述)
sprintf(buf_s, "HTTP/1.1 %d %s
", statnum, state);
//响应头 (content-type content-lenth)
sprintf(buf_s, "Content-Type:%s
", type);
sprintf(buf_s, "Content-Length:%d
", length);
//空行
sprintf(buf_s, "
");
//发送
int num = send(cfd, buf_s, sizeof(buf_s), 0);
if (num == -1)
{
perror("send error!
");
return -1;
}
return 0;
}
不同普通文件对应的content-type
const char* conductType(const char* file)
{
const char* dot = strrchr(file, ".");
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/avi";
}
if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
{
return "video/quicktime";
}
if (strcmp(dot, ".mp3") == 0)
{
return "audio/mp3";
}
if (strcmp(dot, ".webp") == 0)
{
return "image/webp";
}
return "text/plain; charset=utf-8";
}
发送文件内容
int sendMasage(int cfd, const char* file)
{
//打开请求的文件
int fd = open(file, O_RDONLY);
if (fd == -1)
{
perror("open error!
");
return -1;
}
//将请求的文件读到buf缓存之中
char buf[4096];
memset(buf, 0, sizeof(buf));
while (1)
{
int len = read(cfd, buf, sizeof(buf));
if (len < 0)
{
perror("read error
");
return -1;
}
//按照http协议的格式将buf的内容发送出去
else if (len > 0)
{
send(cfd, buf, strlen(buf), 0);
}
//文件读完了
else
{
break;
}
}
return 0;
}
5.2目录文件
对于目录文件的操作和对普通的文件的操作的原理基本一致,但是对于目录文件我们要将目录文件的内容以表格的形式(第一列是文件名。第二列是文件大小)表示出来。因此在传输的过程之中,要以.html的文件传输出去,这样才能在网页中展示出表格的形式来,因此html语言的格式在这一块要熟练掌握,并且对应的content-type也要与.html文件对应。
在遍历目录文件时我们采用scandir()的函数:
scandir()遍历目录的函数介绍
int num = scandir(file, &dir, NULL, alphasort);
参1:要遍历的文件名指针
参2:传出参数,是一个struct dirent**(二级指针)的地址,这个二级指针是指struct dirent*[](是struct dirent 的指针数组),struct dirent中有表示目录名字的元素。
参3:回调函数,表示过滤条件,一般写NULL;
参4:回调函数,表示按照哪种方式来遍历:alphasort(askll码值)
返回值:成功:返会目录中文件的个数
失败:-1 errno;
注意:在使用这个函数时,参数4要在linux的环境小运行,如果报错的话,要在项目的属性中c/c++的语言选项中,将c/c++都设置为gun的模式。
int sendDir(int cfd, const char* file)
{
//要将目录的内容制成表格,并且按照http的协议格式发出
/*
* http的协议格式
*
*
* ...
*
*
*
* 行
* 第一列
* 第二列
*
* 行
* ...........
*
*
*
*
*
*/
char buf_s[4096];
memset(buf_s, 0, sizeof(buf_s));
//先发送前几部分没有文件内容的东西
sprintf(buf_s, "%s ", file);
int ret = send(cfd, buf_s, strlen(buf_s), 0);
if (ret == -1)
{
perror("send error!
");
return -1;
}
//遍历文件夹内容,将其内容发送出去
//*************这里采用scandir()的函数***************手机html p9有这个函数的介绍
struct dirent** dir;//这里的二级指针是指 struct dirent *[](struct dirent的指针数组),struct dirent 的结构体里面有文件名的属性
int num = scandir(file, &dir, NULL, alphasort);
if (num < 0) {
perror("scandir error!
");
return -1;
}
struct stat st;
for (int i = 0; i < num; i++)
{
memset(buf_s, 0, sizeof(buf_s));
char* dirname = dir[i]->d_name;
int ret = stat(dirname, &st);
if (ret < 0)
{
perror("stat error!
");
return -1;
}
sprintf(buf_s, "%s %d ", dirname, st.st_size);
ret = send(cfd, buf_s, strlen(buf_s), 0);
if (ret < 0)
{
perror("send error!
");
return -1;
}
}
//最后的内容
memset(buf_s, 0, sizeof(buf_s));
sprintf(buf_s, "

