最近正在开发一个基于指纹的音乐检索应用,算法部分已经完成,所以尝试做一个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进行通信。句话:通信全部用字节实现。

在介绍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就行,就这么简单。

Java与C之间的socket通信的更多相关文章

  1. java与C++之间进行SOCKET通讯要点简要解析

    原文链接: http://blog.csdn.net/hslinux/article/details/6214594 java与C++之间进行SOCKET通讯要点简要解析 hslinux 0.篇外语 ...

  2. C++/java之间的Socket通信大小端注意事项

    在一个物联往项目中,需要java云平台与一个客户端做socket定制协议的通信:然而在第一次测试时,并没有按照预想的那样完成解析.查找资料以后是因为客户端的数据读取方式为小端模式,而java默认采用大 ...

  3. Delphi和JAVA用UTF-8编码进行Socket通信例子

    最近的项目(Delphi开发),需要经常和java语言开发的系统进行数据交互(Socket通信方式),数据编码约定采用UTF-8编码. 令我无语的是:JAVA系统那边反映说,Delphi发的数据他们收 ...

  4. java与c/c++进行socket通信

    比如Server端只接收一个结构Employee,定义如下: struct UserInfo {   char UserName[20];   int UserId; }; struct Employ ...

  5. java nio--采用Selector实现Socket通信

    server: /** * 选择器服务端 * Created by ascend on 2017/6/9 9:30. */ public class SelectorServer { // publi ...

  6. java nio实现非阻塞Socket通信实例

    服务器 package com.java.xiong.Net17; import java.io.IOException; import java.net.InetSocketAddress; imp ...

  7. Android模拟器的ip获取以及模拟器之间socket通信

    Android模拟器的ip获取以及模拟器之间socket通信           http://kalogen.iteye.com/blog/1565507 作者:李波 实现网络五子棋时用到了两个设备 ...

  8. 使用thrift实现了Javaserver和nodejsclient之间的跨平台通信

    1. 简介 thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发.它结合了功能强大的软件堆栈和代码生成引擎.以构建在 C++, Java, Python, PHP, Ruby, Erlang ...

  9. java和C#之间SOCKET通信的问题

    转自:http://www.cdtarena.com/javapx/201307/9170.html java和C#之间SOCKET通信的问题 一.服务器端(使用java编写) /** * 监听客户端 ...

随机推荐

  1. BIOS设置和CMOS设置的区别与联系

    BIOS是主板上的一块EPROM或EEPROM芯片,里面装有系统的重要信息和设置系统参数的设置程序(BIOS Setup程序): CMOS是主板上的一块可读写的RAM 芯片,里面装的是关于系统配置的具 ...

  2. WCF Security(转载)

    WCF Security 主要包括 "Transfer Security"."Access Control"."Auditing" 几个部分 ...

  3. 查看oracle数据库中的保留字

    SQL> select * from v$reserved_words;

  4. c++内存分配

    [导语] 内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不 ...

  5. Zend Server安装后首次运行就出现Internal Server Error的解决

    无论是使用哪个版本的Zend Server来搭建PHP服务器,首次运行都会出现Internal Server Error的错误,对很多新手而言,每当看到这种错误时,那一刻内心绝对都是崩溃的.然而,这个 ...

  6. NSUserDefaults 很详细的介绍使用(转发)

    http://my.oschina.net/u/1245365/blog/294449

  7. Java jsoup爬取图片

    jsoup爬取百度瀑布流图片 是的,Java也可以做网络爬虫,不仅可以爬静态网页的图片,也可以爬动态网页的图片,比如采用Ajax技术进行异步加载的百度瀑布流. 以前有写过用Java进行百度图片的抓取, ...

  8. SSM-SpringMVC-25:SpringMVC异常顶级之自定义异常解析器

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 上篇博客相信大家也看到了,自定义异常,用了SimpleMappingExceptionResolver这个解析 ...

  9. wsgiref源码解析

    wsgiref是PEP 333定义的wsgi规范的范例实现,里面的功能包括了: wsgi的环境变量 应答头部的处理 实现简单的HTTP服务器 简单的对程序端和服务器端校验函数 我们先看一个简单的代码实 ...

  10. 利用 yEd 软件做元数据管理

    利用 yEd 软件做元数据管理 yEd Diagram editor 是我常用的 flow chart 制图工具, 另外我也用它画 ER 和 use case 图. 总结一下我喜欢 yEd 的原因:1 ...