NOTE: 1.本文所涉及项目来自于互联网教育资源,本文作者将其实现在自己的机器上。
2.本文所涉及网页资源,作者出于版权考虑不便提供,可以考虑换成其它网页资源,本项目的重点的在服务器,而不是前端页面。
WEB 服务器项目
一 项目开发流程
1 需求分析:做个什么东西,实现什么功能,达到什么效果,解决什么问题
2 概要设计:技术选型,采用什么框架,主要的技术路线,大体结构
3 制定计划:评估每个功能模块的投入,编写研发计划
4 详细设计:设计每个模块的功能细节(文件,库,函数,数据类型,类)
5 编写代码:根据详细设计,给出代码实现
6 系统测试:黑盒测试,白盒测试
7 发布产品
8 项目总结
二 需求分析
基于HTTP协议的WEB服务器,提供静态页面的下载
服务器 和 浏览器进行通信,传输层使用tcp协议,应用层,使用HTTP协议,浏览器
向服务器发送http请求,服务器接受请求后,对请求内容进行解析,明确浏览器目的
服务器需要从请求中提取关键数据.判断对方所请求的文件是否存在并确定类型,
构造响应,并回传给浏览器.服务器要支持并发访问.
三 概要设计
主模块启动服务器模块,服务器模块主要负责接受请求开线程
线程模块借助通信模块,接受客户端发送的http请求
线程模块借助http模块,对http请求进行解析
线程模块借助资源模块,对文件进行判断
线程模块借助http模块,构造响应
线程模块借助通信模块,发送响应
四 详细设计
http模块: http.h http.c
通信模块: socket.h socket.c
资源模块: resource.h resource.c
mime.h
信号模块: signals.h signals.c
线程模块: client.h client.c
服务器模块: server.h server.c
主模块: main.c
环境搭建
本文涉及到的服务器运行在ubuntu16.04上。在桌面准备文件夹如下:其中home目录存储的是静态网页文件,code目录存储的是服务器程序。
通信模块实现
头文件定义:
#ifndef __SOCKET_H
#define __SOCKET_H
//init a socket
int init_socket(short port);
//accept client request
int accept_client(void);
//store the request into buffer
char * recv_request(int connect_socket);
//send http head
int send_head(int connect_socket , const char * head);
//send http body
int send_body(int connect_socket , char * path);
//close socket
void close_socket(void);
#endif
实现
#include <unistd.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "socket.h"
static int s_sock = -1; //listening socket
int init_socket(short port){
printf("pid:%d,tid%ld : construct socket\n", getpid(),syscall(SYS_gettid));
//init a socket
s_sock = socket(AF_INET , SOCK_STREAM , 0 );
if (s_sock == -1){
perror("socket:");
return -1;
}
printf("pid:%d,tid%ld : set socket\n", getpid(),syscall(SYS_gettid));
// if server exit in the case of some reasons , then the server would need restart.
// But when the socket used last time is not closed immediately , restart would fail.
// So need multi-use of a single port by some sockets.
int on = 1;
if (setsockopt(s_sock , SOL_SOCKET , SO_REUSEADDR , &on , sizeof(on)) == -1 ){
perror("setsockopt:");
return -1;
}
printf("pid:%d,tid%ld : construct addr\n", getpid(),syscall(SYS_gettid));
//prepare the address for server
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(port); // bytes order
ser.sin_addr.s_addr = INADDR_ANY;
printf("pid:%d,tid%ld : bind addr\n", getpid(),syscall(SYS_gettid));
// bind the socket with server address
if(bind(s_sock , (struct sockaddr * )&ser , sizeof (ser)) == -1){
perror("bind:");
return -1;
}
printf("pid:%d,tid%ld : start listening\n", getpid(),syscall(SYS_gettid));
//listening client request
if (listen(s_sock, 1024) == -1){
perror("listen");
return -1;
}
return 0;
}
int accept_client(void){
printf("pid:%d,tid%ld : wait for clients\n", getpid(),syscall(SYS_gettid));
struct sockaddr_in cli; // the info of client
socklen_t len = sizeof(cli);
// get the socket for client-server communicating
int conn = accept(s_sock , (struct sockaddr*)&cli , &len);
if (conn == -1 ){
perror("accept:");
return -1;
}
printf("pid:%d,tid%ld : accepted a client , ip : %s port %d \n", getpid(),syscall(SYS_gettid), inet_ntoa(cli.sin_addr) , ntohs(cli.sin_port) );
return conn;
}
char * recv_request(int connect_socket){
char * req = NULL;
ssize_t len = 0;
for(;;){
char buffer[1024] = {} ;
ssize_t size = recv(connect_socket , buffer , sizeof (buffer) - 1 , 0);
if ( size == -1 ){
perror("recv:");
free(req);
return NULL;
}
if ( size == 0 ){
printf("pid:%d,tid%ld : client closed\n", getpid(),syscall(SYS_gettid));
free(req);
return NULL;
}
req = realloc(req , len + size + 1 );
memcpy(req + len , buffer , size);
len += size;
// \r\n\r\n for the end of recv
if (strstr(req , "\r\n\r\n")){
break;
}
}
return req;
}
int send_head(int connect_socket , const char * head){
if (send(connect_socket , head , strlen(head) , 0) == -1 ){
perror("send:");
return -1;
}
return 0;
}
int send_body(int connect_socket , char * path){
int fd = open( path , O_RDONLY );
if ( fd == -1 ){
perror("open:");
return -1;
}
char buffer[1024] = {};
ssize_t len;
// len == 0 indicates for the end of file from which the fun reads.
while( ( len=read(fd , buffer , sizeof(buffer) )) > 0 ){
if (send(connect_socket , buffer , len , 0) == -1){
perror("send");
return -1;
}
}
if (len == -1){
perror("read:");
return -1;
}
close(fd);
return 0;
}
void close_socket(void){
close(s_sock);
return ;
}
http模块实现
头文件
#ifndef __HTTP_H
#define __HTTP_H
#include<limits.h>
#include<sys/types.h>
//struct of parse result
typedef struct http_request{
char method[32];
char path[PATH_MAX+1];
char protocol[32];
char connection[32];
}HTTP_REQUEST;
//parse http requests
int parse_request(const char * request , HTTP_REQUEST * result);
//struct of http head
typedef struct http_response{
char protocol[32];
int status;
char desc[128];
char type[64];
off_t length;
char connection[32];
}HTTP_RESPONSE;
//construct http response
int construct_head(const HTTP_RESPONSE * response , char * head);
#endif
实现
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#define __USE_GNU //for the use of strcasestr
#include <string.h>
#include <time.h>
#include "http.h"
int parse_request(const char * request , HTTP_REQUEST * result){
sscanf(request, "%s%s%s" , result->method , result->path , result->protocol);
char * connection = strcasestr(request, "connection");
if (connection){
sscanf(connection , "%*s%s" , result->connection);
}else
return -1;
printf("pid:%d,tid%ld : [%s][%s][%s][%s]\n", getpid(),syscall(SYS_gettid),
result->method , result->path , result->protocol , result->connection);
if (strcasecmp(result->method,"get")){
printf("pid:%d,tid%ld : invaild method\n", getpid(),syscall(SYS_gettid));
return -1;
}
if (strcasecmp(result->protocol,"http/1.0") && strcasecmp(result->protocol,"http/1.1")){
printf("pid:%d,tid%ld : invaild protocol\n", getpid(),syscall(SYS_gettid));
return -1;
}
return 0;
}
int construct_head(const HTTP_RESPONSE * response , char * head){
char date_time[32] = {};
time_t now = time(NULL);
strftime(date_time , sizeof(date_time) , "%a %d %b %Y %T" , gmtime(&now) );
sprintf(head , "%s %d %s\r\n"
"Server: SUNSERVER1.0\r\n"
"Date: %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Connection: %s\r\n\r\n",response->protocol,response->status ,response->desc,date_time,response->type,response->length,response->connection);
return 0;
}
客户端线程模块实现
头文件
#ifndef __CLIENT_H_
#define __CLIENT_H_
// the arg of thread process fun
typedef struct client_arg{
const char * home;
int connect_socket;
} CLIENT_ARG;
//thread process fun
void * client(void* arg);
#endif
实现
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "http.h"
#include "socket.h"
#include "resource.h"
#include "client.h"
void * client(void* arg){
CLIENT_ARG * ca =(CLIENT_ARG *)arg;
printf("pid:%d,tid%ld : client thread running\n", getpid(),syscall(SYS_gettid));
for(;;){
printf("pid:%d,tid%ld : recv http request\n", getpid(),syscall(SYS_gettid));
char * buffer_req = recv_request(ca->connect_socket);
if (buffer_req == NULL){
break;
}
printf("pid:%d,tid%ld : request diagram : \n %s \n", getpid(),syscall(SYS_gettid) , buffer_req);
//parse request
printf("pid:%d,tid%ld : parse http request\n", getpid(),syscall(SYS_gettid));
HTTP_REQUEST hreq = {};
if (parse_request(buffer_req , &hreq) == -1 ){
free(buffer_req); //release mem of http req
break;
}
free(buffer_req);
//path prepare
// /home/huijun/Desktop/sunWebServer/home
char root[PATH_MAX+1]={};
char path[PATH_MAX+1]={};
strcpy(root,ca->home);
if (root[strlen(root)-1] == '/'){
root[strlen(root)-1] = '\0' ;
}
strcpy(path , root );
strcat(path , hreq.path);
//request of "/" , then return index page
if (strcmp(hreq.path , "/") == 0){
strcat(path , "index.html");
}
//char protocol[32];
HTTP_RESPONSE hres = {"HTTP/1.1",200,"OK","text/html"};
//search resource
if(search_resource(path) == -1){
hres.status = 404;
strcpy(hres.desc,"NOT FOUND");
strcpy(path , root);
strcat(path , "/404.html");
}else if(identify_type(path,hres.type) == -1){
hres.status = 404;
strcpy(hres.desc,"NOT FOUND");
strcpy(path , root);
strcat(path , "/404.html");
}
//get length of file
struct stat st;
if (stat(path , &st) == -1){
perror("stat:");
break;
}
hres.length = st.st_size;
//connection state
if (strlen(hreq.connection)){
strcpy(hres.connection , hreq.connection);
}else if (strcasecmp(hreq.protocol, "http/1.0") == 0){
strcpy(hres.connection, "Close");
}else{
strcpy(hres.connection, "Keep-alive");
}
//construct response
printf("pid:%d,tid%ld : construct response\n", getpid(),syscall(SYS_gettid));
char head[1024] = {};
if (construct_head(&hres,head) == -1){
break;
}
printf("pid:%d,tid%ld : response diagram : \n %s\n", getpid(),syscall(SYS_gettid) ,head );
if (send_head(ca->connect_socket , head) == -1 ){
break;
}
if (send_body(ca->connect_socket , path) == -1 ){
break;
}
if(strcasecmp(hres.connection, "Close") == 0){
break;
}
}
close(ca->connect_socket);
free(ca);
printf("pid:%d,tid%ld : client thread end\n", getpid(),syscall(SYS_gettid));
return NULL;
}
资源模块实现
头文件
#ifndef __RESOURCE_H_
#define __RESOURCE_H_
//serarch the requested-resources in the home file
int search_resource(const char * path);
//identify the type of requested-resource
int identify_type(const char * path , char * type);
#endif
实现
#include <unistd.h>
//#include <fcntl.h>
#include <sys/syscall.h>
#include <stdio.h>
#define __USE_GNU
#include <string.h>
#include "mime.h"
#include "resource.h"
int search_resource(const char * path){
return access(path , R_OK);
}
int identify_type(const char * path , char * type){
int len = strlen(path)-1;
//char * ptr = path;
const char * suffix; //= strrchr(path , '.');
while(len>=0 && path[len]!='.') --len;
if (len < 0) suffix = NULL;
else {
suffix = &path[len];
}
printf("\n***file type : %s \n" , suffix);
if (suffix == NULL){
printf("pid:%d,tid%ld : failed to get file extension\n", getpid(),syscall(SYS_gettid));
return -1;
}
for(int i = 0 ; i < sizeof(s_mime) / sizeof(s_mime[0]) ; ++i ){
if ( strcasecmp(suffix , s_mime[i].suffix ) == 0){
strcpy(type , s_mime[i].type);
printf("\n***path %s ***type: %s \n", path ,type);
return 0;
}
}
printf("pid:%d,tid%ld : failed to identify file extension\n", getpid(),syscall(SYS_gettid));
return -1;
}
信号模块实现
头文件
#ifndef __SIGNALS_H_
#define __SIGNALS_H_
//to protect the server from unexpected killing signals
int init_signals(void);
#endif
实现
#include <unistd.h>
#include <signal.h>
#include <sys/syscall.h>
#include <stdio.h>
#include "signals.h"
int init_signals(void){
printf("pid:%d,tid%ld : ignore most signals\n", getpid(),syscall(SYS_gettid));
for (int sn = 1 ; sn <= 64 ; ++sn){
if ( sn!= 2 && sn != 15)
signal(sn , SIG_IGN);
}
return 0;
}
服务器模块实现
头文件
#ifndef __SERVER_H_
#define __SERVER_H_
int init_server(short port);
int run_server(const char * home);
void close_server(void);
#endif
实现
#include <unistd.h>
#include <pthread.h>
#include <sys/resource.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "socket.h"
#include "signals.h"
#include "client.h"
#include "server.h"
//init max - fd - num
static int init_max_files(void){
// res limits
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE,&rl) == -1){
perror("getrlimit:");
return -1;
}
if (rl.rlim_cur < rl.rlim_max){
rl.rlim_cur = rl.rlim_max;
if (setrlimit(RLIMIT_NOFILE,&rl) == -1){
perror("setrlimit:");
return -1;
}
}
return 0;
}
int init_server(short port){
if (init_max_files() == -1 ){
return -1;
}
init_signals();
if (init_socket(port) == -1){
return -1;
}
return 0;
}
int run_server(const char * home){
for(;;){
int connect_socket = accept_client();
if (connect_socket == -1){
return -1;
}
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr , PTHREAD_CREATE_DETACHED);
CLIENT_ARG * ca = malloc(sizeof(CLIENT_ARG));
ca->connect_socket = connect_socket;
ca->home = home;
int error = pthread_create(&tid , &attr ,client , ca);
if (error){
fprintf(stderr,"pthread_create:%s\n" , strerror(error));
return -1;
}
}
return 0;
}
void close_server(void){
close_socket();
return;
}
主函数
#include<stdio.h>
#include<stdlib.h>
#include"server.h"
int main(int argc , char* argv[]){
if (init_server(argc<2 ? 80 : atoi(argv[1])) == -1 ){
return -1;
}
if (run_server(argc<3 ? "../home" : argv[2]) == -1){
return -1;
}
close_server();
return 0;
}
编译运行
写完上述文件.h和.c后,在code文件夹下执行以下操作
1.编译所有的 .c 文件到.o文件
gcc -c *.c
2.删除main.o文件,否则,在最后生产二进制文件时,会有重定义错误
rm main.o
3.编译main.c 文件 连接其它的.o文件,注意由于用到了线程库,需要在编译时加命令
gcc main.c *.o -o sun_webserver -pthread
4.运行服务器
sudo ./sun_webserver
运行后,等待客户端连接:
在同一局域网下输入服务器的网址,就可以实现浏览静态网页了。此为服务器运行时输出的信息: