实验目的与要求
1、让学生应用套接字接口实现网络编程;
2、让学生理解Web服务器开发的相关知识;
3、让学生应用并发编程技术进行并发服务器的开发;
实验原理与内容
Web代理是一种在Web浏览器和终端服务器之间充当中介角色的程序。在Web代理的帮助下,浏览器不是直接联系终端服务器以获取网页,而是浏览器会首先联系代理,代理会向终端服务器转发请求,当终端服务器响应代理时,代理会将响应发送到浏览器。
代理有多种用途,有时可以在防火墙中使用代理,使得防火墙只能通过代理联系防火墙以外的服务器。代理还可以使客户端匿名,通过剥离请求的所有标识信息,代理可以使浏览器对Web服务器匿名。代理甚至可以通过将来自服务器的对象存储到本地来实现缓存,后续的请求可以直接从缓存中获取Web对象而不需要再次与远程服务器通信。
3.1第一部分:实现顺序的Web代理程序
第一步是实现一个处理HTTP/1.0 GET请求的简单顺序代理程序,其它的请求类型(如POST等)不作要求。在代理程序启动时,程序将在命令行参数指定的端口上侦听连接请求。一旦建立了连接,您的代理程序应该读取整个HTTP请求并对请求进行解析。它需要判断客户端是否发送了有效的HTTP请求。如果HTTP请求有效,则建立自己到相应Web服务器的连接,然后向服务器请求客户端所指定的对象。最后代理程序读取服务器的响应并将其转发给客户端。
3.1.1 HTTP/1.0 GET requests
当终端用户在Web浏览器的地址栏中输入URL时,例如http://www.cmu.edu/hub/index.html,浏览器将向代理程序发送HTTP请求,该请求会以类似于以下的内容作为请求行:
GET http://www.cmu.edu/hub/index.html HTTP/1.1
在这种情况下,代理程序应该将请求解析为至少以下字段:主机名(www.cmu.edu)和其后面的路径或查询的内容(/hub/index.html)。这样,代理程序知道自己需要打开到www.cmu.edu的连接并以以下的形式发送自己的HTTP请求:
GET /hub/index.html HTTP/1.0
请注意,HTTP请求中的所有行都要以回车符“\r\n”结尾。而且重要的是,每个HTTP请求都要以空行“\r\n”终止。
在上面的示例中,您应该注意到Web浏览器的请求行以HTTP/1.1结尾,而代理程序的请求行以HTTP/1.0结尾。现代Web浏览器会生成HTTP/1.1请求,但是您的代理程序应该处理它们并将它们以HTTP/1.0的方式进行转发。
需要认识到的是,HTTP请求,即使只是HTTP/1.0 GET请求,也可以复杂到难以置信。书本描述了HTTP事务的某些细节,但如果您想获取完整的HTTP/1.0规范则应该参考RFC 1945。在理想情况下,代理程序应该可以解析所有的HTTP请求,不过在本实验中您的代理程序不要求处理多个请求行的情况。不过您的代理程序绝不能因请求格式错误而终止。
3.1.2 请求头
在本实验中重要的请求头是:Host、User-Agent、Connection和Proxy-Connection。
- 始终发送Host请求头,虽然HTTP/1.0规范中在技术上不支持这种行为,但在某些Web服务器中有必要诱使其做出合理的响应,尤其是那些使用虚拟主机的服务器。
Host请求头描述终端服务器的主机名。例如,访问http://www.cmu.edu/hub/index.html,您的代理程序应该发送以下请求头:
Host: www.cmu.edu
Web浏览器可能会将自己的Host请求头附加到HTTP请求。如果是这样的话,代理程序应使用与浏览器相同的Host请求头。
- 您可以选择始终发送以下User-Agent请求头:
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3)Gecko/20120305 Firefox/10.0.3
代理程序应将请求头作为单行发送。User-Agent请求头用来标识客户端(什么操作系统、什么浏览器等),而Web服务器通常使用请求头所标识的信息来组织其返回的信息。发送这一User-Agent请求头,可以使得返回的字符串在内容和格式上有所改进,在telnet测试中非常有用。
- 始终发送以下的Connection请求头:
Connection: close
- 永远发送以下Proxy-Connection请求头:
Proxy-Connection: close
Connection和Proxy-Connection请求头用于指明连接在第一次请求/响应完成后是否保持活动状态。本实验强烈建议您的代理程序为每个请求打开一个新的连接。把这些请求头的值指定为“close”会提醒Web服务器您的代理程序会在每一次请求/响应后关闭连接。
为了方便起见,在proxy.c文件中以字符串常量的方式为您提供了所述的User-Agent请求头的值。
最后,如果浏览器发送的HTTP请求里面添加了其它的一些额外的请求头,那么您的代理程序应该不做任何更改地转发它们。
3.1.3 端口号
本实验有两类重要的端口号:HTTP请求的端口号和代理程序监听的端口号。
HTTP请求的端口号是HTTP请求中URL里的可选字段。也就是说,URL可能是这种形式:http://www.cmu.edu:8080/hub/index.html。在这种情况下,代理程序应该连接主机www.cmu.edu的8080端口,而不是默认的80端口。您的代理无论URL中是否包含端口号,都必须正常工作。
监听端口号是代理程序监听连接请求的端口号。监听端口号以命令行参数的方式传入代理程序。例如在命令行以以下的命令启动代理程序,代理程序会监听端口15213上的连接:
linux> ./proxy 15213
您可以使用任何未被其它进程使用的非特权监听端口号(大于1024且小于65536)。由于每个代理程序必须使用唯一的监听端口号,而且每台机器上会有许多进程同时在工作,因此我们提供了一个port-for-user.pl脚本来帮助您选择端口号。带上您的用户ID来执行该脚本可以帮您生成端口号:
linux> ./port-for-user.pl droh
droh: 45806
由脚本port-for-user.pl返回的端口号p始终是偶数的,所以如果您需要额外的端口号的话,例如给Tiny server使用,那么您可以安全地使用端口p和p+1。
请不要自己随机选一个端口号,如果这样做,有可能干扰到其他用户。
3.2 第二部分:处理多个并发请求
一旦有了一个正常工作的顺序代理程序,就可以对其进行修改,以同时处理多个请求。实现并发服务器的最简单方法是为每个新连接请求都生成一个新线程来处理,当然也可以使用书本上提到的其它方法来进行实现。
- 请注意,线程应该在分离模式下运行,以避免内存泄漏。
- 书本中描述的open_clientfd和open_listenfd函数是基于现代且与协议无关的getaddrinfo函数的,因此是线程安全的。
3.3 第三部分:缓存Web对象
在本实验的最后一部分中,您将向代理程序添加缓存功能,用于在内存中存储最近使用的Web对象。HTTP实际上定义了一个相当复杂的模型,通过该模型,Web服务器可以指示如何缓存它们所提供的对象,客户端可以指定如何使用这些缓存。但是,您的代理程序将采用简化的方法。
当代理程序从服务器接收到Web对象时,它应该在给客户端传输该对象时将其缓存在内存中。如果另一个客户端向同一服务器请求相同的对象,则代理程序不需要重新连接到服务器,它只要简单地将缓存的对象发回给客户端即可。
显然,如果您的代理程序要缓存所有被请求的对象,那么它将需要无限的内存。此外,由于某些Web对象比其它对象大,因此可能会出现以下情况:一个巨大的对象消耗了整个缓存,从而根本无法缓存其它对象。为了避免这些问题,代理程序应该同时规定最大缓存容量和最大缓存对象尺寸。
3.3.1 最大缓存容量
代理程序的整个缓存应具有以下最大容量:MAX_CACHE_SIZE = 1 MB
当累计缓存的大小时,代理程序应该仅计算用于存储实际Web对象的字节数,其它额外的字节,包括文件的元数据等,应该忽略。
3.3.2 最大对象尺寸
代理程序应仅缓存不超过以下最大尺寸的Web对象:MAX_OBJECT_SIZE = 100 KB
为了方便起见,这两个大小限制都已经在proxy.c文件中以宏的方式进行提供。
正确实现缓存的最简单方法是为每个活动连接都分配缓冲区,并对从服务器接收到的数据进行累计。如果缓冲区的大小超过最大对象尺寸,则丢弃该对象。如果Web服务器的响应没有超过最大对象尺寸则可以缓存该对象。使用此方案,代理程序用于Web对象的最大数据量为如下所示,其中T是最大活动连接数:
MAX_CACHE_SIZE + T * MAX_OBJECT_SIZE
3.3.3 驱逐策略
您的代理程序的缓存应采用最近最少使用(LRU)逐出策略,它不一定要是严格意义上的LRU,但应该要跟LRU相当接近。请注意,对一个对象的读和写都算作是使用该对象。
3.3.4 同步
对缓存的访问必须是线程安全的,并且确保缓存访问不受竞争条件的影响可能是该实验里更有挑战的一个地方。事实上,对缓存有一个特殊的要求,那就是多个线程必须能够同时从缓存中进行读取。当然,一次只能允许有一个线程能够对缓存进行写入,但读取缓存不能存在这种限制。
因此,使用一个大的排它锁来保护对缓存的访问不是一个可取的解决方案。你可能需要探索其它一些新方法,例如对缓存进行分区、使用Pthreads读者-写者锁或使用信号量实现自己的读写方案。无论使用哪种方法,您都不必严格实现LRU逐出策略,这使您在实现缓存时获得一定的灵活性。
在本实验中,您将编写一个可以缓存Web对象的简单HTTP代理程序。对于实验的第一部分,您将开发代理程序以接受连接、读取和分析请求、将请求转发到Web服务器、读取服务器的响应、并将这些响应转发给相应的客户端。在第一部分中您将学习基本的HTTP操作以及如何使用套接字接口编写网络通信程序。在第二部分中,您将对代理程序进行升级以处理多个并发连接。这将使您学习如何处理并发性,这是一个至关重要的系统概念。在最后一部分也就是第三部分,您将为您的代理程序添加缓存功能,使得代理程序可以在内存中简单地缓存最近访问的Web对象。
实验设备与软件环境
1.Linux操作系统—64位 Ubuntu 18.04
2. C编译环境(gcc)
3. 计算机
实验过程与结果
解压proxylab文件,目录结构如下:
文件解析:
csapp.c、csapp.h、 proxy.c 这三个文件是项目主体,可以随意修改
driver.sh 测评脚本
1.使用命令“sudo apt install net-tools” 来安装netstat工具。
2.使用命令“sudo snap install curl” 来安装curl工具。
3.把文件解压下来,先尝试执行“make clean”、“make”命令,对代码进行重新编译。
- 实现顺序web代理
一个简单的HTTP代理服务器的实现,它接收客户端的HTTP请求,解析请求,然后转发到目标服务器,并将服务器的响应返回给客户端。修改proxy.c文件:
代码如下:
#include <stdio.h>
#include "csapp.h"
/* Recommended max cache and object sizes */
/* 定义最大缓存和对象大小 */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
/* User-Agent字符串,模拟浏览器访问 */
/* 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";
/* 函数声明 */
void doit(int clientfd);
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head);
void read_requesthdrs(rio_t *rp,int fd);
void return_content(int serverfd, int clientfd);
int main(int argc,char **argv)
{
int listenfd,connfd;
char hostname[MAXLINE],port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;
/* 检查命令行参数 */
if(argc != 2){
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
/* 代理创建一个监听描述符,准备好接收连接请求 */
listenfd = Open_listenfd(argv[1]);
while(1){ /* 循环接受客户端连接 */
clientlen = sizeof(clientaddr);
/* 等待来着客户端的连接请求到侦听描述符listenfd,
然后在addr中填写客户端的套接字地址,并返回一个已连接描述符
*/
connfd = Accept(listenfd,(SA *)&clientaddr,&clientlen);
/* 将套接字地址结构clientaddr转化成对应的主机和服务名字符串,
并将他们复制到hostname和port缓冲区
*/
Getnameinfo((SA *)&clientaddr,clientlen,hostname,MAXLINE,port,MAXLINE,0);
printf("Accepted connection from (%s, %s)\n",hostname,port);
doit(connfd); /* 处理客户端请求 */
Close(connfd); // 关闭连接
}
}
/* 处理客户端HTTP事务 */
void doit(int clientfd){
/* 定义变量 */
char buf[MAXLINE],method[MAXLINE],uri[MAXLINE],version[MAXLINE];
char hostname[MAXLINE],path[MAXLINE],port[MAXLINE],request_head[MAXLINE];
int serverfd;
rio_t rio;
/* 读取请求行和请求头 */
Rio_readinitb(&rio,clientfd);
Rio_readlineb(&rio,buf,MAXLINE);
sscanf(buf,"%s %s %s",method,uri,version);
/* 仅处理GET请求 */
if(strcasecmp(method,"GET")){
printf("Not implemented");
return;
}
/* 解析uri取hostname和path和port。生成request_head */
parse_uri(uri,hostname,path,port,request_head);
/* 建立与服务器的连接 */
serverfd = Open_clientfd(hostname,port);
/* 将请求头传递给服务器 */
Rio_writen(serverfd,request_head,strlen(request_head));
read_requesthdrs(&rio,serverfd);
/* 将服务端读取的数据返回给客户端 */
return_content(serverfd,clientfd);
}
/* 解析uri中的hostname和path和port。并生成request_head
* uri例子:http://www.cmu.edu:8080/hub/index.html
* hostname:www.cmu.edu
* path:/hub/index.html
* port:8080
*/
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head){
sprintf(port,"80"); //默认值
char *end,*bp;
char *tail = uri+strlen(uri); //uri的最后一个字符,不是'\0'。
char *bg = strstr(uri,"//");
bg = (bg!=NULL ? bg+2 : uri); //取hostname的开头。
end = bg;
//取hostname的结尾。
while(*end != '/' && *end != ':') end++;
strncpy(hostname,bg,end-bg);
bp = end + 1; //取port的开头
if(*end == ':'){ //==':'说明uri中有port
end++;
bp = strstr(bg,"/"); //取port的结尾
strncpy(port,end,bp-end);
end = bp; //取uri的开头
}
strncpy(path,end,(int)(tail-end)+1);
/* 请求的开头行:GET /hub/index.html HTTP/1.0。 */
sprintf(request_head,"GET %s HTTP/1.0\r\nHost: %s\r\n",path,hostname);
return 1;
}
/*
* 读取HTTP请求头
* Host,User-Agent,Connection和Proxy-Connection用指定的的
* 保留其他的头(header)
*/
void read_requesthdrs(rio_t *rp,int fd){
char buf[MAXLINE];
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));
/* 保留其他的头(header) */
for(Rio_readlineb(rp,buf,MAXLINE);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));
return;
}
/*
* 将服务端读取的数据返回给客户端
*/
void return_content(int serverfd, int clientfd){
/* 读取服务器响应并转发到客户端 */
size_t n;
char buf[MAXLINE];
rio_t srio;
Rio_readinitb(&srio,serverfd);
while((n = Rio_readlineb(&srio,buf,MAXLINE)) != 0){
Rio_writen(clientfd,buf,n);
}
}
创建一个监听套接字,等待客户端的连接请求。接受客户端连接后,读取HTTP请求行并解析出请求方法、URI和HTTP版本。如果请求方法是GET,则解析URI以获取主机名、路径和端口。连接到目标服务器,并将客户端的请求头发送给服务器。读取服务器的响应头和内容,并将它们转发回客户端。关闭与客户端和服务器的连接。
每次修改完程序文件后,执行“make clean”、“make”命令,对代码进行重新编译。
执行: ./driver.sh
可以看到第一部分已经完成。
- 处理多个并发请求
多线程HTTP代理服务器的实现,它使用pthread库来处理每个客户端的连接
修改proxy.c文件
代码如下:
#include <stdio.h>
#include "csapp.h"
/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
/* User-Agent字符串,模拟浏览器访问 */
/* 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";
void doit(int clientfd);
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head);
void read_requesthdrs(rio_t *rp,int fd);
void return_content(int serverfd, int clientfd);
void *thread(void *vargp);
int main(int argc,char **argv)
{
int listenfd;
int *connfd; /* 使用指针,避免竞争 */
char hostname[MAXLINE],port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;
pthread_t tid;
/* 检查命令行参数 */
if(argc != 2){
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
/* 代理创建一个监听描述符,准备好接收连接请求 */
listenfd = Open_listenfd(argv[1]);
while(1){
clientlen = sizeof(clientaddr);
connfd = Malloc(sizeof(int)); /* 使用指针避免竞争 */
/* 等待来着客户端的连接请求到侦听描述符listenfd,
然后在addr中填写客户端的套接字地址,并返回一个已连接描述符
*/
*connfd = Accept(listenfd,(SA *)&clientaddr,&clientlen);
/* 将套接字地址结构clientaddr转化成对应的主机和服务名字符串,
并将他们复制到hostname和port缓冲区
*/
Getnameinfo((SA *)&clientaddr,clientlen,hostname,MAXLINE,port,MAXLINE,0);
printf("Accepted connection from (%s, %s)\n",hostname,port);
/* 调用pthread_create函数来创建其他线程 */
Pthread_create(&tid,NULL,thread,connfd);
}
}
/* 线程函数,处理客户端连接 */
void *thread(void *vargp){
int connfd = *((int *)vargp);
Pthread_detach(pthread_self());
Free(vargp); /* 释放指针内存 */
doit(connfd); /* 处理HTTP事务 */
Close(connfd); /* 关闭连接 */
return NULL;
}
/* 处理客户端HTTP事务 */
void doit(int clientfd){
char buf[MAXLINE],method[MAXLINE],uri[MAXLINE],version[MAXLINE];
char hostname[MAXLINE],path[MAXLINE],port[MAXLINE],request_head[MAXLINE];
int serverfd;
rio_t rio;
/* 读取请求行和请求头 */
Rio_readinitb(&rio,clientfd);
Rio_readlineb(&rio,buf,MAXLINE);
sscanf(buf,"%s %s %s",method,uri,version);
if(strcasecmp(method,"GET")){
printf("Not implemented");
return;
}
/* 解析uri取hostname和path和port。生成request_head */
parse_uri(uri,hostname,path,port,request_head);
/* 建立与服务器的连接 */
serverfd = Open_clientfd(hostname,port);
/* 将请求头传递给服务器 */
Rio_writen(serverfd,request_head,strlen(request_head));
read_requesthdrs(&rio,serverfd);
/* 将服务端读取的数据返回给客户端 */
return_content(serverfd,clientfd);
}
/* 解析uri中的hostname和path和port。并生成request_head
* uri例子:http://www.cmu.edu:8080/hub/index.html
* hostname:www.cmu.edu
* path:/hub/index.html
* port:8080
*/
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head){
sprintf(port,"80"); //默认值
char *end,*bp;
char *tail = uri+strlen(uri); //uri的最后一个字符,不是'\0'。
char *bg = strstr(uri,"//");
bg = (bg!=NULL ? bg+2 : uri); //取hostname的开头。
end = bg;
//取hostname的结尾。
while(*end != '/' && *end != ':') end++;
strncpy(hostname,bg,end-bg);
bp = end + 1; //取port的开头
if(*end == ':'){ //==':'说明uri中有port
end++;
bp = strstr(bg,"/"); //取port的结尾
strncpy(port,end,bp-end);
end = bp; //取uri的开头
}
strncpy(path,end,(int)(tail-end)+1);
/* 请求的开头行:GET /hub/index.html HTTP/1.0。 */
sprintf(request_head,"GET %s HTTP/1.0\r\nHost: %s\r\n",path,hostname);
return 1;
}
/*
* 读取HTTP请求头
* Host,User-Agent,Connection和Proxy-Connection用指定的的
* 保留其他的头(header)
*/
void read_requesthdrs(rio_t *rp,int fd){
/* 发送User-Agent头 */
char buf[MAXLINE];
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));
/* 保留其他的头(header) */
for(Rio_readlineb(rp,buf,MAXLINE);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));
return;
}
/*
* 将服务端读取的数据返回给客户端
*/
void return_content(int serverfd, int clientfd){
size_t n;
char buf[MAXLINE];
rio_t srio;
Rio_readinitb(&srio,serverfd);
while((n = Rio_readlineb(&srio,buf,MAXLINE)) != 0){
Rio_writen(clientfd,buf,n);
}
}
通过创建一个监听套接字,等待客户端的连接请求。接受客户端连接后,为每个连接创建一个新线程来处理该连接。在新线程中,读取HTTP请求行并解析出请求方法、URI和HTTP版本。如果请求方法是GET,则解析URI以获取主机名、路径和端口。连接到目标服务器,并将客户端的请求头发送给服务器。读取服务器的响应头和内容,并将它们转发回客户端。关闭与客户端和服务器的连接。
每次修改完程序文件后,执行“make clean”、“make”命令,对代码进行重新编译。
执行: ./driver.sh
可以看到第二部分已经完成
3、缓存web对象
使用一个多线程HTTP代理服务器,实现了LRU(最近最少使用)缓存机制和读写锁。
代码如下:
#include <stdio.h>
#include "csapp.h"
/* 定义常量 */
/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
#define MAX_CACHE 10
/* User-Agent字符串 */
/* 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";
/* 函数声明 */
void doit(int clientfd);
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head);
void read_requesthdrs(rio_t *rp,int fd);
void return_content(int serverfd, int clientfd,char *url);
void *thread(void *vargp);
int maxlrucache();
/*读写者锁的结构体*/
struct RWLOCK_T{
sem_t lock; //基本锁
sem_t writeLock; //写着锁
int readcnt; //读者个数
};
/*LRU缓存的结构体*/ /* 缓存条目结构 */
struct CACHE{
int lruNumber; //LRU计数引用次数,根据大小排位。大的表示最近引用过
char url[MAXLINE]; //通过url唯一标识对应content
char content[MAX_OBJECT_SIZE]; //缓存内容
};
/* 全局缓存数组和读写锁指针 */
struct CACHE cache[MAX_CACHE]; //缓存,最多有MAX_CACHE个
struct RWLOCK_T* rw; //读写者锁指针
void rwlock_init(); //初始化读写者锁指针
char *readcache(char *url); //读缓存
void writecache(char *buf,char *url); //写缓存
int main(int argc,char **argv)
{
int listenfd;
int *connfd; /* 使用指针,避免竞争 */
char hostname[MAXLINE],port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;
pthread_t tid;
if(argc != 2){
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
rw = Malloc(sizeof(struct RWLOCK_T));
rwlock_init();
/* 代理创建一个监听描述符,准备好接收连接请求 */
listenfd = Open_listenfd(argv[1]);
while(1){
clientlen = sizeof(clientaddr);
connfd = Malloc(sizeof(int));
/* 等待来着客户端的连接请求到侦听描述符listenfd,
然后在addr中填写客户端的套接字地址,并返回一个已连接描述符
*/
*connfd = Accept(listenfd,(SA *)&clientaddr,&clientlen);
/* 将套接字地址结构clientaddr转化成对应的主机和服务名字符串,
并将他们复制到hostname和port缓冲区
*/
Getnameinfo((SA *)&clientaddr,clientlen,hostname,MAXLINE,port,MAXLINE,0);
printf("Accepted connection from (%s, %s)\n",hostname,port);
/* 调用pthread_create函数来创建其他线程 */
Pthread_create(&tid,NULL,thread,connfd);
}
}
void *thread(void *vargp){
int connfd = *((int *)vargp);
Pthread_detach(pthread_self());
Free(vargp);
doit(connfd);
Close(connfd);
return NULL;
}
/* 处理客户端HTTP事务 */
void doit(int clientfd){
char buf[MAXLINE],method[MAXLINE],uri[MAXLINE],version[MAXLINE];
char hostname[MAXLINE],path[MAXLINE],port[MAXLINE],request_head[MAXLINE];
int serverfd;
rio_t rio;
/* 读取请求行和请求头 */
Rio_readinitb(&rio,clientfd);
Rio_readlineb(&rio,buf,MAXLINE);
sscanf(buf,"%s %s %s",method,uri,version);
if(strcasecmp(method,"GET")){
printf("Not implemented");
return;
}
// 尝试从缓存读取内容
char *content = readcache(uri);
if(content != NULL){
Rio_writen(clientfd,content,strlen(content));
free(content);
}else{
/* 解析uri取hostname和path和port。生成request_head */
parse_uri(uri,hostname,path,port,request_head);
/* 建立与服务器的连接 */
serverfd = Open_clientfd(hostname,port);
/* 将请求头传递给服务器 */
Rio_writen(serverfd,request_head,strlen(request_head));
read_requesthdrs(&rio,serverfd);
/* 将服务端读取的数据返回给客户端 */
return_content(serverfd,clientfd,uri);
}
}
/* 解析uri中的hostname和path和port。并生成request_head
* uri例子:http://www.cmu.edu:8080/hub/index.html
* hostname:www.cmu.edu
* path:/hub/index.html
* port:8080
*/
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head){
// 解析URI以提取主机名、路径和端口
sprintf(port,"80"); //默认值
char *end,*bp;
char *tail = uri+strlen(uri); //uri的最后一个字符,不是'\0'。
char *bg = strstr(uri,"//");
bg = (bg!=NULL ? bg+2 : uri); //取hostname的开头。
end = bg;
//取hostname的结尾。
while(*end != '/' && *end != ':') end++;
strncpy(hostname,bg,end-bg);
bp = end + 1; //取port的开头
if(*end == ':'){ //==':'说明uri中有port
end++;
bp = strstr(bg,"/"); //取port的结尾
strncpy(port,end,bp-end);
end = bp; //取uri的开头
}
strncpy(path,end,(int)(tail-end)+1);
/* 请求的开头行:GET /hub/index.html HTTP/1.0。 */
sprintf(request_head,"GET %s HTTP/1.0\r\nHost: %s\r\n",path,hostname);
return 1;
}
/*
* 读取HTTP请求头
* Host,User-Agent,Connection和Proxy-Connection用指定的的
* 保留其他的头(header)
*/
void read_requesthdrs(rio_t *rp,int fd){
// 读取并转发HTTP请求头到服务器
char buf[MAXLINE];
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));
/* 保留其他的头(header) */
for(Rio_readlineb(rp,buf,MAXLINE);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));
return;
}
/*
* 将服务端读取的数据返回给客户端
*/
void return_content(int serverfd, int clientfd,char *uri){
// 从服务器读取内容并返回给客户端,同时写入缓存
size_t n,size = 0;
char buf[MAXLINE],content[MAX_OBJECT_SIZE];
rio_t srio;
Rio_readinitb(&srio,serverfd);
while((n = Rio_readlineb(&srio,buf,MAXLINE)) != 0){
Rio_writen(clientfd,buf,n);
if(n + size <= MAX_OBJECT_SIZE){
sprintf(content + size,"%s",buf);
size += n;
}else{
size = MAX_OBJECT_SIZE + 1;
}
}
writecache(content,uri);
}
/*-----cache start-----*/
void rwlock_init(){ // 初始化读写锁
rw->readcnt = 0;
sem_init(&rw->lock,0,1);
sem_init(&rw->writeLock,0,1);
}
void writecache(char *buf,char *url){ // 向缓存写入数据
sem_wait(&rw->writeLock); //等待获得写者锁
int index;
/*看看缓存有没有空位*/
for(index = 0;index < MAX_CACHE;index++){
if(cache[index].lruNumber == 0){
break;
}
}
/*没有空位,根据LRU政策驱逐*/
if(index == MAX_CACHE){
int minlru = cache[0].lruNumber;
/*找到最后访问的缓存*/
for(int i = 1;i < MAX_CACHE;i++){
if(cache[i].lruNumber < minlru){
minlru = cache[i].lruNumber;
index = i;
}
}
}
cache[index].lruNumber = maxlrucache()+1;
strcpy(cache[index].url,url);
strcpy(cache[index].content,buf);
sem_post(&rw->writeLock); //释放锁
return;
}
char *readcache(char *url){ // 从缓存中读取数据
sem_wait(&rw->lock); //读者等待并获取锁
if(rw->readcnt == 1)
sem_wait(&rw->writeLock); //读者在读,不允许有写者
rw->readcnt++;
sem_post(&rw->lock); //释放锁
char *content = NULL;
for(int i = 0;i < MAX_CACHE;i++){
/*找到了对应的缓存*/
if(strcmp(url,cache[i].url) == 0){
content = (char *)Malloc(strlen(cache[i].content));
strcpy(content,cache[i].content);
int maxlru = maxlrucache(); //获取最大的lru
cache[i].lruNumber = maxlru+1; //+1成最大的lru
break;
}
}
sem_wait(&rw->lock); //等待并获取锁
rw->readcnt--;
if(rw->readcnt == 0) //没有读者了,释放写者锁
sem_post(&rw->writeLock);
sem_post(&rw->lock); //释放锁
return content;
}
int maxlrucache(){ // 计算最大的LRU计数
int i;
int max=0;
for(i = 0;i<MAX_CACHE;i++){
if(cache[i].lruNumber > max){
max = cache[i].lruNumber;
}
}
return max;
}
创建一个监听套接字,等待客户端的连接请求。为每个客户端连接创建一个新线程来处理该连接。在新线程中,尝试从缓存中读取内容,如果缓存未命中,则向目标服务器发起请求。将服务器的响应返回给客户端,并将内容写入缓存。缓存使用LRU(最近最少使用)算法管理,当缓存满时,最长时间未被引用的条目将被替换。
使用读写锁来同步对缓存的访问,确保多线程环境下数据的一致性。
每次修改完程序文件后,执行“make clean”、“make”命令,对代码进行重新编译。
执行: ./driver.sh
至此实验已完成。