간단하고 현대적인 C++ 소켓 라이브러리입니다.
이는 다른 언어에서 친숙한 개념인 socket
, acceptor,
및 connector
클래스를 사용하는 버클리 소켓 라이브러리에 대한 상당히 낮은 수준의 C++ 래퍼입니다.
기본 socket
클래스는 시스템 소켓 핸들을 래핑하고 수명을 유지합니다. C++ 개체가 범위를 벗어나면 기본 소켓 핸들을 닫습니다. 소켓 객체는 일반적으로 이동할 수 있지만 복사할 수 는 없습니다. std::move()
사용하여 소켓을 한 범위(또는 스레드)에서 다른 범위(또는 스레드)로 전송할 수 있습니다.
라이브러리는 현재 Linux, Mac 및 Windows에서 IPv4 및 IPv6를 지원합니다. 다른 *nix 및 POSIX 시스템은 수정이 거의 또는 전혀 없이 작동합니다.
Unix-Domain 소켓은 OS 구현이 있는 *nix 시스템에서 사용할 수 있습니다.
OpenSSL 또는 MbedTLS 라이브러리를 사용하는 보안 소켓에 대한 지원이 최근 기본 적용 범위에 추가되었습니다. 이는 앞으로도 계속 확대될 예정입니다.
또한 SocketCAN 패키지를 사용하여 Linux에서 CAN 버스 프로그래밍을 위한 실험적 지원도 있습니다. 이는 CAN 버스 어댑터에 CAN 메시지 프로토콜에 의해 규정된 제한 사항이 있는 네트워크 인터페이스를 제공합니다.
라이브러리의 모든 코드는 sockpp
C++ 네임스페이스 내에 있습니다.
'마스터' 브랜치는 v2.0 API로의 전환을 시작하고 있으며 현재 특히 불안정합니다. 일반적인 용도로 사용하려면 최신 릴리스를 다운로드하는 것이 좋습니다.
버전 1.0이 출시되었습니다!
현재 개발 브랜치에 주요 변경 사항이 쌓이기 시작하면서 지난 몇 년간 꽤 안정적인 API를 1.0으로 출시하기로 결정했습니다. 이것은 최신 v0.8.x 라인의 것입니다. 그러면 앞으로의 상황이 덜 혼란스러워지고 v1.x 분기를 유지할 수 있게 됩니다.
버전 2.0 개발이 진행 중입니다.
PR #17에서 소개된 "상태 비저장" I/O 작업(완전히 병합되지 않은)에 대한 아이디어는 result<T>
클래스와 함께 2.0 API에 제공됩니다. 이는 std::error_code
로 표시되는 오류가 있는 "성공" 반환 유형에 대한 일반적인 것입니다. 이는 오류 추적 및 보고와 관련된 플랫폼 문제를 크게 줄이는 데 도움이 됩니다.
균일한 결과 유형을 사용하면 생성자를 제외한 대부분의 함수에서 예외가 필요하지 않습니다. 함수가 던질 수 있는 경우, 던지기 대신 오류 코드 매개변수를 설정할 수 있는 유사한 noexcept
함수도 제공됩니다. 따라서 응용 프로그램에서 원하는 경우 예외 없이 라이브러리를 사용할 수 있습니다.
시스템 오류로 인해 실패할 수 있는 모든 함수는 결과를 반환합니다. 그러면 "마지막 오류"가 필요하지 않으므로 socket
클래스에 캐시된 마지막 오류 변수가 사라집니다. 그런 다음 소켓 클래스는 소켓 핸들만 래핑하므로 핸들을 공유할 수 있는 것과 동일한 방식으로 스레드 간에 공유하는 것이 더 안전해집니다. 일반적으로 하나의 스레드는 읽기용이고 다른 스레드는 쓰기용입니다.
일부 작업에서는 OpenSSL 또는 MbedTLS 라이브러리를 사용하거나 (아마도) 둘 중 하나에 대한 빌드 타임 선택을 사용하여 보안 소켓을 라이브러리의 2.x 릴리스에 통합하기 시작했습니다. 몇 년 동안 휴면 상태였던 PR #17은 OpenSSL과 비슷한 작업을 수행하기 위한 새로운 작업과 함께 병합 및 업데이트되고 있습니다. sockpp
빌드할 때 보안 라이브러리 중 하나를 선택할 수 있습니다.
2.0 버전은 C++17 및 CMake v3.12 이상으로 이전됩니다.
이 프로젝트에 대한 최신 공지 사항을 확인하려면 다음 주소에서 저를 팔로우하세요.
마스토돈: @[email protected]
트위터: @fmpagliughi
이 라이브러리를 사용하고 있다면 저에게 트윗을 보내거나 메시지를 보내서 어떻게 사용하고 있는지 알려주세요. 항상 어디서 나오는지 궁금하네요!
설치된 라이브러리는 일반적으로 find_package(sockpp)
사용하여 검색할 수 있습니다. 이는 Sockpp
네임스페이스와 sockpp
라이브러리 이름을 사용합니다.
간단한 CMakeLists.txt 파일은 다음과 같습니다.
cmake_minimum_required(VERSION 3.12)
project(mysock VERSION 1.0.0)
find_package(sockpp REQUIRED)
add_executable(mysock mysock.cpp)
target_link_libraries(mysock Sockpp::sockpp)
기여가 인정되고 높이 평가됩니다. 새롭고 불안정한 작업이 develop
브랜치에서 수행됩니다. 마스터가 아닌 해당 브랜치에 대한 모든 풀 요청을 제출하십시오.
자세한 내용은 CONTRIBUTING.md를 참조하세요.
CMake는 지원되는 빌드 시스템입니다.
기본 옵션으로 빌드하려면:
$ cd sockpp
$ cmake -Bbuild .
$ cmake --build build/
설치하려면:
$ cmake --build build/ --target install
라이브러리에는 CMake를 통해 정적 라이브러리나 공유(동적) 라이브러리 생성 중 하나 또는 둘 다를 선택할 수 있는 여러 빌드 옵션이 있습니다. 또한 예제 옵션을 빌드할 수 있으며 Doxygen이 설치되어 있으면 문서를 만드는 데 사용할 수 있습니다.
변하기 쉬운 | 기본값 | 설명 |
---|---|---|
SOCKPP_BUILD_SHARED | 에 | 공유 라이브러리 구축 여부 |
SOCKPP_BUILD_STATIC | 끄다 | 정적 라이브러리 구축 여부 |
SOCKPP_BUILD_DOCUMENTATION | 끄다 | HTML 기반 API 문서 생성 및 설치( Doxygen 필요) |
SOCKPP_BUILD_EXAMPLES | 끄다 | 예제 프로그램 구축 |
SOCKPP_BUILD_TESTS | 끄다 | 단위 테스트 빌드( Catch2 필요) |
SOCKPP_WITH_CAN | 끄다 | SocketCAN 지원을 포함합니다. (리눅스에만 해당) |
CMake 구성 명령에서 '-D' 스위치를 사용하여 이를 설정합니다. 예를 들어 문서와 예제 앱을 빌드하려면 다음을 수행하세요.
$ cd sockpp
$ cmake -Bbuild -DSOCKPP_BUILD_DOCUMENTATION=ON -DSOCKPP_BUILD_EXAMPLES=ON .
$ cmake --build build/
보안 소켓 지원으로 라이브러리를 구축하려면 지원을 제공하기 위해 TLS 라이브러리를 선택해야 합니다. 현재 OpenSSL 또는 MbedTLS를 사용할 수 있습니다.
빌드를 구성할 때 다음 중 하나를 선택하십시오.
변하기 쉬운 | 기본값 | 설명 |
---|---|---|
SOCKPP_WITH_MBEDTLS | 끄다 | MbedTLS를 사용한 보안 소켓 |
SOCKPP_WITH_OPENSSL | 끄다 | OpenSSL을 사용한 보안 소켓 |
sockpp
라이브러리는 현재 MbedTLS v3.3을 지원합니다. 해당 라이브러리를 빌드할 때 구성 파일 include/mbedtls/mbedtls_config.h 에 다음 구성 옵션을 정의해야 합니다.
#define MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK
스레딩을 지원하려면:
#define MBEDTLS_THREADING_PTHREAD
#define MBEDTLS_THREADING_C
CMake 빌드 옵션을 설정합니다.
LINK_WITH_PTHREAD:BOOL=ON
구성 파일의 옵션은 이미 파일에 있어야 하지만 기본적으로 주석 처리되어 있습니다. 주석을 해제하고 저장한 후 빌드하면 됩니다.
sockpp
OpenSSL 래퍼는 현재 OpenSSL v3.0으로 구축 및 테스트되고 있습니다.
TCP 및 기타 "스트리밍" 네트워크 애플리케이션은 일반적으로 서버 또는 클라이언트로 설정됩니다. Acceptor는 TCP/스트리밍 서버를 생성하는 데 사용됩니다. 주소를 바인딩하고 알려진 포트에서 수신하여 들어오는 연결을 수락합니다. 연결이 수락되면 새로운 스트리밍 소켓이 생성됩니다. 해당 새 소켓은 직접 처리되거나 처리를 위해 스레드(또는 스레드 풀)로 이동할 수 있습니다.
반대로, TCP 클라이언트를 생성하기 위해 커넥터 개체가 생성되고 알려진 주소(일반적으로 호스트 및 소켓)의 서버에 연결됩니다. 연결되면 소켓은 직접 읽고 쓰는 데 사용할 수 있는 스트리밍 소켓입니다.
IPv4의 경우 tcp_acceptor
및 tcp_connector
클래스는 각각 서버와 클라이언트를 만드는 데 사용됩니다. 이는 inet_address
클래스를 사용하여 32비트 호스트 주소와 16비트 포트 번호로 구성된 끝점 주소를 지정합니다.
tcp_acceptor
tcp_acceptor
는 서버를 설정하고 들어오는 연결을 수신하는 데 사용됩니다.
int16_t port = 12345;
sockpp::tcp_acceptor acc(port);
if (!acc)
report_error(acc.last_error_str());
// Accept a new client connection
sockpp::tcp_socket sock = acc.accept();
수락자는 일반적으로 새로운 연결을 수락하는 루프에 위치하며 이를 다른 프로세스, 스레드 또는 스레드 풀에 전달하여 클라이언트와 상호 작용합니다. 표준 C++에서는 다음과 같습니다.
while (true) {
// Accept a new client connection
sockpp::tcp_socket sock = acc.accept();
if (!sock) {
cerr << "Error accepting incoming connection: "
<< acc.last_error_str() << endl;
}
else {
// Create a thread and transfer the new stream to it.
thread thr(run_echo, std::move(sock));
thr.detach();
}
}
연결별 스레드 설계의 위험성은 잘 문서화되어 있지만, 동일한 기술을 사용하여 소켓을 스레드 풀(사용 가능한 경우)로 전달할 수 있습니다.
tcpechosvr.cpp 예제를 참조하세요.
tcp_connector
TCP 클라이언트는 tcp_connector
개체를 만들고 연결한 다음 데이터를 직접 읽고 쓰는 데 사용할 수 있다는 점에서 다소 간단합니다.
sockpp::tcp_connector conn;
int16_t port = 12345;
if (!conn.connect(sockpp::inet_address("localhost", port)))
report_error(conn.last_error_str());
conn.write_n("Hello", 5);
char buf[16];
ssize_t n = conn.read(buf, sizeof(buf));
tcpecho.cpp 예제를 참조하세요.
udp_socket
UDP 소켓은 비연결 통신에 사용될 수 있습니다.
sockpp::udp_socket sock;
sockpp::inet_address addr("localhost", 12345);
std::string msg("Hello there!");
sock.send_to(msg, addr);
sockpp::inet_address srcAddr;
char buf[16];
ssize_t n = sock.recv(buf, sizeof(buf), &srcAddr);
udpecho.cpp 및 udpechosvr.cpp 예제를 참조하세요.
다음 클래스를 사용하여 IPv6을 통한 TCP 연결에 동일한 스타일의 커넥터 및 수락자를 사용할 수 있습니다.
inet6_address
tcp6_connector
tcp6_acceptor
tcp6_socket
udp6_socket
예제는 example/tcp 디렉터리에 있습니다.
Unix 도메인 소켓을 구현하는 *nix 시스템의 로컬 연결에서도 마찬가지입니다. 이를 위해 클래스를 사용하십시오.
unix_address
unix_connector
unix_acceptor
unix_socket (unix_stream_socket)
unix_dgram_socket
예제는 example/unix 디렉터리에 있습니다.
CAN 버스(Controller Area Network)는 자동차나 산업용 기계 내부에서 통신하기 위해 마이크로컨트롤러가 일반적으로 사용하는 비교적 간단한 프로토콜입니다. Linux에는 프로세스가 사용자 공간의 소켓을 사용하여 물리적 CAN 버스 인터페이스에 대한 액세스를 공유할 수 있도록 하는 SocketCAN 패키지가 있습니다. 참고: Linux SocketCAN
가장 낮은 수준에서 CAN 장치는 "프레임"이라고 하는 개별 패킷을 버스의 특정 숫자 주소에 씁니다.
예를 들어 온도 센서가 있는 장치는 다음과 같이 주기적으로 온도를 읽고 이를 원시 32비트 정수로 버스에 쓸 수 있습니다.
can_address addr("CAN0");
can_socket sock(addr);
// The agreed ID to broadcast temperature on the bus
canid_t canID = 0x40;
while (true) {
this_thread::sleep_for(1s);
// Write the time to the CAN bus as a 32-bit int
int32_t t = read_temperature();
can_frame frame { canID, &t, sizeof(t) };
sock.send(frame);
}
프레임을 얻는 수신기는 다음과 같습니다.
can_address addr("CAN0");
can_socket sock(addr);
can_frame frame;
sock.recv(&frame);
소켓 클래스 계층 구조는 기본 socket
클래스를 기반으로 구축됩니다. 대부분의 간단한 애플리케이션은 socket
직접 사용하지 않고 tcp_connector
및 tcp_acceptor
와 같은 특정 주소 계열에 대해 정의된 파생 클래스를 사용합니다.
소켓 개체는 기본 OS 소켓 핸들에 대한 핸들과 해당 소켓에 대해 발생한 마지막 오류에 대한 캐시된 값을 유지합니다. 소켓 핸들은 일반적으로 열린 소켓의 경우 값이 >=0이고 열리지 않았거나 유효하지 않은 소켓의 경우 -1인 정수 파일 설명자입니다. 열려 있지 않은 소켓에 사용되는 값은 INVALID_SOCKET
상수로 정의됩니다. 일반적으로 직접 테스트할 필요는 없지만 개체 자체가 초기화되지 않았거나 오류 상태에 있는 경우 false 로 평가됩니다. 일반적인 오류 검사는 다음과 같습니다.
tcp_connector conn({"localhost", 12345});
if (!conn)
cerr << conn.last_error_str() << std::endl;
각 소켓 클래스의 기본 생성자는 아무 작업도 수행하지 않으며 단순히 기본 핸들을 INVALID_SOCKET
으로 설정합니다. 소켓 객체를 생성하지 않습니다. connector
객체를 적극적으로 연결하거나 acceptor
객체를 열기 위한 호출은 기본 OS 소켓을 생성한 다음 요청된 작업을 수행합니다.
응용 프로그램은 일반적으로 라이브러리를 사용하여 대부분의 하위 수준 작업을 수행할 수 있습니다. 대부분의 클래스에서 정적 create()
함수를 사용하여 연결되지 않고 바인딩되지 않은 소켓을 만든 다음 수동으로 해당 소켓을 바인딩하고 수신할 수 있습니다.
socket::handle()
메서드는 라이브러리에서 노출되지 않는 모든 플랫폼 API 호출로 보낼 수 있는 기본 OS 핸들을 노출합니다.
소켓 객체는 스레드로부터 안전하지 않습니다. 소켓에서 읽거나 소켓에 쓰는 여러 스레드를 가지려는 애플리케이션은 액세스를 보호하기 위해 std::mutex
와 같은 직렬화 형식을 사용해야 합니다.
socket
한 스레드에서 다른 스레드로 안전하게 이동할 수 있습니다. 이는 하나의 스레드를 사용하여 들어오는 연결을 수락한 다음 처리를 위해 새 소켓을 다른 스레드 또는 스레드 풀로 전달하는 서버의 일반적인 패턴입니다. 이는 다음과 같이 수행할 수 있습니다.
sockpp::tcp6_socket sock = acc.accept(&peer);
// Create a thread and transfer the new socket to it.
std::thread thr(handle_connection, std::move(sock));
이 경우, handler_connection은 다음과 같이 값으로 소켓을 취하는 함수입니다:
void handle_connection(sockpp::tcp6_socket sock) { ... }
socket
복사할 수 없으므로 유일한 선택은 소켓을 이와 같은 기능으로 이동하는 것입니다.
특히 클라이언트 애플리케이션에서는 하나의 스레드가 소켓에서 읽고 다른 스레드가 소켓에 쓰는 것이 일반적인 패턴입니다. 이 경우 기본 소켓 핸들은 스레드로부터 안전한 것으로 간주될 수 있습니다(읽기 스레드 하나와 쓰기 스레드 하나). 그러나 이 시나리오에서도 sockpp::socket
개체는 특히 캐시된 오류 값으로 인해 여전히 스레드로부터 안전하지 않습니다. 쓰기 스레드는 읽기 스레드에서 발생한 오류를 볼 수 있으며 그 반대의 경우도 마찬가지입니다.
이 경우의 해결책은 socket::clone()
메서드를 사용하여 소켓 복사본을 만드는 것입니다. 이는 시스템의 dup()
함수를 사용하거나 유사한 소켓 핸들의 복사본을 사용하여 다른 소켓을 생성합니다. 이는 소켓의 각 복사본이 독립적인 수명을 유지할 수 있다는 추가 이점을 제공합니다. 기본 소켓은 두 개체가 모두 범위를 벗어날 때까지 닫히지 않습니다.
sockpp::tcp_connector conn({host, port});
auto rdSock = conn.clone();
std::thread rdThr(read_thread_func, std::move(rdSock));
socket::shutdown()
메서드를 사용하면 다른 스레드 신호 메커니즘 없이 이러한 개체 중 하나에서 다른 개체로 소켓을 닫으려는 의도를 전달할 수 있습니다.
tcpechomt.cpp 예제를 참조하세요.