【计网实验——prj3】Socket应用编程实验
实验要求
实现
• 使用C语言分别实现最简单的HTTP服务器和HTTP客户端
服务器监听80端口,收到HTTP请求,解析请求内容,回复HTTP应答
- 所请求的文件在服务器程序当前目录中,返回HTTP 200 OK和相应文件
- 所请求的文件不在服务器程序当前目录中,返回HTTP 404 File not found
服务器、客户端只需要支持HTTP Get方法,不需要支持Post等方法
服务器使用多线程支持多路并发
测试
• 所实现的HTTP服务器、客户端可以完成上述功能
使用客户端连续多次获取文件
同时启动多个客户端进程分别获取文件
使用客户端请求不存在的文件
实现方案
给出的代码框架中,echo-server和echo-client已经利用socket API实现了服务器与客户端的绑定、监听、建立连接和数据传输的过程,因此只需要改进原有的代码使得服务器与客户端能够支持HTTP协议的GET方法即可。
GET方法实现设计
客户端给服务器发送一个HTTP请求
一个HTTP请求报文由请求行(request line)、请求头部(headers)、空行(blank line)和请求数据(request body)4个部分组成。其中请求行中的请求方法为GET。
因此,客户端程序http-client.c的较echo-client.c需要添加生成请求报文的代码段,通过scanf函数获得用户输入的请求文件路径(URL),将其按照格式包装为请求报文,再利用socket API工具将该报文发送给服务器即可。相关代码如下:
char *filepath;
filepath = (char*) malloc(sizeof(char) * 100);
char recv_file[105] = "recv_";
printf("enter message : ");
scanf("%s", filepath);
strcat(recv_file, filepath);
bzero(message, sizeof(message));
bzero(server_reply, sizeof(server_reply));
//add request head
strcat(message, "GET /");
strcat(message, filepath);
strcat(message, request_head);
free(filepath);
printf("send message: %s\n", message);
// send the request message
if (send(sock, message, strlen(message), 0) < 0) {
printf("send failed\n");
return 1;
}
其中,request_head的定义如下:
char request_head[] =
" HTTP/1.1\r\n"
"Accept: */*\r\n"
"Accept-Encoding: identity\r\n"
"Host: 10.0.0.1\r\n"
"Connection: Keep-Alive\r\n\r\n";
服务器端响应HTTP请求
报文格式中的请求数据不在GET方法中使用,而在POST方法中使用。GET方法中,服务器只需要利用URL定位资源并获取,再返回给客户端即可。返回时使用的格式为HTTP响应报文格式。
服务器在获取客户端的请求报文后的第一步是,对请求报文进行解析,获取资源路径,代码中的handle_req_msg函数实现了这一功能:
void *handle_req_msg(char *msg, char *path){
int i, j;
//根据请求报文格式特点可知URL在两个空格之间
for(i = 0; i < strlen(msg) && msg[i] != ' '; i++);
i++;
if(msg[i] == '/') i++;
for(j = 0; i < strlen(msg) && msg[i] != ' '; i++, j++) path[j] = msg[i];
path[j] = '\0';
}
获取目标文件路径后,服务器读取文件内容,并将该内容按照响应报文的格式进行包装,返回给客户端,代码设计如下:
void *handle_request(void *cs_in){
char msg[2000];
int msg_len = 0;
char *path, *buffer;
int cs = *(int *)cs_in;
// receive a message from client
while ((msg_len = recv(cs, msg, sizeof(msg), 0)) > 0){
// send the message back to client
path = (char*) malloc(sizeof(char) * 100);
handle_req_msg(msg, path);
printf("%s\npath: %s\n", msg, path);
FILE *fd = fopen(path, "r");
char return_msg[1000];
bzero(return_msg, sizeof(return_msg));
strcat(return_msg, "HTTP/1.1 ");
if(fd == NULL){
strcat(return_msg, "404 File not found\r\n");
}
else{
//read target file
fseek(fd, 0, SEEK_END);
int flength = ftell(fd);
char flen_str[20];
buffer = (char*) malloc(sizeof(char) * flength);
fseek(fd, 0, 0);
fread(buffer, flength + 1, sizeof(char), fd);
//add response head
strcat(return_msg, "200 OK\r\nContent-Length: ");
sprintf(flen_str, "%d", flength);
strcat(return_msg, flen_str);
strcat(return_msg, "\r\nConnection: Keep-Alive\r\n\r\n");
strcat(return_msg, buffer);
strcat(return_msg, "\r\n");
free(buffer);
fclose(fd);
}
free(path);
printf("%s", return_msg);
write(cs, return_msg, strlen(return_msg));
}
if (msg_len == 0) {
printf("client disconnected\n");
}
else { // msg_len < 0
perror("recv failed\n");
return;
}
return;
}
一台服务器同时处理多个服务请求
使用多线程技术实现一台服务器同时处理多个服务请求的功能:
// use pthread to support multi-request
struct sockaddr_in client;
c = sizeof(struct sockaddr_in);
while(1) {
if ((cs = accept(s, (struct sockaddr *)&client, (socklen_t *)&c)) < 0) {
perror("accept failed\n");
return -1;
}
printf("connection accepted\n");
pthread_t thread;
if((pthread_create(&thread, NULL, handle_request, &cs)) != 0){
perror("pthread create failed\n");
return -1;
}
pthread_detach(thread);
}
客户端得到并保存资源
获取服务器的响应报文后,对其同样按照响应报文格式进行解析,并将资源内容保存在本地,接收的文件名在原文件名前增加 recv_ ,如下图:
这个过程主要通过函数handle_recv_msg实现:
void handle_recv_msg(char *server_reply, FILE *fd){
int i, flag = 0;
for(i = 0; i < strlen(server_reply) - 3; i++){
if(server_reply[i] == '\r' && server_reply[i+1] == '\n' && server_reply[i+2] == '\r' && server_reply[i+3] == '\n'){
i += 4;
flag = 1;
break;
}
}
if(i < strlen(server_reply) && flag){
fwrite(&server_reply[i], 1, strlen(&server_reply[i]), fd);
}
else printf("Saving file failed.\n");
char *result;
if ((result = strtok(server_reply, "\r\n")) != NULL) {
printf("%s\n", result);
}
}
运行结果
1. 所实现的HTTP服务器、客户端可以完成上述功能
使用客户端连续多次获取文件以及请求不存在的文件
客户端连续两次请求文件1.txt和2.txt,打印报文如上图,其中1.txt的内容是"This is a http server created by zzq.",而文件2.txt不存在,打印出"404 Not Found",符合预期。
同时启动多个客户端进程分别获取文件
同时打开两个终端启动两个客户端,服务器打印报文如下,运行正常。
2. 使用“python -m SimpleHTTPServer 80”和wget分别替代服务器和客户端,对端可以正常工作
SimpleHTTPServer替代服务器
用SimpleHTTPServer替代服务器,客户端连续获取1.txt和2.txt,打印正常,说明客户端可以正常工作。
wget替代客户端
用wget替代客户端,获取1.txt或2.txt,打印正常,说明服务器可以正常工作。
遇到的问题及其解决方法
在实验过程中,通过抓包发现,客户端向 SimpleHTTPServer 发送请求信号后, 如果多次发送请求, SimpleHTTPServer 在第⼀次正常返回响应后,TCP会发出RST信号,导致客户端如果不重新建立连接就会发生异常。
因此,如果客户端向 SimpleHTTPServer 连续发包的话,需要在每次发包后重连。在每一次handle_recv_msg之后,添加如下代码:
close(sock);
sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, (struct sockaddr *)&server, sizeof(server));