嵌入式linux的网络编程(6)--多线程文件服务器
1.简介
本文介绍一个多线程文件服务器的例子,服务器根据客户端的请求,将其所要求的文件发给客户端.采用多线程工作,可以同时为多个客户端提供服务.这样的需求情景在很多实际应用中都是存在.
整个服务器端程序由四个文件构成,分别为:fileserver.c,fileclient.c,rw.h和rw.c.其中fileserver.c为多线程服务器的主程序,fileclient.c是测试用的客户端程序,rw.h和rw.c主要提供了大数据量收发的功能函数.
2.文件服务器的主程序
在之前的文章中,我们曾讨论过多线程程序设计的问题,而且还与多进程的程序做过对比,得出在嵌入式系统中,应该优先考虑使用多线程来实现多任务处理.虽然采取创建子进程的方式来实现服务器同时对多个客户端服务的做法是最为常见的,但考虑嵌入式系统的实际,我们还是讨论多线程的实现方法.其实基本原理是一样的,就是为每个与客户端的连接创建一个处理子程序.
下面我们列出服务器主程序的代码.
/**************************************************************************************/
/*文件:fileserver.c */
/*简介:多线程文件服务器主程序。 */
/*************************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "rw.h"
#define BACKLOG 10
/*工作线程函数*/
void *deal_request(void * arg)
{
int connfd = *((int *)arg);
char file_name[MAX_LEN];
/*分离模式,线程工作完毕就清理资源退出*/
pthread_detach(pthread_self());
/*处理客户端请求*/
if(read_cmd(connfd,file_name,sizeof(file_name))==-1)
{
printf("read file name error!\n");
}
else
{
/*发送请求的文件数据*/
if(send_file(connfd,file_name)==-1)
{
printf("Send file error\n");
}
}
/*对应于主程序中的malloc()函数*/
free(arg);
close(connfd);
pthread_exit(NULL);
}
int main(int argc, char* argv[])
{
int sockfd;
int *connfd;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
struct sockaddr_in tempaddr;
socklen_t templen;
socklen_t clilen;
/*建立TCP套接字*/
if((sockfd = socket(AF_INET, SOCK_STREAM,0)) == -1)
{
perror("socket");
exit(1);
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = 0; /*随机的端口号*/
/*绑定TCP套接字*/
if (bind(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) == -1)
{
perror("bind");
exit(1);
}
templen = sizeof(struct sockaddr);
/*获得套接字的本地地址*/
if (getsockname(sockfd, (struct sockaddr *)&tempaddr,&templen) == -1)
{
perror("getsockname");
exit(1);
}
printf("Server is listening on port %d\n", ntohs(tempaddr.sin_port));
/*开始监听端口*/
if (listen(sockfd,BACKLOG) == -1)
{
perror("listen");
exit(1);
}
for(;;)
{
pthread_t th;
clilen = sizeof(cliaddr);
/*通信套接字,一定需要动态分配,以区别不同的线程*/
connfd = (int *)malloc(sizeof(int));
*connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&clilen);
if(*connfd == -1)
{
perror("accept");
continue;
}
/*为每一个与客户端的连接建立一个线程来处理其请求*/
if(pthread_create(&th,NULL,deal_request,connfd)!=0)
{
perror("pthread_create");
break;
}
}
return 0;
}
接下来我们对这个程序进行详细的分析
3.动态分析监听端口
出于某种需求,服务器的监听端口需要动态的分配,getsockname()函数可以实现该功能.getsockname()函数的作用是获得指定套接字的本地地址,其原型为:
#include <sys/socket.h>
int getsockname (int sockfd, struct sockaddr *addr, int *addrlen);
其参数的含义如下:
- sockfd:当前套接字的标示符;
- addr:指向一个套接字地址结构,返回的本地地址信息保存在这个结构体中;
- addrlen:表示addr所占的内存大小.
在绑定一个通配IP地址(INADDR_ANY)的TCP服务器上,通过调用该函数可以获得本地的IP地址和端口等地址信息.在上面的程序中,由于服务器的监听端口是随机的(为0),所以为了让客户端知道服务器的端口号,因此使用该函数以获得实际分配的端口号,这样就实现了动态分配监听端口的功能了.
4.多线程服务器的实现
现在我们具体来分析这段代码.实际上大部分的代码都与前面博客中的例子是相似的,一直到for循环的代码才有了比较明显的区别.所以我们主要分析for循环和线程处理函数的内容.为了便于叙述,这里将for循环单独列出,并标上行号:
在这个for循环中,将要完成的任务是:建立连接套接字描述符,然后新建一个数据处理子线程,并将这个描述符作为参数传递给数据处理子线程.这个过程看似普通,但有一个很容易犯错的地方,请看93和103行的注释,这涉及到线程对贡献数据的访问.
accept函数我们已经比较熟悉了,参数connfd是传递给数据处理线程的函数参数,connfd变量保存着连接套接字描述符.如果connfd不是动态分配的,而是在main函数定义的变量,则每个子线程的参数connfd都指向了同一个内存地址,由于多线程环境下,所有的线程都和创建它们的进程位于同一个内存地址空间中,如果在前一个线程还没有处理完数据传输的情况下,新的连接建立了,这时connfd的值将修改为新的连接的套接字描述符,原来那个还没有处理完的套接字消失了,结果必然导致数据传输的失败.
因此,为每一个连接的套接字描述符新建一块内存来保存其内容,知道数据处理完后再将这块内存释放,就可以避免套接字描述符丢失的问题了.
为了方便处理线程退出的情况,我们设置了线程的分离属性:当线程中止运行后就自动清理其所占的资源并退出.具体的实现可以看看前面给出的源码.
5.大量数据的读写函数
这里所指的大数据量是指超过read()或write()一次操作所能承载的数据量.我们知道read()函数和write()函数都有一个特点,若指定的数据量较大时,往往不能通过一次读写操作就完成所有的数据I/O工作,往往需要根据它们返回的实际读取或写入的数据量,再进行若干此的读写操作.在rw.c文件中提供了readall()和writeall()两个函数,解决了这个问题.代码如下:
/**************************************************************************************/
/*文件:rw.h */
/*简介:自定义I/O函数头文件。 */
/*************************************************************************************/
#ifndef __RW_H
#define __RW_H
/*读写缓冲区的大小*/
#define MAX_LEN 1024*10 /*10KB*/
ssize_t readall(int fd, void* buf,size_t *len);
ssize_t writeall(int fd, void* buf,size_t *len);
int read_cmd(int sockfd,char* cmd_buf,int len);
int send_file(int sockfd,char *file_name);
#endif
接下来是这些函数的实现:
/**************************************************************************************/
/*文件:rw.c */
/*简介:自定义I/O函数实现文件。 */
/*************************************************************************************/
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include "rw.h"
/*read()函数的扩展,解决读不全的问题*/
ssize_t readall(int fd, void* buf,size_t *len)
{
size_t nleft;
ssize_t nread;
ssize_t total;
char *ptr;
ptr = buf;
nleft = *len;
total = 0;
/*反复读,直到没有新的数据可读*/
while( nleft > 0)
{
if((nread = read(fd,ptr,*len)) == -1)
{
perror("readall");
break;
}
if(nread ==0)
break;
nleft -= nread;
ptr += nread;
total += nread;
*len = nleft;
}
*len = total;
return (nread==-1)?-1:0;
}
/*write()函数的扩展,解决写不够的问题*/
ssize_t writeall(int fd, void* buf,size_t *len)
{
size_t nleft;
ssize_t nwrite;
ssize_t total;
const char *ptr;
ptr = buf;
nleft = *len;
total = 0;
/*反复写,直到所有的数据都写入*/
while( nleft > 0)
{
if((nwrite = write(fd,ptr,*len))== -1)
{
perror("write all");
break;
}
nleft -= nwrite;
ptr += nwrite;
total += nwrite;
*len = nleft;
}
*len = total;
return (nwrite==-1)?-1:0;
}
/*分析客户端的命令,提取出请求的文件名*/
int read_cmd(int sockfd,char* cmd_buf,int len)
{
char line[MAX_LEN];
int my_len = 0;
int total_len =0;
char *ptr;
int can_read;
if(len > MAX_LEN)
len = MAX_LEN;
can_read = 1;
strcpy(cmd_buf,"\0");
while(can_read)
{
if((my_len = read(sockfd, line,len))<0 )
{
perror("read");
return -1;
}
total_len += my_len;
if(total_len > len)
{
printf("Recieve command error!\n");
return -1;
}
if((ptr=strstr(line,"\r\n"))==NULL)
{
if(total_len <= len)
strcat(cmd_buf, line);
}
else
{
strncat(cmd_buf,line,ptr-line);
can_read = 0;
}
printf("Client requests file: %s\n",cmd_buf);
}
return 0;
}
/*发送文件至客户端*/
int send_file(int sockfd,char *file_name)
{
int file_fd;
int file_size;
int read_left;
int len;
int error_flag;
int readlen;
struct stat file_state;
char buffer[MAX_LEN];
int dot_number = 0;
if((file_fd = open(file_name,O_RDONLY)) == -1)
{
perror("open");
return -1;
}
if(fstat(file_fd, &file_state)==-1)
{
perror("fstat");
return -1;
}
file_size = file_state.st_size;
read_left = file_size;
len = MAX_LEN;
while(read_left > 0)
{
/* now read the file */
readlen = MAX_LEN;
error_flag = readall(file_fd,buffer,&readlen);
if(error_flag<0 )
{
return -1;
}
read_left -= readlen;
len = readlen;
error_flag = writeall(sockfd,buffer,&len);
if(error_flag == -1)
return -1;
if(readlen==0 && read_left!=0)
{
printf("the file is not read fully!\n");
return -1;
}
if(read_left==0)
{
printf("\nServer sent file over!\n");
}
}
close(file_fd);
return 0;
}
为了解决read和write函数不能一次将要求的数据读出或者写入,read_all和write_all函数都采用了反复读或者写的方法,根据read或者write函数的返回值,判断还需要读或者写的数据量,并计算出偏移量,接着前一次的读或者写位置继续执行.
read_cmd和send_file函数是这个文件服务器实现其服务功能的主要函数.这些函数都比较常用,适用于很多应用场合,大家可以仔细分析一下.
6.客户端测试程序
有了上面的三个文件(http://download.csdn.net/detail/ce123/6764657),就可以构造出这个多线程的文件服务器了.同时,为了对这个服务器的功能进行测试,还需要编写一个合适的客户端程序,这样就有了下面的这个测试用的客户端程序.这个客户端程序是通过TCP协议与服务器端进行通信的,鉴于前面对TCP Client已做了详细的分析,这里就只列出该测试程序的代码,大家自己分析一下.
/*文件:fileclient.c */
/*简介:客户端测试程序。 */
/*************************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "rw.h"
/*换行计数*/
#define DOT_PERIOD 50
int main(int argc, char *argv[])
{
int sockfd;
int conn_ret;
struct sockaddr_in servaddr;
char cmd_buf[MAX_LEN];
char recvbuf[MAX_LEN];
int error_flag;
int len = MAX_LEN;
int file_fd;
int dot_number;
int total_len = 0;
/*参数检查*/
if(argc != 5)
{
printf("Usage: fileclient <address> <port> <src file> <des file>\n");
return 0;
}
/*建立socket*/
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("sock");
exit(1);
}
/*连接服务器*/
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
conn_ret = connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
if(conn_ret == -1)
{
perror("connect");
}
/*创建接收的文件*/
file_fd = open(argv[4],O_CREAT | O_WRONLY);
if(file_fd == -1)
{
perror("open");
exit(1);
}
/*构造客户端的请求消息,格式为“文件名\r\n”*/
len = strlen(argv[3])+3;
snprintf(cmd_buf,len,"%s\r\n",argv[3]);
/*发送请求*/
if((error_flag = writeall(sockfd,cmd_buf,&len))==-1)
{
exit(1);
}
/*缓冲的大小,决定了每次的I/O操作的数据量*/
len = MAX_LEN;
printf("\nfile is transferring:\n");
while ((error_flag = readall(sockfd,recvbuf,&len))==0)
{
if(len == 0)
{
printf("\nClient has received file!\n");
break;
}
/*显示本次接收到的数据量*/
printf(".");
printf("read length is %d\n",len);
dot_number++;
if((dot_number % DOT_PERIOD) ==0)
{
printf("\n");
dot_number = 0;
}
total_len+=len;
/*将接收到的数据写入文件*/
if(writeall(file_fd,recvbuf,&len) == -1)
{
printf("\nclient has some error when receive the file!\n");
break;
}
len = MAX_LEN;
}
printf("\nRecevied %d bytes\n",total_len);
close(file_fd);
close(sockfd);
return 0;
}
这个客户端的功能是从命令行得到所需要获取的文件的名称后,连接服务器并发送文件请求,服务器响应这个请求后将文件的数据发给客户端,然后写入这个文件中,最后断开连接并退出,每次只能请求和接收一个文件.