上一篇文章( < >)是我在写java服务端遇到的问题,本篇文章是完成了发送功能后写出来的。

首先上数据帧格式(帧格式来源:)

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

---------------------------------------------------------发送功能-------------------------------------------------------------------------------------

在上一篇文章中我深刻地认识了websocket数据帧的格式,还是从数据帧入手,我刚入门,就不写太复杂的,以免自己以后回头看也看不懂。只要懂得如何接收,就可以知道如何发送,接收数据是拆包嘛,那发送数据就要进行包装,

第一个字节是0x81(1000 0001)或者0x82(1000 0010),1是字符数据,2是二进制数据,如果单纯发送文字,就1,如果还要发送文件就2,8就没什么好说的,数据不分片(我也不知道要怎么分);

如下如下第二个字节是掩码一位+PayLoadLen,服务端发送给客户端的数据没有严格要求一定要掩码,那就设为0,PayLoadLen的值就等于数据的长度或者126(126<=数据长度<=0xff)或者127(0x100<=数据长度<=0xffffffff);

如果PayLoadLen的值等于数据的长度,那接下来从第三个字节开始就是数据部分PayLoadData;

那如果PayLoadLen的值等于126,则接下来第三,第四字节就是extendedPayLoadLen的值;

如果PayLoadLen的值等于127,则接下来第三到第十字节就是extendedPayLoadLen的值;在extendPayLoadLen后的字节就是PayLoadData。

因为发送的数据帧没有掩码,所以maskingkey4个字节也不用写。

代码如下:

public void sendMsg(String msg) throws IOException {
    byte[] data = msg.getBytes("UTF-8");
    int payLoadlen = data.length;
    byte[] first = new byte[1];
    first[0] = (byte) 0x81; //1000 0010 ,表示最后一个分片,数据为字符
    byte mask = 0; //先试试没有mask的情况,之后再试试有mask的情况
    byte[] second = new byte[1];
    byte[] extendedLoad = null;
    if (payLoadlen < 126){
        payLoadlen = payLoadlen; //脱裤子放屁
    }else if (payLoadlen < (0xffff)){
        //00000000 00000000 11111111 11111111
        extendedLoad = new byte[2];
        int index = 0;
        int rshift = 8;
        while(index < 2){
            extendedLoad[index++] = (byte) (payLoadlen>>rshift&0xff);
            rshift-=8;
        }
        payLoadlen = 126;
    }else{
        //范围:0x00010000 - 0x0fffffff,最高位为0
        //这里其实有一个问题,就是发送的数据理论上能达到2^64-1,
        // 但是就现实而言,能达到2^31-1(即int类型的最大值)都难
        // 因为数组length的返回值是int类型,最大也就32位了,所以,我只能将4个高位设置成全0)
        extendedLoad = new byte[8];
        int index = 0;
        while(index < 4){
            extendedLoad[index++] = 0;
        }
        int rshift = 24;
        while(index < 8){
            extendedLoad[index++] = (byte) (payLoadlen>>rshift&0xff);
            rshift-=8;
        }
        payLoadlen = 127;
    }
    second[0] = (byte) (payLoadlen&((mask<<8)+0x7f));
    byte[] msgbyte = null;
    if (extendedLoad == null){
        msgbyte = new byte[first.length+second.length+data.length];
        System.arraycopy(first,0,msgbyte,0,first.length);
        System.arraycopy(second,0,msgbyte,first.length,second.length);
        System.arraycopy(data,0,msgbyte,first.length+second.length,data.length);
    }else{
        msgbyte = new byte[first.length+second.length+extendedLoad.length+data.length];
        System.arraycopy(first,0,msgbyte,0,first.length);
        System.arraycopy(second,0,msgbyte,first.length,second.length);
        System.arraycopy(extendedLoad,0,msgbyte,first.length+second.length,extendedLoad.length);
        System.arraycopy(data,0,msgbyte,first.length+second.length+extendedLoad.length,data.length);
    }
    outs.write(msgbyte);
    outs.flush();
}

---------------------------------------------------------------------接收功能-------------------------------------------------------------------------

接收功能改动不大,只是把while里的代码写成一个方法,然后是payloadlen=127那里改成了实际上只有四个字节的数据,高位当作0读取,和上面发送功能的一样。

理由如下:

payloadlen=126时,最大的接收数据是0xffff,只有16位,2个字节,一位表示1字节的话也就是64KB,如果发送的文件稍大一点可能就不行了,所以127有必要用,但是127时最大的接收数据有64位,占数据帧8个字节,而java中int类型的数据占32位,4个字节,然后int类型的最大值是2147483647,能表示最大大约2G的数据长度,以我需求来说,4个字节的数据长度完全够用,就不用long类型了。发送和接收数据的时候应该是很少遇到超过int类型表示的范围的。

//接收数据的方法
public void recvMsg() throws IOException {
    byte[] first = new byte[1];
    ins.read(first); //将流中第一个字节的数据读入first
    int finflag = (first[0]&0x80)>>7; //终止位信息fin = (xyyyyyyyy & 10000000)>>7 = x,
    int opcode = first[0]&0x0f; //表示接收的信息类型opcode = yyyyxxxx & 0000ffff = 0000xxxx
    //opcode为0时是接收附加数据,1是文本数据,2是二进制流数据,8是关闭连接
    if (opcode == 8){
        System.out.println("连接被浏览器关闭");
        this.close();
        return ;
    }
    byte[] second = new byte[1];
    ins.read(second); //读入第二个字节
    int mask = (second[0]&0x80)>>7; //是否有掩码,从浏览器发送过来的信息一定有,mask = (xyyyyyyyy & 10000000)>>7 = x
    int payloadlen = second[0]&0x7f; // payloadlen = yxxxxxxxx & 011111111 = 0xxxxxxxx
    int extendPayloadLen = 0; //
    if (payloadlen == 126){
        // 若payloadlen == 126,则接收的数据长度为后两个字节的值,
        int index = 0;
        byte[] extended = new byte[2];//附加长度,下同
        ins.read(extended);
        while(index < 2){
            extendPayloadLen = extendPayloadLen<<8;
            extendPayloadLen += (extended[index++]&0xff);
        }
        payloadlen = extendPayloadLen;
    }else if(payloadlen == 127){
        // 若payloadlen == 127,则接收的数据长度为后八个字节的值。
        int index = 0;
        byte[] extended = new byte[8];
        ins.read(extended);
        while (index < 4){
            index++;
        }
        while(index < 8){
            extendPayloadLen = extendPayloadLen<<8;
            extendPayloadLen += (extended[index++]&0xff);
        }
        payloadlen = extendPayloadLen;
    }else{
        // 若payloadlen < 126,接收的数据长度为0~125,
        payloadlen = payloadlen;
    }
    byte[] maskingkey = new byte[4];
    ins.read(maskingkey); //读入maskingkey
    byte[] themsg = new byte[payloadlen]; //假设数据段最多收1024字节
    //bains.read(themsg,0,payloadlen); //从themsg的0下标出发,读入payloadlen字节
    ins.read(themsg,0,payloadlen);
    int index = 0;
    if (mask == 1){
        while(index < payloadlen){
            themsg[index] = (byte) ((themsg[index]^maskingkey[index%4])&0xff);
            index++;
        }
    }
    System.arraycopy(themsg,0,themsg,0,payloadlen);
    String msg = new String(themsg,"UTF-8");
    System.out.println(msg);
}

然后既然websocket是全双工,那我就把两个功能合起来测试,但是不论是inputstream的read还是scanner.next都会阻塞进程,所以我就写了一个给接收数据专用的线程,发送数据就还是主线程。

运行结果:

HandShake OK
connect success
hello peter
hi?do i know u?
hi?do
i
know
u?

这里说明一下:‘hello peter‘是浏览器输入框输入后发送的;‘hi?do i know u?’是在idea运行的终端输入的,空格就被当作分隔符,下面的分段是浏览器返回的,js里面设置了把接收到的数据发送回去。