---
# System prepended metadata

title: Day16 미션(파일 데이터관리Ⓜ️)

---

53-B, S019, S054

# Day18 미션(챌린지 GPT 서버)

## 체크리스트
- [x] TCP 서버 동작 및 Network Framework 학습
- [x] Telnet, 소켓 옵션 학습
- [x] TCP 에코 서버 설계하기 
- [x] SocketIO 구현하기
- [x] Client 객체를 구현하기
- [x] 여러개의 Client 연결이 가능하도록 구현하기
- [x] Client 그룹 짓기
- [x] Summary 구현하기
- [x] Chat 구현하기
- [x] Broadcast 구현하기
- [x] Direct 구현하기
- [x] !history 구현하기
- [x] Server Log 구현하기
- [ ] 디테일 추가하기
    - [ ] 응답 실패
    - [ ] checkin S001~S256 범위 체크, 벗어난 경우 재입력
    - [ ] checkin 중복 불가능
    - [ ] checkout 활동시간 출력
    - [ ] checkout TCP 연결 해제 시에도 적용


## 문제 해결 과정

### 미션 1

- TCPSocketIO 구현

```swift
final class TCPSocketIO {
    private var listener: NWListener?
    
    init() {
        do {
            let parameters = NWParameters.tcp
            parameters.allowLocalEndpointReuse = true
            
            guard let nwPort = NWEndpoint.Port(rawValue: 2024) else { return }
            listener = try NWListener(using: parameters, on: nwPort)
            
            //.. 중략
            
            /// telnet 명령어를 통해 새로운 Client가 들어오면 호출되는 클로저
            listener?.newConnectionHandler = { [weak self] newConnection in
                self?.handleConnection(newConnection)
            }
            
            listener?.start(queue: .global())
        }
    }
    
    /// Client에게 요청을 받고, echo 응답을 주는 메서드
    private func handleConnection(_ connection: NWConnection) {
        connection.start(queue: .global())
        
        // 요청을 클라이언트로부터 receive
        connection.receive(minimumIncompleteLength: 4, maximumLength: 1024) { data, context, isComplete, error in
                                                                             
            //.. 중략
            
            if let data = data, !data.isEmpty {
                guard let message = String(data: data, encoding: .utf8) else { return connection.cancel() }
                print("Received message: \(message)")
                
                /// 응답 다시 클라이언트에게 send
                connection.send(
                    content: data,
                    completion: .contentProcessed { sendError in
                        if let sendError = sendError {
                            print("Send error: \(sendError)")
                        }
                    }
                )
            }
                        
            /// 다시 입력을 받기 위해 재귀로 호출
            self.handleConnection(connection)
        }
    }
    
    /// 프로세스가 종료 되었을 때, listener 해제
    deinit {
        listener?.cancel()
    }
}
```

### 미션 2

- 설계

<img width="1000" src="https://gist.github.com/user-attachments/assets/60d62588-aecb-4a61-a537-d795a7fcf0f0">

- 결과 캡쳐

<img width="1500" src="https://gist.github.com/user-attachments/assets/a0b084a1-e568-4dcc-81f9-7b99a739151b">

- 구현 내용

```swift
class Client {
    let id: String // 캠퍼 아이디
    let connection: NWConnection
    let checkInDate: Date
    let session: Int
    let group: Int // 그룹 번호
    var inputCommandList: [String]
    
    //.. init() 중략
}

// 입력받을 수 있는 명령어를 Case로 분류한 Enum Type
enum Command: String {
    case checkin
    case checkout
    case summary
    case chat
    case finish
    case broadcast
    case direct
    case clap
    case history = "!history"
}

// summary 명령어 입력받은 날을 Case로 분류한 Enum Type
enum Summary: String {
    case day1, day2, day3, day4, day6, day7, day8, day9, day11, day13, day16, day18
    
    var keyword: String {
        switch self {
        case .day1: "Xcode, Swift"
        case .day2: "Linux, System"
        case .day3: "XML, JSON"
        case .day4: "Heap, Stack"
        case .day6: "Object, Class"
        case .day7: "File Path, UnitTest"
        case .day8: "Immutable, Closure"
        case .day9: "Event, Publisher"
        case .day11: "Async, EventLoop"
        case .day13: "Git, Objects"
        case .day16: "HTTP, SQL"
        case .day18: "Network, Server"
        }
    }
}
```

- TCPSocketIO 기능 확장

```swift
final class TCPSocketIO {
    private var listener: NWListener?
    private var clients = [Client]()
    private var groupChatCount: [Int: (client: Client, count: Int)] = [:]
    private var claps = 0
    
    init() {...}
    
    /// Client에게 요청을 받고, 명령어에 따라 처리해주는 메서드
    private func handleConnection(_ connection: NWConnection) {
        connection.start(queue: .global())
        
        /// 최대길이 1024, 최소 길이 4로 지정
        connection.receive(minimumIncompleteLength: 4, maximumLength: 1024) { data, context, isComplete, error in
                                                            
            //.. 중략
            
            if let data = data, !data.isEmpty {
                guard let message = String(data: data, encoding: .utf8) else { return connection.cancel() }
                let components = message
                    .replacingOccurrences(of: "\r\n", with: "")
                    .components(separatedBy: .whitespaces)
                
                ///   입력받은 명령어에 따라 Case를 나눠 실행
                ///   - components: 입력받은 명령어
                ///   - connection: 요청을 보낸 Client의 connection
                if let command = Command(rawValue: components[0]) {
                    switch command {
                    case .checkin: self.checkin(components: components, connection: connection)
                    case .checkout: self.checkout(connection: connection)
                    case .summary: self.summary(components: components, connection: connection)
                    case .chat: self.chat(components: components, connection: connection)
                    case .finish: self.finish(components: components, connection: connection)
                    case .broadcast: self.broadcast(components: components, connection: connection)
                    case .direct: self.direct(components: components, connection: connection)
                    case .clap: self.clap(connection: connection)
                    case .history: self.history(connection: connection)
                    }
                }
            }
                        
            self.handleConnection(connection)
        }
    }
    
    /// 요청한 Client의 connection을 clients배열에 추가합니다.
    func checkin(components: [String], connection: NWConnection) {
        /// group, session을 clients배열을 필터링 하며 판별
        var group = 1
        var session = 1
        while true {
            let count = clients.filter { $0.group == group }.count
            if count < 4 { session = count + 1; break }
            group += 1
        }
        
        let client = Client(
            id: components[1],
            connection: connection,
            checkInDate: .now,
            session: session,
            group: group,
            inputCommandList: [components.joined(separator: " ")]
        )
        
        clients.append(client)
        
        print(">> checkin \(client.id) (success) from \(connection.endpoint) => session#\(client.session), group#\(client.group)")
    }
    
    /// 요청한 Client의 connection을 clients배열에서 삭제한다.
    func checkout(connection: NWConnection) {
        guard let index = clients.firstIndex(where: { $0.connection === connection }) else { return }
        let removedClient = clients.remove(at: index)
        let clients = clients.filter { $0.group == removedClient.group }
        
        /// connection을 끊어줌
        connection.cancel()
        
        sendMessageToClients(clients, "< broadcast from server, \"\(removedClient.id)님이 퇴장하셨습니다.\"\n")
        print(">> checkout from session#\(removedClient.session)(\(removedClient.id)) - disconnected")
    }
    
    /// 요청한 Client의 connection에만 Summary Enum으로 구분하여 맞는 keyword를 출력해준다.
    func summary(components: [String], connection: NWConnection) {
        guard let client = clients.first(where: { $0.connection === connection }) else { return }
        client.inputCommandList.append(components.joined(separator: " "))
        guard let summary = Summary(rawValue: components[1]) else { return }
        sendMessageToClients([client], "< keywords are \"\(summary.keyword)\"\n")
        print(">> summary from session#\(client.session)(\(client.id)) : \(summary.rawValue) => \"\(summary.keyword)\"")
    }
    
    /// 입력한 maxCount의 값 만큼 groupChatCount Dictionary에 client와 maxCount추가한다.
    func chat(components: [String], connection: NWConnection) {
        guard let client = clients.first(where: { $0.connection === connection }) else { return }
        let clients = clients.filter { $0.group == client.group }
        guard let count = Int(components[1].components(separatedBy: "=")[1]) else { return }
        groupChatCount[client.group] = (client, count)
        
        client.inputCommandList.append(components.joined(separator: " "))
        sendMessageToClients(clients, "< broadcast from server, \"채팅이 시작되었습니다\"\n")
        print(">> chat from session#\(client.session)(\(client.id))")
    }
    
    /// finish명령어 입력 시 groupChatCount Dictionary를 nil로 바꿔준다.
    func finish(components: [String], connection: NWConnection) {
        guard let client = clients.first(where: { $0.connection === connection }) else { return }
        client.inputCommandList.append(components.joined(separator: " "))
        if groupChatCount[client.group]?.client === client {
            groupChatCount[client.group] = nil
            let clients = clients.filter { $0.group == client.group }
            sendMessageToClients(clients, "< broadcast from server, \"채팅이 종료되었습니다\"\n")
            print(">> finish from session#\(client.session)(\(client.id))")
        }
    }
    
    /// chat 명령어를 통해 maxCount 입력 후 `broadcast` 명령어 수행 가능
    /// `broadcast` 호출 시 `groupChatCount` 1 씩 감소 0이 되면, 채팅 종료
    func broadcast(components: [String], connection: NWConnection) {
        guard let client = clients.first(where: { $0.connection === connection }) else { return }
        client.inputCommandList.append(components.joined(separator: " "))
        if groupChatCount[client.group]?.count ?? 0 > 0 {
            groupChatCount[client.group]?.count -= 1
            let clients = clients.filter { $0.group == client.group }
            let message = components[1...].joined(separator: " ")
            sendMessageToClients(clients, "< broadcast from \(client.id), \(message)\n")
            if groupChatCount[client.group]?.count == 0 {
                sendMessageToClients(clients, "< broadcast from server, \"채팅이 종료되었습니다\"\n")
            }
            print(">> broadcast to session#\(client.session)(\(client.id)) => text=\(message)")
        }
    }
    
    /// 그룹 상관없이 특정 클라이언트에게 메시지 전송
    func direct(components: [String], connection: NWConnection) {
        guard let client = clients.first(where: { $0.connection === connection }) else { return }
        let targetClient = clients.filter { $0.id == components[2] }
        let message = components[3...].joined(separator: " ")
        client.inputCommandList.append(components.joined(separator: " "))
        sendMessageToClients(targetClient, "< direct from \(client.id), \(message)\n")
        print(">> direct to session#\(client.session)(\(client.id)) => text=\(message)")
    }
    
    /// Client가 서버에게 clap 명령어를 입력하면 서버는 `claps`의 카운트를 올려 해당 Client에게 메시지를 보낸다.
    func clap(connection: NWConnection) {
        guard let client = clients.first(where: { $0.connection === connection }) else { return }
        claps += 1
        sendMessageToClients([client], "< clap count is \(claps)\n")
        print(">> clap from session#\(client.session)(\(client.id)) => \(claps)")
    }
    
    func history(connection: NWConnection) {
        guard let client = clients.first(where: { $0.connection === connection }) else { return }
        
        /// forEach를 통해 `inputCommandList` 배열을 순회하여 클라이언트가 입력했던 명령어를 해당 클라이언트에게 보내준다.
        /// `enumerated()` 메서드를 통해 인덱스 추출
        client.inputCommandList
            .enumerated()
            .forEach { (index, command) in
                sendMessageToClients([client], "> \(index + 1)\t\(command)\n")
            }
    }
    
    /// 클라이언트에게 메시지를 전송하는 메서드
    /// - Parameters:
    ///   - clients: 메시지를 전송 받을 클라이언트 배열
    ///   - message: 전송 받을 메시지
    func sendMessageToClients(_ clients: [Client], _ message: String) {
        let data = Data(message.utf8)
        
        for client in clients {
            client.connection.send(
                content: data,
                completion: .contentProcessed { sendError in
                    if let sendError = sendError {
                        print("Send error: \(sendError)")
                    }
                }
            )
        }
    }
    
    deinit {...}
}
```

## 학습 메모

- [TCP / UDP](https://nordvpn.com/ko/blog/tcp-udp-comparison/)
- [Network](https://developer.apple.com/documentation/network)
- [NWListener](https://developer.apple.com/documentation/network/nwlistener)
- [NWConnection](https://developer.apple.com/documentation/network/nwconnection)
