基于TCP协议的数据收发实验

来源:互联网 发布:linux select epoll 编辑:程序博客网 时间:2024/05/02 02:13

基于TCP协议的接收和发送实验

选修了一门TCP/IP协议分析及应用,做了几个小实验,这里把实验的原理、自己的代码和经验教训和大家分享一下。

实验中的全部代码托管在Github上,请通过 fork + pull request 方法来帮助改进项目。

  • 基于TCP协议的接收和发送实验
        • 导语
    • 一实验内容
    • 二套接字编程基础
      • 1 套接字地址结构
    • 三TCP网络编程流程
        • TCP网络编程框架
    • 四TCP协议程序设计的常用函数
      • 1 创建网络插口函数socket
      • 2 绑定一个地址端口对bind
      • 3 监听本地端口listen
      • 4 接受一个网络请求accept
      • 5 连接目标网络服务器connect
      • 6 写入函数write
      • 7 读取数据函数read
      • 8 关闭套接字函数close
    • 五实验代码实现
    • 六实验结果

导语:

TCP协议是TCP/IP协议中很重要的一个协议,由于他传输的稳定性,在很多程序中都在使用,例如HTTP、FTP等协议都是在TCP的基础上进行构建的。
本实验目的是使用因特网提供的TCP传输协议,实现一个简单的UDP客户/服务器程序,以了解传输层所提供的TCP服务的特点,应用层和传输层之间的软件接口风格,熟悉socket机制和TCP客户端/服务器方式程序的结构。
本文在介绍TCP协议收发技术的同时,提供了相关代码,并将笔者在debug过程中的经验和教训带给读者。

一,实验内容

设计与实现一个简单的TCP echo客户/服务器程序,完成以下功能:
客户从标准输入读一行文本,写到服务器上;服务器从网络输入读取此行,并回射(echo)给客户;客户读此回射行,并将其写到标准输出。
扩展以下三个内容
* 在客户机上显示服务器的目录
* 将服务器指定的文件下载到客户机
* 讲客户机指定的文件上传到服务器

二,套接字编程基础

2.1 套接字地址结构

进行套接字编程需要要指定套接字的地址作为参数,不同的协议族有不同的地址结构定义方式。这些地址结构通常以sockaddr_开头,每一个协议族有一个唯一的后缀,例如对于以太网,其结构名称为sockaddr_in

  1. 通用套接字数据结构
    通用的套接字地址类型的定义如下,它可以在不同协议族之间进行强制转换。
struct sockaddr{                //套接字地址结构    sa_family_t sa_family;      //协议族    char        sa_data[14];    //协议族数据}

上述结构中协议族成员变量sa_family的类型为sa_family_t,其实这个类型是unsigned short类型,因此成员变量sa_family的长度为16个字节。

typedef unsigned short sa_family_t;
  1. 实际使用的套接字数据结构
    在网络程序设计中所使用的函数中几乎所有的套接字函数都用这个结构作为参数,如bind()函数的原型为:
int bind(int sockfd,                //套接字文件描述符const struct sockaddr *my_addr,     //套接字地址结构    socklen_t addrlen);             //套接字地址结构的长度

但是使用struct sockaddr不方便设置,在以太网中,一般军用结构struct sockaddr_in进行设置,这个结构的定义如下:

struct sockaddr_in{                 //以太网套接字结地址结构    u8      sin_len;                //结构`struct sockaddr_in`的长度,16    u8      sin_family;             //通常为AF_INET    u16     sin_port;               //16位的端口号,网络字节序    sturct in_addr  sin_addr;       //32位IP地址    char            sin_zero[8];    //未用};

结构struct sockaddr_in的成员变量in_addr用于标示IP地址,这个结构的定义如下

struct in_addr{                     //IP地址结构    u32     s_addr;                 //32位IP地址,网络字节序}

三,TCP网络编程流程

TCP网络编程框架

TCP网络编程有服务器模式和客户端模式两种。服务器模式创建一个服务程序,等待客户端用户的链接,接收到用户的请求后,根据用户的请求进行处理;用户端模式则根据服务器的地址和端口进行解析,向服务器发送请求并对服务器的响应进行数据处理。

  1. 服务端的程序设计模式
    图为TCP链接的服务器模式的程序设计流程。流程主要为套接字初始化(socket()),套接字与端口的绑定(bind()),接收和发送数据(read()write())进行数据处理和处理完毕的套接字关闭(close())。

    • 套接字初始化过程中,根据用户对套接字的需求来确定套接字的选项,这个过程中的函数为socket(),他按照用户定义的网络类型、协议类型和具体的协议标号等参数来定义。系统根据用户的需求生成一个套接字文件描述符供用户使用。
    • 套接字与端口绑定过程中,将套接字与一个地址结果进行绑定。绑定之后,在进行网络程序设计的时候,套接字所表示的IP地址和端口地址以及协议类型等参数按照帮定制进行操作。
    • 由于一个服务器需要满足多个客户端的链接请求,而服务器在某个时间仅能处理有限个数的客户端的请求,所以服务器需要设置服务端排队队列的长度。服务器侦听链接会设置这个参数,限制客户端中等待服务器处理请求连接的队列长度。
    • 在客户端发送连接请求后,服务器需要接受客户端的链接,然后才能进行其他的处理。
    • 在服务器接受客户端请求之后,可以从套接字文件描述符中读取数据或者向文件描述符发送数据。接收数据后服务器按照定义的规则对数据进行处理,并将结果发送给客户端。
    • 当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接。
  2. 客户端的程序设计模式
    图中客户端设计模式,主要分为套接字初始化(socket()),连接服务器((connnect()),读写网络数据(read()write())并进行数据处理和最后的套接字关闭(clone())过程。
    客户端程序设计模式流程与服务器端的处理模式流程类似,两者之间的不同之处是客户端在套接字初始化之后可以不进行地址绑定,而是直接连接服务器端。
    客户端链接服务器的过程中,客户端根据用户设置的服务器地址、端口等参数特定的服务器程序进行通信。

  3. 客户端与服务武器的交互过程
    客户端与服务器在链接、读写数据、关闭连接过程中有交互的过程

    • 客户端的连接过程,对服务器是接受过程,在这个过程中客户端与服务器进行三次握手,建立TCP链接。建立TCP连接之后,客户端与服务器之间可以进行数据的交互。
    • 客户端与服务器之间的数据交互是相对的过程,客户端的读数据过程对应了服务器端的写数据过程,客户端的写数据过程对应服务器的读数据的过程。
    • 在服务器和客户端的数据交互完毕后,关闭套接字链接。

四,TCP协议程序设计的常用函数

4.1 创建网络插口函数socket()

网络程序设计中的套接字系统调用socket()函数用来获得文件描述符
socket()函数的原型如下,这个函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。如果函数调用成功,会返回一个表示这个套接字的文件描述符,失败的时候返回-1。

#include<sys/types.h>#include<sys/socket.h>int socket(int domain, int type, int protocol);

4.2 绑定一个地址端口对bind()

在建立套接字文件描述符成功后,需要对套接字进行地址和端口的绑定,才能进行数据的接收和发送操作
bind()函数将长度为addlenstruct sockadd类型的参数my_addrsockfd绑定在一起。将sockfd绑定到某个端口上,如果使用connect()函数则没有绑定的必要。绑定的函数原型如下:

#include<sys/types.h>#include<sys.socket.h>int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

4.3 监听本地端口listen()

服务器模式下同时有listen()accept()两个函数,而客户端则不需要。函数listen()用来初始化服务器可连接队列,服务器处理客户端连接请求的时候是顺序的,同一时间仅能处理一个客户端连接。当多个客户端的链接请求同时到来的时候,服务器并不是同时处理,而是将不能处理的客户端链接请求防盗等待队列中,这个队列的长度由listen()函数来定义。
listen()函数的原型如下,其中backlog标示等待队列的长度。

#include<sys/socket.h>int listen(int sockfd, int backlog);

4.4 接受一个网络请求accept()

当一个客户端的连接请求到达服务器主机侦听的端口时,此时客户端的链接会在队列中等待,直到使用服务器处理接收的请求。
函数accept()成功执行后,会返回一个新的套接口文件描述符来标示客户端的连接,客户端链接的信息可以通过这个新文件描述符来获得。因此当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符标示正在监听的socket,新产生的文件描述符表示客户端的连接,函数send()recv()通过新的文件描述符进行数据收发。
accept()函数的原型如下:

#include<sys/types.h>#inclue<sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

4.5 连接目标网络服务器connect()

客户端在建立套接字之后,不需要进行地质绑定,就可以直接连接服务器。连接服务器的函数为connect(),此函数连接指定参数的服务器,例如IP地址、端口等。
connect()函数的原型如下,其中的参数sockfd是建立套接字时返回的套接字文件描述符,它是由系统调用socket()返回的。参数serv_addr,是一个指向数据结构sockaddr的指针,其中包括客户端需要连接的服务器的目的端口和IP地址以及协议类型。参数addrlen表示第二个参数内容的大小,可以使用sizeof(struct sockaddr)而获得,与bind()不同,这个参数是一个整数型而不是一个指针。

#include<sys/types.h>#include<sys/scoket.h>int connect(int sockfd, struct sockaddr *, int addrlen);

4.6 写入函数write()

当服务器端在接收到一个客户端的连接后,可以通过套接字描述符进行数据的写入操作。对套接字进行写入的形式和过程与普通文件的操作方式一致,内核会根据文件描述符的值来查找对应的属性,当为套接字的时候,会调用相应的内核函数。
下面是一个向套接字文件描述符中写入数据的例子,将缓冲区data的数据全部写入套接字文件描述符s中,返回值为成功写入的数据长度。

int size;char data[MAXLINE];size = write(s, data, MAXLINE);

4.7 读取数据函数read()

与写入数据类似,使用read()函数可以从套接字描述符中读取数据。当然在读取数据之前,必须建立套接字并连接。读取数据的方式如下所示,从套接字描述符s中读取MAXLINE个字节,放入缓冲区data中,size变量的值为成功读取的数据大小。

int size;char data[MAXLINE];size = read(s, data, MAXLINE);

4.8 关闭套接字函数close()

关闭socket()连接可以使用close()函数实现,函数的作用是关闭已经打开的socket连接,内核会释放相关的资源,关闭套接字之后就不能在使用这个套接字文件描述符进行读写操作了。
函数shutdown()可以使用更多方法来关闭连接,允许单方向切断通信或者切断双方的通信。函数原型如下,第一个参数s是切断通信的套接口文件描述符,第二个参数how表明切断的方式:

#include<sys/socket.h>int shutdown(int s, int how);

五,实验代码实现

/* * tcpserv01.cpp * *  Created on: 2015年5月02日 *      Author: gzxultra *  Finished on: 2015年5月03日 */#include        <netinet/in.h>#include        <errno.h>#include        <stdio.h>#include        <stdlib.h>#include        <sys/socket.h>#include        <arpa/inet.h>#include        <string.h>#define MAXLINE                  4096#define LISTENQ                  1024    /* 2nd argument to listen() */#define SERV_PORT                9877#define SA      struct sockaddrvoid str_echo(int sockfd,char* mesg);  void GetList(int sockfd);void DownLoad(int sockfd, char *mesg);void UpLoad(int sockfd, char *mesg);int main(int argc, char **argv){    int  listenfd, connfd;  //socket描述符    pid_t  childpid;        //分叉的进行ID    socklen_t  clilen;    struct sockaddr_in  cliaddr,servaddr;       //客户机和服务器的地址结构    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {        printf("socket error.\n");        exit(1);    }    bzero(&servaddr, sizeof(servaddr)); //清零    servaddr.sin_family = AF_INET;    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    servaddr.sin_port = htons(SERV_PORT);    if (bind(listenfd, (SA *)&servaddr, sizeof(servaddr)) < 0) {        printf("bind error.\n");        exit(1);    }           if (listen(listenfd,LISTENQ) < 0) {     //设置侦听队列        printf("listen error.\n");        exit(1);    }    //char sendline[MAXLINE];    //char recvline[MAXLINE];    char mesg[MAXLINE];    int i;    int op;    ssize_t size = 0;    char instruction[3][10] = { "getlist\n", "download\n", "upload\n" };    for( ; ;) {        if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {            printf("accept error.\n");            exit(1);        }        if((childpid = fork()) == 0) {            close(listenfd) ;  //在子进程中关闭服务器的侦听            //str_echo(connfd);  //处理连接            for(;;){            size = 0;            op = 0;            memset(mesg,0,strlen(mesg));            size = read(connfd,mesg,MAXLINE);            if(size == 0)                return 0;            //write(connfd,mesg,strlen(mesg)+1);            for (i = 1; i <= 3; i++)                if (strcmp(mesg, instruction[i - 1]) == 0) {                    op = i;                    break;                }   //choose operation            printf("op = %d\n", op);            fputs(mesg,stdout);            switch (op) {                case 1:{                    fputs("in getlist()\n",stdout);                    GetList(connfd);                    break;                }                case 2: {                    fputs("in download()\n",stdout);                    DownLoad(connfd, mesg);                    break;                }                case 3: {                    fputs("in upload()\n",stdout);                    UpLoad(connfd, mesg);                    break;                }                default: {                    fputs("in echo()\n",stdout);                    str_echo(connfd, mesg);                    break;                }            }//switch(op)        }            exit(0);        }        close(connfd);         //在父进程中关闭客户端的链接    }    return 0;}       voidstr_echo(int sockfd,char* mesg){    write(sockfd, mesg, strlen(mesg)+1);}void GetList(int sockfd) {    int n;    socklen_t len;    printf("catch\n");    char strlin[100][MAXLINE];  //一百行缓冲区    char sendline[MAXLINE] = { };   //最终发送的字符串    int i = 0;    FILE *fp;    system("ls -l>filelog.txt");    //输出重定向    ssize_t read;    fp = fopen("filelog.txt", "r");    if (!fp) {        printf("get ls order failed!");    } else {        while (1) {            //printf("2done!!!!\n");            if (NULL == fgets(strlin[i], MAXLINE, fp))                break;  //如果读到文件尾,结束读取            //strlin[i][strlen(strlin[i] - 1)] = '\0';  //每行加\0结尾            i++;    //写成strlin[i++]提示i未定义,gcc的bug??        }        for (int n = 0; n < i; n++) {            strcat(sendline, strlin[n]);            //strcat(sendline,"\n");        }        fputs(sendline, stdout);        write(sockfd, sendline, strlen(sendline)+1);    }    //strcat(sendline, '\0');    fclose(fp);    printf("FileList Get!\n");}void DownLoad(int sockfd,char *mesg) {    char text[MAXLINE] = "Please input the file name you'd like to download\n";    char sendline[100 * MAXLINE];    char instr[] = "download\n";    int n = 0;    //fputs(mesg,stdout);    while (strcmp(mesg, instr) == 0) {        printf("start download\n");        write(sockfd, text, strlen(text) + 1);        n = read(sockfd, mesg, MAXLINE);    }    FILE *Downloadfp;    fputs(mesg, stdout);    //fputs("\n",stdout);    for (int j = 0; j <= n; j++) {        if (mesg[j] == '\n')            mesg[j] = '\0';        //break;    }    Downloadfp = fopen(mesg, "r");    int i = 0;    char strlin[1000][MAXLINE];    //printf("don't\n");    if (!Downloadfp) {        printf("get file failed!\n");    } else {        while (1) {            //printf("2done!!!!\n");            if (NULL == fgets(strlin[i], MAXLINE, Downloadfp))                break;  //如果读到文件尾,结束读取            //strlin[i][strlen(strlin[i] - 1)] = '\0';  //每行加\0结尾            i++;    //写成strlin[i++]提示i未定义,gcc的bug??        }        memset(sendline, 0, strlen(sendline));        for (int n = 0; n < i; n++) {            strcat(sendline, strlin[n]);            //strcat(sendline,"\n");        }        fputs(sendline, stdout);        write(sockfd, sendline, strlen(sendline) + 1);    }    //strcat(sendline, '\0');    fclose(Downloadfp);    printf("file download!\n");}void UpLoad(int sockfd,char *mesg) {    char text[MAXLINE] = "Please input the file name you'd like to upload\n";    char recvline[100 * MAXLINE];    char instr[] = "upload\n";    int n = 0;    //fputs(mesg,stdout);    FILE *CopyToServer;    while (strcmp(mesg, instr) == 0) {        printf("start upload\n");        write(sockfd, text, strlen(text) + 1);        n = read(sockfd, mesg, MAXLINE);    }   //上面应该没问题了~    char path[MAXLINE] = "/Users/gzxultra/";    strcat(path, mesg);    CopyToServer = fopen(path, "w");    if (!CopyToServer)        printf("Upload file failed!\n");    else {        read(sockfd, recvline, 100 * MAXLINE);        fputs(recvline, CopyToServer);    }    //fputs(recvline,stdout);    //fputs(recvline,stdout);    //printf("download !");    fclose(CopyToServer);    printf("file Uploadload!\n");}
/* * tcpcli01.cpp * *  Created on: 2015年5月02日 *      Author: gzxultra *  Finished on: 2015年5月03日 */#include        <netinet/in.h>#include        <errno.h>#include        <stdio.h>#include        <stdlib.h>#include        <sys/socket.h>#include        <arpa/inet.h>#include        <string.h>#define MAXLINE                  4096#define LISTENQ                  1024    /* 2nd argument to listen() */#define SERV_PORT                9877#define SA      struct sockaddrintmain(int argc, char **argv){    int sockfd;    struct sockaddr_in  servaddr;    if (argc != 2) {    printf("usage:tcpcli01 <IPaddress>");        exit(1);    }    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {        printf("socket error.\n");        exit(1);    }    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_port = htons(SERV_PORT);    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);    if (connect(sockfd, (SA *)&servaddr, sizeof(servaddr)) < 0) {        printf("connect error.\n");        exit(1);    }    int n;    char sendline[MAXLINE], recvline[MAXLINE + 1];    char instruction[3][10] = {        "getlist\n",        "download\n",        "upload\n"    };    int i=0;    int op =0;    FILE *CopyToLocal;    FILE *Uploadfp;    while (fgets(sendline, MAXLINE,stdin) != NULL) {        write(sockfd, sendline, MAXLINE);        n = read(sockfd, recvline, MAXLINE);        recvline[n] = 0; /* null terminate */        //fputs(recvline,stdout);        op = 0;        for(i = 1;i<=3;i++)        if(strcmp(sendline,instruction[i-1])== 0) {            op = i;            break;        }   //choose operation        printf("op=%d\n",op);        switch(op) {            case 1: {                fputs(recvline, stdout);                break;            }            case 2: {                fputs(recvline, stdout);                fgets(sendline, MAXLINE,stdin);        //给出上传文件的文件名                write(sockfd, sendline, strlen(sendline));                memset(recvline,0,sizeof(recvline));                n = read(sockfd, recvline, MAXLINE);                recvline[n] = 0; /* null terminate */                char path[MAXLINE] = "/Users/gzxultra/downloads/";                strcat(path,sendline);                fputs(path,stdout);                printf("\n");                CopyToLocal = fopen(path,"w");                if(!CopyToLocal)                printf("download file failed!");                //fputs(recvline,stdout);                fputs(recvline, CopyToLocal);                //fputs(recvline,stdout);                //printf("download !");                fclose(CopyToLocal);                break;            }            case 3: {                fputs(recvline, stdout);        //应该是提示输入文件名                fgets(sendline, MAXLINE,stdin);//应该是输入要上传的文件名字吧?                write(sockfd, sendline, strlen(sendline));                memset(recvline,0,sizeof(recvline));                //recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL); //接收一個okay                //recvline[n] = 0;// null terminate                 //fputs(sendline,stdout);                //fputs("\n",stdout);                for(int j=0;j<=n;j++) {                    if(sendline[j] == '\n') sendline[j] = '\0';                    //break;                }                //fputs(sendline,stdout);                //printf("\n");                Uploadfp = fopen(sendline, "r");                int i =0;                char strlin[1000][MAXLINE];                //printf("don't\n");                if (!Uploadfp) {                    printf("get file failed!\n");                } else {                    //printf("4done!\n");                    //printf("%p\n", fp);                    while (1) {                        //printf("2done!!!!\n");                        if (NULL==fgets(strlin[i], MAXLINE, Uploadfp))                        break;//如果读到文件尾,结束读取                        //strlin[i][strlen(strlin[i] - 1)] = '\0';  //每行加\0结尾                        i++;//写成strlin[i++]提示i未定义,gcc的bug??                    }                    memset(sendline,0,sizeof(sendline));                    for (int n = 0; n < i; n++) {                        strcat(sendline, strlin[n]);                        //strcat(sendline,"\n");                    }                    //sendto(sockfd, strlin, sizeof(strlin), 0, pcliaddr, len);                    //fputs(sendline,stdout);                    write(sockfd, sendline, strlen(sendline)+1);                    //recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);                    //fputs(recvline,stdout);//path                    //fputs("\n",stdout);                }                //strcat(sendline, '\0');                fclose(Uploadfp);            }            default: {                fputs(recvline, stdout);                break;            }            //to spilit up the output by ***        }        for(int i=0;i<25;i++) {            printf("*");        }        printf("\n");    }    exit(0);}   

六,实验结果

Alt text

0 0
原创粉丝点击