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型整数。网上很多博客在介绍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就行,就这么简单。

时间: 2024-10-21 02:25:35

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

服务器之间借助Socket通信

正在完成一个并行仿真优化系统,系统流程大致上是如此的:用户在网页端提交需要进行R&S的系统候选集(以及各种参数设置)给主实体服务器,主实体服务器借助Tornado框架处理请求报文,然后借助Socket将系统候选集提交给虚拟机集群的主节点(集群一共16台Linux虚拟机),虚拟机集群在计算的同时,用户还会不断和主实体服务器交互,因此主实体服务器需要随时从主节点获取系统状态,反馈给用户. 关于socket,尽管很早上操作系统.计算机网络等等课程的时候就知道了,但一直没有真正的机会运用,现在先来了解一

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

最近的项目(Delphi开发),需要经常和java语言开发的系统进行数据交互(Socket通信方式),数据编码约定采用UTF-8编码. 令我无语的是:JAVA系统那边反映说,Delphi发的数据他们收到是乱码,而我这边(Delphi7,ANSI)收到的数据将utf-8转码成ansi也是乱码. 因为不太熟悉java语言,还曾经怀疑是不是Delphi的utf-8编码和java语言的不一样. 最近学习了一下java的相关知识,写一个小程序来测试验证一下我曾经的怀疑. 事实证明,Delphi7的UTF-

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

server: 1 /** 2 * 选择器服务端 3 * Created by ascend on 2017/6/9 9:30. 4 */ 5 public class SelectorServer { 6 // public final static String REMOTE_IP = "192.168.0.44"; 7 public final static String REMOTE_IP = "127.0.0.1"; 8 public final stat

实现Winform与Sliverlight之间的Socket通信转换

众所周知,Winform下的Socket与Sliverlight的Socket是不可以直接通信的,原因请参考http://www.cnblogs.com/ZetaChow/archive/2009/05/16/2237347.html.以下是经本人验证过的转换方法,具体流程为:先建立一个winform client端接收已存在的winform server端发来的信息,将接收到的信息通过新建的sliverlight server端发出,再建立一个sliverlight client端接收此消息,

java多线程实现多客户端socket通信

一.服务端 package com.czhappy.hello.socket; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; public class Server { public static void main(String[] args) { //创建ServerSocket示例,指定端口,侦听 try { Ser

使用thrift实现了Java服务器和nodejs客户端之间的跨平台通信

1. 简单介绍 thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发.它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的.高效的服务. 2. 下载与安装 Thrift目前最高0.9.2,地址:http://archive.apache.org/dist

C++服务器与java进行socket通信案例

分类: [java]2012-10-08 12:03 14539人阅读 评论(46) 收藏 举报 注:本代码版权所有!!!转载时请声明源地址:http://blog.csdn.net/nuptboyzhb/article/details/8047619 你可以学习,分享,修改,教学等.但是不得用于商业目的.目前已经发现互联网上大量与本文完全相同的文章,但是却把作者信息删除的干干净净,并且据为己有,打上某培训机构的广告!实属可恶! 最新消息:项目成品连接:http://blog.csdn.net/

Java中Socket通信的知识回顾---学习笔记

两台计算机进行通信的基本前提: (1)IP地址: 每台计算机都有自己独一无二的IP地址,根据IP地址判断与哪台计算机进行通信. (2)端口号: 每个应用程序都有自己专属的端口,根据端口号判断与计算机中的哪个应用程序进行通信. 说明: <1>用于区分不同应用程序 <2>端口号的范围:0-65535,其中0-1023是为系统保留的端口号 <3>常用的协议的端口号: http:80 ftp:21 telnet:23 <4>IP地址+端口号=Socket,Socke

Java:Socket通信

Socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求.ServerSocket用于服务器端,Socket是建立网络连接时使用的.在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话.对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别.套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认. 实例一: