当前位置: 首页 > news >正文

国家商标查询官方网站wordpress 免费 旅游

国家商标查询官方网站,wordpress 免费 旅游,安徽省建设工程执业信息网,html下载官网系统流程概览 main函数 对于一个服务器程序来说#xff0c;因为要为外部的客户端程序提供网络服务#xff0c;也就是进行数据的读写#xff0c;这就必然需要一个 socket 文件描述符#xff0c;只有拥有了文件描述符 C/S 两端才能通过 socket 套接字进行网络通信#xff0…系统流程概览 main函数 对于一个服务器程序来说因为要为外部的客户端程序提供网络服务也就是进行数据的读写这就必然需要一个 socket 文件描述符只有拥有了文件描述符 C/S 两端才能通过 socket 套接字进行网络通信而初始化这样一个 socket 服务端程序套接字接口的流程我们将其封装成了一个函数initListenFd(int port); 在经过该函数的操作之后我们会得到一个用于服务器程序进行网络通信的 socket 文件描述符。 此时我们就应该启动我们的服务器程序了也就是使用创建好的 server socket 去和外部的客户端程序进行网络通信但是如果不借助 IO 多路复用技术的话我们的服务器程序每次就只能和一个 client 进行通信这显然是不合理的。 因此我们这里引入 epoll 这种广受欢迎的 IO 多路复用技术来完成服务器程序的构建我们将这个构建过程封装为一个函数epollRun(server_fd); initListenFd(int port) 函数 在这个函数中主要是完成对于服务器端程序用于进行网络通信的 socket 套接字初始化流程比较固定也比较简单 主要由五个步骤 1、创建监听的套接字 主要是使用 socket 函数这是系统调用socket用于创建一个新的套接字。socket函数有三个参数分别指定了地址族Address Family、套接字类型Socket Type和协议Protocol。 AF_INET: 第一个参数AF_INET指定了地址族为IPv4。这意味着创建的套接字将使用IPv4地址进行通信。 SOCK_STREAM: 第二个参数SOCK_STREAM指定了套接字类型为面向连接的字节流套接字即TCP套接字。这意味着该套接字将使用TCP协议进行数据传输它提供了一套面向连接的、可靠的、有序的数据传输服务。 0: 第三个参数通常为0表示自动选择该地址族和套接字类型所对应的默认协议。对于AF_INET和SOCK_STREAM这个默认协议就是TCP。 2、设置端口复用 setsockopt() 函数用于设置套接字的选项。 第一个参数 lfd 是之前创建的套接字文件描述符。 第二个参数 SOL_SOCKET 是选项所在的协议层这里表示选项应用于套接字层。 第三个参数 SO_REUSEADDR 是选项的名称它允许本地地址和端口号被重用。这对于服务器程序特别有用因为它允许服务器在重启时立即绑定到相同的端口上而不必等待之前绑定的套接字超时为了避免服务器等待 2MSL 超时时间后才能重用之前的 ip 和端口因此需要在程序重启后立即重用之前绑定的地址和端口。 第四个参数 opt 是指向一个变量的指针该变量包含要设置的值。在这个例子中opt应该是一个整数变量通常被设置为1来启用 SO_REUSEADDR 选项。 第五个参数sizeof opt是opt变量的大小。 返回值ret是一个整数表示操作的成功或失败。如果操作成功返回0如果失败返回-1。 3、绑定端口 在网络编程中当你调用bind()函数来将一个套接字socket与一个特定的IP地址和端口号绑定时你需要传递一个指向sockaddr结构体的指针作为参数。然而sockaddr是一个通用的结构体它本身并不包含足够的信息来直接表示一个IP地址和端口号。因此在实际使用中我们通常会使用sockaddr_in对于IPv4或sockaddr_in6对于IPv6这样的结构体它们包含了sockaddr结构体以及额外的信息如IP地址和端口号。 这是一个历史上遗留下来的设计问题因此只需记得我们会使用 sockaddr_in 类型而不是 sockaddr 类型即可。 4、设置监听 listen函数的第二个参数在网络编程中扮演着重要的角色。这个参数通常被称为backlog它定义了操作系统可以接受但尚未被应用程序如服务器通过accept调用接受的传入连接请求的数量。具体来说backlog参数指定了内核中未完成连接队列也称为完全连接队列或accept队列的最大长度。 虽然你可以指定backlog的大小但操作系统可能会对这个值进行限制。在Linux系统中backlog的实际最大值可能受到/proc/sys/net/core/somaxconn文件内容的限制一般是 128 。如果指定的backlog大于这个值它会被默默地截断到这个值。 5、返回已经初始化好的服务器程序的 socket 文件描述符 //初始化用于监听的套接字 int initListenFd(unsigned short port){//1、创建监听的 fdint 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、返回fdreturn lfd; }epollRun(server_fd) 函数 这个函数主要有三个步骤依然是比较流程化的固定行为 1、创建 epoll 红黑树的实例 因此如果使用的是 epoll_create()函数的话函数里面这个参数随便填一个大于 0 的数即可。 2、将创建的 epoll 实例挂到红黑树上 这个也比较流程化epoll 在内核中管理的是数据类型是 struct epoll_event 类型因此我们需要创建一个这个类型的对象然后把我们的刚刚前面创建好的 server_fd 放到这个对象中同时进行需要监听何种事件的设置最后通过 epoll_ctl() 函数将其挂到我们的 epoll 实例上即可。 3、检测挂载在epoll实例上所要监听文件描述符的事件的发生 这个同样是流程化的操作唯一要注意的是在服务器程序开始运行后epoll_create 创建的 epoll 实例 epfd 就是内核红黑树中的根节点绑定了服务器程序 ip 和 端口的 server_fd 以及许多其它的客户端连接所创建的 client_fd 都会被挂载在这颗红黑树上 因为我们实际上对 client_fd 监听的也只是读事件而在代码中我们不难看到对于 server_fd 我们监听的也是读事件因此看起来概念上似乎二者没有区别然而意义上则很有不同。 对于 server_fd 来说其如果有读事件发生则含义是有外部的客户端程序请求与服务器程序建立TCP连接 对于 client_fd 来说其如果有读事件发生则含义是已经建立的客户端程序与服务器程序的TCP连接上有从客户端发送来的数据需要进行读取。 所以从下面代码中我们可以看到当使用 epoll_wait() 函数后其会返回一个数字这个数字代表了对于所有挂载在红黑树上的文件描述符中被监听到有读事件发生的文件描述符的个数同时这些文件描述符对应的存储在红黑树上的 struct epoll_event 结构体也会被内核存储到我们下面代码中 struct epoll_event evs[1024]; 的结构体数组中进行保存。 因此我们直接对这个结构体进行遍历从其中取出每一个文件描述符与我们绑定了服务器程序的 server_fd 进行比较如果相同则表示当前所遍历到的这个文件描述符就是我们的 server_fd而这个文件描述符能被我们遍历到则说明是有新的客户端程序向服务器程序发起了TCP连接读事件发生因此我们就进行 TCP 连接这个进行新的客户端连接的过程我们封装为一个函数acceptClient(lfd,epfd); 而如果不是我们的 server_fd 的话那就说明一定是之前就已经建立了 TCP 连接的 client_fd这个 client_fd 能被我们遍历到则说明在这条连接中有从客户端发来的数据请求需要我们进行读取读事件发生因此我们就进行数据读取而这个数据读取的过程我们也封装为一个函数recvHttpRequest(lfd,epfd); //启动 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 i0; inum; i){int fd evs[i].data.fd;//如果这个就绪读事件的fd的含义是有新连接到来if(fd lfd){//建立新连接 acceptacceptClient(lfd,epfd);}//否则就是用于数据通信的文件描述符else{//处理对端发送来的数据recvHttpRequest(fd,epfd);}}} }acceptClient(lfd,epfd); 函数 本函数用于和客户端建立连接。 本身也比较简单基本就只有两个步骤。 1、为新来的请求建立连接 这个就比较简单啦就是调用 accept() 接收一下即可接收完了之后就会产生一个建立连接了的 client_fd 了。 2、将这个新建立连接的 socket 挂载到 epoll 红黑树上进行监听 同样是监听读事件嘛比较简单但是我们为了进一步榨取服务器程序的性能这里我们将 epoll 技术默认的水平触发模式更改为边缘触发模式。 边缘触发Edge Triggered, ET通常比水平触发Level Triggered, LT效率要高。这主要体现在以下几个方面 因此这里为了更进一步的压榨服务器程序的性能我们修改 epoll 默认的水平触发方式使用边缘触发。 同时将刚刚建立的文件描述符的文件属性由阻塞改为非阻塞边缘触发非阻塞的配合方式能够更进一步的榨干服务器程序性能。 //和客户端建立连接的函数 int acceptClient(int lfd,int epfd){//1、为新来的请求建立连接int cfd accept(lfd,NULL,NULL);printf(New client is connected.\n);if(cfd -1){perror(accept);return -1;}//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(epfd,EPOLL_CTL_ADD,cfd,ev);if(ret -1){perror(epoll_ctl);return -1;}return 0; }recvHttpRequest(lfd,epfd); 函数 在这个函数中要做的就是将客户端发送过来的所有的数据都接收到本地。 对于处理这个函数中的逻辑很简单就两步 1、读取来自客户端的 http 报文数据这就需要我们理解 http 协议的请求数据格式如下图 2、通过读取数据的情况分类讨论一下各自要进行的动作 情况1数据读完了那么就要进行解析接收到的请求行的操作了这个被封装为一个函数parseRequestLine(const char* line,int cfd); 情况2如果 recv 返回 0那么表示客户端已经断开连接那就从epoll树上删除该 socket 连接 情况3读取数据出错了打印日志信息 其它要注意的点在代码注释中已经注明 //接收 http 请求 int recvHttpRequest(int cfd,int epfd){//在这个函数中要做的就是将客户端发送过来的所有的数据都接收到本地char tmp[1024] {0}; //相当于水瓢将客户端的数据从tmp转存到buf中char buf[4096] {0}; //真正存数据的大水缸//因为我们设置epoll事件通知的模式是边缘非阻塞//因此epoll检测到文件描述符对应的读事件之后就只会给我们通知一次//因此我们需要在得到这个通过之后一次性把所有的数据都读出来int len 0, total 0;while((len recv(cfd,tmp,sizeof tmp,0)) 0){//确保数据总量total加上新读取的数据量len的值不会超出缓冲区if(totallen sizeof buf){//这时候再进行数据拷贝memcpy(buftotal,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,cfd);}else if(len 0){//客户端断开了连接epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL);close(cfd);}else{perror(recv);}return 0; }parseRequestLine(const char* line,int cfd); 函数 在 recvHttpRequest 函数中我们会将缓存了 http 请求数据的 buf字符串传递给 parseRequestLine 函数专门用来解析请求行的信息。 对于传进来的请求行要将其解析成三个部分请求方式、客户端请求的静态资源、客户端所使用的http协议版本。 如何拆对于 http 协议而言请求行的每一部分之间会有一个空格我们就通过这个空格来做做文章这里介绍一个 sscanf() 函数可以比较方便的帮助我们完成这个事情 然后我们要注意对于这个项目而言我们只接收解析 get 方法的 http 请求对于 post 我们这里就省略了因为比较复杂我们这个项目主要也是为了深入理解 Reactor 这种高并发服务器模型的如果确实有需要并且希望添加 post 请求的解析那么我相信在掌握了本项目的这些基础之上再去添加丰富这个项目是完全可以做到的。 最后关于这个函数还有一件事情需要完成对于 get 请求其所需要的资源总共有两种目录或者是文件。 因此需要分情况进行处理而对于服务器程序所提供的资源目录我们可以在程序启动的时候把其写死也可以让用户把这个资源目录给传进来这里我们采用后者因此需要修改一下 main 函数。 上面的逻辑应该比较好懂唯一比较难理解的是为什么我们需要将当前服务器进程切换到用户指定的静态资源目录下面。 在服务器端编程中服务器进程的工作目录Current Working Directory, CWD通常指的是进程启动时所在的目录或者之后通过程序代码显式更改的目录。服务器进程将工作目录切换到服务器提供的静态资源目录下面主要是出于以下几个考虑 因为进程启动时其所访问的文件路径特别是使用相对路径时通常是相对于该进程的工作目录Current Working Directory, CWD来解析的。 工作目录是进程在文件系统中当前的工作位置它决定了进程如何解释和执行使用相对路径的文件访问请求。当进程尝试打开一个文件时如果使用的是相对路径那么操作系统就会从工作目录开始按照提供的相对路径来查找并访问该文件。 例如如果进程的工作目录是 /home/user/project并且它尝试使用相对路径data/config.txt来打开一个文件那么操作系统会在/home/user/project/data/目录下查找名为config.txt的文件。 然而值得注意的是并非所有文件访问都依赖于工作目录。如果进程使用绝对路径即以根目录/开始的路径来访问文件那么无论工作目录是什么文件访问都会按照绝对路径指定的位置进行。 此外一些程序或库可能会提供自己的路径解析机制这些机制可能不完全依赖于操作系统的工作目录。但是在大多数情况下特别是在处理静态资源或配置文件时使用相对路径并依赖于工作目录是一种常见的做法。 因此了解并控制进程的工作目录对于确保文件访问的正确性和安全性是非常重要的。在服务器编程中特别是在处理静态资源和配置文件时可能需要显式地更改工作目录以确保文件访问的正确性。 在服务器进程切换到静态资源的目录后对于 get 请求的请求行格式如下 请求方式 资源路径 http协议版本号get /xxx/1.jpg http/1.1其中资源目录/xxx/1.jpg 中开头的斜杠是固定的它代表了客户端请求的服务器程序提供的资源文件目录的根目录。而 xxx/1.jpg 就是这个根目录下的子目录 xxx 和子文件 1.jpg 。 因此我们在服务器程序中此时就可以使用相对路径 ./ 来表示这个服务器程序提供的静态资源的根目录同时也就是上面 get 请求格式中资源目录的 /了。 具体的逻辑在代码中都很好理解直接看代码吧 //解析请求行 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.1char* 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; }sendFile(const char* fileName,int cfd); 函数 就三步 1、打开指定的文件 2、然后从里面读数据读一部分发一部分 为什么这样可行呢 因为发送数据的时候我们底层使用的是 TCP 协议TCP 的特点之一就是面向连接的、流式的传输协议所谓的流式传输协议就是通信的两端只要建立了连接之后只要连接还建立着那么我们就可以把这个数据一点一点发过去因为流并没有数据块大小的限定一次性发多少其实都没问题只要硬件支持的话因此我们可以读一部分发一部分。 另外在这个函数中我们还使用了断言 assert 技术这是一种比较严格的错误检查 另外在这个函数当中我们还介绍了一个更加高效的用于发送数据的 APIsendfile() 我们还使用了一个 lseek() 系统调用 lseek 函数是一个在 UNIX/Linux 系统编程中广泛使用的系统调用它用于改变读写一个文件时读写指针也称为文件偏移量的位置。这个函数允许程序显式地设置下一次读或写操作在文件中的起始位置。以下是对 lseek 函数的详细解释 具体看代码 //发送文件 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){//说明文件读完那么直接breakbreak;}else{//否则就是读文件出现了异常perror(read);}} #elseint 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); #endifreturn 0; }sendHeadMsg(int cfd,int status…); 函数 为什么会需要有这个函数这需要来看一下 http 协议响应的数据格式 可以看见总共有四部分组成而我们的 sendFile() 函数所发送的数据正是 http 返回数据格式中的第四部分响应体的部分相当于只返回了 http 协议的一个部分如果没有状态行和响应头的话我们服务器程序的数据也是无法发送到客户端的因此我们这里要编写这个 sendHeadMsg() 函数来完成状态行和响应头的编写。 也就是说如果我们想要发送一个文件那么在调用 sendFile 函数之前要先调用 sendHeadMsg 函数才行。 同理不管是发送什么数据吧都需要在发送目录函数之前先调用 sendHeadMsg 函数才行不然就没办法封装成完整的 http 协议的报文格式了。 //发送响应头状态行响应头 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(bufstrlen(buf),content-type: %s\r\n,type);sprintf(bufstrlen(buf),content-length: %d\r\n\r\n,length);//注意http响应数据格式还有第三部分空行这里我们一并加在了上面这行代码中send(cfd,buf,strlen(buf),0);return 0; }getFileType() 函数 这个函数的主要作用就是根据服务器程序要发送的文件数据的类型然后返回该文件类型相对应的 http 响应头中的 Content-type 值。 //根据文件名后缀获得其对应的Content-type值 const char* getFileType(const char* name) {// a.jpg a.mp4 a.html// 自右向左查找‘.’字符, 如不存在返回NULLconst char* dot strrchr(name, .);if (dot NULL)return text/plain; charsetutf-8; // 纯文本if (strcmp(dot, .html) 0 || strcmp(dot, .htm) 0)return text/html; charsetutf-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; charsetutf-8;//如果上面的文件类型都不包含的话那么就默认执行下载//application/octet-stream用于表示未知或者二进制文件return application/octet-stream; charsetutf-8; }sendDir() 函数 那么目录要如何发送呢 发送目录之前我们需要搞明白如果是发送目录那我们发送的这个目录到底需要包含什么内容 因此我们需要把要发送目录下的所有的文件的名字给读出来读出来之后把这些文件的名字以及文件的类型发送给客户端即可。 客户端拿到这些东西会展示在自己的页面上因此在这个函数中我们要做的事情就是在服务器端程序上遍历这个要发送的目录。 如何遍历 Linux 系统的目录结构 有两种方式。 第一种是使用opendir() readdir() closedir() 三个函数组合完成遍历。 第二种是使用scandir() 函数。 从数量上也可以明显看出使用第二种方式遍历目录的方式要比第一种简单太多因此这里我们使用第二种。 其中对于 scandir 函数的第四个参数来说我们一般不需要自己写因为 Linux 给我们提供了两个标准的排序函数 int alphasort(const struct dirent** a,const struct dirent** b);int versionsort(const struct dirent**a,const struct dirent** b);一般情况下我们直接使用上面的 alphasort 就行直接将 alphasort 函数的名字写入 scandir 函数的第四个参数中即可。 在这个函数中还有最后一个注意点就是客户端需要按什么方式来展示我们响应的目录列表。 众所周知在浏览器中都是通过 html 标签来显示的。因此我们只要将需要发送给客户端的目录数据包装成一个 html 网页的方式来组织起来将这个 html 数据块发送给客户端浏览器即可。 我们的 html 数据块组织形式如下 htmlheadtitletest/title/headbodytabletrtd/tdtd/td/trtrtd/tdtd/td/tr/table/body /html完整代码如下 //发送目录 int sendDir(const char* dirName,int cfd){//拼接html网页用的缓存空间char buf[4096] {0};//用dirName做标签页的标题sprintf(buf,htmlheadtitle%s/title/headbodytable,dirName);struct dirent** namelist;int num scandir(dirName,namelist,NULL,alphasort);for(int i0; inum;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 hrefname/a使得在浏览器上点击一下目录名就能够进行跳转//注意如果要跳转到某个目录里面那么在这个目录名的后面要加上一个斜杠 ///有斜杠就表示我们要跳转到某个目录里面没有斜杠的话就表示要访问的是某个文件sprintf(bufstrlen(buf),trtda href\%s/\%s/a/tdtd%ld/td/tr,name,name,st.st_size);}//是文件的话else{sprintf(bufstrlen(buf),trtda href\%s\%s/a/tdtd%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; }decodeMsg(); 与 hexToDec(); 函数 在这两个函数中我们解决一下浏览器无法访问带特殊字符的文件的问题。 而在我们这个项目中主要是没办法解决带中文字符的问题。 比如我们要访问的是 代码.h 这个资源 但是如果我们在服务器上打印一下就会发现 我们的中文字符被系统转义了变了一串编码字符串那这样的话显然我们的服务器程序就没办法通过文件名 open 出正确的文件描述符那么访问也就会变成 404 页面。 原因在于对于 http 协议中的 get 请求而言请求行中是不支持有特殊字符的 如果有特殊字符就必须要转义比如说中文。怎么转义系统会把这些特殊字符给转换成 UTF-8UTF-8 在 Linux 里面对应的是三个字符每个字符都有一个字符值。 在 Linux 里面有一个命令可以帮助我们查看这种问题但是得自己安装一下我的是 Ubuntu 系统 sudo apt install unicode安装完之后我们就可以进行一个查看了对于上面的 代码.h 文件 对比后不难发现代这个字对应 Linux 中 UTF-8 的三位编码e4 bb a3而 码字也是同理。 解决方案也很简单只需要将这些被系统编码了的特殊字符给转义回来即可。 这种转义字符的工具函数网上是非常多的直接大致看看 CV 即可 // 将字符转换为整形数 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.jpgif (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; }最后版本的 main 函数 int main(int argc,char** argv){//解决mp3/mp4文件在线播放问题signal(SIGPIPE,SIG_IGN);//argv[0]是可执行程序的名字argv[1]就是传进来的第一个参数//我们让用户传进来两个参数//第一个是port端口第二个则是服务器访问的静态资源目录的文件路径if(argc 3){printf(./a.out port path\n);return -1;}//port端口号字符串转整形unsigned short port atoi(argv[1]);//将当前服务器进程切换到用户指定的静态资源目录下面chdir(argv[2]);//初始化用于监听的套接字int lfd initListenFd(port);//启动服务器程序epollRun(lfd);return 0; }本项目服务器程序所用到的函数汇总 #pragma once #include stdio.h//初始化用于监听套接字 int initListenFd(unsigned short port);//启动 epoll 服务器程序 int epollRun(int lfd);//和客户端建立连接的函数 int acceptClient(int lfd,int epfd);//接收http请求 //客户端发送完数据就会断开连接也就意味着我们不需要对该文件描述符进行监听了 //因此我们还需要epoll实例通过这个epoll实例去红黑树上删除这个客户端所对应的文件描述符 int recvHttpRequest(int cfd,int epfd);//解析请求行 int parseRequestLine(const char* line,int cfd);//发送文件 int sendFile(const char* fileName,int cfd);//发送响应头状态行响应头 /* 状态行就两部分 参数statushttp状态码 参数deschttp状态描述响应头就是一堆键值对这里我们只写比较重要的两个一个是Content-type一个Content-length 参数type响应头Content-type的值 参数length响应头Content-length的值 */ int sendHeadMsg(int cfd,int status,const char* desc,const char* type,int length);//根据文件名后缀获得其对应的响应头中的Content-type值 const char* getFileType(const char* name);//发送目录 int sendDir(const char* dirName,int cfd);//解决Linux转义get请求中特殊字符的编码问题 int hexToDec(char c); void decodeMsg(char* to,char* from);加入多线程技术 如果要加入多线程技术我们需要思考的就是在什么地方需要进行子线程的创建。 从前文可知在服务器器端一共有两类文件描述符一类是用来监听新连接的一类是用于监听和客户端数据通信的。 而前面这一类在服务器端有且仅有一个所以我们在主线程里将它创建出来之后就不需要再做其它用来监听新连接的描述符的创建了。因为通过这唯一一个的监听新连接的文件描述符服务器就能够接收到客户端的连接请求并且和多个客户端建立连接了。 对于 epollRun() 这个函数其核心就是下面这一段永真循环 while(1){int num epoll_wait(epfd,evs,size,-1);for(int i0; inum; i){int fd evs[i].data.fd;//如果这个就绪读事件的fd的含义是有新连接到来if(fd lfd){//建立新连接 acceptacceptClient(lfd,epfd);}//否则就是用于数据通信的文件描述符else{//处理对端发送来的数据recvHttpRequest(fd,epfd);}} }在 while 循环内只做两件事情如果是新连接则建立新连接如果是有数据通信则进行数据处理。 因此对于这两个函数不管是 acceptClient 函数还是 recvHttpRequest 函数我们都可以把它们放入子线程中进行操作也就是说我们需要在这两个函数调用的位置创建子线程然后把这两个函数分别传递给子线程让子线程来处理这两个动作。 逻辑像下面这样以 acceptClient 为例 //建立新连接 accept acceptClient(lfd,epfd);pthread_creat(tid,NULL,acceptClient,线程参数);对于上面的代码对于 pthread_create() 函数来说tid 是线程 id这个显然是我们要创建的第二个参数线程属性我们是不需要特别设置什么的设置为 NULL 即可第三个参数是一个函数指针也就是线程函数的入口很明显就是我们的 acceptClient对于第四个参数则是传递线程入口函数所需要的参数在这里也就是我们的 acceptClient 函数很明显其需要两个参数但是这个参数只能填一个值怎么办 将线程入口函数所需要的参数封装成一个结构体即可 因此我们定义结构体如下 struct FdInfo{int fd;int epfd;pthread_t tid; };有了上面这个结构体之后我们就可以修改 epollRun 函数中的永真循环部分如下 while(1){int num epoll_wait(epfd,evs,size,-1);for(int i0; inum; 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);}} }然后我们需要去修改一下我们的 acceptClient 和 recvHttpRequest 函数因为它们按照我们之前的函数定义是不满足 pthread_create 函数对于线程入口函数要求的因此我们去修改一下它们的定义 除了函数声明函数的定义也需要更改嗷 首先是 acceptClient 函数 //和客户端建立连接的函数 //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; }然后是 recvHttpRequest 函数 //接收 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(totallen sizeof buf){//这时候再进行数据拷贝memcpy(buftotal,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);}//打印一下线程idprintf(recvMsg threadId: %ld\n,info-tid);//数据通信结束关闭用于通信的fdepfd不关闭因为还要使用呢//如果尝试关闭一个已经关闭的文件描述符//大多数Unix-like系统包括Ubuntu会忽略这个操作//并返回错误通常是EBADF表示“Bad file descriptor”。//然而这种错误通常不会导致程序崩溃除非程序没有正确处理这个错误。close(info-fd);//回收info堆空间资源free(info);return 0; }总结以及源码地址 至此本项目作为一个简易的、基于 epoll多线程 技术实现的 Web Server 项目就结束了。 源码地址已经开源在我的 gitee 仓库欢迎 star 链接在这里 epoll多线程 实现的简易版本 Web Server 项目
http://www.tj-hxxt.cn/news/133917.html

相关文章:

  • 各省网站备案时长网站建设 创业
  • 化工网站模板网站制作收费标准
  • 西昌手机网站制作the7做的网站
  • 咨询类网站建设wordpress实例教程
  • 企业网站建设的成本phicomm怎么做网站
  • 怎么免费上传网页网站网页空间的利用要
  • 新余网站网站建设上海 网站 备案
  • 做网站的公司面试帆客建设网站
  • 媒体门户网站建设方案深圳公司名称
  • 医馆网站建设方案重庆佳宇建设集团网站
  • 企业网站的种类校园网站建设考评办法
  • 河北邢台官方网站odoo做网站
  • 小说网站开发需求站点推广促销
  • 申请一个域名后怎么做网站wordpress手赚推广
  • 平原县网站seo优化排名云南建投第十建设有限公司网站
  • 百度网站开发基于什么语言站群网站源码
  • 做房产抵押网站需要什么做国外直播网站
  • 怎么建网站做淘宝客页面设置
  • 全国城市雕塑建设官方网站网站 主营业务
  • wordpress多功能图片主题windows优化大师有用吗
  • 白云网站建设价格广告网站定制
  • 石家庄智能模板建站萝岗网站建设制作
  • 云服务器上建网站wordpress怎么上传文件
  • 网站视频大全字体设计网
  • 做企业网站推广多少钱中国有哪些软件公司
  • 做营销网站视频中国建设教育协会的官方网站
  • 手机怎么开网站wordpress 文章没内容
  • 陕西省建设监理协会官方网站营销型企业网站建站
  • 网站信息备案查询淘宝网站运营的工作怎么做
  • 怎样免费做网站视频讲解skype在网站上怎么做链接