TCP socket多客户端文件传输

这一节讲一个tcp socket编程中常见的多客户端向服务器传输数据的例子。
程序流程如下:

  1. 服务器端:socket建立套接字,bind绑定地址,listen开始监听。
  2. 客户端:socket建立套接字,connect连接服务器,用户输入想要传输的文件名,用send函数把文件发送出去。
  3. 服务器端:每次监听到客户端的连接,服务器端就调用accept函数,返回新的套接字描述符client_fd,然后创建一个新的线程用recv来读取客户端发送的数据,不断循环这个过程。

这里需要重点理解的函数是accept,函数原型如下:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:

  1. sockfd:套接字描述符,该套接口在listen()后监听连接。
  2. addr:(可选)指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。
  3. addrlen:(可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数。

函数accept所返回的文件描述符是套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字(sockfd)具有相同的套接字类型和地址族。传给accept的原始套接字没有关联到这个连接,而是继续保持可用状态并接收其他连接请求。如果没有连接请求在等待,accept会阻塞直到一个请求到来。如果sockfd处于非阻塞模式,accept会返回-1,并将errno设置为EAGAIN或EWOULDBLOCk。

接下来看看多客户端向服务器传输程序源码。
C语言版代码如下:

/*fileserver.c*/
/* 所有的文件都已经以二进制到形式访问,所以打开读写文件都以二进制到形式进行 */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <netinet/in.h>

#define BUFFER_SIZE 4096
#define MAX_QUE_CONN_NM 5
#define PORT 6000
#define MAXSOCKFD       10
#define FILE_NAME_MAX 512

void recv_mul_file(int sockfd);
void* pthread_func(void * arg);

int main(int argc,char* argv[])
{
    int sockfd;
    int sin_size = sizeof(struct sockaddr);
    struct sockaddr_in server_sockaddr, client_sockaddr;
    int i = 1;/*使得重复使用本地地址与套接字进行绑定 */

    /*建立socket连接*/
    if ((sockfd = socket(AF_INET,SOCK_STREAM,0))== -1)
    {
        perror("socket");
        exit(1);
    }
    printf("Socket id = %d\n",sockfd);

    /*设置sockaddr_in 结构体中相关参数*/
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(PORT);
    server_sockaddr.sin_addr.s_addr = INADDR_ANY;
    bzero(&(server_sockaddr.sin_zero), 8);

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));

    /*绑定函数bind*/
    if (bind(sockfd, (struct sockaddr *)&server_sockaddr, sizeof(struct sockaddr))== -1)
    {
        perror("bind");
        exit(1);
    }
    printf("Bind success!\n");

    /*调用listen函数*/
    if (listen(sockfd, MAX_QUE_CONN_NM) == -1)
    {
        perror("listen");
        exit(1);
    }
    printf("Listening....\n");
    recv_mul_file(sockfd);
    close(sockfd);
    return 0;
}

void recv_mul_file(int sockfd)
{
    pthread_t tid;
    struct sockaddr_in client_sockaddr;
    int client_fd, sin_size = sizeof(struct sockaddr);
    while(1)
    {
        if ((client_fd = accept(sockfd, (struct sockaddr *)&client_sockaddr, (socklen_t *)&sin_size)) == -1)
        {
            perror("accept");
            exit(1);
        }
        pthread_create(&tid, NULL, pthread_func, &client_fd);
    }
}

void* pthread_func(void * arg)
{
    //recv file imformation
    int client_fd;

    char buff[BUFFER_SIZE];
    char filename[FILE_NAME_MAX];
    int count;
    bzero(buff,BUFFER_SIZE);
    client_fd = *(int *)arg;
    printf("recv from client,client_fd = %d\n",client_fd);
    //把接受到到字符放在长度为BUFFER_SIZE的buff地址上,接收成功返回接收到到字节数目
    count=recv(client_fd,buff,BUFFER_SIZE,0);   
    if(count<0)
    {
        perror("recv");
        exit(1);
    }
    //把filename地址上的内容复制到地址buff上,第三个参数表明复制多少个字节
    strncpy(filename,buff,strlen(buff)>FILE_NAME_MAX?FILE_NAME_MAX:strlen(buff));

    printf("Preparing recv file : %s\n",filename );

    //recv file
    //告诉函数库,打开的是一个二进制到可写文件,地址在指针filename
    FILE *fd=fopen(filename,"wb+"); 
    if(NULL==fd)
    {
        perror("open");
        exit(1);
    }
    bzero(buff,BUFFER_SIZE); //缓冲区清0

    int length=0;
    while(length=recv(client_fd,buff,BUFFER_SIZE,0)) //这里是分包接收,每次接收4096个字节
    {
        if(length<0)
        {
            perror("recv");
            exit(1);
        }
        //把从buff接收到的字符写入(二进制)文件中
        int writelen=fwrite(buff,sizeof(char),length,fd);
        if(writelen<length)
        {
            perror("write");
            exit(1);
        }
        bzero(buff,BUFFER_SIZE); //每次写完缓冲清0,准备下一次的数据的接收
    }
    printf("Receieved file:%s finished!\n",filename );
    fclose(fd);
    close(client_fd);
    return 0;
}

从上面代码看出,在recv_mul_file(sockfd)函数中会不断循环调用accept等待客户端请求的到来。当有客户端连接时,会返回一个新的套接字描述符,然后pthread_create创建一个新的线程pthread_func,用于接收来自客户端发送的数据。

/*fileclient.c*/

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <netdb.h>
#include <netinet/in.h>
#include <pthread.h>

#define PORT 6000
#define BUFFER_SIZE 4096
#define FILE_NAME_MAX 512

int main(int argc,char* argv[])
{
    int sockfd;
    struct hostent *host;
    struct sockaddr_in serv_addr;

    if(argc != 2)
    {
        fprintf(stderr,"Usage: ./fileclient Hostname(or ip address) \ne.g. ./fileclient 127.0.0.1 \n");
        exit(1);
    }

    //地址解析函数
    if ((host = gethostbyname(argv[1])) == NULL)
    {
        perror("gethostbyname");
        exit(1);
    }

    /*创建socket*/
    if ((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
    {
        perror("socket");
        exit(1);
    }

    /*设置sockaddr_in 结构体中相关参数*/
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);    //将16位的主机字符顺序转换成网络字符顺序
    serv_addr.sin_addr = *((struct in_addr *)host->h_addr);  //获取IP地址
    bzero(&(serv_addr.sin_zero), 8);   //填充0以保持struct sockaddr同样大小

    if(connect(sockfd,(struct sockaddr *)&serv_addr, sizeof(struct sockaddr))== -1)
    {
        perror("connect");
        exit(1);
    }

    //send file name information
    char filename[FILE_NAME_MAX];
    bzero(filename,FILE_NAME_MAX);//字符串数组清零

    char buff[BUFFER_SIZE];  //定义读写缓冲区
    int count;
    bzero(buff,BUFFER_SIZE);  //把缓冲区buff上的4096个字节清0
    printf("Please input the file name you wanna to send:");
    scanf("%s",filename);//在终端输入文件名
    //把filename地址上的内容复制到地址buff上,第三个参数表明复制多少个字节 
    strncpy(buff,filename,strlen(filename)>FILE_NAME_MAX?FILE_NAME_MAX:strlen(filename));  
    count=send(sockfd,buff,BUFFER_SIZE,0);
    if(count<0)
    {
        perror("Send file name");
        exit(1);
    }
    //read file
    //告诉函数库,打开的是一个二进制到可读文件,地址在指针filename
    FILE *fd=fopen(filename,"rb"); 
    if(fd==NULL)
    {
        printf("File:%s not found in current path\n",filename);
    }
    else
    {
        bzero(buff,BUFFER_SIZE);  //把缓冲区清0
        int file_block_length=0;
        //每次读BUFFER_SIZE个字节,下一次读取时内部指针自动偏移到上一次读取到位置
        while((file_block_length=fread(buff,sizeof(char),BUFFER_SIZE,fd))>0)  
        {  
            printf("file_block_length:%d\n",file_block_length);  
            //把每次从文件中读出来到4096字节到数据发出去(最后一次就没有4096字节)
            if(send(sockfd,buff,file_block_length,0)<0)  
            {  
                perror("Send");  
                exit(1);  
            }  
            bzero(buff,BUFFER_SIZE);//发送一次数据之后把缓冲区清零
        }  
        fclose(fd);  
        printf("Transfer file finished !\n");  
    }
    close(sockfd);
}

从上面程序可以看出,客户端以二进制可读的形式打开一个文件,然后循环读取文件内容到缓冲区,每次读取4096字节的数据send发出去,直到文件读取完毕。

打开终端测试一下:
用如下命令编译两个源文件

gcc xxx.c -o xxx -lpthread

在终端A执行./fileserver
终端A:

ubuntu:~/test/filetransfer/test-20170526/server$ ./fileserver
Socket id = 3
Bind success!
Listening....

在终端B执行./fileclient 127.0.0.1,然后输入想要传输的文件4.tar
同时在终端C执行./fileclient 127.0.0.1,然后输入想要传输的文件5.tar
会发现终端B和终端C同时在向服务器传输文件,一次发送的数据是4096字节
终端B:

ubuntu:~/test/filetransfer/test-20170526/client$ ./fileclient 127.0.0.1 
Please input the file name you wanna to send:4.tar
file_block_length:4096
file_block_length:4096
file_block_length:4096
file_block_length:4096
......
file_block_length:2581
Transfer file finished !

终端C:

ubuntu:~/test/filetransfer/test-20170526/client$ ./fileclient 127.0.0.1
Please input the file name you wanna to send:5.tar
file_block_length:4096
file_block_length:4096
file_block_length:4096
.....
file_block_length:1664
Transfer file finished !

再观察终端A,多了如下打印,表示接收到了来自终端B和终端C的文件

recv from client,client_fd = 4
recv from client,client_fd = 5
Preparing recv file : 4.tar
Preparing recv file : 5.tar
Receieved file:4.tar finished!
Receieved file:5.tar finished!
  • 3
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值