잡담
계산기 서버/클라이언트는 어느정도 생각을 해서 풀어야 하는 문제입니다. 클라이언트에서 서버에게 데이터를 어떻게 보내야 할 것인지에 대해서 생각을 해보고, 데이터를 받은 서버가 어떻게 계산해서 클라이언트에게 보내줄 것인지...
이렇게 생각하면서 문제를 풀어야 실력도 늘고, 막혔을 때 끙끙 앓다가 포기하는 것보다는 끝까지 구글링을 하면서, 결국에는 답을 찾아내는 것이 좋다고 생각합니다. 갑자기 왜 이런 소리를 하냐면 제가 이 문제를 풀 때 여태까지는 코드를 복붙만 해서 각 함수에 대한 기능만 알고 있지, 처음부터 끝까지 작성한 적이 없었거든요... 그래서 계산기 문제를 풀 때 좀 오래걸렸었어요. 제 기억에는 한 3시간 정도 붙들고 있었던 것 같아요.
이제 시작해보죠
클라이언트 입력 기능
클라이언트는 아래와 같이 입력을 받는 기능을 구현해야 합니다.
몇개의 숫자를 입력할 것인지에 대한 수 입력
위에서 입력받은 수만큼 숫자 입력
연산기호 입력(+, -, *)
위 그림에서 보이는 것과 같이 숫자 몇개를 입력받을 지 정하고, 그 다음에 지정한 수만큼 숫자들을 입력한 뒤에 연산기호를 입력받는 구조를 설계하면 됩니다. 그냥 서버가 없었던 상황에서는 위의 구조를 만들기가 쉬웠지만, 이게 서버라는 개념이 추가되면서 생각할 것들이 많아졌습니다.
- 클라이언트에서 데이터를 어떤 형식으로 몇 바이트를 보낼 것인지
- 서버에서는 클라이언트로부터 전송된 데이터를 어떤 방식으로 처리할 것인지
제가 이 문제를 풀 때는 책에 나와 있는 답을 안보고 풀었기 때문에 효율적이지 못한 방법일 수도 있습니다. 그래도 봐주시면 감사하겠습니다.
처음에는 이걸 어떻게 보낼까에 대해서 많이 생각을 했습니다. 입력한 모든 데이터를 한개의 배열에 담아서 보내볼까도 생각해봤지만 차례차례 보내는게 좀 더 수월할 것 같아서 3개의 변수를 만들어서 입력한 데이터를 순서대로 보냈습니다.
그래서 서버에서도 데이터를 순서대로 받아서 처리하기 때문에 제가 작성한 서버/클라이언트는 이해하기가 쉬울 것입니다. (책에서는 입력한 데이터를 모두 모아서 한번에 보내기 때문에 좀 생각해야 될 포인터 및 배열 연산이 필요합니다.)
아래의 코드는 제가 작성한 클라이언트 입니다. 서버와 연결하기 위한 코드만 없다면 그냥 일반적인 계산기 코드입니다.
// C++ 계산기 클라이언트 프로그램
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
void Error_Handling(const char* _message);
int main(int argc, char* argv[]){
if(argc!=3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(0);
}
int sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock==-1){
Error_Handling("socket() error!");
}
struct sockaddr_in serv_adr;
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
Error_Handling("connect() error!");
else
puts("Connected....");
// 몇개의 숫자를 입력받을지
// 그리고 입력한 크기만큼 데이터 전송
int count;
printf("Operand count : ");
scanf("%d", &count);
write(sock, &count, sizeof(int));
// 숫자 입력하고 입력한 크기만큼 데이터 전송
int *arr;
arr = new int[count];
for(int i = 0; i < count; i++){
printf("Operand %d : ", i+1);
scanf("%d", &arr[i]);
}
// 문자를 입력받기 위해서 미리 '\n'를 입력받아서
// 엔터값을 없애준다. int * 입력한 수 만큼 데이터 전송
fgetc(stdin);
write(sock, arr, sizeof(int)*count);
char oper = 0;
printf("Operator : ");
scanf("%c", &oper);
write(sock, &oper, sizeof(char));
// 서버로부터 전송된 데이터를 받아온다.
int result = 0;
if(read(sock, &result, sizeof(int))== -1)
Error_Handling("read() error!");
printf("Operation result : %d\n", result);
close(sock);
delete[] arr;
return 0;
}
void Error_Handling(const char* _message){
fputs(_message, stderr);
fputc('\n', stderr);
exit(1);
}
다음은 서버 코드 입니다. 서버에서는 클라이언트로부터 전송된 데이터를 순서대로 받아서 들어온 연산기호에 따라서 계산을 다르게 처리한 후 다시 클라이언트에게 연산 결과를 전송하고 있습니다.
// C++ 계산기 서버 프로그램
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
void Error_Handling(const char* _message);
int main(int argc, char* argv[]){
if(argc!=2){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
int serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock==-1)
Error_Handling("socket() error!");
struct sockaddr_in serv_adr;
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
Error_Handling("bind() error!");
int back_log = 2;
if(listen(serv_sock, back_log)==-1)
Error_Handling("listen() error!");
struct sockaddr_in clnt_adr;
socklen_t clnt_adr_sz = sizeof(clnt_adr);
int clnt_sock;
int count = 0;
int arr_len = 0;
int arr[10];
char oper = 0;
int result = 0;
for(int i = 0; i < back_log; i++){
// 클라이언트의 연결을 수락한 후에 새로운 소켓을 생성해서
// 새로 생성된 소켓으로 데이터를 송수신한다.
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
if(clnt_sock==-1)
Error_Handling("accept() error!");
printf("Connect client : %d\n", i+1);
// 클라이언트가 몇개의 숫자를 입력받는지 전달받는다.
count = 0;
// read Client count
if(read(clnt_sock, &count, sizeof(int))==-1)
Error_Handling("read() error!");
// 배열의 길이(숫자 개수만큼)만큼 데이터를 전달받는다.
arr_len = 0;
while(arr_len < count){
// 배열의 각 방에 데이터를 입력한다.
if(read(clnt_sock, &arr[arr_len++], sizeof(int))==-1)
Error_Handling("read() error!");
}
// 연산기호를 전달받는다.
if(read(clnt_sock, &oper, sizeof(char))==-1)
Error_Handling("read() error!");
result = arr[0];
switch(oper){
case '+':
for(int i = 1; i < count; i++)
result += arr[i];
break;
case '-':
for(int i = 1; i < count; i++)
result -= arr[i];
break;
case '*':
for(int i = 1; i < count; i++)
result *= arr[i];
break;
}
// 클라이언트에게 4바이트만큼의 데이터를 전송한다.
write(clnt_sock, &result, sizeof(int));
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void Error_Handling(const char* _message){
fputs(_message, stderr);
fputc('\n', stderr);
exit(1);
}
결론적으로 서버/클라이언트의 코드 흐름을 보면 클라이언트가 입력을 할 때마다 서버에게 데이터를 보내서 서버는 처리를 완료한 데이터를 클라이언트에게 전송합니다. 이로써 계산기의 서버/클라이언트에 대한 설명을 마쳤습니다.
마치며
윤성우님의 코드를 보고 싶은 분들은 제가 링크를 걸어드릴테니 회원가입을 하시고 자료실에 가셔서 다운 받으시면 됩니다. Chapter 5에 첨부되어 있습니다.
https://www.orentec.co.kr/jaryosil/TCP_IP_1/add_form.php
내일은 TCP에 대한 이론에 대해서 알아보는 시간을 가져보도록 하겠습니다.
오늘도 제 글을 읽어주신 분들 감사합니다. 혹시 코드의 잘못된 부분이나 개선해야 될 점을 알려주시면 빠르게 수정하도록 하겠습니다. 감사합니다.
'개발 > 리눅스' 카테고리의 다른 글
[Linux]TCP의 전송특성과 에코 클라이언트의 문제점 해결 (0) | 2020.04.01 |
---|---|
[Linux]Iterative 기반의 서버, 클라이언트 구현 (0) | 2020.03.31 |
[Linux]TCP 서버/클라이언트 구현 (1) | 2020.03.30 |