간단한 소켓 프로그램
- 서버와 클라이언트를 연결해봄으로써, 간단한 소켓 프로그래밍을 해볼 것이다.
- 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