iOS

CFSocket을 통한 로컬 서버 만들기 3

스엠 2023. 6. 26. 23:02

전글에서 Server Socket을 간단하게 만들어 보았고 이번에는 ClientSocket을 만들어 보겠습니다.

 

4. ClientSocket 만들기

먼저 풀 코드부터 보여 드리겠습니다.

class ClientSocket : NSObject {
    var fileDescriptor: Int32
    private var inputStream: InputStream!
    private var outputStream: OutputStream!
    private var address : sockaddr!
   
    
    
    init(fileDescriptor: Int32, address: sockaddr) throws {
        var readStream : Unmanaged<CFReadStream>?
        var writeStream: Unmanaged<CFWriteStream>?
        self.fileDescriptor = fileDescriptor
        self.address = address
        CFStreamCreatePairWithSocket(nil, fileDescriptor, &readStream, &writeStream)
        inputStream = readStream!.takeRetainedValue()
        outputStream = writeStream!.takeRetainedValue()
        
        inputStream.open()
        outputStream.open()
        super.init()
        
        outputStream.delegate = self
        inputStream.delegate = self
    }
    
    func send(data: Data) throws {
            var fileData : Data = data
            let preferredBufferSize : Int = 512 * 4
            while fileData.count > 0 {
                if (self?.outputStream.hasSpaceAvailable ?? false) == false {
                    break
                }
                var writeData : Data
                var writeBufferSize : Int
                if fileData.count > preferredBufferSize {
                    writeBufferSize = preferredBufferSize
                }
                else {
                    writeBufferSize = fileData.count
                }
                writeData = fileData.prefix(writeBufferSize)
                fileData = fileData.dropFirst(writeBufferSize)
                if (self?.outputStream.write(data: writeData) ?? -1) < 0 {
                    break
                }
            }
    }
    
    func receive(maxLength: Int) throws -> Data? {
        var buffer = [UInt8](repeating: 0, count: maxLength)
        let bytesRead = inputStream.read(&buffer, maxLength: maxLength)
        guard bytesRead >= 0 else {
            throw SocketError.SocketReadFailed
        }
        return bytesRead > 0 ? Data(bytes: buffer, count: bytesRead) : nil
    }
    
    func close() {
        self.inputStream.close()
        self.outputStream.close()
        Darwin.close(self.fileDescriptor)
    }
    
}

 

init 부터 쪼개 봅시다.

    init(fileDescriptor: Int32, address: sockaddr) throws {
        var readStream : Unmanaged<CFReadStream>?
        var writeStream: Unmanaged<CFWriteStream>?
        self.fileDescriptor = fileDescriptor
        self.address = address
        CFStreamCreatePairWithSocket(nil, fileDescriptor, &readStream, &writeStream)
        inputStream = readStream!.takeRetainedValue()
        outputStream = writeStream!.takeRetainedValue()
        
        inputStream.open()
        outputStream.open()
        super.init()
        
        outputStream.delegate = self
        inputStream.delegate = self
    }

가장 먼저 보이는 것이 readStream과 writeStream입니다.
말그대로 데이터를 읽는 것과 쓰는 것을 담당하는 Stream 객체 입니다.

그리고 나오는게 CFStreamCreatePairWithSocket인데요. 요거에 대해서 설명을 해보겠습니다.
CFStreamCreatePairWithSocket의 함수 원형은 다음과 같습니다.

func CFStreamCreatePairWithSocket(_ alloc: CFAllocator?, _ sock: CFSocketNativeHandle, _ readStream: UnsafeMutablePointer<Unmanaged<CFReadStream>?>?, _ writeStream: UnsafeMutablePointer<Unmanaged<CFWriteStream>?>?)

함수의 정의 자체는 xcode에 다음과 같이 나타져 있습니다.

/* Socket streams; the returned streams are paired such that they use the same socket; pass NULL if you want only the read stream or the write stream */
: 반환 받은 stream은 마치 같은 소켓을 이용하는 것 처럼 pairing되어 있다. 
쉽게 말하면 그냥 소켓 기반의 읽기 및 쓰기 스트림을 생성하는 데 사용된다고 보시면 됩니다.

  • 첫번째 인자 (alloc): 메모리를 할당자를 나타내는 변수 입니다. 일반적으로 nil을 전달하여 기본 할당자를 사용합니다.
    종류 로써는 cfallocatorDefault,cfallocatorSystemDefault, CFAllocatorMalloc등 이 있습니다.
  • 두번째 인자(sock): 소켓의 파일 디스크립터 입니다. 우리는 serverSocket에서 생성한 fileDescriptor를 할당합니다.
  • 세번째,네번째 인자: 읽기 쓰기 Stream을 할당하는 곳입니다. 

이렇게 읽기와 쓰기가 가능하게끔 만들고 각각의 스트림을 .takeRetainedValue()를 통하여 
메모리를 잡아놓은 다음에 open()을 이용하여 스트림을 열어 놓으면 읽기와 쓰기를 하기 위한 준비 끝입니다.

이제 데이터를 수신하는 것에 대해서 알아보겠습니다.

    func receive(maxLength: Int) throws -> Data? {
        var buffer = [UInt8](repeating: 0, count: maxLength)
        let bytesRead = inputStream.read(&buffer, maxLength: maxLength)
        guard bytesRead >= 0 else {
            throw SocketError.SocketReadFailed
        }
        return bytesRead > 0 ? Data(bytes: buffer, count: bytesRead) : nil
    }

receive함수를 통해서 데이터를 수신합니다. maxlength의 경우 수신 받을 데이터의 총 크기를 정하는 인자입니다.
위에서 생성한 inputStream을 활용하여 buffer라는 변수에다가 데이터를 축적시킵니다.
읽기를 실패할 경우 .read함수가 -1을 반환하여 읽기에 실패했다고 알려줍니다.
bytesRead가 0 이상일 경우 Data(bytes:,count:)생성자를 통하여 [UInt8]을 Data 타입으로 변형 시킨 후 반환해 줍니다.
해당 함수의 경우 나중에 ProxyServer라는 클라스에서 사용됩니다.

데이터를 쓰는 법입니다.

    func send(data: Data) throws {
            var fileData : Data = data
            let preferredBufferSize : Int = 512 * 4
            while fileData.count > 0 {
                if (self?.outputStream.hasSpaceAvailable ?? false) == false {
                    break
                }
                var writeData : Data
                var writeBufferSize : Int
                if fileData.count > preferredBufferSize {
                    writeBufferSize = preferredBufferSize
                }
                else {
                    writeBufferSize = fileData.count
                }
                writeData = fileData.prefix(writeBufferSize)
                fileData = fileData.dropFirst(writeBufferSize)
                if (self?.outputStream.write(data: writeData) ?? -1) < 0 {
                    break
                }
            }
    }

로직만 간단하게 넘겨 짚고 가도록 합시다.
while문을 넣은 이유는 한방에 큰 데이터를 write할려고 할시 가끔가다가 stream이 터지는 경우가 발생해서 while문으로 쪼개어 보냈습니다.

preferredBufferSize의 경우 512 * 4로 한 page단위로 넘기는 것을 권장사항으로 설정하고 있습니다.
이는 apple에서 bufferSize를 reasonable하게 설정하라고 권하는 것을 적용한 값입니다.

while문 내부의 로직은 다음과 같습니다.
1. outputStream에 할당 공간이 남아있는지 확인합니다.
2. 남아있는 데이터가 preferredBufferSize보다 크면 preferredBufferSize만큼만 보냅니다.
3. 원래의 데이터에서 preferredBufferSize만큼 앞에서부터 제한 후 다시 원본 데이터에 할당합니다.
4. 원본 데이터가 0이 될때까지 반복합니다.
쉽죠?

이렇게 하면 socket을 이용하여 데이터를 쓰기까지 끝입니다.

그리고 어떻게 보면 가장 중요한 소켓을 close하는 법에 대해서 보겠습니다.

    func close() {
        self.inputStream.close()
        self.outputStream.close()
        Darwin.close(self.fileDescriptor)
    }

당연히 inputStream,outputStream을 close합니다.
그리고 중요한게 Darwin.close함수 인데요.

이것 말고 shutDown(fileDescriptr,SHUT_RDWR) 이런식의 함수가 있습니다.
이때 shutDown의 함수의 경우 해당 소켓 주소에 대하여 읽기와 쓰기 권한만 닫아버리고 주소는 할당한채 갖고 있습니다.
이는 무엇을 의미하냐? 시스템 내부적으로 IPv4에 대한 모든 할당 가능한 주소를 썻을때 그 다음 부터는 소켓을 생성 못한 다는 것입니다.
따라서 소켓 자체를 release 해버리기 위해서 Darwin.close함수를 써야 합니다. 

여기까지가 Client Socket을 만드는 방법이었습니다.
다음은 ServerSocket과 ClientSocket을 연결하고 데이터를 외부와 주고 받을 수 있게 해주는 ProxyServer 클라에 대해서 이어가겠습니다.