alin的学习之路(Linux网络编程:十)(http协议,BS模型)

alin的学习之路(Linux网络编程:十)(http协议,BS模型)

需求:使用B/S模型来访问主机中的文件(包括目录)

0. B/S 模型 注意事项

1. 浏览器请求ico

​ 准备一个favicon.ico 文件放置到 服务器提供访问的资源目录中。

​ 浏览器在请求图片的同时,会请求一个ico图标,用于浏览器标签文字部分前端的小图标显示。

​ 这个ico的文件名固定——favicon.ico。因此,自行准备一个ico文件,放置于服务器提供给浏览器访问的目标目录即可。

2. 容错处理

返回值 必须 要检查!!!

尤其是 read、write、recv、send、recvfrom、sendto 函数,返回的errno为 EINTREAGAIN时,不代表是一个错误,但会严重影响程序运行结果!通常使用continue来处理这种情况即可。

1. do_read() 读浏览器发送来的数据

  • 本案例主要用到的是读数据,所以只考虑 GET 方法,那么浏览器发送来的数据也只需要读请求行即可。

  • 读第一行后,其他数据清理掉。如果读到的是0,那么表示浏览器断开连接,使用 epoll_ctl() 函数将这个结点摘下。

  • 读到第一行后要对第一行进行解析:使用 sprintf() 函数搭配正则表达式%[^ ]使用,解析三块内容。

  • 注意解析到的文件路径需要解码

  • **如果路径是 / 代表网址中没有指定访问的文件,即访问 ./ ,要将解析到的文件路径改为 ./ **

  • 随后分析请求文件,封装了 http_request() 函数

2. http_request() 函数分析请求行中的文件

  • 使用 stat() 函数查看文件是否存在,如果不存在的话,封装一个 send_err() 函数用来显示一个404错误网页,这个网页需要以http协议应答的方式发送给浏览器
  • 如果文件存在且是普通文件
    • 封装 send_http_response() 函数来发送应答行,应答头以及空行,包括应答码,描述信息,文件类型,文件大小等
    • 封装 send_file() 函数来发送应答包包体,也就是打开文件的操作和读文件的操作
  • 如果文件存在且是目录:
    • 使用普通文件使用的 send_http_response() 函数发送应答行,应答头以及空行,注意文件类型是 html ,因为如果打开了一个路径是要展示这个目录的文件并做超链接
    • 封装 send_dir() 函数来发送应答包包体,也就是制作一个html网页

3. send_http_response() 函数发送应答行, 应答头以及空行

  • 使用 sprintf 拼接一个字符串,里面写好需要的数据
  • 拼接好后 send 给浏览器

4. send_file() 函数发送访问文件的应答包包体

  • open 打开文件, read 读文件 ,send 发给浏览器

5. send_dir() 函数

  • 制作网页,标题标签显示当前目录
  • 使用 scandir() 函数遍历目录,这个函数可以通过传出参数返回一个包含目录中所有文件目录项的数组。
  • 遍历数组,获取数组元素 struct dirent* 中的信息,通过文件名拼接上当前目录名,得到路径,使用 stat() 函数获得文件大小
  • 然后文件名需要加编码
  • 通过stat 判断,如果文件是普通文件,直接用 html 拼接。如果是目录文件,在文件名后面加上 / ,再用 html 拼接。
  • 拼接后 send 给浏览器

6. 简易版代码实现

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAXSIZE 2048

// 16进制数转化为10进制
int hexit(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;
}

/*
 *  这里的内容是处理%20之类的东西!是"解码"过程。
 *  %20 URL编码中的‘ ’(space)
 *  %21 '!' %22 '"' %23 '#' %24 '$'
 *  %25 '%' %26 '&' %27 ''' %28 '('......
 *  相关知识html中的‘ ’(space)是 
 */
void encode_str(char* to, int tosize, const char* from)
{
    int tolen;

    for (tolen = 0; *from != '\0' && tolen + 4 < tosize; ++from) {
        if (isalnum(*from) || strchr("/_.-~", *from) != (char*)0) {
            *to = *from;
            ++to;
            ++tolen;
        } else {
            sprintf(to, "%%%02x", (int) *from & 0xff);
            to += 3;
            tolen += 3;
        }
    }
    *to = '\0';
}

void decode_str(char *to, char *from)
{
    for ( ; *from != '\0'; ++to, ++from  ) {
        if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])) {
            *to = hexit(from[1])*16 + hexit(from[2]);
            from += 2;
        } else {
            *to = *from;
        }
    }
    *to = '\0';
}

// 通过文件名获取文件的类型
const char *get_file_type(const char *name)
{
    char* dot;

    // 自右向左查找‘.’字符, 如不存在返回NULL
    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";
}

//从数据中读取一行
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);	//#include  模拟读一次/拷贝读一次. 读完 socke 中还有.
				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(-1 == ret)
    {
        perror("epoll_ctl error");
        exit(0);
    }
    close(cfd);
}

void send_http_response(int cfd, int num, const char* desc, const char* type, int size)
{
    char buf[BUFSIZ] = {0};
    sprintf(buf, "%s %d %s\r\n", "HTTP/1.1", num, desc);
    sprintf(buf+strlen(buf), "%s:%s\r\n", "Content-Type", type);
    sprintf(buf+strlen(buf), "%s:%d\r\n", "Content-Length", size);
    sprintf(buf+strlen(buf), "\r\n");
    //printf("写回给client的http应答:|%s|\n", buf);
    //发送给浏览器
    send(cfd, buf, strlen(buf), 0);
    printf("%s",buf);
}

void send_file(int cfd, const char* file)
{
    int n;
    char buf[BUFSIZ] = {0};
    int fd = open(file, O_RDONLY);
    if(-1 == fd)
    {
        perror("open error");
        exit(1);
    }
    while((n = read(fd, buf, sizeof(buf))) > 0)
    {
        send(cfd, buf, n, 0);
        printf("%s", buf);
    }
    close(fd);
}

void send_dir(int cfd, const char* dirname)
{
    int ret;
    char path[256] = {0};
    char enstr[1024] = {0};
    char buf[BUFSIZ];
    sprintf(buf, "当前目录:%s", dirname);
    sprintf(buf+strlen(buf), "

%s

", dirname);struct dirent** namelist =NULL;int num =scandir(dirname,&namelist,NULL, alphasort);for(int i=0;i<num ;++i){char* name = namelist[i]->d_name;// 拼接文件的完整路径sprintf(path,"%s/%s", dirname, name);printf("path = %s ===================\n", path);struct stat st;stat(path,&st);encode_str(enstr,sizeof(enstr), name);// 如果是文件if(S_ISREG(st.st_mode)){sprintf(buf+strlen(buf),"", enstr, name,(long)st.st_size);}elseif(S_ISDIR(st.st_mode)){// 如果是目录 sprintf(buf+strlen(buf),"", enstr, name,(long)st.st_size);} ret =send(cfd, buf,strlen(buf),0);if(ret ==-1){if(errno == EAGAIN){perror("send error:");continue;}elseif(errno == EINTR){perror("send error:");continue;}else{perror("send error:");exit(1);}}memset(buf,0,sizeof(buf));// 字符串拼接}sprintf(buf+strlen(buf),"
%s%ld
%s/%ld
"
); send(cfd, buf, strlen(buf), 0); } void send_err(int cfd, int no, const char* desc, const char* sent) { char buf[BUFSIZ] = {0}; sprintf(buf, "%s %d %s\r\n", "HTTP/1.1", no, desc); sprintf(buf+strlen(buf), "%s:%s\r\n", "Content-Type", get_file_type(".html")); sprintf(buf+strlen(buf), "%s:%d\r\n", "Content-Length", -1); sprintf(buf+strlen(buf), "\r\n"); sprintf(buf+strlen(buf), "%s", desc); sprintf(buf+strlen(buf), "

%d %s


%s"
, no, desc, sent); send(cfd, buf, strlen(buf), 0); printf("%s", buf); } void http_request(int cfd, const char* file) { struct stat buf; printf("%s\n",file); int ret = stat(file, &buf); if(-1 == ret) { //回发错误页面 send_err(cfd, 404, "Not Found", "No such file or directory"); perror("stat error"); exit(1); } if(S_ISREG(buf.st_mode)) { //组织应答协议 应答行,应答头,空行并发送 send_http_response(cfd, 200, "ok", get_file_type(file), buf.st_size); //send_http_response(cfd, 200, "ok", "image/jpeg", buf.st_size); //send_http_response(cfd, 200, "ok", "text/plain; charset=utf-8", buf.st_size); //读取文件内容并发送给浏览器 send_file(cfd, file); } //如果是目录 if(S_ISDIR(buf.st_mode)) { send_http_response(cfd, 200, "ok", get_file_type(".html"), -1); send_dir(cfd, file); } } void do_read(int cfd, int epfd) { char line[BUFSIZ] = {0}; int n = get_line(cfd, line, sizeof(line)); if(0 == n) //读到文件末尾 { disconnect(cfd, epfd); } else //读到了数据 { printf("------%s\n", line); while(1) //将后面的所有数据读出来但不要 { char buf[BUFSIZ]; get_line(cfd, buf, sizeof(buf)); if(buf[0] == '\n') break; //printf("%s\n",buf); } } if(strncasecmp("Get",line,3) == 0) { //解析读到的一行 char opt[16]={0}, path[256]={0}, protocol[16]={0}; sscanf(line, "%[^ ] %[^ ] %[^ ]", opt, path, protocol); printf("%s\n%s\n%s\n", opt, path, protocol); decode_str(path, path); char file[256] = {0}; if(strcmp(path, "/") == 0) strcpy(file, "./"); else strcpy(file, path+1); http_request(cfd, file); printf("%s\n", file); } } void *epoll_run(int port) { int i = 0; struct epoll_event all_events[MAXSIZE]; // 创建一个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, -1); 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("./a.out port path\n"); return 1; } // 获取用户输入的端口 int port = atoi(argv[1]); // 改变进程工作目录 int ret = chdir(argv[2]); if (ret != 0) { perror("chdir error"); exit(1); } // 启动 epoll 监听 epoll_run(port); return 0; }

7. telnet调试

​ 可使用 telnet 命令,借助IP和port,模拟浏览器行为,在终端中对访问的服务器进行调试,方便查看服务器回发给浏览器的http协议数据。使用方式如:

命令行键入:telnet 127.0.0.1 9999 回车,手动写入http请求协议头,如:

GET /hello.c http/1.1 回车

此时,终端中可查看到服务器回发给浏览器的http应答协议及数据内容。可根据该信息进行调试。

nc 127.0.0.1 9999 也可以!!! 并且 nc 不会发送数据后就断开连接。

你可能感兴趣的:(B/S模型,epoll,linux,http)