Proxylab
1. 实验概述
WedProxy是介于浏览器和远端服务器的桥梁,接收浏览器的请求并发送给服务器,接收服务器响应并返回给浏览器。代理具有以下三个功能:
- 建立和某个服务器、客户端的连接,接收客户端请求并发送给服务器,接收服务器响应并返回给浏览器。
- 允许多个连接并发执行。
- 缓存最近访问的内容。
和前一个实验malloclab
相比这个实验不算难,不仅评分宽松、没有考察错误处理外,结构体的使用还可以避免像malloclab
那样因为指针误用导致的莫名其妙的问题。虽然这个实验涉及第十章、十一章、十二章,但是可以参照Tiny
修改,难度也相应减小。
2. 总体思路
主线程先做些初始化工作,主要是初始化描述符池、各个线程、cache、建立监听端口。然后进入一个循环,监听端口,得到连接请求,并把连接描述符放入描述符池。每个线程尝试从描述符池拿描述符,拿到后与客户端建立连接,接受一条请求,先检查cache
中有无缓存对象,有的话直接返回,否则与服务端建立连接,接受服务端响应,把响应内容放入cache
。
3. 解析request
3.1 思路
从与一个客户端关联的连接端口逐行读取request,需要解析request line
得到发给服务器的reques line
并确定主机和端口,与某个服务器建立连接后修改请求行,并将要求的头部替换为自定义头部,保留其它头部,把新产生的请求逐行发给服务器。
3.2 问题
存储每个域(如method
,uri
)的缓冲区多大合适?防止客户端返回的内容过长导致缓存区溢出。
4. 传递文件内容
这一部分需要对课本第十章很熟悉,当连接建立后proxy
会得到与客户端相连的描述符和与服务端相连的描述符,和两者交互直接抽象为对文件的读写,书中提出的RIO
包可以健壮地用于套接字符的读写.
下面简单介绍这个包:
rio_readn
:Unix系统调用read
的限定大小版,一定会从指定描述符读取n
个字节,阻塞直到读完。
rio_writen
:Unix系统调用write
的限定大小版,一定会向指定描述符写n
个字节,阻塞直到写完。
rio_t
:一个用于建立某个描述符与缓冲区联系的结构体,缓冲区的引入使得该结构体除了具有与一般描述符相同的行为(都可以读写,只是要调用定制的函数)外还可以借助缓冲区减少系统调用的次数,提高性能。
下面是与rio_t
配套的函数:
rio_readinitb
:建立缓冲区与指定描述符间的联系。
rio_read
:这是后面几个读有关函数使用的基础函数,调用时若缓冲区为空就会调用read
填满缓冲区,否则返回缓冲区中的内容。与read
有相同的语义,出错时返回-1,遇到EOF返回0,字节数超过缓冲区内未读字节数量会返回不足值。
rio_readnb
:rio_read
的限定大小版,一定会从指定rio_t
读取n
个字节,阻塞直到读完。
rio_readlineb
:从指定rio_t
读取一个文本行,或者读到EOF,或者读完maxlen字节。
不用担心代理使用上面的函数时会发生读取响应不完整的情况,因为
rio_readlineb
会反复调用rio_read
,直到rio_read
返回0,也就是遇到EOF
,EOF
就表明响应已经完整发送了。
如何使用RIO
包传递文件内容
这个很简单,代理从客户端读,再往服务端写,建立一个rio_t
对象与读端关联,反复调用Rio_readlineb
读取客户端发来的内容然后用Rio_writen
写回内容就行。即使二进制文件没有文本行,但是一定有EOF,起始位置到EOF间的数据都会被正确送回。
5. 实现并发
基于预线程化实现并发。
5.1 线程逻辑
每个线程的逻辑流为:从描述符池中取出一个描述符,接受该描述符对应客户端发来的请求并解析出服务端后与服务端建立连接,再发送连接头部,之后把服务端响应返回给客户端,这一切可以通过一个函数doit
完成。
5.2 描述符池
使用书中提供的SBUF
包,可以完成对描述符池的抽象(一个有多个消费者一个生产者的缓冲区),但是subf_insert
存在问题,如果服务的客户端数量过多缓冲区慢,主线程将一直阻塞,改进建议是先检查slots
,如果满了就给客户端返回拒绝请求的消息。
6. cache
6.1 基本结构
每当代理收到服务端发来的数据需要在本地存一个副本,这样当客户端再次要求同样数据时能够直接返回。用一个链表实现cache
。按照writeup
,应当将对cache的访问视为读写者模型,取操作视为读,插入一个新数据对象视为写。
支持下列基本操作:
-
cache_insert(cache_t *cache, block_t *block)
往
cache
中插入一个块,需要给cache
上写锁,块加到链表末尾。 -
cache_get(cache_t *cache, char *url)
遍历链表找出对应
url
的块,并返回内容指针。为了保证该函数是线程安全的,会另外用一段内存区域存块内容并返回内容指针(注意,调用者需要释放内容)。 -
cache_init(cache_t *cache)
初始化cache。
-
cache_evic(cache_t *cache, int size)
驱逐块。
6.2 驱逐策略
如果严格使用LRU
算法,将不存在读者,因为读数据块同时要把数据块放到链表头部,实际上也要写cache
,类似于时钟算法,给每个block
一个标识位表示最近是否被访问,如果被访问了或者被加入cache
就置为1.
需要驱逐一个块时,从头遍历一遍链表,驱逐第一个标识为0的块,并将标识为1的块的表示置为0.如果遍历一遍没有驱逐一个块就从头开始再遍历一遍,这次一定能找到合适的驱逐块。
7. 问题
除了上面列出的问题外,正确编写错误处理函数也是本次实验的一个重要部分,需要改进的地方包括但不限于:
cache.h
中函数没有按预期工作时如何提示错误信息且不影响继续运行- 客户端数量超过线程数时如何处理
8. 调试
- 使用函数
cache_print
和block_print
打印不变量。 - 使用
curl
,通过proxy
连接tiny
。具体使用时只用先在合适端口启动proxy
和tiny
,然后按照writeup
上使用命令curl -v --proxy http://localhost:15214 http://localhost:15213/home.html
来传送tiny
目录下的home.html
。
9. 代码
为了不修改Makefile
(本人太菜,不会改),直接把函数声明和定义写到.h
文件中。
proxy.c
#include <stdio.h>
#include "csapp.h"
#include "sbuf.h"
#include "cache.h"
/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
#define SBUFSIZE 16
#define NTHREADS 4
#define FOR(a, b) for (int i=a; i<b; i++)
// int main()
// {
// printf("%s", user_agent_hdr);
// return 0;
// }
void doit(int fd);
void read_requesthdrs(rio_t *rp, int fd);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_static(int fd, char *filename, int filesize);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum,
char *shortmsg, char *longmsg);
int gen_request();
int gen_host(char *uri, char *host, char *port, char *request);
void return_content(rio_t *rp, int clientfd, char *url);
void *thread(void *vargp);
/* You won't lose style points for including this long line in your code */
static const char *user_agent_hdr = "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3\r\n";
sbuf_t sbuf; /* 共享描述符池 */
cache_t cache;
int main(int argc, char **argv)
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;
pthread_t tid;
/* Check command line args */
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
listenfd = Open_listenfd(argv[1]);
sbuf_init(&sbuf, SBUFSIZE);
cache_init(&cache);
FOR(0, NTHREADS) {
Pthread_create(&tid, NULL, thread, NULL);
}
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); //line:netp:tiny:accept
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
sbuf_insert(&sbuf, connfd);
}
}
/* $end tinymain */
/*
* doit - handle one HTTP request/response transaction
* 解析请求行得到服务端主机和端口
* 解析请求header,改成自己的header
*/
/* $begin doit */
void doit