上一篇文章( <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里面設置了把接收到的數據發送回去。