上一篇文章( <https://www.cnblogs.com/thankvincisdaily/p/16009535.html >)是我在写java服务端遇到的问题,本篇文章是完成了发送功能后写出来的。
首先上数据帧格式(帧格式来源:https://www.cnblogs.com/laohaozi/p/12537571.html)
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里面设置了把接收到的数据发送回去。