C语言编程实现TCPUDPTFTP网络通信

C语言编程实现TCPUDPTFTP网络通信,第1张

一、TCP网络编程 1.TCP协议与UDP协议

相同点:都是传输层的协议

不同点:

TCP协议:是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)。

        适用情况:适合于对传输质量要求较高,以及传输大量数据的通信。在需要可靠数据传输的场合,通常使用TCP协议。
        如:MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议。

UDP协议:用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。

        适用情况:发送小尺寸数据(如对DNS服务器进行IP地址查询时)在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络)适合于广播/组播式通信中,通常使用TCP协议。
        如:MSN/QQ等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议;流媒体、VOD、VoIP等网络多媒体服务中通常采用UDP方式进行实时数据传输。

2.TCP网络编程流程

服务器端:

        1.创建套接字 socket()

        2.填充服务器的网络信息结构体

        3.绑定服务器的套接字和网络信息结构体 bind()

        4.将服务器的套接字设置成被动监听状态 listen()

        5.阻塞等待客户端的连接 accept()

        6.收发数据--read/write

        7.关闭套接字 close()

客户端:

        1.创建套接字 socket()

        2.填充服务器的网络信息结构体

        3.与服务器建立连接 connect()

        4.收发数据--read/write

        5.关闭套接字 close()

示意图:

3.函数说明

int socket(int domain, int type, int protocol);
功能:创建通信的端点,返回文件描述符(当前进程未打开的编号最小的)

#include
#include
参数:
        @domain:指定通信域
                AF_UNIX, AF_LOCAL:本地通信使用
                AF_INET:IPV4使用
                AF_INET6:IPV6使用
                AF_PACKET:原始套接字使用
        @type:指定套接字类型
                SOCK_STREAM:TCP使用
                SOCK_DGRAM:UDP使用
                SOCK_RAW:原始套接字使用
        @protocol:附加协议
               如果使用的是TCP或者UDP 此处传0 即可
返回值:成功返回文件描述符,失败返回-1,置位错误码


int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:将套接字和网络信息结构体进行绑定

#include
#include
参数:
        @sockfd: socket函数返回的文件描述符
        @addr: 要绑定的网络信息结构体
        @addrlen: 网络信息结构体的长度
返回值:成功返回0,失败返回-1,置位错误码


int listen(int sockfd, int backlog);
功能:将套接字设置成被动监听状态

#include
#include
参数:
        @sockfd: socket函数返回的文件描述符
        @backlog: 最大监听队列的长度,表示同时支持的能连接的客户端的个数
                一般不关心,设置成 5 或者 10 都可以,不能是 0
返回值:成功返回0,失败返回-1,置位错误码


int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
功能:阻塞等待客户端的连接,有客户端连接后, 会返回一个新的文件描述符,专门用于和该客户端通信

#include
参数:
        @sockfd: socket函数返回的文件描述符
        @address: 如果想保存客户端的信息,就传一个地址 sockaddr_in
                如果不关心,可以传 NULL
        @backlog: address长度
               如果不关心,可以传 NULL
返回值:成功返回用于和客户端通信的文件描述符,失败返回-1,置位错误码


int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:与服务器建立连接

#include
#include
参数:
        @sockfd: socket函数返回的文件描述符
        @address: 服务器的网络信息结构体,用于标记连接到哪个服务器程序
        @address_len:address长度
返回值:成功返回0,失败返回-1,置位错误码

示例:

//服务器端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(){
    //1.创建套接字   ---相当于 买了一个手机
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd){
        perror("socket error");
        exit(-1);
    }

    //创建服务器网络信息结构体  ---相当于办了一张卡
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));//清空
    //2.填充服务器网络信息结构体
    server_addr.sin_family = AF_INET;
    //网络字节序的端口号,可以是 8888  9999 6789 等都可以
    server_addr.sin_port = htons(8888);
    //IP地址
    //不能随便填,可以填自己主机的IP地址
    //如果只是在本地测试,也可以填 127.0.0.1
    server_addr.sin_addr.s_addr = inet_addr("192.168.70.95");
    socklen_t addrlen = sizeof(server_addr);

    //3.将套接字和网络信息结构体进行绑定---相当于把卡插入手机里
    if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
        perror("bind error");
        exit(-1);
    }

    //4.将服务器的套接字设置成被动监听状态
    if(-1 == listen(sockfd, 5)){
        perror("listen error");
        exit(-1);
    }

    //定义一个结构体,保存客户端的信息
    struct sockaddr_in client_addr;
    memset(&client_addr, 0, sizeof(client_addr));//清空
    socklen_t clientaddrlen = sizeof(client_addr);
    //5.阻塞等待客户端连接
    int acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen);
    if(-1 == acceptfd){
        perror("accept error");
        exit(-1);
    }

    printf("客户端 %s:%d 连接到服务器了\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    //6.与客户端通信
    char buff[128] = {0};
    read(acceptfd, buff, 128);

    printf("%s-%d:[%s]\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff);

    strcat(buff, "--server");
    write(acceptfd, buff, 128);

    //7.关闭套接字
    close(acceptfd);
    close(sockfd);

    return 0;
}
//客户端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(){
    //1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd){
        perror("socket error");
        exit(-1);
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));//清空
    //2.填充服务器网络信息结构体 --需要指定连接哪个服务器
    server_addr.sin_family = AF_INET;
    //网络字节序的端口号,可以是 8888  9999 6789 等都可以
    server_addr.sin_port = htons(8888);
    //IP地址
    //不能随便填,可以填自己主机的IP地址
    //如果只是在本地测试,也可以填 127.0.0.1
    server_addr.sin_addr.s_addr = inet_addr("192.168.70.95");
    socklen_t addrlen = sizeof(server_addr);

    //3.与服务器建立连接
    if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){
        perror("connect error");
        exit(-1);
    }

    //4.与服务器通信
    char buff[128] = {0};
    fgets(buff, 128, stdin);
    buff[strlen(buff)-1] = '\0';//清除 \n
    write(sockfd, buff, 128);
    read(sockfd, buff, 128);
    printf("收到回复:[%s]\n", buff);

    //5.关闭套接字
    close(sockfd);

    return 0;
}

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:在套接字中接收数据

#include
#include
参数:
        @sockfd: 在哪个套接字中接
        @buf: 存放接收的数据的缓冲区
        @len: 想要接多少个字节
        @flags: 如果设置成 MSG_DONTWAIT 表示非阻塞
              如果是0 和read 的用法就一样了
返回值:成功返回实际接收的字节数,失败返回-1,置位错误码
              如果发送方关闭,recv会返回0

注:下面三种用法是等价的
        read(sockfd, buff, 128);
        recv(sockfd, buff, 128, 0);
        recvfrom(sockfd, buf, 128, 0, NULL, NULL);


ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:在套接字中接收数据

#include
#include
参数:
        @sockfd: 向哪个套接字发
        @buf: 要发送的数据的首地址
        @len: 想要发送多少个字节
        @flags: 如果设置成 MSG_DONTWAIT 表示非阻塞
             如果是0 和write 的用法就一样了
返回值:成功返回实际发送的字节数,失败返回-1,置位错误码
              如果接收方关闭,send会返回0
              如果第二次再send,会产生 SIGPIPE 信号

注:下面三种用法是等价的
        write(sockfd, buff, 128);
        send(sockfd, buff, 128, 0);
        sendto(sockfd, buf, 128, 0, NULL, NULL);

示例:实现简单的文件下载功能

要求:客户端和服务器 要在不同的路径下运行服务器运行后,客户端连接到服务器,给服务器发送要下载的文件名
           服务器判断文件是否存在(当前路径下),不管存在不存在都要通知一下客户端
           如果存在,则打开文件,读取文件内容发给客户端,客户端创建并打开一个新文件,接收文件内容,并写入该文件。

//服务器端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define N 128

#define ERRLOG(errmsg) do{\
                perror(errmsg);\
                printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
                exit(-1);\
            }while(0)

typedef struct __MSG{
    char buff[N];
    int bytes;
}msg_t;

int main(int argc, const char *argv[]){
    if(3 != argc){
        printf("Usage : %s  \n", argv[0]);
        exit(-1);
    }
    //1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd){
        ERRLOG("socket error");
    }

    //创建服务器网络信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
  
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t addrlen = sizeof(server_addr);

    //3.将套接字和网络信息结构体进行绑定
    if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
        ERRLOG("bind error");
    }

    //4.将服务器的套接字设置成被动监听状态
    if(-1 == listen(sockfd, 5)){
        ERRLOG("listen error");
    }

    //定义一个结构体,保存客户端的信息
    struct sockaddr_in client_addr;
    memset(&server_addr, 0, sizeof(client_addr));//清空
    socklen_t clientaddrlen = sizeof(client_addr);

    char buff[N] = {0};
        int acceptfd = 0;
ACCEPT:
    //5.阻塞等待客户端连接
    acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen);
    if(-1 == acceptfd){
        ERRLOG("accept error");
    }
    printf("客户端 %s:%d 连接到服务器了\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

RENAME:
    //接收客户端要下载的文件名
    if(-1 == recv(acceptfd, buff, N, 0)){
        ERRLOG("recv error");
    }
    printf("客户端要下载的文件名为:[%s]\n", buff);

    //判断文件是否存在
    int fd = open(buff, O_RDONLY);
    if(-1 == fd){
        //文件不存在
        if(errno == ENOENT){
            printf("文件[%s]不存在\n",buff);
            //告诉客户端文件不存在
            if(-1 == send(acceptfd, "****NO EXIST****", N, 0)){
                perror("send error");
                goto RENAME;
            }
        }else{
            ERRLOG("open error");
        }
    }
    printf("文件[%s]存在\n",buff);
    //文件存在 也要通知客户端
    if(-1 == send(acceptfd, "****EXIST****", N, 0)){
        ERRLOG("send error");
    }
    int bytes = 0;
    #if 0
    //循环读取文件内容并发送给客户端
    while((bytes = read(fd, buff, N))>0){
        if(-1 == send(acceptfd, buff, bytes, 0)){
            ERRLOG("send error");
        }
        memset(buff, 0, N);
    }
    //TCP的粘包问题
    //TCP的底层有一个 Nagel算法,将一定短的时间内的小的数据包
    //组装给一个整体,一次性的发给接收端,接收端是无法判断数据类型的
    //所以发送的数据就会有冲突,就可能产生问题。
    //解决方式1:将文件内容和 结尾的标志 产生点时间差发送
    sleep(1);
    //发送传输完毕的信息
    if(-1 == send(acceptfd, "****OVER****", 13, 0)){
        ERRLOG("send error");
    }
    #endif
    
    //解决方式2:只要保证每次收发的数据包一样大就不会出现这个问题
    //可以定义一个结构体,每次收发数据都是结构体的大小
    msg_t msg;
    memset(&msg, 0, sizeof(msg));
    //循环读取文件内容并发送给客户端
    while((bytes = read(fd, msg.buff, N))>0){
        msg.bytes = bytes;
        if(-1 == send(acceptfd, &msg, sizeof(msg), 0)){
            ERRLOG("send error");
        }
        memset(&msg, 0, sizeof(msg));
    }
    //发送传输完毕的信息
    strcpy(msg.buff, "****OVER****");
    msg.bytes = 0;
    if(-1 == send(acceptfd, &msg, sizeof(msg), 0)){
        ERRLOG("send error");
    }

    close(acceptfd);
    
    goto ACCEPT;

    
    close(sockfd);

    return 0;
}
//客户端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define N 128

#define ERRLOG(errmsg) do{\
                perror(errmsg);\
                printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
                exit(-1);\
            }while(0)

typedef struct __MSG{
    char buff[N];
    int bytes;
}msg_t;

int main(int argc, const char *argv[]){
    if(3 != argc){
        printf("Usage : %s  \n", argv[0]);
        exit(-1);
    }
    //1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd){
        ERRLOG("socket error");
    }

    //创建服务器网络信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
  
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t addrlen = sizeof(server_addr);

    //与服务器建立连接
    if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){
        ERRLOG("connect error");
    }

    char filename[N] = {0};
    char buff[N] = {0};
    int fd = 0;
RENAME:

    printf("请输入要下载的文件:");
    scanf("%s", filename);

    //将要下载的文件发送给服务器
    if(-1 == send(sockfd, filename, N, 0)){
        ERRLOG("send error");
    }
    //接收文件是否存在的信息
    if(-1 == recv(sockfd, buff, N, 0)){
        ERRLOG("recv error");
    }
    
    if(0 == strcmp(buff, "****NO EXIST****")){
        printf("文件不存在\n");
        goto RENAME;
    }else if(0 == strcmp(buff, "****EXIST****")){
        //创建并打开并清空一个文件,准备下载文件内容
        if(-1 == (fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0664))){
            ERRLOG("open error");
        }
    }

    int bytes = 0;
    #if 0
    //循环接收文件内容
    while((bytes = recv(sockfd, buff, N, 0))>0){
        if(0 == strcmp(buff, "****OVER****")){
            break;
        }
        if(-1 == write(fd, buff, bytes)){
            ERRLOG("write error");
        }
        memset(buff, 0, N);
    }
    #endif
    msg_t msg;
    memset(&msg, 0, sizeof(msg));
    while(recv(sockfd, &msg, sizeof(msg), 0)>0){
        if(msg.bytes == 0){
            break;
        }
        if(-1 == write(fd, msg.buff, msg.bytes)){
            ERRLOG("write error");
        }
        memset(&msg, 0, sizeof(msg));
    }
    
    close(fd);
    printf("文件下载完成\n");
    //关闭套接字
    close(sockfd);

    return 0;
}
二、UDP网络编程 1.UDP网络编程流程

服务器流程:

        1.创建用户数据报式套接字

        2.填充服务器网络信息结构体

        3.绑定套接字与网络信息结构体

        4.收发数据 recvfrom/ sendto

        5.关闭套接字

客户端流程:

        1.创建用户数据报式套接字

        2.填充服务器网络信息结构体

        3.收发数据 recvfrom/ sendto

        4.关闭套接字

示意图:

2.函数说明

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
功能:在套接字中接收数据

#include
#include
参数:
        前四个参数和 recv 的用法一样
        后两个参数和 accept 的后两个参数用法一样,是用来保存客户端的信息的
返回值:成功返回实际接收到的数据的大小,失败返回-1,置位错误码
                如果发送方关闭,recvfrom会返回0


ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 
功能:向套接字发送数据

#include
#include
参数:
        前四个参数和 send 的用法一样
        后两个参数和 connect 的后两个参数用法一样,是指定发送给谁的
返回值:成功返回实际发送的字节数,失败返回-1,置位错误码
              如果接收方关闭,sendto会返回0
              如果第二次再sendto,会产生 SIGPIPE 信号

示例:

//服务器端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define N 128

#define ERRLOG(errmsg) do{\
                perror(errmsg);\
                printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
                exit(-1);\
            }while(0)

int main(int argc, const char *argv[]){
    if(3 != argc){
        printf("Usage : %s  \n", argv[0]);
        exit(-1);
    }
    //1.创建用户数据报式套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(-1 == sockfd){
        ERRLOG("socket error");
    }

    //创建服务器网络信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
  
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t addrlen = sizeof(server_addr);

    //3.将套接字和网络信息结构体进行绑定
    if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
        ERRLOG("bind error");
    }


    //定义一个结构体,保存客户端的信息
    struct sockaddr_in client_addr;
    memset(&server_addr, 0, sizeof(client_addr));//清空
    socklen_t clientaddrlen = sizeof(client_addr);

    char buff[N] = {0};

    while(1){
        //接收数据,如果想要给对方回应,就必须保存对方的网络信息结构体
        //如果不回应,后两个参数写 NULL 也行
        if(-1 == recvfrom(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, &clientaddrlen)){
            ERRLOG("recvfrom error");
        }
        printf("%s(%d):%s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff);
        //组装应答信息
        strcat(buff, "--hqyj");
        if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, clientaddrlen)){
            ERRLOG("sendto error");
        }
        memset(buff, 0, N);
    }
    
    //关闭套接字
    close(sockfd);

    return 0;
}
//客户端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define N 128

#define ERRLOG(errmsg) do{\
                perror(errmsg);\
                printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
                exit(-1);\
            }while(0)

int main(int argc, const char *argv[]){
    if(3 != argc){
        printf("Usage : %s  \n", argv[0]);
        exit(-1);
    }
    //1.创建用户数据报套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(-1 == sockfd){
        ERRLOG("socket error");
    }

    //填充服务器网络信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t addrlen = sizeof(server_addr);

    char buff[N] = {0};
    while(1){
        printf("input your msg:");
        fgets(buff, N, stdin);
        buff[strlen(buff)-1] = '\0';//清除 \n
        if(0 == strcmp(buff, "quit")){
            break;
        }
        if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&server_addr, addrlen)){
            ERRLOG("sendto error");
        }

        //因为客户端已经知道服务器的网络信息了 所以后两个参数可以传NULL
        if(-1 == recvfrom(sockfd, buff, N, 0, NULL, NULL)){
            ERRLOG("recvfrom error");
        }
        printf("recv:[%s]\n", buff);
        memset(buff, 0, N);
    }
    //关闭套接字
    close(sockfd);

    return 0;
}
3.TCP和UDP使用总结及注意事项

TCP网络编程流程:

1.客户端是不需要绑定自己的网络信息结构体的,因为运行的时候, *** 作系统会自动分配,如果想要指定IP地址和端口号,也可以调用bind函数。

2.服务器端的accept函数的后两个参数,是用来保存连接过来的客户端的网络信息结构体的。对于TCP而言,如果不保存客户端的网络信息结构体,也是可以和对方通信的,因为accept函数会返回一个新的文件描述符,专门用于和这个客户端通信,服务器的套接字和客户端是一对一的关系。

3.TCP可以使用read/write 、recv/send 、 recvfrom/sendto 收发数据。

4.服务器的accept本质是一个阻塞的读,客户端的connect是一个写。相当于客户端将自己的网络信息结构体发给了服务器。

5.TCP服务器是一个循环服务器,同一时间只能处理同一个客户端的请求。原因是有两个阻塞函数会相互影响 accept 和 recv。

UDP网络编程流程:

1.UDP是无连接的,所以UPD的客户端无需绑定自己的网络信息结构体,如果想绑定,也可以使用bind函数绑定。UDP是使用 recvfrom和sendto进行数据收发的,sendto的后两个参数,表示数据要发给谁,recvfrom的后两个参数是用于接收,发送端的网络信息结构体的,如果想要给对方回消息,就必须要保存,如果特殊场景下服务器只负责收数据,recvfrom的后两个参数置NULL 也行。

        sendto相当于 connect 和 send 的二合一,recvfrom相当于 accept 和 recv 的二合一。

2.UDP客户端也可以使用connect函数向去将自己的网络信息结构体发给服务器,但是UDP使用connect也不会产生三次握手的过程。使用connect之后,UDP的客户端也可以使用send和recv函数和服务器通信了。UDP服务器端不可以这么做。

3.UDP服务器本身就是一个并发服务器。原因是UDP服务器只有一个阻塞的函数 recvfrom

        注:并发服务器在下一篇博客(C语言中IO模型实现并发服务器)中详细讲。

三、TFTP协议简介

TFTP:简单文件传送协议,最初用于引导无盘系统,被设计用来传输小文件

特点:

        基于UDP协议实现

        不进行用户有效性认证

数据传输模式:

        octet:二进制模式

        netascii:文本模式

        mail:已经不再支持

1.TFTP通信过程

1、服务器在69号端口等待客户端的请求

2、服务器若批准此请求,则使用临时端口与客户端进行通信

3、每个数据包的编号都有变化(从1开始)

4、每个数据包都要得到ACK的确认如果出现超时,则需要重新发送最后的包(数据或ACK)

5、数据的长度以512Byte传输

6、小于512Byte的数据意味着传输结束

2.TFTP 协议分析

3.错误码

0 未定义,参见错误信息

1 File not found.

2 Access violation.

3 Disk full or allocation exceeded.

4 illegal TFTP operation.

5 Unknown transfer ID.

6 File already exists.

7 No such user.

8 Unsupported option(s) requested.

4.代码实现

编写TFTP客户端程序 实现 从TFTP 服务器下载文件

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define N 128

#define ERRLOG(errmsg) do{\
                perror(errmsg);\
                printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
                exit(-1);\
            }while(0)

int main(int argc, const char *argv[]){
    if(3 != argc){
        printf("Usage : %s  \n", argv[0]);
        exit(-1);
    }

    char filename[32] = {0};
    char buff[600] = {0};
    int bytes = 0;
            
    unsigned short num = 0;// *** 作码
    unsigned short block_code = 0;//块编号或者差错码
    unsigned short block_count = 0;//已经收到的块的编号
    char text[600] = {0};//文件内容或者差错信息
    int fd = 0;

    //1.创建用户数据报套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(-1 == sockfd){
        ERRLOG("socket error");
    }

    //填充服务器网络信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
LOOP: //LOOP要放到 赋值端口号前面  因为69已经被覆盖了
    server_addr.sin_port = htons(atoi(argv[2]));
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t addrlen = sizeof(server_addr);

    printf("input filename:");
    scanf("%s", filename);


    
    //赋值 *** 作码的方式1
    //*(unsigned short *)buff = htons(1);
    
    //赋值 *** 作码的方式2:一个字节一个字节赋值
    //buff[0] = 0;
    //buff[1] = 1;

    //方式3  使用 sprintf 封包
    bytes = sprintf(buff,"%c%c%s%c%s%c",0,1,filename,0,"octet",0);

    //发送请求
    if(-1 == sendto(sockfd, buff, bytes, 0, (struct sockaddr *)&server_addr, addrlen)){
        ERRLOG("sendto error");
    }
    
    while(1){
        //此处需要重新保存服务器的网络信息结构体
        //因为服务器使用临时端口 而不是 69端口给我们发数据的
        if(-1 == (bytes = recvfrom(sockfd, buff, 600, 0, (struct sockaddr *)&server_addr, &addrlen))){
            ERRLOG("recvfrom error");
        }
        //解析 *** 作码
        num = ntohs(*(unsigned short *)buff);
        //解析块编号或者差错码
        block_code = ntohs(*(unsigned short *)(buff+2));
        //文件内容或差错信息
        strncpy(text, buff+4, bytes-4);
        //printf("num = %d   code = %d\n", num, block_code);
        //printf("%s\n", buff+4);
        if(5 == num){
            printf("num = %d , text = [%s]\n", num, text);
            goto LOOP;
        }else if(3 == num && block_code == block_count+1){//对包的编号做校验
            block_count = block_code;
            //第一个数据包过来是创建文件
            if(1 == block_code){
                if(-1 == (fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0664))){
                    ERRLOG("open error");
                }
            }
            //将内容写入文件
            if(-1 == write(fd, text, bytes-4)){
                ERRLOG("write error");
            }
            //组装ACK
            memset(buff, 0, sizeof(buff));
            *(unsigned short *)buff = htons(4);
            *(unsigned short *)(buff+2) = htons(block_code);
            //发送ACK
            if(-1 == sendto(sockfd, buff, 4, 0, (struct sockaddr *)&server_addr, addrlen)){
                ERRLOG("sendto error");
            }
        }
        //文件传输完毕
        if(bytes-4 < 512){
            break;
        }
    }

    //关闭套接字
    close(sockfd);

    return 0;
}
四、原始套接字(拓展) 1.概念

1.一种不同于SOCK_STREAM、SOCK_DGRAM的套接字,它实现于系统核心

2.可以接收本机网卡上所有的数据帧(数据包),对于监听网络流量和分析网络数据很有作用

3.开发人员可发送自己组装的数据包到网络上

4.广泛应用于高级网络编程

5.网络专家、黑客通常会用此来编写奇特的网络程序

2.创建原始套接字

int socket(int domain, int type, int protocol);
功能:创建套接字

#include
#include
#include
#include
参数:
        @domain:通信域,协议族
                AF_UNIX, AF_LOCAL:本地通信使用
                AF_INET:IPV4使用
                AF_INET6:IPV6使用
                AF_PACKET:原始套接字使用
        @type:套接字类型
                SOCK_STREAM:TCP使用
                SOCK_DGRAM:UDP使用
                SOCK_RAW:原始套接字使用
        @protocol:附加协议
                ETH_P_IP IPV4数据包
                ETH_P_ARP ARP数据包
                ETH_P_ALL 任何协议类型的数据包
返回值:成功返回文件描述符,失败返回-1,置位错误码

示例:

#include 
#include 
#include           
#include 
#include 
#include 
#include 

#define ERRLOG(errmsg) do{\
            perror(errmsg);\
            printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
            exit(1);\
        }while(0)

int main(int argc, char const *argv[])
{
    int sockfd;
    //创建一个原始套接字
    //注意  三个参数 都要设置
    if((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1)
    {
        ERRLOG("socket error");
    }

    printf("sockfd = %d\n", sockfd);
    
    return 0;
}
3.从网卡上直接读取数据包并解析
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define N 128

#define ERRLOG(errmsg) do{\
            perror(errmsg);\
            printf("%s (%s):%d\n", __FILE__, __func__, __LINE__);\
            exit(-1);\
        }while(0)

int main(int argc, char *argv[]){
    int socket_fd = -1;
    if(-1 == (socket_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)))){
        ERRLOG("socket error");
    }
    //直接从网卡上读取数据并解析
    unsigned char buff[1600] = {0};
    unsigned char mac_dest[18] = {0};
    unsigned char mac_src[18] = {0};
    unsigned short mac_type = 0;
    unsigned short ip_len = 0;
    unsigned char ip_ttl = 0;
    unsigned char ip_type = 0;
    unsigned char ip_src[16] = {0};
    unsigned char ip_dest[16] = {0};
    unsigned short port_src = 0;
    unsigned short port_dest = 0;

    while(1){
        printf("---------------recving----------------\n");
        if(-1 == recvfrom(socket_fd, buff, 1600, 0, NULL, NULL)){
            ERRLOG("recvfrom error");
        }
        //----------------解析以太网头------------------
        //目的 mac
        sprintf(mac_dest, "%x:%x:%x:%x:%x:%x", buff[0], buff[1], buff[2], buff[3], buff[4], buff[5]);
        //源mac
        sprintf(mac_src, "%x:%x:%x:%x:%x:%x", buff[6], buff[7], buff[8], buff[9], buff[10], buff[11]);
        //协议类型
        mac_type = ntohs(*(unsigned short *)(buff+12));
        printf("以太网头----mac_dest:[%s], mac_src:[%s], mac_type:[%#x]\n", mac_dest, mac_src, mac_type);
    
        if(mac_type == 0x0800){
            //----------------解析IP头------------------
            ip_len = ntohs(*(unsigned short *)(buff+16));
            ip_ttl = *(buff+22);
            ip_type = *(buff+23);
            sprintf(ip_src, "%d.%d.%d.%d",buff[26], buff[27], buff[28], buff[29]);
            sprintf(ip_dest, "%d.%d.%d.%d",buff[30], buff[31], buff[32], buff[33]);
            printf("IP头----ip_len:[%d], ip_ttl:[%d], ip_type:[%d], ip_src:[%s], ip_dest[%s]\n",
                ip_len, ip_ttl, ip_type, ip_src, ip_dest);
            
            if(ip_type == 6){//TCP
                //----------------解析TCP头------------------
                //源端口号
                port_src = ntohs(*(unsigned short *)(buff+34));
                //目的端口号
                port_dest = ntohs(*(unsigned short *)(buff+36));
                printf("TCP头----port_src:[%d], port_dest:[%d]\n", port_src, port_dest);

            }else if(ip_type == 17){//UDP
                //----------------解析UDP头------------------
                //源端口号
                port_src = ntohs(*(unsigned short *)(buff+34));
                //目的端口号
                port_dest = ntohs(*(unsigned short *)(buff+36));
                printf("UDP头----port_src:[%d], port_dest:[%d]\n", port_src, port_dest);
            }else if(ip_type == 1){
                printf("ICMP头----\n");
            }else if(ip_type == 2){
                printf("IGMP头----\n");
            }
        }else if(mac_type == 0x0806){
            printf("ARP头----\n");
        }else if(mac_type == 0x8035){
            printf("RARP头----\n");
        }
        printf("\n");
        printf("-----------------------------------------------------------------\n");
        printf("\n");
    }
    close(socket_fd);

    return 0;
}
4.arp协议的使用

arp协议:地址解析协议,通过对方的ip地址,获取对方Mac地址

运行机制:源主机进行组包,指定自己的ip地址和mac地址,指定对方的ip地址,通过广播的信息获取,所以mac地址是广播的mac地址(全是f),设置arp请求将数据包发送给对方,交换机接收到这个数据包之后,一看是目的mac地址是广播的,所以会将这个数据包发送给当前网段下所有的主机,主机接收到这个数据包之后对比目的ip地址,如果不是自己的则丢弃,如果是自己的,将组包之后以arp应答的方式将数据包发送给获取者。

arp 数据报:

接下来,我要让ubuntu获取windows的mac地址:

以太网头:

        Dest Mac:目的mac地址,广播的mac地址,全是f

        Src Mac:源mac地址,ubuntu的mac地址,00:0c:29:a1:f9:68

        帧类型:后面跟的协议类型,如果是arp,则指定为0x0806

arp头:

        硬件类型:以太网,1

        协议类型:IP地址,0x0800

        硬件地址长度:6

        协议地址长度:4

        OP:请求还是应答,1

                1(ARP请求),2(ARP应答),3(RARP请求),4(RARP应答)

        发送端以太网地址:源mac地址,ubuntu的mac地址,00:0c:29:a1:f9:68

        发送端ip地址:源ip地址:ubuntu的ip地址,192.168.70.95

        目的以太网地址:目的mac地址,未知,可以传0

        目的ip地址:目的ip地址,windows的ip地址,192.168.70.8

代码实现:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define N 128

#define ERRLOG(errmsg) do{\
            perror(errmsg);\
            printf("%s (%s):%d\n", __FILE__, __func__, __LINE__);\
            exit(-1);\
        }while(0)

int main(int argc, char *argv[]){
    int socket_fd = -1;
    if(-1 == (socket_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)))){
        ERRLOG("socket error");
    }

    //使用arp协议通过对方ip地址获取对方mac地址
    //组包
    unsigned char buff[42] = {
        //以太网头
        //目标mac
        0xff,0xff,0xff,0xff,0xff,0xff,
        //源mac
        0x00,0x0c,0x29,0xa1,0xf9,0x68,
        //协议类型
        0x08,0x06,

        //arp头
        //硬件类型 1(以太网)
        0, 1,
        //协议类型  0800(ip地址)
        0x08, 0x00,
        //硬件地址长度
        6,
        //协议地址长度
        4,
        //op  1 arp请求   2 arp应答
        0, 1,
        //源mac--ubuntu的mac
        0x00,0x0c,0x29,0xa1,0xf9,0x68,
        //源IP地址
        192,168,60,123,
        //目的mac  ----不确定 全0即可
        0, 0, 0, 0, 0, 0,
        //目的IP地址
        192,168,60,112
    };

    //将arp请求报文发送出去,通过 ens37 发送出去                                                                     
    //使用ioctl函数获取本机网络接口                                                                    
    struct ifreq ethreq;                                                                                          
    strncpy(ethreq.ifr_name, "ens37", IFNAMSIZ);//IFNAMSIZ  16                                                                 
    if(ioctl(socket_fd, SIOCGIFINDEX, ðreq) == -1)                                                                
    {                                                                                                             
        perror("fail to ioctl");                                                                                  
        exit(1);                                                                                                  
    }                                                                                                             
                                                                                                                
    //设置本机网络接口                                                                                            
    struct sockaddr_ll sll;                                                                                       
    bzero(&sll, sizeof(sll));                                                                                     
    sll.sll_ifindex = ethreq.ifr_ifindex;

    //将数据包发送给网卡
    if(sendto(socket_fd, buff, 42, 0, (struct sockaddr *)&sll, sizeof(sll)) == -1)
    {
        ERRLOG("sendto error");
    }


    //接收对方的数据包并解析出mac地址
    unsigned char mac[18] = {0};
    memset(buff, 0, 42);
    while(1)//要循环获取,直接一次可能会接不到
    {
        if(recvfrom(socket_fd, buff, 42, 0, NULL, NULL) == -1)
        {
            ERRLOG("recvfrom error");
        }

        //先判断数据包是否是arp数据包
        if(ntohs(*(unsigned short *)(buff+12)) == 0x0806)
        {
            //再判断是否是arp应答
            if(ntohs(*(unsigned short *)(buff+20)) == 2)
            {
                sprintf(mac, "%x:%x:%x:%x:%x:%x", buff[22], buff[23], buff[24], buff[25], buff[26], buff[27]);

                printf("%d.%d.%d.%d -- %s\n", buff[28], buff[29], buff[30], buff[31], mac);
                break;
            }
        }
    }

    close(socket_fd);

    return 0;
}

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/676226.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-19
下一篇 2022-04-19

发表评论

登录后才能评论

评论列表(0条)

保存