[LINUX] 소켓 프로그래밍

[LINUX] 소켓 프로그래밍

http://biscuit.cafe24.com/moniwiki/wiki.php/socket%C7%C1%B7%CE%B1%D7%B7%A1%B9%D6%B1%E2%BA%BB

  1. socket()
  2. bind()
  3. listen()
  4. read() / write()

1. socket() #

socket() 함수는 통신을 위한 하나의 포인트(= endpoint)를 만든다. 통신을 위한 매개체(= fd)를 만든다.

" socket() 함수는, 통신을 위한 끝점 한개를 만든다 (무슨 소리냐! :@). 워낙 여러가지 통신에 사용할 수 있는 함수이지만, 이 글에서는 TCP/IP 통신을 위한 부분에 국한해서, 뭔가 통신을 하기위한 매개체(이것을 fd, file descriptor 라고 한다)를 한개 생성하는데 쓴다. “

#include <sys/types.h>
#include <sys/socket.h>

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

if ((fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
    fprintf(stderr, "socket() error\n");
    exit(-1);
}

socket() 함수의 파라미터 domain과 type은 TCP/IP의 IP와 TCP를 의미한다. 참고로 UDP통신을 위해서는 type에 SOCK_DGRAM 을 쓴다. 프로그래밍에서 Stream이라는 단어는 연속적으로 주르륵 붙어있는 자료구조를 가리키는데, 통신을 하다보면 항상 먼저보낸 데이터가 먼저 도착하지는 않는데, 그럼에도 불구하고 데이터의 순서를 TCP라는 통신규약이 바로잡아주기 때문에 Stream이다. 물론 UDP는 그런것이 보장되지 않는다.

socket() 함수는 리턴 값으로 fd 값을 반환한다.

” socket()함수는 단지 정수값 한개를 돌려주는데 (보통은 4이다. 왜냐면 stdin, stdout, stderr 가 각각 1개씩 차지하고 있어서 4번째 fd 가 된다) 앞으로의 모든 조작은 이 정수값을 통해서 하게 된다 (다른 함수를 호출할때마다 이 값을 넘겨줘야 한다). “


2. bind() #

#include <sys/types.h>
#include <sys/socket.h>

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

bind() 함수는 앞서 만든 fd에 소켓의 설정을 연결시켜준다.

” bind()함수는 Socket과 Name을 연결시켜(binding) 주는 함수이다(역시 무슨 소리냐! :@). socket의 성격을 설명하는 sockaddr라는 구조체의 값들을 앞서 만든 fd 값에 연결시켜준다.

int port_listen = 8000;
struct sockaddr_in server_addr;

...

bzero((char *)&server_addr, sizeof(server_addr));
server_addr.sin_family      = PF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port        = htons(port_listen);

if((bind(fd, (struct sockaddr *)&server_addr, sizeof(server_addr))) < 0 ){
    fprintf(stderr, "bind() error\n");
    exit(-1);
}

sockaddr_in 을 보면 알 수 있듯이, 소켓에 대한 속성 정보(= 설정)을 위한 구조체다.

위 예제는 INADDR_ANY(= 어디에서나 접속 가능한)8000번 포트를 설정해주고 있다.

” sockaddr_in 이라는 구조체가 등장하는데, bind에서 연결할 소켓속성을 담을 구조체이다. 예제에서는 ‘어디에서나 접속가능한’(INADDR_ANY) 8000 번 포트를 listen 하도록 설정하고 있다. bind() 에서 에러가 나는 경우는 첫째 이미 다른프로그램이 이 포트를 이용하는 경우, 둘째 당신의 권한이 부족한 경우이다. 슈퍼유저(root)만이 1024번 아래 포트를 쓸 수 있다.


3. listen() #

소켓에 대한 생성,설정 이후 클라이언트의 요청을 받기 위한 명령어다.

” listen 함수는, 이제 클라이언트들을 위해 귀를 기울여라!하고 최종명령을 내리는 것이다. 여태까지를 정리하면, (1) socket을 생성 (2) sockaddr_in 구조체를 만들어 대충 채우고 (3) bind() 로 1과2 두개를 묶고, (4) listen 하는 순서이다. 이렇게 해서 서버는 연결을 받아들일 준비가 된다. 실제로 클라이언트가 연결을 하면 서버는 accept 함수로 연결을 받아들이고, 적당한 때에 해당 소켓에서 데이터를 읽고 또 적당한 때에 쓰면 되는 것.

” 바로 이 적당한 때를 구현하는 방법이 사실 소켓 프로그래밍의 핵심이다!! “

<br.

4. read() / write() #

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

소켓의 데이터를 읽거나, 소켓에 데이터를 쓸 때 사용한다. (= 파일에 데이터를 읽고, 쓰는 것과 동일하다.)

” read(), write()는 사실 소켓과는 별로 상관이 없지만, unix의 모든 장치는 FILE이다라는 철학에 의해 소켓에 데이터를 쓰거나 읽을때 사용한다. 파일에 쓰거나 읽는거와 다르지 않다. 다만, 소켓연결이 끊기거나 에러가 발생했을 경우 부수적인 처리들이 필요하다. “

소켓은 별 게 아니고, 파일이다. :star:


#define MAX_BUF 1024

int nread;
char buf[MAX_BUF];

nread = read(fd, buf, MAX_BUF);

if(nread <= 0) {
    if(nread < 0){
        // some error handling routine
    }
    close(fd);
}

fd가 가리키는 곳으로부터 MAX_BUF 만큼 데이터를 읽는다. read()의 리턴 값이 0인 경우 EOF 를 의미하고, -1인 경우 에러를 의미한다. (0, -1일 때 소켓 종료 처리에 신경써야 한다.)

write()도 read()와 유사하다.

” write()의 경우는 좀더 신경써야 할 일이 많다. 그 중 가장크게 신경을 써야 하는 것은 WOULDBLOCK이다. 운영체제 내부적으로 통신을 위한 버퍼를 가지고 있는데, 이 버퍼보다 큰 데이터를 보내려고 하거나 이미 가득 차 있는데 무언가를 더 보내려고 하는 경우에 WOULDBLOCK이 발생하게 된다.이런일은 일시적인 통신지체 (흔히 ‘랙’) 상황에서 일반적으로 발생하는데 그냥 연결을 끊어버릴 수도 없고, 버퍼가 비워질때까지 무작정 기다릴 수도 없는 노릇. 그래서 Send Buffering 기법을 쓴다. 보내야 할 데이터가 발생하면 바로 전송하지 않고 어플리케이션 버퍼에 넣어두며, 적당한 시점에서 실제 전송을 시도하도록 하는 방법이다. “