간단한 소켓 프로그램
- 서버와 클라이언트를 연결해봄으로써, 간단한 소켓 프로그래밍을 해볼 것이다.
- A와 B가 연결된 상태로 통신을 하려면 둘 중의 하나가 연결 요청을 해야한다. 그리고 나머지 한쪽은 상대방의 연결 요청을 처리할 준비를 해야한다.
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
-
위의 함수는 연결 요청을 처리하기 위한 함수이다.
-
listen()함수는 해당 소켓을 듣기 상태로 만든다.listen함수가 호출되면 해당 소켓은 상대방의 연결 요청을 받을 준비를 한다. -
프로그램의 흐름은 다음 라인으로 넘어가면 운영체제에게 해당 소켓을 통해 연결 요청이 들어올 경우, 연결 요청 정보를 저장해달라고 부탁한다.
-
backlog는 저장하고 있을 연결 요청의 최대 수를 뜻한다.
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socket_t addrlen);
-
accept()함수가 호출되는 순간, 커널은listen함수에 의해 생성된 대기열에 연결 요청이 있었는지를 확인한다. 연결 요청이 없으면accept함수는 연결 요청이 발생할 때까지 프로그램의 제어권을 가진 상태로 대기한다. -
연결 요청이 있으면 가장 먼저 연결 요청을 한 프로세스와 통신하기 위한 소켓의 파일 디스크립터를 반환한다.
-
여기서 주의해야할 것은
listen함수를 통해 듣기 모드에 돌입한 소켓은 연결 요청을 접수하는 역할을 하는 듣기 소켓이 되고 accept 함수의 결과 반환된 소켓은 실제 데이터 전송에 사용되는 연결 소켓이 된다는 것이다.
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 위의 함수를 통해서,
listen()함수 호출로 대기하고 있는 프로세스에 연결 요청을 할 수 있다.

-
데이터 전송을 위한 연결은
connect()함수의 인자로 전달된 소켓과accept()함수 호출에 의해 듣기 소켓이 된 소켓은 연결 요청을 처리하는 일만을 담당한다. -
연결이 완료된 이후에는 일반 파일과 같이 입출력 함수 (
read,write)를 사용할 수 있다.
서버 프로그램
-
간단한 예제를 통해서, 다룬 함수들의 실제 사용에 대해서 알아볼 것이다.
-
서버 프로그램의 기능은 다음과 같다.
클라이언트쪽으로부터 “How old are you?” 라는 문자열을 전송받은 후에 이것을 전송한다. 클라이언트쪽으로 “I am 20 years old” 라는 문자열을 전송한 후에 화면에 출력한다.
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/socket.h>
#define PORT 9001
int main() {
int srv_sd, client_sd;
struct sockaddr_in srv_addr, client_addr;
int client_addr_len, read_len;
char read_buff[BUFSIZ];
char write_buff[BUFSIZ] = "I am 20 years old.";
srv_sd = socket(PF_INET, SOCK_STREAM, 0);
if (srv_sd == -1) {
printf("socket creation error");
return -1;
}
printf("==== server program ====\n");
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
srv_addr.sin_port = htons(PORT);
if (bind(srv_sd, (struct sockaddr *) &srv_addr, sizeof(srv_addr)) == -1) {
printf("bind error");
return -1;
}
if (listen(srv_sd, 5) == -1) {
printf("listen error");
return -1;
}
client_addr_len = sizeof(client_addr);
client_sd = accept(srv_sd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_sd == -1) {
printf("accept error");
return -1;
}
write(client_sd, write_buff, sizeof(write_buff));
printf("server: %s\n", write_buff);
read_len = read(client_sd, read_buff, sizeof(read_buff));
if (read_len == -1) {
printf("read error");
return -1;
}
read_buff[read_len] = '\0';
printf("client: %s\n", read_buff);
close(client_sd);
close(srv_sd);
return 0;
}
클라이언트 프로그램
같은 호스트 안에 위치한 서버 프로그램에
TCP 9001번 포트로 연결을 시도한다.연결 후 서버 프로그램으로 ‘How old are you?’ 라는 문자열을 전송한 후에 화면에 출력한다. 서버로부터 ‘I am 20 years old.’ 라는 문자열을 전송 받은 후에 화면에 출력한다.
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define PORT 9001
int main() {
int client_sd;
struct sockaddr_in client_addr;
int client_addr_len, read_len;
char write_buff[BUFSIZ] = "How old are you?";
char read_buff[BUFSIZ];
client_sd = socket(PF_INET, SOCK_STREAM, 0);
if (client_sd == -1) {
printf("socket creation error");
return -1;
}
printf("==== client program ====\n");
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
client_addr.sin_port = htons(PORT);
if (connect(client_sd, (struct sockaddr *) &client_addr, sizeof(client_addr)) == -1) {
printf("connection error");
close(client_sd);
return -1;
}
write(client_sd, write_buff, sizeof(write_buff));
printf("client: %s\n", write_buff);
read_len = read(client_sd, read_buff, sizeof(read_buff));
if (read_len == -1) {
printf("read error");
return -1;
}
read_buff[read_len] = '\0';
printf("server: %s\n", read_buff);
close(client_sd);
return 0;
}

-
서버를 실행한 직후
netstat명령을 통해서,listen상태에 있는 연결을 조회한 결과입니다. -
서버는 클라이언트의 연결 요청을 기다리는
listen상태에 머물러 있다는 것을 확인할 수 있습니다.
에러 처리
-
소켓 객체를 관리하는 것은 커널이고, 따라서 소켓 관련 함수들은 대부분 시스템 콜과 관련이 있다.
-
이러한 함수들은 경우에 따라 실패할 수 있으므로, 실패의 종류에 따라서 필요한 루틴을 실행하는 것이 중요하다.
-
리눅스에서는 함수 호출 후에 발생한 에러를 저장하는
errno라는 전역 변수를 제공한다.errno는errno.h파일을include해서 사용할 수 있다. 또한, 에러 종류에 대한 매크로도errno.h에 정의되어 있다. -
예를 들어서,
socket함수의 인자 중 프로토콜 패밀리가 잘못되면socket함수는-1을 리턴하면 종료하고, 종료하면서 자신이 어떤 에러에 의해서 종료되었는지errno변수에 저장하는데 그 값은EINVAL로 미리 정의되어 있다. -
따라서 이 경우 다른 작업을 추가할고 싶으면 코드를 다음과 같이 구성하면 된다.
client_sd = socket(PF_INET, SOCK_STREAM, 0);
if (client_sd == -1) {
if (errno == EINVAL) {
printf("protocol family error");
return -1;
}
printf("socket creation error");
return -1;
}
- 화면에 에러 메시지에 대략적인 내용을 출력하는 함수를 만들고 실패가 발생하는 모든 함수에서 에러 처리 루틴을 삽입하는 것이 좋다.
#include <stdio.h>
void perror(const char *msg);
int foo(int domain, int type, int protocol) {
int res;
res = socket(domain, type, protocol);
if (res == -1) {
perror("socket");
}
return res;
}
참고 문헌
>> Home