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

순서

I. 소켓 프로그래밍 절차

II. 클라이언트 함수 호출 정보
socket() - connect() - read()/write() - close()


III. 클라이언트 전체 구현 코드


I. 소켓 프로그래밍 절차

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

클라이언트는 매우 짧다.

사실상 데이터 송수신, 연결종료는 너무 간단하고,
socket()은 서버측과 동일하고
connect()는 서버측의 bind()와 90% 동일하다.

서버보다 훨~씬 쉽고 간결하다.


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. connect() 함수


listen중인 서버 소켓에 연결요청을 한다.

connect()가 성공적으로 끝나면 바로 read/write가능

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

int connect(int sock, (struct sockaddr) *servaddr, socklen_t addrlen);
    --> 성공 시 0, 실패 시 -1 반환
    ㄴ. sock     : 클라이언트 소켓의 파일 디스크립터
    ㄴ. servaddr : 연결요청 할 서버의 주소정보를 담은 "구조체 변수의 주소 값"
    ㄴ. addrlen  : 두번 째 인자인 servaddr 구조체 변수의 크기정보.


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

// sock()함수 사용해 서버의 파일 디스크립터 얻음.
clnt_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 = inet_addr(serv_ip);
serv_addr.sin_port = htons ( atoi(serv_port) );

// connect() (struct sockaddr*)로 캐스팅*
connect(clnt_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 sock
소켓 파일 디스크립터

sock()함수를 통해 얻은 클라이언트 소켓의 파일 디스크립터이다.

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

캐스팅 된 이유와 개념은
앞의 서버구현 글에 있다.

클라이언트 -> 서버로
연결요청을 하기 때문에 주소정보가 서버측과 동일해야 한다.

따라서 두번 째 인자에서 서버측 구현과 동일하게 서버의 주소정보 구조체가 들어간다.
이 점을 유의하자.

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

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

 


3.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 : 송신할 데이터의 바이트 수 전달

 


4. 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 clnt_sock;
    
    // sockaddr_in 구조체 변수 선언
    struct sockaddr_in st_serv_addr;
    
    // 보내고 받을 버퍼 정의
    char sendmsg[] = "Test_clnt";
    char recvmsg[1024];
    
    // ip, port 정의
    char ip[] = "127.0.0.1" //
    int port = 10000;
    
    // 클라이언트 소켓 TCP/IP 프로토콜 생성
    clnt_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 = inet_addr(ip);
    st_serv_addr.sin_port = htons(port)
    
    
    // connect()으로 서버소켓에 연결요청
    int connret = connect(clnt_sock,
                         (struct sockaddr*) &st_serv_addr,
                          sizeof(st_serv_addr));
                          
    if(connret == -1) errhandle("connect() ERR!");
    
    
    // sendmsg 보내고, recvmsg에 수신된 string데이터 담기
    write(clnt_sock, sendmsg, sizeof(sendmsg) );
    int readstrlen = read(clnt_sock, recvmsg, sizeof(recvmsg)-1);
    
    //소켓은 파일이다! 닫아야 한다.
    close(clnt_sock);
    
    return 0;
    }

+ Recent posts