0%

CSapp-Proxy Lab

Proxy Lab

网络代理是一个在网络浏览器和终端服务器之间充当中间人的程序,而不是直接联系终端服务器以获取网页,浏览器会联系代理,代理会转发请求发送到终端服务器

当终端服务器回复代理时,代理将回复发送到浏览器

代理有很多用途:

  • 有时在防火墙中使用代理,因此防火墙只能通过代理与防火墙之外的服务器联系
  • 代理也可以充当匿名者:通过剥离所有标识信息的请求
  • 代理可以使浏览器对Web匿名服务器
  • 代理甚至可以通过存储来自服务器的对象的本地副本来缓存web对象,通过从缓存中读取请求来响应未来的请求,而不是通过再次与远程服务器

在本实验室中,您将编写一个简单的HTTP代理来缓存web对象,在实验室的第一部分,你将设置代理以接受传入连接、读取和分析请求、将请求转发到web服务器、读取服务器的响应,并将这些响应转发给相应的客户端

第一部分:您将学习基本的HTTP操作,以及如何使用套接字编写通信程序通过网络连接

第二部分:您将升级代理以处理多个并发事件连接,这将向您介绍如何处理并发性,这是一个至关重要的系统概念

第三部分:您将使用一个简单的最近访问的内存缓存向代理添加缓存网络内容

实验文件

  • proxy.c:启动代理服务器的代码写在此处
  • tiny:Tiny web服务器的源代码
  • driver.sh:打分文件

在开始实验前,需要一些储备知识:

服务器简析

每个网络应用都是基于客户端—服务器模型的,釆用这个模型,一个应用是由 一个服务器进程 和一个或者多个 客户端 进程组成

个客户端—服务器事务由以下四步组成:

  1. 当一个客户端需要服务时,它向服务器发送一个请求,发起一个事务,例如,当 Web 浏览器需要一个文件时,它就发送一个请求给 Web 服务器
  2. 服务器收到请求后,解释它,并以适当的方式操作它的资源,例如,当 Web 服务器收到浏览器发出的请求后,它就读一个磁盘文
  3. 服务器给客户端发送一个响应,并等待下一个请求,例如,Web 服务器将文件发送回客户端
  4. 客户端收到响应并处理它,例如,当 Web 浏览器收到来自服务器的一页后,就在屏幕上显示此页

服务器请求

一般的请求消息如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /home.html HTTP/1.0 <!-- 请求消息行 -->
Accept: */* <!-- 请求消息头 -->
Host: localhost:GET /home.html HTTP/1.0
Accept: */*
Host: localhost:GET /home.html HTTP/1.0
Accept: */*
Host: localhost:
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3
Connection: close
Proxy-Connection: close

<html> <!-- 消息正文 -->
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
Dave O'Hallaron
</body>
</html>
  • 请求消息行:请求消息的第一行为请求消息行

    • 例如:GET /test/test.html HTTP/1.1
    • GET 为请求方式,请求方式分为:Get(默认)、POST、DELETE、HEAD等
      • GET:明文传输 不安全,数据量有限,不超过1kb
      • POST:暗文传输,安全,数据量没有限制
    • /test/test.html 为URI,统一资源标识符
    • HTTP/1.1 为协议版本
  • 请求消息头:从第二行开始到空白行统称为请求消息头

    • Accept:浏览器可接受的MIME类型告诉服务器客户端能接收什么样类型的文件
    • Accept-Charset:浏览器通过这个头告诉服务器,它支持哪种字符集
    • Accept-Encoding:浏览器能够进行解码的数据编码方式,比如 gzip
    • Accept-Language:浏览器所希望的语言种类,当服务器能够提供一种以上的语言版本时要用到,可以在浏览器中进行设置
    • Host:初始URL中的主机和端口
    • Referrer:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面
    • Content-Type:内容类型告诉服务器浏览器传输数据的MIME类型,文件传输的类型
    • If-Modified-Since:利用这个头与服务器的文件进行比对,如果一致,则从缓存中直接读取文件
    • User-Agent:浏览器类型
    • Content-Length:表示请求消息正文的长度
    • Connection:表示是否需要持久连接。如果服务器看到这里的值为“Keep -Alive”,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接)
    • Cookie:用于分辨两个请求是否来自同一个浏览器,以及保存一些状态信息
    • Date:请求时间GMT
  • 消息正文:当请求方式是[POST]方式时,才能看见消息正文,消息正文就是要传输的一些数据,如果没有数据需要传输时,消息正文为空

服务器响应

一般的请求消息如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.0 200 OK <!-- 响应消息行 -->
Server: Tiny Web Server <!-- 响应消息头 -->
Content-length: 120
Content-type: text/html

<html> <!-- 响应正文 -->
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
Dave O'Hallaron
</body>
</html>
  • 响应消息行:第一行响应消息为响应消息行
    • 例如:HTTP/1.0 200 OK
    • HTTP/1.0 为协议版本
    • 200 为响应状态码,常用的响应状态码有40余种,这里我们仅列出几种,详细请看:
      • 200:一切正常
      • 302/307:临时重定向
      • 304:未修改,客户端可以从缓存中读取数据,无需从服务器读取
      • 404:服务器上不存在客户端所请求的资源
      • 500:服务器内部错误
    • OK 为状态码描述
  • 响应消息头:
    • Location:指示新的资源的位置通常和302/307一起使用,完成请求重定向
    • Server:指示服务器的类型
    • Content-Encoding:服务器发送的数据采用的编码类型
    • Content-Length:告诉浏览器正文的长度
    • Content-Language:服务发送的文本的语言
    • Content-Type:服务器发送的内容的MIME类型
    • Last-Modified:文件的最后修改时间
    • Refresh:指示客户端刷新频率,单位是秒
    • Content-Disposition:指示客户端下载文件
    • Set-Cookie:服务器端发送的Cookie
    • Expires:-1
    • Cache-Control:no-cache (1.1)
    • Pragma:no-cache (1.0) 表示告诉客户端不要使用缓存
    • Connection:close/Keep-Alive
    • Date:请求时间
  • 响应正文:即网页的源代码(F12可查看)

网络编程结构体

通用结构体:struct sockaddr,16个字节

1
2
3
4
5
/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr {
uint16_t sa_family; /* Protocol family */
char sa_data[14]; /* Address data */
};

struct sockaddr 是一个通用地址结构,这是为了统一地址结构的表示方法,统一接口函数,使不同的地址结构可以被bind() , connect() 等函数调用

sockaddr的缺陷:sa_data 把目标地址和端口信息混在一起了

通用结构体:struct sockaddr_storage,128个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Structure large enough to hold any socket address 
(with the historical exception of AF_UNIX). 128 bytes reserved. */

#if ULONG_MAX > 0xffffffff
# define __ss_aligntype __uint64_t
#else
# define __ss_aligntype __uint32_t
#endif
#define _SS_SIZE 128
#define _SS_PADSIZE (_SS_SIZE - (2 * sizeof (__ss_aligntype)))

struct sockaddr_storage
{
uint16_t ss_family; /* Address family */
__ss_aligntype __ss_align; /* Force desired alignment. */
char __ss_padding[_SS_PADSIZE];
};

struct sockaddr_storage 被设计为同时适合 struct sockaddr_in 和 struct sockaddr_in6

为了避免试图知道要使用的IP版本,可以使用 struct sockaddr_storage,该版本可以保存其中任何一个,后将通过 connect(),bind() 等函数将其类型转换为 struct sockaddr 并以这种方式进行访问

IPv4:struct sockaddr_in,16个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* IP socket address structure */
struct sockaddr_in {
uint16_t sin_family; /* Protocol family (always AF_INET) */
uint16_t sin_port; /* 16位的端口号 */
struct in_addr sin_addr; /* 32位的IP地址 */
unsigned char sin_zero[sizeof (struct sockaddr) -
sizeof (sa_family_t) -
sizeof (in_port_t) -
sizeof (struct in_addr)]; // sin_zero[8]
/* Pad to sizeof(struct sockaddr) */
};

typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr; /* IPv4 address */
};

该结构体解决了 sockaddr 的缺陷,把 port 和 addr 分开储存在两个变量中

IPv6:struct sockaddr_in6,28个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct sockaddr_in6 {
uint16_t sin6_family; /* AF_INET6 */
uint16_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
struct in6_addr {
union {
uint8_t u6_addr8[16];
uint16_t u6_addr16[8];
uint32_t u6_addr32[4];
} in6_u;

#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};

网络编程中的信号

进程组

进程组就是一系列相互关联的进程集合,系统中的每一个进程也必须从属于某一个进程组,每个进程组中都会有一个唯一的 ID(process group id),简称 PGID,PGID 一般等同于进程组的创建进程的 Process ID,而这个进进程一般也会被称为进程组先导(process group leader),同一进程组中除了进程组先导外的其他进程都是其子进程

进程组的存在,方便了系统对多个相关进程执行某些统一的操作,例如:我们可以一次性发送一个信号量给同一进程组中的所有进程

会话

会话(session)是一个若干进程组的集合,同样的,系统中每一个进程组也都必须从属于某一个会话

  • 一个会话只拥有最多一个控制终端(也可以没有),该终端为会话中所有进程组中的进程所共用
  • 一个会话中前台进程组只会有一个,只有其中的进程才可以和控制终端进行交互,除了前台进程组外的进程组,都是后台进程组

和进程组先导类似,会话中也有会话先导(session leader)的概念,用来表示建立起到控制终端连接的进程,在拥有控制终端的会话中,session leader 也被称为控制进程(controlling process),一般来说控制进程也就是登入系统的 shell 进程(login shell)

带外数据

带外数据用于迅速告知对方本端发生的重要的事件,它比普通的数据(带内数据)拥有更高的优先级, 不论发送缓冲区中是否有排队等待发送的数据,它总是被立即发送 ,带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中,

​ // 实际应用中,带外数据是使用很少见,有 telnet 和 ftp 等远程非活跃程序

UDP没有没有实现带外数据传输,TCP也没有真正的带外数据,不过TCP利用头部的紧急指针标志和紧急指针,为应用程序提供了一种紧急方式,含义和带外数据类似,TCP的紧急方式利用传输普通数据的连接来传输紧急数据

SIGHUP信号(关闭进程)

SIGHUP 信号在 用户终端连接(正常或非正常)结束 时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业(任务),这时它们与控制终端不再关联

系统对SIGHUP信号的默认处理是:终止收到该信号的进程 ,所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出

SIGHUP会在以下3种情况下被发送给相应的进程:

  • 终端关闭时,该信号被发送到 session 首进程以及作为 job 提交的进程(即用 & 符号提交的进程)
  • session 首进程退出时,该信号被发送到该 session 中的前台进程组中的每一个进程
  • 若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGSTOP或SIGTSTP信号),该信号会被发送到该进程组中的每一个进程

例如:在我们登录Linux时,系统会分配给登录用户一个终端(Session),在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session,当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号,这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止

​ // 晦涩难懂,需要在实例中理解分析

SIGPIPE信号(告知中断)

往一个写端关闭的管道或 socket 连接中连续写入数据时会引发 SIGPIPE 信号(引发 SIGPIPE 信号的写操作将设置 errno 为EPIPE)

在TCP通信中,当通信的双方中的一方close一个连接时,若另一方接着发数据,根据TCP协议的规定,会收到一个RST(Reset the connection)响应报文,若再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不能再写入数据

  • 即使断开还可以进行一次通信,第二次发送数据时才触发SIGPIPE
  • 可以用相应的 handle 进行处理SIGPIPE,完成想要的操作

服务器代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void handle(int sig) // 处理程序
{
printf("SIGPIPE : %d\n",sig);
}

void mysendmsg(int fd)
{

// 写入第一条消息
char* msg1 = "first msg";
int n = write(fd, msg1, strlen(msg1));

if(n > 0) //成功写入第一条消息,server接收到client发送的RST
{
printf("success write %d bytes\n", n);
}

// 写入第二条消息,触发SIGPIPE
char* msg2 = "second msg";
n = write(fd, msg2, strlen(msg2));
if(n < 0)
{
printf("write error: %s\n", strerror(errno));
}
}
int main()
{
signal(SIGPIPE , handle); //注册信号捕捉函数

struct sockaddr_in server_addr;

bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);

int listenfd = socket(AF_INET , SOCK_STREAM , 0);

bind(listenfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
/* 把一个本地协议地址赋予一个套接字'listenfd' */

listen(listenfd, 128);
/* 让'listenfd'变为被动监听状态 */

int fd = accept(listenfd, NULL, NULL);
/* 等待来自客户端的连接请求到达侦听描述符listenfd,返回一个已连接描述符 */
if(fd < 0)
{
perror("accept");
exit(1);
}

mysendmsg(fd);

return 0;
}

客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int main()
{
char buf[MAX] = {'0'};
int sockfd;
int n;
socklen_t slen;
slen = sizeof(struct sockaddr);
struct sockaddr_in seraddr;

bzero(&seraddr,sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(PORT);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);

//socket()
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket");
exit(-1);
}
//connect()
if(connect(sockfd,(struct sockaddr *)&seraddr,slen) == -1)
{
/* 试图与“套接字地址为seraddr的服务器”建立一个因特网连接 */
perror("connect");
exit(-1);
}

int ret = shutdown(sockfd , SHUT_RDWR);
/* 禁止在sockfd上进行数据的接收与发送(关闭sockfd的读写功能) */
if(ret < 0)
{
perror("shutdown perror");
}

return 0;
}

结果:

依次触发:write_msg1,handle,write_msg2

此外,因为SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为写操作的错误而导致程序退出,尤其是作为服务器程序来说就更恶劣了。所以我们应该对这种信号加以处理,在这里,介绍两种处理SIGPIPE信号的方式:

  1. 给SIGPIPE设置SIG_IGN信号处理函数,忽略该信号:
1
signal(SIGPIPE, SIG_IGN);

前文说过,引发SIGPIPE信号的写操作将设置errno为EPIPE,所以,第二次往关闭的socket中写入数据时,会返回-1,同时errno置为EPIPE,这样,便能知道对端已经关闭,然后进行相应处理,而不会导致整个进程退出

  1. 使用send函数的MSG_NOSIGNAL标志来禁止写操作触发SIGPIPE信号:
1
send(sockfd , buf , size , MSG_NOSIGNAL);

同样,我们可以根据send函数反馈的errno来判断socket的读端是否已经关闭

此外,我们也可以通过IO复用函数来检测管道和socket连接的读端是否已经关闭,以POLL为例,当socket连接被对方关闭时,socket上的POLLRDHUP事件将被触发

SIGURG信号

内核通知应用程序带外数据到达的方式有两种:

  • 一种就是利用IO复用技术的系统调用(如select)在接受到带外数据时将返回,并向应用程序报告socket上的异常事件
  • 另一种方法就是使用SIGURG信号

参考:网络编程的三个重要信号

套接字接口

套接字接口(socket interface)是一组函数,它们和 Unix I/O 函数结合起来,用以创建网络应用

大多数现代系统上都实现套接字接口,包括所有的 Unix 变种、Windows 和 Macintosh 系统

​ // 从 Linux 内核的角度来看,一个套接字就是 通信的一个端点 ,从 Linux 程序的角度来看,套接字就是一个 有相应描述符的打开文件

因特网的套接字地址存放在所示的类型为 sockaddr_in 的 16 字节结构中(IP 地址和端口号总是以网络字节顺序(大端法)存放的)

下面将介绍套接字接口中的部分函数:

1
2
3
4
int socket(int domain, int type, int protocol);
// domain:即协议域,又称为协议族(family)
// type:指定socket类型
// protocol:指定协议

​ // 协议族决定了 socket 的地址类型,在通信中必须采用对应的地址

socket 函数 用于来返回一个 套接字描述符 (clientfd)

  • 套接字描述符:用来标定系统为当前的进程划分的一块缓冲空间,类似于文件描述符
  • 文件描述符:是内核为了高效管理已被打开的文件所创建的索引,用于指代被打开的文件,对文件所有 I/O 操作相关的系统调用都需要通过文件描述符(open的返回值fd)
1
2
3
4
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// sockfd:需要绑定的socket
// *addr:存放了服务端用于通信的地址和端口
// addrlen: sizeof(sockaddr_in)

bind 函数 告诉内核将 addr 中的服务器套接字地址和套接字描述符 sockfd 联系起来

​ // bind函数把一个本地协议地址赋予一个套接字

1
2
3
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
// clientfd:套接字描述符的一种
// addrlen:sizeof(sockaddr_in)

connect 函数 试图与 “套接字地址为 addr 的服务器” 建立一个因特网连接

如果成功,clientfd 描述符现在就准备好可以读写了(最好用 getaddrinfo 来为 connect 提供参数)

1
2
3
int listen(int sockfd, int backlog);
// sockfd:需要绑定的socket
// backlog:暗示了内核在开始拒绝连接请求之前,队列中要排队的未完成的连接请求的数量

listen 函数 将 sockfd 从一个 主动套接字 转化为一个 监听套接字 (listening socket),该套接字可以接受来自客户端的连接请求

1
2
3
4
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
// listenfd:服务器的socket描述符
// *addr:指向struct sockaddr *的指针
// *addrlen:协议地址的长度

accept 函数 等待来自客户端的连接请求到达侦听描述符 listenfd,然后在 addr 中填写客户端的套接字地址,并返回一个 已连接描述符

一个服务器通常通常仅仅只创建一个监听socket描述字,内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭

1
2
3
4
5
6
7
8
9
int getaddrinfo(const char *host, const char *service,
const struct addrinfo *hints,
struct addrinfo **result);
// host & service:套接字地址的两个组成部分
// 可选的参数 hints 是一个 addrinfo 结构,它提供对 getaddrinfo 返回的套接字地址列表的更好的控制
// getaddrinfo 返回 result,result 一个指向 addrinfo 结构的链表

void freeaddrinfo(struct addrinfo *result);
const char *gai_strerror(int errcode); // 返回:错误消息

getaddrinfo 函数 将主机名、主机地址、服务名和端口号的字符串表示转化成套接字地址结构,它是已弃用的 gethostbyname 和 getservbyname 函数的新的替代品

在客户端调用了 getaddrinfo 之后,会遍历这个列表,依次尝试每个套接字地址,直到调用 socket 和 connect 成功,建立起连接,类似地,服务器会尝试遍历列表中的每个套接字地址,直到调用 socket 和 bind 成功,描述符会被绑定到一个合法的套接字地址,

  • 为了避免内存泄漏,应用程序必须在最后调用 freeaddrinfo,释放该链表
  • 如果 getaddrinfo 返回非零的错误代码,应用程序可以调用 gai_streeror,将该代码转换成消息字符串
1
2
3
4
5
6
7
8
9
10
struct addrinfo {
int ai_flags; /* Hints argument flags */
int ai_family; /* First arg to socket function */
int ai_socktype; /* Second arg to socket function */
int ai_protocol; /* Third arg to socket function */
char *ai_canonname; /* Canonical hostname */
size_t ai_addrlen; /* Size of ai_addr struct */
struct sockaddr *ai_addr; /* Ptr to socket address structure */
struct addrinfo *ai_next; /* Ptr to next item in linked list */
};
1
2
3
4
5
6
7
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *service, size_t servlen, int flags);
// *sa:指向大小为 salen 字节的套接字地址结构
// *host 指向大小为 hostlen 字节的缓冲区
// *service 指向大小为 servlen 字节的缓冲区
// 参数 flags 是一个位掩码,能够修改默认的行为

getnameinfo 函数 和 getaddrinfo 是相反的,将一个套接字地址结构转换成相应的主机和服务名字符串,它是已弃用的 gethostbyaddr 和 getservbyport 函数的新的替代品

1
2
3
int open_clientfd(char *hostname, char *port);
// *hostname:服务器运行的地址
// *port:指向端口

客户端调用 open_clientfd 建立与服务器的连接

open_clientfd 函数 建立与服务器的连接,该服务器运行在主机 hostname 上,并在端口号 port 上监听连接请求

1
2
int open_listenfd(char *port);
// *port:指向端口号

open_listenfd 函数 打开和返回一个监听描述符,这个描述符准备好在端口 port_h 接收连接请求

echo 客户端案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "csapp.h"

int main(int argc, char **argv)
{
int clientfd;
char *host, *port, buf[MAXLINE];
rio_t rio;

if (argc != 3) {
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
exit(0);
}
host = argv[1];
port = argv[2];

clientfd = Open_clientfd(host, port);
/* 建立与服务器的连接,该服务器运行在主机hostname上,在端口号port上监听连接请求 */
Rio_readinitb(&rio, clientfd);
/* 将"描述符clientfd"和"地址在rio处的一个的读缓冲区"联系起来 */

while (Fgets(buf, MAXLINE, stdin) != NULL) {
/* 从标准输入中读取数据到buf */
Rio_writen(clientfd, buf, strlen(buf));
/* 从位置buf传送strlen(buf)个字节到描述符clientfd */
Rio_readlineb(&rio, buf, MAXLINE);
/* 从&rio读出下一个文本行(包括结尾的换行符,数最多maxlen-1个字节),将它复制到内存位置buf,并且用NULL字符来结束这个文本行 */
Fputs(buf, stdout);
/* 从"标准输出"输出buf */
}
Close(clientfd);
exit(0);
}

在和服务器建立连接之后,客户端进入一个循环,反复从标准输入读取文本行,发送文本行给服务器,从服务器读取回送的行,并输出结果到标准输出

当 fgets 在标准输入上遇到 EOF 时,或者因为用户在键盘上键入 Ctrl+D,或者因为在一个重定向的输入文件中用尽了所有的文本行时,循环就终止

echo 服务器案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "csapp.h"

void echo(int connfd);

int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr; /* Enough space for any address */
char client_hostname[MAXLINE], client_port[MAXLINE];

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

listenfd = Open_listenfd(argv[1]);
/* 打开和返回一个监听描述符,这个描述符准备好在端口argv[1]接收连接请求 */
while (1) {
clientlen = sizeof(struct sockaddr_storage);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
/* 等待来自客户端的连接请求到达侦听描述符listenfd,然后在clientaddr中填写客户端的套接字地址,并返回一个已连接描述符(connfd) */
Getnameinfo((SA *) &clientaddr, clientlen, client_hostname, MAXLINE,
client_port, MAXLINE, 0);
/* 将一个套接字地址结构转换成相应的主机和服务名字符串 */
printf("Connected to (%s, %s)\n", client_hostname, client_port);
/* 打印客户端信息 */
echo(connfd);
/* 调用echo 函数为这些客户端服务 */
Close(connfd);
}
exit(0);
}

void echo(int connfd)
{
size_t n;
char buf[MAXLINE];
rio_t rio;

Rio_readinitb(&rio, connfd);
/* 将"描述符clientfd"和"地址在rio处的一个的读缓冲区"联系起来 */
while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
/* 从&rio读出下一个文本行(包括结尾的换行符,数最多maxlen-1个字节),将它复制到内存位置buf,并且用NULL字符来结束这个文本行 */
printf("server received %d bytes\n", (int)n);
Rio_writen(connfd, buf, n);
/* 打印buf */
}
}

在打开监听描述符后,它进入一个无限循环,每次循环都等待一个来自客户端的连接请求,输出已连接客户端的域名和 IP 地址,并调用 echo 函数为这些客户端服务,在 echo 程序返回后,主程序关闭已连接描述符,一旦客户端和服务器关闭了它们各自的描述符,连接也就终止了

echo函数将反复读写文本行,直到rio_readlineb函数遇到EOF

参考:Socket原理讲解

多线程编程

多线程是多任务处理的一种特殊形式,多任务处理允许让电脑 同时 运行两个或两个以上的程序,一般情况下,两种类型的多任务处理:基于进程 和 基于线程

  • 基于进程的多任务处理是程序的并发执行
  • 基于线程的多任务处理是同一程序的片段的并发执行

多线程程序包含可以同时运行的两个或多个部分,这样的程序中的每个部分称为一个线程,每个线程定义了一个单独的执行路径

C语言中有专门控制线程的函数:

创建新线程

1
pthread_create (thread, attr, start_routine, arg);
  • thread:指向线程标识符的指针
  • attr:一个不透明的属性对象,可以被用来设置线程属性,您可以指定线程属性对象,也可以使用默认值 NULL
  • start_routine:线程运行函数起始地址,一旦线程被创建就会执行(通常设置为某个函数)
  • arg:运行函数的参数,它必须通过把引用作为指针强制转换为 void 类型进行传递,如果没有传递参数,则使用 NULL

pthread_create 创建一个新的线程,并让它可执行目标函数

终止线程

1
pthread_exit (status);

显式地退出一个线程,通常情况下,pthread_exit() 函数是在线程完成工作后无需继续存在时被调用,如果 main() 是在它所创建的线程之前结束,并通过 pthread_exit() 退出,那么其他线程将继续执行,否则,它们将在 main() 结束时自动被终止

连接和分离线程

1
2
pthread_join (threadid, status) 
pthread_detach (threadid)
  • pthread_join() 阻塞等待线程退出(直到指定的 threadid 线程终止为止),获取线程退出状态
  • pthread_detach() 表示主线程与子线程(threadid)分离,两者相互不干涉,子线程结束同时子线程的资源自动回收

当创建一个线程时,它的某个属性会定义它是否是可连接的(joinable)或可分离的(detached),只有创建时定义为可连接的线程才可以被连接,如果线程创建时被定义为可分离的,则它永远也不能被连,pthread_join() 函数来等待线程的完成

​ // pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a, 在使用pthread_create创建线程时,在编译中要加-lpthread参数:

1
gcc test.c -lpthread -o test

案例:pthread_join

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

typedef struct {
char ch;
int var;
char str[64];
}th_t;

void* thrd_func(void* arg) {
th_t* retvar = (th_t*)arg;

retvar->ch = 'm';
retvar->var = 200;
strcpy(retvar->str, "my thread");
puts("success!!!");

// 线程的退出可以将return换成pthread_exit
pthread_exit((void*)retvar);
}

int main(void) {

pthread_t tid;
int ret;
th_t* retval = NULL;
retval = malloc(sizeof(th_t));

ret = pthread_create(&tid, NULL, thrd_func, (void*)retval);
if (ret != 0) {
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1); // 整个进程退出
}

/* 阻塞并获取返回值(状态值) */
pthread_join(tid, (void**)&retval);
/* 接下来就会演示把pthread_join注释后的结果 */
printf("ch = %c, var = %d, str = %s\n", retval->ch, retval->var, retval->str);
if (retval != NULL) {
free(retval);
retval = NULL;
}
/* 注意:这里只是将主线程退出,若还有其它子线程在运行则仍会运行
而return将会使整个进程结束 */

pthread_exit((void*)1);
}
1
2
3
4
➜  [/home/ywhkkx/桌面] gcc test.c -lpthread -o test
➜ [/home/ywhkkx/桌面] ./test
success!!!
ch = m, var = 200, str = my thread

把“pthread_join(tid, (void **)&retval)”注释掉以后:

1
2
3
4
➜  [/home/ywhkkx/桌面] gcc test.c -lpthread -o test
➜ [/home/ywhkkx/桌面] ./test
ch = , var = 0, str =
success!!!

发现主线程和副线程的执行次序改变了(主副线程同时执行,但是主线程先操作“retval”变量)

添加了“pthread_join”后,主线程就会被阻塞,直到标识符“tid”代表的副线程终止后,主线程才开始执行(需要副线程先给“retval”变量赋值后,主线程才可以打印出来)

案例:pthread_detach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>


void* thrd_func(void* arg)
{
printf("i am detach.\n");
pthread_exit((void*)77);
}

int main(void)
{
pthread_t tid;
int ret;

ret = pthread_create(&tid, NULL, thrd_func, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1);
}

/* 实现线程分离,不再受主线程管理,由系统接任,线程结束后,其退出状态不由其他线程获取,而直接自己自动释放 */
ret = pthread_detach(tid);

if (ret != 0) {
fprintf(stderr, "pthread_detach error:%s\n", strerror(ret));
exit(1);
}

sleep(1);
printf("main pid=%d, tid=%lu\n", getpid(), pthread_self());
pthread_exit((void*)0);
}
1
2
3
4
➜  [/home/ywhkkx/桌面] gcc test.c -lpthread -o test
➜ [/home/ywhkkx/桌面] ./test
i am detach.
main pid=3830, tid=140138689746752

线程可以被置为 detach 状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态,不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误

参考:C语言多线程操作

实验一:实现顺序web代理

实现一个顺序执行的代理,它可以处理GET方法并转发,对于其他方法可以不实现

命令行调用 “./proxy < port >” 来启动代理服务器,其中 port 可以通过实验包中的工具 port-for-user 来获取

测试服务器

运行该服务器,指定一个端口,必须是1024–49151之间的端口,其余端口不能使用

1
➜  [/home/ywhkkx/proxylab-handout/tiny] ./tiny 1444 

在运行了TINY 服务器的基础上,打开另一个terminal,在linux shell输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜  [/home/ywhkkx/proxylab-handout/tiny] telnet localhost 1444
telnet localhost 1444
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /home.html HTTP/1.0

HTTP/1.0 200 OK /* 200表示成功获取 */
Server: Tiny Web Server
Content-length: 120
Content-type: text/html

<html> // 获取的内容
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
Dave O'Hallaron
</body>
</html>
Connection closed by foreign host.

此时服务端的内容:

1
2
3
➜  [/home/ywhkkx/proxylab-handout/tiny] ./tiny 1444
Accepted connection from (ip6-localhost, 47590) // 打印了用户信息
GET /home.html HTTP/1.0

理解服务器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#include "csapp.h"

void doit(int fd);
void read_requesthdrs(rio_t* rp);
int parse_uri(char* uri, char* filename, char* cgiargs);
void serve_static(int fd, char* filename, int filesize);
void get_filetype(char* filename, char* filetype);
void serve_dynamic(int fd, char* filename, char* cgiargs);
void clienterror(int fd, char* cause, char* errnum,
char* shortmsg, char* longmsg);

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]);
/* 打开和返回一个监听描述符,这个描述符准备好在端口 port_h 接收连接请求 */
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA*)&clientaddr, &clientlen);
/* 等待来自客户端的连接请求到达侦听描述符listenfd,然后在clientaddr中填写客户端的套接字地址,并返回一个已连接描述符 */
Getnameinfo((SA*)&clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
/* 将一个套接字地址结构转换成相应的主机和服务名字符串 */
printf("Accepted connection from (%s, %s)\n", hostname, port);
/* 调用打印信息 */
doit(connfd);
/* 进行服务 */
Close(connfd);
/* 关闭连接 */
}
}

void doit(int fd)
{
int is_static;
struct stat sbuf; /* 这个结构体用来描述一个linux系统文件系统中的文件属性的结构 */
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio;

Rio_readinitb(&rio, fd);
/* 将"描述符fd"和"地址在rio处的一个的读缓冲区"联系起来 */
if (!Rio_readlineb(&rio, buf, MAXLINE))
return;
/* 从&rio读出下一个文本行(包括结尾的换行符,数最多MAXLINE-1个字节),将它复制到内存位置buf,并且用NULL字符来结束这个文本行 */
printf("%s", buf);
/* 打印读取的数据 */
sscanf(buf, "%s %s %s", method, uri, version);
/* 输入'方法','标识','版本' */
if (strcasecmp(method, "GET")) {
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio);
/* 忽略请求报头中的信息 */

is_static = parse_uri(uri, filename, cgiargs);
/* 解析uri(标识),得文件名存入filename中,参数存入cgiargs中 */
if (stat(filename, &sbuf) < 0) {
/* 将文件filename中的各个元数据填写进sbuf中,如果找不到文件返回0 */
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
/* 向客户端返回错误信息 */
return;
}

if (is_static) {
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
/* 此文件为普通文件? 有读取权限?(-r)(若没有则return) */
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read the file");
return;
}
serve_static(fd, filename, sbuf.st_size);
/* 提供静态服务(获取文件) */
}
else {
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
/* 此文件为普通文件? 有执行权限?(-x)(若没有则return) */
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs);
/* 提供动态服务(指令控制) */
}
}

void read_requesthdrs(rio_t* rp)
{
/* 读这些请求报头,直到空行,然后返回,跳过这些请求报头的 */
char buf[MAXLINE];

Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
while (strcmp(buf, "\r\n")) {
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
}
return;
}

int parse_uri(char* uri, char* filename, char* cgiargs)
{
/* 解析uri,得文件名存入filename中,参数存入cgiargs中 */
char* ptr;

/* 据uri中是否含有cgi-bin来判断请求的是静态内容还是动态内容 */
if (!strstr(uri, "cgi-bin")) {
/* 静态内容 */
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
if (uri[strlen(uri) - 1] == '/')
strcat(filename, "home.html");
return 1;
}
else {
/* 动态内容 */
ptr = index(uri, '?');
if (ptr) {
strcpy(cgiargs, ptr + 1);
*ptr = '\0';
}
else
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
return 0;
}
}

void serve_static(int fd, char* filename, int filesize)
{
/* 打开文件名为filename的文件,把它映射到一个虚拟存储器空间,将文件的前filesize字节映射到从地址srcp开始的虚拟存储区域 */
int srcfd;
char* srcp, filetype[MAXLINE], buf[MAXBUF];

get_filetype(filename, filetype);
/* 从*filename中获取filetype */
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n", filesize);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: %s\r\n\r\n", filetype);
Rio_writen(fd, buf, strlen(buf));

srcfd = Open(filename, O_RDONLY, 0);
/* 打开目标文件(只读方式) */
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
/* 让系统自动选定地址,把filesize字节的数据映射到内存,映射区域可被读取,对映射区域的写入操作会产生一个映射文件的复制(私人的“写入时复制”),将要映射到内存中的文件描述符为‘srcfd’ */
Close(srcfd);
Rio_writen(fd, srcp, filesize);
/* 把内存中的‘srcp’读到fd */
Munmap(srcp, filesize);
/* 解除内存映射 */
}

void get_filetype(char* filename, char* filetype)
{
/* 根据filename获取文件格式 */
if (strstr(filename, ".html"))
strcpy(filetype, "text/html");
else if (strstr(filename, ".gif"))
strcpy(filetype, "image/gif");
else if (strstr(filename, ".png"))
strcpy(filetype, "image/png");
else if (strstr(filename, ".jpg"))
strcpy(filetype, "image/jpeg");
else
strcpy(filetype, "text/plain");
}

void serve_dynamic(int fd, char* filename, char* cgiargs)
{
/* Tiny在发送了响应的第一部分后,通过派生一个子进程并在子进程的上下文中运行一个cgi程序(可执行文件),来提供各种类型的动态内容 */
char buf[MAXLINE], * emptylist[] = { NULL };

sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));

if (Fork() == 0) {
/* 在父进程中,fork返回子进程的进程ID,在子进程中,fork返回0 */
setenv("QUERY_STRING", cgiargs, 1); /* 设置QUERY_STRING环境变量 */
Dup2(fd, STDOUT_FILENO); /* 重定向它的标准输出到已连接描述符 */
Execve(filename, emptylist, environ); /* 加载运行cgi程序 */
}
Wait(NULL); /* 允许线程阻止其自身执行(父进程等待) */
}

void clienterror(int fd, char* cause, char* errnum,
char* shortmsg, char* longmsg)
{
/* 向客户端返回错误信息 */
char buf[MAXLINE];

/* 打印HTTP响应头 */
sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: text/html\r\n\r\n");
Rio_writen(fd, buf, strlen(buf));

/* 打印HTTP响应正文 */
sprintf(buf, "<html><title>Tiny Error</title>");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "<body bgcolor=""ffffff"">\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "%s: %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "<p>%s: %s\r\n", longmsg, cause);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "<hr><em>The Tiny Web server</em>\r\n");
Rio_writen(fd, buf, strlen(buf));
}

Tiny是一个迭代服务器,监听在命令行中确定的端口上的连接请求,在通过 open_listenedfd 函数打开一个监听套接字以后,Tiny执行典型的无限服务循环,反复地接受一个连接(accept)请求,执行事务(doit),最后关闭连接描述符(close)

编写代理

网络代理是一个在网络浏览器和终端服务器之间充当中间人的程序,而不是直接联系终端服务器以获取网页,浏览器会联系代理,代理会转发请求发送到终端服务器

用户在代理中输入指令,代理会解析指令并发送到服务器,同时它也会接收服务器上的反馈,并返回给用户

先抄 TINY 的框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#include <stdio.h>
#include "csapp.h"
/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400

/* 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";
static const char* conn_hdr = "Connection: close\r\n";
static const char* prox_hdr = "Proxy-Connection: close\r\n";

void doit(int fd);
void clienterror(int fd, char* cause, char* errnum,
char* shortmsg, char* longmsg);
void parse_uri(char* uri, char* hostname, char* path, int* port);
void* thread(void* vargp);

int main(int argc, char** argv)
{
int listenfd, connfd;
pthread_t tid;
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);
}
signal(SIGPIPE, SIG_IGN); /* 新添:给SIGPIPE设置SIG_IGN信号处理函数,忽略该信号 */

listenfd = Open_listenfd(argv[1]);
/* 打开和返回一个监听描述符,这个描述符准备好在端口port_h接收连接请求 */
while (1) {
printf("listening..\n");
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA*)&clientaddr, &clientlen);
/* 等待来自客户端的连接请求到达侦听描述符listenfd,然后在clientaddr中填写客户端的套接字地址,并返回一个已连接描述符 */

Getnameinfo((SA*)&clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
/* 获取用户信息 */
printf("Accepted connection from (%s, %s)\n", hostname, port);
/* 打印用户信息 */
doit(connfd);
/* 执行服务程序 */
Close(connfd);
}
}

void doit(int client_fd)
{
int endserver_fd;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
rio_t from_client, to_endserver;
char hostname[MAXLINE], path[MAXLINE];
int port;

Rio_readinitb(&from_client, client_fd);
/* 将"描述符client_fd"和"地址在from_client处的一个的读缓冲区"联系起来 */
if (!Rio_readlineb(&from_client, buf, MAXLINE))
return;
/* 从&from_client读出下一个文本行(包括结尾的换行符,数最多MAXLINE-1个字节),将它复制到内存位置buf,并且用NULL字符来结束这个文本行 */
sscanf(buf, "%s %s %s", method, uri, version);
if (strcasecmp(method, "GET")) {
clienterror(client_fd, method, "501", "Not Implemented",
"Proxy Server does not implement this method");
return;
}
parse_uri(uri, hostname, path, &port); /* 改写:这个函数代码不同了 */
/* 解析uri,得文件名存入filename中,参数存入cgiargs中 */

/*------------------------------------------------------------*/
/* 后面的内容TINY完全不同了(clienterror函数除外) */
/*------------------------------------------------------------*/

char port_str[10];
sprintf(port_str, "%d", port);
endserver_fd = Open_clientfd(hostname, port_str);
/* 将socket和connect封装成Open_clientfd,客户端可以用它来和服务器建立连接 */
if (endserver_fd < 0) {
printf("connection failed\n");
return;
}

Rio_readinitb(&to_endserver, endserver_fd);
/* 将"描述符endserver_fd"和"地址在to_endserver处的一个的读缓冲区"联系起来 */
char newreq[MAXLINE];
sprintf(newreq, "GET %s HTTP/1.0\r\n", path);
build_requesthdrs(&from_client, newreq, hostname); /* 新添:这是一个全新的函数 */
Rio_writen(endserver_fd, newreq, strlen(newreq));

int n;
while ((n = Rio_readlineb(&to_endserver, buf, MAXLINE))) {
printf("proxy received %d bytes,then send\n",n); /* 新添:提示语句 */
Rio_writen(client_fd, buf, n); /* 新增:向客户端发送请求的数据 */
}
/* 打印获取的数据 */
}

void clienterror(int fd, char* cause, char* errnum,
char* shortmsg, char* longmsg)
{
char buf[MAXLINE], body[MAXBUF];

sprintf(body, "<html><title>Proxy Error</title>");
sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
sprintf(body, "%s<hr><em>The Proxy Web server</em>\r\n", body);

sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: text/html\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
Rio_writen(fd, buf, strlen(buf));
Rio_writen(fd, body, strlen(body));
}

下面编写两个 改动&新增 的函数:(这两个函数需要按照 “实验要求” 进行编写)

实验要求 消息请求行&消息请求头为:

1
2
3
4
GET /hub/index.html HTTP/1.0
Host: www.cmu.edu
Connection: close
Proxy-Connection: close

而我们在 Proxy 写入的指令为:(其中“GET”和“HTTP/1.0”已经被处理)

1
GET http://www.cmu.edu/hub/index.html HTTP/1.0

所以这两个函数的作用,就是把 uri(统一资源标识符)转化为请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void parse_uri(char *uri,char *hostname,char *path,int *port) {
/* hostname: www.cmu.edu */
/* path: /hub/index.html */
*port = 80;
char* pos1 = strstr(uri,"//");
if (pos1 == NULL) {
pos1 = uri;
} else pos1 += 2;

char* pos2 = strstr(pos1,":");
if (pos2 != NULL) {
*pos2 = '\0';
strncpy(hostname,pos1,MAXLINE);
sscanf(pos2+1,"%d%s",port,path);
*pos2 = ':';
} else {
pos2 = strstr(pos1,"/");
if (pos2 == NULL) {
strncpy(hostname,pos1,MAXLINE);
strcpy(path,"");
return;
}
*pos2 = '\0';
strncpy(hostname,pos1,MAXLINE);
*pos2 = '/';
strncpy(path,pos2,MAXLINE);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void build_requesthdrs(rio_t *rp, char *newreq, char *hostname, char* port) {
/* 用于构建请求,把写入代理的指令进行包装,传输进服务器 */
char buf[MAXLINE];

while(Rio_readlineb(rp, buf, MAXLINE) > 0) {
if (!strcmp(buf, "\r\n")) break;
if (strstr(buf,"Host:") != NULL) continue;
if (strstr(buf,"User-Agent:") != NULL) continue;
if (strstr(buf,"Connection:") != NULL) continue;
if (strstr(buf,"Proxy-Connection:") != NULL) continue;

sprintf(newreq,"%s%s", newreq,buf);
}
sprintf(newreq, "%sHost: %s:%s\r\n",newreq, hostname,port);
sprintf(newreq, "%s%s%s%s", newreq, user_agent_hdr,conn_hdr,prox_hdr);
sprintf(newreq,"%s\r\n",newreq);
}

整体逻辑:

  • 代理的前半部分和服务器如出一辙,先作为服务器链接用户端,然后调用 doit 启动自身服务,在 doit 中为“用户端的描述符”绑定内存空间,并把用户端信息复制到该空间,接着输入指令装入其中,并调用 parse_uri 进行解析
  • 代理的后半部分又将作为客户端与目标服务器进行通信,先连接服务器并生成描述符,接着用类似的操作为“服务端的描述符”绑定内存空间,然后调用 build_requesthdrs 生成对应的服务请求,最后发送服务请求并打印数据

打分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
➜  [/home/ywhkkx/proxylab-handout] ./driver.sh 
*** Basic ***
Starting tiny on 4113
Starting proxy on 21528
1: home.html
Fetching ./tiny/home.html into ./.proxy using the proxy
Fetching ./tiny/home.html into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
2: csapp.c
Fetching ./tiny/csapp.c into ./.proxy using the proxy
Fetching ./tiny/csapp.c into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
3: tiny.c
Fetching ./tiny/tiny.c into ./.proxy using the proxy
Fetching ./tiny/tiny.c into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
4: godzilla.jpg
Fetching ./tiny/godzilla.jpg into ./.proxy using the proxy
Fetching ./tiny/godzilla.jpg into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
5: tiny
Fetching ./tiny/tiny into ./.proxy using the proxy
Fetching ./tiny/tiny into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
Killing tiny and proxy
basicScore: 40/40

实验二:实现多线程web代理

整体变化不大,直接挂代码了:(我会标记改动的地方)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#include <stdio.h>
#include "csapp.h"
/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400

/* 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";
static const char* conn_hdr = "Connection: close\r\n";
static const char* prox_hdr = "Proxy-Connection: close\r\n";


void doit(int fd);
void clienterror(int fd, char* cause, char* errnum,
char* shortmsg, char* longmsg);
void parse_uri(char* uri, char* hostname, char* path, int* port);
void* thread(void* vargp);


int main(int argc, char** argv)
{
int listenfd, * connfd; /* 不同:这里变指针了 */
pthread_t tid;
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);
}
signal(SIGPIPE, SIG_IGN);

listenfd = Open_listenfd(argv[1]);
while (1) {
printf("listening..\n");
clientlen = sizeof(clientaddr);
connfd = Malloc(sizeof(int));
/* 不同:这里改为在堆中分配内存空间(原来是栈,栈不稳定) */
*connfd = Accept(listenfd, (SA*)&clientaddr, &clientlen);

Getnameinfo((SA*)&clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
Pthread_create(&tid, NULL, thread, connfd);
/* 新添:Pthread_create用于创建子线程(从thread开始执行,参数为connfd) */
}
}

void* thread(void* vargp)
{
int connfd = *((int*)vargp);
Pthread_detach(pthread_self());
/* 设定线程分离,使其永远不会影响主线程 */
Free(vargp);
/* 因为执行了Pthread_detach,所以free的操作不会影响到主线程 */
doit(connfd);
/* 执行服务程序 */
Close(connfd);
return NULL;
}

void doit(int client_fd)
{
int endserver_fd;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
rio_t from_client, to_endserver;
char hostname[MAXLINE], path[MAXLINE];
int port;

Rio_readinitb(&from_client, client_fd);

if (!Rio_readlineb(&from_client, buf, MAXLINE))
return;
sscanf(buf, "%s %s %s", method, uri, version);
if (strcasecmp(method, "GET")) {
clienterror(client_fd, method, "501", "Not Implemented",
"Proxy Server does not implement this method");
return;
}

parse_uri(uri, hostname, path, &port);
char port_str[10];
sprintf(port_str, "%d", port);
endserver_fd = Open_clientfd(hostname, port_str);
if (endserver_fd < 0) {
printf("connection failed\n");
return;
}
Rio_readinitb(&to_endserver, endserver_fd);

char newreq[MAXLINE];
sprintf(newreq, "GET %s HTTP/1.0\r\n", path);
build_requesthdrs(&from_client, newreq, hostname);

Rio_writen(endserver_fd, newreq, strlen(newreq));
int n;
while ((n = Rio_readlineb(&to_endserver, buf, MAXLINE))) {
printf("proxy received %d bytes,then send\n",n);
Rio_writen(client_fd, buf, n);
}
}

void clienterror(int fd, char* cause, char* errnum,
char* shortmsg, char* longmsg)
{
char buf[MAXLINE], body[MAXBUF];

sprintf(body, "<html><title>Proxy Error</title>");
sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
sprintf(body, "%s<hr><em>The Proxy Web server</em>\r\n", body);

sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: text/html\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
Rio_writen(fd, buf, strlen(buf));
Rio_writen(fd, body, strlen(body));
}

void parse_uri(char* uri, char* hostname, char* path, int* port) {
*port = 80;
char* pos1 = strstr(uri, "//");
if (pos1 == NULL) {
pos1 = uri;
}
else pos1 += 2;

char* pos2 = strstr(pos1, ":");
if (pos2 != NULL) {
*pos2 = '\0';
strncpy(hostname, pos1, MAXLINE);
sscanf(pos2 + 1, "%d%s", port, path);
*pos2 = ':';
}
else {
pos2 = strstr(pos1, "/");
if (pos2 == NULL) {
strncpy(hostname, pos1, MAXLINE);
strcpy(path, "");
return;
}
*pos2 = '\0';
strncpy(hostname, pos1, MAXLINE);
*pos2 = '/';
strncpy(path, pos2, MAXLINE);
}
}

void build_requesthdrs(rio_t* rp, char* newreq, char* hostname, char* port) {
char buf[MAXLINE];

while (Rio_readlineb(rp, buf, MAXLINE) > 0) {
if (!strcmp(buf, "\r\n")) break;
if (strstr(buf, "Host:") != NULL) continue;
if (strstr(buf, "User-Agent:") != NULL) continue;
if (strstr(buf, "Connection:") != NULL) continue;
if (strstr(buf, "Proxy-Connection:") != NULL) continue;

sprintf(newreq, "%s%s", newreq, buf);
}
sprintf(newreq, "%sHost: %s:%s\r\n", newreq, hostname, port);
sprintf(newreq, "%s%s%s%s", newreq, user_agent_hdr, conn_hdr, prox_hdr);
sprintf(newreq, "%s\r\n", newreq);
}

打分:

1
2
3
4
5
6
7
8
9
10
11
*** Concurrency ***
Starting tiny on port 11348
Starting proxy on port 8656
Starting the blocking NOP server on port 8806
Trying to fetch a file from the blocking nop-server
Fetching ./tiny/home.html into ./.noproxy directly from Tiny
Fetching ./tiny/home.html into ./.proxy using the proxy
Checking whether the proxy fetch succeeded
Success: Was able to fetch tiny/home.html from the proxy.
Killing tiny, proxy, and nop-server
concurrencyScore: 15/15

实验三:缓存web对象

要求实现缓存客户端的请求

  • 其中最大的缓存块大小要小于 MAX_OBJECT_SIZE(102400)
  • 总的缓存大小 MAX_CACHE_SIZE(1049000)
  • cache需要牺牲缓存块时,运用LRU算法
  • 在实现过程中需要解决不同线程同时访问cache的问题

定义cache结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define TYPES 6
extern const int cache_block_size[];
extern const int cache_cnt[];

typedef struct cache_block{
char* url; /* 用于唯一确定所需要的cache */
char* data; /* cache中存放的数据 */
int datasize; /* 数据的长度 */
int64_t time; /* 当前用户的时间*/
pthread_rwlock_t rwlock; /* 锁的状态 */
} cache_block;

typedef struct cache_type{
cache_block *cacheobjs; /* 指向“存储客户端的请求消息”的内存空间 */
int size;
} cache_type;

cache_type caches[TYPES];

LRU(基于系统时间)+隐式+暴力获取(直接遍历所有cache)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
const int cache_block_size[] = {102, 1024, 5120 ,10240,25600, 102400};
const int cache_cnt[] = {40,20,20,10,12,5};
int64_t currentTimeMillis();

/* cache初始化 */
void init_cache()
{
int i = 0;
for (; i < TYPES; i++) {
/* 不同组的cache大小不同,组数也不同 */
caches[i].size = cache_cnt[i];
caches[i].cacheobjs
= (cache_block *)malloc(cache_cnt[i] * sizeof(cache_block));
cache_block *j = caches[i].cacheobjs;
int k;
for (k = 0; k < cache_cnt[i]; j++, k++) {
/* 把组中每一个元素的各个区域进行操作(重置为"0"或申请存储区) */
j->time = 0;
j->datasize = 0;
j->url = malloc(sizeof(char) * MAXLINE);
strcpy(j->url,"");
j->data = malloc(sizeof(char) * cache_block_size[i]);
memset(j->data,0,cache_block_size[i]);
pthread_rwlock_init(&j->rwlock,NULL);
/* 初始化一个读写锁 */
}
}
}

void free_cache() {
int i = 0;
for (; i < TYPES; i++) {
cache_block *j = caches[i].cacheobjs;
int k;
for (k = 0; k < cache_cnt[i]; j++, k++) {
free(j->url);
free(j->data);
pthread_rwlock_destroy(&j->rwlock);
/* 释放读写锁 */
}
free(caches[i].cacheobjs);
}
}

int read_cache(char *url,int fd){

int tar = 0, i = 0;
cache_type cur;
cache_block *p;
printf("read cache %s \n", url);
for (; tar < TYPES; tar++) {
/* 遍历cache中的信息,依次对比url */
cur = caches[tar];
p = cur.cacheobjs;
for(i=0;i < cur.size; i++,p++){
if(p->time != 0 && strcmp(url,p->url) == 0) break;
}
if (i < cur.size) break;
}

if(i == cur.size){
printf("read cache fail\n");
return 0;
}
pthread_rwlock_rdlock(&p->rwlock);
/* 读锁定读写锁 */
if(strcmp(url,p->url) != 0){
pthread_rwlock_unlock(&p->rwlock);
/* 解锁读写锁 */
return 0;
}
pthread_rwlock_unlock(&p->rwlock);
/* 解锁读写锁 */
if (!pthread_rwlock_trywrlock(&p->rwlock)) {
/* 非阻塞写锁定 */
p->time = currentTimeMillis();
/* 返回当前的计算机时间 */
pthread_rwlock_unlock(&p->rwlock);
/* 解锁读写锁 */
}
pthread_rwlock_rdlock(&p->rwlock);
/* 读锁定读写锁 */
Rio_writen(fd,p->data,p->datasize);
pthread_rwlock_unlock(&p->rwlock);
/* 解锁读写锁 */
printf("read cache successful\n");
return 1;
}

void write_cache(char *url, char *data, int len){
int tar = 0;
for (; tar < TYPES && len > cache_block_size[tar]; tar++) ;
printf("write cache %s %d\n", url, tar);
/* 寻找空cache块 */
cache_type cur = caches[tar];
cache_block *p = cur.cacheobjs, *pt;
int i;
for(i=0;i < cur.size;i++,p++){
if(p->time == 0){
break;
}
}
/* 实现LRU */
int64_t min = currentTimeMillis();
if(i == cur.size){
for(i=0,pt = cur.cacheobjs;i<cur.size;i++,pt++){
if(pt->time <= min){
min = pt->time;
p = pt;
}
}
}
pthread_rwlock_wrlock(&p->rwlock);
/* 写锁定读写锁 */
p->time = currentTimeMillis();
p->datasize = len;
memcpy(p->url,url,MAXLINE);
memcpy(p->data,data,len);
pthread_rwlock_unlock(&p->rwlock);
/* 解锁读写锁 */
printf("write Cache\n");
}

int64_t currentTimeMillis() {
struct timeval time;
gettimeofday(&time, NULL);
/* 把目前的时间通过time所指的结构返回,当地时区的信息则放到NULL所指的结构中 */
int64_t s1 = (int64_t)(time.tv_sec) * 1000;
int64_t s2 = (time.tv_usec / 1000);
return s1 + s2;
}

关于锁我没有深入学习,这里挂个博客:读写锁函数说明

最后需要把这些函数插入到原来的代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
#include <stdio.h>
#include "csapp.h"
/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400

/* 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";
static const char* conn_hdr = "Connection: close\r\n";
static const char* prox_hdr = "Proxy-Connection: close\r\n";

#define TYPES 6
extern const int cache_block_size[];
extern const int cache_cnt[];

typedef struct cache_block {
char* url;
char* data;
int datasize;
int64_t time;
pthread_rwlock_t rwlock;
} cache_block;

typedef struct cache_type {
cache_block* cacheobjs; /* 指向“存储客户端的请求消息”的内存空间 */
int size;
} cache_type;

cache_type caches[TYPES];


void doit(int fd);
void clienterror(int fd, char* cause, char* errnum,
char* shortmsg, char* longmsg);
void parse_uri(char* uri, char* hostname, char* path, int* port);
void* thread(void* vargp);
void init_cache();
int read_cache(char* url, int fd);
void write_cache(char* url, char* data, int len);
void free_cache();

const int cache_block_size[] = { 102, 1024, 5120 ,10240,25600, 102400 };
const int cache_cnt[] = { 40,20,20,10,12,5 };
int64_t currentTimeMillis();

void init_cache()
{
int i = 0;
for (; i < TYPES; i++) {
caches[i].size = cache_cnt[i];
caches[i].cacheobjs
= (cache_block*)malloc(cache_cnt[i] * sizeof(cache_block));
cache_block* j = caches[i].cacheobjs;
int k;
for (k = 0; k < cache_cnt[i]; j++, k++) {
j->time = 0;
j->datasize = 0;
j->url = malloc(sizeof(char) * MAXLINE);
strcpy(j->url, "");
j->data = malloc(sizeof(char) * cache_block_size[i]);
memset(j->data, 0, cache_block_size[i]);
pthread_rwlock_init(&j->rwlock, NULL);
}
}
}

void free_cache() {
int i = 0;
for (; i < TYPES; i++) {
cache_block* j = caches[i].cacheobjs;
int k;
for (k = 0; k < cache_cnt[i]; j++, k++) {
free(j->url);
free(j->data);
pthread_rwlock_destroy(&j->rwlock);
}
free(caches[i].cacheobjs);
}
}

int read_cache(char* url, int fd) {
int tar = 0, i = 0;
cache_type cur;
cache_block* p;
printf("read cache %s \n", url);
for (; tar < TYPES; tar++) {
cur = caches[tar];
p = cur.cacheobjs;
for (i = 0; i < cur.size; i++, p++) {
if (p->time != 0 && strcmp(url, p->url) == 0) break;
}
if (i < cur.size) break;
}

if (i == cur.size) {
printf("read cache fail\n");
return 0;
}
pthread_rwlock_rdlock(&p->rwlock);
if (strcmp(url, p->url) != 0) {
pthread_rwlock_unlock(&p->rwlock);
return 0;
}
pthread_rwlock_unlock(&p->rwlock);
if (!pthread_rwlock_trywrlock(&p->rwlock)) {
p->time = currentTimeMillis();
pthread_rwlock_unlock(&p->rwlock);
}
pthread_rwlock_rdlock(&p->rwlock);
Rio_writen(fd, p->data, p->datasize);
pthread_rwlock_unlock(&p->rwlock);
printf("read cache successful\n");
return 1;
}

void write_cache(char* url, char* data, int len) {
int tar = 0;
for (; tar < TYPES && len > cache_block_size[tar]; tar++);
printf("write cache %s %d\n", url, tar);
/* 寻找空cache块 */
cache_type cur = caches[tar];
cache_block* p = cur.cacheobjs, * pt;
int i;
for (i = 0; i < cur.size; i++, p++) {
if (p->time == 0) {
break;
}
}
/* 实现LRU */
int64_t min = currentTimeMillis();
if (i == cur.size) {
for (i = 0, pt = cur.cacheobjs; i < cur.size; i++, pt++) {
if (pt->time <= min) {
min = pt->time;
p = pt;
}
}
}
pthread_rwlock_wrlock(&p->rwlock);
p->time = currentTimeMillis();
p->datasize = len;
memcpy(p->url, url, MAXLINE);
memcpy(p->data, data, len);
pthread_rwlock_unlock(&p->rwlock);
printf("write Cache\n");
}

int64_t currentTimeMillis() {
struct timeval time;
gettimeofday(&time, NULL);
int64_t s1 = (int64_t)(time.tv_sec) * 1000;
int64_t s2 = (time.tv_usec / 1000);
return s1 + s2;
}


int main(int argc, char** argv)
{
int listenfd, * connfd;
pthread_t tid;
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);
}
signal(SIGPIPE, SIG_IGN);
init_cache(); /* 新添:增加cache初始化操作 */
listenfd = Open_listenfd(argv[1]);
while (1) {
printf("listening..\n");
clientlen = sizeof(clientaddr);
connfd = Malloc(sizeof(int));

*connfd = Accept(listenfd, (SA*)&clientaddr, &clientlen);

Getnameinfo((SA*)&clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
Pthread_create(&tid, NULL, thread, connfd);
}
free_cache(); /* 新增:服务完成后释放cache */
return 0;
}

void* thread(void* vargp)
{
int connfd = *((int*)vargp);
Pthread_detach(pthread_self());
Free(vargp);
doit(connfd);
Close(connfd);
return NULL;
}

void doit(int client_fd)
{
int endserver_fd;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
rio_t from_client, to_endserver;
char hostname[MAXLINE], path[MAXLINE];
int port;

Rio_readinitb(&from_client, client_fd);

if (!Rio_readlineb(&from_client, buf, MAXLINE))
return;
sscanf(buf, "%s %s %s", method, uri, version);
if (strcasecmp(method, "GET")) {
clienterror(client_fd, method, "501", "Not Implemented",
"Proxy Server does not implement this method");
return;
}
/* 新增:从cache中读取信息,如果读取成功返回给客户端(client_fd) */
int ret = read_cache(uri, client_fd);
if (ret == 1) {
return;
}

parse_uri(uri, hostname, path, &port);
char port_str[10];
sprintf(port_str, "%d", port);
endserver_fd = Open_clientfd(hostname, port_str);
if (endserver_fd < 0) {
printf("connection failed\n");
return;
}
Rio_readinitb(&to_endserver, endserver_fd);

char newreq[MAXLINE];
sprintf(newreq, "GET %s HTTP/1.0\r\n", path);
build_requesthdrs(&from_client, newreq, hostname);

Rio_writen(endserver_fd, newreq, strlen(newreq));
int n, size = 0; /* 新增:参数size */
char data[MAX_OBJECT_SIZE];
while ((n = Rio_readlineb(&to_endserver, buf, MAXLINE))) {
/* 新增:cache存储的数据不能太大,过大的数据将会被丢弃 */
if (size <= MAX_OBJECT_SIZE) {
memcpy(data + size, buf, n);
size += n;
}
printf("proxy received %d bytes,then send\n", n);
Rio_writen(client_fd, buf, n);
}
printf("size: %d\n", size);
/* 新增:在cache中写入信息,下次申请前会进行读取操作,读取到后立刻返回 */
if (size < MAX_OBJECT_SIZE) {
write_cache(uri, data, size);
}
}

void clienterror(int fd, char* cause, char* errnum,
char* shortmsg, char* longmsg)
{
char buf[MAXLINE], body[MAXBUF];

sprintf(body, "<html><title>Proxy Error</title>");
sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
sprintf(body, "%s<hr><em>The Proxy Web server</em>\r\n", body);

sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: text/html\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
Rio_writen(fd, buf, strlen(buf));
Rio_writen(fd, body, strlen(body));
}

void parse_uri(char* uri, char* hostname, char* path, int* port) {
*port = 80;
char* pos1 = strstr(uri, "//");
if (pos1 == NULL) {
pos1 = uri;
}
else pos1 += 2;

char* pos2 = strstr(pos1, ":");
if (pos2 != NULL) {
*pos2 = '\0';
strncpy(hostname, pos1, MAXLINE);
sscanf(pos2 + 1, "%d%s", port, path);
*pos2 = ':';
}
else {
pos2 = strstr(pos1, "/");
if (pos2 == NULL) {
strncpy(hostname, pos1, MAXLINE);
strcpy(path, "");
return;
}
*pos2 = '\0';
strncpy(hostname, pos1, MAXLINE);
*pos2 = '/';
strncpy(path, pos2, MAXLINE);
}
}

void build_requesthdrs(rio_t* rp, char* newreq, char* hostname, char* port) {
char buf[MAXLINE];

while (Rio_readlineb(rp, buf, MAXLINE) > 0) {
if (!strcmp(buf, "\r\n")) break;
if (strstr(buf, "Host:") != NULL) continue;
if (strstr(buf, "User-Agent:") != NULL) continue;
if (strstr(buf, "Connection:") != NULL) continue;
if (strstr(buf, "Proxy-Connection:") != NULL) continue;

sprintf(newreq, "%s%s", newreq, buf);
}
sprintf(newreq, "%sHost: %s:%s\r\n", newreq, hostname, port);
sprintf(newreq, "%s%s%s%s", newreq, user_agent_hdr, conn_hdr, prox_hdr);
sprintf(newreq, "%s\r\n", newreq);
}

打分:

1
2
3
4
5
6
7
8
9
10
11
12
13
*** Cache ***
Starting tiny on port 10306
Starting proxy on port 2574
Fetching ./tiny/tiny.c into ./.proxy using the proxy
Fetching ./tiny/home.html into ./.proxy using the proxy
Fetching ./tiny/csapp.c into ./.proxy using the proxy
Killing tiny
Fetching a cached copy of ./tiny/home.html into ./.noproxy
Success: Was able to fetch tiny/home.html from the cache.
Killing proxy
cacheScore: 15/15

totalScore: 70/70