Protocol : 통신의 약속, 통신의 두 주체가 같은 프로토콜을 쓴다면, 약속된 규칙
으로 통신이 가능하다. 통신 규칙을 알아야 올바른 프로그램(APP,LIB,Server)을 제작가능하다.
IP : Ineternet Protocol로, 송신 호스트와 수신 호스트가 정보를 주고받는데 사용하는 규약
.
특징
: 패킷
혹은 데이터그램이라고 하는 덩어리로 나뉘어 전송
단점
:
1. 비신뢰성 : 통신 도중 패킷이 사라질 수 있으며, 순서대로 도착하지 않을 수 있다.
2. 비연결성 : 패킷을 받을 대상이 없거나 해당 도착지의 주소가 불능 상태여도 패킷을 전송
인터넷상에 있는 컴퓨터의 고유한 주소
이다.
1. IPv4 : 현재는 IPv4 사용중, 42억개까지 표현가능
2. IPv6 : 지구인구가 70억이므로 42억은 부족할 수 있기 때문에 만들어진 IP
TCP : Transmission Control Protocol으로, IP의 단점을 보완한 프로토콜이다. IP에 TCP를 입힌 형태로 데이터 전달 보증, 순서 보증
, 신뢰할 수 있는 프로토콜이지만 속도가 느리다
.
UDP : User Datagram Protocol: 속도는 빠르지만 약간의 끊김(손실)
이 있다. 데이터 전달 및 순서 보장이 안된다.
동시 다발적 통신을 위한 가상의 문
, IP를 통해 어떤 컴퓨팅 장치가 통신 할지 결정한다.
컴퓨터 네트워크를 경유하는 프로세스 간 통신의 종착점
. S/W Interface가 존재하여 내부 프로토콜 원리를 몰라도 Socket Interface만 알고 있으면 즉시 통신 가능하다.
안정성 높은 18.04 사용할 것이다.
AWS 18.04 EC2 생성
보완그룹에서 ICMP & TCP 추가하기
키페어도 설정하기!
TCP 포트는 12345
, 생성된 본인 IP(3.36.74.128)도 기억해주기
MobaXterm과 연결 및 초기세팅 해주기!
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( )
: 송신한다. 파일에 쓴다. 데이터를 보낸다.
시스템으로부터 할당 받은 파일 또는 소켓에 부여된 정수
복잡한 파일을 단순히 파일 디스크립터라는 정수로 치환
하여 작업의 편의성을 높인 것