前言
什么是IM(即时通讯)?
即时通信(instant message,IM)是指能够即时发送和接收互联网消息等的业务,通常集成了电子邮件、博客、音乐、电视、游戏和搜索等多种功能。
即时通信已经发展成集交流、资讯、娱乐、搜索、电子商务、办公协作和企业客户服务等为一体的综合化信息平台。微软、腾讯、AOL、Yahoo等重要即时通信提供商都提供通过手机接入互联网即时通信的业务,国内最常用的即时通讯软件如QQ、微信、百度hi、网易泡泡、淘宝旺旺等等。用户可以通过手机与其他已经安装了相应客户端软件的手机或电脑收发消息。即时通信不再是一个单纯的聊天工具,它已经发展成集交流、资讯、娱乐、搜索、电子商务、办公协作和企业客户服务等为一体的综合化信息平台。
网络通信的三大要素
- ip
- port
- 协议
常用的网络通讯协议
-
TCP/IP:即传输控制协议/网间协议,定义了主机如何连入因特网及数据如何再它们之间传输的标准
-
TCP UDP :TCP和UDP是传输层协议。
-
TCP:面向连接,安全可靠,效率稍低。通过三次握手确保连接的建立。
-
UDP:面向无连接。不可靠。速度快。将数据封包传输,数据包最大64k。
-
Socket:又称“套接字”, 在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
-
HTTP:超文本传输协议, 是一种规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送应用层协议。
IM系统角色分析
用户
系统的使用者,主要有以下特点:
- 用户信息:如账号,昵称,性别,头像,在线时长等
- 关系链:是指用户与用户之间的关系,通常有单向的好友关系、双向的好友关系、关注关系等
- 用户状态:当前是在线、离线还是挂起等状态
消息
用户之间的传播的内容。其特点如下:
- 消息类型:消息通常分为文本消息、表情消息、图片消息、视频消息、文件消息等
- 安全性:是否需要加密以及采用何种加密算法
- 消息状态:当前消息对方已读还是未读
- 消息数量:主要是指用户还没读的消息数
会话
指用户之间因聊天而建立起的通讯线路,其特点如下:
-
会话协议:
在通信协议的选择上,我们主要有以下几个选择:
- 使用 TCP Socket 通信,自己设计协议:58 到家等等
- 使用 UDP Socket 通信:QQ 等等
- 使用 HTTP 长轮循:微信网页版等等
-
会话类型:单聊或者群聊,若是群聊还要考虑群组的大小等问题
-
会话状态:若单向发起的会话,则将消息置为离线状态,否则实时发送
服务器
服务器是会话建立的桥梁,也是消息收发的中转站,其特点如下:
- 会话管理:保存已建立的会话和销毁已失效的会话
- 消息传递:将一个用户的消息转发给指定的其它用户
- 消息队列:实现消息的广播和离线发送
- 用户管理:用户登陆与验证,用户关系的查询等
客户端
消息的接收端,负责和服务端建立连接,并对消息进行编码和解码。
IM系统功能分析
IM系统的实现目标
1.实时性:保证消息实时触达是互动场景的必备能力。
对于一个实时消息系统,“实时”二字很好地表达了这个系统的基本要求。通过微信和你的好友聊天,结果等半天对方才收到,基本上也没有意愿聊了;直播场景下,如果主播的互动消息房间里的粉丝要等很长时间才能收到,也很难让粉丝们有积极参与的欲望。实时性分为:短轮询,长轮询,WebSocket(长链接)。
2.可靠性:“不丢消息”和“消息不重复”是系统值得信赖的前置条件。
如果说“实时性”是即时消息被广泛应用于各种社交、互动领域的基本前置条件,那么消息的可靠性则是实时消息服务可以“被信赖”的另一个重要特性。这里的可靠性通俗来讲,一般包括两个方面。不丢消息。“丢消息”是互动中让人难以接受的
Bug,某些场景下可能导致业务可用性差,甚至不可用的情况。比如直播间“全员禁言”的信令消息丢失,就可能导致直播室不可控的一些情况。消息不重复。消息重复不仅会对用户造成不必要的骚扰和困惑,可能还会导致比较严重的业务异常,比如直播间“送礼物”的消息由于某种原因被重复发出,处理不妥的话可能会导致用户损失。
3.一致性:“多用户”“多终端”的一致性体验能大幅提升 IM 系统的使用体验 。
消息的一致性一般来是指:同一条消息,在多人、多终端需要保证展现顺序的一致性。比如,对于单聊场景,一致性是指希望发送方的消息发送顺序和接收方的接收顺序保持一致;而对于一个群的某一条消息,我们希望群里其他人接收到的消息顺序都是一致的;对于同一个用户的多台终端设备,我们希望发送给这个用户的消息在多台设备上也能保持一致性。缺少“一致性”保障的
IM
系统,经常会导致双方沟通过程中出现一些“奇妙的误会”,语言乱序相关的“惨案”。网络上,你可以想象一下发给下属、领导或合作方的几条重要工作内容,如果消息错乱了,后果可能会比较严重。
4.安全性:“数据传输安全”“数据存储安全”“消息内容安全“三大保障方面提供全面隐私保护。
由于即时消息被广泛应用于各种私密社交和小范围圈子社交,因此用户对于系统的隐私保护能力要求也相对较高。从系统使用安全性的角度来看,首先是要求“数据传输安全”,其次是要求“数据存储安全”,最后就是“消息内容安全”。
IM系统核心功能
IM系统中最核心的部分是消息系统,主要分为以下三点:
- 消息同步:指将消息完整的、快速的从发送方传递到接收方。消息同步系统最重要的衡量指标就是消息传递的实时性、完整性以及能支撑的消息规模。在功能上,至少要支持在线和离线推送,有些IM系统还支持多端同步。
- 消息存储:指消息的持久化保存。传统消息系统通常只能支持消息在接收端的本地存储,数据基本不具备可靠性。现代消息系统能支持消息在服务端的在线存储,功能上对应的就是消息漫游,消息漫游的好处是可以实现账号在任意端登录查看所有历史消息。
- 消息检索:消息一般是文本,所以支持全文检索也是必备的能力之一。传统消息系统通常来说也是只能支持消息的本地检索,基于本地存储的消息数据来构建。而现在消息系统在能支持消息的在线存储后,也具备了消息的在线检索能力。
IM系统执行流程
主要分为以下几个步骤:
-
用户A输入自己的用户名和密码登录IM服务器,服务器通过读取用户数据库来验证用户身份,如果验证通过,登记用户A的IP地址、IM客户端软件的版本号及使用的TCP/UDP端口号,然后返回用户A登录成功的标志,此时用户A在IM系统中的状态为在线。
-
根据用户A存储在IM服务器上的好友列表 ,服务器将用户A在线的相关信息发送给也同时在线的IM好友的PC机,这些信息包括在线状态、IP地址、IM客户端使用的TCP端口(Port)号等,IM好友的客户端收到此信息后将在予以提示。
-
IM服务器把用户A存储在服务器上的好友列表及相关信息回送到他的客户端机,这些信息包括也在线状态、IP地址、IM客户端使用的TCP端口(Port)号等信息,用户A的IM客户端收到后将显示这些好友列表及其在线状态。
基于websocket的通讯系统
用户对象代码:
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
private String mobile;
}
消息对象代码:
@Data
@ToString
public class Message {
private String id;
private String msg;
private String from;
private String to;
private String live;
}
服务端代码:
@Component
@ServerEndpoint(value = "/websocket")
@Slf4j
public class Socket {
public static Map<String, Session> sessionMap = new HashMap<String, Session>();
@Autowired
UserService userService;
private Session session;
@Resource
private RedisTemplate redisTemplate;
@OnOpen
public void startSocket(Session session) {
this.session = session;
log.debug("链接成功");
if (sessionMap.size() == 0) {
return;
}
}
@OnMessage
public void getMessgae(Session session, String str, boolean last) {
if (session.isOpen()) {
try {
log.debug(str);
Message msg = JsonUtils.jsonToPojo(str, Message.class);
Message toMessage = msg;
toMessage.setFrom(msg.getId());
toMessage.setTo(msg.getTo());
//开启socket链接时msg的值是newUser
if (msg.getMsg().equals("newUser")) {
//如果存在这个用户
if (sessionMap.containsKey(msg.getId())) {
//删除掉防止重复(如果更换了电脑或者浏览器。这个操作能保证session与id是唯一对应的且session是最新的)
sessionMap.remove(msg.getId());
}
//将用户id放进去
sessionMap.put(msg.getId(), session);
//发送在线人数
this.pubMessage(session);
} else {
Session toSession = sessionMap.get(msg.getTo());
if (toSession != null && toSession.isOpen()) {
toSession.getBasicRemote().sendText(JsonUtils.objectToJson(toMessage).toString(), last);
//发送在线人数
this.pubMessage(toSession);
} else {
toMessage.setMsg("用户不存在");
toMessage.setFrom("系统");
session.getBasicRemote().sendText(JsonUtils.objectToJson(toMessage).toString(), last);
}
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
log.debug("session is closed");
}
}
private void pubMessage(Session session) throws IOException {
Set userIds = sessionMap.keySet();
StringBuffer sBuffer = new StringBuffer();
for (Object str1 : userIds) {
sBuffer.append(str1.toString() + ",");
}
Message message = new Message();
message.setLive(sBuffer.toString());
session.getBasicRemote().sendText(JsonUtils.objectToJson(message), true);
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set("com.aliencat", 111);
operations.set("com.aliencat.application", 1, 1, TimeUnit.SECONDS);
}
}
客户端代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebChat使用示例</title>
<script src="js/jquery-1.11.3.js" type="text/javascript"></script>
<script src="js/webchat.js" type="text/javascript"></script>
<link href="css/index.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div class="liveDiv">
<p>当前在线用户</p>
<span class="liveSpan">
</span>
</div>
<div class="container">
<div class="item">
<span>称呼:</span>
<span class="myname_span"></span>
<span style="margin-left: 50px">联系人:</span>
<span class="to-user"></span>
</div>
<div class="show-message">
</div>
<div class="item">
<p></p>
<textarea class="msg-context" placeholder="请输入:"></textarea>
</div>
<button class="send-btn" onclick="send()">发送</button>
</div>
<script>
$(function () {
var item = sessionStorage.getItem("user");
var parse = JSON.parse(item);
$('.myname_span').html(parse.username)
console.log(parse)
connection(parse.username);
})
</script>
</body>
</html>