目录
Lab目标
一、知识补充
二、具体实现
1.数据报文格式和字符串处理
2.open函数
3.auth
4.ls
5.get和put
三、总结
请同学们仅以此作为借鉴,,请务必自己先试着写写,然后遇到问题再看看文章。听闻我的文章间接导致了一部分同学被发了查重邮件,请大家务必谨慎再谨慎,不必操之过急。我本人在信科水平算是非常差的,写这篇文章当初纯粹是为了找工作,放在简历上显得自己很勤奋,有经常做总结的习惯(实际并没有),让hr和老板们看着觉得我还不错而已(实际挺拉的)。我这样的水平,计网的lab1也只用了3天就完全完成了(中间熬了个夜到4点想速成,结果脑子不清醒写了一坨,第二天删了重来反而很快就过了),这中间的一些坑(比如转大小端)我也会去问问室友,也都能很快解决。所以希望大家也能有所收获。
ps:本人靠着计网lab几乎就足够在就业行情并不好的23年找到自己满意的工作了,计网lab的教程也非常给力,对我这种恐惧写lab的菜狗都非常友好(本人写lab3确实比较痛苦,因为没什么人可以问)。所以大家一定要珍惜这次机会。共勉!
Lab目标
简介:MyFTP是我们为了方便同学们快速理解POSIX API设计的一个简单的Lab,在这个Lab中你需要完成一个简单的FTP Server和FTP Client CLI(CLI指命令行界面)
- MyFTP的Client支持以下的命令
- open <IP> <port>
- auth <username> <password>:向对侧进行身份验证
- ls:获取对方当前运行目录下的文件列表,一个样例输出如下
- get <filename>:将Server运行目录中的<filename>文件存放到Client运行目录的<filename>中
- put <filename>:将Client运行目录中的<filename>文件存放到Server运行目录的<filename>中
- quit:如果有连接则先断开,后关闭Client
- MyFTP的Server需要支持如下的功能特点
- 权限控制:用户需要登录这里简化为用户名为user,密码为123123
- 获取文件列表:这里文件列表由指令ls生成,可以使用popen或者pipe+fork+execv的手段获取其他进程的输出结果
- 下载文件
- 上传文件
下图是实现如上功能后应该得到的输出
总体而言,这个Lab难度不大,和同学交流后大家的主要时间都花在处理字符串上。我认为亲自完成这个Lab会收获很好的入门体验。在写完open和auth功能后就会基本了解、熟练各个操作,剩下的ls、get、put就会变得非常简单
一、知识补充
make/git知识在这里就不补充了,详情可见PKU网络课程实践
socket编程:
这里直接引用助教老师给的例子
首先是Server端
sock = socket(AF_INET, SOCK_STREAM, 0); // 申请一个TCP的socket
struct sockaddr_in addr; // 描述监听的地址
addr.sin_port = htons(23233); // 在23233端口监听 htons是host to network (short)的简称,表示进行大小端表示法转换,网络中一般使用大端法
addr.sin_family = AF_INET; // 表示使用AF_INET地址族
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); // 监听127.0.0.1地址,将字符串表示转化为二进制表示
bind(sock, (struct sockaddr*)&addr, sizeof(addr));
listen(sock, 128);
int client = accept(sock, nullptr, nullptr);
其次是Client
sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_port = htons(23233);
addr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); // 表示我们要连接到服务器的127.0.0.1:23233
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
这里就基本完成了服务端和客户端的连接,如果再加上数据报文的发送和接收(send和recv函数),实际上就已经完成了open的功能。其中的几个比较重要的函数可以自行搜索了解,尤其重点要注释好函数需要的参数的意义和返回值。(需要引用的头文件可自行查询)
首先给出sockaddr_in的数据结构
struct sockaddr_in 是描述 IPv4 套接字地址的结构• sin_family : AF_INET(IPv4) ,编程时实际使用地址类型• sin_port :存放端口号 ( 按照网络字节序存储 )• sin_addr :存放 32 位 IP 地址 ( 无符号整数• sin_zero:为与 sockaddr 大小兼容而保留的空字节
struct sockaddr可以看作struct sockaddr_in的父类
bind函数用来将服务器本地套接字地址sa与描述符socket_fd绑定,在服务器端调用。其返回值建议进行一层判断,若不为0则可输出“bind error”(在测试时需要换port)
connect函数用于建立连接,客户端调用connect向服务器发起建立连接的请求。
listen函数用于监听。
(这几个函数建议初学者再上网搜索下)
在后续编写时要注意大小端的转换(利用htons等函数),如果编译遇到栈溢出的报错则大概率是这个原因。
在这个例子中端口值默认为23233。
那么server和client如何互相传输呢?助教老师的教程又给出了例子。这个例子实现了Client向Server发送字符串“Hello Server”,而Server收到数据后又会传回Client。
Server:
char buffer[128];
size_t l = recv(client, buffer, 128, 0);
send(client, buffer, l, 0);
Client
char buffer[128];
sprintf(buffer, "Hello Server");
send(sock, buffer, strlen(buffer)+1, 0);
recv(sock, buffer, 128, 0);
这里面最重要的就是send和recv函数,但是使用默认的send和recv并不会一直正确,当遇到大文件传输时,有时并不是所有数据都能成功放入缓冲区,你可能无法像自己想的那样从缓冲区内读取定长的内容,因为建立safe_send和safe_recv是非常重要的。
这里给出safe_send的例子
size_t ret = 0;
while (ret < len) {
size_t b = send(sock, buffer + ret, len - ret, 0);
if (b == 0) printf("socket Closed"); // 当连接断开
if (b < 0) printf("Error ?"); // 这里可能发生了一些意料之外的情况
ret += b; // 成功将b个byte塞进了缓冲区
}
这段代码很好理解,如果没有发送完自己需要的长度,则while循环会保证多次send直到达成目的。safe_recv同理,除了换recv和对应sock不用 进行额外处理。
二、具体实现
1.数据报文格式和字符串处理
数据报文老师的指导上已经明确给出
struct {
byte m_protocol[MAGIC_NUMBER_LENGTH]; /* protocol magic number (6 bytes) */
type m_type; /* type (1 byte) */
status m_status; /* status (1 byte) */
uint32_t m_length; /* length (4 bytes) in Big endian*/
} __attribute__ ((packed));
其中 __attribute__ ((packed)) 是为了进行数据结构的对齐
m_protocol为默认的"\xe3myftp"
m_type则用于区分数据报文是属于哪一部分,如client的open请求时发送数据报文的type是0xa1,server回应open的数据报文的type为0xa2。
m_status在一些功能中鉴定是否成功,如client想下载server中的文件,若server可以找到这个文件,status设置为1,client在读到这一数字后才会进行下载操作(实际为通过socket传字符串),反之若为0,client将直接结束这个操作。
m_length在处理payload字符串时非常重要,server和client在接收对方的额外数据时,需要通过length来计算长度,而有了长度才能知道从缓冲区中接收多少内容,length计算不准会导致意想不到的错误。一般情况下length为12。
下面给出我成功实现的数据报文结构体
struct Header
{
char m_protocol[MAGIC_NUMBER_LENGTH]; /* protocol magic number (6 bytes) */
uint8_t m_type; /* type (1 byte) */
uint8_t m_status; /* status (1 byte) */
uint32_t m_length; /* length (4 bytes) in Big endian*/
} __attribute__ ((packed));
另外需要处理命令行的读取,即通过空格来拆解字符串,以达到提取关键信息的目的。如键入“open 127.0.0.1 12323”,则函数处理后得到“open”、“127.0.0.1”、“12323”。我使用getcommand函数来读取字符串,parseline函数来分割字符串并存储,只要达成所需的目的即可。
int getcommand(char *buf)
{
memset(buf,0,buffsize);
int length;
fgets(buf,buffsize,stdin);
length=strlen(buf);
buf[length-1]='\0';
return strlen(buf);
}
void parseline(char* cmd)
{
int i,j,k;
int len = strlen(cmd);
int num=0;
for(i=0;i<maxargs;i++)
{
argv[i] = NULL;
}
char tmp[buffsize];
j=-1;
for(i=0;i<=len;i++)
{
if(cmd[i]==' '||i==len)
{
if(i-1>j)
{
cmd[i]='\0';
argv[num++] = cmd+j+1;
}
j = i;
}
}
argc = num;
argv[argc] = NULL;
}
最后还要注意维持状态码的更新,教程中给出了FSM示意图
状态码有三个值即可
int status_flag=0;// 0 unconnected
// 1 connect success
// 2 auth success
safe_send的代码如下(safe_recv同理)
void safe_send(int sock,Header* buffer, int len,int d)
{
size_t ret = 0;
while (ret < len)
{
size_t b = send(sock, buffer + ret, len - ret, 0);
if (b == 0) printf("socket Closed"); // 当连接断开
if (b < 0) printf("Error ?"); // 这里可能发生了一些意料之外的情况
ret += b; // 成功将b个byte塞进了缓冲区
}
}
void safe_send(int sock,char* buffer, int len,int d)
{
size_t ret = 0;
while (ret < len)
{
size_t b = send(sock, buffer + ret, len - ret, 0);
if (b == 0) printf("socket Closed"); // 当连接断开
if (b < 0) printf("Error ?"); // 这里可能发生了一些意料之外的情况
ret += b; // 成功将b个byte塞进了缓冲区
}
}
到这里准备工作就结束了,配合上前面的socket编程,open功能应该能被顺利解决
2.open函数
首先是数据报文的初始化,来看一下要求的数据报文内容
而open功能的发送顺序如下图所示
open的整个过程只要按照上图完成即可:客户端输入命令行指令,程序通过读取命令行字符串识别出要进行open操作,随后进入open函数,client发送报文—— server接收报文并完成连接——server发送报文——client接收报文。
我们对各自的报文进行初始化(后续功能的初始化函数,包括FILE_DATA的都不再放出,结构大同小异)
Server:(其中recv_header是用来接受Client发送的数据报文)
void OPEN_CONN_REPLY()
{
memcpy(recv_header.m_protocol,magic_number,6);
recv_header.m_type=0xa2;
recv_header.m_status=1;
recv_header.m_length=12;
}
Client:
void OPEN_CONN_REQUEST()
{
memcpy(cli_header.m_protocol,magic_number,6);
cli_header.m_type=0xa1;
cli_header.m_status=0;
cli_header.m_length=htonl(12);
}
结合前面的parseline函数,在client中进入open的分支,代码如下
void open()
{
struct sockaddr_in servaddr;
sockfd=socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
//初始化地址结构体
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(stoi(argv[2]));
inet_pton(AF_INET,argv[2],&servaddr.sin_addr);
//表示我们要连接到服务器
int ret=connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
if(ret==-1)
{
cout<<"Error:connect error_1"<<endl;
}
else
{
memset(recv_buf,0,buffsize);
memset(send_buf,0,buffsize);
OPEN_CONN_REQUEST();
safe_send(sockfd,&cli_header,header_length,0);
safe_recv(sockfd,recv_buf,header_length,0);
recv_header=(struct Header*)recv_buf;
if(str_equal(magic_number, recv_header->m_protocol,MAGIC_NUMBER_LENGTH)
&& recv_header-> m_type == 0xA2 && recv_header->m_status == 1)
{
fprintf(stdout,"Server connection accepted.\n");
status_flag = 1; //connect success
}
else
{
fprintf(stdout,"Error:connect error_2\n");
}
}
}
注意要检查Server发来的报文内容是否正确(其中str_equal用于检查m_protocol内容是否是“/xe3myftp”)
Server:
我的Server的结构不是很好看,在main部分进行了状态码的判定,并且根据m_type进入相应功能。实际上可以用函数进行结构简化,看起来会更清晰
int main(int argc, char ** argv)
{
//listenfd是监听套接字,connfd是与客户端数据通信的套接字
socklen_t clilen;
struct sockaddr_in cliaddr,servaddr;
listenfd= socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(stoi(argv[2]));
int bind_flag;
bind_flag=bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,LISTENQ);
if(bind_flag!=0)
{
cout<<"Error:bind error"<<endl;
}
else
{
cout<<"else"<<endl;
while(1)
{
cout<<status_flag<<endl;
if(status_flag==0)
{
//cout<<"open"<<endl;
clilen=sizeof(cliaddr);
connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
//accept返回值(connfd)是用来和客户端交换数据的临时套接字描述符
memset(recv_buf,0,buffsize);
memset(send_buf,0,buffsize);
safe_recv(connfd,recv_buf,header_length,0);
recv_header2=(struct Header*)recv_buf;
if(recv_header2->m_type==0xa1)
{
status_flag=1;
open();
cout<<"open"<<status_flag<<endl;
}
else
{
cout<<"no connect"<<endl;
}
}
else
{
//其他功能处理
}
}
}
}
在main函数的主体中已经接收了一次报文,故server的open函数主要就是发送数据报文。
PS:这里的recv_header实际上是要发送的报文,这个命名很差,但是我懒得改了orz
void open()
{
OPEN_CONN_REPLY();
recv_header.m_length=htonl(12);
safe_send(connfd,&recv_header,header_length,0);
}
到这里就完成了open的实现
3.auth
auth和open的唯一区别在于client需要发送一段额外的字符串,包含着用户名和密码的信息(默认为user和123123)
实际上就是client发送两次(数据报文和payload),而server接收两次。将收、发分为两次,可以帮助接收方明确payload的大小(数据报文大小恒为12,从数据报文中读出m_length后再减12即使payload的大小)。
Client:
void auth(int argc, char ** argv)
{
memset(recv_buf,0,buffsize);
memset(send_buf,0,buffsize);
memset(payload,0,sizeof(payload));
AUTH_REQUEST();
//payload memcopy
strcpy(payload,argv[1]);
payload[strlen(payload)]=' ';
strcat(payload, argv[2]);
int send_length=12+strlen(payload)+1;
cli_header.m_length=send_length;
cli_header.m_length=htonl(cli_header.m_length);
int payload_length=send_length-12;
payload[payload_length-1]='\0';
safe_send(sockfd,&cli_header,header_length,0);
safe_send(sockfd,payload,payload_length,0);
safe_recv(sockfd,recv_buf,header_length,0);
recv_header=(struct Header*)recv_buf;
if(str_equal(magic_number, recv_header->m_protocol,MAGIC_NUMBER_LENGTH)
&& recv_header-> m_type == 0xA4 && recv_header->m_status == 1)
{
fprintf(stdout,"Authentication granted.\n");
status_flag = 2; //auth success
}
else
{
fprintf(stdout,"Error: Authentication rejected. Connection closed\n");
close(sockfd);
status_flag=0;
}
}
Server:
注意由于在main中已经接收了数据报文,所以auth函数中仅仅接收payload即可
void auth()
{
init();
AUTH_REPLY();
recv_header.m_length=htonl(12);
int payload_length=ntohl(recv_header2->m_length)-12;
safe_recv(connfd,payload_buf,payload_length,0);
if(str_equal(payload_buf,user,payload_length))
{
recv_header.m_status=1;
safe_send(connfd,&recv_header,header_length,0);
status_flag=2;
cout<<"auth"<<status_flag;
return;
}
else
{
status_flag=0;
safe_send(connfd,&recv_header,header_length,0);
}
}
4.ls
如果已经熟练掌握了包含payload的收发,后面三个功能都可以很快写完。
ls实际上就是利用popen直接得到linux中键入ls的结果,以读文件的方式将该结果传入payload,Client接收Server发来的payload后打印即可。
所以先看Server:
void ls()
{
init();
LIST_REPLY();
FILE *file;
char line[_LINE_LENGTH];
file = popen("ls", "r");
int flag=1;
if (NULL != file)
{
while (fgets(line, _LINE_LENGTH, file) != NULL)
{
if(flag)
{
strcpy(payload_buf,line);
flag=0;
}
else
{
strcat(payload_buf,line);
}
}
}
pclose(file);
int payload_length=strlen(payload_buf);
payload_buf[payload_length]='\0';
payload_length++;
recv_header.m_length=htonl(12+payload_length);
payload_length=htonl(payload_length);
safe_send(connfd,&recv_header,header_length,0);
safe_send(connfd,payload_buf,payload_length,0);
}
Client:
(换行符也会被读入payload,故不需要关心换行,Client会直接打印出来)
void ls()
{
fprintf(stdout,"----- file list start -----\n");
memset(recv_buf,0,buffsize);
memset(send_buf,0,buffsize);
LIST_REQUEST();
safe_send(sockfd,&cli_header,header_length,0);
safe_recv(sockfd,recv_buf,header_length,0);
recv_header=(struct Header*)recv_buf;
int payload_length=ntohl(recv_header->m_length)-12;
if(str_equal(magic_number, recv_header->m_protocol,MAGIC_NUMBER_LENGTH)
&& recv_header-> m_type == 0xA6 )
{
safe_recv(sockfd,payload,payload_length,0);
for(int i=0;i<payload_length;i++)
{
cout<<payload[i];
}
fprintf(stdout,"----- file list end -----\n");
}
}
5.get和put
put和get可以互相抄写(put的server和get的client一样,put的client和get的server一样),因此在这里给出get的代码
其实这两个函数没有难点但是有些麻烦,建议在头脑清醒的时候慢慢写,否则遇到bug还是会头疼。
注意读完文件内容存入payload后,不需要在结尾加\0
如果对于字符串处理没有信心,建议将发送报文前和后,分别打印payload长度和payload内容,来看传、收处理是否有问题。
读写文件的部分如果不熟悉可以上网搜索,有很多种实现方法。
数据报文仅仅给出get的,详细内容(包含put的数据报文)依然见PKU网络课程实践
流程如下:
Client:
void get(int argc, char ** argv)
{
memset(recv_buf,0,buffsize);
memset(send_buf,0,buffsize);
GET_REQUEST();
memcpy(payload,argv[1],strlen(argv[1]));
//cout<<"argv changdu"<<strlen(argv[1])<<endl;
//safe_send(sockfd,&cli_header,header_length,0);
int payload_length=strlen(payload);
payload[payload_length]='\0';
payload_length++;
/*cout<<"test1"<<endl;
for(int i=0;i<payload_length;i++)
{
cout<<payload[i];
}
cout<<endl;
cout<<"test2"<<endl;
*/
//success
cli_header.m_length=12+payload_length;
cli_header.m_length=htonl(cli_header.m_length);
safe_send(sockfd,&cli_header,header_length,0);
safe_send(sockfd,payload,payload_length,0);
safe_recv(sockfd,recv_buf,header_length,0);
recv_header=(struct Header*)recv_buf;
//cout<<int(recv_header->m_status)<<endl;
//success
if(str_equal(magic_number, recv_header->m_protocol,MAGIC_NUMBER_LENGTH)
&& recv_header-> m_type == 0xA8 && recv_header->m_status == 1)
{
//cout<<1<<endl;
//success
memset(recv_buf,0,buffsize);
safe_recv(sockfd,recv_buf,header_length,0);
Header* get_header=(struct Header*)recv_buf;
payload_length=ntohl(get_header->m_length)-12;
//cout<<"file length "<<payload_length<<endl;
memset(payload,0,sizeof(payload));
safe_recv(sockfd,payload,payload_length,0);
FILE* new_file;
new_file=fopen(argv[1],"w");
int j=0;
if (new_file == NULL)
{
fprintf(stdout,"write file error...\n");
}
fwrite(payload, sizeof(size_t), payload_length, new_file);
fclose(new_file);
/*for(int i=0;i<payload_length;i++)
{
cout<<payload[i];
}
success
*/
fprintf(stdout,"File downloaded.\n");
}
}
Server:
void get()
{
//cout<<ntohl(recv_header2->m_length)<<endl;
//success 12+10
int payload_length=ntohl(recv_header2->m_length)-12;
//cout<<"test 1.payload_length 10 ? "<<payload_length<<endl;
//success
safe_recv(connfd,payload_buf,payload_length,0);
//FOPEN
/*for(int i=0;i<payload_length;i++)
{
cout<<payload_buf[i];
}
cout<<endl;
*/
//success chang.txt
FILE *fp;
fp=fopen(payload_buf,"r");
GET_REPLY();
recv_header.m_length=htonl(12);
if(fp == NULL)
{
fclose(fp);
safe_send(connfd,&recv_header,header_length,0);
//cout<<int(recv_header.m_status)<<endl;
printf("Fail to open file!\n");
return;
}
else
{
recv_header.m_status=1;
safe_send(connfd,&recv_header,header_length,0);
//printf("%d",recv_header.m_status); //1
//success
memset(payload_buf,sizeof(payload_buf),0);
unsigned char character = 0;
int i=0;
while (!feof(fp))
{
character = getc(fp);
payload_buf[i]=character;
i++;
}
i--;
fclose(fp);
payload_length=i;
//cout<<"payload_length ,file_length ?? "<<payload_length<<endl;
//success
/*fseek(fp, 0, SEEK_END);
int fileSize;
fileSize = ftell(fp);
cout<<"filesize ?"<<fileSize<<endl;;
fread(payload_buf,fileSize,sizeof(char),fp);
payload_buf[fileSize]='\0';
fclose(fp);
fileSize++;
cout<<"filesize ?"<<fileSize<<endl;;
*/
FILE_DATA();
recv_header.m_length=htonl(12+payload_length);
safe_send(connfd,&recv_header,header_length,0); //FILE_DATA SEND
//printf("%d",payload_length);
//cout<<endl;
//printf("%d",fileSize);
/*for(int i=0;i<payload_length;i++)
{
cout<<payload_buf[i];
}
success
*/
safe_send(connfd,payload_buf,payload_length,0);
return;
}
}
(quit功能省略)
目前得分
三、总结
lab1的整体难度并不高,但是最后早点开始,平和的心态可以更快的完成,如果着急完成反而会因为细节处理的不好出bug(大佬忽略)
我的整体代码可能因为状态机的问题实际上仍然存在问题,但是由于test并不会检测这一部分,所以基础的90分可以拿到。文章来源:https://www.toymoban.com/news/detail-732030.html
总之这个lab可以让人快速了解socket和ftp的工作原理,也为后面的lab打下了基础,所以一定要自己完成。文章来源地址https://www.toymoban.com/news/detail-732030.html
到了这里,关于北京大学计算机网络lab1——MyFTP的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!