项目-天气邮局

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/baidu_37964071/article/details/81700758

一、项目背景

  1. http协议被广泛使用,从移动端,pc端浏览器,http协议无疑是打开互联网应用窗口的重要协议,http在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。
  2. 在学习完网络的有关知识后,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多路转接的一种模型,它由三个接口实现,如下:

  1. 创建epoll模型
    epoll_create调用会创建一个epoll模型,epoll模型包括三个部分,红黑树,回调机制,就绪队列。
    所以说,创建一个epoll模型,操作系统要做三件事情。
  2. 完成事件注册
    调用epoll_ctl,即将我们关心的文件描述符告诉操作系统,操作系统会将我们要关心的文件描述符及事件添加到红黑树中,至此我们就不需要管理它们了,由操作系统帮我们管理。
  3. 等待文件描述符就绪,检查事件是否就绪
    调用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程序

  1. CGI机制
    CGI(common gateway interface)——通用网关接口,是一个web服务器提供信息服务的接口。
  2. 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,版本号组成,由空格隔开。
因此:

  1. 在处理请求报文的时候,我们采取按行读取的方式,这样的话我们可以通过第一行获得方法和URL。
  2. 选择执行方式。由图所示:
    (1)GET方法,则判断其URL中是否有参数,如果有参数,则执行CGI程序,如果没有,服务器就返回其请求资源。
    (2)POST方法,则继续按行读取,直到读取到content_length字段,获取到content_length的值,根据值读取请求正文,执行CGI程序。
父子进程通信

在上图中,我们用父进程读取报头,子进程处理CGI程序,那么?父进程在拿到请求方法和参数后,怎样将数据交给子进程呢?
免不了需要进程间通信。我们知道,进程间通信有很多种,比如管道,共享内存,消息队列,信号量等。在这里我们使用管道实现,因此代码中我们需要创建两个管道。

  1. 父进程:
    要将socket中的内容写出来,关闭读端,close(input[0]);
    要将结果输出到浏览器端,需要关闭写端,close(output[1])
  2. 子进程:
    要读取父进程写入的数据,关闭写端,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异步网络库处理网络通讯。

了解一下它的组件:

  1. Scrapy Engine
    Scrapy的引擎,用来处理整个系统的数据流处理。
  2. Scheduler
    调度器,Scheduler接受从引擎发送过来的请求,压入队列之中,在引擎再次请求的时候返回给引擎。
  3. Downloader
    拿到请求之后,下载网页,并将下载的网页送给Spider进行处理。
  4. Spiders
    Spiders是蜘蛛,主要是解析网页的内容,我们可以在Spiders里面定制解析的规则。
  5. Item Pipeline
    项目管道,主要是用来存储数据以及对数据进行加工处理的。
  6. Downloader Middlewares
    下载器中间件,位于Scrapy引擎和下载器之间的钩子框架,主要是处理Scrapy引擎与下载器之间的请求及响应。
  7. Spider Middlewares
    蜘蛛中间件,介于Scrapy引擎和蜘蛛之间的钩子框架,主要工作是处理蜘蛛的响应输入和请求输出。
  8. Scheduler Middewares
    调度中间件,介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。

数据处理流程

  1. Scrapy的整个数据流是由引擎控制的,引擎打开一个域名,并让蜘蛛处理这个域名
  2. 蜘蛛获取一个需要爬取的URL后,将这个需要爬取URL返回给引擎,引擎再将这个需要爬取的URL放到调度器中。
  3. 接下来引擎再从调度器中取出一个待爬取的URL,将这个URL送给下载器进行下载。
  4. 下载器下载完毕之后,将结果再返回给引擎。引擎再将结果交给蜘蛛进行解析。
  5. 蜘蛛将下载的页面解析为新的需要下载的URL和数据。新的URL发送给引擎。而数据则是交给项目管道进行处理。
  6. 项目管道可以对数据进行处理、加工和存储。

四、推送天气

获取到天气信息,就要发送天气了,那么如何发送天气呢?

  1. 由于天气具有实时性,所以我们必须每天都要进行更新,为此我们可以设置定时任务,每天定时去启动爬虫控制脚本,爬取全国天气信息。
  2. 将天气推送给msg这张表里面的所有人,用msg表里面的city字段的值到weather这张表里面找对应城市的天气,然后用邮件或短信发送。

项目中遇到的问题

猜你喜欢

转载自blog.csdn.net/baidu_37964071/article/details/81700758