소켓이란?
-컴퓨터가 통신을 하기 위한 도구. 이 소켓을 이용해 컴퓨터 간에 데이터를 주고 받을 수 있다.
-패킷이란 소켓이 주고받는 데이터를 정형화 해놓은 것이다.
- 소켓은 다음과 같은 요소들로 구성되어있다.
· 인터넷 프로토콜 (TCP, UDP, raw IP)
· 로컬 IP 주소
· 로컬 포트
· 원격 IP 주소
· 원격 포트
- 소켓에는 크게 2가지 유형이 있다. 연결지향형, 비연결 지향형이다.
연결지향형 : 각 소켓끼리 서로 연결된 상태에서 통신을 하는 것
-일반적으로 말하는 TCP/IP 가 바로 연결 지향형 소켓에 해당된다.
연결된 대상 외에 다른 대상과 통신이 불가능하고 만약 다른 대상과 통신을 하고 싶다면 그 대상과 연결되는 새로운 소켓을 하나 더 만들어 주어야 한다.
연결지향형 소켓은 데이터를 보내두고 제대로 다 받았는지 중간중간 확인작업을 하게 된다.
그러므로 안정적으로 데이터를 모두 보낼 수 있다.
상대적으로 비연결지향형에 비해 속도가 느리지만, 안정적으로 데이터를 전송해줄 수있다.
데이터가 절대 소실되어서는 안되는 경우 무조건 TCP를 활용한다.
따라서 대다수의 경우 TCP를 활용하고있다.
비연결지향형 : 연결되지 않은 상태에서 내가 원하는 주소에 데이터를 보낼 수 있는 통신방법을 말한다.
-UDP가 비연결지향형 소켓에 해당된다.
데이터를 보내고 난 후에 확인작업을 안해주어서 데이터가 다 수신을 했는지 확인이 불가능하다.
상대적으로 연결지향형에 비해 속도가 빠르지만 중간에 데이터가 소실될 수 있다.
예시로는 동영상 스트리밍 서비스를 생각해 볼 수 있다.
소켓 통신이란?
소켓통신이란 PC간에 인터넷을 이용해서 통신을 하는 것을 뜻한다.
통신을 위해선 여러 설정을 해야하는데 그것을 현재는 os에서 거의 다 구현을 해놓고, 그 프로세스 간의 종착점인 소켓에 개발자는 패킷만 보내면 된다는 뜻으로 소켓 통신이라고 부른다.
소켓 통신에서는 하나의 프로토콜(규약)이 있는데 먼저 서버 소켓이 대기(Listen)을 하면 클라이언트 소켓이 접속(Connect)을 한다. 서버 소켓에서는 승인하여(accept) 데이터 송수신(send, receive)를 한다. 종료할 때는 어느 종단점(Server나 client)에서 종료를 해도 상관없다.
서버 구현
서버 : socket 생성 → 소켓에 이름 연결(bind) → 클라이언트의 연결을 기다림(listen) → 클라이언트 받아들임(accept) → 클라이언트의 명령을 받아서 적절한 서비스를 수행
먼저 Server 소켓을 만들어서 텔넷으로 접속한 후 클라이언트를 만들어보자.
// 소켓을 사용하기 위해서 라이브러리 참조해야 한다.
#pragma comment(lib, "ws2_32")
// inet_ntoa가 deprecated가 되었는데.. 사용하려면 아래 설정을 해야 한다.
#pragma warning(disable:4996)
#include <stdio.h>
#include <iostream>
#include <vector>
#include <thread>
// 소켓을 사용하기 위한 라이브러리
#include <WinSock2.h>
// 수신 버퍼 사이즈
#define BUFFERSIZE 1024
using namespace std;
// 수신 했을 때, 콘솔 출력 및 echo 데이터 만드는 함수
char* print(vector<char>* str){
// 포인트 위치
int p = 0;
// 버퍼 설정. +1은 \0를 넣기 위한 크기
char out[BUFFERSIZE + 1];
// return을 하기 위해서는 힙에 데이터를 선언 해야 한다.
char* ret = new char[str->size() + 10];
// 메모리 복사 "echo - "를 붙힌다.
memcpy(ret, "echo - ", 7);
// 콘솔 출력
cout << "From Client message : ";
// buffer사이지를 넘어서는 데이터일 경우 반복을 통해서 받는다.
for (int n = 0; n < (str->size() / BUFFERSIZE) + 1; n++) {
// 버퍼 사이즈 설정
int size = str->size();
// 수신 데이터가 버퍼 사이즈를 넘었을 경우.
if (size > BUFFERSIZE) {
if (str->size() < (n + 1) * BUFFERSIZE)
{
size = str->size() % BUFFERSIZE;
}
else
{
size = BUFFERSIZE;
}
}
// echo 메시지와 콘솔 메시지를 작성한다.
for (int i = 0; i < size; i++, p++) {
out[i] = *(str->begin() + p);
if (out[i] == '\0')
{
out[i] = ' ';
}
*(ret + p + 7) = out[i];
}
out[size] = '\0';
// 콘솔 메시지 콘솔 출력.
cout << out;
}
cout << endl;
// 에코 메시지는 끝에 개행 + ">"를 넣는다.
memcpy(ret + p + 7, "\n>\0", 3);
return ret;
}
// 접속되는 client별 쓰레드
void client(SOCKET clientSock, SOCKADDR_IN clientAddr, vector<thread*>* clientlist){
// 접속 정보를 콘솔에 출력한다.
cout << "Client connected IP address = " << inet_ntoa(clientAddr.sin_addr) << ":" << ntohs(clientAddr.sin_port) << endl;
// client로 메시지를 보낸다.
const char* message = "Welcome server!\r\n>\0"; send(clientSock, message, strlen(message) + 1, 0);
// telent은 한글자씩 데이터가 오기 때문에 글자를 모을 buffer가 필요하다.
vector<char> buffer;
// 수신 데이터
char x;
while (1) { // 데이터를 받는다. 에러가 발생하면 멈춘다.
if (recv(clientSock, &x, sizeof(char), 0) == SOCKET_ERROR)
{
// 에러 콘솔 출력
cout << "error" << endl;
break;
}
// 만약 buffer의 끝자리가 개행일 경우
if (buffer.size() > 0 && *(buffer.end() - 1) == '\r' && x == '\n') {
// 메시지가 exit일 경우는 수신대기를 멈춘다.
if (*buffer.begin() == 'e' && *(buffer.begin() + 1) == 'x' && *(buffer.begin() + 2) == 'i' && *(buffer.begin() + 3) == 't')
{
break;
}
// 콘솔에 출력하고 에코 메시지를 받는다.
const char* echo = print(&buffer);
// client로 에코 메시지 보낸다.
send(clientSock, echo, buffer.size() + 10, 0);
// 에코 메시지를 힙(new을 사용한 선언)에 선언했기 때문에 메모리 해지한다.
delete echo;
// 버퍼를 비운다.
buffer.clear();
// 다음 메시지 수신 대기
continue; }
// 버퍼에 글자를 하나 넣는다.
buffer.push_back(x);
}
// 수신 대기가 끝나면 client와 소켓 통신을 끊는다.
closesocket(clientSock);
// 접속 정보를 콘솔에 출력한다.
cout << "Client disconnected IP address = " << inet_ntoa(clientAddr.sin_addr) << ":" << ntohs(clientAddr.sin_port) << endl;
// threadlist에서 현재 쓰레드를 제거한다.
for (auto ptr = clientlist->begin(); ptr < clientlist->end(); ptr++)
{
// thread 아이디가 같은 것을 찾아서
if ((*ptr)->get_id() == this_thread::get_id())
{
// 리스트에서 뺀다.
clientlist->erase(ptr);
break;
}
}
// thread 메모리 해지는 thread가 종료 됨으로 자동으로 처리된다.
}
//실행 함수
int main(){
// 클라이언트 접속 중인 client list
vector<thread*> clientlist;
// 소켓 정보 데이터 설정
WSADATA wsaData;
// 소켓 실행.
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
return 1;
}
// Internet의 Stream 방식으로 소켓 생성
SOCKET serverSock = socket(PF_INET, SOCK_STREAM, 0);
// 소켓 주소 설정
SOCKADDR_IN addr;
// 구조체 초기화
memset(&addr, 0, sizeof(addr));
// 소켓은 Internet 타입
addr.sin_family = AF_INET;
// 서버이기 때문에 local 설정한다.
// Any인 경우는 호스트를 127.0.0.1로 잡아도 되고 localhost로 잡아도 되고 양쪽 다 허용하게 할 수 있다. 그것이 INADDR_ANY이다.
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 서버 포트 설정...저는 9090으로 설정함.
addr.sin_port = htons(9090);
// 설정된 소켓 정보를 소켓에 바인딩한다.
if (bind(serverSock, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR) {
// 에러 콘솔 출력
cout << "error" << endl;
return 1;
}
// 소켓을 대기 상태로 기다린다.
if (listen(serverSock, SOMAXCONN) == SOCKET_ERROR) {
// 에러 콘솔 출력
cout << "error" << endl;
return 1;
}
// 서버를 시작한다.
cout << "Server Start" << endl;
// 다중 접속을 위해 while로 소켓을 대기한다.
while (1) {
// 접속 설정 구조체 사이즈
int len = sizeof(SOCKADDR_IN);
// 접속 설정 구조체
SOCKADDR_IN clientAddr;
// client가 접속을 하면 SOCKET을 받는다.
SOCKET clientSock = accept(serverSock, (SOCKADDR*)&clientAddr, &len);
// 쓰레드를 실행하고 쓰레드 리스트에 넣는다.
clientlist.push_back(new thread(client, clientSock, clientAddr, &clientlist));
}
// 종료가 되면 쓰레드 리스트에 남아 있는 쓰레드를 종료할 때까지 기다린다.
for (auto ptr = clientlist.begin(); ptr < clientlist.end(); ptr++) { (*ptr)->join(); }
// 서버 소켓 종료
closesocket(serverSock);
// 소켓 종료
WSACleanup();
return 0;
}
위 소스를 실행하자.
Server Start가 콘솔에 출력되고 accept 함수에서 client가 접속할 때까지 대기합니다.
그럼 텔 넷으로 접속하겠습니다.
클라이언트 구현
클라이언트 : Socket 생성 → 서버에 연결 시도(connect) → 서버에 각종 명령을 전달
서버를 기반으로 클라이언트를 구현해보자.
// 소켓을 사용하기 위해서 라이브러리 참조해야 한다.
#pragma comment(lib, "ws2_32")
// inet_ntoa가 deprecated가 되었는데.. 사용하려면 아래 설정을 해야 한다.
#pragma warning(disable:4996)
#include <stdio.h>
#include <iostream>
#include <vector>
#include <thread>
// 소켓을 사용하기 위한 라이브러리
#include <WinSock2.h>
// 수신 버퍼 사이즈
#define BUFFERSIZE 1024
using namespace std;
// 콘솔에 메시지를 출력하는 함수
void print(vector<char>* str)
{
// 포인트 위치
int p = 0;
// 버퍼 설정. +1은 \0를 넣기 위한 크기
char out[BUFFERSIZE + 1];
// 콘솔 출력
cout << "From server message : ";
for (int n = 0; n < (str->size() / BUFFERSIZE) + 1; n++)
{
// 버퍼 사이즈 설정
int size = str->size();
// 수신 데이터가 버퍼 사이즈를 넘었을 경우.
if (size > BUFFERSIZE) {
if (str->size() < (n + 1) * BUFFERSIZE)
{
size = str->size() % BUFFERSIZE;
}
else
{
size = BUFFERSIZE;
}
}
// echo 메시지와 콘솔 메시지를 작성한다.
for (int i = 0; i < size; i++, p++)
{
out[i] = *(str->begin() + p);
}
// 콘솔 메시지 콘솔 출력.
cout << out;
}
}
// 실행 함수
int main()
{
// 소켓 정보 데이터 설정
WSADATA wsaData;
// 소켓 실행.
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
return 1;
}
// Internet의 Stream 방식으로 소켓 생성
SOCKET sock = socket(PF_INET, SOCK_STREAM, 0);
// 소켓 주소 설정
SOCKADDR_IN addr;
// 구조체 초기화
memset(&addr, 0, sizeof(addr));
// 소켓은 Internet 타입
addr.sin_family = AF_INET;
// 127.0.0.1(localhost)로 접속하기
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 포트 9090으로 접속
addr.sin_port = htons(9090);
// 접속
if (connect(sock, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
{
// 에러 콘솔 출력
cout << "error" << endl;
return 1;
}
// telent은 한글자씩 데이터가 오기 때문에 글자를 모을 buffer가 필요하다.
vector<char> buffer;
// 수신 데이터
char x;
while (1)
{
// 데이터를 받는다. 에러가 발생하면 멈춘다.
if (recv(sock, &x, sizeof(char), 0) == SOCKET_ERROR)
{
// 에러 콘솔 출력
cout << "error" << endl;
break;
}
// 버퍼에 글자를 하나 넣는다.
buffer.push_back(x);
// \r\n>\0가 나오면 콘솔에 출력하고 콘솔로 부터 메시지를 기다린다.
if (buffer.size() > 4 && *(buffer.end() - 4) == '\r' && *(buffer.end() - 3) == '\n' && *(buffer.end() - 2) == '>' && *(buffer.end() - 1) == '\0')
{
// 메시지 출력
print(&buffer);
// 버퍼 초기화
buffer.clear();
// 콘솔로 부터 입력을 받는다.
char input[BUFFERSIZE];
// 유저로 부터 입력 받기
cin >> input;
// 입력받은 길이를 받는다.
int size = strlen(input);
// 개행을 넣는다.
*(input + size + 1) = '\r';
*(input + size + 2) = '\n';
// 서버로 보내기
send(sock, input, size + 3, 0);
// 메시지가 exit라면 종료
if (*input == 'e' && *(input + 1) == 'x' && *(input + 2) == 'i' && *(input + 3) == 't')
{
break;
}
continue;
}
}
// 서버 소켓 종료
closesocket(sock);
// 소켓 종료
WSACleanup();
return 0;
}
클라이언트를 실행해 서버로 접속하자.
접속해서 test / test를 작성해서 보냈다.
서버에서는 클라이언트에서 받은 메시지가 표시되고 다시 클라이언트에 echo 메시지가 표시된다. 접속이 된 것이 확인이 된다.
참조 : https://kevinthegrey.tistory.com/26
*) C++를 이용한 TCP 소켓통신 구현
서버-클라이언트 환경을 만들기 위한 과정 서버 : Socket 생성 → Socket에 이름연결(bind) → 클라이언트의 연결을 기다림(listen) → 클라이언트 받아들임(accept) → 클라이언트의 명령을 받아서 적
kevinthegrey.tistory.com
참조 : https://nowonbun.tistory.com/736
[C++] 소켓(Socket) 통신을 하는 방법
안녕하세요. 명월입니다. 이 글은 C++에서 소켓(Socket) 통신을 하는 방법에 대한 글입니다. 소켓 통신이란 PC간에 인터넷을 이용해서 통신하는 것을 소켓 통신입니다. 사실 통신을 하기 위해서는
nowonbun.tistory.com
'Programming Language > c,c++' 카테고리의 다른 글
[C/C++] C++로 2048 구현하기 (0) | 2023.01.03 |
---|