네트워크 프로그래밍과 소켓의 이해
soket()
소켓은 쉽게 말하면 전화기다. soket()이란 함수가 소켓을 생성 한다는 정도로만 이해하면 됩니다.
#include <sys/socket.h>
int socket(int domain, int type, int prtocol);
성공 시 파일 디스크립터, 실패시 -1 반환
bind()
함수호출을 통해서 소켓에 주소정보까지 할당한다고 이해하면 됩니다.
#include<sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
성공시 0, 실패시 -1 반환
listen()
소켓도 연결요청이 가능한 상태가 되어야 사용이 가능한데, 이 함수는 소켓을 연결요청이 가능한 상태가 되게합니다.
#includ<sys/socket.h>
int listen(int sockfd, int backlog);
성공시 0, 실패시 -1 반환
accept()
연결요청에 대한 수락을 의미한다. 누군가 데이터의 송수신을 위해 연결요청을 해오면, 다음 함수호출을 통해서 그 요청을 수락해야합니다.
#includ<sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addlen);
성공시 파일 디스크립터, 실패시 -1 반환
네트워크 프로그래밍에서 연결요청을 허용하는 소켓의 생성과정
1단계 : 소켓 생성 soket 함수호출
2단계 : IP주소와 PORT번호 할당 bind 함수호출
3단계 : 연결요청 가능상태로 변경 listen 함수호출
4단계 : 연결요청에 대한 수락 accept 함수호출
이순서를 머리 속에 항상 기억해 두고 이 흐름을 이해 해야만 소켓에 대한 밑그림이 그려진 셈이 됩니다.
Hello world 서버 프로그램의 구현
연결요청을 수락하는 기능의 프로그램을 가리켜 '서버(server)'라 한다. 그럼 앞서 설명한 함수의 호출과 정을 확인하기 위해서 연결요청 수릭시 "Hellow world!"라고 응답해주는 서버 프로그램을 작성해 보겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[]="Hello World!";
if(argc!=2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0); //soket 함수 호출 소켓 생성
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )
error_handling("bind() error"); //bind함수호출을 통해서 IP주소와 PORT번호를 할당하고 있다.
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_addr_size=sizeof(clnt_addr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size); //연결요청의 수락을 위한 accept함수를 호출하고 있다.
if(clnt_sock==-1) //연결요청이 없는 상태에서 이 함수가 호출되면,
error_handling("accept() error"); //연결요청이 있을 때 까지 함수는 반환하지 않는다.
write(clnt_sock, message, sizeof(message)); //42행을 지나서 이 문장이 실행되었다는 것은 연결요청이 있었따는 뜻이 된다.
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
위 예제를 컴파일 하고 실행하게 되면, 지금까지 언급했던 모든 과정을 거쳐서 연결요청을 기다리는 서버의 상태가 됩니다.
앞서 설명한 네가지 함수가 순서대로 호출되고 있는 것만 확인하면 됩니다. 다만 위의 예제에서 호출하는 write 함수는 잠시 후에 설명할 것입니다.
전화 거는 소켓의 구현
앞서 보인 서버 프로그램에서 생성한 소켓을 가르켜 '서버 소켓' 또는 '리스닝 소켓'이라 합니다. 반면에 이번에 소개할 소켓은 연결요청을 진행하는 '클라이언트 소켓'입니다. 클라이언트 소켓의 생성과정은 서버 소켓의 생성과정에 비해 상대적으로 간단합니다.
#includ <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addlen);
성공시 0, 실패시 -1반환
클라이언트 프로그램에서는 socket 함수호출을 통한 소켓의 생성과 conect 함수호출을 통한 서버로의 연결요청 과정만이 존재합니다. 때문에 서버 프로그램에 비해 상대적으로 간단합니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if(argc!=3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0); //소캣을 생성하고 있다. 소켓을 생성하는 순간에는 서버 소켓과 클라이언트 소켓으로 나뉘지 않는다.
//bind, listen 함수의 호출이 이어지면 서버소켓이 되는 것이고, connect함수의 호출로 이어지면
//클라이언트 소켓이 되는 것이다.
if(sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("connect() error!");
str_len=read(sock, message, sizeof(message)-1); //connect 함수호출을 통해서 서버프로그램에 연결을 요청하고 있다.
if(str_len==-1)
error_handling("read() error!");
printf("Message from server: %s \n", message);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
리눅스 기반에서 실행하기
현재까지 작성한 예제 코드는 리눅스 기반에서 컴파일 및 실행을 해야합니다. 따라서 리눅스 C컴파일러인 GCC컴파일러의 사용방법을 간단히 적어 보겠습니다.
gcc hello)server.c -o
hello_server.c 파일을 컴파일해서 hserver라는 이름의 실행파일을 만드는 문장이다.
위의 문장에서 -o는 실행파일의 이름을 지정하기 위한 옵션입니다. 때문에 컴파일이 완료되면 hserver 라는 이름의 파일이 생성된다. 그리고 이는 다음과 같이 실행하면 됩니다.
./hserver
현재 디렉터리에 있는 hserver라는 이름의 파일을 실행시키라는 의미이다.
실행결과
hello_server.c
제대로 실행이 되었다면, 위의 상태로 멈춰서 있게 됩니다. 이는 서버 프로그램에서 호출한 accept 함수가 반환하지 않았기 때문입니다.
실행결과
hello_client.c
이로써 클라이언트 프로그램의 메시지 수신을 확인하였습니다. 그리고 메시지 수신 후 서버 프로그램도, 클라이언트 프로그램도 종료함을 확인하였을 것입니다. 참고로 실행과정에서 입력한 127.0.0.1은 예제를 실행한 컴퓨터에서 실행된다면, 127.0.0.1을 대신해서 서버의 IP주소를 입력해야 합니다.
위에서 보인 서버 프로그램은 이어서 바로 재실행이 불가능합니다. 재실행을 위해서는 실행과정에서 입력한 PORT번호 9190을 모두 변경해서 실행해야 합니다.
리눅스 기반 파일 조작하기
리눅스에서의 소켓조작은 파일조작과 동일하게 간주되기 땜누에 파일에 애해서 자세히 알 필요가 있습니다. 리눅스는 소켓을 파일의 일종으로 구분한다. 따라서 파일 입출력 함수를 소켓 입출력에, 다시 말해서 네트워크 상에서의 데이터 송수신에 사용할 수 있습니다. 참고로 윈도우는 리눅스와 달리 파일과 소켓을 구분하고 있습니다. 때문에 별도의 송수신 함수를 참조해야 합니다.
저 수준 파일 입출력(Low-level File Access)과 파일 디스크립터(File Descriptor)
저 수준이란 "표준에 상관없이 운영체제가 독립적으로 제공하는~"의 의미로 받아들이면 됩니다. 즉, 이후에 설명하는 함수들은 리눅스에서 제공하는 함수이지, ANSI표준에서 정의한 함수가 아니라는 뜻입니다. 리눅스에서 제공하는 파일 입출력 함수를 사용하려면 파일 드스크립터에 대한 개념을 먼저 세워야 합니다.
여기서 말하는 파일 디스크립터란 시스템으로부터 할당 받은 파일 또는 소켓에 부여된 정수를 의미합니다. 참고로 c언어를 공부하면서 여러분이 입출력의 대상으로 여겨왔던, 표준 입출력 및 표준 에러에도 리눅스에서는 다음과 같이 파일 디스크립터를 할당하고 있습니다.
파일 디스크립터 | 대상 |
0 | 표준입력 : Standard Input |
1 | 표준출력: Standard Output |
2 | 표준에러: Standard Error |
일반적으로 파일과 소켓은 생성의 과정을 거처야 파일 디스크립터가 할당됩니다. 반면 위에서 보이는 세 가지 입출력 대상은 별도의 생성과정을 거치지 않아도 프로그램이 실행되면 자동으로 할당되는 파일 디스크립터들입니다.
파일 디스크립터란 운영체제가 만든 파일 또는 소켓의 지칭을 편히 하기 위해서 부여된 숫자에 지나지 않습니다.
파일열기
#include <sys/types.h>
#include <sys/stat.h>
#include <fcnt1.h>
int open(const char *path, int flag);
성송 시 파일 디스크립터, 실해시 -1 반환
path 파일 이름을 나타내는 문자열의 주소 값 전달.
flag 파일의 오픈 모드 정보 전달.
오픈모드 | 의미 |
O_CREAT | 필요하면 파일을 생성 |
O_TRUNC | 기존 데이터 전부 삭제 |
O_APPEND | 기존 데이터 보존하고, 뒤에 이어서 저장 |
O_RDONLY | 읽기 전용으로 파일 오픈 |
O_WRONLY | 쓰기 전용으로 파일 오픈 |
O_RDWR | 읽기, 쓰기 겸용으로 파일 오픈 |
파일닫기
여러분이 C언어를 공부하면서 알게 된 바와 같이, 파일은 사용 후 반드시 닫아줘야 합니다. 땜누에 파일을 닫을 때 호출하는 함수를 소개하고자 합니다.
#include<unsitd.h>
int close(int fd);
성공 시 0, 실패시 -1 반환
fd 닫고자 하는 또는 소켓의 파일 디스크립터 전달
파일 디스크립터를 인자로 전달하면 해당 파일은 닫히게(종료하게)됩니다. 그런데 여기서 중요한 사실은 위 함수는 파일뿐 아니라, 소켓을 닫을 때도 사용된다는 점 입니다. 이는 파일과 소켓을 구분하지 않는다는 리눅스 운영체제의 특성을 다시 한번 확인 할 수 있는 대목입니다.
파일에 데이터 쓰기
write 함수는 파일에 데이터를 출력(전송)하는 함수입니다. 리눅스에서는 파일과 소켓을 동일하게 취급하므로, 다른 컴퓨터에서 데이터를 전송할 때에도 이 함수를 사용할 수 있습니다. 참고로 앞서 보인 예제에도 "Hello world!"문자열 전달을 위해 사용 했었습니다.
#include <unistd.h>
ssize_t write(in fd, const void *buf, size_t nbytes);
성공시 전달한 바이트 수, 실패시 -1 반환
fd 데이터 전송대상을 나타내는 파일 디스크립터 전달.
buf 전송할 데이터가 저장된 버퍼의 주소값 전달.
nbytes 전송할 데이터의 바이트 수 전달.
파일에 저장된 데이터 읽기
write 함수
상대적인 기능인 기능을 제공하는 read 함수는 데이터를 입력(수신)하는 기능의 함수입니다.
리눅스에서는 파일과 소켓을 동일하게 취급하므로, 소켓을 통해서 다른 컴퓨터에 데이터를 전송할 때에도 이 함수를 사용할 수 있습니다.
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbyes);
성공시 전달한 바이트 수, 실패시 -1 반환
fd 데이터 전송대상을 나타내는 파일 디스크립터 전달.
buf 전송할 데이터가 저장된 버퍼의 주소 값 전달.
nbytes 전송할 데이터의 바이트 수 전달
함수 선언에서 보이는 size_t는 typedef 선언을 통해서 unsigned int로 정의되어 있습니다. 그리고 ssize_t의 경우 sizd_t 앞에 s가 하나 더 붙어 있는 형태인데, 이는 signed를 의미합니다. 즉, ssize_t는 typedef선언을 통해서 signed int로 정의되어 있습니다.
ssize_t, size_t등의 자료형을 '고전적인 자료형'이라 합니다. 이들은 일반적으로 sys/types.h 헤더 파일에 typedef 선언을 통해서 정의되어 있습니다. 지금은 int가 32비트라고 합니다. 16비트 시절에는 int가 16비트였습니다. 이렇듯 시스템의 차이, 또는 시간의 흐름에 따라서 자료형의 표현 방식이 달라지기 때문에 프로그램상에서 선택된 자료형의 변경이 요구되기도 합니다. 그런데 4바이트 자료형이 필요한 곳에 size_t 또는 ssize_t를 사용하면, 코드의 변경을 최소화 할 수 있습니다. size_t 그리고 ssize_t의 typedef 선언만 변경해서 컴파일을 하면 되기 때문입니다. 그래서 일반적으로 프로젝트를 진행 할 때는 기본 자료형 이름에 별도의 이름을 부여하기 위해 많은 양의 typedef 선언이 추가됩니다. 그리고 이렇게 프로그래머에 의해 정의되는 자료형 이름과의 구분을 위해서, 시스템(운영체젬)에서 정의하는 자료형의 이름에는 _t가 붙어있습니다.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void error_handling(char* message);
int main(void)
{
int fd;
char buf[]="Let's go!\n";
fd=open("data.txt", O_CREAT|O_WRONLY|O_TRUNC); //파일 오픈 모드가 O_CREAT, O_WRONLY, O_TRUNC의조합이니
if(fd==-1) //아무것도 저장되어 있지 않은 새로운 파일이 생성되어 쓰기만 가능하게 된다.
error_handling("open() error!"); //물론 이미 data.txt라는 이름의 파일이 존재한다면, 이 파일의 모든 데이터는 지워져 버린다.
printf("file descriptor: %d \n", fd);
//fd에 저장된 파일 디스크립터에 해당하는 파일에 buf에 저장된 데이터를 전송하고 있다.
if(write(fd, buf, sizeof(buf))==-1)
error_handling("write() error!");
close(fd);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
/*
root@com:/home/swyoon/tcpip# gcc low_open.c -o lopen
root@com:/home/swyoon/tcpip# ./lopen
file descriptor: 3
root@com:/home/swyoon/tcpip# cat data.txt
Let's go!
root@com:/home/swyoon/tcpip#
*/
실행결과
파일에 저장된 데이터 읽기
write 함수의 상대적인 기능을 제공하는 read함수는 데이터를 입력(수신)하는 기능의 함수이다.
#include <unised.h>
ssize_t read(int fd, void *buf, size_t nbytes);
fd 데이터 수신대상을 나타내는 파일 디스크립터 전달
buf 수신한 데이터를 저장할 버퍼의 주소 값 전달
nbytes 수신할 최대 바이트 수 전달.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 100
void error_handling(char* message);
int main(void)
{
int fd;
char buf[BUF_SIZE]; //파일 data. txt를 읽기 전용으로 연다.
fd=open("data.txt", O_RDONLY);
if( fd==-1)
error_handling("open() error!");
printf("file descriptor: %d \n" , fd);
if(read(fd, buf, sizeof(buf))==-1) //read함수를 이용해서 11행에 선언된 배열 buf에 읽어 들인 데이터를 저장하고 있다.
error_handling("read() error!");
printf("file data: %s", buf);
close(fd);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
/*
root@com:/home/swyoon/tcpip# gcc low_read.c -o lread
root@com:/home/swyoon/tcpip# ./lread
file descriptor: 3
file data: Let's go!
root@com:/home/swyoon/tcpip#
*/
실행결과
파일 디스크립터와 소켓
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
int main(void)
{
int fd1, fd2, fd3;
fd1=socket(PF_INET, SOCK_STREAM, 0); //하나의 파일과 두 개의 소켓을 생성하고 있다.
fd2=open("test.dat", O_CREAT|O_WRONLY|O_TRUNC);
fd3=socket(PF_INET, SOCK_DGRAM, 0); //여기까지 소켓 생성중
printf("file descriptor 1: %d\n", fd1); //앞서 생성한 파일 디스크립터의 정수 값을 출력하고 있다.
printf("file descriptor 2: %d\n", fd2);
printf("file descriptor 3: %d\n", fd3);
close(fd1);
close(fd2);
close(fd3);
return 0;
}
실행결과
'TCP&IP' 카테고리의 다른 글
TCP/IP 5장 기반서버/클라이언트 2 (0) | 2024.09.04 |
---|---|
TCP/IP 4장 기반서버/클라이언트1 (0) | 2024.09.03 |
TCP/IP 3장 주소체계와 데이터 정렬 (0) | 2024.09.03 |
TCP/IP 2.장 소켓의 프로토콜과 그에 따른 데이터 전송 특성 (0) | 2024.09.03 |
Tcp/ip 사전 학습 (0) | 2024.09.02 |
댓글