整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          自動化生產線的形式劃分

          自動化生產線的形式劃分

          何根據自動化程度劃分自動化生產線?自動化生產線可分為剛性自動化生產線、柔性自動化生產線和智能自動化生產線。這是根據自動化現實工業生產的需求而升級的。以下是自動化生產線制造商廣晟德分享的自動化生產線的分類詳情。

          自動化生產線

          一、剛性自動化生產線

          剛性生產線具有顯著提高生產率、降低工人勞動強度、節約勞動力、保證制造質量、降低生產成本、適合大規模生產的優點。但硬自動化生產線的控制程序不易改變,只適合大量固定產品的生產,不適合多品種、小批量生產的自動化。

          與自動機床相比,它結構復雜,加工工序多,因此生產率也很高。是批量生產必備的加工設備,品種少,適合批量生產。自動化剛體生產線可以有效縮短生產周期,取消半成品的中間庫存,縮短物流,減少生產面積,改善工作條件,便于管理。其主要缺點是投資大、系統調整周期長、產品更換不方便。

          二、柔性自動化生產線

          柔性生產線是將多臺可調機床連接在一起,由自動輸送設備組成的生產線。計算機管理與各種生產模式相結合,可以降低生產成本,使其得到最佳利用。柔性生產技術,簡稱柔性制造技術,是以工藝設計為導向,以數控技術為核心的先進生產技術,可實現企業內多品種、多批次加工、制造、裝配、檢驗的自動化。

          三、智能自動化生產線

          智能生產線是自動化生產線的升級版。在自動化生產過程中,智能生產線可以通過核心自動化大腦自動判斷和分析問題。智能生產線根據系列加工生產線的布局、結構、運行特點、功能、性能和控制要求,采用智能化模式。

          智能裝配線采用在線檢測和自動補償功能,可以分析尺寸誤差的因素,及時處理誤差,實現自動有效加工,以允許偏差大于大允許偏差的臨界誤差值作為判斷比較值,對比較值進行補償,可以避免或降低自動線進入破損狀態的概率和零件超差的概率。http://www.gsdzzx.com/xingyezixun/12-413.html

          概述

          在不用爬蟲框架的情況,經過多方學習,嘗試實現了一個分布式爬蟲系統,并且可以將數據保存到不同地方,類似MySQL、HBase等。



          基于面向接口的編碼思想來開發,因此這個系統具有一定的擴展性,有興趣的朋友直接看一下代碼,就能理解其設計思想,雖然代碼目前來說很多地方還是比較緊耦合,但只要花些時間和精力,很多都是可抽取出來并且可配置化的。

          因為時間的關系,我只寫了京東和蘇寧易購兩個網站的爬蟲,但是完全可以實現不同網站爬蟲的隨機調度,基于其代碼結構,再寫國美、天貓等的商品爬取,難度不大,但是估計需要花很多時間和精力。因為在解析網頁的數據時,實際上需要花很多時間,比如我在爬取蘇寧易購商品的價格時,價格是異步獲取的,并且其api是一長串的數字組合,我花了幾個小時的時間才發現其規律,當然也承認,我的經驗不足。

          這個系統的設計,除了基本的數據爬取以外,更關注以下幾個方面的問題:

          • 1.如何實現分布式,同一個程序打包后分發到不同的節點運行時,不影響整體的數據爬取
          • 2.如何實現url隨機循環調度,核心是針對不同的頂級域名做隨機
          • 3.如何定時向url倉庫中添加種子url,達到不讓爬蟲系統停下來的目的
          • 4.如何實現對爬蟲節點程序的監控,并能夠發郵件報警
          • 5.如何實現一個隨機IP代理庫,目的跟第2點有點類似,都是為了反反爬蟲

          下面會針對這個系統來做一個整體的基本介紹,其實我在代碼中都有非常詳細的注釋,有興趣的朋友可以參考一下代碼,最后我會給出一些我爬蟲時的數據分析。

          另外需要注意的是,這個爬蟲系統是基于Java實現的,但是語言本身仍然不是最重要的,有興趣的朋友可以嘗試用Python實現。

          2 分布式爬蟲系統架構

          整體系統架構如下:

          所以從上面的架構可以看出,整個系統主要分為三個部分:

          • 爬蟲系統
          • URL調度系統
          • 監控報警系統

          爬蟲系統就是用來爬取數據的,因為系統設計為分布式,因此,爬蟲程序本身可以運行在不同的服務器節點上。

          url調度系統核心在于url倉庫,所謂的url倉庫其實就是用Redis保存了需要爬取的url列表,并且在我們的url調度器中根據一定的策略來消費其中的url,從這個角度考慮,url倉庫其實也是一個url隊列。

          監控報警系統主要是對爬蟲節點進行監控,雖然并行執行的爬蟲節點中的某一個掛掉了對整體數據爬取本身沒有影響(只是降低了爬蟲的速度),但是我們還是希望知道能夠主動接收到節點掛掉的通知,而不是被動地發現。

          下面將會針對以上三個方面并結合部分代碼片段來對整個系統的設計思路做一些基本的介紹,對系統完整實現有濃厚興趣的朋友可以直接參考源代碼。

          3 爬蟲系統

          (說明:zookeeper監控屬于監控報警系統,url調度器屬于URL調度系統)

          爬蟲系統是一個獨立運行的進程,我們把我們的爬蟲系統打包成jar包,然后分發到不同的節點上執行,這樣并行爬取數據可以提高爬蟲的效率。

          3.1 隨機IP代理器

          加入隨機IP代理主要是為了反反爬蟲,因此如果有一個IP代理庫,并且可以在構建http客戶端時可以隨機地使用不同的代理,那么對我們進行反反爬蟲則會有很大的幫助。

          在系統中使用IP代理庫,需要先在文本文件中添加可用的代理地址信息:

          # IPProxyRepository.txt
          58.60.255.104:8118
          219.135.164.245:3128
          27.44.171.27:9999
          219.135.164.245:3128
          58.60.255.104:8118
          58.252.6.165:9000
          ......

          需要注意的是,上面的代理IP是我在西刺代理上拿到的一些代理IP,不一定可用,建議是自己花錢購買一批代理IP,這樣可以節省很多時間和精力去尋找代理IP。

          然后在構建http客戶端的工具類中,當第一次使用工具類時,會把這些代理IP加載進內存中,加載到Java的一個HashMap:

          // IP地址代理庫Map
          private static Map<String, Integer> IPProxyRepository=new HashMap<>();
          private static String[] keysArray=null;   // keysArray是為了方便生成隨機的代理對象
          
          /**
               * 初次使用時使用靜態代碼塊將IP代理庫加載進set中
               */
          static {
              InputStream in=HttpUtil.class.getClassLoader().getResourceAsStream("IPProxyRepository.txt");  // 加載包含代理IP的文本
              // 構建緩沖流對象
              InputStreamReader isr=new InputStreamReader(in);
              BufferedReader bfr=new BufferedReader(isr);
              String line=null;
              try {
                  // 循環讀每一行,添加進map中
                  while ((line=bfr.readLine()) !=null) {
                      String[] split=line.split(":");   // 以:作為分隔符,即文本中的數據格式應為192.168.1.1:4893
                      String host=split[0];
                      int port=Integer.valueOf(split[1]);
                      IPProxyRepository.put(host, port);
                  }
                  Set<String> keys=IPProxyRepository.keySet();
                  keysArray=keys.toArray(new String[keys.size()]);  // keysArray是為了方便生成隨機的代理對象
              } catch (IOException e) {
                  e.printStackTrace();
              }
          
          }

          之后,在每次構建http客戶端時,都會先到map中看是否有代理IP,有則使用,沒有則不使用代理:

          CloseableHttpClient httpClient=null;
          HttpHost proxy=null;
          if (IPProxyRepository.size() > 0) {  // 如果ip代理地址庫不為空,則設置代理
              proxy=getRandomProxy();
              httpClient=HttpClients.custom().setProxy(proxy).build();  // 創建httpclient對象
          } else {
              httpClient=HttpClients.custom().build();  // 創建httpclient對象
          }
          HttpGet request=new HttpGet(url); // 構建htttp get請求
          ......

          隨機代理對象則通過下面的方法生成:

          /**
               * 隨機返回一個代理對象
               *
               * @return
               */
          public static HttpHost getRandomProxy() {
              // 隨機獲取host:port,并構建代理對象
              Random random=new Random();
              String host=keysArray[random.nextInt(keysArray.length)];
              int port=IPProxyRepository.get(host);
              HttpHost proxy=new HttpHost(host, port);  // 設置http代理
              return proxy;
          }

          這樣,通過上面的設計,基本就實現了隨機IP代理器的功能,當然,其中還有很多可以完善的地方,比如,當使用這個IP代理而請求失敗時,是否可以把這一情況記錄下來,當超過一定次數時,再將其從代理庫中刪除,同時生成日志供開發人員或運維人員參考,這是完全可以實現的,不過我就不做這一步功能了。

          3.2 網頁下載器

          網頁下載器就是用來下載網頁中的數據,主要基于下面的接口開發:

          /**
           * 網頁數據下載
           */
          public interface IDownload {
              /**
               * 下載給定url的網頁數據
               * @param url
               * @return
               */
              public Page download(String url);
          }

          基于此,在系統中只實現了一個http get的下載器,但是也可以完成我們所需要的功能了:

          /**
           * 數據下載實現類
           */
          public class HttpGetDownloadImpl implements IDownload {
          
              @Override
              public Page download(String url) {
                  Page page=new Page();
                  String content=HttpUtil.getHttpContent(url);  // 獲取網頁數據
                  page.setUrl(url);
                  page.setContent(content);
                  return page;
              }
          }

          3.3 網頁解析器

          網頁解析器就是把下載的網頁中我們感興趣的數據解析出來,并保存到某個對象中,供數據存儲器進一步處理以保存到不同的持久化倉庫中,其基于下面的接口進行開發:

          /**
           * 網頁數據解析
           */
          public interface IParser {
              public void parser(Page page);
          }

          網頁解析器在整個系統的開發中也算是比較重頭戲的一個組件,功能不復雜,主要是代碼比較多,針對不同的商城不同的商品,對應的解析器可能就不一樣了,因此需要針對特別的商城的商品進行開發,因為很顯然,京東用的網頁模板跟蘇寧易購的肯定不一樣,天貓用的跟京東用的也肯定不一樣,所以這個完全是看自己的需要來進行開發了,只是說,在解析器開發的過程當中會發現有部分重復代碼,這時就可以把這些代碼抽象出來開發一個工具類了。

          目前在系統中爬取的是京東和蘇寧易購的手機商品數據,因此與就寫了這兩個實現類:

          /**
           * 解析京東商品的實現類
           */
          public class JDHtmlParserImpl implements IParser {
              ......
          }
          
          /**
           * 蘇寧易購網頁解析
           */
          public class SNHtmlParserImpl implements IParser {
              ......
          }

          3.4 數據存儲器

          數據存儲器主要是將網頁解析器解析出來的數據對象保存到不同的,而對于本次爬取的手機商品,數據對象是下面一個Page對象:

          /**
           * 網頁對象,主要包含網頁內容和商品數據
           */
          public class Page {
              private String content;              // 網頁內容
          
              private String id;                    // 商品Id
              private String source;               // 商品來源
              private String brand;                // 商品品牌
              private String title;                // 商品標題
              private float price;                // 商品價格
              private int commentCount;        // 商品評論數
              private String url;                  // 商品地址
              private String imgUrl;             // 商品圖片地址
              private String params;              // 商品規格參數
          
              private List<String> urls=new ArrayList<>();  // 解析列表頁面時用來保存解析的商品url的容器
          }

          對應的,在MySQL中,表數據結構如下:

          -- ----------------------------
          -- Table structure for phone
          -- ----------------------------
          DROP TABLE IF EXISTS `phone`;
          CREATE TABLE `phone` (
            `id` varchar(30) CHARACTER SET armscii8 NOT NULL COMMENT '商品id',
            `source` varchar(30) NOT NULL COMMENT '商品來源,如jd suning gome等',
            `brand` varchar(30) DEFAULT NULL COMMENT '手機品牌',
            `title` varchar(255) DEFAULT NULL COMMENT '商品頁面的手機標題',
            `price` float(10,2) DEFAULT NULL COMMENT '手機價格',
            `comment_count` varchar(30) DEFAULT NULL COMMENT '手機評論',
            `url` varchar(500) DEFAULT NULL COMMENT '手機詳細信息地址',
            `img_url` varchar(500) DEFAULT NULL COMMENT '圖片地址',
            `params` text COMMENT '手機參數,json格式存儲',
            PRIMARY KEY (`id`,`source`)
          ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

          而在HBase中的表結構則為如下:

          ## cf1 存儲 id source price comment brand url
          ## cf2 存儲 title params imgUrl
          create 'phone', 'cf1', 'cf2'
          
          ## 在HBase shell中查看創建的表
          hbase(main):135:0> desc 'phone'
          Table phone is ENABLED                                                                                                
          phone                                                                                                                 
          COLUMN FAMILIES DESCRIPTION                                                                                           
          {NAME=> 'cf1', BLOOMFILTER=> 'ROW', VERSIONS=> '1', IN_MEMORY=> 'false', KEEP_DELETED_CELLS=> 'FALSE', DATA_BLOCK
          _ENCODING=> 'NONE', TTL=> 'FOREVER', COMPRESSION=> 'NONE', MIN_VERSIONS=> '0', BLOCKCACHE=> 'true', BLOCKSIZE=> 
          '65536', REPLICATION_SCOPE=> '0'}                                                                                    
          {NAME=> 'cf2', BLOOMFILTER=> 'ROW', VERSIONS=> '1', IN_MEMORY=> 'false', KEEP_DELETED_CELLS=> 'FALSE', DATA_BLOCK
          _ENCODING=> 'NONE', TTL=> 'FOREVER', COMPRESSION=> 'NONE', MIN_VERSIONS=> '0', BLOCKCACHE=> 'true', BLOCKSIZE=> 
          '65536', REPLICATION_SCOPE=> '0'}                                                                                    
          2 row(s) in 0.0350 seconds

          即在HBase中建立了兩個列族,分別為cf1、cf2,其中cf1用來保存id source price comment brand url字段信息,cf2用來保存title params imgUrl字段信息。

          不同的數據存儲用的是不同的實現類,但是其都是基于下面同一個接口開發的:

          /**
           * 商品數據的存儲
           */
          public interface IStore {
              public void store(Page page);
          }

          然后基于此開發了MySQL的存儲實現類、HBase的存儲實現類還有控制臺的輸出實現類,如MySQL的存儲實現類,其實就是簡單的數據插入語句:

          /**
           * 使用dbc數據庫連接池將數據寫入mysql表中
           */
          public class MySQLStoreImpl implements IStore {
              private QueryRunner queryRunner=new QueryRunner(DBCPUtil.getDataSource());
          
              @Override
              public void store(Page page) {
                  String sql="insert into phone(id, source, brand, title, price, comment_count, url, img_url, params) values(?, ?, ?, ?, ?, ?, ?, ?, ?)";
                  try {
                      queryRunner.update(sql, page.getId(),
                              page.getSource(),
                              page.getBrand(),
                              page.getTitle(),
                              page.getPrice(),
                              page.getCommentCount(),
                              page.getUrl(),
                              page.getImgUrl(),
                              page.getParams());
                  } catch (SQLException e) {
                      e.printStackTrace();
                  }
              }
          }

          而HBase的存儲實現類,則是HBase Java API的常用插入語句代碼:

          ......
          // cf1:price
          Put pricePut=new Put(rowKey);
          // 必須要做是否為null判斷,否則會有空指針異常
          pricePut.addColumn(cf1, "price".getBytes(), page.getPrice() !=null ? String.valueOf(page.getPrice()).getBytes() : "".getBytes());
          puts.add(pricePut);
          // cf1:comment
          Put commentPut=new Put(rowKey);
          commentPut.addColumn(cf1, "comment".getBytes(), page.getCommentCount() !=null ? String.valueOf(page.getCommentCount()).getBytes() : "".getBytes());
          puts.add(commentPut);
          // cf1:brand
          Put brandPut=new Put(rowKey);
          brandPut.addColumn(cf1, "brand".getBytes(), page.getBrand() !=null ? page.getBrand().getBytes() : "".getBytes());
          puts.add(brandPut);
          ......

          當然,至于要將數據存儲在哪個地方,在初始化爬蟲程序時,是可以手動選擇的:

          // 3.注入存儲器
          iSpider.setStore(new HBaseStoreImpl());

          目前還沒有把代碼寫成可以同時存儲在多個地方,按照目前代碼的架構,要實現這一點也比較簡單,修改一下相應代碼就好了。實際上,是可以先把數據保存到MySQL中,然后通過Sqoop導入到HBase中,詳細操作可以參考我寫的Sqoop文章。

          仍然需要注意的是,如果確定需要將數據保存到HBase中,請保證你有可用的集群環境,并且需要將如下配置文檔添加到classpath下:

          core-site.xml
          hbase-site.xml
          hdfs-site.xml

          對大數據感興趣的同學可以折騰一下這一點,如果之前沒有接觸過的,直接使用MySQL存儲就好了,只需要在初始化爬蟲程序時注入MySQL存儲器即可:

          // 3.注入存儲器
          iSpider.setStore(new MySQLStoreImpl());

          4 URL調度系統

          URL調度系統是實現整個爬蟲系統分布式的橋梁與關鍵,正是通過URL調度系統的使用,才使得整個爬蟲系統可以較為高效(Redis作為存儲)隨機地獲取url,并實現整個系統的分布式。

          4.1 URL倉庫

          通過架構圖可以看出,所謂的URL倉庫不過是Redis倉庫,即在我們的系統中使用Redis來保存url地址列表,正是這樣,才能保證我們的程序實現分布式,只要保存了url是唯一的,這樣不管我們的爬蟲程序有多少個,最終保存下來的數據都是只有唯一一份的,而不會重復,是通過這樣來實現分布式的。

          同時url倉庫中的url地址在獲取時的策略是通過隊列的方式來實現的,待會通過URL調度器的實現即可知道。

          另外,在我們的url倉庫中,主要保存了下面的數據:

          • 種子URL列表

          Redis的數據類型為list。

          種子URL是持久化存儲的,一定時間后,由URL定時器通過種子URL獲取URL,并將其注入到我們的爬蟲程序需要使用的高優先級URL隊列中,這樣就可以保存我們的爬蟲程序可以源源不斷地爬取數據而不需要中止程序的執行。

          • 高優先級URL隊列

          Redis的數據類型為set。

          什么是高優先級URL隊列?其實它就是用來保存列表url的。

          那么什么是列表url呢?

          說白了就是一個列表中含有多個商品,以京東為列,我們打開一個手機列表為例:

          該地址中包含的不是一個具體商品的url,而是包含了多個我們需要爬取的數據(手機商品)的列表,通過對每個高級url的解析,我們可以獲取到非常多的具體商品url,而具體的商品url,就是低優先url,其會保存到低優先級URL隊列中。

          那么以這個系統為例,保存的數據類似如下:

          jd.com.higher
              --https://list.jd.com/list.html?cat=9987,653,655&page=1
              ... 
          suning.com.higher
              --https://list.suning.com/0-20006-0.html
              ...
          • 低優先級URL隊列

          Redis的數據類型為set。

          低優先級URL其實就是具體某個商品的URL,如下面一個手機商品:

          通過下載該url的數據,并對其進行解析,就能夠獲取到我們想要的數據。

          那么以這個系統為例,保存的數據類似如下:

          jd.com.lower
              --https://item.jd.com/23545806622.html
              ...
          suning.com.lower
              --https://product.suning.com/0000000000/690128156.html
              ...

          4.2 URL調度器

          所謂url調度器,其實說白了就是url倉庫java代碼的調度策略,不過因為其核心在于調度,所以將其放到URL調度器中來進行說明,目前其調度基于以下接口開發:

          /**
           * url 倉庫
           * 主要功能:
           *      向倉庫中添加url(高優先級的列表,低優先級的商品url)
           *      從倉庫中獲取url(優先獲取高優先級的url,如果沒有,再獲取低優先級的url)
           *
           */
          public interface IRepository {
          
              /**
               * 獲取url的方法
               * 從倉庫中獲取url(優先獲取高優先級的url,如果沒有,再獲取低優先級的url)
               * @return
               */
              public String poll();
          
              /**
               * 向高優先級列表中添加商品列表url
               * @param highUrl
               */
              public void offerHigher(String highUrl);
          
              /**
               * 向低優先級列表中添加商品url
               * @param lowUrl
               */
              public void offerLower(String lowUrl);
          
          }

          其基于Redis作為URL倉庫的實現如下:

          /**
           * 基于Redis的全網爬蟲,隨機獲取爬蟲url:
           *
           * Redis中用來保存url的數據結構如下:
           * 1.需要爬取的域名集合(存儲數據類型為set,這個需要先在Redis中添加)
           *      key
           *          spider.website.domains
           *      value(set)
           *          jd.com  suning.com  gome.com
           *      key由常量對象SpiderConstants.SPIDER_WEBSITE_DOMAINS_KEY 獲得
           * 2.各個域名所對應的高低優先url隊列(存儲數據類型為list,這個由爬蟲程序解析種子url后動態添加)
           *      key
           *          jd.com.higher
           *          jd.com.lower
           *          suning.com.higher
           *          suning.com.lower
           *          gome.com.higher
           *          gome.come.lower
           *      value(list)
           *          相對應需要解析的url列表
           *      key由隨機的域名 + 常量 SpiderConstants.SPIDER_DOMAIN_HIGHER_SUFFIX或者SpiderConstants.SPIDER_DOMAIN_LOWER_SUFFIX獲得
           * 3.種子url列表
           *      key
           *          spider.seed.urls
           *      value(list)
           *          需要爬取的數據的種子url
           *       key由常量SpiderConstants.SPIDER_SEED_URLS_KEY獲得
           *
           *       種子url列表中的url會由url調度器定時向高低優先url隊列中
           */
          public class RandomRedisRepositoryImpl implements IRepository {
          
              /**
               * 構造方法
               */
              public RandomRedisRepositoryImpl() {
                  init();
              }
          
              /**
               * 初始化方法,初始化時,先將redis中存在的高低優先級url隊列全部刪除
               * 否則上一次url隊列中的url沒有消耗完時,再停止啟動跑下一次,就會導致url倉庫中有重復的url
               */
              public void init() {
                  Jedis jedis=JedisUtil.getJedis();
                  Set<String> domains=jedis.smembers(SpiderConstants.SPIDER_WEBSITE_DOMAINS_KEY);
                  String higherUrlKey;
                  String lowerUrlKey;
                  for(String domain : domains) {
                      higherUrlKey=domain + SpiderConstants.SPIDER_DOMAIN_HIGHER_SUFFIX;
                      lowerUrlKey=domain + SpiderConstants.SPIDER_DOMAIN_LOWER_SUFFIX;
                      jedis.del(higherUrlKey, lowerUrlKey);
                  }
                  JedisUtil.returnJedis(jedis);
              }
          
              /**
               * 從隊列中獲取url,目前的策略是:
               *      1.先從高優先級url隊列中獲取
               *      2.再從低優先級url隊列中獲取
               *  對應我們的實際場景,應該是先解析完列表url再解析商品url
               *  但是需要注意的是,在分布式多線程的環境下,肯定是不能完全保證的,因為在某個時刻高優先級url隊列中
               *  的url消耗完了,但實際上程序還在解析下一個高優先級url,此時,其它線程去獲取高優先級隊列url肯定獲取不到
               *  這時就會去獲取低優先級隊列中的url,在實際考慮分析時,這點尤其需要注意
               * @return
               */
              @Override
              public String poll() {
                  // 從set中隨機獲取一個頂級域名
                  Jedis jedis=JedisUtil.getJedis();
                  String randomDomain=jedis.srandmember(SpiderConstants.SPIDER_WEBSITE_DOMAINS_KEY);    // jd.com
                  String key=randomDomain + SpiderConstants.SPIDER_DOMAIN_HIGHER_SUFFIX;                // jd.com.higher
                  String url=jedis.lpop(key);
                  if(url==null) {   // 如果為null,則從低優先級中獲取
                      key=randomDomain + SpiderConstants.SPIDER_DOMAIN_LOWER_SUFFIX;    // jd.com.lower
                      url=jedis.lpop(key);
                  }
                  JedisUtil.returnJedis(jedis);
                  return url;
              }
          
              /**
               * 向高優先級url隊列中添加url
               * @param highUrl
               */
              @Override
              public void offerHigher(String highUrl) {
                  offerUrl(highUrl, SpiderConstants.SPIDER_DOMAIN_HIGHER_SUFFIX);
              }
          
              /**
               * 向低優先url隊列中添加url
               * @param lowUrl
               */
              @Override
              public void offerLower(String lowUrl) {
                  offerUrl(lowUrl, SpiderConstants.SPIDER_DOMAIN_LOWER_SUFFIX);
              }
          
              /**
               * 添加url的通用方法,通過offerHigher和offerLower抽象而來
               * @param url   需要添加的url
               * @param urlTypeSuffix  url類型后綴.higher或.lower
               */
              public void offerUrl(String url, String urlTypeSuffix) {
                  Jedis jedis=JedisUtil.getJedis();
                  String domain=SpiderUtil.getTopDomain(url);   // 獲取url對應的頂級域名,如jd.com
                  String key=domain + urlTypeSuffix;            // 拼接url隊列的key,如jd.com.higher
                  jedis.lpush(key, url);                          // 向url隊列中添加url
                  JedisUtil.returnJedis(jedis);
              }
          }

          通過代碼分析也是可以知道,其核心就在如何調度url倉庫(Redis)中的url。

          4.3 URL定時器

          一段時間后,高優先級URL隊列和低優先URL隊列中的url都會被消費完,為了讓程序可以繼續爬取數據,同時減少人為的干預,可以預先在Redis中插入種子url,之后定時讓URL定時器從種子url中取出url定存放到高優先級URL隊列中,以此達到程序定時不間斷爬取數據的目的。

          url消費完畢后,是否需要循環不斷爬取數據根據個人業務需求而不同,因此這一步不是必需的,只是也提供了這樣的操作。因為事實上,我們需要爬取的數據也是每隔一段時間就會更新的,如果希望我們爬取的數據也跟著定時更新,那么這時定時器就有非常重要的作用了。不過需要注意的是,一旦決定需要循環重復爬取數據,則在設計存儲器實現時需要考慮重復數據的問題,即重復數據應該是更新操作,目前在我設計的存儲器不包括這個功能,有興趣的朋友可以自己實現,只需要在插入數據前判斷數據庫中是否存在該數據即可。

          另外需要注意的一點是,URL定時器是一個獨立的進程,需要單獨啟動。

          定時器基于Quartz實現,下面是其job的代碼:

          /**
           * 每天定時從url倉庫中獲取種子url,添加進高優先級列表
           */
          public class UrlJob implements Job {
          
              // log4j日志記錄
              private Logger logger=LoggerFactory.getLogger(UrlJob.class);
          
              @Override
              public void execute(JobExecutionContext context) throws JobExecutionException {
                  /**
                   * 1.從指定url種子倉庫獲取種子url
                   * 2.將種子url添加進高優先級列表
                   */
                  Jedis jedis=JedisUtil.getJedis();
                  Set<String> seedUrls=jedis.smembers(SpiderConstants.SPIDER_SEED_URLS_KEY);  // spider.seed.urls Redis數據類型為set,防止重復添加種子url
                  for(String seedUrl : seedUrls) {
                      String domain=SpiderUtil.getTopDomain(seedUrl);   // 種子url的頂級域名
                      jedis.sadd(domain + SpiderConstants.SPIDER_DOMAIN_HIGHER_SUFFIX, seedUrl);
                      logger.info("獲取種子:{}", seedUrl);
                  }
                  JedisUtil.returnJedis(jedis);
          //        System.out.println("Scheduler Job Test...");
              }
          
          }

          調度器的實現如下:

          
          /**
           * url定時調度器,定時向url對應倉庫中存放種子url
           *
           * 業務規定:每天凌晨1點10分向倉庫中存放種子url
           */
          public class UrlJobScheduler {
          
              public UrlJobScheduler() {
                  init();
              }
          
              /**
               * 初始化調度器
               */
              public void init() {
                  try {
                      Scheduler scheduler=StdSchedulerFactory.getDefaultScheduler();
          
                      // 如果沒有以下start方法的執行,則是不會開啟任務的調度
                      scheduler.start();
          
                      String name="URL_SCHEDULER_JOB";
                      String group="URL_SCHEDULER_JOB_GROUP";
                      JobDetail jobDetail=new JobDetail(name, group, UrlJob.class);
                      String cronExpression="0 10 1 * * ?";
                      Trigger trigger=new CronTrigger(name, group, cronExpression);
          
                      // 調度任務
                      scheduler.scheduleJob(jobDetail, trigger);
          
                  } catch (SchedulerException e) {
                      e.printStackTrace();
                  } catch (ParseException e) {
                      e.printStackTrace();
                  }
              }
          
              public static void main(String[] args) {
                  UrlJobScheduler urlJobScheduler=new UrlJobScheduler();
                  urlJobScheduler.start();
              }
          
              /**
               * 定時調度任務
               * 因為我們每天要定時從指定的倉庫中獲取種子url,并存放到高優先級的url列表中
               * 所以是一個不間斷的程序,所以不能停止
               */
              private void start() {
                  while (true) {
          
                  }
              }
          }

          5 監控報警系統

          監控報警系統的加入主要是為了讓使用者可以主動發現節點宕機,而不是被動地發現,因為實際中爬蟲程序可能是持續不斷運行的,并且我們會在多個節點上部署我們的爬蟲程序,因此很有必要對節點進行監控,并且在節點出現問題時可以及時發現并修正,需要注意的是,監控報警系統是一個獨立的進程,需要單獨啟動。

          5.1 基本原理

          首先需要先在zookeeper中創建一個/ispider節點:

          [zk: localhost:2181(CONNECTED) 1] create /ispider ispider
          Created /ispider

          監控報警系統的開發主要依賴于zookeeper實現,監控程序對zookeeper下面的這個節點目錄進行監聽:

          [zk: localhost:2181(CONNECTED) 0] ls /ispider
          []

          爬蟲程序啟動時會在該節點目錄下注冊一個臨時節點目錄:

          [zk: localhost:2181(CONNECTED) 0] ls /ispider
          [192.168.43.166]

          當節點出現宕機時,該臨時節點目錄就會被zookeeper刪除

          [zk: localhost:2181(CONNECTED) 0] ls /ispider
          []

          同時因為我們監聽了節點目錄/ispider,所以當zookeeper刪除其下的節點目錄時(或增加一個節點目錄),zookeeper會給我們的監控程序發送通知,即我們的監控程序會得到回調,這樣便可以在回調程序中執行報警的系統動作,從而完成監控報警的功能。

          5.2 zookeeper Java API使用說明

          可以使用zookeeper原生的Java API,我在另外寫的一個RPC框架(底層基于Netty實現遠程通信)中就是使用原生的API,不過顯然代碼會復雜很多,并且本身需要對zookeeper有更多的學習和了解,這樣用起來才會容易一些。

          所以為了降低開發的難度,這里使用第三方封裝的API,即curator,來進行zookeeper客戶端程序的開發。

          5.3 爬蟲系統zookeeper注冊

          在啟動爬蟲系統時,我們的程序都會啟動一個zookeeper客戶端來向zookeeper來注冊自身的節點信息,主要是ip地址,并在/ispider節點目錄以創建一個以該爬蟲程序所在的節點IP地址命名的節點,如/ispider/192.168.43.116,實現的代碼如下:

          /**
           * 注冊zk
           */
          private void registerZK() {
              String zkStr="uplooking01:2181,uplooking02:2181,uplooking03:2181";
              int baseSleepTimeMs=1000;
              int maxRetries=3;
              RetryPolicy retryPolicy=new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries);
              CuratorFramework curator=CuratorFrameworkFactory.newClient(zkStr, retryPolicy);
              curator.start();
              String ip=null;
              try {
                  // 向zk的具體目錄注冊 寫節點 創建節點
                  ip=InetAddress.getLocalHost().getHostAddress();
                  curator.create().withMode(CreateMode.EPHEMERAL).forPath("/ispider/" + ip, ip.getBytes());
              } catch (UnknownHostException e) {
                  e.printStackTrace();
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }

          應該注意到的是,我們創建的節點為臨時節點,要想實現監控報警功能,必須要為臨時節點。

          5.4 監控程序

          首先需要先監聽zookeeper中的一個節點目錄,在我們的系統中,設計是監聽/ispider這個節點目錄:

          public SpiderMonitorTask() {
              String zkStr="uplooking01:2181,uplooking02:2181,uplooking03:2181";
              int baseSleepTimeMs=1000;
              int maxRetries=3;
              RetryPolicy retryPolicy=new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries);
              curator=CuratorFrameworkFactory.newClient(zkStr, retryPolicy);
              curator.start();
              try {
                  previousNodes=curator.getChildren().usingWatcher(this).forPath("/ispider");
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }

          在上面注冊了zookeeper中的watcher,也就是接收通知的回調程序,在該程序中,執行我們報警的邏輯:

          
          /**
           * 這個方法,當監控的zk對應的目錄一旦有變動,就會被調用
           * 得到當前最新的節點狀態,將最新的節點狀態和初始或者上一次的節點狀態作比較,那我們就知道了是由誰引起的節點變化
           * @param event
           */
          @Override
          public void process(WatchedEvent event) {
              try {
                  List<String> currentNodes=curator.getChildren().usingWatcher(this).forPath("/ispider");
                  //            HashSet<String> previousNodesSet=new HashSet<>(previousNodes);
                  if(currentNodes.size() > previousNodes.size()) { // 最新的節點服務,超過之前的節點服務個數,有新的節點增加進來
                      for(String node : currentNodes) {
                          if(!previousNodes.contains(node)) {
                              // 當前節點就是新增節點
                              logger.info("----有新的爬蟲節點{}新增進來", node);
                          }
                      }
                  } else if(currentNodes.size() < previousNodes.size()) {  // 有節點掛了    發送告警郵件或者短信
                      for(String node : previousNodes) {
                          if(!currentNodes.contains(node)) {
                              // 當前節點掛掉了 得需要發郵件
                              logger.info("----有爬蟲節點{}掛掉了", node);
                              MailUtil.sendMail("有爬蟲節點掛掉了,請人工查看爬蟲節點的情況,節點信息為:", node);
                          }
                      }
                  } // 掛掉和新增的數目一模一樣,上面是不包括這種情況的,有興趣的朋友可以直接實現包括這種特殊情況的監控
                  previousNodes=currentNodes;   // 更新上一次的節點列表,成為最新的節點列表
              } catch (Exception e) {
                  e.printStackTrace();
              }
              // 在原生的API需要再做一次監控,因為每一次監控只會生效一次,所以當上面發現變化后,需要再監聽一次,這樣下一次才能監聽到
              // 但是在使用curator的API時則不需要這樣做
          }
          

          當然,判斷節點是否掛掉,上面的邏輯還是存在一定的問題的,按照上面的邏輯,假如某一時刻新增節點和刪除節點事件同時發生,那么其就不能判斷出來,所以如果需要更精準的話,可以將上面的程序代碼修改一下。

          5.5 郵件發送模塊

          使用模板代碼就可以了,不過需要注意的是,在使用時,發件人的信息請使用自己的郵箱。

          下面是爬蟲節點掛掉時接收到的郵件:

          實際上,如果購買了短信服務,那么通過短信API也可以向我們的手機發送短信。

          6 實戰:爬取京東、蘇寧易購全網手機商品數據

          因為前面在介紹這個系統的時候也提到了,我只寫了京東和蘇寧易購的網頁解析器,所以接下來也就是爬取其全網的手機商品數據。

          6.1 環境說明

          需要確保Redis、Zookeeper服務可用,另外如果需要使用HBase來存儲數據,需要確保Hadoop集群中的HBase可用,并且相關配置文件已經加入到爬蟲程序的classpath中。

          還有一點需要注意的是,URL定時器和監控報警系統是作為單獨的進程來運行的,并且也是可選的。

          6.2 爬蟲結果

          進行了兩次爬取,分別嘗試將數據保存到MySQL和HBase中,給出如下數據情況。

          6.2.1 保存到MySQL

          mysql> select count(*) from phone;
          +----------+
          | count(*) |
          +----------+
          |    12052 |
          +----------+
          1 row in set
          
          mysql> select count(*) from phone where source='jd.com';
          +----------+
          | count(*) |
          +----------+
          |     9578 |
          +----------+
          1 row in set
          
          mysql> select count(*) from phone where source='suning
          .com';
          +----------+
          | count(*) |
          +----------+
          |     2474 |
          +----------+
          1 row in set

          在可視化工具中查看數據情況:

          6.2.2 保存到HBase

          hbase(main):225:0* count 'phone'
          Current count: 1000, row: 11155386088_jd.com
          Current count: 2000, row: 136191393_suning.com
          Current count: 3000, row: 16893837301_jd.com
          Current count: 4000, row: 19036619855_jd.com
          Current count: 5000, row: 1983786945_jd.com
          Current count: 6000, row: 1997392141_jd.com
          Current count: 7000, row: 21798495372_jd.com
          Current count: 8000, row: 24154264902_jd.com
          Current count: 9000, row: 25687565618_jd.com
          Current count: 10000, row: 26458674797_jd.com
          Current count: 11000, row: 617169906_suning.com
          Current count: 12000, row: 769705049_suning.com                 
          12348 row(s) in 1.5720 seconds=> 12348

          在HDFS中查看數據情況:

          6.2.3 數據量與實際情況分析

          • 京東

          京東手機的列表大概有160多頁,每個列表有60個商品數據,所以總量在9600左右,我們的數據基本是符合的,后面通過日志分析其實可以知道,一般丟失的數據為連接超時導致的,所以在選取爬蟲的環境時,更建議在網絡環境好的主機上進行,同時如果可以有IP代理地址庫就更好了,另外對于連接超時的情況,其實是可以進一步在我們的程序中加以控制,一旦出現爬取數據失敗的url,可以將其加入到重試url隊列中,目前這一點功能我是沒有做,有興趣的同學可以試一下。

          • 蘇寧易購

          再來看看蘇寧的,其有100頁左右的手機列表,每頁也是60個商品數據,所以總量在6000左右。但可以看到,我們的數據卻只有3000這樣的數量級(缺少的依然是頻繁爬取造成的連接失敗問題),這是為什么呢?

          這是因為,打開蘇寧的某個列表頁面后,其是先加載30個商品,當鼠標向下滑動時,才會通過另外的API去加載其它的30個商品數據,每一個列表頁面都是如此,所以,實際上,我們是缺少了一半的商品數據沒有爬取。知道這個原因之后,實現也不難,但是因為時間關系,我就沒有做了,有興趣的朋友折騰一下吧。

          6.3 通過日志分析爬蟲系統的性能

          在我們的爬蟲系統中,每個關鍵的地方,如網頁下載、數據解析等都是有打logger的,所以通過日志,可以大概分析出相關的時間參數。

          2018-04-01 21:26:03 [pool-1-thread-1] [cn.xpleaf.spider.utils.HttpUtil] [INFO] - 下載網頁:https://list.jd.com/list.html?cat=9987,653,655&page=1,消耗時長:590 ms,代理信息:null:null
          2018-04-01 21:26:03 [pool-1-thread-1] [cn.xpleaf.spider.core.parser.Impl.JDHtmlParserImpl] [INFO] - 解析列表頁面:https://list.jd.com/list.html?cat=9987,653,655&page=1, 消耗時長:46ms
          2018-04-01 21:26:03 [pool-1-thread-3] [cn.xpleaf.spider.core.parser.Impl.SNHtmlParserImpl] [INFO] - 解析列表頁面:https://list.suning.com/0-20006-0.html, 消耗時長:49ms
          2018-04-01 21:26:04 [pool-1-thread-5] [cn.xpleaf.spider.utils.HttpUtil] [INFO] - 下載網頁:https://item.jd.com/6737464.html,消耗時長:219 ms,代理信息:null:null
          2018-04-01 21:26:04 [pool-1-thread-2] [cn.xpleaf.spider.utils.HttpUtil] [INFO] - 下載網頁:https://list.jd.com/list.html?cat=9987,653,655&page=2&sort=sort_rank_asc&trans=1&JL=6_0_0,消耗時長:276 ms,代理信息:null:null
          2018-04-01 21:26:04 [pool-1-thread-4] [cn.xpleaf.spider.utils.HttpUtil] [INFO] - 下載網頁:https://list.suning.com/0-20006-99.html,消耗時長:300 ms,代理信息:null:null
          2018-04-01 21:26:04 [pool-1-thread-4] [cn.xpleaf.spider.core.parser.Impl.SNHtmlParserImpl] [INFO] - 解析列表頁面:https://list.suning.com/0-20006-99.html, 消耗時長:4ms
          ......
          2018-04-01 21:27:49 [pool-1-thread-3] [cn.xpleaf.spider.utils.HttpUtil] [INFO] - 下載網頁:https://club.jd.com/comment/productCommentSummaries.action?referenceIds=23934388891,消耗時長:176 ms,代理信息:null:null
          2018-04-01 21:27:49 [pool-1-thread-3] [cn.xpleaf.spider.core.parser.Impl.JDHtmlParserImpl] [INFO] - 解析商品頁面:https://item.jd.com/23934388891.html, 消耗時長:413ms
          2018-04-01 21:27:49 [pool-1-thread-2] [cn.xpleaf.spider.utils.HttpUtil] [INFO] - 下載網頁:https://review.suning.com/ajax/review_satisfy/general-00000000010017793337-0070079092-----satisfy.htm,消耗時長:308 ms,代理信息:null:null
          2018-04-01 21:27:49 [pool-1-thread-2] [cn.xpleaf.spider.core.parser.Impl.SNHtmlParserImpl] [INFO] - 解析商品頁面:https://product.suning.com/0070079092/10017793337.html, 消耗時長:588ms
          ......

          平均下來,下載一個商品網頁數據的時間在200~500毫秒不等,當然這個還需要取決于當時的網絡情況。

          另外,如果想要真正計算爬取一個商品的數據,可以通過日志下面的數據來計算:

          • 下載一個商品頁面數據的時間
          • 獲取價格數據的時間
          • 獲取評論數據的時間

          在我的主機上(CPU:E5 10核心,內存:32GB,分別開啟1個虛擬機和3個虛擬機),情況如下:

          節點數每節點線程數商品數量時間15京東+蘇寧易購近13000個商品數據141分鐘35京東+蘇寧易購近13000個商品數據65分鐘

          可以看到,當使用3個節點時,時間并不會相應地縮小為原來的1/3,這是因為此時影響爬蟲性能的問題主要是網絡問題,節點數量多,線程數量大,網絡請求也多,但是帶寬一定,并且在沒有使用代理的情況,請求頻繁,連接失敗的情況也會增多,對時間也有一定的影響,如果使用隨機代理庫,情況將會好很多。

          但可以肯定的是,在橫向擴展增加爬蟲節點之后,確實可以大大縮小我們的爬蟲時間,這也是分布式爬蟲系統的好處。

          7 爬蟲系統中使用的反反爬蟲策略

          在整個爬蟲系統的設計中,主要使用下面的策略來達到反反爬蟲的目的:

          • 使用代理來訪問-->IP代理庫,隨機IP代理
          • 隨機頂級域名url訪問-->url調度系統
          • 每個線程每爬取完一條商品數據sleep一小段時間再進行爬取

          8 總結

          需要說明的是,本系統是基于Java實現的,但個人覺得,語言本身依然不是問題,核心在于對整個系統的設計上以及理解上,寫此文章是希望分享這樣一種分布式爬蟲系統的架構給大家,如果對源代碼感興趣,可以到我的GitHub上查看。



          GitHub:https://github.com/xpleaf/ispider

          原文地址:http://blog.51cto.com/xpleaf/2093952

          簡單匯總

          2 主要介紹

          2.1 HTTP協議簡介

          超文本傳輸協議(英文:HyperText Transfer Protocol,縮寫:HTTP)是一種用于分布式、協作式和超媒體信息系統的應用層協議。HTTP是萬維網的數據通信的基礎。

          HTTP的發展是由蒂姆·伯納斯-李于1989年在歐洲核子研究組織(CERN)所發起。HTTP的標準制定由萬維網協會(World Wide Web Consortium,W3C)和互聯網工程任務組(Internet Engineering Task Force,IETF)進行協調,最終發布了一系列的RFC,其中最著名的是1999年6月公布的 RFC 2616,定義了HTTP協議中現今廣泛使用的一個版本——HTTP 1.1。

          2014年12月,互聯網工程任務組(IETF)的Hypertext Transfer Protocol Bis(httpbis)工作小組將HTTP/2標準提議遞交至IESG進行討論,于2015年2月17日被批準。 HTTP/2標準于2015年5月以RFC 7540正式發表,取代HTTP 1.1成為HTTP的實現標準。

          2.2 HTTP協議概述

          HTTP是一個客戶端終端(用戶)和服務器端(網站)請求和應答的標準(TCP)。通過使用網頁瀏覽器、網絡爬蟲或者其它的工具,客戶端發起一個HTTP請求到服務器上指定端口(默認端口為80)。我們稱這個客戶端為用戶代理程序(user agent)。應答的服務器上存儲著一些資源,比如HTML文件和圖像。我們稱這個應答服務器為源服務器(origin server)。在用戶代理和源服務器中間可能存在多個“中間層”,比如代理服務器、網關或者隧道(tunnel)。

          盡管TCP/IP協議是互聯網上最流行的應用,HTTP協議中,并沒有規定必須使用它或它支持的層。事實上,HTTP可以在任何互聯網協議上,或其他網絡上實現。HTTP假定其下層協議提供可靠的傳輸。因此,任何能夠提供這種保證的協議都可以被其使用。因此也就是其在TCP/IP協議族使用TCP作為其傳輸層。

          通常,由HTTP客戶端發起一個請求,創建一個到服務器指定端口(默認是80端口)的TCP連接。HTTP服務器則在那個端口監聽客戶端的請求。一旦收到請求,服務器會向客戶端返回一個狀態,比如"HTTP/1.1 200 OK",以及返回的內容,如請求的文件、錯誤消息、或者其它信息。

          2.3 HTTP工作原理

          HTTP協議定義Web客戶端如何從Web服務器請求Web頁面,以及服務器如何把Web頁面傳送給客戶端。HTTP協議采用了請求/響應模型??蛻舳讼蚍掌靼l送一個請求報文,請求報文包含請求的方法、URL、協議版本、請求頭部和請求數據。服務器以一個狀態行作為響應,響應的內容包括協議的版本、成功或者錯誤代碼、服務器信息、響應頭部和響應數據。

          以下是 HTTP 請求/響應的步驟

          1. 客戶端連接到Web服務器一個HTTP客戶端,通常是瀏覽器,與Web服務器的HTTP端口(默認為80)建立一個TCP套接字連接。例如,http://www.baidu.com。

          2. 發送HTTP請求通過TCP套接字,客戶端向Web服務器發送一個文本的請求報文,一個請求報文由請求行、請求頭部、空行和請求數據4部分組成。

          3. 服務器接受請求并返回HTTP響應Web服務器解析請求,定位請求資源。服務器將資源復本寫到TCP套接字,由客戶端讀取。一個響應由狀態行、響應頭部、空行和響應數據4部分組成。

          4. 釋放連接TCP連接若connection 模式為close,則服務器主動關閉TCP連接,客戶端被動關閉連接,釋放TCP連接;若connection 模式為keepalive,則該連接會保持一段時間,在該時間內可以繼續接收請求;

          5. 客戶端瀏覽器解析HTML內容客戶端瀏覽器首先解析狀態行,查看表明請求是否成功的狀態代碼。然后解析每一個響應頭,響應頭告知以下為若干字節的HTML文檔和文檔的字符集??蛻舳藶g覽器讀取響應數據HTML,根據HTML的語法對其進行格式化,并在瀏覽器窗口中顯示。

          例如:在瀏覽器地址欄鍵入URL,按下回車之后會經歷以下流程:

          1 瀏覽器向 DNS 服務器請求解析該 URL 中的域名所對應的 IP 地址;

          2 解析出 IP 地址后,根據該 IP 地址和默認端口 80,和服務器建立TCP連接;

          3 瀏覽器發出讀取文件(URL 中域名后面部分對應的文件)的HTTP 請求,該請求報文作為 TCP 三次握手的第三個報文的數據發送給服務器;

          4 服務器對瀏覽器請求作出響應,并把對應的 html 文本發送給瀏覽器;

          5 釋放 TCP連接;

          6 瀏覽器將該 html文本并顯示內容;  


          http協議是基于TCP/IP協議之上的應用層協議。

            基于 請求-響應 的模式

              HTTP協議規定,請求從客戶端發出,最后服務器端響應該請求并 返回。換句話說,肯定是先從客戶端開始建立通信的,服務器端在沒有 接收到請求之前不會發送響應


          無狀態保存

              HTTP是一種不保存狀態,即無狀態(stateless)協議。HTTP協議 自身不對請求和響應之間的通信狀態進行保存。也就是說在HTTP這個 級別,協議對于發送過的請求或響應都不做持久化處理。



           使用HTTP協議,每當有新的請求發送時,就會有對應的新響應產 生。協議本身并不保留之前一切的請求或響應報文的信息。這是為了更快地處理大量事務,確保協議的可伸縮性,而特意把HTTP協議設計成 如此簡單的。可是,隨著Web的不斷發展,因無狀態而導致業務處理變得棘手 的情況增多了。比如,用戶登錄到一家購物網站,即使他跳轉到該站的 其他頁面后,也需要能繼續保持登錄狀態。針對這個實例,網站為了能 夠掌握是誰送出的請求,需要保存用戶的狀態。HTTP/1.1雖然是無狀態協議,但為了實現期望的保持狀態功能, 于是引入了Cookie技術。有了Cookie再用HTTP協議通信,就可以管 理狀態了。有關Cookie的詳細內容稍后講解。

            無連接

              無連接的含義是限制每次連接只處理一個請求。服務器處理完客戶的請求,并收到客戶的應答后,即斷開連接。采用這種方式可以節省傳輸時間,并且可以提高并發性能,不能和每個用戶建立長久的連接,請求一次相應一次,服務端和客戶端就中斷了。但是無連接有兩種方式,早期的http協議是一個請求一個響應之后,直接就斷開了,但是現在的http協議1.1版本不是直接就斷開了,而是等幾秒鐘,這幾秒鐘是等什么呢,等著用戶有后續的操作,如果用戶在這幾秒鐘之內有新的請求,那么還是通過之前的連接通道來收發消息,如果過了這幾秒鐘用戶沒有發送新的請求,那么就會斷開連接,這樣可以提高效率,減少短時間內建立連接的次數,因為建立連接也是耗時的,默認的好像是3秒中現在,但是這個時間是可以通過咱們后端的代碼來調整的,自己網站根據自己網站用戶的行為來分析統計出一個最優的等待時間。

          2.4 在TCP/IP協議棧中的位置

          HTTP協議通常承載于TCP協議之上,有時也承載于TLS或SSL協議層之上,這個時候,就成了我們常說的HTTPS。如下圖所示:

          默認HTTP的端口號為80,HTTPS的端口號為443。

          2.5 HTTP的請求響應模型

          HTTP協議永遠都是客戶端發起請求,服務器回送響應。見下圖:

          這樣就限制了使用HTTP協議,無法實現在客戶端沒有發起請求的時候,服務器將消息推送給客戶端。

          HTTP協議是一個無狀態的協議,同一個客戶端的這次請求和上次請求是沒有對應關系。

          2.6 工作流程

          一次HTTP操作稱為一個事務,其工作過程可分為四步:

          1)首先客戶機與服務器需要建立連接。只要單擊某個超級鏈接,HTTP的工作開始。

          2)建立連接后,客戶機發送一個請求給服務器,請求方式的格式為:統一資源標識符(URL)、協議版本號,后邊是MIME信息包括請求修飾符、客戶機信息和可能的內容。

          3)服務器接到請求后,給予相應的響應信息,其格式為一個狀態行,包括信息的協議版本號、一個成功或錯誤的代碼,后邊是MIME信息包括服務器信息、實體信息和可能的內容。

          4)客戶端接收服務器所返回的信息通過瀏覽器顯示在用戶的顯示屏上,然后客戶機與服務器斷開連接。

          2.7 使用Wireshark抓TCP、http包

          打開Wireshark,選擇工具欄上的“Capture”->“Options”,界面選擇如圖1所示:

          一般讀者只需要選擇最上邊的下拉框,選擇合適的Device,而后點擊“Capture Filter”,此處選擇的是“HTTP TCP port(80)”,選擇后點擊上圖的“Start”開始抓包。

          在上圖中,可清晰地看到客戶端瀏覽器(ip為192.168.2.33)與服務器的交互過程:

          1)No1:瀏覽器(192.168.2.33)向服務器(220.181.50.118)發出連接請求。此為TCP三次握手第一步,此時從圖中可以看出,為SYN,seq:X (x=0)

          2)No2:服務器(220.181.50.118)回應了瀏覽器(192.168.2.33)的請求,并要求確認,此時為:SYN,ACK,此時seq:y(y為0),ACK:x+1(為1)。此為三次握手的第二步;

          3)No3:瀏覽器(192.168.2.33)回應了服務器(220.181.50.118)的確認,連接成功。為:ACK,此時seq:x+1(為1),ACK:y+1(為1)。此為三次握手的第三步;

          4)No4:瀏覽器(192.168.2.33)發出一個頁面HTTP請求;

          5)No5:服務器(220.181.50.118)確認;

          6)No6:服務器(220.181.50.118)發送數據;

          7)No7:客戶端瀏覽器(192.168.2.33)確認;

          8)No14:客戶端(192.168.2.33)發出一個圖片HTTP請求;

          9)No15:服務器(220.181.50.118)發送狀態響應碼200 OK

          1.6 頭域

          每個頭域由一個域名,冒號(:)和域值三部分組成。域名是大小寫無關的,域值前可以添加任何數量的空格符,頭域可以被擴展為多行,在每行開始處,使用至少一個空格或制表符。

          在抓包的圖中,No14點開可看到如圖4所示:

          回應的消息如圖所示:

          1.6.1 host頭域

          Host頭域指定請求資源的Intenet主機和端口號,必須表示請求url的原始服務器或網關的位置。HTTP/1.1請求必須包含主機頭域,否則系統會以400狀態碼返回。

          圖5中host那行為:

          1.6.2 Referer頭域

          Referer頭域允許客戶端指定請求uri的源資源地址,這可以允許服務器生成回退鏈表,可用來登陸、優化cache等。他也允許廢除的或錯誤的連接由于維護的目的被追蹤。如果請求的uri沒有自己的uri地址,Referer不能被發送。如果指定的是部分uri地址,則此地址應該是一個相對地址。

          在圖4中,Referer行的內容為:

          1.6.3 User-Agent頭域

          User-Agent頭域的內容包含發出請求的用戶信息。

          在圖4中,User-Agent行的內容為:

          http://www.blogjava.net/images/blogjava_net/amigoxie/40799/o_http%e5%8d%8f%e8%ae%ae%e5%ad%a6%e4%b9%a0-%e6%a6%82%e5%bf%b5-8.jpg

          1.6.4 Cache-Control頭域

          Cache-Control指定請求和響應遵循的緩存機制。在請求消息或響應消息中設置Cache-Control并不會修改另一個消息處理過程中的緩存處理過程。請求時的緩存指令包括no-cache、no-store、max-age、max-stale、min-fresh、only-if-cached,響應消息中的指令包括public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age。

          在圖5中的該頭域為:

          1.6.5 Date頭域

          Date頭域表示消息發送的時間,時間的描述格式由rfc822定義。例如,Date:Mon,31Dec200104:25:57GMT。Date描述的時間表示世界標準時,換算成本地時間,需要知道用戶所在的時區。

          圖5中,該頭域如下圖所示:

          1.7 HTTP的幾個重要概念

          1.7.1連接:Connection

          一個傳輸層的實際環流,它是建立在兩個相互通訊的應用程序之間。

          在http1.1,request和reponse頭中都有可能出現一個connection的頭,此header的含義是當client和server通信時對于長鏈接如何進行處理。

          在http1.1中,client和server都是默認對方支持長鏈接的, 如果client使用http1.1協議,但又不希望使用長鏈接,則需要在header中指明connection的值為close;如果server方也不想支持長鏈接,則在response中也需要明確說明connection的值為close。不論request還是response的header中包含了值為close的connection,都表明當前正在使用的tcp鏈接在當前請求處理完畢后會被斷掉。以后client再進行新的請求時就必須創建新的tcp鏈接了。

          1.7.2消息:Message

          HTTP通訊的基本單位,包括一個結構化的八元組序列并通過連接傳輸。

          1.7.3請求:Request

          一個從客戶端到服務器的請求信息包括應用于資源的方法、資源的標識符和協議的版本號。

          1.7.4響應:Response

          一個從服務器返回的信息包括HTTP協議的版本號、請求的狀態(例如“成功”或“沒找到”)和文檔的MIME類型。

          1.7.5資源:Resource

          由URI標識的網絡數據對象或服務。

          1.7.6實體:Entity

          數據資源或來自服務資源的回映的一種特殊表示方法,它可能被包圍在一個請求或響應信息中。一個實體包括實體頭信息和實體的本身內容。

          1.7.7客戶機:Client

          一個為發送請求目的而建立連接的應用程序。

          1.7.8用戶代理:UserAgent

          初始化一個請求的客戶機。它們是瀏覽器、編輯器或其它用戶工具。

          1.7.9服務器:Server

          一個接受連接并對請求返回信息的應用程序。

          1.7.10源服務器:Originserver

          是一個給定資源可以在其上駐留或被創建的服務器。

          1.7.11代理:Proxy

          一個中間程序,它可以充當一個服務器,也可以充當一個客戶機,為其它客戶機建立請求。請求是通過可能的翻譯在內部或經過傳遞到其它的服務器中。一個代理在發送請求信息之前,必須解釋并且如果可能重寫它。

          代理經常作為通過防火墻的客戶機端的門戶,代理還可以作為一個幫助應用來通過協議處理沒有被用戶代理完成的請求。

          1.7.12網關:Gateway

          一個作為其它服務器中間媒介的服務器。與代理不同的是,網關接受請求就好象對被請求的資源來說它就是源服務器;發出請求的客戶機并沒有意識到它在同網關打交道。

          網關經常作為通過防火墻的服務器端的門戶,網關還可以作為一個協議翻譯器以便存取那些存儲在非HTTP系統中的資源。

          1.7.13通道:Tunnel

          是作為兩個連接中繼的中介程序。一旦激活,通道便被認為不屬于HTTP通訊,盡管通道可能是被一個HTTP請求初始化的。當被中繼的連接兩端關閉時,通道便消失。當一個門戶(Portal)必須存在或中介(Intermediary)不能解釋中繼的通訊時通道被經常使用。

          1.7.14緩存:Cache

          反應信息的局域存儲。

          3 協議詳解篇

          2.1 HTTP/1.0和HTTP/1.1的比較

          RFC 1945定義了HTTP/1.0版本,RFC 2616定義了HTTP/1.1版本。

          筆者在blog上提供了這兩個RFC中文版的下載地址。

          RFC1945下載地址:

          http://www.blogjava.Net/Files/amigoxie/RFC1945(HTTP)中文版.rar

          RFC2616下載地址:

          http://www.blogjava.net/Files/amigoxie/RFC2616(HTTP)中文版.rar

          2.1.1建立連接方面

          HTTP/1.0 每次請求都需要建立新的TCP連接,連接不能復用。HTTP/1.1 新的請求可以在上次請求建立的TCP連接之上發送,連接可以復用。優點是減少重復進行TCP三次握手的開銷,提高效率。

          注意:在同一個TCP連接中,新的請求需要等上次請求收到響應后,才能發送。

          2.1.2 Host域

          HTTP1.1在Request消息頭里頭多了一個Host域, HTTP1.0則沒有這個域。

          Eg:

          GET /pub/WWW/TheProject.html HTTP/1.1

          Host: www.w3.org

          可能HTTP1.0的時候認為,建立TCP連接的時候已經指定了IP地址,這個IP地址上只有一個host。

          2.1.3日期時間戳

          (接收方向)

          無論是HTTP1.0還是HTTP1.1,都要能解析下面三種date/time stamp:

          Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123

          Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036

          Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format

          (發送方向)

          HTTP1.0要求不能生成第三種asctime格式的date/time stamp;

          HTTP1.1則要求只生成RFC 1123(第一種)格式的date/time stamp。

          2.1.4狀態響應碼

          狀態響應碼100 (Continue) 狀態代碼的使用,允許客戶端在發request消息body之前先用request header試探一下server,看server要不要接收request body,再決定要不要發request body。

          客戶端在Request頭部中包含

          Expect: 100-continue

          Server看到之后呢如果回100 (Continue) 這個狀態代碼,客戶端就繼續發request body。這個是HTTP1.1才有的。

          另外在HTTP/1.1中還增加了101、203、205等等性狀態響應碼

          2.1.5請求方式

          HTTP1.1增加了OPTIONS, PUT, DELETE, TRACE, CONNECT這些Request方法.

          Method="OPTIONS" ; Section 9.2

          | "GET" ; Section 9.3

          | "HEAD" ; Section 9.4

          | "POST" ; Section 9.5

          | "PUT" ; Section 9.6

          | "DELETE" ; Section 9.7

          | "TRACE" ; Section 9.8

          | "CONNECT" ; Section 9.9

          | extension-method

          extension-method=token

          2.2 HTTP請求消息

          2.2.1請求消息格式

          請求消息格式如下所示:

          請求行

          通用信息頭|請求頭|實體頭

          CRLF(回車換行)

          實體內容

          其中“請求行”為:請求行=方法 [空格] 請求URI [空格] 版本號 [回車換行]

          請求行實例:

          Eg1:

          GET /index.html HTTP/1.1

          Eg2:

          POST http://192.168.2.217:8080/index.jsp HTTP/1.1

          HTTP請求消息實例:

          GET /hello.htm HTTP/1.1
          Accept: */*
          Accept-Language: zh-cn
          Accept-Encoding: gzip, deflate
          If-Modified-Since: Wed, 17 Oct 2007 02:15:55 GMT
          If-None-Match: W/"158-1192587355000"
          User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)
          Host: 192.168.2.162:8080
          Connection: Keep-Alive


          2.2.2請求方法

          HTTP的請求方法包括如下幾種:

          GET

          POST

          HEAD

          PUT

          DELETE

          OPTIONS

          TRACE

          CONNECT

          2.3 HTTP響應消息

          2.3.1響應消息格式

          HTTP響應消息的格式如下所示:

          狀態行

          通用信息頭|響應頭|實體頭

          CRLF

          實體內容

          其中:狀態行=版本號 [空格] 狀態碼 [空格] 原因 [回車換行]

          狀態行舉例:

          Eg1:
          HTTP/1.0 200 OK 
                Eg2:
          HTTP/1.1 400 Bad Request

          HTTP響應消息實例如下所示:

          HTTP/1.1 200 OK
          ETag: W/"158-1192590101000"
          Last-Modified: Wed, 17 Oct 2007 03:01:41 GMT
          Content-Type: text/html
          Content-Length: 158
          Date: Wed, 17 Oct 2007 03:01:59 GMT
          Server: Apache-Coyote/1.1


          2.3.2 http的狀態響應碼

          2.3.2.1 1**:請求收到,繼續處理

          100——客戶必須繼續發出請求

          101——客戶要求服務器根據請求轉換HTTP協議版本

          2.3.2.2 2**:操作成功收到,分析、接受

          200——交易成功

          201——提示知道新文件的URL

          202——接受和處理、但處理未完成

          203——返回信息不確定或不完整

          204——請求收到,但返回信息為空

          205——服務器完成了請求,用戶代理必須復位當前已經瀏覽過的文件

          206——服務器已經完成了部分用戶的GET請求

          2.3.2.3 3**:完成此請求必須進一步處理

          300——請求的資源可在多處得到

          301——刪除請求數據

          302——在其他地址發現了請求數據

          303——建議客戶訪問其他URL或訪問方式

          304——客戶端已經執行了GET,但文件未變化

          305——請求的資源必須從服務器指定的地址得到

          306——前一版本HTTP中使用的代碼,現行版本中不再使用

          307——申明請求的資源臨時性刪除

          2.3.2.4 4**:請求包含一個錯誤語法或不能完成

          400——錯誤請求,如語法錯誤

          401——未授權

          HTTP 401.1 - 未授權:登錄失敗

            HTTP 401.2 - 未授權:服務器配置問題導致登錄失敗

            HTTP 401.3 - ACL 禁止訪問資源

            HTTP 401.4 - 未授權:授權被篩選器拒絕

          HTTP 401.5 - 未授權:ISAPI 或 CGI 授權失敗

          402——保留有效ChargeTo頭響應

          403——禁止訪問

          HTTP 403.1 禁止訪問:禁止可執行訪問

            HTTP 403.2 - 禁止訪問:禁止讀訪問

            HTTP 403.3 - 禁止訪問:禁止寫訪問

            HTTP 403.4 - 禁止訪問:要求 SSL

            HTTP 403.5 - 禁止訪問:要求 SSL 128

            HTTP 403.6 - 禁止訪問:IP 地址被拒絕

            HTTP 403.7 - 禁止訪問:要求客戶證書

            HTTP 403.8 - 禁止訪問:禁止站點訪問

            HTTP 403.9 - 禁止訪問:連接的用戶過多

            HTTP 403.10 - 禁止訪問:配置無效

            HTTP 403.11 - 禁止訪問:密碼更改

            HTTP 403.12 - 禁止訪問:映射器拒絕訪問

            HTTP 403.13 - 禁止訪問:客戶證書已被吊銷

            HTTP 403.15 - 禁止訪問:客戶訪問許可過多

            HTTP 403.16 - 禁止訪問:客戶證書不可信或者無效

          HTTP 403.17 - 禁止訪問:客戶證書已經到期或者尚未生效

          404——沒有發現文件、查詢或URl

          405——用戶在Request-Line字段定義的方法不允許

          406——根據用戶發送的Accept拖,請求資源不可訪問

          407——類似401,用戶必須首先在代理服務器上得到授權

          408——客戶端沒有在用戶指定的餓時間內完成請求

          409——對當前資源狀態,請求不能完成

          410——服務器上不再有此資源且無進一步的參考地址

          411——服務器拒絕用戶定義的Content-Length屬性請求

          412——一個或多個請求頭字段在當前請求中錯誤

          413——請求的資源大于服務器允許的大小

          414——請求的資源URL長于服務器允許的長度

          415——請求資源不支持請求項目格式

          416——請求中包含Range請求頭字段,在當前請求資源范圍內沒有range指示值,請求也不包含If-Range請求頭字段

          417——服務器不滿足請求Expect頭字段指定的期望值,如果是代理服務器,可能是下一級服務器不能滿足請求長。

          2.3.2.5 5**:服務器執行一個完全有效請求失敗

            HTTP 500 - 內部服務器錯誤

            HTTP 500.100 - 內部服務器錯誤 - ASP 錯誤

            HTTP 500-11 服務器關閉

            HTTP 500-12 應用程序重新啟動

            HTTP 500-13 - 服務器太忙

            HTTP 500-14 - 應用程序無效

            HTTP 500-15 - 不允許請求 global.asa

            Error 501 - 未實現

          HTTP 502 - 網關錯誤

          2.4 使用telnet進行http測試

          在Windows下,可使用命令窗口進行http簡單測試。

          輸入cmd進入命令窗口,在命令行鍵入如下命令后按回車:

          telnet www.baidu.com 80

          而后在窗口中按下“Ctrl+]”后按回車可讓返回結果回顯。

          接著開始發請求消息,例如發送如下請求消息請求baidu的首頁消息,使用的HTTP協議為HTTP/1.1:

          GET /index.html HTTP/1.1

          注意:copy如上的消息到命令窗口后需要按兩個回車換行才能得到響應的消息,第一個回車換行是在命令后鍵入回車換行,是HTTP協議要求的。第二個是確認輸入,發送請求。

          可看到返回了200 OK的消息,如下圖所示:

          可看到,當采用HTTP/1.1時,連接不是在請求結束后就斷開的。若采用HTTP1.0,在命令窗口鍵入:

          GET /index.html HTTP/1.0

          此時可以看到請求結束之后馬上斷開。

          讀者還可以嘗試在使用GET或POST等時,帶上頭域信息,例如鍵入如下信息:

          GET /index.html HTTP/1.1

          connection: close

          Host: www.baidu.com

          2.5 常用的請求方式

          常用的請求方式是GET和POST.

          l GET方式:是以實體的方式得到由請求URI所指定資源的信息,如果請求URI只是一個數據產生過程,那么最終要在響應實體中返回的是處理過程的結果所指向的資源,而不是處理過程的描述。

          l POST方式:用來向目的服務器發出請求,要求它接受被附在請求后的實體,并把它當作請求隊列中請求URI所指定資源的附加新子項,Post被設計成用統一的方法實現下列功能:

          1:對現有資源的解釋;

          2:向電子公告欄、新聞組、郵件列表或類似討論組發信息;

          3:提交數據塊;

          4:通過附加操作來擴展數據庫 。

          從上面描述可以看出,Get是向服務器發索取數據的一種請求;而Post是向服務器提交數據的一種請求,要提交的數據位于信息頭后面的實體中。

          GET與POST方法有以下區別:

          (1) 在客戶端,Get方式在通過URL提交數據,數據在URL中可以看到;POST方式,數據放置在HTML HEADER內提交。

          (2) GET方式提交的數據最多只能有1024字節,而POST則沒有此限制。

          (3) 安全性問題。正如在(1)中提到,使用 Get 的時候,參數會顯示在地址欄上,而 Post 不會。所以,如果這些數據是中文數據而且是非敏感數據,那么使用 get;如果用戶輸入的數據不是中文字符而且包含敏感數據,那么還是使用 post為好。

          (4) 安全的和冪等的。所謂安全的意味著該操作用于獲取信息而非修改信息。冪等的意味著對同一 URL 的多個請求應該返回同樣的結果。完整的定義并不像看起來那樣嚴格。換句話說,GET 請求一般不應產生副作用。從根本上講,其目標是當用戶打開一個鏈接時,她可以確信從自身的角度來看沒有改變資源。比如,新聞站點的頭版不斷更新。雖然第二次請求會返回不同的一批新聞,該操作仍然被認為是安全的和冪等的,因為它總是返回當前的新聞。反之亦然。POST 請求就不那么輕松了。POST 表示可能改變服務器上的資源的請求。仍然以新聞站點為例,讀者對文章的注解應該通過 POST 請求實現,因為在注解提交之后站點已經不同了(比方說文章下面出現一條注解)。

          2.6 請求頭

          HTTP最常見的請求頭如下:

          l Accept:瀏覽器可接受的MIME類型;

          l Accept-Charset:瀏覽器可接受的字符集;

          l Accept-Encoding:瀏覽器能夠進行解碼的數據編碼方式,比如gzip。Servlet能夠向支持gzip的瀏覽器返回經gzip編碼的HTML頁面。許多情形下這可以減少5到10倍的下載時間;

          l Accept-Language:瀏覽器所希望的語言種類,當服務器能夠提供一種以上的語言版本時要用到;

          l Authorization:授權信息,通常出現在對服務器發送的WWW-Authenticate頭的應答中;

          l Connection:表示是否需要持久連接。如果Servlet看到這里的值為“Keep-Alive”,或者看到請求使用的是HTTP 1.1(HTTP 1.1默認進行持久連接),它就可以利用持久連接的優點,當頁面包含多個元素時(例如Applet,圖片),顯著地減少下載所需要的時間。要實現這一點,Servlet需要在應答中發送一個Content-Length頭,最簡單的實現方法是:先把內容寫入ByteArrayOutputStream,然后在正式寫出內容之前計算它的大??;

          l Content-Length:表示請求消息正文的長度;

          l Cookie:這是最重要的請求頭信息之一;

          l From:請求發送者的email地址,由一些特殊的Web客戶程序使用,瀏覽器不會用到它;

          l Host:初始URL中的主機和端口;

          l If-Modified-Since:只有當所請求的內容在指定的日期之后又經過修改才返回它,否則返回304“Not Modified”應答;

          l Pragma:指定“no-cache”值表示服務器必須返回一個刷新后的文檔,即使它是代理服務器而且已經有了頁面的本地拷貝;

          l Referer:包含一個URL,用戶從該URL代表的頁面出發訪問當前請求的頁面。

          l User-Agent:瀏覽器類型,如果Servlet返回的內容與瀏覽器類型有關則該值非常有用;

          l UA-Pixels,UA-Color,UA-OS,UA-CPU:由某些版本的IE瀏覽器所發送的非標準的請求頭,表示屏幕大小、顏色深度、操作系統和CPU類型。

          2.7 響應頭

          HTTP最常見的響應頭如下所示:

          l Allow:服務器支持哪些請求方法(如GET、POST等);

          l Content-Encoding:文檔的編碼(Encode)方法。只有在解碼之后才可以得到Content-Type頭指定的內容類型。利用gzip壓縮文檔能夠顯著地減少HTML文檔的下載時間。Java的GZIPOutputStream可以很方便地進行gzip壓縮,但只有Unix上的Netscape和Windows上的IE 4、IE 5才支持它。因此,Servlet應該通過查看Accept-Encoding頭(即request.getHeader("Accept-Encoding"))檢查瀏覽器是否支持gzip,為支持gzip的瀏覽器返回經gzip壓縮的HTML頁面,為其他瀏覽器返回普通頁面;

          l Content-Length:表示內容長度。只有當瀏覽器使用持久HTTP連接時才需要這個數據。如果你想要利用持久連接的優勢,可以把輸出文檔寫入ByteArrayOutputStram,完成后查看其大小,然后把該值放入Content-Length頭,最后通過byteArrayStream.writeTo(response.getOutputStream()發送內容;

          l Content-Type: 表示后面的文檔屬于什么MIME類型。Servlet默認為text/plain,但通常需要顯式地指定為text/html。由于經常要設置Content-Type,因此HttpServletResponse提供了一個專用的方法setContentTyep。 可在web.xml文件中配置擴展名和MIME類型的對應關系;

          l Date:當前的GMT時間。你可以用setDateHeader來設置這個頭以避免轉換時間格式的麻煩;

          l Expires:指明應該在什么時候認為文檔已經過期,從而不再緩存它。

          l Last-Modified:文檔的最后改動時間??蛻艨梢酝ㄟ^If-Modified-Since請求頭提供一個日期,該請求將被視為一個條件GET,只有改動時間遲于指定時間的文檔才會返回,否則返回一個304(Not Modified)狀態。Last-Modified也可用setDateHeader方法來設置;

          l Location:表示客戶應當到哪里去提取文檔。Location通常不是直接設置的,而是通過HttpServletResponse的sendRedirect方法,該方法同時設置狀態代碼為302;

          l Refresh:表示瀏覽器應該在多少時間之后刷新文檔,以秒計。除了刷新當前文檔之外,你還可以通過setHeader("Refresh", "5; URL=http://host/path")讓瀏覽器讀取指定的頁面。注意這種功能通常是通過設置HTML頁面HEAD區的http://host/path">實現,這是因為,自動刷新或重定向對于那些不能使用CGI或Servlet的HTML編寫者十分重要。但是,對于Servlet來說,直接設置Refresh頭更加方便。注意Refresh的意義是“N秒之后刷新本頁面或訪問指定頁面”,而不是“每隔N秒刷新本頁面或訪問指定頁面”。因此,連續刷新要求每次都發送一個Refresh頭,而發送204狀態代碼則可以阻止瀏覽器繼續刷新,不管是使用Refresh頭還是。注意Refresh頭不屬于HTTP 1.1正式規范的一部分,而是一個擴展,但Netscape和IE都支持它。

          2.8實體頭

          實體頭用坐實體內容的元信息,描述了實體內容的屬性,包括實體信息類型,長度,壓縮方法,最后一次修改時間,數據有效性等。

          l Allow:GET,POST

          l Content-Encoding:文檔的編碼(Encode)方法,例如:gzip,見“2.5 響應頭”;

          l Content-Language:內容的語言類型,例如:zh-cn;

          l Content-Length:表示內容長度,eg:80,可參考“2.5響應頭”;

          l Content-Location:表示客戶應當到哪里去提取文檔,例如:http://www.dfdf.org/dfdf.html,可參考“2.5響應頭”;

          l Content-MD5:MD5 實體的一種MD5摘要,用作校驗和。發送方和接受方都計算MD5摘要,接受方將其計算的值與此頭標中傳遞的值進行比較。Eg1:Content-MD5: 。Eg2:dfdfdfdfdfdfdff==;

          l Content-Range:隨部分實體一同發送;標明被插入字節的低位與高位字節偏移,也標明此實體的總長度。Eg1:Content-Range: 1001-2000/5000,eg2:bytes 2543-4532/7898

          l Content-Type:標明發送或者接收的實體的MIME類型。Eg:text/html; charset=GB2312 主類型/子類型;

          l Expires:為0證明不緩存;

          l Last-Modified:WEB 服務器認為對象的最后修改時間,比如文件的最后修改時間,動態頁面的最后產生時間等等。例如:Last-Modified:Tue, 06 May 2008 02:42:43 GMT.

          2.8擴展頭

          在HTTP消息中,也可以使用一些再HTTP1.1正式規范里沒有定義的頭字段,這些頭字段統稱為自定義的HTTP頭或者擴展頭,他們通常被當作是一種實體頭處理。

          現在流行的瀏覽器實際上都支持Cookie,Set-Cookie,Refresh和Content-Disposition等幾個常用的擴展頭字段。

          l Refresh:1;url=http://www.dfdf.org //過1秒跳轉到指定位置;

          l Content-Disposition:頭字段,可參考“2.5響應頭”;

          l Content-Type:WEB 服務器告訴瀏覽器自己響應的對象的類型。

          eg1:Content-Type:application/xml ;

          eg2:applicaiton/octet-stream;

          Content-Disposition:attachment; filename=aaa.zip。

          3 深入了解篇

          3.1 Cookie和Session

          Cookie和Session都為了用來保存狀態信息,都是保存客戶端狀態的機制,它們都是為了解決HTTP無狀態的問題而所做的努力。

          Session可以用Cookie來實現,也可以用URL回寫的機制來實現。用Cookie來實現的Session可以認為是對Cookie更高級的應用。

          3.1.1兩者比較

          Cookie和Session有以下明顯的不同點:

          1)Cookie將狀態保存在客戶端,Session將狀態保存在服務器端;

          2)Cookies是服務器在本地機器上存儲的小段文本并隨每一個請求發送至同一個服務器。Cookie最早在RFC2109中實現,后續RFC2965做了增強。網絡服務器用HTTP頭向客戶端發送cookies,在客戶終端,瀏覽器解析這些cookies并將它們保存為一個本地文件,它會自動將同一服務器的任何請求縛上這些cookies。Session并沒有在HTTP的協議中定義;

          3)Session是針對每一個用戶的,變量的值保存在服務器上,用一個sessionID來區分是哪個用戶session變量,這個值是通過用戶的瀏覽器在訪問的時候返回給服務器,當客戶禁用cookie時,這個值也可能設置為由get來返回給服務器;

          4)就安全性來說:當你訪問一個使用session 的站點,同時在自己機子上建立一個cookie,建議在服務器端的SESSION機制更安全些.因為它不會任意讀取客戶存儲的信息。

          3.1.2 Session機制

          Session機制是一種服務器端的機制,服務器使用一種類似于散列表的結構(也可能就是使用散列表)來保存信息。

          當程序需要為某個客戶端的請求創建一個session的時候,服務器首先檢查這個客戶端的請求里是否已包含了一個session標識 - 稱為 session id,如果已包含一個session id則說明以前已經為此客戶端創建過session,服務器就按照session id把這個 session檢索出來使用(如果檢索不到,可能會新建一個),如果客戶端請求不包含session id,則為此客戶端創建一個session并且生成一個與此session相關聯的session id,session id的值應該是一個既不會重復,又不容易被找到規律以仿造的字符串,這個 session id將被在本次響應中返回給客戶端保存。

          3.1.6 Session的實現方式

          3.1.6.1 使用Cookie來實現

          服務器給每個Session分配一個唯一的JSESSIONID,并通過Cookie發送給客戶端。

          當客戶端發起新的請求的時候,將在Cookie頭中攜帶這個JSESSIONID。這樣服務器能夠找到這個客戶端對應的Session。

          流程如下圖所示:

          3.1.6.2 使用URL回顯來實現

          URL回寫是指服務器在發送給瀏覽器頁面的所有鏈接中都攜帶JSESSIONID的參數,這樣客戶端點擊任何一個鏈接都會把JSESSIONID帶會服務器。

          如果直接在瀏覽器輸入服務端資源的url來請求該資源,那么Session是匹配不到的。

          Tomcat對Session的實現,是一開始同時使用Cookie和URL回寫機制,如果發現客戶端支持Cookie,就繼續使用Cookie,停止使用URL回寫。如果發現Cookie被禁用,就一直使用URL回寫。jsp開發處理到Session的時候,對頁面中的鏈接記得使用response.encodeURL() 。

          3.1.3在J2EE項目中Session失效的幾種情況

          1)Session超時:Session在指定時間內失效,例如30分鐘,若在30分鐘內沒有操作,則Session會失效,例如在web.xml中進行了如下設置:

          30 //單位:分鐘

          2)使用session.invalidate()明確的去掉Session。

          3.1.4與Cookie相關的HTTP擴展頭

          1)Cookie:客戶端將服務器設置的Cookie返回到服務器;

          2)Set-Cookie:服務器向客戶端設置Cookie;

          3)Cookie2 (RFC2965)):客戶端指示服務器支持Cookie的版本;

          4)Set-Cookie2 (RFC2965):服務器向客戶端設置Cookie。

          3.1.5Cookie的流程

          服務器在響應消息中用Set-Cookie頭將Cookie的內容回送給客戶端,客戶端在新的請求中將相同的內容攜帶在Cookie頭中發送給服務器。從而實現會話的保持。

          流程如下圖所示:

          3.2 緩存的實現原理

          3.2.1什么是Web緩存

          WEB緩存(cache)位于Web服務器和客戶端之間。

          緩存會根據請求保存輸出內容的副本,例如html頁面,圖片,文件,當下一個請求來到的時候:如果是相同的URL,緩存直接使用副本響應訪問請求,而不是向源服務器再次發送請求。

          HTTP協議定義了相關的消息頭來使WEB緩存盡可能好的工作。

          3.2.2緩存的優點

          1 減少相應延遲:因為請求從緩存服務器(離客戶端更近)而不是源服務器被相應,這個過程耗時更少,讓web服務器看上去相應更快。

          2 減少網絡帶寬消耗:當副本被重用時會減低客戶端的帶寬消耗;客戶可以節省帶寬費用,控制帶寬的需求的增長并更易于管理。

          3.2.3與緩存相關的HTTP擴展消息頭

          q Expires:指示響應內容過期的時間,格林威治時間GMT

          q Cache-Control:更細致的控制緩存的內容

          q Last-Modified:響應中資源最后一次修改的時間

          q ETag:響應中資源的校驗值,在服務器上某個時段是唯一標識的。

          q Date:服務器的時間

          q If-Modified-Since:客戶端存取的該資源最后一次修改的時間,同Last-Modified。

          q If-None-Match:客戶端存取的該資源的檢驗值,同ETag。

          3.2.4客戶端緩存生效的常見流程

          服務器收到請求時,會在200OK中回送該資源的Last-Modified和ETag頭,客戶端將該資源保存在cache中,并記錄這兩個屬性。當客戶端需要發送相同的請求時,會在請求中攜帶If-Modified-Since和If-None-Match兩個頭。兩個頭的值分別是響應中Last-Modified和ETag頭的值。服務器通過這兩個頭判斷本地資源未發生變化,客戶端不需要重新下載,返回304響應。常見流程如下圖所示:


          3.2.5 Web緩存機制

          HTTP/1.1中緩存的目的是為了在很多情況下減少發送請求,同時在許多情況下可以不需要發送完整響應。前者減少了網絡回路的數量;HTTP利用一個“過期(expiration)”機制來為此目的。后者減少了網絡應用的帶寬;HTTP用“驗證(validation)”機制來為此目的。

          HTTP定義了3種緩存機制:

          1)Freshness:允許一個回應消息可以在源服務器不被重新檢查,并且可以由服務器和客戶端來控制。例如,Expires回應頭給了一個文檔不可用的時間。Cache-Control中的max-age標識指明了緩存的最長時間;

          2)Validation:用來檢查以一個緩存的回應是否仍然可用。例如,如果一個回應有一個Last-Modified回應頭,緩存能夠使用If-Modified-Since來判斷是否已改變,以便判斷根據情況發送請求;

          3)Invalidation: 在另一個請求通過緩存的時候,常常有一個副作用。例如,如果一個URL關聯到一個緩存回應,但是其后跟著POST、PUT和DELETE的請求的話,緩存就會過期。

          3.3 斷點續傳和多線程下載的實現原理

          q HTTP協議的GET方法,支持只請求某個資源的某一部分;

          q 206 Partial Content 部分內容響應;

          q Range 請求的資源范圍;

          q Content-Range 響應的資源范圍;

          q 在連接斷開重連時,客戶端只請求該資源未下載的部分,而不是重新請求整個資源,來實現斷點續傳。

          分塊請求資源實例:

          Eg1:Range: bytes=306302- :請求這個資源從306302個字節到末尾的部分;

          Eg2:Content-Range: bytes 306302-604047/604048:響應中指示攜帶的是該資源的第306302-604047的字節,該資源共604048個字節;

          客戶端通過并發的請求相同資源的不同片段,來實現對某個資源的并發分塊下載。從而達到快速下載的目的。目前流行的FlashGet和迅雷基本都是這個原理。

          多線程下載的原理:

          q 下載工具開啟多個發出HTTP請求的線程;

          q 每個http請求只請求資源文件的一部分:Content-Range: bytes 20000-40000/47000;

          q 合并每個線程下載的文件。

          3.4 https通信過程

          3.4.1什么是https

          HTTPS(全稱:Hypertext Transfer Protocol over Secure Socket Layer),是以安全為目標的HTTP通道,簡單講是HTTP的安全版。即HTTP下加入SSL層,HTTPS的安全基礎是SSL,因此加密的詳細內容請看SSL。

          見下圖:

          https所用的端口號是443。

          3.4.2 https的實現原理

          有兩種基本的加解密算法類型:

          1)對稱加密:密鑰只有一個,加密解密為同一個密碼,且加解密速度快,典型的對稱加密算法有DES、AES等;

          2)非對稱加密:密鑰成對出現(且根據公鑰無法推知私鑰,根據私鑰也無法推知公鑰),加密解密使用不同密鑰(公鑰加密需要私鑰解密,私鑰加密需要公鑰解密),相對對稱加密速度較慢,典型的非對稱加密算法有RSA、DSA等。

          下面看一下https的通信過程:

          https通信的優點:

          1)客戶端產生的密鑰只有客戶端和服務器端能得到;

          2)加密的數據只有客戶端和服務器端才能得到明文;

          3)客戶端到服務端的通信是安全的。

          3.5 http代理

          3.5.1 http代理服務器

          代理服務器英文全稱是Proxy Server,其功能就是代理網絡用戶去取得網絡信息。形象的說:它是網絡信息的中轉站。

          代理服務器是介于瀏覽器和Web服務器之間的一臺服務器,有了它之后,瀏覽器不是直接到Web服務器去取回網頁而是向代理服務器發出請求,Request信號會先送到代理服務器,由代理服務器來取回瀏覽器所需要的信息并傳送給你的瀏覽器。

          而且,大部分代理服務器都具有緩沖的功能,就好象一個大的Cache,它有很大的存儲空間,它不斷將新取得數據儲存到它本機的存儲器上,如果瀏覽器所請求的數據在它本機的存儲器上已經存在而且是最新的,那么它就不重新從Web服務器取數據,而直接將存儲器上的數據傳送給用戶的瀏覽器,這樣就能顯著提高瀏覽速度和效率。

          更重要的是:Proxy Server(代理服務器)是Internet鏈路級網關所提供的一種重要的安全功能,它的工作主要在開放系統互聯(OSI)模型的對話層。

          3.5.2 http代理服務器的主要功能

          主要功能如下:

          1)突破自身IP訪問限制,訪問國外站點。如:教育網、169網等網絡用戶可以通過代理訪問國外網站;

          2)訪問一些單位或團體內部資源,如某大學FTP(前提是該代理地址在該資源的允許訪問范圍之內),使用教育網內地址段免費代理服務器,就可以用于對教育 網開放的各類FTP下載上傳,以及各類資料查詢共享等服務;

          3)突破中國電信的IP封鎖:中國電信用戶有很多網站是被限制訪問的,這種限制是人為的,不同Serve對地址的封鎖是不同的。所以不能訪問時可以換一個國 外的代理服務器試試;

          4)提高訪問速度:通常代理服務器都設置一個較大的硬盤緩沖區,當有外界的信息通過時,同時也將其保存到緩沖區中,當其他用戶再訪問相同的信息時, 則直接由緩沖區中取出信息,傳給用戶,以提高訪問速度;

          5)隱藏真實IP:上網者也可以通過這種方法隱藏自己的IP,免受攻擊。

          3.5.3 http代理圖示

          http代理的圖示見下圖:

          對于客戶端瀏覽器而言,http代理服務器相當于服務器。

          而對于Web服務器而言,http代理服務器又擔當了客戶端的角色。

          3.6 虛擬主機的實現

          3.6.1什么是虛擬主機

          虛擬主機:是在網絡服務器上劃分出一定的磁盤空間供用戶放置站點、應用組件等,提供必要的站點功能與數據存放、傳輸功能。

          所謂虛擬主機,也叫“網站空間”就是把一臺運行在互聯網上的服務器劃分成多個“虛擬”的服務器,每一個虛擬主機都具有獨立的域名和完整的Internet服務器(支持WWW、FTP、E-mail等)功能。一臺服務器上的不同虛擬主機是各自獨立的,并由用戶自行管理。但一臺服務器主機只能夠支持一定數量的虛擬主機,當超過這個數量時,用戶將會感到性能急劇下降。

          3.6.2虛擬主機的實現原理

          虛擬主機是用同一個WEB服務器,為不同域名網站提供服務的技術。Apache、Tomcat等均可通過配置實現這個功能。

          相關的HTTP消息頭:Host。

          例如:Host: www.baidu.com

          客戶端發送HTTP請求的時候,會攜帶Host頭,Host頭記錄的是客戶端輸入的域名。這樣服務器可以根據Host頭確認客戶要訪問的是哪一個域名。



          《計算機網絡基礎系列》,會持續更新,想了解的朋友可以關注 ,文章有幫助的話可以長按點贊有驚喜?。?!文章比較長,大家可以先 收藏、轉發后再看,有什么補充可以在下面評論,謝謝大家!


          主站蜘蛛池模板: 久久一区二区三区精华液使用方法| 日韩精品一区二区三区毛片| 在线日韩麻豆一区| 久久精品无码一区二区三区免费| 日本一区免费电影| 久久一区二区精品| 国内国外日产一区二区| 福利一区二区三区视频午夜观看| 午夜福利一区二区三区在线观看 | 无码一区二区波多野结衣播放搜索 | 精品无码一区二区三区亚洲桃色| 一区二区三区杨幂在线观看| 日韩精品一区二区三区老鸦窝| 亚欧在线精品免费观看一区| 国产成人精品日本亚洲专一区| 91国偷自产一区二区三区| 精品人妻系列无码一区二区三区 | ...91久久精品一区二区三区| 2022年亚洲午夜一区二区福利| 日韩欧国产精品一区综合无码| 久久se精品一区二区国产| 精品性影院一区二区三区内射 | 亚洲综合av一区二区三区 | 无码人妻久久一区二区三区蜜桃 | 中文字幕日韩欧美一区二区三区| 精品国产精品久久一区免费式 | 精品日本一区二区三区在线观看| 一区二区三区四区在线播放 | 久久精品一区二区三区中文字幕| 日韩社区一区二区三区| 无码人妻精品一区二区三| 国产一区二区在线观看| 国产精品 视频一区 二区三区| 激情内射亚洲一区二区三区爱妻| 国产成人精品无码一区二区老年人| 精品一区二区三区在线视频观看 | 日韩一区二区超清视频| 国精品无码一区二区三区在线蜜臀 | 国产美女露脸口爆吞精一区二区 | 2021国产精品视频一区| 中文字幕精品一区二区三区视频|