카테고리 없음
Swift - 소켓 라이브러리를 사용하여 소켓 프로그래밍 해보기 (에코서버, 클라이언트 구현)
jin_j_i_n
2022. 1. 10. 22:47
소켓 프로그래밍의 과정을 이해하기 위해 간단한 에코서버와 클라이언트를 구현하였다.
BlueSocket이라는 라이브러리를 사용하여 구현하였다.
BlueSocket을 사용한 에코 서버, 에코 클라이언트 구현 예제
에코 서버
소켓 클라이언트로부터 연결 요청을 받고 클라이언트가 보낸 메세지를 읽고, 같은 메세지를 보내는 서버이다.
서버 입장에선 여러 클라이언트들이 연결 요청을 할 수 있기 때문에 각 연결에 대하여 비동기적으로 진행하고 응답하도록 구현한다.
에코서버 구성요소
- Listening Socket 프로퍼티 : 클라이언트 요청을 받는 리스닝 소켓
- Connected Socket 프로퍼티: 현재 연결되어있는 소켓 딕셔너리, 키값은 socketfd이고 값은 Socket이다.
- run() 메서드: Server 실행을 위한 메서드, 리스닝 소켓을 연결을 위해 열어두고 연결을 요청하는 소켓이 존재한다면 각 요청마다 새로운 연결을 만든다. 연결된 소켓이 있으면 서버를 종료시키지 않는다.
let queue = DispatchQueue.global(qos: .userInteractive)
queue.async { [unowned self] in
do {
// create listening socket
try self.listenSocket = Socket.create(family: .inet6)
guard let socket = self.listenSocket else {
print("Unable to unwrap socket...")
return
}
// start listen socket on port
try socket.listen(on: self.port)
print("Listening on port: \(socket.listeningPort)")
// 연결 요청이 들어오는 소켓이 존재한다면 connection 생성, 반복
repeat {
let newSocket = try socket.acceptClientConnection()
print("Accepted connection from: \(newSocket.remoteHostname) on port \(newSocket.remotePort)")
print("Socket signature: \(String(describing: newSocket.signature?.description))")
// create new connection
self.addNewConnection(socket: newSocket)
} while self.continueRunning
} catch let error {
// 에러 처리
guard let socketError = error as? Socket.Error else {
print("Unexpected Error...")
return
}
if self.continueRunning {
print("Error reported:\n \(socketError.description)")
}
}
}
dispatchMain()
- addNewConnection() : run() 메서드에서 새로운 연결을 만들기 위해 호출하는 함수.
연결이 여러개일 수 있기 때문에 해당 함수를 통해 연결을 만들 때 비동기로 구현한다.
클라이언트 소켓으로부터 오는 메세지에 QUIT 또는 SHUTDOWN 문자열이 있는경우 연결을 종료한다. 그렇지 않으면 메세지 읽는 행위를 반복한다.
socketLockQueue.sync { [unowned self] in
// 현재 연결되어있는 소켓 딕셔너리에 추가
self.connectedSockets[socket.socketfd] = socket
}
let queue = DispatchQueue.global(qos: .default)
// 각 연결마다 비동기로 실행
queue.async { [unowned self, socket] in
var shouldKeepRunning = true
var readData = Data(capacity: EchoServer.bufferSize)
do {
// 연결 후 최초로 write 할 때
if socket.isFirstWriting {
try socket.write(from: "Hello, type 'QUIT' to end session\nor 'SHUTDOWN' to stop server.\n")
socket.isFirstWriting = false
}
// 연결이 되어있는 한 데이터를 계속 읽어옴.
repeat {
let bytesRead = try socket.read(into: &readData)
if bytesRead > 0 {
guard let response = String(data: readData, encoding: .utf8) else {
print("Error decoding response...")
readData.count = 0
break
}
// 서버 종료 커맨드가 있는지 확인
if response.hasPrefix(EchoServer.shutdownCommand) {
print("Shutdown requested by connection at \(socket.remoteHostname): \(response)")
let reply = "Server response \n\(response)\n"
try socket.write(from: reply)
if (response.uppercased().hasPrefix(EchoServer.quitCommand) || response.uppercased().hasPrefix(EchoServer.shutdownCommand)) && (!response.uppercased().hasPrefix(EchoServer.quitCommand) && !response.uppercased().hasPrefix(EchoServer.shutdownCommand)) {
try socket.write(from: "If you want QUIT or SHUTDOWN, please tye the name in all caps. :) \n")
}
if response.uppercased().hasSuffix(EchoServer.quitCommand) || response.uppercased().hasSuffix(EchoServer.shutdownCommand) {
shouldKeepRunning = false
}
}
try socket.write(from: "echo: \(response)")
if bytesRead == 0 {
shouldKeepRunning = false
break
}
readData.count = 0
}
} while shouldKeepRunning
- shutDownServer() : 모든 연결을 끊고 서버를 종료한다.
print("\nShutdown in progress...")
self.continueRunning = false
for socket in connectedSockets.values {
// 모든 소켓을 닫는다.
self.socketLockQueue.sync { [unowned self, socket] in
self.connectedSockets[socket.socketfd] = nil
socket.close()
}
}
DispatchQueue.main.sync {
exit(0)
}
소켓 클라이언트
에코 서버로 소켓 연결을 시도, 연결을 성공하면 문자열 데이터를 쓰고, 서버로부터 응답을 받아온다.
받아온 응답은 "echo: (클라이언트가 전송한 문자열)" 과 같은 형태이다.
소켓 클라이언트 구성요소
- ViewController: UI를 구성, 사용자로부터 포트번호, IP주소를 입력받아 연결하는 버튼, 메세지 입력창과 전송 버튼으로 구성되어 있다.
포트번호와 IP주소를 입력한 뒤 Create Socket 버튼을 누르면 connect() 메서드를 호출한다.
이후에 마지막 텍스트필드에 서버로 보낼 텍스트 입력 후 Submit 버튼을 누르면 서버로부터 응답을 받을 수 있다.
- connect() : 사용자가 지정한 에코서버로 연결을 시도한다. 성공적으로 연결이 되면 서버로부터 응답을 받아온다.
func connect() -> String {
guard socket != nil else { return "there's no connected socket"}
var startMsg = ""
taskWithSocketErrorHandling(closure: {
// 에코 서버에 연결
try socket!.connect(to: to, port: Int32(port))
print("Connected to: \(String(describing: socket?.remoteHostname)) on port \(String(describing: socket?.remotePort))")// 서버로부터 응답 받아옴.
startMsg = try socket?.readString()! as! String
}, errorTask: nil)
return startMsg
}
connect() 메서드 실행 결과
- readFromServer() : 메세지 전송 후 서버로부터 응답을 받아온다.
-
func readFromServer() -> String { guard socket != nil else { return "Error: Socket does not exist" } var responseStr = "" taskWithSocketErrorHandling(closure: { var readData = Data(capacity: self.socket!.readBufferSize) let bytesRead = try socket!.read(into: &readData) guard bytesRead > 0 else { responseStr = "Error: Zero bytes read" return } guard let response = String(data: readData, encoding: .utf8) else { responseStr = "Error: decoding response..." return } responseStr = response }, errorTask: nil) return responseStr }
- writeData() : 에코서버로 메세지 전송
func writeData(message: String) { guard socket != nil else { print("Socket does not exist") return } taskWithSocketErrorHandling(closure: { try socket!.write(from: message) }, errorTask: nil) }
서버로 메세지 전송 후 응답 받아온 결과