간단한 TCP 서버와 클라이언트
어떤 프로그래밍 언어를 시작할 때처럼, 소켓 프로그래밍에서도 간단한 hello world 프로그램을 구현해 보겠습니다.
이 글에서는 간단한 TCP 서버와 클라이언트를 구현합니다.
- 서버는 특정 포트와 IP 주소에서 대기(listen)합니다.
- 클라이언트는 서버에 연결하여 데이터를 보냅니다.
- 서버는 데이터를 수신한 후 클라이언트에 응답을 보냅니다.
- 클라이언트는 응답을 받아 출력합니다.
프로그램 흐름도

image
출처: Socket Programming in C/C++ - GeeksforGeeks
서버 예제
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#define IPADDR "127.0.0.1" /* 서버 IP 주소 */
#define PORT 43866 /* 포트 번호 */
#define STRING_LEN_16 16
#define STRING_LEN_64 64
#define BACKLOG 10
char *_inet_ntoa(struct in_addr *addr, char *ipAddr, int len)
{
// 바이너리 IP 주소를 읽을 수 있는 IP 주소로 변환
if (NULL == addr || NULL == ipAddr || 16 > len)
{
printf("invalid param\n");
return NULL;
}
unsigned char *tmp = (unsigned char *)addr;
snprintf(ipAddr, len, "%d.%d.%d.%d", tmp[0], tmp[1], tmp[2], tmp[3]);
return ipAddr;
}
int main(int argc, char *argv[])
{
// 1. 소켓 생성 socket()
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP 소켓
if (sockfd == -1)
{
printf("fail to call socket, errno[%d, %s]\n", errno, strerror(errno));
exit(0);
}
// 수동으로 sockaddr_in 구조체 구성
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); // 구조체를 0으로 초기화
addr.sin_family = AF_INET; // IPv4
addr.sin_port = htons(PORT); // 포트 번호
inet_aton(IPADDR, &addr.sin_addr); // 읽을 수 있는 IP 주소를 바이너리 IP 주소로 변환
// inet_aton은 IPv6를 지원하지 않으므로 현재는 권장되지 않음
// 2. 소켓을 특정 IP 주소와 포트에 바인딩 bind()
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("fail to call bind, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
// 3. 소켓을 수동 대기 상태로 전환 listen()
if (listen(sockfd, BACKLOG) == -1)
{
printf("fail to call listen, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
printf("server listen on %s:%u\n", IPADDR, PORT);
// 4. 소켓이 대기 상태일 때, accept 함수로 클라이언트 요청 수락 accept()
struct sockaddr_in peerAddr;
socklen_t peerAddrLen = sizeof(struct sockaddr_in);
int connfd = accept(sockfd, (struct sockaddr *)&peerAddr, &peerAddrLen);
if (connfd == -1)
{
printf("fail to call accept, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
char peerIPAddr[STRING_LEN_16];
_inet_ntoa(&peerAddr.sin_addr, peerIPAddr, STRING_LEN_16);
printf("peer client address [%s:%u]\n", peerIPAddr, ntohs(peerAddr.sin_port));
while (1)
{
// 5. 클라이언트가 보낸 데이터 읽기 recv()
char buf[STRING_LEN_64];
int n = recv(connfd, buf, STRING_LEN_64 - 1, 0);
buf[n] = '\0';
if (0 == n) // n이 0이면 상대방이 연결을 종료한 것
{
printf("peer close\n");
break;
}
printf("recv msg from client : %s\n", buf);
sleep(2);
// 6. 클라이언트에 데이터 보내기 send()
char str[] = "recved!";
printf("send msg to client : %s\n", str);
send(connfd, str, strlen(str), 0);
}
// 7. 상호작용 종료, 소켓 닫기 close()
close(connfd);
close(sockfd);
return 0;
}
클라이언트 예제
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#define IPADDR "127.0.0.1" /* 서버 IP 주소 */
#define PORT 43866 /* 서버 포트 번호 */
#define STRING_LEN_64 64
int main()
{
// 1. TCP 소켓 생성 socket()
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sockfd = %d\n", sockfd);
if (-1 == sockfd)
{
printf("fail to call socket, errno[%d, %s]\n", errno, strerror(errno));
exit(0);
}
// 2. 소켓을 특정 IP 주소와 포트 번호에 연결 connect()
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
inet_aton(IPADDR, &addr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("fail to call connect, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
// 3. 소켓을 통해 서버에 데이터 보내기 send()
char str[] = "hello world";
printf("send msg to server : %s\n", str);
send(sockfd, str, strlen(str), 0);
// 4. 소켓을 통해 서버로부터 데이터 수신 recv()
char buf[STRING_LEN_64];
int n = recv(sockfd, buf, STRING_LEN_64 - 1, 0);
buf[n] = '\0';
printf("recv msg from server : %s\n", buf);
// 5. 상호작용 종료, 소켓 닫기 close()
close(sockfd);
}
컴파일 및 실행
Compiling:
gcc client.c -o client
gcc server.c -o server
출력 결과
$ ./server # 서버 터미널에서 서버 실행
server listen on 127.0.0.1:43866
peer client address [127.0.0.1:33844]
recv msg from client : hello world
send msg to client : recved!
peer close
$ ./client # 클라이언트 터미널에서 클라이언트 실행
sockfd = 3
send msg to server : hello world
recv msg from server : recved!
잠재적 문제점
- 서버 프로그램은 한 번에 하나의 클라이언트만 서비스할 수 있습니다.
- 코드가 IPv6를 지원하지 않습니다. (inetntoa, inetaton)
- 이 프로그램에는 보안 조치가 전혀 없습니다. 클라이언트 인증이나 통신 암호화가 없어 스푸핑이나 도청과 같은 공격에 취약합니다.
해결 방안
- select()를 사용하여 여러 클라이언트를 처리합니다.
- getaddrinfo(), ntop(), pton()을 사용하여 IPv6를 지원합니다.
- SSL/TLS를 사용하여 통신을 암호화합니다.