ไลบรารีซ็อกเก็ต C++ ที่เรียบง่าย ทันสมัย
นี่คือ wrapper C++ ระดับต่ำรอบๆ ไลบรารีซ็อกเก็ต Berkeley โดยใช้คลาส socket
acceptor,
และ connector
ซึ่งเป็นแนวคิดที่คุ้นเคยจากภาษาอื่น
คลาส socket
ฐานหุ้มที่จับซ็อกเก็ตของระบบ และรักษาอายุการใช้งาน เมื่อวัตถุ C++ ออกไปนอกขอบเขต ปิดหมายเลขอ้างอิงซ็อกเก็ตต้นแบบ โดยทั่วไปวัตถุซ็อกเก็ต สามารถเคลื่อนย้ายได้ แต่ไม่ สามารถคัดลอกได้ ซ็อกเก็ตสามารถถ่ายโอนจากขอบเขตหนึ่ง (หรือเธรด) ไปยังอีกขอบเขตหนึ่งโดยใช้ std::move()
ปัจจุบันไลบรารีรองรับ: IPv4 และ IPv6 บน Linux, Mac และ Windows ระบบ *nix และ POSIX อื่นๆ ควรทำงานได้โดยมีการแก้ไขเพียงเล็กน้อยหรือไม่มีเลย
Unix-Domain Sockets พร้อมใช้งานบนระบบ *nix ที่มีการปรับใช้ระบบปฏิบัติการ
การสนับสนุนซ็อกเก็ตที่ปลอดภัยโดยใช้ไลบรารี OpenSSL หรือ MbedTLS ได้รับการเพิ่มเข้ามาเมื่อเร็วๆ นี้โดยมีความครอบคลุมขั้นพื้นฐาน สิ่งนี้จะขยายต่อไปในอนาคตอันใกล้นี้
นอกจากนี้ยังมีการสนับสนุนเชิงทดลองสำหรับการเขียนโปรแกรม CAN บัสบน Linux โดยใช้แพ็คเกจ SocketCAN ซึ่งจะช่วยให้อะแด็ปเตอร์ CAN บัสมีอินเทอร์เฟซเครือข่าย โดยมีข้อจำกัดที่กำหนดโดยโปรโตคอลข้อความ CAN
รหัสทั้งหมดในไลบรารีอยู่ภายในเนมสเปซ sockpp
C++
สาขา 'ต้นแบบ' กำลังเริ่มย้ายไปยัง v2.0 API และขณะนี้ไม่เสถียรอย่างยิ่ง ขอแนะนำให้คุณดาวน์โหลดรุ่นล่าสุดเพื่อการใช้งานทั่วไป
เวอร์ชัน 1.0 เปิดตัวแล้ว!
เนื่องจากการเปลี่ยนแปลงที่สำคัญเริ่มสะสมในสาขาการพัฒนาปัจจุบัน จึงได้มีการตัดสินใจปล่อย API ที่ค่อนข้างเสถียรในช่วงไม่กี่ปีที่ผ่านมาเป็นเวอร์ชัน 1.0 นี่มาจากบรรทัด v0.8.x ล่าสุด นั่นจะทำให้สิ่งต่างๆ ในอนาคตเกิดความสับสนน้อยลง และช่วยให้เราสามารถรักษาสาขา v1.x ไว้ได้
การพัฒนาเวอร์ชัน 2.0 อยู่ระหว่างดำเนินการ
แนวคิดของการมีการดำเนินการ I/O แบบ "ไร้สถานะ" ที่นำมาใช้ใน PR #17 (ซึ่งไม่เคยถูกรวมเข้าด้วยกันอย่างสมบูรณ์) กำลังมาใน 2.0 API พร้อมด้วยคลาส result<T>
มันจะเป็นแบบทั่วไปเหนือประเภทการส่งคืน "success" โดยมีข้อผิดพลาดแสดงโดย std::error_code
สิ่งนี้จะช่วยลดปัญหาแพลตฟอร์มในการติดตามและรายงานข้อผิดพลาดได้อย่างมาก
การใช้ประเภทผลลัพธ์ที่เหมือนกันจะขจัดความจำเป็นในการยกเว้นในฟังก์ชันส่วนใหญ่ ยกเว้นตัวสร้าง ในกรณีที่ฟังก์ชันอาจส่งฟังก์ชัน จะมีการจัดหาฟังก์ชัน noexcept
ที่เทียบเคียงได้ ซึ่งสามารถตั้งค่าพารามิเตอร์รหัสข้อผิดพลาดแทนที่จะส่ง ดังนั้นไลบรารีจึงสามารถใช้งานได้โดยไม่มีข้อยกเว้นหากแอปพลิเคชันต้องการ
ฟังก์ชั่นทั้งหมดที่อาจล้มเหลวเนื่องจากข้อผิดพลาดของระบบจะส่งคืนผลลัพธ์ ซึ่งจะขจัดความจำเป็นสำหรับ "ข้อผิดพลาดล่าสุด" และดังนั้นตัวแปรข้อผิดพลาดสุดท้ายที่แคชไว้ในคลาส socket
จะหายไป คลาสซ็อกเก็ตจะพันเฉพาะที่จับซ็อกเก็ตเท่านั้น ทำให้ปลอดภัยยิ่งขึ้นในการแบ่งปันข้ามเธรดในลักษณะเดียวกับที่แฮนเดิลสามารถแชร์ได้ โดยทั่วไปจะมีเธรดหนึ่งสำหรับการอ่านและอีกเธรดสำหรับการเขียน
งานบางอย่างยังได้เริ่มรวม Secure Sockets เข้ากับไลบรารี่รุ่น 2.x โดยใช้ไลบรารี OpenSSL หรือ MbedTLS หรือ (น่าจะ) ตัวเลือกเวลาในการสร้างสำหรับอย่างใดอย่างหนึ่ง 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
โปรดส่งคำขอดึงข้อมูลทั้งหมดไปยังสาขานั้น ไม่ใช่ master
สำหรับข้อมูลเพิ่มเติม โปรดดูที่: 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 | ปิด | สร้างและติดตั้งเอกสาร API ที่ใช้ HTML (ต้องใช้ Doxygen) |
SOCKPP_BUILD_EXAMPLES | ปิด | สร้างโปรแกรมตัวอย่าง |
SOCKPP_BUILD_TESTS | ปิด | สร้างการทดสอบหน่วย (ต้องใช้ Catch2 ) |
SOCKPP_WITH_CAN | ปิด | รวมการสนับสนุน SocketCAN (ลินุกซ์เท่านั้น) |
ตั้งค่าเหล่านี้โดยใช้สวิตช์ '-D' ในคำสั่งการกำหนดค่า CMake ตัวอย่างเช่น หากต้องการสร้างเอกสารประกอบและแอปตัวอย่าง:
$ 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 เมื่อสร้างไลบรารีนั้น ควรกำหนดตัวเลือกการกำหนดค่าต่อไปนี้ในไฟล์กำหนดค่า รวม/mbedtls/mbedtls_config.h
#define MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK
เพื่อรองรับการร้อยด้าย:
#define MBEDTLS_THREADING_PTHREAD
#define MBEDTLS_THREADING_C
และตั้งค่าตัวเลือกการสร้าง CMake:
LINK_WITH_PTHREAD:BOOL=ON
โปรดทราบว่าตัวเลือกในไฟล์กำหนดค่าควรมีอยู่ในไฟล์อยู่แล้ว แต่จะใส่เครื่องหมายความคิดเห็นไว้ตามค่าเริ่มต้น เพียงแค่ไม่แสดงความคิดเห็น บันทึก และสร้าง
Wrapper sockpp
OpenSSL กำลังถูกสร้างขึ้นและทดสอบด้วย OpenSSL v3.0
TCP และแอปพลิเคชันเครือข่าย "สตรีมมิ่ง" อื่นๆ มักจะถูกตั้งค่าเป็นเซิร์ฟเวอร์หรือไคลเอนต์ ตัวรับถูกใช้เพื่อสร้างเซิร์ฟเวอร์ 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
ตัวเชื่อมต่อและตัวรับรูปแบบเดียวกันสามารถใช้สำหรับการเชื่อมต่อ TCP บน IPv6 โดยใช้คลาส:
inet6_address
tcp6_connector
tcp6_acceptor
tcp6_socket
udp6_socket
ตัวอย่างอยู่ในไดเร็กทอรี example/tcp
เช่นเดียวกับการเชื่อมต่อภายในเครื่องบนระบบ *nix ที่ใช้ Unix Domain Sockets เพื่อที่จะใช้คลาส:
unix_address
unix_connector
unix_acceptor
unix_socket (unix_stream_socket)
unix_dgram_socket
ตัวอย่างอยู่ในไดเร็กทอรี example/unix
เครือข่ายพื้นที่ควบคุม (CAN บัส) เป็นโปรโตคอลที่ค่อนข้างง่ายซึ่งไมโครคอนโทรลเลอร์มักใช้ในการสื่อสารภายในรถยนต์หรือเครื่องจักรอุตสาหกรรม Linux มีแพ็คเกจ SocketCAN ซึ่งอนุญาตให้กระบวนการแบ่งปันการเข้าถึงอินเทอร์เฟซ CAN บัสจริงโดยใช้ซ็อกเก็ตในพื้นที่ผู้ใช้ ดู: 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
วัตถุซ็อกเก็ตเก็บหมายเลขอ้างอิงไปยังหมายเลขอ้างอิงซ็อกเก็ตระบบปฏิบัติการพื้นฐานและค่าที่แคชไว้สำหรับข้อผิดพลาดล่าสุดที่เกิดขึ้นสำหรับซ็อกเก็ตนั้น โดยทั่วไปหมายเลขอ้างอิงซ็อกเก็ตจะเป็นตัวอธิบายไฟล์จำนวนเต็ม โดยมีค่า >=0 สำหรับซ็อกเก็ตที่เปิดอยู่ และ -1 สำหรับซ็อกเก็ตที่ยังไม่ได้เปิดหรือไม่ถูกต้อง ค่าที่ใช้สำหรับซ็อกเก็ตที่ยังไม่ได้เปิดถูกกำหนดให้เป็นค่าคงที่ INVALID_SOCKET
แม้ว่าโดยปกติแล้วไม่จำเป็นต้องทดสอบโดยตรง เนื่องจากตัวออบเจ็กต์จะประเมินว่าเป็น เท็จ หากไม่ได้กำหนดค่าเริ่มต้นหรืออยู่ในสถานะข้อผิดพลาด การตรวจสอบข้อผิดพลาดทั่วไปจะเป็นดังนี้:
tcp_connector conn({"localhost", 12345});
if (!conn)
cerr << conn.last_error_str() << std::endl;
ตัวสร้างเริ่มต้นสำหรับแต่ละคลาสซ็อกเก็ตไม่ได้ทำอะไรเลย และเพียงตั้งค่าหมายเลขอ้างอิงพื้นฐานเป็น INVALID_SOCKET
พวกเขาไม่ได้สร้างวัตถุซ็อกเก็ต การเรียกให้เชื่อมต่อออบเจ็กต์ connector
ต่อหรือเปิดออบเจ็กต์ acceptor
จะสร้างซ็อกเก็ตระบบปฏิบัติการพื้นฐาน จากนั้นดำเนินการตามที่ร้องขอ
โดยทั่วไปแอปพลิเคชันสามารถดำเนินการระดับต่ำส่วนใหญ่กับไลบรารีได้ คุณสามารถสร้างซ็อกเก็ตที่ไม่ได้เชื่อมต่อและไม่ถูกผูกไว้ได้ด้วยฟังก์ชัน create()
แบบคงที่ในคลาสส่วนใหญ่ จากนั้นจึงผูกและฟังซ็อกเก็ตเหล่านั้นด้วยตนเอง
เมธอด socket::handle()
เปิดเผยตัวจัดการ OS พื้นฐานซึ่งสามารถส่งไปยังการเรียก API ของแพลตฟอร์มใด ๆ ที่ไลบรารีไม่ได้เปิดเผย
วัตถุซ็อกเก็ตไม่ปลอดภัยสำหรับเธรด แอปพลิเคชันที่ต้องการให้อ่านหลายเธรดจากซ็อกเก็ตหรือเขียนลงในซ็อกเก็ตควรใช้รูปแบบการทำให้เป็นอนุกรมบางรูปแบบ เช่น 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));
ในกรณีนี้ handle_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