일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 포인터
- Houdini
- docker
- 유니티
- Algorithm
- git
- 깃
- dart 언어
- 구조체
- vim
- c# 윈폼
- jupyter lab
- Data Structure
- c# winform
- C++
- gitlab
- C언어 포인터
- Unity
- Python
- jupyter
- 도커
- c언어
- 플러터
- HTML
- c#
- c# 추상 클래스
- C# delegate
- github
- Flutter
- 다트 언어
- Today
- Total
nomad-programmer
[Programming/C] 소켓과 서버 본문
인터넷에 있는 대부분의 저수준 네트워킹 코드는 C로 작성되었다. 네트워크로 연결되는 애플리케이션은 서버와 클라이언트, 두 프로그램이 필요하다.
서버는 동시에 여러 클라이언트와 통신할 수 있어야 한다. 클라이언트와 서버는 프로토콜(protocol)이라는 구조화된 통신 방법을 사용한다. 인터넷에서는 다양한 프로토콜이 사용된다. 이 중에 인터넷 프로토콜(Internet Protocol, IP)과 같은 저수준 프로토콜은 인터넷으로 1과 0으로 된 이진값을 전송하는 방법을 정의한다. 하이퍼텍스트 전송 프로토콜(HyperText Transfer Protocol, HTTP)과 같은 고수준 프로토콜은 웹브라우저와 웹 서버가 통신하는 방법을 정의한다.
클라이언트와 서버는 프로토콜이라는 구조화된 통신 규약을 따른다.
C프로그램이 외부와 소통하려면 바이트들을 읽고 쓰는 데이터 스트림을 사용해야 한다. 네트워트와 소통하는 프로그램을 만드려면 소켓(Socket)이라는 새로운 형태의 데이터 스트림이 필요하다.
#include <sys/socket.h>
// listener_d는 소켓에 대한 디스크립터
int listener_d = socket(PF_INET, SOCK_STREAM, 0);
if(listener_d == -1) {
error("소켓을 열 수 없다");
}
소켓을 이용해 서버와 클라이언트 프로그램 간에 대화를 하려면 네 단계를 거쳐야 한다. 이 단계는 간단히 BLAB(Bind, Listen, Accept, Begin)이라는 약자로 기억하면 된다.
1. Bind: 포트에 바인딩
2. Listen: 듣기
3. Accept: 접수
4. Begin: 대화 시작
포트에 바인딩 (Bind)
컴퓨터는 한 번에 여러 서버 프로그램을 실행할 수 있다. 웹 페이즈를 보내거나, 이메일을 보내거나, 채팅 서버를 한꺼번에 실행할 수 있다. 서로 대화가 섞이지 않도록 각 서버는 각기 다른 포트를 사용한다. 포트는 TV의 채널과 똑같다. 다른 방송을 보려면 다른 채널로 돌리 듯이 다른 서비스를 사용하려면 다른 포트를 사용해야 한다.
시작할 때 서버 프로그램은 운영체제에 어떤 포트를 사용할 지 알려줘야 한다. 이 과정을 "포트에 바인딩한다" 고 한다. 바인딩하기 위해서는 소켓 디스크립터와 소켓 이름이 필요하다. 소켓 이름은 구조체이다.
// 인터넷 주소를 만들려면 이 헤더 파일이 필요하다.
#include <arpa/inet.h>
...
struct sockaddr_in name;
name_sin_family = PF_INET;
name.sin_port = (in_port_t)htons(30000);
name.sin_addr.s_addr = hton1(INADDR_ANY);
int c = bind(listener_d, (struct sockaddr *) &name, sizeof(name));
듣기 (Listen)
서버의 인기가 좋아지면 한꺼번에 많은 클라이언트들이 연결될 것이다. 만약 클라이언트들이 줄을 서서 연결을 기다리게 하고 싶다면 listen() 시스템 API를 호출하면 줄(Queue, 큐)의 길이를 운영체제에 알려줄 수 있다.
if(listen(listener_d, 10) == -1)
error("blah");
큐의 길이를 10으로 listen() 함수를 호출하면 10개의 클라이언트가 한꺼번에 서버에 접속할 수 있다고 알려준다. 한꺼번에 응답하지는 못하더라도 클라이언트가 기다릴 수 있다. 11번째 클라이언트부터는 서버가 너무 바쁘다는 응답을 받게 된다.
접수 (Accept)
일단 포트에 바인딩하고 듣기 큐를 설정한 후에는 그저 기다리면 된다. 서버는 클라이언트가 연결해올 때까지 기다리는데 대부분의 시간을 보낸다. accept() 시스템 API를 호출하면 클라이언트가 서버에 접속할 때까지 기다리고, 클라이언트가 접속하면 대화하기 위해 사용할 수 있는 두 번째 소켓 디스크립터를 반환한다.
// client_addr은 방금 연결한 클라이언트에 대한 상제 정보를 저장한다.
struct sockaddr_storage client_addr;
unsigned int address_size = sizeof(client_addr);
int connect_d = accept(listener_d, (struct sockaddr *) &client_addr, &address_size);
if (connect_d == -1)
error("blah");
새로 만들어진 연결 디스크립터(connect_d)는 서버가 실제 대화하기 위해 사용할 소켓 디스크립터이다.
소켓은 평범한 데이터 스트림이 아니다.
파일이든 표준 입출력이든 일단 연결되면 fprintf()나 fscanf()와 같은 함수를 사용해 스트림과 이야기할 수 있었다. 그러나 소켓은 약간 다르다. 소켓은 양방향이다. 소켓은 입력과 출력 모두를 위해 사용된다. 따라서 소켓에 이야기하려면 다른 함수가 필요하다.
소켓에 데이터를 보내려면 fprintf()를 사용할 수 없고, 대신 send() 함수를 사용해야 한다.
send()와 같은 시스템 API를 호출하면 언제나 반환값을 검사해야 한다. 네트워크 에러는 흔히 발생하므로 서버가 꼭 이 문제를 처리해야 한다.
어느 포트를 사용해야 할까?
서버 애플리케이션이 사용할 포트 번호를 선택할 때 조심해야 한다. 수 많은 서버를 사용할 수 있으므로 다른 서버 프로그램이 일반적으로 사용하는 포트 번호를 사용하면 안된다. 시그윈과 대부분의 유닉스 계열 컴퓨터에서는 널리 사용되는 서버들이 사용하는 포트의 번호를 나열한 /etc/services 라는 파일을 갖고 있다. 포트를 선택할 때 다른 애플리케이션이 사용하는 번호를 사용하지 않게 주의해야 한다.
포트 번호는 0 ~ 65535 까지의 숫자다. 그리고 1024보다 작은 낮은 번호를 사용할 지 높은 번호를 사용할지 결정해야 한다. 대부분의 컴퓨터에서 1024보다 작은 번호의 포트는 관리자 권한을 가진 서버 프로그램만 사용한다. 낮은 포트 번호는 웹 서버나 이메일 서버와 같이 널리 알려진 서버들이 사용하도록 예약되어 있기 때문이다. 운영체제는 이 포트들을 관리자만 사용할 수 있게 제한하여 원치 않는 서비스를 일반 사용자가 실행하지 못하게 한다.
대부분의 경우 1024보다 큰 포트 번호를 사용하게 된다.
정리
프로토콜은 구조화된 통신
서버는 자신의 포트에 연결
클라이언트는 원 포트에 연결
클라이언트와 서버 모두 통신하기 위해 소켓을 사용
send()로 소켓에 데이터를 씀
recv()로 소켓에서 데이터를 받음
HTTP는 웹에서 사용하는 프로토콜
'Programming > C' 카테고리의 다른 글
[Programming/C] Mutex (상호배제, 상호배타) (0) | 2020.06.21 |
---|---|
[Programming/C] 스레드 (Thread) (0) | 2020.06.20 |
[Programming/C] 시그널 (Signal) (0) | 2020.06.20 |
[Programming/C] pipe() 함수 (0) | 2020.06.19 |
[Programming/C] 입출력의 리다이렉션 (0) | 2020.06.19 |