지난 글에서 크로스 플랫폼으로 빌드를 하려면 어떻게 해야 하는지 알아봤습니다. 이번 글에서는 리눅스와 윈도우에서 하나의 코드로 작동할 수 있는 소켓을 만들어 보도록 하겠습니다. 😄
🌳 구성#
1
2
3
4
5
6
7
8
9
10
| .
├── include/
│ └── yami/
│ └── comm/
│ └── socket.hpp # 헤더 (interface)
└── src/
└── comm/
├── socket.cpp # 리눅스, 윈도우 공통 구현
├── socket_linux.cpp # 리눅스 구현부
└── socket_windows.cpp # 윈도우 구현부
|
⚠️ Warning
아래 코드 내용은 전체의 일부분이며 시간이 지남에 따라 수정되었을 수 있습니다. 이곳에서 최신의 전체 코드를 보실 수 있습니다.
CMake#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| # CMakeLists.txt
add_executable(test_socket
src/ds/ring_buffer.cpp
src/utility/event_loop.cpp
src/test_socket.cpp
src/comm/socket.cpp
src/comm/server.cpp
)
if(WIN32)
target_link_libraries(test_socket PRIVATE ws2_32)
target_sources(test_socket PRIVATE
src/utility/event_loop_windows.cpp
src/comm/server_windows.cpp
src/comm/socket_windows.cpp
)
elseif(UNIX)
target_sources(test_socket PRIVATE
src/utility/event_loop_linux.cpp
src/comm/socket_linux.cpp
src/comm/server_linux.cpp
)
endif()
|
CMake에서 위와 같은 방법으로 플랫폼을 나눌 수 있습니다.
🌟 헤더#
윈도우와 리눅스는 소켓을 각각 SOCKET, int 타입으로 사용하고 있습니다. 이러한 데이터 타입을 하나로 통일시키기 위해 typedef 키워드로 먼저 통일을 시켜주었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // socket.hpp
#if defined(_WIN32)
#include <winsock2.h>
typedef SOCKET Socket_t;
#define YAMI_COMM_INVALID_SOCKET INVALID_SOCKET
#define YAMI_COMM_SOCKET_ERROR SOCKET_ERROR
#define YAMI_COMM_SUCCESS (0)
#elif defined(__linux__)
#include <sys/socket.h>
typedef int Socket_t;
#define YAMI_COMM_INVALID_SOCKET (-1)
#define YAMI_COMM_SOCKET_ERROR (-1)
#define YAMI_COMM_ERROR (-1)
#endif
// 만약 운영체제 별 상수 값이 다르면 위에서 재정의
#define YAMI_COMM_AF_INET AF_INET
#define YAMI_COMM_AF_INET6 AF_INET6
#define YAMI_COMM_SOCK_STREAM SOCK_STREAM
#define YAMI_COMM_SOCK_DGRAM SOCK_DGRAM
#define YAMI_COMM_SOCK_RAW SOCK_RAW
|
플랫폼을 전처리기로 나누고, 각 플랫폼에서 사용하는 타입을 Socket_t로 통일 시켜준 모습입니다. 또한 소켓을 생성할 때 사용하는 도메인이나 타입 매크로도 해당 프로젝트에서 하나로 사용할 수 있도록 다시 정의했습니다.
아래에는 통일시킨 타입과 매크로 정의로 작성한 소켓 클래스입니다.
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
| namespace Yami {
namespace Comm {
constexpr static std::size_t YAMI_COMM_BUFFER_SIZE = 1024; // 16k
enum class YAMI_SOCKET_DOMAIN {
UNKNOWN = -1,
IPv4 = YAMI_COMM_AF_INET,
IPv6 = YAMI_COMM_AF_INET6,
};
enum class YAMI_SOCKET_TYPE {
UNKNOWN = -1,
TCP = YAMI_COMM_SOCK_STREAM,
UDP = YAMI_COMM_SOCK_DGRAM,
RAW = YAMI_COMM_SOCK_RAW,
};
class Socket {
public:
Socket(Socket_t = YAMI_COMM_INVALID_SOCKET);
~Socket();
// prevent copy
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
// allow move
Socket(Socket&&);
Socket& operator=(Socket&&);
// return native socket
operator Socket_t();
bool valid();
bool init(YAMI_SOCKET_DOMAIN, YAMI_SOCKET_TYPE);
void destroy();
bool setNonblocking();
DS::RingBuffer& buffer();
std::size_t send(const char*, const std::size_t);
YAMI_SOCKET_DOMAIN getDomain();
YAMI_SOCKET_TYPE getType();
Yami::Utility::Callback<Socket&> onError;
private:
Socket_t _socket;
YAMI_SOCKET_DOMAIN _domain;
YAMI_SOCKET_TYPE _type;
DS::RingBuffer _buffer;
};
} // namespace Comm
} // namespace Yami
|
💡 Info
소켓의 초기화, 해제, 그리고 수신 데이터를 받을 수 있는 버퍼로 구성해서 클래스로 만들었습니다.
다음 글에서 운영체제별로 다른 비동기 IO를 핸들링할 수 있는 객체도 만들 예정입니다.
🚀 구현부#
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
| // socket.cpp
#include "yami/comm/socket.hpp"
#include "yami/ds/ring_buffer.hpp"
#include <utility>
namespace Yami {
namespace Comm {
Socket::Socket(Socket_t nativeSocket)
: _socket(nativeSocket)
, _buffer(YAMI_COMM_BUFFER_SIZE) {}
Socket::~Socket() {
destroy();
}
Socket::Socket(Socket&& other)
: _socket(std::exchange(other._socket, YAMI_COMM_INVALID_SOCKET))
, _domain(std::exchange(other._domain, Yami::Comm::YAMI_SOCKET_DOMAIN::UNKNOWN))
, _type(std::exchange(other._type, Yami::Comm::YAMI_SOCKET_TYPE::UNKNOWN))
, _buffer(std::move(other._buffer)) {}
Socket& Socket::operator=(Socket&& other) {
if(this != &other) {
destroy();
_socket = std::exchange(other._socket, YAMI_COMM_INVALID_SOCKET);
_domain = std::exchange(other._domain, Yami::Comm::YAMI_SOCKET_DOMAIN::UNKNOWN);
_type = std::exchange(other._type, Yami::Comm::YAMI_SOCKET_TYPE::UNKNOWN);
_buffer = std::move(other._buffer);
}
return *this;
}
Socket::operator Socket_t() {
return _socket;
}
bool Socket::valid() {
return _socket != YAMI_COMM_INVALID_SOCKET;
}
DS::RingBuffer& Socket::buffer() { return _buffer; }
YAMI_SOCKET_DOMAIN Socket::getDomain() { return _domain; }
YAMI_SOCKET_TYPE Socket::getType() { return _type; }
} // namespace Comm
} // namespace Yami
|
💡 info
리눅스와 윈도우 둘 다 같이 사용할 수 있는 구현을 socket.cpp에 구현했습니다. 이동 생성자와 이동 대입 연산자, 버퍼 정도 같이 사용할 수 있었습니다.
리눅스#
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
| // socket_linux.cpp
#include "yami/comm/socket.hpp"
#include <cerrno>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
namespace Yami {
namespace Comm {
bool Socket::init(YAMI_SOCKET_DOMAIN domain, YAMI_SOCKET_TYPE type) {
// aleady initialized
if(valid()) return false;
_socket = socket(
static_cast<int>(domain),
static_cast<int>(type),
0
);
if(!valid()) return false;
_domain = domain;
_type = type;
return true;
}
void Socket::destroy() {
if(valid()) {
close(_socket);
_socket = YAMI_COMM_INVALID_SOCKET;
}
}
bool Socket::setNonblocking() {
if(!valid()) return false;
int flags = fcntl(_socket, F_GETFL, 0);
if(!(flags != YAMI_COMM_ERROR && fcntl(_socket, F_SETFL, flags | O_NONBLOCK) != YAMI_COMM_ERROR)) return false;
return true;
}
std::size_t Socket::send(const char* data, const std::size_t size) {
std::size_t result = 0;
ssize_t bytes = 0;
while(result < size) {
bytes = ::send(_socket, data + result, size - result, 0);
if(bytes > 0) {
result += bytes;
} else {
// TODO: 애러 처리
if(errno == EAGAIN || errno == EWOULDBLOCK) {
// 남은 데이터 보관 EPOLLOUT 대기
onError(*this);
destroy();
} else if(errno == EINTR) {
// 재시도
continue;
} else {
onError(*this);
destroy();
}
}
}
return result;
}
} // namespace Comm
} // namespace Yami
|
💡 Info
TCP는 send()호출 시 요청한 데이터가 한 번에 전송된다는 보장이 없어서 실제 처리한 바이트 수를 반환받아서 확인해야 합니다!
윈도우#
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
| // socket_windows.cpp
#include "yami/comm/socket.hpp"
#include "yami/utility/event_loop.hpp"
#undef min
#include <cstring>
#include <algorithm>
namespace Yami {
namespace Comm {
bool WSAHelper(bool);
struct SendContext {
OVERLAPPED ov;
Yami::Utility::EventType type;
WSABUF wsabuf;
char data[1024];
};
bool Socket::init(YAMI_SOCKET_DOMAIN domain, YAMI_SOCKET_TYPE type) {
// aleady initialized
if(valid()) return false;
if(!WSAHelper(true)) return false;
_socket = socket(
static_cast<int>(domain),
static_cast<int>(type),
0
);
if(!valid()) return false;
_domain = domain;
_type = type;
return true;
}
void Socket::destroy() {
if(valid()) {
closesocket(_socket);
_socket = YAMI_COMM_INVALID_SOCKET;
WSAHelper(false);
}
}
bool Socket::setNonblocking() {
u_long nonblocking = 1;
return ioctlsocket(_socket, FIONBIO, &nonblocking) == YAMI_COMM_SUCCESS;
}
std::size_t Socket::send(const char* data, const std::size_t size) {
std::size_t send = 0;
DWORD bytesSent = 0;
int result = YAMI_COMM_SOCKET_ERROR;
while(send < size) {
// 현재 사이클에서 보내야 할 데이터의 사이즈
send += std::min(size - send, YAMI_COMM_BUFFER_SIZE);
SendContext* ctx = new SendContext;
std::memset(&ctx->ov, 0, sizeof(OVERLAPPED));
std::memcpy(ctx->data, data, send);
ctx->wsabuf.buf = ctx->data;
ctx->wsabuf.len = static_cast<ULONG>(send);
DWORD flags = 0;
result = WSASend(
_socket,
&ctx->wsabuf,
1,
&bytesSent,
flags,
&ctx->ov,
nullptr
);
if(result == YAMI_COMM_SOCKET_ERROR) {
const int err = WSAGetLastError();
if(err != WSA_IO_PENDING) {
onError(*this);
destroy();
return 0;
}
}
}
return send;
}
bool WSAHelper(bool init) {
static std::size_t count = 0;
if(init && count == 0) {
WSADATA wsaData{};
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if(result != YAMI_COMM_SUCCESS) {
return false;
}
} else if(!init && count == 1) {
WSACleanup();
}
init ? count++ : count--;
return count > 0;
}
} // namespace Comm
} // namespace Yami
|
💡 Info
헤더 파일에 선언되지 않은 구조체나 함수는 .cpp 파일 내에서만 사용 가능 👍
두 플랫폼 비교#
| 구분 | 리눅스 | 윈도우 | 비고 |
|---|
| 사전 작업 | 없음 | WSAStartup()/WSACleanup() | 윈도우는 Winsock을 사용하기 위해 준비 작업이 필요함 위 코드에서 WSAHelper() 함수로 관리 |
| 소켓 생성 | socket() | socket() | 옵션은 플랫폼마다 다르지만, 기본적으로 생성하는 방식은 같음 |
| 해제 | close() | closesocket() | 리눅스: 소켓도 하나의 파일처럼 사용해서 파일을 닫는 함수로 해제 윈도우: 소켓과 파일(핸들)을 구분하여 사용 |
| 소켓 옵션 | setsockopt() | ioctlsocket() | 비동기IO로 전환하기 위해 사용하는데 두 플랫폼 간에 사용하는 함수 다름 |
| 비동기IO | epoll | IOCompletionPort (IOCP) | 리눅스: IO 준비가 되면 신호 전달(신호가 오면 직접 소켓 버퍼에서 데이터를 꺼내야 함) 윈도우: IO 데이터를 이미 보내거나 받고 신호 전달(신호를 받으려면 미리 소켓과 버퍼를 예약해야 함) |
📝 마무리#
두 플랫폼에서 소켓을 하나의 소스 코드로 사용하기 위해 CMake와 전처리기, typedef를 이용해서 구현까지 해봤습니다. 다음 글에서는 본격적으로 서버 객체를 만들어 통신하기 전 각 플랫폼에서 비동기IO를 핸들링하기 위한 EventLoop 객체를 만들어 보도록 하겠습니다. 😎