Chapter 04. TCP 서버/클라이언트
TCP 서버/클라이언트의 기본 구조와 동작 원리를 이해한다. TCP 애플리케이션 작성에 필요한 소켓 함수를 익힌다. 학습 목표 TCP 서버/클라이언트의 기본 구조와 동작 원리를 이해한다. TCP 애플리케이션 작성에 필요한 소켓 함수를 익힌다. 애플리케이션 프로토콜의 필요성을 이해하고, 메시지 설계 기법을 익힌다.
TCP 서버/클라이언트 예 TCP 서버/클라이언트 동작 원리 (1/6) 웹 서버 웹 클라이언트 웹 클라이언트 GET / HTTP/1.1 Accept: image/gif, ... <HTML> <HEAD>...</HEAD>... 웹 서버 웹 클라이언트 웹 클라이언트
TCP 서버/클라이언트 동작 방식 TCP 서버/클라이언트 동작 원리 (2/6) TCP 서버 TCP 클라이언트 listen accept recv send connect 네트워크
TCP 서버/클라이언트 동작 방식 (cont’d) ① 서버는 먼저 실행하여 클라이언트가 접속하기를 기다린다(listen). ② 클라이언트가 서버에게 접속(connect)하여 데이터를 보낸다(send). ③ 서버는 클라이언트 접속을 수용하고(accept), 클라이언트가 보낸 데이터를 받아서(recv) 처리한다. ④ 서버는 처리한 데이터를 클라이언트에게 보낸다(send). ⑤ 클라이언트는 서버가 보낸 데이터를 받아서(recv) 자신의 목적에 맞게 사용한다.
TCP 서버/클라이언트 동작 원리 TCP 서버/클라이언트 동작 원리 (4/6) TCP 서버 대기 TCP 서버 TCP 클라이언트 #1 클라이언트 접속
TCP 서버/클라이언트 동작 원리 (cont’d) 클라이언트 #1 통신 대기 TCP 서버 TCP 클라이언트 #1 클라이언트 #2 통신 대기
TCP 서버/클라이언트 동작 원리 (cont’d) 클라이언트 #1 대기 클라이언트 #n . . .
예제 동작 방식 TCP 서버/클라이언트 예제 TCP 클라이언트 TCP 서버 fgets() send() printf() recv()
TCP/IP 소켓 통신을 위해 필요한 요소 ① 프로토콜 ② 지역(local) IP 주소와 지역 포트 번호 소켓을 생성할 때 결정 ② 지역(local) IP 주소와 지역 포트 번호 서버 또는 클라이언트 자신의 주소 ③ 원격(remote) IP 주소와 원격 포트 번호 서버 또는 클라이언트가 통신하는 상대방의 주소
소켓 데이터 구조체 TCP 서버/클라이언트 분석 (2/2) 서버 클라이언트 애플리케이션 운영체제 네트워크 지역 IP 주소 지역 포트 번호 원격 IP 주소 원격 포트 번호 클라이언트 애플리케이션 운영체제 네트워크 • • •
TCP 서버 함수 TCP 서버 함수 (1/8) socket() bind() recv() send() closesocket() connect() listen() accept() 네트워크
bind() 함수 서버의 지역 IP 주소와 지역 포트 번호를 결정 TCP 서버 함수 (2/8) int bind ( SOCKET s, const struct sockaddr* name, int namelen ) ; 성공: 0, 실패: SOCKET_ERROR
bind() 함수 사용 예 TCP 서버 함수 (3/8) 050 SOCKADDR_IN serveraddr; 051 ZeroMemory(&serveraddr, sizeof(serveraddr)); 052 serveraddr.sin_family = AF_INET; 053 serveraddr.sin_port = htons(9000); 054 serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); 055 retval = bind(listen_sock, (SOCKADDR *)&serveraddr, sizeof(serveraddr)); 056 if(retval == SOCKET_ERROR) err_quit("bind()");
listen() 함수 소켓과 결합된 TCP 포트 상태를 LISTENING으로 변경 TCP 서버 함수 (4/8) int listen ( SOCKET s, int backlog ) ; 성공: 0, 실패: SOCKET_ERROR
listen() 함수 사용 예 TCP 서버 함수 (5/8) 059 retval = listen(listen_sock, SOMAXCONN); 060 if(retval == SOCKET_ERROR) err_quit("listen()");
accept() 함수 접속한 클라이언트와 통신할 수 있도록 새로운 소켓을 생성하여 리턴 TCP 서버 함수 (6/8) accept() 함수 접속한 클라이언트와 통신할 수 있도록 새로운 소켓을 생성하여 리턴 접속한 클라이언트의 IP 주소와 포트 번호를 알려줌 SOCKET accept ( SOCKET s, struct sockaddr* addr, int* addrlen ) ; 성공: 새로운 소켓, 실패: INVALID_SOCKET
accept() 함수 사용 예 TCP 서버 함수 (7/8) 062 // 데이터 통신에 사용할 변수 062 // 데이터 통신에 사용할 변수 063 SOCKET client_sock; 064 SOCKADDR_IN clientaddr; 065 int addrlen; ... 068 while(1){ 069 // accept() 070 addrlen = sizeof(clientaddr); 071 client_sock = accept(listen_sock, (SOCKADDR *)&clientaddr, &addrlen); 072 if(client_sock == INVALID_SOCKET){ 073 err_display("accept()"); 074 continue; 075 }
accept() 함수 사용 예 (cont’d) TCP 서버 함수 (8/8) accept() 함수 사용 예 (cont’d) 076 printf("\n[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n", 077 inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); 078 079 // 클라이언트와 데이터 통신 080 while(1){ ... 101 } 102 103 // closesocket() 104 closesocket(client_sock); 105 printf("[TCP 서버] 클라이언트 종료: IP 주소=%s, 포트 번호=%d\n", 106 inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); 107 }
TCP 클라이언트 함수 TCP 클라이언트 함수 (1/3) TCP 서버 TCP 클라이언트 socket() socket() bind() listen() 네트워크 accept() connect() recv() send() send() recv() closesocket() closesocket()
connect() 함수 서버에게 접속하여 TCP 프로토콜 수준의 연결 설정 TCP 클라이언트 함수 (2/3) int connect ( SOCKET s, const struct sockaddr* name, int namelen ) ; 성공: 0, 실패: SOCKET_ERROR
connect() 함수 사용 예 TCP 클라이언트 함수 (3/3) 070 SOCKADDR_IN serveraddr; 071 serveraddr.sin_family = AF_INET; 072 serveraddr.sin_port = htons(9000); 073 serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); 074 retval = connect(sock, (SOCKADDR *)&serveraddr, sizeof(serveraddr)); 075 if(retval == SOCKET_ERROR) err_quit("connect()");
소켓 데이터 구조체 데이터 전송 함수 (1/10) 서버 클라이언트 애플리케이션 운영체제 네트워크 지역 IP 주소 지역 포트 번호 원격 IP 주소 원격 포트 번호 클라이언트 애플리케이션 운영체제 네트워크 • • • 수신 버퍼 송신 버퍼
데이터 전송 함수 (2/10) send() 함수 애플리케이션 데이터를 송신 버퍼에 복사함으로써 궁극적으로 하부 프로토콜(예를 들면, TCP/IP)에 의해 데이터가 전송되도록 함 int send ( SOCKET s, const char* buf, int len, int flags ); 성공: 보낸 바이트 수, 실패: SOCKET_ERROR
recv() 함수 수신 버퍼에 도착한 데이터를 애플리케이션 버퍼로 복사 데이터 전송 함수 (3/10) int recv ( SOCKET s, char* buf, int len, int flags ); 성공: 받은 바이트 수 또는 0(연결 종료시), 실패: SOCKET_ERROR
데이터 전송 함수 (4/10) recvn() 함수 037 int recvn(SOCKET s, char *buf, int len, int flags) 038 { 039 int received; 040 char *ptr = buf; 041 int left = len; 042 043 while(left > 0){ 044 received = recv(s, ptr, left, flags); 045 if(received == SOCKET_ERROR) 046 return SOCKET_ERROR; 047 else if(received == 0) 048 break; 049 left -= received; 050 ptr += received; 051 } 052 053 return (len - left); 054 }
데이터 전송 함수 (5/10) recvn() 함수 동작 원리 buf ptr left len 읽은 데이터
데이터 전송 함수 사용 예 – TCP 클라이언트 데이터 전송 함수 (6/10) 078 char buf[BUFSIZE+1]; 079 int len; ... 082 while(1){ 083 // 데이터 입력 084 ZeroMemory(buf, sizeof(buf)); 085 printf("\n[보낼 데이터] "); 086 if(fgets(buf, BUFSIZE+1, stdin) == NULL) 087 break; 088 089 // '\n' 문자 제거 090 len = strlen(buf); 091 if(buf[len-1] == '\n') 092 buf[len-1] = '\0'; 093 if(strlen(buf) == 0) 094 break;
데이터 전송 함수 사용 예 – TCP 클라이언트 (cont’d) 데이터 전송 함수 (7/10) 데이터 전송 함수 사용 예 – TCP 클라이언트 (cont’d) 096 // 데이터 보내기 097 retval = send(sock, buf, strlen(buf), 0); 098 if(retval == SOCKET_ERROR){ 099 err_display("send()"); 100 break; 101 } 102 printf("[TCP 클라이언트] %d바이트를 보냈습니다.\n", retval); 103 104 // 데이터 받기 105 retval = recvn(sock, buf, retval, 0); 106 if(retval == SOCKET_ERROR){ 107 err_display("recv()"); 108 break; 109 } 110 else if(retval == 0) 111 break;
데이터 전송 함수 사용 예 – TCP 클라이언트 (cont’d) 데이터 전송 함수 (8/10) 데이터 전송 함수 사용 예 – TCP 클라이언트 (cont’d) 113 // 받은 데이터 출력 114 buf[retval] = '\0'; 115 printf("[TCP 클라이언트] %d바이트를 받았습니다.\n", retval); 116 printf("[받은 데이터] %s\n", buf); 117 }
데이터 전송 함수 사용 예 – TCP 서버 데이터 전송 함수 (9/10) 066 char buf[BUFSIZE+1]; ... 080 while(1){ 081 // 데이터 받기 082 retval = recv(client_sock, buf, BUFSIZE, 0); 083 if(retval == SOCKET_ERROR){ 084 err_display("recv()"); 085 break; 086 } 087 else if(retval == 0) 088 break; 089 090 // 받은 데이터 출력 091 buf[retval] = '\0'; 092 printf("[TCP/%s:%d] %s\n", inet_ntoa(clientaddr.sin_addr), 093 ntohs(clientaddr.sin_port), buf);
데이터 전송 함수 사용 예 – TCP 서버 (cont’d) 데이터 전송 함수 (10/10) 데이터 전송 함수 사용 예 – TCP 서버 (cont’d) 095 // 데이터 보내기 096 retval = send(client_sock, buf, retval, 0); 097 if(retval == SOCKET_ERROR){ 098 err_display("send()"); 099 break; 100 } 101 }
애플리케이션 프로토콜 애플리케이션 프로토콜 예 애플리케이션 프로토콜과 메시지 설계 (1/3) 애플리케이션 프로토콜 애플리케이션 수준에서 주고 받는 데이터의 형식과 의미, 그리고 처리 방식 등을 정의한 프로토콜 애플리케이션 프로토콜 예 네트워크
메시지 정의 ① 메시지 정의 ② 애플리케이션 프로토콜과 메시지 설계 (2/3) struct DrawMessage1 { int x1, y1; // 선의 시작점 int x2, y2; // 선의 끝점 int width; // 선 두께 int color; // 선 색상 }; struct DrawMessage2 { int x1, y1; // 원의 중심 좌표 int r; // 원의 반지름 int fillcolor; // 내부 색상 int width; // 선 두께 int color; // 선 색상 };
메시지 정의 ③ 애플리케이션 프로토콜과 메시지 설계 (3/3) struct DrawMessage1 { int type; // = LINE int x1, y1; // 선의 시작점 int x2, y2; // 선의 끝점 int width; // 선 두께 int color; // 선 색상 }; struct DrawMessage2 int type; // = CIRCLE int x1, y1; // 원의 중심 좌표 int r; // 원의 반지름 int fillcolor; // 내부 색상 int width; // 선 두께 int color; // 선 색상
경계 구분 [송신자] [수신자] 메시지 설계시 고려 사항 (1/2) ① 항상 고정 길이 데이터를 보낸다. ② 경계 구분을 위해 특별한 표시(EOR; End Of Record)를 삽입한다. ③ 보낼 데이터 길이를 고정 길이 데이터로 보낸 후, 가변 길이 데이터를 이어서 보낸다. [수신자] ① 항상 고정 길이 데이터를 받는다. ② EOR이 나올 때까지 데이터를 읽은 후 처리한다. ③ 고정 길이 데이터를 읽어 뒤따라올 데이터의 길이를 알아낸다. 이 길이만큼 데이터를 읽어 처리한다.
바이트 정렬 멤버 정렬 빅 엔디안 방식으로 통일 구조체(공용체, 클래스 포함) 멤버의 시작 주소에 대한 제약 사항 메시지 설계시 고려 사항 (2/2) 바이트 정렬 빅 엔디안 방식으로 통일 멤버 정렬 구조체(공용체, 클래스 포함) 멤버의 시작 주소에 대한 제약 사항 #pragma pack 컴파일러 명령을 사용