출처: TCP/IP 윈도우 소켓 프로그래밍(https://product.kyobobook.co.kr/detail/S000001636201)
TCP 서버 클라이언트에서 데이터 전송을 진행할 것이다. 클라이언트에서 서버로만 데이터를 보낼 수 있다고 가정한다. 클라이언트는 다음과 같은 데이터를 보낼 수 있다.
char* testdata[] = {
"안녕하세요",
"반가워요",
"오늘따라 할 이야기가 많을 것 같네요",
"저도 그렇네요"
};
for(int i=0; i<4; i++) {
// testdata[i] 전송
}
클라이언트는 testdata
라는 문자열 배열을 서버로 전송하게 될 것이다. 다양한 방법을 통해 이 데이터를 서버로 전송해보자.
1. 고정 길이 데이터 전송
고정 길이 데이터 전송은 개념도 쉽고 구현도 쉽다. 서버와 클라이언트 모두 크기가 같은 버퍼를 정의해두고 데이터를 주고받으면 된다.
먼저 서버에서는 클라이언트가 보낸 메시지를 받기만 하면 된다. 클라이언트와 서버가 모두 크기가 50 바이트로 고정된 버퍼를 사용한다고 가정하고 다음 코드를 살펴보자.
// TCP SERVER
// 버퍼 크기(고정)
#define BUFSIZE 50
// 클라이언트로부터 데이터를 수신하는 함수
int recv(SOCKET s, char* buf, int len, int flags) {
int received;
char* ptr = buf;
int left = len;
while(left > 0) {
received = recv(s, ptr, left, flags);
if(received == SOCKET_ERROR)
return SOCKER_ERROR;
else if(received == 0)
break;
left -= received;
ptr += received;
}
return (len - left);
}
int main()
{
// 서버 설정 코드 생략
// ...
// 통신에 사용할 변수
SOCKET client_sock;
SOCKADDR_IN clientaddr;
int addrlen;
// 버퍼
char buf[BUFSIZE+1];
while(1) {
// accept
// ...
// accept한 클라이언트와 통신
while(1) {
// recv하는 데이터의 크기를 BUFSIZE로 고정
retval = recvn(client_sock, buf, BUFSIZE, 0);
if(retval == SOCKET_ERROR)
{
err_display("recv()");
break;
}
else if(retval == 0)
break;
}
}
// ...
}
- 서버는 클라이언트가 전송한 데이터를 수신하는 역할만 한다.
- 수신하는 데이터의 크기는 50 바이트로 고정되어 있다.
클라이언트는 고정된 크기의 데이터를 전송하기만 하면 된다.
// TCP CLIENT
#define BUFSIZE 50
int main() {
// ..
// 데이터 통신에 사용할 변수
char buf[BUFSIZE];
char *testdata[] = {
"안녕하세요",
"반가워요",
"오늘따라 할 이야기가 많을 것 같네요",
"저도 그렇네요",
};
// 서버와 데이터 통신
for (int i = 0; i < 4; i++) {
// 데이터 입력(시뮬레이션)
memset(buf, '#', sizeof(buf));
strncpy(buf, testdata[i], strlen(testdata[i]));
// 데이터 보내기
retval = send(sock, buf, BUFSIZE, 0);
if (retval == SOCKET_ERROR) {
err_display("send()");
break;
}
printf("[TCP 클라이언트] %d바이트를 보냈습니다.\n", retval);
}
}
2. 가변 길이 데이터 전송
가변 길이 데이터를 주고받으려면 EOR로 사용할 데이터 패턴을 정의해야 한다. \r\n
이나 \n
을 EOR로 사용하는데, 여기에서는 \n
을 EOR로 사용한다.
가장 단순하게 이를 구현하는 방법은 서버에서 수신 버퍼를 1 바이트 단위로 받아 EOR을 검출하는 것이다. 데이터를 1 바이트씩 처리하기 때문에 성능이 떨어지는 방법이다. 보다 효율적으로 처리하려면 수신 버퍼에서 한 번에 데이터를 읽은 뒤 1 바이트 단위로 별도로 검출하는 것이 나을 것이다. 또한 EOR 문자가 데이터 내부에 존재할 수도 있다는 부분을 명심하자.
// TCP SERVER
#define BUFSIZE 512
// 내부 구현용 함수
int _recv_ahead(SOCKET s, char *p)
{
// __declspec 멀티 스레드에서도 정상적으로 동작하도록 만들기 위해 사용
__declspec(thread) static int nbytes = 0;
__declspec(thread) static char buf[1024];
__declspec(thread) static char *ptr;
// 한 번에 sizeof(buf)만큼 읽어서 그 크기를 nbytes에 저장
// 그 뒤 다시 함수가 호출되면 nbytes의 크기가 유효한 경우
// ptr만 옮기면서 데이터를 바이트 단위로 뽑아올 수 있음
if (nbytes == 0 || nbytes == SOCKET_ERROR) {
nbytes = recv(s, buf, sizeof(buf), 0);
if (nbytes == SOCKET_ERROR) {
return SOCKET_ERROR;
}
else if (nbytes == 0)
return 0;
ptr = buf;
}
--nbytes;
*p = *ptr++;
return 1;
}
// 사용자 정의 데이터 수신 함수
int recvline(SOCKET s, char *buf, int maxlen)
{
int n, nbytes;
char c, *ptr = buf;
for (n = 1; n < maxlen; n++) {
nbytes = _recv_ahead(s, &c);
if (nbytes == 1) {
*ptr++ = c;
// EOR 만난 경우
if (c == '\n')
break;
}
// 수신된 데이터 없음(접속 종료)
else if (nbytes == 0) {
// \0 입력
*ptr = 0;
return n - 1;
}
else
return SOCKET_ERROR;
}
*ptr = 0;
return n;
}
int main() {
// ...
while(1) {
// ...
// 클라이언트와 데이터 통신
while (1) {
// 데이터 받기
retval = recvline(client_sock, buf, BUFSIZE + 1);
if (retval == SOCKET_ERROR) {
err_display("recv()");
break;
}
else if (retval == 0)
break;
// 받은 데이터 출력
printf("[TCP/%s:%d] %s", inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port), buf);
}
// ...
}
}
- 서버 코드에서는
recvline
을 사용해서 전송된 데이터를 받게 된다. - EOR이 1 바이트인
\n
이기 때문에 이를 검출하기 위해서는 1 바이트씩 데이터 검사가 필요한데, 실제로 데이터를 수신하는_recv_ahead
함수에서는 1024 바이트만큼의 데이터를 수신 받아 static buf에 저장해 두고 포인터(ptr
)을 이동하면서 바이트에 저장된 문자를recvline
함수에 전달해준다.
클라이언트 코드는 다음과 같다.
// TCP CLIENT
#define BUFSIZE 50
int main() {
// ...
// 데이터 통신에 사용할 변수
char buf[BUFSIZE];
char *testdata[] = {
"안녕하세요",
"반가워요",
"오늘따라 할 이야기가 많을 것 같네요",
"저도 그렇네요",
};
int len;
// 서버와 데이터 통신
for (int i = 0; i < 4; i++) {
// 데이터 입력(시뮬레이션)
len = strlen(testdata[i]);
strncpy(buf, testdata[i], len);
buf[len++] = '\n';
// 데이터 보내기
retval = send(sock, buf, len, 0);
if (retval == SOCKET_ERROR) {
err_display("send()");
break;
}
printf("[TCP 클라이언트] %d바이트를 보냈습니다.\n", retval);
}
// ...
}
- 클라이언트에서는 전송할 데이터의 맨 끝에 EOR 문자를 추가해준다.
3. 고정 길이 + 가변 길이 데이터 전송
송신 측에서 가변 길이 데이터의 크기를 곧바로 계산할 수 있다면 고정 길이 + 가변 길이 데이터 전송이 효과적이다. 수신 측에서는 1번 - 고정 길이 데이터 수신, 2번 - 가변 길이 데이터 수신 두 번의 데이터 수신으로 가변 길이 데이터의 경계를 구분할 수 있다.
서버는 다음과 같다.
// TCP SERVER
#define BUFSIZE 512
// 사용자 정의 데이터 수신 함수
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, left, flags);
if (received == SOCKET_ERROR)
return SOCKET_ERROR;
else if (received == 0)
break;
left -= received;
ptr += received;
}
return (len - left);
}
// 서버 초기화, 소켓 생성, listen, accep 과정 생략
// ...
// 클라이언트와 데이터 통신
while (1) {
// 데이터 받기(고정 길이)
retval = recvn(client_sock, (char *)&len, sizeof(int), 0);
if (retval == SOCKET_ERROR) {
err_display("recv()");
break;
}
else if (retval == 0)
break;
// 데이터 받기(가변 길이)
retval = recvn(client_sock, buf, len, 0);
if (retval == SOCKET_ERROR) {
err_display("recv()");
break;
}
else if (retval == 0)
break;
// 받은 데이터 출력
buf[retval] = '\0';
printf("[TCP/%s:%d] %s\n", inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port), buf);
}
// ..
- 첫 번째로 수신 되는 고정 길이 데이터는 다음에 수신 될 가변 길이 데이터의 길이 정보를 포함하고 있기 때문에
int
사이즈 만큼 읽으면 된다. - 실제 데이터를 받을 때에는
len
만큼만 데이터를 읽으면 된다.
클라이언트는 다음과 같다.
// TCP CLIENT
#define BUFSIZE 50
// 데이터 통신에 사용할 변수
char buf[BUFSIZE];
char *testdata[] = {
"안녕하세요",
"반가워요",
"오늘따라 할 이야기가 많을 것 같네요",
"저도 그렇네요",
};
int len;
// 서버와 데이터 통신
for (int i = 0; i < 4; i++) {
// 데이터 입력(시뮬레이션)
len = strlen(testdata[i]);
strncpy(buf, testdata[i], len);
// 데이터 보내기(고정 길이)
retval = send(sock, (char *)&len, sizeof(int), 0);
if (retval == SOCKET_ERROR) {
err_display("send()");
break;
}
// 데이터 보내기(가변 길이)
retval = send(sock, buf, len, 0);
if (retval == SOCKET_ERROR) {
err_display("send()");
break;
}
printf("[TCP 클라이언트] %d+%d바이트를 "
"보냈습니다.\n", sizeof(int), retval);
}
- 클라이언트에서는 먼저 송신할 가변 길이 데이터의 길이를 보낸다. int 사이즈 고정 길이로 보내서 서버도 int 사이즈로 해당 데이터를 받게 된다.
- 가변 길이 데이터를 두 번째로 보낸다.
4. 데이터 전송 후 종료
데이터 전송 후 종료 방식은 가변 길이 데이터 전송 방식의 일종이라고 볼 수 있다. EOR을 사용하는 대신 연결 종료(수신 데이터 0)를 이용하는 것이다.
서버는 다음과 같다.
// TCP SERVER
#define BUFSIZE 1024
// 사용자 정의 데이터 수신 함수
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, left, flags);
if (received == SOCKET_ERROR)
return SOCKET_ERROR;
else if (received == 0)
break;
left -= received;
ptr += received;
}
return (len - left);
}
// 생략
// ...
// 클라이언트와 데이터 통신
while (1) {
// 데이터 받기
retval = recvn(client_sock, buf, BUFSIZE, 0);
if (retval == SOCKET_ERROR) {
err_display("recv()");
break;
}
else if (retval == 0)
break;
// 받은 데이터 출력
buf[retval] = '\0';
printf("[TCP/%s:%d] %s\n", inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port), buf);
}
- 서버는 그냥 전송되는 데이터를 읽기만 하면 된다.
- 클라이언트가 데이터를 한 번 보내고 접속을 종료하기 때문에 그 다음 recvn에서 break 처리 된다.
- 단, 서버에서 읽을 수 있는 최대 길이를 BUFSIZE로 지정했지만 클라이언트가 BUFSIZE보다 작은 데이터를 전송한 후 접속을 종료한다고 가정한다.
클라이언트는 다음과 같다.
// TCP CLIENT
#define BUFSIZE 50
// 데이터 통신에 사용할 변수
char buf[BUFSIZE];
char *testdata[] = {
"안녕하세요",
"반가워요",
"오늘따라 할 이야기가 많을 것 같네요",
"저도 그렇네요",
};
int len;
// 서버와 데이터 통신
for (int i = 0; i < 4; i++) {
// socket()
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) err_quit("socket()");
// connect()
retval = connect(sock, (SOCKADDR *)&serveraddr, sizeof(serveraddr));
if (retval == SOCKET_ERROR) err_quit("connect()");
// 데이터 입력(시뮬레이션)
len = strlen(testdata[i]);
strncpy(buf, testdata[i], len);
// 데이터 보내기(가변 길이로 보냄)
retval = send(sock, buf, len, 0);
if (retval == SOCKET_ERROR) {
err_display("send()");
break;
}
printf("[TCP 클라이언트] %d바이트를 보냈습니다.\n", retval);
// closesocket()
closesocket(sock);
}
- 클라이언트에서는 매 데이터를 전송할 때마다 소켓을 만들고 데이터를 전송한 뒤에 바로 소켓을 닫아 버린다.
'네트워크 > 윈도우 소켓 프로그래밍' 카테고리의 다른 글
멀티스레드 TCP 서버 (0) | 2024.04.08 |
---|---|
멀티 스레드 서버를 위한 스레드 기초 (0) | 2024.04.08 |
TCP 데이터 전송 함수 (0) | 2024.04.05 |
TCP 클라이언트 함수 (0) | 2024.04.05 |
TCP 서버 함수 (0) | 2024.04.05 |