服务之Socket及Websocket介绍
现在我们编写Web应用或者基于RPC应用,其实说白了,都是基于socket的编程。本文就是记录一下自己直接用Go的net包基于底层socket进行实践,编写了服务端和客户端。服务端采用协程,即启动多个goroutine,思想其实和net/http包里面的Server一致:对于每一个新的客户端连接,都会新开一个goroutine,而不影响其他的连接。
上代码吧:
Server:
func handleconn(conn net.Conn) {
//defer conn.Close()
fmt.Println(conn.RemoteAddr().String())
ch := make(chan bool)
//rch := make(chan bool)
//读取客户端传送的消息
go func() {
fmt.Println("enter go func")
now := time.Now().String()
conn.Write([]byte(now))
fmt.Println("func write complete")
ch <- true
}()
<- ch
go func() {
buf := make([]byte, 4)
n, _ := conn.Read(buf)
fmt.Println(buf[:n])
fmt.Println("func read complete")
ch <- true
}()
<- ch
fmt.Println("read and write finished both")
}
func main() {
socket,err := net.Listen("tcp",":8000")
if err != nil {
fmt.Println(err)
}
for {
conn,err := socket.Accept()
if err != nil {
fmt.Println(err)
continue
}
go handleconn(conn)
}
}
上述服务端代码已经写了注释,解释了基本设计逻辑。
CLIENT:
package main
import (
"fmt"
"net"
"io/ioutil"
"os"
)
func CheckError(err error) {
if err != nil{
fmt.Println("err:",err)
os.Exit(1)
}
}
func main() {
sockaddr,err := net.ResolveTCPAddr("tcp4","localhost:8000")
CheckError(err)
//dial tcp,请求建立和目标服务器的Socket连接。
conn,err := net.DialTCP("tcp",nil,sockaddr)
CheckError(err)
//读取服务器通过已经建立的连接通道发送的数据,读出的是字节流数据
result,err := ioutil.ReadAll(conn)
//将byte转换为字符串
fmt.Println("from server message:",string(result))
os.Exit(0)
}
基本的业务逻辑还是挺简单的,具体的一些业务逻辑可以在这个基础上进行添加。
补充一点:尽量不要用ioutil.ReadAll,该方法是要等到err或者EOF才会返回结果,即一直阻塞,今天做的可把我坑惨了。可以用buf:
buf := make([]byte,1024)
n, _ := conn.Read(buf)
fmt.Println(buf[:n])
Websocket介绍
目前,在socket基础上衍生出了一个WebSocket,其基本思想是每一个Web客户端和服务器建立一个TCP连接,所有的数据通信都走这一个连接通道。
它的出现主要是解决什么问题呢?
我们都知道,对于普通的Web,都是采用HTTP协议,如果我们希望能够实现及时通信,就需要客户端不断地向服务器发送HTTP请求,你有没有新数据,有没有数据,有没有数据。这种经常性地发送HTTP请求势必造成了资源带宽的浪费。而且这也会造成同步的延迟。
上面的问题,通过WebSocket就可以解决。有了Websocket,那客户端和服务端建立了唯一的连接,然后服务端会主动把消息发给服务端。即你建立好之后,客户端就是少女,服务端就是个饥渴男,我有消息,我有消息,我有消息,而不用客户端自己再主动询问。
当然,Websocket同样也需要先进行一次HTTP连接,然后再使用WebSocket进行通信。
在网上找的一个Websocket连接建立的过程示意图:
一个TCP流:
GET /ws/chat/haibo/dhdwdwdnwdindo/ HTTP/1.1
Host: 127.0.0.1:8000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36
Upgrade: websocket
Origin: http://127.0.0.1:8000
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: csrftoken=0uzZVr3DkV2JykWkdXJsrTyKb5L5chnLGKD0zIwdHGeH5P2X3x0h2ThnpM0Jkw0O; remember_admin_59ba36addc2b2f9401580f014c7f58ea4e30989d=eyJpdiI6IkRDSDd2SmpcL1lINEgwWUJZclpRd0FBPT0iLCJ2YWx1ZSI6Ik9qVHBOQ21NV1wvamdsVTNaaHo1c05JRUhKNTBVNk0rSXMrMHJ2ZE1iOExESXJpVGM1WDB1OUI4aHlRUkl1VTkrMDUrTHNYMDF5WkY2ZVNURE54YkxvalFsa1ZrbllmaHhxREQrXC9UZU5tcmtiY1R2ak5qVEJSdkFzVzFQRXk4MTdXZHJCVFVlOHAyTnREc0NncDFWTWNXOGFMbFQ1amJrSXluQTlwM1FQRGhZPSIsIm1hYyI6ImE5MTRiNDM1MzUwYzI4YzRjOTNkNDNhM2NiNGE5YTU5MmZhNjNiZTRiY2MzOTQ3YTAwMDkxMzE3MDFjYTAzODgifQ%3D%3D; XSRF-TOKEN=eyJpdiI6Im1YYjZvYTNOaytlaDJyTUlrZitta1E9PSIsInZhbHVlIjoiNldJN0RHRXNjV2hUTjE4KytocGk5UWd5OHdwdjhkQ01XR1wvUWVENHFxMGRaVlRRaDl0aWpIblJDeXpFaVlmSE5xM1FlTm9PUmx3OG1TWEVHUmcweU5BPT0iLCJtYWMiOiI3NjQ1Y2RkOGJmNmYzNzYzNzA4YTBmOWJhOTZhOGEzMjMxZTc1YzgzZjk5Mzg2N2Q4ZDZjYzY5MWNmMDNiNjJiIn0%3D; youpin_team_session=eyJpdiI6IlJOSXdqaUk2bEJFZElqSGcycVRrVkE9PSIsInZhbHVlIjoiRURtYjhIMVoxQkZGcjF6ZW5ZVHRia1JrOWFUcVNnY1Q4K1lFRFZvNTY3UGlENnVBMVFVNzVMdDYyRHhTRk8wbHowZmk1SGJDZVRlVXRMYnA5aVZcL3B3PT0iLCJtYWMiOiJmOTdhYzJlNmE2MTE0MDNjZTVlNTY0NDVjZmUyNDJkNmJiOTNlNmE3NjRjNjczMWNmMWY2MzFmODE2MjQ1NGM3In0%3D
Sec-WebSocket-Key: ++EcxfMKJZv5ZPzm/Ah0+Q==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
HTTP/1.1 101 Switching Protocols
Server: Daphne
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: MDda/nVP5hymfRIxrEqsDeTXFTA=
.......9...h...9...z...f..{"message": "hello\uff1ahaha"}."{"message": "hello\uff1ahahatest"}..4WZj....HB!..(..un7g....;.......z.4SyV....
我用Python和LayIM实现了基本客服功能,主要也借鉴了网上几个不错的项目,综合了几个优秀的,开发了一个。先上图:
用户客服咨询:
实现WebIM,有几个点:
1、前端,websocket和layIM,后端Server的一个交互;
2、后端Websocket Server的处理。
layIM目前并不开源,所以我弄的就是用来学习的,不参与任何的商业。看官网还是比较清晰的。不过,我本身不是前端出身,刚开始也弄了半天。下面是前端实现的一个代码,基本的客服模式。
主要是建立Websocket,并设定每个onMethod方法。
<script>
//演示代码
layui.use('layim', function(layim){
var layim = layui.layim;
var curuser = $("#barrage").html();
layim.config({
init: {
//配置客户信息
mine: {
"username": "访客" //我的昵称
,"id": curuser //我的ID
,"status": "online" //在线状态 online:在线、hide:隐身
,"remark": "在深邃的编码世界,做一枚轻盈的纸飞机" //我的签名
,"avatar": "//res.layui.com/images/fly/avatar/00.jpg" //我的头像
}
}
//开启客服模式
,brief: true
});
//监听发送消息
layim.on('sendMessage', function (data) {
//接收消息人员信息
var To = data.to;
var mine = data.mine;
if (To.type === 'friend') {
layim.setChatStatus('<span style="color:#FF5722;">对方正在输入。。。</span>');
}
var msg = JSON.stringify(data);
//保存消息
socket.send(msg);
});
layim.chat({
name: '在线客服' //名称
,type: 'friend' //聊天类型
,avatar: 'http://tp1.sinaimg.cn/5619439268/180/40030060651/1' //头像
,id: 9999 //定义唯一的id方便你处理信息
});
var socket = null;
//连接websocket的ip地址
//动态修改查
var user = $("#barrage").html();
var im = {
init: function () {
if ('WebSocket' in window){
socket = new WebSocket( 'ws://' + window.location.host + '/ws/chat/haibo/' + {{user}} + "/");
im.startListener();
}
else if ('MozWebSocket' in window){
//ws = new MozWebSocket("ws://www.isuyu.cn:8086/socketServer/"+$("#username").val());
socket = new MozWebSocket('ws://' + window.location.host + '/ws/chat/haibo/' + user + "/");
}
},
startListener: function () {
if (socket) {
// 连接发生错误的回调方法
socket.onerror = function () {
console.log("通讯连接失败!");
};
// 连接成功建立的回调方法
socket.onopen = function (event) {
console.log("通讯连接成功");
}
// 接收到消息的回调方法
socket.onmessage = function (event) {
console.log("通讯接收到消息");
im.handleMessage(event.data);
}
// 连接关闭的回调方法
socket.onclose = function () {
console.log("通讯关闭连接!!");
}
}
},
handleMessage: function (msg) {
msg = JSON.parse(msg);
//如果是群消息,转换一下id,返回群id的的字段必须是id
if (msg.type == "group") {
var temId = msg.id;
msg.id = msg.toId;
msg.toId = temId;
}
//console.log(msg)
layim.getMessage(msg);
}
};
im.init();
});
上面代码,仔细看还是比较简单的,没有复杂的逻辑。就是Websocket和layim相互实现函数调用。
接下来就是和后端的交互了。每一种语言都有实现好的框架。因为我的hbnnmall是python构建,所以我就用了Django的channels。springboot也有好用的jar包,都过注解可以实现。其实思想都差不多。
class CustomerService(AsyncWebsocketConsumer):
async def connect(self):
self.room_group_name = self.scope['url_route']['kwargs']['room_name']
self.token = self.scope['url_route']['kwargs']['token']
#TODO 获取uid
self.uid = getTempUidByToken(self.token)
users[ self.uid] = self.channel_name
print(self.channel_name)
print(users)
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# await self.sendMessage(self.channel_name, "欢迎访问")
if self.uid == ADMIN_USER:
await self.sendMessage(users.get(ADMIN_USER), "欢迎管理员",ADMIN_USER)
#TODO check所有未被消费的消息,都要发送给管理员
# MessageProcessor.sendAllUnReadMsg(self.channel_name)
else:
await self.sendMessage(users.get(self.uid), "欢迎".format(self.uid))
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
async def receive(self, text_data):
print(users)
print("get data:" + text_data)
text_data_json = json.loads(text_data)
if "data" in text_data_json:
mine = text_data_json["data"]["mine"]
else:
mine = text_data_json["mine"]
message = mine['content'] if 'content' in mine else mine['message']
from_id = int(mine["id"])
# Send message to room group
# await self.channel_layer.group_send(
# self.room_group_name,
# {
# 'type': 'chat_message',
# 'message': "您好,{},您的消息:{} 我已收到".format(from_id,message)
# ,"toId": ADMIN_USER
# }
# )
if from_id != ADMIN_USER:
await self.sendMessage(users.get(from_id), "您好,{},您的消息:{} 我已收到".format(from_id, message), toId=ADMIN_USER)
#转发给目的地
if "data" in text_data_json:
target = text_data_json["data"]["to"]
else:
target = text_data_json["to"]
target_id = int(target["id"])
if target_id in users:
print("send msg from {} to {}".format(from_id,target_id))
await self.sendMessage(users.get(int(target["id"])), message,from_id)
else:
#发送消息到消息Redis中,当管理员上线后,消息需要发送给管理员
await self.sendMessage(users.get(from_id), "对方暂时不在线哦!")
# Receive message from room group
async def chat_message(self, event):
message = event['message']
print(event.keys())
uid = event['toId']
data = {
'message': message,
"mine": False,
'content': message,
"type": "friend",
"username": uid,
"avatar": "http://tp1.sinaimg.cn/1571889140/180/40030060651/1"
, "id": int(uid)
, "fromid": int(uid)
, "timestamp": 1467475443306
}
print(data)
# Send message to WebSocket
await self.send(text_data=json.dumps(data))
async def sendMessage(self,channel,msg,toId=None):
data = {'type': 'chat_message', 'message': msg, "toId": toId if toId is not None else ADMIN_USER}
print("data:{}".format(data))
await self.channel_layer.send(
channel,
data
)
def getUsers(self):
return users
参考资料:
微信分享/微信扫码阅读