【C语言】【网络编程】菜鸟学习日志(三) 一个简易B/S系统——Http Server和精简的浏览器

一个简易B/S系统——Http Server和精简的浏览器

——在实战中开始C语言网络编程

 

  写在前面:

  这篇博文的题目同时也是我们这学期 C语言 课程中综合实验的题目。本菜鸟自然也是第一次干这事儿。在网上找了很多文章来学习,现在也写一篇自己的博文来回馈大家!

  下面先简单介绍一下我们这篇博文要说些什么。

  其实看题目就已经很明白了:就是要做一个Http Server和一个精简的浏览器嘛。而为了实现这两个东西,我们大概要用到套接字(socket)和http协议相关的知识,其中包括URL的知识。

  最后,我们要有一个browser程序,一个server程序。此外还有一个test.html文件作为传送的目标。

  打开server让它运作;然后打开browser,输入URL,回车,请求就会生成报文(http协议)并发送给server。server接收到报文后,解析报文,并返回相应的应答报文。

 

  不同于大多数文章上来就开始罗列相关函数(因为我是个菜鸟),我们不妨先通过一个简单的对比例子来了解一下我们整个B/S系统的通信过程。

  比方说有一天你发短信给你的朋友,想问他/她索要一张近照。你的朋友收到短信后给你回复了一张前几天去海边玩的照片。那么在这个过程中,你和你的朋友就算是构成了一个B/S系统(浏览器/服务器系统)。在这个过程中你和朋友是如何实现通信的呢?

  想想上面的例子,在展开讨论它之前,我们再来回忆一下平时用浏览器访问网页时的情景:

  比如你要上百度,你会在浏览器的导航工具栏中输入:www.baidu.com,然后回车,浏览器就会把百度首页显示给你。什么?你直接去收藏栏里开百度?好吧,那只是浏览器自动帮你完成了填写网址的操作罢了。

  事实上浏览器还帮你做了很多别的事,比如默认帮你补全前面的“http://”。

  当我们要取得网络上的某个资源时,一般是通过各种途径给出了该资源的URL(统一资源定位符)。这是网络上每个资源所独有的地址。完整的格式如下:

    传送协议://服务器:端口号/路径?查询

  详细的介绍大家可以去文后的链接中找,本菜鸟就不赘述了。

 

  网页的访问一般是通过http协议进行的。这种协议规定浏览器和服务器间通过规定格式的报文进行通信。通过协议,通信双方可以完全不用考虑对方通信的具体内部实现,只要能读懂报文(报文的格式是全球统一的)就可以通信了。也许你还没有理解这里的意思。没关系,这里我们先保留一个小惊喜,到后面你就会明白了。

  再回过头看看我们找朋友要照片的例子。大概就是说,你要请求一个照片(资源),于是你填写了一封你们都可以理解的(遵守协议的)短信(报文),然后发送(send)给你的朋友;而另一边,你的朋友总是时刻在注意听手机有没有响(listen),当听到短信提示音后,便接收(accept)了你的短信并进行了阅读(read)。在明白了你的需求后,你的朋友写了一封附带照片的你们可以读懂的短信(应答报文)并回复给了你(send);然后你接收(recv)到了这个短信。这就是整个通信过程。当然在这些短信中,除了你们编写的文字外还附加了诸如发送时间、手机号码等附加属性。

  我们浏览器和服务器之间的通信也是类似的。事实上,上面括号中给出的就是我们的程序中要用到的具体函数!

 

  当然,与我们发短信还是有一些不同的地方,比如说——你可能已经听到过这个名字了——套接字。套接字在度娘那里是这样描述的:

  多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口。

  更具体的定义和用法我在文后同样给出了链接。

 

  到此为止,相信你已经对整个B/S系统有一定的认识了。那么接下来附上我的代码。根据我的注释,大家应该基本可以看懂了!

 

  代码1:

/*

browser.c

一个精简的浏览器。

可以接收用户输入的URL(ip形式),并进行访问

*/

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <errno.h>

#include <string.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <sys/socket.h>

#include <sys/wait.h>

 

#define MAXSIZE 2048

 

//URL解析器

int analysis_URL(char *buf, char *ip, char *port, char *html);

//报文生成器

int packet_builder(char *Header, char *html, char *ip, char *port);

 

int main (void)

{

  int i; //用于计数

  char buf[MAXSIZE]; //存放URL

  char Header[MAXSIZE] = {'\0'}; //填写报文

  char s_ip[16]; //存放ip

  char s_p[5]; //存放端口号 字符串

  unsigned short s_port = 0; //存放端口号 整形

  char s_html[30]; //存放路径

  int sockfd, recvbytes;

  struct sockaddr_in serv_addr;

 

  while(1)

  {

    //获取URL

    printf("URL:\n");

    scanf("%s",buf);

 

    //解析URL

    if(!analysis_URL(buf, s_ip, s_p, s_html))

    {

      i = 0;

      s_port = 0;

      while(s_p[i] != '\0')

        s_port = s_port*10+(s_p[i++]-'0');

 

    }

    else

    {

      printf("不支持的协议!");

      continue;

    }

 

    //创建套接字

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)

    {

      perror("socket error!");

      exit(1);

    }

 

    //根据URL填写服务器端套接字网络地址信息

    bzero(&serv_addr,sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;

    serv_addr.sin_port = htons(s_port);

    serv_addr.sin_addr.s_addr= inet_addr(s_ip);

 

    //与服务器端建立连接

    if (connect(sockfd, (struct sockaddr *)&serv_addr,sizeof(struct sockaddr)) == -1)

    {

      perror("connect error!");

      exit(1);

    }

 

    //填写报文

 

    memset(Header, '\0', MAXSIZE); //先清空原来的报文

    packet_builder(Header, s_html, s_ip, s_p);

 

    //发送请求报文

    send(sockfd, Header, strlen(Header),0);

 

    //接收应答文

    if ((recvbytes = recv(sockfd, buf, MAXSIZE,0)) == -1) 

    {

      perror("recv error!");

      exit(1);

    }

    buf[recvbytes] = '\0';

 

    //打印接收到的响应

    printf("\n响应报文已接收!\n%s\n",buf);

 

    //关闭套接字

    close(sockfd);

    close(recvbytes);

  }

 

  return 0;

}

 

代码2:

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

#include <string.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <sys/socket.h>

#include <sys/wait.h>

#include <time.h>

 

#define SERVPORT 3333 //用户接入端口

#define BACKLOG 10 //允许等待连接数

#define MAXSIZE 2048

 

//报文解析器

int analysis_packet();

//应答报文头生成器

int packet_builder_s();

 

int main(void) {

  int sockfd,client_fd;

  struct sockaddr_in my_addr;

  struct sockaddr_in remote_addr;

  char msg[MAXSIZE] = {'\0'};

  struct tm *ptr;

  time_t it;

 

  it=time(NULL);

 

  //创建套接字

  if ((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)

  {

    perror("socket create failed!");

    exit(1);

  }

 

  //绑定端口地址

  my_addr.sin_family = AF_INET;//通信类型

  my_addr.sin_port = htons(SERVPORT);//端口

  my_addr.sin_addr.s_addr = INADDR_ANY;//使用自己的ip地址

  bzero(&(my_addr.sin_zero),8);

  if (bind(sockfd, (struct sockaddr*)&my_addr, sizeof(struct sockaddr)) == -1)

  {

    perror("bind error!");

    exit(1);

  }

 

  //监听端口

  if (listen(sockfd, BACKLOG) == -1)

  {

    perror("listen error");

    exit(1);

  }

 

  while (1)

  {

    int sin_size = sizeof(struct sockaddr_in);

    if ((client_fd = accept(sockfd, (struct sockaddr*)&remote_addr,&sin_size)) == -1)

    {

      perror("accept error!");

      continue;

    }

    printf("收到来自%s的连接!\n", (char*)inet_ntoa(remote_addr.sin_addr));

 

    //子进程段

    if (!fork())

    {

      //接受client发送的请示信息

      int rval;

      char buf[MAXSIZE];

      char file[50] = {'\0'};

      FILE *fp;

      char text[MAXSIZE];

      char fname[100] = "/home/arcane/C_learning"; //服务器文件的路径

      int size;

 

      if ((rval = read(client_fd, buf, MAXSIZE)) < 0)

      {

        perror("reading stream error!");

        continue;

      }

      printf("%s\n",buf);

 

      //解析报文

      if(!analysis_packet(buf, file)) //解析成功

      {

        //生成本地文件路径

        strcat(fname, file);

        if((fp=fopen(fname,"r")) != NULL) //找到文件

        {

          fseek(fp, 0, SEEK_END);

          size = ftell(fp);

          fseek(fp, 0, SEEK_SET);

 

          //填写响应报文头

          packet_builder_s(msg, 200, size);

          while(fgets(text,2048,fp)!=NULL)

          {

            strcat(msg,text);

          };

          fclose(fp);

 

          //向client发送应答报文

          if (send(client_fd, msg, strlen(msg), 0) == -1) perror("send error!");

            close(client_fd);

        }

        else

        {

          packet_builder_s(msg, 404, 0);

          if (send(client_fd, msg, strlen(msg), 0) == -1) perror("send error!");

            close(client_fd);

        }

      }

      else

        printf("报文解析失败!\n");

      exit(0);

    }

    close(client_fd);

  }

 

  return 0;

}

 

  结合之前的介绍,上面的代码是很好理解的。唯一的难点可能就在结构体 sockaddr_in 上面。这个结构体展开来是这样的:

  struct sockaddr_in 

  {

    short int sin_family; //通信类型

    unsigned short int sin_port; // 端口 

    struct in_addr sin_addr; // Internet 地址 

    unsigned char sin_zero[8]; // 与sockaddr结构的长度相同

  };

 

  关于它的更多描述在文后链接中有提到,有兴趣深究的朋友可以看看。

 

  当然,上面的代码你还不能直接使用。除了要把服务器程序中的资源路径改成你的资源路径外,我们还有4个函数只给出了声明而没有给出实现。它们分别是:browser的URL解析器和报文生成器、server的报文解析器和应答报文头生成器。而要实现这4个函数,我们就要深入了解http协议——因为我们的报文就是http协议规定的嘛!

  报文的生成其实就是按照规则生成一个字符串,而报文解析就是对这个报文字符串按照同样的规则拆分的过程。报文中包含了诸如主机地址、请求资源、发送时间、接受状态等信息。

  具体的格式我同样给出了链接。大家按照同样的规则写好的报文程序应该都是可以解析出来的。

  下面我给出我的实现。像前面说的,我在这里只是实现了一小部分功能,比如browser只能生成GET类型的报文(这是最简单的类型,还有其他类型的可以在链接中找到)。但是它们确实是严格按照规则来的。

 

代码1(添加到browser.c底部):

/*

解析URL

URL一般格式:scheme://host:port/path?query#fragment

这是一个只能解析http协议的URL解析器

*/

int analysis_URL(char *buf, char *ip, char *port, char *html)

{

  char *p;

  int i;

 

  if((p=strtok(buf, ":")) != NULL)

  {

    //确认为 http 协议

    if(!strcmp(p, "http"))

    {

      //确认 http 后的 "://"

      p=strtok(NULL,":");

      if(*p=='/' && *(p+1)=='/')

      {

        strcpy(ip, p+2); //填写 ip

        //确认有端口号

        if((p=strtok(NULL,":")) != NULL)

        {

          if((i=strcspn(p,"/")) < strlen(p))

          {

            strncpy(port, p, i); //填写 port

            //端口号之后的 "/"

            strcpy(html, p+i); //填写具体文件

            return 0;

          }

        }

        else

        {

          printf("缺少端口号!");

          return 1;

        }

      }

    }

    else

    {

      printf("不支持的协议!");

      return 1;

    }

  }

  printf("格式错误!");

 

  return 1;

}

 

/*

报文生成器

这是一个只能生成GET类报文的简单生成器

报文首部只填写部分属性

*/

int packet_builder(char *Header, char *html, char *ip, char *port)

{

  //请求行

  strcat(Header,"GET ");

  strcat(Header,html);

  strcat(Header," HTTP/1.1\r\n");

  //首部

  strcat(Header,"Accept: */*\r\n");

  strcat(Header,"Accept-Language: zh-cn\r\n");

  strcat(Header,"User-Agent: Qbrowser/0.0\r\n");

  strcat(Header,"Host: ");

  strcat(Header,ip);

  strcat(Header,":");

  strcat(Header,port);

  strcat(Header,"\r\nConnection: Keep-Alive\r\n");

  //空行

  strcat(Header,"\r\n");

  //请求主体

 

  return 0;

}

 

代码2(添加到server.c底部):

/*

请求报文解析器

这是一个极简的解析器

事实上只解析了请求行

*/

int analysis_packet(char *packet, char *f)

{

  char *p;

  char *delims={ " \r" };

 

  if((p=strtok(packet, delims)) == NULL)

  {

    return 1;

  }

  if(!strcmp(p,"GET")) //确认请求为 GET

  {

    if((p=strtok(NULL, delims)) == NULL)

    {

      return 1;

    }

    strcpy(f, p); //获取文件路径

    if((p=strtok(NULL, delims)) == NULL)

    {

      return 1;

    }

    if(!strcmp(p, "HTTP/1.1")) //确认协议为 HTTP/1.1

      return 0;

    else

      printf("报文解析:未知的协议!\n");

  }

  else

    printf("报文解析:未知的请求!\n");

  return 1;

}

 

/*

应答报文头生成器

只支持200和404状态

填写了部分属性

*/

int packet_builder_s(char *msg, int status, int s)

{

  char num[5];

  char *wday[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};

  char *wmon[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

  struct tm *ptr;

  time_t it;

 

  time(&it);

  ptr=localtime(&it); //取得本地时间

 

  switch(status)

  {

    case 200:

      //状态行

      strcat(msg,"HTTP/1.1 200 OK\r\n");

      //首部

      strcat(msg,"Date: ");

      strcat(msg, wday[ptr->tm_wday]);

      strcat(msg, ", ");

      sprintf(num,"%d",ptr->tm_mday);

      strcat(msg,num);

      strcat(msg," ");

      strcat(msg, wmon[ptr->tm_mon]);

      strcat(msg," ");

      sprintf(num,"%d",1900+ptr->tm_year);

      strcat(msg,num);

      strcat(msg," ");

      sprintf(num,"%d",ptr->tm_hour);

      strcat(msg,num);

      strcat(msg,":");

      sprintf(num,"%d",ptr->tm_min);

      strcat(msg,num);

      strcat(msg,":");

      sprintf(num,"%d",ptr->tm_sec);

      strcat(msg,num);

      strcat(msg," GMT\r\n");

      strcat(msg,"Content-Type: text/html;charset=gb2312\r\n");

      strcat(msg,"Content-Length: ");

      sprintf(num, "%d", s);

      strcat(msg, num);

 

      strcat(msg,"\r\n\r\n");

    break;

 

    case 404:

      //状态行

      strcat(msg,"HTTP/1.1 404 Not Found\r\n");

      //首部

      strcat(msg,"Date: ");

      strcat(msg, wday[ptr->tm_wday]);

      strcat(msg, ", ");

      sprintf(num,"%d",ptr->tm_mday);

      strcat(msg,num);

      strcat(msg," ");

      strcat(msg, wmon[ptr->tm_mon]);

      strcat(msg," ");

      sprintf(num,"%d",1900+ptr->tm_year);

      strcat(msg,num);

      strcat(msg," ");

      sprintf(num,"%d",ptr->tm_hour);

      strcat(msg,num);

      strcat(msg,":");

      sprintf(num,"%d",ptr->tm_min);

      strcat(msg,num);

      strcat(msg,":");

      sprintf(num,"%d",ptr->tm_sec);

      strcat(msg,num);

      strcat(msg," GMT\r\n\r\n");

 

    break;

  }

  return 0;

}

 

  上面的代码只是一个小小的示范。相信在你充分理解了http协议和一些string操作的方法之后,你可以写出更强大、更稳定的函数!

 

  记得之前说过要把server.c中的服务器资源路径改成你的路径吗?我们在这个路径下放一个用于测试的test.html文件。不然空空如也的服务器象什么话嘛!这个文件是用html语言写的,不用管它,复制粘贴下面的内容就好了!

 

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />

<meta http-equiv="Content-Language" content="zh-cn" />

</head>

<body bgcolor="yellow">

<h2>Hello World</h2>

</body>

</html>

 

  这样服务器就有资源了。服务器每次受到请求,都会在服务器资源路径下查找,如果找到了,就返回200报文,并附上文件内容;如果没找到,则返回404报文。

 

  下面来看看我们的成果吧!

  把我们的程序编译好。

  运行server,它就开始监听连接了。保持它的运行。

  运行browser,输入:

    http://127.0.0.1:3333/test.html

    回车

  你就会看到服务器和浏览器都收到了对方发来的报文!

  在browser中输入:

    http://127.0.0.1:3333/no.html

  由于我们没有在资源路径下放置 no.html 这个文件,于是浏览器会接收到一个404报文。

 

  虽然很简单,但是以上就是我们的精简B/S系统的全部了!

  哦,等等!还有一个说好的小惊喜呢!

  之前我们有说过的,由于我们是严格按照http协议来编写的程序,所以它可以和任何其他同样遵循此协议的程序实现通信而无需考虑其内部实现!什么意思呢?

  现在,保持你的server运行。

  打开你的浏览器。注意,不是那个browser,而是你的系统浏览器,比如火狐。

  在你的导航栏里——不要怀疑——输入:

    http://127.0.0.1:3333/test.html

    回车

  你会发现你的浏览器真的为你打开了一个网页!这个网页是黄色的页面,上面写这一行字:“Hello World!”

  你也许会很奇怪,你完全不知道火狐是如何编写的,但是它却可以访问你的服务器!其实也不必大惊小怪,所谓协议就是用来干这个的呀!

 

 链接:

  这篇文章提供了详细的c语言socket函数讲解,很多定义和用法都可以找到:

    http://blog.csdn.net/shisqf/article/details/6563942

  这篇文章比较直观地给出了http协议的格式和例子:

    http://www.cnblogs.com/shaoge/archive/2009/08/14/1546019.html

  这篇文章更详细地介绍了http的相关概念,包括URL的介绍:

    http://www.360doc.com/content/13/0217/11/9318309_266094744.shtml

  这篇文章是一个介绍操作string的各种函数的好文章,在报文的生成和解析中可能用得到:

    http://blog.csdn.net/sunnylgz/article/details/6677103    

 

posted @ 2014-04-18 22:41  宇坤  阅读(2303)  评论(3编辑  收藏  举报