WebSocket 服务端实现

142 篇文章 0 订阅
113 篇文章 0 订阅

    WebSocket,并非HTML 5独有,WebSocket是一种协议。只是在handshake的时候,发送的链接信息头和HTTP相似。HTML 5只是实现了WebSocket的客户端。其实,难点在于服务端,服务端相对还是比较复杂的。

 

        网上demo很多,但是能讲到点上的不多,而且也不知道作者有没有深入试验过。WebSokect协议 ,handshake这part其实还是比较简单的,比较复杂的还是数据传输(第二部分)比较难。

 

        上图是帧格式,对于解释可以看文档也可以查看文章:http://www.cnblogs.com/imayi/archive/2012/05/05/2485343.html


        也就说,你需要解读opcode, Payload len 这些比较敏感的位值之后,你才开始取后面的Payload Data, 比如opcode如果是1,那么就是读取字符串,如果是2,那么就是读取流,如果是8,那么就是关闭socket。

        如果自己用java做服务端,握手协议的响应,换行符不能使“\r\n”或“\n”,因为这不是标准的换行符,其实是个字符串,只是屏幕显示的时候是换行,可以用PrintWrite.println(),或者

String newLine = (String) java.security.AccessController.doPrivileged(

               new sun.security.action.GetPropertyAction("line.separator"));

        握手之后,根据opcode做相应的操作,JAVA的IO和NIO,在实现方面都缺憾,难以实现。使用IO的话,在获得socket之后,InputStream会处于阻塞,因为阻塞,所以后续做outputStream的操作时就会不方便。NIO的话,是SocketChannel写入读出,基本都是ByteBuffer,用这个的话,有时opcode值为1(读取字符串),ByteBuffer解码得到的字符串经常性是乱码(这个本人没有深究)。感觉

    Tomcat在7.0.27之后就开始支持WebSocket,在它之上建立WebSocket是很简单的,基本就是继承WebSocketServlet,实现createWebSocketInbound方法和重写StreamInbound的方法。Tomcat的example里面有相关的例子。

    ######如果已经知道tomcat的运行流程那么就略过这一段#####

      tomcat的源代码研究,网上挺多的。个人就看过王程斯的Tomcat源码学习

下面引用一下他的插图:

    大体来讲就是通过监听几个端口,运用线程池处理Socket,之后adapter打包数据给容器。

    下面罗嗦一下:

    tomcat启动时,会启用几个AcceptorThread 监控端口,JIoEndpointAcceptor - Acceptor(线程),将接收到的socket放入  JIoEndpointAcceptor - processSocket 处理。processSocket 里面 socket被打包之后SocketWrapper放入JIoEndpointAcceptor - SocketProcessor(线程)处理  ( SSL handshake在这里做处理)。SocketProcessor线程里面,socket被交给 AbstractProtocol  process方法进行处理处理的过程,创建Http11Processor,Http11Protocol - Http11ConnectionHandler - createProcessorprocessor之后会被注册,Http11Processor  register(processor);被注册缓存起来,以便其他socket过来可以沿用(对了,Http11Processor  继承于 AbstractHttp11Processor。继续,socket被Http11Processor  - process(SocketWrapper<S> socketWrapper) 处理生成Request对象,要知道tomcat为我们做了封装,我们的输入输出都只需要调用Request和Response。Http11Processor  的process都做了什么呢??getInputBuffer().init(socketWrapper, endpoint);InternalInputBuffer  获得socket的inputStream,getOutputBuffer().init(socketWrapper, endpoint);InternalOutputBuffer  获得socket的outputStream。读取inputStream的值到buf里面,prepareRequest   将值包装到Request。

######################end

        好吧,很乱,就着源代码画着图就比较容易理解。而且这个只是Http11的普通IO处理流程,tomcat还能处理ajp协议。而且对于HTTP11还有另一套的NIO处理流程。

        将上面的流程,主要是因为:WebSocket启用的是JIOEndpoint,而不是JNIOEndpoint等。

        每次交互都会由Http11Processor  process处理,如果处理的时候返回的状态是upgrade,也就是http升级协议(websocket协议)。那么tomcat就会封装出一个UpgradeInbound 。

?
1
2
3
4
5
6
UpgradeInbound inbound = processor.getUpgradeInbound();
// Release the Http11 processor to be re-used
release(socket, processor, false , false );
// Create the light-weight upgrade processor
processor = createUpgradeProcessor(socket, inbound);
inbound.onUpgradeComplete();

之后就会开始握手:

?
1
state = processor.upgradeDispatch();

        这些过程还会创建WsOutbound 和 StreamInbound、WsInputStream(继承inputStream),很明显,Ws就是websocket的意思,这些输入流和输出流都是WebSocket协议定制的。WsOutbound负责输出结果,StreamInbound和WsInputStream负责接收数据,  tomcat对字节和字符串处理是不一样的。
涉及到的对象(org.apache.catalina):

        WebSokect协议有70页左右,没有深入的看下去,太煎熬了(希望有牛人,可以翻译一下,造福人类)。看了些协议,又看了tomcat的WebSocket实现。现在就是贴自己做的demo(demo很简单,请别喷):

客户端:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<! DOCTYPE HTML>
< html >
< head >
< title >Web Socket Demo --  EchoClient</ title >
< meta charset = "utf-8" >
</ head >
< script type = "text/javascript" >
     var websocket = null;
     function connect(){
         var msg = document.getElementById("msg");
         try{
             var readyState = new Array("正在连接", "已建立连接", "正在关闭连接"
                         , "已关闭连接");
             var host = "ws://localhost:8000";
             websocket = new WebSocket(host);
             websocket.onopen = function(){
                 msg.innerHTML += "< p >Socket状态: " + readyState[websocket.readyState] + "</ p >";
             }
             websocket.onmessage = function(event){
                 msg.innerHTML += "< p >接收信息: " + event.data + "</ p >";
             }
             websocket.onclose = function(){
                 msg.innerHTML += "< p >Socket状态: " + readyState[websocket.readyState] + "</ p >";
             }
             msg = document.getElementById("msg");
             msg.innerHTML += "< p >Socket状态: " + readyState[websocket.readyState] + "</ p >";
         }catch(exception){
             msg.innerHTML += "< p >有错误发生</ p >";
         }
     }
 
     function send(){
         var msg = document.getElementById("msg");
         var text = document.getElementById("text").value;
         if(text == ""){
             msg.innerHTML += "< p >请输入一些文字</ p >";
             return;
         }
         try{
             websocket.send(text);
             msg.innerHTML += "< p >发送数据:  " + text + "</ p >";
         }catch(exception){
             msg.innerHTML += "< p >发送数据出错</ p >";
         }
         document.getElementById("text").value = "";
     }
 
     function disconnect(){
         websocket.close();
     }
</ script >
< body >
     < h1 >WebSocket客户端实例</ h1 >
     < div id = "msg" style = "height: 300px;" ></ div >
     < p >请输入一些文字</ p >
     < input type = "text" id = "text" />
     < button id = "connect" onclick = "connect();" >建立连接</ button >
     < button id = "send" onclick = "send();" >发送数据</ button >
     < button id = "disconnect" onclick = "disconnect();" >断开连接</ button >
</ body >
</ html >

略过,不说,哪哪都有这部分的代码。

服务端:

         上面说了,服务端是挺难的。

1、处理握手,本人使用InputStream.read(byte[], off, len)方法读取了字节之后再处理。

2、使用InputStream.read()方法逐一读取字节,解析FIN,opcode,PayloadLen,mask(掩码,根据websocket协议,客户端传过来的数据必须通过掩码计算再传输)等信息,当然rsv在我的demo没有,没用上。

3、知道数据长度了(PS:JAVA的UTF8里面中文可能占用3-4个字节),读取数据,然后通过解码获取真正的byte值。

4、输出结果,结果无需掩码,但是也有格式要求,看协议,懒得看,就看tomcat源码。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package socket;
 
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.MessageDigest;
 
 
/*
  * 垃圾程序,只求速成,没有效率,复用这个概念,望谅解
  * */
public class EchoServer {
     private int port = 8000 ;
     private ServerSocket serverSocket;
 
     public EchoServer() throws IOException {
         serverSocket = new ServerSocket(port);
         System.out.println( "服务器启动" );
     }
 
     private void service() {
         Socket socket = null ;
         while ( true ) {
             try {
                 socket = serverSocket.accept();
                 Thread workThread = new Thread( new Handler(socket));
                 workThread.start();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
     }
 
     class Handler implements Runnable {
         private Socket socket;
         private boolean hasHandshake = false ;
         Charset charset = Charset.forName( "UTF-8" ); 
         
         public Handler(Socket socket) {
             this .socket = socket;
         }
 
         private PrintWriter getWriter(Socket socket) throws IOException {
             OutputStream socketOut = socket.getOutputStream();
             return new PrintWriter(socketOut, true );
         }
 
 
         public String echo(String msg) {
             return "echo:" + msg;
         }
 
         public void run() {
             
             try {
                 System.out.println( "New connection accepted"
                         + socket.getInetAddress() + ":" + socket.getPort());
                 InputStream in = socket.getInputStream();
                 
                 PrintWriter pw = getWriter(socket);
                 //读入缓存
                 byte [] buf = new byte [ 1024 ];
                 //读到字节
                 int len = in.read(buf, 0 , 1024 );
                 //读到字节数组
                 byte [] res = new byte [len];
                 System.arraycopy(buf, 0 , res, 0 , len);
                 String key = new String(res);
                 if (!hasHandshake && key.indexOf( "Key" ) > 0 ){
                     //握手
                     key = key.substring( 0 , key.indexOf( "==" ) + 2 );
                     key = key.substring(key.indexOf( "Key" ) + 4 , key.length()).trim();
                     key+= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ;
                     MessageDigest md = MessageDigest.getInstance( "SHA-1" ); 
                     md.update(key.getBytes( "utf-8" ), 0 , key.length());
                     byte [] sha1Hash = md.digest(); 
                        sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder(); 
                     key = encoder.encode(sha1Hash); 
                     pw.println( "HTTP/1.1 101 Switching Protocols" );
                     pw.println( "Upgrade: websocket" );
                     pw.println( "Connection: Upgrade" );
                     pw.println( "Sec-WebSocket-Accept: " + key);
                     pw.println();
                     pw.flush();
                     hasHandshake = true ;
                     
                     //接收数据
                     byte [] first = new byte [ 1 ];
                     //这里会阻塞
                        int read = in.read(first, 0 , 1 );
                        while (read > 0 ){
                             int b = first[ 0 ] & 0xFF ;
                             //1为字符数据,8为关闭socket
                            byte opCode = ( byte ) (b & 0x0F );
                         
                            if (opCode == 8 ){
                             socket.getOutputStream().close();
                             break ;
                            }
                            b = in.read();
                            int payloadLength = b & 0x7F ;
                            if (payloadLength == 126 ) {
                                byte [] extended = new byte [ 2 ];
                                in.read(extended, 0 , 2 );
                                int shift = 0 ;
                                payloadLength = 0 ;
                                for ( int i = extended.length - 1 ; i >= 0 ; i--) {
                                    payloadLength = payloadLength + ((extended[i] & 0xFF ) << shift);
                                    shift += 8 ;
                                }
 
                             } else if (payloadLength == 127 ) {
                                byte [] extended = new byte [ 8 ];
                                in.read(extended, 0 , 8 );
                                int shift = 0 ;
                                payloadLength = 0 ;
                                for ( int i = extended.length - 1 ; i >= 0 ; i--) {
                                    payloadLength = payloadLength + ((extended[i] & 0xFF ) << shift);
                                    shift += 8 ;
                                }
                            }
                         
                         //掩码
                         byte [] mask = new byte [ 4 ];
                         in.read(mask, 0 , 4 );
                         int readThisFragment = 1 ;
                         ByteBuffer byteBuf = ByteBuffer.allocate(payloadLength + 10 );
                         byteBuf.put( "echo: " .getBytes( "UTF-8" ));
                         while (payloadLength > 0 ){
                              int masked = in.read();
                              masked = masked ^ (mask[( int ) ((readThisFragment - 1 ) % 4 )] & 0xFF );
                              byteBuf.put(( byte ) masked);
                              payloadLength--;
                              readThisFragment++;
                         }
                         byteBuf.flip();
                         responseClient(byteBuf, true );
                         printRes(byteBuf.array());
                         in.read(first, 0 , 1 );
                     }
                     
                 }
                 in.close();
             } catch (Exception e) {
                 e.printStackTrace();
             } finally {
                 try {
                     if (socket != null )
                         socket.close();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
 
         private void responseClient(ByteBuffer byteBuf, boolean finalFragment) throws IOException {
             OutputStream out = socket.getOutputStream();
             int first = 0x00 ;
             //是否是输出最后的WebSocket响应片段
                if (finalFragment) {
                    first = first + 0x80 ;
                    first = first + 0x1 ;
                }
                out.write(first);
             
 
                if (byteBuf.limit() < 126 ) {
                    out.write(byteBuf.limit());
                } else if (byteBuf.limit() < 65536 ) {
                 out.write( 126 );
                 out.write(byteBuf.limit() >>> 8 );
                 out.write(byteBuf.limit() & 0xFF );
                } else {
                 // Will never be more than 2^31-1
                 out.write( 127 );
                 out.write( 0 );
                 out.write( 0 );
                 out.write( 0 );
                 out.write( 0 );
                 out.write(byteBuf.limit() >>> 24 );
                 out.write(byteBuf.limit() >>> 16 );
                 out.write(byteBuf.limit() >>> 8 );
                 out.write(byteBuf.limit() & 0xFF );
 
                }
 
                // Write the content
                out.write(byteBuf.array(), 0 , byteBuf.limit());
                out.flush();
         }
 
         
         private void printRes( byte [] array) {
             ByteArrayInputStream  byteIn = new ByteArrayInputStream(array);
             InputStreamReader reader = new InputStreamReader(byteIn, charset.newDecoder());
             int b = 0 ;
             String res = "" ;
             try {
                 while ((b = reader.read()) > 0 ){
                     res += ( char )b;
                 }
             } catch (IOException e) {
                 e.printStackTrace();
             }
             System.out.println(res);
         }
     }
 
     public static void main(String[] args) throws IOException {
         new EchoServer().service();
     }
}

        本文出自:      http://my.oschina.net/u/590484/blog/71797

                                                                                                                        by lin_bobo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值