Search
Duplicate

네트워크 프로그래밍과 소켓 이해

태그
소켓
네트워크 프로그래밍
1 more property

1. 네트워크 프로그래밍과 소켓의 이해

네트워크 프로그래밍의 정의를 먼저 살펴보면 네트워크로 연결된 둘 이상의 컴퓨터 사이에서의 데이터 송수신 프로그램의 작성을 의미합니다.
소켓이라는 것을 기반으로 프로그래밍을 하기 때문에 소켓 프로그래밍이라고도 부르기도 합니다.
따라서 네트워크 프로그래밍을 할 때는 운영체제에서 소켓이라는 소프트웨어 모듈을 제공해주고 그것을 이용하여 프로그래밍을 합니다.
소켓을 이용하면 내부적으로 어떻게 통신하는 지 정확하게 알지 못하더라도 컴퓨터 끼리 네트워크 상에서 데이터를 주고 받을 수 있습니다.

2. 소켓 API의 실행 흐름

클라이언트 소켓 흐름

서버 소켓 흐름

1.
소켓 생성
2.
서버 측에 연결
3.
서버 소켓에서 연결을 받으면 데이터 송수신
4.
모든 처리 완료 시 소켓을 닫음
1.
소켓 생성
2.
서버가 사용할 IP 주소와 포트 번호를 생성한 소켓에 bind
3.
클라이언트로부터 연결요청 오는지 확인
4.
요청이 수신되면 Accept 후 소켓 생성
5.
데이터 송수신
6.
소켓 닫음
일반적으로 서버를 보면 연결을 요청하는 클라이언트보다 먼저 실행되어야하고 복잡한 실행 과정을 거치게 됩니다.
socket 함수를 통한 소켓의 생성
#include <sys/socket.h> int socket(int domain, int type, int protocol);
C
복사
소켓의 종류를 지정할 수 있는데, TCP 소켓을 위해서는 스트림 타입, UDP 소켓을 위해서는 데이터그램 타입을 지정할 수 있다.
성공 시 파일 디스크립터를 반환하고 실패 시 -1을 반환한다.
전화를 거는 용도의 소켓과 전화를 수신하는 용도의 소켓 생성 방법에 차이가 있다.
소켓에 주소를 할당하는 bind 함수
#include <sys/socket.h> int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
C
복사
bind함수를 통하여 주소를 할당합니다. 성공 시 0을 실패 시 -1을 반환합니다.
소켓의 주소정보는 IP와 PORT번호로 구성이 됩니다.
수신쪽 소켓을 연결요청 가능 상태로 만드는 listen 함수
#include <sys/socket.h> int listen(int sockfd, int backlog);
C
복사
연결 요청이 가능한 상태의 소켓은 걸려오는 전화를 받을 수 있는 상태에 비유할 수 있습니다.
listen 함수를 호출하면 소켓에 할당된 IP와 PORT번호로 연결 요청이 가능한 상태가 됩니다.
성공 시에는 0을 실패시에는 -1을 받게됩니다.
수신쪽 소켓에 연결요청 들어왔을 때 수락하는 Accept 함수
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
C
복사
어떤 연결 요청이 들어오면 accept 함수를 통하여 전화를 받는 행위를 해주어야 합니다.
두 소켓간 연결이 되면 데이터 송수신이 가능하고 이것은 양방향 송수신이 됩니다.
accept 함수 호출 성공 시, 파일 디스크립터를 반환합니다.
두 소켓 간에 연결이 되면 마치 파일 입출력을 하듯이 프로그램을 하면 네트워크 상에서 데이터를 주고받을 수 있습니다.
서버쪽에 연결을 요청하는 connect 함수
#include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
C
복사
위 함수를 통하여 서버로의 연결 요청을 합니다. 성공 시 0을 실패 시 -1을 반환합니다.

3. 서버 코드 (리스닝 소켓) 살펴보기

// hello_server.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> void ErrorHandling(char *message); int main(int argc, char *argv[]){ int serv_sock; int clnt_sock; struct sockaddr_in serv_addr; struct sockaddr_in clnt_addr; socklen_t clnt_addr_size; char message[] = "Hello world"; if(argc != 2){ printf("Usage : %s <port>\n", argv[0]); exit(1); } // socket을 생성합니다. socket은 운영체제가 관리하고 있습니다. // 여기서 socket을 단지 생성하기만 하면 여기서 생성한 socket이 어떤 socket인 지 알 수 없습니다. // 따라서 운영체제는 socket 함수 호출을 통하여 생성한 socket에 번호를 부여합니다. // 아래 socket 함수에서 반환되는 정수형 값이 바로 그 번호 입니다. // 이 번호를 file descriptor 또는 socket handle 이라고 합니다. serv_sock = socket(PF_INET, SOCK_STREAM, 0); if(serv_sock == -1){ ErrorHandling("socket() error"); } // serv_addr 구조체 변수에 IP와 PORT 정보를 저장합니다. // 정보를 저장하기 전에 초기화를 해줍니다. memset(&serv_addr, 0, sizeof(serv_addr)); // 아래 3줄을 통하여 IP 주소와 PORT 번호를 할당해줍니다. // 상세 내용은 이후에 알아보고 아래 작업을 통하여 IP, PORT가 할당된다는 것만 확인하고 넘어가겠습니다. serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(atoi(argv[1])); // 한 프로그램 내에서는 여러개의 socket을 생성할 수 있습니다. // 따라서 어떤 socket에 해당하는 IP와 PORT 정보를 할당하기 위해서 bind 함수에서는 socket의 file descriptor를 인자로 넘겨줍니다. // 즉, serv_sock에 해당하는 socket에 serv_addr 주소 정보를 할당해 주는 코드입니다. if( bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1 ){ ErrorHandling("bind() error"); } // serv_sock에 해당하는 socket이 연결 가능한 상태가 되도록 listen 함수를 호출합니다. if( listen(serv_sock, 5) == -1){ ErrorHandling("listen() error"); } // accept 함수는 blocking 함수 역할을 합니다. 즉, client의 연락이 올 때 까지 계속 기다리게 됩니다. // client의 연락이 오게 되면 client socket의 file descriptor를 반환하고 다음 코드 라인으로 넘어가게 됩니다. clnt_addr_size = sizeof(clnt_addr); clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); if(clnt_sock == -1){ ErrorHandling("accept() error"); } // server와 client의 연결이 되고 나면 server와 client 간의 데이터 송수신이 가능해 지게 됩니다. // 아래 write를 통하여 server에서 client 쪽으로 데이터를 보낼 수 있습니다. write(clnt_sock, message, sizeof(message)); // close 함수를 통하여 생성한 socket을 닫아주도록 운영체제 쪽으로 요청할 수 있습니다. // 여기서 저희가 생성한 socket은 server의 socket인데 client socket 까지 같이 close 요청을 하고 있습니다. // 관련 내용은 이후에 또 자세하게 다루어 보겠습니다. 일단 큰 틀로 보았을 때 이렇게 하면 간단한 네트워크 프로그래밍이 완료됩니다. close(clnt_sock); close(serv_sock); return 0; } void ErrorHandling(char *message){ fputs(message, stderr); fputc('\n', stderr); exit(1); }
C
복사

4. 클라이언트 코드 (클라이언트 소켓) 살펴보기

// hello_client.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> void ErrorHandling(char* message); int main(int argc, char* argv[]){ int sock; struct sockaddr_in serv_addr; char message[30]; int str_len; if(argc != 3){ print("Usage : %s <IP> <port>\n", argv[0]); exit(1); } // socket을 생성합니다. 여기서 만든 socket은 listening socket 에서 만든 socket의 정보를 저장할 것입니다. sock = socket(PF_INET, SOCK_STREAM, 0); if(sock == -1){ ErrorHandling("socket() error"); } // socket 초기화 memset(&serv_addr, 0, sizeof(serv_addr)); // client를 실행할 때, parameter로 IP와 PORT 순서로 받을 예정입니다. // 이 때 받는 IP와 PORT는 서버에서 정의한 IP와 PORT입니다. serv_addr.sin_family = AF_INET; serv_addr.sin_family.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); // 서버에서 생성한 socket과 클라이언트에서 생성한 socket의 IP와 PORT의 정보가 같기 때문에 // connect 함수를 통하여 두 socket을 연결할 수 있습니다. if( connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){ ErrorHandling("connect() error"); } // 이번 예제 코드에서 할 작업은 아래와 같습니다. // 서버와 클라이언트의 연결이 끝난 후, 서버가 write 함수를 통하여 데이터를 송신합니다. // 이 때, 클라이언트는 read 함수를 통하여 데이터를 읽습니다. str_len = read(sock, message, sizeof(message) - 1); if(str_len == -1){ ErrorHandling("read() error"); } // 서버로 부터 받은 데이터를 출력합니다. printf("Message form server: %s \n", message); // 통신이 끝났으니 socket을 제거합니다. close(sock); return 0; } void ErrorHandling(char* message){ fputs(message, stderr); fputc('\n', stderr); exit(1); }
C
복사