原文地址:http://www.drdobbs.com/jvm/java-and-c-socket-communication/222900697?pgno=1
文中有一些地方翻译的着实不好,还请直接去阅读原文。另外,本文使用的是XML来进行数据的传输,你也可以使用json进行传输,然后在服务器端引入jsoncpp库,使用json传输会更加方便。源码在本文的末尾,可直接下载。
通过socket网络通信集成c++ Windows应用和Java应用
在单一平台上只使用一种语言来部署执行你所有的应用可能是一件非常理想的事情,但这往往是不切实际的。有时候你需要将一个新的程序和一个旧的程序集成,此时两者之间的通信问题可能就会比较棘手了。例如,你想将两个程序分离开来,以便于新程序的设计不会受到干扰,而旧的程序在不影响新程序的情况下也可以得到升级。
可能的解决方案
过去,我探索了那些人们已经讨论过的分布式计算解决方案,即通过Java Native Interface (JNI), Java Message System (JMS)和web services将Java和c++应用集成起来(见本文结尾的“结论”部分)。虽然这些方法在适合的情形下表现很好,但有些时候这些方法就会非常复杂并且表现的非常糟糕。例如,通过jni在Java中调用本地代码是复杂的,耗时的,且易于出错。使用JMS需要注册,安装,配置一个JMS Provider。而web service需要重要的基于网络的基础开发。
另外一个解决方案是在Java和c++应用之间直接使用基于socket的网络通信。虽然这种方法较为低级,但仍然是一种有效的解决方案。使用XML作为两者之间的消息传输协议,你可以在一定程度上维持平台和语言的独立性。图1用一种简单的方式进行了阐述。这里我们展示一个使用Windows socket的c++应用和一个使用Java IO(情景c)的Java程序进行通信的例子,其容易程度就和下面这两个同类的例子(情景a和b)一样,且无需改动代码。
图1 Java应用直接与c++ winsocket应用通信
Socket 解决方案
我们来探索一个示例性的集成解决方案,包括一个使用java.io的Java程序和一个使用Windows sockets的c++程序的通信。Windows应用支持以下三种简单的请求:
<Request>
<Name>GetHostnameName>
Request>
<Request>
<Name>GetMemoryName>
Request>
<Request>
<Name>GetRandomNumberName>
Request>
例1 客户端发送给服务器的XML请求消息
如果你想和每一条请求一起发送数据,添加一个或多个XML节点到请求数据中即可。例如,如果你想改变GetMemory 消息来请求显示内存(物理的或虚拟的)的类型,XML可以改为如下形式:
<Request>
<Name>GetMemoryName>
<Type>PhysicalType>
Request>
服务器响应消息和请求消息类似,但是有一些明显的改变,见例2.主要的不同还是XML响应类型和请求的数据的内容。
<Response>
<Name>HostnameResponseName>
<Hostname> MyServer Hostname>
Response>
<Response>
<Name>MemoryResponseName>
<TotalPhysicalMemory> 1073201152TotalPhysicalMemory>
Response>
<Response>
<Name>RandomNumberResponseName>
<Number>20173Number>
Response>
例2 服务器响应客户端请求的XML响应消息
在本例中,服务器返回的每一个具体的数据根据计算机的不同而不同。让我们先从Java客户端开始深入探究一下解决方案的具体实施。
Java客户端
例子中Java客户端是一个Java Swing应用,包含了这样的一个用户界面:用来发送请求的按钮,一个list view来展示服务器的响应消息(见图2)。也有一个“Connect”的按钮,当按下此按钮时客户端就尝试按照给定的地址来连接服务器。地址默认是localhost,但是你可以更改为任何合法的主机名或IP地址。因此,你可以选择在同一台计算机上执行客户端和服务器程序,也可以在不同的计算机上执行。我们稍后会检查c++服务器程序。
图2 Java客户端程序用户界面
程序的核心是Java网络和IO代码,他们封装在SocketClient类(例3展示了部分代码)的内部。这个类包含了2个嵌入类,Listener和Sender,分别用于监听请求和发送响应消息。
public class SocketClient {
private Listener listener = null; // listens for requests
private Sender sender = null; // sends responses
public boolean connected = false;
ServerResponses viewer = null;
class Listener extends Thread {
// ...
}
class Sender {
static final String HOSTNAME = ...
static final String MEMORY = ...
static final String RANDOM_NUM = ...
// ...
}
public SocketClient(String IPAddress,
ServerResponses viewer) {
// ...
}
public boolean isConnected() {
return connected;
}
public void requestHostname() {
sender.requestHostname();
}
public void requestMemory() {
sender.requestMemory();
}
public void requestRandomNumber() {
sender.requestRandomNumber();
}
}
例3 SocketClient类实现了发送请求和监听服务器响应的所有代码
当SocketClient对象创建时,即建立了和服务器的连接。构造函数中的第一个参数是要连接的服务器的地址,第二个参数是实现ServerResponses接口的对象的引用。见例4.
public interface ServerResponses {
public void onHostnameResponse(String msg);
public void onMemoryResponse(String msg);
public void onRandomNumberResponse(String msg);
}
例4 当响应消息到达时就调用ServerResponses 接口方法
因此,当“Connect”按钮被按下时,用户界面代码以实例化SocketClient类来驱动连接和请求的过程,然后当相关按钮被按下时再调用它的任何一个public方法–requestHostname(), requestMemory(), requestRandomNumber()。当接收到请求所对应的服务器响应数据时,ServerResponses提供的相关方法就会被调用。
在本例中,每次你按下任何一个请求按钮,Java客户端就会发送一条请求消息给服务器,然后相关的响应消息就会在客户端的list view中显示(见图3)。
图3 Java客户端显示一些服务器响应消息
这里你可以看到显示列表已经更新了,并显示建立了和地址为192.168.0.180的服务器的连接。让我们以更加详细的方式来检查一下Java网络通信的代码。
Java网络编程
当SocketClient类被创建时,构造函数中的代码就尝试以8080端口和给定IP地址的服务器建立连接。类似代码如下:
try {
java.net.Socket socket =
new java.net.Socket("localhost", 8080);
this.listener = new Listener(socket);
this.sender = new Sender(socket);
}
catch ( Exception e ) {
e.printStackTrace();
}
代码很简单,如果连接失败就抛出异常,如果成功则被激活的socket对象就传给Listener和Sender类来实现真正的工作。
使用这个激活的socket对象来发送网络数据,你需要使用java.io包中的Java IO 流。Sender类封装好了具体实现,但总的来说,要发送网络信息,你需要以socket的输出流开始,这个输出流就是用来发送数据的。为了提高效率,我们将这个输出流封装进java.io.BufferedOutputStream对象,然后将要发送信息的字节写入进这个输出流中。
String xml = "GetHostname ";
BufferedOutputStream bos =
new BufferedOutputStrean( socket.getOutputStream() );
bos.write( xml.getBytes() ); // writes to the buffer
bos.flush() ; // writes the buffer to the network
监听网络信息的代码也是类似的,因为它也使用了Java IO流。可是,由于等待消息的特性(即程序需要一直监听是否有来自服务器的响应消息,译者注),你应该在一个独立的线程中执行等待消息这项任务。Listener类继承了Thread类,当线程启动时就在Thread.run()方法中调用任何一个read()方法。(这句话翻译的好烂)
在独立的线程中执行这个代码能够保证应用的其他部分能够正常执行(如响应用户输入),因为read方法等待数据传输所需要的时间是不确定的。这个解决方案很简单,对于本例子来说足够完美了,但是对于那些需要建立许多socket连接来进行通信的程序来说,即使在每一个socket中创建一个线程也会显得难以应对。此时,你就可以在read方法中指定一个timeout,并定期调用,或者执行一个非阻塞的IO(这个超出了本文的讨论范围)。
Listener类中执行上述功能的代码如下:
public void run() {
// Create a reader to wrap the socket's InputStream
InputStreamReader inputReader =
new InputStreamReader(socket.getInputStream() );
BufferedReader bufferedReader =
new BufferedReader( inputReader );
while ( true ) {
// readLine blocks until a message arrives
String xml = reader.readLine();
// process the XML ...
}
}
c++Windows应用
我们来检查一下c++ Windows服务器程序,它仍然使用的是sockets来进行网络通信。具体的实现中服务器是不知道客户端所用的语言是Java,C/C++还是其他的语言。XML的使用进一步保证了通信能够成功。
当服务器程序启动时,就会创建一个ServerSocket 类的实例,并在8080端口监听新的客户端的连接。总之呢,这个类使用了Windows Sockets来创建一个网络socket,然后监听,接受新的客户端连接请求(当接收到客户端请求时)。
// Start winsock and create the socket
WSAStartup(...);
listenSocket = socket(...);
// TCP bind and listen
bind( listenSocket, ... );
listen( listenSocket, SOMAXCONN);
while ( listening ) {
// Accept a client socket (
clientSocket = accept(listenSocket, NULL, NULL);
// ...
}
考虑到一直等到有新的客户端连接时才调用accept()代码块,这些代码是在他们自己的线程中执行的。一旦有客户端连接,产生的结果代码和Java socket的实现是类似的,因为我们创建的对象就会通过socket来发送数据并接收数据。(这段翻译的好烂)
// Setup the socket sender and listener
sender = new SocketSender(clientSocket);
listener = new SocketListener(clientSocket);
listener->setSender(sender);
listener->startThread();
和Java socket代码一致,SocketListener 类执行了Windows sockets中的recv()方法,在自己的线程中,此方法会一直阻塞到数据到来。
char recvbuf[BUFFERLEN];
int bytes;
while ( true ) {
bytes = recv(clientSocket, recvbuf, BUFFERLEN, 0);
if (bytes > 0) {
recvbuf[bytes-1] = 0; // null terminate the string
HandleMessage(recvbuf);
}
}
每一个请求会在HandleMessage()方法中做进一步处理,相应的响应也会发送回客户端。
int num = rand();
sprintf_s(buffer, "<Response><Name>RandomNumberResponseName>\
<Number>%iNumber>Response>\n", num);
send( clientSocket, buffer, strlen(buffer), 0 );
总结
现在你应该对Java和C/C++应用程序通过socket网络编程进行通信有了基本的了解。实际上,虽然本文的例子使用的是Windows sockets,但相应的开发一个Linux端的服务器程序也能够与这里的Java客户端很好的交互。结合XML,在某些情况下,本文应该提供了一种合适的解决方案。想要了解更多的其他应用程序集成技术,比如JNI, JMS, 或 web services,那就点击以下链接吧
-C++, Java, and XML Processing
- The Java Message Service
- Java, C++, and SOA
本文代码在这里:http://download.csdn.net/detail/hnyzwtf/9467489
解压后得到三个压缩文件,JavaClient.zip是客户端,需要安装NetBeans IDE才能运行, NativeServer.zip是服务器端,listings.zip没什么大的卵用。其中在NativeServer.zip中有一个类SocketListener,有如下一段代码:
DWORD SocketListener::m_ThreadFunc() {
listening = true;
while ( listening ) {
int iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
if (iResult > 0) {
printf("Bytes received: %d\n", iResult);
***recvbuf[iResult-1] = 0;*** // null terminate the string according to the length
//注意这句话,我认为改成recvbuf[iResult] = 0;会更好一些。
// The message spec indicates the XML will end in a "new line"
// character which can be either a newline or caraiage feed
// Search for either and replace with a NULL to terminate
if ( recvbuf[iResult-2] == '\n' || recvbuf[iResult-2] == '\r' )
recvbuf[iResult-2] = 0;
HandleMessage(recvbuf);
}
else {
printf("Client connection closing\n");
closesocket(ClientSocket);
return 1;
}
}
return 0;
}