Tiny_httpd源码分析

微型httpd项目学习笔记

项目我拷贝到了自己的仓库中:https://gitee.com/codesniperyang/Tinyhttpd

参考博客:https://www.cnblogs.com/nengm1988/p/7816618.html

main()

主要逻辑:主函数运行startup()后进入循环,接受请求accept_request()

在主函数运行startup收到参数后,进入while循环,接收HTTP请求时(其实就是 listen 的端口 accpet 的时候),派生一个线程运行 accept_request 函数。

client_sock = accept(server_sock,(struct sockaddr *)&client_name,
            &client_name_len);

/*  accept() 返回一个新的套接字来和客户端通信,
    addr 保存了客户端的IP地址和端口号,而 sockfd 是服务器端的套接字  */
int accept(int sockfd, void *addr, int *addrlen); 

startup()

startup主要逻辑(参数图一): 初始化 httpd 服务,包括建立套接字,绑定端口,进行监听等。

此函数用于启动侦听web连接的过程在指定的端口上。如果端口为0,则动态分配端口并修改原始端口变量以反映实际端口。

建立套接字

httpd = socket(PF_INET, SOCK_STREAM, 0);

int socket(int af, int type, int protocol);//用来创建套接字
1) af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。
    AF_INET 表示 IPv4 地址,例如 127.0.0.1;
    AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
2) type 为数据传输方式/套接字类型
3) protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
思考:返回值是一个整数,套接字是端口的抽象,感觉就是返回了一个合适的端口
事实:使用 socket() 函数创建套接字以后,返回值就是一个 int 类型的文件描述符。

绑定端口

if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
    error_die("bind");

//在建立套接字文件描述符成功后,需要对套接字进行地址和端口的绑定,才能进行数据的接收和发送操作。
bind(httpd, (struct sockaddr *)&name, sizeof(name)

进行监听

if (listen(httpd, 5) < 0) 
    error_die("listen");

//	listen在套接字函数中表示让一个套接字处于监听到来的连接请求的状态,
//	参数:sockfd 一个已绑定未被连接的套接字描述符,backlog 连接请求队列
int listen(int fd, int backlog);

accept_request()

主要逻辑:
1,读取第一行数据
2,判断是GET还是POST方法。
3,都不是,无法处理
4,是POST方法,做相应处理
5,是GET方法,定位url中的参数,也就是?
6,处理url路径
  文件没找到,调用not_found
  是没有执行权限的普通文件
  有执行权限的cgi脚本

**首先,使用getline()读取第一行的数据: **
numchars = get_line(client, buf, sizeof(buf)); //返回读到的字节个数

其次,读取到的第一行数据存放在buf中,接下需要将method读取到数组,method有两种(GET,POST)

 i = 0; j = 0;
 while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
 										//isspace () 函数用来检测一个字符是否是空白符
 {
  method[i] = buf[j];
  i++; j++;
 }
 method[i] = '\0';

如果请求的方法不是 GET 或 POST 任意一个的话就直接发送 response 告诉客户端没实现该方法

 if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
 {
  unimplemented(client); //调用此函数进行告知
  return;
 }

如果是 POST 方法,把 URL 读出来放到 url 数组中

 //如果是 POST 方法就将 cgi 标志变量置一(true)
 //然后把 URL 读出来放到 url 数组中
 while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))){
  url[i] = buf[j];
  i++; j++;
 }
 url[i] = '\0';

如果这个请求是一个 GET 方法的话

对于 GET 方法,如果有携带参数,则 query_string 指针指向 url 中?后面的 GET 参数。

//用一个指针指向 url
query_string = url;
//去遍历这个 url,跳过字符 ?前面的所有字符,如果遍历完毕也没找到字符 ?则退出循环
while ((*query_string != '?') && (*query_string != '\0'))
    query_string++;
//退出循环后检查当前的字符是 ?还是字符串(url)的结尾
if (*query_string == '?'){
    //如果是 ? 的话,证明这个请求需要调用 cgi,将 cgi 标志变量置一(true)
    cgi = 1;
    //从字符 ? 处把字符串 url 给分隔会两份
    *query_string = '\0';
    //使指针指向字符 ?后面的那个字符
    query_string++;
}

处理url路径

//将前面分隔两份的前面那份字符串,拼接在字符串htdocs的后面之后就输出存储到数组 path 中。相当于现在 path 中存储着一个字符串
sprintf(path, "htdocs%s", url);
 
//如果 path 数组中的这个字符串的最后一个字符是以字符 / 结尾的话,就拼接上一个"index.html"的字符串。首页的意思
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");

//如果不存在,那把这次 http 的请求后续的内容(head 和 body)全部读完并忽略,然后返回一个找不到文件的 response 给客户端
//如果存在,进行细节处理,然后:
if (!cgi)
    serve_file(client, path);//如果不需要 cgi 机制的话,
else
    execute_cgi(client, path, method, query_string);//如果需要则调用
getline()

此函数是读取第一行数据,例如上如中的:GET /index.html HTTP/1.1

//主要逻辑
while 保证在buf内范围 且 读到的字符不是换行
{
    读取一个字符c,没成功返回0
    if 读取到了数据
        添加到buf数组中
     else //没有数据了   
        将c设置为\n
}
在buf结尾添上结束符\0

在读取到的数据中,有一种让特殊情况。
如果是换行,那么在数据中体现为\r\n
if c是\r,那后面还有一个\n, 处理\n

在get_line()函数中有一个需要学习的函数

/*  第一个参数指定接收端套接字描述符;
    第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
    第三个参数指明buf的长度;
    第四个参数一般置0。 
    返回值:recv函数返回其实际copy的字节数。
    	如果recv在copy时出错,那么它返回SOCKET_ERROR;
		如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
    recv函数仅仅是copy数据,真正的接收数据是协议来完成的
    */
int recv( SOCKET s, char FAR *buf, int len, int flags);
n = recv(sock, &c, 1, 0);  //recv函数返回其实际copy的字节数

unimplemented()和not_found()

发现method不是GET也不是POST时,调用此函数。
此函数主要使用send()函数

/*
	不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。
	第一个参数:指定发送端套接字描述符;
    第二个参数:指明一个存放应用程序要发送数据的缓冲区;
    第三个参数:指明实际要发送的数据的字节数;
    第四个参数:一般置0。
    下面是函数原型:
*/
int send( SOCKET s, const char FAR *buf, int len, int flags );

//源码中摘抄
sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
send(client, buf, strlen(buf), 0);

client()给客户端发送一个404,内部也是send()

execute_cgi()

1.判断请求类型 
    (1).GET请求:读取并忽略请求剩下的内容
    (2).POST请求:读取Content-Length
2.创建两个pipe和一个子进程
	子进程:重定向,环境变量,执行cgi程序
	父进程:输入输出,收尾

关于重定向的理解:
对于一个平常程序来说,标准输入输出都是小黑框中。
而现在输入的数据是GET或POST的数据,输出的数据需要发给浏览器。
这时就需要重定向输入输出。
可能是无法直接将输入输出定向到GET的数据和发给浏览器,所以需要借助pipe吧。

其他

1,线程创建:这是一个未接触过的知识点

//源码中如下。第三个参数是线程运行的函数
pthread_create(&newthread , NULL, accept_request, client_sock) != 0

int pthread_create(pthread_t *thread,
                   const pthread_attr_t *attr,
                   void *(*start_routine) (void *),
                   void *arg);
	如果成功创建线程,pthread_create() 函数返回数字 0,反之返回非零值。
1) pthread_t *thread:传递一个 pthread_t 类型的指针变量,也可以直接传递某个 pthread_t 类型变量的地址。pthread_t 是一种用于表示线程的数据类型,每一个 pthread_t 类型的变量都可以表示一个线程。
2) const pthread_attr_t *attr:用于手动设置新建线程的属性,例如线程的调用策略、线程所能使用的栈内存的大小等。大部分场景中,我们都不需要手动修改线程的属性,将 attr 参数赋值为 NULL,pthread_create() 函数会采用系统默认的属性值创建线程。
3) void *(*start_routine) (void *):以函数指针的方式指明新建线程需要执行的函数,该函数的参数最多有 1 个(可以省略不写),形参和返回值的类型都必须为 void* 类型。void* 类型又称空指针类型,表明指针所指数据的类型是未知的。使用此类型指针时,我们通常需要先对其进行强制类型转换,然后才能正常访问指针指向的数据。
4) void *arg:指定传递给 start_routine 函数的实参,当不需要传递任何数据时,将 arg 赋值为 NULL 即可。

2,在startup()函数中,有这么一句struct sockaddr_in name;此结构体源码定义如下:

struct sockaddr_in {
    short            sin_family;      	//地址族
    unsigned short   sin_port;    		//端口号
    struct in_addr   sin_addr;    		//32位IP地址
    char             sin_zero[8];   	//不使用
};

3,htonl()等,端口号是一个无符号短整数类型

/*
    h---host
    to
    n---net
    l---unsigned long
    s---short
*/
#include <netinet/in.h>
uint32_t htonl(uint32_t hostlong);//本机字节顺序转化为网络字节顺序。
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

4,getsockname()

getsockname(httpd, (struct sockaddr *)&name, &namelen);

// s:标识一个已捆绑套接口的描述字。
// name:接收套接口的地址(名字)。
// namelen:名字缓冲区长度。
int PASCAL FAR getsockname( SOCKET s, struct sockaddr FAR* name,int FAR* namelen);

5, stat()和stat结构体

if (stat(path, &st) == -1){......}

//通过文件名filename获取文件信息,并保存在buf所指的结构体stat中
//执行成功则返回0,失败返回-1
int stat(const char *file_name, struct stat *buf);
struct stat {
  	......
    //文件的类型和存取的权限,源代码只用了这一个成员
    mode_t    st_mode;   
	......
};

本文仅供学习。

如有错误,请指出

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值