【详】socketIO与webSocket

补习

先来补习几个月的前某天整理的一些TCP方面的知识

长连接与短连接

TCP协议中有长连接和短连接之分。

众所周知,在网络通信中客户端与服务端建立TCP连接有三次握手和四次挥手,每个连接的建立都是需要资源消耗和时间消耗的

短连接是指通信双方一有数据交互,就建立一个TCP连接,数据发送完后,就断开此TCP连接;

长连接是指一次TCP连接后可以连续发送多个数据包,TCP保持期间,如果没有数据包发送,需要双方检测包以维持此链接:

连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接(一个TCP连接通道多个读写通信)


短连接环境下,数据交互完毕后,主动释放连接;

长连接的环境下,进行一次数据交互后,很长一段时间内无数据交互时,客户端可能意外断电、死机、崩溃、重启,还是中间路由网络无故断开,这些TCP连接并未来得及正常释放,那么,连接的另一方并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,且有可能导致在一个无效的数据链路层面发送业务数据,结果就是发送失败。所以服务器端要做到快速感知失败,减少无效链接操作,这就有了TCP的Keepalive(保活探测)机制。

保活机制原理

一端会启动一个计时器,当计时器到达0之后(达到tcp_keepalive_time),一个TCP探测包会被发出,这个探测包是一个纯ACK包(不应该包含任何数据),其seq号与上一个包是重复的

如果一个给定的连接在(默认)两个小时内没有任何动作,则服务器就发送一个探测报文段,检测客户主机的状态,有以下四种:

  1. 客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。

  2. 客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测 ,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。

  3. 客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。

  4. 客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探测的响应。

利用保活机制,当意外发生时,可以释放半打开的TCP连接

保活机制可能导致的问题

Keepalive 技术只是 TCP 技术中的一个可选项。因为不当的配置可能会引起一些问题,所以默认是关闭的。

可能导致下列问题:

  1. 在短暂的故障期间,Keepalive设置不合理时可能会因为短暂的网络波动而断开健康的TCP连接

  2. 需要消耗额外的宽带和流量

  3. 在以流量计费的互联网环境中增加了费用开销


参考:https://blog.csdn.net/skiof007/article/details/52873421

WebSocket

1、WebSocket是什么?

WebScoket是一种让客户端和服务器之间能进行双向实时通信的技术。它是HTML最新标准HTML5的一个协议规范,本质上是个基于TCP的协议,它通过HTTP/HTTPS协议发送一条特殊的请求进行握手后创建了一个TCP连接,此后浏览器/客户端和服务器之间便可以通过此连接来进行双向实时通信。

2、为什么要用WebSocket?

1)一直以来,HTTP协议是无状态、单向通信的,即客户端请求一次,服务器回复一次。如果想让服务器消息及时下发到客户端,需要采用类似于轮询的机制,即客户端定时频繁的向服务器发出请求,这样效率很低,而且HTTP数据包头本身的字节量较大,浪费了大量带宽和服务器资源;

2)为提高效率,出现了AJAX/Comet技术,它实现了双向通信且节省了一定带宽,但仍然需要发出请求,本质上仍然是轮询;

3)新一代HTML标准HTML5推出了WebSocket技术,它使客户端和服务器之间能通过HTTP协议建立TCP连接,之后便可以随时随地进行双向通信,且交换的数据包头信息量很小;

3、如何使用WebSocket?

在支持WebSocket的浏览器中,创建Socket之后,通过onopen、onmessage、onclose、onerror四个事件的实现来处理Socket的响应;

4、WebSocket与HTTP、TCP的关系

WebSocket和HTTP都属于应用层协议,且都是基于TCP的,它们的send函数最终也是通过TCP系统接口来做数据传输。那么WebSocket和HTTP的关系呢?WebSocket在建立握手连接时,数据是通过HTTP协议传输的,但是在连接建立后,真正的数据传输阶段则不需要HTTP协议的参与。它们之间的关系如下图:

img

5、什么情况下使用WebSocket?

如果需要同时支持手机端、Web端,那毫无疑问应该使用WebSocket,现在各个平台都提供了相应的WebSocket实现。如果游戏不需要支持Web端,且对实时性要求比较高,那么使用TCP/UDP结合的原生Socket会比较好。

6、SocketIO

WebSocket是HTML5最新提出的规范,虽然主流浏览器都已经支持,但仍然可能有不兼容的情况,为了兼容所有浏览器,给程序员提供一致的编程体验,SocketIO将WebSocket、AJAX和其它的通信方式全部封装成了统一的通信接口,也就是说,我们在使用SocketIO时,不用担心兼容问题,底层会自动选用最佳的通信方式。因此说,WebSocket是SocketIO的一个子集。

参考:https://www.cnblogs.com/foupwang/p/7865694.html

如何使用

WebSocket

依赖:

1
2
3
4
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Component
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}

WebSocketServer:

因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller
直接@ServerEndpoint(“/websocket”)@Component启用即可,然后在里面实现@OnOpen,@onClose,@onMessage等方法

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
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import lombok.extern.slf4j.Slf4j;


@ServerEndpoint("/websocket/{sid}")
@Component
public class WebSocketServer {

static Log log=LogFactory.get(WebSocketServer.class);
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();

//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;

//接收sid
private String sid="";
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session,@PathParam("sid") String sid) {
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
log.info("有新窗口开始监听:"+sid+",当前在线人数为" + getOnlineCount());
this.sid=sid;
try {
sendMessage("连接成功");
} catch (IOException e) {
log.error("websocket IO异常");
}
}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
}

/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到来自窗口"+sid+"的信息:"+message);
//群发消息
for (WebSocketServer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}


/**
* 群发自定义消息
* */
public static void sendInfo(String message,@PathParam("sid") String sid) throws IOException {
log.info("推送消息到窗口"+sid+",推送内容:"+message);
for (WebSocketServer item : webSocketSet) {
try {
//这里可以设定只推送给这个sid的,为null则全部推送
if(sid==null) {
item.sendMessage(message);
}else if(item.sid.equals(sid)){
item.sendMessage(message);
}
} catch (IOException e) {
continue;
}
}
}

public static synchronized int getOnlineCount() {
return onlineCount;
}

public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}

public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}

参考:https://blog.csdn.net/moshowgame/article/details/80275084

SocketIO

准确的说是netty-socketio,socketIO的java版,我会着重来讲一下它,先撸代码:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.12</version>
</dependency>
<dependency>
<groupId>io.socket</groupId>
<artifactId>socket.io-client</artifactId>
<version>1.0.0</version>
</dependency>
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
package com.miao.socketio.config;

import com.alibaba.fastjson.JSONObject;
import com.corundumstudio.socketio.*;
import com.corundumstudio.socketio.listener.ConnectListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;

/**
* @author wyy
* @date 2019/9/10 15:25
*/
@org.springframework.context.annotation.Configuration
public class SocketIOConfig {

private Logger logger = LoggerFactory.getLogger(SocketIOConfig.class);

@Bean
public SocketIOServer initSocketIO() {
// 注意是com.corundumstudio.socketio.Configuration
Configuration config = getConfiguration();
SocketIOServer server = new SocketIOServer(config);
server.addConnectListener(client -> {
if (null != client) {
logger.info("连接成功");
// 可给客户端发送事件,参数为事件名及内容
client.sendEvent("testEvent", JSONObject.toJSON("balabala..."));
} else {
logger.warn("没有连接");
}
});
return server;
}

private Configuration getConfiguration() {
Configuration configuration = new Configuration();

// 可不填,默认是0.0.0.0
configuration.setHostname("localhost");
// 端口号
configuration.setPort(8099);
// Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
configuration.setPingTimeout(5000);
// Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
configuration.setPingInterval(10000);

configuration.setMaxFramePayloadLength(1024 *1024);
configuration.setMaxHttpContentLength(1024 *1024);

// 每一次握手时的监听器
// configuration.setAuthorizationListener(data -> true);

return configuration;
}
}

sendEvent是我从前人老代码里看到的最多的一个用法,业务场景是从Netty长链里拿到的实时信息(不知道这样描述是否妥当,Netty之后要好好在学习学习),用发送事件的方式传给前端。

他SocketIOServer这的操作我认为有一些繁琐,他的做法是首先让客户端主动订阅事件,服务端用事件监听器取到client,并把SocketIOClient直接存在内存中的Set集合里,当有从另一个长链拿到消息时又给到每一个SocketIOClient去发event。之所以说这样繁琐是socketIO是一个天然的“聊天室”,当我拿到消息后我可以直接发送给某个房间内所有的client,主要讲怎么使用吧:

先要了解两个概念:

Namespace

套接字,客户端默认连接的namespace为/,服务端也默认监听/,但如果指定了namespace,则默认下发给/的信息就收不到了

Rooms

每个Namespace下都可以定义房间来供客户端加入与离开,如果没有指定会有一个默认的room,上面的例子里应该就是给默认的room里所有的client都发事件,一个client可以连不同的room,类似于一个账号能进很多个群。

1
2
3
4
5
6
7
8
9
// 可以获取Namespace下所有的客户端
server.getNamespace("/").getAllClients();
// 可以给指定room里的客户端发事件
server.getRoomOperations("testRoom").sendEvent("testRoomOne",
JSONObject.toJSON("RoomOne...RoomOne...RoomOne..."));
// 客户端加入指定的房间
client.joinRoom("testRoom");
// 离开
client.leaveRoom("testRoom");

两者区别

写到最后忘记总结这俩的区别了,基于谷歌和百度的小码农找了一个精炼又赞的答案:

websocket是一种长连接协议,用nodejs实现了这个ws协议的库也叫websocket,github搜索一下就有。socket.io也是实现了ws协议的库,不过它支持的更多,不仅实现了ws协议,也支持长轮询等方式,兼容flash,IE6等不支持ws协议的浏览器。

——v站 halfblood的回答