[Multiflexing]echo 서버, 클라이언트 만들어보기

정하나둘·2023년 8월 4일
0

webserv

목록 보기
3/4

소켓: 네트워크에서 서버와 클라이언트가 서로 특정 포트를 이용하여 양방향 통신하도록 만들어주는 소프트웨어 장치

소켓 프로그래밍: 소켓을 사용하여 네트워크를 구성해나가는 과정

서버소켓과 클라이언트 소켓의 흐름은 다음과 같다.

서버소켓:

1. 소켓 생성: Create
2. 서버가 사용할 주소 지정하여 결합(IP, Port): Bind
3. 연결 요청 대기: Listen
4. 요청 수신시 받기: Accept
5. 연결 수립시 데이터 송수신: send/recv
6. 송수신 완료, 소켓 닫기: Close

클라이언트 소켓:

1. 소켓 생성: Create
2. 서버 연결 요청: Connect
3. 연결 요청 받으면 데이터 송수신: send/recv
4. 송수신 완료, 소켓 닫기: Close

이미지 출처: https://saynot.tistory.com/entry/%EC%86%8C%EC%BC%93-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-TCP-Echo-Client-Server-%EA%B5%AC%ED%98%84

서버/클라이언트 함수 정리

->서버 함수 정리

1. Socket() 함수

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

  • 헤더: sys/types.h, sys/socket.h
  • 형태: int socket(int domain, int type, int protocol)
  • 인수: int domain 인터넷을 통해 통신할 지, 같은 시스템 내에서 프로세스 끼리 통신할 지의 여부를 설정합니다.
    int type 데이터의 전송 형태 지정
    int protocol 통신에 있어 특정 프로토콜을 사용을 지정하기 위한 변수이며, 보통 0 값을 사용합니다.
  • 반환: -1 == 실패, -1 이외 소켓 디스크립터

int domain : 인터넷을 통해 통신할지, 같은 시스템 프로세스 끼리 통신할지의 여부를 설정

PF_INET, AF_INET IPv4 인터넷 프로토콜을 사용합니다.
PF_INET6 IPv6 인터넷 프로토콜을 사용합니다.
PF_LOCAL, AF_UNIX 같은 시스템 내에서 프로세스 끼리 통신합니다.
PF_PACKET Low level socket 을 인터페이스를 이용합니다.
PF_IPX IPX 노벨 프로토콜을 사용합니다.

|테스트1|강조3|테스트3|

domain내용
PF_INET, AF_INETIPv4 인터넷 프로토콜을 사용합니다.
PF_INET6IPv6 인터넷 프로토콜을 사용합니다.
PF_LOCAL, AF_UNIX같은 시스템 내에서 프로세스 끼리 통신합니다.
PF_PACKETLow level socket 을 인터페이스를 이용합니다.
PF_IPXIPX 노벨 프로토콜을 사용합니다.

int type: 데이터의 전송 형태를 지정하며 아래와 같은 값을 사용할 수 있다.

type내용
SOCK_STREAMTCP/IP 프로토콜을 이용합니다.
SOCK_DGRAMUDP/IP 프로토콜을 이용합니다.

2. bind() 함수

-bind 함수는 주소정보를 앞에서 Socket()함수로 생성한 소켓에 할당하는 것이다.

  • 헤더: sys/types.h, sys/socket.h
  • 형태: int bind(int sockfd, (struct sockaddr) *myaddr, socklen_t addrlen)
  • 인수: int sockfd 주소정보(IP/Port)할당 할 소켓의 파일 디스크립터(socket()의 반환 값).
    (struct sockaddr) *myaddr 할당 하고자하는 주소정보를 지니는 구조체 변수의 값.
    socklen_t addrlen myaddr 구조체 변수의 길이정보.
  • 성공 시 0, 실패시 -1 반환

2-1. sockaddr 구조체

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]; 항상 0;
}
 
struct in_addr{
    in_addr_t    s_addr;  32비트의 IPv4 인터넷 주소가 담긴다.
}
 
struct sockaddr
{
    sa_family_t sin_family    주소체계(Address Family)
    char        sa_data[14];  주소정보
}

bind 함수의 2번째 인자 (struct sockaddr) *myaddr는
sockaddr 구조체로 캐스팅 된 내가 정의한 sockaddr_in 구조체의 주소이다.
위 코드블록의 구조체에 대한 분석을 보면 우리가 매개변수로 형변환해서 넘겨야할 구조체는 맨 아래에 있는 sockaddr 구조체인 것을 알 수 있다.
그 중 char sa_data[14]에는 bind 함수가 요구대로 IP(4byte)와 PORT(2byte)가 담겨지고 남은 부분은 0(8개 총 8byte)으로 채워야한다.

불편한 이 부분을 해결하기 위해 만들어진 구조체가 sockaddr_in(최상단) 구조체이며
1. sin_family->socket()에서 domain 인자로 들어갔던 값이 담긴다(ex: AF_INET...)
2. sin_port-> 포트번호를 빅엔디안(네트워크 바이트 순서)으로 저장
3. sin_addr-> 32비트 IP주소를 빅엔디안(네트워크 바이트 순서)으로 저장, in_addr 구조체는 속에 in_addr_t는 32비트 정수 자료형이다.
이 곳에는 자신의 IP를 할당하는데 자신의 랜카드가 2개 이상이여서 IP주소가 2개 이상 있지 않는다면 보통 INADDR_ANY를 사용하여 자동으로 할당되도록 할 수 있다.
4. sin_zero->sin_zero는 항상 0이어야 하는데 이를 어기면 간혹 IP주소를 터무니 없는 값으로 인식하는 경우가 생긴다 따라서 일반적으로 memset이나 zeroMemory 등으로 초기화한 후 사용한다.

기본 코드:

 memset(&servAddr, 0, sizeof(servAddr));
 servAddr.sin_family=AF_INET;
 servAddr.sin_addr.s_addr=htonl(INADDR_ANY);
 servAddr.sin_port = htons(atoi("8080"));

htonl,htons 의미:
htonl은 host to network long으로 이를 이해하기 앞서 빅엔디안과 리틀엔디안에 대해 알아야한다. 빅엔디안은 높은 메모리 주소에 뒤의 값을 넣는다. 예를 들자면 char *ch = "abcd";라는 문자열이 있을 경우 빅엔디안은 메모리가 낮은곳에서부터 a,b,c,d를 저장한다는 뜻이다. 물론 리틀 엔디안은 그 반대이다. 이들은 cpu가 데이터를 메모리에 저장하는 방식에 따라 다른게 되는데 네트워크 통신시에 데이터 전송을 정해놓지 않으면 cpu가 다르면 통신이 되지 않는 상황이 벌어질 수 있어 네트워크는 모두 빅 엔디안으로 통일하게 됐다. htonl,htons 함수는 호스트가 무엇이든 네트워크 즉 빅엔디안으로 long 또는 short 타입으로 변경한다는 뜻이다.

3.listen()함수

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

  • 헤더: sys/types.h, sys/socket.h
  • 형태: int listen(int sock, int backlog);
  • 인수: int sock 클라이언트로부터 연결 요청을 받아들이기 위한 소켓 파일 디스크럽터(socket()함수 리턴값)
    int backolog 연결요청 대기 큐의 크기에 대한 설정(크기정보 전달)
    이 Queue의 크기만큼 클라이언트의 연결요청을 대기시킬 수 있다.
    5가 전달되면 큐의 크기가 5가 되어서 클라이언트의 연결요청을 5개까지 대기시킬 수 있다. 적절한 인자의 값은 서버의 성격마다 다르지만 웹 서버와 같이 잦은 연결 요청을 받는 서버의 경우 최소 15번 이상을 전달해야한다.
  • 반환: 실패 시 -1, 성공 시 -1 이외 소켓 디스크립터

연결 요청 대기 상태: 서버의 소켓과 큐가 완전히 준비되어 클라이언트의 연결 요청을 받아들일 수 있는 상태.
연결 요청 대기 큐: 연결 요청을 대기시킬 수 있는 일종의 대기실로 서버 측에서 accept 함수로 허락하기 전까지 머물러있다.

listen 함수 호출을 성공하게 되면, 이제 여러 클라이언트들이 연결을 요청해 올 것이고, 모든 연결 요청은 서버가 미리 만들어 놓은 대기실로 들어가 순서대로 연결요청이 수락될 때까지 기다려야 한다.

listen()함수를 호출하면 서버 소켓 상태는 CLOSE에서 LISTEN 상태로 변경되고, 연결을 요청한 클라이언트 소켓은 SYN_RCVD 상태에서 3-way-handshaking을 완료하고 ESTABLISHED 상태가 된다.

4.accept()함수

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

  • 헤더: sys/types.h, sys/socket.h
  • 형태: int accept(int sock, (struct sockaddr*) addr, socklen_t* addrlen);
  • 인수: int sock 서버 소켓의 파일 디스크럽터
    (struct sockaddr*) addr 연결요청 한 클라이언트의 주소정보를 담을 변수의 주소 값.-> bind()함수에서 "서버"소켓에 주소정보를 담기위해 직접 멤버에 접근해 프로토콜을 지정해주었지만 accept() 함수에서는 struct sockaddr_in 구조체만 정의해주고 그 변수만 인자로 넣어주면 된다.(이 변수에 클라이언트 주소정보가 담긴다)
    socklen_t* addrlen 두 번째 인자로 전달된 addr의 크기정보를 전달.
    단! 미리 변수에 크기정보를 저장한 뒤, 주소를 전달한다.-> bind와 다른점
    bind()에서는 인자 자체에 크기 변수가 들어갔는데, accept()에서는 크기 정보가 들어가 있는 변수의 주소이다.
    ex:
int  client_addr_size;

client_addr_size = sizeof( client_addr);
client_socket = accept( server_socket, (struct sockaddr*)&client_addr,
                                                          &client_addr_size);
if ( -1 == client_socket){
   printf( "클라이언트 연결 수락 실패\n");
   exit( 1);
}

sizeof()로 크기를 전달하던 bind와 차이점이다.

  • 반환: 실패 시 -1, 성공 시 -1 이외 생성된 소켓 디스크립터

5.read()/write()

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

  • 헤더: sys/types.h, sys/socket.h
  • 형태: ssize_t read(int fd, void *buf, size_t nbytes);
    ssize_t write(int fd, void *buf, size_t nbytes);
  • 인수: (read)int fd 데이터를 받을 소켓의 파일 디스크럽터(클라이언트)
    (read)void *buf 수신할 데이터를 저장할 버퍼의 주소 값 전달
    (read)size_t nbytes 수신할 최대 바이트 수 전달

(write)int fd 데이터를 보낼 소켓의 파일 디스크럽터(클라이언트)
(write)void *buf 전송할 데이터를 저장할 버퍼의 주소 값 전달
(write)size_t nbytes 전송할 데이터의 바이트 수 전달

  • 반환: 실패 시 -1, 성공 시 바이트 수

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

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

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

6. close()

-연결된 소켓을 종료한다.

  • 헤더: unistd.h
  • 형태: int close(int fd);
  • 인수: int fd 닫고자 하는 파일 또는 소켓의 파일 디스크립터
  • 반환: 실패 시 -1, 성공 시 0

->클라이언트 함수 정리

서버와 connect 함수만 다르고 다 서버에서 정리한 내용이라 connect 함수만 다뤄보겠다.

1. Connect()함수

-listen 중인 서버 소켓에 연결요청을 한다.
connect()가 성공적으로 끝나면 바로 read/write 가능

  • 헤더: sys/types.h, sys/socket.h
  • 형태: int connect(int sock, (struct sockaddr) *servaddr, socklen_t addrlen);
  • 인수: int sock socket()함수를 통해 얻은 클라이언트 소켓의 파일 디스크럽터
    (struct sockaddr) *servaddr sockaddr 구조체로 캐스팅 된 내가 정의한 sockaddr_in 구조체 주소
    클라이언트->서버로 연결요청을 하기 때문에 주소정보가 서버측과 동일해야 한다.
    따라서 두 번째 인자에서 서버측 구현과 동일하게 서버의 주소정보 구조체가 들어간다.
    socklen_t addrlen 인자2에 넣어준 구조체 변수의 길이정보(sizeof)
    socklen_t는 <sys/socket.h>에 저장된 길이정보 자료형이다. 그냥 sizeof(serv_addr)하면 된다.
  • 반환: 실패 시 -1, 성공 시 0
profile
내가 다시 보려고 만드는 42서울 본과정 블로그

0개의 댓글