Netty简介
Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
文件服务
文件服务基于Netty框架实现,关于Netty,可以了解:https://netty.io/。
class BootStrapServer {
private lateinit var bossGroup: EventLoopGroup
private lateinit var workerGroup: EventLoopGroup
fun startServer(httpFileConfig: HttpFileServerConfig) {
LogTool.i("BootStrapServer->startServer")
bossGroup = NioEventLoopGroup()
workerGroup = NioEventLoopGroup()
try {
val b = ServerBootstrap()
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel::class.java)
.handler(LoggingHandler(LogLevel.INFO))
.childHandler(HttpServerInitializer(httpFileConfig))
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
val ch: Channel = b.bind(httpFileConfig.serverPort).sync().channel()
LogTool.i("服务成功启动,请打开http://127.0.0.1:${httpFileConfig.serverPort}")
ch.closeFuture().sync()
serverStarted = true
} catch (e: InterruptedException) {
e.printStackTrace()
} finally {
LogTool.i("BootStrapServer->finally")
stopServer()
}
}
fun stopServer() {
if (!serverStarted) {
LogTool.e("服务未启动")
return
}
bossGroup.shutdownGracefully()
workerGroup.shutdownGracefully()
serverStarted = false
}
companion object {
val bootStrapServer = BootStrapServer()
var serverStarted = false
}
}
在Netty中,不同的请求使用不同的Handler进行处理。在这里,我们通过HttpServerInitializer
进行Handler绑定。
.childHandler(HttpServerInitializer(httpFileConfig))
class HttpServerInitializer(private val httpFileConfig: HttpFileServerConfig) :
ChannelInitializer<SocketChannel>() {
override fun initChannel(socketChannel: SocketChannel?) {
// 将请求和应答消息编码或解码为HTTP消息
socketChannel?.apply {
pipeline().addLast(HttpServerCodec())
pipeline()
.addLast(HttpObjectAggregator(65536))
pipeline().addLast(ChunkedWriteHandler())
pipeline().addLast("httpAggregator", HttpObjectAggregator(512 * 1024));
pipeline().addLast("explore-file-static-handler", HttpStaticFileServerHandler(httpFileConfig))
}
}
}
除基本设置信息外,在pipeline
中添加的HttpStaticFileServerHandler
用来处理文件请求。
class HttpStaticFileServerHandler internal constructor(config: HttpFileServerConfig) :
SimpleChannelInboundHandler<FullHttpRequest?>() {
private val httpConfig: HttpFileServerConfig = config
override fun channelRead0(ctx: ChannelHandlerContext?, request: FullHttpRequest?) {
if (ctx == null || request == null) {
LogTool.e("ctx or request is null.")
return
}
if (!request.decoderResult().isSuccess) {
sendError(ctx, BAD_REQUEST)
return
}
if (request.method() !== GET) {
sendError(ctx, METHOD_NOT_ALLOWED)
return
}
val uri = request.uri()
val path = sanitizeUri(uri)
val file = File(path)
if (!file.exists()) {
sendError(ctx, NOT_FOUND)
return
}
if (file.isDirectory) {
if (uri.endsWith("/")) {
sendFileListing(ctx, file, uri)
} else {
sendRedirect(ctx, "$uri/")
}
return
}
if (!file.isFile) {
sendError(ctx, FORBIDDEN)
return
}
val raf: RandomAccessFile
try {
raf = RandomAccessFile(file, "r")
} catch (ignore: FileNotFoundException) {
sendError(ctx, NOT_FOUND)
return
}
val fileLength = raf.length()
val response: HttpResponse = DefaultHttpResponse(HTTP_1_1, OK)
HttpUtil.setContentLength(response, fileLength)
setContentTypeHeader(response, file)
response.headers().set(
HttpHeaderNames.CONTENT_DISPOSITION,
String.format("filename=%s", URLEncoder.encode(file.name, "UTF-8"))
)
if (HttpUtil.isKeepAlive(request)) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
}
ctx.write(response)
val sendFileFuture =
ctx.write(DefaultFileRegion(raf.channel, 0, fileLength), ctx.newProgressivePromise())
val lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
sendFileFuture.addListener(object : ChannelProgressiveFutureListener {
override fun operationProgressed(
future: ChannelProgressiveFuture,
progress: Long,
total: Long
) {
// Handle process.
}
override fun operationComplete(future: ChannelProgressiveFuture) {
LogTool.i(future.channel().toString() + " 传输完成.")
}
})
if (!HttpUtil.isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE)
}
}
@Deprecated("Deprecated in Java")
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
cause.printStackTrace()
if (ctx.channel().isActive) {
sendError(ctx, INTERNAL_SERVER_ERROR)
}
}
private fun sanitizeUri(uri: String): String {
var fileUri = uri
try {
fileUri = URLDecoder.decode(fileUri, "UTF-8")
} catch (e: UnsupportedEncodingException) {
throw Error(e)
}
if (fileUri.isEmpty() || fileUri[0] != '/') {
return httpConfig.fileDirectory
}
// Convert to absolute path.
return getPlatform().getPlatformDefaultRoot() + fileUri
}
private fun sendFileListing(ctx: ChannelHandlerContext, dir: File, dirPath: String) {
val response: FullHttpResponse = DefaultFullHttpResponse(HTTP_1_1, OK)
response.headers()[HttpHeaderNames.CONTENT_TYPE] = "text/html; charset=UTF-8"
val buf = StringBuilder()
.append("<!DOCTYPE html>\r\n")
.append("<html><head><meta charset='utf-8' /><title>")
.append("Listing of: ")
.append(dirPath)
.append("</title></head><body>\r\n")
.append("<h3>Listing of: ")
.append(dirPath)
.append("</h3>\r\n")
.append("<ul>")
.append("<li><a href=\"../\">..</a></li>\r\n")
for (f in dir.listFiles()!!) {
if (f.isHidden || !f.canRead()) {
continue
}
val name = f.name
buf.append("<li><a href=\"")
.append(name)
.append("\">")
.append(name)
.append("</a></li>\r\n")
}
buf.append("</ul></body></html>\r\n")
val buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8)
response.content().writeBytes(buffer)
buffer.release()
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE)
}
private fun sendRedirect(ctx: ChannelHandlerContext, newUri: String) {
val response: FullHttpResponse = DefaultFullHttpResponse(HTTP_1_1, FOUND)
response.headers()[HttpHeaderNames.LOCATION] = newUri
// Close the connection as soon as the error message is sent.
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE)
}
private fun sendError(ctx: ChannelHandlerContext, status: HttpResponseStatus) {
val response: FullHttpResponse = DefaultFullHttpResponse(
HTTP_1_1, status, Unpooled.copiedBuffer("Failure: $status\r\n", CharsetUtil.UTF_8)
)
response.headers()[HttpHeaderNames.CONTENT_TYPE] = "text/plain; charset=UTF-8"
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE)
}
companion object {
private fun setContentTypeHeader(response: HttpResponse, file: File) {
val mimeTypesMap = MimetypesFileTypeMap()
response.headers()
.set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.path))
}
}
}
基于KMP的静态文件服务器就基本完成,看看Windows上访问Android的效果。
在Windows或Linux上运行效果也一样,点击目录可以进入下一级,点击文件可以下载。
源码下载
如果不想一步一步实现,也可以关注公众号”梦想周游世界的猿同学“,或扫码关注后直接获取本示例源码。
关注公众号后,在消息中输入 source:FileServer.zip, 点击公众号回复的链接即可下载。如:
感谢阅读和关注,祝大家:有钱、有梦、有远方。