[C언어] 19강 TCP/IP 소켓프로그래밍(채팅 프로그램 )

강지원·2025년 1월 25일

리눅스 기반 C언어

목록 보기
22/24

1. 통신이란?

A가 신호를 보내면 B가 알아들으면 됨.
단방향, 양방향, ...
신호에 대한 의미를 약속한 것이 프로토콜이다.

중요한 것은 통신하기 위한 물리적인 경로이 필요하다(랜선, 와이파이,..)

TCP(Transmission Control Protocol)

A - B
A가 B에게 통신신호(패킷)을 보냈는데 B에게 잘 받았는지 확인받는 절차까지 포함한 프로토콜이다. 따라서 안정성, 신뢰성이 높다. BUT 속도가 느릴 수 있다. 요즘은 대부분 TCP를 사용한다.

TCP 통신이란 네트워크 통신에서 데이터를 신뢰성 있게 전달하기 위해 사용되는 연결 지향형 통신 프로토콜입니다.
인터넷 프로토콜(IP)과 함께 TCP/IP 스택의 핵심 구성 요소로, 웹 브라우징, 파일 전송, 이메일 등 다양한 네트워크 애플리케이션에서 사용됩니다.

UDP(User Datagram Protocol)

A - B
A가 B에게 패킷을 던지고 확인절차는 없다. 안정성이 낮지만, 속도는 빠르다. 게임, 영화 실시간 스트리밍할 때 주로 사용한다.

2. 소켓 프로그래밍 개념

Socket : 네트워크 통신의 끝점(Endpoint)으로, 두 장치(프로세스)가 데이터를 주고받기 위해 사용하는 인터페이스입니다. 소켓은 운영 체제와 네트워크 간의 중간다리 역할을 하며, 프로그램이 네트워크를 통해 데이터를 송수신할 수 있도록 도와줍니다.

  • 클라이언트(Client): 데이터를 요청하는 역할.
    - 소켓을 열고 서버 ip 주소를 통해서 서버로 접속을 시도하는 주체
  • 서버(Server): 데이터를 제공하거나 요청을 처리하는 역할.
    - 소켓을 열고 listen 하는 상태

1) 서버

  1. 빈 소켓 만들기 : 함수 호출로 이루어짐(int로 받음)
  2. bind : 소켓에 어떤 ip, port, 프로토콜을 사용할 지 알려줌
  3. listen : 클라이언트를 기다림
    (클라이언트가 접속을 시도하면)
  4. accept : 클라이언트 허용 (클라이언트 와의 세션(통로)이 맺어짐)
  5. send , receive 반복

서버에서는 두 종류의 소켓을 사용

  1. 서버 소켓
    • listen
    • connection을 맺는 과정에서 진행
  2. 클라이언트 소켓
    • 클라이언트 한명당 소켓이 생성되는 것
    • 실제로 패킷을 주고 받는 소켓

2) 클라이언트

  1. 빈 소켓 만들기
  2. : ip, port, 프로토콜을 정해서 알려줌
  3. connect : 서버 listen과 접속
  4. send, receive 반복

3. 서버 구현하기

1) 헤더

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

* 알아둘 것

#include <>  // 표준 라이브러리
#include ""  // 사용자 정의 라이브러리

2) 빈 소켓 만들기

#define CLNT_MAX 10

int g_clnt_socks[CLNT_MAX];
int g_clnt_count=0;

int main(int argc, char ** argv){
	int serv_sock;
    int clnt_sock;
    
    struct sockaddr_in clnt_addr;
    int clnt_addr_size;
    
    struct sockaddr_in serv_addr;

3) bind 하기(IP, Port, Protocol)

serv_sock = socket(PF_INET,SOCK_STREAM,0);

serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port=htons(7989);
if(bind(serv_sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr))==-1){
		printf("bind error\n");
}

위 코드를 설명하면,

serv_sock 이란 IPv4와 TCP를 사용하는 빈 소켓을 만들어주고,

  • 주소 체계 = AF_INET : IPv4,
  • 서버의 IP주소 = INADDR_ANY : 0.0.0.0 서버가 모든 사용 가능한 네트워크 인터페이스에서 들어오는 요청을 수신하도록 설정
  • 포트번호 : 7989

위 정보를 bind 해준다. = 생성한 소켓과 설정한 주소를 연결한다.

bind() 은 소켓을 특정 IP 주소와 포트 번호에 바인딩하여 서버가 클라이언트 요청을 수신할 준비를 한다.

bind()의 반환값:
성공 시 0 반환.
실패 시 -1 반환하며, 에러 원인은 errno로 확인 가능하다.

* 리틀엔디안 , 빅엔디안

운영체제에서 데이터를 저장하는 방식이 다양하다. 그렇기 때문에 네트워크통신을 하기위해서는 네트워크 오더를 따라야 한다.

  • htonl(INADDR_ANY): INADDR_ANY를 네트워크 바이트 순서로 변환(htonl)하여 서버의 IP 주소에 할당합니다.
  • htons(7989) : 포트 번호를 네트워크 바이트 순서로 변환한다.

domain - 네트워크 주소체계 (IPv4와 IPv6)

#define AF_INET 2   // IPv4
#define AF_INET6 23 // IPv6

*IPv4
ex) 127.36.95.2
주로 IPv4 를 많이 씀
PF_INET 으로도 사용가능하다. 프로토콜을 설정할 땐 PF_INET을 사용하고, 주소를 설정할 땐 AF_INET을 사용한다.

*IPv6
경우의 수를 따져보니까 ip가 모자라서 IPv6를 만듦
근데 왜 잘 안쓰냐? 공유기의 등장으로 인해 v6가 필요없게 된다.
A 공유기가 대표 IP를 받고 많은 기기를 연결할 수 있게 되어서 v4로도 문제가 없게 된다.
+유동 IP개념

type - 소켓 타입

#define SOCK_STREAM 1  //스트림, TCP 프로토콜의 전송방식
#define SOCK_DGRAM 2  //데이터그램, UDP 프로토콜의 전송방식
#define SOCK_RAW 3    //RAW 소켓, 가공하지 않은 소켓

protocol - 프로토콜

#define IPPROTO_TCP 6    //TCP 프로토콜
#define IPPROTO_UDP 17   //UDP 프로토콜
#define IPPROTO_RAW 255   //RAW

protocol 인자는 사용할 프로토콜을 명시적으로 지정할 수 있도록 되어 있지만, 대부분의 경우 명시할 필요가 없다.

0을 전달하면, domaintype에 따라 기본값이 자동으로 선택됩니다.
예를 들어:

  • SOCK_STREAM을 사용하면 TCP가 기본 프로토콜로 선택됩니다.
  • SOCK_DGRAM을 사용하면 UDP가 기본 프로토콜로 선택됩니다.

4) listen()

if(listen(serv_sock,5)== -1){  
        printf("listen error\n");
}

listen() 함수는 소켓을 수신 대기 상태로 전환하며, 서버가 클라이언트의 연결 요청을 받을 준비를 한다.

backlog는 연결 요청 대기열의 최대 길이를 지정하는 인자이다.
즉, 동시에 처리되지 않고 대기 상태에 들어갈 수 있는 클라이언트 연결 요청의 수를 정의하고, 요청 대기열이 가득 차면 추가 연결 요청은 처리되지 않고 거부된다.

5) accept : 클라이언트 허용

char buff[200];
        int recv_len=0;
        while(1){
                clnt_addr_size = sizeof(clnt_addr);
                clnt_sock = accept(serv_sock,(struct sockaddr *)&clnt_addr,&clnt_addr_size);
                
                g_clnt_socks[g_clnt_count++] = clnt_sock;

서버 소켓(serv_sock)에서 클라이언트 연결 요청을 수락한다.
반환값 clnt_sock는 새로 생성된 클라이언트와의 통신 소켓입니다.
clnt_addr에는 연결된 클라이언트의 IP 주소와 포트 번호 정보가 저장됩니다.

6) read, printf

				while(1){
                        recv_len =read(clnt_sock, buff,200);
                                                                                                    printf("recv : ");
                        for(int i=0;i<recv_len;i++){
                                printf("%02X",(unsigned char)buff[i]);                              }
                        printf("\n");
                }
        }

}

클라이언트로부터 데이터를 읽는 무한 루프이다.

read() 함수:
clnt_sock에서 데이터를 읽어 buff에 저장한다.
최대 200바이트 읽어오고, 읽은 데이터 수를 반환하여 recv_len 에 저장한다. 이는 출력을 하기 위함이다.
읽은 데이터의 바이트 수를 반환하며, 이를 recv_len에 저장합니다.

read() 반환값:

  • 양수: 읽은 데이터의 크기(바이트).
  • 0: 클라이언트가 연결을 종료함.
  • -1: 오류 발생.

클라이언트로부터 읽어온 데이터를 16진수(HEX)로 출력합니다.

4. 클라이언트 구현하기

이번 강에서는 대충구현해서 실행한것만 보여주고 다음 강의에서 제대로 설명할 예정

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

#define BUFFSIZE 200
#define NAMESIZE 20

char message[BUFFSIZE];

int main(int argc, int **argv){

        int sock;
        struct sockaddr_in serv_addr;
        pthread_t snd_thread, rcv_thread;
        void* thread_result;

        sock=socket(PF_INET, SOCK_STREAM,0);
        if(sock==-1)
                printf("socket() error");

        memset(&serv_addr,0,sizeof(serv_addr));
        serv_addr.sin_family=AF_INET;
        serv_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
        serv_addr.sin_port=htons(7989);

        if(connect(sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr))==1)
        		printf("connet() error");

        unsigned char msg[100] = {0x01,2,3,4,5,6,1,2,3,4,3,0x03};
        while(1){
                printf("send: ");
                write(sock,msg,12);
                sleep(1);
                }
        close(sock);
        return 0;
}

5. 실행결과

1) 서버실행

$ gcc -o server server.c
$ ./server
서버실행중

2) 클라이언트 실행

gcc -o clnt clnt.c
$ ./clnt

3) 실행결과

클라이언트에서 데이터를 읽어와 무한루프로 실행한다.

0개의 댓글