* 학교 수업 내용과 구글링을 같이 정리
[ 블러킹 I/O ]
클라이언트에서 서버로 데이터를 전송하면 서버가 클라이언트한테 해당 데이터를 재전송한다.
→ 클라이언트 상에서는 통신이 잘되고 있음을 확인할 수 있다.
블러킹 I/O는 데이터를 read하려고 시도하는 경우 데이터가 도착이 되었다면 바로 read할 수 있지만
데이터가 전송이 안되어있다면 read를 할 수 없다. 즉, read를 주도적으로 할 수 없음을 의미한다.
[ echo_server.c와 echo_client.c 작동시키기 ]
1. 외부의 파일을 가상 머신으로 전달
① FTP( File Transfer Protocol ) : 파일을 주고 받는 프로토콜
* FTP는 보안에 취약하기 때문에 sftp를 사용한다.
ps -ef | grep ftp : 현재 서버에서 FTP가 돌고 있는지 확인할 수 있다.
* vsftpd : FTP의 대표적인 서버
② FileZila를 통해서 데이터를 가상 머신으로 전달하기
* 포트번호는 22번이다.
③ tar.gz 파일 해제하기
gz 파일은 gunzip 명령어를 통해 해제하고 tar 파일은 tar xvf [ 파일이름 ] 명령어를 통해 해제한다.
2. echo_server.c와 echo_client.c 작동시키기
① 소스코드 컴파일
gcc -o echo_server echo_server.c
gcc -o echo_client echo_client.c
② 첫번째 putty에서 server 가동시키기
./echo_server 3600
* 전달해주는 포트번호는 3600이다.
③ 두번째 putty에서 client 실행하기
./echo_client
두번째 putty에서 echo_client를 실행하면 echo_server를 실행시킨 첫번째 putty에서 새로운 클라이언트가 해당
서버로 접속했음을 확인할 수 있다.
[ 서버 네트워크 프로그램의 흐름 ]
1. socket() : 소켓 생성하기
소켓 : 프로그램이 네트워크 상에서 데이터를 통신할 수 있도록, 네트워크 환경에 연결할 수 있게 해주는
통신 접속점 → 네트워크 응용 프로그램은 socket을 통해 통신망으로 데이터를 송수신한다.
2. bind() : 소켓을 인터넷 주소와 포트 번호로 묶는다.
각 소켓은 시스템이 관리하는 포트번호( 0~65535 ) 중 하나의 포트 번호를 사용한다. 만약 소켓이 사용하는
포트번호가 다른 소켓의 포트번호와 중복이 된다면 특정한 포트로 데이터가 수신될 때 어떤 소켓이 데이터를
처리해야하는지 알 수 없어 에러가 발생한다. 그래서 운영체제는 소켓들이 중복된 포트번호를 사용하지 않도록
내부적으로 포트번호화 소켓 연결 정보를 관리한다.
즉, bind()는 해당 소켓이 지정된 포트 번호를 사용할 것이라고 운영체제에게 요청하는 API로 지정된 포트번호를
다른 소켓이 사용하게 된다면 bind()는 에러를 리턴한다.
서버 소켓은 고정된 포트번호를 사용하기 때문에, 그 포트번호로 클라이언트의 연결 요청을 받아들일 수 있다.
( 운영체제가 특정 포트 번호를 서버 소켓이 사용하도록 만들기 위해서 소켓과 포트 번호를 결합한다. )
3. listen() : 수신 대기열 생성 ( listen queue )
제일 첫번째로 들어온 클라이언트를 수신하고, 그 뒤에 대기중인 클라이언트들은 buffer에 넣는다. FIFO queue를
OS에 만들고 현재 접속된 클라이언트를 제외한 다른 대기 중인 클라이언트를 buffer에 넣기 위해 수신 대기열
queue의 사이즈를 지정한다.
listen()은 클라이언트로부터 연결 요청이 들어오는지 주시하고 있다. 서버 소켓을 통해 클라이언트의 연결 요청을
받아들일 준비가 되어있고, 클라이언트에 의한 연결 요청이 들어올 때 까지 기다린다.
즉, listen()은 서버 소켓에 바인딩된 포트번호로 클라이언트의 연결 요청이 들어왔는지 확인하면서 대기상태에서
머무른다.
listen()의 리턴 값으로는 클라이언트의 요청에 대한 정보는 들어있지 않고 오직 클라이언트의 연결 요청이 제대로
수신되었는지, 에러가 발생했는지에 대한 결과값이다. 클라이언트 연결 요청에 대한 정보는 queue에 쌓이게 되는데
queue에 쌓여있는 클라이언트들은 not established state 상태이고 queue에서 대기 중인 연결 요청을 완료하기
위해서는 queue로 부터 꺼내와 accpet()을 호출해야한다.
listen()에서 빠져나오는 경우는 클라이언트 요청이 수락이 되는 경우와 에러가 발생하는 경우, close()가 발생하는
경우이다.
4. accept() : 연결 대기
실질적인 소켓 연결 과정은 최종적으로 연결 요청을 받아들이는 역할을 수행하는 accpet()에 의해 완료가 된다.
즉 accpet()은 클라이언트로 부터 접속이 오는 것을 기다리고, 기다리다가 누가 접속하면 받아들이는 것까지
수행을 한다.
클라이언트 소켓과 연결되는 소켓은 서버 소켓이 아니라 accpet()에 의해 새로 만들어지는 소켓이다.
서버 소켓의 역할은 클라이언트의 연결 요청을 수신하는 것이다. bind()와 listen()을 통해 소켓에 포트 번호를
바인딩하고, 요청 대기 queue를 생성해서 연결 요청을 한 클라이언트들을 대기시킨다.그리고 accpet()을
통해 데이터 송수신을 위한 새로운 소켓을 만들고 서버 소켓의 queue에서 대기하고 있는 첫번째 클라이언트와
첫번째 연결 요청을 맵핑 시킨다. 또 다른 연결 요청을 처리하기 위해서는 listen()이나 close()을 수행한다.
즉, 실질적인 데이터 송수신 연결은 accpet()에 의해 생성된 소켓과 서버 소켓의 대기 queue에서 대기하고 있는
not established state 상태의 클라이언트들이다.
5. read/write() : 데이터 송수신
6. close() : 소켓 연결 종료
서버 소켓에는 close()의 대상이 하나만 존재하는 것은 아니다.
socket(), bind(), listen()은 딱 한번만 수행이 되지만 accpet(), read(), write(), close()는 클라이언트마다 실행이
되어야한다.
네트워크 프로그램의 함수들은 모두 블로킹 방식으로 작동이 된다.
bind(), listen(), accpet()은 서버 측에서만 사용하는 함수들이고, connect()은 클라이언트에서만 사용하는 함수이다.
read(), write(), close() 함수들은 서버와 클라이언트 모두 사용한다.
[ 클라이언트 프로그램 만들기 ]
1. 소켓 생성 : 인터넷과 연결하기 위한 접점 소켓( endpoint socket )을 생성한다.
#include <sys/types.h>
#include <sys/socket.h>
int socket (int domain, int type, int protocol);
domain : 소켓의 사용 영역을 정의한다.
type : 소켓 유형을 정의한다.
protocol : 소켓이 사용할 프로토콜을 정의한다.
① domain : 소켓이 사용되는 네트워크의 영역을 정의한다.
* 소켓은 인터넷 프로토콜에만 사용이 되는 것은 아니다.
domain | 설명 |
AF_INET | IPv4 TCP/IP 인터넷 통신 |
AF_INET6 | IPv6 TCP/IP 인터넷 통신 |
② type : 통신에 사용할 패킷의 타입 및 유형을 정의한다.
③ protocol : 소켓이 통신에 사용할 프로토콜을 정의한다.
* type에 따라서 프로토콜이 정해진다.
type | protocol | 설명 |
SOCK_STREAM | IPPROTO_TCP | TCP 기반의 통신에 사용 |
SOCK_DGRAM | IPPROTO_UDP | UDP 기반의 통신에 사용 |
SOCK_RAW | IPPROTO_ICMP | RAW Socket으로 저수준에서 프로토콜을 직접 다룰 때 사용 * ICMP( Internet Control Message Protocol )을 이용하여 서버의 상태를 측정하거나, 특정 port로 향하는 패킷의 헤더 내용을 분석한다. |
ex) socket 함수를 이용한 소켓 생성의 예
1. TCP 소켓
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
2. UDP 소켓
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
3. RAW 소켓
socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)
socket 함수의 반환값은 정수이다. 성공적으로 소켓이 생성이 된다면 0이상인 int 값이 반환이 된다.
socket 함수 반환 값을 소켓 지정 번호, socket descriptor( file descriptor )라고 부른다.
< 소켓 관련 구조체 >
소켓은 인터넷에서만 사용하기 위해 만들어진 것이 아니라 다른 네트워크에서도 사용하기 위해 만들어졌다.
인터넷을 사용하기 위해 IP 주소 4byte와 포트 번호 2byte로 총 6byte만 있어도 된다.
* 1byte = 8bit ( 0~255까지 숫자 표현 가능 )
struct sockaddr {
unsigned short sa_family; // address family, AF_XXXX
char sa_data[14]; // 14 bytes of protocol address
};
struct sockaddr_in { // IPv4 AF_INET sockets:
short sin_family; // e.g. AF_INET, AF_INET6
unsigned short sin_port; // e.g. htons(3490)
struct in_addr sin_addr; // see struct in_addr, below
char sin_zero[8]; // padding or filler
};
struct in_addr {
unsigned long s_addr; // long은 4byte 정수
};
① sockaddr : 14byte로 프로토콜 주소를 표현한다.
② sockaddr_in : 포트번호를 표현하기 위해서는 2byte로 충분하기 때문에 unsigned를 사용한다.
sin_zero[8]은 주소를 저장하기 위한 배열의 사이즈는 14byte를 사용하지만 인터넷에서는 6byte만 필요로 하기 때문에
남은 byte를 저장한다.
2. 소켓에 연결하기
connect() 함수를 사용해서 소켓을 연결한다.
연결하고자 하는 상대 노드의 IP 주소와 연결하고자 하는 프로그램의 port 번호를 명시해줘야한다.
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
sockfd : 파일 지정 번호
serv_addr : 연결할 인터넷 주소와 port 번호를 포함한 구조체 포인터 → sockaddr 구조체의 포인터이다.
addrlen : 두번째 매개 변수로 넘길 데이터의 크기 → 시작 주소로 부터 몇 바이트인지 길이를 전달해준다.
connect() 함수의 반환 값으로는 함수 성공 여부를 반환한다. 서버와 달리 클라이언트는 듣기 소켓과 연결 소켓을
구분하지 않는다.
ex) connect 함수의 사용 예
struct sockaddr_in serveraddr;
server_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 소켓 생성
serveraddr.sin_family = AF_INET; //소켓이 사용하는 네트워크의 영역을 정의
serveraddr.sin_addr.s_addr = inet_addr("218.234.19.87"); //구조체 시작 주소
serveraddr.sin_port = htons(8080);
client_len = sizeof(serveraddr);
connect(server_sockfd, (struct sockaddr *) &serveraddr, client_len);
구조체에 데이터를 저장하고 connect 함수에 그 구조체를 전달한다. sockaddr은 구조체 덩어리로 포인터의 값은
변하지 않는다. &serveraddr은 구조체 시작 주소로 소켓을 시작할 때는 socketaddr를 사용하기 때문에 타입 캐스팅을
해줘야한다. ( serveraddr의 구조체는 serveraddr_in이다. )
해당 코드는 Internet TCP/IP 기반에 사용하는 소켓을 생성하고 그 소켓을 218.234.19.87 주소의 8080 포트에
연결된 프로그램에 연결을 요청한다.
3. 데이터 통신하기
리눅스/유닉스는 소켓을 포함한 모든 자원을 파일로 취급한다.
데이터(파일) 입출력의 함수로는 read와 write를 많이 사용한다.
파일 입출력에 사용하는 함수를 소켓에 사용이 가능하다.
① 데이터 쓰기
ssize_t write(int fd, const void *buf, size_t count);
fd : 소켓 지정 번호
buf : 통신에 사용할 데이터를 가리키는 포인터
→ buf는 소스코드가 메모리 어딘가에 존재하고, 그 소스코드의 시작 주소를 의미한다.
count : 통신에 사용할 데이터의 크기
② 데이터 읽기
ssize_t read(int fd, void *buf, size_t count);
read와 write의 count는 다르다. read의 경우는 읽어서 저장할 버퍼를 준비해놓고 read를 해야하지만,
write의 count는 write 된 만큼의 크기로 설정이 된다. read의 count는 write할 크기를 바랄 수 없으므로 단순
희망 사항이다.
4. 연결 종료
데이터 통신이 끝난다면 close 함수를 사용해서 소켓을 닫아줘야한다. 소켓을 만든다는 의미는 운영채제 내의
버퍼를 차지한다는 의미이므로 소켓을 닫지 않는다면 자원 누수가 발생한다.
close(int sockfd);
'2CHAECHAE 학교생활 > OSNW실습' 카테고리의 다른 글
[ OS/NW 실습 ] 6주차 - 소켓 네트워크 프로그램 이해 (0) | 2022.10.29 |
---|---|
[ OS/NW 실습 ] 6주차 - 소켓 네트워크 프로그램 개발 ② 서버 프로그램 만들기, 리눅스 클라이언트와 서버 코드 분석하기 (0) | 2022.10.29 |
[ OS/NW 실습 ] 5주차 - 네트워크 OSI 모델 7 계층 구조 (0) | 2022.10.08 |
[ OS/NW 실습 ] gcc, 오브젝트 파일, 언어 계층 구조 보충 (0) | 2022.10.08 |
[ OS/NW 실습 ] 4주차 - 파일 아카이브, 프로세스 (0) | 2022.09.30 |