iOS客户端与java后台 socket通信

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
  1. packageData根据bodyData分成zoneNum片,把每个段添加到zoneArray数组中
  2. 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(@"发送数据,请等待发送结果");
}
  1. sendDataWithSocketModel发送数据:遍历pageDataArray,进行写入即可。
  2. 使用时只需要构造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();
    }
}
  1. 定义一个全局的bodyBytes
  2. 每接受一个新的分片,combineArrays把buf追加到bodyBytes
  3. 当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;
    }

}
  1. bodyString转bodyBytes
  2. subByte分割data,按SOCKET_SEND_SIZE偏移生成zoneBodyBytes
  3. packageData组装头部字段与body
  4. 发送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];//解析完成,需要清理数据
            }
        }
    }];
}
  1. 有个全局的receiveData接收数据
  2. 当receiveData.length/totalLen >= zoneNum-1时表示数据接收完成,完成后解析处理业务,并清空receiveData

3.总结

  1. iOS客户端,java后台一套完整的发送接收数据
  2. 基本功能完成,细节还需打磨。
  3. java传输byte[]时,不会按一片一片接收,有可能多片一起到达,所以不能按zoneNum去处理。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343