Added first support for persistent connection in NET http client implementation.

Various improvement related to eventual errors.
This commit is contained in:
2015-09-16 22:51:58 +02:00
parent 65b28ed877
commit b69b8aaaf9
8 changed files with 326 additions and 74 deletions

View File

@@ -7,7 +7,7 @@
<exclude>/EIFGENs$</exclude>
<exclude>/.svn$</exclude>
</file_rule>
<option warning="true" full_class_checking="true" is_attached_by_default="true" void_safety="all" syntax="provisional">
<option debug="false" warning="true" full_class_checking="true" is_attached_by_default="true" void_safety="all" syntax="provisional">
</option>
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
<library name="curl" location="$ISE_LIBRARY\library\cURL\cURL-safe.ecf">
@@ -15,7 +15,6 @@
<custom name="libcurl_http_client_disabled" excluded_value="true"/>
</condition>
</library>
<library name="encoder" location="..\..\text\encoder\encoder-safe.ecf"/>
<library name="http" location="..\protocol\http\http-safe.ecf"/>
<library name="http_auth" location="..\..\server\authentication\http_authorization\http_authorization-safe.ecf"/>
@@ -30,51 +29,55 @@
<custom name="netssl_http_client_enabled" value="true"/>
</condition>
</library>
<library name="uri" location="$ISE_LIBRARY\library\text\uri\uri-safe.ecf"/>
<cluster name="src" location=".\src\">
<cluster name="spec_null" location="$|spec/null" recursive="true"/>
<cluster name="spec_net" location="$|spec/net">
<cluster name="spec_null" location="$|spec\null\" recursive="true"/>
<cluster name="spec_net" location="$|spec\net\">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
</condition>
<cluster name="net_ssl_disabled" location="$|no_ssl">
<cluster name="net_implementation" location="$|implementation\" hidden="true">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
</condition>
</cluster>
<cluster name="net_ssl_disabled" location="$|no_ssl\">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
<custom name="netssl_http_client_enabled" excluded_value="true"/>
</condition>
</cluster>
<cluster name="net_ssl_enabled" location="$|ssl">
<cluster name="net_ssl_enabled" location="$|ssl\">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
<custom name="netssl_http_client_enabled" value="true"/>
</condition>
</cluster>
</cluster>
<cluster name="spec_libcurl" location="$|spec/libcurl" recursive="true">
<cluster name="spec_libcurl" location="$|spec\libcurl\" recursive="true">
<condition>
<custom name="libcurl_http_client_disabled" excluded_value="true"/>
</condition>
</cluster>
<cluster name="default_null" location="$|default/null">
<cluster name="default_null" location="$|default\null\">
<condition>
<custom name="net_http_client_disabled" value="true"/>
<custom name="libcurl_http_client_disabled" value="true"/>
</condition>
</cluster>
<cluster name="default_net" location="$|default/net">
<cluster name="default_net" location="$|default\net\">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
<custom name="libcurl_http_client_disabled" value="true"/>
</condition>
</cluster>
<cluster name="default_libcurl" location="$|default/libcurl">
<cluster name="default_libcurl" location="$|default\libcurl\">
<condition>
<custom name="net_http_client_disabled" value="true"/>
<custom name="libcurl_http_client_disabled" excluded_value="true"/>
</condition>
</cluster>
<cluster name="default_libcurl_or_net" location="$|default/libcurl_or_net">
<cluster name="default_libcurl_or_net" location="$|default\libcurl_or_net\">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
<custom name="libcurl_http_client_disabled" excluded_value="true"/>

View File

@@ -15,7 +15,6 @@
<custom name="libcurl_http_client_disabled" excluded_value="true"/>
</condition>
</library>
<library name="encoder" location="..\..\text\encoder\encoder.ecf"/>
<library name="http" location="..\protocol\http\http.ecf"/>
<library name="http_auth" location="..\..\server\authentication\http_authorization\http_authorization.ecf"/>
@@ -30,57 +29,60 @@
<custom name="netssl_http_client_enabled" value="true"/>
</condition>
</library>
<library name="uri" location="$ISE_LIBRARY\library\text\uri\uri.ecf"/>
<cluster name="src" location=".\src\">
<cluster name="spec_null" location="$|spec/null" recursive="true"/>
<cluster name="spec_net" location="$|spec/net">
<cluster name="spec_null" location="$|spec\null\" recursive="true"/>
<cluster name="spec_net" location="$|spec\net\">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
</condition>
<cluster name="net_ssl_disabled" location="$|no_ssl">
<cluster name="net_implementation" location="$|implementation\" hidden="true">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
</condition>
</cluster>
<cluster name="net_ssl_disabled" location="$|no_ssl\">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
<custom name="netssl_http_client_enabled" excluded_value="true"/>
</condition>
</cluster>
<cluster name="net_ssl_enabled" location="$|ssl">
<cluster name="net_ssl_enabled" location="$|ssl\">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
<custom name="netssl_http_client_enabled" value="true"/>
</condition>
</cluster>
</cluster>
<cluster name="spec_libcurl" location="$|spec/libcurl" recursive="true">
<cluster name="spec_libcurl" location="$|spec\libcurl\" recursive="true">
<condition>
<custom name="libcurl_http_client_disabled" excluded_value="true"/>
</condition>
</cluster>
<cluster name="default_null" location="$|default/null">
<cluster name="default_null" location="$|default\null\">
<condition>
<custom name="net_http_client_disabled" value="true"/>
<custom name="libcurl_http_client_disabled" value="true"/>
</condition>
</cluster>
<cluster name="default_net" location="$|default/net">
<cluster name="default_net" location="$|default\net\">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
<custom name="libcurl_http_client_disabled" value="true"/>
</condition>
</cluster>
<cluster name="default_libcurl" location="$|default/libcurl">
<cluster name="default_libcurl" location="$|default\libcurl\">
<condition>
<custom name="net_http_client_disabled" value="true"/>
<custom name="libcurl_http_client_disabled" excluded_value="true"/>
</condition>
</cluster>
<cluster name="default_libcurl_or_net" location="$|default/libcurl_or_net">
<cluster name="default_libcurl_or_net" location="$|default\libcurl_or_net\">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
<custom name="libcurl_http_client_disabled" excluded_value="true"/>
</condition>
</cluster>
</cluster>
</target>
</system>

View File

@@ -217,6 +217,22 @@ feature -- Access
end
end
feature -- Status report
is_http_1_0: BOOLEAN
-- Is response using HTTP/1.0 protocole?
--| Note: it is relevant once the raw header are set.
do
Result := attached http_version as v and then v.same_string ("1.0")
end
is_http_1_1: BOOLEAN
-- Is response using HTTP/1.1 protocole?
--| Note: it is relevant once the raw header are set.
do
Result := attached http_version as v and then v.same_string ("1.1")
end
feature -- Change
set_http_version (v: like http_version)
@@ -345,10 +361,23 @@ feature -- Change
set_raw_header (h: READABLE_STRING_8)
-- Set http header `raw_header' to `h'
local
i: INTEGER
s: STRING_8
do
raw_header := h
--| Reset internal headers
internal_headers := Void
--| Set status line, right away.
i := h.index_of ('%N', 1)
if i > 0 then
s := h.substring (1, i - 1)
if s.starts_with ("HTTP/") then
s.right_adjust
set_status_line (s)
end
end
end
add_redirection (s: detachable READABLE_STRING_8; h: READABLE_STRING_8; a_body: detachable READABLE_STRING_8)

View File

@@ -50,6 +50,14 @@ feature {NONE} -- Initialization
feature -- Basic operation
close
-- Close session.
--| useful to disconnect persistent connection.
do
end
feature -- Access
url (a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): STRING_8
-- Url computed from Current and `ctx' data.
local
@@ -88,17 +96,23 @@ feature -- Helper
get (a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): HTTP_CLIENT_RESPONSE
-- Response for GET request based on Current, `a_path' and `ctx'.
require
is_available: is_available
deferred
end
head (a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): HTTP_CLIENT_RESPONSE
-- Response for HEAD request based on Current, `a_path' and `ctx'.
require
is_available: is_available
deferred
end
post (a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT; data: detachable READABLE_STRING_8): HTTP_CLIENT_RESPONSE
-- Response for POST request based on Current, `a_path' and `ctx'
-- with input `data'
require
is_available: is_available
deferred
end
@@ -111,29 +125,39 @@ feature -- Helper
patch (a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT; data: detachable READABLE_STRING_8): HTTP_CLIENT_RESPONSE
-- Response for PATCH request based on Current, `a_path' and `ctx'
-- with input `data'
require
is_available: is_available
deferred
end
patch_file (a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT; fn: detachable READABLE_STRING_8): HTTP_CLIENT_RESPONSE
-- Response for PATCH request based on Current, `a_path' and `ctx'
-- with uploaded data file `fn'
require
is_available: is_available
deferred
end
put (a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT; data: detachable READABLE_STRING_8): HTTP_CLIENT_RESPONSE
-- Response for PUT request based on Current, `a_path' and `ctx'
-- with input `data'
require
is_available: is_available
deferred
end
put_file (a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT; fn: detachable READABLE_STRING_8): HTTP_CLIENT_RESPONSE
-- Response for PUT request based on Current, `a_path' and `ctx'
-- with uploaded file `fn'
require
is_available: is_available
deferred
end
delete (a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): HTTP_CLIENT_RESPONSE
-- Response for DELETE request based on Current, `a_path' and `ctx'
require
is_available: is_available
deferred
end

View File

@@ -0,0 +1,52 @@
note
description: "Socket connection information, used for peristent connection implementation."
date: "$Date$"
revision: "$Revision$"
class
NET_HTTP_CLIENT_CONNECTION
create
make
feature {NONE} -- Initialization
make (a_socket: NETWORK_STREAM_SOCKET; a_host: READABLE_STRING_GENERAL; a_port: INTEGER)
do
socket := a_socket
host := a_host
port := a_port
end
feature -- Access
socket: NETWORK_STREAM_SOCKET
-- Persistent connection socket.
host: READABLE_STRING_GENERAL
-- Host used for this connection.
port: INTEGER
-- Port used for this connection.
feature -- Status report
is_reusable (a_host: READABLE_STRING_GENERAL; a_port: INTEGER): BOOLEAN
-- Is Current connection reusable for new connection `a_host:a_port'?
do
if a_host.same_string (host) and port = a_port then
Result := socket.is_connected
end
end
note
copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[
Eiffel Software
5949 Hollister Ave., Goleta, CA 93117 USA
Telephone 805-685-1006, Fax 805-685-6869
Website http://www.eiffel.com
Customer support http://support.eiffel.com
]"
end

View File

@@ -28,13 +28,44 @@ feature {NONE} -- Internal
session: NET_HTTP_CLIENT_SESSION
net_http_client_version: STRING = "0.1"
new_socket (a_host: READABLE_STRING_8; a_port: INTEGER; a_is_https: BOOLEAN; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): NETWORK_STREAM_SOCKET
session_socket (a_host: READABLE_STRING_8; a_port: INTEGER; a_is_https: BOOLEAN; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): NETWORK_STREAM_SOCKET
-- Session socket to use for connection.
-- Eventually reuse the persistent connection if any.
local
l_socket: detachable NETWORK_STREAM_SOCKET
do
if a_is_https then
create {SSL_NETWORK_STREAM_SOCKET} Result.make_client_by_port (a_port, a_host)
if
attached session.persistent_connection as l_persistent_connection and then
l_persistent_connection.is_reusable (a_host, a_port)
then
l_socket := l_persistent_connection.socket
if a_is_https then
if attached {SSL_NETWORK_STREAM_SOCKET} l_socket as l_ssl_socket then
Result := l_ssl_socket
else
l_socket := Void
end
elseif attached {SSL_NETWORK_STREAM_SOCKET} l_socket as l_ssl_socket then
l_socket := Void
end
if l_socket /= Void and then not l_socket.is_connected then
-- Reset persistent connection
l_socket := Void
end
end
if l_socket /= Void then
-- Reuse persistent connection.
Result := l_socket
else
create Result.make_client_by_port (a_port, a_host)
session.set_persistent_connection (Void)
if a_is_https then
create {SSL_NETWORK_STREAM_SOCKET} Result.make_client_by_port (a_port, a_host)
else
create Result.make_client_by_port (a_port, a_host)
end
Result.set_connect_timeout (connect_timeout)
Result.set_timeout (timeout)
Result.connect
end
end
@@ -67,13 +98,14 @@ feature -- Access
l_form_string: STRING
l_prev_header: READABLE_STRING_8
l_boundary: READABLE_STRING_8
l_is_http_1_0: BOOLEAN
l_is_http_1_0_request: BOOLEAN
l_is_keep_alive: BOOLEAN
retried: BOOLEAN
do
if not retried then
ctx := context
if ctx /= Void then
l_is_http_1_0 := attached ctx.http_version as l_http_version and then l_http_version.same_string ("HTTP/1.0")
l_is_http_1_0_request := attached ctx.http_version as l_http_version and then l_http_version.same_string ("HTTP/1.0")
end
create Result.make (url)
@@ -96,10 +128,7 @@ feature -- Access
end
-- Connect
l_socket := new_socket (l_host, l_port, l_is_https, ctx)
l_socket.set_connect_timeout (connect_timeout)
l_socket.set_timeout (timeout)
l_socket.connect
l_socket := session_socket (l_host, l_port, l_is_https, ctx)
if l_socket.is_connected then
create l_form_string.make_empty
@@ -197,7 +226,7 @@ feature -- Access
s.append_character (' ')
s.append (l_request_uri)
s.append_character (' ')
if l_is_http_1_0 then
if l_is_http_1_0_request then
s.append ("HTTP/1.0")
else
s.append ("HTTP/1.1")
@@ -213,12 +242,12 @@ feature -- Access
s.append (l_host)
end
s.append (http_end_of_header_line)
-- s.append ("Connection: close")
-- s.append (http_end_of_header_line)
-- Append the given request headers
l_cookie := Void
if headers.is_empty then
s.append (Http_end_of_command)
else
if not headers.is_empty then
across
headers as ic
loop
@@ -235,11 +264,10 @@ feature -- Access
s.append (Http_end_of_header_line)
end
end
s.append (Http_end_of_header_line)
end
-- Compute Header Cookie: if needed
-- Get session cookie
-- Use session cookie
if l_cookie = Void then
l_cookie := session.cookie
else
@@ -255,6 +283,10 @@ feature -- Access
s.append (l_upload_data)
s.append (http_end_of_header_line)
end
--| End of client header.
s.append (Http_end_of_header_line)
--| Note that any remaining file to upload will be done directly via the socket
--| to optimize memory usage
@@ -270,6 +302,9 @@ feature -- Access
--| Send request |--
--|-------------------------|--
debug ("socket_header")
io.error.put_string (s)
end
l_socket.put_string (s)
--| Send remaining payload data, if needed.
if l_upload_file /= Void then
@@ -283,35 +318,57 @@ feature -- Access
--|-------------------------|--
if l_socket.ready_for_reading then
create l_message.make_empty
append_socket_header_content_to (l_socket, l_message)
append_socket_header_content_to (Result, l_socket, l_message)
l_prev_header := Result.raw_header
Result.set_raw_header (l_message.string)
l_content_length := -1
if attached Result.header ("Content-Length") as s_len and then s_len.is_integer then
l_content_length := s_len.to_integer
end
l_location := Result.header ("Location")
if attached Result.header ("Set-Cookie") as s_cookies then
session.set_cookie (s_cookies)
end
l_message.append (http_end_of_header_line)
-- Get content if any.
append_socket_content_to (Result, l_socket, l_content_length, l_message)
-- Restore previous header
Result.set_raw_header (l_prev_header)
-- Set message
Result.set_response_message (l_message, ctx)
-- Check status code.
check status_coherent: attached Result.status_line as l_status_line implies l_status_line.has_substring (Result.status.out) end
if not Result.error_occurred then
-- Get information from header
l_content_length := -1
if attached Result.header ("Content-Length") as s_len and then s_len.is_integer then
l_content_length := s_len.to_integer
end
l_location := Result.header ("Location")
if attached Result.header ("Set-Cookie") as s_cookies then
session.set_cookie (s_cookies)
end
-- follow redirect
if l_location /= Void then
if Result.redirections_count < max_redirects then
initialize (l_location, ctx)
redirection_response := response
redirection_response.add_redirection (Result.status_line, Result.raw_header, Result.body)
Result := redirection_response
-- Keep-alive connection?
-- with HTTP/1.1, this is the default, and could be changed by Connection: close
-- with HTTP/1.0, it requires "Connection: keep-alive" header line.
if attached Result.header ("Connection") as s_connection then
l_is_keep_alive := s_connection.same_string ("keep-alive")
else
l_is_keep_alive := not Result.is_http_1_0
end
-- Get content if any.
append_socket_content_to (Result, l_socket, l_content_length, l_message)
-- Restore previous header
Result.set_raw_header (l_prev_header)
-- Set message
Result.set_response_message (l_message, ctx)
-- Check status code.
check status_coherent: attached Result.status_line as l_status_line implies l_status_line.has_substring (Result.status.out) end
if l_is_keep_alive then
session.set_persistent_connection (create {NET_HTTP_CLIENT_CONNECTION}.make (l_socket, l_host, l_port))
else
session.set_persistent_connection (Void)
end
-- follow redirect
if l_location /= Void then
if Result.redirections_count < max_redirects then
initialize (l_location, ctx)
redirection_response := response
redirection_response.add_redirection (Result.status_line, Result.raw_header, Result.body)
Result := redirection_response
end
end
if not l_is_keep_alive then
l_socket.cleanup
end
end
else
@@ -477,7 +534,7 @@ feature {NONE} -- Helpers
end
end
append_socket_header_content_to (a_socket: NETWORK_STREAM_SOCKET; a_output: STRING)
append_socket_header_content_to (a_response: HTTP_CLIENT_RESPONSE; a_socket: NETWORK_STREAM_SOCKET; a_output: STRING)
-- Get header from `a_socket' into `a_output'.
local
s: READABLE_STRING_8
@@ -485,16 +542,30 @@ feature {NONE} -- Helpers
from
s := ""
until
s.same_string ("%R") or not a_socket.readable
s.same_string ("%R") or not a_socket.readable or a_response.error_occurred
loop
a_socket.read_line_thread_aware
s := a_socket.last_string
if s.same_string ("%R") then
-- Reach end of header
-- a_output.append (http_end_of_header_line)
if a_socket.ready_for_reading then
a_socket.read_line_thread_aware
s := a_socket.last_string
debug ("socket_header")
io.error.put_string ("header-line: " + s + "%N")
end
if s.is_empty then
debug ("socket_header")
io.error.put_string ("ERROR: zero byte read when receiving header.%N")
end
a_response.set_error_message ("Read zero byte, expecting header line")
elseif s.same_string ("%R") then
-- Reach end of header
else
a_output.append (s)
a_output.append_character ('%N')
end
else
a_output.append (s)
a_output.append_character ('%N')
debug ("socket_header")
io.error.put_string ("ERROR: timeout when receiving header.%N")
end
a_response.set_error_message ("Could not read header data, timeout")
end
end
end

View File

@@ -10,6 +10,9 @@ class
inherit
HTTP_CLIENT_SESSION
redefine
close
end
NET_HTTP_CLIENT_INFO
@@ -33,6 +36,30 @@ feature -- Status report
end
end
feature -- Access
persistent_connection: detachable NET_HTTP_CLIENT_CONNECTION
-- Socket used for persistent connection purpose.
feature -- Element change
set_persistent_connection (a_connection: like persistent_connection)
-- Set `persistent_connection' to `a_connection'.
do
persistent_connection := a_connection
end
feature -- Basic operation
close
-- <Precursor>
do
if attached persistent_connection as l_connection then
persistent_connection := Void
l_connection.socket.cleanup
end
end
feature -- Custom
custom (a_method: READABLE_STRING_8; a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): HTTP_CLIENT_RESPONSE

View File

@@ -37,6 +37,50 @@ feature -- Tests
test_headers
end
test_persistent_connection
local
sess: like new_session
h: STRING_8
do
sess := new_session ("http://www.google.fr")
if attached sess.get ("/", Void) as res then
assert ("Get returned without error", not res.error_occurred)
create h.make_empty
if attached res.headers as hds then
across
hds as c
loop
h.append (c.item.name + ": " + c.item.value + "%R%N")
end
end
if attached res.body as l_body then
assert ("body not empty", not l_body.is_empty)
else
assert ("missing body", False)
end
assert ("same headers", h.same_string (res.raw_header))
end
if attached sess.get ("/", Void) as res then
assert ("Get returned without error", not res.error_occurred)
create h.make_empty
if attached res.headers as hds then
across
hds as c
loop
h.append (c.item.name + ": " + c.item.value + "%R%N")
end
end
if attached res.body as l_body then
assert ("body not empty", not l_body.is_empty)
else
assert ("missing body", False)
end
assert ("same headers", h.same_string (res.raw_header))
end
sess.close
end
end