TCP 서버/클라이언트 Chapter 04. * 학습목표 TCP 서버/클라이언트의 기본 구조와 동작 원리를 이해함 애플리케이션 프로토콜의 필요성을 이해하고, 메시지 설계 기법을 익힘
TCP 서버/클라이언트 동작 원리 - (1) TCP 서버/클라이언트 예 웹 서버 웹 클라이언트 웹 클라이언트 GET / HTTP/1.1 Accept: image/gif, ... <HTML> <HEAD>...</HEAD>... 웹 서버 웹 클라이언트 웹 클라이언트
TCP 서버/클라이언트 동작 원리 - (2) TCP 서버/클라이언트 동작 방식 TCP 서버 TCP 클라이언트 listen accept recv send connect 네트워크 TCP 서버/클라이언트 동작 방식 ① 서버는 먼저 실행하여 클라이언트가 접속하기를 기다린다(listen). ② 클라이언트가 서버에게 접속(connect)하여 데이터를 보낸다(send). ③ 서버는 클라이언트 접속을 수용하고(accept), 클라이언트가 보낸 데이터를 받아서(recv) 처리한다. ④ 서버는 처리한 데이터를 클라이언트에게 보낸다(send). ⑤ 클라이언트는 서버가 보낸 데이터를 받아서(recv) 자신의 목적에 맞게 사용한다.
TCP 서버/클라이언트 동작 원리 - (3) TCP 서버/클라이언트 동작 원리 TCP 서버 대기 클라이언트 접속 TCP 클라이언트 #1 클라이언트 접속 (1) 서버는 소켓을 생성 후 클라이언트를 기다림 포트번호 : 9000 (2) 클라이언트 접속함 (연결설정)
TCP 서버/클라이언트 동작 원리 - (4) TCP 서버/클라이언트 동작 원리 (계속) TCP 서버 대기 통신 TCP 클라이언트 #1 클라이언트 #2 통신 대기 (3) 클라이언트 새로운 소켓을 생성 (4) 1명의 클라이언트가 새로 접속하여 새로운 소켓을 생성한 상태
TCP 서버/클라이언트 동작 원리 - (5) TCP 서버/클라이언트 동작 원리 (계속) TCP 서버/클라이언트 예제 동작 방식 클라이언트 #1 대기 클라이언트 #n . . . (5) N명의 클라이언트가 새로 접속하여 새로운 소켓을 생성한 상태 (1 : 1 통신 구조) TCP 서버/클라이언트 예제 동작 방식 TCP 클라이언트 TCP 서버 fgets() send() printf() recv() 화면 화면 데이터 데이터
TCP 서버/클라이언트 분석 TCP/IP 소켓 통신을 위해 필요한 요소 소켓 데이터 구조체 ① 프로토콜 소켓을 생성할 때 결정 ② 지역(local) IP 주소와 지역 포트 번호 서버 또는 클라이언트 자신의 주소 ③ 원격(remote) IP 주소와 원격 포트 번호 서버 또는 클라이언트가 통신하는 상대방의 주소 소켓 데이터 구조체 서버 지역 IP 주소 지역 포트 번호 원격 IP 주소 원격 포트 번호 클라이언트 애플리케이션 운영체제 네트워크 • • •
TCP 서버 함수 - (1) TCP 서버 함수 socket() bind() recv() send() closesocket() connect() listen() accept() 네트워크
TCP 서버 함수 - (2) bind() 함수 bind() 함수 사용 예 (예제 1 참조) 서버의 지역 IP 주소와 지역 포트 번호를 결정 int bind ( SOCKET s, // 소켓생성 결과값 const struct sockaddr* name, // 소켓구조체(주소, 포트번호, 프로토콜타입) int namelen // 소켓구조체 크기 ) ; 성공: 0, 실패: SOCKET_ERROR bind() 함수 사용 예 (예제 1 참조) 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()");
TCP 서버 함수 - (3) listen() 함수 listen() 함수 사용 예 소켓과 결합된 TCP 포트 상태를 LISTENING으로 변경 int listen ( SOCKET s, // 소켓생성 결과값 int backlog // 연결큐의 길이 (최대 접속자수) ) ; 성공: 0, 실패: SOCKET_ERROR listen() 함수 사용 예 059 retval = listen (listen_sock, SOMAXCONN); 060 if(retval == SOCKET_ERROR) err_quit("listen()");
TCP 서버 함수 - (4) accept() 함수 accept() 함수 사용 예 접속한 클라이언트와 통신할 수 있도록 새로운 소켓을 생성하여 리턴 접속한 클라이언트의 IP 주소와 포트 번호를 알려줌 SOCKET accept ( SOCKET s, // 소켓(클라이언트전용) 생성 결과값 struct sockaddr* addr, // 클라이언트의 IP주소와 포트번호 지정된 메모리에 저장 int* addrlen // 지정된 메모리 주소값 ) ; 성공: 새로운 소켓, 실패: INVALID_SOCKET accept() 함수 사용 예 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 }
TCP 서버 함수 - (5) accept() 함수 사용 예 (계속) 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 }
실습 예제 – (1) 소요시간 : 1시간 30분. 직접 코딩해서 실습할 것. 매우 중요한 예제임. 각 라인별로 소스를 분석하시오.
TCP 클라이언트 함수 - (1) TCP 클라이언트 함수 네트워크 socket() bind() recv() send() closesocket() TCP 서버 TCP 클라이언트 connect() listen() accept() 네트워크
TCP 클라이언트 함수 - (2) connect() 함수 connect() 함수 사용 예 int connect ( SOCKET s, // 서버와 통신을 하기 위해 만든 소켓 const struct sockaddr* name, // 서버 주소, 포트번호 -> 특정 주소값에 저장 int namelen // 소켓 주소 구조체 변수의 크기 ) ; 성공: 0, 실패: SOCKET_ERROR connect() 함수 사용 예 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) 소켓 데이터 구조체 서버 클라이언트 네트워크 지역 IP 주소 지역 포트 번호 원격 IP 주소 원격 포트 번호 클라이언트 애플리케이션 운영체제 네트워크 • • • 수신 버퍼 송신 버퍼
데이터 전송 함수 - (2) send() 함수 recv() 함수 애플리케이션 데이터를 송신 버퍼에 복사함으로써 궁극적으로 하부 프로토콜 (Ex. TCP/IP)에 의해 데이터가 전송 int send ( SOCKET s, // 통신할 대상과 연결된 소켓 const char* buf, // 보낼 데이터를 담고 있는 애플리케이션 버퍼 주소 int len, // 보낼 데이터 크기 int flags // send( ) 함수의 동작을 바꾸는 옵션 (보통 0) ); 성공: 보낸 바이트 수, 실패: SOCKET_ERROR recv() 함수 수신 버퍼에 도착한 데이터를 애플리케이션 버퍼로 복사 int recv ( SOCKET s, // 통신할 대상과 연결된 소켓 char* buf, // 받은 데이터를 저장할 애플리케이션 버퍼의 주소 int len, // 수신 버퍼로부터 복사할 최대 데이터 크기 int flags // recv( ) 함수의 동작을 바꾸는 옵션 (보통 0) ); 성공: 받은 바이트 수 또는 0 (연결 종료 시), 실패: SOCKET_ERROR
데이터 전송 함수 - (3) recvn() 함수 : 만약 수신할 전체 크기를 알고 있다면 037 int recvn (SOCKET s, char *buf, int len, int flags) 038 { 039 int received; 040 char *ptr = buf; 041 int left = len; // left : 보낼 총 바이트 수 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 }
데이터 전송 함수 - (4) recvn( ) 함수 동작 원리 buf ptr left len 읽은 데이터
데이터 전송 함수 - (5) 데이터 전송 함수 사용 예 – TCP 클라이언트 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;
데이터 전송 함수 - (6) 데이터 전송 함수 사용 예 – TCP 클라이언트 (계속) 096 // 데이터 보내기 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) break; 113 // 받은 데이터 출력 114 buf[retval] = '\0'; // 맨 끝에 0을 입력함. 115 printf("[TCP 클라이언트] %d바이트를 받았습니다.\n", retval); 116 printf("[받은 데이터] %s\n", buf); 117 }
데이터 전송 함수 - (7) 데이터 전송 함수 사용 예 – TCP 서버 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; 090 // 받은 데이터 출력 091 buf[retval] = '\0'; 092 printf("[TCP/%s:%d] %s\n", inet_ntoa(clientaddr.sin_addr), 093 ntohs(clientaddr.sin_port), buf); 095 // 데이터 보내기 096 retval = send(client_sock, buf, retval, 0); 097 if(retval == SOCKET_ERROR){ 098 err_display("send()"); 099 break; 100 } 101 }
실습 예제 – (1) 마무리 하시오.
애플리케이션 프로토콜과 메시지 설계 - (1) 애플리케이션 프로토콜 애플리케이션 프로토콜 예 애플리케이션 수준에서 주고 받는 데이터의 형식과 의미, 처리 방식 등을 정의한 프로토콜 애플리케이션 프로토콜 예 네트워크
애플리케이션 프로토콜과 메시지 설계 - (2) 메시지 정의 ① - 직선을 그리기(타입 없음) 메시지 정의 ② - 원 그리기(타입 없음) 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) 메시지 정의 ③ - 직선 및 원 그리기(타입 존재) 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; // 선 색상
메시지 설계 시 고려 사항 경계 구분 [송신자] ① 항상 고정 길이 데이터를 보낸다. ② 경계 구분을 위해 특별한 표시(EOR; End Of Record)를 삽입한다. ③ 보낼 데이터 길이를 고정 길이 데이터로 보낸 후, 가변 길이 데이터를 이어서 보낸다. [수신자] ① 항상 고정 길이 데이터를 받는다. ② EOR이 나올 때까지 데이터를 읽은 후 처리한다. ③ 고정 길이 데이터를 읽어 뒤따라올 데이터의 길이를 알아낸다. 이 길이만큼 데이터를 읽어 처리한다. 바이트 정렬 빅 엔디안 방식으로 통일 멤버 정렬 구조체(공용체, 클래스 포함) 멤버의 시작 주소에 대한 제약 사항 #pragma pack 컴파일러 명령을 사용
실습 예제 – (2) 소요시간 : 1시간 30분. 직접 코딩해서 실습할 것. 앞 예제랑 비교하면서 코드 분석할 것.