Websocket实现简易聊天室功能

WebSocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

WebSocket的优势

很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。(即在做实时推送或者聊天等业务场景,通常使用WebSocket)
在这里插入图片描述
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。

客户端的 JavaScript

 var url="ip:port/xf/chatroom";   //远程Websocket服务端站点,改为你自己的接口
        var websocket = null;
        if ('WebSocket' in window) {
            websocket = new WebSocket("ws://"+url);
        } else {
            alert("Not support WebScoket!")
        }
        websocket.onopen = onOpen;
        websocket.onmessage = onMessage;
        websocket.onerror = onError;
        websocket.onclose = onClose;

///
		/**
		 * Websocket前端业务逻辑
		 */
///
 
		/**客户端发起连接
		 * @param {Object} openEvent
		 */
		function onOpen(openEvent) {
			console.log("正在连接...")
        }

		/**
		 * @param {Object} 收到服务端的消息
		 */
        function onMessage(event) {
            if(typeof event.data =='string'){
                var element = document.createElement("p");
                element.innerHTML=event.data;
                document.getElementById("plane").appendChild(element);
            }else{
                var reader = new FileReader();
                reader.onload=function(eve){
                    if(eve.target.readyState==FileReader.DONE)
                    {
                        var img = document.createElement("img");
                        img.src=this.result;
                        document.getElementById("plane").appendChild(img);
                    }
                };
                reader.readAsDataURL(event.data);
            }
        }
		
		/**
		 * 连接出错
		 */
        function onError() {
			console.log("连接失败,请检查服务端是否正常启动");
        }
		
		/**
		 * 客户端关闭连接
		 * @param {Object} event
		 */
        function onClose(event) {
            console.log(event.reason)
            
        }

		
		/**
		 * 客户端发送消息
		 */
        function doSend() {
            if (websocket.readyState == 1) {  //0-CONNECTING;1-OPEN;2-CLOSING;3-CLOSED
                var msg = document.getElementById("message").value;
                if(msg){
                    websocket.send(msg);
                }
                sendFile(msg);
                document.getElementById("message").value="";
            } else {
                alert("connect fail!");
            }
        }

		
		
		/**
		 * 发送消息
		 * @param {Object} isWithText
		 */
        function sendFile(isWithText){
            var inputElement = document.getElementById("file");
            var fileList = inputElement.files;
            var file=fileList[0];
            if(!file) return;
            websocket.send(file.name+":fileStart");
            var reader = new FileReader();
            //以二进制形式读取文件
            reader.readAsArrayBuffer(file);
            //文件读取完毕后该函数响应
            reader.onload = function loaded(evt) {
                var blob = evt.target.result;
                //发送二进制表示的文件
                websocket.send(blob);
                if(isWithText){
                    websocket.send(file.name+":fileFinishWithText");
                }else{
                    websocket.send(file.name+":fileFinishSingle");
                }
                console.log("finnish");
            }
            inputElement.outerHTML=inputElement.outerHTML; //清空<input type="file">的值
        }

		/**
		 * 客户端断开连接
		 */
        function disconnect(){
            if (websocket != null) {
                websocket.close();
                websocket = null;
            }
        }	
		

服务端Java实现

服务端主要通过Springboot编写,所以首页引入websocket starter

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
  • 拦截器配置,客户端与服务端做握手动作前后的相关工作
package cn.xfnihao.chat.interceptor;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

import java.util.Map;
import java.util.Random;

/**
 * @Author Fang chenjiang
 * @Date 2020/11/20
 */
public class HandlershakeInterceptor 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)];
    }

}
  • WebSocket核心配置
/**
 * @Author Fang chenjiang
 * @Date 2020/11/20
 */

@Configuration  //配置类
@EnableWebSocket  //声明支持websocket
public class WebSocketConfig  implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //addHandler注册和路由的功能,当客户端发起websocket连接,把/path交给对应的handler处理,而不实现具体的业务逻辑,可以理解为收集和任务分发中心。
        //setAllowedOrigins(String[] domains),允许指定的域名或IP(含端口号)建立长连接,默认只有本地。如果不限时使用"*"号,如果指定了域名,则必须要以http或https开头。
        //addInterceptors,顾名思义就是为handler添加拦截器,可以在调用handler前后加入自定义的逻辑代码。
        //Websocket:ws://ip:port/path
        registry.addHandler(ChatRoom(), "xf/chatroom").setAllowedOrigins("*").addInterceptors(handshakeInterceptor());
    }

    @Bean
    public HandshakeInterceptor handshakeInterceptor(){
        return new HandlershakeInterceptor();
    }

    @Bean
    public XfChatRoom ChatRoom(){
        return new XfChatRoom();
    }

}

  • 服务端聊天室核心代码
/**
 * 聊天室核心业务
 * @Author Fang chenjiang
 * @Date 2020/11/20
 */
@Slf4j
public class XfChatRoom extends AbstractWebSocketHandler {

    @Autowired
    QiniuUtils qiniuUtils;

    public final static List<WebSocketSession> sessionList =Collections.synchronizedList(new ArrayList<>());
    SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    FileOutputStream output;
    @Override
    public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
        System.out.println("Connection established..."+webSocketSession.getId());
        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("F:\\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("F:\\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();
        }
    }
}

当前的整个demo支持文字和文件传输,不过文件传输仅在本地实现,通常在实际开发中,我们会借助第三方文件服务器(例如七牛,阿里云等)对文件进行存储管理。

总结

WebSocket实现聊天室功能大致框架就如上所说,整个客户端(HTML+JavaScript)和服务端(Java)交互过程并不难,主要关注其中核心部分即可,另外,注意体会WebSocket与传统轮训方式的区别,以及TCP在整个全双工通信的实现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值