websocket实现聊天室应用,包括文字和图片上传_websocket onmessage怎么接收客户端的图片(3)

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

WebSocket

WebSocket是什么?

WebSocket和HTTP一样,是基于TCP的应用层通信协议,通过维持一条持久的连接以实现浏览器与服务器全双工通信,是随着H5一起出来的。

WebSocket和HTTP有什么关系么?

Websocket其实是一个新协议,除了最开始借用了HTTP来完成握手,后面跟HTTP基本没有关系了。

传统的HTTP要不断的建立、关闭连接(这里的关闭并不是说关闭TCP连接,而是关闭HTTP连接),而且由于HTTP是无状态性的,每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁,当然,一般只是带个JSESSIONID之类的,服务端通过这个就知道你的其他信息了。WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。在断掉 WebSocket 连接前,都不需要客户端和服务端重新发起连接请求,即只需一次HTTP握手,整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的无状态性。

WebSocket握手

WebSocket的握手过程就是建立WebSocket连接的过程,握手是借用了HTTP的。

总体步骤大致如下:

Step 1: 建立TCP连接(这一步是一切的基础,HTTP也一样)。

Step 2: 浏览器借助一个WebSocket 客户端对象,连接类似 ws://yourdomain:port/path或wss://yourdomain:port/path的URL,WebSocket 客户端对象会自动解析并识别为要建立WebSocket 连接,从而发送HTTP Get 请求,并为请求自动添加一些供WebSocket使用的HTTP Headers字段。注意,连接头变成了ws,而不是一般的http,这是协议的意思,ws代表websocket。

Step 3: Server收到HTTP请求后,会把Step 1的TCP连接的应用层协议从HTTP转变为WebSocket。至此,HTTP部分就退出舞台了,WebSocket开始接管一切。

WebSocket握手报文(HTTP)分析:

请求

GET /chat HTTP/1.1
Host: 127.0.0.1:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: JkTukkPc4Rha+nIkbYhEkQ==
Sec-WebSocket-Version: 13
Origin: http://localhost:8080

响应

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: RD69G0BS8RPH/GbY6rBsZI75pjk=
Server:Apache-Coyote/1.1
Date:Sat, 17 Jun 2017 15:35:41 GMT

以上报文和一般的HTTP有很多不同的地方:

1、请求和响应都有Upgrade: websocket和Connection: Upgrade,这是协议转换的意思,也就是请求时告诉服务器,本次连接需要的是websocket通信协议,而不是一般的HTTP,响应也带有该响应头表示转换成功。

一般响应成功的响应头是 HTTP/1.1 200 OK、HTTP/1.1 404 NOT FOUND等,此处是HTTP/1.1 101 Switching Protocols,同样101也是HTTP的一种响应状态,表示协议转换成功。

2、Sec-WebSocket-Key: JkTukkPc4Rha+nIkbYhEkQ==和Sec-WebSocket-Accept: RD69G0BS8RPH/GbY6rBsZI75pjk=

这是websocket握手的检验,也即测试服务器是否提供websocket服务,如果提供,Sec-WebSocket-Key和Sec-WebSocket-Accept必然是有对应关系的,具体的关系如下:

Sec-WebSocket-Key拼接上258EAFA5-E914-47DA-95CA-C5AB0DC85B11(这串magic string是写在协议里面的,所有的websocket底层实现都要拼接该串)

得到对拼接结果

JkTukkPc4Rha+nIkbYhEkQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

使用SHA-1(160数位)进行哈希操作,对哈希后的结果base64 进行编码,就可以得到结果

RD69G0BS8RPH/GbY6rBsZI75pjk=

以上操作不仅仅是服务端要做,客户端也同样做了该操作,然后将自己计算得到的结果和服务器传来的Sec-WebSocket-Accept作比较,如果相等则没问题,握手成功,如果不一致,则握手失败。

3、Origin用于防止未授权的跨域脚本攻击,服务器可以从Origin决定是否接受该WebSocket连接,所有来源于浏览器的请求都会带上该请求头。如,已授权站点www.mydomain.com,并且产生cookie,用户没关闭授权站点的时候点击另外的www.hack.com下面的链接,则该连接可以带着授权的cookie去做很多事。如果有了Origin,并且在服务端setAllowedOrigins(“www.mydomain.com”)则可以避免。

4、Sec-WebSocket-Version: 13 ,表明了websocket的版本,以前各厂商混战的时候什么样的版本都有,还好现在已经统一了,用13版即可。

代码以及部署

我的程序部署于tomcat7.0.5,基于Spring 4.3.7,需要使用支持websocket的IE10、Chrome和Firefox等较高级别版本的浏览器打开。

程序主要分为6个部分:2个配置xml、3个java文件、1个jsp。

前台主要用H5提供的websocket对象,它存在于顶层对象window下,凡是支持H5的浏览器都实现了该对象。

后台主要用Spring提供的websocket配置类WebSocketConfigurer、拦截器HttpSessionHandshakeInterceptor和处理器AbstractWebSocketHandler来实现。

如下:

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>WebsocketTest</display-name>
  
	<servlet>
		<servlet-name>springmvc</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
		<param-name>contextConfigLocation</param-name>
		<!-- <param-value>classpath:spring-mvc.xml</param-value> --><!-- 类路径 -->
		<param-value>/WEB-INF/spring-mvc.xml</param-value> <!-- 相对路径 -->
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>springmvc</servlet-name>
		<url-pattern>*.sc</url-pattern>
	</servlet-mapping>

  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
</web-app>

spring-mvc.xml

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"  
    xmlns:tx="http://www.springframework.org/schema/tx" xmlns:p="http://www.springframework.org/schema/p"  
    xmlns:context="http://www.springframework.org/schema/context"  
    xsi:schemaLocation="  
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd  
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd  
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd  
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">  
   <context:component-scan base-package="com.websocket"></context:component-scan>  
</beans>  

WebSocketConfig.java


package com.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;


/**
 * @author wuwenhai
 * @since JDK1.6
 * @history 2017-6-3 wuwenhai 新建
 * 配置websocket入口,允许访问的域、注册Handler、SockJs支持和拦截器
 */
@Configuration  //配置类  
@EnableWebSocket  //声明支持websocket  
public class WebSocketConfig implements WebSocketConfigurer{  
  
    @Override  
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {  
     String[] allowsOrigins={"http://localhost:8080"};
     //addHandler注册和路由的功能,当客户端发起websocket连接,把/path交给对应的handler处理,而不实现具体的业务逻辑,可以理解为收集和任务分发中心。
     //setAllowedOrigins(String[] domains),允许指定的域名或IP(含端口号)建立长连接,默认只有本地。如果不限时使用"*"号,如果指定了域名,则必须要以http或https开头。
     //addInterceptors,顾名思义就是为handler添加拦截器,可以在调用handler前后加入自定义的逻辑代码。
        registry.addHandler(ChatRoom(), "/chat.sc").setAllowedOrigins("*").addInterceptors(handshakeInterceptor()); 
        //允许客户端使用SockJS  
        //SockJS 是一个浏览器上运行的 JavaScript 库,如果浏览器不支持 WebSocket,该库可以模拟对 WebSocket 的支持。
        registry.addHandler(ChatRoom(), "/sockjs/chat.sc").addInterceptors(handshakeInterceptor()).withSockJS();  
    }  
  
    @Bean  
    public HandshakeInterceptor handshakeInterceptor(){  
        return new HandshakeInterceptor();  
    }  
    
    @Bean  
    public ChatRoom ChatRoom(){  
        return new ChatRoom();  
    }
  
}  
  

HandshakeInterceptor.java

package com.websocket;

import java.util.Map;
import java.util.Random;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

/**
 * @author wuwenhai
 * @since JDK1.6
 * @history 2017-6-3 wuwenhai 新建
 * 握手拦截器,在连接时做一些事情
 */
public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor{  
	@Override  
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> attributes) throws Exception {  
        //attributes是session里面的所有属性的map表示
        attributes.put("user", getRandomNickName());
        return super.beforeHandshake(request, response, handler, attributes);  
    } 
	
    @Override  
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {  
        super.afterHandshake(request, response, wsHandler, ex);  
    }        
    
    //给每个进来的人(session)随机分配个昵称,这里没做控制,所以聊天室内的昵称可能发生重复
    public String getRandomNickName(){
    	String[] nickNameArray={"Captain America","Deadpool","Hawkeye","Hulk","Iron Man","Spider Man","Thor","Wolverine","Black Panther","Colossus"};
    	Random random=new Random();
    	return nickNameArray[random.nextInt(10)];
    }
}  

ChatRoom.java

package com.websocket;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.apache.log4j.Logger;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;


/**
 * @author wuwenhai
 * @since JDK1.6
 * @history 2017-6-3 wuwenhai 新建
 * 聊天室代码
 */
public class ChatRoom extends AbstractWebSocketHandler{

	public final static List<WebSocketSession> sessionList = Collections.synchronizedList(new ArrayList<WebSocketSession>());
	SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
	Logger logger = Logger.getLogger(this.getClass());
	FileOutputStream output;
	@Override  
    public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {  
		System.out.println("Connection established..."+webSocketSession.getRemoteAddress());  
		System.out.println(webSocketSession.getAttributes().get("user")+" Login");
		webSocketSession.sendMessage(new TextMessage("I'm "+(webSocketSession.getAttributes().get("user"))));
        sessionList.add(webSocketSession);
    }  
	
	@Override  
    public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus status) throws Exception {  
        System.out.println("Connection closed..."+webSocketSession.getRemoteAddress()+" "+status); 
        System.out.println(webSocketSession.getAttributes().get("user")+" Logout");
        sessionList.remove(webSocketSession);
    }
	
    @Override
	public void handleTextMessage(WebSocketSession websocketsession, TextMessage message)
    {
		String payload=message.getPayload();
		String textString;
		try {
			if(payload.endsWith(":fileStart")){
				output=new FileOutputStream(new File("D:\\images\\"+payload.split(":")[0]));
			}else if(payload.endsWith(":fileFinishSingle")){
				output.close();
				String fileName=payload.split(":")[0];
				for(WebSocketSession session:sessionList){
					if(session.getId().equals(websocketsession.getId())){
	            		textString=" I ("+format.format(new Date())+")<br>";
	            	}else{
	            		textString=websocketsession.getAttributes().get("user")+" ("+format.format(new Date())+")<br>";
	            	}
	            	TextMessage textMessage = new TextMessage(textString); 
	        		session.sendMessage(textMessage); 
					sendPicture(session,fileName);
				}
			}else if(payload.endsWith(":fileFinishWithText")){
				output.close();
				String fileName=payload.split(":")[0];
				for(WebSocketSession session:sessionList){
					sendPicture(session,fileName);
				}
			}else{
	            for(WebSocketSession session: sessionList){
	            	if(session.getId().equals(websocketsession.getId())){
	            		textString=" I ("+format.format(new Date())+")<br>"+payload;
	            	}else{
	            		textString=websocketsession.getAttributes().get("user")+" ("+format.format(new Date())+")<br>"+payload;
	            	}
	            	TextMessage textMessage = new TextMessage(textString); 
	        		session.sendMessage(textMessage); 
	        	}
	        }
		} catch (IOException e) {
			e.printStackTrace();
		}
    }
  
    @Override
	public void handleBinaryMessage(WebSocketSession session, BinaryMessage message)
    {
		ByteBuffer buffer= message.getPayload();
		try {
			output.write(buffer.array());
		} catch (IOException e) {
			e.printStackTrace();
		}
    }
    
    @Override  
    public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {  
        if(webSocketSession.isOpen()){  
            webSocketSession.close();  
        }  
        System.out.println(throwable.toString());  
        System.out.println("WS connection error,close..."+webSocketSession.getRemoteAddress());  
    }  
  
    @Override  
    public boolean supportsPartialMessages() {  
        return true;  
    }  
      
    public void sendPicture(WebSocketSession session,String fileName){
		FileInputStream input;
		try {
			File file=new File("D:\\images\\"+fileName);
			input = new FileInputStream(file);
	    	byte bytes[] = new byte[(int) file.length()]; 
	    	input.read(bytes);
	    	BinaryMessage byteMessage=new BinaryMessage(bytes);
	    	session.sendMessage(byteMessage);
	    	input.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

chatRoom.jsp

<!DOCTYPE html>  
<html>  
<head>  
<meta charset="UTF-8">  
<title>Insert title here</title>  
<script type="text/javascript" src="js/sockjs.min.js"></script>  
<script type="text/javascript" src="js/jquery.js"></script>  
<script type="text/javascript">  
    var url = "127.0.0.1:8080/websocket";  
    var websocket = null;  
    if ('WebSocket' in window) {  
        websocket = new WebSocket("ws://" + url + "/chat.sc");  
    } else {  
        websocket = new SockJS("http://" + url + "/sockjs/chat.sc");  
    }  
    websocket.onopen = onOpen;  
    websocket.onmessage = onMessage;  
    websocket.onerror = onError;  
    websocket.onclose = onClose;  
  
    function onOpen(openEvent) {  
        document.getElementById("plane").innerHTML = document.getElementById("plane").innerHTML+ "OPEN<br/>"; 
    }  
  
    function onMessage(event) {  
    	if(typeof event.data =='string'){
    		var element = document.createElement("p");


![img](https://img-blog.csdnimg.cn/img_convert/f822a61a63f217f3693218d38812e0e0.png)
![img](https://img-blog.csdnimg.cn/img_convert/8219c927cc15beadbd1bba2e2d3664f8.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**

rHTML = document.getElementById("plane").innerHTML+ "OPEN<br/>"; 
    }  
  
    function onMessage(event) {  
    	if(typeof event.data =='string'){
    		var element = document.createElement("p");


[外链图片转存中...(img-8nUhcJSa-1715844506179)]
[外链图片转存中...(img-K7r381pb-1715844506179)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值