【学习记录】C&C++网络编程——文件传输(兼容Windows和Linux)
Monday, June 5, 2023
本文共2465字
5分钟阅读时长
⚠️本文是作者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%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%85%BC%E5%AE%B9windows%E5%92%8Clinux/。商业转载请联系作者获得授权,非商业转载请注明出处!
In the depth of winter, I finally learned that there was within me an invincible summer.
— Albert Camus
引入
之前在【学习记录】C&C++网络编程初步的时候写了如何初步通过socket api进行服务器和客户端之间的通信,于是我就想,能不能做一个文件传输系统,客户端发送给服务器一个文件路径,然后服务器在自己的系统上打开然后输出给客户端?就类似于scp那样的。
同时这个文件传输系统必须同时在Linux和Windows上兼容,因为很多情况下人们测试是在Windows上打开Linux虚拟机进行测试的。做到这一点可以使用宏控制,但是会比较麻烦,我决定将来要用C++标准库重写一遍。
基本原理
在客户端的shell中通过命令行参数指定IP地址、port和主机系统文件路径,然后通过IP:port连接服务器,再将主机系统文件路径传递给服务器,然后服务器在自己的系统上打开文件并传输给客户端,同时客户端使用select(IO多路转接)检测在一定时间内是否还有接收,如果超时则停止。
如何与Windows兼容
代码能使用C标准库就使用标准库(比如Windows上没有open/read/write而是另一套API,这时我选择标准输入输出流),尽量不使用外部第三方库(比如我先在Linux上用pthread写了服务器,虽然Windows上也有第三方pthread库,但是尽量不使用),使用宏控制代码生成:
#ifdef _WIN32
...
#else
...
#endif
同时CMakeLists.txt也对两个平台进行适配:
cmake_minimum_required(VERSION 3.25)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
project(cweb)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/bin)
add_executable(file_client file_client.c)
add_executable(file_server file_server.c)
if(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
target_link_libraries(file_client PRIVATE wsock32)
target_link_libraries(file_client PRIVATE Ws2_32)
target_link_libraries(file_server PRIVATE wsock32)
target_link_libraries(file_server PRIVATE Ws2_32)
endif(CMAKE_HOST_SYSTEM_NAME "Windows")
如何确定服务器和客户端的IP和port
服务器的IP不用指定,但是port可以通过命令行指定,如不指定则默认端口为10000
客户端通过命令行参数指定IP:port,其第一个命令行参数可以是"127.0.0.1"这种不带port的(默认port为10000),也可以是"127.0.0.1:12345"这种加一个冒号后面跟port的
完整代码
服务器
#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ---web---
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
// --- windows.h要求要在winsock2.h下面,为防止格式化程序更换次序所以加注释隔开
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
#else
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#endif
// 几个宏用来防止手动写入过多错误判断代码
#define CKEX(EXP) \
do { \
if ((EXP)) { \
fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", #EXP, strerror(errno)); \
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; // 保存客户端的端口
#ifdef _WIN32
DWORD tid;
#else
pthread_t tid;
#endif // _WIN32
} netfd;
#ifdef _WIN32
static DWORD WINAPI communicate(void* arg)
#else
static void* communicate(void* arg)
#endif // _WIN32
{
// 通过强制转换
netfd* netinfo = (netfd*)arg;
// 每个已连接的通信线程都有1kb的缓冲区
char fname[1024] = { 0 };
// 固定的回复
char response[1024] = { 0 };
// TODOrecv & send
// 获取收到的字节数量
int len = recv(netinfo->cfd, fname, sizeof(fname) - 1, 0);
if (len > 0) {
printf("client from %s:%d ask for file: %s\n", netinfo->ip, netinfo->port, fname);
long int num;
FILE* fp = fopen(fname, "rb");
CKEX(fp == NULL);
while ((num = fread(response, sizeof(char), sizeof(response)/sizeof(char)-1, fp)) > 0) {
// response[status] = 0;
send(netinfo->cfd, response, sizeof(char) * num, 0);
// #ifdef _WIN32
// Sleep(1000);
// #else
// sleep(1);
// #endif // _WIN32
// Sleep(1000);
}
// puts("b 1");
CKEN((!feof(fp)) && ferror(fp));
if (fp != NULL){
fclose(fp);
fp = NULL;
}
} else if (len == 0) {
// TODO接收到客户端终止连接请求
puts("the client has ended the connection...");
} else {
// 接收到错误就打印出来,并且退出线程
fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", "recv()", strerror(errno));
}
printf("exiting thread %ld (communication with %s:%d)...", netinfo->tid, netinfo->ip, netinfo->port);
#ifdef _WIN32
CloseHandle((HANDLE)netinfo->tid);
#endif // _WIN32
free(netinfo);
netinfo = NULL;
#ifdef _WIN32
closesocket(netinfo->cfd);
return 0;
#else
close(netinfo->cfd);
pthread_exit((void*)0);
#endif // _WIN32
}
static void sig_int(int signo)
{
#ifdef _WIN32
printf("thread %ld received: %s\n", GetCurrentThreadId(), _sys_errlist[signo]);
#else
printf("thread %ld received: %s\n", pthread_self(), strsignal(signo));
#endif // _WIN32
puts("server exiting...");
exit(signo);
}
int main(int argc, char const* argv[])
{
int port = 10000;
if (argc >= 2)
{
port = atoi(argv[1]);
}
// 声明局部变量
#ifdef _WIN32
SOCKET lfd;
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
HANDLE err;
#else
int lfd;
pthread_attr_t tid_attr;
int err;
#endif // _WIN32
struct sockaddr_in server_addr; // 保存服务器的相关信息
const int connection_num = 8; // 设置最大连接数
// 设置信号处理函数
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));
#ifndef _WIN32
// 初始化线程属性,设置为线程启动即分离
CKEN((err = pthread_attr_init(&tid_attr)));
CKEN((err = pthread_attr_setdetachstate(&tid_attr, PTHREAD_CREATE_DETACHED)));
#endif //_WIN32
// TODO开始接收连接请求
// 阻塞直到有客户端连接
// 在栈空间中保存该结构体的大小,因为后面的api需要传指针进去,可能会更改这个值
// 不停地检测是否有连接
// 如果没有连接就阻塞
while (true) {
// 这个结构体包括了传给线程的参数,因此不能是局部变量,可以在堆上分配
netfd* netinfo = (netfd*)malloc(sizeof(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
// 打印客户端相关信息
printf(
"client's IP: %s, port: %d\n",
netinfo->ip,
netinfo->port);
// 创建信息
#ifdef _WIN32
CKEZ(err = CreateThread(NULL, 0, communicate, (LPVOID)netinfo, DETACHED_PROCESS, &netinfo->tid));
// CloseHandle((HANDLE)tid);
#else
CKEN(err = pthread_create(&netinfo->tid, &tid_attr, communicate, (void*)netinfo));
#endif // _WIN32
}
// TODO关闭两个端口
// 在主线程只用关闭监听的文件描述符
#ifdef _WIN32
closesocket(lfd);
WSACleanup();
#else
close(lfd);
#endif // _WIN32
return 0;
}
客户端
#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
// --- windows.h要求要在winsock2.h下面,为防止格式化程序更换次序所以加注释隔开
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
#define REMOTE_PATH_SEP '/'
#define LOCAL_PATH_SEP '\\'
#else
#include <arpa/inet.h>
#include <unistd.h>
#include <limits.h>
#define REMOTE_PATH_SEP '\\'
#define LOCAL_PATH_SEP '/'
#endif
#define CKEX(EXP) \
do { \
if ((EXP)) { \
fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", #EXP, strerror(errno)); \
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[])
{
if (argc < 3) {
fprintf(stderr, "Usage: %s <IP address[:port]> <filename>", argv[0]);
exit(127);
}
const char* IP = argv[1];
// 尝试从IP中分离出port
char* port_str = strchr(IP, ':');
int port = 10000;
if (port_str != NULL)
{
*port_str = 0;
port_str++;
port = atoi(port_str);
}
const char* fname = argv[2];
char* store_fname = strrchr(fname, REMOTE_PATH_SEP)+1;
int path_ptr = 0;
#ifdef _WIN32
char path[MAX_PATH] = { 0 };
path_ptr = MAX_PATH - strlen(store_fname) - 2;
CKEZ(GetCurrentDirectory(path_ptr, path));
#else
char path[PATH_MAX] = {0};
path_ptr = PATH_MAX - strlen(store_fname) - 2;
CKEZ(getcwd(path, path_ptr));
#endif
path_ptr = strlen(path);
path[path_ptr++] = LOCAL_PATH_SEP;
// puts("b1");
strncpy(path + path_ptr, store_fname, strlen(store_fname) + 1);
store_fname = path;
printf("Writing to %s\n", store_fname);
// 声明局部变量
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, &(server_addr.sin_addr));
CKEX(connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr)));
// TODO send & recv
int num = strlen(fname);
send(fd, fname, sizeof(char) * (num + 1), 0);
FILE* fp = fopen(store_fname, "wb");
// 超时时间定为5秒
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
// 设置监控的文件描述符集合
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
while (1) {
int result = select(fd + 1, &read_fds, NULL, NULL, &timeout);
if (result == -1) {
fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", "select()", strerror(errno));
exit(127 + errno);
} else if (result == 0) {
fprintf(stderr, "Timeout waiting for data.\n");
break;
}
int received = recv(fd, buf, sizeof(buf)-1, 0);
if (received == 0) {
puts("the server has ended its service...");
printf("check your file (%s) to find out if it went well...\n", store_fname);
break;
} else if (received == -1){
fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", "recv()", strerror(errno));
exit(127 + errno);
} else{
fwrite(buf, sizeof(char), received, fp);
fwrite(buf, sizeof(char), received, stdout);
}
}
puts("transformation done!");
fclose(fp);
// TODO 关闭连接
#ifdef _WIN32
closesocket(fd);
// 注销Winsock相关库
WSACleanup();
#else
close(fd);
#endif
return 0;
}
扫码阅读此文章
点击按钮复制分享信息
点击订阅