Articles Scalable HTTP/S and TCP client sockets for the cloud by Allen Drennan

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
Scalable HTTP/S and TCP client sockets for the cloud
January 9, 2017 Allen Drennan
[SHOWTOGROUPS=4,20]
Now that the cloud has become a core part of every developer’s life, we are faced with designing scalable, distributed services that reside on these platforms (Для просмотра ссылки Войди или Зарегистрируйся, Для просмотра ссылки Войди или Зарегистрируйся and Для просмотра ссылки Войди или Зарегистрируйся) and are able to interact with other third-party facing services and middle-ware such as databases, messaging brokers, remote push messaging services and more.

The scalable client socket problem
One goal we have from our services is to squeeze as much resources out of the node to handle user load and volume without adding more nodes or servers to our service. Typical third-party services either take an approach where they offer an HTTP enabled API (such as REST/JSON) or a TCP binary protocol interface (such as remote push messaging). Some third-party services offer their own libraries to interact with their protocol (common with databases, messaging brokers).

If your service runs in the cloud and your are connecting to other third-party products or services, then you are creating a client socket originating from your process and connecting to a server-socket at the third-party service.

With all of these solutions you are faced with a common bottleneck, client sockets. Because you are originating a socket connection from your service to another third-party service, you are limited by the operating system’s implementation of sockets. Most operating systems offer a scalable socket option (such as IOCP on Windows or Для просмотра ссылки Войди или Зарегистрируйся) but all too often the client-side HTTP and TCP support libraries only implement basic Для просмотра ссылки Войди или Зарегистрируйся. We also see this problem in many third-party drivers interfaces when they provide their own library for the service. Whether you are using HTTP, TCP or a library driver, these limitations can become an issue for your service if you intend to scale.

Some forward thinking libraries have embedded scalable sockets into their drivers to work around these limitations, but most of the time this is focused on server sockets.

The lack of a foundation
The team here at Grijjy was faced with the same reality. We wanted scalable sockets for our distributed cloud services but the embedded libraries often only leverage Berkeley sockets. While there are numerous libraries that fully exploit scalable server sockets, there are relatively few examples of scalable client sockets. In addition, very few communication libraries or examples have built a reusable base foundation for scalable client sockets that provides a cross-platform (Windows, Linux) model where other protocols could be layered and built upon.

A good model would involve a base class model for client sockets that is scalable on Windows and Linux with a common class syntax. The base class would support SSL for TCP and HTTP. The base class would be inherited to provide not just TCP clients, but HTTP, HTTPS and HTTP2/S clients on Windows and Linux with a common class syntax.

Then upon this foundation we could be build scalable client drivers, whether those drivers are HTTP/REST oriented or they are binary protocols, to provide a better solution to interacting with third-party services.

Over the coming months we will be demonstrating a variety of protocols and drivers that implement over these base classes including RabbitMQ, MongoDB, Google APIs, iOS and Android remote push message sending, etc.

Windows IO Completion Ports (IOCP)
On Windows we have IO Completion Ports for scalable client sockets. IOCP has been around for quite some time, but it is quite tricky to utilize and operations are difficult to manage so developers typically have only used it for server sockets. This means very little is documented about using IOCP for strictly client sockets even though it is an ideal solution to scalable client sockets on Windows.

A full primer on using IOCP for client scalable sockets is beyond the scope of this article, but we will are glad to provide guidance for your efforts if you need more information.
Memory pooling
In order for scalable client sockets to remain efficient, we use very small memory buffers. Because these memory buffers are used for a single operation and must be maintained until the operation is completed, it requires us to create our own memory pooling class. Without a memory pooling class we would be constantly allocating and releasing small blocks of memory and this operation alone could become the primary bottleneck for performance of the scalable client socket class.

The memory pool doesn’t release the memory block but instead preserves it and provides it again for future scalable client socket operations.

Linux (EPOLL)
On Linux we utilize EPOLL for scalable client sockets. On Linux there are several accepted mechanisms for scalable sockets but most documentation relates to scalable server sockets, not client sockets. Our class is a mirror of the Windows class structure and works in the same manner.

A full primer on using EPOLL for client scalable sockets is also beyond the scope of this article.

First steps
Our first step is to create a base class designed specifically for scalable client sockets that works on both Windows and Linux. To accomplish this objective we created the TgoSocketConnection base class. This class abstracts the internals of managing a scalable client socket connection for the platform. It takes care of connecting, disconnecting, sending and receiving. It handles SSL with basic certificate support using OpenSSL internally so that other higher level protocols can use SSL, such as HTTPS.
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
{ Socket connection instance }
TgoSocketConnection = class(TObject)
public
constructor Create(const AOwner: TgoClientSocketManager; const AHostname: String; const APort: Word);
destructor Destroy; override;
public
{ Connects the socket }
function Connect(const AUseNagle: Boolean = True): Boolean;

{ Sends the bytes to the socket }
function Send(const ABytes: TBytes): Boolean;
public
{ Socket handle }
property Socket: TgoSocket read FSocket write FSocket;

{ Current state of the socket connection }
property State: TgoConnectionState read FState write FState;

{ Number of pending operations on the socket }
property Pending: Integer read GetPending;

{ Socket is shutdown }
property Shutdown: Boolean read GetShutdown;

{ Connection is closed }
property Closed: Boolean read GetClosed write SetClosed;

{ OpenSSL interface }
property OpenSSL: TgoOpenSSL read GetOpenSSL;
public
{ Using SSL }
property SSL: Boolean read FSSL write FSSL;

{ Using ALPN }
property ALPN: Boolean read FALPN write FALPN;

{ Certificate in PEM format }
property Certificate: TBytes read GetCertificate write SetCertificate;

{ Private key in PEM format }
property PrivateKey: TBytes read GetPrivateKey write SetPrivateKey;

{ Password for private key }
property Password: String read GetPassword write SetPassword;
public
{ Fired when the socket is connected and ready to be written }
property OnConnected: TgoSocketNotifyEvent read FOnConnected write FOnConnected;

{ Fired when the socket is disconnected, either gracefully if the state
is Disconnecting or abrupt if the state is Connected }
property OnDisconnected: TgoSocketNotifyEvent read FOnDisconnected write FOnDisconnected;

{ Fired when the data has been received by the socket }
property OnRecv: TgoSocketDataEvent read FOnRecv write FOnRecv;

{ Fired when the data has been sent by the socket }
property OnSent: TgoSocketDataEvent read FOnSent write FOnSent;
end;

TgoClientSocketManager was created so that we could pool, reuse and cleanup client sockets. This lessens the burden of operating system resources. Use of the socket manager is abstracted and transparent to the parent protocol class that uses it.

Our TgoClientSocketManager helps manage the issues of releasing resources, cleaning up and allocating objects in a manner that is compatible with the given platform. In the case of IOCP, we are making sure that all IO activity on a given completion port has completed and we are safe to remove and destroy objects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{ Client socket manager }
TgoClientSocketManager = class(TThread)
protected
procedure Execute; override;
public
constructor Create(const AOptimization: TgoSocketOptimization = TgoSocketOptimization.Scale;
const ABehavior: TgoSocketPoolBehavior = TgoSocketPoolBehavior.CreateAndDestroy; const AWorkers: Integer = 0);
destructor Destroy; override;
public
{ Releases the connection back to the socket pool }
procedure Release(const AConnection: TgoSocketConnection);

{ Requests a connection from the socket pool }
function Request(const AHostname: String; const APort: Word): TgoSocketConnection;
public
{ Completion handle for IOCP }
property Handle: THandle read FHandle;

{ Optimization mode }
property Optimization: TgoSocketOptimization read FOptimization;
end;

The full implementation of the base socket classes for Windows is contained in the repository Для просмотра ссылки Войди или Зарегистрируйся


The full implementation of the base http class for Windows is contained in the repository Для просмотра ссылки Войди или Зарегистрируйся

[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20]
HTTP/S Protocol
The first protocol we demonstrate using scalable client sockets is a HTTP/S. This basic implementation of HTTP and HTTPS shows how we can use scalable client sockets transparently and create a highly scalable protocol.

Embedded into your Windows or Linux service, this example protocol could handle large amounts of API activity using higher level REST/JSON/XML calls.
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
{ HTTP client }
TgoHTTPClient = class(TObject)
public
constructor Create;
destructor Destroy; override;
public
{ Add a header and value to a list of headers }
procedure AddHeader(AHTTPHeaders: TStrings; const AHeader, AValue: UnicodeString);

{ Find the index of a header from a list of headers }
function IndexOfHeader(AHTTPHeaders: TStrings; const AHeader: UnicodeString): Integer;

{ Get the value associated with a header from a list of headers }
function GetHeaderValue(AHTTPHeaders: TStrings; const AHeader: UnicodeString): UnicodeString;

{ Get method }
function Get(const AURL: UnicodeString;
const ARecvTimeout: Integer = DEFAULT_TIMEOUT_RECV): UnicodeString;

{ Post method }
function Post(const AURL: UnicodeString;
const ARecvTimeout: Integer = DEFAULT_TIMEOUT_RECV): UnicodeString;

{ Put method }
function Put(const AURL: UnicodeString;
const ARecvTimeout: Integer = DEFAULT_TIMEOUT_RECV): UnicodeString;

{ Delete method }
function Delete(const AURL: UnicodeString;
const ARecvTimeout: Integer = DEFAULT_TIMEOUT_RECV): UnicodeString;

{ Options method }
function Options(const AURL: UnicodeString;
const ARecvTimeout: Integer = DEFAULT_TIMEOUT_RECV): UnicodeString;

{ Cookies sent to the server and received from the server }
property Cookies: TStrings read GetCookies write SetCookies;

{ Optional body for a request.
You can either use RequestBody or RequestData. If both are specified then
only RequestBody is used. }
property RequestBody: UnicodeString read FRequestBody write FRequestBody;

{ Optional binary body data for a request.
You can either use RequestBody or RequestData. If both are specified then
only RequestBody is used. }
property RequestData: TBytes read FRequestData write FRequestData;

{ Request headers }
property RequestHeaders: TStrings read FRequestHeaders;

{ Response headers from the server }
property ResponseHeaders: TStrings read FResponseHeaders;

{ Response status code }
property ResponseStatusCode: Integer read FResponseStatusCode;

{ Allow 301 and other redirects }
property FollowRedirects: Boolean read FFollowRedirects write FFollowRedirects;

{ Called when a redirect is requested }
property OnRedirect: TOnRedirect read FOnRedirect write FOnRedirect;

{ Called when a password is needed }
property OnPassword: TOnPassword read FOnPassword write FOnPassword;

{ Username and password for Basic Authentication }
property UserName: UnicodeString read FUserName write FUserName;
property Password: UnicodeString read FPassword write FPassword;

{ Content type }
property ContentType: UnicodeString read FContentType write FContentType;

{ User agent }
property UserAgent: UnicodeString read FUserAgent write FUserAgent;

{ Authorization }
property Authorization: UnicodeString read FAuthorization write FAuthorization;
end;

The full implementation of the base socket classes for Windows is contained in the repository Для просмотра ссылки Войди или Зарегистрируйся

Example application
The example Для просмотра ссылки Войди или Зарегистрируйся demonstrates how to actually make http calls using these new classes.

yxV5Re8.jpg


Scalable HTTP sockets for the cloud, Part 2

In this article we will expand on our TgoHttpClient class by adding some core new features including non-blocking http responses, unifying HTTP 1.1, HTTP/S and HTTP/2 support into a common class, incremental data transfers, various fixes and performance improvements.

Для просмотра ссылки Войди или Зарегистрируйся we discussed the scalable client socket problem and how it creates a bottleneck for services that need to communicate over TCP as a client socket. This model is common in backend services when you interact with third-party providers such as databases, remote push notification services (Apple or Google) or just about any JSON/REST based HTTP API.

If you use a typical HTTP client or component in your Windows or Linux service, you are almost always using Для просмотра ссылки Войди или Зарегистрируйся. There are numerous scale issues presented by the Winsock stack under Windows including port limitations, connection timeout retry and delay limitations, page pool issues and much more. As discussed previously, most of these limitations are worked around in Windows by using Windows IO Completion Ports (IOCP). IOCP has been available for quite some time but it is notoriously difficult to use and has been almost exclusively used for creating server applications for TCP sockets.

IOCP is well designed solution to scalable client sockets but it is rarely used as a foundation model that allows you to build client sockets for other protocols. In our previous article on Для просмотра ссылки Войди или Зарегистрируйся, we demonstrated how to build a foundation class that uses IOCP to create client socket protocols with a brief examination of the HTTP protocol.
This foundation model could easily be expanded and future client protocols and the apps that rely upon them could operate without modification on other operating systems such as Linux. Internally we have done this on Linux directly using FPC and Lazarus and hope to soon demonstrate this running for Linux on the latest Delphi compilers.
IOCP is at the heart of how Windows IIS operates in conjunction with the HTTP.SYS kernel driver and scales up efficiently. On Linux we have similar technologies such as EPOLL and KQueue and they are widely utilized in server based sockets. These technologies are also a perfect match for client based socket issues.

The TgoHttpClient class demonstrates how you can leverage IOCP and our foundation class TgoSocketConnection to create client sockets for the purpose of HTTP and take advantage of the many benefits of IOCP at the same time.


[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20]
Non-blocking HTTP
IOCP is designed to be non-blocking and handle numerous activities simultaneously. This is due to the fact that IOCP operations occur within a thread pool.

In our previous example we showed a simple blocking HTTP client. The example waited in a loop for the entire http response to arrive or a timeout to happen. Under normal circumstances this is the behavior we want for http requests. You send a request and you wait for a response. Your code is blocked while this transaction takes place.

Because IOCP operates in a thread pool your app does not necessarily have to be blocked, and activity and progress can occur during the request and response cycle. There are times when you either do not want to wait for a response or you do not care about the response. This is when non-blocking could be helpful.

To create a non-blocking http client we first construct TgoHttpClient with the Blocking parameter set to False.
1HTTP := TgoHttpClient.Create(False, False);

Because it is non-blocking, the response will arrive as a TMessage and at a later time. Therefore we cannot simply Free the TgoHttpClient immediately. Instead we use the helper class TgoHttpClientManager to handle the destruction of the class by calling TgoHttpClientManager.Release. This method does not immediately destruct the object but instead waits until the response is fully received and the TMessage is called, an error occurs or a timeout elapses.
1
2
3
4
5
6
HTTP := TgoHttpClient.Create(True, False);
try
HTTP.Get('Для просмотра ссылки Войди или Зарегистрируйся');
finally
HttpClientManager.Release(HTTP);
end;

In the above example we set the HTTP2 parameter to True because we know that the nghttp2.org web server supports the HTTP/2 protocol and we set the parameter Blocking to False. After performing the request to Get we immediately call HTTPClientManager.Release which queues the object for destruction.
It is interesting to note that under a typical Winsock model, each construction and destruction of an object that directly related to socket resources would deplete vital system resources or trigger other unwanted behavior such as TIMEWAIT on sockets, however in our model the TgoHttpClient is only a simple Delphi object with straightforward properties that do not directly depend on system resources.
To handle non-blocking responses we declare a listener for the response TMessage.
1
2
3
4
5
6
7
8
9
procedure TFormMain.HttpResponseMessageListener(const Sender: TObject;
const M: TMessage);
var
HttpResponseMessage: TgoHttpResponseMessage;
begin
HttpResponseMessage := M as TgoHttpResponseMessage;
Writeln('ResponseStatusCode = ' + HttpResponseMessage.ResponseStatusCode.ToString);
Writeln('Response = ' + HttpResponseMessage.Response);
end;

After you make a request using one of the various methods, for example:
1HTTP.Get('Для просмотра ссылки Войди или Зарегистрируйся');

The TMessage will be created and your HttpResponseMessageListener will be called. The TMessage has the following details:
1
2
3
4
5
6
7
8
9
10
{ Http response message }
TgoHttpResponseMessage = class(TMessage)
public
property HttpClient: TgoHttpClient read FHttpClient;
property ResponseHeaders: TgoHttpHeaders read FResponseHeaders;
property ResponseStatusCode: Integer read FResponseStatusCode;
property ResponseContentType: String read FResponseContentType;
property ResponseContentCharset: String read FResponseContentCharset;
property Response: TBytes read FResponse;
end;

This includes the actual Response, the ResponseStatusCode and various other details.

Incremental data transfers
One of the many benefits of IOCP and scalable sockets is that you have easy and direct access to the data as it is being sent or received over the wire.

This latest version of the TgoHttpClient adds the ability to receive data in chunks as it arrives over the wire. This methodology works the same for HTTP 1.1, HTTP/S and HTTP/2 requests as well as Content-Length and Chunked transfer encoding.

To use this new feature assign an event and define the event procedure.
1
2
HTTP := TgoHTTPClient.Create;
HTTP.OnRecv := OnRecv;
1
2
3
4
5
procedure TFormMain.OnRecv(Sender: TObject; const ABuffer: Pointer; const ASize: Integer; var ACreateResponse: Boolean);
begin
{ do not buffer the response }
ACreateResponse := False;
end;

In the above example the latest received bytes are indicated as a the pointer ABuffer with a length of ASize.

If we are receiving a large response and we do not want a complete response built in memory we can set ACreateResponse to False. This would be useful if we wanted to download a large file but we did not want it cached into memory for efficiency reasons.

HTTP/2
HTTP/2 is now becoming more widely used on web servers and in various interfaces such as Apple’s latest Push Notification service for iOS devices. This latest revision to TgoHttpClient seamlessly blends the ability to perform HTTP 1.1, HTTP/S and HTTP/2 into a single class.

In order to make HTTP/2 requests you simply need to construct the TgoHttpClient with the HTTP2 parameter set to True as follows:
1FHTTP := TgoHttpClient.Create(True);

Once enabled TgoHttpClient will only make requests using HTTP/2. Not all web servers support HTTP/2 so your mileage may vary.

To perform HTTP/2 requests we rely on the nghttp2 library. You will need the nghttp2.dll in your distribution in order to utilize HTTP/2.

nghttp2
Для просмотра ссылки Войди или Зарегистрируйся for implementing the HTTP/2 protocol. It handles all the compression and bitstreaming for implementing HTTP/2.

The Для просмотра ссылки Войди или Зарегистрируйся and its header compression are rather complex and evolving so it is probably best not to try and implement this ourselves.

One of the great things about nghttp2 is you can utilize it entirely with memory buffers so it is a good match for our own Для просмотра ссылки Войди или Зарегистрируйся for Windows and Linux. This way we get the benefit of HTTP/2 but we don’t have to rely on another implementation of sockets for scaling up our service.

Building nghttp2
If you want to build the nghttp2 library for Windows to use in your Delphi application you will need to download the Для просмотра ссылки Войди или Зарегистрируйся. To build you can use Visual Studio or just Для просмотра ссылки Войди или Зарегистрируйся. You will also need to download and install Для просмотра ссылки Войди или Зарегистрируйся.
  1. Download the latest nghttp2 source from Для просмотра ссылки Войди или Зарегистрируйся
  2. Install CMAKE and the Build Tools for Visual Studio.
  3. Run CMAKE followed by a period
    cmake .
  4. Run CMAKE to build the release version
    cmake --build . --config RELEASE
Delphi header translation
Once completed you will have a nghttp2.dll. We will need our Для просмотра ссылки Войди или Зарегистрируйся so we can use the nghttp2 methods directly.
If you do not want HTTP/2 support and nghttp2.dll you can simple comment out the reference at the top of the Grijjy.Http unit called {$DEFINE HTTP2}.
Example application
The example Для просмотра ссылки Войди или Зарегистрируйся demonstrates how to actually make blocking and non-blocking http calls using the TgoHttpClient class.

Fj3N78d.png


The full implementation of the base http class for Windows is contained in the repository Для просмотра ссылки Войди или Зарегистрируйся

[/SHOWTOGROUPS]