1. 소켓 프로그래밍 시작하기
1.1 hello, world를 출력하는 소켓 프로그램의 구현
일반적으로 소켓 프로그램은 서비스를 요청하는 클라이언트 측과 클라이언트로부터의 요청을 받아 서비스하는 서버픅, 이렇게 두 곳에 상주하는 프로그램으로 구성된다.
즉, 클라이언트 프로그램이 네트워크 상에서 통신 채널을 통해 서버측에 연결되면 서버 프로그램은 즉시 문자열을 클라이언트에 전송하고, 클라이언트 프로그램은 전송받은 문자열을 화면에 출력한다.
네트워크 상에서 문자열을 전송하려면 소켓과 같은 네트워크 장치가 필요하다. 구현할 소켓ㅎ의 기능을 정리하면 다음과 같다.
a. 연결 요청 : 클라이언트 프로그램은 소켓 API함수를 호출하여 서버 프로그램에 연결을 요청한다.
b. 문자열 요청 : 연결 요청을 받은 서버 프로그램은 클라리언트 프로그램과 연결되자마자 문자열을 클라이언트에 전송한다.
c. 화면 출력 : 클라이언트 프로그램은 전송받은 문자열을 자신의 화면에 출력한다.
서버 프로그램
서버 프로그램은 전화기에 대응하는 네트워크 연결 장치인 소켓을 생성하여 포트를 할당하고, 해당 포트
롷 클라이언트로부터의 연결 요청이 들어오면 즉시 연결한다. 그런다음 클라이언트 측에 전송하도록 구현한다.
다음은 서버 프로그램의 예제이다.
#include
#include
#define PORT 9000
char buffer[BUFSIZ] = "hello world";
int main()
{
int c_socket, s_socket;
struct sockaddr_in s_addr,c_addr;
int len;
int n;
// 1. 소켓을 생성한다.
// 이 경우에는 TCP통신에 해당한다.
s_socket = socket(PF_INET, SOCK_STREAM, 0);
// 2. 연결 요청을 수신할 주소 설정
// INADDR_ANY로 할당하면 어느 아이피도 허용한다는 말이다.
// 이것을 설정하는 이유는 한 컴퓨터에 여러 장의 랜카드가
// 장착되어 있어서 여러개의 아이피 주소가 할당되고 서버 응용 프로그램은
// 모든 요청을 처리해야하기 때문이다.
// 포트 번호를 설정하면 특정 내선번호라고 생각하면 된다.
memset(&s_addr, 0, sizeof(s_addr));
s_addr.sin_addr.s_addr = htonl(INADDR_ANY);
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(PORT);
// 3. 소켓을 포트에 연결
// 소켓을 만들고 나면 그 소켓을 주소와 연결해야한다.
// 이 예제에서는 포트번호가 9000번이다.
if(bind(s_socket, (struct sockaddr *)&s_addr, sizeof(s_addr)) == -1)
{
printf("Can not Bind\n");
return -1;
}
// 4. 커널에 개통을 요청
// 주소와 소켓을 연결한 이후 그 소켓을 이용하여 커널에
// 개통 요청을 해야한다. 즉, 운영체제상에서 발생하는 과정이다.
if(listen(s_socket, 5) == -1)
{
printf("listen Fail\n");
return -1;
}
// 5. 무한 반복
while(1)
{
// 6. 클라이언트로부터의 연결 요청을 수신
// s_socket으로 클라이언트의 요청을 받으면 연결소켓(c_socket)
// 을 반환한다.
len = sizeof(c_addr);
c_socket = accept(s_socket, (struct sockaddr *)&c_addr, &len);
// 7. 클라이언트에게 서비스를 제공
// 이제 연결되었으므로 연결 소켓에 데이터를 보낸다.
n = strlen(buffer);
write(c_socket, buffer, n);
// 8. 통신이 완료되면 클라이언트에 연결된 연결 소켓을 끊는다.
// 그리고 클라이언트로부터 새로운 연결 요청이 오기를 듣기 소켓(s_socket)
// 을 이용하여 기다린다.
close(c_socket);
}
// 9. 서비스의 완전 종료를 의미한다.
close(s_socket);
}
클라이언트로부터 연결 요청을 받아들이기 위해 socket함수로 소켓을 생성하고, 연결할 주소(아이피, 소켓)을 준비한다. bind함수로 소켓을 주소에 연결한다. listen함수로 커널에 개통을 요청하고, accept함수로 요청을 받아들이는 모든 과정은 의례적으로 해야 하는 작업이다.
서버가 서비스 하는 것을 보려면 클라이언트 프로그램도 실행해야만 한다. 그렇게 해야만 서버 프로그램은 write함수로 데이터를 보낼 것이다.
이것을 수행해보기 위해서는 서버를 열고 다른 쪽에서 서버의 열린 포트를 이용하여 접속해야한다.
여기서 주목할 점은 포트번호이다. 만약 다른 프로세스가 해당 포트번호를 사용하고 있다면 이 동작은 에러가 뜰것이고 그 에러는 bind함수의 에러이다.
응용 프로그램은 1번에서 65535번까지의 포트에 소켓을 연결하여 사용한다. 10254번 까지의 포트는 FTP, telnet과 같은 다른 서비스에 대부분 예약되어 있다. 그러므로 1024번 이후로 사용하는 것이 좋다.
%% 만약 다른 프로그램이 해당 포트를 점유하고 있지 않으며, 코드상에서의 오류도 없는데 접속이 되지 않는다면, 방화벽을 확인해보자 텔넷에 대한 방화벽이 동작하고 있을 수 있다.
클라이언트 프로그램
이번에는 클라이언트 프로그램을 구현해서 서버에 연결하고 전송 받은 문자열을 화면에 출력 해본다.
서버에 연결하려면 먼저 소켓을 연결하고 사전에 서버의 아이피 주소와 서버의 응용프로그램이 연결된 포트번호를 알아야 한다.
다음 코드는 클라이언트 프로그램을 구현한 코드이다. 서버측과 연결하여 자료를 가져오기 위해서 socket, connect, read등의 소켓 관련 API함수를 사용하고 있다.
#include<stdio.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#define PORT 9000
#define IPADDR "127.0.0.1"
int main()
{
Int c_socket;
Struct sockaddr_in c_addr;
socklen_t len;
int n;
char rcvBuffer[BUFSIZ];
c_socket = socket(PF_INET, SOCK_STREAM, 0);
memset(&c_addr, 0, sizeof(c_addr));
c_addr.sin_addr.s_addr = inet_addr(IPADDR);
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(PORT);
if(connect(c_socket,(struct sockaddr *) &c_addr, sizeof(c_addr))==-1)
{
printf("Can't not connect\n");
close(c_socket);
return -1;
}
if((n = read(c_socket, rcvBuffer, sizeof(rcvBuffer))) < 0)
{
Return -1;
}
rcvBuffer[n] = '\0';
printf("receive Data : %s\n", rcvBuffer);
close(c_socket);
return 0;
}
여기서 주목해야 할 점은 커널의 업데이트로 인해서 몇몇 레퍼런스가 변했다는 점이다. 예를 들어 inet_addr함수가 arpa/inet.h헤더로 들어갔다던지 len의 타입이 int에서 socklen_t로 변했다던지 하는 것들이다.