본문 바로가기

컴퓨터 네트워크

컴퓨터 네트워크 : Chapter 1-1 네트워크 프로그래밍과 소켓의 이해

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

  • 네트워크 프로그래밍이란?
    • 소켓이라는 것을 기반으로 프로그래밍을 하기 때문에 소켓 프로그래밍이라고도 함
    • 네트워크로 연결된 둘 이상의 컴퓨터 사이에서의 데이터 송수신 프로그램의 작성을 의미
  • 소켓에 대한 간단한 이해
    • 네트워크(인터넷)의 연결 도구
    • 운영체제에 의해 제공이 되는 소프트웨어적인 장치
    • 소켓은 프로그래머에게 데이터 송수신에 대한 물리적, 소프트웨어적 세세한 내용을 신경 쓰지 않게 한다.

2. 전화 받는 소켓(서버 소켓)의 생성

1) 소켓의 비유와 분류

  • TCP 소켓은 전화기에 비유될 수 있다.
  • 소켓은 socket 함수의 호출을 통해서 생성
  • 단, 전화를 거는 용도의 소켓(클라이언트)전화를 수신하는 용도의 소켓(서버) 생성 방법에는 차이가 있다.
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
→ 성공 시 파일 디스크립터, 실패 시 -1 반환

2) 전화번호의 부여

  • 소켓의 주소 할당 및 연결
    • 전화기에 전화번호가 부여되듯이 소켓에도 주소정보가 할당
    • 소켓의 주소정보는 IP와 Port 번호로 구성
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
→ 성공 시 0, 실패 시 -1 반환

3) 전화기의 연결

  • 연결요청이 가능한 상태의 소켓
    • 연결요청이 가능한 상태의 소켓은 걸려오는 전화를 받을 수 있는 상태에 비유할 수 있다.
    • 전화를 거는 용도의 소켓은 연결요청이 가능한 상태의 소켓이 될 필요가 없다.이는 걸려오는 전화를 받는 용도의 소켓에서 필요한 상태
#include <sys/socket.h>
int listen(int sockfd, int backlog);
→ 성공 시 0, 실패 시 -1 반환

4) 수화기를 드는 상황

  • 연결요청의 수락
    • 걸려오는 전화에 대해서 수락의 의미로 수화기를 드는 것에 비유할 수 있다,
    • 연결요청이 수락되어야 데이터의 송수신이 가능
    • 수락된 이후에 데이터의 송수신은 양방향으로 가능
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addlen);
→ 성공 시 파일 디스크립터, 실패 시 -1 반환
accept 함수 호출 이후에는 데이터 송수신 가능
단, 연결요청이 없을 때에만 accept 힘수가 반환block 함수

5) 서버 소켓(리스닝 소켓) 생성 과정 정리

1단계 : 소켓의 생성 → socket 함수 호출

2단계 : IP와 PORT번호 할당 → bind 함수 호출

3단계 : 연결 요청 수락가능상태로 변경 → listen 함수 호출

4단계 : 연결 요청에 대한 수락 → accept 함수 호출

→ 서버는 연결을 요청하는 클라이언트보다 먼저 실행되어야 함

 

3. 전화 거는 소켓의 구현

1) 연결을 요청하는 소켓의 구현

  • 전화를 거는 상황에 비유할 수 있다.
  • 리스닝 소켓과 달리 구현의 과정이 매우 간단
  • '소켓의 생성'과 '연결의 요청'으로 구분
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen); // block 함수
→ 성공 시 0, 실패 시 -1 반환

// sockfd : client에서 생성된 socket의 파일 디스크립트
// serv_addr : 서버의 주소 정보가 저장된 구조체

2) 클라이언트 소켓 생성 과정 정리

1단계 : 소켓의 생성 → socket 함수 호출

2단계 :연결 요청 → connect 함수 호출

 

4. 리눅스 기반에서의 실행

1) 컴파일 및 실행방법

// 컴파일 방법 : hello_server.c 파일을 컴파일해서 hserver라는 이름의 실행파일을 만드는 문장
gcc hello_server.c -o hserver

// 실행방법 : 현재 디렉토리에 있는 hserver라는 이름의 파일을 실행시키라는 의미
./hserver

2) 리눅스 기반에서의 실행 결과

  • 실행결과 : hello_server.c
$ gcc hello_server.c -o hserver
$ ./hserver 9190
  • 실행결과 : hello_client.c
$ gcc hello_client.c -o hclient
$ ./hclient 127.0.0.1 9190			# 127.0.0.1은 예제를 실행하는 로컬 컴퓨터를 의미
Message from server: Hello World!
127.0.0.1로컬호스트로 컴퓨터 네트워크에서 사용하는 loopback 호스트 명으로, 자신의 컴퓨터를 의미함
→ 서버 프로그램과 클라이언트 프로그램을 한 컴퓨터에서 실행해서 서로 연결할 수 있음

 

5. 리눅스 기반 파일 조작

1) 저 수준 파일 입출력과 파일 디스크립터

  • 저 수준 파일 입출력
    • ANSI의 표준함수가 아닌, 운영체제가 제공하는 함수 기반의 파일 입출력
    • 표준이 아니기 때문에 운영체제에 대한 호환성이 없다.
    • 리눅스는 소켓도 파일로 간주하기 때문에 저 수준 파일 입출력 함수를 기반으로 소켓 기반의 데이터 송수신이 가능
  • 파일 디스크립터
    • 운영체제가 만든 파일(그리고 소켓)을 구분하기 위한 일종의 숫자
    • 저 수준 파일 입출력 함수는 입출력을 목적으로 파일 디스크립터를 요구
    • 저 수준 파일 입출력 함수에게 소켓의 파일 디스크립터를 전달하면, 소켓을 대상으로 입출력을 진행
    • 3부터 할당됨

2) 파일 열기와 닫기

(1) 파일 열기

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *path, int flag);
→ 성공 시 파일 디스크립터, 실패 시 -1 반환
// path : 파일 이름을 나타내는 문자열의 주소 값 전달
// flag : 파일의 오픈 모드 정보 전달

오픈 모드 종류

(2) 파일 닫기

#include <unistd.h>

int close(int fd);
→ 성공 시 0, 실패 시 -1 반환
// fd : 닫고자 하는 파일 또는 소켓의 파일 디스크립터 전달

3) 파일에 데이터 쓰고 읽기

(1) 파일 쓰기

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t nbytes);
→ 성공 시 전달한 바이트 수, 실패 시 -1 반환
// fd : 데이터 전송대상을 나타내는 파일 디스크립터 전달
// buf : 전송할 데이터가 저장된 버퍼의 주소 값 전달
// nbytes : 전송할 데이터의 바이트 수 전달

<예시>

int main(void)
{
	int fd;
    char buf[] = "Let's go!\n";
    fd=open("data.txt", O_CREATE|O_WRONLY|O_TRUNK);
    if(fd == -1)
    	error_handling("open() error!");
    printf("file descriptor : %d \n", fd);
    
    if(write(fd, buf, sizeof(buf)) == -1)
    	error_handling("write() error!");
        close(fd);
        return 0;
}

(2) 파일 읽기

#incldue <unistd.h>

ssize_t read(int fd, void *buf, size_t nbytes);
→ 성공 시 수신한 바이트 수(단 파일의 끝을 만나면 0), 실패 시 -1 반환
// fd : 데이터 수신대상을 나타내는 파일 디스크립터 전달
// buf : 수신할 데이터를 저장할 버퍼의 주소 값 전달
// nbytes : 수신할 최대 바이트 수 전달

<예시>

int main(void)
{
	int fd;
    char buf[BUF_SIZE];
    fd = open("data.txt", O_RDONLY);
    if(fd == -1)
    	error_handling("open() error!");
    printf("file descriptor: %d \n", fd);
    
    if(read(fd, buf, sizeof(buf)) == -1)
    	error_handling("read() error!");
    printf("file data: %s", buf);
    close(fd);
    return 0;
}

4) 파일 디스크립터와 소켓

int main(void)
{
	int fd1, fd2, fd3;
    fd1 = socket(PF_INET, SOCK_STREAM, 0);
    fd2 = open("Test.dat", O_CREAT|O_WRONLY|O_TRUNC);
    fd3 = socket(PF_INET, SOCK_DGRAM, 0);
    
    printf("file descriptor 1: %d\n", fd1);
    printf("file descriptor 2: %d\n", fd2);
    printf("file descriptor 3: $d\n", fd3);
    close(fd1); close(fd2); close(fd3);
    return 0;
}

실행결과를 통해서 소켓과 파일에 일련의 파일 디스크립터 정수 값이 할당됨을 알 수 있다.
→ 이를 통해 리눅스는 파일과 소켓을 동일하게 간주함을 확인할 수 있다.