Linux C小项目 —— 简单的web服务器

简单的Web服务器

实现一个基于HTTP通信协议的web服务器。客户端向服务器程序发送所需文件的请求,服务器端分析请求并将文件发送个客户端。

1、整体程序设计

客户端发送所需文件,服务器返回该文件,通信结束。

服务器程序的任务大致分为3步:

a、解析客户端程序发来的内容,找到需求文件的路径或者名字

b、在服务器主机上查找该文件

c、将该文件内容组织成客户端程序可以理解的形式,并将其发回给客户端。

 

步骤一:解析客户端发送的内容

Note:客户端程序使用web浏览器,浏览器程序是默认使用HTTP通信协议。通常,http默认使用的端口号是80

打开浏览器,在地址栏输入需要访问的服务器的ip地址和端口号。例,”httP://127.0.0.1:8080” 表示浏览器希望从地址为127.0.0.1的服务器上使用8080端口的应用程序那里接收数据,这些数据是遵守http通信协议的。

这时,浏览器向服务器程序发送一个HTTP协议的“协议头”,包含了一些客户端程序的基本信息,例如客户机的ip地址、浏览器版本、使用协议版本以及请求文件的方式等。协议头:

GET / HTTP/1.1

Host: 192.168.159.2:8080

User-Agent: Mozilla/5.0 (X11; U; Linux i586; en-US; rv:1.8.1.6)

Gecko/20061201 Firefox/2.0.0.6 (Redhat-feisty)

Accept:

text/xml, application/xml, application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5

Accept-Language: en-us,en; q=0.5

Accept-Encoding: gzip, deflate

Accept-Charset: ISO-8859-1, utf-8; q=0.7,*;q=0.7

Keep-Alive: 300

Connection: keep-alive

 

附:协议头最后有一个空行。

和本程序有关的是第一行。该行包括以下3个信息:

a、GET请求文件的方式,默认使用的是GET的方法。这个方法其实只是一个约定,具体的还要看服务器程序的实现。

b、“/”请求服务器程序的根目录。这个目录可由服务器程序编写时自行配置。例,如果服务器程序的根目录是“/home/admin”,那么“/”所代表的就是这个目录。

c、HTTP协议的版本号,本程序中使用的是1.1版本。这个字段对本程序没有什么影响。

服务器程序的第1步就是要解析“协议头”的第1行,文件请求方式和协议版本我们不关心,主要是要找到请求的目录。如果浏览器地址栏输入“http://192.168.11.6:8080”表示的是服务器程序根目录“/home/admin”,那么“http://192.168.11.6:8080/web/test.html”就表示的是“/home/admin/web/test.html”文件。因此,服务器程序的第一个任务就是解析出这个客户端请求的目录和该目录下的文件。

 

步骤二:寻找客户端需要的文件

在指定目录下寻找文件。如果是一个普通文件,且该文件没有执行权限,则将其打开,把文件返回给客户端。如果该文件是一个可执行文件,例如二进制可执行文件或者解释器文件,则执行该文件,将执行的输出结果返回给客户端,这种可执行文件成为CGI程序。

 

步骤三:将客户端所请求的信息返回

Note:客户端使用的是浏览器,简单地返回一个文本文件是不对的,理想的方式是返回一个客户端可以理解的格式的文件,这里使用HTML格式的文件。

文件index.html的内容如下所示:

<html>

<head><title>This is a test</title></head>

<body>

<p>successfully communicate</p>

<img src=’test.jpg’>

</body>

</html>

这个页面只有一行字和一张图片,图片的路径是根目录下的test.jpg文件。

除了浏览器可以解析的正文外,服务器还需要为返回的信息加上HTTP的协议头。该协议头包含2行:第1行是版本信息和应答码,其后是一个随便写的字符串“OK”用于调试目的;第2行是即将发送的文件类型。

版本协议和客户端发送来的是一致的,连接成功,应答码为200;如果失败,例如指定的文件不存在,应答码为404。文件类型为text/html,是因为index.html文件中既有文本又有图片,单纯的文本类型为text/plain,单纯的图片类型为image/jpg等。完整的返回信息应该如下所示:

HTTP/1.1 200 OK # 协议版本和应答码

Content-Type: text/html # 文件类型

# 注意协议头结束后有一个空行

<html>

<head><title>This is a test</title></head>

<body>

<p>successfully communicate</p>

<img src=’test.jpg’>

</body>

</html>

到此,服务器程序的任务就结束了,完成传输后,服务器端需要主动将连接断开,这样客户端就知道数据已经传输完毕。这是由HTTP协议所规定的。

浏览器接收到index.html后,发现里面需要一张图片,因此浏览器会再次向服务器发出一次请求,要求获得相应的文件。

经过两次请求后,客户端的浏览器获得了index.html所需要的所有文件。这时,浏览器就可以正确显示该页面了,至于如何显示则交给浏览器去做。

 

测试:

web_server程序所在的目录下准备配置文件config.ini

Port:  8080

Root-path: /home/trb331617/www

该程序使用8080端口,使用/home/trb331617/www作为根目录

 

/home/trb331617/www目录下准备index.html文件;准备图片png_favicon.pngtest.jpg

 

shell中运行该程序:“./build

 

打开web浏览器,在地址栏中输入:http://127.0.0.1:8080,即可得到index.html页面。

 

附:

Signal(SIGCHLD, SIG_IGN);

Signal(SIGPIPE, SIG_IGN);

 

网络编程

服务端

// 填充服务端地址结构

Bzero(serv_addr, sizeof(struct sockaddr_in));

Serv_addr->sin_family = AF_INET;

Serv_addr->sin_addr.s_addr = htonl(INADDR_ANY);

Serv_addr->sin_port = htons(port);

 

Listenfd = socket(AF_INET, SOCK_STREAM, 0); // socket创建套接字

Bind(fd, (struct sockaddr *)serv_addr, sizeof(struct sockaddr_in)); // bind 套接字和填充好的地址绑定

Listen(fd, 20); // listen 监听套接字

Accept(listenfd, (struct sockaddr *)&serv_addr, &len); // accept 接受连接请求

文件操作

Int fd = open(path, O_RDONLY);

FILE *fp = fopen(“./config.ini”, “r”);

Fgets(buf, BUFFSIZE, fp);

Read(connfd, buf, BUFFSIZE);

Write(connfd, buf, strlen(buf));

Struct stat stat_buf; fstat(fd, &stat_buf);

S_ISREG(stat_buf.st_mode); // 判断是否为普通文件

Stat_buf.st_mode & S_IXOTH // 判断是否为可执行文件

Dup2(connfd, STDOUT_FILENO); // dup2重定向标准输出至连接套接字

Execl(path, path, NULL); // execl执行

字符串操作

Strstr(buf, “port”); // 返回指定字符子串第一次出现的位置

Strcpy(path, p); // 复制字符串

Strcat(path, “/index.html”); // 连接字符串

Strtok(&buf[4], “ ”); // strtok 分割字符串


源代码文件汇总:

  1 /*
  2  * FILE: main.c
  3  * DATE: 20180205
  4  * ===============
  5  */
  6 
  7 #include "common.h"
  8 
  9 int main(void)
 10 {
 11         struct sockaddr_in serv_addr, cli_addr;
 12         struct stat stat_buf;
 13         int listenfd, connfd, len, fd, port;
 14         pid_t pid;
 15         char path[BUFFSIZE];
 16 
 17 
 18         signal(SIGCHLD, SIG_IGN);       // signal 信号处理
 19         signal(SIGPIPE, SIG_IGN);
 20 
 21         printf("initializing ...\n");
 22         if(init(&serv_addr, &listenfd, &port, path) < 0)        // 自定义init初始化
 23         {
 24                 DEBUG("error during initializing\n");
 25                 exit(-1);
 26         }
 27         while(1)
 28         {
 29                 DEBUG("waiting connection ...\n");
 30                 connfd = accept(listenfd, (struct sockaddr *)&serv_addr, &len);
 31                 if(connfd < 0)
 32                 {
 33                         perror("fail to accept");
 34                         exit(-2);
 35                 }
 36                 pid = fork();   // fork 子进程,并发处理请求
 37                 if(pid < 0)
 38                 {
 39                         perror("fail to fork");
 40                         exit(-3);
 41                 }
 42                 if(pid == 0)
 43                 {
 44                         close(listenfd);        // 关闭监听套接字
 45                         if(get_path(connfd, path) < 0)  // 分析客户端发来的信息,得到请求文件路径
 46                         {
 47                                 DEBUG("error during geting filepath\n");
 48                                 exit(-4);
 49                         }
 50                         DEBUG("%s\n", path);
 51                         if((fd=open(path, O_RDONLY)) < 0)
 52                         {
 53                                 error_page(connfd);
 54                                 close(connfd);
 55                                 exit(0);
 56                         }
 57                         if(fstat(fd, &stat_buf) < 0)
 58                         {
 59                                 perror("fail to get file status");
 60                                 exit(-5);
 61                         }
 62                         if(!S_ISREG(stat_buf.st_mode))  // S_ISREG 判断是否为普通文件
 63                         {
 64                                 if(error_page(connfd) < 0)      // 自定义error_page 输出出错页面
 65                                 {
 66                                         DEBUG("error during writing error-page\n");
 67                                         close(connfd);
 68                                         exit(-6);
 69                                 }
 70                                 close(connfd);
 71                                 exit(0);
 72                         }
 73                         if(stat_buf.st_mode & S_IXOTH)  // 可执行文件,说明是一个CGI文件
 74                         {
 75                                 dup2(connfd, STDOUT_FILENO);    // dup2重定向标准输出到连接套接字
 76                                 if(execl(path, path, NULL) < 0) // exec 执行该CGI文件
 77                                 {
 78                                         perror("fail to exec");
 79                                         exit(-7);
 80                                 }
 81                         }
 82                         if(write_page(connfd, path, fd) < 0)    // 若为普通文件,则将文件内容发送给客户端
 83                         {
 84                                 DEBUG("error during writing page\n");
 85                                 exit(-8);
 86                         }
 87                         close(fd);      // 关闭文件
 88                         close(connfd);  // 服务器端主动关闭连接套接字,表示数据传输完毕
 89                         exit(0);        // 子进程正常退出
 90                 }
 91                 else
 92                         close(connfd);  // 父进程关闭连接套接字,继续监听
 93         }
 94         return 0;
 95 
 96 }
 97 
 98 
 99 
100 /* FILE: web_server.c
101  * DATE: 20180205
102  * ===============
103  */
104 
105 #include "common.h"
106 /*
107  * 读取配置文件,对端口号和根目录进行配置。
108  * 只在本文件内调用,使用static关键字进行声明。
109  * port: 端口号 path: 服务器程序的根目录
110  */
111 static int configuration(int *port, char *path)
112 {
113         FILE *fp;
114         char buf[BUFFSIZE];
115         char *p;
116         // 打开配置文件。该文件放在服务器程序所在目录下
117         fp = fopen("./config.ini", "r");
118         if(fp == NULL)
119         {
120                 perror("fail to open config.ini");
121                 return -1;
122         }
123         while(fgets(buf, BUFFSIZE, fp) != NULL) // fgets 读取文件每一行的内容
124         {
125                 if(buf[strlen(buf)-1] != '\n')  // 判断文件格式
126                 {
127                         printf("error in config.ini\n");
128                         return -1;
129                 }
130                 else
131                         buf[strlen(buf)-1] = '\0';      // 将换行符\n改为结束符\0
132                 // 端口号的配置格式为 port: 8080,注意冒号:后面有一个空格
133                 if(strstr(buf, "port") == buf)  // 匹配port关键字,读取端口号
134                 {
135                         if((p=strchr(buf, ':')) == NULL)
136                         {
137                                 printf("config.ini expect ':'\n");
138                                 return -1;
139                         }
140                         *port = atoi(p+2);      // 跳过冒号:和空格得到端口号
141                         if(*port <= 0)
142                         {
143                                 printf("error port\n");
144                                 return -1;
145                         }
146                 }
147                 // 根目录的配置格式为 root-path: /root,注意冒号:后面有一个空格
148                 else if(strstr(buf, "root-path") == buf)
149                 {
150                         if((p=strchr(buf, ':')) == NULL)
151                         {
152                                 printf("config.ini expect ':'\n");
153                                 return -1;
154                         }
155                         p = p + 2;      // 跳过冒号和空格,得到根目录
156                         strcpy(path, p);
157                 }
158                 else
159                 {
160                         printf("error in config.ini\n");
161                         return -1;
162                 }
163         }
164         return 0;
165 }
166 
167 int init(struct sockaddr_in *serv_addr, int *listenfd, int *port, char *path)
168 {
169         int fd;
170 
171         configuration(port, path);
172 
173         bzero(serv_addr, sizeof(struct sockaddr_in));   // bzero
174         serv_addr->sin_family = AF_INET;        // sin_family   AF_INET
175         serv_addr->sin_addr.s_addr = htonl(INADDR_ANY); // sin_addr.s_addr      INADDR_ANY
176         serv_addr->sin_port = htons(*port);     // sin_port
177 
178         if((fd=socket(AF_INET, SOCK_STREAM, 0)) < 0)    // socker 创建套接字
179         {
180                 perror("fail to creat socket");
181                 return -1;
182         }
183         // bind 将已创建的套接字与填充好的服务端地址绑定
184         if(bind(fd, (struct sockaddr *)serv_addr, sizeof(struct sockaddr_in)) < 0)
185         {
186                 perror("fail to bind");
187                 return -2;
188         }
189         if(listen(fd, 20) < 0)  // listen 监听套接字
190         {
191                 perror("fail to listen");
192                 return -3;
193         }
194         *listenfd = fd;
195         return 0;
196 }
197 
198 /*
199  * 分析http协议头的第一行,得到请求文件方式和文件路径
200  * connfd: 连接套接字
201  * path: 服务器程序的根目录,用于和解析出的文件名拼成完整的文件路径
202  */
203 int get_path(int connfd, char *path)
204 {
205         char buf[BUFFSIZE];
206 
207         read(connfd, buf, BUFFSIZE);    // 读取HTTP协议头的第一行
208         // HTTP协议头第一行的格式为“GET / HTTP/1.1”
209         // 第四个字符为空格,第五个字符开始是所要求的文件路径
210         if(strstr(buf, "GET") != buf)   // 协议的开始说明取得文件的方式“GET”
211         {
212                 DEBUG("wrong request\n");
213                 return -1;
214         }
215         // 若没有指定文件名,则使用默认文件index.html
216         if(buf[4]=='/' && buf[5]==' ')
217                 strcat(path, "/index.html");
218         else
219         {
220                 strtok(&buf[4], " ");   // strtok 分割字符串
221                 strcat(path, &buf[4]);  // strcat 连接字符串
222         }
223         return 0;
224 }
225 
226 int error_page(int sockfd)
227 {
228         char err_str[BUFFSIZE];
229         #ifdef DEBUG_PRINT      // 调试时,用于向客户端输出出错信息
230                 sprintf(err_str, "HTTP/1.1 404 %s\r\n", strerror(errno));
231         #else   // 发布版,则不输出具体出错信息
232                 sprintf(err_str, "HTTP/1.1 404 Not Exsit\r\n");
233         #endif
234         // http协议头第一行
235         write(sockfd, err_str, strlen(err_str));
236         // 协议头第2行,说明页面的类型。只输出出错信息,所以页面类型为文本类型text/html
237         write(sockfd, "Content-Type: text/html\r\n\r\n", strlen("Content-Type: text/html\r\n\r\n"));
238         // 输出html内容
239         write(sockfd, "<html><body> the file dose not exsit </body></html>",
240                         strlen("<html><body> the file not exist </body></html>"));
241         // http协议的每一行以\r\n结尾,整个协议的结尾还有额外的一行\r\n
242         return 0;
243 }
244 // 向客户端输出需要的页面
245 // 将文件内容发送给客户端,同样,服务器程序需要为该文件添加HTTP协议头
246 // connfd: 连接套接字   path: 文件的完整路径
247 int write_page(int connfd, char *path, int fd)
248 {
249         int len = strlen(path);
250         char buf[BUFFSIZE];
251         // 协议头的第一行
252         write(connfd, "HTTP/1.1 200 OK\r\n", strlen("HTTP/1.1 200 OK\r\n"));
253         // 协议头的第2行,页面类型。需要根据文件的扩展名来进行判断
254         write(connfd, "Content-Type: ", strlen("Content-Type: "));
255         // 三种图片格式
256         if(strcasecmp(&path[len-3], "jpg")==0 || strcasecmp(&path[len-4], "jpeg")==0)
257                 write(connfd, "image/jpeg", strlen("image/jpeg"));
258         else if(strcasecmp(&path[len-3], "gif") == 0)
259                 write(connfd, "image/gif", strlen("image/gif"));
260         else if(strcasecmp(&path[len]-3, "png") == 0)
261                 write(connfd, "image/png", strlen("image/png"));
262         else
263                 write(connfd, "text/html", strlen("text/html"));
264         // 添加协议尾,最后需多出一个\r\n空行
265         write(connfd, "\r\n\r\n", 4);
266 //      fd = open(path, O_RDONLY);
267         while((len=read(fd, buf, BUFFSIZE)) > 0)
268                 write(connfd, buf, len);
269 
270         return 0;
271 }
272 
273 
274 
275 
276 # FILE: Makefile
277 # DATE: 20180205
278 # ==============
279 
280 OBJECTS = common.h web_server.c main.c
281 
282 all: build
283 
284 build: $(OBJECTS)
285         gcc -o build $(OBJECTS)
286 
287 .PHONY: clean
288 clean:
289         rm *.o
290 
291 
292 # FILE: config.ini
293 # DATE: 20180205
294 # ===============
295 
296 port: 8000
297 root-path: /home/admin
298 
299 
300 # FILE: index.html
301 # DATE: 20180205
302 # ===============
303 
304 <html>
305 <head>
306 <title>This is a test</title>
307 <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
308 <link rel="icon" href="png_favicon.png" type="image/png" >
309 </head>
310 <body>
311         <p>successfully communicate</p>
312         <img src='test.jpg'>
313 </body>
314 </html>


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章