OkHttp 之 网络请求耗时统计

https://blog.csdn.net/joye123/article/details/82115562?utm_source=blogxgwz9


OkHttp 之 网络请求耗时统计

OkHttp 3.11.0版本提供了EventListener接口,可以让调用者接收一系列网络请求过程中的事件,例如DNS解析、TSL/SSL连接、Response接收等。通过继承此接口,调用者可以监视整个应用中网络请求次数、流量大小、耗时情况。

使用方法如下:

public class HttpEventListener extends EventListener {

    /**

    * 自定义EventListener工厂

    */

    public static final Factory FACTORY = new Factory() {

        final AtomicLong nextCallId = new AtomicLong(1L);

        @Override

        public EventListener create(Call call) {

            long callId = nextCallId.getAndIncrement();

            return new HttpEventListener(callId, call.request().url(), System.nanoTime());

        }

    };

    /**

    * 每次请求的标识

    */

    private final long callId;

    /**

    * 每次请求的开始时间,单位纳秒

    */

    private final long callStartNanos;

    private StringBuilder sbLog;

    public HttpEventListener(long callId, HttpUrl url, long callStartNanos) {

        this.callId = callId;

        this.callStartNanos = callStartNanos;

        this.sbLog = new StringBuilder(url.toString()).append(" ").append(callId).append(":");

    }

    private void recordEventLog(String name) {

        long elapseNanos = System.nanoTime() - callStartNanos;

        sbLog.append(String.format(Locale.CHINA, "%.3f-%s", elapseNanos / 1000000000d, name)).append(";");

        if (name.equalsIgnoreCase("callEnd") || name.equalsIgnoreCase("callFailed")) {

            //打印出每个步骤的时间点

            NearLogger.i(sbLog.toString());

        }

    }

    @Override

    public void callStart(Call call) {

        super.callStart(call);

        recordEventLog("callStart");

    }

    @Override

    public void dnsStart(Call call, String domainName) {

        super.dnsStart(call, domainName);

        recordEventLog("dnsStart");

    }

    @Override

    public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {

        super.dnsEnd(call, domainName, inetAddressList);

        recordEventLog("dnsEnd");

    }

    @Override

    public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {

        super.connectStart(call, inetSocketAddress, proxy);

        recordEventLog("connectStart");

    }

    @Override

    public void secureConnectStart(Call call) {

        super.secureConnectStart(call);

        recordEventLog("secureConnectStart");

    }

    @Override

    public void secureConnectEnd(Call call, @Nullable Handshake handshake) {

        super.secureConnectEnd(call, handshake);

        recordEventLog("secureConnectEnd");

    }

    @Override

    public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol) {

        super.connectEnd(call, inetSocketAddress, proxy, protocol);

        recordEventLog("connectEnd");

    }

    @Override

    public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol, IOException ioe) {

        super.connectFailed(call, inetSocketAddress, proxy, protocol, ioe);

        recordEventLog("connectFailed");

    }

    @Override

    public void connectionAcquired(Call call, Connection connection) {

        super.connectionAcquired(call, connection);

        recordEventLog("connectionAcquired");

    }

    @Override

    public void connectionReleased(Call call, Connection connection) {

        super.connectionReleased(call, connection);

        recordEventLog("connectionReleased");

    }

    @Override

    public void requestHeadersStart(Call call) {

        super.requestHeadersStart(call);

        recordEventLog("requestHeadersStart");

    }

    @Override

    public void requestHeadersEnd(Call call, Request request) {

        super.requestHeadersEnd(call, request);

        recordEventLog("requestHeadersEnd");

    }

    @Override

    public void requestBodyStart(Call call) {

        super.requestBodyStart(call);

        recordEventLog("requestBodyStart");

    }

    @Override

    public void requestBodyEnd(Call call, long byteCount) {

        super.requestBodyEnd(call, byteCount);

        recordEventLog("requestBodyEnd");

    }

    @Override

    public void responseHeadersStart(Call call) {

        super.responseHeadersStart(call);

        recordEventLog("responseHeadersStart");

    }

    @Override

    public void responseHeadersEnd(Call call, Response response) {

        super.responseHeadersEnd(call, response);

        recordEventLog("responseHeadersEnd");

    }

    @Override

    public void responseBodyStart(Call call) {

        super.responseBodyStart(call);

        recordEventLog("responseBodyStart");

    }

    @Override

    public void responseBodyEnd(Call call, long byteCount) {

        super.responseBodyEnd(call, byteCount);

        recordEventLog("responseBodyEnd");

    }

    @Override

    public void callEnd(Call call) {

        super.callEnd(call);

        recordEventLog("callEnd");

    }

    @Override

    public void callFailed(Call call, IOException ioe) {

        super.callFailed(call, ioe);

        recordEventLog("callFailed");

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

自定义EventListener实现和EventListener工厂实现,其中每个网络请求都对应一个EventListener监听。对于相同地址的请求,为了区分其对应的EventListener,需要通过EventListener工厂创建带有唯一标识的EventListener。这里是为每个EventListener分配唯一的自增编号。

在创建OkHttpClient时将EventListener工厂作为构建参数添加进去。

OkHttpClient.Builder builder = new OkHttpClient.Builder()

          .eventListenerFactory(HttpEventListener.FACTORY)

          .build();

1

2

3

下面详细说明每个回调的触发时机:

callStart(Call call) 请求开始

当一个Call(代表一个请求)被同步执行或被添加异步队列中时。

//okhttp3

final class RealCall implements Call {

    @Override

    public Response execute() throws IOException {

        eventListener.callStart(this);

        client.dispatcher().executed(this);

        Response result = getResponseWithInterceptorChain();

        if (result == null) throw new IOException("Canceled");

        return result;   

    }

    @Override

    public void enqueue(Callback responseCallback) {

        eventListener.callStart(this);

        client.dispatcher().enqueue(new AsyncCall(responseCallback));

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

由于线程或事件流的限制,这里的请求开始并不是真正的去执行的这个请求。

如果发生重定向和多域名重试时,这个方法也仅被调用一次。

callFailed/callEnd 请求异常和请求结束

每一个callStart都对应着一个callFailed或callEnd。

callFailed在两种情况下被调用

第一种是在请求执行的过程中发生异常时。

第二种是在请求结束后,关闭输入流时产生异常时。

//okhttp3

final class RealCall implements Call {

    @Override

    public Response execute() throws IOException {

        try {

          client.dispatcher().executed(this);

          Response result = getResponseWithInterceptorChain();

          if (result == null) throw new IOException("Canceled");

          return result;

        } catch (IOException e) {

          eventListener.callFailed(this, e);

          throw e;

        }

    }

    final class AsyncCall extends NamedRunnable {

        @Override

        protected void execute() {

            try {

                Response response = getResponseWithInterceptorChain();

            } catch (IOException e) {

                eventListener.callFailed(RealCall.this, e);

            }

        }

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

//okhttp3.internal.connection

public final class StreamAllocation {

    public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) {

        ...

        if (e != null) {

          eventListener.callFailed(call, e);

        } else if (callEnd) {

          eventListener.callEnd(call);

        }

        ...

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

callEnd也有两种调用场景。

第一种也是在关闭流时。

第二种是在释放连接时。

//okhttp3.internal.connection

public final class StreamAllocation {

    public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) {

        ...

        if (e != null) {

          eventListener.callFailed(call, e);

        } else if (callEnd) {

          eventListener.callEnd(call);

        }

        ...

    }

    public void release() {

        ...

        if (releasedConnection != null) {

          eventListener.connectionReleased(call, releasedConnection);

          eventListener.callEnd(call);

        }

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

为什么会将关闭流和关闭连接区分开?

在http2版本中,一个连接上允许打开多个流,OkHttp使用StreamAllocation来作为流和连接的桥梁。当一个流被关闭时,要检查这条连接上还有没有其他流,如果没有其他流了,则可以将连接关闭了。

streamFinished和release作用是一样的,都是关闭当前流,并检查是否需要关闭连接。

不同的是,当调用者手动取消请求时,调用的是release方法,并由调用者负责关闭请求输出流和响应输入流。

dnsStart/dnsEnd dns解析开始/结束

DNS解析是请求DNS(Domain Name System)服务器,将域名解析成ip的过程。

域名解析工作是由JDK中的InetAddress类完成的。

//java.net.InetAddress

public class InetAddress implements java.io.Serializable {

    ...

    public static InetAddress[] getAllByName(String host)

        throws UnknownHostException {

        return impl.lookupAllHostAddr(host, NETID_UNSET).clone();

    }

}

1

2

3

4

5

6

7

8

9

这个解析过程会默认交给系统配置的DNS服务器完成。当然,我们也可以自定义DNS服务器的地址。

返回值InetAddress[]是一个数组,代表着一个域名可能对应着无数多个ip地址,像腾讯的域名对应了十几个ip地址。OkHttp会依次尝试连接这些ip地址,直到连接成功。

//okhttp3.Dns

public interface Dns {

    List<InetAddress> lookup(String hostname) throws UnknownHostException;

}

1

2

3

4

OkHttp中定义了一个Dns接口,其中的lookup(String hostname)方法代表了域名解析的过程。

dnsStart/dnsEnd就是在lookup前后被调用的。

//okhttp3.internal.connection.RouteSelector

public final class RouteSelector {

    private void resetNextInetSocketAddress(Proxy proxy) throws IOException {

        ...

        if (proxy.type() == Proxy.Type.SOCKS) {

          inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));

        } else {

          eventListener.dnsStart(call, socketHost);

        }

        //dns解析

        List<InetAddress> addresses = address.dns().lookup(socketHost);

        eventListener.dnsEnd(call, socketHost, addresses);

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

connectStart/connectEnd 连接开始结束

OkHttp是使用Socket接口建立Tcp连接的,所以这里的连接就是指Socket建立一个连接的过程。

当连接被重用时,connectStart/connectEnd不会被调用。

当请求被重定向到新的域名后,connectStart/connectEnd会被调用多次。

//okhttp3.internal.connection.RealConnection

public final class RealConnection extends Http2Connection.Listener implements Connection {

    private void connectSocket(int connectTimeout, int readTimeout, Call call,

      EventListener eventListener) throws IOException {

            ...

            rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP

            ? address.socketFactory().createSocket()

            : new Socket(proxy);

            //连接开始

            eventListener.connectStart(call, route.socketAddress(), proxy);

            rawSocket.setSoTimeout(readTimeout);

            Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);

      }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

因为创建的连接有两种类型(服务端直连和隧道代理),所以callEnd有两处调用位置。为了在基于代理的连接上使用SSL,需要单独发送CONECT请求。

//okhttp3.internal.connection

public final class RealConnection extends Http2Connection.Listener implements Connection {

    public void connect(int connectTimeout, int readTimeout, int writeTimeout,

      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,

      EventListener eventListener) {

            while (true) {

                if (route.requiresTunnel()) {

          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);

          if (rawSocket == null) {

            // We were unable to connect the tunnel but properly closed down our resources.

            break;

          }

        } else {

          connectSocket(connectTimeout, readTimeout, call, eventListener);

        }

        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);

        //连接结束

        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);

        break;

            }

      }

      private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,

      EventListener eventListener) throws IOException {

            Request tunnelRequest = createTunnelRequest();

            HttpUrl url = tunnelRequest.url();

            for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {

              connectSocket(connectTimeout, readTimeout, call, eventListener);

              tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);

              if (tunnelRequest == null) break; // Tunnel successfully created.

              // The proxy decided to close the connection after an auth challenge. We need to create a new

              // connection, but this time with the auth credentials.

              closeQuietly(rawSocket);

              rawSocket = null;

              sink = null;

              source = null;

              //连接结束

              eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null);

            }

      }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

secureConnectStart/secureConnectEnd TLS安全连接开始和结束

如果我们使用了HTTPS安全连接,在TCP连接成功后需要进行TLS安全协议通信,等TLS通讯结束后才能算是整个连接过程的结束,也就是说connectEnd在secureConnectEnd之后调用。

当存在重定向或连接重试的情况下,secureConnectStart/secureConnectEnd会被调用多次。

在上面看到,在Socket建立连接后,会执行一个establishProtocol方法,这个方法的作用就是TSL/SSL握手。

//okhttp3.internal.connection

public final class RealConnection extends Http2Connection.Listener implements Connection {

    private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,

      int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {

    if (route.address().sslSocketFactory() == null) {

      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {

        socket = rawSocket;

        protocol = Protocol.H2_PRIOR_KNOWLEDGE;

        startHttp2(pingIntervalMillis);

        return;

      }

      socket = rawSocket;

      protocol = Protocol.HTTP_1_1;

      return;

    }

    //安全连接开始

    eventListener.secureConnectStart(call);

    connectTls(connectionSpecSelector);

    //安全连接结束

    eventListener.secureConnectEnd(call, handshake);

    if (protocol == Protocol.HTTP_2) {

      startHttp2(pingIntervalMillis);

    }

  }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

connectEnd 连接失败

在连接过程中,无论是Socket连接失败,还是TSL/SSL握手失败,都会回调connectEnd。

//okhttp3.internal.connection

public final class RealConnection extends Http2Connection.Listener implements Connection {

    public void connect(int connectTimeout, int readTimeout, int writeTimeout,

          int pingIntervalMillis, boolean connectionRetryEnabled, Call call,

          EventListener eventListener) {

                ...

                while (true) {

                    try {

                        ...

                    } catch (IOException e) {

                        eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);

                    }

          }

}     

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

connectionAcquired/connectReleased 连接绑定和释放

因为OkHttp是基于连接复用的,当一次请求结束后并不会马上关闭当前连接,而是放到连接池中,当有相同域名的请求时,会从连接池中取出对应的连接使用,减少了连接的频繁创建和销毁。

当根据一个请求从连接池取连接时,并打开输入输出流就是acquired,用完释放流就是released。

connectionAcquired是在连接成功后被调用的。但是在连接复用的情况下没有连接步骤,connectAcquired会在获取缓存连接后被调用。由于StreamAllocation是连接“Stream”和“Connection”的桥梁,所以在StreamAllocation中会持有一个RealConnection引用。StreamAllocation在查找可用连接的顺序为:StreamAllocation.RealConnection -> ConnectionPool -> ConnectionPool -> new RealConnection

如果直接复用StreamAllocation中的连接,则不会调用connectionAcquired/connectReleased。

//okhttp3.internal.connection

public final class StreamAllocation {

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,

      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {

        // 第一次查缓存 Attempt to get a connection from the pool.

        Internal.instance.get(connectionPool, address, this, null);

        if (foundPooledConnection) {

            eventListener.connectionAcquired(call, result);

        }

        //第二次查缓存

        List<Route> routes = routeSelection.getAll();

        for (int i = 0, size = routes.size(); i < size; i++) {

          Route route = routes.get(i);

          Internal.instance.get(connectionPool, address, this, route);

          if (connection != null) {

            foundPooledConnection = true;

            result = connection;

            this.route = route;

            break;

          }

        }

        // If we found a pooled connection on the 2nd time around, we're done.

        if (foundPooledConnection) {

              eventListener.connectionAcquired(call, result);

              return result;

        }

        //如果缓存没有,则新建连接

        result = new RealConnection(connectionPool, selectedRoute);

        result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,

        connectionRetryEnabled, call, eventListener);

    routeDatabase().connected(result.route());

        eventListener.connectionAcquired(call, result);

      }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

找到合适的连接后,会在基于当前连接构建Http的编解码器HttpCodec,来解析Http请求和响应。

当一个流被主动关闭或异常关闭时,就需要把这个流对应的资源释放(deallocate)掉。

资源释放的两个方面:

1. 将StreamAllocation的引用从RealConnection的队列中移除掉

2. 将RealConnection在连接池中变成空闲状态

请求数据发送和响应数据读取

在OkHttp中,HttpCodec负责对请求和响应按照Http协议进行编解码,包含发送请求头、发送请求体、读取响应头、读取响应体。

//okhttp3.internal.http

public final class CallServerInterceptor implements Interceptor {

    @Override

    public Response intercept(Chain chain) throws IOException {

        ...

        //发送请求头

    realChain.eventListener().requestHeadersStart(realChain.call());

        httpCodec.writeRequestHeaders(request);

        realChain.eventListener().requestHeadersEnd(realChain.call(), request);

        //发送请求体

        realChain.eventListener().requestBodyStart(realChain.call());

        long contentLength = request.body().contentLength();

        CountingSink requestBodyOut =

            new CountingSink(httpCodec.createRequestBody(request, contentLength));

        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

        request.body().writeTo(bufferedRequestBody);

        bufferedRequestBody.close();

        realChain.eventListener()

            .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);

        //读取响应头       

        if (responseBuilder == null) {

            realChain.eventListener().responseHeadersStart(realChain.call());

            responseBuilder = httpCodec.readResponseHeaders(false);

        }

        Response response = responseBuilder

            .request(request)

            .handshake(streamAllocation.connection().handshake())

            .sentRequestAtMillis(sentRequestMillis)

            .receivedResponseAtMillis(System.currentTimeMillis())

            .build();

        int code = response.code();

        if (code == 100) {

          // server sent a 100-continue even though we did not request one.

          // try again to read the actual response

          responseBuilder = httpCodec.readResponseHeaders(false);

          response = responseBuilder

                  .request(request)

                  .handshake(streamAllocation.connection().handshake())

                  .sentRequestAtMillis(sentRequestMillis)

                  .receivedResponseAtMillis(System.currentTimeMillis())

                  .build();

          code = response.code();

        }

        realChain.eventListener()

                .responseHeadersEnd(realChain.call(), response);

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

响应体的读取有些复杂,要根据不同类型的Content-Type决定如何读取响应体,例如固定长度的、基于块(chunk)数据的、未知长度的。同时Http1与Http2也有不同的解析方式。下面以Http1为例。

//okhttp3.internal.http1

public final class Http1Codec implements HttpCodec {

    @Override

    public ResponseBody openResponseBody(Response response) throws IOException {

        //开始解析响应体

        streamAllocation.eventListener.responseBodyStart(streamAllocation.call);

        ...

    }

}

1

2

3

4

5

6

7

8

9

10

11

//okhttp3.internal.connection

public final class StreamAllocation {

    public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) {

        //响应体解析结束

        eventListener.responseBodyEnd(call, bytesRead);

        ...

    }

}

---------------------

作者:joye123

来源:CSDN

原文:https://blog.csdn.net/joye123/article/details/82115562

版权声明:本文为博主原创文章,转载请附上博文链接!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 简介 目前在HTTP协议请求库中,OKHttp应当是非常火的,使用也非常的简单。网上有很多文章写了关于OkHttp...
    第八区阅读 1,374评论 1 5
  • 1 介绍 在我们所处的互联网世界中,HTTP协议算得上是使用最广泛的网络协议。OKHttp是一款高效的HTTP客户...
    天才木木阅读 5,724评论 7 53
  • 喝多了鸡汤 增加了多愁善感的营养 游历于理想与现实的差异间 在钢筋水泥森林里迷失了方向 心雄仰望着高不可攀的标尺 ...
    蜀山袖手人阅读 175评论 1 3
  • 你还记得第一次吃棉花糖的场景吗? 我记得,先是手握竹签转一圈,眼睛里冒着星星,然后小心翼翼的舔着,生怕一不小心全融...
    发射一枚周大仙阅读 193评论 0 0