加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
Server.c 16.70 KB
一键复制 编辑 原始数据 按行查看 历史
#include "Server.h"
#include <arpa/inet.h>
#include <asm-generic/errno-base.h>
#include <stdio.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <strings.h>
#include <sys/stat.h>
#include <assert.h>
#include <sys/sendfile.h>
#include <dirent.h>
#include <stdlib.h>
#include <ctype.h>
#include <pthread.h>
struct FdInfo{
int fd;
int epfd;
pthread_t tid;
};
//初始化用于监听的套接字
int initListenFd(unsigned short port){
//1、创建监听的 fd
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1){
perror("socket");
return -1;
}
//2、设置端口复用
int opt = 1;
int ret = setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof opt);
if(lfd == -1){
perror("setsockopt");
return -1;
}
//3、绑定端口
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");
return -1;
}
//4、设置监听
ret = listen(lfd,128);
if(ret == -1){
perror("listen");
return -1;
}
//5、返回fd
return lfd;
}
//启动 epoll 服务器程序
int epollRun(int lfd){
printf("Server is started.\n");
//1、创建epoll红黑树的实例
int epfd = epoll_create(1);
if(epfd == -1){
perror("epoll_create");
return -1;
}
//2、将 epoll 实例挂到树上
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;
}
//3、检测事件发生
//这个evs检测数组是用来存储epoll内核所检测到的就绪事件的
//这样我们就可以通过这个检测数组知道有哪些事件已经就绪了
struct epoll_event evs[1024];
//计算 evs 数组的大小
int size = sizeof(evs) / sizeof(struct epoll_event);
while(1){
int num = epoll_wait(epfd,evs,size,-1);
for(int i=0; i<num; ++i){
struct FdInfo* info = (struct FdInfo*)malloc(sizeof(struct FdInfo));
int fd = evs[i].data.fd;
info->fd = fd;
info->epfd = epfd;
//如果这个就绪读事件的fd的含义是有新连接到来
if(fd == lfd){
//建立新连接 accept
//acceptClient(lfd,epfd);
pthread_create(&info->tid,NULL,acceptClient,info);
}
//否则就是用于数据通信的文件描述符
else{
//处理对端发送来的数据
//recvHttpRequest(fd,epfd);
pthread_create(&info->tid,NULL,recvHttpRequest,info);
}
}
}
}
//和客户端建立连接的函数
//int acceptClient(int lfd,int epfd){
void* acceptClient(void* arg){
struct FdInfo* info = (struct FdInfo*)arg;
//1、为新来的请求建立连接
int cfd = accept(info->fd,NULL,NULL);
printf("New client is connected.\n");
if(cfd == -1){
perror("accept");
return NULL;
}
//2、将刚刚建立的连接添加到epfd红黑树上
//在添加之前,将cfd的属性改为非阻塞的
//因为epoll的边缘非阻塞模式的效率是最高的
int flag = fcntl(cfd,F_GETFL);//先得到cfd的文件属性
flag |= O_NONBLOCK; //在原来的文件属性中追加一个非阻塞属性
fcntl(cfd,F_SETFL,flag); //再设置回cfd的文件属性当中
struct epoll_event ev;
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET; //边缘模式监听读事件
int ret =epoll_ctl(info->epfd,EPOLL_CTL_ADD,cfd,&ev);
if(ret == -1){
perror("epoll_ctl");
return NULL;
}
printf("acceptClient threadId: %ld\n",info->tid);
//当客户端与服务器建立连接之后这块空间就可以回收了
//其中的lfd和epfd不可以关闭,因为还要用呢
free(info);
return NULL;
}
//接收 http 请求
//int recvHttpRequest(int cfd,int epfd){
void* recvHttpRequest(void* arg){
struct FdInfo* info = (struct FdInfo*)arg;
//在这个函数中要做的就是将客户端发送过来的所有的数据都接收到本地
char tmp[1024] = {0}; //相当于水瓢,将客户端的数据从tmp转存到buf中
char buf[4096] = {0}; //真正存数据的大水缸
//因为我们设置epoll事件通知的模式是边缘非阻塞,
//因此epoll检测到文件描述符对应的读事件之后就只会给我们通知一次
//因此我们需要在得到这个通过之后一次性把所有的数据都读出来
int len = 0, total = 0;
while((len = recv(info->fd,tmp,sizeof tmp,0)) > 0){
//确保数据总量total加上新读取的数据量len的值不会超出缓冲区
if(total+len < sizeof buf){
//这时候再进行数据拷贝
memcpy(buf+total,tmp,len);
}
//如果超出了buf大小的数据是可以丢弃的,因为对于get请求来说
//最重要的是请求行,也就是只要知道请求方式、要请求的资源即可
total += len;
}
//判断数据是否被接收完毕
//因为套接字是非阻塞的,当数据接收完毕之后,recv函数还会继续读数据
//继续读数据但是没有数据会返回什么呢?返回 -1
//而如果是阻塞的话,数据读完时recv就会被阻塞住的
//另外读数据如果失败的话也是会返回 -1 的
//既然都会返回 -1,那么怎么判断是读完了还是出现了错误呢?
//因此这里有一个细节,如果是数据读完的话对应的errno会有一个值,
//同理如果是读取出错errno则会有另外一个值
if(len == -1 && errno == EAGAIN){
//说明已经将客户端发来的数据处理完毕了
//现在开始进行请求的http协议进行解析
//解析请求行
char* pt = strstr(buf,"\r\n"); //先取出请求行
int reqLen = pt - buf; //获得请求行长度
buf[reqLen] = '\0'; //在请求行的最后加个\0就可以从请求报文数据中截取出请求行的内容
//然后调用一下解析请求行的函数即可完成对请求行的解析
parseRequestLine(buf,info->fd);
}
else if(len == 0){
//客户端断开了连接
epoll_ctl(info->epfd,EPOLL_CTL_DEL,info->fd,NULL);
close(info->fd);
}
else{
perror("recv");
}
//打印一下线程id
printf("recvMsg threadId: %ld\n",info->tid);
//数据通信结束,关闭用于通信的fd,epfd不关闭,因为还要使用呢
close(info->fd);
//回收info堆空间资源
free(info);
return 0;
}
//解析请求行
int parseRequestLine(const char* line,int cfd){
//解析请求行,主要将三部分切出来:请求方式、请求资源、http协议版本
char method[12] = {0}; //请求方式
char path[1024] = {0}; //请求的资源路径
//开始进行子字符串的提取,也就是解析操作
sscanf(line,"%[^ ] %[^ ]",method,path);
printf("method is %s, resource path is %s \n",method,path);
//不区分大小写的比较解析出来的是否是get请求
//不处理post请求,太复杂了项目主要是为了理解高并发服务器模型
//因此就省略了 post 请求的解析了
if(strcasecmp(method,"get") != 0){
return -1;
}
//调用解码函数,将请求行中的特殊字符转义回去
decodeMsg(path,path);
//如果是get请求,那么就开始解析静态资源(目录或者是文件)
//get请求格式:get /xxx/1.jpg http/1.1
char* file = NULL;
//先判断一下客户端访问的资源路径是否为服务器提供的静态资源路径的根目录
if(strcmp(path,"/") == 0){
//如果是,那么我们就让file转化为 ./,表示静态资源的根目录
//然后我们把file传进读写函数中进行处理
file = "./";
}
else{
//不是 / 根目录的话,那么要访问的资源就是 xxx/1.jpg,这明显是一个相对路径
//其等同于 ./xxx/1.jpg
//那么我们让path指针地址往后偏移一个char单位即可略过字符 '/'
file = path + 1;
}
printf("file is : %s \n",file);
//此时有了文件资源地址file之后,我们要做的就是判断这个file是文件还是目录
//通过 OS 的 stat API 我们可以拿到文件属性,通过文件属性判断file所代表的是文件还是目录
struct stat st;
int ret = stat(file,&st);
printf("文件属性返回 ret == %d\n",ret);
if(ret == -1){
//文件不存在,那么回复404页面
sendHeadMsg(cfd,404,"Not Found",getFileType(".html"),-1);
sendFile("404.html",cfd);
//访问资源造成404的话,那么下面的事情就不需要再做了,那么return即可
return 0;
}
//如果存在,那么判断文件类型,Linux提供了一个S_ISDIR来帮助判断
if(S_ISDIR(st.st_mode)){
//如果是目录,那就把所请求资源目录下的内容发送给客户端
//Content-length不知道大小的话就填-1让浏览器自己决定即可
sendHeadMsg(cfd,200,"Ok",getFileType(".html"),-1);
sendDir(file,cfd);
}
else{
//否则就是文件,那么就把文件内容发送给客户端
sendHeadMsg(cfd,200,"Ok",getFileType(file),st.st_size);
sendFile(file,cfd);
}
return 0;
}
//根据文件名后缀获得其对应的Content-type值
const char* getFileType(const char* name)
{
// a.jpg a.mp4 a.html
// 自右向左查找‘.’字符, 如不存在返回NULL
const char* 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, ".pdf") == 0)
return "application/pdf";
if (strcmp(dot, ".ogg") == 0)
return "application/ogg";
if (strcmp(dot, ".pac") == 0)
return "application/x-ns-proxy-autoconfig";
if (strcmp(dot, ".mp4") == 0)
return "video/mp4";
//return "text/plain; charset=utf-8";
//如果上面的文件类型都不包含的话,那么就默认执行下载
//application/octet-stream用于表示未知或者二进制文件
return "application/octet-stream; charset=utf-8";
}
//发送文件
int sendFile(const char* fileName,int cfd){
//1、打开文件
int fd = open(fileName,O_RDONLY);
//使用断言进行严苛的判断,断言如果出现错误那么直接程序挂掉
assert(fd > 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
break;
}
else{
//否则就是读文件出现了异常
perror("read");
}
}
#else
int size = lseek(fd,0,SEEK_END);
//上面这行代码将fd的读写指针拉到了文件尾部
//单我们发送文件时还需要对其进行读数据操作呢
//因此这里我们还要将这个文件的读写指针给重置回开始处
lseek(fd,0,SEEK_SET);
printf("文件大小:%d\n",size);
off_t offset = 0;
while(offset < size){
//sendfile的第三个参数是偏移量,其会被执行两个操作
//1、发送数据之前,sendfile根据该偏移量开始读文件数据
//2、发送数据之后,sendfile会在底层自动更新该偏移量
//假如数据为1000个字节,第一次sendfile发送了100个字节,此时offset就0变成了100
//那么下一次再进行读的时候,sendfile就会从第100个字节处开始读取
int ret = sendfile(cfd,fd,&offset,size-offset);
if(ret > 0){
printf("发送数据量: %d \n",ret);
}
if (ret == -1 && errno == EAGAIN){
printf("没数据...\n");
continue;
}
else if(ret == -1){
perror("sendfile");
break;
}
}
close(fd);
#endif
return 0;
}
//发送目录
int sendDir(const char* dirName,int cfd){
//拼接html网页用的缓存空间
char buf[4096] = {0};
//用dirName做标签页的标题
sprintf(buf,"<html><head><title>%s</title></head><body><table>",dirName);
struct dirent** namelist;
int num = scandir(dirName,&namelist,NULL,alphasort);
for(int i=0; i<num;++i){
// 取出文件名
// namelist 指向的是一个指针数组struct dirent* tmp[]
char* name = namelist[i]->d_name;
//取出后也还是要判断是文件名还是目录名
struct stat st;
/*
* 要注意这里的name只是一个名字,它只能表示一个相对路径,比如 xxx
* 直接传入这个name能正确吗?显然是不对的
* 因为我们要指定相对路径的话就必须要把dirName一起指定进来才行
* 因为dirName才是真正的相对路径,name只是dirName目录里的一个子目录或者子文件
* 因此我们要再次对字符串进行拼接,把dirName和name拼接到一起然后再传给stat进行判断
* 拼接后的结果才是一个合理的正确的相对路径(这样才能定位到正确的目录或者文件)
*/
char subPath[1024] = {0};
sprintf(subPath,"%s/%s",dirName,name);
stat(subPath,&st);
//是目录的话
if(S_ISDIR(st.st_mode)){
//添加一个a标签<a href="">name</a>使得在浏览器上点击一下目录名就能够进行跳转
//注意如果要跳转到某个目录里面,那么在这个目录名的后面要加上一个斜杠 /
//有斜杠就表示我们要跳转到某个目录里面,没有斜杠的话就表示要访问的是某个文件
sprintf(buf+strlen(buf)
,"<tr><td><a href=\"%s/\">%s</a></td><td>%ld</td></tr>"
,name,name,st.st_size);
}
//是文件的话
else{
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));
//namelist[i]是struct dirent*类型的指针元素,被分配了内存空间那么就需要回收
free(namelist[i]);
}
//还剩最后的结束标签
sprintf(buf,"</table></body></html>");
send(cfd,buf,strlen(buf),0);
//同理,namelist是个二级指针,也被分配了内存空间因此也需要回收
free(namelist);
return 0;
}
//发送响应头(状态行+响应头)
int sendHeadMsg(int cfd,int status,const char* desc,const char* type,int length){
//封装状态行
char buf[4096] = {0};
//拼接字符串
sprintf(buf,"http/1.1 %d %s\r\n",status,desc);
//封装响应头
sprintf(buf+strlen(buf),"content-type: %s\r\n",type);
sprintf(buf+strlen(buf),"content-length: %d\r\n\r\n",length);
//注意,http响应数据格式还有第三部分空行,这里我们一并加在了上面这行代码中
send(cfd,buf,strlen(buf),0);
return 0;
}
// 将字符转换为整形数
int hexToDec(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 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 = hexToDec(from[1]) * 16 + hexToDec(from[2]);
// 跳过 from[1] 和 from[2] 因此在当前循环中已经处理过了
from += 2;
}
else
{
// 字符拷贝, 赋值
*to = *from;
}
}
*to = '\0';
}
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化