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(int clientfd)
{
int serverfd;
// struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE], host[MAXLINE], port[MAXLINE], request[MAXLINE];
// char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio, srio;
/* Read request line and headers */
Rio_readinitb(&rio, clientfd);
if (!Rio_readlineb(&rio, buf, MAXLINE)) //假设一次读把所有客户端发来的消息放到了缓冲区中
return;
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //line:netp:doit:parserequest
if (strcasecmp(method, "GET")) { //line:netp:doit:beginrequesterr
clienterror(clientfd, method, "501", "Not Implemented",
"Proxy does not implement this method");
return;
} //line:netp:doit:endrequesterr
if (gen_host(uri, host, port, request) < 0) {
clienterror(clientfd, uri, "501", "Error with request",
"Proxy find error with uri");
return;
}
// printf("test\n");
/* 先查找本地cache */
char *content = cache_get(&cache, uri);
if (content != NULL) {
Rio_writen(clientfd, content, strlen(content));
free(content);
}
else {
/* 向服务端发送请求 */
serverfd = Open_clientfd(host, port);
Rio_writen(serverfd, request, strlen(request));
read_requesthdrs(&rio, serverfd); //line:netp:doit:readrequesthdrs
/* 等待服务器返回内容,将读取内容返回给客户端 */
Rio_readinitb(&srio, serverfd);
return_content(&srio, clientfd, uri);
}
cache_print(&cache);
}
/* $end doit */
/*
* read_requesthdrs - read HTTP request headers
* 抛弃原有host,User-Agent,Connection,Proxy-Connection,用自己的替代
* 保留其余header
*/
/* $begin read_requesthdrs */
void read_requesthdrs(rio_t *rp, int fd)
{
char buf[MAXLINE];
// 发送自定义header
sprintf(buf, "%s", user_agent_hdr);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Connection: close\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Proxy-Connection: close\r\n");
Rio_writen(fd, buf, strlen(buf));
// printf("fffff\n");
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
// 保留其它header
for (;strcmp(buf, "\r\n"); Rio_readlineb(rp, buf, MAXLINE)) {
if (strncmp("Host", buf, 4) == 0 || strncmp("User-Agent", buf, 10) == 0
|| strncmp("Connection", buf, 10) == 0 || strncmp("Proxy-Connection", buf, 16) == 0)
continue;
printf("%s", buf);
Rio_writen(fd, buf, strlen(buf));
}
Rio_writen(fd, buf, strlen(buf));
printf("test\n");
return;
}
/* $end read_requesthdrs */
/*
* clienterror - returns an error message to the client
*/
/* $begin clienterror */
void clienterror(int fd, char *cause, char *errnum,
char *shortmsg, char *longmsg)
{
char buf[MAXLINE];
/* Print the HTTP response headers */
sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: text/html\r\n\r\n");
Rio_writen(fd, buf, strlen(buf));
/* Print the HTTP response body */
sprintf(buf, "<html><title>Tiny Error</title>");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "<body bgcolor=""ffffff"">\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "%s: %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "<p>%s: %s\r\n", longmsg, cause);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "<hr><em>The Tiny Web server</em>\r\n");
Rio_writen(fd, buf, strlen(buf));
}
/* $end clienterror */
/*
* gen_host - 传入absoluteURI,生成主机和端口,并把新的request line,host header写入最终请求
*/
int gen_host(char *uri, char *host, char *port, char *request)
{
// printf("into\n");
sprintf(port, "80");
// printf("uri: %s\n", uri);
// [bg, end-1]为host,[end, bp-1]为uri,[end]
char *bg = uri, *end, *bp;
while (*bg != '/')
bg++;
while (*bg == '/')
bg++;
// printf("%s\n", bg);
end = bg;
while (*end != '/' && *end != ':')
end++;
// printf("%c\n", *end);
strncpy(host, bg, end-bg); // 得到host
// printf("host: %s\n", host);
bp = end + 1;
if (*end == ':') { // [end, bp-1]为端口号
end++;
while (*bp != '/')
bp++;
strncpy(port, end, bp-end);
end = bp;
}
// end到uri字符串末尾的部分都是uri
uri += strlen(uri);
sprintf(request, "GET %.*s HTTP/1.0\r\nHost: %s\r\n", (int)(uri-end), end, host);
// sprintf(request, "GET %.*s HTTP/1.0\r\n", (int)(uri-end), end);
return 1;
}
/*
* return_content - 从rp读取内容并逐行返回给客户端clientfd
*/
void return_content(rio_t *rp, int clientfd, char *url)
{
char buf[MAXLINE], block[MAXBLOCKSIZE];
size_t n, size = 0;
while ((n = Rio_readlineb(rp, buf, MAXLINE)) != 0) {
Rio_writen(clientfd, buf, n);
/* 添加到数据对象缓冲区中,抛弃那些过大的 */
if (n + size <= MAXCACHESIZE) {
sprintf(block + size, "%s", buf);
size += n;
}
else
size = MAXCACHESIZE + 1;
}
/* 生成新块并添加到cache */
block_t *bp = (block_t *)Malloc(sizeof(block_t));
block_init(bp, url, block, NULL, size);
cache_insert(&cache, bp);
}
/*
* thread - 线程例程
*/
void *thread(void *vargp)
{
Pthread_detach(pthread_self());
while (1) {
int connfd = sbuf_remove(&sbuf);
doit(connfd);
Close(connfd);
}
}
cache.h
#ifndef __CACHE_H__
#define __CACHE_H__
#include "csapp.h"
#define MAXBLOCKSIZE 102400
#define MAXCACHESIZE 1048576
typedef struct block_t{
char *url;
char *content;
struct block_t *next;
// struct block_t *prev;
int size;
int visited; // 表明这个块是否最近被访问
pthread_rwlock_t rwlock;
} block_t;
typedef struct cache_t {
block_t *head;
block_t *tail;
pthread_rwlock_t rwlock;
int size;
} cache_t;
int block_init(block_t *block, char *_url, char *_content, block_t *_next, int size);
void cache_insert(cache_t *cache, block_t *block);
char *cache_get(cache_t *cache, char *url);
void block_print(block_t *block);
void cache_print(cache_t *cache);
static void cache_evic(cache_t *cache, int size);
/*
* block_init - 初始化一个cache块
* 深拷贝传入的每个参数
* 如果size大于限定大小则返回1,否则返回0
*/
int block_init(block_t *block, char *_url, char *_content, block_t *_next, int size)
{
if (size > MAXBLOCKSIZE)
return 1;
if (block->url != NULL)
free(block->url);
block->url = NULL;
if (_url != NULL) {
block->url = (char *)Malloc(strlen(_url));
strcpy(block->url, _url);
}
if (block->content != NULL)
free(block->content);
if (_content != NULL) {
block->content = (char *)Malloc(size);
strcpy(block->content, _content);
}
block->next = _next;
block->size = size;
block->visited = 1;
return 0;
}
/*
* cache_init - 初始化cache
*/
void cache_init(cache_t *cache)
{
struct block_t *block = (block_t *)Malloc(sizeof(block_t));
block_init(block, NULL, NULL, NULL, 0);
cache->head = block;
cache->tail = block;
cache->size = 0;
pthread_rwlock_init(&block->rwlock, NULL);
}
/*
* cache_insert - 往cache中插入内容,插到链表末尾
* 这是一个线程安全函数
*/
void cache_insert(cache_t *cache, block_t *block) //char *url, char *content)
{
pthread_rwlock_wrlock(&cache->rwlock);
if (cache->size + block->size > MAXCACHESIZE)
cache_evic(cache, block->size);
cache->size += block->size;
cache->tail->next = block;
cache->tail = block;
pthread_rwlock_unlock(&cache->rwlock);
}
/*
* cache_evic - 驱逐足够数量的块使得cache中可以装下大小为size的新块
* precondition:链表不为空即可满足一个块的插入请求,这样就不需要修改tail和检查temp是否为空
* postcondition:cache能够装下大小为size的块
* 这不是一个线程安全函数
*/
static void cache_evic(cache_t *cache, int size)
{
block_t *p = cache->head, *temp;
int sum = cache->size;
while (sum + size > MAXCACHESIZE && p != NULL) {
temp = p->next;
if (temp->visited) { // 最近被访问了
p = temp;
temp->visited = 0;
}
else {
p->next = temp->next;
sum -= temp->size;
free(temp);
}
}
/* 前一次遍历没有找到最近未被访问块,重新遍历一遍,就不需要考虑块是否最近被访问了 */
p = cache->head;
while (sum + size > MAXCACHESIZE) {
temp = p->next;
p->next = temp->next;
sum -= temp->size;
free(temp);
}
cache->size = sum;
}
/*
* cache_get - 顺序遍历链表直到找到匹配url的块
* 这是一个线程安全函数
*/
char *cache_get(cache_t *cache, char *url)
{
pthread_rwlock_rdlock(&cache->rwlock);
block_t *p = cache->head->next;
for (; p != NULL; p = p->next) {
if (!strcmp(p->url, url)) {
char *buf = (char *)Malloc(strlen(p->content));
memcpy(buf, p->content, p->size + 1);
p->visited = 1;
return buf;
}
}
pthread_rwlock_unlock(&cache->rwlock);
printf("test\n");
return NULL;
}
/*
* block_print - 打印一个块
*/
void block_print(block_t *block)
{
printf("the url is:\n");
if (block->url != NULL)
printf("%s\n", block->url);
else
printf("\n");
printf("the content is:\n");
if (block->content != NULL)
printf("%s\n", block->content);
else
printf("\n");
printf("the size is:\n");
printf("%d\n", block->size);
printf("the visited sign:\n");
printf("%d\n", block->visited);
}
/*
* cache_print - 检查cache不变量:打印每个块,检查size是否匹配
*/
void cache_print(cache_t *cache)
{
int size = 0;
block_t *p = cache->head;
printf("the head..........\n");
for (; p != NULL; p = p->next) {
size += p->size;
block_print(p);
}
printf("print end\n");
}
#endif /* __CACHE_H__ */
sbuf.h
书上提供的解决生产者消费者问题的缓存结构。
#ifndef __SBUF_H__
#define __SBUF_H__
#include "csapp.h"
/* $begin sbuft */
typedef struct {
int *buf; /* Buffer array */
int n; /* Maximum number of slots */
int front; /* buf[(front+1)%n] is first item */
int rear; /* buf[rear%n] is last item */
sem_t mutex; /* Protects accesses to buf */
sem_t slots; /* Counts available slots */
sem_t items; /* Counts available items */
} sbuf_t;
/* $end sbuft */
void sbuf_init(sbuf_t *sp, int n);
void sbuf_deinit(sbuf_t *sp);
void sbuf_insert(sbuf_t *sp, int item);
int sbuf_remove(sbuf_t *sp);
/* Create an empty, bounded, shared FIFO buffer with n slots */
/* $begin sbuf_init */
void sbuf_init(sbuf_t *sp, int n)
{
sp->buf = (int *)Calloc(n, sizeof(int));
sp->n = n; /* Buffer holds max of n items */
sp->front = sp->rear = 0; /* Empty buffer iff front == rear */
Sem_init(&sp->mutex, 0, 1); /* Binary semaphore for locking */
Sem_init(&sp->slots, 0, n); /* Initially, buf has n empty slots */
Sem_init(&sp->items, 0, 0); /* Initially, buf has zero data items */
}
/* $end sbuf_init */
/* Clean up buffer sp */
/* $begin sbuf_deinit */
void sbuf_deinit(sbuf_t *sp)
{
Free(sp->buf);
}
/* $end sbuf_deinit */
/* Insert item onto the rear of shared buffer sp */
/* $begin sbuf_insert */
void sbuf_insert(sbuf_t *sp, int item)
{
P(&sp->slots); /* Wait for available slot */
P(&sp->mutex); /* Lock the buffer */
sp->buf[(++sp->rear)%(sp->n)] = item; /* Insert the item */
V(&sp->mutex); /* Unlock the buffer */
V(&sp->items); /* Announce available item */
}
/* $end sbuf_insert */
/* Remove and return the first item from buffer sp */
/* $begin sbuf_remove */
int sbuf_remove(sbuf_t *sp)
{
int item;
P(&sp->items); /* Wait for available item */
P(&sp->mutex); /* Lock the buffer */
item = sp->buf[(++sp->front)%(sp->n)]; /* Remove the item */
V(&sp->mutex); /* Unlock the buffer */
V(&sp->slots); /* Announce available slot */
return item;
}
/* $end sbuf_remove */
/* $end sbufc */
#endif /* __SBUF_H__ */
本文详细介绍了CSAPP Proxylab实验,包括代理服务器的功能、并发执行、缓存策略及其实现细节,强调了解析request、传递文件内容和并发处理等方面的关键技术。
2610

被折叠的 条评论
为什么被折叠?



