네크워크 프로그래밍과 소켓에 대한 매우 간단한 이해
네크워크로 연결되어 있는 서로 다른 두 컴퓨터가 데이터를 주고받을 수 있도록 하는 것이 네크워크 프로그래밍!
데이터를 주고 받기 위해선 물리적인 연결이 필요하지만 이건 인터넷이라는 거대한 네크워크로 연결 되어 있으니
우리는 소프트웨어로 데이터 송수신만 신경 쓰면 된다.
하지만 이 역시도 운영체제에서 소켓(Socket)이라는 것을 제공하기 때문에 걱정할 필요가 없다. 소켓이라는걸 이용해서 데이터를 송수신 하기 때문에 네크워크 프로그래밍을 소켓 프로그래밍이라고도 한다.
소켓(Socket): 네크워크 망의 연결에 사용되는 도구 연결이라는 의미가 담겨있다.
예를 들자면 전화기도 전화망을 통해서 음성 데이터를 주고 받는데 소켓이나 다름이 없다.
하지만 전화기는 거는 것과 받는 것이 동시에 가능하지만 소켓은 거는 용도의 소켓 받는 용도의 소켓 방식에 차이가 있다.
-> 전화기의 장만에 비유되는 socket 함수호출의 이해를 위한 대화
질문: 전화를 받으려면 무엇이 필요한지?
답변: 당연히 전화기!
#include <sys/socket.h>
int socket(int domain, int type, int protocool);
-> 성공 시 파일 디스크립터, 실패 시 -1 반환
&& 디스크립터: 파일 디스크립터(File Descriptor)란 리눅스 혹은 유닉스 계열의 시스템에서 프로세스(process)가 파일(file)을 다룰 때 사용하는 개념으로, 프로세스에서 특정 파일에 접근할 때 사용하는 추상적인 값이다. 파일 디스크럽터는 일반적으로 0이 아닌 정수값을 갖는다.
일단 이 함수를 이해를 제대로 이해를 할려는 것 보단 socket이라는 함수가 소켓을 생성하는 구나 정도만 이해해도 된다.
소켓은 전화기와 달리 우리가 직접 전화번호 같은걸 할당해줘야한다.
-> 전화번호의 부여에 비유되는 bind 함수호출의 이해를 위한 대화
질문: 당신의 전화번호는 어떻게 되는지?
답변: 나의 전화번호는 1230-1234 이다
#include <sys/soket.h>
int bind(int sockfd, struck sockaddr *myaddr, socklen_t addrlen);
-> 성공시 0, 실패시 -1 반환
bind 함수호출을 통해 소켓에 주소정보를 할당 했으면 이제 전화기를 케이블에 연결시켜서 전화 걸려오기만 기다리면 된다.
전화기의 케이블 연결에 비유되는 listen 함수호출의 이해를 위한 대화
질문: 가설이 끝났으니 전화기를 연결하면 되는지?
답변: 연결을 끝마쳤으면 이제 걸려오는 전화만 기다리면 됩니다.
전화기가 전화 케이블에 연결되는 순간 전화를 받을 수 있는 상태가 되기 때문에 소켓도 마찬가지로 연결요청이 가능한 상태가 되어야한다.
#include <sys/socket.h>
int listen(int sockfd, int backlog);
-> 성공시 0 실패시 -1 반환
listen 함수를 통해 전화 케이블에 연결되는 것처럼 소켓을 연결 시킬 수 있다. 이제 기다리면된다 누구? (클라이언트)
그리고 나서 전화가 오면 수화기를 들어야한다.
수화기를 드는 것에 비유되는 accept 함수 호출의 이해를 위한 대화
질문: 전화벨이 울립니다 어떻게 해야하나?
답변: 당연히 전화를 받으면 됩니다.
수화기를 들었다는 건 연결 요청에 대한 수락을 의미. 소켓도 마찬가지로 연결요청을 해오면 함수 호출을 통해 그 요청을 수락해야한다.
#include <sys/socket.h>
int accept(int sockfd, struck sockaddr *addr, socklen_t *addrlen);
-> 성공 시 파일 디스크립터, 실패 시 -1 반환
지금까지 설명한 내용을 정리 해보면 네트워크 프로그래밍에서 연결요청을 허용하는 소켓의 생성과정은 다음과 같다.
- 1단계 소켓생성 -> socket 함수 호출
- 2단계 IP주소와 PORT번호 할당 -> bind 함수 호출
- 3단계 연결요청 가능한 상태로 변경 -> listen 함수 호출
- 4단계 연결요청에 대한 수락 -> accept 함수 호출
이 순서들을 머리 속에 기억해 둬야한다.
Hello world 서버 프로그램의 구현
연결요청을 수락하는 기능의 프로그램을 가리켜 서버server)라고 한다.
#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);
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");
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);
if(clnt_sock==-1)
error_handling("accept() error");
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
-> socket 함수 호출을 통해서 소켓을 생성하고 있다.
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");
-> listen 함수를 호출 하고 있다. 이로써 소켓은 연결요청을 받아들일 수 있는 상태가 된다.
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
if(clnt_sock==-1)
error_handling("accept() error");
-> 연결요청의 수락을 위한 accept 함수를 호출하고 있다. 연결요청이 없는 상태에서 이 함수가 호출되면, 연결요청이 있을 떄까지 함수는 반환하지 않는다.
전화 거는 소켓의 구현
클라이언트 쪽에서 전화를거는것 즉 연결요청을 진행하는건 클라이언트 소켓이다.
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
성공시 0 실패시 -1 반환
클라이언트 프로그램에선 socket 함수호출을 통한 소켓의 생성과 connect 함수호출을 통한 서버로의 연결요청 과정만이 존재
밑의 예제는 서버 프로그램과 함께 실행새서 문자열 데이터를 주고받는것을 확인 할 수 있다.
#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);
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);
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);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
-> 소켓 생성 (소켓을 생성하는 순간에는 서버 소켓과 클라이언트 소켓으로 나눠지지 않는다. 즉 bind, listen 함수의 호출이 이어지면 서버 소켓 connect 함수가 나오면 클라이언트 소켓이다)
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("connect() error!");
-> connect 함수호출을 통해 서버 프로그램에 연결 요청중이다.
리눅스 기반에서 실행하기
gcc hello_server.c -o hserver
-> hello_server.c 파일을 컴파일 해서 hserver라는 이름의 실행파일을 만드는 문장
./hserver
-> 현재 디렉터리에 있는 hserver라는 이름의 파일을 실행시키라는 의미
저 수준 파일 입출력(Low-level File Access)과 파일 디스크립터(File Descriptor)
저 수준이란 표준에 상관없이 운영체제가 독립적으로 제공하는~ 의 의미.
리눅스에선 다음과 같이 파일 디스크립터를 할당하고 있다.
파일 디스크립터 | 대상 |
0 | 표준입력: Standard Input |
1 | 표준출력: Standard Output |
2 | 표준에러: Standard Error |
파일 열기
데이터를 읽거나 쓰기 위해 파일을 열 때 사용하는 함수
이 함수는 두 개 의 인자를 전달 받는다 첫 번째 인자로는 파일의 이름 및 경로 정보, 두 번째 인자로는 파일의 오픈 모드(파일의 특성 정보)
#include <sys/types.h>
#include <sys/stat.h>
#include <fcn1.h>
int open(const char *path, int flag);
성공시 파일 디스크립터, 실패 시 -1 반환
파일 닫기
#include <unistd.h>
int close(int fd);
성공시 0 실패시 -1
파일에 데이터 쓰기
write 함수는 파일데 데이터를 출력하는 함수
#include <unistd.h>
ssize_t write(int fd, const void * buf, size_t nbytes);
fd : 데이터 전송대상을 나타내는 파일 디스크립터 전달
buf: 전송할 데이터가 저장된 버퍼의 주소 값 전달.
nbytes: 전송할 데이터의 바이트 수 전달
지금까지 설명한 함수의 활용
#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);
if(fd==-1)
error_handling("open() error!");
printf("file descriptor: %d \n", fd);
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#
*/
fd=open("data.txt", O_CREAT|O_WRONLY|O_TRUNC);
-> 파일 오픈 모드가 O_CREAT, O_WRONLY, O_TRUNC의 조합이니 아무것도 저장되 어 있지 않은 새로운 파일이 생성되어
쓰기만 가능하게 된다.
if(write(fd, buf, sizeof(buf))==-1)
error_handling("write() error!");
-> fd에 저장된 파일 디스크립터에 해당되는 파일에 buf에 저장된 데이터를 전송.
실행 결과:
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 <unistd.h>
ssize_t read(int fd, void *buf, size_t nbyres);
fd: 데이터 수신대상을 나타내는 파일 디스크립터 전달
buf: 수신한 데이터를 저장할 버퍼의 주소 값 전달
nbytes: 수신할 최대 바이트 수 전달.
파일 디스크립터와 소켓
파일도 생성하고 소켓도 생성한다음 반한된 파일 디스크립터의 값을 정수형태로 비교
#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;
}
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);
-> 앞서 생성한 파일 디스크립터의 정수 값을 출력.
'스터디 > TCP 와 IP' 카테고리의 다른 글
6. 소켓의 우아한 연결 종료 (0) | 2024.09.03 |
---|---|
5. TCP 기반 서버/ 클라이언트 2 (0) | 2024.09.03 |
4. TCP 기반 서버/ 클라이언트 1 (0) | 2024.09.03 |
3. 주소체계와 데이터 정렬 (1) | 2024.09.03 |
2. 소켓의 타입과 프로토콜의 설정 (0) | 2024.09.02 |