├── Makefile ├── README ├── README.md ├── htdocs ├── README ├── check.cgi ├── color.cgi ├── index.html └── static │ └── index.html ├── httpd ├── httpd.c └── simpleclient.c /Makefile: -------------------------------------------------------------------------------- 1 | all: httpd 2 | 3 | httpd: httpd.c 4 | #gcc -W -Wall -lpthread -o httpd httpd.c 5 | # -W -WALL选项配合使用,显示编译时警告信息的 6 | gcc -W -Wall -o httpd httpd.c 7 | 8 | clean: 9 | rm httpd 10 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TinyHttpd - 对原版加了一些修改和bug修复 2 | =================================== 3 | 4 | 实现一个webServer要做的事: 5 | 6 | 1、建立一条TCP通道(startup,accept_request) 7 | 8 | 2、能够解析HTTP协议包,包括消息头解析和消息体解析(get_line) 9 | 10 | 3、能够响应符合HTTP规范的包(bad_request,headers,not_found,unimplemented) 11 | 12 | 4、处理静态请求(serve_file,cat) 13 | 14 | 5、处理动态请求(execute_cgi,cannot_execute) 15 | 16 | 17 | 修改 18 | ------------ 19 | 1、添加函数readBufferBeforeSend,这个函数通过判断请求方法是GET或POST来进行相应的head和body的读取。原版中,当返回数据时,要判断请求方法是GET就先读出全部head并丢弃后返回数据,是POST就从http head中读出CONTENT-LENGTH,然后按这个长度读取body后再返回数据,我给整合成一个方法,并且用这个方法修复了一个bug。 20 | 21 | 2、修改main函数中u_short port = 8008,对于测试来讲,并不希望web的端口通过系统分配。 22 | 23 | 3、在startup函数中加入 24 | ``` 25 | int reuse = 1; 26 | if (setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) { 27 | error_die("setsockopet"); 28 | } 29 | ``` 30 | 在测试中,经常需要修改源码,改完后重新编译再启动会报错: 31 | ``` 32 | bind: Address already in use 33 | ``` 34 | 但是httpd是已经关闭了的,往往过一会再启动就没事了。 35 | 因为TCP协议在四次挥手中主动关闭方会进入TIME_WAIT状态,并且会等待2MSL的时间,而服务端的重启和崩溃都会进入这个状态。这里通过重用TCP连接解决。 36 | 37 | 38 | 39 | 修复bug 40 | ------------------------- 41 | 1、原版中,在接收请求(accept_request)后,通过get_line解析请求行,发现不是GET或POST方法后,就调用unimplemented方法,通知客户端方法没有实现,但是在返回前没有读取tcp通道中的stream数据,这样客户端(可以用postman测试)就得不到响应,通过在调用unimplemented方法前添加readBufferBeforeSend方法,并在其后调用close关闭连接描述符, 42 | 43 | 44 | 45 | License 46 | ------- 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /htdocs/README: -------------------------------------------------------------------------------- 1 | These are sample CGI scripts and webpages for tinyhttpd. They can 2 | be redistributed under the terms of the GPL. 3 | 4 | The most impressive demonstration I gave of tinyhttpd to my 5 | professor and my classmates was to load color.cgi with a value of 6 | "chartreuse." :) It's actually a very simple script, guys. 7 | 8 | jdb 9 | -------------------------------------------------------------------------------- /htdocs/check.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -Tw 2 | 3 | use strict; 4 | use CGI; 5 | #use Data::Dumper; 6 | 7 | my($cgi) = new CGI; 8 | 9 | print $cgi->header('text/html'); 10 | print $cgi->start_html(-title => "Example CGI script", 11 | -BGCOLOR => 'red'); 12 | print $cgi->h1("CGI Example"); 13 | print $cgi->p, "This is an example of CGI\n"; 14 | print $cgi->p, "Parameters given to this script:\n"; 15 | print ""; 21 | 22 | print $cgi->end_html, "\n"; 23 | -------------------------------------------------------------------------------- /htdocs/color.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -Tw 2 | 3 | use strict; 4 | use CGI; 5 | 6 | my($cgi) = new CGI; 7 | 8 | print $cgi->header; 9 | my($color) = "blue"; 10 | $color = $cgi->param('color') if defined $cgi->param('color'); 11 | 12 | print $cgi->start_html(-title => uc($color), 13 | -BGCOLOR => $color); 14 | print $cgi->h1("This is $color"); 15 | print $cgi->end_html; 16 | -------------------------------------------------------------------------------- /htdocs/index.html: -------------------------------------------------------------------------------- 1 | 2 | Index 3 | 4 |

Welcome to J. David's webserver. 5 |

CGI demo 6 |
7 | Enter a color: 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /htdocs/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | Static 3 | 4 |

这是一个静态页面

5 | 6 | 7 | -------------------------------------------------------------------------------- /httpd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxeth/tiny-httpd/6c99251db000275b4731bb49abc36e95ad620277/httpd -------------------------------------------------------------------------------- /httpd.c: -------------------------------------------------------------------------------- 1 | /* J. David's webserver */ 2 | /* This is a simple webserver. 3 | * Created November 1999 by J. David Blackstone. 4 | * CSE 4344 (Network concepts), Prof. Zeigler 5 | * University of Texas at Arlington 6 | */ 7 | /* This program compiles for Sparc Solaris 2.6. 8 | * To compile for Linux: 9 | * 1) Comment out the #include line. 10 | * 2) Comment out the line that defines the variable newthread. 11 | * 3) Comment out the two lines that run pthread_create(). 12 | * 4) Uncomment the line that runs accept_request(). 13 | * 5) Remove -lsocket from the Makefile. 14 | */ 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | //#include 27 | #include 28 | #include 29 | 30 | //C库函数int isspace(int c),检查传递的字符是否是空白。也就是判断是否为空格(' ')、 31 | //定位字符(' \t ')、CR(' \r ')、换行(' \n ')、垂直定位字符(' \v ')或翻页(' \f ')的情况。 32 | //若参数c 为空白字符,则返回非 0,否则返回 0。 33 | #define ISspace(x) isspace((int)(x)) 34 | 35 | #define SERVER_STRING "Server: lzx-tiny-httpd/0.1.0\r\n" 36 | 37 | void accept_request(int); 38 | void bad_request(int); 39 | void cat(int, FILE *); 40 | void cannot_execute(int); 41 | void error_die(const char *); 42 | void execute_cgi(int, const char *, const char *, const char *); 43 | int get_line(int, char *, int); 44 | void headers(int, const char *); 45 | void not_found(int); 46 | void serve_file(int, const char *); 47 | int startup(u_short *); 48 | void unimplemented(int); 49 | void readBufferBeforeSend(int); 50 | 51 | /**********************************************************************/ 52 | /* A request has caused a call to accept() on the server port to 53 | * return. Process the request appropriately. 54 | * Parameters: the socket connected to the connfd */ 55 | /**********************************************************************/ 56 | void accept_request(int connfd) 57 | { 58 | char buf[1024]; //缓冲从socket中读取的字节 59 | int numchars; //读取字节数 60 | char method[255]; //请求方法 61 | char url[255]; //请求的url,包括参数 62 | char path[512]; //文件路径,不包括参数 63 | size_t i, j; 64 | struct stat st; 65 | int cgi = 0; /* 如果确定是cgi请求需要把这个变量置为1 */ 66 | char *query_string = NULL; //参数 67 | 68 | //从socket中读取一行数据 69 | //这里就是读取请求行(GET /index.html HTTP/1.1),行数据放到buf中,长度返回给numchars 70 | numchars = get_line(connfd, buf, sizeof(buf)); 71 | 72 | i = 0; j = 0; //这里使用两个变量i,j因为后边i被重置为0了。j用来保持请求行的seek 73 | while (!ISspace(buf[j]) && (i < sizeof(method) - 1)) //小于method-1是因为最后一位要放\0 74 | { 75 | method[i] = buf[j]; //获取请求方法放入method 76 | i++; j++; 77 | } 78 | method[i] = '\0'; 79 | 80 | //如果请求的方法不是 GET 或 POST 任意一个的话就直接发送 response 告诉客户端没实现该方法 81 | //strcasecmp 忽略大小写 82 | if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) 83 | { 84 | //清除缓冲区消息头和消息体 85 | readBufferBeforeSend(connfd); 86 | 87 | unimplemented(connfd); 88 | 89 | close(connfd); //TODO 在调用没有实现的方法后没有关闭链接,这里要把connfd关闭 90 | 91 | return; 92 | } 93 | 94 | //如果是 POST 方法就将 cgi 标志变量置一(true) 95 | if (strcasecmp(method, "POST") == 0) { 96 | cgi = 1; 97 | } 98 | 99 | i = 0; 100 | //跳过所有的空白字符 101 | //此时buf里装的是请求行的内容(GET /index.html HTTP/1.1),而j的指针是读完GET之后的位置 102 | //所以跳过空格后获取的就是请求url了 103 | while (ISspace(buf[j]) && (j < sizeof(buf))) 104 | j++; 105 | 106 | //然后把 URL 读出来放到 url 数组中,url结尾要补充\0,所以长度要-1,buf已经存在了,判断长度只是避免越界 107 | while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) 108 | { 109 | url[i] = buf[j]; 110 | i++; j++; 111 | } 112 | url[i] = '\0'; 113 | 114 | //如果这个请求是一个 GET 方法的话 115 | //TODO 对于不是GET方法的请求其实也是需要解析query_string的, 116 | //TODO 否则对于POST:http://10.33.106.82:8008/check.cgi?name=foo 带参数的情况path解析是失败的。 117 | //TODO 但这里只是简单的区分GET和POST请求,这个就不考虑了吧 118 | if (strcasecmp(method, "GET") == 0) 119 | { 120 | //用一个指针指向 url 121 | query_string = url; 122 | 123 | //去遍历这个 url,跳过字符 ?前面的所有字符,如果遍历完毕也没找到字符 ?则退出循环 124 | while ((*query_string != '?') && (*query_string != '\0')) 125 | query_string++; 126 | 127 | //退出循环后检查当前的字符是 ?还是字符串(url)的结尾 128 | //因为如果存在?的话经过上边的循环query_string的指针正好在?处,如果没有?的话query_string指针正好在url字符串的结尾\0处 129 | if (*query_string == '?') 130 | { 131 | //如果是 ? 的话,证明这个请求需要调用 cgi,将cgi标志变量置一(true),因为请求静态文件不用参数的。 132 | cgi = 1; 133 | //从字符 ? 处把字符串 url 给分隔为两份 134 | *query_string = '\0'; //把url的?改为\0,这样url只是?的前边部分了。 135 | //使指针指向字符 ?后面的那个字符 136 | query_string++; 137 | } 138 | } 139 | 140 | //将url字符串(只包含url,参数已经被截断了),拼接在字符串htdocs的后面之后就输出存储到path中。 141 | //因为url的第一个字符是/,所以不用加/ 142 | sprintf(path, "htdocs%s", url); 143 | 144 | //如果 path 数组中的这个字符串的最后一个字符是以字符 / 结尾的话,就拼接上一个"index.html"的字符串。首页的意思 145 | if (path[strlen(path) - 1] == '/') { 146 | strcat(path, "index.html"); 147 | } 148 | 149 | //在系统上去查询该文件是否存在 150 | //int stat(const char * file_name, struct stat *buf); 151 | //stat()用来将参数file_name 所指的文件状态, 复制到参数buf 所指的结构中。执行成功则返回0,失败返回-1,错误代码存于errno。 152 | if (stat(path, &st) == -1) { 153 | //如果不存在,那把这次 http 的请求后续的内容(head 和 body)全部读完并忽略 154 | readBufferBeforeSend(connfd); 155 | 156 | //返回方法不存在 157 | not_found(connfd); 158 | } else { 159 | //文件存在,那去跟常量S_IFMT相与,相与之后的值可以用来判断该文件是什么类型的 160 | if ((st.st_mode & S_IFMT) == S_IFDIR) 161 | //如果这个文件是个目录,那就需要再在 path 后面拼接一个"/index.html"的字符串 162 | //注意此时访问需要http://10.33.106.82:8008/static/,后边不带/不能识别为目录 163 | strcat(path, "/index.html"); 164 | 165 | //S_IXUSR:用户可以执行 166 | // S_IXGRP:组可以执行 167 | // S_IXOTH:其它人可以执行 168 | //如果这个文件是一个可执行文件,不论是属于用户/组/其他这三者类型的,就将 cgi 标志变量置一 169 | if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH)) { 170 | cgi = 1; 171 | } 172 | 173 | if (!cgi) { 174 | //静态解析 175 | serve_file(connfd, path); 176 | } else { 177 | //CGI解析 178 | execute_cgi(connfd, path, method, query_string); 179 | } 180 | } 181 | 182 | close(connfd); 183 | } 184 | 185 | /**********************************************************************/ 186 | /* Inform the connfd that a request it has made has a problem. 187 | * Parameters: connfd socket */ 188 | /**********************************************************************/ 189 | void bad_request(int connfd) 190 | { 191 | char buf[1024]; 192 | 193 | sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n"); 194 | send(connfd, buf, sizeof(buf), 0); 195 | sprintf(buf, "Content-type: text/html\r\n"); 196 | send(connfd, buf, sizeof(buf), 0); 197 | sprintf(buf, "\r\n"); 198 | send(connfd, buf, sizeof(buf), 0); 199 | sprintf(buf, "

Your browser sent a bad request, "); 200 | send(connfd, buf, sizeof(buf), 0); 201 | sprintf(buf, "such as a POST without a Content-Length.\r\n"); 202 | send(connfd, buf, sizeof(buf), 0); 203 | } 204 | 205 | /**********************************************************************/ 206 | /* Put the entire contents of a file out on a socket. This function 207 | * is named after the UNIX "cat" command, because it might have been 208 | * easier just to do something like pipe, fork, and exec("cat"). 209 | * Parameters: the connfd socket descriptor 210 | * FILE pointer for the file to cat */ 211 | /**********************************************************************/ 212 | void cat(int connfd, FILE *resource) 213 | { 214 | char buf[1024]; 215 | 216 | //从文件描述符中读取指定内容 217 | fgets(buf, sizeof(buf), resource); 218 | while (!feof(resource)) 219 | { 220 | send(connfd, buf, strlen(buf), 0); 221 | fgets(buf, sizeof(buf), resource); 222 | } 223 | } 224 | 225 | /**********************************************************************/ 226 | /* Inform the connfd that a CGI script could not be executed. 227 | * Parameter: the connfd socket descriptor. */ 228 | /**********************************************************************/ 229 | void cannot_execute(int connfd) 230 | { 231 | char buf[1024]; 232 | 233 | sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n"); 234 | send(connfd, buf, strlen(buf), 0); 235 | sprintf(buf, "Content-type: text/html\r\n"); 236 | send(connfd, buf, strlen(buf), 0); 237 | sprintf(buf, "\r\n"); 238 | send(connfd, buf, strlen(buf), 0); 239 | sprintf(buf, "

Error prohibited CGI execution.\r\n"); 240 | send(connfd, buf, strlen(buf), 0); 241 | } 242 | 243 | /**********************************************************************/ 244 | /* Print out an error message with perror() (for system errors; based 245 | * on value of errno, which indicates system call errors) and exit the 246 | * program indicating an error. */ 247 | /**********************************************************************/ 248 | void error_die(const char *sc) 249 | { 250 | // #include ,void perror(char *string); 251 | // 在输出错误信息前加上字符串sc: 252 | perror(sc); 253 | exit(1); 254 | } 255 | 256 | /**********************************************************************/ 257 | /* 执行一个cgi脚本,设置一些环境变量 258 | 259 | * Parameters: connfd socket descriptor 260 | * path to the CGI script */ 261 | /**********************************************************************/ 262 | void execute_cgi(int connfd, const char *path, const char *method, const char *query_string) 263 | { 264 | char buf[1024]; 265 | int cgi_output[2]; //重定向输出的管道 266 | int cgi_input[2]; //重定向输入的管道 267 | pid_t pid; 268 | int status; 269 | int i; 270 | char c; 271 | int numchars = 1; 272 | int content_length = -1; 273 | 274 | buf[0] = 'A'; buf[1] = '\0'; 275 | 276 | //如果是 http 请求是 GET 方法的话读取并忽略请求剩下的内容 277 | if (strcasecmp(method, "GET") == 0) { 278 | while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */ 279 | numchars = get_line(connfd, buf, sizeof(buf)); 280 | } else { 281 | //只有 POST 方法才继续读内容 282 | numchars = get_line(connfd, buf, sizeof(buf)); 283 | //这个循环的目的是读出指示 body 长度大小的参数,并记录 body 的长度大小。其余的 header 里面的参数一律忽略 284 | //注意这里只读完 header 的内容,body 的内容没有读 285 | while ((numchars > 0) && strcmp("\n", buf)) 286 | { 287 | buf[15] = '\0'; 288 | if (strcasecmp(buf, "Content-Length:") == 0) 289 | content_length = atoi(&(buf[16])); //记录 body 的长度大小 290 | numchars = get_line(connfd, buf, sizeof(buf)); 291 | } 292 | 293 | //如果 http 请求的 header 没有指示 body 长度大小的参数,则报错返回 294 | if (content_length == -1) { 295 | bad_request(connfd); 296 | return; 297 | } 298 | } 299 | 300 | sprintf(buf, "HTTP/1.0 200 OK\r\n"); 301 | send(connfd, buf, strlen(buf), 0); 302 | 303 | //下面这里创建两个管道,用于两个进程间通信 304 | //管道是一种进程间通信的方法,但是管道是半双工的,就是流只能向一个方向流动, 305 | //所以如果想把一个进程的输入和输出都重定向的话,则需要建立两条管道,并且需要关闭不需要的管道的读端或者写端 306 | if (pipe(cgi_output) < 0) { 307 | cannot_execute(connfd); 308 | return; 309 | } 310 | if (pipe(cgi_input) < 0) { 311 | cannot_execute(connfd); 312 | return; 313 | } 314 | 315 | //创建一个子进程 316 | if ( (pid = fork()) < 0 ) { 317 | cannot_execute(connfd); 318 | return; 319 | } 320 | 321 | //子进程用来执行 cgi 脚本 322 | if (pid == 0) 323 | { 324 | /* child: CGI script */ 325 | char meth_env[255]; 326 | char query_env[255]; 327 | char length_env[255]; 328 | 329 | //将子进程的输出由标准输出重定向到 cgi_ouput 的管道写端上 330 | dup2(cgi_output[1], 1); 331 | //将子进程的输入由标准输入重定向到 cgi_input 的管道读端上 332 | dup2(cgi_input[0], 0); 333 | //关闭 cgi_ouput 管道的读端与cgi_input 管道的写端 334 | close(cgi_output[0]); 335 | close(cgi_input[1]); 336 | 337 | //构造一个环境变量 338 | sprintf(meth_env, "REQUEST_METHOD=%s", method); 339 | 340 | //将这个环境变量加进子进程的运行环境中 341 | putenv(meth_env); 342 | 343 | //根据http 请求的不同方法,构造并存储不同的环境变量 344 | if (strcasecmp(method, "GET") == 0) { 345 | sprintf(query_env, "QUERY_STRING=%s", query_string); 346 | putenv(query_env); 347 | } else { 348 | /* POST */ 349 | sprintf(length_env, "CONTENT_LENGTH=%d", content_length); 350 | putenv(length_env); 351 | } 352 | 353 | execl(path, path, NULL); 354 | exit(0); 355 | 356 | } else { /* parent */ 357 | //父进程则关闭了 cgi_output管道的写端和 cgi_input 管道的读端 358 | close(cgi_output[1]); 359 | close(cgi_input[0]); 360 | 361 | //如果是 POST 方法的话就继续读 body 的内容,并写到 cgi_input 管道里让子进程去读 362 | if (strcasecmp(method, "POST") == 0) { 363 | 364 | for (i = 0; i < content_length; i++) { 365 | recv(connfd, &c, 1, 0); 366 | write(cgi_input[1], &c, 1); 367 | } 368 | } 369 | 370 | //然后从 cgi_output 管道中读子进程的输出,并发送到客户端去 371 | while (read(cgi_output[0], &c, 1) > 0) 372 | send(connfd, &c, 1, 0); 373 | 374 | //关闭管道 375 | close(cgi_output[0]); 376 | close(cgi_input[1]); 377 | //等待子进程的退出 378 | waitpid(pid, &status, 0); 379 | } 380 | } 381 | 382 | /**********************************************************************/ 383 | /* 从socket读取一行。 384 | * 如果行以换行(\n)或者回车(\r)或者CRLF(\r\n)做为结束,就使用'\0'停止字符串读取 385 | * 如果buffer读取完都没有发现换行符,使用'\0'结束字符串 386 | * 如果上边三个任意一个行终止符被读出,都会被替换为\n,并且在末尾补充'\0' 387 | * 意思就是不管换行符是\r还是\n还是\r\n,都会被替换为\n\0 388 | * 389 | * 关于回车和换行: 390 | * '\r'是回车,使光标到行首,(carriage return) 391 | * '\n'是换行,使光标下移一格,(line feed / newline) 392 | * '\r\n'就是CRLF啦 393 | * 394 | * Parameters: the socket descriptor 395 | * the buffer to save the data in 396 | * the size of the buffer 397 | * Returns: the number of bytes stored (excluding null) */ 398 | /**********************************************************************/ 399 | int get_line(int sock, char *buf, int size) 400 | { 401 | int i = 0; 402 | char c = '\0'; //补充到结尾的字符 403 | int n; 404 | 405 | while ((i < size - 1) && (c != '\n')) //因为字符串的最后一位要使用'\0'结束,所以size需要 -1 406 | { 407 | //从sock中一次读一个字符,循环读 408 | //int recv(int s, void *buf, int len, unsigned int flags); 409 | //recv()用来接收远端主机经指定的socket传来的数据, 并把数据存到由参数buf指向的内存空间, 参数len为可接收数据的最大长度. 410 | n = recv(sock, &c, 1, 0); 411 | if (n > 0) { 412 | if (c == '\r') { //这个if里边处理了\r和\r\n结尾的情况,把c统一改为\n,如果是\n结尾那无需处理直接就赋值给buf啦 413 | 414 | //参数 flags 一般设0,MSG_PEEK表示返回来的数据并不会在系统内删除, 如果再调用recv()会返回相同的数据内容. 415 | //这个选项用于测试下一个字符是不是\n,并不会把当前读出的字符从缓冲区中删掉 416 | n = recv(sock, &c, 1, MSG_PEEK); 417 | /* DEBUG printf("%02X\n", c); */ 418 | if ((n > 0) && (c == '\n')) //如果是\n,就把下个字符读取出来,如果不是\n就把\r改为\n 419 | recv(sock, &c, 1, 0); 420 | else 421 | c = '\n'; 422 | } 423 | 424 | buf[i] = c; 425 | i++; 426 | } else { 427 | //读取失败直接赋值\n 428 | c = '\n'; 429 | } 430 | } 431 | 432 | //读取完一行数据后在获得的字符串末尾补上\0 433 | buf[i] = '\0'; 434 | 435 | return (i); 436 | } 437 | 438 | /** 439 | * 处理请求方法不是GET和POST的情况 440 | * @param connfd [description] 441 | */ 442 | void readBufferBeforeSend(int connfd) 443 | { 444 | int numchars = 1; 445 | char buf[1024]; 446 | 447 | int content_length = -1; //post要读取的长度 448 | int i; //for循环准备 449 | char c; //for循环读取消息体准备 450 | 451 | buf[0] = 'A'; buf[1] = '\0'; 452 | 453 | //对于GET和HEAD方法,只有请求头部分,不会发送请求体 454 | //注意HEAD方法只会返回请求头,不返回请求体,页面上看不到输出是正常的,只要返回状态码正确就行了。 455 | //除了GET和HEAD其他方法都会发送请求体,这里先读出请求头,根据Content-Length判断是否有请求体 456 | 457 | //strcmp若参数s1 和s2 字符串相同则返回0。s1 若大于s2 则返回大于0 的值。s1 若小于s2 则返回小于0 的值。 458 | //完全等于"\n"代表当前行是消息头和消息体之前的那个空行 459 | while ((numchars > 0) && strcmp("\n", buf)) 460 | { 461 | buf[15] = '\0'; 462 | if (strcasecmp(buf, "Content-Length:") == 0) { 463 | content_length = atoi(&(buf[16])); //把ascii码转为整形,http协议传输的是ascii码 464 | } 465 | numchars = get_line(connfd, buf, sizeof(buf)); 466 | } 467 | 468 | //读出消息体并忽略,这里不能多读也不能少读 469 | if(content_length != -1){ 470 | for (i = 0; i < content_length; i++) { 471 | recv(connfd, &c, 1, 0); 472 | } 473 | } 474 | } 475 | 476 | /**********************************************************************/ 477 | /* Return the informational HTTP headers about a file. */ 478 | /* Parameters: the socket to print the headers on 479 | * the name of the file */ 480 | /**********************************************************************/ 481 | void headers(int connfd, const char *filename) 482 | { 483 | char buf[1024]; 484 | (void)filename; /* could use filename to determine file type */ 485 | 486 | strcpy(buf, "HTTP/1.0 200 OK\r\n"); 487 | send(connfd, buf, strlen(buf), 0); 488 | strcpy(buf, SERVER_STRING); 489 | send(connfd, buf, strlen(buf), 0); 490 | sprintf(buf, "Content-Type: text/html\r\n"); 491 | send(connfd, buf, strlen(buf), 0); 492 | strcpy(buf, "\r\n"); 493 | send(connfd, buf, strlen(buf), 0); 494 | } 495 | 496 | /**********************************************************************/ 497 | /* Give a connfd a 404 not found status message. */ 498 | /**********************************************************************/ 499 | void not_found(int connfd) 500 | { 501 | char buf[1024]; 502 | 503 | sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n"); 504 | send(connfd, buf, strlen(buf), 0); 505 | sprintf(buf, SERVER_STRING); 506 | send(connfd, buf, strlen(buf), 0); 507 | sprintf(buf, "Content-Type: text/html\r\n"); 508 | send(connfd, buf, strlen(buf), 0); 509 | sprintf(buf, "\r\n"); 510 | send(connfd, buf, strlen(buf), 0); 511 | sprintf(buf, "Not Found\r\n"); 512 | send(connfd, buf, strlen(buf), 0); 513 | sprintf(buf, "

The server could not fulfill\r\n"); 514 | send(connfd, buf, strlen(buf), 0); 515 | sprintf(buf, "your request because the resource specified\r\n"); 516 | send(connfd, buf, strlen(buf), 0); 517 | sprintf(buf, "is unavailable or nonexistent.\r\n"); 518 | send(connfd, buf, strlen(buf), 0); 519 | sprintf(buf, "\r\n"); 520 | send(connfd, buf, strlen(buf), 0); 521 | } 522 | 523 | /**********************************************************************/ 524 | /* Send a regular file to the connfd. Use headers, and report 525 | * errors to connfd if they occur. 526 | * Parameters: a pointer to a file structure produced from the socket 527 | * file descriptor 528 | * the name of the file to serve */ 529 | /**********************************************************************/ 530 | void serve_file(int connfd, const char *filename) 531 | { 532 | FILE *resource = NULL; 533 | int numchars = 1; 534 | char buf[1024]; 535 | 536 | buf[0] = 'A'; buf[1] = '\0'; 537 | 538 | //循环读出请求头内容并忽略,这里也可以调用我添加的函数readBufferBeforeSend完成. 539 | while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */ 540 | numchars = get_line(connfd, buf, sizeof(buf)); 541 | 542 | //打开这个传进来的这个路径所指的文件 543 | resource = fopen(filename, "r"); 544 | if (resource == NULL) { 545 | not_found(connfd); 546 | } else { 547 | //打开成功后,将这个文件的基本信息封装成 response 的头部(header)并返回 548 | headers(connfd, filename); 549 | //接着把这个文件的内容读出来作为 response 的 body 发送到客户端 550 | cat(connfd, resource); 551 | } 552 | 553 | fclose(resource); 554 | } 555 | 556 | /**********************************************************************/ 557 | /* 这个函数在指定的端口上启动一个监听进程,如果端口是0,就动态分配一个 558 | * 并把值赋给变量port 559 | * 560 | * Parameters: pointer to variable containing the port to connect on 561 | * Returns: the socket */ 562 | /**********************************************************************/ 563 | int startup(u_short *port) 564 | { 565 | int httpd = 0; 566 | struct sockaddr_in name; 567 | 568 | //建立TCP SOCKET,PF_INET等同于AF_INET 569 | httpd = socket(PF_INET, SOCK_STREAM, 0); 570 | if (httpd == -1) 571 | error_die("socket"); 572 | 573 | //在修改源码后重启启动总是提示bind: Address already in use,使用tcpreuse解决 574 | int reuse = 1; 575 | if (setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) { 576 | error_die("setsockopet"); 577 | } 578 | 579 | memset(&name, 0, sizeof(name)); 580 | name.sin_family = AF_INET; 581 | 582 | //,将*port 转换成以网络字节序表示的16位整数 583 | //这里port已被我修改为8008,如果为0的话内核会在bind时自动分配一个端口号 584 | name.sin_port = htons(*port); 585 | name.sin_addr.s_addr = htonl(INADDR_ANY); 586 | 587 | if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0) 588 | error_die("bind"); 589 | 590 | //这里是原来的代码逻辑,如果port是0的话通过bind后内核会随机分配一个端口号给当前的socket连接, 591 | //获取这个端口号给port变量 592 | if (*port == 0) /* if dynamically allocating a port */ 593 | { 594 | int namelen = sizeof(name); 595 | if (getsockname(httpd, (struct sockaddr *)&name, (socklen_t *)&namelen) == -1) 596 | error_die("getsockname"); 597 | *port = ntohs(name.sin_port); 598 | } 599 | 600 | 601 | //启动监听,backlog为5 602 | if (listen(httpd, 5) < 0) 603 | error_die("listen"); 604 | return (httpd); 605 | } 606 | 607 | /**********************************************************************/ 608 | /* 通知connfd请求的web方法没有实现 */ 609 | /**********************************************************************/ 610 | void unimplemented(int connfd) 611 | { 612 | char buf[1024]; 613 | 614 | sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n"); 615 | send(connfd, buf, strlen(buf), 0); 616 | sprintf(buf, SERVER_STRING); 617 | send(connfd, buf, strlen(buf), 0); 618 | sprintf(buf, "Content-Type: text/html\r\n"); 619 | send(connfd, buf, strlen(buf), 0); 620 | sprintf(buf, "\r\n"); 621 | send(connfd, buf, strlen(buf), 0); 622 | sprintf(buf, "Method Not Implemented\r\n"); 623 | send(connfd, buf, strlen(buf), 0); 624 | sprintf(buf, "\r\n"); 625 | send(connfd, buf, strlen(buf), 0); 626 | sprintf(buf, "

HTTP request method not supported.\r\n"); 627 | send(connfd, buf, strlen(buf), 0); 628 | sprintf(buf, "\r\n"); 629 | send(connfd, buf, strlen(buf), 0); 630 | } 631 | 632 | /**********************************************************************/ 633 | 634 | int main(void) 635 | { 636 | int listenfd = -1; 637 | u_short port = 8008; 638 | int connfd = -1; 639 | 640 | struct sockaddr_in client; 641 | int client_len = sizeof(client); 642 | 643 | //pthread_t newthread; 644 | 645 | //绑定监听端口 646 | listenfd = startup(&port); 647 | printf("httpd running on port %d\n", port); 648 | 649 | while (1) 650 | { 651 | //loop waiting for client connection 652 | connfd = accept(listenfd, (struct sockaddr *)&client, (socklen_t *)&client_len); 653 | if (connfd == -1) { 654 | error_die("accept"); 655 | } 656 | 657 | //处理请求 658 | accept_request(connfd); 659 | 660 | /*if (pthread_create(&newthread , NULL, accept_request, connfd_sock) != 0) 661 | perror("pthread_create");*/ 662 | } 663 | 664 | //关闭监听描述符,注意在这之前需要关闭close(connfd) 665 | close(listenfd); 666 | 667 | return (0); 668 | } 669 | -------------------------------------------------------------------------------- /simpleclient.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include //自己加的,exit函数需要 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int sockfd; 12 | int len; 13 | struct sockaddr_in address; 14 | int result; 15 | char ch = 'A'; 16 | 17 | //申请一个流 socket 18 | sockfd = socket(AF_INET, SOCK_STREAM, 0); 19 | //填充地址结构,指定服务器的 IP 和 端口 20 | address.sin_family = AF_INET; 21 | //inet_addr 可以参考 man inet_addr 22 | //可以用现代的inet_pton()替代inet_addr(), example 中有参考例子 23 | address.sin_addr.s_addr = inet_addr("127.0.0.1"); 24 | address.sin_port = htons(8008); 25 | len = sizeof(address); 26 | 27 | //下面的语句可以输出连接的 IP 地址 28 | //但是inet_ntoa()是过时的方法,应该改用 inet_ntop(可参考 example)。但很多代码仍然遗留着inet_ntoa. 29 | //printf("%s\n", inet_ntoa( address.sin_addr)); 30 | 31 | result = connect(sockfd, (struct sockaddr *)&address, len); 32 | 33 | if (result == -1) 34 | { 35 | perror("oops: client1"); 36 | exit(1); 37 | } 38 | 39 | //往服务端写一个字节 40 | write(sockfd, &ch, 1); 41 | //从服务端读一个字符 42 | read(sockfd, &ch, 1); 43 | printf("char from server = %c\n", ch); 44 | close(sockfd); 45 | exit(0); 46 | } 47 | --------------------------------------------------------------------------------