最近正在開發一個基於指紋的音樂檢索應用,算法部分已經完成,所以嘗試做一個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);
-
-
-
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);
-
-
-
recv(sockfd,length,10,0);
-
file_length=atoi(length);
-
printf("file length is %d\n",file_length);
-
-
-
if(recv_file(path,sockfd,file_length)==-1)
-
{
-
perror("receive file failed");
-
close(sockfd);
-
pthread_exit(NULL);
-
}
-
-
result* list;
-
-
-
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));
-
-
-
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);
-
-
-
sprintf(length,"%d",1);
-
send(sockfd,length,10,0);
-
-
-
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());
-
-
-
netOutputStream.write(Integer.toString(fileLength).getBytes());
-
-
-
while((len=fileInputStream.read(buffer))>0)
-
{
-
netOutputStream.write(buffer,0,len);
-
}
-
-
-
netInputStream.read(readLen);
-
-
while(((char)readLen[0])=='1')
-
{
-
-
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就行,就這麼簡單。