Server
- 1_server
: 계산기 서버 초기 구조
: log 메세지를 확인할 때, \n 빼먹으면 문제가 생길 가능성이 크다.
: 프로토콜 설계
: 데이터 파싱
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int calculate(char op, int value1, int value2)
{
switch (op) {
case '+': return value1 + value2;
case '-': return value1 - value2;
case '*': return value1 * value2;
default:
return -1;
}
}
int main()
{
int sock = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr = {0, };
saddr.sin_family = AF_INET;
saddr.sin_port = htons(4000);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
int option = true;
socklen_t optlen = sizeof(option);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
listen(sock, 5);
while (1)
{
struct sockaddr_in caddr = {0, };
socklen_t clen = sizeof(caddr);
int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
char* cip = inet_ntoa(caddr.sin_addr);
printf("Connected from %s\n", cip);
// TCP 는 데이터의 경계가 존재하지 않는다.
//-------------
char buf[1024];
int ret = read(csock, buf, 1024);
printf("ret : %d\n", ret);
if (ret == 0) {
close(csock);
break;
} else if (ret == -1) {
perror("read()");
close(csock);
break;
}
char* p = buf;
char op = *p;
++p;
int value1 = *(int*)p;
p += sizeof(int);
int value2 = *(int*)p;
p += sizeof(int);
printf("%2d%2c%2d\n", value1, op, value2);
int result = calculate(op, value1, value2);
write(csock, &result, sizeof result);
//-----------------
close(csock);
}
close(sock);
}
- 2_server
: 수식 여러번 받아서 계산을 할 수 있는 구조
: tcp는 write와 read가 병합되는 형태의 스트림 단위의 처리방식, 데이터의 경계가 존재하지 않는다.(반면 udp는 패킷 덩어리로 보낸만큼 간다.)
: 100번 보낸다고해서 100번을 받는것이 아니다. -> 패킷을 분리해야한다. -> 무조건 패킷의 길이가 들어가야한다.
: 길이는 약 3배정도 여유있게 잡는 것이 좋다.
: 서버는 온전하게 동작하게 만든 뒤, 고성능 서버로 바꾸는 것!
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int calculate(char op, int value1, int value2)
{
switch (op) {
case '+': return value1 + value2;
case '-': return value1 - value2;
case '*': return value1 * value2;
default:
return -1;
}
}
int main()
{
int sock = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr = {0, };
saddr.sin_family = AF_INET;
saddr.sin_port = htons(4000);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
int option = true;
socklen_t optlen = sizeof(option);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
listen(sock, 5);
while (1)
{
struct sockaddr_in caddr = {0, };
socklen_t clen = sizeof(caddr);
int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
char* cip = inet_ntoa(caddr.sin_addr);
printf("Connected from %s\n", cip);
// TCP 는 데이터의 경계가 존재하지 않는다.
//-------------
// int len;
// int ret = read(csock, &len, sizeof len);
char buf[4096];
int ret = read(csock, buf, sizeof buf);
printf("ret : %d\n", ret);
if (ret == 0) {
close(csock);
break;
} else if (ret == -1) {
perror("read()");
close(csock);
break;
}
while (ret > 0) {
char* p = buf;
int len = *(int*)p;
p += sizeof(int);
char op = *p;
++p;
int value1 = *(int*)p;
p += sizeof(int);
int value2 = *(int*)p;
p += sizeof(int);
ret -= len;
printf("%2d%2c%2d\n", value1, op, value2);
int result = calculate(op, value1, value2);
write(csock, &result, sizeof result);
}
//-----------------
close(csock);
}
close(sock);
}
- 3_server
: 2_server은 위의 패킷(스트림)이 올 경우 9에서 끊어질 수가 있다. -> readn 구현이 필요 -> block이 필요
: TCP의 경계가 모호하거나, 데이터가 도달하지 않는 문제를 해결하기 위해서는 readn을 도입하면 된다.
: ★★readn, tcp 패킷자체에 대한 프로토콜이 잘못될 가능성은 거의 없어진다. 즉, 길이 base로 만들어야한다.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int calculate(char op, int value1, int value2)
{
switch (op) {
case '+': return value1 + value2;
case '-': return value1 - value2;
case '*': return value1 * value2;
default:
return -1;
}
}
// TCP의 경계가 모호하거나, 데이터가 도달하지 않는 문제를
// 해결하기 위해서는 readn을 도입하면 된다.
int readn(int sd, char* buf, int len)
{
int n;
int ret;
n = len;
while (n > 0)
{
ret = read(sd, buf, len);
if (ret < 0) return -1;
if (ret == 0) return len - n;
buf += ret;
n -= ret;
}
return len;
}
int main()
{
int sock = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr = {0, };
saddr.sin_family = AF_INET;
saddr.sin_port = htons(4000);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
int option = true;
socklen_t optlen = sizeof(option);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
listen(sock, 5);
while (1)
{
struct sockaddr_in caddr = {0, };
socklen_t clen = sizeof(caddr);
int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
char* cip = inet_ntoa(caddr.sin_addr);
printf("Connected from %s\n", cip);
while (1)
{
int len;
int ret = readn(csock, (char*)&len, sizeof(len));
if (ret == 0) {
close(csock);
break;
} else if (ret == -1) {
perror("readn()");
close(csock);
break;
}
char buf[4096];
ret = readn(csock, buf, len);
if (ret == 0) {
break;
} else if (ret == -1) {
perror("readn()");
break;
}
// 이제 데이터를 파싱하면 됩니다.
char* p = buf;
char op = *p;
++p;
int value1 = *(int*)p;
p += sizeof(int);
int value2 = *(int*)p;
p += sizeof(int);
printf("%2d%2c%-2d\n", value1, op, value2);
int result = calculate(op, value1, value2);
write(csock, &result, sizeof result);
}
//-----------------
close(csock);
}
close(sock);
}
- 4_server
: 패킷 객체화
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Packet.h"
int calculate(char op, int value1, int value2)
{
switch (op) {
case '+': return value1 + value2;
case '-': return value1 - value2;
case '*': return value1 * value2;
default:
return -1;
}
}
// TCP의 경계가 모호하거나, 데이터가 도달하지 않는 문제를
// 해결하기 위해서는 readn을 도입하면 된다.
int readn(int sd, char* buf, int len)
{
int n;
int ret;
n = len;
while (n > 0)
{
ret = read(sd, buf, len);
if (ret < 0) return -1;
if (ret == 0) return len - n;
buf += ret;
n -= ret;
}
return len;
}
int main()
{
int sock = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr = {0, };
saddr.sin_family = AF_INET;
saddr.sin_port = htons(4000);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
int option = true;
socklen_t optlen = sizeof(option);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
listen(sock, 5);
while (1)
{
struct sockaddr_in caddr = {0, };
socklen_t clen = sizeof(caddr);
int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
char* cip = inet_ntoa(caddr.sin_addr);
printf("Connected from %s\n", cip);
while (1)
{
int len;
int ret = readn(csock, (char*)&len, sizeof(len));
printf("len : %d\n", len);
if (ret != sizeof len) {
close(csock);
break;
} else if (ret == -1) {
perror("readn()");
close(csock);
break;
}
char buf[4096];
ret = readn(csock, buf, len);
if (ret != len) {
break;
} else if (ret == -1) {
perror("readn()");
break;
}
// 이제 데이터를 파싱하면 됩니다.
Packet packet(buf);
char op = packet.getByte();
int value1 = packet.getInt32();
int value2 = packet.getInt32();
printf("%2d%2c%-2d\n", value1, op, value2);
int result = calculate(op, value1, value2);
write(csock, &result, sizeof result);
}
//-----------------
close(csock);
}
close(sock);
}
■ 패킷 클래스
∴ 게임에서는?
: 패킷을 만드는 클래스를 하나 둔다.
: Builder pattern? 객체가 만들어지는 공정은 동일하지만, 내부적으로 표현이 달라지는 형태의 객체를 만들 때 사용(ex. StringBuilder), 코드 반복을 줄일 수 있다.
#ifndef PACKET_H
#define PACKET_H
class Packet
{
public:
inline Packet(char* buf) : start(buf), pos(buf) {}
inline int getInt32() const
{
int ret = *(int*)pos;
pos += sizeof(int);
return ret;
}
inline char getByte() const
{
char ret = *(char*)pos;
++pos;
return ret;
}
inline Packet& putByte(char value)
{
*pos = value;
++pos;
return *this;
}
inline Packet& putInt32(int value)
{
*(int*)pos = value;
pos += sizeof(int);
return *this;
}
inline int build()
{
int len = pos - start;
return len;
}
private:
char* start;
mutable char* pos;
};
#endif
Client
- 1_client
: 계산기 클라이언트 초기 구조
: 패킷 만들기
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <iostream>
using namespace std;
// 계산기 클라이언트를 만들어보자.
// 1. 프로토콜을 설계해야 한다.
// [op:1][lvalue:4][rvalue:4]
int main()
{
int sd = socket(PF_INET,
SOCK_STREAM,
IPPROTO_TCP);
struct sockaddr_in addr = {0, };
addr.sin_family = AF_INET;
addr.sin_port = htons(4000);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = connect(sd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("connect()");
return -1;
}
printf("Connection succeed!!\n");
//-------------
char op = '+';
int value1 = 10, value2 = 20;
// cout << "value 1: "; cin >> value1;
// cout << "value 2: "; cin >> value2;
// cout << "op : "; cin >> op;
// 패킷을 만들어야 한다.
char buf[1024];
char* p = buf;
*p = op;
++p;
*(int*)p = value1;
p += sizeof(int);
*(int*)p = value2;
p += sizeof(int);
int size = p - buf;
for (int i = 0 ; i < 100; i++) {
write(sd, buf, size);
}
for (int i = 0 ; i < 100; i++) {
int result;
int n = read(sd, &result, sizeof result);
cout << i+1 << "- result : " << result << endl;
}
close(sd);
}
- 2_client ~ 3_client
: 수식을 여러번 보내서, 여러 결과값을 받을 수 있는 구조
: 패킷의 길이까지 붙여서 패킷 만들기
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <iostream>
using namespace std;
// 계산기 클라이언트를 만들어보자.
// 1. 프로토콜을 설계해야 한다.
// [op:1][lvalue:4][rvalue:4]
int main()
{
int sd = socket(PF_INET,
SOCK_STREAM,
IPPROTO_TCP);
struct sockaddr_in addr = {0, };
addr.sin_family = AF_INET;
addr.sin_port = htons(4000);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = connect(sd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("connect()");
return -1;
}
printf("Connection succeed!!\n");
//-------------
char op = '+';
int value1 = 10, value2 = 20;
// cout << "value 1: "; cin >> value1;
// cout << "value 2: "; cin >> value2;
// cout << "op : "; cin >> op;
// 패킷을 만들어야 한다.
char buf[1024];
char* p = buf;
*p = op;
++p;
*(int*)p = value1;
p += sizeof(int);
*(int*)p = value2;
p += sizeof(int);
int size = p - buf;
for (int i = 0 ; i < 1000; i++) {
write(sd, &size, sizeof size);
write(sd, buf, size);
}
for (int i = 0 ; i < 1000; i++) {
int result;
int n = read(sd, &result, sizeof result);
cout << i+1 << "- result : " << result << endl;
}
close(sd);
}
- 4_client
: 패킷 클래스로 만들기
: 체이닝 방식 권장
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <iostream>
using namespace std;
#include "Packet.h"
int main()
{
int sd = socket(PF_INET,
SOCK_STREAM,
IPPROTO_TCP);
struct sockaddr_in addr = {0, };
addr.sin_family = AF_INET;
addr.sin_port = htons(4000);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = connect(sd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("connect()");
return -1;
}
printf("Connection succeed!!\n");
//-------------
char op = '+';
int value1 = 10, value2 = 20;
// 패킷을 만들어야 한다.
char buf[1024];
// 체이닝 방식 - 완전한 객체로
Packet packet = Packet(buf).putByte(op)
.putInt32(value1)
.putInt32(value2);
// vs
// 불완전한 객체로 존재
// Packet packet(buf);
// packet.putByte(op);
// packet.putInt32(value1);
// packet.putInt32(value2);
int size = packet.build();
/*
char* p = buf;
*p = op;
++p;
*(int*)p = value1;
p += sizeof(int);
*(int*)p = value2;
p += sizeof(int);
int size = p - buf;
*/
for (int i = 0 ; i < 10000; i++) {
write(sd, &size, sizeof size);
write(sd, buf, size);
}
for (int i = 0 ; i < 10000; i++) {
int result;
int n = read(sd, &result, sizeof result);
cout << i+1 << "- result : " << result << endl;
}
close(sd);
}