Home 슈도코드로 이해하는 소켓 프로그래밍
Post
Cancel

슈도코드로 이해하는 소켓 프로그래밍

인터넷을 통해 데이터를 주고받을 때 가장 먼저 떠오르는 두가지는 http 통신과 소켓 통신입니다.
이 둘엔 한가지 큰 차이점이 있는데 http 통신의 경우에는 클라이언트가 서버에 요청을 보내면 서버는 받은 요청에 알맞는 응답을 보내주는 식으로 동작을 하는데 이것을 단방향 통신이라고 해요.
반면에 소켓 통신의 경우에는 클라이언트와 서버 측 둘 다 데이터를 주고 받을 수 있습니다.
그래서 채팅과 같이 양 쪽 모두 데이터를 주고 받아야 하는 상황에서는 양방향 통신이 가능한 소켓 통신이 유리하다고 할 수 있겠습니다.
오늘은 이 소켓 통신, 소켓 프로그래밍이 어떤 원리로 이루어 지는지 정리를 해보겠습니다.
소켓 통신에는 대표적으로 tcp 통신, udp 통신이 있는데 이 두가지에 대한 내용은 다음에 기회가 되면 다뤄보도록 하겠습니다.

Connect to TCP socket

통신을 하는 개체에는 서버와 클라이언트가 있는데 먼저 서버에서의 tcp 네트워킹 구성은 어떻게 이루어 지는지 슈도 코드로 알아보겠습니다.
먼저 서버 측 슈도코드입니다.

Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
main() {
	s = socket(TCP);
	s.bind(5959);
	s.listen();
	
	s2 = s1.accept();
	
	for() {
		r = s2.recv();
		if (r.length <= 0) break;
		print(r);
	}
	s2.close();
}

위 코드를 나누어 설명하면

  1. TCP 소켓을 생성합니다.

    1
    
    s = socket(TCP);
    
  2. 생성된 소켓을 특정 포트번호에 바인딩 시킵니다. (클라이언트는 이 바인드된 포트번호로 연결을 요청해야 서버측과 TCP 네트워킹을 할 수 있습니다.).

    1
    
    s.bind(5959);
    
  3. 소켓을 요청을 받는 상태로 만들어 준 다음 요청을 기다리는 상태로 전환합니다.
    accept()메소드가 실행되면 서버로의 연결 요청이 들어올 때 까지 대기상태로 들어가게 됩니다.
    요청이 들어오게되면 accept()는 요청을 보낸 클라이언트와 통신할 수 있는 소켓 핸들러를 반환하게 됩니다.

    1
    2
    
    s.listen();
    s2 = s.accept();
    
  4. 이제 무한 루프를 돌면서 클라이언트가 보낸 데이터가 없을 때 까지 받아온 데이터를 출력해줍니다. TCP 소켓에선 수신 받은 데이터의 크기가 0바이트면 상대방이 tcp연결을 끝낸 것을 의미합니다.

    1
    2
    3
    4
    5
    
    for() {
    	receivedMessage = s2.recv();
    	if (receivedMessage.length <= 0) break;
    	print(receivedMessage);
    }
    
  5. 통신이 끝났음으로 소켓을 닫아줍니다.

    1
    
    s2.close();
    

Client

클라이언트 측의 코드도 간단합니다. 서버 측 코드를 보고 왔다면 더 빨리 이해할 수 있을거같네요.

1
2
3
4
5
6
7
main() {
	s = socket(TCP);
	s.bind(6000);
	s.connect("localhost:5959");
	s.send("hello server ;p");
	s.close();
}
  1. 마찬가지로 TCP소켓을 생성하고 특정 포트에 바인드 시켜 줍니다.

    1
    2
    
    s = socket(TCP);
    s.bind(6000);
    
  2. TCP소켓을 열어둔 서버로 연결을 요청합니다.

    1
    
    s.connect("localhost:5959");
    
  3. 연결이 완료 되었다면 메세지를 보낸 후 소켓을 닫습니다.

    1
    2
    
    s.send("Hello server ;p");
    s.close();
    

송수신 버퍼

위 코드에서 특정 상황에서 send(), recv()메소드가 블로킹이 걸리는 경우가 있습니다. 우선 왜 블로킹이 걸리는지 알기 전에 소켓 버퍼에 대한 이해가 필요한데 각 소켓에는 송신 버퍼와 수신 버퍼를 하나씩 갖고 있습니다.

송신 버퍼 (send buffer)

송신 버퍼는 큐의 형태로 작동합니다. send(A)메소드가 실행이 되면 송신 버퍼에 A를 푸시하게 됩니다. (상대방에게 A를 보내는것이 아님!!) 송신 버퍼가 채워지면 잠시 후 가장 먼저 들어온 데이터가 운영체제로 송출됩니다.

1. [ , , ] 송신 버퍼 비어있음.
2. send(A,B);
	=> [B, A, ] 송신 버퍼에 A, B가 채워짐.
3. 시간이 지나면 가장 먼저 들어온 데이터부터 pop되어 운영체제로 송출됨.
4. [B, , ]
5. [ , , ]

그렇다면 send()메소드는 어떤 경우에 블로킹이 걸리게 될까요? 바로 송신 버퍼가 가득 차 있을 때 입니다.
아래와 같이 송신 버퍼가 가득 차 있는 상황에서 send(data) 메소드가 실행되면 블로킹이 걸려 다음 코드로 진행되지 못하고 송신 버퍼에 빈 자리가 생길 때 까지 대기하게 됩니다.

1. [A, B, C, D] 송신 버퍼가 가득 차 있는 상태.
2. send(E);
3. E -> [A, B, C, D] 송신 버퍼에 빈 자리가 없어서 블로킹 발생!
4. [ , A, B, C] 빈자리가 생김.
5. [E, A, B, C] 송신 버퍼에 E가 채워지고 블로킹 해제.

수신 버퍼 (receive buffer)

반대로 receive()메소드는 수신 버퍼가 비어있을 때 블로킹이 발생하게 됩니다.
블로킹 상태로 대기를 하다가 1바이트라도 받을 데이터가 채워지면 블로킹을 해제하게 됩니다.
그런데 여기서 수신 버퍼가 가득 찬 상태가 되면 어떤 일이 발생할까요?
수신 버퍼가 가득 차게 되는 상황은 수신 버퍼에서 데이터를 꺼내는 속도가 운영체제가 수신 버퍼에 데이터를 채우는 속도보다 느릴 때 발생하게 되는데 이 경우에는 송신 측의 send()메소드에 블로킹이 걸리게 됩니다.
TCP연결의 경우 이런식으로 수신버퍼가 찼을 때 블로킹을 걸어 데이터 유실이 발생하지 않도록 하여 통신의 신뢰성을 높여줍니다.
그렇다면 UDP 통신의 경우에는 수신 버퍼가 가득 차면 어떻게 될까요?
UDP 소켓의 경우엔 수신 버퍼가 가득차도 블로킹을 걸어 주지 않아 데이터 유실이 발생하게 됩니다.
이게 tcpudp의 차점 중 한가지 입니다.


여기까지 소켓 프로그래밍에 대해 간단히 알아보았습니다.
오늘 설명한 소켓은 전부 블로킹 소켓입니다 (소켓의 디폴트 값이 블로킹 소켓임).
오늘처럼 1대1 통신과 같은 상황에서는 블로킹 소켓을 사용해도 큰 문제가 없지만 여럿과 통신을 할때에는 블로킹 소켓을 사용할 경우 여러 문제상황과 마주 할 수 있는데 이를 해결하기 위해 논-블록 소켓이란 것이 존재합니다.
기존 블록 소켓의 경우에는 send()혹은 recv() 메소드가 블로킹에 빠져 대기상태에 들어가는 경우가 있었지만 소켓을 논-블로킹 소켓으로 전환을 해주면 버퍼가 꽉차이든 비어있든 원래는 블로킹이 발생해야 하는 상황에서 대기 상태에 빠지지 않고 바로바로 값을 리턴을 해줍니다. 그래서 블로킹 소켓을 사용할 때 발생할 수 있는 여러 문제상황을 해결할 수 있게 해줍니다.
오늘은 여기까지만 알아보고 다음에 non-blocking socket에 대해 공부해보는 시간을 가져보도록 하겠습니다,,

Reference

  • 배현직, 게임서버 프로그래밍 교과서 (2019)
This post is licensed under CC BY 4.0 by the author.

[Setting] M1 pro 맥북 개발환경 세팅.기록

Connection: Upgrade의 의미