最近正在开发一个基于指纹的音乐检索应用,算法部分已经完成,所以尝试做一个Android App。Android与服务器通信通常采用HTTP通信方式和Socket通信方式。由于对web服务器编程了解较少,而且后台服务器已经采用原始socket实现与c客户端通信,这就要求Android客户端也采用socket实现。所以在开发Android app时采用了原始socket进行编程。
由于算法是用C语言实现的,而Android应用一般是Java开发,这就不可避免得涉及Java和C语言之间的通信问题。一种方案是在客户端采用JNI方式,上层UI用Java开发,但是底层通信还是用C的socket完成。这种方案需要掌握JNI编程,对不少Java开发者是个障碍。为了减小开发难度,最好的方案是直接用Java socket与C socket进行通信。但是这种方案也有问题,最大的问题在于API和数据格式的不统一。本人在本科曾尝试利用Java和c的socket进行通信,发现根本无法传递数据,一度认为这两种socket之间无法通信。今天重拾旧问题,必须一次性地完美地解决Java和C之间的socket通信问题。在此可以先将实现总结为1句话:通信全部用字节实现。
在介绍Java和c之间的socket通信之前,首先将音乐检索大概介绍一下,更详细的内容可参考基于指纹的音乐检索。基于指纹的音乐检索就是让用户录制一段正在播放的音乐上传服务器,服务器通过提取指纹进行检索获得相应的歌名返回给用户,就这么简单。简单的工作原理如图一。所以在该应用中,socket通信主要涉及两个方面:客户端向服务器发送文件和服务器向客户端发送结果两部分。下面先介绍服务器部分。
图1 音乐检索的简单工作原理示意图
1 服务器设计
服务器端采用C socket进行通信,同时为了能响应多用户请求,服务器端需要采用多线程编程。为了专注于socket通信,已经将无关代码去掉,首先看main方法。
typedef struct { int client_sockfd; …… }client_arg; void get_ip_address(unsigned long address,char* ip) { sprintf(ip,"%d.%d.%d.%d",address>>24,(address&0xFF0000)>>24,(address&0xFF00)>>24,address&0xFF); } int main() { int server_sockfd; int server_len; struct sockaddr_in server_address; int result; server_sockfd=socket(AF_INET,SOCK_STREAM,0); server_address.sin_family=AF_INET; server_address.sin_addr.s_addr=htonl(INADDR_ANY); server_address.sin_port=htons(9527); server_len=sizeof(server_address); bind(server_sockfd,(struct sockaddr*)&server_address,server_len); listen(server_sockfd,MAX_THREAD); while(true) { int client_sockfd; struct sockaddr_in client_address; int client_len; char ip_address[16]; client_arg* args; client_len=sizeof(client_address); client_sockfd=accept(server_sockfd,(struct sockaddr*)&client_address,(socklen_t*)&client_len); args=(client_arg*)malloc(sizeof(client_arg)); args->client_sockfd=client_sockfd; get_ip_address(ntohl(client_address.sin_addr.s_addr),ip_address); printf("get connection from %s\n",ip_address); //////////////////////create a thread to process the query///////////////////// pthread_t client_thread; pthread_attr_t thread_attr; int res; res=pthread_attr_init(&thread_attr); if(res !=0) { perror("Attribute creation failed"); free(args); close(client_sockfd); continue; } res=pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED); if(res !=0) { perror("Setting detached attribute failed"); free(args); close(client_sockfd); continue; } res=pthread_create(&client_thread,&thread_attr,one_query,(void*)args); if(res !=0) { perror("Thread creation failed"); free(args); close(client_sockfd); continue; } pthread_attr_destroy(&thread_attr); } return 0; }
服务器端采用标准的TPC(threadper connection)架构,即服务器每获得一个客户端请求,都会创建一个新的线程负责与客户端通信,具体的任务都在每一个线程中完成。这种方式有个缺点,就是存在线程的频繁创建和删除,所以还可以将accept函数放入每一个线程中进行独立监听(这种方式需要加锁)。需要注意的是我们需要设置线程属性为detached,表示主线程不等待子线程。下面介绍每个线程具体完成的任务:
void get_time(char* times) { time_t timep; struct tm* p; timep=time(NULL); p=gmtime(&timep); sprintf(times,"%d-%02d-%02d-%02d-%02d-%02d",p->tm_year+1900,p->tm_mon+1,p->tm_mday, p->tm_hour+8,p->tm_min,p->tm_sec); } int recv_file(char* path,int client_sockfd,int file_length) { FILE* fp; int read_length; char buffer[1024]; fp=fopen(path,"wb"); if(fp==NULL) { perror("Open file failed"); return -1; } while((read_length=recv(client_sockfd,buffer,1023,0))>0) { buffer[read_length]=‘\0‘; fwrite(buffer,1,read_length,fp); file_length-=read_length; if(!file_length) { fclose(fp); printf("write to file %s\n",path); return 0; } } return 0; } void* one_query(void* arg) { char file_name[32]; char path[64]="./recv_data/"; char length[10]; int file_length=0; client_arg* args=(client_arg*)arg; int sockfd=args->client_sockfd; get_time(file_name); strcat(file_name,".wav"); strcat(path,file_name); /////////////1.receive file length////////////////// recv(sockfd,length,10,0); file_length=atoi(length); printf("file length is %d\n",file_length); /////////////2.receive file content////////////////// if(recv_file(path,sockfd,file_length)==-1) { perror("receive file failed"); close(sockfd); pthread_exit(NULL); } result* list; //////////////3.search the fingerprint library, and get the expected music id////////////// int count=match(&list); char result_to_client[2000]; for(int i=0;i<count;i++) { if(list[i].confidence>0.4) { memset(length,0,sizeof(length)); memset(result_to_client,0,sizeof(result_to_client)); /////////////////4. retrieve the database to get detailed information ////////////// MYSQL_RES* res=select_music_based_on_id(list[i].id); row_result* row_res=fetch_row(res); sprintf(result_to_client,"%s,%s,%s,%d,%d,%lf",row_res->name,row_res->artist,row_res->album,list[i].score,list[i].start_time,list[i].confidence); /////////////////5. Send a retrieval flag(1:success,0:fail)////////////////////// sprintf(length,"%d",1); send(sockfd,length,10,0); /////////////////6. Send the result//////////////////////////////////// send(sockfd,result_to_client,2000,0); free_result(res); free_row(row_res); } else { memset(length,0,sizeof(length)); sprintf(length,"%d",0); send(sockfd,length,10,0); } } free(list); close(sockfd); pthread_exit(NULL); }
one_query函数实现了每个线程与客户端通信的代码。代码核心的部分可以表示为六步:1. 从客户端读取录制音频的长度;2. 读取实际的音频,并保存到文件,文件以当前时间命名;3. 检索指纹服务器,获得检索的音乐id;4. 如果检索结果置信度高,则利用检索到的id访问数据库获得更加详细的音乐信息;5. 给用户发送一个成功/失败标注;6. 如果检索成功,发送具体的音乐信息。
1.1 读取文件长度
在第一步读取音频长度时,我们采用了原始socket中的recv函数。该函数原型为:
Int recv(intsocket, void *buff, int length, int flags)
接收数据用void* 获取,我们可以用char数组按照字节来读取,读取之后再解析。需要注意的一点是参数中传递的长度必须大于客户端可能传递过来的长度,在此我们用10字节来表示传递的上限(int型最大约为4*109,需要10位,加上’\0’需要11位,但是音频长度远小于最大的int值,所以只分配10位)。读到的char数组之后利用atoi转化为实际的int型整数。网上很多博客在介绍Java和C之间的socket通信时会涉及复杂的大小端问题,由于我们将所有的数据都转成字节数组传递,所以不存在这个问题。
1.2 读取音频文件
音频文件的读取在recv_file中实现。读取的核心还是按照字节流来完成,每次读取1023字节的数据,然后写入文件。这里有两点需要注意:首先recv读取的长度和我们指定的长度可能不一致,也即返回的长度小于1023,我们需要以返回的长度为准;分配的数组长度是1024,但是我们每次读取的数据最长只能为1023,这是因为我们需要在读取数据的最后添加一个’\0’标记,用来标记数据的末尾。读取结束的标志是达到之前传递过来的文件长度。
1.3 检索指纹库
该步骤在获得完整的音频文件之后,就对该文件提取指纹然后检索指纹库,原理可参考基于指纹的音乐检索,在此不再赘述。检索的结果是一个音乐的top5列表。每一项结果都有检索得到的音乐id和相应的置信度。
1.4 访问数据库
该步骤在top 5列表中有置信度大于0.4的音乐时执行。利用检索得到的id去访问数据库,获得音乐的名字和作者等信息。
1.5 发送flag标记
在发送具体的信息之前先发送一个标记,表示此次检索是成功还是失败,方便客户端显示。如果成功,发送标记‘1’,失败则发送标记‘0’。发送时,并不是直接发送一个int型的整数,而是首先利用sprintf将整型变为char型字符串,交给客户端去解析。发送函数采用原始socket中的send函数,原型为:
Int send(int socket, const void * buff, int length, int flags)
1.6 发送音乐信息
当检索到对应的音乐时,则把具体的音乐信息发送给客户端。这里还是利用sprintf将信息都打印到字符串中。可以看出,为了与Javasocket通信,所有的数据传递都被转换成char*字符串。
2 客户端实现
在介绍客户端之前,先把代码贴出来:
import java.io.*; import java.net.*; public class Client { void query(String file,String ip,int port) { FileInputStream fileInputStream; DataInputStream netInputStream; DataOutputStream netOutputStream; Socket sc; int fileLength; byte[] buffer=new byte[1023]; byte[] readLen=new byte[10]; byte[] readResult=new byte[2000]; int len; int result_count=0; File f=new File(file); if(f.exists()) { fileLength=(int)f.length(); } else { System.out.println("No such file"); return; } try { fileInputStream=new FileInputStream(file); sc=new Socket(ip,port); netInputStream=new DataInputStream(sc.getInputStream()); netOutputStream=new DataOutputStream(sc.getOutputStream()); /////////////////////1.send file length////////////////////// netOutputStream.write(Integer.toString(fileLength).getBytes()); /////////////////////2. send file/////////////////////////// while((len=fileInputStream.read(buffer))>0) { netOutputStream.write(buffer,0,len); } ////////////////3. read result symbol/////////////////////////////// netInputStream.read(readLen); while(((char)readLen[0])==‘1‘) { /////////////////////4. Read result////////////////////////////// netInputStream.read(readResult); String result=new String(readResult); String[] ss=result.split(","); int score=Integer.parseInt(ss[3]); int startTime=Integer.parseInt(ss[4]); double confidence=Double.parseDouble(ss[5]); System.out.println("name:"+ss[0].trim()); System.out.println("artist:"+ss[1].trim()); System.out.println("album:"+ss[2].trim()); System.out.println("score:"+score); System.out.println("startTime:"+startTime); System.out.println("confidence:"+confidence); result_count++; netInputStream.read(readLen); } if(result_count==0) { System.out.println("No match music"); } fileInputStream.close(); netInputStream.close(); netOutputStream.close(); sc.close(); } catch(Exception e) { e.printStackTrace(); } } public static void main(String[] args) { Client client=new Client(); client.query(args[0],args[1],9527); } }
与服务器端相对应,客户端的流程主要分为四步:1. 发送文件长度;2. 发送文件内容;3. 读取标记;4. 读取检索结果。在此,读取文件采用FileInputStream流,网络通信采用DataInputStream和DataOutputStream。
2.1 发送文件长度
Java在发送int型时,也需要转换成字符串,在此我们先用Integer封装类获取int型的字符串表示,然后利用String类的getBytes函数获得其字节数组。最后利用DataOutputStream的write函数发送给服务器。
2.2 发送文件
发送文件的过程是:首先从文件中读取固定长度的内容,然后再利用write函数发送同等长度的字节数组。
2.3 读取标记
发送完文件之后,客户端就等着从服务器端获取检索结果。服务器首先返回一个0/1标记。由于该标记有效内容只有一个字节,所以我们可以通过读取第0个字节的内容来判断检索是否成功。读取是通过DataInputStream的read函数完成,读取的内容会放在原始的字节数组中。
2.4 读取音乐信息
如果检索成功,服务器在发送成功标记之后还会将完整的音乐信息发送过来。读取还是利用DataInputStream的read函数。读取的内容比较复杂,我们首先将字节数组转换成字符串,然后利用split函数解析出每一部分内容。之后就可以在Android UI界面中显示。
3 总结
在亲自完成Java和c之间的socket通信之后,感觉也没有那么复杂。其实核心就一点:所有的数据类型都转换成字节数组进行传递。C端用recv和send函数就行,Java端用read和write就行,就这么简单。