study/NETWORK PROGRAMMING
02-1. [Linux/Ubuntu] 네트워크 프로그래밍 - 소켓의 이해
김팥빵_
2025. 3. 15. 00:18
네트워크 프로그래밍과 소켓의 이해
- OSI 7 Layers
- ISO(국제표준화기구)에서 개발한 모델
- 1~4번은 OS에 대부분 탑재
- NetworkProgramming에서 할 건 application 부분
- TCP/IP Protocol
- Transmission Control Protocol / Internet Protocol
- OSI 7 layer가 프로토콜 개발 참조 용도인 레퍼런스 모델인 것과는 달리 실제로 구현된 프로토콜 모델임
- 1계층 : Physical Layer
- 데이터를 전달하는 기능
- 단순히 0,1의 비트만 통신함 -> 어떤 데이터인지 판단x
- ex. 광랜, 통신 케이블, 리피터, 허브 등
- 2계층 : Data Link Layer
- physical layer을 통해 송수신되는 정보의 오류 검출 및 흐름 관리
- MAC 주소를 이용하여 통신
- mac주소는 절대 중복되지 않고 고유한 번호를 할당받음
- ex. 브리지, 스위치 등
- 3계층 : Network Layer
- 데이터를 목적지까지 전달하는 기능 -> 라우팅
- 경로를 선택 -> 경로에 따라 패킷을 전송
- IP주소 사용
- ex. 라우터
- 그 외 계층들
- 4계층 : Transport layer -> TCP/UDP 프로토콜 사용
- 1~4계층까지가 커널 s/w
- 5계층 : Session layer -> port 연결
- 6계층 : Presentation layer
- 7계층 : Application layer -> 응용 프로세스간 정보 교환
- 4계층 : Transport layer -> TCP/UDP 프로토콜 사용
- Socket API
- 기본적으로 OS가 다 제공하기 때문에 우리는 "Socket API"만 정상적으로 호출하면 데이터를 전송하고 socket programming을 할 수 있음.
- Socket(Network) Programming
- 네트워킹을 위한 / 응용 프로그램 개발 (http, smtp, ftp, ...)
- Scoket API를 어떻게 사용할 것인가? <-응용 개발자는 이것만 잘 해내면 됨
- Application <-> API <-> TCP/IP
- Client - Server 통신 모델 (1:1도 있지만 n(client):1(server)이 주)
- User level 프로그래밍
- socket programming이란?
- 네트워크로 연결된 둘 이상의 컴퓨터 사이에서의 데이터 송수신 프로그램 작성을 의미
- socket
- 네트워크(인터넷)의 연결 도구
- OS가 제공하는 소프트웨어 장치(라이브러리) -> 소프트웨어적으로 다 구현되어 있음 -> API만 잘 사용하자
- 소켓 통신 API 함수 호출 순서 : TCP (!중요!)
- server 프로그램은 가장 먼저 실행되어야 함!
- 1:1통신에서는 소켓 통신 부분이 바뀜(참고)
- 1:n통신에서는 연결 요청 수락 부분에서부터 주소할당까지 바뀜 (참고)
- 무조건 client쪽에서 연결 요청을 한다 해도 server가 받지 못하면 소켓 통신은 불가
- 받지 못하는 이유 -> 경우에 따라 server가 요청받을 수 있는 수가 초과한 경우 ex. 수강신청
Server의 연결 요청
- socket()
- 소켓 생성 함수
- server, client 공통
- TCP 소켓 : !연결 설정이 먼저 되어야 한다. ex. 전화기처럼
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-> 리턴: 성공 시 소켓(파일) 디스크립터, 실패 시 -1 반환
- domain : IPv4(제일 많이 쓰는 것), IPv6, IPX, low level socket 등 설정
- type : TCP, UDP 설정
- protocol : 특정 프로토콜 사용을 지정 (보통 0)
- (파일) 디스크립터 : 시스템으로부터 할당받은 파일이나 소켓을 대표하는 정수
- linux에서는 디스크립터, window에서는 핸들이라고 부름
- ex. 1번, 2번... 은 디스크립터이고 제품명은 소켓이나 파일을 의미함 -> 시스템이 만들어 놓은 것을 가리키기 좋고 편하게 하기 위해 시스템이 우리에게 건네주는 숫자
- bind()
- 소켓에 주소 할당 -> 서버 자신의 IP 주소와 port번호를 소켓에 연결한다.
- (참고)인바운드, 아웃바운드 -> 방화벽에서 확인 가능 -> 임의로 port번호를 연결하기 위해선 기존에 쓰고있는 port번호는 제외하고 다른 port번호를 연결
- port번호 : 응용프로그램 구분
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklent_t addrlen);
-> 리턴: 성공 시 0, 실패 시 -1
- sockfd : sock()으로 받아온 소켓 디스크립터
- sockaddr *myaddr : 서버의 ip 주소와 port번호 등 정보 입력 구조체
- struct sockaddr_in 설정
- serv_addr.sin_family = AF_INET; // IPv4 사용
- serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); // 주소 할당
- INADDR_ANY: 소켓이 동작하는 컴퓨터의 IP 주소가 자동으로 할당
- serv_addr.sin_port = htons(atoi(argv[1])); // 포트번호 할당
- htons: 호스트 바이트 순서를 네트워크 바이트 순서(Big endian)로 변경
- struct sockaddr_in 설정
- socklent_t addrlen : myaddr 구조체의 크기
- listen()
- bind까지 해서 연결했다면 client가 언제 접속할지 모르므로, client의 연결 요청을 기다리는 상태로 변경하는 함수
- 서버에서만 필요
- 연결 요청 대기 큐(backog queue)를 설정 : 대기할 수 있는 client 연결 요청의 최대 개수를 지정
#include <sys/socket.h>
int listen(int sockfd, int backlog);
-> 리턴: 성공 시 0, 실패 시 -1
- backlog : 연결 요청 대기 큐의 수
- accept()
- client의 연결 요청을 수락하는 기능
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-> 리턴: 성공 시 통신을 위한 소켓 디스크립터, 실패 시 -1 반환
- int sockfd : 소켓 디스크립터 -> 통신용도의 디스크립터를 할당해줌
- struct sockaddr *addr : 연결을 요청한 client의 정보가 저장됨
- Server Socket API 호출 순서
- socket() -> bind() -> listen() -> accept()
Client의 연결 요청
- connect()
- client의 연결 요청 함수
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-> 리턴: 성공 시 소켓 디스크립터, 실패 시 -1 반환
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklent_t addrlen);
-> 성공 시 0, 실패 시 -1 반환
- sockaddr 구조체 vs. sockaddr_in 구조체
- sockaddr 구조체
- 소켓의 주소를 담는 범용 구조체 (16bytes)
- char sa_data[14]; // ip주소 + port번호 -> 14byte에 정보를 모두 담기가 힘듦
- sockaddr_in 구조체(현재 더 많이 사용)
- 마찬가지로 16bytes
- sockaddr 구조체의 sa_family가 AF_INET (IPv4)인 경우에 사용
- IPv4 주소 체계를 쉽게 사용하기 위해 sockaddr_in 구조체를 사용 : port번호, IPv4 주소
- port번호와 ip주소를 나눠서 저장함 -> 이 구조체를 더 많이 사용하는 이유
- *uint16 : unsigned ->16비트 크기
- *s_addr의 용도: socketaddr 구조체와 크기를 똑같이 맞추기 위해 존재
- sockaddr 구조체
/* sockaddr 구조체 */
struct sockaddr
{
sa_family_t sa_family; // address family (2 bytes)
char sa_data[14]; // IP address + Port number (14 bytes)
};
/* sockaddr_in 구조체 */
struct sockaddr_in {
sa_family_t sin_family; // 2 bytes
uint16_t sin_port; // 2 bytes (0~65535): Port number
struct in_addr sin_addr; // 4 bytes: IP address
char sin_zero[8]; // 8 bytes: 전체 크기를 16바이트로 맞추기 위함
};
struct in_addr {
in_addr_t s_addr; // 4 bytes
};
- 저수준(low level) 파일 입출력과 파일 디스크립터
- 리눅스용 : write() -> read()
- 리눅스는 소켓도 파일로 간주한다! -> 그래서 입출력 함수를 기반으로 소켓 기반 데이터 송수신이 가능한 거임
- 파일 디스크립터란 os가 만든 파일(파일, 소켓, 디바이스 등)을구분하기 위한 숫자
- 저수준 파일 입출력 함수는 입출력을 목적으로 파일 디스크립터를 사용
- 그래서 저 수준 파일 입출력 함수에게 소켓 디스크립터를 전달하면, 소켓을 대상으로 입출력을 진행
파일 열기와 닫기
- open()
- 파일 열기
- path : 파일 경로 or 파일 이름 문자열
- flag : 파일 열기 모드 정보
- O_CREAT
- O_TRUNC
- O_APPEND
- O_RDONLY
- O_WRONLY
- O_RDWR
- ex. 0644 -> r 4, w 2, x 1 이므로 -> rw r r
- close()
- 파일 닫기
- 파일 디스크립터를 받아온 int변수를 파라미터로 사용
파일에 데이터 쓰기
- write()
- 파일 쓰기
- fd : 소켓 디스크립터 or 파일 디스크립터 둘 다 사용 가능
- buf : 전송할 데이터가 저장된 버퍼의 주소값, 한마디로 전송 또는 저장할 데이터
- nbytes : 전송할 데이터의 바이트 수
- sszie_t : signed int
- size_t : unsigned int
- 리턴값 : 실제 전송(저장)한 바이트 수
- 파일 쓰기
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
-> return the number of bytes written if success, -1 if fails
파일에 저장된 데이터 읽기
- read()
- fd : 소켓 디스크립터 or 파일 디스크립터
- buf : 읽은 데이터를 저장할 공간
- nbytes : 수신할 최대 바이트 수
- 리턴값 : 실제 읽은 바이트 수
- read 함수는 보통 디버깅 용으로 많이 사용
#include <unistd.h>
ssize_t read(int fd, const void *buf, size_t nbytes);
-> return the number of bytes read if success, -1 if fails
-> return 0 when reading end-of-file
- 파일 디스크립터와 소켓
- 새로운 파일을 열거나(open()) 소켓 생성(socket()): 3번부터 할당됨
- 왜냐면, 0, 1, 2는 이미 할당되어 있으므로
- 0 : 표준 입력 - stdin
- 1 : 표준 출력 - stdout
- 2 : 표준 에러 - stderr
- -------------------------------------
- 3 : socket() -> tcp 소켓
- 4 : open() ->파일 open
- 5 : socket() -> udp 소켓