1. socket需要解决粘包和分包问题
1.1什么是分包?
socket一次性传输的数据大小是有限制的,当有数据有一定大小时,需要拆分成多个包进行传输。
1.2什么是粘包?
socket在接受多个分片数据时,接受的数据并不是按我们规定的片大小传输,有可能一片分两次到达,也有可能是一次有多片数据达到。因此我们需要在接受完整数据时再进行解析。
2.代码解析
2.1 iOS 客户端分片发送数据
2.1.1 header
- 2个字节表示包长度,即一个short
- 1个字节表示类型,即一个char类型,0:字符 1:图片
- 2个字节表示分了多少片,即一个short
2.1.2 body
一次传输20000byte,假如有40005byte,body需要分三片传输
- 第一片:0-19999
- 第二片:20000-39999
- 第三片:40000-40005
2.1.3 发送代码
@interface SocketSendModel()
/*
长度:一个byte
0:文字,1:图片
*/
@property (nonatomic,copy) NSString * type;
@property (nonatomic,strong) NSData * bodyData;
@property (nonatomic,assign) short zoneNum;//分了多少片
@end
@implementation SocketSendModel
- (id)initWithType:(NSString *)type bodyData:(NSData *)data{
self = [super init];
if (self) {
_type = type;
_bodyData = data;
}
return self;
}
- (NSData *)packageDataWithType:(NSString *)type data:(NSData *)data{
NSMutableData * mutableData = [NSMutableData new];
NSData * typeData = [type dataUsingEncoding:NSUTF8StringEncoding];
short totalByteLength = sizeof(short)+typeData.length+sizeof(short)+data.length;
[mutableData appendBytes:&totalByteLength length:sizeof(short)];
[mutableData appendData:typeData];
[mutableData appendBytes:&_zoneNum length:sizeof(short)];
[mutableData appendData:data];
return mutableData;
}
- (NSArray<NSData *> *)packageData{
NSInteger zoneNum = self.bodyData.length/SOCKET_SEND_SIZE;
NSMutableArray<NSData *> * zoneArray = [NSMutableArray arrayWithCapacity:zoneNum+1];
self.zoneNum = zoneNum+1;
NSInteger sendLength = 0;
while (sendLength<self.bodyData.length) {
NSInteger remainLength = self.bodyData.length-sendLength;
if (remainLength>SOCKET_SEND_SIZE) {
remainLength = SOCKET_SEND_SIZE;
}
NSData * zoneData = [self.bodyData subdataWithRange:NSMakeRange(sendLength, remainLength)];
sendLength += SOCKET_SEND_SIZE;
NSData * zonePackData = [self packageDataWithType:self.type data:zoneData];
[zoneArray addObject:zonePackData];
}
return zoneArray;
}
@end
- packageData根据bodyData分成zoneNum片,把每个段添加到zoneArray数组中
- packageDataWithType把body与header组装成一个新的data,即每片长度是20000+5
//发送文字
- (void)sendMessage:(NSString *)msg{
if (!msg) {
return;
}
NSDictionary * dic =@{@"user":@"客户端1",@"massage":msg};
NSData * data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
SocketSendModel * model = [[SocketSendModel alloc] initWithType:@"0" bodyData:data];
[self sendDataWithSocketModel:model];
}
//发送图片
- (void)sendImageMessage:(UIImage *)image{
if (!image) {
return;
}
NSData * imageData = UIImageJPEGRepresentation(image, 1);
SocketSendModel * model = [[SocketSendModel alloc] initWithType:@"1" bodyData:imageData];
[self sendDataWithSocketModel:model];
}
//如果数据过大,自动分片发送
- (void)sendDataWithSocketModel:(SocketSendModel *)model{
NSArray<NSData *> * pageDataArray = [model packageData];
[pageDataArray enumerateObjectsUsingBlock:^(NSData * _Nonnull sendData, NSUInteger idx, BOOL * _Nonnull stop) {
[self.socket writeData:sendData withTimeout:3 tag:1];
}];
[self.socket readDataWithTimeout:-1 tag:0];
NSLog(@"发送数据,请等待发送结果");
}
- sendDataWithSocketModel发送数据:遍历pageDataArray,进行写入即可。
- 使用时只需要构造SocketSendModel,传入type和data.
2.2 java 后台分片接送数据
public class SocketReceiveModel extends SocketBaseModel{
public final static int LENBYTE = 2;//消息长度(2个字节)
public final static int TYPEBYTE = 1;//消息类型(1个字节):0字符,1图片
public final static int ZONEBYTE = 2;//分了多少片(2个字节)
String fileName;//文件名称
int writeByte;//写入字节
int zoneCount = 0;//当前的分片
InputStream inputStream;
byte[] bodyBytes = new byte[0];
public SocketReceiveModel(InputStream inputStream) {
this.inputStream = inputStream;
}
public int headerTotalBytes(){
return LENBYTE+TYPEBYTE+ZONEBYTE;
}
public void setWriteByte(int writeByte,int realByte) {
this.writeByte = writeByte+realByte;
}
//接收消息
public void receiveMsg() throws IOException {
//2字节长度域
byte[] lenbuf = new byte[LENBYTE];//长度
byte[] typebuf = new byte[TYPEBYTE];//类型
byte[] zoneNumbuf = new byte[ZONEBYTE];//分了几片
while (inputStream.read(lenbuf) != -1 && inputStream.read(typebuf)!=-1 && inputStream.read(zoneNumbuf)!=-1){
short totalByte = Base64Coded.bytesToShort(lenbuf,0);
String type = Base64Coded.bytesToStr(typebuf);
short zoneNum = Base64Coded.bytesToShort(zoneNumbuf,0);
int realTotalByte = 0;
if (totalByte >= headerTotalBytes()) realTotalByte = totalByte-headerTotalBytes();
System.out.println("实际大小:" + realTotalByte+" 类型:"+type+" ,分了"+zoneNum+"片");
byte[] buf = new byte[realTotalByte];
//内容域
int readCount = 0;
while (readCount < realTotalByte) {
readCount += inputStream.read(buf, readCount, realTotalByte - readCount);
}
if (type != null){
zoneCount++;
if (type.equals("0")){
bodyBytes = combineArrays(bodyBytes,buf);
if (zoneCount == zoneNum){
String rspStr = new String(bodyBytes);
System.out.println(rspStr);
}
}else if(type.equals("1")){
saveFile(buf,realTotalByte);
setWriteByte(writeByte,realTotalByte);
}
if (zoneCount == zoneNum){
bodyBytes = new byte[0];
zoneCount = 0;
fileName = null;
writeByte = 0;
}
}else {
System.out.println("出错了吧");
}
}
}
//保存文件
private void saveFile(byte[] bty,int len)throws IOException {
if (fileName == null){
fileName = UUID.randomUUID()+".png";//随机的生存一个32的字符串
}
//动态获取服务器的路径
String serverpath = "/Users/wupeng/Desktop/code/NodeAppServer/JavaWeb/out/artifacts/JavaWeb_war_exploded/UploadVideo";
File myfile=new File(serverpath);
if(!myfile.exists()){
myfile.mkdirs();
}
String newFilePath = serverpath+"/"+fileName;
FileOutputStream fos = new FileOutputStream(newFilePath,true);
fos.write(bty,0,len);
fos.close();
}
}
- 定义一个全局的bodyBytes
- 每接受一个新的分片,combineArrays把buf追加到bodyBytes
- 当count == zoneNum时,打印字符,并清空bodyBytes
2.3 java后台分片推送消息
public class SocketSendModel extends SocketBaseModel{
public static final int SOCKET_SEND_SIZE = 20000;
char type;
String bodyString;
private byte[] bodyBytes;
private short zoneNum;
public SocketSendModel(char type, String bodyString) {
this.type = type;
this.bodyString = bodyString;
bodyBytes = Base64Coded.stringToBytes(bodyString);
zoneNum = (short)(bodyBytes.length/SOCKET_SEND_SIZE+1);
}
public void sendData(DataOutputStream out)throws IOException {
int sendLen = 0;
while (sendLen<bodyBytes.length){
int remainLength = bodyBytes.length-sendLen;
if (remainLength>SOCKET_SEND_SIZE){
remainLength = SOCKET_SEND_SIZE;
}
System.out.println("发送:"+sendLen+" "+remainLength+"");
byte[] zoneBodyBytes = subByte(bodyBytes,sendLen,remainLength);//分片body数据
sendLen+=SOCKET_SEND_SIZE;
byte[] zonePackData = packageData(type,zoneBodyBytes);
out.write(zonePackData);
out.flush();
}
}
//组装发送data
private byte[] packageData(char type,byte[] data){
byte[] lenBytes = new byte[2];//两个字节表示长度
byte[] typeBytes = new byte[1]; //一个字节表示类型: 0文字;1图片
byte[] zoneNumBytes = new byte[2];//两个字节表示分了多少片
short total = (short)(lenBytes.length+typeBytes.length+zoneNumBytes.length+data.length);
Base64Coded.shortToBytes(lenBytes,total,0);
typeBytes[0] = (byte)type;
Base64Coded.shortToBytes(zoneNumBytes,zoneNum,0);
byte[] sendBytes = combineArrays(lenBytes,typeBytes,zoneNumBytes,data);
return sendBytes;
}
public byte[] subByte(byte[] b,int off,int length){
byte[] b1 = new byte[length];
System.arraycopy(b, off, b1, 0, length);
return b1;
}
}
- bodyString转bodyBytes
- subByte分割data,按SOCKET_SEND_SIZE偏移生成zoneBodyBytes
- packageData组装头部字段与body
- 发送zonePackData
2.4 iOS客户端接受数据
@implementation SocketReceiveModel
- (NSInteger)headerSize{
return 5;
}
- (void)praseHeader:(short*)_totalLen zoneNum:(short*)_zoneNum data:(NSData *)data{
if (data.length<[self headerSize]) {
return;
}
short totalLen = -1;
short zoneNum = -1;
NSRange range = NSMakeRange(0, sizeof(short));
[data getBytes:&totalLen range:range];
range = NSMakeRange(range.location+range.length, 1);
NSData * typeData = [data subdataWithRange:range];
NSString * typeString = [NSString stringWithUTF8String:[typeData bytes]];
range = NSMakeRange(range.location+range.length, 2);
[data getBytes:&zoneNum range:range];
* _totalLen = totalLen;
* _zoneNum = zoneNum;
}
- (void)parseBody:(NSData *)receiveData block:(void(^)(NSString * bodyString,NSError * error))block{
short totalLen = -1;
short zoneNum = -1;
[self praseHeader:&totalLen zoneNum:&zoneNum data:receiveData];
NSLog(@"socket 接受数据长度%lu,分%d片,接受数据分片%.2lu",(unsigned long)receiveData.length,zoneNum,receiveData.length/totalLen);
if (totalLen>0 && receiveData.length/totalLen >= zoneNum-1) {
NSMutableData * bodyData = [NSMutableData data];
int readLoc = 0;
while (readLoc<receiveData.length) {
NSRange range = NSMakeRange(readLoc, [self headerSize]);
[self praseHeader:&totalLen zoneNum:&zoneNum data:[receiveData subdataWithRange:range]];
range = NSMakeRange(NSMaxRange(range), totalLen-NSMaxRange(range));
[bodyData appendData:[receiveData subdataWithRange:range]];
readLoc+=totalLen;
}
NSLog(@"socket 解析数据receiveData = %ld,bodyData = %ld",receiveData.length,bodyData.length);
NSString *bodyString = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];
if (bodyString) {
block(bodyString,nil);
}else{
block(nil,[NSError errorWithDomain:@"parse error" code:-1 userInfo:nil]);
}
}
}
@end
- (void)receiveData:(NSData *)data{
[self.receiveData appendData:data];
SocketReceiveModel * receiveModel = [SocketReceiveModel new];
[receiveModel parseBody:self.receiveData block:^(NSString * _Nonnull bodyString, NSError * _Nonnull error) {
if (!error && bodyString.length>0) {
NSDictionary * messageDict = [self dictionaryWithJsonString:bodyString];
if (messageDict) {
[self receiveMessageDict:messageDict];
self.receiveData = [NSMutableData data];//解析完成,需要清理数据
}
}
}];
}
- 有个全局的receiveData接收数据
- 当receiveData.length/totalLen >= zoneNum-1时表示数据接收完成,完成后解析处理业务,并清空receiveData
3.总结
- iOS客户端,java后台一套完整的发送接收数据
- 基本功能完成,细节还需打磨。
- java传输byte[]时,不会按一片一片接收,有可能多片一起到达,所以不能按zoneNum去处理。