소켓프로그래밍#1 : 소켓의 이해와 기본 뼈대

kkado·2022년 4월 29일
1
post-thumbnail

들어가기 앞서

첫 글입니다. 대학교에서 네트워크 프로그래밍 과목을 수강하면서 전반적인 이해가 많이 부족하다고 느껴 공부한 것을 정리하는 차원에서 가볍게 작성한 글이므로, 내용이 부족하거나 틀린 점이 있을 수 있습니다. 그리고 모든 코드의 작성과 구현은 리눅스 운영체제에서 실행하고 있음을 미리 알려드립니다.
틀린 점 지적이나 첨언은 언제든지 환영합니다!! 잘 부탁드립니다. :D

소켓(socket) 이란??

소켓은 프로그램들이 서로 통신할 수 있게 하는 창구 역할을 한다고 볼 수 있다. 각각의 프로세스(프로그램)는 소켓을 통해 데이터를 송신하고, 또 소켓을 통해 데이터를 수신한다.
소켓은 크게 프로토콜, IP주소, 포트로 정의된다. 하나하나에 대해 알아보자!

통신 프로토콜(protocol)

프로토콜이란 원래 외교 용어로써 사용되며, 국가 간에 으레 지켜야 하는 약속 등을 뜻한다. 네트워크 통신에서도 이와 비슷하게 통신 프로토콜, 또는 통신 규약은 컴퓨터 사이에서 메시지를 주고받는 데 필요한 양식, 약속이나 규약이다. 전세계에서 자유롭게 인터넷을 사용할 수 있는 것도 프로토콜 덕분이다. 프로토콜아 고마워

IP 주소

IP(Internet Protocol) 주소란, 각 장치(호스트)를 식별하기 위한 고유 주소이다. 이 주소를 통해 '내가 어디로 데이터를 보낸다' 를 확실히 알아야 통신이 가능할 것이다. 현재 널리 사용되는 주소 체계로는 IPv4와 IPv6 체계가 있다.

포트(port)

IP 주소를 통해 원하는 호스트에 도착했다고 끝이 아니라, 어떤 프로세스와 통신할지에 대한 정보 역시 필요하다. 예를 들어 내 컴퓨터에 데이터를 보내는 프로그램에게 내 컴퓨터에 켜져 있는 두 개의 인터넷 창 중 어디로 보내야 할 지를 알려줘야 하고 이 때 포트 번호를 통해 각 프로세스를 구분한다.

소켓 프로그램의 구현 - 연결 받는 소켓

소켓의 생성 : socket( )

소켓은 각 프로그램 간에 통신하는 '전화기' 라고 흔히들 비유하곤 한다. 소켓에 필요한 각 단계를 전화에 빗대어 설명할 수 있다.

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

위의 코드와 같이 socket() 함수를 통해 소켓을 처음 생성한다.
domain과 type, protocol 총 3가지의 파라미터들을 넘겨주게 되는데 하나씩 살펴보자.

  • domain
    이 통신에 사용되는 protocol family를 지정한다. family들은 sys/socket.h에 정의되어 있다.
    다양한 속성들이 있지만 주로 AF_INET(IPv4라는 뜻)을 사용한다.

  • type
    SOCK_STREAM, SOCK_DGRAM 두 가지가 가장 많이 사용된다.
    SOCK_STREAM은 TCP 통신 체계에서 사용되고,
    SOCK_DGRAM은 UDP 통신 체계에서 사용된다. TCP와 UDP에 대한 설명은 다음에...
    SOCK_STREAM은 양방향성이며 신뢰성 있고, 연결을 기반으로 한다. 반면 SOCK_DGRAM은 신뢰도를 보장하지 않으며 연결 기반보다는 데이터 전송적 성격이 강하다.

  • protocol
    이 소켓에서 사용되는 별도의 protocol이 필요할 경우 protocol 파라미터로 지정해 주면 되는데, 그런 것들이 없는 일반적 경우 보통 0을 전달한다.

    "소켓의 생성은 전화기를 새로 장만하는 것에 비유할 수 있다."

소켓의 주소 할당 및 연결 : bind( )

앞서 소켓을 전화기에 비유한다고 하였다. 전화기만 있으면 될 게 아니라 그 전화기에 전화번호를 할당해 주듯이, 소켓에도 특정한 주소 정보가 할당된다.

#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);

sockfd에는 해당 소켓을 나타내는 file descriptor 변수를, myaddr부분에는 소켓의 주소와 포트 번호를 담고 있는 sockaddr 구조체 포인터를 넣어 준다.

소켓을 연결 가능 상태로 : listen( )

전화기에 전화번호까지 할당이 됐으면, 이제 이 전화기를 연결 가능 상태로 만들어야 한다.

#include <sys/socket.h>

int listen(int sockfd, int backlog);

sockfd에는 어떤 소켓인지, 그리고 backlog는 연결을 위해 대기하는 큐의 최대 길이를 지정한다. 1:1로 서버-클라이언트가 통신하면 backlog는 1로 해 주어도 되고 보통 5 정도를 지정해 준다.

"bind()와 listen()을 거쳐 소켓은 연결 가능한 상태가 된다."

연결 수락 : accept( )

이 전화기로 전화가 왔다! 그 전화를 받기 위해서 우리는 수화기를 든다.
이 때 수화기를 드는 동작, 즉 이 연결 요청을 수용하는 함수가 accept()이다.

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *restrict addr,
			socklen_t *restrict addrlen);
            

accept를 함으로써 두 소켓은 서로 연결된 상태가 되었고, 이제 양방향적으로 통신이 가능하다.

소켓의 구현 - 연결 하는 소켓

앞서 전화를 받는 입장의 전화기에 대해 알아보았다면 이제는 전화를 거는 입장의 전화기를 알아보자. 전화 받는 전화기보다 그 기능이 매우 간단하다.

연결 요청 : connect( )

#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *addr, socklen_t addrlen);

끝! socket()을 통해 소켓을 생성하고, connect()로 연결 요청만 하면 끝이다.

간단한 소켓 통신 프로그램

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define BUFSIZE 1024

int main(int argc, char *argv[])
{
    int sockfd, cSockfd;
    char buf[BUFSIZE];
    socklen_t len;
    struct sockaddr_in servaddr, cliaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    memset(&servaddr, 0, sizeof(servaddr));

    int enable = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));

    servaddr.sin_family = AF_INET;
    // 주소 체계 설정. AF_INET = IPv4 주소 체계
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 
    // 자신의 IP주소를 할당. INADDR_ANY 하면 자동으로 할당 됨.
    // htonl(host to network long) (big endian 방식을 사용하기 때문에 바꿔줄 필요가 있다)
    servaddr.sin_port = htons(atoi(argv[1]));
    // 포트 넘버 입력

    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    listen(sockfd, 5);

    cSockfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);

    memset(buf, 0, sizeof(buf));
    while((read(cSockfd, buf, sizeof(buf))) > 0)
    {
        printf("%s", buf);
        memset(buf, 0, sizeof(buf));
    }

    close(sockfd);
    close(cSockfd);
    return 0;
}

클라이언트에서 메시지를 입력받아서 표준 출력으로 출력하는 server.c

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<arpa/inet.h>

int main(int argc, char** argv)
{
    int sockfd;
    char buf[1024];
    struct sockaddr_in servaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(atoi(argv[1]));
    servaddr.sin_addr.s_addr = inet_addr(argv[2]);
    // 입력받은 IP(127.0.0.1)을 inet_addr()로 IP화 시켜서 넣어줌

    connect(sockfd, (const struct sockaddr*)&servaddr, sizeof(servaddr));

    while(1)
    {
        memset(buf, 0, sizeof(buf));
        printf("Input Message : ");
        fgets(buf, sizeof(buf), stdin);

        write(sockfd, buf, strlen(buf));
    }

    close(sockfd);
    return 0;
}

서버로 메시지를 보내는 client.c

실행 결과


myclient.c에서 작성하여 보낸 메시지들이 server.c 터미널에 출력되는 것을 볼 수 있다.

profile
울면안돼 쫄면안돼 냉면됩니다

0개의 댓글