새로운 회사에 취직한지 어언 1달이 되었습니다.
새롭게 취직한 회사는 라이브 스트리밍 솔루션을 제공하는 업체이다 보니 라이브 스트리밍과 관련된 기술들을 많이 배우고 접하게 되었습니다.
이러한 와중에 iOS에서 hls 라이브 스트리밍을 캐싱해야 하는 과제를 부여받았습니다.
단순 mp4 캐싱이었으면 얼마나 좋았겠습니까? ㅋㅋㅋㅋ
근데 이놈의 애플은 hls 캐싱을 지원해주지 않더군요....
그렇다고 또 못하는 것도 아니고 결국 삽질을 하게 되었습니다.
방법은 제목에 쓴 것처럼 앱 안에 로컬 서버를 만들고 이를 통해 외부와 소통하면서 m3u8파일과 ts파일을 추출해서 저장해야 합니다.
일단 가장 간단한 방법으로는 바로 그냥 라이브러리 쓰시면 됩니다.
https://github.com/StyleShare/HLSCachingReverseProxyServer
참 쉽죠?
하지만 이렇게 했으면 제가 블로그를 쓰고 있지 않았겠죠 ㅎㅎ
저희 회사는 외부 라이브러리를 쓰면 안된다는 방침이 있어서 결국 처음부터 scratch로 홀로 짜게 되었습니다.
처음부터 짜면서 많은 검색을 해봤는데 생각보다 자료가 없다고 느껴서 가장 기본적인 형태의 로컬 서버 정도를 만드는 방법을 기술할 생각입니다. 게다가 pure Swift입니다. 자 서론을 마치고 천천히 들어가 봅시다.
1.CFSocket
일단 로컬 서버를 만들려면 CFSocket이라는 놈부터 알아야 됩니다.
애플 문서왈 CFSocket은 다음과 같은 놈이라고 합니다.
A CFSocket is a communications channel implemented with a BSD socket.
우리가 흔히 아는 BSD 소켓을 Swift로 래핑 해놓은 놈입니다.
2.서버 구성하기
본격적으로 만들기 전에 제가 구성할 방식의 서버를 대략적으로 설명해 드리겠습니다.
서버 구성도는 아래와 같이 되어 있습니다.
avplayer로 부터 request를 받으면 로컬 서버 내부에서 request를 파싱하여 리모트 서버에 요청한 후 반환 받은 데이터를 avplayer에 돌려줍니다.
3. CFSocket 만들기
구성도도 다 나왔겠다 이제 소켓 부터 만들어야 합니다.
class ServerSocket {
private let port: UInt16
private var socket: CFSocket?
private var fileDescriptor : CFSocketNativeHandle?
func start() throws {
let sock = CFSocketCreate(kCFAllocatorDefault,
AF_INET, SOCK_STREAM,
IPPROTO_TCP,
0,nil,nil)
guard sock != nil else {
throw SocketError.SocketCreationFailed
}
var reuse = true
fileDescriptor = CFSocketGetNative(sock!)
guard let fileDescriptor = fileDescriptor else {
throw SocketError.FileDescriptionCreationFailed
}
if setsockopt(fileDescriptor, SOL_SOCKET, SO_REUSEADDR, &reuse, socklen_t(MemoryLayout<Int>.size)) < 0 {
throw SocketError.SocketCreationFailed
}
var addr = sockaddr_in(sin_len: __uint8_t(MemoryLayout<sockaddr_in>.size),
sin_family: sa_family_t(AF_INET),
sin_port: port.bigEndian,
sin_addr: in_addr(s_addr: INADDR_ANY),
sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
let addrData = withUnsafePointer(to: &addr) { ptr in
return ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { addrPtr in
return Data(bytes: addrPtr, count: MemoryLayout<sockaddr_in>.size)
}
}
let result = CFSocketSetAddress(sock!, addrData as CFData)
guard result == .success else {
throw SocketError.SocketBindingFailed
}
let source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, sock!, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, .commonModes)
socket = sock
}
}
변수 부터 설명 드리자면
- port : 말 그대로 포트 번호 입니다.
- socket : 앞서 말씀드린 소켓입니다.
- fileDescriptor : 애가 좀 애매합니다. 애플 문서에 검색을 해봐도 다음과 같이 나옵니다.
Type for the platform-specific native socket handle.
: 플랫폼별을 위한 네이티브 소켓 핸들러? 대체 뭔말이여
근데 추측을 해보자면 일단 이녀석의 타입이 Int32이 이고 이녀석을 가지고 소켓 설정을 하는 것을 알 수 있습니다.
따라서 저는 이 것을 소켓의 주소를 가지고 있는 녀석이다. 정도로 이해하고 있습니다.
이제 start 함수 안으로 들어가 보겠습니다.
첫 단계 소켓을 만들기 입니다.
let sock = CFSocketCreate(kCFAllocatorDefault,
AF_INET, SOCK_STREAM,
IPPROTO_TCP,
0,nil,nil)
guard sock != nil else {
throw SocketError.SocketCreationFailed
}
CFSocketCreate의 인자들을 하나씩 소개 하겠습니다.
- 0번째 인자 : memory를 어떤방식으로 적재할건지 옵션을 선택하는 인자입니다.
애플 문서에서 CFAllocator를 찾아보시면 다른 옵션들도 볼 수 있지만 kCFAllocatorDefault로 거의 고정되어 사용됩니다. - 1번째 인자 : 어떤 종류의 네트워크 프로토콜 패밀리를 쓸지 선택하는 인자입니다. default는 PF_INET으로 설정 되어 있습니다.
PF_INET은 Protocol Family의 약자이고 AF_INET은 Address Family의 약자입니다.
둘의 차이가 뭐냐?
PF는 실제 연결을 하기 위한 ‘프로토콜’을 지정할때 쓰는 것을 권장하고
AF는 소켓의 주소와 함께 ‘주소 체계’를 지정할때 쓰는 것을 권장한다고 합니다.
하지만 둘이 원초적으로 다른게 아니고 의미를 구분하기 위해서 있는 것이라 싹다 AF로 쓰기를 권장하고 있다는 정보도 있습니다.
물론 이 두옵션만 있는 것도 아니면 xcode에서 점프해서 들어가시면 이거저것 옵션이 엄청 많습니다. - 2번째 인자: 이것은 소켓의 타입을 설정할 수 있는 인자입니다. SOCK_STREAM이 디폴트 이며 말그대로 스트림 소켓 타입을 나타냅니다.
이것 역시 SOCK_DGRAM, SOCK_RAW,SOCK_RDM등등 여러종류의 타입이 있고 xcode에서 확인이 가능합니다. - 3번째 인자: 1번째 인자가 프로토콜 패밀리를 선택하는 것이었다면 이 인자는 어떤 프로토콜을 쓸지 선택하는 인자입니다.
기본적으로 SOCK_STREAM의 경우 tcp로 지정되어 있습니다. - 4번째 인자: 4번째 인자는 socket에서 이벤트가 발생하는데 이때 어떠한 이벤트들을 callBack으로 받을지 설정하는 인자입니다.
자세한 콜백 타입들은 https://developer.apple.com/documentation/corefoundation/cfsocketcallbacktype 여기에서 보시면 됩니다. - 5번째 인자: callBack이 발생했을때 받기위한 인자 입니다. closure의 형태를 갖고 있습니다.
역시 자세한건 링크를 통해... https://developer.apple.com/documentation/corefoundation/cfsocketcallback 보시면 됩니다. - 6번째 인자: socket에대한 정보를 담을수 있는 포인터 인자입니다. UnsafePointer<CFSocketContext>로 구성되어 있습니다.
어휴 CFSocketCreate 함수 하나만으로도 분량이 꽤 나오네요.
이제 소켓을 만들었으니 옵션을 설정해야 합니다.
그리고 옵션을 설정하기 전에 우리가 만든 소켓의 위치를 가지고 와야 합니다.
이 작업이 다음 코드를 통해서 이루어집니다.
var reuse = true
fileDescriptor = CFSocketGetNative(sock!)
guard let fileDescriptor = fileDescriptor else {
throw SocketError.FileDescriptionCreationFailed
}
if setsockopt(fileDescriptor, SOL_SOCKET, SO_REUSEADDR, &reuse, socklen_t(MemoryLayout<Int>.size)) < 0 {
throw SocketError.SocketCreationFailed
}
CFSocketGetNative함수를 통해 바로 전에 생성한 socket의 위치를 가져옵니다.
그리고 가져온 위치를 토대로 setsockopt함수를 통하여 소켓 옵션을 설정합니다.
setsockopt의 인자는 다음과 같습니다.
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)
- 0번째 인자: 어떤 소켓을 설정할지 받는 인자 입니다.
- 1번째 인자 : 옵션의 프로토콜 레벨을 지정합니다. 예를 들어, SOL_SOCKET은 소켓 레벨 수준의 옵션을 설정하고, IPPROTO_TCP는 TCP 레벨 수준의 옵션을 설정합니다.
- 2번째 인자: 설정하려는 옵션의 식별자입니다. 예를 들어, SO_REUSEADDR은 주소 재사용 옵션입니다. 이것도 xcode안에서 여러가지 옵션을 확인할 수 있습니다.
- 3번째 인자: 설정값을 저장하기 위한 버퍼의 포인터
- 4번째 인자: 버퍼의 크기입니다.
여기까지 하면 소켓 객체를 생성하고 소켓의 설정까지 끝났습니다.
이제 소켓의 주소를 설정해야 합니다.
이건 다음글에 이어 쓰겠습니다. 힘들어서....
'iOS' 카테고리의 다른 글
CFSocket을 통한 로컬 서버 만들기 3 (0) | 2023.06.26 |
---|---|
CFSocket을 통한 로컬 서버 만들기 2 (0) | 2023.06.26 |
Rx와 XCTest를 이용할시 생기는 에러사항들 3 (0) | 2023.03.27 |
Rx와 XCTest를 이용할시 생기는 에러사항들 2 [missing Required module "RxCocoaRuntime"] (0) | 2023.03.27 |
Rx와 XCTest를 이용할시 생기는 에러사항들 1 [library not loaded XCTest] (0) | 2023.03.26 |