네트워크 프로그래밍과 소켓에 대한 매우 간단한 이해
네트워크로 연결되어 있는 서로 다른 두 컴퓨터가 데이터를 주고받을 수 있도록 하는 것이 네트워크 프로그래밍입니다.
네트워크 프로그래밍은 소켓 프로그래밍이라고도 합니다.
그런데 왜 소켓이라는 단어를 사용했을까요?
우리는 전력망으로부터 전기를 공급받기 위해서 소켓을 꽂습니다. 즉, 가전기기의 소켓은 전력망으로의 연결에 사용됩니다. 마찬가지로 멀리 떨어져있는 컴퓨터와 데이터를 송수신하려면 인터넷이라는 네트워크 망에 연결해야 합니다. 그리고 프로그래밍의 소켓은 네트워크 망의 연결에 사용되는 도구입니다. 이렇듯 연결이라는 의미가 담겨있어서 소켓이라는 표현을 사용합니다. 그리고 그 의미를 조금 더 확장해서 소켓은 네트워크를 통한 두 컴퓨터의 연결을 의미하기도 합니다.
소켓도 크게 두 가지로 나뉘는데, 먼저 다뤄볼 TCP 소켓은 전화기에 비유할 수 있습니다. 전화기는 거는 것과 받는 것이 동시에 가능하지만, 소켓은 거는 용도의 소켓을 완성하는 방식과 받는 용도의 소켓을 완성하는 방식에 차이가 있습니다.
집에 전화를 놓으려면 먼저 전화기를 구입해야 합니다.
먼저 전화기를 구입해보겠습니다.
다음은 전화기에 해당하는 소켓을 생성하는 함수입니다.
// 성공 시 파일 디스크립터, 실패 시 -1 반환
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
그런데 전화기만 구입한다고 끝나는 게 아니라 전화번호를 부여받아야 합니다.
소켓도 마찬가지입니다. 다음 함수를 사용해서 앞서 생성한 소켓에 IP와 포트번호라는 소켓의 주소정보에 해당하는 것을 할당해야 합니다.
// 성공 시 0, 실패 시 -1 반환
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);
bind 함수호출을 통해 소켓에 주소정보까지 할당했으니, 이제 전화를 받기 위한 모든 준비가 끝났습니다. 이제 전화기를 전화 케이블에 연결해 전화가 걸려오기만을 기다리면 됩니다. 전화기가 전화 케이블에 연결되는 순간 전화를 받을 수 있는 상태가 됩니다. 소켓도 연결요청이 가능한 상태가 되어야 합니다. 다음 함수는 소켓이 연결요청이 가능한 상태가 되게 해줍니다.
// 성공 시 0, 실패 시 -1 반환
#include <sys/socket.h>
int listen(int sockfd, int backlog);
이제 전화기가 전화 케이블에 연결됐으니 전화벨이 울릴 것이고, 전화벨이 울리면 통화를 위해서 수화기를 들어야 합니다. 수화기를 들었다는 것은 연결요청에 대한 수락을 의미합니다. 이는 소켓도 마찬가지이고, 누군가 데이터의 송수신을 위해 연결요청을 해오면 다음 함수호출을 통해서 그 요청을 수락해야 합니다.
// 성공 시 파일 디스크립터, 실패 시 -1 반환
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
정리해보면, 네트워크 프로그래밍에서 연결요청을 허용하는 소켓의 생성과정을 다음과 같이 정리할 수 있습니다.
- 소켓 생성: socket 함수호출
- IP주소와 PORT 번호 할당: bind 함수호출
- 연결요청 가능상태로 변경: listen 함수호출
- 연결요청에 대한 수락: accept 함수호출
"Hello world!" 서버 프로그램을 구현
연결요청을 수락하는 기능의 프로그램을 가리켜 서버라고 합니다. 앞서 설명한 함수의 호출과정을 확인하기 위해서 연결요청 수락 시 "Hello 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, 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]));
// bind로 IP 주소와 PORT 번호 할당
if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
// listen 함수를 호출해 소켓이 연결요청을 받아들일 수 있게 한다.
if (listen(serv_sock, 5) == 1)
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
// 연결요청의 수락을 위한 accept 함수 호출,
// 연결요청이 없는 상태에서 이 함수가 호출되면, 연결 요청이 있을 때까지 기다린다.
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);
}
전화 거는 소켓의 구현
앞서 보인 서버 프로그램에서 생성한 소켓을 가리켜 서버 소켓 또는 리스닝 소켓이라고 합니다. 이번에 소개할 소켓은 연결요청을 생성하는 클라이언트 소켓입니다.
아래는 연결을 요청하는 기능의 함수입니다.
// 성공 시 0, 실패 시 -1 반환
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr* serv_addr, socklen_t addrlen);
이를 이용하여 앞서 보인 서버 프로그램에 연결을 요청하는 클라이언트 프로그램을 구현해보겠습니다.
#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]));
// connect 함수호출로 서버 프로그램에 연결 요청
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);
}
이제 실행결과를 살펴보겠습니다.
'네트워크 프로그래밍 > C' 카테고리의 다른 글
[네트워크 프로그래밍/C] 네트워크 바이트 순서와 인터넷 주소 변환 (0) | 2022.02.21 |
---|---|
[네트워크 프로그래밍/C] 주소정보의 표현 (0) | 2022.02.17 |
[네트워크 프로그래밍/C] 소켓에 할당되는 IP주소와 PORT 번호 (0) | 2022.02.17 |
[네트워크 프로그래밍/C] 소켓의 프로토콜과 그에 따른 데이터 전송 특성 (0) | 2022.02.17 |
[네트워크 프로그래밍/C] 리눅스 기반 파일 조작하기 (0) | 2022.02.16 |