근 4 달만에 이어지는 글이네요.
이번편은 단순하게 전 글들에서 만든 ServerSocket과 ClientSocket을 객체화 시켜 연결하는 과정이기 때문에 어려울 것도 없어 최대한 짧게 쓰려고 합니다.
CFSocket에 관한 제일 첫번째 글에 나왔던 이 이미지를 다시 한번 상기시켜 봅시다.
ServerSocket을 만든 목적은 Avplayer에서 request한 Network를 intercept하기 위함이었고 ClientSocket이 intercept한 url을 대신 remote Server에 요청하여 데이터를 기기에 저장 및 serverSocket을 거쳐 avplayer에 반환하는 것이 전체 과정입니다.
5. ProxyServer 만들기
class ProxyServer {
private var serverSocket: ServerSocket
private var clientSocket : ClientSocket?
init(port: Int) throws {
serverSocket = try ServerSocket(port: UInt16(port))
super.init()
}
func start() {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let self = self else { return }
self.manageClientSocket()
}
}
private func manageClientSocket(){
do {
self.clientSocket = try self.serverSocket.acceptClientSocket()
self.handleRequest(clientSocket: self.clientSocket)
} catch {
debugLog("[ProxyHTTPServer] Error accepting connection: \(error)")
}
}
private func handleRequest(clientSocket: ClientSocket?) {
guard let clientSocket = clientSocket else {
debugLog("[ProxyHTTPServer] clientSocket lost in handle Request")
return
}
do {
guard let requestData = try clientSocket.receive(maxLength: 50000) else {
self.closeClientSocket()
return
}
guard let requestDataString = String(data: requestData, encoding: .utf8) else {
self.sendDataWithClientSocket(data: Data())
return
}
if requestDataString.components(separatedBy: " ").filter({ $0.contains("m3u8") }).isEmpty == false {
playListHandler(requestData: requestData) { [weak self] data in
guard let self = self else { return }
self.sendDataWithClientSocket(data: data)
}
}
else {
self.tsFileHandler(requestData: requestData) { [weak self] data in
self?.sendDataWithClientSocket(data: data)
}
}
}
catch(let error) {
debugLog("[ProxyHTTPServer] handleRequest error \(error.localizedDescription)")
if let error = error as? SocketError, error == .SocketWriteFailed {
debugLog("[ProxyHTTPServer] trying to restore on write failure")
self.start()
}
}
}
private func sendDataWithClientSocket(data : Data) {
do {
try clientSocket?.send(data: data)
self.closeClientSocket()
}
catch(let error) {
debugLog("[ProxyHTTPServer] send data error \(error.localizedDescription)")
}
}
func closeClientSocket(){
self.clientSocket?.close()
self.clientSocket = nil
self.manageClientSocket()
}
}
중요한 함수들만 나열해 보았습니다.
함수들이 어떻게 작동하는 지 순서대로 간단하게 설명하겠습니다.
1. init을 하면서 ServerSocket의 port를 설정하고 ServerSocket을 가동시킵니다.(하나의 앱 안에 같은 port의 다중 server socket은 생성이 불가합니다. 다중 ServerSocket을 만들시 다른 port를 적용해야 합니다.).
2.backgroundQueue에서 start를 돌리며 server socket이 client socket의 요청을 받을 때까지 기다립니다.
이때 mainQueue에서 이작업을 진행하시면 UI가 정지되는 현상을 경험할 수 있습니다.
3. ClientSocket을 성공적으로 반환받고 나서는 avplayer에서 ClientSocket으로 Network request가 들어오기 시작합니다.
이때 handleRequest에서 avplayer의 NetworkRequest를 분석하여 어떤 Url을 실제 Remote Server에 요청하여 데이터를 받아올건지 작업합니다.
HSL caching의 경우는 M3U8을 요청하고 받아온 데이터의 tsFile의 경로는 proxyServer주소로 변환하여 avplayer가 ts segment를 요청할때 다시 proxyServer로 요청하게 만들어야 합니다.
몇가지 단계가 더 있지만 이부분은 순전히 String을 어떻게 가공할지의 케바케 영역이므로 자세하게 다루지 않겠습니다.
4. ClientSocket이 실제 데이터를 받아오면 내부 디비에 저장함과 동시에 avplayer에게 데이터를 넘겨줍니다.
5. 데이터 저장 작업과 반환 작업이 끝났으면 메모리 관리를 위하여 ClientSocket을 닫아줍니다.
현재 ClientSocket을 닫아주면서 새로운 ClientSocket을 받을 준비를 합니다.
생각보다 그냥 일반적인 로직이라 딱히 어려울건 없다고 봅니다.
여기서 제일 중요한건 Socket들을 어떻게 관리하느냐와 originUrl을 어떻게 parsing하여 proxyUrl로 만드느냐가 관건이라고 생각합니다.
참고로 proxyServer Url 주소는 다음과 같습니다.
components.scheme = "http"
components.host = "127.0.0.1"
components.port = self._currentPortNumber
따라서 RemoteServer에서 받은 Url을 http://127.0.0.1:(port)?_originUrl_식으로 바꾸어서 avplayerItem을 만들어줘야 합니다.
요정도까지가 ProxyServer 만드는 방법이 끝입니다. 물론 더욱더 고도화된 작업들을 할 수 있겠죠. 예를 들어 한번에 다중 요청이 들어왔을때 throttling 작업이라든가. 백그라운드 작업이라던가. 하지만 어떻게 동작하는지의 코어 원리는 글에서 쓴것과 다르지 않을 것 이라 생각합니다.
뭔가 되게 어영부영 끝나게 되었지만 모쪼록 도움이 되었기를 바라며 금편 CFSocket에 대한 episode는 여기서 마무리 짓겠습니다.
감사합니다.
'iOS' 카테고리의 다른 글
CFSocket을 통한 로컬 서버 만들기 5 (0) | 2024.05.02 |
---|---|
[Swift] AVAssetDownLoadTask 사용하기 (1) | 2023.12.03 |
CFSocket을 통한 로컬 서버 만들기 3 (0) | 2023.06.26 |
CFSocket을 통한 로컬 서버 만들기 2 (0) | 2023.06.26 |
CFSocket을 통한 로컬 서버 만들기 1 (0) | 2023.05.29 |