利用SOCKET 套接字网络编程实现的小型FTP服务器
SOCKET服务端与客户端的代码框架
一:服务端
1. 开始创建socket();
返回值为int型,命名s_fd;
int socket(int domain, int type, int protocol)
domain(域) : AF_INET //选用IPv4因特网域
type : SOCK_STREAM/ SOCK_DGRAM : //SOCK_STREAM为TCP协议,SOCK_DGRAM为UDP协议
protocol : 0;///通常赋值为零;
2.地址准备好bind(1网络描述符.s_fd, 2地址(struct socket*)&s_addr,3大小sizeof(struct sockaddr_in)));
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
*********************************************************************************
struct sockaddr_in s_addr; //定义一个为sockaddr_in_s_addr的变量
memset(&s_addr,0,sizeof(struct sockaddr_in));//初始化结构体(清空)
s_addr.sin_family = AF_INET;//协议族为ipv4
s_addr.sin_port = htons(atoi(argv[2]));//端口号通过main(int argc,char **argv)可自主设置
inet_aton(argv[1],&s_addr.sin_addr);// 使用的IP地址
htons(atoi()):联合使用就是把输入的整型数端口号变成网络字节序输出
函数原型:uint16_t htons(uint16_t hostshort);
函数作用:htons是将整型变量从主机字节顺序转变成网络字节顺序,
就是整数在地址空间存储方式变为高位字节存放在内存的低地址处,
网络字节顺序采用big-endian排序方式。
aton()函数作用:将字符串形式的”192.168.xxx.xx“转换为网络能识别的格式
int inet_aton(const char *strptr,struct in_addr *addrptr)
将字符串转换成一个32位的网络字节序二进制值,并同过addrptr指针来存储,成功返回1,失败返回0
//2 bind
bind(sk_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
、bind函数:
函数原型:bind( SOCKET sockaddr, const struct sockaddr my_addr,int addrlen);
函数作用:套接字绑定到一个地址,并制定一个端口号。
将套接字绑定一个IP地址和端口号,因为这两个元素可以在网络环境中唯一地址表示一个进程。
3.监听 listen(1.s_fd, 2,最大可连接数)
listen函数:
函数原型:int listen(SOCKET sockfd, int backlog);
函数作用:listen函数使用主动连接套接字变为被连接套接口,使得一个进程可以接受其它进程的请求,
从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。
listen函数一般在调用bind之后-调用accept之前调用。
4.连接accept()函数
accept()系统调用主要用在基于连接的套接字类型,比如SOCK_STREAM和SOCK_SEQPACKET。它提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符。新建立的套接字不在监听状态,原来所监听的套接字也不受该系统调用的影响。
参数:
sockfd, 利用系统调用socket()建立的套接字描述符,通过bind()绑定到一个本地地址(一般为服务器的套接字),并且通过listen()一直在监听连接;
addr, 指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写,返回地址addr的确切格式由套接字的地址类别(比如TCP或UDP)决定;若addr为NULL,没有有效地址填写,这种情况下,addrlen也不使用,应该置为NULL;
备注:addr是个指向局部数据结构sockaddr_in的指针,这就是要求接入的信息本地的套接字(地址和指针)。
addrlen, 一个值结果参数,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值;
struct sockaddr_in c_addr;
int con_fd;
int cln = sizeof(struct sockaddr_in);
con_fd = accept(sk_fd,(struct sockaddr *)&c_addr,&cln);
备注:addrlen是个局部整形变量,设置为sizeof(struct sockaddr_in)。
备注:新建立的套接字准备发送send()和接收数据recv()。
备注:一般来说,实现时accept()为阻塞函数,当监听socket调用accept()时,它先到自己的receive_buf中查看是否有连接数据包;
若有,把数据拷贝出来,删掉接收到的数据包,创建新的socket与客户发来的地址建立连接;
若没有,就阻塞等待
5.accept()完了之后
可创建父子进程来处理客户端发来的数据,父进程等待,子进程处理。
read()从客户端读;
write()写进服务端;
**************************************************************************************************************
二:客户端
1. 开始创建socket();同上。
struct sockaddr_in c_addr;
struct Msg msg;
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket
c_fd = socket(AF_INET,SOCK_STREAM,0);
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
2.连接服务端connect();
struct sockaddr_in c_addr;
struct Msg msg;
memset(&c_addr,0,sizeof(struct sockaddr_in));
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
// connect
con = connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr_in));
1)connect描述
定义函数:
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
connect函数通常用于客户端建立tcp连接。
参数:
sockfd:标识一个套接字。
serv_addr:套接字s想要连接的主机地址和端口号。
addrlen:name缓冲区的长度。
返回值:
成功则返回0,失败返回-1,错误原因存于errno中。
错误代码:
EBADF 参数sockfd 非合法socket处理代码
EFAULT 参数serv_addr指针指向无法存取的内存空间
ENOTSOCK 参数sockfd为一文件描述词,非socket。
EISCONN 参数sockfd的socket已是连线状态
ECONNREFUSED 连线要求被server端拒绝。
ETIMEDOUT 企图连线的操作超过限定时间仍未有响应。
ENETUNREACH 无法传送数据包至指定的主机。
EAFNOSUPPORT sockaddr结构的sa_family不正确。
EALREADY socket为不可阻塞且先前的连线操作还未完成。
(2)SOCKET中连接过程比较
connect是套接字连接操作,connect操作之后代表对应的套接字已连接,UDP协议在创建套接字之后,可以同多个服务器端建立通信,而TCP协议只能与一个服务器端建立通信,TCP不允许目的地址是广播或多播地址,UDP允许。当然UDP协议也可以像TCP协议一样,通过connect来指定对方的ip地址、端口。
UDP协议经过connect之后,在通过sendto来发送数据报时不需要指定目的地址、端口,如果指定了目的地址、端口,那么会返回错误。通过UDP协议可以给同一个套接字指定多次connect操作,而TCP协议不可以,TCP只能指定一次connect操作。UDP协议指定第二次connect操作之后会先断口第一次的连接,然后建立第二次的连接。
(3)客户端在建立同服务器端的连接过程
第一步都会通过socket建立连接套接字;
第二步通过bind来绑定本地地址、本地端口,当然绑定操作可以不用指定;
对于UDP协议:若未指定绑定操作,那么可以通过下面connect操作来由内核负责套接字的绑定操作,若
connect又未指定,那么绑定操作只好通过套接字的写操作(sendto、sendmsg)来指定目的地址、端口,这时
套接字本地地址不会指定,为通配地址,而本地端口由内核指定,第一次sendto操作之后,插口的本地端口经
过内核指定之后就不会更改。
对于TCP协议:若未指定绑定操作,可以通过下面connect操作来由内核负责套接字的绑定操作。内核会根
据套接字中的目的地址来判断外出接口,然后指定该外出接口的IP地址为插口的本地地址。Connect操作对于TCP
协议的客户端是必不可少的,必须指定。
3.connect()之后
客户端就能从服务端获取数据,也能发送数据给服务端。
从而实现网络通讯。
**************************************************************************************************************
简易ftp服务器带有的功能
客户端输入指令
1 LS:可列出服务端当前目录下的所以文件。
2 PWD:可获取服务端当前路径。
3 CD :可进入服务端相应文件夹,也可返回服务端上层目录利用cd ..指令。
4 GET:可从服务端获取对应文件到客户端。
5 PUT:可将客户端对应文件上传到服务端。
客户端本地指令
1 LLS:查看客户端本地文件。
2 LCD:进入客户端本地的某个文件。
退出:QUIT 输入quit客户端断开与服务端连接。
一.服务端代码实现
服务端需要包含的头文件
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include "config.h"
#include <sys/stat.h>
#include <fcntl.h>
编写的头文件config.h
#define LS 0
#define GET 1
#define PWD 2
#define IFGO 3 ///一些宏定义
#define LCD 4
#define LLS 5
#define CD 6
#define PUT 7
#define QUIT 8
#define DOFILE 9
struct Msg
{
int type;
char data[1024]; //存放一些读取数据,服务端与客户端都可访问
char secondBuf[128]; //用于GET或者PUT指令,为对需要执行的文件的内容的读和写。
};
先看主函数
int main(int argc,char**argv)//可自主输入ip地址,和端口号
{
int con_fd;
int n_read;
int sk_fd;
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
struct Msg msg;
memset(&s_addr,0,sizeof(struct sockaddr_in)); //使用结构体前先初始化
memset(&c_addr,0,sizeof(struct sockaddr_in)); //使用结构体前先初始化
//1.socket
sk_fd = socket(AF_INET,SOCK_STREAM,0); ///创建套接字
if(sk_fd == -1)
{
perror("socket:");
exit(-1);
}
if(argc != 3) //调试信息如果输入的执行文件(1.文件名,2.ip地址,3.端口号)不足3个,则退出程序,重新输入
{
perror("main");
exit(-2);
}
///可参考文章前面
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&s_addr.sin_addr);
//2 bind //绑定
bind(sk_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3.listen ///监听
listen(sk_fd,10);
//4.accept //连接
int cln = sizeof(struct sockaddr_in);
while(1){
con_fd = accept(sk_fd,(struct sockaddr *)&c_addr,&cln);
if(con_fd == -1)
{
perror("accept:");
}
printf("gain connect from client:%s\n",inet_ntoa(c_addr.sin_addr));
//5.read
if(fork() == 0)
{
while(1){
memset(msg.data,0,sizeof(msg.data));
n_read = read(con_fd,&msg,sizeof(msg));
if(n_read == 0)
{
printf("client out\n");
break;
}
else if(n_read>0){
msg_handler(msg,con_fd);
}
}
}
}
close(con_fd);
close(sk_fd);
return 0;
}
单独来看while(1)里的程序
int cln = sizeof(struct sockaddr_in);
while(1){
con_fd = accept(sk_fd,(struct sockaddr *)&c_addr,&cln);
if(con_fd == -1) 调试信息 ,当没有客户端连接时,accept()会阻塞。当有客户端连入
{ 时,成功返回0,失败返回-1,并且利用perror显示原因
perror("accept:");
}
printf("gain connect from client:%s\n",inet_ntoa(c_addr.sin_addr));//如果用户成功接入,在服务端打印接入的客户端的ip地址。
//5.read
if(fork() == 0) ///利用父子进程,使父子进程同时执行不同的代码段,这在网络服务进程中很常见,父进程等待客户端的服务请求。当这种请求到达时父进程调用fork(),使子进程处理此请求。父进程继续等待下一个请求到来。 fork的返回值大于>0时为父进程,返回值等于0时为子进程。
{
///用子进程来处理客户端的请求
while(1){
memset(msg.data,0,sizeof(msg.data)); //清空msg.data里的数据,初始化
n_read = read(con_fd,&msg,sizeof(msg)); //读取从客户端写入的内容,读到msg里用&地址符号,读取的大小为整个msg变量的大小。
if(n_read == 0)
{
printf("client out\n");
break;
}
else if(n_read>0){ /如果读到数据了 调用msg_handler(msg,con_fd)函数处理数据,
该函数看下文。
msg_handler(msg,con_fd);
}
}
}
}
**************************************************************************************************************
msg_handler(msg,con_fd)函数
void msg_handler(struct Msg msg,int fd) //两个参数,1.为存放数据的结构体,2.为accept()函数返回的描述符。
{
char dataBuf[1024]={0};
char *file = NULL;
int fdfile;
int w;
printf("cmd:%s\n",msg.data); ///打印客户端输入的指令。
int ret = get_cmd_type(msg.data); /// 利用get_cmd_type()函数判断 ret的返回值,再进行选择分支。
int get_cmd_type(char *cmd)
{
if(!strcmp("ls",cmd)) return LS;
if(!strcmp("quit",cmd)) return QUIT;///利用比较函数strcmp()来处理这些只有单个字符串的指令
if(!strcmp("pwd",cmd)) return PWD;///客户端输入的指令与相对指令比较,如果完全相同,返回相对应的指令
///用于switch函数。
if(strstr(cmd,"cd")!=NULL) return CD;
if(strstr(cmd,"get")!=NULL) return GET;///strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串。
如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。
if(strstr(cmd,"put")!=NULL) return PUT;
return 100;
}
switch(ret)
{
case LS:
case PWD:
msg.type = 0;
FILE *r = popen(msg.data,"r");//popen函数利用第一个参数的指令执行一个shell以运行命令来开启一个进程,第二个参数是"r"则文件指针连接到
//command的标准输出;如果type是"w"则文件指针连接到command的标准输入。
fread(msg.data,sizeof(msg.data),1,r);//从popen的返回指针”r“里面读取数据到msg.data,一次读sizeof(msg.data)这么多,读一次。
write(fd,&msg,sizeof(msg));//1.写进fd文件指针,在msg地址下写msg的内容,写多大?整个msg的大小。
break;
case CD:
msg.type = 1;
char *dir = getDir(msg.data); ///利用getDir()函数取出cd xx后,的返回值为xx,
printf("dir:%s\n",dir);//将xx的字符串形式打印出来。
chdir(dir);//chdir 是C语言中的一个系统调用函数(同cd),用于改变当前工作目录,其参数为Path 目标目录,可以是绝对目录或相对目录
//成功返回0 ,失败返回-1
break;
case GET:
file = getDir(msg.data); //将get xx 的返回值xx等于file这个字符指针
if(access(file,F_OK) == -1)///access函数用来判断指定的文件或目录是否存在(F_OK),
//已存在的文件或目录是否有可读(R_OK)、可写(W_OK)、可执行(X_OK)权限。F_OK、R_OK、W_OK、X_OK这四种方式通过access函数中的第二个参数mode指定。如果指定的方式有效,则此函数返回0,否则返回-1。
{
strcpy(msg.data,"NO this file");//若get的文件不存在向客户端写”NO this file“,客户端打印。
write(fd,&msg,sizeof(msg));
}
else{
msg.type = DOFILE;//若文件存在,提醒客户端创建文件,详情看客户端代码。
fdfile = open(file,O_RDWR);//open一个get xx的xx这个文件,权限为可读可写。
read(fdfile,dataBuf,sizeof(dataBuf));//读open的这个文件的内容,读到dataBuf里面来,读整个databuf的大小。
close(fdfile);//读完之后关闭这个文件。
strcpy(msg.data,dataBuf);//利用拷贝函数,将读到的dataBuf的内容拷贝进msg.data里。
write(fd,&msg,sizeof(msg));//写进fd里,写整个msg的内容和大小,主要为了客户端那边读取数据在创建的文件里写入。
}
break;
case PUT:
fdfile = open(getDir(msg.data),O_RDWR|O_CREAT,0777);//open一个客户端发来的put xx ,xx这个文件,若文件不存在则创建,权限为可读可写
//0777代表该文件拥有者对该文件拥有读写操作的权限
//该文件拥有者所在组的其他成员对该文件拥有读写操作的权限
//其他用户组的成员对该文件也拥有读写操作权限
w = write(fdfile,msg.secondBuf,strlen(msg.secondBuf));//会把msg.secondBuf的内容写到fdfile这个文件指针里。
if( w != -1)
{
printf("put success\n");// 调试信息,如果写成功则打印信息提醒。
fflush(stdout);//
}
close(fdfile);//open的文件,进行读写操作完之后,必须关闭。
break;
case QUIT://客户端退出。
printf("client quit!\n");
exit(-1);
}
}
getDir()函数
char * getDir(char *cmd)
{
char *p;
p = strtok(cmd," ");//strok分割字符串,以” 空格“分割字符串,返回值为cmd里的字符串
//
p = strtok(NULL," ");//当执行完这个后返回值为空格后的字符串。一般搭配使用,用来返回第二个
//字符串来实现我们ftp服务器想要的功能。
return p;
}
二、客户端代码实现
客户端需要包含的头文件
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include "config.h"
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
编写的config.h头文件
#define LS 0
#define GET 1
#define PWD 2
#define IFGO 3 ///一些宏定义
#define LCD 4
#define LLS 5
#define CD 6
#define PUT 7
#define QUIT 8
#define DOFILE 9
struct Msg
{
int type;
char data[1024]; //存放一些读取数据,服务端与客户端都可访问
char secondBuf[128]; //用于GET或者PUT指令,为对需要执行的文件的内容的读和写。
};
先看主函数
int main(int argc,char **argv)
{
int c_fd;
int con;
int n_read;
struct sockaddr_in c_addr;
struct Msg msg;
memset(&c_addr,0,sizeof(struct sockaddr_in));//清空结构体,初始化。
//1.socket
c_fd = socket(AF_INET,SOCK_STREAM,0);//创建socket套接字,返回网络描述符。
if(c_fd == -1)
{
perror("socket:");//创建失败的调试信息
exit(-1);
}
c_addr.sin_family = AF_INET; //参考文章开头,大同小异
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
// connect
con = connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr_in));//与服务端进行连接。与服务端accept函数相似,参数也相似,可参考上文。
if(con == -1)
{
perror("connect:");//连接失败的调试信息
exit(0);
}
printf("connect ...\n");//连接成功打印该信息
int mark = 0;
while(1){
memset(msg.data,0,sizeof(msg.data));
if(mark == 0) printf(">");
gets(msg.data);
if(strlen(msg.data) == 0)
{
if(mark == 1)
{
printf(">");
}
continue;
}
mark = 1;
int ret = cmd_handler(msg,c_fd);
if(ret>IFGO)
{
putchar('>');
fflush(stdout);
continue;
}
if(ret == -1)
{
printf("command not \n");
printf(">");
fflush(stdout);
continue;
}
handler_server_message(c_fd,msg);
}
return 0;
}
单独来看while(1)里的程序
int mark ==0;
while(1){
memset(msg.data,0,sizeof(msg.data));//清空,初始化
if(mark == 0) printf(">");//为了客户端输入界面看起来比较舒服
gets(msg.data);//输入对应指令。
if(strlen(msg.data) == 0)//如果输入的内容为空,则重新再次输入,利用continue。
{
if(mark == 1)
{
printf(">");
}
continue;//下面程序不走了,重新从循环开始走,回到while(1)开头,重新循环
}
mark = 1;//如果输入不为空;往下继续
int ret = cmd_handler(msg,c_fd);//利用cmd_handler函数判断ret的返回值。
int cmd_handler(struct Msg msg,int fd)
{
char *dir = NULL;
char buf[32];
int ret;
int filefd;
ret = get_cmd_type(msg.data);
switch(ret)
{
case LS:
case PWD:
case CD:
msg.type = 0;
write(fd,&msg,sizeof(msg));//write()会把参数地址所指的内存写入sizeof(msg)个字节到参数fd所指的文件内。写的时服务端写入的内容。
break;
case GET:
msg.type = 2;
write(fd,&msg,sizeof(msg));///write()会把参数地址所指的内存写入sizeof(msg)个字节到参数fd所指的文件内。写的是客户端需要从服务端上得到的文件的内容。
break;
case PUT:
strcpy(buf,msg.data);//输入的指令拷贝给buf。
dir = getdir(buf);//一样的取 put xx ,xx这个字符串
if(access(dir,F_OK) == -1)//判断xx这个文件是否存在。
{
printf("%s not exsit\n",dir);//不存在的话,打印提示信息。
}
else{
filefd = open(dir,O_RDWR);//存在的话,打开这个xx文件权限为可读可写。
read(filefd,msg.secondBuf,sizeof(msg.secondBuf));//读xx文件里的内容读到
//msg.secondBuf里,读的大小为整个seconBuf的大小。
close(filefd);//读完后关闭。
write(fd,&msg,sizeof(msg));//把整个msg里的内容写到fd,为服务端读写xx文件的
//内容做准备。
}
break;
case LLS:
system("ls");//本地客户端展示客户端当前目录的文件。
break;
case LCD://进入到客户端的相对应文件。
dir = getdir(msg.data);
chdir(dir);
break;
case QUIT://断开连接
strcpy(msg.data,"quit");//告诉服务端,我断开连接了
write(fd,&msg,sizeof(msg));
close(fd);
exit(-1);
}
return ret;
}
if(ret>IFGO)//
{
putchar('>');
fflush(stdout);//立刻输出,puchar的内容
continue;
}
if(ret == -1)//没有这个指令,重新输入
{
printf("command not \n");//打印提醒信息
printf(">");
fflush(stdout);
continue;//回到while(1)开头,重新循环。
}
handler_server_message(c_fd,msg);//处理服务端的函数
}
return 0;
}
handler_server_message()函数:在客户端输出从服务端获取的数据信息。
void handler_server_message(int c_fd,struct Msg msg)
{
int n_read;
struct Msg msgget;
int newfilefd;
n_read = read(c_fd,&msgget,sizeof(msgget));//read函数(读取文件)read函数可以读取文件。读取文件指从某一个已打开地文件中,读取一定数量地字符,然后将这些读取的字符放入某一个预存的缓冲区内,供以后使用
if(n_read == 0)//如果读的数据为0,打印服务器已退出。
{
printf("server is out quit\n");
exit(-1);
}
else if(msgget.type == DOFILE)//如果有数据读到,判断type是不是等于DOFILE。可参考服务端的GET指令。如果等于执行以下操作。
{
char *p = getdir(msg.data);//返回get xx ,xx这个字符串,赋值给字符型指针。
newfilefd = open(p,O_RDWR|O_CREAT,0600);//open这个xx文件,如果不存在则创建
//0600->仅拥有者具有文件的读取和写入权限
write(newfilefd,msgget.data,strlen(msgget.data));//从msgget.data里写数据到newfilefd这个文件描述符,写全部内容。为了让xx这个文件和服务端中的xx文件内容一致。
putchar('>');
fflush(stdout);//这个简单的c程序中用到了fflush(stdout),目的是清空缓冲,强制结果马上显示到屏幕上。
}
else{
putchar('\n');
printf("*************************************\n");//为了客户端显示数据的美观
printf("\n%s\n",msgget.data);//打印得到的服务端的数据
printf("*************************************\n");
putchar('\n');
putchar('>');
fflush(stdout);
}
}
**************************************************************************************************************
成果展示:
1.左边为服务端,右边为客户端。连接成功后服务端显示客户端的ip地址。客户端显示连接成功
2.客户端输入 “ls”,客户端显示服务端当前目录的所有文件。
3.客户端输入“pwd”,客户端显示服务端的当前路径。
4.输入cd xx指令
cd .. 为返回上层路径
cd ftp文件
5.输入"lls"指令,显示客户端当前路径下的所有文件。
6.输入“lcd”指令,可进入或返回客户端下的文件。
例如,进入IPC文件
7.输入“get xx”指令
需要从服务端get得到a.out 文件到客户端的路径下,如图所示。
8.输入“put xx”指令
需要把客户端路径下的struct.c文件放到服务端当前路径下。
总结
这是一个简易的ftp服务器,可以实现多机间的通讯,同时可对服务器端的文件进行操作,可以得到服务端文件,也可以发送客户端文件给服务端,功能不多,适合刚接触linux网络编程的伙伴练手,将来有空会继续添加功能。一个linux编程小白。