온라인 게임 프로그래밍과 같은 곳에선 소켓은 파일 핸들 방식과는 약간 다르다.
1) 서버에서 다루어야 하는 소켓 개수가 많다. TCP를 이용해서 통신해야 하는 경우 클라이언트 개수만큼 소켓이 있어야 한다.
2) 디스크를 읽거나 쓸 때 사용하는 read(), write() 함수는 호출 후 실행이 완료될 때까지 리턴하지 않는다.
소켓을 이용해서 읽기/쓰기를 하는 함수를 호출했는데, 즉시 리턴하지 않는다면 이들을 호출한 메인 쓰레드는 사용자 입장에서 일시 정지를 하는 것처럼 보인다.
위와 같은 이유 때문에 네트워크 프로그래밍에서 소켓은 보통 "비동기 입출력(Asynchronous I/O)"상태로 다룬다. 소켓을 비동기 입출력으로 다루는 방식은 크 아래와 같다.
비동기 입출력 상태로 다루는 방식 이전에 블로킹 소켓 방식의 동기 입출력 상태에 대한 설명을 한다.
source: https://12bme.tistory.com/231
소켓에 대한 동기 입출력(synchronous I/O) 방식을 블로킹 소켓이라고 한다.
디바이스 처리에 요청을 걸어 놓고 응답을 대기하는 함수를 호출할 때 쓰레드에서 발생하는 현상을 블로킹이라 한다. 파일 읽기/쓰기 함수를 호출할 때 이러한 블로킹 현상이 발생한다.
소켓에서도 마찬가지이다. 수신할 수 있는 데이터가 생길 때까지 쓰레드는 waitable 상태, 즉 블로킹이 발생한다. 데이터를 수신할 함수를 호출했으나, 상대방 컴퓨터에서 아무런 데이터를 보내지 않고 있다면 영원히 블로킹이 발생한다.
// TCP 소켓 프로그래밍
main() {
s = socket(TCP);
// 빈 포트가 없을 경우 이미 다른 곳에서 점유한 포트라고 하더라도 그것을 공유
s.bind(any_port);
// connect(): TCP 연결이 완료될 때까지 "블로킹"을 유지하다가
// 상대방이 연결을 수락하면 함수는 리턴
s.connect("55.66.77.88:5959");
// OS에서 상대방 컴퓨터로 데이터를 전송하는 처리가 완료되면 리턴
// 함수가 리턴했다고 상대방이 데이터를 수신했다는 말이 아님
s.send("hello");
s.close();
}
호출한 send()는 블로킹 없이 즉시 리턴된다. 하지만 이것이 수신측에서 데이터를 받았음을 의미하진 않는다. 이어서 설명할 소켓 송신/수신, 소켓 버퍼에 대한 이해를 파악하며 알아본다.
블로킹과 소켓 버퍼
reference: https://palamore.tistory.com/363
소켓은 각각 송신 버퍼(send buffer)와 수신 버퍼(receive buffer)을 하나씩 가지고 있다. 일련의 바이트 배열이라고 보면 된다.
송신 버퍼가 가득 차 다음 send() 함수 호출로 인해 버퍼에 넣을 공간이 없으면 이 send() 함수는 블로킹이 발생한다.
예들 들어, 수신하는 컴퓨터에서 recv() 처리를 하지 않아 송신 쪽 컴퓨터의 소켓 송신 버퍼가 가득차면 이후 send()가 블로킹된다.
OS에서 이 버퍼를 POP하여 공간이 생기면 블로킹된 send() 함수는 블로킹이 해제되어 리턴한다.
main() {
s = socket(TCP);
s.bind(5959);
s.listen();
// TCP 연결이 들어올 때까지 블로킹
s2 = s.accept();
while(true) {
// 수신할 수 있는 데이터가 없으면 블로킹 발생
// 수신할 수 있는 데이터가 있을 때까지 블로킹 유지
r = s2.recv();
if(r.length <= 0)
break;
print(r);
}
s2.close();
}
수신 측 프로그램은 소켓에서 데이터를 수신하는 함수를 호출하여 수신 버퍼에서 이미 수신된 데이터를 꺼낼 수 있다. 수신 버퍼가 완전히 비어있으면 데이터를 수신하는 함수는 블로킹이 일어난다.
이는 수신된 데이터 크기가 0바이트라는 의미와는 다르다. 수신된 데이터 크기가 0바이트라는 것은 상대방이 TCP 연결을 끝냈음을 의미한다.
TCP 통신에서 send, recv 함수 블로킹
- 송신 버퍼가 가득 참 => TCP 소켓 send() 함수 블로킹
- 수신 버퍼가 가득 참
=> (수신 쪽) TCP 소켓 recv() 함수 블로킹
=> (송신 쪽) TCP 소켓 send() 함수 블로킹
TCP는 수신 버퍼가 꽉 차면, 수신 쪽의 해당 수신 버퍼와 연관된 소켓의 recv() 함수가 블로킹된다. 또한 TCP로 데이터를 보내는 쪽에서는 송신 함수인 send()도 블로킹된다.
극단적으로 이 상태에서 TCP recv()를 전혀 하지 않으면 send()도 계속 블로킹 상태를 유지한다. TCP 통신은 전혀 없고 TCP 연결만 살아 있는 것이다.
=> TCP 송신 함수로 송신 버퍼에 데이터를 쌓는 속도보다 수신 함수로 수신 버퍼에서 데이터를 꺼내는 속도가 느리다고 해서 TCP 연결은 끊어지지 않는다. 단지 실제 송신 속도가 느린 쪽에 맞추어 작동할 뿐.
UDP 소켓의 recv()는 데이터그램이 도착할 때까지 블로킹된다.
TCP와의 차이점은 수신 버퍼가 오버플로우될 때 뒤이어 오는 데이터그램을 버린다는 것이다. 네트워크 선로로 UDP 데이터그램A가 도착했지만, UDP 소켓 안의 수신 버퍼가 데이터그램 A를 담을 여유 공간이 없으면 데이터그램A는 그냥 버려진다. 이때 송신 함수 sendTo()의 블로킹은 발생하지 않는 것이 TCP와 차이점이다.
=> UDP 송신 함수로 송신 버퍼에 데이터를 쌓는 속도보다 수신 함수로 수신 버퍼에서 데이터를 꺼내는 속도가 느리면, 데이터그램 유실이 발생한다.
혼잡 현상
라우터에 연결된 한 곳 A에서 도착하는 패킷이 압도적으로 많으면, 라우터는 A에서 도착하는 패킷을 처리하느라 A 이외의 곳에서 오는 패킷을 원할하게 처리하지 못함. 이 경우 A 이외 다른 곳의 네트워킹은 원할한 속도를 내지 못함. 즉, A 이외의 다른 곳들은 네트워킹 경쟁에서 밀림.
UDP는 제어 기능이 없기에 UDP를 속도 제한없이 마구 송신하면 주변의 네트워킹이 경쟁에서 밀리고, 주변의 네트워킹이 두절된다. 이러한 현상을 혼잡 현상이라 한다.