lab7 proxylab

news/2024/7/7 12:04:51

在这里插入图片描述

前情提要,如果看了书本,这个lab难度不高,但是如果不看书,难度还是挺高的,并且这个lab会用到cachelab中学到的东西,需要阅读

  1. 第十章:系统编程
  2. 第十一章:网络编程
  3. 第十二章:并发

实验介绍

  1. 使用代理完成客户端和服务器的连接(HTTP操作,socket通信)
    1. 接受客户端的连接,读并分析请求
    2. 将请求发送给服务器
    3. 读取服务器的回应,并将回应发送给对应的客户端
  2. 实现多线程的功能
  3. 增加cache功能

测试

测试:./driver.sh
50 15 15

第一部分:实现一个顺序的网络代理

任务要求

  1. 最开始,代理应该监听某个端口来等待连接的请求,这个端口通过命令行给出
  2. 一旦建立连接,代理应该读取并解析请求。它需要确定这个请求是否发送了一个合法的HTTP请求
  3. 如果这个请求合法,则发送给服务器,然后将服务器的response返回给客户

具体实现

  1. main函数打开一个监听的描述符,如果通过这个监听描述符accept成功了,则打开了一个用于通信的描述符fd,将fd作为doit的函数,调用doit
  2. doit函数与描述符b建立通信,读取客户端发来的请求,这个请求一定是以下两种形式之一
    1. 指定端口 GET http://www.cmu.edu:8080/hub/index.html HTTP/1.1
    2. 固定端口80 GET http://www.cmu.edu/hub/index.html HTTP/1.1
  3. 将上面收到的请求分解,主要是得到中间的url,然后将url分解,得到hostportpath,以指定端口为例,这三个分别是
    1. www.cmu.edu
    2. 8080
    3. /hub/index.html
  4. 根据上面得到的三个参数,构建发往服务器的request
  5. 这个request是HTTP格式(具体实现上就把这个放到一个字符数组就行了,每一行通过\r\n隔开,并且最后要多一行\r\n),由请求头和请求行组成,实验文档要求格式如下:
    1. GET /hub/index.html HTTP/1.0
    2. Host: www.cmu.edu
    3. User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3
    4. Connection: close
    5. Proxy-Connection: close
  6. 与服务器建立连接,得到server_fd描述符,将上面已经生成好的request发往服务器
  7. 不断地读服务器返回的值,写入fd文件描述符
#include "csapp.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>

/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
#define MAXLINE 8192

/* 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";

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

void cout_uri_format_error() { printf("wrong uri format\n"); }

void parse_line(URI *req_uri, char *uri) {
  char *host_start = strstr(uri, "://");
  if (host_start == NULL) {
    cout_uri_format_error();
    return;
  }
  host_start += 3;
  char *port_start = strstr(host_start, ":");
  char *path_start = strstr(host_start, "/");
  if (path_start == NULL) {
    cout_uri_format_error();
    return;
  }
  strcpy(req_uri->path, path_start);
  *path_start = '\0';
  if (port_start != NULL) {
    strcpy(req_uri->port, port_start + 1);
    *port_start = '\0';
  } else {
    strcpy(req_uri->port, "80");
  }
  strcpy(req_uri->host, host_start);
  return;
}

void build_req_server(char *req_server, URI *req_uri) {
  sprintf(req_server, "GET %s HTTP/1.0\r\n", req_uri->path);
  sprintf(req_server, "%sHost: %s\r\n", req_server, req_uri->host);
  sprintf(req_server, "%s%s", req_server, user_agent_hdr);
  sprintf(req_server, "%sConnection: close\r\n", req_server);
  sprintf(req_server, "%sProxy-Connection: close\r\n", req_server);
  sprintf(req_server, "%s\r\n", req_server);
}

void doit(int fd) {
  // 初始化rio类函数的缓冲区
  rio_t rio;
  Rio_readinitb(&rio, fd);
  // 读入这一行http请求
  char buf[MAXLINE];
  Rio_readlineb(&rio, buf, MAXLINE);
  printf("Request headers:\n");
  printf("%s", buf);
  char method[MAXLINE], uri[MAXLINE], version[MAXLINE];
  // 解析这一行http请求,总共三个部分
  if (sscanf(buf, "%s %s %s", method, uri, version) != 3) {
    printf("HTTP Requset Format Wrong!\n");
    return;
  }
  // 判断是否是GET请求,这个比较函数忽略大小写,get也行
  if (strcasecmp(method, "GET")) {
    printf("method: %s not implemented\n", method);
    return;
  }
  // 至此,已经完成了对客户端请求的解析,接下来要构造出对服务器的请求
  // 首先解析我们的uri,得到host port path
  URI *req_uri = (URI *)malloc(sizeof(URI));
  parse_line(req_uri, uri);
  // 根据我们的信息,构造出真正的发往服务器的请求
  char req_server[MAXLINE];
  build_req_server(req_server, req_uri);
  // 开始连接服务器
  int server_fd = Open_clientfd(req_uri->host, req_uri->port);
  if (server_fd < 0) {
    printf("connection failed\n");
    return;
  }
  // 连接成功,设置缓冲区,将request写入
  rio_t server_rio;
  Rio_readinitb(&server_rio, server_fd);
  Rio_writen(server_fd, req_server, strlen(req_server));
  // 等待服务器的返回,并写入客户端的fd中
  size_t rec_bytes;
  while ((rec_bytes = Rio_readlineb(&server_rio, buf, MAXLINE)) != 0) {
    printf("proxy received %d bytes\n", (int)rec_bytes);
    Rio_writen(fd, buf, rec_bytes);
  }
  Close(server_fd);
}

int main(int argc, char **argv) {

  if (argc != 2) {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(1);
  }
  // 监听请求连接的端口
  int listenfd = Open_listenfd(argv[1]);
  // 与客户端进行连接
  int connfd;
  char hostname[MAXLINE], port[MAXLINE];
  socklen_t clientlen;
  struct sockaddr_storage clientaddr;
  while (1) {
    clientlen = sizeof(clientaddr);
    connfd = Accept(listenfd, (SA *)(&clientaddr), &clientlen);
    Getnameinfo((SA *)(&clientaddr), clientlen, hostname, MAXLINE, port,
                MAXLINE, 0);
    printf("Accepted connection from(%s,%s)\n", hostname, port);
    doit(connfd);
    Close(connfd);
  }

  return 0;
}

第二部分:并发

任务要求

  1. 实现并发即可,没有要求用什么样的方式

具体实现

  1. 采用生产者消费者的方式,和书上第12.5.5节的代码几乎完全一样
  2. 需要在main函数中加入一个Signal(SIGPIPE, SIG_IGN);以屏蔽SIGPIPE信号。我不太清楚不屏蔽会怎么样,可能是不屏蔽的话,客户端如果意外挂了,会导致代理服务器一起挂了
#define SUBFSIZE 16
#define NTHREADS 4

typedef struct {
  int *buf;
  int n;
  int front;
  int rear;
  sem_t mutex;
  sem_t slots;
  sem_t items;
} sbuf_t;

sbuf_t sbuf;

void sbuf_init(sbuf_t *sp, int n) {
  sp->buf = Calloc(n, sizeof(int));
  sp->n = n;
  sp->front = sp->rear = 0;
  Sem_init(&sp->mutex, 0, 1);
  Sem_init(&sp->slots, 0, n);
  Sem_init(&sp->items, 0, 0);
}

void sbuf_deinit(sbuf_t *sp) { Free(sp->buf); }

void sbuf_insert(sbuf_t *sp, int item) {
  P(&sp->slots);
  P(&sp->mutex);
  sp->buf[(++sp->rear) % (sp->n)] = item;
  V(&sp->mutex);
  V(&sp->items);
}

int sbuf_remove(sbuf_t *sp) {
  P(&sp->items);
  P(&sp->mutex);
  int item = sp->buf[(++sp->front) % (sp->n)];
  V(&sp->mutex);
  V(&sp->slots);
  return item;
}

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

int main(int argc, char **argv) {

  if (argc != 2) {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(1);
  }
  // 监听请求连接的端口
  Signal(SIGPIPE, SIG_IGN);
  int listenfd = Open_listenfd(argv[1]);

  // 线程池
  sbuf_init(&sbuf, SUBFSIZE);
  pthread_t pid;
  for (int i = 0; i < NTHREADS; i++) {
    Pthread_create(&pid, NULL, thread, NULL);
  }

  // 与客户端进行连接
  int connfd;
  char hostname[MAXLINE], port[MAXLINE];
  socklen_t clientlen;
  struct sockaddr_storage clientaddr;
  while (1) {
    clientlen = sizeof(clientaddr);
    connfd = Accept(listenfd, (SA *)(&clientaddr), &clientlen);
    sbuf_insert(&sbuf, connfd);
  }

  return 0;
}

第三部分:cache

任务要求

  1. 这里说是cache,还不如说是一个大号的哈希表,以uri为键,以对应的资源为值。然后对这个哈希表的长度有点要求,大概10个表项。因为题目要求#define MAX_CACHE_SIZE 1049000#define MAX_OBJECT_SIZE 102400,其中object的意思就是一行,差不多就是十倍的样子
  2. 如果某个uri对应的资源太大了, 那就不考虑加入cache
  3. 对这个cahce需要实现并发访问,即加上锁,这里加入读写锁

具体实现

  1. 结合cachelab中cache的结构,还需要额外加上data字段
  2. 如果要实现真正的LRU,在并发访问的基础上,还需要对timestamp也加锁,否则就要用原子类型的变量
  3. 这个实现结合代码来看,思路还是比较清晰的,不再赘述
    我在这里犯了两个小错,结果导致debug了好久
  4. cacheline中的tagdata的长度是不一样的,我一开始把data长度弄成了MAXLINE,结果0分
  5. doit中我们用uri去读cache以及写cache,但是我们在doitparse_line函数里,是修改了uri的,因此要给uri搞一个备份,否则在写cache的时候,就错了
/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
#define MAXLINE 8192

typedef struct {
  int is_valid;
  char tag[MAXLINE];
  char data[MAX_OBJECT_SIZE];
  long long access_time;
  int read_cnt;
  sem_t read_lock;
  sem_t write_lock;
} cacheline;

#define MAX_CACHE_LINES 10
cacheline Cache[MAX_CACHE_LINES];

sem_t time_mutex;
long long time_stamp = 1;

void init_cache() {
  for (int i = 0; i < MAX_CACHE_LINES; i++) {
    Cache[i].is_valid = 0;
    Cache[i].access_time = 0;
    Cache[i].read_cnt = 0;
    Sem_init(&Cache[i].read_lock, 0, 1);
    Sem_init(&Cache[i].write_lock, 0, 1);
  }
  Sem_init(&time_mutex, 0, 1);
}

void read_in(int i) {
  P(&Cache[i].read_lock);
  if (Cache[i].read_cnt == 0) {
    P(&Cache[i].write_lock);
  }
  Cache[i].read_cnt++;
  V(&Cache[i].read_lock);
}

void read_out(int i) {
  P(&Cache[i].read_lock);
  if (Cache[i].read_cnt == 1) {
    V(&Cache[i].write_lock);
  }
  Cache[i].read_cnt--;
  V(&Cache[i].read_lock);
}

int read_cache(int fd, char *uri) {
  int flag = 0;
  for (int i = 0; i < MAX_CACHE_LINES; i++) {
    read_in(i);
    if (Cache[i].is_valid && !strcmp(uri, Cache[i].tag)) {
      flag = 1;
      P(&time_mutex);
      Cache[i].access_time = time_stamp++;
      V(&time_mutex);
      Rio_writen(fd, Cache[i].data, strlen(Cache[i].data));
    }
    read_out(i);
    if (flag) {
      return 0;
    }
  }
  return -1;
}

void write_cache(char *uri, char *data) {
  int has_empty = -1;
  int lru_evict = 0;
  for (int i = 0; i < MAX_CACHE_LINES; i++) {
    read_in(i);
    if (Cache[i].is_valid == 0) {
      has_empty = i;
    }
    if (Cache[i].access_time < Cache[lru_evict].access_time) {
      lru_evict = i;
    }
    read_out(i);
    if (has_empty != -1) {
      break;
    }
  }
  int write_index = (has_empty == -1) ? lru_evict : has_empty;
  P(&Cache[write_index].write_lock);
  Cache[write_index].is_valid = 1;
  P(&time_mutex);
  Cache[write_index].access_time = time_stamp++;
  V(&time_mutex);
  strcpy(Cache[write_index].tag, uri);
  strcpy(Cache[write_index].data, data);
  V(&Cache[write_index].write_lock);
}

http://www.niftyadmin.cn/n/4924508.html

相关文章

【腾讯云 Cloud Studio 实战训练营】使用 Cloud Studio 快速构建 Vue + Vite 完成律师 H5 页面

【腾讯云 Cloud Studio 实战训练营】使用 Cloud Studio 快速构建 Vue Vite 完成律师 H5 页面 前言一、基本介绍1.应用场景2.产品优势 二、准备工作1.注册 Cloud Studio2.进入 Vue 预置开发环境 三、使用 Cloud Studio 快速构建 Vue Vite 完成律师 H5 页面1.安装相关依赖包2.主…

SpringBoot+JWT

一、maven坐标 <!-- JWT依赖 --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>com.auth0</groupId>&…

vite项目中使用@代表根路径

1.配置vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue import path from pathexport default defineConfig({plugins: [vue()],resolve: {alias:{: path.resolve(__dirname, src) }} })2.报错path和__dirname 找不到模块“path”或其相…

一文解决JWT相同签名不匹配问题【JWT signature does not match locally computed signature.】

今天做项目的时候&#xff0c;涉及到一个支付记账的功能&#xff0c;想着不能将这些金额数据显示暴露的通过常规的请求体封装来进行传输&#xff0c;想着要是被中途抓包修改了不就麻烦了&#xff0c;所以考虑到这种安全性的需求&#xff0c;就利用上了JWT来进行数据的封装传递&…

激活函数总结(一):ReLU及其变体

激活函数总结&#xff08;一&#xff09; 1 引言2 常用激活函数介绍2.1 Sigmoid激活函数2.2 Tanh激活函数2.3 ReLU激活函数2.4 Leaky ReLU激活函数2.5 Parametric ReLU&#xff08;PReLU&#xff09;激活函数2.6 Swish激活函数 3. 总结 总结的激活函数都在目录中有所展示&#…

@MapperScan 和 @Mapper 源码解读

一.从开发中遇到的问题开始 问题描述 : 在一个springbootmybatis的项目中,在dao也就是Mapper接口上配置了Mapper注解&#xff0c;其他同事在启动类还配置了MapperScan注解&#xff08;包扫描没有配全面&#xff09;&#xff0c;进行批量指定所生成的Mapper接口动态代理接口类&a…

wsl(在windows中使用呢linux系统)适用于windows的linux子系统

步骤可参考微软官方文档https://learn.microsoft.com/zh-cn/windows/wsl/install-manual#step-4—download-the-linux-kernel-update-package 在这里主要列举一些需要注意的点 wsl2的要求 一定要检查下windows版本&#xff0c;版本不对的先升级版本不然无法使用wsl2 wsl支持…

MyBatis和MyBatis-plus配置多数据源和操作多数据库

一&#xff0c;学习MyBatis和MyBatis-plus&#xff1a; mybatis:官网文档:mybatis – MyBatis 3 | 简介 mybatis-plus:官网文档&#xff1a;MyBatis-Plus 二&#xff0c;MyBatis来实现多数据源的查询&#xff1a; 配置文件的格式&#xff1a; spring:datasource: db1:driv…