CSAPP: Proxy lab

介绍

该实验中需要实现一个web proxy。Web代理的主要功能是充当客户端和服务器之间的中间人,客户端的请求发送给proxy,proxy把请求转发给服务器,服务器的返回结果也通过proxy转发给客户端。proxy可以对于相同url的请求进行缓存。

准备

  1. 原始实验包github下载地址

  2. 必要知识《深入理解计算机系统》第二版中的第10章、第11章、第12章内容。

  3. 因为本实验包是针对CMU当前用的《CS:APP》第三版的,可能有些函数有些更改。比如getaddrinfo函数在第2版中并没有,可以结合原版CSAPP第三版的第11章来了解。
    当然这些函数均通过封装,最后我们调用的函数形式与第2版中的相似。

getaddrinfo能够将传入的用String变量代表的 hostnames、 host addresses、 service names和port number等转换成为socket address structure。它代替了原来的geihostbynamegetservbyname。该函数是线程安全的,并且是可重入的函数,而且该函数与协议无关,适用IPV4 IPV6 。

getnameinfo函数是getaddrinfo函数的逆函数,功能恰好相反

4.调试./proxy的方法
- telnet
- curl
- netcat


Part 1

实验源码见链接: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充当客户端将信息转发给正确的服务器,接受服务器的返回并转发给正真的请求客户端。
    其中解析request line需要调用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

实验源码见链接: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

实验源码见链接: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);
}

你可能感兴趣的:(计算机体系结构)