응용 프로그램 프로토콜
▶응용 프로그램 수준에서 주고받는 데이터의 형식과 의미, 처리 방식을 정의한 프로토콜
데이터 전송 시 고려할 사항
1. 메시지 경계 구분
a) 송신자는 항상 고정 길이 데이터를 보내고,
수신자는 항상 고정 길이 데이터를 읽는 경우
- 구현이 쉬움
- 짧은 데이터 전송 시 고정 길이만큼 보내지 않아서 불필요한 자원 낭비가 발생함
- 프로토콜 변경이 어려움
b) 송신자는 가변 길이 데이터를 보내면서 끝부분에 EOR (End Of Record)를 붙임,
수신자는 EOR이 나올 때까지 데이터를 읽음
- 데이터 길이를 알 수 없을 때 효과적임
- 송신자는 데이터 전송 후 마지막으로 EOR 전송
- 데이터 길이를 모르기 때문에 EOR이 나올 때까지 하나씩 읽어가야함, 성능문제가 생김
c) 송신자는 보낼 데이터 크기를 고정 길이 데이터로 보내고 이어서 가변 길이 데이터를 보냄,
수신자는 고정 길이 데이터를 읽어서 뒤따라올 가변 데이터의 길이를 알아내고 이 길이만큼 데이터를 읽음
- 처리가 쉽고 효율적
- retval = recv(client_sock, buf, BUFSIZE, MSG_WAITALL); 을 두 번 호출하여 모든 데이터를 읽을 수 있음
d) 송신자는 가변 길이 데이터 전송한 후 연결을 정상 종료
수신자는 recv() 함수의 리턴값이 0(정상 종료)이 될 때까지 데이터를 읽음
- 한 쪽에서 일방적으로 가변 길이 데이터를 보낼 때 적합
- 비효울적 : <연결설정 - 전송 -연결종료> 를 반복해야 함
2. 바이트 정렬
이기종 시스템 간 데이터 교환 시 바이트 정렬 방식이 통일 되어야 함
주로 빅 엔디언 방식을 추천
3. 구조체 멤버 맞춤
a □□□□
b □
c □□□□
d □
컴파일러가 속도를 높이기 위해 char형 b, d를 1byte가 아닌, 4byte로 만듦
a □□□□
b □□□□
c □□□□
d □□□□
a,b,c,d 총 16바이트 하나의 큰 박스처럼 만듦
#pragma pack(1) 은 4바이트로 안 읽고 1바이트는 1바이트로 읽겠다는 선언
a □□□□ 4byte
b □ 1byte
c □□□□ 4byte
d □ 1byrte
a,b,c,d 총 10byte
#pragma pack(2) 은 2byte로 읽겠다
a □□□□ 4byte
b □□ 2byte
c □□□□ 4byte
d □□ 2byrte
a,b,c,d 총 12byte
다양한 데이터 전송 방식
고정 길이 데이터 전송
▶서버와 클라이언트 모두 크기가 같은 버퍼를 정의해두고 데이터를 주고 받음
실습 고정길이 데이터 전송 - 서버
#include "../../common.h"
#include <string.h>
#define SERVERPORT 9000
#define BUFSIZE 50
int main(int argc, char* argv[])
{
int retval = -1;
// initialize winsock
WSADATA wsa;
if (0 != WSAStartup(MAKEWORD(2, 2), &wsa))
return 1;
// create socket
SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == listen_sock)
err_quit("socket()");
// bind socket
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(SERVERPORT);
retval = bind(listen_sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (retval == SOCKET_ERROR)
err_quit("bind()");
// listen
retval = listen(listen_sock, SOMAXCONN);
if (SOCKET_ERROR == retval)
err_quit("listen()");
// data communication
SOCKET client_sock;
struct sockaddr_in clientaddr;
int addrlen;
char buf[BUFSIZE + 1] = { 0, };
while (1)
{
// accept()
addrlen = sizeof(clientaddr);
client_sock = accept(listen_sock, (struct sockaddr*)&clientaddr, &addrlen);
if (INVALID_SOCKET == client_sock)
{
err_display("accept()");
break;
}
// print the client information
char addr[INET_ADDRSTRLEN] = { 0, };
inet_ntop(AF_INET, &clientaddr.sin_addr, addr, sizeof(addr));
printf("\n[TCP Server] Client IP = %s, port = %d\n", addr, ntohs(clientaddr.sin_port));
// send/recv data
while (1)
{
// recv
retval = recv(client_sock, buf, BUFSIZE, MSG_WAITALL); //recv flag를 MSG_WAITALL로 줘서 버퍼크기 50까지 계속받음
// recv error
if (SOCKET_ERROR == retval)
{
err_display("recv()");
break;
}
// no data received
else if (0 == retval)
break;
// print the received data
buf[retval] = '\0';
printf("[TCP/%s:%d] %s\n", addr, ntohs(clientaddr.sin_port), buf);
// closing condition
retval = strncmp(buf, "99", sizeof(buf));
if (0 == retval)
break;
}
// 소켓 닫기
closesocket(client_sock);
printf("[TCP server] client close: IP = %s, port =%d\n", addr, ntohs(clientaddr.sin_port));
}
// close socket
closesocket(listen_sock);
// close winsock
WSACleanup();
return 0;
}
고정 길이 데이터 전송 - 클라이언트
#include "../../common.h"
#include <string.h>
char* SERVERIP = (char*)"127.0.0.1";
#define SERVERPORT 9000
#define BUFSIZE 50
int main(int argc, char* argv[])
{
int retval;
// 명령행 인수가 있으면 IP주소로 사용
// get IP address from users
if (argc > 1)
SERVERIP = argv[1];
// 윈속 초기화
WSADATA wsa;
if (0 != WSAStartup(MAKEWORD(2, 2), &wsa))
return 1;
// 소켓 생성
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == sock)
err_quit("socket()");
// connet()
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, SERVERIP, &serveraddr.sin_addr);
serveraddr.sin_port = htons(SERVERPORT);
retval = connect(sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));)
if (SOCKET_ERROR == retval)
err_quit("connect()");
// buf
// 데이터 통신에 사용할 변수
char buf[BUFSIZE];
const char* testdata[] = {
"안녕하세요",
"반가워요",
"오늘따라 할 이야기가 많을 것 같네요",
"저도 그렇네요",
};
// 서버와 데이터 통신
for (int i = 0; i < 4; i++) {
// 데이터 입력(시뮬레이션)
memset(buf, '#', sizeof(buf));
strncpy(buf, testdata[i], strlen(testdata[i]));
// 데이터 보내기
retval = send(sock, buf, BUFSIZE, 0);
if (retval == SOCKET_ERROR) {
err_display("send()");
break;
}
printf("[TCP 클라이언트] %d바이트를 보냈습니다.\n", retval);
}
// 소켓 닫기
closesocket(sock);
// 윈속 종료
WSACleanup();
return 0;
}
가변 길이 데이터 전송
▶가변 길이 데이터 경계를 구분하기 위해 EOR로 사용할 데이터 패턴을 정해야 함
흔히 '\n' or \r \n'을 사용
ex) '\n' 을 검출하는 가상 코드
*안쓰는 것이 좋음 -> 성능저하
고정길이 + 가변길이 데이터 전송
▶송신 측에서 가변 길이 데이터의 크기를 미리 계산할 수 있다면 <고정길이 + 가변길이 데이터> 전송이 효과적임
▶수신 측에서는
1) 고정 길이 데이터 수신
2) 가변 길이 데이터 수신
두 번의 데이터 읽기 작업으로 가변 길이 데이터의 경계를 구분해 읽을 수 있음
고정길이 + 가변길이 실습
1) client
#include "../../common.h"
#include <string.h>
char* SERVERIP = (char*)"127.0.0.1";
#define SERVERPORT 9000
#define BUFSIZE 50
int main(int argc, char* argv[])
{
int retval = -1;
// get IP address from users
if (argc > 1)
SERVERIP = argv[1];
// initialize winsock
WSADATA wsa;
if (0 != WSAStartup(MAKEWORD(2, 2), &wsa))
return 1;
// create socket
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == sock)
err_quit("socket()");
// connect()
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, SERVERIP, &serveraddr.sin_addr);
serveraddr.sin_port = htons(SERVERPORT);
retval = connect(sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (SOCKET_ERROR == retval)
err_quit("connect()");
// variables declaration
char buf[BUFSIZE + 1] = { 0, };
const char* testdata[] = {
"안녕하세요",
"반가워요",
"오늘따라 할 이야기가 많을 것 같네요",
"저도 그렇네요"
};
int len = 0;
for (int i = 0; i < 4; i++)
{
len = (int)strlen(testdata[i]);
// 서버에 사이즈를 정수로 보냄
// 첫 데이터 10byte를 정수 10으로 보냄
strncpy(buf, testdata[i], len);
retval = send(sock, (char*)&len, sizeof(int), 0);
if (SOCKET_ERROR == retval)
{
err_display("send()");
break;
}
retval = send(sock, buf, len, 0);
if (SOCKET_ERROR == retval)
{
err_display("send()");
break;
}
printf("[TCP Client] %d bytes has been sent.\n", retval);
}
// close socket
closesocket(sock);
// close socket
WSACleanup();
return 0;
}
2) Server
#include "../../common.h"
#include <string.h>
#define SERVERPORT 9000
#define BUFSIZE 512
int main(int argc, char* argv[])
{
int retval = -1;
// initialize winsock
WSADATA wsa;
if (0 != WSAStartup(MAKEWORD(2, 2), &wsa))
return 1;
// create socket
SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == listen_sock)
err_quit("socket()");
// bind socket
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(SERVERPORT);
retval = bind(listen_sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (retval == SOCKET_ERROR)
err_quit("bind()");
// listen
retval = listen(listen_sock, SOMAXCONN);
if (SOCKET_ERROR == retval)
err_quit("listen()");
// data communication
SOCKET client_sock;
struct sockaddr_in clientaddr;
int addrlen;
int len = 0; // data length of the fixed parts
char buf[BUFSIZE + 1] = { 0, }; // data length of the variable parts
while (1)
{
// accept()
addrlen = sizeof(clientaddr);
client_sock = accept(listen_sock, (struct sockaddr*)&clientaddr, &addrlen);
if (INVALID_SOCKET == client_sock)
{
err_display("accept()");
break;
}
// print the client information
char addr[INET_ADDRSTRLEN] = { 0, };
inet_ntop(AF_INET, &clientaddr.sin_addr, addr, sizeof(addr));
printf("\n[TCP Server] Client IP = %s, port = %d\n", addr, ntohs(clientaddr.sin_port));
// send/recv data
while (1)
{
// receive a header
retval = recv(client_sock, (char*)&len, sizeof(int), MSG_WAITALL);
// 포인터에 &변수 하면 결국 len이 됨
// &이 주소를 의미하니까 거기에 포인터면 결국 len임
// recv error
if (SOCKET_ERROR == retval)
{
err_display("recv()");
break;
}
else if (0 == retval)
break;
// receive data
retval = recv(client_sock, buf, len, MSG_WAITALL);
// recv error
if (SOCKET_ERROR == retval)
{
err_display("recv()");
break;
}
// no data received
else if (0 == retval)
break;
// print the received data
buf[retval] = '\0';
printf("[TCP/%s:%d] %s\n", addr, ntohs(clientaddr.sin_port), buf);
// closing condition
retval = strncmp(buf, "99", sizeof(buf));
if (0 == retval)
break;
}
// 소켓 닫기
closesocket(client_sock);
printf("[TCP server] client close: IP = %s, port =%d\n", addr, ntohs(clientaddr.sin_port));
}
// close socket
closesocket(listen_sock);
// close winsock
WSACleanup();
return 0;
}
다양한 데이터 전송 방식
-> 암호화 통신 - 하이브리드 암호화
1) C→S 접속 및 공개키 요청
2) S→C 공개키 전송
3) C 난수 생성 후 서버의 공개키로 암호화
4) C→S 암호화된 난수 전송
5) S 자신의 개인키로 복호화
6) S, C 공유한 난수를 기반으로 세션키 유도 (KDF, 키 유도 함수)
7) S→C ACK전송
8) C→S 대칭키 암호 알고리즘을 이용하여 데이터 암호화 후 전송
9) S 수신 후 복호화
*C = Client , S = Server
프로토콜 설계
Packet = Delimiter + Header + payload
프로토콜의 확장성이 있는가?
STX | var | Type | Length | payload | ETX |
var을 추가하여 => 프로토콜을 확장
'코딩 > Network' 카테고리의 다른 글
[네트워크] 스레드 API, 스레드 제어 (0) | 2023.04.26 |
---|---|
[네트워크] 멀티스레드 (0) | 2023.04.15 |
[네트워크] TCP 서버-클라이언트 분석 (0) | 2023.04.15 |
[네트워크] TCP 서버-클라이언트 (0) | 2023.04.15 |
[네트워크] DNS와 도메인, IP 주소 변환 함수 (0) | 2023.04.15 |