출처: TCP/IP 윈도우 소켓 프로그래밍(https://product.kyobobook.co.kr/detail/S000001636201)
데이터 전송 함수는 크게 데이터를 보내는 함수와 받는 함수로 구분할 수 있다. 가장 기본이 되는 함수는 send()
와 recv()
이며, 그 외에 다양한 형태의 확장 함수가 존재한다. 여기에서는 기본 함수만 살펴 본다.
위의 그림은 TCP 소켓과 연관된 데이터 구조체다.(UDP 소켓 연관 구조체는 별도로 존재한다.) 지역/원격 IP 주소, 포트 번호 외에 송수신 버퍼가 존재한다.
송신 버퍼는 데이터를 전송하기 전에 임시로 저장해두는 영역이고, 수신 버퍼는 받은 데이터를 응용 프로그램이 처리하기 전까지 임시로 저장해두는 영역이다. 송수신 버퍼를 통틀어 소켓 버퍼라 부르며 send
와 recv
함수는 소켓 버퍼에 접근할 수 있게 만든 함수라고 보면 된다.
TCP는 응용 프로그램이 보낸 데이터의 경계를 구분하지 않는다는 특징이 있다. 예를 들어, 클라이언트가 100, 200, 300 바이트 데이터를 차례로 보낼 경우 서버가 데이터의 경계를 구분하지 못해 250, 350 바이트 단위로 데이터를 읽을 수 있다는 것이다.
따라서 TCP 서버 클라이언트 프로그램을 작성할 때는 데이터 경계 구분을 위한 상호 약속이 필요하며, 이를 응용 프로그램 수준에서 처리해야 한다. 효과적인 데이터 송수신 방법은 나중에 살펴 본다.
1. send()
send()
함수는 응용 프로그램 데이터를 운영체제의 송신 버퍼에 복사함으로써 데이터를 전송한다. 데이터 복사가 성공하면 곧바로 리턴한다.
send
함수가 리턴 됐다고 실제로 데이터가 상대방에게 전송된 것은 아니며, 일정 시간이 지나 하부 프로토콜에서 실제 전송이 진행된다.(응용 프로그램 -> TCP / IP)
int send(
SOCKET s,
const char* buf,
int len,
int flags
);
s
: 통신할 대상과 연결된 소켓이다.buf
: 보낼 데이터를 담고 있는 응용 프로그램 버퍼의 주소다.len
: 보낼 데이터 크기다.flags
:send
함수의 동작을 바꾸는 옵션이다. 대부분 0으로 사용하며 다양한 옵션을 설정할 수 있다.
send
함수는 첫 번째 인자로 전달되는 소켓의 특성에 따라 다음과 같이 두 종류의 성공적인 리턴을 할 수 있다.
- 블로킹 소켓: 지금까지 생성한 소켓은 모두 블로킹 소켓이다. 블로킹 소켓을 대상으로
send
함수를 호출하면, 송신 버퍼의 여유 공간이len
보다 작을 경우에는 해당 프로세스가 대기 상태가 된다. 송신 버퍼에 충분한 공간이 생기면 프로세스가 활성화되고len
크기만큼 복사가 진행된 후send
함수가 리턴한다. 리턴값은len
과 같다. - 논블로킹 소켓:
ioctlsocket
을 사용하면 논블로킹 소켓을 사용할 수 있다. 논블로킹 소켓을 대상으로send
함수를 호출하면 송신 버퍼의 여유 공간만큼 데이터를 복사한 후 실제 복사한 바이트 수를 리턴한다. 리턴값은 최수 1부터 최대len
까지 범위를 가진다.
2. recv()
recv()
함수는 운영체제의 수신 버퍼에 도착한 데이터를 응용 프로그램 버퍼에 복사한다.
int recv(
SOCKET s,
char* buf,
int len,
int flags
);
s
: 통신할 대상과 연결된 소켓이다.buf
: 받은 데이터를 저장할 응용 프로그램의 버퍼다.len
: 운영체제의 수신 버퍼로부터 복사할 최대 데이터 크기다. 이 값은buf
가 가리키는 응용 프로그램 버퍼보다 크지 않아야 한다.flags
:recv
함수의 동작을 바꾸는 옵션으로, 대부분 0을 사용한다.
recv
함수는 두 종류의 성공적인 리턴을 할 수 있다.
- 수신 버퍼에 데이터가 도달한 경우:
recv
함수의 세 번째 인자인len
보다 크지 않은 범위에서 가능하면 많은 데이터를 응용 프로그램 버퍼에 복사한 후 실제 복사한 바이트 수를 리턴한다. 최소 1, 최대len
의 범위를 가진다. - 접속이 정상 종료한 경우: 상대편 응용 프로그램이
closesocket
을 호출해 접속을 종료하면, TCP 수준에서 접속 종료를 위한 패킷 교환 절차가 일어난다.(4-way-handshaking) 이 경우recv
함수는 0을 리턴한다.
recv
함수 사용 시 주의 사항은 len
으로 지정한 크기보다 적은 데이터가 응용 프로그램 버퍼에 복사될 수 있다는 부분이다. 이는 TCP가 데이터 경계를 구분하지 않는다는 특성에 기인한다. 따라서 자신이 받을 데이터의 크기를 미리 알고 있다면 그만큼 받을 때까지 recv
함수를 여러 번 호출해야 한다.
예제에서는 사용자 정의 함수 recvn
을 정의해서 이를 편리하게 처리한다.
int recvn(SOCKET s, char* buf, int len, int flags) {
int received;
char* ptr = buf;
int left = len;
// 받아야 되는 데이터가 남아 있으면 반복
while(left > 0) {
received = recv(s, ptr, len, flags);
if(received == SOCKET_ERROR)
return SOCKET_ERROR;
// 상대방이 접속이 종료된 상황
else if(received == 0)
break;
left -= received;
ptr += received;
}
return (len - left);
}
TCP 클라이언트에서 send
, recv
는 다음과 같이 사용 했다.
// 데이터 통신에 사용할 변수
char buf[BUFSIZE + 1];
int len;
// 서버와 데이터 통신
while (1) {
// 데이터 입력
printf("\n[보낼 데이터] ");
if (fgets(buf, BUFSIZE + 1, stdin) == NULL)
break;
// '\n' 문자 제거
len = strlen(buf);
if (buf[len - 1] == '\n')
buf[len - 1] = '\0';
if (strlen(buf) == 0)
break;
// 데이터 보내기
retval = send(sock, buf, strlen(buf), 0);
if (retval == SOCKET_ERROR) {
err_display("send()");
break;
}
printf("[TCP 클라이언트] %d바이트를 보냈습니다.\n", retval);
// 데이터 받기
// 블로킹 소켓에서 send의 리턴값은 복사된 데이터의 길이이므로
// retval을 recv에서 받아야 할 데이터의 크기로 사용 가능
retval = recvn(sock, buf, retval, 0);
if (retval == SOCKET_ERROR) {
err_display("recv()");
break;
}
else if (retval == 0)
break;
// 받은 데이터 출력
buf[retval] = '\0';
printf("[TCP 클라이언트] %d바이트를 받았습니다.\n", retval);
printf("[받은 데이터] %s\n", buf);
}
TCP 서버에서는 다음과 같이 사용 했다.
while (1) {
// ...
// accept 호출 후 클라이언트 접속 대기
addrlen = sizeof(clientaddr);
client_sock = accept(listen_sock, (SOCKADDR *)&clientaddr, &addrlen);
if (client_sock == INVALID_SOCKET) {
err_display("accept()");
break;
}
// 클라이언트 접속 중
while (1) {
// BUFSIZE만큼 수신 버퍼에서 데이터 복사
retval = recv(client_sock, buf, BUFSIZE, 0);
if (retval == SOCKET_ERROR) {
err_display("recv()");
break;
}
// 복사된 데이터 크기가 0이면 접속 종료
else if (retval == 0)
break;
buf[retval] = '\0';
printf("[TCP/%s:%d] %s\n", inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port), buf);
// 클라이언트에게 데이터 전송
retval = send(client_sock, buf, retval, 0);
if (retval == SOCKET_ERROR) {
err_display("send()");
break;
}
}
// ...
// 클라이언트 접속 종료 후 서버 소켓 닫기
closesocket(client_sock);
}
'네트워크 > 윈도우 소켓 프로그래밍' 카테고리의 다른 글
멀티 스레드 서버를 위한 스레드 기초 (0) | 2024.04.08 |
---|---|
응용 프로그램 데이터 전송 (0) | 2024.04.08 |
TCP 클라이언트 함수 (0) | 2024.04.05 |
TCP 서버 함수 (0) | 2024.04.05 |
소켓 주소 구조체 다루기 (0) | 2024.04.04 |