[네트워크] Socket Programming in C

Youngeui Hong·2023년 9월 21일
0

🖥 Server

#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main()
{
    char server_message[256] = "You have reached the server.";

    /********************************************************************************
    1) socket: create socket
    - 소켓 생성에 성공하면 새로운 소켓의 file descriptor를 반환하고, 실패하면 -1을 반환함
    */
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    /********************************************************************************
    2) bind
    - The bind function asks the kernel to associate the server’s socket address in addr with the socket descriptor sockfd.
    */

    // define the server address
    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(9002);
    server_address.sin_addr.s_addr = INADDR_ANY;

    // bind the socket to our specified IP and Port
    bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    /********************************************************************************
     3) listen
    - The listen function converts sockfd from an active socket to a listening socket that can accept connection requests from clients.
    - The backlog argument is a hint about the number of outstanding connection requests that the kernel should queue up before it starts to refuse requests.
    */
    listen(server_socket, 5);

    /********************************************************************************
     4) accept
    - int accept(int listenfd, struct sockaddr *addr, int *addrlen);
    - Servers wait for connection requests from clients by calling the accept function. 
    - 서버는 listenfd 파일 디스크립터에 연결 요청이 오면, addr에 클라이언트 소켓 주소를 채워서 connected descriptor를 리턴함
    - 서버는 이 connected descriptor와 Unix I/O function을 사용하여 클라이언트와 통신할 수 있음 
    */
    int client_socket;
    client_socket = accept(server_socket, NULL, NULL);

/*
🤔 listening descriptor와 connected descriptor는 무슨 차이일까?
- listening descriptor: client connection requests의 endpoint. 한 번 생성되면 서버의 lifetime 동안 유효함
- connected descriptor: 클라이언트의 connection을 서버가 accept할 때마다 생성됨. 서버와 클라이언트가 연결되어 있는 동안만 유효함
*/   
   
   /********************************************************************************
     5) send
    */
    send(client_socket, server_message, sizeof(server_message), 0);

    /********************************************************************************
     6) close
    */
    close(client_socket);
    close(server_socket);

    return 0;
}

👩🏻‍💻 Client

#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main()
{
    /********************************************************************************
    1) socket: create socket
    - 소켓 생성에 성공하면 새로운 소켓의 file descriptor를 반환하고, 실패하면 -1을 반환함
    - 여기에서 반환 받은 file descriptor는 아직 읽고 쓸 준비가 되지 않은 상태임
    */
    int network_socket;
    // AF_INET: 32-bit IP 주소, SOCK_STREAM: 소켓이 연결의 endpoint가 된다는 뜻
    network_socket = socket(AF_INET, SOCK_STREAM, 0);

    /********************************************************************************
    2) connect
    - 클라이언트는 connect 함수를 호출하여 서버와 연결
    - 연결에 성공하면 0을 반환, 에러가 발생하면 -1을 반환
    - 연결에 성공하거나 실패할 때까지 connect 함수는 block하고 있음
    - 연결에 성공하면 client descriptor는 읽고 쓸 준비가 완료됨
    */
    // sockaddr_in: specify an address for the socket
    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(9002);       // 포트 번호를 네트워크 바이트 순서로 변환
    server_address.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0

    // sockaddr_in은 IPv4를 위한 구조체라면 sockaddr은 모든 종류의 주소를 처리하기 위한 범용 구조체
    int connection_status = connect(network_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    // check for error with the connection
    if (connection_status == -1)
    {
        printf("There was an error making a connection to the remote socket\n\n");
    }

    /********************************************************************************
    3) recv: receive data from the server
    */
    char server_response[256];
    recv(network_socket, &server_response, sizeof(server_response), 0);

    // print out the server response
    printf("The server sent the data %s\n", server_response);

    /********************************************************************************
    4) close: Close the socket
    */
    close(network_socket);

    return 0;
}

👀 더 알아보기

👯 Little Endian과 Big Endian

위의 코드에서 포트를 지정할 때 htons 함수를 사용하는데, 이때 hhost를 의미하고 nnetwork를 의미한다. 즉, host byte order를 network byte order로 변환한다는 의미이다.

htonl, htons, ntohl, ntohs 함수 모두 host byte order와 network byte order 간의 변환에 사용되는 함수이다.

그런데 왜 숫자를 바로 집어넣지 않고 바이트 순서 변환을 하는 걸까?

이를 위해서는 Little Endian과 Big Endian에 대한 이해가 필요하다.

아래의 그림과 같이 비트 표시에서는 가장 앞에 있는 바이트를 가장 중요한 바이트로 본다.

이때 Little Endian은 가장 덜 중요한 바이트가 먼저 오도록 정렬하는 방식이고, Big Endian은 가장 중요한 바이트가 먼저 오도록 정렬하는 방식이다.

이러한 바이트 순서가 이슈가 될 수 있는 경우는 이진 데이터가 네트워크를 통해 다른 컴퓨터로 전송될 때이다.

만약 리틀 엔디안 컴퓨터에서 보낸 데이터를 빅 엔디안 컴퓨터에서 그대로 처리해버린다면 워드들의 바이트 순서가 뒤바뀌어서 잘못된 데이터가 나올 것이다.

이러한 문제를 방지하기 위해 네트워크의 표준에 맞춰 바이트를 정렬하는 것이 필요한 것이다.

0개의 댓글