에코 서버와 에코 클라이언트를 구현
에코 서버는 클라이언트가 전송하는 데이터를 그대로 재전송하는, 말 그대로 echo(메아리)시키는 서버이다.
계속해서 들어오는 클라이언트의 연결 요청을 수락하기 위해서 서버는 accept를 반복적으로 수행할 수 있어야 한다.
While{
accept()
read() / write()
close(client)
}
하나의 클라이언트에 대한 서비스를 완료하면 또 다른 클라이언트의 요청을 수락해줘야 한다.
실행 할 Iterative 서버와 클라이언트 프로그램의 기본 동작 방식은 다음과 같다.
echo_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#incldue <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_hadling(char * message)
int main(int argc, char *argv[]){
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len, i;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if(argc != 2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1){
error_handling("socket() error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s = 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");
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_adr_sz = sizeof(clnt_adr);
for(i = 0; i < 5; i++){
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
if(clnt_sock == -1)
error_handling("accept() error");
else
printf("Connected client %d \n", i+1);
while((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
write(clnt_sock, message, str_len);
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void error_handling(char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
실행 결과는 다음과 같다.
$ gcc echo_server.c -o eserver
$ ./eserver 2929
Connected client 1
Connected client 2
Connected client 3
echo_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[]){
int sock;
char message[BUF_SIZE];
int str_len;
struct sock_addr_in serv_adr;
if(argc != 3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
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.....");
while(1){
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE - 1);
message[str_len] = 0;
printf("Message from server : %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
$ gcc echo_clinet.c -o eclient
$ ./eclient 127.0.0.1 2929
Connected.....
Input message(Q to quit) : Good
Message from server : Good
Input message(Q to quit) : Hi
Message from server : Hi
Input message(Q to quit) : q
$
에코 클라이언트의 다음 코드는 잘못된 가정이 존재한다.
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE - 1);
message[str_len] = 0;
printf("Message from server : %s", message);
위 코드에서는 read, write 함수가 호출될 때마다 문자열 단위로 실제 입출력이 이뤄진다고 가정하고 있다.
TCP의 경우 데이터의 경계가 존재하지 않기 때문에 둘 이상의 write 함수 호출로 전달된 문자열 정보가 묶여서 한번에 서버로 전송될 수 있다. 그러한 경우 클라이언트는 한번에 둘 이상의 문자열 정보를 서버로부터 받기 때문에 원하는 결과를 얻지 못할 수 있다.
즉, 서버는 한번의 write로 데이터 전송을 하지만, 데이터의 크기가 크다면 운영체제가 내부적으로 이를 여러 조각으로 나누어 클라이언트에게 전송할 수도 있다. 이 과정에서 모든 조각이 오지 않았음에도 불구하고 클라이언트는 read()를 호출하게 된다.