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层实现剖析

Logo

更多推荐