study/NETWORK PROGRAMMING

02-1. [Linux/Ubuntu] 네트워크 프로그래밍 - 소켓의 이해

김팥빵_ 2025. 3. 15. 00:18

네트워크 프로그래밍과 소켓의 이해

  • OSI 7 Layers
    • ISO(국제표준화기구)에서 개발한 모델
    • 1~4번은 OS에 대부분 탑재
    • NetworkProgramming에서 할 건 application 부분 

OSI 7 Layers 설명한 그림

  • 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 -> 응용 프로세스간 정보 교환
  • Socket API
    • 기본적으로 OS가 다 제공하기 때문에 우리는 "Socket API"만 정상적으로 호출하면 데이터를 전송하고 socket programming을 할 수 있음.

Socket API를 설명하는 그림

  • Socket(Network) Programming
    • 네트워킹을 위한 / 응용 프로그램 개발 (http, smtp, ftp, ...)
    • Scoket API를 어떻게 사용할 것인가? <-응용 개발자는 이것만 잘 해내면 됨
      • Application <-> API <-> TCP/IP
    • Client - Server 통신 모델 (1:1도 있지만 n(client):1(server)이 주)
    • User level 프로그래밍

socket programming 과정 - 3계층과 application program 사이의 통신

  • socket programming이란?
    • 네트워크로 연결된 둘 이상의 컴퓨터 사이에서의 데이터 송수신 프로그램 작성을 의미
  • socket
    • 네트워크(인터넷)의 연결 도구
    • OS가 제공하는 소프트웨어 장치(라이브러리) -> 소프트웨어적으로 다 구현되어 있음 -> API만 잘 사용하자
  • 소켓 통신 API 함수 호출 순서 : TCP (!중요!)
    • server 프로그램은 가장 먼저 실행되어야 함!
    • 1:1통신에서는 소켓 통신 부분이 바뀜(참고)
    • 1:n통신에서는 연결 요청 수락 부분에서부터 주소할당까지 바뀜 (참고)
    • 무조건 client쪽에서 연결 요청을 한다 해도 server가 받지 못하면 소켓 통신은 불가
      • 받지 못하는 이유 -> 경우에 따라 server가 요청받을 수 있는 수가 초과한 경우 ex. 수강신청

Network Programming의 전체를 관통하는 그림 -> 중요

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)로 변경
  • 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 구조체 */
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 소켓