Java与C++之间的Socket通信,对于小的数据量和控制命令,直接可以封装成json或xml格式,进行传输。但对于文件等大数据量传输,必须要将文件封装成帧,每一帧都设定固定大小的缓冲区,逐帧传输。此时json和xml便不再适用了。
在此过程中要需解决如下问题:
1. Java和C++数据基本类型不同,不仅所占字节数不同(如long型,java占8bytes,C++一般为4bytes)。C++缓冲区一般使用char*型,但是java中没有char*型,相互之间传输的数据,如何接收解析?
2. C++常用的特殊类型:结构体,如何解析成Java中的类。即使是两者都具有的枚举类型,两者的机制是不一样的,如何进行对接?
3. Java端和C++端,发送给socket的数据形式是什么?char数组型还是字节型C++端又有何种形式进行接收?接收到的数据又如何正确解析出来?
4. 字节序问题。Java为大字节序,而大部分PC主机C++都是小字节序,大小字节序和网络字节序相互之间的转化,也是需要考虑的问题。
这些问题,本文将对其逐一进行讲述和解决。话不多说,先普及熟悉一下基础知识。
本文涉及的问题较为基础,大神若有垂怜本文,可直接跳过基础部分。
一、基础知识
1.大小字节序
1.1基本概念
由于不同主处理器和操作系统,多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序会有所不同,为保证不同系统之间的正确数据传输需要做相应的字节序调整,即为字节序问题。例如两个字节的short int和4个字节的float类型变量都会有字节序问题。
字节序通常分为大字节序(big-endian)和小字节序(little-endian)。
Ø 大字节序(big-endian):表示变量的起始地址存放低字节,高字节依顺序存放。
Ø 小字节序(little-endian):表示变量的起始地址存放高字节,低字节依顺序存放,这种存储方式对于人类来说是最自然的。
1.2 字节序范例
例如:在内存中双字0x01020304(DWORD)的存储方式,设定其内存地址为:4000&4001&4002&4003。
Ø 对于小字节序而言,其在内存中的存储方式为:01 02 03 04
Ø 对于大字节序而言,字节存储顺序恰好相反:04 03 02 01
如果用byte数组来保存数据,则具体的存储方式为:
Ø 小字节序:byteToByteValue={0x01,0x02,0x03,0x04}。
Ø 大字节序:与小字节序顺序恰好相反,byte ToByteValue = {0x04 , 0x03, 0x02 ,0x01 }。
1.3 主机大小字节序的判断
1.新建一个联合体union
typedef Union{
//存储双字节变量
unsigned short int value;
//与value共享一个内存地址,可以通过byte数组来访问value的高低字节
unsigned char byte[2];
}
2.通过对value赋值,如0xabcd,若byte中存储的高字节存储的是0xab,则为大字节序,否则为小字节序。
2.网络字节序和主机字节序
2.1基本概念
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。
如对4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit,这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。
主机字节序就是我们平常说的大端和小端模式,由主机CPU决定,不同CPU会有不同字节序。x86系列CPU都是小字节序。
2.2 常用字节序转换函数
在主机之间通过网络进行通信时,往往需要进行字节序转换,linux系统中常用字节序转换函数如下:
#include <arpa/inet.h>
uint32_t htonl(uint 32_t) 主机字节序到网络字节序的长整形转换
uint16_t htons(uint16_t):主机字节序到网络字节序短整型转换
uint32_t ntohl(uint32_t):网络字节序到主机字节序长整型转换
uint16_t ntohs(uint16_t):网络字节序到主机字节序短整型转换
其中XtoX就是进行数据存储顺序的主机和网络顺序的转换,h—host—主机,n—net—网络,l—long—长整形,s—short—短整形。
2.3 Java字节序和c++字节序
对于x86系列CPU都是小字节序,因此对于c++而言,主机字节序通常为小字节序。
JAVA字节序指的是在JAVA虚拟机中多字节类型数据的存放顺序,JAVA字节序也是BIG-ENDIAN。所以Android客户端和c++服务器数据通信时要注意字节序转换问题。
二、java和c++之间的socket通信代码实现
本系统在Android客户端采用将高字节序转换为低字节序,并存储在字节数组当中,进行socket字节传输,C++服务器直接按照各数据类型所占空间大小,解析出不同数据。反之,服务器向客户端发送数据也是字节数组形式,在客户单调用BasicDataTypeTransfer中的函数,将字节数组中的数据解析出来。
2.1 java和c++数据类型对比
1.C++与Java基本类型占用内存空间差别
----------------C++----------- ------------Java----------
(01)bool------------------1byte 01)boolean--1 byte
(02)char------------------1byte 02)char------2bytes
(03)signedchar---------1byte 03)byte------1 byte
(04)unsignedchar------1 byte
(05)wchar_t--------------2bytes
(06)short-----------------2bytes 04)short-----2 bytes
(07)unsignedshort-----2 bytes
(08)int--------------------4bytes 05)int---------4 bytes
(09)unsignedint--------4 bytes
(10)long------------------4bytes 06)long-------8bytes
(11)unsignedlong-------4 bytes
(12)longlong-------------8 bytes
(13)unsignedlong long--8 bytes
(14)float-------------------4bytes 07)float-------4 bytes
(15)double----------------8bytes 08)double----8 bytes
(16)longdouble---------12 bytes
Java中每一种数据类型的内存大小是已经严格限定。但C++标准规定了每个算术类型的最小存储空间,它并不阻止编译器使用更大的存储空间,以上采用32位机器上的C++数据类型的典型占用字节数。
Java中没有无符号类型,对于long型Java是8个字节,C++是4个字节。进行相应数据传输的时候,要特别注意,以免造成数据丢失或错误。
2.特殊类型:结构体和枚举类型的传输和转换
结构体是C++中经常要用到的数据结构,而Java中没有结构体,以及枚举类型的传输,要实现这些特殊类型的传输,需要对其内存中存储方式有一个透彻了解,特别是要把握字节存储的概念。
2.2代码实现
//文件数据帧数据类型
enum FssFileFrameType
{
FILE_NAME_TYPE = 1, //文件名称
FILE_DATA_TYPE = 2, //文件数据
FILE_END_TYPE = 3, //文件结尾
FILE_ERROR_TYPE = 4,//文件出错
FILE_OVERWRITE_TYPE = 5, //覆盖文件
FILE_SKIP_TYPE = 6, //跳过文件传送
DISCONNECT_TYPE = 7//断开连接
};
//文件数据帧
typedef struct
{
FssFileFrameType type;
int length;
char buffer[MAX_BUFFER_SIZE];
}FileDataFrame;
//状态控制帧数据类型
enum TransactionStateType
{
OK_TYPE = 1, //表明服务器端正确接收到了客户端的数据
FILE_EXIST_TYPE = 2, //文件已经存在
ERROR_TYPE = 3, //出错
FINISHED_TYPE =4, //文件传输并保存成功
INVALID_TYPE = 5 //保留的无效位,主要用来简化程序的控制逻辑
};
//状态控制帧
typedef struct
{
TransactionStateType type;
int length;
char info[MAX_INFO_SIZE];
}TransactionStateFrame;
//文件数据帧数据类型
public enum FssFileFrameType
{
FILE_INFO_TYPE(1), //文件名称
FILE_DATA_TYPE(2), //文件数据
FILE_END_TYPE(3), //文件结尾
FILE_ERROR_TYPE(4),//文件出错
FILE_OVERWRITE_TYPE(5), //覆盖文件
FILE_SKIP_TYPE(6), //跳过文件传送
DISCONNECT_TYPE(7); //断开连接
//用于对枚举成员初始化
public int value;
FssFileFrameType(int value){
this.value=value;
}
}
//状态控制帧数据类型
public enum TransactionStateType
{
OK_TYPE(1), //表明服务器端正确接收到了客户端的数据
FILE_EXIST_TYPE(2), //文件已经存在
ERROR_TYPE(3), //出错
FINISHED_TYPE(4), //文件传输并保存成功
INVALID_TYPE(5); //保留的无效位,主要用来简化程序的控制逻辑
//用于对枚举成员初始化
public int value;
TransactionStateType(int value){
this.value=value;
}
}
/**
* 文件数据帧
* @author yin
*
*/
public static class FileDataFrame{
public int type;
public byte[] packetArray =new byte[FILE_DATA_PACKET_SIZE];
private byte[] dataArray =new byte[MAX_BUFFER_SIZE];//缓冲数据
/**
* 用于下载文件,接收服务器文件帧,并解析出FileDataFrame各成员变量
* @param byteArray
*/
public FileDataFrame(byte[] byteArray){
}
/**
* 用于上传文件将文件数据包封装成帧
* @param msgType
* @param packLen
* @param packetArray
*/
public FileDataFrame(int msgType,int packLen,byte[] bufferArray){
}
}
/**
* 文件状态控制帧
* @author yin
*
*/
public static class TransactionStateFrame{
public int type;
public int length;
public byte[] infoArray =new byte[MAX_INFO_SIZE];//具体消息内容
//发送或接收到的字节数组帧
byte[] packetArray =new byte[STATE_CONTROL_PACKET_SIZE];
/**
* 用于接收消息,解析数据帧
* @param byteArray
*/
public TransactionStateFrame(byte[] byteArray){
}
/**
* 用于发送消息,将数据包封装成帧
* @param msgType
* @param packLen
* @param packetArray
*/
public TransactionStateFrame(int msgType,int packLen,byte[] bufferArray){
}
}
}
1. BasicDataTypeTransfer类
BasicDataTypeTransfer类负责各基本类型和字节数组的相互字节序转换,同时对将服务器通过socket发过来的字节数组进行解析。以下只提供几种类型的代码实现,其他类型原理类似。
package com.scut.util;
import java.io.UnsupportedEncodingException;
/**
* 字节序转换类
* @author yin
*
*/
public class BasicDataTypeTransfer {
public static BasicDataTypeTransfer basicDataTypeTransfer=null;
public static BasicDataTypeTransfer getInstance(){
if(basicDataTypeTransfer==null){
basicDataTypeTransfer=new BasicDataTypeTransfer();
}
return basicDataTypeTransfer;
}
/*
*将int转换为小字节序
**/
public byte[] IntToByteArray(int n){
byte[] b = new byte[4];
b[0] = (byte)(n & 0xff);
b[1] = (byte)(n>>8 & 0xff);
b[2] = (byte)(n>>16 &0xff);
b[3] = (byte)(n>>24 &0xff);
return b;
}
public int ByteArrayToInt(byte[] bAttr){
int n=0;
int leftmove;
for(int i=0;i<4&&(i<bAttr.length);i++){
leftmove = i*8;
n += bAttr[i]<<leftmove;
}
return n;
}
public byte[] StringToByteArray(String str){
byte[] temp=new byte[100];
try {
temp=str.getBytes("GBK");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return temp;
}
public String ByteArrayToString(byte[] bAttr,int maxLen){
int index=0;
while(index <bAttr.length&&index<maxLen){
if(bAttr[index] == 0){
break;
}
index++;
}
byte[] tmp = new byte[index];
System.arraycopy(bAttr, 0, tmp, 0, index);
String str=null;
try {
str = new String(tmp,"GBK");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return str;
}
}
2. FssCommon类
调用上述BasicdataTypeTransfer,针对C++服务器结构体的具体结构,进行相应的解析,以还原成对应的java类。
package com.scut.util;
/**
* 通用全局变量封装类
* @author yin
*
*/
public class FssCommon {
/**
* 文件数据帧
* @author yin
*
*/
public static class FileDataFrame{
public int type;
public byte[] packetArray =new byte[FILE_DATA_PACKET_SIZE];
private byte[] dataArray =new byte[MAX_BUFFER_SIZE];//缓冲数据
/**
* 用于下载文件,接收服务器文件帧,并解析出FileDataFrame各成员变量
* @param byteArray
*/
public FileDataFrame(byte[] byteArray){
BasicDataTypeTransfer basicDataTypeTransfer=new BasicDataTypeTransfer();
if(byteArray!=null && byteArray.length>=sizeOfInt){
byte[] typeArray = new byte[sizeOfInt];
//读出存储在前面的int型数据
System.arraycopy(byteArray, 0, typeArray, 0, sizeOfInt);
//将其转换为整形,从而转换为枚举类型
this.type=basicDataTypeTransfer.ByteArrayToInt(typeArray);
//读取length
byte[] lenByteArray = new byte[sizeOfInt];
System.arraycopy(byteArray, sizeOfInt, lenByteArray, 0, sizeOfInt);
//读取缓冲数据
System.arraycopy(byteArray,2*sizeOfInt, dataArray, 0, dataArray.length);
}
}
/**
* 用于上传文件将文件数据包封装成帧
* @param msgType
* @param packLen
* @param packetArray
*/
public FileDataFrame(int msgType,int packLen,byte[] bufferArray){
this.type=msgType;
System.arraycopy(bufferArray, 0, this.dataArray, 0, bufferArray.length);
BasicDataTypeTransfer basicDataTypeTransfer=new BasicDataTypeTransfer();
//cmdType
byte[] cmdTypeByte = basicDataTypeTransfer.IntToByteArray(msgType);
System.arraycopy(cmdTypeByte, 0, packetArray, 0, cmdTypeByte.length);
//packet length
byte[] packLenByte = basicDataTypeTransfer.IntToByteArray(packLen);
System.arraycopy(packLenByte, 0, packetArray, cmdTypeByte.length, packLenByte.length);
//packBuff
if(dataArray!=null){
System.arraycopy(dataArray, 0, packetArray, cmdTypeByte.length+packLenByte.length, dataArray.length);
}
//数据包总大小,4+4+MAX_BUFFER_SIZE(8*1024)
}
}
/**
* 文件状态控制帧
* @author yin
*
*/
public static class TransactionStateFrame{
public int type;
public int length;
public byte[] infoArray =new byte[MAX_INFO_SIZE];//具体消息内容
//发送或接收到的字节数组帧
byte[] packetArray =new byte[STATE_CONTROL_PACKET_SIZE];
/**
* 用于接收消息,解析数据帧
* @param byteArray
*/
public TransactionStateFrame(byte[] byteArray){
BasicDataTypeTransfer basicDataTypeTransfer=new BaisicDataTypeTransfer();
if(byteArray!=null && byteArray.length>=sizeOfInt){
byte[] typeArray = new byte[sizeOfInt];
//读出存储在前面的int型数据
System.arraycopy(byteArray, 0, typeArray, 0, sizeOfInt);
//将其转换为整形,从而转换为枚举类型
this.type=basicDataTypeTransfer.ByteArrayToInt(typeArray);
//读取length
byte[] lenByteArray = new byte[sizeOfInt];
System.arraycopy(byteArray, sizeOfInt, lenByteArray, 0, sizeOfInt);
this.length=basicDataTypeTransfer.ByteArrayToInt(lenByteArray);
//读取缓冲数据
System.arraycopy(byteArray,2*sizeOfInt, infoArray, 0, infoArray.length);
}
}
/**
* 用于发送消息,将数据包封装成帧
* @param msgType
* @param packLen
* @param packetArray
*/
public TransactionStateFrame(int msgType,int packLen,byte[] bufferArray){
this.type=msgType;
this.length=packLen;
System.arraycopy(bufferArray, 0, this.infoArray, 0, bufferArray.length);
BasicDataTypeTransfer basicDataTypeTransfer=new BasicDataTypeTransfer();
//cmdType
byte[] cmdTypeByte = basicDataTypeTransfer.IntToByteArray(msgType);
System.arraycopy(cmdTypeByte, 0, packetArray, 0, cmdTypeByte.length);
//packet length
byte[] packLenByte = basicDataTypeTransfer.IntToByteArray(packLen);
System.arraycopy(packLenByte, 0, packetArray, cmdTypeByte.length, packLenByte.length);
//packBuff
if(infoArray!=null){
System.arraycopy(infoArray, 0, packetArray, cmdTypeByte.length+packLenByte.length, infoArray.length);
}
//数据包总大小,4+4+MAX_INFO_SIZE(8*1024)
}
}
}
3.C++将字节数组还原成结构体
这一步就很简单啦,强制转换一下就okay。
if(!newsocket.recv((char *)&fileDataFrame,sizeof(fileDataFrame)))
{
std::cout <<"recv error:"<<errno;
}
switch(fileDataFrame.type)
{
case FILE_NAME_TYPE:
{
std::cout <<"file information received";
break;
}
case FILE_DATA_TYPE:
break;
case FILE_END_TYPE:
break;
default:
break;
三、总结
在实际编程中,不同编程语言,如Java、c、c++、delphi所写的网络程序进行通讯时,需要进行相应的数据转换。从不同数据之间的字节存储方式来理解不同主机和语言的数据通信,将各数据转换成字节流的形式进行传输和解析,是一种非常有效的方式。