admin 管理员组文章数量: 887021
Tiny
微型httpd项目学习笔记
项目我拷贝到了自己的仓库中:
参考博客:.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 机制的话,
elseexecute_cgi(client, path, method, query_string);//如果需要则调用
getline()
此函数是读取第一行数据,例如上如中的:GET /index.html HTTP/1.1
//主要逻辑
while 保证在buf内范围 且 读到的字符不是换行
{读取一个字符c,没成功返回0if 读取到了数据添加到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) != 0int 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---hostton---netl---unsigned longs---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; ......
};
本文仅供学习。
如有错误,请指出
本文标签: Tiny
版权声明:本文标题:Tiny 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/free/1699123105h330782.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论