美文网首页JavaJava学习之路Java学习笔记
基于WebSocket的在线聊天室(一)

基于WebSocket的在线聊天室(一)

作者: anyesu | 来源:发表于2016-04-28 01:04 被阅读14649次
效果预览

前言


去年在tomcat7自带的例子中发现了两个有趣的demo,贪食蛇游戏和画板。很有意思的是打开的几个窗口内容都是一样的,而且还会同步更新,如果换做以往做web开发的套路来实现这个效果还是比较费劲的。于是心血来潮就去查了一些关于websocket的资料并做了这么一个文字聊天室。前段时间应别人的需要又把它翻了出来加上了视频和语音功能,瞬间高大上了很多。做完之后当然得趁热打铁总结下,顺便作为第一次写文章的素材。(∩_∩)

简介


WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据。

说起实时通讯就不得不提一些“服务器推”技术。

  • 轮询

客户端以一定的时间间隔发送Ajax请求,优点实现起来比较简单、省事,不过缺点也很明显,请求有很大一部分是无用的,而且需要频繁建立和释放TCP连接,很消耗带宽和服务器资源。

  • 长轮询

与普通轮询不同的地方在于,服务端接收到请求后会保持住不立即返回响应,等到有消息更新才返回响应并关闭连接,客户端处理完响应再重新发起请求。较之普通轮询没有无用的请求,但服务器保持连接也是有消耗的,如果服务端数据变化频繁的话和普通轮询并无两样。

  • 长连接

在页面中嵌入一个隐藏的iframe,将其src设为一个长连接的请求,这样服务端就能不断向客户端发送数据。优缺点与长轮询相仿。

这些技术都明显存在两个相同的缺点:1. 服务器需要很大的开销 2. 都做不到真正意义上的“主动推送”,服务端只能“被动”地响应,于是就轮到正主出场了。在websocket中,只需要做一个握手动作就可以在客户端和服务器之间建立连接,之后通过数据帧的形式在这个连接上进行通讯,并且,由于连接是双向的,在连接建立之后服务端随时可以主动向客户端发送消息(前提是连接没有断开)。

实现


以前一些websocket的例子都是基于某个特定的容器(如Tomcat,Jetty),在Oracle发布了JSR356规范之后,websocket的JavaAPI得到了统一,所以只要Web容器支持JSR356,那么我们写websocket时,代码都是一样的了.Tomcat从7.0.47开始支持JSR356.另外有一点要说明的是JDK的要求是7及以上。
我本地的环境为jdk1.7,nginx1.7.8(反向代理),tomcat7.0.52(需要在buildpath中还要添加tomcat7的library),chrome。

废话不多说,先上代码

消息结构Message类
public class Message {
    
        private int type;//消息类型

        private String msg;//消息主题

        private String host;// 发送者

        private String[] dests;// 接受者

        private RoomInfo roomInfo;//聊天室信息

        public class MsgConstant {

            public final static int Open = 1;// 新连接

            public final static int Close = 2;// 连接断开

            public final static int MsgToAll = 3;// 发送给所有人

            public final static int MsgToPoints = 4;// 发送给指定用户

            public final static int RequireLogin = 5;// 需要登录

            public final static int setName = 6;// 设置用户名
        }

        public static class RoomInfo {
    
            private String name;// 聊天室名称

            private String creater;//创建人

            private String createTime;// 创建时间

            public RoomInfo(String creater, String createTime) {
                this.creater = creater;
                this.createTime = createTime;
            }
    
            public RoomInfo(String name) {
                this.name = name;
            }
            // 省略set get
        }

        public Message() {
            setType(MsgConstant.MsgToAll);
        }
    
        public Message(String host, int type) {
            setHost(host);
            setType(type);
        }
    
        public Message(String host, int type, String msg) {
            this(host, type);
            setMsg(msg);
        }

        public Message(String host, int type, String[] dests) {
            this(host, type);
            setDests(dests);
        }

        @Override
        public String toString() {
            // 序列化成json串
            return JSONObject.toJSONString(this);
        }
    }

public class wsConfigurator extends ServerEndpointConfig.Configurator {
    @Override
    public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
        //通过配置来获取httpsession
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        config.getUserProperties().put(HttpSession.class.getName(), httpSession);
    }
}

@ServerEndpoint(value = "/websocket/chat/{uid}", configurator = wsConfigurator.class)
public class textController {

    private Session session;

    private LoginUser loginUser;

    private static RoomInfo roomInfo;

    //连接集合
    private static final Set<textController> connections = new CopyOnWriteArraySet<textController>();

    /**
     * websock连接建立后触发
     * 
     * @param session
     * @param config
     */
    @OnOpen
    public void OnOpen(Session session, EndpointConfig config, @PathParam(value = "uid") String uid) {
        //设置websock连接的session
        setSession(session);
        // 获取HttpSession
        HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        // 从HttpSession中取得当前登录的用户作为当前连接的用户
        setLoginUser((LoginUser) httpSession.getAttribute("LoginUser"));
        if (getLoginUser() == null) {
            requireLogin();// 未登录需要进行登录
            return;
        }
        // 设置聊天室信息
        if (getConnections().size() == 0) {// 如果当前聊天室为空,建立新的信息
            setRoomInfo(new RoomInfo(getUserName(), (new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")).format(new Date())));
        }
        //加入连接集合
        getConnections().add(this);
        //广播通知所有连接有新用户加入
        broadcastToAll(new Message(getUserName(), MsgConstant.Open, getUsers()));
    }

    /**
     * websock连接断开后触发
     */
    @OnClose
    public void OnClose() {
        //从连接集合中移除
        getConnections().remove(this);
        //广播通知所有连接有用户退出
        broadcastToAll(new Message(getUserName(), MsgConstant.Close, getUsers()));
    }

    /**
     * 接受到客户端发送的字符串时触发
     * 
     * @param message
     */
    @OnMessage(maxMessageSize = 1000)
    public void OnMessage(String message) {
        //消息内容反序列化
        Message msg = JSONObject.parseObject(message, Message.class);
        msg.setHost(getUserName());
        //对html代码进行转义
        msg.setMsg(txt2htm(msg.getMsg()));
        if (msg.getDests() == null)
            broadcastToAll(msg);
        else
            broadcastToSpecia(msg);
    }

    @OnError
    public void onError(Throwable t) throws Throwable {
        System.err.println("Chat Error: " + t.toString());
    }

    /**
     * 广播给所有用户
     * 
     * @param msg
     */
    private static void broadcastToAll(Message msg) {
        for (textController client : getConnections())
            client.call(msg);
    }

    /**
     * 发送给指定的用户
     * 
     * @param msg
     */
    private static void broadcastToSpecia(Message msg) {
        for (textController client : getConnections())
            // 感觉用map进行映射会更好点
            if (Contains(msg.getDests(), client.getUserName()))
                client.call(msg);
    }

    private void call(Message msg) {
        try {
            synchronized (this) {
                if (getUserName().equals(msg.getHost()) && msg.getType() == MsgConstant.Open)
                    msg.setRoomInfo(getRoomInfo());
                this.getSession().getBasicRemote().sendText(msg.toString());
            }
        } catch (IOException e) {
            try {
                //断开连接
                this.getSession().close();
            } catch (IOException e1) {
            }
            OnClose();
        }
    }

    private void requireLogin() {
        Message msg = new Message();
        msg.setType(MsgConstant.RequireLogin);
        call(msg);
    }

    public void setSession(Session session) {
        this.session = session;
    }

    public Session getSession() {
        return this.session;
    }

    public LoginUser getLoginUser() {
        return loginUser;
    }

    public void setLoginUser(LoginUser loginUser) {
        this.loginUser = loginUser;
    }

    /**
     * 设置聊天室信息
     */
    public static void setRoomInfo(RoomInfo info) {
        roomInfo = info;
    }

    public static RoomInfo getRoomInfo() {
        return roomInfo;
    }

    private String getUserName() {
        if (getLoginUser() == null)
            return "";
        return getLoginUser().getUserName();
    }

    public static Set<textController> getConnections() {
        return connections;
    }

    private String[] getUsers() {
        int i = 0;
        String[] destArrary = new String[getConnections().size()];
        for (textController client : getConnections())
            destArrary[i++] = client.getUserName();
        return destArrary;
    }

    /**
     * html代码转义
     * 
     * @param txt
     * @return
     */
    public static String txt2htm(String txt) {
        if (StringUtils.isBlank(txt)) {
            return txt;
        }
        return txt.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll(" ", " ").replaceAll("\n", "<br/>").replaceAll("\'", "'");
    }

    /**
     * 字符串数组是否包含指定字符串
     * 
     * @param strs
     * @param str
     * @return
     */
    public static boolean Contains(String[] strs, String str) {
        if (StringUtils.isBlank(str) || strs.length == 0)
            return false;
        for (String s : strs)
            if (s.equals(str))
                return true;
        return false;
    }
}

服务端代码就这么三个类,还是比较简单的(>_<|||还是比别人的例子复杂好多)。

  • Message类是与客户端统一的消息结构,消息序列化成json串进行传输,客户端再反序列化为对象进行操作,感觉还是比较方便的。
  • wsConfigurator类继承ServerEndpointConfig.Configurator并实现了modifyHandshake方法,将其作为ServerEndpoint的configurator参数值,这里的用途是拿到HttpSession,之后就可以取得HttpSession中的内容(比如登录用户信息)。
  • textController类,上面两个类都是可有可无的东西,这个就是websocket的关键,这里以注解的方式实现,很方便。除此之外,另一种方式是继承javax.websocket.Endpoint类,不过我没试过就不废话了。

@ServerEndpoint

用来标记一个websocket服务器终端

  • value : websocket连接的url,类似spring mvc 的 @RequestMapping,区别的话就是不用再补后缀了
  • configurator : 这个参数没有深究过,只用到过之前说的提取HttpSession的作用

@ClientEndpoint

用来标记一个websocket客户器

@OnOpen

websock连接建立后执行,主要进行一些初始化操作

  • session : 此session非HttpSession,而是websock通讯所使用的session,因此需要上述方法另外得到HttpSession
  • EndpointConfig : 应该包含一些这个Endpoint的配置信息之类的(瞎猜的)
  • @PathParam(value = "uid") : 这个是自定义的参数,主要对应ServerEndpoint注解的value值中的{uid}参数占位符,除此之外还可以通过session.getRequestParameterMap()来获取url参数(如"/websocket/chat?uid=123")

@OnClose

websock连接断开后执行,没什么好说的

@OnMessage

接收到客户端发送端的消息后执行,值得注意的是,OnMessage注解的方法可以有多个重载,方法参数可以为String,ByteBuffer等类型,相应的,session有这么几个方法可以向客户端发送消息:sendText,sendBinary,sendObject等。另外,上面代码里向客户端发消息用的是session.getBasicRemote().sendText方法,这是阻塞的方式,还有一种异步方式session.getAsyncRemote().sendText,虽说是异步,不过高频率发送并没有出现错乱的情况,还有待研究。

  • maxMessageSize : 用来指定消息字节最大限制,超过限制就会关闭连接,文字聊天不设置基本没什么问题,默认的大小够用了,传图片或者文件可能就会因为超出限制而导致连接“莫名其妙”被关闭,这个坑还是比较难发现的。

@OnError

报错的时候会执行,不过试过各种异常下这个方法都没有执行,很奇怪

服务端功能比较简单,主要实现了几个注解的方法,对客户端传来的消息进行广播,并无其他额外操作。再来看下前端的代码:

(function(window) {
    Blob.prototype.appendAtFirst = function(blob) {
        return new Blob([blob, this]);
    };
    var WS_Open = 1,
        WS_Close = 2,
        WS_MsgToAll = 3,
        WS_MsgToPoints = 4,
        WS_RequireLogin = 5,
        WS_setName = 6,
        types = ["文本", "视频", "语音"],
        getWebSocket = function(host) {
            var socket;
            if ('WebSocket' in window) {
                socket = new WebSocket(host);
            } else if ('MozWebSocket' in window) {
                socket = new MozWebSocket(host);
            }
            return socket;
        },
        WSClient = function(option) {
            var isReady = false,
                init = function(client, option) {
                    client.socket = null;
                    client.online = false;
                    client.isUserClose = false;
                    client.option = option || {};
                };

            this.connect = function(host) {
                var client = this,
                    socket = getWebSocket(host);

                if (socket == null) {
                    console.log('错误: 当前浏览器不支持WebSocket,请更换其他浏览器', true);
                    alert('错误: 当前浏览器不支持WebSocket,请更换其他浏览器');
                    return;
                }

                socket.onopen = function() {
                    var onopen = client.option.onopen,
                        type = types[client.option.type];
                    console.log('WebSocket已连接.');
                    console.log("%c类型:" + type, "color:rgb(228, 186, 20)");
                    onopen && onopen();
                };

                socket.onclose = function() {
                    var onclose = client.option.onclose,
                        type = types[client.option.type];
                    client.online = false;
                    console.error('WebSocket已断开.');
                    console.error("%c类型:" + type, "color:rgb(228, 186, 20)");
                    onclose && onclose();
                    if (!client.isUserClose) {
                        client.initialize();
                    }
                };

                socket.onmessage = function(message) {
                    var option = client.option;
                    if (typeof(message.data) == "string") {
                        var msg = JSON.parse(message.data);
                        switch (msg.type) {
                        case WS_Open:
                            option.wsonopen && option.wsonopen(msg);
                            break;
                        case WS_Close:
                            option.wsonclose && option.wsonclose(msg);
                            break;
                        case WS_MsgToAll:
                        case WS_MsgToPoints:
                            option.wsonmessage && option.wsonmessage(msg);
                            break;
                        case WS_RequireLogin:
                            option.wsrequirelogin && option.wsrequirelogin();
                            break;
                        case WS_setName:
                            option.userName = msg.host;
                            option.wssetname && option.wssetname(msg);
                            break;
                        }
                    } else if (message.data instanceof Blob) {
                        option.wsonblob && option.wsonblob(message);
                    }

                };

                isReady = true;
                this.socket = socket;
                return this;
            };

            this.initialize = function(param) {
                return this.connect(this.option.host + (param ? "?" + param : ""));
            };

            this.sendString = function(message) {// 向服务端发送给字符串
                return isReady && this.socket.send(message);
            };

            this.sendBlob = function(blob) {// 向服务端发送二进制数据
                return isReady && this.socket.send(blob.appendAtFirst(this.option.userName));
            };

            this.close = function() {
                this.isReady = false;
                this.online = false;
                this.isUserClose = true;
                this.socket.close();
                return true;
            };

            this.isMe = function(name) {
                return this.option.userName == name;
            }

            init(this, option);
        };

    window.WSClient = WSClient;

})(window);

这里的代码我做了下粗劣的封装,就不给出具体实现了,调用的时候实现具体的逻辑即可。如下形式:

var textClient = new WSClient({
    host: "ws://" + window.location.host + "/websocket/chat/123",// 注意这里不是http协议
    type: MODE_TEXT,
    onopen: function() {
        console.log('WebSocket已连接.');
    },
    onclose: function() {
        console.log('Info: WebSocket已断开.');
    },
    wsonopen: function(msg) {
        console.log("***加入聊天室");
    },
    wsonclose: function(msg) {
        console.log("***退出了聊天室");
    },
    wsonmessage: function(msg) {
        console.log(“收到消息:” + msg.msg);
    },
    wsrequirelogin: function(msg) {
        document.location.href = "http://" + window.location.host + "/login.htm?to_url=" + document.location.href;
    },
    wssetname: function(msg) {
    }
});

和服务端要实现的几个方法类似,就不多说了。其中,socket.onmessage中message.data有两种类型:string和Blob,Blob表示二进制数据,比如图片和声音,文件就可以通过Blob对象来传输。另外,服务端发送消息的send方法是有好几种的,而这里WebSocket对象的send方法只有一个,参数可以是Blob或string。

最后附上websocket的nginx配置:

location /websocket/chat { 
    proxy_pass http://localhost:8080/websocket/chat; 
    include websocket.conf;
}

websocket.conf:

#避免nginx超时
proxy_read_timeout 86400;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

参考文章:


唉,第一次写文章,加上基础不扎实,磨磨蹭蹭写了一晚上才结束战斗,真是不容易。水平有限,有讲错的地方欢迎指出,之后会继续关于视频和音频通讯方式的总结。

相关文章

网友评论

  • c033899b2f34:谢谢大大,写的很棒,代码简介,功能齐备,下载完改下路径就能访问,太赞了!
  • 62b9cc6cec34:Exception in thread "WebSocketServer-/trains-13" java.lang.IllegalStateException: Message will not be sent because the WebSocket session has been closed
    at org.apache.tomcat.websocket.WsRemoteEndpointImplBase.writeMessagePart(WsRemoteEndpointImplBase.java:384)
    at org.apache.tomcat.websocket.WsRemoteEndpointImplBase.startMessage(WsRemoteEndpointImplBase.java:341)
    at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$TextMessageSendHandler.write(WsRemoteEndpointImplBase.java:757)
    at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$TextMessageSendHandler.onResult(WsRemoteEndpointImplBase.java:773)
    at org.apache.tomcat.websocket.WsRemoteEndpointImplBase.startMessage(WsRemoteEndpointImplBase.java:306)
    at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$TextMessageSendHandler.write(WsRemoteEndpointImplBase.java:757)
    at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$TextMessageSendHandler.onResult(WsRemoteEndpointImplBase.java:773)
    at org.apache.tomcat.websocket.WsRemoteEndpointImplBase.endMessage(WsRemoteEndpointImplBase.java:377)
    at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$EndMessageHandler.onResult(WsRemoteEndpointImplBase.java:491)
    at org.apache.tomcat.websocket.server.WsRemoteEndpointImplServer$OnResultRunnable.run(WsRemoteEndpointImplServer.java:242)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:745)
    62b9cc6cec34:大神遇到过这个问题吗
  • 62b9cc6cec34:手机浏览器不知道能不能实现app网页视频通话额,高手懂吗
    anyesu:@风雨无阻_66ea 我的手机以前试过可以用,只要浏览器支持就行。不过手机上浏览器版本会很多,很难做兼容。具体的可以查下支持websocket和getUserMedia的浏览器版本
  • 62b9cc6cec34:getUserMedia() no longer works on insecure origins. To use this feature, you should consider switching your application to a secure origin, such as HTTPS. See https://goo.gl/rStTGz for more details.
    高手 这个问题你遇到过吗?有时候会视频出来!
    62b9cc6cec34:@风雨无阻_66ea 谢谢 已完美解决
    62b9cc6cec34: @风雨无阻_66ea 谢谢大神
    anyesu:@风雨无阻_66ea chrome对安全性有要求,要使用https方式来访问服务(本地访问可以用localhost)。火狐没这个限制,可以试试
  • 王先森QAQ:基于tomcat的demo,那个roomInfo类,在哪里?吗,没有这个类啊
    王先森QAQ:@anyesu 嗯,刚开始没看见,这个demo,那个传输文件,是不是没有完成?我这点击没有反应,js里边我看也没有,
    anyesu:@wangdyqxx 是静态内部类,可以直接用的。import indi.anyesu.model.Message.RoomInfo;
    王先森QAQ:ok,找到了,但是直接调用RoomInfo是不行的,那是内部类啊
  • mochisher:楼主,请教怎么设置多个房间啊,是不是要开多个websocket
    anyesu:@mochisher 本文中所有连接都是放在一个列表里(List<AbstractWSController>),要多个房间的话可以按Map<String, List<AbstractWSController>>的形式管理连接,将房间号作为key
  • 9f53aa8a2d43:楼主 nodejs怎么运行文件啊 运行了启动服务的文件 然后打开index.html 有页面出来 但是发送不了消息 也不能操作其他的选项
    anyesu:@唱遇见的风行 F12看下是不是报错了?
  • 阿灯_supwinr:博主请问下, 为什么我部署Tomcat之后访问是 404 呢,路径没问题
  • e31400b4503e:用tomcat 7部署 为啥总是执行Info: WebSocket已断开?多谢
    13793d7e6890:朋友,我也遇到了Info: WebSocket已断开的问题,如何在依赖管理中把websocket-api的scope改为provided啊?
    anyesu:不好意思,现在才看到。我用idea的时候遇到过这个问题,部署的时候把websocket-api.jar又拷贝了一份到WEB-INF\lib目录下,你看下是不是也这样。在依赖管理中把websocket-api的scope改为provided就行了
  • rwy:楼主,怎么创建一个聊天室呀
    rwy:你贴的那张聊天室图片 是怎么创建的,没有弄清楚是怎么一个流程,
    rwy:@anyesu 但是我在tomcat运行 地址栏就是http://desktop-2690269:8080/ 就会报404的错误
    anyesu:@rwy 这个demo就是多人的聊天室
  • 听见下雨丶声音:tomcat版本那个demo是怎么访问的?
  • 20f72fe8d87b:不错,公司正在调研如果使用websocket实现web和app间的实时聊天。不过现在spring有对websocket的整合技术,不知道楼主有没有研究过
    念残泊宇: @cumtbao 这个没问题,我用的长轮询实现web与app之间的通讯。
    猿气十足: @cumtbao 可以实现
    anyesu:@cumtbao 很久没继续深入研究了,spring的整合方式倒还真没试过:sweat_smile:
  • 09c96b247b8a:楼主你好,我试用了你的WEBRTC那个DEMO,效果不错啊,就是想局域网内别的主机访问这个DEMO,该怎么操作啊
    anyesu:@aa5566xzy 如果你是想简单的把web项目和websocket结合在一起的话,可以使用我那个tomcat版本的demo,里面已经配置好了@ServerEndpoint注解的configurator属性,之后就可以通过 (HttpSession) config.getUserProperties().get(HttpSession.class.getName())的方式获取web项目的session,这样web项目把登录信息保存在session中,websocket可以从session中获取登录信息。如果是跨项目的话,可以考虑把登录信息保存到memcached或redis之类的缓存中,将key保存在cookie中,在websocket项目中根据key从缓存中取出数据即可。
    09c96b247b8a:@anyesu 明白了,如果我用TOMCAT运行的网页端如何能调用到这边的NODEJS服务运行的视频服务,直接打开网址的话,那TOMCAT那边的人的信息咋传进来呢
    anyesu:@aa5566xzy 直接使用局域网ip和端口访问就行了,访问不了可能是被防火墙拦截了
  • a77118d36d9b:请问,音视频是怎么支持的
    anyesu:@decodelife 多谢支持!音视频可以参考 http://www.jianshu.com/p/03a74d489f34 这篇文章,音频通过定时录音并发送实现的,视频则是将摄像头画面定时发送来实现,因为只是简单的处理,性能上一般,对网络带宽有一定要求,WebRTC已经有现成的功能的了,性能也比较不错,可以参考这篇文章 http://www.jianshu.com/p/63d4d15280f4。
  • a77118d36d9b:不错,支持下
  • edcb9f1dafba:楼主,@OnError 调用这个方法的情景有:设置session超时(如:session.getAsyncRemote().setSendTimeout(5))客户端与服务器连接,在连接界面,突然断网,过一会就会调用这个方法。但是现在的问题是,设置的时间和实际调用这个方法的用时对不上,并找不出规律。能不能帮我看看,你那边也会有这种情况吗?
    anyesu:@jeniss 这几篇文章写完之后就没怎么继续深入研究了,对于OnError方法的触发情景我也没怎么去了解过,比较惭愧,不过我测试了下以下两种情况:
    1)服务端收到消息后,服务端断开网络,服务端再继续向客户端发送消息会立刻触发OnError方法。
    2)服务端收到消息后,客户端断开网络,服务端再继续向客户端发送消息会过一段时间触发OnError方法,这个时间间隔稳定在18.9秒左右(应该是有一个默认值的),说明sendTimeout并未生效。
    ps:有精力的话可以去tomcat目录的websocket包下看源码具体怎么实现的。
  • c21c952f9f05:可以的
  • 896155ecc2c0:楼主,能否发一份demo给我,没用过websocket,项目需要,网上资料太少!完全不懂如何入手,方便的话可否详细普及下,iOS小白!xiaosima1024@163.com
    谢谢
    anyesu:@听见下雨丶声音 比如我的端口是8080,项目路径是ROOT,那么就访问http://127.0.0.1:8080/ws/main.html。当然前提是你已经配置好了
    听见下雨丶声音:@anyesu 亲 我下载你的tomcat demo 启动了没法访问啊 是什么情况 我没访问对吗
    anyesu:@不罕有便乌有 可以看下我的另外两篇文章,虽然介绍的也不是很详细。。代码我上传到github上了https://github.com/anyesu/websocket

本文标题:基于WebSocket的在线聊天室(一)

本文链接:https://www.haomeiwen.com/subject/rzvcrttx.html