Netty是一個NIO的客戶端服務器框架,它使我們可以快速而簡單地開發網絡應用程序,比如協議服務器和客戶端。它大大簡化了網絡編程,比如TCP和UDP socket服務器。
“快速而簡單”並不意味着開發出來的應用可維護性或性能不好。Netty已經實現了大量的協議,比如FTP,SMTP,HTTP,以及各種基於二進制和文本的傳統協議。可以說Netty已經找到了一種方法來實現簡單的開發,高性能,穩定性,靈活性而不需要做妥協。
Netty的結構大體如下圖這樣:
就設計而言,Netty給不同的傳輸類型,不管是阻塞的還是非阻塞的,提供了統一的接口。它基於一個靈活的和可擴展的事件模型,這使得處理不同邏輯的部分可以有效的隔離開來。它具有高度可定製的線程模型 - 單線程,一個或多個線程池,比如SEDA。它還提供無連接的datagram socket支持。
如Netty這般,功能如此強大,性能如此優良的網絡庫,不用在Android上真是可惜了。這裏我們就嘗試將Netty用在Android上。
下載Netty
首先是下載Netty。Netty的官網地址,我們可以在這裏找到下載Netty的地址,當然還有許許多多的文檔。這裏使用了當前4.1.x最新版的Netty 4.1.4,下載地址:
http://dl.bintray.com/netty/downloads/netty-4.1.4.Final.tar.bz2
解壓之後,爲了省事,直接將netty-4.1.4.Final/jar/all-in-one/下的netty-all-4.1.4.Final.jar拷貝進了工程下app module的libs目錄下。
Netty的簡單使用
不出意外,直接通過編譯。接着我們就參考netty/example/src/main/java/io/netty/example/http/snoop
中client部分的代碼,將Netty用起來,這主要有如下幾個類:
package io.netty.example.http.snoop;
import java.net.URI;
import java.net.URISyntaxException;
import javax.net.ssl.SSLException;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
public class HttpClient {
public static final String TAG = "NettyClient";
public static void getResponse(final String url) {
new Thread() {
@Override
public void run() {
try {
URI uri = new URI(url);
String scheme = uri.getScheme() == null ? "http" : uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();
if (port == -1) {
if ("http".equalsIgnoreCase(scheme)) {
port = 80;
} else if ("https".equalsIgnoreCase(scheme)) {
port = 443;
}
}
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
System.err.println("Only HTTP(S) is supported.");
return;
}
final boolean ssl = "https".equalsIgnoreCase(scheme);
final SslContext sslCtx;
if (ssl) {
sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} else {
sslCtx = null;
}
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class)
.handler(new HttpClientInitializer(sslCtx));
// Make the connection attempt.
Channel channel = bootstrap.connect(host, port).sync().channel();
// Prepare the HTTP request.
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.GET, url);
request.headers().set(HttpHeaderNames.HOST, host);
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderNames.KEEP_ALIVE);
request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
// Send the HTTP request.
channel.writeAndFlush(request);
// Wait for the server to close the connection.
channel.closeFuture().sync();
} finally {
group.shutdownGracefully();
}
} catch (URISyntaxException e) {
} catch (SSLException e) {
} catch (InterruptedException e) {
}
}
}.start();
}
}
這個class提供給外部調用的接口。使用者可以傳入URL,將藉由這個類,通過Netty來訪問網絡並獲取響應。然後來看HttpClientInitializer:
package io.netty.example.http.snoop;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.ssl.SslContext;
public class HttpClientInitializer extends ChannelInitializer<SocketChannel> {
private final SslContext sslCtx;
public HttpClientInitializer(SslContext sslCtx) {
this.sslCtx = sslCtx;
}
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
// Enable HTTPS if necessary.
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
p.addLast(new HttpClientCodec());
// Remove the following line if you don't want automatic content decompression.
p.addLast(new HttpContentDecompressor());
// Uncomment the following line if you don't want to handle HttpContents.
//p.addLast(new HttpObjectAggregator(1048576));
p.addLast(new HttpClientHandler());
}
}
這個class負責對Channel的Pipeline進行初始化,這其中最關鍵的是HttpClientHandler:
package io.netty.example.http.snoop;
import android.util.Log;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.CharsetUtil;
public class HttpClientHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
if (msg instanceof HttpResponse) {
HttpResponse response = (HttpResponse) msg;
Log.i(HttpClient.TAG, "STATUS: " + response.status());
Log.i(HttpClient.TAG, "VERSION: " + response.protocolVersion());
if (!response.headers().isEmpty()) {
for (CharSequence name: response.headers().names()) {
for (CharSequence value: response.headers().getAll(name)) {
Log.i(HttpClient.TAG, "HEADER: " + name + " = " + value);
}
}
}
}
if (msg instanceof HttpContent) {
HttpContent content = (HttpContent) msg;
String responseContent = content.content().toString(CharsetUtil.UTF_8);
Log.i(HttpClient.TAG, responseContent);
if (content instanceof LastHttpContent) {
ctx.close();
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
我們正是通過實現HttpClientHandler,而獲取到Netty返回給我們的響應的。
在我們的Android應用代碼中調用HttpClient
來通過Netty從網絡獲取響應:
package io.netty.example.http.snoop;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private TextView mTextScreen;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btnGetIpInfo = (Button) findViewById(R.id.btn_get_ip_info_with_netty);
btnGetIpInfo.setOnClickListener(mBtnClickListener);
mTextScreen = (TextView) findViewById(R.id.text_screen);
}
View.OnClickListener mBtnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
String url = "http://ip.taobao.com/service/getIpInfo.php?ip=123.58.191.68";
mTextScreen.setText("To access " + url);
Log.i(TAG, "To access " + url);
if (R.id.btn_get_ip_info_with_netty == v.getId()) {
HttpClient.getResponse(url);
}
}
};
}
當然不能忘記了在AndroidManifest.xml中添加對INTERNET權限的請求:
<uses-permission android:name="android.permission.INTERNET"/>
做完了所有這些之後,Netty基本上就可以跑起來了。不過意外還是發生了:
08-10 10:13:33.670 17720-17720/io.netty.example.http.snoop6.myapplication I/MainActivity: To access http://ip.taobao.com/service/getIpInfo.php?ip=123.58.191.68
08-10 10:13:33.818 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: java.lang.NoClassDefFoundError: com.jcraft.jzlib.Inflater
08-10 10:13:33.818 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.handler.codec.compression.JZlibDecoder.<init>(JZlibDecoder.java:27)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.handler.codec.compression.ZlibCodecFactory.newZlibDecoder(ZlibCodecFactory.java:122)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.handler.codec.http.HttpContentDecompressor.newContentDecoder(HttpContentDecompressor.java:57)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.handler.codec.http.HttpContentDecoder.decode(HttpContentDecoder.java:87)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.handler.codec.http.HttpContentDecoder.decode(HttpContentDecoder.java:46)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:88)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:372)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:358)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:350)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:435)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:293)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:280)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:396)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:248)
08-10 10:13:33.826 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:250)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:372)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:358)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:350)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1334)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:372)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:358)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:926)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:129)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:571)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.nio.NioEventLoop.processSelectedKeysPlain(NioEventLoop.java:474)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:428)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:398)
08-10 10:13:33.834 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:877)
08-10 10:13:33.841 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:144)
08-10 10:13:33.841 17720-18708/io.netty.example.http.snoop6.myapplication W/System.err: at java.lang.Thread.run(Thread.java:841)
08-10 10:13:33.841 17720-18708/io.netty.example.http.snoop6.myapplication I/NettyClient: --------- beginning of /dev/log/system
08-10 10:35:02.162 17720-17720/io.netty.example.http.snoop6.myapplication I/Timeline: Timeline: Activity_idle id: android.os.BinderProxy@41c10f28 time:1282964865
有一個class com.jcraft.jzlib.Inflater
找不到。這還需要添加對jzlib的依賴:
compile 'com.jcraft:jzlib:1.1.2'
至此順利地將Netty跑起來:
{
"code":0,
"data":{
"country":"中國",
"country_id":"CN",
"area":"華東",
"area_id":"300000",
"region":"浙江省",
"region_id":"330000",
"city":"杭州市",
"city_id":"330100",
"county":"",
"county_id":"-1",
"isp":"網易網絡",
"isp_id":"1000119",
"ip":"123.58.191.68"
}
}
Netty的裁剪
Netty很強大,all-in-one jar用起來很方便,這很不錯。但all-in-one jar有點大,其中包含的一些諸如對memcache,redis,stomp,sctp和udt等的支持,我們在移動端並不會用掉。因而需要對它做一點裁剪。
既然不能用all-in-one包,那就把需要的幾個jar文件單獨copy進我們的工程好了。對於android而言,目測netty-4.1.4.Final/jar/下我們需要拷貝的jar文件主要有下面這些:
netty-codec-4.1.4.Final.jar
netty-codec-http-4.1.4.Final.jar
netty-codec-http2-4.1.4.Final.jar
netty-codec-socks-4.1.4.Final.jar
netty-common-4.1.4.Final.jar
netty-handler-4.1.4.Final.jar
netty-transport-4.1.4.Final.jar
用這些文件來替換之前的netty-all-4.1.4.Final.jar。遇到了編譯錯誤:
:app:compileDebugJavaWithJavac
Full recompilation is required because at least one of the classes of removed jar 'netty-all-4.1.4.Final.jar' requires it. Analysis took 0.364 secs.
/media/data/MyProjects/MyApplication/app/src/main/java/io/netty/example/http/snoop/myapplication/HttpClient.java:67: 錯誤: 無法訪問ByteBufHolder
request.headers().set(HttpHeaderNames.HOST, host);
^
找不到io.netty.buffer.ByteBufHolder的類文件
/media/data/MyProjects/MyApplication/app/src/main/java/io/netty/example/http/snoop/myapplication/HttpClientInitializer.java:39: 錯誤: 無法訪問ByteBufAllocator
p.addLast(sslCtx.newHandler(ch.alloc()));
^
找不到io.netty.buffer.ByteBufAllocator的類文件
/media/data/MyProjects/MyApplication/app/src/main/java/io/netty/example/http/snoop/myapplication/HttpClientHandler.java:48: 錯誤: 找不到符號
String responseContent = content.content().toString(CharsetUtil.UTF_8);
^
符號: 方法 content()
位置: 類型爲HttpContent的變量 content
注: /media/data/MyProjects/MyApplication/app/src/main/java/io/netty/example/http/snoop/myapplication/HttpClient.java使用或覆蓋了已過時的 API。
注: 有關詳細信息, 請使用 -Xlint:deprecation 重新編譯。
3 個錯誤
:app:compileDebugJavaWithJavac FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:compileDebugJavaWithJavac'.
> Compilation failed; see the compiler error output for details.
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
BUILD FAILED
看來是少了一些東西了,ByteBufHolder
。那就把netty-buffer-4.1.4.Final.jar
也加進工程裏。再次編譯,繼續出錯,這是在產生APK時遇到了麻煩:
:app:transformResourcesWithMergeJavaResForDebug FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:transformResourcesWithMergeJavaResForDebug'.
> com.android.build.api.transform.TransformException: com.android.builder.packaging.DuplicateFileException: Duplicate files copied in APK META-INF/INDEX.LIST
File1: /media/data/MyProjects/MyApplication/app/libs/netty-codec-4.1.4.Final.jar
File2: /media/data/MyProjects/MyApplication/app/libs/netty-transport-4.1.4.Final.jar
File3: /media/data/MyProjects/MyApplication/app/libs/netty-buffer-4.1.4.Final.jar
File4: /media/data/MyProjects/MyApplication/app/libs/netty-codec-socks-4.1.4.Final.jar
File5: /media/data/MyProjects/MyApplication/app/libs/netty-handler-4.1.4.Final.jar
File6: /media/data/MyProjects/MyApplication/app/libs/netty-codec-http2-4.1.4.Final.jar
File7: /media/data/MyProjects/MyApplication/app/libs/netty-codec-http-4.1.4.Final.jar
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
BUILD FAILED
Total time: 15.352 secs
Duplicate files copied in APK META-INF/INDEX.LIST
File1: /media/data/MyProjects/MyApplication/app/libs/netty-codec-4.1.4.Final.jar
File2: /media/data/MyProjects/MyApplication/app/libs/netty-transport-4.1.4.Final.jar
File3: /media/data/MyProjects/MyApplication/app/libs/netty-buffer-4.1.4.Final.jar
File4: /media/data/MyProjects/MyApplication/app/libs/netty-codec-socks-4.1.4.Final.jar
File5: /media/data/MyProjects/MyApplication/app/libs/netty-handler-4.1.4.Final.jar
File6: /media/data/MyProjects/MyApplication/app/libs/netty-codec-http2-4.1.4.Final.jar
File7: /media/data/MyProjects/MyApplication/app/libs/netty-codec-http-4.1.4.Final.jar
11:03:39: External task execution finished 'assembleDebug'.
總是報Duplicate files copied in APK META-INF/INDEX.LIST
的錯誤。這主要是因爲這些jar文件裏,不同的jar文件中的META-INF/INDEX.LIST
包含了相同的內容所致。這需要在build.gradle
中的android元素裏添加如下的配置:
packagingOptions {
exclude 'META-INF/INDEX.LIST'
}
再次編譯,這次則是包Duplicate files copied in APK META-INF/io.netty.versions.properties
。這證明了前面的方法是行之有效的,於是把META-INF/io.netty.versions.properties
也加進packagingOptions
的exclude列表:
packagingOptions {
exclude 'META-INF/INDEX.LIST'
exclude 'META-INF/io.netty.versions.properties'
}
再次編譯,編譯通過。但是在運行的時候遇到了一點小麻煩:
11:29:59.685 9005-9050/com.example.hanpfei0306.myapplication W/dalvikvm: threadid=11: thread exiting with uncaught exception (group=0x41959ce0)
08-11 11:29:59.685 9005-9050/com.example.hanpfei0306.myapplication E/AndroidRuntime: FATAL EXCEPTION: Thread-1197
Process: com.example.hanpfei0306.myapplication, PID: 9005
java.lang.NoClassDefFoundError: io.netty.resolver.DefaultAddressResolverGroup
at io.netty.bootstrap.Bootstrap.<clinit>(Bootstrap.java:53)
at com.example.hanpfei0306.myapplication.HttpClient$1.run(HttpClient.java:57)
提示找不到class io.netty.resolver.DefaultAddressResolverGroup,看來我們的jar文件是加少了,把netty-resolver-4.1.4.Final.jar
也加進來。終於,我們前面編寫的HttpClient能夠正常地跑起來了。經過一番裁剪,Netty的大小大概從3.4MB減小到2.9MB。
總結一下,若不用體型巨大的netty-all
jar文件,則我們需要導入如下的這些jar文件以編譯和運行Netty:
netty-buffer-4.1.4.Final.jar
netty-codec-4.1.4.Final.jar
netty-codec-http-4.1.4.Final.jar
netty-codec-http2-4.1.4.Final.jar
netty-codec-socks-4.1.4.Final.jar
netty-common-4.1.4.Final.jar
netty-handler-4.1.4.Final.jar
netty-resolver-4.1.4.Final.jar
netty-transport-4.1.4.Final.jar
TODO
Netty的諸多抽象,比如Bootstrap,Channel,EventLoopGroup,Handler,codec,Buffer等諸多高級特性,以及它的NIO接口,這裏都沒有涉及,線程模型也沒有仔細釐清。這裏只是最最簡單的一個使用範例,要把Netty很好地應用在實際的項目中,還需要對Netty本身更深入的研究。
同時,對Netty的裁剪可能也過於粗糙,或許還有更多的東西可以裁剪掉,以減小最終的APP的大小。
要使用Netty來支持HTTP/2也還需要做更多的事情。
但窺探到Netty的靈活強大,還是讓我們對這個庫充滿期待。