基于多反应堆的高并发服务器【C/C++/Reactor】(上)

(一)初始化服务器端用于监听的套接字

  • Server.h
#pragma once 
// 初始化监听的套接字
int initListenFd(unsigned short port);
  • Server.c
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(ret == -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;
    }
    // 返回fd
    return lfd;
}

>>>>>>>>>>>>>>>>>>>>>>>>>>>>知识回顾>>>>>>>>>>>>>>>>>>>>>>>>>>>>

1. socket 

// 套接字通信分两部分:
    - 服务器端:被动接受连接,一般不会主动发起连接
    - 客户端:主动向服务器发起连接

 2.字节序转换函数

    当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。
    解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端
    知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定
    是否对接收到的数据进行转换(小端机转换,大端机不转换)。
    
    网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统
    等无关,从而 可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端
    排序方式。
    
    BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的
    转换函数: htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
  • h - host 主机,主机字节序
  • to - 转换成什么
  • n - network 网络字节序
  • s - short unsigned short
  • l - long unsigned int
#include <arpa/inet.h>
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); //  网络字节序 - 主机字节序

3.socket 地址

socket 地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中
需要使用到这个socket地址。客户端 -> 服务端(IP,Port)
#include <netinet/in.h>
struct sockaddr_in
{
    sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
    in_port_t sin_port; /* Port number. */
    struct in_addr sin_addr; /* Internet address. */
    /* Pad to size of `struct sockaddr'. */
    unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
                sizeof (in_port_t) - sizeof (struct in_addr)];
};
struct in_addr
{
    in_addr_t s_addr;
};

4. IP地址转换(字符串ip-整数 ,主机、网络 字节序的转换)

通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用 十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。下面 3 个函数可用于用点分十进制字 符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:

#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);

下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:

#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
    af:地址族: AF_INET AF_INET6
    src:需要转换的点分十进制的IP字符串
    dst:转换后的结果保存在这个里面
 
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    af:地址族: AF_INET AF_INET6
    src: 要转换的ip的整数的地址
    dst: 转换成IP地址字符串保存的地方
    size:第三个参数的大小(数组的大小)
    返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

5. TCP通信流程

// TCP 和 UDP -> 传输层的协议
UDP:用户数据报协议,面向无连接,可以单播,多播,广播, 面向数据报,不可靠
TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输
 
 
                   UDP                                     TCP
是否创建连接       无连接                                  面向连接
是否可靠           不可靠                                   可靠的
连接的对象个数   一对一、一对多、多对一、多对多              支持一对一
传输的方式         面向数据报                              面向字节流
首部开销           8个字节                               最少20个字节
适用场景        实时应用(视频会议,直播)           可靠性高的应用(文件传输)

// TCP 通信的流程
// 服务器端 (被动接受连接的角色)
1. 创建一个用于监听的套接字
    - 监听:监听有客户端的连接
    - 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
    - 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字
(fd)
5. 通信
    - 接收数据
    - 发送数据
6. 通信结束,断开连接
// 客户端
1. 创建一个用于通信的套接字(fd)
2. 连接服务器,需要指定连接的服务器的 IP 和 端口
3. 连接成功了,客户端可以直接和服务器通信
    - 接收数据
    - 发送数据
4. 通信结束,断开连接

6. 套接字函数

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
    - 功能:创建一个套接字
    - 参数:
        - domain: 协议族
            AF_INET : ipv4
            AF_INET6 : ipv6
            AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
        - type: 通信过程中使用的协议类型
            SOCK_STREAM : 流式协议
            SOCK_DGRAM : 报式协议
    - protocol : 具体的一个协议。一般写0
            - SOCK_STREAM : 流式协议默认使用 TCP
            - SOCK_DGRAM : 报式协议默认使用 UDP
    - 返回值:
        - 成功:返回文件描述符,操作的就是内核缓冲区。
        - 失败:-1
 
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命
名
	- 功能:绑定,将fd 和本地的IP + 端口进行绑定
	- 参数:
		- sockfd : 通过socket函数得到的文件描述符
		- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
		- addrlen : 第二个参数结构体占的内存大小
		
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
	- 功能:监听这个socket上的连接
	- 参数:
		- sockfd : 通过socket()函数得到的文件描述符
		- backlog : 未连接的和已经连接的和的最大值, 5
		
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
	- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
	- 参数:
		- sockfd : 用于监听的文件描述符
		- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
		- addrlen : 指定第二个参数的对应的内存大小
	- 返回值:
		- 成功 :用于通信的文件描述符
		- 失败 : -1
		
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	- 功能: 客户端连接服务器
	- 参数:
		- sockfd : 用于通信的文件描述符
		- addr : 客户端要连接的服务器的地址信息
		- addrlen : 第二个参数的内存大小
	- 返回值:成功 0, 失败 -1
 
//读写数据
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据

7.SIGCHLD信号

SIGCHLD的产生条件:

  • 子进程终止
  • 子进程接收到SIGSTOP信号停止时
  • 子进程处于停止状态,接收到SIGCONT后唤醒

注意:通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。

(二)epoll 工作模型的雏形

  • Server.h
// 启动epoll
int epollRun(int lfd);
  • Server.c
int epollRun(int lfd) {
    // 1.创建epoll实例
    int epfd = epoll_create(1);
    if(epfd == -1) {
        perror("epoll_create");
        return -1;
    }
    // 2.添加监听fd lfd上树 对于监听的描述符来说只需要看一下有没有新的客户端连接
    struct epoll_event ev;
    ev.data.fd = lfd;
    ev.events = EPOLLIN;// 委托epoll(内核)帮我们检测lfd的读事件
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
    if(ret == -1) {
        perror("epoll_ctl");
        return -1;
    }
    // 3.检测
    struct epoll_event evs[1024];
    // int size = sizeof(evs)/sizeof(epoll_event);
    int size = sizeof(evs)/sizeof(evs[0]);
    while(1) {
        int num = epoll_wait(epfd,evs,size,-1);
        if(num == -1) {
            perror("epoll_wait");
            return -1;
        }
        for(int i=0;i<num;++i) {
            int fd = evs[i].data.fd;
            if(fd == lfd) {
                // 建立新连接 accept
                acceptClient(lfd,epfd);
            }else{
                // 主要是接收对端的数据
                recvHttpRequest(fd,epfd);
            }

        }
    }
    return 0;
}

>>>>>>>>>>>>>>>>>>>>>>>>>>>>知识回顾>>>>>>>>>>>>>>>>>>>>>>>>>>>>

1.epoll() 多路复用 和 两种工作模式

epollLinux内核中的一个事件驱动I/O机制,用于处理多个文件描述符上的事件。它是一个高效且强大的 I/O 多路复用工具,可以用于处理大量文件描述符的 I/O操作epoll 的主要优点是它只占用较少的资源,并且比传统的 select 和 poll 更易于使用。

epoll的工作原理是通过一个事件表来跟踪所有需要监控的文件描述符,当某个文件描述符上有事件发生时,epoll会通知程序去处理这些事件。这种方式可以确保程序在等待某个文件描述符上有事件发生时只占用较少的资源,而不是像selectpoll那样整个程序

----来自CodeGeex

2.epoll API介绍

typedef union epoll_data {
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;
 
struct epoll_event {
	uint32_t events; /* Epoll events */
	epoll_data_t data; /* User data variable */
};
 
常见的Epoll检测事件:
	- EPOLLIN
	- EPOLLOUT
	- EPOLLERR
	
// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
	- 参数:
		- epfd : epoll实例对应的文件描述符
		- op : 要进行什么操作
				EPOLL_CTL_ADD: 添加
				EPOLL_CTL_MOD: 修改
				EPOLL_CTL_DEL: 删除
		- fd : 要检测的文件描述符
		- event : 检测文件描述符什么事情
 
// 检测函数----检测epoll树中是否有就绪的文件描述符
// 创建了epfd,设置好某个fd上需要检测事件并将该fd绑定到epfd上去后,就可以调用epoll_wait
// 检测事件了
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
	- 参数:
		- epfd : epoll实例对应的文件描述符
		- events : 传出参数,保存了发送了变化的文件描述符的信息
		- maxevents : 第二个参数结构体数组的大小
		- timeout : 阻塞时间
			- 0 : 不阻塞
			- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
			- > 0 : 阻塞的时长(毫秒)
	- 返回值:
		- 成功,返回发送变化的文件描述符的个数 > 0
		- 失败 -1
 
// 创建epoll实例,通过一棵红黑树管理待检测集合
// 参数 size 从 Linux 2.6.8 以后就不再使用,但是必须设置一个大于 0 的值。epoll_create 函数调用成功返回一个非负值的 epollfd,调用失败返回 -1。
int epoll_create(int size);
>>epoll_wait 缺点:
    ① epoll_wait 调用之后,需要将所有fd的event参数重新设置一遍,
      如果fd比较多的话,会比较消耗性能。----来自CodeGeeX
 
>>epoll_wait 优点:
    ① epoll_wait 调用之后,直接在event参数中拿到所有有事件就绪的fd,直接处理即可。
    ② 一般在fd数量比较多,但某段时间内,就绪事件fd数量较少的情况下,epoll_wait才会
    体现出它的优势,也就是说socket连接数量较大时而活跃连接较少时epoll模型更高效。

// epoll 的使用
// 操作步骤
// 在服务器使用 epoll 进行 IO 多路转接的操作步骤如下:
    1.创建监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
 
    2.设置端口复用(可选)
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
 
    3.使用本地的IP与端口和监听的套接字进行绑定
    int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
 
    4.给监听的套接字设置监听
    listen(lfd, 128);
 
    5.创建 epoll 实例
    int epfd = epoll_create(100);
 
    6.将用于监听的套接字添加到 epoll 实例中
    struct epoll_event ev;
    ev.events = EPOLLIN; //检测lfd读缓冲区是否有数据
    ev.data.fd = lfd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
 
    接着创建一个数组,用于存储epoll_wait()返回的文件描述符
    struct epoll_event evs[1024];
 
    7.检测添加到epoll实例中的文件描述符是否已经就绪,并将这些已就绪的文件描述符进行处理
    int num = epoll_wait(epfd, evs, size, -1);
 
    ① 如果监听的是文件描述符,和新客户端建立连接,将得到的文件描述符添加到epoll实例中
    int cfd = accept(curfd,NULL,NULL);
    ev.events = EPOLLIN;
    ev.data.fd = cfd;
 
    新得到的文件描述符添加到epoll模型中,下一轮循环的时候就可以被检测了
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
 
    ② 如果是通信的文件描述符,和对应的客户端通信,如果连接已断开,将该文件描述符从epoll实例中删除
    int len = recv(curfd,buf,sizeof(buf),0);
    if(len == 0) {
        // 将这个文件描述符从epoll实例中删除
        epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
        close(curfd);
    }else if(len > 0) {
        send(curfd,buf,len,0);
    }
 
    8.重复第 7 步的操作

3.epoll 的两种工作模式 
Epoll 的工作模式:
	LT 模式 (水平触发)
		假设委托内核检测读事件 -> 检测fd的读缓冲区
			读缓冲区有数据 - > epoll检测到了会给用户通知
				a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
				b.用户只读了一部分数据,epoll会通知
				c.缓冲区的数据读完了,不通知
	
	LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这
	种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操
	作。如果你不作任何操作,内核还是会继续通知你的。
 
	ET 模式(边沿触发)
		假设委托内核检测读事件 -> 检测fd的读缓冲区
			读缓冲区有数据 - > epoll检测到了会给用户通知
				a.用户不读数据,数据一直在缓冲区中,epoll下次检测的时候就不通知了
				b.用户只读了一部分数据,epoll不通知
				c.缓冲区的数据读完了,不通知
 
	ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述
	符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,
	并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述
	符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成
	未就绪),内核不会发送更多的通知(only once)。
	
	ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll
	工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写
	操作把处理多个文件描述符的任务饿死。
 
 
综上所述:epoll的边沿模式下 epoll_wait检测到文件描述符有新事件才会通知,
如果不是新的事情就不通知,通知的次数比水平模式少,效率比水平模式高。

 【注意】 ET模式需要配合循环+非阻塞

>> epoll在边沿模式下非阻塞接收数据
    循环接收数据的处理方式:对于每次接收的buffer多小都不重要了,只不过我们需要多接收几次数据。
    效率相对来说低一些;如果说buffer稍微大一点,接收数据的次数就少一些,效率相对来说高一些;
    可以把recv写到一个while循环里,通过while循环,每次读取5个字节,直到把客户端发过来的数据全部都读到本地。
    【思考】这种方式的弊端在哪里?
    【思考】进行套接字通信时阻塞的还是非阻塞的?
 
    【回答】很显然默认情况下进行套接字通信,这个处理流程是阻塞的。如果是阻塞的,
    当这个服务器端循环接收客户端发过来的数据,假设客户端发来了100个字节的数据,
    在服务端接收了20次,就把客户端发过来的数据全部读到本地了,但是在做第21次读
    数据的时候,这个recv它还能读到数据吗?
    没有了,也就是说这个文件描述符对应的读缓冲区里边是空的。如果说这个文件描述符
    对应的读缓冲区里边是空的。这个recv再去接收数据的话,服务器端的线程或者服务器
    端的进程它就阻塞了。如果这个线程/进程阻塞了,就不能干别的事情了。如果说写的
    这个程序里边就是单线程或者单进程的程序,在这里阻塞了,就不能够去做其他的事情
    了,整个程序就停止在这里了。
 
    【问题】如何让while循环中的break起作用?
        修改文件描述符为非阻塞,而不是修改read/recv函数,因为这函数时基于文件描述符
        去进行数据的接收操作,所以说需要修改一下这个文件描述符的属性,把这个文件描述
        符的默认阻塞属性修改为非阻塞属性。再次调用recv/read函数的时候,它们也就不会阻塞了
 
    【思考】如何把这个文件描述符修改为非阻塞属性?
        解决阻塞问题,需要将套接字默认的阻塞行为修改为非阻塞,需要使用fcntl()函数进行处理
 
        // 设置完成之后,读写都变成了非阻塞模式
        int flag = fcntl(cfd,F_GETFL);
        flag |= O_NOBLOCK;
        fcntl(cfd,F_SETFL,flag);  

(三)和客户端建立新连接

  • Server.h
// 和客户端建立连接
int acceptClient(int lfd,int epfd);
  • Server.c
// 接受新连接,把得到的新的文件描述符cfd也添加到epoll树上
int acceptClient(int lfd,int epfd) {
    // 1.创建一个套接字(建立连接)
    int cfd = accept(lfd,NULL,NULL);
    if(cfd == -1) {
        perror("accept");
        return -1;
    }
    // 2.添加到epoll中
    int flag = fcntl(cfd,F_GETFL);// 设置非阻塞
    flag |= O_NONBLOCK;
    fcntl(cfd,F_SETFL,flag);

    // 3.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");
        return -1;
    }
    return 0;
}

(四)接收客户端的http请求消息

WebServer 解析HTTP 请求报文-CSDN博客icon-default.png?t=N7T8https://heheda.blog.csdn.net/article/details/132695415WebServer 解析HTTP 响应报文-CSDN博客icon-default.png?t=N7T8https://heheda.blog.csdn.net/article/details/132746046

  • Server.h
// 主要是接收对端的数据
int recvHttpRequest(int cfd,int epfd);
  • Server.c
int recvHttpRequest(int cfd,int epfd) {
    // 有了存储数据的内存之后,接下来就是读数据
    // 注意:前面已经把用于通信的文件描述符的事件改成了边缘非阻塞
    // 如果是边缘模式epoll检测到文件描述符对应的读事件之后,只会给我们通知一次
    // 因此需要得到这个通知之后,
    printf("开始接收数据了...\n");
    int len = 0,total = 0;
    char buf[4096] = {0};
    char tmp[1024] = {0};
    while((len = recv(cfd,tmp,sizeof tmp,0))>0) {
        if(total + len < sizeof buf)
            memcpy(buf+total,tmp,len);
        total += len;
    }
    
    // 判断数据是否接收完毕
    if(len == -1 && errno == EAGAIN) {
        // 说明服务器已经把客户端发过来的请求数据接收完毕了
        // 解析请求行
        char* pt = strstr(buf,"\r\n");
        int reqLen = pt - buf;
        buf[reqLen] = '\0';
        parseRequestLine(buf,cfd);
    }
    else if(len == 0) {
        // 说明客户端断开了连接
        int ret = epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL);
        if(ret == -1) {
            perror("epoll_ctl");
            return -1;
        }
        close(cfd);
    }else{
        perror("recv");
    }
    return 0;
}

(五)解析请求行

  • Server.h 
// 解析请求行
int parseRequestLine(const char* line,int cfd);
  • Server.c
int parseRequestLine(const char* line,int cfd){
    // 解析请求行 
    // 请求行格式:GET /index.html HTT   -------------------P/1.1
    // 解析出请求方法、请求路径、协议版本
    // 请求方法:GET
    // 请求路径:/index.html
    // 协议版本:HTTP/1.1
    // 请求方法GET、请求路径/index.html、协议版本HTTP/1.1
    // 请求行长度:GET /index.html HTTP/1.1
    // 请求行长度:29
    char method[12];
    char path[1024];
    sscanf(line,"%[^ ] %[^ ]",method,path);
    printf("method: %s,path: %s\n",method,path);
    if(strcasecmp(method,"get") != 0) {
        return -1;
    }
    decodeMsg(path,path);
    // 处理客户端请求的静态资源(目录或者文件)
    char* file = NULL; 
    if(strcmp(path,"/") == 0) {
        file = "./";
    }else {
        file = path+1;
    }
    // 获取文件属性
    struct stat st;
    int ret = stat(file,&st);
    if(ret == -1) {
        // 文件不存在 -- 回复404
        sendHeadMsg(cfd,404,"Not Found",getFileType(".html"),-1);
        sendFile("404.html",cfd);
        return 0;
    }
    // 判断文件类型
    if(S_ISDIR(st.st_mode)) {
        // 把这个目录中的内容发送给客户端
        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;
}

(六)组织Http响应的数据块头

  • Server.h  
// 发送响应头(状态行 + 响应头)
int sendHeadMsg(int cfd,int status,const char* descr,const char* type,int length);
const char* getFileType(const char* name);
  • Server.c
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);
    // 响应头
    sprintf(buf+strlen(buf),"content-type: %s\r\n",type);
    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,'.');
    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";//纯文本
}

(七)发送文件的两种方式

  • Server.h  
// 发送文件
int sendFile(const char* fileName,int cfd);
  • Server.c
int sendFile(const char* fileName,int cfd) {
    // 打开文件
    int fd = open(fileName,O_RDONLY);
    // assert(fd > 0);
    if(fd == -1){
        perror("open");
        return -1;
    }
#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
    // 把文件内容发送给客户端
    // int size = lseek(fd,0,SEEK_END);// 文件指针移动到了尾部
    // lseek(fd,0,SEEK_SET);
    // int ret = sendfile(cfd,fd,NULL,size);
    // off_t offset = 0;
    // while (offset < size){
    //     int ret = sendfile(cfd,fd,&offset,size- offset);
    //     printf("ret value: %d\n",ret);
    //     if (ret == -1 && errno == EAGAIN)
    //     {
    //         printf("没数据...\n");
    //     }
    // }
    struct stat st;
    fstat(fd,&st);
    off_t offset = 0;
    while (offset < st.st_size){
        int ret = sendfile(cfd,fd,&offset,st.st_size- offset);
        printf("ret value: %d\n",ret);
        if (ret == -1 && errno == EAGAIN)
        {
            printf("没数据...\n");
        }
    }
#endif
    close(fd);
    return 0;
}

(八)发送目录

  • Server.h
// 发送目录
int sendDir(const char* dirName,int cfd); 
  • Server.c
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 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;
        char subPath[1024] = {0};
        sprintf(subPath,"%s/%s",dirName,name);
        stat(subPath,&st);
        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{
            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]); 
    }
    sprintf(buf,"</table></body></html>");
    send(cfd,buf,strlen(buf),0);
    free(nameList);
    return 0;
}
/*
<html>
    <head>
        <title>test</title>
    </head>
    <body>
        <table>
            <tr>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td></td>
                <td></td>
            </tr>
        </table>
    </body>
</html>
*/

(八)解决浏览器无法访问带特殊字符的文件得到问题

  • Server.h
int hexToDec(char c);
void decodeMsg(char* to,char* from);
  • Server.c 
// 将字符转换为整型数
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 == '%' && isxdigit(from[1]) && isxdigit(from[2])){
            // 将16进制的数 -> 十进制 将这个数值赋值给了字符 int -> char
            // B2 == 178
            // 将3个字符, 变成了一个字符, 这个字符就是原始数据
            // *to = (hexToDec(from[1]) * 16) + hexToDec(from[2]);
            *to = (hexToDec(from[1]) << 4) + hexToDec(from[2]);

            // 跳过 from[1] 和 from[2] ,因此在当前循环中已经处理过了
            from += 2;
        }else{
            // 字符拷贝,赋值
            *to = *from;
        }
    }
    *to = '\0';
}

完整代码:

main.c

#include <stdio.h>
#include "Server.h"
#include "Server.c"
#include <unistd.h>
#include <stdlib.h>

int main(int argc,char* argv[]) {
    if(argc < 3) {
        printf("./a.out port path\n");
        return -1;
    }
    unsigned short port = atoi(argv[1]);
    // 切换服务器的工作路径
    chdir(argv[2]);
    // 初始化用于监听的套接字
    int lfd = initListenFd(port);
    // 启动服务器程序
    epollRun(lfd);
    return  0;
}

Server.c

#include "Server.h"
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <dirent.h>
#include <assert.h>
#include <ctype.h>
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(ret == -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;
    }
    // 返回fd
    return lfd;
}

int epollRun(int lfd) {
    // 1.创建epoll实例
    int epfd = epoll_create(1);
    if(epfd == -1) {
        perror("epoll_create");
        return -1;
    }
    // 2.添加监听fd lfd上树 对于监听的描述符来说只需要看一下有没有新的客户端连接
    struct epoll_event ev;
    ev.data.fd = lfd;
    ev.events = EPOLLIN;// 委托epoll(内核)帮我们检测lfd的读事件
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
    if(ret == -1) {
        perror("epoll_ctl");
        return -1;
    }
    // 3.检测
    struct epoll_event evs[1024];
    // int size = sizeof(evs)/sizeof(epoll_event);
    int size = sizeof(evs)/sizeof(evs[0]);
    while(1) {
        int num = epoll_wait(epfd,evs,size,-1);
        if(num == -1) {
            perror("epoll_wait");
            return -1;
        }
        for(int i=0;i<num;++i) {
            int fd = evs[i].data.fd;
            if(fd == lfd) {
                // 建立新连接 accept
                acceptClient(lfd,epfd);
            }else{
                // 主要是接收对端的数据
                recvHttpRequest(fd,epfd);
            }

        }
    }
    return 0;
}

// 接受新连接,把得到的新的文件描述符cfd也添加到epoll树上
int acceptClient(int lfd,int epfd) {
    // 1.创建一个套接字(建立连接)
    int cfd = accept(lfd,NULL,NULL);
    if(cfd == -1) {
        perror("accept");
        return -1;
    }
    // 2.添加到epoll中
    int flag = fcntl(cfd,F_GETFL);// 设置非阻塞
    flag |= O_NONBLOCK;
    fcntl(cfd,F_SETFL,flag);

    // 3.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");
        return -1;
    }
    return 0;
}

int recvHttpRequest(int cfd,int epfd) {
    // 有了存储数据的内存之后,接下来就是读数据
    // 注意:前面已经把用于通信的文件描述符的事件改成了边缘非阻塞
    // 如果是边缘模式epoll检测到文件描述符对应的读事件之后,只会给我们通知一次
    // 因此需要得到这个通知之后,
    printf("开始接收数据了...\n");
    int len = 0,total = 0;
    char buf[4096] = {0};
    char tmp[1024] = {0};
    while((len = recv(cfd,tmp,sizeof tmp,0))>0) {
        if(total + len < sizeof buf)
            memcpy(buf+total,tmp,len);
        total += len;
    }
    
    // 判断数据是否接收完毕
    if(len == -1 && errno == EAGAIN) {
        // 说明服务器已经把客户端发过来的请求数据接收完毕了
        // 解析请求行
        char* pt = strstr(buf,"\r\n");
        int reqLen = pt - buf;
        buf[reqLen] = '\0';
        parseRequestLine(buf,cfd);
    }
    else if(len == 0) {
        // 说明客户端断开了连接
        int ret = epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL);
        if(ret == -1) {
            perror("epoll_ctl");
            return -1;
        }
        close(cfd);
    }else{
        perror("recv");
    }
    return 0;
}
int parseRequestLine(const char* line,int cfd){
    // 解析请求行 
    // 请求行格式:GET /index.html HTT   -------------------P/1.1
    // 解析出请求方法、请求路径、协议版本
    // 请求方法:GET
    // 请求路径:/index.html
    // 协议版本:HTTP/1.1
    // 请求方法GET、请求路径/index.html、协议版本HTTP/1.1
    // 请求行长度:GET /index.html HTTP/1.1
    // 请求行长度:29
    char method[12];
    char path[1024];
    sscanf(line,"%[^ ] %[^ ]",method,path);
    printf("method: %s,path: %s\n",method,path);
    if(strcasecmp(method,"get") != 0) {
        return -1;
    }
    decodeMsg(path,path);
    // 处理客户端请求的静态资源(目录或者文件)
    char* file = NULL; 
    if(strcmp(path,"/") == 0) {
        file = "./";
    }else {
        file = path+1;
    }
    // 获取文件属性
    struct stat st;
    int ret = stat(file,&st);
    if(ret == -1) {
        // 文件不存在 -- 回复404
        sendHeadMsg(cfd,404,"Not Found",getFileType(".html"),-1);
        sendFile("404.html",cfd);
        return 0;
    }
    // 判断文件类型
    if(S_ISDIR(st.st_mode)) {
        // 把这个目录中的内容发送给客户端
        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;
}

int sendFile(const char* fileName,int cfd) {
    // 打开文件
    int fd = open(fileName,O_RDONLY);
    // assert(fd > 0);
    if(fd == -1){
        perror("open");
        return -1;
    }
#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
    // 把文件内容发送给客户端
    // int size = lseek(fd,0,SEEK_END);// 文件指针移动到了尾部
    // lseek(fd,0,SEEK_SET);
    // int ret = sendfile(cfd,fd,NULL,size);
    // off_t offset = 0;
    // while (offset < size){
    //     int ret = sendfile(cfd,fd,&offset,size- offset);
    //     printf("ret value: %d\n",ret);
    //     if (ret == -1 && errno == EAGAIN)
    //     {
    //         printf("没数据...\n");
    //     }
    // }
    struct stat st;
    fstat(fd,&st);
    off_t offset = 0;
    while (offset < st.st_size){
        int ret = sendfile(cfd,fd,&offset,st.st_size- offset);
        printf("ret value: %d\n",ret);
        if (ret == -1 && errno == EAGAIN)
        {
            printf("没数据...\n");
        }
    }
#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);
    // 响应头
    sprintf(buf+strlen(buf),"content-type: %s\r\n",type);
    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,'.');
    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 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;
        char subPath[1024] = {0};
        sprintf(subPath,"%s/%s",dirName,name);
        stat(subPath,&st);
        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{
            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]); 
    }
    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;
    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 == '%' && isxdigit(from[1]) && isxdigit(from[2])){
            // 将16进制的数 -> 十进制 将这个数值赋值给了字符 int -> char
            // B2 == 178
            // 将3个字符, 变成了一个字符, 这个字符就是原始数据
            // *to = (hexToDec(from[1]) * 16) + hexToDec(from[2]);
            *to = (hexToDec(from[1]) << 4) + hexToDec(from[2]);

            // 跳过 from[1] 和 from[2] ,因此在当前循环中已经处理过了
            from += 2;
        }else{
            // 字符拷贝,赋值
            *to = *from;
        }
    }
    *to = '\0';
}

Server.h

#pragma once 
// 初始化监听的套接字
int initListenFd(unsigned short port);
// 启动epoll
int epollRun(int lfd);
// 和客户端建立连接
int acceptClient(int lfd,int epfd);
// 主要是接收对端的数据
int recvHttpRequest(int cfd,int epfd);
// 解析请求行
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); 
int hexToDec(char c);
void decodeMsg(char* to,char* from);

演示效果: