1. 程式人生 > >Java與C之間的socket通訊

Java與C之間的socket通訊

最近正在開發一個基於指紋的音樂檢索應用,演算法部分已經完成,所以嘗試做一個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型整數。網上很多部落格在介紹JavaC之間的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就行,就這麼簡單。