本文主要介绍GCDWebServer
内部的处理过程,关于GCDWebServer
的使用可以参考GCDWebServer GitHub
GCDWebServer
的处理流程分为三个步骤:
- 启动&&监听请求
- 处理请求
- 响应
启动&&监听请求
GCDWebServer
提供了若干个启动方法:
- (BOOL)start;
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(nullable NSString*)name;
- (BOOL)runWithPort:(NSUInteger)port bonjourName:(nullable NSString*)name;
- (BOOL)runWithOptions:(nullable NSDictionary*)options error:(NSError** _Nullable)error;
这些启动方法,最终都会调用_start:
方法,下面看下_start:
方法中到底做了什么:
- (BOOL)_start:(NSError**)error {
GWS_DCHECK(_source4 == NULL);
//获取外部配置的服务端的监听端口
NSUInteger port = [_GetOption(_options, GCDWebServerOption_Port, @0) unsignedIntegerValue];
//表示是否是使用 127.0.0.1 或 localhost 的形式访问服务器
BOOL bindToLocalhost = [_GetOption(_options, GCDWebServerOption_BindToLocalhost, @NO) boolValue];
//等待连接队列中连接的最大数量
NSUInteger maxPendingConnections = [_GetOption(_options, GCDWebServerOption_MaxPendingConnections, @16) unsignedIntegerValue];
//服务器listen socket的本地地址信息的设置
struct sockaddr_in addr4;
bzero(&addr4, sizeof(addr4));
addr4.sin_len = sizeof(addr4);
addr4.sin_family = AF_INET;
addr4.sin_port = htons(port);
addr4.sin_addr.s_addr = bindToLocalhost ? htonl(INADDR_LOOPBACK) : htonl(INADDR_ANY);
//创建 listen scoket
int listeningSocket4 = [self _createListeningSocket:NO localAddress:&addr4 length:sizeof(addr4) maxPendingConnections:maxPendingConnections error:error];
if (listeningSocket4 <= 0) {
return NO;
}
//如果外部设置的端口为0 那么将由系统分配一个可用端口
if (port == 0) {
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
if (getsockname(listeningSocket4, (struct sockaddr*)&addr, &addrlen) == 0) {
port = ntohs(addr.sin_port);
} else {
GWS_LOG_ERROR(@"Failed retrieving socket address: %s (%i)", strerror(errno), errno);
}
}
//作用同IPv4 只是用于支持IPv6
struct sockaddr_in6 addr6;
bzero(&addr6, sizeof(addr6));
addr6.sin6_len = sizeof(addr6);
addr6.sin6_family = AF_INET6;
addr6.sin6_port = htons(port);
addr6.sin6_addr = bindToLocalhost ? in6addr_loopback : in6addr_any;
int listeningSocket6 = [self _createListeningSocket:YES localAddress:&addr6 length:sizeof(addr6) maxPendingConnections:maxPendingConnections error:error];
if (listeningSocket6 <= 0) {
close(listeningSocket4);
return NO;
}
_serverName = [_GetOption(_options, GCDWebServerOption_ServerName, NSStringFromClass([self class])) copy];
//获取外部设置的 http 认证类型(参考下面备注)
NSString* authenticationMethod = _GetOption(_options, GCDWebServerOption_AuthenticationMethod, nil);
//保存用于基本认证的账号密码
if ([authenticationMethod isEqualToString:GCDWebServerAuthenticationMethod_Basic]) {
_authenticationRealm = [_GetOption(_options, GCDWebServerOption_AuthenticationRealm, _serverName) copy];
_authenticationBasicAccounts = [[NSMutableDictionary alloc] init];
NSDictionary* accounts = _GetOption(_options, GCDWebServerOption_AuthenticationAccounts, @{});
[accounts enumerateKeysAndObjectsUsingBlock:^(NSString* username, NSString* password, BOOL* stop) {
[_authenticationBasicAccounts setObject:_EncodeBase64([NSString stringWithFormat:@"%@:%@", username, password]) forKey:username];
}];
}
//保存用于摘要认证的账号密码
else if ([authenticationMethod isEqualToString:GCDWebServerAuthenticationMethod_DigestAccess]) {
_authenticationRealm = [_GetOption(_options, GCDWebServerOption_AuthenticationRealm, _serverName) copy];
_authenticationDigestAccounts = [[NSMutableDictionary alloc] init];
NSDictionary* accounts = _GetOption(_options, GCDWebServerOption_AuthenticationAccounts, @{});
[accounts enumerateKeysAndObjectsUsingBlock:^(NSString* username, NSString* password, BOOL* stop) {
[_authenticationDigestAccounts setObject:GCDWebServerComputeMD5Digest(@"%@:%@:%@", username, _authenticationRealm, password) forKey:username];
}];
}
_connectionClass = _GetOption(_options, GCDWebServerOption_ConnectionClass, [GCDWebServerConnection class]);
_shouldAutomaticallyMapHEADToGET = [_GetOption(_options, GCDWebServerOption_AutomaticallyMapHEADToGET, @YES) boolValue];
_disconnectDelay = [_GetOption(_options, GCDWebServerOption_ConnectedStateCoalescingInterval, @1.0) doubleValue];
_dispatchQueuePriority = [_GetOption(_options, GCDWebServerOption_DispatchQueuePriority, @(DISPATCH_QUEUE_PRIORITY_DEFAULT)) longValue];
//创建对 listen scoket 是否有可读内容的监听source(即是否有客户端的连接请求产生)
_source4 = [self _createDispatchSourceWithListeningSocket:listeningSocket4 isIPv6:NO];
_source6 = [self _createDispatchSourceWithListeningSocket:listeningSocket6 isIPv6:YES];
_port = port;
_bindToLocalhost = bindToLocalhost;
NSString* bonjourName = _GetOption(_options, GCDWebServerOption_BonjourName, nil);
NSString* bonjourType = _GetOption(_options, GCDWebServerOption_BonjourType, @"_http._tcp");
//Bonjour 服务
if (bonjourName) {
_registrationService = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), (__bridge CFStringRef)bonjourType, (__bridge CFStringRef)(bonjourName.length ? bonjourName : _serverName), (SInt32)_port);
if (_registrationService) {
CFNetServiceClientContext context = {0, (__bridge void*)self, NULL, NULL, NULL};
CFNetServiceSetClient(_registrationService, _NetServiceRegisterCallBack, &context);
CFNetServiceScheduleWithRunLoop(_registrationService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
CFStreamError streamError = {0};
CFNetServiceRegisterWithOptions(_registrationService, 0, &streamError);
_resolutionService = CFNetServiceCreateCopy(kCFAllocatorDefault, _registrationService);
if (_resolutionService) {
CFNetServiceSetClient(_resolutionService, _NetServiceResolveCallBack, &context);
CFNetServiceScheduleWithRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
} else {
GWS_LOG_ERROR(@"Failed creating CFNetService for resolution");
}
} else {
GWS_LOG_ERROR(@"Failed creating CFNetService for registration");
}
}
//NAT服务
if ([_GetOption(_options, GCDWebServerOption_RequestNATPortMapping, @NO) boolValue]) {
DNSServiceErrorType status = DNSServiceNATPortMappingCreate(&_dnsService, 0, 0, kDNSServiceProtocol_TCP, htons(port), htons(port), 0, _DNSServiceCallBack, (__bridge void*)self);
if (status == kDNSServiceErr_NoError) {
CFSocketContext context = {0, (__bridge void*)self, NULL, NULL, NULL};
_dnsSocket = CFSocketCreateWithNative(kCFAllocatorDefault, DNSServiceRefSockFD(_dnsService), kCFSocketReadCallBack, _SocketCallBack, &context);
if (_dnsSocket) {
CFSocketSetSocketFlags(_dnsSocket, CFSocketGetSocketFlags(_dnsSocket) & ~kCFSocketCloseOnInvalidate);
_dnsSource = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _dnsSocket, 0);
if (_dnsSource) {
CFRunLoopAddSource(CFRunLoopGetMain(), _dnsSource, kCFRunLoopCommonModes);
} else {
GWS_LOG_ERROR(@"Failed creating CFRunLoopSource");
GWS_DNOT_REACHED();
}
} else {
GWS_LOG_ERROR(@"Failed creating CFSocket");
GWS_DNOT_REACHED();
}
} else {
GWS_LOG_ERROR(@"Failed creating NAT port mapping (%i)", status);
}
}
//开始监听 listen socket
dispatch_resume(_source4);
dispatch_resume(_source6);
GWS_LOG_INFO(@"%@ started on port %i and reachable at %@", [self class], (int)_port, self.serverURL);
if ([_delegate respondsToSelector:@selector(webServerDidStart:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[_delegate webServerDidStart:self];
});
}
return YES;
}
更多关于socket编程,可以参考这篇博客。
关于 HTTP 认证,可以参考 <<HTTP 权威指南>> 的12、13章
创建监听 listen socket 的source的方法:
- (dispatch_source_t)_createDispatchSourceWithListeningSocket:(int)listeningSocket isIPv6:(BOOL)isIPv6 {
dispatch_group_enter(_sourceGroup);
//创建一个监听I/O是否可读的source
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listeningSocket, 0, dispatch_get_global_queue(_dispatchQueuePriority, 0));
//设置source取消监听逻辑
dispatch_source_set_cancel_handler(source, ^{
@autoreleasepool {
int result = close(listeningSocket);
if (result != 0) {
GWS_LOG_ERROR(@"Failed closing %s listening socket: %s (%i)", isIPv6 ? "IPv6" : "IPv4", strerror(errno), errno);
} else {
GWS_LOG_DEBUG(@"Did close %s listening socket %i", isIPv6 ? "IPv6" : "IPv4", listeningSocket);
}
}
dispatch_group_leave(_sourceGroup);
});
//设置source监听到事件后的处理逻辑
dispatch_source_set_event_handler(source, ^{
@autoreleasepool {
struct sockaddr_storage remoteSockAddr;
socklen_t remoteAddrLen = sizeof(remoteSockAddr);
//与客户端建立连接,此处生成的socket称为:connect socket
int socket = accept(listeningSocket, (struct sockaddr*)&remoteSockAddr, &remoteAddrLen);
//连接如果建立成功
if (socket > 0) {
/*
获取连接者的地址信息
*/
NSData* remoteAddress = [NSData dataWithBytes:&remoteSockAddr length:remoteAddrLen];
struct sockaddr_storage localSockAddr;
socklen_t localAddrLen = sizeof(localSockAddr);
NSData* localAddress = nil;
if (getsockname(socket, (struct sockaddr*)&localSockAddr, &localAddrLen) == 0) {
localAddress = [NSData dataWithBytes:&localSockAddr length:localAddrLen];
GWS_DCHECK((!isIPv6 && localSockAddr.ss_family == AF_INET) || (isIPv6 && localSockAddr.ss_family == AF_INET6));
} else {
GWS_DNOT_REACHED();
}
int noSigPipe = 1;
//(此处设置原因参考下面的备注)
setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)); // Make sure this socket cannot generate SIG_PIPE
GCDWebServerConnection* connection = [[_connectionClass alloc] initWithServer:self localAddress:localAddress remoteAddress:remoteAddress socket:socket]; // Connection will automatically retain itself while opened
[connection self]; // Prevent compiler from complaining about unused variable / useless statement
} else {
GWS_LOG_ERROR(@"Failed accepting %s socket: %s (%i)", isIPv6 ? "IPv6" : "IPv4", strerror(errno), errno);
}
}
});
return source;
}
关于 SIG_PIPE 信号
当在一个已经断开的socket的上调用send函数发送数据会产生SIG_PIPE信号,系统接收到该信号后会将App进程终止。假设App没有崩溃,那么
send函数会返回一个-1,我们可以通过该值判断发送失败,然后重新建立链接,但是SIG_PIPE信号发生在send函数返回之前,所以我们需要通过设置SO_NOSIGPIPE选项让socket不要
发射SIG_PIPE信号。
至此,服务器已经处于监听状态,并设置好了监听到连接后的处理逻辑。
处理请求
读取header
上一节中,在监听到连接请求并建立完连接后,便会开始处理请求,首先肯定是要从连接中把 header 部分数据读取出来。
从哪里读
从connect socket
中读取,也就是accept()
函数返回的那个socket,也就是服务端与客户端的那条连接。
怎么读
在Unix-like的系统中,一切皆文件,所以socket也是文件,accpet()
函数返回的其实是个用于操作对应socket的文件描述符(File Descriptor 简称:FD)。
GCDWebServer中使用dispatch_read
读取数据,调用dispatch_read
时,会传入一个handler
,当读取到数据或读取出错时,该handler
会被调用。
那么什么时候表示header
读取完毕了呢?根据http的规范,header
与body
部分通过CRLF(\r\n\r\n)进行分隔,所以每读取一部分数据后,都会判断下当前数据中是否有CRLF,如果有则表示header
读取完毕了。
这种读取方式有个需要注意的地方,因为读取header
的时候,每次都是读取尽量多的数据,那么,如果此次读取时,socket中如果已经有了部分body
的数据,则会被一同读取到,
所以,header
读取完后,如果发现CRLF后面还有其他数据,那么会将这些数据保存下来,之后会作为body
数据的一部分。
读取header的逻辑在GCDWebServerConnection
类的_readRequestHeaders
方法中。
header读取完毕后的处理
header
读取完毕后并不会立即就开始读取body
数据,还要进行一些header
字段校验的工作,以确保这是一条有效的、服务端支持的http请求(因为http中header
字段非常多,服务器有时候
并不会支持所有字段,对于不支持的字段则需要将报错信息响应给客户端)。下面用一个流程图表示header
读取完后在读取body
数据前的处理:
读取body
从哪里读
当然,还是从connect socket
中读取,只不过body
数据读取的起点是从上一步的header
数据读取的终点开始。
怎么读
这里会涉及到两个概念: 内容编码(Content-Encoding) 和 传输编码(Transfer-Encoding)。
内容编码介绍 —– 摘自<<HTTP 权威指南>>
HTTP 应用程序有时在发送之前需要对内容进行编码。例如,在把很大的 HTML 文 档发送给通过慢速连接连上来的客户端之前 , 服务器可能会对它进行压缩,这样有 助于减少传输实体的时间。服务器还可以把内容搅乱或加密,以此来防止未经授权 的第三方看到文档的内容。
这种类型的编码是在发送方应用到内容之上的。当内容经过内容编码之后,编好码 的数据就放在实体主体中,像往常一样发送给接收方。
HTTP 定义了一些标准的内容编码类型,并允许用扩展编码的形式增添更多的编码。 由互联网号码分配机构(IANA)对各种编码进行标准化,它给每个内容编码算法分 配了唯一的代号。Content-Encoding 首部就用这些标准化的代号来说明编码时使 用的算法。
gzip : 表明实体采用 GNU zip 编码
compress : 表明实体采用 Unix 的文件压缩程序
deflate : 表明实体是用 zlib 的格式压缩的
identity : 表明没有对实体进行编码。当没有 Content-Encoding 首部时,就默认 为这种情况
关于HTTP内容编码更详细的介绍可参考<<HTTP 权威指南>> 15.5 章节。传输编码介绍 —– 摘自<<HTTP 权威指南>>
传输编码也是作用在实体主体上的可逆变换,但使用它们是由 于架构方面的原因,同内容的格式无关。使用传输编码是为了改变 报文中的数据在网络上传输的方式。
长久以来,在其他一些协议中会用传输编码来保证报文经过网络时能得到“可靠传 输”。在 HTTP 协议中,可靠传输关注的焦点有所不同,因为底层的传输设施已经标 准化并且容错性更好。在 HTTP 中,只有少数一些情况下,所传输的报文主体可能 会引发问题。其中两种情况如下所述。
未知的尺寸
如果不先生成内容,某些网关应用程序和内容编码器就无法确定报文主体的最终 大小。通常,这些服务器希望在知道大小之前就开始传输数据。因为 HTTP 协议 要求 Content-Length 首部必须在数据之前,有些服务器就使用传输编码来发送数据,并用特别的结束脚注表明数据结束。
安全性
你可以用传输编码来把报文内容扰乱,然后在共享的传输网络上发送。不过,由 于像 SSL 这样的传输层安全体系的流行,就很少需要靠传输编码来实现安全性了。
关于HTTP传输编码更详细的介绍可参考<<HTTP 权威指南>> 15.6 章节。
这里需要注意的是,客户端在发出http报文时,是先进行内容编码再进行传输编码的,这个很好理解: 内容确定了,才会开始传输。
因此,在读取body
数据的时候则与发送http报文时相反,先根据传输编码读取body
数据,再根据内容编码解码body
数据。
body 数据存哪儿
在GCDWebServer
中,GCDWebServerRequest
类是用来包装请求信息的,所以body
数据都会被”写”到GCDWebServerRequest
实例中,因为body
数据会有各种格式而且服务端对不同请求的body
数据的处理也可能各不相同,所以 GCDWebServer
中提供了若干个GCDWebServerRequest
的子类,以方便的格式化或存储body
数据:
-GCDWebServerDataRequest
: 将 body
的数据以原始data的形式保存,并且提供了text、jsonObject等便捷属性用来获取解码后的body数据。
-GCDWebServerFileRequest
: body
读取完毕后,会将body数据保存在磁盘上,并会提供文件保存的位置。
-GCDWebServerMultiPartFormRequest
:用来解析Content-Type
为multipart/form-data
时的body
数据。
-GCDWebServerURLEncodedFormRequest
:用来解析Content-Type
为application/x-www-form-urlencoded
时的body
数据。
代码说明
读取body
的方式会根据header
中是否设置了Transfer-Encoding: chunked
而不同,GCDWebServer
封装了两个读取body
数据的方法: _readBodyWithLength:initialData:
_readChunkedBodyWithInitialData:
,下面依次说明下这两个方法:
_readBodyWithLength:initialData:
/*
header 中未设置 Transfer-Encoding: chunked 时,使用这个方法读取 body 数据
注意第二个参数 initialData,它是读取 header 时额外读取到的 body 数据。
*/
- (void)_readBodyWithLength:(NSUInteger)length initialData:(NSData*)initialData {
NSError* error = nil;
/*
前面讲过,body 的数据会被"写"到 GCDWebServerRequest 实例中,所以 GCDWebServerRequest 内部有一个 _writer 属性,
它是 id<GCDWebServerBodyWriter> 类型,专门用来负责将读取到的 body 数据"写"入到 GCDWebServerRequest 中,其会根据
Content-Encoding 对body 数据进行解码。
目前 GCDWebServer 中只支持gzip的解码,就是说只支持 Content-Encoding: gzip的情况,GCDWebServer 中 GCDWebServerGZipDecoder
类实现了 gzip 的解码。
_writer 属性的初始化是在读取完header 数据之后,见 GCDWebServerConnection类 _readRequestHeaders 方法。
此处调动 performOpen: 是处理一些写入前的准备工作。
*/
if (![_request performOpen:&error]) {
GWS_LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
return;
}
if (initialData.length) {
//先将先前读取 header 时额外读取到的 body 数据"写"到 _request 中。
if (![_request performWriteData:initialData error:&error]) {
GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
if (![_request performClose:&error]) {
GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
}
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
return;
}
length -= initialData.length;
}
//如果 body 数据还有剩余
if (length) {
//读取剩余的 body 数据
[self readBodyWithRemainingLength:length
completionBlock:^(BOOL success) {
NSError* localError = nil;
//处理一些"写"完后的清理工作
if ([_request performClose:&localError]) {
[self _startProcessingRequest];
} else {
GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
}
}];
} else {
//处理一些"写"完后的清理工作
if ([_request performClose:&error]) {
[self _startProcessingRequest];
} else {
GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
}
}
}
读取剩余的 body 数据
- (void)readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block {
GWS_DCHECK([_request hasBody] && ![_request usesChunkedTransferEncoding]);
NSMutableData* bodyData = [[NSMutableData alloc] initWithCapacity:kBodyReadCapacity];
//GCDWebServer 封装的从 connect socket 读取数据的方法
[self readData:bodyData
withLength:length
completionBlock:^(BOOL success) {
if (success) {
if (bodyData.length <= length) {
NSError* error = nil;
//这里则会调用内部的 _writer 将读取到的 body 数据"写"到 _request中
if ([_request performWriteData:bodyData error:&error]) {
NSUInteger remainingLength = length - bodyData.length;
//之所以可能会有遗留,是因为可能由于网络或其他原因,数据并未一次性的接收完毕
if (remainingLength) {
[self readBodyWithRemainingLength:remainingLength completionBlock:block];
} else {
block(YES);
}
} else {
GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
block(NO);
}
} else {
GWS_LOG_ERROR(@"Unexpected extra content reading request body on socket %i", _socket);
block(NO);
GWS_DNOT_REACHED();
}
} else {
block(NO);
}
}];
}
_readChunkedBodyWithInitialData:
- (void)_readChunkedBodyWithInitialData:(NSData*)initialData {
NSError* error = nil;
if (![_request performOpen:&error]) {
GWS_LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
return;
}
NSMutableData* chunkData = [[NSMutableData alloc] initWithData:initialData];
/* _readChunkedBodyWithInitialData: 只是个入口方法,readNextBodyChunk: 才是真正的通过 chunked 协议要求的格式读取body数据,
这里就不做额外说明了 chunked 协议要求的格式规范可以参考 <<HTTP 权威指南>> 的 15.6 章节
*/
[self readNextBodyChunk:chunkData
completionBlock:^(BOOL success) {
NSError* localError = nil;
if ([_request performClose:&localError]) {
[self _startProcessingRequest];
} else {
GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
}
}];
}
响应
在读取完body
数据后,一个完整的http请求信息便生成了,请求信息会被封装在GCDWebServerRequest
实例中传递给外部,外部获取到GCDWebServerRequest
实例后进行处理,然后
返回一个GCDWebServerResponse
实例,GCDWebServerResponse
中封装了响应信息,同样的,GCDWebServerResponse
也有若干个子类,以提供便捷的响应信息构造方法:
-GCDWebServerDataResponse
: 通过传入原始的Data构造响应信息
-GCDWebServerErrorResponse
: 提供快速构造出错响应信息的方法
-GCDWebServerFileResponse
: 提供便捷的将文件内容作为响应信息的方式
-GCDWebServerStreamedResponse
: 提供一种异步返回响应信息的方式
当GCDWebServerResponse
实例构造完后,接着便是将响应信息写入connect socket
中,这些逻辑在GCDWebServerConnection
的_finishProcessingRequest
中。具体的步骤如下:
第一步: 对比请求和响应信息中的缓存控制字段,以决定响应码是否是302,这段逻辑在 _CompareResources
函数中,参与对
比的字段有Last-Modified
、ETag
。
第二步: 设置响应信息中的header部分,设置的header字段有:
- Connection: 值设置为close
- Server: 值设置为服务器名称
- Date: 值设置为当前时间
- Last-Modified
- ETag
- Cache-Control
- Content-Type
- Content-Length
- Transfer-Encoding
- 其他的由 GCDWebServer 的使用者设置的额外头部字段
第三步: 往connect socket中写入http头部信息
第四步: 往connect socket中写入http body
数据。GCDWebServerResponse
封装了响应信息,所以 GCDWebServerResponse
中拥有http body
的数据,这一步要做的就是从 GCDWebServerResponse
实例中把body
数据读取出来然后写入 connect socket 中,GCDWebServerResponse
有多个
子类,所以根据不同类的实例body数据的来源会不同:
- 如果是 GCDWebServerResponse
实例,那么body为空。
- 如果是 GCDWebServerDataResponse
实例,那么 body 数据则来自其内部的 _data 属性。
- 如果是 GCDWebServerErrorResponse
实例,由于其实继承于 GCDWebServerDataResponse
,所以 body 数据则来自其内部的 _data 属性。
- 如果是 GCDWebServerFileResponse
实例,那么 body 数据则来自其内部的 _file
属性所表示的文件中。
- 如果是 GCDWebServerStreamedResponse
实例,那么 body
数据则来自于外部提供的 GCDWebServerStreamBlock
执行后的结果。
body
数据的来源已经确定了,但是这些数据不一定就是最终响应的http body
数据,GCDWebServer
还需要根据设置决定是否需要对body数据
进行内容编码,目前 GCDWebServer
实现了的 gzip
编码。
以上步骤完成后,http body
的数据就已经确定了,接着便是将 body 数据写入 connect socket 中,写入
其实就是将body数据从服务端
传输到客户端的过程,此时会涉及到另外一个概念:http的传输编码(Transfer-Encoding)。
说明完处理请求及响应的逻辑后,下面讲一下代码中的实现:
处理完请求后,会调用 _finishProcessingRequest:
方法
- (void)_finishProcessingRequest:(GCDWebServerResponse*)response {
GWS_DCHECK(_responseMessage == NULL);
BOOL hasBody = NO;
if (response) {
//这里处理了缓存控制字段的逻辑
response = [self overrideResponse:response forRequest:_request];
}
if (response) {
if ([response hasBody]) {
//初始化 GCDWebServerResponse 内部的 _reader 属性(id <GCDWebServerBodyReader> 类型)
[response prepareForReading];
hasBody = !_virtualHEAD;
}
NSError* error = nil;
// performOpen: 方法是用来完成 _reader 读取数据前的准备工作
if (hasBody && ![response performOpen:&error]) {
GWS_LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error);
} else {
_response = response;
}
}
if (_response) {
//初始化头部信息(状态码 等)
[self _initializeResponseHeadersWithStatusCode:_response.statusCode];
/*
从 GCDWebServerResponse 实例中取出header信息,构造响应header数据。
*/
if (_response.lastModifiedDate) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Last-Modified"), (__bridge CFStringRef)GCDWebServerFormatRFC822((NSDate*)_response.lastModifiedDate));
}
if (_response.eTag) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("ETag"), (__bridge CFStringRef)_response.eTag);
}
if ((_response.statusCode >= 200) && (_response.statusCode < 300)) {
if (_response.cacheControlMaxAge > 0) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (__bridge CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)_response.cacheControlMaxAge]);
} else {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), CFSTR("no-cache"));
}
}
if (_response.contentType != nil) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (__bridge CFStringRef)GCDWebServerNormalizeHeaderValue(_response.contentType));
}
if (_response.contentLength != NSUIntegerMax) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (__bridge CFStringRef)[NSString stringWithFormat:@"%lu", (unsigned long)_response.contentLength]);
}
if (_response.usesChunkedTransferEncoding) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Transfer-Encoding"), CFSTR("chunked"));
}
[_response.additionalHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
//往 connect socket中写入头部数据
[self writeHeadersWithCompletionBlock:^(BOOL success) {
if (success) {
if (hasBody) {
//往 connect socket中写入body数据
[self writeBodyWithCompletionBlock:^(BOOL successInner) {
//处理 _reader 读取完后的清理工作
[_response performClose]; // TODO: There's nothing we can do on failure as headers have already been sent
}];
}
} else if (hasBody) {
//处理 _reader 读取完后的清理工作
[_response performClose];
}
}];
} else {
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
}
}
往 connect socket 中写入body数据
- (void)writeBodyWithCompletionBlock:(WriteBodyCompletionBlock)block {
GWS_DCHECK([_response hasBody]);
//从 _response 中读取出body数据
[_response performReadDataWithCompletion:^(NSData* data, NSError* error) {
if (data) {
if (data.length) {
//如果需要使用 chunked 传输编码(header 中 Transfer-Encoding: chunked)
if (_response.usesChunkedTransferEncoding) {
//以 chunked 要求的格式对 data 进行编码
const char* hexString = [[NSString stringWithFormat:@"%lx", (unsigned long)data.length] UTF8String];
size_t hexLength = strlen(hexString);
NSData* chunk = [NSMutableData dataWithLength:(hexLength + 2 + data.length + 2)];
if (chunk == nil) {
GWS_LOG_ERROR(@"Failed allocating memory for response body chunk for socket %i: %@", _socket, error);
block(NO);
return;
}
char* ptr = (char*)[(NSMutableData*)chunk mutableBytes];
bcopy(hexString, ptr, hexLength);
ptr += hexLength;
*ptr++ = '\r';
*ptr++ = '\n';
bcopy(data.bytes, ptr, data.length);
ptr += data.length;
*ptr++ = '\r';
*ptr = '\n';
data = chunk;
}
//写入至connect socket
[self writeData:data
withCompletionBlock:^(BOOL success) {
if (success) {
//只要当次读取有数据就会一直读,直到读不到数据后,添加 _lastChunkData
[self writeBodyWithCompletionBlock:block];
} else {
block(NO);
}
}];
} else {
if (_response.usesChunkedTransferEncoding) {
//在使用 chunked 传输编码情况下,尾部需要添加一个0字节chunk块
[self writeData:_lastChunkData
withCompletionBlock:^(BOOL success) {
block(success);
}];
} else {
block(YES);
}
}
} else {
GWS_LOG_ERROR(@"Failed reading response body for socket %i: %@", _socket, error);
block(NO);
}
}];
}
写入完成后,服务端便会将连接关闭。(目前GCDWebServer
并未实现Keep-Alive
)