高端大气上档次的网页PPT:http://www.ipresst.com/play/52df66bb1f3a3b3448003c67
1.很多人写了多年代码,从未接触过Socket
2.很多人做了多年Web,从未了解过HTTP协议
3.很多人看过HTML5规范,从未使用过WebSocket
Java Aio , JDK1.7之后新出的一个重量级API,可以看作是JDK1.4 NIO 之后的一次升级,但相比NIO 的Reactor模型,AIO的Proactor模型优势在于当操作系统内核处于可读或则可写状态的时候,操作系统会主动通知应用程序,所以AIO使用起来比NIO就简单很多。
HTTP 协议,如果问起来,很多人一致的回答都是基于TCP的文本传输协议,但是真正了解它如何工作的人少之有少,什么是包头,什么是包体,分割符号是什么,程序如何才能够有条不紊地处理,什么是Keep-Alive,什么是长轮询,WebSocket的内部原理是什么。或许很多人看过相关书籍,但却止步于书籍,Tomcat、Netty、Jetty的源码又相当复杂,难于记忆。TCP协议、HTTP协议的一时半会儿说不清,所以就跟随我一步一步实现一个轻量的http-aio-server,相信大家很快就能够明白它们原理。
Server的目标1:实现常用的GET/POST请求方法,GET方法支持URL参数,POST方法支持application/x-www-form-urlencoded、multipart/form-data和流式上传,再就是全双工WebSocket协议封装。
Server的目标2:基于Spring的开发,并且实现类似Spring MVC的Controller开发。
源代码已上传,详细请见Maven项目源码,test目录的Bootstrap可以直接启动
下面是精简后的源码,简单过目后便可大致了解AIO处理流程
/**
* @author fangjialong
* @description 服务器启动类,主要作用是创建线程池,绑定端口,开始接受Socket请求,该代码是精简后的代码,详细请下载源码
*/
public class HttpServer {
private ExecutorService channelWorkers;
private ExecutorService processWorkers;
private AsynchronousChannelGroup workerGroup = null;
private AsynchronousServerSocketChannel serverSocket = null;
private SocketAcceptHandler socketAcceptHandler;
public synchronized void startup() throws IOException {
//根据硬件环境创建线程池
int availableProcessors = Runtime.getRuntime().availableProcessors();
channelWorkers = Executors.newFixedThreadPool(availableProcessors+1,new ProcessorThreadFactory());
workerGroup = AsynchronousChannelGroup.withCachedThreadPool(channelWorkers, 1);
serverSocket = AsynchronousServerSocketChannel.open(workerGroup);
//绑定服务器端口
serverSocket.bind(new InetSocketAddress(80), 100);
//开始接收请求,并且传入异步回调
serverSocket.accept(null, socketAcceptHandler);
}
}
下面是接受到请求后创建套接字会话,创建会话是为了方便编解码,和等待接受下次HTTP请求。
/**
* @author cannonfang
* @name 房佳龙
* @date 2014-1-9
* @qq 271398203
* @todo 该类用于接受客户端TCP连接,如果有一个新的TCP连接,该类的completed函数将会被调用
*/
@Component
public class SocketAcceptHandler implements CompletionHandler<AsynchronousSocketChannel,Object>{
private static final Logger logger = LoggerFactory.getLogger(SocketAcceptHandler.class);
private HttpServer server;
public void setServer(HttpServer server) {
this.server = server;
}
@Override
public void completed(AsynchronousSocketChannel socket,
Object obj) {
try{
SocketSession session = new SocketSession(socket,server);
// logger.debug("Socket Session({}) Create",session.hashCode());
session.read();
}catch(Throwable t){
logger.error(t.getMessage(),t);
try {
socket.close();
} catch (IOException e) {}
}finally{
server.accept();
}
}
@Override
public void failed(Throwable t, Object obj) {
server.accept();
}
}
HTTP 编码和解码对象,HTTP请求的解析总共分为三部,读取第一行,通过该行可以判断HTTP请求方法,版本和请求URI,每行以回车(ASCII:13)换行(ASCII:10)分割;然后读取HTTP Header , 每行以分好(:)和一个空白符作为分割组成Key-Value,已两个回车换行作为头部读取完毕;最后判断HTTP Header 中是否存在Content-Length头,如果存在,则通过Value中的数字作为长度继续向后读取作为包体,如果Content-Length: 1024, 那么就应该继续读取1024个字节作为包体。下面的代码部分来至Netty源码,相信阅读过Netty源码的同学会再这发现曾经的影子。
/**
* @author cannonfang
* @name 房佳龙
* @date 2014-1-13
* @qq 271398203
* @todo HTTP Response Encode And Decode Class
*/
@Component
public class HttpMessageSerializer implements InitializingBean{
protected Logger logger = LoggerFactory.getLogger(HttpMessageSerializer.class);
private int maxInitialLineLength = 1024*2; //Default 2KB
private int maxHeaderSize = 1024*4; //Default 4KB
private int maxContextSize = 1024*1024*5 ;//Default 5MB
private String charset = "UTF-8";
private String dynamicSuffix;
private String defaultIndex;
private ServerConfig serverConfig;
@Autowired
public void setServerConfig(ServerConfig serverConfig) {
this.serverConfig = serverConfig;
}
@Override
public void afterPropertiesSet() throws Exception {
this.charset = serverConfig.getString("server.http.charset", charset);
logger.info("server.http.charset : {}",charset);
this.maxHeaderSize = serverConfig.getBytesLength("server.http.maxHeaderSize", this.maxHeaderSize);
logger.info("server.http.maxHeaderSize : {}",maxHeaderSize);
this.maxContextSize = serverConfig.getBytesLength("server.http.maxContextSize", this.maxContextSize);
logger.info("server.http.maxContextSize : {}",maxContextSize);
this.dynamicSuffix = serverConfig.getString("server.http.dynamic.suffix", ".do");
logger.info("server.http.dynamic.suffix : {}",dynamicSuffix);
this.defaultIndex = serverConfig.getString("server.http.index", ".html");
logger.info("server.http.dynamic.suffix : {}",this.defaultIndex);
}
public boolean decode(ByteBuffer buffer,HttpProcessor processor)throws Exception{
boolean finished = false;
DefaultHttpRequest request = null;
try{
buffer.flip();
HttpSocketStatus status = processor.getSocketStatus();
request = processor.getRequest();
switch(status){
case SKIP_CONTROL_CHARS: {
skipControlCharacters(buffer);
processor.setSocketStatus(HttpSocketStatus.READ_INITIAL);
}
case READ_INITIAL:{
String line = readLine(buffer,maxInitialLineLength);
if(line==null){
break;
}
String[] initialLine = splitInitialLine(line);
String text = initialLine[0].toUpperCase();
HttpMethod method = HttpMethod.getHttpMethod(text);
if(method==null){
throw new HttpException(HttpResponseStatus.METHOD_NOT_ALLOWED, "Unsuported HTTP Method "+text);
}
String uri = initialLine[1];
text = initialLine[2].toUpperCase();
HttpVersion version;
if (text.equals("HTTP/1.1")) {
version=HttpVersion.HTTP_1_1;
}else if (text.equals("HTTP/1.0")) {
version=HttpVersion.HTTP_1_0;
}else{
throw new HttpException(HttpResponseStatus.BAD_REQUEST,"Unsuported HTTP Protocol "+text);
}
request = new DefaultHttpRequest(version,method,uri);
request.setCharacterEncoding(charset);
int at = uri.indexOf('?');
String queryString ;
if(at>=0){
queryString = uri.substring(0, at);
}else{
queryString = uri;
}
if(queryString.endsWith("/")){
queryString = queryString+this.defaultIndex;
request.setQueryString(queryString);
}else{
request.setQueryString(queryString);
}
if(queryString.endsWith(this.dynamicSuffix)){
request.setDynamic(true);
if(at>0){
String params = uri.substring(at);
request.decodeContentAsURL(params,charset);
}
}else{
request.setDynamic(false);
}
// logger.debug("Socket Session({}) : {}",processor.hashCode(),queryString);
processor.setRequest(request);
processor.setSocketStatus(HttpSocketStatus.READ_HEADER);
}
case READ_HEADER:{
if(!readHeaders(buffer,request)){
break;
}
long contentLength = HttpHeaders.getContentLength(request, -1);
if(request.isDynamic()){
if(contentLength>0){
if(contentLength>this.maxContextSize){
throw new HttpException(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, "Request Entity Too Large : "+contentLength);
}
try {
request.createContentBuffer((int)contentLength,request.getHeader(HttpHeaders.Names.CONTENT_TYPE));
} catch (IOException e) {
logger.info(e.getMessage(),e);
throw new HttpException(HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
processor.setSocketStatus(HttpSocketStatus.READ_VARIABLE_LENGTH_CONTENT);
}else{
processor.setSocketStatus(HttpSocketStatus.RUNNING);
finished=true;
break;
}
}else{
if(contentLength>0){
throw new HttpException(HttpResponseStatus.BAD_REQUEST,"Http Static Request Do Not Suport Content Length : " + contentLength);
}else{
processor.setSocketStatus(HttpSocketStatus.RUNNING);
finished=true;
break;
}
}
}
case READ_VARIABLE_LENGTH_CONTENT:{
try {
if(request.readContentBuffer(buffer)){
processor.setSocketStatus(HttpSocketStatus.RUNNING);
finished=true;
}
} catch (IOException e) {
logger.info(e.getMessage(),e);
throw new HttpException(HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
break;
}
default:throw new HttpException(HttpResponseStatus.BAD_REQUEST,"Error Scoket Status : " + status);
}
}catch(Exception e){
if(request!=null){
request.destroy();
}
throw e;
}finally{
if(buffer!=null){
buffer.compact();
}
}
return finished;
}
public void encodeInitialLine(ByteBuffer buffer,HttpResponse response) throws IOException{
byte[] bytes = response.getProtocolVersion().toString().getBytes(charset);
buffer.put(bytes);
buffer.put(HttpCodecUtil.SP);
buffer.put(response.getStatus().getBytes());
buffer.put(HttpCodecUtil.CRLF);
}
public void encodeHeaders(ByteBuffer buffer,HttpResponse response,SocketSession session) throws IOException, InterruptedException, ExecutionException {
int remaining = buffer.remaining();
for(Entry<String,String> header : response.getHeaders()){
byte[] key = header.getKey().getBytes(charset);
byte[] value = header.getValue().getBytes(charset);
remaining-=key.length+value.length+3;
if(remaining<=0){
buffer.flip();
session.write(buffer).get();
remaining = buffer.remaining();
buffer.compact();
}
buffer.put(key);
buffer.put(HttpCodecUtil.COLON_SP);
buffer.put(value);
buffer.put(HttpCodecUtil.CRLF);
}
if(remaining<=0){
session.write(buffer).get();
}
buffer.put(HttpCodecUtil.CRLF);
}
public String getCharset() {
return charset;
}
private boolean readHeaders(ByteBuffer buffer,HttpRequest request) throws HttpException {
StringBuilder sb = new StringBuilder(64);
int limit = buffer.limit();
int position = buffer.position();
int lineLength = 0;
for(int index=position;index<limit;index++){
byte nextByte = buffer.get(index);
if (nextByte == HttpConstants.CR) {
nextByte = buffer.get(index+1);
if (nextByte == HttpConstants.LF) {
buffer.position(index);
if(lineLength==0){
buffer.position(index+2);
return true;
}else{
buffer.position(index);
}
readHeader(request,sb.toString());
lineLength=0;
sb.setLength(0);
index++;
}
}else if (nextByte == HttpConstants.LF) {
if(lineLength==0){
buffer.position(index+2);
return true;
}else{
buffer.position(index);
}
readHeader(request,sb.toString());
lineLength=0;
sb.setLength(0);
index++;
}else{
if (lineLength >= maxHeaderSize) {
throw new HttpException(HttpResponseStatus.BAD_REQUEST,"An HTTP header is larger than " + maxHeaderSize +" bytes.");
}
lineLength ++;
sb.append((char) nextByte);
}
}
return false;
}
private static void readHeader(HttpRequest request,String header){
String[] kv = splitHeader(header);
request.addHeader(kv[0], kv[1]);
}
private static String[] splitHeader(String sb) {
final int length = sb.length();
int nameStart;
int nameEnd;
int colonEnd;
int valueStart;
int valueEnd;
nameStart = findNonWhitespace(sb, 0);
for (nameEnd = nameStart; nameEnd < length; nameEnd ++) {
char ch = sb.charAt(nameEnd);
if (ch == ':' || Character.isWhitespace(ch)) {
break;
}
}
for (colonEnd = nameEnd; colonEnd < length; colonEnd ++) {
if (sb.charAt(colonEnd) == ':') {
colonEnd ++;
break;
}
}
valueStart = findNonWhitespace(sb, colonEnd);
if (valueStart == length) {
return new String[] {
sb.substring(nameStart, nameEnd),
""
};
}
valueEnd = findEndOfString(sb);
return new String[] {
sb.substring(nameStart, nameEnd),
sb.substring(valueStart, valueEnd)
};
}
private static String readLine(ByteBuffer buffer, int maxLineLength) throws HttpException {
StringBuilder sb = new StringBuilder(64);
int lineLength = 0;
int limit = buffer.limit();
int position = buffer.position();
for(int index=position;index<limit;index++){
byte nextByte = buffer.get(index);
if (nextByte == HttpConstants.CR) {
nextByte = buffer.get(index+1);
if (nextByte == HttpConstants.LF) {
buffer.position(index+2);
return sb.toString();
}
}else if (nextByte == HttpConstants.LF) {
buffer.position(index+2);
return sb.toString();
}else{
if (lineLength >= maxLineLength) {
throw new HttpException(HttpResponseStatus.REQUEST_URI_TOO_LONG,"An HTTP line is larger than " + maxLineLength +" bytes.");
}
lineLength ++;
sb.append((char) nextByte);
}
}
return null;
}
private static String[] splitInitialLine(String sb) {
int aStart;
int aEnd;
int bStart;
int bEnd;
int cStart;
int cEnd;
aStart = findNonWhitespace(sb, 0);
aEnd = findWhitespace(sb, aStart);
bStart = findNonWhitespace(sb, aEnd);
bEnd = findWhitespace(sb, bStart);
cStart = findNonWhitespace(sb, bEnd);
cEnd = findEndOfString(sb);
return new String[] {
sb.substring(aStart, aEnd),
sb.substring(bStart, bEnd),
cStart < cEnd? sb.substring(cStart, cEnd) : "" };
}
private static int findNonWhitespace(String sb, int offset) {
int result;
for (result = offset; result < sb.length(); result ++) {
if (!Character.isWhitespace(sb.charAt(result))) {
break;
}
}
return result;
}
private static int findWhitespace(String sb, int offset) {
int result;
for (result = offset; result < sb.length(); result ++) {
if (Character.isWhitespace(sb.charAt(result))) {
break;
}
}
return result;
}
private static int findEndOfString(String sb) {
int result;
for (result = sb.length(); result > 0; result --) {
if (!Character.isWhitespace(sb.charAt(result - 1))) {
break;
}
}
return result;
}
private static void skipControlCharacters(ByteBuffer buffer) {
int limit = buffer.limit();
int position = buffer.position();
for(int index=position;index<limit;index++){
char c = (char) (buffer.get(index) & 0xFF);
if (!Character.isISOControl(c) &&
!Character.isWhitespace(c)) {
buffer.position(index);
break;
}
}
}
}
接下来是模拟Spring MVC的控制器,或许有人会问为什么不能直接用Spring MVC,因为Spring MVC实现的基础是在Servlet.api之上的.也就是可以反射传入HttpServletRequest接口实现类,但是在这而没有完全实现Servlet规范,所以无法与Spring MVC进行适配。但是其原理其实非常简单,就是通过Java自带的动态反射,或许会有人会说反射影响性能,其实解决这个问题不也不难,反射性能损耗最大的是通过类获取到Method对象,服务器启动的时候便可以对所有标记过Controller注解的类进行Method缓存,请求到来时只需要Invoke即可,此处几乎无性能损耗。为了实现Spring MVC的参数反转,在这里也对每种基本类型、包装类型、集合类型都做了判断,并且协助解析,达到Spring MVC最常用的参数自动解析的作用。
public final class MethodHandler {
private static final Logger logger = LoggerFactory.getLogger(MethodHandler.class);
private final Object object;
private final Method method;
private final boolean responseBody;
private final boolean xssFilter;
private RequestParamType[] requestParamTypes;
private int parameterLength;
private ObjectMapper objectMapper;
private boolean matcherHandler = false;
private Pattern pattern;
private String[] keys;
public MethodHandler(Object object,Method method){
this.object = object;
this.method = method;
this.responseBody=method.isAnnotationPresent(ResponseBody.class);
XssFilter filter = method.getAnnotation(XssFilter.class);
this.xssFilter = filter==null?true:filter.value();
}
public Pattern getPattern() {
return pattern;
}
public String[] getKeys() {
return keys;
}
public boolean isMatcherHandler() {
return matcherHandler;
}
void setPathPattern(Pattern pattern,String[] keys) {
this.pattern = pattern;
this.matcherHandler = true;
this.keys = keys;
}
void setObjectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public Method getMethod() {
return method;
}
public Object getObject() {
return object;
}
void setParameterTypes(Class<?> clazz,Method method) {
Class<?>[] parameterTypes = method.getParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
this.parameterLength = parameterTypes.length;
this.requestParamTypes = new RequestParamType[this.parameterLength];
for(int i=0;i<this.parameterLength;i++){
Class<?> classType = parameterTypes[i];
RequestParamType paramType = new RequestParamType();
Type type;
if(classType==HttpRequest.class){
type = Type.HTTP_REQUEST;
}else if(classType==HttpResponse.class){
type = Type.HTTP_RESPONSE;
}else{
if(classType==String.class){
type = Type.STRING;
}else if(classType.isAssignableFrom(List.class)){
type = Type.LIST;
}else if(classType.isAssignableFrom(Set.class)){
type = Type.SET;
}else if(classType.isAssignableFrom(Map.class)){
type = Type.MAP;
}else if(classType.isArray()){
type = Type.ARRARY;
}else if(classType==Boolean.class||classType==boolean.class){
type = Type.BOOLEAN;
}else if(classType==Short.class||classType==short.class){
type = Type.SHORT;
}else if(classType==Integer.class||classType==int.class){
type = Type.INTEGER;
}else if(classType==Long.class||classType==long.class){
type = Type.LONG;
}else if(classType==Float.class||classType==float.class){
type = Type.FLOAT;
}else if(classType==Double.class||classType==double.class){
type = Type.DOUBLE;
}else if(classType==Character.class||classType==char.class){
type = Type.CHAR;
}else if(classType==Byte.class||classType==byte.class){
type = Type.BYTE;
}else{
throw new RuntimeException(clazz.getSimpleName()+"."+method.getName()+" param["+i+"] is not suported to request params ioc");
}
Annotation[] annotations = parameterAnnotations[i];
RequestParam requestParam = null;
PathVariable pathVariable = null;
for(Annotation annotation:annotations){
if(annotation instanceof RequestParam){
requestParam = (RequestParam)annotation;
paramType.setName(requestParam.value());
if(!requestParam.defaultValue().equals(ValueConstants.DEFAULT_NONE)){
paramType.setDefaultValue(requestParam.defaultValue());
paramType.setRequired(false);
}else{
paramType.setRequired(requestParam.required());
}
}else if(annotation instanceof PathVariable){
pathVariable = (PathVariable)annotation;
paramType.setName(pathVariable.value());
paramType.setRequired(true);
}
}
if(requestParam==null&& pathVariable == null){
throw new RuntimeException(clazz.getSimpleName()+"."+method.getName()+" param["+i+"] must be annotation present RequestParam or PathVariable");
}
}
paramType.setType(type);
this.requestParamTypes[i] = paramType;
}
}
public Object invoke(HttpRequest request,HttpResponse response) throws Throwable{
try{
if(this.parameterLength>0){
Object[] params = new Object[this.parameterLength];
for(int i=0;i<this.parameterLength;i++){
RequestParamType requestParamType = requestParamTypes[i];
Type type = requestParamType.getType();
if(type==Type.HTTP_REQUEST){
params[i]=request;
}else if(type == Type.HTTP_RESPONSE){
params[i]=response;
}else{
String name = requestParamType.getName();
String value = request.getParameter(name);
if(value==null){
value = requestParamType.getDefaultValue();
}
if(requestParamType.isRequired()&&value==null){
logger.warn("bad request http param[{}]=null",name);
throw new HttpException(HttpResponseStatus.BAD_REQUEST);
}
if(value==null){
continue;
}
switch(type){
case STRING:params[i]=value;break;
case LIST:params[i]=objectMapper.readValue(value, List.class);break;
case SET:params[i]=objectMapper.readValue(value, Set.class);break;
case MAP:params[i]=objectMapper.readValue(value, Map.class);break;
case ARRARY:params[i]=objectMapper.readValue(value, List.class).toArray();break;
case BOOLEAN:params[i]=Boolean.parseBoolean(value);break;
case SHORT:params[i]=Short.parseShort(value);break;
case INTEGER:params[i]=Integer.parseInt(value);break;
case LONG:params[i]=Long.parseLong(value);break;
case FLOAT:params[i]=Float.parseFloat(value);break;
case DOUBLE:params[i]=Double.parseDouble(value);break;
case CHAR:params[i]=value.charAt(0);break;
case BYTE:params[i]=value.getBytes()[0];break;
default:{}
}
}
}
return method.invoke(object,params);
}else{
return method.invoke(object);
}
}catch(InvocationTargetException e){
throw e.getTargetException();
}
}
public boolean isResponseBody() {
return responseBody;
}
public boolean isXssFilter(){
return xssFilter;
}
}
最终实现的结果如下,Object消息输出默认采用XSS过滤:
@Controller
public class TestController {
protected Logger logger = LoggerFactory.getLogger(TestController.class);
@RequestMapping("/test1.do")
@ResponseBody
public Object test1(){
Map<String,String> result = new HashMap<String,String>();
result.put("msg", "Hello World!");
return result;
}
@RequestMapping("/test2.do")
@ResponseBody
public Object test2(
@RequestParam(required=true,value="id")String id,
@RequestParam(value="name",defaultValue="Unknown")String name){
Map<String,String> result = new HashMap<String,String>();
result.put("id", id);
result.put("name", name);
return result;
}
@RequestMapping("/multipart.do")
public String multipart(HttpRequest request) throws Exception{
FileItem file = request.getFile("file");
logger.info(file.getFieldName()+":"+file.getFieldName()+":"+file.getSize());
FileUtils.writeByteArrayToFile(new File("D://test.jpg"), file.get());
return "success";
}
@RequestMapping("/xssFilter1.do")
@ResponseBody
public Object xssFilter1(HttpRequest request,HttpResponse response){
return request.getParametersMap();
}
@RequestMapping("/xssFilter2.do")
@ResponseBody
@XssFilter(false)
public Object xssFilter2(HttpRequest request,HttpResponse response){
return request.getParametersMap();
}
@RequestMapping("/test/test/*")
@ResponseBody
public Object test10(){
Map<String,String> result = new HashMap<String,String>();
result.put("msg", "Hello World!");
return result;
}
@RequestMapping("/test/{id}/index.do")
@ResponseBody
public Object test11(@PathVariable("id")String id){
Map<String,String> result = new HashMap<String,String>();
result.put("msg", "Hello World!");
return result;
}
}
接下来是WebSocket协议包体
当浏览器发起一个请求建立WebSocket连接的时候,首先还是发起一个原始的HTTP请求,这个请求通常是GET请求,但是Header(Connection)不在是Keep-Alive而是Upgrade,Header(Upgrade) 是WebSocket,并且会带上来一个密钥Header(Sec-WebSocket-Key)和一个Header(Sec-WebSocket-Version),最新的WebSocket版本是13,Chrome IE10 Firefox 等等发起的请求都是13。虽然Ajax 可以修改请求Header,但是WebSocket规范浏览器不允许浏览器设置以上4个Header所以服务器需要通过这四个头来判断请求的合法性,防止被伪造。
服务器判断成功后则将响应的返回码设置为101 Switching Protocols ,代表协议被切换,并且同样返回Upgrade 和Connection头,并且将密钥通过拼接规范指定的“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”后通过SHA1签名,再通过Base64加密返回,浏览器判断返回预期数据则表示WebSocket握手成功。
以下WebSocket包的协议规范,从左到有总共32位,1位表示是否是结束帧,2-4位保留位,除非特定协商才使用,5-8四位作为一个数字表示操作码,0:继续帧,1:文本帧,2:二进制帧,3-7:保留用于未来使用,8:关闭帧,9:ping帧,A:pong帧,B-F:保留用于未来使用;
第9位表示是否掩码,WebSocket规范浏览器发出的消息必须掩码,服务器发出的消息必须不掩码。
10-16位组成一个无符号的数字N最大127,如果小于等于125则代表包体长度只有N,如果126或则127则使用扩展长度,126使用16位扩展127则使用64位扩展,扩展长度中的无符号数字则表示包体长度。
如果第9位判断需要掩码,则后面4个字节(32位)作为掩码,读取完掩码之后按照之前读取出来的包体长度读取包体,掩码解码规则如下所示:
for(;index<end;index++,this.payloadIndex++){
byte masked = buffer.get(index);
masked = (byte)(masked ^ (mask[(int)(this.payloadIndex%4)] & 0xFF));
buffer.put(index, masked);
}
简单的聊天室测试类,浏览器想服务器发出一段消息,服务器数秒后,异步主动通知浏览器。
public class ChartSession extends SimpleWebSocket implements WebSocketSession{
private Map<String,ChartSession> sessions;
private String name;
public ChartSession(String name ,Map<String,ChartSession> sessions) {
super(65535);
this.sessions = sessions;
this.name = name;
}
private static final Logger logger = LoggerFactory.getLogger(ChartSession.class);
private WebSocketSession session;
@Override
public void onClose() {
logger.info("{} exit",name);
sessions.remove(name);
}
@Override
public void setWebSocketSession(WebSocketSession session) {
this.session = session;
}
@Override
public void onMessage(byte[] message) {
String messageStr;
try {
messageStr = new String(message,"UTF-8");
} catch (UnsupportedEncodingException e) {
messageStr = new String(message);
}
logger.info("{} say : ",messageStr);
Iterator<ChartSession> iter = sessions.values().iterator();
byte[] m;
try {
m = (name+" say : "+messageStr).getBytes("UTF-8");
} catch (UnsupportedEncodingException e1) {
m = (name+" say : "+messageStr).getBytes();
}
while(iter.hasNext()){
ChartSession s = iter.next();
try {
s.sendText(m, 1, TimeUnit.SECONDS, null);
} catch (InterruptedException e) {}
}
}
@Override
public Future<Void> sendText(byte[] bytes, long timeout, TimeUnit unit,
WebSocketCallback callback) throws InterruptedException {
return session.sendText(bytes, timeout, unit, callback);
}
@Override
public Future<Void> sendBinary(byte[] bytes, long timeout, TimeUnit unit,
WebSocketCallback callback) throws InterruptedException {
// TODO Auto-generated method stub
return session.sendBinary(bytes, timeout, unit, callback);
}
@Override
public Future<Void> close(WebSocketCallback callback, long timeout,
TimeUnit unit) throws InterruptedException {
// TODO Auto-generated method stub
return session.close(callback, timeout, unit);
}
}
--------------2014-03-16-更新
1.修改了启动方式,Http Server 通过传统构建,Controller采用Spring Context
2.增加了Velocity Controller 模版引擎
3.增加了Velocity 页面输出的deflate,gzip输出压缩,优先deflate
--------------2014-03-17-更新
1.修复了启动时,velocity 初始化配置过晚导致spring初始化类无法找到模版的BUG
2.调整了全局拦截器的执行位置,使其能够在未找到Handler时执行
--------------2014-04-21-更新
1.将@RequestParam的required默认设置成了true
2.解决了指定静态目录的BUG
--------------2014-04-27-更新
1.修复了302跳转没有返回Content-Length:0 ,导致FireFox处于盲听状态。
--------------2014-06-19-更新
1.优化了静态服务处理类,处理htdocs路径的时候由getAbsolutePath改成了getCanonicalPath,并且将目录分割符号改成了File.separator,因为getAbsolutePath可能在指定相对静态目录路径的时候导致路径混淆。