1 简介
在《Linux socket编程案例》中,实现了服务器/客户端之间的字符串传输,本文要将其改造为实现文件传输。
角色:服务器--文件接收者
客户端--文件发送者
2 主要流程
文件发送端(client):fopen(打开本地文件)->fread(读取本地文件内容)->write(通过socket将本地文件内容发送到服务器)
文件接收端(server):read(通过socket读取client发送过来的内容)->fopen(创建一个本地文件)->fwrite(将通过socket接收的内容写入文件)
3 主要难题
文件传输一般需要协议(例如ftp)。由于这里基于TCP实现文件传输,发送端/接收端之间需要协调一致,例如,何时开始传输,何时结束传输。
开始传输:如果发送端与接收端连接上,就随时可以传输内容,一般以发送端为主导;
结束传输:a) 接收端主动结束;b) 发送端主动结束。
文件的开始传输比较简单,这里重点分析结束传输。
3.1 接收端主动结束
接收端主动结束接受文件,有多种情况:a)当接受不到新数据时,结束文件接收;当接收到特定的字符时,停止接收。
对于第a)种情况,如果文件传输过程中出现中断,或者网络状况不好时,数据之间出现时间间隔,将会导致错误的结束文件传输。
3.2 发送端主动结束
3.2.1 发送结束符
文件发送端发送完数据后,发送一个结束符告诉接收端,通知其结束文件接收。
3.2.2 断开连接
文件发送端发送完数据后,直接断开与接收端(服务器)的连接。接受端要负责检测连接是否已经断开,如果是,则停止接受文件。本文将重点分析此方法。下面,先给出发送端代码:
/******* 发送端:客户端sent.c ************/ #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <netdb.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/types.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { int sockfd; char buffer[1024]; struct sockaddr_in server_addr; struct hostent *host; int portnumber; FILE *fp = fopen( "./sent.c", "rb" ); if ( fp == NULL) { fprintf(stderr, "Open file error\n"); exit( 1 ); } if( argc != 3) { fprintf( stderr, "Usage:%s hostname portnumber\a\n", argv[0] ); exit(1); } if( ( host = gethostbyname( argv[1] ) ) == NULL) { fprintf(stderr,"Gethostname error\n"); exit(1); } if( ( portnumber = atoi( argv[2] ) )<0) { fprintf( stderr, "Usage:%s hostname portnumber\a\n", argv[0] ); exit(1); } /* 客户程序开始建立 sockfd描述符 */ if( ( sockfd = socket( AF_INET,SOCK_STREAM, 0 ) ) == -1) { fprintf( stderr, "Socket Error:%s\a\n", strerror(errno) ); exit(1); } /* 客户程序填充服务端的资料 */ bzero( &server_addr, sizeof( server_addr ) ); server_addr.sin_family = AF_INET; server_addr.sin_port = htons( portnumber ); server_addr.sin_addr = *( ( struct in_addr * )host->h_addr ); /* 客户程序发起连接请求 */ if( connect( sockfd, ( struct sockaddr * )( &server_addr ), sizeof( struct sockaddr ) ) ==-1 ) { fprintf(stderr, "Connect Error:%s\a\n", strerror(errno)); exit(1); } size_t nreads, nwrites; while( nreads = fread( buffer, sizeof(char), sizeof( buffer ), fp) ) { if ( ( nwrites = write( sockfd, buffer , nreads) ) != nreads ) { fprintf(stderr, "write error\n"); fclose( fp ); close( sockfd ); exit( 1 ); } } /* 结束通讯 */ fclose( fp ); close( sockfd ); exit(0); }说明:上述代码将本地文件sent.c读取并通过socket发送。
4 网络连接状况检测
根据第3章的描述,为了实现由文件发送端断开连接而达到结束文件传输的目的,需要接收端能够检测当前的网络状况(是否已经断开)。那么,怎样检测呢[11]? 参考资料[9]说明了如果不检测是否已经断开就继续发数据,将会导致程挂掉。参考资料[10]总结了判断客户端socket断开连接的方法,本文选择其方法二,接收端完整代码如下:
/******* 文件接收端:服务器(recv.c) ************/ #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <netdb.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/types.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/tcp.h> // struct tcp_info类定义 #include <stdbool.h> // bool类型[12] int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in server_addr; struct sockaddr_in client_addr; int portnumber; if( argc != 2 ) { fprintf(stderr,"Usage:%s portnumber\a\n", argv[0]); exit(1); } if( ( portnumber = atoi( argv [1] )) < 0 ){ fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]); exit(1); } /* 服务器端开始建立socket描述符 */ if( ( sockfd = socket( AF_INET, SOCK_STREAM, 0) ) == -1 ) { fprintf(stderr,"Socket error:%s\n\a",strerror(errno)); exit(1); } /* 服务器端填充sockaddr结构 */ bzero( &server_addr, sizeof(struct sockaddr_in) ); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl( INADDR_ANY ); server_addr.sin_port = htons( portnumber ); /* 捆绑sockfd描述符 */ if( bind( sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr))==-1) { fprintf( stderr, "Bind error:%s\n\a", strerror( errno ) ); exit(1); } /* 监听sockfd描述符 */ if( listen(sockfd,5) == -1) { fprintf( stderr, "Listen error:%s\n\a", strerror( errno ) ); exit(1); } while( 1 ) { /* 服务器阻塞,直到客户程序建立连接 */ int sin_size = sizeof(struct sockaddr_in); int new_fd = accept( sockfd, (struct sockaddr *)(&client_addr), &sin_size ); if( new_fd == -1 ) { fprintf(stderr,"Accept error:%s\n\a", strerror( errno ) ); exit( 1 ); } printf("Server get connection from %s\n", inet_ntoa( client_addr.sin_addr ) ); bool tcp_established = true; FILE *save_fp = fopen("out.txt", "w"); while( tcp_established ) { struct tcp_info info; int len = sizeof(info); getsockopt( new_fd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len ); if( info.tcpi_state == TCP_ESTABLISHED ) { // TCP连接还没有中断,可以读数据 char buffer[1024]; ssize_t length = read( new_fd, buffer, sizeof(buffer) ); if( length == -1 ) { fprintf(stderr, "Read Error:%s\n", strerror( errno )); exit(1); } else if ( length > 0){ fwrite(buffer, sizeof(char), length, save_fp); } } else { tcp_established = false; fclose( save_fp ); printf("received finished !\n"); } } /* 这个通讯已经结束 */ close( new_fd ); /* 循环下一个 */ } close( sockfd ); exit(0); }
说明:上述代码将接受到的文件内容保存于out.txt文件中。
5 代码完善
更加完善的代码,见《Linux socket文件传输2》
参考资料
[1]套接字传输文件的试验
[2]套接字实现文件传输
[3]怎么利用套接字传输大文件
[4]应用Socket套接字技术实现文件远程传输的方式分析
[5]CFile类循环读取大文件及套接字传输文件
[6]Linux网络编程:UDP实现可靠的文件传输
[7]Linux网络编程之socket文件传输示例
[8]Linux下的socket文件传输
[9]linux socket客户端被断开的后果和处理方法
[10]服务器中判断客户端socket断开连接的方法
[11]linux socket怎么检测断开
[12]关于linux下C语言编译器gcc不认识bool型的问题
[13]getsockopt的TCP层实现剖析