CMU15-213 Proxy Lab

CMU15-213 Proxy Lab

该Lab主要分为三个部分,完成代理服务器的基本功能,即实现基本的转发;支持多线程并发的功能;实现LRU的缓存功能。
在下面对三部分所具备的知识进行依次的说明,并将三部分的代码托管于github中,github链接见文末。

代理服务器基本功能相关知识

对于客户端来说,在建立连接前需要进行socket以及connect这两个步骤。
socket函数用来打开一个socket描述符,即类似于文件的打开操作,(文件打开的是文件描述符)。函数中的参数为控制参数,指明所要的协议簇、socket类型以及具体的协议。

int socket(int domain, int type, int protocol);

connect函数则是通过socket函数打开的描述符向服务器端的sockaddr(即IP地址和端口号)发起连接请求。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

将上述两个函数功能进行封装后便为open_clientfd函数,客户端通过这个函数传入服务器端的hostname以及端口号进行连接的建立,成功则返回客户端的描述符。

int open_clientfd(char * hostname, int port);

以上便是建立连接前客户端所需要完成的工作步骤。
对于服务器端来说,所需要进行的步骤相对于客户端来说多了几个,服务器端在建立连接前需要进行socket、bind、listen、accept这四个步骤。
其中,socket函数的功能与客户端是相通的。
bind函数用于将socket函数返回的socket描述符与指定的服务器地址及端口号进行绑定。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen函数则用于将前面过程建立的socket变为被动类型、等待连接请求的监听描述符。

int listen(int sockfd, int backlog);

上述三个函数的封装函数便为Open_listenfd,服务器端通过Open_listenfd函数进行socket、bind、listen的对应操作,返回监听描述符。

int Open_listenfd(int port);

最后便是服务器端的accept函数,用于接收客户端的连接请求,它的第一个参数为监听描述符,第二个参数为收到的发起连接请求的客户端的请求地址,最终返回一个已连接的socket描述符。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

注该描述符与上文提到的监听描述符不是同一个描述符。具体区别如下图所示。
在这里插入图片描述
以上便是客户端以及服务器端建立连接前的整体过程。如下图所示。
服务器端与客户端响应过程

Rio包系列函数

Rio_readinitb(rio_t &, int); // 将文件描述符int与rio数据结构绑定起来
Rio_readlineb(&rio_t, buf, MAXLINE) // 从rio数据结构中读取一行数据到buf中
Rio_writen(int, newreq, strlen(newreq)) //将newreq数据写到int的文件描述符中

多线程并发功能

这一部分主要是为了使得我们的代理服务器能够处理多个客户端请求,而不是在一个客户端请求到来后便等这一请求处理完后再进行接下来的请求,这样做一定程度上提高了我们程序的并发性。
这个功能的实现只需要在第一部分的基础上增加多线程的功能,即每到来一个请求便开启一个新线程去执行。

Pthread_create(&tid, NULL, thread, connfd);

上述函数将新开启一个线程id为tid的线程来执行thread这个函数并将connfd这个socket描述符传入thread这一函数中。主进程将继续执行。
在thread这一函数中,将进行连接的建立以及相应请求的释放。
关于连接请求的释放,除了正常描述符的close以及动态申请内存的free之外还需要注意将线程状态切换为unjoinable状态。即,

Pthread_detach(pthread_self()); // 将线程状态改为unjoinable

linux下,pthread一共有两种状态,即joinable状态和unjoinable状态,在joinable状态下,当线程函数自己退出时,需要主进程调用pthread_join这个函数才会使线程所占用的栈和线程描述符这些内存资源;若为unjoinable状态,则会在线程退出时自动将这些资源进行释放。
在这一步的编程中需要有两个注意点⚠️
变量的动态内存申请空间:由于我们不同的线程的socket描述符均使用connfd这一变量,因此我们需要采用malloc来对这一变量进行动态内存分配,否则connfd这一变量的地址将是固定的,又可能因下一线程的值的覆盖,导致上一线程该地址下的connfd值发生改变,因此发生错误。
Timeout waiting for the server to grab the port reserved for it:这一问题的发生是在我们测试我们的程序,即./driver.sh运行这一过程中的,发生这一问题的原因不是我们代码的问题,而是在driver.sh这一脚本中的环境变量与我们本地的环境变量有所不一致而导致出现的问题。

//修改前
./nop-server.py ${nop_port} &> /dev/null &
// 修改后
python3 ./nop-server.py ${nop_port} &> /dev/null &

即运行.py文件需要通过python3这一工具来进行运行,具体情况下需要根据当前主机的py文件的运行指令来决定。

LRU缓存机制

LRU缓存机制的实现主要是需要通过我们来实现一个LRU的cache来进行的。cache的实现我们只需要通过一个数组对数据进行存储,然后实现一个LRU算法来进行数据的读取与写入便可以完成。
但由于我们的代理服务器支持并发请求,即多线程访问,因此,在这个过程中,我们需要解决多个线程同时读写cache的问题,也就是说需要一定的同步机制来避免并发问题所带来的影响。
处理读写问题的经典模型为读者写者模型,通过对读写操作加锁来进行控制。

pthread_rwlock_t rwlock; //定义一个读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); // 初始化读写锁,初始状态为未锁定的
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);// 进行读锁定,
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); //pthread_rwlock_tryrdlock()函数和pthread_rwlock_rdlock函数的功能相近,不同的是,当已有线程写锁定读写锁,或是有试图写锁定的线程被阻塞时,pthread_rwlock_tryrdlock函数失败返回。
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //写锁定读写锁rwlock。如果没有线程读或写锁定读写锁rwlock,当前线程将写锁定读写锁rwlock。否则线程将被阻塞,直到没有线程锁定这个读写锁为止。
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); //pthread_rwlock_trywrlock函数的功能和pthread_rwlock_wrlock函数相近。不同的是如果有其他线程锁定了读写锁,pthread_rwlock_trywrlock函数会失败返回。
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // 解锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); //释放

代码链接:https://github.com/WellYixuanDu/CMU15-213/tree/main/proxylab-handout

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值