引言
做一個老實人挺好的,至少還覺得自己挺老實的.

再分享一首 自己喜歡的詩人的一首 情景詩. 每個人總會有問題,至少喜歡就好,

本文 參照
http 協議 http://www.cnblogs.com/rayray/p/3729533.html
html格式 http://blog.csdn.net/allenjy123/article/details/7375029
tinyhttpd 源碼 https://github.com/EZLippi/Tinyhttpd
附錄 本文最後完稿的資源
httpd 源碼打包 http://download.csdn.net/detail/wangzhione/9461441
通過本文練習, 至少會學會 Linux上fork用法, pipe管道用法0讀1寫, pthread用法等.
其它的都是業務解析內容.
前言
講的不好望見諒, 因為很多東西需要自己去寫一寫就有感悟了. 看懂源碼和會改源碼是兩碼事. 和 會優化更不同了.
凡事多練習. 不懂也都懂了. 我們先說一下總的結構.

client.c 是一個簡易的 測試 http請求的客戶端
httpd.c 使我們重點要說的 小型簡易的Linux上的http服務器
index.html 測試網頁 是client.c 想請求的網頁
Makefile 編譯文件.
這裡先總的展示一下 httpd.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// --------------------------------------- 輔助參數宏 ----------------------------------------------
/*
* c 如果是空白字符返回 true, 否則返回false
* c : 必須是 int 值,最好是 char 范圍
*/
#define sh_isspace(c) \
((c==' ')||(c>='\t'&&c<='\r'))
//4.0 控制台打印錯誤信息, fmt必須是雙引號括起來的宏
#define CERR(fmt, ...) \
fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
__FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)
//4.1 控制台打印錯誤信息並退出, t同樣fmt必須是 ""括起來的字符串常量
#define CERR_EXIT(fmt,...) \
CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
//4.3 if 的 代碼檢測
#define IF_CHECK(code) \
if((code) < 0) \
CERR_EXIT(#code)
// --------------------------------------- 輔助變量宏 和 聲明 ------------------------------------------
// char[]緩沖區大小
#define _INT_BUF (1024)
// listen監聽隊列的大小
#define _INT_LIS (7)
/*
* 讀取文件描述符 fd 一行的內容,保存在buf中,返回讀取內容長度
* fd : 文件描述符
* buf : 保存的內容
* sz : buf 的大小
* : 返回讀取的長度
*/
int getfdline(int fd, char buf[], int sz);
// 返回400 請求解析失敗,客戶端代碼錯誤
extern inline void response_400(int cfd);
// 返回404 文件內容, 請求文件沒有找見
extern inline void response_404(int cfd);
// 返回501 錯誤, 不支持的請求
extern inline void response_501(int cfd);
// 服務器內部錯誤,無法處理等
extern inline void response_500(int cfd);
// 返回200 請求成功 內容, 後面可以加上其它參數,處理文件輸出
extern inline void response_200(int cfd);
/*
* 將文件 發送給客戶端
* cfd : 客戶端文件描述符
* path : 發送的文件路徑
*/
void response_file(int cfd, const char* path);
/*
* 返回啟動的服務器描述符(句柄), 這裡沒有采用8080端口,防止沖突,用了隨機端口
* pport : 輸出參數和輸出參數, 如果傳入NULL,將不返回自動分配的端口
* : 返回 啟動的文件描述符
*/
int serstart(uint16_t* pport);
/*
* 在客戶端鏈接過來,多線程處理的函數
* arg : 傳入的參數, 客戶端文件描述符 (int)arg
* : 返回處理結果,這裡默認返回 NULL
*/
void* request_accept(void* arg);
/*
* 處理客戶端的http請求.
* cfd : 客戶端文件描述符
* path : 請求的文件路徑
* type : 請求類型,默認是POST,其它是GET
* query : 請求發送的過來的數據, url ? 後面那些數據
*/
void request_cgi(int cfd, const char* path, const char* type, const char* query);
/*
* 主邏輯,啟動服務,可以做成守護進程.
* 具體的實現邏輯, 啟動小型玩樂級別的httpd 服務
*/
int main(int argc, char* argv[])
{
pthread_attr_t attr;
uint16_t port = 0;
int sfd = serstart(&port);
printf("httpd running on port %u.\n", port);
// 初始化線程屬性
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
for(;;){
pthread_t tid;
struct sockaddr_in caddr;
socklen_t clen = sizeof caddr;
int cfd = accept(sfd, (struct sockaddr*)&caddr, &clen);
if(cfd < 0){
CERR("accept sfd = %d is error!", sfd);
break;
}
if(pthread_create(&tid, &attr, request_accept, (void*)cfd) < 0)
CERR("pthread_create run is error!");
}
// 銷毀吧, 一切都結束了
pthread_attr_destroy(&attr);
close(sfd);
return 0;
}
// ----------------------------------------- 具體的函數實現過程 ------------------------------------------------
/*
* 讀取文件描述符 fd 一行的內容,保存在buf中,返回讀取內容長度
* fd : 文件描述符
* buf : 保存的內容
* sz : buf 的大小
* : 返回讀取的長度
*/
int
getfdline(int fd, char buf[], int sz)
{
char* tp = buf;
char c;
--sz;
while((tp-buf)<sz){
if(read(fd, &c, 1) <= 0) //偽造結束條件
break;
if(c == '\r'){ //全部以\r分割
if(recv(fd, &c, 1, MSG_PEEK)>0 && c == '\n')
read(fd, &c, 1);
else //意外的結束,填充 \n 結束讀取
*tp++ = '\n';
break;
}
*tp++ = c;
}
*tp = '\0';
return tp - buf;
}
// 返回400 請求解析失敗,客戶端代碼錯誤
inline void
response_400(int cfd)
{
const char* estr = "HTTP/1.0 400 BAD REQUEST\r\n"
"Server: wz simple httpd 1.0\r\n"
"Content-Type: text/html\r\n"
"\r\n"
"<p>你的請求有問題,請檢查語法!</p>\r\n";
write(cfd, estr, strlen(estr));
}
// 返回404 文件內容, 請求文件沒有找見
inline void
response_404(int cfd)
{
const char* estr = "HTTP/1.0 404 NOT FOUND\r\n"
"Server: wz simple httpd 1.0\r\n"
"Content-Type: text/html\r\n"
"\r\n"
"<html>"
"<head><title>你請求的界面被查水表了!</title></head>\r\n"
"<body><p>404: 估計是回不來了</p></body>"
"</html>";
//開始發送數據
write(cfd, estr, strlen(estr));
}
// 返回501 錯誤, 請求解析失敗,不支持的請求
inline void
response_501(int cfd)
{
const char* estr = "HTTP/1.0 501 Method Not Implemented\r\n"
"Server: wz simple httpd 1.0\r\n"
"Content-Type: text/html\r\n"
"\r\n"
"<html>"
"<head><title>小伙子不要亂請求</title></head>\r\n"
"<body><p>too young too simple, 年輕人別總想弄出個大新聞.</p></body>"
"</html>";
//這裡還有一個好的做法是將這些內容定義在文件中輸出文件
write(cfd, estr, strlen(estr));
}
// 服務器內部錯誤,無法處理等
inline void
response_500(int cfd)
{
const char* estr = "HTTP/1.0 500 Internal Server Error\r\n"
"Server: wz simple httpd 1.0\r\n"
"Content-Type: text/html\r\n"
"\r\n"
"<html>"
"<head><title>Sorry </title></head>\r\n"
"<body><p>最近有點方了!</p></body>"
"</html>";
write(cfd, estr, strlen(estr));
}
// 返回200 請求成功 內容, 後面可以加上其它參數,處理文件輸出
inline void
response_200(int cfd)
{
// 打印返回200的報文頭
const char* str = "HTTP/1.0 200 OK\r\n"
"Server: wz simple httpd 1.0\r\n"
"Content-Type: text/html\r\n"
"\r\n";
write(cfd, str, strlen(str));
}
/*
* 將文件 發送給客戶端
* cfd : 客戶端文件描述符
* path : 發送的文件路徑
*/
void
response_file(int cfd, const char* path)
{
FILE* txt;
char buf[_INT_BUF];
// 讀取報文頭,就是過濾
while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf))
;
// 這裡開始處理 文件內容
if((txt = fopen(path, "r")) == NULL) //文件解析錯誤,給它個404
response_404(cfd);
else{
response_200(cfd); //發送給200的報文頭過去
// 先判斷文件內容存在
while(!feof(txt) && fgets(buf, sizeof buf, txt))
write(cfd, buf, strlen(buf));
}
fclose(txt);
}
/*
* 返回啟動的服務器描述符(句柄)
* pport : 輸出參數和輸出參數, 如果傳入NULL,將不返回自動分配的端口
* : 返回 啟動的文件描述符
*/
int
serstart(uint16_t* pport)
{
int sfd;
struct sockaddr_in saddr = { AF_INET };
IF_CHECK(sfd = socket(PF_INET, SOCK_STREAM, 0));
saddr.sin_port = !pport || !*pport ? 0 : htons(*pport);
saddr.sin_addr.s_addr = INADDR_ANY;
// 綁定一下端口信息
IF_CHECK(bind(sfd, (struct sockaddr*)&saddr, sizeof saddr));
if(pport && !*pport){
socklen_t clen = sizeof saddr;
IF_CHECK(getsockname(sfd, (struct sockaddr*)&saddr, &clen));
*pport = ntohs(saddr.sin_port);
}
// 開啟監聽任務
IF_CHECK(listen(sfd, _INT_LIS));
return sfd;
}
/*
* 在客戶端鏈接過來,多線程處理的函數
* arg : 傳入的參數, 客戶端文件描述符 (int)arg
* : 返回處理結果,這裡默認返回 NULL
*/
void*
request_accept(void* arg)
{
char buf[_INT_BUF], path[_INT_BUF>>1], type[_INT_BUF>>5];
char *lt, *rt, *query, *nb = buf;
struct stat st;
int iscgi, cfd = (int)arg;
if(getfdline(cfd, buf, sizeof buf) <= 0){ //請求錯誤
response_501(cfd);
close(cfd);
return NULL;
}
// 合法請求處理
for(lt=type, rt=nb; !sh_isspace(*rt) && (lt-type)< sizeof type - 1; *lt++ = *rt++)
;
*lt = '\0'; //已經將 buf中開始不為empty 部分塞入了 type 中
//同樣處理合法與否判斷, 出錯了直接返回錯誤結果
if((iscgi = strcasecmp(type, "POST")) && strcasecmp(type, "GET")){
response_501(cfd);
close(cfd);
return NULL;
}
// 在buf中 去掉空字符
while(*rt && sh_isspace(*rt))
++rt;
// 這裡得到路徑信息
*path = '.';
for(lt = path + 1; (lt-path)<sizeof path - 1 && !sh_isspace(*rt); *lt++ = *rt++)
;
*lt = '\0'; //query url路徑就拼接好了
//單獨處理 get 獲取 ? 後面數據, 不是POST那就是GET
if(iscgi != 0){
for(query = path; *query && *query != '?'; ++query)
;
if(*query == '?'){
iscgi = 0;
*query++ = '\0';
}
}
// type , path 和 query 已經構建好了
if(stat(path, &st) < 0){
while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf))// 讀取內容直到結束
;
response_404(cfd);
close(cfd);
return NULL;
}
// 合法情況, 執行,寫入,讀取權限
if ((st.st_mode & S_IXUSR) ||(st.st_mode & S_IXGRP) ||(st.st_mode & S_IXOTH))
iscgi = 0;
if(iscgi) //沒有cgi
response_file(cfd, path);
else
request_cgi(cfd, path, type, query);
close(cfd);
return NULL;
}
/*
* 處理客戶端的http請求.
* cfd : 客戶端文件描述符
* path : 請求的文件路徑
* type : 請求類型,默認是POST,其它是GET
* query : 請求發送的過來的數據, url ? 後面那些數據
*/
void
request_cgi(int cfd, const char* path, const char* type, const char* query)
{
char buf[_INT_BUF];
int pocgi[2], picgi[2];
pid_t pid;
int contlen = -1; //報文長度
char c;
if(strcasecmp(type, "POST") == 0){
while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf)){
buf[15] = '\0';
if(!strcasecmp(buf, "Content-Length:"))
contlen = atoi(buf + 16);
}
if(contlen == -1){ //錯誤的報文,直接返回錯誤結果
response_400(cfd);
return;
}
}
else{ // 讀取報文頭,就是過濾, 後面就假定是 GET
while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf))
;
}
//這裡處理請求內容, 先處理錯誤信息
if(pipe(pocgi) < 0){
response_500(cfd);
return;
}
if(pipe(picgi) < 0){ //管道 是 0讀取, 1寫入
close(pocgi[0]), close(pocgi[1]);
response_500(cfd);
return;
}
if((pid = fork())<0){
close(pocgi[0]), close(pocgi[1]);
close(picgi[0]), close(picgi[1]);
response_500(cfd);
return;
}
// 這裡就是多進程處理了, 先處理子進程
if(pid == 0) {
// dup2 讓 前者共享後者同樣的文件表
dup2(pocgi[1], STDOUT_FILENO); //標准輸出算作 pocgi管道 的寫入端
dup2(picgi[0], STDIN_FILENO); //標准輸入做為picgif管道的讀取端
close(pocgi[0]);
close(pocgi[1]);
// 添加環境變量,用於當前會話中
sprintf(buf, "REQUEST_METHOD=%s", type);
putenv(buf);
// 繼續湊環境變量串,放到當前會話種
if(strcasecmp(buf, "POST") == 0)
sprintf(buf, "CONTENT_LENGTH=%d", contlen);
else
sprintf(buf, "QUERY_STRING=%s", query);
putenv(buf);
// 成功的話調到 新的執行體上
execl(path, path, NULL);
// 這行代碼原本是不用的, 但是防止 execl執行失敗, 子進程沒有退出.妙招
exit(EXIT_SUCCESS);
}
// 父進程, 為所欲為了,先發送個OK
write(cfd, "HTTP/1.0 200 OK\r\n", 17);
close(pocgi[1]);
close(picgi[0]);
if(strcasecmp(type, "POST") == 0){
int i; //將數據都寫入到 picgi 管道中, 讓子進程在 picgi[0]中讀取 => STDIN_FILENO
for(i=0; i<contlen; ++i){
read(cfd, &c, 1);
write(picgi[1], &c, 1);
}
}
//從子進程中 讀取數據 發送給客戶端, 多線程跨進程阻塞模型
while(read(pocgi[0], &c, 1) > 0)
write(cfd, &c, 1);
close(pocgi[0]);
close(picgi[1]);
//等待子進程結束
waitpid(pid, NULL, 0);
}
我們看見 上面 函數 解釋的很清楚, 對於 response_* 響應部分占了大頭的一半.其實本質也就200行左右. 很適合臨摹一下.
正文
現在到了正文,說的很水. 再扯一點. 自己學習反人類的庫libuv, 就是note.js 底層通信的那套網絡庫. 也就是看官方的demo
一個個的臨摹. 了解的. 也就會用. 後面也就簡易的看看源碼. 也就懂了. 最經看的深入之後還是覺得,越簡單越直白越好.封裝太多了,
容易繞暈自己,而且很多功能用不上,遇到bug了又得查看繁瑣的萬行源碼.
總結就是, 學好基礎 問題, 走到哪裡都容易, 至少能做. 做的好不好, 以後再說.
那我們分析了. 第一個 看下面函數聲明
// 返回400 請求解析失敗,客戶端代碼錯誤 extern inline void response_400(int cfd);
這裡使用了C的內聯函數, 內聯函數聲明必須要有inline.否則編譯器解析 函數名稱會不一致找不見. 再扯一點對於
strcasecmp 其實是 linux上提供的函數 , window上使用需要做額外配置. 說白了就是不跨平台. 下面一種跨平台的實現如下
/*
* 這是個不區分大小寫的比較函數
* ls : 左邊比較字符串
* rs : 右邊比較字符串
* : 返回 ls>rs => >0 ; ls = rs => 0 ; ls<rs => <0
*/
int
str_icmp(const char* ls, const char* rs)
{
int l, r;
if(!ls || !rs)
return (int)(ls - rs);
do {
if((l=*ls++)>='a' && l<='z')
l -= 'a' - 'A';
if((r=*rs++)>='a' && r<='z')
r -= 'a' - 'A';
} while(l && l==r);
return l-r;
}
參照編譯器源碼給的一種實現. 性能方面基本上還可以. 這裡再扯一點. 為什麼C中常說用指針速度快.
分析如下 普通的 a[6] ,訪問過程是 先取a首地址,再取a+6地址後面 取*(a+6)的值.
而如果直接用 ptr = a, ++ => ptr -> a+6 那就省略了一步 a+6的問題. 所以快一點.
再扯一點 a[6]其實就是語法糖, 本質也就是 *(a + 6), 通過這個推廣, a[-1] 也合法 等價於 *(a - 1).
後面再簡單分析一下 細節
我們總的思路是 服務器httpd 采用多線程接收客戶端請求. 再分析報文, 主要是分get請求和post請求.
get請求直接請求, 如果get 後面有? 或post請求 走 cgi 動態處理界面.
說白都很簡單, http 是在tcp 基礎上添加了 http報文的基礎解析內容. 本質是業務邏輯的處理.
這裡繼續說一說 本文中采用的管道細節
//這裡處理請求內容, 先處理錯誤信息
if(pipe(pocgi) < 0){
response_500(cfd);
return;
}
if(pipe(picgi) < 0){ //管道 是 0讀取, 1寫入
close(pocgi[0]), close(pocgi[1]);
response_500(cfd);
return;
}
if((pid = fork())<0){
close(pocgi[0]), close(pocgi[1]);
close(picgi[0]), close(picgi[1]);
response_500(cfd);
return;
}
這裡是請求失敗會相應釋放打開的端口. 理論上在exit之後系統會自動回收打開的端口.但是不及時.
對於上面管道 是 子進程充定向管道為標准輸入輸出. 父進程向管道中寫入給子進程標准輸入輸出. 這就是傳說的cgi.
最後說明一段代碼
/*
* 主邏輯,啟動服務,可以做成守護進程.
* 具體的實現邏輯, 啟動小型玩樂級別的httpd 服務
*/
int main(int argc, char* argv[])
{
pthread_attr_t attr;
uint16_t port = 0;
int sfd = serstart(&port);
printf("httpd running on port %u.\n", port);
// 初始化線程屬性
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
for(;;){
pthread_t tid;
struct sockaddr_in caddr;
socklen_t clen = sizeof caddr;
int cfd = accept(sfd, (struct sockaddr*)&caddr, &clen);
if(cfd < 0){
CERR("accept sfd = %d is error!", sfd);
break;
}
if(pthread_create(&tid, &attr, request_accept, (void*)cfd) < 0)
CERR("pthread_create run is error!");
}
// 銷毀吧, 一切都結束了
pthread_attr_destroy(&attr);
close(sfd);
return 0;
}
這是主業務, 亮點在於 pthread_attr 這塊, 添加了線程分離屬性, 自己回收. 不需要內核繼續保存線程屍體.
最後記得釋放.
到這裡基本細節我們都說完了. 對於 serstart 中采用了隨機端口, 是為了不合 服務器可能的http服務8080端口沖突, 就來個隨機端口.
對於socket 采用0端口,意思就是操作系統隨機分配.
測試
下面我們開始測試測試 的 client.c 代碼
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//4.0 控制台打印錯誤信息, fmt必須是雙引號括起來的宏
#define CERR(fmt, ...) \
fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
__FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)
//4.1 控制台打印錯誤信息並退出, t同樣fmt必須是 ""括起來的字符串常量
#define CERR_EXIT(fmt,...) \
CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
//4.3 if 的 代碼檢測
#define IF_CHECK(code) \
if((code) < 0) \
CERR_EXIT(#code)
//待拼接的字符串
#define _STR_HTTP_1 "GET /index.html HTTP/1.0\r\nUser-Agent: Happy is good.\r\nHost: 127.0.0.1:"
#define _STR_HTTP_3 "\r\nConnection: close\r\n\r\n"
// 簡單請求一下
int main(int argc, char* argv[])
{
char buf[1024];
int sfd;
struct sockaddr_in saddr = { AF_INET };
int len, port;
// argc 默認為1 第一個參數 就是 執行程序串
if((argc != 2) || (port=atoi(argv[1])) <= 0 )
CERR_EXIT("Usage: %s [port]", argv[0]);
// 開始了,就這樣了
IF_CHECK(sfd = socket(PF_INET, SOCK_STREAM, 0));
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = INADDR_ANY;
IF_CHECK(connect(sfd, (struct sockaddr*)&saddr, sizeof saddr));
//開始發送請求
strcpy(buf, _STR_HTTP_1);
strcat(buf, argv[1]);
strcat(buf, _STR_HTTP_3);
write(sfd, buf, strlen(buf));
//讀取所喲內容
while((len = read(sfd, buf, sizeof buf - 1))){
buf[len] = '\0';
printf("%s", buf);
}
putchar('\n');
close(sfd);
return 0;
}
這裡就簡單向httpd 發送get 請求 index.html界面. 這裡再扯一點, 這個httpd 許多細節沒有考慮,容錯性不是那麼健全.
這些都好做,只要理解了實現思路和詳細了解HTTP協議就可以寫出好的HTTP知識,當然TCP的功底不可或缺,這點也很有挑戰.
對於index.html 界面如下
<html>
<head>
<title> 有意思 </title>
</head>
<body>
<p> 只有野獸不會欺騙 <p>
</body>
</html>
最後上 Makefile
all:httpd.out client.out
httpd.out:httpd.c
gcc -g -Wall -o $@ $^ -lpthread
client.out:client.c
gcc -g -Wall -o $@ $^
最後執行結果示意圖圖如下,先啟動 httpd服務器

後面開啟http測試機, 需要輸入端口34704 如下

到這裡我們至少簡單測試都過了.
一切都是那麼自然而然. 前提你要個節奏,這個你能堅持. 節奏很重要, 裝逼是次要的.下次有機會再分享
開發中需要用到的一些開發模型和細節. 或者分享簡單高效的網絡庫知識. 最後扯一點, 都是從不懂,一點都不懂
堅持臨摹開始的.後面就懂了, 只有不懂和痛苦,惡心才會有點意思.哈哈.
後記
錯誤是難免的, 歡迎交流, 拜~~~
