[Linux] Network 기본

정재훈·2022년 4월 12일
0

Linux

목록 보기
16/19
post-thumbnail

Protocol

Protocol : 통신의 약속, 통신의 두 주체가 같은 프로토콜을 쓴다면, 약속된 규칙으로 통신이 가능하다. 통신 규칙을 알아야 올바른 프로그램(APP,LIB,Server)을 제작가능하다.

IP

IP : Ineternet Protocol로, 송신 호스트와 수신 호스트가 정보를 주고받는데 사용하는 규약.
특징 : 패킷 혹은 데이터그램이라고 하는 덩어리로 나뉘어 전송
단점 :

1. 비신뢰성 : 통신 도중 패킷이 사라질 수 있으며, 순서대로 도착하지 않을 수 있다.
2. 비연결성 : 패킷을 받을 대상이 없거나 해당 도착지의 주소가 불능 상태여도 패킷을 전송

IP Address

인터넷상에 있는 컴퓨터의 고유한 주소이다.

IP 종류 2가지

1. IPv4 : 현재는 IPv4 사용중, 42억개까지 표현가능
2. IPv6 : 지구인구가 70억이므로 42억은 부족할 수 있기 때문에 만들어진 IP

TCP

TCP : Transmission Control Protocol으로, IP의 단점을 보완한 프로토콜이다. IP에 TCP를 입힌 형태로 데이터 전달 보증, 순서 보증, 신뢰할 수 있는 프로토콜이지만 속도가 느리다.

UDP

UDP : User Datagram Protocol: 속도는 빠르지만 약간의 끊김(손실)이 있다. 데이터 전달 및 순서 보장이 안된다.

Port

동시 다발적 통신을 위한 가상의 문, IP를 통해 어떤 컴퓨팅 장치가 통신 할지 결정한다.

Socket

컴퓨터 네트워크를 경유하는 프로세스 간 통신의 종착점. S/W Interface가 존재하여 내부 프로토콜 원리를 몰라도 Socket Interface만 알고 있으면 즉시 통신 가능하다.

AWS 실습

안정성 높은 18.04 사용할 것이다.
AWS 18.04 EC2 생성

보완그룹에서 ICMP & TCP 추가하기
키페어도 설정하기!

TCP 포트는 12345 , 생성된 본인 IP(3.36.74.128)도 기억해주기

MobaXterm과 연결 및 초기세팅 해주기!

Echo 서버 & 클라이언트 테스트

Echo - 사용자가 무엇을 입력하든 서버가 입력한 데이터를 그대로 되돌려주는 프로그램
서버소켓(=리스닝 소켓) : 서버 쪽에 생성한 소켓
클라이언트 코드를 작성하지 않았지만, 우분투 내부적으로 NetCat 존재한다.
NetCat : 가짜 클라이언트, TCP/UDP 네트워크 연결을 통해 데이터를 읽거나 쓸 수 있도록 만든 유틸리티 프로그램
테스트 방식 : 각각의 코드를 테스트 후 성공 시 서로 이어 붙여 최종 테스트를 진행한다.

가짜 클라이언트로 서버 코드 테스트

echo_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.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[]) // COMMAND LINE ARGUMENT
{
  int serv_sock;
  int clnt_sock;

  // 주소 정보
  struct sockaddr_in serv_addr;
  struct sockaddr_in clnt_addr;
  // _t : 시스템 자료형 - 세월이 지나 버전 업그레이드 되어도 동작가능토록
  socklen_t clnt_addr_size;

  // 클라이언트에서 보낸 메세지가 담길 것이다.
  char message[BUF_SIZE];
  // 문자열 길이 받을 용도
  int str_len;

  // 소켓 옵션 설정을 위한 두 변수
  int option;
  socklen_t optlen;

  if (argc != 2) // 파일명 + PORT 번호 = 2개
  {
    printf("Usage : %s <port>\n", argv[0]); // 에러 처리 구문
    exit(1);
  }

  // 소켓의 생성
  // 그러나, 아직 서버 소켓이라 부르긴 무리
  serv_sock = socket(PF_INET, SOCK_STREAM, 0); // TCP/IP 통신을 하기 위한 구문
  if (serv_sock == -1)
    error_handling("socket() error");

  // Time-wait 해제 - 3분안에 재시작 불가한 것을 풀어주는 옵션
  // SO_REUSEADDR 를 0에서 1 로 변경
  optlen = sizeof(option);
  option = 1;
  setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen);

  // 소켓의 주소 할당을 위해 구조체 변수 0으로 초기화
  memset(&serv_addr, 0, sizeof(serv_addr)); // serv_addr = {0}; - 마지막 널값을 통해 길이를 확인하기 위한 초기화
  // 주소체계 지정 - IPv4로 설정한다.
  serv_addr.sin_family = AF_INET;  
  
  // < htonl & htons>
  // 모든 네트워크 앱은 "빅 엔디안"이 원칙 => "네트워크 바이트 주소(n)"
  // 호스트가 가지고 있는 cpu의 엔디안 방식 => "호스트 바이트 주소(h)"
  // htonl : h(호스트 바이트 순서) + to(~로) + n(네트워크 바이트 주소) + l(long int)
  // htonl - IP주소를 빅 엔디안 방식 long int로 바꿀 때
  // htons - PORT 주소를 빅 엔디안 방식 small int로 바꿀 때
  
  // IP 주소 배정(네트워크 바이트 순서)
  // INNADDR_ANY 는 내 컴퓨터의 아이피 의미하는 주소 상수 : int형이다.
  serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  // 문자열 기반 port 번호 초기화
  // 네트워크 바이트 순서
  // argv[1] : 입력받는 PORT 번호로 문자열로 입력된다. 이를 정수형으로 형 변환해준다.
  serv_addr.sin_port = htons(atoi(argv[1])); 
  
  // bind : "내가", "내 IP", "몇번째 포트"에 서비스 하겠다.
  
  // 소켓에 주소 정보 할당을 위해 bind 호출
  // 그러나, 아직 서버 소켓이라 부르긴 무리
  // 만들어준 서버 변수(serv_addr)를 형변환하여 서버 파일 디스크립터(serv_sock)에 넣어준다.
  if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
    error_handling("bind() error");

  // Listen = "크기가 정해진" 대기실
  // 연결요청 대기상태로 들어가기 위해 listen 호출
  // queue 의 크기는 5
  // 드디어, 서버 소켓(리스닝 소켓) 이라 부를 수 있게 됨
  if (listen(serv_sock, 5) == -1)
    error_handling("listen() error");

  clnt_addr_size = sizeof(clnt_addr);

  // 이 부분이 핵심이다.
  // 무한루프 돌릴 것
  while (1)
  {
    // accept 함수 호출
    // 대기 큐에서 첫번째 대기중에 있는 연결을 참조해 클라이언트와 연결
    // accpet 의 리턴값으로 별도의 "새로 만들어진" 소켓의 파일 디스크립터 반환
    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1)
      error_handling("accept() error");

    // 클라이언트에서 보낸 메세지를,
    // message 변수에 담을 것이다.
    // read 된 결과가 0 이 아닐 때, 즉 클라이언트에서 어떤 메세지를 보냈을 때
    // 반복은 진행된다.
    while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
      // 받은 메세지를 그대로, 클라이언트에게 전송한다.
      write(clnt_sock, message, str_len);

    // str_len 이 0이라는 뜻은, 클라이언트에서 EOF를 보냈다는 뜻 = 아무것도 안 보냈다는 뜻이다.
    // 할 일이 끝났으니, 클라이언트 소켓"만" 닫는다. 즉, 연결을 끊는다.
    close(clnt_sock);
  }
  // 서버 소켓을 닫는다.
  close(serv_sock);
  return 0;
}

void error_handling(char *message) // 에러 출력 함수
{
	// stderr : 버퍼없이 바로 출력하도록
  fputs(message, stderr); // 모든 기기에 출력가능케 하기 위해 fput 사용
  fputc('\n', stderr); 
  exit(1);
}

서버 소켓 작성 프레임워크

socket() : 소켓 생성
bind() : 소켓에 주소 할당
listen() : 클라이언트 연결 요청 대기
accept() : 클라이언트 연결 승인
read() / write() : 통신
close() : 소켓 닫기

sock = socket(PF_INET, SOCK_STREAM, 0); - TCP/IP 통신을 위해 사용되는 구문

가짜 클러이언트 만들고 연결

위에 작성한 서버 코드를 우선 실행시킨다.
gcc ./echo_server.c -o ./echo_server./echo_server 12345로 실행
다른 터미널에서 가짜 클라이언트 제작 후 실행
nc [IP 주소] [port] : 가짜 클라이언트를 내 컴퓨터로 만들기 때문에, IP는 127.0.0.1로 고정된다. nc 127.0.0.1 12345

가짜 서버로 클라이언트 코드 테스트

echo_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.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;
  // BUF_SIZE 로 크기를 정했다.
  char message[BUF_SIZE];
  // 길이는 따로 초기화하지 않겠다.
  int str_len;

  if (argc != 3)
  {
    printf("Usage : %s <IP> <port>\n", argv[0]);
    exit(1);
  }

  // 서버 접속을 위한 TCP 소켓 생성
  sock = socket(PF_INET, SOCK_STREAM, 0);
  if (sock == -1)
    error_handling("socket() error");

  // 구조체 변수 serv_addr 에 "서버의 IP 와 PORT 정보 초기화
  memset(&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  
  // argv[1] : 문자열 형식의 서버 IP => int형 && 빅 엔디안(네트워크 바이트 순서)로 바꿔줘야 함
  // inet_addr() : 문자열 -> int형 , 빅 엔디안(네트워크 바이트 순서)로 바꿔준다.
  serv_addr.sin_addr.s_addr = inet_addr(argv[1]); // 서버의 IP
  serv_addr.sin_port = htons(atoi(argv[2])); // PORT

	// connect : "서버"의 "IP"에 "몇번째 포트"에 연결하겠다.

  // connect 함수 호출을 통해 서버로 연결 요청 - bind 역할을 자동으로 실행됨
  if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
    error_handling("connect() error!");
  // 연결 성공 시, 메세지를 보여주자.
  // 단, connect 되었다 하더라도, 서버에서 accept 해주지 않았을 수도 있다.
  // connect 성공이라는 뜻은 그저 서버의 대기 큐에 등록되었다는 뜻인데,
  // 즉, listen() 까지 통과되었다는 뜻이다.
  else
    puts("Connected.........");

  while (1)
  {
    // 화면에서(stdout), 입력 안내구문 띄움
    fputs("Input message(Q to quit): ", stdout);
    // 화면에서(stdin), 입력받음
    fgets(message, BUF_SIZE, stdin);

    // 무한루프 탈출조건
    // 사용자가 엔터까지 쳤으므로, \n 을 붙여줘야.
    if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
      break;

    // 소켓에 입력한 메세지 실어서 서버로 보냄
    write(sock, message, strlen(message));
    // 서버에서 보낸 메세지(사실은 동일한 메세지) 읽음
    str_len = read(sock, message, BUF_SIZE - 1);
    // 맨 끝부분 null 로 만듦
    message[str_len] = 0;
    printf("Message from server: %s", message);
  }
  // close 가 호출되면 상대 소켓으로 EOF 가 전달된다.
  // 즉, 서버 입장에선 while 반복문에서 str_len 이 0 이 된 경우이므로,
  // 서버에서 "현재의 클라이언트 소켓"을 종료할 것이다.
  close(sock);
  return 0;
}

void error_handling(char *message)
{
  fputs(message, stderr);
  fputc('\n', stderr);
  exit(1);
}

클라이언트 소켓 작성 프레임워크

socket() : 소켓 생성
connect() : 서버 소켓에 연결
read() / write() : 통신
close() : 소켓 닫기

가짜 서버 만들고 연결

위에 작성한 가짜 서버 제작 후 킨다.
nc -l [port] => nc -l 12345로 가짜 서버 제작 후 킨다.

다른 터미널에서 gcc ./echo_client.c -o ./echo_client./echo_client 127.0.0.1 12345로 클라이언트 코드 실행
./[클라이언트 코드] [IP] [Port]테스트를 위해 내 컴퓨터 IP인 127.0.0.1로 실행한다.

클라이언트 코드 & 서버 코드 연결

AWS 터미널에서는 서버를 ./echo_server 12345를 다음과 같이 실행시키고,
다른 터미널에서는 클라이언트를 ./echo_client 3.36.74.128 12345와 같이 실행시킵니다.
클라이언트 생성시 aws에서 생성한 나의 IP를 입력해줘야합니다.

파일 입출력

리눅스의 모든 것을 파일로 간주한다. 따라서, 소켓 또한 파일로 취급한다. 파일 입출력 함수를 네트워크 송수신에서 사용가능하다.

read( ) : 수신한다. 파일을 읽는다. 데이터를 받는다.
write( ) : 송신한다. 파일에 쓴다. 데이터를 보낸다.

파일 디스크립터

시스템으로부터 할당 받은 파일 또는 소켓에 부여된 정수
복잡한 파일을 단순히 파일 디스크립터라는 정수로 치환하여 작업의 편의성을 높인 것

profile
여러 방향으로 접근하는 개발자

0개의 댓글