这之中有两个网站对我助力很大
Linux 的 man page 是另一个最有用的工具。
我怎么想的,在最开始我对照着 CSAPP 对最开始的版本代码进行编写,不得不说里面有一个对系统调用( read
, write
)的调用在后面给我一个大坑,也给我很多思路上的启发。
rio
形式的读写封装,无法进行长久性的数据缓存,在这里长久性的数据缓存指的是,假设对端( peer )正忙,或者自己的TCP发送缓冲区已满,那这个数据要怎么办? 不过读了 CSAPP 之后,最基本的服务器工作原理还是可以知道的,至少我看到了某些让我目瞪口呆的HTTP协议之间的交互方式。
每个人最开始学网络编程的时候,或者说几乎每本讲述网络编程的书籍,都会教你实现一个 echo 程序,无论是 TCP 还是 UDP , 当然这并不是说没有用,再复杂的网络程序,只要还是使用 套接字进行编程,那就不会走出这个最简单的模型
socket->bind->listen->accept deal_event
socket->bind->connect deal_event
HTTP服务器程序也是如此,由于HTTP协议支持几个标准的 方法 ,但是一般的浏览器实现会实现两个: GET, POST
GET
和 POST
本质功能 都是查询,只不过有时候用途不同,所以会造成某些人认为这两个的意义不同 GET
很明确,就是提供了查询功能,如果使用它对服务器进行访问,那么它所要查询的信息会放在状态信息中: GET /3/18/hello-world.html HTTP/1.1
。这就是每个浏览器向一个网站请求时,在成功建立 TCP 连接之后的 第一条信息 ,也就是服务器可以从 peer 读取的第一条信息。 1024
,那只是你所用的浏览器的实现中,浏览器规定这个请求资源的参数长度不能超过 1024
,你要是自己做一个浏览器,你可以放长点。 GET
方法会让某些特殊用处 参数 显示在你的地址栏中,例如动态请求一些东西的时候,也就是我们在HTTP服务器中的一个叫做 CGI 程序的概念。 POST
则是将请求放在报文段,这样就给了很多人误解,就是上面所说的 GET
所传的参数长度有限,而 POST
是几乎无限的。两个实际上没有差太多。至少在服务器这一端,只不过是获取参数的时候,方式稍微有些不同。 echo
程序,那些服务器所必须的硬配置,也就是 五元组 里的 <IP address, Port>
Shell
自带记录功能,不然更累) 让自己的调试之路更加顺畅,那就用配置文件的形式进行传递参数,不然效率一定会被这个给拖住。
int init_config(wsx_config_t * config){ const char ** roll = config_path_search; FILE * file; for (int i = 0; roll[i] != NULL; ++i) { file = fopen(roll[i], "r"); if (file != NULL) break; } char buf[PATH_LENGTH] = {"/0"}; char * ret; ret = fgets(buf, PATH_LENGTH, file); while (ret != NULL) { char * pos = strchr(buf, ':'); char * check = strchr(buf, '#'); /* Start with # will be ignore */ if (check != NULL) *check = '/0'; if (pos != NULL) { *pos++ = '/0'; if (0 == strncasecmp(buf, "thread", 6)) sscanf(pos, "%d", &config->core_num); else if (0 == strncasecmp(buf, "root", 4)) { sscanf(pos, "%s", &config->root_path); /* End up without "/", Add it */ if ((config->root_path)[strlen(config->root_path)-1] != '/') strncat(config->root_path, "/", 1); } else if (0 == strncasecmp(buf, "port", 4)) sscanf(pos, "%s", &config->listen_port); else if (0 == strncasecmp(buf, "addr", 4)) sscanf(pos, "%s", &config->use_addr); } ret = fgets(buf, PATH_LENGTH, file); }/* while */ fclose(file); return 0; }
inport
机制,甚至连C++那样的 namespace
都没有。那对于C语言程序的整体把握,就都在程序员手里,如果没有办法做好模块化,那真的是世界上最可怕的事情 而对于C语言的模块化,唯一能起到作用的就是 文件夹 ,没错就是强行做一波解释,让自己相信这就是隔离开的模块。但确实很有效果:有两种形式:
include
文件夹,将这些头文件都放进去。而源文件,以及一些自用的头文件放在根目录下的 src
文件夹中, src
文件夹中再进行模块细分,例如 内存管理(manage) , 页面分发(http_page) 之类的模块文件夹。 static
来修饰函数。 在目前阶段,先不要考虑使用外来库,对于一些不必要的库一定要自行编写,而不要采用第三方库,这样会提高编译成本。让用户使用体验降低。
tcmalloc/jemalloc
以及 ssl
的通信加密库之类的,而对于数据结构而言,千万要选择自行编写,而不应该依赖其他库例如 glib
之类的。 realloc
) 封装这个词,我也不知道起源何处,只是第一次听说是从面向对象里听来的,但是觉得就算是过程式的编程,也可以有这个方面的用处,当然不像那些概念说的那么严谨,我一直觉得功能拿来用就很好,不要墨守成规,虽然没有规矩不成方圆,但那对于程序员这种职业,如果思维都被限制了,那还怎么改变世界,对吧。
Trie
树,当初第一眼看到这个数据结构的时候,心里一直打鼓这什么东西,结果仅仅只是去百科了一下这个树的概念,脑海里立即浮现出这个树的构造来,就是每个节点一个字符,依次向下增长,空间换时间的意思,正好依此做了一个生命学院里DNA序列匹配的程序,还实现了一个很纯粹的 KMP 对比了一下,前后不过一天时间而已。 要尽量将某些零散的功能进行合并,保持代码的模块化,而且容易维护,例如对于打开监听套接字这个功能
socket->bind
,像这种代码就不需要整个塞在 main
函数里,那样的话错误处理代码会把真个函数挤满。而不如使用一个函数将这些代码包装起来,咋 main
中调用。
int open_listenfd(const char * restrict host_addr, const char * restrict port, int * restrict sock_type){ int listenfd = 0; /* listen the Port, To accept the new Connection */ struct addrinfo info_of_host; struct addrinfo * result; struct addrinfo * p; memset(&info_of_host, 0, sizeof(info_of_host)); info_of_host.ai_family = AF_UNSPEC; /* Unknown Socket Type */ info_of_host.ai_flags = AI_PASSIVE; /* Let the Program to help us fill the Message we need */ info_of_host.ai_socktype = SOCK_STREAM; /* TCP */ int error_code; if(0 != (error_code = getaddrinfo(host_addr, port, &info_of_host, &result))){ fputs(gai_strerror(error_code), stderr); return ERR_GETADDRINFO; /* -2 */ } for(p = result; p != NULL; p = p->ai_next) { listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if(-1 == listenfd) continue; /* Try the Next Possibility */ if(-1 == bind(listenfd, p->ai_addr, p->ai_addrlen)){ close(listenfd); continue; /* Same Reason */ } break; /* If we get here, it means that we have succeed to do all the Work */ } freeaddrinfo(result); if (NULL == p) { /* TODO ERROR HANDLE */ fprintf(stderr, "In %s, Line: %d/nError Occur while Open/Binding the listen fd/n",__FILE__, __LINE__); return ERR_BINDIND; } fprintf(stderr, "DEBUG MESG: Now We(%d) are in : %s , listen the %s port Success/n", listenfd, inet_ntoa(((struct sockaddr_in *)p->ai_addr)->sin_addr), port); *sock_type = p->ai_family; set_nonblock(listenfd); optimizes(listenfd); return listenfd; }
这段代码就是对打开监听套接字的 socket->bind
的一个功能封装,使用的是 getaddrinfo
而不是传统的手工转换,降低了出错的概率而且更加可靠。
有一个问题就是,注释太多,没有必要,这个只是在编写过程中让我快速回溯自己的想法,所以写了许多注释。
set_nonblock
和 optimizes
都是自己包装的函数,功能和名字一样。在用程序内信息Debug的时候,记得不要使用 stdout
,而使用 stderr
,并且在正式使用时,程序应该 尽可能减少输出到终端 。