cks5730 2019. 12. 27. 17:14

소켓

네트워크를 이용하여 서로 통신하기 위한 수단입니다.

 

TCP 통신

연결 지향형 소켓입니다. 두 소켓이 서로 연결된 상태에서 통신합니다.

예를 들어, 송신자가 전화를 걸면 수신자가 받아야 통신이 가능한 상태가 됩니다.

그래서 안정적인 데이터 전송이 가능해서 신뢰성이 높지만 UDP에 비해 속도가 느릴 수 있습니다.

 

UDP 통신

비 연결 지향형 소켓입니다. 서로 연결이 되어 있지 않더라도 통신이 가능합니다.

통보하는 형태로, 송신측에서 데이터를 보내면 수신측에서 받은것에 대한 확인이 없습니다.

그래서 TCP에 비해 속도가 빠르지만, 중간에 데이터를 잃어버릴 염려가 있어서 신뢰성이 낮습니다.

 

TCP 통신 : 서버

1. create socket

TCP 통신용 소켓을 생성합니다.

 

2. bind

서버에서 사용할 IP 주소와 포트 번호를 create socket으로 생성한 소켓에 바인딩합니다.

 

3. listen

바인딩한 소켓에 클라이언트로부터 연결 요청이 있는지 주시합니다.

 

4. accept

클라이언트로 부터 connect 요청이 받아들여지면 서로 통신을 하기 위한 accept 소켓을 생성합니다.

accept은 승인 대기 상태로 블로킹 방식으로 처리합니다.

블로킹 방식은 별도의 요청이 있을 때 까지 무한 대기상태에 빠져있는 것입니다.

그래서 아무런 요청이 없다면 블로킹 방식 함수에서 빠져나올 수 없습니다.

클라이언트에서 connect로 연결되면 accept로 서로 통신할 새로운 소켓을 생성하고 빠져나오게 됩니다.

 

5. send / recv

서로 패킷을 주고 받습니다.

 

6. close

종료시 사용한 소켓 리소스를 반환합니다.

 

TCP 통신 : 클라이언트

1. create socket

TCP 통신용 소켓을 생성합니다.

 

2. connect

서버측에 연결을 요청합니다.

 

3. 서버에서 accept 이후 send / recv 시작

서로 패킷을 주고 받습니다.

 

4. close

종료시 사용한 소켓 리소스를 반환합니다.

 

TCP 통신 : 서버 C++ 소스 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// 윈속 헤더 추가
#include <WinSock2.h>
#include <iostream>
 
using namespace std;
 
// 윈속의 기능을 쓰기 위한 라이브러리 추가
#pragma comment(lib, "ws2_32.lib")
 
#define PORT             7890
#define PACKET_LENGTH    1024
 
int main()
{
  // 윈속 초기화를 위한 구조체 선언
  WSADATA wsaData;
  
  /*
    WSAStartup(윈속 초기화) ~ WSACleanup(윈속 종료) 사이에 
    윈속 코드를 작성한다.
  */
  
  // <<< 윈속 초기화 >>>
  // arg 1 : WORD *wVersionRequested
  // 프로그램이 요구하는 winsock 버전 중 최상위 버전.
  // MAKEWORD 매크로를 통해 2.2버전을 쓰겠다고 설정.
  // arg 2 : LPWSADATA *lpWSAData
  // WSADATA 구조체 포인터가 들어간다.
  // 함수가 반환되면 윈속을 위한 세부 정보가 구조체에 포함된다.
  WSAStartup(MAKEWORD(2,2), &wsaData);
  
  // <<< Create Socket >>>
  // PF_INET : Protocol Family를 설정한다(IPv4).
  // SOCK_STREAM : 새로 생성할 소켓의 타입을 정한다(TCP).
  // IPPROTO_TCP : 실제로 사용할 프로토콜을 최종적으로 결정한다(TCP).
  // 세 번째 인자로 IPPROTO_HOPOPTS을 전달해도 된다.
  // 이 값은 디폴트 0으로 잡혀있다. 이 값을 쓰면 자동으로
  // TCP 전용 소켓인지, UDP 전용 소켓인지 자동으로 결정해준다. 그래서 0으로 전달해도 된다.
  SOCKET hListen = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  
  // 이제 위에서 생성한 소켓의 포트 번호와 IP 주소, Address Family를 정해야한다.
  // 이를 위해 SOCKADDR_IN 구조체를 생성한다. (bind를 하기 위한 준비)
  SOCKADDR_IN tListenAddr = {};
  
  // AF_INET : IPv4 Address Family를 쓰겠다. 
  tListenAddr.sin_family = AF_INET;
  
  // htons(PORT) : 위에서 매크로로 정의한 포트 번호를 설정한다.
  // htons : Host To Network Short의 약자로
  // Network로 short형 데이터를 보낼때 바이트 오더를 해주는 함수이다.
  // 네트워크 주소는 CPU 바이트 순서대로 저장되는데, 바이트 오더 방식은 2가지가 있다.
 
  // [빅 엔디안]
  // SPARC, RISC CPU 계열에서 사용하는 바이트 오더 방식.
  // 빅 엔디안 방식의 바이트 오더는 큰 단위부터 메모리에 적는다.
  // (ex) 0x12345678을 적는다고 하면 큰 단위가 1234 여기니까
  // 12, 34, 56, 78 이렇게 메모리를 적는다.
  // 이처럼 최고 단위가 앞에 있으므로 부호에 대한 비트 확인이 빨라서
  // 네트워크 통신에서 사용하는 방식이라고 한다.
 
  // [리틀 엔디안]
  // 인텔, 암드 계열에서 사용하는 바이트 오더 방식이다.
  // (대부분 컴퓨터는 인텔, 암드 계열을 사용하므로... 
  // 네트워크 통신을 위해 htons로 빅 엔디안 방식의 바이트 오더로 전환해야한다)
  // 이 방식은 작은 단위부터 메모리에 적는 방식이다.
  // (ex) 0x12345678을 적는다고 하면 5678 여기가 작은 단위니까
  // 78, 56, 34, 12 이런식으로 CPU에 메모리를 적는다.
  // 연산을 할 때 가장 뒷 자리부터 계산하므로 연산이 빠르다는 장점이 있다. 
  tListenAddr.sin_port = htons(PORT);
  
  // <<< bind 준비 >>>
  // htonl(INADDR_ANY) : Host To Network Long
  // long 타입 데이터의 바이트 오더를 빅 엔디안 바이트 오더 방식으로 바꿔준다.
  // INADDR_ANY : 컴퓨터에서 존재하는 랜카드 중에서 사용 가능한 IP 주소를 찾아서
  // 자동으로 대입해준다. 서버 입장에서 현재 동작되는 컴퓨터가 서버의 IP 주소이므로
  // INADDR_ANY로 설정해준다.
  tListenAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  
  // <<< bind >>>
  // 위에서 지정한 포트 번호, IP 주소를 생성한 Listen Socket에 바인딩한다.
  // 이렇게 바인딩하는 이유는 운영체제가 포트 번호를 관리하기 위함이다.
  // 만약, 서버 소켓의 포트 번호가 다른 소켓에서 중복되었다고 한다면
  // 운영체제는 이를 감지하여 에러를 리턴하게 된다.
  bind(hListen, (SOCKADDR*)&tListenAddr, sizeof(tListenAddr));
  
  // <<< Listen >>>
  // 귀를 기울인다. 즉, 위에서 포트 번호와 IP 주소를 바인딩한 서버 소켓을 통해서
  // 클라이언트의 연결 요청이 수신될 때 까지 기다린다.
  // 두 번째 인자는 클라이언트 연결 대기열의 크기이다. 여러 클라이언트가 한 번에 들어올 수 있다.
  // 보통 5로 설정한다고 하지만, 윈속2부터 SOMAXCONN이라는 상수값을 지정할 수 있다.
  // 이를 통해 소켓 지정자가 알아서 연결 대기열의 크기를 결정할 수 있게 되었다고 한다.
  // 그리고 listen api가 성공적으로 리턴했다고 해서 무조건 연결되어 있는 상태가 아니다.
  // 클라이언트의 최종적인 연결 요청을 받아들이는 함수는 아래에서 사용될 accept 함수이다.
  listen(hListen, SOMAXCONN);
  
  // <<< accept 준비 >>>
  // 클라이언트의 connect를 accept 하기 전에
  // 미리 새롭게 생성될 소켓의 구조체 정보를 생성해둔다.
  // 클라이언트와 연결이 성공되면 이 구조체의 정보를 이용해서
  // 클라이언트의 주소 정보를 알아낼 수 있다.
  SOCKADDR_IN tClntAddr = {};
  int iSize = sizeof(tAddr);
  
  // <<< accept >>>
  // accept는 연결지향형 소켓에서 사용하는 함수이다 !!
  // (비연결지향형 UDP에서는 사용하지 않는다)
  // 그리고 accept는 블로킹 방식으로 처리된다.
  // 블로킹 방식 : 요청이 올 때 까지 대기한다.
  // (요청이 오기 전 까지 이 함수에서 빠져나올 수 없다)
  // 클라이언트와 연결이 되면 서로 통신할 새로운 소켓을 반환한다.
  // 이 소켓을 바탕으로 서로 통신할 수 있게 되는 것이다.
  // 에러가 발생하면 -1(SOCKET_ERROR)을 리턴하고
  // 성공하면 0보다 큰 파일지정번호가 리턴된다.
  SOCKET hSocket = accept(hListen, (SOCKADDR*)&tClntAddr, &iSize);
  
  if(hSocket == SOCKET_ERROR)
  {
    // 에러가 발생했다면 사용한 소켓을 닫아주고 바로 종료한다.
    // 사용한 소켓의 리소스를 반환한다.
    closesocket(hListen);
    
    // 윈속 종료
    WSACleanup();
    
    return 0;
  }
  
  // 클라이언트에게 접속되었다는 메시지를 보낸다.
  // (캐릭터 배열로 구성)
  char Packet[PACKET_LENGTH] = {};
  strcpy_s(Packet, "클라님 접속을 성공했습니다.");
  
  // <<< send >>>
  // 지정된 소켓으로 패킷을 전송한다.
  // send 또한 블로킹 방식으로 처리 되기 때문에
  // 실행 결과(성공, 실패, 종료)가 결정되기 전 까지 값을 리턴하지 않는다.
  // 마지막 인자는 0으로 설정하면 일반적인 패킷 데이터를 전송할 수 있다.
  send(hSocket, Packet, strlen(Packet), 0);
  
  // 클라이언트로 받을 패킷 메시지를 위해 깨끗하게 비워둔다.
  memset(Packet, 0sizeof(Packet));
  
  // <<< recv >>>
  // 서버에서 응답받은 클라이언트로부터 패킷을 받는다.
  // 이 함수도 블로킹 방식으로 처리된다.
  recv(hSocket, Packet, PACKET_LENGTH, 0);
  
  cout << "Client Message : " << Packet << endl;
  
  // 종료시 사용이 끝난 소켓 리소스를 반환한다.
  closesocket(hSocket);
  closesocket(hListen);
  
  // 윈속을 종료한다.
  WSACleanup();
  
  return 0;
}
cs

 

TCP 통신 : 클라이언트 C++ 소스 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// 비주얼 스튜디오 상위 버전에서 error C4996 : inet_addr의 보안 오류가 발생하면
// 아래와 같은 매크로를 통해 해결할 수 있다. 
// 또는 <WS2tcpip.h> 헤더 추가 후 inet_addr 대신 inet_pton API를 사용한다.
#define _WINSOCK_DEPRECATED_NO_WARNINGS
 
#include <WinSock2.h>
#include <iostream>
 
using namespace std;
 
#pragma comment(lib, "ws2_32.lib")
 
#define PORT             7890
#define PACKET_LENGTH    1024
 
// 서버쪽에서 INADDR_ANY로 잡아준 컴퓨터 IP에
// 접속하기 위해 "127.0.0.1"의 IP를 지정한다.
// (같은 컴퓨터의 IP 주소)
#define LOCAP_IP    "127.0.0.1"
 
int main()
{
  WSADATA wsaData;
  WSAStartup(MAKEWORD(2,2), &wsaData);
 
  // <<< Create Socket >>>
  // 서버에 접속 요청을 위한 소켓을 생성한다.
  // TCP 전용 소켓을 생성했으므로 똑같이 맞춰준다.
  // 마지막 세 번째 인자는 IPPROTO_HOPOPTS(0)으로 전달해도 된다.
  // (서버코드 주석 참고)
  SOCKET hSocket = socket(PF_INET, SOCK_STREAM, iPPROTO_TCP);
  
  SOCKADDR_IN tAddr = {};
  tAddr.sin_family = AF_INET;
  tAddr.sin_port = htons(PORT);
  tAddr.sin_addr.s_addr = inet_addr(LOCAL_IP);
 
  // <<< Connect 요청 >>>
  // 운영체제를 통해 포트 번호와 IP 주소가 바인딩 된 서버 소켓에 접속을 요청한다.
  // 이 함수도 블로킹 방식으로 처리 된다.
  // 서버 소켓이 클라이언트의 연결 요청에 대한 성공, 거절, 또는 시관 초과 등에 대한
  // 결정이 되기 전까지 이 함수에서 빠져나올 수 없다.
  // (이 함수가 실행되면 곧 바로 리턴되지 않을 수 있다)
  connect(hSocket, (SOCKADDR*)&tAddr, sizeof(tAddr));
  
  // <<< recv >>>
  // 연결 요청이 성공되었을 때 서버로 부터 연결이 완료 되었다는 확인용 패킷을 받는다.
  // recv API는 패킷을 받을 때, send API는 패킷을 전송할 때 쓰는 함수이다.
  // send와 recv 모두 블로킹 방식으로 처리 된다.
  // 성공, 실패, 종료가 결정되기 전까지 함수에서 빠져나올 수 없다는 뜻. (언제끝날지 모름)
  
  // (추가 내용)
  // 보통 recv는 send와 달리 별도의 스레드에서 처리하는 방식을 활용한다.
  // send는 데이터를 보내는 주체가 자기 자신이기 때문에 얼마만큼의 패킷을 전송할 지
  // 언제 전송해야 할 지 알 수 있지만, recv는 대상이 어떤 데이터를 언제 수신할 지에 대한
  // 특정한 시점을 알 수 없다. 그리고 recv API는 실행되면 언제 끝날지 모르는 블로킹 상태로
  // 빠지게 된다. 이를 해결하기 위해 데이터 수신을 위한 recv API는 별도의 스레드에서 해결한다. 
  // 소켓과 연결이 완료되면 새로운 스레드를 생성하고 그곳에서 recv를 통해 
  // 데이터가 수신 되길 기다리는 방식으로 처리할 수 있다.
  char Packet[PACKET_LENGTH] = {};
  recv(hSocket, Packet, PACKET_LENGTH, 0);
 
  // 서버 소켓으로 부터 받은 패킷 메시지를 출력한다.
  cout << "Server Message : " << Packet << endl;
  
  // 서버 소켓에 패킷을 전송할 메시지를 입력
  strcpy_s(Packet, "접속 완료 !!");
  
  // <<< send >>>
  // 서버 소켓에 패킷 "접속 완료 !!" 메시지를 전송한다.
  send(hSocket, Packet, strlen(Packet), 0);
  
  // 종료시 사용이 끝난 소켓 리소스를 반환한다.
  closesocket(hSocket);
  
  // 윈속 초기화 해제
  WSACleanup();
 
  return 0;
}
cs

 

TCP 통신 실행 결과 

이처럼 TCP 통신은 서로 연결되면 새롭게 생성되는 accept 소켓으로 통신합니다.

반면에 UDP 통신은 하나의 소켓으로 여러 상대방과 통신할 수 있습니다.

 

그래서 UDP 통신은 TCP처럼 서버 / 클라이언트라는 구분 없이

패킷을 먼저 받는 쪽, 패킷을 전송하는 쪽으로 작성했습니다.

 

UDP 통신 : 패킷을 먼저 받는 쪽

1. create socket

소켓을 생성합니다.

 

2. bind

위에서 생성한 소켓에 통신할 IP 주소와 포트 번호를 묶어줍니다.

 

3. recvfrom

바인딩한 소켓 주소로 패킷을 받을 준비를 합니다.

패킷을 받게 되면, 패킷을 전송한 상대방의 주소를 알게 됩니다.

 

4. sendto

상대방의 주소로 자신의 패킷을 전송합니다.

 

5. close

종료시 사용한 소켓 핸들을 닫습니다.

 

UDP 통신 : 패킷을 전송하는 쪽

1. create socket

소켓을 생성합니다.

 

2. sendto

원하는 포트 번호와 IP 주소로 자신의 패킷을 전송합니다.

 

3. recvfrom

패킷을 받습니다.

 

4. close

송수신이 완료되면 사용한 소켓을 닫는다.

 

UDP 통신 : 패킷을 먼저 받는 쪽 C++ 소스 코드

(패킷을 먼저 받는 쪽을 Server 문자열로 표현했습니다)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <WinSock2.h>
#include <iostream>
 
using namespace std;
 
#pragma comment(lib, "ws2_32.lib")
 
#define PORT             7890
#define PACKET_LENGTH    1024
 
int main()
{
  // <<< UDP 소켓 통신 >>>
  // 목적 시스템으로 목적지로 패킷을 전송하면 끝이다.
  // 그러므로 먼저 sendto를 하게 되는 쪽이 bind한 쪽의 포트 번호와 IP 주소를 알아야하고
  // 서로 통신할 소켓을 bind한 쪽에서 먼저 패킷을 받게 된다.
  // 여기서는 먼저 받게 되는 쪽이므로
  // 소켓 생성 -> bind -> recvfrom -> sendto의 순서를 갖는다.
  
  // <<< 윈속 초기화 >>>
  WSADATA wsaData;
  WSAStartup(MAKEWORD(2,2), &wsaData);
  
  // <<< Create Socket >>>
  // PF_INET : IPv4 protocol family
  // SOCK_DGRAM : UDP 프로토콜 전송 방식
  // IPPROTO_UDP : 실제로 사용할 프로토콜 통신 방식 결정(UDP)
  // 마찬가지로 IPPROTO_HOPOPTS 값(0)을 설정하면 TCP인지 UDP인지 자동으로 잡힌다.
  SOCKET hSocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
  
  // <<< bind 준비 >>>
  SOCKADDR_IN tAddr = {};
  tAddr.sin_family = AF_INET;
  tAddr.sin_port = htons(PORT);
  tAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  
  // <<< bind >>>
  bind(hSocket, (SOCKADDR*)&tAddr, sizeof(tAddr));
  
  // <<< 패킷을 sendto한 쪽의 주소를 얻기 위한 준비 >>>
  SOCKADDR_IN tDestAddr = {};
  int iDestLength = sizeof(tDestAddr);
  char Packet[PACKET_LENGTH] = {};
  
  // <<< recvfrom >>>
  // arg 1 : 바인딩 한 소켓
  // arg 2 : 패킷을 받을 그릇
  // arg 3 : 패킷을 받을 그릇의 사이즈
  // arg 4 : flag 값으로 다양한 옵션값을 줄 수 있지만 보통 디폴트로 0을 전달
  // arg 5 : 패킷을 전송한 주소가 여기 구조체로 들어온다.
  // arg 6 : 패킷을 전송한 주소 구조체의 사이즈
  recvfrom(hSocket, Packet, sizeof(Packet), 0, (SOCKADDR*)&tDestAddr, &iDestLength);
  
  cout << "I am Server : " << Packet << endl;
  
  // 다시 데이터를 송신하기 전에 패킷 비우기
  memset(Packet, 0, PACKET_LENGTH);
  
  // 패킷 메시지 설정
  strcpy_s(Packet, "Send from Server");
  
  // <<< sendto >>>
  // strlen(Packet) : 보내고자 하는 패킷의 길이 만큼 전송
  // (SOCKADDR*)&tDestAddr : recvfrom을 통해서 받게된 전송한 쪽의 주소로 패킷 전송
  sendto(hSocket, Packet, strlen(Packet), 0, (SOCKADDR*)&tDestAddr, iDestLength);
  
  // <<< close >>>
  // 종료시 사용한 소켓 자원 반환
  closesocket(hSocket);
  
  // 윈속 종료
  WSACleanup();
  
  return 0;
}
cs

 

UDP 통신 : 패킷을 전송하는 쪽 C++ 소스 코드

(패킷을 전송하는 쪽을 Client 문자열로 표현했습니다)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#define _WINSOCK_DEPRECATED_NO_WARNINGS
 
#include <WinSock2.h>
#include <iostream>
 
using namespace std;
 
#pragma comment(lib, "ws2_32.lib")
 
#define PORT             7890
#define PACKET_LENGTH    1024
#define LOCAL_IP    "127.0.0.1"
 
int main()
{
  // <<< 윈속 초기화 >>>
  WSADATA wsaData;
  WSAStartup(MAKEWORD(2,2), &wsaData);
  
  // <<< Create Socket >>>
  SOCKET hSocket = socket(PF_INET, SOCK_DGRAM, IPPRPTO_UDP);
 
  // <<< sendto 준비 >>>
  SOCKADDR_IN tAddr = {};
  tAddr.sin_family = AF_INET;
  tAddr.sin_port = htons(PORT);
  tAddr.sin_addr.s_addr = inet_addr(LOCAL_IP);
  
  char Packet[PACKET_LENGTH] = {};
  strcpy_s(Packet, "Send from Client");
 
  // <<< sendto >>>
  sendto(hSocket, Packet, strlen(Packet), 0, (SOCKADDR*)&tAddr, sizeof(tAddr));
  
  // <<< recvfrom 준비 >>>
  // 여기서도 서로 송수신을 위해 응답 받을 Dest Address 준비.
  SOCKADDR_IN tDestAddr = {};
  int iDestSize = sizeof(tDestAddr);
  
  // <<< recvfrom >>>
  // (SOCKADDR*)&tDestAddr : 패킷을 전송한 주소가 여기로 들어온다.
  recvfrom(hSocket, Packet, PACKET_LENGTH, 0, (SOCKADDR*)&tDestAddr, &iDestSize);
 
  // 패킷 메시지 출력
  cout << "I am Client : " << Packet << endl;
  
  // <<< close >>>
  // 종료시 사용한 소켓 자원 반환
  closesocket(hSocket);
 
  // 윈속 종료
  WSACleanup();
  
  return 0;
}
cs

 

UDP 통신 실행 결과