[TCP] Iterative echo server
TCP 기반 Iterative echo server와 client를 구현하고, 이에 대한 한계점을 분석해보자.
Apr 09, 2024
✅ Iterative echo server & client 실행하기
- 우분투에서 2개의 terminal을 연다. 하나는 server용, 다른하나는 client 용
- Lab1 디렉토리를 생성한다.
- 그 디렉토리안에 echo_server.c 및 echo_client.c를 각각 만들고 컴파일하여 수행화일을 만든다.
- serve용 terminal에서 echo_server 코드를 먼저 수행하고, client용 terminal에서 echo_client 코드를 수행한다. (다음 페이지 화면 참고).
- client terminal에서 message를 입력하여 그 message가 echo 되는 걸 확인한다.
- client terminal에서 Q를 입력하면 client 코드가 수행 종료됨을 확인한다.
- server terminal에서 control-c를 입력하여 server 코드를 강제 종료한다.
#include <stdio.h>
#include <string.h>
#include <stdlib.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 serv_sock;
int clnt_sock;
char message[BUF_SIZE];
int str_len, i;
struct sockaddr_in serv_adr;
struct sockaddr_in clnt_adr;
socklen_t clnt_addr_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_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");
}
if (listen(serv_sock, 5) == -1) {
error_handling("listen() error");
}
clnt_addr_sz = sizeof(clnt_adr);
for (i=0; i<5; i++) {
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_addr_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);
}
#include <stdio.h>
#include <string.h>
#include <stdlib.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;
struct sockaddr_in serv_addr;
char message[BUF_SIZE];
int str_len, recv_len, recv_cnt;
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_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -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;
str_len = write(sock, message, strlen(message));
recv_len = 0;
while (recv_len < str_len) {
recv_cnt = read(sock, &message[recv_len], BUF_SIZE-1);
if (recv_cnt == -1) {
error_handling("read() error!");
}
recv_len += recv_cnt;
}
message[recv_len] = 0;
printf("Message from server: %s\n", message);
}
close(sock);
return 0;
}
void error_handling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

✅ server 와 client 코드 내 소켓 함수 실행 순서(timing) 제약
- 위의 4번 과정에서 echo_client 코드를 먼저 수행하고, echo_server 코드를 나중에 수행한다
- echo_client 수행이 실패하는데 그 이유를 소스코드 분석을 통해 찾아낸다.

[client 실행 실패 원인]
일반적인 클라이언트-서버 통신 모델에서:
- 서버 설정: 서버는 소켓을 초기화하고 포트에 바인딩한다. 그런 다음 들어오는 연결을 수신하기 시작한다.
- 클라이언트 연결: 클라이언트가 소켓을 생성하고 서버에 연결을 시도한다.
- 서버가 연결 수락: 클라이언트가 성공적으로 연결되면 서버가 연결을 수락한다.
이제 클라이언트가 서버보다 먼저 실행되는 시나리오를 고려해보자.
- 클라이언트 실행: 클라이언트 프로그램이 먼저 실행된다. 즉시
connect()
를 사용하여 서버와의 연결 설정을 시도한다.
- 서버 준비 안 됨: 이 시점에서는 서버가 아직 실행 중이 아니거나 연결을 수신 대기 중이 아니다. 소켓을 초기화하지도 않았다.
- 연결 시도: 서버가 준비되지 않았으므로 클라이언트의
connect()
함수가 오류를 반환한다. 이 오류는 일반적으로 지정된 IP 주소 및 포트에서 수신 대기하는 서버가 없기 때문에 연결 시도가 실패했음을 나타낸다.
- 클라이언트 오류 처리: 클라이언트 코드에는 오류 메시지 인쇄 또는 정상적으로 종료 등 이 상황을 처리하기 위한 오류 처리 루틴이 있을 수 있다.
요약하면 'connect()' 오류는 서버가 연결을 수락할 준비가 되기 전에 클라이언트가 서버에 연결을 시도하기 때문에 발생한다. 이 오류를 방지하려면 클라이언트 프로그램을 실행하기 전에 서버가 실행 중이고 연결을 수신하고 있는지 확인해야 한다. 따라서, 일반적으로 서버를 먼저 시작한 다음 클라이언트를 실행해야 한다. 이렇게 하면 클라이언트가 연결을 시도할 때 서버가 연결을 수락할 준비가 된다.
✅ iterative echo server의 한계점
- 우분투에서 3개의 terminal을 연다. 하나는 server용, 다른 2개는 client1 및 client2 용
- Lab1 디렉토리를 생성한다.
- 그 디렉토리안에 echo_server.c 및 echo_client.c를 각각 만들고 컴파일하여 수행화일을 만든다.
→ 2, 3번은 위에서 이미 만들었다!
- server용 terminal에서 echo_server 코드를 먼저 수행하고, client1 및 client2용 terminal에서 echo_client 코드를 각각 수행한다.
- client1 terminal에서 message를 입력하여 그 message가 echo 되는 걸 확인한다.
- client2 terminal에서 message를 입력 시도하나 입력되지 않는다.
- 서버가 client1 서비스중 client2 서비스가 되지 않는 이유를 echo_server 코드 분석을 통해 찾아낸다

[2번째 client부터 대기 상태에 빠지는 원인]
위 코드에서 서버는 루프를 사용하여 여러 클라이언트 연결을 순차적으로 처리하도록 설계되었다.
for (i=0; i<5; i++) {
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_addr_sz);
if (clnt_sock == -1) {
error_handling("accept() error");
} else {
printf("Connected client %d \n", i+1);
}
// ...
}
이 루프에서 서버는 클라이언트의 연결을 순차적으로 수락한다. 클라이언트가 연결되면 'accept()' 함수는 클라이언트 연결 요청이 수신될 때까지 차단된다. 연결 요청이 수신되면
accept()
는 클라이언트와의 통신을 위해 새로운 소켓 설명자(clnt_sock
)를 반환한다.만약 여러 클라이언트가 동시에 연결을 시도할 때 어떤 일이 발생할까?
- 첫 번째 클라이언트가 서버에 성공적으로 연결되고, 서버는
accept()
를 사용하여 연결을 수락한다.
- 첫 번째 클라이언트가 서비스를 받는 동안 나머지 클라이언트는 여전히 서버에 연결을 시도하고 있다.
- 서버가 첫 번째 클라이언트를 서비스하는 중이므로 다른 클라이언트의 후속
accept()
호출이 차단된다. 따라서, 서버가 연결 요청을 수락할 수 있을 때까지 기다리고 있다.
- 후속 클라이언트는 서버가 현재 클라이언트에 대한 서비스를 완료하고 해당 연결을 수락할 수 있게 될 때까지 대기 상태로 유지된다.
따라서 서버는 클라이언트 연결을 순차적으로 처리하며 여러 클라이언트가 동시에 연결을 시도하는 경우 첫 번째 클라이언트를 제외한 모든 클라이언트는 서버가 연결을 수락할 수 있을 때까지 대기 상태로 전환된다. 이 동작을 통해 서버는 여러 연결을 동시에 처리하지 않고 도착하는 순서대로 한 번에 하나씩 처리한다.
Share article