首页 > 其他分享 >CSAPP Lab 8 Proxy lab

CSAPP Lab 8 Proxy lab

时间:2024-04-24 13:45:19浏览次数:15  
标签:CSAPP url ca lab URL int Proxy line MAXLINE

终于到最后一个 Lab 啦!

这个 Lab 的任务是实现一个代理服务器,将客户端发送的请求转发到服务端。

这个 Lab 分为三个任务,第一个任务需要实现这个代理服务,第二个任务支持处理并发请求,第三个任务需要实现缓存。

Part I: Implementing a sequential web proxy

这个部分其实很好写,很多部分都可以照抄下发文件中的 tiny 服务器的内容。

main 函数

main 函数完全复制了 tiny 程序的 main

大概过程就是使用一个 while 循环,不停地等待客户端连接,一旦连接成功,就调用 doit 函数进行处理。

int main(int argc, char **argv) 
{
    int listenfd, connfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;

    /* Check command line args */
    if (argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(1);
    }

    listenfd = Open_listenfd(argv[1]);
    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);
        doit(connfd);                                             //line:netp:tiny:doit
        Close(connfd);                                            //line:netp:tiny:close
    }
}

parseUrl 函数:URL 切分

首先我们在程序中定义了一个 URL 结构体,存放一个 URLhost、端口和路径。

typedef struct URL {
    char host[MAXLINE];
    char port[MAXLINE];
    char path[MAXLINE];
} URL;

然后,对于一个传入的 URL 字符串,我们首先忽略掉 http:// 这种协议标识,可以通过查找 // 字符串来实现。

然后,我们找到第一个 / 字符,在这个字符后面的就是路径了。如果找不到这个字符,我们就认为访问的是根路径 /。随后我们将这个字符设置为 '\0',方便之后端口的读取。

然后,我们通过 : 字符来定位端口,: 后面的就是端口,如果找不到 : 那么就认为端口是 80,然后我们将这个字符设置为 '\0'

最后,剩下的字符串就是 host 了。

void parseUrl(char *s, URL *url) {
    char *ptr = strstr(s, "//");
    if (ptr != NULL) s = ptr + 2;

    ptr = strchr(s, '/');
    if (ptr != NULL) {
        strcpy(url->path, ptr);
        *ptr = '\0';
    }
    
    ptr = strchr(s, ':');
    if (ptr != NULL) {
        strcpy(url->port, ptr + 1);
        *ptr = '\0';
    } else strcpy(url->port, "80");

    strcpy(url->host, s);
}

readClient 函数:从客户端读取请求并生成新请求

readClient 函数会从客户端读取请求的全部内容,并生成即将发送到服务端的新请求。

首先,从第一行读取 URL 扔给 parseUrl 函数提取。

然后,设置默认的 Hosts header 为 URL 中的 host。接着从接下来读取到的 headers 中,如果是 Hosts 那么就更新 Hosts,如果是 User-AgentConnectionProxy-Connection,那么就忽略(因为我们有我们要设定的专属 header)。其余的 header 收集起来,稍后一并作为新请求发过去。

最后,向新请求字符串中写入我们生成的 HTTP 头和 headers。

void readClient(rio_t *rio, URL *url, char *data) {
    char host[MAXLINE];
    char line[MAXLINE];
    char other[MAXLINE];
    char method[MAXLINE], urlstr[MAXLINE], version[MAXLINE];

    Rio_readlineb(rio, line, MAXLINE);
    sscanf(line, "%s %s %s\n", method, urlstr, version);
    parseUrl(urlstr, url);

    sprintf(host, "Host: %s\r\n", url->host);
    while (Rio_readlineb(rio, line, MAXLINE) > 0) {
        if (strcmp(line, "\r\n") == 0) break;
        if (strncmp(line, "Host", 4) == 0) strcpy(host, line);
        if (strncmp(line, "User-Agent", 10) &&
            strncmp(line, "Connection", 10) &&
            strncmp(line, "Proxy-Connection", 16)) strcat(other, line);
    }
    
    sprintf(data, "%s %s HTTP/1.0\r\n"
                     "%s%s"
                     "Connection: close\r\n"
                     "Proxy-Connection: close\r\n"
                     "%s\r\n", method, url->path, host, user_agent_hdr, other);
}

doit 函数

doit 函数的功能是处理成功连接的套接字接口。

首先就是调用 readClient 函数处理读入和生成新的请求。

然后调用 open_clientfd 函数连接服务器。

接着,向服务器发送新的请求。

最后,将服务器发送回来的结果读入,发送到客户端套接字。

void doit(int connfd) {
    rio_t rio;
    char line[MAXLINE];
    Rio_readinitb(&rio, connfd);
    
    URL url;
    char data[MAXLINE];
    readClient(&rio, &url, data);

    int serverfd = open_clientfd(url.host, url.port);
    if (serverfd < 0) printf("Connection failed!\n");
    
    rio_readinitb(&rio, serverfd);
    Rio_writen(serverfd, data, strlen(data));
    
    int len;
    while ((len = Rio_readlineb(&rio, line, MAXLINE)) > 0)
        Rio_writen(connfd, line, len);
    
    Close(serverfd);
}

测试结果

./20230908-csapp-proxylab/image-20230908233430125

完美通关!

完整代码:链接

Part II: Dealing with multiple concurrent requests

这一部分的任务是实现处理并发请求。

这一部分很简单,课本上讲过怎么处理,只需要将课本上的处理方法搬过来就行了。

课本上使用的是一种预线程化的方法实现的,这里不做讲解了,大家可以看看书上 12.5.4 和 12.5.5 节,分别在 P704 和 P708。

main 函数

没什么区别,就是加入了一个初始化生产者-消费者模型,以及创建 NTHREADS 个线程。还有在循环内部不再是直接执行,而是加入缓冲区。

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);
    }

    sbuf_init(&sbuf, SBUFSIZE);
    for (int i = 0; i < NTHREADS; ++i)
        Pthread_create(&tid, NULL, thread, NULL);

    listenfd = Open_listenfd(argv[1]);
    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);
    }
}

线程执行函数

和书上的例子没有区别,每次从缓冲区取出一个套接字以后就执行 doit 函数。

void thread(void *vargp) {
    Pthread_detach(pthread_self());
    while (1) {
        int connfd = sbuf_remove(&sbuf);
        doit(connfd);
        Close(connfd);
    }
}

测试结果

./20230908-csapp-proxylab/image-20230908234238430

下载完整程序:链接

Part III: Caching web objects

第三个任务就是实现缓存一些网页对象。

具体地,当我们的代理访问了一个服务器网页的时候,我们需要将这个网页缓存下来,在之后的请求中就不需要再次从服务器那里请求这个网页了。

但是,缓存的大小不是无限的,这就需要我们在缓存使用满的时候驱逐一部分已经缓存的网页出去。本实验要求我们使用 LRU (最近最少使用)的方法,也就是找到上一次访问时间最远的对象替换掉。

读者-写者模型

因为涉及到多个线程对于「缓存」这个公共资源的读写,因此我们往往需要使用互斥的方法来避免出错。

但是,在很多情况中,只是读与写或者写与写之间会发生冲突,读和读之间不会发生冲突,因此完全使用互斥是很浪费时间的。这就需要读者-写者模型了。

读者-写者模型实现了多个读者可以同时读取,但是读写、写写会互斥。在本实验中,我们采用的是读者优先的策略。

int read_cnt;
sem_t mutex, w;  // Both initially = 1

void reader(void) 
{
    while(1){
        P(&mutex);
        readcnt++;
        if(readcnt==1)
            P(&w);
        V(&mutex);
        
        // reading
        
        P(&mutex);
        readcnt--;
        if(readcnt==0)
            V(&w);
        V(&mutex);
    }
}

void writer(void)
{
    while(1){
        P(&w);
        
        // writing
        
        V(&w)
    }
}

几个模型相关的函数

为了方便,在本实验中,我们定义了四个函数,处理读者-写者模型的限制。

void readBegin(Cache *c) {
    P(&c->mutex);
    if (++c->read_cnt == 1) P(&c->w);
    V(&c->mutex);
}
void readEnd(Cache *c) {
    P(&c->mutex);
    if (--c->read_cnt == 0) V(&c->w);
    V(&c->mutex);
}
void writeBegin(Cache *c) {
    P(&c->w);
}
void writeEnd(Cache *c) {
    V(&c->w);
}

缓存结构

typedef struct Cache {
    bool empty;					// 是否为空
    URL url;					// 缓存 URL
    char data[MAX_OBJECT_SIZE]; // 缓存内容
    int lru;					// 上次访问时间
    int read_cnt;
    sem_t mutex, w;				// 读者-写者模型相关信号量
} Cache;

Cache ca[MAX_CACHE];

initCache

初始化缓存。没有什么好解释的

void initCache() {
    for (int i = 0; i < MAX_CACHE; ++i) {
        ca[i].empty = 1;
        Sem_init(&ca[i].mutex, 0, 1);
        Sem_init(&ca[i].w, 0, 1);
    }
}

getCache

寻找 URL 对应的缓存,如果不存在返回 NULL

具体地,枚举每一个缓存,判断是否为空,如果不为空判断 URL 是否相等。

urlEqual 函数为判断 URL 相同的函数。

如果找到,要顺便更新一下这个缓存的 LRU,即上次访问时间。

Cache *getCache(URL *url) {
    Cache *ans = NULL;
    for (int i = 0; i < MAX_CACHE && ans == NULL; ++i) {
        readBegin(&ca[i]);
        if (!ca[i].empty && urlEqual(&ca[i].url, url)) ans = &ca[i];
        readEnd(&ca[i]);
    }
    if (ans != NULL) updateLRU(ans);
    return ans;
}

fillCache

将一个 URL 和网页内容装载入缓存。

void fillCache(Cache *c, URL *url, char *data) {
    writeBegin(c);
    c->empty = 0;
    urlCopy(&c->url, url);
    strcpy(c->data, data);
    writeEnd(c);
    updateLRU(c);
}

updateLRU

更新 LRU。

这里使用了一个 static 变量,每次调用都会加 \(1\),因此可以作为时钟使用。

void updateLRU(Cache *c) {
    static int clock = 0;
    writeBegin(c);
    c->lru = ++clock;
    writeEnd(c);
}

insCache

将一个 URL 及对应的网页插入缓存。

首先判断是否有空余位置,如果有的话可以直接放入。

否则,遍历所有缓存,找到上次访问时间最远的(lru 成员最小的),将之替换掉。

void insCache(URL *url, char *data) {
    Cache *pos = NULL;
    for (int i = 0; i < MAX_CACHE && pos == NULL; ++i) {
        readBegin(&ca[i]);
        if (ca[i].empty) pos = &ca[i];
        readEnd(&ca[i]);
    }
    // fprintf(stderr, "insCache: pos = %#p\n", pos);
    if (pos != NULL) {
        fillCache(pos, url, data);
        return;
    }
    
    int minLRU = __INT_MAX__;
    for (int i = 0; i < MAX_CACHE; ++i) {
        readBegin(&ca[i]);
        if (!ca[i].empty && ca[i].lru < minLRU) {
            minLRU = ca[i].lru;
            pos = &ca[i];
        }
        readEnd(&ca[i]);
    }
    fillCache(pos, url, data);
}

测试结果

./20230908-csapp-proxylab/image-20230909000110138

完整程序:链接

总结

先放一下最终得分:

./20230908-csapp-proxylab/image-20230909000241476

嘿嘿,终于做完 CSAPP 的所有实验啦!

收获很多,这本书算是我真正的计算机启蒙教材吧。

接下来准备去看一些操作系统方向的书籍,希望能找到适合自己的学习方向!

标签:CSAPP,url,ca,lab,URL,int,Proxy,line,MAXLINE
From: https://www.cnblogs.com/hankeke303/p/18155107/csapp-proxylab

相关文章

  • CSAPP Lab-1 DATALAB
    本文原发于2023-09-0215:32:57于我的hexo博客,现迁移至此。最近看完了CSAPP整本书,发现官网上还有11次实验可以做。UPD:好像只有9个,因为有两个是旧版本的,可以被新版的替代掉。UPD:好像只有8个,performance也算是旧的实验了,但是没有明确指出。Lab地址:http://csapp.cs......
  • CSAPP Lab-3 ATTACKLAB
    书接上回,这次做到了第三个Lab啦。任务描述这一个Lab的任务就更有意思了,实验给了我们两个程序,每个程序都会让我们输入一行字符串,而它们是通过下面这个函数来读取的:unsignedgetbuf(){ charbuf[BUFFER_SIZE]; Gets(buf); return1;}其中,Gets函数和C库的gets函数......
  • CSAPP Lab-4 Architecture Lab
    本次实验是有关书上第四章设计的Y86-64处理器的,实验分为三个部分,分别是编写几个简单的Y86-64程序、使用一条新指令扩展SEQ模拟器以及优化Y86-64的基准测试程序和处理器设计。实验准备需要简单复习一下Y86-64的指令集架构以及处理器架构呢。指令集架构指令集:指令功......
  • CSAPP Lab5 Cache Lab
    到实验5啦!这次的实验是有关高速缓存的。让我们先来复习一下高速缓存的基础知识吧!复习高速缓存的结构在一个存储器地址有\(m\)位的系统上,一共有\(M=2^m\)个地址。假设高速缓存被组织成一个有\(S=2^s\)个高速缓存组的数组,其中每个组包括\(E\)个高速缓存行,每行存......
  • CSAPP Lab6 Shell Lab
    本次实验的任务很清晰,实现一个简单的UnixShell。需要用到基础的进程控制、信号处理等知识。简单来说,实验已经提供了一些简单的功能,我们需要在此基础上,实现下面的功能:eval:解析和解释命令行的主例程。[70行]builtin_cmd:识别并解释内置命令quit(退出)、fg(前台运行某个作业)、bg(后......
  • CSAPP Lab-2 BOMBLAB
    第二个Lab就比较有趣了。这一个Lab的任务是,我们得到了一个bomb炸弹程序,这个炸弹程序有\(6\)个phase,每个phase都会读取我们的输入,判断我们的输入是否符合要求,如果正确这个phase的炸弹就会被拆除,否则炸弹就会爆炸。我们需要借助各种工具,对程序进行反汇编等等,获得能够......
  • 伯克利大学 CS61B Lab配置教程
    基本过程:首先将伯克利大学的代码框架下载到自己的电脑,然后我们直接在框架里修改就行将自己的代码上传到github上,然后使用伯克利大学的Gradescope评测自己写的代码下载代码在自己电脑桌面新建一个文件夹,这里我命名为:cs61b,打开gitbash,使用cd进入我们新创建的文件夹,注意路径......
  • 读《我和Labview》5条件结构和顺序结构
    5条件结构和顺序结构条件结构布尔类型条件选择结构其它数据类型的条件选择是否要设置默认分支?合理设置悬着条件隧道避免把控件放在条件结构内选择函数顺序结构程序执行顺序创建顺序结构层叠式顺序结构平铺式顺序结构无形胜有形的最高境界6用户自定义控件7控件的局......
  • Linux服务器中Docker部署的GitLab镜像访问出现500错误
    一背景这几天发现在Linux服务器中Docker部署的GitLab镜像访问出现500错误,在重启服务器后大概10分钟再次出现该情况,后面登录服务器一步步排查最终解决问题,现在将解决问题的过程做一个总结。二过程分析首先第一步就是看看我们Docker目录下文件占用的情况,因为我们的Linux服务......
  • 如何将Docker中GitLab数据备份到宿主Linux上
    一宿主机准备存放备份文件的目录建议以年月日进行命名使用putty.exe或者PowerShell登录远程服务器cdshare(如果没有当前目录请创建该共享目录)mkdir20220930(在共享目录下创建备份文件夹)二进入Docker容器内部备份数据1.执行命令sudodockerexec-itgitlab/bin/......