【学习记录】C&C++网络编程——用C++标准线程库实现跨平台
Friday, June 9, 2023
本文共1790字
4分钟阅读时长
⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/posts/%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95cc++%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E7%94%A8c++%E6%A0%87%E5%87%86%E7%BA%BF%E7%A8%8B%E5%BA%93%E5%AE%9E%E7%8E%B0%E8%B7%A8%E5%B9%B3%E5%8F%B0/。商业转载请联系作者获得授权,非商业转载请注明出处!
The only real mistake is the one from which we learn nothing.
— John Powell
引入
在【学习记录】C&C++网络编程——文件传输(兼容Windows和Linux) - P3troL1er 的个人博客我提到兼容Windows和Linux太累了,所以想用C++标准库代替。但是C++暂时没有标准网络库,而且socket库在Windows和Linux差别并不大,又暂时不想引入外部库,同时代码中用到的api都还比较浅,所以暂时只能替代多线程相关的部分。
完整代码
其实代码逻辑差别并不大,只是把能替换的C-style api全部替换成C++风格了
server
#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#endif
// C库
#include <cerrno>
#include <csignal>
#include <cstring>
// C++库
#include <string>
#include <thread>
#include <chrono>
#include <iostream>
#include <fstream>
// ---web---
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
// ---
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
#else
#include <arpa/inet.h>
// #include <unistd.h>
#endif
#define readonly const
#define BUFSIZE 1024
// 几个宏用来防止手动写入过多错误判断代码
#define CKEX(EXP) \
do { \
if ((EXP)) { \
std::cerr << "[EXIT ERROR] " << #EXP << " raise error: " << strerror(errno) << std::endl; \
exit(127 + errno); \
} \
} while (0)
#define CKE(EXP) CKEX((EXP) == -1)
#define CKEN(EXP) CKEX((EXP) != 0)
#define CKEZ(EXP) CKEX((EXP) == 0)
// 用来保存通信线程所需的相关信息
typedef struct {
#ifdef _WIN32
SOCKET cfd;
#else
int cfd; // 通信的文件描述符
#endif // _WIN32
struct sockaddr_in details; // 客户端的相关信息
socklen_t details_len; // 上面那个结构体的长度,如果通信线程调用recvfrom就可以用到
char ip[16]; // 保存客户端的IP字符串
int port; // 保存客户端的端口
std:🧵:id tid;
} netfd;
static void communicate(netfd* arg)
{
netfd& netinfo = *arg;
netinfo.tid = std::this_thread::get_id();
// 每个已连接的通信线程都有1kb的缓冲区
char fname[BUFSIZE] { 0 };
// 固定的回复
char response[BUFSIZE] { 0 };
// TODOrecv & send
// 获取收到的字节数量
int len = recv(netinfo.cfd, fname, sizeof(fname) - 1, 0);
if (len > 0) {
std::cout << "client from "
<< netinfo.ip
<< ':'
<< netinfo.port
<< " ask for file: "
<< fname
<< std::endl;
std::ifstream inFile(fname, std::ios::in | std::ios::binary);
CKEX(!inFile.is_open());
while (!inFile.eof()) {
inFile.read(response, BUFSIZE);
send(netinfo.cfd, response, sizeof(char) * (inFile.gcount()), 0);
}
std::cout << "Transformation Done!" << std::endl;
inFile.close();
} else if (len == 0) {
// TODO接收到客户端终止连接请求
std::cerr << "the client has ended the connection..." << std::endl;
} else {
// 接收到错误就打印出来,并且退出线程
std::cerr << "[EXIT ERROR] recv() raise error: " << strerror(errno) << std::endl;
}
std::cout << "exiting thread "
<< netinfo.tid
<< " (communication with "
<< netinfo.ip
<< ':'
<< netinfo.port
<< ")..."
<< std::endl;
delete &netinfo;
#ifdef _WIN32
closesocket(netinfo.cfd);
#else
close(netinfo.cfd);
#endif // _WIN32
}
int main(int argc, char const* argv[])
{
// 解除std::cout和std::cin之间的同步
std::ios_base::sync_with_stdio(false);
int port = 10000;
if (argc >= 2)
{
std::string tmp(argv[1]);
port = std::stoi(tmp);
}
// 声明局部变量
#ifdef _WIN32
SOCKET lfd;
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
#else
int lfd;
#endif // _WIN32
struct sockaddr_in server_addr; // 保存服务器的相关信息
constexpr readonly int connection_num = 8; // 设置最大连接数
// 设置信号处理函数
auto sig_int = [](int signo) -> void {
std::cout << "thread " << std::this_thread::get_id() << "received: ";
#ifdef _WIN32
std::cout << _sys_errlist[signo] << std::endl;
#else
std::cout << strsignal(signo) << std::endl;
#endif // _WIN32
std::cout << "server exiting..." << std::endl;
exit(signo);
};
signal(SIGINT, sig_int);
// TODO设置监听的套接字
// 设置TCP协议
CKE(lfd = socket(AF_INET, SOCK_STREAM, 0));
// TODO绑定端口
server_addr.sin_family = AF_INET;
// from host byte order to network byte order
server_addr.sin_port = htons(port);
// 代表本机的任意地址
// 或者设置其中一个IP,使用inet_pton将IP地址字符串转换为二进制格式
// inet_pton - convert IPv4 and IPv6 addresses from text to binary form
server_addr.sin_addr.s_addr = INADDR_ANY;
CKEN(bind(lfd, (struct sockaddr*)&server_addr, sizeof(server_addr)));
// TODO设置端口监听
CKEN(listen(lfd, connection_num));
// TODO开始接收连接请求
// 阻塞直到有客户端连接
// 在栈空间中保存该结构体的大小,因为后面的api需要传指针进去,可能会更改这个值
// 不停地检测是否有连接
// 如果没有连接就阻塞
while (true) {
// 这个结构体包括了传给线程的参数,因此不能是局部变量,可以在堆上分配
netfd* netinfo = new netfd;
netinfo->details_len = sizeof(netinfo->details);
/**
On success, these system calls return a file descriptor for the accepted socket (a nonnegative integer). On error, -1 is returned, errno is set appropriately, and addrlen is left unchanged.
*/
CKE(netinfo->cfd = accept(lfd, (struct sockaddr*)&(netinfo->details), &(netinfo->details_len)));
// 此时应该已经收到了客户端的相关信息
netinfo->port = ntohs(netinfo->details.sin_port);
#ifdef _WIN32
inet_ntop(AF_INET, &(netinfo->details.sin_addr), netinfo->ip, netinfo->details_len);
#else
inet_ntop(AF_INET, &(netinfo->details.sin_addr.s_addr), netinfo->ip, netinfo->details_len);
#endif // _WIN32
// 打印客户端相关信息
std::cout << "client's IP: "
<< netinfo->ip
<< ", port: "
<< netinfo->port
<< std::endl;
// 创建信息
std::thread t(communicate, netinfo);
t.detach();
}
// TODO关闭两个端口
// 在主线程只用关闭监听的文件描述符
#ifdef _WIN32
closesocket(lfd);
WSACleanup();
#else
close(lfd);
#endif // _WIN32
return 0;
}
client
#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <cerrno>
#include <cstring>
#include <climits>
#include <iostream>
#include <fstream>
#include <string>
#include <filesystem>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
// ---
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
#define REMOTE_PATH_SEP '/'
#else
#include <arpa/inet.h>
#include <unistd.h>
#define REMOTE_PATH_SEP '\\'
#endif
#define LOCAL_PATH_SEP std::filesystem::path::preferred_separator
#define readonly const
// 几个宏用来防止手动写入过多错误判断代码
#define CKEX(EXP) \
do { \
if ((EXP)) { \
std::cerr << "[EXIT ERROR] " << #EXP << " raise error: " << strerror(errno) << std::endl; \
exit(127 + errno); \
} \
} while (0)
#define CKE(EXP) CKEX((EXP) == -1)
#define CKEN(EXP) CKEX((EXP) != 0)
#define CKEZ(EXP) CKEX((EXP) == 0)
int main(int argc, char const* argv[])
{
std::ios_base::sync_with_stdio(false);
if (argc < 3) {
std::cerr << "Usage: "
<< argv[0]
<< " <IP address> <filename>"
<< std::endl;
exit(127);
}
std::string IP = argv[1];
// 尝试从IP中分离出port
size_t port_place = IP.find(':');
int port = 10000;
if (port_place != std::string::npos)
{
IP = IP.substr(0, port_place);
port_place++;
port = std::stoi(IP.substr(port_place));
}
std::string fname = argv[2];
size_t sep_place = fname.rfind(REMOTE_PATH_SEP) + 1;
if (sep_place == std::string::npos)
{
sep_place = 0;
}
int path_ptr = 0;
std::filesystem::path currentPath = std::filesystem::current_path();
std::string path = currentPath.generic_string();
path += LOCAL_PATH_SEP;
path += fname.substr(sep_place);
readonly char* store_fname = path.c_str();
std::cout << "Writing to " << path << std::endl;
// 声明局部变量
char buf[1024] { 0 };
#ifdef _WIN32
struct WSAData wsa;
SOCKET fd;
// 初始化套接字库
WSAStartup(MAKEWORD(2, 2), &wsa);
#else
int fd;
#endif
// TODO 建立套接字
#ifdef _WIN32
CKEX((fd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET);
#else
CKE(fd = socket(AF_INET, SOCK_STREAM, 0));
#endif // _WIN32
// TODO 建立连接
// 设置服务器地址、端口
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
inet_pton(AF_INET, IP.c_str(), &(server_addr.sin_addr));
CKEX(connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr)));
// TODO send & recv
int num = fname.size();
send(fd, fname.c_str(), sizeof(char) * fname.size(), 0);
std::ofstream outFile(path, std::ios::out | std::ios::binary);
struct timeval timeout;
timeout.tv_sec = 2;
timeout.tv_usec = 0;
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
while (true) {
int result = select(fd + 1, &read_fds, NULL, NULL, &timeout);
if (result == -1) {
std::cerr << "[EXIT ERROR] select() raise error: " << strerror(errno) << std::endl;
exit(127 + errno);
} else if (result == 0) {
std::cerr << "Timeout waiting for data." << std::endl;
break;
}
int received = recv(fd, buf, sizeof(buf)-1, 0);
if (received == 0) {
std::cout << "the server has ended its service...\n"
<< "check your file ("
<< path
<< ") to find out if it went well..."
<< std::endl;
break;
} else if (received == -1){
std::cerr << "[EXIT ERROR] recv() raise error: " << strerror(errno) << std::endl;
exit(127 + errno);
} else{
outFile.write(buf, sizeof(char)*received);
std::cout.write(buf, sizeof(char)*received);
}
}
std::cout << "Transformation Done!" << std::endl;
outFile.close();
// TODO 关闭连接
#ifdef _WIN32
closesocket(fd);
// 注销Winsock相关库
WSACleanup();
#else
close(fd);
#endif
return 0;
}
扫码阅读此文章
点击按钮复制分享信息
点击订阅