基于socket完成了一个FTP服务器,实现了其基本功能
操作系统:Windows 10企业版 LTSC
开发语言:C++
开发工具:Visual Studio
1.用户登录功能
2.显示服务器/本地目录的内容
3.更改服务器和本地的工作目录
4.文件上传、下载
5.关闭连接
help:打印出支持的命令及其使用方法
pwd:服务器当前工作目录
cd:更改服务器工作目录
ls:列出服务器工作目录下的内容及其相关属性信息
put:将指定文件上传至服务器
get:从服务器上下载指定文件
!pwd:更改客户端当前工作目录
!cd:更改客户端当前工作目录 !ls:列出客户端工作目录下的内容及其相关属性信息
exit&quit:关闭与服务器的连接
连接服务器成功后会要求输入账户密码,直接回车即可登入匿名用户并进入命令界面
最好先阅读
《windows网络编程(第2版)》
按照参考书即可完成客户端与服务器socket的创建并连接
核心代码
cout << "Welcome to the FTP server made by Helix" << endl;
version = MAKEWORD(2, 2);
int error = WSAStartup(version, &wsadata);
if (error != 0)
{
cout << "Socket load failed" << endl;
return 0;
}
if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wVersion) != 2)
{
cout << "Version error" << endl;
WSACleanup();
return 0;
}
server_add.sin_family = AF_INET;
server_add.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
server_add.sin_port = htons(data_port);
socket_server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //FTP使用TCP协议簇
if (bind(socket_server, (SOCKADDR*)&server_add, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
cout << "Bind failed" << endl;
}
if (listen(socket_server, 5) < 0)
{
cout << "Listen failed" << endl;
}
length = sizeof(SOCKADDR);
cout << "Waiting to connect" << endl;
socket_receive = accept(socket_server, (SOCKADDR*)&receive_add, &length);
if (socket_receive == SOCKET_ERROR)
{
cout << "Connect failed" << endl;
closesocket(socket_receive);
closesocket(socket_server);
WSACleanup();
return 0;
}
cout << "Connect successfully!" << endl;
client
char send_message[100]; //两个缓冲区
char receive_message[100];
char* p = send_message;
int sendlen;
int receive_len;
int islogin = 0; //登录标志位
int data_port = 8000; //指定连接端口
cout << "Welcome to the FTP client made by Helix" << endl;
WSADATA mywsadata;
int error = WSAStartup(MAKEWORD(2, 2), &mywsadata);
if (error != 0)
{
cout << "Socket load failed" << endl;
return 0;
}
if (LOBYTE(mywsadata.wVersion) != 2 || HIBYTE(mywsadata.wVersion) != 2)
{
cout << "Version error" << endl;
WSACleanup();
return 0;
}
SOCKADDR_IN server_data;
server_data.sin_family = AF_INET;
server_data.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
server_data.sin_port = htons(data_port);
SOCKET client_socket;
client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //FTP使用TCP协议簇
if (connect(client_socket, (SOCKADDR*)&server_data, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
cout << "Connection failed.The possible reason is that the server is not turned on" << endl;
closesocket(client_socket);
WSACleanup();
return 0;
}
cout << "Connect successful" << endl;
只是做了个流程,并没有去严格核实用户身份,回车即可使用匿名用户登入server
server
while (!islogin) {
strcpy_s(sendbuf, "username:");
sendlen = send(socket_receive, sendbuf, 100, 0);
recv(socket_receive, receivebuf, 100, 0);
if (!receivebuf) {
continue;
}
strcpy_s(sendbuf, "password:");
sendlen = send(socket_receive, sendbuf, 100, 0);
recv(socket_receive, receivebuf, 100, 0);
if (receivebuf) {
islogin = 1;
}
else {
continue;
}
strcpy_s(sendbuf, "Login successful!");
sendlen = send(socket_receive, sendbuf, 100, 0);
}
client
while (!islogin) {
receive_len = recv(client_socket, receive_message, 100, 0);
if (receive_len < 0)
{
break;
}
else if (strcmp(receive_message, "Login successful!") == 0)
{
islogin = 1;
break;
}
cout << receive_message << endl;
cin.getline(send_message, 100);
sendlen = send(client_socket, send_message, 100, 0);
}
client和server都将用户输入/收到的命令进行分割,然后进行分析
关键代码:
char seps[] = " ";
char* token = NULL;
char* ptr = NULL;
token = strtok_s(receivebuf, seps, &ptr);
此时token为命令的首部
使用
token = strtok_s(NULL, seps, &ptr);
即可获得后面的参数部分
windows自带的dir命令以及c++自带的shell即可完成
服务器接到ls命令后发送check标志位以及系统返回信息
客户端接受到服务端信息后对check进行判断,check为0表示信息传输完毕
server
if ((strcmp("ls", token) == 0) || (strcmp("dir", receivebuf) == 0))
{
FILE* in;
char temp[100];
if (!(in = _popen("dir", "r"))) {
cout << "error" << endl;
};
if (in)
{
while (fgets(temp, sizeof(temp), in) != NULL) {
sendlen = send(socket_receive, "1", 2, 0);
sendlen = send(socket_receive, temp, 100, 0);
}
sendlen = send(socket_receive, "0", 2, 0);
memset(sendbuf, 0, 100);
_pclose(in);
};
continue;
}
client
if ((strcmp("!ls", token) == 0) && (strcmp("!dir", token) == 0))
{
FILE* in;
char temp[100];
// 直接丢进内置的os,然后输出
if (!(in = _popen("dir", "r"))) {
cout << "error" << endl;
};
if (in)
{
while (fgets(temp, sizeof(temp), in) != NULL) {
cout << temp;
}
_pclose(in);
};
continue;
}
使用getcwd()函数即可实现
server
if (strcmp("pwd", receivebuf) == 0)
{
char* path;
path = _getcwd(NULL, 0);
if (path != 0)
{
strcpy_s(sendbuf, path);
sendlen = send(socket_receive, sendbuf, 100, 0);
};
continue;
}
client
if (strcmp("!pwd", token) == 0)
{
char* path;
path = _getcwd(NULL, 0);
cout << path << endl;
}
使用put,get命令来实现
下载:服务器接到客户端的下载命令后,将全局设置中的端口号+1后创建新的socket
客户端同理,在新端口成功建立连接后,服务器先发送1表示文件流开始传输。
客户端接到1后打开文件,按行接收文件流并写入文件。
文件发送完毕后服务器发送0表示发送完毕,客户端收到0后停止写入并关闭文件。
上传的实现与下载类似。
server
if (strcmp("get", token) == 0) {
char port[MAXLINE], buffer[MAXLINE], char_num_blks[MAXLINE], char_num_last_blk[MAXLINE];
int datasock, lSize, num_blks, num_last_blk, i;
FILE* fp;
token = strtok_s(NULL, seps, &ptr);
cout << "Filename given is: " << token << endl;
data_port = data_port + 1;
sprintf_s(port, "%d", data_port);
sendlen = send(socket_receive, port, MAXLINE, 0);
// 接收到get命令后在port+1创建一个socket用于文件传输
datasock = create_socket(data_port);
datasock = accept_conn(datasock);
errno_t err = fopen_s(&fp, token, "r");
if (fp != NULL)
{
sendlen = send(socket_receive, "1", 2, 0);
// 文件发送之前先发个1给客户端,让客户端做好准备
fseek(fp, 0, SEEK_END);
lSize = ftell(fp);
rewind(fp);
num_blks = lSize / MAXLINE;
num_last_blk = lSize % MAXLINE;
sprintf_s(char_num_blks, "%d", num_blks);
sendlen = send(socket_receive, char_num_blks, MAXLINE, 0);
for (i = 0; i < num_blks; i++) {
fread(buffer, sizeof(char), MAXLINE, fp);
sendlen = send(datasock, buffer, MAXLINE, 0);
}
sprintf_s(char_num_last_blk, "%d", num_last_blk);
sendlen = send(socket_receive, char_num_last_blk, MAXLINE, 0);
if (num_last_blk > 0) {
fread(buffer, sizeof(char), num_last_blk, fp);
sendlen = send(datasock, buffer, MAXLINE, 0);
}
fclose(fp);
cout << "File upload done.\n";
}
else {
sendlen = send(socket_receive, "0", 2, 0);
// 文件发送完毕后给客户端发个0告诉客户端文件发送完成
}
}
else if (strcmp("put", token) == 0) {
// 实现原理和get类似,只不过是客户端发送文件流
char port[MAXLINE], buffer[MAXLINE], char_num_blks[MAXLINE], char_num_last_blk[MAXLINE], check[MAXLINE];
int datasock, num_blks, num_last_blk, i;
FILE* fp;
token = strtok_s(NULL, seps, &ptr);
cout << "Filename given is: " << token << endl;
data_port = data_port + 1;
sprintf_s(port, "%d", data_port);
datasock = create_socket(data_port);
send(socket_receive, port, MAXLINE, 0);
datasock = accept_conn(datasock);
recv(socket_receive, check, MAXLINE, 0);
if (strcmp("1", check) == 0) {
errno_t err = fopen_s(&fp, token, "w");
if (fp == NULL) {
cout << "Error in creating file\n";
}
else
{
recv(socket_receive, char_num_blks, MAXLINE, 0);
num_blks = atoi(char_num_blks);
for (i = 0; i < num_blks; i++) {
recv(datasock, buffer, MAXLINE, 0);
fwrite(buffer, sizeof(char), MAXLINE, fp);
}
recv(socket_receive, char_num_last_blk, MAXLINE, 0);
num_last_blk = atoi(char_num_last_blk);
if (num_last_blk > 0) {
recv(datasock, buffer, MAXLINE, 0);
fwrite(buffer, sizeof(char), num_last_blk, fp);
}
fclose(fp);
cout << "File download done." << endl;
}
}
}
client
if (strcmp("get", token) == 0) {
char port[MAXLINE], buffer[MAXLINE], char_num_blks[MAXLINE], char_num_last_blk[MAXLINE], message[MAXLINE];
int data_port, num_blks, num_last_blk, i;
SOCKET datasock;
// 新创建一个socket用来接收文件
FILE* fp;
recv(client_socket, port, MAXLINE, 0);
data_port = atoi(port);
datasock = create_socket(data_port);
token = strtok_s(NULL, seps, &ptr);
recv(client_socket, message, MAXLINE, 0);
//开始发送文件之前服务器会发个1过来
if (strcmp("1", message) == 0) {
errno_t err = fopen_s(&fp, token, "w");
if (fp == NULL)
cout << "Error in creating file" << endl;
else
{
recv(client_socket, char_num_blks, MAXLINE, 0);
num_blks = atoi(char_num_blks);
for (i = 0; i < num_blks; i++) {
recv(datasock, buffer, MAXLINE, 0);
fwrite(buffer, sizeof(char), MAXLINE, fp);
//写入文件
}
recv(client_socket, char_num_last_blk, MAXLINE, 0);
num_last_blk = atoi(char_num_last_blk);
if (num_last_blk > 0) {
recv(datasock, buffer, MAXLINE, 0);
fwrite(buffer, sizeof(char), num_last_blk, fp);
}
//文件发送完毕后服务器会发个0过来
fclose(fp);
cout << "File download done." << endl;
}
}
else {
cout << "Error in opening file. Check filename\nUsage: put filename" << endl;
}
}
else if (strcmp("put", token) == 0) {
// 实现原理和get差不多,客户端发送文件流给server
char port[MAXLINE], buffer[MAXLINE], char_num_blks[MAXLINE], char_num_last_blk[MAXLINE];
int data_port, datasock, lSize, num_blks, num_last_blk, i;
FILE* fp;
recv(client_socket, port, MAXLINE, 0);
data_port = atoi(port);
datasock = create_socket(data_port);
token = strtok_s(NULL, seps, &ptr);
errno_t err = fopen_s(&fp, token, "r");
if (fp != NULL)
{
send(client_socket, "1", 2, 0);
fseek(fp, 0, SEEK_END);
lSize = ftell(fp);
rewind(fp);
num_blks = lSize / MAXLINE;
num_last_blk = lSize % MAXLINE;
sprintf_s(char_num_blks, "%d", num_blks);
send(client_socket, char_num_blks, MAXLINE, 0);
for (i = 0; i < num_blks; i++) {
fread(buffer, sizeof(char), MAXLINE, fp);
send(datasock, buffer, MAXLINE, 0);
}
sprintf_s(char_num_last_blk, "%d", num_last_blk);
send(client_socket, char_num_last_blk, MAXLINE, 0);
if (num_last_blk > 0) {
fread(buffer, sizeof(char), num_last_blk, fp);
send(datasock, buffer, MAXLINE, 0);
}
fclose(fp);
cout << "File upload done.\n";
}
else {
send(client_socket, "0", 2, 0);
cerr << "Error in opening file. Check filename\nUsage: put filename" << endl;
}
}
客户端break循环,并关闭socket即可
if (strcmp("quit", token) == 0 || strcmp("exit", token) == 0) {
cout << "Bye";
break;
}
closesocket(socket_receive);
closesocket(socket_server);
WSACleanup();
return 0;
Linux下可以使用fork()函数
windows下可以使用线程包thread,但可能因为封装函数太大导致出现
搞不定这个问题就放弃多用户访问了
需要配合winsock等依赖,建议使用Visual Studio
可执行文件和工程文件传
gitee传送门
FTP还是基于TCP协议簇的,FTP server的实现并不是很难