Commit Graph

6654 Commits

Author SHA1 Message Date
Jakob Buchgraber
e3980a6487 Http2ConnectionHandler should propagate channelActive and channelInactive events.
Motivation:

The Http2ConnectionHandler incorrectly doesn't propagate channelActive and channelInactive events and thus breaks the pipeline
for other ChannelHandler.

Modification:

- Add calls to super.channelActive() and super.channelInactive().
- Remove unused methods.

Result:

- Http2ConnectionHandler can be used with other ChannelHandlers.
2015-04-28 20:11:12 -07:00
Norman Maurer
14e1d0f9f7 [#3662] Fail the connect future on close
Motivation:

Because of a bug we missed to fail the connect future when doClose() is called. This can lead to a future which is never notified and so may lead to deadlocks in user-programs.

Modifications:

Correctly fail the connect future when doClose() is called and the connection was not established yet.

Result:

Connect future is always notified.
2015-04-27 20:05:26 +02:00
Norman Maurer
18ace807b8 [#3680] Enabled SecurityManager results in ClassNotFoundError during io.netty.util.NetUtil initialization
Motivation:

When a SecurityManager is in place that preven reading the somaxconn file trying to bootstrap a channel later will result in a ClassNotFoundError.

Modifications:

- Reading the file in a privileged block.

Result:

No more ClassNotFoundError when a SecurityManager is in place.
2015-04-27 20:02:16 +02:00
Scott Mitchell
4baea3ea7a ByteString arrayOffset master branch cleanup
Motivation:
Commit f1e122a introduced the arrayOffset method into the ByteString class. That means anywhere that used the array() method must also use the arrayOffset() method.

Modifications:
- Find all uses of ByteString.array() and ensure they are taking into account ByteString.arrayOffset().

Result:
The correct data from ByteString.array() will be used.
2015-04-25 14:24:39 -07:00
Scott Mitchell
f1e122a0c1 ByteString arrayOffset method
Motivation:
The ByteString class currently assumes the underlying array will be a complete representation of data. This is limiting as it does not allow a subsection of another array to be used. The forces copy operations to take place to compensate for the lack of API support.

Modifications:
- add arrayOffset method to ByteString
- modify all ByteString and AsciiString methods that loop over or index into the underlying array to use this offset
- update all code that uses ByteString.array to ensure it accounts for the offset
- add unit tests to test the implementation respects the offset

Result:
ByteString and AsciiString can represent a sub region of a byte[].
2015-04-24 18:54:19 -07:00
nmittler
c98195714d Optimizing user-defined stream properties.
Motivation:

Streams currently maintain a hash map of user-defined properties, which has been shown to add significant memory overhead as well as being a performance bottleneck for lookup of frequently used properties.

Modifications:

Modifying the connection/stream to use an array as the storage of user-defined properties, indexed by the class that identifies the index into the array where the property is stored.

Result:

Stream processing performance should be improved.
2015-04-23 12:40:51 -07:00
Scott Mitchell
69558999eb Compile error introduced in ee9233d
Motivation:
Commit ee9233d introduced a compile error in microbench.

Modifications:
Fix compile error.

Result:
Code now builds.
2015-04-22 16:23:59 -07:00
Scott Mitchell
9d66180290 HTTP/2 Flow Controller required memory reduction
Motivation:
Currently we allocate the full amount of state for each stream as soon as the stream is created, and keep that state until the stream is GC. The full set of state is only needed when the stream can support flow controlled frames. There is an opportunity to reduce the required amount of memory, and make memory eligible for GC sooner by only allocating what is necessary for flow control stream state.

Modifications:

Introduce objects which require 'less' state for local/remote flow control stream state.
Use these new objects when streams have been created but will not transition out of idle AND when streams are no longer eligible for flow controlled frame transfer but still must persist in the priority tree.
Result:
Memory allocations are reduced to what is actually needed, and memory is made eligible for GC potentially sooner.
2015-04-22 14:40:45 -07:00
Norman Maurer
a4e9ba5472 [#3652] Improve performance of StringUtil.simpleClassName()
Motivation:

static Package getPackage(Class<?> c) uses synchronized block internally.
Thanks to @jingene for the hint and initial report of the issue.

Modifications:

-Use simple lastIndexOf(...) and substring for a faster implementation

Result:

No more lock condition.
2015-04-22 09:15:16 +02:00
nmittler
ec6cd54f85 Ignore frames for streams that may have previously existed.
Motivation:

The recent PR that discarded the Http2StreamRemovalPolicy causes connection errors when receiving a frame for a stream that no longer exists. We should ignore these frames if we think there's a chance that the stream has existed previously

Modifications:

Modified the Http2Connection interface to provide a `streamMayHaveExisted` method. Also removed the requireStream() method to identify all of the places in the code that need to be updated.

Modified the encoder and decoder to properly handle cases where a stream may have existed but no longer does.

Result:

Fixes #3643
2015-04-21 20:46:31 -07:00
nmittler
ed99766c26 Always consume bytes for closed HTTP/2 streams.
Motivation:

The current local flow controller does not guarantee that unconsumed bytes for a closed stream will be restored to the connection window.  This may lead to degradation of the connection window over time.

Modifications:

Modified DefaultHttp2LocalFlowController to guarantee that any unconsumed bytes are returned to the connection window as soon as the stream is closed. We also immediately consume any bytes when receiving DATA for a closed stream.

Result:

Fixes #3668
2015-04-21 12:33:24 -07:00
Alwayswithme
4b13a7c729 fix the discardedBytes counting on LineBasedFrameDecoder
Motivation:

The LineBasedFrameDecoder discardedBytes counting different compare to
DelimiterBasedFrameDecoder.

Modifications:

Add plus sign

Result:

DiscardedBytes counting correctly
2015-04-21 09:13:54 +02:00
Scott Mitchell
477571f7e5 HTTP/2 Flow Controller interface updates
Motivation:
Flow control is a required part of the HTTP/2 specification but it is currently structured more like an optional item. It must be accessed through the property map which is time consuming and does not represent its required nature. This access pattern does not give any insight into flow control outside of the codec (or flow controller implementation).

Modifications:
1. Create a read only public interface for LocalFlowState and RemoteFlowState.
2. Add a LocalFlowState localFlowState(); and RemoteFlowState remoteFlowState(); to Http2Stream.

Result:
Flow control is not part of the Http2Stream interface. This clarifies its responsibility and logical relationship to other interfaces. The flow controller no longer must be acquired though a map lookup.
2015-04-20 20:02:39 -07:00
Eric Anderson
aa9d73c6b2 Fix SslContextBuilder swapping client and server
The 'forClient' boolean was swapped to 'forServer' in code review of #3531.
Not all locations were updated.
2015-04-20 17:25:39 -07:00
Norman Maurer
43d57ddd46 [#3623] CompositeByteBuf.iterator() should return optimized Iterable
Motivation:

CompositeByteBuf.iterator() currently creates a new ArrayList and fill it with the ByteBufs, which is more expensive then it needs to be.

Modifications:

- Use special Iterator implementation

Result:

Less overhead when calling iterator()
2015-04-20 10:45:43 +02:00
Norman Maurer
52878880b4 Fix handling of non-auto read for ByteToMessageDecoder and SslHandler
Motivation:

Our automatically handling of non-auto-read failed because it not detected the need of calling read again by itself if nothing was decoded. Beside this handling of non-auto-read never worked for SslHandler as it always triggered a read even if it decoded a message and auto-read was false.

This fixes [#3529] and [#3587].

Modifications:

- Implement handling of calling read when nothing was decoded (with non-auto-read) to ByteToMessageDecoder again
- Correctly respect non-auto-read by SslHandler

Result:

No more stales and correctly respecting of non-auto-read by SslHandler.
2015-04-20 10:38:13 +02:00
Norman Maurer
71838342ce Correctly test for non-auto-read correctness in testsuite
Motiviation:

Our tests for non-auto-read did actually not test this correctly as auto-read was never disabled on the Bootstrap and ServerBootstrap.

Modifications:

- Correctly disable auto-read on Bootstrap and ServerBootstrap
- Fix tests to call ChannelHandlerContext.read() once a Channel becomes active.

Result:

Correctly test that non-auto-read works.
2015-04-20 10:15:41 +02:00
Norman Maurer
c0b1ab7a68 Revert "Ensure channelReadComplete() is called only when necessary"
This reverts commit 14d64d0966.
2015-04-20 10:13:44 +02:00
Norman Maurer
9da310dbcd Revert "Do not suppress channelReadComplete() when a handler was just added"
This reverts commit c4483c25e4.
2015-04-20 10:13:36 +02:00
Norman Maurer
55afb90b80 Revert "Add another test case for channelReadComplete() suppression"
This reverts commit 8f4ead0ff3.
2015-04-20 10:13:15 +02:00
Norman Maurer
b63fe25774 Reduce object allocation during wrap/unwrap while handshake is in progress
Motivation:

Unnecessary object allocation is currently done during wrap/unwrap while a handshake is still in progress.

Modifications:

Use static instances when possible.

Result:

Less object creations.
2015-04-20 06:48:35 +02:00
Norman Maurer
dad6e15ffe Allow to get version of available OpenSSL library
Motivation:

Sometimes it's useful to get informations about the available OpenSSL library that is used for the OpenSslEngine.

Modifications:

Add two new methods which allows to get the available OpenSSL version as either
an int or an String.

Result:

Easy to access details about OpenSSL version.
2015-04-18 20:56:37 +02:00
Scott Mitchell
392cdc1c61 HTTP/2 Priority Tree Benchmark
Motivation:
There is no benchmark to measure the priority tree implementation performance.

Modifications:
Introduce a new benchmark which will populate the priority tree, and then shuffle parent/child links around.

Result:
A simple benchmark to get a baseline for the HTTP/2 codec's priority tree implementation.
2015-04-17 10:14:50 -07:00
Louis Ryan
b05c6476f1 Have microbenchmarks produce a deployable artifact. Fix some minor miscellaneous issues.
Motivation:
Allows for running benchmarks from built jars which is useful in development environments that only take released artifacts.

Modifications:
Move benchmarks into 'main' from 'test'
Add @State annotations to benchmarks that are missing them
Fix timing issue grabbing context during channel initialization

Result:
Users can run benchmarks more easily.
2015-04-17 10:03:39 -07:00
Trustin Lee
7b7cf29eef Fix checkstyle 2015-04-17 11:39:57 +09:00
Jakob Buchgraber
52e05b3224 Improve performance of AsciiString.equals(Object).
Motivation:

The current implementation does byte by byte comparison, which we have seen
can be a performance bottleneck when the AsciiString is used as the key in
a Map.

Modifications:

Use sun.misc.Unsafe (on supporting platforms) to compare up to eight bytes at a time
and get closer to the performance of String.equals(Object).

Result:

Significant improvement (2x - 6x) in performance over the current implementation.

Benchmark                                             (size)   Mode   Samples        Score  Score error    Units
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual       10  thrpt        10 118843477.518 2347259.347    ops/s
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual       50  thrpt        10 43910319.773   198376.996    ops/s
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual      100  thrpt        10 26339969.001   159599.252    ops/s
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual     1000  thrpt        10  2873119.030    20779.056    ops/s
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual    10000  thrpt        10   306370.450     1933.303    ops/s
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual   100000  thrpt        10    25750.415      108.391    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual       10  thrpt        10 248077563.510  635320.093    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual       50  thrpt        10 128198943.138  614827.548    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual      100  thrpt        10 86195621.349  1063959.307    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual     1000  thrpt        10 16920264.598    61615.365    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual    10000  thrpt        10  1687454.747     6367.602    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual   100000  thrpt        10   153717.851      586.916    ops/s
2015-04-16 17:29:08 -07:00
Roger Kapsi
afadb98d9d Fix for ByteString#hashCode()
Motivation:

ByteString#hashCode() trashes its own hash code if it's being accessed concurrently

Modifications:

Pull the ByteString#hash into a local variable and calculate it locally.

Result:

ByteString#hashCode() is no longer returning a junk value.
2015-04-16 17:01:51 -07:00
nmittler
7e8ab2118d Optimizing KObjectHashMap hashIndex()
Motivation:

The IntObjectHashMap benchmarks show the Agrona collections to be faster on put, lookup, and remove. One major difference is that we're using 2 modulus operations each time we increment the position index while iterating.  Agrona uses a mask instead.

Modifications:

Modified the KObjectHashMap to use masking rather than modulus when wrapping the position index. This requires that the capacity be a power of 2.

Result:

Improved performance of IntObjectHashMap.
2015-04-16 10:26:50 -07:00
Eric Anderson
772482559d The "null" ClassLoader is the bootstrap ClassLoader
Motivation:
Class.forName() documents that null will use bootstrap loader:
http://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#forName-java.lang.String-boolean-java.lang.ClassLoader-

But the link between "null" and bootstrap loader is even more explicit
in ClassLoader's documentation:
http://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html#getParent--

The current code is trying to use the bootstrap loader but seems to have
not been aware of the meaning of null.

Modifications:
Use "null" as the class loader when we want to load classes in the
bootstrap loader.

Result:
More reliable ALPN/NPN loading and simpler code.
2015-04-16 17:28:49 +02:00
Derek Troy-West
8796fd3025 Change AggregatedFullHttpMessage to contain a content ByteBuf
Motivation:

Other implementations of FullHttpMessage allow .toString to be called after the Message has been released
This brings AggregatedFullHttpMessage into line with those impls.

Modifications:

- Changed AggregatedFullHttpMessage to no longer be a sub-class of DefaultByteBufHolder
- Changes AggregatedFullHttpMessage to implement ByteBufHolder
- Hold the content buffer internally to AggregatedFullHttpMessage
- Implement the required content() and release() methods that were missing
- Do not check refcnt when accessing content() (similar to DefaultFullHttpMessage)

Result:

A released AggregatedFullHttpMessage can have .toString called without throwing an exception
2015-04-16 17:28:39 +02:00
Scott Mitchell
d17fcd7805 HTTP/2 Priority tree circular link
Motivation:
If an exclusive dependency change stream B should be an exclusive dependency of stream A is requested and stream B is already a child of stream A...then we will add B to B's own children map and create a circular link in the priority tree. This leads to an infinite recursive loop and a stack overflow exception.

Modifications:
-when removeAllChildren is called it should not remove the exclusive dependency.
-unit test to ensure this case is covered.

Result:
No more circular link in the priority tree.
2015-04-15 14:26:29 -07:00
Scott Mitchell
485c17d911 Commit b823bfa missed an include change
Motivation:
b823bfa introduced a compile error for the microbench package.

Modifications:
change AsciiString import to new package.

Result:
No more compile error.
2015-04-15 10:50:04 -07:00
Scott Mitchell
b823bfa950 ByteString introduced as AsciiString super class
Motivation:
The usage and code within AsciiString has exceeded the original design scope for this class. Its usage as a binary string is confusing and on the verge of violating interface assumptions in some spots.

Modifications:
- ByteString will be created as a base class to AsciiString. All of the generic byte handling processing will live in ByteString and all the special character encoding will live in AsciiString.

Results:
The AsciiString interface will be clarified. Users of AsciiString can now be clear of the limitations the class imposes while users of the ByteString class don't have to live with those limitations.
2015-04-15 10:45:18 -07:00
Norman Maurer
f58fd3f8c7 Document the contract of Attribute.getAndSet(...) and set(...)
Motivation:

Attribute.getAndRemove() will return the value but also remove the AttributeKey itself from the AttributeMap. This may not
what you want as you may want to keep an instance of it and just set it later again. Document the contract so the user know what to expect.

Modifications:

- Make it clear when to use AttributeKey.getAndRemove() / AttributeKey.remove() and when AttributeKey.getAndSet(null) / AttributeKey.set(null).

Result:

Less suprising behaviour.
2015-04-14 09:54:01 +02:00
Norman Maurer
216d5ee583 Add support for EC Keys when using SslServerContext
Motivation:

Sometimes it's useful to use EC keys and not DSA or RSA. We should support it.

Modifications:

Support EC keys and share the code between JDK and Openssl impl.

Result:

It's possible to use EC keys now.
2015-04-14 08:45:46 +02:00
Eric Anderson
4d56028df5 [#3531] Create SslContext.Builder
Motivation:

SslContext factory methods have gotten out of control; it's past time to
swap to a builder.

Modifications:

New Builder class. The existing factory methods must be left as-is for
backward compatibility.

Result:

Fixes #3531
2015-04-14 07:50:58 +02:00
Norman Maurer
acb6902f68 [#3539] Correctly handle EPOLLRDHUP
Motivation:

As we missed to correctly handle EPOLLRDHUP we produce an IOException which is unnessary. This leads
to have exceptionCaught(...) methods called.

Modifications:

When EPOLLRDHUP was received just close the socket and fail all pending writes.

Result:

Correctly handle of EPOLLRDHUP and so not miss-leading exceptions.
2015-04-14 06:57:25 +02:00
nmittler
a5eb2e66d2 Adding basic benchmarks for IntObjectHashMap
Motivation:

It needs to be fast :)

Modifications:

Added a simple benchmark to the microbench module.

Result:

Yay, benchmarks!
2015-04-13 12:23:44 -07:00
nmittler
c6cfb683f5 Removing Http2StreamRemovalPolicy
Motivation:

Due to a recent flurry of cleanup and fixes, we no longer need the stream removal policy to protect against recently removed streams. We should get rid of it.

Modifications:

Removed Http2StreamRemovalPolicy and everywhere it's used.

Result:

Fixes #3448
2015-04-13 12:18:29 -07:00
Scott Mitchell
d556269810 HTTP/2 Frame Writer Microbenchmark Fix
Motivation:
The Http2FrameWriterBenchmark JMH harness class name was not updated for the JVM arguments. The number of forks is 0 which means the JHM will share a JVM with the benchmarks.  Sharing the JVM may lead to less reliable benchmarking results and as doesn't allow for the command line arguments to be applied for each benchmark.

Modifications:
- Update the JMH version from 0.9 to 1.7.1.  Benchmarks wouldn't run on old version.
- Increase the number of forks from 0 to 1.
- Remove allocation of environment from static and cleanup AfterClass to using the Setup and Teardown methods. The forked JVM would not shut down correctly otherwise (and wait for 30+ seconds before timeing out).

Result:
Benchmarks that run as intended.
2015-04-13 11:00:15 -07:00
Scott Mitchell
9c4af6af7e HTTP/2 Connection Listener Unchecked Exceptions
Motivation:
The DefaultHttp2Connection is not checking for RuntimeExceptions when invoking Http2Connection.Listener methods. This is a problem for a few reasons: 1. The state of DefaultHttp2Connection will be corrupted if a listener throws a RuntimeException. 2. If the first listener throws then no other listeners will be notified, which may further corrupt state that is updated as a result of listeners being notified.

Modifications:
- Document that RuntimeExceptions are not supported for Http2Connection.Listener methods, and will be logged as an error.
- Update DefaultHttp2Connection to handle and exception for each listener that is notified, and be sure that 1 listener throwing an exception does not prevent others from being notified.

Result:
More robust DefaultHttp2Connection.
2015-04-13 08:57:08 -07:00
garywu
d14afe88a4 [#2925] Bug fix for NormalMemoryRegionCache overbooked for PoolThreadCache
Motivation:

When create NormalMemoryRegionCache for PoolThreadCache, we overbooked
cache array size. This means unnecessary overhead for thread local cache
as we will create multi cache enties for each element in cache array.

Modifications:

change:
int arraySize = Math.max(1, max / area.pageSize);
to:
int arraySize = Math.max(1, log2(max / area.pageSize) + 1);

Result:

Now arraySize won't introduce unnecessary overhead.

 Changes to be committed:
	modified:   buffer/src/main/java/io/netty/buffer/PoolThreadCache.java
2015-04-13 09:02:21 +02:00
Norman Maurer
418c81542b Let CompositeByteBuf implement Iterable
Motivation:

CompositeByteBuf has an iterator() method but fails to implement Iterable

Modifications:

Let CompositeByteBuf implement Iterable<ByteBuf>

Result:

Easier usage
2015-04-12 13:38:20 +02:00
Norman Maurer
5dae3475f2 [#3373] Rename class to match naming scheme
Motivation:

The ReplayingDecoderBuffer does not match the naming scheme we use for ByteBuf types.

Modifications:

Rename to ReplayingDecoderByteBuf to match naming scheme

Result:

Consistent naming
2015-04-12 13:36:04 +02:00
Norman Maurer
8ac902be52 Revert "Dereference when calling PooledByteBuf.deallocate()"
This reverts commit 326d57f0c3.
2015-04-11 06:45:03 +02:00
Norman Maurer
326d57f0c3 Dereference when calling PooledByteBuf.deallocate()
Motivation:

We missed to dereference the chunk and tmpNioBuf when calling deallocate(). This means the GC can not collect these as we still hold a reference while have the PooledByteBuf in the recycler stack.

Modifications:

Dereference chunk and tmpNioBuf.

Result:

GC can collect things.
2015-04-10 21:47:34 +02:00
Norman Maurer
675b5641f4 [#3592] Flush when writing HttpChunkedInput
Motivation:

We missed to flush the channel when using HttpChunkedInput (this is done when using SSL). This will result in a stale.

Modifications:

Replace ctx.write(...) with ctx.writeAndFlush(...)

Result:

Correctly working example.
2015-04-10 21:18:51 +02:00
Norman Maurer
5519bbcb95 Change PoolThreadCache to use LIFO for better cache performance
Motiviation:

At the moment we use FIFO for the PoolThreadCache which is sub-optimal as this may reduce the changes to have the cached memory actual still in the cpu-cache.

Modification:

- Change to use LIFO as this increase the chance to be able to serve buffers from the cpu-cache

Results:

Faster allocation out of the ThreadLocal cache.

Before the commit:
[xxx wrk]$ ./wrk -H 'Connection: keep-alive' -d 120 -c 256 -t 16 -s scripts/pipeline-many.lua  http://xxx:8080/plaintext
Running 2m test @ http://xxx:8080/plaintext
  16 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    14.69ms   10.06ms 131.43ms   80.10%
    Req/Sec   283.89k    40.37k  433.69k    66.81%
  533859742 requests in 2.00m, 72.09GB read
Requests/sec: 4449510.51
Transfer/sec:    615.29MB

After the commit:
[xxx wrk]$ ./wrk -H 'Connection: keep-alive' -d 120 -c 256 -t 16 -s scripts/pipeline-many.lua  http://xxx:8080/plaintext
Running 2m test @ http://xxx:8080/plaintext
  16 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    16.38ms   26.32ms 734.06ms   97.38%
    Req/Sec   283.86k    39.31k  361.69k    83.38%
  540836511 requests in 2.00m, 73.04GB read
Requests/sec: 4508150.18
Transfer/sec:    623.40MB
2015-04-10 20:58:12 +02:00
nmittler
044e1eca69 Change Http2Settings to use char keys.
Motivation:

Now that we have a CharObjectHashMap, we should change Http2Settings to use it.

Modifications:

Changed Http2Settings to extend CharObjectHashMap rather than IntObjectHashMap.

Result:

Http2Settings uses less memory to store keys.
2015-04-10 11:49:46 -07:00
Norman Maurer
d1a9a08f36 Add support for ALPN when using openssl + NPN client mode and support for CipherSuiteFilter
Motivation:

To support HTTP2 we need APLN support. This was not provided before when using OpenSslEngine, so SSLEngine (JDK one) was the only bet.
Beside this CipherSuiteFilter was not supported

Modifications:

- Upgrade netty-tcnative and make use of new features to support ALPN and NPN in server and client mode.
- Guard against segfaults after the ssl pointer is freed
- support correctly different failure behaviours
- add support for CipherSuiteFilter

Result:

Be able to use OpenSslEngine for ALPN / NPN for server and client.
2015-04-10 18:34:09 +02:00