基于TCP协议
HTTP协议解析请求,构造响应
用命令行参数来传入参数
一.void HttpServerStart(const char* ip,short port )来启动服务器
分为如下几步
1.创建socket
2.绑定端口号
3.监听
4.进入事件循环,当每次有新的连接的时候创建一个新线程,使accept被快速调用到,调用越频繁的话性能越高
在线程入口函数ThreadEntry()中,对HTTP协议解析请求,构造相应,完成主体的业务逻辑
分为如下步骤
1.解析请求
(1)按行读socket读出HTTP请求的首行,分别解析方法,和URL,注意的从socket中读取一行数据,一个一个字符的读,如果当前字符是航分割符
。就认为这一行读完了,而不同的浏览器的航分割符不尽相同,有\n \r \r\n 做法是把他们都归结为\n处理,上述函数实现的行为是读出的>一行末尾带\n,int ReadLine(int new_sock,char line[])
(2)解析首行,把空格换成\0.用一组指针记录切分出的每个部分。应切出三个部分。分别为方法,URL,版本号 ParseFirstLine(char first_line[],char ** p_method,char ** p_url)
(3)解析URL,以问好为分割符左边就是url_path右边就是query_string ,int ParseUrl(char url[],char ** p_url_path,char ** p_query_string )
(4) 解析Header,在我的程序里只处理了Centend_length,注意当遇到了\n 说明处理完成
2.构造响应
(1) 处理页面找不到void Handler404(int new_sock) 并发出
(2)处理静态页面 (本质是在url_path中存在的页面,和服务其上的这个路经是匹配的,注意这个路径看起来是一个绝对路径,但其实是一个相
对路径,是相对于HTTP服务器的根路径,把允许对外访问的文件集中放入某个目录下,作为服务器的跟目录,我的服务器使用./wwwroot 作为根目
录,所以可以看到url_path=>/index.html其实访问的就是./wwwroot/index.html) 并发出int HandlerStaticFile(const HttpRequest* req>,int new_sock)
((1))拼接目录(根据url_path构造出当前文件在文件系统上的真实路径,,如果用户传的是一个目录,那么会有这个目录下的默认文件供人访
>问,这时需要用系统提供的API,也就造成了不可跨平台的问题。)GetFilePath (const char*url_path,char file_path[])
((2))打开文件,读取文件内容,根据文件内容构造HTTP响应,将相应内容写入sock中。其中文件的内容就叫做HTTP相应的body部分,这时需要
先把数据从内核到用户(读),又从用户到内核(写),系统开销较大,于是我们用ssize_t sendfile (int out_fd,int in_fd,off_t *offset,size_t count)来优化,这个函数的操作全都在内核中,这里out_fd必须是socket,当我们发现乱码时大部分都是编解码不一致造成的,只是>可以打开浏览器随意找一个包,把这个包中header部分的编解码信息加入我们要发送的信息中。另外说明的一点是当客户端发起一次静态页面的请
求时,浏览器不一定只发一次请求,如果发现还有链接信息时,或其他路径的信息时,会继续发起请求,比如我的服务器中请求静态页面时就会发
起两次请求,一次显示Hello world,一次显示猫的图片,WriteStaticFile(const char * file_path,int new_sock) ,写成功返回200,写失
败返回404
(3)处理动态页面 并发出(使用CGI技术.这时一个协议)
协议的内容是
((1))HTTP服务器需要创建子进程
((2))子进程进行程序替换到磁盘上的某个可执行程序,这里需要把HTTP的请求方法,query_string(GET) ,body(POST),因为要进行
程序替换,程序替换虽然不改变PCB,但是要替换了代码段数据段,堆栈上内存从新分配,所以前两个要用环境变量出入子进程,而body用匿名管>道传输。最后要把构造好的动态页面通过匿名管道传回子进程。因为管道是单向通信所以要用两个管道,又因为创建管道其实也就是要创建两个变>量,程序替换之后,也就找不到了,但是管道的文件描述符还是在的,为了可以不通过这两个已经访问不到的变量访问文件,我们在进行程序替换
之前,把输入输出定向导管道上。
也就是说在这一步应做的是如下几点
((1))先创建一对匿名管道
((2))创建子进程
((3))父进程执行父进程的相关逻辑(在最开始关掉不用的文件描述符)
int HanderCGIFather(const HttpRequest* req,int new_sock,int father_read,int father_write)
(((1)))父进程把HTTP请求的body部分写到管道中
(((2)))父进程尝试读取子进程(进程等待)
(((3)))父进程构造HTTP响应,写会客户端
( ( (4) ) )父进程回收子进程
((4))子进程执行子进程的相关逻辑(在最开始关掉不用的文件描述符)
(((1)))设定环境变量,只可在子进程中设置(子进程的环境变量相互独立)。
(((2)))进行重定向,标准输入输出重定向到管道上。
(((3)))根据url_path构造出CGI程序的路径。
(((4)))进行程序替换了(一旦进行程序替换,代码就执行到了CGI程序内部,也就是进入了CGI程序的内部)
3.把相应返回到客户端上
一般用sprintf ()函数构造字符串
这个项目中我将会做一个计算器来测试我的动态页面,其中提交数据是静态页面,返回结果为动态页面
后面是这个自制服务器的缺点;
CGI经典协议的问题在于性能问题。核心在于CGI协议中要求创建子进程。
改进方法,基于线程池的思想出现了一个改进版本FastCGI当前主流的HTTP服务器和应用程序交互的协议之一。
下面是我的代码,为了让大家看清楚服务器的根路径与虚拟机的根路径,我会把每个源代码文件的路径都加上
/home/a/Desktop/Mycode/http_server/http_server.c
1 #include <stdio.h>
2 #include <string.h>
3 #include <pthread.h>
4 #include <stdlib.h>
5 #include <unistd.h>
6 #include <sys/socket.h>
7 #include <netinet/in.h>
8 #include <arpa/inet.h>
9 #include <sys/stat.h>
10 #include <fcntl.h>
11 #include <sys/sendfile.h>
12 #include <sys/wait.h>
13 #include "http_server.h"
14
15
16 typedef struct sockaddr sockaddr;
17 typedef struct sockaddr_in sockaddr_in ;
18
19
20
21 //一次从socket中读取以行数据
22 //把数据放在buf缓冲区中
23 //如果读取失败,返回值就是-1;
24 //遇到\n或\r\n或\r证明读取完成
25 int ReadLine(int sock,char buf[],ssize_t size){
26 //1.从socket中一次读取一个字符
27 char c='\0';
28 ssize_t i=0;//当前读了多少个字符
29 //结束条件;
30 //a)读到的长度太长,达到了缓冲区的上限
31 //b)读到了结束标志,并把所有结束标字都转换成\n
32
33 while(i<size-1&&c!='\n'){
34 ssize_t read_size=recv(sock,&c,1,0);
35 //读取失败有两种
36 if(read_size<0){
37 return -1;
38 }
39 if(read_size==0){
40 //因为预期是要读到\n这样的换行符,
41 //结果还没有读得到就先读了EOF,这种情况我们也暂时认为是失败的
42 return -1;
43 }
44 if(c=='\r')
45 {
46 //当前遇到了\r,但还需要确定下一个字符是不是\n
47 //MSG_PEEK选项从内核的缓冲区中读取数据,
48 //但是读到的数据不会从缓冲区中删除掉。
49 recv(sock,&c,1,MSG_PEEK);
50 if (c=='\n'){
51 //此时的分隔符就是\r\n
52 recv(sock,&c,1,0);
53 }else
54 {
55 //当前分隔符确定是\r,此时把分隔符转换成\n
56 c='\n';
57 }
58 }
59 //只要上面的c读到的是\r,那么if结束后,c都变成了\n
60 //这种方式就是把前面的\r和\r\n两种情况都统一成了\n
61 buf[i++]=c;
62
63 }
64 buf [i]='\0';
65 // printf ("ReadLine\n");
66 return i;//真正想缓冲区中放置的字符的个数
67 }
68
69
70 int Split(char input[], const char *split_char,char* output[],int output_size)
71 {
72 //使用strtok
73 int i=0;
74 char *tmp=NULL;//保存上次的切分结果
75 char * pch;
76 //使用线程安全的strtok_r代替strtok
77 //这一点是以后非常容易出错的一点
78 pch = strtok_r (input,split_char,&tmp);
79 while (pch != NULL)
80 {
81 if (i>=output_size)
82 {
83 return i;
84 }
85 output[i++]=pch;
86 pch = strtok_r (NULL, split_char,&tmp);
87 }
88
89 //printf ("Split\n");
90 return i;
91
92 }
93
94 int ParseFirstLine(char first_line[],char**p_url,char **p_method)
95 {
96 //把首行按空格进行字符串切分
97 //切分得到的每一个部分,就放在tok数组里
98 //返回值,就是tok数组包含几个元素。
99
100 char *tok[10];
101 //最后一个参数10表示tok数组中最多可以放几个元素
102 int tok_size=Split(first_line," ",tok,10);
103 if (tok_size!=3)
104 {
105 printf ("Split failed! tok_size =%d\n",tok_size);
106 return -1;
107 }
108 *p_method=tok[0];
109 *p_url=tok[1];
110
111 // printf ("ParseFirstLine\n");
112 return 0;
113 }
114
115
116
117 int ParseQueryString (char *url,char **p_url_path,char **p_query_string )
118 {
119 *p_url_path=url;
120 char *p=url;
121 for (;*p!='\0';++p)
122 {
123 if (*p=='?')
124 {
125 *p='\0';
126 *p_query_string =p+1;
127 return 0;
128 }
129 }
130 //循环结束都没有找到?,说明这个请求不带query_string
131 *p_query_string=NULL;
132 // printf ("ParseQueryString\n");
133 return 0;
134 }
135
136
137
138 int ParseHeader (int sock ,int *content_length)
139 {
140 //1.循环从socket中读取一行。
141 //2.判断当前行是不是Content—Length
142 //3.如果是Content-Length就直接把value读出来
143 //4.如果不是就直接丢弃
144 //5.读到空行,循环结束。
145 char buf [SIZE]={0};
146 while (1)
147 {
148 //1.循环从socket中读取一行。
149 ssize_t read_size=ReadLine (sock,buf,sizeof (buf));
150 //处理读失败的情况
151 if (read_size<=0)
152 {
153 return -1;
154 }
155 //处理读完的情况
156 if (strcmp (buf ,"\n")==0)
157 {
158 return 0;
159 }
160 //2.判定当前行是不是Content-Length
161 //如果是Content-Length就直接把value读出来
162 //如果不是就直接丢弃
163 const char *content_length_str="Content-Length: ";
164 if (content_length!=NULL&&strncmp(buf,content_length_str,strlen(content_length_str))==0)
165 {
166 *content_length=atoi(buf+strlen(content_length_str));
167
168 }
169
170
171 }
172 return 0;
173 //读到空行循环结束
174 }
175
176
177
178 void Handler404(int sock)
179 {
180 //构造一个完整的HTTP响应
181 //状态码是404
182 //body部分也应该是一个404相关的错误页面。
183 const char* first_line="HTTP/1.1 404 Not Found\n";
184 const char* type_line="Content-Type: text/html;charset=utf-8\n";//提示浏览器按照utf-8方式解码于下面成双重保证
185 const char * blank_line="\n";
186 const char * html="<head><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"></head>"
187 "<h1>您的页面被喵星人吃掉了!!!</h1>";//提示浏览器按照utf8方式解码
188 send (sock,first_line ,strlen(first_line),0);
189 send (sock,type_line ,strlen(type_line),0);
190 send (sock,blank_line ,strlen(blank_line),0);
191 send (sock,html ,strlen(html),0);
192 return;
193 }
194
195
196
197 void PrintRequest (Request*req)
198 {
199 printf ("method: %s\n",req->method);
200 printf ("url_path: %s\n",req->url_path);
201 printf ("query_string: %s\n",req->query_string);
202 printf ("content_length: %d\n",req->content_length);
203 return;
204 }
205
206
207
208 int IsDir(const char *file_path)
209 {
210 struct stat st;
211 int ret=stat(file_path,&st);
212 if (ret<0)
213 {
214 return 0;
215 }
216 if (S_ISDIR(st.st_mode))
217 {
218 return 1;
219 }
220 return 0;
221 }
222
223
224 void HandlerFilePath(const char*url_path,char file_path[])
225 {
226 //(a,给url_path 加上前缀名(HTTP服务器的根目录)
227 //url_path-->/index.html
228 //file_path--->./wwwroot/index.html
229 sprintf (file_path,"./wwwroot%s",url_path);
230 //b)例如url_path是/,此时url_path 其实就是一个目录。
231 //如果是目录的话,就演给这个目录之中追加一个index.html
232 //url_path /或者 /image/
233 if (file_path[strlen(file_path)-1]=='/')
234 {
235 strcat(file_path,"index.html");
236 }
237 //url_path=>/image
238 if (IsDir(file_path))
239 {
240 strcat (file_path,"/index.html");
241 }
242 return ;
243
244 }
245
246
247
248 ssize_t GetFileSize(const char* file_path)
249 {
250 struct stat st;
251 int ret=stat(file_path,&st);
252 if (ret<0)
253 {
254 //打开文件失败,很可能是文件不存在
255 //此时直接返回文件长度为0
256 return 0;
257 }
258 return st.st_size;
259 }
260
261
262
263
264 int WriteStaticFile (int sock,const char* file_path)
265 {
266 //1.打开文件
267 int fd=open (file_path,O_RDONLY);
268 if (fd<0)
269 {
270 //文件描述符不够用
271 //文件不存在(404)不好意思找不到
272 perror ("open");
273 return 404;
274 }
275 //2.把构造出来的HTTP响应写到socket之中
276 //1).写入首行
277 //2).写入head
278 //3).写入空行
279 //4).写入body(文件内容)
280 const char * first_line="HTTP/1.1 200 OK\n";
281 send (sock,first_line,strlen(first_line ),0);
282 //const char* type_line="Content-Type: text/html;charset=utf-8\n";//提示浏览器按照utf-8方式解码于下面成双重保证
283 //const char* type_line="Content-Type: image/png;charset=utf-8\n";//提示浏览器按照utf-8方式解码于下面成双重保证
284 //send (sock,type_line ,strlen(type_line),0);
285 //两个都不添,让浏览器自己决定
286 const char * blank_line="\n";
287 send (sock,blank_line ,strlen(blank_line),0);
288 /*ssize_t file_size=GetFileSize(file_path);
289 ssize_t i=0;
290 for(;i<file_size;i++)
291 {
292 char c;
293 read(fd,&c,1);
294 send (sock,&c,1,0);
295 }
296 */
297 sendfile(sock,fd,NULL,GetFileSize(file_path));
298 //3.关闭文件
299 close (fd);
300 return 200;
301 }
302
303
304
305 int HandlerStaticFile (int sock ,Request *req)
306 {
307 //1根据url_path获取到文件在服务器上的真实路径。
308 char file_path[SIZE]={0};
309 HandlerFilePath(req->url_path,file_path);
310 //2。读取文件,把文件的内容直接写道socket之中
311 int err_code=WriteStaticFile (sock,file_path);
312 return err_code;
313 }
314
315 int HandlerCGIFather(int new_sock,int father_read,int father_write,int child_pid,Request *req){
316 //1如果是POST请求,就把body写到管道中
317 if (strcasecmp(req->method,"POST")==0){
318 int i=0;
319 char c='\0';
320 for (;i<req->content_length;++i){
321 read(new_sock,&c,1);
322 write(father_write,&c,1);
323 }
324 }
325 //2构造HTTP响应
326 const char * first_line="HTTP/1.1 200 OK\n";
327 send (new_sock,first_line,strlen(first_line ),0);
328 const char* type_line="Content-Type: text/html;charset=utf-8\n";//提示浏览器按照utf-8方式解码于下面成双重保证
329 //const char* type_line="Content-Type: image/png;charset=utf-8\n";//提示浏览器按照utf-8方式解码于下面成双重保证
330 send (new_sock,type_line ,strlen(type_line),0);
331 //两个都可以不添(长度和类型),让浏览器自己决定
332 const char * blank_line="\n";
333 send (new_sock,blank_line ,strlen(blank_line),0);
334 //循环的从管道中读取数据并写入数据到socket
335 char c='\0';
336 while (read(father_read,&c,1)>0){
337 //从管道中读数据,如果(父进程,子进程)所有写端关闭,read将会读到EOF,从而返回0
338 send(new_sock,&c,1,0);
339 }
340 //4回收子进程的资源
341 waitpid(child_pid,NULL,0);
342 return 200;
343 }
344
345
346 int HandlerCGIChild(int child_read,int child_write,Request* req){
347 //1.设置必要的环境变量
348 char method_env[SIZE]={0};
349 sprintf (method_env,"REQUEST_METHOD=%s",req->method);
350 putenv(method_env);
351 //还需要设置QUERY_STRING或者是CONTENT_LENGTH
352 if(strcasecmp(req->method,"GET")==0){
353 char query_string_env[SIZE]={0};
354 sprintf (query_string_env,"QUERY_STRING=%s",req->query_string);
355 putenv(query_string_env);
356
357 }else {
358 char content_length_env[SIZE]={0};
359 sprintf (content_length_env,"CONTENT_LENGTH=%d",req->content_length);
360 putenv(content_length_env);
361
362 }
363 //2把标准输入输出重定向到管道里
364 dup2(child_read,0);
365 dup2(child_write,1);
366 //3对子进程进行程序替换
367 // url_path: /cgi-bin/test
368 // file_path: ./wwwroot/cgi-bin/test
369 char file_path[SIZE]={0};
370 HandlerFilePath(req->url_path,file_path);
371
372 execl(file_path,file_path,NULL);
373 exit(1);//当程序替换失败时。一定让子进程终止,如果子进程不终止,因为父子进程是同一块代码,父进程一直在Listen状态一直等待,子进程也会等待,所以要直接结束进程,避免一直等待端口数据的返回;
374 //有 exec l lp le v vp ve,首先看知不知道可执行文件的完整路径 ,如有就可以不带P,因为P是在PATH中找,再看要不要环境变量,如果不用,就不需要带e,这里因为通过putenv ()的方式直接设到了ENV里面所以不用,说一直接用execl()
375 return 200;
376 }
377
378
379
380 int HandlerCGI(int new_sock,Request *req)
381 {
382 int err_code=200;
383 //1.创建一对匿名管道
384 int fd1[2],fd2[2];
385 int ret =pipe(fd1);
386 if (ret<0)
387 {
388 return 404;
389 }
390 ret=pipe (fd2);
391 if (ret<0)
392 {
393 close(fd1[0]);
394 close(fd1[1]);
395 return 404;
396 }
397 //fd1,fd2这种变量名的描述性太差,后面直接用的话
398 //是非常容易弄混的,所以直接在此处定义几个
399 //更加明确的变量名来描述该文件描述符的用途
400 int father_read=fd1[0];
401 int child_write=fd1[1];
402 int father_write=fd2[1];
403 int child_read=fd2[0];
404 //2.创建子进程
405 ret=fork();
406 //3.父子进程各执行不同的逻辑
407 if (ret>0){
408
409 //father
410 //此处父进程优先关闭这两个管道的文件描述符
411 //是为了后续父进程从子进程这里读数据时,能够读到EOF,对于管道来说,所有写端关闭,继续读,才有EOF,而此时所有写端,一方面是父进程需要关闭,另一方面子进程也需要关闭。所以此处父进程先关闭不必要的写端之后,后续子进程用完了
412 //直接关闭,父进程也就读到了EOF
413 close (child_read);
414 close(child_write);
415 err_code=HandlerCGIFather(new_sock,father_read,father_write,ret,req);
416 close(father_read);
417 close(father_write);
418 }else if (ret==0){
419
420 //child
421 close (father_read);
422 close (father_write);
423 err_code=HandlerCGIChild(child_read,child_write,req);
424 //此处代码写上也执行不到,所以不用写close文件描述符
425
426 }else{
427 perror("fork");
428 close (fd1[0]);
429 close (fd1[1]);
430 close (fd2[0]);
431 close (fd2[1]);
432 return 404;
433 }
434 //4.收尾工作和错误处理
435 return 200;
436 }
437
438
439
440 void HandlerRequest(int new_sock)
441 {
442 int err_code=200;//错误码初始200,默认没错
443 //1.读取并解析请求(反序列化)
444 Request req;
445 memset (&req,0,sizeof (req));
446 //a)从socket中读取出首行
447 if (ReadLine(new_sock,req.first_line,sizeof (req.first_line))<0)
448 {
449 //失败处理
450 err_code =404;
451 goto END;
452 }
453 //b)解析首行,从首行中解析出url和method
454 if (ParseFirstLine(req.first_line,&req.url,&req.method))
455 {
456 //失败处理
457 err_code =404;
458 goto END;
459
460 }
461 //c)解析url,从url中解析出url_path和query_string
462 if (ParseQueryString(req.url,&req.url_path,&req.query_string))
463 {
464 //失败处理
465 err_code =404;
466 goto END;
467
468 }
469 //d)解析Header,丢弃了大部分header,只读取Content—Length
470 if (ParseHeader(new_sock,&req.content_length))
471 {
472 //失败处理
473 err_code =404;
474 goto END;
475
476 }
477
478
479 PrintRequest (&req);
480 //2.静态/动态方式生成页面
481 //3.把生成结果写回客户端上
482 if (strcasecmp(req.method,"GET")==0&&req.query_string==NULL)
483 {
484 //a)如果是GET方式请求,并且没有query_string,
485 //那么返回静态页面
486 err_code=HandlerStaticFile(new_sock,&req);
487 }else if (strcasecmp(req.method,"GET")==0&&req.query_string!=NULL)
488 {
489 //b)如果是GET方式请求,并且有query_string,
490 //那么返回动态页面
491 err_code=HandlerCGI(new_sock,&req);
492 }else if (strcasecmp(req.method,"POST")==0)
493 {
494 //c)如果请求是POST类型的(一定是带参的,参数是通过body来传给服务器的),那么也返回动态页面
495 err_code=HandlerCGI(new_sock,&req);
496 }else
497 {
498 //失败处理
499 err_code =404;
500 goto END;
501 }
502
503 //错误处理;直接返回一个404的HTTP响应
504 END:
505 if (err_code !=200)
506 {
507 Handler404(new_sock);
508 }
509 close(new_sock);
510 return;
511
512 }
513
514
515
516
517 void *ThreadEntry(void*arg)// 因为在64位机中指针占8个字节,如果强转回int会有丢失数据的危险
518 {
519 int64_t new_sock=(int64_t )arg;
520 //使用HandlerRequest完成具体的处理请求过程
521 //相当于线程入口函数只有一个包装,真正干活的是这个函数,这样最大的好处还是解耦和
522 //一旦需要把服务器改成多进程或者IO多路复用的形式
523 //代码的改动都是比较小的
524 HandlerRequest(new_sock);
525 return NULL;
526 }
527
528
529
530
531 //服务器启动
532 void HttpServerStart(const char* ip,short port)
533 {
534 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
535 if (listen_sock<0)
536 {
537 perror("socket");
538 return ;
539 }
540 int opt=1;
541 setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof (opt));//加上这个函数是为了使端口处于TIME-WAIT时复用地址
542 sockaddr_in addr;
543 addr.sin_family=AF_INET;
544 addr.sin_addr.s_addr=inet_addr(ip);
545 addr.sin_port=htons(port);
546
547 int ret=bind(listen_sock,(sockaddr*)&addr,sizeof (addr));
548 if (ret<0)
549 {
550 perror("bind");
551 return ;
552 }
553 ret=listen(listen_sock,5);
554 if (ret<0)
555 {
556 perror("listen");
557 return ;
558 }
559 printf ("ServerInit OK\n");
560 while (1)
561 {
562
563 sockaddr_in peer;
564 socklen_t len=sizeof (peer);
565 printf ("3\n");
566 int64_t new_sock=accept(listen_sock,(sockaddr*)&peer,&len);
567 printf ("2\n");
568 if (new_sock<0)
569 {
570 perror("accept");
571 continue;
572 }
573 //使用多线程的方式来实现TCP服务器
574 pthread_t tid;
575 printf ("ThreadEntry\n");
576 pthread_create(&tid,NULL,ThreadEntry,(void *)new_sock);
577 pthread_detach(tid);
578 }
579
580 }
581
582
583
584
585 //主函数的参数 ./http_server [ip] [port]
586 int main (int argc,char* argv[])
587 {
588 if (argc!=3)
589 {
590 printf ("Usage ./http_server [ip] [port]\n");
591 return 1;
592 }
593 HttpServerStart( argv[1],atoi(argv[2]));
594 return 0;
595 }
/home/a/Desktop/Mycode/http_server/http_server.h
1 #pragma once
2
3 #define SIZE 10240
4 //我们需要一个结构体来解析http协议请求
5 typedef struct Request
6 {
7 //解析首行
8 char first_line[SIZE];
9 char *method;// [SIZE];//方法
10 char *url;//[SIZE]; //URL
11 //char *version;//[SIZE];//版本号,在这里我们暂时用不上
12 char *url_path;//带层次的文件路径,重点关注的对象内容1
13 char *query_string;//查询字符串,重点关注的内容2
14 //接下来解析header部分。
15 //此处需要使用二叉搜索树或hash表
16 //这里我们偷懒一下,其他的header都不要了,只保留一个Content——Length
17 int content_length;
18 }Request;
19 /*typedef struct Request
20 {
21 char first_line[SIZE];//首行
22 char method [SIZE];//方法
23 char url[SIZE]; //URL
24 char version[SIZE];//版本号
25 char url_path[SIZE];//带层次的文件路径
26 char query_string[SIZE];//查询字符串
27 }Request;
28 */
/home/a/Desktop/Mycode/http_server/Makefile
1 http_server:http_server.c
2 gcc $^ -o $@ -lpthread
3
4
5 .PHONY:clean
6 clean:
7 rm http_server
/home/a/Desktop/Mycode/http_server/wwwroot/index.html
<h1>Hello world</h1>
<img src="/image/1.png">
/home/a/Desktop/Mycode/http_server/wwwroot/Makefile
select:select.c
gcc $^ -o $@ -L /usr/lib64/mysql -lmysqlclient
.PHONY:clean
clean:
rm select
/home/a/Desktop/Mycode/http_server2/wwwroot/calc/calc.c
1 //实现一个用来简单计算的CGI程序
2
3
4
5
6
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <unistd.h>
10 #include <string.h>
11 #define SIZE (1024*10)
12
13
14 void GetQueryString(char output[]){
15 //按照CGI的协议来实现此处的逻辑
16 //1.先获取到方法
17 char* method=getenv("REQUEST_METHOD");
18 if (method==NULL){
19 //没有获取到环境变量
20 fprintf(stderr,"REQUEST_METHOD filed\n");
21 return ;
22 }
23 if (strcmp(method,"GET")==0){
24 //获取QUERY_STRING
25 char *query_string =getenv("QUERY_STRING");
26 if (query_string==NULL){
27 fprintf(stderr,"QUERY_STRING failed\n");
28 return ;
29 }
30 strcpy(output,query_string);
31 }else{
32 //post
33 //获取CONTENT_LENGTH
34 char* content_length_env=getenv("CONTENT_LENGTH");
35 if (content_length_env==NULL){
36 fprintf(stderr,"CENTENT_LENGTH failed\n");
37 return ;
38 }
39 int content_length=atoi(content_length_env);
40 int i=0;
41 for (;i<content_length;++i){
42 char c='\0';
43 read(0,&c,1);
44 output[i]=c;
45 }
46 return ;
47 }
48 }
49
50 int main (){
51 //1.基于CGI协议获取到需要的参数
52 char query_string[SIZE]={0};
53 GetQueryString(query_string);
54 //2.根据业务逻辑进行计算
55 //此时时获取到的QUERY_STRING的形式如下
56 //a=10&b=20
57 int a=0;
58 int b=0;
59 sscanf(query_string ,"a=%d&b=%d\n",&a,&b);
60 int sum =a+b;
61 //3.把结果构造成HTML写回到标准输出
62 printf ("<html><h1>sum=%d</h1></html>",sum);
63 return 0;
64 }
/home/a/Desktop/Mycode/http_server2/wwwroot/calc/index.html
4 <html>
5 <form action="/calc/calc"method="POST">
6 a:<br>
7 <input type="text" name="a" >
8 <br>
9 b:<br>
10 <input type="text" name="b" >
11 <br><br>
12 <input type="submit" value="Submit">
13 </form>
14 </html>
/home/a/Desktop/Mycode/http_server2/wwwroot/calc/Makefile
1 calc:calc.c
2 gcc $^ -o $@
3
4 .PHONY:clean
5 clean:
6 rm calc
/home/a/Desktop/Mycode/http_server2/wwwroot/image/1.png 这是一张图片
/home/a/Desktop/Mycode/http_server2/wwwroot/mysql/select.c
1 //本程序基于MySQL API完成对数据库的查询操作
2 //不管用户输入什么,都尝试把TestTable这个表里的所有数据都查询出来,展示到以页面上
3 //注意这个CGI存在的问题是,如果访问时不加参数会当成要下载的静态页面,所以访问时应该在后面加上一个参数虽然我们不用,原因是我们前面判断是动态页面还是静态页面的///根据以下步骤1,先看方法,Get方法的QUERY_STRING是否为空,为空时静态页面> ,不为空时,是动态页面,post请求直接是动态页面
4 //改进通过stat函数判断是否是可执行程序。
5
6
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <mysql/mysql.h>
10
11
12 int main (){
13 //1.获取到CGI程序需要的相关参数(此处可以不获取)
14 //2.调用MySQL API 来访问和操作数据库
15 //(a)创建一个MySQL的连接句柄
16 MYSQL * connect_fd=mysql_init(NULL);
17 //(b)和数据库服务器建立连接
18 if (mysql_real_connect(connect_fd,"192.168.0.104","root"," ","TestDB",3306,NULL,0)==NULL){
19 fprintf(stderr,"mysql_real_connect failed\n");
20 return 1;
21 }
22 fprintf(stderr,"mysql_real_connect ok\n");
23 //(c)构造SQL语句,此处不需要加分号,但在SQL语句时是要加的
24 const char* sql="select *from TestTable";
25 //(d)把构造的SQL语句发上给Mysql 服务器,并执行
26 int ret=mysql_query(connect_fd,sql);
27 if (ret<0){
28 fprintf(stderr,"mysql_query failed\n");
29 return 1;
30 }
31 //(e)解析和遍历返回结果()结果是一个表格
32 MYSQL_RES* result=mysql_store_result(connect_fd);
33 //先获取行数和列数
34 int rows=mysql_num_rows(result);
35 int fields=mysql_num_fields(result);
36 //获取表头(每列的意思)
37 printf(<html>\n);
38 printf("rows=%d,fields=%d<br>\n",rows,fields);
39 MYSQL_FIELD* field=mysql_fetch_field(result);//一次只取一列的表头
40 while (field!=NULL){
41 printf("%s\t",field->name );
42 field=mysql_fetch_field(result);//取下一列的表头
43 }
44 printf("<br>");
45 //按行获取表的具体内容
46 int i=0;
47 for (;i<rows;i++){
48 MYSQL_ROW row=mysql_fetch_row(result);//每调用一次就取一行
49 int j=0;
50 for (;j<fields;++j){
51 printf("%s\t",row[j]);
52 }
53 printf ("<br>");
54 }
55 printf("</html>\n");
56 //(f)断开连接
57 mysql_close(connect_fd);
58 //3。把获取的数据组织成html 返回给客户端
59 return ;
60 }
基于HTTP服务器实现的业务:
(1)用户通过浏览器网页(静态页面)提供给服务器一个日期和一个天数(N)。
(2)服务器会对日期和天数进行计算(动态页面),计算出这个日期N天后的日期。
(3)通过浏览器返回,以便用户更合理的安排时间。
日期类我写在了另一篇博客https://blog.csdn.net/a15929748502/article/details/81369350欢迎查阅
下面是我的测试;
及结果: