[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