网络编程:基于C语言的简易代理服务器实现(proxylab)
本文记录了一个基于c socket的简易代理服务器的实现。(CS:APP lab 10 proxy lab)
本代理服务器支持keep-alive连接,将访问记录保存在log文件。
Github: https://github.com/He11oLiu/proxy
全文分为以下部分
- HINT:
CS:APP对服务器的要求 - Part1:迭代服务器实现 & 简易处理(强制
HTTP/1.0) - Part2:并行服务器 & 互斥量
- Part3:进一步理解
HTTP协议,修改处理函数使其支持keep-alive - Part4:
readn与writen的优化 - Q&A :出现的问题及解决方法
HINT
- [x] Be careful about memory leaks. When the processing for an HTTP request fails for any reason, the thread must close all open socket descriptors and free all memory resources before terminating.
- [x] You will find it very useful to assign each thread a small unique integer ID (such as the current requestnumber) and then pass this ID as one of the arguments to the thread routine. If you display this ID ineach of your debugging output statements, then you can accurately track the activity of each thread.
- [x] To avoid a potentially fatal memory leak, your threads should run as detached, not joinable (CS:APP 13.3.6).
- [x] Since the log file is being written to by multiple threads, you must protect it with mutual exclusion semaphores wdfhenever you write to it (CS:APP 13.5.2 and 13.5.3).
- [x] Be very careful about calling thread-unsafe functions such as
inetntoa,gethostbyname, andgethostbyaddrinside a thread. In particular, theopen clientfdfunction in csapp.c is thread-unsafe because it callsgethostbyaddr, a Class-3 thread unsafe function (CSAPP 13.7.1).You will need to write a thread-safe version of openclientfd, calledopen_clientfd_ts, that uses the lock-and-copy technique (CS:APP 13.7.1) when it callsgethostbyaddr. - [x] Use the RIO (Robust I/O) package (CS:APP 11.4) for all I/O on sockets. Do not use standard I/O onsockets. You will quickly run into problems if you do. However, standard I/O calls such as fopenand fwrite are fine for I/O on the log file.
- [x] The
Rio_readn,Rio_readlineb, and Rio writen error checking wrappers incsapp.carenot appropriate for a realistic proxy because they terminate the process when they encounter an error. Instead, you should write new wrappers calledRio readn w,Rio readlineb w, and Rio writen w that simply return after printing a warning message when I/O fails. When either of the read wrappers detects an error, it should return 0, as though it encountered EOF on the socket. - [x] Reads and writes can fail for a variety of reasons. The most common read failure is an
errno =ECONNRESETerror caused by reading from a connection that has already been closed by the peeron the other end, typically an overloaded end server. The most common write failure is anerrno =EPIPEerror caused by writing to a connection that has been closed by its peer on the other end. This can occur for example, when a user hits their browser’s Stop button during a long transfer. - [x] Writing to connection that has been closed by the peer first time elicits an error with errno set to EPIPE. Writing to such a connection a second time elicits a
SIGPIPEsignal whose default action isto terminate the process. To keep your proxy from crashing you can use the SIGIGN argument to th esignal function (CS:APP 8.5.3) to explicitly ignore these SIGPIPE signals
Part 1
Implementing a Sequential Web Proxy
简易proxy lab雏形
服务器框架
int main(int argc, char **argv){
int lisenfd, port;
unsigned int clientlen;
clientinfo* client;
/* Ignore SIGPIPE */
Signal(SIGPIPE, SIG_IGN);
if (argc != 2){
fprintf(stderr, "usage:%s <port>\n", argv[0]);
exit(1);
}
port = atoi(argv[1]);
/* open log file */
logfile = fopen("proxylog","w");
lisenfd = Open_listenfd(port);
clientlen = sizeof(struct sockaddr_in);
while (1){
/* Create a new memory area to pass arguments to doit */
/* It will be free by doit */
client = (clientinfo*)Malloc(sizeof(clientinfo));
client->socketfd = Accept(lisenfd, (SA *)&client->clientaddr, &clientlen);
printf("Client %s connected\n",inet_ntoa(client->clientaddr.sin_addr));
doit(client);
}
return 0;
}
作为最初版本,先完成一个迭代服务器,而非并行服务器,这类服务器的框架相对简单,这个部分主要测试对于期功能的理解,并在只针对一个用户接入的情况下进行处理。
服务器框架可简化为如下,其中doit()为实际处理客户端请求的函数。
init_server();
while(1){
accept();
doit();
}
doit()处理客户端的请求
对于代理的处理条例很清晰
- 获取从客户发来的
HTTP请求 - 拆解其中的
uri - 连接服务器,并重新发送
HTTP请求 - 获取服务器的反馈并输出给客户端
- 记录该条访问记录
/*
* doit
*/
void doit(clientinfo *client){
int serverfd;
char buf[MAXLINE],method[MAXLINE],uri[MAXLINE],version[MAXLINE];
char hostname[MAXLINE],pathname[MAXLINE];
int port;
char errorstr[MAXLINE];
char logstring[MAXLINE];
rio_t rio;
ssize_t len = 0;
int resplen = 0;
/* init args */
Rio_readinitb(&rio,client->socketfd);
Rio_readlineb(&rio,buf,MAXLINE);
sscanf(buf,"%s %s %s",method,uri,version);
if(strcmp(method,"GET")){
fprintf(stderr, "error request\n");
sprintf(errorstr,"%s Not Implement",method);
clienterror(client->socketfd, method, "501","Not Implement", errorstr);
Close(client->socketfd);
return;
}
if(parse_uri(uri,hostname,pathname,&port)!=0){
fprintf(stderr, "parse error\n");
clienterror(client->socketfd, method, "400","uri error","URI error");
Close(client->socketfd);
return;
}
#ifdef DEBUG
printf("Finish parse %s %s %s %d\n",uri,hostname,pathname,port);
#endif
/* connect to server */
if((serverfd=open_clientfd(hostname,port))<0){
printf("Cannot connect to server %s %d\n",hostname,port);
clienterror(client->socketfd, method, "302","Server not found", "Server not found");
Close(client->socketfd);
return;
}
/* generate and push the request to server */
if(pathname[0]=='\0') strcpy(pathname,"/");
if(strcmp("HTTP/1.0",version)!=0) printf("Only support HTTP/1.0");
sprintf(buf,"%s %s HTTP/1.0\r\n",method, pathname);
Rio_writen(serverfd,buf,strlen(buf));
sprintf(buf,"Host: %s\r\n",hostname);
Rio_writen(serverfd,buf,strlen(buf));
sprintf(buf,"\r\n");
Rio_writen(serverfd,buf,strlen(buf));
/* receive the response from server */
Rio_readinitb(&rio, serverfd);
while((len = rio_readnb(&rio, buf, MAXLINE)>0)){
Rio_writen(client->socketfd, buf, MAXLINE);
resplen += MAXLINE - len;
memset(buf, 0, MAXLINE);
}
format_log_entry(logstring, &client->clientaddr, uri, resplen);
fprintf(logfile, "%s\n", logstring);
close(client->socketfd);
close(serverfd);
/* free the clientinfo space */
free(client);
}
在这里遇到Q&A中的第二个问题,无法支持HTTP/1.1
尝试直接在设置中接入此proxy
而网页一般发出为HTTP/1.1,导致也存在卡在read的情况,需要特殊处理
另一种尝试
但是由于浏览器发出的变量中有要求keep-alive的,导致read不能用,还是放弃此种方法。
/* Or just copy the HTTP request from client */
Rio_writen_w(serverfd, buf, strlen(buf));
while ((len = Rio_readlineb_w(&rio, buf, MAXLINE)) != 0) {
Rio_writen_w(serverfd, buf,len);
if (!strcmp(buf, "\r\n")) /* End of request */
break;
memset(buf,0,MAXLINE);
}
Parse_uri的小BUG
hostend = strpbrk(hostbegin, " :/\r\n\0");
/* when no ':' show up in the end,hostend may be NULL */
if(hostend == NULL) hostend = hostbegin + strlen(hostbegin);
简易代理测试
设置http代理
尝试连接
Part 2
Dealing with multiple requests concurrently
多线程
支持多线程是非常简单的,但是稍微复杂一点的是后面的互斥量处理。
这里先新写一个线程处理函数。
void *thread_handler(void *arg){
doit((clientinfo*)arg);
return NULL;
}
然后在原来的doit的地方改为
Pthread_create(&thread, NULL, thread_handler, client);
Pthread_detach(thread);
现在服务器的框架如下:
main(){
init_server();
while(1){
accept();
create_newThread(handler,arg);
}
}
//每个线程的处理
handler(arg){
initThread();
doit(arg);
}
互斥量
由于在macOS中的sem_init已经被标记为__deprecated,内存中的互斥量已经不能用了。这里改为基于文件的sem_open来替代sem_init。
/* Mutex semaphores */
sem_t *mutex_host, *mutex_file;
if((mutex_host = sem_open("mutex_host",O_CREAT,S_IRUSR | S_IWUSR, 1))==NULL){
fprintf(stderr,"cannot create mutex");
}
if((mutex_file = sem_open("mutex_file",O_CREAT,S_IRUSR | S_IWUSR, 1))==NULL){
fprintf(stderr,"cannot create mutex");
}
在文档中提到过open_client中由于调用了getaddrbyhost,必须要在调用之前获取互斥量,故完成新的open_clientfd。
在CSAPP中打包好了PV原语的接口,可以直接调用。
原来的open_clientfd的实现方法如下,只用在注视掉的地方加上PV原语保证只有一个thread在cs区域即可。
/* Fill in the server's IP address and port */
bzero((char *) &serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
//P(mutex_host);
if ((hp = gethostbyname(hostname)) == NULL)
return -2; /* check h_errno for cause of error */
bcopy((char *)hp->h_addr_list[0],
(char *)&serveraddr.sin_addr.s_addr, hp->h_length);
serveraddr.sin_port = htons(port);
//V(mutex_host);
对于文件,进行类似操作
format_log_entry(logstring, &client->clientaddr, uri, resplen);
P(mutex_file);
fprintf(logfile, "%s\n", logstring);
V(mutex_file);
为了能够在服务器运行的时候打开文件,将文件操作修改为如下:
format_log_entry(logstring, &client->clientaddr, uri, resplen);
P(mutex_file);
logfile = fopen("proxy.log","a");
fprintf(logfile, "%s\n", logstring);
fclose(logfile);
V(mutex_file);
thread_id
利用一个全局变量来记录当前thread的id。并通过clientinfo将其传走。
/* thread id */
unsigned long tid = 0;
printf("Client %s connected tid = %zd\n",inet_ntoa(client->clientaddr.sin_addr),tid);
client->tid = tid ++;
Pthread_create(&thread, NULL, thread_handler, client);
Rio_xxx_w
由于Rio_writen与Rio_readnb遇到错误时会直接unix_error。为了保证服务器继续运行,需要将其改为打印错误并返回。
void Rio_writen_w(int fd, void *usrbuf, size_t n){
if (rio_writen(fd, usrbuf, n) != n)
printf("Rio_writen_w error\n");
}
ssize_t Rio_readnb_w(rio_t *rp, void *usrbuf, size_t n){
ssize_t rc;
if ((rc = rio_readnb(rp, usrbuf, n)) < 0) {
printf("Rio_readnb_w error\n");
rc = 0;
}
return rc;
}
ssize_t Rio_readlineb_w(rio_t *rp, void *usrbuf, size_t maxlen){
ssize_t rc;
if ((rc = rio_readlineb(rp, usrbuf, maxlen)) < 0) {
printf("Rio_readlineb_w failed\n");
return 0;
}
return rc;
}
Part3
解决HTTP/1.1以及图片加载问题
在解决之前,在Github上转了一圈,所看有限几个repo中有的绕过了这个部分,直接像上面一样直接解析发送HTTP/1.0的请求,有的直接无差别用readline导致图片等文件仍然陷入read导致必须等待对方服务器断开连接后才能读到完整数据从read中出来,而导致网页加载速度奇慢。
下面就从HTTP的协议入手,寻找一个妥善的方法解决该问题。
当客户端请求时是
Connection: keep-alive的时候,服务器返回的形式Transfer-Encoding: chunked的形式,以确保页面数据是否结束,长连接就是这种方式,用chunked形式就不能用content-length
content-length设置响应消息的实体内容的大小,单位为字节。对于HTTP协议来说,这个方法就是设置Content-Length响应头字段的值。- 因为当浏览器与WEB服务器之间使用持久(
keep-alive)的HTTP连接,如果WEB服务器没有采用chunked传输编码方式,那么它必须在每一个应答中发送一个Content-Length的响应头来表示各个实体内容的长度,以便客户端能够分辨出上一个响应内容的结束位置。- 当不是
keep-alive,就是常用短连接形式,会直接把连接关掉,不需要长度。- 服务器上取得是动态内容,所有没有
content-length这项- 如果是静态页面,则有
content-length
故,对于服务器传回来的信息,不能直接无脑读,要对头部进行解析。对于服务器传回来的信息进行处理的步骤如下:
- 读头,头里面有几个比较重要的信息
\n\r代表着头的结束。Content-Length:条目代表着时明确给出长度的case,需要记录下长度的大小Transfer-Encoding:Chunked条目代表着属于Chunked编码的case,在后面用readline进行处理。
- 读
body
- 若为
Chunked编码,则直接使用readline进行读取。若读到0/r/n时,代表当前的body已经结束。退出循环。 - 若有
content-length属性,则利用read_size = MAXLINE > content_length ?content_length : MAXLINE计算每次需要读取的byte,然后调用readnb来精确读取字节。当读取到指定字节代表着body结束,退出循环。
- 若为
这样可以解决keep-alive导致的问题。
/* Receive response from target server and forward to client */
Rio_readinitb(&rio, serverfd);
/* Read head */
while ((len = Rio_readlineb_w(&rio, buf, MAXLINE)) != 0) {
/* Fix bug of return value when response line exceeds MAXLINE */
if (len == MAXLINE && buf[MAXLINE - 2] != '\n') --len;
/* when found "\r\n" means head ends */
if (!strcmp(buf, "\r\n")){
Rio_writen_w(client->socketfd, buf, len);
break;
}
if (!strncasecmp(buf, "Content-Length:", 15)) {
sscanf(buf + 15, "%u", &content_length);
chunked = False;
}
if (!strncasecmp(buf, "Transfer-Encoding:", sizeof("Transfer-Encoding:"))) {
if(strstr(buf,"chunked")!=NULL || strstr(buf,"Chunked")!=NULL)
chunked = True;
}
/* Send the response line to client and count the total len */
Rio_writen_w(client->socketfd, buf, len);
recv_len += len;
}
/* Read body */
if(chunked){
/* Transfer-Encoding:chuncked */
while ((len = Rio_readlineb_w(&rio, buf, MAXLINE)) != 0) {
/* Fix bug of return value when response line exceeds MAXLINE */
if (len == MAXLINE && buf[MAXLINE - 2] != '\n') --len;
/* Send the response line to client and count the total len */
Rio_writen_w(client->socketfd, buf, len);
recv_len += len;
/* End of response */
if (!strcmp(buf, "0\r\n")) {
Rio_writen_w(client->socketfd, "0\r\n", 2);
recv_len += 2;
break;
}
}
}
else{
read_size = MAXLINE > content_length?content_length:MAXLINE;
while((len = Rio_readnb_w(&rio,buf,read_size))!=0){
content_length -= len;
recv_len += len;
Rio_writen_w(client->socketfd, buf, len);
if(content_length == 0) break;
read_size = MAXLINE > content_length?content_length:MAXLINE;
}
}
当然这不是真正意义上的keep-alive。要做到持续链接少TCP建立几次,需要利用循环,再回到上面从客户端获取信息。
Part4
再次回到writen与readn的函数上。但用户还没加载完内容,就开始点击进入下一个网页,导致关闭了当前的网页,就会导致writen出现错误。
Reads and writes can fail for a variety of reasons. The most common read failure is an
errno =ECONNRESETerror caused by reading from a connection that has already been closed by the peeron the other end, typically an overloaded end server.The most common write failure is an
errno = EPIPEerror caused by writing to a connection that has been closed by its peer on the other end. This can occur for example, when a user hits their browser’s Stop button during a long transfer.
首先将这种错误情况单独处理
int Rio_writen_w(int fd, void *usrbuf, size_t n){
if (rio_writen(fd, usrbuf, n) != n){
printf("Rio_writen_w error\n");
if(errno == EPIPE)
/* client have closed this connection */
return CLIENT_CLOSED;
return UNKOWN_ERROR;
}
return NO_ERROR;
}
然后将所有的writen_w替换为
if(Rio_writen_w(client->socketfd, buf, len)==CLIENT_CLOSED){
clienton = False;
break;
}
当clienton为false的情况就可以直接跳过剩余,直接到log
同样的,修改read为
ssize_t Rio_readnb_w(rio_t *rp, void *usrbuf, size_t n,bool *serverstat){
ssize_t rc;
if ((rc = rio_readnb(rp, usrbuf, n)) < 0) {
printf("Rio_readnb_w error\n");
rc = 0;
if(errno == ECONNRESET) *serverstat = False;
}
return rc;
}
ssize_t Rio_readlineb_w(rio_t *rp, void *usrbuf, size_t maxlen,bool *serverstat){
ssize_t rc;
if ((rc = rio_readlineb(rp, usrbuf, maxlen)) < 0) {
printf("Rio_readlineb_w failed\n");
rc = 0;
if(errno == ECONNRESET) *serverstat = False;
}
return rc;
}
修改从客户端读取的readline为
Rio_readlineb_w(&rio, buf, MAXLINE,&clienton)
修改从服务器读取的readline为
Rio_readlineb_w(&rio, buf, MAXLINE,&serveron)
并添加一些对于server与client状态的检查避免消耗资源。
Q&A
为何都适用
fd来描述套接字从
unix程序的角度来看,socket是一个有相应描述符的打开文件。为何在
HTTP/1.1的情况下,需要中断等很久才能够读出来Client 127.0.0.1 connected
error request
Client 127.0.0.1 connected
Finish parse http://www.baidu.com www.baidu.com 80
Interrupted and Rebegin
Interrupted and Rebegin
Interrupted and Rebegin
Interrupted and Rebeginwhile (nleft > 0) {
//在这一步出不来????
if ((nread = read(fd, bufp, nleft)) < 0) {
if (errno == EINTR) /* interrupted by sig handler return */
nread = 0; /* and call read() again */
else
return -1; /* errno set by read() */
}
else if (nread == 0)
break; /* EOF */
nleft -= nread;
bufp += nread;
}观察是在
HTTP/1.1的情况下,在read函数出不来。
猜测可能是1.1是持续链接,不存在EOF,需要手动判断是否该退出while已解决,见
Part3非内存的
mutex打开时会读到上次的值先利用
unlink来取消链接。sem_unlink("mutex_host");
sem_unlink("mutex_file");
if((mutex_host = sem_open("mutex_host",O_CREAT,S_IRUSR | S_IWUSR, 1))==NULL){
fprintf(stderr,"cannot create mutex");
}
if((mutex_file = sem_open("mutex_file",O_CREAT,S_IRUSR | S_IWUSR, 1))==NULL){
fprintf(stderr,"cannot create mutex");
}
网络编程:基于C语言的简易代理服务器实现(proxylab)的更多相关文章
- Java 网络编程 -- 基于TCP 模拟多用户登录
Java TCP的基本操作参考前一篇:Java 网络编程 – 基于TCP实现文件上传 实现多用户操作之前先实现以下单用户操作,假设目前有一个用户: 账号:zs 密码:123 服务端: public c ...
- Linux网络编程:一个简单的正向代理服务器的实现
Linux是一个可靠性非常高的操作系统,但是所有用过Linux的朋友都会感觉到, Linux和Windows这样的"傻瓜"操作系统(这里丝毫没有贬低Windows的意思,相反这应该 ...
- 网络编程——基于TCP协议的Socket编程,基于UDP协议的Socket编程
Socket编程 目前较为流行的网络编程模型是客户机/服务器通信模式 客户进程向服务器进程发出要求某种服务的请求,服务器进程响应该请求.如图所示,通常,一个服务器进程会同时为多个客户端进程服务,图中服 ...
- 网络编程-基于Websocket聊天室(IM)系统
目录 一.HTML5 - Websocket协议 二.聊天室(IM)系统的设计 2.1.使用者眼中的聊天系统 2.2.开发者眼中的聊天系统 2.3.IM系统的特性 2.4.心跳机制:解决网络的不确定性 ...
- 网络编程(基于udp协议的套接字/socketserver模块/进程简介)
一.基于UDP协议的套接字 TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据.相对TCP,UDP则是面向无连接的协议. 使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就 ...
- JAVA基础知识之网络编程——-基于UDP协议的通信例子
UDP是一种不可靠的协议,它在通信两端各建立一个socket,这两个socket不会建立持久的通信连接,只会单方面向对方发送数据,不检查发送结果. java中基于UDP协议的通信使用DatagramS ...
- JAVA基础知识之网络编程——-基于TCP通信的简单聊天室
下面将基于TCP协议用JAVA写一个非常简单的聊天室程序, 聊天室具有以下功能, 在服务器端,可以接受客户端注册(用户名),可以显示注册成功的账户 在客户端,可以注册一个账号,并用这个账号发送信息 发 ...
- (网络编程)基于tcp(粘包问题) udp协议的套接字通信
import socket 1.通信套接字(1人1句)服务端和1个客户端 2.通信循环(1人多句)服务端和1个客户端 3.通信循环(多人(串行)多句)多个客户端(服务端服务死:1个客户端---&g ...
- JAVA基础知识之网络编程——-基于AIO的异步Socket通信
异步IO 下面摘子李刚的<疯狂JAVA讲义> 按照POSIX标准来划分IO,分为同步IO和异步IO.对于IO操作分为两步,1)程序发出IO请求. 2)完成实际的IO操作. 阻塞IO和非阻塞 ...
随机推荐
- ABP+AdminLTE+Bootstrap Table权限管理系统第二节--数据库脚本
第一点,上一篇文章中我们讲到codefirst中一些问题包括如图 1,codefirst在执行的数据库迁移过程中产生了很多文件,对于强迫症的我而言特别不爽,这些是可以不用生成的啊 2,在codefir ...
- hdu--1548--dfs--蜘蛛牌
/* Name: hdu--1548--蜘蛛牌 Author: shen_渊 Date: 17/04/17 09:15 Description: dfs,不好想,看两个大神的代码好久http://ww ...
- multiset与set
set的含义是集合,它是一个有序的容器,里面的元素都是排序好的,支持插入,删除,查找等操作,就 像一个集合一样.所有的操作的都是严格在logn时间之内完成,效率非常高. set和multiset的 ...
- Hadoop 之 NameNode 元数据原理
在对NameNode节点进行格式化时,调用了FSImage的saveFSImage()方法和FSEditLog.createEditLogFile()存储当前的元数据.Namenode主要维护两个文件 ...
- 01迷宫 洛谷 p1141
题目描述 有一个仅由数字0与1组成的n×n格迷宫.若你位于一格0上,那么你可以移动到相邻4格中的某一格1上,同样若你位于一格1上,那么你可以移动到相邻4格中的某一格0上. 你的任务是:对于给定的迷宫, ...
- DOS批处理中%cd%和%~dp0的区别[forward]
DOS批处理中%cd%和%~dp0的区别 在DOS的批处理中,有时候需要知道当前的路径. 在DOS中,有两个环境变量可以跟当前路径有关,一个是%cd%, 一个是%~dp0. 这两个变量的 ...
- C/C++ 知识点---设计模式
在软件工程中,设计模式用来描述在各种不同情况下,要怎么解决问题的一种方案.面向对象设计模式通常以类或对象来描述其中的关系和相互作用,是软件“设计”层次上的问题.使用设计模式可提高代码的重用性和可靠性, ...
- 你不可不看的Android开发命名规范
标识符命名法最要有四种: Camel(骆驼)命名法:除首单词外,其余所有单词的第一个字母大写,如:fooBar; Pascal命名法:所有单词的第一个字母大写,如:FooBar: 下划线命名法:单词与 ...
- 【Eclipse】更改部署位置
在使用eclipse启动tomcat时,偶尔会遇到应用没被部署的现象,导致访问时出现404 问题原因:应用部署在了eclipse自带的webapps中. 我们通常不喜欢eclipse自带的tomcat ...
- asp.net mvc 动态编译生成Controller
做网站后台管理系统的时候,有时我们需要根据用户的录入配置动态生成一些频道,这些频道需要用到独立的Controller,这时就需要用到运行时动态编译了.代码如下: using System.Web.Mv ...