美文网首页
iOS客户端与java后台 socket通信

iOS客户端与java后台 socket通信

作者: 某非著名程序员 | 来源:发表于2020-05-12 20:47 被阅读0次

    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去处理。

    相关文章

      网友评论

          本文标题:iOS客户端与java后台 socket通信

          本文链接:https://www.haomeiwen.com/subject/gqkgthtx.html