北京大学计算机网络lab1——MyFTP

目录

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指命令行界面)

  1. MyFTP的Client支持以下的命令
    • open <IP> <port>
    • auth <username> <password>:向对侧进行身份验证
    • ls:获取对方当前运行目录下的文件列表,一个样例输出如下
    • get <filename>:将Server运行目录中的<filename>文件存放到Client运行目录的<filename>中
    • put <filename>:将Client运行目录中的<filename>文件存放到Server运行目录的<filename>中
    • quit:如果有连接则先断开,后关闭Client

  2. 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分可以拿到。

总之这个lab可以让人快速了解socket和ftp的工作原理,也为后面的lab打下了基础,所以一定要自己完成。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值