본 글은 윤성우의 TCP/IP 소켓 프로그래밍을 참고하였습니다.

순서

I. 소켓 프로그래밍 절차

II. 서버 호출 함수 정보
socket() - bind() - listen() - accept() - read()/write() - close()

III. 서버 전체 구현 코드


I. 소켓 프로그래밍 절차

서버와 클라이언트의 함수 호출 순서

 


II. 서버 호출 함수 정보

#include <sys/socket.h>

1. socket()함수

- socket 함수는 어떤 프로토콜을 가진 소켓으로 통신할 것인지 결정하여 생성하는 함수이다.

#include <sys/socket.h>

int socket( int domain, int type, int protocol );
	--> 성공 시 파일디스크립터(이하 fd), 실패시 -1 반환.
    ㄴ. domain   : 소켓이 사용할 프로토콜 체계(Protocol Family) 정보 전달
    ㄴ. type     : 소켓의 데이터 전송방식에 대한 정보 전달
    ㄴ. protocol : 두 컴퓨터간 통신에 사용되는 프로토콜 정보 전달

"IPv4 인터넷 프로토콜 체계에서 동작하는 연결지향형 데이터 전송 소켓"
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

"IPv4 인터넷 프로토콜 체계에서 동작하는 비 연결지향형 데이터 전송 소켓"
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
더보기

인자1. int domain
프로토콜 체계

우리는 주로 IPv4를 사용한다. 
IPv6은 IP주소가 모두 고갈될 것을 염려하여 만들어진 16byte 주소 표준이다.

(+ 주소 표준이란?)

주로 사용하는 IPv4 (4byte)기준으로 ABCD 4개의 주소 클래스가 있는데,

차례대로 네트워크 ID의 Byte크기가 1,2,3,4이고
호스트 ID의 Byte크기가 3,2,1,0 이다.

이 주소 클래스에 따라 127.x.x.x 인지, 192.x.x.x인지 첫 번째 바이트 범위가 나누어진다.
A : 0~127
B: 128~191
C : 192~223

<sys/socket.h>에 포함된 프로토콜 체계

우리는 주로 IPv4를 사용한다. 
IPv6은 IP주소가 모두 고갈될 것을 염려하여 만들어진 16byte 주소 표준이다.

(+ 주소 표준이란?)

주로 사용하는 IPv4 (4byte)기준으로 ABCD 4개의 주소 클래스가 있는데,

차례대로 네트워크 ID의 Byte크기가 1,2,3,4이고
호스트 ID의 Byte크기가 3,2,1,0 이다.

이 주소 클래스에 따라 127.x.x.x 인지, 192.x.x.x인지 첫 번째 바이트 범위가 나누어진다.
A : 0~127
B: 128~191
C : 192~223

인자2. int type
전송방식

위 II - 3에서 설명한 TCP/UDP 계층을 선택한다.

TCP : SOCK_STREAM 
UDP : SOCK_DGRAM

인자3. int protocol
프로토콜 정보

하나의 프로토콜 체계 안에 데이터의 전송방식이 동일한 프로토콜이 둘 이상 존재할 때 필요한 인자이다.

말이 길고 어려운데, 우리가 원하는 TCP, UDP를 사용할 때는 0을 넣어도 무관하다. (만족하는 것이 딱 1개 이므로)

 


2. **bind() 함수

- bind()함수는 주소정보를 앞서 생성한 소켓에 할당하는 것이다. 개념이 조금,,, 어렵다ㅠ

#include <sys/socket.h>
#include <string.h>

int bind(int sockfd, (struct sockaddr) *myaddr, socklen_t addrlen);
    --> 성공 시 0, 실패 시 -1 반환
    ㄴ. sockfd  : 주소정보(IP,PORT)할당 할 소켓의 파일 디스크립터 (sock()의 반환 값)
    ㄴ. myaddr  : 할당 하고자하는 주소정보를 지니는 "구조체 변수의 주소 값"
    ㄴ. addrlen : myaddr 구조체 변수의 길이정보.


int serv_sock;
struct sockaddr_in serv_addr; // 이 구조체 개념 중요. 글 하단 설명 참고
char *serv_port = "9190";

// sock()함수 사용해 서버의 파일 디스크립터 얻음.
serv_sock = socket(PF_INET, SOCK_STREAM, 0)

// 구조체 주소정보 초기화 ** 이부분 개념 중요. 글 하단 설명 참고
memset(&serv_addr, 0, sizeof(serv_addr)); // 메모리 초기화
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons ( atoi(serv_port) );

// bind() (struct sockaddr*)로 캐스팅*
bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr));
구조체에 대한 분석

struct sockaddr_in
{
    sa_family_t    sin_family;  주소체계(Address Family)
    uint16_t       sin_port;    16비트 TCP/UDP PORT번호
    struct in_addr sin_addr;    32비트의 IP주소
    char           sin_zero[8]; 사용되지 않음 -> ??
}

struct in_addr{
    in_addr_t    s_addr;  32비트의 IPv4 인터넷 주소가 담긴다.
}

struct sockaddr
{
    sa_family_t sin_family    주소체계(Address Family)
    char        sa_data[14];  주소정보
}
더보기

인자1. int sockfd
소켓 파일 디스크립터

sock()함수를 통해 우리가 원하는 프로토콜을 가진 소켓의 파일 디스크립터이다.

인자2. (struct sockaddr) *myaddr
*sockaddr 구조체로 캐스팅 된 내가 정의한 sockaddr_in 구조체 주소 

우선 위 코드블럭의 "구조체에 대한 분석"을 보자. 

우선, 우리는 결과적으로 3번째 구조체인 sockaddr을 함수 인자로 보내야 한다.그 중 char sa_data[14]에는 bind함수가 요구대로
IP(4byte)PORT(2byte)가 모두 담겨지고 남은 부분은 또 0(8개 총 8byte)으로 채워야한다. 

이것은 매우 불편한 상황인데 이를 해결해 주는 구조체가 sockaddr_in 구조체이다!!

 

이 불편함을 해결해 줄 sockaddr_in의 멤버들을 보자.

1. sin_family -> sock()에서 인자로 넣었던 주소 체계로 AF_INET등 이 담긴다.

2. sin_port   -> 포트번호를 "네트워크 바이트 순서"로 저장해야함!!!

3. sin_addr   -> 32비트 IP주소를 "네트워크 바이트 순서"로 저장해야함!!
   그 속의 in_addr_t는 32비트 정수자료형임.
  
4. sin_zero[8] -> (struct sockaddr*)로 캐스팅 시 바이트 열 맞추기 위함. 0으로 채워짐

이 구조체의 2,3,4는 sockaddr구조체의 sa_data를 3개로 쪼개 놓아서 편하게 정의할 수 있도록 해준다!!
이 용도의 구조체이며, sin_zero를 위해 처음에 0으로 memset()을 해준다.

인자3. socklen_t addrlen
인자2에 넣어준 구조체 변수의 길이정보 (sizeof)

socklen_t는 <sys/socket.h>에 저장된 길이정보 자료형이다.
그냥 sizeof(myaddr) 하면 된다.

 


3. listen() 함수

- 주소가 할당된 소켓이 연결요청 대기상태로 들어간다.

#include <sys/socket.h>

int listen(int sock, int backlog);
    --> 성공 시 0, 실패 시 -1 반환
    ㄴ. sock    : 연결요청 대기상태로 두고자 하는 소켓의 파일 디스크립터
                  이 인자의 디스크립터의 소켓이 서버 소켓이 된다.
    ㄴ. backlog : 연결요청 대기 큐(Queue)의 크기정보 전달.
                  이 Queue의 크기만큼 클라이언트의 연결요청을 대기시킬 수 있다.
더보기

인자1. int sock
소켓의 파일 디스크립터

이 인자는 우리가 앞서 생성하고 주소할당을 한 소켓의 파일 디스크립터이다.

이 파일 디스크립터의 소켓이 서버 소켓(리스닝 소켓)이 된다.

인자2. int backlog
연결요청 대기 큐(Queue)의 크기

"은행 창구"와 같다.

모든 클라이언트의 요청은 순서대로 이루어지는데, 이미 창구가 사용 중 이면 (server-client 연결 중)
이후 클라이언트 들은 대기표를 뽑고 대기하고 있는 것이다.
이 대기창구를 시스템에서는 Queue에 저장하고 있다.

따라서 이 인자는 최대 몇 개의 클라이언트를 대기상태로 둘 것인지 정하는 숫자가 된다. 

 


4. accept() 함수

 - 대기상태의 클라이언트 요청을 수락한다.

#include <sys/socket.h>

int accept(int sock, (struct sockaddr*) addr, socklen_t* addrlen)
    --> 성공 시 생성된 소켓의 파일 디스크립터, 실패 시 -1 반환
    ㄴ. sock    : 서버 소켓의 파일 디스크립터 전달
    ㄴ. addr    : 연결요청 한 클라이언트의 주소정보를 담을 변수의 주소 값.
    ㄴ. addrlen : 두 번째 인자로 전달된 addr의 크기정보를 전달.
                  단! 미리 변수에 크기정보를 저장한 뒤, 주소를 전달한다. -> bind와 다른점
더보기

인자1. sock
서버 소켓의 파일 디스크립터를 전달한다.

인자2. addr
클라이언트 연결요청이 오면 그 주소정보를 담는 변수이다.

bind() 함수에서는 "서버"소켓에 주소정보를 담기위해 직접 멤버에 접근해 프로토콜을 지정해주었다.

하지만 accept() 함수에서는 struct sockaddr_in 구조체만 정의해주고 그 변수만 인자로 넣어주면 된다.
(이 변수에 클라이언트 주소정보가 담긴다)

이 변수 또한 struct sockaddr_in으로 정의해서 (struct sockaddr*)로 캐스팅 한다.

인자3. addrlen
인자2의 구조체의 길이정보를 담고 있는 주소 

핵심은 길이정보를 이미 담고 있어야 한다.

bind()에서는 인자 자체에 크기 변수가 들어갔는데,
accept()에서는 크기 정보가 들어가 있는 변수의 주소이다.

위와 같은 차이점에 주의하자.

 


5.read()/write()

데이터를 수신/송신한다.

#include <sys/socket.h>

int str_len;

ssize_t read(int fd, void *buf, size_t nbytes);
    -> 성공 시 수신한 byte 수(단 파일의 끝을 만나면 0), 실패 시 -1 반환
    ㄴ. fd     : 데이터를 받을 소켓의 파일 디스크립터(클라이언트)
    ㄴ. buf    : 수신한 데이터를 저장할 버퍼의 주소 값 전달
    ㄴ. nbytes : 수신할 최대 바이트 수 전달.
    
ssize_t write(int fd, void *buf, size_t nbytes);
    --> 성공 시 전달한 byte 수, 실패 시 -1 반환.
    ㄴ. fd     : 데이터를 보낼 소켓의 파일 디스크립터(클라이언트)
    ㄴ. buf    : 전송할 데이터가 저장된 버퍼의 주소 값 전달
    ㄴ. nbytes : 전송할 데이터의 바이트 수 전달.



int clnt_sock; //파일 디스크립터
char read_message[BUF_SIZE];
char send_message[BUF_SIZE];
int str_len, send_str_len;

str_len = read(cnlt_sock, read_message, BUF_SIZE);

write(clnt_sock, send_message, send_str_len); 
더보기

인자1. fd
read  : 데이터를 받을 대상 소켓의 파일 디스크립터 (클라이언트)
write : 데이터를 보낼 대상 소켓의 파일 디스크립터 (클라이언트)

인자2. buf
read  : 데이터를 받을 버퍼의 주소 값
write : 데이터를 보낼 버퍼의 주소 값  

인자3. nbytes
read  : 수신할 최대 바이트 수 전달
write : 송신할 데이터의 바이트 수 전달

 


6. close()

연결된 소켓을 종료한다.

#include <unistd.h>

int close(int fd);
    --> 성공 시 0, 실패 시 -1반환
    ㄴ. fd : 닫고자 하는 파일 또는 소켓의 파일 디스크립터

 

 


III. 서버 전체 구현 코드

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // 파일 관리 함수 헤더
#include <arpa/inet.h>
#include <sys/socket.h> // 소켓프로그래밍 함수선언

void errhandle(char *errmsg){
	fputs(errmsg, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(){
    // 서버, 클라이언트 소켓 파일디스크립터 변수 선언
	int serv_sock, clnt_sock;
    
    // sockaddr_in 구조체 변수 선언
    struct sockaddr_in st_serv_addr;
    struct sockaddr_in st_clnt_addr;
    
    // 보내고 받을 버퍼 정의
    char sendmsg[] = "Test";
    char recvmsg[1024];
    
    // ip, port 정의
    char ip[] = "127.0.0.1" //
    int port = 10000;
    
    // 서버 소켓 TCP/IP 프로토콜 생성
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock == -1) errhandle("socket() ERR!")
    
    // serv_sock에 bind로 주소 넣기 위한 밑작업
    memset(&st_serv_addr,0,sizeof(st_serv_addr));
    st_serv_addr.sin_family = AF_INET;
    st_serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    st_serv_addr.sin_port = htons(port)
    
    // bind()로 서버 소켓에 주소정보 할당
    int bindret = bind(serv_sock, (struct sockaddr*) &st_serv_addr, sizeof(st_serv_addr) );
    if(bindret == -1) errhandle("bind() ERR!");
    
    // listen()으로 서버소켓으로 오는 클라이언트 요청 대기
    int listenret = listen(serv_sock,10);
    if(listenret == -1) errhandle("listen() ERR!");
    
    // accept 하기위한 클라이언트 구조체 크기 밑작업 및 함수호출
    int clnt_addr_size = sizeof(st_clnt_addr);
    int acceptret = accept(serv_sock,
                          (struct sockaddr*) &st_clnt_addr,
                           &clnt_addr_size );
    if(acceptret == -1) errhandle("accept() ERR!");
    
    // sendmsg 보내고, recvmsg에 수신된 string데이터 담기
    write(clnt_sock, sendmsg, sizeof(sendmsg) );
    int readstrlen = read(clnt_sock, recvmsg, sizeof(recvmsg)-1);
    
    //소켓은 파일이다! 닫아야 한다.
    close(clnt_sock);
    close(serv_sock);
    
    return 0;
    }

+ Recent posts