echo server
echo server는 클라이언트가 전송해주는 데이터를 그대로 되돌려 전송해 주는 서버를 말합니다.
소켓통신을 공부해보려 하는데 연결할 서버가 없어서 echo server를 이용해보기로 했습니다.
간단한 echo_server입니다.
# echo_server.py
import socket
def start_echo_server():
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('ip주소', 8085))
server_socket.listen(5)
print("Echo server is listening...")
while True:
client_socket, addr = server_socket.accept()
print(f"Connection from {addr}")
data = client_socket.recv(1024)
while data:
print(data)
client_socket.send(data)
data = client_socket.recv(1024)
client_socket.close()
if __name__ == "__main__":
start_echo_server()
python으로 작성되었고, 실행시키면
"Echo server is listening..." 가 출력되면 정상입니다.
그 후 TCP 연결이 되면 "Connection from [연결정보]" 가 출력됩니다.
이 python을 실행하면 제 컴퓨터가 간단한 소켓통신 서버가 됩니다.
이제 Swift는 클라이언트가 됩니다.
Swift로 TCP소켓통신을 정리해봅니다.
class ViewController: UIViewController {
var inputStream: InputStream?
var outputStream: OutputStream?
맨 처음 등장하는 각 Stream들은 나중에 정리하고 일단 이 스트림들을 생성하는 메서드를 먼저 정리해봅니다.
func setupConnection() {
var readStream: Unmanaged<CFReadStream>?
var writeStream: Unmanaged<CFWriteStream>?
// host와 port에 대해 읽기와 쓰기 스트림의 쌍을 생성하는 메서드. TCP/IP 소켓 연결을 설정하고 통신을 시작하기위해 사용
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault,
"IP주소" as CFString,
8085,
&readStream,
&writeStream)
inputStream = readStream!.takeRetainedValue()
outputStream = writeStream!.takeRetainedValue()
...
}
CFStreamCreatePairWithSocketToHost메서드
host와 port에 대해 읽기와 쓰기 스트림의 쌍을 생성. TCP/IP 소켓 연결을 설정하고 통신을 시작하기위해 사용.
매개변수는
- alloc: CFAllocator? - 메모리 할당자.
- host: CFString - 연결할 서버의 호스트 이름, IP주소를 CFString 형식으로
- port: UInt32 - 연결할 서버의 포트번호
- readStream: UnsafeMutablePointer<Unmanaged<CFReadStream>?>?
- writeStream: UnsafeMutablePointer<Unmanaged<CFWriteStream>?>?
Unmanaged는 non-ARC 환경에서 객체를 관리하기 위한 일종의 래퍼입니다. C기반의 Core Foundation의 API를 사용하기 위해 사용되는데 Core Foundation과같은 C API는 ARC를 따르지 않기 때문에 사용합니다.
이 Unmanaged로 래핑한 Unmanaged<CFReadStream>? 객체의 공간을 확보해 메서드로 "&"를 활용해 주소를 전달하게됩니다.
그럼 이 메서드는 각각 전달된 주소로 Unmanaged<CFReadStream>과 Unmanaged<CFWriteStream>타입의 객체를 생성합니다.
다음 takeRetainValue() 메서드가 보이는데 이 메서드는 객체의 소유권을 가져오고 reference count를 감소시키는 메서드입니다.
Unmanaged인스턴스에서 CFReadStream과 CFWriteStream객체를 추출하는 용도로 사용됩니다.
func setupConnection() {
...
inputStream?.delegate = self
inputStream?.schedule(in: .current, forMode: .common)
inputStream?.open()
outputStream?.schedule(in: .current, forMode: .common)
outputStream?.open()
}
이렇게 생성한 CFReadStream과 CFWriteStream객체를 설정합니다.
InputStream과 OutputStream을 스케줄링하고 열어서 I/O작업을 수행할 준비를 합니다. >> 소켓 = File입니다.
각각의 스트림은 RunLoop의 특정 모드에 스케줄링됩니다.
- in: .current - RunLoop를 지정합니다. 스레드를 지정해주는 작업이라 합니다.
- forMode - 스케줄링할 RunLoop의 모드를 지정하는 작업이라고 합니다.
이후 open해서 Stream 객체를 열어 데이터를 읽고 쓸 준비를 합니다.
var inputStream: InputStream?
var outputStream: OutputStream?
이들은 스트림 기반의 입력과 출력을 관리하는 클래스들입니다.
TCP는 스트림 프로토콜이였죠
InputStream은 데이터를 읽기위한 스트림입니다. 서버, 메모리, 파일 등 다양한 소스로부터 데이터를 읽어들일 때 사용됩니다.
OutputStream은 데이터를 쓰기위한 스트림입니다. 데이터를 쓸 때 사용됩니다.
각 Stream의 open() 메서드를 통해 스트림을 열어 데이터를 읽고 쓸 준비를 하게됩니다.
여기서 StreamDelege를 self를 지정했으니 Delegate를 채택해야합니다.
extension ViewController: StreamDelegate {
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
switch eventCode {
case .hasBytesAvailable:
var buffer = [UInt8](repeating: 0, count: 4096)
let byteRead = inputStream!.read(&buffer, maxLength: buffer.count)
if byteRead >= 0, let output = String(bytes: buffer, encoding: .utf8) {
print("Recv from server: \(output)")
}
default:
break
}
}
}
StreamDelegate프로토콜은 Stream객체에 대한 이벤트를 처리하기 위한 메서드를 제공합니다.
stream(_: handle:) 메서드를 사용해 스트림에서 발생하는 이벤트를 처리할 수 있습니다. 이 메서드는 스트림에서 이벤트가 발생할 때 마다 호출됩니다.
- aStream: 이벤트가 발생된 스트림이 전달됩니다. 고로 어떤 스트림인지 식별할 수 있습니다.
- eventCode: 발생한 이벤트의 유형을 나타내는 Stream.Event의 열거형 값이 전달됩니다.
여기서 사용된 case.hasBytesAvailable 이벤트는 스트림에 읽을 수 있는 데이터가 있음을 나타냅니다.
그리고, buffer를 생성하게 되는데 4096바이트의 데이터를 읽을 수 있는 버퍼를 생성합니다.
이후 buffer로 데이터를 읽게됩니다. byteRead는 실제로 읽은 바이트 수가 저장이됩니다.
이제 String으로 읽은 데이터를 변환합니다. 변환이 되면 출력을합니다.
이제 서버로부터 데이터를 읽을 준비가 됐습니다.
이제 서버로 데이터를 보내봅시다.
func send(message: String) {
let data = message.data(using: .utf8)!
_ = data.withUnsafeBytes { outputStream?.write($0.bindMemory(to: UInt8.self).baseAddress!, maxLength: data.count)}
}
message를 outputStream에 작성하여 외부로 보내는 역할을 하게됩니다.
파라미터로 받은 문자열을 utf8 인코딩으로 Data객체로 변환합니다. 그리고 outputStream에 write합니다.
이렇게 send하게되면 서버로 전달됩니다.
서버에서 연결이 일어나면 Connection이 되었다고 알리고, 데이터가 전달되면 출력하도록 만들었습니다.
이렇게 데이터가 전달되면 서버는 다시 받은 데이터를 그대로 clientd에게 send하게됩니다.
그럼 아까 작성해놓은 Delegate를 통해 InputStream에게 전달된 데이터를 출력하게됩니다.
화면은 간단히 서버로 전달할 텍스트필드, 전송버튼으로 구성했습니다.
전체 코드
import UIKit
class ViewController: UIViewController {
var inputStream: InputStream?
var outputStream: OutputStream?
@IBOutlet weak var messageInputTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
setupConnection()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
closeConnection()
}
@IBAction func sendButtonAction(_ sender: UIButton) {
let message = messageInputTextField.text!
send(message: message)
}
func setupConnection() {
var readStream: Unmanaged<CFReadStream>?
var writeStream: Unmanaged<CFWriteStream>?
// host와 port에 대해 읽기와 쓰기 스트림의 쌍을 생성하는 메서드. TCP/IP 소켓 연결을 설정하고 통신을 시작하기위해 사용
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, "192.168.1.101" as CFString, 8085, &readStream, &writeStream)
inputStream = readStream!.takeRetainedValue()
outputStream = writeStream!.takeRetainedValue()
inputStream?.delegate = self
inputStream?.schedule(in: .current, forMode: .common)
inputStream?.open()
outputStream?.schedule(in: .current, forMode: .common)
outputStream?.open()
}
func send(message: String) {
let data = message.data(using: .utf8)!
_ = data.withUnsafeBytes { outputStream?.write($0.bindMemory(to: UInt8.self).baseAddress!, maxLength: data.count)}
}
func closeConnection() {
inputStream?.close()
outputStream?.close()
}
}
extension ViewController: StreamDelegate {
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
switch eventCode {
case .hasBytesAvailable:
var buffer = [UInt8](repeating: 0, count: 4096)
let byteRead = inputStream!.read(&buffer, maxLength: buffer.count)
if byteRead >= 0, let output = String(bytes: buffer, encoding: .utf8) {
print("Recv from server: \(output)")
}
default:
break
}
}
}