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;
}
실행결과를 통해서 소켓과 파일에 일련의 파일 디스크립터 정수 값이 할당됨을 알 수 있다.
→ 이를 통해 리눅스는 파일과 소켓을 동일하게 간주함을 확인할 수 있다.
'컴퓨터 네트워크' 카테고리의 다른 글
컴퓨터 네트워크 : Chapter 2. 소켓의 타입과 프로토콜의 설정 (0) | 2023.04.13 |
---|---|
컴퓨터 네트워크 : 15. Transmission Control Protocol(TCP) (0) | 2023.04.11 |
컴퓨터 네트워크 : 2. The OSI Model and the TCP/IP proctocal Suite (2) | 2023.04.10 |