티스토리 뷰

데이터의 전송과 수신은 네트워크 프로그램의 전부라고도 할 수 있다.

데이터를 주고받기 위한 데이터 형식은 바이트 단위의 char형.

따라서 데이터의 형식은 문자열이든 바이너리든 상관없이 어떠한 데이터 형식도 전송이 가능하다.

데이터를 전송하는 함수는 send/WSASend 수신하는 함수는 recv/WSARecv가 있다.

 

send와 WSASend

int send(
 SOCKET s,
 const char FAR * buf,
 int len,
 int flags
);

s : 데이터를 전송하길 원하는 연결된 소켓이다.

buf : 전송할 데이터가 담긴 char형 buffer이다.

len : 전송할 데이터의 크기

flag : 0 혹은 MSG_DONTROUTE, MSG_OOB 같은 값으로 설정한다. flags는 위 값들의 비트 OR을 사용할 수 있다.

       MSG_DONTROUTE는 전송층에 이 패킷을 라우팅하지 말 것을 요청

       MSG_OOB는 데이터가 데역 외 데이터(out-of-band data)임을 표시한다.

 

send에서 발생할 수 있는 에러

WSAECONNABORTED : 프로토콜상의 에러나 타임아웃에 의해 연결이 끊어진 경우에 발생, 이런 경우 소켓을 닫아야함

WSAECONNERESET : 상대방 컴퓨터가 리붓되거나 네트워크 선이 끊어진 경우와 같이 하드웨어적인 원인,

WSAETIMEDOUT : 연결이 두절된 경우로 네트워크 에러가 발생되거나 연결된 시스템이 통지없이 다운된 경우

 

WSASend는 윈속2 버전의 send 함수

int WSASend(
 SOCKET s,
 LPWSABUF lpBuffers,
 DWORD dwBufferCount,
 LPDWORD lpNumberOfBytesSent,
 DWORD dwFlags,
 LPWSAOVERLAPPED lpOverlapped,
 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

s : 연결될 소켓

lpBuffers : 하나 이상의 구조체 WSABUF의 포인터, 하나의 구조체의 포인터거나 구조체 배열의 포인터가 될 수 있다.

dwBufferCount : 입력한 구조체 WSABuf의 개수이다.

lpNumberOfBytesSent :  WSASend를 통해 전송된 데이터의 총 크기를 리턴해줄 DWORD 변수의 포인터

dwFlags : send에서의 flags와 동일한 플래그다

Overlapped와 CompletionsRoutine : 윈속에서 지원하는 Overlapped I/O에서 이용하기 위한 파라메터  

 

여기서 구조체 배열로 한번에 여러 개의 버퍼를 전송하는 이유가 궁금할 수도 있다

이런 방법을 scatter-gather I/O라고 한다.

여러 개의 버퍼를 이용해 전송할 때는 WSABUF 배열의 첫번째 요소부터 마지막 요소의 순으로 전송이 된다.

 

WSASend는 보낸 데이터의 바이트 수를 lpNumberOfBytesSend 파라메터에 채워서 리턴한다

WSASend의 리턴 값이 0인 경우는 성공일 때 실패일 때는 SOCKET_ERROR가 된다.

발생하는에러는 send함수와 똑같다고 생각하면 편하다.

 

recv와 WSARecv

int recv(
 SOCKET s,
 char FAR* buf,
 int len,
 int flags
);

s : 데이터를 수신할 소켓

buf : 수신된 데이터를 담을 char형 buffer

len : 수신하길 원하는 버퍼의 크기

flags : 0 혹은 MSG_PEEK, MSG_OOB flags는 이 값들을 비트 OR로 쓸 수 있다.

       0 - 특별한 행동을 취하지 않는다

       MSG_PEEK - 네트워크 버퍼에서 데이터를 제거하지 않고 버퍼에 데이터를 복사만 해온다.

                       자주 사용은 하지 않음, 두번의 함수를 호출해야 되기 때문(데이터 복사 및 데이터 제거)

                       전체 데이터가 리턴되지 않는 경우에는 비정상적으로 작동하게 된다.

                       시스템 버퍼에 데이터를 남겨두는 것은 수신된 데이터를 저장할 버퍼 공간을 더 작게 만듦으로

                       시스템으로 하여금 전송측을 위한 TCP 윈도우 크기를 축소시키도록 만든다.

 

UDP/ 데이터그램 기반 소켓에서 사용시 고려해야할 사항

 만약 데이터의 크기가 버퍼의 크기보다 크다면 recv는 WSAEMSGSIZE에러를 발생한다.

 메시지 중심의 프로토콜에선 메시지 크기에 관련된 에러가 발생한다는 걸 기억해야한다.

 TCP와 같은 스트림 프로토콜은 수신된 데이터의 크기가 크더라도 프로그램에서 요청한 크기만큼

 처리 가능한 데이터만을 리턴해서 위와 같은 에러가 발생하지 않는다.

 

int WSARecv(
 SOCKET s,
 LPWSABUF lpBuffers,
 DWORD dwBufferCount,
 LPDWORD lpNumberOfBytesRecvd,
 LPDWORD lpFlags,
 LPWSAOVERLAPPED lpOverlapped,
 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

s : 연결된 소켓

lpBuffers : WSABuf의 배열

dwBufferCount : 배열의 WSABUF의 개수

lpNumberOfBytesRecvd : 수신작업이 즉시 완료됐을 때 수신된 데이터의 바이트 크기를 받을 변수의 포인터

flags : 0, MSG_PEEK, MSG_OOB, MSG_PARTIAL과 같은 값으로 설정

        MSG_PARTIAL - partial message(부분 데이터)를 지운하는 메시지 중심 프로토콜에선 WSARecv를 호출할 때

                            설정되어서 리턴되기도 한다(버퍼 공간이 부족해 전체 데이터가 리턴될 수 없을 때)

                            이 플래그가 설정되어서 호출되면 수신 작업은 가능한 빨리 리턴되며

                            전체 데이터의 일부분만을 리턴할 수도 있다. 메시지 중심 프로토콜에서만 사용된다

 

스트림 프로토콜(Stream Protocol)

 스트림 프로토콜에서는 전송측이나 수신측에서

 데이터가 더 작게 쪼개지거나 더 크게 합쳐져서 송수신된다.

 따라서 모든 송수신 함수는 자신이 송수신하길 원하는 양만큼 정확히 송수신 할수없다.

 TCP와 같은 대부분의 연결지향 프로토콜을 스트림 프로토콜에서 다루게 된다.

 send함수를 통해 2048바이트만큼 데이터를 송신한다고 생각해보자.

char sendbuff[2048];
int nBytes = 2048;

ret = send(s, sendbuff, nBytes, 0);

 send는 2048보다 작은 바이트를 리턴할 수 있다.

 이 경우에 내부 버퍼는 데이터가 네트워크 선로로 나가기 전까지 데이터를 보관하는 역할을 한다.

 또한 아주 큰 양의 데이터를 전송할 때 내부 버퍼는 데이터로 바로 채워질 것이다.

 TCP/IP에는 Window Size라는 것도 가지고 있는데 수신측에서는 이 윈도우 크기의 변화로

 얼만큼의 데이터를 수신할지 결정하게 된다.

 수신측에서 받는 데이터가 많아지게 되면 데이터가 넘치지 않도록 윈도우 크기를 0으로 설정한다.

 이로 인해 전송측은 크기가 0보다 커질 때까지 데이터를 전송할 수 없게 된다.

 다음 예제는 내부적인 버퍼가 1024바이트만큼의 데이터만을 수용한다면 나머지 1024바이트를 재전송해야 한다.

 

char sendbuff[2048];
int nBytes = 2045,
    nLeft,
    idx;
nLeft = nBytes;
idx = 0;

while(nLeft > 0)
{
  ret = send(s, &sendbuff[idx], nLeft, 0);
  if(ret == SOCKET_ERROR)
  {
    //ERROR
  }
  nLeft -= ret;
  idx += ret;
}

 

TCP/IP Window Size란?

 한번에 받을 수 있는 데이터의 양을 뜻한다.

 통신을 할 때 상대방에게 자신의 Windows Size를 알려주면, 상대방은 그만큼의 양을 한번에 전송하고

 상대방이 다 처리했는지 확인 후에 다음 데이터를 전송하는 것이다.

 

소켓의 수신측에서도 같은 원리가 적용된다. 하지만 스트림 소켓의 데이터는 연속된 스트림 형식이기 때문에

응용프로그램 입장에서는 얼만큼의 데이터가 읽혀졌는지 중요하지 않을 수 있기 때문이다.

1. 응용프로그램에서 스트림 형식이 아닌 개별적인 데이터 형식으로 데이터를 수신하고자 한다면 약간의 작업이 더 필요2. 모든 데이터의 크기가 같다면 조금 더 간단해진다.

다음은 512byte의 메시지를 수신하기 위한 코드이다.

char recvBuff[1024];
int ret,
    nLeft,
    idx;

nLeft = 512;
idx = 0;

while(nLeft > 0)
{
 ret = recv(s, &recvbuff[idx], nLeft, 0);
 if(ret == SOCKET_ERROR)
 {
   //ERROR
 }
 idx += ret;
 nLeft -= ret;
}

메시지의 크기가 가변적이라면 메시지의 크기를 알 수 있도록 프로토콜을 직접 만들어야 한다.

 

 

연결종료

소켓 사용이 끝나면 소켓을 닫아서 관련된 리소스를 반납해야 한다.

반납하는 방법은 closesocket을 호출하는 것인데 어떻게 호출하냐에 따라 데이터 손실을 불러올 수도 있다.

따라서 closesocket을 호출하기 전에 shut-down을 호출하여 단계별로 종료되도록 해야 한다.

shutdown

모든 데이터 송수신이 끝나면 더 이상 전송할 데이터가 없다는 걸 상대방에게 알려줘야 한다.

이러한 작업을 graceful close라고 하고 shutdown 함수에 의해 이루어진다.

int shutdown(
 SOCKET s,
 int how
);

how : SD_RECEIVE, SD_SEND, SD_BOTH 중 하나의 값을 가진다

     SD_RECEIVE - 이것을 사용하면 이후에 더 이상 수신함수를 호출할 수 없다.

                       하지만 이러한 행위가 더 낮은 프토토콜 레이어에는 영향을 주지 않는다.

                       TCP 소켓은 데이터가 수신되기 위해 대기중이거나 데이터가 이어서 도착하면 연결이 리셋된다.

                       UDP는 데이터 수신이 계속 가능해서 비연결형 프로토콜에서는 의미가 없다.

     SD_SEND - 더 이상 송신 함수의 호출이 불가능해진다. TCP 소켓에서는

                   모든 데이터가 전송된 이후에 FIN 패킷이 전송되고 수신측에 의해 FIN 패킷이 감지된다.

     SD_BOTH - 송신과 수신 둘다 불가능해진다.

모든 연결지향 프로토콜이 graceful closure를 지원하는 것은 아니라 프로토콜에 맞게 shutdown API를 사용해야 한다.

 

closesocket

int closesocket(SOCKET s);

closesocket을 호출하면 소켓 디스크립터를 시스템에 반납하게 된다.

이미 closesocket을 호출한 소켓으로 윈속 함수를 호출하면 WSAENOTSOCK에러가 발생된다

또한 이 소켓에 관련된 모든 리소드들도 반납되며 큐에 쌓여있던 데이터들은 제거가 된다.

 

비연결형 통신

비연결형 통신은 연결지향 통신과는 다르게 작동을 한다. 따라서 데이터를 주고받는 방식도 다르다.

IP에서 비연결형 통신은 UDP/IP를 통해 이루어진다.

UDP의 특징은 데이터 전송의 신뢰성을 제공하지 않는다는 것이다.

또한 다수의 목적지에 데이터를 전송할 수 있고 다수의 전송측으로부터 수신이 가능하다.

그리고 클라는 서버가 데이터를 받을 준비가 됐는지와는 상관없이 데이터를 전송하며

클라이언트는 내가 보낸 데이터가 정확히 전송되었는지 확인할 수 없다.데이터는 datagram에 실려 개별 메시지로 전송 

수신측은 연결지향 통신의 수신측에서 약간의 변화만 주면 되기 때문에 수신측을 우선 살펴볼 것이다.

 

데이터 수신

 비연결형에서 데이터의 수신은 다음과 같이 이루어진다.

 1. socket이나 WSASocket을 이용해 소켓을 생성한다.

 2. 데이터를 받을 인터페이스의 포트로 bind 함수를 이용해 바인드한다.

 3. listen이나 accept를 사용하지 않고 들어오는 데이터를 수신하면 된다.

연결이 없기 때문에 네트워크 상의 어떤 장치로부터도 데이터를 받을 수 있다.

가장 간단한 수신 함수인 recvfrom은 다음과 같이 정의되어 있다.

int recvfrom(
 SOCEKT s,
 char FAR* buf,
 int len,
 int flags,
 struct sockaddr FAR* from,
 int FAR* fromlen
);

 from : 구조체 sockaddr의 포인터로 소켓의 프로토콜에 따라 구성

 fromlen : from에서 사용한 구조체의 크기

recvfrom이 데이터를 받아 리턴되면 from에는 데이터를 보낸 쪽의 주소가 채워진다.

 

int WSARecvFrom(
 SOCKET s,
 LPWSABUF lpBuffers,
 DWORD dwBufferCount,
 LPDWORD lpNumberOfBytesRecvd,
 LPDWORD lpFlags,
 struct sockaddr FAR * lpFrom,
 LPINT lpFromlen,
 LPWSAOVERLAPPED lpOverlapped,
 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

lpFrom : WSARecvFrom이 리턴되면 전송측에 대한 정보가 lpFrom에 담겨져 온다.

lpFromlen : lpFrom의 크기를 알려준다.

그 외의 파라미터는 이 글 위에 보면 WSARecv 함수가 있는데 거기서 사용하는 용도랑 똑같다.

 

비연결형 소켓에서 데이터를 수신하는 또 다른 방법은 연결(connection)을 사용하는 것이다.

비연결형 소켓을 생성하고 sockaddr에 통신할 상대방의 주소를 입력하여 connect나 WSAConnect를 호출할 수 있다.

함수를 호출했다고 실제로 연결된 것은 아니다. connect 함수를 통해 주소를 입력햇으므로

recv나 WSARecv를 recvfrom이나 WSARecvFrom 대신 사용 할 수 있다.

만인 한번에 하나의 지점하고만 통신하고자 한다면 데이터그램 소켓을 연결하는 것은 어렵지 않다.

 

데이터 송신

 비연결형 소켓에서 데이터를 전송하는 두 가지 함수는 sendto와 WSASendTo가 있다.

int sendto(
 SOCKET s,
 const char FAR * buf,
 int len,
 int flags,
 const struct sockaddr FAR * to,
 int tolen
);

buf와 len은 전송할 데이터의 정보라는 점을 제외하고 recvfrom과 파라메터의 의미는 비슷하다.

to는 sockaddr의 포인터로 데이터를 전송할 목적지의 주소를 담고 있다.

이 함수의 윈속 2 버전은 다음과 같다.

int WSASendTo(
 SOCKET s,
 LPWSABUF lpBuffers,
 DWORD dwBufferCount,
 LPDWORD lpNumberOfBytesSent,
 DWORD dwFlags,
 const struct sockaddr FAR * lpTo,
 int iToLen,
 LPWSAOVERLAPPED lpOverlapped,
 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpComletionRoutine
);

WSASendTo는 sendto와 유사하다. 

다른 점으로는 lpto와 iToLen이 있다.

lpto : 프로토콜에 알맞은 sockaddr의 주소로 수신측의 주소를 입력해야 한다.

iToLen : sockaddr의 크기를 의미한다.

나머지 파라메터는 WSASend와 똑같다.

 

비연결형 소켓도 데이터를 보낼 상대방에 연결하는 것이 가능하다. 

이런 경우에는 send나 WSASend를 통해 데이터를 송신한다.

한 번 연결이 이루어지면 다른 주소를 쓴다고 해도 연결한 주소 이외에 다른 주소로 데이터를 전송할 수 없다.

만일 연결한 주소와 다른 주소로 데이터를 전송하면 WSAEISCONN에러가 발생한다.

이 주소로부터 소켓 핸들을 떼어내는 유일한 방법은 목적지 주소로 INADDR_ANY로 설정하여 다시 connect를 호출하는 방법이다.

 

메시지 기반의 프로토콜

비연결형 통신의 대부분은 메시지 기반의 프로토콜이다.

메시지 기반의 프로토콜은 하나의 구분된 데이터 블록으로 전송하기 때문에

데이터를 전송하는 함수는 작업을 끝날 때까지 블록된다.

non-blocking I/O mode에선 전송함수를 호출했을 때 전송 작업이 완전히 끝나지 못했다면

전송 함수는 WSAEWOULDBLOCK 에러를 발생할 것이다.

이런 경우는 이 데이터를 처리할 수 없음을 의미하며 다음에 재전송 해야한다.

기억해야할 것은 스트리밍 프로토콜처럼 전송되지 않은 부분에 대해 재전송하는 루틴을 만들지 않는다는 것이다.

그리고 수신 함수를 호출할 때 충분히 큰 버퍼를 제공하여 수신해야 한다.

만약 데이터의 크기보다 버퍼의 크기가 작다면 WSAEMSGSIZE에러가 발생하며 버퍼의 크기만큼만

데이터가 채워지게 된다. 잘려나간 데이터는 다시 읽을 수 없다.

프로토콜의 데이터그램이 partial message를 지원한다면 WSARecv 류의 함수를 사용하는 것이 좋다.

왜냐하면 recv나 recvfrom을 사용하면 데이터가 partial message인지 파악할 수 없기 때문이다.

 

하나의 UDP/IP 메시지를 다수의 네트워크 인터페이스를 통해 전송할 수 있는가?

UDP 소켓의 로컬 IP에 명시적으로 바인드하고 데이터를 전송하면 어떻게 될까?

UDP 소켓은 실제로 네트워크 인터페이스에 바인드 되는 것이 아니다.

단지 데이터그램을 전송할 때 소스 IP주소로 참조될 IP를 설정하는 것이다.

실제로는 라우팅 테이블이 데이터가 전송될 물리적 인터페이스를 선택한다.

따라서 먼저 bind를 호출하면 소스 IP주소가 실제 데이터그램을 전송하는 주소와 다를 수 있다

 

소켓 리소스 반납

 비연결형 프로토콜에서는 연결이라는 것이 없어서 리소스를 반납할 것이 없다.

 

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함