一、项目背景
- http协议被广泛使用,从移动端,pc端浏览器,http协议无疑是打开互联网应用窗口的重要协议,http在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。
- 在学习完网络的有关知识后,HTTP服务器无疑是巩固及应用所学知识的最好选择,从技术上更多的理解从上网开始,到关闭浏览器的所有操作中的细节。
二、项目简介
主要功能:
用户可输入服务器网址,服务器响应,返回一个登陆页面,用户通过服务器暴露出来的接口进行注册,注册完毕之后,用户可登陆,添加一些自己的亲朋好友的信息,服务器将其存储到数据库。服务器每天定时爬取全国的天气,根据数据库的信息然后推送给用户的亲朋好友。
实现技术
在开发项目时,我们是在Linux平台下,用到了如下知识:
C/C++,vim编辑器,socket套接字,CGI模型,shell脚本,epoll模型
三、项目流程
下图为主要流程:
由图我们可以看到项目共分为如下部分:
(1)实现HTTP服务器
(2)建立数据库
(3)获取天气信息
(4)推送天气
我们分别来分析每一步。
项目实现
一、HTTP服务器
这个是项目中的核心。利用epoll模型处理浏览器发送的请求,服务器响应,执行CGI程序,结果返回给浏览器。
关于HTTP的基础知识,请看博文:HTTP协议基础
1、socket编程
即网络套接字编程,可以参考网络套接字编程
socket编程有很多接口,首先我们要创建套接字,并将其与固定端口号绑定,创建监听套接字。
代码如下:
static int startup(int port){
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
perror("socket");
exit(2);
}
int opt=1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_addr.s_addr=htonl(INADDR_ANY);
local.sin_port=htons(port);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
perror("bind");
exit(3);
}
if(listen(sock,5)<0){
perror("listen");
exit(4);
}
return sock;
}
2、epoll模型
关于epoll模型请看博文:epoll服务器
epoll是实现IO多路转接的一种模型,它由三个接口实现,如下:
- 创建epoll模型
epoll_create调用会创建一个epoll模型,epoll模型包括三个部分,红黑树,回调机制,就绪队列。
所以说,创建一个epoll模型,操作系统要做三件事情。 - 完成事件注册
调用epoll_ctl,即将我们关心的文件描述符告诉操作系统,操作系统会将我们要关心的文件描述符及事件添加到红黑树中,至此我们就不需要管理它们了,由操作系统帮我们管理。 - 等待文件描述符就绪,检查事件是否就绪
调用epoll_wait,检查就绪队列是否为空,如果不为空,就绪队列中保存的就是已经就绪的事件;然后操作系统将数据按顺序放置在用户提供的缓冲区,同时将事件数量返回给用户。
在代码中,我们使用epoll来帮我们管理连接请求,代码如下:
void serviceIO(int efd,struct epoll_event* buf,int num,int listen_sock)
{
int i=0;
struct epoll_event eve;
for(i=0;i<num;i++)
{
int fd=buf[i].data.fd;
if((buf[i].events)&EPOLLIN)
{
if(buf[i].data.fd==listen_sock)
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
int newsock=accept(listen_sock,(struct sockaddr*)&client,&len);
if(newsock<0)
{
print_log("accept failed",FATAL);
continue;
}
printf("get a connect:%s:%d\n",inet_ntoa(client.sin_addr),\
ntohs(client.sin_port));
eve.events=EPOLLIN;
eve.data.fd=newsock;
int ret=epoll_ctl(efd,EPOLL_CTL_ADD,newsock,&eve);
if(ret<0)
{
print_log("epoll_ctl failed",FATAL);
exit(9);
}
else
{
eve.events=EPOLLOUT;
eve.data.fd=fd;
handler_request(fd,efd,eve);
}
}
}
}
}
3、CGI模型
关于CGI的知识请看:CGI机制与CGI程序
- CGI机制
CGI(common gateway interface)——通用网关接口,是一个web服务器提供信息服务的接口。 - CGI程序
CGI程序就是基于CGI标准所编写的程序,CGI程序必须按照CGI接口规范来写。
我们主要来分析项目中用到的POST方法和GET方法:
(1)GET方法从浏览器传参数给http服务器时,需要将参数跟到URI后面
(2)POST方法从浏览器传参数给http服务器时,需要将参数放到请求正文
(3)GET方法,如果没有传参,http按照一般的方式进行,返回资源即可,如果有参数传入,http就需要按照CGI方式处理参数,并将执行结果(期望资源)返回给浏览器
(4)POST方法,一般都需要使用CGI方式来进行处理
下面我们通过一张图来理解一下HTTP里面CGI模式的运行流程:
POST||GET
在上图中,我们首先需要知道是get方法还是post方法。
我们知道,HTTP请求报头由四部分组成,请求行,消息报头,空行及请求正文。而请求行又由方法,URL,版本号组成,由空格隔开。
因此:
- 在处理请求报文的时候,我们采取按行读取的方式,这样的话我们可以通过第一行获得方法和URL。
- 选择执行方式。由图所示:
(1)GET方法,则判断其URL中是否有参数,如果有参数,则执行CGI程序,如果没有,服务器就返回其请求资源。
(2)POST方法,则继续按行读取,直到读取到content_length字段,获取到content_length的值,根据值读取请求正文,执行CGI程序。
父子进程通信
在上图中,我们用父进程读取报头,子进程处理CGI程序,那么?父进程在拿到请求方法和参数后,怎样将数据交给子进程呢?
免不了需要进程间通信。我们知道,进程间通信有很多种,比如管道,共享内存,消息队列,信号量等。在这里我们使用管道实现,因此代码中我们需要创建两个管道。
- 父进程:
要将socket中的内容写出来,关闭读端,close(input[0]);
要将结果输出到浏览器端,需要关闭写端,close(output[1]) - 子进程:
要读取父进程写入的数据,关闭写端,close(input[1]);
需要将结果返回给父进程,关闭读端,close(output[0])
CGI程序如下:
int exe_cgi(int sock,char path[],char method[],char *query_string){
char line[MAX];
int content_length=-1;
char method_env[MAX/32];
char content_length_env[MAX/8];
if(strcasecmp(method,"GET")==0){
clear_header(sock);
}
else{//post
do{
get_line(sock,line,sizeof(line));
if(strncmp(line,"Content-Length: ",16)==0){
content_length=atoi(line+16);
}
}while(strcmp(line,"\n")!=0);
if(content_length==-1){
return 404;
}
}
sprintf(line,"HTTP/1.0 200 OK\r\n");
send(sock,line,strlen(line),0);
sprintf(line,"Content-Type:text/html;charset=ISO-8859-1\r\n");
send(sock,line,strlen(line),0);
sprintf(line,"\r\n");
send(sock,line,strlen(line),0);
int input[2];
int output[2];
pipe(input);
pipe(output);
pid_t id=fork();
if(id<0){
return 404;
}
else if(id==0){
//child
//method,GET[query_string],POST[content_length]
close(input[1]);
close(output[0]);
dup2(input[0],0);
dup2(output[1],1);
sprintf(method,"METHOD_ENV=%s",method);
putenv(method_env);
if(strcasecmp(method,"GET")==0){
sprintf(query_string,"QUERY_STRING=%s",query_string);
putenv(query_string);
}
else{
sprintf(content_length_env,"CONTENT_LENGTH=%d",content_length);
putenv(content_length_env);
}
//execl(...); //mycmd
execl(path,path,NULL);
exit(1);
}else{
//father
close(input[0]);
close(output[1]);
char c;
if(strcasecmp(method,"POST")==0){
int i=0;
while(i<content_length){
read(sock,&c,1);
write(input[1],&c,1);
i++;
}
}
while(read(output[0],&c,1)>0){
send(sock,&c,1,0);
}
waitpid(id,NULL,0);
close(input[1]);
close(output[0]);
}
return 200;
}
二、建立数据库
首先,我们需要建立一个数据库,就叫做weather吧。
由开始的流程图我们知道,需要建立三张表:
用户信息表(login table):存放用户登录信息
我们需要几个字段,姓名,邮箱,账号,密码,且以账号为主键,可以唯一确定用户和注册登录信息(主键是唯一的,不能为NULL值)。
表的结构如下:
朋友信息表(msg table):存放用户的朋友
用来存放用户好友的信息,其中tel是该用户的电话号码,剩下的信息是该用户的朋友的信息。
我们要发邮件,必须知道该好友的城市及联系方式吧。因此最重要的两列是city和value,city将来要被用来在weather表里面查找天气,value记录的是邮箱或者电话被用于推送信息。
表的结构如下:
天气信息表(weather table):存放天气信息
表中存储的是各城市的天气信息,表中必须包含字段城市,日期,天气,温度,风速,风向等情况。根据好友信息表中的城市来匹配天气表中的城市,发送天气信息。
表的内容如下:
接口分析
三、获取天气信息
天气信息怎么获得呢?
项目中用了Python爬取了15tianqi.com天气网,得到了天气信息。
在Python中,使用的是scrapy框架进行天气爬取。
这块内容是在网上查的,不太懂,简单总结一下。
scrapy框架
scrapy是一个用python编写的,轻量级,简单轻巧,使用简单的爬虫框架,它使用Twisted异步网络库处理网络通讯。
了解一下它的组件:
- Scrapy Engine
Scrapy的引擎,用来处理整个系统的数据流处理。 - Scheduler
调度器,Scheduler接受从引擎发送过来的请求,压入队列之中,在引擎再次请求的时候返回给引擎。 - Downloader
拿到请求之后,下载网页,并将下载的网页送给Spider进行处理。 - Spiders
Spiders是蜘蛛,主要是解析网页的内容,我们可以在Spiders里面定制解析的规则。 - Item Pipeline
项目管道,主要是用来存储数据以及对数据进行加工处理的。 - Downloader Middlewares
下载器中间件,位于Scrapy引擎和下载器之间的钩子框架,主要是处理Scrapy引擎与下载器之间的请求及响应。 - Spider Middlewares
蜘蛛中间件,介于Scrapy引擎和蜘蛛之间的钩子框架,主要工作是处理蜘蛛的响应输入和请求输出。 - Scheduler Middewares
调度中间件,介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。
数据处理流程
- Scrapy的整个数据流是由引擎控制的,引擎打开一个域名,并让蜘蛛处理这个域名
- 蜘蛛获取一个需要爬取的URL后,将这个需要爬取URL返回给引擎,引擎再将这个需要爬取的URL放到调度器中。
- 接下来引擎再从调度器中取出一个待爬取的URL,将这个URL送给下载器进行下载。
- 下载器下载完毕之后,将结果再返回给引擎。引擎再将结果交给蜘蛛进行解析。
- 蜘蛛将下载的页面解析为新的需要下载的URL和数据。新的URL发送给引擎。而数据则是交给项目管道进行处理。
- 项目管道可以对数据进行处理、加工和存储。
四、推送天气
获取到天气信息,就要发送天气了,那么如何发送天气呢?
- 由于天气具有实时性,所以我们必须每天都要进行更新,为此我们可以设置定时任务,每天定时去启动爬虫控制脚本,爬取全国天气信息。
- 将天气推送给msg这张表里面的所有人,用msg表里面的city字段的值到weather这张表里面找对应城市的天气,然后用邮件或短信发送。