Datagram Socket 통신
Stream Socket은 연결 지향형이고 Datagram은 비연결형의 Socket 통신 방법입니다. 신뢰성은 그만큼 떨어지지만 단순하고 빠른 전송을 위한 것으로 UDP 프로토콜을 사용합니다. Stream Socket과는 다르게 Datagram 소켓을 사용하여 간단히 Server - Client 통신 소스코드를 짜보도록 합시다. Stream Socket은 서버와 클라이언트 사이에 연결을 맺어주느라 소스 코드의 길이가 길지요. 지금 볼 datagram socket은 이러한 연결(Connection) 과정이 생략이 됩니다.
그러니까 Stream Socket과는 다르게 listen, accept를 datagram socket에서 사용할 필요가 없어졌습니다. 그래서 다행이라고 할까요? Stream socket보다는 코드의 길이가 짧습니다.
서버 코드
아래에서 구현한 소스코드는 client의 메시지를 받아서 그대로 client쪽으로 돌려주는 echo 서버의 소스코드입니다.
//dgram-server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 12346
#define BUF_SIZE 128
void err_exit(const char *err){
perror(err);
exit(1);
}
int main(void){
int socket_fd, n;
struct sockaddr_in host_addr, client_addr;
socklen_t size;
char buffer[BUF_SIZE] = {0,};
//Datagram socket 생성
socket_fd=socket(PF_INET,SOCK_DGRAM,0);
if(socket_fd < 0) err_exit("socket error ");
//server ip와 bind하기 위해 주소 설정
memset(&host_addr, 0, sizeof(host_addr));
host_addr.sin_family=AF_INET;
host_addr.sin_port=htons(PORT);
host_addr.sin_addr.s_addr=0; //자신의 주소로 자동으로 설정
//socket과 host_addr과 bind
if(bind(socket_fd,(struct sockaddr *)&host_addr,sizeof(struct sockaddr)) < 0)
err_exit("bind error ");
//Client의 메시지를 그대로 다시 돌려준다.
memset(&client_addr, 0, sizeof(struct sockaddr));
while(1){
size = sizeof(struct sockaddr);
//accept에서 client_addr을 얻어온 stream socket방식과 다르게
//accept가 없는 datagram socket은
//recvfrom에서 client 정보를 얻어올 수 있다.
n = recvfrom(socket_fd, buffer, BUF_SIZE, 0,
(struct sockaddr*)&client_addr, &size);
if(n < 0) err_exit("recvfrom error ");
//Client의 접속 정보를 출력
printf("Client Info : IP %s, Port %d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
//quit라는 메시지를 받으면 종료
if(!strcmp(buffer,"quit") || n == 0) break;
//보낼 client_addr 객체까지 있어야 전달 가능
n = sendto(socket_fd, buffer, n, 0,
(struct sockaddr*)&client_addr, sizeof(struct sockaddr));
if(n < 0)err_exit("send error ");
}
shutdown(socket_fd, SHUT_RDWR);
return 0;
}
이전 stream socket과 다른 점이 있다면, SOCK_STREAM이 아닌 SOCK_DGRAM을 사용한다는 점입니다.
socket_fd=socket(PF_INET,SOCK_DGRAM,0);
stream socket은 send, recv를 사용하였는데, Datagram socket에서는 sendto, recvfrom을 사용하는 것을 알 수 있네요.
이 두 함수들의 끝 2개는 sockaddr의 구조체와 크기입니다. Server는 accept과정이 없기 때문에 client의 주소를 알 방법이 없습니다. 그래서 다시 echo할 주소가 없다는 거죠. recvfrom은 메시지를 받을때 송신자의 주소를 가져오기 위해서 사용합니다. sendto도 마찬가지로 보낼 곳을 지정할때 사용합니다. 이 주소 정보가 없다면 아래와 같은 관계가 성립됩니다.
send(sockfd, buf, len, flags) -> sendto(sockfd, buf, len, flags, NULL, 0)
recv(sockfd, buf, len, flags) -> recvfrom(sockfd, buf, len, flags, NULL, NULL)
클라이언트 코드
이에 대응하는 client의 소스코드입니다.
//dgram-client.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#define BUF_SIZE 128
void err_exit(const char *err){
perror(err);
exit(1);
}
int main(int argc, char *argv[])
{
int sockfd = 0, n = 0;
uint16_t port;
char buffer[BUF_SIZE] = {0,};
struct sockaddr_in server_addr;
int size;
if(argc != 3){
printf("\n Usage: %s server_ip port \n",argv[0]);
return 1;
}
port = atoi(argv[2]);
//Internet용 socket(Datagram)을 연다.
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
err_exit("socket error ");
//server_addr의 구조체를 깔끔히 0으로 도배
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
//127.0.0.1은 본인의 IP이다.
inet_aton(argv[1], (struct in_addr*)&server_addr.sin_addr);
while(1){
while(1){
printf("message:");
fgets(buffer, BUF_SIZE, stdin);
if(strlen(buffer) >= 100)
printf("message too large\n");
else break;
};
// \n을 \0로 처리
buffer[strlen(buffer)-1] = '\0';
size = sizeof(struct sockaddr);
//서버에게 메시지 전송
//connect 콜이 없어서 server ip로 직접 보낸다.
n = sendto(sockfd, buffer, strlen(buffer)+1, 0,
(struct sockaddr*)&server_addr, sizeof(struct sockaddr));
if(n < 0) err_exit("send error ");
if(!strcmp(buffer, "quit")) break;
//서버로부터 에코된 메시지를 받음
n = recvfrom(sockfd, buffer, BUF_SIZE, 0,
(struct sockaddr*)&server_addr, &size);
if(n < 0) err_exit("recv error ");
printf("%s\n", buffer);
}
shutdown(sockfd, SHUT_RDWR);
return 0;
}
결과 화면
Server | Client |
# ./server Client Info : IP 127.0.0.1, Port 60112 Client Info : IP 127.0.0.1, Port 60112 Client Info : IP 127.0.0.1, Port 60112 |
# ./client 127.0.0.1 12346 message:hello datagram server~~ hello datagram server~~ message:good good good good message:quit |
여기까지 간단하게 UDP를 사용하는 server - client 통신 구현을 해보았습니다. 여러 클라이언트를 받기 위해서는 다중 프로세스, 다중 쓰레드, 다중입출력을 활용해야겠죠. 다중입출력 방식 중 하나인 epoll에 관한 설명도 저의 블로그에 있으니 확인해보세요.
https://reakwon.tistory.com/236
'컴퓨터 > 운영체제(주로 리눅스)' 카테고리의 다른 글
[openssl] Server - Client 인증서 검증과 통신 소스 코드 (2) | 2023.10.03 |
---|---|
openssl CA를 통한 Server - Client 인증서 검증 및 대칭키 공유 과정 (0) | 2023.10.02 |
[LINUX] epoll의 개념과 이를 활용한 다중입출력 방식의 서버, 클라이언트 (0) | 2023.09.23 |
[리눅스] 코드로 이해하는 저장된 사용자 ID(Saved UID)가 있는 이유 (0) | 2023.08.16 |
[리눅스] 환경 변수 개념과 환경 변수를 다루는 방법 - 환경 변수는 어쩌면 맛있는게 아닐까? (1) | 2023.08.03 |