概览
GCDWebServer 是一个现代且轻量级的基于 GCD 的 HTTP 1.1 服务器,专为嵌入 iOS、macOS 和 tvOS 应用而设计。它是从头开始编写的,并考虑以下目标
- 具有优雅且易于使用的架构,仅使用 4 个核心类:服务器、连接、请求和响应(请参阅以下 "理解 GCDWebServer 架构")
- 经过良好设计的 API,带有完整文档的头部,以便于集成和定制
- 使用 Grand Central Dispatch 完全事件驱动设计构建,以提高性能和并发性
- 不依赖于第三方源代码
- 在友好的 New BSD 许可证 下可用
额外内置功能
- 允许实现完全异步的 HTTP 请求处理器
- 通过磁盘流式传输大型的 HTTP 请求或响应体,最小化内存使用
- 支持使用 "application/x-www-form-urlencoded" 或 "multipart/form-data" 编码提交的 web 表单 解析器(包括文件上传)
- 请求和响应 HTTP 体的 JSON 解析和序列化
- 请求和响应 HTTP 体的 分块传输编码
- 请求和响应 HTTP 体的 HTTP 压缩,使用 gzip
- 支持局部文件的请求的 HTTP 范围
- 基于密码保护的 基本 和 摘要访问 认证
- 自动处理在 iOS 应用中前台、后台和暂停模式之间的转换
- 完全支持 IPv4 和 IPv6
- NAT 端口映射(仅 IPv4)
内置的扩展
- GCDWebUploader:是
GCDWebServer
的子类,它实现了使用网页浏览器上传和下载文件的用户界面 - GCDWebDAVServer:是
GCDWebServer
的子类,它实现了一个类 1 WebDAV 服务器(包含对 macOS Finder 的类 2 部分支持)
不支持的功能(但对于嵌入式 HTTP 服务器来说并非必需)
- 持久连接
- HTTPS
要求
- macOS 10.7 或更高版本(x86_64)
- iOS 8.0 或更高版本(armv7, armv7s 或 arm64)
- tvOS 9.0 或更高版本(arm64架构)
- 仅支持ARC内存管理(如需MRC支持,请使用GCDWebServer 3.1或更早版本)
入门指南
下载或检出GCDWebServer的最新版本,然后将整个“GCDWebServer”子文件夹添加到您的Xcode项目中。如果您打算使用GCDWebDAVServer或GCDWebUploader等扩展,请将这两个子文件夹也添加进去。最后,通过Target > Build Phases > Link Binary With Libraries链接到libz
(通过Target > Build Settings > HEADER_SEARCH_PATHS添加$(SDKROOT)/usr/include/libxml2
到您的头文件搜索路径)。
或者,您可以使用CocoaPods安装GCDWebServer,只需将以下行添加到您的Podfile
pod "GCDWebServer", "~> 3.0"
如果您想使用GCDWebUploader,使用此行代替
pod "GCDWebServer/WebUploader", "~> 3.0"
或者此行用于GCDWebDAVServer
pod "GCDWebServer/WebDAV", "~> 3.0"
最后运行$ pod install
。
您还可以使用Carthage,只需将此行添加到您的Cartfile(3.2.5是首次有Carthage支持的版本)
github "swisspol/GCDWebServer" ~> 3.2.5
然后运行$ carthage update
并将生成的框架添加到您的Xcode项目中(请参阅Carthage说明)。
帮助与支持
对于使用GCDWebServer的帮助,最好的做法是在Stack Overflow上以gcdwebserver
标签提问。对于错误报告和增强请求,您可以使用该项目的问题。
不过,请先阅读整个README文件!
Hello World
这些代码片段展示了如何实现一个自定义HTTP服务器,该服务器在端口8080上运行,并返回一个“Hello World”HTML页面作为任何请求的响应。由于GCDWebServer使用GCD块来处理请求,因此不需要子类或分发,这使得代码非常简洁。
重要:如果不使用CocoaPods,请确保将共享系统库libz
添加到您应用程序的Xcode目标中。
macOS版本(命令行工具)
#import "GCDWebServer.h"
#import "GCDWebServerDataResponse.h"
int main(int argc, const char* argv[]) {
@autoreleasepool {
// Create server
GCDWebServer* webServer = [[GCDWebServer alloc] init];
// Add a handler to respond to GET requests on any URL
[webServer addDefaultHandlerForMethod:@"GET"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
return [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
}];
// Use convenience method that runs server on port 8080
// until SIGINT (Ctrl-C in Terminal) or SIGTERM is received
[webServer runWithPort:8080 bonjourName:nil];
NSLog(@"Visit %@ in your web browser", webServer.serverURL);
}
return 0;
}
iOS版本
#import "GCDWebServer.h"
#import "GCDWebServerDataResponse.h"
@interface AppDelegate : NSObject <UIApplicationDelegate> {
GCDWebServer* _webServer;
}
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
// Create server
_webServer = [[GCDWebServer alloc] init];
// Add a handler to respond to GET requests on any URL
[_webServer addDefaultHandlerForMethod:@"GET"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
return [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
}];
// Start server on port 8080
[_webServer startWithPort:8080 bonjourName:nil];
NSLog(@"Visit %@ in your web browser", _webServer.serverURL);
return YES;
}
@end
macOS Swift版本(命令行工具)
webServer.swift
import Foundation
import GCDWebServer
func initWebServer() {
let webServer = GCDWebServer()
webServer.addDefaultHandler(forMethod: "GET", request: GCDWebServerRequest.self, processBlock: {request in
return GCDWebServerDataResponse(html:"<html><body><p>Hello World</p></body></html>")
})
webServer.start(withPort: 8080, bonjourName: "GCD Web Server")
print("Visit \(webServer.serverURL) in your web browser")
}
WebServer-Bridging-Header.h
#import <GCDWebServer/GCDWebServer.h>
#import <GCDWebServer/GCDWebServerDataResponse.h>
基于Web的iOS应用上传
GCDWebUploader是GCDWebServer
的一个子类,提供了一个现成的HTML 5文件上传和下载功能。这允许用户通过他们的网络浏览器中干净的用户界面,从iOS应用沙盒内的一个目录上传、下载、删除文件以及创建目录。
只需实例化并运行一个GCDWebUploader
实例,然后从您的网络浏览器访问http://{YOUR-IOS-DEVICE-IP-ADDRESS}/
。
#import "GCDWebUploader.h"
@interface AppDelegate : NSObject <UIApplicationDelegate> {
GCDWebUploader* _webUploader;
}
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
_webUploader = [[GCDWebUploader alloc] initWithUploadDirectory:documentsPath];
[_webUploader start];
NSLog(@"Visit %@ in your web browser", _webUploader.serverURL);
return YES;
}
@end
iOS应用中的WebDAV服务器
GCDWebDAVServer是GCDWebServer
的一个子类,提供了一个遵循类1规范WebDAV的Web服务器。这允许用户通过任何WebDAV客户端(如Transmit (Mac),ForkLift (Mac) 或 CyberDuck (Mac / Windows))从iOS应用沙盒内的一个目录上传、下载、删除文件以及创建目录。
GCDWebDAVServer也应与macOS Finder一起使用,因为它部分符合类2规范(但只有当客户是macOS WebDAV实现时)。
只需实例化并运行一个GCDWebDAVServer
实例,然后使用WebDAV客户端连接到http://{YOUR-IOS-DEVICE-IP-ADDRESS}/
。
#import "GCDWebDAVServer.h"
@interface AppDelegate : NSObject <UIApplicationDelegate> {
GCDWebDAVServer* _davServer;
}
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
_davServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:documentsPath];
[_davServer start];
NSLog(@"Visit %@ in your WebDAV client", _davServer.serverURL);
return YES;
}
@end
托管静态网站
GCDWebServer
包含一个内建的处理器,可以递归地托管目录(它还允许您控制如何设置“Cache-Control”头)。
macOS版本(命令行工具)
#import "GCDWebServer.h"
int main(int argc, const char* argv[]) {
@autoreleasepool {
GCDWebServer* webServer = [[GCDWebServer alloc] init];
[webServer addGETHandlerForBasePath:@"/" directoryPath:NSHomeDirectory() indexFilename:nil cacheAge:3600 allowRangeRequests:YES];
[webServer runWithPort:8080];
}
return 0;
}
使用GCDWebServer
您首先创建一个GCDWebServer
类的实例。请注意,只要它们监听不同的端口,您可以在同一个应用程序中运行多个网络服务器。
然后,您向服务器添加一个或多个“处理器”:每个处理器都有机会处理传入的Web请求并提供响应。处理器在LIFO队列中调用,所以最后添加的处理器会覆盖之前添加的。
最后,您在特定的端口上启动服务器。
理解 GCDWebServer 的架构
GCDWebServer 的架构仅包含 4 个核心类
- GCDWebServer 管理用于监听新 HTTP 连接的套接字和服务器使用的处理程序列表。
- GCDWebServerConnection 由
GCDWebServer
实例化以处理每个新的 HTTP 连接。每个实例在连接关闭前一直存活。您不能直接使用这个类,但它是公开的,以便您可以对其进行子类化以覆盖一些钩子。 - GCDWebServerRequest 在收到 HTTP 头部后由
GCDWebServerConnection
实例创建。它包装请求并处理任何 HTTP 主体。GCDWebServer 伴随着几个 GCDWebServerRequest 的子类来处理常见情况,如将主体存储在内存中或将它流式传输到磁盘上的文件。 - GCDWebServerResponse 由请求处理程序创建,包装 HTTP 响应头部和可选主体。GCDWebServer 伴随着几个 GCDWebServerResponse 的子类来处理常见情况,如 HTML 文本在内存中或从磁盘流式传输文件。
实现处理程序
GCDWebServer 依赖于“处理程序”来处理传入的 Web 请求并生成响应。处理程序使用 GCD 块实现,这使得提供您自己的处理程序变得非常简单。然而,它们在 GCD 中的任意线程上执行,因此必须注意线程安全性和可重入性。
处理程序需要 2 个 GCD 块
GCDWebServerMatchBlock
在将每个处理程序添加到GCDWebServer
实例时被调用,每次 Web 请求启动时(即收到 HTTP 头部)。它传递关于 Web 请求的基本信息(HTTP 方法、URL、头部...),并必须决定是否要处理它。如果是,它必须返回一个新创建的GCDWebServerRequest
实例(见上文)。如果不处理,它简单地返回 nil。GCDWebServerProcessBlock
或GCDWebServerAsyncProcessBlock
在完全收到 Web 请求后被调用,并传递前一阶段创建的GCDWebServerRequest
实例。它必须同步返回(如果使用GCDWebServerProcessBlock
)或异步返回(如果使用GCDWebServerAsyncProcessBlock
)一个GCDWebServerResponse
实例(见上文)或在错误时返回 nil,这将导致向客户端返回 500 HTTP 状态码。然而,通常建议在错误时返回 GCDWebServerErrorResponse 的实例,以便能够向客户端返回更有用的信息。
请注意,大多数用于向 GCDWebServer
添加处理程序的方法仅需要 GCDWebServerProcessBlock
或 GCDWebServerAsyncProcessBlock
,因为它们已经提供了内置的 GCDWebServerMatchBlock
,例如,可以与正则表达式匹配 URL 路径。
异步 HTTP 响应
在 GCDWebServer 3.0 中新增了处理 HTTP 请求异步的功能,即在服务器上添加处理程序,生成其 GCDWebServerResponse
是异步的。这是通过添加使用 GCDWebServerAsyncProcessBlock
而不是 GCDWebServerProcessBlock
的处理程序来实现的。以下是一个示例:
(同步版本) 处理程序在生成 HTTP 响应时被阻塞。
[webServer addDefaultHandlerForMethod:@"GET"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
return response;
}];
(异步版本) 处理程序立即返回,并稍后回调 GCDWebServer 生成 HTTP 响应。
[webServer addDefaultHandlerForMethod:@"GET"
requestClass:[GCDWebServerRequest class]
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
// Do some async operation like network access or file I/O (simulated here using dispatch_after())
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
completionBlock(response);
});
}];
(高级异步版本) 处理程序立即返回一个流式 HTTP 响应,该响应本身异步生成其内容。
[webServer addDefaultHandlerForMethod:@"GET"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
NSMutableArray* contents = [NSMutableArray arrayWithObjects:@"<html><body><p>\n", @"Hello World!\n", @"</p></body></html>\n", nil]; // Fake data source we are reading from
GCDWebServerStreamedResponse* response = [GCDWebServerStreamedResponse responseWithContentType:@"text/html" asyncStreamBlock:^(GCDWebServerBodyReaderCompletionBlock completionBlock) {
// Simulate a delay reading from the fake data source
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString* string = contents.firstObject;
if (string) {
[contents removeObjectAtIndex:0];
completionBlock([string dataUsingEncoding:NSUTF8StringEncoding], nil); // Generate the 2nd part of the stream data
} else {
completionBlock([NSData data], nil); // Must pass an empty NSData to signal the end of the stream
}
});
}];
return response;
}];
请注意,您甚至可以将异步和高级异步版本结合起来,异步返回异步 HTTP 响应!
GCDWebServer 与 iOS 应用后台模式
在 iOS 应用中执行网络操作时,您必须小心处理 iOS 将应用置于后台时会发生的情况。通常,您必须在应用处于后台时停止任何网络服务器,并在应用回到前台时重启它们。考虑到服务器可能需要在停止时保留正在进行的连接,这可能相当复杂。
幸运的是,GCDWebServer 会自动为您完成所有这一切
- 每当首次打开 HTTP 连接时,GCDWebServer 就开始一个 后台任务,仅当最后一个连接关闭时结束它。这阻止了 iOS 在应用进入后台后挂起应用,这将立即杀死客户端的 HTTP 连接。
- 当应用处于后台时,只要仍然有新的 HTTP 连接被发起,后台任务将继续存在,iOS 不会挂起应用 长达 10 分钟(除非遇到突然的意外内存压力)。
- 如果应用在最后一个 HTTP 连接关闭时仍处于后台,GCDWebServer 将挂起自身,停止接收新的连接,就好像您已经调用了
-stop
(此行为可以通过GCDWebServerOption_AutomaticallySuspendInBackground
选项禁用)。 - 如果应用在没有打开HTTP连接的情况下进入后台,GCDWebServer 将立即挂起自身,停止接收新的连接,就好像您已经调用了
-stop
(此行为可以通过GCDWebServerOption_AutomaticallySuspendInBackground
选项禁用)。 - 如果应用回到前台而 GCDWebServer 已被挂起,它将自动恢复并重新开始接收新的 HTTP 连接,就好像您已经调用了
-start
。
HTTP 连接通常以批量(或突发)方式启动,例如加载具有多个资源的网页。这使得难以准确检测最后一个 HTTP 连接何时被关闭:有可能两个连续的 HTTP 连接属于同一批,但中间却有一个小的延迟,而不是重叠。如果 GCDWebServer 正好在这个时候暂停,对客户端将是不利的。通过强制 GCDWebServer 在关闭最后一个 HTTP 连接之后等待额外的延迟,然后再执行任何操作,GCDWebServerOption_ConnectedStateCoalescingInterval
选项巧妙地解决了这个问题,以防这个延迟内启动新的 HTTP 连接。
这是一个示例处理器,它使用 GCDWebServerResponse
上的便利方法将 "/" 重定向到 "/index.html"(它自动设置 HTTP 状态码和 "Location" 标头)。
[self addHandlerForMethod:@"GET"
path:@"/"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
return [GCDWebServerResponse responseWithRedirect:[NSURL URLWithString:@"index.html" relativeToURL:request.URL]
permanent:NO];
}];
要实现HTTP表单,您需要一个处理器对
- GET处理器不需要HTTP请求的任何正文,因此使用
GCDWebServerRequest
类。处理器生成一个包含简单HTML表单的响应。 - POST处理器期望表单值在HTTP请求中位于正文部分,并进行百分编码。幸运的是,GCDWebServer提供了请求类
GCDWebServerURLEncodedFormRequest
,它可以自动解析这样的正文。处理器简单地回显用户提交的表单值。
[webServer addHandlerForMethod:@"GET"
path:@"/"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
NSString* html = @" \
<html><body> \
<form name=\"input\" action=\"/\" method=\"post\" enctype=\"application/x-www-form-urlencoded\"> \
Value: <input type=\"text\" name=\"value\"> \
<input type=\"submit\" value=\"Submit\"> \
</form> \
</body></html> \
";
return [GCDWebServerDataResponse responseWithHTML:html];
}];
[webServer addHandlerForMethod:@"POST"
path:@"/"
requestClass:[GCDWebServerURLEncodedFormRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
NSString* value = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"value"];
NSString* html = [NSString stringWithFormat:@"<html><body><p>%@</p></body></html>", value];
return [GCDWebServerDataResponse responseWithHTML:html];
}];
高级示例 3:提供动态网站
GCDWebServer为GCDWebServerDataResponse
类提供了一个扩展,可以返回从模板和一组变量(使用格式%variable%
)生成的HTML内容。这是一个非常基本的模板系统,并且真正目的是作为通过派生自GCDWebServerResponse
来构建更高级模板系统的起点。
假设您在应用中有一个包含HTML模板文件的网站目录,以及相应的CSS、脚本和图像,将它变成一个动态网站是相当容易的。
// Get the path to the website directory
NSString* websitePath = [[NSBundle mainBundle] pathForResource:@"Website" ofType:nil];
// Add a default handler to serve static files (i.e. anything other than HTML files)
[self addGETHandlerForBasePath:@"/" directoryPath:websitePath indexFilename:nil cacheAge:3600 allowRangeRequests:YES];
// Add an override handler for all requests to "*.html" URLs to do the special HTML templatization
[self addHandlerForMethod:@"GET"
pathRegex:@"/.*\.html"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
NSDictionary* variables = [NSDictionary dictionaryWithObjectsAndKeys:@"value", @"variable", nil];
return [GCDWebServerDataResponse responseWithHTMLTemplate:[websitePath stringByAppendingPathComponent:request.path]
variables:variables];
}];
// Add an override handler to redirect "/" URL to "/index.html"
[self addHandlerForMethod:@"GET"
path:@"/"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
return [GCDWebServerResponse responseWithRedirect:[NSURL URLWithString:@"index.html" relativeToURL:request.URL]
permanent:NO];
];
最终示例:从iOS应用下载和上传文件
GCDWebServer最初是为iPad漫画阅读应用ComicFlow编写的。它允许用户通过WiFi使用Web浏览器连接到iPad,然后在应用内上传、下载和组织漫画文件。
ComicFlow是完全开源的,您可以在WebServer.h和WebServer.m文件中查看它如何使用GCDWebServer。