Connection.java revision 9db3d07b9620b4269ab33f78604a36327e536ce1
1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.net.http; 18 19import android.content.Context; 20import android.os.SystemClock; 21 22import java.io.IOException; 23import java.net.UnknownHostException; 24import java.util.ListIterator; 25import java.util.LinkedList; 26 27import javax.net.ssl.SSLHandshakeException; 28 29import org.apache.http.ConnectionReuseStrategy; 30import org.apache.http.HttpEntity; 31import org.apache.http.HttpException; 32import org.apache.http.HttpHost; 33import org.apache.http.HttpVersion; 34import org.apache.http.ParseException; 35import org.apache.http.ProtocolVersion; 36import org.apache.http.protocol.ExecutionContext; 37import org.apache.http.protocol.HttpContext; 38import org.apache.http.protocol.BasicHttpContext; 39 40/** 41 * {@hide} 42 */ 43abstract class Connection { 44 45 /** 46 * Allow a TCP connection 60 idle seconds before erroring out 47 */ 48 static final int SOCKET_TIMEOUT = 60000; 49 50 private static final int SEND = 0; 51 private static final int READ = 1; 52 private static final int DRAIN = 2; 53 private static final int DONE = 3; 54 private static final String[] states = {"SEND", "READ", "DRAIN", "DONE"}; 55 56 Context mContext; 57 58 /** The low level connection */ 59 protected AndroidHttpClientConnection mHttpClientConnection = null; 60 61 /** 62 * The server SSL certificate associated with this connection 63 * (null if the connection is not secure) 64 * It would be nice to store the whole certificate chain, but 65 * we want to keep things as light-weight as possible 66 */ 67 protected SslCertificate mCertificate = null; 68 69 /** 70 * The host this connection is connected to. If using proxy, 71 * this is set to the proxy address 72 */ 73 HttpHost mHost; 74 75 /** true if the connection can be reused for sending more requests */ 76 private boolean mCanPersist; 77 78 /** context required by ConnectionReuseStrategy. */ 79 private HttpContext mHttpContext; 80 81 /** set when cancelled */ 82 private static int STATE_NORMAL = 0; 83 private static int STATE_CANCEL_REQUESTED = 1; 84 private int mActive = STATE_NORMAL; 85 86 /** The number of times to try to re-connect (if connect fails). */ 87 private final static int RETRY_REQUEST_LIMIT = 2; 88 89 private static final int MIN_PIPE = 2; 90 private static final int MAX_PIPE = 3; 91 92 /** 93 * Doesn't seem to exist anymore in the new HTTP client, so copied here. 94 */ 95 private static final String HTTP_CONNECTION = "http.connection"; 96 97 RequestQueue.ConnectionManager mConnectionManager; 98 RequestFeeder mRequestFeeder; 99 100 /** 101 * Buffer for feeding response blocks to webkit. One block per 102 * connection reduces memory churn. 103 */ 104 private byte[] mBuf; 105 106 protected Connection(Context context, HttpHost host, 107 RequestQueue.ConnectionManager connectionManager, 108 RequestFeeder requestFeeder) { 109 mContext = context; 110 mHost = host; 111 mConnectionManager = connectionManager; 112 mRequestFeeder = requestFeeder; 113 114 mCanPersist = false; 115 mHttpContext = new BasicHttpContext(null); 116 } 117 118 HttpHost getHost() { 119 return mHost; 120 } 121 122 /** 123 * connection factory: returns an HTTP or HTTPS connection as 124 * necessary 125 */ 126 static Connection getConnection( 127 Context context, HttpHost host, 128 RequestQueue.ConnectionManager connectionManager, 129 RequestFeeder requestFeeder) { 130 131 if (host.getSchemeName().equals("http")) { 132 return new HttpConnection(context, host, connectionManager, 133 requestFeeder); 134 } 135 136 // Otherwise, default to https 137 return new HttpsConnection(context, host, connectionManager, 138 requestFeeder); 139 } 140 141 /** 142 * @return The server SSL certificate associated with this 143 * connection (null if the connection is not secure) 144 */ 145 /* package */ SslCertificate getCertificate() { 146 return mCertificate; 147 } 148 149 /** 150 * Close current network connection 151 * Note: this runs in non-network thread 152 */ 153 void cancel() { 154 mActive = STATE_CANCEL_REQUESTED; 155 closeConnection(); 156 if (HttpLog.LOGV) HttpLog.v( 157 "Connection.cancel(): connection closed " + mHost); 158 } 159 160 /** 161 * Process requests in queue 162 * pipelines requests 163 */ 164 void processRequests(Request firstRequest) { 165 Request req = null; 166 boolean empty; 167 int error = EventHandler.OK; 168 Exception exception = null; 169 170 LinkedList<Request> pipe = new LinkedList<Request>(); 171 172 int minPipe = MIN_PIPE, maxPipe = MAX_PIPE; 173 int state = SEND; 174 175 while (state != DONE) { 176 if (HttpLog.LOGV) HttpLog.v( 177 states[state] + " pipe " + pipe.size()); 178 179 /* If a request was cancelled, give other cancel requests 180 some time to go through so we don't uselessly restart 181 connections */ 182 if (mActive == STATE_CANCEL_REQUESTED) { 183 try { 184 Thread.sleep(100); 185 } catch (InterruptedException x) { /* ignore */ } 186 mActive = STATE_NORMAL; 187 } 188 189 switch (state) { 190 case SEND: { 191 if (pipe.size() == maxPipe) { 192 state = READ; 193 break; 194 } 195 /* get a request */ 196 if (firstRequest == null) { 197 req = mRequestFeeder.getRequest(mHost); 198 } else { 199 req = firstRequest; 200 firstRequest = null; 201 } 202 if (req == null) { 203 state = DRAIN; 204 break; 205 } 206 req.setConnection(this); 207 208 /* Don't work on cancelled requests. */ 209 if (req.mCancelled) { 210 if (HttpLog.LOGV) HttpLog.v( 211 "processRequests(): skipping cancelled request " 212 + req); 213 req.complete(); 214 break; 215 } 216 217 if (mHttpClientConnection == null || 218 !mHttpClientConnection.isOpen()) { 219 /* If this call fails, the address is bad or 220 the net is down. Punt for now. 221 222 FIXME: blow out entire queue here on 223 connection failure if net up? */ 224 225 if (!openHttpConnection(req)) { 226 state = DONE; 227 break; 228 } 229 } 230 231 try { 232 /* FIXME: don't increment failure count if old 233 connection? There should not be a penalty for 234 attempting to reuse an old connection */ 235 req.sendRequest(mHttpClientConnection); 236 } catch (HttpException e) { 237 exception = e; 238 error = EventHandler.ERROR; 239 } catch (IOException e) { 240 exception = e; 241 error = EventHandler.ERROR_IO; 242 } catch (IllegalStateException e) { 243 exception = e; 244 error = EventHandler.ERROR_IO; 245 } 246 if (exception != null) { 247 if (httpFailure(req, error, exception) && 248 !req.mCancelled) { 249 /* retry request if not permanent failure 250 or cancelled */ 251 pipe.addLast(req); 252 } 253 exception = null; 254 state = clearPipe(pipe) ? DONE : SEND; 255 minPipe = maxPipe = 1; 256 break; 257 } 258 259 pipe.addLast(req); 260 if (!mCanPersist) state = READ; 261 break; 262 263 } 264 case DRAIN: 265 case READ: { 266 empty = !mRequestFeeder.haveRequest(mHost); 267 int pipeSize = pipe.size(); 268 if (state != DRAIN && pipeSize < minPipe && 269 !empty && mCanPersist) { 270 state = SEND; 271 break; 272 } else if (pipeSize == 0) { 273 /* Done if no other work to do */ 274 state = empty ? DONE : SEND; 275 break; 276 } 277 278 req = (Request)pipe.removeFirst(); 279 if (HttpLog.LOGV) HttpLog.v( 280 "processRequests() reading " + req); 281 282 try { 283 req.readResponse(mHttpClientConnection); 284 } catch (ParseException e) { 285 exception = e; 286 error = EventHandler.ERROR_IO; 287 } catch (IOException e) { 288 exception = e; 289 error = EventHandler.ERROR_IO; 290 } catch (IllegalStateException e) { 291 exception = e; 292 error = EventHandler.ERROR_IO; 293 } 294 if (exception != null) { 295 if (httpFailure(req, error, exception) && 296 !req.mCancelled) { 297 /* retry request if not permanent failure 298 or cancelled */ 299 req.reset(); 300 pipe.addFirst(req); 301 } 302 exception = null; 303 mCanPersist = false; 304 } 305 if (!mCanPersist) { 306 if (HttpLog.LOGV) HttpLog.v( 307 "processRequests(): no persist, closing " + 308 mHost); 309 310 closeConnection(); 311 312 mHttpContext.removeAttribute(HTTP_CONNECTION); 313 clearPipe(pipe); 314 minPipe = maxPipe = 1; 315 state = SEND; 316 } 317 break; 318 } 319 } 320 } 321 } 322 323 /** 324 * After a send/receive failure, any pipelined requests must be 325 * cleared back to the mRequest queue 326 * @return true if mRequests is empty after pipe cleared 327 */ 328 private boolean clearPipe(LinkedList<Request> pipe) { 329 boolean empty = true; 330 if (HttpLog.LOGV) HttpLog.v( 331 "Connection.clearPipe(): clearing pipe " + pipe.size()); 332 synchronized (mRequestFeeder) { 333 Request tReq; 334 while (!pipe.isEmpty()) { 335 tReq = (Request)pipe.removeLast(); 336 if (HttpLog.LOGV) HttpLog.v( 337 "clearPipe() adding back " + mHost + " " + tReq); 338 mRequestFeeder.requeueRequest(tReq); 339 empty = false; 340 } 341 if (empty) empty = mRequestFeeder.haveRequest(mHost); 342 } 343 return empty; 344 } 345 346 /** 347 * @return true on success 348 */ 349 private boolean openHttpConnection(Request req) { 350 351 long now = SystemClock.uptimeMillis(); 352 int error = EventHandler.OK; 353 Exception exception = null; 354 355 try { 356 // reset the certificate to null before opening a connection 357 mCertificate = null; 358 mHttpClientConnection = openConnection(req); 359 if (mHttpClientConnection != null) { 360 mHttpClientConnection.setSocketTimeout(SOCKET_TIMEOUT); 361 mHttpContext.setAttribute(HTTP_CONNECTION, 362 mHttpClientConnection); 363 } else { 364 // we tried to do SSL tunneling, failed, 365 // and need to drop the request; 366 // we have already informed the handler 367 req.mFailCount = RETRY_REQUEST_LIMIT; 368 return false; 369 } 370 } catch (UnknownHostException e) { 371 if (HttpLog.LOGV) HttpLog.v("Failed to open connection"); 372 error = EventHandler.ERROR_LOOKUP; 373 exception = e; 374 } catch (IllegalArgumentException e) { 375 if (HttpLog.LOGV) HttpLog.v("Illegal argument exception"); 376 error = EventHandler.ERROR_CONNECT; 377 req.mFailCount = RETRY_REQUEST_LIMIT; 378 exception = e; 379 } catch (SSLConnectionClosedByUserException e) { 380 // hack: if we have an SSL connection failure, 381 // we don't want to reconnect 382 req.mFailCount = RETRY_REQUEST_LIMIT; 383 // no error message 384 return false; 385 } catch (SSLHandshakeException e) { 386 // hack: if we have an SSL connection failure, 387 // we don't want to reconnect 388 req.mFailCount = RETRY_REQUEST_LIMIT; 389 if (HttpLog.LOGV) HttpLog.v( 390 "SSL exception performing handshake"); 391 error = EventHandler.ERROR_FAILED_SSL_HANDSHAKE; 392 exception = e; 393 } catch (IOException e) { 394 error = EventHandler.ERROR_CONNECT; 395 exception = e; 396 } 397 398 if (HttpLog.LOGV) { 399 long now2 = SystemClock.uptimeMillis(); 400 HttpLog.v("Connection.openHttpConnection() " + 401 (now2 - now) + " " + mHost); 402 } 403 404 if (error == EventHandler.OK) { 405 return true; 406 } else { 407 if (req.mFailCount < RETRY_REQUEST_LIMIT) { 408 // requeue 409 mRequestFeeder.requeueRequest(req); 410 req.mFailCount++; 411 } else { 412 httpFailure(req, error, exception); 413 } 414 return error == EventHandler.OK; 415 } 416 } 417 418 /** 419 * Helper. Calls the mEventHandler's error() method only if 420 * request failed permanently. Increments mFailcount on failure. 421 * 422 * Increments failcount only if the network is believed to be 423 * connected 424 * 425 * @return true if request can be retried (less than 426 * RETRY_REQUEST_LIMIT failures have occurred). 427 */ 428 private boolean httpFailure(Request req, int errorId, Exception e) { 429 boolean ret = true; 430 431 // e.printStackTrace(); 432 if (HttpLog.LOGV) HttpLog.v( 433 "httpFailure() ******* " + e + " count " + req.mFailCount + 434 " " + mHost + " " + req.getUri()); 435 436 if (++req.mFailCount >= RETRY_REQUEST_LIMIT) { 437 ret = false; 438 String error; 439 if (errorId < 0) { 440 error = mContext.getText( 441 EventHandler.errorStringResources[-errorId]).toString(); 442 } else { 443 Throwable cause = e.getCause(); 444 error = cause != null ? cause.toString() : e.getMessage(); 445 } 446 req.mEventHandler.error(errorId, error); 447 req.complete(); 448 } 449 450 closeConnection(); 451 mHttpContext.removeAttribute(HTTP_CONNECTION); 452 453 return ret; 454 } 455 456 HttpContext getHttpContext() { 457 return mHttpContext; 458 } 459 460 /** 461 * Use same logic as ConnectionReuseStrategy 462 * @see ConnectionReuseStrategy 463 */ 464 private boolean keepAlive(HttpEntity entity, 465 ProtocolVersion ver, int connType, final HttpContext context) { 466 org.apache.http.HttpConnection conn = (org.apache.http.HttpConnection) 467 context.getAttribute(ExecutionContext.HTTP_CONNECTION); 468 469 if (conn != null && !conn.isOpen()) 470 return false; 471 // do NOT check for stale connection, that is an expensive operation 472 473 if (entity != null) { 474 if (entity.getContentLength() < 0) { 475 if (!entity.isChunked() || ver.lessEquals(HttpVersion.HTTP_1_0)) { 476 // if the content length is not known and is not chunk 477 // encoded, the connection cannot be reused 478 return false; 479 } 480 } 481 } 482 // Check for 'Connection' directive 483 if (connType == Headers.CONN_CLOSE) { 484 return false; 485 } else if (connType == Headers.CONN_KEEP_ALIVE) { 486 return true; 487 } 488 // Resorting to protocol version default close connection policy 489 return !ver.lessEquals(HttpVersion.HTTP_1_0); 490 } 491 492 void setCanPersist(HttpEntity entity, ProtocolVersion ver, int connType) { 493 mCanPersist = keepAlive(entity, ver, connType, mHttpContext); 494 } 495 496 void setCanPersist(boolean canPersist) { 497 mCanPersist = canPersist; 498 } 499 500 boolean getCanPersist() { 501 return mCanPersist; 502 } 503 504 /** typically http or https... set by subclass */ 505 abstract String getScheme(); 506 abstract void closeConnection(); 507 abstract AndroidHttpClientConnection openConnection(Request req) throws IOException; 508 509 /** 510 * Prints request queue to log, for debugging. 511 * returns request count 512 */ 513 public synchronized String toString() { 514 return mHost.toString(); 515 } 516 517 byte[] getBuf() { 518 if (mBuf == null) mBuf = new byte[8192]; 519 return mBuf; 520 } 521 522} 523