Alamofire源碼解讀系列(十三)之請求(Request)

本篇是Alamofire中的請求抽象層的講解

前言

在Alamofire中,圍繞著Request鸯旁,設(shè)計了很多額外的特性责鳍,這也恰恰表明,Request是所有請求的基礎(chǔ)部分和發(fā)起點榆俺。這無疑給我們一個Request很復(fù)雜的想法。但看了Alamofire中Request.swift中的代碼,Request被設(shè)計的又是如此的簡單茴晋,這就是為什么這些頂級框架如此讓人喜愛的原因陪捷。

在后續(xù)的文章中,我會單獨寫一篇Swift中協(xié)議的使用技巧诺擅,在Alamofire源碼解讀系列(一)之概述和使用這篇的Alamofire高級用法中市袖,我根據(jù)Alamofire官方文檔做了一些補充,其中涉及到了URLConvertible和URLRequestConvertible的高級用法烁涌,在本篇中同樣出現(xiàn)了3個協(xié)議:

  • RequestAdapter 請求適配器苍碟,目的是自定義修改請求,一個典型的例子是為每一個請求調(diào)價Token請求頭
  • RequestRetrier 請求重試器撮执, 目的是控制請求的重試機制微峰,一個典型的例子是當(dāng)某個特殊的請求失敗后,是否重試抒钱。
  • TaskConvertible task轉(zhuǎn)換器蜓肆,目的是把task裝換成特定的類型,在Alamofire中有4中task:Data/Download/Upload/Stream

有一點需要特別說明的是继效,在使用Alamofire的高級用法時症杏,需要操作SessionManager這個類。

請求過程

明白Alamofire中一個請求的過程瑞信,是非常有必要的。先看下邊的代碼:

Alamofire.request("https://httpbin.org/get")

上邊的代碼是最簡單的一個請求穴豫,我們看看Alamofire.request中究竟干了什么凡简?

@discardableResult
public func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest
{
    return SessionManager.default.request(
        url,
        method: method,
        parameters: parameters,
        encoding: encoding,
        headers: headers
    )
}

該函數(shù)內(nèi)部調(diào)用了SessionManager的request方法,這說明請求的第一個發(fā)起點來自SessionManager精肃,Alamofire.swift該文件是最上層的封裝秤涩,緊鄰其下的就是SessionManager.swift。接下來我們再看看SessionManager.default.request做了什么司抱?

@discardableResult
    open func request(
        _ url: URLConvertible,
        method: HTTPMethod = .get,
        parameters: Parameters? = nil,
        encoding: ParameterEncoding = URLEncoding.default,
        headers: HTTPHeaders? = nil)
        -> DataRequest
    {
        var originalRequest: URLRequest?
        /// 在這里計算出可能出現(xiàn)的額錯誤的類型
        /// 1.url 如果不能被轉(zhuǎn)成URL被拋出一個error
        /// 2.originalRequest不能轉(zhuǎn)換為URLRequest會拋出error
        do {
            originalRequest = try URLRequest(url: url, method: method, headers: headers)
            let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters)
            return request(encodedURLRequest)
        } catch {
            return request(originalRequest, failedWith: error)
        }
    }

上邊的函數(shù)內(nèi)部創(chuàng)建了一個Request對象筐眷,然后把參數(shù)編碼進這個Request中,之后又調(diào)用了內(nèi)部的一個request函數(shù)习柠,函數(shù)的參數(shù)就是上邊的Request對象匀谣。我們就緒看看這個request函數(shù)做了什么?

    open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
        var originalRequest: URLRequest?

        do {
            originalRequest = try urlRequest.asURLRequest()
            /// 這里需要注意的是Requestable并不是DataRequest的一個屬性资溃,前邊是沒有加let/var的武翎,所以可以通過DataRequest.Requestable來操作
            let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)

            let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
            let request = DataRequest(session: session, requestTask: .data(originalTask, task))

            delegate[task] = request

            if startRequestsImmediately { request.resume() }

            return request
        } catch {
            return request(originalRequest, failedWith: error)
        }
    }

注意,上邊的函數(shù)是一個open函數(shù)溶锭,因此可以使用SessionManager.request來發(fā)起請求宝恶,不過參數(shù)是_ urlRequest: URLRequestConvertible

URLRequestConvertible協(xié)議的目的是對URLRequest進行自定義的轉(zhuǎn)換,因此垫毙,在獲得轉(zhuǎn)換后的URLRequest后霹疫,需要用URLRequest生成task,這樣才能發(fā)起網(wǎng)絡(luò)請求综芥,在Alamofire中丽蝎,但凡是request開頭的函數(shù),默認(rèn)的都是DataRequest類型毫痕,現(xiàn)在有了URLRequest還不夠征峦,還需要檢測她能否生成與之相對應(yīng)的task。

在上邊的函數(shù)中消请,用到了DataRequest.Requestable栏笆,Requestable其實一個結(jié)構(gòu)體,他實現(xiàn)了TaskConvertible協(xié)議臊泰,因此蛉加,它能夠用URLRequest生成與之相對應(yīng)的task。接下來就初始化DataRequest,然后真正的開始發(fā)起請求缸逃。

我們總結(jié)一下這個過程:

明白了上邊的過程针饥,再回過頭來看Request.swift也就是本篇的內(nèi)容就簡單多了,就下邊幾個目的:

  • 創(chuàng)建DataRequest/DownloadRequest/UploadRequest/StreamRequest
  • 發(fā)起請求

Request

有很多二次封裝的網(wǎng)絡(luò)框架中需频,一般都有這么一個Request類丁眼,用于發(fā)送網(wǎng)絡(luò)請求,接受response昭殉,關(guān)聯(lián)服務(wù)器返回的數(shù)據(jù)并且管理task苞七。Alamofire中的Request同樣主要實現(xiàn)上邊的任務(wù)。

Request作為DataRequest挪丢、DownloadRequest蹂风、UploadRequest、StreamRequest的基類乾蓬,我們一起來看看它有哪些屬性:

/// The delegate for the underlying task.
    /// 由于某個屬性是通過另一個屬性來setter和getter的惠啄,因此建議加一個鎖
    open internal(set) var delegate: TaskDelegate {
        get {
            taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
            return taskDelegate
        }
        set {
            taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
            taskDelegate = newValue
        }
    }

    /// The underlying task.
    open var task: URLSessionTask? { return delegate.task }

    /// The session belonging to the underlying task.
    open let session: URLSession

    /// The request sent or to be sent to the server.
    open var request: URLRequest? { return task?.originalRequest }

    /// The response received from the server, if any.
    open var response: HTTPURLResponse? { return task?.response as? HTTPURLResponse }

    /// The number of times the request has been retried.
    open internal(set) var retryCount: UInt = 0

    let originalTask: TaskConvertible?

    var startTime: CFAbsoluteTime?
    var endTime: CFAbsoluteTime?

    var validations: [() -> Void] = []

    private var taskDelegate: TaskDelegate
    private var taskDelegateLock = NSLock()

這些屬性沒什么好說的,我們就略過這些內(nèi)容任内,Request的初始化方法撵渡,有點意思,我們先看看代碼:

init(session: URLSession, requestTask: RequestTask, error: Error? = nil) {
        self.session = session

        switch requestTask {
        case .data(let originalTask, let task):
            taskDelegate = DataTaskDelegate(task: task)
            self.originalTask = originalTask
        case .download(let originalTask, let task):
            taskDelegate = DownloadTaskDelegate(task: task)
            self.originalTask = originalTask
        case .upload(let originalTask, let task):
            taskDelegate = UploadTaskDelegate(task: task)
            self.originalTask = originalTask
        case .stream(let originalTask, let task):
            taskDelegate = TaskDelegate(task: task)
            self.originalTask = originalTask
        }

        delegate.error = error
        delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() }
    }

要想發(fā)起一個請求族奢,有一個task就足夠了姥闭,在上邊的方法中傳遞過來的session主要用于CustomStringConvertibleCustomDebugStringConvertible這兩個協(xié)議的實現(xiàn)方法中獲取特定的數(shù)據(jù)。

這里有一點小提示越走,在創(chuàng)建自定義的類的時候棚品,實現(xiàn)上邊這兩個協(xié)議靠欢,通過打印,能夠進行快速的調(diào)試铜跑。

上邊方法中第二個參數(shù)是requestTask门怪,它是一個枚舉類型,我們看一下:

enum RequestTask {
        case data(TaskConvertible?, URLSessionTask?)
        case download(TaskConvertible?, URLSessionTask?)
        case upload(TaskConvertible?, URLSessionTask?)
        case stream(TaskConvertible?, URLSessionTask?)
    }

在swift中枚舉不僅僅用來區(qū)分不同的選項锅纺,更強大的是為每個選項綁定的數(shù)據(jù)掷空。大家仔細(xì)想一下,在初始化Request的時候囤锉,只需要傳遞requestTask這個枚舉值坦弟,我們就得到了兩個重要的數(shù)據(jù):Request的類型和相對應(yīng)的task。這一變成手法的運用官地,大大提高了代碼的質(zhì)量酿傍。

RequestTask枚舉中和選項綁定的數(shù)據(jù)有兩個,TaskConvertible表示原始的對象驱入,該對象實現(xiàn)了TaskConvertible協(xié)議赤炒,能夠轉(zhuǎn)換成task。URLSessionTask是原始對象轉(zhuǎn)換后的task亏较。因此衍生出一種高級使用方法的可能性莺褒,可以自定義一個類,實現(xiàn)TaskConvertible協(xié)議雪情,就能夠操縱task的轉(zhuǎn)換過程遵岩,很靈活。

delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() }

上邊的這一行代碼巡通。給代理的queue添加了一個操作陨舱,隊列是先進先出原則显押,但是可以通過isSuspended暫停隊列內(nèi)部的操作,下邊是一個例子演示:

let queue = { () -> OperationQueue in 
    let operationQueue = OperationQueue()
    
    operationQueue.maxConcurrentOperationCount = 1
    operationQueue.isSuspended = true
    operationQueue.qualityOfService = .utility
    
    return operationQueue
}()

queue.addOperation {
    print("1")
}

queue.addOperation {
    print("2")
}

queue.addOperation {
    print("3")
}

queue.isSuspended = false

打印結(jié)果:

1
2
3

隊列提供了強大的功能惯豆,了解隊列的知識點非常有必要蠢熄,有很大的一種可能性跪解,也許某個問題卡住了,用隊列能夠很輕松的解決签孔。有興趣可以看看我模仿SDWebImage寫的下載器MCDownloader(iOS下載器)說明書叉讥。

處理網(wǎng)絡(luò)請求,就必須要面對安全的問題饥追,為了解決數(shù)據(jù)傳輸安全問題图仓,到目前為止,已經(jīng)出現(xiàn)了很多種解決方式但绕。想了解這方面的知識救崔,可以去看<<HTTP權(quán)威指南>>惶看。

Alamofire源碼解讀系列(一)之概述和使用中的Alamofire高級使用技巧部分。

 /// Associates an HTTP Basic credential with the request.
    ///
    /// - parameter user:        The user.
    /// - parameter password:    The password.
    /// - parameter persistence: The URL credential persistence. `.ForSession` by default.
    ///
    /// - returns: The request.
    /// 這里需要注意一點六孵,persistence表示持久性纬黎,可以點擊去查看詳細(xì)說明
    @discardableResult
    open func authenticate(
        user: String,
        password: String,
        persistence: URLCredential.Persistence = .forSession)
        -> Self
    {
        let credential = URLCredential(user: user, password: password, persistence: persistence)
        return authenticate(usingCredential: credential)
    }

    /// Associates a specified credential with the request.
    ///
    /// - parameter credential: The credential.
    ///
    /// - returns: The request.
    @discardableResult
    open func authenticate(usingCredential credential: URLCredential) -> Self {
        delegate.credential = credential
        return self
    }

上邊的這兩個函數(shù)能夠處理請求中的驗證問題,可以用來應(yīng)對用戶密碼和證書驗證劫窒。

  /// Returns a base64 encoded basic authentication credential as an authorization header tuple.
    ///
    /// - parameter user:     The user.
    /// - parameter password: The password.
    ///
    /// - returns: A tuple with Authorization header and credential value if encoding succeeds, `nil` otherwise.
    open static func authorizationHeader(user: String, password: String) -> (key: String, value: String)? {
        guard let data = "\(user):\(password)".data(using: .utf8) else { return nil }

        let credential = data.base64EncodedString(options: [])

        return (key: "Authorization", value: "Basic \(credential)")
    }

這個方法是一個輔助函數(shù)本今,某些服務(wù)器可能需要把用戶名和密碼拼接到請求頭中,那么可以使用這個函數(shù)來實現(xiàn)主巍。

我們對一個請求的操作有下邊3中可能:

  • resume 喚醒該請求冠息,這個非常簡單,函數(shù)中做了3件事:記錄開始時間孕索,喚醒task逛艰,發(fā)通知。

      /// Resumes the request.
          open func resume() {
              guard let task = task else { delegate.queue.isSuspended = false ; return }
      
              if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() }
      
              task.resume()
      
              NotificationCenter.default.post(
                  name: Notification.Name.Task.DidResume,
                  object: self,
                  userInfo: [Notification.Key.Task: task]
              )
          }
    
  • suspend 暫停

        /// Suspends the request.
          open func suspend() {
              guard let task = task else { return }
      
              task.suspend()
      
              NotificationCenter.default.post(
                  name: Notification.Name.Task.DidSuspend,
                  object: self,
                  userInfo: [Notification.Key.Task: task]
              )
          }
    
  • cancel 取消

      /// Cancels the request.
          open func cancel() {
              guard let task = task else { return }
      
              task.cancel()
      
              NotificationCenter.default.post(
                  name: Notification.Name.Task.DidCancel,
                  object: self,
                  userInfo: [Notification.Key.Task: task]
              )
          }
    

Request中對CustomDebugStringConvertible和CustomStringConvertible的實現(xiàn)檬果,我們就不做太多介紹了瓮孙,有兩點需要注意:

  1. 類似像urlCredentialStorage, httpCookieStorage這種帶有Storage字段的對象,需要仔細(xì)研究一下這種代碼設(shè)計的規(guī)律选脊。

  2. 下邊這一小段代碼正好提現(xiàn)了swift的優(yōu)雅之處杭抠,需要記住:

     for (field, value) in headerFields where field != "Cookie" {
                     headers[field] = value
                 }
    

TaskConvertible

TaskConvertible協(xié)議給了給了我們轉(zhuǎn)換task的能力恳啥,任何實現(xiàn)了該協(xié)議的對象偏灿,都表示能夠轉(zhuǎn)換成一個task。我們都知道DataRequest钝的,DownloadRequest翁垂,UploadRequest,StreamRequest都繼承自Request硝桩,最終應(yīng)該是通過TaskConvertible協(xié)議來把一個URLRequest轉(zhuǎn)換成對應(yīng)的task沿猜。

而Alamofire的Request的設(shè)計中,采用struct或者enum來實現(xiàn)這個協(xié)議,我們來看看這些實現(xiàn)碗脊;

DataRequest:

  struct Requestable: TaskConvertible {
        let urlRequest: URLRequest

        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
            do {
                let urlRequest = try self.urlRequest.adapt(using: adapter)
                return queue.sync { session.dataTask(with: urlRequest) }
            } catch {
                throw AdaptError(error: error)
            }
        }
    }

DownloadRequest:

  enum Downloadable: TaskConvertible {
        case request(URLRequest)
        case resumeData(Data)

        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
            do {
                let task: URLSessionTask

                switch self {
                case let .request(urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.downloadTask(with: urlRequest) }
                case let .resumeData(resumeData):
                    task = queue.sync { session.downloadTask(withResumeData: resumeData) }
                }

                return task
            } catch {
                throw AdaptError(error: error)
            }
        }
    }

如果task的類型是下載啼肩,會出現(xiàn)兩種情況,一種是直接通過URLRequest生成downloadTask衙伶,另一種是根據(jù)已有的數(shù)據(jù)恢復(fù)成downloadTask祈坠。我們之前已經(jīng)講過了,下載失敗后會有resumeData矢劲。里邊保存了下載信息赦拘,這里就不提了》页粒總之躺同,上邊這個enum給我們提供了兩種不同的方式來生成downloadTask阁猜。

這種代碼的設(shè)計值得學(xué)習(xí)。

UploadRequest:

 enum Uploadable: TaskConvertible {
        case data(Data, URLRequest)
        case file(URL, URLRequest)
        case stream(InputStream, URLRequest)

        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
            do {
                let task: URLSessionTask

                switch self {
                case let .data(data, urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.uploadTask(with: urlRequest, from: data) }
                case let .file(url, urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.uploadTask(with: urlRequest, fromFile: url) }
                case let .stream(_, urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.uploadTask(withStreamedRequest: urlRequest) }
                }

                return task
            } catch {
                throw AdaptError(error: error)
            }
        }
    }

雖然內(nèi)容與上邊的DownloadRequest不同笋籽,但是套路卻相同蹦漠。從代碼中,也能看出车海,上傳數(shù)據(jù)有3種介質(zhì)笛园,分別是:data,file,stream。

StreamRequest:

enum Streamable: TaskConvertible {
        case stream(hostName: String, port: Int)
        case netService(NetService)

        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
            let task: URLSessionTask

            switch self {
            case let .stream(hostName, port):
                task = queue.sync { session.streamTask(withHostName: hostName, port: port) }
            case let .netService(netService):
                task = queue.sync { session.streamTask(with: netService) }
            }

            return task
        }
    }

netService超出了本文的范圍侍芝,就不做介紹了研铆,平時用的也少。

我們對上邊這些struct州叠,enum做一個總結(jié):由于struct棵红,enum是值拷貝,因此他們比較適合作為數(shù)據(jù)的載體咧栗。一個方案的邏輯中逆甜,如果可能出現(xiàn)多個可能性,就考慮使用enum致板。還有最重要的一點交煞,盡量把一個單一的功能的作用域限制的越小越好。功能越單一斟或,結(jié)構(gòu)越簡單的函數(shù)越安全素征。

忽略的內(nèi)容

在Request.swift的源碼中,還有一個給任務(wù)添加進度的方法萝挤,在這里就不做介紹了御毅,原理就是自定義一個函數(shù),傳遞給task的代理怜珍。在DownloadRequest中對取消下載任務(wù)做了一些額外的處理端蛆。還有設(shè)置下載后的目錄等等。

DownloadOptions

這個DownloadOptions其實挺有意思的酥泛,他實現(xiàn)了OptionSet協(xié)議欺税,因此它就有了集合的一些特性。

在OC中揭璃,我們往往通過掩碼來實現(xiàn)多個選項共存這一功能,但DownloadOptions用另一種方式實現(xiàn)了這一功能:

/// A collection of options to be executed prior to moving a downloaded file from the temporary URL to the
    /// destination URL.
    public struct DownloadOptions: OptionSet {
        /// Returns the raw bitmask value of the option and satisfies the `RawRepresentable` protocol.
        public let rawValue: UInt

        /// A `DownloadOptions` flag that creates intermediate directories for the destination URL if specified.
        public static let createIntermediateDirectories = DownloadOptions(rawValue: 1 << 0)

        /// A `DownloadOptions` flag that removes a previous file from the destination URL if specified.
        public static let removePreviousFile = DownloadOptions(rawValue: 1 << 1)

        /// Creates a `DownloadFileDestinationOptions` instance with the specified raw value.
        ///
        /// - parameter rawValue: The raw bitmask value for the option.
        ///
        /// - returns: A new log level instance.
        public init(rawValue: UInt) {
            self.rawValue = rawValue
        }
    }

上邊的代碼只擴展了兩個默認(rèn)選項:

  • createIntermediateDirectories
  • removePreviousFile

可以采用類似的手法亭罪,自己擴展更多的選項瘦馍。看一下下邊的例子就明白了:

var op = DownloadRequest.DownloadOptions(rawValue: 1)
        op.insert(DownloadRequest.DownloadOptions(rawValue: 2))
        if op.contains(.createIntermediateDirectories) {
            print("createIntermediateDirectories")
        }
        if op.contains(.removePreviousFile) {
            print("removePreviousFile")
        }

上邊代碼中应役,if語句內(nèi)的打印都會調(diào)用情组。

總結(jié)

這一篇文章與上一篇間隔了很長時間燥筷,原因是公司做了一個項目。這個中小型項目結(jié)束后院崇,也有一些需要總結(jié)的地方肆氓,我會把這些感觸寫下來,和大家討論一些項目開發(fā)的內(nèi)容底瓣。

讀的越多谢揪,越發(fā)覺得Alamofire中的函數(shù)的設(shè)計很厲害。不是一時半會能夠全部串聯(lián)的捐凭。

由于知識水平有限拨扶,如有錯誤,還望指出

鏈接

Alamofire源碼解讀系列(一)之概述和使用 簡書-----博客園

Alamofire源碼解讀系列(二)之錯誤處理(AFError) 簡書-----博客園

Alamofire源碼解讀系列(三)之通知處理(Notification) 簡書-----博客園

Alamofire源碼解讀系列(四)之參數(shù)編碼(ParameterEncoding) 簡書-----博客園

Alamofire源碼解讀系列(五)之結(jié)果封裝(Result) 簡書-----博客園

Alamofire源碼解讀系列(六)之Task代理(TaskDelegate) 簡書-----博客園

Alamofire源碼解讀系列(七)之網(wǎng)絡(luò)監(jiān)控(NetworkReachabilityManager) 簡書-----博客園

Alamofire源碼解讀系列(八)之安全策略(ServerTrustPolicy) 簡書-----博客園

Alamofire源碼解讀系列(九)之響應(yīng)封裝(Response) 簡書-----博客園

Alamofire源碼解讀系列(十)之序列化(ResponseSerialization) 簡書-----博客園

Alamofire源碼解讀系列(十一)之多表單(MultipartFormData) 簡書-----博客園

Alamofire源碼解讀系列(十二)之時間軸(Timeline) 簡書-----博客園

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末茁肠,一起剝皮案震驚了整個濱河市患民,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌垦梆,老刑警劉巖匹颤,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異托猩,居然都是意外死亡印蓖,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門站刑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來另伍,“玉大人,你說我怎么就攤上這事绞旅“诔ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵因悲,是天一觀的道長堕汞。 經(jīng)常有香客問我,道長晃琳,這世上最難降的妖魔是什么讯检? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮卫旱,結(jié)果婚禮上人灼,老公的妹妹穿的比我還像新娘。我一直安慰自己顾翼,他們只是感情好投放,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著适贸,像睡著了一般灸芳。 火紅的嫁衣襯著肌膚如雪涝桅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天烙样,我揣著相機與錄音冯遂,去河邊找鬼。 笑死谒获,一個胖子當(dāng)著我的面吹牛蛤肌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播究反,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼寻定,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了精耐?” 一聲冷哼從身側(cè)響起狼速,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎卦停,沒想到半個月后向胡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡惊完,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年僵芹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片小槐。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡拇派,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出凿跳,到底是詐尸還是另有隱情件豌,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布控嗜,位于F島的核電站茧彤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏疆栏。R本人自食惡果不足惜曾掂,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望壁顶。 院中可真熱鬧珠洗,春花似錦、人聲如沸若专。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蛔糯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窖式,已是汗流浹背蚁飒。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留萝喘,地道東北人淮逻。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像阁簸,于是被迫代替她去往敵國和親爬早。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容