iOS 移動開發網路 part5.6:CocoaAsyncSocket^write

weixin_34208283發表於2018-01-03

寫入的部分會比讀取的部程式碼簡單很多.

- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;

CocoaAsyncSocket提供的寫入方法只有上面這一個.

按照已經知道的讀取資料的套路也可以大概推出寫入的大概過程.寫入:建GCDAsyncWritePacket的包加入writeQueue,呼叫maybeDequeueWrite.maybeDequeueWrite進而再呼叫doWriteData.

doWriteDatadoReadData一樣,在正式做讀取之前,都會有一些規避判斷(如:flag表示現在正處於開啟加密的階段,則要呼叫ssl_continueSSLHandshake),這些在doReadData內已經說過了,在這就不再贅述了.

doWriteData的寫入也會被分成三個模組:正常寫入,CFStream For TLS寫入,SSL For TLS寫入.按照前面的套路我們也就知道SSL For TLS寫入是最複雜的.

三個模組進行之後的善後判斷也是一樣的,如圖:

1691771-23d6a4ad28e70a07.png
CocoaAsyncSocket doWriteData.png

1.正常寫入

int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;

const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone;

NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone;

if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
{
    bytesToWrite = SIZE_MAX;
}
//zc read3:socket寫
ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite);
LogVerbose(@"wrote to socket = %zd", result);

// Check results
if (result < 0)
{
    if (errno == EWOULDBLOCK)
    {
        waiting = YES;
    }
    else
    {
        error = [self errnoErrorWithReason:@"Error in write() function"];
    }
}
else
{
    bytesWritten = result;
}

單個包寫入完畢的標準是:包所帶的資料量等於寫入的資料量.

write()後:

result < 0:
  (errno == EWOULDBLOCK)==>waiting = YES,需要等待doWriteData的再一次呼叫,進入善後判斷;
  其他==>生成報錯,進入善後判斷.
result >= 0:
  bytesWritten = result,進入善後判斷.

等待doWriteData的再一次呼叫也就是writeSource的監聽再一次觸發,開啟監聽程式碼如下:

//取消可以接受資料的標記,開啟對writeSource的監聽(有空間可寫立刻呼叫doWriteData)
flags &= ~kSocketCanAcceptBytes;

if (![self usingCFStreamForTLS])
{
    [self resumeWriteSource];
}

2.CFStream For TLS寫入

const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone;

NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone;

if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
{
    bytesToWrite = SIZE_MAX;
}

CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite);
LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result);

if (result < 0)
{
    error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream);
}
else
{
    bytesWritten = (size_t)result;
    
    // We always set waiting to true in this scenario.
    // CFStream may have altered our underlying socket to non-blocking.
    // Thus if we attempt to write without a callback, we may end up blocking our queue.
    waiting = YES;
}
CFWriteStreamWrite()後:

result < 0:
  生成報錯,進入善後判斷.
result >= 0:
  bytesWritten = result,waiting = YES,進入善後判斷.

CFStream For TLS寫入不用writeSourcebsd socket進行監聽,所以不用開啟writeSource的監聽.

if (![self usingCFStreamForTLS])
{
    [self resumeWriteSource];
}

可以看到CFStream寫入的邏輯特別簡單.這是因為CFStream內部會自己控制bsd socket傳輸資料,我們在外圍只呼叫一次CFWriteStreamWrite()就夠了,所以CFStream寫入資料非常非常簡單.

3.SSL For TLS寫入

OSStatus result;

BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0);
BOOL hasNewDataToWrite = YES;

if (hasCachedDataToWrite)
{
    size_t processed = 0;
    
    result = SSLWrite(sslContext, NULL, 0, &processed);
    
    if (result == noErr)
    {
        bytesWritten = sslWriteCachedLength;
        sslWriteCachedLength = 0;
        
        if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten))
        {
            // We've written all data for the current write.
            hasNewDataToWrite = NO;
        }
    }
    else
    {
        if (result == errSSLWouldBlock)
        {
            waiting = YES;
        }
        else
        {
            error = [self sslError:result];
        }
        
        // Can't write any new data since we were unable to write the cached data.
        hasNewDataToWrite = NO;
    }
}

if (hasNewDataToWrite)
{
    const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes]
    + currentWrite->bytesDone
    + bytesWritten;
    
    NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten;
    
    if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
    {
        bytesToWrite = SIZE_MAX;
    }
    
    size_t bytesRemaining = bytesToWrite;
    
    BOOL keepLooping = YES;
    while (keepLooping)
    {
        const size_t sslMaxBytesToWrite = 32768;
        size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite);
        size_t sslBytesWritten = 0;
        
        result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten);
        
        if (result == noErr)
        {
            buffer += sslBytesWritten;
            bytesWritten += sslBytesWritten;
            bytesRemaining -= sslBytesWritten;
            
            keepLooping = (bytesRemaining > 0);
        }
        else
        {
            if (result == errSSLWouldBlock)
            {
                waiting = YES;
                sslWriteCachedLength = sslBytesToWrite;//沒寫給對面,寫入了sslContext內,需要記錄sslWriteCachedLength
            }
            else
            {
                error = [self sslError:result];
            }
            
            keepLooping = NO;
        }
        
    } // while (keepLooping)
    
} // if (hasNewDataToWrite)

SSL For TLS寫入的程式碼很清晰的分成了兩塊:寫入快取的資料,寫入包內的資料(寫入都有快取,厲害了吧!).我們先說寫入包內的資料,再說寫入快取的資料.

  • 寫入包內的資料

單次呼叫SSLWrite()寫入資料的大小是MIN(bytesRemaining, sslMaxBytesToWrite)(包內所剩資料量ssl能寫的最大資料量小的那一個).

SSLWrite()後:
沒報錯:就迴圈呼叫SSLWrite()做寫入,直到寫完;
擁塞報錯:把sslBytesToWrite的計給sslWriteCachedLength,停止迴圈,進入善後判斷,需要等待doWriteData的再一次呼叫;(這就是ssl快取的由來)
其他報錯:停止迴圈,生成報錯,進入善後判斷.

正常寫入SSL For TLS寫入用的都是writeSourcebsd socket的監聽(上篇文章反覆說,這就不贅述了).

  • 寫入快取的資料
SSLWrite()後:
沒報錯:寫入的資料量等於sslWriteCachedLength,再進行寫入包內的資料;
擁塞報錯:不再寫入包內的資料,waiting = YES,需要等待doWriteData的再一次呼叫,進入善後判斷;
其他報錯:不再寫入包內的資料,生成報錯,進入善後判斷;

SSLWrite()方法流程如下:

SSLWrite()
  SSLWriteFunction()
    - (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength
- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength
{
    if (!(flags & kSocketCanAcceptBytes))
    {
        // Unable to write.
        // 
        // Need to wait for writeSource to fire and notify us of
        // available space in the socket's internal write buffer.
        
        [self resumeWriteSource];
        
        *bufferLength = 0;
        return errSSLWouldBlock;
    }
    
    size_t bytesToWrite = *bufferLength;
    size_t bytesWritten = 0;
    
    BOOL done = NO;
    BOOL socketError = NO;
    
    int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;
    
    ssize_t result = write(socketFD, buffer, bytesToWrite);
    
    if (result < 0)
    {
        if (errno != EWOULDBLOCK)
        {
            socketError = YES;
        }
        
        flags &= ~kSocketCanAcceptBytes;
    }
    else if (result == 0)
    {
        flags &= ~kSocketCanAcceptBytes;
    }
    else
    {
        bytesWritten = result;
        
        done = (bytesWritten == bytesToWrite);
    }
    
    *bufferLength = bytesWritten;
    
    if (done)
        return noErr;
    
    if (socketError)
        return errSSLClosedAbort;
    
    return errSSLWouldBlock;
}

SSL For TLS寫入最終轉換成bsd socket的寫入,程式碼也是一目瞭然,就不多做解釋了.

相關文章