소켓 프로그래밍은 네트워크 통신을 위한 기본적인 도구로, 리눅스에서는 C 언어로 간단하게 구현할 수 있습니다. 이 글에서는 기본적인 클라이언트-서버 소켓 프로그래밍 방법을 알려드리겠습니다.
소켓 프로그래밍이란?
소켓은 네트워크 통신을 위한 엔드포인트로, 프로세스 간 통신의 핵심 요소입니다. 쉽게 말해 소켓은 네트워크 상에서 데이터를 주고받을 수 있는 통로라고 생각하면 됩니다. 소켓은 IP 주소와 포트 번호의 조합으로 식별됩니다.
클라이언트-서버 모델의 기본 흐름
서버 측 동작 순서:
- 소켓 생성
- 소켓을 특정 주소와 포트에 바인딩
- 연결 요청 대기(listen)
- 클라이언트 연결 수락(accept)
- 데이터 송수신
- 연결 종료
클라이언트 측 동작 순서:
- 소켓 생성
- 서버에 연결 요청
- 데이터 송수신
- 연결 종료
이 기본적인 흐름을 코드로 구현해보겠습니다.
서버 구현하기
다음은 기본적인 에코 서버의 코드입니다. 클라이언트로부터 메시지를 받아 그대로 다시 전송하는 간단한 서버입니다.
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main()
{
int socket_desc, client_sock, c, read_size;
struct sockaddr_in server, client;
char client_message[2000];
*// 소켓 생성*
socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (socket_desc == -1)
{
printf("소켓을 생성할 수 없습니다");
return 1;
}
*// 서버 주소 구조체 준비*
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY; *// 모든 인터페이스에서 연결 수락*
server.sin_port = htons(8888); *// 포트 8888 사용*
*// 소켓을 주소에 바인딩*
if(bind(socket_desc, (struct sockaddr *)&server, sizeof(server)) < 0)
{
perror("바인딩 실패");
return 1;
}
*// 연결 대기*
listen(socket_desc, 3);
*// 연결 수락*
printf("연결을 기다리는 중...\\n");
c = sizeof(struct sockaddr_in);
client_sock = accept(socket_desc, (struct sockaddr *)&client, (socklen_t*)&c);
if (client_sock < 0)
{
perror("연결 수락 실패");
return 1;
}
printf("연결이 수락되었습니다\\n");
*// 클라이언트로부터 메시지 수신 및 에코*
while((read_size = recv(client_sock, client_message, 2000, 0)) > 0)
{
*// 받은 메시지를 클라이언트에게 다시 전송*
write(client_sock, client_message, strlen(client_message));
}
if(read_size == 0)
{
printf("클라이언트 연결 종료\\n");
}
else if(read_size == -1)
{
perror("수신 실패");
}
return 0;
}
클라이언트 구현하기
이제 서버에 연결하여 메시지를 주고받을 클라이언트 코드를 만들어 봅시다.
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main()
{
int sock;
struct sockaddr_in server;
char message[1000], server_reply[2000];
*// 소켓 생성*
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1)
{
printf("소켓을 생성할 수 없습니다");
return 1;
}
*// 서버 주소 설정*
server.sin_addr.s_addr = inet_addr("127.0.0.1"); *// 로컬호스트*
server.sin_family = AF_INET;
server.sin_port = htons(8888);
*// 서버에 연결*
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
{
perror("연결 실패");
return 1;
}
printf("연결 성공\\n");
*// 서버와 통신*
while(1)
{
printf("메시지 입력: ");
scanf("%s", message);
*// 서버에 데이터 전송*
if(send(sock, message, strlen(message), 0) < 0)
{
puts("전송 실패");
return 1;
}
*// 서버로부터 응답 수신*
if(recv(sock, server_reply, 2000, 0) < 0)
{
puts("수신 실패");
break;
}
printf("서버 응답: %s\\n", server_reply);
*// 메시지 버퍼 초기화*
memset(server_reply, 0, 2000);
}
close(sock);
return 0;
}
컴파일 및 실행 방법
리눅스 환경에서 작성한 코드를 컴파일하고 실행하는 방법은 다음과 같습니다.
1. 컴파일하기
*# 서버 컴파일*
gcc server.c -o server
*# 클라이언트 컴파일*
gcc client.c -o client
2. 실행하기
두 개의 터미널 창을 열고 다음과 같이 실행합니다.
첫 번째 터미널 (서버):
./server
두 번째 터미널 (클라이언트):
./client
주요 소켓 함수 정리
소켓 프로그래밍에서 사용하는 주요 함수들의 역할을 간략히 정리하면 다음과 같습니다.
- socket(): 소켓 생성
- bind(): 소켓을 특정 IP 주소와 포트에 바인딩
- listen(): 연결 요청 대기 상태로 전환
- accept(): 클라이언트 연결 수락
- connect(): 서버에 연결 요청
- send(), write(): 데이터 전송
- recv(), read(): 데이터 수신
- close(): 소켓 연결 종료
자주 발생하는 문제 및 해결 방법
소켓 프로그래밍 시 자주 발생하는 문제와 해결 방법을 알아봅시다.
- Address already in use 오류
- 문제: 서버를 다시 시작할 때 이미 포트가 사용 중이라는 오류
- 해결: SO_REUSEADDR 옵션 설정
int opt = 1; setsockopt(socket_desc, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - Connection refused
- 문제: 서버가 실행 중이지 않거나 포트가 다를 때 발생
- 해결: 서버 상태 확인 및 포트 번호 확인
- 데이터 수신 버퍼 오버플로우
- 문제: 예상보다 큰 데이터 수신 시 버퍼 오버플로우 발생
- 해결: 충분한 크기의 버퍼 사용 및 수신 데이터 크기 제한
'임베디드SW 기초' 카테고리의 다른 글
| 리눅스 네트워크프로그래밍(1) - 워치독 타이머(WDT) (0) | 2025.03.07 |
|---|---|
| 시스템 프로그래밍(4) - 시그널 관련 시스템 콜 (0) | 2025.02.26 |
| 시스템 프로그래밍(3) - 프로세스 관련 시스템 콜 (0) | 2025.02.26 |
| 시스템 프로그래밍(2) - 파일 및 디렉토리 관련 시스템 콜 (0) | 2025.02.26 |
| 시스템 프로그래밍 (1) - 주요 개념 (0) | 2025.02.26 |