基于epoll实现的简单httpserver
作者:互联网
基于epoll实现的httpserver端
流程:
1 main函数: 传入端口号, 传入 想要访问的目录。
通过atoi 函数将获取到的port转成integer , 通过chdir将当前进程的工作路径转为要访问的目录。 然后将端口传入到自定义的epoll_run函数中。
2 epoll_run 函数: 传入端口号
实现epoll的三个主要 操作 1、新建epoll红黑树 2、将事件挂到树根上 3、 监听事件
- 通过epoll_create函数创建树根。
- 通过自定义的init_listen_fd函数完成 lfd的创建和监听。
- while中处理监听事件,通过访问由epoll传出的event数组,依次处理连接或通信请求。连接请求通过do_accept函数实现, 通信访问资源通过do_read 函数实现。
3 init_listen_fd :传入端口号和在run中生成的epoll树根 返回值为listen文件描述符lfd
- 创建套接字lfd = socket(AF_INET, SOCK_STREAM, 0);
- 创建服务器地址结构 IP+port
- 设置端口复用
- 绑定地址结构
- 设置监听上限
- 通过epoll_ctl函数将lfd 挂到红黑树根上
- 返回lfd
4 do_accept()传入lfd ,红黑树根
- 通过accept得到cfd ——connect fd
- 设置非阻塞方式
- 将cfd 挂到红黑树根中
- 设置ET模式——边沿非触发模式
5 do_read () 传入cfd 红黑树根 处理http协议
读取第一行http协议头 拆分成三部分: GET 文件名 协议号 例如GET /hello.c HTTP/1.1
如果第一行为空,则表示为客户端要与服务器端口连接。执行dis_connect 函数
否则用三个数组依次接收上面的三部分。 将发送过来的请求不断读到缓冲区。
通过c中函数strcasecmp(method, "GET", 3)比对前三个字符是否为GET ,如果是就获取到想要访问的文件名然后通过http_request 处理get请求,处理完之后断开连接。
6 http_request()传入cfd , 文件名
通过stat函数判断文件是否存在,并且把文件放到缓冲区 。
如果是一个普通文件就要给客户端发送HTTP应答和所要请求的数据。 发送应答通过send_respond 实现 发送文件用 send_file实现
7 send_respond 传入客户端端的fd, 错误号,错误描述,回发文件类型, 文件长度
通过
sprintf(buf, "HTTP/1.1 %d %s\r\n", no, disp);
send(cfd, buf, strlen(buf), 0);
等函数把需要的东西放到buf中 再通过send 函数发送到cfd
8 send_file 传入cfd 文件名
用open 函数打开的服务器本地文件。 --- cfd 能访问客户端的 socket
open成功后通过read函数读到buf中, 再通过send函数发送出去
在send函数的返回值中有两个小细节。 当返回值为-1 时 表示发送失败。但是这个时候我们不能直接退出进程。 因为在错误号中有两个错误我们要注意: EAGAIN 和EINTR
- EAGAIN :如字面意思, 就是让我们再发一次, 它是怎么产生的呢, 它通常出现在应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候,write\send将要发送的数据提交到发送缓冲区后是立即返回的,并不需要对端确认数据已接收。在这种情况下是很有可能出现
发送缓冲区被填满
,导致write\send无法再向缓冲区提交要发送的数据.
其中发送缓冲区是指 :send()仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中 而内核buffer就是发送缓冲区。发送是TCP的事情,和send其实没有太大关系。
如:以 O_NONBLOCK的标志打开文件/socket/FIFO,如果连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
所以针对这种ERROR,我们不能直接退出进程,要continue 使其重发。
- EINTR:系统调用中断, 同样continue即可。
以上两种的其他错误情况就要退出进程。
点击查看代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#define MAXSIZE 2048
// 获取一行 \r\n 结尾的数据
int get_line(int cfd, char *buf, int size)
{
int i = 0;
char c = '\0';
int n;
while ((i < size-1) && (c != '\n')) {
n = recv(cfd, &c, 1, 0);
if (n > 0) {
if (c == '\r') {
n = recv(cfd, &c, 1, MSG_PEEK);
if ((n > 0) && (c == '\n')) {
recv(cfd, &c, 1, 0);
} else {
c = '\n';
}
}
buf[i] = c;
i++;
} else {
c = '\n';
}
}
buf[i] = '\0';
if (-1 == n)
i = n;
return i;
}
int init_listen_fd(int port, int epfd)
{
// 创建监听的套接字 lfd
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
perror("socket error");
exit(1);
}
// 创建服务器地址结构 IP+port
struct sockaddr_in srv_addr;
bzero(&srv_addr, sizeof(srv_addr)); //清空地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(port);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 给 lfd 绑定地址结构
int ret = bind(lfd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
if (ret == -1) {
perror("bind error");
exit(1);
}
// 设置监听上限
ret = listen(lfd, 128);
if (ret == -1) {
perror("listen error");
exit(1);
}
// lfd 添加到 epoll 树上
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1) {
perror("epoll_ctl add lfd error");
exit(1);
}
return lfd;
}
void do_accept(int lfd, int epfd)
{
struct sockaddr_in clt_addr;
socklen_t clt_addr_len = sizeof(clt_addr);
int cfd = accept(lfd, (struct sockaddr*)&clt_addr, &clt_addr_len);
if (cfd == -1) {
perror("accept error");
exit(1);
}
// 打印客户端IP+port
char client_ip[64] = {0};
printf("New Client IP: %s, Port: %d, cfd = %d\n",
inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
ntohs(clt_addr.sin_port), cfd);
// 设置 cfd 非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 将新节点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 add cfd error");
exit(1);
}
}
// 断开链接
void disconnect(int cfd, int epfd)
{
int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
if (ret != 0) {
perror("epoll_ctl error");
exit(1);
}
close(cfd);
}
// 客户端端的fd, 错误号,错误描述,回发文件类型, 文件长度
void send_respond(int cfd, int no, char *disp, char *type, int len)
{
char buf[4096] = {0};
sprintf(buf, "HTTP/1.1 %d %s\r\n", no, disp);
send(cfd, buf, strlen(buf), 0);
memset(buf, 0, sizeof(buf));
sprintf(buf, "Content-Type:%s\r\n", type);
sprintf(buf+strlen(buf), "Content-Length:%d\r\n", len);
send(cfd, buf, strlen(buf), 0);
send(cfd, "\r\n", 2, 0);
}
// 发送服务器本地文件 给浏览器
void send_file(int cfd, const char *file)
{
int n = 0, ret;
char buf[4096] = {0};
// 打开的服务器本地文件。 --- cfd 能访问客户端的 socket
int fd = open(file, O_RDONLY);
if (fd == -1) {
// 404 错误页面
perror("open error");
exit(1);
}
while ((n = read(fd, buf, sizeof(buf))) > 0) {
ret = send(cfd, buf, n, 0);
if (ret == -1) {
printf("errno = %d\n", errno);
if (errno == EAGAIN) {
printf("-----------------EAGAIN\n");
continue;
} else if(errno == EINTR) {
printf("-----------------EINTR\n");
continue;
} else {
perror("send error");
exit(1);
}
}
if (ret < 4096)
printf("-----send ret: %d\n", ret);
}
close(fd);
}
// 处理http请求, 判断文件是否存在, 回发
void http_request(int cfd, const char *file)
{
struct stat sbuf;
// 判断文件是否存在
int ret = stat(file, &sbuf);
if (ret != 0) {
perror("stat");
exit(1);
}
if(S_ISREG(sbuf.st_mode)) { // 是一个普通文件
send_respond(cfd, 200, "OK", "text/plain; charset=iso-8859-1", -1);
// 回发 给客户端请求数据内容。
send_file(cfd, file);
}
}
void do_read(int cfd, int epfd)
{
// 读取一行http协议, 拆分, 获取 get 文件名 协议号
char line[1024] = {0};
char method[16], path[256], protocol[16];
int len = get_line(cfd, line, sizeof(line)); //读http请求协议首行 GET /hello.c HTTP/1.1
if (len == 0) {
printf("客户端关闭....\n");
disconnect(cfd, epfd);
} else {
sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
printf("method=%s, path=%s, protocol=%s\n", method, path, protocol);
while (1) {
char buf[1024] = {0};
len = get_line(cfd, buf, sizeof(buf));
if (buf[0] == '\n') {
break;
} else if (len == -1)
break;
}
}
if (strncasecmp(method, "GET", 3) == 0)
{
char *file = path+1; // 取出 客户端要访问的文件名
http_request(cfd, file);
disconnect(cfd, epfd);
}
}
void epoll_run(int port)
{
int i = 0;
struct epoll_event all_events[MAXSIZE]; //用event数组接收监听事件,为wait的传出做准备
// 创建一个epoll监听树根
int epfd = epoll_create(MAXSIZE);
if (epfd == -1) {
perror("epoll_create error");
exit(1);
}
// 创建lfd,并添加至监听树
int lfd = init_listen_fd(port, epfd);
while (1) {
// 监听节点对应事件
int ret = epoll_wait(epfd, all_events, MAXSIZE, 0);
if (ret == -1) {
perror("epoll_wait error");
exit(1);
}
for (i=0; i<ret; ++i) {
// 只处理读事件, 其他事件默认不处理
struct epoll_event *pev = &all_events[i];
// 不是读事件
if (!(pev->events & EPOLLIN)) {
continue;
}
if (pev->data.fd == lfd) { // 接受连接请求
do_accept(lfd, epfd);
} else { // 读数据
do_read(pev->data.fd, epfd);
}
}
}
}
int main(int argc, char *argv[])
{
// 命令行参数获取 端口 和 server提供的目录
if (argc < 3)
{
printf("./server port path\n");
}
// 获取用户输入的端口
int port = atoi(argv[1]);
// 改变进程工作目录
int ret = chdir(argv[2]);
if (ret != 0) {
perror("chdir error");
exit(1);
}
// 启动 epoll监听
epoll_run(port);
return 0;
}
我们通过telnet命令进行测试
可以成功访问path中的文本文件。
标签:lfd,基于,httpserver,epoll,int,cfd,send,buf 来源: https://www.cnblogs.com/fight-nxlwwy/p/15899955.html