iOS開發經驗(13)-網絡

目錄

  1. 網絡基本概念
  2. TCP/IP協議簇基本概念
  3. HTTP
  4. 網絡開發技術解決方案
  5. 數據解析
  6. 網絡優化
1. 網絡基本概念

為了使不同計算機廠家生產的計算機能夠相互通信,以便在更大的范圍內建立計算機網絡,國際標準化組織(ISO)在1978年提出了“開放系統互聯參考模型”,即著名的OSI/RM模型。它將計算機網絡體系結構的通信協議劃分為七層:

  • 模型分層思想:

| 協議 | 具體
| -------------
| 應用層 | OSI 的應用層協議包括文件的傳輸、訪問及管理協議(FTAM) ,以及文件虛擬終端協議(VIP)和公用管理系統信息(CMIP)等;規定數據的傳輸協議;eg:RJ45等將數據轉化成0和1
| 表示層 | 表示層供多種功能用于應用層數據編碼和轉化,以確保以一個系統應用層發送的信息 可以被另一個系統應用層識別,可以理解為:解決不同系統之間的通信,eg:Linux下的QQ和Windows下的QQ可以通信??
| 會話層 | 會話層建立、管理和終止表示層與實體之間的通信會話;建立一個連接(自動的手機信息、自動的網絡尋址);
| 傳輸層 | 傳輸層向高層??提供可靠的端到端的網絡數據流服務。可以理解為:每一個應用程序都會在網卡注冊一個端口號,該層就是端口與端口的通信!常用的(TCP/IP)協議;
| 網絡層 | 網絡層負責在源和終點之間建立連接;可以理解為,此處需要確定計算機的位置,怎么確定?IPv4,IPv6!
| 數據鏈路層 | 數據鏈路層通過物理網絡鏈路??供數據傳輸。不同數據鏈路層定義了不同的網絡和協 議特征,其中包括物理編址、網絡拓撲結構、錯誤校驗、數據幀序列以及流控;可以簡單的理解為:規定了0和1的分包形式,確定了網絡數據包的形式;
| 物理層 | 物理層負責最后將信息編碼成電流脈沖或其它信號用于網上傳輸;eg:RJ45等將數據轉化成0和1;

其中第四層完成數據傳送服務,上面三層面向用戶。對于每一層,至少制定兩項標準:服務定義和協議規范。前者給出了該層所提供的服務的準確定義,后者詳細描述了該協議的動作和各種有關規程,以保證服務的提供。

網絡層次分層.png
2. TCP/IP協議簇基本概念

基本概念
TCP/IP協議簇(協議簇通常指彼此相關聯的一系列協議的總稱),譯名為傳輸控制協議/因特網互聯協議,又名網絡通訊協議,是Internet最基本的協議、Internet國際互聯網的基礎,由網絡層的IP協議和傳輸層的TCP協議組成。TCP/IP定義了電子設備如何連入因特網,以及數據如何在它們之間傳輸的標準。協議采用了4層的層級結構,每一層都呼叫它的下一層所提供的協議來完成自己的需求。通俗而言:TCP負責發現傳輸的問題,一有問題就發出信號,要求重新傳輸,直到所有數據安全正確地傳輸到目的地。而IP是給因特網的每一臺聯網設備規定一個地址。
不同于OSI/RM的七層結構,TCP/IP模型是一個四層模型,從上到下依次是:

應用層
傳輸層
網絡層
物理鏈路層

值得一提的是,傳輸層可以使用兩種不同的協議,一個是面向連接的傳輸控制協議(TCP),另一個是無連接的用戶數據報協議(UDP),我們耳熟能詳的的協議如HTTP、FTP、POP3、DNS等都屬于應用層的協議,它們要么構建在TCP之上,要么構建在UDP之上。

| 協議 | 具體
| -------------
| 應用層協議 | FTP · HTTP· XMPP ·POP3 · SMTP
| 傳輸層協議 | TCP · UDP · TLS
| 網絡層協議 | IP (IPv4 · IPv6)
| 物理鏈路層 | Wi-Fi· GPRS·以太網 · 調制解調器

常見的應用層協議:

| 協議 | 端口 | 說明
| -------------
| HTTP | 80 | 超文本傳輸協議
| HTTPS | 443 | HTTP+SSL
| POP3 | 110 | 郵件協議

傳輸層
傳輸層(Transport Layer)是TCP/IP協議簇(四層模型)中最重要、最關鍵的一層,它負責總體的數據傳輸和數據控制的一層,傳輸層提供端到端(應用會在網卡注冊一個端口號)的交換數據的機制,檢查分組編號與次序。傳輸層對其上三層如會話層等,提供可靠的傳輸服務,對網絡層提供可靠的目的地站點信息。

  • 傳輸層的核心協議是TCP和UDP。
  • 傳輸層它為應用層提供會話和數據報通信服務。
  • 傳輸層承擔OSI傳輸層的職責。

TCP

  • TCP基本概念
    TCP提供一對一的、面向連接的可靠通信服務。TCP建立連接,對發送的數據包進行排序和確認,并恢復在傳輸過程中丟失的數據包。與TCP不同,UDP提供一對一或一對多的、無連接的不可靠通信服務。
    不論是TCP/IP還是在OSI參考模型中,任意相鄰兩層的下層為服務提供者,上層為服務調用者。下層為上層提供的服務可分為兩類:面向連接服務和無連接服務。

  • TCP工作原理-三次握手**
    TCP的連接建立過程又稱為TCP三次握手;
    首先發送方主機向接收方主機發起一個建立連接的同步(SYN)請求;
    接收方主機在收到這個請求后向發送方主機回復一個同步/確認(SYN/ACK)應答;
    發送方主機收到此包后再向接收方主機發送一個確認(ACK),此時TCP連接成功建立.
    一旦初始的三次握手完成,在發送和接收主機之間將按順序發送和確認段。關閉連接之前,TCP使用類似的握手過程驗證兩個主機是否都完成發送和接收全部數據。
    完成三次握手,客戶端與服務器開始傳送數據

UDP

  • UDP全稱是User Datagram Protocol,中文名為用戶數據報協議。UDP 提供無連接的網絡服務,該服務對消息中傳輸的數據提供不可靠的、最大努力傳送。這意味著它不保證數據報的到達,也不保證所傳送數據包的順序是否正確。
    我最初就有一個疑惑:“既然UDP是一種不可靠的網絡協議,那么還有什么使用價值或必要呢?”
    在有些情況下UDP可能會變得非常有用。因為UDP具有TCP所望塵莫及的速度優勢。雖然TCP中植入了各種安全保障功能,但是在實際執行的過程中會占用大量的系統開銷,無疑使速度受到嚴重的影響。反觀UDP由于排除了信息可靠傳遞機制,將安全和排序等功能移交給上層應用來完成,極大地降低了執行時間,使速度得到了保證。

IP地址
IP地址[主機名],英文全稱:Internet Protocol Address,又譯為網際協議地址。IP協議的作用就是把各種數據包傳給對方。為了能確保準確送達,所以需要IP地址和MAC地址,IP地址指明了節點被分配的地址,MAC地址指的是網卡所屬的固定地址,IP地址可變換,但是MAC地址是不會改變的。所以IP間的通信就需要MAC地址,這里就需要知道IP地址和MAC地址的對應關系,所以又出現了另一種協議ARP(Address Resolution Protocol),能通過IP地址查詢到MAC地址。
1.網絡中設備的標識,用來唯一標識每一臺計算機。通常現在常用的IP地址是IPV4地址。
2.IPV4就是有4段數字,格式是xxx.xxx.xxx.xxx,每一段數字由8位二進制做成,取值范圍是0~255。
3.IPV4采用32位地址長度,只有大約43億個地址,IPv4定義的有限地址空間將被耗盡。
4.為了擴大地址空間,擬通過IPV6重新定義地址空間。IPv6采用128位地址長度,幾乎可以不受限制地提供地址,但IPV6現在還沒有正式普及。
為了解決IPV4有限地址空間的問題,IP地址又分內網地址和外網地址。(比如校園網,每一個學生都會有一個內網地址,學校會有一個路由器,路由器會有個外網地址,學生想要上外網都必須通過路由器出去,只要通過同一個路由器出去的,他們對應的外網地址都是一樣的)
本質上所有的網絡訪問是通過ip地址訪問的,域名是一個速記符號,不用記住IP地址復雜的數字。
本地回環地址:127.0.0.1 主機名:localhost
5.每臺計算機都有一個127.0.0.1
如果127.0.0.1 ping不通,說明網卡不工作(比如裝黑蘋果,檢測網卡驅動有沒裝好,可以 ping下回環地址)
如果本機地址 ping不通,說明網線壞了。

端口號
端口號用來標識進程的邏輯地址,不同進程的標識。
有效的端口:0~65535。
其中0~1024由系統使用或保留端口。
開發中不要使用1024以下的端口。
(端口有什么用呢?我們知道,一臺擁有IP地址的主機可以提供許多服務,比如Web服務、FTP服務、SMTP服務等,這些服務完全可以通過1個IP地址來實現。那么,主機是怎樣區分不同的網絡服務呢?顯然不能只靠IP地址,因為IP 地址與網絡服務的關系是一對多的關系。實際上是通過“IP地址+端口號”來區 分不同的服務的。)

** DNS 「Domain Name System」域名系統協議**
DNS 就是進行域名解析的服務器
DNS 命名用于 Internet 等 TCP/IP 網絡中,通過用戶友好的名稱查找計算機和服務
用于命名、組織到域層次結構中的計算機和網絡服務,可以簡單地理解為 將 URL 轉換為 IP 地址
域名:是由圓點分開一串單詞或縮寫組成的,域名與 IP 地址之間是一一對應的

Socket 簡介
Socket(套接字)是通信的基石,是 應用層 和 傳輸層 之間的橋梁,是支持TCP/IP協議的網絡通信的基本操作單元,網絡通信其實就是 Socket 間的通信「數據在兩個 Socket 間通過 IO 傳輸」。
HTTP 支持短鏈接 Socket 支持長鏈接 用于IM。
Socket包含進行網絡通信必須的五種信息:連接使用的協議,本地主機的IP地址,本地進程的協議端口,遠地主機的IP地址,遠地進程的協議端口。

通常情況下Socket連接就是TCP連接,因此Socket連接一旦建立,通信雙方即可開始相互發送數據內容,直到雙方連接斷開。但在實際應用中,客戶端到服務器之間的通信防火墻默認會關閉長時間處于非活躍狀態的連接而導致 Socket 連接斷連,因此需要通過輪詢告訴網絡,該連接處于活躍狀態。

而HTTP連接使用的是“請求—響應”的方式,不僅在請求時需要先建立連接,而且需要客戶端向服務器發出請求后,服務器端才能回復數據。

HTTP 與 Socket 的區別

  • Socket 是對 TCP/IP 協議的封裝,Socket 本身并不是協議,而是一個調用接口「API」
  • 通過 Socket 我們才能使用 TCP/IP 協議
  • HTTP 是基于 Socket 的實現;HTTP 應用層協議,主要解決如何包裝數據
  • HTTP 傳輸的數據格式是規定好的,Socket 實現數據傳輸是最原始,Socket 實現的數據傳輸格式可自定義

Socket通信.png

Socket 的連接過程
長連接:指在一個連接上可以連續發送多個數據包,在連接保持期間,如果沒有數據包發送,需要雙方發鏈路檢測包
短連接:指通訊雙方有數據交互時,就建立一個連接,數據發送完成后,則斷開此連接,即每次連接只完成一項業務的發送

3. HTTP協議

后來發明了瀏覽器,瀏覽器通過超文本傳輸協議(HTTP)跟服務器交換超文本數據,通過圖形用戶界面顯示從服務器獲得的超文本數據,這一切都讓使用Internet變得無比簡單,于是計算機網絡的用戶數量開始爆炸式的增長。
我們先來解釋一下什么是協議以及HTTP到底是一個怎樣的協議。我們將任何可發送或接收信息的硬件或程序稱之為實體,而協議則是控制兩個對等實體進行通信的規則的集合。簡單的說,協議就是通信雙方必須遵循的對話的標準和規范。HTTP是構建在TCP之上的協議,之所以選擇TCP作為底層傳輸協議是因為TCP除了可以保證可靠通信之外,還具備流量控制和擁塞控制的能力,如果這一點不能理解也不要緊,我么只需要知道HTTP需要可靠的傳輸層協議的支持就夠了。

再次重復一遍:
超文本傳輸協議(HTTP,HyperText Transfer Protocol)是互聯網上應用最為廣泛的一種網絡協議。
HTTP協議規定了客戶端和服務器之間的數據傳輸格式.

HTTP優點:

  • 簡單快速: HTTP協議簡單,通信速度很快;
  • 靈活: HTTP協議允許傳輸任意類型的數據;
  • 短連接: HTTP協議限制每次連接只處理一個請求,服務器對客戶端的請求作出響應后,馬上斷開連接.這種方式可以節省傳輸時間.

HTTP請求/響應:
HTTP有兩種類型的報文:請求報文和響應報文。請求報文和響應報文都是由三個部分組成的。

請求報文是由請求行、請求頭和消息體構成的。

  • 請求行包含了命令(通常是GET或POST)、資源和協議版本;
  • 請求頭是鍵值對映射形式的和請求相關的信息,如客戶端使用的語言、使用的瀏覽器等信息;
  • 消息體是客戶端發給服務器的數據;在請求頭和消息體之間有一個空行。

響應報文是由響應行、響應頭和消息體構成的。

  • 響應行包含了協議版本和狀態碼;
  • 響應頭是鍵值對形式的和響應相關的信息,如服務器的軟件版本、時間日期、緩存策略、響應內容類型等信息;
  • 消息體是服務器發給客戶端的數據;在響應頭和消息體之間有一個空行。

HTTP基本通信過程

請求:客戶端向服務器索要數據
響應:服務器返回客戶端相應的數據
1.確定請求路徑url
http://www.baidu.com:80/tools.html
2.獲取主機名
www.baidu.com
3.DNS域名解析
192.168.31.1
4.獲得端口號80
5.連接到192.168.31.1的端口80
6.發送HTTP GET請求 
7.接收到服務器的響應
8.關閉連接

HTTP請求方法
HTTP:是應用層的網絡傳輸協議

  • 對于HTTP的請求方式包括
eg:GET、POST、OPTIONS、HEAD、PUT、DELETE、TRACE、CONNECT、PATCH

其中最常用的是GET和POST方法。
因為GET和POST可以實現上述所有操作,所以,在現實開發中,GET和POST方法使用的最為廣泛,除此以外HEAD請求使用頻率也比較高。

  • 連接方式也包括兩種:同步連接和異步連接
    此外定義了其他方法對應不同的資源操作:

GET和POST的區別:
1.GET請求:服務器的地址和請求參數 都會出現在請求接口中,也就是說 服務器地址和請求參數共同組成了請求接口,而POST請求,請求參數不會出現在請求接口中,作為請求體,提交給路由器。
2.因為GET請求的請求參數會出現請求接口中所以信息容易被捕獲,安全性低,而POST請求的請求參數封裝在請求體中,作為二進制流進行傳輸,安全性高。
3.GET請求的請求接口中,有請求參數,而對于請求接口我們有字節限制,這樣我們導致GET請求有一定局限性。所以,對于GET請求只能上傳小型數據,而對于POST請求,請求體 理論上可以無限大,所以一般來說,從服務器請求數據用GET,上傳數據用POST。

相關API
NSURL
URL的格式如下所示:

協議://域名或IP地址:端口號/路徑/資源

下面是百度logo的URL:

http://www.baidu.com:80/img/bd_logo1.png

URL的基本格式 = 協議://主機地址/資源路徑

http://www.example.com/img/logo.png
----- ================ ------------
協議名        主機地址      資源路徑

http://8.8.8.8:8080/img/bdlogo.gif
----- ============= ---------------
協議名  主機地址+端口    資源路徑

說明:端口號是對IP地址的擴展。例如我們的服務器只有一個IP地址,但是我們可以在這臺服務器上開設多個服務,如Web服務、郵件服務和數據庫服務,當服務器收到一個請求時會根據端口號來區分到底請求的是Web服務還是郵件服務,或者是數據庫服務。我們在瀏覽器中輸入URL的時候通常都會省略端口號,因為HTTP協議默認使用80端口,也就是說除非你訪問的Web服務器沒有使用80端口,你才需要輸入相應的端口號。

狀態碼

| 狀態碼 | 英文名稱 | 描述
| -------------
| 200 | OK | 請求成功
| 400 | Bad Request | 客戶端請求的語法錯誤,服務器無法解析
| 404 | Not Found | 服務器無法根據客戶端的請求找到資源
| 500 | Internal Server Error | 服務器內部錯誤,無法完成請求

HTTPS簡介
HTTPS是以安全為目標的HTTP通道,簡單講是HTTP的安全版。即HTTP下加入SSL層,HTTPS的安全基礎是SSL,因此加密的詳細內容就需要SSL。

4. 網絡開發技術解決方案

在iOS中,常見的發送HTTP請求的方案包括:

  • 蘋果官方

| 名稱 | 說明
| -------------
| NSURLConnection | iOS 2.0 推出,用法簡單,最古老最經典最直接的一種方案,在 iOS 9.0 被廢棄
| NSURLSession | iOS 7 推出,功能比 NSURLConnection 更加強大
| CFNetwork |NSURL 的底層,純C語言,幾乎不用

  • 第三方框架

| 名稱 | 說明
| -------------
| AFNetworking |NSURLConnection & NSURLSession 簡單易用,提供了基本夠用的常用功能,維護和使用者多

1.NSURLConnection
在iOS 7以前,基于HTTP協議聯網的操作最終都要由NSURLConnection類來完成。
利用NSURLConnection進行網絡請求分為3步:

  • 創建請求路徑(NSURL)
  • 創建請求對象(NSURLRequest)
  • 發送請求(NSURLConnection)

發送請求主要有兩個方法,一個用于發送同步請求,一個用于發送異步請求。

// 發送同步請求的方法:該方法是阻塞式的會卡住線程。
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request 
      returningResponse:(NSURLResponse **)response error:(NSError **)error;

// 發送異步請求的方法
+ (void)sendAsynchronousRequest:(NSURLRequest *)request 
      queue:(NSOperationQueue *)queue 
      completionHandler:(void (^)(NSURLResponse *response, NSData *data, NSError *connectionError))handler

提示:同步請求是阻塞式請求,這就意味著同步請求的方法在返回數據之前會一直阻塞;異步請求是非阻塞式請求,當服務器返回數據時可以回調的方式對數據進行處理。如果明白這一點,就很容易理解為什么上面的同步請求方法會返回NSData指針,而異步請求方法沒有返回值但有一個Block類型的參數(Block最適合用來書寫回調代碼)。

1.get 同步:代碼正常寫法
get 異步:block寫法;get異步:代理寫法

2.post同步:代碼正常寫法
post異步 :代理寫法; post異步:block寫法

3.同步下,都是代碼正常寫法,異步下,才會用代理或者block
get,post跟數據安全及大小有關系,跟線程無關系。同步異步只跟線程有關系。
4.代理相較于block優勢:下載文件可監聽文件下載進度

2.NSURLSession
2013的WWDC,蘋果推出了NSURLConnection的繼任者NSURLSession。與NSURLConnection相比,NSURLsession最直接的改進就是可以配置每個會話(session)的緩存、協議、cookie以及證書策略(credential policy)等,而且你可以跨程序共享這些信息。每個NSURLSession對象都由一個NSURLSessionConfiguration對象來進行初始化,NSURLSessionConfiguration對象代表了會話的配置以及一些用來增強移動設備上性能的新選項。

URL加載系統的API.png

NSURLSession優勢:(相較于NSURLConnection)

  • 支持后臺上傳和下載:在程序在前臺時,NSURLSession與NSURLConnection可以相互的替代。但是當用戶在對程序進行強制關閉的時候此時NSURLSession會默認的自動斷開。相比而言NSURLSession的優勢主要體現在后臺操作時候,而且在最流行的框架AFNetworking中也對NSURLSession提供了更好的支持。
  • 在處理下載任務的時候可以直接把數據下載到磁盤:NSURLConnection下載文件時,先是將整個文件下載到內存,然后再寫入到沙盒,如果文件比較大,就會出現內存暴漲的情況。
    而使用NSURLSessionUploadTask下載文件,會默認下載到沙盒中的tem文件中,不會出現內存暴漲的情況,但是在下載完成后會把tem中的臨時文件刪除,需要在初始化任務方法時,在completionHandler回調中增加保存文件的代碼。(后面會詳細說)
  • 下載速度: 支持HTTP 2.0協議,HTTP/2比HTTP/1.1在速度上快很多
  • 能夠暫停和恢復網絡操作:暫停以后可以通過繼續恢復當前的請求任務。
  • 同一個 session 發送多個請求,只需要建立一次連接(復用了TCP);
  • 下載的時候是多線程異步處理,效率更高;
  • 可配置容器:提供了全局的 session 并且可以統一配置,使用更加方便。對于NSURLSession里面的requests來說,每個NSURLSession都是可配置的容器。舉個例來說,假如你需要設置HTTP header選項,你只用做一次,session里面的每個request就會有同樣的配置。
  • 提高認證處理:認證是在一個指定的連接基礎上完成的。在使用NSURLConnection時,如果發出一個訪問,會返回一個任意的request。此時,你就不能確切的知道哪個request收到了訪問。而在NSURLSession中,就能用代理處理認證。

NSURLSession主要有三個類:

  • NSURLSession:NSURLSession類以及相關類提供了一組用于通過HTTP下載內容的API,這組API中具有許多委托方法用來進行身份驗證、后臺下載等活動,具有異步性。
  • NSURLSessionConfiguration:該類定義了使用NSURLSession對象進行上傳和下載時的行為和策略,在使用NSURLSession對象時,總是要首先定義該類對象。
  • NSURLSessionTask:該類是NSURLSession中所有任務的基類,task是session的一部分。任務通過調用NSURLSession對象中的方法進行創建。

注:NSURLSession的行為由session的類型(取決于NSURLSessionConfiguration類型決定)、task類型以及創建task時,App是運行在前臺還是后臺

1. NSURLSession
NSURLSession 本身是不會進行請求的,而是通過創建 task 的形式進行網絡請求,同一個 NSURLSession 可以創建多個 task,并且這些 task 之間的 cache 和 cookie 是共享的。那么我們就來看看 NSURLSession 都能創建哪些 task 吧。

2. NSURLSessionConfiguration:會話Session控制
如前面所述,NSURLSessionConfiguration代表了會話的配置,該類的三個創建對象的類方法很好的詮釋了NSURLSession類設計時所考慮的不同的使用場景。
一般情況下全局的Session我們夠用了,但是如果遇到兩個連接使用不同配置的情況,就得重新配置session
NSURLSession支持的3種會話配置:

  1. defaultSessionConfiguration (default)
    進程內會話(默認會話),用硬盤來緩存數據
  2. ephemeralSessionConfiguration (ephemeral)
    臨時的進程內會話,不會將cookie,緩存儲存到本地,只會放在內存中,當應用程序退出后數據也會消失。(瀏覽器的無痕瀏覽模式)
  3. backgroundSessionConfiguration (background)
    后臺會話,相比默認會話,該會話會在后臺開啟一個線程進行網絡數據處理。
// 返回一個標準的配置,標準配置會使用默認的緩存策略、超時時間等
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;
// 返回一個臨時性的配置,這個配置中不會對緩存,Cookie和證書進行持久化存儲
// 對于實現無痕瀏覽這種功能來說這種配置是非常理想的
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;
// 返回一個后臺配置
// 后臺會話不同于普通的會話,它甚至可以在應用程序掛起,退出或者崩潰的情況下運行上傳和下載任務
// 初始化時指定的標識符,被用于向任何可能在進程外恢復后臺傳輸的守護進程(daemon)提供上下文
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:
      (NSString *)identifier

常規配置屬性 :

/* 常規屬性 - 常規屬性有7個  */ 
/*background 模式下需要標識 */
@property (nullable, readonly, copy) NSString *identifier;

/*超時屬性 ,用作設置timeoutIntervalForRequest時間內, 如果沒有請求數據發送,則請求超時 */
@property NSTimeInterval timeoutIntervalForRequest;

/* 超時屬性, 用作設置timeoutIntervalForResource時間內,如果沒有返回響應,則響應超時 */
@property NSTimeInterval timeoutIntervalForResource;

/* 網絡服務類型 */
typedef NS_ENUM(NSUInteger, NSURLRequestNetworkServiceType)
{
    NSURLNetworkServiceTypeDefault = 0, // Standard internet traffic
    NSURLNetworkServiceTypeVoIP = 1, // Voice over IP control traffic  
    NSURLNetworkServiceTypeVideo = 2, // Video traffic
    NSURLNetworkServiceTypeBackground = 3, // Background traffic
    NSURLNetworkServiceTypeVoice = 4    // Voice data
};
// 這個屬性一般用于音頻處理流
@property NSURLRequestNetworkServiceType networkServiceType;

// 設置這個屬性, 會為所有的NSURLSessionTask加上基礎request頭,可以統一請求頭
@property (nullable, copy) NSDictionary *HTTPAdditionalHeaders;

// 一個布爾值,確定是否通過蜂窩網絡連接, 默認為YES , 
@property BOOL allowsCellularAccess;

// iOS8.0后才能用, backgroundSessionConfigurationWithIdentifier中的identifier中對應使用, 被標識的文件,會在后臺中下載
@property (nullable, copy) NSString *sharedContainerIdentifier NS_AVAILABLE(10_10, 8_0);

使用NSURLSessionConfiguration配置一個默認session的具體方法

NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.allowsCellularAccess = true;//是否允許蜂窩網絡下載(2G/3G/4G)
[sessionConfig setHTTPAdditionalHeaders: @{@"Accept": @"application/json"}];//所有的請求只接收JSON數據
sessionConfig.timeoutIntervalForRequest = 25.0f;//請求超時時間
sessionConfig.timeoutIntervalForResource = 60.0;
sessionConfig.HTTPMaximumConnectionsPerHost = 1;//限制一個主機只有一個網絡連接。

//創建會話,指定配置和代理
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig 
                                                  delegate:self 

3. NSURLSessionTask
NSURLSession在使用上需要先根據會話對象(NSURLSession)創建一個請求NSURLSessionTask(會話任務。NSURLSessionTask是抽象類,我們需要使用它的子類,先看看關系:

NSURLSession類繼承關系.png

會話任務有三個子類對應不同的場景,分別是:

  • NSURLSessionDataTask(獲取數據的任務):使用NSData對象收發數據,主要用于CS之間的臨時請求。數據任務可以在每次接收到數據后將數據提交給App,也可以通過處理程序一次將所有數據返回給App。
  • NSURLSessionDownloadTask(下載任務):以文件的形式檢索數據,支持后臺下載。
  • NSURLSessionUploadTask(上傳任務):以文件形式發送數據,支持后臺上傳。

說明:代理相較于block優勢:下載文件可監聽文件下載進度!

已經有了NSURLSessionDataTask, 為什么還有繼承多一層NSURLSessionUploadTask?

答:簡化了操作!從兩者提供的對應task 生成的方法能看。比如使用dataTask來進行上傳任務的時候,需要指定HTTPMethod為POST或PUT,并且提供的數據(NSData)得賦值給request.HTTPBody。而使用uploadTask來進行上傳任務的時候,只需要使用- uploadTaskWithRequest:fromData:或- uploadTaskWithRequest:fromFile:之類的方法,其中參數的話只需要根提供數據(NSData)或者數據的磁盤位置(NSURL*fileURL)就可以構造出一個上傳的session task了,簡化了操作。

我們通過HTTP協議可以完成的操作都屬于這三類任務之一。

NSURLSessionTask主要有三個任務操作方法,分別是:

  • resume(恢復任務)
  • suspend(掛起任務)
  • cancel(取消任務)

NSURLSession一般使用方法:

  • 創建一個URL
  • 創建NSURLRequest請求
  • 創建會話:NSURLSession
  • 通過會話創建務Task
  • 通過Task調用resume方法啟動任務。
#import "NetRequestViewController.h"

@interface NetRequestViewController ()<NSURLSessionDelegate,NSURLSessionDataDelegate,NSURLSessionTaskDelegate,NSURLSessionDownloadDelegate>

@property (nonatomic,strong) NSMutableData *dataM;

@property (nonatomic,strong) NSURLSession *session;

@end

static NSString *urlstr =@"http://api.yanagou.net/app/web.json";

static NSString *urlstr1 = @"http://api.yanagou.net/app/user/login.json";

static NSString *urlstrimage = @"http://120.25.226.186:32812/resources/images/minion_01.png";

static NSString *urlstrimage1 = @"http://120.25.226.186:32812/resources/images/minion_02.png";
@implementation NetRequestViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    //一般的三種請求方式
    //GET In Block
    //[self setDataTask_get];
    
    //POST In Block
    //[self setDataTask_post];
    
    //代理方法 In delegate
    //[self setDataTask_delegate];
    
    
    //下載方法
    //下載1. In Block
    [self setDataTask_1];

    //下載2. In Block
    [self setDownloadTask_2];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    /*
     針對:setDataTask_delegate
     設置代理之后的強引用問題
     NSURLSession 對象在使用的時候,如果設置了代理,那么 session 會對代理對象保持一個強引用,在合適的時候應該主動進行釋放
     可以在控制器調用 viewDidDisappear 方法的時候來進行處理,可以通過調用 invalidateAndCancel 方法或者是 finishTasksAndInvalidate 方法來釋放對代理對象的強引用
     
     invalidateAndCancel 方法直接取消請求然后釋放代理對象
     finishTasksAndInvalidate 方法等請求完成之后釋放代理對象。
     */
    [self.session finishTasksAndInvalidate];

}

/*
        NSURLSessionDataTask 發送 GET 請求:
        發送 GET 請求的步驟非常簡單,只需要兩步就可以完成:
            1.使用 NSURLSession 對象創建 Task
            2.執行 Task
 
*/
- (void)setDataTask_get
{
    //確定請求路徑
    NSURL *url = [NSURL URLWithString:urlstr];
    //創建 NSURLSession 對象
    NSURLSession *session = [NSURLSession sharedSession];
    
    /**
     根據對象創建 Task 請求
     
     url  方法內部會自動將 URL 包裝成一個請求對象(默認是 GET 請求)
     completionHandler  完成之后的回調(成功或失敗)
     
     param data     返回的數據(響應體)
     param response 響應頭
     param error    錯誤信息
     */
    NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:
                                      ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                                          
                                          //解析服務器返回的數據
//                                          NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
                                          NSLog(@"%@", response);

                                          NSDictionary *dic = [self dictionaryWithdata:data];
                                          NSLog(@"%@", dic);


                                          //默認在子線程中解析數據
                                          NSLog(@"%@", [NSThread currentThread]);
                                      }];
    //發送請求(執行Task)
    [dataTask resume];
}

/*
        NSURLSessionDataTask 發送 POST 請求
        發送 POST 請求的步驟與發送 GET 請求一樣:
            1.使用 NSURLSession 對象創建 Task
            2.執行 Task
 */
- (void)setDataTask_post
{
    //確定請求路徑
    NSURL *url = [NSURL URLWithString:urlstr1];
    //創建可變請求對象
    NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];
    //修改請求方法
    requestM.HTTPMethod = @"POST";
    //設置請求體
    requestM.HTTPBody = [@"username=name&password=000000&type=JSON" dataUsingEncoding:NSUTF8StringEncoding];
    //創建會話對象
    NSURLSession *session = [NSURLSession sharedSession];
    //創建請求 Task
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:requestM completionHandler:
                                      ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                                          
                                          //解析返回的數據
                                          NSDictionary *dic = [self dictionaryWithdata:data];
                                          NSLog(@"%@",dic);

                                      }];
    //發送請求
    [dataTask resume];
}

/*
            NSURLSessionDataTask 設置代理發送請求
            創建 NSURLSession 對象設置代理
                1.使用 NSURLSession 對象創建 Task
                2.執行 Task
 */

- (void)setDataTask_delegate
{
    //確定請求路徑
    NSURL *url = [NSURL URLWithString:urlstr1];
    //創建可變請求對象
    NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];
    //設置請求方法
    requestM.HTTPMethod = @"POST";
    //設置請求體
    requestM.HTTPBody = [@"username=name&password=000000&type=JSON" dataUsingEncoding:NSUTF8StringEncoding];

    //創建會話對象,設置代理
    /**
     第一個參數:配置信息
     第二個參數:設置代理
     第三個參數:隊列,如果該參數傳遞nil 那么默認在子線程中執行
     */
    self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
                                                          delegate:self delegateQueue:nil];
    //創建請求 Task
    NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:requestM];
    //發送請求
    [dataTask resume];
    
}
//遵守協議,實現代理方法(常用的有三種代理方法)

- (void)URLSession:(NSURLSession *)session dataTask:(nonnull NSURLSessionDataTask *)dataTask didReceiveResponse:(nonnull NSURLResponse *)response completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler
{
    //子線程中執行
    NSLog(@"接收到服務器響應的時候調用 -- %@", [NSThread currentThread]);
    
    self.dataM = [NSMutableData data];
    //默認情況下不接收數據
    //必須告訴系統是否接收服務器返回的數據
    completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    
    NSLog(@"接受到服務器返回數據的時候調用,可能被調用多次");
    //拼接服務器返回的數據
    [self.dataM appendData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    NSLog(@"請求完成或者是失敗的時候調用");
    //解析服務器返回數據
    NSLog(@"%@", [self dictionaryWithdata:self.dataM]);
    
}

/*
    以下兩個方法無法監聽下載進度,如要獲取下載進度,可以使用代理的方式進行下載。
 
    dataTask 和 downloadTask 下載對比:
 
    * NSURLSessionDataTask:
    1.下載文件可以實現離線斷點下載,但是代碼相對復雜.
 
    * NSURLSessionDownloadTask:
    1.下載文件可以實現斷點下載,但不能離線斷點下載
    2.內部已經完成了邊接收數據邊寫入沙盒的操作
    3.解決了下載大文件時的內存飆升問題

*/

/*
    NSURLSessionDataTask 簡單下載:
    在前面請求數據的時候就相當于一個簡單的下載過程,NSURLSessionDataTask 下載文件具體的步驟與上類似:
        1.使用 NSURLSession 對象創建一個 Task 請求
        2.執行請求
 */

- (void)setDataTask_1
{
    [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:
                                                    urlstrimage]
                                 completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                                     
                                     //解析數據
                                     UIImage *image = [UIImage imageWithData:data];
                                     NSLog(@"%@",image);
                                     //回到主線程設置圖片
                                     dispatch_async(dispatch_get_main_queue(), ^{
//                                         self.imageView.image = image;
                                     });
                                     
                                 }] resume];
}




/*
        NSURLSessionDownloadTask 簡單下載:
        使用 NSURLSession 對象創建下載請求:
            1.在下載請求中移動文件到指定位置
            2.執行請求
*/

- (void)setDownloadTask_2
{
    //確定請求路徑
    NSURL *url = [NSURL URLWithString:urlstrimage1];
    //創建請求對象
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    //創建會話對象
    NSURLSession *session = [NSURLSession sharedSession];
    //創建會話請求
    //優點:該方法內部已經完成了邊接收數據邊寫沙盒的操作,解決了內存飆升的問題
    NSURLSessionDownloadTask *downTask = [session downloadTaskWithRequest:request
                                                        completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                                                            
                                                            //默認存儲到臨時文件夾 tmp 中,需要剪切文件到 cache
                                                            NSLog(@"%@", location);//目標位置
                                                            NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]
                                                                                  stringByAppendingPathComponent:response.suggestedFilename];
                                                            
                                                            /**
                                                             fileURLWithPath:有協議頭
                                                             URLWithString:無協議頭
                                                             */
                                                            [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
                                                            
                                                        }];
    //發送請求
    [downTask resume];
}

/*
 上傳文件操作:
 使用 NSURLSession 進行上傳文件操作,有些麻煩,如果嫌麻煩,也可以使用AFN框架就好。
 
 使用 NSURLSession 上傳文件主要步驟及注意點
 主要步驟:
 1.確定上傳請求的路徑( NSURL )
 2.創建可變的請求對象( NSMutableURLRequest )
 3.修改請求方法為 POST
 4.設置請求頭信息(告知服務器端這是一個文件上傳請求)
 5.按照固定的格式拼接要上傳的文件等參數
 6.根據請求對象創建會話對象( NSURLSession 對象)
 7.根據 session 對象來創建一個 uploadTask 上傳請求任務
 8.執行該上傳請求任務(調用 resume 方法)
 9.得到服務器返回的數據,解析數據(上傳成功 | 上傳失敗)
 
 注意點:
 1.創建可變的請求對象,因為需要修改請求方法為 POST,設置請求頭信息
 2.設置請求頭這個步驟可能會被遺漏
 3.要處理上傳參數的時候,一定要按照固定的格式來進行拼接
 4.需要采用合適的方法來獲得上傳文件的二進制數據類型( MIMEType,獲取方式如下)
 對著該文件發送一個網絡請求,接收到該請求響應的時候,可以通過響應頭信息中的 MIMEType 屬性得到
 使用通用的二進制數據類型表示任意的二進制數據 application/octet-stream
 調用 C 語言的 API 來獲取
 [self mimeTypeForFileAtPath:@"此處為上傳文件的路徑"]
 */

/*!
 * @brief 把格式化的JSON格式的字符串轉換成字典
 * @return 返回字典
 */
- (NSDictionary *)dictionaryWithdata:(NSData *)data {
    
    NSError *err;
    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data
                                                        options:NSJSONReadingMutableContainers
                                                          error:&err];
    
    if(err) {
        NSLog(@"json解析失敗:%@",err);
        return nil;
    }
    return dic;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}
@end

3.AFNetworking
AFNetworking是基于URL加載系統的網絡框架,AFNetworking 1.0建立在NSURLConnection的基礎API之上 ,AFNetworking 2.0開始使用NSURLConnection的基礎API ,以及較新基于NSURLSession的API的選項。 AFNetworking 3.0現已完全基于NSURLSession的API,這降低了維護的負擔,同時支持蘋果增強關于NSURLSession提供的任何額外功能。
我們重點探討AFURLSessionManager和AFHTTPSessionManager兩個類,因為它們都是基于NSURLSession的,前者的用法可以在官方文檔上找到,而且用起來稍顯麻煩,AFHTTPSessionManager的用法如下所示。

下面的代碼演示如何向服務器發送獲取數據的GET請求

    // 創建HTTP會話管理器對象
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    // AFNetworking默認接受的MIME類型是application/json
    // 有些服務器雖然返回JSON格式的數據但MIME類型設置的是text/html
    // 通過下面的代碼可以指定支持的MIME類型有哪些
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:
        @"application/json", @"text/html", nil];
    // 向服務器發送GET請求獲取JSON數據
    [manager
        // 統一資源定位符
        GET:@""
        // 請求參數
        parameters:@{  }
        // 當完成進度變化時回調的Block
        progress:nil
        // 服務器響應成功要回調的Block
        success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        }
        // 服務器響應失敗要回調的Block
        failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        }
    ];

下面的代碼演示了如何向服務器發送上傳數據的POST請求

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    [manager
        // 統一資源定位符
        POST:@""
        // 請求參數
        parameters:@{ }
        // 構造請求報文消息體的Block
        constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
            // 可以調用appendPartWithFileData:name:fileName:mimeType:等方法
            // 將上傳給服務器的數據放到請求報文的消息體中
        }
        // 當上傳進度變化時回調的Block
        progress:^(NSProgress * _Nonnull uploadProgress) {

        }
        // 服務器響應成功要回調的Block
        success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {    
        } 
        // 服務器響應失敗要回調的Block
        failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        }
    ];

AFNetworking還封裝了判斷網絡可達性的功能,使用該功能的代碼如下所示:

    // 創建網絡可達性管理器
    AFNetworkReachabilityManager *manager = [AFNetworkReachabilityManager manager];
    // 設置當網絡狀況發生變化時要回調的Block
    [manager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
        switch (status) {
            case AFNetworkReachabilityStatusNotReachable:
                NSLog(@"沒有網絡連接");
                break;
            case AFNetworkReachabilityStatusReachableViaWiFi:
                NSLog(@"使用Wi-Fi");
                break;
            case AFNetworkReachabilityStatusReachableViaWWAN:
                NSLog(@"使用移動蜂窩網絡");
                break;
            default:
                break;
        }
    }];
    // 開始監控網絡狀況變換
    [manager startMonitoring];

補充
1. 斷點續傳的實現原理
HTTP協議設置請求頭內容,支持只請求某個資源的某一部分。
Range 請求的資源范圍;
Content-Range 響應的資源范圍;
在連接斷開重連時,客戶端只請求該資源未下載的部分,而不是重新請求整個資源,來實現斷點續傳。
分塊請求資源實例:
Eg1:Range: bytes=306302- :請求這個資源從306302個字節到末尾的部分;
Eg2:Content-Range: bytes 306302-604047/604048:響應中指示攜帶的是該資源的第306302-604047的字節,該資源共604048個字節;
客戶端通過并發的請求相同資源的不同片段,來實現對某個資源的并發分塊下載。從而達到快速下載的目的。目前流行的FlashGet和迅雷基本都是這個原理。
2. 多線程下載的原理
下載工具開啟多個發出HTTP請求的線程,每個http請求只請求資源文件的一部分,例如:Content-Range: bytes 20000-40000/47000;
最后合并每個線程下載的文件。

5. 數據解析

通過HTTP從服務器獲得的數據通常都是JSON格式或XML格式的,下面對這兩種數據格式做一個簡單的介紹。
1.JSON
JSON全稱JavaScript對象表達式(JavaScript Object Notation),是目前最流行的存儲和交換文本信息的語法,和XML相比,它更小、更快,更易解析,是一種輕量級的文本數據交換格式。

JSON的語法規則可以簡單的總結成以下幾條:

  • 數據在名/值對中;
  1. 數據由逗號分隔;
  2. 花括號保存對象;
  3. 方括號保存數組。

JSON中的值可以是:

- 數字(整數或浮點數)
- 字符串(在雙引號中)
- 邏輯值(true 或 false)
- 數組(在方括號中)
- 對象(在花括號中)
- null

不難看出,JSON用鍵值對的方式描述了對象,它的形態跟Objective-C的NSDictionary類型是完全一致的,可以通過NSJSONSerialization類的兩個類方法實現JSON數據和字典或數組之間的相互轉換。

// 將數據轉換成對象(通常是數組或字典)
+ (id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;
// 將數組或字典裝換成JSON數據
+ (NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:(NSError **)error

通過服務器獲得JSON數據后,最終需要將它轉換成我們程序中的對象。事實上,將JSON轉換成模型對象的操作在開發網絡應用中是很常見的,我們可以使用KVC(Key-Value Coding)的方式將一個字典賦值給一個對象的屬性。

說明:KVC通常翻譯為鍵值編碼,它允許開發者通過名字訪問對象屬性,而無需調用明確的存取方法,這樣就可以實現在運行時而不是在編譯時確定屬性的綁定。這種間接訪問能讓代碼變得更靈活和更具復用性。

對于對象中關聯了其他對象或者對象的屬性跟字典中的鍵不完全匹配的場景,KVC就顯得不那么方便了,但是已經有很多優秀的第三方庫幫助我們實現了JSON和模型對象的雙向轉換,比如JSONModel、MJExtension和YYModel。

說明:YYModel> MJExtension> JSONModel(侵入性:模型類必須繼承JSONModel)

XML
XML全稱可擴展標記語言,被設計用來傳輸和存儲數據。在JSON被廣泛使用之前,XML是異構系統之間交換數據的事實標準,它是一種具有自我描述能力的傳輸數據的標記語言,如下所示。

<?xml version="1.0" encoding="ISO-8859-1"?>
<note>
    <to>Tove</to>
    <from>Jani</from>
    <heading>Reminder</heading>
    <body>Don't forget me this weekend!</body>
</note>

因為不常用XML解析,所以其他的就不一一介紹了。

6. 網絡優化
  1. 數據格式優化,減少數據傳輸量和序列化時間
  2. 引入重試機制,提升網絡服務成功率
  3. 弱網和網絡抖動優化
    引入了網絡質量參數,通過網絡類型(網絡類型切換時,例如WIFI和移動網絡、4G/3G切換至2G時,)和端到端Ping值進行計算,根據不同的網絡質量改變網絡服務策略

以上是iOS開發中跟網絡相關的知識,但是還遠不止這些,例如:如何通過證書保證網絡通信的安全,如何有效的使用緩存來提升性能和減少網絡開銷以及URL緩存的過期模型和驗證模型等。都需要我們深入學習研究!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容

  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,060評論 6 13
  • 個人認為,Goodboy1881先生的TCP /IP 協議詳解學習博客系列博客是一部非常精彩的學習筆記,這雖然只是...
    貳零壹柒_fc10閱讀 5,083評論 0 8
  • 1.這篇文章不是本人原創的,只是個人為了對這部分知識做一個整理和系統的輸出而編輯成的,在此鄭重地向本文所引用文章的...
    SOMCENT閱讀 13,110評論 6 174
  • 桃花沒等到春雨, 落紅也不是無情。 荷葉沒等到蟬鳴, 雷雨也不是動情。 殘菊沒等到暖陽, 百花也不是癡情。 冬雪沒...
    大風起兮云飛揚_閱讀 152評論 1 2
  • What is it?單例也就是單實例,也就是JVM中只存在一個某類對象,且對外提供一個獲取該對象的方法。 Whe...
    愛做夢的胖子閱讀 222評論 0 0