[c++] tcp 소켓 통신을 이용한 단일 클라이언트 채팅 프로그램 구현
네트워크 소켓 통신에 대한 개념을 익히고 사용해보기 위해 c++을 이용해 간단한 채팅을 구현해보여고 한다.
socket은 기본적으로 <protocol,Ip주소,포트num>으로 정의 되며, 클라이언트와는 달리 서버 측에서는 port 넘버를 명시적으로 바인딩 해주어야 한다.
udp 와는 달리, tcp는 ip 주소와 port number로 유니크하게 식별되지 않음.
why?
서버에서 대기하고 있는 listen 소켓과 클라이언트 사이 3-way handshake 과정을 통해 클라이언트와 connet 되면 서버 측에서는 소켓을 하나 더 열어 new 소켓과 통신을 하게됨(서버 측에서는 소켓이 여러 개)
-> 서버에서는 여러 개의 소켓을 열게 되는 데, 소켓들의 ip 주소와 port number가 같다
따라서 listen 이후에는 서버의 어느 소켓으로 통신할지 정해야하기 때문에 <src,src port, dest IP, dest port> 로 소켓을 식별한다
아래 코드에서도 TCP의 동작 원리에 따라 <소스 IP, 소스 포트, 목적지 IP, 목적지 포트> 조합을 기반으로 데이터 송수신이 이루어 진다. 서버 측에서 서버는 accept()를 호출하여 클라이언트 연결 요청을 수락 하고 ,accept()로 생성된 New 소켓은 각각 다른 TCP 연결을 처리하기 때문에 여러 클라이언트와 동시 통신이 가능하다.
서버 측
- 소켓 생성 : socket()
SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0);
- IP 주소와 포트 번호를 바인딩 : bind()
bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr));
- 클라이언트 요청 대기 상태로 전환 : listen()
listen(serverSocket, SOMAXCONN);
- 클라이언트 연결 수락 : accept()
SOCKET clientSocket = accept(serverSocket, nullptr, nullptr);
- 데이터 송수신 : recv(), send()
- 데이터 수신
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
- 데이터 전송
send(clientSocket, reply.c_str(), reply.size(), 0);
- 데이터 수신
- 소켓 종료 : closesocket()
closesocket(clientSocket); closesocket(serverSocket); WSACleanup();
클라이언트 측
- 소켓 생성 : socket()
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, 0);
- 서버 연결 요청 : connect()
connect(clientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr));
- 데이터 송수신 : send(), recv()
- 데이터 전송
send(clientSocket, message.c_str(), message.size(), 0);
- 데이터 수신
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
- 데이터 전송
- 소켓 종료 : closesocket()
closesocket(clientSocket); WSACleanup();
서버 측 코드
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <string>
#pragma comment(lib, "ws2_32.lib") // Winsock 라이브러리 링크
#define PORT 8080
int main() {
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serverAddr{};
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
serverAddr.sin_addr.s_addr = INADDR_ANY;
bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr));
listen(serverSocket, SOMAXCONN);
std::cout << "서버 실행 중... 클라이언트 연결 대기 중\n";
SOCKET clientSocket = accept(serverSocket, nullptr, nullptr);
std::cout << "클라이언트 연결됨!\n";
char buffer[1024];
while (true) {
memset(buffer, 0, sizeof(buffer));
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
if (bytesReceived <= 0) {
std::cout << "클라이언트 연결 종료\n";
break;
}
std::cout << "클라이언트: " << buffer << std::endl;
// 서버도 메시지를 입력해서 클라이언트에게 보낼 수 있음
std::cout << "서버: ";
std::string reply;
std::getline(std::cin, reply);
send(clientSocket, reply.c_str(), reply.size(), 0);
}
closesocket(clientSocket);
closesocket(serverSocket);
WSACleanup();
return 0;
}
- 기능 요약:
- 서버는 단일 클라이언트와 연결
- 서버는 클라이언트로부터 메시지를 받으면 즉시 출력, 서버도 클라이언트에게 메시지 송신
- 단일 클라이언트 통신만 지원하며, 다른 클라이언트와 동시 연결이 불가능
- 특징:
- 클라이언트 연결 후 recv로 메시지를 받고, 서버가 입력한 메시지를 send로 클라이언트에게 전송.
- 단순한 구조로, 다중 클라이언트를 처리할 수 없음.
클라이언트 코드
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include<string>
#pragma comment(lib, "ws2_32.lib") // Winsock 라이브러리 링크
#define PORT 8080
#define SERVER_IP "127.0.0.1"
int main() {
WSADATA wsaData;
//MAKEWORD(2, 2) → Winsock 2.2 버전 요청
// wsaData → 초기화된 정보 저장
WSAStartup(MAKEWORD(2, 2), &wsaData);
//AF_INET : IPv4 사용
//SOCK_STREAM : TCP(연결 지향) 방식
//0: 기본 프로토콜(TCP) 선택
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, 0);
/* serverAddr.sin_family → 주소 체계(AF_INET)
serverAddr.sin_port → 포트 번호 저장
serverAddr.sin_addr.s_addr → IP 주소 저장*/
sockaddr_in serverAddr{};
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &serverAddr.sin_addr);
if (connect(clientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
std::cerr << "서버 연결 실패\n";
WSACleanup();
return 1;
}
std::cout << connect(clientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) << std::endl;
std::cout << "서버에 연결됨! 채팅을 입력하세요 (종료하려면 'exit' 입력)\n";
char buffer[1024];
while (true) {
std::cout << "클라이언트: ";
std::string message;
std::getline(std::cin, message);
if (message == "exit") {
break;
}
send(clientSocket, message.c_str(), message.size(), 0);
memset(buffer, 0, sizeof(buffer));
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
if (bytesReceived <= 0) {
std::cout << "서버 연결 종료\n";
break;
}
std::cout << "서버: " << buffer << std::endl;
}
closesocket(clientSocket);
WSACleanup();
return 0;
}
기능 요약:
- 클라이언트는 서버와의 단일 연결만 처리합니다.
- 클라이언트는 서버와 1:1 통신을 수행하며, 송수신이 반복됩니다.
- 클라이언트는 사용자가 입력한 메시지를 전송하고, 서버로부터 메시지를 수신해 출력합니다.
이 코드를 visual studio 에서 실행하면 다음 처럼 메세지 송수신이 가능해진다.
참고:
소켓 동작 방식
https://www.youtube.com/watch?v=WwseO8l8rZc&t=530s
winsocket 사용하기 (차례대로 따라가면 된다)
https://learn.microsoft.com/ko-kr/windows/win32/winsock/creating-a-basic-winsock-application