该实验中需要实现一个web proxy。Web代理的主要功能是充当客户端和服务器之间的中间人,客户端的请求发送给proxy,proxy把请求转发给服务器,服务器的返回结果也通过proxy转发给客户端。proxy可以对于相同url的请求进行缓存。
原始实验包github下载地址
必要知识《深入理解计算机系统》第二版中的第10章、第11章、第12章内容。
因为本实验包是针对CMU当前用的《CS:APP》第三版的,可能有些函数有些更改。比如getaddrinfo
函数在第2版中并没有,可以结合原版CSAPP第三版的第11章来了解。
当然这些函数均通过封装,最后我们调用的函数形式与第2版中的相似。
getaddrinfo
能够将传入的用String变量代表的 hostnames、 host addresses、 service names和port number等转换成为socket address structure。它代替了原来的geihostbyname
和getservbyname
。该函数是线程安全的,并且是可重入的函数,而且该函数与协议无关,适用IPV4 IPV6 。
getnameinfo
函数是getaddrinfo
函数的逆函数,功能恰好相反
4.调试./proxy的方法
- telnet
- curl
- netcat
实验源码见链接:Part 1 proxy.c
要求:实现一个顺序执行的代理,它可以处理GET方法并转发,对于其他方法可以不实现。
命令行调用./proxy < port >来启动代理服务器,其中port可以通过实验包中的工具port-for-user来获取。
主要流程及函数实现如下:
main函数
中启动服务器端socket,监听接受Port端口客户度HTTP请求,如有连接调用doit函数
进行处理
int main(int argc,char **argv)
{
int listenfd,connfd;
socklen_t clientlen;
char hostname[MAXLINE],port[MAXLINE];
struct sockaddr_storage clientaddr;/*generic sockaddr struct which is 28 Bytes.The same use as sockaddr*/
if(argc != 2){
fprintf(stderr,"usage :%s \n" ,argv[0]);
exit(1);
}
listenfd = Open_listenfd(argv[1]);
while(1){
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd,(SA *)&clientaddr,&clientlen);
/*print accepted message*/
Getnameinfo((SA*)&clientaddr,clientlen,hostname,MAXLINE,port,MAXLINE,0);
printf("Accepted connection from (%s %s).\n",hostname,port);
/*sequential handle the client transaction*/
doit(connfd);
Close(connfd);
}
return 0;
}
doit函数
中对于客户端请求的HTTP Header进行处理,首先获request header(例:GET http://www.zhihu.com HTTP/1.1)部分,判断是否是请求类型(GET)。然后对于请求URL进行分析,获取需要连接的服务器的hostname,port。修改客户端的HTTP Header,让proxy充当客户端将信息转发给正确的服务器,接受服务器的返回并转发给正真的请求客户端。 parse_uri函数
,修改客户端的HTTP Header 调用build_http_header函数
/*handle the client HTTP transaction*/
void doit(int connfd)
{
int end_serverfd;/*the end server file descriptor*/
char buf[MAXLINE],method[MAXLINE],uri[MAXLINE],version[MAXLINE];
char endserver_http_header [MAXLINE];
/*store the request line arguments*/
char hostname[MAXLINE],path[MAXLINE];
int port;
rio_t rio,server_rio;/*rio is client's rio,server_rio is endserver's rio*/
Rio_readinitb(&rio,connfd);
Rio_readlineb(&rio,buf,MAXLINE);
sscanf(buf,"%s %s %s",method,uri,version); /*read the client request line*/
if(strcasecmp(method,"GET")){
printf("Proxy does not implement the method");
return;
}
/*parse the uri to get hostname,file path ,port*/
parse_uri(uri,hostname,path,&port);
/*build the http header which will send to the end server*/
build_http_header(endserver_http_header,hostname,path,port,&rio);
/*connect to the end server*/
end_serverfd = connect_endServer(hostname,port,endserver_http_header);
if(end_serverfd<0){
printf("connection failed\n");
return;
}
Rio_readinitb(&server_rio,end_serverfd);
/*write the http header to endserver*/
Rio_writen(end_serverfd,endserver_http_header,strlen(endserver_http_header));
/*receive message from end server and send to the client*/
size_t n;
while((n=Rio_readlineb(&server_rio,buf,MAXLINE))!=0)
{
printf("proxy received %d bytes,then send\n",n);
Rio_writen(connfd,buf,n);
}
Close(end_serverfd);
}
build_http_header函数
:构建新的请求头,根据实验指导书的要求,需要将请求头中Host、User-Agent、Connection、Proxy-Connection等key的value部分修改为指定的信息,并且其他请求头不变
void build_http_header(char *http_header,char *hostname,char *path,int port,rio_t *client_rio)
{
char buf[MAXLINE],request_hdr[MAXLINE],other_hdr[MAXLINE],host_hdr[MAXLINE];
/*request line*/
sprintf(request_hdr,requestlint_hdr_format,path);
/*get other request header for client rio and change it */
while(Rio_readlineb(client_rio,buf,MAXLINE)>0)
{
if(strcmp(buf,endof_hdr)==0) break;/*EOF*/
if(!strncasecmp(buf,host_key,strlen(host_key)))/*Host:*/
{
strcpy(host_hdr,buf);
continue;
}
if(!strncasecmp(buf,connection_key,strlen(connection_key))
&&!strncasecmp(buf,proxy_connection_key,strlen(proxy_connection_key))
&&!strncasecmp(buf,user_agent_key,strlen(user_agent_key)))
{
strcat(other_hdr,buf);
}
}
if(strlen(host_hdr)==0)
{
sprintf(host_hdr,host_hdr_format,hostname);
}
sprintf(http_header,"%s%s%s%s%s%s%s",
request_hdr,
host_hdr,
conn_hdr,
prox_hdr,
user_agent_hdr,
other_hdr,
endof_hdr);
return ;
}
-parse_uri函数
:分析uri
/*parse the uri to get hostname,file path ,port*/
void parse_uri(char *uri,char *hostname,char *path,int *port)
{
*port = 80;
char* pos = strstr(uri,"//");
pos = pos!=NULL? pos+2:uri;
char*pos2 = strstr(pos,":");
if(pos2!=NULL)
{
*pos2 = '\0';
sscanf(pos,"%s",hostname);
sscanf(pos2+1,"%d%s",port,path);
}
else
{
pos2 = strstr(pos,"/");
if(pos2!=NULL)
{
*pos2 = '\0';
sscanf(pos,"%s",hostname);
*pos2 = '/';
sscanf(pos2,"%s",path);
}
else
{
sscanf(pos,"%s",hostname);
}
}
return;
}
实验源码见链接:Part 2 proxy.c
主要要求是实现可以响应多客户端连接的Proxy
下面的代码主要运用了多线程的方法,通过main监听到客户端连接后,调用Pthread_create来创建线程,并且将connfd参数通过传值传递给线程thread函数。
注意:如果是connfd是传地址的,会出现竞争(race)问题。当然通过一定的方法可以避免,一种是我上面用到的传值,另外就是每个connfd在堆中申请一个区域存储,这样主线程和子线程就出现竞争。
---main START---
.....
connfd = Accept(listenfd,(SA *)&clientaddr,&clientlen);
/*print accepted message*/
Getnameinfo((SA*)&clientaddr,clientlen,hostname,MAXLINE,port,MAXLINE,0);
printf("Accepted connection from (%s %s).\n",hostname,port);
Pthread_create(&tid,NULL,thread,(void *)connfd);
....
---main END---
void *thread(void *vargp){
int connfd = (int)vargp;
Pthread_detach(pthread_self());
doit(connfd);
Close(connfd);
}
实验源码见链接:Part 3 proxy.c
要求实现缓存客户端的请求,其中最大的缓存块大小要小于MAX_OBJECT_SIZE(102400),总的缓存大小MAX_CACHE_SIZE(1049000)。cache需要牺牲缓存块时,运用LRU算法
在实现过程中需要解决不同线程同时访问cache的问题,我的解决方法是使用读者写者的中的写者优先方法。
相关cache的数据结构如下
typedef struct {
char cache_obj[MAX_OBJECT_SIZE];
char cache_url[MAXLINE];
int LRU;
int isEmpty;
int readCnt; /*count of readers*/
sem_t wmutex; /*protects accesses to cache*/
sem_t rdcntmutex; /*protects accesses to readcnt*/
int writeCnt; /*count of writer*/
sem_t wtcntMutex; /*protects accesses to writeCnt*/
sem_t queue; /*protects writer first*/
}cache_block;
typedef struct {
cache_block cacheobjs[CACHE_OBJS_COUNT]; /*ten cache blocks*/
int cache_num;
}Cache;
相关的PV操作
void readerPre(int i){
P(&cache.cacheobjs[i].queue);
P(&cache.cacheobjs[i].rdcntmutex);
cache.cacheobjs[i].readCnt++;
if(cache.cacheobjs[i].readCnt==1) P(&cache.cacheobjs[i].wmutex);
V(&cache.cacheobjs[i].rdcntmutex);
V(&cache.cacheobjs[i].queue);
}
void readerAfter(int i){
P(&cache.cacheobjs[i].rdcntmutex);
cache.cacheobjs[i].readCnt--;
if(cache.cacheobjs[i].readCnt==0) V(&cache.cacheobjs[i].wmutex);
V(&cache.cacheobjs[i].rdcntmutex);
}
void writePre(int i){
P(&cache.cacheobjs[i].wtcntMutex);
cache.cacheobjs[i].writeCnt++;
if(cache.cacheobjs[i].writeCnt==1) P(&cache.cacheobjs[i].queue);
V(&cache.cacheobjs[i].wtcntMutex);
P(&cache.cacheobjs[i].wmutex);
}
void writeAfter(int i){
V(&cache.cacheobjs[i].wmutex);
P(&cache.cacheobjs[i].wtcntMutex);
cache.cacheobjs[i].writeCnt--;
if(cache.cacheobjs[i].writeCnt==0) V(&cache.cacheobjs[i].queue);
V(&cache.cacheobjs[i].wtcntMutex);
}